From f39042654625f0ff86a18e39337b9d35596d6c9a Mon Sep 17 00:00:00 2001 From: root Date: Mon, 16 Mar 2026 08:42:57 +0000 Subject: [PATCH] =?UTF-8?q?=D0=9E=D0=B1=D0=BD=D0=BE=D0=B2=D0=BB=D0=B5?= =?UTF-8?q?=D0=BD=D0=B8=D0=B5=20=D0=BA=D0=BB=D0=B8=D0=B5=D0=BD=D1=82=D0=B0?= =?UTF-8?q?=20(apps,=203rdparty,=20install)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .gitignore | 2 +- .htaccess | 2 +- .../mp3info-break-frame-parsing.patch | 26 + ...fix-incorrect-lookup-for-mpeg-header.patch | 33 + 3rdparty/LICENSE INFO | 26 + 3rdparty/autoload.php | 22 + 3rdparty/aws/aws-crt-php/LICENSE | 175 + 3rdparty/aws/aws-crt-php/NOTICE | 1 + .../src/AWS/CRT/Auth/AwsCredentials.php | 69 + .../src/AWS/CRT/Auth/CredentialsProvider.php | 23 + .../aws-crt-php/src/AWS/CRT/Auth/Signable.php | 43 + .../src/AWS/CRT/Auth/SignatureType.php | 15 + .../src/AWS/CRT/Auth/SignedBodyHeaderType.php | 11 + .../aws-crt-php/src/AWS/CRT/Auth/Signing.php | 22 + .../src/AWS/CRT/Auth/SigningAlgorithm.php | 11 + .../src/AWS/CRT/Auth/SigningConfigAWS.php | 75 + .../src/AWS/CRT/Auth/SigningResult.php | 33 + .../CRT/Auth/StaticCredentialsProvider.php | 35 + 3rdparty/aws/aws-crt-php/src/AWS/CRT/CRT.php | 358 + .../aws-crt-php/src/AWS/CRT/HTTP/Headers.php | 50 + .../aws-crt-php/src/AWS/CRT/HTTP/Message.php | 95 + .../aws-crt-php/src/AWS/CRT/HTTP/Request.php | 32 + .../aws-crt-php/src/AWS/CRT/HTTP/Response.php | 27 + .../src/AWS/CRT/IO/EventLoopGroup.php | 39 + .../src/AWS/CRT/IO/InputStream.php | 50 + .../src/AWS/CRT/Internal/Encoding.php | 37 + .../src/AWS/CRT/Internal/Extension.php | 29 + 3rdparty/aws/aws-crt-php/src/AWS/CRT/Log.php | 47 + .../src/AWS/CRT/NativeResource.php | 42 + .../aws/aws-crt-php/src/AWS/CRT/Options.php | 77 + 3rdparty/aws/aws-sdk-php/CRT_INSTRUCTIONS.md | 4 + 3rdparty/aws/aws-sdk-php/LICENSE | 141 + 3rdparty/aws/aws-sdk-php/NOTICE | 17 + 3rdparty/aws/aws-sdk-php/THIRD-PARTY-LICENSES | 84 + .../src/AbstractConfigurationProvider.php | 157 + .../aws/aws-sdk-php/src/Api/AbstractModel.php | 89 + .../aws/aws-sdk-php/src/Api/ApiProvider.php | 244 + .../aws-sdk-php/src/Api/DateTimeResult.php | 134 + 3rdparty/aws/aws-sdk-php/src/Api/DocModel.php | 139 + .../Api/ErrorParser/AbstractErrorParser.php | 99 + .../src/Api/ErrorParser/JsonParserTrait.php | 52 + .../Api/ErrorParser/JsonRpcErrorParser.php | 47 + .../Api/ErrorParser/RestJsonErrorParser.php | 58 + .../src/Api/ErrorParser/XmlErrorParser.php | 111 + .../aws/aws-sdk-php/src/Api/ListShape.php | 35 + 3rdparty/aws/aws-sdk-php/src/Api/MapShape.php | 54 + .../aws/aws-sdk-php/src/Api/Operation.php | 158 + .../src/Api/Parser/AbstractParser.php | 46 + .../src/Api/Parser/AbstractRestParser.php | 184 + .../src/Api/Parser/Crc32ValidatingParser.php | 54 + .../Parser/DecodingEventStreamIterator.php | 347 + .../src/Api/Parser/EventParsingIterator.php | 211 + .../Api/Parser/Exception/ParserException.php | 56 + .../aws-sdk-php/src/Api/Parser/JsonParser.php | 71 + .../src/Api/Parser/JsonRpcParser.php | 82 + .../src/Api/Parser/MetadataParserTrait.php | 90 + ...kableStreamDecodingEventStreamIterator.php | 101 + .../src/Api/Parser/PayloadParserTrait.php | 61 + .../src/Api/Parser/QueryParser.php | 60 + .../src/Api/Parser/RestJsonParser.php | 49 + .../src/Api/Parser/RestXmlParser.php | 42 + .../aws-sdk-php/src/Api/Parser/XmlParser.php | 179 + .../src/Api/Serializer/Ec2ParamBuilder.php | 38 + .../src/Api/Serializer/JsonBody.php | 108 + .../src/Api/Serializer/JsonRpcSerializer.php | 84 + .../src/Api/Serializer/QueryParamBuilder.php | 157 + .../src/Api/Serializer/QuerySerializer.php | 81 + .../src/Api/Serializer/RestJsonSerializer.php | 42 + .../src/Api/Serializer/RestSerializer.php | 330 + .../src/Api/Serializer/RestXmlSerializer.php | 48 + .../src/Api/Serializer/XmlBody.php | 220 + 3rdparty/aws/aws-sdk-php/src/Api/Service.php | 564 ++ 3rdparty/aws/aws-sdk-php/src/Api/Shape.php | 77 + 3rdparty/aws/aws-sdk-php/src/Api/ShapeMap.php | 68 + .../aws-sdk-php/src/Api/StructureShape.php | 79 + .../src/Api/SupportedProtocols.php | 26 + .../aws-sdk-php/src/Api/TimestampShape.php | 48 + .../aws/aws-sdk-php/src/Api/Validator.php | 346 + .../aws-sdk-php/src/Arn/AccessPointArn.php | 66 + .../src/Arn/AccessPointArnInterface.php | 10 + 3rdparty/aws/aws-sdk-php/src/Arn/Arn.php | 188 + .../aws/aws-sdk-php/src/Arn/ArnInterface.php | 37 + .../aws/aws-sdk-php/src/Arn/ArnParser.php | 69 + .../src/Arn/Exception/InvalidArnException.php | 7 + .../src/Arn/ObjectLambdaAccessPointArn.php | 35 + .../src/Arn/ResourceTypeAndIdTrait.php | 30 + .../aws-sdk-php/src/Arn/S3/AccessPointArn.php | 27 + .../src/Arn/S3/BucketArnInterface.php | 12 + .../src/Arn/S3/MultiRegionAccessPointArn.php | 38 + .../src/Arn/S3/OutpostsAccessPointArn.php | 110 + .../src/Arn/S3/OutpostsArnInterface.php | 12 + .../src/Arn/S3/OutpostsBucketArn.php | 99 + .../src/Auth/AuthSchemeResolver.php | 181 + .../src/Auth/AuthSchemeResolverInterface.php | 24 + .../src/Auth/AuthSelectionMiddleware.php | 106 + .../UnresolvedAuthSchemeException.php | 15 + 3rdparty/aws/aws-sdk-php/src/AwsClient.php | 793 ++ .../aws-sdk-php/src/AwsClientInterface.php | 169 + .../aws/aws-sdk-php/src/AwsClientTrait.php | 101 + .../aws/aws-sdk-php/src/CacheInterface.php | 34 + .../aws/aws-sdk-php/src/ClientResolver.php | 1412 ++++ .../AbstractMonitoringMiddleware.php | 309 + .../ApiCallAttemptMonitoringMiddleware.php | 262 + .../ApiCallMonitoringMiddleware.php | 175 + .../ClientSideMonitoring/Configuration.php | 77 + .../ConfigurationInterface.php | 44 + .../ConfigurationProvider.php | 236 + .../Exception/ConfigurationException.php | 15 + .../MonitoringMiddlewareInterface.php | 35 + 3rdparty/aws/aws-sdk-php/src/Command.php | 134 + .../aws/aws-sdk-php/src/CommandInterface.php | 42 + 3rdparty/aws/aws-sdk-php/src/CommandPool.php | 151 + .../Configuration/ConfigurationResolver.php | 250 + .../src/ConfigurationProviderInterface.php | 13 + .../AssumeRoleCredentialProvider.php | 67 + ...eRoleWithWebIdentityCredentialProvider.php | 170 + .../src/Credentials/CredentialProvider.php | 1025 +++ .../src/Credentials/CredentialSources.php | 22 + .../src/Credentials/Credentials.php | 150 + .../src/Credentials/CredentialsInterface.php | 52 + .../src/Credentials/CredentialsUtils.php | 35 + .../src/Credentials/EcsCredentialProvider.php | 262 + .../Credentials/InstanceProfileProvider.php | 468 ++ .../src/Crypto/AbstractCryptoClient.php | 121 + .../src/Crypto/AbstractCryptoClientV2.php | 119 + .../src/Crypto/AesDecryptingStream.php | 146 + .../src/Crypto/AesEncryptingStream.php | 151 + .../src/Crypto/AesGcmDecryptingStream.php | 104 + .../src/Crypto/AesGcmEncryptingStream.php | 119 + .../src/Crypto/AesStreamInterface.php | 30 + .../src/Crypto/AesStreamInterfaceV2.php | 31 + .../aws/aws-sdk-php/src/Crypto/Cipher/Cbc.php | 88 + .../src/Crypto/Cipher/CipherBuilderTrait.php | 72 + .../src/Crypto/Cipher/CipherMethod.php | 59 + .../src/Crypto/DecryptionTrait.php | 181 + .../src/Crypto/DecryptionTraitV2.php | 249 + .../src/Crypto/EncryptionTrait.php | 192 + .../src/Crypto/EncryptionTraitV2.php | 196 + .../src/Crypto/KmsMaterialsProvider.php | 121 + .../src/Crypto/KmsMaterialsProviderV2.php | 100 + .../src/Crypto/MaterialsProvider.php | 105 + .../src/Crypto/MaterialsProviderInterface.php | 61 + .../Crypto/MaterialsProviderInterfaceV2.php | 53 + .../src/Crypto/MaterialsProviderV2.php | 66 + .../src/Crypto/MetadataEnvelope.php | 61 + .../src/Crypto/MetadataStrategyInterface.php | 30 + .../src/DefaultsMode/Configuration.php | 131 + .../DefaultsMode/ConfigurationInterface.php | 51 + .../DefaultsMode/ConfigurationProvider.php | 201 + .../Exception/ConfigurationException.php | 14 + .../aws-sdk-php/src/DoctrineCacheAdapter.php | 70 + .../src/Endpoint/EndpointProvider.php | 96 + .../aws-sdk-php/src/Endpoint/Partition.php | 322 + .../Endpoint/PartitionEndpointProvider.php | 130 + .../src/Endpoint/PartitionInterface.php | 56 + .../src/Endpoint/PatternEndpointProvider.php | 51 + .../UseDualstackEndpoint/Configuration.php | 41 + .../ConfigurationInterface.php | 19 + .../ConfigurationProvider.php | 173 + .../Exception/ConfigurationException.php | 14 + .../UseFipsEndpoint/Configuration.php | 37 + .../ConfigurationInterface.php | 19 + .../UseFipsEndpoint/ConfigurationProvider.php | 179 + .../Exception/ConfigurationException.php | 14 + .../src/EndpointDiscovery/Configuration.php | 48 + .../ConfigurationInterface.php | 30 + .../ConfigurationProvider.php | 240 + .../EndpointDiscoveryMiddleware.php | 423 ++ .../src/EndpointDiscovery/EndpointList.php | 85 + .../Exception/ConfigurationException.php | 14 + .../src/EndpointParameterMiddleware.php | 89 + .../EndpointV2/EndpointDefinitionProvider.php | 71 + .../src/EndpointV2/EndpointProviderV2.php | 69 + .../src/EndpointV2/EndpointV2Middleware.php | 427 ++ .../EndpointV2/EndpointV2SerializerTrait.php | 77 + .../src/EndpointV2/Rule/AbstractRule.php | 62 + .../src/EndpointV2/Rule/EndpointRule.php | 111 + .../src/EndpointV2/Rule/ErrorRule.php | 45 + .../src/EndpointV2/Rule/RuleCreator.php | 26 + .../src/EndpointV2/Rule/TreeRule.php | 61 + .../src/EndpointV2/Ruleset/Ruleset.php | 117 + .../EndpointV2/Ruleset/RulesetEndpoint.php | 63 + .../EndpointV2/Ruleset/RulesetParameter.php | 179 + .../Ruleset/RulesetStandardLibrary.php | 434 ++ .../src/Exception/AwsException.php | 270 + .../src/Exception/CommonRuntimeException.php | 7 + .../CouldNotCreateChecksumException.php | 25 + .../src/Exception/CredentialsException.php | 11 + .../src/Exception/CryptoException.php | 11 + .../src/Exception/CryptoPolyfillException.php | 10 + .../Exception/EventStreamDataException.php | 38 + .../IncalculablePayloadException.php | 11 + .../src/Exception/InvalidJsonException.php | 11 + .../src/Exception/InvalidRegionException.php | 11 + .../Exception/MultipartUploadException.php | 63 + .../src/Exception/TokenException.php | 11 + .../src/Exception/UnresolvedApiException.php | 11 + .../Exception/UnresolvedEndpointException.php | 11 + .../UnresolvedSignatureException.php | 11 + .../src/Handler/Guzzle/GuzzleHandler.php | 89 + .../src/Handler/GuzzleV6/GuzzleHandler.php | 11 + 3rdparty/aws/aws-sdk-php/src/HandlerList.php | 469 ++ 3rdparty/aws/aws-sdk-php/src/HasDataTrait.php | 81 + .../src/HasMonitoringEventsTrait.php | 39 + .../aws/aws-sdk-php/src/HashInterface.php | 27 + .../aws/aws-sdk-php/src/HashingStream.php | 63 + 3rdparty/aws/aws-sdk-php/src/History.php | 161 + .../src/IdempotencyTokenMiddleware.php | 120 + .../src/Identity/AwsCredentialIdentity.php | 19 + .../src/Identity/BearerTokenIdentity.php | 19 + .../src/Identity/IdentityInterface.php | 21 + .../src/Identity/S3/S3ExpressIdentity.php | 6 + .../Identity/S3/S3ExpressIdentityProvider.php | 53 + .../src/InputValidationMiddleware.php | 74 + 3rdparty/aws/aws-sdk-php/src/JsonCompiler.php | 25 + .../src/Kms/Exception/KmsException.php | 9 + .../aws/aws-sdk-php/src/Kms/KmsClient.php | 116 + .../aws/aws-sdk-php/src/LruArrayCache.php | 83 + .../aws/aws-sdk-php/src/MetricsBuilder.php | 480 ++ 3rdparty/aws/aws-sdk-php/src/Middleware.php | 466 ++ 3rdparty/aws/aws-sdk-php/src/MockHandler.php | 148 + .../src/MonitoringEventsInterface.php | 30 + .../aws/aws-sdk-php/src/MultiRegionClient.php | 269 + .../src/Multipart/AbstractUploadManager.php | 328 + .../src/Multipart/AbstractUploader.php | 153 + .../aws-sdk-php/src/Multipart/UploadState.php | 218 + 3rdparty/aws/aws-sdk-php/src/PhpHash.php | 81 + .../aws-sdk-php/src/PresignUrlMiddleware.php | 128 + .../aws/aws-sdk-php/src/Psr16CacheAdapter.php | 30 + .../aws/aws-sdk-php/src/PsrCacheAdapter.php | 38 + .../src/QueryCompatibleInputMiddleware.php | 234 + .../src/RequestCompressionMiddleware.php | 170 + .../src/ResponseContainerInterface.php | 15 + 3rdparty/aws/aws-sdk-php/src/Result.php | 57 + .../aws/aws-sdk-php/src/ResultInterface.php | 54 + .../aws/aws-sdk-php/src/ResultPaginator.php | 206 + .../aws-sdk-php/src/Retry/Configuration.php | 61 + .../src/Retry/ConfigurationInterface.php | 30 + .../src/Retry/ConfigurationProvider.php | 222 + .../Exception/ConfigurationException.php | 14 + .../aws-sdk-php/src/Retry/QuotaManager.php | 86 + .../aws/aws-sdk-php/src/Retry/RateLimiter.php | 182 + .../src/Retry/RetryHelperTrait.php | 56 + .../aws/aws-sdk-php/src/RetryMiddleware.php | 277 + .../aws/aws-sdk-php/src/RetryMiddlewareV2.php | 377 + .../src/S3/AmbiguousSuccessParser.php | 79 + .../src/S3/ApplyChecksumMiddleware.php | 242 + .../aws/aws-sdk-php/src/S3/BatchDelete.php | 240 + .../src/S3/BucketEndpointArnMiddleware.php | 355 + .../src/S3/BucketEndpointMiddleware.php | 120 + .../src/S3/CalculatesChecksumTrait.php | 59 + .../src/S3/Crypto/CryptoParamsTrait.php | 75 + .../src/S3/Crypto/CryptoParamsTraitV2.php | 19 + .../src/S3/Crypto/HeadersMetadataStrategy.php | 52 + .../InstructionFileMetadataStrategy.php | 90 + .../src/S3/Crypto/S3EncryptionClient.php | 344 + .../src/S3/Crypto/S3EncryptionClientV2.php | 450 ++ .../Crypto/S3EncryptionMultipartUploader.php | 169 + .../S3EncryptionMultipartUploaderV2.php | 176 + .../src/S3/Crypto/UserAgentTrait.php | 31 + .../src/S3/EndpointRegionHelperTrait.php | 106 + .../DeleteMultipleObjectsException.php | 68 + .../Exception/PermanentRedirectException.php | 4 + .../src/S3/Exception/S3Exception.php | 9 + .../Exception/S3MultipartUploadException.php | 84 + .../src/S3/ExpiresParsingMiddleware.php | 56 + .../src/S3/GetBucketLocationParser.php | 49 + .../aws/aws-sdk-php/src/S3/MultipartCopy.php | 249 + .../aws-sdk-php/src/S3/MultipartUploader.php | 181 + .../src/S3/MultipartUploadingTrait.php | 151 + .../aws/aws-sdk-php/src/S3/ObjectCopier.php | 170 + .../aws/aws-sdk-php/src/S3/ObjectUploader.php | 150 + .../Parser/GetBucketLocationResultMutator.php | 42 + .../aws-sdk-php/src/S3/Parser/S3Parser.php | 275 + .../src/S3/Parser/S3ResultMutator.php | 36 + .../ValidateResponseChecksumResultMutator.php | 207 + .../src/S3/PermanentRedirectMiddleware.php | 62 + .../aws/aws-sdk-php/src/S3/PostObject.php | 160 + .../aws/aws-sdk-php/src/S3/PostObjectV4.php | 195 + .../src/S3/PutObjectUrlMiddleware.php | 59 + .../src/S3/RegionalEndpoint/Configuration.php | 42 + .../ConfigurationInterface.php | 22 + .../ConfigurationProvider.php | 195 + .../Exception/ConfigurationException.php | 14 + .../S3/RetryableMalformedResponseParser.php | 56 + 3rdparty/aws/aws-sdk-php/src/S3/S3Client.php | 1302 ++++ .../aws-sdk-php/src/S3/S3ClientInterface.php | 369 + .../aws/aws-sdk-php/src/S3/S3ClientTrait.php | 399 + .../src/S3/S3EndpointMiddleware.php | 343 + .../src/S3/S3MultiRegionClient.php | 370 + .../aws/aws-sdk-php/src/S3/S3UriParser.php | 163 + .../aws/aws-sdk-php/src/S3/SSECMiddleware.php | 75 + .../aws/aws-sdk-php/src/S3/StreamWrapper.php | 1005 +++ 3rdparty/aws/aws-sdk-php/src/S3/Transfer.php | 456 ++ .../src/S3/UseArnRegion/Configuration.php | 37 + .../UseArnRegion/ConfigurationInterface.php | 19 + .../S3/UseArnRegion/ConfigurationProvider.php | 175 + .../Exception/ConfigurationException.php | 14 + .../src/S3/ValidateResponseChecksumParser.php | 149 + .../src/SSO/Exception/SSOException.php | 9 + .../aws/aws-sdk-php/src/SSO/SSOClient.php | 17 + .../SSOOIDC/Exception/SSOOIDCException.php | 9 + .../aws-sdk-php/src/SSOOIDC/SSOOIDCClient.php | 17 + .../src/Script/Composer/Composer.php | 131 + 3rdparty/aws/aws-sdk-php/src/Sdk.php | 940 +++ .../src/Signature/AnonymousSignature.php | 33 + .../src/Signature/S3ExpressSignature.php | 47 + .../src/Signature/S3SignatureV4.php | 125 + .../src/Signature/SignatureInterface.php | 45 + .../src/Signature/SignatureProvider.php | 147 + .../src/Signature/SignatureTrait.php | 48 + .../aws-sdk-php/src/Signature/SignatureV4.php | 584 ++ .../src/StreamRequestPayloadMiddleware.php | 85 + .../src/Sts/Exception/StsException.php | 9 + .../Sts/RegionalEndpoints/Configuration.php | 42 + .../ConfigurationInterface.php | 22 + .../ConfigurationProvider.php | 202 + .../Exception/ConfigurationException.php | 14 + .../aws/aws-sdk-php/src/Sts/StsClient.php | 134 + .../src/Token/BearerTokenAuthorization.php | 33 + .../aws-sdk-php/src/Token/ParsesIniTrait.php | 44 + .../RefreshableTokenProviderInterface.php | 23 + .../aws/aws-sdk-php/src/Token/SsoToken.php | 129 + .../src/Token/SsoTokenProvider.php | 280 + 3rdparty/aws/aws-sdk-php/src/Token/Token.php | 111 + .../src/Token/TokenAuthorization.php | 24 + .../aws-sdk-php/src/Token/TokenInterface.php | 36 + .../aws-sdk-php/src/Token/TokenProvider.php | 270 + .../aws/aws-sdk-php/src/TraceMiddleware.php | 360 + .../aws-sdk-php/src/UserAgentMiddleware.php | 265 + 3rdparty/aws/aws-sdk-php/src/Waiter.php | 285 + .../aws-sdk-php/src/WrappedHttpHandler.php | 208 + 3rdparty/aws/aws-sdk-php/src/functions.php | 573 ++ 3rdparty/bantu/ini-get-wrapper/LICENSE | 19 + .../ini-get-wrapper/src/IniGetWrapper.php | 162 + 3rdparty/brick/math/LICENSE | 20 + 3rdparty/brick/math/src/BigDecimal.php | 754 ++ 3rdparty/brick/math/src/BigInteger.php | 1051 +++ 3rdparty/brick/math/src/BigNumber.php | 509 ++ 3rdparty/brick/math/src/BigRational.php | 413 ++ .../src/Exception/DivisionByZeroException.php | 35 + .../Exception/IntegerOverflowException.php | 23 + .../math/src/Exception/MathException.php | 12 + .../src/Exception/NegativeNumberException.php | 12 + .../src/Exception/NumberFormatException.php | 41 + .../Exception/RoundingNecessaryException.php | 19 + .../brick/math/src/Internal/Calculator.php | 668 ++ .../Internal/Calculator/BcMathCalculator.php | 65 + .../src/Internal/Calculator/GmpCalculator.php | 108 + .../Internal/Calculator/NativeCalculator.php | 572 ++ 3rdparty/brick/math/src/RoundingMode.php | 98 + 3rdparty/composer.json | 85 + 3rdparty/composer.lock | 6222 ++++++++++++++++ 3rdparty/composer.patches.json | 8 + 3rdparty/composer/ClassLoader.php | 579 ++ 3rdparty/composer/InstalledVersions.php | 396 + 3rdparty/composer/LICENSE | 21 + 3rdparty/composer/autoload_classmap.php | 3656 ++++++++++ 3rdparty/composer/autoload_files.php | 31 + 3rdparty/composer/autoload_namespaces.php | 12 + 3rdparty/composer/autoload_psr4.php | 88 + 3rdparty/composer/autoload_real.php | 55 + 3rdparty/composer/autoload_static.php | 4195 +++++++++++ 3rdparty/composer/include_paths.php | 13 + 3rdparty/composer/installed.json | 6475 +++++++++++++++++ 3rdparty/composer/installed.php | 926 +++ 3rdparty/composer/platform_check.php | 25 + 3rdparty/cweagans/composer-patches/LICENSE.md | 9 + .../composer-patches/src/PatchEvent.php | 70 + .../composer-patches/src/PatchEvents.php | 30 + .../cweagans/composer-patches/src/Patches.php | 599 ++ 3rdparty/deepdiver/zipstreamer/COPYING | 674 ++ 3rdparty/deepdiver/zipstreamer/MANUAL.md | 153 + 3rdparty/deepdiver/zipstreamer/src/COMPR.php | 36 + .../deepdiver/zipstreamer/src/Count64.php | 147 + .../zipstreamer/src/Lib/Count64Base.php | 42 + .../zipstreamer/src/Lib/Count64_32.php | 96 + .../zipstreamer/src/Lib/Count64_64.php | 84 + .../deepdiver/zipstreamer/src/ZipStreamer.php | 730 ++ 3rdparty/deepdiver1975/tarstreamer/LICENSE | 20 + .../tarstreamer/src/TarHeader.php | 139 + .../tarstreamer/src/TarStreamer.php | 267 + 3rdparty/doctrine/dbal/LICENSE | 19 + .../doctrine/dbal/src/ArrayParameterType.php | 42 + .../dbal/src/ArrayParameters/Exception.php | 10 + .../Exception/MissingNamedParameter.php | 18 + .../Exception/MissingPositionalParameter.php | 19 + .../doctrine/dbal/src/Cache/ArrayResult.php | 116 + .../dbal/src/Cache/CacheException.php | 20 + .../dbal/src/Cache/QueryCacheProfile.php | 190 + 3rdparty/doctrine/dbal/src/ColumnCase.php | 28 + 3rdparty/doctrine/dbal/src/Configuration.php | 280 + 3rdparty/doctrine/dbal/src/Connection.php | 2038 ++++++ .../doctrine/dbal/src/ConnectionException.php | 30 + .../PrimaryReadReplicaConnection.php | 374 + 3rdparty/doctrine/dbal/src/Driver.php | 57 + .../src/Driver/API/ExceptionConverter.php | 25 + .../Driver/API/IBMDB2/ExceptionConverter.php | 65 + .../Driver/API/MySQL/ExceptionConverter.php | 131 + .../src/Driver/API/OCI/ExceptionConverter.php | 106 + .../API/PostgreSQL/ExceptionConverter.php | 89 + .../Driver/API/SQLSrv/ExceptionConverter.php | 69 + .../Driver/API/SQLite/ExceptionConverter.php | 85 + .../API/SQLite/UserDefinedFunctions.php | 80 + .../dbal/src/Driver/AbstractDB2Driver.php | 100 + .../dbal/src/Driver/AbstractException.php | 42 + .../dbal/src/Driver/AbstractMySQLDriver.php | 236 + .../dbal/src/Driver/AbstractOracleDriver.php | 65 + .../EasyConnectString.php | 116 + .../src/Driver/AbstractPostgreSQLDriver.php | 93 + .../src/Driver/AbstractSQLServerDriver.php | 53 + .../Exception/PortWithoutHost.php | 16 + .../dbal/src/Driver/AbstractSQLiteDriver.php | 52 + .../Middleware/EnableForeignKeys.php | 31 + .../doctrine/dbal/src/Driver/Connection.php | 86 + .../doctrine/dbal/src/Driver/Exception.php | 19 + .../Driver/Exception/UnknownParameterType.php | 19 + .../doctrine/dbal/src/Driver/FetchUtils.php | 73 + .../dbal/src/Driver/IBMDB2/Connection.php | 141 + .../dbal/src/Driver/IBMDB2/DataSourceName.php | 84 + .../dbal/src/Driver/IBMDB2/Driver.php | 41 + .../Exception/CannotCopyStreamToStream.php | 23 + .../Exception/CannotCreateTemporaryFile.php | 23 + .../IBMDB2/Exception/ConnectionError.php | 25 + .../IBMDB2/Exception/ConnectionFailed.php | 24 + .../src/Driver/IBMDB2/Exception/Factory.php | 31 + .../Driver/IBMDB2/Exception/PrepareFailed.php | 21 + .../IBMDB2/Exception/StatementError.php | 30 + .../dbal/src/Driver/IBMDB2/Result.php | 113 + .../dbal/src/Driver/IBMDB2/Statement.php | 220 + .../doctrine/dbal/src/Driver/Middleware.php | 12 + .../AbstractConnectionMiddleware.php | 113 + .../Middleware/AbstractDriverMiddleware.php | 73 + .../Middleware/AbstractResultMiddleware.php | 78 + .../AbstractStatementMiddleware.php | 71 + .../dbal/src/Driver/Mysqli/Connection.php | 141 + .../dbal/src/Driver/Mysqli/Driver.php | 117 + .../Mysqli/Exception/ConnectionError.php | 27 + .../Mysqli/Exception/ConnectionFailed.php | 32 + .../Exception/FailedReadingStreamOffset.php | 18 + .../Driver/Mysqli/Exception/HostRequired.php | 16 + .../Mysqli/Exception/InvalidCharset.php | 38 + .../Driver/Mysqli/Exception/InvalidOption.php | 21 + .../NonStreamResourceUsedAsLargeObject.php | 20 + .../Mysqli/Exception/StatementError.php | 27 + .../dbal/src/Driver/Mysqli/Initializer.php | 14 + .../src/Driver/Mysqli/Initializer/Charset.php | 35 + .../src/Driver/Mysqli/Initializer/Options.php | 32 + .../src/Driver/Mysqli/Initializer/Secure.php | 38 + .../dbal/src/Driver/Mysqli/Result.php | 191 + .../dbal/src/Driver/Mysqli/Statement.php | 244 + .../dbal/src/Driver/OCI8/Connection.php | 170 + .../ConvertPositionalToNamedPlaceholders.php | 56 + .../doctrine/dbal/src/Driver/OCI8/Driver.php | 58 + .../OCI8/Exception/ConnectionFailed.php | 22 + .../dbal/src/Driver/OCI8/Exception/Error.php | 23 + .../OCI8/Exception/InvalidConfiguration.php | 16 + .../Exception/NonTerminatedStringLiteral.php | 23 + .../OCI8/Exception/SequenceDoesNotExist.php | 16 + .../OCI8/Exception/UnknownParameterIndex.php | 20 + .../dbal/src/Driver/OCI8/ExecutionMode.php | 30 + .../OCI8/Middleware/InitializeSession.php | 38 + .../doctrine/dbal/src/Driver/OCI8/Result.php | 145 + .../dbal/src/Driver/OCI8/Statement.php | 174 + .../dbal/src/Driver/PDO/Connection.php | 158 + .../dbal/src/Driver/PDO/Exception.php | 26 + .../dbal/src/Driver/PDO/MySQL/Driver.php | 79 + .../dbal/src/Driver/PDO/OCI/Driver.php | 64 + .../dbal/src/Driver/PDO/PDOConnect.php | 27 + .../dbal/src/Driver/PDO/PDOException.php | 29 + .../dbal/src/Driver/PDO/ParameterTypeMap.php | 49 + .../dbal/src/Driver/PDO/PgSQL/Driver.php | 144 + .../doctrine/dbal/src/Driver/PDO/Result.php | 124 + .../dbal/src/Driver/PDO/SQLSrv/Connection.php | 70 + .../dbal/src/Driver/PDO/SQLSrv/Driver.php | 111 + .../dbal/src/Driver/PDO/SQLSrv/Statement.php | 109 + .../dbal/src/Driver/PDO/SQLite/Driver.php | 80 + .../dbal/src/Driver/PDO/Statement.php | 137 + .../dbal/src/Driver/PgSQL/Connection.php | 161 + .../src/Driver/PgSQL/ConvertParameters.php | 49 + .../doctrine/dbal/src/Driver/PgSQL/Driver.php | 96 + .../dbal/src/Driver/PgSQL/Exception.php | 26 + .../PgSQL/Exception/UnexpectedValue.php | 28 + .../PgSQL/Exception/UnknownParameter.php | 17 + .../doctrine/dbal/src/Driver/PgSQL/Result.php | 282 + .../dbal/src/Driver/PgSQL/Statement.php | 186 + 3rdparty/doctrine/dbal/src/Driver/Result.php | 93 + .../dbal/src/Driver/SQLSrv/Connection.php | 144 + .../dbal/src/Driver/SQLSrv/Driver.php | 73 + .../src/Driver/SQLSrv/Exception/Error.php | 40 + .../dbal/src/Driver/SQLSrv/Result.php | 118 + .../dbal/src/Driver/SQLSrv/Statement.php | 223 + .../dbal/src/Driver/SQLite3/Connection.php | 107 + .../dbal/src/Driver/SQLite3/Driver.php | 49 + .../dbal/src/Driver/SQLite3/Exception.php | 14 + .../dbal/src/Driver/SQLite3/Result.php | 91 + .../dbal/src/Driver/SQLite3/Statement.php | 136 + .../src/Driver/ServerInfoAwareConnection.php | 21 + .../doctrine/dbal/src/Driver/Statement.php | 78 + 3rdparty/doctrine/dbal/src/DriverManager.php | 288 + .../dbal/src/Event/ConnectionEventArgs.php | 27 + .../src/Event/Listeners/OracleSessionInit.php | 77 + .../src/Event/Listeners/SQLSessionInit.php | 43 + .../src/Event/Listeners/SQLiteSessionInit.php | 30 + .../SchemaAlterTableAddColumnEventArgs.php | 81 + .../SchemaAlterTableChangeColumnEventArgs.php | 71 + .../src/Event/SchemaAlterTableEventArgs.php | 62 + .../SchemaAlterTableRemoveColumnEventArgs.php | 71 + .../SchemaAlterTableRenameColumnEventArgs.php | 82 + .../Event/SchemaColumnDefinitionEventArgs.php | 87 + .../SchemaCreateTableColumnEventArgs.php | 71 + .../src/Event/SchemaCreateTableEventArgs.php | 87 + .../src/Event/SchemaDropTableEventArgs.php | 59 + .../dbal/src/Event/SchemaEventArgs.php | 29 + .../Event/SchemaIndexDefinitionEventArgs.php | 75 + .../src/Event/TransactionBeginEventArgs.php | 10 + .../src/Event/TransactionCommitEventArgs.php | 10 + .../dbal/src/Event/TransactionEventArgs.php | 24 + .../Event/TransactionRollBackEventArgs.php | 10 + 3rdparty/doctrine/dbal/src/Events.php | 64 + 3rdparty/doctrine/dbal/src/Exception.php | 141 + .../src/Exception/ConnectionException.php | 10 + .../dbal/src/Exception/ConnectionLost.php | 7 + .../ConstraintViolationException.php | 10 + .../src/Exception/DatabaseDoesNotExist.php | 7 + .../DatabaseObjectExistsException.php | 14 + .../DatabaseObjectNotFoundException.php | 14 + .../dbal/src/Exception/DatabaseRequired.php | 17 + .../dbal/src/Exception/DeadlockException.php | 10 + .../dbal/src/Exception/DriverException.php | 55 + ...ForeignKeyConstraintViolationException.php | 10 + .../Exception/InvalidArgumentException.php | 17 + .../Exception/InvalidFieldNameException.php | 10 + .../dbal/src/Exception/InvalidLockMode.php | 21 + .../Exception/LockWaitTimeoutException.php | 10 + .../src/Exception/MalformedDsnException.php | 13 + .../dbal/src/Exception/NoKeyValue.php | 21 + .../Exception/NonUniqueFieldNameException.php | 10 + .../NotNullConstraintViolationException.php | 10 + .../dbal/src/Exception/ReadOnlyException.php | 10 + .../dbal/src/Exception/RetryableException.php | 12 + .../dbal/src/Exception/SchemaDoesNotExist.php | 7 + .../dbal/src/Exception/ServerException.php | 10 + .../src/Exception/SyntaxErrorException.php | 10 + .../src/Exception/TableExistsException.php | 10 + .../src/Exception/TableNotFoundException.php | 10 + .../src/Exception/TransactionRolledBack.php | 7 + .../UniqueConstraintViolationException.php | 10 + .../dbal/src/ExpandArrayParameters.php | 143 + 3rdparty/doctrine/dbal/src/FetchMode.php | 20 + .../doctrine/dbal/src/Id/TableGenerator.php | 169 + .../src/Id/TableGeneratorSchemaVisitor.php | 77 + 3rdparty/doctrine/dbal/src/LockMode.php | 23 + .../doctrine/dbal/src/Logging/Connection.php | 82 + .../doctrine/dbal/src/Logging/DebugStack.php | 75 + 3rdparty/doctrine/dbal/src/Logging/Driver.php | 58 + .../doctrine/dbal/src/Logging/LoggerChain.php | 48 + .../doctrine/dbal/src/Logging/Middleware.php | 24 + .../doctrine/dbal/src/Logging/SQLLogger.php | 32 + .../doctrine/dbal/src/Logging/Statement.php | 100 + 3rdparty/doctrine/dbal/src/ParameterType.php | 57 + .../src/Platforms/AbstractMySQLPlatform.php | 1497 ++++ .../dbal/src/Platforms/AbstractPlatform.php | 4727 ++++++++++++ .../dbal/src/Platforms/DB2111Platform.php | 40 + .../dbal/src/Platforms/DB2Platform.php | 1053 +++ .../dbal/src/Platforms/DateIntervalUnit.php | 29 + .../src/Platforms/Keywords/DB2Keywords.php | 430 ++ .../src/Platforms/Keywords/KeywordList.php | 56 + .../Platforms/Keywords/MariaDBKeywords.php | 276 + .../Platforms/Keywords/MariaDb102Keywords.php | 27 + .../Platforms/Keywords/MariaDb117Keywords.php | 46 + .../Platforms/Keywords/MySQL57Keywords.php | 275 + .../Platforms/Keywords/MySQL80Keywords.php | 75 + .../Platforms/Keywords/MySQL84Keywords.php | 64 + .../src/Platforms/Keywords/MySQLKeywords.php | 275 + .../src/Platforms/Keywords/OracleKeywords.php | 149 + .../Keywords/PostgreSQL100Keywords.php | 27 + .../Keywords/PostgreSQL94Keywords.php | 12 + .../Platforms/Keywords/PostgreSQLKeywords.php | 135 + .../Keywords/ReservedKeywordsValidator.php | 130 + .../Keywords/SQLServer2012Keywords.php | 12 + .../Platforms/Keywords/SQLServerKeywords.php | 224 + .../src/Platforms/Keywords/SQLiteKeywords.php | 157 + .../dbal/src/Platforms/MariaDBPlatform.php | 55 + .../src/Platforms/MariaDb1010Platform.php | 47 + .../src/Platforms/MariaDb1027Platform.php | 13 + .../src/Platforms/MariaDb1043Platform.php | 131 + .../src/Platforms/MariaDb1052Platform.php | 36 + .../src/Platforms/MariaDb1060Platform.php | 16 + .../src/Platforms/MariaDb110700Platform.php | 26 + .../MySQL/CollationMetadataProvider.php | 11 + .../CachingCollationMetadataProvider.php | 33 + .../ConnectionCollationMetadataProvider.php | 41 + .../dbal/src/Platforms/MySQL/Comparator.php | 94 + .../dbal/src/Platforms/MySQL57Platform.php | 99 + .../dbal/src/Platforms/MySQL80Platform.php | 34 + .../dbal/src/Platforms/MySQL84Platform.php | 28 + .../dbal/src/Platforms/MySQLPlatform.php | 11 + .../dbal/src/Platforms/OraclePlatform.php | 1329 ++++ .../src/Platforms/PostgreSQL100Platform.php | 36 + .../src/Platforms/PostgreSQL120Platform.php | 30 + .../src/Platforms/PostgreSQL94Platform.php | 12 + .../dbal/src/Platforms/PostgreSQLPlatform.php | 1430 ++++ .../src/Platforms/SQLServer/Comparator.php | 63 + .../SQL/Builder/SQLServerSelectSQLBuilder.php | 86 + .../src/Platforms/SQLServer2012Platform.php | 13 + .../dbal/src/Platforms/SQLServerPlatform.php | 1848 +++++ .../dbal/src/Platforms/SQLite/Comparator.php | 61 + .../dbal/src/Platforms/SqlitePlatform.php | 1545 ++++ .../doctrine/dbal/src/Platforms/TrimMode.php | 21 + .../dbal/src/Portability/Connection.php | 45 + .../dbal/src/Portability/Converter.php | 300 + .../doctrine/dbal/src/Portability/Driver.php | 83 + .../dbal/src/Portability/Middleware.php | 38 + .../dbal/src/Portability/OptimizeFlags.php | 42 + .../doctrine/dbal/src/Portability/Result.php | 81 + .../dbal/src/Portability/Statement.php | 36 + 3rdparty/doctrine/dbal/src/Query.php | 60 + .../Query/Expression/CompositeExpression.php | 183 + .../Query/Expression/ExpressionBuilder.php | 323 + .../doctrine/dbal/src/Query/ForUpdate.php | 21 + .../ForUpdate/ConflictResolutionMode.php | 27 + 3rdparty/doctrine/dbal/src/Query/Limit.php | 30 + .../doctrine/dbal/src/Query/QueryBuilder.php | 1759 +++++ .../dbal/src/Query/QueryException.php | 36 + .../doctrine/dbal/src/Query/SelectQuery.php | 107 + 3rdparty/doctrine/dbal/src/Result.php | 339 + .../Builder/CreateSchemaObjectsSQLBuilder.php | 85 + .../SQL/Builder/DefaultSelectSQLBuilder.php | 95 + .../Builder/DropSchemaObjectsSQLBuilder.php | 62 + .../dbal/src/SQL/Builder/SelectSQLBuilder.php | 12 + 3rdparty/doctrine/dbal/src/SQL/Parser.php | 114 + .../dbal/src/SQL/Parser/Exception.php | 11 + .../Exception/RegularExpressionError.php | 19 + .../doctrine/dbal/src/SQL/Parser/Visitor.php | 26 + .../dbal/src/Schema/AbstractAsset.php | 224 + .../dbal/src/Schema/AbstractSchemaManager.php | 1808 +++++ 3rdparty/doctrine/dbal/src/Schema/Column.php | 466 ++ .../doctrine/dbal/src/Schema/ColumnDiff.php | 169 + .../doctrine/dbal/src/Schema/Comparator.php | 716 ++ .../doctrine/dbal/src/Schema/Constraint.php | 41 + .../dbal/src/Schema/DB2SchemaManager.php | 451 ++ .../Schema/DefaultSchemaManagerFactory.php | 20 + .../Schema/Exception/ColumnAlreadyExists.php | 20 + .../Schema/Exception/ColumnDoesNotExist.php | 20 + .../Exception/ForeignKeyDoesNotExist.php | 20 + .../Schema/Exception/IndexAlreadyExists.php | 20 + .../Schema/Exception/IndexDoesNotExist.php | 20 + .../src/Schema/Exception/IndexNameInvalid.php | 20 + .../src/Schema/Exception/InvalidTableName.php | 17 + .../Exception/NamedForeignKeyRequired.php | 29 + .../Exception/NamespaceAlreadyExists.php | 20 + .../Exception/SequenceAlreadyExists.php | 20 + .../Schema/Exception/SequenceDoesNotExist.php | 20 + .../Schema/Exception/TableAlreadyExists.php | 20 + .../Schema/Exception/TableDoesNotExist.php | 20 + .../UniqueConstraintDoesNotExist.php | 20 + .../Schema/Exception/UnknownColumnOption.php | 19 + .../dbal/src/Schema/ForeignKeyConstraint.php | 410 ++ .../doctrine/dbal/src/Schema/Identifier.php | 27 + 3rdparty/doctrine/dbal/src/Schema/Index.php | 365 + .../src/Schema/LegacySchemaManagerFactory.php | 19 + .../dbal/src/Schema/MySQLSchemaManager.php | 605 ++ .../dbal/src/Schema/OracleSchemaManager.php | 537 ++ .../src/Schema/PostgreSQLSchemaManager.php | 784 ++ .../src/Schema/SQLServerSchemaManager.php | 615 ++ 3rdparty/doctrine/dbal/src/Schema/Schema.php | 523 ++ .../doctrine/dbal/src/Schema/SchemaConfig.php | 120 + .../doctrine/dbal/src/Schema/SchemaDiff.php | 294 + .../dbal/src/Schema/SchemaException.php | 203 + .../dbal/src/Schema/SchemaManagerFactory.php | 17 + .../doctrine/dbal/src/Schema/Sequence.php | 151 + .../dbal/src/Schema/SqliteSchemaManager.php | 807 ++ 3rdparty/doctrine/dbal/src/Schema/Table.php | 1041 +++ .../doctrine/dbal/src/Schema/TableDiff.php | 361 + .../dbal/src/Schema/UniqueConstraint.php | 154 + 3rdparty/doctrine/dbal/src/Schema/View.php | 28 + .../src/Schema/Visitor/AbstractVisitor.php | 49 + .../Visitor/CreateSchemaSqlCollector.php | 104 + .../Schema/Visitor/DropSchemaSqlCollector.php | 107 + .../dbal/src/Schema/Visitor/Graphviz.php | 164 + .../src/Schema/Visitor/NamespaceVisitor.php | 20 + .../Schema/Visitor/RemoveNamespacedAssets.php | 103 + .../dbal/src/Schema/Visitor/Visitor.php | 45 + 3rdparty/doctrine/dbal/src/Statement.php | 261 + .../Console/Command/CommandCompatibility.php | 63 + .../Console/Command/ReservedWordsCommand.php | 221 + .../Tools/Console/Command/RunSqlCommand.php | 119 + .../src/Tools/Console/ConnectionNotFound.php | 9 + .../src/Tools/Console/ConnectionProvider.php | 13 + .../SingleConnectionProvider.php | 36 + .../dbal/src/Tools/Console/ConsoleRunner.php | 81 + .../doctrine/dbal/src/Tools/DsnParser.php | 218 + .../dbal/src/TransactionIsolationLevel.php | 33 + .../doctrine/dbal/src/Types/ArrayType.php | 91 + .../dbal/src/Types/AsciiStringType.php | 29 + .../doctrine/dbal/src/Types/BigIntType.php | 50 + .../doctrine/dbal/src/Types/BinaryType.php | 67 + 3rdparty/doctrine/dbal/src/Types/BlobType.php | 67 + .../doctrine/dbal/src/Types/BooleanType.php | 79 + .../dbal/src/Types/ConversionException.php | 121 + .../dbal/src/Types/DateImmutableType.php | 92 + .../dbal/src/Types/DateIntervalType.php | 110 + .../dbal/src/Types/DateTimeImmutableType.php | 98 + .../doctrine/dbal/src/Types/DateTimeType.php | 115 + .../src/Types/DateTimeTzImmutableType.php | 92 + .../dbal/src/Types/DateTimeTzType.php | 122 + 3rdparty/doctrine/dbal/src/Types/DateType.php | 104 + .../doctrine/dbal/src/Types/DecimalType.php | 47 + .../doctrine/dbal/src/Types/FloatType.php | 38 + 3rdparty/doctrine/dbal/src/Types/GuidType.php | 45 + .../doctrine/dbal/src/Types/IntegerType.php | 50 + 3rdparty/doctrine/dbal/src/Types/JsonType.php | 96 + .../doctrine/dbal/src/Types/ObjectType.php | 88 + .../dbal/src/Types/PhpDateTimeMappingType.php | 12 + .../dbal/src/Types/PhpIntegerMappingType.php | 12 + .../dbal/src/Types/SimpleArrayType.php | 88 + .../doctrine/dbal/src/Types/SmallIntType.php | 50 + .../doctrine/dbal/src/Types/StringType.php | 27 + 3rdparty/doctrine/dbal/src/Types/TextType.php | 38 + .../dbal/src/Types/TimeImmutableType.php | 92 + 3rdparty/doctrine/dbal/src/Types/TimeType.php | 104 + 3rdparty/doctrine/dbal/src/Types/Type.php | 296 + .../doctrine/dbal/src/Types/TypeRegistry.php | 127 + 3rdparty/doctrine/dbal/src/Types/Types.php | 47 + .../src/Types/VarDateTimeImmutableType.php | 89 + .../dbal/src/Types/VarDateTimeType.php | 42 + .../dbal/src/VersionAwarePlatformDriver.php | 30 + 3rdparty/doctrine/deprecations/LICENSE | 19 + .../doctrine/deprecations/src/Deprecation.php | 309 + .../src/PHPUnit/VerifyDeprecations.php | 66 + 3rdparty/doctrine/event-manager/LICENSE | 19 + .../doctrine/event-manager/src/EventArgs.php | 37 + .../event-manager/src/EventManager.php | 129 + .../event-manager/src/EventSubscriber.php | 21 + 3rdparty/doctrine/lexer/LICENSE | 19 + 3rdparty/doctrine/lexer/src/AbstractLexer.php | 328 + 3rdparty/doctrine/lexer/src/Token.php | 56 + .../egulias/email-validator/CONTRIBUTING.md | 153 + 3rdparty/egulias/email-validator/LICENSE | 19 + .../email-validator/src/EmailLexer.php | 329 + .../email-validator/src/EmailParser.php | 90 + .../email-validator/src/EmailValidator.php | 67 + .../email-validator/src/MessageIDParser.php | 91 + .../egulias/email-validator/src/Parser.php | 78 + .../email-validator/src/Parser/Comment.php | 102 + .../CommentStrategy/CommentStrategy.php | 22 + .../Parser/CommentStrategy/DomainComment.php | 33 + .../Parser/CommentStrategy/LocalComment.php | 38 + .../src/Parser/DomainLiteral.php | 210 + .../email-validator/src/Parser/DomainPart.php | 327 + .../src/Parser/DoubleQuote.php | 91 + .../src/Parser/FoldingWhiteSpace.php | 87 + .../email-validator/src/Parser/IDLeftPart.php | 15 + .../src/Parser/IDRightPart.php | 29 + .../email-validator/src/Parser/LocalPart.php | 163 + .../email-validator/src/Parser/PartParser.php | 63 + .../src/Result/InvalidEmail.php | 49 + .../src/Result/MultipleErrors.php | 56 + .../src/Result/Reason/AtextAfterCFWS.php | 16 + .../src/Result/Reason/CRLFAtTheEnd.php | 19 + .../src/Result/Reason/CRLFX2.php | 16 + .../src/Result/Reason/CRNoLF.php | 16 + .../src/Result/Reason/CharNotAllowed.php | 16 + .../src/Result/Reason/CommaInDomain.php | 16 + .../src/Result/Reason/CommentsInIDRight.php | 16 + .../src/Result/Reason/ConsecutiveAt.php | 17 + .../src/Result/Reason/ConsecutiveDot.php | 16 + .../src/Result/Reason/DetailedReason.php | 13 + .../src/Result/Reason/DomainAcceptsNoMail.php | 16 + .../src/Result/Reason/DomainHyphened.php | 16 + .../src/Result/Reason/DomainTooLong.php | 16 + .../src/Result/Reason/DotAtEnd.php | 16 + .../src/Result/Reason/DotAtStart.php | 16 + .../src/Result/Reason/EmptyReason.php | 16 + .../src/Result/Reason/ExceptionFound.php | 26 + .../src/Result/Reason/ExpectingATEXT.php | 16 + .../src/Result/Reason/ExpectingCTEXT.php | 16 + .../src/Result/Reason/ExpectingDTEXT.php | 16 + .../Reason/ExpectingDomainLiteralClose.php | 16 + .../src/Result/Reason/LabelTooLong.php | 16 + .../Result/Reason/LocalOrReservedDomain.php | 16 + .../src/Result/Reason/NoDNSRecord.php | 16 + .../src/Result/Reason/NoDomainPart.php | 16 + .../src/Result/Reason/NoLocalPart.php | 16 + .../src/Result/Reason/RFCWarnings.php | 16 + .../src/Result/Reason/Reason.php | 16 + .../src/Result/Reason/SpoofEmail.php | 17 + .../src/Result/Reason/UnOpenedComment.php | 16 + .../Result/Reason/UnableToGetDNSRecord.php | 19 + .../src/Result/Reason/UnclosedComment.php | 16 + .../Result/Reason/UnclosedQuotedString.php | 16 + .../src/Result/Reason/UnusualElements.php | 26 + .../email-validator/src/Result/Result.php | 31 + .../email-validator/src/Result/SpoofEmail.php | 13 + .../email-validator/src/Result/ValidEmail.php | 27 + .../src/Validation/DNSCheckValidation.php | 211 + .../src/Validation/DNSGetRecordWrapper.php | 30 + .../src/Validation/DNSRecords.php | 27 + .../src/Validation/EmailValidation.php | 34 + .../Exception/EmptyValidationList.php | 16 + .../Validation/Extra/SpoofCheckValidation.php | 46 + .../src/Validation/MessageIDValidation.php | 55 + .../Validation/MultipleValidationWithAnd.php | 105 + .../Validation/NoRFCWarningsValidation.php | 41 + .../src/Validation/RFCValidation.php | 54 + .../src/Warning/AddressLiteral.php | 14 + .../src/Warning/CFWSNearAt.php | 13 + .../src/Warning/CFWSWithFWS.php | 13 + .../email-validator/src/Warning/Comment.php | 13 + .../src/Warning/DeprecatedComment.php | 13 + .../src/Warning/DomainLiteral.php | 14 + .../src/Warning/EmailTooLong.php | 15 + .../src/Warning/IPV6BadChar.php | 14 + .../src/Warning/IPV6ColonEnd.php | 14 + .../src/Warning/IPV6ColonStart.php | 14 + .../src/Warning/IPV6Deprecated.php | 14 + .../src/Warning/IPV6DoubleColon.php | 14 + .../src/Warning/IPV6GroupCount.php | 14 + .../src/Warning/IPV6MaxGroups.php | 14 + .../src/Warning/LocalTooLong.php | 15 + .../src/Warning/NoDNSMXRecord.php | 14 + .../src/Warning/ObsoleteDTEXT.php | 14 + .../src/Warning/QuotedPart.php | 27 + .../src/Warning/QuotedString.php | 17 + .../email-validator/src/Warning/TLD.php | 13 + .../email-validator/src/Warning/Warning.php | 53 + 3rdparty/f7cloud/lognormalizer/COPYING | 661 ++ .../f7cloud/lognormalizer/src/Normalizer.php | 330 + 3rdparty/fusonic/opengraph/LICENSE | 20 + 3rdparty/fusonic/opengraph/src/Consumer.php | 142 + .../fusonic/opengraph/src/Elements/Audio.php | 66 + .../opengraph/src/Elements/ElementBase.php | 25 + .../fusonic/opengraph/src/Elements/Image.php | 93 + .../fusonic/opengraph/src/Elements/Video.php | 84 + .../opengraph/src/Objects/ObjectBase.php | 355 + .../fusonic/opengraph/src/Objects/Website.php | 26 + 3rdparty/fusonic/opengraph/src/Property.php | 58 + 3rdparty/fusonic/opengraph/src/Publisher.php | 66 + .../libphonenumber-for-php-lite/LICENSE.txt | 177 + .../METADATA-VERSION.php | 11 + .../src/CountryCodeSource.php | 36 + .../src/CountryCodeToRegionCodeMap.php | 269 + .../src/MatchType.php | 18 + .../src/Matcher.php | 151 + .../src/MatcherAPIInterface.php | 23 + .../src/MetadataSourceInterface.php | 18 + .../src/MultiFileMetadataSourceImpl.php | 84 + .../src/NumberFormat.php | 183 + .../src/NumberParseException.php | 64 + .../src/PhoneMetadata.php | 305 + .../src/PhoneNumber.php | 409 ++ .../src/PhoneNumberDesc.php | 140 + .../src/PhoneNumberFormat.php | 25 + .../src/PhoneNumberMatch.php | 86 + .../src/PhoneNumberType.php | 56 + .../src/PhoneNumberUtil.php | 3505 +++++++++ .../src/RegexBasedMatcher.php | 42 + .../src/ShortNumberCost.php | 17 + .../src/ShortNumberInfo.php | 613 ++ .../src/ShortNumbersRegionCodeSet.php | 266 + .../src/ValidationResult.php | 47 + 3rdparty/guzzlehttp/guzzle/LICENSE | 27 + .../guzzlehttp/guzzle/src/BodySummarizer.php | 28 + .../guzzle/src/BodySummarizerInterface.php | 13 + 3rdparty/guzzlehttp/guzzle/src/Client.php | 483 ++ .../guzzlehttp/guzzle/src/ClientInterface.php | 84 + .../guzzlehttp/guzzle/src/ClientTrait.php | 241 + .../guzzle/src/Cookie/CookieJar.php | 307 + .../guzzle/src/Cookie/CookieJarInterface.php | 80 + .../guzzle/src/Cookie/FileCookieJar.php | 101 + .../guzzle/src/Cookie/SessionCookieJar.php | 77 + .../guzzle/src/Cookie/SetCookie.php | 492 ++ .../src/Exception/BadResponseException.php | 39 + .../guzzle/src/Exception/ClientException.php | 10 + .../guzzle/src/Exception/ConnectException.php | 56 + .../guzzle/src/Exception/GuzzleException.php | 9 + .../Exception/InvalidArgumentException.php | 7 + .../guzzle/src/Exception/RequestException.php | 150 + .../guzzle/src/Exception/ServerException.php | 10 + .../Exception/TooManyRedirectsException.php | 7 + .../src/Exception/TransferException.php | 7 + .../guzzle/src/Handler/CurlFactory.php | 736 ++ .../src/Handler/CurlFactoryInterface.php | 25 + .../guzzle/src/Handler/CurlHandler.php | 49 + .../guzzle/src/Handler/CurlMultiHandler.php | 284 + .../guzzle/src/Handler/EasyHandle.php | 112 + .../guzzle/src/Handler/HeaderProcessor.php | 42 + .../guzzle/src/Handler/MockHandler.php | 212 + .../guzzlehttp/guzzle/src/Handler/Proxy.php | 51 + .../guzzle/src/Handler/StreamHandler.php | 627 ++ .../guzzlehttp/guzzle/src/HandlerStack.php | 275 + .../guzzle/src/MessageFormatter.php | 199 + .../guzzle/src/MessageFormatterInterface.php | 18 + 3rdparty/guzzlehttp/guzzle/src/Middleware.php | 268 + 3rdparty/guzzlehttp/guzzle/src/Pool.php | 125 + .../guzzle/src/PrepareBodyMiddleware.php | 105 + .../guzzle/src/RedirectMiddleware.php | 228 + .../guzzlehttp/guzzle/src/RequestOptions.php | 274 + .../guzzlehttp/guzzle/src/RetryMiddleware.php | 119 + .../guzzlehttp/guzzle/src/TransferStats.php | 133 + 3rdparty/guzzlehttp/guzzle/src/Utils.php | 384 + 3rdparty/guzzlehttp/guzzle/src/functions.php | 167 + .../guzzle/src/functions_include.php | 6 + 3rdparty/guzzlehttp/promises/LICENSE | 24 + .../promises/src/AggregateException.php | 19 + .../promises/src/CancellationException.php | 12 + .../guzzlehttp/promises/src/Coroutine.php | 162 + 3rdparty/guzzlehttp/promises/src/Create.php | 79 + 3rdparty/guzzlehttp/promises/src/Each.php | 81 + .../guzzlehttp/promises/src/EachPromise.php | 248 + .../promises/src/FulfilledPromise.php | 89 + 3rdparty/guzzlehttp/promises/src/Is.php | 40 + 3rdparty/guzzlehttp/promises/src/Promise.php | 281 + .../promises/src/PromiseInterface.php | 91 + .../promises/src/PromisorInterface.php | 16 + .../promises/src/RejectedPromise.php | 95 + .../promises/src/RejectionException.php | 49 + .../guzzlehttp/promises/src/TaskQueue.php | 71 + .../promises/src/TaskQueueInterface.php | 24 + 3rdparty/guzzlehttp/promises/src/Utils.php | 261 + 3rdparty/guzzlehttp/psr7/LICENSE | 26 + 3rdparty/guzzlehttp/psr7/src/AppendStream.php | 248 + 3rdparty/guzzlehttp/psr7/src/BufferStream.php | 147 + .../guzzlehttp/psr7/src/CachingStream.php | 153 + .../guzzlehttp/psr7/src/DroppingStream.php | 49 + .../src/Exception/MalformedUriException.php | 14 + 3rdparty/guzzlehttp/psr7/src/FnStream.php | 180 + 3rdparty/guzzlehttp/psr7/src/Header.php | 134 + 3rdparty/guzzlehttp/psr7/src/HttpFactory.php | 94 + .../guzzlehttp/psr7/src/InflateStream.php | 37 + .../guzzlehttp/psr7/src/LazyOpenStream.php | 49 + 3rdparty/guzzlehttp/psr7/src/LimitStream.php | 157 + 3rdparty/guzzlehttp/psr7/src/Message.php | 246 + 3rdparty/guzzlehttp/psr7/src/MessageTrait.php | 265 + 3rdparty/guzzlehttp/psr7/src/MimeType.php | 1259 ++++ .../guzzlehttp/psr7/src/MultipartStream.php | 165 + 3rdparty/guzzlehttp/psr7/src/NoSeekStream.php | 28 + 3rdparty/guzzlehttp/psr7/src/PumpStream.php | 179 + 3rdparty/guzzlehttp/psr7/src/Query.php | 118 + 3rdparty/guzzlehttp/psr7/src/Request.php | 159 + 3rdparty/guzzlehttp/psr7/src/Response.php | 161 + 3rdparty/guzzlehttp/psr7/src/Rfc7230.php | 23 + .../guzzlehttp/psr7/src/ServerRequest.php | 340 + 3rdparty/guzzlehttp/psr7/src/Stream.php | 283 + .../psr7/src/StreamDecoratorTrait.php | 156 + .../guzzlehttp/psr7/src/StreamWrapper.php | 207 + 3rdparty/guzzlehttp/psr7/src/UploadedFile.php | 211 + 3rdparty/guzzlehttp/psr7/src/Uri.php | 743 ++ .../guzzlehttp/psr7/src/UriComparator.php | 52 + .../guzzlehttp/psr7/src/UriNormalizer.php | 220 + 3rdparty/guzzlehttp/psr7/src/UriResolver.php | 211 + 3rdparty/guzzlehttp/psr7/src/Utils.php | 477 ++ 3rdparty/guzzlehttp/uri-template/LICENSE | 23 + .../uri-template/src/UriTemplate.php | 295 + 3rdparty/icewind/searchdav/LICENSE | 661 ++ .../searchdav/src/Backend/ISearchBackend.php | 94 + .../src/Backend/SearchPropertyDefinition.php | 66 + .../searchdav/src/Backend/SearchResult.php | 42 + .../searchdav/src/DAV/DiscoverHandler.php | 107 + .../icewind/searchdav/src/DAV/PathHelper.php | 50 + .../icewind/searchdav/src/DAV/QueryParser.php | 83 + .../searchdav/src/DAV/SearchHandler.php | 200 + .../searchdav/src/DAV/SearchPlugin.php | 128 + .../icewind/searchdav/src/Query/Limit.php | 39 + .../icewind/searchdav/src/Query/Literal.php | 40 + .../icewind/searchdav/src/Query/Operator.php | 68 + .../icewind/searchdav/src/Query/Order.php | 52 + .../icewind/searchdav/src/Query/Query.php | 78 + .../icewind/searchdav/src/Query/Scope.php | 59 + .../icewind/searchdav/src/XML/BasicSearch.php | 98 + .../searchdav/src/XML/BasicSearchSchema.php | 49 + 3rdparty/icewind/searchdav/src/XML/Limit.php | 45 + .../icewind/searchdav/src/XML/Literal.php | 41 + .../icewind/searchdav/src/XML/Operator.php | 94 + 3rdparty/icewind/searchdav/src/XML/Order.php | 63 + .../icewind/searchdav/src/XML/PropDesc.php | 78 + .../src/XML/QueryDiscoverResponse.php | 61 + 3rdparty/icewind/searchdav/src/XML/Scope.php | 38 + .../src/XML/SupportedQueryGrammar.php | 38 + 3rdparty/icewind/smb/LICENSE.txt | 19 + .../smb/LICENSES/AGPL-3.0-or-later.txt | 235 + 3rdparty/icewind/smb/LICENSES/CC0-1.0.txt | 121 + 3rdparty/icewind/smb/LICENSES/MIT.txt | 9 + 3rdparty/icewind/smb/src/ACL.php | 69 + 3rdparty/icewind/smb/src/AbstractServer.php | 61 + 3rdparty/icewind/smb/src/AbstractShare.php | 37 + 3rdparty/icewind/smb/src/AnonymousAuth.php | 33 + 3rdparty/icewind/smb/src/BasicAuth.php | 42 + 3rdparty/icewind/smb/src/Change.php | 27 + .../src/Exception/AccessDeniedException.php | 10 + .../src/Exception/AlreadyExistsException.php | 10 + .../src/Exception/AuthenticationException.php | 10 + .../smb/src/Exception/ConnectException.php | 10 + .../Exception/ConnectionAbortedException.php | 10 + .../smb/src/Exception/ConnectionException.php | 10 + .../Exception/ConnectionRefusedException.php | 10 + .../Exception/ConnectionResetException.php | 10 + .../smb/src/Exception/DependencyException.php | 10 + .../icewind/smb/src/Exception/Exception.php | 51 + .../smb/src/Exception/FileInUseException.php | 10 + .../smb/src/Exception/ForbiddenException.php | 10 + .../smb/src/Exception/HostDownException.php | 10 + .../Exception/InvalidArgumentException.php | 10 + .../src/Exception/InvalidHostException.php | 10 + .../Exception/InvalidParameterException.php | 10 + .../src/Exception/InvalidPathException.php | 10 + .../src/Exception/InvalidRequestException.php | 29 + .../Exception/InvalidResourceException.php | 10 + .../smb/src/Exception/InvalidTicket.php | 13 + .../src/Exception/InvalidTypeException.php | 10 + .../src/Exception/NoLoginServerException.php | 10 + .../src/Exception/NoRouteToHostException.php | 10 + .../smb/src/Exception/NotEmptyException.php | 10 + .../smb/src/Exception/NotFoundException.php | 10 + .../smb/src/Exception/OutOfSpaceException.php | 10 + .../Exception/RevisionMismatchException.php | 15 + .../smb/src/Exception/TimedOutException.php | 10 + 3rdparty/icewind/smb/src/IAuth.php | 29 + 3rdparty/icewind/smb/src/IFileInfo.php | 45 + 3rdparty/icewind/smb/src/INotifyHandler.php | 43 + 3rdparty/icewind/smb/src/IOptions.php | 26 + 3rdparty/icewind/smb/src/IServer.php | 31 + 3rdparty/icewind/smb/src/IShare.php | 164 + 3rdparty/icewind/smb/src/ISystem.php | 63 + .../icewind/smb/src/ITimeZoneProvider.php | 17 + .../icewind/smb/src/KerberosApacheAuth.php | 47 + 3rdparty/icewind/smb/src/KerberosAuth.php | 64 + 3rdparty/icewind/smb/src/KerberosTicket.php | 85 + .../icewind/smb/src/Native/NativeFileInfo.php | 142 + .../smb/src/Native/NativeReadStream.php | 92 + .../icewind/smb/src/Native/NativeServer.php | 64 + .../icewind/smb/src/Native/NativeShare.php | 369 + .../icewind/smb/src/Native/NativeState.php | 433 ++ .../icewind/smb/src/Native/NativeStream.php | 158 + .../smb/src/Native/NativeWriteStream.php | 95 + 3rdparty/icewind/smb/src/Options.php | 41 + 3rdparty/icewind/smb/src/ServerFactory.php | 70 + 3rdparty/icewind/smb/src/StringBuffer.php | 48 + 3rdparty/icewind/smb/src/System.php | 75 + 3rdparty/icewind/smb/src/TimeZoneProvider.php | 52 + .../icewind/smb/src/Wrapped/Connection.php | 115 + .../icewind/smb/src/Wrapped/ErrorCodes.php | 30 + 3rdparty/icewind/smb/src/Wrapped/FileInfo.php | 88 + .../icewind/smb/src/Wrapped/NotifyHandler.php | 111 + 3rdparty/icewind/smb/src/Wrapped/Parser.php | 276 + .../icewind/smb/src/Wrapped/RawConnection.php | 250 + 3rdparty/icewind/smb/src/Wrapped/Server.php | 103 + 3rdparty/icewind/smb/src/Wrapped/Share.php | 553 ++ 3rdparty/icewind/streams/LICENSE.txt | 19 + .../streams/LICENSES/AGPL-3.0-or-later.txt | 235 + 3rdparty/icewind/streams/LICENSES/CC0-1.0.txt | 121 + 3rdparty/icewind/streams/LICENSES/MIT.txt | 9 + .../icewind/streams/composer.json.license | 2 + .../icewind/streams/src/CallbackWrapper.php | 131 + 3rdparty/icewind/streams/src/CountWrapper.php | 99 + 3rdparty/icewind/streams/src/Directory.php | 34 + .../icewind/streams/src/DirectoryFilter.php | 56 + .../icewind/streams/src/DirectoryWrapper.php | 46 + 3rdparty/icewind/streams/src/File.php | 85 + 3rdparty/icewind/streams/src/HashWrapper.php | 61 + .../icewind/streams/src/IteratorDirectory.php | 112 + 3rdparty/icewind/streams/src/NullWrapper.php | 26 + 3rdparty/icewind/streams/src/Path.php | 107 + 3rdparty/icewind/streams/src/PathWrapper.php | 22 + .../icewind/streams/src/ReadHashWrapper.php | 23 + 3rdparty/icewind/streams/src/RetryWrapper.php | 51 + .../icewind/streams/src/SeekableWrapper.php | 82 + 3rdparty/icewind/streams/src/Url.php | 63 + 3rdparty/icewind/streams/src/UrlCallback.php | 134 + 3rdparty/icewind/streams/src/Wrapper.php | 130 + .../icewind/streams/src/WrapperHandler.php | 99 + .../icewind/streams/src/WriteHashWrapper.php | 22 + 3rdparty/justinrainbow/json-schema/LICENSE | 21 + .../src/JsonSchema/ConstraintError.php | 117 + .../JsonSchema/Constraints/BaseConstraint.php | 170 + .../Constraints/CollectionConstraint.php | 136 + .../Constraints/ConstConstraint.php | 51 + .../src/JsonSchema/Constraints/Constraint.php | 202 + .../Constraints/ConstraintInterface.php | 59 + .../JsonSchema/Constraints/EnumConstraint.php | 59 + .../src/JsonSchema/Constraints/Factory.php | 217 + .../Constraints/FormatConstraint.php | 217 + .../Constraints/NumberConstraint.php | 84 + .../Constraints/ObjectConstraint.php | 190 + .../Constraints/SchemaConstraint.php | 97 + .../Constraints/StringConstraint.php | 63 + .../Constraints/TypeCheck/LooseTypeCheck.php | 70 + .../Constraints/TypeCheck/StrictTypeCheck.php | 42 + .../TypeCheck/TypeCheckInterface.php | 20 + .../JsonSchema/Constraints/TypeConstraint.php | 365 + .../Constraints/UndefinedConstraint.php | 428 ++ .../src/JsonSchema/Entity/JsonPointer.php | 163 + .../json-schema/src/JsonSchema/Enum.php | 9 + .../Exception/ExceptionInterface.php | 9 + .../Exception/InvalidArgumentException.php | 19 + .../Exception/InvalidConfigException.php | 19 + .../Exception/InvalidSchemaException.php | 19 + .../InvalidSchemaMediaTypeException.php | 19 + .../Exception/InvalidSourceUriException.php | 19 + .../Exception/JsonDecodingException.php | 42 + .../Exception/ResourceNotFoundException.php | 19 + .../JsonSchema/Exception/RuntimeException.php | 19 + .../UnresolvableJsonPointerException.php | 21 + .../Exception/UriResolverException.php | 19 + .../Exception/ValidationException.php | 16 + .../JsonSchema/Iterator/ObjectIterator.php | 152 + .../json-schema/src/JsonSchema/Rfc3339.php | 32 + .../src/JsonSchema/SchemaStorage.php | 212 + .../src/JsonSchema/SchemaStorageInterface.php | 38 + .../src/JsonSchema/Tool/DeepComparer.php | 71 + .../src/JsonSchema/Tool/DeepCopy.php | 38 + .../Validator/RelativeReferenceValidator.php | 53 + .../Tool/Validator/UriValidator.php | 65 + .../Uri/Retrievers/AbstractRetriever.php | 37 + .../src/JsonSchema/Uri/Retrievers/Curl.php | 85 + .../Uri/Retrievers/FileGetContents.php | 95 + .../Uri/Retrievers/PredefinedArray.php | 58 + .../Uri/Retrievers/UriRetrieverInterface.php | 38 + .../src/JsonSchema/Uri/UriResolver.php | 189 + .../src/JsonSchema/Uri/UriRetriever.php | 351 + .../src/JsonSchema/UriResolverInterface.php | 28 + .../src/JsonSchema/UriRetrieverInterface.php | 28 + .../json-schema/src/JsonSchema/Validator.php | 108 + 3rdparty/kornrunner/blurhash/LICENSE | 21 + 3rdparty/kornrunner/blurhash/src/AC.php | 34 + 3rdparty/kornrunner/blurhash/src/Base83.php | 39 + 3rdparty/kornrunner/blurhash/src/Blurhash.php | 139 + 3rdparty/kornrunner/blurhash/src/Color.php | 20 + 3rdparty/kornrunner/blurhash/src/DC.php | 24 + .../laravel/serializable-closure/LICENSE.md | 21 + .../src/Contracts/Serializable.php | 20 + .../src/Contracts/Signer.php | 22 + .../Exceptions/InvalidSignatureException.php | 19 + .../Exceptions/MissingSecretKeyException.php | 19 + .../PhpVersionNotSupportedException.php | 19 + .../src/SerializableClosure.php | 126 + .../src/Serializers/Native.php | 526 ++ .../src/Serializers/Signed.php | 91 + .../serializable-closure/src/Signers/Hmac.php | 53 + .../src/Support/ClosureScope.php | 22 + .../src/Support/ClosureStream.php | 181 + .../src/Support/ReflectionClosure.php | 1243 ++++ .../src/Support/SelfReference.php | 24 + .../src/UnsignedSerializableClosure.php | 69 + 3rdparty/lcobucci/clock/LICENSE | 21 + 3rdparty/lcobucci/clock/src/Clock.php | 12 + 3rdparty/lcobucci/clock/src/FrozenClock.php | 29 + 3rdparty/lcobucci/clock/src/SystemClock.php | 31 + 3rdparty/marc-mabe/php-enum/LICENSE.txt | 27 + 3rdparty/marc-mabe/php-enum/src/Enum.php | 484 ++ 3rdparty/marc-mabe/php-enum/src/EnumMap.php | 393 + .../php-enum/src/EnumSerializableTrait.php | 110 + 3rdparty/marc-mabe/php-enum/src/EnumSet.php | 1193 +++ .../marc-mabe/php-enum/stubs/Stringable.php | 11 + 3rdparty/masterminds/html5/LICENSE.txt | 66 + 3rdparty/masterminds/html5/src/HTML5.php | 246 + .../masterminds/html5/src/HTML5/Elements.php | 637 ++ .../masterminds/html5/src/HTML5/Entities.php | 2236 ++++++ .../masterminds/html5/src/HTML5/Exception.php | 10 + .../html5/src/HTML5/InstructionProcessor.php | 41 + .../src/HTML5/Parser/CharacterReference.php | 61 + .../html5/src/HTML5/Parser/DOMTreeBuilder.php | 715 ++ .../html5/src/HTML5/Parser/EventHandler.php | 114 + .../src/HTML5/Parser/FileInputStream.php | 33 + .../html5/src/HTML5/Parser/InputStream.php | 87 + .../html5/src/HTML5/Parser/ParseError.php | 10 + .../html5/src/HTML5/Parser/Scanner.php | 416 ++ .../src/HTML5/Parser/StringInputStream.php | 336 + .../html5/src/HTML5/Parser/Tokenizer.php | 1214 +++ .../src/HTML5/Parser/TreeBuildingRules.php | 127 + .../html5/src/HTML5/Parser/UTF8Utils.php | 177 + .../src/HTML5/Serializer/HTML5Entities.php | 1533 ++++ .../src/HTML5/Serializer/OutputRules.php | 553 ++ .../src/HTML5/Serializer/RulesInterface.php | 99 + .../html5/src/HTML5/Serializer/Traverser.php | 142 + 3rdparty/mexitek/phpcolors/LICENSE | 21 + .../phpcolors/src/Mexitek/PHPColors/Color.php | 801 ++ .../azure-storage-blob/BreakingChanges.md | 5 + .../azure-storage-blob/CONTRIBUTING.md | 1 + .../microsoft/azure-storage-blob/ChangeLog.md | 58 + 3rdparty/microsoft/azure-storage-blob/LICENSE | 21 + .../src/Blob/BlobRestProxy.php | 4788 ++++++++++++ .../Blob/BlobSharedAccessSignatureHelper.php | 213 + .../src/Blob/Internal/BlobResources.php | 110 + .../src/Blob/Internal/IBlob.php | 1665 +++++ .../src/Blob/Models/AccessCondition.php | 355 + .../src/Blob/Models/AccessTierTrait.php | 70 + .../src/Blob/Models/AppendBlockOptions.php | 108 + .../src/Blob/Models/AppendBlockResult.php | 244 + .../src/Blob/Models/Blob.php | 153 + .../src/Blob/Models/BlobAccessPolicy.php | 61 + .../src/Blob/Models/BlobBlockType.php | 64 + .../src/Blob/Models/BlobPrefix.php | 62 + .../src/Blob/Models/BlobProperties.php | 887 +++ .../src/Blob/Models/BlobServiceOptions.php | 92 + .../src/Blob/Models/BlobType.php | 42 + .../src/Blob/Models/Block.php | 97 + .../src/Blob/Models/BlockList.php | 172 + .../src/Blob/Models/BreakLeaseResult.php | 83 + .../Blob/Models/CommitBlobBlocksOptions.php | 224 + .../src/Blob/Models/Container.php | 131 + .../src/Blob/Models/ContainerACL.php | 164 + .../src/Blob/Models/ContainerAccessPolicy.php | 61 + .../src/Blob/Models/ContainerProperties.php | 184 + .../Blob/Models/CopyBlobFromURLOptions.php | 138 + .../src/Blob/Models/CopyBlobOptions.php | 62 + .../src/Blob/Models/CopyBlobResult.php | 179 + .../src/Blob/Models/CopyState.php | 294 + .../Blob/Models/CreateBlobBlockOptions.php | 101 + .../src/Blob/Models/CreateBlobOptions.php | 247 + .../Blob/Models/CreateBlobPagesOptions.php | 62 + .../src/Blob/Models/CreateBlobPagesResult.php | 207 + .../Blob/Models/CreateBlobSnapshotOptions.php | 62 + .../Blob/Models/CreateBlobSnapshotResult.php | 139 + .../Blob/Models/CreateBlockBlobOptions.php | 42 + .../Blob/Models/CreateContainerOptions.php | 113 + .../CreatePageBlobFromContentOptions.php | 42 + .../src/Blob/Models/CreatePageBlobOptions.php | 40 + .../src/Blob/Models/DeleteBlobOptions.php | 88 + .../Blob/Models/GetBlobMetadataOptions.php | 62 + .../src/Blob/Models/GetBlobMetadataResult.php | 54 + .../src/Blob/Models/GetBlobOptions.php | 112 + .../Blob/Models/GetBlobPropertiesOptions.php | 62 + .../Blob/Models/GetBlobPropertiesResult.php | 84 + .../src/Blob/Models/GetBlobResult.php | 134 + .../src/Blob/Models/GetContainerACLResult.php | 137 + .../Models/GetContainerPropertiesResult.php | 175 + .../src/Blob/Models/LeaseMode.php | 44 + .../src/Blob/Models/LeaseResult.php | 85 + .../src/Blob/Models/ListBlobBlocksOptions.php | 141 + .../src/Blob/Models/ListBlobBlocksResult.php | 252 + .../src/Blob/Models/ListBlobsOptions.php | 236 + .../src/Blob/Models/ListBlobsResult.php | 313 + .../src/Blob/Models/ListContainersOptions.php | 126 + .../src/Blob/Models/ListContainersResult.php | 251 + .../Models/ListPageBlobRangesDiffResult.php | 101 + .../Blob/Models/ListPageBlobRangesOptions.php | 90 + .../Blob/Models/ListPageBlobRangesResult.php | 182 + .../src/Blob/Models/PageWriteOption.php | 41 + .../src/Blob/Models/PublicAccessType.php | 67 + .../src/Blob/Models/PutBlobResult.php | 182 + .../src/Blob/Models/PutBlockResult.php | 118 + .../src/Blob/Models/SetBlobMetadataResult.php | 151 + .../Blob/Models/SetBlobPropertiesOptions.php | 252 + .../Blob/Models/SetBlobPropertiesResult.php | 144 + .../src/Blob/Models/SetBlobTierOptions.php | 42 + .../src/Blob/Models/UndeleteBlobOptions.php | 39 + .../azure-storage-common/BreakingChanges.md | 10 + .../azure-storage-common/CONTRIBUTING.md | 1 + .../azure-storage-common/ChangeLog.md | 57 + .../microsoft/azure-storage-common/LICENSE | 21 + .../src/Common/CloudConfigurationManager.php | 155 + .../InvalidArgumentTypeException.php | 55 + .../Common/Exceptions/ServiceException.php | 177 + .../src/Common/Internal/ACLBase.php | 260 + .../Internal/Authentication/IAuthScheme.php | 52 + .../SharedAccessSignatureAuthScheme.php | 96 + .../Authentication/SharedKeyAuthScheme.php | 317 + .../Authentication/TokenAuthScheme.php | 73 + .../Internal/ConnectionStringParser.php | 335 + .../Internal/ConnectionStringSource.php | 83 + .../Common/Internal/Http/HttpCallContext.php | 432 ++ .../Common/Internal/Http/HttpFormatter.php | 59 + .../src/Common/Internal/MetadataTrait.php | 142 + .../Middlewares/CommonRequestMiddleware.php | 132 + .../src/Common/Internal/Resources.php | 422 ++ .../src/Common/Internal/RestProxy.php | 131 + .../Internal/Serialization/ISerializer.php | 70 + .../Internal/Serialization/JsonSerializer.php | 96 + .../Serialization/MessageSerializer.php | 178 + .../Internal/Serialization/XmlSerializer.php | 245 + .../src/Common/Internal/ServiceRestProxy.php | 658 ++ .../src/Common/Internal/ServiceRestTrait.php | 259 + .../src/Common/Internal/ServiceSettings.php | 288 + .../Internal/StorageServiceSettings.php | 713 ++ .../src/Common/Internal/Utilities.php | 907 +++ .../src/Common/Internal/Validate.php | 461 ++ .../src/Common/LocationMode.php | 53 + .../src/Common/Logger.php | 77 + .../Common/MarkerContinuationTokenTrait.php | 109 + .../Common/Middlewares/HistoryMiddleware.php | 200 + .../src/Common/Middlewares/IMiddleware.php | 71 + .../src/Common/Middlewares/MiddlewareBase.php | 115 + .../Common/Middlewares/MiddlewareStack.php | 70 + .../Common/Middlewares/RetryMiddleware.php | 181 + .../Middlewares/RetryMiddlewareFactory.php | 271 + .../src/Common/Models/AccessPolicy.php | 218 + .../src/Common/Models/CORS.php | 269 + .../src/Common/Models/ContinuationToken.php | 82 + .../Models/GetServicePropertiesResult.php | 78 + .../Common/Models/GetServiceStatsResult.php | 118 + .../src/Common/Models/Logging.php | 198 + .../Common/Models/MarkerContinuationToken.php | 72 + .../src/Common/Models/Metrics.php | 181 + .../src/Common/Models/Range.php | 142 + .../src/Common/Models/RangeDiff.php | 75 + .../src/Common/Models/RetentionPolicy.php | 124 + .../src/Common/Models/ServiceOptions.php | 309 + .../src/Common/Models/ServiceProperties.php | 279 + .../src/Common/Models/SignedIdentifier.php | 118 + .../Common/Models/TransactionalMD5Trait.php | 64 + .../Common/SharedAccessSignatureHelper.php | 331 + 3rdparty/mlocati/ip-lib/LICENSE.txt | 20 + 3rdparty/mlocati/ip-lib/ip-lib.php | 13 + .../ip-lib/src/Address/AddressInterface.php | 182 + .../ip-lib/src/Address/AssignedRange.php | 140 + 3rdparty/mlocati/ip-lib/src/Address/IPv4.php | 573 ++ 3rdparty/mlocati/ip-lib/src/Address/IPv6.php | 666 ++ 3rdparty/mlocati/ip-lib/src/Address/Type.php | 44 + 3rdparty/mlocati/ip-lib/src/Factory.php | 298 + .../mlocati/ip-lib/src/ParseStringFlag.php | 79 + .../ip-lib/src/Range/AbstractRange.php | 170 + 3rdparty/mlocati/ip-lib/src/Range/Pattern.php | 324 + .../ip-lib/src/Range/RangeInterface.php | 186 + 3rdparty/mlocati/ip-lib/src/Range/Single.php | 254 + 3rdparty/mlocati/ip-lib/src/Range/Subnet.php | 353 + 3rdparty/mlocati/ip-lib/src/Range/Type.php | 152 + .../mlocati/ip-lib/src/Service/BinaryMath.php | 120 + .../Service/RangesFromBoundaryCalculator.php | 168 + .../src/Service/UnsignedIntegerMath.php | 171 + 3rdparty/mtdowling/jmespath.php/LICENSE | 19 + .../mtdowling/jmespath.php/src/AstRuntime.php | 47 + .../jmespath.php/src/CompilerRuntime.php | 83 + .../jmespath.php/src/DebugRuntime.php | 109 + 3rdparty/mtdowling/jmespath.php/src/Env.php | 91 + .../jmespath.php/src/FnDispatcher.php | 407 ++ .../mtdowling/jmespath.php/src/JmesPath.php | 17 + 3rdparty/mtdowling/jmespath.php/src/Lexer.php | 444 ++ .../mtdowling/jmespath.php/src/Parser.php | 519 ++ .../jmespath.php/src/SyntaxErrorException.php | 36 + .../jmespath.php/src/TreeCompiler.php | 419 ++ .../jmespath.php/src/TreeInterpreter.php | 235 + 3rdparty/mtdowling/jmespath.php/src/Utils.php | 258 + .../constant_time_encoding/LICENSE.txt | 48 + .../constant_time_encoding/src/Base32.php | 519 ++ .../constant_time_encoding/src/Base32Hex.php | 111 + .../constant_time_encoding/src/Base64.php | 314 + .../src/Base64DotSlash.php | 88 + .../src/Base64DotSlashOrdered.php | 82 + .../src/Base64UrlSafe.php | 95 + .../constant_time_encoding/src/Binary.php | 90 + .../src/EncoderInterface.php | 52 + .../constant_time_encoding/src/Encoding.php | 262 + .../constant_time_encoding/src/Hex.php | 146 + .../constant_time_encoding/src/RFC4648.php | 186 + 3rdparty/pear/archive_tar/Archive/Tar.php | 2530 +++++++ 3rdparty/pear/archive_tar/package.xml | 749 ++ .../pear/console_getopt/Console/Getopt.php | 365 + 3rdparty/pear/console_getopt/LICENSE | 25 + 3rdparty/pear/console_getopt/package.xml | 302 + .../pear/pear-core-minimal/src/OS/Guess.php | 395 + 3rdparty/pear/pear-core-minimal/src/PEAR.php | 1138 +++ .../pear-core-minimal/src/PEAR/ErrorStack.php | 979 +++ .../pear/pear-core-minimal/src/System.php | 630 ++ 3rdparty/pear/pear_exception/LICENSE | 27 + .../pear/pear_exception/PEAR/Exception.php | 456 ++ 3rdparty/php-http/guzzle7-adapter/LICENSE | 19 + .../php-http/guzzle7-adapter/src/Client.php | 69 + .../Exception/UnexpectedValueException.php | 9 + .../php-http/guzzle7-adapter/src/Promise.php | 123 + 3rdparty/php-http/httplug/LICENSE | 20 + 3rdparty/php-http/httplug/puli.json | 12 + 3rdparty/php-http/httplug/src/Exception.php | 14 + .../httplug/src/Exception/HttpException.php | 65 + .../src/Exception/NetworkException.php | 28 + .../src/Exception/RequestAwareTrait.php | 23 + .../src/Exception/RequestException.php | 29 + .../src/Exception/TransferException.php | 14 + .../php-http/httplug/src/HttpAsyncClient.php | 25 + 3rdparty/php-http/httplug/src/HttpClient.php | 17 + .../src/Promise/HttpFulfilledPromise.php | 45 + .../src/Promise/HttpRejectedPromise.php | 49 + 3rdparty/php-http/promise/LICENSE | 19 + .../php-http/promise/src/FulfilledPromise.php | 51 + 3rdparty/php-http/promise/src/Promise.php | 69 + .../php-http/promise/src/RejectedPromise.php | 48 + 3rdparty/php-opencloud/openstack/LICENSE | 201 + .../openstack/src/BlockStorage/v2/Api.php | 349 + .../src/BlockStorage/v2/Models/QuotaSet.php | 78 + .../src/BlockStorage/v2/Models/Snapshot.php | 130 + .../src/BlockStorage/v2/Models/Volume.php | 184 + .../v2/Models/VolumeAttachment.php | 29 + .../src/BlockStorage/v2/Models/VolumeType.php | 53 + .../openstack/src/BlockStorage/v2/Params.php | 280 + .../openstack/src/BlockStorage/v2/Service.php | 115 + .../openstack/src/BlockStorage/v3/Api.php | 7 + .../openstack/src/BlockStorage/v3/Service.php | 7 + .../openstack/src/Common/Api/AbstractApi.php | 35 + .../src/Common/Api/AbstractParams.php | 113 + .../openstack/src/Common/Api/ApiInterface.php | 20 + .../openstack/src/Common/Api/Operation.php | 132 + .../src/Common/Api/OperatorInterface.php | 56 + .../src/Common/Api/OperatorTrait.php | 144 + .../openstack/src/Common/Api/Parameter.php | 373 + .../openstack/src/Common/ArrayAccessTrait.php | 58 + .../openstack/src/Common/Auth/AuthHandler.php | 72 + .../openstack/src/Common/Auth/Catalog.php | 22 + .../src/Common/Auth/IdentityService.php | 15 + .../openstack/src/Common/Auth/Token.php | 17 + .../src/Common/Error/BadResponseError.php | 40 + .../openstack/src/Common/Error/BaseError.php | 12 + .../openstack/src/Common/Error/Builder.php | 187 + .../src/Common/Error/NotImplementedError.php | 12 + .../src/Common/Error/UserInputError.php | 12 + .../src/Common/HydratorStrategyTrait.php | 35 + .../openstack/src/Common/JsonPath.php | 109 + .../src/Common/JsonSchema/JsonPatch.php | 120 + .../src/Common/JsonSchema/Schema.php | 78 + .../src/Common/Resource/AbstractResource.php | 162 + .../openstack/src/Common/Resource/Alias.php | 53 + .../src/Common/Resource/Creatable.php | 18 + .../src/Common/Resource/Deletable.php | 16 + .../src/Common/Resource/HasMetadata.php | 59 + .../src/Common/Resource/HasWaiterTrait.php | 123 + .../src/Common/Resource/Iterator.php | 99 + .../src/Common/Resource/Listable.php | 28 + .../src/Common/Resource/OperatorResource.php | 148 + .../src/Common/Resource/ResourceInterface.php | 37 + .../src/Common/Resource/Retrievable.php | 16 + .../src/Common/Resource/Updateable.php | 16 + .../src/Common/Service/AbstractService.php | 15 + .../openstack/src/Common/Service/Builder.php | 144 + .../src/Common/Service/ServiceInterface.php | 14 + .../src/Common/Transport/HandlerStack.php | 17 + .../Common/Transport/HandlerStackFactory.php | 58 + .../src/Common/Transport/JsonSerializer.php | 107 + .../src/Common/Transport/Middleware.php | 88 + .../Common/Transport/RequestSerializer.php | 92 + .../src/Common/Transport/Serializable.php | 11 + .../openstack/src/Common/Transport/Utils.php | 99 + .../openstack/src/Compute/v2/Api.php | 916 +++ .../openstack/src/Compute/v2/Enum.php | 19 + .../Compute/v2/Models/AvailabilityZone.php | 33 + .../openstack/src/Compute/v2/Models/Fault.php | 25 + .../src/Compute/v2/Models/Flavor.php | 61 + .../openstack/src/Compute/v2/Models/Host.php | 39 + .../src/Compute/v2/Models/Hypervisor.php | 102 + .../Compute/v2/Models/HypervisorStatistic.php | 29 + .../openstack/src/Compute/v2/Models/Image.php | 145 + .../src/Compute/v2/Models/Keypair.php | 92 + .../openstack/src/Compute/v2/Models/Limit.php | 23 + .../src/Compute/v2/Models/QuotaSet.php | 167 + .../src/Compute/v2/Models/Server.php | 585 ++ .../openstack/src/Compute/v2/Params.php | 665 ++ .../openstack/src/Compute/v2/Service.php | 278 + .../openstack/src/Identity/v2/Api.php | 42 + .../src/Identity/v2/Models/Catalog.php | 58 + .../src/Identity/v2/Models/Endpoint.php | 86 + .../src/Identity/v2/Models/Entry.php | 54 + .../src/Identity/v2/Models/Tenant.php | 14 + .../src/Identity/v2/Models/Token.php | 54 + .../openstack/src/Identity/v2/Service.php | 54 + .../openstack/src/Identity/v3/Api.php | 882 +++ .../openstack/src/Identity/v3/Enum.php | 12 + .../v3/Models/ApplicationCredential.php | 69 + .../src/Identity/v3/Models/Assignment.php | 36 + .../src/Identity/v3/Models/Catalog.php | 58 + .../src/Identity/v3/Models/Credential.php | 65 + .../src/Identity/v3/Models/Domain.php | 148 + .../src/Identity/v3/Models/Endpoint.php | 79 + .../src/Identity/v3/Models/Group.php | 108 + .../src/Identity/v3/Models/Policy.php | 71 + .../src/Identity/v3/Models/Project.php | 160 + .../openstack/src/Identity/v3/Models/Role.php | 43 + .../src/Identity/v3/Models/Service.php | 109 + .../src/Identity/v3/Models/Token.php | 150 + .../openstack/src/Identity/v3/Models/User.php | 113 + .../openstack/src/Identity/v3/Params.php | 336 + .../openstack/src/Identity/v3/Service.php | 430 ++ .../openstack/src/Images/v2/Api.php | 196 + .../openstack/src/Images/v2/JsonPatch.php | 66 + .../openstack/src/Images/v2/Models/Image.php | 225 + .../openstack/src/Images/v2/Models/Member.php | 77 + .../openstack/src/Images/v2/Models/Schema.php | 25 + .../openstack/src/Images/v2/Params.php | 214 + .../openstack/src/Images/v2/Service.php | 38 + .../openstack/src/Metric/v1/Gnocchi/Api.php | 133 + .../src/Metric/v1/Gnocchi/Models/Metric.php | 55 + .../src/Metric/v1/Gnocchi/Models/Resource.php | 150 + .../Metric/v1/Gnocchi/Models/ResourceType.php | 19 + .../src/Metric/v1/Gnocchi/Params.php | 95 + .../src/Metric/v1/Gnocchi/Service.php | 122 + .../openstack/src/Networking/v2/Api.php | 711 ++ .../Networking/v2/Extensions/Layer3/Api.php | 180 + .../v2/Extensions/Layer3/ApiTrait.php | 175 + .../v2/Extensions/Layer3/Models/FixedIp.php | 19 + .../Extensions/Layer3/Models/FloatingIp.php | 88 + .../Extensions/Layer3/Models/GatewayInfo.php | 30 + .../v2/Extensions/Layer3/Models/Router.php | 101 + .../v2/Extensions/Layer3/Params.php | 130 + .../v2/Extensions/Layer3/ParamsTrait.php | 130 + .../v2/Extensions/Layer3/Service.php | 61 + .../v2/Extensions/Layer3/ServiceTrait.php | 62 + .../v2/Extensions/SecurityGroups/Api.php | 182 + .../v2/Extensions/SecurityGroups/ApiTrait.php | 158 + .../SecurityGroups/Models/SecurityGroup.php | 88 + .../Models/SecurityGroupRule.php | 103 + .../v2/Extensions/SecurityGroups/Params.php | 106 + .../Extensions/SecurityGroups/ParamsTrait.php | 97 + .../v2/Extensions/SecurityGroups/Service.php | 63 + .../SecurityGroups/ServiceTrait.php | 62 + .../v2/Models/InterfaceAttachment.php | 52 + .../src/Networking/v2/Models/LoadBalancer.php | 146 + .../v2/Models/LoadBalancerHealthMonitor.php | 137 + .../v2/Models/LoadBalancerListener.php | 137 + .../v2/Models/LoadBalancerMember.php | 108 + .../Networking/v2/Models/LoadBalancerPool.php | 174 + .../Networking/v2/Models/LoadBalancerStat.php | 57 + .../v2/Models/LoadBalancerStatus.php | 70 + .../src/Networking/v2/Models/Network.php | 96 + .../src/Networking/v2/Models/Port.php | 169 + .../src/Networking/v2/Models/Quota.php | 96 + .../src/Networking/v2/Models/Subnet.php | 122 + .../openstack/src/Networking/v2/Params.php | 736 ++ .../openstack/src/Networking/v2/Service.php | 332 + .../openstack/src/ObjectStore/v1/Api.php | 242 + .../src/ObjectStore/v1/Models/Account.php | 84 + .../src/ObjectStore/v1/Models/Container.php | 247 + .../ObjectStore/v1/Models/MetadataTrait.php | 24 + .../ObjectStore/v1/Models/StorageObject.php | 187 + .../openstack/src/ObjectStore/v1/Params.php | 504 ++ .../openstack/src/ObjectStore/v1/Service.php | 83 + .../php-opencloud/openstack/src/OpenStack.php | 203 + 3rdparty/phpseclib/phpseclib/AUTHORS | 7 + 3rdparty/phpseclib/phpseclib/LICENSE | 20 + .../phpseclib/phpseclib/Crypt/AES.php | 96 + .../phpseclib/phpseclib/Crypt/Base.php | 2909 ++++++++ .../phpseclib/phpseclib/Crypt/Blowfish.php | 993 +++ .../phpseclib/phpseclib/Crypt/DES.php | 1449 ++++ .../phpseclib/phpseclib/Crypt/Hash.php | 893 +++ .../phpseclib/phpseclib/Crypt/RC2.php | 694 ++ .../phpseclib/phpseclib/Crypt/RC4.php | 348 + .../phpseclib/phpseclib/Crypt/RSA.php | 3343 +++++++++ .../phpseclib/phpseclib/Crypt/Random.php | 280 + .../phpseclib/phpseclib/Crypt/Rijndael.php | 939 +++ .../phpseclib/phpseclib/Crypt/TripleDES.php | 460 ++ .../phpseclib/phpseclib/Crypt/Twofish.php | 852 +++ .../phpseclib/phpseclib/File/ANSI.php | 577 ++ .../phpseclib/phpseclib/File/ASN1.php | 1474 ++++ .../phpseclib/phpseclib/File/ASN1/Element.php | 47 + .../phpseclib/phpseclib/File/X509.php | 5121 +++++++++++++ .../phpseclib/phpseclib/Math/BigInteger.php | 3826 ++++++++++ .../phpseclib/phpseclib/phpseclib/Net/SCP.php | 349 + .../phpseclib/phpseclib/Net/SFTP.php | 3898 ++++++++++ .../phpseclib/phpseclib/Net/SFTP/Stream.php | 796 ++ .../phpseclib/phpseclib/Net/SSH1.php | 1662 +++++ .../phpseclib/phpseclib/Net/SSH2.php | 5529 ++++++++++++++ .../phpseclib/phpseclib/System/SSH/Agent.php | 361 + .../phpseclib/System/SSH/Agent/Identity.php | 241 + .../phpseclib/phpseclib/bootstrap.php | 17 + .../phpseclib/phpseclib/phpseclib/openssl.cnf | 6 + 3rdparty/pimple/pimple/LICENSE | 19 + .../pimple/pimple/src/Pimple/Container.php | 305 + .../Exception/ExpectedInvokableException.php | 38 + .../Exception/FrozenServiceException.php | 45 + .../InvalidServiceIdentifierException.php | 45 + .../Exception/UnknownIdentifierException.php | 45 + .../pimple/src/Pimple/Psr11/Container.php | 55 + .../src/Pimple/Psr11/ServiceLocator.php | 75 + .../pimple/src/Pimple/ServiceIterator.php | 89 + .../src/Pimple/ServiceProviderInterface.php | 44 + 3rdparty/psr/cache/LICENSE.txt | 19 + 3rdparty/psr/cache/src/CacheException.php | 10 + 3rdparty/psr/cache/src/CacheItemInterface.php | 105 + .../psr/cache/src/CacheItemPoolInterface.php | 138 + .../cache/src/InvalidArgumentException.php | 13 + 3rdparty/psr/clock/LICENSE | 19 + 3rdparty/psr/clock/src/ClockInterface.php | 13 + 3rdparty/psr/container/LICENSE | 21 + .../src/ContainerExceptionInterface.php | 12 + .../psr/container/src/ContainerInterface.php | 36 + .../src/NotFoundExceptionInterface.php | 10 + 3rdparty/psr/event-dispatcher/LICENSE | 21 + .../src/EventDispatcherInterface.php | 21 + .../src/ListenerProviderInterface.php | 19 + .../src/StoppableEventInterface.php | 26 + 3rdparty/psr/http-client/LICENSE | 19 + .../src/ClientExceptionInterface.php | 10 + .../psr/http-client/src/ClientInterface.php | 20 + .../src/NetworkExceptionInterface.php | 24 + .../src/RequestExceptionInterface.php | 24 + 3rdparty/psr/http-factory/LICENSE | 21 + .../src/RequestFactoryInterface.php | 18 + .../src/ResponseFactoryInterface.php | 18 + .../src/ServerRequestFactoryInterface.php | 24 + .../src/StreamFactoryInterface.php | 45 + .../src/UploadedFileFactoryInterface.php | 34 + .../http-factory/src/UriFactoryInterface.php | 17 + 3rdparty/psr/http-message/LICENSE | 19 + .../psr/http-message/src/MessageInterface.php | 187 + .../psr/http-message/src/RequestInterface.php | 130 + .../http-message/src/ResponseInterface.php | 68 + .../src/ServerRequestInterface.php | 261 + .../psr/http-message/src/StreamInterface.php | 158 + .../src/UploadedFileInterface.php | 123 + .../psr/http-message/src/UriInterface.php | 324 + 3rdparty/psr/log/LICENSE | 19 + 3rdparty/psr/log/src/AbstractLogger.php | 15 + .../psr/log/src/InvalidArgumentException.php | 7 + 3rdparty/psr/log/src/LogLevel.php | 18 + 3rdparty/psr/log/src/LoggerAwareInterface.php | 14 + 3rdparty/psr/log/src/LoggerAwareTrait.php | 22 + 3rdparty/psr/log/src/LoggerInterface.php | 98 + 3rdparty/psr/log/src/LoggerTrait.php | 98 + 3rdparty/psr/log/src/NullLogger.php | 26 + .../punic/punic/LIBPHONENUMBER-LICENSE.txt | 176 + 3rdparty/punic/punic/LICENSE.txt | 24 + 3rdparty/punic/punic/UNICODE-LICENSE.txt | 50 + 3rdparty/punic/punic/punic.php | 13 + 3rdparty/punic/punic/src/Calendar.php | 3151 ++++++++ 3rdparty/punic/punic/src/Comparer.php | 171 + 3rdparty/punic/punic/src/Currency.php | 252 + 3rdparty/punic/punic/src/Data.php | 756 ++ 3rdparty/punic/punic/src/Exception.php | 91 + .../punic/src/Exception/BadArgumentType.php | 65 + .../src/Exception/BadDataFileContents.php | 48 + .../punic/src/Exception/DataFileNotFound.php | 72 + .../src/Exception/DataFileNotReadable.php | 34 + .../src/Exception/DataFolderNotFound.php | 52 + .../punic/src/Exception/InvalidDataFile.php | 37 + .../punic/src/Exception/InvalidLocale.php | 37 + .../punic/src/Exception/InvalidOverride.php | 36 + .../punic/src/Exception/NotImplemented.php | 34 + .../punic/src/Exception/ValueNotInList.php | 46 + 3rdparty/punic/punic/src/Language.php | 94 + 3rdparty/punic/punic/src/Misc.php | 320 + 3rdparty/punic/punic/src/Number.php | 442 ++ 3rdparty/punic/punic/src/Phone.php | 77 + 3rdparty/punic/punic/src/Plural.php | 193 + 3rdparty/punic/punic/src/Script.php | 141 + 3rdparty/punic/punic/src/Territory.php | 573 ++ 3rdparty/punic/punic/src/Unit.php | 355 + 3rdparty/ralouphie/getallheaders/LICENSE | 21 + .../getallheaders/src/getallheaders.php | 46 + 3rdparty/sabre/dav/LICENSE | 27 + .../lib/CalDAV/Backend/AbstractBackend.php | 216 + .../lib/CalDAV/Backend/BackendInterface.php | 273 + .../CalDAV/Backend/NotificationSupport.php | 62 + 3rdparty/sabre/dav/lib/CalDAV/Backend/PDO.php | 1487 ++++ .../lib/CalDAV/Backend/SchedulingSupport.php | 66 + .../dav/lib/CalDAV/Backend/SharingSupport.php | 60 + .../dav/lib/CalDAV/Backend/SimplePDO.php | 289 + .../CalDAV/Backend/SubscriptionSupport.php | 89 + .../dav/lib/CalDAV/Backend/SyncSupport.php | 83 + 3rdparty/sabre/dav/lib/CalDAV/Calendar.php | 460 ++ .../sabre/dav/lib/CalDAV/CalendarHome.php | 356 + .../sabre/dav/lib/CalDAV/CalendarObject.php | 223 + .../dav/lib/CalDAV/CalendarQueryValidator.php | 354 + .../sabre/dav/lib/CalDAV/CalendarRoot.php | 75 + .../CalDAV/Exception/InvalidComponentType.php | 31 + .../sabre/dav/lib/CalDAV/ICSExportPlugin.php | 377 + 3rdparty/sabre/dav/lib/CalDAV/ICalendar.php | 20 + .../sabre/dav/lib/CalDAV/ICalendarObject.php | 23 + .../lib/CalDAV/ICalendarObjectContainer.php | 39 + .../sabre/dav/lib/CalDAV/ISharedCalendar.php | 27 + .../lib/CalDAV/Notifications/Collection.php | 96 + .../lib/CalDAV/Notifications/ICollection.php | 25 + .../dav/lib/CalDAV/Notifications/INode.php | 41 + .../dav/lib/CalDAV/Notifications/Node.php | 112 + .../dav/lib/CalDAV/Notifications/Plugin.php | 161 + 3rdparty/sabre/dav/lib/CalDAV/Plugin.php | 1011 +++ .../dav/lib/CalDAV/Principal/Collection.php | 32 + .../dav/lib/CalDAV/Principal/IProxyRead.php | 21 + .../dav/lib/CalDAV/Principal/IProxyWrite.php | 21 + .../dav/lib/CalDAV/Principal/ProxyRead.php | 161 + .../dav/lib/CalDAV/Principal/ProxyWrite.php | 161 + .../sabre/dav/lib/CalDAV/Principal/User.php | 136 + .../sabre/dav/lib/CalDAV/Schedule/IInbox.php | 17 + .../dav/lib/CalDAV/Schedule/IMipPlugin.php | 178 + .../sabre/dav/lib/CalDAV/Schedule/IOutbox.php | 17 + .../lib/CalDAV/Schedule/ISchedulingObject.php | 15 + .../sabre/dav/lib/CalDAV/Schedule/Inbox.php | 198 + .../sabre/dav/lib/CalDAV/Schedule/Outbox.php | 119 + .../sabre/dav/lib/CalDAV/Schedule/Plugin.php | 1006 +++ .../lib/CalDAV/Schedule/SchedulingObject.php | 130 + .../sabre/dav/lib/CalDAV/SharedCalendar.php | 219 + .../sabre/dav/lib/CalDAV/SharingPlugin.php | 346 + .../CalDAV/Subscriptions/ISubscription.php | 41 + .../dav/lib/CalDAV/Subscriptions/Plugin.php | 108 + .../lib/CalDAV/Subscriptions/Subscription.php | 204 + .../lib/CalDAV/Xml/Filter/CalendarData.php | 80 + .../dav/lib/CalDAV/Xml/Filter/CompFilter.php | 94 + .../dav/lib/CalDAV/Xml/Filter/ParamFilter.php | 79 + .../dav/lib/CalDAV/Xml/Filter/PropFilter.php | 95 + .../lib/CalDAV/Xml/Notification/Invite.php | 290 + .../CalDAV/Xml/Notification/InviteReply.php | 199 + .../Notification/NotificationInterface.php | 43 + .../CalDAV/Xml/Notification/SystemStatus.php | 171 + .../Xml/Property/AllowedSharingModes.php | 81 + .../CalDAV/Xml/Property/EmailAddressSet.php | 71 + .../dav/lib/CalDAV/Xml/Property/Invite.php | 120 + .../Xml/Property/ScheduleCalendarTransp.php | 124 + .../SupportedCalendarComponentSet.php | 118 + .../Xml/Property/SupportedCalendarData.php | 57 + .../Xml/Property/SupportedCollationSet.php | 54 + .../Xml/Request/CalendarMultiGetReport.php | 119 + .../Xml/Request/CalendarQueryReport.php | 137 + .../Xml/Request/FreeBusyQueryReport.php | 90 + .../lib/CalDAV/Xml/Request/InviteReply.php | 145 + .../dav/lib/CalDAV/Xml/Request/MkCalendar.php | 77 + .../dav/lib/CalDAV/Xml/Request/Share.php | 107 + .../sabre/dav/lib/CardDAV/AddressBook.php | 335 + .../sabre/dav/lib/CardDAV/AddressBookHome.php | 178 + .../sabre/dav/lib/CardDAV/AddressBookRoot.php | 75 + .../lib/CardDAV/Backend/AbstractBackend.php | 38 + .../lib/CardDAV/Backend/BackendInterface.php | 194 + .../sabre/dav/lib/CardDAV/Backend/PDO.php | 539 ++ .../dav/lib/CardDAV/Backend/SyncSupport.php | 83 + 3rdparty/sabre/dav/lib/CardDAV/Card.php | 202 + .../sabre/dav/lib/CardDAV/IAddressBook.php | 20 + 3rdparty/sabre/dav/lib/CardDAV/ICard.php | 21 + 3rdparty/sabre/dav/lib/CardDAV/IDirectory.php | 22 + 3rdparty/sabre/dav/lib/CardDAV/Plugin.php | 886 +++ .../sabre/dav/lib/CardDAV/VCFExportPlugin.php | 165 + .../lib/CardDAV/Xml/Filter/AddressData.php | 66 + .../lib/CardDAV/Xml/Filter/ParamFilter.php | 86 + .../dav/lib/CardDAV/Xml/Filter/PropFilter.php | 95 + .../Xml/Property/SupportedAddressData.php | 77 + .../Xml/Property/SupportedCollationSet.php | 44 + .../Xml/Request/AddressBookMultiGetReport.php | 116 + .../Xml/Request/AddressBookQueryReport.php | 193 + .../lib/DAV/Auth/Backend/AbstractBasic.php | 136 + .../lib/DAV/Auth/Backend/AbstractBearer.php | 130 + .../lib/DAV/Auth/Backend/AbstractDigest.php | 160 + .../sabre/dav/lib/DAV/Auth/Backend/Apache.php | 93 + .../lib/DAV/Auth/Backend/BackendInterface.php | 65 + .../lib/DAV/Auth/Backend/BasicCallBack.php | 56 + .../sabre/dav/lib/DAV/Auth/Backend/File.php | 74 + .../sabre/dav/lib/DAV/Auth/Backend/IMAP.php | 82 + .../sabre/dav/lib/DAV/Auth/Backend/PDO.php | 55 + .../dav/lib/DAV/Auth/Backend/PDOBasicAuth.php | 114 + 3rdparty/sabre/dav/lib/DAV/Auth/Plugin.php | 255 + .../dav/lib/DAV/Browser/GuessContentType.php | 93 + .../sabre/dav/lib/DAV/Browser/HtmlOutput.php | 34 + .../dav/lib/DAV/Browser/HtmlOutputHelper.php | 118 + .../dav/lib/DAV/Browser/MapGetToPropFind.php | 58 + 3rdparty/sabre/dav/lib/DAV/Browser/Plugin.php | 789 ++ .../sabre/dav/lib/DAV/Browser/PropFindAll.php | 128 + .../dav/lib/DAV/Browser/assets/favicon.ico | Bin 0 -> 4286 bytes .../Browser/assets/openiconic/ICON-LICENSE | 21 + .../Browser/assets/openiconic/open-iconic.css | 510 ++ .../Browser/assets/openiconic/open-iconic.eot | Bin 0 -> 23144 bytes .../Browser/assets/openiconic/open-iconic.otf | Bin 0 -> 21048 bytes .../Browser/assets/openiconic/open-iconic.svg | 543 ++ .../Browser/assets/openiconic/open-iconic.ttf | Bin 0 -> 25568 bytes .../assets/openiconic/open-iconic.woff | Bin 0 -> 12404 bytes .../dav/lib/DAV/Browser/assets/sabredav.css | 228 + .../dav/lib/DAV/Browser/assets/sabredav.png | Bin 0 -> 2825 bytes 3rdparty/sabre/dav/lib/DAV/Client.php | 485 ++ 3rdparty/sabre/dav/lib/DAV/Collection.php | 106 + 3rdparty/sabre/dav/lib/DAV/CorePlugin.php | 907 +++ 3rdparty/sabre/dav/lib/DAV/Exception.php | 50 + .../dav/lib/DAV/Exception/BadRequest.php | 30 + .../sabre/dav/lib/DAV/Exception/Conflict.php | 30 + .../dav/lib/DAV/Exception/ConflictingLock.php | 32 + .../sabre/dav/lib/DAV/Exception/Forbidden.php | 29 + .../lib/DAV/Exception/InsufficientStorage.php | 29 + .../lib/DAV/Exception/InvalidResourceType.php | 29 + .../lib/DAV/Exception/InvalidSyncToken.php | 34 + .../dav/lib/DAV/Exception/LengthRequired.php | 30 + .../Exception/LockTokenMatchesRequestUri.php | 36 + .../sabre/dav/lib/DAV/Exception/Locked.php | 68 + .../lib/DAV/Exception/MethodNotAllowed.php | 46 + .../lib/DAV/Exception/NotAuthenticated.php | 30 + .../sabre/dav/lib/DAV/Exception/NotFound.php | 29 + .../dav/lib/DAV/Exception/NotImplemented.php | 29 + .../dav/lib/DAV/Exception/PaymentRequired.php | 30 + .../lib/DAV/Exception/PreconditionFailed.php | 65 + .../lib/DAV/Exception/ReportNotSupported.php | 28 + .../RequestedRangeNotSatisfiable.php | 30 + .../lib/DAV/Exception/ServiceUnavailable.php | 30 + .../dav/lib/DAV/Exception/TooManyMatches.php | 34 + .../DAV/Exception/UnsupportedMediaType.php | 30 + 3rdparty/sabre/dav/lib/DAV/FS/Directory.php | 147 + 3rdparty/sabre/dav/lib/DAV/FS/File.php | 87 + 3rdparty/sabre/dav/lib/DAV/FS/Node.php | 96 + .../sabre/dav/lib/DAV/FSExt/Directory.php | 212 + 3rdparty/sabre/dav/lib/DAV/FSExt/File.php | 153 + 3rdparty/sabre/dav/lib/DAV/File.php | 93 + 3rdparty/sabre/dav/lib/DAV/ICollection.php | 79 + 3rdparty/sabre/dav/lib/DAV/ICopyTarget.php | 38 + .../sabre/dav/lib/DAV/IExtendedCollection.php | 43 + 3rdparty/sabre/dav/lib/DAV/IFile.php | 83 + 3rdparty/sabre/dav/lib/DAV/IMoveTarget.php | 46 + 3rdparty/sabre/dav/lib/DAV/IMultiGet.php | 38 + 3rdparty/sabre/dav/lib/DAV/INode.php | 44 + 3rdparty/sabre/dav/lib/DAV/INodeByPath.php | 36 + 3rdparty/sabre/dav/lib/DAV/IProperties.php | 46 + 3rdparty/sabre/dav/lib/DAV/IQuota.php | 27 + .../lib/DAV/Locks/Backend/AbstractBackend.php | 20 + .../DAV/Locks/Backend/BackendInterface.php | 52 + .../sabre/dav/lib/DAV/Locks/Backend/File.php | 182 + .../sabre/dav/lib/DAV/Locks/Backend/PDO.php | 172 + 3rdparty/sabre/dav/lib/DAV/Locks/LockInfo.php | 82 + 3rdparty/sabre/dav/lib/DAV/Locks/Plugin.php | 550 ++ 3rdparty/sabre/dav/lib/DAV/MkCol.php | 71 + 3rdparty/sabre/dav/lib/DAV/Mount/Plugin.php | 78 + 3rdparty/sabre/dav/lib/DAV/Node.php | 51 + .../lib/DAV/PartialUpdate/IPatchSupport.php | 49 + .../dav/lib/DAV/PartialUpdate/Plugin.php | 212 + 3rdparty/sabre/dav/lib/DAV/PropFind.php | 335 + 3rdparty/sabre/dav/lib/DAV/PropPatch.php | 337 + .../Backend/BackendInterface.php | 75 + .../lib/DAV/PropertyStorage/Backend/PDO.php | 224 + .../dav/lib/DAV/PropertyStorage/Plugin.php | 176 + 3rdparty/sabre/dav/lib/DAV/Server.php | 1682 +++++ 3rdparty/sabre/dav/lib/DAV/ServerPlugin.php | 105 + .../sabre/dav/lib/DAV/Sharing/ISharedNode.php | 69 + 3rdparty/sabre/dav/lib/DAV/Sharing/Plugin.php | 312 + .../sabre/dav/lib/DAV/SimpleCollection.php | 109 + 3rdparty/sabre/dav/lib/DAV/SimpleFile.php | 118 + 3rdparty/sabre/dav/lib/DAV/StringUtil.php | 86 + .../dav/lib/DAV/Sync/ISyncCollection.php | 90 + 3rdparty/sabre/dav/lib/DAV/Sync/Plugin.php | 249 + .../dav/lib/DAV/TemporaryFileFilterPlugin.php | 298 + 3rdparty/sabre/dav/lib/DAV/Tree.php | 342 + 3rdparty/sabre/dav/lib/DAV/UUIDUtil.php | 66 + 3rdparty/sabre/dav/lib/DAV/Version.php | 20 + .../sabre/dav/lib/DAV/Xml/Element/Prop.php | 110 + .../dav/lib/DAV/Xml/Element/Response.php | 267 + .../sabre/dav/lib/DAV/Xml/Element/Sharee.php | 189 + .../dav/lib/DAV/Xml/Property/Complex.php | 87 + .../lib/DAV/Xml/Property/GetLastModified.php | 103 + .../sabre/dav/lib/DAV/Xml/Property/Href.php | 166 + .../sabre/dav/lib/DAV/Xml/Property/Invite.php | 66 + .../dav/lib/DAV/Xml/Property/LocalHref.php | 48 + .../lib/DAV/Xml/Property/LockDiscovery.php | 105 + .../dav/lib/DAV/Xml/Property/ResourceType.php | 120 + .../dav/lib/DAV/Xml/Property/ShareAccess.php | 135 + .../lib/DAV/Xml/Property/SupportedLock.php | 52 + .../DAV/Xml/Property/SupportedMethodSet.php | 114 + .../DAV/Xml/Property/SupportedReportSet.php | 144 + .../sabre/dav/lib/DAV/Xml/Request/Lock.php | 84 + .../sabre/dav/lib/DAV/Xml/Request/MkCol.php | 80 + .../dav/lib/DAV/Xml/Request/PropFind.php | 79 + .../dav/lib/DAV/Xml/Request/PropPatch.php | 109 + .../dav/lib/DAV/Xml/Request/ShareResource.php | 80 + .../DAV/Xml/Request/SyncCollectionReport.php | 118 + .../dav/lib/DAV/Xml/Response/MultiStatus.php | 136 + 3rdparty/sabre/dav/lib/DAV/Xml/Service.php | 47 + 3rdparty/sabre/dav/lib/DAVACL/ACLTrait.php | 94 + .../DAVACL/AbstractPrincipalCollection.php | 178 + .../dav/lib/DAVACL/Exception/AceConflict.php | 31 + .../lib/DAVACL/Exception/NeedPrivileges.php | 73 + .../dav/lib/DAVACL/Exception/NoAbstract.php | 31 + .../Exception/NotRecognizedPrincipal.php | 31 + .../Exception/NotSupportedPrivilege.php | 31 + .../sabre/dav/lib/DAVACL/FS/Collection.php | 109 + 3rdparty/sabre/dav/lib/DAVACL/FS/File.php | 78 + .../dav/lib/DAVACL/FS/HomeCollection.php | 123 + 3rdparty/sabre/dav/lib/DAVACL/IACL.php | 72 + 3rdparty/sabre/dav/lib/DAVACL/IPrincipal.php | 75 + .../dav/lib/DAVACL/IPrincipalCollection.php | 64 + 3rdparty/sabre/dav/lib/DAVACL/Plugin.php | 1545 ++++ 3rdparty/sabre/dav/lib/DAVACL/Principal.php | 199 + .../PrincipalBackend/AbstractBackend.php | 54 + .../PrincipalBackend/BackendInterface.php | 143 + .../CreatePrincipalSupport.php | 29 + .../dav/lib/DAVACL/PrincipalBackend/PDO.php | 443 ++ .../dav/lib/DAVACL/PrincipalCollection.php | 96 + .../sabre/dav/lib/DAVACL/Xml/Property/Acl.php | 257 + .../DAVACL/Xml/Property/AclRestrictions.php | 42 + .../Xml/Property/CurrentUserPrivilegeSet.php | 145 + .../dav/lib/DAVACL/Xml/Property/Principal.php | 186 + .../Xml/Property/SupportedPrivilegeSet.php | 146 + .../Xml/Request/AclPrincipalPropSetReport.php | 66 + .../Xml/Request/ExpandPropertyReport.php | 100 + .../Xml/Request/PrincipalMatchReport.php | 106 + .../Request/PrincipalPropertySearchReport.php | 122 + .../PrincipalSearchPropertySetReport.php | 58 + 3rdparty/sabre/event/LICENSE | 27 + 3rdparty/sabre/event/lib/Emitter.php | 19 + 3rdparty/sabre/event/lib/EmitterInterface.php | 78 + 3rdparty/sabre/event/lib/EmitterTrait.php | 178 + 3rdparty/sabre/event/lib/EventEmitter.php | 20 + 3rdparty/sabre/event/lib/Loop/Loop.php | 343 + 3rdparty/sabre/event/lib/Loop/functions.php | 143 + 3rdparty/sabre/event/lib/Promise.php | 253 + .../sabre/event/lib/Promise/functions.php | 125 + .../lib/PromiseAlreadyResolvedException.php | 17 + 3rdparty/sabre/event/lib/Version.php | 20 + 3rdparty/sabre/event/lib/WildcardEmitter.php | 36 + .../sabre/event/lib/WildcardEmitterTrait.php | 233 + 3rdparty/sabre/event/lib/coroutine.php | 122 + 3rdparty/sabre/http/LICENSE | 27 + 3rdparty/sabre/http/lib/Auth/AWS.php | 220 + 3rdparty/sabre/http/lib/Auth/AbstractAuth.php | 65 + 3rdparty/sabre/http/lib/Auth/Basic.php | 60 + 3rdparty/sabre/http/lib/Auth/Bearer.php | 53 + 3rdparty/sabre/http/lib/Auth/Digest.php | 208 + 3rdparty/sabre/http/lib/Client.php | 620 ++ 3rdparty/sabre/http/lib/ClientException.php | 17 + .../sabre/http/lib/ClientHttpException.php | 50 + 3rdparty/sabre/http/lib/HttpException.php | 31 + 3rdparty/sabre/http/lib/Message.php | 291 + .../sabre/http/lib/MessageDecoratorTrait.php | 206 + 3rdparty/sabre/http/lib/MessageInterface.php | 151 + 3rdparty/sabre/http/lib/Request.php | 266 + 3rdparty/sabre/http/lib/RequestDecorator.php | 179 + 3rdparty/sabre/http/lib/RequestInterface.php | 114 + 3rdparty/sabre/http/lib/Response.php | 187 + 3rdparty/sabre/http/lib/ResponseDecorator.php | 72 + 3rdparty/sabre/http/lib/ResponseInterface.php | 42 + 3rdparty/sabre/http/lib/Sapi.php | 240 + 3rdparty/sabre/http/lib/Version.php | 20 + 3rdparty/sabre/http/lib/functions.php | 410 ++ 3rdparty/sabre/uri/LICENSE | 27 + .../sabre/uri/lib/InvalidUriException.php | 19 + 3rdparty/sabre/uri/lib/Version.php | 20 + 3rdparty/sabre/uri/lib/functions.php | 412 ++ 3rdparty/sabre/vobject/LICENSE | 27 + .../vobject/lib/BirthdayCalendarGenerator.php | 172 + 3rdparty/sabre/vobject/lib/Cli.php | 705 ++ 3rdparty/sabre/vobject/lib/Component.php | 672 ++ .../sabre/vobject/lib/Component/Available.php | 123 + .../sabre/vobject/lib/Component/VAlarm.php | 138 + .../vobject/lib/Component/VAvailability.php | 149 + .../sabre/vobject/lib/Component/VCalendar.php | 528 ++ .../sabre/vobject/lib/Component/VCard.php | 541 ++ .../sabre/vobject/lib/Component/VEvent.php | 140 + .../sabre/vobject/lib/Component/VFreeBusy.php | 93 + .../sabre/vobject/lib/Component/VJournal.php | 101 + .../sabre/vobject/lib/Component/VTimeZone.php | 63 + .../sabre/vobject/lib/Component/VTodo.php | 181 + 3rdparty/sabre/vobject/lib/DateTimeParser.php | 560 ++ 3rdparty/sabre/vobject/lib/Document.php | 262 + 3rdparty/sabre/vobject/lib/ElementList.php | 48 + 3rdparty/sabre/vobject/lib/EofException.php | 15 + 3rdparty/sabre/vobject/lib/FreeBusyData.php | 185 + .../sabre/vobject/lib/FreeBusyGenerator.php | 549 ++ 3rdparty/sabre/vobject/lib/ITip/Broker.php | 986 +++ .../sabre/vobject/lib/ITip/ITipException.php | 16 + 3rdparty/sabre/vobject/lib/ITip/Message.php | 136 + ...SameOrganizerForAllComponentsException.php | 18 + .../vobject/lib/InvalidDataException.php | 15 + 3rdparty/sabre/vobject/lib/Node.php | 252 + .../sabre/vobject/lib/PHPUnitAssertions.php | 75 + 3rdparty/sabre/vobject/lib/Parameter.php | 368 + 3rdparty/sabre/vobject/lib/ParseException.php | 14 + 3rdparty/sabre/vobject/lib/Parser/Json.php | 190 + 3rdparty/sabre/vobject/lib/Parser/MimeDir.php | 689 ++ 3rdparty/sabre/vobject/lib/Parser/Parser.php | 75 + 3rdparty/sabre/vobject/lib/Parser/XML.php | 377 + .../lib/Parser/XML/Element/KeyValue.php | 63 + 3rdparty/sabre/vobject/lib/Property.php | 642 ++ .../sabre/vobject/lib/Property/Binary.php | 109 + .../sabre/vobject/lib/Property/Boolean.php | 72 + .../sabre/vobject/lib/Property/FlatText.php | 46 + .../sabre/vobject/lib/Property/FloatValue.php | 124 + .../lib/Property/ICalendar/CalAddress.php | 63 + .../vobject/lib/Property/ICalendar/Date.php | 18 + .../lib/Property/ICalendar/DateTime.php | 364 + .../lib/Property/ICalendar/Duration.php | 79 + .../vobject/lib/Property/ICalendar/Period.php | 135 + .../vobject/lib/Property/ICalendar/Recur.php | 344 + .../vobject/lib/Property/IntegerValue.php | 76 + 3rdparty/sabre/vobject/lib/Property/Text.php | 392 + 3rdparty/sabre/vobject/lib/Property/Time.php | 131 + .../sabre/vobject/lib/Property/Unknown.php | 41 + 3rdparty/sabre/vobject/lib/Property/Uri.php | 116 + .../sabre/vobject/lib/Property/UtcOffset.php | 70 + .../sabre/vobject/lib/Property/VCard/Date.php | 36 + .../lib/Property/VCard/DateAndOrTime.php | 367 + .../vobject/lib/Property/VCard/DateTime.php | 28 + .../lib/Property/VCard/LanguageTag.php | 53 + .../lib/Property/VCard/PhoneNumber.php | 30 + .../vobject/lib/Property/VCard/TimeStamp.php | 81 + 3rdparty/sabre/vobject/lib/Reader.php | 95 + .../sabre/vobject/lib/Recur/EventIterator.php | 497 ++ .../Recur/MaxInstancesExceededException.php | 17 + .../lib/Recur/NoInstancesException.php | 18 + .../sabre/vobject/lib/Recur/RDateIterator.php | 175 + .../sabre/vobject/lib/Recur/RRuleIterator.php | 1079 +++ 3rdparty/sabre/vobject/lib/Settings.php | 55 + .../sabre/vobject/lib/Splitter/ICalendar.php | 106 + .../lib/Splitter/SplitterInterface.php | 38 + 3rdparty/sabre/vobject/lib/Splitter/VCard.php | 74 + 3rdparty/sabre/vobject/lib/StringUtil.php | 50 + 3rdparty/sabre/vobject/lib/TimeZoneUtil.php | 272 + .../lib/TimezoneGuesser/FindFromOffset.php | 31 + .../FindFromTimezoneIdentifier.php | 71 + .../TimezoneGuesser/FindFromTimezoneMap.php | 78 + .../lib/TimezoneGuesser/GuessFromLicEntry.php | 33 + .../lib/TimezoneGuesser/GuessFromMsTzId.php | 119 + .../lib/TimezoneGuesser/TimezoneFinder.php | 10 + .../lib/TimezoneGuesser/TimezoneGuesser.php | 11 + 3rdparty/sabre/vobject/lib/UUIDUtil.php | 66 + 3rdparty/sabre/vobject/lib/VCardConverter.php | 421 ++ 3rdparty/sabre/vobject/lib/Version.php | 18 + 3rdparty/sabre/vobject/lib/Writer.php | 68 + .../lib/timezonedata/exchangezones.php | 94 + .../vobject/lib/timezonedata/lotuszones.php | 101 + .../sabre/vobject/lib/timezonedata/php-bc.php | 152 + .../lib/timezonedata/php-workaround.php | 46 + .../vobject/lib/timezonedata/windowszones.php | 152 + .../sabre/vobject/resources/schema/xcal.rng | 1192 +++ .../sabre/vobject/resources/schema/xcard.rng | 388 + 3rdparty/sabre/xml/LICENSE | 27 + 3rdparty/sabre/xml/lib/ContextStackTrait.php | 118 + .../sabre/xml/lib/Deserializer/functions.php | 360 + 3rdparty/sabre/xml/lib/Element.php | 22 + 3rdparty/sabre/xml/lib/Element/Base.php | 80 + 3rdparty/sabre/xml/lib/Element/Cdata.php | 59 + 3rdparty/sabre/xml/lib/Element/Elements.php | 98 + 3rdparty/sabre/xml/lib/Element/KeyValue.php | 98 + 3rdparty/sabre/xml/lib/Element/Uri.php | 97 + .../sabre/xml/lib/Element/XmlFragment.php | 146 + 3rdparty/sabre/xml/lib/LibXMLException.php | 47 + 3rdparty/sabre/xml/lib/ParseException.php | 18 + 3rdparty/sabre/xml/lib/Reader.php | 307 + .../sabre/xml/lib/Serializer/functions.php | 207 + 3rdparty/sabre/xml/lib/Service.php | 326 + 3rdparty/sabre/xml/lib/Version.php | 20 + 3rdparty/sabre/xml/lib/Writer.php | 255 + 3rdparty/sabre/xml/lib/XmlDeserializable.php | 36 + 3rdparty/sabre/xml/lib/XmlSerializable.php | 34 + 3rdparty/spomky-labs/cbor-php/LICENSE | 21 + .../cbor-php/src/AbstractCBORObject.php | 32 + .../cbor-php/src/ByteStringObject.php | 56 + .../spomky-labs/cbor-php/src/CBORObject.php | 94 + 3rdparty/spomky-labs/cbor-php/src/Decoder.php | 239 + .../cbor-php/src/DecoderInterface.php | 10 + .../src/IndefiniteLengthByteStringObject.php | 84 + .../src/IndefiniteLengthListObject.php | 137 + .../src/IndefiniteLengthMapObject.php | 149 + .../src/IndefiniteLengthTextStringObject.php | 84 + .../cbor-php/src/LengthCalculator.php | 65 + .../spomky-labs/cbor-php/src/ListObject.php | 162 + 3rdparty/spomky-labs/cbor-php/src/MapItem.php | 29 + .../spomky-labs/cbor-php/src/MapObject.php | 180 + .../cbor-php/src/NegativeIntegerObject.php | 112 + .../spomky-labs/cbor-php/src/Normalizable.php | 13 + .../spomky-labs/cbor-php/src/OtherObject.php | 34 + .../cbor-php/src/OtherObject/BreakObject.php | 30 + .../DoublePrecisionFloatObject.php | 77 + .../cbor-php/src/OtherObject/FalseObject.php | 36 + .../src/OtherObject/GenericObject.php | 26 + .../OtherObject/HalfPrecisionFloatObject.php | 77 + .../cbor-php/src/OtherObject/NullObject.php | 36 + .../src/OtherObject/OtherObjectInterface.php | 17 + .../src/OtherObject/OtherObjectManager.php | 47 + .../OtherObjectManagerInterface.php | 10 + .../cbor-php/src/OtherObject/SimpleObject.php | 72 + .../SinglePrecisionFloatObject.php | 76 + .../cbor-php/src/OtherObject/TrueObject.php | 36 + .../src/OtherObject/UndefinedObject.php | 30 + 3rdparty/spomky-labs/cbor-php/src/Stream.php | 10 + .../spomky-labs/cbor-php/src/StringStream.php | 76 + 3rdparty/spomky-labs/cbor-php/src/Tag.php | 74 + .../cbor-php/src/Tag/Base16EncodingTag.php | 28 + .../cbor-php/src/Tag/Base64EncodingTag.php | 28 + .../cbor-php/src/Tag/Base64Tag.php | 40 + .../cbor-php/src/Tag/Base64UrlEncodingTag.php | 28 + .../cbor-php/src/Tag/Base64UrlTag.php | 40 + .../cbor-php/src/Tag/BigFloatTag.php | 83 + .../cbor-php/src/Tag/CBOREncodingTag.php | 40 + .../spomky-labs/cbor-php/src/Tag/CBORTag.php | 37 + .../cbor-php/src/Tag/DatetimeTag.php | 63 + .../cbor-php/src/Tag/DecimalFractionTag.php | 82 + .../cbor-php/src/Tag/GenericTag.php | 21 + .../spomky-labs/cbor-php/src/Tag/MimeTag.php | 52 + .../src/Tag/NegativeBigIntegerTag.php | 54 + .../cbor-php/src/Tag/TagInterface.php | 20 + .../cbor-php/src/Tag/TagManager.php | 52 + .../cbor-php/src/Tag/TagManagerInterface.php | 12 + .../cbor-php/src/Tag/TimestampTag.php | 82 + .../src/Tag/UnsignedBigIntegerTag.php | 50 + .../spomky-labs/cbor-php/src/Tag/UriTag.php | 49 + .../cbor-php/src/TextStringObject.php | 56 + .../cbor-php/src/UnsignedIntegerObject.php | 118 + 3rdparty/spomky-labs/cbor-php/src/Utils.php | 60 + 3rdparty/spomky-labs/pki-framework/LICENSE | 22 + .../src/ASN1/Component/Identifier.php | 278 + .../src/ASN1/Component/Length.php | 204 + .../pki-framework/src/ASN1/DERData.php | 81 + .../pki-framework/src/ASN1/Element.php | 475 ++ .../src/ASN1/Exception/DecodeException.php | 14 + .../src/ASN1/Feature/ElementBase.php | 77 + .../src/ASN1/Feature/Encodable.php | 16 + .../src/ASN1/Feature/Stringable.php | 21 + .../src/ASN1/Type/BaseString.php | 56 + .../pki-framework/src/ASN1/Type/BaseTime.php | 59 + .../Type/Constructed/ConstructedString.php | 156 + .../src/ASN1/Type/Constructed/Sequence.php | 92 + .../src/ASN1/Type/Constructed/Set.php | 135 + .../src/ASN1/Type/Primitive/BMPString.php | 35 + .../src/ASN1/Type/Primitive/BitString.php | 191 + .../src/ASN1/Type/Primitive/Boolean.php | 62 + .../ASN1/Type/Primitive/CharacterString.php | 26 + .../src/ASN1/Type/Primitive/EOC.php | 49 + .../src/ASN1/Type/Primitive/Enumerated.php | 34 + .../src/ASN1/Type/Primitive/GeneralString.php | 32 + .../ASN1/Type/Primitive/GeneralizedTime.php | 133 + .../src/ASN1/Type/Primitive/GraphicString.php | 32 + .../src/ASN1/Type/Primitive/IA5String.php | 31 + .../src/ASN1/Type/Primitive/Integer.php | 108 + .../src/ASN1/Type/Primitive/NullType.php | 49 + .../src/ASN1/Type/Primitive/Number.php | 88 + .../src/ASN1/Type/Primitive/NumericString.php | 31 + .../ASN1/Type/Primitive/ObjectDescriptor.php | 34 + .../ASN1/Type/Primitive/ObjectIdentifier.php | 198 + .../src/ASN1/Type/Primitive/OctetString.php | 26 + .../ASN1/Type/Primitive/PrintableString.php | 32 + .../src/ASN1/Type/Primitive/Real.php | 692 ++ .../src/ASN1/Type/Primitive/RelativeOID.php | 163 + .../src/ASN1/Type/Primitive/T61String.php | 33 + .../src/ASN1/Type/Primitive/UTCTime.php | 81 + .../src/ASN1/Type/Primitive/UTF8String.php | 33 + .../ASN1/Type/Primitive/UniversalString.php | 38 + .../ASN1/Type/Primitive/VideotexString.php | 32 + .../src/ASN1/Type/Primitive/VisibleString.php | 31 + .../src/ASN1/Type/PrimitiveString.php | 40 + .../src/ASN1/Type/PrimitiveType.php | 19 + .../src/ASN1/Type/StringType.php | 16 + .../pki-framework/src/ASN1/Type/Structure.php | 281 + .../src/ASN1/Type/Tagged/ApplicationType.php | 12 + .../ASN1/Type/Tagged/ContextSpecificType.php | 12 + .../src/ASN1/Type/Tagged/DERTaggedType.php | 107 + .../src/ASN1/Type/Tagged/ExplicitTagging.php | 19 + .../ASN1/Type/Tagged/ExplicitlyTaggedType.php | 45 + .../src/ASN1/Type/Tagged/ImplicitTagging.php | 23 + .../ASN1/Type/Tagged/ImplicitlyTaggedType.php | 58 + .../src/ASN1/Type/Tagged/PrivateType.php | 12 + .../src/ASN1/Type/Tagged/TaggedTypeWrap.php | 27 + .../src/ASN1/Type/TaggedType.php | 78 + .../pki-framework/src/ASN1/Type/TimeType.php | 18 + .../src/ASN1/Type/UniversalClass.php | 21 + .../src/ASN1/Type/UnspecifiedType.php | 519 ++ .../pki-framework/src/ASN1/Util/BigInt.php | 126 + .../pki-framework/src/ASN1/Util/Flags.php | 148 + .../pki-framework/src/CryptoBridge/Crypto.php | 90 + .../src/CryptoBridge/Crypto/OpenSSLCrypto.php | 234 + .../pki-framework/src/CryptoEncoding/PEM.php | 138 + .../src/CryptoEncoding/PEMBundle.php | 155 + .../AlgorithmIdentifier.php | 137 + .../AlgorithmIdentifierFactory.php | 158 + .../AlgorithmIdentifierProvider.php | 31 + .../ECPublicKeyAlgorithmIdentifier.php | 291 + .../Asymmetric/Ed25519AlgorithmIdentifier.php | 52 + .../Asymmetric/Ed448AlgorithmIdentifier.php | 52 + .../RFC8410EdAlgorithmIdentifier.php | 39 + .../RFC8410XAlgorithmIdentifier.php | 35 + .../RSAEncryptionAlgorithmIdentifier.php | 63 + ...RSAPSSSSAEncryptionAlgorithmIdentifier.php | 63 + .../Asymmetric/X25519AlgorithmIdentifier.php | 43 + .../Asymmetric/X448AlgorithmIdentifier.php | 43 + .../Cipher/AES128CBCAlgorithmIdentifier.php | 55 + .../Cipher/AES192CBCAlgorithmIdentifier.php | 55 + .../Cipher/AES256CBCAlgorithmIdentifier.php | 55 + .../Cipher/AESCBCAlgorithmIdentifier.php | 42 + .../Cipher/BlockCipherAlgorithmIdentifier.php | 16 + .../Cipher/CipherAlgorithmIdentifier.php | 64 + .../Cipher/DESCBCAlgorithmIdentifier.php | 86 + .../Cipher/DESEDE3CBCAlgorithmIdentifier.php | 87 + .../Cipher/RC2CBCAlgorithmIdentifier.php | 200 + .../Feature/AlgorithmIdentifierType.php | 30 + .../AsymmetricCryptoAlgorithmIdentifier.php | 12 + .../Feature/EncryptionAlgorithmIdentifier.php | 12 + .../Feature/HashAlgorithmIdentifier.php | 12 + .../Feature/PRFAlgorithmIdentifier.php | 12 + .../Feature/SignatureAlgorithmIdentifier.php | 18 + .../GenericAlgorithmIdentifier.php | 45 + .../Hash/HMACWithSHA1AlgorithmIdentifier.php | 60 + .../HMACWithSHA224AlgorithmIdentifier.php | 40 + .../HMACWithSHA256AlgorithmIdentifier.php | 40 + .../HMACWithSHA384AlgorithmIdentifier.php | 40 + .../HMACWithSHA512AlgorithmIdentifier.php | 40 + .../Hash/MD5AlgorithmIdentifier.php | 74 + .../Hash/RFC4231HMACAlgorithmIdentifier.php | 37 + .../Hash/SHA1AlgorithmIdentifier.php | 70 + .../Hash/SHA224AlgorithmIdentifier.php | 47 + .../Hash/SHA256AlgorithmIdentifier.php | 46 + .../Hash/SHA2AlgorithmIdentifier.php | 39 + .../Hash/SHA384AlgorithmIdentifier.php | 46 + .../Hash/SHA512AlgorithmIdentifier.php | 46 + .../ECDSAWithSHA1AlgorithmIdentifier.php | 39 + .../ECDSAWithSHA224AlgorithmIdentifier.php | 39 + .../ECDSAWithSHA256AlgorithmIdentifier.php | 39 + .../ECDSAWithSHA384AlgorithmIdentifier.php | 39 + .../ECDSAWithSHA512AlgorithmIdentifier.php | 39 + .../ECSignatureAlgorithmIdentifier.php | 38 + ...D2WithRSAEncryptionAlgorithmIdentifier.php | 39 + ...D4WithRSAEncryptionAlgorithmIdentifier.php | 39 + ...D5WithRSAEncryptionAlgorithmIdentifier.php | 39 + ...RFC3279RSASignatureAlgorithmIdentifier.php | 29 + ...RFC4055RSASignatureAlgorithmIdentifier.php | 35 + .../RSASignatureAlgorithmIdentifier.php | 20 + ...A1WithRSAEncryptionAlgorithmIdentifier.php | 40 + ...24WithRSAEncryptionAlgorithmIdentifier.php | 37 + ...56WithRSAEncryptionAlgorithmIdentifier.php | 37 + ...84WithRSAEncryptionAlgorithmIdentifier.php | 37 + ...12WithRSAEncryptionAlgorithmIdentifier.php | 37 + .../SpecificAlgorithmIdentifier.php | 20 + .../Attribute/OneAsymmetricKeyAttributes.php | 26 + .../Asymmetric/EC/ECConversion.php | 109 + .../Asymmetric/EC/ECPrivateKey.php | 190 + .../CryptoTypes/Asymmetric/EC/ECPublicKey.php | 209 + .../Asymmetric/OneAsymmetricKey.php | 322 + .../src/CryptoTypes/Asymmetric/PrivateKey.php | 71 + .../CryptoTypes/Asymmetric/PrivateKeyInfo.php | 31 + .../src/CryptoTypes/Asymmetric/PublicKey.php | 57 + .../CryptoTypes/Asymmetric/PublicKeyInfo.php | 186 + .../Curve25519/Curve25519PrivateKey.php | 32 + .../Curve25519/Curve25519PublicKey.php | 28 + .../RFC8410/Curve25519/Ed25519PrivateKey.php | 48 + .../RFC8410/Curve25519/Ed25519PublicKey.php | 26 + .../RFC8410/Curve25519/X25519PrivateKey.php | 48 + .../RFC8410/Curve25519/X25519PublicKey.php | 26 + .../RFC8410/Curve448/Ed448PrivateKey.php | 66 + .../RFC8410/Curve448/Ed448PublicKey.php | 40 + .../RFC8410/Curve448/X448PrivateKey.php | 66 + .../RFC8410/Curve448/X448PublicKey.php | 40 + .../Asymmetric/RFC8410/RFC8410PrivateKey.php | 102 + .../Asymmetric/RFC8410/RFC8410PublicKey.php | 37 + .../Asymmetric/RSA/RSAPrivateKey.php | 226 + .../Asymmetric/RSA/RSAPublicKey.php | 124 + .../Asymmetric/RSA/RSASSAPSSPrivateKey.php | 226 + .../src/CryptoTypes/Signature/ECSignature.php | 95 + .../Signature/Ed25519Signature.php | 42 + .../CryptoTypes/Signature/Ed448Signature.php | 42 + .../Signature/GenericSignature.php | 42 + .../CryptoTypes/Signature/RSASignature.php | 50 + .../src/CryptoTypes/Signature/Signature.php | 46 + .../pki-framework/src/X501/ASN1/Attribute.php | 180 + .../src/X501/ASN1/AttributeType.php | 525 ++ .../src/X501/ASN1/AttributeTypeAndValue.php | 115 + .../ASN1/AttributeValue/AttributeValue.php | 146 + .../ASN1/AttributeValue/CommonNameValue.php | 21 + .../ASN1/AttributeValue/CountryNameValue.php | 35 + .../ASN1/AttributeValue/DescriptionValue.php | 21 + .../Feature/DirectoryString.php | 148 + .../Feature/PrintableStringValue.php | 55 + .../ASN1/AttributeValue/GivenNameValue.php | 21 + .../ASN1/AttributeValue/LocalityNameValue.php | 21 + .../X501/ASN1/AttributeValue/NameValue.php | 21 + .../AttributeValue/OrganizationNameValue.php | 21 + .../OrganizationalUnitNameValue.php | 21 + .../ASN1/AttributeValue/PseudonymValue.php | 21 + .../ASN1/AttributeValue/SerialNumberValue.php | 35 + .../StateOrProvinceNameValue.php | 21 + .../X501/ASN1/AttributeValue/SurnameValue.php | 21 + .../X501/ASN1/AttributeValue/TitleValue.php | 21 + .../AttributeValue/UnknownAttributeValue.php | 82 + .../ASN1/Collection/AttributeCollection.php | 187 + .../ASN1/Collection/SequenceOfAttributes.php | 46 + .../X501/ASN1/Collection/SetOfAttributes.php | 47 + .../pki-framework/src/X501/ASN1/Name.php | 193 + .../pki-framework/src/X501/ASN1/RDN.php | 167 + .../pki-framework/src/X501/DN/DNParser.php | 363 + .../src/X501/MatchingRule/BinaryMatch.php | 18 + .../src/X501/MatchingRule/CaseExactMatch.php | 28 + .../src/X501/MatchingRule/CaseIgnoreMatch.php | 30 + .../src/X501/MatchingRule/MatchingRule.php | 24 + .../MatchingRule/StringPrepMatchingRule.php | 25 + .../src/X501/StringPrep/CheckBidiStep.php | 22 + .../InsignificantNonSubstringSpaceStep.php | 32 + .../src/X501/StringPrep/MapStep.php | 40 + .../src/X501/StringPrep/NormalizeStep.php | 23 + .../src/X501/StringPrep/PrepareStep.php | 22 + .../src/X501/StringPrep/ProhibitStep.php | 22 + .../src/X501/StringPrep/StringPreparer.php | 78 + .../src/X501/StringPrep/TranscodeStep.php | 82 + .../AttributeCertificate/AttCertIssuer.php | 69 + .../AttCertValidityPeriod.php | 86 + .../AccessIdentityAttributeValue.php | 43 + .../AuthenticationInfoAttributeValue.php | 43 + .../ChargingIdentityAttributeValue.php | 20 + .../Attribute/GroupAttributeValue.php | 20 + .../Attribute/IetfAttrSyntax.php | 176 + .../Attribute/IetfAttrValue.php | 128 + .../Attribute/RoleAttributeValue.php | 129 + .../Attribute/SvceAuthInfo.php | 95 + .../AttributeCertificate.php | 175 + .../AttributeCertificateInfo.php | 366 + .../X509/AttributeCertificate/Attributes.php | 151 + .../src/X509/AttributeCertificate/Holder.php | 242 + .../AttributeCertificate/IssuerSerial.php | 141 + .../AttributeCertificate/ObjectDigestInfo.php | 80 + .../src/X509/AttributeCertificate/V2Form.php | 116 + .../Validation/ACValidationConfig.php | 98 + .../Validation/ACValidator.php | 185 + .../Exception/ACValidationException.php | 11 + .../src/X509/Certificate/Certificate.php | 207 + .../X509/Certificate/CertificateBundle.php | 202 + .../src/X509/Certificate/CertificateChain.php | 124 + .../Extension/AAControlsExtension.php | 184 + .../AccessDescription/AccessDescription.php | 57 + .../AuthorityAccessDescription.php | 52 + .../SubjectAccessDescription.php | 52 + .../AuthorityInformationAccessExtension.php | 87 + .../AuthorityKeyIdentifierExtension.php | 163 + .../Extension/BasicConstraintsExtension.php | 95 + .../CRLDistributionPointsExtension.php | 94 + .../CertificatePoliciesExtension.php | 124 + .../CertificatePolicy/CPSQualifier.php | 46 + .../CertificatePolicy/DisplayText.php | 78 + .../CertificatePolicy/NoticeReference.php | 80 + .../CertificatePolicy/PolicyInformation.php | 190 + .../CertificatePolicy/PolicyQualifierInfo.php | 79 + .../CertificatePolicy/UserNoticeQualifier.php | 98 + .../DistributionPoint/DistributionPoint.php | 181 + .../DistributionPointName.php | 66 + .../Extension/DistributionPoint/FullName.php | 47 + .../DistributionPoint/ReasonFlags.php | 133 + .../DistributionPoint/RelativeName.php | 38 + .../Extension/ExtendedKeyUsageExtension.php | 160 + .../X509/Certificate/Extension/Extension.php | 327 + .../Extension/FreshestCRLExtension.php | 20 + .../Extension/InhibitAnyPolicyExtension.php | 44 + .../IssuerAlternativeNameExtension.php | 44 + .../Extension/KeyUsageExtension.php | 142 + .../NameConstraints/GeneralSubtree.php | 98 + .../NameConstraints/GeneralSubtrees.php | 94 + .../Extension/NameConstraintsExtension.php | 106 + .../NoRevocationAvailableExtension.php | 37 + .../Extension/PolicyConstraintsExtension.php | 96 + .../PolicyMappings/PolicyMapping.php | 76 + .../Extension/PolicyMappingsExtension.php | 165 + .../SubjectAlternativeNameExtension.php | 44 + .../SubjectDirectoryAttributesExtension.php | 120 + .../SubjectInformationAccessExtension.php | 87 + .../SubjectKeyIdentifierExtension.php | 47 + .../Certificate/Extension/Target/Target.php | 79 + .../Extension/Target/TargetGroup.php | 55 + .../Extension/Target/TargetName.php | 52 + .../Certificate/Extension/Target/Targets.php | 126 + .../Extension/TargetInformationExtension.php | 137 + .../Extension/UnknownExtension.php | 61 + .../src/X509/Certificate/Extensions.php | 344 + .../src/X509/Certificate/TBSCertificate.php | 527 ++ .../src/X509/Certificate/Time.php | 95 + .../src/X509/Certificate/UniqueIdentifier.php | 65 + .../src/X509/Certificate/Validity.php | 72 + .../CertificationPath/CertificationPath.php | 178 + .../Exception/PathBuildingException.php | 14 + .../Exception/PathValidationException.php | 14 + .../PathBuilding/CertificationPathBuilder.php | 136 + .../PathValidation/PathValidationConfig.php | 210 + .../PathValidation/PathValidationResult.php | 99 + .../PathValidation/PathValidator.php | 484 ++ .../PathValidation/ValidatorState.php | 379 + .../CertificationPath/Policy/PolicyNode.php | 242 + .../CertificationPath/Policy/PolicyTree.php | 389 + .../Attribute/ExtensionRequestValue.php | 77 + .../X509/CertificationRequest/Attributes.php | 68 + .../CertificationRequest.php | 146 + .../CertificationRequestInfo.php | 181 + .../Exception/X509ValidationException.php | 11 + .../src/X509/Feature/DateTimeHelper.php | 59 + .../src/X509/GeneralName/DNSName.php | 58 + .../src/X509/GeneralName/DirectoryName.php | 65 + .../src/X509/GeneralName/EDIPartyName.php | 51 + .../src/X509/GeneralName/GeneralName.php | 119 + .../src/X509/GeneralName/GeneralNames.php | 174 + .../src/X509/GeneralName/IPAddress.php | 86 + .../src/X509/GeneralName/IPv4Address.php | 48 + .../src/X509/GeneralName/IPv6Address.php | 59 + .../src/X509/GeneralName/OtherName.php | 80 + .../src/X509/GeneralName/RFC822Name.php | 52 + .../src/X509/GeneralName/RegisteredID.php | 60 + .../GeneralName/UniformResourceIdentifier.php | 52 + .../src/X509/GeneralName/X400Address.php | 48 + .../symfony-console-completion/LICENCE | 21 + .../src/Completion.php | 180 + .../Completion/CompletionAwareInterface.php | 27 + .../src/Completion/CompletionInterface.php | 48 + .../src/Completion/ShellPathCompletion.php | 65 + .../src/CompletionCommand.php | 240 + .../src/CompletionContext.php | 390 + .../src/CompletionHandler.php | 504 ++ .../src/EnvironmentCompletionContext.php | 46 + .../src/HookFactory.php | 214 + 3rdparty/symfony/console/Application.php | 1331 ++++ .../symfony/console/Attribute/AsCommand.php | 39 + .../console/CI/GithubActionReporter.php | 99 + 3rdparty/symfony/console/Color.php | 133 + 3rdparty/symfony/console/Command/Command.php | 725 ++ .../console/Command/CompleteCommand.php | 223 + .../console/Command/DumpCompletionCommand.php | 161 + .../symfony/console/Command/HelpCommand.php | 82 + .../symfony/console/Command/LazyCommand.php | 207 + .../symfony/console/Command/ListCommand.php | 75 + .../symfony/console/Command/LockableTrait.php | 68 + .../Command/SignalableCommandInterface.php | 34 + .../console/Command/TraceableCommand.php | 356 + .../CommandLoader/CommandLoaderInterface.php | 38 + .../CommandLoader/ContainerCommandLoader.php | 55 + .../CommandLoader/FactoryCommandLoader.php | 54 + .../console/Completion/CompletionInput.php | 248 + .../Completion/CompletionSuggestions.php | 97 + .../Output/BashCompletionOutput.php | 33 + .../Output/CompletionOutputInterface.php | 25 + .../Output/FishCompletionOutput.php | 33 + .../Completion/Output/ZshCompletionOutput.php | 36 + .../symfony/console/Completion/Suggestion.php | 41 + 3rdparty/symfony/console/ConsoleEvents.php | 72 + 3rdparty/symfony/console/Cursor.php | 204 + .../DataCollector/CommandDataCollector.php | 234 + 3rdparty/symfony/console/Debug/CliRequest.php | 70 + .../AddConsoleCommandPass.php | 134 + .../Descriptor/ApplicationDescription.php | 139 + .../symfony/console/Descriptor/Descriptor.php | 74 + .../Descriptor/DescriptorInterface.php | 27 + .../console/Descriptor/JsonDescriptor.php | 166 + .../console/Descriptor/MarkdownDescriptor.php | 173 + .../Descriptor/ReStructuredTextDescriptor.php | 272 + .../console/Descriptor/TextDescriptor.php | 317 + .../console/Descriptor/XmlDescriptor.php | 232 + .../console/Event/ConsoleCommandEvent.php | 54 + .../console/Event/ConsoleErrorEvent.php | 57 + .../symfony/console/Event/ConsoleEvent.php | 61 + .../console/Event/ConsoleSignalEvent.php | 56 + .../console/Event/ConsoleTerminateEvent.php | 50 + .../console/EventListener/ErrorListener.php | 101 + .../Exception/CommandNotFoundException.php | 43 + .../console/Exception/ExceptionInterface.php | 21 + .../Exception/InvalidArgumentException.php | 19 + .../Exception/InvalidOptionException.php | 21 + .../console/Exception/LogicException.php | 19 + .../Exception/MissingInputException.php | 21 + .../Exception/NamespaceNotFoundException.php | 21 + .../Exception/RunCommandFailedException.php | 29 + .../console/Exception/RuntimeException.php | 19 + .../console/Formatter/NullOutputFormatter.php | 51 + .../Formatter/NullOutputFormatterStyle.php | 54 + .../console/Formatter/OutputFormatter.php | 277 + .../Formatter/OutputFormatterInterface.php | 56 + .../Formatter/OutputFormatterStyle.php | 110 + .../OutputFormatterStyleInterface.php | 60 + .../Formatter/OutputFormatterStyleStack.php | 107 + .../WrappableOutputFormatterInterface.php | 27 + .../console/Helper/DebugFormatterHelper.php | 98 + .../console/Helper/DescriptorHelper.php | 93 + 3rdparty/symfony/console/Helper/Dumper.php | 57 + .../console/Helper/FormatterHelper.php | 81 + 3rdparty/symfony/console/Helper/Helper.php | 174 + .../console/Helper/HelperInterface.php | 39 + 3rdparty/symfony/console/Helper/HelperSet.php | 77 + .../console/Helper/InputAwareHelper.php | 33 + .../symfony/console/Helper/OutputWrapper.php | 76 + .../symfony/console/Helper/ProcessHelper.php | 137 + .../symfony/console/Helper/ProgressBar.php | 618 ++ .../console/Helper/ProgressIndicator.php | 235 + .../symfony/console/Helper/QuestionHelper.php | 600 ++ .../console/Helper/SymfonyQuestionHelper.php | 109 + 3rdparty/symfony/console/Helper/Table.php | 930 +++ 3rdparty/symfony/console/Helper/TableCell.php | 72 + .../symfony/console/Helper/TableCellStyle.php | 84 + 3rdparty/symfony/console/Helper/TableRows.php | 30 + .../symfony/console/Helper/TableSeparator.php | 25 + .../symfony/console/Helper/TableStyle.php | 362 + 3rdparty/symfony/console/Input/ArgvInput.php | 370 + 3rdparty/symfony/console/Input/ArrayInput.php | 196 + 3rdparty/symfony/console/Input/Input.php | 193 + .../symfony/console/Input/InputArgument.php | 154 + .../console/Input/InputAwareInterface.php | 28 + .../symfony/console/Input/InputDefinition.php | 416 ++ .../symfony/console/Input/InputInterface.php | 150 + .../symfony/console/Input/InputOption.php | 255 + .../Input/StreamableInputInterface.php | 39 + .../symfony/console/Input/StringInput.php | 87 + 3rdparty/symfony/console/LICENSE | 19 + .../symfony/console/Logger/ConsoleLogger.php | 119 + .../console/Messenger/RunCommandContext.php | 25 + .../console/Messenger/RunCommandMessage.php | 36 + .../Messenger/RunCommandMessageHandler.php | 48 + .../symfony/console/Output/AnsiColorMode.php | 106 + .../symfony/console/Output/BufferedOutput.php | 43 + .../symfony/console/Output/ConsoleOutput.php | 165 + .../console/Output/ConsoleOutputInterface.php | 33 + .../console/Output/ConsoleSectionOutput.php | 244 + .../symfony/console/Output/NullOutput.php | 104 + 3rdparty/symfony/console/Output/Output.php | 155 + .../console/Output/OutputInterface.php | 111 + .../symfony/console/Output/StreamOutput.php | 125 + .../console/Output/TrimmedBufferOutput.php | 61 + .../console/Question/ChoiceQuestion.php | 177 + .../console/Question/ConfirmationQuestion.php | 57 + .../symfony/console/Question/Question.php | 291 + .../symfony/console/Resources/completion.bash | 94 + .../symfony/console/Resources/completion.fish | 29 + .../symfony/console/Resources/completion.zsh | 82 + .../console/SignalRegistry/SignalMap.php | 36 + .../console/SignalRegistry/SignalRegistry.php | 57 + .../console/SingleCommandApplication.php | 72 + .../symfony/console/Style/OutputStyle.php | 132 + .../symfony/console/Style/StyleInterface.php | 138 + .../symfony/console/Style/SymfonyStyle.php | 514 ++ 3rdparty/symfony/console/Terminal.php | 235 + .../console/Tester/ApplicationTester.php | 85 + .../Tester/CommandCompletionTester.php | 56 + .../symfony/console/Tester/CommandTester.php | 76 + .../Tester/Constraint/CommandIsSuccessful.php | 43 + .../symfony/console/Tester/TesterTrait.php | 178 + .../css-selector/CssSelectorConverter.php | 67 + .../Exception/ExceptionInterface.php | 24 + .../Exception/ExpressionErrorException.php | 24 + .../Exception/InternalErrorException.php | 24 + .../css-selector/Exception/ParseException.php | 24 + .../Exception/SyntaxErrorException.php | 55 + 3rdparty/symfony/css-selector/LICENSE | 19 + .../css-selector/Node/AbstractNode.php | 32 + .../css-selector/Node/AttributeNode.php | 79 + .../symfony/css-selector/Node/ClassNode.php | 54 + .../Node/CombinedSelectorNode.php | 63 + .../symfony/css-selector/Node/ElementNode.php | 56 + .../css-selector/Node/FunctionNode.php | 71 + .../symfony/css-selector/Node/HashNode.php | 54 + .../css-selector/Node/NegationNode.php | 54 + .../css-selector/Node/NodeInterface.php | 29 + .../symfony/css-selector/Node/PseudoNode.php | 54 + .../css-selector/Node/SelectorNode.php | 54 + .../symfony/css-selector/Node/Specificity.php | 73 + .../Parser/Handler/CommentHandler.php | 45 + .../Parser/Handler/HandlerInterface.php | 30 + .../Parser/Handler/HashHandler.php | 55 + .../Parser/Handler/IdentifierHandler.php | 55 + .../Parser/Handler/NumberHandler.php | 51 + .../Parser/Handler/StringHandler.php | 74 + .../Parser/Handler/WhitespaceHandler.php | 43 + .../symfony/css-selector/Parser/Parser.php | 359 + .../css-selector/Parser/ParserInterface.php | 34 + .../symfony/css-selector/Parser/Reader.php | 86 + .../Parser/Shortcut/ClassParser.php | 48 + .../Parser/Shortcut/ElementParser.php | 44 + .../Parser/Shortcut/EmptyStringParser.php | 43 + .../Parser/Shortcut/HashParser.php | 48 + .../symfony/css-selector/Parser/Token.php | 111 + .../css-selector/Parser/TokenStream.php | 156 + .../Parser/Tokenizer/Tokenizer.php | 73 + .../Parser/Tokenizer/TokenizerEscaping.php | 65 + .../Parser/Tokenizer/TokenizerPatterns.php | 89 + .../XPath/Extension/AbstractExtension.php | 50 + .../Extension/AttributeMatchingExtension.php | 113 + .../XPath/Extension/CombinationExtension.php | 65 + .../XPath/Extension/ExtensionInterface.php | 67 + .../XPath/Extension/FunctionExtension.php | 165 + .../XPath/Extension/HtmlExtension.php | 178 + .../XPath/Extension/NodeExtension.php | 191 + .../XPath/Extension/PseudoClassExtension.php | 122 + .../symfony/css-selector/XPath/Translator.php | 224 + .../XPath/TranslatorInterface.php | 37 + .../symfony/css-selector/XPath/XPathExpr.php | 111 + .../symfony/deprecation-contracts/LICENSE | 19 + .../deprecation-contracts/function.php | 27 + .../dom-crawler/AbstractUriElement.php | 123 + 3rdparty/symfony/dom-crawler/Crawler.php | 1265 ++++ .../dom-crawler/Field/ChoiceFormField.php | 309 + .../dom-crawler/Field/FileFormField.php | 112 + .../symfony/dom-crawler/Field/FormField.php | 125 + .../dom-crawler/Field/InputFormField.php | 48 + .../dom-crawler/Field/TextareaFormField.php | 39 + 3rdparty/symfony/dom-crawler/Form.php | 469 ++ .../symfony/dom-crawler/FormFieldRegistry.php | 175 + 3rdparty/symfony/dom-crawler/Image.php | 40 + 3rdparty/symfony/dom-crawler/LICENSE | 19 + 3rdparty/symfony/dom-crawler/Link.php | 37 + 3rdparty/symfony/dom-crawler/UriResolver.php | 136 + .../event-dispatcher-contracts/Event.php | 51 + .../EventDispatcherInterface.php | 33 + .../event-dispatcher-contracts/LICENSE | 19 + .../Attribute/AsEventListener.php | 29 + .../Debug/TraceableEventDispatcher.php | 375 + .../Debug/WrappedListener.php | 144 + .../AddEventAliasesPass.php | 40 + .../RegisterListenersPass.php | 216 + .../event-dispatcher/EventDispatcher.php | 270 + .../EventDispatcherInterface.php | 75 + .../EventSubscriberInterface.php | 49 + .../symfony/event-dispatcher/GenericEvent.php | 158 + .../ImmutableEventDispatcher.php | 79 + 3rdparty/symfony/event-dispatcher/LICENSE | 19 + .../symfony/http-foundation/AcceptHeader.php | 150 + .../http-foundation/AcceptHeaderItem.php | 159 + .../http-foundation/BinaryFileResponse.php | 385 + .../http-foundation/ChainRequestMatcher.php | 38 + 3rdparty/symfony/http-foundation/Cookie.php | 412 ++ .../Exception/BadRequestException.php | 19 + .../Exception/ConflictingHeadersException.php | 21 + .../Exception/JsonException.php | 21 + .../Exception/RequestExceptionInterface.php | 21 + .../Exception/SessionNotFoundException.php | 27 + .../SuspiciousOperationException.php | 20 + .../Exception/UnexpectedValueException.php | 16 + .../ExpressionRequestMatcher.php | 56 + .../File/Exception/AccessDeniedException.php | 25 + .../Exception/CannotWriteFileException.php | 21 + .../File/Exception/ExtensionFileException.php | 21 + .../File/Exception/FileException.php | 21 + .../File/Exception/FileNotFoundException.php | 25 + .../File/Exception/FormSizeFileException.php | 21 + .../File/Exception/IniSizeFileException.php | 21 + .../File/Exception/NoFileException.php | 21 + .../File/Exception/NoTmpDirFileException.php | 21 + .../File/Exception/PartialFileException.php | 21 + .../Exception/UnexpectedTypeException.php | 20 + .../File/Exception/UploadException.php | 21 + .../symfony/http-foundation/File/File.php | 141 + .../symfony/http-foundation/File/Stream.php | 25 + .../http-foundation/File/UploadedFile.php | 269 + 3rdparty/symfony/http-foundation/FileBag.php | 136 + .../symfony/http-foundation/HeaderBag.php | 290 + .../symfony/http-foundation/HeaderUtils.php | 298 + 3rdparty/symfony/http-foundation/InputBag.php | 140 + 3rdparty/symfony/http-foundation/IpUtils.php | 251 + .../symfony/http-foundation/JsonResponse.php | 190 + 3rdparty/symfony/http-foundation/LICENSE | 19 + .../symfony/http-foundation/ParameterBag.php | 258 + .../AbstractRequestRateLimiter.php | 81 + .../PeekableRequestRateLimiterInterface.php | 35 + .../RequestRateLimiterInterface.php | 30 + .../http-foundation/RedirectResponse.php | 92 + 3rdparty/symfony/http-foundation/Request.php | 2163 ++++++ .../http-foundation/RequestMatcher.php | 200 + .../AttributesRequestMatcher.php | 45 + .../ExpressionRequestMatcher.php | 43 + .../RequestMatcher/HostRequestMatcher.php | 32 + .../RequestMatcher/IpsRequestMatcher.php | 44 + .../RequestMatcher/IsJsonRequestMatcher.php | 28 + .../RequestMatcher/MethodRequestMatcher.php | 46 + .../RequestMatcher/PathRequestMatcher.php | 32 + .../RequestMatcher/PortRequestMatcher.php | 32 + .../RequestMatcher/SchemeRequestMatcher.php | 46 + .../RequestMatcherInterface.php | 25 + .../symfony/http-foundation/RequestStack.php | 116 + 3rdparty/symfony/http-foundation/Response.php | 1353 ++++ .../http-foundation/ResponseHeaderBag.php | 292 + .../symfony/http-foundation/ServerBag.php | 97 + .../Session/Attribute/AttributeBag.php | 130 + .../Attribute/AttributeBagInterface.php | 58 + .../Session/Flash/AutoExpireFlashBag.php | 137 + .../Session/Flash/FlashBag.php | 128 + .../Session/Flash/FlashBagInterface.php | 78 + .../Session/FlashBagAwareSessionInterface.php | 22 + .../http-foundation/Session/Session.php | 244 + .../Session/SessionBagInterface.php | 44 + .../Session/SessionBagProxy.php | 83 + .../Session/SessionFactory.php | 40 + .../Session/SessionFactoryInterface.php | 20 + .../Session/SessionInterface.php | 154 + .../http-foundation/Session/SessionUtils.php | 59 + .../Handler/AbstractSessionHandler.php | 111 + .../Storage/Handler/IdentityMarshaller.php | 36 + .../Handler/MarshallingSessionHandler.php | 76 + .../Handler/MemcachedSessionHandler.php | 112 + .../Handler/MigratingSessionHandler.php | 100 + .../Storage/Handler/MongoDbSessionHandler.php | 186 + .../Handler/NativeFileSessionHandler.php | 55 + .../Storage/Handler/NullSessionHandler.php | 55 + .../Storage/Handler/PdoSessionHandler.php | 901 +++ .../Storage/Handler/RedisSessionHandler.php | 103 + .../Storage/Handler/SessionHandlerFactory.php | 99 + .../Storage/Handler/StrictSessionHandler.php | 89 + .../Session/Storage/MetadataBag.php | 148 + .../Storage/MockArraySessionStorage.php | 238 + .../Storage/MockFileSessionStorage.php | 152 + .../Storage/MockFileSessionStorageFactory.php | 42 + .../Session/Storage/NativeSessionStorage.php | 449 ++ .../Storage/NativeSessionStorageFactory.php | 50 + .../Storage/PhpBridgeSessionStorage.php | 58 + .../PhpBridgeSessionStorageFactory.php | 45 + .../Session/Storage/Proxy/AbstractProxy.php | 110 + .../Storage/Proxy/SessionHandlerProxy.php | 76 + .../SessionStorageFactoryInterface.php | 25 + .../Storage/SessionStorageInterface.php | 126 + .../http-foundation/StreamedJsonResponse.php | 162 + .../http-foundation/StreamedResponse.php | 131 + .../symfony/http-foundation/UriSigner.php | 112 + .../symfony/http-foundation/UrlHelper.php | 108 + .../mailer/Command/MailerTestCommand.php | 75 + .../DataCollector/MessageDataCollector.php | 59 + 3rdparty/symfony/mailer/DelayedEnvelope.php | 98 + 3rdparty/symfony/mailer/Envelope.php | 88 + .../mailer/Event/FailedMessageEvent.php | 37 + .../symfony/mailer/Event/MessageEvent.php | 105 + .../symfony/mailer/Event/MessageEvents.php | 74 + .../symfony/mailer/Event/SentMessageEvent.php | 30 + .../mailer/EventListener/EnvelopeListener.php | 71 + .../mailer/EventListener/MessageListener.php | 134 + .../EventListener/MessageLoggerListener.php | 57 + .../MessengerTransportListener.php | 49 + .../mailer/Exception/ExceptionInterface.php | 21 + .../Exception/HttpTransportException.php | 34 + .../Exception/IncompleteDsnException.php | 19 + .../Exception/InvalidArgumentException.php | 19 + .../mailer/Exception/LogicException.php | 19 + .../mailer/Exception/RuntimeException.php | 19 + .../mailer/Exception/TransportException.php | 30 + .../Exception/TransportExceptionInterface.php | 22 + .../Exception/UnexpectedResponseException.php | 16 + .../Exception/UnsupportedSchemeException.php | 101 + .../symfony/mailer/Header/MetadataHeader.php | 34 + 3rdparty/symfony/mailer/Header/TagHeader.php | 25 + 3rdparty/symfony/mailer/LICENSE | 19 + 3rdparty/symfony/mailer/Mailer.php | 76 + 3rdparty/symfony/mailer/MailerInterface.php | 30 + .../mailer/Messenger/MessageHandler.php | 33 + .../mailer/Messenger/SendEmailMessage.php | 40 + 3rdparty/symfony/mailer/SentMessage.php | 95 + 3rdparty/symfony/mailer/Transport.php | 188 + .../mailer/Transport/AbstractApiTransport.php | 47 + .../Transport/AbstractHttpTransport.php | 78 + .../mailer/Transport/AbstractTransport.php | 136 + .../Transport/AbstractTransportFactory.php | 51 + 3rdparty/symfony/mailer/Transport/Dsn.php | 89 + .../mailer/Transport/FailoverTransport.php | 41 + .../Transport/NativeTransportFactory.php | 63 + .../mailer/Transport/NullTransport.php | 31 + .../mailer/Transport/NullTransportFactory.php | 34 + .../mailer/Transport/RoundRobinTransport.php | 125 + .../mailer/Transport/SendmailTransport.php | 124 + .../Transport/SendmailTransportFactory.php | 34 + .../Smtp/Auth/AuthenticatorInterface.php | 35 + .../Smtp/Auth/CramMd5Authenticator.php | 65 + .../Smtp/Auth/LoginAuthenticator.php | 37 + .../Smtp/Auth/PlainAuthenticator.php | 35 + .../Smtp/Auth/XOAuth2Authenticator.php | 37 + .../mailer/Transport/Smtp/EsmtpTransport.php | 228 + .../Transport/Smtp/EsmtpTransportFactory.php | 78 + .../mailer/Transport/Smtp/SmtpTransport.php | 392 + .../Transport/Smtp/Stream/AbstractStream.php | 145 + .../Transport/Smtp/Stream/ProcessStream.php | 81 + .../Transport/Smtp/Stream/SocketStream.php | 193 + .../Transport/TransportFactoryInterface.php | 29 + .../mailer/Transport/TransportInterface.php | 33 + .../symfony/mailer/Transport/Transports.php | 75 + 3rdparty/symfony/mime/Address.php | 120 + .../symfony/mime/BodyRendererInterface.php | 20 + 3rdparty/symfony/mime/CharacterStream.php | 211 + 3rdparty/symfony/mime/Crypto/DkimOptions.php | 97 + 3rdparty/symfony/mime/Crypto/DkimSigner.php | 217 + 3rdparty/symfony/mime/Crypto/SMime.php | 111 + .../symfony/mime/Crypto/SMimeEncrypter.php | 63 + 3rdparty/symfony/mime/Crypto/SMimeSigner.php | 65 + .../AddMimeTypeGuesserPass.php | 37 + 3rdparty/symfony/mime/DraftEmail.php | 45 + 3rdparty/symfony/mime/Email.php | 591 ++ .../mime/Encoder/AddressEncoderInterface.php | 28 + .../mime/Encoder/Base64ContentEncoder.php | 45 + .../symfony/mime/Encoder/Base64Encoder.php | 41 + .../mime/Encoder/Base64MimeHeaderEncoder.php | 43 + .../mime/Encoder/ContentEncoderInterface.php | 30 + .../mime/Encoder/EightBitContentEncoder.php | 35 + .../symfony/mime/Encoder/EncoderInterface.php | 26 + .../mime/Encoder/IdnAddressEncoder.php | 44 + .../Encoder/MimeHeaderEncoderInterface.php | 23 + .../symfony/mime/Encoder/QpContentEncoder.php | 55 + 3rdparty/symfony/mime/Encoder/QpEncoder.php | 192 + .../mime/Encoder/QpMimeHeaderEncoder.php | 40 + .../symfony/mime/Encoder/Rfc2231Encoder.php | 50 + .../Exception/AddressEncoderException.php | 19 + .../mime/Exception/ExceptionInterface.php | 19 + .../Exception/InvalidArgumentException.php | 19 + .../symfony/mime/Exception/LogicException.php | 19 + .../mime/Exception/RfcComplianceException.php | 19 + .../mime/Exception/RuntimeException.php | 19 + .../mime/FileBinaryMimeTypeGuesser.php | 87 + .../symfony/mime/FileinfoMimeTypeGuesser.php | 63 + .../symfony/mime/Header/AbstractHeader.php | 288 + 3rdparty/symfony/mime/Header/DateHeader.php | 62 + .../symfony/mime/Header/HeaderInterface.php | 72 + 3rdparty/symfony/mime/Header/Headers.php | 316 + .../mime/Header/IdentificationHeader.php | 107 + .../symfony/mime/Header/MailboxHeader.php | 85 + .../symfony/mime/Header/MailboxListHeader.php | 136 + .../mime/Header/ParameterizedHeader.php | 191 + 3rdparty/symfony/mime/Header/PathHeader.php | 62 + .../mime/Header/UnstructuredHeader.php | 70 + .../DefaultHtmlToTextConverter.php | 23 + .../HtmlToTextConverterInterface.php | 25 + .../LeagueHtmlToMarkdownConverter.php | 35 + 3rdparty/symfony/mime/LICENSE | 19 + 3rdparty/symfony/mime/Message.php | 169 + 3rdparty/symfony/mime/MessageConverter.php | 122 + .../symfony/mime/MimeTypeGuesserInterface.php | 33 + 3rdparty/symfony/mime/MimeTypes.php | 3655 ++++++++++ 3rdparty/symfony/mime/MimeTypesInterface.php | 32 + .../mime/Part/AbstractMultipartPart.php | 95 + 3rdparty/symfony/mime/Part/AbstractPart.php | 65 + 3rdparty/symfony/mime/Part/DataPart.php | 168 + 3rdparty/symfony/mime/Part/File.php | 51 + 3rdparty/symfony/mime/Part/MessagePart.php | 72 + .../mime/Part/Multipart/AlternativePart.php | 25 + .../mime/Part/Multipart/DigestPart.php | 31 + .../mime/Part/Multipart/FormDataPart.php | 108 + .../symfony/mime/Part/Multipart/MixedPart.php | 25 + .../mime/Part/Multipart/RelatedPart.php | 55 + 3rdparty/symfony/mime/Part/SMimePart.php | 111 + 3rdparty/symfony/mime/Part/TextPart.php | 248 + 3rdparty/symfony/mime/RawMessage.php | 115 + .../polyfill-intl-grapheme/Grapheme.php | 247 + .../symfony/polyfill-intl-grapheme/LICENSE | 19 + .../polyfill-intl-grapheme/bootstrap.php | 58 + .../polyfill-intl-grapheme/bootstrap80.php | 50 + 3rdparty/symfony/polyfill-intl-idn/Idn.php | 941 +++ 3rdparty/symfony/polyfill-intl-idn/Info.php | 23 + 3rdparty/symfony/polyfill-intl-idn/LICENSE | 19 + .../Resources/unidata/DisallowedRanges.php | 384 + .../Resources/unidata/Regex.php | 33 + .../Resources/unidata/deviation.php | 8 + .../Resources/unidata/disallowed.php | 2638 +++++++ .../unidata/disallowed_STD3_mapped.php | 308 + .../unidata/disallowed_STD3_valid.php | 71 + .../Resources/unidata/ignored.php | 273 + .../Resources/unidata/mapped.php | 5778 +++++++++++++++ .../Resources/unidata/virama.php | 65 + .../symfony/polyfill-intl-idn/bootstrap.php | 145 + .../symfony/polyfill-intl-idn/bootstrap80.php | 125 + .../symfony/polyfill-intl-normalizer/LICENSE | 19 + .../polyfill-intl-normalizer/Normalizer.php | 310 + .../Resources/stubs/Normalizer.php | 17 + .../unidata/canonicalComposition.php | 945 +++ .../unidata/canonicalDecomposition.php | 2065 ++++++ .../Resources/unidata/combiningClass.php | 876 +++ .../unidata/compatibilityDecomposition.php | 3695 ++++++++++ .../polyfill-intl-normalizer/bootstrap.php | 23 + .../polyfill-intl-normalizer/bootstrap80.php | 19 + 3rdparty/symfony/polyfill-php82/LICENSE | 19 + .../polyfill-php82/NoDynamicProperties.php | 23 + 3rdparty/symfony/polyfill-php82/Php82.php | 394 + .../polyfill-php82/Random/Engine/Secure.php | 50 + .../stubs/AllowDynamicProperties.php | 20 + .../stubs/Random/BrokenRandomEngineError.php | 18 + .../stubs/Random/CryptoSafeEngine.php | 18 + .../Resources/stubs/Random/Engine.php | 19 + .../Resources/stubs/Random/Engine/Secure.php | 20 + .../Resources/stubs/Random/RandomError.php | 21 + .../stubs/Random/RandomException.php | 21 + .../Resources/stubs/SensitiveParameter.php | 20 + .../stubs/SensitiveParameterValue.php | 16 + .../SensitiveParameterValue.php | 47 + 3rdparty/symfony/polyfill-php82/bootstrap.php | 34 + 3rdparty/symfony/polyfill-php83/LICENSE | 19 + 3rdparty/symfony/polyfill-php83/Php83.php | 197 + .../Resources/stubs/DateError.php | 16 + .../Resources/stubs/DateException.php | 16 + .../stubs/DateInvalidOperationException.php | 16 + .../stubs/DateInvalidTimeZoneException.php | 16 + .../DateMalformedIntervalStringException.php | 16 + .../DateMalformedPeriodStringException.php | 16 + .../stubs/DateMalformedStringException.php | 16 + .../Resources/stubs/DateObjectError.php | 16 + .../Resources/stubs/DateRangeError.php | 16 + .../Resources/stubs/Override.php | 20 + .../Resources/stubs/SQLite3Exception.php | 16 + 3rdparty/symfony/polyfill-php83/bootstrap.php | 50 + .../symfony/polyfill-php83/bootstrap81.php | 22 + 3rdparty/symfony/polyfill-php84/LICENSE | 19 + 3rdparty/symfony/polyfill-php84/Php84.php | 177 + .../Resources/stubs/Deprecated.php | 25 + 3rdparty/symfony/polyfill-php84/bootstrap.php | 68 + 3rdparty/symfony/polyfill-uuid/LICENSE | 19 + 3rdparty/symfony/polyfill-uuid/Uuid.php | 531 ++ 3rdparty/symfony/polyfill-uuid/bootstrap.php | 97 + .../symfony/polyfill-uuid/bootstrap80.php | 89 + .../process/Exception/ExceptionInterface.php | 21 + .../Exception/InvalidArgumentException.php | 21 + .../process/Exception/LogicException.php | 21 + .../Exception/ProcessFailedException.php | 57 + .../Exception/ProcessSignaledException.php | 41 + .../Exception/ProcessTimedOutException.php | 73 + .../Exception/RunProcessFailedException.php | 25 + .../process/Exception/RuntimeException.php | 21 + 3rdparty/symfony/process/ExecutableFinder.php | 105 + 3rdparty/symfony/process/InputStream.php | 99 + 3rdparty/symfony/process/LICENSE | 19 + .../process/Messenger/RunProcessContext.php | 33 + .../process/Messenger/RunProcessMessage.php | 32 + .../Messenger/RunProcessMessageHandler.php | 33 + .../symfony/process/PhpExecutableFinder.php | 92 + 3rdparty/symfony/process/PhpProcess.php | 69 + 3rdparty/symfony/process/PhpSubprocess.php | 164 + .../symfony/process/Pipes/AbstractPipes.php | 176 + .../symfony/process/Pipes/PipesInterface.php | 61 + 3rdparty/symfony/process/Pipes/UnixPipes.php | 148 + .../symfony/process/Pipes/WindowsPipes.php | 186 + 3rdparty/symfony/process/Process.php | 1618 ++++ 3rdparty/symfony/process/ProcessUtils.php | 64 + 3rdparty/symfony/routing/Alias.php | 93 + 3rdparty/symfony/routing/Annotation/Route.php | 23 + 3rdparty/symfony/routing/Attribute/Route.php | 259 + 3rdparty/symfony/routing/CompiledRoute.php | 157 + .../AddExpressionLanguageProvidersPass.php | 36 + .../RoutingResolverPass.php | 43 + .../routing/Exception/ExceptionInterface.php | 21 + .../Exception/InvalidArgumentException.php | 16 + .../Exception/InvalidParameterException.php | 21 + .../Exception/MethodNotAllowedException.php | 44 + .../MissingMandatoryParametersException.php | 57 + .../Exception/NoConfigurationException.php | 21 + .../Exception/ResourceNotFoundException.php | 23 + .../RouteCircularReferenceException.php | 20 + .../Exception/RouteNotFoundException.php | 21 + .../routing/Exception/RuntimeException.php | 16 + .../Generator/CompiledUrlGenerator.php | 69 + .../ConfigurableRequirementsInterface.php | 53 + .../Dumper/CompiledUrlGeneratorDumper.php | 121 + .../Generator/Dumper/GeneratorDumper.php | 34 + .../Dumper/GeneratorDumperInterface.php | 33 + .../routing/Generator/UrlGenerator.php | 358 + .../Generator/UrlGeneratorInterface.php | 80 + 3rdparty/symfony/routing/LICENSE | 19 + .../routing/Loader/AnnotationClassLoader.php | 25 + .../Loader/AnnotationDirectoryLoader.php | 25 + .../routing/Loader/AnnotationFileLoader.php | 25 + .../routing/Loader/AttributeClassLoader.php | 431 ++ .../Loader/AttributeDirectoryLoader.php | 92 + .../routing/Loader/AttributeFileLoader.php | 145 + .../symfony/routing/Loader/ClosureLoader.php | 38 + .../Loader/Configurator/AliasConfigurator.php | 43 + .../Configurator/CollectionConfigurator.php | 128 + .../Configurator/ImportConfigurator.php | 90 + .../Loader/Configurator/RouteConfigurator.php | 49 + .../Configurator/RoutingConfigurator.php | 78 + .../Loader/Configurator/Traits/AddTrait.php | 60 + .../Loader/Configurator/Traits/HostTrait.php | 49 + .../Traits/LocalizedRouteTrait.php | 76 + .../Configurator/Traits/PrefixTrait.php | 63 + .../Loader/Configurator/Traits/RouteTrait.php | 175 + .../routing/Loader/ContainerLoader.php | 40 + .../routing/Loader/DirectoryLoader.php | 52 + .../symfony/routing/Loader/GlobFileLoader.php | 41 + .../symfony/routing/Loader/ObjectLoader.php | 77 + .../symfony/routing/Loader/PhpFileLoader.php | 77 + .../routing/Loader/Psr4DirectoryLoader.php | 91 + .../symfony/routing/Loader/XmlFileLoader.php | 470 ++ .../symfony/routing/Loader/YamlFileLoader.php | 302 + .../Loader/schema/routing/routing-1.0.xsd | 201 + .../routing/Matcher/CompiledUrlMatcher.php | 31 + .../Dumper/CompiledUrlMatcherDumper.php | 501 ++ .../Dumper/CompiledUrlMatcherTrait.php | 186 + .../routing/Matcher/Dumper/MatcherDumper.php | 34 + .../Matcher/Dumper/MatcherDumperInterface.php | 33 + .../Matcher/Dumper/StaticPrefixCollection.php | 204 + .../Matcher/ExpressionLanguageProvider.php | 51 + .../Matcher/RedirectableUrlMatcher.php | 61 + .../RedirectableUrlMatcherInterface.php | 29 + .../Matcher/RequestMatcherInterface.php | 37 + .../routing/Matcher/TraceableUrlMatcher.php | 172 + .../symfony/routing/Matcher/UrlMatcher.php | 287 + .../routing/Matcher/UrlMatcherInterface.php | 39 + 3rdparty/symfony/routing/RequestContext.php | 303 + .../routing/RequestContextAwareInterface.php | 27 + .../routing/Requirement/EnumRequirement.php | 56 + .../routing/Requirement/Requirement.php | 36 + 3rdparty/symfony/routing/Route.php | 458 ++ 3rdparty/symfony/routing/RouteCollection.php | 415 ++ 3rdparty/symfony/routing/RouteCompiler.php | 339 + .../routing/RouteCompilerInterface.php | 28 + 3rdparty/symfony/routing/Router.php | 358 + 3rdparty/symfony/routing/RouterInterface.php | 35 + .../service-contracts/Attribute/Required.php | 25 + .../Attribute/SubscribedService.php | 47 + 3rdparty/symfony/service-contracts/LICENSE | 19 + .../service-contracts/ResetInterface.php | 33 + .../ServiceCollectionInterface.php | 26 + .../service-contracts/ServiceLocatorTrait.php | 115 + .../ServiceMethodsSubscriberTrait.php | 80 + .../ServiceProviderInterface.php | 45 + .../ServiceSubscriberInterface.php | 62 + .../ServiceSubscriberTrait.php | 84 + 3rdparty/symfony/string/AbstractString.php | 702 ++ .../symfony/string/AbstractUnicodeString.php | 590 ++ 3rdparty/symfony/string/ByteString.php | 485 ++ 3rdparty/symfony/string/CodePointString.php | 260 + .../string/Exception/ExceptionInterface.php | 16 + .../Exception/InvalidArgumentException.php | 16 + .../string/Exception/RuntimeException.php | 16 + .../string/Inflector/EnglishInflector.php | 586 ++ .../string/Inflector/FrenchInflector.php | 151 + .../string/Inflector/InflectorInterface.php | 33 + 3rdparty/symfony/string/LICENSE | 19 + 3rdparty/symfony/string/LazyString.php | 145 + .../symfony/string/Resources/functions.php | 38 + .../symfony/string/Slugger/AsciiSlugger.php | 210 + .../string/Slugger/SluggerInterface.php | 27 + 3rdparty/symfony/string/UnicodeString.php | 385 + .../symfony/translation-contracts/LICENSE | 19 + .../LocaleAwareInterface.php | 29 + .../TranslatableInterface.php | 20 + .../TranslatorInterface.php | 68 + .../translation-contracts/TranslatorTrait.php | 225 + .../Catalogue/AbstractOperation.php | 187 + .../translation/Catalogue/MergeOperation.php | 72 + .../Catalogue/OperationInterface.php | 61 + .../translation/Catalogue/TargetOperation.php | 86 + .../CatalogueMetadataAwareInterface.php | 48 + .../Command/TranslationPullCommand.php | 184 + .../Command/TranslationPushCommand.php | 182 + .../translation/Command/TranslationTrait.php | 77 + .../translation/Command/XliffLintCommand.php | 285 + .../TranslationDataCollector.php | 148 + .../translation/DataCollectorTranslator.php | 143 + .../DataCollectorTranslatorPass.php | 36 + .../LoggingTranslatorPass.php | 59 + .../TranslationDumperPass.php | 38 + .../TranslationExtractorPass.php | 43 + .../DependencyInjection/TranslatorPass.php | 94 + .../TranslatorPathsPass.php | 145 + .../translation/Dumper/CsvFileDumper.php | 56 + .../translation/Dumper/DumperInterface.php | 32 + .../symfony/translation/Dumper/FileDumper.php | 108 + .../translation/Dumper/IcuResFileDumper.php | 95 + .../translation/Dumper/IniFileDumper.php | 39 + .../translation/Dumper/JsonFileDumper.php | 34 + .../translation/Dumper/MoFileDumper.php | 76 + .../translation/Dumper/PhpFileDumper.php | 32 + .../translation/Dumper/PoFileDumper.php | 131 + .../translation/Dumper/QtFileDumper.php | 55 + .../translation/Dumper/XliffFileDumper.php | 221 + .../translation/Dumper/YamlFileDumper.php | 56 + .../Exception/ExceptionInterface.php | 21 + .../Exception/IncompleteDsnException.php | 24 + .../Exception/InvalidArgumentException.php | 21 + .../Exception/InvalidResourceException.php | 21 + .../translation/Exception/LogicException.php | 21 + .../MissingRequiredOptionException.php | 25 + .../Exception/NotFoundResourceException.php | 21 + .../Exception/ProviderException.php | 41 + .../Exception/ProviderExceptionInterface.php | 23 + .../Exception/RuntimeException.php | 21 + .../Exception/UnsupportedSchemeException.php | 58 + .../Extractor/AbstractFileExtractor.php | 67 + .../translation/Extractor/ChainExtractor.php | 59 + .../Extractor/ExtractorInterface.php | 39 + .../translation/Extractor/PhpAstExtractor.php | 85 + .../translation/Extractor/PhpExtractor.php | 333 + .../Extractor/PhpStringTokenParser.php | 141 + .../Extractor/Visitor/AbstractVisitor.php | 135 + .../Extractor/Visitor/ConstraintVisitor.php | 112 + .../Extractor/Visitor/TransMethodVisitor.php | 65 + .../Visitor/TranslatableMessageVisitor.php | 65 + .../translation/Formatter/IntlFormatter.php | 57 + .../Formatter/IntlFormatterInterface.php | 27 + .../Formatter/MessageFormatter.php | 46 + .../Formatter/MessageFormatterInterface.php | 28 + .../translation/IdentityTranslator.php | 26 + 3rdparty/symfony/translation/LICENSE | 19 + .../translation/Loader/ArrayLoader.php | 57 + .../translation/Loader/CsvFileLoader.php | 64 + .../symfony/translation/Loader/FileLoader.php | 57 + .../translation/Loader/IcuDatFileLoader.php | 58 + .../translation/Loader/IcuResFileLoader.php | 86 + .../translation/Loader/IniFileLoader.php | 25 + .../translation/Loader/JsonFileLoader.php | 51 + .../translation/Loader/LoaderInterface.php | 32 + .../translation/Loader/MoFileLoader.php | 138 + .../translation/Loader/PhpFileLoader.php | 35 + .../translation/Loader/PoFileLoader.php | 147 + .../translation/Loader/QtFileLoader.php | 78 + .../translation/Loader/XliffFileLoader.php | 237 + .../translation/Loader/YamlFileLoader.php | 51 + .../symfony/translation/LocaleSwitcher.php | 78 + .../symfony/translation/LoggingTranslator.php | 115 + .../symfony/translation/MessageCatalogue.php | 338 + .../translation/MessageCatalogueInterface.php | 134 + .../translation/MetadataAwareInterface.php | 48 + .../Provider/AbstractProviderFactory.php | 37 + 3rdparty/symfony/translation/Provider/Dsn.php | 110 + .../Provider/FilteringProvider.php | 62 + .../translation/Provider/NullProvider.php | 39 + .../Provider/NullProviderFactory.php | 34 + .../Provider/ProviderFactoryInterface.php | 26 + .../Provider/ProviderInterface.php | 30 + .../TranslationProviderCollection.php | 57 + .../TranslationProviderCollectionFactory.php | 57 + .../PseudoLocalizationTranslator.php | 365 + .../translation/Reader/TranslationReader.php | 64 + .../Reader/TranslationReaderInterface.php | 29 + .../translation/Resources/functions.php | 22 + .../schemas/xliff-core-1.2-transitional.xsd | 2261 ++++++ .../Resources/schemas/xliff-core-2.0.xsd | 411 ++ .../translation/Resources/schemas/xml.xsd | 309 + .../translation/TranslatableMessage.php | 60 + 3rdparty/symfony/translation/Translator.php | 472 ++ .../symfony/translation/TranslatorBag.php | 102 + .../translation/TranslatorBagInterface.php | 36 + .../translation/Util/ArrayConverter.php | 142 + .../symfony/translation/Util/XliffUtils.php | 191 + .../translation/Writer/TranslationWriter.php | 75 + .../Writer/TranslationWriterInterface.php | 35 + 3rdparty/symfony/uid/AbstractUid.php | 176 + 3rdparty/symfony/uid/BinaryUtil.php | 175 + .../uid/Command/GenerateUlidCommand.php | 119 + .../uid/Command/GenerateUuidCommand.php | 208 + .../uid/Command/InspectUlidCommand.php | 67 + .../uid/Command/InspectUuidCommand.php | 85 + .../uid/Factory/NameBasedUuidFactory.php | 44 + .../uid/Factory/RandomBasedUuidFactory.php | 31 + .../uid/Factory/TimeBasedUuidFactory.php | 44 + 3rdparty/symfony/uid/Factory/UlidFactory.php | 22 + 3rdparty/symfony/uid/Factory/UuidFactory.php | 95 + 3rdparty/symfony/uid/LICENSE | 19 + 3rdparty/symfony/uid/MaxUlid.php | 20 + 3rdparty/symfony/uid/MaxUuid.php | 22 + 3rdparty/symfony/uid/NilUlid.php | 20 + 3rdparty/symfony/uid/NilUuid.php | 25 + .../symfony/uid/TimeBasedUidInterface.php | 22 + 3rdparty/symfony/uid/Ulid.php | 208 + 3rdparty/symfony/uid/Uuid.php | 185 + 3rdparty/symfony/uid/UuidV1.php | 73 + 3rdparty/symfony/uid/UuidV3.php | 29 + 3rdparty/symfony/uid/UuidV4.php | 36 + 3rdparty/symfony/uid/UuidV5.php | 29 + 3rdparty/symfony/uid/UuidV6.php | 66 + 3rdparty/symfony/uid/UuidV7.php | 125 + 3rdparty/symfony/uid/UuidV8.php | 27 + 3rdparty/wapmorgan/mp3info/LICENSE | 165 + 3rdparty/wapmorgan/mp3info/PATCHES.txt | 11 + 3rdparty/wapmorgan/mp3info/src/Mp3Info.php | 1090 +++ 3rdparty/web-auth/cose-lib/LICENSE | 21 + .../cose-lib/src/Algorithm/Algorithm.php | 10 + .../cose-lib/src/Algorithm/Mac/HS256.php | 30 + .../src/Algorithm/Mac/HS256Truncated64.php | 30 + .../cose-lib/src/Algorithm/Mac/HS384.php | 30 + .../cose-lib/src/Algorithm/Mac/HS512.php | 30 + .../cose-lib/src/Algorithm/Mac/Hmac.php | 43 + .../cose-lib/src/Algorithm/Mac/Mac.php | 15 + .../cose-lib/src/Algorithm/Manager.php | 61 + .../cose-lib/src/Algorithm/ManagerFactory.php | 57 + .../src/Algorithm/Signature/ECDSA/ECDSA.php | 50 + .../Algorithm/Signature/ECDSA/ECSignature.php | 132 + .../src/Algorithm/Signature/ECDSA/ES256.php | 38 + .../src/Algorithm/Signature/ECDSA/ES256K.php | 38 + .../src/Algorithm/Signature/ECDSA/ES384.php | 38 + .../src/Algorithm/Signature/ECDSA/ES512.php | 38 + .../src/Algorithm/Signature/EdDSA/Ed25519.php | 20 + .../src/Algorithm/Signature/EdDSA/Ed256.php | 36 + .../src/Algorithm/Signature/EdDSA/Ed512.php | 36 + .../src/Algorithm/Signature/EdDSA/EdDSA.php | 63 + .../src/Algorithm/Signature/RSA/PS256.php | 27 + .../src/Algorithm/Signature/RSA/PS384.php | 27 + .../src/Algorithm/Signature/RSA/PS512.php | 27 + .../src/Algorithm/Signature/RSA/PSSRSA.php | 183 + .../src/Algorithm/Signature/RSA/RS1.php | 27 + .../src/Algorithm/Signature/RSA/RS256.php | 27 + .../src/Algorithm/Signature/RSA/RS384.php | 27 + .../src/Algorithm/Signature/RSA/RS512.php | 27 + .../src/Algorithm/Signature/RSA/RSA.php | 49 + .../src/Algorithm/Signature/Signature.php | 15 + 3rdparty/web-auth/cose-lib/src/Algorithms.php | 175 + 3rdparty/web-auth/cose-lib/src/BigInteger.php | 108 + 3rdparty/web-auth/cose-lib/src/Hash.php | 61 + 3rdparty/web-auth/cose-lib/src/Key/Ec2Key.php | 195 + 3rdparty/web-auth/cose-lib/src/Key/Key.php | 111 + 3rdparty/web-auth/cose-lib/src/Key/OkpKey.php | 110 + 3rdparty/web-auth/cose-lib/src/Key/RsaKey.php | 262 + .../cose-lib/src/Key/SymmetricKey.php | 44 + 3rdparty/web-auth/webauthn-lib/LICENSE | 21 + .../AndroidKeyAttestationStatementSupport.php | 227 + ...idSafetyNetAttestationStatementSupport.php | 382 + .../AppleAttestationStatementSupport.php | 177 + .../AttestationObject.php | 84 + .../AttestationObjectLoader.php | 115 + .../AttestationStatement.php | 190 + .../AttestationStatementSupport.php | 23 + .../AttestationStatementSupportManager.php | 51 + .../FidoU2FAttestationStatementSupport.php | 181 + .../NoneAttestationStatementSupport.php | 78 + .../PackedAttestationStatementSupport.php | 303 + .../TPMAttestationStatementSupport.php | 445 ++ .../src/AttestedCredentialData.php | 128 + .../AuthenticationExtension.php | 50 + .../AuthenticationExtensions.php | 164 + .../AuthenticationExtensionsClientInputs.php | 12 + .../AuthenticationExtensionsClientOutputs.php | 12 + ...nticationExtensionsClientOutputsLoader.php | 25 + .../ExtensionOutputChecker.php | 10 + .../ExtensionOutputCheckerHandler.php | 30 + .../ExtensionOutputError.php | 29 + .../src/AuthenticatorAssertionResponse.php | 51 + ...uthenticatorAssertionResponseValidator.php | 325 + .../src/AuthenticatorAttestationResponse.php | 55 + ...henticatorAttestationResponseValidator.php | 328 + .../webauthn-lib/src/AuthenticatorData.php | 140 + .../src/AuthenticatorDataLoader.php | 117 + .../src/AuthenticatorResponse.php | 25 + .../src/AuthenticatorSelectionCriteria.php | 252 + .../src/CeremonyStep/CeremonyStep.php | 22 + .../src/CeremonyStep/CeremonyStepManager.php | 40 + .../CeremonyStepManagerFactory.php | 159 + .../src/CeremonyStep/CheckAlgorithm.php | 76 + .../CheckAllowedCredentialList.php | 38 + .../CheckAttestationFormatIsKnownAndValid.php | 48 + .../CheckBackupBitsAreConsistent.php | 31 + .../src/CeremonyStep/CheckChallenge.php | 31 + .../CheckClientDataCollectorType.php | 41 + .../src/CeremonyStep/CheckCounter.php | 36 + .../src/CeremonyStep/CheckCredentialId.php | 28 + .../src/CeremonyStep/CheckExtensions.php | 37 + .../CheckHasAttestedCredentialData.php | 32 + .../CeremonyStep/CheckMetadataStatement.php | 190 + .../src/CeremonyStep/CheckOrigin.php | 78 + .../CheckRelyingPartyIdIdHash.php | 63 + .../src/CeremonyStep/CheckSignature.php | 90 + .../src/CeremonyStep/CheckTopOrigin.php | 41 + .../src/CeremonyStep/CheckUserHandle.php | 37 + .../CeremonyStep/CheckUserVerification.php | 33 + .../src/CeremonyStep/CheckUserWasPresent.php | 26 + .../CeremonyStep/HostTopOriginValidator.php | 22 + .../src/CeremonyStep/TopOriginValidator.php | 10 + .../CertificateChainChecker.php | 15 + .../PhpCertificateChainChecker.php | 15 + .../webauthn-lib/src/CertificateToolbox.php | 15 + .../ClientDataCollector.php | 24 + .../ClientDataCollectorManager.php | 43 + .../WebauthnAuthenticationCollector.php | 34 + .../webauthn-lib/src/CollectedClientData.php | 177 + .../src/Counter/CounterChecker.php | 12 + .../src/Counter/ThrowExceptionIfInvalid.php | 41 + .../web-auth/webauthn-lib/src/Credential.php | 60 + .../AttestationObjectDenormalizer.php | 63 + .../AttestationStatementDenormalizer.php | 39 + .../AttestedCredentialDataNormalizer.php | 45 + .../AuthenticationExtensionNormalizer.php | 37 + .../AuthenticationExtensionsDenormalizer.php | 79 + ...enticatorAssertionResponseDenormalizer.php | 59 + ...ticatorAttestationResponseDenormalizer.php | 59 + .../AuthenticatorDataDenormalizer.php | 140 + .../AuthenticatorResponseDenormalizer.php | 45 + .../CollectedClientDataDenormalizer.php | 36 + .../ExtensionDescriptorDenormalizer.php | 52 + .../PublicKeyCredentialDenormalizer.php | 53 + ...ublicKeyCredentialDescriptorNormalizer.php | 47 + ...PublicKeyCredentialOptionsDenormalizer.php | 179 + ...licKeyCredentialParametersDenormalizer.php | 37 + .../PublicKeyCredentialSourceDenormalizer.php | 96 + ...licKeyCredentialUserEntityDenormalizer.php | 67 + .../Denormalizer/TrustPathDenormalizer.php | 66 + ...ationMethodANDCombinationsDenormalizer.php | 45 + .../WebauthnSerializerFactory.php | 92 + .../src/Event/AttestationObjectLoaded.php | 20 + .../src/Event/AttestationStatementLoaded.php | 20 + ...AssertionResponseValidationFailedEvent.php | 94 + ...ertionResponseValidationSucceededEvent.php | 90 + ...testationResponseValidationFailedEvent.php | 65 + ...tationResponseValidationSucceededEvent.php | 65 + .../BeforeCertificateChainValidation.php | 28 + .../src/Event/CanDispatchEvents.php | 12 + .../CertificateChainValidationFailed.php | 28 + .../CertificateChainValidationSucceeded.php | 28 + .../src/Event/MetadataStatementFound.php | 23 + .../src/Event/NullEventDispatcher.php | 18 + .../webauthn-lib/src/Event/WebauthnEvent.php | 9 + .../AttestationStatementException.php | 9 + .../AttestationStatementLoadingException.php | 32 + ...estationStatementVerificationException.php | 15 + .../AuthenticationExtensionException.php | 15 + ...enticatorResponseVerificationException.php | 15 + .../Exception/CertificateChainException.php | 36 + .../src/Exception/CertificateException.php | 18 + .../CertificateRevocationListException.php | 26 + .../src/Exception/CounterException.php | 28 + .../Exception/ExpiredCertificateException.php | 21 + .../InvalidAttestationStatementException.php | 27 + .../Exception/InvalidCertificateException.php | 26 + .../src/Exception/InvalidDataException.php | 23 + .../Exception/InvalidTrustPathException.php | 15 + .../Exception/InvalidUserHandleException.php | 15 + .../Exception/MetadataServiceException.php | 16 + .../Exception/MetadataStatementException.php | 9 + .../MetadataStatementLoadingException.php | 20 + .../MissingMetadataStatementException.php | 29 + .../Exception/RevokedCertificateException.php | 12 + .../Exception/UnsupportedFeatureException.php | 15 + .../src/Exception/WebauthnException.php | 16 + .../src/FakeCredentialGenerator.php | 15 + .../src/MetadataService/CanLogData.php | 12 + .../CertificateChainValidator.php | 14 + .../CertificateChain/CertificateToolbox.php | 71 + .../PhpCertificateChainValidator.php | 295 + .../ExtensionDescriptorDenormalizer.php | 14 + .../MetadataStatementSerializerFactory.php | 23 + .../BeforeCertificateChainValidation.php | 14 + .../Event/CanDispatchEvents.php | 14 + .../CertificateChainValidationFailed.php | 14 + .../CertificateChainValidationSucceeded.php | 14 + .../Event/MetadataStatementFound.php | 14 + .../Event/NullEventDispatcher.php | 14 + .../MetadataService/Event/WebauthnEvent.php | 14 + .../Exception/CertificateChainException.php | 14 + .../Exception/CertificateException.php | 14 + .../CertificateRevocationListException.php | 14 + .../Exception/ExpiredCertificateException.php | 14 + .../Exception/InvalidCertificateException.php | 14 + .../Exception/MetadataServiceException.php | 14 + .../Exception/MetadataStatementException.php | 14 + .../MetadataStatementLoadingException.php | 14 + .../MissingMetadataStatementException.php | 14 + .../Exception/RevokedCertificateException.php | 14 + .../MetadataStatementRepository.php | 12 + .../src/MetadataService/Psr18HttpClient.php | 128 + .../Service/ChainedMetadataServices.php | 66 + .../DistantResourceMetadataService.php | 170 + .../FidoAllianceCompliantMetadataService.php | 286 + .../Service/FolderResourceMetadataService.php | 79 + .../Service/InMemoryMetadataService.php | 73 + .../Service/JsonMetadataService.php | 81 + .../Service/LocalResourceMetadataService.php | 102 + .../Service/MetadataBLOBPayload.php | 162 + .../Service/MetadataBLOBPayloadEntry.php | 231 + .../Service/MetadataService.php | 19 + .../Service/StringMetadataService.php | 77 + .../Statement/AbstractDescriptor.php | 41 + .../Statement/AlternativeDescriptions.php | 61 + .../Statement/AuthenticatorGetInfo.php | 51 + .../Statement/AuthenticatorStatus.php | 72 + .../Statement/BiometricAccuracyDescriptor.php | 93 + .../Statement/BiometricStatusReport.php | 146 + .../Statement/CodeAccuracyDescriptor.php | 95 + .../DisplayPNGCharacteristicsDescriptor.php | 206 + .../Statement/EcdaaTrustAnchor.php | 107 + .../Statement/ExtensionDescriptor.php | 112 + .../Statement/MetadataStatement.php | 700 ++ .../Statement/PatternAccuracyDescriptor.php | 82 + .../Statement/RgbPaletteEntry.php | 93 + .../Statement/RogueListEntry.php | 76 + .../Statement/StatusReport.php | 205 + .../VerificationMethodANDCombinations.php | 79 + .../VerificationMethodDescriptor.php | 273 + .../src/MetadataService/Statement/Version.php | 88 + .../StatusReportRepository.php | 15 + .../src/MetadataService/ValueFilter.php | 21 + .../webauthn-lib/src/PublicKeyCredential.php | 80 + .../PublicKeyCredentialCreationOptions.php | 351 + .../src/PublicKeyCredentialDescriptor.php | 130 + ...ublicKeyCredentialDescriptorCollection.php | 137 + .../src/PublicKeyCredentialEntity.php | 49 + .../src/PublicKeyCredentialLoader.php | 191 + .../src/PublicKeyCredentialOptions.php | 123 + .../src/PublicKeyCredentialParameters.php | 97 + .../src/PublicKeyCredentialRequestOptions.php | 239 + .../src/PublicKeyCredentialRpEntity.php | 67 + .../src/PublicKeyCredentialSource.php | 275 + .../PublicKeyCredentialSourceRepository.php | 21 + .../src/PublicKeyCredentialUserEntity.php | 102 + .../src/SimpleFakeCredentialGenerator.php | 67 + .../webauthn-lib/src/StringStream.php | 61 + .../IgnoreTokenBindingHandler.php | 24 + .../TokenBinding/SecTokenBindingHandler.php | 43 + .../src/TokenBinding/TokenBinding.php | 78 + .../src/TokenBinding/TokenBindingHandler.php | 16 + .../TokenBindingNotSupportedHandler.php | 28 + .../src/TrustPath/CertificateTrustPath.php | 70 + .../src/TrustPath/EcdaaKeyIdTrustPath.php | 45 + .../src/TrustPath/EmptyTrustPath.php | 38 + .../webauthn-lib/src/TrustPath/TrustPath.php | 17 + .../src/TrustPath/TrustPathLoader.php | 33 + .../webauthn-lib/src/U2FPublicKey.php | 56 + .../web-auth/webauthn-lib/src/Util/Base64.php | 26 + .../src/Util/CoseSignatureFixer.php | 59 + install.sh | 17 + lib/private/Log.php | 2 +- 3354 files changed, 505213 insertions(+), 3 deletions(-) create mode 100644 3rdparty/.patches/mp3info-break-frame-parsing.patch create mode 100644 3rdparty/.patches/mp3info-fix-incorrect-lookup-for-mpeg-header.patch create mode 100644 3rdparty/LICENSE INFO create mode 100644 3rdparty/autoload.php create mode 100644 3rdparty/aws/aws-crt-php/LICENSE create mode 100644 3rdparty/aws/aws-crt-php/NOTICE create mode 100644 3rdparty/aws/aws-crt-php/src/AWS/CRT/Auth/AwsCredentials.php create mode 100644 3rdparty/aws/aws-crt-php/src/AWS/CRT/Auth/CredentialsProvider.php create mode 100644 3rdparty/aws/aws-crt-php/src/AWS/CRT/Auth/Signable.php create mode 100644 3rdparty/aws/aws-crt-php/src/AWS/CRT/Auth/SignatureType.php create mode 100644 3rdparty/aws/aws-crt-php/src/AWS/CRT/Auth/SignedBodyHeaderType.php create mode 100644 3rdparty/aws/aws-crt-php/src/AWS/CRT/Auth/Signing.php create mode 100644 3rdparty/aws/aws-crt-php/src/AWS/CRT/Auth/SigningAlgorithm.php create mode 100644 3rdparty/aws/aws-crt-php/src/AWS/CRT/Auth/SigningConfigAWS.php create mode 100644 3rdparty/aws/aws-crt-php/src/AWS/CRT/Auth/SigningResult.php create mode 100644 3rdparty/aws/aws-crt-php/src/AWS/CRT/Auth/StaticCredentialsProvider.php create mode 100644 3rdparty/aws/aws-crt-php/src/AWS/CRT/CRT.php create mode 100644 3rdparty/aws/aws-crt-php/src/AWS/CRT/HTTP/Headers.php create mode 100644 3rdparty/aws/aws-crt-php/src/AWS/CRT/HTTP/Message.php create mode 100644 3rdparty/aws/aws-crt-php/src/AWS/CRT/HTTP/Request.php create mode 100644 3rdparty/aws/aws-crt-php/src/AWS/CRT/HTTP/Response.php create mode 100644 3rdparty/aws/aws-crt-php/src/AWS/CRT/IO/EventLoopGroup.php create mode 100644 3rdparty/aws/aws-crt-php/src/AWS/CRT/IO/InputStream.php create mode 100644 3rdparty/aws/aws-crt-php/src/AWS/CRT/Internal/Encoding.php create mode 100644 3rdparty/aws/aws-crt-php/src/AWS/CRT/Internal/Extension.php create mode 100644 3rdparty/aws/aws-crt-php/src/AWS/CRT/Log.php create mode 100644 3rdparty/aws/aws-crt-php/src/AWS/CRT/NativeResource.php create mode 100644 3rdparty/aws/aws-crt-php/src/AWS/CRT/Options.php create mode 100644 3rdparty/aws/aws-sdk-php/CRT_INSTRUCTIONS.md create mode 100644 3rdparty/aws/aws-sdk-php/LICENSE create mode 100644 3rdparty/aws/aws-sdk-php/NOTICE create mode 100644 3rdparty/aws/aws-sdk-php/THIRD-PARTY-LICENSES create mode 100644 3rdparty/aws/aws-sdk-php/src/AbstractConfigurationProvider.php create mode 100644 3rdparty/aws/aws-sdk-php/src/Api/AbstractModel.php create mode 100644 3rdparty/aws/aws-sdk-php/src/Api/ApiProvider.php create mode 100644 3rdparty/aws/aws-sdk-php/src/Api/DateTimeResult.php create mode 100644 3rdparty/aws/aws-sdk-php/src/Api/DocModel.php create mode 100644 3rdparty/aws/aws-sdk-php/src/Api/ErrorParser/AbstractErrorParser.php create mode 100644 3rdparty/aws/aws-sdk-php/src/Api/ErrorParser/JsonParserTrait.php create mode 100644 3rdparty/aws/aws-sdk-php/src/Api/ErrorParser/JsonRpcErrorParser.php create mode 100644 3rdparty/aws/aws-sdk-php/src/Api/ErrorParser/RestJsonErrorParser.php create mode 100644 3rdparty/aws/aws-sdk-php/src/Api/ErrorParser/XmlErrorParser.php create mode 100644 3rdparty/aws/aws-sdk-php/src/Api/ListShape.php create mode 100644 3rdparty/aws/aws-sdk-php/src/Api/MapShape.php create mode 100644 3rdparty/aws/aws-sdk-php/src/Api/Operation.php create mode 100644 3rdparty/aws/aws-sdk-php/src/Api/Parser/AbstractParser.php create mode 100644 3rdparty/aws/aws-sdk-php/src/Api/Parser/AbstractRestParser.php create mode 100644 3rdparty/aws/aws-sdk-php/src/Api/Parser/Crc32ValidatingParser.php create mode 100644 3rdparty/aws/aws-sdk-php/src/Api/Parser/DecodingEventStreamIterator.php create mode 100644 3rdparty/aws/aws-sdk-php/src/Api/Parser/EventParsingIterator.php create mode 100644 3rdparty/aws/aws-sdk-php/src/Api/Parser/Exception/ParserException.php create mode 100644 3rdparty/aws/aws-sdk-php/src/Api/Parser/JsonParser.php create mode 100644 3rdparty/aws/aws-sdk-php/src/Api/Parser/JsonRpcParser.php create mode 100644 3rdparty/aws/aws-sdk-php/src/Api/Parser/MetadataParserTrait.php create mode 100644 3rdparty/aws/aws-sdk-php/src/Api/Parser/NonSeekableStreamDecodingEventStreamIterator.php create mode 100644 3rdparty/aws/aws-sdk-php/src/Api/Parser/PayloadParserTrait.php create mode 100644 3rdparty/aws/aws-sdk-php/src/Api/Parser/QueryParser.php create mode 100644 3rdparty/aws/aws-sdk-php/src/Api/Parser/RestJsonParser.php create mode 100644 3rdparty/aws/aws-sdk-php/src/Api/Parser/RestXmlParser.php create mode 100644 3rdparty/aws/aws-sdk-php/src/Api/Parser/XmlParser.php create mode 100644 3rdparty/aws/aws-sdk-php/src/Api/Serializer/Ec2ParamBuilder.php create mode 100644 3rdparty/aws/aws-sdk-php/src/Api/Serializer/JsonBody.php create mode 100644 3rdparty/aws/aws-sdk-php/src/Api/Serializer/JsonRpcSerializer.php create mode 100644 3rdparty/aws/aws-sdk-php/src/Api/Serializer/QueryParamBuilder.php create mode 100644 3rdparty/aws/aws-sdk-php/src/Api/Serializer/QuerySerializer.php create mode 100644 3rdparty/aws/aws-sdk-php/src/Api/Serializer/RestJsonSerializer.php create mode 100644 3rdparty/aws/aws-sdk-php/src/Api/Serializer/RestSerializer.php create mode 100644 3rdparty/aws/aws-sdk-php/src/Api/Serializer/RestXmlSerializer.php create mode 100644 3rdparty/aws/aws-sdk-php/src/Api/Serializer/XmlBody.php create mode 100644 3rdparty/aws/aws-sdk-php/src/Api/Service.php create mode 100644 3rdparty/aws/aws-sdk-php/src/Api/Shape.php create mode 100644 3rdparty/aws/aws-sdk-php/src/Api/ShapeMap.php create mode 100644 3rdparty/aws/aws-sdk-php/src/Api/StructureShape.php create mode 100644 3rdparty/aws/aws-sdk-php/src/Api/SupportedProtocols.php create mode 100644 3rdparty/aws/aws-sdk-php/src/Api/TimestampShape.php create mode 100644 3rdparty/aws/aws-sdk-php/src/Api/Validator.php create mode 100644 3rdparty/aws/aws-sdk-php/src/Arn/AccessPointArn.php create mode 100644 3rdparty/aws/aws-sdk-php/src/Arn/AccessPointArnInterface.php create mode 100644 3rdparty/aws/aws-sdk-php/src/Arn/Arn.php create mode 100644 3rdparty/aws/aws-sdk-php/src/Arn/ArnInterface.php create mode 100644 3rdparty/aws/aws-sdk-php/src/Arn/ArnParser.php create mode 100644 3rdparty/aws/aws-sdk-php/src/Arn/Exception/InvalidArnException.php create mode 100644 3rdparty/aws/aws-sdk-php/src/Arn/ObjectLambdaAccessPointArn.php create mode 100644 3rdparty/aws/aws-sdk-php/src/Arn/ResourceTypeAndIdTrait.php create mode 100644 3rdparty/aws/aws-sdk-php/src/Arn/S3/AccessPointArn.php create mode 100644 3rdparty/aws/aws-sdk-php/src/Arn/S3/BucketArnInterface.php create mode 100644 3rdparty/aws/aws-sdk-php/src/Arn/S3/MultiRegionAccessPointArn.php create mode 100644 3rdparty/aws/aws-sdk-php/src/Arn/S3/OutpostsAccessPointArn.php create mode 100644 3rdparty/aws/aws-sdk-php/src/Arn/S3/OutpostsArnInterface.php create mode 100644 3rdparty/aws/aws-sdk-php/src/Arn/S3/OutpostsBucketArn.php create mode 100644 3rdparty/aws/aws-sdk-php/src/Auth/AuthSchemeResolver.php create mode 100644 3rdparty/aws/aws-sdk-php/src/Auth/AuthSchemeResolverInterface.php create mode 100644 3rdparty/aws/aws-sdk-php/src/Auth/AuthSelectionMiddleware.php create mode 100644 3rdparty/aws/aws-sdk-php/src/Auth/Exception/UnresolvedAuthSchemeException.php create mode 100644 3rdparty/aws/aws-sdk-php/src/AwsClient.php create mode 100644 3rdparty/aws/aws-sdk-php/src/AwsClientInterface.php create mode 100644 3rdparty/aws/aws-sdk-php/src/AwsClientTrait.php create mode 100644 3rdparty/aws/aws-sdk-php/src/CacheInterface.php create mode 100644 3rdparty/aws/aws-sdk-php/src/ClientResolver.php create mode 100644 3rdparty/aws/aws-sdk-php/src/ClientSideMonitoring/AbstractMonitoringMiddleware.php create mode 100644 3rdparty/aws/aws-sdk-php/src/ClientSideMonitoring/ApiCallAttemptMonitoringMiddleware.php create mode 100644 3rdparty/aws/aws-sdk-php/src/ClientSideMonitoring/ApiCallMonitoringMiddleware.php create mode 100644 3rdparty/aws/aws-sdk-php/src/ClientSideMonitoring/Configuration.php create mode 100644 3rdparty/aws/aws-sdk-php/src/ClientSideMonitoring/ConfigurationInterface.php create mode 100644 3rdparty/aws/aws-sdk-php/src/ClientSideMonitoring/ConfigurationProvider.php create mode 100644 3rdparty/aws/aws-sdk-php/src/ClientSideMonitoring/Exception/ConfigurationException.php create mode 100644 3rdparty/aws/aws-sdk-php/src/ClientSideMonitoring/MonitoringMiddlewareInterface.php create mode 100644 3rdparty/aws/aws-sdk-php/src/Command.php create mode 100644 3rdparty/aws/aws-sdk-php/src/CommandInterface.php create mode 100644 3rdparty/aws/aws-sdk-php/src/CommandPool.php create mode 100644 3rdparty/aws/aws-sdk-php/src/Configuration/ConfigurationResolver.php create mode 100644 3rdparty/aws/aws-sdk-php/src/ConfigurationProviderInterface.php create mode 100644 3rdparty/aws/aws-sdk-php/src/Credentials/AssumeRoleCredentialProvider.php create mode 100644 3rdparty/aws/aws-sdk-php/src/Credentials/AssumeRoleWithWebIdentityCredentialProvider.php create mode 100644 3rdparty/aws/aws-sdk-php/src/Credentials/CredentialProvider.php create mode 100644 3rdparty/aws/aws-sdk-php/src/Credentials/CredentialSources.php create mode 100644 3rdparty/aws/aws-sdk-php/src/Credentials/Credentials.php create mode 100644 3rdparty/aws/aws-sdk-php/src/Credentials/CredentialsInterface.php create mode 100644 3rdparty/aws/aws-sdk-php/src/Credentials/CredentialsUtils.php create mode 100644 3rdparty/aws/aws-sdk-php/src/Credentials/EcsCredentialProvider.php create mode 100644 3rdparty/aws/aws-sdk-php/src/Credentials/InstanceProfileProvider.php create mode 100644 3rdparty/aws/aws-sdk-php/src/Crypto/AbstractCryptoClient.php create mode 100644 3rdparty/aws/aws-sdk-php/src/Crypto/AbstractCryptoClientV2.php create mode 100644 3rdparty/aws/aws-sdk-php/src/Crypto/AesDecryptingStream.php create mode 100644 3rdparty/aws/aws-sdk-php/src/Crypto/AesEncryptingStream.php create mode 100644 3rdparty/aws/aws-sdk-php/src/Crypto/AesGcmDecryptingStream.php create mode 100644 3rdparty/aws/aws-sdk-php/src/Crypto/AesGcmEncryptingStream.php create mode 100644 3rdparty/aws/aws-sdk-php/src/Crypto/AesStreamInterface.php create mode 100644 3rdparty/aws/aws-sdk-php/src/Crypto/AesStreamInterfaceV2.php create mode 100644 3rdparty/aws/aws-sdk-php/src/Crypto/Cipher/Cbc.php create mode 100644 3rdparty/aws/aws-sdk-php/src/Crypto/Cipher/CipherBuilderTrait.php create mode 100644 3rdparty/aws/aws-sdk-php/src/Crypto/Cipher/CipherMethod.php create mode 100644 3rdparty/aws/aws-sdk-php/src/Crypto/DecryptionTrait.php create mode 100644 3rdparty/aws/aws-sdk-php/src/Crypto/DecryptionTraitV2.php create mode 100644 3rdparty/aws/aws-sdk-php/src/Crypto/EncryptionTrait.php create mode 100644 3rdparty/aws/aws-sdk-php/src/Crypto/EncryptionTraitV2.php create mode 100644 3rdparty/aws/aws-sdk-php/src/Crypto/KmsMaterialsProvider.php create mode 100644 3rdparty/aws/aws-sdk-php/src/Crypto/KmsMaterialsProviderV2.php create mode 100644 3rdparty/aws/aws-sdk-php/src/Crypto/MaterialsProvider.php create mode 100644 3rdparty/aws/aws-sdk-php/src/Crypto/MaterialsProviderInterface.php create mode 100644 3rdparty/aws/aws-sdk-php/src/Crypto/MaterialsProviderInterfaceV2.php create mode 100644 3rdparty/aws/aws-sdk-php/src/Crypto/MaterialsProviderV2.php create mode 100644 3rdparty/aws/aws-sdk-php/src/Crypto/MetadataEnvelope.php create mode 100644 3rdparty/aws/aws-sdk-php/src/Crypto/MetadataStrategyInterface.php create mode 100644 3rdparty/aws/aws-sdk-php/src/DefaultsMode/Configuration.php create mode 100644 3rdparty/aws/aws-sdk-php/src/DefaultsMode/ConfigurationInterface.php create mode 100644 3rdparty/aws/aws-sdk-php/src/DefaultsMode/ConfigurationProvider.php create mode 100644 3rdparty/aws/aws-sdk-php/src/DefaultsMode/Exception/ConfigurationException.php create mode 100644 3rdparty/aws/aws-sdk-php/src/DoctrineCacheAdapter.php create mode 100644 3rdparty/aws/aws-sdk-php/src/Endpoint/EndpointProvider.php create mode 100644 3rdparty/aws/aws-sdk-php/src/Endpoint/Partition.php create mode 100644 3rdparty/aws/aws-sdk-php/src/Endpoint/PartitionEndpointProvider.php create mode 100644 3rdparty/aws/aws-sdk-php/src/Endpoint/PartitionInterface.php create mode 100644 3rdparty/aws/aws-sdk-php/src/Endpoint/PatternEndpointProvider.php create mode 100644 3rdparty/aws/aws-sdk-php/src/Endpoint/UseDualstackEndpoint/Configuration.php create mode 100644 3rdparty/aws/aws-sdk-php/src/Endpoint/UseDualstackEndpoint/ConfigurationInterface.php create mode 100644 3rdparty/aws/aws-sdk-php/src/Endpoint/UseDualstackEndpoint/ConfigurationProvider.php create mode 100644 3rdparty/aws/aws-sdk-php/src/Endpoint/UseDualstackEndpoint/Exception/ConfigurationException.php create mode 100644 3rdparty/aws/aws-sdk-php/src/Endpoint/UseFipsEndpoint/Configuration.php create mode 100644 3rdparty/aws/aws-sdk-php/src/Endpoint/UseFipsEndpoint/ConfigurationInterface.php create mode 100644 3rdparty/aws/aws-sdk-php/src/Endpoint/UseFipsEndpoint/ConfigurationProvider.php create mode 100644 3rdparty/aws/aws-sdk-php/src/Endpoint/UseFipsEndpoint/Exception/ConfigurationException.php create mode 100644 3rdparty/aws/aws-sdk-php/src/EndpointDiscovery/Configuration.php create mode 100644 3rdparty/aws/aws-sdk-php/src/EndpointDiscovery/ConfigurationInterface.php create mode 100644 3rdparty/aws/aws-sdk-php/src/EndpointDiscovery/ConfigurationProvider.php create mode 100644 3rdparty/aws/aws-sdk-php/src/EndpointDiscovery/EndpointDiscoveryMiddleware.php create mode 100644 3rdparty/aws/aws-sdk-php/src/EndpointDiscovery/EndpointList.php create mode 100644 3rdparty/aws/aws-sdk-php/src/EndpointDiscovery/Exception/ConfigurationException.php create mode 100644 3rdparty/aws/aws-sdk-php/src/EndpointParameterMiddleware.php create mode 100644 3rdparty/aws/aws-sdk-php/src/EndpointV2/EndpointDefinitionProvider.php create mode 100644 3rdparty/aws/aws-sdk-php/src/EndpointV2/EndpointProviderV2.php create mode 100644 3rdparty/aws/aws-sdk-php/src/EndpointV2/EndpointV2Middleware.php create mode 100644 3rdparty/aws/aws-sdk-php/src/EndpointV2/EndpointV2SerializerTrait.php create mode 100644 3rdparty/aws/aws-sdk-php/src/EndpointV2/Rule/AbstractRule.php create mode 100644 3rdparty/aws/aws-sdk-php/src/EndpointV2/Rule/EndpointRule.php create mode 100644 3rdparty/aws/aws-sdk-php/src/EndpointV2/Rule/ErrorRule.php create mode 100644 3rdparty/aws/aws-sdk-php/src/EndpointV2/Rule/RuleCreator.php create mode 100644 3rdparty/aws/aws-sdk-php/src/EndpointV2/Rule/TreeRule.php create mode 100644 3rdparty/aws/aws-sdk-php/src/EndpointV2/Ruleset/Ruleset.php create mode 100644 3rdparty/aws/aws-sdk-php/src/EndpointV2/Ruleset/RulesetEndpoint.php create mode 100644 3rdparty/aws/aws-sdk-php/src/EndpointV2/Ruleset/RulesetParameter.php create mode 100644 3rdparty/aws/aws-sdk-php/src/EndpointV2/Ruleset/RulesetStandardLibrary.php create mode 100644 3rdparty/aws/aws-sdk-php/src/Exception/AwsException.php create mode 100644 3rdparty/aws/aws-sdk-php/src/Exception/CommonRuntimeException.php create mode 100644 3rdparty/aws/aws-sdk-php/src/Exception/CouldNotCreateChecksumException.php create mode 100644 3rdparty/aws/aws-sdk-php/src/Exception/CredentialsException.php create mode 100644 3rdparty/aws/aws-sdk-php/src/Exception/CryptoException.php create mode 100644 3rdparty/aws/aws-sdk-php/src/Exception/CryptoPolyfillException.php create mode 100644 3rdparty/aws/aws-sdk-php/src/Exception/EventStreamDataException.php create mode 100644 3rdparty/aws/aws-sdk-php/src/Exception/IncalculablePayloadException.php create mode 100644 3rdparty/aws/aws-sdk-php/src/Exception/InvalidJsonException.php create mode 100644 3rdparty/aws/aws-sdk-php/src/Exception/InvalidRegionException.php create mode 100644 3rdparty/aws/aws-sdk-php/src/Exception/MultipartUploadException.php create mode 100644 3rdparty/aws/aws-sdk-php/src/Exception/TokenException.php create mode 100644 3rdparty/aws/aws-sdk-php/src/Exception/UnresolvedApiException.php create mode 100644 3rdparty/aws/aws-sdk-php/src/Exception/UnresolvedEndpointException.php create mode 100644 3rdparty/aws/aws-sdk-php/src/Exception/UnresolvedSignatureException.php create mode 100644 3rdparty/aws/aws-sdk-php/src/Handler/Guzzle/GuzzleHandler.php create mode 100644 3rdparty/aws/aws-sdk-php/src/Handler/GuzzleV6/GuzzleHandler.php create mode 100644 3rdparty/aws/aws-sdk-php/src/HandlerList.php create mode 100644 3rdparty/aws/aws-sdk-php/src/HasDataTrait.php create mode 100644 3rdparty/aws/aws-sdk-php/src/HasMonitoringEventsTrait.php create mode 100644 3rdparty/aws/aws-sdk-php/src/HashInterface.php create mode 100644 3rdparty/aws/aws-sdk-php/src/HashingStream.php create mode 100644 3rdparty/aws/aws-sdk-php/src/History.php create mode 100644 3rdparty/aws/aws-sdk-php/src/IdempotencyTokenMiddleware.php create mode 100644 3rdparty/aws/aws-sdk-php/src/Identity/AwsCredentialIdentity.php create mode 100644 3rdparty/aws/aws-sdk-php/src/Identity/BearerTokenIdentity.php create mode 100644 3rdparty/aws/aws-sdk-php/src/Identity/IdentityInterface.php create mode 100644 3rdparty/aws/aws-sdk-php/src/Identity/S3/S3ExpressIdentity.php create mode 100644 3rdparty/aws/aws-sdk-php/src/Identity/S3/S3ExpressIdentityProvider.php create mode 100644 3rdparty/aws/aws-sdk-php/src/InputValidationMiddleware.php create mode 100644 3rdparty/aws/aws-sdk-php/src/JsonCompiler.php create mode 100644 3rdparty/aws/aws-sdk-php/src/Kms/Exception/KmsException.php create mode 100644 3rdparty/aws/aws-sdk-php/src/Kms/KmsClient.php create mode 100644 3rdparty/aws/aws-sdk-php/src/LruArrayCache.php create mode 100644 3rdparty/aws/aws-sdk-php/src/MetricsBuilder.php create mode 100644 3rdparty/aws/aws-sdk-php/src/Middleware.php create mode 100644 3rdparty/aws/aws-sdk-php/src/MockHandler.php create mode 100644 3rdparty/aws/aws-sdk-php/src/MonitoringEventsInterface.php create mode 100644 3rdparty/aws/aws-sdk-php/src/MultiRegionClient.php create mode 100644 3rdparty/aws/aws-sdk-php/src/Multipart/AbstractUploadManager.php create mode 100644 3rdparty/aws/aws-sdk-php/src/Multipart/AbstractUploader.php create mode 100644 3rdparty/aws/aws-sdk-php/src/Multipart/UploadState.php create mode 100644 3rdparty/aws/aws-sdk-php/src/PhpHash.php create mode 100644 3rdparty/aws/aws-sdk-php/src/PresignUrlMiddleware.php create mode 100644 3rdparty/aws/aws-sdk-php/src/Psr16CacheAdapter.php create mode 100644 3rdparty/aws/aws-sdk-php/src/PsrCacheAdapter.php create mode 100644 3rdparty/aws/aws-sdk-php/src/QueryCompatibleInputMiddleware.php create mode 100644 3rdparty/aws/aws-sdk-php/src/RequestCompressionMiddleware.php create mode 100644 3rdparty/aws/aws-sdk-php/src/ResponseContainerInterface.php create mode 100644 3rdparty/aws/aws-sdk-php/src/Result.php create mode 100644 3rdparty/aws/aws-sdk-php/src/ResultInterface.php create mode 100644 3rdparty/aws/aws-sdk-php/src/ResultPaginator.php create mode 100644 3rdparty/aws/aws-sdk-php/src/Retry/Configuration.php create mode 100644 3rdparty/aws/aws-sdk-php/src/Retry/ConfigurationInterface.php create mode 100644 3rdparty/aws/aws-sdk-php/src/Retry/ConfigurationProvider.php create mode 100644 3rdparty/aws/aws-sdk-php/src/Retry/Exception/ConfigurationException.php create mode 100644 3rdparty/aws/aws-sdk-php/src/Retry/QuotaManager.php create mode 100644 3rdparty/aws/aws-sdk-php/src/Retry/RateLimiter.php create mode 100644 3rdparty/aws/aws-sdk-php/src/Retry/RetryHelperTrait.php create mode 100644 3rdparty/aws/aws-sdk-php/src/RetryMiddleware.php create mode 100644 3rdparty/aws/aws-sdk-php/src/RetryMiddlewareV2.php create mode 100644 3rdparty/aws/aws-sdk-php/src/S3/AmbiguousSuccessParser.php create mode 100644 3rdparty/aws/aws-sdk-php/src/S3/ApplyChecksumMiddleware.php create mode 100644 3rdparty/aws/aws-sdk-php/src/S3/BatchDelete.php create mode 100644 3rdparty/aws/aws-sdk-php/src/S3/BucketEndpointArnMiddleware.php create mode 100644 3rdparty/aws/aws-sdk-php/src/S3/BucketEndpointMiddleware.php create mode 100644 3rdparty/aws/aws-sdk-php/src/S3/CalculatesChecksumTrait.php create mode 100644 3rdparty/aws/aws-sdk-php/src/S3/Crypto/CryptoParamsTrait.php create mode 100644 3rdparty/aws/aws-sdk-php/src/S3/Crypto/CryptoParamsTraitV2.php create mode 100644 3rdparty/aws/aws-sdk-php/src/S3/Crypto/HeadersMetadataStrategy.php create mode 100644 3rdparty/aws/aws-sdk-php/src/S3/Crypto/InstructionFileMetadataStrategy.php create mode 100644 3rdparty/aws/aws-sdk-php/src/S3/Crypto/S3EncryptionClient.php create mode 100644 3rdparty/aws/aws-sdk-php/src/S3/Crypto/S3EncryptionClientV2.php create mode 100644 3rdparty/aws/aws-sdk-php/src/S3/Crypto/S3EncryptionMultipartUploader.php create mode 100644 3rdparty/aws/aws-sdk-php/src/S3/Crypto/S3EncryptionMultipartUploaderV2.php create mode 100644 3rdparty/aws/aws-sdk-php/src/S3/Crypto/UserAgentTrait.php create mode 100644 3rdparty/aws/aws-sdk-php/src/S3/EndpointRegionHelperTrait.php create mode 100644 3rdparty/aws/aws-sdk-php/src/S3/Exception/DeleteMultipleObjectsException.php create mode 100644 3rdparty/aws/aws-sdk-php/src/S3/Exception/PermanentRedirectException.php create mode 100644 3rdparty/aws/aws-sdk-php/src/S3/Exception/S3Exception.php create mode 100644 3rdparty/aws/aws-sdk-php/src/S3/Exception/S3MultipartUploadException.php create mode 100644 3rdparty/aws/aws-sdk-php/src/S3/ExpiresParsingMiddleware.php create mode 100644 3rdparty/aws/aws-sdk-php/src/S3/GetBucketLocationParser.php create mode 100644 3rdparty/aws/aws-sdk-php/src/S3/MultipartCopy.php create mode 100644 3rdparty/aws/aws-sdk-php/src/S3/MultipartUploader.php create mode 100644 3rdparty/aws/aws-sdk-php/src/S3/MultipartUploadingTrait.php create mode 100644 3rdparty/aws/aws-sdk-php/src/S3/ObjectCopier.php create mode 100644 3rdparty/aws/aws-sdk-php/src/S3/ObjectUploader.php create mode 100644 3rdparty/aws/aws-sdk-php/src/S3/Parser/GetBucketLocationResultMutator.php create mode 100644 3rdparty/aws/aws-sdk-php/src/S3/Parser/S3Parser.php create mode 100644 3rdparty/aws/aws-sdk-php/src/S3/Parser/S3ResultMutator.php create mode 100644 3rdparty/aws/aws-sdk-php/src/S3/Parser/ValidateResponseChecksumResultMutator.php create mode 100644 3rdparty/aws/aws-sdk-php/src/S3/PermanentRedirectMiddleware.php create mode 100644 3rdparty/aws/aws-sdk-php/src/S3/PostObject.php create mode 100644 3rdparty/aws/aws-sdk-php/src/S3/PostObjectV4.php create mode 100644 3rdparty/aws/aws-sdk-php/src/S3/PutObjectUrlMiddleware.php create mode 100644 3rdparty/aws/aws-sdk-php/src/S3/RegionalEndpoint/Configuration.php create mode 100644 3rdparty/aws/aws-sdk-php/src/S3/RegionalEndpoint/ConfigurationInterface.php create mode 100644 3rdparty/aws/aws-sdk-php/src/S3/RegionalEndpoint/ConfigurationProvider.php create mode 100644 3rdparty/aws/aws-sdk-php/src/S3/RegionalEndpoint/Exception/ConfigurationException.php create mode 100644 3rdparty/aws/aws-sdk-php/src/S3/RetryableMalformedResponseParser.php create mode 100644 3rdparty/aws/aws-sdk-php/src/S3/S3Client.php create mode 100644 3rdparty/aws/aws-sdk-php/src/S3/S3ClientInterface.php create mode 100644 3rdparty/aws/aws-sdk-php/src/S3/S3ClientTrait.php create mode 100644 3rdparty/aws/aws-sdk-php/src/S3/S3EndpointMiddleware.php create mode 100644 3rdparty/aws/aws-sdk-php/src/S3/S3MultiRegionClient.php create mode 100644 3rdparty/aws/aws-sdk-php/src/S3/S3UriParser.php create mode 100644 3rdparty/aws/aws-sdk-php/src/S3/SSECMiddleware.php create mode 100644 3rdparty/aws/aws-sdk-php/src/S3/StreamWrapper.php create mode 100644 3rdparty/aws/aws-sdk-php/src/S3/Transfer.php create mode 100644 3rdparty/aws/aws-sdk-php/src/S3/UseArnRegion/Configuration.php create mode 100644 3rdparty/aws/aws-sdk-php/src/S3/UseArnRegion/ConfigurationInterface.php create mode 100644 3rdparty/aws/aws-sdk-php/src/S3/UseArnRegion/ConfigurationProvider.php create mode 100644 3rdparty/aws/aws-sdk-php/src/S3/UseArnRegion/Exception/ConfigurationException.php create mode 100644 3rdparty/aws/aws-sdk-php/src/S3/ValidateResponseChecksumParser.php create mode 100644 3rdparty/aws/aws-sdk-php/src/SSO/Exception/SSOException.php create mode 100644 3rdparty/aws/aws-sdk-php/src/SSO/SSOClient.php create mode 100644 3rdparty/aws/aws-sdk-php/src/SSOOIDC/Exception/SSOOIDCException.php create mode 100644 3rdparty/aws/aws-sdk-php/src/SSOOIDC/SSOOIDCClient.php create mode 100644 3rdparty/aws/aws-sdk-php/src/Script/Composer/Composer.php create mode 100644 3rdparty/aws/aws-sdk-php/src/Sdk.php create mode 100644 3rdparty/aws/aws-sdk-php/src/Signature/AnonymousSignature.php create mode 100644 3rdparty/aws/aws-sdk-php/src/Signature/S3ExpressSignature.php create mode 100644 3rdparty/aws/aws-sdk-php/src/Signature/S3SignatureV4.php create mode 100644 3rdparty/aws/aws-sdk-php/src/Signature/SignatureInterface.php create mode 100644 3rdparty/aws/aws-sdk-php/src/Signature/SignatureProvider.php create mode 100644 3rdparty/aws/aws-sdk-php/src/Signature/SignatureTrait.php create mode 100644 3rdparty/aws/aws-sdk-php/src/Signature/SignatureV4.php create mode 100644 3rdparty/aws/aws-sdk-php/src/StreamRequestPayloadMiddleware.php create mode 100644 3rdparty/aws/aws-sdk-php/src/Sts/Exception/StsException.php create mode 100644 3rdparty/aws/aws-sdk-php/src/Sts/RegionalEndpoints/Configuration.php create mode 100644 3rdparty/aws/aws-sdk-php/src/Sts/RegionalEndpoints/ConfigurationInterface.php create mode 100644 3rdparty/aws/aws-sdk-php/src/Sts/RegionalEndpoints/ConfigurationProvider.php create mode 100644 3rdparty/aws/aws-sdk-php/src/Sts/RegionalEndpoints/Exception/ConfigurationException.php create mode 100644 3rdparty/aws/aws-sdk-php/src/Sts/StsClient.php create mode 100644 3rdparty/aws/aws-sdk-php/src/Token/BearerTokenAuthorization.php create mode 100644 3rdparty/aws/aws-sdk-php/src/Token/ParsesIniTrait.php create mode 100644 3rdparty/aws/aws-sdk-php/src/Token/RefreshableTokenProviderInterface.php create mode 100644 3rdparty/aws/aws-sdk-php/src/Token/SsoToken.php create mode 100644 3rdparty/aws/aws-sdk-php/src/Token/SsoTokenProvider.php create mode 100644 3rdparty/aws/aws-sdk-php/src/Token/Token.php create mode 100644 3rdparty/aws/aws-sdk-php/src/Token/TokenAuthorization.php create mode 100644 3rdparty/aws/aws-sdk-php/src/Token/TokenInterface.php create mode 100644 3rdparty/aws/aws-sdk-php/src/Token/TokenProvider.php create mode 100644 3rdparty/aws/aws-sdk-php/src/TraceMiddleware.php create mode 100644 3rdparty/aws/aws-sdk-php/src/UserAgentMiddleware.php create mode 100644 3rdparty/aws/aws-sdk-php/src/Waiter.php create mode 100644 3rdparty/aws/aws-sdk-php/src/WrappedHttpHandler.php create mode 100644 3rdparty/aws/aws-sdk-php/src/functions.php create mode 100644 3rdparty/bantu/ini-get-wrapper/LICENSE create mode 100644 3rdparty/bantu/ini-get-wrapper/src/IniGetWrapper.php create mode 100644 3rdparty/brick/math/LICENSE create mode 100644 3rdparty/brick/math/src/BigDecimal.php create mode 100644 3rdparty/brick/math/src/BigInteger.php create mode 100644 3rdparty/brick/math/src/BigNumber.php create mode 100644 3rdparty/brick/math/src/BigRational.php create mode 100644 3rdparty/brick/math/src/Exception/DivisionByZeroException.php create mode 100644 3rdparty/brick/math/src/Exception/IntegerOverflowException.php create mode 100644 3rdparty/brick/math/src/Exception/MathException.php create mode 100644 3rdparty/brick/math/src/Exception/NegativeNumberException.php create mode 100644 3rdparty/brick/math/src/Exception/NumberFormatException.php create mode 100644 3rdparty/brick/math/src/Exception/RoundingNecessaryException.php create mode 100644 3rdparty/brick/math/src/Internal/Calculator.php create mode 100644 3rdparty/brick/math/src/Internal/Calculator/BcMathCalculator.php create mode 100644 3rdparty/brick/math/src/Internal/Calculator/GmpCalculator.php create mode 100644 3rdparty/brick/math/src/Internal/Calculator/NativeCalculator.php create mode 100644 3rdparty/brick/math/src/RoundingMode.php create mode 100644 3rdparty/composer.json create mode 100644 3rdparty/composer.lock create mode 100644 3rdparty/composer.patches.json create mode 100644 3rdparty/composer/ClassLoader.php create mode 100644 3rdparty/composer/InstalledVersions.php create mode 100644 3rdparty/composer/LICENSE create mode 100644 3rdparty/composer/autoload_classmap.php create mode 100644 3rdparty/composer/autoload_files.php create mode 100644 3rdparty/composer/autoload_namespaces.php create mode 100644 3rdparty/composer/autoload_psr4.php create mode 100644 3rdparty/composer/autoload_real.php create mode 100644 3rdparty/composer/autoload_static.php create mode 100644 3rdparty/composer/include_paths.php create mode 100644 3rdparty/composer/installed.json create mode 100644 3rdparty/composer/installed.php create mode 100644 3rdparty/composer/platform_check.php create mode 100644 3rdparty/cweagans/composer-patches/LICENSE.md create mode 100644 3rdparty/cweagans/composer-patches/src/PatchEvent.php create mode 100644 3rdparty/cweagans/composer-patches/src/PatchEvents.php create mode 100644 3rdparty/cweagans/composer-patches/src/Patches.php create mode 100644 3rdparty/deepdiver/zipstreamer/COPYING create mode 100644 3rdparty/deepdiver/zipstreamer/MANUAL.md create mode 100644 3rdparty/deepdiver/zipstreamer/src/COMPR.php create mode 100644 3rdparty/deepdiver/zipstreamer/src/Count64.php create mode 100644 3rdparty/deepdiver/zipstreamer/src/Lib/Count64Base.php create mode 100644 3rdparty/deepdiver/zipstreamer/src/Lib/Count64_32.php create mode 100644 3rdparty/deepdiver/zipstreamer/src/Lib/Count64_64.php create mode 100644 3rdparty/deepdiver/zipstreamer/src/ZipStreamer.php create mode 100644 3rdparty/deepdiver1975/tarstreamer/LICENSE create mode 100644 3rdparty/deepdiver1975/tarstreamer/src/TarHeader.php create mode 100644 3rdparty/deepdiver1975/tarstreamer/src/TarStreamer.php create mode 100644 3rdparty/doctrine/dbal/LICENSE create mode 100644 3rdparty/doctrine/dbal/src/ArrayParameterType.php create mode 100644 3rdparty/doctrine/dbal/src/ArrayParameters/Exception.php create mode 100644 3rdparty/doctrine/dbal/src/ArrayParameters/Exception/MissingNamedParameter.php create mode 100644 3rdparty/doctrine/dbal/src/ArrayParameters/Exception/MissingPositionalParameter.php create mode 100644 3rdparty/doctrine/dbal/src/Cache/ArrayResult.php create mode 100644 3rdparty/doctrine/dbal/src/Cache/CacheException.php create mode 100644 3rdparty/doctrine/dbal/src/Cache/QueryCacheProfile.php create mode 100644 3rdparty/doctrine/dbal/src/ColumnCase.php create mode 100644 3rdparty/doctrine/dbal/src/Configuration.php create mode 100644 3rdparty/doctrine/dbal/src/Connection.php create mode 100644 3rdparty/doctrine/dbal/src/ConnectionException.php create mode 100644 3rdparty/doctrine/dbal/src/Connections/PrimaryReadReplicaConnection.php create mode 100644 3rdparty/doctrine/dbal/src/Driver.php create mode 100644 3rdparty/doctrine/dbal/src/Driver/API/ExceptionConverter.php create mode 100644 3rdparty/doctrine/dbal/src/Driver/API/IBMDB2/ExceptionConverter.php create mode 100644 3rdparty/doctrine/dbal/src/Driver/API/MySQL/ExceptionConverter.php create mode 100644 3rdparty/doctrine/dbal/src/Driver/API/OCI/ExceptionConverter.php create mode 100644 3rdparty/doctrine/dbal/src/Driver/API/PostgreSQL/ExceptionConverter.php create mode 100644 3rdparty/doctrine/dbal/src/Driver/API/SQLSrv/ExceptionConverter.php create mode 100644 3rdparty/doctrine/dbal/src/Driver/API/SQLite/ExceptionConverter.php create mode 100644 3rdparty/doctrine/dbal/src/Driver/API/SQLite/UserDefinedFunctions.php create mode 100644 3rdparty/doctrine/dbal/src/Driver/AbstractDB2Driver.php create mode 100644 3rdparty/doctrine/dbal/src/Driver/AbstractException.php create mode 100644 3rdparty/doctrine/dbal/src/Driver/AbstractMySQLDriver.php create mode 100644 3rdparty/doctrine/dbal/src/Driver/AbstractOracleDriver.php create mode 100644 3rdparty/doctrine/dbal/src/Driver/AbstractOracleDriver/EasyConnectString.php create mode 100644 3rdparty/doctrine/dbal/src/Driver/AbstractPostgreSQLDriver.php create mode 100644 3rdparty/doctrine/dbal/src/Driver/AbstractSQLServerDriver.php create mode 100644 3rdparty/doctrine/dbal/src/Driver/AbstractSQLServerDriver/Exception/PortWithoutHost.php create mode 100644 3rdparty/doctrine/dbal/src/Driver/AbstractSQLiteDriver.php create mode 100644 3rdparty/doctrine/dbal/src/Driver/AbstractSQLiteDriver/Middleware/EnableForeignKeys.php create mode 100644 3rdparty/doctrine/dbal/src/Driver/Connection.php create mode 100644 3rdparty/doctrine/dbal/src/Driver/Exception.php create mode 100644 3rdparty/doctrine/dbal/src/Driver/Exception/UnknownParameterType.php create mode 100644 3rdparty/doctrine/dbal/src/Driver/FetchUtils.php create mode 100644 3rdparty/doctrine/dbal/src/Driver/IBMDB2/Connection.php create mode 100644 3rdparty/doctrine/dbal/src/Driver/IBMDB2/DataSourceName.php create mode 100644 3rdparty/doctrine/dbal/src/Driver/IBMDB2/Driver.php create mode 100644 3rdparty/doctrine/dbal/src/Driver/IBMDB2/Exception/CannotCopyStreamToStream.php create mode 100644 3rdparty/doctrine/dbal/src/Driver/IBMDB2/Exception/CannotCreateTemporaryFile.php create mode 100644 3rdparty/doctrine/dbal/src/Driver/IBMDB2/Exception/ConnectionError.php create mode 100644 3rdparty/doctrine/dbal/src/Driver/IBMDB2/Exception/ConnectionFailed.php create mode 100644 3rdparty/doctrine/dbal/src/Driver/IBMDB2/Exception/Factory.php create mode 100644 3rdparty/doctrine/dbal/src/Driver/IBMDB2/Exception/PrepareFailed.php create mode 100644 3rdparty/doctrine/dbal/src/Driver/IBMDB2/Exception/StatementError.php create mode 100644 3rdparty/doctrine/dbal/src/Driver/IBMDB2/Result.php create mode 100644 3rdparty/doctrine/dbal/src/Driver/IBMDB2/Statement.php create mode 100644 3rdparty/doctrine/dbal/src/Driver/Middleware.php create mode 100644 3rdparty/doctrine/dbal/src/Driver/Middleware/AbstractConnectionMiddleware.php create mode 100644 3rdparty/doctrine/dbal/src/Driver/Middleware/AbstractDriverMiddleware.php create mode 100644 3rdparty/doctrine/dbal/src/Driver/Middleware/AbstractResultMiddleware.php create mode 100644 3rdparty/doctrine/dbal/src/Driver/Middleware/AbstractStatementMiddleware.php create mode 100644 3rdparty/doctrine/dbal/src/Driver/Mysqli/Connection.php create mode 100644 3rdparty/doctrine/dbal/src/Driver/Mysqli/Driver.php create mode 100644 3rdparty/doctrine/dbal/src/Driver/Mysqli/Exception/ConnectionError.php create mode 100644 3rdparty/doctrine/dbal/src/Driver/Mysqli/Exception/ConnectionFailed.php create mode 100644 3rdparty/doctrine/dbal/src/Driver/Mysqli/Exception/FailedReadingStreamOffset.php create mode 100644 3rdparty/doctrine/dbal/src/Driver/Mysqli/Exception/HostRequired.php create mode 100644 3rdparty/doctrine/dbal/src/Driver/Mysqli/Exception/InvalidCharset.php create mode 100644 3rdparty/doctrine/dbal/src/Driver/Mysqli/Exception/InvalidOption.php create mode 100644 3rdparty/doctrine/dbal/src/Driver/Mysqli/Exception/NonStreamResourceUsedAsLargeObject.php create mode 100644 3rdparty/doctrine/dbal/src/Driver/Mysqli/Exception/StatementError.php create mode 100644 3rdparty/doctrine/dbal/src/Driver/Mysqli/Initializer.php create mode 100644 3rdparty/doctrine/dbal/src/Driver/Mysqli/Initializer/Charset.php create mode 100644 3rdparty/doctrine/dbal/src/Driver/Mysqli/Initializer/Options.php create mode 100644 3rdparty/doctrine/dbal/src/Driver/Mysqli/Initializer/Secure.php create mode 100644 3rdparty/doctrine/dbal/src/Driver/Mysqli/Result.php create mode 100644 3rdparty/doctrine/dbal/src/Driver/Mysqli/Statement.php create mode 100644 3rdparty/doctrine/dbal/src/Driver/OCI8/Connection.php create mode 100644 3rdparty/doctrine/dbal/src/Driver/OCI8/ConvertPositionalToNamedPlaceholders.php create mode 100644 3rdparty/doctrine/dbal/src/Driver/OCI8/Driver.php create mode 100644 3rdparty/doctrine/dbal/src/Driver/OCI8/Exception/ConnectionFailed.php create mode 100644 3rdparty/doctrine/dbal/src/Driver/OCI8/Exception/Error.php create mode 100644 3rdparty/doctrine/dbal/src/Driver/OCI8/Exception/InvalidConfiguration.php create mode 100644 3rdparty/doctrine/dbal/src/Driver/OCI8/Exception/NonTerminatedStringLiteral.php create mode 100644 3rdparty/doctrine/dbal/src/Driver/OCI8/Exception/SequenceDoesNotExist.php create mode 100644 3rdparty/doctrine/dbal/src/Driver/OCI8/Exception/UnknownParameterIndex.php create mode 100644 3rdparty/doctrine/dbal/src/Driver/OCI8/ExecutionMode.php create mode 100644 3rdparty/doctrine/dbal/src/Driver/OCI8/Middleware/InitializeSession.php create mode 100644 3rdparty/doctrine/dbal/src/Driver/OCI8/Result.php create mode 100644 3rdparty/doctrine/dbal/src/Driver/OCI8/Statement.php create mode 100644 3rdparty/doctrine/dbal/src/Driver/PDO/Connection.php create mode 100644 3rdparty/doctrine/dbal/src/Driver/PDO/Exception.php create mode 100644 3rdparty/doctrine/dbal/src/Driver/PDO/MySQL/Driver.php create mode 100644 3rdparty/doctrine/dbal/src/Driver/PDO/OCI/Driver.php create mode 100644 3rdparty/doctrine/dbal/src/Driver/PDO/PDOConnect.php create mode 100644 3rdparty/doctrine/dbal/src/Driver/PDO/PDOException.php create mode 100644 3rdparty/doctrine/dbal/src/Driver/PDO/ParameterTypeMap.php create mode 100644 3rdparty/doctrine/dbal/src/Driver/PDO/PgSQL/Driver.php create mode 100644 3rdparty/doctrine/dbal/src/Driver/PDO/Result.php create mode 100644 3rdparty/doctrine/dbal/src/Driver/PDO/SQLSrv/Connection.php create mode 100644 3rdparty/doctrine/dbal/src/Driver/PDO/SQLSrv/Driver.php create mode 100644 3rdparty/doctrine/dbal/src/Driver/PDO/SQLSrv/Statement.php create mode 100644 3rdparty/doctrine/dbal/src/Driver/PDO/SQLite/Driver.php create mode 100644 3rdparty/doctrine/dbal/src/Driver/PDO/Statement.php create mode 100644 3rdparty/doctrine/dbal/src/Driver/PgSQL/Connection.php create mode 100644 3rdparty/doctrine/dbal/src/Driver/PgSQL/ConvertParameters.php create mode 100644 3rdparty/doctrine/dbal/src/Driver/PgSQL/Driver.php create mode 100644 3rdparty/doctrine/dbal/src/Driver/PgSQL/Exception.php create mode 100644 3rdparty/doctrine/dbal/src/Driver/PgSQL/Exception/UnexpectedValue.php create mode 100644 3rdparty/doctrine/dbal/src/Driver/PgSQL/Exception/UnknownParameter.php create mode 100644 3rdparty/doctrine/dbal/src/Driver/PgSQL/Result.php create mode 100644 3rdparty/doctrine/dbal/src/Driver/PgSQL/Statement.php create mode 100644 3rdparty/doctrine/dbal/src/Driver/Result.php create mode 100644 3rdparty/doctrine/dbal/src/Driver/SQLSrv/Connection.php create mode 100644 3rdparty/doctrine/dbal/src/Driver/SQLSrv/Driver.php create mode 100644 3rdparty/doctrine/dbal/src/Driver/SQLSrv/Exception/Error.php create mode 100644 3rdparty/doctrine/dbal/src/Driver/SQLSrv/Result.php create mode 100644 3rdparty/doctrine/dbal/src/Driver/SQLSrv/Statement.php create mode 100644 3rdparty/doctrine/dbal/src/Driver/SQLite3/Connection.php create mode 100644 3rdparty/doctrine/dbal/src/Driver/SQLite3/Driver.php create mode 100644 3rdparty/doctrine/dbal/src/Driver/SQLite3/Exception.php create mode 100644 3rdparty/doctrine/dbal/src/Driver/SQLite3/Result.php create mode 100644 3rdparty/doctrine/dbal/src/Driver/SQLite3/Statement.php create mode 100644 3rdparty/doctrine/dbal/src/Driver/ServerInfoAwareConnection.php create mode 100644 3rdparty/doctrine/dbal/src/Driver/Statement.php create mode 100644 3rdparty/doctrine/dbal/src/DriverManager.php create mode 100644 3rdparty/doctrine/dbal/src/Event/ConnectionEventArgs.php create mode 100644 3rdparty/doctrine/dbal/src/Event/Listeners/OracleSessionInit.php create mode 100644 3rdparty/doctrine/dbal/src/Event/Listeners/SQLSessionInit.php create mode 100644 3rdparty/doctrine/dbal/src/Event/Listeners/SQLiteSessionInit.php create mode 100644 3rdparty/doctrine/dbal/src/Event/SchemaAlterTableAddColumnEventArgs.php create mode 100644 3rdparty/doctrine/dbal/src/Event/SchemaAlterTableChangeColumnEventArgs.php create mode 100644 3rdparty/doctrine/dbal/src/Event/SchemaAlterTableEventArgs.php create mode 100644 3rdparty/doctrine/dbal/src/Event/SchemaAlterTableRemoveColumnEventArgs.php create mode 100644 3rdparty/doctrine/dbal/src/Event/SchemaAlterTableRenameColumnEventArgs.php create mode 100644 3rdparty/doctrine/dbal/src/Event/SchemaColumnDefinitionEventArgs.php create mode 100644 3rdparty/doctrine/dbal/src/Event/SchemaCreateTableColumnEventArgs.php create mode 100644 3rdparty/doctrine/dbal/src/Event/SchemaCreateTableEventArgs.php create mode 100644 3rdparty/doctrine/dbal/src/Event/SchemaDropTableEventArgs.php create mode 100644 3rdparty/doctrine/dbal/src/Event/SchemaEventArgs.php create mode 100644 3rdparty/doctrine/dbal/src/Event/SchemaIndexDefinitionEventArgs.php create mode 100644 3rdparty/doctrine/dbal/src/Event/TransactionBeginEventArgs.php create mode 100644 3rdparty/doctrine/dbal/src/Event/TransactionCommitEventArgs.php create mode 100644 3rdparty/doctrine/dbal/src/Event/TransactionEventArgs.php create mode 100644 3rdparty/doctrine/dbal/src/Event/TransactionRollBackEventArgs.php create mode 100644 3rdparty/doctrine/dbal/src/Events.php create mode 100644 3rdparty/doctrine/dbal/src/Exception.php create mode 100644 3rdparty/doctrine/dbal/src/Exception/ConnectionException.php create mode 100644 3rdparty/doctrine/dbal/src/Exception/ConnectionLost.php create mode 100644 3rdparty/doctrine/dbal/src/Exception/ConstraintViolationException.php create mode 100644 3rdparty/doctrine/dbal/src/Exception/DatabaseDoesNotExist.php create mode 100644 3rdparty/doctrine/dbal/src/Exception/DatabaseObjectExistsException.php create mode 100644 3rdparty/doctrine/dbal/src/Exception/DatabaseObjectNotFoundException.php create mode 100644 3rdparty/doctrine/dbal/src/Exception/DatabaseRequired.php create mode 100644 3rdparty/doctrine/dbal/src/Exception/DeadlockException.php create mode 100644 3rdparty/doctrine/dbal/src/Exception/DriverException.php create mode 100644 3rdparty/doctrine/dbal/src/Exception/ForeignKeyConstraintViolationException.php create mode 100644 3rdparty/doctrine/dbal/src/Exception/InvalidArgumentException.php create mode 100644 3rdparty/doctrine/dbal/src/Exception/InvalidFieldNameException.php create mode 100644 3rdparty/doctrine/dbal/src/Exception/InvalidLockMode.php create mode 100644 3rdparty/doctrine/dbal/src/Exception/LockWaitTimeoutException.php create mode 100644 3rdparty/doctrine/dbal/src/Exception/MalformedDsnException.php create mode 100644 3rdparty/doctrine/dbal/src/Exception/NoKeyValue.php create mode 100644 3rdparty/doctrine/dbal/src/Exception/NonUniqueFieldNameException.php create mode 100644 3rdparty/doctrine/dbal/src/Exception/NotNullConstraintViolationException.php create mode 100644 3rdparty/doctrine/dbal/src/Exception/ReadOnlyException.php create mode 100644 3rdparty/doctrine/dbal/src/Exception/RetryableException.php create mode 100644 3rdparty/doctrine/dbal/src/Exception/SchemaDoesNotExist.php create mode 100644 3rdparty/doctrine/dbal/src/Exception/ServerException.php create mode 100644 3rdparty/doctrine/dbal/src/Exception/SyntaxErrorException.php create mode 100644 3rdparty/doctrine/dbal/src/Exception/TableExistsException.php create mode 100644 3rdparty/doctrine/dbal/src/Exception/TableNotFoundException.php create mode 100644 3rdparty/doctrine/dbal/src/Exception/TransactionRolledBack.php create mode 100644 3rdparty/doctrine/dbal/src/Exception/UniqueConstraintViolationException.php create mode 100644 3rdparty/doctrine/dbal/src/ExpandArrayParameters.php create mode 100644 3rdparty/doctrine/dbal/src/FetchMode.php create mode 100644 3rdparty/doctrine/dbal/src/Id/TableGenerator.php create mode 100644 3rdparty/doctrine/dbal/src/Id/TableGeneratorSchemaVisitor.php create mode 100644 3rdparty/doctrine/dbal/src/LockMode.php create mode 100644 3rdparty/doctrine/dbal/src/Logging/Connection.php create mode 100644 3rdparty/doctrine/dbal/src/Logging/DebugStack.php create mode 100644 3rdparty/doctrine/dbal/src/Logging/Driver.php create mode 100644 3rdparty/doctrine/dbal/src/Logging/LoggerChain.php create mode 100644 3rdparty/doctrine/dbal/src/Logging/Middleware.php create mode 100644 3rdparty/doctrine/dbal/src/Logging/SQLLogger.php create mode 100644 3rdparty/doctrine/dbal/src/Logging/Statement.php create mode 100644 3rdparty/doctrine/dbal/src/ParameterType.php create mode 100644 3rdparty/doctrine/dbal/src/Platforms/AbstractMySQLPlatform.php create mode 100644 3rdparty/doctrine/dbal/src/Platforms/AbstractPlatform.php create mode 100644 3rdparty/doctrine/dbal/src/Platforms/DB2111Platform.php create mode 100644 3rdparty/doctrine/dbal/src/Platforms/DB2Platform.php create mode 100644 3rdparty/doctrine/dbal/src/Platforms/DateIntervalUnit.php create mode 100644 3rdparty/doctrine/dbal/src/Platforms/Keywords/DB2Keywords.php create mode 100644 3rdparty/doctrine/dbal/src/Platforms/Keywords/KeywordList.php create mode 100644 3rdparty/doctrine/dbal/src/Platforms/Keywords/MariaDBKeywords.php create mode 100644 3rdparty/doctrine/dbal/src/Platforms/Keywords/MariaDb102Keywords.php create mode 100644 3rdparty/doctrine/dbal/src/Platforms/Keywords/MariaDb117Keywords.php create mode 100644 3rdparty/doctrine/dbal/src/Platforms/Keywords/MySQL57Keywords.php create mode 100644 3rdparty/doctrine/dbal/src/Platforms/Keywords/MySQL80Keywords.php create mode 100644 3rdparty/doctrine/dbal/src/Platforms/Keywords/MySQL84Keywords.php create mode 100644 3rdparty/doctrine/dbal/src/Platforms/Keywords/MySQLKeywords.php create mode 100644 3rdparty/doctrine/dbal/src/Platforms/Keywords/OracleKeywords.php create mode 100644 3rdparty/doctrine/dbal/src/Platforms/Keywords/PostgreSQL100Keywords.php create mode 100644 3rdparty/doctrine/dbal/src/Platforms/Keywords/PostgreSQL94Keywords.php create mode 100644 3rdparty/doctrine/dbal/src/Platforms/Keywords/PostgreSQLKeywords.php create mode 100644 3rdparty/doctrine/dbal/src/Platforms/Keywords/ReservedKeywordsValidator.php create mode 100644 3rdparty/doctrine/dbal/src/Platforms/Keywords/SQLServer2012Keywords.php create mode 100644 3rdparty/doctrine/dbal/src/Platforms/Keywords/SQLServerKeywords.php create mode 100644 3rdparty/doctrine/dbal/src/Platforms/Keywords/SQLiteKeywords.php create mode 100644 3rdparty/doctrine/dbal/src/Platforms/MariaDBPlatform.php create mode 100644 3rdparty/doctrine/dbal/src/Platforms/MariaDb1010Platform.php create mode 100644 3rdparty/doctrine/dbal/src/Platforms/MariaDb1027Platform.php create mode 100644 3rdparty/doctrine/dbal/src/Platforms/MariaDb1043Platform.php create mode 100644 3rdparty/doctrine/dbal/src/Platforms/MariaDb1052Platform.php create mode 100644 3rdparty/doctrine/dbal/src/Platforms/MariaDb1060Platform.php create mode 100644 3rdparty/doctrine/dbal/src/Platforms/MariaDb110700Platform.php create mode 100644 3rdparty/doctrine/dbal/src/Platforms/MySQL/CollationMetadataProvider.php create mode 100644 3rdparty/doctrine/dbal/src/Platforms/MySQL/CollationMetadataProvider/CachingCollationMetadataProvider.php create mode 100644 3rdparty/doctrine/dbal/src/Platforms/MySQL/CollationMetadataProvider/ConnectionCollationMetadataProvider.php create mode 100644 3rdparty/doctrine/dbal/src/Platforms/MySQL/Comparator.php create mode 100644 3rdparty/doctrine/dbal/src/Platforms/MySQL57Platform.php create mode 100644 3rdparty/doctrine/dbal/src/Platforms/MySQL80Platform.php create mode 100644 3rdparty/doctrine/dbal/src/Platforms/MySQL84Platform.php create mode 100644 3rdparty/doctrine/dbal/src/Platforms/MySQLPlatform.php create mode 100644 3rdparty/doctrine/dbal/src/Platforms/OraclePlatform.php create mode 100644 3rdparty/doctrine/dbal/src/Platforms/PostgreSQL100Platform.php create mode 100644 3rdparty/doctrine/dbal/src/Platforms/PostgreSQL120Platform.php create mode 100644 3rdparty/doctrine/dbal/src/Platforms/PostgreSQL94Platform.php create mode 100644 3rdparty/doctrine/dbal/src/Platforms/PostgreSQLPlatform.php create mode 100644 3rdparty/doctrine/dbal/src/Platforms/SQLServer/Comparator.php create mode 100644 3rdparty/doctrine/dbal/src/Platforms/SQLServer/SQL/Builder/SQLServerSelectSQLBuilder.php create mode 100644 3rdparty/doctrine/dbal/src/Platforms/SQLServer2012Platform.php create mode 100644 3rdparty/doctrine/dbal/src/Platforms/SQLServerPlatform.php create mode 100644 3rdparty/doctrine/dbal/src/Platforms/SQLite/Comparator.php create mode 100644 3rdparty/doctrine/dbal/src/Platforms/SqlitePlatform.php create mode 100644 3rdparty/doctrine/dbal/src/Platforms/TrimMode.php create mode 100644 3rdparty/doctrine/dbal/src/Portability/Connection.php create mode 100644 3rdparty/doctrine/dbal/src/Portability/Converter.php create mode 100644 3rdparty/doctrine/dbal/src/Portability/Driver.php create mode 100644 3rdparty/doctrine/dbal/src/Portability/Middleware.php create mode 100644 3rdparty/doctrine/dbal/src/Portability/OptimizeFlags.php create mode 100644 3rdparty/doctrine/dbal/src/Portability/Result.php create mode 100644 3rdparty/doctrine/dbal/src/Portability/Statement.php create mode 100644 3rdparty/doctrine/dbal/src/Query.php create mode 100644 3rdparty/doctrine/dbal/src/Query/Expression/CompositeExpression.php create mode 100644 3rdparty/doctrine/dbal/src/Query/Expression/ExpressionBuilder.php create mode 100644 3rdparty/doctrine/dbal/src/Query/ForUpdate.php create mode 100644 3rdparty/doctrine/dbal/src/Query/ForUpdate/ConflictResolutionMode.php create mode 100644 3rdparty/doctrine/dbal/src/Query/Limit.php create mode 100644 3rdparty/doctrine/dbal/src/Query/QueryBuilder.php create mode 100644 3rdparty/doctrine/dbal/src/Query/QueryException.php create mode 100644 3rdparty/doctrine/dbal/src/Query/SelectQuery.php create mode 100644 3rdparty/doctrine/dbal/src/Result.php create mode 100644 3rdparty/doctrine/dbal/src/SQL/Builder/CreateSchemaObjectsSQLBuilder.php create mode 100644 3rdparty/doctrine/dbal/src/SQL/Builder/DefaultSelectSQLBuilder.php create mode 100644 3rdparty/doctrine/dbal/src/SQL/Builder/DropSchemaObjectsSQLBuilder.php create mode 100644 3rdparty/doctrine/dbal/src/SQL/Builder/SelectSQLBuilder.php create mode 100644 3rdparty/doctrine/dbal/src/SQL/Parser.php create mode 100644 3rdparty/doctrine/dbal/src/SQL/Parser/Exception.php create mode 100644 3rdparty/doctrine/dbal/src/SQL/Parser/Exception/RegularExpressionError.php create mode 100644 3rdparty/doctrine/dbal/src/SQL/Parser/Visitor.php create mode 100644 3rdparty/doctrine/dbal/src/Schema/AbstractAsset.php create mode 100644 3rdparty/doctrine/dbal/src/Schema/AbstractSchemaManager.php create mode 100644 3rdparty/doctrine/dbal/src/Schema/Column.php create mode 100644 3rdparty/doctrine/dbal/src/Schema/ColumnDiff.php create mode 100644 3rdparty/doctrine/dbal/src/Schema/Comparator.php create mode 100644 3rdparty/doctrine/dbal/src/Schema/Constraint.php create mode 100644 3rdparty/doctrine/dbal/src/Schema/DB2SchemaManager.php create mode 100644 3rdparty/doctrine/dbal/src/Schema/DefaultSchemaManagerFactory.php create mode 100644 3rdparty/doctrine/dbal/src/Schema/Exception/ColumnAlreadyExists.php create mode 100644 3rdparty/doctrine/dbal/src/Schema/Exception/ColumnDoesNotExist.php create mode 100644 3rdparty/doctrine/dbal/src/Schema/Exception/ForeignKeyDoesNotExist.php create mode 100644 3rdparty/doctrine/dbal/src/Schema/Exception/IndexAlreadyExists.php create mode 100644 3rdparty/doctrine/dbal/src/Schema/Exception/IndexDoesNotExist.php create mode 100644 3rdparty/doctrine/dbal/src/Schema/Exception/IndexNameInvalid.php create mode 100644 3rdparty/doctrine/dbal/src/Schema/Exception/InvalidTableName.php create mode 100644 3rdparty/doctrine/dbal/src/Schema/Exception/NamedForeignKeyRequired.php create mode 100644 3rdparty/doctrine/dbal/src/Schema/Exception/NamespaceAlreadyExists.php create mode 100644 3rdparty/doctrine/dbal/src/Schema/Exception/SequenceAlreadyExists.php create mode 100644 3rdparty/doctrine/dbal/src/Schema/Exception/SequenceDoesNotExist.php create mode 100644 3rdparty/doctrine/dbal/src/Schema/Exception/TableAlreadyExists.php create mode 100644 3rdparty/doctrine/dbal/src/Schema/Exception/TableDoesNotExist.php create mode 100644 3rdparty/doctrine/dbal/src/Schema/Exception/UniqueConstraintDoesNotExist.php create mode 100644 3rdparty/doctrine/dbal/src/Schema/Exception/UnknownColumnOption.php create mode 100644 3rdparty/doctrine/dbal/src/Schema/ForeignKeyConstraint.php create mode 100644 3rdparty/doctrine/dbal/src/Schema/Identifier.php create mode 100644 3rdparty/doctrine/dbal/src/Schema/Index.php create mode 100644 3rdparty/doctrine/dbal/src/Schema/LegacySchemaManagerFactory.php create mode 100644 3rdparty/doctrine/dbal/src/Schema/MySQLSchemaManager.php create mode 100644 3rdparty/doctrine/dbal/src/Schema/OracleSchemaManager.php create mode 100644 3rdparty/doctrine/dbal/src/Schema/PostgreSQLSchemaManager.php create mode 100644 3rdparty/doctrine/dbal/src/Schema/SQLServerSchemaManager.php create mode 100644 3rdparty/doctrine/dbal/src/Schema/Schema.php create mode 100644 3rdparty/doctrine/dbal/src/Schema/SchemaConfig.php create mode 100644 3rdparty/doctrine/dbal/src/Schema/SchemaDiff.php create mode 100644 3rdparty/doctrine/dbal/src/Schema/SchemaException.php create mode 100644 3rdparty/doctrine/dbal/src/Schema/SchemaManagerFactory.php create mode 100644 3rdparty/doctrine/dbal/src/Schema/Sequence.php create mode 100644 3rdparty/doctrine/dbal/src/Schema/SqliteSchemaManager.php create mode 100644 3rdparty/doctrine/dbal/src/Schema/Table.php create mode 100644 3rdparty/doctrine/dbal/src/Schema/TableDiff.php create mode 100644 3rdparty/doctrine/dbal/src/Schema/UniqueConstraint.php create mode 100644 3rdparty/doctrine/dbal/src/Schema/View.php create mode 100644 3rdparty/doctrine/dbal/src/Schema/Visitor/AbstractVisitor.php create mode 100644 3rdparty/doctrine/dbal/src/Schema/Visitor/CreateSchemaSqlCollector.php create mode 100644 3rdparty/doctrine/dbal/src/Schema/Visitor/DropSchemaSqlCollector.php create mode 100644 3rdparty/doctrine/dbal/src/Schema/Visitor/Graphviz.php create mode 100644 3rdparty/doctrine/dbal/src/Schema/Visitor/NamespaceVisitor.php create mode 100644 3rdparty/doctrine/dbal/src/Schema/Visitor/RemoveNamespacedAssets.php create mode 100644 3rdparty/doctrine/dbal/src/Schema/Visitor/Visitor.php create mode 100644 3rdparty/doctrine/dbal/src/Statement.php create mode 100644 3rdparty/doctrine/dbal/src/Tools/Console/Command/CommandCompatibility.php create mode 100644 3rdparty/doctrine/dbal/src/Tools/Console/Command/ReservedWordsCommand.php create mode 100644 3rdparty/doctrine/dbal/src/Tools/Console/Command/RunSqlCommand.php create mode 100644 3rdparty/doctrine/dbal/src/Tools/Console/ConnectionNotFound.php create mode 100644 3rdparty/doctrine/dbal/src/Tools/Console/ConnectionProvider.php create mode 100644 3rdparty/doctrine/dbal/src/Tools/Console/ConnectionProvider/SingleConnectionProvider.php create mode 100644 3rdparty/doctrine/dbal/src/Tools/Console/ConsoleRunner.php create mode 100644 3rdparty/doctrine/dbal/src/Tools/DsnParser.php create mode 100644 3rdparty/doctrine/dbal/src/TransactionIsolationLevel.php create mode 100644 3rdparty/doctrine/dbal/src/Types/ArrayType.php create mode 100644 3rdparty/doctrine/dbal/src/Types/AsciiStringType.php create mode 100644 3rdparty/doctrine/dbal/src/Types/BigIntType.php create mode 100644 3rdparty/doctrine/dbal/src/Types/BinaryType.php create mode 100644 3rdparty/doctrine/dbal/src/Types/BlobType.php create mode 100644 3rdparty/doctrine/dbal/src/Types/BooleanType.php create mode 100644 3rdparty/doctrine/dbal/src/Types/ConversionException.php create mode 100644 3rdparty/doctrine/dbal/src/Types/DateImmutableType.php create mode 100644 3rdparty/doctrine/dbal/src/Types/DateIntervalType.php create mode 100644 3rdparty/doctrine/dbal/src/Types/DateTimeImmutableType.php create mode 100644 3rdparty/doctrine/dbal/src/Types/DateTimeType.php create mode 100644 3rdparty/doctrine/dbal/src/Types/DateTimeTzImmutableType.php create mode 100644 3rdparty/doctrine/dbal/src/Types/DateTimeTzType.php create mode 100644 3rdparty/doctrine/dbal/src/Types/DateType.php create mode 100644 3rdparty/doctrine/dbal/src/Types/DecimalType.php create mode 100644 3rdparty/doctrine/dbal/src/Types/FloatType.php create mode 100644 3rdparty/doctrine/dbal/src/Types/GuidType.php create mode 100644 3rdparty/doctrine/dbal/src/Types/IntegerType.php create mode 100644 3rdparty/doctrine/dbal/src/Types/JsonType.php create mode 100644 3rdparty/doctrine/dbal/src/Types/ObjectType.php create mode 100644 3rdparty/doctrine/dbal/src/Types/PhpDateTimeMappingType.php create mode 100644 3rdparty/doctrine/dbal/src/Types/PhpIntegerMappingType.php create mode 100644 3rdparty/doctrine/dbal/src/Types/SimpleArrayType.php create mode 100644 3rdparty/doctrine/dbal/src/Types/SmallIntType.php create mode 100644 3rdparty/doctrine/dbal/src/Types/StringType.php create mode 100644 3rdparty/doctrine/dbal/src/Types/TextType.php create mode 100644 3rdparty/doctrine/dbal/src/Types/TimeImmutableType.php create mode 100644 3rdparty/doctrine/dbal/src/Types/TimeType.php create mode 100644 3rdparty/doctrine/dbal/src/Types/Type.php create mode 100644 3rdparty/doctrine/dbal/src/Types/TypeRegistry.php create mode 100644 3rdparty/doctrine/dbal/src/Types/Types.php create mode 100644 3rdparty/doctrine/dbal/src/Types/VarDateTimeImmutableType.php create mode 100644 3rdparty/doctrine/dbal/src/Types/VarDateTimeType.php create mode 100644 3rdparty/doctrine/dbal/src/VersionAwarePlatformDriver.php create mode 100644 3rdparty/doctrine/deprecations/LICENSE create mode 100644 3rdparty/doctrine/deprecations/src/Deprecation.php create mode 100644 3rdparty/doctrine/deprecations/src/PHPUnit/VerifyDeprecations.php create mode 100644 3rdparty/doctrine/event-manager/LICENSE create mode 100644 3rdparty/doctrine/event-manager/src/EventArgs.php create mode 100644 3rdparty/doctrine/event-manager/src/EventManager.php create mode 100644 3rdparty/doctrine/event-manager/src/EventSubscriber.php create mode 100644 3rdparty/doctrine/lexer/LICENSE create mode 100644 3rdparty/doctrine/lexer/src/AbstractLexer.php create mode 100644 3rdparty/doctrine/lexer/src/Token.php create mode 100644 3rdparty/egulias/email-validator/CONTRIBUTING.md create mode 100644 3rdparty/egulias/email-validator/LICENSE create mode 100644 3rdparty/egulias/email-validator/src/EmailLexer.php create mode 100644 3rdparty/egulias/email-validator/src/EmailParser.php create mode 100644 3rdparty/egulias/email-validator/src/EmailValidator.php create mode 100644 3rdparty/egulias/email-validator/src/MessageIDParser.php create mode 100644 3rdparty/egulias/email-validator/src/Parser.php create mode 100644 3rdparty/egulias/email-validator/src/Parser/Comment.php create mode 100644 3rdparty/egulias/email-validator/src/Parser/CommentStrategy/CommentStrategy.php create mode 100644 3rdparty/egulias/email-validator/src/Parser/CommentStrategy/DomainComment.php create mode 100644 3rdparty/egulias/email-validator/src/Parser/CommentStrategy/LocalComment.php create mode 100644 3rdparty/egulias/email-validator/src/Parser/DomainLiteral.php create mode 100644 3rdparty/egulias/email-validator/src/Parser/DomainPart.php create mode 100644 3rdparty/egulias/email-validator/src/Parser/DoubleQuote.php create mode 100644 3rdparty/egulias/email-validator/src/Parser/FoldingWhiteSpace.php create mode 100644 3rdparty/egulias/email-validator/src/Parser/IDLeftPart.php create mode 100644 3rdparty/egulias/email-validator/src/Parser/IDRightPart.php create mode 100644 3rdparty/egulias/email-validator/src/Parser/LocalPart.php create mode 100644 3rdparty/egulias/email-validator/src/Parser/PartParser.php create mode 100644 3rdparty/egulias/email-validator/src/Result/InvalidEmail.php create mode 100644 3rdparty/egulias/email-validator/src/Result/MultipleErrors.php create mode 100644 3rdparty/egulias/email-validator/src/Result/Reason/AtextAfterCFWS.php create mode 100644 3rdparty/egulias/email-validator/src/Result/Reason/CRLFAtTheEnd.php create mode 100644 3rdparty/egulias/email-validator/src/Result/Reason/CRLFX2.php create mode 100644 3rdparty/egulias/email-validator/src/Result/Reason/CRNoLF.php create mode 100644 3rdparty/egulias/email-validator/src/Result/Reason/CharNotAllowed.php create mode 100644 3rdparty/egulias/email-validator/src/Result/Reason/CommaInDomain.php create mode 100644 3rdparty/egulias/email-validator/src/Result/Reason/CommentsInIDRight.php create mode 100644 3rdparty/egulias/email-validator/src/Result/Reason/ConsecutiveAt.php create mode 100644 3rdparty/egulias/email-validator/src/Result/Reason/ConsecutiveDot.php create mode 100644 3rdparty/egulias/email-validator/src/Result/Reason/DetailedReason.php create mode 100644 3rdparty/egulias/email-validator/src/Result/Reason/DomainAcceptsNoMail.php create mode 100644 3rdparty/egulias/email-validator/src/Result/Reason/DomainHyphened.php create mode 100644 3rdparty/egulias/email-validator/src/Result/Reason/DomainTooLong.php create mode 100644 3rdparty/egulias/email-validator/src/Result/Reason/DotAtEnd.php create mode 100644 3rdparty/egulias/email-validator/src/Result/Reason/DotAtStart.php create mode 100644 3rdparty/egulias/email-validator/src/Result/Reason/EmptyReason.php create mode 100644 3rdparty/egulias/email-validator/src/Result/Reason/ExceptionFound.php create mode 100644 3rdparty/egulias/email-validator/src/Result/Reason/ExpectingATEXT.php create mode 100644 3rdparty/egulias/email-validator/src/Result/Reason/ExpectingCTEXT.php create mode 100644 3rdparty/egulias/email-validator/src/Result/Reason/ExpectingDTEXT.php create mode 100644 3rdparty/egulias/email-validator/src/Result/Reason/ExpectingDomainLiteralClose.php create mode 100644 3rdparty/egulias/email-validator/src/Result/Reason/LabelTooLong.php create mode 100644 3rdparty/egulias/email-validator/src/Result/Reason/LocalOrReservedDomain.php create mode 100644 3rdparty/egulias/email-validator/src/Result/Reason/NoDNSRecord.php create mode 100644 3rdparty/egulias/email-validator/src/Result/Reason/NoDomainPart.php create mode 100644 3rdparty/egulias/email-validator/src/Result/Reason/NoLocalPart.php create mode 100644 3rdparty/egulias/email-validator/src/Result/Reason/RFCWarnings.php create mode 100644 3rdparty/egulias/email-validator/src/Result/Reason/Reason.php create mode 100644 3rdparty/egulias/email-validator/src/Result/Reason/SpoofEmail.php create mode 100644 3rdparty/egulias/email-validator/src/Result/Reason/UnOpenedComment.php create mode 100644 3rdparty/egulias/email-validator/src/Result/Reason/UnableToGetDNSRecord.php create mode 100644 3rdparty/egulias/email-validator/src/Result/Reason/UnclosedComment.php create mode 100644 3rdparty/egulias/email-validator/src/Result/Reason/UnclosedQuotedString.php create mode 100644 3rdparty/egulias/email-validator/src/Result/Reason/UnusualElements.php create mode 100644 3rdparty/egulias/email-validator/src/Result/Result.php create mode 100644 3rdparty/egulias/email-validator/src/Result/SpoofEmail.php create mode 100644 3rdparty/egulias/email-validator/src/Result/ValidEmail.php create mode 100644 3rdparty/egulias/email-validator/src/Validation/DNSCheckValidation.php create mode 100644 3rdparty/egulias/email-validator/src/Validation/DNSGetRecordWrapper.php create mode 100644 3rdparty/egulias/email-validator/src/Validation/DNSRecords.php create mode 100644 3rdparty/egulias/email-validator/src/Validation/EmailValidation.php create mode 100644 3rdparty/egulias/email-validator/src/Validation/Exception/EmptyValidationList.php create mode 100644 3rdparty/egulias/email-validator/src/Validation/Extra/SpoofCheckValidation.php create mode 100644 3rdparty/egulias/email-validator/src/Validation/MessageIDValidation.php create mode 100644 3rdparty/egulias/email-validator/src/Validation/MultipleValidationWithAnd.php create mode 100644 3rdparty/egulias/email-validator/src/Validation/NoRFCWarningsValidation.php create mode 100644 3rdparty/egulias/email-validator/src/Validation/RFCValidation.php create mode 100644 3rdparty/egulias/email-validator/src/Warning/AddressLiteral.php create mode 100644 3rdparty/egulias/email-validator/src/Warning/CFWSNearAt.php create mode 100644 3rdparty/egulias/email-validator/src/Warning/CFWSWithFWS.php create mode 100644 3rdparty/egulias/email-validator/src/Warning/Comment.php create mode 100644 3rdparty/egulias/email-validator/src/Warning/DeprecatedComment.php create mode 100644 3rdparty/egulias/email-validator/src/Warning/DomainLiteral.php create mode 100644 3rdparty/egulias/email-validator/src/Warning/EmailTooLong.php create mode 100644 3rdparty/egulias/email-validator/src/Warning/IPV6BadChar.php create mode 100644 3rdparty/egulias/email-validator/src/Warning/IPV6ColonEnd.php create mode 100644 3rdparty/egulias/email-validator/src/Warning/IPV6ColonStart.php create mode 100644 3rdparty/egulias/email-validator/src/Warning/IPV6Deprecated.php create mode 100644 3rdparty/egulias/email-validator/src/Warning/IPV6DoubleColon.php create mode 100644 3rdparty/egulias/email-validator/src/Warning/IPV6GroupCount.php create mode 100644 3rdparty/egulias/email-validator/src/Warning/IPV6MaxGroups.php create mode 100644 3rdparty/egulias/email-validator/src/Warning/LocalTooLong.php create mode 100644 3rdparty/egulias/email-validator/src/Warning/NoDNSMXRecord.php create mode 100644 3rdparty/egulias/email-validator/src/Warning/ObsoleteDTEXT.php create mode 100644 3rdparty/egulias/email-validator/src/Warning/QuotedPart.php create mode 100644 3rdparty/egulias/email-validator/src/Warning/QuotedString.php create mode 100644 3rdparty/egulias/email-validator/src/Warning/TLD.php create mode 100644 3rdparty/egulias/email-validator/src/Warning/Warning.php create mode 100644 3rdparty/f7cloud/lognormalizer/COPYING create mode 100644 3rdparty/f7cloud/lognormalizer/src/Normalizer.php create mode 100644 3rdparty/fusonic/opengraph/LICENSE create mode 100644 3rdparty/fusonic/opengraph/src/Consumer.php create mode 100644 3rdparty/fusonic/opengraph/src/Elements/Audio.php create mode 100644 3rdparty/fusonic/opengraph/src/Elements/ElementBase.php create mode 100644 3rdparty/fusonic/opengraph/src/Elements/Image.php create mode 100644 3rdparty/fusonic/opengraph/src/Elements/Video.php create mode 100644 3rdparty/fusonic/opengraph/src/Objects/ObjectBase.php create mode 100644 3rdparty/fusonic/opengraph/src/Objects/Website.php create mode 100644 3rdparty/fusonic/opengraph/src/Property.php create mode 100644 3rdparty/fusonic/opengraph/src/Publisher.php create mode 100644 3rdparty/giggsey/libphonenumber-for-php-lite/LICENSE.txt create mode 100644 3rdparty/giggsey/libphonenumber-for-php-lite/METADATA-VERSION.php create mode 100644 3rdparty/giggsey/libphonenumber-for-php-lite/src/CountryCodeSource.php create mode 100644 3rdparty/giggsey/libphonenumber-for-php-lite/src/CountryCodeToRegionCodeMap.php create mode 100644 3rdparty/giggsey/libphonenumber-for-php-lite/src/MatchType.php create mode 100644 3rdparty/giggsey/libphonenumber-for-php-lite/src/Matcher.php create mode 100644 3rdparty/giggsey/libphonenumber-for-php-lite/src/MatcherAPIInterface.php create mode 100644 3rdparty/giggsey/libphonenumber-for-php-lite/src/MetadataSourceInterface.php create mode 100644 3rdparty/giggsey/libphonenumber-for-php-lite/src/MultiFileMetadataSourceImpl.php create mode 100644 3rdparty/giggsey/libphonenumber-for-php-lite/src/NumberFormat.php create mode 100644 3rdparty/giggsey/libphonenumber-for-php-lite/src/NumberParseException.php create mode 100644 3rdparty/giggsey/libphonenumber-for-php-lite/src/PhoneMetadata.php create mode 100644 3rdparty/giggsey/libphonenumber-for-php-lite/src/PhoneNumber.php create mode 100644 3rdparty/giggsey/libphonenumber-for-php-lite/src/PhoneNumberDesc.php create mode 100644 3rdparty/giggsey/libphonenumber-for-php-lite/src/PhoneNumberFormat.php create mode 100644 3rdparty/giggsey/libphonenumber-for-php-lite/src/PhoneNumberMatch.php create mode 100644 3rdparty/giggsey/libphonenumber-for-php-lite/src/PhoneNumberType.php create mode 100644 3rdparty/giggsey/libphonenumber-for-php-lite/src/PhoneNumberUtil.php create mode 100644 3rdparty/giggsey/libphonenumber-for-php-lite/src/RegexBasedMatcher.php create mode 100644 3rdparty/giggsey/libphonenumber-for-php-lite/src/ShortNumberCost.php create mode 100644 3rdparty/giggsey/libphonenumber-for-php-lite/src/ShortNumberInfo.php create mode 100644 3rdparty/giggsey/libphonenumber-for-php-lite/src/ShortNumbersRegionCodeSet.php create mode 100644 3rdparty/giggsey/libphonenumber-for-php-lite/src/ValidationResult.php create mode 100644 3rdparty/guzzlehttp/guzzle/LICENSE create mode 100644 3rdparty/guzzlehttp/guzzle/src/BodySummarizer.php create mode 100644 3rdparty/guzzlehttp/guzzle/src/BodySummarizerInterface.php create mode 100644 3rdparty/guzzlehttp/guzzle/src/Client.php create mode 100644 3rdparty/guzzlehttp/guzzle/src/ClientInterface.php create mode 100644 3rdparty/guzzlehttp/guzzle/src/ClientTrait.php create mode 100644 3rdparty/guzzlehttp/guzzle/src/Cookie/CookieJar.php create mode 100644 3rdparty/guzzlehttp/guzzle/src/Cookie/CookieJarInterface.php create mode 100644 3rdparty/guzzlehttp/guzzle/src/Cookie/FileCookieJar.php create mode 100644 3rdparty/guzzlehttp/guzzle/src/Cookie/SessionCookieJar.php create mode 100644 3rdparty/guzzlehttp/guzzle/src/Cookie/SetCookie.php create mode 100644 3rdparty/guzzlehttp/guzzle/src/Exception/BadResponseException.php create mode 100644 3rdparty/guzzlehttp/guzzle/src/Exception/ClientException.php create mode 100644 3rdparty/guzzlehttp/guzzle/src/Exception/ConnectException.php create mode 100644 3rdparty/guzzlehttp/guzzle/src/Exception/GuzzleException.php create mode 100644 3rdparty/guzzlehttp/guzzle/src/Exception/InvalidArgumentException.php create mode 100644 3rdparty/guzzlehttp/guzzle/src/Exception/RequestException.php create mode 100644 3rdparty/guzzlehttp/guzzle/src/Exception/ServerException.php create mode 100644 3rdparty/guzzlehttp/guzzle/src/Exception/TooManyRedirectsException.php create mode 100644 3rdparty/guzzlehttp/guzzle/src/Exception/TransferException.php create mode 100644 3rdparty/guzzlehttp/guzzle/src/Handler/CurlFactory.php create mode 100644 3rdparty/guzzlehttp/guzzle/src/Handler/CurlFactoryInterface.php create mode 100644 3rdparty/guzzlehttp/guzzle/src/Handler/CurlHandler.php create mode 100644 3rdparty/guzzlehttp/guzzle/src/Handler/CurlMultiHandler.php create mode 100644 3rdparty/guzzlehttp/guzzle/src/Handler/EasyHandle.php create mode 100644 3rdparty/guzzlehttp/guzzle/src/Handler/HeaderProcessor.php create mode 100644 3rdparty/guzzlehttp/guzzle/src/Handler/MockHandler.php create mode 100644 3rdparty/guzzlehttp/guzzle/src/Handler/Proxy.php create mode 100644 3rdparty/guzzlehttp/guzzle/src/Handler/StreamHandler.php create mode 100644 3rdparty/guzzlehttp/guzzle/src/HandlerStack.php create mode 100644 3rdparty/guzzlehttp/guzzle/src/MessageFormatter.php create mode 100644 3rdparty/guzzlehttp/guzzle/src/MessageFormatterInterface.php create mode 100644 3rdparty/guzzlehttp/guzzle/src/Middleware.php create mode 100644 3rdparty/guzzlehttp/guzzle/src/Pool.php create mode 100644 3rdparty/guzzlehttp/guzzle/src/PrepareBodyMiddleware.php create mode 100644 3rdparty/guzzlehttp/guzzle/src/RedirectMiddleware.php create mode 100644 3rdparty/guzzlehttp/guzzle/src/RequestOptions.php create mode 100644 3rdparty/guzzlehttp/guzzle/src/RetryMiddleware.php create mode 100644 3rdparty/guzzlehttp/guzzle/src/TransferStats.php create mode 100644 3rdparty/guzzlehttp/guzzle/src/Utils.php create mode 100644 3rdparty/guzzlehttp/guzzle/src/functions.php create mode 100644 3rdparty/guzzlehttp/guzzle/src/functions_include.php create mode 100644 3rdparty/guzzlehttp/promises/LICENSE create mode 100644 3rdparty/guzzlehttp/promises/src/AggregateException.php create mode 100644 3rdparty/guzzlehttp/promises/src/CancellationException.php create mode 100644 3rdparty/guzzlehttp/promises/src/Coroutine.php create mode 100644 3rdparty/guzzlehttp/promises/src/Create.php create mode 100644 3rdparty/guzzlehttp/promises/src/Each.php create mode 100644 3rdparty/guzzlehttp/promises/src/EachPromise.php create mode 100644 3rdparty/guzzlehttp/promises/src/FulfilledPromise.php create mode 100644 3rdparty/guzzlehttp/promises/src/Is.php create mode 100644 3rdparty/guzzlehttp/promises/src/Promise.php create mode 100644 3rdparty/guzzlehttp/promises/src/PromiseInterface.php create mode 100644 3rdparty/guzzlehttp/promises/src/PromisorInterface.php create mode 100644 3rdparty/guzzlehttp/promises/src/RejectedPromise.php create mode 100644 3rdparty/guzzlehttp/promises/src/RejectionException.php create mode 100644 3rdparty/guzzlehttp/promises/src/TaskQueue.php create mode 100644 3rdparty/guzzlehttp/promises/src/TaskQueueInterface.php create mode 100644 3rdparty/guzzlehttp/promises/src/Utils.php create mode 100644 3rdparty/guzzlehttp/psr7/LICENSE create mode 100644 3rdparty/guzzlehttp/psr7/src/AppendStream.php create mode 100644 3rdparty/guzzlehttp/psr7/src/BufferStream.php create mode 100644 3rdparty/guzzlehttp/psr7/src/CachingStream.php create mode 100644 3rdparty/guzzlehttp/psr7/src/DroppingStream.php create mode 100644 3rdparty/guzzlehttp/psr7/src/Exception/MalformedUriException.php create mode 100644 3rdparty/guzzlehttp/psr7/src/FnStream.php create mode 100644 3rdparty/guzzlehttp/psr7/src/Header.php create mode 100644 3rdparty/guzzlehttp/psr7/src/HttpFactory.php create mode 100644 3rdparty/guzzlehttp/psr7/src/InflateStream.php create mode 100644 3rdparty/guzzlehttp/psr7/src/LazyOpenStream.php create mode 100644 3rdparty/guzzlehttp/psr7/src/LimitStream.php create mode 100644 3rdparty/guzzlehttp/psr7/src/Message.php create mode 100644 3rdparty/guzzlehttp/psr7/src/MessageTrait.php create mode 100644 3rdparty/guzzlehttp/psr7/src/MimeType.php create mode 100644 3rdparty/guzzlehttp/psr7/src/MultipartStream.php create mode 100644 3rdparty/guzzlehttp/psr7/src/NoSeekStream.php create mode 100644 3rdparty/guzzlehttp/psr7/src/PumpStream.php create mode 100644 3rdparty/guzzlehttp/psr7/src/Query.php create mode 100644 3rdparty/guzzlehttp/psr7/src/Request.php create mode 100644 3rdparty/guzzlehttp/psr7/src/Response.php create mode 100644 3rdparty/guzzlehttp/psr7/src/Rfc7230.php create mode 100644 3rdparty/guzzlehttp/psr7/src/ServerRequest.php create mode 100644 3rdparty/guzzlehttp/psr7/src/Stream.php create mode 100644 3rdparty/guzzlehttp/psr7/src/StreamDecoratorTrait.php create mode 100644 3rdparty/guzzlehttp/psr7/src/StreamWrapper.php create mode 100644 3rdparty/guzzlehttp/psr7/src/UploadedFile.php create mode 100644 3rdparty/guzzlehttp/psr7/src/Uri.php create mode 100644 3rdparty/guzzlehttp/psr7/src/UriComparator.php create mode 100644 3rdparty/guzzlehttp/psr7/src/UriNormalizer.php create mode 100644 3rdparty/guzzlehttp/psr7/src/UriResolver.php create mode 100644 3rdparty/guzzlehttp/psr7/src/Utils.php create mode 100644 3rdparty/guzzlehttp/uri-template/LICENSE create mode 100644 3rdparty/guzzlehttp/uri-template/src/UriTemplate.php create mode 100644 3rdparty/icewind/searchdav/LICENSE create mode 100644 3rdparty/icewind/searchdav/src/Backend/ISearchBackend.php create mode 100644 3rdparty/icewind/searchdav/src/Backend/SearchPropertyDefinition.php create mode 100644 3rdparty/icewind/searchdav/src/Backend/SearchResult.php create mode 100644 3rdparty/icewind/searchdav/src/DAV/DiscoverHandler.php create mode 100644 3rdparty/icewind/searchdav/src/DAV/PathHelper.php create mode 100644 3rdparty/icewind/searchdav/src/DAV/QueryParser.php create mode 100644 3rdparty/icewind/searchdav/src/DAV/SearchHandler.php create mode 100644 3rdparty/icewind/searchdav/src/DAV/SearchPlugin.php create mode 100644 3rdparty/icewind/searchdav/src/Query/Limit.php create mode 100644 3rdparty/icewind/searchdav/src/Query/Literal.php create mode 100644 3rdparty/icewind/searchdav/src/Query/Operator.php create mode 100644 3rdparty/icewind/searchdav/src/Query/Order.php create mode 100644 3rdparty/icewind/searchdav/src/Query/Query.php create mode 100644 3rdparty/icewind/searchdav/src/Query/Scope.php create mode 100644 3rdparty/icewind/searchdav/src/XML/BasicSearch.php create mode 100644 3rdparty/icewind/searchdav/src/XML/BasicSearchSchema.php create mode 100644 3rdparty/icewind/searchdav/src/XML/Limit.php create mode 100644 3rdparty/icewind/searchdav/src/XML/Literal.php create mode 100644 3rdparty/icewind/searchdav/src/XML/Operator.php create mode 100644 3rdparty/icewind/searchdav/src/XML/Order.php create mode 100644 3rdparty/icewind/searchdav/src/XML/PropDesc.php create mode 100644 3rdparty/icewind/searchdav/src/XML/QueryDiscoverResponse.php create mode 100644 3rdparty/icewind/searchdav/src/XML/Scope.php create mode 100644 3rdparty/icewind/searchdav/src/XML/SupportedQueryGrammar.php create mode 100644 3rdparty/icewind/smb/LICENSE.txt create mode 100644 3rdparty/icewind/smb/LICENSES/AGPL-3.0-or-later.txt create mode 100644 3rdparty/icewind/smb/LICENSES/CC0-1.0.txt create mode 100644 3rdparty/icewind/smb/LICENSES/MIT.txt create mode 100644 3rdparty/icewind/smb/src/ACL.php create mode 100644 3rdparty/icewind/smb/src/AbstractServer.php create mode 100644 3rdparty/icewind/smb/src/AbstractShare.php create mode 100644 3rdparty/icewind/smb/src/AnonymousAuth.php create mode 100644 3rdparty/icewind/smb/src/BasicAuth.php create mode 100644 3rdparty/icewind/smb/src/Change.php create mode 100644 3rdparty/icewind/smb/src/Exception/AccessDeniedException.php create mode 100644 3rdparty/icewind/smb/src/Exception/AlreadyExistsException.php create mode 100644 3rdparty/icewind/smb/src/Exception/AuthenticationException.php create mode 100644 3rdparty/icewind/smb/src/Exception/ConnectException.php create mode 100644 3rdparty/icewind/smb/src/Exception/ConnectionAbortedException.php create mode 100644 3rdparty/icewind/smb/src/Exception/ConnectionException.php create mode 100644 3rdparty/icewind/smb/src/Exception/ConnectionRefusedException.php create mode 100644 3rdparty/icewind/smb/src/Exception/ConnectionResetException.php create mode 100644 3rdparty/icewind/smb/src/Exception/DependencyException.php create mode 100644 3rdparty/icewind/smb/src/Exception/Exception.php create mode 100644 3rdparty/icewind/smb/src/Exception/FileInUseException.php create mode 100644 3rdparty/icewind/smb/src/Exception/ForbiddenException.php create mode 100644 3rdparty/icewind/smb/src/Exception/HostDownException.php create mode 100644 3rdparty/icewind/smb/src/Exception/InvalidArgumentException.php create mode 100644 3rdparty/icewind/smb/src/Exception/InvalidHostException.php create mode 100644 3rdparty/icewind/smb/src/Exception/InvalidParameterException.php create mode 100644 3rdparty/icewind/smb/src/Exception/InvalidPathException.php create mode 100644 3rdparty/icewind/smb/src/Exception/InvalidRequestException.php create mode 100644 3rdparty/icewind/smb/src/Exception/InvalidResourceException.php create mode 100644 3rdparty/icewind/smb/src/Exception/InvalidTicket.php create mode 100644 3rdparty/icewind/smb/src/Exception/InvalidTypeException.php create mode 100644 3rdparty/icewind/smb/src/Exception/NoLoginServerException.php create mode 100644 3rdparty/icewind/smb/src/Exception/NoRouteToHostException.php create mode 100644 3rdparty/icewind/smb/src/Exception/NotEmptyException.php create mode 100644 3rdparty/icewind/smb/src/Exception/NotFoundException.php create mode 100644 3rdparty/icewind/smb/src/Exception/OutOfSpaceException.php create mode 100644 3rdparty/icewind/smb/src/Exception/RevisionMismatchException.php create mode 100644 3rdparty/icewind/smb/src/Exception/TimedOutException.php create mode 100644 3rdparty/icewind/smb/src/IAuth.php create mode 100644 3rdparty/icewind/smb/src/IFileInfo.php create mode 100644 3rdparty/icewind/smb/src/INotifyHandler.php create mode 100644 3rdparty/icewind/smb/src/IOptions.php create mode 100644 3rdparty/icewind/smb/src/IServer.php create mode 100644 3rdparty/icewind/smb/src/IShare.php create mode 100644 3rdparty/icewind/smb/src/ISystem.php create mode 100644 3rdparty/icewind/smb/src/ITimeZoneProvider.php create mode 100644 3rdparty/icewind/smb/src/KerberosApacheAuth.php create mode 100644 3rdparty/icewind/smb/src/KerberosAuth.php create mode 100644 3rdparty/icewind/smb/src/KerberosTicket.php create mode 100644 3rdparty/icewind/smb/src/Native/NativeFileInfo.php create mode 100644 3rdparty/icewind/smb/src/Native/NativeReadStream.php create mode 100644 3rdparty/icewind/smb/src/Native/NativeServer.php create mode 100644 3rdparty/icewind/smb/src/Native/NativeShare.php create mode 100644 3rdparty/icewind/smb/src/Native/NativeState.php create mode 100644 3rdparty/icewind/smb/src/Native/NativeStream.php create mode 100644 3rdparty/icewind/smb/src/Native/NativeWriteStream.php create mode 100644 3rdparty/icewind/smb/src/Options.php create mode 100644 3rdparty/icewind/smb/src/ServerFactory.php create mode 100644 3rdparty/icewind/smb/src/StringBuffer.php create mode 100644 3rdparty/icewind/smb/src/System.php create mode 100644 3rdparty/icewind/smb/src/TimeZoneProvider.php create mode 100644 3rdparty/icewind/smb/src/Wrapped/Connection.php create mode 100644 3rdparty/icewind/smb/src/Wrapped/ErrorCodes.php create mode 100644 3rdparty/icewind/smb/src/Wrapped/FileInfo.php create mode 100644 3rdparty/icewind/smb/src/Wrapped/NotifyHandler.php create mode 100644 3rdparty/icewind/smb/src/Wrapped/Parser.php create mode 100644 3rdparty/icewind/smb/src/Wrapped/RawConnection.php create mode 100644 3rdparty/icewind/smb/src/Wrapped/Server.php create mode 100644 3rdparty/icewind/smb/src/Wrapped/Share.php create mode 100644 3rdparty/icewind/streams/LICENSE.txt create mode 100644 3rdparty/icewind/streams/LICENSES/AGPL-3.0-or-later.txt create mode 100644 3rdparty/icewind/streams/LICENSES/CC0-1.0.txt create mode 100644 3rdparty/icewind/streams/LICENSES/MIT.txt create mode 100644 3rdparty/icewind/streams/composer.json.license create mode 100644 3rdparty/icewind/streams/src/CallbackWrapper.php create mode 100644 3rdparty/icewind/streams/src/CountWrapper.php create mode 100644 3rdparty/icewind/streams/src/Directory.php create mode 100644 3rdparty/icewind/streams/src/DirectoryFilter.php create mode 100644 3rdparty/icewind/streams/src/DirectoryWrapper.php create mode 100644 3rdparty/icewind/streams/src/File.php create mode 100644 3rdparty/icewind/streams/src/HashWrapper.php create mode 100644 3rdparty/icewind/streams/src/IteratorDirectory.php create mode 100644 3rdparty/icewind/streams/src/NullWrapper.php create mode 100644 3rdparty/icewind/streams/src/Path.php create mode 100644 3rdparty/icewind/streams/src/PathWrapper.php create mode 100644 3rdparty/icewind/streams/src/ReadHashWrapper.php create mode 100644 3rdparty/icewind/streams/src/RetryWrapper.php create mode 100644 3rdparty/icewind/streams/src/SeekableWrapper.php create mode 100644 3rdparty/icewind/streams/src/Url.php create mode 100644 3rdparty/icewind/streams/src/UrlCallback.php create mode 100644 3rdparty/icewind/streams/src/Wrapper.php create mode 100644 3rdparty/icewind/streams/src/WrapperHandler.php create mode 100644 3rdparty/icewind/streams/src/WriteHashWrapper.php create mode 100644 3rdparty/justinrainbow/json-schema/LICENSE create mode 100644 3rdparty/justinrainbow/json-schema/src/JsonSchema/ConstraintError.php create mode 100644 3rdparty/justinrainbow/json-schema/src/JsonSchema/Constraints/BaseConstraint.php create mode 100644 3rdparty/justinrainbow/json-schema/src/JsonSchema/Constraints/CollectionConstraint.php create mode 100644 3rdparty/justinrainbow/json-schema/src/JsonSchema/Constraints/ConstConstraint.php create mode 100644 3rdparty/justinrainbow/json-schema/src/JsonSchema/Constraints/Constraint.php create mode 100644 3rdparty/justinrainbow/json-schema/src/JsonSchema/Constraints/ConstraintInterface.php create mode 100644 3rdparty/justinrainbow/json-schema/src/JsonSchema/Constraints/EnumConstraint.php create mode 100644 3rdparty/justinrainbow/json-schema/src/JsonSchema/Constraints/Factory.php create mode 100644 3rdparty/justinrainbow/json-schema/src/JsonSchema/Constraints/FormatConstraint.php create mode 100644 3rdparty/justinrainbow/json-schema/src/JsonSchema/Constraints/NumberConstraint.php create mode 100644 3rdparty/justinrainbow/json-schema/src/JsonSchema/Constraints/ObjectConstraint.php create mode 100644 3rdparty/justinrainbow/json-schema/src/JsonSchema/Constraints/SchemaConstraint.php create mode 100644 3rdparty/justinrainbow/json-schema/src/JsonSchema/Constraints/StringConstraint.php create mode 100644 3rdparty/justinrainbow/json-schema/src/JsonSchema/Constraints/TypeCheck/LooseTypeCheck.php create mode 100644 3rdparty/justinrainbow/json-schema/src/JsonSchema/Constraints/TypeCheck/StrictTypeCheck.php create mode 100644 3rdparty/justinrainbow/json-schema/src/JsonSchema/Constraints/TypeCheck/TypeCheckInterface.php create mode 100644 3rdparty/justinrainbow/json-schema/src/JsonSchema/Constraints/TypeConstraint.php create mode 100644 3rdparty/justinrainbow/json-schema/src/JsonSchema/Constraints/UndefinedConstraint.php create mode 100644 3rdparty/justinrainbow/json-schema/src/JsonSchema/Entity/JsonPointer.php create mode 100644 3rdparty/justinrainbow/json-schema/src/JsonSchema/Enum.php create mode 100644 3rdparty/justinrainbow/json-schema/src/JsonSchema/Exception/ExceptionInterface.php create mode 100644 3rdparty/justinrainbow/json-schema/src/JsonSchema/Exception/InvalidArgumentException.php create mode 100644 3rdparty/justinrainbow/json-schema/src/JsonSchema/Exception/InvalidConfigException.php create mode 100644 3rdparty/justinrainbow/json-schema/src/JsonSchema/Exception/InvalidSchemaException.php create mode 100644 3rdparty/justinrainbow/json-schema/src/JsonSchema/Exception/InvalidSchemaMediaTypeException.php create mode 100644 3rdparty/justinrainbow/json-schema/src/JsonSchema/Exception/InvalidSourceUriException.php create mode 100644 3rdparty/justinrainbow/json-schema/src/JsonSchema/Exception/JsonDecodingException.php create mode 100644 3rdparty/justinrainbow/json-schema/src/JsonSchema/Exception/ResourceNotFoundException.php create mode 100644 3rdparty/justinrainbow/json-schema/src/JsonSchema/Exception/RuntimeException.php create mode 100644 3rdparty/justinrainbow/json-schema/src/JsonSchema/Exception/UnresolvableJsonPointerException.php create mode 100644 3rdparty/justinrainbow/json-schema/src/JsonSchema/Exception/UriResolverException.php create mode 100644 3rdparty/justinrainbow/json-schema/src/JsonSchema/Exception/ValidationException.php create mode 100644 3rdparty/justinrainbow/json-schema/src/JsonSchema/Iterator/ObjectIterator.php create mode 100644 3rdparty/justinrainbow/json-schema/src/JsonSchema/Rfc3339.php create mode 100644 3rdparty/justinrainbow/json-schema/src/JsonSchema/SchemaStorage.php create mode 100644 3rdparty/justinrainbow/json-schema/src/JsonSchema/SchemaStorageInterface.php create mode 100644 3rdparty/justinrainbow/json-schema/src/JsonSchema/Tool/DeepComparer.php create mode 100644 3rdparty/justinrainbow/json-schema/src/JsonSchema/Tool/DeepCopy.php create mode 100644 3rdparty/justinrainbow/json-schema/src/JsonSchema/Tool/Validator/RelativeReferenceValidator.php create mode 100644 3rdparty/justinrainbow/json-schema/src/JsonSchema/Tool/Validator/UriValidator.php create mode 100644 3rdparty/justinrainbow/json-schema/src/JsonSchema/Uri/Retrievers/AbstractRetriever.php create mode 100644 3rdparty/justinrainbow/json-schema/src/JsonSchema/Uri/Retrievers/Curl.php create mode 100644 3rdparty/justinrainbow/json-schema/src/JsonSchema/Uri/Retrievers/FileGetContents.php create mode 100644 3rdparty/justinrainbow/json-schema/src/JsonSchema/Uri/Retrievers/PredefinedArray.php create mode 100644 3rdparty/justinrainbow/json-schema/src/JsonSchema/Uri/Retrievers/UriRetrieverInterface.php create mode 100644 3rdparty/justinrainbow/json-schema/src/JsonSchema/Uri/UriResolver.php create mode 100644 3rdparty/justinrainbow/json-schema/src/JsonSchema/Uri/UriRetriever.php create mode 100644 3rdparty/justinrainbow/json-schema/src/JsonSchema/UriResolverInterface.php create mode 100644 3rdparty/justinrainbow/json-schema/src/JsonSchema/UriRetrieverInterface.php create mode 100644 3rdparty/justinrainbow/json-schema/src/JsonSchema/Validator.php create mode 100644 3rdparty/kornrunner/blurhash/LICENSE create mode 100644 3rdparty/kornrunner/blurhash/src/AC.php create mode 100644 3rdparty/kornrunner/blurhash/src/Base83.php create mode 100644 3rdparty/kornrunner/blurhash/src/Blurhash.php create mode 100644 3rdparty/kornrunner/blurhash/src/Color.php create mode 100644 3rdparty/kornrunner/blurhash/src/DC.php create mode 100644 3rdparty/laravel/serializable-closure/LICENSE.md create mode 100644 3rdparty/laravel/serializable-closure/src/Contracts/Serializable.php create mode 100644 3rdparty/laravel/serializable-closure/src/Contracts/Signer.php create mode 100644 3rdparty/laravel/serializable-closure/src/Exceptions/InvalidSignatureException.php create mode 100644 3rdparty/laravel/serializable-closure/src/Exceptions/MissingSecretKeyException.php create mode 100644 3rdparty/laravel/serializable-closure/src/Exceptions/PhpVersionNotSupportedException.php create mode 100644 3rdparty/laravel/serializable-closure/src/SerializableClosure.php create mode 100644 3rdparty/laravel/serializable-closure/src/Serializers/Native.php create mode 100644 3rdparty/laravel/serializable-closure/src/Serializers/Signed.php create mode 100644 3rdparty/laravel/serializable-closure/src/Signers/Hmac.php create mode 100644 3rdparty/laravel/serializable-closure/src/Support/ClosureScope.php create mode 100644 3rdparty/laravel/serializable-closure/src/Support/ClosureStream.php create mode 100644 3rdparty/laravel/serializable-closure/src/Support/ReflectionClosure.php create mode 100644 3rdparty/laravel/serializable-closure/src/Support/SelfReference.php create mode 100644 3rdparty/laravel/serializable-closure/src/UnsignedSerializableClosure.php create mode 100644 3rdparty/lcobucci/clock/LICENSE create mode 100644 3rdparty/lcobucci/clock/src/Clock.php create mode 100644 3rdparty/lcobucci/clock/src/FrozenClock.php create mode 100644 3rdparty/lcobucci/clock/src/SystemClock.php create mode 100644 3rdparty/marc-mabe/php-enum/LICENSE.txt create mode 100644 3rdparty/marc-mabe/php-enum/src/Enum.php create mode 100644 3rdparty/marc-mabe/php-enum/src/EnumMap.php create mode 100644 3rdparty/marc-mabe/php-enum/src/EnumSerializableTrait.php create mode 100644 3rdparty/marc-mabe/php-enum/src/EnumSet.php create mode 100644 3rdparty/marc-mabe/php-enum/stubs/Stringable.php create mode 100644 3rdparty/masterminds/html5/LICENSE.txt create mode 100644 3rdparty/masterminds/html5/src/HTML5.php create mode 100644 3rdparty/masterminds/html5/src/HTML5/Elements.php create mode 100644 3rdparty/masterminds/html5/src/HTML5/Entities.php create mode 100644 3rdparty/masterminds/html5/src/HTML5/Exception.php create mode 100644 3rdparty/masterminds/html5/src/HTML5/InstructionProcessor.php create mode 100644 3rdparty/masterminds/html5/src/HTML5/Parser/CharacterReference.php create mode 100644 3rdparty/masterminds/html5/src/HTML5/Parser/DOMTreeBuilder.php create mode 100644 3rdparty/masterminds/html5/src/HTML5/Parser/EventHandler.php create mode 100644 3rdparty/masterminds/html5/src/HTML5/Parser/FileInputStream.php create mode 100644 3rdparty/masterminds/html5/src/HTML5/Parser/InputStream.php create mode 100644 3rdparty/masterminds/html5/src/HTML5/Parser/ParseError.php create mode 100644 3rdparty/masterminds/html5/src/HTML5/Parser/Scanner.php create mode 100644 3rdparty/masterminds/html5/src/HTML5/Parser/StringInputStream.php create mode 100644 3rdparty/masterminds/html5/src/HTML5/Parser/Tokenizer.php create mode 100644 3rdparty/masterminds/html5/src/HTML5/Parser/TreeBuildingRules.php create mode 100644 3rdparty/masterminds/html5/src/HTML5/Parser/UTF8Utils.php create mode 100644 3rdparty/masterminds/html5/src/HTML5/Serializer/HTML5Entities.php create mode 100644 3rdparty/masterminds/html5/src/HTML5/Serializer/OutputRules.php create mode 100644 3rdparty/masterminds/html5/src/HTML5/Serializer/RulesInterface.php create mode 100644 3rdparty/masterminds/html5/src/HTML5/Serializer/Traverser.php create mode 100644 3rdparty/mexitek/phpcolors/LICENSE create mode 100644 3rdparty/mexitek/phpcolors/src/Mexitek/PHPColors/Color.php create mode 100644 3rdparty/microsoft/azure-storage-blob/BreakingChanges.md create mode 100644 3rdparty/microsoft/azure-storage-blob/CONTRIBUTING.md create mode 100644 3rdparty/microsoft/azure-storage-blob/ChangeLog.md create mode 100644 3rdparty/microsoft/azure-storage-blob/LICENSE create mode 100644 3rdparty/microsoft/azure-storage-blob/src/Blob/BlobRestProxy.php create mode 100644 3rdparty/microsoft/azure-storage-blob/src/Blob/BlobSharedAccessSignatureHelper.php create mode 100644 3rdparty/microsoft/azure-storage-blob/src/Blob/Internal/BlobResources.php create mode 100644 3rdparty/microsoft/azure-storage-blob/src/Blob/Internal/IBlob.php create mode 100644 3rdparty/microsoft/azure-storage-blob/src/Blob/Models/AccessCondition.php create mode 100644 3rdparty/microsoft/azure-storage-blob/src/Blob/Models/AccessTierTrait.php create mode 100644 3rdparty/microsoft/azure-storage-blob/src/Blob/Models/AppendBlockOptions.php create mode 100644 3rdparty/microsoft/azure-storage-blob/src/Blob/Models/AppendBlockResult.php create mode 100644 3rdparty/microsoft/azure-storage-blob/src/Blob/Models/Blob.php create mode 100644 3rdparty/microsoft/azure-storage-blob/src/Blob/Models/BlobAccessPolicy.php create mode 100644 3rdparty/microsoft/azure-storage-blob/src/Blob/Models/BlobBlockType.php create mode 100644 3rdparty/microsoft/azure-storage-blob/src/Blob/Models/BlobPrefix.php create mode 100644 3rdparty/microsoft/azure-storage-blob/src/Blob/Models/BlobProperties.php create mode 100644 3rdparty/microsoft/azure-storage-blob/src/Blob/Models/BlobServiceOptions.php create mode 100644 3rdparty/microsoft/azure-storage-blob/src/Blob/Models/BlobType.php create mode 100644 3rdparty/microsoft/azure-storage-blob/src/Blob/Models/Block.php create mode 100644 3rdparty/microsoft/azure-storage-blob/src/Blob/Models/BlockList.php create mode 100644 3rdparty/microsoft/azure-storage-blob/src/Blob/Models/BreakLeaseResult.php create mode 100644 3rdparty/microsoft/azure-storage-blob/src/Blob/Models/CommitBlobBlocksOptions.php create mode 100644 3rdparty/microsoft/azure-storage-blob/src/Blob/Models/Container.php create mode 100644 3rdparty/microsoft/azure-storage-blob/src/Blob/Models/ContainerACL.php create mode 100644 3rdparty/microsoft/azure-storage-blob/src/Blob/Models/ContainerAccessPolicy.php create mode 100644 3rdparty/microsoft/azure-storage-blob/src/Blob/Models/ContainerProperties.php create mode 100644 3rdparty/microsoft/azure-storage-blob/src/Blob/Models/CopyBlobFromURLOptions.php create mode 100644 3rdparty/microsoft/azure-storage-blob/src/Blob/Models/CopyBlobOptions.php create mode 100644 3rdparty/microsoft/azure-storage-blob/src/Blob/Models/CopyBlobResult.php create mode 100644 3rdparty/microsoft/azure-storage-blob/src/Blob/Models/CopyState.php create mode 100644 3rdparty/microsoft/azure-storage-blob/src/Blob/Models/CreateBlobBlockOptions.php create mode 100644 3rdparty/microsoft/azure-storage-blob/src/Blob/Models/CreateBlobOptions.php create mode 100644 3rdparty/microsoft/azure-storage-blob/src/Blob/Models/CreateBlobPagesOptions.php create mode 100644 3rdparty/microsoft/azure-storage-blob/src/Blob/Models/CreateBlobPagesResult.php create mode 100644 3rdparty/microsoft/azure-storage-blob/src/Blob/Models/CreateBlobSnapshotOptions.php create mode 100644 3rdparty/microsoft/azure-storage-blob/src/Blob/Models/CreateBlobSnapshotResult.php create mode 100644 3rdparty/microsoft/azure-storage-blob/src/Blob/Models/CreateBlockBlobOptions.php create mode 100644 3rdparty/microsoft/azure-storage-blob/src/Blob/Models/CreateContainerOptions.php create mode 100644 3rdparty/microsoft/azure-storage-blob/src/Blob/Models/CreatePageBlobFromContentOptions.php create mode 100644 3rdparty/microsoft/azure-storage-blob/src/Blob/Models/CreatePageBlobOptions.php create mode 100644 3rdparty/microsoft/azure-storage-blob/src/Blob/Models/DeleteBlobOptions.php create mode 100644 3rdparty/microsoft/azure-storage-blob/src/Blob/Models/GetBlobMetadataOptions.php create mode 100644 3rdparty/microsoft/azure-storage-blob/src/Blob/Models/GetBlobMetadataResult.php create mode 100644 3rdparty/microsoft/azure-storage-blob/src/Blob/Models/GetBlobOptions.php create mode 100644 3rdparty/microsoft/azure-storage-blob/src/Blob/Models/GetBlobPropertiesOptions.php create mode 100644 3rdparty/microsoft/azure-storage-blob/src/Blob/Models/GetBlobPropertiesResult.php create mode 100644 3rdparty/microsoft/azure-storage-blob/src/Blob/Models/GetBlobResult.php create mode 100644 3rdparty/microsoft/azure-storage-blob/src/Blob/Models/GetContainerACLResult.php create mode 100644 3rdparty/microsoft/azure-storage-blob/src/Blob/Models/GetContainerPropertiesResult.php create mode 100644 3rdparty/microsoft/azure-storage-blob/src/Blob/Models/LeaseMode.php create mode 100644 3rdparty/microsoft/azure-storage-blob/src/Blob/Models/LeaseResult.php create mode 100644 3rdparty/microsoft/azure-storage-blob/src/Blob/Models/ListBlobBlocksOptions.php create mode 100644 3rdparty/microsoft/azure-storage-blob/src/Blob/Models/ListBlobBlocksResult.php create mode 100644 3rdparty/microsoft/azure-storage-blob/src/Blob/Models/ListBlobsOptions.php create mode 100644 3rdparty/microsoft/azure-storage-blob/src/Blob/Models/ListBlobsResult.php create mode 100644 3rdparty/microsoft/azure-storage-blob/src/Blob/Models/ListContainersOptions.php create mode 100644 3rdparty/microsoft/azure-storage-blob/src/Blob/Models/ListContainersResult.php create mode 100644 3rdparty/microsoft/azure-storage-blob/src/Blob/Models/ListPageBlobRangesDiffResult.php create mode 100644 3rdparty/microsoft/azure-storage-blob/src/Blob/Models/ListPageBlobRangesOptions.php create mode 100644 3rdparty/microsoft/azure-storage-blob/src/Blob/Models/ListPageBlobRangesResult.php create mode 100644 3rdparty/microsoft/azure-storage-blob/src/Blob/Models/PageWriteOption.php create mode 100644 3rdparty/microsoft/azure-storage-blob/src/Blob/Models/PublicAccessType.php create mode 100644 3rdparty/microsoft/azure-storage-blob/src/Blob/Models/PutBlobResult.php create mode 100644 3rdparty/microsoft/azure-storage-blob/src/Blob/Models/PutBlockResult.php create mode 100644 3rdparty/microsoft/azure-storage-blob/src/Blob/Models/SetBlobMetadataResult.php create mode 100644 3rdparty/microsoft/azure-storage-blob/src/Blob/Models/SetBlobPropertiesOptions.php create mode 100644 3rdparty/microsoft/azure-storage-blob/src/Blob/Models/SetBlobPropertiesResult.php create mode 100644 3rdparty/microsoft/azure-storage-blob/src/Blob/Models/SetBlobTierOptions.php create mode 100644 3rdparty/microsoft/azure-storage-blob/src/Blob/Models/UndeleteBlobOptions.php create mode 100644 3rdparty/microsoft/azure-storage-common/BreakingChanges.md create mode 100644 3rdparty/microsoft/azure-storage-common/CONTRIBUTING.md create mode 100644 3rdparty/microsoft/azure-storage-common/ChangeLog.md create mode 100644 3rdparty/microsoft/azure-storage-common/LICENSE create mode 100644 3rdparty/microsoft/azure-storage-common/src/Common/CloudConfigurationManager.php create mode 100644 3rdparty/microsoft/azure-storage-common/src/Common/Exceptions/InvalidArgumentTypeException.php create mode 100644 3rdparty/microsoft/azure-storage-common/src/Common/Exceptions/ServiceException.php create mode 100644 3rdparty/microsoft/azure-storage-common/src/Common/Internal/ACLBase.php create mode 100644 3rdparty/microsoft/azure-storage-common/src/Common/Internal/Authentication/IAuthScheme.php create mode 100644 3rdparty/microsoft/azure-storage-common/src/Common/Internal/Authentication/SharedAccessSignatureAuthScheme.php create mode 100644 3rdparty/microsoft/azure-storage-common/src/Common/Internal/Authentication/SharedKeyAuthScheme.php create mode 100644 3rdparty/microsoft/azure-storage-common/src/Common/Internal/Authentication/TokenAuthScheme.php create mode 100644 3rdparty/microsoft/azure-storage-common/src/Common/Internal/ConnectionStringParser.php create mode 100644 3rdparty/microsoft/azure-storage-common/src/Common/Internal/ConnectionStringSource.php create mode 100644 3rdparty/microsoft/azure-storage-common/src/Common/Internal/Http/HttpCallContext.php create mode 100644 3rdparty/microsoft/azure-storage-common/src/Common/Internal/Http/HttpFormatter.php create mode 100644 3rdparty/microsoft/azure-storage-common/src/Common/Internal/MetadataTrait.php create mode 100644 3rdparty/microsoft/azure-storage-common/src/Common/Internal/Middlewares/CommonRequestMiddleware.php create mode 100644 3rdparty/microsoft/azure-storage-common/src/Common/Internal/Resources.php create mode 100644 3rdparty/microsoft/azure-storage-common/src/Common/Internal/RestProxy.php create mode 100644 3rdparty/microsoft/azure-storage-common/src/Common/Internal/Serialization/ISerializer.php create mode 100644 3rdparty/microsoft/azure-storage-common/src/Common/Internal/Serialization/JsonSerializer.php create mode 100644 3rdparty/microsoft/azure-storage-common/src/Common/Internal/Serialization/MessageSerializer.php create mode 100644 3rdparty/microsoft/azure-storage-common/src/Common/Internal/Serialization/XmlSerializer.php create mode 100644 3rdparty/microsoft/azure-storage-common/src/Common/Internal/ServiceRestProxy.php create mode 100644 3rdparty/microsoft/azure-storage-common/src/Common/Internal/ServiceRestTrait.php create mode 100644 3rdparty/microsoft/azure-storage-common/src/Common/Internal/ServiceSettings.php create mode 100644 3rdparty/microsoft/azure-storage-common/src/Common/Internal/StorageServiceSettings.php create mode 100644 3rdparty/microsoft/azure-storage-common/src/Common/Internal/Utilities.php create mode 100644 3rdparty/microsoft/azure-storage-common/src/Common/Internal/Validate.php create mode 100644 3rdparty/microsoft/azure-storage-common/src/Common/LocationMode.php create mode 100644 3rdparty/microsoft/azure-storage-common/src/Common/Logger.php create mode 100644 3rdparty/microsoft/azure-storage-common/src/Common/MarkerContinuationTokenTrait.php create mode 100644 3rdparty/microsoft/azure-storage-common/src/Common/Middlewares/HistoryMiddleware.php create mode 100644 3rdparty/microsoft/azure-storage-common/src/Common/Middlewares/IMiddleware.php create mode 100644 3rdparty/microsoft/azure-storage-common/src/Common/Middlewares/MiddlewareBase.php create mode 100644 3rdparty/microsoft/azure-storage-common/src/Common/Middlewares/MiddlewareStack.php create mode 100644 3rdparty/microsoft/azure-storage-common/src/Common/Middlewares/RetryMiddleware.php create mode 100644 3rdparty/microsoft/azure-storage-common/src/Common/Middlewares/RetryMiddlewareFactory.php create mode 100644 3rdparty/microsoft/azure-storage-common/src/Common/Models/AccessPolicy.php create mode 100644 3rdparty/microsoft/azure-storage-common/src/Common/Models/CORS.php create mode 100644 3rdparty/microsoft/azure-storage-common/src/Common/Models/ContinuationToken.php create mode 100644 3rdparty/microsoft/azure-storage-common/src/Common/Models/GetServicePropertiesResult.php create mode 100644 3rdparty/microsoft/azure-storage-common/src/Common/Models/GetServiceStatsResult.php create mode 100644 3rdparty/microsoft/azure-storage-common/src/Common/Models/Logging.php create mode 100644 3rdparty/microsoft/azure-storage-common/src/Common/Models/MarkerContinuationToken.php create mode 100644 3rdparty/microsoft/azure-storage-common/src/Common/Models/Metrics.php create mode 100644 3rdparty/microsoft/azure-storage-common/src/Common/Models/Range.php create mode 100644 3rdparty/microsoft/azure-storage-common/src/Common/Models/RangeDiff.php create mode 100644 3rdparty/microsoft/azure-storage-common/src/Common/Models/RetentionPolicy.php create mode 100644 3rdparty/microsoft/azure-storage-common/src/Common/Models/ServiceOptions.php create mode 100644 3rdparty/microsoft/azure-storage-common/src/Common/Models/ServiceProperties.php create mode 100644 3rdparty/microsoft/azure-storage-common/src/Common/Models/SignedIdentifier.php create mode 100644 3rdparty/microsoft/azure-storage-common/src/Common/Models/TransactionalMD5Trait.php create mode 100644 3rdparty/microsoft/azure-storage-common/src/Common/SharedAccessSignatureHelper.php create mode 100644 3rdparty/mlocati/ip-lib/LICENSE.txt create mode 100644 3rdparty/mlocati/ip-lib/ip-lib.php create mode 100644 3rdparty/mlocati/ip-lib/src/Address/AddressInterface.php create mode 100644 3rdparty/mlocati/ip-lib/src/Address/AssignedRange.php create mode 100644 3rdparty/mlocati/ip-lib/src/Address/IPv4.php create mode 100644 3rdparty/mlocati/ip-lib/src/Address/IPv6.php create mode 100644 3rdparty/mlocati/ip-lib/src/Address/Type.php create mode 100644 3rdparty/mlocati/ip-lib/src/Factory.php create mode 100644 3rdparty/mlocati/ip-lib/src/ParseStringFlag.php create mode 100644 3rdparty/mlocati/ip-lib/src/Range/AbstractRange.php create mode 100644 3rdparty/mlocati/ip-lib/src/Range/Pattern.php create mode 100644 3rdparty/mlocati/ip-lib/src/Range/RangeInterface.php create mode 100644 3rdparty/mlocati/ip-lib/src/Range/Single.php create mode 100644 3rdparty/mlocati/ip-lib/src/Range/Subnet.php create mode 100644 3rdparty/mlocati/ip-lib/src/Range/Type.php create mode 100644 3rdparty/mlocati/ip-lib/src/Service/BinaryMath.php create mode 100644 3rdparty/mlocati/ip-lib/src/Service/RangesFromBoundaryCalculator.php create mode 100644 3rdparty/mlocati/ip-lib/src/Service/UnsignedIntegerMath.php create mode 100644 3rdparty/mtdowling/jmespath.php/LICENSE create mode 100644 3rdparty/mtdowling/jmespath.php/src/AstRuntime.php create mode 100644 3rdparty/mtdowling/jmespath.php/src/CompilerRuntime.php create mode 100644 3rdparty/mtdowling/jmespath.php/src/DebugRuntime.php create mode 100644 3rdparty/mtdowling/jmespath.php/src/Env.php create mode 100644 3rdparty/mtdowling/jmespath.php/src/FnDispatcher.php create mode 100644 3rdparty/mtdowling/jmespath.php/src/JmesPath.php create mode 100644 3rdparty/mtdowling/jmespath.php/src/Lexer.php create mode 100644 3rdparty/mtdowling/jmespath.php/src/Parser.php create mode 100644 3rdparty/mtdowling/jmespath.php/src/SyntaxErrorException.php create mode 100644 3rdparty/mtdowling/jmespath.php/src/TreeCompiler.php create mode 100644 3rdparty/mtdowling/jmespath.php/src/TreeInterpreter.php create mode 100644 3rdparty/mtdowling/jmespath.php/src/Utils.php create mode 100644 3rdparty/paragonie/constant_time_encoding/LICENSE.txt create mode 100644 3rdparty/paragonie/constant_time_encoding/src/Base32.php create mode 100644 3rdparty/paragonie/constant_time_encoding/src/Base32Hex.php create mode 100644 3rdparty/paragonie/constant_time_encoding/src/Base64.php create mode 100644 3rdparty/paragonie/constant_time_encoding/src/Base64DotSlash.php create mode 100644 3rdparty/paragonie/constant_time_encoding/src/Base64DotSlashOrdered.php create mode 100644 3rdparty/paragonie/constant_time_encoding/src/Base64UrlSafe.php create mode 100644 3rdparty/paragonie/constant_time_encoding/src/Binary.php create mode 100644 3rdparty/paragonie/constant_time_encoding/src/EncoderInterface.php create mode 100644 3rdparty/paragonie/constant_time_encoding/src/Encoding.php create mode 100644 3rdparty/paragonie/constant_time_encoding/src/Hex.php create mode 100644 3rdparty/paragonie/constant_time_encoding/src/RFC4648.php create mode 100644 3rdparty/pear/archive_tar/Archive/Tar.php create mode 100644 3rdparty/pear/archive_tar/package.xml create mode 100644 3rdparty/pear/console_getopt/Console/Getopt.php create mode 100644 3rdparty/pear/console_getopt/LICENSE create mode 100644 3rdparty/pear/console_getopt/package.xml create mode 100644 3rdparty/pear/pear-core-minimal/src/OS/Guess.php create mode 100644 3rdparty/pear/pear-core-minimal/src/PEAR.php create mode 100644 3rdparty/pear/pear-core-minimal/src/PEAR/ErrorStack.php create mode 100644 3rdparty/pear/pear-core-minimal/src/System.php create mode 100644 3rdparty/pear/pear_exception/LICENSE create mode 100644 3rdparty/pear/pear_exception/PEAR/Exception.php create mode 100644 3rdparty/php-http/guzzle7-adapter/LICENSE create mode 100644 3rdparty/php-http/guzzle7-adapter/src/Client.php create mode 100644 3rdparty/php-http/guzzle7-adapter/src/Exception/UnexpectedValueException.php create mode 100644 3rdparty/php-http/guzzle7-adapter/src/Promise.php create mode 100644 3rdparty/php-http/httplug/LICENSE create mode 100644 3rdparty/php-http/httplug/puli.json create mode 100644 3rdparty/php-http/httplug/src/Exception.php create mode 100644 3rdparty/php-http/httplug/src/Exception/HttpException.php create mode 100644 3rdparty/php-http/httplug/src/Exception/NetworkException.php create mode 100644 3rdparty/php-http/httplug/src/Exception/RequestAwareTrait.php create mode 100644 3rdparty/php-http/httplug/src/Exception/RequestException.php create mode 100644 3rdparty/php-http/httplug/src/Exception/TransferException.php create mode 100644 3rdparty/php-http/httplug/src/HttpAsyncClient.php create mode 100644 3rdparty/php-http/httplug/src/HttpClient.php create mode 100644 3rdparty/php-http/httplug/src/Promise/HttpFulfilledPromise.php create mode 100644 3rdparty/php-http/httplug/src/Promise/HttpRejectedPromise.php create mode 100644 3rdparty/php-http/promise/LICENSE create mode 100644 3rdparty/php-http/promise/src/FulfilledPromise.php create mode 100644 3rdparty/php-http/promise/src/Promise.php create mode 100644 3rdparty/php-http/promise/src/RejectedPromise.php create mode 100644 3rdparty/php-opencloud/openstack/LICENSE create mode 100644 3rdparty/php-opencloud/openstack/src/BlockStorage/v2/Api.php create mode 100644 3rdparty/php-opencloud/openstack/src/BlockStorage/v2/Models/QuotaSet.php create mode 100644 3rdparty/php-opencloud/openstack/src/BlockStorage/v2/Models/Snapshot.php create mode 100644 3rdparty/php-opencloud/openstack/src/BlockStorage/v2/Models/Volume.php create mode 100644 3rdparty/php-opencloud/openstack/src/BlockStorage/v2/Models/VolumeAttachment.php create mode 100644 3rdparty/php-opencloud/openstack/src/BlockStorage/v2/Models/VolumeType.php create mode 100644 3rdparty/php-opencloud/openstack/src/BlockStorage/v2/Params.php create mode 100644 3rdparty/php-opencloud/openstack/src/BlockStorage/v2/Service.php create mode 100644 3rdparty/php-opencloud/openstack/src/BlockStorage/v3/Api.php create mode 100644 3rdparty/php-opencloud/openstack/src/BlockStorage/v3/Service.php create mode 100644 3rdparty/php-opencloud/openstack/src/Common/Api/AbstractApi.php create mode 100644 3rdparty/php-opencloud/openstack/src/Common/Api/AbstractParams.php create mode 100644 3rdparty/php-opencloud/openstack/src/Common/Api/ApiInterface.php create mode 100644 3rdparty/php-opencloud/openstack/src/Common/Api/Operation.php create mode 100644 3rdparty/php-opencloud/openstack/src/Common/Api/OperatorInterface.php create mode 100644 3rdparty/php-opencloud/openstack/src/Common/Api/OperatorTrait.php create mode 100644 3rdparty/php-opencloud/openstack/src/Common/Api/Parameter.php create mode 100644 3rdparty/php-opencloud/openstack/src/Common/ArrayAccessTrait.php create mode 100644 3rdparty/php-opencloud/openstack/src/Common/Auth/AuthHandler.php create mode 100644 3rdparty/php-opencloud/openstack/src/Common/Auth/Catalog.php create mode 100644 3rdparty/php-opencloud/openstack/src/Common/Auth/IdentityService.php create mode 100644 3rdparty/php-opencloud/openstack/src/Common/Auth/Token.php create mode 100644 3rdparty/php-opencloud/openstack/src/Common/Error/BadResponseError.php create mode 100644 3rdparty/php-opencloud/openstack/src/Common/Error/BaseError.php create mode 100644 3rdparty/php-opencloud/openstack/src/Common/Error/Builder.php create mode 100644 3rdparty/php-opencloud/openstack/src/Common/Error/NotImplementedError.php create mode 100644 3rdparty/php-opencloud/openstack/src/Common/Error/UserInputError.php create mode 100644 3rdparty/php-opencloud/openstack/src/Common/HydratorStrategyTrait.php create mode 100644 3rdparty/php-opencloud/openstack/src/Common/JsonPath.php create mode 100644 3rdparty/php-opencloud/openstack/src/Common/JsonSchema/JsonPatch.php create mode 100644 3rdparty/php-opencloud/openstack/src/Common/JsonSchema/Schema.php create mode 100644 3rdparty/php-opencloud/openstack/src/Common/Resource/AbstractResource.php create mode 100644 3rdparty/php-opencloud/openstack/src/Common/Resource/Alias.php create mode 100644 3rdparty/php-opencloud/openstack/src/Common/Resource/Creatable.php create mode 100644 3rdparty/php-opencloud/openstack/src/Common/Resource/Deletable.php create mode 100644 3rdparty/php-opencloud/openstack/src/Common/Resource/HasMetadata.php create mode 100644 3rdparty/php-opencloud/openstack/src/Common/Resource/HasWaiterTrait.php create mode 100644 3rdparty/php-opencloud/openstack/src/Common/Resource/Iterator.php create mode 100644 3rdparty/php-opencloud/openstack/src/Common/Resource/Listable.php create mode 100644 3rdparty/php-opencloud/openstack/src/Common/Resource/OperatorResource.php create mode 100644 3rdparty/php-opencloud/openstack/src/Common/Resource/ResourceInterface.php create mode 100644 3rdparty/php-opencloud/openstack/src/Common/Resource/Retrievable.php create mode 100644 3rdparty/php-opencloud/openstack/src/Common/Resource/Updateable.php create mode 100644 3rdparty/php-opencloud/openstack/src/Common/Service/AbstractService.php create mode 100644 3rdparty/php-opencloud/openstack/src/Common/Service/Builder.php create mode 100644 3rdparty/php-opencloud/openstack/src/Common/Service/ServiceInterface.php create mode 100644 3rdparty/php-opencloud/openstack/src/Common/Transport/HandlerStack.php create mode 100644 3rdparty/php-opencloud/openstack/src/Common/Transport/HandlerStackFactory.php create mode 100644 3rdparty/php-opencloud/openstack/src/Common/Transport/JsonSerializer.php create mode 100644 3rdparty/php-opencloud/openstack/src/Common/Transport/Middleware.php create mode 100644 3rdparty/php-opencloud/openstack/src/Common/Transport/RequestSerializer.php create mode 100644 3rdparty/php-opencloud/openstack/src/Common/Transport/Serializable.php create mode 100644 3rdparty/php-opencloud/openstack/src/Common/Transport/Utils.php create mode 100644 3rdparty/php-opencloud/openstack/src/Compute/v2/Api.php create mode 100644 3rdparty/php-opencloud/openstack/src/Compute/v2/Enum.php create mode 100644 3rdparty/php-opencloud/openstack/src/Compute/v2/Models/AvailabilityZone.php create mode 100644 3rdparty/php-opencloud/openstack/src/Compute/v2/Models/Fault.php create mode 100644 3rdparty/php-opencloud/openstack/src/Compute/v2/Models/Flavor.php create mode 100644 3rdparty/php-opencloud/openstack/src/Compute/v2/Models/Host.php create mode 100644 3rdparty/php-opencloud/openstack/src/Compute/v2/Models/Hypervisor.php create mode 100644 3rdparty/php-opencloud/openstack/src/Compute/v2/Models/HypervisorStatistic.php create mode 100644 3rdparty/php-opencloud/openstack/src/Compute/v2/Models/Image.php create mode 100644 3rdparty/php-opencloud/openstack/src/Compute/v2/Models/Keypair.php create mode 100644 3rdparty/php-opencloud/openstack/src/Compute/v2/Models/Limit.php create mode 100644 3rdparty/php-opencloud/openstack/src/Compute/v2/Models/QuotaSet.php create mode 100644 3rdparty/php-opencloud/openstack/src/Compute/v2/Models/Server.php create mode 100644 3rdparty/php-opencloud/openstack/src/Compute/v2/Params.php create mode 100644 3rdparty/php-opencloud/openstack/src/Compute/v2/Service.php create mode 100644 3rdparty/php-opencloud/openstack/src/Identity/v2/Api.php create mode 100644 3rdparty/php-opencloud/openstack/src/Identity/v2/Models/Catalog.php create mode 100644 3rdparty/php-opencloud/openstack/src/Identity/v2/Models/Endpoint.php create mode 100644 3rdparty/php-opencloud/openstack/src/Identity/v2/Models/Entry.php create mode 100644 3rdparty/php-opencloud/openstack/src/Identity/v2/Models/Tenant.php create mode 100644 3rdparty/php-opencloud/openstack/src/Identity/v2/Models/Token.php create mode 100644 3rdparty/php-opencloud/openstack/src/Identity/v2/Service.php create mode 100644 3rdparty/php-opencloud/openstack/src/Identity/v3/Api.php create mode 100644 3rdparty/php-opencloud/openstack/src/Identity/v3/Enum.php create mode 100644 3rdparty/php-opencloud/openstack/src/Identity/v3/Models/ApplicationCredential.php create mode 100644 3rdparty/php-opencloud/openstack/src/Identity/v3/Models/Assignment.php create mode 100644 3rdparty/php-opencloud/openstack/src/Identity/v3/Models/Catalog.php create mode 100644 3rdparty/php-opencloud/openstack/src/Identity/v3/Models/Credential.php create mode 100644 3rdparty/php-opencloud/openstack/src/Identity/v3/Models/Domain.php create mode 100644 3rdparty/php-opencloud/openstack/src/Identity/v3/Models/Endpoint.php create mode 100644 3rdparty/php-opencloud/openstack/src/Identity/v3/Models/Group.php create mode 100644 3rdparty/php-opencloud/openstack/src/Identity/v3/Models/Policy.php create mode 100644 3rdparty/php-opencloud/openstack/src/Identity/v3/Models/Project.php create mode 100644 3rdparty/php-opencloud/openstack/src/Identity/v3/Models/Role.php create mode 100644 3rdparty/php-opencloud/openstack/src/Identity/v3/Models/Service.php create mode 100644 3rdparty/php-opencloud/openstack/src/Identity/v3/Models/Token.php create mode 100644 3rdparty/php-opencloud/openstack/src/Identity/v3/Models/User.php create mode 100644 3rdparty/php-opencloud/openstack/src/Identity/v3/Params.php create mode 100644 3rdparty/php-opencloud/openstack/src/Identity/v3/Service.php create mode 100644 3rdparty/php-opencloud/openstack/src/Images/v2/Api.php create mode 100644 3rdparty/php-opencloud/openstack/src/Images/v2/JsonPatch.php create mode 100644 3rdparty/php-opencloud/openstack/src/Images/v2/Models/Image.php create mode 100644 3rdparty/php-opencloud/openstack/src/Images/v2/Models/Member.php create mode 100644 3rdparty/php-opencloud/openstack/src/Images/v2/Models/Schema.php create mode 100644 3rdparty/php-opencloud/openstack/src/Images/v2/Params.php create mode 100644 3rdparty/php-opencloud/openstack/src/Images/v2/Service.php create mode 100644 3rdparty/php-opencloud/openstack/src/Metric/v1/Gnocchi/Api.php create mode 100644 3rdparty/php-opencloud/openstack/src/Metric/v1/Gnocchi/Models/Metric.php create mode 100644 3rdparty/php-opencloud/openstack/src/Metric/v1/Gnocchi/Models/Resource.php create mode 100644 3rdparty/php-opencloud/openstack/src/Metric/v1/Gnocchi/Models/ResourceType.php create mode 100644 3rdparty/php-opencloud/openstack/src/Metric/v1/Gnocchi/Params.php create mode 100644 3rdparty/php-opencloud/openstack/src/Metric/v1/Gnocchi/Service.php create mode 100644 3rdparty/php-opencloud/openstack/src/Networking/v2/Api.php create mode 100644 3rdparty/php-opencloud/openstack/src/Networking/v2/Extensions/Layer3/Api.php create mode 100644 3rdparty/php-opencloud/openstack/src/Networking/v2/Extensions/Layer3/ApiTrait.php create mode 100644 3rdparty/php-opencloud/openstack/src/Networking/v2/Extensions/Layer3/Models/FixedIp.php create mode 100644 3rdparty/php-opencloud/openstack/src/Networking/v2/Extensions/Layer3/Models/FloatingIp.php create mode 100644 3rdparty/php-opencloud/openstack/src/Networking/v2/Extensions/Layer3/Models/GatewayInfo.php create mode 100644 3rdparty/php-opencloud/openstack/src/Networking/v2/Extensions/Layer3/Models/Router.php create mode 100644 3rdparty/php-opencloud/openstack/src/Networking/v2/Extensions/Layer3/Params.php create mode 100644 3rdparty/php-opencloud/openstack/src/Networking/v2/Extensions/Layer3/ParamsTrait.php create mode 100644 3rdparty/php-opencloud/openstack/src/Networking/v2/Extensions/Layer3/Service.php create mode 100644 3rdparty/php-opencloud/openstack/src/Networking/v2/Extensions/Layer3/ServiceTrait.php create mode 100644 3rdparty/php-opencloud/openstack/src/Networking/v2/Extensions/SecurityGroups/Api.php create mode 100644 3rdparty/php-opencloud/openstack/src/Networking/v2/Extensions/SecurityGroups/ApiTrait.php create mode 100644 3rdparty/php-opencloud/openstack/src/Networking/v2/Extensions/SecurityGroups/Models/SecurityGroup.php create mode 100644 3rdparty/php-opencloud/openstack/src/Networking/v2/Extensions/SecurityGroups/Models/SecurityGroupRule.php create mode 100644 3rdparty/php-opencloud/openstack/src/Networking/v2/Extensions/SecurityGroups/Params.php create mode 100644 3rdparty/php-opencloud/openstack/src/Networking/v2/Extensions/SecurityGroups/ParamsTrait.php create mode 100644 3rdparty/php-opencloud/openstack/src/Networking/v2/Extensions/SecurityGroups/Service.php create mode 100644 3rdparty/php-opencloud/openstack/src/Networking/v2/Extensions/SecurityGroups/ServiceTrait.php create mode 100644 3rdparty/php-opencloud/openstack/src/Networking/v2/Models/InterfaceAttachment.php create mode 100644 3rdparty/php-opencloud/openstack/src/Networking/v2/Models/LoadBalancer.php create mode 100644 3rdparty/php-opencloud/openstack/src/Networking/v2/Models/LoadBalancerHealthMonitor.php create mode 100644 3rdparty/php-opencloud/openstack/src/Networking/v2/Models/LoadBalancerListener.php create mode 100644 3rdparty/php-opencloud/openstack/src/Networking/v2/Models/LoadBalancerMember.php create mode 100644 3rdparty/php-opencloud/openstack/src/Networking/v2/Models/LoadBalancerPool.php create mode 100644 3rdparty/php-opencloud/openstack/src/Networking/v2/Models/LoadBalancerStat.php create mode 100644 3rdparty/php-opencloud/openstack/src/Networking/v2/Models/LoadBalancerStatus.php create mode 100644 3rdparty/php-opencloud/openstack/src/Networking/v2/Models/Network.php create mode 100644 3rdparty/php-opencloud/openstack/src/Networking/v2/Models/Port.php create mode 100644 3rdparty/php-opencloud/openstack/src/Networking/v2/Models/Quota.php create mode 100644 3rdparty/php-opencloud/openstack/src/Networking/v2/Models/Subnet.php create mode 100644 3rdparty/php-opencloud/openstack/src/Networking/v2/Params.php create mode 100644 3rdparty/php-opencloud/openstack/src/Networking/v2/Service.php create mode 100644 3rdparty/php-opencloud/openstack/src/ObjectStore/v1/Api.php create mode 100644 3rdparty/php-opencloud/openstack/src/ObjectStore/v1/Models/Account.php create mode 100644 3rdparty/php-opencloud/openstack/src/ObjectStore/v1/Models/Container.php create mode 100644 3rdparty/php-opencloud/openstack/src/ObjectStore/v1/Models/MetadataTrait.php create mode 100644 3rdparty/php-opencloud/openstack/src/ObjectStore/v1/Models/StorageObject.php create mode 100644 3rdparty/php-opencloud/openstack/src/ObjectStore/v1/Params.php create mode 100644 3rdparty/php-opencloud/openstack/src/ObjectStore/v1/Service.php create mode 100644 3rdparty/php-opencloud/openstack/src/OpenStack.php create mode 100644 3rdparty/phpseclib/phpseclib/AUTHORS create mode 100644 3rdparty/phpseclib/phpseclib/LICENSE create mode 100644 3rdparty/phpseclib/phpseclib/phpseclib/Crypt/AES.php create mode 100644 3rdparty/phpseclib/phpseclib/phpseclib/Crypt/Base.php create mode 100644 3rdparty/phpseclib/phpseclib/phpseclib/Crypt/Blowfish.php create mode 100644 3rdparty/phpseclib/phpseclib/phpseclib/Crypt/DES.php create mode 100644 3rdparty/phpseclib/phpseclib/phpseclib/Crypt/Hash.php create mode 100644 3rdparty/phpseclib/phpseclib/phpseclib/Crypt/RC2.php create mode 100644 3rdparty/phpseclib/phpseclib/phpseclib/Crypt/RC4.php create mode 100644 3rdparty/phpseclib/phpseclib/phpseclib/Crypt/RSA.php create mode 100644 3rdparty/phpseclib/phpseclib/phpseclib/Crypt/Random.php create mode 100644 3rdparty/phpseclib/phpseclib/phpseclib/Crypt/Rijndael.php create mode 100644 3rdparty/phpseclib/phpseclib/phpseclib/Crypt/TripleDES.php create mode 100644 3rdparty/phpseclib/phpseclib/phpseclib/Crypt/Twofish.php create mode 100644 3rdparty/phpseclib/phpseclib/phpseclib/File/ANSI.php create mode 100644 3rdparty/phpseclib/phpseclib/phpseclib/File/ASN1.php create mode 100644 3rdparty/phpseclib/phpseclib/phpseclib/File/ASN1/Element.php create mode 100644 3rdparty/phpseclib/phpseclib/phpseclib/File/X509.php create mode 100644 3rdparty/phpseclib/phpseclib/phpseclib/Math/BigInteger.php create mode 100644 3rdparty/phpseclib/phpseclib/phpseclib/Net/SCP.php create mode 100644 3rdparty/phpseclib/phpseclib/phpseclib/Net/SFTP.php create mode 100644 3rdparty/phpseclib/phpseclib/phpseclib/Net/SFTP/Stream.php create mode 100644 3rdparty/phpseclib/phpseclib/phpseclib/Net/SSH1.php create mode 100644 3rdparty/phpseclib/phpseclib/phpseclib/Net/SSH2.php create mode 100644 3rdparty/phpseclib/phpseclib/phpseclib/System/SSH/Agent.php create mode 100644 3rdparty/phpseclib/phpseclib/phpseclib/System/SSH/Agent/Identity.php create mode 100644 3rdparty/phpseclib/phpseclib/phpseclib/bootstrap.php create mode 100644 3rdparty/phpseclib/phpseclib/phpseclib/openssl.cnf create mode 100644 3rdparty/pimple/pimple/LICENSE create mode 100644 3rdparty/pimple/pimple/src/Pimple/Container.php create mode 100644 3rdparty/pimple/pimple/src/Pimple/Exception/ExpectedInvokableException.php create mode 100644 3rdparty/pimple/pimple/src/Pimple/Exception/FrozenServiceException.php create mode 100644 3rdparty/pimple/pimple/src/Pimple/Exception/InvalidServiceIdentifierException.php create mode 100644 3rdparty/pimple/pimple/src/Pimple/Exception/UnknownIdentifierException.php create mode 100644 3rdparty/pimple/pimple/src/Pimple/Psr11/Container.php create mode 100644 3rdparty/pimple/pimple/src/Pimple/Psr11/ServiceLocator.php create mode 100644 3rdparty/pimple/pimple/src/Pimple/ServiceIterator.php create mode 100644 3rdparty/pimple/pimple/src/Pimple/ServiceProviderInterface.php create mode 100644 3rdparty/psr/cache/LICENSE.txt create mode 100644 3rdparty/psr/cache/src/CacheException.php create mode 100644 3rdparty/psr/cache/src/CacheItemInterface.php create mode 100644 3rdparty/psr/cache/src/CacheItemPoolInterface.php create mode 100644 3rdparty/psr/cache/src/InvalidArgumentException.php create mode 100644 3rdparty/psr/clock/LICENSE create mode 100644 3rdparty/psr/clock/src/ClockInterface.php create mode 100644 3rdparty/psr/container/LICENSE create mode 100644 3rdparty/psr/container/src/ContainerExceptionInterface.php create mode 100644 3rdparty/psr/container/src/ContainerInterface.php create mode 100644 3rdparty/psr/container/src/NotFoundExceptionInterface.php create mode 100644 3rdparty/psr/event-dispatcher/LICENSE create mode 100644 3rdparty/psr/event-dispatcher/src/EventDispatcherInterface.php create mode 100644 3rdparty/psr/event-dispatcher/src/ListenerProviderInterface.php create mode 100644 3rdparty/psr/event-dispatcher/src/StoppableEventInterface.php create mode 100644 3rdparty/psr/http-client/LICENSE create mode 100644 3rdparty/psr/http-client/src/ClientExceptionInterface.php create mode 100644 3rdparty/psr/http-client/src/ClientInterface.php create mode 100644 3rdparty/psr/http-client/src/NetworkExceptionInterface.php create mode 100644 3rdparty/psr/http-client/src/RequestExceptionInterface.php create mode 100644 3rdparty/psr/http-factory/LICENSE create mode 100644 3rdparty/psr/http-factory/src/RequestFactoryInterface.php create mode 100644 3rdparty/psr/http-factory/src/ResponseFactoryInterface.php create mode 100644 3rdparty/psr/http-factory/src/ServerRequestFactoryInterface.php create mode 100644 3rdparty/psr/http-factory/src/StreamFactoryInterface.php create mode 100644 3rdparty/psr/http-factory/src/UploadedFileFactoryInterface.php create mode 100644 3rdparty/psr/http-factory/src/UriFactoryInterface.php create mode 100644 3rdparty/psr/http-message/LICENSE create mode 100644 3rdparty/psr/http-message/src/MessageInterface.php create mode 100644 3rdparty/psr/http-message/src/RequestInterface.php create mode 100644 3rdparty/psr/http-message/src/ResponseInterface.php create mode 100644 3rdparty/psr/http-message/src/ServerRequestInterface.php create mode 100644 3rdparty/psr/http-message/src/StreamInterface.php create mode 100644 3rdparty/psr/http-message/src/UploadedFileInterface.php create mode 100644 3rdparty/psr/http-message/src/UriInterface.php create mode 100644 3rdparty/psr/log/LICENSE create mode 100644 3rdparty/psr/log/src/AbstractLogger.php create mode 100644 3rdparty/psr/log/src/InvalidArgumentException.php create mode 100644 3rdparty/psr/log/src/LogLevel.php create mode 100644 3rdparty/psr/log/src/LoggerAwareInterface.php create mode 100644 3rdparty/psr/log/src/LoggerAwareTrait.php create mode 100644 3rdparty/psr/log/src/LoggerInterface.php create mode 100644 3rdparty/psr/log/src/LoggerTrait.php create mode 100644 3rdparty/psr/log/src/NullLogger.php create mode 100644 3rdparty/punic/punic/LIBPHONENUMBER-LICENSE.txt create mode 100644 3rdparty/punic/punic/LICENSE.txt create mode 100644 3rdparty/punic/punic/UNICODE-LICENSE.txt create mode 100644 3rdparty/punic/punic/punic.php create mode 100644 3rdparty/punic/punic/src/Calendar.php create mode 100644 3rdparty/punic/punic/src/Comparer.php create mode 100644 3rdparty/punic/punic/src/Currency.php create mode 100644 3rdparty/punic/punic/src/Data.php create mode 100644 3rdparty/punic/punic/src/Exception.php create mode 100644 3rdparty/punic/punic/src/Exception/BadArgumentType.php create mode 100644 3rdparty/punic/punic/src/Exception/BadDataFileContents.php create mode 100644 3rdparty/punic/punic/src/Exception/DataFileNotFound.php create mode 100644 3rdparty/punic/punic/src/Exception/DataFileNotReadable.php create mode 100644 3rdparty/punic/punic/src/Exception/DataFolderNotFound.php create mode 100644 3rdparty/punic/punic/src/Exception/InvalidDataFile.php create mode 100644 3rdparty/punic/punic/src/Exception/InvalidLocale.php create mode 100644 3rdparty/punic/punic/src/Exception/InvalidOverride.php create mode 100644 3rdparty/punic/punic/src/Exception/NotImplemented.php create mode 100644 3rdparty/punic/punic/src/Exception/ValueNotInList.php create mode 100644 3rdparty/punic/punic/src/Language.php create mode 100644 3rdparty/punic/punic/src/Misc.php create mode 100644 3rdparty/punic/punic/src/Number.php create mode 100644 3rdparty/punic/punic/src/Phone.php create mode 100644 3rdparty/punic/punic/src/Plural.php create mode 100644 3rdparty/punic/punic/src/Script.php create mode 100644 3rdparty/punic/punic/src/Territory.php create mode 100644 3rdparty/punic/punic/src/Unit.php create mode 100644 3rdparty/ralouphie/getallheaders/LICENSE create mode 100644 3rdparty/ralouphie/getallheaders/src/getallheaders.php create mode 100644 3rdparty/sabre/dav/LICENSE create mode 100644 3rdparty/sabre/dav/lib/CalDAV/Backend/AbstractBackend.php create mode 100644 3rdparty/sabre/dav/lib/CalDAV/Backend/BackendInterface.php create mode 100644 3rdparty/sabre/dav/lib/CalDAV/Backend/NotificationSupport.php create mode 100644 3rdparty/sabre/dav/lib/CalDAV/Backend/PDO.php create mode 100644 3rdparty/sabre/dav/lib/CalDAV/Backend/SchedulingSupport.php create mode 100644 3rdparty/sabre/dav/lib/CalDAV/Backend/SharingSupport.php create mode 100644 3rdparty/sabre/dav/lib/CalDAV/Backend/SimplePDO.php create mode 100644 3rdparty/sabre/dav/lib/CalDAV/Backend/SubscriptionSupport.php create mode 100644 3rdparty/sabre/dav/lib/CalDAV/Backend/SyncSupport.php create mode 100644 3rdparty/sabre/dav/lib/CalDAV/Calendar.php create mode 100644 3rdparty/sabre/dav/lib/CalDAV/CalendarHome.php create mode 100644 3rdparty/sabre/dav/lib/CalDAV/CalendarObject.php create mode 100644 3rdparty/sabre/dav/lib/CalDAV/CalendarQueryValidator.php create mode 100644 3rdparty/sabre/dav/lib/CalDAV/CalendarRoot.php create mode 100644 3rdparty/sabre/dav/lib/CalDAV/Exception/InvalidComponentType.php create mode 100644 3rdparty/sabre/dav/lib/CalDAV/ICSExportPlugin.php create mode 100644 3rdparty/sabre/dav/lib/CalDAV/ICalendar.php create mode 100644 3rdparty/sabre/dav/lib/CalDAV/ICalendarObject.php create mode 100644 3rdparty/sabre/dav/lib/CalDAV/ICalendarObjectContainer.php create mode 100644 3rdparty/sabre/dav/lib/CalDAV/ISharedCalendar.php create mode 100644 3rdparty/sabre/dav/lib/CalDAV/Notifications/Collection.php create mode 100644 3rdparty/sabre/dav/lib/CalDAV/Notifications/ICollection.php create mode 100644 3rdparty/sabre/dav/lib/CalDAV/Notifications/INode.php create mode 100644 3rdparty/sabre/dav/lib/CalDAV/Notifications/Node.php create mode 100644 3rdparty/sabre/dav/lib/CalDAV/Notifications/Plugin.php create mode 100644 3rdparty/sabre/dav/lib/CalDAV/Plugin.php create mode 100644 3rdparty/sabre/dav/lib/CalDAV/Principal/Collection.php create mode 100644 3rdparty/sabre/dav/lib/CalDAV/Principal/IProxyRead.php create mode 100644 3rdparty/sabre/dav/lib/CalDAV/Principal/IProxyWrite.php create mode 100644 3rdparty/sabre/dav/lib/CalDAV/Principal/ProxyRead.php create mode 100644 3rdparty/sabre/dav/lib/CalDAV/Principal/ProxyWrite.php create mode 100644 3rdparty/sabre/dav/lib/CalDAV/Principal/User.php create mode 100644 3rdparty/sabre/dav/lib/CalDAV/Schedule/IInbox.php create mode 100644 3rdparty/sabre/dav/lib/CalDAV/Schedule/IMipPlugin.php create mode 100644 3rdparty/sabre/dav/lib/CalDAV/Schedule/IOutbox.php create mode 100644 3rdparty/sabre/dav/lib/CalDAV/Schedule/ISchedulingObject.php create mode 100644 3rdparty/sabre/dav/lib/CalDAV/Schedule/Inbox.php create mode 100644 3rdparty/sabre/dav/lib/CalDAV/Schedule/Outbox.php create mode 100644 3rdparty/sabre/dav/lib/CalDAV/Schedule/Plugin.php create mode 100644 3rdparty/sabre/dav/lib/CalDAV/Schedule/SchedulingObject.php create mode 100644 3rdparty/sabre/dav/lib/CalDAV/SharedCalendar.php create mode 100644 3rdparty/sabre/dav/lib/CalDAV/SharingPlugin.php create mode 100644 3rdparty/sabre/dav/lib/CalDAV/Subscriptions/ISubscription.php create mode 100644 3rdparty/sabre/dav/lib/CalDAV/Subscriptions/Plugin.php create mode 100644 3rdparty/sabre/dav/lib/CalDAV/Subscriptions/Subscription.php create mode 100644 3rdparty/sabre/dav/lib/CalDAV/Xml/Filter/CalendarData.php create mode 100644 3rdparty/sabre/dav/lib/CalDAV/Xml/Filter/CompFilter.php create mode 100644 3rdparty/sabre/dav/lib/CalDAV/Xml/Filter/ParamFilter.php create mode 100644 3rdparty/sabre/dav/lib/CalDAV/Xml/Filter/PropFilter.php create mode 100644 3rdparty/sabre/dav/lib/CalDAV/Xml/Notification/Invite.php create mode 100644 3rdparty/sabre/dav/lib/CalDAV/Xml/Notification/InviteReply.php create mode 100644 3rdparty/sabre/dav/lib/CalDAV/Xml/Notification/NotificationInterface.php create mode 100644 3rdparty/sabre/dav/lib/CalDAV/Xml/Notification/SystemStatus.php create mode 100644 3rdparty/sabre/dav/lib/CalDAV/Xml/Property/AllowedSharingModes.php create mode 100644 3rdparty/sabre/dav/lib/CalDAV/Xml/Property/EmailAddressSet.php create mode 100644 3rdparty/sabre/dav/lib/CalDAV/Xml/Property/Invite.php create mode 100644 3rdparty/sabre/dav/lib/CalDAV/Xml/Property/ScheduleCalendarTransp.php create mode 100644 3rdparty/sabre/dav/lib/CalDAV/Xml/Property/SupportedCalendarComponentSet.php create mode 100644 3rdparty/sabre/dav/lib/CalDAV/Xml/Property/SupportedCalendarData.php create mode 100644 3rdparty/sabre/dav/lib/CalDAV/Xml/Property/SupportedCollationSet.php create mode 100644 3rdparty/sabre/dav/lib/CalDAV/Xml/Request/CalendarMultiGetReport.php create mode 100644 3rdparty/sabre/dav/lib/CalDAV/Xml/Request/CalendarQueryReport.php create mode 100644 3rdparty/sabre/dav/lib/CalDAV/Xml/Request/FreeBusyQueryReport.php create mode 100644 3rdparty/sabre/dav/lib/CalDAV/Xml/Request/InviteReply.php create mode 100644 3rdparty/sabre/dav/lib/CalDAV/Xml/Request/MkCalendar.php create mode 100644 3rdparty/sabre/dav/lib/CalDAV/Xml/Request/Share.php create mode 100644 3rdparty/sabre/dav/lib/CardDAV/AddressBook.php create mode 100644 3rdparty/sabre/dav/lib/CardDAV/AddressBookHome.php create mode 100644 3rdparty/sabre/dav/lib/CardDAV/AddressBookRoot.php create mode 100644 3rdparty/sabre/dav/lib/CardDAV/Backend/AbstractBackend.php create mode 100644 3rdparty/sabre/dav/lib/CardDAV/Backend/BackendInterface.php create mode 100644 3rdparty/sabre/dav/lib/CardDAV/Backend/PDO.php create mode 100644 3rdparty/sabre/dav/lib/CardDAV/Backend/SyncSupport.php create mode 100644 3rdparty/sabre/dav/lib/CardDAV/Card.php create mode 100644 3rdparty/sabre/dav/lib/CardDAV/IAddressBook.php create mode 100644 3rdparty/sabre/dav/lib/CardDAV/ICard.php create mode 100644 3rdparty/sabre/dav/lib/CardDAV/IDirectory.php create mode 100644 3rdparty/sabre/dav/lib/CardDAV/Plugin.php create mode 100644 3rdparty/sabre/dav/lib/CardDAV/VCFExportPlugin.php create mode 100644 3rdparty/sabre/dav/lib/CardDAV/Xml/Filter/AddressData.php create mode 100644 3rdparty/sabre/dav/lib/CardDAV/Xml/Filter/ParamFilter.php create mode 100644 3rdparty/sabre/dav/lib/CardDAV/Xml/Filter/PropFilter.php create mode 100644 3rdparty/sabre/dav/lib/CardDAV/Xml/Property/SupportedAddressData.php create mode 100644 3rdparty/sabre/dav/lib/CardDAV/Xml/Property/SupportedCollationSet.php create mode 100644 3rdparty/sabre/dav/lib/CardDAV/Xml/Request/AddressBookMultiGetReport.php create mode 100644 3rdparty/sabre/dav/lib/CardDAV/Xml/Request/AddressBookQueryReport.php create mode 100644 3rdparty/sabre/dav/lib/DAV/Auth/Backend/AbstractBasic.php create mode 100644 3rdparty/sabre/dav/lib/DAV/Auth/Backend/AbstractBearer.php create mode 100644 3rdparty/sabre/dav/lib/DAV/Auth/Backend/AbstractDigest.php create mode 100644 3rdparty/sabre/dav/lib/DAV/Auth/Backend/Apache.php create mode 100644 3rdparty/sabre/dav/lib/DAV/Auth/Backend/BackendInterface.php create mode 100644 3rdparty/sabre/dav/lib/DAV/Auth/Backend/BasicCallBack.php create mode 100644 3rdparty/sabre/dav/lib/DAV/Auth/Backend/File.php create mode 100644 3rdparty/sabre/dav/lib/DAV/Auth/Backend/IMAP.php create mode 100644 3rdparty/sabre/dav/lib/DAV/Auth/Backend/PDO.php create mode 100644 3rdparty/sabre/dav/lib/DAV/Auth/Backend/PDOBasicAuth.php create mode 100644 3rdparty/sabre/dav/lib/DAV/Auth/Plugin.php create mode 100644 3rdparty/sabre/dav/lib/DAV/Browser/GuessContentType.php create mode 100644 3rdparty/sabre/dav/lib/DAV/Browser/HtmlOutput.php create mode 100644 3rdparty/sabre/dav/lib/DAV/Browser/HtmlOutputHelper.php create mode 100644 3rdparty/sabre/dav/lib/DAV/Browser/MapGetToPropFind.php create mode 100644 3rdparty/sabre/dav/lib/DAV/Browser/Plugin.php create mode 100644 3rdparty/sabre/dav/lib/DAV/Browser/PropFindAll.php create mode 100644 3rdparty/sabre/dav/lib/DAV/Browser/assets/favicon.ico create mode 100644 3rdparty/sabre/dav/lib/DAV/Browser/assets/openiconic/ICON-LICENSE create mode 100644 3rdparty/sabre/dav/lib/DAV/Browser/assets/openiconic/open-iconic.css create mode 100644 3rdparty/sabre/dav/lib/DAV/Browser/assets/openiconic/open-iconic.eot create mode 100644 3rdparty/sabre/dav/lib/DAV/Browser/assets/openiconic/open-iconic.otf create mode 100644 3rdparty/sabre/dav/lib/DAV/Browser/assets/openiconic/open-iconic.svg create mode 100644 3rdparty/sabre/dav/lib/DAV/Browser/assets/openiconic/open-iconic.ttf create mode 100644 3rdparty/sabre/dav/lib/DAV/Browser/assets/openiconic/open-iconic.woff create mode 100644 3rdparty/sabre/dav/lib/DAV/Browser/assets/sabredav.css create mode 100644 3rdparty/sabre/dav/lib/DAV/Browser/assets/sabredav.png create mode 100644 3rdparty/sabre/dav/lib/DAV/Client.php create mode 100644 3rdparty/sabre/dav/lib/DAV/Collection.php create mode 100644 3rdparty/sabre/dav/lib/DAV/CorePlugin.php create mode 100644 3rdparty/sabre/dav/lib/DAV/Exception.php create mode 100644 3rdparty/sabre/dav/lib/DAV/Exception/BadRequest.php create mode 100644 3rdparty/sabre/dav/lib/DAV/Exception/Conflict.php create mode 100644 3rdparty/sabre/dav/lib/DAV/Exception/ConflictingLock.php create mode 100644 3rdparty/sabre/dav/lib/DAV/Exception/Forbidden.php create mode 100644 3rdparty/sabre/dav/lib/DAV/Exception/InsufficientStorage.php create mode 100644 3rdparty/sabre/dav/lib/DAV/Exception/InvalidResourceType.php create mode 100644 3rdparty/sabre/dav/lib/DAV/Exception/InvalidSyncToken.php create mode 100644 3rdparty/sabre/dav/lib/DAV/Exception/LengthRequired.php create mode 100644 3rdparty/sabre/dav/lib/DAV/Exception/LockTokenMatchesRequestUri.php create mode 100644 3rdparty/sabre/dav/lib/DAV/Exception/Locked.php create mode 100644 3rdparty/sabre/dav/lib/DAV/Exception/MethodNotAllowed.php create mode 100644 3rdparty/sabre/dav/lib/DAV/Exception/NotAuthenticated.php create mode 100644 3rdparty/sabre/dav/lib/DAV/Exception/NotFound.php create mode 100644 3rdparty/sabre/dav/lib/DAV/Exception/NotImplemented.php create mode 100644 3rdparty/sabre/dav/lib/DAV/Exception/PaymentRequired.php create mode 100644 3rdparty/sabre/dav/lib/DAV/Exception/PreconditionFailed.php create mode 100644 3rdparty/sabre/dav/lib/DAV/Exception/ReportNotSupported.php create mode 100644 3rdparty/sabre/dav/lib/DAV/Exception/RequestedRangeNotSatisfiable.php create mode 100644 3rdparty/sabre/dav/lib/DAV/Exception/ServiceUnavailable.php create mode 100644 3rdparty/sabre/dav/lib/DAV/Exception/TooManyMatches.php create mode 100644 3rdparty/sabre/dav/lib/DAV/Exception/UnsupportedMediaType.php create mode 100644 3rdparty/sabre/dav/lib/DAV/FS/Directory.php create mode 100644 3rdparty/sabre/dav/lib/DAV/FS/File.php create mode 100644 3rdparty/sabre/dav/lib/DAV/FS/Node.php create mode 100644 3rdparty/sabre/dav/lib/DAV/FSExt/Directory.php create mode 100644 3rdparty/sabre/dav/lib/DAV/FSExt/File.php create mode 100644 3rdparty/sabre/dav/lib/DAV/File.php create mode 100644 3rdparty/sabre/dav/lib/DAV/ICollection.php create mode 100644 3rdparty/sabre/dav/lib/DAV/ICopyTarget.php create mode 100644 3rdparty/sabre/dav/lib/DAV/IExtendedCollection.php create mode 100644 3rdparty/sabre/dav/lib/DAV/IFile.php create mode 100644 3rdparty/sabre/dav/lib/DAV/IMoveTarget.php create mode 100644 3rdparty/sabre/dav/lib/DAV/IMultiGet.php create mode 100644 3rdparty/sabre/dav/lib/DAV/INode.php create mode 100644 3rdparty/sabre/dav/lib/DAV/INodeByPath.php create mode 100644 3rdparty/sabre/dav/lib/DAV/IProperties.php create mode 100644 3rdparty/sabre/dav/lib/DAV/IQuota.php create mode 100644 3rdparty/sabre/dav/lib/DAV/Locks/Backend/AbstractBackend.php create mode 100644 3rdparty/sabre/dav/lib/DAV/Locks/Backend/BackendInterface.php create mode 100644 3rdparty/sabre/dav/lib/DAV/Locks/Backend/File.php create mode 100644 3rdparty/sabre/dav/lib/DAV/Locks/Backend/PDO.php create mode 100644 3rdparty/sabre/dav/lib/DAV/Locks/LockInfo.php create mode 100644 3rdparty/sabre/dav/lib/DAV/Locks/Plugin.php create mode 100644 3rdparty/sabre/dav/lib/DAV/MkCol.php create mode 100644 3rdparty/sabre/dav/lib/DAV/Mount/Plugin.php create mode 100644 3rdparty/sabre/dav/lib/DAV/Node.php create mode 100644 3rdparty/sabre/dav/lib/DAV/PartialUpdate/IPatchSupport.php create mode 100644 3rdparty/sabre/dav/lib/DAV/PartialUpdate/Plugin.php create mode 100644 3rdparty/sabre/dav/lib/DAV/PropFind.php create mode 100644 3rdparty/sabre/dav/lib/DAV/PropPatch.php create mode 100644 3rdparty/sabre/dav/lib/DAV/PropertyStorage/Backend/BackendInterface.php create mode 100644 3rdparty/sabre/dav/lib/DAV/PropertyStorage/Backend/PDO.php create mode 100644 3rdparty/sabre/dav/lib/DAV/PropertyStorage/Plugin.php create mode 100644 3rdparty/sabre/dav/lib/DAV/Server.php create mode 100644 3rdparty/sabre/dav/lib/DAV/ServerPlugin.php create mode 100644 3rdparty/sabre/dav/lib/DAV/Sharing/ISharedNode.php create mode 100644 3rdparty/sabre/dav/lib/DAV/Sharing/Plugin.php create mode 100644 3rdparty/sabre/dav/lib/DAV/SimpleCollection.php create mode 100644 3rdparty/sabre/dav/lib/DAV/SimpleFile.php create mode 100644 3rdparty/sabre/dav/lib/DAV/StringUtil.php create mode 100644 3rdparty/sabre/dav/lib/DAV/Sync/ISyncCollection.php create mode 100644 3rdparty/sabre/dav/lib/DAV/Sync/Plugin.php create mode 100644 3rdparty/sabre/dav/lib/DAV/TemporaryFileFilterPlugin.php create mode 100644 3rdparty/sabre/dav/lib/DAV/Tree.php create mode 100644 3rdparty/sabre/dav/lib/DAV/UUIDUtil.php create mode 100644 3rdparty/sabre/dav/lib/DAV/Version.php create mode 100644 3rdparty/sabre/dav/lib/DAV/Xml/Element/Prop.php create mode 100644 3rdparty/sabre/dav/lib/DAV/Xml/Element/Response.php create mode 100644 3rdparty/sabre/dav/lib/DAV/Xml/Element/Sharee.php create mode 100644 3rdparty/sabre/dav/lib/DAV/Xml/Property/Complex.php create mode 100644 3rdparty/sabre/dav/lib/DAV/Xml/Property/GetLastModified.php create mode 100644 3rdparty/sabre/dav/lib/DAV/Xml/Property/Href.php create mode 100644 3rdparty/sabre/dav/lib/DAV/Xml/Property/Invite.php create mode 100644 3rdparty/sabre/dav/lib/DAV/Xml/Property/LocalHref.php create mode 100644 3rdparty/sabre/dav/lib/DAV/Xml/Property/LockDiscovery.php create mode 100644 3rdparty/sabre/dav/lib/DAV/Xml/Property/ResourceType.php create mode 100644 3rdparty/sabre/dav/lib/DAV/Xml/Property/ShareAccess.php create mode 100644 3rdparty/sabre/dav/lib/DAV/Xml/Property/SupportedLock.php create mode 100644 3rdparty/sabre/dav/lib/DAV/Xml/Property/SupportedMethodSet.php create mode 100644 3rdparty/sabre/dav/lib/DAV/Xml/Property/SupportedReportSet.php create mode 100644 3rdparty/sabre/dav/lib/DAV/Xml/Request/Lock.php create mode 100644 3rdparty/sabre/dav/lib/DAV/Xml/Request/MkCol.php create mode 100644 3rdparty/sabre/dav/lib/DAV/Xml/Request/PropFind.php create mode 100644 3rdparty/sabre/dav/lib/DAV/Xml/Request/PropPatch.php create mode 100644 3rdparty/sabre/dav/lib/DAV/Xml/Request/ShareResource.php create mode 100644 3rdparty/sabre/dav/lib/DAV/Xml/Request/SyncCollectionReport.php create mode 100644 3rdparty/sabre/dav/lib/DAV/Xml/Response/MultiStatus.php create mode 100644 3rdparty/sabre/dav/lib/DAV/Xml/Service.php create mode 100644 3rdparty/sabre/dav/lib/DAVACL/ACLTrait.php create mode 100644 3rdparty/sabre/dav/lib/DAVACL/AbstractPrincipalCollection.php create mode 100644 3rdparty/sabre/dav/lib/DAVACL/Exception/AceConflict.php create mode 100644 3rdparty/sabre/dav/lib/DAVACL/Exception/NeedPrivileges.php create mode 100644 3rdparty/sabre/dav/lib/DAVACL/Exception/NoAbstract.php create mode 100644 3rdparty/sabre/dav/lib/DAVACL/Exception/NotRecognizedPrincipal.php create mode 100644 3rdparty/sabre/dav/lib/DAVACL/Exception/NotSupportedPrivilege.php create mode 100644 3rdparty/sabre/dav/lib/DAVACL/FS/Collection.php create mode 100644 3rdparty/sabre/dav/lib/DAVACL/FS/File.php create mode 100644 3rdparty/sabre/dav/lib/DAVACL/FS/HomeCollection.php create mode 100644 3rdparty/sabre/dav/lib/DAVACL/IACL.php create mode 100644 3rdparty/sabre/dav/lib/DAVACL/IPrincipal.php create mode 100644 3rdparty/sabre/dav/lib/DAVACL/IPrincipalCollection.php create mode 100644 3rdparty/sabre/dav/lib/DAVACL/Plugin.php create mode 100644 3rdparty/sabre/dav/lib/DAVACL/Principal.php create mode 100644 3rdparty/sabre/dav/lib/DAVACL/PrincipalBackend/AbstractBackend.php create mode 100644 3rdparty/sabre/dav/lib/DAVACL/PrincipalBackend/BackendInterface.php create mode 100644 3rdparty/sabre/dav/lib/DAVACL/PrincipalBackend/CreatePrincipalSupport.php create mode 100644 3rdparty/sabre/dav/lib/DAVACL/PrincipalBackend/PDO.php create mode 100644 3rdparty/sabre/dav/lib/DAVACL/PrincipalCollection.php create mode 100644 3rdparty/sabre/dav/lib/DAVACL/Xml/Property/Acl.php create mode 100644 3rdparty/sabre/dav/lib/DAVACL/Xml/Property/AclRestrictions.php create mode 100644 3rdparty/sabre/dav/lib/DAVACL/Xml/Property/CurrentUserPrivilegeSet.php create mode 100644 3rdparty/sabre/dav/lib/DAVACL/Xml/Property/Principal.php create mode 100644 3rdparty/sabre/dav/lib/DAVACL/Xml/Property/SupportedPrivilegeSet.php create mode 100644 3rdparty/sabre/dav/lib/DAVACL/Xml/Request/AclPrincipalPropSetReport.php create mode 100644 3rdparty/sabre/dav/lib/DAVACL/Xml/Request/ExpandPropertyReport.php create mode 100644 3rdparty/sabre/dav/lib/DAVACL/Xml/Request/PrincipalMatchReport.php create mode 100644 3rdparty/sabre/dav/lib/DAVACL/Xml/Request/PrincipalPropertySearchReport.php create mode 100644 3rdparty/sabre/dav/lib/DAVACL/Xml/Request/PrincipalSearchPropertySetReport.php create mode 100644 3rdparty/sabre/event/LICENSE create mode 100644 3rdparty/sabre/event/lib/Emitter.php create mode 100644 3rdparty/sabre/event/lib/EmitterInterface.php create mode 100644 3rdparty/sabre/event/lib/EmitterTrait.php create mode 100644 3rdparty/sabre/event/lib/EventEmitter.php create mode 100644 3rdparty/sabre/event/lib/Loop/Loop.php create mode 100644 3rdparty/sabre/event/lib/Loop/functions.php create mode 100644 3rdparty/sabre/event/lib/Promise.php create mode 100644 3rdparty/sabre/event/lib/Promise/functions.php create mode 100644 3rdparty/sabre/event/lib/PromiseAlreadyResolvedException.php create mode 100644 3rdparty/sabre/event/lib/Version.php create mode 100644 3rdparty/sabre/event/lib/WildcardEmitter.php create mode 100644 3rdparty/sabre/event/lib/WildcardEmitterTrait.php create mode 100644 3rdparty/sabre/event/lib/coroutine.php create mode 100644 3rdparty/sabre/http/LICENSE create mode 100644 3rdparty/sabre/http/lib/Auth/AWS.php create mode 100644 3rdparty/sabre/http/lib/Auth/AbstractAuth.php create mode 100644 3rdparty/sabre/http/lib/Auth/Basic.php create mode 100644 3rdparty/sabre/http/lib/Auth/Bearer.php create mode 100644 3rdparty/sabre/http/lib/Auth/Digest.php create mode 100644 3rdparty/sabre/http/lib/Client.php create mode 100644 3rdparty/sabre/http/lib/ClientException.php create mode 100644 3rdparty/sabre/http/lib/ClientHttpException.php create mode 100644 3rdparty/sabre/http/lib/HttpException.php create mode 100644 3rdparty/sabre/http/lib/Message.php create mode 100644 3rdparty/sabre/http/lib/MessageDecoratorTrait.php create mode 100644 3rdparty/sabre/http/lib/MessageInterface.php create mode 100644 3rdparty/sabre/http/lib/Request.php create mode 100644 3rdparty/sabre/http/lib/RequestDecorator.php create mode 100644 3rdparty/sabre/http/lib/RequestInterface.php create mode 100644 3rdparty/sabre/http/lib/Response.php create mode 100644 3rdparty/sabre/http/lib/ResponseDecorator.php create mode 100644 3rdparty/sabre/http/lib/ResponseInterface.php create mode 100644 3rdparty/sabre/http/lib/Sapi.php create mode 100644 3rdparty/sabre/http/lib/Version.php create mode 100644 3rdparty/sabre/http/lib/functions.php create mode 100644 3rdparty/sabre/uri/LICENSE create mode 100644 3rdparty/sabre/uri/lib/InvalidUriException.php create mode 100644 3rdparty/sabre/uri/lib/Version.php create mode 100644 3rdparty/sabre/uri/lib/functions.php create mode 100644 3rdparty/sabre/vobject/LICENSE create mode 100644 3rdparty/sabre/vobject/lib/BirthdayCalendarGenerator.php create mode 100644 3rdparty/sabre/vobject/lib/Cli.php create mode 100644 3rdparty/sabre/vobject/lib/Component.php create mode 100644 3rdparty/sabre/vobject/lib/Component/Available.php create mode 100644 3rdparty/sabre/vobject/lib/Component/VAlarm.php create mode 100644 3rdparty/sabre/vobject/lib/Component/VAvailability.php create mode 100644 3rdparty/sabre/vobject/lib/Component/VCalendar.php create mode 100644 3rdparty/sabre/vobject/lib/Component/VCard.php create mode 100644 3rdparty/sabre/vobject/lib/Component/VEvent.php create mode 100644 3rdparty/sabre/vobject/lib/Component/VFreeBusy.php create mode 100644 3rdparty/sabre/vobject/lib/Component/VJournal.php create mode 100644 3rdparty/sabre/vobject/lib/Component/VTimeZone.php create mode 100644 3rdparty/sabre/vobject/lib/Component/VTodo.php create mode 100644 3rdparty/sabre/vobject/lib/DateTimeParser.php create mode 100644 3rdparty/sabre/vobject/lib/Document.php create mode 100644 3rdparty/sabre/vobject/lib/ElementList.php create mode 100644 3rdparty/sabre/vobject/lib/EofException.php create mode 100644 3rdparty/sabre/vobject/lib/FreeBusyData.php create mode 100644 3rdparty/sabre/vobject/lib/FreeBusyGenerator.php create mode 100644 3rdparty/sabre/vobject/lib/ITip/Broker.php create mode 100644 3rdparty/sabre/vobject/lib/ITip/ITipException.php create mode 100644 3rdparty/sabre/vobject/lib/ITip/Message.php create mode 100644 3rdparty/sabre/vobject/lib/ITip/SameOrganizerForAllComponentsException.php create mode 100644 3rdparty/sabre/vobject/lib/InvalidDataException.php create mode 100644 3rdparty/sabre/vobject/lib/Node.php create mode 100644 3rdparty/sabre/vobject/lib/PHPUnitAssertions.php create mode 100644 3rdparty/sabre/vobject/lib/Parameter.php create mode 100644 3rdparty/sabre/vobject/lib/ParseException.php create mode 100644 3rdparty/sabre/vobject/lib/Parser/Json.php create mode 100644 3rdparty/sabre/vobject/lib/Parser/MimeDir.php create mode 100644 3rdparty/sabre/vobject/lib/Parser/Parser.php create mode 100644 3rdparty/sabre/vobject/lib/Parser/XML.php create mode 100644 3rdparty/sabre/vobject/lib/Parser/XML/Element/KeyValue.php create mode 100644 3rdparty/sabre/vobject/lib/Property.php create mode 100644 3rdparty/sabre/vobject/lib/Property/Binary.php create mode 100644 3rdparty/sabre/vobject/lib/Property/Boolean.php create mode 100644 3rdparty/sabre/vobject/lib/Property/FlatText.php create mode 100644 3rdparty/sabre/vobject/lib/Property/FloatValue.php create mode 100644 3rdparty/sabre/vobject/lib/Property/ICalendar/CalAddress.php create mode 100644 3rdparty/sabre/vobject/lib/Property/ICalendar/Date.php create mode 100644 3rdparty/sabre/vobject/lib/Property/ICalendar/DateTime.php create mode 100644 3rdparty/sabre/vobject/lib/Property/ICalendar/Duration.php create mode 100644 3rdparty/sabre/vobject/lib/Property/ICalendar/Period.php create mode 100644 3rdparty/sabre/vobject/lib/Property/ICalendar/Recur.php create mode 100644 3rdparty/sabre/vobject/lib/Property/IntegerValue.php create mode 100644 3rdparty/sabre/vobject/lib/Property/Text.php create mode 100644 3rdparty/sabre/vobject/lib/Property/Time.php create mode 100644 3rdparty/sabre/vobject/lib/Property/Unknown.php create mode 100644 3rdparty/sabre/vobject/lib/Property/Uri.php create mode 100644 3rdparty/sabre/vobject/lib/Property/UtcOffset.php create mode 100644 3rdparty/sabre/vobject/lib/Property/VCard/Date.php create mode 100644 3rdparty/sabre/vobject/lib/Property/VCard/DateAndOrTime.php create mode 100644 3rdparty/sabre/vobject/lib/Property/VCard/DateTime.php create mode 100644 3rdparty/sabre/vobject/lib/Property/VCard/LanguageTag.php create mode 100644 3rdparty/sabre/vobject/lib/Property/VCard/PhoneNumber.php create mode 100644 3rdparty/sabre/vobject/lib/Property/VCard/TimeStamp.php create mode 100644 3rdparty/sabre/vobject/lib/Reader.php create mode 100644 3rdparty/sabre/vobject/lib/Recur/EventIterator.php create mode 100644 3rdparty/sabre/vobject/lib/Recur/MaxInstancesExceededException.php create mode 100644 3rdparty/sabre/vobject/lib/Recur/NoInstancesException.php create mode 100644 3rdparty/sabre/vobject/lib/Recur/RDateIterator.php create mode 100644 3rdparty/sabre/vobject/lib/Recur/RRuleIterator.php create mode 100644 3rdparty/sabre/vobject/lib/Settings.php create mode 100644 3rdparty/sabre/vobject/lib/Splitter/ICalendar.php create mode 100644 3rdparty/sabre/vobject/lib/Splitter/SplitterInterface.php create mode 100644 3rdparty/sabre/vobject/lib/Splitter/VCard.php create mode 100644 3rdparty/sabre/vobject/lib/StringUtil.php create mode 100644 3rdparty/sabre/vobject/lib/TimeZoneUtil.php create mode 100644 3rdparty/sabre/vobject/lib/TimezoneGuesser/FindFromOffset.php create mode 100644 3rdparty/sabre/vobject/lib/TimezoneGuesser/FindFromTimezoneIdentifier.php create mode 100644 3rdparty/sabre/vobject/lib/TimezoneGuesser/FindFromTimezoneMap.php create mode 100644 3rdparty/sabre/vobject/lib/TimezoneGuesser/GuessFromLicEntry.php create mode 100644 3rdparty/sabre/vobject/lib/TimezoneGuesser/GuessFromMsTzId.php create mode 100644 3rdparty/sabre/vobject/lib/TimezoneGuesser/TimezoneFinder.php create mode 100644 3rdparty/sabre/vobject/lib/TimezoneGuesser/TimezoneGuesser.php create mode 100644 3rdparty/sabre/vobject/lib/UUIDUtil.php create mode 100644 3rdparty/sabre/vobject/lib/VCardConverter.php create mode 100644 3rdparty/sabre/vobject/lib/Version.php create mode 100644 3rdparty/sabre/vobject/lib/Writer.php create mode 100644 3rdparty/sabre/vobject/lib/timezonedata/exchangezones.php create mode 100644 3rdparty/sabre/vobject/lib/timezonedata/lotuszones.php create mode 100644 3rdparty/sabre/vobject/lib/timezonedata/php-bc.php create mode 100644 3rdparty/sabre/vobject/lib/timezonedata/php-workaround.php create mode 100644 3rdparty/sabre/vobject/lib/timezonedata/windowszones.php create mode 100644 3rdparty/sabre/vobject/resources/schema/xcal.rng create mode 100644 3rdparty/sabre/vobject/resources/schema/xcard.rng create mode 100644 3rdparty/sabre/xml/LICENSE create mode 100644 3rdparty/sabre/xml/lib/ContextStackTrait.php create mode 100644 3rdparty/sabre/xml/lib/Deserializer/functions.php create mode 100644 3rdparty/sabre/xml/lib/Element.php create mode 100644 3rdparty/sabre/xml/lib/Element/Base.php create mode 100644 3rdparty/sabre/xml/lib/Element/Cdata.php create mode 100644 3rdparty/sabre/xml/lib/Element/Elements.php create mode 100644 3rdparty/sabre/xml/lib/Element/KeyValue.php create mode 100644 3rdparty/sabre/xml/lib/Element/Uri.php create mode 100644 3rdparty/sabre/xml/lib/Element/XmlFragment.php create mode 100644 3rdparty/sabre/xml/lib/LibXMLException.php create mode 100644 3rdparty/sabre/xml/lib/ParseException.php create mode 100644 3rdparty/sabre/xml/lib/Reader.php create mode 100644 3rdparty/sabre/xml/lib/Serializer/functions.php create mode 100644 3rdparty/sabre/xml/lib/Service.php create mode 100644 3rdparty/sabre/xml/lib/Version.php create mode 100644 3rdparty/sabre/xml/lib/Writer.php create mode 100644 3rdparty/sabre/xml/lib/XmlDeserializable.php create mode 100644 3rdparty/sabre/xml/lib/XmlSerializable.php create mode 100644 3rdparty/spomky-labs/cbor-php/LICENSE create mode 100644 3rdparty/spomky-labs/cbor-php/src/AbstractCBORObject.php create mode 100644 3rdparty/spomky-labs/cbor-php/src/ByteStringObject.php create mode 100644 3rdparty/spomky-labs/cbor-php/src/CBORObject.php create mode 100644 3rdparty/spomky-labs/cbor-php/src/Decoder.php create mode 100644 3rdparty/spomky-labs/cbor-php/src/DecoderInterface.php create mode 100644 3rdparty/spomky-labs/cbor-php/src/IndefiniteLengthByteStringObject.php create mode 100644 3rdparty/spomky-labs/cbor-php/src/IndefiniteLengthListObject.php create mode 100644 3rdparty/spomky-labs/cbor-php/src/IndefiniteLengthMapObject.php create mode 100644 3rdparty/spomky-labs/cbor-php/src/IndefiniteLengthTextStringObject.php create mode 100644 3rdparty/spomky-labs/cbor-php/src/LengthCalculator.php create mode 100644 3rdparty/spomky-labs/cbor-php/src/ListObject.php create mode 100644 3rdparty/spomky-labs/cbor-php/src/MapItem.php create mode 100644 3rdparty/spomky-labs/cbor-php/src/MapObject.php create mode 100644 3rdparty/spomky-labs/cbor-php/src/NegativeIntegerObject.php create mode 100644 3rdparty/spomky-labs/cbor-php/src/Normalizable.php create mode 100644 3rdparty/spomky-labs/cbor-php/src/OtherObject.php create mode 100644 3rdparty/spomky-labs/cbor-php/src/OtherObject/BreakObject.php create mode 100644 3rdparty/spomky-labs/cbor-php/src/OtherObject/DoublePrecisionFloatObject.php create mode 100644 3rdparty/spomky-labs/cbor-php/src/OtherObject/FalseObject.php create mode 100644 3rdparty/spomky-labs/cbor-php/src/OtherObject/GenericObject.php create mode 100644 3rdparty/spomky-labs/cbor-php/src/OtherObject/HalfPrecisionFloatObject.php create mode 100644 3rdparty/spomky-labs/cbor-php/src/OtherObject/NullObject.php create mode 100644 3rdparty/spomky-labs/cbor-php/src/OtherObject/OtherObjectInterface.php create mode 100644 3rdparty/spomky-labs/cbor-php/src/OtherObject/OtherObjectManager.php create mode 100644 3rdparty/spomky-labs/cbor-php/src/OtherObject/OtherObjectManagerInterface.php create mode 100644 3rdparty/spomky-labs/cbor-php/src/OtherObject/SimpleObject.php create mode 100644 3rdparty/spomky-labs/cbor-php/src/OtherObject/SinglePrecisionFloatObject.php create mode 100644 3rdparty/spomky-labs/cbor-php/src/OtherObject/TrueObject.php create mode 100644 3rdparty/spomky-labs/cbor-php/src/OtherObject/UndefinedObject.php create mode 100644 3rdparty/spomky-labs/cbor-php/src/Stream.php create mode 100644 3rdparty/spomky-labs/cbor-php/src/StringStream.php create mode 100644 3rdparty/spomky-labs/cbor-php/src/Tag.php create mode 100644 3rdparty/spomky-labs/cbor-php/src/Tag/Base16EncodingTag.php create mode 100644 3rdparty/spomky-labs/cbor-php/src/Tag/Base64EncodingTag.php create mode 100644 3rdparty/spomky-labs/cbor-php/src/Tag/Base64Tag.php create mode 100644 3rdparty/spomky-labs/cbor-php/src/Tag/Base64UrlEncodingTag.php create mode 100644 3rdparty/spomky-labs/cbor-php/src/Tag/Base64UrlTag.php create mode 100644 3rdparty/spomky-labs/cbor-php/src/Tag/BigFloatTag.php create mode 100644 3rdparty/spomky-labs/cbor-php/src/Tag/CBOREncodingTag.php create mode 100644 3rdparty/spomky-labs/cbor-php/src/Tag/CBORTag.php create mode 100644 3rdparty/spomky-labs/cbor-php/src/Tag/DatetimeTag.php create mode 100644 3rdparty/spomky-labs/cbor-php/src/Tag/DecimalFractionTag.php create mode 100644 3rdparty/spomky-labs/cbor-php/src/Tag/GenericTag.php create mode 100644 3rdparty/spomky-labs/cbor-php/src/Tag/MimeTag.php create mode 100644 3rdparty/spomky-labs/cbor-php/src/Tag/NegativeBigIntegerTag.php create mode 100644 3rdparty/spomky-labs/cbor-php/src/Tag/TagInterface.php create mode 100644 3rdparty/spomky-labs/cbor-php/src/Tag/TagManager.php create mode 100644 3rdparty/spomky-labs/cbor-php/src/Tag/TagManagerInterface.php create mode 100644 3rdparty/spomky-labs/cbor-php/src/Tag/TimestampTag.php create mode 100644 3rdparty/spomky-labs/cbor-php/src/Tag/UnsignedBigIntegerTag.php create mode 100644 3rdparty/spomky-labs/cbor-php/src/Tag/UriTag.php create mode 100644 3rdparty/spomky-labs/cbor-php/src/TextStringObject.php create mode 100644 3rdparty/spomky-labs/cbor-php/src/UnsignedIntegerObject.php create mode 100644 3rdparty/spomky-labs/cbor-php/src/Utils.php create mode 100644 3rdparty/spomky-labs/pki-framework/LICENSE create mode 100644 3rdparty/spomky-labs/pki-framework/src/ASN1/Component/Identifier.php create mode 100644 3rdparty/spomky-labs/pki-framework/src/ASN1/Component/Length.php create mode 100644 3rdparty/spomky-labs/pki-framework/src/ASN1/DERData.php create mode 100644 3rdparty/spomky-labs/pki-framework/src/ASN1/Element.php create mode 100644 3rdparty/spomky-labs/pki-framework/src/ASN1/Exception/DecodeException.php create mode 100644 3rdparty/spomky-labs/pki-framework/src/ASN1/Feature/ElementBase.php create mode 100644 3rdparty/spomky-labs/pki-framework/src/ASN1/Feature/Encodable.php create mode 100644 3rdparty/spomky-labs/pki-framework/src/ASN1/Feature/Stringable.php create mode 100644 3rdparty/spomky-labs/pki-framework/src/ASN1/Type/BaseString.php create mode 100644 3rdparty/spomky-labs/pki-framework/src/ASN1/Type/BaseTime.php create mode 100644 3rdparty/spomky-labs/pki-framework/src/ASN1/Type/Constructed/ConstructedString.php create mode 100644 3rdparty/spomky-labs/pki-framework/src/ASN1/Type/Constructed/Sequence.php create mode 100644 3rdparty/spomky-labs/pki-framework/src/ASN1/Type/Constructed/Set.php create mode 100644 3rdparty/spomky-labs/pki-framework/src/ASN1/Type/Primitive/BMPString.php create mode 100644 3rdparty/spomky-labs/pki-framework/src/ASN1/Type/Primitive/BitString.php create mode 100644 3rdparty/spomky-labs/pki-framework/src/ASN1/Type/Primitive/Boolean.php create mode 100644 3rdparty/spomky-labs/pki-framework/src/ASN1/Type/Primitive/CharacterString.php create mode 100644 3rdparty/spomky-labs/pki-framework/src/ASN1/Type/Primitive/EOC.php create mode 100644 3rdparty/spomky-labs/pki-framework/src/ASN1/Type/Primitive/Enumerated.php create mode 100644 3rdparty/spomky-labs/pki-framework/src/ASN1/Type/Primitive/GeneralString.php create mode 100644 3rdparty/spomky-labs/pki-framework/src/ASN1/Type/Primitive/GeneralizedTime.php create mode 100644 3rdparty/spomky-labs/pki-framework/src/ASN1/Type/Primitive/GraphicString.php create mode 100644 3rdparty/spomky-labs/pki-framework/src/ASN1/Type/Primitive/IA5String.php create mode 100644 3rdparty/spomky-labs/pki-framework/src/ASN1/Type/Primitive/Integer.php create mode 100644 3rdparty/spomky-labs/pki-framework/src/ASN1/Type/Primitive/NullType.php create mode 100644 3rdparty/spomky-labs/pki-framework/src/ASN1/Type/Primitive/Number.php create mode 100644 3rdparty/spomky-labs/pki-framework/src/ASN1/Type/Primitive/NumericString.php create mode 100644 3rdparty/spomky-labs/pki-framework/src/ASN1/Type/Primitive/ObjectDescriptor.php create mode 100644 3rdparty/spomky-labs/pki-framework/src/ASN1/Type/Primitive/ObjectIdentifier.php create mode 100644 3rdparty/spomky-labs/pki-framework/src/ASN1/Type/Primitive/OctetString.php create mode 100644 3rdparty/spomky-labs/pki-framework/src/ASN1/Type/Primitive/PrintableString.php create mode 100644 3rdparty/spomky-labs/pki-framework/src/ASN1/Type/Primitive/Real.php create mode 100644 3rdparty/spomky-labs/pki-framework/src/ASN1/Type/Primitive/RelativeOID.php create mode 100644 3rdparty/spomky-labs/pki-framework/src/ASN1/Type/Primitive/T61String.php create mode 100644 3rdparty/spomky-labs/pki-framework/src/ASN1/Type/Primitive/UTCTime.php create mode 100644 3rdparty/spomky-labs/pki-framework/src/ASN1/Type/Primitive/UTF8String.php create mode 100644 3rdparty/spomky-labs/pki-framework/src/ASN1/Type/Primitive/UniversalString.php create mode 100644 3rdparty/spomky-labs/pki-framework/src/ASN1/Type/Primitive/VideotexString.php create mode 100644 3rdparty/spomky-labs/pki-framework/src/ASN1/Type/Primitive/VisibleString.php create mode 100644 3rdparty/spomky-labs/pki-framework/src/ASN1/Type/PrimitiveString.php create mode 100644 3rdparty/spomky-labs/pki-framework/src/ASN1/Type/PrimitiveType.php create mode 100644 3rdparty/spomky-labs/pki-framework/src/ASN1/Type/StringType.php create mode 100644 3rdparty/spomky-labs/pki-framework/src/ASN1/Type/Structure.php create mode 100644 3rdparty/spomky-labs/pki-framework/src/ASN1/Type/Tagged/ApplicationType.php create mode 100644 3rdparty/spomky-labs/pki-framework/src/ASN1/Type/Tagged/ContextSpecificType.php create mode 100644 3rdparty/spomky-labs/pki-framework/src/ASN1/Type/Tagged/DERTaggedType.php create mode 100644 3rdparty/spomky-labs/pki-framework/src/ASN1/Type/Tagged/ExplicitTagging.php create mode 100644 3rdparty/spomky-labs/pki-framework/src/ASN1/Type/Tagged/ExplicitlyTaggedType.php create mode 100644 3rdparty/spomky-labs/pki-framework/src/ASN1/Type/Tagged/ImplicitTagging.php create mode 100644 3rdparty/spomky-labs/pki-framework/src/ASN1/Type/Tagged/ImplicitlyTaggedType.php create mode 100644 3rdparty/spomky-labs/pki-framework/src/ASN1/Type/Tagged/PrivateType.php create mode 100644 3rdparty/spomky-labs/pki-framework/src/ASN1/Type/Tagged/TaggedTypeWrap.php create mode 100644 3rdparty/spomky-labs/pki-framework/src/ASN1/Type/TaggedType.php create mode 100644 3rdparty/spomky-labs/pki-framework/src/ASN1/Type/TimeType.php create mode 100644 3rdparty/spomky-labs/pki-framework/src/ASN1/Type/UniversalClass.php create mode 100644 3rdparty/spomky-labs/pki-framework/src/ASN1/Type/UnspecifiedType.php create mode 100644 3rdparty/spomky-labs/pki-framework/src/ASN1/Util/BigInt.php create mode 100644 3rdparty/spomky-labs/pki-framework/src/ASN1/Util/Flags.php create mode 100644 3rdparty/spomky-labs/pki-framework/src/CryptoBridge/Crypto.php create mode 100644 3rdparty/spomky-labs/pki-framework/src/CryptoBridge/Crypto/OpenSSLCrypto.php create mode 100644 3rdparty/spomky-labs/pki-framework/src/CryptoEncoding/PEM.php create mode 100644 3rdparty/spomky-labs/pki-framework/src/CryptoEncoding/PEMBundle.php create mode 100644 3rdparty/spomky-labs/pki-framework/src/CryptoTypes/AlgorithmIdentifier/AlgorithmIdentifier.php create mode 100644 3rdparty/spomky-labs/pki-framework/src/CryptoTypes/AlgorithmIdentifier/AlgorithmIdentifierFactory.php create mode 100644 3rdparty/spomky-labs/pki-framework/src/CryptoTypes/AlgorithmIdentifier/AlgorithmIdentifierProvider.php create mode 100644 3rdparty/spomky-labs/pki-framework/src/CryptoTypes/AlgorithmIdentifier/Asymmetric/ECPublicKeyAlgorithmIdentifier.php create mode 100644 3rdparty/spomky-labs/pki-framework/src/CryptoTypes/AlgorithmIdentifier/Asymmetric/Ed25519AlgorithmIdentifier.php create mode 100644 3rdparty/spomky-labs/pki-framework/src/CryptoTypes/AlgorithmIdentifier/Asymmetric/Ed448AlgorithmIdentifier.php create mode 100644 3rdparty/spomky-labs/pki-framework/src/CryptoTypes/AlgorithmIdentifier/Asymmetric/RFC8410EdAlgorithmIdentifier.php create mode 100644 3rdparty/spomky-labs/pki-framework/src/CryptoTypes/AlgorithmIdentifier/Asymmetric/RFC8410XAlgorithmIdentifier.php create mode 100644 3rdparty/spomky-labs/pki-framework/src/CryptoTypes/AlgorithmIdentifier/Asymmetric/RSAEncryptionAlgorithmIdentifier.php create mode 100644 3rdparty/spomky-labs/pki-framework/src/CryptoTypes/AlgorithmIdentifier/Asymmetric/RSAPSSSSAEncryptionAlgorithmIdentifier.php create mode 100644 3rdparty/spomky-labs/pki-framework/src/CryptoTypes/AlgorithmIdentifier/Asymmetric/X25519AlgorithmIdentifier.php create mode 100644 3rdparty/spomky-labs/pki-framework/src/CryptoTypes/AlgorithmIdentifier/Asymmetric/X448AlgorithmIdentifier.php create mode 100644 3rdparty/spomky-labs/pki-framework/src/CryptoTypes/AlgorithmIdentifier/Cipher/AES128CBCAlgorithmIdentifier.php create mode 100644 3rdparty/spomky-labs/pki-framework/src/CryptoTypes/AlgorithmIdentifier/Cipher/AES192CBCAlgorithmIdentifier.php create mode 100644 3rdparty/spomky-labs/pki-framework/src/CryptoTypes/AlgorithmIdentifier/Cipher/AES256CBCAlgorithmIdentifier.php create mode 100644 3rdparty/spomky-labs/pki-framework/src/CryptoTypes/AlgorithmIdentifier/Cipher/AESCBCAlgorithmIdentifier.php create mode 100644 3rdparty/spomky-labs/pki-framework/src/CryptoTypes/AlgorithmIdentifier/Cipher/BlockCipherAlgorithmIdentifier.php create mode 100644 3rdparty/spomky-labs/pki-framework/src/CryptoTypes/AlgorithmIdentifier/Cipher/CipherAlgorithmIdentifier.php create mode 100644 3rdparty/spomky-labs/pki-framework/src/CryptoTypes/AlgorithmIdentifier/Cipher/DESCBCAlgorithmIdentifier.php create mode 100644 3rdparty/spomky-labs/pki-framework/src/CryptoTypes/AlgorithmIdentifier/Cipher/DESEDE3CBCAlgorithmIdentifier.php create mode 100644 3rdparty/spomky-labs/pki-framework/src/CryptoTypes/AlgorithmIdentifier/Cipher/RC2CBCAlgorithmIdentifier.php create mode 100644 3rdparty/spomky-labs/pki-framework/src/CryptoTypes/AlgorithmIdentifier/Feature/AlgorithmIdentifierType.php create mode 100644 3rdparty/spomky-labs/pki-framework/src/CryptoTypes/AlgorithmIdentifier/Feature/AsymmetricCryptoAlgorithmIdentifier.php create mode 100644 3rdparty/spomky-labs/pki-framework/src/CryptoTypes/AlgorithmIdentifier/Feature/EncryptionAlgorithmIdentifier.php create mode 100644 3rdparty/spomky-labs/pki-framework/src/CryptoTypes/AlgorithmIdentifier/Feature/HashAlgorithmIdentifier.php create mode 100644 3rdparty/spomky-labs/pki-framework/src/CryptoTypes/AlgorithmIdentifier/Feature/PRFAlgorithmIdentifier.php create mode 100644 3rdparty/spomky-labs/pki-framework/src/CryptoTypes/AlgorithmIdentifier/Feature/SignatureAlgorithmIdentifier.php create mode 100644 3rdparty/spomky-labs/pki-framework/src/CryptoTypes/AlgorithmIdentifier/GenericAlgorithmIdentifier.php create mode 100644 3rdparty/spomky-labs/pki-framework/src/CryptoTypes/AlgorithmIdentifier/Hash/HMACWithSHA1AlgorithmIdentifier.php create mode 100644 3rdparty/spomky-labs/pki-framework/src/CryptoTypes/AlgorithmIdentifier/Hash/HMACWithSHA224AlgorithmIdentifier.php create mode 100644 3rdparty/spomky-labs/pki-framework/src/CryptoTypes/AlgorithmIdentifier/Hash/HMACWithSHA256AlgorithmIdentifier.php create mode 100644 3rdparty/spomky-labs/pki-framework/src/CryptoTypes/AlgorithmIdentifier/Hash/HMACWithSHA384AlgorithmIdentifier.php create mode 100644 3rdparty/spomky-labs/pki-framework/src/CryptoTypes/AlgorithmIdentifier/Hash/HMACWithSHA512AlgorithmIdentifier.php create mode 100644 3rdparty/spomky-labs/pki-framework/src/CryptoTypes/AlgorithmIdentifier/Hash/MD5AlgorithmIdentifier.php create mode 100644 3rdparty/spomky-labs/pki-framework/src/CryptoTypes/AlgorithmIdentifier/Hash/RFC4231HMACAlgorithmIdentifier.php create mode 100644 3rdparty/spomky-labs/pki-framework/src/CryptoTypes/AlgorithmIdentifier/Hash/SHA1AlgorithmIdentifier.php create mode 100644 3rdparty/spomky-labs/pki-framework/src/CryptoTypes/AlgorithmIdentifier/Hash/SHA224AlgorithmIdentifier.php create mode 100644 3rdparty/spomky-labs/pki-framework/src/CryptoTypes/AlgorithmIdentifier/Hash/SHA256AlgorithmIdentifier.php create mode 100644 3rdparty/spomky-labs/pki-framework/src/CryptoTypes/AlgorithmIdentifier/Hash/SHA2AlgorithmIdentifier.php create mode 100644 3rdparty/spomky-labs/pki-framework/src/CryptoTypes/AlgorithmIdentifier/Hash/SHA384AlgorithmIdentifier.php create mode 100644 3rdparty/spomky-labs/pki-framework/src/CryptoTypes/AlgorithmIdentifier/Hash/SHA512AlgorithmIdentifier.php create mode 100644 3rdparty/spomky-labs/pki-framework/src/CryptoTypes/AlgorithmIdentifier/Signature/ECDSAWithSHA1AlgorithmIdentifier.php create mode 100644 3rdparty/spomky-labs/pki-framework/src/CryptoTypes/AlgorithmIdentifier/Signature/ECDSAWithSHA224AlgorithmIdentifier.php create mode 100644 3rdparty/spomky-labs/pki-framework/src/CryptoTypes/AlgorithmIdentifier/Signature/ECDSAWithSHA256AlgorithmIdentifier.php create mode 100644 3rdparty/spomky-labs/pki-framework/src/CryptoTypes/AlgorithmIdentifier/Signature/ECDSAWithSHA384AlgorithmIdentifier.php create mode 100644 3rdparty/spomky-labs/pki-framework/src/CryptoTypes/AlgorithmIdentifier/Signature/ECDSAWithSHA512AlgorithmIdentifier.php create mode 100644 3rdparty/spomky-labs/pki-framework/src/CryptoTypes/AlgorithmIdentifier/Signature/ECSignatureAlgorithmIdentifier.php create mode 100644 3rdparty/spomky-labs/pki-framework/src/CryptoTypes/AlgorithmIdentifier/Signature/MD2WithRSAEncryptionAlgorithmIdentifier.php create mode 100644 3rdparty/spomky-labs/pki-framework/src/CryptoTypes/AlgorithmIdentifier/Signature/MD4WithRSAEncryptionAlgorithmIdentifier.php create mode 100644 3rdparty/spomky-labs/pki-framework/src/CryptoTypes/AlgorithmIdentifier/Signature/MD5WithRSAEncryptionAlgorithmIdentifier.php create mode 100644 3rdparty/spomky-labs/pki-framework/src/CryptoTypes/AlgorithmIdentifier/Signature/RFC3279RSASignatureAlgorithmIdentifier.php create mode 100644 3rdparty/spomky-labs/pki-framework/src/CryptoTypes/AlgorithmIdentifier/Signature/RFC4055RSASignatureAlgorithmIdentifier.php create mode 100644 3rdparty/spomky-labs/pki-framework/src/CryptoTypes/AlgorithmIdentifier/Signature/RSASignatureAlgorithmIdentifier.php create mode 100644 3rdparty/spomky-labs/pki-framework/src/CryptoTypes/AlgorithmIdentifier/Signature/SHA1WithRSAEncryptionAlgorithmIdentifier.php create mode 100644 3rdparty/spomky-labs/pki-framework/src/CryptoTypes/AlgorithmIdentifier/Signature/SHA224WithRSAEncryptionAlgorithmIdentifier.php create mode 100644 3rdparty/spomky-labs/pki-framework/src/CryptoTypes/AlgorithmIdentifier/Signature/SHA256WithRSAEncryptionAlgorithmIdentifier.php create mode 100644 3rdparty/spomky-labs/pki-framework/src/CryptoTypes/AlgorithmIdentifier/Signature/SHA384WithRSAEncryptionAlgorithmIdentifier.php create mode 100644 3rdparty/spomky-labs/pki-framework/src/CryptoTypes/AlgorithmIdentifier/Signature/SHA512WithRSAEncryptionAlgorithmIdentifier.php create mode 100644 3rdparty/spomky-labs/pki-framework/src/CryptoTypes/AlgorithmIdentifier/SpecificAlgorithmIdentifier.php create mode 100644 3rdparty/spomky-labs/pki-framework/src/CryptoTypes/Asymmetric/Attribute/OneAsymmetricKeyAttributes.php create mode 100644 3rdparty/spomky-labs/pki-framework/src/CryptoTypes/Asymmetric/EC/ECConversion.php create mode 100644 3rdparty/spomky-labs/pki-framework/src/CryptoTypes/Asymmetric/EC/ECPrivateKey.php create mode 100644 3rdparty/spomky-labs/pki-framework/src/CryptoTypes/Asymmetric/EC/ECPublicKey.php create mode 100644 3rdparty/spomky-labs/pki-framework/src/CryptoTypes/Asymmetric/OneAsymmetricKey.php create mode 100644 3rdparty/spomky-labs/pki-framework/src/CryptoTypes/Asymmetric/PrivateKey.php create mode 100644 3rdparty/spomky-labs/pki-framework/src/CryptoTypes/Asymmetric/PrivateKeyInfo.php create mode 100644 3rdparty/spomky-labs/pki-framework/src/CryptoTypes/Asymmetric/PublicKey.php create mode 100644 3rdparty/spomky-labs/pki-framework/src/CryptoTypes/Asymmetric/PublicKeyInfo.php create mode 100644 3rdparty/spomky-labs/pki-framework/src/CryptoTypes/Asymmetric/RFC8410/Curve25519/Curve25519PrivateKey.php create mode 100644 3rdparty/spomky-labs/pki-framework/src/CryptoTypes/Asymmetric/RFC8410/Curve25519/Curve25519PublicKey.php create mode 100644 3rdparty/spomky-labs/pki-framework/src/CryptoTypes/Asymmetric/RFC8410/Curve25519/Ed25519PrivateKey.php create mode 100644 3rdparty/spomky-labs/pki-framework/src/CryptoTypes/Asymmetric/RFC8410/Curve25519/Ed25519PublicKey.php create mode 100644 3rdparty/spomky-labs/pki-framework/src/CryptoTypes/Asymmetric/RFC8410/Curve25519/X25519PrivateKey.php create mode 100644 3rdparty/spomky-labs/pki-framework/src/CryptoTypes/Asymmetric/RFC8410/Curve25519/X25519PublicKey.php create mode 100644 3rdparty/spomky-labs/pki-framework/src/CryptoTypes/Asymmetric/RFC8410/Curve448/Ed448PrivateKey.php create mode 100644 3rdparty/spomky-labs/pki-framework/src/CryptoTypes/Asymmetric/RFC8410/Curve448/Ed448PublicKey.php create mode 100644 3rdparty/spomky-labs/pki-framework/src/CryptoTypes/Asymmetric/RFC8410/Curve448/X448PrivateKey.php create mode 100644 3rdparty/spomky-labs/pki-framework/src/CryptoTypes/Asymmetric/RFC8410/Curve448/X448PublicKey.php create mode 100644 3rdparty/spomky-labs/pki-framework/src/CryptoTypes/Asymmetric/RFC8410/RFC8410PrivateKey.php create mode 100644 3rdparty/spomky-labs/pki-framework/src/CryptoTypes/Asymmetric/RFC8410/RFC8410PublicKey.php create mode 100644 3rdparty/spomky-labs/pki-framework/src/CryptoTypes/Asymmetric/RSA/RSAPrivateKey.php create mode 100644 3rdparty/spomky-labs/pki-framework/src/CryptoTypes/Asymmetric/RSA/RSAPublicKey.php create mode 100644 3rdparty/spomky-labs/pki-framework/src/CryptoTypes/Asymmetric/RSA/RSASSAPSSPrivateKey.php create mode 100644 3rdparty/spomky-labs/pki-framework/src/CryptoTypes/Signature/ECSignature.php create mode 100644 3rdparty/spomky-labs/pki-framework/src/CryptoTypes/Signature/Ed25519Signature.php create mode 100644 3rdparty/spomky-labs/pki-framework/src/CryptoTypes/Signature/Ed448Signature.php create mode 100644 3rdparty/spomky-labs/pki-framework/src/CryptoTypes/Signature/GenericSignature.php create mode 100644 3rdparty/spomky-labs/pki-framework/src/CryptoTypes/Signature/RSASignature.php create mode 100644 3rdparty/spomky-labs/pki-framework/src/CryptoTypes/Signature/Signature.php create mode 100644 3rdparty/spomky-labs/pki-framework/src/X501/ASN1/Attribute.php create mode 100644 3rdparty/spomky-labs/pki-framework/src/X501/ASN1/AttributeType.php create mode 100644 3rdparty/spomky-labs/pki-framework/src/X501/ASN1/AttributeTypeAndValue.php create mode 100644 3rdparty/spomky-labs/pki-framework/src/X501/ASN1/AttributeValue/AttributeValue.php create mode 100644 3rdparty/spomky-labs/pki-framework/src/X501/ASN1/AttributeValue/CommonNameValue.php create mode 100644 3rdparty/spomky-labs/pki-framework/src/X501/ASN1/AttributeValue/CountryNameValue.php create mode 100644 3rdparty/spomky-labs/pki-framework/src/X501/ASN1/AttributeValue/DescriptionValue.php create mode 100644 3rdparty/spomky-labs/pki-framework/src/X501/ASN1/AttributeValue/Feature/DirectoryString.php create mode 100644 3rdparty/spomky-labs/pki-framework/src/X501/ASN1/AttributeValue/Feature/PrintableStringValue.php create mode 100644 3rdparty/spomky-labs/pki-framework/src/X501/ASN1/AttributeValue/GivenNameValue.php create mode 100644 3rdparty/spomky-labs/pki-framework/src/X501/ASN1/AttributeValue/LocalityNameValue.php create mode 100644 3rdparty/spomky-labs/pki-framework/src/X501/ASN1/AttributeValue/NameValue.php create mode 100644 3rdparty/spomky-labs/pki-framework/src/X501/ASN1/AttributeValue/OrganizationNameValue.php create mode 100644 3rdparty/spomky-labs/pki-framework/src/X501/ASN1/AttributeValue/OrganizationalUnitNameValue.php create mode 100644 3rdparty/spomky-labs/pki-framework/src/X501/ASN1/AttributeValue/PseudonymValue.php create mode 100644 3rdparty/spomky-labs/pki-framework/src/X501/ASN1/AttributeValue/SerialNumberValue.php create mode 100644 3rdparty/spomky-labs/pki-framework/src/X501/ASN1/AttributeValue/StateOrProvinceNameValue.php create mode 100644 3rdparty/spomky-labs/pki-framework/src/X501/ASN1/AttributeValue/SurnameValue.php create mode 100644 3rdparty/spomky-labs/pki-framework/src/X501/ASN1/AttributeValue/TitleValue.php create mode 100644 3rdparty/spomky-labs/pki-framework/src/X501/ASN1/AttributeValue/UnknownAttributeValue.php create mode 100644 3rdparty/spomky-labs/pki-framework/src/X501/ASN1/Collection/AttributeCollection.php create mode 100644 3rdparty/spomky-labs/pki-framework/src/X501/ASN1/Collection/SequenceOfAttributes.php create mode 100644 3rdparty/spomky-labs/pki-framework/src/X501/ASN1/Collection/SetOfAttributes.php create mode 100644 3rdparty/spomky-labs/pki-framework/src/X501/ASN1/Name.php create mode 100644 3rdparty/spomky-labs/pki-framework/src/X501/ASN1/RDN.php create mode 100644 3rdparty/spomky-labs/pki-framework/src/X501/DN/DNParser.php create mode 100644 3rdparty/spomky-labs/pki-framework/src/X501/MatchingRule/BinaryMatch.php create mode 100644 3rdparty/spomky-labs/pki-framework/src/X501/MatchingRule/CaseExactMatch.php create mode 100644 3rdparty/spomky-labs/pki-framework/src/X501/MatchingRule/CaseIgnoreMatch.php create mode 100644 3rdparty/spomky-labs/pki-framework/src/X501/MatchingRule/MatchingRule.php create mode 100644 3rdparty/spomky-labs/pki-framework/src/X501/MatchingRule/StringPrepMatchingRule.php create mode 100644 3rdparty/spomky-labs/pki-framework/src/X501/StringPrep/CheckBidiStep.php create mode 100644 3rdparty/spomky-labs/pki-framework/src/X501/StringPrep/InsignificantNonSubstringSpaceStep.php create mode 100644 3rdparty/spomky-labs/pki-framework/src/X501/StringPrep/MapStep.php create mode 100644 3rdparty/spomky-labs/pki-framework/src/X501/StringPrep/NormalizeStep.php create mode 100644 3rdparty/spomky-labs/pki-framework/src/X501/StringPrep/PrepareStep.php create mode 100644 3rdparty/spomky-labs/pki-framework/src/X501/StringPrep/ProhibitStep.php create mode 100644 3rdparty/spomky-labs/pki-framework/src/X501/StringPrep/StringPreparer.php create mode 100644 3rdparty/spomky-labs/pki-framework/src/X501/StringPrep/TranscodeStep.php create mode 100644 3rdparty/spomky-labs/pki-framework/src/X509/AttributeCertificate/AttCertIssuer.php create mode 100644 3rdparty/spomky-labs/pki-framework/src/X509/AttributeCertificate/AttCertValidityPeriod.php create mode 100644 3rdparty/spomky-labs/pki-framework/src/X509/AttributeCertificate/Attribute/AccessIdentityAttributeValue.php create mode 100644 3rdparty/spomky-labs/pki-framework/src/X509/AttributeCertificate/Attribute/AuthenticationInfoAttributeValue.php create mode 100644 3rdparty/spomky-labs/pki-framework/src/X509/AttributeCertificate/Attribute/ChargingIdentityAttributeValue.php create mode 100644 3rdparty/spomky-labs/pki-framework/src/X509/AttributeCertificate/Attribute/GroupAttributeValue.php create mode 100644 3rdparty/spomky-labs/pki-framework/src/X509/AttributeCertificate/Attribute/IetfAttrSyntax.php create mode 100644 3rdparty/spomky-labs/pki-framework/src/X509/AttributeCertificate/Attribute/IetfAttrValue.php create mode 100644 3rdparty/spomky-labs/pki-framework/src/X509/AttributeCertificate/Attribute/RoleAttributeValue.php create mode 100644 3rdparty/spomky-labs/pki-framework/src/X509/AttributeCertificate/Attribute/SvceAuthInfo.php create mode 100644 3rdparty/spomky-labs/pki-framework/src/X509/AttributeCertificate/AttributeCertificate.php create mode 100644 3rdparty/spomky-labs/pki-framework/src/X509/AttributeCertificate/AttributeCertificateInfo.php create mode 100644 3rdparty/spomky-labs/pki-framework/src/X509/AttributeCertificate/Attributes.php create mode 100644 3rdparty/spomky-labs/pki-framework/src/X509/AttributeCertificate/Holder.php create mode 100644 3rdparty/spomky-labs/pki-framework/src/X509/AttributeCertificate/IssuerSerial.php create mode 100644 3rdparty/spomky-labs/pki-framework/src/X509/AttributeCertificate/ObjectDigestInfo.php create mode 100644 3rdparty/spomky-labs/pki-framework/src/X509/AttributeCertificate/V2Form.php create mode 100644 3rdparty/spomky-labs/pki-framework/src/X509/AttributeCertificate/Validation/ACValidationConfig.php create mode 100644 3rdparty/spomky-labs/pki-framework/src/X509/AttributeCertificate/Validation/ACValidator.php create mode 100644 3rdparty/spomky-labs/pki-framework/src/X509/AttributeCertificate/Validation/Exception/ACValidationException.php create mode 100644 3rdparty/spomky-labs/pki-framework/src/X509/Certificate/Certificate.php create mode 100644 3rdparty/spomky-labs/pki-framework/src/X509/Certificate/CertificateBundle.php create mode 100644 3rdparty/spomky-labs/pki-framework/src/X509/Certificate/CertificateChain.php create mode 100644 3rdparty/spomky-labs/pki-framework/src/X509/Certificate/Extension/AAControlsExtension.php create mode 100644 3rdparty/spomky-labs/pki-framework/src/X509/Certificate/Extension/AccessDescription/AccessDescription.php create mode 100644 3rdparty/spomky-labs/pki-framework/src/X509/Certificate/Extension/AccessDescription/AuthorityAccessDescription.php create mode 100644 3rdparty/spomky-labs/pki-framework/src/X509/Certificate/Extension/AccessDescription/SubjectAccessDescription.php create mode 100644 3rdparty/spomky-labs/pki-framework/src/X509/Certificate/Extension/AuthorityInformationAccessExtension.php create mode 100644 3rdparty/spomky-labs/pki-framework/src/X509/Certificate/Extension/AuthorityKeyIdentifierExtension.php create mode 100644 3rdparty/spomky-labs/pki-framework/src/X509/Certificate/Extension/BasicConstraintsExtension.php create mode 100644 3rdparty/spomky-labs/pki-framework/src/X509/Certificate/Extension/CRLDistributionPointsExtension.php create mode 100644 3rdparty/spomky-labs/pki-framework/src/X509/Certificate/Extension/CertificatePoliciesExtension.php create mode 100644 3rdparty/spomky-labs/pki-framework/src/X509/Certificate/Extension/CertificatePolicy/CPSQualifier.php create mode 100644 3rdparty/spomky-labs/pki-framework/src/X509/Certificate/Extension/CertificatePolicy/DisplayText.php create mode 100644 3rdparty/spomky-labs/pki-framework/src/X509/Certificate/Extension/CertificatePolicy/NoticeReference.php create mode 100644 3rdparty/spomky-labs/pki-framework/src/X509/Certificate/Extension/CertificatePolicy/PolicyInformation.php create mode 100644 3rdparty/spomky-labs/pki-framework/src/X509/Certificate/Extension/CertificatePolicy/PolicyQualifierInfo.php create mode 100644 3rdparty/spomky-labs/pki-framework/src/X509/Certificate/Extension/CertificatePolicy/UserNoticeQualifier.php create mode 100644 3rdparty/spomky-labs/pki-framework/src/X509/Certificate/Extension/DistributionPoint/DistributionPoint.php create mode 100644 3rdparty/spomky-labs/pki-framework/src/X509/Certificate/Extension/DistributionPoint/DistributionPointName.php create mode 100644 3rdparty/spomky-labs/pki-framework/src/X509/Certificate/Extension/DistributionPoint/FullName.php create mode 100644 3rdparty/spomky-labs/pki-framework/src/X509/Certificate/Extension/DistributionPoint/ReasonFlags.php create mode 100644 3rdparty/spomky-labs/pki-framework/src/X509/Certificate/Extension/DistributionPoint/RelativeName.php create mode 100644 3rdparty/spomky-labs/pki-framework/src/X509/Certificate/Extension/ExtendedKeyUsageExtension.php create mode 100644 3rdparty/spomky-labs/pki-framework/src/X509/Certificate/Extension/Extension.php create mode 100644 3rdparty/spomky-labs/pki-framework/src/X509/Certificate/Extension/FreshestCRLExtension.php create mode 100644 3rdparty/spomky-labs/pki-framework/src/X509/Certificate/Extension/InhibitAnyPolicyExtension.php create mode 100644 3rdparty/spomky-labs/pki-framework/src/X509/Certificate/Extension/IssuerAlternativeNameExtension.php create mode 100644 3rdparty/spomky-labs/pki-framework/src/X509/Certificate/Extension/KeyUsageExtension.php create mode 100644 3rdparty/spomky-labs/pki-framework/src/X509/Certificate/Extension/NameConstraints/GeneralSubtree.php create mode 100644 3rdparty/spomky-labs/pki-framework/src/X509/Certificate/Extension/NameConstraints/GeneralSubtrees.php create mode 100644 3rdparty/spomky-labs/pki-framework/src/X509/Certificate/Extension/NameConstraintsExtension.php create mode 100644 3rdparty/spomky-labs/pki-framework/src/X509/Certificate/Extension/NoRevocationAvailableExtension.php create mode 100644 3rdparty/spomky-labs/pki-framework/src/X509/Certificate/Extension/PolicyConstraintsExtension.php create mode 100644 3rdparty/spomky-labs/pki-framework/src/X509/Certificate/Extension/PolicyMappings/PolicyMapping.php create mode 100644 3rdparty/spomky-labs/pki-framework/src/X509/Certificate/Extension/PolicyMappingsExtension.php create mode 100644 3rdparty/spomky-labs/pki-framework/src/X509/Certificate/Extension/SubjectAlternativeNameExtension.php create mode 100644 3rdparty/spomky-labs/pki-framework/src/X509/Certificate/Extension/SubjectDirectoryAttributesExtension.php create mode 100644 3rdparty/spomky-labs/pki-framework/src/X509/Certificate/Extension/SubjectInformationAccessExtension.php create mode 100644 3rdparty/spomky-labs/pki-framework/src/X509/Certificate/Extension/SubjectKeyIdentifierExtension.php create mode 100644 3rdparty/spomky-labs/pki-framework/src/X509/Certificate/Extension/Target/Target.php create mode 100644 3rdparty/spomky-labs/pki-framework/src/X509/Certificate/Extension/Target/TargetGroup.php create mode 100644 3rdparty/spomky-labs/pki-framework/src/X509/Certificate/Extension/Target/TargetName.php create mode 100644 3rdparty/spomky-labs/pki-framework/src/X509/Certificate/Extension/Target/Targets.php create mode 100644 3rdparty/spomky-labs/pki-framework/src/X509/Certificate/Extension/TargetInformationExtension.php create mode 100644 3rdparty/spomky-labs/pki-framework/src/X509/Certificate/Extension/UnknownExtension.php create mode 100644 3rdparty/spomky-labs/pki-framework/src/X509/Certificate/Extensions.php create mode 100644 3rdparty/spomky-labs/pki-framework/src/X509/Certificate/TBSCertificate.php create mode 100644 3rdparty/spomky-labs/pki-framework/src/X509/Certificate/Time.php create mode 100644 3rdparty/spomky-labs/pki-framework/src/X509/Certificate/UniqueIdentifier.php create mode 100644 3rdparty/spomky-labs/pki-framework/src/X509/Certificate/Validity.php create mode 100644 3rdparty/spomky-labs/pki-framework/src/X509/CertificationPath/CertificationPath.php create mode 100644 3rdparty/spomky-labs/pki-framework/src/X509/CertificationPath/Exception/PathBuildingException.php create mode 100644 3rdparty/spomky-labs/pki-framework/src/X509/CertificationPath/Exception/PathValidationException.php create mode 100644 3rdparty/spomky-labs/pki-framework/src/X509/CertificationPath/PathBuilding/CertificationPathBuilder.php create mode 100644 3rdparty/spomky-labs/pki-framework/src/X509/CertificationPath/PathValidation/PathValidationConfig.php create mode 100644 3rdparty/spomky-labs/pki-framework/src/X509/CertificationPath/PathValidation/PathValidationResult.php create mode 100644 3rdparty/spomky-labs/pki-framework/src/X509/CertificationPath/PathValidation/PathValidator.php create mode 100644 3rdparty/spomky-labs/pki-framework/src/X509/CertificationPath/PathValidation/ValidatorState.php create mode 100644 3rdparty/spomky-labs/pki-framework/src/X509/CertificationPath/Policy/PolicyNode.php create mode 100644 3rdparty/spomky-labs/pki-framework/src/X509/CertificationPath/Policy/PolicyTree.php create mode 100644 3rdparty/spomky-labs/pki-framework/src/X509/CertificationRequest/Attribute/ExtensionRequestValue.php create mode 100644 3rdparty/spomky-labs/pki-framework/src/X509/CertificationRequest/Attributes.php create mode 100644 3rdparty/spomky-labs/pki-framework/src/X509/CertificationRequest/CertificationRequest.php create mode 100644 3rdparty/spomky-labs/pki-framework/src/X509/CertificationRequest/CertificationRequestInfo.php create mode 100644 3rdparty/spomky-labs/pki-framework/src/X509/Exception/X509ValidationException.php create mode 100644 3rdparty/spomky-labs/pki-framework/src/X509/Feature/DateTimeHelper.php create mode 100644 3rdparty/spomky-labs/pki-framework/src/X509/GeneralName/DNSName.php create mode 100644 3rdparty/spomky-labs/pki-framework/src/X509/GeneralName/DirectoryName.php create mode 100644 3rdparty/spomky-labs/pki-framework/src/X509/GeneralName/EDIPartyName.php create mode 100644 3rdparty/spomky-labs/pki-framework/src/X509/GeneralName/GeneralName.php create mode 100644 3rdparty/spomky-labs/pki-framework/src/X509/GeneralName/GeneralNames.php create mode 100644 3rdparty/spomky-labs/pki-framework/src/X509/GeneralName/IPAddress.php create mode 100644 3rdparty/spomky-labs/pki-framework/src/X509/GeneralName/IPv4Address.php create mode 100644 3rdparty/spomky-labs/pki-framework/src/X509/GeneralName/IPv6Address.php create mode 100644 3rdparty/spomky-labs/pki-framework/src/X509/GeneralName/OtherName.php create mode 100644 3rdparty/spomky-labs/pki-framework/src/X509/GeneralName/RFC822Name.php create mode 100644 3rdparty/spomky-labs/pki-framework/src/X509/GeneralName/RegisteredID.php create mode 100644 3rdparty/spomky-labs/pki-framework/src/X509/GeneralName/UniformResourceIdentifier.php create mode 100644 3rdparty/spomky-labs/pki-framework/src/X509/GeneralName/X400Address.php create mode 100644 3rdparty/stecman/symfony-console-completion/LICENCE create mode 100644 3rdparty/stecman/symfony-console-completion/src/Completion.php create mode 100644 3rdparty/stecman/symfony-console-completion/src/Completion/CompletionAwareInterface.php create mode 100644 3rdparty/stecman/symfony-console-completion/src/Completion/CompletionInterface.php create mode 100644 3rdparty/stecman/symfony-console-completion/src/Completion/ShellPathCompletion.php create mode 100644 3rdparty/stecman/symfony-console-completion/src/CompletionCommand.php create mode 100644 3rdparty/stecman/symfony-console-completion/src/CompletionContext.php create mode 100644 3rdparty/stecman/symfony-console-completion/src/CompletionHandler.php create mode 100644 3rdparty/stecman/symfony-console-completion/src/EnvironmentCompletionContext.php create mode 100644 3rdparty/stecman/symfony-console-completion/src/HookFactory.php create mode 100644 3rdparty/symfony/console/Application.php create mode 100644 3rdparty/symfony/console/Attribute/AsCommand.php create mode 100644 3rdparty/symfony/console/CI/GithubActionReporter.php create mode 100644 3rdparty/symfony/console/Color.php create mode 100644 3rdparty/symfony/console/Command/Command.php create mode 100644 3rdparty/symfony/console/Command/CompleteCommand.php create mode 100644 3rdparty/symfony/console/Command/DumpCompletionCommand.php create mode 100644 3rdparty/symfony/console/Command/HelpCommand.php create mode 100644 3rdparty/symfony/console/Command/LazyCommand.php create mode 100644 3rdparty/symfony/console/Command/ListCommand.php create mode 100644 3rdparty/symfony/console/Command/LockableTrait.php create mode 100644 3rdparty/symfony/console/Command/SignalableCommandInterface.php create mode 100644 3rdparty/symfony/console/Command/TraceableCommand.php create mode 100644 3rdparty/symfony/console/CommandLoader/CommandLoaderInterface.php create mode 100644 3rdparty/symfony/console/CommandLoader/ContainerCommandLoader.php create mode 100644 3rdparty/symfony/console/CommandLoader/FactoryCommandLoader.php create mode 100644 3rdparty/symfony/console/Completion/CompletionInput.php create mode 100644 3rdparty/symfony/console/Completion/CompletionSuggestions.php create mode 100644 3rdparty/symfony/console/Completion/Output/BashCompletionOutput.php create mode 100644 3rdparty/symfony/console/Completion/Output/CompletionOutputInterface.php create mode 100644 3rdparty/symfony/console/Completion/Output/FishCompletionOutput.php create mode 100644 3rdparty/symfony/console/Completion/Output/ZshCompletionOutput.php create mode 100644 3rdparty/symfony/console/Completion/Suggestion.php create mode 100644 3rdparty/symfony/console/ConsoleEvents.php create mode 100644 3rdparty/symfony/console/Cursor.php create mode 100644 3rdparty/symfony/console/DataCollector/CommandDataCollector.php create mode 100644 3rdparty/symfony/console/Debug/CliRequest.php create mode 100644 3rdparty/symfony/console/DependencyInjection/AddConsoleCommandPass.php create mode 100644 3rdparty/symfony/console/Descriptor/ApplicationDescription.php create mode 100644 3rdparty/symfony/console/Descriptor/Descriptor.php create mode 100644 3rdparty/symfony/console/Descriptor/DescriptorInterface.php create mode 100644 3rdparty/symfony/console/Descriptor/JsonDescriptor.php create mode 100644 3rdparty/symfony/console/Descriptor/MarkdownDescriptor.php create mode 100644 3rdparty/symfony/console/Descriptor/ReStructuredTextDescriptor.php create mode 100644 3rdparty/symfony/console/Descriptor/TextDescriptor.php create mode 100644 3rdparty/symfony/console/Descriptor/XmlDescriptor.php create mode 100644 3rdparty/symfony/console/Event/ConsoleCommandEvent.php create mode 100644 3rdparty/symfony/console/Event/ConsoleErrorEvent.php create mode 100644 3rdparty/symfony/console/Event/ConsoleEvent.php create mode 100644 3rdparty/symfony/console/Event/ConsoleSignalEvent.php create mode 100644 3rdparty/symfony/console/Event/ConsoleTerminateEvent.php create mode 100644 3rdparty/symfony/console/EventListener/ErrorListener.php create mode 100644 3rdparty/symfony/console/Exception/CommandNotFoundException.php create mode 100644 3rdparty/symfony/console/Exception/ExceptionInterface.php create mode 100644 3rdparty/symfony/console/Exception/InvalidArgumentException.php create mode 100644 3rdparty/symfony/console/Exception/InvalidOptionException.php create mode 100644 3rdparty/symfony/console/Exception/LogicException.php create mode 100644 3rdparty/symfony/console/Exception/MissingInputException.php create mode 100644 3rdparty/symfony/console/Exception/NamespaceNotFoundException.php create mode 100644 3rdparty/symfony/console/Exception/RunCommandFailedException.php create mode 100644 3rdparty/symfony/console/Exception/RuntimeException.php create mode 100644 3rdparty/symfony/console/Formatter/NullOutputFormatter.php create mode 100644 3rdparty/symfony/console/Formatter/NullOutputFormatterStyle.php create mode 100644 3rdparty/symfony/console/Formatter/OutputFormatter.php create mode 100644 3rdparty/symfony/console/Formatter/OutputFormatterInterface.php create mode 100644 3rdparty/symfony/console/Formatter/OutputFormatterStyle.php create mode 100644 3rdparty/symfony/console/Formatter/OutputFormatterStyleInterface.php create mode 100644 3rdparty/symfony/console/Formatter/OutputFormatterStyleStack.php create mode 100644 3rdparty/symfony/console/Formatter/WrappableOutputFormatterInterface.php create mode 100644 3rdparty/symfony/console/Helper/DebugFormatterHelper.php create mode 100644 3rdparty/symfony/console/Helper/DescriptorHelper.php create mode 100644 3rdparty/symfony/console/Helper/Dumper.php create mode 100644 3rdparty/symfony/console/Helper/FormatterHelper.php create mode 100644 3rdparty/symfony/console/Helper/Helper.php create mode 100644 3rdparty/symfony/console/Helper/HelperInterface.php create mode 100644 3rdparty/symfony/console/Helper/HelperSet.php create mode 100644 3rdparty/symfony/console/Helper/InputAwareHelper.php create mode 100644 3rdparty/symfony/console/Helper/OutputWrapper.php create mode 100644 3rdparty/symfony/console/Helper/ProcessHelper.php create mode 100644 3rdparty/symfony/console/Helper/ProgressBar.php create mode 100644 3rdparty/symfony/console/Helper/ProgressIndicator.php create mode 100644 3rdparty/symfony/console/Helper/QuestionHelper.php create mode 100644 3rdparty/symfony/console/Helper/SymfonyQuestionHelper.php create mode 100644 3rdparty/symfony/console/Helper/Table.php create mode 100644 3rdparty/symfony/console/Helper/TableCell.php create mode 100644 3rdparty/symfony/console/Helper/TableCellStyle.php create mode 100644 3rdparty/symfony/console/Helper/TableRows.php create mode 100644 3rdparty/symfony/console/Helper/TableSeparator.php create mode 100644 3rdparty/symfony/console/Helper/TableStyle.php create mode 100644 3rdparty/symfony/console/Input/ArgvInput.php create mode 100644 3rdparty/symfony/console/Input/ArrayInput.php create mode 100644 3rdparty/symfony/console/Input/Input.php create mode 100644 3rdparty/symfony/console/Input/InputArgument.php create mode 100644 3rdparty/symfony/console/Input/InputAwareInterface.php create mode 100644 3rdparty/symfony/console/Input/InputDefinition.php create mode 100644 3rdparty/symfony/console/Input/InputInterface.php create mode 100644 3rdparty/symfony/console/Input/InputOption.php create mode 100644 3rdparty/symfony/console/Input/StreamableInputInterface.php create mode 100644 3rdparty/symfony/console/Input/StringInput.php create mode 100644 3rdparty/symfony/console/LICENSE create mode 100644 3rdparty/symfony/console/Logger/ConsoleLogger.php create mode 100644 3rdparty/symfony/console/Messenger/RunCommandContext.php create mode 100644 3rdparty/symfony/console/Messenger/RunCommandMessage.php create mode 100644 3rdparty/symfony/console/Messenger/RunCommandMessageHandler.php create mode 100644 3rdparty/symfony/console/Output/AnsiColorMode.php create mode 100644 3rdparty/symfony/console/Output/BufferedOutput.php create mode 100644 3rdparty/symfony/console/Output/ConsoleOutput.php create mode 100644 3rdparty/symfony/console/Output/ConsoleOutputInterface.php create mode 100644 3rdparty/symfony/console/Output/ConsoleSectionOutput.php create mode 100644 3rdparty/symfony/console/Output/NullOutput.php create mode 100644 3rdparty/symfony/console/Output/Output.php create mode 100644 3rdparty/symfony/console/Output/OutputInterface.php create mode 100644 3rdparty/symfony/console/Output/StreamOutput.php create mode 100644 3rdparty/symfony/console/Output/TrimmedBufferOutput.php create mode 100644 3rdparty/symfony/console/Question/ChoiceQuestion.php create mode 100644 3rdparty/symfony/console/Question/ConfirmationQuestion.php create mode 100644 3rdparty/symfony/console/Question/Question.php create mode 100644 3rdparty/symfony/console/Resources/completion.bash create mode 100644 3rdparty/symfony/console/Resources/completion.fish create mode 100644 3rdparty/symfony/console/Resources/completion.zsh create mode 100644 3rdparty/symfony/console/SignalRegistry/SignalMap.php create mode 100644 3rdparty/symfony/console/SignalRegistry/SignalRegistry.php create mode 100644 3rdparty/symfony/console/SingleCommandApplication.php create mode 100644 3rdparty/symfony/console/Style/OutputStyle.php create mode 100644 3rdparty/symfony/console/Style/StyleInterface.php create mode 100644 3rdparty/symfony/console/Style/SymfonyStyle.php create mode 100644 3rdparty/symfony/console/Terminal.php create mode 100644 3rdparty/symfony/console/Tester/ApplicationTester.php create mode 100644 3rdparty/symfony/console/Tester/CommandCompletionTester.php create mode 100644 3rdparty/symfony/console/Tester/CommandTester.php create mode 100644 3rdparty/symfony/console/Tester/Constraint/CommandIsSuccessful.php create mode 100644 3rdparty/symfony/console/Tester/TesterTrait.php create mode 100644 3rdparty/symfony/css-selector/CssSelectorConverter.php create mode 100644 3rdparty/symfony/css-selector/Exception/ExceptionInterface.php create mode 100644 3rdparty/symfony/css-selector/Exception/ExpressionErrorException.php create mode 100644 3rdparty/symfony/css-selector/Exception/InternalErrorException.php create mode 100644 3rdparty/symfony/css-selector/Exception/ParseException.php create mode 100644 3rdparty/symfony/css-selector/Exception/SyntaxErrorException.php create mode 100644 3rdparty/symfony/css-selector/LICENSE create mode 100644 3rdparty/symfony/css-selector/Node/AbstractNode.php create mode 100644 3rdparty/symfony/css-selector/Node/AttributeNode.php create mode 100644 3rdparty/symfony/css-selector/Node/ClassNode.php create mode 100644 3rdparty/symfony/css-selector/Node/CombinedSelectorNode.php create mode 100644 3rdparty/symfony/css-selector/Node/ElementNode.php create mode 100644 3rdparty/symfony/css-selector/Node/FunctionNode.php create mode 100644 3rdparty/symfony/css-selector/Node/HashNode.php create mode 100644 3rdparty/symfony/css-selector/Node/NegationNode.php create mode 100644 3rdparty/symfony/css-selector/Node/NodeInterface.php create mode 100644 3rdparty/symfony/css-selector/Node/PseudoNode.php create mode 100644 3rdparty/symfony/css-selector/Node/SelectorNode.php create mode 100644 3rdparty/symfony/css-selector/Node/Specificity.php create mode 100644 3rdparty/symfony/css-selector/Parser/Handler/CommentHandler.php create mode 100644 3rdparty/symfony/css-selector/Parser/Handler/HandlerInterface.php create mode 100644 3rdparty/symfony/css-selector/Parser/Handler/HashHandler.php create mode 100644 3rdparty/symfony/css-selector/Parser/Handler/IdentifierHandler.php create mode 100644 3rdparty/symfony/css-selector/Parser/Handler/NumberHandler.php create mode 100644 3rdparty/symfony/css-selector/Parser/Handler/StringHandler.php create mode 100644 3rdparty/symfony/css-selector/Parser/Handler/WhitespaceHandler.php create mode 100644 3rdparty/symfony/css-selector/Parser/Parser.php create mode 100644 3rdparty/symfony/css-selector/Parser/ParserInterface.php create mode 100644 3rdparty/symfony/css-selector/Parser/Reader.php create mode 100644 3rdparty/symfony/css-selector/Parser/Shortcut/ClassParser.php create mode 100644 3rdparty/symfony/css-selector/Parser/Shortcut/ElementParser.php create mode 100644 3rdparty/symfony/css-selector/Parser/Shortcut/EmptyStringParser.php create mode 100644 3rdparty/symfony/css-selector/Parser/Shortcut/HashParser.php create mode 100644 3rdparty/symfony/css-selector/Parser/Token.php create mode 100644 3rdparty/symfony/css-selector/Parser/TokenStream.php create mode 100644 3rdparty/symfony/css-selector/Parser/Tokenizer/Tokenizer.php create mode 100644 3rdparty/symfony/css-selector/Parser/Tokenizer/TokenizerEscaping.php create mode 100644 3rdparty/symfony/css-selector/Parser/Tokenizer/TokenizerPatterns.php create mode 100644 3rdparty/symfony/css-selector/XPath/Extension/AbstractExtension.php create mode 100644 3rdparty/symfony/css-selector/XPath/Extension/AttributeMatchingExtension.php create mode 100644 3rdparty/symfony/css-selector/XPath/Extension/CombinationExtension.php create mode 100644 3rdparty/symfony/css-selector/XPath/Extension/ExtensionInterface.php create mode 100644 3rdparty/symfony/css-selector/XPath/Extension/FunctionExtension.php create mode 100644 3rdparty/symfony/css-selector/XPath/Extension/HtmlExtension.php create mode 100644 3rdparty/symfony/css-selector/XPath/Extension/NodeExtension.php create mode 100644 3rdparty/symfony/css-selector/XPath/Extension/PseudoClassExtension.php create mode 100644 3rdparty/symfony/css-selector/XPath/Translator.php create mode 100644 3rdparty/symfony/css-selector/XPath/TranslatorInterface.php create mode 100644 3rdparty/symfony/css-selector/XPath/XPathExpr.php create mode 100644 3rdparty/symfony/deprecation-contracts/LICENSE create mode 100644 3rdparty/symfony/deprecation-contracts/function.php create mode 100644 3rdparty/symfony/dom-crawler/AbstractUriElement.php create mode 100644 3rdparty/symfony/dom-crawler/Crawler.php create mode 100644 3rdparty/symfony/dom-crawler/Field/ChoiceFormField.php create mode 100644 3rdparty/symfony/dom-crawler/Field/FileFormField.php create mode 100644 3rdparty/symfony/dom-crawler/Field/FormField.php create mode 100644 3rdparty/symfony/dom-crawler/Field/InputFormField.php create mode 100644 3rdparty/symfony/dom-crawler/Field/TextareaFormField.php create mode 100644 3rdparty/symfony/dom-crawler/Form.php create mode 100644 3rdparty/symfony/dom-crawler/FormFieldRegistry.php create mode 100644 3rdparty/symfony/dom-crawler/Image.php create mode 100644 3rdparty/symfony/dom-crawler/LICENSE create mode 100644 3rdparty/symfony/dom-crawler/Link.php create mode 100644 3rdparty/symfony/dom-crawler/UriResolver.php create mode 100644 3rdparty/symfony/event-dispatcher-contracts/Event.php create mode 100644 3rdparty/symfony/event-dispatcher-contracts/EventDispatcherInterface.php create mode 100644 3rdparty/symfony/event-dispatcher-contracts/LICENSE create mode 100644 3rdparty/symfony/event-dispatcher/Attribute/AsEventListener.php create mode 100644 3rdparty/symfony/event-dispatcher/Debug/TraceableEventDispatcher.php create mode 100644 3rdparty/symfony/event-dispatcher/Debug/WrappedListener.php create mode 100644 3rdparty/symfony/event-dispatcher/DependencyInjection/AddEventAliasesPass.php create mode 100644 3rdparty/symfony/event-dispatcher/DependencyInjection/RegisterListenersPass.php create mode 100644 3rdparty/symfony/event-dispatcher/EventDispatcher.php create mode 100644 3rdparty/symfony/event-dispatcher/EventDispatcherInterface.php create mode 100644 3rdparty/symfony/event-dispatcher/EventSubscriberInterface.php create mode 100644 3rdparty/symfony/event-dispatcher/GenericEvent.php create mode 100644 3rdparty/symfony/event-dispatcher/ImmutableEventDispatcher.php create mode 100644 3rdparty/symfony/event-dispatcher/LICENSE create mode 100644 3rdparty/symfony/http-foundation/AcceptHeader.php create mode 100644 3rdparty/symfony/http-foundation/AcceptHeaderItem.php create mode 100644 3rdparty/symfony/http-foundation/BinaryFileResponse.php create mode 100644 3rdparty/symfony/http-foundation/ChainRequestMatcher.php create mode 100644 3rdparty/symfony/http-foundation/Cookie.php create mode 100644 3rdparty/symfony/http-foundation/Exception/BadRequestException.php create mode 100644 3rdparty/symfony/http-foundation/Exception/ConflictingHeadersException.php create mode 100644 3rdparty/symfony/http-foundation/Exception/JsonException.php create mode 100644 3rdparty/symfony/http-foundation/Exception/RequestExceptionInterface.php create mode 100644 3rdparty/symfony/http-foundation/Exception/SessionNotFoundException.php create mode 100644 3rdparty/symfony/http-foundation/Exception/SuspiciousOperationException.php create mode 100644 3rdparty/symfony/http-foundation/Exception/UnexpectedValueException.php create mode 100644 3rdparty/symfony/http-foundation/ExpressionRequestMatcher.php create mode 100644 3rdparty/symfony/http-foundation/File/Exception/AccessDeniedException.php create mode 100644 3rdparty/symfony/http-foundation/File/Exception/CannotWriteFileException.php create mode 100644 3rdparty/symfony/http-foundation/File/Exception/ExtensionFileException.php create mode 100644 3rdparty/symfony/http-foundation/File/Exception/FileException.php create mode 100644 3rdparty/symfony/http-foundation/File/Exception/FileNotFoundException.php create mode 100644 3rdparty/symfony/http-foundation/File/Exception/FormSizeFileException.php create mode 100644 3rdparty/symfony/http-foundation/File/Exception/IniSizeFileException.php create mode 100644 3rdparty/symfony/http-foundation/File/Exception/NoFileException.php create mode 100644 3rdparty/symfony/http-foundation/File/Exception/NoTmpDirFileException.php create mode 100644 3rdparty/symfony/http-foundation/File/Exception/PartialFileException.php create mode 100644 3rdparty/symfony/http-foundation/File/Exception/UnexpectedTypeException.php create mode 100644 3rdparty/symfony/http-foundation/File/Exception/UploadException.php create mode 100644 3rdparty/symfony/http-foundation/File/File.php create mode 100644 3rdparty/symfony/http-foundation/File/Stream.php create mode 100644 3rdparty/symfony/http-foundation/File/UploadedFile.php create mode 100644 3rdparty/symfony/http-foundation/FileBag.php create mode 100644 3rdparty/symfony/http-foundation/HeaderBag.php create mode 100644 3rdparty/symfony/http-foundation/HeaderUtils.php create mode 100644 3rdparty/symfony/http-foundation/InputBag.php create mode 100644 3rdparty/symfony/http-foundation/IpUtils.php create mode 100644 3rdparty/symfony/http-foundation/JsonResponse.php create mode 100644 3rdparty/symfony/http-foundation/LICENSE create mode 100644 3rdparty/symfony/http-foundation/ParameterBag.php create mode 100644 3rdparty/symfony/http-foundation/RateLimiter/AbstractRequestRateLimiter.php create mode 100644 3rdparty/symfony/http-foundation/RateLimiter/PeekableRequestRateLimiterInterface.php create mode 100644 3rdparty/symfony/http-foundation/RateLimiter/RequestRateLimiterInterface.php create mode 100644 3rdparty/symfony/http-foundation/RedirectResponse.php create mode 100644 3rdparty/symfony/http-foundation/Request.php create mode 100644 3rdparty/symfony/http-foundation/RequestMatcher.php create mode 100644 3rdparty/symfony/http-foundation/RequestMatcher/AttributesRequestMatcher.php create mode 100644 3rdparty/symfony/http-foundation/RequestMatcher/ExpressionRequestMatcher.php create mode 100644 3rdparty/symfony/http-foundation/RequestMatcher/HostRequestMatcher.php create mode 100644 3rdparty/symfony/http-foundation/RequestMatcher/IpsRequestMatcher.php create mode 100644 3rdparty/symfony/http-foundation/RequestMatcher/IsJsonRequestMatcher.php create mode 100644 3rdparty/symfony/http-foundation/RequestMatcher/MethodRequestMatcher.php create mode 100644 3rdparty/symfony/http-foundation/RequestMatcher/PathRequestMatcher.php create mode 100644 3rdparty/symfony/http-foundation/RequestMatcher/PortRequestMatcher.php create mode 100644 3rdparty/symfony/http-foundation/RequestMatcher/SchemeRequestMatcher.php create mode 100644 3rdparty/symfony/http-foundation/RequestMatcherInterface.php create mode 100644 3rdparty/symfony/http-foundation/RequestStack.php create mode 100644 3rdparty/symfony/http-foundation/Response.php create mode 100644 3rdparty/symfony/http-foundation/ResponseHeaderBag.php create mode 100644 3rdparty/symfony/http-foundation/ServerBag.php create mode 100644 3rdparty/symfony/http-foundation/Session/Attribute/AttributeBag.php create mode 100644 3rdparty/symfony/http-foundation/Session/Attribute/AttributeBagInterface.php create mode 100644 3rdparty/symfony/http-foundation/Session/Flash/AutoExpireFlashBag.php create mode 100644 3rdparty/symfony/http-foundation/Session/Flash/FlashBag.php create mode 100644 3rdparty/symfony/http-foundation/Session/Flash/FlashBagInterface.php create mode 100644 3rdparty/symfony/http-foundation/Session/FlashBagAwareSessionInterface.php create mode 100644 3rdparty/symfony/http-foundation/Session/Session.php create mode 100644 3rdparty/symfony/http-foundation/Session/SessionBagInterface.php create mode 100644 3rdparty/symfony/http-foundation/Session/SessionBagProxy.php create mode 100644 3rdparty/symfony/http-foundation/Session/SessionFactory.php create mode 100644 3rdparty/symfony/http-foundation/Session/SessionFactoryInterface.php create mode 100644 3rdparty/symfony/http-foundation/Session/SessionInterface.php create mode 100644 3rdparty/symfony/http-foundation/Session/SessionUtils.php create mode 100644 3rdparty/symfony/http-foundation/Session/Storage/Handler/AbstractSessionHandler.php create mode 100644 3rdparty/symfony/http-foundation/Session/Storage/Handler/IdentityMarshaller.php create mode 100644 3rdparty/symfony/http-foundation/Session/Storage/Handler/MarshallingSessionHandler.php create mode 100644 3rdparty/symfony/http-foundation/Session/Storage/Handler/MemcachedSessionHandler.php create mode 100644 3rdparty/symfony/http-foundation/Session/Storage/Handler/MigratingSessionHandler.php create mode 100644 3rdparty/symfony/http-foundation/Session/Storage/Handler/MongoDbSessionHandler.php create mode 100644 3rdparty/symfony/http-foundation/Session/Storage/Handler/NativeFileSessionHandler.php create mode 100644 3rdparty/symfony/http-foundation/Session/Storage/Handler/NullSessionHandler.php create mode 100644 3rdparty/symfony/http-foundation/Session/Storage/Handler/PdoSessionHandler.php create mode 100644 3rdparty/symfony/http-foundation/Session/Storage/Handler/RedisSessionHandler.php create mode 100644 3rdparty/symfony/http-foundation/Session/Storage/Handler/SessionHandlerFactory.php create mode 100644 3rdparty/symfony/http-foundation/Session/Storage/Handler/StrictSessionHandler.php create mode 100644 3rdparty/symfony/http-foundation/Session/Storage/MetadataBag.php create mode 100644 3rdparty/symfony/http-foundation/Session/Storage/MockArraySessionStorage.php create mode 100644 3rdparty/symfony/http-foundation/Session/Storage/MockFileSessionStorage.php create mode 100644 3rdparty/symfony/http-foundation/Session/Storage/MockFileSessionStorageFactory.php create mode 100644 3rdparty/symfony/http-foundation/Session/Storage/NativeSessionStorage.php create mode 100644 3rdparty/symfony/http-foundation/Session/Storage/NativeSessionStorageFactory.php create mode 100644 3rdparty/symfony/http-foundation/Session/Storage/PhpBridgeSessionStorage.php create mode 100644 3rdparty/symfony/http-foundation/Session/Storage/PhpBridgeSessionStorageFactory.php create mode 100644 3rdparty/symfony/http-foundation/Session/Storage/Proxy/AbstractProxy.php create mode 100644 3rdparty/symfony/http-foundation/Session/Storage/Proxy/SessionHandlerProxy.php create mode 100644 3rdparty/symfony/http-foundation/Session/Storage/SessionStorageFactoryInterface.php create mode 100644 3rdparty/symfony/http-foundation/Session/Storage/SessionStorageInterface.php create mode 100644 3rdparty/symfony/http-foundation/StreamedJsonResponse.php create mode 100644 3rdparty/symfony/http-foundation/StreamedResponse.php create mode 100644 3rdparty/symfony/http-foundation/UriSigner.php create mode 100644 3rdparty/symfony/http-foundation/UrlHelper.php create mode 100644 3rdparty/symfony/mailer/Command/MailerTestCommand.php create mode 100644 3rdparty/symfony/mailer/DataCollector/MessageDataCollector.php create mode 100644 3rdparty/symfony/mailer/DelayedEnvelope.php create mode 100644 3rdparty/symfony/mailer/Envelope.php create mode 100644 3rdparty/symfony/mailer/Event/FailedMessageEvent.php create mode 100644 3rdparty/symfony/mailer/Event/MessageEvent.php create mode 100644 3rdparty/symfony/mailer/Event/MessageEvents.php create mode 100644 3rdparty/symfony/mailer/Event/SentMessageEvent.php create mode 100644 3rdparty/symfony/mailer/EventListener/EnvelopeListener.php create mode 100644 3rdparty/symfony/mailer/EventListener/MessageListener.php create mode 100644 3rdparty/symfony/mailer/EventListener/MessageLoggerListener.php create mode 100644 3rdparty/symfony/mailer/EventListener/MessengerTransportListener.php create mode 100644 3rdparty/symfony/mailer/Exception/ExceptionInterface.php create mode 100644 3rdparty/symfony/mailer/Exception/HttpTransportException.php create mode 100644 3rdparty/symfony/mailer/Exception/IncompleteDsnException.php create mode 100644 3rdparty/symfony/mailer/Exception/InvalidArgumentException.php create mode 100644 3rdparty/symfony/mailer/Exception/LogicException.php create mode 100644 3rdparty/symfony/mailer/Exception/RuntimeException.php create mode 100644 3rdparty/symfony/mailer/Exception/TransportException.php create mode 100644 3rdparty/symfony/mailer/Exception/TransportExceptionInterface.php create mode 100644 3rdparty/symfony/mailer/Exception/UnexpectedResponseException.php create mode 100644 3rdparty/symfony/mailer/Exception/UnsupportedSchemeException.php create mode 100644 3rdparty/symfony/mailer/Header/MetadataHeader.php create mode 100644 3rdparty/symfony/mailer/Header/TagHeader.php create mode 100644 3rdparty/symfony/mailer/LICENSE create mode 100644 3rdparty/symfony/mailer/Mailer.php create mode 100644 3rdparty/symfony/mailer/MailerInterface.php create mode 100644 3rdparty/symfony/mailer/Messenger/MessageHandler.php create mode 100644 3rdparty/symfony/mailer/Messenger/SendEmailMessage.php create mode 100644 3rdparty/symfony/mailer/SentMessage.php create mode 100644 3rdparty/symfony/mailer/Transport.php create mode 100644 3rdparty/symfony/mailer/Transport/AbstractApiTransport.php create mode 100644 3rdparty/symfony/mailer/Transport/AbstractHttpTransport.php create mode 100644 3rdparty/symfony/mailer/Transport/AbstractTransport.php create mode 100644 3rdparty/symfony/mailer/Transport/AbstractTransportFactory.php create mode 100644 3rdparty/symfony/mailer/Transport/Dsn.php create mode 100644 3rdparty/symfony/mailer/Transport/FailoverTransport.php create mode 100644 3rdparty/symfony/mailer/Transport/NativeTransportFactory.php create mode 100644 3rdparty/symfony/mailer/Transport/NullTransport.php create mode 100644 3rdparty/symfony/mailer/Transport/NullTransportFactory.php create mode 100644 3rdparty/symfony/mailer/Transport/RoundRobinTransport.php create mode 100644 3rdparty/symfony/mailer/Transport/SendmailTransport.php create mode 100644 3rdparty/symfony/mailer/Transport/SendmailTransportFactory.php create mode 100644 3rdparty/symfony/mailer/Transport/Smtp/Auth/AuthenticatorInterface.php create mode 100644 3rdparty/symfony/mailer/Transport/Smtp/Auth/CramMd5Authenticator.php create mode 100644 3rdparty/symfony/mailer/Transport/Smtp/Auth/LoginAuthenticator.php create mode 100644 3rdparty/symfony/mailer/Transport/Smtp/Auth/PlainAuthenticator.php create mode 100644 3rdparty/symfony/mailer/Transport/Smtp/Auth/XOAuth2Authenticator.php create mode 100644 3rdparty/symfony/mailer/Transport/Smtp/EsmtpTransport.php create mode 100644 3rdparty/symfony/mailer/Transport/Smtp/EsmtpTransportFactory.php create mode 100644 3rdparty/symfony/mailer/Transport/Smtp/SmtpTransport.php create mode 100644 3rdparty/symfony/mailer/Transport/Smtp/Stream/AbstractStream.php create mode 100644 3rdparty/symfony/mailer/Transport/Smtp/Stream/ProcessStream.php create mode 100644 3rdparty/symfony/mailer/Transport/Smtp/Stream/SocketStream.php create mode 100644 3rdparty/symfony/mailer/Transport/TransportFactoryInterface.php create mode 100644 3rdparty/symfony/mailer/Transport/TransportInterface.php create mode 100644 3rdparty/symfony/mailer/Transport/Transports.php create mode 100644 3rdparty/symfony/mime/Address.php create mode 100644 3rdparty/symfony/mime/BodyRendererInterface.php create mode 100644 3rdparty/symfony/mime/CharacterStream.php create mode 100644 3rdparty/symfony/mime/Crypto/DkimOptions.php create mode 100644 3rdparty/symfony/mime/Crypto/DkimSigner.php create mode 100644 3rdparty/symfony/mime/Crypto/SMime.php create mode 100644 3rdparty/symfony/mime/Crypto/SMimeEncrypter.php create mode 100644 3rdparty/symfony/mime/Crypto/SMimeSigner.php create mode 100644 3rdparty/symfony/mime/DependencyInjection/AddMimeTypeGuesserPass.php create mode 100644 3rdparty/symfony/mime/DraftEmail.php create mode 100644 3rdparty/symfony/mime/Email.php create mode 100644 3rdparty/symfony/mime/Encoder/AddressEncoderInterface.php create mode 100644 3rdparty/symfony/mime/Encoder/Base64ContentEncoder.php create mode 100644 3rdparty/symfony/mime/Encoder/Base64Encoder.php create mode 100644 3rdparty/symfony/mime/Encoder/Base64MimeHeaderEncoder.php create mode 100644 3rdparty/symfony/mime/Encoder/ContentEncoderInterface.php create mode 100644 3rdparty/symfony/mime/Encoder/EightBitContentEncoder.php create mode 100644 3rdparty/symfony/mime/Encoder/EncoderInterface.php create mode 100644 3rdparty/symfony/mime/Encoder/IdnAddressEncoder.php create mode 100644 3rdparty/symfony/mime/Encoder/MimeHeaderEncoderInterface.php create mode 100644 3rdparty/symfony/mime/Encoder/QpContentEncoder.php create mode 100644 3rdparty/symfony/mime/Encoder/QpEncoder.php create mode 100644 3rdparty/symfony/mime/Encoder/QpMimeHeaderEncoder.php create mode 100644 3rdparty/symfony/mime/Encoder/Rfc2231Encoder.php create mode 100644 3rdparty/symfony/mime/Exception/AddressEncoderException.php create mode 100644 3rdparty/symfony/mime/Exception/ExceptionInterface.php create mode 100644 3rdparty/symfony/mime/Exception/InvalidArgumentException.php create mode 100644 3rdparty/symfony/mime/Exception/LogicException.php create mode 100644 3rdparty/symfony/mime/Exception/RfcComplianceException.php create mode 100644 3rdparty/symfony/mime/Exception/RuntimeException.php create mode 100644 3rdparty/symfony/mime/FileBinaryMimeTypeGuesser.php create mode 100644 3rdparty/symfony/mime/FileinfoMimeTypeGuesser.php create mode 100644 3rdparty/symfony/mime/Header/AbstractHeader.php create mode 100644 3rdparty/symfony/mime/Header/DateHeader.php create mode 100644 3rdparty/symfony/mime/Header/HeaderInterface.php create mode 100644 3rdparty/symfony/mime/Header/Headers.php create mode 100644 3rdparty/symfony/mime/Header/IdentificationHeader.php create mode 100644 3rdparty/symfony/mime/Header/MailboxHeader.php create mode 100644 3rdparty/symfony/mime/Header/MailboxListHeader.php create mode 100644 3rdparty/symfony/mime/Header/ParameterizedHeader.php create mode 100644 3rdparty/symfony/mime/Header/PathHeader.php create mode 100644 3rdparty/symfony/mime/Header/UnstructuredHeader.php create mode 100644 3rdparty/symfony/mime/HtmlToTextConverter/DefaultHtmlToTextConverter.php create mode 100644 3rdparty/symfony/mime/HtmlToTextConverter/HtmlToTextConverterInterface.php create mode 100644 3rdparty/symfony/mime/HtmlToTextConverter/LeagueHtmlToMarkdownConverter.php create mode 100644 3rdparty/symfony/mime/LICENSE create mode 100644 3rdparty/symfony/mime/Message.php create mode 100644 3rdparty/symfony/mime/MessageConverter.php create mode 100644 3rdparty/symfony/mime/MimeTypeGuesserInterface.php create mode 100644 3rdparty/symfony/mime/MimeTypes.php create mode 100644 3rdparty/symfony/mime/MimeTypesInterface.php create mode 100644 3rdparty/symfony/mime/Part/AbstractMultipartPart.php create mode 100644 3rdparty/symfony/mime/Part/AbstractPart.php create mode 100644 3rdparty/symfony/mime/Part/DataPart.php create mode 100644 3rdparty/symfony/mime/Part/File.php create mode 100644 3rdparty/symfony/mime/Part/MessagePart.php create mode 100644 3rdparty/symfony/mime/Part/Multipart/AlternativePart.php create mode 100644 3rdparty/symfony/mime/Part/Multipart/DigestPart.php create mode 100644 3rdparty/symfony/mime/Part/Multipart/FormDataPart.php create mode 100644 3rdparty/symfony/mime/Part/Multipart/MixedPart.php create mode 100644 3rdparty/symfony/mime/Part/Multipart/RelatedPart.php create mode 100644 3rdparty/symfony/mime/Part/SMimePart.php create mode 100644 3rdparty/symfony/mime/Part/TextPart.php create mode 100644 3rdparty/symfony/mime/RawMessage.php create mode 100644 3rdparty/symfony/polyfill-intl-grapheme/Grapheme.php create mode 100644 3rdparty/symfony/polyfill-intl-grapheme/LICENSE create mode 100644 3rdparty/symfony/polyfill-intl-grapheme/bootstrap.php create mode 100644 3rdparty/symfony/polyfill-intl-grapheme/bootstrap80.php create mode 100644 3rdparty/symfony/polyfill-intl-idn/Idn.php create mode 100644 3rdparty/symfony/polyfill-intl-idn/Info.php create mode 100644 3rdparty/symfony/polyfill-intl-idn/LICENSE create mode 100644 3rdparty/symfony/polyfill-intl-idn/Resources/unidata/DisallowedRanges.php create mode 100644 3rdparty/symfony/polyfill-intl-idn/Resources/unidata/Regex.php create mode 100644 3rdparty/symfony/polyfill-intl-idn/Resources/unidata/deviation.php create mode 100644 3rdparty/symfony/polyfill-intl-idn/Resources/unidata/disallowed.php create mode 100644 3rdparty/symfony/polyfill-intl-idn/Resources/unidata/disallowed_STD3_mapped.php create mode 100644 3rdparty/symfony/polyfill-intl-idn/Resources/unidata/disallowed_STD3_valid.php create mode 100644 3rdparty/symfony/polyfill-intl-idn/Resources/unidata/ignored.php create mode 100644 3rdparty/symfony/polyfill-intl-idn/Resources/unidata/mapped.php create mode 100644 3rdparty/symfony/polyfill-intl-idn/Resources/unidata/virama.php create mode 100644 3rdparty/symfony/polyfill-intl-idn/bootstrap.php create mode 100644 3rdparty/symfony/polyfill-intl-idn/bootstrap80.php create mode 100644 3rdparty/symfony/polyfill-intl-normalizer/LICENSE create mode 100644 3rdparty/symfony/polyfill-intl-normalizer/Normalizer.php create mode 100644 3rdparty/symfony/polyfill-intl-normalizer/Resources/stubs/Normalizer.php create mode 100644 3rdparty/symfony/polyfill-intl-normalizer/Resources/unidata/canonicalComposition.php create mode 100644 3rdparty/symfony/polyfill-intl-normalizer/Resources/unidata/canonicalDecomposition.php create mode 100644 3rdparty/symfony/polyfill-intl-normalizer/Resources/unidata/combiningClass.php create mode 100644 3rdparty/symfony/polyfill-intl-normalizer/Resources/unidata/compatibilityDecomposition.php create mode 100644 3rdparty/symfony/polyfill-intl-normalizer/bootstrap.php create mode 100644 3rdparty/symfony/polyfill-intl-normalizer/bootstrap80.php create mode 100644 3rdparty/symfony/polyfill-php82/LICENSE create mode 100644 3rdparty/symfony/polyfill-php82/NoDynamicProperties.php create mode 100644 3rdparty/symfony/polyfill-php82/Php82.php create mode 100644 3rdparty/symfony/polyfill-php82/Random/Engine/Secure.php create mode 100644 3rdparty/symfony/polyfill-php82/Resources/stubs/AllowDynamicProperties.php create mode 100644 3rdparty/symfony/polyfill-php82/Resources/stubs/Random/BrokenRandomEngineError.php create mode 100644 3rdparty/symfony/polyfill-php82/Resources/stubs/Random/CryptoSafeEngine.php create mode 100644 3rdparty/symfony/polyfill-php82/Resources/stubs/Random/Engine.php create mode 100644 3rdparty/symfony/polyfill-php82/Resources/stubs/Random/Engine/Secure.php create mode 100644 3rdparty/symfony/polyfill-php82/Resources/stubs/Random/RandomError.php create mode 100644 3rdparty/symfony/polyfill-php82/Resources/stubs/Random/RandomException.php create mode 100644 3rdparty/symfony/polyfill-php82/Resources/stubs/SensitiveParameter.php create mode 100644 3rdparty/symfony/polyfill-php82/Resources/stubs/SensitiveParameterValue.php create mode 100644 3rdparty/symfony/polyfill-php82/SensitiveParameterValue.php create mode 100644 3rdparty/symfony/polyfill-php82/bootstrap.php create mode 100644 3rdparty/symfony/polyfill-php83/LICENSE create mode 100644 3rdparty/symfony/polyfill-php83/Php83.php create mode 100644 3rdparty/symfony/polyfill-php83/Resources/stubs/DateError.php create mode 100644 3rdparty/symfony/polyfill-php83/Resources/stubs/DateException.php create mode 100644 3rdparty/symfony/polyfill-php83/Resources/stubs/DateInvalidOperationException.php create mode 100644 3rdparty/symfony/polyfill-php83/Resources/stubs/DateInvalidTimeZoneException.php create mode 100644 3rdparty/symfony/polyfill-php83/Resources/stubs/DateMalformedIntervalStringException.php create mode 100644 3rdparty/symfony/polyfill-php83/Resources/stubs/DateMalformedPeriodStringException.php create mode 100644 3rdparty/symfony/polyfill-php83/Resources/stubs/DateMalformedStringException.php create mode 100644 3rdparty/symfony/polyfill-php83/Resources/stubs/DateObjectError.php create mode 100644 3rdparty/symfony/polyfill-php83/Resources/stubs/DateRangeError.php create mode 100644 3rdparty/symfony/polyfill-php83/Resources/stubs/Override.php create mode 100644 3rdparty/symfony/polyfill-php83/Resources/stubs/SQLite3Exception.php create mode 100644 3rdparty/symfony/polyfill-php83/bootstrap.php create mode 100644 3rdparty/symfony/polyfill-php83/bootstrap81.php create mode 100644 3rdparty/symfony/polyfill-php84/LICENSE create mode 100644 3rdparty/symfony/polyfill-php84/Php84.php create mode 100644 3rdparty/symfony/polyfill-php84/Resources/stubs/Deprecated.php create mode 100644 3rdparty/symfony/polyfill-php84/bootstrap.php create mode 100644 3rdparty/symfony/polyfill-uuid/LICENSE create mode 100644 3rdparty/symfony/polyfill-uuid/Uuid.php create mode 100644 3rdparty/symfony/polyfill-uuid/bootstrap.php create mode 100644 3rdparty/symfony/polyfill-uuid/bootstrap80.php create mode 100644 3rdparty/symfony/process/Exception/ExceptionInterface.php create mode 100644 3rdparty/symfony/process/Exception/InvalidArgumentException.php create mode 100644 3rdparty/symfony/process/Exception/LogicException.php create mode 100644 3rdparty/symfony/process/Exception/ProcessFailedException.php create mode 100644 3rdparty/symfony/process/Exception/ProcessSignaledException.php create mode 100644 3rdparty/symfony/process/Exception/ProcessTimedOutException.php create mode 100644 3rdparty/symfony/process/Exception/RunProcessFailedException.php create mode 100644 3rdparty/symfony/process/Exception/RuntimeException.php create mode 100644 3rdparty/symfony/process/ExecutableFinder.php create mode 100644 3rdparty/symfony/process/InputStream.php create mode 100644 3rdparty/symfony/process/LICENSE create mode 100644 3rdparty/symfony/process/Messenger/RunProcessContext.php create mode 100644 3rdparty/symfony/process/Messenger/RunProcessMessage.php create mode 100644 3rdparty/symfony/process/Messenger/RunProcessMessageHandler.php create mode 100644 3rdparty/symfony/process/PhpExecutableFinder.php create mode 100644 3rdparty/symfony/process/PhpProcess.php create mode 100644 3rdparty/symfony/process/PhpSubprocess.php create mode 100644 3rdparty/symfony/process/Pipes/AbstractPipes.php create mode 100644 3rdparty/symfony/process/Pipes/PipesInterface.php create mode 100644 3rdparty/symfony/process/Pipes/UnixPipes.php create mode 100644 3rdparty/symfony/process/Pipes/WindowsPipes.php create mode 100644 3rdparty/symfony/process/Process.php create mode 100644 3rdparty/symfony/process/ProcessUtils.php create mode 100644 3rdparty/symfony/routing/Alias.php create mode 100644 3rdparty/symfony/routing/Annotation/Route.php create mode 100644 3rdparty/symfony/routing/Attribute/Route.php create mode 100644 3rdparty/symfony/routing/CompiledRoute.php create mode 100644 3rdparty/symfony/routing/DependencyInjection/AddExpressionLanguageProvidersPass.php create mode 100644 3rdparty/symfony/routing/DependencyInjection/RoutingResolverPass.php create mode 100644 3rdparty/symfony/routing/Exception/ExceptionInterface.php create mode 100644 3rdparty/symfony/routing/Exception/InvalidArgumentException.php create mode 100644 3rdparty/symfony/routing/Exception/InvalidParameterException.php create mode 100644 3rdparty/symfony/routing/Exception/MethodNotAllowedException.php create mode 100644 3rdparty/symfony/routing/Exception/MissingMandatoryParametersException.php create mode 100644 3rdparty/symfony/routing/Exception/NoConfigurationException.php create mode 100644 3rdparty/symfony/routing/Exception/ResourceNotFoundException.php create mode 100644 3rdparty/symfony/routing/Exception/RouteCircularReferenceException.php create mode 100644 3rdparty/symfony/routing/Exception/RouteNotFoundException.php create mode 100644 3rdparty/symfony/routing/Exception/RuntimeException.php create mode 100644 3rdparty/symfony/routing/Generator/CompiledUrlGenerator.php create mode 100644 3rdparty/symfony/routing/Generator/ConfigurableRequirementsInterface.php create mode 100644 3rdparty/symfony/routing/Generator/Dumper/CompiledUrlGeneratorDumper.php create mode 100644 3rdparty/symfony/routing/Generator/Dumper/GeneratorDumper.php create mode 100644 3rdparty/symfony/routing/Generator/Dumper/GeneratorDumperInterface.php create mode 100644 3rdparty/symfony/routing/Generator/UrlGenerator.php create mode 100644 3rdparty/symfony/routing/Generator/UrlGeneratorInterface.php create mode 100644 3rdparty/symfony/routing/LICENSE create mode 100644 3rdparty/symfony/routing/Loader/AnnotationClassLoader.php create mode 100644 3rdparty/symfony/routing/Loader/AnnotationDirectoryLoader.php create mode 100644 3rdparty/symfony/routing/Loader/AnnotationFileLoader.php create mode 100644 3rdparty/symfony/routing/Loader/AttributeClassLoader.php create mode 100644 3rdparty/symfony/routing/Loader/AttributeDirectoryLoader.php create mode 100644 3rdparty/symfony/routing/Loader/AttributeFileLoader.php create mode 100644 3rdparty/symfony/routing/Loader/ClosureLoader.php create mode 100644 3rdparty/symfony/routing/Loader/Configurator/AliasConfigurator.php create mode 100644 3rdparty/symfony/routing/Loader/Configurator/CollectionConfigurator.php create mode 100644 3rdparty/symfony/routing/Loader/Configurator/ImportConfigurator.php create mode 100644 3rdparty/symfony/routing/Loader/Configurator/RouteConfigurator.php create mode 100644 3rdparty/symfony/routing/Loader/Configurator/RoutingConfigurator.php create mode 100644 3rdparty/symfony/routing/Loader/Configurator/Traits/AddTrait.php create mode 100644 3rdparty/symfony/routing/Loader/Configurator/Traits/HostTrait.php create mode 100644 3rdparty/symfony/routing/Loader/Configurator/Traits/LocalizedRouteTrait.php create mode 100644 3rdparty/symfony/routing/Loader/Configurator/Traits/PrefixTrait.php create mode 100644 3rdparty/symfony/routing/Loader/Configurator/Traits/RouteTrait.php create mode 100644 3rdparty/symfony/routing/Loader/ContainerLoader.php create mode 100644 3rdparty/symfony/routing/Loader/DirectoryLoader.php create mode 100644 3rdparty/symfony/routing/Loader/GlobFileLoader.php create mode 100644 3rdparty/symfony/routing/Loader/ObjectLoader.php create mode 100644 3rdparty/symfony/routing/Loader/PhpFileLoader.php create mode 100644 3rdparty/symfony/routing/Loader/Psr4DirectoryLoader.php create mode 100644 3rdparty/symfony/routing/Loader/XmlFileLoader.php create mode 100644 3rdparty/symfony/routing/Loader/YamlFileLoader.php create mode 100644 3rdparty/symfony/routing/Loader/schema/routing/routing-1.0.xsd create mode 100644 3rdparty/symfony/routing/Matcher/CompiledUrlMatcher.php create mode 100644 3rdparty/symfony/routing/Matcher/Dumper/CompiledUrlMatcherDumper.php create mode 100644 3rdparty/symfony/routing/Matcher/Dumper/CompiledUrlMatcherTrait.php create mode 100644 3rdparty/symfony/routing/Matcher/Dumper/MatcherDumper.php create mode 100644 3rdparty/symfony/routing/Matcher/Dumper/MatcherDumperInterface.php create mode 100644 3rdparty/symfony/routing/Matcher/Dumper/StaticPrefixCollection.php create mode 100644 3rdparty/symfony/routing/Matcher/ExpressionLanguageProvider.php create mode 100644 3rdparty/symfony/routing/Matcher/RedirectableUrlMatcher.php create mode 100644 3rdparty/symfony/routing/Matcher/RedirectableUrlMatcherInterface.php create mode 100644 3rdparty/symfony/routing/Matcher/RequestMatcherInterface.php create mode 100644 3rdparty/symfony/routing/Matcher/TraceableUrlMatcher.php create mode 100644 3rdparty/symfony/routing/Matcher/UrlMatcher.php create mode 100644 3rdparty/symfony/routing/Matcher/UrlMatcherInterface.php create mode 100644 3rdparty/symfony/routing/RequestContext.php create mode 100644 3rdparty/symfony/routing/RequestContextAwareInterface.php create mode 100644 3rdparty/symfony/routing/Requirement/EnumRequirement.php create mode 100644 3rdparty/symfony/routing/Requirement/Requirement.php create mode 100644 3rdparty/symfony/routing/Route.php create mode 100644 3rdparty/symfony/routing/RouteCollection.php create mode 100644 3rdparty/symfony/routing/RouteCompiler.php create mode 100644 3rdparty/symfony/routing/RouteCompilerInterface.php create mode 100644 3rdparty/symfony/routing/Router.php create mode 100644 3rdparty/symfony/routing/RouterInterface.php create mode 100644 3rdparty/symfony/service-contracts/Attribute/Required.php create mode 100644 3rdparty/symfony/service-contracts/Attribute/SubscribedService.php create mode 100644 3rdparty/symfony/service-contracts/LICENSE create mode 100644 3rdparty/symfony/service-contracts/ResetInterface.php create mode 100644 3rdparty/symfony/service-contracts/ServiceCollectionInterface.php create mode 100644 3rdparty/symfony/service-contracts/ServiceLocatorTrait.php create mode 100644 3rdparty/symfony/service-contracts/ServiceMethodsSubscriberTrait.php create mode 100644 3rdparty/symfony/service-contracts/ServiceProviderInterface.php create mode 100644 3rdparty/symfony/service-contracts/ServiceSubscriberInterface.php create mode 100644 3rdparty/symfony/service-contracts/ServiceSubscriberTrait.php create mode 100644 3rdparty/symfony/string/AbstractString.php create mode 100644 3rdparty/symfony/string/AbstractUnicodeString.php create mode 100644 3rdparty/symfony/string/ByteString.php create mode 100644 3rdparty/symfony/string/CodePointString.php create mode 100644 3rdparty/symfony/string/Exception/ExceptionInterface.php create mode 100644 3rdparty/symfony/string/Exception/InvalidArgumentException.php create mode 100644 3rdparty/symfony/string/Exception/RuntimeException.php create mode 100644 3rdparty/symfony/string/Inflector/EnglishInflector.php create mode 100644 3rdparty/symfony/string/Inflector/FrenchInflector.php create mode 100644 3rdparty/symfony/string/Inflector/InflectorInterface.php create mode 100644 3rdparty/symfony/string/LICENSE create mode 100644 3rdparty/symfony/string/LazyString.php create mode 100644 3rdparty/symfony/string/Resources/functions.php create mode 100644 3rdparty/symfony/string/Slugger/AsciiSlugger.php create mode 100644 3rdparty/symfony/string/Slugger/SluggerInterface.php create mode 100644 3rdparty/symfony/string/UnicodeString.php create mode 100644 3rdparty/symfony/translation-contracts/LICENSE create mode 100644 3rdparty/symfony/translation-contracts/LocaleAwareInterface.php create mode 100644 3rdparty/symfony/translation-contracts/TranslatableInterface.php create mode 100644 3rdparty/symfony/translation-contracts/TranslatorInterface.php create mode 100644 3rdparty/symfony/translation-contracts/TranslatorTrait.php create mode 100644 3rdparty/symfony/translation/Catalogue/AbstractOperation.php create mode 100644 3rdparty/symfony/translation/Catalogue/MergeOperation.php create mode 100644 3rdparty/symfony/translation/Catalogue/OperationInterface.php create mode 100644 3rdparty/symfony/translation/Catalogue/TargetOperation.php create mode 100644 3rdparty/symfony/translation/CatalogueMetadataAwareInterface.php create mode 100644 3rdparty/symfony/translation/Command/TranslationPullCommand.php create mode 100644 3rdparty/symfony/translation/Command/TranslationPushCommand.php create mode 100644 3rdparty/symfony/translation/Command/TranslationTrait.php create mode 100644 3rdparty/symfony/translation/Command/XliffLintCommand.php create mode 100644 3rdparty/symfony/translation/DataCollector/TranslationDataCollector.php create mode 100644 3rdparty/symfony/translation/DataCollectorTranslator.php create mode 100644 3rdparty/symfony/translation/DependencyInjection/DataCollectorTranslatorPass.php create mode 100644 3rdparty/symfony/translation/DependencyInjection/LoggingTranslatorPass.php create mode 100644 3rdparty/symfony/translation/DependencyInjection/TranslationDumperPass.php create mode 100644 3rdparty/symfony/translation/DependencyInjection/TranslationExtractorPass.php create mode 100644 3rdparty/symfony/translation/DependencyInjection/TranslatorPass.php create mode 100644 3rdparty/symfony/translation/DependencyInjection/TranslatorPathsPass.php create mode 100644 3rdparty/symfony/translation/Dumper/CsvFileDumper.php create mode 100644 3rdparty/symfony/translation/Dumper/DumperInterface.php create mode 100644 3rdparty/symfony/translation/Dumper/FileDumper.php create mode 100644 3rdparty/symfony/translation/Dumper/IcuResFileDumper.php create mode 100644 3rdparty/symfony/translation/Dumper/IniFileDumper.php create mode 100644 3rdparty/symfony/translation/Dumper/JsonFileDumper.php create mode 100644 3rdparty/symfony/translation/Dumper/MoFileDumper.php create mode 100644 3rdparty/symfony/translation/Dumper/PhpFileDumper.php create mode 100644 3rdparty/symfony/translation/Dumper/PoFileDumper.php create mode 100644 3rdparty/symfony/translation/Dumper/QtFileDumper.php create mode 100644 3rdparty/symfony/translation/Dumper/XliffFileDumper.php create mode 100644 3rdparty/symfony/translation/Dumper/YamlFileDumper.php create mode 100644 3rdparty/symfony/translation/Exception/ExceptionInterface.php create mode 100644 3rdparty/symfony/translation/Exception/IncompleteDsnException.php create mode 100644 3rdparty/symfony/translation/Exception/InvalidArgumentException.php create mode 100644 3rdparty/symfony/translation/Exception/InvalidResourceException.php create mode 100644 3rdparty/symfony/translation/Exception/LogicException.php create mode 100644 3rdparty/symfony/translation/Exception/MissingRequiredOptionException.php create mode 100644 3rdparty/symfony/translation/Exception/NotFoundResourceException.php create mode 100644 3rdparty/symfony/translation/Exception/ProviderException.php create mode 100644 3rdparty/symfony/translation/Exception/ProviderExceptionInterface.php create mode 100644 3rdparty/symfony/translation/Exception/RuntimeException.php create mode 100644 3rdparty/symfony/translation/Exception/UnsupportedSchemeException.php create mode 100644 3rdparty/symfony/translation/Extractor/AbstractFileExtractor.php create mode 100644 3rdparty/symfony/translation/Extractor/ChainExtractor.php create mode 100644 3rdparty/symfony/translation/Extractor/ExtractorInterface.php create mode 100644 3rdparty/symfony/translation/Extractor/PhpAstExtractor.php create mode 100644 3rdparty/symfony/translation/Extractor/PhpExtractor.php create mode 100644 3rdparty/symfony/translation/Extractor/PhpStringTokenParser.php create mode 100644 3rdparty/symfony/translation/Extractor/Visitor/AbstractVisitor.php create mode 100644 3rdparty/symfony/translation/Extractor/Visitor/ConstraintVisitor.php create mode 100644 3rdparty/symfony/translation/Extractor/Visitor/TransMethodVisitor.php create mode 100644 3rdparty/symfony/translation/Extractor/Visitor/TranslatableMessageVisitor.php create mode 100644 3rdparty/symfony/translation/Formatter/IntlFormatter.php create mode 100644 3rdparty/symfony/translation/Formatter/IntlFormatterInterface.php create mode 100644 3rdparty/symfony/translation/Formatter/MessageFormatter.php create mode 100644 3rdparty/symfony/translation/Formatter/MessageFormatterInterface.php create mode 100644 3rdparty/symfony/translation/IdentityTranslator.php create mode 100644 3rdparty/symfony/translation/LICENSE create mode 100644 3rdparty/symfony/translation/Loader/ArrayLoader.php create mode 100644 3rdparty/symfony/translation/Loader/CsvFileLoader.php create mode 100644 3rdparty/symfony/translation/Loader/FileLoader.php create mode 100644 3rdparty/symfony/translation/Loader/IcuDatFileLoader.php create mode 100644 3rdparty/symfony/translation/Loader/IcuResFileLoader.php create mode 100644 3rdparty/symfony/translation/Loader/IniFileLoader.php create mode 100644 3rdparty/symfony/translation/Loader/JsonFileLoader.php create mode 100644 3rdparty/symfony/translation/Loader/LoaderInterface.php create mode 100644 3rdparty/symfony/translation/Loader/MoFileLoader.php create mode 100644 3rdparty/symfony/translation/Loader/PhpFileLoader.php create mode 100644 3rdparty/symfony/translation/Loader/PoFileLoader.php create mode 100644 3rdparty/symfony/translation/Loader/QtFileLoader.php create mode 100644 3rdparty/symfony/translation/Loader/XliffFileLoader.php create mode 100644 3rdparty/symfony/translation/Loader/YamlFileLoader.php create mode 100644 3rdparty/symfony/translation/LocaleSwitcher.php create mode 100644 3rdparty/symfony/translation/LoggingTranslator.php create mode 100644 3rdparty/symfony/translation/MessageCatalogue.php create mode 100644 3rdparty/symfony/translation/MessageCatalogueInterface.php create mode 100644 3rdparty/symfony/translation/MetadataAwareInterface.php create mode 100644 3rdparty/symfony/translation/Provider/AbstractProviderFactory.php create mode 100644 3rdparty/symfony/translation/Provider/Dsn.php create mode 100644 3rdparty/symfony/translation/Provider/FilteringProvider.php create mode 100644 3rdparty/symfony/translation/Provider/NullProvider.php create mode 100644 3rdparty/symfony/translation/Provider/NullProviderFactory.php create mode 100644 3rdparty/symfony/translation/Provider/ProviderFactoryInterface.php create mode 100644 3rdparty/symfony/translation/Provider/ProviderInterface.php create mode 100644 3rdparty/symfony/translation/Provider/TranslationProviderCollection.php create mode 100644 3rdparty/symfony/translation/Provider/TranslationProviderCollectionFactory.php create mode 100644 3rdparty/symfony/translation/PseudoLocalizationTranslator.php create mode 100644 3rdparty/symfony/translation/Reader/TranslationReader.php create mode 100644 3rdparty/symfony/translation/Reader/TranslationReaderInterface.php create mode 100644 3rdparty/symfony/translation/Resources/functions.php create mode 100644 3rdparty/symfony/translation/Resources/schemas/xliff-core-1.2-transitional.xsd create mode 100644 3rdparty/symfony/translation/Resources/schemas/xliff-core-2.0.xsd create mode 100644 3rdparty/symfony/translation/Resources/schemas/xml.xsd create mode 100644 3rdparty/symfony/translation/TranslatableMessage.php create mode 100644 3rdparty/symfony/translation/Translator.php create mode 100644 3rdparty/symfony/translation/TranslatorBag.php create mode 100644 3rdparty/symfony/translation/TranslatorBagInterface.php create mode 100644 3rdparty/symfony/translation/Util/ArrayConverter.php create mode 100644 3rdparty/symfony/translation/Util/XliffUtils.php create mode 100644 3rdparty/symfony/translation/Writer/TranslationWriter.php create mode 100644 3rdparty/symfony/translation/Writer/TranslationWriterInterface.php create mode 100644 3rdparty/symfony/uid/AbstractUid.php create mode 100644 3rdparty/symfony/uid/BinaryUtil.php create mode 100644 3rdparty/symfony/uid/Command/GenerateUlidCommand.php create mode 100644 3rdparty/symfony/uid/Command/GenerateUuidCommand.php create mode 100644 3rdparty/symfony/uid/Command/InspectUlidCommand.php create mode 100644 3rdparty/symfony/uid/Command/InspectUuidCommand.php create mode 100644 3rdparty/symfony/uid/Factory/NameBasedUuidFactory.php create mode 100644 3rdparty/symfony/uid/Factory/RandomBasedUuidFactory.php create mode 100644 3rdparty/symfony/uid/Factory/TimeBasedUuidFactory.php create mode 100644 3rdparty/symfony/uid/Factory/UlidFactory.php create mode 100644 3rdparty/symfony/uid/Factory/UuidFactory.php create mode 100644 3rdparty/symfony/uid/LICENSE create mode 100644 3rdparty/symfony/uid/MaxUlid.php create mode 100644 3rdparty/symfony/uid/MaxUuid.php create mode 100644 3rdparty/symfony/uid/NilUlid.php create mode 100644 3rdparty/symfony/uid/NilUuid.php create mode 100644 3rdparty/symfony/uid/TimeBasedUidInterface.php create mode 100644 3rdparty/symfony/uid/Ulid.php create mode 100644 3rdparty/symfony/uid/Uuid.php create mode 100644 3rdparty/symfony/uid/UuidV1.php create mode 100644 3rdparty/symfony/uid/UuidV3.php create mode 100644 3rdparty/symfony/uid/UuidV4.php create mode 100644 3rdparty/symfony/uid/UuidV5.php create mode 100644 3rdparty/symfony/uid/UuidV6.php create mode 100644 3rdparty/symfony/uid/UuidV7.php create mode 100644 3rdparty/symfony/uid/UuidV8.php create mode 100644 3rdparty/wapmorgan/mp3info/LICENSE create mode 100644 3rdparty/wapmorgan/mp3info/PATCHES.txt create mode 100644 3rdparty/wapmorgan/mp3info/src/Mp3Info.php create mode 100644 3rdparty/web-auth/cose-lib/LICENSE create mode 100644 3rdparty/web-auth/cose-lib/src/Algorithm/Algorithm.php create mode 100644 3rdparty/web-auth/cose-lib/src/Algorithm/Mac/HS256.php create mode 100644 3rdparty/web-auth/cose-lib/src/Algorithm/Mac/HS256Truncated64.php create mode 100644 3rdparty/web-auth/cose-lib/src/Algorithm/Mac/HS384.php create mode 100644 3rdparty/web-auth/cose-lib/src/Algorithm/Mac/HS512.php create mode 100644 3rdparty/web-auth/cose-lib/src/Algorithm/Mac/Hmac.php create mode 100644 3rdparty/web-auth/cose-lib/src/Algorithm/Mac/Mac.php create mode 100644 3rdparty/web-auth/cose-lib/src/Algorithm/Manager.php create mode 100644 3rdparty/web-auth/cose-lib/src/Algorithm/ManagerFactory.php create mode 100644 3rdparty/web-auth/cose-lib/src/Algorithm/Signature/ECDSA/ECDSA.php create mode 100644 3rdparty/web-auth/cose-lib/src/Algorithm/Signature/ECDSA/ECSignature.php create mode 100644 3rdparty/web-auth/cose-lib/src/Algorithm/Signature/ECDSA/ES256.php create mode 100644 3rdparty/web-auth/cose-lib/src/Algorithm/Signature/ECDSA/ES256K.php create mode 100644 3rdparty/web-auth/cose-lib/src/Algorithm/Signature/ECDSA/ES384.php create mode 100644 3rdparty/web-auth/cose-lib/src/Algorithm/Signature/ECDSA/ES512.php create mode 100644 3rdparty/web-auth/cose-lib/src/Algorithm/Signature/EdDSA/Ed25519.php create mode 100644 3rdparty/web-auth/cose-lib/src/Algorithm/Signature/EdDSA/Ed256.php create mode 100644 3rdparty/web-auth/cose-lib/src/Algorithm/Signature/EdDSA/Ed512.php create mode 100644 3rdparty/web-auth/cose-lib/src/Algorithm/Signature/EdDSA/EdDSA.php create mode 100644 3rdparty/web-auth/cose-lib/src/Algorithm/Signature/RSA/PS256.php create mode 100644 3rdparty/web-auth/cose-lib/src/Algorithm/Signature/RSA/PS384.php create mode 100644 3rdparty/web-auth/cose-lib/src/Algorithm/Signature/RSA/PS512.php create mode 100644 3rdparty/web-auth/cose-lib/src/Algorithm/Signature/RSA/PSSRSA.php create mode 100644 3rdparty/web-auth/cose-lib/src/Algorithm/Signature/RSA/RS1.php create mode 100644 3rdparty/web-auth/cose-lib/src/Algorithm/Signature/RSA/RS256.php create mode 100644 3rdparty/web-auth/cose-lib/src/Algorithm/Signature/RSA/RS384.php create mode 100644 3rdparty/web-auth/cose-lib/src/Algorithm/Signature/RSA/RS512.php create mode 100644 3rdparty/web-auth/cose-lib/src/Algorithm/Signature/RSA/RSA.php create mode 100644 3rdparty/web-auth/cose-lib/src/Algorithm/Signature/Signature.php create mode 100644 3rdparty/web-auth/cose-lib/src/Algorithms.php create mode 100644 3rdparty/web-auth/cose-lib/src/BigInteger.php create mode 100644 3rdparty/web-auth/cose-lib/src/Hash.php create mode 100644 3rdparty/web-auth/cose-lib/src/Key/Ec2Key.php create mode 100644 3rdparty/web-auth/cose-lib/src/Key/Key.php create mode 100644 3rdparty/web-auth/cose-lib/src/Key/OkpKey.php create mode 100644 3rdparty/web-auth/cose-lib/src/Key/RsaKey.php create mode 100644 3rdparty/web-auth/cose-lib/src/Key/SymmetricKey.php create mode 100644 3rdparty/web-auth/webauthn-lib/LICENSE create mode 100644 3rdparty/web-auth/webauthn-lib/src/AttestationStatement/AndroidKeyAttestationStatementSupport.php create mode 100644 3rdparty/web-auth/webauthn-lib/src/AttestationStatement/AndroidSafetyNetAttestationStatementSupport.php create mode 100644 3rdparty/web-auth/webauthn-lib/src/AttestationStatement/AppleAttestationStatementSupport.php create mode 100644 3rdparty/web-auth/webauthn-lib/src/AttestationStatement/AttestationObject.php create mode 100644 3rdparty/web-auth/webauthn-lib/src/AttestationStatement/AttestationObjectLoader.php create mode 100644 3rdparty/web-auth/webauthn-lib/src/AttestationStatement/AttestationStatement.php create mode 100644 3rdparty/web-auth/webauthn-lib/src/AttestationStatement/AttestationStatementSupport.php create mode 100644 3rdparty/web-auth/webauthn-lib/src/AttestationStatement/AttestationStatementSupportManager.php create mode 100644 3rdparty/web-auth/webauthn-lib/src/AttestationStatement/FidoU2FAttestationStatementSupport.php create mode 100644 3rdparty/web-auth/webauthn-lib/src/AttestationStatement/NoneAttestationStatementSupport.php create mode 100644 3rdparty/web-auth/webauthn-lib/src/AttestationStatement/PackedAttestationStatementSupport.php create mode 100644 3rdparty/web-auth/webauthn-lib/src/AttestationStatement/TPMAttestationStatementSupport.php create mode 100644 3rdparty/web-auth/webauthn-lib/src/AttestedCredentialData.php create mode 100644 3rdparty/web-auth/webauthn-lib/src/AuthenticationExtensions/AuthenticationExtension.php create mode 100644 3rdparty/web-auth/webauthn-lib/src/AuthenticationExtensions/AuthenticationExtensions.php create mode 100644 3rdparty/web-auth/webauthn-lib/src/AuthenticationExtensions/AuthenticationExtensionsClientInputs.php create mode 100644 3rdparty/web-auth/webauthn-lib/src/AuthenticationExtensions/AuthenticationExtensionsClientOutputs.php create mode 100644 3rdparty/web-auth/webauthn-lib/src/AuthenticationExtensions/AuthenticationExtensionsClientOutputsLoader.php create mode 100644 3rdparty/web-auth/webauthn-lib/src/AuthenticationExtensions/ExtensionOutputChecker.php create mode 100644 3rdparty/web-auth/webauthn-lib/src/AuthenticationExtensions/ExtensionOutputCheckerHandler.php create mode 100644 3rdparty/web-auth/webauthn-lib/src/AuthenticationExtensions/ExtensionOutputError.php create mode 100644 3rdparty/web-auth/webauthn-lib/src/AuthenticatorAssertionResponse.php create mode 100644 3rdparty/web-auth/webauthn-lib/src/AuthenticatorAssertionResponseValidator.php create mode 100644 3rdparty/web-auth/webauthn-lib/src/AuthenticatorAttestationResponse.php create mode 100644 3rdparty/web-auth/webauthn-lib/src/AuthenticatorAttestationResponseValidator.php create mode 100644 3rdparty/web-auth/webauthn-lib/src/AuthenticatorData.php create mode 100644 3rdparty/web-auth/webauthn-lib/src/AuthenticatorDataLoader.php create mode 100644 3rdparty/web-auth/webauthn-lib/src/AuthenticatorResponse.php create mode 100644 3rdparty/web-auth/webauthn-lib/src/AuthenticatorSelectionCriteria.php create mode 100644 3rdparty/web-auth/webauthn-lib/src/CeremonyStep/CeremonyStep.php create mode 100644 3rdparty/web-auth/webauthn-lib/src/CeremonyStep/CeremonyStepManager.php create mode 100644 3rdparty/web-auth/webauthn-lib/src/CeremonyStep/CeremonyStepManagerFactory.php create mode 100644 3rdparty/web-auth/webauthn-lib/src/CeremonyStep/CheckAlgorithm.php create mode 100644 3rdparty/web-auth/webauthn-lib/src/CeremonyStep/CheckAllowedCredentialList.php create mode 100644 3rdparty/web-auth/webauthn-lib/src/CeremonyStep/CheckAttestationFormatIsKnownAndValid.php create mode 100644 3rdparty/web-auth/webauthn-lib/src/CeremonyStep/CheckBackupBitsAreConsistent.php create mode 100644 3rdparty/web-auth/webauthn-lib/src/CeremonyStep/CheckChallenge.php create mode 100644 3rdparty/web-auth/webauthn-lib/src/CeremonyStep/CheckClientDataCollectorType.php create mode 100644 3rdparty/web-auth/webauthn-lib/src/CeremonyStep/CheckCounter.php create mode 100644 3rdparty/web-auth/webauthn-lib/src/CeremonyStep/CheckCredentialId.php create mode 100644 3rdparty/web-auth/webauthn-lib/src/CeremonyStep/CheckExtensions.php create mode 100644 3rdparty/web-auth/webauthn-lib/src/CeremonyStep/CheckHasAttestedCredentialData.php create mode 100644 3rdparty/web-auth/webauthn-lib/src/CeremonyStep/CheckMetadataStatement.php create mode 100644 3rdparty/web-auth/webauthn-lib/src/CeremonyStep/CheckOrigin.php create mode 100644 3rdparty/web-auth/webauthn-lib/src/CeremonyStep/CheckRelyingPartyIdIdHash.php create mode 100644 3rdparty/web-auth/webauthn-lib/src/CeremonyStep/CheckSignature.php create mode 100644 3rdparty/web-auth/webauthn-lib/src/CeremonyStep/CheckTopOrigin.php create mode 100644 3rdparty/web-auth/webauthn-lib/src/CeremonyStep/CheckUserHandle.php create mode 100644 3rdparty/web-auth/webauthn-lib/src/CeremonyStep/CheckUserVerification.php create mode 100644 3rdparty/web-auth/webauthn-lib/src/CeremonyStep/CheckUserWasPresent.php create mode 100644 3rdparty/web-auth/webauthn-lib/src/CeremonyStep/HostTopOriginValidator.php create mode 100644 3rdparty/web-auth/webauthn-lib/src/CeremonyStep/TopOriginValidator.php create mode 100644 3rdparty/web-auth/webauthn-lib/src/CertificateChainChecker/CertificateChainChecker.php create mode 100644 3rdparty/web-auth/webauthn-lib/src/CertificateChainChecker/PhpCertificateChainChecker.php create mode 100644 3rdparty/web-auth/webauthn-lib/src/CertificateToolbox.php create mode 100644 3rdparty/web-auth/webauthn-lib/src/ClientDataCollector/ClientDataCollector.php create mode 100644 3rdparty/web-auth/webauthn-lib/src/ClientDataCollector/ClientDataCollectorManager.php create mode 100644 3rdparty/web-auth/webauthn-lib/src/ClientDataCollector/WebauthnAuthenticationCollector.php create mode 100644 3rdparty/web-auth/webauthn-lib/src/CollectedClientData.php create mode 100644 3rdparty/web-auth/webauthn-lib/src/Counter/CounterChecker.php create mode 100644 3rdparty/web-auth/webauthn-lib/src/Counter/ThrowExceptionIfInvalid.php create mode 100644 3rdparty/web-auth/webauthn-lib/src/Credential.php create mode 100644 3rdparty/web-auth/webauthn-lib/src/Denormalizer/AttestationObjectDenormalizer.php create mode 100644 3rdparty/web-auth/webauthn-lib/src/Denormalizer/AttestationStatementDenormalizer.php create mode 100644 3rdparty/web-auth/webauthn-lib/src/Denormalizer/AttestedCredentialDataNormalizer.php create mode 100644 3rdparty/web-auth/webauthn-lib/src/Denormalizer/AuthenticationExtensionNormalizer.php create mode 100644 3rdparty/web-auth/webauthn-lib/src/Denormalizer/AuthenticationExtensionsDenormalizer.php create mode 100644 3rdparty/web-auth/webauthn-lib/src/Denormalizer/AuthenticatorAssertionResponseDenormalizer.php create mode 100644 3rdparty/web-auth/webauthn-lib/src/Denormalizer/AuthenticatorAttestationResponseDenormalizer.php create mode 100644 3rdparty/web-auth/webauthn-lib/src/Denormalizer/AuthenticatorDataDenormalizer.php create mode 100644 3rdparty/web-auth/webauthn-lib/src/Denormalizer/AuthenticatorResponseDenormalizer.php create mode 100644 3rdparty/web-auth/webauthn-lib/src/Denormalizer/CollectedClientDataDenormalizer.php create mode 100644 3rdparty/web-auth/webauthn-lib/src/Denormalizer/ExtensionDescriptorDenormalizer.php create mode 100644 3rdparty/web-auth/webauthn-lib/src/Denormalizer/PublicKeyCredentialDenormalizer.php create mode 100644 3rdparty/web-auth/webauthn-lib/src/Denormalizer/PublicKeyCredentialDescriptorNormalizer.php create mode 100644 3rdparty/web-auth/webauthn-lib/src/Denormalizer/PublicKeyCredentialOptionsDenormalizer.php create mode 100644 3rdparty/web-auth/webauthn-lib/src/Denormalizer/PublicKeyCredentialParametersDenormalizer.php create mode 100644 3rdparty/web-auth/webauthn-lib/src/Denormalizer/PublicKeyCredentialSourceDenormalizer.php create mode 100644 3rdparty/web-auth/webauthn-lib/src/Denormalizer/PublicKeyCredentialUserEntityDenormalizer.php create mode 100644 3rdparty/web-auth/webauthn-lib/src/Denormalizer/TrustPathDenormalizer.php create mode 100644 3rdparty/web-auth/webauthn-lib/src/Denormalizer/VerificationMethodANDCombinationsDenormalizer.php create mode 100644 3rdparty/web-auth/webauthn-lib/src/Denormalizer/WebauthnSerializerFactory.php create mode 100644 3rdparty/web-auth/webauthn-lib/src/Event/AttestationObjectLoaded.php create mode 100644 3rdparty/web-auth/webauthn-lib/src/Event/AttestationStatementLoaded.php create mode 100644 3rdparty/web-auth/webauthn-lib/src/Event/AuthenticatorAssertionResponseValidationFailedEvent.php create mode 100644 3rdparty/web-auth/webauthn-lib/src/Event/AuthenticatorAssertionResponseValidationSucceededEvent.php create mode 100644 3rdparty/web-auth/webauthn-lib/src/Event/AuthenticatorAttestationResponseValidationFailedEvent.php create mode 100644 3rdparty/web-auth/webauthn-lib/src/Event/AuthenticatorAttestationResponseValidationSucceededEvent.php create mode 100644 3rdparty/web-auth/webauthn-lib/src/Event/BeforeCertificateChainValidation.php create mode 100644 3rdparty/web-auth/webauthn-lib/src/Event/CanDispatchEvents.php create mode 100644 3rdparty/web-auth/webauthn-lib/src/Event/CertificateChainValidationFailed.php create mode 100644 3rdparty/web-auth/webauthn-lib/src/Event/CertificateChainValidationSucceeded.php create mode 100644 3rdparty/web-auth/webauthn-lib/src/Event/MetadataStatementFound.php create mode 100644 3rdparty/web-auth/webauthn-lib/src/Event/NullEventDispatcher.php create mode 100644 3rdparty/web-auth/webauthn-lib/src/Event/WebauthnEvent.php create mode 100644 3rdparty/web-auth/webauthn-lib/src/Exception/AttestationStatementException.php create mode 100644 3rdparty/web-auth/webauthn-lib/src/Exception/AttestationStatementLoadingException.php create mode 100644 3rdparty/web-auth/webauthn-lib/src/Exception/AttestationStatementVerificationException.php create mode 100644 3rdparty/web-auth/webauthn-lib/src/Exception/AuthenticationExtensionException.php create mode 100644 3rdparty/web-auth/webauthn-lib/src/Exception/AuthenticatorResponseVerificationException.php create mode 100644 3rdparty/web-auth/webauthn-lib/src/Exception/CertificateChainException.php create mode 100644 3rdparty/web-auth/webauthn-lib/src/Exception/CertificateException.php create mode 100644 3rdparty/web-auth/webauthn-lib/src/Exception/CertificateRevocationListException.php create mode 100644 3rdparty/web-auth/webauthn-lib/src/Exception/CounterException.php create mode 100644 3rdparty/web-auth/webauthn-lib/src/Exception/ExpiredCertificateException.php create mode 100644 3rdparty/web-auth/webauthn-lib/src/Exception/InvalidAttestationStatementException.php create mode 100644 3rdparty/web-auth/webauthn-lib/src/Exception/InvalidCertificateException.php create mode 100644 3rdparty/web-auth/webauthn-lib/src/Exception/InvalidDataException.php create mode 100644 3rdparty/web-auth/webauthn-lib/src/Exception/InvalidTrustPathException.php create mode 100644 3rdparty/web-auth/webauthn-lib/src/Exception/InvalidUserHandleException.php create mode 100644 3rdparty/web-auth/webauthn-lib/src/Exception/MetadataServiceException.php create mode 100644 3rdparty/web-auth/webauthn-lib/src/Exception/MetadataStatementException.php create mode 100644 3rdparty/web-auth/webauthn-lib/src/Exception/MetadataStatementLoadingException.php create mode 100644 3rdparty/web-auth/webauthn-lib/src/Exception/MissingMetadataStatementException.php create mode 100644 3rdparty/web-auth/webauthn-lib/src/Exception/RevokedCertificateException.php create mode 100644 3rdparty/web-auth/webauthn-lib/src/Exception/UnsupportedFeatureException.php create mode 100644 3rdparty/web-auth/webauthn-lib/src/Exception/WebauthnException.php create mode 100644 3rdparty/web-auth/webauthn-lib/src/FakeCredentialGenerator.php create mode 100644 3rdparty/web-auth/webauthn-lib/src/MetadataService/CanLogData.php create mode 100644 3rdparty/web-auth/webauthn-lib/src/MetadataService/CertificateChain/CertificateChainValidator.php create mode 100644 3rdparty/web-auth/webauthn-lib/src/MetadataService/CertificateChain/CertificateToolbox.php create mode 100644 3rdparty/web-auth/webauthn-lib/src/MetadataService/CertificateChain/PhpCertificateChainValidator.php create mode 100644 3rdparty/web-auth/webauthn-lib/src/MetadataService/Denormalizer/ExtensionDescriptorDenormalizer.php create mode 100644 3rdparty/web-auth/webauthn-lib/src/MetadataService/Denormalizer/MetadataStatementSerializerFactory.php create mode 100644 3rdparty/web-auth/webauthn-lib/src/MetadataService/Event/BeforeCertificateChainValidation.php create mode 100644 3rdparty/web-auth/webauthn-lib/src/MetadataService/Event/CanDispatchEvents.php create mode 100644 3rdparty/web-auth/webauthn-lib/src/MetadataService/Event/CertificateChainValidationFailed.php create mode 100644 3rdparty/web-auth/webauthn-lib/src/MetadataService/Event/CertificateChainValidationSucceeded.php create mode 100644 3rdparty/web-auth/webauthn-lib/src/MetadataService/Event/MetadataStatementFound.php create mode 100644 3rdparty/web-auth/webauthn-lib/src/MetadataService/Event/NullEventDispatcher.php create mode 100644 3rdparty/web-auth/webauthn-lib/src/MetadataService/Event/WebauthnEvent.php create mode 100644 3rdparty/web-auth/webauthn-lib/src/MetadataService/Exception/CertificateChainException.php create mode 100644 3rdparty/web-auth/webauthn-lib/src/MetadataService/Exception/CertificateException.php create mode 100644 3rdparty/web-auth/webauthn-lib/src/MetadataService/Exception/CertificateRevocationListException.php create mode 100644 3rdparty/web-auth/webauthn-lib/src/MetadataService/Exception/ExpiredCertificateException.php create mode 100644 3rdparty/web-auth/webauthn-lib/src/MetadataService/Exception/InvalidCertificateException.php create mode 100644 3rdparty/web-auth/webauthn-lib/src/MetadataService/Exception/MetadataServiceException.php create mode 100644 3rdparty/web-auth/webauthn-lib/src/MetadataService/Exception/MetadataStatementException.php create mode 100644 3rdparty/web-auth/webauthn-lib/src/MetadataService/Exception/MetadataStatementLoadingException.php create mode 100644 3rdparty/web-auth/webauthn-lib/src/MetadataService/Exception/MissingMetadataStatementException.php create mode 100644 3rdparty/web-auth/webauthn-lib/src/MetadataService/Exception/RevokedCertificateException.php create mode 100644 3rdparty/web-auth/webauthn-lib/src/MetadataService/MetadataStatementRepository.php create mode 100644 3rdparty/web-auth/webauthn-lib/src/MetadataService/Psr18HttpClient.php create mode 100644 3rdparty/web-auth/webauthn-lib/src/MetadataService/Service/ChainedMetadataServices.php create mode 100644 3rdparty/web-auth/webauthn-lib/src/MetadataService/Service/DistantResourceMetadataService.php create mode 100644 3rdparty/web-auth/webauthn-lib/src/MetadataService/Service/FidoAllianceCompliantMetadataService.php create mode 100644 3rdparty/web-auth/webauthn-lib/src/MetadataService/Service/FolderResourceMetadataService.php create mode 100644 3rdparty/web-auth/webauthn-lib/src/MetadataService/Service/InMemoryMetadataService.php create mode 100644 3rdparty/web-auth/webauthn-lib/src/MetadataService/Service/JsonMetadataService.php create mode 100644 3rdparty/web-auth/webauthn-lib/src/MetadataService/Service/LocalResourceMetadataService.php create mode 100644 3rdparty/web-auth/webauthn-lib/src/MetadataService/Service/MetadataBLOBPayload.php create mode 100644 3rdparty/web-auth/webauthn-lib/src/MetadataService/Service/MetadataBLOBPayloadEntry.php create mode 100644 3rdparty/web-auth/webauthn-lib/src/MetadataService/Service/MetadataService.php create mode 100644 3rdparty/web-auth/webauthn-lib/src/MetadataService/Service/StringMetadataService.php create mode 100644 3rdparty/web-auth/webauthn-lib/src/MetadataService/Statement/AbstractDescriptor.php create mode 100644 3rdparty/web-auth/webauthn-lib/src/MetadataService/Statement/AlternativeDescriptions.php create mode 100644 3rdparty/web-auth/webauthn-lib/src/MetadataService/Statement/AuthenticatorGetInfo.php create mode 100644 3rdparty/web-auth/webauthn-lib/src/MetadataService/Statement/AuthenticatorStatus.php create mode 100644 3rdparty/web-auth/webauthn-lib/src/MetadataService/Statement/BiometricAccuracyDescriptor.php create mode 100644 3rdparty/web-auth/webauthn-lib/src/MetadataService/Statement/BiometricStatusReport.php create mode 100644 3rdparty/web-auth/webauthn-lib/src/MetadataService/Statement/CodeAccuracyDescriptor.php create mode 100644 3rdparty/web-auth/webauthn-lib/src/MetadataService/Statement/DisplayPNGCharacteristicsDescriptor.php create mode 100644 3rdparty/web-auth/webauthn-lib/src/MetadataService/Statement/EcdaaTrustAnchor.php create mode 100644 3rdparty/web-auth/webauthn-lib/src/MetadataService/Statement/ExtensionDescriptor.php create mode 100644 3rdparty/web-auth/webauthn-lib/src/MetadataService/Statement/MetadataStatement.php create mode 100644 3rdparty/web-auth/webauthn-lib/src/MetadataService/Statement/PatternAccuracyDescriptor.php create mode 100644 3rdparty/web-auth/webauthn-lib/src/MetadataService/Statement/RgbPaletteEntry.php create mode 100644 3rdparty/web-auth/webauthn-lib/src/MetadataService/Statement/RogueListEntry.php create mode 100644 3rdparty/web-auth/webauthn-lib/src/MetadataService/Statement/StatusReport.php create mode 100644 3rdparty/web-auth/webauthn-lib/src/MetadataService/Statement/VerificationMethodANDCombinations.php create mode 100644 3rdparty/web-auth/webauthn-lib/src/MetadataService/Statement/VerificationMethodDescriptor.php create mode 100644 3rdparty/web-auth/webauthn-lib/src/MetadataService/Statement/Version.php create mode 100644 3rdparty/web-auth/webauthn-lib/src/MetadataService/StatusReportRepository.php create mode 100644 3rdparty/web-auth/webauthn-lib/src/MetadataService/ValueFilter.php create mode 100644 3rdparty/web-auth/webauthn-lib/src/PublicKeyCredential.php create mode 100644 3rdparty/web-auth/webauthn-lib/src/PublicKeyCredentialCreationOptions.php create mode 100644 3rdparty/web-auth/webauthn-lib/src/PublicKeyCredentialDescriptor.php create mode 100644 3rdparty/web-auth/webauthn-lib/src/PublicKeyCredentialDescriptorCollection.php create mode 100644 3rdparty/web-auth/webauthn-lib/src/PublicKeyCredentialEntity.php create mode 100644 3rdparty/web-auth/webauthn-lib/src/PublicKeyCredentialLoader.php create mode 100644 3rdparty/web-auth/webauthn-lib/src/PublicKeyCredentialOptions.php create mode 100644 3rdparty/web-auth/webauthn-lib/src/PublicKeyCredentialParameters.php create mode 100644 3rdparty/web-auth/webauthn-lib/src/PublicKeyCredentialRequestOptions.php create mode 100644 3rdparty/web-auth/webauthn-lib/src/PublicKeyCredentialRpEntity.php create mode 100644 3rdparty/web-auth/webauthn-lib/src/PublicKeyCredentialSource.php create mode 100644 3rdparty/web-auth/webauthn-lib/src/PublicKeyCredentialSourceRepository.php create mode 100644 3rdparty/web-auth/webauthn-lib/src/PublicKeyCredentialUserEntity.php create mode 100644 3rdparty/web-auth/webauthn-lib/src/SimpleFakeCredentialGenerator.php create mode 100644 3rdparty/web-auth/webauthn-lib/src/StringStream.php create mode 100644 3rdparty/web-auth/webauthn-lib/src/TokenBinding/IgnoreTokenBindingHandler.php create mode 100644 3rdparty/web-auth/webauthn-lib/src/TokenBinding/SecTokenBindingHandler.php create mode 100644 3rdparty/web-auth/webauthn-lib/src/TokenBinding/TokenBinding.php create mode 100644 3rdparty/web-auth/webauthn-lib/src/TokenBinding/TokenBindingHandler.php create mode 100644 3rdparty/web-auth/webauthn-lib/src/TokenBinding/TokenBindingNotSupportedHandler.php create mode 100644 3rdparty/web-auth/webauthn-lib/src/TrustPath/CertificateTrustPath.php create mode 100644 3rdparty/web-auth/webauthn-lib/src/TrustPath/EcdaaKeyIdTrustPath.php create mode 100644 3rdparty/web-auth/webauthn-lib/src/TrustPath/EmptyTrustPath.php create mode 100644 3rdparty/web-auth/webauthn-lib/src/TrustPath/TrustPath.php create mode 100644 3rdparty/web-auth/webauthn-lib/src/TrustPath/TrustPathLoader.php create mode 100644 3rdparty/web-auth/webauthn-lib/src/U2FPublicKey.php create mode 100644 3rdparty/web-auth/webauthn-lib/src/Util/Base64.php create mode 100644 3rdparty/web-auth/webauthn-lib/src/Util/CoseSignatureFixer.php diff --git a/.gitignore b/.gitignore index 7753891e..f9a0bf93 100644 --- a/.gitignore +++ b/.gitignore @@ -13,7 +13,7 @@ data/ # apps/*/node_modules/ # apps/*/vendor/ # apps/*/vendor-bin/*/vendor/ -3rdparty/ +# 3rdparty/ — в отслеживании dist/ build/ diff --git a/.htaccess b/.htaccess index 8ea03a15..7f6aed28 100644 --- a/.htaccess +++ b/.htaccess @@ -144,4 +144,4 @@ ErrorDocument 404 /index.php/error/404 DirectorySlash off - + \ No newline at end of file diff --git a/3rdparty/.patches/mp3info-break-frame-parsing.patch b/3rdparty/.patches/mp3info-break-frame-parsing.patch new file mode 100644 index 00000000..05d273f8 --- /dev/null +++ b/3rdparty/.patches/mp3info-break-frame-parsing.patch @@ -0,0 +1,26 @@ +From 186b99ac4a57d091e9414c0944524a9e098835f3 Mon Sep 17 00:00:00 2001 +From: grnd-alt +Date: Mon, 13 Oct 2025 12:18:37 +0200 +Subject: [PATCH] fix: break frame parsing on short frame + +Signed-off-by: grnd-alt +--- + src/Mp3Info.php | 5 +++++ + 1 file changed, 5 insertions(+) + +diff --git a/src/Mp3Info.php b/src/Mp3Info.php +index ccf97f4..24781d7 100644 +--- a/src/Mp3Info.php ++++ b/src/Mp3Info.php +@@ -584,6 +584,11 @@ protected function parseId3v23Body($fp, $lastByte) { + $raw = fread($fp, 10); + $frame_id = substr($raw, 0, 4); + ++ if (strlen($raw) < 10) { ++ fseek($fp, $lastByte); ++ break; ++ } ++ + if ($frame_id == str_repeat(chr(0), 4)) { + fseek($fp, $lastByte); + break; diff --git a/3rdparty/.patches/mp3info-fix-incorrect-lookup-for-mpeg-header.patch b/3rdparty/.patches/mp3info-fix-incorrect-lookup-for-mpeg-header.patch new file mode 100644 index 00000000..35d9d27f --- /dev/null +++ b/3rdparty/.patches/mp3info-fix-incorrect-lookup-for-mpeg-header.patch @@ -0,0 +1,33 @@ +From 37365fd60dd3f4a637a887376b32f4d5e05726ce Mon Sep 17 00:00:00 2001 +From: wapmorgan +Date: Sat, 28 Jun 2025 03:34:24 +0300 +Subject: [PATCH] #30 Fix incorrect lookup for mpeg header. Previously it skips + 1 extra byte every jump that leads to 50/50% chance to find mpeg header + +--- + bin/mp3scan | 2 +- + src/Mp3Info.php | 1 - + 2 files changed, 1 insertion(+), 2 deletions(-) + +diff --git a/bin/mp3scan b/bin/mp3scan +index 8f3e28c..6352cc5 100755 +--- a/bin/mp3scan ++++ b/bin/mp3scan +@@ -1,4 +1,4 @@ +-#!/usr/bin/php ++#!/usr/bin/env php + > 5) & 0b111) != 0b111) { diff --git a/3rdparty/LICENSE INFO b/3rdparty/LICENSE INFO new file mode 100644 index 00000000..05ad628d --- /dev/null +++ b/3rdparty/LICENSE INFO @@ -0,0 +1,26 @@ +Licenses: +Archive - New BSD License +aws-sdk - apache 2.0 +class.phpmailer.php - LGPL +class.smtp.php - LGPL +Console - PHP License 3.0 +Crypt_Blowfish - PHP License 3.0 +chosen - MIT License +css - see chosen +DropBox - MIT +fullcalendar - GPL / MIT +getid3 - GPL +Google - MIT +js - see chosen +MDB2 - BSD +mediawiki - Apache 2.0 +miniColors - GPL / MIT +openid - GPLv3 +OS - New BSD License +PEAR - Simplified BSD License +phpass - Public domain +Sabre - New BSD License +smb4php - GPL +System.php - New BSD License +timepicker - GPL / MIT +XML - New BSD License diff --git a/3rdparty/autoload.php b/3rdparty/autoload.php new file mode 100644 index 00000000..4a17d6f5 --- /dev/null +++ b/3rdparty/autoload.php @@ -0,0 +1,22 @@ + '', + 'secret_access_key' => '', + 'session_token' => '', + 'expiration_timepoint_seconds' => 0, + ]; + } + + private $access_key_id; + private $secret_access_key; + private $session_token; + private $expiration_timepoint_seconds = 0; + + public function __get($name) { + return $this->$name; + } + + function __construct(array $options = []) { + parent::__construct(); + + $options = new Options($options, self::defaults()); + $this->access_key_id = $options->access_key_id->asString(); + $this->secret_access_key = $options->secret_access_key->asString(); + $this->session_token = $options->session_token ? $options->session_token->asString() : null; + $this->expiration_timepoint_seconds = $options->expiration_timepoint_seconds->asInt(); + + if (strlen($this->access_key_id) == 0) { + throw new \InvalidArgumentException("access_key_id must be provided"); + } + if (strlen($this->secret_access_key) == 0) { + throw new \InvalidArgumentException("secret_access_key must be provided"); + } + + $creds_options = self::$crt->aws_credentials_options_new(); + self::$crt->aws_credentials_options_set_access_key_id($creds_options, $this->access_key_id); + self::$crt->aws_credentials_options_set_secret_access_key($creds_options, $this->secret_access_key); + self::$crt->aws_credentials_options_set_session_token($creds_options, $this->session_token); + self::$crt->aws_credentials_options_set_expiration_timepoint_seconds($creds_options, $this->expiration_timepoint_seconds); + $this->acquire(self::$crt->aws_credentials_new($creds_options)); + self::$crt->aws_credentials_options_release($creds_options); + } + + function __destruct() { + self::$crt->aws_credentials_release($this->release()); + parent::__destruct(); + } +} diff --git a/3rdparty/aws/aws-crt-php/src/AWS/CRT/Auth/CredentialsProvider.php b/3rdparty/aws/aws-crt-php/src/AWS/CRT/Auth/CredentialsProvider.php new file mode 100644 index 00000000..e9d35886 --- /dev/null +++ b/3rdparty/aws/aws-crt-php/src/AWS/CRT/Auth/CredentialsProvider.php @@ -0,0 +1,23 @@ +credentials_provider_release($this->release()); + parent::__destruct(); + } +} diff --git a/3rdparty/aws/aws-crt-php/src/AWS/CRT/Auth/Signable.php b/3rdparty/aws/aws-crt-php/src/AWS/CRT/Auth/Signable.php new file mode 100644 index 00000000..100b56ad --- /dev/null +++ b/3rdparty/aws/aws-crt-php/src/AWS/CRT/Auth/Signable.php @@ -0,0 +1,43 @@ +signable_new_from_http_request($http_message->native); + }); + } + + public static function fromChunk($chunk_stream, $previous_signature="") { + if (!($chunk_stream instanceof InputStream)) { + $chunk_stream = new InputStream($chunk_stream); + } + return new Signable(function() use($chunk_stream, $previous_signature) { + return self::$crt->signable_new_from_chunk($chunk_stream->native, $previous_signature); + }); + } + + public static function fromCanonicalRequest($canonical_request) { + return new Signable(function() use($canonical_request) { + return self::$crt->signable_new_from_canonical_request($canonical_request); + }); + } + + protected function __construct($ctor) { + parent::__construct(); + $this->acquire($ctor()); + } + + function __destruct() { + self::$crt->signable_release($this->release()); + parent::__destruct(); + } +} \ No newline at end of file diff --git a/3rdparty/aws/aws-crt-php/src/AWS/CRT/Auth/SignatureType.php b/3rdparty/aws/aws-crt-php/src/AWS/CRT/Auth/SignatureType.php new file mode 100644 index 00000000..3d3b99f0 --- /dev/null +++ b/3rdparty/aws/aws-crt-php/src/AWS/CRT/Auth/SignatureType.php @@ -0,0 +1,15 @@ +sign_request_aws($signable->native, $signing_config->native, + function($result, $error_code) use ($on_complete) { + $signing_result = SigningResult::fromNative($result); + $on_complete($signing_result, $error_code); + }, null); + } + + static function testVerifySigV4ASigning($signable, $signing_config, $expected_canonical_request, $signature, $ecc_key_pub_x, $ecc_key_pub_y) { + return self::$crt->test_verify_sigv4a_signing($signable, $signing_config, $expected_canonical_request, $signature, $ecc_key_pub_x, $ecc_key_pub_y); + } +} diff --git a/3rdparty/aws/aws-crt-php/src/AWS/CRT/Auth/SigningAlgorithm.php b/3rdparty/aws/aws-crt-php/src/AWS/CRT/Auth/SigningAlgorithm.php new file mode 100644 index 00000000..dd110598 --- /dev/null +++ b/3rdparty/aws/aws-crt-php/src/AWS/CRT/Auth/SigningAlgorithm.php @@ -0,0 +1,11 @@ + SigningAlgorithm::SIGv4, + 'signature_type' => SignatureType::HTTP_REQUEST_HEADERS, + 'credentials_provider' => null, + 'region' => null, + 'service' => null, + 'use_double_uri_encode' => false, + 'should_normalize_uri_path' => false, + 'omit_session_token' => false, + 'signed_body_value' => null, + 'signed_body_header_type' => SignedBodyHeaderType::NONE, + 'expiration_in_seconds' => 0, + 'date' => time(), + 'should_sign_header' => null, + ]; + } + + private $options; + + public function __construct(array $options = []) { + parent::__construct(); + $this->options = $options = new Options($options, self::defaults()); + $sc = $this->acquire(self::$crt->signing_config_aws_new()); + self::$crt->signing_config_aws_set_algorithm($sc, $options->algorithm->asInt()); + self::$crt->signing_config_aws_set_signature_type($sc, $options->signature_type->asInt()); + if ($credentials_provider = $options->credentials_provider->asObject()) { + self::$crt->signing_config_aws_set_credentials_provider( + $sc, + $credentials_provider->native); + } + self::$crt->signing_config_aws_set_region( + $sc, $options->region->asString()); + self::$crt->signing_config_aws_set_service( + $sc, $options->service->asString()); + self::$crt->signing_config_aws_set_use_double_uri_encode( + $sc, $options->use_double_uri_encode->asBool()); + self::$crt->signing_config_aws_set_should_normalize_uri_path( + $sc, $options->should_normalize_uri_path->asBool()); + self::$crt->signing_config_aws_set_omit_session_token( + $sc, $options->omit_session_token->asBool()); + self::$crt->signing_config_aws_set_signed_body_value( + $sc, $options->signed_body_value->asString()); + self::$crt->signing_config_aws_set_signed_body_header_type( + $sc, $options->signed_body_header_type->asInt()); + self::$crt->signing_config_aws_set_expiration_in_seconds( + $sc, $options->expiration_in_seconds->asInt()); + self::$crt->signing_config_aws_set_date($sc, $options->date->asInt()); + if ($should_sign_header = $options->should_sign_header->asCallable()) { + self::$crt->signing_config_aws_set_should_sign_header_fn($sc, $should_sign_header); + } + } + + function __destruct() + { + self::$crt->signing_config_aws_release($this->release()); + parent::__destruct(); + } + + public function __get($name) { + return $this->options->get($name); + } +} \ No newline at end of file diff --git a/3rdparty/aws/aws-crt-php/src/AWS/CRT/Auth/SigningResult.php b/3rdparty/aws/aws-crt-php/src/AWS/CRT/Auth/SigningResult.php new file mode 100644 index 00000000..b8a4ab56 --- /dev/null +++ b/3rdparty/aws/aws-crt-php/src/AWS/CRT/Auth/SigningResult.php @@ -0,0 +1,33 @@ +acquire($native); + } + + function __destruct() { + // No destruction necessary, SigningResults are transient, just release + $this->release(); + parent::__destruct(); + } + + public static function fromNative($ptr) { + return new SigningResult($ptr); + } + + public function applyToHttpRequest(&$http_request) { + self::$crt->signing_result_apply_to_http_request($this->native, $http_request->native); + // Update http_request from native + $http_request = Request::unmarshall($http_request->toBlob()); + } +} \ No newline at end of file diff --git a/3rdparty/aws/aws-crt-php/src/AWS/CRT/Auth/StaticCredentialsProvider.php b/3rdparty/aws/aws-crt-php/src/AWS/CRT/Auth/StaticCredentialsProvider.php new file mode 100644 index 00000000..8dc62494 --- /dev/null +++ b/3rdparty/aws/aws-crt-php/src/AWS/CRT/Auth/StaticCredentialsProvider.php @@ -0,0 +1,35 @@ +$name; + } + + function __construct(array $options = []) { + parent::__construct(); + $this->credentials = new AwsCredentials($options); + + $provider_options = self::$crt->credentials_provider_static_options_new(); + self::$crt->credentials_provider_static_options_set_access_key_id($provider_options, $this->credentials->access_key_id); + self::$crt->credentials_provider_static_options_set_secret_access_key($provider_options, $this->credentials->secret_access_key); + self::$crt->credentials_provider_static_options_set_session_token($provider_options, $this->credentials->session_token); + $this->acquire(self::$crt->credentials_provider_static_new($provider_options)); + self::$crt->credentials_provider_static_options_release($provider_options); + } +} diff --git a/3rdparty/aws/aws-crt-php/src/AWS/CRT/CRT.php b/3rdparty/aws/aws-crt-php/src/AWS/CRT/CRT.php new file mode 100644 index 00000000..d196a473 --- /dev/null +++ b/3rdparty/aws/aws-crt-php/src/AWS/CRT/CRT.php @@ -0,0 +1,358 @@ +aws_crt_last_error(); + } + + /** + * @param integer $error Error code from the CRT, usually delivered via callback or {@see last_error} + * @return string Human-readable description of the provided error code + */ + public static function error_str($error) { + return self::$impl->aws_crt_error_str((int) $error); + } + + /** + * @param integer $error Error code from the CRT, usually delivered via callback or {@see last_error} + * @return string Name/enum identifier for the provided error code + */ + public static function error_name($error) { + return self::$impl->aws_crt_error_name((int) $error); + } + + public static function log_to_stdout() { + return self::$impl->aws_crt_log_to_stdout(); + } + + public static function log_to_stderr() { + return self::$impl->aws_crt_log_to_stderr(); + } + + public static function log_to_file($filename) { + return self::$impl->aws_crt_log_to_file($filename); + } + + public static function log_to_stream($stream) { + return self::$impl->aws_crt_log_to_stream($stream); + } + + public static function log_set_level($level) { + return self::$impl->aws_crt_log_set_level($level); + } + + public static function log_stop() { + return self::$impl->aws_crt_log_stop(); + } + + public static function log_message($level, $message) { + return self::$impl->aws_crt_log_message($level, $message); + } + + /** + * @return object Pointer to native event_loop_group_options + */ + function event_loop_group_options_new() { + return self::$impl->aws_crt_event_loop_group_options_new(); + } + + /** + * @param object $elg_options Pointer to native event_loop_group_options + */ + function event_loop_group_options_release($elg_options) { + self::$impl->aws_crt_event_loop_group_options_release($elg_options); + } + + /** + * @param object $elg_options Pointer to native event_loop_group_options + * @param integer $max_threads Maximum number of threads to allow the event loop group to use, default: 0/1 per CPU core + */ + function event_loop_group_options_set_max_threads($elg_options, $max_threads) { + self::$impl->aws_crt_event_loop_group_options_set_max_threads($elg_options, (int)$max_threads); + } + + /** + * @param object Pointer to event_loop_group_options, {@see event_loop_group_options_new} + * @return object Pointer to the new event loop group + */ + function event_loop_group_new($options) { + return self::$impl->aws_crt_event_loop_group_new($options); + } + + /** + * @param object $elg Pointer to the event loop group to release + */ + function event_loop_group_release($elg) { + self::$impl->aws_crt_event_loop_group_release($elg); + } + + /** + * return object Pointer to native AWS credentials options + */ + function aws_credentials_options_new() { + return self::$impl->aws_crt_credentials_options_new(); + } + + function aws_credentials_options_release($options) { + self::$impl->aws_crt_credentials_options_release($options); + } + + function aws_credentials_options_set_access_key_id($options, $access_key_id) { + self::$impl->aws_crt_credentials_options_set_access_key_id($options, $access_key_id); + } + + function aws_credentials_options_set_secret_access_key($options, $secret_access_key) { + self::$impl->aws_crt_credentials_options_set_secret_access_key($options, $secret_access_key); + } + + function aws_credentials_options_set_session_token($options, $session_token) { + self::$impl->aws_crt_credentials_options_set_session_token($options, $session_token); + } + + function aws_credentials_options_set_expiration_timepoint_seconds($options, $expiration_timepoint_seconds) { + self::$impl->aws_crt_credentials_options_set_expiration_timepoint_seconds($options, $expiration_timepoint_seconds); + } + + function aws_credentials_new($options) { + return self::$impl->aws_crt_credentials_new($options); + } + + function aws_credentials_release($credentials) { + self::$impl->aws_crt_credentials_release($credentials); + } + + function credentials_provider_release($provider) { + self::$impl->aws_crt_credentials_provider_release($provider); + } + + function credentials_provider_static_options_new() { + return self::$impl->aws_crt_credentials_provider_static_options_new(); + } + + function credentials_provider_static_options_release($options) { + self::$impl->aws_crt_credentials_provider_static_options_release($options); + } + + function credentials_provider_static_options_set_access_key_id($options, $access_key_id) { + self::$impl->aws_crt_credentials_provider_static_options_set_access_key_id($options, $access_key_id); + } + + function credentials_provider_static_options_set_secret_access_key($options, $secret_access_key) { + self::$impl->aws_crt_credentials_provider_static_options_set_secret_access_key($options, $secret_access_key); + } + + function credentials_provider_static_options_set_session_token($options, $session_token) { + self::$impl->aws_crt_credentials_provider_static_options_set_session_token($options, $session_token); + } + + function credentials_provider_static_new($options) { + return self::$impl->aws_crt_credentials_provider_static_new($options); + } + + function input_stream_options_new() { + return self::$impl->aws_crt_input_stream_options_new(); + } + + function input_stream_options_release($options) { + self::$impl->aws_crt_input_stream_options_release($options); + } + + function input_stream_options_set_user_data($options, $user_data) { + self::$impl->aws_crt_input_stream_options_set_user_data($options, $user_data); + } + + function input_stream_new($options) { + return self::$impl->aws_crt_input_stream_new($options); + } + + function input_stream_release($stream) { + self::$impl->aws_crt_input_stream_release($stream); + } + + function input_stream_seek($stream, $offset, $basis) { + return self::$impl->aws_crt_input_stream_seek($stream, $offset, $basis); + } + + function input_stream_read($stream, $length) { + return self::$impl->aws_crt_input_stream_read($stream, $length); + } + + function input_stream_eof($stream) { + return self::$impl->aws_crt_input_stream_eof($stream); + } + + function input_stream_get_length($stream) { + return self::$impl->aws_crt_input_stream_get_length($stream); + } + + function http_message_new_from_blob($blob) { + return self::$impl->aws_crt_http_message_new_from_blob($blob); + } + + function http_message_to_blob($message) { + return self::$impl->aws_crt_http_message_to_blob($message); + } + + function http_message_release($message) { + self::$impl->aws_crt_http_message_release($message); + } + + function signing_config_aws_new() { + return self::$impl->aws_crt_signing_config_aws_new(); + } + + function signing_config_aws_release($signing_config) { + return self::$impl->aws_crt_signing_config_aws_release($signing_config); + } + + function signing_config_aws_set_algorithm($signing_config, $algorithm) { + self::$impl->aws_crt_signing_config_aws_set_algorithm($signing_config, (int)$algorithm); + } + + function signing_config_aws_set_signature_type($signing_config, $signature_type) { + self::$impl->aws_crt_signing_config_aws_set_signature_type($signing_config, (int)$signature_type); + } + + function signing_config_aws_set_credentials_provider($signing_config, $credentials_provider) { + self::$impl->aws_crt_signing_config_aws_set_credentials_provider($signing_config, $credentials_provider); + } + + function signing_config_aws_set_region($signing_config, $region) { + self::$impl->aws_crt_signing_config_aws_set_region($signing_config, $region); + } + + function signing_config_aws_set_service($signing_config, $service) { + self::$impl->aws_crt_signing_config_aws_set_service($signing_config, $service); + } + + function signing_config_aws_set_use_double_uri_encode($signing_config, $use_double_uri_encode) { + self::$impl->aws_crt_signing_config_aws_set_use_double_uri_encode($signing_config, $use_double_uri_encode); + } + + function signing_config_aws_set_should_normalize_uri_path($signing_config, $should_normalize_uri_path) { + self::$impl->aws_crt_signing_config_aws_set_should_normalize_uri_path($signing_config, $should_normalize_uri_path); + } + + function signing_config_aws_set_omit_session_token($signing_config, $omit_session_token) { + self::$impl->aws_crt_signing_config_aws_set_omit_session_token($signing_config, $omit_session_token); + } + + function signing_config_aws_set_signed_body_value($signing_config, $signed_body_value) { + self::$impl->aws_crt_signing_config_aws_set_signed_body_value($signing_config, $signed_body_value); + } + + function signing_config_aws_set_signed_body_header_type($signing_config, $signed_body_header_type) { + self::$impl->aws_crt_signing_config_aws_set_signed_body_header_type($signing_config, $signed_body_header_type); + } + + function signing_config_aws_set_expiration_in_seconds($signing_config, $expiration_in_seconds) { + self::$impl->aws_crt_signing_config_aws_set_expiration_in_seconds($signing_config, $expiration_in_seconds); + } + + function signing_config_aws_set_date($signing_config, $timestamp) { + self::$impl->aws_crt_signing_config_aws_set_date($signing_config, $timestamp); + } + + function signing_config_aws_set_should_sign_header_fn($signing_config, $should_sign_header_fn) { + self::$impl->aws_crt_signing_config_aws_set_should_sign_header_fn($signing_config, $should_sign_header_fn); + } + + function signable_new_from_http_request($http_message) { + return self::$impl->aws_crt_signable_new_from_http_request($http_message); + } + + function signable_new_from_chunk($chunk_stream, $previous_signature) { + return self::$impl->aws_crt_signable_new_from_chunk($chunk_stream, $previous_signature); + } + + function signable_new_from_canonical_request($canonical_request) { + return self::$impl->aws_crt_signable_new_from_canonical_request($canonical_request); + } + + function signable_release($signable) { + self::$impl->aws_crt_signable_release($signable); + } + + function signing_result_release($signing_result) { + self::$impl->aws_crt_signing_result_release($signing_result); + } + + function signing_result_apply_to_http_request($signing_result, $http_message) { + return self::$impl->aws_crt_signing_result_apply_to_http_request( + $signing_result, $http_message); + } + + function sign_request_aws($signable, $signing_config, $on_complete, $user_data) { + return self::$impl->aws_crt_sign_request_aws($signable, $signing_config, $on_complete, $user_data); + } + + function test_verify_sigv4a_signing($signable, $signing_config, $expected_canonical_request, $signature, $ecc_key_pub_x, $ecc_key_pub_y) { + return self::$impl->aws_crt_test_verify_sigv4a_signing($signable, $signing_config, $expected_canonical_request, $signature, $ecc_key_pub_x, $ecc_key_pub_y); + } + + public static function crc32($input, $previous = 0) { + return self::$impl->aws_crt_crc32($input, $previous); + } + + public static function crc32c($input, $previous = 0) { + return self::$impl->aws_crt_crc32c($input, $previous); + } +} diff --git a/3rdparty/aws/aws-crt-php/src/AWS/CRT/HTTP/Headers.php b/3rdparty/aws/aws-crt-php/src/AWS/CRT/HTTP/Headers.php new file mode 100644 index 00000000..8d1457cf --- /dev/null +++ b/3rdparty/aws/aws-crt-php/src/AWS/CRT/HTTP/Headers.php @@ -0,0 +1,50 @@ +headers = $headers; + } + + public static function marshall($headers) { + $buf = ""; + foreach ($headers->headers as $header => $value) { + $buf .= Encoding::encodeString($header); + $buf .= Encoding::encodeString($value); + } + return $buf; + } + + public static function unmarshall($buf) { + $strings = Encoding::readStrings($buf); + $headers = []; + for ($idx = 0; $idx < count($strings);) { + $headers[$strings[$idx++]] = $strings[$idx++]; + } + return new Headers($headers); + } + + public function count() { + return count($this->headers); + } + + public function get($header) { + return isset($this->headers[$header]) ? $this->headers[$header] : null; + } + + public function set($header, $value) { + $this->headers[$header] = $value; + } + + public function toArray() { + return $this->headers; + } +} diff --git a/3rdparty/aws/aws-crt-php/src/AWS/CRT/HTTP/Message.php b/3rdparty/aws/aws-crt-php/src/AWS/CRT/HTTP/Message.php new file mode 100644 index 00000000..a8c151fe --- /dev/null +++ b/3rdparty/aws/aws-crt-php/src/AWS/CRT/HTTP/Message.php @@ -0,0 +1,95 @@ +method = $method; + $this->path = $path; + $this->query = $query; + $this->headers = new Headers($headers); + $this->acquire(self::$crt->http_message_new_from_blob(self::marshall($this))); + } + + public function __destruct() { + self::$crt->http_message_release($this->release()); + parent::__destruct(); + } + + public function toBlob() { + return self::$crt->http_message_to_blob($this->native); + } + + protected static function marshall($msg) { + $buf = ""; + $buf .= Encoding::encodeString($msg->method); + $buf .= Encoding::encodeString($msg->pathAndQuery()); + $buf .= Headers::marshall($msg->headers); + return $buf; + } + + protected static function _unmarshall($buf, $class=Message::class) { + $method = Encoding::readString($buf); + $path_and_query = Encoding::readString($buf); + $parts = explode("?", $path_and_query, 2); + $path = isset($parts[0]) ? $parts[0] : ""; + $query = isset($parts[1]) ? $parts[1] : ""; + $headers = Headers::unmarshall($buf); + + // Turn query params back into a dictionary + if (strlen($query)) { + $query = rawurldecode($query); + $query = explode("&", $query); + $query = array_reduce($query, function($params, $pair) { + list($param, $value) = explode("=", $pair, 2); + $params[$param] = $value; + return $params; + }, []); + } else { + $query = []; + } + + return new $class($method, $path, $query, $headers->toArray()); + } + + public function pathAndQuery() { + $path = $this->path; + $queries = []; + foreach ($this->query as $param => $value) { + $queries []= urlencode($param) . "=" . urlencode($value); + } + $query = implode("&", $queries); + if (strlen($query)) { + $path = implode("?", [$path, $query]); + } + return $path; + } + + public function method() { + return $this->method; + } + + public function path() { + return $this->path; + } + + public function query() { + return $this->query; + } + + public function headers() { + return $this->headers; + } +} diff --git a/3rdparty/aws/aws-crt-php/src/AWS/CRT/HTTP/Request.php b/3rdparty/aws/aws-crt-php/src/AWS/CRT/HTTP/Request.php new file mode 100644 index 00000000..bec4ac1a --- /dev/null +++ b/3rdparty/aws/aws-crt-php/src/AWS/CRT/HTTP/Request.php @@ -0,0 +1,32 @@ +body_stream = $body_stream; + } + + public static function marshall($request) { + return parent::marshall($request); + } + + public static function unmarshall($buf) { + return parent::_unmarshall($buf, Request::class); + } + + public function body_stream() { + return $this->body_stream; + } +} diff --git a/3rdparty/aws/aws-crt-php/src/AWS/CRT/HTTP/Response.php b/3rdparty/aws/aws-crt-php/src/AWS/CRT/HTTP/Response.php new file mode 100644 index 00000000..526edc3a --- /dev/null +++ b/3rdparty/aws/aws-crt-php/src/AWS/CRT/HTTP/Response.php @@ -0,0 +1,27 @@ +status_code = $status_code; + } + + public static function marshall($response) { + return parent::marshall($response); + } + + public static function unmarshall($buf) { + return parent::_unmarshall($buf, Response::class); + } + + public function status_code() { + return $this->status_code; + } +} diff --git a/3rdparty/aws/aws-crt-php/src/AWS/CRT/IO/EventLoopGroup.php b/3rdparty/aws/aws-crt-php/src/AWS/CRT/IO/EventLoopGroup.php new file mode 100644 index 00000000..7e989e76 --- /dev/null +++ b/3rdparty/aws/aws-crt-php/src/AWS/CRT/IO/EventLoopGroup.php @@ -0,0 +1,39 @@ + 0, + ]; + } + + function __construct(array $options = []) { + parent::__construct(); + $options = new Options($options, self::defaults()); + $elg_options = self::$crt->event_loop_group_options_new(); + self::$crt->event_loop_group_options_set_max_threads($elg_options, $options->getInt('max_threads')); + $this->acquire(self::$crt->event_loop_group_new($elg_options)); + self::$crt->event_loop_group_options_release($elg_options); + } + + function __destruct() { + self::$crt->event_loop_group_release($this->release()); + parent::__destruct(); + } +} diff --git a/3rdparty/aws/aws-crt-php/src/AWS/CRT/IO/InputStream.php b/3rdparty/aws/aws-crt-php/src/AWS/CRT/IO/InputStream.php new file mode 100644 index 00000000..6aef7092 --- /dev/null +++ b/3rdparty/aws/aws-crt-php/src/AWS/CRT/IO/InputStream.php @@ -0,0 +1,50 @@ +stream = $stream; + $options = self::$crt->input_stream_options_new(); + // The stream implementation in native just converts the PHP stream into + // a native php_stream* and executes operations entirely in native + self::$crt->input_stream_options_set_user_data($options, $stream); + $this->acquire(self::$crt->input_stream_new($options)); + self::$crt->input_stream_options_release($options); + } + + public function __destruct() { + $this->release(); + parent::__destruct(); + } + + public function eof() { + return self::$crt->input_stream_eof($this->native); + } + + public function length() { + return self::$crt->input_stream_get_length($this->native); + } + + public function read($length = 0) { + if ($length == 0) { + $length = $this->length(); + } + return self::$crt->input_stream_read($this->native, $length); + } + + public function seek($offset, $basis) { + return self::$crt->input_stream_seek($this->native, $offset, $basis); + } +} diff --git a/3rdparty/aws/aws-crt-php/src/AWS/CRT/Internal/Encoding.php b/3rdparty/aws/aws-crt-php/src/AWS/CRT/Internal/Encoding.php new file mode 100644 index 00000000..7c625592 --- /dev/null +++ b/3rdparty/aws/aws-crt-php/src/AWS/CRT/Internal/Encoding.php @@ -0,0 +1,37 @@ += self::NONE && $level <= self::TRACE); + CRT::log_set_level($level); + } + + public static function log($level, $message) { + CRT::log_message($level, $message); + } +} diff --git a/3rdparty/aws/aws-crt-php/src/AWS/CRT/NativeResource.php b/3rdparty/aws/aws-crt-php/src/AWS/CRT/NativeResource.php new file mode 100644 index 00000000..528df759 --- /dev/null +++ b/3rdparty/aws/aws-crt-php/src/AWS/CRT/NativeResource.php @@ -0,0 +1,42 @@ +native = $handle; + } + + protected function release() { + $native = $this->native; + $this->native = null; + return $native; + } + + function __destruct() { + // Should have been destroyed and released by derived resource + assert($this->native == null); + unset(self::$resources[spl_object_hash($this)]); + } +} diff --git a/3rdparty/aws/aws-crt-php/src/AWS/CRT/Options.php b/3rdparty/aws/aws-crt-php/src/AWS/CRT/Options.php new file mode 100644 index 00000000..363a396c --- /dev/null +++ b/3rdparty/aws/aws-crt-php/src/AWS/CRT/Options.php @@ -0,0 +1,77 @@ +value = $value; + } + + public function asObject() { + return $this->value; + } + + public function asMixed() { + return $this->value; + } + + public function asInt() { + return empty($this->value) ? 0 : (int)$this->value; + } + + public function asBool() { + return boolval($this->value); + } + + public function asString() { + return !empty($this->value) ? strval($this->value) : ""; + } + + public function asArray() { + return is_array($this->value) ? $this->value : (!empty($this->value) ? [$this->value] : []); + } + + public function asCallable() { + return is_callable($this->value) ? $this->value : null; + } +} + +final class Options { + private $options; + + public function __construct($opts = [], $defaults = []) { + $this->options = array_replace($defaults, empty($opts) ? [] : $opts); + } + + public function __get($name) { + return $this->get($name); + } + + public function asArray() { + return $this->options; + } + + public function toArray() { + return array_merge_recursive([], $this->options); + } + + public function get($name) { + return new OptionValue($this->options[$name]); + } + + public function getInt($name) { + return $this->get($name)->asInt(); + } + + public function getString($name) { + return $this->get($name)->asString(); + } + + public function getBool($name) { + return $this->get($name)->asBool(); + } +} diff --git a/3rdparty/aws/aws-sdk-php/CRT_INSTRUCTIONS.md b/3rdparty/aws/aws-sdk-php/CRT_INSTRUCTIONS.md new file mode 100644 index 00000000..047b4b08 --- /dev/null +++ b/3rdparty/aws/aws-sdk-php/CRT_INSTRUCTIONS.md @@ -0,0 +1,4 @@ +## Building and enabling the Common Run Time + +1. **Follow instructions on crt repo** – Clone and build the repo as shown [here][https://github.com/awslabs/aws-crt-php]. +1. **Enable the CRT** – add the following line to your php.ini file `extension=path/to/aws-crt-php/modules/awscrt.so` \ No newline at end of file diff --git a/3rdparty/aws/aws-sdk-php/LICENSE b/3rdparty/aws/aws-sdk-php/LICENSE new file mode 100644 index 00000000..8d53e9f5 --- /dev/null +++ b/3rdparty/aws/aws-sdk-php/LICENSE @@ -0,0 +1,141 @@ +# Apache License +Version 2.0, January 2004 + +TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION + +## 1. Definitions. + +"License" shall mean the terms and conditions for use, reproduction, and distribution as defined by Sections 1 +through 9 of this document. + +"Licensor" shall mean the copyright owner or entity authorized by the copyright owner that is granting the +License. + +"Legal Entity" shall mean the union of the acting entity and all other entities that control, are controlled +by, or are under common control with that entity. For the purposes of this definition, "control" means +(i) the power, direct or indirect, to cause the direction or management of such entity, whether by contract +or otherwise, or (ii) ownership of fifty percent (50%) or more of the outstanding shares, or (iii) beneficial +ownership of such entity. + +"You" (or "Your") shall mean an individual or Legal Entity exercising permissions granted by this License. + +"Source" form shall mean the preferred form for making modifications, including but not limited to software +source code, documentation source, and configuration files. + +"Object" form shall mean any form resulting from mechanical transformation or translation of a Source form, +including but not limited to compiled object code, generated documentation, and conversions to other media +types. + +"Work" shall mean the work of authorship, whether in Source or Object form, made available under the License, +as indicated by a copyright notice that is included in or attached to the work (an example is provided in the +Appendix below). + +"Derivative Works" shall mean any work, whether in Source or Object form, that is based on (or derived from) +the Work and for which the editorial revisions, annotations, elaborations, or other modifications represent, +as a whole, an original work of authorship. For the purposes of this License, Derivative Works shall not +include works that remain separable from, or merely link (or bind by name) to the interfaces of, the Work +and Derivative Works thereof. + +"Contribution" shall mean any work of authorship, including the original version of the Work and any +modifications or additions to that Work or Derivative Works thereof, that is intentionally submitted to +Licensor for inclusion in the Work by the copyright owner or by an individual or Legal Entity authorized to +submit on behalf of the copyright owner. For the purposes of this definition, "submitted" means any form of +electronic, verbal, or written communication sent to the Licensor or its representatives, including but not +limited to communication on electronic mailing lists, source code control systems, and issue tracking systems +that are managed by, or on behalf of, the Licensor for the purpose of discussing and improving the Work, but +excluding communication that is conspicuously marked or otherwise designated in writing by the copyright +owner as "Not a Contribution." + +"Contributor" shall mean Licensor and any individual or Legal Entity on behalf of whom a Contribution has been +received by Licensor and subsequently incorporated within the Work. + +## 2. Grant of Copyright License. + +Subject to the terms and conditions of this License, each Contributor hereby grants to You a perpetual, +worldwide, non-exclusive, no-charge, royalty-free, irrevocable copyright license to reproduce, prepare +Derivative Works of, publicly display, publicly perform, sublicense, and distribute the Work and such +Derivative Works in Source or Object form. + +## 3. Grant of Patent License. + +Subject to the terms and conditions of this License, each Contributor hereby grants to You a perpetual, +worldwide, non-exclusive, no-charge, royalty-free, irrevocable (except as stated in this section) patent +license to make, have made, use, offer to sell, sell, import, and otherwise transfer the Work, where such +license applies only to those patent claims licensable by such Contributor that are necessarily infringed by +their Contribution(s) alone or by combination of their Contribution(s) with the Work to which such +Contribution(s) was submitted. If You institute patent litigation against any entity (including a cross-claim +or counterclaim in a lawsuit) alleging that the Work or a Contribution incorporated within the Work +constitutes direct or contributory patent infringement, then any patent licenses granted to You under this +License for that Work shall terminate as of the date such litigation is filed. + +## 4. Redistribution. + +You may reproduce and distribute copies of the Work or Derivative Works thereof in any medium, with or without +modifications, and in Source or Object form, provided that You meet the following conditions: + + 1. You must give any other recipients of the Work or Derivative Works a copy of this License; and + + 2. You must cause any modified files to carry prominent notices stating that You changed the files; and + + 3. You must retain, in the Source form of any Derivative Works that You distribute, all copyright, patent, + trademark, and attribution notices from the Source form of the Work, excluding those notices that do + not pertain to any part of the Derivative Works; and + + 4. If the Work includes a "NOTICE" text file as part of its distribution, then any Derivative Works that + You distribute must include a readable copy of the attribution notices contained within such NOTICE + file, excluding those notices that do not pertain to any part of the Derivative Works, in at least one + of the following places: within a NOTICE text file distributed as part of the Derivative Works; within + the Source form or documentation, if provided along with the Derivative Works; or, within a display + generated by the Derivative Works, if and wherever such third-party notices normally appear. The + contents of the NOTICE file are for informational purposes only and do not modify the License. You may + add Your own attribution notices within Derivative Works that You distribute, alongside or as an + addendum to the NOTICE text from the Work, provided that such additional attribution notices cannot be + construed as modifying the License. + +You may add Your own copyright statement to Your modifications and may provide additional or different license +terms and conditions for use, reproduction, or distribution of Your modifications, or for any such Derivative +Works as a whole, provided Your use, reproduction, and distribution of the Work otherwise complies with the +conditions stated in this License. + +## 5. Submission of Contributions. + +Unless You explicitly state otherwise, any Contribution intentionally submitted for inclusion in the Work by +You to the Licensor shall be under the terms and conditions of this License, without any additional terms or +conditions. Notwithstanding the above, nothing herein shall supersede or modify the terms of any separate +license agreement you may have executed with Licensor regarding such Contributions. + +## 6. Trademarks. + +This License does not grant permission to use the trade names, trademarks, service marks, or product names of +the Licensor, except as required for reasonable and customary use in describing the origin of the Work and +reproducing the content of the NOTICE file. + +## 7. Disclaimer of Warranty. + +Unless required by applicable law or agreed to in writing, Licensor provides the Work (and each Contributor +provides its Contributions) on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express +or implied, including, without limitation, any warranties or conditions of TITLE, NON-INFRINGEMENT, +MERCHANTABILITY, or FITNESS FOR A PARTICULAR PURPOSE. You are solely responsible for determining the +appropriateness of using or redistributing the Work and assume any risks associated with Your exercise of +permissions under this License. + +## 8. Limitation of Liability. + +In no event and under no legal theory, whether in tort (including negligence), contract, or otherwise, unless +required by applicable law (such as deliberate and grossly negligent acts) or agreed to in writing, shall any +Contributor be liable to You for damages, including any direct, indirect, special, incidental, or consequential +damages of any character arising as a result of this License or out of the use or inability to use the Work +(including but not limited to damages for loss of goodwill, work stoppage, computer failure or malfunction, or +any and all other commercial damages or losses), even if such Contributor has been advised of the possibility +of such damages. + +## 9. Accepting Warranty or Additional Liability. + +While redistributing the Work or Derivative Works thereof, You may choose to offer, and charge a fee for, +acceptance of support, warranty, indemnity, or other liability obligations and/or rights consistent with this +License. However, in accepting such obligations, You may act only on Your own behalf and on Your sole +responsibility, not on behalf of any other Contributor, and only if You agree to indemnify, defend, and hold +each Contributor harmless for any liability incurred by, or claims asserted against, such Contributor by reason +of your accepting any such warranty or additional liability. + +END OF TERMS AND CONDITIONS diff --git a/3rdparty/aws/aws-sdk-php/NOTICE b/3rdparty/aws/aws-sdk-php/NOTICE new file mode 100644 index 00000000..e231afd7 --- /dev/null +++ b/3rdparty/aws/aws-sdk-php/NOTICE @@ -0,0 +1,17 @@ +# AWS SDK for PHP + + + +Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. + +Licensed under the Apache License, Version 2.0 (the "License"). +You may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. diff --git a/3rdparty/aws/aws-sdk-php/THIRD-PARTY-LICENSES b/3rdparty/aws/aws-sdk-php/THIRD-PARTY-LICENSES new file mode 100644 index 00000000..6f7dc625 --- /dev/null +++ b/3rdparty/aws/aws-sdk-php/THIRD-PARTY-LICENSES @@ -0,0 +1,84 @@ +The AWS SDK for PHP includes the following third-party software/licensing: + + +** Guzzle - https://github.com/guzzle/guzzle + +Copyright (c) 2014 Michael Dowling, https://github.com/mtdowling + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in +all copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +THE SOFTWARE. + +---------------- + +** jmespath.php - https://github.com/mtdowling/jmespath.php + +Copyright (c) 2014 Michael Dowling, https://github.com/mtdowling + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in +all copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +THE SOFTWARE. + +---------------- + +** phpunit-mock-objects -- https://github.com/sebastianbergmann/phpunit-mock-objects + +Copyright (c) 2002-2018, Sebastian Bergmann . +All rights reserved. + +Redistribution and use in source and binary forms, with or without +modification, are permitted provided that the following conditions +are met: + + * Redistributions of source code must retain the above copyright + notice, this list of conditions and the following disclaimer. + + * Redistributions in binary form must reproduce the above copyright + notice, this list of conditions and the following disclaimer in + the documentation and/or other materials provided with the + distribution. + + * Neither the name of Sebastian Bergmann nor the names of his + contributors may be used to endorse or promote products derived + from this software without specific prior written permission. + +THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS +"AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT +LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS +FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE +COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, +INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, +BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; +LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER +CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT +LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN +ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE +POSSIBILITY OF SUCH DAMAGE. \ No newline at end of file diff --git a/3rdparty/aws/aws-sdk-php/src/AbstractConfigurationProvider.php b/3rdparty/aws/aws-sdk-php/src/AbstractConfigurationProvider.php new file mode 100644 index 00000000..78a5cdad --- /dev/null +++ b/3rdparty/aws/aws-sdk-php/src/AbstractConfigurationProvider.php @@ -0,0 +1,157 @@ +get($cacheKey); + if ($found instanceof static::$interfaceClass) { + return Promise\Create::promiseFor($found); + } + + return $provider() + ->then(function ($config) use ( + $cache, + $cacheKey + ) { + $cache->set($cacheKey, $config); + return $config; + }); + }; + } + + /** + * Creates an aggregate configuration provider that invokes the provided + * variadic providers one after the other until a provider returns + * configuration. + * + * @return callable + */ + public static function chain() + { + $links = func_get_args(); + if (empty($links)) { + throw new \InvalidArgumentException('No providers in chain'); + } + + return function () use ($links) { + /** @var callable $parent */ + $parent = array_shift($links); + $promise = $parent(); + while ($next = array_shift($links)) { + $promise = $promise->otherwise($next); + } + return $promise; + }; + } + + /** + * Gets the environment's HOME directory if available. + * + * @return null|string + */ + protected static function getHomeDir() + { + // On Linux/Unix-like systems, use the HOME environment variable + if ($homeDir = getenv('HOME')) { + return $homeDir; + } + + // Get the HOMEDRIVE and HOMEPATH values for Windows hosts + $homeDrive = getenv('HOMEDRIVE'); + $homePath = getenv('HOMEPATH'); + + return ($homeDrive && $homePath) ? $homeDrive . $homePath : null; + } + + /** + * Gets default config file location from environment, falling back to aws + * default location + * + * @return string + */ + protected static function getDefaultConfigFilename() + { + if ($filename = getenv(self::ENV_CONFIG_FILE)) { + return $filename; + } + return self::getHomeDir() . '/.aws/config'; + } + + /** + * Wraps a config provider and caches previously provided configuration. + * + * @param callable $provider Config provider function to wrap. + * + * @return callable + */ + public static function memoize(callable $provider) + { + return function () use ($provider) { + static $result; + static $isConstant; + + // Constant config will be returned constantly. + if ($isConstant) { + return $result; + } + + // Create the initial promise that will be used as the cached value + if (null === $result) { + $result = $provider(); + } + + // Return config and set flag that provider is already set + return $result + ->then(function ($config) use (&$isConstant) { + $isConstant = true; + return $config; + }); + }; + } + + /** + * Reject promise with standardized exception. + * + * @param $msg + * @return Promise\RejectedPromise + */ + protected static function reject($msg) + { + $exceptionClass = static::$exceptionClass; + return new Promise\RejectedPromise(new $exceptionClass($msg)); + } +} diff --git a/3rdparty/aws/aws-sdk-php/src/Api/AbstractModel.php b/3rdparty/aws/aws-sdk-php/src/Api/AbstractModel.php new file mode 100644 index 00000000..2c9b412d --- /dev/null +++ b/3rdparty/aws/aws-sdk-php/src/Api/AbstractModel.php @@ -0,0 +1,89 @@ +definition = $definition; + $this->shapeMap = $shapeMap; + if (isset($definition['contextParam'])) { + $this->contextParam = $definition['contextParam']; + } + } + + public function toArray() + { + return $this->definition; + } + + /** + * @return mixed|null + */ + #[\ReturnTypeWillChange] + public function offsetGet($offset) + { + return isset($this->definition[$offset]) + ? $this->definition[$offset] : null; + } + + /** + * @return void + */ + #[\ReturnTypeWillChange] + public function offsetSet($offset, $value) + { + $this->definition[$offset] = $value; + } + + /** + * @return bool + */ + #[\ReturnTypeWillChange] + public function offsetExists($offset) + { + return isset($this->definition[$offset]); + } + + /** + * @return void + */ + #[\ReturnTypeWillChange] + public function offsetUnset($offset) + { + unset($this->definition[$offset]); + } + + protected function shapeAt($key) + { + if (!isset($this->definition[$key])) { + throw new \InvalidArgumentException('Expected shape definition at ' + . $key); + } + + return $this->shapeFor($this->definition[$key]); + } + + protected function shapeFor(array $definition) + { + return isset($definition['shape']) + ? $this->shapeMap->resolve($definition) + : Shape::create($definition, $this->shapeMap); + } +} diff --git a/3rdparty/aws/aws-sdk-php/src/Api/ApiProvider.php b/3rdparty/aws/aws-sdk-php/src/Api/ApiProvider.php new file mode 100644 index 00000000..818ff5da --- /dev/null +++ b/3rdparty/aws/aws-sdk-php/src/Api/ApiProvider.php @@ -0,0 +1,244 @@ + 'api-2', + 'paginator' => 'paginators-1', + 'waiter' => 'waiters-2', + 'docs' => 'docs-2', + ]; + + /** @var array API manifest */ + private $manifest; + + /** @var string The directory containing service models. */ + private $modelsDir; + + /** + * Resolves an API provider and ensures a non-null return value. + * + * @param callable $provider Provider function to invoke. + * @param string $type Type of data ('api', 'waiter', 'paginator'). + * @param string $service Service name. + * @param string $version API version. + * + * @return array + * @throws UnresolvedApiException + */ + public static function resolve(callable $provider, $type, $service, $version) + { + // Execute the provider and return the result, if there is one. + $result = $provider($type, $service, $version); + if (is_array($result)) { + if (!isset($result['metadata']['serviceIdentifier'])) { + $result['metadata']['serviceIdentifier'] = $service; + } + return $result; + } + + // Throw an exception with a message depending on the inputs. + if (!isset(self::$typeMap[$type])) { + $msg = "The type must be one of: " . implode(', ', self::$typeMap); + } elseif ($service) { + $msg = "The {$service} service does not have version: {$version}."; + } else { + $msg = "You must specify a service name to retrieve its API data."; + } + + throw new UnresolvedApiException($msg); + } + + /** + * Default SDK API provider. + * + * This provider loads pre-built manifest data from the `data` directory. + * + * @return self + */ + public static function defaultProvider() + { + return new self(__DIR__ . '/../data', \Aws\manifest()); + } + + /** + * Loads API data after resolving the version to the latest, compatible, + * available version based on the provided manifest data. + * + * Manifest data is essentially an associative array of service names to + * associative arrays of API version aliases. + * + * [ + * ... + * 'ec2' => [ + * 'latest' => '2014-10-01', + * '2014-10-01' => '2014-10-01', + * '2014-09-01' => '2014-10-01', + * '2014-06-15' => '2014-10-01', + * ... + * ], + * 'ecs' => [...], + * 'elasticache' => [...], + * ... + * ] + * + * @param string $dir Directory containing service models. + * @param array $manifest The API version manifest data. + * + * @return self + */ + public static function manifest($dir, array $manifest) + { + return new self($dir, $manifest); + } + + /** + * Loads API data from the specified directory. + * + * If "latest" is specified as the version, this provider must glob the + * directory to find which is the latest available version. + * + * @param string $dir Directory containing service models. + * + * @return self + * @throws \InvalidArgumentException if the provided `$dir` is invalid. + */ + public static function filesystem($dir) + { + return new self($dir); + } + + /** + * Retrieves a list of valid versions for the specified service. + * + * @param string $service Service name + * + * @return array + */ + public function getVersions($service) + { + if (!isset($this->manifest)) { + $this->buildVersionsList($service); + } + + if (!isset($this->manifest[$service]['versions'])) { + return []; + } + + return array_values(array_unique($this->manifest[$service]['versions'])); + } + + /** + * Execute the provider. + * + * @param string $type Type of data ('api', 'waiter', 'paginator'). + * @param string $service Service name. + * @param string $version API version. + * + * @return array|null + */ + public function __invoke($type, $service, $version) + { + // Resolve the type or return null. + if (isset(self::$typeMap[$type])) { + $type = self::$typeMap[$type]; + } else { + return null; + } + + // Resolve the version or return null. + if (!isset($this->manifest)) { + $this->buildVersionsList($service); + } + + if (!isset($this->manifest[$service]['versions'][$version])) { + return null; + } + + $version = $this->manifest[$service]['versions'][$version]; + $path = "{$this->modelsDir}/{$service}/{$version}/{$type}.json"; + + try { + return \Aws\load_compiled_json($path); + } catch (\InvalidArgumentException $e) { + return null; + } + } + + /** + * @param string $modelsDir Directory containing service models. + * @param array $manifest The API version manifest data. + */ + private function __construct($modelsDir, ?array $manifest = null) + { + $this->manifest = $manifest; + $this->modelsDir = rtrim($modelsDir, '/'); + if (!is_dir($this->modelsDir)) { + throw new \InvalidArgumentException( + "The specified models directory, {$modelsDir}, was not found." + ); + } + } + + /** + * Build the versions list for the specified service by globbing the dir. + */ + private function buildVersionsList($service) + { + $dir = "{$this->modelsDir}/{$service}/"; + + if (!is_dir($dir)) { + return; + } + + // Get versions, remove . and .., and sort in descending order. + $results = array_diff(scandir($dir, SCANDIR_SORT_DESCENDING), ['..', '.']); + + if (!$results) { + $this->manifest[$service] = ['versions' => []]; + } else { + $this->manifest[$service] = [ + 'versions' => [ + 'latest' => $results[0] + ] + ]; + $this->manifest[$service]['versions'] += array_combine($results, $results); + } + } +} diff --git a/3rdparty/aws/aws-sdk-php/src/Api/DateTimeResult.php b/3rdparty/aws/aws-sdk-php/src/Api/DateTimeResult.php new file mode 100644 index 00000000..b296abc4 --- /dev/null +++ b/3rdparty/aws/aws-sdk-php/src/Api/DateTimeResult.php @@ -0,0 +1,134 @@ +format('Y-m-d H:i:s.u'), + new DateTimeZone('UTC') + ); + } + + /** + * @return DateTimeResult + */ + public static function fromISO8601($iso8601Timestamp) + { + if (is_numeric($iso8601Timestamp) || !is_string($iso8601Timestamp)) { + throw new ParserException('Invalid timestamp value passed to DateTimeResult::fromISO8601'); + } + + // Prior to 8.0.10, nanosecond precision is not supported + // Reduces to microsecond precision if nanosecond precision is detected + if (PHP_VERSION_ID < 80010 + && preg_match(self::ISO8601_NANOSECOND_REGEX, $iso8601Timestamp, $matches) + ) { + $iso8601Timestamp = $matches[1] . ($matches[3] ?? ''); + } + + return new DateTimeResult($iso8601Timestamp); + } + + /** + * Create a new DateTimeResult from an unknown timestamp. + * + * @return DateTimeResult + * @throws Exception + */ + public static function fromTimestamp($timestamp, $expectedFormat = null) + { + if (empty($timestamp)) { + return self::fromEpoch(0); + } + + if (!(is_string($timestamp) || is_numeric($timestamp))) { + throw new ParserException('Invalid timestamp value passed to DateTimeResult::fromTimestamp'); + } + + try { + if ($expectedFormat == 'iso8601') { + try { + return self::fromISO8601($timestamp); + } catch (Exception $exception) { + return self::fromEpoch($timestamp); + } + } else if ($expectedFormat == 'unixTimestamp') { + try { + return self::fromEpoch($timestamp); + } catch (Exception $exception) { + return self::fromISO8601($timestamp); + } + } else if (\Aws\is_valid_epoch($timestamp)) { + return self::fromEpoch($timestamp); + } + return self::fromISO8601($timestamp); + } catch (Exception $exception) { + throw new ParserException('Invalid timestamp value passed to DateTimeResult::fromTimestamp'); + } + } + + /** + * Serialize the DateTimeResult as an ISO 8601 date string. + * + * @return string + */ + public function __toString() + { + return $this->format('c'); + } + + /** + * Serialize the date as an ISO 8601 date when serializing as JSON. + * + * @return string + */ + #[\ReturnTypeWillChange] + public function jsonSerialize() + { + return (string) $this; + } +} diff --git a/3rdparty/aws/aws-sdk-php/src/Api/DocModel.php b/3rdparty/aws/aws-sdk-php/src/Api/DocModel.php new file mode 100644 index 00000000..1a0ecf95 --- /dev/null +++ b/3rdparty/aws/aws-sdk-php/src/Api/DocModel.php @@ -0,0 +1,139 @@ +docs = $docs; + } + + /** + * Convert the doc model to an array. + * + * @return array + */ + public function toArray() + { + return $this->docs; + } + + /** + * Retrieves documentation about the service. + * + * @return null|string + */ + public function getServiceDocs() + { + return isset($this->docs['service']) ? $this->docs['service'] : null; + } + + /** + * Retrieves documentation about an operation. + * + * @param string $operation Name of the operation + * + * @return null|string + */ + public function getOperationDocs($operation) + { + return isset($this->docs['operations'][$operation]) + ? $this->docs['operations'][$operation] + : null; + } + + /** + * Retrieves documentation about an error. + * + * @param string $error Name of the error + * + * @return null|string + */ + public function getErrorDocs($error) + { + return isset($this->docs['shapes'][$error]['base']) + ? $this->docs['shapes'][$error]['base'] + : null; + } + + /** + * Retrieves documentation about a shape, specific to the context. + * + * @param string $shapeName Name of the shape. + * @param string $parentName Name of the parent/context shape. + * @param string $ref Name used by the context to reference the shape. + * + * @return null|string + */ + public function getShapeDocs($shapeName, $parentName, $ref) + { + if (!isset($this->docs['shapes'][$shapeName])) { + return ''; + } + + $result = ''; + $d = $this->docs['shapes'][$shapeName]; + if (isset($d['refs']["{$parentName}\${$ref}"])) { + $result = $d['refs']["{$parentName}\${$ref}"]; + } elseif (isset($d['base'])) { + $result = $d['base']; + } + + if (isset($d['append'])) { + if (!isset($d['excludeAppend']) + || !in_array($parentName, $d['excludeAppend']) + ) { + $result .= $d['append']; + } + } + + if (isset($d['appendOnly']) + && in_array($parentName, $d['appendOnly']['shapes']) + ) { + $result .= $d['appendOnly']['message']; + } + + return $this->clean($result); + } + + + private function clean($content) + { + if (!$content) { + return ''; + } + + $tidy = new \tidy(); + $tidy->parseString($content, [ + 'indent' => true, + 'doctype' => 'omit', + 'output-html' => true, + 'show-body-only' => true, + 'drop-empty-paras' => true, + 'clean' => true, + 'drop-proprietary-attributes' => true, + 'hide-comments' => true, + 'logical-emphasis' => true + ]); + $tidy->cleanRepair(); + + return (string) $content; + } +} diff --git a/3rdparty/aws/aws-sdk-php/src/Api/ErrorParser/AbstractErrorParser.php b/3rdparty/aws/aws-sdk-php/src/Api/ErrorParser/AbstractErrorParser.php new file mode 100644 index 00000000..c72f03f6 --- /dev/null +++ b/3rdparty/aws/aws-sdk-php/src/Api/ErrorParser/AbstractErrorParser.php @@ -0,0 +1,99 @@ +api = $api; + } + + abstract protected function payload( + ResponseInterface $response, + StructureShape $member + ); + + protected function extractPayload( + StructureShape $member, + ResponseInterface $response + ) { + if ($member instanceof StructureShape) { + // Structure members parse top-level data into a specific key. + return $this->payload($response, $member); + } else { + // Streaming data is just the stream from the response body. + return $response->getBody(); + } + } + + protected function populateShape( + array &$data, + ResponseInterface $response, + ?CommandInterface $command = null + ) { + $data['body'] = []; + + if (!empty($command) && !empty($this->api)) { + + // If modeled error code is indicated, check for known error shape + if (!empty($data['code'])) { + + $errors = $this->api->getOperation($command->getName())->getErrors(); + foreach ($errors as $key => $error) { + + // If error code matches a known error shape, populate the body + if ($this->errorCodeMatches($data, $error)) { + $modeledError = $error; + $data['body'] = $this->extractPayload( + $modeledError, + $response + ); + $data['error_shape'] = $modeledError; + + foreach ($error->getMembers() as $name => $member) { + switch ($member['location']) { + case 'header': + $this->extractHeader($name, $member, $response, $data['body']); + break; + case 'headers': + $this->extractHeaders($name, $member, $response, $data['body']); + break; + case 'statusCode': + $this->extractStatus($name, $response, $data['body']); + break; + } + } + + break; + } + } + } + } + + return $data; + } + + private function errorCodeMatches(array $data, $error): bool + { + return $data['code'] == $error['name'] + || (isset($error['error']['code']) && $data['code'] === $error['error']['code']); + } +} diff --git a/3rdparty/aws/aws-sdk-php/src/Api/ErrorParser/JsonParserTrait.php b/3rdparty/aws/aws-sdk-php/src/Api/ErrorParser/JsonParserTrait.php new file mode 100644 index 00000000..da825979 --- /dev/null +++ b/3rdparty/aws/aws-sdk-php/src/Api/ErrorParser/JsonParserTrait.php @@ -0,0 +1,52 @@ +getStatusCode(); + if ($this->api + && !is_null($this->api->getMetadata('awsQueryCompatible')) + && $response->getHeaderLine('x-amzn-query-error') + ) { + $queryError = $response->getHeaderLine('x-amzn-query-error'); + $parts = explode(';', $queryError); + if (isset($parts) && count($parts) == 2 && $parts[0] && $parts[1]) { + $error_code = $parts[0]; + $error_type = $parts[1]; + } + } + if (!isset($error_type)) { + $error_type = $code[0] == '4' ? 'client' : 'server'; + } + + return [ + 'request_id' => (string) $response->getHeaderLine('x-amzn-requestid'), + 'code' => isset($error_code) ? $error_code : null, + 'message' => null, + 'type' => $error_type, + 'parsed' => $this->parseJson($response->getBody(), $response) + ]; + } + + protected function payload( + ResponseInterface $response, + StructureShape $member + ) { + $jsonBody = $this->parseJson($response->getBody(), $response); + + if ($jsonBody) { + return $this->parser->parse($member, $jsonBody); + } + } +} diff --git a/3rdparty/aws/aws-sdk-php/src/Api/ErrorParser/JsonRpcErrorParser.php b/3rdparty/aws/aws-sdk-php/src/Api/ErrorParser/JsonRpcErrorParser.php new file mode 100644 index 00000000..35e8ebe0 --- /dev/null +++ b/3rdparty/aws/aws-sdk-php/src/Api/ErrorParser/JsonRpcErrorParser.php @@ -0,0 +1,47 @@ +parser = $parser ?: new JsonParser(); + } + + public function __invoke( + ResponseInterface $response, + ?CommandInterface $command = null + ) { + $data = $this->genericHandler($response); + + // Make the casing consistent across services. + if ($data['parsed']) { + $data['parsed'] = array_change_key_case($data['parsed']); + } + + if (isset($data['parsed']['__type'])) { + if (!isset($data['code'])) { + $parts = explode('#', $data['parsed']['__type']); + $data['code'] = isset($parts[1]) ? $parts[1] : $parts[0]; + } + $data['message'] = $data['parsed']['message'] ?? null; + } + + $this->populateShape($data, $response, $command); + + return $data; + } +} diff --git a/3rdparty/aws/aws-sdk-php/src/Api/ErrorParser/RestJsonErrorParser.php b/3rdparty/aws/aws-sdk-php/src/Api/ErrorParser/RestJsonErrorParser.php new file mode 100644 index 00000000..e060d104 --- /dev/null +++ b/3rdparty/aws/aws-sdk-php/src/Api/ErrorParser/RestJsonErrorParser.php @@ -0,0 +1,58 @@ +parser = $parser ?: new JsonParser(); + } + + public function __invoke( + ResponseInterface $response, + ?CommandInterface $command = null + ) { + $data = $this->genericHandler($response); + + // Merge in error data from the JSON body + if ($json = $data['parsed']) { + $data = array_replace($data, $json); + } + + // Correct error type from services like Amazon Glacier + if (!empty($data['type'])) { + $data['type'] = strtolower($data['type']); + } + + // Retrieve the error code from services like Amazon Elastic Transcoder + if ($code = $response->getHeaderLine('x-amzn-errortype')) { + $colon = strpos($code, ':'); + $data['code'] = $colon ? substr($code, 0, $colon) : $code; + } + + // Retrieve error message directly + $data['message'] = isset($data['parsed']['message']) + ? $data['parsed']['message'] + : (isset($data['parsed']['Message']) + ? $data['parsed']['Message'] + : null); + + $this->populateShape($data, $response, $command); + + return $data; + } +} diff --git a/3rdparty/aws/aws-sdk-php/src/Api/ErrorParser/XmlErrorParser.php b/3rdparty/aws/aws-sdk-php/src/Api/ErrorParser/XmlErrorParser.php new file mode 100644 index 00000000..86f5d0be --- /dev/null +++ b/3rdparty/aws/aws-sdk-php/src/Api/ErrorParser/XmlErrorParser.php @@ -0,0 +1,111 @@ +parser = $parser ?: new XmlParser(); + } + + public function __invoke( + ResponseInterface $response, + ?CommandInterface $command = null + ) { + $code = (string) $response->getStatusCode(); + + $data = [ + 'type' => $code[0] == '4' ? 'client' : 'server', + 'request_id' => null, + 'code' => null, + 'message' => null, + 'parsed' => null + ]; + + $body = $response->getBody(); + if ($body->getSize() > 0) { + $this->parseBody($this->parseXml($body, $response), $data); + } else { + $this->parseHeaders($response, $data); + } + + $this->populateShape($data, $response, $command); + + return $data; + } + + private function parseHeaders(ResponseInterface $response, array &$data) + { + if ($response->getStatusCode() == '404') { + $data['code'] = 'NotFound'; + } + + $data['message'] = $response->getStatusCode() . ' ' + . $response->getReasonPhrase(); + + if ($requestId = $response->getHeaderLine('x-amz-request-id')) { + $data['request_id'] = $requestId; + $data['message'] .= " (Request-ID: $requestId)"; + } + } + + private function parseBody(\SimpleXMLElement $body, array &$data) + { + $data['parsed'] = $body; + $prefix = $this->registerNamespacePrefix($body); + + if ($tempXml = $body->xpath("//{$prefix}Code[1]")) { + $data['code'] = (string) $tempXml[0]; + } + + if ($tempXml = $body->xpath("//{$prefix}Message[1]")) { + $data['message'] = (string) $tempXml[0]; + } + + $tempXml = $body->xpath("//{$prefix}RequestId[1]"); + if (isset($tempXml[0])) { + $data['request_id'] = (string)$tempXml[0]; + } + } + + protected function registerNamespacePrefix(\SimpleXMLElement $element) + { + $namespaces = $element->getDocNamespaces(); + if (!isset($namespaces[''])) { + return ''; + } + + // Account for the default namespace being defined and PHP not + // being able to handle it :(. + $element->registerXPathNamespace('ns', $namespaces['']); + return 'ns:'; + } + + protected function payload( + ResponseInterface $response, + StructureShape $member + ) { + $xmlBody = $this->parseXml($response->getBody(), $response); + $prefix = $this->registerNamespacePrefix($xmlBody); + $errorBody = $xmlBody->xpath("//{$prefix}Error"); + + if (is_array($errorBody) && !empty($errorBody[0])) { + return $this->parser->parse($member, $errorBody[0]); + } + } +} diff --git a/3rdparty/aws/aws-sdk-php/src/Api/ListShape.php b/3rdparty/aws/aws-sdk-php/src/Api/ListShape.php new file mode 100644 index 00000000..a425efa7 --- /dev/null +++ b/3rdparty/aws/aws-sdk-php/src/Api/ListShape.php @@ -0,0 +1,35 @@ +member) { + if (!isset($this->definition['member'])) { + throw new \RuntimeException('No member attribute specified'); + } + $this->member = Shape::create( + $this->definition['member'], + $this->shapeMap + ); + } + + return $this->member; + } +} diff --git a/3rdparty/aws/aws-sdk-php/src/Api/MapShape.php b/3rdparty/aws/aws-sdk-php/src/Api/MapShape.php new file mode 100644 index 00000000..f180f9a6 --- /dev/null +++ b/3rdparty/aws/aws-sdk-php/src/Api/MapShape.php @@ -0,0 +1,54 @@ +value) { + if (!isset($this->definition['value'])) { + throw new \RuntimeException('No value specified'); + } + + $this->value = Shape::create( + $this->definition['value'], + $this->shapeMap + ); + } + + return $this->value; + } + + /** + * @return Shape + */ + public function getKey() + { + if (!$this->key) { + $this->key = isset($this->definition['key']) + ? Shape::create($this->definition['key'], $this->shapeMap) + : new Shape(['type' => 'string'], $this->shapeMap); + } + + return $this->key; + } +} diff --git a/3rdparty/aws/aws-sdk-php/src/Api/Operation.php b/3rdparty/aws/aws-sdk-php/src/Api/Operation.php new file mode 100644 index 00000000..36ba3950 --- /dev/null +++ b/3rdparty/aws/aws-sdk-php/src/Api/Operation.php @@ -0,0 +1,158 @@ +staticContextParams = $definition['staticContextParams']; + } + + if (isset($definition['operationContextParams'])) { + $this->operationContextParams = $definition['operationContextParams']; + } + + parent::__construct($definition, $shapeMap); + $this->contextParams = $this->setContextParams(); + } + + /** + * Returns an associative array of the HTTP attribute of the operation: + * + * - method: HTTP method of the operation + * - requestUri: URI of the request (can include URI template placeholders) + * + * @return array + */ + public function getHttp() + { + return $this->definition['http']; + } + + /** + * Get the input shape of the operation. + * + * @return StructureShape + */ + public function getInput() + { + if (!$this->input) { + if ($input = $this['input']) { + $this->input = $this->shapeFor($input); + } else { + $this->input = new StructureShape([], $this->shapeMap); + } + } + + return $this->input; + } + + /** + * Get the output shape of the operation. + * + * @return StructureShape + */ + public function getOutput() + { + if (!$this->output) { + if ($output = $this['output']) { + $this->output = $this->shapeFor($output); + } else { + $this->output = new StructureShape([], $this->shapeMap); + } + } + + return $this->output; + } + + /** + * Get an array of operation error shapes. + * + * @return Shape[] + */ + public function getErrors() + { + if ($this->errors === null) { + if ($errors = $this['errors']) { + foreach ($errors as $key => $error) { + $errors[$key] = $this->shapeFor($error); + } + $this->errors = $errors; + } else { + $this->errors = []; + } + } + + return $this->errors; + } + + /** + * Gets static modeled static values used for + * endpoint resolution. + * + * @return array + */ + public function getStaticContextParams() + { + return $this->staticContextParams; + } + + /** + * Gets definition of modeled dynamic values used + * for endpoint resolution + * + * @return array + */ + public function getContextParams() + { + return $this->contextParams; + } + + /** + * Gets definition of modeled dynamic values used + * for endpoint resolution + * + * @return array + */ + public function getOperationContextParams(): array + { + return $this->operationContextParams; + } + + private function setContextParams() + { + $members = $this->getInput()->getMembers(); + $contextParams = []; + + foreach($members as $name => $shape) { + if (!empty($contextParam = $shape->getContextParam())) { + $contextParams[$contextParam['name']] = [ + 'shape' => $name, + 'type' => $shape->getType() + ]; + } + } + return $contextParams; + } +} diff --git a/3rdparty/aws/aws-sdk-php/src/Api/Parser/AbstractParser.php b/3rdparty/aws/aws-sdk-php/src/Api/Parser/AbstractParser.php new file mode 100644 index 00000000..2d515d2a --- /dev/null +++ b/3rdparty/aws/aws-sdk-php/src/Api/Parser/AbstractParser.php @@ -0,0 +1,46 @@ +api = $api; + } + + /** + * @param CommandInterface $command Command that was executed. + * @param ResponseInterface $response Response that was received. + * + * @return ResultInterface + */ + abstract public function __invoke( + CommandInterface $command, + ResponseInterface $response + ); + + abstract public function parseMemberFromStream( + StreamInterface $stream, + StructureShape $member, + $response + ); +} diff --git a/3rdparty/aws/aws-sdk-php/src/Api/Parser/AbstractRestParser.php b/3rdparty/aws/aws-sdk-php/src/Api/Parser/AbstractRestParser.php new file mode 100644 index 00000000..c977588b --- /dev/null +++ b/3rdparty/aws/aws-sdk-php/src/Api/Parser/AbstractRestParser.php @@ -0,0 +1,184 @@ +api->getOperation($command->getName())->getOutput(); + $result = []; + + if ($payload = $output['payload']) { + $this->extractPayload($payload, $output, $response, $result); + } + + foreach ($output->getMembers() as $name => $member) { + switch ($member['location']) { + case 'header': + $this->extractHeader($name, $member, $response, $result); + break; + case 'headers': + $this->extractHeaders($name, $member, $response, $result); + break; + case 'statusCode': + $this->extractStatus($name, $response, $result); + break; + } + } + + if (!$payload + && $response->getBody()->getSize() > 0 + && count($output->getMembers()) > 0 + ) { + // if no payload was found, then parse the contents of the body + $this->payload($response, $output, $result); + } + + return new Result($result); + } + + private function extractPayload( + $payload, + StructureShape $output, + ResponseInterface $response, + array &$result + ) { + $member = $output->getMember($payload); + + if (!empty($member['eventstream'])) { + $result[$payload] = new EventParsingIterator( + $response->getBody(), + $member, + $this + ); + } else if ($member instanceof StructureShape) { + // Structure members parse top-level data into a specific key. + $result[$payload] = []; + $this->payload($response, $member, $result[$payload]); + } else { + // Streaming data is just the stream from the response body. + $result[$payload] = $response->getBody(); + } + } + + /** + * Extract a single header from the response into the result. + */ + private function extractHeader( + $name, + Shape $shape, + ResponseInterface $response, + &$result + ) { + $value = $response->getHeaderLine($shape['locationName'] ?: $name); + + switch ($shape->getType()) { + case 'float': + case 'double': + $value = (float) $value; + break; + case 'long': + $value = (int) $value; + break; + case 'boolean': + $value = filter_var($value, FILTER_VALIDATE_BOOLEAN); + break; + case 'blob': + $value = base64_decode($value); + break; + case 'timestamp': + try { + $value = DateTimeResult::fromTimestamp( + $value, + !empty($shape['timestampFormat']) ? $shape['timestampFormat'] : null + ); + break; + } catch (\Exception $e) { + // If the value cannot be parsed, then do not add it to the + // output structure. + return; + } + case 'string': + try { + if ($shape['jsonvalue']) { + $value = $this->parseJson(base64_decode($value), $response); + } + + // If value is not set, do not add to output structure. + if (!isset($value)) { + return; + } + break; + } catch (\Exception $e) { + //If the value cannot be parsed, then do not add it to the + //output structure. + return; + } + } + + $result[$name] = $value; + } + + /** + * Extract a map of headers with an optional prefix from the response. + */ + private function extractHeaders( + $name, + Shape $shape, + ResponseInterface $response, + &$result + ) { + // Check if the headers are prefixed by a location name + $result[$name] = []; + $prefix = $shape['locationName']; + $prefixLen = $prefix !== null ? strlen($prefix) : 0; + + foreach ($response->getHeaders() as $k => $values) { + if (!$prefixLen) { + $result[$name][$k] = implode(', ', $values); + } elseif (stripos($k, $prefix) === 0) { + $result[$name][substr($k, $prefixLen)] = implode(', ', $values); + } + } + } + + /** + * Places the status code of the response into the result array. + */ + private function extractStatus( + $name, + ResponseInterface $response, + array &$result + ) { + $result[$name] = (int) $response->getStatusCode(); + } +} diff --git a/3rdparty/aws/aws-sdk-php/src/Api/Parser/Crc32ValidatingParser.php b/3rdparty/aws/aws-sdk-php/src/Api/Parser/Crc32ValidatingParser.php new file mode 100644 index 00000000..8e5d4f08 --- /dev/null +++ b/3rdparty/aws/aws-sdk-php/src/Api/Parser/Crc32ValidatingParser.php @@ -0,0 +1,54 @@ +parser = $parser; + } + + public function __invoke( + CommandInterface $command, + ResponseInterface $response + ) { + if ($expected = $response->getHeaderLine('x-amz-crc32')) { + $hash = hexdec(Psr7\Utils::hash($response->getBody(), 'crc32b')); + if ($expected != $hash) { + throw new AwsException( + "crc32 mismatch. Expected {$expected}, found {$hash}.", + $command, + [ + 'code' => 'ClientChecksumMismatch', + 'connection_error' => true, + 'response' => $response + ] + ); + } + } + + $fn = $this->parser; + return $fn($command, $response); + } + + public function parseMemberFromStream( + StreamInterface $stream, + StructureShape $member, + $response + ) { + return $this->parser->parseMemberFromStream($stream, $member, $response); + } +} diff --git a/3rdparty/aws/aws-sdk-php/src/Api/Parser/DecodingEventStreamIterator.php b/3rdparty/aws/aws-sdk-php/src/Api/Parser/DecodingEventStreamIterator.php new file mode 100644 index 00000000..b31cbd82 --- /dev/null +++ b/3rdparty/aws/aws-sdk-php/src/Api/Parser/DecodingEventStreamIterator.php @@ -0,0 +1,347 @@ + 'decodeUint32', + self::LENGTH_HEADERS => 'decodeUint32', + self::CRC_PRELUDE => 'decodeUint32', + ]; + + private static $lengthFormatMap = [ + 1 => 'decodeUint8', + 2 => 'decodeUint16', + 4 => 'decodeUint32', + 8 => 'decodeUint64', + ]; + + private static $headerTypeMap = [ + 0 => 'decodeBooleanTrue', + 1 => 'decodeBooleanFalse', + 2 => 'decodeInt8', + 3 => 'decodeInt16', + 4 => 'decodeInt32', + 5 => 'decodeInt64', + 6 => 'decodeBytes', + 7 => 'decodeString', + 8 => 'decodeTimestamp', + 9 => 'decodeUuid', + ]; + + /** @var StreamInterface Stream of eventstream shape to parse. */ + protected $stream; + + /** @var array Currently parsed event. */ + protected $currentEvent; + + /** @var int Current in-order event key. */ + protected $key; + + /** @var resource|\HashContext CRC32 hash context for event validation */ + protected $hashContext; + + /** @var int $currentPosition */ + protected $currentPosition; + + /** + * DecodingEventStreamIterator constructor. + * + * @param StreamInterface $stream + */ + public function __construct(StreamInterface $stream) + { + $this->stream = $stream; + $this->rewind(); + } + + protected function parseHeaders($headerBytes) + { + $headers = []; + $bytesRead = 0; + + while ($bytesRead < $headerBytes) { + list($key, $numBytes) = $this->decodeString(1); + $bytesRead += $numBytes; + + list($type, $numBytes) = $this->decodeUint8(); + $bytesRead += $numBytes; + + $f = self::$headerTypeMap[$type]; + list($value, $numBytes) = $this->{$f}(); + $bytesRead += $numBytes; + + if (isset($headers[$key])) { + throw new ParserException('Duplicate key in event headers.'); + } + $headers[$key] = $value; + } + + return [$headers, $bytesRead]; + } + + protected function parsePrelude() + { + $prelude = []; + $bytesRead = 0; + + $calculatedCrc = null; + foreach (self::$preludeFormat as $key => $decodeFunction) { + if ($key === self::CRC_PRELUDE) { + $hashCopy = hash_copy($this->hashContext); + $calculatedCrc = hash_final($this->hashContext, true); + $this->hashContext = $hashCopy; + } + list($value, $numBytes) = $this->{$decodeFunction}(); + $bytesRead += $numBytes; + + $prelude[$key] = $value; + } + + if (unpack('N', $calculatedCrc)[1] !== $prelude[self::CRC_PRELUDE]) { + throw new ParserException('Prelude checksum mismatch.'); + } + + return [$prelude, $bytesRead]; + } + + /** + * This method decodes an event from the stream. + * + * @return array + */ + protected function parseEvent() + { + $event = []; + + if ($this->stream->tell() < $this->stream->getSize()) { + $this->hashContext = hash_init('crc32b'); + + $bytesLeft = $this->stream->getSize() - $this->stream->tell(); + list($prelude, $numBytes) = $this->parsePrelude(); + if ($prelude[self::LENGTH_TOTAL] > $bytesLeft) { + throw new ParserException('Message length too long.'); + } + $bytesLeft -= $numBytes; + + if ($prelude[self::LENGTH_HEADERS] > $bytesLeft) { + throw new ParserException('Headers length too long.'); + } + + list( + $event[self::HEADERS], + $numBytes + ) = $this->parseHeaders($prelude[self::LENGTH_HEADERS]); + + $event[self::PAYLOAD] = Psr7\Utils::streamFor( + $this->readAndHashBytes( + $prelude[self::LENGTH_TOTAL] - self::BYTES_PRELUDE + - $numBytes - self::BYTES_TRAILING + ) + ); + + $calculatedCrc = hash_final($this->hashContext, true); + $messageCrc = $this->stream->read(4); + if ($calculatedCrc !== $messageCrc) { + throw new ParserException('Message checksum mismatch.'); + } + } + + return $event; + } + + // Iterator Functionality + + /** + * @return array + */ + #[\ReturnTypeWillChange] + public function current() + { + return $this->currentEvent; + } + + /** + * @return int + */ + #[\ReturnTypeWillChange] + public function key() + { + return $this->key; + } + + /** + * @return void + */ + #[\ReturnTypeWillChange] + public function next() + { + $this->currentPosition = $this->stream->tell(); + if ($this->valid()) { + $this->key++; + $this->currentEvent = $this->parseEvent(); + } + } + + /** + * @return void + */ + #[\ReturnTypeWillChange] + public function rewind() + { + $this->stream->rewind(); + $this->key = 0; + $this->currentPosition = 0; + $this->currentEvent = $this->parseEvent(); + } + + /** + * @return bool + */ + #[\ReturnTypeWillChange] + public function valid() + { + return $this->currentPosition < $this->stream->getSize(); + } + + // Decoding Utilities + + protected function readAndHashBytes($num) + { + $bytes = $this->stream->read($num); + hash_update($this->hashContext, $bytes); + return $bytes; + } + + private function decodeBooleanTrue() + { + return [true, 0]; + } + + private function decodeBooleanFalse() + { + return [false, 0]; + } + + private function uintToInt($val, $size) + { + $signedCap = pow(2, $size - 1); + if ($val > $signedCap) { + $val -= (2 * $signedCap); + } + return $val; + } + + private function decodeInt8() + { + $val = (int)unpack('C', $this->readAndHashBytes(1))[1]; + return [$this->uintToInt($val, 8), 1]; + } + + private function decodeUint8() + { + return [unpack('C', $this->readAndHashBytes(1))[1], 1]; + } + + private function decodeInt16() + { + $val = (int)unpack('n', $this->readAndHashBytes(2))[1]; + return [$this->uintToInt($val, 16), 2]; + } + + private function decodeUint16() + { + return [unpack('n', $this->readAndHashBytes(2))[1], 2]; + } + + private function decodeInt32() + { + $val = (int)unpack('N', $this->readAndHashBytes(4))[1]; + return [$this->uintToInt($val, 32), 4]; + } + + private function decodeUint32() + { + return [unpack('N', $this->readAndHashBytes(4))[1], 4]; + } + + private function decodeInt64() + { + $val = $this->unpackInt64($this->readAndHashBytes(8))[1]; + return [$this->uintToInt($val, 64), 8]; + } + + private function decodeUint64() + { + return [$this->unpackInt64($this->readAndHashBytes(8))[1], 8]; + } + + private function unpackInt64($bytes) + { + return unpack('J', $bytes); + } + + private function decodeBytes($lengthBytes=2) + { + if (!isset(self::$lengthFormatMap[$lengthBytes])) { + throw new ParserException('Undefined variable length format.'); + } + $f = self::$lengthFormatMap[$lengthBytes]; + list($len, $bytes) = $this->{$f}(); + return [$this->readAndHashBytes($len), $len + $bytes]; + } + + private function decodeString($lengthBytes=2) + { + if (!isset(self::$lengthFormatMap[$lengthBytes])) { + throw new ParserException('Undefined variable length format.'); + } + $f = self::$lengthFormatMap[$lengthBytes]; + list($len, $bytes) = $this->{$f}(); + return [$this->readAndHashBytes($len), $len + $bytes]; + } + + private function decodeTimestamp() + { + list($val, $bytes) = $this->decodeInt64(); + return [ + DateTimeResult::createFromFormat('U.u', $val / 1000), + $bytes + ]; + } + + private function decodeUuid() + { + $val = unpack('H32', $this->readAndHashBytes(16))[1]; + return [ + substr($val, 0, 8) . '-' + . substr($val, 8, 4) . '-' + . substr($val, 12, 4) . '-' + . substr($val, 16, 4) . '-' + . substr($val, 20, 12), + 16 + ]; + } +} diff --git a/3rdparty/aws/aws-sdk-php/src/Api/Parser/EventParsingIterator.php b/3rdparty/aws/aws-sdk-php/src/Api/Parser/EventParsingIterator.php new file mode 100644 index 00000000..b14c5fcb --- /dev/null +++ b/3rdparty/aws/aws-sdk-php/src/Api/Parser/EventParsingIterator.php @@ -0,0 +1,211 @@ +decodingIterator = $this->chooseDecodingIterator($stream); + $this->shape = $shape; + $this->parser = $parser; + } + + /** + * This method choose a decoding iterator implementation based on if the stream + * is seekable or not. + * + * @param $stream + * + * @return Iterator + */ + private function chooseDecodingIterator($stream) + { + if ($stream->isSeekable()) { + return new DecodingEventStreamIterator($stream); + } else { + return new NonSeekableStreamDecodingEventStreamIterator($stream); + } + } + + /** + * @return mixed + */ + #[\ReturnTypeWillChange] + public function current() + { + return $this->parseEvent($this->decodingIterator->current()); + } + + /** + * @return mixed + */ + #[\ReturnTypeWillChange] + public function key() + { + return $this->decodingIterator->key(); + } + + /** + * @return void + */ + #[\ReturnTypeWillChange] + public function next() + { + $this->decodingIterator->next(); + } + + /** + * @return void + */ + #[\ReturnTypeWillChange] + public function rewind() + { + $this->decodingIterator->rewind(); + } + + /** + * @return bool + */ + #[\ReturnTypeWillChange] + public function valid() + { + return $this->decodingIterator->valid(); + } + + private function parseEvent(array $event) + { + if (!empty($event['headers'][':message-type'])) { + if ($event['headers'][':message-type'] === 'error') { + return $this->parseError($event); + } + + if ($event['headers'][':message-type'] === 'exception') { + return $this->parseException($event); + } + + if ($event['headers'][':message-type'] !== 'event') { + throw new ParserException('Failed to parse unknown message type.'); + } + } + + $eventType = $event['headers'][':event-type'] ?? null; + if (empty($eventType)) { + throw new ParserException('Failed to parse without event type.'); + } + + $eventPayload = $event['payload']; + if ($eventType === 'initial-response') { + return $this->parseInitialResponseEvent($eventPayload); + } + + $eventShape = $this->shape->getMember($eventType); + + return [ + $eventType => array_merge( + $this->parseEventHeaders($event['headers'], $eventShape), + $this->parseEventPayload($eventPayload, $eventShape) + ) + ]; + } + + /** + * @param $headers + * @param $eventShape + * + * @return array + */ + private function parseEventHeaders($headers, $eventShape): array + { + $parsedHeaders = []; + foreach ($eventShape->getMembers() as $memberName => $memberProps) { + if (isset($memberProps['eventheader'])) { + $parsedHeaders[$memberName] = $headers[$memberName]; + } + } + + return $parsedHeaders; + } + + /** + * @param $payload + * @param $eventShape + * + * @return array + */ + private function parseEventPayload($payload, $eventShape): array + { + $parsedPayload = []; + foreach ($eventShape->getMembers() as $memberName => $memberProps) { + $memberShape = $eventShape->getMember($memberName); + if (isset($memberProps['eventpayload'])) { + if ($memberShape->getType() === 'blob') { + $parsedPayload[$memberName] = $payload; + } else { + $parsedPayload[$memberName] = $this->parser->parseMemberFromStream( + $payload, + $memberShape, + null + ); + } + + break; + } + } + + if (empty($parsedPayload) && !empty($payload->getContents())) { + /** + * If we did not find a member with an eventpayload trait, then we should deserialize the payload + * using the event's shape. + */ + $parsedPayload = $this->parser->parseMemberFromStream($payload, $eventShape, null); + } + + return $parsedPayload; + } + + private function parseError(array $event) + { + throw new EventStreamDataException( + $event['headers'][':error-code'], + $event['headers'][':error-message'] + ); + } + + private function parseException(array $event) { + $payload = $event['payload']?->getContents(); + $parsedPayload = json_decode($payload, true); + + throw new EventStreamDataException( + $event['headers'][':exception-type'] ?? 'Unknown', + $parsedPayload['message'] ?? $payload, + ); + } + + private function parseInitialResponseEvent($payload): array + { + return ['initial-response' => json_decode($payload, true)]; + } +} diff --git a/3rdparty/aws/aws-sdk-php/src/Api/Parser/Exception/ParserException.php b/3rdparty/aws/aws-sdk-php/src/Api/Parser/Exception/ParserException.php new file mode 100644 index 00000000..f5fd9ec9 --- /dev/null +++ b/3rdparty/aws/aws-sdk-php/src/Api/Parser/Exception/ParserException.php @@ -0,0 +1,56 @@ +errorCode = isset($context['error_code']) ? $context['error_code'] : null; + $this->requestId = isset($context['request_id']) ? $context['request_id'] : null; + $this->response = isset($context['response']) ? $context['response'] : null; + parent::__construct($message, $code, $previous); + } + + /** + * Get the error code, if any. + * + * @return string|null + */ + public function getErrorCode() + { + return $this->errorCode; + } + + /** + * Get the request ID, if any. + * + * @return string|null + */ + public function getRequestId() + { + return $this->requestId; + } + + /** + * Get the received HTTP response if any. + * + * @return ResponseInterface|null + */ + public function getResponse() + { + return $this->response; + } +} diff --git a/3rdparty/aws/aws-sdk-php/src/Api/Parser/JsonParser.php b/3rdparty/aws/aws-sdk-php/src/Api/Parser/JsonParser.php new file mode 100644 index 00000000..ef981541 --- /dev/null +++ b/3rdparty/aws/aws-sdk-php/src/Api/Parser/JsonParser.php @@ -0,0 +1,71 @@ +getMembers() as $name => $member) { + $locationName = $member['locationName'] ?: $name; + if (isset($value[$locationName])) { + $target[$name] = $this->parse($member, $value[$locationName]); + } + } + if (isset($shape['union']) + && $shape['union'] + && is_array($value) + && empty($target) + ) { + foreach ($value as $key => $val) { + $target['Unknown'][$key] = $val; + } + } + return $target; + + case 'list': + $member = $shape->getMember(); + $target = []; + foreach ($value as $v) { + $target[] = $this->parse($member, $v); + } + return $target; + + case 'map': + $values = $shape->getValue(); + $target = []; + foreach ($value as $k => $v) { + $target[$k] = $this->parse($values, $v); + } + return $target; + + case 'timestamp': + return DateTimeResult::fromTimestamp( + $value, + !empty($shape['timestampFormat']) ? $shape['timestampFormat'] : null + ); + + case 'blob': + return base64_decode($value); + + default: + return $value; + } + } +} + diff --git a/3rdparty/aws/aws-sdk-php/src/Api/Parser/JsonRpcParser.php b/3rdparty/aws/aws-sdk-php/src/Api/Parser/JsonRpcParser.php new file mode 100644 index 00000000..cd6549c7 --- /dev/null +++ b/3rdparty/aws/aws-sdk-php/src/Api/Parser/JsonRpcParser.php @@ -0,0 +1,82 @@ +parser = $parser ?: new JsonParser(); + } + + public function __invoke( + CommandInterface $command, + ResponseInterface $response + ) { + $operation = $this->api->getOperation($command->getName()); + + return $this->parseResponse($response, $operation); + } + + /** + * This method parses a response based on JSON RPC protocol. + * + * @param ResponseInterface $response the response to parse. + * @param Operation $operation the operation which holds information for + * parsing the response. + * + * @return Result + */ + private function parseResponse(ResponseInterface $response, Operation $operation) + { + if (null === $operation['output']) { + return new Result([]); + } + + $outputShape = $operation->getOutput(); + foreach ($outputShape->getMembers() as $memberName => $memberProps) { + if (!empty($memberProps['eventstream'])) { + return new Result([ + $memberName => new EventParsingIterator( + $response->getBody(), + $outputShape->getMember($memberName), + $this + ) + ]); + } + } + + $result = $this->parseMemberFromStream( + $response->getBody(), + $operation->getOutput(), + $response + ); + + return new Result(is_null($result) ? [] : $result); + } + + public function parseMemberFromStream( + StreamInterface $stream, + StructureShape $member, + $response + ) { + return $this->parser->parse($member, $this->parseJson($stream, $response)); + } +} diff --git a/3rdparty/aws/aws-sdk-php/src/Api/Parser/MetadataParserTrait.php b/3rdparty/aws/aws-sdk-php/src/Api/Parser/MetadataParserTrait.php new file mode 100644 index 00000000..e64d5a85 --- /dev/null +++ b/3rdparty/aws/aws-sdk-php/src/Api/Parser/MetadataParserTrait.php @@ -0,0 +1,90 @@ +getHeaderLine($shape['locationName'] ?: $name); + + switch ($shape->getType()) { + case 'float': + case 'double': + $value = (float) $value; + break; + case 'long': + $value = (int) $value; + break; + case 'boolean': + $value = filter_var($value, FILTER_VALIDATE_BOOLEAN); + break; + case 'blob': + $value = base64_decode($value); + break; + case 'timestamp': + try { + $value = DateTimeResult::fromTimestamp( + $value, + !empty($shape['timestampFormat']) ? $shape['timestampFormat'] : null + ); + break; + } catch (\Exception $e) { + // If the value cannot be parsed, then do not add it to the + // output structure. + return; + } + case 'string': + if ($shape['jsonvalue']) { + $value = $this->parseJson(base64_decode($value), $response); + } + break; + } + + $result[$name] = $value; + } + + /** + * Extract a map of headers with an optional prefix from the response. + */ + protected function extractHeaders( + $name, + Shape $shape, + ResponseInterface $response, + &$result + ) { + // Check if the headers are prefixed by a location name + $result[$name] = []; + $prefix = $shape['locationName']; + $prefixLen = strlen($prefix); + + foreach ($response->getHeaders() as $k => $values) { + if (!$prefixLen) { + $result[$name][$k] = implode(', ', $values); + } elseif (stripos($k, $prefix) === 0) { + $result[$name][substr($k, $prefixLen)] = implode(', ', $values); + } + } + } + + /** + * Places the status code of the response into the result array. + */ + protected function extractStatus( + $name, + ResponseInterface $response, + array &$result + ) { + $result[$name] = (int) $response->getStatusCode(); + } +} diff --git a/3rdparty/aws/aws-sdk-php/src/Api/Parser/NonSeekableStreamDecodingEventStreamIterator.php b/3rdparty/aws/aws-sdk-php/src/Api/Parser/NonSeekableStreamDecodingEventStreamIterator.php new file mode 100644 index 00000000..ca5388db --- /dev/null +++ b/3rdparty/aws/aws-sdk-php/src/Api/Parser/NonSeekableStreamDecodingEventStreamIterator.php @@ -0,0 +1,101 @@ +stream = $stream; + if ($this->stream->isSeekable()) { + throw new \InvalidArgumentException('The stream provided must be not seekable.'); + } + + $this->tempBuffer = []; + } + + /** + * @inheritDoc + * + * @return array + */ + protected function parseEvent(): array + { + $event = []; + $this->hashContext = hash_init('crc32b'); + $prelude = $this->parsePrelude()[0]; + list( + $event[self::HEADERS], + $numBytes + ) = $this->parseHeaders($prelude[self::LENGTH_HEADERS]); + $event[self::PAYLOAD] = Psr7\Utils::streamFor( + $this->readAndHashBytes( + $prelude[self::LENGTH_TOTAL] - self::BYTES_PRELUDE + - $numBytes - self::BYTES_TRAILING + ) + ); + $calculatedCrc = hash_final($this->hashContext, true); + $messageCrc = $this->stream->read(4); + if ($calculatedCrc !== $messageCrc) { + throw new ParserException('Message checksum mismatch.'); + } + + return $event; + } + + protected function readAndHashBytes($num): string + { + $bytes = ''; + while (!empty($this->tempBuffer) && $num > 0) { + $byte = array_shift($this->tempBuffer); + $bytes .= $byte; + $num = $num - 1; + } + + $bytes = $bytes . $this->stream->read($num); + hash_update($this->hashContext, $bytes); + + return $bytes; + } + + // Iterator Functionality + + #[\ReturnTypeWillChange] + public function rewind() + { + $this->currentEvent = $this->parseEvent(); + } + + public function next() + { + $this->tempBuffer[] = $this->stream->read(1); + if ($this->valid()) { + $this->key++; + $this->currentEvent = $this->parseEvent(); + } + } + + /** + * @return bool + */ + #[\ReturnTypeWillChange] + public function valid() + { + return !$this->stream->eof(); + } +} diff --git a/3rdparty/aws/aws-sdk-php/src/Api/Parser/PayloadParserTrait.php b/3rdparty/aws/aws-sdk-php/src/Api/Parser/PayloadParserTrait.php new file mode 100644 index 00000000..43d3d567 --- /dev/null +++ b/3rdparty/aws/aws-sdk-php/src/Api/Parser/PayloadParserTrait.php @@ -0,0 +1,61 @@ + $response] + ); + } + + return $jsonPayload; + } + + /** + * @param string $xml + * + * @throws ParserException + * + * @return \SimpleXMLElement + */ + protected function parseXml($xml, $response) + { + $priorSetting = libxml_use_internal_errors(true); + try { + libxml_clear_errors(); + $xmlPayload = new \SimpleXMLElement($xml); + if ($error = libxml_get_last_error()) { + throw new \RuntimeException($error->message); + } + } catch (\Exception $e) { + throw new ParserException( + "Error parsing XML: {$e->getMessage()}", + 0, + $e, + ['response' => $response] + ); + } finally { + libxml_use_internal_errors($priorSetting); + } + + return $xmlPayload; + } +} diff --git a/3rdparty/aws/aws-sdk-php/src/Api/Parser/QueryParser.php b/3rdparty/aws/aws-sdk-php/src/Api/Parser/QueryParser.php new file mode 100644 index 00000000..7762ac59 --- /dev/null +++ b/3rdparty/aws/aws-sdk-php/src/Api/Parser/QueryParser.php @@ -0,0 +1,60 @@ +parser = $xmlParser ?: new XmlParser(); + $this->honorResultWrapper = $honorResultWrapper; + } + + public function __invoke( + CommandInterface $command, + ResponseInterface $response + ) { + $output = $this->api->getOperation($command->getName())->getOutput(); + $xml = $this->parseXml($response->getBody(), $response); + + if ($this->honorResultWrapper && $output['resultWrapper']) { + $xml = $xml->{$output['resultWrapper']}; + } + + return new Result($this->parser->parse($output, $xml)); + } + + public function parseMemberFromStream( + StreamInterface $stream, + StructureShape $member, + $response + ) { + $xml = $this->parseXml($stream, $response); + return $this->parser->parse($member, $xml); + } +} diff --git a/3rdparty/aws/aws-sdk-php/src/Api/Parser/RestJsonParser.php b/3rdparty/aws/aws-sdk-php/src/Api/Parser/RestJsonParser.php new file mode 100644 index 00000000..bb09f401 --- /dev/null +++ b/3rdparty/aws/aws-sdk-php/src/Api/Parser/RestJsonParser.php @@ -0,0 +1,49 @@ +parser = $parser ?: new JsonParser(); + } + + protected function payload( + ResponseInterface $response, + StructureShape $member, + array &$result + ) { + $jsonBody = $this->parseJson($response->getBody(), $response); + + if ($jsonBody) { + $result += $this->parser->parse($member, $jsonBody); + } + } + + public function parseMemberFromStream( + StreamInterface $stream, + StructureShape $member, + $response + ) { + $jsonBody = $this->parseJson($stream, $response); + if ($jsonBody) { + return $this->parser->parse($member, $jsonBody); + } + return []; + } +} diff --git a/3rdparty/aws/aws-sdk-php/src/Api/Parser/RestXmlParser.php b/3rdparty/aws/aws-sdk-php/src/Api/Parser/RestXmlParser.php new file mode 100644 index 00000000..057c00ce --- /dev/null +++ b/3rdparty/aws/aws-sdk-php/src/Api/Parser/RestXmlParser.php @@ -0,0 +1,42 @@ +parser = $parser ?: new XmlParser(); + } + + protected function payload( + ResponseInterface $response, + StructureShape $member, + array &$result + ) { + $result += $this->parseMemberFromStream($response->getBody(), $member, $response); + } + + public function parseMemberFromStream( + StreamInterface $stream, + StructureShape $member, + $response + ) { + $xml = $this->parseXml($stream, $response); + return $this->parser->parse($member, $xml); + } +} diff --git a/3rdparty/aws/aws-sdk-php/src/Api/Parser/XmlParser.php b/3rdparty/aws/aws-sdk-php/src/Api/Parser/XmlParser.php new file mode 100644 index 00000000..744f02e7 --- /dev/null +++ b/3rdparty/aws/aws-sdk-php/src/Api/Parser/XmlParser.php @@ -0,0 +1,179 @@ +dispatch($shape, $value); + } + + private function dispatch($shape, \SimpleXMLElement $value) + { + static $methods = [ + 'structure' => 'parse_structure', + 'list' => 'parse_list', + 'map' => 'parse_map', + 'blob' => 'parse_blob', + 'boolean' => 'parse_boolean', + 'integer' => 'parse_integer', + 'float' => 'parse_float', + 'double' => 'parse_float', + 'timestamp' => 'parse_timestamp', + ]; + + $type = $shape['type']; + if (isset($methods[$type])) { + return $this->{$methods[$type]}($shape, $value); + } + + return (string) $value; + } + + private function parse_structure( + StructureShape $shape, + \SimpleXMLElement $value + ) { + $target = []; + + foreach ($shape->getMembers() as $name => $member) { + // Extract the name of the XML node + $node = $this->memberKey($member, $name); + if (isset($value->{$node})) { + $target[$name] = $this->dispatch($member, $value->{$node}); + } else { + $memberShape = $shape->getMember($name); + if (!empty($memberShape['xmlAttribute'])) { + $target[$name] = $this->parse_xml_attribute( + $shape, + $memberShape, + $value + ); + } + } + } + if (isset($shape['union']) + && $shape['union'] + && empty($target) + ) { + foreach ($value as $key => $val) { + $name = $val->children()->getName(); + $target['Unknown'][$name] = $val->$name; + } + } + return $target; + } + + private function memberKey(Shape $shape, $name) + { + if (null !== $shape['locationName']) { + return $shape['locationName']; + } + + if ($shape instanceof ListShape && $shape['flattened']) { + return $shape->getMember()['locationName'] ?: $name; + } + + return $name; + } + + private function parse_list(ListShape $shape, \SimpleXMLElement $value) + { + $target = []; + $member = $shape->getMember(); + + if (!$shape['flattened']) { + $value = $value->{$member['locationName'] ?: 'member'}; + } + + foreach ($value as $v) { + $target[] = $this->dispatch($member, $v); + } + + return $target; + } + + private function parse_map(MapShape $shape, \SimpleXMLElement $value) + { + $target = []; + + if (!$shape['flattened']) { + $value = $value->entry; + } + + $mapKey = $shape->getKey(); + $mapValue = $shape->getValue(); + $keyName = $shape->getKey()['locationName'] ?: 'key'; + $valueName = $shape->getValue()['locationName'] ?: 'value'; + + foreach ($value as $node) { + $key = $this->dispatch($mapKey, $node->{$keyName}); + $value = $this->dispatch($mapValue, $node->{$valueName}); + $target[$key] = $value; + } + + return $target; + } + + private function parse_blob(Shape $shape, $value) + { + return base64_decode((string) $value); + } + + private function parse_float(Shape $shape, $value) + { + return (float) (string) $value; + } + + private function parse_integer(Shape $shape, $value) + { + return (int) (string) $value; + } + + private function parse_boolean(Shape $shape, $value) + { + return $value == 'true'; + } + + private function parse_timestamp(Shape $shape, $value) + { + if (is_string($value) + || is_int($value) + || (is_object($value) + && method_exists($value, '__toString')) + ) { + return DateTimeResult::fromTimestamp( + (string) $value, + !empty($shape['timestampFormat']) ? $shape['timestampFormat'] : null + ); + } + throw new ParserException('Invalid timestamp value passed to XmlParser::parse_timestamp'); + } + + private function parse_xml_attribute(Shape $shape, Shape $memberShape, $value) + { + $namespace = $shape['xmlNamespace']['uri'] + ? $shape['xmlNamespace']['uri'] + : ''; + $prefix = $shape['xmlNamespace']['prefix'] + ? $shape['xmlNamespace']['prefix'] + : ''; + if (!empty($prefix)) { + $prefix .= ':'; + } + $key = str_replace($prefix, '', $memberShape['locationName']); + + $attributes = $value->attributes($namespace); + return isset($attributes[$key]) ? (string) $attributes[$key] : null; + } +} diff --git a/3rdparty/aws/aws-sdk-php/src/Api/Serializer/Ec2ParamBuilder.php b/3rdparty/aws/aws-sdk-php/src/Api/Serializer/Ec2ParamBuilder.php new file mode 100644 index 00000000..a3753cdf --- /dev/null +++ b/3rdparty/aws/aws-sdk-php/src/Api/Serializer/Ec2ParamBuilder.php @@ -0,0 +1,38 @@ +getMember(); + foreach ($value as $k => $v) { + $this->format($items, $v, $prefix . '.' . ($k + 1), $query); + } + } + } +} diff --git a/3rdparty/aws/aws-sdk-php/src/Api/Serializer/JsonBody.php b/3rdparty/aws/aws-sdk-php/src/Api/Serializer/JsonBody.php new file mode 100644 index 00000000..0b116e0e --- /dev/null +++ b/3rdparty/aws/aws-sdk-php/src/Api/Serializer/JsonBody.php @@ -0,0 +1,108 @@ +api = $api; + } + + /** + * Gets the JSON Content-Type header for a service API + * + * @param Service $service + * + * @return string + */ + public static function getContentType(Service $service) + { + if ($service->getMetadata('protocol') === 'rest-json') { + return 'application/json'; + } + + $jsonVersion = $service->getMetadata('jsonVersion'); + if (empty($jsonVersion)) { + throw new \InvalidArgumentException('invalid json'); + } else { + return 'application/x-amz-json-' + . @number_format($service->getMetadata('jsonVersion'), 1); + } + } + + /** + * Builds the JSON body based on an array of arguments. + * + * @param Shape $shape Operation being constructed + * @param array $args Associative array of arguments + * + * @return string + */ + public function build(Shape $shape, array $args) + { + $result = json_encode($this->format($shape, $args)); + return $result == '[]' ? '{}' : $result; + } + + private function format(Shape $shape, $value) + { + switch ($shape['type']) { + case 'structure': + $data = []; + if (isset($shape['document']) && $shape['document']) { + return $value; + } + foreach ($value as $k => $v) { + if ($v !== null && $shape->hasMember($k)) { + $valueShape = $shape->getMember($k); + $data[$valueShape['locationName'] ?: $k] + = $this->format($valueShape, $v); + } + } + if (empty($data)) { + return new \stdClass; + } + return $data; + + case 'list': + $items = $shape->getMember(); + foreach ($value as $k => $v) { + $value[$k] = $this->format($items, $v); + } + return $value; + + case 'map': + if (empty($value)) { + return new \stdClass; + } + $values = $shape->getValue(); + foreach ($value as $k => $v) { + $value[$k] = $this->format($values, $v); + } + return $value; + + case 'blob': + return base64_encode($value); + + case 'timestamp': + $timestampFormat = !empty($shape['timestampFormat']) + ? $shape['timestampFormat'] + : 'unixTimestamp'; + return TimestampShape::format($value, $timestampFormat); + + default: + return $value; + } + } +} diff --git a/3rdparty/aws/aws-sdk-php/src/Api/Serializer/JsonRpcSerializer.php b/3rdparty/aws/aws-sdk-php/src/Api/Serializer/JsonRpcSerializer.php new file mode 100644 index 00000000..61845b08 --- /dev/null +++ b/3rdparty/aws/aws-sdk-php/src/Api/Serializer/JsonRpcSerializer.php @@ -0,0 +1,84 @@ +endpoint = $endpoint; + $this->api = $api; + $this->jsonFormatter = $jsonFormatter ?: new JsonBody($this->api); + $this->contentType = JsonBody::getContentType($api); + } + + /** + * When invoked with an AWS command, returns a serialization array + * containing "method", "uri", "headers", and "body" key value pairs. + * + * @param CommandInterface $command Command to serialize into a request. + * @param $endpointProvider Provider used for dynamic endpoint resolution. + * @param $clientArgs Client arguments used for dynamic endpoint resolution. + * + * @return RequestInterface + */ + public function __invoke( + CommandInterface $command, + $endpoint = null + ) + { + $operationName = $command->getName(); + $operation = $this->api->getOperation($operationName); + $commandArgs = $command->toArray(); + $headers = [ + 'X-Amz-Target' => $this->api->getMetadata('targetPrefix') . '.' . $operationName, + 'Content-Type' => $this->contentType + ]; + + if ($endpoint instanceof RulesetEndpoint) { + $this->setEndpointV2RequestOptions($endpoint, $headers); + } + + return new Request( + $operation['http']['method'], + $this->endpoint, + $headers, + $this->jsonFormatter->build( + $operation->getInput(), + $commandArgs + ) + ); + } +} diff --git a/3rdparty/aws/aws-sdk-php/src/Api/Serializer/QueryParamBuilder.php b/3rdparty/aws/aws-sdk-php/src/Api/Serializer/QueryParamBuilder.php new file mode 100644 index 00000000..3d96334e --- /dev/null +++ b/3rdparty/aws/aws-sdk-php/src/Api/Serializer/QueryParamBuilder.php @@ -0,0 +1,157 @@ +isFlat($shape) && !empty($shape['member']['locationName'])) { + return $shape['member']['locationName']; + } + + return $default; + } + + protected function isFlat(Shape $shape) + { + return $shape['flattened'] === true; + } + + public function __invoke(StructureShape $shape, array $params) + { + if (!$this->methods) { + $this->methods = array_fill_keys(get_class_methods($this), true); + } + + $query = []; + $this->format_structure($shape, $params, '', $query); + + return $query; + } + + protected function format(Shape $shape, $value, $prefix, array &$query) + { + $type = 'format_' . $shape['type']; + if (isset($this->methods[$type])) { + $this->{$type}($shape, $value, $prefix, $query); + } else { + $query[$prefix] = (string) $value; + } + } + + protected function format_structure( + StructureShape $shape, + array $value, + $prefix, + &$query + ) { + if ($prefix) { + $prefix .= '.'; + } + + foreach ($value as $k => $v) { + if ($shape->hasMember($k)) { + $member = $shape->getMember($k); + $this->format( + $member, + $v, + $prefix . $this->queryName($member, $k), + $query + ); + } + } + } + + protected function format_list( + ListShape $shape, + array $value, + $prefix, + &$query + ) { + // Handle empty list serialization + if (!$value) { + $query[$prefix] = ''; + return; + } + + $items = $shape->getMember(); + + if (!$this->isFlat($shape)) { + $locationName = $shape->getMember()['locationName'] ?: 'member'; + $prefix .= ".$locationName"; + } elseif ($name = $this->queryName($items)) { + $parts = explode('.', $prefix); + $parts[count($parts) - 1] = $name; + $prefix = implode('.', $parts); + } + + foreach ($value as $k => $v) { + $this->format($items, $v, $prefix . '.' . ($k + 1), $query); + } + } + + protected function format_map( + MapShape $shape, + array $value, + $prefix, + array &$query + ) { + $vals = $shape->getValue(); + $keys = $shape->getKey(); + + if (!$this->isFlat($shape)) { + $prefix .= '.entry'; + } + + $i = 0; + $keyName = '%s.%d.' . $this->queryName($keys, 'key'); + $valueName = '%s.%s.' . $this->queryName($vals, 'value'); + + foreach ($value as $k => $v) { + $i++; + $this->format($keys, $k, sprintf($keyName, $prefix, $i), $query); + $this->format($vals, $v, sprintf($valueName, $prefix, $i), $query); + } + } + + protected function format_blob(Shape $shape, $value, $prefix, array &$query) + { + $query[$prefix] = base64_encode($value); + } + + protected function format_timestamp( + TimestampShape $shape, + $value, + $prefix, + array &$query + ) { + $timestampFormat = !empty($shape['timestampFormat']) + ? $shape['timestampFormat'] + : 'iso8601'; + $query[$prefix] = TimestampShape::format($value, $timestampFormat); + } + + protected function format_boolean(Shape $shape, $value, $prefix, array &$query) + { + $query[$prefix] = ($value) ? 'true' : 'false'; + } +} diff --git a/3rdparty/aws/aws-sdk-php/src/Api/Serializer/QuerySerializer.php b/3rdparty/aws/aws-sdk-php/src/Api/Serializer/QuerySerializer.php new file mode 100644 index 00000000..1a55b03e --- /dev/null +++ b/3rdparty/aws/aws-sdk-php/src/Api/Serializer/QuerySerializer.php @@ -0,0 +1,81 @@ +api = $api; + $this->endpoint = $endpoint; + $this->paramBuilder = $paramBuilder ?: new QueryParamBuilder(); + } + + /** + * When invoked with an AWS command, returns a serialization array + * containing "method", "uri", "headers", and "body" key value pairs. + * + * @param CommandInterface $command Command to serialize into a request. + * @param $endpointProvider Provider used for dynamic endpoint resolution. + * @param $clientArgs Client arguments used for dynamic endpoint resolution. + * + * @return RequestInterface + */ + public function __invoke( + CommandInterface $command, + $endpoint = null + ) + { + $operation = $this->api->getOperation($command->getName()); + $body = [ + 'Action' => $command->getName(), + 'Version' => $this->api->getMetadata('apiVersion') + ]; + $commandArgs = $command->toArray(); + + // Only build up the parameters when there are parameters to build + if ($commandArgs) { + $body += call_user_func( + $this->paramBuilder, + $operation->getInput(), + $commandArgs + ); + } + $body = http_build_query($body, '', '&', PHP_QUERY_RFC3986); + $headers = [ + 'Content-Length' => strlen($body), + 'Content-Type' => 'application/x-www-form-urlencoded' + ]; + + if ($endpoint instanceof RulesetEndpoint) { + $this->setEndpointV2RequestOptions($endpoint, $headers); + } + + return new Request( + 'POST', + $this->endpoint, + $headers, + $body + ); + } +} diff --git a/3rdparty/aws/aws-sdk-php/src/Api/Serializer/RestJsonSerializer.php b/3rdparty/aws/aws-sdk-php/src/Api/Serializer/RestJsonSerializer.php new file mode 100644 index 00000000..2e281211 --- /dev/null +++ b/3rdparty/aws/aws-sdk-php/src/Api/Serializer/RestJsonSerializer.php @@ -0,0 +1,42 @@ +contentType = JsonBody::getContentType($api); + $this->jsonFormatter = $jsonFormatter ?: new JsonBody($api); + } + + protected function payload(StructureShape $member, array $value, array &$opts) + { + $body = isset($value) ? + ((string) $this->jsonFormatter->build($member, $value)) + : "{}"; + $opts['headers']['Content-Type'] = $this->contentType; + $opts['body'] = $body; + } +} diff --git a/3rdparty/aws/aws-sdk-php/src/Api/Serializer/RestSerializer.php b/3rdparty/aws/aws-sdk-php/src/Api/Serializer/RestSerializer.php new file mode 100644 index 00000000..dfc13362 --- /dev/null +++ b/3rdparty/aws/aws-sdk-php/src/Api/Serializer/RestSerializer.php @@ -0,0 +1,330 @@ +api = $api; + $this->endpoint = Psr7\Utils::uriFor($endpoint); + } + + /** + * @param CommandInterface $command Command to serialize into a request. + * @param $clientArgs Client arguments used for dynamic endpoint resolution. + * + * @return RequestInterface + */ + public function __invoke( + CommandInterface $command, + $endpoint = null + ) + { + $operation = $this->api->getOperation($command->getName()); + $commandArgs = $command->toArray(); + $opts = $this->serialize($operation, $commandArgs); + $headers = isset($opts['headers']) ? $opts['headers'] : []; + + if ($endpoint instanceof RulesetEndpoint) { + $this->isUseEndpointV2 = true; + $this->setEndpointV2RequestOptions($endpoint, $headers); + } + + $uri = $this->buildEndpoint($operation, $commandArgs, $opts); + + return new Request( + $operation['http']['method'], + $uri, + $headers, + isset($opts['body']) ? $opts['body'] : null + ); + } + + /** + * Modifies a hash of request options for a payload body. + * + * @param StructureShape $member Member to serialize + * @param array $value Value to serialize + * @param array $opts Request options to modify. + */ + abstract protected function payload( + StructureShape $member, + array $value, + array &$opts + ); + + private function serialize(Operation $operation, array $args) + { + $opts = []; + $input = $operation->getInput(); + + // Apply the payload trait if present + if ($payload = $input['payload']) { + $this->applyPayload($input, $payload, $args, $opts); + } + + foreach ($args as $name => $value) { + if ($input->hasMember($name)) { + $member = $input->getMember($name); + $location = $member['location']; + if (!$payload && !$location) { + $bodyMembers[$name] = $value; + } elseif ($location == 'header') { + $this->applyHeader($name, $member, $value, $opts); + } elseif ($location == 'querystring') { + $this->applyQuery($name, $member, $value, $opts); + } elseif ($location == 'headers') { + $this->applyHeaderMap($name, $member, $value, $opts); + } + } + } + + if (isset($bodyMembers)) { + $this->payload($operation->getInput(), $bodyMembers, $opts); + } else if (!isset($opts['body']) && $this->hasPayloadParam($input, $payload)) { + $this->payload($operation->getInput(), [], $opts); + } + + return $opts; + } + + private function applyPayload(StructureShape $input, $name, array $args, array &$opts) + { + if (!isset($args[$name])) { + return; + } + + $m = $input->getMember($name); + + if ($m['streaming'] || + ($m['type'] == 'string' || $m['type'] == 'blob') + ) { + // Streaming bodies or payloads that are strings are + // always just a stream of data. + $opts['body'] = Psr7\Utils::streamFor($args[$name]); + return; + } + + $this->payload($m, $args[$name], $opts); + } + + private function applyHeader($name, Shape $member, $value, array &$opts) + { + if ($member->getType() === 'timestamp') { + $timestampFormat = !empty($member['timestampFormat']) + ? $member['timestampFormat'] + : 'rfc822'; + $value = TimestampShape::format($value, $timestampFormat); + } elseif ($member->getType() === 'boolean') { + $value = $value ? 'true' : 'false'; + } + + if ($member['jsonvalue']) { + $value = json_encode($value); + if (empty($value) && JSON_ERROR_NONE !== json_last_error()) { + throw new \InvalidArgumentException('Unable to encode the provided value' + . ' with \'json_encode\'. ' . json_last_error_msg()); + } + + $value = base64_encode($value); + } + + $opts['headers'][$member['locationName'] ?: $name] = $value; + } + + /** + * Note: This is currently only present in the Amazon S3 model. + */ + private function applyHeaderMap($name, Shape $member, array $value, array &$opts) + { + $prefix = $member['locationName']; + foreach ($value as $k => $v) { + $opts['headers'][$prefix . $k] = $v; + } + } + + private function applyQuery($name, Shape $member, $value, array &$opts) + { + if ($member instanceof MapShape) { + $opts['query'] = isset($opts['query']) && is_array($opts['query']) + ? $opts['query'] + $value + : $value; + } elseif ($value !== null) { + $type = $member->getType(); + if ($type === 'boolean') { + $value = $value ? 'true' : 'false'; + } elseif ($type === 'timestamp') { + $timestampFormat = !empty($member['timestampFormat']) + ? $member['timestampFormat'] + : 'iso8601'; + $value = TimestampShape::format($value, $timestampFormat); + } + + $opts['query'][$member['locationName'] ?: $name] = $value; + } + } + + private function buildEndpoint(Operation $operation, array $args, array $opts) + { + $serviceName = $this->api->getServiceName(); + // Create an associative array of variable definitions used in expansions + $varDefinitions = $this->getVarDefinitions($operation, $args); + + $relative = preg_replace_callback( + '/\{([^\}]+)\}/', + function (array $matches) use ($varDefinitions) { + $isGreedy = substr($matches[1], -1, 1) == '+'; + $k = $isGreedy ? substr($matches[1], 0, -1) : $matches[1]; + if (!isset($varDefinitions[$k])) { + return ''; + } + + if ($isGreedy) { + return str_replace('%2F', '/', rawurlencode($varDefinitions[$k])); + } + + return rawurlencode($varDefinitions[$k]); + }, + $operation['http']['requestUri'] + ); + + // Add the query string variables or appending to one if needed. + if (!empty($opts['query'])) { + $relative = $this->appendQuery($opts['query'], $relative); + } + + $path = $this->endpoint->getPath(); + + if ($this->isUseEndpointV2 && $serviceName === 's3') { + if (substr($path, -1) === '/' && $relative[0] === '/') { + $path = rtrim($path, '/'); + } + $relative = $path . $relative; + + if (strpos($relative, '../') !== false + || substr($relative, -2) === '..' + ) { + if ($relative[0] !== '/') { + $relative = '/' . $relative; + } + + return new Uri($this->endpoint->withPath('') . $relative); + } + } + + if (((!empty($relative) && $relative !== '/') + && !$this->isUseEndpointV2) + || (isset($serviceName) && str_starts_with($serviceName, 'geo-')) + ) { + $this->normalizePath($path); + } + + // If endpoint has path, remove leading '/' to preserve URI resolution. + if ($path && $relative[0] === '/') { + $relative = substr($relative, 1); + } + + //Append path to endpoint when leading '//...' + // present as uri cannot be properly resolved + if ($this->isUseEndpointV2 && strpos($relative, '//') === 0) { + return new Uri($this->endpoint . $relative); + } + + // Expand path place holders using Amazon's slightly different URI + // template syntax. + return UriResolver::resolve($this->endpoint, new Uri($relative)); + } + + /** + * @param StructureShape $input + */ + private function hasPayloadParam(StructureShape $input, $payload) + { + if ($payload) { + $potentiallyEmptyTypes = ['blob','string']; + if ($this->api->getMetadata('protocol') == 'rest-xml') { + $potentiallyEmptyTypes[] = 'structure'; + } + $payloadMember = $input->getMember($payload); + if (in_array($payloadMember['type'], $potentiallyEmptyTypes)) { + return false; + } + } + foreach ($input->getMembers() as $member) { + if (!isset($member['location'])) { + return true; + } + } + return false; + } + + private function appendQuery($query, $endpoint) + { + $append = Psr7\Query::build($query); + return $endpoint .= strpos($endpoint, '?') !== false ? "&{$append}" : "?{$append}"; + } + + private function getVarDefinitions($command, $args) + { + $varDefinitions = []; + + foreach ($command->getInput()->getMembers() as $name => $member) { + if ($member['location'] == 'uri') { + $varDefinitions[$member['locationName'] ?: $name] = + isset($args[$name]) + ? $args[$name] + : null; + } + } + return $varDefinitions; + } + + /** + * Appends trailing slash to non-empty paths with at least one segment + * to ensure proper URI resolution + * + * @param string $path + * + * @return void + */ + private function normalizePath(string $path): void + { + if (!empty($path) && $path !== '/' && substr($path, -1) !== '/') { + $this->endpoint = $this->endpoint->withPath($path . '/'); + } + } +} diff --git a/3rdparty/aws/aws-sdk-php/src/Api/Serializer/RestXmlSerializer.php b/3rdparty/aws/aws-sdk-php/src/Api/Serializer/RestXmlSerializer.php new file mode 100644 index 00000000..5e7bc646 --- /dev/null +++ b/3rdparty/aws/aws-sdk-php/src/Api/Serializer/RestXmlSerializer.php @@ -0,0 +1,48 @@ +xmlBody = $xmlBody ?: new XmlBody($api); + } + + protected function payload(StructureShape $member, array $value, array &$opts) + { + $opts['headers']['Content-Type'] = 'application/xml'; + $opts['body'] = $this->getXmlBody($member, $value); + } + + /** + * @param StructureShape $member + * @param array $value + * @return string + */ + private function getXmlBody(StructureShape $member, array $value) + { + $xmlBody = (string)$this->xmlBody->build($member, $value); + $xmlBody = str_replace("'", "'", $xmlBody); + $xmlBody = str_replace('\r', " ", $xmlBody); + $xmlBody = str_replace('\n', " ", $xmlBody); + return $xmlBody; + } +} diff --git a/3rdparty/aws/aws-sdk-php/src/Api/Serializer/XmlBody.php b/3rdparty/aws/aws-sdk-php/src/Api/Serializer/XmlBody.php new file mode 100644 index 00000000..0488eba3 --- /dev/null +++ b/3rdparty/aws/aws-sdk-php/src/Api/Serializer/XmlBody.php @@ -0,0 +1,220 @@ +api = $api; + } + + /** + * Builds the XML body based on an array of arguments. + * + * @param Shape $shape Operation being constructed + * @param array $args Associative array of arguments + * + * @return string + */ + public function build(Shape $shape, array $args) + { + $xml = new XMLWriter(); + $xml->openMemory(); + $xml->startDocument('1.0', 'UTF-8'); + $this->format($shape, $shape['locationName'] ?: $shape['name'], $args, $xml); + $xml->endDocument(); + + return $xml->outputMemory(); + } + + private function startElement(Shape $shape, $name, XMLWriter $xml) + { + $xml->startElement($name); + + if ($ns = $shape['xmlNamespace']) { + $xml->writeAttribute( + isset($ns['prefix']) ? "xmlns:{$ns['prefix']}" : 'xmlns', + $shape['xmlNamespace']['uri'] + ); + } + } + + private function format(Shape $shape, $name, $value, XMLWriter $xml) + { + // Any method mentioned here has a custom serialization handler. + static $methods = [ + 'add_structure' => true, + 'add_list' => true, + 'add_blob' => true, + 'add_timestamp' => true, + 'add_boolean' => true, + 'add_map' => true, + 'add_string' => true + ]; + + $type = 'add_' . $shape['type']; + if (isset($methods[$type])) { + $this->{$type}($shape, $name, $value, $xml); + } else { + $this->defaultShape($shape, $name, $value, $xml); + } + } + + private function defaultShape(Shape $shape, $name, $value, XMLWriter $xml) + { + $this->startElement($shape, $name, $xml); + $xml->text($value); + $xml->endElement(); + } + + private function add_structure( + StructureShape $shape, + $name, + array $value, + \XMLWriter $xml + ) { + $this->startElement($shape, $name, $xml); + + foreach ($this->getStructureMembers($shape, $value) as $k => $definition) { + $this->format( + $definition['member'], + $definition['member']['locationName'] ?: $k, + $definition['value'], + $xml + ); + } + + $xml->endElement(); + } + + private function getStructureMembers(StructureShape $shape, array $value) + { + $members = []; + + foreach ($value as $k => $v) { + if ($v !== null && $shape->hasMember($k)) { + $definition = [ + 'member' => $shape->getMember($k), + 'value' => $v, + ]; + + if ($definition['member']['xmlAttribute']) { + // array_unshift_associative + $members = [$k => $definition] + $members; + } else { + $members[$k] = $definition; + } + } + } + + return $members; + } + + private function add_list( + ListShape $shape, + $name, + array $value, + XMLWriter $xml + ) { + $items = $shape->getMember(); + + if ($shape['flattened']) { + $elementName = $name; + } else { + $this->startElement($shape, $name, $xml); + $elementName = $items['locationName'] ?: 'member'; + } + + foreach ($value as $v) { + $this->format($items, $elementName, $v, $xml); + } + + if (!$shape['flattened']) { + $xml->endElement(); + } + } + + private function add_map( + MapShape $shape, + $name, + array $value, + XMLWriter $xml + ) { + $xmlEntry = $shape['flattened'] ? $shape['locationName'] : 'entry'; + $xmlKey = $shape->getKey()['locationName'] ?: 'key'; + $xmlValue = $shape->getValue()['locationName'] ?: 'value'; + + $this->startElement($shape, $name, $xml); + + foreach ($value as $key => $v) { + $this->startElement($shape, $xmlEntry, $xml); + $this->format($shape->getKey(), $xmlKey, $key, $xml); + $this->format($shape->getValue(), $xmlValue, $v, $xml); + $xml->endElement(); + } + + $xml->endElement(); + } + + private function add_blob(Shape $shape, $name, $value, XMLWriter $xml) + { + $this->startElement($shape, $name, $xml); + $xml->writeRaw(base64_encode($value)); + $xml->endElement(); + } + + private function add_timestamp( + TimestampShape $shape, + $name, + $value, + XMLWriter $xml + ) { + $this->startElement($shape, $name, $xml); + $timestampFormat = !empty($shape['timestampFormat']) + ? $shape['timestampFormat'] + : 'iso8601'; + $xml->writeRaw(TimestampShape::format($value, $timestampFormat)); + $xml->endElement(); + } + + private function add_boolean( + Shape $shape, + $name, + $value, + XMLWriter $xml + ) { + $this->startElement($shape, $name, $xml); + $xml->writeRaw($value ? 'true' : 'false'); + $xml->endElement(); + } + + private function add_string( + Shape $shape, + $name, + $value, + XMLWriter $xml + ) { + if ($shape['xmlAttribute']) { + $xml->writeAttribute($shape['locationName'] ?: $name, $value); + } else { + $this->defaultShape($shape, $name, $value, $xml); + } + } +} diff --git a/3rdparty/aws/aws-sdk-php/src/Api/Service.php b/3rdparty/aws/aws-sdk-php/src/Api/Service.php new file mode 100644 index 00000000..38bd4513 --- /dev/null +++ b/3rdparty/aws/aws-sdk-php/src/Api/Service.php @@ -0,0 +1,564 @@ + [], + 'shapes' => [], + 'metadata' => [], + 'clientContextParams' => [] + ], $defaultMeta = [ + 'apiVersion' => null, + 'serviceFullName' => null, + 'serviceId' => null, + 'endpointPrefix' => null, + 'signingName' => null, + 'signatureVersion' => null, + 'protocol' => null, + 'uid' => null + ]; + + $definition += $defaults; + $definition['metadata'] += $defaultMeta; + $this->definition = $definition; + $this->apiProvider = $provider; + parent::__construct($definition, new ShapeMap($definition['shapes'])); + + if (isset($definition['metadata']['serviceIdentifier'])) { + $this->serviceName = $this->getServiceName(); + } else { + $this->serviceName = $this->getEndpointPrefix(); + } + $this->apiVersion = $this->getApiVersion(); + if (isset($definition['clientContextParams'])) { + $this->clientContextParams = $definition['clientContextParams']; + } + + $this->protocol = $this->selectProtocol($definition); + } + + /** + * Creates a request serializer for the provided API object. + * + * @param Service $api API that contains a protocol. + * @param string $endpoint Endpoint to send requests to. + * + * @return callable + * @throws \UnexpectedValueException + */ + public static function createSerializer(Service $api, $endpoint) + { + static $mapping = [ + 'json' => Serializer\JsonRpcSerializer::class, + 'query' => Serializer\QuerySerializer::class, + 'rest-json' => Serializer\RestJsonSerializer::class, + 'rest-xml' => Serializer\RestXmlSerializer::class + ]; + + $proto = $api->getProtocol(); + + if (isset($mapping[$proto])) { + return new $mapping[$proto]($api, $endpoint); + } + + if ($proto == 'ec2') { + return new Serializer\QuerySerializer($api, $endpoint, new Serializer\Ec2ParamBuilder()); + } + + throw new \UnexpectedValueException( + 'Unknown protocol: ' . $api->getProtocol() + ); + } + + /** + * Creates an error parser for the given protocol. + * + * Redundant method signature to preserve backwards compatibility. + * + * @param string $protocol Protocol to parse (e.g., query, json, etc.) + * + * @return callable + * @throws \UnexpectedValueException + */ + public static function createErrorParser($protocol, ?Service $api = null) + { + static $mapping = [ + 'json' => ErrorParser\JsonRpcErrorParser::class, + 'query' => ErrorParser\XmlErrorParser::class, + 'rest-json' => ErrorParser\RestJsonErrorParser::class, + 'rest-xml' => ErrorParser\XmlErrorParser::class, + 'ec2' => ErrorParser\XmlErrorParser::class + ]; + + if (isset($mapping[$protocol])) { + return new $mapping[$protocol]($api); + } + + throw new \UnexpectedValueException("Unknown protocol: $protocol"); + } + + /** + * Applies the listeners needed to parse client models. + * + * @param Service $api API to create a parser for + * @return callable + * @throws \UnexpectedValueException + */ + public static function createParser(Service $api) + { + static $mapping = [ + 'json' => Parser\JsonRpcParser::class, + 'query' => Parser\QueryParser::class, + 'rest-json' => Parser\RestJsonParser::class, + 'rest-xml' => Parser\RestXmlParser::class + ]; + + $proto = $api->getProtocol(); + if (isset($mapping[$proto])) { + return new $mapping[$proto]($api); + } + + if ($proto == 'ec2') { + return new Parser\QueryParser($api, null, false); + } + + throw new \UnexpectedValueException( + 'Unknown protocol: ' . $api->getProtocol() + ); + } + + /** + * Get the full name of the service + * + * @return string + */ + public function getServiceFullName() + { + return $this->definition['metadata']['serviceFullName']; + } + + /** + * Get the service id + * + * @return string + */ + public function getServiceId() + { + return $this->definition['metadata']['serviceId']; + } + + /** + * Get the API version of the service + * + * @return string + */ + public function getApiVersion() + { + return $this->definition['metadata']['apiVersion']; + } + + /** + * Get the API version of the service + * + * @return string + */ + public function getEndpointPrefix() + { + return $this->definition['metadata']['endpointPrefix']; + } + + /** + * Get the signing name used by the service. + * + * @return string + */ + public function getSigningName() + { + return $this->definition['metadata']['signingName'] + ?: $this->definition['metadata']['endpointPrefix']; + } + + /** + * Get the service name. + * + * @return string + */ + public function getServiceName() + { + return $this->definition['metadata']['serviceIdentifier'] ?? null; + } + + /** + * Get the default signature version of the service. + * + * Note: this method assumes "v4" when not specified in the model. + * + * @return string + */ + public function getSignatureVersion() + { + return $this->definition['metadata']['signatureVersion'] ?: 'v4'; + } + + /** + * Get the protocol used by the service. + * + * @return string + */ + public function getProtocol() + { + return $this->protocol; + } + + /** + * Get the uid string used by the service + * + * @return string + */ + public function getUid() + { + return $this->definition['metadata']['uid']; + } + + /** + * Check if the description has a specific operation by name. + * + * @param string $name Operation to check by name + * + * @return bool + */ + public function hasOperation($name) + { + return isset($this['operations'][$name]); + } + + /** + * Get an operation by name. + * + * @param string $name Operation to retrieve by name + * + * @return Operation + * @throws \InvalidArgumentException If the operation is not found + */ + public function getOperation($name) + { + if (!isset($this->operations[$name])) { + if (!isset($this->definition['operations'][$name])) { + throw new \InvalidArgumentException("Unknown operation: $name"); + } + $this->operations[$name] = new Operation( + $this->definition['operations'][$name], + $this->shapeMap + ); + } elseif ($this->modifiedModel) { + $this->operations[$name] = new Operation( + $this->definition['operations'][$name], + $this->shapeMap + ); + } + + return $this->operations[$name]; + } + + /** + * Get all of the operations of the description. + * + * @return Operation[] + */ + public function getOperations() + { + $result = []; + foreach ($this->definition['operations'] as $name => $definition) { + $result[$name] = $this->getOperation($name); + } + + return $result; + } + + /** + * Get all of the error shapes of the service + * + * @return array + */ + public function getErrorShapes() + { + $result = []; + foreach ($this->definition['shapes'] as $name => $definition) { + if (!empty($definition['exception'])) { + $definition['name'] = $name; + $result[] = new StructureShape($definition, $this->getShapeMap()); + } + } + + return $result; + } + + /** + * Get all of the service metadata or a specific metadata key value. + * + * @param string|null $key Key to retrieve or null to retrieve all metadata + * + * @return mixed Returns the result or null if the key is not found + */ + public function getMetadata($key = null) + { + if (!$key) { + return $this['metadata']; + } + + if (isset($this->definition['metadata'][$key])) { + return $this->definition['metadata'][$key]; + } + + return null; + } + + /** + * Gets an associative array of available paginator configurations where + * the key is the name of the paginator, and the value is the paginator + * configuration. + * + * @return array + * @unstable The configuration format of paginators may change in the future + */ + public function getPaginators() + { + if (!isset($this->paginators)) { + $res = call_user_func( + $this->apiProvider, + 'paginator', + $this->serviceName, + $this->apiVersion + ); + $this->paginators = isset($res['pagination']) + ? $res['pagination'] + : []; + } + + return $this->paginators; + } + + /** + * Determines if the service has a paginator by name. + * + * @param string $name Name of the paginator. + * + * @return bool + */ + public function hasPaginator($name) + { + return isset($this->getPaginators()[$name]); + } + + /** + * Retrieve a paginator by name. + * + * @param string $name Paginator to retrieve by name. This argument is + * typically the operation name. + * @return array + * @throws \UnexpectedValueException if the paginator does not exist. + * @unstable The configuration format of paginators may change in the future + */ + public function getPaginatorConfig($name) + { + static $defaults = [ + 'input_token' => null, + 'output_token' => null, + 'limit_key' => null, + 'result_key' => null, + 'more_results' => null, + ]; + + if ($this->hasPaginator($name)) { + return $this->paginators[$name] + $defaults; + } + + throw new \UnexpectedValueException("There is no {$name} " + . "paginator defined for the {$this->serviceName} service."); + } + + /** + * Gets an associative array of available waiter configurations where the + * key is the name of the waiter, and the value is the waiter + * configuration. + * + * @return array + */ + public function getWaiters() + { + if (!isset($this->waiters)) { + $res = call_user_func( + $this->apiProvider, + 'waiter', + $this->serviceName, + $this->apiVersion + ); + $this->waiters = isset($res['waiters']) + ? $res['waiters'] + : []; + } + + return $this->waiters; + } + + /** + * Determines if the service has a waiter by name. + * + * @param string $name Name of the waiter. + * + * @return bool + */ + public function hasWaiter($name) + { + return isset($this->getWaiters()[$name]); + } + + /** + * Get a waiter configuration by name. + * + * @param string $name Name of the waiter by name. + * + * @return array + * @throws \UnexpectedValueException if the waiter does not exist. + */ + public function getWaiterConfig($name) + { + // Error if the waiter is not defined + if ($this->hasWaiter($name)) { + return $this->waiters[$name]; + } + + throw new \UnexpectedValueException("There is no {$name} waiter " + . "defined for the {$this->serviceName} service."); + } + + /** + * Get the shape map used by the API. + * + * @return ShapeMap + */ + public function getShapeMap() + { + return $this->shapeMap; + } + + /** + * Get all the context params of the description. + * + * @return array + */ + public function getClientContextParams() + { + return $this->clientContextParams; + } + + /** + * Get the service's api provider. + * + * @return callable + */ + public function getProvider() + { + return $this->apiProvider; + } + + /** + * Get the service's definition. + * + * @return callable + */ + public function getDefinition() + { + return $this->definition; + } + + /** + * Sets the service's api definition. + * Intended for internal use only. + * + * @return void + * + * @internal + */ + public function setDefinition($definition) + { + $this->definition = $definition; + $this->shapeMap = new ShapeMap($definition['shapes']); + $this->modifiedModel = true; + } + + /** + * Denotes whether or not a service's definition has + * been modified. Intended for internal use only. + * + * @return bool + * + * @internal + */ + public function isModifiedModel() + { + return $this->modifiedModel; + } + + /** + * Accepts a list of protocols derived from the service model. + * Returns the highest priority compatible auth scheme if the `protocols` trait is present. + * Otherwise, returns the value of the `protocol` field, if set, or null. + * + * @param array $definition + * + * @return string|null + */ + private function selectProtocol(array $definition): string | null + { + $modeledProtocols = $definition['metadata']['protocols'] ?? null; + if (!empty($modeledProtocols)) { + foreach(SupportedProtocols::cases() as $protocol) { + if (in_array($protocol->value, $modeledProtocols)) { + return $protocol->value; + } + } + } + + return $definition['metadata']['protocol'] ?? null; + } +} diff --git a/3rdparty/aws/aws-sdk-php/src/Api/Shape.php b/3rdparty/aws/aws-sdk-php/src/Api/Shape.php new file mode 100644 index 00000000..765efc03 --- /dev/null +++ b/3rdparty/aws/aws-sdk-php/src/Api/Shape.php @@ -0,0 +1,77 @@ + StructureShape::class, + 'map' => MapShape::class, + 'list' => ListShape::class, + 'timestamp' => TimestampShape::class, + 'integer' => Shape::class, + 'double' => Shape::class, + 'float' => Shape::class, + 'long' => Shape::class, + 'string' => Shape::class, + 'byte' => Shape::class, + 'character' => Shape::class, + 'blob' => Shape::class, + 'boolean' => Shape::class + ]; + + if (isset($definition['shape'])) { + return $shapeMap->resolve($definition); + } + + if (!isset($map[$definition['type']])) { + throw new \RuntimeException('Invalid type: ' + . print_r($definition, true)); + } + + $type = $map[$definition['type']]; + + return new $type($definition, $shapeMap); + } + + /** + * Get the type of the shape + * + * @return string + */ + public function getType() + { + return $this->definition['type']; + } + + /** + * Get the name of the shape + * + * @return string + */ + public function getName() + { + return $this->definition['name']; + } + + /** + * Get a context param definition. + */ + public function getContextParam() + { + return $this->contextParam; + } +} diff --git a/3rdparty/aws/aws-sdk-php/src/Api/ShapeMap.php b/3rdparty/aws/aws-sdk-php/src/Api/ShapeMap.php new file mode 100644 index 00000000..b576e9ba --- /dev/null +++ b/3rdparty/aws/aws-sdk-php/src/Api/ShapeMap.php @@ -0,0 +1,68 @@ +definitions = $shapeModels; + } + + /** + * Get an array of shape names. + * + * @return array + */ + public function getShapeNames() + { + return array_keys($this->definitions); + } + + /** + * Resolve a shape reference + * + * @param array $shapeRef Shape reference shape + * + * @return Shape + * @throws \InvalidArgumentException + */ + public function resolve(array $shapeRef) + { + $shape = $shapeRef['shape']; + + if (!isset($this->definitions[$shape])) { + throw new \InvalidArgumentException('Shape not found: ' . $shape); + } + + $isSimple = count($shapeRef) == 1; + if ($isSimple && isset($this->simple[$shape])) { + return $this->simple[$shape]; + } + + $definition = $shapeRef + $this->definitions[$shape]; + $definition['name'] = $definition['shape']; + if (isset($definition['shape'])) { + unset($definition['shape']); + } + + $result = Shape::create($definition, $this); + + if ($isSimple) { + $this->simple[$shape] = $result; + } + + return $result; + } +} diff --git a/3rdparty/aws/aws-sdk-php/src/Api/StructureShape.php b/3rdparty/aws/aws-sdk-php/src/Api/StructureShape.php new file mode 100644 index 00000000..a2879359 --- /dev/null +++ b/3rdparty/aws/aws-sdk-php/src/Api/StructureShape.php @@ -0,0 +1,79 @@ +members)) { + $this->generateMembersHash(); + } + + return $this->members; + } + + /** + * Check if a specific member exists by name. + * + * @param string $name Name of the member to check + * + * @return bool + */ + public function hasMember($name) + { + return isset($this->definition['members'][$name]); + } + + /** + * Retrieve a member by name. + * + * @param string $name Name of the member to retrieve + * + * @return Shape + * @throws \InvalidArgumentException if the member is not found. + */ + public function getMember($name) + { + $members = $this->getMembers(); + + if (!isset($members[$name])) { + throw new \InvalidArgumentException('Unknown member ' . $name); + } + + return $members[$name]; + } + + + private function generateMembersHash() + { + $this->members = []; + + foreach ($this->definition['members'] as $name => $definition) { + $this->members[$name] = $this->shapeFor($definition); + } + } +} diff --git a/3rdparty/aws/aws-sdk-php/src/Api/SupportedProtocols.php b/3rdparty/aws/aws-sdk-php/src/Api/SupportedProtocols.php new file mode 100644 index 00000000..8a06bd0e --- /dev/null +++ b/3rdparty/aws/aws-sdk-php/src/Api/SupportedProtocols.php @@ -0,0 +1,26 @@ +getTimestamp(); + } elseif (is_string($value)) { + $value = strtotime($value); + } elseif (!is_int($value)) { + throw new \InvalidArgumentException('Unable to handle the provided' + . ' timestamp type: ' . gettype($value)); + } + + switch ($format) { + case 'iso8601': + return gmdate('Y-m-d\TH:i:s\Z', $value); + case 'rfc822': + return gmdate('D, d M Y H:i:s \G\M\T', $value); + case 'unixTimestamp': + return $value; + default: + throw new \UnexpectedValueException('Unknown timestamp format: ' + . $format); + } + } +} diff --git a/3rdparty/aws/aws-sdk-php/src/Api/Validator.php b/3rdparty/aws/aws-sdk-php/src/Api/Validator.php new file mode 100644 index 00000000..88609264 --- /dev/null +++ b/3rdparty/aws/aws-sdk-php/src/Api/Validator.php @@ -0,0 +1,346 @@ + true, + 'min' => true, + 'max' => false, + 'pattern' => false + ]; + + /** + * @param array $constraints Associative array of constraints to enforce. + * Accepts the following keys: "required", "min", + * "max", and "pattern". If a key is not + * provided, the constraint will assume false. + */ + public function __construct(?array $constraints = null) + { + static $assumedFalseValues = [ + 'required' => false, + 'min' => false, + 'max' => false, + 'pattern' => false + ]; + $this->constraints = empty($constraints) + ? self::$defaultConstraints + : $constraints + $assumedFalseValues; + } + + /** + * Validates the given input against the schema. + * + * @param string $name Operation name + * @param Shape $shape Shape to validate + * @param array $input Input to validate + * + * @throws \InvalidArgumentException if the input is invalid. + */ + public function validate($name, Shape $shape, array $input) + { + $this->dispatch($shape, $input); + + if ($this->errors) { + $message = sprintf( + "Found %d error%s while validating the input provided for the " + . "%s operation:\n%s", + count($this->errors), + count($this->errors) > 1 ? 's' : '', + $name, + implode("\n", $this->errors) + ); + $this->errors = []; + + throw new \InvalidArgumentException($message); + } + } + + private function dispatch(Shape $shape, $value) + { + static $methods = [ + 'structure' => 'check_structure', + 'list' => 'check_list', + 'map' => 'check_map', + 'blob' => 'check_blob', + 'boolean' => 'check_boolean', + 'integer' => 'check_numeric', + 'float' => 'check_numeric', + 'long' => 'check_numeric', + 'string' => 'check_string', + 'byte' => 'check_string', + 'char' => 'check_string' + ]; + + $type = $shape->getType(); + if (isset($methods[$type])) { + $this->{$methods[$type]}($shape, $value); + } + } + + private function check_structure(StructureShape $shape, $value) + { + $isDocument = (isset($shape['document']) && $shape['document']); + $isUnion = (isset($shape['union']) && $shape['union']); + if ($isDocument) { + if (!$this->checkDocumentType($value)) { + $this->addError("is not a valid document type"); + return; + } + } elseif ($isUnion) { + if (!$this->checkUnion($value)) { + $this->addError("is a union type and must have exactly one non null value"); + return; + } + } elseif (!$this->checkAssociativeArray($value)) { + return; + } + + if ($this->constraints['required'] && $shape['required']) { + foreach ($shape['required'] as $req) { + if (!isset($value[$req])) { + $this->path[] = $req; + $this->addError('is missing and is a required parameter'); + array_pop($this->path); + } + } + } + if (!$isDocument) { + foreach ($value as $name => $v) { + if ($shape->hasMember($name)) { + $this->path[] = $name; + $this->dispatch( + $shape->getMember($name), + isset($value[$name]) ? $value[$name] : null + ); + array_pop($this->path); + } + } + } + } + + private function check_list(ListShape $shape, $value) + { + if (!is_array($value)) { + $this->addError('must be an array. Found ' + . Aws\describe_type($value)); + return; + } + + $this->validateRange($shape, count($value), "list element count"); + + $items = $shape->getMember(); + foreach ($value as $index => $v) { + $this->path[] = $index; + $this->dispatch($items, $v); + array_pop($this->path); + } + } + + private function check_map(MapShape $shape, $value) + { + if (!$this->checkAssociativeArray($value)) { + return; + } + + $values = $shape->getValue(); + foreach ($value as $key => $v) { + $this->path[] = $key; + $this->dispatch($values, $v); + array_pop($this->path); + } + } + + private function check_blob(Shape $shape, $value) + { + static $valid = [ + 'string' => true, + 'integer' => true, + 'double' => true, + 'resource' => true + ]; + + $type = gettype($value); + if (!isset($valid[$type])) { + if ($type != 'object' || !method_exists($value, '__toString')) { + $this->addError('must be an fopen resource, a ' + . 'GuzzleHttp\Stream\StreamInterface object, or something ' + . 'that can be cast to a string. Found ' + . Aws\describe_type($value)); + } + } + } + + private function check_numeric(Shape $shape, $value) + { + if (!is_numeric($value)) { + $this->addError('must be numeric. Found ' + . Aws\describe_type($value)); + return; + } + + $this->validateRange($shape, $value, "numeric value"); + } + + private function check_boolean(Shape $shape, $value) + { + if (!is_bool($value)) { + $this->addError('must be a boolean. Found ' + . Aws\describe_type($value)); + } + } + + private function check_string(Shape $shape, $value) + { + if ($shape['jsonvalue']) { + if (!self::canJsonEncode($value)) { + $this->addError('must be a value encodable with \'json_encode\'.' + . ' Found ' . Aws\describe_type($value)); + } + return; + } + + if (!$this->checkCanString($value)) { + $this->addError('must be a string or an object that implements ' + . '__toString(). Found ' . Aws\describe_type($value)); + return; + } + + $value = isset($value) ? $value : ''; + $this->validateRange($shape, strlen($value), "string length"); + + if ($this->constraints['pattern']) { + $pattern = $shape['pattern']; + if ($pattern && !preg_match("/$pattern/", $value)) { + $this->addError("Pattern /$pattern/ failed to match '$value'"); + } + } + } + + private function validateRange(Shape $shape, $length, $descriptor) + { + if ($this->constraints['min']) { + $min = $shape['min']; + if ($min && $length < $min) { + $this->addError("expected $descriptor to be >= $min, but " + . "found $descriptor of $length"); + } + } + + if ($this->constraints['max']) { + $max = $shape['max']; + if ($max && $length > $max) { + $this->addError("expected $descriptor to be <= $max, but " + . "found $descriptor of $length"); + } + } + } + + private function checkArray($arr) + { + return $this->isIndexed($arr) || $this->isAssociative($arr); + } + + private function isAssociative($arr) + { + return count(array_filter(array_keys($arr), "is_string")) == count($arr); + } + + private function isIndexed(array $arr) + { + return $arr == array_values($arr); + } + + private function checkCanString($value) + { + static $valid = [ + 'string' => true, + 'integer' => true, + 'double' => true, + 'NULL' => true, + ]; + + $type = gettype($value); + + return isset($valid[$type]) || + ($type == 'object' && method_exists($value, '__toString')); + } + + private function checkAssociativeArray($value) + { + $isAssociative = false; + + if (is_array($value)) { + $expectedIndex = 0; + $key = key($value); + + do { + $isAssociative = $key !== $expectedIndex++; + next($value); + $key = key($value); + } while (!$isAssociative && null !== $key); + } + + if (!$isAssociative) { + $this->addError('must be an associative array. Found ' + . Aws\describe_type($value)); + return false; + } + + return true; + } + + private function checkDocumentType($value) + { + if (is_array($value)) { + $typeOfFirstKey = gettype(key($value)); + foreach ($value as $key => $val) { + if (!$this->checkDocumentType($val) || gettype($key) != $typeOfFirstKey) { + return false; + } + } + return $this->checkArray($value); + } + return is_null($value) + || is_numeric($value) + || is_string($value) + || is_bool($value); + } + + private function checkUnion($value) + { + if (is_array($value)) { + $nonNullCount = 0; + foreach ($value as $key => $val) { + if (!is_null($val) && !(strpos($key, "@") === 0)) { + $nonNullCount++; + } + } + return $nonNullCount == 1; + } + return !is_null($value); + } + + private function addError($message) + { + $this->errors[] = + implode('', array_map(function ($s) { return "[{$s}]"; }, $this->path)) + . ' ' + . $message; + } + + private function canJsonEncode($data) + { + return !is_resource($data); + } +} diff --git a/3rdparty/aws/aws-sdk-php/src/Arn/AccessPointArn.php b/3rdparty/aws/aws-sdk-php/src/Arn/AccessPointArn.php new file mode 100644 index 00000000..e7b88eed --- /dev/null +++ b/3rdparty/aws/aws-sdk-php/src/Arn/AccessPointArn.php @@ -0,0 +1,66 @@ +data); + } + + public static function parse($string) + { + $data = parent::parse($string); + $data = self::parseResourceTypeAndId($data); + $data['accesspoint_name'] = $data['resource_id']; + return $data; + } + + public function getAccesspointName() + { + return $this->data['accesspoint_name']; + } + + /** + * Validation specific to AccessPointArn + * + * @param array $data + */ + protected static function validate(array $data) + { + self::validateRegion($data, 'access point ARN'); + self::validateAccountId($data, 'access point ARN'); + + if ($data['resource_type'] !== 'accesspoint') { + throw new InvalidArnException("The 6th component of an access point ARN" + . " represents the resource type and must be 'accesspoint'."); + } + + if (empty($data['resource_id'])) { + throw new InvalidArnException("The 7th component of an access point ARN" + . " represents the resource ID and must not be empty."); + } + if (strpos($data['resource_id'], ':') !== false) { + throw new InvalidArnException("The resource ID component of an access" + . " point ARN must not contain additional components" + . " (delimited by ':')."); + } + if (!self::isValidHostLabel($data['resource_id'])) { + throw new InvalidArnException("The resource ID in an access point ARN" + . " must be a valid host label value."); + } + } +} diff --git a/3rdparty/aws/aws-sdk-php/src/Arn/AccessPointArnInterface.php b/3rdparty/aws/aws-sdk-php/src/Arn/AccessPointArnInterface.php new file mode 100644 index 00000000..9eb5f2bf --- /dev/null +++ b/3rdparty/aws/aws-sdk-php/src/Arn/AccessPointArnInterface.php @@ -0,0 +1,10 @@ + null, + 'partition' => null, + 'service' => null, + 'region' => null, + 'account_id' => null, + 'resource' => null, + ]; + + $length = strlen($string); + $lastDelim = 0; + $numComponents = 0; + for ($i = 0; $i < $length; $i++) { + + if (($numComponents < 5 && $string[$i] === ':')) { + // Split components between delimiters + $data[key($data)] = substr($string, $lastDelim, $i - $lastDelim); + + // Do not include delimiter character itself + $lastDelim = $i + 1; + next($data); + $numComponents++; + } + + if ($i === $length - 1) { + // Put the remainder in the last component. + if (in_array($numComponents, [5])) { + $data['resource'] = substr($string, $lastDelim); + } else { + // If there are < 5 components, put remainder in current + // component. + $data[key($data)] = substr($string, $lastDelim); + } + } + } + + return $data; + } + + public function __construct($data) + { + if (is_array($data)) { + $this->data = $data; + } elseif (is_string($data)) { + $this->data = static::parse($data); + } else { + throw new InvalidArnException('Constructor accepts a string or an' + . ' array as an argument.'); + } + + static::validate($this->data); + } + + public function __toString() + { + if (!isset($this->string)) { + $components = [ + $this->getPrefix(), + $this->getPartition(), + $this->getService(), + $this->getRegion(), + $this->getAccountId(), + $this->getResource(), + ]; + + $this->string = implode(':', $components); + } + return $this->string; + } + + public function getPrefix() + { + return $this->data['arn']; + } + + public function getPartition() + { + return $this->data['partition']; + } + + public function getService() + { + return $this->data['service']; + } + + public function getRegion() + { + return $this->data['region']; + } + + public function getAccountId() + { + return $this->data['account_id']; + } + + public function getResource() + { + return $this->data['resource']; + } + + public function toArray() + { + return $this->data; + } + + /** + * Minimally restrictive generic ARN validation + * + * @param array $data + */ + protected static function validate(array $data) + { + if ($data['arn'] !== 'arn') { + throw new InvalidArnException("The 1st component of an ARN must be" + . " 'arn'."); + } + + if (empty($data['partition'])) { + throw new InvalidArnException("The 2nd component of an ARN" + . " represents the partition and must not be empty."); + } + + if (empty($data['service'])) { + throw new InvalidArnException("The 3rd component of an ARN" + . " represents the service and must not be empty."); + } + + if (empty($data['resource'])) { + throw new InvalidArnException("The 6th component of an ARN" + . " represents the resource information and must not be empty." + . " Individual service ARNs may include additional delimiters" + . " to further qualify resources."); + } + } + + protected static function validateAccountId($data, $arnName) + { + if (!self::isValidHostLabel($data['account_id'])) { + throw new InvalidArnException("The 5th component of a {$arnName}" + . " is required, represents the account ID, and" + . " must be a valid host label."); + } + } + + protected static function validateRegion($data, $arnName) + { + if (empty($data['region'])) { + throw new InvalidArnException("The 4th component of a {$arnName}" + . " represents the region and must not be empty."); + } + } + + /** + * Validates whether a string component is a valid host label + * + * @param $string + * @return bool + */ + protected static function isValidHostLabel($string) + { + if (empty($string) || strlen($string) > 63) { + return false; + } + if ($value = preg_match("/^[a-zA-Z0-9-]+$/", $string)) { + return true; + } + return false; + } +} \ No newline at end of file diff --git a/3rdparty/aws/aws-sdk-php/src/Arn/ArnInterface.php b/3rdparty/aws/aws-sdk-php/src/Arn/ArnInterface.php new file mode 100644 index 00000000..c30c6ccd --- /dev/null +++ b/3rdparty/aws/aws-sdk-php/src/Arn/ArnInterface.php @@ -0,0 +1,37 @@ +data['resource_type']; + } + + public function getResourceId() + { + return $this->data['resource_id']; + } + + protected static function parseResourceTypeAndId(array $data) + { + $resourceData = preg_split("/[\/:]/", $data['resource'], 2); + $data['resource_type'] = isset($resourceData[0]) + ? $resourceData[0] + : null; + $data['resource_id'] = isset($resourceData[1]) + ? $resourceData[1] + : null; + return $data; + } +} \ No newline at end of file diff --git a/3rdparty/aws/aws-sdk-php/src/Arn/S3/AccessPointArn.php b/3rdparty/aws/aws-sdk-php/src/Arn/S3/AccessPointArn.php new file mode 100644 index 00000000..5841d904 --- /dev/null +++ b/3rdparty/aws/aws-sdk-php/src/Arn/S3/AccessPointArn.php @@ -0,0 +1,27 @@ +data['outpost_id']; + } + + public function getAccesspointName() + { + return $this->data['accesspoint_name']; + } + + private static function parseOutpostData(array $data) + { + $resourceData = preg_split("/[\/:]/", $data['resource_id']); + + $data['outpost_id'] = isset($resourceData[0]) + ? $resourceData[0] + : null; + $data['accesspoint_type'] = isset($resourceData[1]) + ? $resourceData[1] + : null; + $data['accesspoint_name'] = isset($resourceData[2]) + ? $resourceData[2] + : null; + if (isset($resourceData[3])) { + $data['resource_extra'] = implode(':', array_slice($resourceData, 3)); + } + + return $data; + } + + /** + * Validation specific to OutpostsAccessPointArn. Note this uses the base Arn + * class validation instead of the direct parent due to it having slightly + * differing requirements from its parent. + * + * @param array $data + */ + public static function validate(array $data) + { + Arn::validate($data); + + if (($data['service'] !== 's3-outposts')) { + throw new InvalidArnException("The 3rd component of an S3 Outposts" + . " access point ARN represents the service and must be" + . " 's3-outposts'."); + } + + self::validateRegion($data, 'S3 Outposts access point ARN'); + self::validateAccountId($data, 'S3 Outposts access point ARN'); + + if (($data['resource_type'] !== 'outpost')) { + throw new InvalidArnException("The 6th component of an S3 Outposts" + . " access point ARN represents the resource type and must be" + . " 'outpost'."); + } + + if (!self::isValidHostLabel($data['outpost_id'])) { + throw new InvalidArnException("The 7th component of an S3 Outposts" + . " access point ARN is required, represents the outpost ID, and" + . " must be a valid host label."); + } + + if ($data['accesspoint_type'] !== 'accesspoint') { + throw new InvalidArnException("The 8th component of an S3 Outposts" + . " access point ARN must be 'accesspoint'"); + } + + if (!self::isValidHostLabel($data['accesspoint_name'])) { + throw new InvalidArnException("The 9th component of an S3 Outposts" + . " access point ARN is required, represents the accesspoint name," + . " and must be a valid host label."); + } + + if (!empty($data['resource_extra'])) { + throw new InvalidArnException("An S3 Outposts access point ARN" + . " should only have 9 components, delimited by the characters" + . " ':' and '/'. '{$data['resource_extra']}' was found after the" + . " 9th component."); + } + } +} diff --git a/3rdparty/aws/aws-sdk-php/src/Arn/S3/OutpostsArnInterface.php b/3rdparty/aws/aws-sdk-php/src/Arn/S3/OutpostsArnInterface.php new file mode 100644 index 00000000..20285e0c --- /dev/null +++ b/3rdparty/aws/aws-sdk-php/src/Arn/S3/OutpostsArnInterface.php @@ -0,0 +1,12 @@ +data['bucket_name']; + } + + public function getOutpostId() + { + return $this->data['outpost_id']; + } + + private static function parseOutpostData(array $data) + { + $resourceData = preg_split("/[\/:]/", $data['resource_id'], 3); + + $data['outpost_id'] = isset($resourceData[0]) + ? $resourceData[0] + : null; + $data['bucket_label'] = isset($resourceData[1]) + ? $resourceData[1] + : null; + $data['bucket_name'] = isset($resourceData[2]) + ? $resourceData[2] + : null; + + return $data; + } + + /** + * + * @param array $data + */ + public static function validate(array $data) + { + Arn::validate($data); + + if (($data['service'] !== 's3-outposts')) { + throw new InvalidArnException("The 3rd component of an S3 Outposts" + . " bucket ARN represents the service and must be 's3-outposts'."); + } + + self::validateRegion($data, 'S3 Outposts bucket ARN'); + self::validateAccountId($data, 'S3 Outposts bucket ARN'); + + if (($data['resource_type'] !== 'outpost')) { + throw new InvalidArnException("The 6th component of an S3 Outposts" + . " bucket ARN represents the resource type and must be" + . " 'outpost'."); + } + + if (!self::isValidHostLabel($data['outpost_id'])) { + throw new InvalidArnException("The 7th component of an S3 Outposts" + . " bucket ARN is required, represents the outpost ID, and" + . " must be a valid host label."); + } + + if ($data['bucket_label'] !== 'bucket') { + throw new InvalidArnException("The 8th component of an S3 Outposts" + . " bucket ARN must be 'bucket'"); + } + + if (empty($data['bucket_name'])) { + throw new InvalidArnException("The 9th component of an S3 Outposts" + . " bucket ARN represents the bucket name and must not be empty."); + } + } +} diff --git a/3rdparty/aws/aws-sdk-php/src/Auth/AuthSchemeResolver.php b/3rdparty/aws/aws-sdk-php/src/Auth/AuthSchemeResolver.php new file mode 100644 index 00000000..ea7847ac --- /dev/null +++ b/3rdparty/aws/aws-sdk-php/src/Auth/AuthSchemeResolver.php @@ -0,0 +1,181 @@ + 'v4', + 'aws.auth#sigv4a' => 'v4a', + 'smithy.api#httpBearerAuth' => 'bearer', + 'smithy.api#noAuth' => 'anonymous' + ]; + + /** + * @var array Mapping of auth schemes to signature versions used in + * resolving a signature version. + */ + private $authSchemeMap; + private $tokenProvider; + private $credentialProvider; + + + public function __construct( + callable $credentialProvider, + ?callable $tokenProvider = null, + array $authSchemeMap = [] + ){ + $this->credentialProvider = $credentialProvider; + $this->tokenProvider = $tokenProvider; + $this->authSchemeMap = empty($authSchemeMap) + ? self::$defaultAuthSchemeMap + : $authSchemeMap; + } + + /** + * Accepts a priority-ordered list of auth schemes and an Identity + * and selects the first compatible auth schemes, returning a normalized + * signature version. For example, based on the default auth scheme mapping, + * if `aws.auth#sigv4` is selected, `v4` will be returned. + * + * @param array $authSchemes + * @param $identity + * + * @return string + * @throws UnresolvedAuthSchemeException + */ + public function selectAuthScheme( + array $authSchemes, + array $args = [] + ): string + { + $failureReasons = []; + + foreach($authSchemes as $authScheme) { + $normalizedAuthScheme = $this->authSchemeMap[$authScheme] ?? $authScheme; + + if ($this->isCompatibleAuthScheme($normalizedAuthScheme)) { + if ($normalizedAuthScheme === 'v4' && !empty($args['unsigned_payload'])) { + return $normalizedAuthScheme . self::UNSIGNED_BODY; + } + + return $normalizedAuthScheme; + } else { + $failureReasons[] = $this->getIncompatibilityMessage($normalizedAuthScheme); + } + } + + throw new UnresolvedAuthSchemeException( + 'Could not resolve an authentication scheme: ' + . implode('; ', $failureReasons) + ); + } + + /** + * Determines compatibility based on either Identity or the availability + * of the CRT extension. + * + * @param $authScheme + * + * @return bool + */ + private function isCompatibleAuthScheme($authScheme): bool + { + switch ($authScheme) { + case 'v4': + case 'anonymous': + return $this->hasAwsCredentialIdentity(); + case 'v4a': + return extension_loaded('awscrt') && $this->hasAwsCredentialIdentity(); + case 'bearer': + return $this->hasBearerTokenIdentity(); + default: + return false; + } + } + + /** + * Provides incompatibility messages in the event an incompatible auth scheme + * is encountered. + * + * @param $authScheme + * + * @return string + */ + private function getIncompatibilityMessage($authScheme): string + { + switch ($authScheme) { + case 'v4': + return 'Signature V4 requires AWS credentials for request signing'; + case 'anonymous': + return 'Anonymous signatures require AWS credentials for request signing'; + case 'v4a': + return 'The aws-crt-php extension and AWS credentials are required to use Signature V4A'; + case 'bearer': + return 'Bearer token credentials must be provided to use Bearer authentication'; + default: + return "The service does not support `{$authScheme}` authentication."; + } + } + + /** + * @return bool + */ + private function hasAwsCredentialIdentity(): bool + { + $fn = $this->credentialProvider; + $result = $fn(); + + if ($result instanceof PromiseInterface) { + try { + $resolved = $result->wait(); + return $resolved instanceof AwsCredentialIdentity; + } catch (CredentialsException $e) { + return false; + } + } + + return $result instanceof AwsCredentialIdentity; + } + + /** + * @return bool + */ + private function hasBearerTokenIdentity(): bool + { + if ($this->tokenProvider) { + $fn = $this->tokenProvider; + $result = $fn(); + + if ($result instanceof PromiseInterface) { + try { + $resolved = $result->wait(); + return $resolved instanceof BearerTokenIdentity; + } catch (TokenException $e) { + return false; + } + } + + return $result instanceof BearerTokenIdentity; + } + + return false; + } +} diff --git a/3rdparty/aws/aws-sdk-php/src/Auth/AuthSchemeResolverInterface.php b/3rdparty/aws/aws-sdk-php/src/Auth/AuthSchemeResolverInterface.php new file mode 100644 index 00000000..54a59aa6 --- /dev/null +++ b/3rdparty/aws/aws-sdk-php/src/Auth/AuthSchemeResolverInterface.php @@ -0,0 +1,24 @@ +nextHandler = $nextHandler; + $this->authResolver = $authResolver; + $this->api = $api; + } + + /** + * @param CommandInterface $command + * + * @return Promise + */ + public function __invoke(CommandInterface $command) + { + $nextHandler = $this->nextHandler; + $serviceAuth = $this->api->getMetadata('auth') ?: []; + $operation = $this->api->getOperation($command->getName()); + $operationAuth = $operation['auth'] ?? []; + $unsignedPayload = $operation['unsignedpayload'] ?? false; + $resolvableAuth = $operationAuth ?: $serviceAuth; + + if (!empty($resolvableAuth)) { + if (isset($command['@context']['auth_scheme_resolver']) + && $command['@context']['auth_scheme_resolver'] instanceof AuthSchemeResolverInterface + ){ + $resolver = $command['@context']['auth_scheme_resolver']; + } else { + $resolver = $this->authResolver; + } + + try { + $selectedAuthScheme = $resolver->selectAuthScheme( + $resolvableAuth, + ['unsigned_payload' => $unsignedPayload] + ); + } catch (UnresolvedAuthSchemeException $e) { + // There was an error resolving auth + // The signature version will fall back to the modeled `signatureVersion` + // or auth schemes resolved during endpoint resolution + } + + if (!empty($selectedAuthScheme)) { + $command['@context']['signature_version'] = $selectedAuthScheme; + } + } + + return $nextHandler($command); + } +} diff --git a/3rdparty/aws/aws-sdk-php/src/Auth/Exception/UnresolvedAuthSchemeException.php b/3rdparty/aws/aws-sdk-php/src/Auth/Exception/UnresolvedAuthSchemeException.php new file mode 100644 index 00000000..98f33f4d --- /dev/null +++ b/3rdparty/aws/aws-sdk-php/src/Auth/Exception/UnresolvedAuthSchemeException.php @@ -0,0 +1,15 @@ +parseClass(); + if (!isset($args['service'])) { + $args['service'] = manifest($service)['endpoint']; + } + if (!isset($args['exception_class'])) { + $args['exception_class'] = $exceptionClass; + } + $this->handlerList = new HandlerList(); + $resolver = new ClientResolver(static::getArguments()); + $config = $resolver->resolve($args, $this->handlerList); + $this->api = $config['api']; + $this->signatureProvider = $config['signature_provider']; + $this->authSchemeResolver = $config['auth_scheme_resolver']; + $this->endpoint = new Uri($config['endpoint']); + $this->credentialProvider = $config['credentials']; + $this->tokenProvider = $config['token']; + $this->region = $config['region'] ?? null; + $this->signingRegionSet = $config['sigv4a_signing_region_set'] ?? null; + $this->config = $config['config']; + $this->setClientBuiltIns($args, $config); + $this->clientContextParams = $this->setClientContextParams($args); + $this->defaultRequestOptions = $config['http']; + $this->endpointProvider = $config['endpoint_provider']; + $this->serializer = $config['serializer']; + $this->addSignatureMiddleware($args); + $this->addInvocationId(); + $this->addEndpointParameterMiddleware($args); + $this->addEndpointDiscoveryMiddleware($config, $args); + $this->addRequestCompressionMiddleware($config); + $this->loadAliases(); + $this->addStreamRequestPayload(); + $this->addRecursionDetection(); + if ($this->isUseEndpointV2()) { + $this->addEndpointV2Middleware(); + } + $this->addAuthSelectionMiddleware(); + + if (!is_null($this->api->getMetadata('awsQueryCompatible'))) { + $this->addQueryCompatibleInputMiddleware($this->api); + $this->addQueryModeHeader(); + } + + if (isset($args['with_resolved'])) { + $args['with_resolved']($config); + } + $this->addUserAgentMiddleware($config); + } + + public function getHandlerList() + { + return $this->handlerList; + } + + public function getConfig($option = null) + { + return $option === null + ? $this->config + : $this->config[$option] ?? null; + } + + public function getCredentials() + { + $fn = $this->credentialProvider; + return $fn(); + } + + + public function getEndpoint() + { + return $this->endpoint; + } + + public function getRegion() + { + return $this->region; + } + + public function getApi() + { + return $this->api; + } + + public function getCommand($name, array $args = []) + { + // Fail fast if the command cannot be found in the description. + if (!isset($this->getApi()['operations'][$name])) { + $name = ucfirst($name); + if (!isset($this->getApi()['operations'][$name])) { + throw new \InvalidArgumentException("Operation not found: $name"); + } + } + + if (!isset($args['@http'])) { + $args['@http'] = $this->defaultRequestOptions; + } else { + $args['@http'] += $this->defaultRequestOptions; + } + + return new Command($name, $args, clone $this->getHandlerList()); + } + + public function getEndpointProvider() + { + return $this->endpointProvider; + } + + /** + * Provides the set of service context parameter + * key-value pairs used for endpoint resolution. + * + * @return array + */ + public function getClientContextParams() + { + return $this->clientContextParams; + } + + /** + * Provides the set of built-in keys and values + * used for endpoint resolution + * + * @return array + */ + public function getClientBuiltIns() + { + return $this->clientBuiltIns; + } + + public function __sleep() + { + throw new \RuntimeException('Instances of ' . static::class + . ' cannot be serialized'); + } + + /** + * Get the signature_provider function of the client. + * + * @return callable + */ + final public function getSignatureProvider() + { + return $this->signatureProvider; + } + + /** + * Parse the class name and setup the custom exception class of the client + * and return the "service" name of the client and "exception_class". + * + * @return array + */ + private function parseClass() + { + $klass = get_class($this); + + if ($klass === __CLASS__) { + return ['', AwsException::class]; + } + + $service = substr($klass, strrpos($klass, '\\') + 1, -6); + + return [ + strtolower($service), + "Aws\\{$service}\\Exception\\{$service}Exception" + ]; + } + + private function addEndpointParameterMiddleware($args) + { + if (empty($args['disable_host_prefix_injection'])) { + $list = $this->getHandlerList(); + $list->appendBuild( + EndpointParameterMiddleware::wrap( + $this->api + ), + 'endpoint_parameter' + ); + } + } + + private function addEndpointDiscoveryMiddleware($config, $args) + { + $list = $this->getHandlerList(); + + if (!isset($args['endpoint'])) { + $list->appendBuild( + EndpointDiscoveryMiddleware::wrap( + $this, + $args, + $config['endpoint_discovery'] + ), + 'EndpointDiscoveryMiddleware' + ); + } + } + + private function addSignatureMiddleware(array $args) + { + $api = $this->getApi(); + $provider = $this->signatureProvider; + $signatureVersion = $this->config['signature_version']; + $name = $this->config['signing_name']; + $region = $this->config['signing_region']; + $signingRegionSet = $this->signingRegionSet; + + if (isset($args['signature_version']) + || isset($this->config['configured_signature_version']) + ) { + $configuredSignatureVersion = true; + } else { + $configuredSignatureVersion = false; + } + + $resolver = static function ( + CommandInterface $command + ) use ( + $api, + $provider, + $name, + $region, + $signatureVersion, + $configuredSignatureVersion, + $signingRegionSet + ) { + if (!$configuredSignatureVersion) { + if (!empty($command['@context']['signing_region'])) { + $region = $command['@context']['signing_region']; + } + if (!empty($command['@context']['signing_service'])) { + $name = $command['@context']['signing_service']; + } + if (!empty($command['@context']['signature_version'])) { + $signatureVersion = $command['@context']['signature_version']; + } + + $authType = $api->getOperation($command->getName())['authtype']; + switch ($authType){ + case 'none': + $signatureVersion = 'anonymous'; + break; + case 'v4-unsigned-body': + $signatureVersion = 'v4-unsigned-body'; + break; + case 'bearer': + $signatureVersion = 'bearer'; + break; + } + } + + if ($signatureVersion === 'v4a') { + $commandSigningRegionSet = !empty($command['@context']['signing_region_set']) + ? implode(', ', $command['@context']['signing_region_set']) + : null; + + $region = $signingRegionSet + ?? $commandSigningRegionSet + ?? $region; + } + + // Capture signature metric + $command->getMetricsBuilder()->identifyMetricByValueAndAppend( + 'signature', + $signatureVersion + ); + + return SignatureProvider::resolve($provider, $signatureVersion, $name, $region); + }; + $this->handlerList->appendSign( + Middleware::signer($this->credentialProvider, + $resolver, + $this->tokenProvider, + $this->getConfig() + ), + 'signer' + ); + } + + private function addRequestCompressionMiddleware($config) + { + if (empty($config['disable_request_compression'])) { + $list = $this->getHandlerList(); + $list->appendBuild( + RequestCompressionMiddleware::wrap($config), + 'request-compression' + ); + } + } + + private function addQueryCompatibleInputMiddleware(Service $api) + { + $list = $this->getHandlerList(); + $list->appendValidate( + QueryCompatibleInputMiddleware::wrap($api), + 'query-compatible-input' + ); + } + + private function addQueryModeHeader(): void + { + $list = $this->getHandlerList(); + $list->appendBuild( + Middleware::mapRequest(function (RequestInterface $r) { + return $r->withHeader( + 'x-amzn-query-mode', + "true" + ); + }), + 'x-amzn-query-mode-header' + ); + } + + private function addInvocationId() + { + // Add invocation id to each request + $this->handlerList->prependSign(Middleware::invocationId(), 'invocation-id'); + } + + private function loadAliases($file = null) + { + if (!isset($this->aliases)) { + if (is_null($file)) { + $file = __DIR__ . '/data/aliases.json'; + } + $aliases = \Aws\load_compiled_json($file); + $serviceId = $this->api->getServiceId(); + $version = $this->getApi()->getApiVersion(); + if (!empty($aliases['operations'][$serviceId][$version])) { + $this->aliases = array_flip($aliases['operations'][$serviceId][$version]); + } + } + } + + private function addStreamRequestPayload() + { + $streamRequestPayloadMiddleware = StreamRequestPayloadMiddleware::wrap( + $this->api + ); + + $this->handlerList->prependSign( + $streamRequestPayloadMiddleware, + 'StreamRequestPayloadMiddleware' + ); + } + + private function addRecursionDetection() + { + // Add recursion detection header to requests + // originating in supported Lambda runtimes + $this->handlerList->appendBuild( + Middleware::recursionDetection(), 'recursion-detection' + ); + } + + private function addAuthSelectionMiddleware() + { + $list = $this->getHandlerList(); + + $list->prependBuild( + AuthSelectionMiddleware::wrap( + $this->authSchemeResolver, + $this->getApi() + ), + 'auth-selection' + ); + } + + private function addEndpointV2Middleware() + { + $list = $this->getHandlerList(); + $endpointArgs = $this->getEndpointProviderArgs(); + + $list->prependBuild( + EndpointV2Middleware::wrap( + $this->endpointProvider, + $this->getApi(), + $endpointArgs, + $this->credentialProvider + ), + 'endpoint-resolution' + ); + } + + /** + * Appends the user agent middleware. + * This middleware MUST be appended after the + * signature middleware `addSignatureMiddleware`, + * so that metrics around signatures are properly + * captured. + * + * @param $args + * @return void + */ + private function addUserAgentMiddleware($args) + { + $this->getHandlerList()->appendSign( + UserAgentMiddleware::wrap($args), + 'user-agent' + ); + } + + /** + * Retrieves client context param definition from service model, + * creates mapping of client context param names with client-provided + * values. + * + * @return array + */ + private function setClientContextParams($args) + { + $api = $this->getApi(); + $resolvedParams = []; + if (!empty($paramDefinitions = $api->getClientContextParams())) { + foreach($paramDefinitions as $paramName => $paramValue) { + if (isset($args[$paramName])) { + $resolvedParams[$paramName] = $args[$paramName]; + } + } + } + return $resolvedParams; + } + + /** + * Retrieves and sets default values used for endpoint resolution. + */ + private function setClientBuiltIns($args, $resolvedConfig) + { + $builtIns = []; + $config = $resolvedConfig['config']; + $service = $args['service']; + + $builtIns['SDK::Endpoint'] = null; + if (!empty($args['endpoint'])) { + $builtIns['SDK::Endpoint'] = $args['endpoint']; + } elseif (isset($config['configured_endpoint_url'])) { + $builtIns['SDK::Endpoint'] = (string) $this->getEndpoint(); + } + $builtIns['AWS::Region'] = $this->getRegion(); + $builtIns['AWS::UseFIPS'] = $config['use_fips_endpoint']->isUseFipsEndpoint(); + $builtIns['AWS::UseDualStack'] = $config['use_dual_stack_endpoint']->isUseDualstackEndpoint(); + if ($service === 's3' || $service === 's3control'){ + $builtIns['AWS::S3::UseArnRegion'] = $config['use_arn_region']->isUseArnRegion(); + } + if ($service === 's3') { + $builtIns['AWS::S3::UseArnRegion'] = $config['use_arn_region']->isUseArnRegion(); + $builtIns['AWS::S3::Accelerate'] = $config['use_accelerate_endpoint']; + $builtIns['AWS::S3::ForcePathStyle'] = $config['use_path_style_endpoint']; + $builtIns['AWS::S3::DisableMultiRegionAccessPoints'] = $config['disable_multiregion_access_points']; + } + $builtIns['AWS::Auth::AccountIdEndpointMode'] = $resolvedConfig['account_id_endpoint_mode']; + + $this->clientBuiltIns += $builtIns; + } + + /** + * Retrieves arguments to be used in endpoint resolution. + * + * @return array + */ + public function getEndpointProviderArgs() + { + return $this->normalizeEndpointProviderArgs(); + } + + /** + * Combines built-in and client context parameter values in + * order of specificity. Client context parameter values supersede + * built-in values. + * + * @return array + */ + private function normalizeEndpointProviderArgs() + { + $normalizedBuiltIns = []; + + foreach($this->clientBuiltIns as $name => $value) { + $normalizedName = explode('::', $name); + $normalizedName = $normalizedName[count($normalizedName) - 1]; + $normalizedBuiltIns[$normalizedName] = $value; + } + + return array_merge($normalizedBuiltIns, $this->getClientContextParams()); + } + + protected function isUseEndpointV2() + { + return $this->endpointProvider instanceof EndpointProviderV2; + } + + public static function emitDeprecationWarning() { + trigger_error( + "This method is deprecated. It will be removed in an upcoming release." + , E_USER_DEPRECATED + ); + + $phpVersion = PHP_VERSION_ID; + if ($phpVersion < 70205) { + $phpVersionString = phpversion(); + @trigger_error( + "This installation of the SDK is using PHP version" + . " {$phpVersionString}, which will be deprecated on August" + . " 15th, 2023. Please upgrade your PHP version to a minimum of" + . " 7.2.5 before then to continue receiving updates to the AWS" + . " SDK for PHP. To disable this warning, set" + . " suppress_php_deprecation_warning to true on the client constructor" + . " or set the environment variable AWS_SUPPRESS_PHP_DEPRECATION_WARNING" + . " to true.", + E_USER_DEPRECATED + ); + } + } + + + /** + * Returns a service model and doc model with any necessary changes + * applied. + * + * @param array $api Array of service data being documented. + * @param array $docs Array of doc model data. + * + * @return array Tuple containing a [Service, DocModel] + * + * @internal This should only used to document the service API. + * @codeCoverageIgnore + */ + public static function applyDocFilters(array $api, array $docs) + { + $aliases = \Aws\load_compiled_json(__DIR__ . '/data/aliases.json'); + $serviceId = $api['metadata']['serviceId'] ?? ''; + $version = $api['metadata']['apiVersion']; + + // Replace names for any operations with SDK aliases + if (!empty($aliases['operations'][$serviceId][$version])) { + foreach ($aliases['operations'][$serviceId][$version] as $op => $alias) { + $api['operations'][$alias] = $api['operations'][$op]; + $docs['operations'][$alias] = $docs['operations'][$op]; + unset($api['operations'][$op], $docs['operations'][$op]); + } + } + ksort($api['operations']); + + return [ + new Service($api, ApiProvider::defaultProvider()), + new DocModel($docs) + ]; + } + + /** + * @deprecated + * @return static + */ + public static function factory(array $config = []) + { + return new static($config); + } +} diff --git a/3rdparty/aws/aws-sdk-php/src/AwsClientInterface.php b/3rdparty/aws/aws-sdk-php/src/AwsClientInterface.php new file mode 100644 index 00000000..12a57018 --- /dev/null +++ b/3rdparty/aws/aws-sdk-php/src/AwsClientInterface.php @@ -0,0 +1,169 @@ +getWaiter('foo', ['bar' => 'baz']); + * $waiter->promise()->then(function () { echo 'Done!'; }); + * + * @param string|callable $name Name of the waiter that defines the wait + * configuration and conditions. + * @param array $args Args to be used with each command executed + * by the waiter. Waiter configuration options + * can be provided in an associative array in + * the @waiter key. + * @return \Aws\Waiter + * @throws \UnexpectedValueException if the waiter is invalid. + */ + public function getWaiter($name, array $args = []); +} diff --git a/3rdparty/aws/aws-sdk-php/src/AwsClientTrait.php b/3rdparty/aws/aws-sdk-php/src/AwsClientTrait.php new file mode 100644 index 00000000..f31a24ed --- /dev/null +++ b/3rdparty/aws/aws-sdk-php/src/AwsClientTrait.php @@ -0,0 +1,101 @@ +getApi()->getPaginatorConfig($name); + + return new ResultPaginator($this, $name, $args, $config); + } + + public function getIterator($name, array $args = []) + { + $config = $this->getApi()->getPaginatorConfig($name); + if (!$config['result_key']) { + throw new \UnexpectedValueException(sprintf( + 'There are no resources to iterate for the %s operation of %s', + $name, $this->getApi()['serviceFullName'] + )); + } + + $key = is_array($config['result_key']) + ? $config['result_key'][0] + : $config['result_key']; + + if ($config['output_token'] && $config['input_token']) { + return $this->getPaginator($name, $args)->search($key); + } + + $result = $this->execute($this->getCommand($name, $args))->search($key); + + return new \ArrayIterator((array) $result); + } + + public function waitUntil($name, array $args = []) + { + return $this->getWaiter($name, $args)->promise()->wait(); + } + + public function getWaiter($name, array $args = []) + { + $config = isset($args['@waiter']) ? $args['@waiter'] : []; + $config += $this->getApi()->getWaiterConfig($name); + + return new Waiter($this, $name, $args, $config); + } + + public function execute(CommandInterface $command) + { + return $this->executeAsync($command)->wait(); + } + + public function executeAsync(CommandInterface $command) + { + $handler = $command->getHandlerList()->resolve(); + return $handler($command); + } + + public function __call($name, array $args) + { + if (substr($name, -5) === 'Async') { + $name = substr($name, 0, -5); + $isAsync = true; + } + + if (!empty($this->aliases[ucfirst($name)])) { + $name = $this->aliases[ucfirst($name)]; + } + + $params = isset($args[0]) ? $args[0] : []; + + if (!empty($isAsync)) { + return $this->executeAsync( + $this->getCommand($name, $params) + ); + } + + return $this->execute($this->getCommand($name, $params)); + } + + /** + * @param string $name + * @param array $args + * + * @return CommandInterface + */ + abstract public function getCommand($name, array $args = []); + + /** + * @return Service + */ + abstract public function getApi(); +} diff --git a/3rdparty/aws/aws-sdk-php/src/CacheInterface.php b/3rdparty/aws/aws-sdk-php/src/CacheInterface.php new file mode 100644 index 00000000..e77f18b1 --- /dev/null +++ b/3rdparty/aws/aws-sdk-php/src/CacheInterface.php @@ -0,0 +1,34 @@ + 'is_resource', + 'callable' => 'is_callable', + 'int' => 'is_int', + 'bool' => 'is_bool', + 'boolean' => 'is_bool', + 'string' => 'is_string', + 'object' => 'is_object', + 'array' => 'is_array', + ]; + + private static $defaultArgs = [ + 'service' => [ + 'type' => 'value', + 'valid' => ['string'], + 'doc' => 'Name of the service to utilize. This value will be supplied by default when using one of the SDK clients (e.g., Aws\\S3\\S3Client).', + 'required' => true, + 'internal' => true + ], + 'exception_class' => [ + 'type' => 'value', + 'valid' => ['string'], + 'doc' => 'Exception class to create when an error occurs.', + 'default' => AwsException::class, + 'internal' => true + ], + 'scheme' => [ + 'type' => 'value', + 'valid' => ['string'], + 'default' => 'https', + 'doc' => 'URI scheme to use when connecting connect. The SDK will utilize "https" endpoints (i.e., utilize SSL/TLS connections) by default. You can attempt to connect to a service over an unencrypted "http" endpoint by setting ``scheme`` to "http".', + ], + 'disable_host_prefix_injection' => [ + 'type' => 'value', + 'valid' => ['bool'], + 'doc' => 'Set to true to disable host prefix injection logic for services that use it. This disables the entire prefix injection, including the portions supplied by user-defined parameters. Setting this flag will have no effect on services that do not use host prefix injection.', + 'default' => false, + ], + 'ignore_configured_endpoint_urls' => [ + 'type' => 'value', + 'valid' => ['bool'], + 'doc' => 'Set to true to disable endpoint urls configured using `AWS_ENDPOINT_URL` and `endpoint_url` shared config option.', + 'fn' => [__CLASS__, '_apply_ignore_configured_endpoint_urls'], + 'default' => self::DEFAULT_FROM_ENV_INI, + ], + 'endpoint' => [ + 'type' => 'value', + 'valid' => ['string'], + 'doc' => 'The full URI of the webservice. This is only required when connecting to a custom endpoint (e.g., a local version of S3).', + 'fn' => [__CLASS__, '_apply_endpoint'], + 'default' => [__CLASS__, '_default_endpoint'] + ], + 'region' => [ + 'type' => 'value', + 'valid' => ['string'], + 'doc' => 'Region to connect to. See http://docs.aws.amazon.com/general/latest/gr/rande.html for a list of available regions.', + 'fn' => [__CLASS__, '_apply_region'], + 'default' => self::DEFAULT_FROM_ENV_INI + ], + 'version' => [ + 'type' => 'value', + 'valid' => ['string'], + 'doc' => 'The version of the webservice to utilize (e.g., 2006-03-01).', + 'default' => 'latest', + ], + 'signature_provider' => [ + 'type' => 'value', + 'valid' => ['callable'], + 'doc' => 'A callable that accepts a signature version name (e.g., "v4"), a service name, and region, and returns a SignatureInterface object or null. This provider is used to create signers utilized by the client. See Aws\\Signature\\SignatureProvider for a list of built-in providers', + 'default' => [__CLASS__, '_default_signature_provider'], + ], + 'api_provider' => [ + 'type' => 'value', + 'valid' => ['callable'], + 'doc' => 'An optional PHP callable that accepts a type, service, and version argument, and returns an array of corresponding configuration data. The type value can be one of api, waiter, or paginator.', + 'fn' => [__CLASS__, '_apply_api_provider'], + 'default' => [ApiProvider::class, 'defaultProvider'], + ], + 'configuration_mode' => [ + 'type' => 'value', + 'valid' => [ConfigModeInterface::class, CacheInterface::class, 'string', 'closure'], + 'doc' => "Sets the default configuration mode. Otherwise provide an instance of Aws\DefaultsMode\ConfigurationInterface, an instance of Aws\CacheInterface, or a string containing a valid mode", + 'fn' => [__CLASS__, '_apply_defaults'], + 'default' => [ConfigModeProvider::class, 'defaultProvider'] + ], + 'use_fips_endpoint' => [ + 'type' => 'value', + 'valid' => ['bool', UseFipsEndpointConfiguration::class, CacheInterface::class, 'callable'], + 'doc' => 'Set to true to enable the use of FIPS pseudo regions', + 'fn' => [__CLASS__, '_apply_use_fips_endpoint'], + 'default' => [__CLASS__, '_default_use_fips_endpoint'], + ], + 'use_dual_stack_endpoint' => [ + 'type' => 'value', + 'valid' => ['bool', UseDualStackEndpointConfiguration::class, CacheInterface::class, 'callable'], + 'doc' => 'Set to true to enable the use of dual-stack endpoints', + 'fn' => [__CLASS__, '_apply_use_dual_stack_endpoint'], + 'default' => [__CLASS__, '_default_use_dual_stack_endpoint'], + ], + 'endpoint_provider' => [ + 'type' => 'value', + 'valid' => ['callable', EndpointV2\EndpointProviderV2::class], + 'fn' => [__CLASS__, '_apply_endpoint_provider'], + 'doc' => 'An optional PHP callable that accepts a hash of options including a "service" and "region" key and returns NULL or a hash of endpoint data, of which the "endpoint" key is required. See Aws\\Endpoint\\EndpointProvider for a list of built-in providers.', + 'default' => [__CLASS__, '_default_endpoint_provider'], + ], + 'serializer' => [ + 'default' => [__CLASS__, '_default_serializer'], + 'fn' => [__CLASS__, '_apply_serializer'], + 'internal' => true, + 'type' => 'value', + 'valid' => ['callable'], + ], + 'signature_version' => [ + 'type' => 'config', + 'valid' => ['string'], + 'doc' => 'A string representing a custom signature version to use with a service (e.g., v4). Note that per/operation signature version MAY override this requested signature version.', + 'default' => [__CLASS__, '_default_signature_version'], + ], + 'signing_name' => [ + 'type' => 'config', + 'valid' => ['string'], + 'doc' => 'A string representing a custom service name to be used when calculating a request signature.', + 'default' => [__CLASS__, '_default_signing_name'], + ], + 'signing_region' => [ + 'type' => 'config', + 'valid' => ['string'], + 'doc' => 'A string representing a custom region name to be used when calculating a request signature.', + 'default' => [__CLASS__, '_default_signing_region'], + ], + 'profile' => [ + 'type' => 'config', + 'valid' => ['string'], + 'doc' => 'Allows you to specify which profile to use when credentials are created from the AWS credentials file in your HOME directory. This setting overrides the AWS_PROFILE environment variable. Note: Specifying "profile" will cause the "credentials" and "use_aws_shared_config_files" keys to be ignored.', + 'fn' => [__CLASS__, '_apply_profile'], + ], + 'credentials' => [ + 'type' => 'value', + 'valid' => [CredentialsInterface::class, CacheInterface::class, 'array', 'bool', 'callable'], + 'doc' => 'Specifies the credentials used to sign requests. Provide an Aws\Credentials\CredentialsInterface object, an associative array of "key", "secret", and an optional "token" key, `false` to use null credentials, or a callable credentials provider used to create credentials or return null. See Aws\\Credentials\\CredentialProvider for a list of built-in credentials providers. If no credentials are provided, the SDK will attempt to load them from the environment.', + 'fn' => [__CLASS__, '_apply_credentials'], + 'default' => [__CLASS__, '_default_credential_provider'], + ], + 'token' => [ + 'type' => 'value', + 'valid' => [TokenInterface::class, CacheInterface::class, 'array', 'bool', 'callable'], + 'doc' => 'Specifies the token used to authorize requests. Provide an Aws\Token\TokenInterface object, an associative array of "token", and an optional "expiration" key, `false` to use a null token, or a callable token provider used to fetch a token or return null. See Aws\\Token\\TokenProvider for a list of built-in credentials providers. If no token is provided, the SDK will attempt to load one from the environment.', + 'fn' => [__CLASS__, '_apply_token'], + 'default' => [__CLASS__, '_default_token_provider'], + ], + 'auth_scheme_resolver' => [ + 'type' => 'value', + 'valid' => [AuthSchemeResolverInterface::class], + 'doc' => 'An instance of Aws\Auth\AuthSchemeResolverInterface which selects a modeled auth scheme and returns a signature version', + 'default' => [__CLASS__, '_default_auth_scheme_resolver'], + ], + 'endpoint_discovery' => [ + 'type' => 'value', + 'valid' => [ConfigurationInterface::class, CacheInterface::class, 'array', 'callable'], + 'doc' => 'Specifies settings for endpoint discovery. Provide an instance of Aws\EndpointDiscovery\ConfigurationInterface, an instance Aws\CacheInterface, a callable that provides a promise for a Configuration object, or an associative array with the following keys: enabled: (bool) Set to true to enable endpoint discovery, false to explicitly disable it. Defaults to false; cache_limit: (int) The maximum number of keys in the endpoints cache. Defaults to 1000.', + 'fn' => [__CLASS__, '_apply_endpoint_discovery'], + 'default' => [__CLASS__, '_default_endpoint_discovery_provider'] + ], + 'stats' => [ + 'type' => 'value', + 'valid' => ['bool', 'array'], + 'default' => false, + 'doc' => 'Set to true to gather transfer statistics on requests sent. Alternatively, you can provide an associative array with the following keys: retries: (bool) Set to false to disable reporting on retries attempted; http: (bool) Set to true to enable collecting statistics from lower level HTTP adapters (e.g., values returned in GuzzleHttp\TransferStats). HTTP handlers must support an http_stats_receiver option for this to have an effect; timer: (bool) Set to true to enable a command timer that reports the total wall clock time spent on an operation in seconds.', + 'fn' => [__CLASS__, '_apply_stats'], + ], + 'retries' => [ + 'type' => 'value', + 'valid' => ['int', RetryConfigInterface::class, CacheInterface::class, 'callable', 'array'], + 'doc' => "Configures the retry mode and maximum number of allowed retries for a client (pass 0 to disable retries). Provide an integer for 'legacy' mode with the specified number of retries. Otherwise provide an instance of Aws\Retry\ConfigurationInterface, an instance of Aws\CacheInterface, a callable function, or an array with the following keys: mode: (string) Set to 'legacy', 'standard' (uses retry quota management), or 'adapative' (an experimental mode that adds client-side rate limiting to standard mode); max_attempts: (int) The maximum number of attempts for a given request. ", + 'fn' => [__CLASS__, '_apply_retries'], + 'default' => [RetryConfigProvider::class, 'defaultProvider'] + ], + 'validate' => [ + 'type' => 'value', + 'valid' => ['bool', 'array'], + 'default' => true, + 'doc' => 'Set to false to disable client-side parameter validation. Set to true to utilize default validation constraints. Set to an associative array of validation options to enable specific validation constraints.', + 'fn' => [__CLASS__, '_apply_validate'], + ], + 'debug' => [ + 'type' => 'value', + 'valid' => ['bool', 'array'], + 'doc' => 'Set to true to display debug information when sending requests. Alternatively, you can provide an associative array with the following keys: logfn: (callable) Function that is invoked with log messages; stream_size: (int) When the size of a stream is greater than this number, the stream data will not be logged (set to "0" to not log any stream data); scrub_auth: (bool) Set to false to disable the scrubbing of auth data from the logged messages; http: (bool) Set to false to disable the "debug" feature of lower level HTTP adapters (e.g., verbose curl output).', + 'fn' => [__CLASS__, '_apply_debug'], + ], + 'disable_request_compression' => [ + 'type' => 'value', + 'valid' => ['bool', 'callable'], + 'doc' => 'Set to true to disable request compression for supported operations', + 'fn' => [__CLASS__, '_apply_disable_request_compression'], + 'default' => self::DEFAULT_FROM_ENV_INI, + ], + 'request_min_compression_size_bytes' => [ + 'type' => 'value', + 'valid' => ['int', 'callable'], + 'doc' => 'Set to a value between between 0 and 10485760 bytes, inclusive. This value will be ignored if `disable_request_compression` is set to `true`', + 'fn' => [__CLASS__, '_apply_min_compression_size'], + 'default' => [__CLASS__, '_default_min_compression_size'], + ], + 'csm' => [ + 'type' => 'value', + 'valid' => [\Aws\ClientSideMonitoring\ConfigurationInterface::class, 'callable', 'array', 'bool'], + 'doc' => 'CSM options for the client. Provides a callable wrapping a promise, a boolean "false", an instance of ConfigurationInterface, or an associative array of "enabled", "host", "port", and "client_id".', + 'fn' => [__CLASS__, '_apply_csm'], + 'default' => [\Aws\ClientSideMonitoring\ConfigurationProvider::class, 'defaultProvider'] + ], + 'http' => [ + 'type' => 'value', + 'valid' => ['array'], + 'default' => [], + 'doc' => 'Set to an array of SDK request options to apply to each request (e.g., proxy, verify, etc.).', + ], + 'http_handler' => [ + 'type' => 'value', + 'valid' => ['callable'], + 'doc' => 'An HTTP handler is a function that accepts a PSR-7 request object and returns a promise that is fulfilled with a PSR-7 response object or rejected with an array of exception data. NOTE: This option supersedes any provided "handler" option.', + 'fn' => [__CLASS__, '_apply_http_handler'] + ], + 'handler' => [ + 'type' => 'value', + 'valid' => ['callable'], + 'doc' => 'A handler that accepts a command object, request object and returns a promise that is fulfilled with an Aws\ResultInterface object or rejected with an Aws\Exception\AwsException. A handler does not accept a next handler as it is terminal and expected to fulfill a command. If no handler is provided, a default Guzzle handler will be utilized.', + 'fn' => [__CLASS__, '_apply_handler'], + 'default' => [__CLASS__, '_default_handler'] + ], + 'app_id' => [ + 'type' => 'value', + 'valid' => ['string'], + 'doc' => 'app_id(AppId) is an optional application specific identifier that can be set. + When set it will be appended to the User-Agent header of every request in the form of App/{AppId}. + This value is also sourced from environment variable AWS_SDK_UA_APP_ID or the shared config profile attribute sdk_ua_app_id.', + 'fn' => [__CLASS__, '_apply_app_id'], + 'default' => [__CLASS__, '_default_app_id'] + ], + 'ua_append' => [ + 'type' => 'value', + 'valid' => ['string', 'array'], + 'doc' => 'Provide a string or array of strings to send in the User-Agent header.', + 'fn' => [__CLASS__, '_apply_user_agent'], + 'default' => [], + ], + 'idempotency_auto_fill' => [ + 'type' => 'value', + 'valid' => ['bool', 'callable'], + 'doc' => 'Set to false to disable SDK to populate parameters that enabled \'idempotencyToken\' trait with a random UUID v4 value on your behalf. Using default value \'true\' still allows parameter value to be overwritten when provided. Note: auto-fill only works when cryptographically secure random bytes generator functions(random_bytes, openssl_random_pseudo_bytes or mcrypt_create_iv) can be found. You may also provide a callable source of random bytes.', + 'default' => true, + 'fn' => [__CLASS__, '_apply_idempotency_auto_fill'] + ], + 'use_aws_shared_config_files' => [ + 'type' => 'value', + 'valid' => ['bool'], + 'doc' => 'Set to false to disable checking for shared aws config files usually located in \'~/.aws/config\' and \'~/.aws/credentials\'. This will be ignored if you set the \'profile\' setting.', + 'default' => true, + ], + 'suppress_php_deprecation_warning' => [ + 'type' => 'value', + 'valid' => ['bool'], + 'doc' => 'Set to true to suppress PHP runtime deprecation warnings. The current deprecation campaign is PHP versions 8.0.x and below, taking effect on 1/13/2025.', + 'default' => false, + 'fn' => [__CLASS__, '_apply_suppress_php_deprecation_warning'] + ], + 'account_id_endpoint_mode' => [ + 'type' => 'value', + 'valid' => ['string'], + 'doc' => 'Decides whether account_id must a be a required resolved credentials property. If this configuration is set to disabled, then account_id is not required. If set to preferred a warning will be logged when account_id is not resolved, and when set to required an exception will be thrown if account_id is not resolved.', + 'default' => [__CLASS__, '_default_account_id_endpoint_mode'], + 'fn' => [__CLASS__, '_apply_account_id_endpoint_mode'] + ], + 'sigv4a_signing_region_set' => [ + 'type' => 'value', + 'valid' => ['string', 'array'], + 'doc' => 'A comma-delimited list of supported regions sent in sigv4a requests.', + 'fn' => [__CLASS__, '_apply_sigv4a_signing_region_set'], + 'default' => self::DEFAULT_FROM_ENV_INI + ] + ]; + + /** + * Gets an array of default client arguments, each argument containing a + * hash of the following: + * + * - type: (string, required) option type described as follows: + * - value: The default option type. + * - config: The provided value is made available in the client's + * getConfig() method. + * - valid: (array, required) Valid PHP types or class names. Note: null + * is not an allowed type. + * - required: (bool, callable) Whether or not the argument is required. + * Provide a function that accepts an array of arguments and returns a + * string to provide a custom error message. + * - default: (mixed) The default value of the argument if not provided. If + * a function is provided, then it will be invoked to provide a default + * value. The function is provided the array of options and is expected + * to return the default value of the option. The default value can be a + * closure and can not be a callable string that is not part of the + * defaultArgs array. + * - doc: (string) The argument documentation string. + * - fn: (callable) Function used to apply the argument. The function + * accepts the provided value, array of arguments by reference, and an + * event emitter. + * + * Note: Order is honored and important when applying arguments. + * + * @return array + */ + public static function getDefaultArguments() + { + return self::$defaultArgs; + } + + /** + * @param array $argDefinitions Client arguments. + */ + public function __construct(array $argDefinitions) + { + $this->argDefinitions = $argDefinitions; + } + + /** + * Resolves client configuration options and attached event listeners. + * Check for missing keys in passed arguments + * + * @param array $args Provided constructor arguments. + * @param HandlerList $list Handler list to augment. + * + * @return array Returns the array of provided options. + * @throws \InvalidArgumentException + * @see Aws\AwsClient::__construct for a list of available options. + */ + public function resolve(array $args, HandlerList $list) + { + $args['config'] = []; + foreach ($this->argDefinitions as $key => $a) { + // Add defaults, validate required values, and skip if not set. + if (!isset($args[$key])) { + if (isset($a['default'])) { + // Merge defaults in when not present. + if (is_callable($a['default']) + && ( + is_array($a['default']) + || $a['default'] instanceof \Closure + ) + ) { + if ($a['default'] === self::DEFAULT_FROM_ENV_INI) { + $args[$key] = $a['default']( + $key, + $a['valid'][0] ?? 'string', + $args + ); + } else { + $args[$key] = $a['default']($args); + } + } else { + $args[$key] = $a['default']; + } + } elseif (empty($a['required'])) { + continue; + } else { + $this->throwRequired($args); + } + } + + // Validate the types against the provided value. + foreach ($a['valid'] as $check) { + if (isset(self::$typeMap[$check])) { + $fn = self::$typeMap[$check]; + if ($fn($args[$key])) { + goto is_valid; + } + } elseif ($args[$key] instanceof $check) { + goto is_valid; + } + } + + $this->invalidType($key, $args[$key]); + + // Apply the value + is_valid: + if (isset($a['fn'])) { + $a['fn']($args[$key], $args, $list); + } + + if ($a['type'] === 'config') { + $args['config'][$key] = $args[$key]; + } + } + $this->_apply_client_context_params($args); + + return $args; + } + + /** + * Creates a verbose error message for an invalid argument. + * + * @param string $name Name of the argument that is missing. + * @param array $args Provided arguments + * @param bool $useRequired Set to true to show the required fn text if + * available instead of the documentation. + * @return string + */ + private function getArgMessage($name, $args = [], $useRequired = false) + { + $arg = $this->argDefinitions[$name]; + $msg = ''; + $modifiers = []; + if (isset($arg['valid'])) { + $modifiers[] = implode('|', $arg['valid']); + } + if (isset($arg['choice'])) { + $modifiers[] = 'One of ' . implode(', ', $arg['choice']); + } + if ($modifiers) { + $msg .= '(' . implode('; ', $modifiers) . ')'; + } + $msg = wordwrap("{$name}: {$msg}", 75, "\n "); + + if ($useRequired && is_callable($arg['required'])) { + $msg .= "\n\n "; + $msg .= str_replace("\n", "\n ", call_user_func($arg['required'], $args)); + } elseif (isset($arg['doc'])) { + $msg .= wordwrap("\n\n {$arg['doc']}", 75, "\n "); + } + + return $msg; + } + + /** + * Throw when an invalid type is encountered. + * + * @param string $name Name of the value being validated. + * @param mixed $provided The provided value. + * @throws \InvalidArgumentException + */ + private function invalidType($name, $provided) + { + $expected = implode('|', $this->argDefinitions[$name]['valid']); + $msg = "Invalid configuration value " + . "provided for \"{$name}\". Expected {$expected}, but got " + . describe_type($provided) . "\n\n" + . $this->getArgMessage($name); + throw new IAE($msg); + } + + /** + * Throws an exception for missing required arguments. + * + * @param array $args Passed in arguments. + * @throws \InvalidArgumentException + */ + private function throwRequired(array $args) + { + $missing = []; + foreach ($this->argDefinitions as $k => $a) { + if (empty($a['required']) + || isset($a['default']) + || isset($args[$k]) + ) { + continue; + } + $missing[] = $this->getArgMessage($k, $args, true); + } + $msg = "Missing required client configuration options: \n\n"; + $msg .= implode("\n\n", $missing); + throw new IAE($msg); + } + + public static function _apply_retries($value, array &$args, HandlerList $list) + { + // A value of 0 for the config option disables retries + if ($value) { + $config = RetryConfigProvider::unwrap($value); + + if ($config->getMode() === 'legacy') { + // # of retries is 1 less than # of attempts + $decider = RetryMiddleware::createDefaultDecider( + $config->getMaxAttempts() - 1 + ); + $list->appendSign( + Middleware::retry($decider, null, $args['stats']['retries']), + 'retry' + ); + } else { + $list->appendSign( + RetryMiddlewareV2::wrap( + $config, + ['collect_stats' => $args['stats']['retries']] + ), + 'retry' + ); + } + } + } + + public static function _apply_defaults($value, array &$args, HandlerList $list) + { + $config = ConfigModeProvider::unwrap($value); + if ($config->getMode() !== 'legacy') { + if (!isset($args['retries']) && !is_null($config->getRetryMode())) { + $args['retries'] = ['mode' => $config->getRetryMode()]; + } + if ( + !isset($args['sts_regional_endpoints']) + && !is_null($config->getStsRegionalEndpoints()) + ) { + $args['sts_regional_endpoints'] = ['mode' => $config->getStsRegionalEndpoints()]; + } + if ( + !isset($args['s3_us_east_1_regional_endpoint']) + && !is_null($config->getS3UsEast1RegionalEndpoints()) + ) { + $args['s3_us_east_1_regional_endpoint'] = ['mode' => $config->getS3UsEast1RegionalEndpoints()]; + } + + if (!isset($args['http'])) { + $args['http'] = []; + } + if ( + !isset($args['http']['connect_timeout']) + && !is_null($config->getConnectTimeoutInMillis()) + ) { + $args['http']['connect_timeout'] = $config->getConnectTimeoutInMillis() / 1000; + } + if ( + !isset($args['http']['timeout']) + && !is_null($config->getHttpRequestTimeoutInMillis()) + ) { + $args['http']['timeout'] = $config->getHttpRequestTimeoutInMillis() / 1000; + } + } + } + + public static function _apply_disable_request_compression($value, array &$args) { + if (is_callable($value)) { + $value = $value(); + } + if (!is_bool($value)) { + throw new IAE( + "Invalid configuration value provided for 'disable_request_compression'." + . " value must be a bool." + ); + } + $args['config']['disable_request_compression'] = $value; + } + + public static function _apply_min_compression_size($value, array &$args) { + if (is_callable($value)) { + $value = $value(); + } + if (!is_int($value) + || (is_int($value) + && ($value < 0 || $value > 10485760)) + ) { + throw new IAE(" Invalid configuration value provided for 'min_compression_size_bytes'." + . " value must be an integer between 0 and 10485760, inclusive."); + } + $args['config']['request_min_compression_size_bytes'] = $value; + } + + public static function _default_min_compression_size(array &$args) { + return ConfigurationResolver::resolve( + 'request_min_compression_size_bytes', + 10240, + 'int', + $args + ); + } + + public static function _apply_credentials($value, array &$args) + { + if (is_callable($value)) { + return; + } + + if ($value instanceof CredentialsInterface) { + $args['credentials'] = CredentialProvider::fromCredentials($value); + } elseif (is_array($value) + && isset($value['key']) + && isset($value['secret']) + ) { + $args['credentials'] = CredentialProvider::fromCredentials( + new Credentials( + $value['key'], + $value['secret'], + $value['token'] ?? null, + $value['expires'] ?? null, + $value['accountId'] ?? null + ) + ); + } elseif ($value === false) { + $args['credentials'] = CredentialProvider::fromCredentials( + new Credentials('', '') + ); + $args['config']['signature_version'] = 'anonymous'; + $args['config']['configured_signature_version'] = true; + } elseif ($value instanceof CacheInterface) { + $args['credentials'] = CredentialProvider::defaultProvider($args); + } else { + throw new IAE('Credentials must be an instance of ' + . "'" . CredentialsInterface::class . ', an associative ' + . 'array that contains "key", "secret", and an optional "token" ' + . 'key-value pairs, a credentials provider function, or false.'); + } + } + + public static function _default_credential_provider(array $args) + { + return CredentialProvider::defaultProvider($args); + } + + public static function _apply_token($value, array &$args) + { + if (is_callable($value)) { + return; + } + + if ($value instanceof Token) { + $args['token'] = TokenProvider::fromToken($value); + } elseif (is_array($value) + && isset($value['token']) + ) { + $args['token'] = TokenProvider::fromToken( + new Token( + $value['token'], + $value['expires'] ?? null + ) + ); + } elseif ($value instanceof CacheInterface) { + $args['token'] = TokenProvider::defaultProvider($args); + } else { + throw new IAE('Token must be an instance of ' + . TokenInterface::class . ', an associative ' + . 'array that contains "token" and an optional "expires" ' + . 'key-value pairs, a token provider function, or false.'); + } + } + + public static function _default_token_provider(array $args) + { + return TokenProvider::defaultProvider($args); + } + + public static function _apply_csm($value, array &$args, HandlerList $list) + { + if ($value === false) { + $value = new Configuration( + false, + \Aws\ClientSideMonitoring\ConfigurationProvider::DEFAULT_HOST, + \Aws\ClientSideMonitoring\ConfigurationProvider::DEFAULT_PORT, + \Aws\ClientSideMonitoring\ConfigurationProvider::DEFAULT_CLIENT_ID + ); + $args['csm'] = $value; + } + + $list->appendBuild( + ApiCallMonitoringMiddleware::wrap( + $args['credentials'], + $value, + $args['region'], + $args['api']->getServiceId() + ), + 'ApiCallMonitoringMiddleware' + ); + + $list->appendAttempt( + ApiCallAttemptMonitoringMiddleware::wrap( + $args['credentials'], + $value, + $args['region'], + $args['api']->getServiceId() + ), + 'ApiCallAttemptMonitoringMiddleware' + ); + } + + public static function _apply_api_provider(callable $value, array &$args) + { + $api = new Service( + ApiProvider::resolve( + $value, + 'api', + $args['service'], + $args['version'] + ), + $value + ); + + if ( + empty($args['config']['signing_name']) + && isset($api['metadata']['signingName']) + ) { + $args['config']['signing_name'] = $api['metadata']['signingName']; + } + + $args['api'] = $api; + $args['parser'] = Service::createParser($api); + $args['error_parser'] = Service::createErrorParser($api->getProtocol(), $api); + } + + public static function _apply_endpoint_provider($value, array &$args) + { + if (!isset($args['endpoint'])) { + if ($value instanceof \Aws\EndpointV2\EndpointProviderV2) { + $options = self::getEndpointProviderOptions($args); + $value = PartitionEndpointProvider::defaultProvider($options) + ->getPartition($args['region'], $args['service']); + } + + $endpointPrefix = $args['api']['metadata']['endpointPrefix'] ?? $args['service']; + + // Check region is a valid host label when it is being used to + // generate an endpoint + if (!self::isValidRegion($args['region'])) { + throw new InvalidRegionException('Region must be a valid RFC' + . ' host label.'); + } + $serviceEndpoints = + is_array($value) && isset($value['services'][$args['service']]['endpoints']) + ? $value['services'][$args['service']]['endpoints'] + : null; + if (isset($serviceEndpoints[$args['region']]['deprecated'])) { + trigger_error("The service " . $args['service'] . "has " + . " deprecated the region " . $args['region'] . ".", + E_USER_WARNING + ); + } + + $args['region'] = \Aws\strip_fips_pseudo_regions($args['region']); + + // Invoke the endpoint provider and throw if it does not resolve. + $result = EndpointProvider::resolve($value, [ + 'service' => $endpointPrefix, + 'region' => $args['region'], + 'scheme' => $args['scheme'], + 'options' => self::getEndpointProviderOptions($args), + ]); + + $args['endpoint'] = $result['endpoint']; + + if (empty($args['config']['signature_version'])) { + if ( + isset($args['api']) + && $args['api']->getSignatureVersion() == 'bearer' + ) { + $args['config']['signature_version'] = 'bearer'; + } elseif (isset($result['signatureVersion'])) { + $args['config']['signature_version'] = $result['signatureVersion']; + } + } + + if ( + empty($args['config']['signing_region']) + && isset($result['signingRegion']) + ) { + $args['config']['signing_region'] = $result['signingRegion']; + } + + if ( + empty($args['config']['signing_name']) + && isset($result['signingName']) + ) { + $args['config']['signing_name'] = $result['signingName']; + } + } + } + + public static function _apply_endpoint_discovery($value, array &$args) { + $args['endpoint_discovery'] = $value; + } + + public static function _default_endpoint_discovery_provider(array $args) + { + return ConfigurationProvider::defaultProvider($args); + } + + public static function _apply_use_fips_endpoint($value, array &$args) { + if ($value instanceof CacheInterface) { + $value = UseFipsConfigProvider::defaultProvider($args); + } + if (is_callable($value)) { + $value = $value(); + } + if ($value instanceof PromiseInterface) { + $value = $value->wait(); + } + if ($value instanceof UseFipsEndpointConfigurationInterface) { + $args['config']['use_fips_endpoint'] = $value; + } else { + // The Configuration class itself will validate other inputs + $args['config']['use_fips_endpoint'] = new UseFipsEndpointConfiguration($value); + } + } + + public static function _default_use_fips_endpoint(array &$args) { + return UseFipsConfigProvider::defaultProvider($args); + } + + public static function _apply_use_dual_stack_endpoint($value, array &$args) { + if ($value instanceof CacheInterface) { + $value = UseDualStackConfigProvider::defaultProvider($args); + } + if (is_callable($value)) { + $value = $value(); + } + if ($value instanceof PromiseInterface) { + $value = $value->wait(); + } + if ($value instanceof UseDualStackEndpointConfigurationInterface) { + $args['config']['use_dual_stack_endpoint'] = $value; + } else { + // The Configuration class itself will validate other inputs + $args['config']['use_dual_stack_endpoint'] = + new UseDualStackEndpointConfiguration($value, $args['region']); + } + } + + public static function _default_use_dual_stack_endpoint(array &$args) { + return UseDualStackConfigProvider::defaultProvider($args); + } + + public static function _apply_serializer($value, array &$args, HandlerList $list) + { + $list->prependBuild(Middleware::requestBuilder($value), 'builder'); + } + + public static function _apply_debug($value, array &$args, HandlerList $list) + { + if ($value !== false) { + $list->interpose( + new TraceMiddleware( + $value === true ? [] : $value, + $args['api']) + ); + } + } + + public static function _apply_stats($value, array &$args, HandlerList $list) + { + // Create an array of stat collectors that are disabled (set to false) + // by default. If the user has passed in true, enable all stat + // collectors. + $defaults = array_fill_keys( + ['http', 'retries', 'timer'], + $value === true + ); + $args['stats'] = is_array($value) + ? array_replace($defaults, $value) + : $defaults; + + if ($args['stats']['timer']) { + $list->prependInit(Middleware::timer(), 'timer'); + } + } + + public static function _apply_profile($_, array &$args) + { + $args['credentials'] = CredentialProvider::ini($args['profile']); + } + + public static function _apply_validate($value, array &$args, HandlerList $list) + { + if ($value === false) { + return; + } + + $validator = $value === true + ? new Validator() + : new Validator($value); + $list->appendValidate( + Middleware::validation($args['api'], $validator), + 'validation' + ); + } + + public static function _apply_handler($value, array &$args, HandlerList $list) + { + $list->setHandler($value); + } + + public static function _default_handler(array &$args) + { + return new WrappedHttpHandler( + default_http_handler(), + $args['parser'], + $args['error_parser'], + $args['exception_class'], + $args['stats']['http'] + ); + } + + public static function _apply_http_handler($value, array &$args, HandlerList $list) + { + $args['handler'] = new WrappedHttpHandler( + $value, + $args['parser'], + $args['error_parser'], + $args['exception_class'], + $args['stats']['http'] + ); + } + + public static function _apply_app_id($value, array &$args) + { + // AppId should not be longer than 50 chars + static $MAX_APP_ID_LENGTH = 50; + if (strlen($value) > $MAX_APP_ID_LENGTH) { + trigger_error("The provided or configured value for `AppId`, " + ."which is an user agent parameter, exceeds the maximum length of " + ."$MAX_APP_ID_LENGTH characters.", E_USER_WARNING); + } + + $args['app_id'] = $value; + } + + public static function _default_app_id(array $args) + { + return ConfigurationResolver::resolve( + 'sdk_ua_app_id', + '', + 'string', + $args + ); + } + + public static function _apply_user_agent( + $inputUserAgent, + array &$args, + HandlerList $list + ): void + { + // Add endpoint discovery if set + $userAgent = []; + // Add the input to the end + if ($inputUserAgent){ + if (!is_array($inputUserAgent)) { + $inputUserAgent = [$inputUserAgent]; + } + $inputUserAgent = array_map('strval', $inputUserAgent); + $userAgent = array_merge($userAgent, $inputUserAgent); + } + + $args['ua_append'] = $userAgent; + + $list->appendBuild( + Middleware::mapRequest(function (RequestInterface $request) use ($userAgent) { + return $request->withHeader( + 'X-Amz-User-Agent', + implode(' ', array_merge( + $userAgent, + $request->getHeader('X-Amz-User-Agent') + )) + ); + }) + ); + } + + public static function _apply_endpoint($value, array &$args, HandlerList $list) + { + if (empty($value)) { + unset($args['endpoint']); + return; + } + + $args['endpoint_override'] = true; + $args['endpoint'] = $value; + } + + public static function _apply_idempotency_auto_fill( + $value, + array &$args, + HandlerList $list + ) { + $enabled = false; + $generator = null; + + + if (is_bool($value)) { + $enabled = $value; + } elseif (is_callable($value)) { + $enabled = true; + $generator = $value; + } + + if ($enabled) { + $list->prependInit( + IdempotencyTokenMiddleware::wrap($args['api'], $generator), + 'idempotency_auto_fill' + ); + } + } + + public static function _default_account_id_endpoint_mode($args) + { + return ConfigurationResolver::resolve( + 'account_id_endpoint_mode', + 'preferred', + 'string', + $args + ); + } + + public static function _apply_account_id_endpoint_mode($value, array &$args) + { + static $accountIdEndpointModes = ['disabled', 'required', 'preferred']; + if (!in_array($value, $accountIdEndpointModes)) { + throw new IAE( + "The value provided for the config account_id_endpoint_mode is invalid." + ."Valid values are: " . implode(", ", $accountIdEndpointModes) + ); + } + + $args['account_id_endpoint_mode'] = $value; + } + + public static function _default_endpoint_provider(array $args) + { + $service = $args['api'] ?? null; + $serviceName = isset($service) ? $service->getServiceName() : null; + $apiVersion = isset($service) ? $service->getApiVersion() : null; + + if (self::isValidService($serviceName) + && self::isValidApiVersion($serviceName, $apiVersion) + ) { + $ruleset = EndpointDefinitionProvider::getEndpointRuleset( + $service->getServiceName(), + $service->getApiVersion() + ); + return new \Aws\EndpointV2\EndpointProviderV2( + $ruleset, + EndpointDefinitionProvider::getPartitions() + ); + } + $options = self::getEndpointProviderOptions($args); + return PartitionEndpointProvider::defaultProvider($options) + ->getPartition($args['region'], $args['service']); + } + + public static function _default_serializer(array $args) + { + return Service::createSerializer( + $args['api'], + $args['endpoint'] + ); + } + + public static function _default_signature_provider() + { + return SignatureProvider::defaultProvider(); + } + + public static function _default_auth_scheme_resolver(array $args) + { + return new AuthSchemeResolver($args['credentials'], $args['token']); + } + + public static function _default_signature_version(array &$args) + { + if (isset($args['config']['signature_version'])) { + return $args['config']['signature_version']; + } + + $args['__partition_result'] = isset($args['__partition_result']) + ? isset($args['__partition_result']) + : call_user_func(PartitionEndpointProvider::defaultProvider(), [ + 'service' => $args['service'], + 'region' => $args['region'], + ]); + + return isset($args['__partition_result']['signatureVersion']) + ? $args['__partition_result']['signatureVersion'] + : $args['api']->getSignatureVersion(); + } + + public static function _default_signing_name(array &$args) + { + if (isset($args['config']['signing_name'])) { + return $args['config']['signing_name']; + } + + $args['__partition_result'] = isset($args['__partition_result']) + ? isset($args['__partition_result']) + : call_user_func(PartitionEndpointProvider::defaultProvider(), [ + 'service' => $args['service'], + 'region' => $args['region'], + ]); + + if (isset($args['__partition_result']['signingName'])) { + return $args['__partition_result']['signingName']; + } + + if ($signingName = $args['api']->getSigningName()) { + return $signingName; + } + + return $args['service']; + } + + public static function _default_signing_region(array &$args) + { + if (isset($args['config']['signing_region'])) { + return $args['config']['signing_region']; + } + + $args['__partition_result'] = isset($args['__partition_result']) + ? isset($args['__partition_result']) + : call_user_func(PartitionEndpointProvider::defaultProvider(), [ + 'service' => $args['service'], + 'region' => $args['region'], + ]); + + return $args['__partition_result']['signingRegion'] ?? $args['region']; + } + + public static function _apply_ignore_configured_endpoint_urls($value, array &$args) + { + $args['config']['ignore_configured_endpoint_urls'] = $value; + } + + public static function _apply_suppress_php_deprecation_warning($value, &$args) + { + if ($value) { + $args['suppress_php_deprecation_warning'] = true; + } elseif (!empty(getenv("AWS_SUPPRESS_PHP_DEPRECATION_WARNING"))) { + $args['suppress_php_deprecation_warning'] + = \Aws\boolean_value(getenv("AWS_SUPPRESS_PHP_DEPRECATION_WARNING")); + } elseif (!empty($_SERVER["AWS_SUPPRESS_PHP_DEPRECATION_WARNING"])) { + $args['suppress_php_deprecation_warning'] = + \Aws\boolean_value($_SERVER["AWS_SUPPRESS_PHP_DEPRECATION_WARNING"]); + } elseif (!empty($_ENV["AWS_SUPPRESS_PHP_DEPRECATION_WARNING"])) { + $args['suppress_php_deprecation_warning'] = + \Aws\boolean_value($_ENV["AWS_SUPPRESS_PHP_DEPRECATION_WARNING"]); + } + + if ($args['suppress_php_deprecation_warning'] === false + && PHP_VERSION_ID < 80100 + ) { + self::emitDeprecationWarning(); + } + } + + public static function _default_endpoint(array &$args) + { + if ($args['config']['ignore_configured_endpoint_urls'] + || !self::isValidService($args['service']) + ) { + return ''; + } + + $serviceIdentifier = \Aws\manifest($args['service'])['serviceIdentifier']; + $value = ConfigurationResolver::resolve( + 'endpoint_url_' . $serviceIdentifier, + '', + 'string', + $args + [ + 'ini_resolver_options' => [ + 'section' => 'services', + 'subsection' => $serviceIdentifier, + 'key' => 'endpoint_url' + ] + ] + ); + + if (empty($value)) { + $value = ConfigurationResolver::resolve( + 'endpoint_url', + '', + 'string', + $args + ); + } + + if (!empty($value)) { + $args['config']['configured_endpoint_url'] = true; + } + + return $value; + } + + public static function _apply_sigv4a_signing_region_set($value, array &$args) + { + if (empty($value)) { + $args['sigv4a_signing_region_set'] = null; + } elseif (is_array($value)) { + $args['sigv4a_signing_region_set'] = implode(', ', $value); + } else { + $args['sigv4a_signing_region_set'] = $value; + } + } + + public static function _apply_region($value, array &$args) + { + if (empty($value)) { + self::_missing_region($args); + } + $args['region'] = $value; + } + + public static function _missing_region(array $args) + { + $service = $args['service'] ?? ''; + + $msg = << 0, + 'bool' => false, + 'boolean' => false, + 'string' => '', + ]; + + return ConfigurationResolver::resolve( + $key, + $typeDefaultMap[$expectedType] ?? '', + $expectedType, + $args + ); + } + + /** + * Extracts client options for the endpoint provider to its own array + * + * @param array $args + * @return array + */ + private static function getEndpointProviderOptions(array $args) + { + $options = []; + $optionKeys = [ + 'sts_regional_endpoints', + 's3_us_east_1_regional_endpoint', + ]; + $configKeys = [ + 'use_dual_stack_endpoint', + 'use_fips_endpoint', + ]; + foreach ($optionKeys as $key) { + if (isset($args[$key])) { + $options[$key] = $args[$key]; + } + } + foreach ($configKeys as $key) { + if (isset($args['config'][$key])) { + $options[$key] = $args['config'][$key]; + } + } + return $options; + } + + /** + * Validates a region to be used for endpoint construction + * + * @param $region + * @return bool + */ + private static function isValidRegion($region) + { + return is_valid_hostlabel($region); + } + + private function _apply_client_context_params(array $args) + { + if (isset($args['api']) + && !empty($args['api']->getClientContextParams())) + { + $clientContextParams = $args['api']->getClientContextParams(); + foreach($clientContextParams as $paramName => $paramDefinition) { + $definition = [ + 'type' => 'value', + 'valid' => [$paramDefinition['type']], + 'doc' => $paramDefinition['documentation'] ?? null + ]; + $this->argDefinitions[$paramName] = $definition; + + if (isset($args[$paramName])) { + $fn = self::$typeMap[$paramDefinition['type']]; + if (!$fn($args[$paramName])) { + $this->invalidType($paramName, $args[$paramName]); + } + } + } + } + } + + private static function isValidService($service) + { + if (is_null($service)) { + return false; + } + $services = \Aws\manifest(); + return isset($services[$service]); + } + + private static function isValidApiVersion($service, $apiVersion) + { + if (is_null($apiVersion)) { + return false; + } + return is_dir( + __DIR__ . "/data/{$service}/$apiVersion" + ); + } + + private static function emitDeprecationWarning() + { + $phpVersionString = phpversion(); + trigger_error( + "This installation of the SDK is using PHP version" + . " {$phpVersionString}, which will be deprecated on January" + . " 13th, 2025.\nPlease upgrade your PHP version to a minimum of" + . " 8.1.x to continue receiving updates for the AWS" + . " SDK for PHP.\nTo disable this warning, set" + . " suppress_php_deprecation_warning to true on the client constructor" + . " or set the environment variable AWS_SUPPRESS_PHP_DEPRECATION_WARNING" + . " to true.\nMore information can be found at: " + . "https://aws.amazon.com/blogs/developer/announcing-the-end-of-support-for-php-runtimes-8-0-x-and-below-in-the-aws-sdk-for-php/\n", + E_USER_DEPRECATED + ); + } +} diff --git a/3rdparty/aws/aws-sdk-php/src/ClientSideMonitoring/AbstractMonitoringMiddleware.php b/3rdparty/aws/aws-sdk-php/src/ClientSideMonitoring/AbstractMonitoringMiddleware.php new file mode 100644 index 00000000..d514e835 --- /dev/null +++ b/3rdparty/aws/aws-sdk-php/src/ClientSideMonitoring/AbstractMonitoringMiddleware.php @@ -0,0 +1,309 @@ +getResponse(); + if ($response !== null) { + $header = $response->getHeader($headerName); + if (!empty($header[0])) { + return $header[0]; + } + } + return null; + } + + protected static function getResultHeader(ResultInterface $result, $headerName) + { + if (isset($result['@metadata']['headers'][$headerName])) { + return $result['@metadata']['headers'][$headerName]; + } + return null; + } + + protected static function getExceptionHeader(\Exception $e, $headerName) + { + if ($e instanceof ResponseContainerInterface) { + $response = $e->getResponse(); + if ($response instanceof ResponseInterface) { + $header = $response->getHeader($headerName); + if (!empty($header[0])) { + return $header[0]; + } + } + } + return null; + } + + /** + * Constructor stores the passed in handler and options. + * + * @param callable $handler + * @param callable $credentialProvider + * @param $options + * @param $region + * @param $service + */ + public function __construct( + callable $handler, + callable $credentialProvider, + $options, + $region, + $service + ) { + $this->nextHandler = $handler; + $this->credentialProvider = $credentialProvider; + $this->options = $options; + $this->region = $region; + $this->service = $service; + } + + /** + * Standard invoke pattern for middleware execution to be implemented by + * child classes. + * + * @param CommandInterface $cmd + * @param RequestInterface $request + * @return Promise\PromiseInterface + */ + public function __invoke(CommandInterface $cmd, RequestInterface $request) + { + $handler = $this->nextHandler; + $eventData = null; + $enabled = $this->isEnabled(); + + if ($enabled) { + $cmd['@http']['collect_stats'] = true; + $eventData = $this->populateRequestEventData( + $cmd, + $request, + $this->getNewEvent($cmd, $request) + ); + } + + $g = function ($value) use ($eventData, $enabled) { + if ($enabled) { + $eventData = $this->populateResultEventData( + $value, + $eventData + ); + $this->sendEventData($eventData); + + if ($value instanceof MonitoringEventsInterface) { + $value->appendMonitoringEvent($eventData); + } + } + if ($value instanceof \Exception || $value instanceof \Throwable) { + return Promise\Create::rejectionFor($value); + } + return $value; + }; + + return Promise\Create::promiseFor($handler($cmd, $request))->then($g, $g); + } + + private function getClientId() + { + return $this->unwrappedOptions()->getClientId(); + } + + private function getNewEvent( + CommandInterface $cmd, + RequestInterface $request + ) { + $event = [ + 'Api' => $cmd->getName(), + 'ClientId' => $this->getClientId(), + 'Region' => $this->getRegion(), + 'Service' => $this->getService(), + 'Timestamp' => (int) floor(microtime(true) * 1000), + 'UserAgent' => substr( + $request->getHeaderLine('User-Agent') . ' ' . \Aws\default_user_agent(), + 0, + 256 + ), + 'Version' => 1 + ]; + return $event; + } + + private function getHost() + { + return $this->unwrappedOptions()->getHost(); + } + + private function getPort() + { + return $this->unwrappedOptions()->getPort(); + } + + private function getRegion() + { + return $this->region; + } + + private function getService() + { + return $this->service; + } + + /** + * Returns enabled flag from options, unwrapping options if necessary. + * + * @return bool + */ + private function isEnabled() + { + return $this->unwrappedOptions()->isEnabled(); + } + + /** + * Returns $eventData array with information from the request and command. + * + * @param CommandInterface $cmd + * @param RequestInterface $request + * @param array $event + * @return array + */ + protected function populateRequestEventData( + CommandInterface $cmd, + RequestInterface $request, + array $event + ) { + $dataFormat = static::getRequestData($request); + foreach ($dataFormat as $eventKey => $value) { + if ($value !== null) { + $event[$eventKey] = $value; + } + } + return $event; + } + + /** + * Returns $eventData array with information from the response, including + * the calculation for attempt latency. + * + * @param ResultInterface|\Exception $result + * @param array $event + * @return array + */ + protected function populateResultEventData( + $result, + array $event + ) { + $dataFormat = static::getResponseData($result); + foreach ($dataFormat as $eventKey => $value) { + if ($value !== null) { + $event[$eventKey] = $value; + } + } + return $event; + } + + + /** + * Checks if the socket is created. If PHP version is greater or equals to 8 then, + * it will check if the var is instance of \Socket otherwise it will check if is + * a resource. + * + * @return bool Returns true if the socket is created, false otherwise. + */ + private function isSocketCreated(): bool + { + // Before version 8, sockets are resources + // After version 8, sockets are instances of Socket + if (PHP_MAJOR_VERSION >= 8) { + $socketClass = '\Socket'; + return self::$socket instanceof $socketClass; + } else { + return is_resource(self::$socket); + } + } + + /** + * Creates a UDP socket resource and stores it with the class, or retrieves + * it if already instantiated and connected. Handles error-checking and + * re-connecting if necessary. If $forceNewConnection is set to true, a new + * socket will be created. + * + * @param bool $forceNewConnection + * @return Resource + */ + private function prepareSocket($forceNewConnection = false) + { + if (!$this->isSocketCreated() + || $forceNewConnection + || socket_last_error(self::$socket) + ) { + self::$socket = socket_create(AF_INET, SOCK_DGRAM, SOL_UDP); + socket_clear_error(self::$socket); + socket_connect(self::$socket, $this->getHost(), $this->getPort()); + } + + return self::$socket; + } + + /** + * Sends formatted monitoring event data via the UDP socket connection to + * the CSM agent endpoint. + * + * @param array $eventData + * @return int + */ + private function sendEventData(array $eventData) + { + $socket = $this->prepareSocket(); + $datagram = json_encode($eventData); + $result = socket_write($socket, $datagram, strlen($datagram)); + if ($result === false) { + $this->prepareSocket(true); + } + return $result; + } + + /** + * Unwraps options, if needed, and returns them. + * + * @return ConfigurationInterface + */ + private function unwrappedOptions() + { + if (!($this->options instanceof ConfigurationInterface)) { + try { + $this->options = ConfigurationProvider::unwrap($this->options); + } catch (\Exception $e) { + // Errors unwrapping CSM config defaults to disabling it + $this->options = new Configuration( + false, + ConfigurationProvider::DEFAULT_HOST, + ConfigurationProvider::DEFAULT_PORT + ); + } + } + return $this->options; + } +} diff --git a/3rdparty/aws/aws-sdk-php/src/ClientSideMonitoring/ApiCallAttemptMonitoringMiddleware.php b/3rdparty/aws/aws-sdk-php/src/ClientSideMonitoring/ApiCallAttemptMonitoringMiddleware.php new file mode 100644 index 00000000..91810bb9 --- /dev/null +++ b/3rdparty/aws/aws-sdk-php/src/ClientSideMonitoring/ApiCallAttemptMonitoringMiddleware.php @@ -0,0 +1,262 @@ + $request->getUri()->getHost(), + ]; + } + + /** + * {@inheritdoc} + */ + public static function getResponseData($klass) + { + if ($klass instanceof ResultInterface) { + return [ + 'AttemptLatency' => self::getResultAttemptLatency($klass), + 'DestinationIp' => self::getResultDestinationIp($klass), + 'DnsLatency' => self::getResultDnsLatency($klass), + 'HttpStatusCode' => self::getResultHttpStatusCode($klass), + 'XAmzId2' => self::getResultHeader($klass, 'x-amz-id-2'), + 'XAmzRequestId' => self::getResultHeader($klass, 'x-amz-request-id'), + 'XAmznRequestId' => self::getResultHeader($klass, 'x-amzn-RequestId'), + ]; + } + if ($klass instanceof AwsException) { + return [ + 'AttemptLatency' => self::getAwsExceptionAttemptLatency($klass), + 'AwsException' => substr( + self::getAwsExceptionErrorCode($klass), + 0, + 128 + ), + 'AwsExceptionMessage' => substr( + self::getAwsExceptionMessage($klass), + 0, + 512 + ), + 'DestinationIp' => self::getAwsExceptionDestinationIp($klass), + 'DnsLatency' => self::getAwsExceptionDnsLatency($klass), + 'HttpStatusCode' => self::getAwsExceptionHttpStatusCode($klass), + 'XAmzId2' => self::getAwsExceptionHeader($klass, 'x-amz-id-2'), + 'XAmzRequestId' => self::getAwsExceptionHeader( + $klass, + 'x-amz-request-id' + ), + 'XAmznRequestId' => self::getAwsExceptionHeader( + $klass, + 'x-amzn-RequestId' + ), + ]; + } + if ($klass instanceof \Exception) { + return [ + 'HttpStatusCode' => self::getExceptionHttpStatusCode($klass), + 'SdkException' => substr( + self::getExceptionCode($klass), + 0, + 128 + ), + 'SdkExceptionMessage' => substr( + self::getExceptionMessage($klass), + 0, + 512 + ), + 'XAmzId2' => self::getExceptionHeader($klass, 'x-amz-id-2'), + 'XAmzRequestId' => self::getExceptionHeader($klass, 'x-amz-request-id'), + 'XAmznRequestId' => self::getExceptionHeader($klass, 'x-amzn-RequestId'), + ]; + } + + throw new \InvalidArgumentException('Parameter must be an instance of ResultInterface, AwsException or Exception.'); + } + + private static function getResultAttemptLatency(ResultInterface $result) + { + if (isset($result['@metadata']['transferStats']['http'])) { + $attempt = end($result['@metadata']['transferStats']['http']); + if (isset($attempt['total_time'])) { + return (int) floor($attempt['total_time'] * 1000); + } + } + return null; + } + + private static function getResultDestinationIp(ResultInterface $result) + { + if (isset($result['@metadata']['transferStats']['http'])) { + $attempt = end($result['@metadata']['transferStats']['http']); + if (isset($attempt['primary_ip'])) { + return $attempt['primary_ip']; + } + } + return null; + } + + private static function getResultDnsLatency(ResultInterface $result) + { + if (isset($result['@metadata']['transferStats']['http'])) { + $attempt = end($result['@metadata']['transferStats']['http']); + if (isset($attempt['namelookup_time'])) { + return (int) floor($attempt['namelookup_time'] * 1000); + } + } + return null; + } + + private static function getResultHttpStatusCode(ResultInterface $result) + { + return $result['@metadata']['statusCode']; + } + + private static function getAwsExceptionAttemptLatency(AwsException $e) { + $attempt = $e->getTransferInfo(); + if (isset($attempt['total_time'])) { + return (int) floor($attempt['total_time'] * 1000); + } + return null; + } + + private static function getAwsExceptionErrorCode(AwsException $e) { + return $e->getAwsErrorCode(); + } + + private static function getAwsExceptionMessage(AwsException $e) { + return $e->getAwsErrorMessage(); + } + + private static function getAwsExceptionDestinationIp(AwsException $e) { + $attempt = $e->getTransferInfo(); + if (isset($attempt['primary_ip'])) { + return $attempt['primary_ip']; + } + return null; + } + + private static function getAwsExceptionDnsLatency(AwsException $e) { + $attempt = $e->getTransferInfo(); + if (isset($attempt['namelookup_time'])) { + return (int) floor($attempt['namelookup_time'] * 1000); + } + return null; + } + + private static function getAwsExceptionHttpStatusCode(AwsException $e) { + $response = $e->getResponse(); + if ($response !== null) { + return $response->getStatusCode(); + } + return null; + } + + private static function getExceptionHttpStatusCode(\Exception $e) { + if ($e instanceof ResponseContainerInterface) { + $response = $e->getResponse(); + if ($response instanceof ResponseInterface) { + return $response->getStatusCode(); + } + } + return null; + } + + private static function getExceptionCode(\Exception $e) { + if (!($e instanceof AwsException)) { + return get_class($e); + } + return null; + } + + private static function getExceptionMessage(\Exception $e) { + if (!($e instanceof AwsException)) { + return $e->getMessage(); + } + return null; + } + + /** + * {@inheritdoc} + */ + protected function populateRequestEventData( + CommandInterface $cmd, + RequestInterface $request, + array $event + ) { + $event = parent::populateRequestEventData($cmd, $request, $event); + $event['Type'] = 'ApiCallAttempt'; + return $event; + } + + /** + * {@inheritdoc} + */ + protected function populateResultEventData( + $result, + array $event + ) { + $event = parent::populateResultEventData($result, $event); + + $provider = $this->credentialProvider; + /** @var CredentialsInterface $credentials */ + $credentials = $provider()->wait(); + $event['AccessKey'] = $credentials->getAccessKeyId(); + $sessionToken = $credentials->getSecurityToken(); + if ($sessionToken !== null) { + $event['SessionToken'] = $sessionToken; + } + if (empty($event['AttemptLatency'])) { + $event['AttemptLatency'] = (int) (floor(microtime(true) * 1000) - $event['Timestamp']); + } + return $event; + } +} diff --git a/3rdparty/aws/aws-sdk-php/src/ClientSideMonitoring/ApiCallMonitoringMiddleware.php b/3rdparty/aws/aws-sdk-php/src/ClientSideMonitoring/ApiCallMonitoringMiddleware.php new file mode 100644 index 00000000..42080738 --- /dev/null +++ b/3rdparty/aws/aws-sdk-php/src/ClientSideMonitoring/ApiCallMonitoringMiddleware.php @@ -0,0 +1,175 @@ + 'AwsException', + 'FinalAwsExceptionMessage' => 'AwsExceptionMessage', + 'FinalSdkException' => 'SdkException', + 'FinalSdkExceptionMessage' => 'SdkExceptionMessage', + 'FinalHttpStatusCode' => 'HttpStatusCode', + ]; + + /** + * Standard middleware wrapper function with CSM options passed in. + * + * @param callable $credentialProvider + * @param mixed $options + * @param string $region + * @param string $service + * @return callable + */ + public static function wrap( + callable $credentialProvider, + $options, + $region, + $service + ) { + return function (callable $handler) use ( + $credentialProvider, + $options, + $region, + $service + ) { + return new static( + $handler, + $credentialProvider, + $options, + $region, + $service + ); + }; + } + + /** + * {@inheritdoc} + */ + public static function getRequestData(RequestInterface $request) + { + return []; + } + + /** + * {@inheritdoc} + */ + public static function getResponseData($klass) + { + if ($klass instanceof ResultInterface) { + $data = [ + 'AttemptCount' => self::getResultAttemptCount($klass), + 'MaxRetriesExceeded' => 0, + ]; + } elseif ($klass instanceof \Exception) { + $data = [ + 'AttemptCount' => self::getExceptionAttemptCount($klass), + 'MaxRetriesExceeded' => self::getMaxRetriesExceeded($klass), + ]; + } else { + throw new \InvalidArgumentException('Parameter must be an instance of ResultInterface or Exception.'); + } + + return $data + self::getFinalAttemptData($klass); + } + + private static function getResultAttemptCount(ResultInterface $result) { + if (isset($result['@metadata']['transferStats']['http'])) { + return count($result['@metadata']['transferStats']['http']); + } + return 1; + } + + private static function getExceptionAttemptCount(\Exception $e) { + $attemptCount = 0; + if ($e instanceof MonitoringEventsInterface) { + foreach ($e->getMonitoringEvents() as $event) { + if (isset($event['Type']) && + $event['Type'] === 'ApiCallAttempt') { + $attemptCount++; + } + } + + } + return $attemptCount; + } + + private static function getFinalAttemptData($klass) + { + $data = []; + if ($klass instanceof MonitoringEventsInterface) { + $finalAttempt = self::getFinalAttempt($klass->getMonitoringEvents()); + + if (!empty($finalAttempt)) { + foreach (self::$eventKeys as $callKey => $attemptKey) { + if (isset($finalAttempt[$attemptKey])) { + $data[$callKey] = $finalAttempt[$attemptKey]; + } + } + } + } + + return $data; + } + + private static function getFinalAttempt(array $events) + { + for (end($events); key($events) !== null; prev($events)) { + $current = current($events); + if (isset($current['Type']) + && $current['Type'] === 'ApiCallAttempt' + ) { + return $current; + } + } + + return null; + } + + private static function getMaxRetriesExceeded($klass) + { + if ($klass instanceof AwsException && $klass->isMaxRetriesExceeded()) { + return 1; + } + return 0; + } + + /** + * {@inheritdoc} + */ + protected function populateRequestEventData( + CommandInterface $cmd, + RequestInterface $request, + array $event + ) { + $event = parent::populateRequestEventData($cmd, $request, $event); + $event['Type'] = 'ApiCall'; + return $event; + } + + /** + * {@inheritdoc} + */ + protected function populateResultEventData( + $result, + array $event + ) { + $event = parent::populateResultEventData($result, $event); + $event['Latency'] = (int) (floor(microtime(true) * 1000) - $event['Timestamp']); + return $event; + } +} diff --git a/3rdparty/aws/aws-sdk-php/src/ClientSideMonitoring/Configuration.php b/3rdparty/aws/aws-sdk-php/src/ClientSideMonitoring/Configuration.php new file mode 100644 index 00000000..b875274b --- /dev/null +++ b/3rdparty/aws/aws-sdk-php/src/ClientSideMonitoring/Configuration.php @@ -0,0 +1,77 @@ +host = $host; + $this->port = filter_var($port, FILTER_VALIDATE_INT); + if ($this->port === false) { + throw new \InvalidArgumentException( + "CSM 'port' value must be an integer!"); + } + + // Unparsable $enabled flag errors on the side of disabling CSM + $this->enabled = filter_var($enabled, FILTER_VALIDATE_BOOLEAN); + $this->clientId = trim($clientId); + } + + /** + * {@inheritdoc} + */ + public function isEnabled() + { + return $this->enabled; + } + + /** + * {@inheritdoc} + */ + public function getClientId() + { + return $this->clientId; + } + + /** + * /{@inheritdoc} + */ + public function getHost() + { + return $this->host; + } + + /** + * {@inheritdoc} + */ + public function getPort() + { + return $this->port; + } + + /** + * {@inheritdoc} + */ + public function toArray() + { + return [ + 'client_id' => $this->getClientId(), + 'enabled' => $this->isEnabled(), + 'host' => $this->getHost(), + 'port' => $this->getPort() + ]; + } +} \ No newline at end of file diff --git a/3rdparty/aws/aws-sdk-php/src/ClientSideMonitoring/ConfigurationInterface.php b/3rdparty/aws/aws-sdk-php/src/ClientSideMonitoring/ConfigurationInterface.php new file mode 100644 index 00000000..9a548279 --- /dev/null +++ b/3rdparty/aws/aws-sdk-php/src/ClientSideMonitoring/ConfigurationInterface.php @@ -0,0 +1,44 @@ + + * use Aws\ClientSideMonitoring\ConfigurationProvider; + * $provider = ConfigurationProvider::defaultProvider(); + * // Returns a ConfigurationInterface or throws. + * $config = $provider()->wait(); + * + * + * Configuration providers can be composed to create configuration using + * conditional logic that can create different configurations in different + * environments. You can compose multiple providers into a single provider using + * {@see Aws\ClientSideMonitoring\ConfigurationProvider::chain}. This function + * accepts providers as variadic arguments and returns a new function that will + * invoke each provider until a successful configuration is returned. + * + * + * // First try an INI file at this location. + * $a = ConfigurationProvider::ini(null, '/path/to/file.ini'); + * // Then try an INI file at this location. + * $b = ConfigurationProvider::ini(null, '/path/to/other-file.ini'); + * // Then try loading from environment variables. + * $c = ConfigurationProvider::env(); + * // Combine the three providers together. + * $composed = ConfigurationProvider::chain($a, $b, $c); + * // Returns a promise that is fulfilled with a configuration or throws. + * $promise = $composed(); + * // Wait on the configuration to resolve. + * $config = $promise->wait(); + * + */ +class ConfigurationProvider extends AbstractConfigurationProvider + implements ConfigurationProviderInterface +{ + const DEFAULT_CLIENT_ID = ''; + const DEFAULT_ENABLED = false; + const DEFAULT_HOST = '127.0.0.1'; + const DEFAULT_PORT = 31000; + const ENV_CLIENT_ID = 'AWS_CSM_CLIENT_ID'; + const ENV_ENABLED = 'AWS_CSM_ENABLED'; + const ENV_HOST = 'AWS_CSM_HOST'; + const ENV_PORT = 'AWS_CSM_PORT'; + const ENV_PROFILE = 'AWS_PROFILE'; + + public static $cacheKey = 'aws_cached_csm_config'; + + protected static $interfaceClass = ConfigurationInterface::class; + protected static $exceptionClass = ConfigurationException::class; + + /** + * Create a default config provider that first checks for environment + * variables, then checks for a specified profile in the environment-defined + * config file location (env variable is 'AWS_CONFIG_FILE', file location + * defaults to ~/.aws/config), then checks for the "default" profile in the + * environment-defined config file location, and failing those uses a default + * fallback set of configuration options. + * + * This provider is automatically wrapped in a memoize function that caches + * previously provided config options. + * + * @param array $config + * + * @return callable + */ + public static function defaultProvider(array $config = []) + { + $configProviders = [self::env()]; + if ( + !isset($config['use_aws_shared_config_files']) + || $config['use_aws_shared_config_files'] != false + ) { + $configProviders[] = self::ini(); + } + $configProviders[] = self::fallback(); + + $memo = self::memoize( + call_user_func_array([ConfigurationProvider::class, 'chain'], $configProviders) + ); + + if (isset($config['csm']) && $config['csm'] instanceof CacheInterface) { + return self::cache($memo, $config['csm'], self::$cacheKey); + } + + return $memo; + } + + /** + * Provider that creates CSM config from environment variables. + * + * @return callable + */ + public static function env() + { + return function () { + // Use credentials from environment variables, if available + $enabled = getenv(self::ENV_ENABLED); + if ($enabled !== false) { + return Promise\Create::promiseFor( + new Configuration( + $enabled, + getenv(self::ENV_HOST) ?: self::DEFAULT_HOST, + getenv(self::ENV_PORT) ?: self::DEFAULT_PORT, + getenv(self:: ENV_CLIENT_ID) ?: self::DEFAULT_CLIENT_ID + ) + ); + } + + return self::reject('Could not find environment variable CSM config' + . ' in ' . self::ENV_ENABLED. '/' . self::ENV_HOST . '/' + . self::ENV_PORT . '/' . self::ENV_CLIENT_ID); + }; + } + + /** + * Fallback config options when other sources are not set. + * + * @return callable + */ + public static function fallback() + { + return function() { + return Promise\Create::promiseFor( + new Configuration( + self::DEFAULT_ENABLED, + self::DEFAULT_HOST, + self::DEFAULT_PORT, + self::DEFAULT_CLIENT_ID + ) + ); + }; + } + + /** + * Config provider that creates config using a config file whose location + * is specified by an environment variable 'AWS_CONFIG_FILE', defaulting to + * ~/.aws/config if not specified + * + * @param string|null $profile Profile to use. If not specified will use + * the "default" profile. + * @param string|null $filename If provided, uses a custom filename rather + * than looking in the default directory. + * + * @return callable + */ + public static function ini($profile = null, $filename = null) + { + $filename = $filename ?: (self::getDefaultConfigFilename()); + $profile = $profile ?: (getenv(self::ENV_PROFILE) ?: 'aws_csm'); + + return function () use ($profile, $filename) { + if (!@is_readable($filename)) { + return self::reject("Cannot read CSM config from $filename"); + } + $data = \Aws\parse_ini_file($filename, true); + if ($data === false) { + return self::reject("Invalid config file: $filename"); + } + if (!isset($data[$profile])) { + return self::reject("'$profile' not found in config file"); + } + if (!isset($data[$profile]['csm_enabled'])) { + return self::reject("Required CSM config values not present in + INI profile '{$profile}' ({$filename})"); + } + + // host is optional + if (empty($data[$profile]['csm_host'])) { + $data[$profile]['csm_host'] = self::DEFAULT_HOST; + } + + // port is optional + if (!filter_var($data[$profile]['csm_port'] ?? null, FILTER_VALIDATE_INT)) { + $data[$profile]['csm_port'] = self::DEFAULT_PORT; + } + + // client_id is optional + if (empty($data[$profile]['csm_client_id'])) { + $data[$profile]['csm_client_id'] = self::DEFAULT_CLIENT_ID; + } + + return Promise\Create::promiseFor( + new Configuration( + $data[$profile]['csm_enabled'], + $data[$profile]['csm_host'], + $data[$profile]['csm_port'], + $data[$profile]['csm_client_id'] + ) + ); + }; + } + + /** + * Unwraps a configuration object in whatever valid form it is in, + * always returning a ConfigurationInterface object. + * + * @param mixed $config + * @return ConfigurationInterface + * @throws \InvalidArgumentException + */ + public static function unwrap($config) + { + if (is_callable($config)) { + $config = $config(); + } + if ($config instanceof PromiseInterface) { + $config = $config->wait(); + } + if ($config instanceof ConfigurationInterface) { + return $config; + } elseif (is_array($config) && isset($config['enabled'])) { + $client_id = isset($config['client_id']) ? $config['client_id'] + : self::DEFAULT_CLIENT_ID; + $host = isset($config['host']) ? $config['host'] + : self::DEFAULT_HOST; + $port = isset($config['port']) ? $config['port'] + : self::DEFAULT_PORT; + return new Configuration($config['enabled'], $host, $port, $client_id); + } + + throw new \InvalidArgumentException('Not a valid CSM configuration ' + . 'argument.'); + } +} diff --git a/3rdparty/aws/aws-sdk-php/src/ClientSideMonitoring/Exception/ConfigurationException.php b/3rdparty/aws/aws-sdk-php/src/ClientSideMonitoring/Exception/ConfigurationException.php new file mode 100644 index 00000000..827743e2 --- /dev/null +++ b/3rdparty/aws/aws-sdk-php/src/ClientSideMonitoring/Exception/ConfigurationException.php @@ -0,0 +1,15 @@ +name = $name; + $this->data = $args; + $this->handlerList = $list ?: new HandlerList(); + + if (!isset($this->data['@http'])) { + $this->data['@http'] = []; + } + if (!isset($this->data['@context'])) { + $this->data['@context'] = []; + } + $this->metricsBuilder = $metricsBuilder ?: new MetricsBuilder(); + } + + public function __clone() + { + $this->handlerList = clone $this->handlerList; + } + + public function getName() + { + return $this->name; + } + + public function hasParam($name) + { + return array_key_exists($name, $this->data); + } + + public function getHandlerList() + { + return $this->handlerList; + } + + /** + * For overriding auth schemes on a per endpoint basis when using + * EndpointV2 provider. Intended for internal use only. + * + * @param array $authSchemes + * + * @deprecated In favor of using the @context property bag. + * Auth Schemes are now accessible via the `signature_version` key + * in a Command's context, if applicable. Auth Schemes set using + * This method are no longer consumed. + * + * @internal + */ + public function setAuthSchemes(array $authSchemes) + { + trigger_error(__METHOD__ . ' is deprecated. Auth schemes ' + . 'resolved using the service `auth` trait or via endpoint resolution ' + . 'are now set in the command `@context` property.`' + , E_USER_WARNING + ); + + $this->authSchemes = $authSchemes; + } + + /** + * Get auth schemes added to command as required + * for endpoint resolution + * + * @returns array + * + * @deprecated In favor of using the @context property bag. + * Auth schemes are now accessible via the `signature_version` key + * in a Command's context, if applicable. + */ + public function getAuthSchemes() + { + trigger_error(__METHOD__ . ' is deprecated. Auth schemes ' + . 'resolved using the service `auth` trait or via endpoint resolution ' + . 'can now be found in the command `@context` property.`' + , E_USER_WARNING + ); + + return $this->authSchemes ?: []; + } + + /** @deprecated */ + public function get($name) + { + return $this[$name]; + } + + /** + * Returns the metrics builder instance tied up to this command. + * + * @internal + * + * @return MetricsBuilder + */ + public function getMetricsBuilder(): MetricsBuilder + { + return $this->metricsBuilder; + } +} diff --git a/3rdparty/aws/aws-sdk-php/src/CommandInterface.php b/3rdparty/aws/aws-sdk-php/src/CommandInterface.php new file mode 100644 index 00000000..b35c75d3 --- /dev/null +++ b/3rdparty/aws/aws-sdk-php/src/CommandInterface.php @@ -0,0 +1,42 @@ +getBefore($config); + $mapFn = function ($commands) use ($client, $before, $config) { + foreach ($commands as $key => $command) { + if (!($command instanceof CommandInterface)) { + throw new \InvalidArgumentException('Each value yielded by ' + . 'the iterator must be an Aws\CommandInterface.'); + } + if ($before) { + $before($command, $key); + } + if (!empty($config['preserve_iterator_keys'])) { + yield $key => $client->executeAsync($command); + } else { + yield $client->executeAsync($command); + } + } + }; + + $this->each = new EachPromise($mapFn($commands), $config); + } + + /** + * @return PromiseInterface + */ + public function promise(): PromiseInterface + { + return $this->each->promise(); + } + + /** + * Executes a pool synchronously and aggregates the results of the pool + * into an indexed array in the same order as the passed in array. + * + * @param AwsClientInterface $client Client used to execute commands. + * @param mixed $commands Iterable that yields commands. + * @param array $config Configuration options. + * + * @return array + * @see \Aws\CommandPool::__construct for available configuration options. + */ + public static function batch( + AwsClientInterface $client, + $commands, + array $config = [] + ) { + $results = []; + self::cmpCallback($config, 'fulfilled', $results); + self::cmpCallback($config, 'rejected', $results); + + return (new self($client, $commands, $config)) + ->promise() + ->then(static function () use (&$results) { + ksort($results); + return $results; + }) + ->wait(); + } + + /** + * @return callable + */ + private function getBefore(array $config) + { + if (!isset($config['before'])) { + return null; + } + + if (is_callable($config['before'])) { + return $config['before']; + } + + throw new \InvalidArgumentException('before must be callable'); + } + + /** + * Adds an onFulfilled or onRejected callback that aggregates results into + * an array. If a callback is already present, it is replaced with the + * composed function. + * + * @param array $config + * @param $name + * @param array $results + */ + private static function cmpCallback(array &$config, $name, array &$results) + { + if (!isset($config[$name])) { + $config[$name] = function ($v, $k) use (&$results) { + $results[$k] = $v; + }; + } else { + $currentFn = $config[$name]; + $config[$name] = function ($v, $k) use (&$results, $currentFn) { + $currentFn($v, $k); + $results[$k] = $v; + }; + } + } +} diff --git a/3rdparty/aws/aws-sdk-php/src/Configuration/ConfigurationResolver.php b/3rdparty/aws/aws-sdk-php/src/Configuration/ConfigurationResolver.php new file mode 100644 index 00000000..a08595a7 --- /dev/null +++ b/3rdparty/aws/aws-sdk-php/src/Configuration/ConfigurationResolver.php @@ -0,0 +1,250 @@ +client = $config['client']; + $this->assumeRoleParams = $config['assume_role_params']; + } + + /** + * Loads assume role credentials. + * + * @return PromiseInterface + */ + public function __invoke() + { + $client = $this->client; + return $client->assumeRoleAsync($this->assumeRoleParams) + ->then(function (Result $result) { + return $this->client->createCredentials( + $result, + CredentialSources::STS_ASSUME_ROLE + ); + })->otherwise(function (\RuntimeException $exception) { + throw new CredentialsException( + "Error in retrieving assume role credentials.", + 0, + $exception + ); + }); + } +} diff --git a/3rdparty/aws/aws-sdk-php/src/Credentials/AssumeRoleWithWebIdentityCredentialProvider.php b/3rdparty/aws/aws-sdk-php/src/Credentials/AssumeRoleWithWebIdentityCredentialProvider.php new file mode 100644 index 00000000..ea705223 --- /dev/null +++ b/3rdparty/aws/aws-sdk-php/src/Credentials/AssumeRoleWithWebIdentityCredentialProvider.php @@ -0,0 +1,170 @@ +arn = $config['RoleArn']; + + if (!isset($config['WebIdentityTokenFile'])) { + throw new \InvalidArgumentException(self::ERROR_MSG . "'WebIdentityTokenFile'."); + } + $this->tokenFile = $config['WebIdentityTokenFile']; + + if (!preg_match("/^\w\:|^\/|^\\\/", $this->tokenFile)) { + throw new \InvalidArgumentException("'WebIdentityTokenFile' must be an absolute path."); + } + + $this->retries = (int) getenv(self::ENV_RETRIES) ?: (isset($config['retries']) ? $config['retries'] : 3); + $this->authenticationAttempts = 0; + $this->tokenFileReadAttempts = 0; + $this->session = $config['SessionName'] + ?? 'aws-sdk-php-' . round(microtime(true) * 1000); + $region = $config['region'] ?? 'us-east-1'; + if (isset($config['client'])) { + $this->client = $config['client']; + } else { + $this->client = new StsClient([ + 'credentials' => false, + 'region' => $region, + 'version' => 'latest' + ]); + } + + $this->source = $config['source'] + ?? CredentialSources::STS_WEB_ID_TOKEN; + } + + /** + * Loads assume role with web identity credentials. + * + * @return Promise\PromiseInterface + */ + public function __invoke() + { + return Promise\Coroutine::of(function () { + $client = $this->client; + $result = null; + while ($result == null) { + try { + $token = @file_get_contents($this->tokenFile); + if (false === $token) { + clearstatcache(true, dirname($this->tokenFile) . "/" . readlink($this->tokenFile)); + clearstatcache(true, dirname($this->tokenFile) . "/" . dirname(readlink($this->tokenFile))); + clearstatcache(true, $this->tokenFile); + if (!@is_readable($this->tokenFile)) { + throw new CredentialsException( + "Unreadable tokenfile at location {$this->tokenFile}" + ); + } + + $token = @file_get_contents($this->tokenFile); + } + if (empty($token)) { + if ($this->tokenFileReadAttempts < $this->retries) { + sleep((int) pow(1.2, $this->tokenFileReadAttempts)); + $this->tokenFileReadAttempts++; + continue; + } + throw new CredentialsException("InvalidIdentityToken from file: {$this->tokenFile}"); + } + } catch (\Exception $exception) { + throw new CredentialsException( + "Error reading WebIdentityTokenFile from " . $this->tokenFile, + 0, + $exception + ); + } + + $assumeParams = [ + 'RoleArn' => $this->arn, + 'RoleSessionName' => $this->session, + 'WebIdentityToken' => $token + ]; + + try { + $result = $client->assumeRoleWithWebIdentity($assumeParams); + } catch (AwsException $e) { + if ($e->getAwsErrorCode() == 'InvalidIdentityToken') { + if ($this->authenticationAttempts < $this->retries) { + sleep((int) pow(1.2, $this->authenticationAttempts)); + } else { + throw new CredentialsException( + "InvalidIdentityToken, retries exhausted" + ); + } + } else { + throw new CredentialsException( + "Error assuming role from web identity credentials", + 0, + $e + ); + } + } catch (\Exception $e) { + throw new CredentialsException( + "Error retrieving web identity credentials: " . $e->getMessage() + . " (" . $e->getCode() . ")" + ); + } + $this->authenticationAttempts++; + } + + yield $this->client->createCredentials( + $result, + $this->source + ); + }); + } +} diff --git a/3rdparty/aws/aws-sdk-php/src/Credentials/CredentialProvider.php b/3rdparty/aws/aws-sdk-php/src/Credentials/CredentialProvider.php new file mode 100644 index 00000000..698e6095 --- /dev/null +++ b/3rdparty/aws/aws-sdk-php/src/Credentials/CredentialProvider.php @@ -0,0 +1,1025 @@ + + * use Aws\Credentials\CredentialProvider; + * $provider = CredentialProvider::defaultProvider(); + * // Returns a CredentialsInterface or throws. + * $creds = $provider()->wait(); + * + * + * Credential providers can be composed to create credentials using conditional + * logic that can create different credentials in different environments. You + * can compose multiple providers into a single provider using + * {@see Aws\Credentials\CredentialProvider::chain}. This function accepts + * providers as variadic arguments and returns a new function that will invoke + * each provider until a successful set of credentials is returned. + * + * + * // First try an INI file at this location. + * $a = CredentialProvider::ini(null, '/path/to/file.ini'); + * // Then try an INI file at this location. + * $b = CredentialProvider::ini(null, '/path/to/other-file.ini'); + * // Then try loading from environment variables. + * $c = CredentialProvider::env(); + * // Combine the three providers together. + * $composed = CredentialProvider::chain($a, $b, $c); + * // Returns a promise that is fulfilled with credentials or throws. + * $promise = $composed(); + * // Wait on the credentials to resolve. + * $creds = $promise->wait(); + * + */ +class CredentialProvider +{ + const ENV_ARN = 'AWS_ROLE_ARN'; + const ENV_KEY = 'AWS_ACCESS_KEY_ID'; + const ENV_PROFILE = 'AWS_PROFILE'; + const ENV_ROLE_SESSION_NAME = 'AWS_ROLE_SESSION_NAME'; + const ENV_SECRET = 'AWS_SECRET_ACCESS_KEY'; + const ENV_ACCOUNT_ID = 'AWS_ACCOUNT_ID'; + const ENV_SESSION = 'AWS_SESSION_TOKEN'; + const ENV_TOKEN_FILE = 'AWS_WEB_IDENTITY_TOKEN_FILE'; + const ENV_SHARED_CREDENTIALS_FILE = 'AWS_SHARED_CREDENTIALS_FILE'; + + /** + * Create a default credential provider that + * first checks for environment variables, + * then checks for assumed role via web identity, + * then checks for cached SSO credentials from the CLI, + * then check for credential_process in the "default" profile in ~/.aws/credentials, + * then checks for the "default" profile in ~/.aws/credentials, + * then for credential_process in the "default profile" profile in ~/.aws/config, + * then checks for "profile default" profile in ~/.aws/config (which is + * the default profile of AWS CLI), + * then tries to make a GET Request to fetch credentials if ECS environment variable is presented, + * finally checks for EC2 instance profile credentials. + * + * This provider is automatically wrapped in a memoize function that caches + * previously provided credentials. + * + * @param array $config Optional array of ecs/instance profile credentials + * provider options. + * + * @return callable + */ + public static function defaultProvider(array $config = []) + { + $cacheable = [ + 'web_identity', + 'sso', + 'process_credentials', + 'process_config', + 'ecs', + 'instance' + ]; + + $profileName = getenv(self::ENV_PROFILE) ?: 'default'; + + $defaultChain = [ + 'env' => self::env(), + 'web_identity' => self::assumeRoleWithWebIdentityCredentialProvider($config), + ]; + if ( + !isset($config['use_aws_shared_config_files']) + || $config['use_aws_shared_config_files'] !== false + ) { + $defaultChain['sso'] = self::sso( + $profileName, + self::getHomeDir() . '/.aws/config', + $config + ); + $defaultChain['process_credentials'] = self::process(); + $defaultChain['ini'] = self::ini(); + $defaultChain['process_config'] = self::process( + 'profile ' . $profileName, + self::getHomeDir() . '/.aws/config' + ); + $defaultChain['ini_config'] = self::ini( + 'profile '. $profileName, + self::getHomeDir() . '/.aws/config' + ); + } + + if (self::shouldUseEcs()) { + $defaultChain['ecs'] = self::ecsCredentials($config); + } else { + $defaultChain['instance'] = self::instanceProfile($config); + } + + if (isset($config['credentials']) + && $config['credentials'] instanceof CacheInterface + ) { + foreach ($cacheable as $provider) { + if (isset($defaultChain[$provider])) { + $defaultChain[$provider] = self::cache( + $defaultChain[$provider], + $config['credentials'], + 'aws_cached_' . $provider . '_credentials' + ); + } + } + } + + return self::memoize( + call_user_func_array( + [CredentialProvider::class, 'chain'], + array_values($defaultChain) + ) + ); + } + + /** + * Create a credential provider function from a set of static credentials. + * + * @param CredentialsInterface $creds + * + * @return callable + */ + public static function fromCredentials(CredentialsInterface $creds) + { + $promise = Promise\Create::promiseFor($creds); + + return function () use ($promise) { + return $promise; + }; + } + + /** + * Creates an aggregate credentials provider that invokes the provided + * variadic providers one after the other until a provider returns + * credentials. + * + * @return callable + */ + public static function chain() + { + $links = func_get_args(); + if (empty($links)) { + throw new \InvalidArgumentException('No providers in chain'); + } + + return function ($previousCreds = null) use ($links) { + /** @var callable $parent */ + $parent = array_shift($links); + $promise = $parent(); + while ($next = array_shift($links)) { + if ($next instanceof InstanceProfileProvider + && $previousCreds instanceof Credentials + ) { + $promise = $promise->otherwise( + function () use ($next, $previousCreds) {return $next($previousCreds);} + ); + } else { + $promise = $promise->otherwise($next); + } + } + return $promise; + }; + } + + /** + * Wraps a credential provider and caches previously provided credentials. + * + * Ensures that cached credentials are refreshed when they expire. + * + * @param callable $provider Credentials provider function to wrap. + * + * @return callable + */ + public static function memoize(callable $provider) + { + return function () use ($provider) { + static $result; + static $isConstant; + + // Constant credentials will be returned constantly. + if ($isConstant) { + return $result; + } + + // Create the initial promise that will be used as the cached value + // until it expires. + if (null === $result) { + $result = $provider(); + } + + // Return credentials that could expire and refresh when needed. + return $result + ->then(function (CredentialsInterface $creds) use ($provider, &$isConstant, &$result) { + // Determine if these are constant credentials. + if (!$creds->getExpiration()) { + $isConstant = true; + return $creds; + } + + // Refresh expired credentials. + if (!$creds->isExpired()) { + return $creds; + } + // Refresh the result and forward the promise. + return $result = $provider($creds); + }) + ->otherwise(function($reason) use (&$result) { + // Cleanup rejected promise. + $result = null; + return new Promise\RejectedPromise($reason); + }); + }; + } + + /** + * Wraps a credential provider and saves provided credentials in an + * instance of Aws\CacheInterface. Forwards calls when no credentials found + * in cache and updates cache with the results. + * + * @param callable $provider Credentials provider function to wrap + * @param CacheInterface $cache Cache to store credentials + * @param string|null $cacheKey (optional) Cache key to use + * + * @return callable + */ + public static function cache( + callable $provider, + CacheInterface $cache, + $cacheKey = null + ) { + $cacheKey = $cacheKey ?: 'aws_cached_credentials'; + + return function () use ($provider, $cache, $cacheKey) { + $found = $cache->get($cacheKey); + if ($found instanceof CredentialsInterface && !$found->isExpired()) { + return Promise\Create::promiseFor($found); + } + + return $provider() + ->then(function (CredentialsInterface $creds) use ( + $cache, + $cacheKey + ) { + $cache->set( + $cacheKey, + $creds, + null === $creds->getExpiration() ? + 0 : $creds->getExpiration() - time() + ); + + return $creds; + }); + }; + } + + /** + * Provider that creates credentials from environment variables + * AWS_ACCESS_KEY_ID, AWS_SECRET_ACCESS_KEY, and AWS_SESSION_TOKEN. + * + * @return callable + */ + public static function env() + { + return function () { + // Use credentials from environment variables, if available + $key = getenv(self::ENV_KEY); + $secret = getenv(self::ENV_SECRET); + $accountId = getenv(self::ENV_ACCOUNT_ID) ?: null; + $token = getenv(self::ENV_SESSION) ?: null; + + if ($key && $secret) { + return Promise\Create::promiseFor( + new Credentials( + $key, + $secret, + $token, + null, + $accountId, + CredentialSources::ENVIRONMENT + ) + ); + } + + return self::reject('Could not find environment variable ' + . 'credentials in ' . self::ENV_KEY . '/' . self::ENV_SECRET); + }; + } + + /** + * Credential provider that creates credentials using instance profile + * credentials. + * + * @param array $config Array of configuration data. + * + * @return InstanceProfileProvider + * @see Aws\Credentials\InstanceProfileProvider for $config details. + */ + public static function instanceProfile(array $config = []) + { + return new InstanceProfileProvider($config); + } + + /** + * Credential provider that retrieves cached SSO credentials from the CLI + * + * @return callable + */ + public static function sso($ssoProfileName = 'default', + $filename = null, + $config = [] + ) { + $filename = $filename ?: (self::getHomeDir() . '/.aws/config'); + + return function () use ($ssoProfileName, $filename, $config) { + if (!@is_readable($filename)) { + return self::reject("Cannot read credentials from $filename"); + } + $profiles = self::loadProfiles($filename); + + if (isset($profiles[$ssoProfileName])) { + $ssoProfile = $profiles[$ssoProfileName]; + } elseif (isset($profiles['profile ' . $ssoProfileName])) { + $ssoProfileName = 'profile ' . $ssoProfileName; + $ssoProfile = $profiles[$ssoProfileName]; + } else { + return self::reject("Profile {$ssoProfileName} does not exist in {$filename}."); + } + + if (!empty($ssoProfile['sso_session'])) { + return CredentialProvider::getSsoCredentials($profiles, $ssoProfileName, $filename, $config); + } else { + return CredentialProvider::getSsoCredentialsLegacy($profiles, $ssoProfileName, $filename, $config); + } + }; + } + + /** + * Credential provider that creates credentials using + * ecs credentials by a GET request, whose uri is specified + * by environment variable + * + * @param array $config Array of configuration data. + * + * @return EcsCredentialProvider + * @see Aws\Credentials\EcsCredentialProvider for $config details. + */ + public static function ecsCredentials(array $config = []) + { + return new EcsCredentialProvider($config); + } + + /** + * Credential provider that creates credentials using assume role + * + * @param array $config Array of configuration data + * @return callable + * @see Aws\Credentials\AssumeRoleCredentialProvider for $config details. + */ + public static function assumeRole(array $config=[]) + { + return new AssumeRoleCredentialProvider($config); + } + + /** + * Credential provider that creates credentials by assuming role from a + * Web Identity Token + * + * @param array $config Array of configuration data + * @return callable + * @see Aws\Credentials\AssumeRoleWithWebIdentityCredentialProvider for + * $config details. + */ + public static function assumeRoleWithWebIdentityCredentialProvider(array $config = []) + { + return function () use ($config) { + $arnFromEnv = getenv(self::ENV_ARN); + $tokenFromEnv = getenv(self::ENV_TOKEN_FILE); + $stsClient = isset($config['stsClient']) + ? $config['stsClient'] + : null; + $region = isset($config['region']) + ? $config['region'] + : null; + + if ($tokenFromEnv && $arnFromEnv) { + $sessionName = getenv(self::ENV_ROLE_SESSION_NAME) + ? getenv(self::ENV_ROLE_SESSION_NAME) + : null; + $provider = new AssumeRoleWithWebIdentityCredentialProvider([ + 'RoleArn' => $arnFromEnv, + 'WebIdentityTokenFile' => $tokenFromEnv, + 'SessionName' => $sessionName, + 'client' => $stsClient, + 'region' => $region, + 'source' => CredentialSources::ENVIRONMENT_STS_WEB_ID_TOKEN + ]); + + return $provider(); + } + + $profileName = getenv(self::ENV_PROFILE) ?: 'default'; + if (isset($config['filename'])) { + $profiles = self::loadProfiles($config['filename']); + } else { + $profiles = self::loadDefaultProfiles(); + } + + if (isset($profiles[$profileName])) { + $profile = $profiles[$profileName]; + if (isset($profile['region'])) { + $region = $profile['region']; + } + if (isset($profile['web_identity_token_file']) + && isset($profile['role_arn']) + ) { + $sessionName = isset($profile['role_session_name']) + ? $profile['role_session_name'] + : null; + $provider = new AssumeRoleWithWebIdentityCredentialProvider([ + 'RoleArn' => $profile['role_arn'], + 'WebIdentityTokenFile' => $profile['web_identity_token_file'], + 'SessionName' => $sessionName, + 'client' => $stsClient, + 'region' => $region, + 'source' => CredentialSources::PROFILE_STS_WEB_ID_TOKEN + ]); + + return $provider(); + } + } else { + return self::reject("Unknown profile: $profileName"); + } + return self::reject("No RoleArn or WebIdentityTokenFile specified"); + }; + } + + /** + * Credentials provider that creates credentials using an ini file stored + * in the current user's home directory. A source can be provided + * in this file for assuming a role using the credential_source config option. + * + * @param string|null $profile Profile to use. If not specified will use + * the "default" profile in "~/.aws/credentials". + * @param string|null $filename If provided, uses a custom filename rather + * than looking in the home directory. + * @param array|null $config If provided, may contain the following: + * preferStaticCredentials: If true, prefer static + * credentials to role_arn if both are present + * disableAssumeRole: If true, disable support for + * roles that assume an IAM role. If true and role profile + * is selected, an error is raised. + * stsClient: StsClient used to assume role specified in profile + * + * @return callable + */ + public static function ini($profile = null, $filename = null, array $config = []) + { + $filename = self::getFileName($filename); + $profile = $profile ?: (getenv(self::ENV_PROFILE) ?: 'default'); + + return function () use ($profile, $filename, $config) { + $preferStaticCredentials = isset($config['preferStaticCredentials']) + ? $config['preferStaticCredentials'] + : false; + $disableAssumeRole = isset($config['disableAssumeRole']) + ? $config['disableAssumeRole'] + : false; + $stsClient = isset($config['stsClient']) ? $config['stsClient'] : null; + + if (!@is_readable($filename)) { + return self::reject("Cannot read credentials from $filename"); + } + $data = self::loadProfiles($filename); + if ($data === false) { + return self::reject("Invalid credentials file: $filename"); + } + if (!isset($data[$profile])) { + return self::reject("'$profile' not found in credentials file"); + } + + /* + In the CLI, the presence of both a role_arn and static credentials have + different meanings depending on how many profiles have been visited. For + the first profile processed, role_arn takes precedence over any static + credentials, but for all subsequent profiles, static credentials are + used if present, and only in their absence will the profile's + source_profile and role_arn keys be used to load another set of + credentials. This bool is intended to yield compatible behaviour in this + sdk. + */ + $preferStaticCredentialsToRoleArn = ($preferStaticCredentials + && isset($data[$profile]['aws_access_key_id']) + && isset($data[$profile]['aws_secret_access_key'])); + + if (isset($data[$profile]['role_arn']) + && !$preferStaticCredentialsToRoleArn + ) { + if ($disableAssumeRole) { + return self::reject( + "Role assumption profiles are disabled. " + . "Failed to load profile " . $profile); + } + return self::loadRoleProfile( + $data, + $profile, + $filename, + $stsClient, + $config + ); + } + + if (!isset($data[$profile]['aws_access_key_id']) + || !isset($data[$profile]['aws_secret_access_key']) + ) { + return self::reject("No credentials present in INI profile " + . "'$profile' ($filename)"); + } + + if (empty($data[$profile]['aws_session_token'])) { + $data[$profile]['aws_session_token'] + = isset($data[$profile]['aws_security_token']) + ? $data[$profile]['aws_security_token'] + : null; + } + + return Promise\Create::promiseFor( + new Credentials( + $data[$profile]['aws_access_key_id'], + $data[$profile]['aws_secret_access_key'], + $data[$profile]['aws_session_token'], + null, + $data[$profile]['aws_account_id'] ?? null, + CredentialSources::PROFILE + ) + ); + }; + } + + /** + * Credentials provider that creates credentials using a process configured in + * ini file stored in the current user's home directory. + * + * @param string|null $profile Profile to use. If not specified will use + * the "default" profile in "~/.aws/credentials". + * @param string|null $filename If provided, uses a custom filename rather + * than looking in the home directory. + * + * @return callable + */ + public static function process($profile = null, $filename = null) + { + $filename = self::getFileName($filename); + $profile = $profile ?: (getenv(self::ENV_PROFILE) ?: 'default'); + + return function () use ($profile, $filename) { + if (!@is_readable($filename)) { + return self::reject("Cannot read process credentials from $filename"); + } + $data = \Aws\parse_ini_file($filename, true, INI_SCANNER_RAW); + if ($data === false) { + return self::reject("Invalid credentials file: $filename"); + } + if (!isset($data[$profile])) { + return self::reject("'$profile' not found in credentials file"); + } + if (!isset($data[$profile]['credential_process'])) { + return self::reject("No credential_process present in INI profile " + . "'$profile' ($filename)"); + } + + $credentialProcess = $data[$profile]['credential_process']; + $json = shell_exec($credentialProcess); + + $processData = json_decode($json, true); + + // Only support version 1 + if (isset($processData['Version'])) { + if ($processData['Version'] !== 1) { + return self::reject("credential_process does not return Version == 1"); + } + } + + if (!isset($processData['AccessKeyId']) + || !isset($processData['SecretAccessKey'])) + { + return self::reject("credential_process does not return valid credentials"); + } + + if (isset($processData['Expiration'])) { + try { + $expiration = new DateTimeResult($processData['Expiration']); + } catch (\Exception $e) { + return self::reject("credential_process returned invalid expiration"); + } + $now = new DateTimeResult(); + if ($expiration < $now) { + return self::reject("credential_process returned expired credentials"); + } + $expires = $expiration->getTimestamp(); + } else { + $expires = null; + } + + if (empty($processData['SessionToken'])) { + $processData['SessionToken'] = null; + } + + $accountId = null; + if (!empty($processData['AccountId'])) { + $accountId = $processData['AccountId']; + } elseif (!empty($data[$profile]['aws_account_id'])) { + $accountId = $data[$profile]['aws_account_id']; + } + + return Promise\Create::promiseFor( + new Credentials( + $processData['AccessKeyId'], + $processData['SecretAccessKey'], + $processData['SessionToken'], + $expires, + $accountId, + CredentialSources::PROFILE_PROCESS + ) + ); + }; + } + + /** + * Assumes role for profile that includes role_arn + * + * @return callable + */ + private static function loadRoleProfile( + $profiles, + $profileName, + $filename, + $stsClient, + $config = [] + ) { + $roleProfile = $profiles[$profileName]; + $roleArn = isset($roleProfile['role_arn']) ? $roleProfile['role_arn'] : ''; + $roleSessionName = isset($roleProfile['role_session_name']) + ? $roleProfile['role_session_name'] + : 'aws-sdk-php-' . round(microtime(true) * 1000); + + if ( + empty($roleProfile['source_profile']) + == empty($roleProfile['credential_source']) + ) { + return self::reject("Either source_profile or credential_source must be set " . + "using profile " . $profileName . ", but not both." + ); + } + + $sourceProfileName = ""; + if (!empty($roleProfile['source_profile'])) { + $sourceProfileName = $roleProfile['source_profile']; + if (!isset($profiles[$sourceProfileName])) { + return self::reject("source_profile " . $sourceProfileName + . " using profile " . $profileName . " does not exist" + ); + } + if (isset($config['visited_profiles']) && + in_array($roleProfile['source_profile'], $config['visited_profiles']) + ) { + return self::reject("Circular source_profile reference found."); + } + $config['visited_profiles'] [] = $roleProfile['source_profile']; + } else { + if (empty($roleArn)) { + return self::reject( + "A role_arn must be provided with credential_source in " . + "file {$filename} under profile {$profileName} " + ); + } + } + + if (empty($stsClient)) { + $sourceRegion = isset($profiles[$sourceProfileName]['region']) + ? $profiles[$sourceProfileName]['region'] + : 'us-east-1'; + $config['preferStaticCredentials'] = true; + $sourceCredentials = null; + if (!empty($roleProfile['source_profile'])){ + $sourceCredentials = call_user_func( + CredentialProvider::ini($sourceProfileName, $filename, $config) + )->wait(); + } else { + $sourceCredentials = self::getCredentialsFromSource( + $profileName, + $filename + ); + } + $stsClient = new StsClient([ + 'credentials' => $sourceCredentials, + 'region' => $sourceRegion, + 'version' => '2011-06-15', + ]); + } + + $result = $stsClient->assumeRole([ + 'RoleArn' => $roleArn, + 'RoleSessionName' => $roleSessionName + ]); + $credentials = $stsClient->createCredentials( + $result, + CredentialSources::STS_ASSUME_ROLE + ); + + return Promise\Create::promiseFor($credentials); + } + + /** + * Gets the environment's HOME directory if available. + * + * @return null|string + */ + private static function getHomeDir() + { + // On Linux/Unix-like systems, use the HOME environment variable + if ($homeDir = getenv('HOME')) { + return $homeDir; + } + + // Get the HOMEDRIVE and HOMEPATH values for Windows hosts + $homeDrive = getenv('HOMEDRIVE'); + $homePath = getenv('HOMEPATH'); + + return ($homeDrive && $homePath) ? $homeDrive . $homePath : null; + } + + /** + * Gets profiles from specified $filename, or default ini files. + */ + private static function loadProfiles($filename) + { + $profileData = \Aws\parse_ini_file($filename, true, INI_SCANNER_RAW); + + // If loading .aws/credentials, also load .aws/config when AWS_SDK_LOAD_NONDEFAULT_CONFIG is set + if ($filename === self::getHomeDir() . '/.aws/credentials' + && getenv('AWS_SDK_LOAD_NONDEFAULT_CONFIG') + ) { + $configFilename = self::getHomeDir() . '/.aws/config'; + $configProfileData = \Aws\parse_ini_file($configFilename, true, INI_SCANNER_RAW); + foreach ($configProfileData as $name => $profile) { + // standardize config profile names + $name = str_replace('profile ', '', $name); + if (!isset($profileData[$name])) { + $profileData[$name] = $profile; + } + } + } + + return $profileData; + } + + /** + * Gets profiles from ~/.aws/credentials and ~/.aws/config ini files + */ + private static function loadDefaultProfiles() { + $profiles = []; + $credFile = self::getHomeDir() . '/.aws/credentials'; + $configFile = self::getHomeDir() . '/.aws/config'; + if (file_exists($credFile)) { + $profiles = \Aws\parse_ini_file($credFile, true, INI_SCANNER_RAW); + } + + if (file_exists($configFile)) { + $configProfileData = \Aws\parse_ini_file($configFile, true, INI_SCANNER_RAW); + foreach ($configProfileData as $name => $profile) { + // standardize config profile names + $name = str_replace('profile ', '', $name); + if (!isset($profiles[$name])) { + $profiles[$name] = $profile; + } + } + } + + return $profiles; + } + + public static function getCredentialsFromSource( + $profileName = '', + $filename = '', + $config = [] + ) { + $data = self::loadProfiles($filename); + $credentialSource = !empty($data[$profileName]['credential_source']) + ? $data[$profileName]['credential_source'] + : null; + $credentialsPromise = null; + + switch ($credentialSource) { + case 'Environment': + $credentialsPromise = self::env(); + break; + case 'Ec2InstanceMetadata': + $credentialsPromise = self::instanceProfile($config); + break; + case 'EcsContainer': + $credentialsPromise = self::ecsCredentials($config); + break; + default: + throw new CredentialsException( + "Invalid credential_source found in config file: {$credentialSource}. Valid inputs " + . "include Environment, Ec2InstanceMetadata, and EcsContainer." + ); + } + + $credentialsResult = null; + try { + $credentialsResult = $credentialsPromise()->wait(); + } catch (\Exception $reason) { + return self::reject( + "Unable to successfully retrieve credentials from the source specified in the" + . " credentials file: {$credentialSource}; failure message was: " + . $reason->getMessage() + ); + } + return function () use ($credentialsResult) { + return Promise\Create::promiseFor($credentialsResult); + }; + } + + private static function reject($msg) + { + return new Promise\RejectedPromise(new CredentialsException($msg)); + } + + /** + * @param $filename + * @return string + */ + private static function getFileName($filename) + { + if (!isset($filename)) { + $filename = getenv(self::ENV_SHARED_CREDENTIALS_FILE) ?: + (self::getHomeDir() . '/.aws/credentials'); + } + return $filename; + } + + /** + * @return boolean + */ + public static function shouldUseEcs() + { + //Check for relative uri. if not, then full uri. + //fall back to server for each as getenv is not thread-safe. + return !empty(getenv(EcsCredentialProvider::ENV_URI)) + || !empty($_SERVER[EcsCredentialProvider::ENV_URI]) + || !empty(getenv(EcsCredentialProvider::ENV_FULL_URI)) + || !empty($_SERVER[EcsCredentialProvider::ENV_FULL_URI]); + } + + /** + * @param $profiles + * @param $ssoProfileName + * @param $filename + * @param $config + * @return Promise\PromiseInterface + */ + private static function getSsoCredentials($profiles, $ssoProfileName, $filename, $config) + { + if (empty($config['ssoOidcClient'])) { + $ssoProfile = $profiles[$ssoProfileName]; + $sessionName = $ssoProfile['sso_session']; + if (empty($profiles['sso-session ' . $sessionName])) { + return self::reject( + "Could not find sso-session {$sessionName} in {$filename}" + ); + } + $ssoSession = $profiles['sso-session ' . $ssoProfile['sso_session']]; + $ssoOidcClient = new Aws\SSOOIDC\SSOOIDCClient([ + 'region' => $ssoSession['sso_region'], + 'version' => '2019-06-10', + 'credentials' => false + ]); + } else { + $ssoOidcClient = $config['ssoClient']; + } + + $tokenPromise = new Aws\Token\SsoTokenProvider( + $ssoProfileName, + $filename, + $ssoOidcClient + ); + $token = $tokenPromise()->wait(); + $ssoCredentials = CredentialProvider::getCredentialsFromSsoService( + $ssoProfile, + $ssoSession['sso_region'], + $token->getToken(), + $config + ); + + //Expiration value is returned in epoch milliseconds. Conversion to seconds + $expiration = intdiv($ssoCredentials['expiration'], 1000); + return Promise\Create::promiseFor( + new Credentials( + $ssoCredentials['accessKeyId'], + $ssoCredentials['secretAccessKey'], + $ssoCredentials['sessionToken'], + $expiration, + $ssoProfile['sso_account_id'], + CredentialSources::PROFILE_SSO + ) + ); + } + + /** + * @param $profiles + * @param $ssoProfileName + * @param $filename + * @param $config + * @return Promise\PromiseInterface + */ + private static function getSsoCredentialsLegacy($profiles, $ssoProfileName, $filename, $config) + { + $ssoProfile = $profiles[$ssoProfileName]; + if (empty($ssoProfile['sso_start_url']) + || empty($ssoProfile['sso_region']) + || empty($ssoProfile['sso_account_id']) + || empty($ssoProfile['sso_role_name']) + ) { + return self::reject( + "Profile {$ssoProfileName} in {$filename} must contain the following keys: " + . "sso_start_url, sso_region, sso_account_id, and sso_role_name." + ); + } + $tokenLocation = self::getHomeDir() + . '/.aws/sso/cache/' + . sha1($ssoProfile['sso_start_url']) + . ".json"; + + if (!@is_readable($tokenLocation)) { + return self::reject("Unable to read token file at $tokenLocation"); + } + $tokenData = json_decode(file_get_contents($tokenLocation), true); + if (empty($tokenData['accessToken']) || empty($tokenData['expiresAt'])) { + return self::reject( + "Token file at {$tokenLocation} must contain an access token and an expiration" + ); + } + try { + $expiration = (new DateTimeResult($tokenData['expiresAt']))->getTimestamp(); + } catch (\Exception $e) { + return self::reject("Cached SSO credentials returned an invalid expiration"); + } + $now = time(); + if ($expiration < $now) { + return self::reject("Cached SSO credentials returned expired credentials"); + } + $ssoCredentials = CredentialProvider::getCredentialsFromSsoService( + $ssoProfile, + $ssoProfile['sso_region'], + $tokenData['accessToken'], + $config + ); + return Promise\Create::promiseFor( + new Credentials( + $ssoCredentials['accessKeyId'], + $ssoCredentials['secretAccessKey'], + $ssoCredentials['sessionToken'], + $expiration, + $ssoProfile['sso_account_id'], + CredentialSources::PROFILE_SSO_LEGACY + ) + ); + } + /** + * @param array $ssoProfile + * @param string $clientRegion + * @param string $accessToken + * @param array $config + * @return array|null + */ + private static function getCredentialsFromSsoService($ssoProfile, $clientRegion, $accessToken, $config) + { + if (empty($config['ssoClient'])) { + $ssoClient = new Aws\SSO\SSOClient([ + 'region' => $clientRegion, + 'version' => '2019-06-10', + 'credentials' => false + ]); + } else { + $ssoClient = $config['ssoClient']; + } + $ssoResponse = $ssoClient->getRoleCredentials([ + 'accessToken' => $accessToken, + 'accountId' => $ssoProfile['sso_account_id'], + 'roleName' => $ssoProfile['sso_role_name'] + ]); + + $ssoCredentials = $ssoResponse['roleCredentials']; + return $ssoCredentials; + } +} + diff --git a/3rdparty/aws/aws-sdk-php/src/Credentials/CredentialSources.php b/3rdparty/aws/aws-sdk-php/src/Credentials/CredentialSources.php new file mode 100644 index 00000000..829aa919 --- /dev/null +++ b/3rdparty/aws/aws-sdk-php/src/Credentials/CredentialSources.php @@ -0,0 +1,22 @@ +key = trim((string) $key); + $this->secret = trim((string) $secret); + $this->token = $token; + $this->expires = $expires; + $this->accountId = $accountId; + $this->source = $source ?? CredentialSources::STATIC; + } + + public static function __set_state(array $state) + { + return new self( + $state['key'], + $state['secret'], + $state['token'], + $state['expires'], + $state['accountId'], + $state['source'] ?? null + ); + } + + public function getAccessKeyId() + { + return $this->key; + } + + public function getSecretKey() + { + return $this->secret; + } + + public function getSecurityToken() + { + return $this->token; + } + + public function getExpiration() + { + return $this->expires; + } + + public function isExpired() + { + return $this->expires !== null && time() >= $this->expires; + } + + public function getAccountId() + { + return $this->accountId; + } + + public function getSource() + { + return $this->source; + } + + public function toArray() + { + return [ + 'key' => $this->key, + 'secret' => $this->secret, + 'token' => $this->token, + 'expires' => $this->expires, + 'accountId' => $this->accountId, + 'source' => $this->source + ]; + } + + public function serialize() + { + return json_encode($this->__serialize()); + } + + public function unserialize($serialized) + { + $data = json_decode($serialized, true); + + $this->__unserialize($data); + } + + public function __serialize() + { + return $this->toArray(); + } + + public function __unserialize($data) + { + $this->key = $data['key']; + $this->secret = $data['secret']; + $this->token = $data['token']; + $this->expires = $data['expires']; + $this->accountId = $data['accountId'] ?? null; + $this->source = $data['source'] ?? null; + } + + /** + * Internal-only. Used when IMDS is unreachable + * or returns expires credentials. + * + * @internal + */ + public function extendExpiration() { + $extension = mt_rand(5, 10); + $this->expires = time() + $extension * 60; + + $message = <<= $loopbackStart && $ipLong <= $loopbackEnd); + } +} diff --git a/3rdparty/aws/aws-sdk-php/src/Credentials/EcsCredentialProvider.php b/3rdparty/aws/aws-sdk-php/src/Credentials/EcsCredentialProvider.php new file mode 100644 index 00000000..e95b2b00 --- /dev/null +++ b/3rdparty/aws/aws-sdk-php/src/Credentials/EcsCredentialProvider.php @@ -0,0 +1,262 @@ +timeout = (float) isset($config['timeout']) + ? $config['timeout'] + : (getenv(self::ENV_TIMEOUT) ?: self::DEFAULT_ENV_TIMEOUT); + $this->retries = (int) isset($config['retries']) + ? $config['retries'] + : ((int) getenv(self::ENV_RETRIES) ?: self::DEFAULT_ENV_RETRIES); + + $this->client = $config['client'] ?? \Aws\default_http_handler(); + } + + /** + * Load container credentials. + * + * @return PromiseInterface + * @throws GuzzleException + */ + public function __invoke() + { + $this->attempts = 0; + $uri = $this->getEcsUri(); + if ($this->isCompatibleUri($uri)) { + return Promise\Coroutine::of(function () { + $client = $this->client; + $request = new Request('GET', $this->getEcsUri()); + $headers = $this->getHeadersForAuthToken(); + $credentials = null; + while ($credentials === null) { + $credentials = (yield $client( + $request, + [ + 'timeout' => $this->timeout, + 'proxy' => '', + 'headers' => $headers, + ] + )->then(function (ResponseInterface $response) { + $result = $this->decodeResult((string)$response->getBody()); + if (!isset($result['AccountId']) && isset($result['RoleArn'])) { + try { + $parsedArn = new Arn($result['RoleArn']); + $result['AccountId'] = $parsedArn->getAccountId(); + } catch (\Exception $e) { + // AccountId will be null + } + } + + return new Credentials( + $result['AccessKeyId'], + $result['SecretAccessKey'], + $result['Token'], + strtotime($result['Expiration']), + $result['AccountId'] ?? null, + CredentialSources::ECS + ); + })->otherwise(function ($reason) { + $reason = is_array($reason) ? $reason['exception'] : $reason; + + $isRetryable = $reason instanceof ConnectException; + if ($isRetryable && ($this->attempts < $this->retries)) { + sleep((int)pow(1.2, $this->attempts)); + } else { + $msg = $reason->getMessage(); + throw new CredentialsException( + sprintf('Error retrieving credentials from container metadata after attempt %d/%d (%s)', $this->attempts, $this->retries, $msg) + ); + } + })); + $this->attempts++; + } + + yield $credentials; + }); + } + + throw new CredentialsException("Uri '{$uri}' contains an unsupported host."); + } + + /** + * Returns the number of attempts that have been done. + * + * @return int + */ + public function getAttempts(): int + { + return $this->attempts; + } + + /** + * Retrieves authorization token. + * + * @return array|false|string + */ + private function getEcsAuthToken() + { + if (!empty($path = getenv(self::ENV_AUTH_TOKEN_FILE))) { + $token = @file_get_contents($path); + if (false === $token) { + clearstatcache(true, dirname($path) . DIRECTORY_SEPARATOR . @readlink($path)); + clearstatcache(true, dirname($path) . DIRECTORY_SEPARATOR . dirname(@readlink($path))); + clearstatcache(true, $path); + } + + if (!is_readable($path)) { + throw new CredentialsException("Failed to read authorization token from '{$path}': no such file or directory."); + } + + $token = @file_get_contents($path); + + if (empty($token)) { + throw new CredentialsException("Invalid authorization token read from `$path`. Token file is empty!"); + } + + return $token; + } + + return getenv(self::ENV_AUTH_TOKEN); + } + + /** + * Provides headers for credential metadata request. + * + * @return array|array[]|string[] + */ + private function getHeadersForAuthToken() + { + $authToken = self::getEcsAuthToken(); + $headers = []; + + if (!empty($authToken)) + $headers = ['Authorization' => $authToken]; + + return $headers; + } + + /** @deprecated */ + public function setHeaderForAuthToken() + { + $authToken = self::getEcsAuthToken(); + $headers = []; + if (!empty($authToken)) + $headers = ['Authorization' => $authToken]; + + return $headers; + } + + /** + * Fetch container metadata URI from container environment variable. + * + * @return string Returns container metadata URI + */ + private function getEcsUri() + { + $credsUri = getenv(self::ENV_URI); + + if ($credsUri === false) { + $credsUri = $_SERVER[self::ENV_URI] ?? ''; + } + + if (empty($credsUri)){ + $credFullUri = getenv(self::ENV_FULL_URI); + if ($credFullUri === false){ + $credFullUri = $_SERVER[self::ENV_FULL_URI] ?? ''; + } + + if (!empty($credFullUri)) + return $credFullUri; + } + + return self::SERVER_URI . $credsUri; + } + + private function decodeResult($response) + { + $result = json_decode($response, true); + + if (!isset($result['AccessKeyId'])) { + throw new CredentialsException('Unexpected container metadata credentials value'); + } + return $result; + } + + /** + * Determines whether or not a given request URI is a valid + * container credential request URI. + * + * @param $uri + * + * @return bool + */ + private function isCompatibleUri($uri) + { + $parsed = parse_url($uri); + + if ($parsed['scheme'] !== 'https') { + $host = trim($parsed['host'], '[]'); + $ecsHost = parse_url(self::SERVER_URI)['host']; + $eksHost = self::EKS_SERVER_HOST_IPV4; + + if ($host !== $ecsHost + && $host !== $eksHost + && $host !== self::EKS_SERVER_HOST_IPV6 + && !CredentialsUtils::isLoopBackAddress(gethostbyname($host)) + ) { + return false; + } + } + + return true; + } +} diff --git a/3rdparty/aws/aws-sdk-php/src/Credentials/InstanceProfileProvider.php b/3rdparty/aws/aws-sdk-php/src/Credentials/InstanceProfileProvider.php new file mode 100644 index 00000000..c17a5641 --- /dev/null +++ b/3rdparty/aws/aws-sdk-php/src/Credentials/InstanceProfileProvider.php @@ -0,0 +1,468 @@ +timeout = (float) getenv(self::ENV_TIMEOUT) ?: ($config['timeout'] ?? self::DEFAULT_TIMEOUT); + $this->profile = $config['profile'] ?? null; + $this->retries = (int) getenv(self::ENV_RETRIES) ?: ($config['retries'] ?? self::DEFAULT_RETRIES); + $this->client = $config['client'] ?? \Aws\default_http_handler(); + $this->ec2MetadataV1Disabled = $config[self::CFG_EC2_METADATA_V1_DISABLED] ?? null; + $this->endpoint = $config[self::CFG_EC2_METADATA_SERVICE_ENDPOINT] ?? null; + if (!empty($this->endpoint) && !$this->isValidEndpoint($this->endpoint)) { + throw new \InvalidArgumentException('The provided URI "' . $this->endpoint . '" is invalid, or contains an unsupported host'); + } + + $this->endpointMode = $config[self::CFG_EC2_METADATA_SERVICE_ENDPOINT_MODE] ?? null; + $this->config = $config; + } + + /** + * Loads instance profile credentials. + * + * @return PromiseInterface + */ + public function __invoke($previousCredentials = null) + { + $this->attempts = 0; + return Promise\Coroutine::of(function () use ($previousCredentials) { + + // Retrieve token or switch out of secure mode + $token = null; + while ($this->secureMode && is_null($token)) { + try { + $token = (yield $this->request( + self::TOKEN_PATH, + 'PUT', + [ + 'x-aws-ec2-metadata-token-ttl-seconds' => self::DEFAULT_TOKEN_TTL_SECONDS + ] + )); + } catch (TransferException $e) { + if ($this->getExceptionStatusCode($e) === 500 + && $previousCredentials instanceof Credentials + ) { + goto generateCredentials; + } elseif ($this->shouldFallbackToIMDSv1() + && (!method_exists($e, 'getResponse') + || empty($e->getResponse()) + || !in_array( + $e->getResponse()->getStatusCode(), + [400, 500, 502, 503, 504] + )) + ) { + $this->secureMode = false; + } else { + $this->handleRetryableException( + $e, + [], + $this->createErrorMessage( + 'Error retrieving metadata token' + ) + ); + } + } + $this->attempts++; + } + + // Set token header only for secure mode + $headers = []; + if ($this->secureMode) { + $headers = [ + 'x-aws-ec2-metadata-token' => $token + ]; + } + + // Retrieve profile + while (!$this->profile) { + try { + $this->profile = (yield $this->request( + self::CRED_PATH, + 'GET', + $headers + )); + } catch (TransferException $e) { + // 401 indicates insecure flow not supported, switch to + // attempting secure mode for subsequent calls + if (!empty($this->getExceptionStatusCode($e)) + && $this->getExceptionStatusCode($e) === 401 + ) { + $this->secureMode = true; + } + $this->handleRetryableException( + $e, + [ 'blacklist' => [401, 403] ], + $this->createErrorMessage($e->getMessage()) + ); + } + + $this->attempts++; + } + + // Retrieve credentials + $result = null; + while ($result == null) { + try { + $json = (yield $this->request( + self::CRED_PATH . $this->profile, + 'GET', + $headers + )); + $result = $this->decodeResult($json); + } catch (InvalidJsonException $e) { + $this->handleRetryableException( + $e, + [ 'blacklist' => [401, 403] ], + $this->createErrorMessage( + 'Invalid JSON response, retries exhausted' + ) + ); + } catch (TransferException $e) { + // 401 indicates insecure flow not supported, switch to + // attempting secure mode for subsequent calls + if (($this->getExceptionStatusCode($e) === 500 + || strpos($e->getMessage(), "cURL error 28") !== false) + && $previousCredentials instanceof Credentials + ) { + goto generateCredentials; + } elseif (!empty($this->getExceptionStatusCode($e)) + && $this->getExceptionStatusCode($e) === 401 + ) { + $this->secureMode = true; + } + $this->handleRetryableException( + $e, + [ 'blacklist' => [401, 403] ], + $this->createErrorMessage($e->getMessage()) + ); + } + $this->attempts++; + } + generateCredentials: + + if (!isset($result)) { + $credentials = $previousCredentials; + } else { + $credentials = new Credentials( + $result['AccessKeyId'], + $result['SecretAccessKey'], + $result['Token'], + strtotime($result['Expiration']), + $result['AccountId'] ?? null, + CredentialSources::IMDS + ); + } + + if ($credentials->isExpired()) { + $credentials->extendExpiration(); + } + + yield $credentials; + }); + } + + /** + * @param string $url + * @param string $method + * @param array $headers + * @return PromiseInterface Returns a promise that is fulfilled with the + * body of the response as a string. + */ + private function request($url, $method = 'GET', $headers = []) + { + $disabled = getenv(self::ENV_DISABLE) ?: false; + if (strcasecmp($disabled, 'true') === 0) { + throw new CredentialsException( + $this->createErrorMessage('EC2 metadata service access disabled') + ); + } + + $fn = $this->client; + $request = new Request($method, $this->resolveEndpoint() . $url); + $userAgent = 'aws-sdk-php/' . Sdk::VERSION; + if (defined('HHVM_VERSION')) { + $userAgent .= ' HHVM/' . HHVM_VERSION; + } + $userAgent .= ' ' . \Aws\default_user_agent(); + $request = $request->withHeader('User-Agent', $userAgent); + foreach ($headers as $key => $value) { + $request = $request->withHeader($key, $value); + } + + return $fn($request, ['timeout' => $this->timeout]) + ->then(function (ResponseInterface $response) { + return (string) $response->getBody(); + })->otherwise(function (array $reason) { + $reason = $reason['exception']; + if ($reason instanceof TransferException) { + throw $reason; + } + $msg = $reason->getMessage(); + throw new CredentialsException( + $this->createErrorMessage($msg) + ); + }); + } + + private function handleRetryableException( + \Exception $e, + $retryOptions, + $message + ) { + $isRetryable = true; + if (!empty($status = $this->getExceptionStatusCode($e)) + && isset($retryOptions['blacklist']) + && in_array($status, $retryOptions['blacklist']) + ) { + $isRetryable = false; + } + if ($isRetryable && $this->attempts < $this->retries) { + sleep((int) pow(1.2, $this->attempts)); + } else { + throw new CredentialsException($message); + } + } + + private function getExceptionStatusCode(\Exception $e) + { + if (method_exists($e, 'getResponse') + && !empty($e->getResponse()) + ) { + return $e->getResponse()->getStatusCode(); + } + return null; + } + + private function createErrorMessage($previous) + { + return "Error retrieving credentials from the instance profile " + . "metadata service. ({$previous})"; + } + + private function decodeResult($response) + { + $result = json_decode($response, true); + + if (json_last_error() > 0) { + throw new InvalidJsonException(); + } + + if ($result['Code'] !== 'Success') { + throw new CredentialsException('Unexpected instance profile ' + . 'response code: ' . $result['Code']); + } + + return $result; + } + + /** + * This functions checks for whether we should fall back to IMDSv1 or not. + * If $ec2MetadataV1Disabled is null then we will try to resolve this value from + * the following sources: + * - From environment: "AWS_EC2_METADATA_V1_DISABLED". + * - From config file: aws_ec2_metadata_v1_disabled + * - Defaulted to false + * + * @return bool + */ + private function shouldFallbackToIMDSv1(): bool + { + $isImdsV1Disabled = \Aws\boolean_value($this->ec2MetadataV1Disabled) + ?? \Aws\boolean_value( + ConfigurationResolver::resolve( + self::CFG_EC2_METADATA_V1_DISABLED, + self::DEFAULT_AWS_EC2_METADATA_V1_DISABLED, + 'bool', + $this->config + ) + ) + ?? self::DEFAULT_AWS_EC2_METADATA_V1_DISABLED; + + return !$isImdsV1Disabled; + } + + /** + * Resolves the metadata service endpoint. If the endpoint is not provided + * or configured then, the default endpoint, based on the endpoint mode resolved, + * will be used. + * Example: if endpoint_mode is resolved to be IPv4 and the endpoint is not provided + * then, the endpoint to be used will be http://169.254.169.254. + * + * @return string + */ + private function resolveEndpoint(): string + { + $endpoint = $this->endpoint; + if (is_null($endpoint)) { + $endpoint = ConfigurationResolver::resolve( + self::CFG_EC2_METADATA_SERVICE_ENDPOINT, + $this->getDefaultEndpoint(), + 'string', + $this->config + ); + } + + if (!$this->isValidEndpoint($endpoint)) { + throw new CredentialsException('The provided URI "' . $endpoint . '" is invalid, or contains an unsupported host'); + } + + if (substr($endpoint, strlen($endpoint) - 1) !== '/') { + $endpoint = $endpoint . '/'; + } + + return $endpoint . 'latest/'; + } + + /** + * Resolves the default metadata service endpoint. + * If endpoint_mode is resolved as IPv4 then: + * - endpoint = http://169.254.169.254 + * If endpoint_mode is resolved as IPv6 then: + * - endpoint = http://[fd00:ec2::254] + * + * @return string + */ + private function getDefaultEndpoint(): string + { + $endpointMode = $this->resolveEndpointMode(); + switch ($endpointMode) { + case self::ENDPOINT_MODE_IPv4: + return self::DEFAULT_METADATA_SERVICE_IPv4_ENDPOINT; + case self::ENDPOINT_MODE_IPv6: + return self::DEFAULT_METADATA_SERVICE_IPv6_ENDPOINT; + } + + throw new CredentialsException("Invalid endpoint mode '$endpointMode' resolved"); + } + + /** + * Resolves the endpoint mode to be considered when resolving the default + * metadata service endpoint. + * + * @return string + */ + private function resolveEndpointMode(): string + { + $endpointMode = $this->endpointMode; + if (is_null($endpointMode)) { + $endpointMode = ConfigurationResolver::resolve( + self::CFG_EC2_METADATA_SERVICE_ENDPOINT_MODE, + self::ENDPOINT_MODE_IPv4, + 'string', + $this->config + ); + } + + return $endpointMode; + } + + /** + * This method checks for whether a provide URI is valid. + * @param string $uri this parameter is the uri to do the validation against to. + * + * @return string|null + */ + private function isValidEndpoint( + $uri + ): bool + { + // We make sure first the provided uri is a valid URL + $isValidURL = filter_var($uri, FILTER_VALIDATE_URL) !== false; + if (!$isValidURL) { + return false; + } + + // We make sure that if is a no secure host then it must be a loop back address. + $parsedUri = parse_url($uri); + if ($parsedUri['scheme'] !== 'https') { + $host = trim($parsedUri['host'], '[]'); + + return CredentialsUtils::isLoopBackAddress(gethostbyname($host)) + || in_array( + $uri, + [self::DEFAULT_METADATA_SERVICE_IPv4_ENDPOINT, self::DEFAULT_METADATA_SERVICE_IPv6_ENDPOINT] + ); + } + + return true; + } +} diff --git a/3rdparty/aws/aws-sdk-php/src/Crypto/AbstractCryptoClient.php b/3rdparty/aws/aws-sdk-php/src/Crypto/AbstractCryptoClient.php new file mode 100644 index 00000000..823467b7 --- /dev/null +++ b/3rdparty/aws/aws-sdk-php/src/Crypto/AbstractCryptoClient.php @@ -0,0 +1,121 @@ +stream = $cipherText; + $this->key = $key; + $this->cipherMethod = clone $cipherMethod; + } + + public function getOpenSslName() + { + return $this->cipherMethod->getOpenSslName(); + } + + public function getAesName() + { + return $this->cipherMethod->getAesName(); + } + + public function getCurrentIv() + { + return $this->cipherMethod->getCurrentIv(); + } + + public function getSize(): ?int + { + $plainTextSize = $this->stream->getSize(); + + if ($this->cipherMethod->requiresPadding()) { + // PKCS7 padding requires that between 1 and self::BLOCK_SIZE be + // added to the plaintext to make it an even number of blocks. The + // plaintext is between strlen($cipherText) - self::BLOCK_SIZE and + // strlen($cipherText) - 1 + return null; + } + + return $plainTextSize; + } + + public function isWritable(): bool + { + return false; + } + + public function read($length): string + { + if ($length > strlen($this->buffer)) { + $this->buffer .= $this->decryptBlock( + (int) ( + self::BLOCK_SIZE * ceil(($length - strlen($this->buffer)) / self::BLOCK_SIZE) + ) + ); + } + + $data = substr($this->buffer, 0, $length); + $this->buffer = substr($this->buffer, $length); + + return $data ? $data : ''; + } + + public function seek($offset, $whence = SEEK_SET): void + { + if ($offset === 0 && $whence === SEEK_SET) { + $this->buffer = ''; + $this->cipherMethod->seek(0, SEEK_SET); + $this->stream->seek(0, SEEK_SET); + } else { + throw new LogicException('AES encryption streams only support being' + . ' rewound, not arbitrary seeking.'); + } + } + + private function decryptBlock($length) + { + if ($this->stream->eof()) { + return ''; + } + + $cipherText = ''; + do { + $cipherText .= $this->stream->read((int) ($length - strlen($cipherText))); + } while (strlen($cipherText) < $length && !$this->stream->eof()); + + $options = OPENSSL_RAW_DATA; + if (!$this->stream->eof() + && $this->stream->getSize() !== $this->stream->tell() + ) { + $options |= OPENSSL_ZERO_PADDING; + } + + $plaintext = openssl_decrypt( + $cipherText, + $this->cipherMethod->getOpenSslName(), + $this->key, + $options, + $this->cipherMethod->getCurrentIv() + ); + + $this->cipherMethod->update($cipherText); + + return $plaintext; + } +} diff --git a/3rdparty/aws/aws-sdk-php/src/Crypto/AesEncryptingStream.php b/3rdparty/aws/aws-sdk-php/src/Crypto/AesEncryptingStream.php new file mode 100644 index 00000000..2cb5ab69 --- /dev/null +++ b/3rdparty/aws/aws-sdk-php/src/Crypto/AesEncryptingStream.php @@ -0,0 +1,151 @@ +stream = $plainText; + $this->key = $key; + $this->cipherMethod = clone $cipherMethod; + } + + public function getOpenSslName() + { + return $this->cipherMethod->getOpenSslName(); + } + + public function getAesName() + { + return $this->cipherMethod->getAesName(); + } + + public function getCurrentIv() + { + return $this->cipherMethod->getCurrentIv(); + } + + public function getSize(): ?int + { + $plainTextSize = $this->stream->getSize(); + + if ($this->cipherMethod->requiresPadding() && $plainTextSize !== null) { + // PKCS7 padding requires that between 1 and self::BLOCK_SIZE be + // added to the plaintext to make it an even number of blocks. + $padding = self::BLOCK_SIZE - $plainTextSize % self::BLOCK_SIZE; + return $plainTextSize + $padding; + } + + return $plainTextSize; + } + + public function isWritable(): bool + { + return false; + } + + public function read($length): string + { + if ($length > strlen($this->buffer)) { + $this->buffer .= $this->encryptBlock( + (int) + self::BLOCK_SIZE * ceil(($length - strlen($this->buffer)) / self::BLOCK_SIZE) + ); + } + + $data = substr($this->buffer, 0, $length); + $this->buffer = substr($this->buffer, $length); + + return $data ? $data : ''; + } + + public function seek($offset, $whence = SEEK_SET): void + { + if ($whence === SEEK_CUR) { + $offset = $this->tell() + $offset; + $whence = SEEK_SET; + } + + if ($whence === SEEK_SET) { + $this->buffer = ''; + $wholeBlockOffset + = (int) ($offset / self::BLOCK_SIZE) * self::BLOCK_SIZE; + $this->stream->seek($wholeBlockOffset); + $this->cipherMethod->seek($wholeBlockOffset); + $this->read($offset - $wholeBlockOffset); + } else { + throw new LogicException('Unrecognized whence.'); + } + } + + private function encryptBlock($length) + { + if ($this->stream->eof()) { + return ''; + } + + $plainText = ''; + do { + $plainText .= $this->stream->read((int) ($length - strlen($plainText))); + } while (strlen($plainText) < $length && !$this->stream->eof()); + + $options = OPENSSL_RAW_DATA; + if (!$this->stream->eof() + || $this->stream->getSize() !== $this->stream->tell() + ) { + $options |= OPENSSL_ZERO_PADDING; + } + + $cipherText = openssl_encrypt( + $plainText, + $this->cipherMethod->getOpenSslName(), + $this->key, + $options, + $this->cipherMethod->getCurrentIv() + ); + + $this->cipherMethod->update($cipherText); + + return $cipherText; + } +} diff --git a/3rdparty/aws/aws-sdk-php/src/Crypto/AesGcmDecryptingStream.php b/3rdparty/aws/aws-sdk-php/src/Crypto/AesGcmDecryptingStream.php new file mode 100644 index 00000000..d3a5d11d --- /dev/null +++ b/3rdparty/aws/aws-sdk-php/src/Crypto/AesGcmDecryptingStream.php @@ -0,0 +1,104 @@ +cipherText = $cipherText; + $this->key = $key; + $this->initializationVector = $initializationVector; + $this->tag = $tag; + $this->aad = $aad; + $this->tagLength = $tagLength; + $this->keySize = $keySize; + // unsetting the property forces the first access to go through + // __get(). + unset($this->stream); + } + + public function getOpenSslName() + { + return "aes-{$this->keySize}-gcm"; + } + + public function getAesName() + { + return 'AES/GCM/NoPadding'; + } + + public function getCurrentIv() + { + return $this->initializationVector; + } + + public function createStream() + { + + $result = \openssl_decrypt( + (string)$this->cipherText, + $this->getOpenSslName(), + $this->key, + OPENSSL_RAW_DATA, + $this->initializationVector, + $this->tag, + $this->aad + ); + if ($result === false) { + throw new CryptoException('The requested object could not be ' + . 'decrypted due to an invalid authentication tag.'); + } + return Psr7\Utils::streamFor($result); + + } + + public function isWritable(): bool + { + return false; + } +} diff --git a/3rdparty/aws/aws-sdk-php/src/Crypto/AesGcmEncryptingStream.php b/3rdparty/aws/aws-sdk-php/src/Crypto/AesGcmEncryptingStream.php new file mode 100644 index 00000000..ab2e5fad --- /dev/null +++ b/3rdparty/aws/aws-sdk-php/src/Crypto/AesGcmEncryptingStream.php @@ -0,0 +1,119 @@ +plaintext = $plaintext; + $this->key = $key; + $this->initializationVector = $initializationVector; + $this->aad = $aad; + $this->tagLength = $tagLength; + $this->keySize = $keySize; + // unsetting the property forces the first access to go through + // __get(). + unset($this->stream); + } + + public function getOpenSslName() + { + return "aes-{$this->keySize}-gcm"; + } + + /** + * Same as static method and retained for backwards compatibility + * + * @return string + */ + public function getAesName() + { + return self::getStaticAesName(); + } + + public function getCurrentIv() + { + return $this->initializationVector; + } + + public function createStream() + { + return Psr7\Utils::streamFor(\openssl_encrypt( + (string)$this->plaintext, + $this->getOpenSslName(), + $this->key, + OPENSSL_RAW_DATA, + $this->initializationVector, + $this->tag, + $this->aad, + $this->tagLength + )); + } + + /** + * @return string + */ + public function getTag() + { + return $this->tag; + } + + public function isWritable(): bool + { + return false; + } +} diff --git a/3rdparty/aws/aws-sdk-php/src/Crypto/AesStreamInterface.php b/3rdparty/aws/aws-sdk-php/src/Crypto/AesStreamInterface.php new file mode 100644 index 00000000..ce7b85d7 --- /dev/null +++ b/3rdparty/aws/aws-sdk-php/src/Crypto/AesStreamInterface.php @@ -0,0 +1,30 @@ +baseIv = $this->iv = $iv; + $this->keySize = $keySize; + + if (strlen($iv) !== openssl_cipher_iv_length($this->getOpenSslName())) { + throw new InvalidArgumentException('Invalid initialization vector'); + } + } + + public function getOpenSslName() + { + return "aes-{$this->keySize}-cbc"; + } + + public function getAesName() + { + return 'AES/CBC/PKCS5Padding'; + } + + public function getCurrentIv() + { + return $this->iv; + } + + public function requiresPadding() + { + return true; + } + + public function seek($offset, $whence = SEEK_SET) + { + if ($offset === 0 && $whence === SEEK_SET) { + $this->iv = $this->baseIv; + } else { + throw new LogicException('CBC initialization only support being' + . ' rewound, not arbitrary seeking.'); + } + } + + public function update($cipherTextBlock) + { + $this->iv = substr($cipherTextBlock, self::BLOCK_SIZE * -1); + } +} diff --git a/3rdparty/aws/aws-sdk-php/src/Crypto/Cipher/CipherBuilderTrait.php b/3rdparty/aws/aws-sdk-php/src/Crypto/Cipher/CipherBuilderTrait.php new file mode 100644 index 00000000..ed9feb9a --- /dev/null +++ b/3rdparty/aws/aws-sdk-php/src/Crypto/Cipher/CipherBuilderTrait.php @@ -0,0 +1,72 @@ +decryptCek( + base64_decode( + $envelope[MetadataEnvelope::CONTENT_KEY_V2_HEADER] + ), + json_decode( + $envelope[MetadataEnvelope::MATERIALS_DESCRIPTION_HEADER], + true + ) + ); + $cipherOptions['KeySize'] = strlen($cek) * 8; + $cipherOptions['Cipher'] = $this->getCipherFromAesName( + $envelope[MetadataEnvelope::CONTENT_CRYPTO_SCHEME_HEADER] + ); + + $decryptionStream = $this->getDecryptingStream( + $cipherText, + $cek, + $cipherOptions + ); + unset($cek); + + return $decryptionStream; + } + + private function getTagFromCiphertextStream( + StreamInterface $cipherText, + $tagLength + ) { + $cipherTextSize = $cipherText->getSize(); + if ($cipherTextSize == null || $cipherTextSize <= 0) { + throw new \RuntimeException('Cannot decrypt a stream of unknown' + . ' size.'); + } + return (string) new LimitStream( + $cipherText, + $tagLength, + $cipherTextSize - $tagLength + ); + } + + private function getStrippedCiphertextStream( + StreamInterface $cipherText, + $tagLength + ) { + $cipherTextSize = $cipherText->getSize(); + if ($cipherTextSize == null || $cipherTextSize <= 0) { + throw new \RuntimeException('Cannot decrypt a stream of unknown' + . ' size.'); + } + return new LimitStream( + $cipherText, + $cipherTextSize - $tagLength, + 0 + ); + } + + /** + * Generates a stream that wraps the cipher text with the proper cipher and + * uses the content encryption key (CEK) to decrypt the data when read. + * + * @param string $cipherText Plain-text data to be encrypted using the + * materials, algorithm, and data provided. + * @param string $cek A content encryption key for use by the stream for + * encrypting the plaintext data. + * @param array $cipherOptions Options for use in determining the cipher to + * be used for encrypting data. + * + * @return AesStreamInterface + * + * @internal + */ + protected function getDecryptingStream( + $cipherText, + $cek, + $cipherOptions + ) { + $cipherTextStream = Psr7\Utils::streamFor($cipherText); + switch ($cipherOptions['Cipher']) { + case 'gcm': + $cipherOptions['Tag'] = $this->getTagFromCiphertextStream( + $cipherTextStream, + $cipherOptions['TagLength'] + ); + + return new AesGcmDecryptingStream( + $this->getStrippedCiphertextStream( + $cipherTextStream, + $cipherOptions['TagLength'] + ), + $cek, + $cipherOptions['Iv'], + $cipherOptions['Tag'], + $cipherOptions['Aad'] = isset($cipherOptions['Aad']) + ? $cipherOptions['Aad'] + : '', + $cipherOptions['TagLength'] ?: null, + $cipherOptions['KeySize'] + ); + default: + $cipherMethod = $this->buildCipherMethod( + $cipherOptions['Cipher'], + $cipherOptions['Iv'], + $cipherOptions['KeySize'] + ); + return new AesDecryptingStream( + $cipherTextStream, + $cek, + $cipherMethod + ); + } + } +} diff --git a/3rdparty/aws/aws-sdk-php/src/Crypto/DecryptionTraitV2.php b/3rdparty/aws/aws-sdk-php/src/Crypto/DecryptionTraitV2.php new file mode 100644 index 00000000..ed63e0be --- /dev/null +++ b/3rdparty/aws/aws-sdk-php/src/Crypto/DecryptionTraitV2.php @@ -0,0 +1,249 @@ +decryptCek( + base64_decode( + $envelope[MetadataEnvelope::CONTENT_KEY_V2_HEADER] + ), + json_decode( + $envelope[MetadataEnvelope::MATERIALS_DESCRIPTION_HEADER], + true + ), + $options + ); + $options['@CipherOptions']['KeySize'] = strlen($cek) * 8; + $options['@CipherOptions']['Cipher'] = $this->getCipherFromAesName( + $envelope[MetadataEnvelope::CONTENT_CRYPTO_SCHEME_HEADER] + ); + + $this->validateOptionsAndEnvelope($options, $envelope); + + $decryptionStream = $this->getDecryptingStream( + $cipherText, + $cek, + $options['@CipherOptions'] + ); + unset($cek); + + return $decryptionStream; + } + + private function getTagFromCiphertextStream( + StreamInterface $cipherText, + $tagLength + ) { + $cipherTextSize = $cipherText->getSize(); + if ($cipherTextSize == null || $cipherTextSize <= 0) { + throw new \RuntimeException('Cannot decrypt a stream of unknown' + . ' size.'); + } + return (string) new LimitStream( + $cipherText, + $tagLength, + $cipherTextSize - $tagLength + ); + } + + private function getStrippedCiphertextStream( + StreamInterface $cipherText, + $tagLength + ) { + $cipherTextSize = $cipherText->getSize(); + if ($cipherTextSize == null || $cipherTextSize <= 0) { + throw new \RuntimeException('Cannot decrypt a stream of unknown' + . ' size.'); + } + return new LimitStream( + $cipherText, + $cipherTextSize - $tagLength, + 0 + ); + } + + private function validateOptionsAndEnvelope($options, $envelope) + { + $allowedCiphers = AbstractCryptoClientV2::$supportedCiphers; + $allowedKeywraps = AbstractCryptoClientV2::$supportedKeyWraps; + if ($options['@SecurityProfile'] == 'V2_AND_LEGACY') { + $allowedCiphers = array_unique(array_merge( + $allowedCiphers, + AbstractCryptoClient::$supportedCiphers + )); + $allowedKeywraps = array_unique(array_merge( + $allowedKeywraps, + AbstractCryptoClient::$supportedKeyWraps + )); + } + + $v1SchemaException = new CryptoException("The requested object is encrypted" + . " with V1 encryption schemas that have been disabled by" + . " client configuration @SecurityProfile=V2. Retry with" + . " V2_AND_LEGACY enabled or reencrypt the object."); + + if (!in_array($options['@CipherOptions']['Cipher'], $allowedCiphers)) { + if (in_array($options['@CipherOptions']['Cipher'], AbstractCryptoClient::$supportedCiphers)) { + throw $v1SchemaException; + } + throw new CryptoException("The requested object is encrypted with" + . " the cipher '{$options['@CipherOptions']['Cipher']}', which is not" + . " supported for decryption with the selected security profile." + . " This profile allows decryption with: " + . implode(", ", $allowedCiphers)); + } + if (!in_array( + $envelope[MetadataEnvelope::KEY_WRAP_ALGORITHM_HEADER], + $allowedKeywraps + )) { + if (in_array( + $envelope[MetadataEnvelope::KEY_WRAP_ALGORITHM_HEADER], + AbstractCryptoClient::$supportedKeyWraps) + ) { + throw $v1SchemaException; + } + throw new CryptoException("The requested object is encrypted with" + . " the keywrap schema '{$envelope[MetadataEnvelope::KEY_WRAP_ALGORITHM_HEADER]}'," + . " which is not supported for decryption with the current security" + . " profile."); + } + + $matdesc = json_decode( + $envelope[MetadataEnvelope::MATERIALS_DESCRIPTION_HEADER], + true + ); + if (isset($matdesc['aws:x-amz-cek-alg']) + && $envelope[MetadataEnvelope::CONTENT_CRYPTO_SCHEME_HEADER] !== + $matdesc['aws:x-amz-cek-alg'] + ) { + throw new CryptoException("There is a mismatch in specified content" + . " encryption algrithm between the materials description value" + . " and the metadata envelope value: {$matdesc['aws:x-amz-cek-alg']}" + . " vs. {$envelope[MetadataEnvelope::CONTENT_CRYPTO_SCHEME_HEADER]}."); + } + } + + /** + * Generates a stream that wraps the cipher text with the proper cipher and + * uses the content encryption key (CEK) to decrypt the data when read. + * + * @param string $cipherText Plain-text data to be encrypted using the + * materials, algorithm, and data provided. + * @param string $cek A content encryption key for use by the stream for + * encrypting the plaintext data. + * @param array $cipherOptions Options for use in determining the cipher to + * be used for encrypting data. + * + * @return AesStreamInterface + * + * @internal + */ + protected function getDecryptingStream( + $cipherText, + $cek, + $cipherOptions + ) { + $cipherTextStream = Psr7\Utils::streamFor($cipherText); + switch ($cipherOptions['Cipher']) { + case 'gcm': + $cipherOptions['Tag'] = $this->getTagFromCiphertextStream( + $cipherTextStream, + $cipherOptions['TagLength'] + ); + + return new AesGcmDecryptingStream( + $this->getStrippedCiphertextStream( + $cipherTextStream, + $cipherOptions['TagLength'] + ), + $cek, + $cipherOptions['Iv'], + $cipherOptions['Tag'], + $cipherOptions['Aad'] = isset($cipherOptions['Aad']) + ? $cipherOptions['Aad'] + : '', + $cipherOptions['TagLength'] ?: null, + $cipherOptions['KeySize'] + ); + default: + $cipherMethod = $this->buildCipherMethod( + $cipherOptions['Cipher'], + $cipherOptions['Iv'], + $cipherOptions['KeySize'] + ); + return new AesDecryptingStream( + $cipherTextStream, + $cek, + $cipherMethod + ); + } + } +} diff --git a/3rdparty/aws/aws-sdk-php/src/Crypto/EncryptionTrait.php b/3rdparty/aws/aws-sdk-php/src/Crypto/EncryptionTrait.php new file mode 100644 index 00000000..fcddba69 --- /dev/null +++ b/3rdparty/aws/aws-sdk-php/src/Crypto/EncryptionTrait.php @@ -0,0 +1,192 @@ + true, + 'KeySize' => true, + 'Aad' => true, + ]; + + /** + * Dependency to generate a CipherMethod from a set of inputs for loading + * in to an AesEncryptingStream. + * + * @param string $cipherName Name of the cipher to generate for encrypting. + * @param string $iv Base Initialization Vector for the cipher. + * @param int $keySize Size of the encryption key, in bits, that will be + * used. + * + * @return Cipher\CipherMethod + * + * @internal + */ + abstract protected function buildCipherMethod($cipherName, $iv, $keySize); + + /** + * Builds an AesStreamInterface and populates encryption metadata into the + * supplied envelope. + * + * @param Stream $plaintext Plain-text data to be encrypted using the + * materials, algorithm, and data provided. + * @param array $cipherOptions Options for use in determining the cipher to + * be used for encrypting data. + * @param MaterialsProvider $provider A provider to supply and encrypt + * materials used in encryption. + * @param MetadataEnvelope $envelope A storage envelope for encryption + * metadata to be added to. + * + * @return AesStreamInterface + * + * @throws \InvalidArgumentException Thrown when a value in $cipherOptions + * is not valid. + * + * @internal + */ + public function encrypt( + Stream $plaintext, + array $cipherOptions, + MaterialsProvider $provider, + MetadataEnvelope $envelope + ) { + $materialsDescription = $provider->getMaterialsDescription(); + + $cipherOptions = array_intersect_key( + $cipherOptions, + self::$allowedOptions + ); + + if (empty($cipherOptions['Cipher'])) { + throw new \InvalidArgumentException('An encryption cipher must be' + . ' specified in the "cipher_options".'); + } + + if (!self::isSupportedCipher($cipherOptions['Cipher'])) { + throw new \InvalidArgumentException('The cipher requested is not' + . ' supported by the SDK.'); + } + + if (empty($cipherOptions['KeySize'])) { + $cipherOptions['KeySize'] = 256; + } + if (!is_int($cipherOptions['KeySize'])) { + throw new \InvalidArgumentException('The cipher "KeySize" must be' + . ' an integer.'); + } + + if (!MaterialsProvider::isSupportedKeySize( + $cipherOptions['KeySize'] + )) { + throw new \InvalidArgumentException('The cipher "KeySize" requested' + . ' is not supported by AES (128, 192, or 256).'); + } + + $cipherOptions['Iv'] = $provider->generateIv( + $this->getCipherOpenSslName( + $cipherOptions['Cipher'], + $cipherOptions['KeySize'] + ) + ); + + $cek = $provider->generateCek($cipherOptions['KeySize']); + + list($encryptingStream, $aesName) = $this->getEncryptingStream( + $plaintext, + $cek, + $cipherOptions + ); + + // Populate envelope data + $envelope[MetadataEnvelope::CONTENT_KEY_V2_HEADER] = + $provider->encryptCek( + $cek, + $materialsDescription + ); + unset($cek); + + $envelope[MetadataEnvelope::IV_HEADER] = + base64_encode($cipherOptions['Iv']); + $envelope[MetadataEnvelope::KEY_WRAP_ALGORITHM_HEADER] = + $provider->getWrapAlgorithmName(); + $envelope[MetadataEnvelope::CONTENT_CRYPTO_SCHEME_HEADER] = $aesName; + $envelope[MetadataEnvelope::UNENCRYPTED_CONTENT_LENGTH_HEADER] = + strlen($plaintext); + $envelope[MetadataEnvelope::MATERIALS_DESCRIPTION_HEADER] = + json_encode($materialsDescription); + if (!empty($cipherOptions['Tag'])) { + $envelope[MetadataEnvelope::CRYPTO_TAG_LENGTH_HEADER] = + strlen($cipherOptions['Tag']) * 8; + } + + return $encryptingStream; + } + + /** + * Generates a stream that wraps the plaintext with the proper cipher and + * uses the content encryption key (CEK) to encrypt the data when read. + * + * @param Stream $plaintext Plain-text data to be encrypted using the + * materials, algorithm, and data provided. + * @param string $cek A content encryption key for use by the stream for + * encrypting the plaintext data. + * @param array $cipherOptions Options for use in determining the cipher to + * be used for encrypting data. + * + * @return array returns an array with two elements as follows: [string, AesStreamInterface] + * + * @internal + */ + protected function getEncryptingStream( + Stream $plaintext, + $cek, + &$cipherOptions + ) { + switch ($cipherOptions['Cipher']) { + case 'gcm': + $cipherOptions['TagLength'] = 16; + + $cipherTextStream = new AesGcmEncryptingStream( + $plaintext, + $cek, + $cipherOptions['Iv'], + $cipherOptions['Aad'] = isset($cipherOptions['Aad']) + ? $cipherOptions['Aad'] + : '', + $cipherOptions['TagLength'], + $cipherOptions['KeySize'] + ); + + if (!empty($cipherOptions['Aad'])) { + trigger_error("'Aad' has been supplied for content encryption" + . " with " . $cipherTextStream->getAesName() . ". The" + . " PHP SDK encryption client can decrypt an object" + . " encrypted in this way, but other AWS SDKs may not be" + . " able to.", E_USER_WARNING); + } + + $appendStream = new AppendStream([ + $cipherTextStream->createStream() + ]); + $cipherOptions['Tag'] = $cipherTextStream->getTag(); + $appendStream->addStream(Psr7\Utils::streamFor($cipherOptions['Tag'])); + return [$appendStream, $cipherTextStream->getAesName()]; + default: + $cipherMethod = $this->buildCipherMethod( + $cipherOptions['Cipher'], + $cipherOptions['Iv'], + $cipherOptions['KeySize'] + ); + $cipherTextStream = new AesEncryptingStream( + $plaintext, + $cek, + $cipherMethod + ); + return [$cipherTextStream, $cipherTextStream->getAesName()]; + } + } +} diff --git a/3rdparty/aws/aws-sdk-php/src/Crypto/EncryptionTraitV2.php b/3rdparty/aws/aws-sdk-php/src/Crypto/EncryptionTraitV2.php new file mode 100644 index 00000000..2bce1833 --- /dev/null +++ b/3rdparty/aws/aws-sdk-php/src/Crypto/EncryptionTraitV2.php @@ -0,0 +1,196 @@ + true, + 'KeySize' => true, + 'Aad' => true, + ]; + + private static $encryptClasses = [ + 'gcm' => AesGcmEncryptingStream::class + ]; + + /** + * Dependency to generate a CipherMethod from a set of inputs for loading + * in to an AesEncryptingStream. + * + * @param string $cipherName Name of the cipher to generate for encrypting. + * @param string $iv Base Initialization Vector for the cipher. + * @param int $keySize Size of the encryption key, in bits, that will be + * used. + * + * @return Cipher\CipherMethod + * + * @internal + */ + abstract protected function buildCipherMethod($cipherName, $iv, $keySize); + + /** + * Builds an AesStreamInterface and populates encryption metadata into the + * supplied envelope. + * + * @param Stream $plaintext Plain-text data to be encrypted using the + * materials, algorithm, and data provided. + * @param array $options Options for use in encryption, including cipher + * options, and encryption context. + * @param MaterialsProviderV2 $provider A provider to supply and encrypt + * materials used in encryption. + * @param MetadataEnvelope $envelope A storage envelope for encryption + * metadata to be added to. + * + * @return StreamInterface + * + * @throws \InvalidArgumentException Thrown when a value in $options['@CipherOptions'] + * is not valid. + *s + * @internal + */ + public function encrypt( + Stream $plaintext, + array $options, + MaterialsProviderV2 $provider, + MetadataEnvelope $envelope + ) { + $options = array_change_key_case($options); + $cipherOptions = array_intersect_key( + $options['@cipheroptions'], + self::$allowedOptions + ); + + if (empty($cipherOptions['Cipher'])) { + throw new \InvalidArgumentException('An encryption cipher must be' + . ' specified in @CipherOptions["Cipher"].'); + } + + $cipherOptions['Cipher'] = strtolower($cipherOptions['Cipher']); + + if (!self::isSupportedCipher($cipherOptions['Cipher'])) { + throw new \InvalidArgumentException('The cipher requested is not' + . ' supported by the SDK.'); + } + + if (empty($cipherOptions['KeySize'])) { + $cipherOptions['KeySize'] = 256; + } + if (!is_int($cipherOptions['KeySize'])) { + throw new \InvalidArgumentException('The cipher "KeySize" must be' + . ' an integer.'); + } + + if (!MaterialsProviderV2::isSupportedKeySize( + $cipherOptions['KeySize'] + )) { + throw new \InvalidArgumentException('The cipher "KeySize" requested' + . ' is not supported by AES (128 or 256).'); + } + + $cipherOptions['Iv'] = $provider->generateIv( + $this->getCipherOpenSslName( + $cipherOptions['Cipher'], + $cipherOptions['KeySize'] + ) + ); + + $encryptClass = self::$encryptClasses[$cipherOptions['Cipher']]; + $aesName = $encryptClass::getStaticAesName(); + $materialsDescription = ['aws:x-amz-cek-alg' => $aesName]; + + $keys = $provider->generateCek( + $cipherOptions['KeySize'], + $materialsDescription, + $options + ); + + // Some providers modify materials description based on options + if (isset($keys['UpdatedContext'])) { + $materialsDescription = $keys['UpdatedContext']; + } + + $encryptingStream = $this->getEncryptingStream( + $plaintext, + $keys['Plaintext'], + $cipherOptions + ); + + // Populate envelope data + $envelope[MetadataEnvelope::CONTENT_KEY_V2_HEADER] = $keys['Ciphertext']; + unset($keys); + + $envelope[MetadataEnvelope::IV_HEADER] = + base64_encode($cipherOptions['Iv']); + $envelope[MetadataEnvelope::KEY_WRAP_ALGORITHM_HEADER] = + $provider->getWrapAlgorithmName(); + $envelope[MetadataEnvelope::CONTENT_CRYPTO_SCHEME_HEADER] = $aesName; + $envelope[MetadataEnvelope::UNENCRYPTED_CONTENT_LENGTH_HEADER] = + strlen($plaintext); + $envelope[MetadataEnvelope::MATERIALS_DESCRIPTION_HEADER] = + json_encode($materialsDescription); + if (!empty($cipherOptions['Tag'])) { + $envelope[MetadataEnvelope::CRYPTO_TAG_LENGTH_HEADER] = + strlen($cipherOptions['Tag']) * 8; + } + + return $encryptingStream; + } + + /** + * Generates a stream that wraps the plaintext with the proper cipher and + * uses the content encryption key (CEK) to encrypt the data when read. + * + * @param Stream $plaintext Plain-text data to be encrypted using the + * materials, algorithm, and data provided. + * @param string $cek A content encryption key for use by the stream for + * encrypting the plaintext data. + * @param array $cipherOptions Options for use in determining the cipher to + * be used for encrypting data. + * + * @return array returns an array with two elements as follows: [string, AesStreamInterface] + * + * @internal + */ + protected function getEncryptingStream( + Stream $plaintext, + $cek, + &$cipherOptions + ) { + switch ($cipherOptions['Cipher']) { + // Only 'gcm' is supported for encryption currently + case 'gcm': + $cipherOptions['TagLength'] = 16; + $encryptClass = self::$encryptClasses['gcm']; + $cipherTextStream = new $encryptClass( + $plaintext, + $cek, + $cipherOptions['Iv'], + $cipherOptions['Aad'] = isset($cipherOptions['Aad']) + ? $cipherOptions['Aad'] + : '', + $cipherOptions['TagLength'], + $cipherOptions['KeySize'] + ); + + if (!empty($cipherOptions['Aad'])) { + trigger_error("'Aad' has been supplied for content encryption" + . " with " . $cipherTextStream->getAesName() . ". The" + . " PHP SDK encryption client can decrypt an object" + . " encrypted in this way, but other AWS SDKs may not be" + . " able to.", E_USER_WARNING); + } + + $appendStream = new AppendStream([ + $cipherTextStream->createStream() + ]); + $cipherOptions['Tag'] = $cipherTextStream->getTag(); + $appendStream->addStream(Psr7\Utils::streamFor($cipherOptions['Tag'])); + return $appendStream; + } + } +} diff --git a/3rdparty/aws/aws-sdk-php/src/Crypto/KmsMaterialsProvider.php b/3rdparty/aws/aws-sdk-php/src/Crypto/KmsMaterialsProvider.php new file mode 100644 index 00000000..fc75138b --- /dev/null +++ b/3rdparty/aws/aws-sdk-php/src/Crypto/KmsMaterialsProvider.php @@ -0,0 +1,121 @@ +kmsClient = $kmsClient; + $this->kmsKeyId = $kmsKeyId; + } + + public function fromDecryptionEnvelope(MetadataEnvelope $envelope) + { + if (empty($envelope[MetadataEnvelope::MATERIALS_DESCRIPTION_HEADER])) { + throw new \RuntimeException('Not able to detect the materials description.'); + } + + $materialsDescription = json_decode( + $envelope[MetadataEnvelope::MATERIALS_DESCRIPTION_HEADER], + true + ); + + if (empty($materialsDescription['kms_cmk_id']) + && empty($materialsDescription['aws:x-amz-cek-alg'])) { + throw new \RuntimeException('Not able to detect kms_cmk_id (legacy' + . ' implementation) or aws:x-amz-cek-alg (current implementation)' + . ' from kms materials description.'); + } + + return new self( + $this->kmsClient, + isset($materialsDescription['kms_cmk_id']) + ? $materialsDescription['kms_cmk_id'] + : null + ); + } + + /** + * The KMS key id for use in matching this Provider to its keys, + * consistently with other SDKs as 'kms_cmk_id'. + * + * @return array + */ + public function getMaterialsDescription() + { + return ['kms_cmk_id' => $this->kmsKeyId]; + } + + public function getWrapAlgorithmName() + { + return self::WRAP_ALGORITHM_NAME; + } + + /** + * Takes a content encryption key (CEK) and description to return an encrypted + * key by using KMS' Encrypt API. + * + * @param string $unencryptedCek Key for use in encrypting other data + * that itself needs to be encrypted by the + * Provider. + * @param string $materialDescription Material Description for use in + * encrypting the $cek. + * + * @return string + */ + public function encryptCek($unencryptedCek, $materialDescription) + { + $encryptedDataKey = $this->kmsClient->encrypt([ + 'Plaintext' => $unencryptedCek, + 'KeyId' => $this->kmsKeyId, + 'EncryptionContext' => $materialDescription + ]); + return base64_encode($encryptedDataKey['CiphertextBlob']); + } + + /** + * Takes an encrypted content encryption key (CEK) and material description + * for use decrypting the key by using KMS' Decrypt API. + * + * @param string $encryptedCek Encrypted key to be decrypted by the Provider + * for use decrypting other data. + * @param string $materialDescription Material Description for use in + * encrypting the $cek. + * + * @return string + */ + public function decryptCek($encryptedCek, $materialDescription) + { + $result = $this->kmsClient->decrypt([ + 'CiphertextBlob' => $encryptedCek, + 'EncryptionContext' => $materialDescription + ]); + + return $result['Plaintext']; + } +} diff --git a/3rdparty/aws/aws-sdk-php/src/Crypto/KmsMaterialsProviderV2.php b/3rdparty/aws/aws-sdk-php/src/Crypto/KmsMaterialsProviderV2.php new file mode 100644 index 00000000..e7da8b92 --- /dev/null +++ b/3rdparty/aws/aws-sdk-php/src/Crypto/KmsMaterialsProviderV2.php @@ -0,0 +1,100 @@ +kmsClient = $kmsClient; + $this->kmsKeyId = $kmsKeyId; + } + + /** + * @inheritDoc + */ + public function getWrapAlgorithmName() + { + return self::WRAP_ALGORITHM_NAME; + } + + /** + * @inheritDoc + */ + public function decryptCek($encryptedCek, $materialDescription, $options) + { + $params = [ + 'CiphertextBlob' => $encryptedCek, + 'EncryptionContext' => $materialDescription + ]; + if (empty($options['@KmsAllowDecryptWithAnyCmk'])) { + if (empty($this->kmsKeyId)) { + throw new CryptoException('KMS CMK ID was not specified and the' + . ' operation is not opted-in to attempting to use any valid' + . ' CMK it discovers. Please specify a CMK ID, or explicitly' + . ' enable attempts to use any valid KMS CMK with the' + . ' @KmsAllowDecryptWithAnyCmk option.'); + } + $params['KeyId'] = $this->kmsKeyId; + } + + $result = $this->kmsClient->decrypt($params); + return $result['Plaintext']; + } + + /** + * @inheritDoc + */ + public function generateCek($keySize, $context, $options) + { + if (empty($this->kmsKeyId)) { + throw new CryptoException('A KMS key id is required for encryption' + . ' with KMS keywrap. Use a KmsMaterialsProviderV2 that has been' + . ' instantiated with a KMS key id.'); + } + $options = array_change_key_case($options); + if (!isset($options['@kmsencryptioncontext']) + || !is_array($options['@kmsencryptioncontext']) + ) { + throw new CryptoException("'@KmsEncryptionContext' is a" + . " required argument when using KmsMaterialsProviderV2, and" + . " must be an associative array (or empty array)."); + } + if (isset($options['@kmsencryptioncontext']['aws:x-amz-cek-alg'])) { + throw new CryptoException("Conflict in reserved @KmsEncryptionContext" + . " key aws:x-amz-cek-alg. This value is reserved for the S3" + . " Encryption Client and cannot be set by the user."); + } + $context = array_merge($options['@kmsencryptioncontext'], $context); + $result = $this->kmsClient->generateDataKey([ + 'KeyId' => $this->kmsKeyId, + 'KeySpec' => "AES_{$keySize}", + 'EncryptionContext' => $context + ]); + return [ + 'Plaintext' => $result['Plaintext'], + 'Ciphertext' => base64_encode($result['CiphertextBlob']), + 'UpdatedContext' => $context + ]; + } +} diff --git a/3rdparty/aws/aws-sdk-php/src/Crypto/MaterialsProvider.php b/3rdparty/aws/aws-sdk-php/src/Crypto/MaterialsProvider.php new file mode 100644 index 00000000..1c6941c2 --- /dev/null +++ b/3rdparty/aws/aws-sdk-php/src/Crypto/MaterialsProvider.php @@ -0,0 +1,105 @@ + true, + 192 => true, + 256 => true, + ]; + + /** + * Returns if the requested size is supported by AES. + * + * @param int $keySize Size of the requested key in bits. + * + * @return bool + */ + public static function isSupportedKeySize($keySize) + { + return isset(self::$supportedKeySizes[$keySize]); + } + + /** + * Performs further initialization of the MaterialsProvider based on the + * data inside the MetadataEnvelope. + * + * @param MetadataEnvelope $envelope A storage envelope for encryption + * metadata to be read from. + * + * @return MaterialsProvider + * + * @throws \RuntimeException Thrown when there is an empty or improperly + * formed materials description in the envelope. + * + * @internal + */ + abstract public function fromDecryptionEnvelope(MetadataEnvelope $envelope); + + /** + * Returns the material description for this Provider so it can be verified + * by encryption mechanisms. + * + * @return string + */ + abstract public function getMaterialsDescription(); + + /** + * Returns the wrap algorithm name for this Provider. + * + * @return string + */ + abstract public function getWrapAlgorithmName(); + + /** + * Takes a content encryption key (CEK) and description to return an + * encrypted key according to the Provider's specifications. + * + * @param string $unencryptedCek Key for use in encrypting other data + * that itself needs to be encrypted by the + * Provider. + * @param string $materialDescription Material Description for use in + * encrypting the $cek. + * + * @return string + */ + abstract public function encryptCek($unencryptedCek, $materialDescription); + + /** + * Takes an encrypted content encryption key (CEK) and material description + * for use decrypting the key according to the Provider's specifications. + * + * @param string $encryptedCek Encrypted key to be decrypted by the Provider + * for use decrypting other data. + * @param string $materialDescription Material Description for use in + * encrypting the $cek. + * + * @return string + */ + abstract public function decryptCek($encryptedCek, $materialDescription); + + /** + * @param string $keySize Length of a cipher key in bits for generating a + * random content encryption key (CEK). + * + * @return string + */ + public function generateCek($keySize) + { + return openssl_random_pseudo_bytes($keySize / 8); + } + + /** + * @param string $openSslName Cipher OpenSSL name to use for generating + * an initialization vector. + * + * @return string + */ + public function generateIv($openSslName) + { + return openssl_random_pseudo_bytes( + openssl_cipher_iv_length($openSslName) + ); + } +} diff --git a/3rdparty/aws/aws-sdk-php/src/Crypto/MaterialsProviderInterface.php b/3rdparty/aws/aws-sdk-php/src/Crypto/MaterialsProviderInterface.php new file mode 100644 index 00000000..a22016d6 --- /dev/null +++ b/3rdparty/aws/aws-sdk-php/src/Crypto/MaterialsProviderInterface.php @@ -0,0 +1,61 @@ + true, + 256 => true, + ]; + + /** + * Returns if the requested size is supported by AES. + * + * @param int $keySize Size of the requested key in bits. + * + * @return bool + */ + public static function isSupportedKeySize($keySize) + { + return isset(self::$supportedKeySizes[$keySize]); + } + + /** + * Returns the wrap algorithm name for this Provider. + * + * @return string + */ + abstract public function getWrapAlgorithmName(); + + /** + * Takes an encrypted content encryption key (CEK) and material description + * for use decrypting the key according to the Provider's specifications. + * + * @param string $encryptedCek Encrypted key to be decrypted by the Provider + * for use decrypting other data. + * @param string $materialDescription Material Description for use in + * decrypting the CEK. + * @param string $options Options for use in decrypting the CEK. + * + * @return string + */ + abstract public function decryptCek($encryptedCek, $materialDescription, $options); + + /** + * @param string $keySize Length of a cipher key in bits for generating a + * random content encryption key (CEK). + * @param array $context Context map needed for key encryption + * @param array $options Additional options to be used in CEK generation + * + * @return array + */ + abstract public function generateCek($keySize, $context, $options); + + /** + * @param string $openSslName Cipher OpenSSL name to use for generating + * an initialization vector. + * + * @return string + */ + public function generateIv($openSslName) + { + return openssl_random_pseudo_bytes( + openssl_cipher_iv_length($openSslName) + ); + } +} diff --git a/3rdparty/aws/aws-sdk-php/src/Crypto/MetadataEnvelope.php b/3rdparty/aws/aws-sdk-php/src/Crypto/MetadataEnvelope.php new file mode 100644 index 00000000..5a7c6920 --- /dev/null +++ b/3rdparty/aws/aws-sdk-php/src/Crypto/MetadataEnvelope.php @@ -0,0 +1,61 @@ +getConstants()) as $constant) { + self::$constants[$constant] = true; + } + } + + return array_keys(self::$constants); + } + + /** + * @return void + */ + #[\ReturnTypeWillChange] + public function offsetSet($name, $value) + { + $constants = self::getConstantValues(); + if (is_null($name) || !in_array($name, $constants)) { + throw new InvalidArgumentException('MetadataEnvelope fields must' + . ' must match a predefined offset; use the header constants.'); + } + + $this->data[$name] = $value; + } + + #[\ReturnTypeWillChange] + public function jsonSerialize() + { + return $this->data; + } +} diff --git a/3rdparty/aws/aws-sdk-php/src/Crypto/MetadataStrategyInterface.php b/3rdparty/aws/aws-sdk-php/src/Crypto/MetadataStrategyInterface.php new file mode 100644 index 00000000..5270c7e8 --- /dev/null +++ b/3rdparty/aws/aws-sdk-php/src/Crypto/MetadataStrategyInterface.php @@ -0,0 +1,30 @@ +validModes)) { + throw new \InvalidArgumentException("'{$mode}' is not a valid mode." + . " The mode has to be 'legacy', 'standard', 'cross-region', 'in-region'," + . " 'mobile', or 'auto'."); + } + + $this->mode = $mode; + if ($this->mode == 'legacy') { + return; + } + + $data = \Aws\load_compiled_json( + __DIR__ . '/../data/sdk-default-configuration.json' + ); + + $this->retryMode = $data['base']['retryMode']; + $this->stsRegionalEndpoints = $data['base']['stsRegionalEndpoints']; + $this->s3UsEast1RegionalEndpoints = $data['base']['s3UsEast1RegionalEndpoints']; + $this->connectTimeoutInMillis = $data['base']['connectTimeoutInMillis']; + + if (isset($data['modes'][$mode])) { + $modeData = $data['modes'][$mode]; + foreach ($modeData as $settingName => $settingValue) { + if (isset($this->$settingName)) { + if (isset($settingValue['override'])) { + $this->$settingName = $settingValue['override']; + } else if (isset($settingValue['multiply'])) { + $this->$settingName *= $settingValue['multiply']; + } else if (isset($settingValue['add'])) { + $this->$settingName += $settingValue['add']; + } + } else { + if (isset($settingValue['override'])) { + if (property_exists($this, $settingName)) { + $this->$settingName = $settingValue['override']; + } + } + } + } + } + } + + /** + * {@inheritdoc} + */ + public function getMode() + { + return $this->mode; + } + + /** + * {@inheritdoc} + */ + public function getRetryMode() + { + return $this->retryMode; + } + + /** + * {@inheritdoc} + */ + public function getStsRegionalEndpoints() + { + return $this->stsRegionalEndpoints; + } + + /** + * {@inheritdoc} + */ + public function getS3UsEast1RegionalEndpoints() + { + return $this->s3UsEast1RegionalEndpoints; + } + + /** + * {@inheritdoc} + */ + public function getConnectTimeoutInMillis() + { + return $this->connectTimeoutInMillis; + } + + /** + * {@inheritdoc} + */ + public function getHttpRequestTimeoutInMillis() + { + return $this->httpRequestTimeoutInMillis; + } + + /** + * {@inheritdoc} + */ + public function toArray() + { + return [ + 'mode' => $this->getMode(), + 'retry_mode' => $this->getRetryMode(), + 'sts_regional_endpoints' => $this->getStsRegionalEndpoints(), + 's3_us_east_1_regional_endpoint' => $this->getS3UsEast1RegionalEndpoints(), + 'connect_timeout_in_milliseconds' => $this->getConnectTimeoutInMillis(), + 'http_request_timeout_in_milliseconds' => $this->getHttpRequestTimeoutInMillis(), + ]; + } + +} diff --git a/3rdparty/aws/aws-sdk-php/src/DefaultsMode/ConfigurationInterface.php b/3rdparty/aws/aws-sdk-php/src/DefaultsMode/ConfigurationInterface.php new file mode 100644 index 00000000..34c5a63f --- /dev/null +++ b/3rdparty/aws/aws-sdk-php/src/DefaultsMode/ConfigurationInterface.php @@ -0,0 +1,51 @@ + + * use Aws\Sts\RegionalEndpoints\ConfigurationProvider; + * $provider = ConfigurationProvider::defaultProvider(); + * // Returns a ConfigurationInterface or throws. + * $config = $provider()->wait(); + * + * + * Configuration providers can be composed to create configuration using + * conditional logic that can create different configurations in different + * environments. You can compose multiple providers into a single provider using + * {@see \Aws\DefaultsMode\ConfigurationProvider::chain}. This function + * accepts providers as variadic arguments and returns a new function that will + * invoke each provider until a successful configuration is returned. + * + * + * // First try an INI file at this location. + * $a = ConfigurationProvider::ini(null, '/path/to/file.ini'); + * // Then try an INI file at this location. + * $b = ConfigurationProvider::ini(null, '/path/to/other-file.ini'); + * // Then try loading from environment variables. + * $c = ConfigurationProvider::env(); + * // Combine the three providers together. + * $composed = ConfigurationProvider::chain($a, $b, $c); + * // Returns a promise that is fulfilled with a configuration or throws. + * $promise = $composed(); + * // Wait on the configuration to resolve. + * $config = $promise->wait(); + * + */ +class ConfigurationProvider extends AbstractConfigurationProvider + implements ConfigurationProviderInterface +{ + const DEFAULT_MODE = 'legacy'; + const ENV_MODE = 'AWS_DEFAULTS_MODE'; + const ENV_PROFILE = 'AWS_PROFILE'; + const INI_MODE = 'defaults_mode'; + + public static $cacheKey = 'aws_defaults_mode'; + + protected static $interfaceClass = ConfigurationInterface::class; + protected static $exceptionClass = ConfigurationException::class; + + /** + * Create a default config provider that first checks for environment + * variables, then checks for a specified profile in the environment-defined + * config file location (env variable is 'AWS_CONFIG_FILE', file location + * defaults to ~/.aws/config), then checks for the "default" profile in the + * environment-defined config file location, and failing those uses a default + * fallback set of configuration options. + * + * This provider is automatically wrapped in a memoize function that caches + * previously provided config options. + * + * @param array $config + * + * @return callable + */ + public static function defaultProvider(array $config = []) + { + $configProviders = [self::env()]; + if ( + !isset($config['use_aws_shared_config_files']) + || $config['use_aws_shared_config_files'] != false + ) { + $configProviders[] = self::ini(); + } + $configProviders[] = self::fallback(); + + $memo = self::memoize( + call_user_func_array([ConfigurationProvider::class, 'chain'], $configProviders) + ); + + if (isset($config['defaultsMode']) + && $config['defaultsMode'] instanceof CacheInterface + ) { + return self::cache($memo, $config['defaultsMode'], self::$cacheKey); + } + + return $memo; + } + + /** + * Provider that creates config from environment variables. + * + * @return callable + */ + public static function env() + { + return function () { + // Use config from environment variables, if available + $mode = getenv(self::ENV_MODE); + if (!empty($mode)) { + return Promise\Create::promiseFor( + new Configuration($mode) + ); + } + + return self::reject('Could not find environment variable config' + . ' in ' . self::ENV_MODE); + }; + } + + /** + * Fallback config options when other sources are not set. + * + * @return callable + */ + public static function fallback() + { + return function () { + return Promise\Create::promiseFor( + new Configuration( self::DEFAULT_MODE) + ); + }; + } + + /** + * Config provider that creates config using a config file whose location + * is specified by an environment variable 'AWS_CONFIG_FILE', defaulting to + * ~/.aws/config if not specified + * + * @param string|null $profile Profile to use. If not specified will use + * the "default" profile. + * @param string|null $filename If provided, uses a custom filename rather + * than looking in the default directory. + * + * @return callable + */ + public static function ini( + $profile = null, + $filename = null + ) { + $filename = $filename ?: (self::getDefaultConfigFilename()); + $profile = $profile ?: (getenv(self::ENV_PROFILE) ?: 'default'); + + return function () use ($profile, $filename) { + if (!is_readable($filename)) { + return self::reject("Cannot read configuration from $filename"); + } + $data = \Aws\parse_ini_file($filename, true); + if ($data === false) { + return self::reject("Invalid config file: $filename"); + } + if (!isset($data[$profile])) { + return self::reject("'$profile' not found in config file"); + } + if (!isset($data[$profile][self::INI_MODE])) { + return self::reject("Required defaults mode config values + not present in INI profile '{$profile}' ({$filename})"); + } + return Promise\Create::promiseFor( + new Configuration( + $data[$profile][self::INI_MODE] + ) + ); + }; + } + + /** + * Unwraps a configuration object in whatever valid form it is in, + * always returning a ConfigurationInterface object. + * + * @param mixed $config + * @return ConfigurationInterface + * @throws \InvalidArgumentException + */ + public static function unwrap($config) + { + if (is_callable($config)) { + $config = $config(); + } + if ($config instanceof PromiseInterface) { + $config = $config->wait(); + } + if ($config instanceof ConfigurationInterface) { + return $config; + } + + if (is_string($config)) { + return new Configuration($config); + } + + throw new \InvalidArgumentException('Not a valid defaults mode configuration' + . ' argument.'); + } +} diff --git a/3rdparty/aws/aws-sdk-php/src/DefaultsMode/Exception/ConfigurationException.php b/3rdparty/aws/aws-sdk-php/src/DefaultsMode/Exception/ConfigurationException.php new file mode 100644 index 00000000..b7186a5b --- /dev/null +++ b/3rdparty/aws/aws-sdk-php/src/DefaultsMode/Exception/ConfigurationException.php @@ -0,0 +1,14 @@ +cache = $cache; + } + + public function get($key) + { + return $this->cache->fetch($key); + } + + /** + * @return mixed + */ + public function fetch($key) + { + return $this->get($key); + } + + public function set($key, $value, $ttl = 0) + { + return $this->cache->save($key, $value, $ttl); + } + + /** + * @return bool + */ + public function save($key, $value, $ttl = 0) + { + return $this->set($key, $value, $ttl); + } + + public function remove($key) + { + return $this->cache->delete($key); + } + + /** + * @return bool + */ + public function delete($key) + { + return $this->remove($key); + } + + /** + * @return bool + */ + public function contains($key) + { + return $this->cache->contains($key); + } + + /** + * @return mixed[]|null + */ + public function getStats() + { + return $this->cache->getStats(); + } +} diff --git a/3rdparty/aws/aws-sdk-php/src/Endpoint/EndpointProvider.php b/3rdparty/aws/aws-sdk-php/src/Endpoint/EndpointProvider.php new file mode 100644 index 00000000..ba535742 --- /dev/null +++ b/3rdparty/aws/aws-sdk-php/src/Endpoint/EndpointProvider.php @@ -0,0 +1,96 @@ + 'ec2', 'region' => 'us-west-2']); + * // Returns an endpoint array or throws. + * $endpoint = EndpointProvider::resolve($provider, [ + * 'service' => 'ec2', + * 'region' => 'us-west-2' + * ]); + * + * You can compose multiple providers into a single provider using + * {@see Aws\or_chain}. This function accepts providers as arguments and + * returns a new function that will invoke each provider until a non-null value + * is returned. + * + * $a = function (array $args) { + * if ($args['region'] === 'my-test-region') { + * return ['endpoint' => 'http://localhost:123/api']; + * } + * }; + * $b = EndpointProvider::defaultProvider(); + * $c = \Aws\or_chain($a, $b); + * $config = ['service' => 'ec2', 'region' => 'my-test-region']; + * $res = $c($config); // $a handles this. + * $config['region'] = 'us-west-2'; + * $res = $c($config); // $b handles this. + */ +class EndpointProvider +{ + /** + * Resolves and endpoint provider and ensures a non-null return value. + * + * @param callable $provider Provider function to invoke. + * @param array $args Endpoint arguments to pass to the provider. + * + * @return array + * @throws UnresolvedEndpointException + */ + public static function resolve(callable $provider, array $args = []) + { + $result = $provider($args); + if (is_array($result)) { + return $result; + } + + throw new UnresolvedEndpointException( + 'Unable to resolve an endpoint using the provider arguments: ' + . json_encode($args) . '. Note: you can provide an "endpoint" ' + . 'option to a client constructor to bypass invoking an endpoint ' + . 'provider.'); + } + + /** + * Creates and returns the default SDK endpoint provider. + * + * @deprecated Use an instance of \Aws\Endpoint\Partition instead. + * + * @return callable + */ + public static function defaultProvider() + { + return PartitionEndpointProvider::defaultProvider(); + } + + /** + * Creates and returns an endpoint provider that uses patterns from an + * array. + * + * @param array $patterns Endpoint patterns + * + * @return callable + */ + public static function patterns(array $patterns) + { + return new PatternEndpointProvider($patterns); + } +} diff --git a/3rdparty/aws/aws-sdk-php/src/Endpoint/Partition.php b/3rdparty/aws/aws-sdk-php/src/Endpoint/Partition.php new file mode 100644 index 00000000..46d23712 --- /dev/null +++ b/3rdparty/aws/aws-sdk-php/src/Endpoint/Partition.php @@ -0,0 +1,322 @@ +data = $definition; + } + + public function getName() + { + return $this->data['partition']; + } + + /** + * @internal + * @return mixed + */ + public function getDnsSuffix() + { + return $this->data['dnsSuffix']; + } + + public function isRegionMatch($region, $service) + { + if (isset($this->data['regions'][$region]) + || isset($this->data['services'][$service]['endpoints'][$region]) + ) { + return true; + } + + if (isset($this->data['regionRegex'])) { + return (bool) preg_match( + "@{$this->data['regionRegex']}@", + $region + ); + } + + return false; + } + + public function getAvailableEndpoints( + $service, + $allowNonRegionalEndpoints = false + ) { + if ($this->isServicePartitionGlobal($service)) { + return [$this->getPartitionEndpoint($service)]; + } + + if (isset($this->data['services'][$service]['endpoints'])) { + $serviceRegions = array_keys( + $this->data['services'][$service]['endpoints'] + ); + + return $allowNonRegionalEndpoints + ? $serviceRegions + : array_intersect($serviceRegions, array_keys( + $this->data['regions'] + )); + } + + return []; + } + + public function __invoke(array $args = []) + { + $service = isset($args['service']) ? $args['service'] : ''; + $region = isset($args['region']) ? $args['region'] : ''; + $scheme = isset($args['scheme']) ? $args['scheme'] : 'https'; + $options = isset($args['options']) ? $args['options'] : []; + $data = $this->getEndpointData($service, $region, $options); + $variant = $this->getVariant($options, $data); + if (isset($variant['hostname'])) { + $template = $variant['hostname']; + } else { + $template = isset($data['hostname']) ? $data['hostname'] : ''; + } + $dnsSuffix = isset($variant['dnsSuffix']) + ? $variant['dnsSuffix'] + : $this->data['dnsSuffix']; + return [ + 'endpoint' => "{$scheme}://" . $this->formatEndpoint( + $template, + $service, + $region, + $dnsSuffix + ), + 'signatureVersion' => $this->getSignatureVersion($data), + 'signingRegion' => isset($data['credentialScope']['region']) + ? $data['credentialScope']['region'] + : $region, + 'signingName' => isset($data['credentialScope']['service']) + ? $data['credentialScope']['service'] + : $service, + ]; + } + + private function getEndpointData($service, $region, $options) + { + $defaultRegion = $this->resolveRegion($service, $region, $options); + $data = isset($this->data['services'][$service]['endpoints'][$defaultRegion]) + ? $this->data['services'][$service]['endpoints'][$defaultRegion] + : []; + $data += isset($this->data['services'][$service]['defaults']) + ? $this->data['services'][$service]['defaults'] + : []; + $data += isset($this->data['defaults']) + ? $this->data['defaults'] + : []; + + return $data; + } + + private function getSignatureVersion(array $data) + { + static $supportedBySdk = [ + 's3v4', + 'v4', + 'anonymous', + ]; + + $possibilities = array_intersect( + $supportedBySdk, + isset($data['signatureVersions']) + ? $data['signatureVersions'] + : ['v4'] + ); + + return array_shift($possibilities); + } + + private function resolveRegion($service, $region, $options) + { + if (isset($this->data['services'][$service]['endpoints'][$region]) + && $this->isFipsEndpointUsed($region) + ) { + return $region; + } + + if ($this->isServicePartitionGlobal($service) + || $this->isStsLegacyEndpointUsed($service, $region, $options) + || $this->isS3LegacyEndpointUsed($service, $region, $options) + ) { + return $this->getPartitionEndpoint($service); + } + + return $region; + } + + private function isServicePartitionGlobal($service) + { + return isset($this->data['services'][$service]['isRegionalized']) + && false === $this->data['services'][$service]['isRegionalized'] + && isset($this->data['services'][$service]['partitionEndpoint']); + } + + /** + * STS legacy endpoints used for valid regions unless option is explicitly + * set to 'regional' + * + * @param string $service + * @param string $region + * @param array $options + * @return bool + */ + private function isStsLegacyEndpointUsed($service, $region, $options) + { + return $service === 'sts' + && in_array($region, $this->stsLegacyGlobalRegions) + && (empty($options['sts_regional_endpoints']) + || ConfigurationProvider::unwrap( + $options['sts_regional_endpoints'] + )->getEndpointsType() !== 'regional' + ); + } + + /** + * S3 legacy us-east-1 endpoint used for valid regions unless option is explicitly + * set to 'regional' + * + * @param string $service + * @param string $region + * @param array $options + * @return bool + */ + private function isS3LegacyEndpointUsed($service, $region, $options) + { + return $service === 's3' + && $region === 'us-east-1' + && (empty($options['s3_us_east_1_regional_endpoint']) + || S3ConfigurationProvider::unwrap( + $options['s3_us_east_1_regional_endpoint'] + )->getEndpointsType() !== 'regional' + ); + } + + private function getPartitionEndpoint($service) + { + return $this->data['services'][$service]['partitionEndpoint']; + } + + private function formatEndpoint($template, $service, $region, $dnsSuffix) + { + return strtr($template, [ + '{service}' => $service, + '{region}' => $region, + '{dnsSuffix}' => $dnsSuffix, + ]); + } + + /** + * @param $region + * @return bool + */ + private function isFipsEndpointUsed($region) + { + return strpos($region, "fips") !== false; + } + + /** + * @param array $options + * @param array $data + * @return array + */ + private function getVariant(array $options, array $data) + { + $variantTags = []; + if (isset($options['use_fips_endpoint'])) { + $useFips = $options['use_fips_endpoint']; + if (is_bool($useFips)) { + $useFips && $variantTags[] = 'fips'; + } elseif ($useFips->isUseFipsEndpoint()) { + $variantTags[] = 'fips'; + } + } + if (isset($options['use_dual_stack_endpoint'])) { + $useDualStack = $options['use_dual_stack_endpoint']; + if (is_bool($useDualStack)) { + $useDualStack && $variantTags[] = 'dualstack'; + } elseif ($useDualStack->isUseDualStackEndpoint()) { + $variantTags[] = 'dualstack'; + } + } + if (!empty($variantTags)) { + if (isset($data['variants'])) { + foreach ($data['variants'] as $variant) { + if (array_count_values($variant['tags']) == array_count_values($variantTags)) { + return $variant; + } + } + } + if (isset($this->data['defaults']['variants'])) { + foreach ($this->data['defaults']['variants'] as $variant) { + if (array_count_values($variant['tags']) == array_count_values($variantTags)) { + return $variant; + } + } + } + } + } +} diff --git a/3rdparty/aws/aws-sdk-php/src/Endpoint/PartitionEndpointProvider.php b/3rdparty/aws/aws-sdk-php/src/Endpoint/PartitionEndpointProvider.php new file mode 100644 index 00000000..21ca2c83 --- /dev/null +++ b/3rdparty/aws/aws-sdk-php/src/Endpoint/PartitionEndpointProvider.php @@ -0,0 +1,130 @@ +partitions = array_map(function (array $definition) { + return new Partition($definition); + }, array_values($partitions)); + $this->defaultPartition = $defaultPartition; + $this->options = $options; + } + + public function __invoke(array $args = []) + { + $partition = $this->getPartition( + isset($args['region']) ? $args['region'] : '', + isset($args['service']) ? $args['service'] : '' + ); + $args['options'] = $this->options; + + return $partition($args); + } + + /** + * Returns the partition containing the provided region or the default + * partition if no match is found. + * + * @param string $region + * @param string $service + * + * @return Partition + */ + public function getPartition($region, $service) + { + foreach ($this->partitions as $partition) { + if ($partition->isRegionMatch($region, $service)) { + return $partition; + } + } + + return $this->getPartitionByName($this->defaultPartition); + } + + /** + * Returns the partition with the provided name or null if no partition with + * the provided name can be found. + * + * @param string $name + * + * @return Partition|null + */ + public function getPartitionByName($name) + { + foreach ($this->partitions as $partition) { + if ($name === $partition->getName()) { + return $partition; + } + } + } + + /** + * Creates and returns the default SDK partition provider. + * + * @param array $options + * @return PartitionEndpointProvider + */ + public static function defaultProvider($options = []) + { + $data = \Aws\load_compiled_json(__DIR__ . '/../data/endpoints.json'); + $prefixData = \Aws\load_compiled_json(__DIR__ . '/../data/endpoints_prefix_history.json'); + $mergedData = self::mergePrefixData($data, $prefixData); + + return new self($mergedData['partitions'], 'aws', $options); + } + + /** + * Copy endpoint data for other prefixes used by a given service + * + * @param $data + * @param $prefixData + * @return array + */ + public static function mergePrefixData($data, $prefixData) + { + $prefixGroups = $prefixData['prefix-groups']; + + foreach ($data["partitions"] as $index => $partition) { + foreach ($prefixGroups as $current => $old) { + $serviceData = Env::search("services.\"{$current}\"", $partition); + if (!empty($serviceData)) { + foreach ($old as $prefix) { + if (empty(Env::search("services.\"{$prefix}\"", $partition))) { + $data["partitions"][$index]["services"][$prefix] = $serviceData; + } + } + } + } + } + + return $data; + } +} diff --git a/3rdparty/aws/aws-sdk-php/src/Endpoint/PartitionInterface.php b/3rdparty/aws/aws-sdk-php/src/Endpoint/PartitionInterface.php new file mode 100644 index 00000000..0f2572d9 --- /dev/null +++ b/3rdparty/aws/aws-sdk-php/src/Endpoint/PartitionInterface.php @@ -0,0 +1,56 @@ +patterns = $patterns; + } + + public function __invoke(array $args = []) + { + $service = isset($args['service']) ? $args['service'] : ''; + $region = isset($args['region']) ? $args['region'] : ''; + $keys = ["{$region}/{$service}", "{$region}/*", "*/{$service}", "*/*"]; + + foreach ($keys as $key) { + if (isset($this->patterns[$key])) { + return $this->expand( + $this->patterns[$key], + isset($args['scheme']) ? $args['scheme'] : 'https', + $service, + $region + ); + } + } + + return null; + } + + private function expand(array $config, $scheme, $service, $region) + { + $config['endpoint'] = $scheme . '://' + . strtr($config['endpoint'], [ + '{service}' => $service, + '{region}' => $region + ]); + + return $config; + } +} diff --git a/3rdparty/aws/aws-sdk-php/src/Endpoint/UseDualstackEndpoint/Configuration.php b/3rdparty/aws/aws-sdk-php/src/Endpoint/UseDualstackEndpoint/Configuration.php new file mode 100644 index 00000000..5506fca9 --- /dev/null +++ b/3rdparty/aws/aws-sdk-php/src/Endpoint/UseDualstackEndpoint/Configuration.php @@ -0,0 +1,41 @@ +useDualstackEndpoint = Aws\boolean_value($useDualstackEndpoint); + if (is_null($this->useDualstackEndpoint)) { + throw new ConfigurationException("'use_dual_stack_endpoint' config option" + . " must be a boolean value."); + } + if ($this->useDualstackEndpoint == true + && (strpos($region, "iso-") !== false || strpos($region, "-iso") !== false) + ) { + throw new ConfigurationException("Dual-stack is not supported in ISO regions"); } + } + + /** + * {@inheritdoc} + */ + public function isUseDualstackEndpoint() + { + return $this->useDualstackEndpoint; + } + + /** + * {@inheritdoc} + */ + public function toArray() + { + return [ + 'use_dual_stack_endpoint' => $this->isUseDualstackEndpoint(), + ]; + } +} diff --git a/3rdparty/aws/aws-sdk-php/src/Endpoint/UseDualstackEndpoint/ConfigurationInterface.php b/3rdparty/aws/aws-sdk-php/src/Endpoint/UseDualstackEndpoint/ConfigurationInterface.php new file mode 100644 index 00000000..e1c7d5e8 --- /dev/null +++ b/3rdparty/aws/aws-sdk-php/src/Endpoint/UseDualstackEndpoint/ConfigurationInterface.php @@ -0,0 +1,19 @@ + + * use Aws\Endpoint\UseDualstackEndpoint\ConfigurationProvider; + * $provider = ConfigurationProvider::defaultProvider(); + * // Returns a ConfigurationInterface or throws. + * $config = $provider()->wait(); + * + * + * Configuration providers can be composed to create configuration using + * conditional logic that can create different configurations in different + * environments. You can compose multiple providers into a single provider using + * {@see Aws\Endpoint\UseDualstackEndpoint\ConfigurationProvider::chain}. This function + * accepts providers as variadic arguments and returns a new function that will + * invoke each provider until a successful configuration is returned. + * + * + * // First try an INI file at this location. + * $a = ConfigurationProvider::ini(null, '/path/to/file.ini'); + * // Then try an INI file at this location. + * $b = ConfigurationProvider::ini(null, '/path/to/other-file.ini'); + * // Then try loading from environment variables. + * $c = ConfigurationProvider::env(); + * // Combine the three providers together. + * $composed = ConfigurationProvider::chain($a, $b, $c); + * // Returns a promise that is fulfilled with a configuration or throws. + * $promise = $composed(); + * // Wait on the configuration to resolve. + * $config = $promise->wait(); + * + */ +class ConfigurationProvider extends AbstractConfigurationProvider + implements ConfigurationProviderInterface +{ + const ENV_USE_DUAL_STACK_ENDPOINT = 'AWS_USE_DUALSTACK_ENDPOINT'; + const INI_USE_DUAL_STACK_ENDPOINT = 'use_dualstack_endpoint'; + + public static $cacheKey = 'aws_cached_use_dualstack_endpoint_config'; + + protected static $interfaceClass = ConfigurationInterface::class; + protected static $exceptionClass = ConfigurationException::class; + + /** + * Create a default config provider that first checks for environment + * variables, then checks for a specified profile in the environment-defined + * config file location (env variable is 'AWS_CONFIG_FILE', file location + * defaults to ~/.aws/config), then checks for the "default" profile in the + * environment-defined config file location, and failing those uses a default + * fallback set of configuration options. + * + * This provider is automatically wrapped in a memoize function that caches + * previously provided config options. + * + * @param array $config + * + * @return callable + */ + public static function defaultProvider(array $config = []) + { + $region = $config['region']; + $configProviders = [self::env($region)]; + if ( + !isset($config['use_aws_shared_config_files']) + || $config['use_aws_shared_config_files'] != false + ) { + $configProviders[] = self::ini($region); + } + $configProviders[] = self::fallback($region); + + $memo = self::memoize( + call_user_func_array([ConfigurationProvider::class, 'chain'], $configProviders) + ); + + if (isset($config['use_dual_stack_endpoint']) + && $config['use_dual_stack_endpoint'] instanceof CacheInterface + ) { + return self::cache($memo, $config['use_dual_stack_endpoint'], self::$cacheKey); + } + + return $memo; + } + + /** + * Provider that creates config from environment variables. + * + * @return callable + */ + public static function env($region) + { + return function () use ($region) { + // Use config from environment variables, if available + $useDualstackEndpoint = getenv(self::ENV_USE_DUAL_STACK_ENDPOINT); + if (!empty($useDualstackEndpoint)) { + return Promise\Create::promiseFor( + new Configuration($useDualstackEndpoint, $region) + ); + } + + return self::reject('Could not find environment variable config' + . ' in ' . self::ENV_USE_DUAL_STACK_ENDPOINT); + }; + } + + /** + * Config provider that creates config using a config file whose location + * is specified by an environment variable 'AWS_CONFIG_FILE', defaulting to + * ~/.aws/config if not specified + * + * @param string|null $profile Profile to use. If not specified will use + * the "default" profile. + * @param string|null $filename If provided, uses a custom filename rather + * than looking in the default directory. + * + * @return callable + */ + public static function ini($region, $profile = null, $filename = null) + { + $filename = $filename ?: (self::getDefaultConfigFilename()); + $profile = $profile ?: (getenv(self::ENV_PROFILE) ?: 'default'); + + return function () use ($region, $profile, $filename) { + if (!@is_readable($filename)) { + return self::reject("Cannot read configuration from $filename"); + } + + // Use INI_SCANNER_NORMAL instead of INI_SCANNER_TYPED for PHP 5.5 compatibility + $data = \Aws\parse_ini_file($filename, true, INI_SCANNER_NORMAL); + if ($data === false) { + return self::reject("Invalid config file: $filename"); + } + if (!isset($data[$profile])) { + return self::reject("'$profile' not found in config file"); + } + if (!isset($data[$profile][self::INI_USE_DUAL_STACK_ENDPOINT])) { + return self::reject("Required use dualstack endpoint config values + not present in INI profile '{$profile}' ({$filename})"); + } + + // INI_SCANNER_NORMAL parses false-y values as an empty string + if ($data[$profile][self::INI_USE_DUAL_STACK_ENDPOINT] === "") { + $data[$profile][self::INI_USE_DUAL_STACK_ENDPOINT] = false; + } + + return Promise\Create::promiseFor( + new Configuration($data[$profile][self::INI_USE_DUAL_STACK_ENDPOINT], $region) + ); + }; + } + + /** + * Fallback config options when other sources are not set. + * + * @return callable + */ + public static function fallback($region) + { + return function () use ($region) { + return Promise\Create::promiseFor(new Configuration(false, $region)); + }; + } +} diff --git a/3rdparty/aws/aws-sdk-php/src/Endpoint/UseDualstackEndpoint/Exception/ConfigurationException.php b/3rdparty/aws/aws-sdk-php/src/Endpoint/UseDualstackEndpoint/Exception/ConfigurationException.php new file mode 100644 index 00000000..796adc94 --- /dev/null +++ b/3rdparty/aws/aws-sdk-php/src/Endpoint/UseDualstackEndpoint/Exception/ConfigurationException.php @@ -0,0 +1,14 @@ +useFipsEndpoint = Aws\boolean_value($useFipsEndpoint); + if (is_null($this->useFipsEndpoint)) { + throw new ConfigurationException("'use_fips_endpoint' config option" + . " must be a boolean value."); + } + } + + /** + * {@inheritdoc} + */ + public function isUseFipsEndpoint() + { + return $this->useFipsEndpoint; + } + + /** + * {@inheritdoc} + */ + public function toArray() + { + return [ + 'use_fips_endpoint' => $this->isUseFipsEndpoint(), + ]; + } +} diff --git a/3rdparty/aws/aws-sdk-php/src/Endpoint/UseFipsEndpoint/ConfigurationInterface.php b/3rdparty/aws/aws-sdk-php/src/Endpoint/UseFipsEndpoint/ConfigurationInterface.php new file mode 100644 index 00000000..da23f872 --- /dev/null +++ b/3rdparty/aws/aws-sdk-php/src/Endpoint/UseFipsEndpoint/ConfigurationInterface.php @@ -0,0 +1,19 @@ + + * use Aws\Endpoint\UseFipsEndpoint\ConfigurationProvider; + * $provider = ConfigurationProvider::defaultProvider(); + * // Returns a ConfigurationInterface or throws. + * $config = $provider()->wait(); + * + * + * Configuration providers can be composed to create configuration using + * conditional logic that can create different configurations in different + * environments. You can compose multiple providers into a single provider using + * {@see Aws\Endpoint\UseFipsEndpoint\ConfigurationProvider::chain}. This function + * accepts providers as variadic arguments and returns a new function that will + * invoke each provider until a successful configuration is returned. + * + * + * // First try an INI file at this location. + * $a = ConfigurationProvider::ini(null, '/path/to/file.ini'); + * // Then try an INI file at this location. + * $b = ConfigurationProvider::ini(null, '/path/to/other-file.ini'); + * // Then try loading from environment variables. + * $c = ConfigurationProvider::env(); + * // Combine the three providers together. + * $composed = ConfigurationProvider::chain($a, $b, $c); + * // Returns a promise that is fulfilled with a configuration or throws. + * $promise = $composed(); + * // Wait on the configuration to resolve. + * $config = $promise->wait(); + * + */ +class ConfigurationProvider extends AbstractConfigurationProvider + implements ConfigurationProviderInterface +{ + const ENV_USE_FIPS_ENDPOINT = 'AWS_USE_FIPS_ENDPOINT'; + const INI_USE_FIPS_ENDPOINT = 'use_fips_endpoint'; + + public static $cacheKey = 'aws_cached_use_fips_endpoint_config'; + + protected static $interfaceClass = ConfigurationInterface::class; + protected static $exceptionClass = ConfigurationException::class; + + /** + * Create a default config provider that first checks for environment + * variables, then checks for a specified profile in the environment-defined + * config file location (env variable is 'AWS_CONFIG_FILE', file location + * defaults to ~/.aws/config), then checks for the "default" profile in the + * environment-defined config file location, and failing those uses a default + * fallback set of configuration options. + * + * This provider is automatically wrapped in a memoize function that caches + * previously provided config options. + * + * @param array $config + * + * @return callable + */ + public static function defaultProvider(array $config = []) + { + $configProviders = [self::env()]; + if ( + !isset($config['use_aws_shared_config_files']) + || $config['use_aws_shared_config_files'] != false + ) { + $configProviders[] = self::ini(); + } + $configProviders[] = self::fallback($config['region']); + + $memo = self::memoize( + call_user_func_array([ConfigurationProvider::class, 'chain'], $configProviders) + ); + + if (isset($config['use_fips_endpoint']) + && $config['use_fips_endpoint'] instanceof CacheInterface + ) { + return self::cache($memo, $config['use_fips_endpoint'], self::$cacheKey); + } + + return $memo; + } + + /** + * Provider that creates config from environment variables. + * + * @return callable + */ + public static function env() + { + return function () { + // Use config from environment variables, if available + $useFipsEndpoint = getenv(self::ENV_USE_FIPS_ENDPOINT); + if (!empty($useFipsEndpoint)) { + return Promise\Create::promiseFor( + new Configuration($useFipsEndpoint) + ); + } + + return self::reject('Could not find environment variable config' + . ' in ' . self::ENV_USE_FIPS_ENDPOINT); + }; + } + + /** + * Config provider that creates config using a config file whose location + * is specified by an environment variable 'AWS_CONFIG_FILE', defaulting to + * ~/.aws/config if not specified + * + * @param string|null $profile Profile to use. If not specified will use + * the "default" profile. + * @param string|null $filename If provided, uses a custom filename rather + * than looking in the default directory. + * + * @return callable + */ + public static function ini($profile = null, $filename = null) + { + $filename = $filename ?: (self::getDefaultConfigFilename()); + $profile = $profile ?: (getenv(self::ENV_PROFILE) ?: 'default'); + + return function () use ($profile, $filename) { + if (!@is_readable($filename)) { + return self::reject("Cannot read configuration from $filename"); + } + + // Use INI_SCANNER_NORMAL instead of INI_SCANNER_TYPED for PHP 5.5 compatibility + $data = \Aws\parse_ini_file($filename, true, INI_SCANNER_NORMAL); + if ($data === false) { + return self::reject("Invalid config file: $filename"); + } + if (!isset($data[$profile])) { + return self::reject("'$profile' not found in config file"); + } + if (!isset($data[$profile][self::INI_USE_FIPS_ENDPOINT])) { + return self::reject("Required use fips endpoint config values + not present in INI profile '{$profile}' ({$filename})"); + } + + // INI_SCANNER_NORMAL parses false-y values as an empty string + if ($data[$profile][self::INI_USE_FIPS_ENDPOINT] === "") { + $data[$profile][self::INI_USE_FIPS_ENDPOINT] = false; + } + + return Promise\Create::promiseFor( + new Configuration($data[$profile][self::INI_USE_FIPS_ENDPOINT]) + ); + }; + } + + /** + * Fallback config options when other sources are not set. + * + * @return callable + */ + public static function fallback($region) + { + return function () use ($region) { + $isFipsPseudoRegion = strpos($region, 'fips-') !== false + || strpos($region, '-fips') !== false; + if ($isFipsPseudoRegion){ + $configuration = new Configuration(true); + } else { + $configuration = new Configuration(false); + } + return Promise\Create::promiseFor($configuration); + }; + } +} diff --git a/3rdparty/aws/aws-sdk-php/src/Endpoint/UseFipsEndpoint/Exception/ConfigurationException.php b/3rdparty/aws/aws-sdk-php/src/Endpoint/UseFipsEndpoint/Exception/ConfigurationException.php new file mode 100644 index 00000000..468aa650 --- /dev/null +++ b/3rdparty/aws/aws-sdk-php/src/Endpoint/UseFipsEndpoint/Exception/ConfigurationException.php @@ -0,0 +1,14 @@ +cacheLimit = filter_var($cacheLimit, FILTER_VALIDATE_INT); + if ($this->cacheLimit == false || $this->cacheLimit < 1) { + throw new \InvalidArgumentException( + "'cache_limit' value must be a positive integer." + ); + } + + // Unparsable $enabled flag errs on the side of disabling endpoint discovery + $this->enabled = filter_var($enabled, FILTER_VALIDATE_BOOLEAN); + } + + /** + * {@inheritdoc} + */ + public function isEnabled() + { + return $this->enabled; + } + + /** + * {@inheritdoc} + */ + public function getCacheLimit() + { + return $this->cacheLimit; + } + + /** + * {@inheritdoc} + */ + public function toArray() + { + return [ + 'enabled' => $this->isEnabled(), + 'cache_limit' => $this->getCacheLimit() + ]; + } +} diff --git a/3rdparty/aws/aws-sdk-php/src/EndpointDiscovery/ConfigurationInterface.php b/3rdparty/aws/aws-sdk-php/src/EndpointDiscovery/ConfigurationInterface.php new file mode 100644 index 00000000..3228d1d8 --- /dev/null +++ b/3rdparty/aws/aws-sdk-php/src/EndpointDiscovery/ConfigurationInterface.php @@ -0,0 +1,30 @@ + + * use Aws\EndpointDiscovery\ConfigurationProvider; + * $provider = ConfigurationProvider::defaultProvider(); + * // Returns a ConfigurationInterface or throws. + * $config = $provider()->wait(); + * + * + * Configuration providers can be composed to create configuration using + * conditional logic that can create different configurations in different + * environments. You can compose multiple providers into a single provider using + * {@see Aws\EndpointDiscovery\ConfigurationProvider::chain}. This function + * accepts providers as variadic arguments and returns a new function that will + * invoke each provider until a successful configuration is returned. + * + * + * // First try an INI file at this location. + * $a = ConfigurationProvider::ini(null, '/path/to/file.ini'); + * // Then try an INI file at this location. + * $b = ConfigurationProvider::ini(null, '/path/to/other-file.ini'); + * // Then try loading from environment variables. + * $c = ConfigurationProvider::env(); + * // Combine the three providers together. + * $composed = ConfigurationProvider::chain($a, $b, $c); + * // Returns a promise that is fulfilled with a configuration or throws. + * $promise = $composed(); + * // Wait on the configuration to resolve. + * $config = $promise->wait(); + * + */ +class ConfigurationProvider extends AbstractConfigurationProvider + implements ConfigurationProviderInterface +{ + const DEFAULT_ENABLED = false; + const DEFAULT_CACHE_LIMIT = 1000; + const ENV_ENABLED = 'AWS_ENDPOINT_DISCOVERY_ENABLED'; + const ENV_ENABLED_ALT = 'AWS_ENABLE_ENDPOINT_DISCOVERY'; + const ENV_PROFILE = 'AWS_PROFILE'; + + public static $cacheKey = 'aws_cached_endpoint_discovery_config'; + + protected static $interfaceClass = ConfigurationInterface::class; + protected static $exceptionClass = ConfigurationException::class; + + /** + * Create a default config provider that first checks for environment + * variables, then checks for a specified profile in the environment-defined + * config file location (env variable is 'AWS_CONFIG_FILE', file location + * defaults to ~/.aws/config), then checks for the "default" profile in the + * environment-defined config file location, and failing those uses a default + * fallback set of configuration options. + * + * This provider is automatically wrapped in a memoize function that caches + * previously provided config options. + * + * @param array $config + * + * @return callable + */ + public static function defaultProvider(array $config = []) + { + $configProviders = [self::env()]; + if ( + !isset($config['use_aws_shared_config_files']) + || $config['use_aws_shared_config_files'] != false + ) { + $configProviders[] = self::ini(); + } + $configProviders[] = self::fallback($config); + + $memo = self::memoize( + call_user_func_array([ConfigurationProvider::class, 'chain'], $configProviders) + ); + + if (isset($config['endpoint_discovery']) + && $config['endpoint_discovery'] instanceof CacheInterface + ) { + return self::cache($memo, $config['endpoint_discovery'], self::$cacheKey); + } + + return $memo; + } + + /** + * Provider that creates config from environment variables. + * + * @param $cacheLimit + * @return callable + */ + public static function env($cacheLimit = self::DEFAULT_CACHE_LIMIT) + { + return function () use ($cacheLimit) { + // Use config from environment variables, if available + $enabled = getenv(self::ENV_ENABLED); + if ($enabled === false || $enabled === '') { + $enabled = getenv(self::ENV_ENABLED_ALT); + } + if ($enabled !== false && $enabled !== '') { + return Promise\Create::promiseFor( + new Configuration($enabled, $cacheLimit) + ); + } + + return self::reject('Could not find environment variable config' + . ' in ' . self::ENV_ENABLED); + }; + } + + /** + * Fallback config options when other sources are not set. Will check the + * service model for any endpoint discovery required operations, and enable + * endpoint discovery in that case. If no required operations found, will use + * the class default values. + * + * @param array $config + * @return callable + */ + public static function fallback($config = []) + { + $enabled = self::DEFAULT_ENABLED; + if (!empty($config['api_provider']) + && !empty($config['service']) + && !empty($config['version']) + ) { + $provider = $config['api_provider']; + $apiData = $provider('api', $config['service'], $config['version']); + if (!empty($apiData['operations'])) { + foreach ($apiData['operations'] as $operation) { + if (!empty($operation['endpointdiscovery']['required'])) { + $enabled = true; + } + } + } + } + + return function () use ($enabled) { + return Promise\Create::promiseFor( + new Configuration( + $enabled, + self::DEFAULT_CACHE_LIMIT + ) + ); + }; + } + + /** + * Config provider that creates config using a config file whose location + * is specified by an environment variable 'AWS_CONFIG_FILE', defaulting to + * ~/.aws/config if not specified + * + * @param string|null $profile Profile to use. If not specified will use + * the "default" profile. + * @param string|null $filename If provided, uses a custom filename rather + * than looking in the default directory. + * @param int $cacheLimit + * + * @return callable + */ + public static function ini( + $profile = null, + $filename = null, + $cacheLimit = self::DEFAULT_CACHE_LIMIT + ) { + $filename = $filename ?: (self::getDefaultConfigFilename()); + $profile = $profile ?: (getenv(self::ENV_PROFILE) ?: 'default'); + + return function () use ($profile, $filename, $cacheLimit) { + if (!@is_readable($filename)) { + return self::reject("Cannot read configuration from $filename"); + } + $data = \Aws\parse_ini_file($filename, true); + if ($data === false) { + return self::reject("Invalid config file: $filename"); + } + if (!isset($data[$profile])) { + return self::reject("'$profile' not found in config file"); + } + if (!isset($data[$profile]['endpoint_discovery_enabled'])) { + return self::reject("Required endpoint discovery config values + not present in INI profile '{$profile}' ({$filename})"); + } + + return Promise\Create::promiseFor( + new Configuration( + $data[$profile]['endpoint_discovery_enabled'], + $cacheLimit + ) + ); + }; + } + + /** + * Unwraps a configuration object in whatever valid form it is in, + * always returning a ConfigurationInterface object. + * + * @param mixed $config + * @return ConfigurationInterface + * @throws \InvalidArgumentException + */ + public static function unwrap($config) + { + if (is_callable($config)) { + $config = $config(); + } + if ($config instanceof PromiseInterface) { + $config = $config->wait(); + } + if ($config instanceof ConfigurationInterface) { + return $config; + } elseif (is_array($config) && isset($config['enabled'])) { + if (isset($config['cache_limit'])) { + return new Configuration( + $config['enabled'], + $config['cache_limit'] + ); + } + return new Configuration( + $config['enabled'], + self::DEFAULT_CACHE_LIMIT + ); + } + + throw new \InvalidArgumentException('Not a valid endpoint_discovery ' + . 'configuration argument.'); + } +} diff --git a/3rdparty/aws/aws-sdk-php/src/EndpointDiscovery/EndpointDiscoveryMiddleware.php b/3rdparty/aws/aws-sdk-php/src/EndpointDiscovery/EndpointDiscoveryMiddleware.php new file mode 100644 index 00000000..30f18200 --- /dev/null +++ b/3rdparty/aws/aws-sdk-php/src/EndpointDiscovery/EndpointDiscoveryMiddleware.php @@ -0,0 +1,423 @@ +nextHandler = $handler; + $this->client = $client; + $this->args = $args; + $this->service = $client->getApi(); + $this->config = $config; + } + + public function __invoke(CommandInterface $cmd, RequestInterface $request) + { + $nextHandler = $this->nextHandler; + $op = $this->service->getOperation($cmd->getName())->toArray(); + + // Continue only if endpointdiscovery trait is set + if (isset($op['endpointdiscovery'])) { + $config = ConfigurationProvider::unwrap($this->config); + $isRequired = !empty($op['endpointdiscovery']['required']); + + if ($isRequired && !($config->isEnabled())) { + throw new UnresolvedEndpointException('This operation ' + . 'requires the use of endpoint discovery, but this has ' + . 'been disabled in the configuration. Enable endpoint ' + . 'discovery or use a different operation.'); + } + + // Continue only if enabled by config + if ($config->isEnabled()) { + if (isset($op['endpointoperation'])) { + throw new UnresolvedEndpointException('This operation is ' + . 'contradictorily marked both as using endpoint discovery ' + . 'and being the endpoint discovery operation. Please ' + . 'verify the accuracy of your model files.'); + } + + // Original endpoint may be used if discovery optional + $originalUri = $request->getUri(); + + $identifiers = $this->getIdentifiers($op); + + $cacheKey = $this->getCacheKey( + $this->client->getCredentials()->wait(), + $cmd, + $identifiers + ); + + // Check/create cache + if (!isset(self::$cache)) { + self::$cache = new LruArrayCache($config->getCacheLimit()); + } + + if (empty($endpointList = self::$cache->get($cacheKey))) { + $endpointList = new EndpointList([]); + } + $endpoint = $endpointList->getActive(); + + // Retrieve endpoints if there is no active endpoint + if (empty($endpoint)) { + try { + $endpoint = $this->discoverEndpoint( + $cacheKey, + $cmd, + $identifiers + ); + } catch (\Exception $e) { + // Use cached endpoint, expired or active, if any remain + $endpoint = $endpointList->getEndpoint(); + + if (empty($endpoint)) { + return $this->handleDiscoveryException( + $isRequired, + $originalUri, + $e, + $cmd, + $request + ); + } + } + } + + $request = $this->modifyRequest($request, $endpoint); + + $g = function ($value) use ( + $cacheKey, + $cmd, + $identifiers, + $isRequired, + $originalUri, + $request, + &$endpoint, + &$g + ) { + if ($value instanceof AwsException + && ( + $value->getAwsErrorCode() == 'InvalidEndpointException' + || $value->getStatusCode() == 421 + ) + ) { + return $this->handleInvalidEndpoint( + $cacheKey, + $cmd, + $identifiers, + $isRequired, + $originalUri, + $request, + $value, + $endpoint, + $g + ); + } + + return $value; + }; + + return $nextHandler($cmd, $request)->otherwise($g); + } + } + + return $nextHandler($cmd, $request); + } + + private function discoverEndpoint( + $cacheKey, + CommandInterface $cmd, + array $identifiers + ) { + $discCmd = $this->getDiscoveryCommand($cmd, $identifiers); + $this->discoveryTimes[$cacheKey] = time(); + $result = $this->client->execute($discCmd); + + if (isset($result['Endpoints'])) { + $endpointData = []; + foreach ($result['Endpoints'] as $datum) { + $endpointData[$datum['Address']] = time() + + ($datum['CachePeriodInMinutes'] * 60); + } + $endpointList = new EndpointList($endpointData); + self::$cache->set($cacheKey, $endpointList); + return $endpointList->getEndpoint(); + } + + throw new UnresolvedEndpointException('The endpoint discovery operation ' + . 'yielded a response that did not contain properly formatted ' + . 'endpoint data.'); + } + + private function getCacheKey( + CredentialsInterface $creds, + CommandInterface $cmd, + array $identifiers + ) { + $key = $this->service->getServiceName() . '_' . $creds->getAccessKeyId(); + if (!empty($identifiers)) { + $key .= '_' . $cmd->getName(); + foreach ($identifiers as $identifier) { + $key .= "_{$cmd[$identifier]}"; + } + } + + return $key; + } + + private function getDiscoveryCommand( + CommandInterface $cmd, + array $identifiers + ) { + foreach ($this->service->getOperations() as $op) { + if (isset($op['endpointoperation'])) { + $endpointOperation = $op->toArray()['name']; + break; + } + } + + if (!isset($endpointOperation)) { + throw new UnresolvedEndpointException('This command is set to use ' + . 'endpoint discovery, but no endpoint discovery operation was ' + . 'found. Please verify the accuracy of your model files.'); + } + + $params = []; + if (!empty($identifiers)) { + $params['Operation'] = $cmd->getName(); + $params['Identifiers'] = []; + foreach ($identifiers as $identifier) { + $params['Identifiers'][$identifier] = $cmd[$identifier]; + } + } + $command = $this->client->getCommand($endpointOperation, $params); + $command->getHandlerList()->appendBuild( + Middleware::mapRequest(function (RequestInterface $r) { + return $r->withHeader( + 'x-amz-api-version', + $this->service->getApiVersion() + ); + }), + 'x-amz-api-version-header' + ); + + return $command; + } + + private function getIdentifiers(array $operation) + { + $inputShape = $this->service->getShapeMap() + ->resolve($operation['input']) + ->toArray(); + $identifiers = []; + foreach ($inputShape['members'] as $key => $member) { + if (!empty($member['endpointdiscoveryid'])) { + $identifiers[] = $key; + } + } + return $identifiers; + } + + private function handleDiscoveryException( + $isRequired, + $originalUri, + \Exception $e, + CommandInterface $cmd, + RequestInterface $request + ) { + // If no cached endpoints and discovery required, + // throw exception + if ($isRequired) { + $message = 'The endpoint required for this service is currently ' + . 'unable to be retrieved, and your request can not be fulfilled ' + . 'unless you manually specify an endpoint.'; + throw new AwsException( + $message, + $cmd, + [ + 'code' => 'EndpointDiscoveryException', + 'message' => $message + ], + $e + ); + } + + // If discovery isn't required, use original endpoint + return $this->useOriginalUri( + $originalUri, + $cmd, + $request + ); + } + + private function handleInvalidEndpoint( + $cacheKey, + $cmd, + $identifiers, + $isRequired, + $originalUri, + $request, + $value, + &$endpoint, + &$g + ) { + $nextHandler = $this->nextHandler; + $endpointList = self::$cache->get($cacheKey); + if ($endpointList instanceof EndpointList) { + + // Remove invalid endpoint from cached list + $endpointList->remove($endpoint); + + // If possible, get another cached endpoint + $newEndpoint = $endpointList->getEndpoint(); + } + if (empty($newEndpoint)) { + + // If no more cached endpoints, make discovery call + // if none made within cooldown for given key + if (time() - $this->discoveryTimes[$cacheKey] + < self::$discoveryCooldown + ) { + + // If no more cached endpoints and it's required, + // fail with original exception + if ($isRequired) { + return $value; + } + + // Use original endpoint if not required + return $this->useOriginalUri( + $originalUri, + $cmd, + $request + ); + } + + $newEndpoint = $this->discoverEndpoint( + $cacheKey, + $cmd, + $identifiers + ); + } + $endpoint = $newEndpoint; + $request = $this->modifyRequest($request, $endpoint); + return $nextHandler($cmd, $request)->otherwise($g); + } + + private function modifyRequest(RequestInterface $request, $endpoint) + { + $parsed = $this->parseEndpoint($endpoint); + if (!empty($request->getHeader('User-Agent'))) { + $userAgent = $request->getHeader('User-Agent')[0]; + if (strpos($userAgent, 'endpoint-discovery') === false) { + $userAgent = $userAgent . ' endpoint-discovery'; + } + } else { + $userAgent = 'endpoint-discovery'; + } + + return $request + ->withUri( + $request->getUri() + ->withHost($parsed['host']) + ->withPath($parsed['path']) + ) + ->withHeader('User-Agent', $userAgent); + } + + /** + * Parses an endpoint returned from the discovery API into an array with + * 'host' and 'path' keys. + * + * @param $endpoint + * @return array + */ + private function parseEndpoint($endpoint) + { + $parsed = parse_url($endpoint); + + // parse_url() will correctly parse full URIs with schemes + if (isset($parsed['host'])) { + return $parsed; + } + + // parse_url() will put host & path in 'path' if scheme is not provided + if (isset($parsed['path'])) { + $split = explode('/', $parsed['path'], 2); + $parsed['host'] = $split[0]; + if (isset($split[1])) { + if (substr($split[1], 0 , 1) !== '/') { + $split[1] = '/' . $split[1]; + } + $parsed['path'] = $split[1]; + } else { + $parsed['path'] = ''; + } + return $parsed; + } + + throw new UnresolvedEndpointException("The supplied endpoint '" + . "{$endpoint}' is invalid."); + } + + private function useOriginalUri( + UriInterface $uri, + CommandInterface $cmd, + RequestInterface $request + ) { + $nextHandler = $this->nextHandler; + $endpoint = $uri->getHost() . $uri->getPath(); + $request = $this->modifyRequest( + $request, + $endpoint + ); + return $nextHandler($cmd, $request); + } +} diff --git a/3rdparty/aws/aws-sdk-php/src/EndpointDiscovery/EndpointList.php b/3rdparty/aws/aws-sdk-php/src/EndpointDiscovery/EndpointList.php new file mode 100644 index 00000000..80ccc472 --- /dev/null +++ b/3rdparty/aws/aws-sdk-php/src/EndpointDiscovery/EndpointList.php @@ -0,0 +1,85 @@ +active = $endpoints; + reset($this->active); + } + + /** + * Gets an active (unexpired) endpoint. Returns null if none found. + * + * @return null|string + */ + public function getActive() + { + if (count($this->active) < 1) { + return null; + } + while (time() > current($this->active)) { + $key = key($this->active); + $this->expired[$key] = current($this->active); + $this->increment($this->active); + unset($this->active[$key]); + if (count($this->active) < 1) { + return null; + } + } + $active = key($this->active); + $this->increment($this->active); + return $active; + } + + /** + * Gets an active endpoint if possible, then an expired endpoint if possible. + * Returns null if no endpoints found. + * + * @return null|string + */ + public function getEndpoint() + { + if (!empty($active = $this->getActive())) { + return $active; + } + return $this->getExpired(); + } + + /** + * Removes an endpoint from both lists. + * + * @param string $key + */ + public function remove($key) + { + unset($this->active[$key]); + unset($this->expired[$key]); + } + + /** + * Get an expired endpoint. Returns null if none found. + * + * @return null|string + */ + private function getExpired() + { + if (count($this->expired) < 1) { + return null; + } + $expired = key($this->expired); + $this->increment($this->expired); + return $expired; + } + + private function increment(&$array) + { + if (next($array) === false) { + reset($array); + } + } +} diff --git a/3rdparty/aws/aws-sdk-php/src/EndpointDiscovery/Exception/ConfigurationException.php b/3rdparty/aws/aws-sdk-php/src/EndpointDiscovery/Exception/ConfigurationException.php new file mode 100644 index 00000000..f87cdbfa --- /dev/null +++ b/3rdparty/aws/aws-sdk-php/src/EndpointDiscovery/Exception/ConfigurationException.php @@ -0,0 +1,14 @@ +nextHandler = $nextHandler; + $this->service = $service; + } + + public function __invoke(CommandInterface $command, RequestInterface $request) + { + $nextHandler = $this->nextHandler; + + $operation = $this->service->getOperation($command->getName()); + + if (!empty($operation['endpoint']['hostPrefix'])) { + $prefix = $operation['endpoint']['hostPrefix']; + + // Captures endpoint parameters stored in the modeled host. + // These are denoted by enclosure in braces, i.e. '{param}' + preg_match_all("/\{([a-zA-Z0-9]+)}/", $prefix, $parameters); + + if (!empty($parameters[1])) { + + // Captured parameters without braces stored in $parameters[1], + // which should correspond to members in the Command object + foreach ($parameters[1] as $index => $parameter) { + if (empty($command[$parameter])) { + throw new \InvalidArgumentException( + "The parameter '{$parameter}' must be set and not empty." + ); + } + + // Captured parameters with braces stored in $parameters[0], + // which are replaced by their corresponding Command value + $prefix = str_replace( + $parameters[0][$index], + $command[$parameter], + $prefix + ); + } + } + + $uri = $request->getUri(); + $host = $prefix . $uri->getHost(); + if (!\Aws\is_valid_hostname($host)) { + throw new \InvalidArgumentException( + "The supplied parameters result in an invalid hostname: '{$host}'." + ); + } + $request = $request->withUri($uri->withHost($host)); + } + + return $nextHandler($command, $request); + } +} diff --git a/3rdparty/aws/aws-sdk-php/src/EndpointV2/EndpointDefinitionProvider.php b/3rdparty/aws/aws-sdk-php/src/EndpointV2/EndpointDefinitionProvider.php new file mode 100644 index 00000000..6da2685c --- /dev/null +++ b/3rdparty/aws/aws-sdk-php/src/EndpointV2/EndpointDefinitionProvider.php @@ -0,0 +1,71 @@ +ruleset = new Ruleset($ruleset, $partitions); + $this->cache = new LruArrayCache(100); + } + + /** + * @return Ruleset + */ + public function getRuleset() + { + return $this->ruleset; + } + + /** + * Given a Ruleset and input parameters, determines the correct endpoint + * or an error to be thrown for a given request. + * + * @return RulesetEndpoint + * @throws UnresolvedEndpointException + */ + public function resolveEndpoint(array $inputParameters) + { + $hashedParams = $this->hashInputParameters($inputParameters); + $match = $this->cache->get($hashedParams); + + if (!is_null($match)) { + return $match; + } + + $endpoint = $this->ruleset->evaluate($inputParameters); + if ($endpoint === false) { + throw new UnresolvedEndpointException( + 'Unable to resolve an endpoint using the provider arguments: ' + . json_encode($inputParameters) + ); + } + $this->cache->set($hashedParams, $endpoint); + + return $endpoint; + } + + private function hashInputParameters($inputParameters) + { + return md5(serialize($inputParameters)); + } +} diff --git a/3rdparty/aws/aws-sdk-php/src/EndpointV2/EndpointV2Middleware.php b/3rdparty/aws/aws-sdk-php/src/EndpointV2/EndpointV2Middleware.php new file mode 100644 index 00000000..d9fe4431 --- /dev/null +++ b/3rdparty/aws/aws-sdk-php/src/EndpointV2/EndpointV2Middleware.php @@ -0,0 +1,427 @@ + 'v4', + 'sigv4a' => 'v4a', + 'none' => 'anonymous', + 'bearer' => 'bearer', + 'sigv4-s3express' => 'v4-s3express' + ]; + + /** @var callable */ + private $nextHandler; + + /** @var EndpointProviderV2 */ + private $endpointProvider; + + /** @var Service */ + private $api; + + /** @var array */ + private $clientArgs; + + /** @var Closure */ + private $credentialProvider; + + /** + * Create a middleware wrapper function + * + * @param EndpointProviderV2 $endpointProvider + * @param Service $api + * @param array $args + * @param callable $credentialProvider + * + * @return Closure + */ + public static function wrap( + EndpointProviderV2 $endpointProvider, + Service $api, + array $args, + callable $credentialProvider + ) : Closure + { + return function (callable $handler) use ($endpointProvider, $api, $args, $credentialProvider) { + return new self($handler, $endpointProvider, $api, $args, $credentialProvider); + }; + } + + /** + * @param callable $nextHandler + * @param EndpointProviderV2 $endpointProvider + * @param Service $api + * @param array $args + */ + public function __construct( + callable $nextHandler, + EndpointProviderV2 $endpointProvider, + Service $api, + array $args, + ?callable $credentialProvider = null + ) + { + $this->nextHandler = $nextHandler; + $this->endpointProvider = $endpointProvider; + $this->api = $api; + $this->clientArgs = $args; + $this->credentialProvider = $credentialProvider; + } + + /** + * @param CommandInterface $command + * + * @return Promise + */ + public function __invoke(CommandInterface $command) + { + $nextHandler = $this->nextHandler; + $operation = $this->api->getOperation($command->getName()); + $commandArgs = $command->toArray(); + $providerArgs = $this->resolveArgs($commandArgs, $operation); + + $endpoint = $this->endpointProvider->resolveEndpoint($providerArgs); + + $this->appendEndpointMetrics($providerArgs, $endpoint, $command); + + if (!empty($authSchemes = $endpoint->getProperty('authSchemes'))) { + $this->applyAuthScheme( + $authSchemes, + $command + ); + } + + return $nextHandler($command, $endpoint); + } + + /** + * Resolves client, context params, static context params and endpoint provider + * arguments provided at the command level. + * + * @param array $commandArgs + * @param Operation $operation + * + * @return array + */ + private function resolveArgs(array $commandArgs, Operation $operation): array + { + $rulesetParams = $this->endpointProvider->getRuleset()->getParameters(); + + if (isset($rulesetParams[self::ACCOUNT_ID_PARAM]) + && isset($rulesetParams[self::ACCOUNT_ID_ENDPOINT_MODE_PARAM])) { + $this->clientArgs[self::ACCOUNT_ID_PARAM] = $this->resolveAccountId(); + } + + $endpointCommandArgs = $this->filterEndpointCommandArgs( + $rulesetParams, + $commandArgs + ); + $staticContextParams = $this->bindStaticContextParams( + $operation->getStaticContextParams() + ); + $contextParams = $this->bindContextParams( + $commandArgs, $operation->getContextParams() + ); + $operationContextParams = $this->bindOperationContextParams( + $commandArgs, + $operation->getOperationContextParams() + ); + + return array_merge( + $this->clientArgs, + $operationContextParams, + $contextParams, + $staticContextParams, + $endpointCommandArgs + ); + } + + /** + * Compares Ruleset parameters against Command arguments + * to create a mapping of arguments to pass into the + * endpoint provider for endpoint resolution. + * + * @param array $rulesetParams + * @param array $commandArgs + * @return array + */ + private function filterEndpointCommandArgs( + array $rulesetParams, + array $commandArgs + ): array + { + $endpointMiddlewareOpts = [ + '@use_dual_stack_endpoint' => 'UseDualStack', + '@use_accelerate_endpoint' => 'Accelerate', + '@use_path_style_endpoint' => 'ForcePathStyle' + ]; + + $filteredArgs = []; + + foreach($rulesetParams as $name => $value) { + if (isset($commandArgs[$name])) { + if (!empty($value->getBuiltIn())) { + continue; + } + $filteredArgs[$name] = $commandArgs[$name]; + } + } + + if ($this->api->getServiceName() === 's3') { + foreach($endpointMiddlewareOpts as $optionName => $newValue) { + if (isset($commandArgs[$optionName])) { + $filteredArgs[$newValue] = $commandArgs[$optionName]; + } + } + } + + return $filteredArgs; + } + + /** + * Binds static context params to their corresponding values. + * + * @param $staticContextParams + * + * @return array + */ + private function bindStaticContextParams($staticContextParams): array + { + $scopedParams = []; + + forEach($staticContextParams as $paramName => $paramValue) { + $scopedParams[$paramName] = $paramValue['value']; + } + + return $scopedParams; + } + + /** + * Binds context params to their corresponding values found in + * command arguments. + * + * @param array $commandArgs + * @param array $contextParams + * + * @return array + */ + private function bindContextParams( + array $commandArgs, + array $contextParams + ): array + { + $scopedParams = []; + + foreach($contextParams as $name => $spec) { + if (isset($commandArgs[$spec['shape']])) { + $scopedParams[$name] = $commandArgs[$spec['shape']]; + } + } + + return $scopedParams; + } + + /** + * Binds context params to their corresponding values found in + * command arguments. + * + * @param array $commandArgs + * @param array $contextParams + * + * @return array + */ + private function bindOperationContextParams( + array $commandArgs, + array $operationContextParams + ): array + { + $scopedParams = []; + + foreach($operationContextParams as $name => $spec) { + $scopedValue = search($spec['path'], $commandArgs); + + if ($scopedValue) { + $scopedParams[$name] = $scopedValue; + } + } + + return $scopedParams; + } + + /** + * Applies resolved auth schemes to the command object. + * + * @param $authSchemes + * @param $command + * + * @return void + */ + private function applyAuthScheme( + array $authSchemes, + CommandInterface $command + ): void + { + $authScheme = $this->resolveAuthScheme($authSchemes); + + $command['@context']['signature_version'] = $authScheme['version']; + + if (isset($authScheme['name'])) { + $command['@context']['signing_service'] = $authScheme['name']; + } + + if (isset($authScheme['region'])) { + $command['@context']['signing_region'] = $authScheme['region']; + } elseif (isset($authScheme['signingRegionSet'])) { + $command['@context']['signing_region_set'] = $authScheme['signingRegionSet']; + } + } + + /** + * Returns the first compatible auth scheme in an endpoint object's + * auth schemes. + * + * @param array $authSchemes + * + * @return array + */ + private function resolveAuthScheme(array $authSchemes): array + { + $invalidAuthSchemes = []; + + foreach($authSchemes as $authScheme) { + if ($this->isValidAuthScheme($authScheme['name'])) { + return $this->normalizeAuthScheme($authScheme); + } + $invalidAuthSchemes[$authScheme['name']] = false; + } + + $invalidAuthSchemesString = '`' . implode( + '`, `', + array_keys($invalidAuthSchemes)) + . '`'; + $validAuthSchemesString = '`' + . implode('`, `', array_keys( + array_diff_key(self::$validAuthSchemes, $invalidAuthSchemes)) + ) + . '`'; + throw new UnresolvedAuthSchemeException( + "This operation requests {$invalidAuthSchemesString}" + . " auth schemes, but the client currently supports {$validAuthSchemesString}." + ); + } + + /** + * Normalizes an auth scheme's name, signing region or signing region set + * to the auth keys recognized by the SDK. + * + * @param array $authScheme + * @return array + */ + private function normalizeAuthScheme(array $authScheme): array + { + /* + sigv4a will contain a regionSet property. which is guaranteed to be `*` + for now. The SigV4 class handles this automatically for now. It seems + complexity will be added here in the future. + */ + $normalizedAuthScheme = []; + + if (isset($authScheme['disableDoubleEncoding']) + && $authScheme['disableDoubleEncoding'] === true + && $authScheme['name'] !== 'sigv4a' + && $authScheme['name'] !== 'sigv4-s3express' + ) { + $normalizedAuthScheme['version'] = 's3v4'; + } else { + $normalizedAuthScheme['version'] = self::$validAuthSchemes[$authScheme['name']]; + } + + $normalizedAuthScheme['name'] = $authScheme['signingName'] ?? null; + $normalizedAuthScheme['region'] = $authScheme['signingRegion'] ?? null; + $normalizedAuthScheme['signingRegionSet'] = $authScheme['signingRegionSet'] ?? null; + + return $normalizedAuthScheme; + } + + private function isValidAuthScheme($signatureVersion): bool + { + if (isset(self::$validAuthSchemes[$signatureVersion])) { + if ($signatureVersion === 'sigv4a') { + return extension_loaded('awscrt'); + } + return true; + } + + return false; + } + + /** + * This method tries to resolve an `AccountId` parameter from a resolved identity. + * We will just perform this operation if the parameter `AccountId` is part of the ruleset parameters and + * `AccountIdEndpointMode` is not disabled, otherwise, we will ignore it. + * + * @return null|string + */ + private function resolveAccountId(): ?string + { + if (isset($this->clientArgs[self::ACCOUNT_ID_ENDPOINT_MODE_PARAM]) + && $this->clientArgs[self::ACCOUNT_ID_ENDPOINT_MODE_PARAM] === 'disabled') { + return null; + } + + if (is_null($this->credentialProvider)) { + return null; + } + + $identityProviderFn = $this->credentialProvider; + $identity = $identityProviderFn()->wait(); + + return $identity->getAccountId(); + } + + private function appendEndpointMetrics( + array $providerArgs, + RulesetEndpoint $endpoint, + CommandInterface $command + ): void + { + // Resolved AccountId Metric + if (!empty($providerArgs[self::ACCOUNT_ID_PARAM])) { + $command->getMetricsBuilder()->append(MetricsBuilder::RESOLVED_ACCOUNT_ID); + } + // AccountIdMode Metric + if(!empty($providerArgs[self::ACCOUNT_ID_ENDPOINT_MODE_PARAM])) { + $command->getMetricsBuilder()->identifyMetricByValueAndAppend( + 'account_id_endpoint_mode', + $providerArgs[self::ACCOUNT_ID_ENDPOINT_MODE_PARAM] + ); + } + + // AccountId Endpoint Metric + $command->getMetricsBuilder()->identifyMetricByValueAndAppend( + 'account_id_endpoint', + $endpoint->getUrl() + ); + } +} diff --git a/3rdparty/aws/aws-sdk-php/src/EndpointV2/EndpointV2SerializerTrait.php b/3rdparty/aws/aws-sdk-php/src/EndpointV2/EndpointV2SerializerTrait.php new file mode 100644 index 00000000..951cbcbc --- /dev/null +++ b/3rdparty/aws/aws-sdk-php/src/EndpointV2/EndpointV2SerializerTrait.php @@ -0,0 +1,77 @@ +applyHeaders($endpoint, $headers); + $resolvedUrl = $endpoint->getUrl(); + $this->applyScheme($resolvedUrl); + $this->endpoint = $this instanceof RestSerializer + ? new Uri($resolvedUrl) + : $resolvedUrl; + } + + /** + * Combines modeled headers and headers resolved from an endpoint object. + * + * @param $endpoint + * @param $headers + * @return void + */ + private function applyHeaders(RulesetEndpoint $endpoint, array &$headers): void + { + if (!is_null($endpoint->getHeaders())) { + $headers = array_merge( + $headers, + $endpoint->getHeaders() + ); + } + } + + /** + * Applies custom HTTP schemes provided in client configuration. + * + * @param $resolvedUrl + * @return void + */ + private function applyScheme(&$resolvedUrl): void + { + $resolvedEndpointScheme = parse_url($resolvedUrl, PHP_URL_SCHEME); + $scheme = $this->endpoint instanceof Uri + ? $this->endpoint->getScheme() + : parse_url($this->endpoint, PHP_URL_SCHEME); + + if (!empty($scheme) && $scheme !== $resolvedEndpointScheme) { + $resolvedUrl = str_replace( + $resolvedEndpointScheme, + $scheme, + $resolvedUrl + ); + } + } +} diff --git a/3rdparty/aws/aws-sdk-php/src/EndpointV2/Rule/AbstractRule.php b/3rdparty/aws/aws-sdk-php/src/EndpointV2/Rule/AbstractRule.php new file mode 100644 index 00000000..adedb4e1 --- /dev/null +++ b/3rdparty/aws/aws-sdk-php/src/EndpointV2/Rule/AbstractRule.php @@ -0,0 +1,62 @@ +conditions = $definition['conditions']; + $this->documentation = isset($definition['documentation']) ? + $definition['documentation'] : null; + } + + /** + * @return array + */ + public function getConditions() + { + return $this->conditions; + } + + /** + * @return mixed + */ + public function getDocumentation() + { + return $this->documentation; + } + + /** + * Determines if all conditions for a given rule are met. + * + * @return boolean + */ + protected function evaluateConditions( + array &$inputParameters, + RulesetStandardLibrary $standardLibrary + ) + { + foreach($this->getConditions() as $condition) { + $result = $standardLibrary->callFunction($condition, $inputParameters); + if (is_null($result) || $result === false) { + return false; + } + } + return true; + } + + abstract public function evaluate( + array $inputParameters, + RulesetStandardLibrary $standardLibrary + ); +} diff --git a/3rdparty/aws/aws-sdk-php/src/EndpointV2/Rule/EndpointRule.php b/3rdparty/aws/aws-sdk-php/src/EndpointV2/Rule/EndpointRule.php new file mode 100644 index 00000000..c3a0e9b7 --- /dev/null +++ b/3rdparty/aws/aws-sdk-php/src/EndpointV2/Rule/EndpointRule.php @@ -0,0 +1,111 @@ +endpoint = $definition['endpoint']; + } + + /** + * @return array + */ + public function getEndpoint() + { + return $this->endpoint; + } + + /** + * If all the rule's conditions are met, return the resolved + * endpoint object. + * + * @return RulesetEndpoint | null + */ + public function evaluate(array $inputParameters, RulesetStandardLibrary $standardLibrary) + { + if ($this->evaluateConditions($inputParameters, $standardLibrary)) { + return $this->resolve($inputParameters, $standardLibrary); + } + return false; + } + + /** + * Given input parameters, resolve an endpoint in its entirety. + * + * @return RulesetEndpoint + */ + private function resolve( + array $inputParameters, + RulesetStandardLibrary $standardLibrary + ) + { + $uri = $standardLibrary->resolveValue($this->endpoint['url'], $inputParameters); + $properties = isset($this->endpoint['properties']) + ? $this->resolveProperties($this->endpoint['properties'], $inputParameters, $standardLibrary) + : null; + $headers = $this->resolveHeaders($inputParameters, $standardLibrary); + + return new RulesetEndpoint($uri, $properties, $headers); + } + + /** + * Recurse through an endpoint's `properties` attribute, resolving template + * strings when found. Return the fully resolved attribute. + * + * @return array + */ + private function resolveProperties( + $properties, + array $inputParameters, + RulesetStandardLibrary $standardLibrary + ) + { + if (is_array($properties)) { + $propertiesArr = []; + foreach($properties as $key => $val) { + $propertiesArr[$key] = $this->resolveProperties($val, $inputParameters, $standardLibrary); + } + return $propertiesArr; + } elseif ($standardLibrary->isTemplate($properties)) { + return $standardLibrary->resolveTemplateString($properties, $inputParameters); + } + return $properties; + } + + /** + * If present, iterate through an endpoint's headers attribute resolving + * values along the way. Return the fully resolved attribute. + * + * @return array + */ + private function resolveHeaders( + array $inputParameters, + RulesetStandardLibrary $standardLibrary + ) + { + $headers = isset($this->endpoint['headers']) ? $this->endpoint['headers'] : null; + if (is_null($headers)) { + return null; + } + $resolvedHeaders = []; + + foreach($headers as $headerName => $headerValues) { + $resolvedValues = []; + foreach($headerValues as $value) { + $resolvedValue = $standardLibrary->resolveValue($value, $inputParameters, $standardLibrary); + $resolvedValues[] = $resolvedValue; + } + $resolvedHeaders[$headerName] = $resolvedValues; + } + return $resolvedHeaders; + } +} diff --git a/3rdparty/aws/aws-sdk-php/src/EndpointV2/Rule/ErrorRule.php b/3rdparty/aws/aws-sdk-php/src/EndpointV2/Rule/ErrorRule.php new file mode 100644 index 00000000..941624a1 --- /dev/null +++ b/3rdparty/aws/aws-sdk-php/src/EndpointV2/Rule/ErrorRule.php @@ -0,0 +1,45 @@ +error = $definition['error']; + } + + /** + * @return array + */ + public function getError() + { + return $this->error; + } + + /** + * If an error rule's conditions are met, raise an + * UnresolvedEndpointError containing the fully resolved error string. + * + * @return null + * @throws UnresolvedEndpointException + */ + public function evaluate( + array $inputParameters, + RulesetStandardLibrary $standardLibrary + ) + { + if ($this->evaluateConditions($inputParameters, $standardLibrary)) { + $message = $standardLibrary->resolveValue($this->error, $inputParameters); + throw new UnresolvedEndpointException($message); + } + return false; + } +} diff --git a/3rdparty/aws/aws-sdk-php/src/EndpointV2/Rule/RuleCreator.php b/3rdparty/aws/aws-sdk-php/src/EndpointV2/Rule/RuleCreator.php new file mode 100644 index 00000000..279477e8 --- /dev/null +++ b/3rdparty/aws/aws-sdk-php/src/EndpointV2/Rule/RuleCreator.php @@ -0,0 +1,26 @@ +rules = $this->createRules($definition['rules']); + } + + /** + * @return array + */ + public function getRules() + { + return $this->rules; + } + + /** + * If a tree rule's conditions evaluate successfully, iterate over its + * subordinate rules and return a result if there is one. If any of the + * subsequent rules are trees, the function will recurse until it reaches + * an error or an endpoint rule + * + * @return mixed + */ + public function evaluate( + array $inputParameters, + RulesetStandardLibrary $standardLibrary + ) + { + if ($this->evaluateConditions($inputParameters, $standardLibrary)) { + foreach($this->rules as $rule) { + $inputParametersCopy = $inputParameters; + $evaluation = $rule->evaluate($inputParametersCopy, $standardLibrary); + if ($evaluation !== false) { + return $evaluation; + } + } + } + return false; + } + + private function createRules(array $rules) + { + $rulesList = []; + + forEach($rules as $rule) { + $ruleType = RuleCreator::create($rule['type'], $rule); + $rulesList[] = $ruleType; + } + return $rulesList; + } +} diff --git a/3rdparty/aws/aws-sdk-php/src/EndpointV2/Ruleset/Ruleset.php b/3rdparty/aws/aws-sdk-php/src/EndpointV2/Ruleset/Ruleset.php new file mode 100644 index 00000000..21828c19 --- /dev/null +++ b/3rdparty/aws/aws-sdk-php/src/EndpointV2/Ruleset/Ruleset.php @@ -0,0 +1,117 @@ +version = $ruleset['version']; + $this->parameters = $this->createParameters($ruleset['parameters']); + $this->rules = $this->createRules($ruleset['rules']); + $this->standardLibrary = new RulesetStandardLibrary($partitions); + } + + /** + * @return mixed + */ + public function getVersion() + { + return $this->version; + } + + /** + * @return array + */ + public function getParameters() + { + return $this->parameters; + } + + /** + * @return array + */ + public function getRules() + { + return $this->rules; + } + + /** + * Evaluate the ruleset against the input parameters. + * Return the first rule the parameters match against. + * + * @return mixed + */ + public function evaluate(array $inputParameters) + { + $this->validateInputParameters($inputParameters); + + foreach($this->rules as $rule) { + $evaluation = $rule->evaluate($inputParameters, $this->standardLibrary); + if ($evaluation !== false) { + return $evaluation; + } + } + return false; + } + + /** + * Ensures all corresponding client-provided parameters match + * the Ruleset parameter's specified type. + * + * @return void + */ + private function validateInputParameters(array &$inputParameters) + { + foreach($this->parameters as $paramName => $param) { + $inputParam = isset($inputParameters[$paramName]) ? $inputParameters[$paramName] : null; + + if (is_null($inputParam) && !is_null($param->getDefault())) { + $inputParameters[$paramName] = $param->getDefault(); + } elseif (!is_null($inputParam)) { + $param->validateInputParam($inputParam); + } + } + } + + private function createParameters(array $parameters) + { + $parameterList = []; + + foreach($parameters as $name => $definition) { + $parameterList[$name] = new RulesetParameter($name, $definition); + } + + return $parameterList; + } + + private function createRules(array $rules) + { + $rulesList = []; + + forEach($rules as $rule) { + $ruleObj = RuleCreator::create($rule['type'], $rule); + $rulesList[] = $ruleObj; + } + return $rulesList; + } +} + diff --git a/3rdparty/aws/aws-sdk-php/src/EndpointV2/Ruleset/RulesetEndpoint.php b/3rdparty/aws/aws-sdk-php/src/EndpointV2/Ruleset/RulesetEndpoint.php new file mode 100644 index 00000000..46f844e4 --- /dev/null +++ b/3rdparty/aws/aws-sdk-php/src/EndpointV2/Ruleset/RulesetEndpoint.php @@ -0,0 +1,63 @@ +url = $url; + $this->properties = $properties; + $this->headers = $headers; + } + + /** + * @return mixed + */ + public function getUrl() + { + return $this->url; + } + + /** + * @param $property + * @return mixed + */ + public function getProperty($property) + { + if (isset($this->properties[$property])) { + return $this->properties[$property]; + } + + return null; + } + + /** + * @return mixed + */ + public function getProperties() + { + return $this->properties; + } + + /** + * @return mixed + */ + public function getHeaders() + { + return $this->headers; + } +} diff --git a/3rdparty/aws/aws-sdk-php/src/EndpointV2/Ruleset/RulesetParameter.php b/3rdparty/aws/aws-sdk-php/src/EndpointV2/Ruleset/RulesetParameter.php new file mode 100644 index 00000000..1a3322cd --- /dev/null +++ b/3rdparty/aws/aws-sdk-php/src/EndpointV2/Ruleset/RulesetParameter.php @@ -0,0 +1,179 @@ + */ + private static $typeMap = [ + 'String' => 'is_string', + 'Boolean' => 'is_bool', + 'StringArray' => 'isStringArray' + ]; + + public function __construct($name, array $definition) + { + $type = ucfirst($definition['type']); + if ($this->isValidType($type)) { + $this->type = $type; + } else { + throw new UnresolvedEndpointException( + 'Unknown parameter type ' . "`{$type}`" . + '. Parameters must be of type `String`, `Boolean` or `StringArray.' + ); + } + + $this->name = $name; + $this->builtIn = $definition['builtIn'] ?? null; + $this->default = $definition['default'] ?? null; + $this->required = $definition['required'] ?? false; + $this->documentation = $definition['documentation'] ?? null; + $this->deprecated = $definition['deprecated'] ?? false; + } + + /** + * @return mixed + */ + public function getName() + { + return $this->name; + } + + /** + * @return mixed + */ + public function getType() + { + return $this->type; + } + + /** + * @return mixed + */ + public function getBuiltIn() + { + return $this->builtIn; + } + + /** + * @return mixed + */ + public function getDefault() + { + return $this->default; + } + + /** + * @return boolean + */ + public function getRequired() + { + return $this->required; + } + + /** + * @return string + */ + public function getDocumentation() + { + return $this->documentation; + } + + /** + * @return boolean + */ + public function getDeprecated() + { + return $this->deprecated; + } + + /** + * Validates that an input parameter matches the type provided in its definition. + * + * @return void + * @throws InvalidArgumentException + */ + public function validateInputParam($inputParam) + { + if (!$this->isValidInput($inputParam)) { + throw new UnresolvedEndpointException( + "Input parameter `{$this->name}` is the wrong type. Must be a {$this->type}." + ); + } + + if ($this->deprecated) { + $deprecated = $this->deprecated; + $deprecationString = "{$this->name} has been deprecated "; + $msg = $deprecated['message'] ?? null; + $since = $deprecated['since'] ?? null; + + if (!is_null($since)){ + $deprecationString .= 'since ' . $since . '. '; + } + if (!is_null($msg)) { + $deprecationString .= $msg; + } + + trigger_error($deprecationString, E_USER_WARNING); + } + } + + private function isValidType($type) + { + return isset(self::$typeMap[$type]); + } + + private function isValidInput($inputParam): bool + { + $method = self::$typeMap[$this->type]; + if (is_callable($method)) { + return $method($inputParam); + } elseif (method_exists($this, $method)) { + return $this->$method($inputParam); + } + + return false; + } + + private function isStringArray(array $array): bool + { + if (is_associative($array)) { + return false; + } + + foreach($array as $value) { + if (!is_string($value)) { + return false; + } + } + + return true; + } +} diff --git a/3rdparty/aws/aws-sdk-php/src/EndpointV2/Ruleset/RulesetStandardLibrary.php b/3rdparty/aws/aws-sdk-php/src/EndpointV2/Ruleset/RulesetStandardLibrary.php new file mode 100644 index 00000000..910bc5a4 --- /dev/null +++ b/3rdparty/aws/aws-sdk-php/src/EndpointV2/Ruleset/RulesetStandardLibrary.php @@ -0,0 +1,434 @@ +[^\{\}]+)|(?R))*\}#x'; + const HOST_LABEL_RE = '/^(?!-)[a-zA-Z\d-]{1,63}(?partitions = $partitions; + } + + /** + * Determines if a value is set. + * + * @return boolean + */ + public function is_set($value) + { + return isset($value); + } + + /** + * Function implementation of logical operator `not` + * + * @return boolean + */ + public function not($value) + { + return !$value; + } + + /** + * Find an attribute within a value given a path string. + * + * @return mixed + */ + public function getAttr($from, $path) + { + // Handles the case where "[= $stop or strlen($input) < $stop) { + return null; + } + if (!$reverse) { + return substr($input, $start, $stop - $start); + } else { + $offset = strlen($input) - $stop; + $length = $stop - $start; + return substr($input, $offset, $length); + } + } + + /** + * Evaluates two strings for equality. + * + * @return boolean + */ + public function stringEquals($string1, $string2) + { + if (!is_string($string1) || !is_string($string2)) { + throw new UnresolvedEndpointException( + 'Values passed to StringEquals must be `string`.' + ); + } + return $string1 === $string2; + } + + /** + * Evaluates two booleans for equality. + * + * @return boolean + */ + public function booleanEquals($boolean1, $boolean2) + { + return + filter_var($boolean1, FILTER_VALIDATE_BOOLEAN) + === filter_var($boolean2, FILTER_VALIDATE_BOOLEAN); + } + + /** + * Percent-encodes an input string. + * + * @return mixed + */ + public function uriEncode($input) + { + if (is_null($input)) { + return null; + } + return str_replace('%7E', '~', rawurlencode($input)); + } + + /** + * Parses URL string into components. + * + * @return mixed + */ + public function parseUrl($url) + { + if (is_null($url)) { + return null; + } + + $parsed = parse_url($url); + + if ($parsed === false || !empty($parsed['query'])) { + return null; + } elseif (!isset($parsed['scheme'])) { + return null; + } + + if ($parsed['scheme'] !== 'http' + && $parsed['scheme'] !== 'https' + ) { + return null; + } + + $urlInfo = []; + $urlInfo['scheme'] = $parsed['scheme']; + $urlInfo['authority'] = isset($parsed['host']) ? $parsed['host'] : ''; + if (isset($parsed['port'])) { + $urlInfo['authority'] = $urlInfo['authority'] . ":" . $parsed['port']; + } + $urlInfo['path'] = isset($parsed['path']) ? $parsed['path'] : ''; + $urlInfo['normalizedPath'] = !empty($parsed['path']) + ? rtrim($urlInfo['path'] ?: '', '/' . "/") . '/' + : '/'; + $urlInfo['isIp'] = !isset($parsed['host']) ? + 'false' : $this->isValidIp($parsed['host']); + + return $urlInfo; + } + + /** + * Evaluates whether a value is a valid host label per + * RFC 1123. If allow_subdomains is true, split on `.` and validate + * each subdomain separately. + * + * @return boolean + */ + public function isValidHostLabel($hostLabel, $allowSubDomains) + { + if (!isset($hostLabel) + || (!$allowSubDomains && strpos($hostLabel, '.') != false) + ) { + return false; + } + + if ($allowSubDomains) { + foreach (explode('.', $hostLabel) as $subdomain) { + if (!$this->validateHostLabel($subdomain)) { + return false; + } + } + return true; + } else { + return $this->validateHostLabel($hostLabel); + } + } + + /** + * Parse and validate string for ARN components. + * + * @return array|null + */ + public function parseArn($arnString) + { + if (is_null($arnString) + || substr( $arnString, 0, 3 ) !== "arn" + ) { + return null; + } + + $arn = []; + $parts = explode(':', $arnString, 6); + if (sizeof($parts) < 6) { + return null; + } + + $arn['partition'] = isset($parts[1]) ? $parts[1] : null; + $arn['service'] = isset($parts[2]) ? $parts[2] : null; + $arn['region'] = isset($parts[3]) ? $parts[3] : null; + $arn['accountId'] = isset($parts[4]) ? $parts[4] : null; + $arn['resourceId'] = isset($parts[5]) ? $parts[5] : null; + + if (empty($arn['partition']) + || empty($arn['service']) + || empty($arn['resourceId']) + ) { + return null; + } + $resource = $arn['resourceId']; + $arn['resourceId'] = preg_split("/[:\/]/", $resource); + + return $arn; + } + + /** + * Matches a region string to an AWS partition. + * + * @return mixed + */ + public function partition($region) + { + if (!is_string($region)) { + throw new UnresolvedEndpointException( + 'Value passed to `partition` must be `string`.' + ); + } + + $partitions = $this->partitions; + foreach ($partitions['partitions'] as $partition) { + if (array_key_exists($region, $partition['regions']) + || preg_match("/{$partition['regionRegex']}/", $region) + ) { + return $partition['outputs']; + } + } + //return `aws` partition if no match is found. + return $partitions['partitions'][0]['outputs']; + } + + /** + * Evaluates whether a value is a valid bucket name for virtual host + * style bucket URLs. + * + * @return boolean + */ + public function isVirtualHostableS3Bucket($bucketName, $allowSubdomains) + { + if ((is_null($bucketName) + || (strlen($bucketName) < 3 || strlen($bucketName) > 63)) + || preg_match(self::IPV4_RE, $bucketName) + || strtolower($bucketName) !== $bucketName + ) { + return false; + } + + if ($allowSubdomains) { + $labels = explode('.', $bucketName); + $results = []; + forEach($labels as $label) { + $results[] = $this->isVirtualHostableS3Bucket($label, false); + } + return !in_array(false, $results); + } + return $this->isValidHostLabel($bucketName, false); + } + + public function callFunction($funcCondition, &$inputParameters) + { + $funcArgs = []; + + forEach($funcCondition['argv'] as $arg) { + $funcArgs[] = $this->resolveValue($arg, $inputParameters); + } + + $funcName = str_replace('aws.', '', $funcCondition['fn']); + if ($funcName === 'isSet') { + $funcName = 'is_set'; + } + + $result = call_user_func_array( + [RulesetStandardLibrary::class, $funcName], + $funcArgs + ); + + if (isset($funcCondition['assign'])) { + $assign = $funcCondition['assign']; + if (isset($inputParameters[$assign])){ + throw new UnresolvedEndpointException( + "Assignment `{$assign}` already exists in input parameters" . + " or has already been assigned by an endpoint rule and cannot be overwritten." + ); + } + $inputParameters[$assign] = $result; + } + return $result; + } + + public function resolveValue($value, $inputParameters) + { + //Given a value, check if it's a function, reference or template. + //returns resolved value + if ($this->isFunc($value)) { + return $this->callFunction($value, $inputParameters); + } elseif ($this->isRef($value)) { + return isset($inputParameters[$value['ref']]) ? $inputParameters[$value['ref']] : null; + } elseif ($this->isTemplate($value)) { + return $this->resolveTemplateString($value, $inputParameters); + } + return $value; + } + + public function isFunc($arg) + { + return is_array($arg) && isset($arg['fn']); + } + + public function isRef($arg) + { + return is_array($arg) && isset($arg['ref']); + } + + public function isTemplate($arg) + { + return is_string($arg) && !empty(preg_match(self::TEMPLATE_SEARCH_RE, $arg)); + } + + public function resolveTemplateString($value, $inputParameters) + { + return preg_replace_callback( + self::TEMPLATE_PARSE_RE, + function ($match) use ($inputParameters) { + if (preg_match(self::TEMPLATE_ESCAPE_RE, $match[0])) { + return $match[1]; + } + + $notFoundMessage = 'Resolved value was null. Please check rules and ' . + 'input parameters and try again.'; + + $parts = explode("#", $match[1]); + if (count($parts) > 1) { + $resolvedValue = $inputParameters; + foreach($parts as $part) { + if (!isset($resolvedValue[$part])) { + throw new UnresolvedEndpointException($notFoundMessage); + } + $resolvedValue = $resolvedValue[$part]; + } + return $resolvedValue; + } else { + if (!isset($inputParameters[$parts[0]])) { + throw new UnresolvedEndpointException($notFoundMessage); + } + return $inputParameters[$parts[0]]; + } + }, + $value + ); + } + + private function validateHostLabel ($hostLabel) + { + if (empty($hostLabel) || strlen($hostLabel) > 63) { + return false; + } + if (preg_match(self::HOST_LABEL_RE, $hostLabel)) { + return true; + } + return false; + } + + private function isValidIp($hostName) + { + $isWrapped = strpos($hostName, '[') === 0 + && strrpos($hostName, ']') === strlen($hostName) - 1; + + return preg_match( + self::IPV4_RE, + $hostName + ) + //IPV6 enclosed in brackets + || ($isWrapped && preg_match( + self::IPV6_RE, + $hostName + )) + ? 'true' : 'false'; + } +} diff --git a/3rdparty/aws/aws-sdk-php/src/Exception/AwsException.php b/3rdparty/aws/aws-sdk-php/src/Exception/AwsException.php new file mode 100644 index 00000000..05b7b955 --- /dev/null +++ b/3rdparty/aws/aws-sdk-php/src/Exception/AwsException.php @@ -0,0 +1,270 @@ +data = isset($context['body']) ? $context['body'] : []; + $this->command = $command; + $this->response = isset($context['response']) ? $context['response'] : null; + $this->request = isset($context['request']) ? $context['request'] : null; + $this->requestId = isset($context['request_id']) + ? $context['request_id'] + : null; + $this->errorType = isset($context['type']) ? $context['type'] : null; + $this->errorCode = isset($context['code']) ? $context['code'] : null; + $this->errorShape = isset($context['error_shape']) ? $context['error_shape'] : null; + $this->connectionError = !empty($context['connection_error']); + $this->result = isset($context['result']) ? $context['result'] : null; + $this->transferInfo = isset($context['transfer_stats']) + ? $context['transfer_stats'] + : []; + $this->errorMessage = isset($context['message']) + ? $context['message'] + : null; + $this->monitoringEvents = []; + $this->maxRetriesExceeded = false; + parent::__construct($message, 0, $previous); + } + + public function __toString() + { + if (!$this->getPrevious()) { + return parent::__toString(); + } + + // PHP strangely shows the innermost exception first before the outer + // exception message. It also has a default character limit for + // exception message strings such that the "next" exception (this one) + // might not even get shown, causing developers to attempt to catch + // the inner exception instead of the actual exception because they + // can't see the outer exception's __toString output. + return sprintf( + "exception '%s' with message '%s'\n\n%s", + get_class($this), + $this->getMessage(), + parent::__toString() + ); + } + + /** + * Get the command that was executed. + * + * @return CommandInterface + */ + public function getCommand() + { + return $this->command; + } + + /** + * Get the concise error message if any. + * + * @return string|null + */ + public function getAwsErrorMessage() + { + return $this->errorMessage; + } + + /** + * Get the sent HTTP request if any. + * + * @return RequestInterface|null + */ + public function getRequest() + { + return $this->request; + } + + /** + * Get the received HTTP response if any. + * + * @return ResponseInterface|null + */ + public function getResponse() + { + return $this->response; + } + + /** + * Get the result of the exception if available + * + * @return ResultInterface|null + */ + public function getResult() + { + return $this->result; + } + + /** + * Returns true if this is a connection error. + * + * @return bool + */ + public function isConnectionError() + { + return $this->connectionError; + } + + /** + * If available, gets the HTTP status code of the corresponding response + * + * @return int|null + */ + public function getStatusCode() + { + return $this->response ? $this->response->getStatusCode() : null; + } + + /** + * Get the request ID of the error. This value is only present if a + * response was received and is not present in the event of a networking + * error. + * + * @return string|null Returns null if no response was received + */ + public function getAwsRequestId() + { + return $this->requestId; + } + + /** + * Get the AWS error type. + * + * @return string|null Returns null if no response was received + */ + public function getAwsErrorType() + { + return $this->errorType; + } + + /** + * Get the AWS error code. + * + * @return string|null Returns null if no response was received + */ + public function getAwsErrorCode() + { + return $this->errorCode; + } + + /** + * Get the AWS error shape. + * + * @return Shape|null Returns null if no response was received + */ + public function getAwsErrorShape() + { + return $this->errorShape; + } + + /** + * Get all transfer information as an associative array if no $name + * argument is supplied, or gets a specific transfer statistic if + * a $name attribute is supplied (e.g., 'retries_attempted'). + * + * @param string $name Name of the transfer stat to retrieve + * + * @return mixed|null|array + */ + public function getTransferInfo($name = null) + { + if (!$name) { + return $this->transferInfo; + } + + return isset($this->transferInfo[$name]) + ? $this->transferInfo[$name] + : null; + } + + /** + * Replace the transfer information associated with an exception. + * + * @param array $info + */ + public function setTransferInfo(array $info) + { + $this->transferInfo = $info; + } + + /** + * Returns whether the max number of retries is exceeded. + * + * @return bool + */ + public function isMaxRetriesExceeded() + { + return $this->maxRetriesExceeded; + } + + /** + * Sets the flag for max number of retries exceeded. + */ + public function setMaxRetriesExceeded() + { + $this->maxRetriesExceeded = true; + } + + public function hasKey($name) + { + return isset($this->data[$name]); + } + + public function get($key) + { + return $this[$key]; + } + + public function search($expression) + { + return JmesPath::search($expression, $this->toArray()); + } +} diff --git a/3rdparty/aws/aws-sdk-php/src/Exception/CommonRuntimeException.php b/3rdparty/aws/aws-sdk-php/src/Exception/CommonRuntimeException.php new file mode 100644 index 00000000..d17cd3a7 --- /dev/null +++ b/3rdparty/aws/aws-sdk-php/src/Exception/CommonRuntimeException.php @@ -0,0 +1,7 @@ +errorCode = $code; + $this->errorMessage = $message; + parent::__construct($message); + } + + /** + * Get the AWS error code. + * + * @return string|null Returns null if no response was received + */ + public function getAwsErrorCode() + { + return $this->errorCode; + } + + /** + * Get the concise error message if any. + * + * @return string|null + */ + public function getAwsErrorMessage() + { + return $this->errorMessage; + } +} diff --git a/3rdparty/aws/aws-sdk-php/src/Exception/IncalculablePayloadException.php b/3rdparty/aws/aws-sdk-php/src/Exception/IncalculablePayloadException.php new file mode 100644 index 00000000..a64e7428 --- /dev/null +++ b/3rdparty/aws/aws-sdk-php/src/Exception/IncalculablePayloadException.php @@ -0,0 +1,11 @@ + 'uploading parts to']); + $msg .= ". The following parts had errors:\n"; + /** @var $error AwsException */ + foreach ($prev as $part => $error) { + $msg .= "- Part {$part}: " . $error->getMessage(). "\n"; + } + } elseif ($prev instanceof AwsException) { + switch ($prev->getCommand()->getName()) { + case 'CreateMultipartUpload': + case 'InitiateMultipartUpload': + $action = 'initiating'; + break; + case 'CompleteMultipartUpload': + $action = 'completing'; + break; + } + if (isset($action)) { + $msg = strtr($msg, ['performing' => $action]); + } + $msg .= ": {$prev->getMessage()}"; + } + + if (!$prev instanceof \Exception) { + $prev = null; + } + + parent::__construct($msg, 0, $prev); + $this->state = $state; + } + + /** + * Get the state of the transfer + * + * @return UploadState + */ + public function getState() + { + return $this->state; + } +} diff --git a/3rdparty/aws/aws-sdk-php/src/Exception/TokenException.php b/3rdparty/aws/aws-sdk-php/src/Exception/TokenException.php new file mode 100644 index 00000000..f6696015 --- /dev/null +++ b/3rdparty/aws/aws-sdk-php/src/Exception/TokenException.php @@ -0,0 +1,11 @@ +client = $client ?: new Client(); + } + + /** + * @param Psr7Request $request + * @param array $options + * + * @return Promise\Promise + */ + public function __invoke(Psr7Request $request, array $options = []) + { + $request = $request->withHeader( + 'User-Agent', + $request->getHeaderLine('User-Agent') + . ' ' . Utils::defaultUserAgent() + ); + + return $this->client->sendAsync($request, $this->parseOptions($options)) + ->otherwise( + static function ($e) { + $error = [ + 'exception' => $e, + 'connection_error' => $e instanceof ConnectException, + 'response' => null, + ]; + + if ( + ($e instanceof RequestException) + && $e->getResponse() + ) { + $error['response'] = $e->getResponse(); + } + + return new Promise\RejectedPromise($error); + } + ); + } + + private function parseOptions(array $options) + { + if (isset($options['http_stats_receiver'])) { + $fn = $options['http_stats_receiver']; + unset($options['http_stats_receiver']); + + $prev = isset($options['on_stats']) + ? $options['on_stats'] + : null; + + $options['on_stats'] = static function ( + TransferStats $stats + ) use ($fn, $prev) { + if (is_callable($prev)) { + $prev($stats); + } + $transferStats = ['total_time' => $stats->getTransferTime()]; + $transferStats += $stats->getHandlerStats(); + $fn($transferStats); + }; + } + + return $options; + } +} diff --git a/3rdparty/aws/aws-sdk-php/src/Handler/GuzzleV6/GuzzleHandler.php b/3rdparty/aws/aws-sdk-php/src/Handler/GuzzleV6/GuzzleHandler.php new file mode 100644 index 00000000..9d08d2a8 --- /dev/null +++ b/3rdparty/aws/aws-sdk-php/src/Handler/GuzzleV6/GuzzleHandler.php @@ -0,0 +1,11 @@ + [], + self::SIGN => [], + self::BUILD => [], + self::VALIDATE => [], + self::INIT => [], + ]; + + /** + * @param callable $handler HTTP handler. + */ + public function __construct(?callable $handler = null) + { + $this->handler = $handler; + } + + /** + * Dumps a string representation of the list. + * + * @return string + */ + public function __toString() + { + $str = ''; + $i = 0; + + foreach (array_reverse($this->steps) as $k => $step) { + foreach (array_reverse($step) as $j => $tuple) { + $str .= "{$i}) Step: {$k}, "; + if ($tuple[1]) { + $str .= "Name: {$tuple[1]}, "; + } + $str .= "Function: " . $this->debugCallable($tuple[0]) . "\n"; + $i++; + } + } + + if ($this->handler) { + $str .= "{$i}) Handler: " . $this->debugCallable($this->handler) . "\n"; + } + + return $str; + } + + /** + * Set the HTTP handler that actually returns a response. + * + * @param callable $handler Function that accepts a request and array of + * options and returns a Promise. + */ + public function setHandler(callable $handler) + { + $this->handler = $handler; + } + + /** + * Returns true if the builder has a handler. + * + * @return bool + */ + public function hasHandler() + { + return (bool) $this->handler; + } + + /** + * Checks if a middleware exists. The middleware + * should have been added with a name in order to + * use this method. + * + * @param string $name + * + * @return bool + */ + public function hasMiddleware(string $name): bool + { + return isset($this->named[$name]); + } + + /** + * Append a middleware to the init step. + * + * @param callable $middleware Middleware function to add. + * @param string $name Name of the middleware. + */ + public function appendInit(callable $middleware, $name = null) + { + $this->add(self::INIT, $name, $middleware); + } + + /** + * Prepend a middleware to the init step. + * + * @param callable $middleware Middleware function to add. + * @param string $name Name of the middleware. + */ + public function prependInit(callable $middleware, $name = null) + { + $this->add(self::INIT, $name, $middleware, true); + } + + /** + * Append a middleware to the validate step. + * + * @param callable $middleware Middleware function to add. + * @param string $name Name of the middleware. + */ + public function appendValidate(callable $middleware, $name = null) + { + $this->add(self::VALIDATE, $name, $middleware); + } + + /** + * Prepend a middleware to the validate step. + * + * @param callable $middleware Middleware function to add. + * @param string $name Name of the middleware. + */ + public function prependValidate(callable $middleware, $name = null) + { + $this->add(self::VALIDATE, $name, $middleware, true); + } + + /** + * Append a middleware to the build step. + * + * @param callable $middleware Middleware function to add. + * @param string $name Name of the middleware. + */ + public function appendBuild(callable $middleware, $name = null) + { + $this->add(self::BUILD, $name, $middleware); + } + + /** + * Prepend a middleware to the build step. + * + * @param callable $middleware Middleware function to add. + * @param string $name Name of the middleware. + */ + public function prependBuild(callable $middleware, $name = null) + { + $this->add(self::BUILD, $name, $middleware, true); + } + + /** + * Append a middleware to the sign step. + * + * @param callable $middleware Middleware function to add. + * @param string $name Name of the middleware. + */ + public function appendSign(callable $middleware, $name = null) + { + $this->add(self::SIGN, $name, $middleware); + } + + /** + * Prepend a middleware to the sign step. + * + * @param callable $middleware Middleware function to add. + * @param string $name Name of the middleware. + */ + public function prependSign(callable $middleware, $name = null) + { + $this->add(self::SIGN, $name, $middleware, true); + } + + /** + * Append a middleware to the attempt step. + * + * @param callable $middleware Middleware function to add. + * @param string $name Name of the middleware. + */ + public function appendAttempt(callable $middleware, $name = null) + { + $this->add(self::ATTEMPT, $name, $middleware); + } + + /** + * Prepend a middleware to the attempt step. + * + * @param callable $middleware Middleware function to add. + * @param string $name Name of the middleware. + */ + public function prependAttempt(callable $middleware, $name = null) + { + $this->add(self::ATTEMPT, $name, $middleware, true); + } + + /** + * Add a middleware before the given middleware by name. + * + * @param string|callable $findName Add before this + * @param string $withName Optional name to give the middleware + * @param callable $middleware Middleware to add. + */ + public function before($findName, $withName, callable $middleware) + { + $this->splice($findName, $withName, $middleware, true); + } + + /** + * Add a middleware after the given middleware by name. + * + * @param string|callable $findName Add after this + * @param string $withName Optional name to give the middleware + * @param callable $middleware Middleware to add. + */ + public function after($findName, $withName, callable $middleware) + { + $this->splice($findName, $withName, $middleware, false); + } + + /** + * Remove a middleware by name or by instance from the list. + * + * @param string|callable $nameOrInstance Middleware to remove. + */ + public function remove($nameOrInstance) + { + if (is_callable($nameOrInstance)) { + $this->removeByInstance($nameOrInstance); + } elseif (is_string($nameOrInstance)) { + $this->removeByName($nameOrInstance); + } + } + + /** + * Interpose a function between each middleware (e.g., allowing for a trace + * through the middleware layers). + * + * The interpose function is a function that accepts a "step" argument as a + * string and a "name" argument string. This function must then return a + * function that accepts the next handler in the list. This function must + * then return a function that accepts a CommandInterface and optional + * RequestInterface and returns a promise that is fulfilled with an + * Aws\ResultInterface or rejected with an Aws\Exception\AwsException + * object. + * + * @param callable|null $fn Pass null to remove any previously set function + */ + public function interpose(?callable $fn = null) + { + $this->sorted = null; + $this->interposeFn = $fn; + } + + /** + * Compose the middleware and handler into a single callable function. + * + * @return callable + */ + public function resolve() + { + if (!($prev = $this->handler)) { + throw new \LogicException('No handler has been specified'); + } + + if ($this->sorted === null) { + $this->sortMiddleware(); + } + + foreach ($this->sorted as $fn) { + $prev = $fn($prev); + } + + return $prev; + } + + /** + * @return int + */ + #[\ReturnTypeWillChange] + public function count() + { + return count($this->steps[self::INIT]) + + count($this->steps[self::VALIDATE]) + + count($this->steps[self::BUILD]) + + count($this->steps[self::SIGN]) + + count($this->steps[self::ATTEMPT]); + } + + /** + * Splices a function into the middleware list at a specific position. + * + * @param $findName + * @param $withName + * @param callable $middleware + * @param $before + */ + private function splice($findName, $withName, callable $middleware, $before) + { + if (!isset($this->named[$findName])) { + throw new \InvalidArgumentException("$findName not found"); + } + + $idx = $this->sorted = null; + $step = $this->named[$findName]; + + if ($withName) { + $this->named[$withName] = $step; + } + + foreach ($this->steps[$step] as $i => $tuple) { + if ($tuple[1] === $findName) { + $idx = $i; + break; + } + } + + $replacement = $before + ? [$this->steps[$step][$idx], [$middleware, $withName]] + : [[$middleware, $withName], $this->steps[$step][$idx]]; + array_splice($this->steps[$step], $idx, 1, $replacement); + } + + /** + * Provides a debug string for a given callable. + * + * @param array|callable $fn Function to write as a string. + * + * @return string + */ + private function debugCallable($fn) + { + if (is_string($fn)) { + return "callable({$fn})"; + } + + if (is_array($fn)) { + $ele = is_string($fn[0]) ? $fn[0] : get_class($fn[0]); + return "callable(['{$ele}', '{$fn[1]}'])"; + } + + return 'callable(' . spl_object_hash($fn) . ')'; + } + + /** + * Sort the middleware, and interpose if needed in the sorted list. + */ + private function sortMiddleware() + { + $this->sorted = []; + + if (!$this->interposeFn) { + foreach ($this->steps as $step) { + foreach ($step as $fn) { + $this->sorted[] = $fn[0]; + } + } + return; + } + + $ifn = $this->interposeFn; + // Interpose the interposeFn into the handler stack. + foreach ($this->steps as $stepName => $step) { + foreach ($step as $fn) { + $this->sorted[] = $ifn($stepName, $fn[1]); + $this->sorted[] = $fn[0]; + } + } + } + + private function removeByName($name) + { + if (!isset($this->named[$name])) { + return; + } + + $this->sorted = null; + $step = $this->named[$name]; + $this->steps[$step] = array_values( + array_filter( + $this->steps[$step], + function ($tuple) use ($name) { + return $tuple[1] !== $name; + } + ) + ); + } + + private function removeByInstance(callable $fn) + { + foreach ($this->steps as $k => $step) { + foreach ($step as $j => $tuple) { + if ($tuple[0] === $fn) { + $this->sorted = null; + unset($this->named[$this->steps[$k][$j][1]]); + unset($this->steps[$k][$j]); + } + } + } + } + + /** + * Add a middleware to a step. + * + * @param string $step Middleware step. + * @param string $name Middleware name. + * @param callable $middleware Middleware function to add. + * @param bool $prepend Prepend instead of append. + */ + private function add($step, $name, callable $middleware, $prepend = false) + { + $this->sorted = null; + + if ($prepend) { + $this->steps[$step][] = [$middleware, $name]; + } else { + array_unshift($this->steps[$step], [$middleware, $name]); + } + + if ($name) { + $this->named[$name] = $step; + } + } +} diff --git a/3rdparty/aws/aws-sdk-php/src/HasDataTrait.php b/3rdparty/aws/aws-sdk-php/src/HasDataTrait.php new file mode 100644 index 00000000..5910fff5 --- /dev/null +++ b/3rdparty/aws/aws-sdk-php/src/HasDataTrait.php @@ -0,0 +1,81 @@ +data); + } + + /** + * This method returns a reference to the variable to allow for indirect + * array modification (e.g., $foo['bar']['baz'] = 'qux'). + * + * @param $offset + * + * @return mixed|null + */ + #[\ReturnTypeWillChange] + public function & offsetGet($offset) + { + if (isset($this->data[$offset])) { + return $this->data[$offset]; + } + + $value = null; + return $value; + } + + /** + * @return void + */ + #[\ReturnTypeWillChange] + public function offsetSet($offset, $value) + { + $this->data[$offset] = $value; + } + + /** + * @return bool + */ + #[\ReturnTypeWillChange] + public function offsetExists($offset) + { + return isset($this->data[$offset]); + } + + /** + * @return void + */ + #[\ReturnTypeWillChange] + public function offsetUnset($offset) + { + unset($this->data[$offset]); + } + + public function toArray() + { + return $this->data; + } + + /** + * @return int + */ + #[\ReturnTypeWillChange] + public function count() + { + return count($this->data); + } +} diff --git a/3rdparty/aws/aws-sdk-php/src/HasMonitoringEventsTrait.php b/3rdparty/aws/aws-sdk-php/src/HasMonitoringEventsTrait.php new file mode 100644 index 00000000..b28f0a46 --- /dev/null +++ b/3rdparty/aws/aws-sdk-php/src/HasMonitoringEventsTrait.php @@ -0,0 +1,39 @@ +monitoringEvents; + } + + /** + * Prepend a client-side monitoring event to this object's event list + * + * @param array $event + */ + public function prependMonitoringEvent(array $event) + { + array_unshift($this->monitoringEvents, $event); + } + + /** + * Append a client-side monitoring event to this object's event list + * + * @param array $event + */ + public function appendMonitoringEvent(array $event) + { + $this->monitoringEvents []= $event; + } +} diff --git a/3rdparty/aws/aws-sdk-php/src/HashInterface.php b/3rdparty/aws/aws-sdk-php/src/HashInterface.php new file mode 100644 index 00000000..6304e4df --- /dev/null +++ b/3rdparty/aws/aws-sdk-php/src/HashInterface.php @@ -0,0 +1,27 @@ +stream = $stream; + $this->hash = $hash; + $this->callback = $onComplete; + } + + public function read($length): string + { + $data = $this->stream->read($length); + $this->hash->update($data); + if ($this->eof()) { + $result = $this->hash->complete(); + if ($this->callback) { + call_user_func($this->callback, $result); + } + } + + return $data; + } + + public function seek($offset, $whence = SEEK_SET): void + { + // Seeking arbitrarily is not supported. + if ($offset !== 0) { + return; + } + + $this->hash->reset(); + $this->stream->seek($offset); + } +} diff --git a/3rdparty/aws/aws-sdk-php/src/History.php b/3rdparty/aws/aws-sdk-php/src/History.php new file mode 100644 index 00000000..9f8a3bf0 --- /dev/null +++ b/3rdparty/aws/aws-sdk-php/src/History.php @@ -0,0 +1,161 @@ +maxEntries = $maxEntries; + } + + /** + * @return int + */ + #[\ReturnTypeWillChange] + public function count() + { + return count($this->entries); + } + + #[\ReturnTypeWillChange] + public function getIterator() + { + return new \ArrayIterator(array_values($this->entries)); + } + + /** + * Get the last finished command seen by the history container. + * + * @return CommandInterface + * @throws \LogicException if no commands have been seen. + */ + public function getLastCommand() + { + if (!$this->entries) { + throw new \LogicException('No commands received'); + } + + return end($this->entries)['command']; + } + + /** + * Get the last finished request seen by the history container. + * + * @return RequestInterface + * @throws \LogicException if no requests have been seen. + */ + public function getLastRequest() + { + if (!$this->entries) { + throw new \LogicException('No requests received'); + } + + return end($this->entries)['request']; + } + + /** + * Get the last received result or exception. + * + * @return ResultInterface|AwsException + * @throws \LogicException if no return values have been received. + */ + public function getLastReturn() + { + if (!$this->entries) { + throw new \LogicException('No entries'); + } + + $last = end($this->entries); + + if (isset($last['result'])) { + return $last['result']; + } + + if (isset($last['exception'])) { + return $last['exception']; + } + + throw new \LogicException('No return value for last entry.'); + } + + /** + * Initiate an entry being added to the history. + * + * @param CommandInterface $cmd Command be executed. + * @param RequestInterface $req Request being sent. + * + * @return string Returns the ticket used to finish the entry. + */ + public function start(CommandInterface $cmd, RequestInterface $req) + { + $ticket = uniqid(); + $this->entries[$ticket] = [ + 'command' => $cmd, + 'request' => $req, + 'result' => null, + 'exception' => null, + ]; + + return $ticket; + } + + /** + * Finish adding an entry to the history container. + * + * @param string $ticket Ticket returned from the start call. + * @param mixed $result The result (an exception or AwsResult). + */ + public function finish($ticket, $result) + { + if (!isset($this->entries[$ticket])) { + throw new \InvalidArgumentException('Invalid history ticket'); + } + + if (isset($this->entries[$ticket]['result']) + || isset($this->entries[$ticket]['exception']) + ) { + throw new \LogicException('History entry is already finished'); + } + + if ($result instanceof \Exception) { + $this->entries[$ticket]['exception'] = $result; + } else { + $this->entries[$ticket]['result'] = $result; + } + + if (count($this->entries) >= $this->maxEntries) { + $this->entries = array_slice($this->entries, -$this->maxEntries, null, true); + } + } + + /** + * Flush the history + */ + public function clear() + { + $this->entries = []; + } + + /** + * Converts the history to an array. + * + * @return array + */ + public function toArray() + { + return array_values($this->entries); + } +} diff --git a/3rdparty/aws/aws-sdk-php/src/IdempotencyTokenMiddleware.php b/3rdparty/aws/aws-sdk-php/src/IdempotencyTokenMiddleware.php new file mode 100644 index 00000000..c87d0bba --- /dev/null +++ b/3rdparty/aws/aws-sdk-php/src/IdempotencyTokenMiddleware.php @@ -0,0 +1,120 @@ +bytesGenerator = $bytesGenerator + ?: $this->findCompatibleRandomSource(); + $this->service = $service; + $this->nextHandler = $nextHandler; + } + + public function __invoke( + CommandInterface $command, + ?RequestInterface $request = null + ) { + $handler = $this->nextHandler; + if ($this->bytesGenerator) { + $operation = $this->service->getOperation($command->getName()); + $members = $operation->getInput()->getMembers(); + foreach ($members as $member => $value) { + if ($value['idempotencyToken']) { + $bytes = call_user_func($this->bytesGenerator, 16); + // populating UUIDv4 only when the parameter is not set + $command[$member] = $command[$member] + ?: $this->getUuidV4($bytes); + // only one member could have the trait enabled + break; + } + } + } + return $handler($command, $request); + } + + /** + * This function generates a random UUID v4 string, + * which is used as auto filled token value. + * + * @param string $bytes 16 bytes of pseudo-random bytes + * @return string + * More information about UUID v4, see: + * https://en.wikipedia.org/wiki/Universally_unique_identifier#Version_4_.28random.29 + * https://tools.ietf.org/html/rfc4122#page-14 + */ + private static function getUuidV4($bytes) + { + // set version to 0100 + $bytes[6] = chr(ord($bytes[6]) & 0x0f | 0x40); + // set bits 6-7 to 10 + $bytes[8] = chr(ord($bytes[8]) & 0x3f | 0x80); + return vsprintf('%s%s-%s-%s-%s-%s%s%s', str_split(bin2hex($bytes), 4)); + } + + /** + * This function decides the PHP function used in generating random bytes. + * + * @return callable|null + */ + private function findCompatibleRandomSource() + { + if (function_exists('random_bytes')) { + return 'random_bytes'; + } + + if (function_exists('openssl_random_pseudo_bytes')) { + return 'openssl_random_pseudo_bytes'; + } + + if (function_exists('mcrypt_create_iv')) { + return 'mcrypt_create_iv'; + } + } +} diff --git a/3rdparty/aws/aws-sdk-php/src/Identity/AwsCredentialIdentity.php b/3rdparty/aws/aws-sdk-php/src/Identity/AwsCredentialIdentity.php new file mode 100644 index 00000000..f7971126 --- /dev/null +++ b/3rdparty/aws/aws-sdk-php/src/Identity/AwsCredentialIdentity.php @@ -0,0 +1,19 @@ +cache = new LruArrayCache(100); + $this->region = $clientRegion; + $this->config = $config; + } + + public function __invoke($command) + { + $s3Client = $this->getS3Client(); + $bucket = $command['Bucket']; + if ($identity = $this->cache->get($bucket)) { + if (!$identity->isExpired()) { + return Promise\Create::promiseFor($identity); + } + } + $response = $s3Client->createSession(['Bucket' => $bucket]); + $identity = new Aws\Identity\S3\S3ExpressIdentity( + $response['Credentials']['AccessKeyId'], + $response['Credentials']['SecretAccessKey'], + $response['Credentials']['SessionToken'], + $response['Credentials']['Expiration']->getTimestamp() + ); + $this->cache->set($bucket, $identity); + return Promise\Create::promiseFor($identity); + } + + private function getS3Client() + { + if (is_null($this->s3Client)) { + $this->s3Client = $this->config['client'] + ?? new Aws\S3\S3Client([ + 'region' => $this->region, + 'disable_express_session_auth' => true + ]); + } + return $this->s3Client; + } +} diff --git a/3rdparty/aws/aws-sdk-php/src/InputValidationMiddleware.php b/3rdparty/aws/aws-sdk-php/src/InputValidationMiddleware.php new file mode 100644 index 00000000..0c5a24dc --- /dev/null +++ b/3rdparty/aws/aws-sdk-php/src/InputValidationMiddleware.php @@ -0,0 +1,74 @@ +service = $service; + $this->nextHandler = $nextHandler; + $this->mandatoryAttributeList = $mandatoryAttributeList; + } + + public function __invoke(CommandInterface $cmd) { + $nextHandler = $this->nextHandler; + $op = $this->service->getOperation($cmd->getName())->toArray(); + if (!empty($op['input']['shape'])) { + $service = $this->service->toArray(); + if (!empty($input = $service['shapes'][$op['input']['shape']])) { + if (!empty($input['required'])) { + foreach ($input['required'] as $key => $member) { + if (in_array($member, $this->mandatoryAttributeList)) { + $argument = is_string($cmd[$member]) ? trim($cmd[$member]) : $cmd[$member]; + if ($argument === '' || $argument === null) { + $commandName = $cmd->getName(); + throw new \InvalidArgumentException( + "The {$commandName} operation requires non-empty parameter: {$member}" + ); + } + } + } + } + } + } + return $nextHandler($cmd); + } +} diff --git a/3rdparty/aws/aws-sdk-php/src/JsonCompiler.php b/3rdparty/aws/aws-sdk-php/src/JsonCompiler.php new file mode 100644 index 00000000..5358b315 --- /dev/null +++ b/3rdparty/aws/aws-sdk-php/src/JsonCompiler.php @@ -0,0 +1,25 @@ +maxItems = $maxItems; + } + + public function get($key) + { + if (!isset($this->items[$key])) { + return null; + } + + $entry = $this->items[$key]; + + // Ensure the item is not expired. + if (!$entry[1] || time() < $entry[1]) { + // LRU: remove the item and push it to the end of the array. + unset($this->items[$key]); + $this->items[$key] = $entry; + return $entry[0]; + } + + unset($this->items[$key]); + return null; + } + + public function set($key, $value, $ttl = 0) + { + // Only call time() if the TTL is not 0/false/null + $ttl = $ttl ? time() + $ttl : 0; + $this->items[$key] = [$value, $ttl]; + + // Determine if there are more items in the cache than allowed. + $diff = count($this->items) - $this->maxItems; + + // Clear out least recently used items. + if ($diff > 0) { + // Reset to the beginning of the array and begin unsetting. + reset($this->items); + for ($i = 0; $i < $diff; $i++) { + unset($this->items[key($this->items)]); + next($this->items); + } + } + } + + public function remove($key) + { + unset($this->items[$key]); + } + + /** + * @return int + */ + #[\ReturnTypeWillChange] + public function count() + { + return count($this->items); + } +} diff --git a/3rdparty/aws/aws-sdk-php/src/MetricsBuilder.php b/3rdparty/aws/aws-sdk-php/src/MetricsBuilder.php new file mode 100644 index 00000000..2d634cf5 --- /dev/null +++ b/3rdparty/aws/aws-sdk-php/src/MetricsBuilder.php @@ -0,0 +1,480 @@ +metrics = []; + // The first metrics does not include the separator + // therefore it is reduced by default. + $this->metricsSize = -(strlen(self::$METRIC_SEPARATOR)); + } + + /** + * Build the metrics string value. + * + * @return string + */ + public function build(): string + { + if (empty($this->metrics)) { + return ""; + } + + return $this->encode(); + } + + /** + * Encodes the metrics by separating each metric + * with a comma. Example: for the metrics[A,B,C] then + * the output would be "A,B,C". + * + * @return string + */ + private function encode(): string + { + return implode(self::$METRIC_SEPARATOR, array_keys($this->metrics)); + } + + /** + * Appends a metric to the internal metrics holder after validating it. + * Increases the current metrics size by the length of the new metric + * plus the length of the encoding separator. + * Example: $currentSize = $currentSize + len($newMetric) + len($separator) + * + * @param string $metric The metric to append. + * + * @return void + */ + public function append(string $metric): void + { + if (!$this->canMetricBeAppended($metric)) { + return; + } + + $this->metrics[$metric] = true; + $this->metricsSize += strlen($metric) + strlen(self::$METRIC_SEPARATOR); + } + + /** + * Receives a feature group and a value to identify which one is the metric. + * For example, a group could be `signature` and a value could be `v4a`, + * then the metric will be `SIGV4A_SIGNING`. + * + * @param string $featureGroup the feature group such as `signature`. + * @param mixed $value the value for identifying the metric. + * + * @return void + */ + public function identifyMetricByValueAndAppend( + string $featureGroup, + $value + ): void + { + if (empty($value)) { + return; + } + + static $appendMetricFns = [ + 'signature' => 'appendSignatureMetric', + 'request_compression' => 'appendRequestCompressionMetric', + 'request_checksum' => 'appendRequestChecksumMetric', + 'credentials' => 'appendCredentialsMetric', + 'account_id_endpoint_mode' => 'appendAccountIdEndpointMode', + 'account_id_endpoint' => 'appendAccountIdEndpoint', + 'request_checksum_calculation' => 'appendRequestChecksumCalculationMetric', + ]; + + $fn = $appendMetricFns[$featureGroup]; + $this->{$fn}($value); + } + + /** + * Appends the signature metric based on the signature value. + * + * @param string $signature + * + * @return void + */ + private function appendSignatureMetric(string $signature): void + { + if ($signature === 'v4-s3express') { + $this->append(self::S3_EXPRESS_BUCKET); + } elseif ($signature === 'v4a') { + $this->append(self::SIGV4A_SIGNING); + } + } + + /** + * Appends the request compression metric based on the format resolved. + * + * @param string $format + * + * @return void + */ + private function appendRequestCompressionMetric(string $format): void + { + if ($format === 'gzip') { + $this->append(self::GZIP_REQUEST_COMPRESSION); + } + } + + /** + * Appends the request checksum metric based on the algorithm. + * + * @param string $algorithm + * + * @return void + */ + private function appendRequestChecksumMetric(string $algorithm): void + { + if ($algorithm === 'crc32') { + $this->append(self::FLEXIBLE_CHECKSUMS_REQ_CRC32); + } elseif ($algorithm === 'crc32c') { + $this->append(self::FLEXIBLE_CHECKSUMS_REQ_CRC32C); + } elseif ($algorithm === 'crc64') { + $this->append(self::FLEXIBLE_CHECKSUMS_REQ_CRC64); + } elseif ($algorithm === 'sha1') { + $this->append(self::FLEXIBLE_CHECKSUMS_REQ_SHA1); + } elseif ($algorithm === 'sha256') { + $this->append(self::FLEXIBLE_CHECKSUMS_REQ_SHA256); + } + } + + + /** + * Appends the credentials metric based on the type of credentials + * resolved. + * + * @param CredentialsInterface $credentials + * + * @return void + */ + private function appendCredentialsMetric( + CredentialsInterface $credentials + ): void + { + $source = $credentials->toArray()['source'] ?? null; + if (empty($source)) { + return; + } + + static $credentialsMetricMapping = [ + CredentialSources::STATIC => + self::CREDENTIALS_CODE, + CredentialSources::ENVIRONMENT => + self::CREDENTIALS_ENV_VARS, + CredentialSources::ENVIRONMENT_STS_WEB_ID_TOKEN => + self::CREDENTIALS_ENV_VARS_STS_WEB_ID_TOKEN, + CredentialSources::STS_ASSUME_ROLE => + self::CREDENTIALS_STS_ASSUME_ROLE, + CredentialSources::STS_WEB_ID_TOKEN => + self::CREDENTIALS_STS_ASSUME_ROLE_WEB_ID, + CredentialSources::PROFILE => + self::CREDENTIALS_PROFILE, + CredentialSources::IMDS => + self::CREDENTIALS_IMDS, + CredentialSources::ECS => + self::CREDENTIALS_HTTP, + CredentialSources::PROFILE_STS_WEB_ID_TOKEN => + self::CREDENTIALS_PROFILE_STS_WEB_ID_TOKEN, + CredentialSources::PROFILE_PROCESS => + self::CREDENTIALS_PROFILE_PROCESS, + CredentialSources::PROFILE_SSO => + self::CREDENTIALS_PROFILE_SSO, + CredentialSources::PROFILE_SSO_LEGACY => + self::CREDENTIALS_PROFILE_SSO_LEGACY, + ]; + if (isset($credentialsMetricMapping[$source])) { + $this->append($credentialsMetricMapping[$source]); + } + } + + private function appendRequestChecksumCalculationMetric( + string $checkSumCalculation + ): void + { + static $checksumCalculationMetricMapping = [ + 'when_supported' => self::FLEXIBLE_CHECKSUMS_REQ_WHEN_SUPPORTED, + 'when_required' => self::FLEXIBLE_CHECKSUMS_REQ_WHEN_REQUIRED, + ]; + + if (isset($checksumCalculationMetricMapping[$checkSumCalculation])) { + $this->append($checksumCalculationMetricMapping[$checkSumCalculation]); + } + } + + /** + * Appends the account_id_endpoint_mode metrics based on + * the value resolved. + * + * @param string $accountIdEndpointMode + * + * @return void + */ + private function appendAccountIdEndpointMode( + string $accountIdEndpointMode + ): void + { + if (empty($accountIdEndpointMode)) { + return; + } + + if ($accountIdEndpointMode === 'preferred') { + $this->append(self::ACCOUNT_ID_MODE_PREFERRED); + } elseif ($accountIdEndpointMode === 'disabled') { + $this->append(self::ACCOUNT_ID_MODE_DISABLED); + } elseif ($accountIdEndpointMode === 'required') { + $this->append(self::ACCOUNT_ID_MODE_REQUIRED); + } + } + + /** + * Appends the account_id_endpoint metric whenever a resolved endpoint + * matches an account_id endpoint pattern which also defined here. + * + * @param string $endpoint + * + * @return void + */ + private function appendAccountIdEndpoint(string $endpoint): void + { + static $pattern = "/(https|http):\\/\\/\\d{12}\\.ddb/"; + if (preg_match($pattern, $endpoint)) { + $this->append(self::ACCOUNT_ID_ENDPOINT); + } + } + + /** + * Resolves metrics from client arguments. + * + * @param array $args + * + * @return void + */ + public function resolveAndAppendFromArgs(array $args = []): void + { + static $metricsFnList = [ + 'appendEndpointMetric', + 'appendRetryConfigMetric', + 'appendResponseChecksumValidationMetric', + ]; + foreach ($metricsFnList as $metricFn) { + $this->{$metricFn}($args); + } + } + + /** + * Appends the endpoint metric into the metrics builder, + * just if a custom endpoint was provided at client construction. + * + * @param array $args + * + * @return void + */ + private function appendEndpointMetric(array $args): void + { + if (!empty($args['endpoint_override'])) { + $this->append(MetricsBuilder::ENDPOINT_OVERRIDE); + } + } + + /** + * Appends the retry mode metric into the metrics builder, + * based on the resolved retry config mode. + * + * @param array $args + * + * @return void + */ + private function appendRetryConfigMetric(array $args): void + { + $retries = $args['retries'] ?? null; + if ($retries === null) { + return; + } + + $retryMode = ''; + if ($retries instanceof \Aws\Retry\Configuration) { + $retryMode = $retries->getMode(); + } elseif (is_array($retries) + && isset($retries["mode"]) + ) { + $retryMode = $retries["mode"]; + } + + if ($retryMode === 'legacy') { + $this->append( + MetricsBuilder::RETRY_MODE_LEGACY + ); + } elseif ($retryMode === 'standard') { + $this->append( + MetricsBuilder::RETRY_MODE_STANDARD + ); + } elseif ($retryMode === 'adaptive') { + $this->append( + MetricsBuilder::RETRY_MODE_ADAPTIVE + ); + } + } + + /** + * Appends the provided/resolved response checksum validation mode. + * + * @param array $args + * + * @return void + */ + private function appendResponseChecksumValidationMetric(array $args): void + { + if (empty($args['response_checksum_validation'])) { + return; + } + + $checksumValidation = $args['response_checksum_validation']; + static $checksumValidationMetricMapping = [ + 'when_supported' => MetricsBuilder::FLEXIBLE_CHECKSUMS_RES_WHEN_SUPPORTED, + 'when_required' => MetricsBuilder::FLEXIBLE_CHECKSUMS_RES_WHEN_REQUIRED, + ]; + + if (isset($checksumValidationMetricMapping[$checksumValidation])) { + $this->append($checksumValidationMetricMapping[$checksumValidation]); + } + } + + /** + * Validates if a metric can be appended by ensuring the total size, + * including the new metric and separator, does not exceed the limit. + * Also checks that the metric does not already exist. + * Example: Appendable if: + * $currentSize + len($newMetric) + len($separator) <= MAX_SIZE + * and: + * $newMetric not in $existingMetrics + * + * @param string $newMetric The metric to validate. + * + * @return bool True if the metric can be appended, false otherwise. + */ + private function canMetricBeAppended(string $newMetric): bool + { + if ($newMetric === "") { + return false; + } + + if ($this->metricsSize + + (strlen($newMetric) + strlen(self::$METRIC_SEPARATOR)) + > self::$MAX_METRICS_SIZE + ) { + return false; + } + + if (isset($this->metrics[$newMetric])) { + return false; + } + + return true; + } + + /** + * Returns the metrics builder from the property @context of a command. + * + * @param Command $command + * + * @return MetricsBuilder + */ + public static function fromCommand(CommandInterface $command): MetricsBuilder + { + return $command->getMetricsBuilder(); + } + + /** + * Helper method for appending a metrics capture middleware into a + * handler stack given. The middleware appended here is on top of the + * build step. + * + * @param HandlerList $handlerList + * @param $metric + * + * @return void + */ + public static function appendMetricsCaptureMiddleware( + HandlerList $handlerList, + $metric + ): void + { + $middlewareName = 'metrics-capture-'.$metric; + if (!$handlerList->hasMiddleware($middlewareName)) { + $handlerList->appendBuild( + Middleware::tap( + function (CommandInterface $command) use ($metric) { + self::fromCommand($command)->append( + $metric + ); + } + ), + $middlewareName + ); + } + } +} diff --git a/3rdparty/aws/aws-sdk-php/src/Middleware.php b/3rdparty/aws/aws-sdk-php/src/Middleware.php new file mode 100644 index 00000000..edfddf2c --- /dev/null +++ b/3rdparty/aws/aws-sdk-php/src/Middleware.php @@ -0,0 +1,466 @@ +getOperation($command->getName()); + $source = $command[$sourceParameter]; + + if ($source !== null + && $operation->getInput()->hasMember($bodyParameter) + ) { + $lazyOpenStream = new LazyOpenStream($source, 'r'); + $command[$bodyParameter] = $lazyOpenStream; + unset($command[$sourceParameter]); + + $next = $handler($command, $request); + // To avoid failures in some tests cases + if ($next !== null && method_exists($next, 'then')) { + return $next->then( + function ($result) use ($lazyOpenStream) { + // To make sure the resource is closed. + $lazyOpenStream->close(); + + return $result; + } + )->otherwise(function (\Throwable $e) use ($lazyOpenStream) { + $lazyOpenStream->close(); + + throw $e; + }); + } + + return $next; + } + + return $handler($command, $request); + }; + }; + } + + /** + * Adds a middleware that uses client-side validation. + * + * @param Service $api API being accessed. + * + * @return callable + */ + public static function validation(Service $api, ?Validator $validator = null) + { + $validator = $validator ?: new Validator(); + return function (callable $handler) use ($api, $validator) { + return function ( + CommandInterface $command, + ?RequestInterface $request = null + ) use ($api, $validator, $handler) { + if ($api->isModifiedModel()) { + $api = new Service( + $api->getDefinition(), + $api->getProvider() + ); + } + $operation = $api->getOperation($command->getName()); + $validator->validate( + $command->getName(), + $operation->getInput(), + $command->toArray() + ); + return $handler($command, $request); + }; + }; + } + + /** + * Builds an HTTP request for a command. + * + * @param callable $serializer Function used to serialize a request for a + * command. + * @param EndpointProviderV2 | null $endpointProvider + * @param array $providerArgs + * @return callable + */ + public static function requestBuilder($serializer) + { + return function (callable $handler) use ($serializer) { + return function (CommandInterface $command, $endpoint = null) use ($serializer, $handler) { + return $handler($command, $serializer($command, $endpoint)); + }; + }; + } + + /** + * Creates a middleware that signs requests for a command. + * + * @param callable $credProvider Credentials provider function that + * returns a promise that is resolved + * with a CredentialsInterface object. + * @param callable $signatureFunction Function that accepts a Command + * object and returns a + * SignatureInterface. + * + * @return callable + */ + public static function signer(callable $credProvider, callable $signatureFunction, $tokenProvider = null, $config = []) + { + return function (callable $handler) use ($signatureFunction, $credProvider, $tokenProvider, $config) { + return function ( + CommandInterface $command, + RequestInterface $request + ) use ($handler, $signatureFunction, $credProvider, $tokenProvider, $config) { + $signer = $signatureFunction($command); + if ($signer instanceof TokenAuthorization) { + return $tokenProvider()->then( + function (TokenInterface $token) + use ($handler, $command, $signer, $request) { + return $handler( + $command, + $signer->authorizeRequest($request, $token) + ); + } + ); + } + + if ($signer instanceof S3ExpressSignature) { + $credentialPromise = $config['s3_express_identity_provider']($command); + } else { + $credentialPromise = $credProvider(); + } + + return $credentialPromise->then( + function (CredentialsInterface $creds) + use ($handler, $command, $signer, $request) { + // Capture credentials metric + $command->getMetricsBuilder()->identifyMetricByValueAndAppend( + 'credentials', + $creds + ); + + return $handler( + $command, + $signer->signRequest($request, $creds) + ); + } + ); + }; + }; + } + + /** + * Creates a middleware that invokes a callback at a given step. + * + * The tap callback accepts a CommandInterface and RequestInterface as + * arguments but is not expected to return a new value or proxy to + * downstream middleware. It's simply a way to "tap" into the handler chain + * to debug or get an intermediate value. + * + * @param callable $fn Tap function + * + * @return callable + */ + public static function tap(callable $fn) + { + return function (callable $handler) use ($fn) { + return function ( + CommandInterface $command, + ?RequestInterface $request = null + ) use ($handler, $fn) { + $fn($command, $request); + return $handler($command, $request); + }; + }; + } + + /** + * Middleware wrapper function that retries requests based on the boolean + * result of invoking the provided "decider" function. + * + * If no delay function is provided, a simple implementation of exponential + * backoff will be utilized. + * + * @param callable $decider Function that accepts the number of retries, + * a request, [result], and [exception] and + * returns true if the command is to be retried. + * @param callable $delay Function that accepts the number of retries and + * returns the number of milliseconds to delay. + * @param bool $stats Whether to collect statistics on retries and the + * associated delay. + * + * @return callable + */ + public static function retry( + ?callable $decider = null, + ?callable $delay = null, + $stats = false + ) { + $decider = $decider ?: RetryMiddleware::createDefaultDecider(); + $delay = $delay ?: [RetryMiddleware::class, 'exponentialDelay']; + + return function (callable $handler) use ($decider, $delay, $stats) { + return new RetryMiddleware($decider, $delay, $handler, $stats); + }; + } + /** + * Middleware wrapper function that adds an invocation id header to + * requests, which is only applied after the build step. + * + * This is a uniquely generated UUID to identify initial and subsequent + * retries as part of a complete request lifecycle. + * + * @return callable + */ + public static function invocationId() + { + return function (callable $handler) { + return function ( + CommandInterface $command, + RequestInterface $request + ) use ($handler){ + return $handler($command, $request->withHeader( + 'aws-sdk-invocation-id', + md5(uniqid(gethostname(), true)) + )); + }; + }; + } + /** + * Middleware wrapper function that adds a Content-Type header to requests. + * This is only done when the Content-Type has not already been set, and the + * request body's URI is available. It then checks the file extension of the + * URI to determine the mime-type. + * + * @param array $operations Operations that Content-Type should be added to. + * + * @return callable + */ + public static function contentType(array $operations) + { + return function (callable $handler) use ($operations) { + return function ( + CommandInterface $command, + ?RequestInterface $request = null + ) use ($handler, $operations) { + if (!$request->hasHeader('Content-Type') + && in_array($command->getName(), $operations, true) + && ($uri = $request->getBody()->getMetadata('uri')) + ) { + $request = $request->withHeader( + 'Content-Type', + Psr7\MimeType::fromFilename($uri) ?: 'application/octet-stream' + ); + } + + return $handler($command, $request); + }; + }; + } + /** + * Middleware wrapper function that adds a trace id header to requests + * from clients instantiated in supported Lambda runtime environments. + * + * The purpose for this header is to track and stop Lambda functions + * from being recursively invoked due to misconfigured resources. + * + * @return callable + */ + public static function recursionDetection() + { + return function (callable $handler) { + return function ( + CommandInterface $command, + RequestInterface $request + ) use ($handler){ + $isLambda = getenv('AWS_LAMBDA_FUNCTION_NAME'); + $traceId = str_replace('\e', '\x1b', getenv('_X_AMZN_TRACE_ID')); + + if ($isLambda && $traceId) { + if (!$request->hasHeader('X-Amzn-Trace-Id')) { + $ignoreChars = ['=', ';', ':', '+', '&', '[', ']', '{', '}', '"', '\'', ',']; + $traceIdEncoded = rawurlencode(stripcslashes($traceId)); + + foreach($ignoreChars as $char) { + $encodedChar = rawurlencode($char); + $traceIdEncoded = str_replace($encodedChar, $char, $traceIdEncoded); + } + + return $handler($command, $request->withHeader( + 'X-Amzn-Trace-Id', + $traceIdEncoded + )); + } + } + return $handler($command, $request); + }; + }; + } + /** + * Tracks command and request history using a history container. + * + * This is useful for testing. + * + * @param History $history History container to store entries. + * + * @return callable + */ + public static function history(History $history) + { + return function (callable $handler) use ($history) { + return function ( + CommandInterface $command, + ?RequestInterface $request = null + ) use ($handler, $history) { + $ticket = $history->start($command, $request); + return $handler($command, $request) + ->then( + function ($result) use ($history, $ticket) { + $history->finish($ticket, $result); + return $result; + }, + function ($reason) use ($history, $ticket) { + $history->finish($ticket, $reason); + return Promise\Create::rejectionFor($reason); + } + ); + }; + }; + } + + /** + * Creates a middleware that applies a map function to requests as they + * pass through the middleware. + * + * @param callable $f Map function that accepts a RequestInterface and + * returns a RequestInterface. + * + * @return callable + */ + public static function mapRequest(callable $f) + { + return function (callable $handler) use ($f) { + return function ( + CommandInterface $command, + ?RequestInterface $request = null + ) use ($handler, $f) { + return $handler($command, $f($request)); + }; + }; + } + + /** + * Creates a middleware that applies a map function to commands as they + * pass through the middleware. + * + * @param callable $f Map function that accepts a command and returns a + * command. + * + * @return callable + */ + public static function mapCommand(callable $f) + { + return function (callable $handler) use ($f) { + return function ( + CommandInterface $command, + ?RequestInterface $request = null + ) use ($handler, $f) { + return $handler($f($command), $request); + }; + }; + } + + /** + * Creates a middleware that applies a map function to results. + * + * @param callable $f Map function that accepts an Aws\ResultInterface and + * returns an Aws\ResultInterface. + * + * @return callable + */ + public static function mapResult(callable $f) + { + return function (callable $handler) use ($f) { + return function ( + CommandInterface $command, + ?RequestInterface $request = null + ) use ($handler, $f) { + return $handler($command, $request)->then($f); + }; + }; + } + + public static function timer() + { + return function (callable $handler) { + return function ( + CommandInterface $command, + ?RequestInterface $request = null + ) use ($handler) { + $start = microtime(true); + return $handler($command, $request) + ->then( + function (ResultInterface $res) use ($start) { + if (!isset($res['@metadata'])) { + $res['@metadata'] = []; + } + if (!isset($res['@metadata']['transferStats'])) { + $res['@metadata']['transferStats'] = []; + } + + $res['@metadata']['transferStats']['total_time'] + = microtime(true) - $start; + + return $res; + }, + function ($err) use ($start) { + if ($err instanceof AwsException) { + $err->setTransferInfo([ + 'total_time' => microtime(true) - $start, + ] + $err->getTransferInfo()); + } + return Promise\Create::rejectionFor($err); + } + ); + }; + }; + } +} diff --git a/3rdparty/aws/aws-sdk-php/src/MockHandler.php b/3rdparty/aws/aws-sdk-php/src/MockHandler.php new file mode 100644 index 00000000..7d6b4539 --- /dev/null +++ b/3rdparty/aws/aws-sdk-php/src/MockHandler.php @@ -0,0 +1,148 @@ +queue = []; + $this->onFulfilled = $onFulfilled; + $this->onRejected = $onRejected; + + if ($resultOrQueue) { + call_user_func_array([$this, 'append'], array_values($resultOrQueue)); + } + } + + /** + * Adds one or more variadic ResultInterface or AwsException objects to the + * queue. + */ + public function append() + { + foreach (func_get_args() as $value) { + if ($value instanceof ResultInterface + || $value instanceof Exception + || is_callable($value) + ) { + $this->queue[] = $value; + } else { + throw new \InvalidArgumentException('Expected an Aws\ResultInterface or Exception.'); + } + } + } + + /** + * Adds one or more \Exception or \Throwable to the queue + */ + public function appendException() + { + foreach (func_get_args() as $value) { + if ($value instanceof \Exception || $value instanceof \Throwable) { + $this->queue[] = $value; + } else { + throw new \InvalidArgumentException('Expected an \Exception or \Throwable.'); + } + } + } + + public function __invoke( + CommandInterface $command, + RequestInterface $request + ) { + if (!$this->queue) { + $last = $this->lastCommand + ? ' The last command sent was ' . $this->lastCommand->getName() . '.' + : ''; + throw new \RuntimeException('Mock queue is empty. Trying to send a ' + . $command->getName() . ' command failed.' . $last); + } + + $this->lastCommand = $command; + $this->lastRequest = $request; + + $result = array_shift($this->queue); + + if (is_callable($result)) { + $result = $result($command, $request); + } + + if ($result instanceof \Exception) { + $result = new RejectedPromise($result); + } else { + // Add an effective URI and statusCode if not present. + $meta = $result['@metadata']; + if (!isset($meta['effectiveUri'])) { + $meta['effectiveUri'] = (string) $request->getUri(); + } + if (!isset($meta['statusCode'])) { + $meta['statusCode'] = 200; + } + $result['@metadata'] = $meta; + $result = Promise\Create::promiseFor($result); + } + + $result->then($this->onFulfilled, $this->onRejected); + + return $result; + } + + /** + * Get the last received request. + * + * @return RequestInterface|null + */ + public function getLastRequest() + { + return $this->lastRequest; + } + + /** + * Get the last received command. + * + * @return CommandInterface + */ + public function getLastCommand() + { + return $this->lastCommand; + } + + /** + * Returns the number of remaining items in the queue. + * + * @return int + */ + #[\ReturnTypeWillChange] + public function count() + { + return count($this->queue); + } +} diff --git a/3rdparty/aws/aws-sdk-php/src/MonitoringEventsInterface.php b/3rdparty/aws/aws-sdk-php/src/MonitoringEventsInterface.php new file mode 100644 index 00000000..662927fe --- /dev/null +++ b/3rdparty/aws/aws-sdk-php/src/MonitoringEventsInterface.php @@ -0,0 +1,30 @@ + true, 'region' => true] + ); + $args['region']['required'] = false; + unset($args['region']['fn']); + unset($args['region']['default']); + + return $args + [ + 'client_factory' => [ + 'type' => 'config', + 'valid' => ['callable'], + 'doc' => 'A callable that takes an array of client' + . ' configuration arguments and returns a regionalized' + . ' client.', + 'required' => true, + 'internal' => true, + 'default' => function (array $args) { + $namespace = manifest($args['service'])['namespace']; + $klass = "Aws\\{$namespace}\\{$namespace}Client"; + $region = isset($args['region']) ? $args['region'] : null; + + return function (array $args) use ($klass, $region) { + if ($region && empty($args['region'])) { + $args['region'] = $region; + } + + return new $klass($args); + }; + }, + ], + 'partition' => [ + 'type' => 'config', + 'valid' => ['string', PartitionInterface::class], + 'doc' => 'AWS partition to connect to. Valid partitions' + . ' include "aws," "aws-cn," and "aws-us-gov." Used to' + . ' restrict the scope of the mapRegions method.', + 'default' => function (array $args) { + $region = isset($args['region']) ? $args['region'] : ''; + return PartitionEndpointProvider::defaultProvider() + ->getPartition($region, $args['service']); + }, + 'fn' => function ($value, array &$args) { + if (is_string($value)) { + $value = PartitionEndpointProvider::defaultProvider() + ->getPartitionByName($value); + } + + if (!$value instanceof PartitionInterface) { + throw new \InvalidArgumentException('No valid partition' + . ' was provided. Provide a concrete partition or' + . ' the name of a partition (e.g., "aws," "aws-cn,"' + . ' or "aws-us-gov").' + ); + } + $ruleset = EndpointDefinitionProvider::getEndpointRuleset( + $args['service'], + isset($args['version']) ? $args['version'] : 'latest' + ); + $partitions = EndpointDefinitionProvider::getPartitions(); + $args['endpoint_provider'] = new EndpointProviderV2($ruleset, $partitions); + } + ], + ]; + } + + /** + * The multi-region client constructor accepts the following options: + * + * - client_factory: (callable) An optional callable that takes an array of + * client configuration arguments and returns a regionalized client. + * - partition: (Aws\Endpoint\Partition|string) AWS partition to connect to. + * Valid partitions include "aws," "aws-cn," and "aws-us-gov." Used to + * restrict the scope of the mapRegions method. + * - region: (string) Region to connect to when no override is provided. + * Used to create the default client factory and determine the appropriate + * AWS partition when present. + * + * @param array $args Client configuration arguments. + */ + public function __construct(array $args = []) + { + if (!isset($args['service'])) { + $args['service'] = $this->parseClass(); + } + + $this->handlerList = new HandlerList(function ( + CommandInterface $command + ) { + list($region, $args) = $this->getRegionFromArgs($command->toArray()); + $command = $this->getClientFromPool($region) + ->getCommand($command->getName(), $args); + + if ($this->isUseCustomHandler()) { + $command->getHandlerList()->setHandler($this->customHandler); + } + + return $this->executeAsync($command); + }); + + $argDefinitions = static::getArguments(); + $resolver = new ClientResolver($argDefinitions); + $args = $resolver->resolve($args, $this->handlerList); + $this->config = $args['config']; + $this->factory = $args['client_factory']; + $this->partition = $args['partition']; + $this->args = array_diff_key($args, $args['config']); + } + + /** + * Get the region to which the client is configured to send requests by + * default. + * + * @return string + */ + public function getRegion() + { + return $this->getClientFromPool()->getRegion(); + } + + /** + * Create a command for an operation name. + * + * Special keys may be set on the command to control how it behaves, + * including: + * + * - @http: Associative array of transfer specific options to apply to the + * request that is serialized for this command. Available keys include + * "proxy", "verify", "timeout", "connect_timeout", "debug", "delay", and + * "headers". + * - @region: The region to which the command should be sent. + * + * @param string $name Name of the operation to use in the command + * @param array $args Arguments to pass to the command + * + * @return CommandInterface + * @throws \InvalidArgumentException if no command can be found by name + */ + public function getCommand($name, array $args = []) + { + return new Command($name, $args, clone $this->getHandlerList()); + } + + public function getConfig($option = null) + { + if (null === $option) { + return $this->config; + } + + if (isset($this->config[$option])) { + return $this->config[$option]; + } + + return $this->getClientFromPool()->getConfig($option); + } + + public function getCredentials() + { + return $this->getClientFromPool()->getCredentials(); + } + + public function getHandlerList() + { + return $this->handlerList; + } + + public function getApi() + { + return $this->getClientFromPool()->getApi(); + } + + public function getEndpoint() + { + return $this->getClientFromPool()->getEndpoint(); + } + + public function useCustomHandler(callable $handler) + { + $this->customHandler = $handler; + } + + private function isUseCustomHandler() + { + return isset($this->customHandler); + } + + /** + * @param string $region Omit this argument or pass in an empty string to + * allow the configured client factory to apply the + * region. + * + * @return AwsClientInterface + */ + protected function getClientFromPool($region = '') + { + if (empty($this->clientPool[$region])) { + $factory = $this->factory; + $this->clientPool[$region] = $factory( + array_replace($this->args, array_filter(['region' => $region])) + ); + } + + return $this->clientPool[$region]; + } + + /** + * Parse the class name and return the "service" name of the client. + * + * @return string + */ + private function parseClass() + { + $klass = get_class($this); + + if ($klass === __CLASS__) { + return ''; + } + + return strtolower(substr($klass, strrpos($klass, '\\') + 1, -17)); + } + + private function getRegionFromArgs(array $args) + { + $region = isset($args['@region']) + ? $args['@region'] + : $this->getRegion(); + unset($args['@region']); + + return [$region, $args]; + } +} diff --git a/3rdparty/aws/aws-sdk-php/src/Multipart/AbstractUploadManager.php b/3rdparty/aws/aws-sdk-php/src/Multipart/AbstractUploadManager.php new file mode 100644 index 00000000..adc356ed --- /dev/null +++ b/3rdparty/aws/aws-sdk-php/src/Multipart/AbstractUploadManager.php @@ -0,0 +1,328 @@ + null, + 'state' => null, + 'concurrency' => self::DEFAULT_CONCURRENCY, + 'prepare_data_source' => null, + 'before_initiate' => null, + 'before_upload' => null, + 'before_complete' => null, + 'exception_class' => MultipartUploadException::class, + ]; + + /** @var Client Client used for the upload. */ + protected $client; + + /** @var array Configuration used to perform the upload. */ + protected $config; + + /** @var array Service-specific information about the upload workflow. */ + protected $info; + + /** @var PromiseInterface Promise that represents the multipart upload. */ + protected $promise; + + /** @var UploadState State used to manage the upload. */ + protected $state; + + /** @var bool Configuration used to indicate if upload progress will be displayed. */ + protected $displayProgress; + + /** + * @param Client $client + * @param array $config + */ + public function __construct(Client $client, array $config = []) + { + $this->client = $client; + $this->info = $this->loadUploadWorkflowInfo(); + $this->config = $config + self::$defaultConfig; + $this->state = $this->determineState(); + + if (isset($config['display_progress']) + && is_bool($config['display_progress']) + ) { + $this->displayProgress = $config['display_progress']; + } + } + + /** + * Returns the current state of the upload + * + * @return UploadState + */ + public function getState() + { + return $this->state; + } + + /** + * Upload the source using multipart upload operations. + * + * @return Result The result of the CompleteMultipartUpload operation. + * @throws \LogicException if the upload is already complete or aborted. + * @throws MultipartUploadException if an upload operation fails. + */ + public function upload() + { + return $this->promise()->wait(); + } + + /** + * Upload the source asynchronously using multipart upload operations. + * + * @return PromiseInterface + */ + public function promise(): PromiseInterface + { + if ($this->promise) { + return $this->promise; + } + + return $this->promise = Promise\Coroutine::of(function () { + // Initiate the upload. + if ($this->state->isCompleted()) { + throw new \LogicException('This multipart upload has already ' + . 'been completed or aborted.' + ); + } + + if (!$this->state->isInitiated()) { + // Execute the prepare callback. + if (is_callable($this->config["prepare_data_source"])) { + $this->config["prepare_data_source"](); + } + + $result = (yield $this->execCommand('initiate', $this->getInitiateParams())); + $this->state->setUploadId( + $this->info['id']['upload_id'], + $result[$this->info['id']['upload_id']] + ); + $this->state->setStatus(UploadState::INITIATED); + } + + // Create a command pool from a generator that yields UploadPart + // commands for each upload part. + $resultHandler = $this->getResultHandler($errors); + $commands = new CommandPool( + $this->client, + $this->getUploadCommands($resultHandler), + [ + 'concurrency' => $this->config['concurrency'], + 'before' => $this->config['before_upload'], + ] + ); + + // Execute the pool of commands concurrently, and process errors. + yield $commands->promise(); + if ($errors) { + throw new $this->config['exception_class']($this->state, $errors); + } + + // Complete the multipart upload. + yield $this->execCommand('complete', $this->getCompleteParams()); + $this->state->setStatus(UploadState::COMPLETED); + })->otherwise($this->buildFailureCatch()); + } + + private function transformException($e) + { + // Throw errors from the operations as a specific Multipart error. + if ($e instanceof AwsException) { + $e = new $this->config['exception_class']($this->state, $e); + } + throw $e; + } + + private function buildFailureCatch() + { + if (interface_exists("Throwable")) { + return function (\Throwable $e) { + return $this->transformException($e); + }; + } else { + return function (\Exception $e) { + return $this->transformException($e); + }; + } + } + + protected function getConfig() + { + return $this->config; + } + + /** + * Provides service-specific information about the multipart upload + * workflow. + * + * This array of data should include the keys: 'command', 'id', and 'part_num'. + * + * @return array + */ + abstract protected function loadUploadWorkflowInfo(); + + /** + * Determines the part size to use for upload parts. + * + * Examines the provided partSize value and the source to determine the + * best possible part size. + * + * @throws \InvalidArgumentException if the part size is invalid. + * + * @return int + */ + abstract protected function determinePartSize(); + + /** + * Uses information from the Command and Result to determine which part was + * uploaded and mark it as uploaded in the upload's state. + * + * @param CommandInterface $command + * @param ResultInterface $result + */ + abstract protected function handleResult( + CommandInterface $command, + ResultInterface $result + ); + + /** + * Gets the service-specific parameters used to initiate the upload. + * + * @return array + */ + abstract protected function getInitiateParams(); + + /** + * Gets the service-specific parameters used to complete the upload. + * + * @return array + */ + abstract protected function getCompleteParams(); + + /** + * Based on the config and service-specific workflow info, creates a + * `Promise` for an `UploadState` object. + */ + private function determineState(): UploadState + { + // If the state was provided via config, then just use it. + if ($this->config['state'] instanceof UploadState) { + return $this->config['state']; + } + + // Otherwise, construct a new state from the provided identifiers. + $required = $this->info['id']; + $id = [$required['upload_id'] => null]; + unset($required['upload_id']); + foreach ($required as $key => $param) { + if (!$this->config[$key]) { + throw new IAE('You must provide a value for "' . $key . '" in ' + . 'your config for the MultipartUploader for ' + . $this->client->getApi()->getServiceFullName() . '.'); + } + $id[$param] = $this->config[$key]; + } + $state = new UploadState($id, $this->config); + $state->setPartSize($this->determinePartSize()); + + return $state; + } + + /** + * Executes a MUP command with all of the parameters for the operation. + * + * @param string $operation Name of the operation. + * @param array $params Service-specific params for the operation. + * + * @return PromiseInterface + */ + protected function execCommand($operation, array $params) + { + // Create the command. + $command = $this->client->getCommand( + $this->info['command'][$operation], + $params + $this->state->getId() + ); + + // Execute the before callback. + if (is_callable($this->config["before_{$operation}"])) { + $this->config["before_{$operation}"]($command); + } + + // Execute the command asynchronously and return the promise. + return $this->client->executeAsync($command); + } + + /** + * Returns a middleware for processing responses of part upload operations. + * + * - Adds an onFulfilled callback that calls the service-specific + * handleResult method on the Result of the operation. + * - Adds an onRejected callback that adds the error to an array of errors. + * - Has a passedByRef $errors arg that the exceptions get added to. The + * caller should use that &$errors array to do error handling. + * + * @param array $errors Errors from upload operations are added to this. + * + * @return callable + */ + protected function getResultHandler(&$errors = []) + { + return function (callable $handler) use (&$errors) { + return function ( + CommandInterface $command, + ?RequestInterface $request = null + ) use ($handler, &$errors) { + return $handler($command, $request)->then( + function (ResultInterface $result) use ($command) { + $this->handleResult($command, $result); + return $result; + }, + function (AwsException $e) use (&$errors) { + $errors[$e->getCommand()[$this->info['part_num']]] = $e; + return new Result(); + } + ); + }; + }; + } + + /** + * Creates a generator that yields part data for the upload's source. + * + * Yields associative arrays of parameters that are ultimately merged in + * with others to form the complete parameters of a command. This can + * include the Body parameter, which is a limited stream (i.e., a Stream + * object, decorated with a LimitStream). + * + * @param callable $resultHandler + * + * @return \Generator + */ + abstract protected function getUploadCommands(callable $resultHandler); +} diff --git a/3rdparty/aws/aws-sdk-php/src/Multipart/AbstractUploader.php b/3rdparty/aws/aws-sdk-php/src/Multipart/AbstractUploader.php new file mode 100644 index 00000000..306be14f --- /dev/null +++ b/3rdparty/aws/aws-sdk-php/src/Multipart/AbstractUploader.php @@ -0,0 +1,153 @@ +source = $this->determineSource($source); + parent::__construct($client, $config); + } + + /** + * Create a stream for a part that starts at the current position and + * has a length of the upload part size (or less with the final part). + * + * @param Stream $stream + * + * @return Psr7\LimitStream + */ + protected function limitPartStream(Stream $stream) + { + // Limit what is read from the stream to the part size. + return new Psr7\LimitStream( + $stream, + $this->state->getPartSize(), + $this->source->tell() + ); + } + + protected function getUploadCommands(callable $resultHandler) + { + // Determine if the source can be seeked. + $seekable = $this->source->isSeekable() + && $this->source->getMetadata('wrapper_type') === 'plainfile'; + + for ($partNumber = 1; $this->isEof($seekable); $partNumber++) { + // If we haven't already uploaded this part, yield a new part. + if (!$this->state->hasPartBeenUploaded($partNumber)) { + $partStartPos = $this->source->tell(); + if (!($data = $this->createPart($seekable, $partNumber))) { + break; + } + $command = $this->client->getCommand( + $this->info['command']['upload'], + $data + $this->state->getId() + ); + $command->getHandlerList()->appendSign($resultHandler, 'mup'); + $numberOfParts = $this->getNumberOfParts($this->state->getPartSize()); + if (isset($numberOfParts) && $partNumber > $numberOfParts) { + throw new $this->config['exception_class']( + $this->state, + new AwsException( + "Maximum part number for this job exceeded, file has likely been corrupted." . + " Please restart this upload.", + $command + ) + ); + } + + yield $command; + if ($this->source->tell() > $partStartPos) { + continue; + } + } + + // Advance the source's offset if not already advanced. + if ($seekable) { + $this->source->seek(min( + $this->source->tell() + $this->state->getPartSize(), + $this->source->getSize() + )); + } else { + $this->source->read($this->state->getPartSize()); + } + } + } + + /** + * Generates the parameters for an upload part by analyzing a range of the + * source starting from the current offset up to the part size. + * + * @param bool $seekable + * @param int $number + * + * @return array|null + */ + abstract protected function createPart($seekable, $number); + + /** + * Checks if the source is at EOF. + * + * @param bool $seekable + * + * @return bool + */ + private function isEof($seekable) + { + return $seekable + ? $this->source->tell() < $this->source->getSize() + : !$this->source->eof(); + } + + /** + * Turns the provided source into a stream and stores it. + * + * If a string is provided, it is assumed to be a filename, otherwise, it + * passes the value directly to `Psr7\Utils::streamFor()`. + * + * @param mixed $source + * + * @return Stream + */ + private function determineSource($source) + { + // Use the contents of a file as the data source. + if (is_string($source)) { + $source = Psr7\Utils::tryFopen($source, 'r'); + } + + // Create a source stream. + $stream = Psr7\Utils::streamFor($source); + if (!$stream->isReadable()) { + throw new IAE('Source stream must be readable.'); + } + + return $stream; + } + + protected function getNumberOfParts($partSize) + { + if ($sourceSize = $this->source->getSize()) { + return ceil($sourceSize/$partSize); + } + return null; + } +} diff --git a/3rdparty/aws/aws-sdk-php/src/Multipart/UploadState.php b/3rdparty/aws/aws-sdk-php/src/Multipart/UploadState.php new file mode 100644 index 00000000..83b05650 --- /dev/null +++ b/3rdparty/aws/aws-sdk-php/src/Multipart/UploadState.php @@ -0,0 +1,218 @@ +id = $id; + + if (isset($config['display_progress']) + && is_bool($config['display_progress']) + ) { + $this->displayProgress = $config['display_progress']; + } + } + + /** + * Get the upload's ID, which is a tuple of parameters that can uniquely + * identify the upload. + * + * @return array + */ + public function getId() + { + return $this->id; + } + + /** + * Sets the "upload_id", or 3rd part of the upload's ID. This typically + * only needs to be done after initiating an upload. + * + * @param string $key The param key of the upload_id. + * @param string $value The param value of the upload_id. + */ + public function setUploadId($key, $value) + { + $this->id[$key] = $value; + } + + /** + * Get the part size. + * + * @return int + */ + public function getPartSize() + { + return $this->partSize; + } + + /** + * Set the part size. + * + * @param $partSize int Size of upload parts. + */ + public function setPartSize($partSize) + { + $this->partSize = $partSize; + } + + /** + * Sets the 1/8th thresholds array. $totalSize is only sent if + * 'track_upload' is true. + * + * @param $totalSize numeric Size of object to upload. + * + * @return array + */ + public function setProgressThresholds($totalSize): array + { + if(!is_numeric($totalSize)) { + throw new \InvalidArgumentException( + 'The total size of the upload must be a number.' + ); + } + + $this->progressThresholds[0] = 0; + for ($i = 1; $i <= self::PROGRESS_THRESHOLD_SIZE; $i++) { + $this->progressThresholds[] = round( + $totalSize * ($i / self::PROGRESS_THRESHOLD_SIZE) + ); + } + + return $this->progressThresholds; + } + + /** + * Prints progress of upload. + * + * @param $totalUploaded numeric Size of upload so far. + */ + public function getDisplayProgress($totalUploaded): void + { + if (!is_numeric($totalUploaded)) { + throw new \InvalidArgumentException( + 'The size of the bytes being uploaded must be a number.' + ); + } + + if ($this->displayProgress) { + while (!empty($this->progressBar) + && $totalUploaded >= $this->progressThresholds[0] + ) { + echo array_shift($this->progressBar); + array_shift($this->progressThresholds); + } + } + } + + /** + * Marks a part as being uploaded. + * + * @param int $partNumber The part number. + * @param array $partData Data from the upload operation that needs to be + * recalled during the complete operation. + */ + public function markPartAsUploaded($partNumber, array $partData = []) + { + $this->uploadedParts[$partNumber] = $partData; + } + + /** + * Returns whether a part has been uploaded. + * + * @param int $partNumber The part number. + * + * @return bool + */ + public function hasPartBeenUploaded($partNumber) + { + return isset($this->uploadedParts[$partNumber]); + } + + /** + * Returns a sorted list of all the uploaded parts. + * + * @return array + */ + public function getUploadedParts() + { + ksort($this->uploadedParts); + return $this->uploadedParts; + } + + /** + * Set the status of the upload. + * + * @param int $status Status is an integer code defined by the constants + * CREATED, INITIATED, and COMPLETED on this class. + */ + public function setStatus($status) + { + $this->status = $status; + } + + /** + * Determines whether the upload state is in the INITIATED status. + * + * @return bool + */ + public function isInitiated() + { + return $this->status === self::INITIATED; + } + + /** + * Determines whether the upload state is in the COMPLETED status. + * + * @return bool + */ + public function isCompleted() + { + return $this->status === self::COMPLETED; + } +} diff --git a/3rdparty/aws/aws-sdk-php/src/PhpHash.php b/3rdparty/aws/aws-sdk-php/src/PhpHash.php new file mode 100644 index 00000000..7a82815a --- /dev/null +++ b/3rdparty/aws/aws-sdk-php/src/PhpHash.php @@ -0,0 +1,81 @@ +algo = $algo; + $this->options = $options; + } + + public function update($data) + { + if ($this->hash !== null) { + $this->reset(); + } + + hash_update($this->getContext(), $data); + } + + public function complete() + { + if ($this->hash) { + return $this->hash; + } + + $this->hash = hash_final($this->getContext(), true); + + if (isset($this->options['base64']) && $this->options['base64']) { + $this->hash = base64_encode($this->hash); + } + + return $this->hash; + } + + public function reset() + { + $this->context = $this->hash = null; + } + + /** + * Get a hash context or create one if needed + * + * @return resource|\HashContext + */ + private function getContext() + { + if (!$this->context) { + $key = isset($this->options['key']) ? $this->options['key'] : ''; + $this->context = hash_init( + $this->algo, + $key ? HASH_HMAC : 0, + $key + ); + } + + return $this->context; + } +} diff --git a/3rdparty/aws/aws-sdk-php/src/PresignUrlMiddleware.php b/3rdparty/aws/aws-sdk-php/src/PresignUrlMiddleware.php new file mode 100644 index 00000000..6fb3c92f --- /dev/null +++ b/3rdparty/aws/aws-sdk-php/src/PresignUrlMiddleware.php @@ -0,0 +1,128 @@ +endpointProvider = $endpointProvider; + $this->client = $client; + $this->nextHandler = $nextHandler; + $this->commandPool = $options['operations']; + $this->serviceName = $options['service']; + $this->presignParam = !empty($options['presign_param']) + ? $options['presign_param'] + : 'PresignedUrl'; + $this->extraQueryParams = !empty($options['extra_query_params']) + ? $options['extra_query_params'] + : []; + $this->requireDifferentRegion = !empty($options['require_different_region']); + } + + public static function wrap( + AwsClientInterface $client, + $endpointProvider, + array $options = [] + ) { + return function (callable $handler) use ($endpointProvider, $client, $options) { + $f = new PresignUrlMiddleware($options, $endpointProvider, $client, $handler); + return $f; + }; + } + + public function __invoke(CommandInterface $cmd, ?RequestInterface $request = null) + { + if (in_array($cmd->getName(), $this->commandPool) + && (!isset($cmd['__skip' . $cmd->getName()])) + ) { + $cmd['DestinationRegion'] = $this->client->getRegion(); + if (!empty($cmd['SourceRegion']) && !empty($cmd[$this->presignParam])) { + goto nexthandler; + } + if (!$this->requireDifferentRegion + || (!empty($cmd['SourceRegion']) + && $cmd['SourceRegion'] !== $cmd['DestinationRegion']) + ) { + $cmd[$this->presignParam] = $this->createPresignedUrl($this->client, $cmd); + } + } + nexthandler: + $nextHandler = $this->nextHandler; + return $nextHandler($cmd, $request); + } + + private function createPresignedUrl( + AwsClientInterface $client, + CommandInterface $cmd + ) { + $cmdName = $cmd->getName(); + $newCmd = $client->getCommand($cmdName, $cmd->toArray()); + // Avoid infinite recursion by flagging the new command. + $newCmd['__skip' . $cmdName] = true; + + // Serialize a request for the operation. + $request = \Aws\serialize($newCmd); + // Create the new endpoint for the target endpoint. + if ($this->endpointProvider instanceof \Aws\EndpointV2\EndpointProviderV2) { + $providerArgs = array_merge( + $this->client->getEndpointProviderArgs(), + ['Region' => $cmd['SourceRegion']] + ); + $endpoint = $this->endpointProvider->resolveEndpoint($providerArgs)->getUrl(); + } else { + $endpoint = EndpointProvider::resolve($this->endpointProvider, [ + 'region' => $cmd['SourceRegion'], + 'service' => $this->serviceName, + ])['endpoint']; + } + + // Set the request to hit the target endpoint. + $uri = $request->getUri()->withHost((new Uri($endpoint))->getHost()); + $request = $request->withUri($uri); + + // Create a presigned URL for our generated request. + $signer = new SignatureV4($this->serviceName, $cmd['SourceRegion']); + + $currentQueryParams = (string) $request->getBody(); + $paramsToAdd = false; + if (!empty($this->extraQueryParams[$cmdName])) { + foreach ($this->extraQueryParams[$cmdName] as $param) { + if (!strpos($currentQueryParams, $param)) { + $paramsToAdd = "&{$param}={$cmd[$param]}"; + } + } + } + + return (string) $signer->presign( + SignatureV4::convertPostToGet($request, $paramsToAdd ?: ""), + $client->getCredentials()->wait(), + '+1 hour' + )->getUri(); + } +} diff --git a/3rdparty/aws/aws-sdk-php/src/Psr16CacheAdapter.php b/3rdparty/aws/aws-sdk-php/src/Psr16CacheAdapter.php new file mode 100644 index 00000000..b1ac8edf --- /dev/null +++ b/3rdparty/aws/aws-sdk-php/src/Psr16CacheAdapter.php @@ -0,0 +1,30 @@ +cache = $cache; + } + + public function get($key) + { + return $this->cache->get($key); + } + + public function set($key, $value, $ttl = 0) + { + $this->cache->set($key, $value, $ttl); + } + + public function remove($key) + { + $this->cache->delete($key); + } +} diff --git a/3rdparty/aws/aws-sdk-php/src/PsrCacheAdapter.php b/3rdparty/aws/aws-sdk-php/src/PsrCacheAdapter.php new file mode 100644 index 00000000..9dd2d941 --- /dev/null +++ b/3rdparty/aws/aws-sdk-php/src/PsrCacheAdapter.php @@ -0,0 +1,38 @@ +pool = $pool; + } + + public function get($key) + { + $item = $this->pool->getItem($key); + + return $item->isHit() ? $item->get() : null; + } + + public function set($key, $value, $ttl = 0) + { + $item = $this->pool->getItem($key); + $item->set($value); + if ($ttl > 0) { + $item->expiresAfter($ttl); + } + + $this->pool->save($item); + } + + public function remove($key) + { + $this->pool->deleteItem($key); + } +} diff --git a/3rdparty/aws/aws-sdk-php/src/QueryCompatibleInputMiddleware.php b/3rdparty/aws/aws-sdk-php/src/QueryCompatibleInputMiddleware.php new file mode 100644 index 00000000..9da75b6c --- /dev/null +++ b/3rdparty/aws/aws-sdk-php/src/QueryCompatibleInputMiddleware.php @@ -0,0 +1,234 @@ +service = $service; + $this->nextHandler = $nextHandler; + } + + public function __invoke(CommandInterface $cmd) + { + $this->command = $cmd; + $nextHandler = $this->nextHandler; + $op = $this->service->getOperation($cmd->getName()); + $inputMembers = $op->getInput()->getMembers(); + $input = $cmd->toArray(); + + foreach ($input as $param => $value) { + if (isset($inputMembers[$param])) { + $shape = $inputMembers[$param]; + $this->processInput($value, $shape, [$param]); + } + } + + return $nextHandler($this->command); + } + + /** + * Recurses a given input shape. if a given scalar input does not match its + * modeled type, it is cast to its modeled type. + * + * @param $input + * @param $shape + * @param array $path + * + * @return void + */ + private function processInput($input, $shape, array $path) : void + { + switch ($shape->getType()) { + case 'structure': + $this->processStructure($input, $shape, $path); + break; + case 'list': + $this->processList($input, $shape, $path); + break; + case 'map': + $this->processMap($input, $shape, $path); + break; + default: + $this->processScalar($input, $shape, $path); + } + } + + /** + * @param array $input + * @param StructureShape $shape + * @param array $path + * + * @return void + */ + private function processStructure( + array $input, + StructureShape $shape, + array $path + ) : void + { + foreach ($input as $param => $value) { + if ($shape->hasMember($param)) { + $memberPath = array_merge($path, [$param]); + $this->processInput($value, $shape->getMember($param), $memberPath); + } + } + } + + /** + * @param array $input + * @param ListShape $shape + * @param array $path + * + * @return void + */ + private function processList( + array $input, + ListShape $shape, + array $path + ) : void + { + foreach ($input as $param => $value) { + $memberPath = array_merge($path, [$param]); + $this->processInput($value, $shape->getMember(), $memberPath); + } + } + + /** + * @param array $input + * @param MapShape $shape + * @param array $path + * + * @return void + */ + private function processMap(array $input, MapShape $shape, array $path) : void + { + foreach ($input as $param => $value) { + $memberPath = array_merge($path, [$param]); + $this->processInput($value, $shape->getValue(), $memberPath); + } + } + + /** + * @param $input + * @param Shape $shape + * @param array $path + * + * @return void + */ + private function processScalar($input, Shape $shape, array $path) : void + { + $expectedType = $shape->getType(); + + if (!$this->isModeledType($input, $expectedType)) { + trigger_error( + "The provided type for `". implode(' -> ', $path) ."` value was `" + . (gettype($input) === 'double' ? 'float' : gettype($input)) . "`." + . " The modeled type is `{$expectedType}`.", + E_USER_WARNING + ); + $value = $this->castValue($input, $expectedType); + $this->changeValueAtPath($path, $value); + } + } + + /** + * Modifies command in place + * + * @param array $path + * @param $newValue + * + * @return void + */ + private function changeValueAtPath(array $path, $newValue) : void + { + $commandRef = &$this->command; + + foreach ($path as $segment) { + if (!isset($commandRef[$segment])) { + return; + } + $commandRef = &$commandRef[$segment]; + } + $commandRef = $newValue; + } + + /** + * @param $value + * @param $type + * + * @return bool + */ + private function isModeledType($value, $type) : bool + { + switch ($type) { + case 'string': + return is_string($value); + case 'integer': + case 'long': + return is_int($value); + case 'float': + return is_float($value); + default: + return true; + } + } + + /** + * @param $value + * @param $type + * + * @return float|int|mixed|string + */ + private function castValue($value, $type) + { + switch ($type) { + case 'integer': + return (int) $value; + case 'long' : + return $value + 0; + case 'float': + return (float) $value; + case 'string': + return (string) $value; + default: + return $value; + } + } +} diff --git a/3rdparty/aws/aws-sdk-php/src/RequestCompressionMiddleware.php b/3rdparty/aws/aws-sdk-php/src/RequestCompressionMiddleware.php new file mode 100644 index 00000000..667761df --- /dev/null +++ b/3rdparty/aws/aws-sdk-php/src/RequestCompressionMiddleware.php @@ -0,0 +1,170 @@ + 'gzencode' + ]; + + /** + * Create a middleware wrapper function. + * + * @return callable + */ + public static function wrap(array $config) + { + return function (callable $handler) use ($config) { + return new self($handler, $config); + }; + } + + public function __construct(callable $nextHandler, $config) + { + $this->minimumCompressionSize = $this->determineMinimumCompressionSize($config); + $this->api = $config['api']; + $this->nextHandler = $nextHandler; + } + + public function __invoke(CommandInterface $command, RequestInterface $request) + { + if (isset($command['@request_min_compression_size_bytes']) + && is_int($command['@request_min_compression_size_bytes']) + && $this->isValidCompressionSize($command['@request_min_compression_size_bytes']) + ) { + $this->minimumCompressionSize = $command['@request_min_compression_size_bytes']; + } + $nextHandler = $this->nextHandler; + $operation = $this->api->getOperation($command->getName()); + $compressionInfo = isset($operation['requestcompression']) + ? $operation['requestcompression'] + : null; + + if (!$this->shouldCompressRequestBody( + $compressionInfo, + $command, + $operation, + $request + )) { + return $nextHandler($command, $request); + } + + $this->encodings = $compressionInfo['encodings']; + $request = $this->compressRequestBody($request); + + // Capture request compression metric + $command->getMetricsBuilder()->identifyMetricByValueAndAppend( + 'request_compression', + $request->getHeaderLine('content-encoding') + ); + + return $nextHandler($command, $request); + } + + private function compressRequestBody( + RequestInterface $request + ) { + $fn = $this->determineEncoding(); + if (is_null($fn)) { + return $request; + } + + $body = $request->getBody()->getContents(); + $compressedBody = $fn($body); + + return $request->withBody(Psr7\Utils::streamFor($compressedBody)) + ->withHeader('content-encoding', $this->encoding); + } + + private function determineEncoding() + { + foreach ($this->encodings as $encoding) { + if (isset($this->encodingMap[$encoding])) { + $this->encoding = $encoding; + return $this->encodingMap[$encoding]; + } + } + return null; + } + + private function shouldCompressRequestBody( + $compressionInfo, + $command, + $operation, + $request + ){ + if ($compressionInfo) { + if (isset($command['@disable_request_compression']) + && $command['@disable_request_compression'] === true + ) { + return false; + } elseif ($this->hasStreamingTraitWithoutRequiresLength($command, $operation) + ) { + return true; + } + + $requestBodySize = $request->hasHeader('content-length') + ? (int) $request->getHeaderLine('content-length') + : $request->getBody()->getSize(); + + if ($requestBodySize >= $this->minimumCompressionSize) { + return true; + } + } + return false; + } + + private function hasStreamingTraitWithoutRequiresLength($command, $operation) + { + foreach ($operation->getInput()->getMembers() as $name => $member) { + if (isset($command[$name]) + && !empty($member['streaming']) + && empty($member['requiresLength']) + ){ + return true; + } + } + return false; + } + + private function determineMinimumCompressionSize($config) { + if (is_callable($config['request_min_compression_size_bytes'])) { + $minCompressionSz = $config['request_min_compression_size_bytes'](); + } else { + $minCompressionSz = $config['request_min_compression_size_bytes']; + } + + if ($this->isValidCompressionSize($minCompressionSz)) { + return $minCompressionSz; + } + } + + private function isValidCompressionSize($compressionSize) + { + if (is_numeric($compressionSize) + && ($compressionSize >= 0 && $compressionSize <= 10485760) + ) { + return true; + } + + throw new \InvalidArgumentException( + 'The minimum request compression size must be a ' + . 'non-negative integer value between 0 and 10485760 bytes, inclusive.' + ); + } +} diff --git a/3rdparty/aws/aws-sdk-php/src/ResponseContainerInterface.php b/3rdparty/aws/aws-sdk-php/src/ResponseContainerInterface.php new file mode 100644 index 00000000..d26921bb --- /dev/null +++ b/3rdparty/aws/aws-sdk-php/src/ResponseContainerInterface.php @@ -0,0 +1,15 @@ +data = $data; + } + + public function hasKey($name) + { + return isset($this->data[$name]); + } + + public function get($key) + { + return $this[$key]; + } + + public function search($expression) + { + return JmesPath::search($expression, $this->toArray()); + } + + public function __toString() + { + $jsonData = json_encode($this->toArray(), JSON_PRETTY_PRINT); + return <<get(\$key)`) or "accessing the result like an +associative array (e.g. `\$result['key']`). You can also execute JMESPath +expressions on the result data using the search() method. + +{$jsonData} + +EOT; + } + + /** + * @deprecated + */ + public function getPath($path) + { + return $this->search(str_replace('/', '.', $path)); + } +} diff --git a/3rdparty/aws/aws-sdk-php/src/ResultInterface.php b/3rdparty/aws/aws-sdk-php/src/ResultInterface.php new file mode 100644 index 00000000..18a166ab --- /dev/null +++ b/3rdparty/aws/aws-sdk-php/src/ResultInterface.php @@ -0,0 +1,54 @@ +execute($command); + * $jpResult = $result->search('foo.*.bar[?baz > `10`]'); + * + * @param string $expression JMESPath expression to execute + * + * @return mixed Returns the result of the JMESPath expression. + * @link http://jmespath.readthedocs.org/en/latest/ JMESPath documentation + */ + public function search($expression); +}; diff --git a/3rdparty/aws/aws-sdk-php/src/ResultPaginator.php b/3rdparty/aws/aws-sdk-php/src/ResultPaginator.php new file mode 100644 index 00000000..396ed1a9 --- /dev/null +++ b/3rdparty/aws/aws-sdk-php/src/ResultPaginator.php @@ -0,0 +1,206 @@ +client = $client; + $this->operation = $operation; + $this->args = $args; + $this->config = $config; + MetricsBuilder::appendMetricsCaptureMiddleware( + $this->client->getHandlerList(), + MetricsBuilder::PAGINATOR + ); + } + + /** + * Runs a paginator asynchronously and uses a callback to handle results. + * + * The callback should have the signature: function (Aws\Result $result). + * A non-null return value from the callback will be yielded by the + * promise. This means that you can return promises from the callback that + * will need to be resolved before continuing iteration over the remaining + * items, essentially merging in other promises to the iteration. The last + * non-null value returned by the callback will be the result that fulfills + * the promise to any downstream promises. + * + * @param callable $handleResult Callback for handling each page of results. + * The callback accepts the result that was + * yielded as a single argument. If the + * callback returns a promise, the promise + * will be merged into the coroutine. + * + * @return Promise\Promise + */ + public function each(callable $handleResult) + { + return Promise\Coroutine::of(function () use ($handleResult) { + $nextToken = null; + do { + $command = $this->createNextCommand($this->args, $nextToken); + $result = (yield $this->client->executeAsync($command)); + $nextToken = $this->determineNextToken($result); + $retVal = $handleResult($result); + if ($retVal !== null) { + yield Promise\Create::promiseFor($retVal); + } + } while ($nextToken); + }); + } + + /** + * Returns an iterator that iterates over the values of applying a JMESPath + * search to each result yielded by the iterator as a flat sequence. + * + * @param string $expression JMESPath expression to apply to each result. + * + * @return \Iterator + */ + public function search($expression) + { + // Apply JMESPath expression on each result, but as a flat sequence. + return flatmap($this, function (Result $result) use ($expression) { + return (array) $result->search($expression); + }); + } + + /** + * @return Result + */ + #[\ReturnTypeWillChange] + public function current() + { + return $this->valid() ? $this->result : false; + } + + /** + * @return mixed + */ + #[\ReturnTypeWillChange] + public function key() + { + return $this->valid() ? $this->requestCount - 1 : null; + } + + /** + * @return void + */ + #[\ReturnTypeWillChange] + public function next() + { + $this->result = null; + } + + /** + * @return bool + */ + #[\ReturnTypeWillChange] + public function valid() + { + if ($this->result) { + return true; + } + + if ($this->nextToken || !$this->requestCount) { + //Forward/backward paging can result in a case where the last page's nextforwardtoken + //is the same as the one that came before it. This can cause an infinite loop. + $hasBidirectionalPaging = $this->config['output_token'] === 'nextForwardToken'; + if ($hasBidirectionalPaging && $this->nextToken) { + $tokenKey = $this->config['input_token']; + $previousToken = $this->nextToken[$tokenKey]; + } + + $this->result = $this->client->execute( + $this->createNextCommand($this->args, $this->nextToken) + ); + + $this->nextToken = $this->determineNextToken($this->result); + + if (isset($previousToken) + && $previousToken === $this->nextToken[$tokenKey] + ) { + return false; + } + + $this->requestCount++; + return true; + } + + return false; + } + + /** + * @return void + */ + #[\ReturnTypeWillChange] + public function rewind() + { + $this->requestCount = 0; + $this->nextToken = null; + $this->result = null; + } + + private function createNextCommand(array $args, ?array $nextToken = null) + { + return $this->client->getCommand($this->operation, array_merge($args, ($nextToken ?: []))); + } + + private function determineNextToken(Result $result) + { + if (!$this->config['output_token']) { + return null; + } + + if ($this->config['more_results'] + && !$result->search($this->config['more_results']) + ) { + return null; + } + + $nextToken = is_scalar($this->config['output_token']) + ? [$this->config['input_token'] => $this->config['output_token']] + : array_combine($this->config['input_token'], $this->config['output_token']); + + return array_filter(array_map(function ($outputToken) use ($result) { + return $result->search($outputToken); + }, $nextToken)); + } +} diff --git a/3rdparty/aws/aws-sdk-php/src/Retry/Configuration.php b/3rdparty/aws/aws-sdk-php/src/Retry/Configuration.php new file mode 100644 index 00000000..aa370c40 --- /dev/null +++ b/3rdparty/aws/aws-sdk-php/src/Retry/Configuration.php @@ -0,0 +1,61 @@ +validModes)) { + throw new ConfigurationException("'{$mode}' is not a valid mode." + . " The mode has to be 'legacy', 'standard', or 'adaptive'."); + } + if (!is_numeric($maxAttempts) + || intval($maxAttempts) != $maxAttempts + || $maxAttempts < 1 + ) { + throw new ConfigurationException("The 'maxAttempts' parameter has" + . " to be an integer >= 1."); + } + + $this->mode = $mode; + $this->maxAttempts = intval($maxAttempts); + } + + /** + * {@inheritdoc} + */ + public function getMode() + { + return $this->mode; + } + + /** + * {@inheritdoc} + */ + public function getMaxAttempts() + { + return $this->maxAttempts; + } + + /** + * {@inheritdoc} + */ + public function toArray() + { + return [ + 'mode' => $this->getMode(), + 'max_attempts' => $this->getMaxAttempts(), + ]; + } +} diff --git a/3rdparty/aws/aws-sdk-php/src/Retry/ConfigurationInterface.php b/3rdparty/aws/aws-sdk-php/src/Retry/ConfigurationInterface.php new file mode 100644 index 00000000..3f57b62d --- /dev/null +++ b/3rdparty/aws/aws-sdk-php/src/Retry/ConfigurationInterface.php @@ -0,0 +1,30 @@ + + * use Aws\Sts\RegionalEndpoints\ConfigurationProvider; + * $provider = ConfigurationProvider::defaultProvider(); + * // Returns a ConfigurationInterface or throws. + * $config = $provider()->wait(); + * + * + * Configuration providers can be composed to create configuration using + * conditional logic that can create different configurations in different + * environments. You can compose multiple providers into a single provider using + * {@see \Aws\Retry\ConfigurationProvider::chain}. This function + * accepts providers as variadic arguments and returns a new function that will + * invoke each provider until a successful configuration is returned. + * + * + * // First try an INI file at this location. + * $a = ConfigurationProvider::ini(null, '/path/to/file.ini'); + * // Then try an INI file at this location. + * $b = ConfigurationProvider::ini(null, '/path/to/other-file.ini'); + * // Then try loading from environment variables. + * $c = ConfigurationProvider::env(); + * // Combine the three providers together. + * $composed = ConfigurationProvider::chain($a, $b, $c); + * // Returns a promise that is fulfilled with a configuration or throws. + * $promise = $composed(); + * // Wait on the configuration to resolve. + * $config = $promise->wait(); + * + */ +class ConfigurationProvider extends AbstractConfigurationProvider + implements ConfigurationProviderInterface +{ + const DEFAULT_MAX_ATTEMPTS = 3; + const DEFAULT_MODE = 'legacy'; + const ENV_MAX_ATTEMPTS = 'AWS_MAX_ATTEMPTS'; + const ENV_MODE = 'AWS_RETRY_MODE'; + const ENV_PROFILE = 'AWS_PROFILE'; + const INI_MAX_ATTEMPTS = 'max_attempts'; + const INI_MODE = 'retry_mode'; + + public static $cacheKey = 'aws_retries_config'; + + protected static $interfaceClass = ConfigurationInterface::class; + protected static $exceptionClass = ConfigurationException::class; + + /** + * Create a default config provider that first checks for environment + * variables, then checks for a specified profile in the environment-defined + * config file location (env variable is 'AWS_CONFIG_FILE', file location + * defaults to ~/.aws/config), then checks for the "default" profile in the + * environment-defined config file location, and failing those uses a default + * fallback set of configuration options. + * + * This provider is automatically wrapped in a memoize function that caches + * previously provided config options. + * + * @param array $config + * + * @return callable + */ + public static function defaultProvider(array $config = []) + { + $configProviders = [self::env()]; + if ( + !isset($config['use_aws_shared_config_files']) + || $config['use_aws_shared_config_files'] != false + ) { + $configProviders[] = self::ini(); + } + $configProviders[] = self::fallback(); + + $memo = self::memoize( + call_user_func_array([ConfigurationProvider::class, 'chain'], $configProviders) + ); + + if (isset($config['retries']) + && $config['retries'] instanceof CacheInterface + ) { + return self::cache($memo, $config['retries'], self::$cacheKey); + } + + return $memo; + } + + /** + * Provider that creates config from environment variables. + * + * @return callable + */ + public static function env() + { + return function () { + // Use config from environment variables, if available + $mode = getenv(self::ENV_MODE); + $maxAttempts = getenv(self::ENV_MAX_ATTEMPTS) + ? getenv(self::ENV_MAX_ATTEMPTS) + : self::DEFAULT_MAX_ATTEMPTS; + if (!empty($mode)) { + return Promise\Create::promiseFor( + new Configuration($mode, $maxAttempts) + ); + } + + return self::reject('Could not find environment variable config' + . ' in ' . self::ENV_MODE); + }; + } + + /** + * Fallback config options when other sources are not set. + * + * @return callable + */ + public static function fallback() + { + return function () { + return Promise\Create::promiseFor( + new Configuration(self::DEFAULT_MODE, self::DEFAULT_MAX_ATTEMPTS) + ); + }; + } + + /** + * Config provider that creates config using a config file whose location + * is specified by an environment variable 'AWS_CONFIG_FILE', defaulting to + * ~/.aws/config if not specified + * + * @param string|null $profile Profile to use. If not specified will use + * the "default" profile. + * @param string|null $filename If provided, uses a custom filename rather + * than looking in the default directory. + * + * @return callable + */ + public static function ini( + $profile = null, + $filename = null + ) { + $filename = $filename ?: (self::getDefaultConfigFilename()); + $profile = $profile ?: (getenv(self::ENV_PROFILE) ?: 'default'); + + return function () use ($profile, $filename) { + if (!@is_readable($filename)) { + return self::reject("Cannot read configuration from $filename"); + } + $data = \Aws\parse_ini_file($filename, true); + if ($data === false) { + return self::reject("Invalid config file: $filename"); + } + if (!isset($data[$profile])) { + return self::reject("'$profile' not found in config file"); + } + if (!isset($data[$profile][self::INI_MODE])) { + return self::reject("Required retry config values + not present in INI profile '{$profile}' ({$filename})"); + } + + $maxAttempts = isset($data[$profile][self::INI_MAX_ATTEMPTS]) + ? $data[$profile][self::INI_MAX_ATTEMPTS] + : self::DEFAULT_MAX_ATTEMPTS; + + return Promise\Create::promiseFor( + new Configuration( + $data[$profile][self::INI_MODE], + $maxAttempts + ) + ); + }; + } + + /** + * Unwraps a configuration object in whatever valid form it is in, + * always returning a ConfigurationInterface object. + * + * @param mixed $config + * @return ConfigurationInterface + * @throws \InvalidArgumentException + */ + public static function unwrap($config) + { + if (is_callable($config)) { + $config = $config(); + } + if ($config instanceof PromiseInterface) { + $config = $config->wait(); + } + if ($config instanceof ConfigurationInterface) { + return $config; + } + + // An integer value for this config indicates the legacy 'retries' + // config option, which is incremented to translate to max attempts + if (is_int($config)) { + return new Configuration('legacy', $config + 1); + } + + if (is_array($config) && isset($config['mode'])) { + $maxAttempts = isset($config['max_attempts']) + ? $config['max_attempts'] + : self::DEFAULT_MAX_ATTEMPTS; + return new Configuration($config['mode'], $maxAttempts); + } + + throw new \InvalidArgumentException('Not a valid retry configuration' + . ' argument.'); + } +} diff --git a/3rdparty/aws/aws-sdk-php/src/Retry/Exception/ConfigurationException.php b/3rdparty/aws/aws-sdk-php/src/Retry/Exception/ConfigurationException.php new file mode 100644 index 00000000..0705c2ee --- /dev/null +++ b/3rdparty/aws/aws-sdk-php/src/Retry/Exception/ConfigurationException.php @@ -0,0 +1,14 @@ +initialRetryTokens = isset($config['initial_retry_tokens']) + ? $config['initial_retry_tokens'] + : 500; + $this->noRetryIncrement = isset($config['no_retry_increment']) + ? $config['no_retry_increment'] + : 1; + $this->retryCost = isset($config['retry_cost']) + ? $config['retry_cost'] + : 5; + $this->timeoutRetryCost = isset($config['timeout_retry_cost']) + ? $config['timeout_retry_cost'] + : 10; + $this->maxCapacity = $this->initialRetryTokens; + $this->availableCapacity = $this->initialRetryTokens; + } + + public function hasRetryQuota($result) + { + if ($result instanceof AwsException && $result->isConnectionError()) { + $this->capacityAmount = $this->timeoutRetryCost; + } else { + $this->capacityAmount = $this->retryCost; + } + + if ($this->capacityAmount > $this->availableCapacity) { + return false; + } + + $this->availableCapacity -= $this->capacityAmount; + return true; + } + + public function releaseToQuota($result) + { + if ($result instanceof AwsException) { + $statusCode = (int) $result->getStatusCode(); + } elseif ($result instanceof ResultInterface) { + $statusCode = isset($result['@metadata']['statusCode']) + ? (int) $result['@metadata']['statusCode'] + : null; + } + + if (!empty($statusCode) && $statusCode >= 200 && $statusCode < 300) { + if (isset($this->capacityAmount)) { + $amount = $this->capacityAmount; + $this->availableCapacity += $amount; + unset($this->capacityAmount); + } else { + $amount = $this->noRetryIncrement; + $this->availableCapacity += $amount; + } + $this->availableCapacity = min( + $this->availableCapacity, + $this->maxCapacity + ); + } + + return (isset($amount) ? $amount : 0); + } + + public function getAvailableCapacity() + { + return $this->availableCapacity; + } +} diff --git a/3rdparty/aws/aws-sdk-php/src/Retry/RateLimiter.php b/3rdparty/aws/aws-sdk-php/src/Retry/RateLimiter.php new file mode 100644 index 00000000..b58cc592 --- /dev/null +++ b/3rdparty/aws/aws-sdk-php/src/Retry/RateLimiter.php @@ -0,0 +1,182 @@ +beta = isset($options['beta']) + ? $options['beta'] + : 0.7; + $this->minCapacity = isset($options['min_capacity']) + ? $options['min_capacity'] + : 1; + $this->minFillRate = isset($options['min_fill_rate']) + ? $options['min_fill_rate'] + : 0.5; + $this->scaleConstant = isset($options['scale_constant']) + ? $options['scale_constant'] + : 0.4; + $this->smooth = isset($options['smooth']) + ? $options['smooth'] + : 0.8; + $this->timeProvider = isset($options['time_provider']) + ? $options['time_provider'] + : null; + + $this->lastTxRateBucket = floor($this->time()); + $this->lastThrottleTime = $this->time(); + } + + public function isEnabled() + { + return $this->enabled; + } + + public function getSendToken() + { + $this->acquireToken(1); + } + + public function updateSendingRate($isThrottled) + { + $this->updateMeasuredRate(); + + if ($isThrottled) { + if (!$this->isEnabled()) { + $rateToUse = $this->measuredTxRate; + } else { + $rateToUse = min($this->measuredTxRate, $this->fillRate); + } + + $this->lastMaxRate = $rateToUse; + $this->calculateTimeWindow(); + $this->lastThrottleTime = $this->time(); + $calculatedRate = $this->cubicThrottle($rateToUse); + $this->enableTokenBucket(); + } else { + $this->calculateTimeWindow(); + $calculatedRate = $this->cubicSuccess($this->time()); + } + $newRate = min($calculatedRate, 2 * $this->measuredTxRate); + $this->updateTokenBucketRate($newRate); + return $newRate; + } + + private function acquireToken($amount) + { + if (!$this->enabled) { + return true; + } + + $this->refillTokenBucket(); + + if ($amount > $this->currentCapacity) { + usleep((int) (1000000 * ($amount - $this->currentCapacity) / $this->fillRate)); + } + + $this->currentCapacity -= $amount; + return true; + } + + private function calculateTimeWindow() + { + $this->timeWindow = pow(($this->lastMaxRate * (1 - $this->beta) / $this->scaleConstant), 0.333); + } + + private function cubicSuccess($timestamp) + { + $dt = $timestamp - $this->lastThrottleTime; + return $this->scaleConstant * pow($dt - $this->timeWindow, 3) + $this->lastMaxRate; + } + + private function cubicThrottle($rateToUse) + { + return $rateToUse * $this->beta; + } + + private function enableTokenBucket() + { + $this->enabled = true; + } + + private function refillTokenBucket() + { + $timestamp = $this->time(); + if (!isset($this->lastTimestamp)) { + $this->lastTimestamp = $timestamp; + return; + } + $fillAmount = ($timestamp - $this->lastTimestamp) * $this->fillRate; + $this->currentCapacity = $this->currentCapacity + $fillAmount; + if (!is_null($this->maxCapacity)) { + $this->currentCapacity = min( + $this->maxCapacity, + $this->currentCapacity + ); + } + + $this->lastTimestamp = $timestamp; + } + + private function time() + { + if (is_callable($this->timeProvider)) { + $provider = $this->timeProvider; + $time = $provider(); + return $time; + } + return microtime(true); + } + + private function updateMeasuredRate() + { + $timestamp = $this->time(); + $timeBucket = floor(round($timestamp, 3) * 2) / 2; + $this->requestCount++; + if ($timeBucket > $this->lastTxRateBucket) { + $currentRate = $this->requestCount / ($timeBucket - $this->lastTxRateBucket); + $this->measuredTxRate = ($currentRate * $this->smooth) + + ($this->measuredTxRate * (1 - $this->smooth)); + $this->requestCount = 0; + $this->lastTxRateBucket = $timeBucket; + } + } + + private function updateTokenBucketRate($newRps) + { + $this->refillTokenBucket(); + $this->fillRate = max($newRps, $this->minFillRate); + $this->maxCapacity = max($newRps, $this->minCapacity); + $this->currentCapacity = min($this->currentCapacity, $this->maxCapacity); + } +} diff --git a/3rdparty/aws/aws-sdk-php/src/Retry/RetryHelperTrait.php b/3rdparty/aws/aws-sdk-php/src/Retry/RetryHelperTrait.php new file mode 100644 index 00000000..a7edb72b --- /dev/null +++ b/3rdparty/aws/aws-sdk-php/src/Retry/RetryHelperTrait.php @@ -0,0 +1,56 @@ +withHeader('aws-sdk-retry', "{$retries}/{$delayBy}"); + } + + + private function updateStats($retries, $delay, array &$stats) + { + if (!isset($stats['total_retry_delay'])) { + $stats['total_retry_delay'] = 0; + } + + $stats['total_retry_delay'] += $delay; + $stats['retries_attempted'] = $retries; + } + + private function updateHttpStats($value, array &$stats) + { + if (empty($stats['http'])) { + $stats['http'] = []; + } + + if ($value instanceof AwsException) { + $resultStats = $value->getTransferInfo(); + $stats['http'] []= $resultStats; + } elseif ($value instanceof ResultInterface) { + $resultStats = isset($value['@metadata']['transferStats']['http'][0]) + ? $value['@metadata']['transferStats']['http'][0] + : []; + $stats['http'] []= $resultStats; + } + } + + private function bindStatsToReturn($return, array $stats) + { + if ($return instanceof ResultInterface) { + if (!isset($return['@metadata'])) { + $return['@metadata'] = []; + } + + $return['@metadata']['transferStats'] = $stats; + } elseif ($return instanceof AwsException) { + $return->setTransferInfo($stats); + } + + return $return; + } +} diff --git a/3rdparty/aws/aws-sdk-php/src/RetryMiddleware.php b/3rdparty/aws/aws-sdk-php/src/RetryMiddleware.php new file mode 100644 index 00000000..bb550df9 --- /dev/null +++ b/3rdparty/aws/aws-sdk-php/src/RetryMiddleware.php @@ -0,0 +1,277 @@ + true, + 502 => true, + 503 => true, + 504 => true + ]; + + private static $retryCodes = [ + // Throttling error + 'RequestLimitExceeded' => true, + 'Throttling' => true, + 'ThrottlingException' => true, + 'ThrottledException' => true, + 'ProvisionedThroughputExceededException' => true, + 'RequestThrottled' => true, + 'BandwidthLimitExceeded' => true, + 'RequestThrottledException' => true, + 'TooManyRequestsException' => true, + 'IDPCommunicationError' => true, + 'EC2ThrottledException' => true, + ]; + + private $decider; + private $delay; + private $nextHandler; + private $collectStats; + + public function __construct( + callable $decider, + callable $delay, + callable $nextHandler, + $collectStats = false + ) { + $this->decider = $decider; + $this->delay = $delay; + $this->nextHandler = $nextHandler; + $this->collectStats = (bool) $collectStats; + } + + /** + * Creates a default AWS retry decider function. + * + * The optional $extraConfig parameter is an associative array + * that specifies additional retry conditions on top of the ones specified + * by default by the Aws\RetryMiddleware class, with the following keys: + * + * - errorCodes: (string[]) An indexed array of AWS exception codes to retry. + * Optional. + * - statusCodes: (int[]) An indexed array of HTTP status codes to retry. + * Optional. + * - curlErrors: (int[]) An indexed array of Curl error codes to retry. Note + * these should be valid Curl constants. Optional. + * + * @param int $maxRetries + * @param array $extraConfig + * @return callable + */ + public static function createDefaultDecider( + $maxRetries = 3, + $extraConfig = [] + ) { + $retryCurlErrors = []; + if (extension_loaded('curl')) { + $retryCurlErrors[CURLE_RECV_ERROR] = true; + } + + return function ( + $retries, + CommandInterface $command, + RequestInterface $request, + ?ResultInterface $result = null, + $error = null + ) use ($maxRetries, $retryCurlErrors, $extraConfig) { + // Allow command-level options to override this value + $maxRetries = null !== $command['@retries'] ? + $command['@retries'] + : $maxRetries; + + $isRetryable = self::isRetryable( + $result, + $error, + $retryCurlErrors, + $extraConfig + ); + + if ($retries >= $maxRetries) { + if (!empty($error) + && $error instanceof AwsException + && $isRetryable + ) { + $error->setMaxRetriesExceeded(); + } + return false; + } + + return $isRetryable; + }; + } + + private static function isRetryable( + $result, + $error, + $retryCurlErrors, + $extraConfig = [] + ) { + $errorCodes = self::$retryCodes; + if (!empty($extraConfig['error_codes']) + && is_array($extraConfig['error_codes']) + ) { + foreach($extraConfig['error_codes'] as $code) { + $errorCodes[$code] = true; + } + } + + $statusCodes = self::$retryStatusCodes; + if (!empty($extraConfig['status_codes']) + && is_array($extraConfig['status_codes']) + ) { + foreach($extraConfig['status_codes'] as $code) { + $statusCodes[$code] = true; + } + } + + if (!empty($extraConfig['curl_errors']) + && is_array($extraConfig['curl_errors']) + ) { + foreach($extraConfig['curl_errors'] as $code) { + $retryCurlErrors[$code] = true; + } + } + + if (!$error) { + if (!isset($result['@metadata']['statusCode'])) { + return false; + } + return isset($statusCodes[$result['@metadata']['statusCode']]); + } + + if (!($error instanceof AwsException)) { + return false; + } + + if ($error->isConnectionError()) { + return true; + } + + if (isset($errorCodes[$error->getAwsErrorCode()])) { + return true; + } + + if (isset($statusCodes[$error->getStatusCode()])) { + return true; + } + + if (count($retryCurlErrors) + && ($previous = $error->getPrevious()) + && $previous instanceof RequestException + ) { + if (method_exists($previous, 'getHandlerContext')) { + $context = $previous->getHandlerContext(); + return !empty($context['errno']) + && isset($retryCurlErrors[$context['errno']]); + } + + $message = $previous->getMessage(); + foreach (array_keys($retryCurlErrors) as $curlError) { + if (strpos($message, 'cURL error ' . $curlError . ':') === 0) { + return true; + } + } + } + + return false; + } + + /** + * Delay function that calculates an exponential delay. + * + * Exponential backoff with jitter, 100ms base, 20 sec ceiling + * + * @param $retries - The number of retries that have already been attempted + * + * @return int + * + * @link https://aws.amazon.com/blogs/architecture/exponential-backoff-and-jitter/ + */ + public static function exponentialDelay($retries) + { + return mt_rand(0, (int) min(20000, (int) pow(2, $retries) * 100)); + } + + /** + * @param CommandInterface $command + * @param RequestInterface $request + * + * @return PromiseInterface + */ + public function __invoke( + CommandInterface $command, + ?RequestInterface $request = null + ) { + $retries = 0; + $requestStats = []; + $monitoringEvents = []; + $handler = $this->nextHandler; + $decider = $this->decider; + $delay = $this->delay; + + $request = $this->addRetryHeader($request, 0, 0); + + $g = function ($value) use ( + $handler, + $decider, + $delay, + $command, + $request, + &$retries, + &$requestStats, + &$monitoringEvents, + &$g + ) { + $this->updateHttpStats($value, $requestStats); + + if ($value instanceof MonitoringEventsInterface) { + $reversedEvents = array_reverse($monitoringEvents); + $monitoringEvents = array_merge($monitoringEvents, $value->getMonitoringEvents()); + foreach ($reversedEvents as $event) { + $value->prependMonitoringEvent($event); + } + } + if ($value instanceof \Exception || $value instanceof \Throwable) { + if (!$decider($retries, $command, $request, null, $value)) { + return Promise\Create::rejectionFor( + $this->bindStatsToReturn($value, $requestStats) + ); + } + } elseif ($value instanceof ResultInterface + && !$decider($retries, $command, $request, $value, null) + ) { + return $this->bindStatsToReturn($value, $requestStats); + } + + // Delay fn is called with 0, 1, ... so increment after the call. + $delayBy = $delay($retries++); + $command['@http']['delay'] = $delayBy; + if ($this->collectStats) { + $this->updateStats($retries, $delayBy, $requestStats); + } + + // Update retry header with retry count and delayBy + $request = $this->addRetryHeader($request, $retries, $delayBy); + + return $handler($command, $request)->then($g, $g); + }; + + return $handler($command, $request)->then($g, $g); + } +} diff --git a/3rdparty/aws/aws-sdk-php/src/RetryMiddlewareV2.php b/3rdparty/aws/aws-sdk-php/src/RetryMiddlewareV2.php new file mode 100644 index 00000000..ba989ad6 --- /dev/null +++ b/3rdparty/aws/aws-sdk-php/src/RetryMiddlewareV2.php @@ -0,0 +1,377 @@ + true, + 'ThrottlingException' => true, + 'ThrottledException' => true, + 'RequestThrottledException' => true, + 'TooManyRequestsException' => true, + 'ProvisionedThroughputExceededException' => true, + 'TransactionInProgressException' => true, + 'RequestLimitExceeded' => true, + 'BandwidthLimitExceeded' => true, + 'LimitExceededException' => true, + 'RequestThrottled' => true, + 'SlowDown' => true, + 'PriorRequestNotComplete' => true, + 'EC2ThrottledException' => true, + ]; + + private static $standardTransientErrors = [ + 'RequestTimeout' => true, + 'RequestTimeoutException' => true, + ]; + + private static $standardTransientStatusCodes = [ + 500 => true, + 502 => true, + 503 => true, + 504 => true, + ]; + + private $collectStats; + private $decider; + private $delayer; + private $maxAttempts; + private $maxBackoff; + private $mode; + private $nextHandler; + private $options; + private $quotaManager; + private $rateLimiter; + + public static function wrap($config, $options) + { + return function (callable $handler) use ( + $config, + $options + ) { + return new static( + $config, + $handler, + $options + ); + }; + } + + public static function createDefaultDecider( + QuotaManager $quotaManager, + $maxAttempts = 3, + $options = [] + ) { + $retryCurlErrors = []; + if (extension_loaded('curl')) { + $retryCurlErrors[CURLE_RECV_ERROR] = true; + } + + return function( + $attempts, + CommandInterface $command, + $result + ) use ($options, $quotaManager, $retryCurlErrors, $maxAttempts) { + + // Release retry tokens back to quota on a successful result + $quotaManager->releaseToQuota($result); + + // Allow command-level option to override this value + // # of attempts = # of retries + 1 + $maxAttempts = (null !== $command['@retries']) + ? $command['@retries'] + 1 + : $maxAttempts; + + $isRetryable = self::isRetryable( + $result, + $retryCurlErrors, + $options + ); + + if ($isRetryable) { + + // Retrieve retry tokens and check if quota has been exceeded + if (!$quotaManager->hasRetryQuota($result)) { + return false; + } + + if ($attempts >= $maxAttempts) { + if (!empty($result) && $result instanceof AwsException) { + $result->setMaxRetriesExceeded(); + } + return false; + } + } + + return $isRetryable; + }; + } + + public function __construct( + ConfigurationInterface $config, + callable $handler, + $options = [] + ) { + $this->options = $options; + $this->maxAttempts = $config->getMaxAttempts(); + $this->mode = $config->getMode(); + $this->nextHandler = $handler; + $this->quotaManager = new QuotaManager(); + + $this->maxBackoff = isset($options['max_backoff']) + ? $options['max_backoff'] + : 20000; + + $this->collectStats = isset($options['collect_stats']) + ? (bool) $options['collect_stats'] + : false; + + $this->decider = isset($options['decider']) + ? $options['decider'] + : self::createDefaultDecider( + $this->quotaManager, + $this->maxAttempts, + $options + ); + + $this->delayer = isset($options['delayer']) + ? $options['delayer'] + : function ($attempts) { + return $this->exponentialDelayWithJitter($attempts); + }; + + if ($this->mode === 'adaptive') { + $this->rateLimiter = isset($options['rate_limiter']) + ? $options['rate_limiter'] + : new RateLimiter(); + } + } + + public function __invoke(CommandInterface $cmd, RequestInterface $req) + { + $decider = $this->decider; + $delayer = $this->delayer; + $handler = $this->nextHandler; + + $attempts = 1; + $monitoringEvents = []; + $requestStats = []; + + $req = $this->addRetryHeader($req, 0, 0); + + $callback = function ($value) use ( + $handler, + $cmd, + $req, + $decider, + $delayer, + &$attempts, + &$requestStats, + &$monitoringEvents, + &$callback + ) { + if ($this->mode === 'adaptive') { + $this->rateLimiter->updateSendingRate($this->isThrottlingError($value)); + } + + $this->updateHttpStats($value, $requestStats); + + if ($value instanceof MonitoringEventsInterface) { + $reversedEvents = array_reverse($monitoringEvents); + $monitoringEvents = array_merge($monitoringEvents, $value->getMonitoringEvents()); + foreach ($reversedEvents as $event) { + $value->prependMonitoringEvent($event); + } + } + if ($value instanceof \Exception || $value instanceof \Throwable) { + if (!$decider($attempts, $cmd, $value)) { + return Promise\Create::rejectionFor( + $this->bindStatsToReturn($value, $requestStats) + ); + } + } elseif ($value instanceof ResultInterface + && !$decider($attempts, $cmd, $value) + ) { + return $this->bindStatsToReturn($value, $requestStats); + } + + $delayBy = $delayer($attempts++); + $cmd['@http']['delay'] = $delayBy; + if ($this->collectStats) { + $this->updateStats($attempts - 1, $delayBy, $requestStats); + } + + // Update retry header with retry count and delayBy + $req = $this->addRetryHeader($req, $attempts - 1, $delayBy); + + // Get token from rate limiter, which will sleep if necessary + if ($this->mode === 'adaptive') { + $this->rateLimiter->getSendToken(); + } + + return $handler($cmd, $req)->then($callback, $callback); + }; + + // Get token from rate limiter, which will sleep if necessary + if ($this->mode === 'adaptive') { + $this->rateLimiter->getSendToken(); + } + + return $handler($cmd, $req)->then($callback, $callback); + } + + /** + * Amount of milliseconds to delay as a function of attempt number + * + * @param $attempts + * @return mixed + */ + public function exponentialDelayWithJitter($attempts) + { + $rand = mt_rand() / mt_getrandmax(); + return min(1000 * $rand * pow(2, $attempts) , $this->maxBackoff); + } + + private static function isRetryable( + $result, + $retryCurlErrors, + $options = [] + ) { + $errorCodes = self::$standardThrottlingErrors + self::$standardTransientErrors; + if (!empty($options['transient_error_codes']) + && is_array($options['transient_error_codes']) + ) { + foreach($options['transient_error_codes'] as $code) { + $errorCodes[$code] = true; + } + } + if (!empty($options['throttling_error_codes']) + && is_array($options['throttling_error_codes']) + ) { + foreach($options['throttling_error_codes'] as $code) { + $errorCodes[$code] = true; + } + } + + $statusCodes = self::$standardTransientStatusCodes; + if (!empty($options['status_codes']) + && is_array($options['status_codes']) + ) { + foreach($options['status_codes'] as $code) { + $statusCodes[$code] = true; + } + } + + if (!empty($options['curl_errors']) + && is_array($options['curl_errors']) + ) { + foreach($options['curl_errors'] as $code) { + $retryCurlErrors[$code] = true; + } + } + + if ($result instanceof \Exception || $result instanceof \Throwable) { + $isError = true; + } else { + $isError = false; + } + + if (!$isError) { + if (!isset($result['@metadata']['statusCode'])) { + return false; + } + return isset($statusCodes[$result['@metadata']['statusCode']]); + } + + if (!($result instanceof AwsException)) { + return false; + } + + if ($result->isConnectionError()) { + return true; + } + + if (!empty($errorCodes[$result->getAwsErrorCode()])) { + return true; + } + + if (!empty($statusCodes[$result->getStatusCode()])) { + return true; + } + + if (count($retryCurlErrors) + && ($previous = $result->getPrevious()) + && $previous instanceof RequestException + ) { + if (method_exists($previous, 'getHandlerContext')) { + $context = $previous->getHandlerContext(); + return !empty($context['errno']) + && isset($retryCurlErrors[$context['errno']]); + } + + $message = $previous->getMessage(); + foreach (array_keys($retryCurlErrors) as $curlError) { + if (strpos($message, 'cURL error ' . $curlError . ':') === 0) { + return true; + } + } + } + + // Check error shape for the retryable trait + if (!empty($errorShape = $result->getAwsErrorShape())) { + $definition = $errorShape->toArray(); + if (!empty($definition['retryable'])) { + return true; + } + } + + return false; + } + + private function isThrottlingError($result) + { + if ($result instanceof AwsException) { + // Check pre-defined throttling errors + $throttlingErrors = self::$standardThrottlingErrors; + if (!empty($this->options['throttling_error_codes']) + && is_array($this->options['throttling_error_codes']) + ) { + foreach($this->options['throttling_error_codes'] as $code) { + $throttlingErrors[$code] = true; + } + } + if (!empty($result->getAwsErrorCode()) + && !empty($throttlingErrors[$result->getAwsErrorCode()]) + ) { + return true; + } + + // Check error shape for the throttling trait + if (!empty($errorShape = $result->getAwsErrorShape())) { + $definition = $errorShape->toArray(); + if (!empty($definition['retryable']['throttling'])) { + return true; + } + } + } + + return false; + } +} diff --git a/3rdparty/aws/aws-sdk-php/src/S3/AmbiguousSuccessParser.php b/3rdparty/aws/aws-sdk-php/src/S3/AmbiguousSuccessParser.php new file mode 100644 index 00000000..18d3a174 --- /dev/null +++ b/3rdparty/aws/aws-sdk-php/src/S3/AmbiguousSuccessParser.php @@ -0,0 +1,79 @@ + true, + 'UploadPartCopy' => true, + 'CopyObject' => true, + 'CompleteMultipartUpload' => true, + ]; + + /** @var callable */ + private $errorParser; + /** @var string */ + private $exceptionClass; + + public function __construct( + callable $parser, + callable $errorParser, + $exceptionClass = AwsException::class + ) { + $this->parser = $parser; + $this->errorParser = $errorParser; + $this->exceptionClass = $exceptionClass; + } + + public function __invoke( + CommandInterface $command, + ResponseInterface $response + ) { + if (200 === $response->getStatusCode() + && isset(self::$ambiguousSuccesses[$command->getName()]) + ) { + $errorParser = $this->errorParser; + try { + $parsed = $errorParser($response); + } catch (ParserException $e) { + $parsed = [ + 'code' => 'ConnectionError', + 'message' => "An error connecting to the service occurred" + . " while performing the " . $command->getName() + . " operation." + ]; + } + if (isset($parsed['code']) && isset($parsed['message'])) { + throw new $this->exceptionClass( + $parsed['message'], + $command, + ['connection_error' => true] + ); + } + } + + $fn = $this->parser; + return $fn($command, $response); + } + + public function parseMemberFromStream( + StreamInterface $stream, + StructureShape $member, + $response + ) { + return $this->parser->parseMemberFromStream($stream, $member, $response); + } +} diff --git a/3rdparty/aws/aws-sdk-php/src/S3/ApplyChecksumMiddleware.php b/3rdparty/aws/aws-sdk-php/src/S3/ApplyChecksumMiddleware.php new file mode 100644 index 00000000..2ed45192 --- /dev/null +++ b/3rdparty/aws/aws-sdk-php/src/S3/ApplyChecksumMiddleware.php @@ -0,0 +1,242 @@ + true, + 'UploadPart' => true, + ]; + + /** @var Service */ + private $api; + + /** @var array */ + private $config; + + /** @var callable */ + private $nextHandler; + + /** + * Create a middleware wrapper function. + * + * @param Service $api + * @return callable + */ + public static function wrap(Service $api, array $config = []) + { + return function (callable $handler) use ($api, $config) { + return new self($handler, $api, $config); + }; + } + + public function __construct( + callable $nextHandler, + Service $api, + array $config = [] + ) + { + $this->api = $api; + $this->nextHandler = $nextHandler; + $this->config = $config; + } + + public function __invoke( + CommandInterface $command, + RequestInterface $request + ) { + $next = $this->nextHandler; + $name = $command->getName(); + $body = $request->getBody(); + $operation = $this->api->getOperation($name); + $mode = $this->config['request_checksum_calculation'] + ?? self::DEFAULT_CALCULATION_MODE; + + $command->getMetricsBuilder()->identifyMetricByValueAndAppend( + 'request_checksum_calculation', + $mode + ); + + // Trigger warning if AddContentMD5 is specified for PutObject or UploadPart + $this->handleDeprecatedAddContentMD5($command); + + $checksumInfo = $operation['httpChecksum'] ?? []; + $checksumMemberName = $checksumInfo['requestAlgorithmMember'] ?? ''; + $checksumMember = !empty($checksumMemberName) + ? $operation->getInput()->getMember($checksumMemberName) + : null; + $checksumRequired = $checksumInfo['requestChecksumRequired'] ?? false; + $requestedAlgorithm = $command[$checksumMemberName] ?? null; + + $shouldAddChecksum = $this->shouldAddChecksum( + $mode, + $checksumRequired, + $checksumMember, + $requestedAlgorithm + ); + if ($shouldAddChecksum) { + if (!$this->hasAlgorithmHeader($request)) { + $supportedAlgorithms = array_map('strtolower', $checksumMember['enum'] ?? []); + $algorithm = $this->determineChecksumAlgorithm( + $supportedAlgorithms, + $requestedAlgorithm, + $checksumMemberName + ); + $request = $this->addAlgorithmHeader($algorithm, $request, $body); + + $command->getMetricsBuilder()->identifyMetricByValueAndAppend( + 'request_checksum', + $algorithm + ); + } + } + + // Set the content hash header if ContentSHA256 is provided + if (isset(self::$sha256[$name]) && $command['ContentSHA256']) { + $request = $request->withHeader( + 'X-Amz-Content-Sha256', + $command['ContentSHA256'] + ); + $command->getMetricsBuilder()->append( + MetricsBuilder::FLEXIBLE_CHECKSUMS_REQ_SHA256 + ); + } + + return $next($command, $request); + } + + /** + * @param CommandInterface $command + * + * @return void + */ + private function handleDeprecatedAddContentMD5(CommandInterface $command): void + { + if (!empty($command['AddContentMD5'])) { + trigger_error( + 'S3 no longer supports MD5 checksums. ' . + 'A CRC32 checksum will be computed and applied on your behalf.', + E_USER_DEPRECATED + ); + $command['ChecksumAlgorithm'] = self::DEFAULT_ALGORITHM; + } + } + + /** + * @param string $mode + * @param Shape|null $checksumMember + * @param string $name + * @param bool $checksumRequired + * @param string|null $requestedAlgorithm + * + * @return bool + */ + private function shouldAddChecksum( + string $mode, + bool $checksumRequired, + ?Shape $checksumMember, + ?string $requestedAlgorithm + ): bool + { + return ($mode === 'when_supported' && $checksumMember) + || ($mode === 'when_required' + && ($checksumRequired || ($checksumMember && $requestedAlgorithm))); + } + + /** + * @param Shape|null $checksumMember + * @param string|null $requestedAlgorithm + * @param string|null $checksumMemberName + * + * @return string + */ + private function determineChecksumAlgorithm( + array $supportedAlgorithms, + ?string $requestedAlgorithm, + ?string $checksumMemberName + ): string + { + $algorithm = self::DEFAULT_ALGORITHM; + + if ($requestedAlgorithm) { + $requestedAlgorithm = strtolower($requestedAlgorithm); + if (!in_array($requestedAlgorithm, $supportedAlgorithms)) { + throw new InvalidArgumentException( + "Unsupported algorithm supplied for input variable {$checksumMemberName}. " . + "Supported checksums for this operation include: " + . implode(", ", $supportedAlgorithms) . "." + ); + } + $algorithm = $requestedAlgorithm; + } + + return $algorithm; + } + + /** + * @param string $requestedAlgorithm + * @param RequestInterface $request + * @param StreamInterface $body + * + * @return RequestInterface + */ + private function addAlgorithmHeader( + string $requestedAlgorithm, + RequestInterface $request, + StreamInterface $body + ): RequestInterface + { + $headerName = "x-amz-checksum-{$requestedAlgorithm}"; + if (!$request->hasHeader($headerName)) { + $encoded = self::getEncodedValue($requestedAlgorithm, $body); + $request = $request->withHeader($headerName, $encoded); + } + + return $request; + } + + /** + * @param RequestInterface $request + * + * @return bool + */ + private function hasAlgorithmHeader(RequestInterface $request): bool + { + $headers = $request->getHeaders(); + + foreach ($headers as $name => $values) { + if (stripos($name, 'x-amz-checksum-') === 0) { + return true; + } + } + + return false; + } +} diff --git a/3rdparty/aws/aws-sdk-php/src/S3/BatchDelete.php b/3rdparty/aws/aws-sdk-php/src/S3/BatchDelete.php new file mode 100644 index 00000000..db81bebc --- /dev/null +++ b/3rdparty/aws/aws-sdk-php/src/S3/BatchDelete.php @@ -0,0 +1,240 @@ + 'us-west-2', + * 'version' => 'latest' + * ]); + * + * $listObjectsParams = ['Bucket' => 'foo', 'Prefix' => 'starts/with/']; + * $delete = Aws\S3\BatchDelete::fromListObjects($s3, $listObjectsParams); + * // Asynchronously delete + * $promise = $delete->promise(); + * // Force synchronous completion + * $delete->delete(); + * + * When using one of the batch delete creational static methods, you can supply + * an associative array of options: + * + * - before: Function invoked before executing a command. The function is + * passed the command that is about to be executed. This can be useful + * for logging, adding custom request headers, etc. + * - batch_size: The size of each delete batch. Defaults to 1000. + * + * @link http://docs.aws.amazon.com/AmazonS3/latest/API/multiobjectdeleteapi.html + */ +class BatchDelete implements PromisorInterface +{ + private $bucket; + /** @var AwsClientInterface */ + private $client; + /** @var callable */ + private $before; + /** @var PromiseInterface */ + private $cachedPromise; + /** @var callable */ + private $promiseCreator; + private $batchSize = 1000; + private $queue = []; + + /** + * Creates a BatchDelete object from all of the paginated results of a + * ListObjects operation. Each result that is returned by the ListObjects + * operation will be deleted. + * + * @param AwsClientInterface $client AWS Client to use. + * @param array $listObjectsParams ListObjects API parameters + * @param array $options BatchDelete options. + * + * @return BatchDelete + */ + public static function fromListObjects( + AwsClientInterface $client, + array $listObjectsParams, + array $options = [] + ) { + $iter = $client->getPaginator('ListObjects', $listObjectsParams); + $bucket = $listObjectsParams['Bucket']; + $fn = function (BatchDelete $that) use ($iter) { + return $iter->each(function ($result) use ($that) { + $promises = []; + if (is_array($result['Contents'])) { + foreach ($result['Contents'] as $object) { + if ($promise = $that->enqueue($object)) { + $promises[] = $promise; + } + } + } + return $promises ? Promise\Utils::all($promises) : null; + }); + }; + + return new self($client, $bucket, $fn, $options); + } + + /** + * Creates a BatchDelete object from an iterator that yields results. + * + * @param AwsClientInterface $client AWS Client to use to execute commands + * @param string $bucket Bucket where the objects are stored + * @param \Iterator $iter Iterator that yields assoc arrays + * @param array $options BatchDelete options + * + * @return BatchDelete + */ + public static function fromIterator( + AwsClientInterface $client, + $bucket, + \Iterator $iter, + array $options = [] + ) { + $fn = function (BatchDelete $that) use ($iter) { + return Promise\Coroutine::of(function () use ($that, $iter) { + foreach ($iter as $obj) { + if ($promise = $that->enqueue($obj)) { + yield $promise; + } + } + }); + }; + + return new self($client, $bucket, $fn, $options); + } + + /** + * @return PromiseInterface + */ + public function promise(): PromiseInterface + { + if (!$this->cachedPromise) { + $this->cachedPromise = $this->createPromise(); + } + + return $this->cachedPromise; + } + + /** + * Synchronously deletes all of the objects. + * + * @throws DeleteMultipleObjectsException on error. + */ + public function delete() + { + $this->promise()->wait(); + } + + /** + * @param AwsClientInterface $client Client used to transfer the requests + * @param string $bucket Bucket to delete from. + * @param callable $promiseFn Creates a promise. + * @param array $options Hash of options used with the batch + * + * @throws \InvalidArgumentException if the provided batch_size is <= 0 + */ + private function __construct( + AwsClientInterface $client, + $bucket, + callable $promiseFn, + array $options = [] + ) { + $this->client = $client; + $this->bucket = $bucket; + $this->promiseCreator = $promiseFn; + + if (isset($options['before'])) { + if (!is_callable($options['before'])) { + throw new \InvalidArgumentException('before must be callable'); + } + $this->before = $options['before']; + } + + if (isset($options['batch_size'])) { + if ($options['batch_size'] <= 0) { + throw new \InvalidArgumentException('batch_size is not > 0'); + } + $this->batchSize = min($options['batch_size'], 1000); + } + } + + private function enqueue(array $obj) + { + $this->queue[] = $obj; + return count($this->queue) >= $this->batchSize + ? $this->flushQueue() + : null; + } + + private function flushQueue() + { + static $validKeys = ['Key' => true, 'VersionId' => true]; + + if (count($this->queue) === 0) { + return null; + } + + $batch = []; + while ($obj = array_shift($this->queue)) { + $batch[] = array_intersect_key($obj, $validKeys); + } + + $command = $this->client->getCommand('DeleteObjects', [ + 'Bucket' => $this->bucket, + 'Delete' => ['Objects' => $batch] + ]); + + if ($this->before) { + call_user_func($this->before, $command); + } + + return $this->client->executeAsync($command) + ->then(function ($result) { + if (!empty($result['Errors'])) { + throw new DeleteMultipleObjectsException( + $result['Deleted'] ?: [], + $result['Errors'] + ); + } + return $result; + }); + } + + /** + * Returns a promise that will clean up any references when it completes. + * + * @return PromiseInterface + */ + private function createPromise() + { + // Create the promise + $promise = call_user_func($this->promiseCreator, $this); + $this->promiseCreator = null; + + // Cleans up the promise state and references. + $cleanup = function () { + $this->before = $this->client = $this->queue = null; + }; + + // When done, ensure cleanup and that any remaining are processed. + return $promise->then( + function () use ($cleanup) { + return Promise\Create::promiseFor($this->flushQueue()) + ->then($cleanup); + }, + function ($reason) use ($cleanup) { + $cleanup(); + return Promise\Create::rejectionFor($reason); + } + ); + } +} diff --git a/3rdparty/aws/aws-sdk-php/src/S3/BucketEndpointArnMiddleware.php b/3rdparty/aws/aws-sdk-php/src/S3/BucketEndpointArnMiddleware.php new file mode 100644 index 00000000..becfded6 --- /dev/null +++ b/3rdparty/aws/aws-sdk-php/src/S3/BucketEndpointArnMiddleware.php @@ -0,0 +1,355 @@ +partitionProvider = PartitionEndpointProvider::defaultProvider(); + $this->region = $region; + $this->service = $service; + $this->config = $config; + $this->nextHandler = $nextHandler; + $this->isUseEndpointV2 = $isUseEndpointV2; + } + + public function __invoke(CommandInterface $cmd, RequestInterface $req) + { + $nextHandler = $this->nextHandler; + + $op = $this->service->getOperation($cmd->getName())->toArray(); + if (!empty($op['input']['shape'])) { + $service = $this->service->toArray(); + if (!empty($input = $service['shapes'][$op['input']['shape']])) { + foreach ($input['members'] as $key => $member) { + if ($member['shape'] === 'BucketName') { + $arnableKey = $key; + break; + } + } + + if (!empty($arnableKey) && ArnParser::isArn($cmd[$arnableKey])) { + + try { + // Throw for commands that do not support ARN inputs + if (in_array($cmd->getName(), $this->nonArnableCommands)) { + throw new S3Exception( + 'ARN values cannot be used in the bucket field for' + . ' the ' . $cmd->getName() . ' operation.', + $cmd + ); + } + + if (!$this->isUseEndpointV2) { + $arn = ArnParser::parse($cmd[$arnableKey]); + $partition = $this->validateArn($arn); + $host = $this->generateAccessPointHost($arn, $req); + } + // Remove encoded bucket string from path + $path = $req->getUri()->getPath(); + $encoded = rawurlencode($cmd[$arnableKey]); + $len = strlen($encoded) + 1; + if (trim(substr($path, 0, $len), '/') === "{$encoded}") { + $path = substr($path, $len); + if (substr($path, 0, 1) !== "/") { + $path = '/' . $path; + } + } + if (empty($path)) { + $path = ''; + } + + // Set modified request + if ($this->isUseEndpointV2) { + $req = $req->withUri( + $req->getUri()->withPath($path) + + ); + goto next; + } + + $req = $req->withUri( + $req->getUri()->withPath($path)->withHost($host) + ); + + // Update signing region based on ARN data if configured to do so + if ($this->config['use_arn_region']->isUseArnRegion() + && !$this->config['use_fips_endpoint']->isUseFipsEndpoint() + ) { + $region = $arn->getRegion(); + } else { + $region = $this->region; + } + $endpointData = $partition([ + 'region' => $region, + 'service' => $arn->getService() + ]); + $cmd['@context']['signing_region'] = $endpointData['signingRegion']; + + // Update signing service for Outposts and Lambda ARNs + if ($arn instanceof OutpostsArnInterface + || $arn instanceof ObjectLambdaAccessPointArn + ) { + $cmd['@context']['signing_service'] = $arn->getService(); + } + } catch (InvalidArnException $e) { + // Add context to ARN exception + throw new S3Exception( + 'Bucket parameter parsed as ARN and failed with: ' + . $e->getMessage(), + $cmd, + [], + $e + ); + } + } + } + } + next: + return $nextHandler($cmd, $req); + } + + + private function generateAccessPointHost( + BaseAccessPointArn $arn, + RequestInterface $req + ) { + if ($arn instanceof OutpostsAccessPointArn) { + $accesspointName = $arn->getAccesspointName(); + } else { + $accesspointName = $arn->getResourceId(); + } + + if ($arn instanceof MultiRegionAccessPointArn) { + $partition = $this->partitionProvider->getPartitionByName( + $arn->getPartition(), + 's3' + ); + $dnsSuffix = $partition->getDnsSuffix(); + return "{$accesspointName}.accesspoint.s3-global.{$dnsSuffix}"; + } + + $host = "{$accesspointName}-" . $arn->getAccountId(); + + $useFips = $this->config['use_fips_endpoint']->isUseFipsEndpoint(); + $fipsString = $useFips ? "-fips" : ""; + + if ($arn instanceof OutpostsAccessPointArn) { + $host .= '.' . $arn->getOutpostId() . '.s3-outposts'; + } else if ($arn instanceof ObjectLambdaAccessPointArn) { + if (!empty($this->config['endpoint'])) { + return $host . '.' . $this->config['endpoint']; + } else { + $host .= ".s3-object-lambda{$fipsString}"; + } + } else { + $host .= ".s3-accesspoint{$fipsString}"; + if (!empty($this->config['dual_stack'])) { + $host .= '.dualstack'; + } + } + + if (!empty($this->config['use_arn_region']->isUseArnRegion())) { + $region = $arn->getRegion(); + } else { + $region = $this->region; + } + $region = \Aws\strip_fips_pseudo_regions($region); + $host .= '.' . $region . '.' . $this->getPartitionSuffix($arn, $this->partitionProvider); + return $host; + } + + /** + * Validates an ARN, returning a partition object corresponding to the ARN + * if successful + * + * @param $arn + * @return \Aws\Endpoint\Partition + */ + private function validateArn($arn) + { + if ($arn instanceof AccessPointArnInterface) { + + // Dualstack is not supported with Outposts access points + if ($arn instanceof OutpostsAccessPointArn + && !empty($this->config['dual_stack']) + ) { + throw new UnresolvedEndpointException( + 'Dualstack is currently not supported with S3 Outposts access' + . ' points. Please disable dualstack or do not supply an' + . ' access point ARN.'); + } + if ($arn instanceof MultiRegionAccessPointArn) { + if (!empty($this->config['disable_multiregion_access_points'])) { + throw new UnresolvedEndpointException( + 'Multi-Region Access Point ARNs are disabled, but one was provided. Please' + . ' enable them or provide a different ARN.' + ); + } + if (!empty($this->config['dual_stack'])) { + throw new UnresolvedEndpointException( + 'Multi-Region Access Point ARNs do not currently support dual stack. Please' + . ' disable dual stack or provide a different ARN.' + ); + } + } + // Accelerate is not supported with access points + if (!empty($this->config['accelerate'])) { + throw new UnresolvedEndpointException( + 'Accelerate is currently not supported with access points.' + . ' Please disable accelerate or do not supply an access' + . ' point ARN.'); + } + + // Path-style is not supported with access points + if (!empty($this->config['path_style'])) { + throw new UnresolvedEndpointException( + 'Path-style addressing is currently not supported with' + . ' access points. Please disable path-style or do not' + . ' supply an access point ARN.'); + } + + // Custom endpoint is not supported with access points + if (!is_null($this->config['endpoint']) + && !$arn instanceof ObjectLambdaAccessPointArn + ) { + throw new UnresolvedEndpointException( + 'A custom endpoint has been supplied along with an access' + . ' point ARN, and these are not compatible with each other.' + . ' Please only use one or the other.'); + } + + // Dualstack is not supported with object lambda access points + if ($arn instanceof ObjectLambdaAccessPointArn + && !empty($this->config['dual_stack']) + ) { + throw new UnresolvedEndpointException( + 'Dualstack is currently not supported with Object Lambda access' + . ' points. Please disable dualstack or do not supply an' + . ' access point ARN.'); + } + // Global endpoints do not support cross-region requests + if ($this->isGlobal($this->region) + && $this->config['use_arn_region']->isUseArnRegion() == false + && $arn->getRegion() != $this->region + && !$arn instanceof MultiRegionAccessPointArn + ) { + throw new UnresolvedEndpointException( + 'Global endpoints do not support cross region requests.' + . ' Please enable use_arn_region or do not supply a global region' + . ' with a different region in the ARN.'); + } + + // Get partitions for ARN and client region + $arnPart = $this->partitionProvider->getPartition( + $arn->getRegion(), + 's3' + ); + $clientPart = $this->partitionProvider->getPartition( + $this->region, + 's3' + ); + + // If client partition not found, try removing pseudo-region qualifiers + if (!($clientPart->isRegionMatch($this->region, 's3'))) { + $clientPart = $this->partitionProvider->getPartition( + \Aws\strip_fips_pseudo_regions($this->region), + 's3' + ); + } + if (!$arn instanceof MultiRegionAccessPointArn) { + // Verify that the partition matches for supplied partition and region + if ($arn->getPartition() !== $clientPart->getName()) { + throw new InvalidRegionException('The supplied ARN partition' + . " does not match the client's partition."); + } + if ($clientPart->getName() !== $arnPart->getName()) { + throw new InvalidRegionException('The corresponding partition' + . ' for the supplied ARN region does not match the' + . " client's partition."); + } + + // Ensure ARN region matches client region unless + // configured for using ARN region over client region + $this->validateMatchingRegion($arn); + + // Ensure it is not resolved to fips pseudo-region for S3 Outposts + $this->validateFipsConfigurations($arn); + } + + return $arnPart; + } + + throw new InvalidArnException('Provided ARN was not a valid S3 access' + . ' point ARN or S3 Outposts access point ARN.'); + } + + /** + * Checks if a region is global + * + * @param $region + * @return bool + */ + private function isGlobal($region) + { + return $region == 's3-external-1' || $region == 'aws-global'; + } +} diff --git a/3rdparty/aws/aws-sdk-php/src/S3/BucketEndpointMiddleware.php b/3rdparty/aws/aws-sdk-php/src/S3/BucketEndpointMiddleware.php new file mode 100644 index 00000000..1a915ca5 --- /dev/null +++ b/3rdparty/aws/aws-sdk-php/src/S3/BucketEndpointMiddleware.php @@ -0,0 +1,120 @@ + true]; + private $nextHandler; + + /** + * Create a middleware wrapper function. + * + * @return callable + */ + public static function wrap() + { + return function (callable $handler) { + return new self($handler); + }; + } + + public function __construct(callable $nextHandler) + { + $this->nextHandler = $nextHandler; + } + + public function __invoke(CommandInterface $command, RequestInterface $request) + { + $nextHandler = $this->nextHandler; + $bucket = $command['Bucket']; + + if ($bucket && !isset(self::$exclusions[$command->getName()])) { + $request = $this->modifyRequest($request, $command); + } + + return $nextHandler($command, $request); + } + + /** + * Performs a one-time removal of Bucket from path, then if + * the bucket name is duplicated in the path, performs additional + * removal which is dependent on the number of occurrences of the bucket + * name in a path-like format in the key name. + * + * @return string + */ + private function removeBucketFromPath($path, $bucket, $key) + { + $occurrencesInKey = $this->getBucketNameOccurrencesInKey($key, $bucket); + do { + $len = strlen($bucket) + 1; + if (substr($path, 0, $len) === "/{$bucket}") { + $path = substr($path, $len); + } + } while (substr_count($path, "/{$bucket}") > $occurrencesInKey + 1); + + return $path ?: '/'; + } + + private function removeDuplicateBucketFromHost($host, $bucket) + { + if (substr_count($host, $bucket) > 1) { + while (strpos($host, "{$bucket}.{$bucket}") === 0) { + $hostArr = explode('.', $host); + array_shift($hostArr); + $host = implode('.', $hostArr); + } + } + return $host; + } + + private function getBucketNameOccurrencesInKey($key, $bucket) + { + $occurrences = 0; + if (empty($key)) { + return $occurrences; + } + + $segments = explode('/', $key); + foreach($segments as $segment) { + if (strpos($segment, $bucket) === 0) { + $occurrences++; + } + } + return $occurrences; + } + + private function modifyRequest( + RequestInterface $request, + CommandInterface $command + ) { + $key = isset($command['Key']) ? $command['Key'] : null; + $uri = $request->getUri(); + $path = $uri->getPath(); + $host = $uri->getHost(); + $bucket = $command['Bucket']; + $path = $this->removeBucketFromPath($path, $bucket, $key); + $host = $this->removeDuplicateBucketFromHost($host, $bucket); + + // Modify the Key to make sure the key is encoded, but slashes are not. + if ($key) { + $path = S3Client::encodeKey(rawurldecode($path)); + } + + return $request->withUri( + $uri->withHost($host) + ->withPath($path) + ); + } +} diff --git a/3rdparty/aws/aws-sdk-php/src/S3/CalculatesChecksumTrait.php b/3rdparty/aws/aws-sdk-php/src/S3/CalculatesChecksumTrait.php new file mode 100644 index 00000000..6b2b1941 --- /dev/null +++ b/3rdparty/aws/aws-sdk-php/src/S3/CalculatesChecksumTrait.php @@ -0,0 +1,59 @@ + true, + 'crc32' => true, + 'sha256' => true, + 'sha1' => true + ]; + + /** + * @param string $requestedAlgorithm the algorithm to encode with + * @param string $value the value to be encoded + * @return string + */ + public static function getEncodedValue($requestedAlgorithm, $value) { + $requestedAlgorithm = strtolower($requestedAlgorithm); + $useCrt = extension_loaded('awscrt'); + + if (isset(self::$supportedAlgorithms[$requestedAlgorithm])) { + if ($useCrt) { + $crt = new Crt(); + switch ($requestedAlgorithm) { + case 'crc32c': + return base64_encode(pack('N*',($crt::crc32c($value)))); + case 'crc32': + return base64_encode(pack('N*',($crt::crc32($value)))); + default: + break; + } + } + + if ($requestedAlgorithm === 'crc32c') { + throw new CommonRuntimeException("crc32c is not supported for checksums " + . "without use of the common runtime for php. Please enable the CRT or choose " + . "a different algorithm." + ); + } + + if ($requestedAlgorithm === "crc32") { + $requestedAlgorithm = "crc32b"; + } + return base64_encode(Psr7\Utils::hash($value, $requestedAlgorithm, true)); + } + + $validAlgorithms = implode(', ', array_keys(self::$supportedAlgorithms)); + throw new InvalidArgumentException( + "Invalid checksum requested: {$requestedAlgorithm}." + . " Valid algorithms supported by the runtime are {$validAlgorithms}." + ); + } +} diff --git a/3rdparty/aws/aws-sdk-php/src/S3/Crypto/CryptoParamsTrait.php b/3rdparty/aws/aws-sdk-php/src/S3/Crypto/CryptoParamsTrait.php new file mode 100644 index 00000000..57253a4d --- /dev/null +++ b/3rdparty/aws/aws-sdk-php/src/S3/Crypto/CryptoParamsTrait.php @@ -0,0 +1,75 @@ +instructionFileSuffix; + } + + protected function determineGetObjectStrategy( + $result, + $instructionFileSuffix + ) { + if (isset($result['Metadata'][MetadataEnvelope::CONTENT_KEY_V2_HEADER])) { + return new HeadersMetadataStrategy(); + } + + return new InstructionFileMetadataStrategy( + $this->client, + $instructionFileSuffix + ); + } + + protected function getMetadataStrategy(array $args, $instructionFileSuffix) + { + if (!empty($args['@MetadataStrategy'])) { + if ($args['@MetadataStrategy'] instanceof MetadataStrategyInterface) { + return $args['@MetadataStrategy']; + } + + if (is_string($args['@MetadataStrategy'])) { + switch ($args['@MetadataStrategy']) { + case HeadersMetadataStrategy::class: + return new HeadersMetadataStrategy(); + case InstructionFileMetadataStrategy::class: + return new InstructionFileMetadataStrategy( + $this->client, + $instructionFileSuffix + ); + default: + throw new \InvalidArgumentException('Could not match the' + . ' specified string in "MetadataStrategy" to a' + . ' predefined strategy.'); + } + } else { + throw new \InvalidArgumentException('The metadata strategy that' + . ' was passed to "MetadataStrategy" was unrecognized.'); + } + } elseif ($instructionFileSuffix) { + return new InstructionFileMetadataStrategy( + $this->client, + $instructionFileSuffix + ); + } + + return null; + } +} diff --git a/3rdparty/aws/aws-sdk-php/src/S3/Crypto/CryptoParamsTraitV2.php b/3rdparty/aws/aws-sdk-php/src/S3/Crypto/CryptoParamsTraitV2.php new file mode 100644 index 00000000..05498176 --- /dev/null +++ b/3rdparty/aws/aws-sdk-php/src/S3/Crypto/CryptoParamsTraitV2.php @@ -0,0 +1,19 @@ +$value) { + $args['Metadata'][$header] = $value; + } + + return $args; + } + + /** + * Generates a MetadataEnvelope according to the metadata headers from the + * GetObject result. + * + * @param array $args Arguments from Command and Result that contains + * S3 Object information, relevant headers, and command + * configuration. + * + * @return MetadataEnvelope + */ + public function load(array $args) + { + $envelope = new MetadataEnvelope(); + $constantValues = MetadataEnvelope::getConstantValues(); + + foreach ($constantValues as $constant) { + if (!empty($args['Metadata'][$constant])) { + $envelope[$constant] = $args['Metadata'][$constant]; + } + } + + return $envelope; + } +} diff --git a/3rdparty/aws/aws-sdk-php/src/S3/Crypto/InstructionFileMetadataStrategy.php b/3rdparty/aws/aws-sdk-php/src/S3/Crypto/InstructionFileMetadataStrategy.php new file mode 100644 index 00000000..5065928c --- /dev/null +++ b/3rdparty/aws/aws-sdk-php/src/S3/Crypto/InstructionFileMetadataStrategy.php @@ -0,0 +1,90 @@ +suffix = empty($suffix) + ? self::DEFAULT_FILE_SUFFIX + : $suffix; + $this->client = $client; + } + + /** + * Places the information in the MetadataEnvelope to a location on S3. + * + * @param MetadataEnvelope $envelope Encryption data to save according to + * the strategy. + * @param array $args Starting arguments for PutObject, used for saving + * extra the instruction file. + * + * @return array Updated arguments for PutObject. + */ + public function save(MetadataEnvelope $envelope, array $args) + { + $this->client->putObject([ + 'Bucket' => $args['Bucket'], + 'Key' => $args['Key'] . $this->suffix, + 'Body' => json_encode($envelope) + ]); + + return $args; + } + + /** + * Uses the strategy's client to retrieve the instruction file from S3 and generates + * a MetadataEnvelope from its contents. + * + * @param array $args Arguments from Command and Result that contains + * S3 Object information, relevant headers, and command + * configuration. + * + * @return MetadataEnvelope + */ + public function load(array $args) + { + $result = $this->client->getObject([ + 'Bucket' => $args['Bucket'], + 'Key' => $args['Key'] . $this->suffix + ]); + + $metadataHeaders = json_decode($result['Body'], true); + $envelope = new MetadataEnvelope(); + $constantValues = MetadataEnvelope::getConstantValues(); + + foreach ($constantValues as $constant) { + if (!empty($metadataHeaders[$constant])) { + $envelope[$constant] = $metadataHeaders[$constant]; + } + } + + return $envelope; + } +} diff --git a/3rdparty/aws/aws-sdk-php/src/S3/Crypto/S3EncryptionClient.php b/3rdparty/aws/aws-sdk-php/src/S3/Crypto/S3EncryptionClient.php new file mode 100644 index 00000000..96cb152a --- /dev/null +++ b/3rdparty/aws/aws-sdk-php/src/S3/Crypto/S3EncryptionClient.php @@ -0,0 +1,344 @@ +client = $client; + $this->instructionFileSuffix = $instructionFileSuffix; + MetricsBuilder::appendMetricsCaptureMiddleware( + $this->client->getHandlerList(), + MetricsBuilder::S3_CRYPTO_V1N + ); + } + + private static function getDefaultStrategy() + { + return new HeadersMetadataStrategy(); + } + + /** + * Encrypts the data in the 'Body' field of $args and promises to upload it + * to the specified location on S3. + * + * @param array $args Arguments for encrypting an object and uploading it + * to S3 via PutObject. + * + * The required configuration arguments are as follows: + * + * - @MaterialsProvider: (MaterialsProvider) Provides Cek, Iv, and Cek + * encrypting/decrypting for encryption metadata. + * - @CipherOptions: (array) Cipher options for encrypting data. Only the + * Cipher option is required. Accepts the following: + * - Cipher: (string) cbc|gcm + * See also: AbstractCryptoClient::$supportedCiphers. Note that + * cbc is deprecated and gcm should be used when possible. + * - KeySize: (int) 128|192|256 + * See also: MaterialsProvider::$supportedKeySizes + * - Aad: (string) Additional authentication data. This option is + * passed directly to OpenSSL when using gcm. It is ignored when + * using cbc. Note if you pass in Aad for gcm encryption, the + * PHP SDK will be able to decrypt the resulting object, but other + * AWS SDKs may not be able to do so. + * + * The optional configuration arguments are as follows: + * + * - @MetadataStrategy: (MetadataStrategy|string|null) Strategy for storing + * MetadataEnvelope information. Defaults to using a + * HeadersMetadataStrategy. Can either be a class implementing + * MetadataStrategy, a class name of a predefined strategy, or empty/null + * to default. + * - @InstructionFileSuffix: (string|null) Suffix used when writing to an + * instruction file if using an InstructionFileMetadataHandler. + * + * @return PromiseInterface + * + * @throws \InvalidArgumentException Thrown when arguments above are not + * passed or are passed incorrectly. + */ + public function putObjectAsync(array $args) + { + $provider = $this->getMaterialsProvider($args); + unset($args['@MaterialsProvider']); + + $instructionFileSuffix = $this->getInstructionFileSuffix($args); + unset($args['@InstructionFileSuffix']); + + $strategy = $this->getMetadataStrategy($args, $instructionFileSuffix); + unset($args['@MetadataStrategy']); + + $envelope = new MetadataEnvelope(); + + return Promise\Create::promiseFor($this->encrypt( + Psr7\Utils::streamFor($args['Body']), + $args['@CipherOptions'] ?: [], + $provider, + $envelope + ))->then( + function ($encryptedBodyStream) use ($args) { + $hash = new PhpHash('sha256'); + $hashingEncryptedBodyStream = new HashingStream( + $encryptedBodyStream, + $hash, + self::getContentShaDecorator($args) + ); + return [$hashingEncryptedBodyStream, $args]; + } + )->then( + function ($putObjectContents) use ($strategy, $envelope) { + list($bodyStream, $args) = $putObjectContents; + if ($strategy === null) { + $strategy = self::getDefaultStrategy(); + } + + $updatedArgs = $strategy->save($envelope, $args); + $updatedArgs['Body'] = $bodyStream; + return $updatedArgs; + } + )->then( + function ($args) { + unset($args['@CipherOptions']); + return $this->client->putObjectAsync($args); + } + ); + } + + private static function getContentShaDecorator(&$args) + { + return function ($hash) use (&$args) { + $args['ContentSHA256'] = bin2hex($hash); + }; + } + + /** + * Encrypts the data in the 'Body' field of $args and uploads it to the + * specified location on S3. + * + * @param array $args Arguments for encrypting an object and uploading it + * to S3 via PutObject. + * + * The required configuration arguments are as follows: + * + * - @MaterialsProvider: (MaterialsProvider) Provides Cek, Iv, and Cek + * encrypting/decrypting for encryption metadata. + * - @CipherOptions: (array) Cipher options for encrypting data. A Cipher + * is required. Accepts the following options: + * - Cipher: (string) cbc|gcm + * See also: AbstractCryptoClient::$supportedCiphers. Note that + * cbc is deprecated and gcm should be used when possible. + * - KeySize: (int) 128|192|256 + * See also: MaterialsProvider::$supportedKeySizes + * - Aad: (string) Additional authentication data. This option is + * passed directly to OpenSSL when using gcm. It is ignored when + * using cbc. Note if you pass in Aad for gcm encryption, the + * PHP SDK will be able to decrypt the resulting object, but other + * AWS SDKs may not be able to do so. + * + * The optional configuration arguments are as follows: + * + * - @MetadataStrategy: (MetadataStrategy|string|null) Strategy for storing + * MetadataEnvelope information. Defaults to using a + * HeadersMetadataStrategy. Can either be a class implementing + * MetadataStrategy, a class name of a predefined strategy, or empty/null + * to default. + * - @InstructionFileSuffix: (string|null) Suffix used when writing to an + * instruction file if an using an InstructionFileMetadataHandler was + * determined. + * + * @return \Aws\Result PutObject call result with the details of uploading + * the encrypted file. + * + * @throws \InvalidArgumentException Thrown when arguments above are not + * passed or are passed incorrectly. + */ + public function putObject(array $args) + { + return $this->putObjectAsync($args)->wait(); + } + + /** + * Promises to retrieve an object from S3 and decrypt the data in the + * 'Body' field. + * + * @param array $args Arguments for retrieving an object from S3 via + * GetObject and decrypting it. + * + * The required configuration argument is as follows: + * + * - @MaterialsProvider: (MaterialsProvider) Provides Cek, Iv, and Cek + * encrypting/decrypting for decryption metadata. May have data loaded + * from the MetadataEnvelope upon decryption. + * + * The optional configuration arguments are as follows: + * + * - SaveAs: (string) The path to a file on disk to save the decrypted + * object data. This will be handled by file_put_contents instead of the + * Guzzle sink. + * + * - @MetadataStrategy: (MetadataStrategy|string|null) Strategy for reading + * MetadataEnvelope information. Defaults to determining based on object + * response headers. Can either be a class implementing MetadataStrategy, + * a class name of a predefined strategy, or empty/null to default. + * - @InstructionFileSuffix: (string) Suffix used when looking for an + * instruction file if an InstructionFileMetadataHandler is being used. + * - @CipherOptions: (array) Cipher options for decrypting data. A Cipher + * is required. Accepts the following options: + * - Aad: (string) Additional authentication data. This option is + * passed directly to OpenSSL when using gcm. It is ignored when + * using cbc. + * + * @return PromiseInterface + * + * @throws \InvalidArgumentException Thrown when required arguments are not + * passed or are passed incorrectly. + */ + public function getObjectAsync(array $args) + { + $provider = $this->getMaterialsProvider($args); + unset($args['@MaterialsProvider']); + + $instructionFileSuffix = $this->getInstructionFileSuffix($args); + unset($args['@InstructionFileSuffix']); + + $strategy = $this->getMetadataStrategy($args, $instructionFileSuffix); + unset($args['@MetadataStrategy']); + + $saveAs = null; + if (!empty($args['SaveAs'])) { + $saveAs = $args['SaveAs']; + } + + $promise = $this->client->getObjectAsync($args) + ->then( + function ($result) use ( + $provider, + $instructionFileSuffix, + $strategy, + $args + ) { + if ($strategy === null) { + $strategy = $this->determineGetObjectStrategy( + $result, + $instructionFileSuffix + ); + } + + $envelope = $strategy->load($args + [ + 'Metadata' => $result['Metadata'] + ]); + + $provider = $provider->fromDecryptionEnvelope($envelope); + + $result['Body'] = $this->decrypt( + $result['Body'], + $provider, + $envelope, + isset($args['@CipherOptions']) + ? $args['@CipherOptions'] + : [] + ); + return $result; + } + )->then( + function ($result) use ($saveAs) { + if (!empty($saveAs)) { + file_put_contents( + $saveAs, + (string)$result['Body'], + LOCK_EX + ); + } + return $result; + } + ); + + return $promise; + } + + /** + * Retrieves an object from S3 and decrypts the data in the 'Body' field. + * + * @param array $args Arguments for retrieving an object from S3 via + * GetObject and decrypting it. + * + * The required configuration argument is as follows: + * + * - @MaterialsProvider: (MaterialsProvider) Provides Cek, Iv, and Cek + * encrypting/decrypting for decryption metadata. May have data loaded + * from the MetadataEnvelope upon decryption. + * + * The optional configuration arguments are as follows: + * + * - SaveAs: (string) The path to a file on disk to save the decrypted + * object data. This will be handled by file_put_contents instead of the + * Guzzle sink. + * - @InstructionFileSuffix: (string|null) Suffix used when looking for an + * instruction file if an InstructionFileMetadataHandler was detected. + * - @CipherOptions: (array) Cipher options for encrypting data. A Cipher + * is required. Accepts the following options: + * - Aad: (string) Additional authentication data. This option is + * passed directly to OpenSSL when using gcm. It is ignored when + * using cbc. + * + * @return \Aws\Result GetObject call result with the 'Body' field + * wrapped in a decryption stream with its metadata + * information. + * + * @throws \InvalidArgumentException Thrown when arguments above are not + * passed or are passed incorrectly. + */ + public function getObject(array $args) + { + return $this->getObjectAsync($args)->wait(); + } +} diff --git a/3rdparty/aws/aws-sdk-php/src/S3/Crypto/S3EncryptionClientV2.php b/3rdparty/aws/aws-sdk-php/src/S3/Crypto/S3EncryptionClientV2.php new file mode 100644 index 00000000..fe917800 --- /dev/null +++ b/3rdparty/aws/aws-sdk-php/src/S3/Crypto/S3EncryptionClientV2.php @@ -0,0 +1,450 @@ + + * use Aws\Crypto\KmsMaterialsProviderV2; + * use Aws\S3\Crypto\S3EncryptionClientV2; + * use Aws\S3\S3Client; + * + * $encryptionClient = new S3EncryptionClientV2( + * new S3Client([ + * 'region' => 'us-west-2', + * 'version' => 'latest' + * ]) + * ); + * $materialsProvider = new KmsMaterialsProviderV2( + * new KmsClient([ + * 'profile' => 'default', + * 'region' => 'us-east-1', + * 'version' => 'latest', + * ], + * 'your-kms-key-id' + * ); + * + * $encryptionClient->putObject([ + * '@MaterialsProvider' => $materialsProvider, + * '@CipherOptions' => [ + * 'Cipher' => 'gcm', + * 'KeySize' => 256, + * ], + * '@KmsEncryptionContext' => ['foo' => 'bar'], + * 'Bucket' => 'your-bucket', + * 'Key' => 'your-key', + * 'Body' => 'your-encrypted-data', + * ]); + * + * + * Example read call (using objects from previous example): + * + * + * $encryptionClient->getObject([ + * '@MaterialsProvider' => $materialsProvider, + * '@CipherOptions' => [ + * 'Cipher' => 'gcm', + * 'KeySize' => 256, + * ], + * 'Bucket' => 'your-bucket', + * 'Key' => 'your-key', + * ]); + * + */ +class S3EncryptionClientV2 extends AbstractCryptoClientV2 +{ + use CipherBuilderTrait; + use CryptoParamsTraitV2; + use DecryptionTraitV2; + use EncryptionTraitV2; + use UserAgentTrait; + + const CRYPTO_VERSION = '2.1'; + + private $client; + private $instructionFileSuffix; + private $legacyWarningCount; + + /** + * @param S3Client $client The S3Client to be used for true uploading and + * retrieving objects from S3 when using the + * encryption client. + * @param string|null $instructionFileSuffix Suffix for a client wide + * default when using instruction + * files for metadata storage. + */ + public function __construct( + S3Client $client, + $instructionFileSuffix = null + ) { + $this->client = $client; + $this->instructionFileSuffix = $instructionFileSuffix; + $this->legacyWarningCount = 0; + MetricsBuilder::appendMetricsCaptureMiddleware( + $this->client->getHandlerList(), + MetricsBuilder::S3_CRYPTO_V2 + ); + } + + private static function getDefaultStrategy() + { + return new HeadersMetadataStrategy(); + } + + /** + * Encrypts the data in the 'Body' field of $args and promises to upload it + * to the specified location on S3. + * + * Note that for PHP versions of < 7.1, this operation uses an AES-GCM + * polyfill for encryption since there is no native PHP support. The + * performance for large inputs will be a lot slower than for PHP 7.1+, so + * upgrading older PHP version environments may be necessary to use this + * effectively. + * + * @param array $args Arguments for encrypting an object and uploading it + * to S3 via PutObject. + * + * The required configuration arguments are as follows: + * + * - @MaterialsProvider: (MaterialsProviderV2) Provides Cek, Iv, and Cek + * encrypting/decrypting for encryption metadata. + * - @CipherOptions: (array) Cipher options for encrypting data. Only the + * Cipher option is required. Accepts the following: + * - Cipher: (string) gcm + * See also: AbstractCryptoClientV2::$supportedCiphers + * - KeySize: (int) 128|256 + * See also: MaterialsProvider::$supportedKeySizes + * - Aad: (string) Additional authentication data. This option is + * passed directly to OpenSSL when using gcm. Note if you pass in + * Aad, the PHP SDK will be able to decrypt the resulting object, + * but other AWS SDKs may not be able to do so. + * - @KmsEncryptionContext: (array) Only required if using + * KmsMaterialsProviderV2. An associative array of key-value + * pairs to be added to the encryption context for KMS key encryption. An + * empty array may be passed if no additional context is desired. + * + * The optional configuration arguments are as follows: + * + * - @MetadataStrategy: (MetadataStrategy|string|null) Strategy for storing + * MetadataEnvelope information. Defaults to using a + * HeadersMetadataStrategy. Can either be a class implementing + * MetadataStrategy, a class name of a predefined strategy, or empty/null + * to default. + * - @InstructionFileSuffix: (string|null) Suffix used when writing to an + * instruction file if using an InstructionFileMetadataHandler. + * + * @return PromiseInterface + * + * @throws \InvalidArgumentException Thrown when arguments above are not + * passed or are passed incorrectly. + */ + public function putObjectAsync(array $args) + { + $provider = $this->getMaterialsProvider($args); + unset($args['@MaterialsProvider']); + + $instructionFileSuffix = $this->getInstructionFileSuffix($args); + unset($args['@InstructionFileSuffix']); + + $strategy = $this->getMetadataStrategy($args, $instructionFileSuffix); + unset($args['@MetadataStrategy']); + + $envelope = new MetadataEnvelope(); + + return Promise\Create::promiseFor($this->encrypt( + Psr7\Utils::streamFor($args['Body']), + $args, + $provider, + $envelope + ))->then( + function ($encryptedBodyStream) use ($args) { + $hash = new PhpHash('sha256'); + $hashingEncryptedBodyStream = new HashingStream( + $encryptedBodyStream, + $hash, + self::getContentShaDecorator($args) + ); + return [$hashingEncryptedBodyStream, $args]; + } + )->then( + function ($putObjectContents) use ($strategy, $envelope) { + list($bodyStream, $args) = $putObjectContents; + if ($strategy === null) { + $strategy = self::getDefaultStrategy(); + } + + $updatedArgs = $strategy->save($envelope, $args); + $updatedArgs['Body'] = $bodyStream; + return $updatedArgs; + } + )->then( + function ($args) { + unset($args['@CipherOptions']); + return $this->client->putObjectAsync($args); + } + ); + } + + private static function getContentShaDecorator(&$args) + { + return function ($hash) use (&$args) { + $args['ContentSHA256'] = bin2hex($hash); + }; + } + + /** + * Encrypts the data in the 'Body' field of $args and uploads it to the + * specified location on S3. + * + * Note that for PHP versions of < 7.1, this operation uses an AES-GCM + * polyfill for encryption since there is no native PHP support. The + * performance for large inputs will be a lot slower than for PHP 7.1+, so + * upgrading older PHP version environments may be necessary to use this + * effectively. + * + * @param array $args Arguments for encrypting an object and uploading it + * to S3 via PutObject. + * + * The required configuration arguments are as follows: + * + * - @MaterialsProvider: (MaterialsProvider) Provides Cek, Iv, and Cek + * encrypting/decrypting for encryption metadata. + * - @CipherOptions: (array) Cipher options for encrypting data. A Cipher + * is required. Accepts the following options: + * - Cipher: (string) gcm + * See also: AbstractCryptoClientV2::$supportedCiphers + * - KeySize: (int) 128|256 + * See also: MaterialsProvider::$supportedKeySizes + * - Aad: (string) Additional authentication data. This option is + * passed directly to OpenSSL when using gcm. Note if you pass in + * Aad, the PHP SDK will be able to decrypt the resulting object, + * but other AWS SDKs may not be able to do so. + * - @KmsEncryptionContext: (array) Only required if using + * KmsMaterialsProviderV2. An associative array of key-value + * pairs to be added to the encryption context for KMS key encryption. An + * empty array may be passed if no additional context is desired. + * + * The optional configuration arguments are as follows: + * + * - @MetadataStrategy: (MetadataStrategy|string|null) Strategy for storing + * MetadataEnvelope information. Defaults to using a + * HeadersMetadataStrategy. Can either be a class implementing + * MetadataStrategy, a class name of a predefined strategy, or empty/null + * to default. + * - @InstructionFileSuffix: (string|null) Suffix used when writing to an + * instruction file if an using an InstructionFileMetadataHandler was + * determined. + * + * @return \Aws\Result PutObject call result with the details of uploading + * the encrypted file. + * + * @throws \InvalidArgumentException Thrown when arguments above are not + * passed or are passed incorrectly. + */ + public function putObject(array $args) + { + return $this->putObjectAsync($args)->wait(); + } + + /** + * Promises to retrieve an object from S3 and decrypt the data in the + * 'Body' field. + * + * @param array $args Arguments for retrieving an object from S3 via + * GetObject and decrypting it. + * + * The required configuration argument is as follows: + * + * - @MaterialsProvider: (MaterialsProviderInterface) Provides Cek, Iv, and Cek + * encrypting/decrypting for decryption metadata. May have data loaded + * from the MetadataEnvelope upon decryption. + * - @SecurityProfile: (string) Must be set to 'V2' or 'V2_AND_LEGACY'. + * - 'V2' indicates that only objects encrypted with S3EncryptionClientV2 + * content encryption and key wrap schemas are able to be decrypted. + * - 'V2_AND_LEGACY' indicates that objects encrypted with both + * S3EncryptionClientV2 and older legacy encryption clients are able + * to be decrypted. + * + * The optional configuration arguments are as follows: + * + * - SaveAs: (string) The path to a file on disk to save the decrypted + * object data. This will be handled by file_put_contents instead of the + * Guzzle sink. + * + * - @MetadataStrategy: (MetadataStrategy|string|null) Strategy for reading + * MetadataEnvelope information. Defaults to determining based on object + * response headers. Can either be a class implementing MetadataStrategy, + * a class name of a predefined strategy, or empty/null to default. + * - @InstructionFileSuffix: (string) Suffix used when looking for an + * instruction file if an InstructionFileMetadataHandler is being used. + * - @CipherOptions: (array) Cipher options for decrypting data. A Cipher + * is required. Accepts the following options: + * - Aad: (string) Additional authentication data. This option is + * passed directly to OpenSSL when using gcm. It is ignored when + * using cbc. + * - @KmsAllowDecryptWithAnyCmk: (bool) This allows decryption with + * KMS materials for any KMS key ID, instead of needing the KMS key ID to + * be specified and provided to the decrypt operation. Ignored for non-KMS + * materials providers. Defaults to false. + * + * @return PromiseInterface + * + * @throws \InvalidArgumentException Thrown when required arguments are not + * passed or are passed incorrectly. + */ + public function getObjectAsync(array $args) + { + $provider = $this->getMaterialsProvider($args); + unset($args['@MaterialsProvider']); + + $instructionFileSuffix = $this->getInstructionFileSuffix($args); + unset($args['@InstructionFileSuffix']); + + $strategy = $this->getMetadataStrategy($args, $instructionFileSuffix); + unset($args['@MetadataStrategy']); + + if (!isset($args['@SecurityProfile']) + || !in_array($args['@SecurityProfile'], self::$supportedSecurityProfiles) + ) { + throw new CryptoException("@SecurityProfile is required and must be" + . " set to 'V2' or 'V2_AND_LEGACY'"); + } + + // Only throw this legacy warning once per client + if (in_array($args['@SecurityProfile'], self::$legacySecurityProfiles) + && $this->legacyWarningCount < 1 + ) { + $this->legacyWarningCount++; + trigger_error( + "This S3 Encryption Client operation is configured to" + . " read encrypted data with legacy encryption modes. If you" + . " don't have objects encrypted with these legacy modes," + . " you should disable support for them to enhance security. ", + E_USER_WARNING + ); + } + + $saveAs = null; + if (!empty($args['SaveAs'])) { + $saveAs = $args['SaveAs']; + } + + $promise = $this->client->getObjectAsync($args) + ->then( + function ($result) use ( + $provider, + $instructionFileSuffix, + $strategy, + $args + ) { + if ($strategy === null) { + $strategy = $this->determineGetObjectStrategy( + $result, + $instructionFileSuffix + ); + } + + $envelope = $strategy->load($args + [ + 'Metadata' => $result['Metadata'] + ]); + + $result['Body'] = $this->decrypt( + $result['Body'], + $provider, + $envelope, + $args + ); + return $result; + } + )->then( + function ($result) use ($saveAs) { + if (!empty($saveAs)) { + file_put_contents( + $saveAs, + (string)$result['Body'], + LOCK_EX + ); + } + return $result; + } + ); + + return $promise; + } + + /** + * Retrieves an object from S3 and decrypts the data in the 'Body' field. + * + * @param array $args Arguments for retrieving an object from S3 via + * GetObject and decrypting it. + * + * The required configuration argument is as follows: + * + * - @MaterialsProvider: (MaterialsProviderInterface) Provides Cek, Iv, and Cek + * encrypting/decrypting for decryption metadata. May have data loaded + * from the MetadataEnvelope upon decryption. + * - @SecurityProfile: (string) Must be set to 'V2' or 'V2_AND_LEGACY'. + * - 'V2' indicates that only objects encrypted with S3EncryptionClientV2 + * content encryption and key wrap schemas are able to be decrypted. + * - 'V2_AND_LEGACY' indicates that objects encrypted with both + * S3EncryptionClientV2 and older legacy encryption clients are able + * to be decrypted. + * + * The optional configuration arguments are as follows: + * + * - SaveAs: (string) The path to a file on disk to save the decrypted + * object data. This will be handled by file_put_contents instead of the + * Guzzle sink. + * - @InstructionFileSuffix: (string|null) Suffix used when looking for an + * instruction file if an InstructionFileMetadataHandler was detected. + * - @CipherOptions: (array) Cipher options for encrypting data. A Cipher + * is required. Accepts the following options: + * - Aad: (string) Additional authentication data. This option is + * passed directly to OpenSSL when using gcm. It is ignored when + * using cbc. + * - @KmsAllowDecryptWithAnyCmk: (bool) This allows decryption with + * KMS materials for any KMS key ID, instead of needing the KMS key ID to + * be specified and provided to the decrypt operation. Ignored for non-KMS + * materials providers. Defaults to false. + * + * @return \Aws\Result GetObject call result with the 'Body' field + * wrapped in a decryption stream with its metadata + * information. + * + * @throws \InvalidArgumentException Thrown when arguments above are not + * passed or are passed incorrectly. + */ + public function getObject(array $args) + { + return $this->getObjectAsync($args)->wait(); + } +} diff --git a/3rdparty/aws/aws-sdk-php/src/S3/Crypto/S3EncryptionMultipartUploader.php b/3rdparty/aws/aws-sdk-php/src/S3/Crypto/S3EncryptionMultipartUploader.php new file mode 100644 index 00000000..ddf1d2df --- /dev/null +++ b/3rdparty/aws/aws-sdk-php/src/S3/Crypto/S3EncryptionMultipartUploader.php @@ -0,0 +1,169 @@ +appendUserAgent($client, 'feat/s3-encrypt/' . self::CRYPTO_VERSION); + $this->client = $client; + $config['params'] = []; + if (!empty($config['bucket'])) { + $config['params']['Bucket'] = $config['bucket']; + } + if (!empty($config['key'])) { + $config['params']['Key'] = $config['key']; + } + + $this->provider = $this->getMaterialsProvider($config); + unset($config['@MaterialsProvider']); + + $this->instructionFileSuffix = $this->getInstructionFileSuffix($config); + unset($config['@InstructionFileSuffix']); + $this->strategy = $this->getMetadataStrategy( + $config, + $this->instructionFileSuffix + ); + if ($this->strategy === null) { + $this->strategy = self::getDefaultStrategy(); + } + unset($config['@MetadataStrategy']); + + $config['prepare_data_source'] = $this->getEncryptingDataPreparer(); + + parent::__construct($client, $source, $config); + } + + private static function getDefaultStrategy() + { + return new HeadersMetadataStrategy(); + } + + private function getEncryptingDataPreparer() + { + return function() { + // Defer encryption work until promise is executed + $envelope = new MetadataEnvelope(); + + list($this->source, $params) = Promise\Create::promiseFor($this->encrypt( + $this->source, + $this->config['@cipheroptions'] ?: [], + $this->provider, + $envelope + ))->then( + function ($bodyStream) use ($envelope) { + $params = $this->strategy->save( + $envelope, + $this->config['params'] + ); + return [$bodyStream, $params]; + } + )->wait(); + + $this->source->rewind(); + $this->config['params'] = $params; + }; + } +} \ No newline at end of file diff --git a/3rdparty/aws/aws-sdk-php/src/S3/Crypto/S3EncryptionMultipartUploaderV2.php b/3rdparty/aws/aws-sdk-php/src/S3/Crypto/S3EncryptionMultipartUploaderV2.php new file mode 100644 index 00000000..1bdbccf3 --- /dev/null +++ b/3rdparty/aws/aws-sdk-php/src/S3/Crypto/S3EncryptionMultipartUploaderV2.php @@ -0,0 +1,176 @@ +appendUserAgent($client, 'feat/s3-encrypt/' . self::CRYPTO_VERSION); + $this->client = $client; + $config['params'] = []; + if (!empty($config['bucket'])) { + $config['params']['Bucket'] = $config['bucket']; + } + if (!empty($config['key'])) { + $config['params']['Key'] = $config['key']; + } + + $this->provider = $this->getMaterialsProvider($config); + unset($config['@MaterialsProvider']); + + $this->instructionFileSuffix = $this->getInstructionFileSuffix($config); + unset($config['@InstructionFileSuffix']); + $this->strategy = $this->getMetadataStrategy( + $config, + $this->instructionFileSuffix + ); + if ($this->strategy === null) { + $this->strategy = self::getDefaultStrategy(); + } + unset($config['@MetadataStrategy']); + + $config['prepare_data_source'] = $this->getEncryptingDataPreparer(); + + parent::__construct($client, $source, $config); + } + + private static function getDefaultStrategy() + { + return new HeadersMetadataStrategy(); + } + + private function getEncryptingDataPreparer() + { + return function() { + // Defer encryption work until promise is executed + $envelope = new MetadataEnvelope(); + + list($this->source, $params) = Promise\Create::promiseFor($this->encrypt( + $this->source, + $this->config ?: [], + $this->provider, + $envelope + ))->then( + function ($bodyStream) use ($envelope) { + $params = $this->strategy->save( + $envelope, + $this->config['params'] + ); + return [$bodyStream, $params]; + } + )->wait(); + + $this->source->rewind(); + $this->config['params'] = $params; + }; + } +} diff --git a/3rdparty/aws/aws-sdk-php/src/S3/Crypto/UserAgentTrait.php b/3rdparty/aws/aws-sdk-php/src/S3/Crypto/UserAgentTrait.php new file mode 100644 index 00000000..45662758 --- /dev/null +++ b/3rdparty/aws/aws-sdk-php/src/S3/Crypto/UserAgentTrait.php @@ -0,0 +1,31 @@ +getHandlerList(); + $list->appendBuild(Middleware::mapRequest( + function(RequestInterface $req) use ($agentString) { + if (!empty($req->getHeader('User-Agent')) + && !empty($req->getHeader('User-Agent')[0]) + ) { + $userAgent = $req->getHeader('User-Agent')[0]; + if (strpos($userAgent, $agentString) === false) { + $userAgent .= " {$agentString}"; + }; + } else { + $userAgent = $agentString; + } + + $req = $req->withHeader('User-Agent', $userAgent); + return $req; + } + )); + } +} diff --git a/3rdparty/aws/aws-sdk-php/src/S3/EndpointRegionHelperTrait.php b/3rdparty/aws/aws-sdk-php/src/S3/EndpointRegionHelperTrait.php new file mode 100644 index 00000000..3c35c4b9 --- /dev/null +++ b/3rdparty/aws/aws-sdk-php/src/S3/EndpointRegionHelperTrait.php @@ -0,0 +1,106 @@ +getPartition( + $arn->getRegion(), + $arn->getService() + ); + return $partition->getDnsSuffix(); + } + + private function getSigningRegion( + $region, + $service, + PartitionEndpointProvider $provider + ) { + $partition = $provider->getPartition($region, $service); + $data = $partition->toArray(); + if (isset($data['services'][$service]['endpoints'][$region]['credentialScope']['region'])) { + return $data['services'][$service]['endpoints'][$region]['credentialScope']['region']; + } + return $region; + } + + private function isMatchingSigningRegion( + $arnRegion, + $clientRegion, + $service, + PartitionEndpointProvider $provider + ) { + $arnRegion = \Aws\strip_fips_pseudo_regions(strtolower($arnRegion)); + $clientRegion = strtolower($clientRegion); + if ($arnRegion === $clientRegion) { + return true; + } + if ($this->getSigningRegion($clientRegion, $service, $provider) === $arnRegion) { + return true; + } + return false; + } + + private function validateFipsConfigurations(ArnInterface $arn) + { + $useFipsEndpoint = !empty($this->config['use_fips_endpoint']); + if ($arn instanceof OutpostsArnInterface) { + if (empty($this->config['use_arn_region']) + || !($this->config['use_arn_region']->isUseArnRegion()) + ) { + $region = $this->region; + } else { + $region = $arn->getRegion(); + } + if (\Aws\is_fips_pseudo_region($region)) { + throw new InvalidRegionException( + 'Fips is currently not supported with S3 Outposts access' + . ' points. Please provide a non-fips region or do not supply an' + . ' access point ARN.'); + } + } + } + + private function validateMatchingRegion(ArnInterface $arn) + { + if (!($this->isMatchingSigningRegion( + $arn->getRegion(), + $this->region, + $this->service->getEndpointPrefix(), + $this->partitionProvider) + )) { + if (empty($this->config['use_arn_region']) + || !($this->config['use_arn_region']->isUseArnRegion()) + ) { + throw new InvalidRegionException('The region' + . " specified in the ARN (" . $arn->getRegion() + . ") does not match the client region (" + . "{$this->region})."); + } + } + } +} diff --git a/3rdparty/aws/aws-sdk-php/src/S3/Exception/DeleteMultipleObjectsException.php b/3rdparty/aws/aws-sdk-php/src/S3/Exception/DeleteMultipleObjectsException.php new file mode 100644 index 00000000..5b4c2890 --- /dev/null +++ b/3rdparty/aws/aws-sdk-php/src/S3/Exception/DeleteMultipleObjectsException.php @@ -0,0 +1,68 @@ +deleted = array_values($deleted); + $this->errors = array_values($errors); + parent::__construct('Unable to delete certain keys when executing a' + . ' DeleteMultipleObjects request: ' + . self::createMessageFromErrors($errors)); + } + + /** + * Create a single error message from multiple errors. + * + * @param array $errors Errors encountered + * + * @return string + */ + public static function createMessageFromErrors(array $errors) + { + return "\n- " . implode("\n- ", array_map(function ($key) { + return json_encode($key); + }, $errors)); + } + + /** + * Get the errored objects + * + * @return array Returns an array of associative arrays, each containing + * a 'Code', 'Message', and 'Key' key. + */ + public function getErrors() + { + return $this->errors; + } + + /** + * Get the successfully deleted objects + * + * @return array Returns an array of associative arrays, each containing + * a 'Key' and optionally 'DeleteMarker' and + * 'DeleterMarkerVersionId' + */ + public function getDeleted() + { + return $this->deleted; + } +} diff --git a/3rdparty/aws/aws-sdk-php/src/S3/Exception/PermanentRedirectException.php b/3rdparty/aws/aws-sdk-php/src/S3/Exception/PermanentRedirectException.php new file mode 100644 index 00000000..67d916e8 --- /dev/null +++ b/3rdparty/aws/aws-sdk-php/src/S3/Exception/PermanentRedirectException.php @@ -0,0 +1,4 @@ +collectPathInfo($error->getCommand()); + } elseif ($prev instanceof AwsException) { + $this->collectPathInfo($prev->getCommand()); + } + parent::__construct($state, $prev); + } + + /** + * Get the Bucket information of the transfer object + * + * @return string|null Returns null when 'Bucket' information + * is unavailable. + */ + public function getBucket() + { + return $this->bucket; + } + + /** + * Get the Key information of the transfer object + * + * @return string|null Returns null when 'Key' information + * is unavailable. + */ + public function getKey() + { + return $this->key; + } + + /** + * Get the source file name of the transfer object + * + * @return string|null Returns null when metadata of the stream + * wrapped in 'Body' parameter is unavailable. + */ + public function getSourceFileName() + { + return $this->filename; + } + + /** + * Collect file path information when accessible. (Bucket, Key) + * + * @param CommandInterface $cmd + */ + private function collectPathInfo(CommandInterface $cmd) + { + if (empty($this->bucket) && isset($cmd['Bucket'])) { + $this->bucket = $cmd['Bucket']; + } + if (empty($this->key) && isset($cmd['Key'])) { + $this->key = $cmd['Key']; + } + if (empty($this->filename) && isset($cmd['Body'])) { + $this->filename = $cmd['Body']->getMetadata('uri'); + } + } +} diff --git a/3rdparty/aws/aws-sdk-php/src/S3/ExpiresParsingMiddleware.php b/3rdparty/aws/aws-sdk-php/src/S3/ExpiresParsingMiddleware.php new file mode 100644 index 00000000..5bdb34a7 --- /dev/null +++ b/3rdparty/aws/aws-sdk-php/src/S3/ExpiresParsingMiddleware.php @@ -0,0 +1,56 @@ +nextHandler = $nextHandler; + } + + public function __invoke(CommandInterface $command, ?RequestInterface $request = null) + { + $next = $this->nextHandler; + return $next($command, $request)->then( + function (ResultInterface $result) { + if (empty($result['Expires']) && !empty($result['ExpiresString'])) { + trigger_error( + "Failed to parse the `expires` header as a timestamp due to " + . " an invalid timestamp format.\nPlease refer to `ExpiresString` " + . "for the unparsed string format of this header.\n" + , E_USER_WARNING + ); + } + return $result; + } + ); + } +} diff --git a/3rdparty/aws/aws-sdk-php/src/S3/GetBucketLocationParser.php b/3rdparty/aws/aws-sdk-php/src/S3/GetBucketLocationParser.php new file mode 100644 index 00000000..94aee696 --- /dev/null +++ b/3rdparty/aws/aws-sdk-php/src/S3/GetBucketLocationParser.php @@ -0,0 +1,49 @@ +parser = $parser; + } + + public function __invoke( + CommandInterface $command, + ResponseInterface $response + ) { + $fn = $this->parser; + $result = $fn($command, $response); + + if ($command->getName() === 'GetBucketLocation') { + $location = 'us-east-1'; + if (preg_match('/>(.+?)<\/LocationConstraint>/', $response->getBody(), $matches)) { + $location = $matches[1] === 'EU' ? 'eu-west-1' : $matches[1]; + } + $result['LocationConstraint'] = $location; + } + + return $result; + } + + public function parseMemberFromStream( + StreamInterface $stream, + StructureShape $member, + $response + ) { + return $this->parser->parseMemberFromStream($stream, $member, $response); + } +} diff --git a/3rdparty/aws/aws-sdk-php/src/S3/MultipartCopy.php b/3rdparty/aws/aws-sdk-php/src/S3/MultipartCopy.php new file mode 100644 index 00000000..6a7bc04d --- /dev/null +++ b/3rdparty/aws/aws-sdk-php/src/S3/MultipartCopy.php @@ -0,0 +1,249 @@ +/). If the key contains a '?' + * character, instead pass an array of source_key, + * source_bucket, and source_version_id. + * @param array $config Configuration used to perform the upload. + */ + public function __construct( + S3ClientInterface $client, + $source, + array $config = [] + ) { + if (is_array($source)) { + $this->source = $source; + } else { + $this->source = $this->getInputSource($source); + } + + parent::__construct( + $client, + array_change_key_case($config) + ['source_metadata' => null] + ); + + if ($this->displayProgress) { + $this->getState()->setProgressThresholds( + $this->sourceMetadata["ContentLength"] + ); + } + } + + /** + * An alias of the self::upload method. + * + * @see self::upload + */ + public function copy() + { + return $this->upload(); + } + + protected function loadUploadWorkflowInfo() + { + return [ + 'command' => [ + 'initiate' => 'CreateMultipartUpload', + 'upload' => 'UploadPartCopy', + 'complete' => 'CompleteMultipartUpload', + ], + 'id' => [ + 'bucket' => 'Bucket', + 'key' => 'Key', + 'upload_id' => 'UploadId', + ], + 'part_num' => 'PartNumber', + ]; + } + + protected function getUploadCommands(callable $resultHandler) + { + $parts = ceil($this->getSourceSize() / $this->determinePartSize()); + + for ($partNumber = 1; $partNumber <= $parts; $partNumber++) { + // If we haven't already uploaded this part, yield a new part. + if (!$this->state->hasPartBeenUploaded($partNumber)) { + $command = $this->client->getCommand( + $this->info['command']['upload'], + $this->createPart($partNumber, $parts) + $this->getState()->getId() + ); + $command->getHandlerList()->appendSign($resultHandler, 'mup'); + yield $command; + } + } + } + + private function createPart($partNumber, $partsCount) + { + $data = []; + + // Apply custom params to UploadPartCopy data + $config = $this->getConfig(); + $params = isset($config['params']) ? $config['params'] : []; + foreach ($params as $k => $v) { + $data[$k] = $v; + } + // The source parameter here is usually a string, but can be overloaded as an array + // if the key contains a '?' character to specify where the query parameters start + if (is_array($this->source)) { + $key = str_replace('%2F', '/', rawurlencode($this->source['source_key'])); + $bucket = $this->source['source_bucket']; + } else { + list($bucket, $key) = explode('/', ltrim($this->source, '/'), 2); + $key = implode( + '/', + array_map( + 'urlencode', + explode('/', rawurldecode($key)) + ) + ); + } + + $uri = ArnParser::isArn($bucket) ? '' : '/'; + $uri .= $bucket . '/' . $key; + $data['CopySource'] = $uri; + $data['PartNumber'] = $partNumber; + if (!empty($this->sourceVersionId)) { + $data['CopySource'] .= "?versionId=" . $this->sourceVersionId; + } + + $defaultPartSize = $this->determinePartSize(); + $startByte = $defaultPartSize * ($partNumber - 1); + $data['ContentLength'] = $partNumber < $partsCount + ? $defaultPartSize + : $this->getSourceSize() - ($defaultPartSize * ($partsCount - 1)); + $endByte = $startByte + $data['ContentLength'] - 1; + $data['CopySourceRange'] = "bytes=$startByte-$endByte"; + + return $data; + } + + protected function extractETag(ResultInterface $result) + { + return $result->search('CopyPartResult.ETag'); + } + + protected function getSourceMimeType() + { + return $this->getSourceMetadata()['ContentType']; + } + + protected function getSourceSize() + { + return $this->getSourceMetadata()['ContentLength']; + } + + private function getSourceMetadata() + { + if (empty($this->sourceMetadata)) { + $this->sourceMetadata = $this->fetchSourceMetadata(); + } + + return $this->sourceMetadata; + } + + private function fetchSourceMetadata() + { + if ($this->config['source_metadata'] instanceof ResultInterface) { + return $this->config['source_metadata']; + } + //if the source variable was overloaded with an array, use the inputs for key and bucket + if (is_array($this->source)) { + $headParams = [ + 'Key' => $this->source['source_key'], + 'Bucket' => $this->source['source_bucket'] + ]; + if (isset($this->source['source_version_id'])) { + $this->sourceVersionId = $this->source['source_version_id']; + $headParams['VersionId'] = $this->sourceVersionId; + } + //otherwise, use the default source parsing behavior + } else { + list($bucket, $key) = explode('/', ltrim($this->source, '/'), 2); + $headParams = [ + 'Bucket' => $bucket, + 'Key' => $key, + ]; + if (strpos($key, '?')) { + list($key, $query) = explode('?', $key, 2); + $headParams['Key'] = $key; + $query = Psr7\Query::parse($query, false); + if (isset($query['versionId'])) { + $this->sourceVersionId = $query['versionId']; + $headParams['VersionId'] = $this->sourceVersionId; + } + } + } + return $this->client->headObject($headParams); + } + + /** + * Get the url decoded input source, starting with a slash if it is not an + * ARN to standardize the source location syntax. + * + * @param string $inputSource The source that was passed to the constructor + * @return string The source, starting with a slash if it's not an arn + */ + private function getInputSource($inputSource) + { + $sourceBuilder = ArnParser::isArn($inputSource) ? '' : '/'; + $sourceBuilder .= ltrim(rawurldecode($inputSource), '/'); + return $sourceBuilder; + } +} diff --git a/3rdparty/aws/aws-sdk-php/src/S3/MultipartUploader.php b/3rdparty/aws/aws-sdk-php/src/S3/MultipartUploader.php new file mode 100644 index 00000000..b2bed2b6 --- /dev/null +++ b/3rdparty/aws/aws-sdk-php/src/S3/MultipartUploader.php @@ -0,0 +1,181 @@ + null, + 'key' => null, + 'exception_class' => S3MultipartUploadException::class, + ]); + + if ($this->displayProgress) { + $this->getState()->setProgressThresholds($this->source->getSize()); + } + } + + protected function loadUploadWorkflowInfo() + { + return [ + 'command' => [ + 'initiate' => 'CreateMultipartUpload', + 'upload' => 'UploadPart', + 'complete' => 'CompleteMultipartUpload', + ], + 'id' => [ + 'bucket' => 'Bucket', + 'key' => 'Key', + 'upload_id' => 'UploadId', + ], + 'part_num' => 'PartNumber', + ]; + } + + protected function createPart($seekable, $number) + { + // Initialize the array of part data that will be returned. + $data = []; + + // Apply custom params to UploadPart data + $config = $this->getConfig(); + $params = isset($config['params']) ? $config['params'] : []; + foreach ($params as $k => $v) { + $data[$k] = $v; + } + + $data['PartNumber'] = $number; + + // Read from the source to create the body stream. + if ($seekable) { + // Case 1: Source is seekable, use lazy stream to defer work. + $body = $this->limitPartStream( + new Psr7\LazyOpenStream($this->source->getMetadata('uri'), 'r') + ); + } else { + // Case 2: Stream is not seekable; must store in temp stream. + $source = $this->limitPartStream($this->source); + $source = $this->decorateWithHashes($source, $data); + $body = Psr7\Utils::streamFor(); + Psr7\Utils::copyToStream($source, $body); + } + + $contentLength = $body->getSize(); + + // Do not create a part if the body size is zero. + if ($contentLength === 0) { + return false; + } + + $body->seek(0); + $data['Body'] = $body; + + if (isset($config['add_content_md5']) + && $config['add_content_md5'] === true + ) { + $data['AddContentMD5'] = true; + } + + $data['ContentLength'] = $contentLength; + + return $data; + } + + protected function extractETag(ResultInterface $result) + { + return $result['ETag']; + } + + protected function getSourceMimeType() + { + if ($uri = $this->source->getMetadata('uri')) { + return Psr7\MimeType::fromFilename($uri) + ?: 'application/octet-stream'; + } + } + + protected function getSourceSize() + { + return $this->source->getSize(); + } + + /** + * Decorates a stream with a sha256 linear hashing stream. + * + * @param Stream $stream Stream to decorate. + * @param array $data Part data to augment with the hash result. + * + * @return Stream + */ + private function decorateWithHashes(Stream $stream, array &$data) + { + // Decorate source with a hashing stream + $hash = new PhpHash('sha256'); + return new HashingStream($stream, $hash, function ($result) use (&$data) { + $data['ContentSHA256'] = bin2hex($result); + }); + } +} diff --git a/3rdparty/aws/aws-sdk-php/src/S3/MultipartUploadingTrait.php b/3rdparty/aws/aws-sdk-php/src/S3/MultipartUploadingTrait.php new file mode 100644 index 00000000..b98a2d79 --- /dev/null +++ b/3rdparty/aws/aws-sdk-php/src/S3/MultipartUploadingTrait.php @@ -0,0 +1,151 @@ + $bucket, + 'Key' => $key, + 'UploadId' => $uploadId, + ]); + + foreach ($client->getPaginator('ListParts', $state->getId()) as $result) { + // Get the part size from the first part in the first result. + if (!$state->getPartSize()) { + $state->setPartSize($result->search('Parts[0].Size')); + } + // Mark all the parts returned by ListParts as uploaded. + foreach ($result['Parts'] as $part) { + $state->markPartAsUploaded($part['PartNumber'], [ + 'PartNumber' => $part['PartNumber'], + 'ETag' => $part['ETag'] + ]); + } + } + + $state->setStatus(UploadState::INITIATED); + + return $state; + } + + protected function handleResult(CommandInterface $command, ResultInterface $result) + { + $partData = []; + $partData['PartNumber'] = $command['PartNumber']; + $partData['ETag'] = $this->extractETag($result); + $commandName = $command->getName(); + $checksumResult = $commandName === 'UploadPart' + ? $result + : $result[$commandName . 'Result']; + + if (isset($command['ChecksumAlgorithm'])) { + $checksumMemberName = 'Checksum' . strtoupper($command['ChecksumAlgorithm']); + $partData[$checksumMemberName] = $checksumResult[$checksumMemberName] ?? null; + } + + $this->getState()->markPartAsUploaded($command['PartNumber'], $partData); + + // Updates counter for uploaded bytes. + $this->uploadedBytes += $command["ContentLength"]; + // Sends uploaded bytes to progress tracker if getDisplayProgress set + if ($this->displayProgress) { + $this->getState()->getDisplayProgress($this->uploadedBytes); + } + } + + abstract protected function extractETag(ResultInterface $result); + + protected function getCompleteParams() + { + $config = $this->getConfig(); + $params = isset($config['params']) ? $config['params'] : []; + + $params['MultipartUpload'] = [ + 'Parts' => $this->getState()->getUploadedParts() + ]; + + return $params; + } + + protected function determinePartSize() + { + // Make sure the part size is set. + $partSize = $this->getConfig()['part_size'] ?: MultipartUploader::PART_MIN_SIZE; + + // Adjust the part size to be larger for known, x-large uploads. + if ($sourceSize = $this->getSourceSize()) { + $partSize = (int) max( + $partSize, + ceil($sourceSize / MultipartUploader::PART_MAX_NUM) + ); + } + + // Ensure that the part size follows the rules: 5 MB <= size <= 5 GB. + if ($partSize < MultipartUploader::PART_MIN_SIZE || $partSize > MultipartUploader::PART_MAX_SIZE) { + throw new \InvalidArgumentException('The part size must be no less ' + . 'than 5 MB and no greater than 5 GB.'); + } + + return $partSize; + } + + protected function getInitiateParams() + { + $config = $this->getConfig(); + $params = isset($config['params']) ? $config['params'] : []; + + if (isset($config['acl'])) { + $params['ACL'] = $config['acl']; + } + + // Set the ContentType if not already present + if (empty($params['ContentType']) && $type = $this->getSourceMimeType()) { + $params['ContentType'] = $type; + } + + return $params; + } + + /** + * @return UploadState + */ + abstract protected function getState(); + + /** + * @return array + */ + abstract protected function getConfig(); + + /** + * @return int + */ + abstract protected function getSourceSize(); + + /** + * @return string|null + */ + abstract protected function getSourceMimeType(); +} diff --git a/3rdparty/aws/aws-sdk-php/src/S3/ObjectCopier.php b/3rdparty/aws/aws-sdk-php/src/S3/ObjectCopier.php new file mode 100644 index 00000000..66e4446d --- /dev/null +++ b/3rdparty/aws/aws-sdk-php/src/S3/ObjectCopier.php @@ -0,0 +1,170 @@ + null, + 'before_upload' => null, + 'concurrency' => 5, + 'mup_threshold' => self::DEFAULT_MULTIPART_THRESHOLD, + 'params' => [], + 'part_size' => null, + 'version_id' => null, + ]; + + /** + * @param S3ClientInterface $client The S3 Client used to execute + * the copy command(s). + * @param array $source The object to copy, specified as + * an array with a 'Bucket' and + * 'Key' keys. Provide a + * 'VersionID' key to copy a + * specified version of an object. + * @param array $destination The bucket and key to which to + * copy the $source, specified as + * an array with a 'Bucket' and + * 'Key' keys. + * @param string $acl ACL to apply to the copy + * (default: private). + * @param array $options Options used to configure the + * copy process. Options passed in + * through 'params' are added to + * the sub commands. + * + * @throws InvalidArgumentException + */ + public function __construct( + S3ClientInterface $client, + array $source, + array $destination, + $acl = 'private', + array $options = [] + ) { + $this->validateLocation($source); + $this->validateLocation($destination); + + $this->client = $client; + $this->source = $source; + $this->destination = $destination; + $this->acl = $acl; + $this->options = $options + self::$defaults; + } + + /** + * Perform the configured copy asynchronously. Returns a promise that is + * fulfilled with the result of the CompleteMultipartUpload or CopyObject + * operation or rejected with an exception. + * + * @return Coroutine + */ + public function promise(): PromiseInterface + { + return Coroutine::of(function () { + $headObjectCommand = $this->client->getCommand( + 'HeadObject', + $this->options['params'] + $this->source + ); + if (is_callable($this->options['before_lookup'])) { + $this->options['before_lookup']($headObjectCommand); + } + $objectStats = (yield $this->client->executeAsync( + $headObjectCommand + )); + + if ($objectStats['ContentLength'] > $this->options['mup_threshold']) { + $mup = new MultipartCopy( + $this->client, + $this->getSourcePath(), + ['source_metadata' => $objectStats, 'acl' => $this->acl] + + $this->destination + + $this->options + ); + + yield $mup->promise(); + } else { + $defaults = [ + 'ACL' => $this->acl, + 'MetadataDirective' => 'COPY', + 'CopySource' => $this->getSourcePath(), + ]; + + $params = array_diff_key($this->options, self::$defaults) + + $this->destination + $defaults + $this->options['params']; + + yield $this->client->executeAsync( + $this->client->getCommand('CopyObject', $params) + ); + } + }); + } + + /** + * Perform the configured copy synchronously. Returns the result of the + * CompleteMultipartUpload or CopyObject operation. + * + * @return Result + * + * @throws S3Exception + * @throws MultipartUploadException + */ + public function copy() + { + return $this->promise()->wait(); + } + + private function validateLocation(array $location) + { + if (empty($location['Bucket']) || empty($location['Key'])) { + throw new \InvalidArgumentException('Locations provided to an' + . ' Aws\S3\ObjectCopier must have a non-empty Bucket and Key'); + } + } + + private function getSourcePath() + { + $path = "/{$this->source['Bucket']}/"; + if (ArnParser::isArn($this->source['Bucket'])) { + try { + new AccessPointArn($this->source['Bucket']); + $path = "{$this->source['Bucket']}/object/"; + } catch (\Exception $e) { + throw new \InvalidArgumentException( + 'Provided ARN was a not a valid S3 access point ARN (' + . $e->getMessage() . ')', + 0, + $e + ); + } + } + + $sourcePath = $path . rawurlencode($this->source['Key']); + if (isset($this->source['VersionId'])) { + $sourcePath .= "?versionId={$this->source['VersionId']}"; + } + + return $sourcePath; + } +} diff --git a/3rdparty/aws/aws-sdk-php/src/S3/ObjectUploader.php b/3rdparty/aws/aws-sdk-php/src/S3/ObjectUploader.php new file mode 100644 index 00000000..b73b7b12 --- /dev/null +++ b/3rdparty/aws/aws-sdk-php/src/S3/ObjectUploader.php @@ -0,0 +1,150 @@ + null, + 'concurrency' => 3, + 'mup_threshold' => self::DEFAULT_MULTIPART_THRESHOLD, + 'params' => [], + 'part_size' => null, + ]; + private $addContentMD5; + + /** + * @param S3ClientInterface $client The S3 Client used to execute + * the upload command(s). + * @param string $bucket Bucket to upload the object, or + * an S3 access point ARN. + * @param string $key Key of the object. + * @param mixed $body Object data to upload. Can be a + * StreamInterface, PHP stream + * resource, or a string of data to + * upload. + * @param string $acl ACL to apply to the copy + * (default: private). + * @param array $options Options used to configure the + * copy process. Options passed in + * through 'params' are added to + * the sub command(s). + */ + public function __construct( + S3ClientInterface $client, + $bucket, + $key, + $body, + $acl = 'private', + array $options = [] + ) { + $this->client = $client; + $this->bucket = $bucket; + $this->key = $key; + $this->body = Psr7\Utils::streamFor($body); + $this->acl = $acl; + $this->options = $options + self::$defaults; + // Handle "add_content_md5" option. + $this->addContentMD5 = isset($options['add_content_md5']) + && $options['add_content_md5'] === true; + } + + /** + * @return PromiseInterface + */ + public function promise(): PromiseInterface + { + /** @var int $mup_threshold */ + $mup_threshold = $this->options['mup_threshold']; + if ($this->requiresMultipart($this->body, $mup_threshold)) { + // Perform a multipart upload. + return (new MultipartUploader($this->client, $this->body, [ + 'bucket' => $this->bucket, + 'key' => $this->key, + 'acl' => $this->acl + ] + $this->options))->promise(); + } + + // Perform a regular PutObject operation. + $command = $this->client->getCommand('PutObject', [ + 'Bucket' => $this->bucket, + 'Key' => $this->key, + 'Body' => $this->body, + 'ACL' => $this->acl, + 'AddContentMD5' => $this->addContentMD5 + ] + $this->options['params']); + if (is_callable($this->options['before_upload'])) { + $this->options['before_upload']($command); + } + return $this->client->executeAsync($command); + } + + public function upload() + { + return $this->promise()->wait(); + } + + /** + * Determines if the body should be uploaded using PutObject or the + * Multipart Upload System. It also modifies the passed-in $body as needed + * to support the upload. + * + * @param StreamInterface $body Stream representing the body. + * @param integer $threshold Minimum bytes before using Multipart. + * + * @return bool + */ + private function requiresMultipart(StreamInterface &$body, $threshold) + { + // If body size known, compare to threshold to determine if Multipart. + if ($body->getSize() !== null) { + return $body->getSize() >= $threshold; + } + + /** + * Handle the situation where the body size is unknown. + * Read up to 5MB into a buffer to determine how to upload the body. + * @var StreamInterface $buffer + */ + $buffer = Psr7\Utils::streamFor(); + Psr7\Utils::copyToStream($body, $buffer, MultipartUploader::PART_MIN_SIZE); + + // If body < 5MB, use PutObject with the buffer. + if ($buffer->getSize() < MultipartUploader::PART_MIN_SIZE) { + $buffer->seek(0); + $body = $buffer; + return false; + } + + // If body >= 5 MB, then use multipart. [YES] + if ($body->isSeekable() && $body->getMetadata('uri') !== 'php://input') { + // If the body is seekable, just rewind the body. + $body->seek(0); + } else { + // If the body is non-seekable, stitch the rewind the buffer and + // the partially read body together into one stream. This avoids + // unnecessary disc usage and does not require seeking on the + // original stream. + $buffer->seek(0); + $body = new Psr7\AppendStream([$buffer, $body]); + } + + return true; + } +} diff --git a/3rdparty/aws/aws-sdk-php/src/S3/Parser/GetBucketLocationResultMutator.php b/3rdparty/aws/aws-sdk-php/src/S3/Parser/GetBucketLocationResultMutator.php new file mode 100644 index 00000000..2e8d1a1a --- /dev/null +++ b/3rdparty/aws/aws-sdk-php/src/S3/Parser/GetBucketLocationResultMutator.php @@ -0,0 +1,42 @@ +getName() !== 'GetBucketLocation') { + return $result; + } + + $location = 'us-east-1'; + static $pattern = '/>(.+?)<\/LocationConstraint>/'; + if (preg_match($pattern, $response->getBody(), $matches)) { + $location = $matches[1] === 'EU' ? 'eu-west-1' : $matches[1]; + } + + $result['LocationConstraint'] = $location; + $response->getBody()->rewind(); + + return $result; + } +} diff --git a/3rdparty/aws/aws-sdk-php/src/S3/Parser/S3Parser.php b/3rdparty/aws/aws-sdk-php/src/S3/Parser/S3Parser.php new file mode 100644 index 00000000..3f6f3541 --- /dev/null +++ b/3rdparty/aws/aws-sdk-php/src/S3/Parser/S3Parser.php @@ -0,0 +1,275 @@ +protocolParser = $protocolParser; + $this->errorParser = $errorParser; + $this->exceptionClass = $exceptionClass; + $this->s3ResultMutators = []; + } + + /** + * Parses a S3 response. + * + * @param CommandInterface $command The command that originated the request. + * @param ResponseInterface $response The response received from the service. + * + * @return ResultInterface|null + */ + public function __invoke( + CommandInterface $command, + ResponseInterface $response + ):? ResultInterface + { + // Check first if the response is an error + $this->parse200Error($command, $response); + + try { + $parseFn = $this->protocolParser; + $result = $parseFn($command, $response); + } catch (ParserException $e) { + // Parsing errors will be considered retryable. + throw new $this->exceptionClass( + "Error parsing response for {$command->getName()}:" + . " AWS parsing error: {$e->getMessage()}", + $command, + ['connection_error' => true, 'exception' => $e], + $e + ); + } + + return $this->executeS3ResultMutators($result, $command, $response); + } + + /** + * Tries to parse a 200 response as an error from S3. + * If the parsed result contains a code and message then that means an error + * was found, and hence an exception is thrown with that error. + * + * @param CommandInterface $command + * @param ResponseInterface $response + * + * @return void + */ + private function parse200Error( + CommandInterface $command, + ResponseInterface $response + ): void + { + // This error parsing should be just for 200 error responses + // and operations where its output shape does not have a streaming + // member and the body of the response is seekable. + if (200 !== $response->getStatusCode() + || !$this->shouldBeConsidered200Error($command->getName()) + || !$response->getBody()->isSeekable()) { + return; + } + + // To guarantee we try the error parsing just for an Error xml response. + if (!$this->isFirstRootElementError($response->getBody())) { + return; + } + + try { + $errorParserFn = $this->errorParser; + $parsedError = $errorParserFn($response, $command); + } catch (ParserException $e) { + // Parsing errors will be considered retryable. + $parsedError = [ + 'code' => 'ConnectionError', + 'message' => "An error connecting to the service occurred" + . " while performing the " . $command->getName() + . " operation." + ]; + } + + if (isset($parsedError['code']) && isset($parsedError['message'])) { + throw new $this->exceptionClass( + $parsedError['message'], + $command, + [ + 'connection_error' => true, + 'code' => $parsedError['code'], + 'message' => $parsedError['message'] + ] + ); + } + } + + /** + * Checks if a specific operation should be considered + * a s3 200 error. Operations where any of its output members + * has a streaming or httpPayload trait should be not considered. + * + * @param $commandName + * + * @return bool + */ + private function shouldBeConsidered200Error($commandName): bool + { + $operation = $this->api->getOperation($commandName); + $output = $operation->getOutput(); + foreach ($output->getMembers() as $_ => $memberProps) { + if (!empty($memberProps['eventstream']) || !empty($memberProps['streaming'])) { + return false; + } + } + + return true; + } + + /** + * Checks if the root element of the response body is "Error", which is + * when we should try to parse an error from a 200 response from s3. + * It is recommended to make sure the stream given is seekable, otherwise + * the rewind call will cause a user warning. + * + * @param StreamInterface $responseBody + * + * @return bool + */ + private function isFirstRootElementError(StreamInterface $responseBody): bool + { + static $pattern = '/<\?xml version="1\.0" encoding="UTF-8"\?>\s*/'; + // To avoid performance overhead in large streams + $reducedBodyContent = $responseBody->read(64); + $foundErrorElement = preg_match($pattern, $reducedBodyContent); + // A rewind is needed because the stream is partially or entirely consumed + // in the previous read operation. + $responseBody->rewind(); + + return $foundErrorElement; + } + + /** + * Execute mutator implementations over a result. + * Mutators are logics that modifies a result. + * + * @param ResultInterface $result + * @param CommandInterface $command + * @param ResponseInterface $response + * + * @return ResultInterface + */ + private function executeS3ResultMutators( + ResultInterface $result, + CommandInterface $command, + ResponseInterface $response + ): ResultInterface + { + foreach ($this->s3ResultMutators as $mutator) { + $result = $mutator($result, $command, $response); + } + + return $result; + } + + /** + * Adds a mutator into the list of mutators. + * + * @param string $mutatorName + * @param S3ResultMutator $s3ResultMutator + * @return void + */ + public function addS3ResultMutator( + string $mutatorName, + S3ResultMutator $s3ResultMutator + ): void + { + if (isset($this->s3ResultMutators[$mutatorName])) { + trigger_error( + "The S3 Result Mutator {$mutatorName} already exists!", + E_USER_WARNING + ); + + return; + } + + $this->s3ResultMutators[$mutatorName] = $s3ResultMutator; + } + + /** + * Removes a mutator from the mutator list. + * + * @param string $mutatorName + * @return void + */ + public function removeS3ResultMutator(string $mutatorName): void + { + if (!isset($this->s3ResultMutators[$mutatorName])) { + trigger_error( + "The S3 Result Mutator {$mutatorName} does not exist!", + E_USER_WARNING + ); + + return; + } + + unset($this->s3ResultMutators[$mutatorName]); + } + + /** + * Returns the list of result mutators available. + * + * @return array + */ + public function getS3ResultMutators(): array + { + return $this->s3ResultMutators; + } + + public function parseMemberFromStream( + StreamInterface $stream, + StructureShape $member, + $response + ) + { + return $this->protocolParser->parseMemberFromStream( + $stream, + $member, + $response + ); + } +} diff --git a/3rdparty/aws/aws-sdk-php/src/S3/Parser/S3ResultMutator.php b/3rdparty/aws/aws-sdk-php/src/S3/Parser/S3ResultMutator.php new file mode 100644 index 00000000..5119501f --- /dev/null +++ b/3rdparty/aws/aws-sdk-php/src/S3/Parser/S3ResultMutator.php @@ -0,0 +1,36 @@ +api = $api; + $this->config = $config; + } + + /** + * @param ResultInterface $result + * @param CommandInterface|null $command + * @param ResponseInterface|null $response + * + * @return ResultInterface + */ + public function __invoke( + ResultInterface $result, + ?CommandInterface $command = null, + ?ResponseInterface $response = null + ): ResultInterface + { + $operation = $this->api->getOperation($command->getName()); + + // Skip this middleware if the operation doesn't have an httpChecksum + $checksumInfo = $operation['httpChecksum'] ?? null; + if (is_null($checksumInfo)) { + return $result; + } + + $mode = $this->config['response_checksum_validation'] ?? self::DEFAULT_VALIDATION_MODE; + $checksumModeEnabledMember = $checksumInfo['requestValidationModeMember'] ?? ""; + $checksumModeEnabled = strtolower($command[$checksumModeEnabledMember] ?? ""); + $responseAlgorithms = $checksumInfo['responseAlgorithms'] ?? []; + $shouldSkipValidation = $this->shouldSkipValidation( + $mode, + $checksumModeEnabled, + $responseAlgorithms + ); + + if ($shouldSkipValidation) { + return $result; + } + + $checksumPriority = $this->getChecksumPriority(); + $checksumsToCheck = array_intersect($responseAlgorithms, array_map( + 'strtoupper', + array_keys($checksumPriority)) + ); + $checksumValidationInfo = $this->validateChecksum($checksumsToCheck, $response); + + if ($checksumValidationInfo['status'] === "SUCCEEDED") { + $result['ChecksumValidated'] = $checksumValidationInfo['checksum']; + } elseif ($checksumValidationInfo['status'] === "FAILED") { + if ($this->isMultipartGetObject($command, $checksumValidationInfo)) { + return $result; + } + throw new S3Exception( + "Calculated response checksum did not match the expected value", + $command + ); + } + + return $result; + } + + /** + * @param $checksumPriority + * @param ResponseInterface $response + * + * @return array + */ + private function validateChecksum( + $checksumPriority, + ResponseInterface $response + ): array + { + $checksumToValidate = $this->chooseChecksumHeaderToValidate( + $checksumPriority, + $response + ); + $validationStatus = "SKIPPED"; + $checksumHeaderValue = null; + if (!empty($checksumToValidate)) { + $checksumHeaderValue = $response->getHeaderLine( + 'x-amz-checksum-' . $checksumToValidate + ); + if (!empty($checksumHeaderValue)) { + $calculatedChecksumValue = $this->getEncodedValue( + $checksumToValidate, + $response->getBody() + ); + $validationStatus = $checksumHeaderValue == $calculatedChecksumValue + ? "SUCCEEDED" + : "FAILED"; + } + } + return [ + "status" => $validationStatus, + "checksum" => $checksumToValidate, + "checksumHeaderValue" => $checksumHeaderValue, + ]; + } + + /** + * @param $checksumPriority + * @param ResponseInterface $response + * + * @return string + */ + private function chooseChecksumHeaderToValidate( + $checksumPriority, + ResponseInterface $response + ):? string + { + foreach ($checksumPriority as $checksum) { + $checksumHeader = 'x-amz-checksum-' . $checksum; + if ($response->hasHeader($checksumHeader)) { + return $checksum; + } + } + + return null; + } + + /** + * @param string $mode + * @param string $checksumModeEnabled + * @param array $responseAlgorithms + * + * @return bool + */ + private function shouldSkipValidation( + string $mode, + string $checksumModeEnabled, + array $responseAlgorithms + ): bool + { + return empty($responseAlgorithms) + || ($mode === 'when_required' && $checksumModeEnabled !== 'enabled'); + } + + /** + * @return string[] + */ + private function getChecksumPriority(): array + { + return extension_loaded('awscrt') + ? self::$supportedAlgorithms + : array_slice(self::$supportedAlgorithms, 1); + } + + /** + * @param CommandInterface $command + * @param array $checksumValidationInfo + * + * @return bool + */ + private function isMultipartGetObject( + CommandInterface $command, + array $checksumValidationInfo + ): bool + { + if ($command->getName() !== "GetObject" + || empty($checksumValidationInfo['checksumHeaderValue']) + ) { + return false; + } + + $headerValue = $checksumValidationInfo['checksumHeaderValue']; + $lastDashPos = strrpos($headerValue, '-'); + $endOfChecksum = substr($headerValue, $lastDashPos + 1); + + return is_numeric($endOfChecksum) + && (int) $endOfChecksum > 1 + && (int) $endOfChecksum < 10000; + } +} diff --git a/3rdparty/aws/aws-sdk-php/src/S3/PermanentRedirectMiddleware.php b/3rdparty/aws/aws-sdk-php/src/S3/PermanentRedirectMiddleware.php new file mode 100644 index 00000000..36a0e683 --- /dev/null +++ b/3rdparty/aws/aws-sdk-php/src/S3/PermanentRedirectMiddleware.php @@ -0,0 +1,62 @@ +nextHandler = $nextHandler; + } + + public function __invoke(CommandInterface $command, ?RequestInterface $request = null) + { + $next = $this->nextHandler; + return $next($command, $request)->then( + function (ResultInterface $result) use ($command) { + $status = isset($result['@metadata']['statusCode']) + ? $result['@metadata']['statusCode'] + : null; + if ($status == 301) { + throw new PermanentRedirectException( + 'Encountered a permanent redirect while requesting ' + . $result->search('"@metadata".effectiveUri') . '. ' + . 'Are you sure you are using the correct region for ' + . 'this bucket?', + $command, + ['result' => $result] + ); + } + return $result; + } + ); + } +} diff --git a/3rdparty/aws/aws-sdk-php/src/S3/PostObject.php b/3rdparty/aws/aws-sdk-php/src/S3/PostObject.php new file mode 100644 index 00000000..48913ea1 --- /dev/null +++ b/3rdparty/aws/aws-sdk-php/src/S3/PostObject.php @@ -0,0 +1,160 @@ +client = $client; + $this->bucket = $bucket; + + if (is_array($jsonPolicy)) { + $jsonPolicy = json_encode($jsonPolicy); + } + + $this->jsonPolicy = $jsonPolicy; + $this->formAttributes = [ + 'action' => $this->generateUri(), + 'method' => 'POST', + 'enctype' => 'multipart/form-data' + ]; + + $this->formInputs = $formInputs + ['key' => '${filename}']; + $credentials = $client->getCredentials()->wait(); + $this->formInputs += $this->getPolicyAndSignature($credentials); + } + + /** + * Gets the S3 client. + * + * @return S3ClientInterface + */ + public function getClient() + { + return $this->client; + } + + /** + * Gets the bucket name. + * + * @return string + */ + public function getBucket() + { + return $this->bucket; + } + + /** + * Gets the form attributes as an array. + * + * @return array + */ + public function getFormAttributes() + { + return $this->formAttributes; + } + + /** + * Set a form attribute. + * + * @param string $attribute Form attribute to set. + * @param string $value Value to set. + */ + public function setFormAttribute($attribute, $value) + { + $this->formAttributes[$attribute] = $value; + } + + /** + * Gets the form inputs as an array. + * + * @return array + */ + public function getFormInputs() + { + return $this->formInputs; + } + + /** + * Set a form input. + * + * @param string $field Field name to set + * @param string $value Value to set. + */ + public function setFormInput($field, $value) + { + $this->formInputs[$field] = $value; + } + + /** + * Gets the raw JSON policy. + * + * @return string + */ + public function getJsonPolicy() + { + return $this->jsonPolicy; + } + + private function generateUri() + { + $uri = new Uri($this->client->getEndpoint()); + + if ($this->client->getConfig('use_path_style_endpoint') === true + || ($uri->getScheme() === 'https' + && strpos($this->bucket, '.') !== false) + ) { + // Use path-style URLs + $uri = $uri->withPath("/{$this->bucket}"); + } else { + // Use virtual-style URLs + $uri = $uri->withHost($this->bucket . '.' . $uri->getHost()); + } + + return (string) $uri; + } + + protected function getPolicyAndSignature(CredentialsInterface $creds) + { + $jsonPolicy64 = base64_encode($this->jsonPolicy); + + return [ + 'AWSAccessKeyId' => $creds->getAccessKeyId(), + 'policy' => $jsonPolicy64, + 'signature' => base64_encode(hash_hmac( + 'sha1', + $jsonPolicy64, + $creds->getSecretKey(), + true + )) + ]; + } +} diff --git a/3rdparty/aws/aws-sdk-php/src/S3/PostObjectV4.php b/3rdparty/aws/aws-sdk-php/src/S3/PostObjectV4.php new file mode 100644 index 00000000..19763722 --- /dev/null +++ b/3rdparty/aws/aws-sdk-php/src/S3/PostObjectV4.php @@ -0,0 +1,195 @@ +client = $client; + $this->bucket = $bucket; + + // setup form attributes + $this->formAttributes = [ + 'action' => $this->generateUri(), + 'method' => 'POST', + 'enctype' => 'multipart/form-data' + ]; + + $credentials = $this->client->getCredentials()->wait(); + + if ($securityToken = $credentials->getSecurityToken()) { + $options [] = ['x-amz-security-token' => $securityToken]; + $formInputs['X-Amz-Security-Token'] = $securityToken; + } + + // setup basic policy + $policy = [ + 'expiration' => TimestampShape::format($expiration, 'iso8601'), + 'conditions' => $options, + ]; + + // setup basic formInputs + $this->formInputs = $formInputs + ['key' => '${filename}']; + + // finalize policy and signature + + $this->formInputs += $this->getPolicyAndSignature( + $credentials, + $policy + ); + } + + /** + * Gets the S3 client. + * + * @return S3ClientInterface + */ + public function getClient() + { + return $this->client; + } + + /** + * Gets the bucket name. + * + * @return string + */ + public function getBucket() + { + return $this->bucket; + } + + /** + * Gets the form attributes as an array. + * + * @return array + */ + public function getFormAttributes() + { + return $this->formAttributes; + } + + /** + * Set a form attribute. + * + * @param string $attribute Form attribute to set. + * @param string $value Value to set. + */ + public function setFormAttribute($attribute, $value) + { + $this->formAttributes[$attribute] = $value; + } + + /** + * Gets the form inputs as an array. + * + * @return array + */ + public function getFormInputs() + { + return $this->formInputs; + } + + /** + * Set a form input. + * + * @param string $field Field name to set + * @param string $value Value to set. + */ + public function setFormInput($field, $value) + { + $this->formInputs[$field] = $value; + } + + private function generateUri() + { + $uri = new Uri($this->client->getEndpoint()); + + if ($this->client->getConfig('use_path_style_endpoint') === true + || ($uri->getScheme() === 'https' + && strpos($this->bucket, '.') !== false) + ) { + // Use path-style URLs + $uri = $uri->withPath("/{$this->bucket}"); + } else { + // Use virtual-style URLs if haven't been set up already + if (strpos($uri->getHost(), $this->bucket . '.') !== 0) { + $uri = $uri->withHost($this->bucket . '.' . $uri->getHost()); + } + } + + return (string) $uri; + } + + protected function getPolicyAndSignature( + CredentialsInterface $credentials, + array $policy + ){ + $ldt = gmdate(SignatureV4::ISO8601_BASIC); + $sdt = substr($ldt, 0, 8); + $policy['conditions'][] = ['X-Amz-Date' => $ldt]; + + $region = $this->client->getRegion(); + $scope = $this->createScope($sdt, $region, 's3'); + $creds = "{$credentials->getAccessKeyId()}/$scope"; + $policy['conditions'][] = ['X-Amz-Credential' => $creds]; + + $policy['conditions'][] = ['X-Amz-Algorithm' => "AWS4-HMAC-SHA256"]; + + $jsonPolicy64 = base64_encode(json_encode($policy)); + $key = $this->getSigningKey( + $sdt, + $region, + 's3', + $credentials->getSecretKey() + ); + + return [ + 'X-Amz-Credential' => $creds, + 'X-Amz-Algorithm' => "AWS4-HMAC-SHA256", + 'X-Amz-Date' => $ldt, + 'Policy' => $jsonPolicy64, + 'X-Amz-Signature' => bin2hex( + hash_hmac('sha256', $jsonPolicy64, $key, true) + ), + ]; + } +} diff --git a/3rdparty/aws/aws-sdk-php/src/S3/PutObjectUrlMiddleware.php b/3rdparty/aws/aws-sdk-php/src/S3/PutObjectUrlMiddleware.php new file mode 100644 index 00000000..9b80406e --- /dev/null +++ b/3rdparty/aws/aws-sdk-php/src/S3/PutObjectUrlMiddleware.php @@ -0,0 +1,59 @@ +nextHandler = $nextHandler; + } + + public function __invoke(CommandInterface $command, ?RequestInterface $request = null) + { + $next = $this->nextHandler; + return $next($command, $request)->then( + function (ResultInterface $result) use ($command) { + $name = $command->getName(); + switch ($name) { + case 'PutObject': + case 'CopyObject': + $result['ObjectURL'] = isset($result['@metadata']['effectiveUri']) + ? $result['@metadata']['effectiveUri'] + : null; + break; + case 'CompleteMultipartUpload': + $result['ObjectURL'] = urldecode($result['Location'] ?? ''); + break; + } + return $result; + } + ); + } +} diff --git a/3rdparty/aws/aws-sdk-php/src/S3/RegionalEndpoint/Configuration.php b/3rdparty/aws/aws-sdk-php/src/S3/RegionalEndpoint/Configuration.php new file mode 100644 index 00000000..48dc63d7 --- /dev/null +++ b/3rdparty/aws/aws-sdk-php/src/S3/RegionalEndpoint/Configuration.php @@ -0,0 +1,42 @@ +endpointsType = strtolower($endpointsType); + $this->isFallback = $isFallback; + if (!in_array($this->endpointsType, ['legacy', 'regional'])) { + throw new \InvalidArgumentException( + "Configuration parameter must either be 'legacy' or 'regional'." + ); + } + } + + /** + * {@inheritdoc} + */ + public function getEndpointsType() + { + return $this->endpointsType; + } + + /** + * {@inheritdoc} + */ + public function toArray() + { + return [ + 'endpoints_type' => $this->getEndpointsType() + ]; + } + + public function isFallback() + { + return $this->isFallback; + } +} diff --git a/3rdparty/aws/aws-sdk-php/src/S3/RegionalEndpoint/ConfigurationInterface.php b/3rdparty/aws/aws-sdk-php/src/S3/RegionalEndpoint/ConfigurationInterface.php new file mode 100644 index 00000000..10fbf941 --- /dev/null +++ b/3rdparty/aws/aws-sdk-php/src/S3/RegionalEndpoint/ConfigurationInterface.php @@ -0,0 +1,22 @@ + + * use Aws\S3\RegionalEndpoint\ConfigurationProvider; + * $provider = ConfigurationProvider::defaultProvider(); + * // Returns a ConfigurationInterface or throws. + * $config = $provider()->wait(); + * + * + * Configuration providers can be composed to create configuration using + * conditional logic that can create different configurations in different + * environments. You can compose multiple providers into a single provider using + * {@see \Aws\S3\RegionalEndpoint\ConfigurationProvider::chain}. This function + * accepts providers as variadic arguments and returns a new function that will + * invoke each provider until a successful configuration is returned. + * + * + * // First try an INI file at this location. + * $a = ConfigurationProvider::ini(null, '/path/to/file.ini'); + * // Then try an INI file at this location. + * $b = ConfigurationProvider::ini(null, '/path/to/other-file.ini'); + * // Then try loading from environment variables. + * $c = ConfigurationProvider::env(); + * // Combine the three providers together. + * $composed = ConfigurationProvider::chain($a, $b, $c); + * // Returns a promise that is fulfilled with a configuration or throws. + * $promise = $composed(); + * // Wait on the configuration to resolve. + * $config = $promise->wait(); + * + */ +class ConfigurationProvider extends AbstractConfigurationProvider + implements ConfigurationProviderInterface +{ + const ENV_ENDPOINTS_TYPE = 'AWS_S3_US_EAST_1_REGIONAL_ENDPOINT'; + const INI_ENDPOINTS_TYPE = 's3_us_east_1_regional_endpoint'; + const DEFAULT_ENDPOINTS_TYPE = 'legacy'; + + public static $cacheKey = 'aws_s3_us_east_1_regional_endpoint_config'; + + protected static $interfaceClass = ConfigurationInterface::class; + protected static $exceptionClass = ConfigurationException::class; + + /** + * Create a default config provider that first checks for environment + * variables, then checks for a specified profile in the environment-defined + * config file location (env variable is 'AWS_CONFIG_FILE', file location + * defaults to ~/.aws/config), then checks for the "default" profile in the + * environment-defined config file location, and failing those uses a default + * fallback set of configuration options. + * + * This provider is automatically wrapped in a memoize function that caches + * previously provided config options. + * + * @param array $config + * + * @return callable + */ + public static function defaultProvider(array $config = []) + { + $configProviders = [self::env()]; + if ( + !isset($config['use_aws_shared_config_files']) + || $config['use_aws_shared_config_files'] != false + ) { + $configProviders[] = self::ini(); + } + $configProviders[] = self::fallback(); + + $memo = self::memoize( + call_user_func_array([ConfigurationProvider::class, 'chain'], $configProviders) + ); + + if (isset($config['s3_us_east_1_regional_endpoint']) + && $config['s3_us_east_1_regional_endpoint'] instanceof CacheInterface + ) { + return self::cache($memo, $config['s3_us_east_1_regional_endpoint'], self::$cacheKey); + } + + return $memo; + } + + public static function env() + { + return function () { + // Use config from environment variables, if available + $endpointsType = getenv(self::ENV_ENDPOINTS_TYPE); + if (!empty($endpointsType)) { + return Promise\Create::promiseFor( + new Configuration($endpointsType) + ); + } + + return self::reject('Could not find environment variable config' + . ' in ' . self::ENV_ENDPOINTS_TYPE); + }; + } + + /** + * Config provider that creates config using a config file whose location + * is specified by an environment variable 'AWS_CONFIG_FILE', defaulting to + * ~/.aws/config if not specified + * + * @param string|null $profile Profile to use. If not specified will use + * the "default" profile. + * @param string|null $filename If provided, uses a custom filename rather + * than looking in the default directory. + * + * @return callable + */ + public static function ini( + $profile = null, + $filename = null + ) { + $filename = $filename ?: (self::getDefaultConfigFilename()); + $profile = $profile ?: (getenv(self::ENV_PROFILE) ?: 'default'); + + return function () use ($profile, $filename) { + if (!@is_readable($filename)) { + return self::reject("Cannot read configuration from $filename"); + } + $data = \Aws\parse_ini_file($filename, true); + if ($data === false) { + return self::reject("Invalid config file: $filename"); + } + if (!isset($data[$profile])) { + return self::reject("'$profile' not found in config file"); + } + if (!isset($data[$profile][self::INI_ENDPOINTS_TYPE])) { + return self::reject("Required S3 regional endpoint config values + not present in INI profile '{$profile}' ({$filename})"); + } + + return Promise\Create::promiseFor( + new Configuration($data[$profile][self::INI_ENDPOINTS_TYPE]) + ); + }; + } + + /** + * Fallback config options when other sources are not set. + * + * @return callable + */ + public static function fallback() + { + return function () { + return Promise\Create::promiseFor( + new Configuration(self::DEFAULT_ENDPOINTS_TYPE, true) + ); + }; + } + + /** + * Unwraps a configuration object in whatever valid form it is in, + * always returning a ConfigurationInterface object. + * + * @param mixed $config + * @return ConfigurationInterface + * @throws \InvalidArgumentException + */ + public static function unwrap($config) + { + if (is_callable($config)) { + $config = $config(); + } + if ($config instanceof Promise\PromiseInterface) { + $config = $config->wait(); + } + if ($config instanceof ConfigurationInterface) { + return $config; + } + if (is_string($config)) { + return new Configuration($config); + } + if (is_array($config) && isset($config['endpoints_type'])) { + return new Configuration($config['endpoints_type']); + } + + throw new \InvalidArgumentException('Not a valid S3 regional endpoint ' + . 'configuration argument.'); + } +} diff --git a/3rdparty/aws/aws-sdk-php/src/S3/RegionalEndpoint/Exception/ConfigurationException.php b/3rdparty/aws/aws-sdk-php/src/S3/RegionalEndpoint/Exception/ConfigurationException.php new file mode 100644 index 00000000..29e211f5 --- /dev/null +++ b/3rdparty/aws/aws-sdk-php/src/S3/RegionalEndpoint/Exception/ConfigurationException.php @@ -0,0 +1,14 @@ +parser = $parser; + $this->exceptionClass = $exceptionClass; + } + + public function __invoke( + CommandInterface $command, + ResponseInterface $response + ) { + $fn = $this->parser; + + try { + return $fn($command, $response); + } catch (ParserException $e) { + throw new $this->exceptionClass( + "Error parsing response for {$command->getName()}:" + . " AWS parsing error: {$e->getMessage()}", + $command, + ['connection_error' => true, 'exception' => $e], + $e + ); + } + } + + public function parseMemberFromStream( + StreamInterface $stream, + StructureShape $member, + $response + ) { + return $this->parser->parseMemberFromStream($stream, $member, $response); + } +} diff --git a/3rdparty/aws/aws-sdk-php/src/S3/S3Client.php b/3rdparty/aws/aws-sdk-php/src/S3/S3Client.php new file mode 100644 index 00000000..585de5bf --- /dev/null +++ b/3rdparty/aws/aws-sdk-php/src/S3/S3Client.php @@ -0,0 +1,1302 @@ + true, + 'when_required' => true + ]; + + public static function getArguments() + { + $args = parent::getArguments(); + $args['retries']['fn'] = [__CLASS__, '_applyRetryConfig']; + $args['api_provider']['fn'] = [__CLASS__, '_applyApiProvider']; + + return + [ + 'request_checksum_calculation' => [ + 'type' => 'config', + 'valid' => ['string'], + 'doc' => 'Valid values are `when_supported` and `when_required`. Default is `when_supported`.' + . ' `when_supported` results in checksum calculation when an operation has modeled checksum support.' + . ' `when_required` results in checksum calculation when an operation has modeled checksum support and' + . ' request checksums are modeled as required.', + 'fn' => [__CLASS__, '_apply_request_checksum_calculation'], + 'default' => [__CLASS__, '_default_request_checksum_calculation'], + ], + 'response_checksum_validation' => [ + 'type' => 'config', + 'valid' => ['string'], + 'doc' => 'Valid values are `when_supported` and `when_required`. Default is `when_supported`.' + . ' `when_supported` results in checksum validation when an operation has modeled checksum support.' + . ' `when_required` results in checksum validation when an operation has modeled checksum support and' + . ' `CheckSumMode` is set to `enabled`.', + 'fn' => [__CLASS__, '_apply_response_checksum_validation'], + 'default' => [__CLASS__, '_default_response_checksum_validation'], + ] + ] + + $args + [ + 'bucket_endpoint' => [ + 'type' => 'config', + 'valid' => ['bool'], + 'doc' => 'Set to true to send requests to a hardcoded ' + . 'bucket endpoint rather than create an endpoint as a ' + . 'result of injecting the bucket into the URL. This ' + . 'option is useful for interacting with CNAME endpoints.', + ], + 'use_arn_region' => [ + 'type' => 'config', + 'valid' => [ + 'bool', + Configuration::class, + CacheInterface::class, + 'callable' + ], + 'doc' => 'Set to true to allow passed in ARNs to override' + . ' client region. Accepts...', + 'fn' => [__CLASS__, '_apply_use_arn_region'], + 'default' => [UseArnRegionConfigurationProvider::class, 'defaultProvider'], + ], + 'use_accelerate_endpoint' => [ + 'type' => 'config', + 'valid' => ['bool'], + 'doc' => 'Set to true to send requests to an S3 Accelerate' + . ' endpoint by default. Can be enabled or disabled on' + . ' individual operations by setting' + . ' \'@use_accelerate_endpoint\' to true or false. Note:' + . ' you must enable S3 Accelerate on a bucket before it can' + . ' be accessed via an Accelerate endpoint.', + 'default' => false, + ], + 'use_path_style_endpoint' => [ + 'type' => 'config', + 'valid' => ['bool'], + 'doc' => 'Set to true to send requests to an S3 path style' + . ' endpoint by default.' + . ' Can be enabled or disabled on individual operations by setting' + . ' \'@use_path_style_endpoint\' to true or false.', + 'default' => false, + ], + 'disable_multiregion_access_points' => [ + 'type' => 'config', + 'valid' => ['bool'], + 'doc' => 'Set to true to disable the usage of' + . ' multi region access points. These are enabled by default.' + . ' Can be enabled or disabled on individual operations by setting' + . ' \'@disable_multiregion_access_points\' to true or false.', + 'default' => false, + ], + 'disable_express_session_auth' => [ + 'type' => 'config', + 'valid' => ['bool'], + 'doc' => 'Set to true to disable the usage of' + . ' s3 express session authentication. This is enabled by default.', + 'default' => [__CLASS__, '_default_disable_express_session_auth'], + ], + 's3_express_identity_provider' => [ + 'type' => 'config', + 'valid' => [ + 'bool', + 'callable' + ], + 'doc' => 'Specifies the provider used to generate identities to sign s3 express requests. ' + . 'Set to `false` to disable s3 express auth, or a callable provider used to create s3 express ' + . 'identities or return null.', + 'default' => [__CLASS__, '_default_s3_express_identity_provider'], + ], + ]; + } + + /** + * {@inheritdoc} + * + * In addition to the options available to + * {@see Aws\AwsClient::__construct}, S3Client accepts the following + * options: + * + * - bucket_endpoint: (bool) Set to true to send requests to a + * hardcoded bucket endpoint rather than create an endpoint as a result + * of injecting the bucket into the URL. This option is useful for + * interacting with CNAME endpoints. Note: if you are using version 2.243.0 + * and above and do not expect the bucket name to appear in the host, you will + * also need to set `use_path_style_endpoint` to `true`. + * - calculate_md5: (bool) Set to false to disable calculating an MD5 + * for all Amazon S3 signed uploads. + * - s3_us_east_1_regional_endpoint: + * (Aws\S3\RegionalEndpoint\ConfigurationInterface|Aws\CacheInterface\|callable|string|array) + * Specifies whether to use regional or legacy endpoints for the us-east-1 + * region. Provide an Aws\S3\RegionalEndpoint\ConfigurationInterface object, an + * instance of Aws\CacheInterface, a callable configuration provider used + * to create endpoint configuration, a string value of `legacy` or + * `regional`, or an associative array with the following keys: + * endpoint_types: (string) Set to `legacy` or `regional`, defaults to + * `legacy` + * - use_accelerate_endpoint: (bool) Set to true to send requests to an S3 + * Accelerate endpoint by default. Can be enabled or disabled on + * individual operations by setting '@use_accelerate_endpoint' to true or + * false. Note: you must enable S3 Accelerate on a bucket before it can be + * accessed via an Accelerate endpoint. + * - use_arn_region: (Aws\S3\UseArnRegion\ConfigurationInterface, + * Aws\CacheInterface, bool, callable) Set to true to enable the client + * to use the region from a supplied ARN argument instead of the client's + * region. Provide an instance of Aws\S3\UseArnRegion\ConfigurationInterface, + * an instance of Aws\CacheInterface, a callable that provides a promise for + * a Configuration object, or a boolean value. Defaults to false (i.e. + * the SDK will not follow the ARN region if it conflicts with the client + * region and instead throw an error). + * - use_dual_stack_endpoint: (bool) Set to true to send requests to an S3 + * Dual Stack endpoint by default, which enables IPv6 Protocol. + * Can be enabled or disabled on individual operations by setting + * '@use_dual_stack_endpoint\' to true or false. Note: + * you cannot use it together with an accelerate endpoint. + * - use_path_style_endpoint: (bool) Set to true to send requests to an S3 + * path style endpoint by default. + * Can be enabled or disabled on individual operations by setting + * '@use_path_style_endpoint\' to true or false. Note: + * you cannot use it together with an accelerate endpoint. + * - disable_multiregion_access_points: (bool) Set to true to disable + * sending multi region requests. They are enabled by default. + * Can be enabled or disabled on individual operations by setting + * '@disable_multiregion_access_points\' to true or false. Note: + * you cannot use it together with an accelerate or dualstack endpoint. + * + * @param array $args + */ + public function __construct(array $args) + { + if ( + !isset($args['s3_us_east_1_regional_endpoint']) + || $args['s3_us_east_1_regional_endpoint'] instanceof CacheInterface + ) { + $args['s3_us_east_1_regional_endpoint'] = ConfigurationProvider::defaultProvider($args); + } + $this->addBuiltIns($args); + parent::__construct($args); + $stack = $this->getHandlerList(); + $stack->appendInit(SSECMiddleware::wrap($this->getEndpoint()->getScheme()), 's3.ssec'); + $stack->appendBuild( + ApplyChecksumMiddleware::wrap($this->getApi(), $this->getConfig()), + 's3.checksum' + ); + $stack->appendBuild( + Middleware::contentType(['PutObject', 'UploadPart']), + 's3.content_type' + ); + + if ($this->getConfig('bucket_endpoint')) { + $stack->appendBuild(BucketEndpointMiddleware::wrap(), 's3.bucket_endpoint'); + } elseif (!$this->isUseEndpointV2()) { + $stack->appendBuild( + S3EndpointMiddleware::wrap( + $this->getRegion(), + $this->getConfig('endpoint_provider'), + [ + 'accelerate' => $this->getConfig('use_accelerate_endpoint'), + 'path_style' => $this->getConfig('use_path_style_endpoint'), + 'use_fips_endpoint' => $this->getConfig('use_fips_endpoint'), + 'dual_stack' => + $this->getConfig('use_dual_stack_endpoint')->isUseDualStackEndpoint(), + + ] + ), + 's3.endpoint_middleware' + ); + } + + $stack->appendBuild( + BucketEndpointArnMiddleware::wrap( + $this->getApi(), + $this->getRegion(), + [ + 'use_arn_region' => $this->getConfig('use_arn_region'), + 'accelerate' => $this->getConfig('use_accelerate_endpoint'), + 'path_style' => $this->getConfig('use_path_style_endpoint'), + 'dual_stack' => + $this->getConfig('use_dual_stack_endpoint')->isUseDualStackEndpoint(), + 'use_fips_endpoint' => $this->getConfig('use_fips_endpoint'), + 'disable_multiregion_access_points' => + $this->getConfig('disable_multiregion_access_points'), + 'endpoint' => $args['endpoint'] ?? null + ], + $this->isUseEndpointV2() + ), + 's3.bucket_endpoint_arn' + ); + if ($this->getConfig('disable_express_session_auth')) { + $stack->prependSign( + $this->getDisableExpressSessionAuthMiddleware(), + 's3.disable_express_session_auth' + ); + } + + $stack->appendValidate( + InputValidationMiddleware::wrap($this->getApi(), self::$mandatoryAttributes), + 'input_validation_middleware' + ); + $stack->appendSign(ExpiresParsingMiddleware::wrap(), 's3.expires_parsing'); + $stack->appendSign(PutObjectUrlMiddleware::wrap(), 's3.put_object_url'); + $stack->appendSign(PermanentRedirectMiddleware::wrap(), 's3.permanent_redirect'); + $stack->appendInit(Middleware::sourceFile($this->getApi()), 's3.source_file'); + $stack->appendInit($this->getSaveAsParameter(), 's3.save_as'); + $stack->appendInit($this->getLocationConstraintMiddleware(), 's3.location'); + $stack->appendInit($this->getEncodingTypeMiddleware(), 's3.auto_encode'); + $stack->appendInit($this->getHeadObjectMiddleware(), 's3.head_object'); + $this->processModel($this->isUseEndpointV2()); + if ($this->isUseEndpointV2()) { + $stack->after('builder', + 's3.check_empty_path_with_query', + $this->getEmptyPathWithQuery()); + } + } + + /** + * Determine if a string is a valid name for a DNS compatible Amazon S3 + * bucket. + * + * DNS compatible bucket names can be used as a subdomain in a URL (e.g., + * ".s3.amazonaws.com"). + * + * @param string $bucket Bucket name to check. + * + * @return bool + */ + public static function isBucketDnsCompatible($bucket) + { + if (!is_string($bucket)) { + return false; + } + $bucketLen = strlen($bucket); + + return ($bucketLen >= 3 && $bucketLen <= 63) && + // Cannot look like an IP address + !filter_var($bucket, FILTER_VALIDATE_IP) && + preg_match('/^[a-z0-9]([a-z0-9\-\.]*[a-z0-9])?$/', $bucket); + } + + public static function _apply_use_arn_region($value, array &$args, HandlerList $list) + { + if ($value instanceof CacheInterface) { + $value = UseArnRegionConfigurationProvider::defaultProvider($args); + } + if (is_callable($value)) { + $value = $value(); + } + if ($value instanceof PromiseInterface) { + $value = $value->wait(); + } + if ($value instanceof ConfigurationInterface) { + $args['use_arn_region'] = $value; + } else { + // The Configuration class itself will validate other inputs + $args['use_arn_region'] = new Configuration($value); + } + } + + public static function _default_request_checksum_calculation(array $args): string + { + return ConfigurationResolver::resolve( + 'request_checksum_calculation', + ApplyChecksumMiddleware::DEFAULT_CALCULATION_MODE, + 'string', + $args + ); + } + + public static function _apply_request_checksum_calculation( + string $value, + array &$args + ): void + { + $value = strtolower($value); + if (array_key_exists($value, self::$checksumOptionEnum)) { + $args['request_checksum_calculation'] = $value; + } else { + $validValues = implode(' | ', array_keys(self::$checksumOptionEnum)); + throw new \InvalidArgumentException( + 'invalid value provided for `request_checksum_calculation`.' + . ' valid values are: ' . $validValues . '.' + ); + } + } + + public static function _default_response_checksum_validation(array $args): string + { + return ConfigurationResolver::resolve( + 'response_checksum_validation', + ValidateResponseChecksumResultMutator::DEFAULT_VALIDATION_MODE, + 'string', + $args + ); + } + + public static function _apply_response_checksum_validation( + $value, + array &$args + ): void + { + $value = strtolower($value); + if (array_key_exists($value, self::$checksumOptionEnum)) { + $args['response_checksum_validation'] = $value; + } else { + $validValues = implode(' | ', array_keys(self::$checksumOptionEnum)); + throw new \InvalidArgumentException( + 'invalid value provided for `response_checksum_validation`.' + . ' valid values are: ' . $validValues . '.' + ); + } + } + + public static function _default_disable_express_session_auth(array &$args) + { + return ConfigurationResolver::resolve( + 's3_disable_express_session_auth', + false, + 'bool', + $args + ); + } + + public static function _default_s3_express_identity_provider(array $args) + { + if ($args['config']['disable_express_session_auth']) { + return false; + } + return new S3ExpressIdentityProvider($args['region']); + } + + public function createPresignedRequest(CommandInterface $command, $expires, array $options = []) + { + $command = clone $command; + $list = $command->getHandlerList(); + $list->remove('signer'); + + //Removes checksum calculation behavior by default + if (empty($command['ChecksumAlgorithm']) + && empty($command['AddContentMD5']) + ) { + $list->remove('s3.checksum'); + } + + $request = \Aws\serialize($command); + + //Applies ContentSHA256 parameter, if provided and not applied + // by middleware + $commandName = $command->getName(); + if (!empty($command['ContentSHA256'] + && isset(ApplyChecksumMiddleware::$sha256[$commandName]) + && !$request->hasHeader('X-Amz-Content-Sha256') + )) { + $request = $request->withHeader( + 'X-Amz-Content-Sha256', + $command['ContentSHA256'] + ); + } + + $signing_name = $command['@context']['signing_service'] + ?? $this->getSigningName($request->getUri()->getHost()); + $signature_version = $this->getSignatureVersionFromCommand($command); + + /** @var \Aws\Signature\SignatureInterface $signer */ + $signer = call_user_func( + $this->getSignatureProvider(), + $signature_version, + $signing_name, + $this->getConfig('signing_region') + ); + if ($signature_version == 'v4-s3express') { + $provider = $this->getConfig('s3_express_identity_provider'); + $credentials = $provider($command)->wait(); + } else { + $credentials = $this->getCredentials()->wait(); + } + return $signer->presign( + $request, + $credentials, + $expires, + $options + ); + } + + /** + * Returns the URL to an object identified by its bucket and key. + * + * The URL returned by this method is not signed nor does it ensure that the + * bucket and key given to the method exist. If you need a signed URL, then + * use the {@see \Aws\S3\S3Client::createPresignedRequest} method and get + * the URI of the signed request. + * + * @param string $bucket The name of the bucket where the object is located + * @param string $key The key of the object + * + * @return string The URL to the object + */ + public function getObjectUrl($bucket, $key) + { + $command = $this->getCommand('GetObject', [ + 'Bucket' => $bucket, + 'Key' => $key + ]); + + return (string) \Aws\serialize($command)->getUri(); + } + + /** + * Raw URL encode a key and allow for '/' characters + * + * @param string $key Key to encode + * + * @return string Returns the encoded key + */ + public static function encodeKey($key) + { + return str_replace('%2F', '/', rawurlencode($key)); + } + + /** + * Provides a middleware that removes the need to specify LocationConstraint on CreateBucket. + * + * @return \Closure + */ + private function getLocationConstraintMiddleware() + { + $region = $this->getRegion(); + return static function (callable $handler) use ($region) { + return function (Command $command, $request = null) use ($handler, $region) { + if ($command->getName() === 'CreateBucket' + && !self::isDirectoryBucket($command['Bucket']) + ) { + $locationConstraint = $command['CreateBucketConfiguration']['LocationConstraint'] + ?? null; + + if ($locationConstraint === 'us-east-1') { + unset($command['CreateBucketConfiguration']); + } elseif ('us-east-1' !== $region && empty($locationConstraint)) { + if (isset($command['CreateBucketConfiguration'])) { + $command['CreateBucketConfiguration']['LocationConstraint'] = $region; + } else { + $command['CreateBucketConfiguration'] = ['LocationConstraint' => $region]; + } + } + } + + return $handler($command, $request); + }; + }; + } + + /** + * Provides a middleware that supports the `SaveAs` parameter. + * + * @return \Closure + */ + private function getSaveAsParameter() + { + return static function (callable $handler) { + return function (Command $command, $request = null) use ($handler) { + if ($command->getName() === 'GetObject' && isset($command['SaveAs'])) { + $command['@http']['sink'] = $command['SaveAs']; + unset($command['SaveAs']); + } + + return $handler($command, $request); + }; + }; + } + + /** + * Provides a middleware that disables content decoding on HeadObject + * commands. + * + * @return \Closure + */ + private function getHeadObjectMiddleware() + { + return static function (callable $handler) { + return function ( + CommandInterface $command, + ?RequestInterface $request = null + ) use ($handler) { + if ($command->getName() === 'HeadObject' + && !isset($command['@http']['decode_content']) + ) { + $command['@http']['decode_content'] = false; + } + + return $handler($command, $request); + }; + }; + } + + /** + * Provides a middleware that autopopulates the EncodingType parameter on + * ListObjects commands. + * + * @return \Closure + */ + private function getEncodingTypeMiddleware() + { + return static function (callable $handler) { + return function (Command $command, $request = null) use ($handler) { + $autoSet = false; + if ($command->getName() === 'ListObjects' + && empty($command['EncodingType']) + ) { + $command['EncodingType'] = 'url'; + $autoSet = true; + } + + return $handler($command, $request) + ->then(function (ResultInterface $result) use ($autoSet) { + if ($result['EncodingType'] === 'url' && $autoSet) { + static $topLevel = [ + 'Delimiter', + 'Marker', + 'NextMarker', + 'Prefix', + ]; + static $nested = [ + ['Contents', 'Key'], + ['CommonPrefixes', 'Prefix'], + ]; + + foreach ($topLevel as $key) { + if (isset($result[$key])) { + $result[$key] = urldecode($result[$key]); + } + } + foreach ($nested as $steps) { + if (isset($result[$steps[0]])) { + foreach ($result[$steps[0]] as $key => $part) { + if (isset($part[$steps[1]])) { + $result[$steps[0]][$key][$steps[1]] + = urldecode($part[$steps[1]]); + } + } + } + } + + } + + return $result; + }); + }; + }; + } + + /** + * Provides a middleware that checks for an empty path and a + * non-empty query string. + * + * @return \Closure + */ + private function getEmptyPathWithQuery() + { + return static function (callable $handler) { + return function (Command $command, RequestInterface $request) use ($handler) { + $uri = $request->getUri(); + if (empty($uri->getPath()) && !empty($uri->getQuery())) { + $uri = $uri->withPath('/'); + $request = $request->withUri($uri); + } + + return $handler($command, $request); + }; + }; + } + + /** + * Provides a middleware that disables express session auth when + * customers opt out of it. + * + * @return \Closure + */ + private function getDisableExpressSessionAuthMiddleware() + { + return function (callable $handler) { + return function ( + CommandInterface $command, + ?RequestInterface $request = null + ) use ($handler) { + if (!empty($command['@context']['signature_version']) + && $command['@context']['signature_version'] === 'v4-s3express' + ) { + $command['@context']['signature_version'] = 's3v4'; + } + return $handler($command, $request); + }; + }; + } + + /** + * Special handling for when the service name is s3-object-lambda. + * So, if the host contains s3-object-lambda, then the service name + * returned is s3-object-lambda, otherwise the default signing service is returned. + * @param string $host The host to validate if is a s3-object-lambda URL. + * @return string returns the signing service name to be used + */ + private function getSigningName($host) + { + if (strpos( $host, 's3-object-lambda')) { + return 's3-object-lambda'; + } + + return $this->getConfig('signing_name'); + } + + /** + * If EndpointProviderV2 is used, removes `Bucket` from request URIs. + * This is now handled by the endpoint ruleset. + * + * Additionally adds a synthetic shape `ExpiresString` and modifies + * `Expires` type to ensure it remains set to `timestamp`. + * + * @param array $args + * @return void + * + * @internal + */ + private function processModel(bool $isUseEndpointV2): void + { + $definition = $this->getApi()->getDefinition(); + + if ($isUseEndpointV2) { + foreach($definition['operations'] as &$operation) { + if (isset($operation['http']['requestUri'])) { + $requestUri = $operation['http']['requestUri']; + if ($requestUri === "/{Bucket}") { + $requestUri = str_replace('/{Bucket}', '/', $requestUri); + } else { + $requestUri = str_replace('/{Bucket}', '', $requestUri); + } + $operation['http']['requestUri'] = $requestUri; + } + } + } + + foreach ($definition['shapes'] as $key => &$value) { + $suffix = 'Output'; + if (substr($key, -strlen($suffix)) === $suffix) { + if (isset($value['members']['Expires'])) { + $value['members']['Expires']['deprecated'] = true; + $value['members']['ExpiresString'] = [ + 'shape' => 'ExpiresString', + 'location' => 'header', + 'locationName' => 'Expires' + ]; + } + } + } + $definition['shapes']['ExpiresString']['type'] = 'string'; + $definition['shapes']['Expires']['type'] = 'timestamp'; + + $this->getApi()->setDefinition($definition); + } + + /** + * Adds service-specific client built-in values + * + * @return void + */ + private function addBuiltIns($args) + { + if (isset($args['region']) + && $args['region'] !== 'us-east-1' + ) { + return false; + } + + if (!isset($args['region']) + && ConfigurationResolver::resolve('region', '', 'string') !== 'us-east-1' + ) { + return false; + } + + $key = 'AWS::S3::UseGlobalEndpoint'; + $result = $args['s3_us_east_1_regional_endpoint'] instanceof \Closure ? + $args['s3_us_east_1_regional_endpoint']()->wait() : $args['s3_us_east_1_regional_endpoint']; + + if (is_string($result)) { + if ($result === 'regional') { + $value = false; + } else if ($result === 'legacy') { + $value = true; + } else { + return; + } + } else { + if ($result->isFallback() + || $result->getEndpointsType() === 'legacy' + ) { + $value = true; + } else { + $value = false; + } + } + $this->clientBuiltIns[$key] = $value; + } + + /** + * Determines whether a bucket is a directory bucket. + * Only considers the availability zone/suffix format + * + * @param string $bucket + * @return bool + */ + public static function isDirectoryBucket(string $bucket): bool + { + return preg_match(self::DIRECTORY_BUCKET_REGEX, $bucket) === 1; + } + + /** @internal */ + public static function _applyRetryConfig($value, $args, HandlerList $list) + { + if ($value) { + $config = \Aws\Retry\ConfigurationProvider::unwrap($value); + + if ($config->getMode() === 'legacy') { + $maxRetries = $config->getMaxAttempts() - 1; + $decider = RetryMiddleware::createDefaultDecider($maxRetries); + $decider = function ($retries, $command, $request, $result, $error) use ($decider, $maxRetries) { + $maxRetries = $command['@retries'] ?? $maxRetries; + + if ($decider($retries, $command, $request, $result, $error)) { + return true; + } + + if ($error instanceof AwsException + && $retries < $maxRetries + ) { + if ($error->getResponse() + && $error->getResponse()->getStatusCode() >= 400 + ) { + return strpos( + $error->getResponse()->getBody(), + 'Your socket connection to the server' + ) !== false; + } + + if ($error->getPrevious() instanceof RequestException) { + // All commands except CompleteMultipartUpload are + // idempotent and may be retried without worry if a + // networking error has occurred. + return $command->getName() !== 'CompleteMultipartUpload'; + } + } + + return false; + }; + + $delay = [RetryMiddleware::class, 'exponentialDelay']; + $list->appendSign(Middleware::retry($decider, $delay), 'retry'); + } else { + $defaultDecider = RetryMiddlewareV2::createDefaultDecider( + new QuotaManager(), + $config->getMaxAttempts() + ); + + $list->appendSign( + RetryMiddlewareV2::wrap( + $config, + [ + 'collect_stats' => $args['stats']['retries'], + 'decider' => function( + $attempts, + CommandInterface $cmd, + $result + ) use ($defaultDecider, $config) { + $isRetryable = $defaultDecider($attempts, $cmd, $result); + if (!$isRetryable + && $result instanceof AwsException + && $attempts < $config->getMaxAttempts() + ) { + if (!empty($result->getResponse()) + && $result->getResponse()->getStatusCode() >= 400 + ) { + return strpos( + $result->getResponse()->getBody(), + 'Your socket connection to the server' + ) !== false; + } + + if ($result->getPrevious() instanceof RequestException + && $cmd->getName() !== 'CompleteMultipartUpload' + ) { + $isRetryable = true; + } + } + + return $isRetryable; + } + ] + ), + 'retry' + ); + } + } + } + + /** @internal */ + public static function _applyApiProvider($value, array &$args, HandlerList $list) + { + ClientResolver::_apply_api_provider($value, $args); + $s3Parser = new S3Parser( + $args['parser'], + $args['error_parser'], + $args['api'], + $args['exception_class'] + ); + $s3Parser->addS3ResultMutator( + 'get-bucket-location', + new GetBucketLocationResultMutator() + ); + $s3Parser->addS3ResultMutator( + 'validate-response-checksum', + new ValidateResponseChecksumResultMutator( + $args['api'], + ['response_checksum_validation' => $args['response_checksum_validation']] + ) + ); + $args['parser'] = $s3Parser; + } + + /** + * @internal + * @codeCoverageIgnore + */ + public static function applyDocFilters(array $api, array $docs) + { + $b64 = '
This value will be base64 encoded on your behalf.
'; + $opt = '
This value will be computed for you it is not supplied.
'; + + // Add a note on the CopyObject docs + $s3ExceptionRetryMessage = "

Additional info on response behavior: if there is" + . " an internal error in S3 after the request was successfully recieved," + . " a 200 response will be returned with an S3Exception embedded" + . " in it; this will still be caught and retried by" + . " RetryMiddleware.

"; + + $docs['operations']['CopyObject'] .= $s3ExceptionRetryMessage; + $docs['operations']['CompleteMultipartUpload'] .= $s3ExceptionRetryMessage; + $docs['operations']['UploadPartCopy'] .= $s3ExceptionRetryMessage; + $docs['operations']['UploadPart'] .= $s3ExceptionRetryMessage; + + // Add note about stream ownership in the putObject call + $guzzleStreamMessage = "

Additional info on behavior of the stream" + . " parameters: Psr7 takes ownership of streams and will automatically close" + . " streams when this method is called with a stream as the Body" + . " parameter. To prevent this, set the Body using" + . " GuzzleHttp\Psr7\stream_for method with a is an instance of" + . " Psr\Http\Message\StreamInterface, and it will be returned" + . " unmodified. This will allow you to keep the stream in scope.

"; + $docs['operations']['PutObject'] .= $guzzleStreamMessage; + + // Add the SourceFile parameter. + $docs['shapes']['SourceFile']['base'] = 'The path to a file on disk to use instead of the Body parameter.'; + $api['shapes']['SourceFile'] = ['type' => 'string']; + $api['shapes']['PutObjectRequest']['members']['SourceFile'] = ['shape' => 'SourceFile']; + $api['shapes']['UploadPartRequest']['members']['SourceFile'] = ['shape' => 'SourceFile']; + + // Add the ContentSHA256 parameter. + $docs['shapes']['ContentSHA256']['base'] = 'A SHA256 hash of the body content of the request.'; + $api['shapes']['ContentSHA256'] = ['type' => 'string']; + $api['shapes']['PutObjectRequest']['members']['ContentSHA256'] = ['shape' => 'ContentSHA256']; + $api['shapes']['UploadPartRequest']['members']['ContentSHA256'] = ['shape' => 'ContentSHA256']; + $docs['shapes']['ContentSHA256']['append'] = $opt; + + // Add the AddContentMD5 parameter. + $docs['shapes']['AddContentMD5']['base'] = 'Set to true to calculate the ContentMD5 for the upload.'; + $api['shapes']['AddContentMD5'] = ['type' => 'boolean']; + $api['shapes']['PutObjectRequest']['members']['AddContentMD5'] = ['shape' => 'AddContentMD5']; + $api['shapes']['UploadPartRequest']['members']['AddContentMD5'] = ['shape' => 'AddContentMD5']; + + // Add the SaveAs parameter. + $docs['shapes']['SaveAs']['base'] = 'The path to a file on disk to save the object data.'; + $api['shapes']['SaveAs'] = ['type' => 'string']; + $api['shapes']['GetObjectRequest']['members']['SaveAs'] = ['shape' => 'SaveAs']; + + // Several SSECustomerKey documentation updates. + $docs['shapes']['SSECustomerKey']['append'] = $b64; + $docs['shapes']['CopySourceSSECustomerKey']['append'] = $b64; + $docs['shapes']['SSECustomerKeyMd5']['append'] = $opt; + + // Add the ObjectURL to various output shapes and documentation. + $docs['shapes']['ObjectURL']['base'] = 'The URI of the created object.'; + $api['shapes']['ObjectURL'] = ['type' => 'string']; + $api['shapes']['PutObjectOutput']['members']['ObjectURL'] = ['shape' => 'ObjectURL']; + $api['shapes']['CopyObjectOutput']['members']['ObjectURL'] = ['shape' => 'ObjectURL']; + $api['shapes']['CompleteMultipartUploadOutput']['members']['ObjectURL'] = ['shape' => 'ObjectURL']; + + // Fix references to Location Constraint. + unset($api['shapes']['CreateBucketRequest']['payload']); + $api['shapes']['BucketLocationConstraint']['enum'] = [ + "ap-northeast-1", + "ap-southeast-2", + "ap-southeast-1", + "cn-north-1", + "eu-central-1", + "eu-west-1", + "us-east-1", + "us-west-1", + "us-west-2", + "sa-east-1", + ]; + + // Add a note that the ContentMD5 is automatically computed, except for with PutObject and UploadPart + $docs['shapes']['ContentMD5']['append'] = '
The value will be computed on ' + . 'your behalf.
'; + $docs['shapes']['ContentMD5']['excludeAppend'] = ['PutObjectRequest', 'UploadPartRequest']; + + //Add a note to ContentMD5 for PutObject and UploadPart that specifies the value is required + // When uploading to a bucket with object lock enabled and that it is not computed automatically + $objectLock = '
This value is required if uploading to a bucket ' + . 'which has Object Lock enabled. It will not be calculated for you automatically. If you wish to have ' + . 'the value calculated for you, use the `AddContentMD5` parameter.
'; + $docs['shapes']['ContentMD5']['appendOnly'] = [ + 'message' => $objectLock, + 'shapes' => ['PutObjectRequest', 'UploadPartRequest'] + ]; + + // Add `ExpiresString` shape to output structures which contain `Expires` + // Deprecate existing `Expires` shapes in output structures + // Add/Update documentation for both `ExpiresString` and `Expires` + // Ensure `Expires` type remains timestamp + foreach ($api['shapes'] as $key => &$value) { + $suffix = 'Output'; + if (substr($key, -strlen($suffix)) === $suffix) { + if (isset($value['members']['Expires'])) { + $value['members']['Expires']['deprecated'] = true; + $value['members']['ExpiresString'] = [ + 'shape' => 'ExpiresString', + 'location' => 'header', + 'locationName' => 'Expires' + ]; + $docs['shapes']['Expires']['refs'][$key . '$Expires'] + .= '

This output shape has been deprecated. Please refer to ExpiresString instead.

.'; + } + } + } + $api['shapes']['ExpiresString']['type'] = 'string'; + $docs['shapes']['ExpiresString']['base'] = 'The unparsed string value of the Expires output member.'; + $api['shapes']['Expires']['type'] = 'timestamp'; + + return [ + new Service($api, ApiProvider::defaultProvider()), + new DocModel($docs) + ]; + } + + /** + * @internal + * @codeCoverageIgnore + */ + public static function addDocExamples($examples) + { + $getObjectExample = [ + 'input' => [ + 'Bucket' => 'arn:aws:s3:us-east-1:123456789012:accesspoint:myaccesspoint', + 'Key' => 'my-key' + ], + 'output' => [ + 'Body' => 'class GuzzleHttp\Psr7\Stream#208 (7) {...}', + 'ContentLength' => '11', + 'ContentType' => 'application/octet-stream', + ], + 'comments' => [ + 'input' => '', + 'output' => 'Simplified example output' + ], + 'description' => 'The following example retrieves an object by referencing the bucket via an S3 accesss point ARN. Result output is simplified for the example.', + 'id' => '', + 'title' => 'To get an object via an S3 access point ARN' + ]; + if (isset($examples['GetObject'])) { + $examples['GetObject'] []= $getObjectExample; + } else { + $examples['GetObject'] = [$getObjectExample]; + } + + $putObjectExample = [ + 'input' => [ + 'Bucket' => 'arn:aws:s3:us-east-1:123456789012:accesspoint:myaccesspoint', + 'Key' => 'my-key', + 'Body' => 'my-body', + ], + 'output' => [ + 'ObjectURL' => 'https://my-bucket.s3.us-east-1.amazonaws.com/my-key' + ], + 'comments' => [ + 'input' => '', + 'output' => 'Simplified example output' + ], + 'description' => 'The following example uploads an object by referencing the bucket via an S3 accesss point ARN. Result output is simplified for the example.', + 'id' => '', + 'title' => 'To upload an object via an S3 access point ARN' + ]; + if (isset($examples['PutObject'])) { + $examples['PutObject'] []= $putObjectExample; + } else { + $examples['PutObject'] = [$putObjectExample]; + } + + return $examples; + } + + /** + * @param CommandInterface $command + * @return array|mixed|null + */ + private function getSignatureVersionFromCommand(CommandInterface $command) + { + return $command['@context']['signature_version'] + ?? $this->getConfig('signature_version'); + } +} diff --git a/3rdparty/aws/aws-sdk-php/src/S3/S3ClientInterface.php b/3rdparty/aws/aws-sdk-php/src/S3/S3ClientInterface.php new file mode 100644 index 00000000..261d7dd3 --- /dev/null +++ b/3rdparty/aws/aws-sdk-php/src/S3/S3ClientInterface.php @@ -0,0 +1,369 @@ +uploadAsync($bucket, $key, $body, $acl, $options) + ->wait(); + } + + /** + * @see S3ClientInterface::uploadAsync() + */ + public function uploadAsync( + $bucket, + $key, + $body, + $acl = 'private', + array $options = [] + ) { + return (new ObjectUploader($this, $bucket, $key, $body, $acl, $options)) + ->promise(); + } + + /** + * @see S3ClientInterface::copy() + */ + public function copy( + $fromB, + $fromK, + $destB, + $destK, + $acl = 'private', + array $opts = [] + ) { + return $this->copyAsync($fromB, $fromK, $destB, $destK, $acl, $opts) + ->wait(); + } + + /** + * @see S3ClientInterface::copyAsync() + */ + public function copyAsync( + $fromB, + $fromK, + $destB, + $destK, + $acl = 'private', + array $opts = [] + ) { + $source = [ + 'Bucket' => $fromB, + 'Key' => $fromK, + ]; + if (isset($opts['version_id'])) { + $source['VersionId'] = $opts['version_id']; + } + $destination = [ + 'Bucket' => $destB, + 'Key' => $destK + ]; + + return (new ObjectCopier($this, $source, $destination, $acl, $opts)) + ->promise(); + } + + /** + * @see S3ClientInterface::registerStreamWrapper() + */ + public function registerStreamWrapper() + { + StreamWrapper::register($this); + } + + /** + * @see S3ClientInterface::registerStreamWrapperV2() + */ + public function registerStreamWrapperV2() + { + StreamWrapper::register( + $this, + 's3', + null, + true + ); + } + + /** + * @see S3ClientInterface::deleteMatchingObjects() + */ + public function deleteMatchingObjects( + $bucket, + $prefix = '', + $regex = '', + array $options = [] + ) { + $this->deleteMatchingObjectsAsync($bucket, $prefix, $regex, $options) + ->wait(); + } + + /** + * @see S3ClientInterface::deleteMatchingObjectsAsync() + */ + public function deleteMatchingObjectsAsync( + $bucket, + $prefix = '', + $regex = '', + array $options = [] + ) { + if (!$prefix && !$regex) { + return new RejectedPromise( + new \RuntimeException('A prefix or regex is required.') + ); + } + + $params = ['Bucket' => $bucket, 'Prefix' => $prefix]; + $iter = $this->getIterator('ListObjects', $params); + + if ($regex) { + $iter = \Aws\filter($iter, function ($c) use ($regex) { + return preg_match($regex, $c['Key']); + }); + } + + return BatchDelete::fromIterator($this, $bucket, $iter, $options) + ->promise(); + } + + /** + * @see S3ClientInterface::uploadDirectory() + */ + public function uploadDirectory( + $directory, + $bucket, + $keyPrefix = null, + array $options = [] + ) { + $this->uploadDirectoryAsync($directory, $bucket, $keyPrefix, $options) + ->wait(); + } + + /** + * @see S3ClientInterface::uploadDirectoryAsync() + */ + public function uploadDirectoryAsync( + $directory, + $bucket, + $keyPrefix = null, + array $options = [] + ) { + $d = "s3://$bucket" . ($keyPrefix ? '/' . ltrim($keyPrefix, '/') : ''); + return (new Transfer($this, $directory, $d, $options))->promise(); + } + + /** + * @see S3ClientInterface::downloadBucket() + */ + public function downloadBucket( + $directory, + $bucket, + $keyPrefix = '', + array $options = [] + ) { + $this->downloadBucketAsync($directory, $bucket, $keyPrefix, $options) + ->wait(); + } + + /** + * @see S3ClientInterface::downloadBucketAsync() + */ + public function downloadBucketAsync( + $directory, + $bucket, + $keyPrefix = '', + array $options = [] + ) { + $s = "s3://$bucket" . ($keyPrefix ? '/' . ltrim($keyPrefix, '/') : ''); + return (new Transfer($this, $s, $directory, $options))->promise(); + } + + /** + * @see S3ClientInterface::determineBucketRegion() + */ + public function determineBucketRegion($bucketName) + { + return $this->determineBucketRegionAsync($bucketName)->wait(); + } + + /** + * @see S3ClientInterface::determineBucketRegionAsync() + * + * @param string $bucketName + * + * @return PromiseInterface + */ + public function determineBucketRegionAsync($bucketName) + { + $command = $this->getCommand('HeadBucket', ['Bucket' => $bucketName]); + $handlerList = clone $this->getHandlerList(); + $handlerList->remove('s3.permanent_redirect'); + $handlerList->remove('signer'); + $handler = $handlerList->resolve(); + + return $handler($command) + ->then(static function (ResultInterface $result) { + return $result['@metadata']['headers']['x-amz-bucket-region']; + }, function (AwsException $e) { + $response = $e->getResponse(); + if ($response === null) { + throw $e; + } + + if ($e->getAwsErrorCode() === 'AuthorizationHeaderMalformed') { + $region = $this->determineBucketRegionFromExceptionBody( + $response + ); + if (!empty($region)) { + return $region; + } + throw $e; + } + + return $response->getHeaderLine('x-amz-bucket-region'); + }); + } + + private function determineBucketRegionFromExceptionBody(ResponseInterface $response) + { + try { + $element = $this->parseXml($response->getBody(), $response); + if (!empty($element->Region)) { + return (string)$element->Region; + } + } catch (\Exception $e) { + // Fallthrough on exceptions from parsing + } + return false; + } + + /** + * @see S3ClientInterface::doesBucketExist() + */ + public function doesBucketExist($bucket) + { + return $this->checkExistenceWithCommand( + $this->getCommand('HeadBucket', ['Bucket' => $bucket]) + ); + } + + /** + * @see S3ClientInterface::doesBucketExistV2() + */ + public function doesBucketExistV2($bucket, $accept403 = false) + { + $command = $this->getCommand('HeadBucket', ['Bucket' => $bucket]); + + try { + $this->execute($command); + return true; + } catch (S3Exception $e) { + if ( + ($accept403 && $e->getStatusCode() === 403) + || $e instanceof PermanentRedirectException + ) { + return true; + } + if ($e->getStatusCode() === 404) { + return false; + } + throw $e; + } + } + + /** + * @see S3ClientInterface::doesObjectExist() + */ + public function doesObjectExist($bucket, $key, array $options = []) + { + return $this->checkExistenceWithCommand( + $this->getCommand('HeadObject', [ + 'Bucket' => $bucket, + 'Key' => $key + ] + $options) + ); + } + + /** + * @see S3ClientInterface::doesObjectExistV2() + */ + public function doesObjectExistV2( + $bucket, + $key, + $includeDeleteMarkers = false, + array $options = [] + ){ + $command = $this->getCommand('HeadObject', [ + 'Bucket' => $bucket, + 'Key' => $key + ] + $options + ); + + try { + $this->execute($command); + return true; + } catch (S3Exception $e) { + if ($includeDeleteMarkers + && $this->useDeleteMarkers($e) + ) { + return true; + } + if ($e->getStatusCode() === 404) { + return false; + } + throw $e; + } + } + + private function useDeleteMarkers($exception) + { + $response = $exception->getResponse(); + return !empty($response) + && $response->getHeader('x-amz-delete-marker'); + } + + /** + * Determines whether or not a resource exists using a command + * + * @param CommandInterface $command Command used to poll for the resource + * + * @return bool + * @throws S3Exception|\Exception if there is an unhandled exception + */ + private function checkExistenceWithCommand(CommandInterface $command) + { + try { + $this->execute($command); + return true; + } catch (S3Exception $e) { + if ($e->getAwsErrorCode() == 'AccessDenied') { + return true; + } + if ($e->getStatusCode() >= 500) { + throw $e; + } + return false; + } + } + + /** + * @see S3ClientInterface::execute() + */ + abstract public function execute(CommandInterface $command); + + /** + * @see S3ClientInterface::getCommand() + */ + abstract public function getCommand($name, array $args = []); + + /** + * @see S3ClientInterface::getHandlerList() + * + * @return HandlerList + */ + abstract public function getHandlerList(); + + /** + * @see S3ClientInterface::getIterator() + * + * @return \Iterator + */ + abstract public function getIterator($name, array $args = []); +} diff --git a/3rdparty/aws/aws-sdk-php/src/S3/S3EndpointMiddleware.php b/3rdparty/aws/aws-sdk-php/src/S3/S3EndpointMiddleware.php new file mode 100644 index 00000000..c8dd0070 --- /dev/null +++ b/3rdparty/aws/aws-sdk-php/src/S3/S3EndpointMiddleware.php @@ -0,0 +1,343 @@ + true, + 'DeleteBucket' => true, + 'ListBuckets' => true, + ]; + + const NO_PATTERN = 0; + const DUALSTACK = 1; + const ACCELERATE = 2; + const ACCELERATE_DUALSTACK = 3; + const PATH_STYLE = 4; + const HOST_STYLE = 5; + + /** @var bool */ + private $accelerateByDefault; + /** @var bool */ + private $dualStackByDefault; + /** @var bool */ + private $pathStyleByDefault; + /** @var string */ + private $region; + /** @var callable */ + private $endpointProvider; + /** @var callable */ + private $nextHandler; + /** @var string */ + private $endpoint; + + /** + * Create a middleware wrapper function + * + * @param string $region + * @param EndpointProvider $endpointProvider + * @param array $options + * + * @return callable + */ + public static function wrap($region, $endpointProvider, array $options) + { + return function (callable $handler) use ($region, $endpointProvider, $options) { + return new self($handler, $region, $options, $endpointProvider); + }; + } + + public function __construct( + callable $nextHandler, + $region, + array $options, + $endpointProvider = null + ) { + $this->pathStyleByDefault = isset($options['path_style']) + ? (bool) $options['path_style'] : false; + $this->dualStackByDefault = isset($options['dual_stack']) + ? (bool) $options['dual_stack'] : false; + $this->accelerateByDefault = isset($options['accelerate']) + ? (bool) $options['accelerate'] : false; + $this->region = (string) $region; + $this->endpoint = isset($options['endpoint']) + ? $options['endpoint'] : ""; + $this->endpointProvider = is_null($endpointProvider) + ? PartitionEndpointProvider::defaultProvider() + : $endpointProvider; + $this->nextHandler = $nextHandler; + } + + public function __invoke(CommandInterface $command, RequestInterface $request) + { + if (!empty($this->endpoint)) { + $request = $this->applyEndpoint($command, $request); + } else { + switch ($this->endpointPatternDecider($command, $request)) { + case self::HOST_STYLE: + $request = $this->applyHostStyleEndpoint($command, $request); + break; + case self::NO_PATTERN: + break; + case self::PATH_STYLE: + $request = $this->applyPathStyleEndpointCustomizations($command, $request); + break; + case self::DUALSTACK: + $request = $this->applyDualStackEndpoint($command, $request); + break; + case self::ACCELERATE: + $request = $this->applyAccelerateEndpoint( + $command, + $request, + 's3-accelerate' + ); + break; + case self::ACCELERATE_DUALSTACK: + $request = $this->applyAccelerateEndpoint( + $command, + $request, + 's3-accelerate.dualstack' + ); + break; + } + } + $nextHandler = $this->nextHandler; + return $nextHandler($command, $request); + } + + private static function isRequestHostStyleCompatible( + CommandInterface $command, + RequestInterface $request + ) { + return S3Client::isBucketDnsCompatible($command['Bucket']) + && ( + $request->getUri()->getScheme() === 'http' + || strpos($command['Bucket'], '.') === false + ) + && filter_var($request->getUri()->getHost(), FILTER_VALIDATE_IP) === false; + } + + private function endpointPatternDecider( + CommandInterface $command, + RequestInterface $request + ) { + $accelerate = isset($command['@use_accelerate_endpoint']) + ? $command['@use_accelerate_endpoint'] : $this->accelerateByDefault; + $dualStack = isset($command['@use_dual_stack_endpoint']) + ? $command['@use_dual_stack_endpoint'] : $this->dualStackByDefault; + $pathStyle = isset($command['@use_path_style_endpoint']) + ? $command['@use_path_style_endpoint'] : $this->pathStyleByDefault; + + if ($accelerate && $dualStack) { + // When try to enable both for operations excluded from s3-accelerate, + // only dualstack endpoints will be enabled. + return $this->canAccelerate($command) + ? self::ACCELERATE_DUALSTACK + : self::DUALSTACK; + } + + if ($accelerate && $this->canAccelerate($command)) { + return self::ACCELERATE; + } + + if ($dualStack) { + return self::DUALSTACK; + } + + if (!$pathStyle + && self::isRequestHostStyleCompatible($command, $request) + ) { + return self::HOST_STYLE; + } + + return self::PATH_STYLE; + } + + private function canAccelerate(CommandInterface $command) + { + return empty(self::$exclusions[$command->getName()]) + && S3Client::isBucketDnsCompatible($command['Bucket']); + } + + private function getBucketStyleHost(CommandInterface $command, $host) + { + // For operations on the base host (e.g. ListBuckets) + if (!isset($command['Bucket'])) { + return $host; + } + + return "{$command['Bucket']}.{$host}"; + } + + private function applyHostStyleEndpoint( + CommandInterface $command, + RequestInterface $request + ) { + $uri = $request->getUri(); + $request = $request->withUri( + $uri->withHost($this->getBucketStyleHost( + $command, + $uri->getHost() + )) + ->withPath($this->getBucketlessPath( + $uri->getPath(), + $command + )) + ); + return $request; + } + + private function applyPathStyleEndpointCustomizations( + CommandInterface $command, + RequestInterface $request + ) { + if ($command->getName() == 'WriteGetObjectResponse') { + $dnsSuffix = $this->endpointProvider + ->getPartition($this->region, 's3') + ->getDnsSuffix(); + $fips = \Aws\is_fips_pseudo_region($this->region) ? "-fips" : ""; + $region = \Aws\strip_fips_pseudo_regions($this->region); + $host = + "{$command['RequestRoute']}.s3-object-lambda{$fips}.{$region}.{$dnsSuffix}"; + + $uri = $request->getUri(); + $request = $request->withUri( + $uri->withHost($host) + ->withPath($this->getBucketlessPath( + $uri->getPath(), + $command + )) + ); + } + return $request; + } + + + private function applyDualStackEndpoint( + CommandInterface $command, + RequestInterface $request + ) { + $request = $request->withUri( + $request->getUri()->withHost($this->getDualStackHost()) + ); + + if (empty($command['@use_path_style_endpoint']) + && !$this->pathStyleByDefault + && self::isRequestHostStyleCompatible($command, $request) + ) { + $request = $this->applyHostStyleEndpoint($command, $request); + } + return $request; + } + + private function getDualStackHost() + { + $dnsSuffix = $this->endpointProvider + ->getPartition($this->region, 's3') + ->getDnsSuffix(); + return "s3.dualstack.{$this->region}.{$dnsSuffix}"; + } + + private function applyAccelerateEndpoint( + CommandInterface $command, + RequestInterface $request, + $pattern + ) { + $request = $request->withUri( + $request->getUri() + ->withHost($this->getAccelerateHost($command, $pattern)) + ->withPath($this->getBucketlessPath( + $request->getUri()->getPath(), + $command + )) + ); + return $request; + } + + private function getAccelerateHost(CommandInterface $command, $pattern) + { + $dnsSuffix = $this->endpointProvider + ->getPartition($this->region, 's3') + ->getDnsSuffix(); + return "{$command['Bucket']}.{$pattern}.{$dnsSuffix}"; + } + + private function getBucketlessPath($path, CommandInterface $command) + { + $pattern = '/^\\/' . preg_quote($command['Bucket'], '/') . '/'; + $path = preg_replace($pattern, '', $path) ?: '/'; + if (substr($path, 0 , 1) !== '/') { + $path = '/' . $path; + } + return $path; + } + + private function applyEndpoint( + CommandInterface $command, + RequestInterface $request + ) { + $dualStack = isset($command['@use_dual_stack_endpoint']) + ? $command['@use_dual_stack_endpoint'] : $this->dualStackByDefault; + if (ArnParser::isArn($command['Bucket'])) { + $arn = ArnParser::parse($command['Bucket']); + $outpost = $arn->getService() == 's3-outposts'; + if ($outpost && $dualStack) { + throw new InvalidArgumentException("Outposts + dualstack is not supported"); + } + if ($arn instanceof ObjectLambdaAccessPointArn) { + return $request; + } + } + if ($dualStack) { + throw new InvalidArgumentException("Custom Endpoint + Dualstack not supported"); + } + if ($command->getName() == 'WriteGetObjectResponse') { + $host = "{$command['RequestRoute']}.{$this->endpoint}"; + $uri = $request->getUri(); + return $request = $request->withUri( + $uri->withHost($host) + ->withPath($this->getBucketlessPath( + $uri->getPath(), + $command + )) + ); + } + $host = ($this->pathStyleByDefault) ? + $this->endpoint : + $this->getBucketStyleHost( + $command, + $this->endpoint + ); + $uri = $request->getUri(); + $scheme = $uri->getScheme(); + if(empty($scheme)){ + $request = $request->withUri( + $uri->withHost($host) + ); + } else { + $request = $request->withUri($uri); + } + + return $request; + } +} diff --git a/3rdparty/aws/aws-sdk-php/src/S3/S3MultiRegionClient.php b/3rdparty/aws/aws-sdk-php/src/S3/S3MultiRegionClient.php new file mode 100644 index 00000000..c6ca323e --- /dev/null +++ b/3rdparty/aws/aws-sdk-php/src/S3/S3MultiRegionClient.php @@ -0,0 +1,370 @@ + function (array &$args) { + $availableRegions = array_keys($args['partition']['regions']); + return end($availableRegions); + }]; + unset($args['region']); + + return $args + [ + 'bucket_region_cache' => [ + 'type' => 'config', + 'valid' => [CacheInterface::class], + 'doc' => 'Cache of regions in which given buckets are located.', + 'default' => function () { return new LruArrayCache; }, + ], + 'region' => $regionDef, + ]; + } + + public function __construct(array $args) + { + parent::__construct($args); + $this->cache = $this->getConfig('bucket_region_cache'); + + $this->getHandlerList()->prependInit( + $this->determineRegionMiddleware(), + 'determine_region' + ); + } + + private function determineRegionMiddleware() + { + return function (callable $handler) { + return function (CommandInterface $command) use ($handler) { + $cacheKey = $this->getCacheKey($command['Bucket']); + if ( + empty($command['@region']) && + $region = $this->cache->get($cacheKey) + ) { + $command['@region'] = $region; + } + + return Promise\Coroutine::of(function () use ( + $handler, + $command, + $cacheKey + ) { + try { + yield $handler($command); + } catch (PermanentRedirectException $e) { + if (empty($command['Bucket'])) { + throw $e; + } + $result = $e->getResult(); + $region = null; + if (isset($result['@metadata']['headers']['x-amz-bucket-region'])) { + $region = $result['@metadata']['headers']['x-amz-bucket-region']; + $this->cache->set($cacheKey, $region); + } else { + $region = (yield $this->determineBucketRegionAsync( + $command['Bucket'] + )); + } + + $command['@region'] = $region; + yield $handler($command); + } catch (AwsException $e) { + if ($e->getAwsErrorCode() === 'AuthorizationHeaderMalformed') { + $region = $this->determineBucketRegionFromExceptionBody( + $e->getResponse() + ); + if (!empty($region)) { + $this->cache->set($cacheKey, $region); + + $command['@region'] = $region; + yield $handler($command); + } else { + throw $e; + } + } else { + throw $e; + } + } + }); + }; + }; + } + + public function createPresignedRequest(CommandInterface $command, $expires, array $options = []) + { + if (empty($command['Bucket'])) { + throw new \InvalidArgumentException('The S3\\MultiRegionClient' + . ' cannot create presigned requests for commands without a' + . ' specified bucket.'); + } + + /** @var S3ClientInterface $client */ + $client = $this->getClientFromPool( + $this->determineBucketRegion($command['Bucket']) + ); + return $client->createPresignedRequest( + $client->getCommand($command->getName(), $command->toArray()), + $expires, + $options + ); + } + + public function getObjectUrl($bucket, $key) + { + /** @var S3Client $regionalClient */ + $regionalClient = $this->getClientFromPool( + $this->determineBucketRegion($bucket) + ); + + return $regionalClient->getObjectUrl($bucket, $key); + } + + public function determineBucketRegionAsync($bucketName) + { + $cacheKey = $this->getCacheKey($bucketName); + if ($cached = $this->cache->get($cacheKey)) { + return Promise\Create::promiseFor($cached); + } + + /** @var S3ClientInterface $regionalClient */ + $regionalClient = $this->getClientFromPool(); + return $regionalClient->determineBucketRegionAsync($bucketName) + ->then( + function ($region) use ($cacheKey) { + $this->cache->set($cacheKey, $region); + + return $region; + } + ); + } + + private function getCacheKey($bucketName) + { + return "aws:s3:{$bucketName}:location"; + } +} diff --git a/3rdparty/aws/aws-sdk-php/src/S3/S3UriParser.php b/3rdparty/aws/aws-sdk-php/src/S3/S3UriParser.php new file mode 100644 index 00000000..508bff14 --- /dev/null +++ b/3rdparty/aws/aws-sdk-php/src/S3/S3UriParser.php @@ -0,0 +1,163 @@ + true, + 'bucket' => null, + 'key' => null, + 'region' => null + ]; + + /** + * Parses a URL or S3 StreamWrapper Uri (s3://) into an associative array + * of Amazon S3 data including: + * + * - bucket: The Amazon S3 bucket (null if none) + * - key: The Amazon S3 key (null if none) + * - path_style: Set to true if using path style, or false if not + * - region: Set to a string if a non-class endpoint is used or null. + * + * @param string|UriInterface $uri + * + * @return array + * @throws \InvalidArgumentException|InvalidArnException + */ + public function parse($uri) + { + // Attempt to parse host component of uri as an ARN + $components = $this->parseS3UrlComponents($uri); + if (!empty($components)) { + if (ArnParser::isArn($components['host'])) { + $arn = new AccessPointArn($components['host']); + return [ + 'bucket' => $components['host'], + 'key' => $components['path'], + 'path_style' => false, + 'region' => $arn->getRegion() + ]; + } + } + + $url = Psr7\Utils::uriFor($uri); + + if ($url->getScheme() == $this->streamWrapperScheme) { + return $this->parseStreamWrapper($url); + } + + if (!$url->getHost()) { + throw new \InvalidArgumentException('No hostname found in URI: ' + . $uri); + } + + if (!preg_match($this->pattern, $url->getHost(), $matches)) { + return $this->parseCustomEndpoint($url); + } + + // Parse the URI based on the matched format (path / virtual) + $result = empty($matches[1]) + ? $this->parsePathStyle($url) + : $this->parseVirtualHosted($url, $matches); + + // Add the region if one was found and not the classic endpoint + $result['region'] = $matches[2] == 'amazonaws' ? null : $matches[2]; + + return $result; + } + + private function parseS3UrlComponents($uri) + { + preg_match("/^([a-zA-Z0-9]*):\/\/([a-zA-Z0-9:-]*)\/(.*)/", $uri, $components); + if (empty($components)) { + return []; + } + return [ + 'scheme' => $components[1], + 'host' => $components[2], + 'path' => $components[3], + ]; + } + + private function parseStreamWrapper(UriInterface $url) + { + $result = self::$defaultResult; + $result['path_style'] = false; + + $result['bucket'] = $url->getHost(); + if ($url->getPath()) { + $key = ltrim($url->getPath(), '/ '); + if (!empty($key)) { + $result['key'] = $key; + } + } + + return $result; + } + + private function parseCustomEndpoint(UriInterface $url) + { + $result = self::$defaultResult; + $path = ltrim($url->getPath(), '/ '); + $segments = explode('/', $path, 2); + + if (isset($segments[0])) { + $result['bucket'] = $segments[0]; + if (isset($segments[1])) { + $result['key'] = $segments[1]; + } + } + + return $result; + } + + private function parsePathStyle(UriInterface $url) + { + $result = self::$defaultResult; + + if ($url->getPath() != '/') { + $path = ltrim($url->getPath(), '/'); + if ($path) { + $pathPos = strpos($path, '/'); + if ($pathPos === false) { + // https://s3.amazonaws.com/bucket + $result['bucket'] = $path; + } elseif ($pathPos == strlen($path) - 1) { + // https://s3.amazonaws.com/bucket/ + $result['bucket'] = substr($path, 0, -1); + } else { + // https://s3.amazonaws.com/bucket/key + $result['bucket'] = substr($path, 0, $pathPos); + $result['key'] = substr($path, $pathPos + 1) ?: null; + } + } + } + + return $result; + } + + private function parseVirtualHosted(UriInterface $url, array $matches) + { + $result = self::$defaultResult; + $result['path_style'] = false; + // Remove trailing "." from the prefix to get the bucket + $result['bucket'] = substr($matches[1], 0, -1); + $path = $url->getPath(); + // Check if a key was present, and if so, removing the leading "/" + $result['key'] = !$path || $path == '/' ? null : substr($path, 1); + + return $result; + } +} diff --git a/3rdparty/aws/aws-sdk-php/src/S3/SSECMiddleware.php b/3rdparty/aws/aws-sdk-php/src/S3/SSECMiddleware.php new file mode 100644 index 00000000..628ddef1 --- /dev/null +++ b/3rdparty/aws/aws-sdk-php/src/S3/SSECMiddleware.php @@ -0,0 +1,75 @@ +nextHandler = $nextHandler; + $this->endpointScheme = $endpointScheme; + } + + public function __invoke( + CommandInterface $command, + ?RequestInterface $request = null + ) { + // Allows only HTTPS connections when using SSE-C + if (($command['SSECustomerKey'] || $command['CopySourceSSECustomerKey']) + && $this->endpointScheme !== 'https' + ) { + throw new \RuntimeException('You must configure your S3 client to ' + . 'use HTTPS in order to use the SSE-C features.'); + } + + // Prepare the normal SSE-CPK headers + if ($command['SSECustomerKey']) { + $this->prepareSseParams($command); + } + + // If it's a copy operation, prepare the SSE-CPK headers for the source. + if ($command['CopySourceSSECustomerKey']) { + $this->prepareSseParams($command, 'CopySource'); + } + + $f = $this->nextHandler; + return $f($command, $request); + } + + private function prepareSseParams(CommandInterface $command, $prefix = '') + { + // Base64 encode the provided key + $key = $command[$prefix . 'SSECustomerKey']; + $command[$prefix . 'SSECustomerKey'] = base64_encode($key); + + // Base64 the provided MD5 or, generate an MD5 if not provided + if ($md5 = $command[$prefix . 'SSECustomerKeyMD5']) { + $command[$prefix . 'SSECustomerKeyMD5'] = base64_encode($md5); + } else { + $command[$prefix . 'SSECustomerKeyMD5'] = base64_encode(md5($key, true)); + } + } +} diff --git a/3rdparty/aws/aws-sdk-php/src/S3/StreamWrapper.php b/3rdparty/aws/aws-sdk-php/src/S3/StreamWrapper.php new file mode 100644 index 00000000..47861435 --- /dev/null +++ b/3rdparty/aws/aws-sdk-php/src/S3/StreamWrapper.php @@ -0,0 +1,1005 @@ +/" files with PHP + * streams, supporting "r", "w", "a", "x". + * + * # Opening "r" (read only) streams: + * + * Read only streams are truly streaming by default and will not allow you to + * seek. This is because data read from the stream is not kept in memory or on + * the local filesystem. You can force a "r" stream to be seekable by setting + * the "seekable" stream context option true. This will allow true streaming of + * data from Amazon S3, but will maintain a buffer of previously read bytes in + * a 'php://temp' stream to allow seeking to previously read bytes from the + * stream. + * + * You may pass any GetObject parameters as 's3' stream context options. These + * options will affect how the data is downloaded from Amazon S3. + * + * # Opening "w" and "x" (write only) streams: + * + * Because Amazon S3 requires a Content-Length header, write only streams will + * maintain a 'php://temp' stream to buffer data written to the stream until + * the stream is flushed (usually by closing the stream with fclose). + * + * You may pass any PutObject parameters as 's3' stream context options. These + * options will affect how the data is uploaded to Amazon S3. + * + * When opening an "x" stream, the file must exist on Amazon S3 for the stream + * to open successfully. + * + * # Opening "a" (write only append) streams: + * + * Similar to "w" streams, opening append streams requires that the data be + * buffered in a "php://temp" stream. Append streams will attempt to download + * the contents of an object in Amazon S3, seek to the end of the object, then + * allow you to append to the contents of the object. The data will then be + * uploaded using a PutObject operation when the stream is flushed (usually + * with fclose). + * + * You may pass any GetObject and/or PutObject parameters as 's3' stream + * context options. These options will affect how the data is downloaded and + * uploaded from Amazon S3. + * + * Stream context options: + * + * - "seekable": Set to true to create a seekable "r" (read only) stream by + * using a php://temp stream buffer + * - For "unlink" only: Any option that can be passed to the DeleteObject + * operation + */ +class StreamWrapper +{ + /** @var resource|null Stream context (this is set by PHP) */ + public $context; + + /** @var StreamInterface Underlying stream resource */ + private $body; + + /** @var int Size of the body that is opened */ + private $size; + + /** @var array Hash of opened stream parameters */ + private $params = []; + + /** @var string Mode in which the stream was opened */ + private $mode; + + /** @var \Iterator Iterator used with opendir() related calls */ + private $objectIterator; + + /** @var string The bucket that was opened when opendir() was called */ + private $openedBucket; + + /** @var string The prefix of the bucket that was opened with opendir() */ + private $openedBucketPrefix; + + /** @var string Opened bucket path */ + private $openedPath; + + /** @var CacheInterface Cache for object and dir lookups */ + private $cache; + + /** @var string The opened protocol (e.g., "s3") */ + private $protocol = 's3'; + + /** @var bool Keeps track of whether stream has been flushed since opening */ + private $isFlushed = false; + + /** @var bool Whether or not to use V2 bucket and object existence methods */ + private static $useV2Existence = false; + + /** + * Register the 's3://' stream wrapper + * + * @param S3ClientInterface $client Client to use with the stream wrapper + * @param string $protocol Protocol to register as. + * @param CacheInterface $cache Default cache for the protocol. + */ + public static function register( + S3ClientInterface $client, + $protocol = 's3', + ?CacheInterface $cache = null, + $v2Existence = false + ) { + self::$useV2Existence = $v2Existence; + if (in_array($protocol, stream_get_wrappers())) { + stream_wrapper_unregister($protocol); + } + + // Set the client passed in as the default stream context client + stream_wrapper_register($protocol, get_called_class(), STREAM_IS_URL); + $default = stream_context_get_options(stream_context_get_default()); + $default[$protocol]['client'] = $client; + + if ($cache) { + $default[$protocol]['cache'] = $cache; + } elseif (!isset($default[$protocol]['cache'])) { + // Set a default cache adapter. + $default[$protocol]['cache'] = new LruArrayCache(); + } + + stream_context_set_default($default); + } + + public function stream_close() + { + if (!$this->isFlushed + && empty($this->body->getSize()) + && $this->mode !== 'r' + ) { + $this->stream_flush(); + } + $this->body = $this->cache = null; + } + + public function stream_open($path, $mode, $options, &$opened_path) + { + $this->initProtocol($path); + $this->isFlushed = false; + $this->params = $this->getBucketKey($path); + $this->mode = rtrim($mode, 'bt'); + + if ($errors = $this->validate($path, $this->mode)) { + return $this->triggerError($errors); + } + + return $this->boolCall(function() { + switch ($this->mode) { + case 'r': return $this->openReadStream(); + case 'a': return $this->openAppendStream(); + default: return $this->openWriteStream(); + } + }); + } + + public function stream_eof() + { + return $this->body->eof(); + } + + public function stream_flush() + { + // Check if stream body size has been + // calculated via a flush or close + if($this->body->getSize() === null && $this->mode !== 'r') { + return $this->triggerError( + "Unable to determine stream size. Did you forget to close or flush the stream?" + ); + } + + $this->isFlushed = true; + if ($this->mode == 'r') { + return false; + } + + if ($this->body->isSeekable()) { + $this->body->seek(0); + } + $params = $this->getOptions(true); + $params['Body'] = $this->body; + + // Attempt to guess the ContentType of the upload based on the + // file extension of the key + if (!isset($params['ContentType']) && + ($type = Psr7\MimeType::fromFilename($params['Key'])) + ) { + $params['ContentType'] = $type; + } + + $this->clearCacheKey("{$this->protocol}://{$params['Bucket']}/{$params['Key']}"); + return $this->boolCall(function () use ($params) { + return (bool) $this->getClient()->putObject($params); + }); + } + + public function stream_read($count) + { + return $this->body->read($count); + } + + public function stream_seek($offset, $whence = SEEK_SET) + { + return !$this->body->isSeekable() + ? false + : $this->boolCall(function () use ($offset, $whence) { + $this->body->seek($offset, $whence); + return true; + }); + } + + public function stream_tell() + { + return $this->boolCall(function() { return $this->body->tell(); }); + } + + public function stream_write($data) + { + return $this->body->write($data); + } + + public function unlink($path) + { + $this->initProtocol($path); + + return $this->boolCall(function () use ($path) { + $this->clearCacheKey($path); + $this->getClient()->deleteObject($this->withPath($path)); + return true; + }); + } + + public function stream_stat() + { + $stat = $this->getStatTemplate(); + $stat[7] = $stat['size'] = $this->getSize(); + $stat[2] = $stat['mode'] = $this->mode; + + return $stat; + } + + /** + * Provides information for is_dir, is_file, filesize, etc. Works on + * buckets, keys, and prefixes. + * @link http://www.php.net/manual/en/streamwrapper.url-stat.php + */ + public function url_stat($path, $flags) + { + $this->initProtocol($path); + + // Some paths come through as S3:// for some reason. + $split = explode('://', $path); + $path = strtolower($split[0]) . '://' . $split[1]; + + // Check if this path is in the url_stat cache + if ($value = $this->getCacheStorage()->get($path)) { + return $value; + } + + $stat = $this->createStat($path, $flags); + + if (is_array($stat)) { + $this->getCacheStorage()->set($path, $stat); + } + + return $stat; + } + + /** + * Parse the protocol out of the given path. + * + * @param $path + */ + private function initProtocol($path) + { + $parts = explode('://', $path, 2); + $this->protocol = $parts[0] ?: 's3'; + } + + private function createStat($path, $flags) + { + $this->initProtocol($path); + $parts = $this->withPath($path); + + if (!$parts['Key']) { + return $this->statDirectory($parts, $path, $flags); + } + + return $this->boolCall(function () use ($parts, $path) { + try { + $result = $this->getClient()->headObject($parts); + if (substr($parts['Key'], -1, 1) == '/' && + $result['ContentLength'] == 0 + ) { + // Return as if it is a bucket to account for console + // bucket objects (e.g., zero-byte object "foo/") + return $this->formatUrlStat($path); + } + + // Attempt to stat and cache regular object + return $this->formatUrlStat($result->toArray()); + } catch (S3Exception $e) { + // Maybe this isn't an actual key, but a prefix. Do a prefix + // listing of objects to determine. + $result = $this->getClient()->listObjects([ + 'Bucket' => $parts['Bucket'], + 'Prefix' => rtrim($parts['Key'], '/') . '/', + 'MaxKeys' => 1 + ]); + if (!$result['Contents'] && !$result['CommonPrefixes']) { + throw new \Exception("File or directory not found: $path"); + } + return $this->formatUrlStat($path); + } + }, $flags); + } + + private function statDirectory($parts, $path, $flags) + { + // Stat "directories": buckets, or "s3://" + $method = self::$useV2Existence ? 'doesBucketExistV2' : 'doesBucketExist'; + + if (!$parts['Bucket'] || + $this->getClient()->$method($parts['Bucket']) + ) { + return $this->formatUrlStat($path); + } + + return $this->triggerError("File or directory not found: $path", $flags); + } + + /** + * Support for mkdir(). + * + * @param string $path Directory which should be created. + * @param int $mode Permissions. 700-range permissions map to + * ACL_PUBLIC. 600-range permissions map to + * ACL_AUTH_READ. All other permissions map to + * ACL_PRIVATE. Expects octal form. + * @param int $options A bitwise mask of values, such as + * STREAM_MKDIR_RECURSIVE. + * + * @return bool + * @link http://www.php.net/manual/en/streamwrapper.mkdir.php + */ + public function mkdir($path, $mode, $options) + { + $this->initProtocol($path); + $params = $this->withPath($path); + $this->clearCacheKey($path); + if (!$params['Bucket']) { + return false; + } + + if (!isset($params['ACL'])) { + $params['ACL'] = $this->determineAcl($mode); + } + + return empty($params['Key']) + ? $this->createBucket($path, $params) + : $this->createSubfolder($path, $params); + } + + public function rmdir($path, $options) + { + $this->initProtocol($path); + $this->clearCacheKey($path); + $params = $this->withPath($path); + $client = $this->getClient(); + + if (!$params['Bucket']) { + return $this->triggerError('You must specify a bucket'); + } + + return $this->boolCall(function () use ($params, $path, $client) { + if (!$params['Key']) { + $client->deleteBucket(['Bucket' => $params['Bucket']]); + return true; + } + return $this->deleteSubfolder($path, $params); + }); + } + + /** + * Support for opendir(). + * + * The opendir() method of the Amazon S3 stream wrapper supports a stream + * context option of "listFilter". listFilter must be a callable that + * accepts an associative array of object data and returns true if the + * object should be yielded when iterating the keys in a bucket. + * + * @param string $path The path to the directory + * (e.g. "s3://dir[]") + * @param string $options Unused option variable + * + * @return bool true on success + * @see http://www.php.net/manual/en/function.opendir.php + */ + public function dir_opendir($path, $options) + { + $this->initProtocol($path); + $this->openedPath = $path; + $params = $this->withPath($path); + $delimiter = $this->getOption('delimiter'); + /** @var callable $filterFn */ + $filterFn = $this->getOption('listFilter'); + $op = ['Bucket' => $params['Bucket']]; + $this->openedBucket = $params['Bucket']; + + if ($delimiter === null) { + $delimiter = '/'; + } + + if ($delimiter) { + $op['Delimiter'] = $delimiter; + } + + if ($params['Key']) { + $params['Key'] = rtrim($params['Key'], $delimiter) . $delimiter; + $op['Prefix'] = $params['Key']; + } + + $this->openedBucketPrefix = $params['Key']; + + // Filter our "/" keys added by the console as directories, and ensure + // that if a filter function is provided that it passes the filter. + $this->objectIterator = \Aws\flatmap( + $this->getClient()->getPaginator('ListObjects', $op), + function (Result $result) use ($filterFn) { + $contentsAndPrefixes = $result->search('[Contents[], CommonPrefixes[]][]'); + // Filter out dir place holder keys and use the filter fn. + return array_filter( + $contentsAndPrefixes, + function ($key) use ($filterFn) { + return (!$filterFn || call_user_func($filterFn, $key)) + && (!isset($key['Key']) || substr($key['Key'], -1, 1) !== '/'); + } + ); + } + ); + + return true; + } + + /** + * Close the directory listing handles + * + * @return bool true on success + */ + public function dir_closedir() + { + $this->objectIterator = null; + gc_collect_cycles(); + + return true; + } + + /** + * This method is called in response to rewinddir() + * + * @return boolean true on success + */ + public function dir_rewinddir() + { + return $this->boolCall(function() { + $this->objectIterator = null; + $this->dir_opendir($this->openedPath, null); + return true; + }); + } + + /** + * This method is called in response to readdir() + * + * @return string Should return a string representing the next filename, or + * false if there is no next file. + * @link http://www.php.net/manual/en/function.readdir.php + */ + public function dir_readdir() + { + // Skip empty result keys + if (!$this->objectIterator->valid()) { + return false; + } + + // First we need to create a cache key. This key is the full path to + // then object in s3: protocol://bucket/key. + // Next we need to create a result value. The result value is the + // current value of the iterator without the opened bucket prefix to + // emulate how readdir() works on directories. + // The cache key and result value will depend on if this is a prefix + // or a key. + $cur = $this->objectIterator->current(); + if (isset($cur['Prefix'])) { + // Include "directories". Be sure to strip a trailing "/" + // on prefixes. + $result = rtrim($cur['Prefix'], '/'); + $key = $this->formatKey($result); + $stat = $this->formatUrlStat($key); + } else { + $result = $cur['Key']; + $key = $this->formatKey($cur['Key']); + $stat = $this->formatUrlStat($cur); + } + + // Cache the object data for quick url_stat lookups used with + // RecursiveDirectoryIterator. + $this->getCacheStorage()->set($key, $stat); + $this->objectIterator->next(); + + // Remove the prefix from the result to emulate other stream wrappers. + return $this->openedBucketPrefix + ? substr($result, strlen($this->openedBucketPrefix)) + : $result; + } + + private function formatKey($key) + { + $protocol = explode('://', $this->openedPath)[0]; + return "{$protocol}://{$this->openedBucket}/{$key}"; + } + + /** + * Called in response to rename() to rename a file or directory. Currently + * only supports renaming objects. + * + * @param string $path_from the path to the file to rename + * @param string $path_to the new path to the file + * + * @return bool true if file was successfully renamed + * @link http://www.php.net/manual/en/function.rename.php + */ + public function rename($path_from, $path_to) + { + // PHP will not allow rename across wrapper types, so we can safely + // assume $path_from and $path_to have the same protocol + $this->initProtocol($path_from); + $partsFrom = $this->withPath($path_from); + $partsTo = $this->withPath($path_to); + $this->clearCacheKey($path_from); + $this->clearCacheKey($path_to); + + if (!$partsFrom['Key'] || !$partsTo['Key']) { + return $this->triggerError('The Amazon S3 stream wrapper only ' + . 'supports copying objects'); + } + + return $this->boolCall(function () use ($partsFrom, $partsTo) { + $options = $this->getOptions(true); + // Copy the object and allow overriding default parameters if + // desired, but by default copy metadata + $this->getClient()->copy( + $partsFrom['Bucket'], + $partsFrom['Key'], + $partsTo['Bucket'], + $partsTo['Key'], + isset($options['acl']) ? $options['acl'] : 'private', + $options + ); + // Delete the original object + $this->getClient()->deleteObject([ + 'Bucket' => $partsFrom['Bucket'], + 'Key' => $partsFrom['Key'] + ] + $options); + return true; + }); + } + + public function stream_cast($cast_as) + { + return false; + } + + public function stream_set_option($option, $arg1, $arg2) + { + return false; + } + + public function stream_metadata($path, $option, $value) + { + return false; + } + + public function stream_lock($operation) + { + trigger_error( + 'stream_lock() is not supported by the Amazon S3 stream wrapper', + E_USER_WARNING + ); + return false; + } + + public function stream_truncate($new_size) + { + return false; + } + + /** + * Validates the provided stream arguments for fopen and returns an array + * of errors. + */ + private function validate($path, $mode) + { + $errors = []; + + if (!$this->getOption('Key')) { + $errors[] = 'Cannot open a bucket. You must specify a path in the ' + . 'form of s3://bucket/key'; + } + + if (!in_array($mode, ['r', 'w', 'a', 'x'])) { + $errors[] = "Mode not supported: {$mode}. " + . "Use one 'r', 'w', 'a', or 'x'."; + } + + if ($mode === 'x') { + $method = self::$useV2Existence ? 'doesObjectExistV2' : 'doesObjectExist'; + + if ($this->getClient()->$method( + $this->getOption('Bucket'), + $this->getOption('Key'), + $this->getOptions(true) + )) { + $errors[] = "{$path} already exists on Amazon S3"; + } + } + + return $errors; + } + + /** + * Get the stream context options available to the current stream + * + * @param bool $removeContextData Set to true to remove contextual kvp's + * like 'client' from the result. + * + * @return array + */ + private function getOptions($removeContextData = false) + { + // Context is not set when doing things like stat + if ($this->context === null) { + $options = []; + } else { + $options = stream_context_get_options($this->context); + $options = isset($options[$this->protocol]) + ? $options[$this->protocol] + : []; + } + + $default = stream_context_get_options(stream_context_get_default()); + $default = isset($default[$this->protocol]) + ? $default[$this->protocol] + : []; + $result = $this->params + $options + $default; + + if ($removeContextData) { + unset($result['client'], $result['seekable'], $result['cache']); + } + + return $result; + } + + /** + * Get a specific stream context option + * + * @param string $name Name of the option to retrieve + * + * @return mixed|null + */ + private function getOption($name) + { + $options = $this->getOptions(); + + return isset($options[$name]) ? $options[$name] : null; + } + + /** + * Gets the client from the stream context + * + * @return S3ClientInterface + * @throws \RuntimeException if no client has been configured + */ + private function getClient() + { + if (!$client = $this->getOption('client')) { + throw new \RuntimeException('No client in stream context'); + } + + return $client; + } + + private function getBucketKey($path) + { + // Remove the protocol + $parts = explode('://', $path); + // Get the bucket, key + $parts = explode('/', $parts[1], 2); + + return [ + 'Bucket' => $parts[0], + 'Key' => isset($parts[1]) ? $parts[1] : null + ]; + } + + /** + * Get the bucket and key from the passed path (e.g. s3://bucket/key) + * + * @param string $path Path passed to the stream wrapper + * + * @return array Hash of 'Bucket', 'Key', and custom params from the context + */ + private function withPath($path) + { + $params = $this->getOptions(true); + + return $this->getBucketKey($path) + $params; + } + + private function openReadStream() + { + $client = $this->getClient(); + $command = $client->getCommand('GetObject', $this->getOptions(true)); + $command['@http']['stream'] = true; + $result = $client->execute($command); + $this->size = $result['ContentLength']; + $this->body = $result['Body']; + + // Wrap the body in a caching entity body if seeking is allowed + if ($this->getOption('seekable') && !$this->body->isSeekable()) { + $this->body = new CachingStream($this->body); + } + + return true; + } + + private function openWriteStream() + { + $this->body = new Stream(fopen('php://temp', 'r+')); + return true; + } + + private function openAppendStream() + { + try { + // Get the body of the object and seek to the end of the stream + $client = $this->getClient(); + $this->body = $client->getObject($this->getOptions(true))['Body']; + $this->body->seek(0, SEEK_END); + return true; + } catch (S3Exception $e) { + // The object does not exist, so use a simple write stream + return $this->openWriteStream(); + } + } + + /** + * Trigger one or more errors + * + * @param string|array $errors Errors to trigger + * @param mixed $flags If set to STREAM_URL_STAT_QUIET, then no + * error or exception occurs + * + * @return bool Returns false + * @throws \RuntimeException if throw_errors is true + */ + private function triggerError($errors, $flags = null) + { + // This is triggered with things like file_exists() + if ($flags & STREAM_URL_STAT_QUIET) { + return $flags & STREAM_URL_STAT_LINK + // This is triggered for things like is_link() + ? $this->formatUrlStat(false) + : false; + } + + // This is triggered when doing things like lstat() or stat() + trigger_error(implode("\n", (array) $errors), E_USER_WARNING); + + return false; + } + + /** + * Prepare a url_stat result array + * + * @param string|array $result Data to add + * + * @return array Returns the modified url_stat result + */ + private function formatUrlStat($result = null) + { + $stat = $this->getStatTemplate(); + switch (gettype($result)) { + case 'NULL': + case 'string': + // Directory with 0777 access - see "man 2 stat". + $stat['mode'] = $stat[2] = 0040777; + break; + case 'array': + // Regular file with 0777 access - see "man 2 stat". + $stat['mode'] = $stat[2] = 0100777; + // Pluck the content-length if available. + if (isset($result['ContentLength'])) { + $stat['size'] = $stat[7] = $result['ContentLength']; + } elseif (isset($result['Size'])) { + $stat['size'] = $stat[7] = $result['Size']; + } + if (isset($result['LastModified'])) { + // ListObjects or HeadObject result + $stat['mtime'] = $stat[9] = $stat['ctime'] = $stat[10] + = strtotime($result['LastModified']); + } + } + + return $stat; + } + + /** + * Creates a bucket for the given parameters. + * + * @param string $path Stream wrapper path + * @param array $params A result of StreamWrapper::withPath() + * + * @return bool Returns true on success or false on failure + */ + private function createBucket($path, array $params) + { + $method = self::$useV2Existence ? 'doesBucketExistV2' : 'doesBucketExist'; + + if ($this->getClient()->$method($params['Bucket'])) { + return $this->triggerError("Bucket already exists: {$path}"); + } + + unset($params['ACL']); + return $this->boolCall(function () use ($params, $path) { + $this->getClient()->createBucket($params); + $this->clearCacheKey($path); + return true; + }); + } + + /** + * Creates a pseudo-folder by creating an empty "/" suffixed key + * + * @param string $path Stream wrapper path + * @param array $params A result of StreamWrapper::withPath() + * + * @return bool + */ + private function createSubfolder($path, array $params) + { + // Ensure the path ends in "/" and the body is empty. + $params['Key'] = rtrim($params['Key'], '/') . '/'; + $params['Body'] = ''; + + // Fail if this pseudo directory key already exists + $method = self::$useV2Existence ? 'doesObjectExistV2' : 'doesObjectExist'; + + if ($this->getClient()->$method( + $params['Bucket'], + $params['Key'] + )) { + return $this->triggerError("Subfolder already exists: {$path}"); + } + + return $this->boolCall(function () use ($params, $path) { + $this->getClient()->putObject($params); + $this->clearCacheKey($path); + return true; + }); + } + + /** + * Deletes a nested subfolder if it is empty. + * + * @param string $path Path that is being deleted (e.g., 's3://a/b/c') + * @param array $params A result of StreamWrapper::withPath() + * + * @return bool + */ + private function deleteSubfolder($path, $params) + { + // Use a key that adds a trailing slash if needed. + $prefix = rtrim($params['Key'], '/') . '/'; + $result = $this->getClient()->listObjects([ + 'Bucket' => $params['Bucket'], + 'Prefix' => $prefix, + 'MaxKeys' => 1 + ]); + + // Check if the bucket contains keys other than the placeholder + if ($contents = $result['Contents']) { + return (count($contents) > 1 || $contents[0]['Key'] != $prefix) + ? $this->triggerError('Subfolder is not empty') + : $this->unlink(rtrim($path, '/') . '/'); + } + + return $result['CommonPrefixes'] + ? $this->triggerError('Subfolder contains nested folders') + : true; + } + + /** + * Determine the most appropriate ACL based on a file mode. + * + * @param int $mode File mode + * + * @return string + */ + private function determineAcl($mode) + { + switch (substr(decoct($mode), 0, 1)) { + case '7': return 'public-read'; + case '6': return 'authenticated-read'; + default: return 'private'; + } + } + + /** + * Gets a URL stat template with default values + * + * @return array + */ + private function getStatTemplate() + { + return [ + 0 => 0, 'dev' => 0, + 1 => 0, 'ino' => 0, + 2 => 0, 'mode' => 0, + 3 => 0, 'nlink' => 0, + 4 => 0, 'uid' => 0, + 5 => 0, 'gid' => 0, + 6 => -1, 'rdev' => -1, + 7 => 0, 'size' => 0, + 8 => 0, 'atime' => 0, + 9 => 0, 'mtime' => 0, + 10 => 0, 'ctime' => 0, + 11 => -1, 'blksize' => -1, + 12 => -1, 'blocks' => -1, + ]; + } + + /** + * Invokes a callable and triggers an error if an exception occurs while + * calling the function. + * + * @param callable $fn + * @param int $flags + * + * @return bool + */ + private function boolCall(callable $fn, $flags = null) + { + try { + return $fn(); + } catch (\Exception $e) { + return $this->triggerError($e->getMessage(), $flags); + } + } + + /** + * @return LruArrayCache + */ + private function getCacheStorage() + { + if (!$this->cache) { + $this->cache = $this->getOption('cache') ?: new LruArrayCache(); + } + + return $this->cache; + } + + /** + * Clears a specific stat cache value from the stat cache and LRU cache. + * + * @param string $key S3 path (s3://bucket/key). + */ + private function clearCacheKey($key) + { + clearstatcache(true, $key); + $this->getCacheStorage()->remove($key); + } + + /** + * Returns the size of the opened object body. + * + * @return int|null + */ + private function getSize() + { + $size = $this->body->getSize(); + + return !empty($size) ? $size : $this->size; + } +} diff --git a/3rdparty/aws/aws-sdk-php/src/S3/Transfer.php b/3rdparty/aws/aws-sdk-php/src/S3/Transfer.php new file mode 100644 index 00000000..7679950b --- /dev/null +++ b/3rdparty/aws/aws-sdk-php/src/S3/Transfer.php @@ -0,0 +1,456 @@ +client = $client; + + // Prepare the destination. + $this->destination = $this->prepareTarget($dest); + if ($this->destination['scheme'] === 's3') { + $this->s3Args = $this->getS3Args($this->destination['path']); + } + + // Prepare the source. + if (is_string($source)) { + $this->sourceMetadata = $this->prepareTarget($source); + $this->source = $source; + } elseif ($source instanceof Iterator) { + if (empty($options['base_dir'])) { + throw new \InvalidArgumentException('You must provide the source' + . ' argument as a string or provide the "base_dir" option.'); + } + + $this->sourceMetadata = $this->prepareTarget($options['base_dir']); + $this->source = $source; + } else { + throw new \InvalidArgumentException('source must be the path to a ' + . 'directory or an iterator that yields file names.'); + } + + // Validate schemes. + if ($this->sourceMetadata['scheme'] === $this->destination['scheme']) { + throw new \InvalidArgumentException("You cannot copy from" + . " {$this->sourceMetadata['scheme']} to" + . " {$this->destination['scheme']}." + ); + } + + // Handle multipart-related options. + $this->concurrency = isset($options['concurrency']) + ? $options['concurrency'] + : MultipartUploader::DEFAULT_CONCURRENCY; + $this->mupThreshold = isset($options['mup_threshold']) + ? $options['mup_threshold'] + : 16777216; + if ($this->mupThreshold < MultipartUploader::PART_MIN_SIZE) { + throw new \InvalidArgumentException('mup_threshold must be >= 5MB'); + } + + // Handle "before" callback option. + if (isset($options['before'])) { + $this->before = $options['before']; + if (!is_callable($this->before)) { + throw new \InvalidArgumentException('before must be a callable.'); + } + } + + // Handle "after" callback option. + if (isset($options['after'])) { + $this->after = $options['after']; + if (!is_callable($this->after)) { + throw new \InvalidArgumentException('after must be a callable.'); + } + } + + // Handle "debug" option. + if (isset($options['debug'])) { + if ($options['debug'] === true) { + $options['debug'] = fopen('php://output', 'w'); + } + if (is_resource($options['debug'])) { + $this->addDebugToBefore($options['debug']); + } + } + + // Handle "add_content_md5" option. + $this->addContentMD5 = isset($options['add_content_md5']) + && $options['add_content_md5'] === true; + MetricsBuilder::appendMetricsCaptureMiddleware( + $this->client->getHandlerList(), + MetricsBuilder::S3_TRANSFER + ); + } + + /** + * Transfers the files. + * + * @return PromiseInterface + */ + public function promise(): PromiseInterface + { + // If the promise has been created, just return it. + if (!$this->promise) { + // Create an upload/download promise for the transfer. + $this->promise = $this->sourceMetadata['scheme'] === 'file' + ? $this->createUploadPromise() + : $this->createDownloadPromise(); + } + + return $this->promise; + } + + /** + * Transfers the files synchronously. + */ + public function transfer() + { + $this->promise()->wait(); + } + + private function prepareTarget($targetPath) + { + $target = [ + 'path' => $this->normalizePath($targetPath), + 'scheme' => $this->determineScheme($targetPath), + ]; + + if ($target['scheme'] !== 's3' && $target['scheme'] !== 'file') { + throw new \InvalidArgumentException('Scheme must be "s3" or "file".'); + } + + return $target; + } + + /** + * Creates an array that contains Bucket and Key by parsing the filename. + * + * @param string $path Path to parse. + * + * @return array + */ + private function getS3Args($path) + { + $parts = explode('/', str_replace('s3://', '', $path), 2); + $args = ['Bucket' => $parts[0]]; + if (isset($parts[1])) { + $args['Key'] = $parts[1]; + } + + return $args; + } + + /** + * Parses the scheme from a filename. + * + * @param string $path Path to parse. + * + * @return string + */ + private function determineScheme($path) + { + return !strpos($path, '://') ? 'file' : explode('://', $path)[0]; + } + + /** + * Normalize a path so that it has UNIX-style directory separators and no trailing / + * + * @param string $path + * + * @return string + */ + private function normalizePath($path) + { + return rtrim(str_replace('\\', '/', $path), '/'); + } + + private function resolvesOutsideTargetDirectory($sink, $objectKey) + { + $resolved = []; + $sections = explode('/', $sink); + $targetSectionsLength = count(explode('/', $objectKey)); + $targetSections = array_slice($sections, -($targetSectionsLength + 1)); + $targetDirectory = $targetSections[0]; + + foreach ($targetSections as $section) { + if ($section === '.' || $section === '') { + continue; + } + if ($section === '..') { + array_pop($resolved); + if (empty($resolved) || $resolved[0] !== $targetDirectory) { + return true; + } + } else { + $resolved []= $section; + } + } + return false; + } + + private function createDownloadPromise() + { + $parts = $this->getS3Args($this->sourceMetadata['path']); + $prefix = "s3://{$parts['Bucket']}/" + . (isset($parts['Key']) ? $parts['Key'] . '/' : ''); + + $commands = []; + foreach ($this->getDownloadsIterator() as $object) { + // Prepare the sink. + $objectKey = preg_replace('/^' . preg_quote($prefix, '/') . '/', '', $object); + $sink = $this->destination['path'] . '/' . $objectKey; + + $command = $this->client->getCommand( + 'GetObject', + $this->getS3Args($object) + ['@http' => ['sink' => $sink]] + ); + + if ($this->resolvesOutsideTargetDirectory($sink, $objectKey)) { + throw new AwsException( + 'Cannot download key ' . $objectKey + . ', its relative path resolves outside the' + . ' parent directory', $command); + } + + // Create the directory if needed. + $dir = dirname($sink); + if (!is_dir($dir) && !mkdir($dir, 0777, true)) { + throw new \RuntimeException("Could not create dir: {$dir}"); + } + + // Create the command. + $commands []= $command; + } + + // Create a GetObject command pool and return the promise. + return (new Aws\CommandPool($this->client, $commands, [ + 'concurrency' => $this->concurrency, + 'before' => $this->before, + 'fulfill' => $this->after, + 'rejected' => function ($reason, $idx, Promise\PromiseInterface $p) { + $p->reject($reason); + } + ]))->promise(); + } + + private function createUploadPromise() + { + // Map each file into a promise that performs the actual transfer. + $files = \Aws\map($this->getUploadsIterator(), function ($file) { + return (filesize($file) >= $this->mupThreshold) + ? $this->uploadMultipart($file) + : $this->upload($file); + }); + + // Create an EachPromise, that will concurrently handle the upload + // operations' yielded promises from the iterator. + return Promise\Each::ofLimitAll($files, $this->concurrency, $this->after); + } + + /** @return Iterator */ + private function getUploadsIterator() + { + if (is_string($this->source)) { + return Aws\filter( + Aws\recursive_dir_iterator($this->sourceMetadata['path']), + function ($file) { return !is_dir($file); } + ); + } + + return $this->source; + } + + /** @return Iterator */ + private function getDownloadsIterator() + { + if (is_string($this->source)) { + $listArgs = $this->getS3Args($this->sourceMetadata['path']); + if (isset($listArgs['Key'])) { + $listArgs['Prefix'] = $listArgs['Key'] . '/'; + unset($listArgs['Key']); + } + + $files = $this->client + ->getPaginator('ListObjects', $listArgs) + ->search('Contents[].Key'); + $files = Aws\map($files, function ($key) use ($listArgs) { + return "s3://{$listArgs['Bucket']}/$key"; + }); + return Aws\filter($files, function ($key) { + return substr($key, -1, 1) !== '/'; + }); + } + + return $this->source; + } + + private function upload($filename) + { + $args = $this->s3Args; + $args['SourceFile'] = $filename; + $args['Key'] = $this->createS3Key($filename); + $args['AddContentMD5'] = $this->addContentMD5; + $command = $this->client->getCommand('PutObject', $args); + $this->before and call_user_func($this->before, $command); + + return $this->client->executeAsync($command); + } + + private function uploadMultipart($filename) + { + $args = $this->s3Args; + $args['Key'] = $this->createS3Key($filename); + $filename = $filename instanceof \SplFileInfo ? $filename->getPathname() : $filename; + + return (new MultipartUploader($this->client, $filename, [ + 'bucket' => $args['Bucket'], + 'key' => $args['Key'], + 'before_initiate' => $this->before, + 'before_upload' => $this->before, + 'before_complete' => $this->before, + 'concurrency' => $this->concurrency, + 'add_content_md5' => $this->addContentMD5 + ]))->promise(); + } + + private function createS3Key($filename) + { + $filename = $this->normalizePath($filename); + $relative_file_path = ltrim( + preg_replace('#^' . preg_quote($this->sourceMetadata['path']) . '#', '', $filename), + '/\\' + ); + + if (isset($this->s3Args['Key'])) { + return rtrim($this->s3Args['Key'], '/').'/'.$relative_file_path; + } + + return $relative_file_path; + } + + private function addDebugToBefore($debug) + { + $before = $this->before; + $sourcePath = $this->sourceMetadata['path']; + $s3Args = $this->s3Args; + + $this->before = static function ( + CommandInterface $command + ) use ($before, $debug, $sourcePath, $s3Args) { + // Call the composed before function. + $before and $before($command); + + // Determine the source and dest values based on operation. + switch ($operation = $command->getName()) { + case 'GetObject': + $source = "s3://{$command['Bucket']}/{$command['Key']}"; + $dest = $command['@http']['sink']; + break; + case 'PutObject': + $source = $command['SourceFile']; + $dest = "s3://{$command['Bucket']}/{$command['Key']}"; + break; + case 'UploadPart': + $part = $command['PartNumber']; + case 'CreateMultipartUpload': + case 'CompleteMultipartUpload': + $sourceKey = $command['Key']; + if (isset($s3Args['Key']) && strpos($sourceKey, $s3Args['Key']) === 0) { + $sourceKey = substr($sourceKey, strlen($s3Args['Key']) + 1); + } + $source = "{$sourcePath}/{$sourceKey}"; + $dest = "s3://{$command['Bucket']}/{$command['Key']}"; + break; + default: + throw new \UnexpectedValueException( + "Transfer encountered an unexpected operation: {$operation}." + ); + } + + // Print the debugging message. + $context = sprintf('%s -> %s (%s)', $source, $dest, $operation); + if (isset($part)) { + $context .= " : Part={$part}"; + } + fwrite($debug, "Transferring {$context}\n"); + }; + } +} diff --git a/3rdparty/aws/aws-sdk-php/src/S3/UseArnRegion/Configuration.php b/3rdparty/aws/aws-sdk-php/src/S3/UseArnRegion/Configuration.php new file mode 100644 index 00000000..91277d61 --- /dev/null +++ b/3rdparty/aws/aws-sdk-php/src/S3/UseArnRegion/Configuration.php @@ -0,0 +1,37 @@ +useArnRegion = Aws\boolean_value($useArnRegion); + if (is_null($this->useArnRegion)) { + throw new ConfigurationException("'use_arn_region' config option" + . " must be a boolean value."); + } + } + + /** + * {@inheritdoc} + */ + public function isUseArnRegion() + { + return $this->useArnRegion; + } + + /** + * {@inheritdoc} + */ + public function toArray() + { + return [ + 'use_arn_region' => $this->isUseArnRegion(), + ]; + } +} diff --git a/3rdparty/aws/aws-sdk-php/src/S3/UseArnRegion/ConfigurationInterface.php b/3rdparty/aws/aws-sdk-php/src/S3/UseArnRegion/ConfigurationInterface.php new file mode 100644 index 00000000..c7f3b24d --- /dev/null +++ b/3rdparty/aws/aws-sdk-php/src/S3/UseArnRegion/ConfigurationInterface.php @@ -0,0 +1,19 @@ + + * use Aws\S3\UseArnRegion\ConfigurationProvider; + * $provider = ConfigurationProvider::defaultProvider(); + * // Returns a ConfigurationInterface or throws. + * $config = $provider()->wait(); + * + * + * Configuration providers can be composed to create configuration using + * conditional logic that can create different configurations in different + * environments. You can compose multiple providers into a single provider using + * {@see Aws\S3\UseArnRegion\ConfigurationProvider::chain}. This function + * accepts providers as variadic arguments and returns a new function that will + * invoke each provider until a successful configuration is returned. + * + * + * // First try an INI file at this location. + * $a = ConfigurationProvider::ini(null, '/path/to/file.ini'); + * // Then try an INI file at this location. + * $b = ConfigurationProvider::ini(null, '/path/to/other-file.ini'); + * // Then try loading from environment variables. + * $c = ConfigurationProvider::env(); + * // Combine the three providers together. + * $composed = ConfigurationProvider::chain($a, $b, $c); + * // Returns a promise that is fulfilled with a configuration or throws. + * $promise = $composed(); + * // Wait on the configuration to resolve. + * $config = $promise->wait(); + * + */ +class ConfigurationProvider extends AbstractConfigurationProvider + implements ConfigurationProviderInterface +{ + const ENV_USE_ARN_REGION = 'AWS_S3_USE_ARN_REGION'; + const INI_USE_ARN_REGION = 's3_use_arn_region'; + const DEFAULT_USE_ARN_REGION = true; + + public static $cacheKey = 'aws_s3_use_arn_region_config'; + + protected static $interfaceClass = ConfigurationInterface::class; + protected static $exceptionClass = ConfigurationException::class; + + /** + * Create a default config provider that first checks for environment + * variables, then checks for a specified profile in the environment-defined + * config file location (env variable is 'AWS_CONFIG_FILE', file location + * defaults to ~/.aws/config), then checks for the "default" profile in the + * environment-defined config file location, and failing those uses a default + * fallback set of configuration options. + * + * This provider is automatically wrapped in a memoize function that caches + * previously provided config options. + * + * @param array $config + * + * @return callable + */ + public static function defaultProvider(array $config = []) + { + $configProviders = [self::env()]; + if ( + !isset($config['use_aws_shared_config_files']) + || $config['use_aws_shared_config_files'] != false + ) { + $configProviders[] = self::ini(); + } + $configProviders[] = self::fallback(); + + $memo = self::memoize( + call_user_func_array([ConfigurationProvider::class, 'chain'], $configProviders) + ); + + if (isset($config['use_arn_region']) + && $config['use_arn_region'] instanceof CacheInterface + ) { + return self::cache($memo, $config['use_arn_region'], self::$cacheKey); + } + + return $memo; + } + + /** + * Provider that creates config from environment variables. + * + * @return callable + */ + public static function env() + { + return function () { + // Use config from environment variables, if available + $useArnRegion = getenv(self::ENV_USE_ARN_REGION); + if (!empty($useArnRegion)) { + return Promise\Create::promiseFor( + new Configuration($useArnRegion) + ); + } + + return self::reject('Could not find environment variable config' + . ' in ' . self::ENV_USE_ARN_REGION); + }; + } + + /** + * Config provider that creates config using a config file whose location + * is specified by an environment variable 'AWS_CONFIG_FILE', defaulting to + * ~/.aws/config if not specified + * + * @param string|null $profile Profile to use. If not specified will use + * the "default" profile. + * @param string|null $filename If provided, uses a custom filename rather + * than looking in the default directory. + * + * @return callable + */ + public static function ini($profile = null, $filename = null) + { + $filename = $filename ?: (self::getDefaultConfigFilename()); + $profile = $profile ?: (getenv(self::ENV_PROFILE) ?: 'default'); + + return function () use ($profile, $filename) { + if (!@is_readable($filename)) { + return self::reject("Cannot read configuration from $filename"); + } + + // Use INI_SCANNER_NORMAL instead of INI_SCANNER_TYPED for PHP 5.5 compatibility + $data = \Aws\parse_ini_file($filename, true, INI_SCANNER_NORMAL); + if ($data === false) { + return self::reject("Invalid config file: $filename"); + } + if (!isset($data[$profile])) { + return self::reject("'$profile' not found in config file"); + } + if (!isset($data[$profile][self::INI_USE_ARN_REGION])) { + return self::reject("Required S3 Use Arn Region config values + not present in INI profile '{$profile}' ({$filename})"); + } + + // INI_SCANNER_NORMAL parses false-y values as an empty string + if ($data[$profile][self::INI_USE_ARN_REGION] === "") { + $data[$profile][self::INI_USE_ARN_REGION] = false; + } + + return Promise\Create::promiseFor( + new Configuration($data[$profile][self::INI_USE_ARN_REGION]) + ); + }; + } + + /** + * Fallback config options when other sources are not set. + * + * @return callable + */ + public static function fallback() + { + return function () { + return Promise\Create::promiseFor( + new Configuration(self::DEFAULT_USE_ARN_REGION) + ); + }; + } +} diff --git a/3rdparty/aws/aws-sdk-php/src/S3/UseArnRegion/Exception/ConfigurationException.php b/3rdparty/aws/aws-sdk-php/src/S3/UseArnRegion/Exception/ConfigurationException.php new file mode 100644 index 00000000..15d06a9c --- /dev/null +++ b/3rdparty/aws/aws-sdk-php/src/S3/UseArnRegion/Exception/ConfigurationException.php @@ -0,0 +1,14 @@ +api = $api; + $this->parser = $parser; + } + + public function __invoke( + CommandInterface $command, + ResponseInterface $response + ) { + $fn = $this->parser; + $result = $fn($command, $response); + + //Skip this middleware if the operation doesn't have an httpChecksum + $op = $this->api->getOperation($command->getName()); + $checksumInfo = isset($op['httpChecksum']) + ? $op['httpChecksum'] + : []; + if (empty($checksumInfo)) { + return $result; + } + + //Skip this middleware if the operation doesn't send back a checksum, or the user doesn't opt in + $checksumModeEnabledMember = isset($checksumInfo['requestValidationModeMember']) + ? $checksumInfo['requestValidationModeMember'] + : ""; + $checksumModeEnabled = isset($command[$checksumModeEnabledMember]) + ? $command[$checksumModeEnabledMember] + : ""; + $responseAlgorithms = isset($checksumInfo['responseAlgorithms']) + ? $checksumInfo['responseAlgorithms'] + : []; + if (empty($responseAlgorithms) + || strtolower($checksumModeEnabled) !== "enabled" + ) { + return $result; + } + + if (extension_loaded('awscrt')) { + $checksumPriority = ['CRC32C', 'CRC32', 'SHA1', 'SHA256']; + } else { + $checksumPriority = ['CRC32', 'SHA1', 'SHA256']; + } + $checksumsToCheck = array_intersect($responseAlgorithms, $checksumPriority); + $checksumValidationInfo = $this->validateChecksum($checksumsToCheck, $response); + + if ($checksumValidationInfo['status'] == "SUCCEEDED") { + $result['ChecksumValidated'] = $checksumValidationInfo['checksum']; + } else if ($checksumValidationInfo['status'] == "FAILED"){ + //Ignore failed validations on GetObject if it's a multipart get which returned a full multipart object + if ($command->getName() == "GetObject" + && !empty($checksumValidationInfo['checksumHeaderValue']) + ) { + $headerValue = $checksumValidationInfo['checksumHeaderValue']; + $lastDashPos = strrpos($headerValue, '-'); + $endOfChecksum = substr($headerValue, $lastDashPos + 1); + if (is_numeric($endOfChecksum) + && intval($endOfChecksum) > 1 + && intval($endOfChecksum) < 10000) { + return $result; + } + } + throw new S3Exception( + "Calculated response checksum did not match the expected value", + $command + ); + } + return $result; + } + + public function parseMemberFromStream( + StreamInterface $stream, + StructureShape $member, + $response + ) { + return $this->parser->parseMemberFromStream($stream, $member, $response); + } + + /** + * @param $checksumPriority + * @param ResponseInterface $response + */ + public function validateChecksum($checksumPriority, ResponseInterface $response) + { + $checksumToValidate = $this->chooseChecksumHeaderToValidate( + $checksumPriority, + $response + ); + $validationStatus = "SKIPPED"; + $checksumHeaderValue = null; + if (!empty($checksumToValidate)) { + $checksumHeaderValue = $response->getHeader( + 'x-amz-checksum-' . $checksumToValidate + ); + if (isset($checksumHeaderValue)) { + $checksumHeaderValue = $checksumHeaderValue[0]; + $calculatedChecksumValue = $this->getEncodedValue( + $checksumToValidate, + $response->getBody() + ); + $validationStatus = $checksumHeaderValue == $calculatedChecksumValue + ? "SUCCEEDED" + : "FAILED"; + } + } + return [ + "status" => $validationStatus, + "checksum" => $checksumToValidate, + "checksumHeaderValue" => $checksumHeaderValue, + ]; + } + + /** + * @param $checksumPriority + * @param ResponseInterface $response + */ + public function chooseChecksumHeaderToValidate( + $checksumPriority, + ResponseInterface $response + ) { + foreach ($checksumPriority as $checksum) { + $checksumHeader = 'x-amz-checksum-' . $checksum; + if ($response->hasHeader($checksumHeader)) { + return $checksum; + } + } + return null; + } +} diff --git a/3rdparty/aws/aws-sdk-php/src/SSO/Exception/SSOException.php b/3rdparty/aws/aws-sdk-php/src/SSO/Exception/SSOException.php new file mode 100644 index 00000000..6392a3f0 --- /dev/null +++ b/3rdparty/aws/aws-sdk-php/src/SSO/Exception/SSOException.php @@ -0,0 +1,9 @@ +isDevMode()){ + return; + } + + $composer = $event->getComposer(); + $extra = $composer->getPackage()->getExtra(); + $listedServices = isset($extra['aws/aws-sdk-php']) + ? $extra['aws/aws-sdk-php'] + : []; + + if ($listedServices) { + $serviceMapping = self::buildServiceMapping(); + self::verifyListedServices($serviceMapping, $listedServices); + $filesystem = $filesystem ?: new Filesystem(); + $vendorPath = $composer->getConfig()->get('vendor-dir'); + self::removeServiceDirs( + $event, + $filesystem, + $serviceMapping, + $listedServices, + $vendorPath + ); + } else { + throw new \InvalidArgumentException( + 'There are no services listed. Did you intend to use this script?' + ); + } + } + + public static function buildServiceMapping() + { + $serviceMapping = []; + $manifest = require(__DIR__ . '/../../data/manifest.json.php'); + + foreach ($manifest as $service => $attributes) { + $serviceMapping[$attributes['namespace']] = $service; + } + + return $serviceMapping; + } + + private static function verifyListedServices($serviceMapping, $listedServices) + { + foreach ($listedServices as $serviceToKeep) { + if (!isset($serviceMapping[$serviceToKeep])) { + throw new \InvalidArgumentException( + "'$serviceToKeep' is not a valid AWS service namespace. Please check spelling and casing." + ); + } + } + } + + private static function removeServiceDirs( + $event, + $filesystem, + $serviceMapping, + $listedServices, + $vendorPath + ) { + $unsafeForDeletion = ['Kms', 'S3', 'SSO', 'SSOOIDC', 'Sts']; + if (in_array('DynamoDbStreams', $listedServices)) { + $unsafeForDeletion[] = 'DynamoDb'; + } + + $clientPath = $vendorPath . '/aws/aws-sdk-php/src/'; + $modelPath = $clientPath . 'data/'; + $deleteCount = 0; + + foreach ($serviceMapping as $clientName => $modelName) { + if (!in_array($clientName, $listedServices) && + !in_array($clientName, $unsafeForDeletion) + ) { + $clientDir = $clientPath . $clientName; + $modelDir = $modelPath . $modelName; + + if ($filesystem->exists([$clientDir, $modelDir])) { + $attempts = 3; + $delay = 2; + + while ($attempts) { + try { + $filesystem->remove([$clientDir, $modelDir]); + $deleteCount++; + break; + } catch (IOException $e) { + $attempts--; + + if (!$attempts) { + throw new IOException( + "Removal failed after several attempts. Last error: " . $e->getMessage() + ); + } else { + sleep($delay); + $event->getIO()->write( + "Error encountered: " . $e->getMessage() . ". Retrying..." + ); + $delay += 2; + } + } + } + + } + } + } + $event->getIO()->write( + "Removed $deleteCount AWS service" . ($deleteCount === 1 ? '' : 's') + ); + } +} \ No newline at end of file diff --git a/3rdparty/aws/aws-sdk-php/src/Sdk.php b/3rdparty/aws/aws-sdk-php/src/Sdk.php new file mode 100644 index 00000000..356203d3 --- /dev/null +++ b/3rdparty/aws/aws-sdk-php/src/Sdk.php @@ -0,0 +1,940 @@ +args = $args; + + if (!isset($args['handler']) && !isset($args['http_handler'])) { + $this->args['http_handler'] = default_http_handler(); + } + } + + public function __call($name, array $args) + { + $args = isset($args[0]) ? $args[0] : []; + if (strpos($name, 'createMultiRegion') === 0) { + return $this->createMultiRegionClient(substr($name, 17), $args); + } + + if (strpos($name, 'create') === 0) { + return $this->createClient(substr($name, 6), $args); + } + + throw new \BadMethodCallException("Unknown method: {$name}."); + } + + /** + * Get a client by name using an array of constructor options. + * + * @param string $name Service name or namespace (e.g., DynamoDb, s3). + * @param array $args Arguments to configure the client. + * + * @return AwsClientInterface + * @throws \InvalidArgumentException if any required options are missing or + * the service is not supported. + * @see Aws\AwsClient::__construct for a list of available options for args. + */ + public function createClient($name, array $args = []) + { + // Get information about the service from the manifest file. + $service = manifest($name); + $namespace = $service['namespace']; + + // Instantiate the client class. + $client = "Aws\\{$namespace}\\{$namespace}Client"; + return new $client($this->mergeArgs($namespace, $service, $args)); + } + + public function createMultiRegionClient($name, array $args = []) + { + // Get information about the service from the manifest file. + $service = manifest($name); + $namespace = $service['namespace']; + + $klass = "Aws\\{$namespace}\\{$namespace}MultiRegionClient"; + $klass = class_exists($klass) ? $klass : MultiRegionClient::class; + + return new $klass($this->mergeArgs($namespace, $service, $args)); + } + + /** + * Clone existing SDK instance with ability to pass an associative array + * of extra client settings. + * + * @param array $args + * + * @return self + */ + public function copy(array $args = []) + { + return new self($args + $this->args); + } + + private function mergeArgs($namespace, array $manifest, array $args = []) + { + // Merge provided args with stored, service-specific args. + if (isset($this->args[$namespace])) { + $args += $this->args[$namespace]; + } + + // Provide the endpoint prefix in the args. + if (!isset($args['service'])) { + $args['service'] = $manifest['endpoint']; + } + + return $args + $this->args; + } + + /** + * Determine the endpoint prefix from a client namespace. + * + * @param string $name Namespace name + * + * @return string + * @internal + * @deprecated Use the `\Aws\manifest()` function instead. + */ + public static function getEndpointPrefix($name) + { + return manifest($name)['endpoint']; + } +} diff --git a/3rdparty/aws/aws-sdk-php/src/Signature/AnonymousSignature.php b/3rdparty/aws/aws-sdk-php/src/Signature/AnonymousSignature.php new file mode 100644 index 00000000..9ccc8df6 --- /dev/null +++ b/3rdparty/aws/aws-sdk-php/src/Signature/AnonymousSignature.php @@ -0,0 +1,33 @@ +modifyTokenHeaders($request, $credentials); + $credentials = $this->getSigningCredentials($credentials); + return parent::signRequest($request, $credentials, $signingService); + } + + public function presign(RequestInterface $request, CredentialsInterface $credentials, $expires, array $options = []) + { + $request = $this->modifyTokenHeaders($request, $credentials); + $credentials = $this->getSigningCredentials($credentials); + return parent::presign($request, $credentials, $expires, $options); + } + + private function modifyTokenHeaders( + RequestInterface $request, + CredentialsInterface $credentials + ) { + //The x-amz-security-token header is not supported by s3 express + $request = $request->withoutHeader('X-Amz-Security-Token'); + return $request->withHeader( + 'x-amz-s3session-token', + $credentials->getSecurityToken() + ); + } + + private function getSigningCredentials(CredentialsInterface $credentials) + { + return new Credentials( + $credentials->getAccessKeyId(), + $credentials->getSecretKey() + ); + } +} diff --git a/3rdparty/aws/aws-sdk-php/src/Signature/S3SignatureV4.php b/3rdparty/aws/aws-sdk-php/src/Signature/S3SignatureV4.php new file mode 100644 index 00000000..c99b281c --- /dev/null +++ b/3rdparty/aws/aws-sdk-php/src/Signature/S3SignatureV4.php @@ -0,0 +1,125 @@ +hasHeader('x-amz-content-sha256')) { + $request = $request->withHeader( + 'x-amz-content-sha256', + $this->getPayload($request) + ); + } + $useCrt = + strpos($request->getUri()->getHost(), "accesspoint.s3-global") + !== false; + if (!$useCrt) { + if (strpos($request->getUri()->getHost(), "s3-object-lambda")) { + return parent::signRequest($request, $credentials, "s3-object-lambda"); + } + return parent::signRequest($request, $credentials); + } + $signingService = $signingService ?: 's3'; + return $this->signWithV4a($credentials, $request, $signingService); + } + + /** + * @param CredentialsInterface $credentials + * @param RequestInterface $request + * @param $signingService + * @param SigningConfigAWS|null $signingConfig + * @return RequestInterface + * + * Instantiates a separate sigv4a signing config. All services except S3 + * use double encoding. All services except S3 require path normalization. + */ + protected function signWithV4a( + CredentialsInterface $credentials, + RequestInterface $request, + $signingService, + ?SigningConfigAWS $signingConfig = null + ){ + $this->verifyCRTLoaded(); + $credentials_provider = $this->createCRTStaticCredentialsProvider($credentials); + $signingConfig = new SigningConfigAWS([ + 'algorithm' => SigningAlgorithm::SIGv4_ASYMMETRIC, + 'signature_type' => SignatureType::HTTP_REQUEST_HEADERS, + 'credentials_provider' => $credentials_provider, + 'signed_body_value' => $this->getPayload($request), + 'region' => $this->region, + 'should_normalize_uri_path' => false, + 'use_double_uri_encode' => false, + 'service' => $signingService, + 'date' => time(), + ]); + + return parent::signWithV4a($credentials, $request, $signingService, $signingConfig); + } + + /** + * Always add a x-amz-content-sha-256 for data integrity. + * + * {@inheritdoc} + */ + public function presign( + RequestInterface $request, + CredentialsInterface $credentials, + $expires, + array $options = [] + ) { + if (!$request->hasHeader('x-amz-content-sha256')) { + $request = $request->withHeader( + 'X-Amz-Content-Sha256', + $this->getPresignedPayload($request) + ); + } + + if (strpos($request->getUri()->getHost(), "accesspoint.s3-global")) { + $request = $request->withHeader("x-amz-region-set", "*"); + } + + return parent::presign($request, $credentials, $expires, $options); + } + + /** + * Override used to allow pre-signed URLs to be created for an + * in-determinate request payload. + */ + protected function getPresignedPayload(RequestInterface $request) + { + return SignatureV4::UNSIGNED_PAYLOAD; + } + + /** + * Amazon S3 does not double-encode the path component in the canonical request + */ + protected function createCanonicalizedPath($path) + { + // Only remove one slash in case of keys that have a preceding slash + if (substr($path, 0, 1) === '/') { + $path = substr($path, 1); + } + return '/' . $path; + } +} diff --git a/3rdparty/aws/aws-sdk-php/src/Signature/SignatureInterface.php b/3rdparty/aws/aws-sdk-php/src/Signature/SignatureInterface.php new file mode 100644 index 00000000..cedfc45e --- /dev/null +++ b/3rdparty/aws/aws-sdk-php/src/Signature/SignatureInterface.php @@ -0,0 +1,45 @@ + true, + 's3control' => true, + 's3-outposts' => true, + 's3-object-lambda' => true, + 's3express' => true + ]; + + /** + * Resolves and signature provider and ensures a non-null return value. + * + * @param callable $provider Provider function to invoke. + * @param string $version Signature version. + * @param string $service Service name. + * @param string $region Region name. + * + * @return SignatureInterface + * @throws UnresolvedSignatureException + */ + public static function resolve(callable $provider, $version, $service, $region) + { + $result = $provider($version, $service, $region); + if ($result instanceof SignatureInterface + || $result instanceof BearerTokenAuthorization + ) { + return $result; + } + + throw new UnresolvedSignatureException( + "Unable to resolve a signature for $version/$service/$region.\n" + . "Valid signature versions include v4 and anonymous." + ); + } + + /** + * Default SDK signature provider. + * + * @return callable + */ + public static function defaultProvider() + { + return self::memoize(self::version()); + } + + /** + * Creates a signature provider that caches previously created signature + * objects. The computed cache key is the concatenation of the version, + * service, and region. + * + * @param callable $provider Signature provider to wrap. + * + * @return callable + */ + public static function memoize(callable $provider) + { + $cache = []; + return function ($version, $service, $region) use (&$cache, $provider) { + $key = "($version)($service)($region)"; + if (!isset($cache[$key])) { + $cache[$key] = $provider($version, $service, $region); + } + return $cache[$key]; + }; + } + + /** + * Creates signature objects from known signature versions. + * + * This provider currently recognizes the following signature versions: + * + * - v4: Signature version 4. + * - anonymous: Does not sign requests. + * + * @return callable + */ + public static function version() + { + return function ($version, $service, $region) { + switch ($version) { + case 'v4-s3express': + return new S3ExpressSignature($service, $region); + case 's3v4': + case 'v4': + return !empty(self::$s3v4SignedServices[$service]) + ? new S3SignatureV4($service, $region) + : new SignatureV4($service, $region); + case 'v4a': + return !empty(self::$s3v4SignedServices[$service]) + ? new S3SignatureV4($service, $region, ['use_v4a' => true]) + : new SignatureV4($service, $region, ['use_v4a' => true]); + case 'v4-unsigned-body': + return !empty(self::$s3v4SignedServices[$service]) + ? new S3SignatureV4($service, $region, ['unsigned-body' => 'true']) + : new SignatureV4($service, $region, ['unsigned-body' => 'true']); + case 'bearer': + return new BearerTokenAuthorization(); + case 'anonymous': + return new AnonymousSignature(); + default: + return null; + } + }; + } +} diff --git a/3rdparty/aws/aws-sdk-php/src/Signature/SignatureTrait.php b/3rdparty/aws/aws-sdk-php/src/Signature/SignatureTrait.php new file mode 100644 index 00000000..5dcfe9df --- /dev/null +++ b/3rdparty/aws/aws-sdk-php/src/Signature/SignatureTrait.php @@ -0,0 +1,48 @@ +cache[$k])) { + // Clear the cache when it reaches 50 entries + if (++$this->cacheSize > 50) { + $this->cache = []; + $this->cacheSize = 0; + } + + $dateKey = hash_hmac( + 'sha256', + $shortDate, + "AWS4{$secretKey}", + true + ); + $regionKey = hash_hmac('sha256', $region, $dateKey, true); + $serviceKey = hash_hmac('sha256', $service, $regionKey, true); + $this->cache[$k] = hash_hmac( + 'sha256', + 'aws4_request', + $serviceKey, + true + ); + } + return $this->cache[$k]; + } +} diff --git a/3rdparty/aws/aws-sdk-php/src/Signature/SignatureV4.php b/3rdparty/aws/aws-sdk-php/src/Signature/SignatureV4.php new file mode 100644 index 00000000..74c3940f --- /dev/null +++ b/3rdparty/aws/aws-sdk-php/src/Signature/SignatureV4.php @@ -0,0 +1,584 @@ + true, + 'content-type' => true, + 'content-length' => true, + 'expect' => true, + 'max-forwards' => true, + 'pragma' => true, + 'range' => true, + 'te' => true, + 'if-match' => true, + 'if-none-match' => true, + 'if-modified-since' => true, + 'if-unmodified-since' => true, + 'if-range' => true, + 'accept' => true, + 'authorization' => true, + 'proxy-authorization' => true, + 'from' => true, + 'referer' => true, + 'user-agent' => true, + 'X-Amz-User-Agent' => true, + 'x-amzn-trace-id' => true, + 'aws-sdk-invocation-id' => true, + 'aws-sdk-retry' => true, + ]; + } + + /** + * @param string $service Service name to use when signing + * @param string $region Region name to use when signing + * @param array $options Array of configuration options used when signing + * - unsigned-body: Flag to make request have unsigned payload. + * Unsigned body is used primarily for streaming requests. + */ + public function __construct($service, $region, array $options = []) + { + $this->service = $service; + $this->region = $region; + $this->unsigned = isset($options['unsigned-body']) ? $options['unsigned-body'] : false; + $this->useV4a = isset($options['use_v4a']) && $options['use_v4a'] === true; + } + + /** + * {@inheritdoc} + */ + public function signRequest( + RequestInterface $request, + CredentialsInterface $credentials, + $signingService = null + ) { + $ldt = gmdate(self::ISO8601_BASIC); + $sdt = substr($ldt, 0, 8); + $parsed = $this->parseRequest($request); + $parsed['headers']['X-Amz-Date'] = [$ldt]; + + if ($token = $credentials->getSecurityToken()) { + $parsed['headers']['X-Amz-Security-Token'] = [$token]; + } + $service = isset($signingService) ? $signingService : $this->service; + + if ($this->useV4a) { + return $this->signWithV4a($credentials, $request, $service); + } + + $cs = $this->createScope($sdt, $this->region, $service); + $payload = $this->getPayload($request); + + if ($payload == self::UNSIGNED_PAYLOAD) { + $parsed['headers'][self::AMZ_CONTENT_SHA256_HEADER] = [$payload]; + } + + $context = $this->createContext($parsed, $payload); + $toSign = $this->createStringToSign($ldt, $cs, $context['creq']); + $signingKey = $this->getSigningKey( + $sdt, + $this->region, + $service, + $credentials->getSecretKey() + ); + $signature = hash_hmac('sha256', $toSign, $signingKey); + $parsed['headers']['Authorization'] = [ + "AWS4-HMAC-SHA256 " + . "Credential={$credentials->getAccessKeyId()}/{$cs}, " + . "SignedHeaders={$context['headers']}, Signature={$signature}" + ]; + + return $this->buildRequest($parsed); + } + + /** + * Get the headers that were used to pre-sign the request. + * Used for the X-Amz-SignedHeaders header. + * + * @param array $headers + * @return array + */ + private function getPresignHeaders(array $headers) + { + $presignHeaders = []; + $blacklist = $this->getHeaderBlacklist(); + foreach ($headers as $name => $value) { + $lName = strtolower($name); + if (!isset($blacklist[$lName]) + && $name !== self::AMZ_CONTENT_SHA256_HEADER + ) { + $presignHeaders[] = $lName; + } + } + return $presignHeaders; + } + + /** + * {@inheritdoc} + */ + public function presign( + RequestInterface $request, + CredentialsInterface $credentials, + $expires, + array $options = [] + ) { + $startTimestamp = isset($options['start_time']) + ? $this->convertToTimestamp($options['start_time'], null) + : time(); + $expiresTimestamp = $this->convertToTimestamp($expires, $startTimestamp); + + if ($this->useV4a) { + return $this->presignWithV4a( + $request, + $credentials, + $this->convertExpires($expiresTimestamp, $startTimestamp) + ); + } + + $parsed = $this->createPresignedRequest($request, $credentials); + + $payload = $this->getPresignedPayload($request); + $httpDate = gmdate(self::ISO8601_BASIC, $startTimestamp); + $shortDate = substr($httpDate, 0, 8); + $scope = $this->createScope($shortDate, $this->region, $this->service); + $credential = $credentials->getAccessKeyId() . '/' . $scope; + if ($credentials->getSecurityToken()) { + unset($parsed['headers']['X-Amz-Security-Token']); + } + $parsed['query']['X-Amz-Algorithm'] = 'AWS4-HMAC-SHA256'; + $parsed['query']['X-Amz-Credential'] = $credential; + $parsed['query']['X-Amz-Date'] = gmdate('Ymd\THis\Z', $startTimestamp); + $parsed['query']['X-Amz-SignedHeaders'] = implode(';', $this->getPresignHeaders($parsed['headers'])); + $parsed['query']['X-Amz-Expires'] = $this->convertExpires($expiresTimestamp, $startTimestamp); + $context = $this->createContext($parsed, $payload); + $stringToSign = $this->createStringToSign($httpDate, $scope, $context['creq']); + $key = $this->getSigningKey( + $shortDate, + $this->region, + $this->service, + $credentials->getSecretKey() + ); + $parsed['query']['X-Amz-Signature'] = hash_hmac('sha256', $stringToSign, $key); + + return $this->buildRequest($parsed); + } + + /** + * Converts a POST request to a GET request by moving POST fields into the + * query string. + * + * Useful for pre-signing query protocol requests. + * + * @param RequestInterface $request Request to clone + * + * @return RequestInterface + * @throws \InvalidArgumentException if the method is not POST + */ + public static function convertPostToGet(RequestInterface $request, $additionalQueryParams = "") + { + if ($request->getMethod() !== 'POST') { + throw new \InvalidArgumentException('Expected a POST request but ' + . 'received a ' . $request->getMethod() . ' request.'); + } + + $sr = $request->withMethod('GET') + ->withBody(Psr7\Utils::streamFor('')) + ->withoutHeader('Content-Type') + ->withoutHeader('Content-Length'); + + // Move POST fields to the query if they are present + if ($request->getHeaderLine('Content-Type') === 'application/x-www-form-urlencoded') { + $body = (string) $request->getBody() . $additionalQueryParams; + $sr = $sr->withUri($sr->getUri()->withQuery($body)); + } + + return $sr; + } + + protected function getPayload(RequestInterface $request) + { + if ($this->unsigned && $request->getUri()->getScheme() == 'https') { + return self::UNSIGNED_PAYLOAD; + } + // Calculate the request signature payload + if ($request->hasHeader(self::AMZ_CONTENT_SHA256_HEADER)) { + // Handle streaming operations (e.g. Glacier.UploadArchive) + return $request->getHeaderLine(self::AMZ_CONTENT_SHA256_HEADER); + } + + if (!$request->getBody()->isSeekable()) { + throw new CouldNotCreateChecksumException('sha256'); + } + + try { + return Psr7\Utils::hash($request->getBody(), 'sha256'); + } catch (\Exception $e) { + throw new CouldNotCreateChecksumException('sha256', $e); + } + } + + protected function getPresignedPayload(RequestInterface $request) + { + return $this->getPayload($request); + } + + protected function createCanonicalizedPath($path) + { + $doubleEncoded = rawurlencode(ltrim($path, '/')); + + return '/' . str_replace('%2F', '/', $doubleEncoded); + } + + private function createStringToSign($longDate, $credentialScope, $creq) + { + $hash = hash('sha256', $creq); + + return "AWS4-HMAC-SHA256\n{$longDate}\n{$credentialScope}\n{$hash}"; + } + + private function createPresignedRequest( + RequestInterface $request, + CredentialsInterface $credentials + ) { + $parsedRequest = $this->parseRequest($request); + + // Make sure to handle temporary credentials + if ($token = $credentials->getSecurityToken()) { + $parsedRequest['headers']['X-Amz-Security-Token'] = [$token]; + } + + return $this->moveHeadersToQuery($parsedRequest); + } + + /** + * @param array $parsedRequest + * @param string $payload Hash of the request payload + * @return array Returns an array of context information + */ + private function createContext(array $parsedRequest, $payload) + { + $blacklist = $this->getHeaderBlacklist(); + + // Normalize the path as required by SigV4 + $canon = $parsedRequest['method'] . "\n" + . $this->createCanonicalizedPath($parsedRequest['path']) . "\n" + . $this->getCanonicalizedQuery($parsedRequest['query']) . "\n"; + + // Case-insensitively aggregate all of the headers. + $aggregate = []; + foreach ($parsedRequest['headers'] as $key => $values) { + $key = strtolower($key); + if (!isset($blacklist[$key])) { + foreach ($values as $v) { + $aggregate[$key][] = $v; + } + } + } + + ksort($aggregate); + $canonHeaders = []; + foreach ($aggregate as $k => $v) { + if (count($v) > 0) { + sort($v); + } + $canonHeaders[] = $k . ':' . preg_replace('/\s+/', ' ', implode(',', $v)); + } + + $signedHeadersString = implode(';', array_keys($aggregate)); + $canon .= implode("\n", $canonHeaders) . "\n\n" + . $signedHeadersString . "\n" + . $payload; + + return ['creq' => $canon, 'headers' => $signedHeadersString]; + } + + private function getCanonicalizedQuery(array $query) + { + unset($query['X-Amz-Signature']); + + if (!$query) { + return ''; + } + + $qs = ''; + ksort($query); + foreach ($query as $k => $v) { + if (!is_array($v)) { + $qs .= rawurlencode($k) . '=' . rawurlencode($v !== null ? $v : '') . '&'; + } else { + sort($v); + foreach ($v as $value) { + $qs .= rawurlencode($k) . '=' . rawurlencode($value !== null ? $value : '') . '&'; + } + } + } + + return substr($qs, 0, -1); + } + + private function convertToTimestamp($dateValue, $relativeTimeBase = null) + { + if ($dateValue instanceof \DateTimeInterface) { + $timestamp = $dateValue->getTimestamp(); + } elseif (!is_numeric($dateValue)) { + $timestamp = strtotime($dateValue, + $relativeTimeBase === null ? time() : $relativeTimeBase + ); + } else { + $timestamp = $dateValue; + } + + return $timestamp; + } + + private function convertExpires($expiresTimestamp, $startTimestamp) + { + $duration = $expiresTimestamp - $startTimestamp; + + // Ensure that the duration of the signature is not longer than a week + if ($duration > 604800) { + throw new \InvalidArgumentException('The expiration date of a ' + . 'signature version 4 presigned URL must be less than one ' + . 'week'); + } + + return $duration; + } + + private function moveHeadersToQuery(array $parsedRequest) + { + //x-amz-user-agent shouldn't be put in a query param + unset($parsedRequest['headers']['X-Amz-User-Agent']); + + foreach ($parsedRequest['headers'] as $name => $header) { + $lname = strtolower($name); + if (substr($lname, 0, 5) == 'x-amz') { + $parsedRequest['query'][$name] = $header; + } + $blacklist = $this->getHeaderBlacklist(); + if (isset($blacklist[$lname]) + || $lname === strtolower(self::AMZ_CONTENT_SHA256_HEADER) + ) { + unset($parsedRequest['headers'][$name]); + } + } + + return $parsedRequest; + } + + private function parseRequest(RequestInterface $request) + { + // Clean up any previously set headers. + /** @var RequestInterface $request */ + $request = $request + ->withoutHeader('X-Amz-Date') + ->withoutHeader('Date') + ->withoutHeader('Authorization'); + $uri = $request->getUri(); + + return [ + 'method' => $request->getMethod(), + 'path' => $uri->getPath(), + 'query' => Psr7\Query::parse($uri->getQuery()), + 'uri' => $uri, + 'headers' => $request->getHeaders(), + 'body' => $request->getBody(), + 'version' => $request->getProtocolVersion() + ]; + } + + private function buildRequest(array $req) + { + if ($req['query']) { + $req['uri'] = $req['uri']->withQuery(Psr7\Query::build($req['query'])); + } + + return new Psr7\Request( + $req['method'], + $req['uri'], + $req['headers'], + $req['body'], + $req['version'] + ); + } + + protected function verifyCRTLoaded() + { + if (!extension_loaded('awscrt')) { + throw new CommonRuntimeException( + "AWS Common Runtime for PHP is required to use Signature V4A" + . ". Please install it using the instructions found at" + . " https://github.com/aws/aws-sdk-php/blob/master/CRT_INSTRUCTIONS.md" + ); + } + } + + protected function createCRTStaticCredentialsProvider($credentials) + { + return new StaticCredentialsProvider([ + 'access_key_id' => $credentials->getAccessKeyId(), + 'secret_access_key' => $credentials->getSecretKey(), + 'session_token' => $credentials->getSecurityToken(), + ]); + } + + private function removeIllegalV4aHeaders(&$request) + { + static $illegalV4aHeaders = [ + self::AMZ_CONTENT_SHA256_HEADER, + 'aws-sdk-invocation-id', + 'aws-sdk-retry', + 'x-amz-region-set', + 'transfer-encoding' + ]; + $storedHeaders = []; + + foreach ($illegalV4aHeaders as $header) { + if ($request->hasHeader($header)) { + $storedHeaders[$header] = $request->getHeader($header); + $request = $request->withoutHeader($header); + } + } + + return $storedHeaders; + } + + private function CRTRequestFromGuzzleRequest($request) + { + return new Request( + $request->getMethod(), + (string) $request->getUri(), + [], //leave empty as the query is parsed from the uri object + array_map(function ($header) {return $header[0];}, $request->getHeaders()) + ); + } + + /** + * @param CredentialsInterface $credentials + * @param RequestInterface $request + * @param $signingService + * @param SigningConfigAWS|null $signingConfig + * @return RequestInterface + */ + protected function signWithV4a( + CredentialsInterface $credentials, + RequestInterface $request, + $signingService, + ?SigningConfigAWS $signingConfig = null + ){ + $this->verifyCRTLoaded(); + $signingConfig = $signingConfig ?? new SigningConfigAWS([ + 'algorithm' => SigningAlgorithm::SIGv4_ASYMMETRIC, + 'signature_type' => SignatureType::HTTP_REQUEST_HEADERS, + 'credentials_provider' => $this->createCRTStaticCredentialsProvider($credentials), + 'signed_body_value' => $this->getPayload($request), + 'should_normalize_uri_path' => true, + 'use_double_uri_encode' => true, + 'region' => $this->region, + 'service' => $signingService, + 'date' => time(), + ]); + + $removedIllegalHeaders = $this->removeIllegalV4aHeaders($request); + $http_request = $this->CRTRequestFromGuzzleRequest($request); + + Signing::signRequestAws( + Signable::fromHttpRequest($http_request), + $signingConfig, function ($signing_result, $error_code) use (&$http_request) { + $signing_result->applyToHttpRequest($http_request); + }); + foreach ($removedIllegalHeaders as $header => $value) { + $request = $request->withHeader($header, $value); + } + + $sigV4AHeaders = $http_request->headers(); + foreach ($sigV4AHeaders->toArray() as $h => $v) { + $request = $request->withHeader($h, $v); + } + + return $request; + } + + protected function presignWithV4a( + RequestInterface $request, + CredentialsInterface $credentials, + $expires + ) + { + $this->verifyCRTLoaded(); + $credentials_provider = $this->createCRTStaticCredentialsProvider($credentials); + $signingConfig = new SigningConfigAWS([ + 'algorithm' => SigningAlgorithm::SIGv4_ASYMMETRIC, + 'signature_type' => SignatureType::HTTP_REQUEST_QUERY_PARAMS, + 'credentials_provider' => $credentials_provider, + 'signed_body_value' => $this->getPresignedPayload($request), + 'region' => "*", + 'service' => $this->service, + 'date' => time(), + 'expiration_in_seconds' => $expires + ]); + + $this->removeIllegalV4aHeaders($request); + foreach ($this->getHeaderBlacklist() as $headerName => $headerValue) { + if ($request->hasHeader($headerName)) { + $request = $request->withoutHeader($headerName); + } + } + + $http_request = $this->CRTRequestFromGuzzleRequest($request); + Signing::signRequestAws( + Signable::fromHttpRequest($http_request), + $signingConfig, function ($signing_result, $error_code) use (&$http_request) { + $signing_result->applyToHttpRequest($http_request); + }); + + return $request->withUri( + new Psr7\Uri($http_request->pathAndQuery()) + ); + } +} diff --git a/3rdparty/aws/aws-sdk-php/src/StreamRequestPayloadMiddleware.php b/3rdparty/aws/aws-sdk-php/src/StreamRequestPayloadMiddleware.php new file mode 100644 index 00000000..dd63d3a5 --- /dev/null +++ b/3rdparty/aws/aws-sdk-php/src/StreamRequestPayloadMiddleware.php @@ -0,0 +1,85 @@ +nextHandler = $nextHandler; + $this->service = $service; + } + + public function __invoke(CommandInterface $command, RequestInterface $request) + { + $nextHandler = $this->nextHandler; + + $operation = $this->service->getOperation($command->getName()); + $contentLength = $request->getHeader('content-length'); + $hasStreaming = false; + $requiresLength = false; + + // Check if any present input member is a stream and requires the + // content length + foreach ($operation->getInput()->getMembers() as $name => $member) { + if (!empty($member['streaming']) && isset($command[$name])) { + $hasStreaming = true; + if (!empty($member['requiresLength'])) { + $requiresLength = true; + } + } + } + + if ($hasStreaming) { + + // Add 'transfer-encoding' header if payload size not required to + // to be calculated and not already known + if (empty($requiresLength) + && empty($contentLength) + && isset($operation['authtype']) + && $operation['authtype'] == 'v4-unsigned-body' + ) { + $request = $request->withHeader('transfer-encoding', 'chunked'); + + // Otherwise, make sure 'content-length' header is added + } else { + if (empty($contentLength)) { + $size = $request->getBody()->getSize(); + if (is_null($size)) { + throw new IncalculablePayloadException('Payload' + . ' content length is required and can not be' + . ' calculated.'); + } + $request = $request->withHeader( + 'content-length', + $size + ); + } + } + } + + return $nextHandler($command, $request); + } +} diff --git a/3rdparty/aws/aws-sdk-php/src/Sts/Exception/StsException.php b/3rdparty/aws/aws-sdk-php/src/Sts/Exception/StsException.php new file mode 100644 index 00000000..81cff402 --- /dev/null +++ b/3rdparty/aws/aws-sdk-php/src/Sts/Exception/StsException.php @@ -0,0 +1,9 @@ +endpointsType = strtolower($endpointsType); + $this->isFallback = $isFallback; + if (!in_array($this->endpointsType, ['legacy', 'regional'])) { + throw new \InvalidArgumentException( + "Configuration parameter must either be 'legacy' or 'regional'." + ); + } + } + + /** + * {@inheritdoc} + */ + public function getEndpointsType() + { + return $this->endpointsType; + } + + /** + * {@inheritdoc} + */ + public function toArray() + { + return [ + 'endpoints_type' => $this->getEndpointsType() + ]; + } + + public function isFallback() + { + return $this->isFallback; + } +} diff --git a/3rdparty/aws/aws-sdk-php/src/Sts/RegionalEndpoints/ConfigurationInterface.php b/3rdparty/aws/aws-sdk-php/src/Sts/RegionalEndpoints/ConfigurationInterface.php new file mode 100644 index 00000000..41d543b8 --- /dev/null +++ b/3rdparty/aws/aws-sdk-php/src/Sts/RegionalEndpoints/ConfigurationInterface.php @@ -0,0 +1,22 @@ + + * use Aws\Sts\RegionalEndpoints\ConfigurationProvider; + * $provider = ConfigurationProvider::defaultProvider(); + * // Returns a ConfigurationInterface or throws. + * $config = $provider()->wait(); + * + * + * Configuration providers can be composed to create configuration using + * conditional logic that can create different configurations in different + * environments. You can compose multiple providers into a single provider using + * {@see \Aws\Sts\RegionalEndpoints\ConfigurationProvider::chain}. This function + * accepts providers as variadic arguments and returns a new function that will + * invoke each provider until a successful configuration is returned. + * + * + * // First try an INI file at this location. + * $a = ConfigurationProvider::ini(null, '/path/to/file.ini'); + * // Then try an INI file at this location. + * $b = ConfigurationProvider::ini(null, '/path/to/other-file.ini'); + * // Then try loading from environment variables. + * $c = ConfigurationProvider::env(); + * // Combine the three providers together. + * $composed = ConfigurationProvider::chain($a, $b, $c); + * // Returns a promise that is fulfilled with a configuration or throws. + * $promise = $composed(); + * // Wait on the configuration to resolve. + * $config = $promise->wait(); + * + */ +class ConfigurationProvider extends AbstractConfigurationProvider + implements ConfigurationProviderInterface +{ + const DEFAULT_ENDPOINTS_TYPE = 'legacy'; + const ENV_ENDPOINTS_TYPE = 'AWS_STS_REGIONAL_ENDPOINTS'; + const ENV_PROFILE = 'AWS_PROFILE'; + const INI_ENDPOINTS_TYPE = 'sts_regional_endpoints'; + + public static $cacheKey = 'aws_sts_regional_endpoints_config'; + + protected static $interfaceClass = ConfigurationInterface::class; + protected static $exceptionClass = ConfigurationException::class; + + /** + * Create a default config provider that first checks for environment + * variables, then checks for a specified profile in the environment-defined + * config file location (env variable is 'AWS_CONFIG_FILE', file location + * defaults to ~/.aws/config), then checks for the "default" profile in the + * environment-defined config file location, and failing those uses a default + * fallback set of configuration options. + * + * This provider is automatically wrapped in a memoize function that caches + * previously provided config options. + * + * @param array $config + * + * @return callable + */ + public static function defaultProvider(array $config = []) + { + $configProviders = [self::env()]; + if ( + !isset($config['use_aws_shared_config_files']) + || $config['use_aws_shared_config_files'] != false + ) { + $configProviders[] = self::ini(); + } + $configProviders[] = self::fallback(); + + $memo = self::memoize( + call_user_func_array([ConfigurationProvider::class, 'chain'], $configProviders) + ); + + if (isset($config['sts_regional_endpoints']) + && $config['sts_regional_endpoints'] instanceof CacheInterface + ) { + return self::cache($memo, $config['sts_regional_endpoints'], self::$cacheKey); + } + + return $memo; + } + + /** + * Provider that creates config from environment variables. + * + * @return callable + */ + public static function env() + { + return function () { + // Use config from environment variables, if available + $endpointsType = getenv(self::ENV_ENDPOINTS_TYPE); + if (!empty($endpointsType)) { + return Promise\Create::promiseFor( + new Configuration($endpointsType) + ); + } + + return self::reject('Could not find environment variable config' + . ' in ' . self::ENV_ENDPOINTS_TYPE); + }; + } + + /** + * Fallback config options when other sources are not set. + * + * @return callable + */ + public static function fallback() + { + return function () { + return Promise\Create::promiseFor( + new Configuration(self::DEFAULT_ENDPOINTS_TYPE, true) + ); + }; + } + + /** + * Config provider that creates config using a config file whose location + * is specified by an environment variable 'AWS_CONFIG_FILE', defaulting to + * ~/.aws/config if not specified + * + * @param string|null $profile Profile to use. If not specified will use + * the "default" profile. + * @param string|null $filename If provided, uses a custom filename rather + * than looking in the default directory. + * + * @return callable + */ + public static function ini( + $profile = null, + $filename = null + ) { + $filename = $filename ?: (self::getDefaultConfigFilename()); + $profile = $profile ?: (getenv(self::ENV_PROFILE) ?: 'default'); + + return function () use ($profile, $filename) { + if (!@is_readable($filename)) { + return self::reject("Cannot read configuration from $filename"); + } + $data = \Aws\parse_ini_file($filename, true); + if ($data === false) { + return self::reject("Invalid config file: $filename"); + } + if (!isset($data[$profile])) { + return self::reject("'$profile' not found in config file"); + } + if (!isset($data[$profile][self::INI_ENDPOINTS_TYPE])) { + return self::reject("Required STS regional endpoints config values + not present in INI profile '{$profile}' ({$filename})"); + } + + return Promise\Create::promiseFor( + new Configuration($data[$profile][self::INI_ENDPOINTS_TYPE]) + ); + }; + } + + /** + * Unwraps a configuration object in whatever valid form it is in, + * always returning a ConfigurationInterface object. + * + * @param mixed $config + * @return ConfigurationInterface + * @throws \InvalidArgumentException + */ + public static function unwrap($config) + { + if (is_callable($config)) { + $config = $config(); + } + if ($config instanceof PromiseInterface) { + $config = $config->wait(); + } + if ($config instanceof ConfigurationInterface) { + return $config; + } + if (is_string($config)) { + return new Configuration($config); + } + if (is_array($config) && isset($config['endpoints_type'])) { + return new Configuration($config['endpoints_type']); + } + + throw new \InvalidArgumentException('Not a valid STS regional endpoints ' + . 'configuration argument.'); + } +} diff --git a/3rdparty/aws/aws-sdk-php/src/Sts/RegionalEndpoints/Exception/ConfigurationException.php b/3rdparty/aws/aws-sdk-php/src/Sts/RegionalEndpoints/Exception/ConfigurationException.php new file mode 100644 index 00000000..66842667 --- /dev/null +++ b/3rdparty/aws/aws-sdk-php/src/Sts/RegionalEndpoints/Exception/ConfigurationException.php @@ -0,0 +1,14 @@ +addBuiltIns($args); + parent::__construct($args); + } + + /** + * Creates credentials from the result of an STS operations + * + * @param Result $result Result of an STS operation + * + * @return Credentials + * @throws \InvalidArgumentException if the result contains no credentials + */ + public function createCredentials(Result $result, $source=null) + { + if (!$result->hasKey('Credentials')) { + throw new \InvalidArgumentException('Result contains no credentials'); + } + + $accountId = null; + if ($result->hasKey('AssumedRoleUser')) { + $parsedArn = ArnParser::parse($result->get('AssumedRoleUser')['Arn']); + $accountId = $parsedArn->getAccountId(); + } elseif ($result->hasKey('FederatedUser')) { + $parsedArn = ArnParser::parse($result->get('FederatedUser')['Arn']); + $accountId = $parsedArn->getAccountId(); + } + + $credentials = $result['Credentials']; + $expiration = isset($credentials['Expiration']) && $credentials['Expiration'] instanceof \DateTimeInterface + ? (int) $credentials['Expiration']->format('U') + : null; + + return new Credentials( + $credentials['AccessKeyId'], + $credentials['SecretAccessKey'], + isset($credentials['SessionToken']) ? $credentials['SessionToken'] : null, + $expiration, + $accountId, + $source + ); + } + + /** + * Adds service-specific client built-in value + * + * @return void + */ + private function addBuiltIns($args) + { + $key = 'AWS::STS::UseGlobalEndpoint'; + $result = $args['sts_regional_endpoints'] instanceof \Closure ? + $args['sts_regional_endpoints']()->wait() : $args['sts_regional_endpoints']; + + if (is_string($result)) { + if ($result === 'regional') { + $value = false; + } else if ($result === 'legacy') { + $value = true; + } else { + return; + } + } else { + if ($result->getEndpointsType() === 'regional') { + $value = false; + } else { + $value = true; + } + } + + $this->clientBuiltIns[$key] = $value; + } +} diff --git a/3rdparty/aws/aws-sdk-php/src/Token/BearerTokenAuthorization.php b/3rdparty/aws/aws-sdk-php/src/Token/BearerTokenAuthorization.php new file mode 100644 index 00000000..7d830f3b --- /dev/null +++ b/3rdparty/aws/aws-sdk-php/src/Token/BearerTokenAuthorization.php @@ -0,0 +1,33 @@ +getToken())) { + throw new InvalidArgumentException( + "Cannot authorize a request with an empty token" + ); + } + $accessToken = $token->getToken(); + return $request->withHeader('Authorization', "Bearer {$accessToken}"); + } +} diff --git a/3rdparty/aws/aws-sdk-php/src/Token/ParsesIniTrait.php b/3rdparty/aws/aws-sdk-php/src/Token/ParsesIniTrait.php new file mode 100644 index 00000000..b96a6d97 --- /dev/null +++ b/3rdparty/aws/aws-sdk-php/src/Token/ParsesIniTrait.php @@ -0,0 +1,44 @@ + $profile) { + // standardize config profile names + $name = str_replace('profile ', '', $name); + $profileData[$name] = $profile; + } + + return $profileData; + } + + /** + * Gets the environment's HOME directory if available. + * + * @return null|string + */ + private static function getHomeDir() + { + // On Linux/Unix-like systems, use the HOME environment variable + if ($homeDir = getenv('HOME')) { + return $homeDir; + } + + // Get the HOMEDRIVE and HOMEPATH values for Windows hosts + $homeDrive = getenv('HOMEDRIVE'); + $homePath = getenv('HOMEPATH'); + + return ($homeDrive && $homePath) ? $homeDrive . $homePath : null; + } +} diff --git a/3rdparty/aws/aws-sdk-php/src/Token/RefreshableTokenProviderInterface.php b/3rdparty/aws/aws-sdk-php/src/Token/RefreshableTokenProviderInterface.php new file mode 100644 index 00000000..4c88f3f0 --- /dev/null +++ b/3rdparty/aws/aws-sdk-php/src/Token/RefreshableTokenProviderInterface.php @@ -0,0 +1,23 @@ +refreshToken = $refreshToken; + $this->clientId = $clientId; + $this->clientSecret = $clientSecret; + $this->registrationExpiresAt = $registrationExpiresAt; + $this->region = $region; + $this->startUrl = $startUrl; + } + + /** + * @return bool + */ + public function isExpired() + { + if (isset($this->registrationExpiresAt) + && time() >= $this->registrationExpiresAt + ) { + return false; + } + return $this->expires !== null && time() >= $this->expires; + } + + /** + * @return string|null + */ + public function getRefreshToken() + { + return $this->refreshToken; + } + + /** + * @return string|null + */ + public function getClientId() + { + return $this->clientId; + } + + /** + * @return string|null + */ + public function getClientSecret() + { + return $this->clientSecret; + } + + /** + * @return int|null + */ + public function getRegistrationExpiresAt() + { + return $this->registrationExpiresAt; + } + + /** + * @return string|null + */ + public function getRegion() + { + return $this->region; + } + + /** + * @return string|null + */ + public function getStartUrl() + { + return $this->startUrl; + } + + /** + * Creates an instance of SsoToken from a token data. + * + * @param $tokenData + * + * @return SsoToken + */ + public static function fromTokenData($tokenData): SsoToken + { + return new SsoToken( + $tokenData['accessToken'], + \strtotime($tokenData['expiresAt']), + isset($tokenData['refreshToken']) ? $tokenData['refreshToken'] : null, + isset($tokenData['clientId']) ? $tokenData['clientId'] : null, + isset($tokenData['clientSecret']) ? $tokenData['clientSecret'] : null, + isset($tokenData['registrationExpiresAt']) ? $tokenData['registrationExpiresAt'] : null, + isset($tokenData['region']) ? $tokenData['region'] : null, + isset($tokenData['startUrl']) ? $tokenData['startUrl'] : null + ); + } +} diff --git a/3rdparty/aws/aws-sdk-php/src/Token/SsoTokenProvider.php b/3rdparty/aws/aws-sdk-php/src/Token/SsoTokenProvider.php new file mode 100644 index 00000000..13345a7c --- /dev/null +++ b/3rdparty/aws/aws-sdk-php/src/Token/SsoTokenProvider.php @@ -0,0 +1,280 @@ +profileName = $this->resolveProfileName($profileName); + $this->configFilePath = $this->resolveConfigFile($configFilePath); + $this->ssoOidcClient = $ssoOidcClient; + } + + /** + * This method resolves the profile name to be used. The + * profile provided as instantiation argument takes precedence, + * followed by AWS_PROFILE env variable, otherwise `default` is + * used. + * + * @param string|null $argProfileName The profile provided as argument. + * + * @return string + */ + private function resolveProfileName($argProfileName): string + { + if (empty($argProfileName)) { + return getenv(self::ENV_PROFILE) ?: 'default'; + } else { + return $argProfileName; + } + } + + /** + * This method resolves the config file from where the profiles + * are going to be loaded from. If $argFileName is not empty then, + * it takes precedence over the default config file location. + * + * @param string|null $argConfigFilePath The config path provided as argument. + * + * @return string + */ + private function resolveConfigFile($argConfigFilePath): string + { + if (empty($argConfigFilePath)) { + return self::getHomeDir() . '/.aws/config'; + } else{ + return $argConfigFilePath; + } + } + + /** + * Loads cached sso credentials. + * + * @return Promise\PromiseInterface + */ + public function __invoke() + { + return Promise\Coroutine::of(function () { + if (empty($this->configFilePath) || !is_readable($this->configFilePath)) { + throw new TokenException("Cannot read profiles from {$this->configFilePath}"); + } + + $profiles = self::loadProfiles($this->configFilePath); + if (!isset($profiles[$this->profileName])) { + throw new TokenException("Profile `{$this->profileName}` does not exist in {$this->configFilePath}."); + } + + $profile = $profiles[$this->profileName]; + if (empty($profile['sso_session'])) { + throw new TokenException( + "Profile `{$this->profileName}` in {$this->configFilePath} must contain an sso_session." + ); + } + + $ssoSessionName = $profile['sso_session']; + $this->ssoSessionName = $ssoSessionName; + $profileSsoSession = 'sso-session ' . $ssoSessionName; + if (empty($profiles[$profileSsoSession])) { + throw new TokenException( + "Sso session `{$ssoSessionName}` does not exist in {$this->configFilePath}" + ); + } + + $sessionProfileData = $profiles[$profileSsoSession]; + foreach (['sso_start_url', 'sso_region'] as $requiredProp) { + if (empty($sessionProfileData[$requiredProp])) { + throw new TokenException( + "Sso session `{$ssoSessionName}` in {$this->configFilePath} is missing the required property `{$requiredProp}`" + ); + } + } + + $tokenData = $this->refresh(); + $tokenLocation = self::getTokenLocation($ssoSessionName); + $this->validateTokenData($tokenLocation, $tokenData); + $ssoToken = SsoToken::fromTokenData($tokenData); + // To make sure the token is not expired + if ($ssoToken->isExpired()) { + throw new TokenException("Cached SSO token returned an expired token."); + } + + yield $ssoToken; + }); + } + + /** + * This method attempt to refresh when possible. + * If a refresh is not possible then it just returns + * the current token data as it is. + * + * @return array + * @throws TokenException + */ + public function refresh(): array + { + $tokenLocation = self::getTokenLocation($this->ssoSessionName); + $tokenData = $this->getTokenData($tokenLocation); + if (!$this->shouldAttemptRefresh()) { + return $tokenData; + } + + if (null === $this->ssoOidcClient) { + throw new TokenException( + "Cannot refresh this token without an 'ssooidcClient' " + ); + } + + foreach (['clientId', 'clientSecret', 'refreshToken'] as $requiredProp) { + if (empty($tokenData[$requiredProp])) { + throw new TokenException( + "Cannot refresh this token without `{$requiredProp}` being set" + ); + } + } + + $response = $this->ssoOidcClient->createToken([ + 'clientId' => $tokenData['clientId'], + 'clientSecret' => $tokenData['clientSecret'], + 'grantType' => 'refresh_token', // REQUIRED + 'refreshToken' => $tokenData['refreshToken'], + ]); + if ($response['@metadata']['statusCode'] !== 200) { + throw new TokenException('Unable to create a new sso token'); + } + + $tokenData['accessToken'] = $response['accessToken']; + $tokenData['expiresAt'] = time () + $response['expiresIn']; + $tokenData['refreshToken'] = $response['refreshToken']; + + return $this->writeNewTokenDataToDisk($tokenData, $tokenLocation); + } + + /** + * This method checks for whether a token refresh should happen. + * It will return true just if more than 30 seconds has happened + * since last refresh, and if the expiration is within a 5-minutes + * window from the current time. + * + * @return bool + */ + public function shouldAttemptRefresh(): bool + { + $tokenLocation = self::getTokenLocation($this->ssoSessionName); + $tokenData = $this->getTokenData($tokenLocation); + if (empty($tokenData['expiresAt'])) { + throw new TokenException( + "Token file at $tokenLocation must contain an expiration date" + ); + } + + $tokenExpiresAt = strtotime($tokenData['expiresAt']); + $lastRefreshAt = filemtime($tokenLocation); + $now = \time(); + + // If last refresh happened after 30 seconds + // and if the token expiration is in the 5 minutes window + return ($now - $lastRefreshAt) > self::REFRESH_ATTEMPT_WINDOW_IN_SECS + && ($tokenExpiresAt - $now) < self::REFRESH_WINDOW_IN_SECS; + } + + /** + * @param $sso_session + * @return string + */ + public static function getTokenLocation($sso_session): string + { + return self::getHomeDir() + . '/.aws/sso/cache/' + . mb_convert_encoding(sha1($sso_session), "UTF-8") + . ".json"; + } + + /** + * @param $tokenLocation + * @return array + */ + function getTokenData($tokenLocation): array + { + if (empty($tokenLocation) || !is_readable($tokenLocation)) { + throw new TokenException("Unable to read token file at {$tokenLocation}"); + } + + return json_decode(file_get_contents($tokenLocation), true); + } + + /** + * @param $tokenData + * @param $tokenLocation + * @return mixed + */ + private function validateTokenData($tokenLocation, $tokenData) + { + foreach (['accessToken', 'expiresAt'] as $requiredProp) { + if (empty($tokenData[$requiredProp])) { + throw new TokenException( + "Token file at {$tokenLocation} must contain the required property `{$requiredProp}`" + ); + } + } + + $expiration = strtotime($tokenData['expiresAt']); + if ($expiration === false) { + throw new TokenException("Cached SSO token returned an invalid expiration"); + } elseif ($expiration < time()) { + throw new TokenException("Cached SSO token returned an expired token"); + } + + return $tokenData; + } + + /** + * @param array $tokenData + * @param string $tokenLocation + * + * @return array + */ + private function writeNewTokenDataToDisk(array $tokenData, $tokenLocation): array + { + $tokenData['expiresAt'] = gmdate( + 'Y-m-d\TH:i:s\Z', + $tokenData['expiresAt'] + ); + file_put_contents($tokenLocation, json_encode(array_filter($tokenData))); + + return $tokenData; + } +} diff --git a/3rdparty/aws/aws-sdk-php/src/Token/Token.php b/3rdparty/aws/aws-sdk-php/src/Token/Token.php new file mode 100644 index 00000000..51e83c36 --- /dev/null +++ b/3rdparty/aws/aws-sdk-php/src/Token/Token.php @@ -0,0 +1,111 @@ +token = $token; + $this->expires = $expires; + } + + /** + * Sets the state of a token object + * + * @param array $state array containing 'token' and 'expires' + */ + public static function __set_state(array $state) + { + return new self( + $state['token'], + $state['expires'] + ); + } + + /** + * @return string + */ + public function getToken() + { + return $this->token; + } + + /** + * @return int + */ + public function getExpiration() + { + return $this->expires; + } + + /** + * @return bool + */ + public function isExpired() + { + return $this->expires !== null && time() >= $this->expires; + } + + /** + * @return array + */ + public function toArray() + { + return [ + 'token' => $this->token, + 'expires' => $this->expires + ]; + } + + /** + * @return string + */ + public function serialize() + { + return json_encode($this->__serialize()); + } + + /** + * Sets the state of the object from serialized json data + */ + public function unserialize($serialized) + { + $data = json_decode($serialized, true); + + $this->__unserialize($data); + } + + /** + * @return array + */ + public function __serialize() + { + return $this->toArray(); + } + + /** + * Sets the state of this object from an array + */ + public function __unserialize($data) + { + $this->token = $data['token']; + $this->expires = $data['expires']; + } +} diff --git a/3rdparty/aws/aws-sdk-php/src/Token/TokenAuthorization.php b/3rdparty/aws/aws-sdk-php/src/Token/TokenAuthorization.php new file mode 100644 index 00000000..3fab516f --- /dev/null +++ b/3rdparty/aws/aws-sdk-php/src/Token/TokenAuthorization.php @@ -0,0 +1,24 @@ + + * use Aws\Token\TokenProvider; + * $provider = TokenProvider::defaultProvider(); + * // Returns a TokenInterface or throws. + * $token = $provider()->wait(); + * + * + * Token providers can be composed to create a token using conditional + * logic that can create different tokens in different environments. You + * can compose multiple providers into a single provider using + * {@see Aws\Token\TokenProvider::chain}. This function accepts + * providers as variadic arguments and returns a new function that will invoke + * each provider until a token is successfully returned. + */ +class TokenProvider +{ + use ParsesIniTrait; + const ENV_PROFILE = 'AWS_PROFILE'; + + /** + * Create a default token provider tha checks for cached a SSO token from + * the CLI + * + * This provider is automatically wrapped in a memoize function that caches + * previously provided tokens. + * + * @param array $config Optional array of token provider options. + * + * @return callable + */ + public static function defaultProvider(array $config = []) + { + + $cacheable = [ + 'sso', + ]; + + $defaultChain = []; + + if ( + !isset($config['use_aws_shared_config_files']) + || $config['use_aws_shared_config_files'] !== false + ) { + $profileName = getenv(self::ENV_PROFILE) ?: 'default'; + $defaultChain['sso'] = self::sso( + $profileName, + self::getHomeDir() . '/.aws/config', + $config + ); + } + + if (isset($config['token']) + && $config['token'] instanceof CacheInterface + ) { + foreach ($cacheable as $provider) { + if (isset($defaultChain[$provider])) { + $defaultChain[$provider] = self::cache( + $defaultChain[$provider], + $config['token'], + 'aws_cached_' . $provider . '_token' + ); + } + } + } + + return self::memoize( + call_user_func_array( + [TokenProvider::class, 'chain'], + array_values($defaultChain) + ) + ); + } + + /** + * Create a token provider function from a static token. + * + * @param TokenInterface $token + * + * @return callable + */ + public static function fromToken(TokenInterface $token) + { + $promise = Promise\Create::promiseFor($token); + + return function () use ($promise) { + return $promise; + }; + } + + /** + * Creates an aggregate token provider that invokes the provided + * variadic providers one after the other until a provider returns + * a token. + * + * @return callable + */ + public static function chain() + { + $links = func_get_args(); + //Common use case for when aws_shared_config_files is false + if (empty($links)) { + return function () { + return Promise\Create::promiseFor(false); + }; + } + + return function () use ($links) { + /** @var callable $parent */ + $parent = array_shift($links); + $promise = $parent(); + while ($next = array_shift($links)) { + $promise = $promise->otherwise($next); + } + return $promise; + }; + } + + /** + * Wraps a token provider and caches a previously provided token. + * Ensures that cached tokens are refreshed when they expire. + * + * @param callable $provider Token provider function to wrap. + * @return callable + */ + public static function memoize(callable $provider) + { + return function () use ($provider) { + static $result; + static $isConstant; + + // Constant tokens will be returned constantly. + if ($isConstant) { + return $result; + } + + // Create the initial promise that will be used as the cached value + // until it expires. + if (null === $result) { + $result = $provider(); + } + + // Return a token that could expire and refresh when needed. + return $result + ->then(function (TokenInterface $token) use ($provider, &$isConstant, &$result) { + // Determine if the token is constant. + if (!$token->getExpiration()) { + $isConstant = true; + return $token; + } + + if (!$token->isExpired()) { + return $token; + } + return $result = $provider(); + }) + ->otherwise(function($reason) use (&$result) { + // Cleanup rejected promise. + $result = null; + return Promise\Create::promiseFor(null); + }); + }; + } + + /** + * Wraps a token provider and saves provided token in an + * instance of Aws\CacheInterface. Forwards calls when no token found + * in cache and updates cache with the results. + * + * @param callable $provider Token provider function to wrap + * @param CacheInterface $cache Cache to store the token + * @param string|null $cacheKey (optional) Cache key to use + * + * @return callable + */ + public static function cache( + callable $provider, + CacheInterface $cache, + $cacheKey = null + ) { + $cacheKey = $cacheKey ?: 'aws_cached_token'; + + return function () use ($provider, $cache, $cacheKey) { + $found = $cache->get($cacheKey); + if (is_array($found) && isset($found['token'])) { + $foundToken = $found['token']; + if ($foundToken instanceof TokenInterface) { + if (!$foundToken->isExpired()) { + return Promise\Create::promiseFor($foundToken); + } + if (isset($found['refreshMethod']) && is_callable($found['refreshMethod'])) { + return Promise\Create::promiseFor($found['refreshMethod']()); + } + } + } + + return $provider() + ->then(function (TokenInterface $token) use ( + $cache, + $cacheKey + ) { + $cache->set( + $cacheKey, + $token, + null === $token->getExpiration() ? + 0 : $token->getExpiration() - time() + ); + + return $token; + }); + }; + } + + /** + * Gets profiles from the ~/.aws/config ini file + */ + private static function loadDefaultProfiles() { + $profiles = []; + $configFile = self::getHomeDir() . '/.aws/config'; + + if (file_exists($configFile)) { + $configProfileData = \Aws\parse_ini_file($configFile, true, INI_SCANNER_RAW); + foreach ($configProfileData as $name => $profile) { + // standardize config profile names + $name = str_replace('profile ', '', $name); + if (!isset($profiles[$name])) { + $profiles[$name] = $profile; + } + } + } + + return $profiles; + } + + private static function reject($msg) + { + return new Promise\RejectedPromise(new TokenException($msg)); + } + + /** + * Token provider that creates a token from cached sso credentials + * + * @param string $profileName the name of the ini profile name + * @param string $filename the location of the ini file + * @param array $config configuration options + * + * @return SsoTokenProvider + * @see Aws\Token\SsoTokenProvider for $config details. + */ + public static function sso($profileName, $filename, $config = []) + { + $ssoClient = isset($config['ssoClient']) ? $config['ssoClient'] : null; + + return new SsoTokenProvider($profileName, $filename, $ssoClient); + } +} + diff --git a/3rdparty/aws/aws-sdk-php/src/TraceMiddleware.php b/3rdparty/aws/aws-sdk-php/src/TraceMiddleware.php new file mode 100644 index 00000000..032043b8 --- /dev/null +++ b/3rdparty/aws/aws-sdk-php/src/TraceMiddleware.php @@ -0,0 +1,360 @@ + '[TOKEN]', + ]; + + private static $authStrings = [ + // S3Signature + '/AWSAccessKeyId=[A-Z0-9]{20}&/i' => 'AWSAccessKeyId=[KEY]&', + // SignatureV4 Signature and S3Signature + '/Signature=.+/i' => 'Signature=[SIGNATURE]', + // SignatureV4 access key ID + '/Credential=[A-Z0-9]{20}\//i' => 'Credential=[KEY]/', + // S3 signatures + '/AWS [A-Z0-9]{20}:.+/' => 'AWS AKI[KEY]:[SIGNATURE]', + // STS Presigned URLs + '/X-Amz-Security-Token=[^&]+/i' => 'X-Amz-Security-Token=[TOKEN]', + // Crypto *Stream Keys + '/\["key.{27,36}Stream.{9}\]=>\s+.{7}\d{2}\) "\X{16,64}"/U' => '["key":[CONTENT KEY]]', + ]; + + /** + * Configuration array can contain the following key value pairs. + * + * - logfn: (callable) Function that is invoked with log messages. By + * default, PHP's "echo" function will be utilized. + * - stream_size: (int) When the size of a stream is greater than this + * number, the stream data will not be logged. Set to "0" to not log any + * stream data. + * - scrub_auth: (bool) Set to false to disable the scrubbing of auth data + * from the logged messages. + * - http: (bool) Set to false to disable the "debug" feature of lower + * level HTTP adapters (e.g., verbose curl output). + * - auth_strings: (array) A mapping of authentication string regular + * expressions to scrubbed strings. These mappings are passed directly to + * preg_replace (e.g., preg_replace($key, $value, $debugOutput) if + * "scrub_auth" is set to true. + * - auth_headers: (array) A mapping of header names known to contain + * sensitive data to what the scrubbed value should be. The value of any + * headers contained in this array will be replaced with the if + * "scrub_auth" is set to true. + */ + public function __construct(array $config = [], ?Service $service = null) + { + $this->config = $config + [ + 'logfn' => function ($value) { echo $value; }, + 'stream_size' => 524288, + 'scrub_auth' => true, + 'http' => true, + 'auth_strings' => [], + 'auth_headers' => [], + ]; + + $this->config['auth_strings'] += self::$authStrings; + $this->config['auth_headers'] += self::$authHeaders; + $this->service = $service; + } + + public function __invoke($step, $name) + { + $this->prevOutput = $this->prevInput = []; + + return function (callable $next) use ($step, $name) { + return function ( + CommandInterface $command, + $request = null + ) use ($next, $step, $name) { + $this->createHttpDebug($command); + $start = microtime(true); + $this->stepInput([ + 'step' => $step, + 'name' => $name, + 'request' => $this->requestArray($request), + 'command' => $this->commandArray($command) + ]); + + return $next($command, $request)->then( + function ($value) use ($step, $name, $command, $start) { + $this->flushHttpDebug($command); + $this->stepOutput($start, [ + 'step' => $step, + 'name' => $name, + 'result' => $this->resultArray($value), + 'error' => null + ]); + return $value; + }, + function ($reason) use ($step, $name, $start, $command) { + $this->flushHttpDebug($command); + $this->stepOutput($start, [ + 'step' => $step, + 'name' => $name, + 'result' => null, + 'error' => $this->exceptionArray($reason) + ]); + return new RejectedPromise($reason); + } + ); + }; + }; + } + + private function stepInput($entry) + { + static $keys = ['command', 'request']; + $this->compareStep($this->prevInput, $entry, '-> Entering', $keys); + $this->write("\n"); + $this->prevInput = $entry; + } + + private function stepOutput($start, $entry) + { + static $keys = ['result', 'error']; + $this->compareStep($this->prevOutput, $entry, '<- Leaving', $keys); + $totalTime = microtime(true) - $start; + $this->write(" Inclusive step time: " . $totalTime . "\n\n"); + $this->prevOutput = $entry; + } + + private function compareStep(array $a, array $b, $title, array $keys) + { + $changes = []; + foreach ($keys as $key) { + $av = isset($a[$key]) ? $a[$key] : null; + $bv = isset($b[$key]) ? $b[$key] : null; + $this->compareArray($av, $bv, $key, $changes); + } + $str = "\n{$title} step {$b['step']}, name '{$b['name']}'"; + $str .= "\n" . str_repeat('-', strlen($str) - 1) . "\n\n "; + $str .= $changes + ? implode("\n ", str_replace("\n", "\n ", $changes)) + : 'no changes'; + $this->write($str . "\n"); + } + + private function commandArray(CommandInterface $cmd) + { + return [ + 'instance' => spl_object_hash($cmd), + 'name' => $cmd->getName(), + 'params' => $this->getRedactedArray($cmd) + ]; + } + + private function requestArray($request = null) + { + return !$request instanceof RequestInterface + ? [] + : array_filter([ + 'instance' => spl_object_hash($request), + 'method' => $request->getMethod(), + 'headers' => $this->redactHeaders($request->getHeaders()), + 'body' => $this->streamStr($request->getBody()), + 'scheme' => $request->getUri()->getScheme(), + 'port' => $request->getUri()->getPort(), + 'path' => $request->getUri()->getPath(), + 'query' => $request->getUri()->getQuery(), + ]); + } + + private function responseArray(?ResponseInterface $response = null) + { + return !$response ? [] : [ + 'instance' => spl_object_hash($response), + 'statusCode' => $response->getStatusCode(), + 'headers' => $this->redactHeaders($response->getHeaders()), + 'body' => $this->streamStr($response->getBody()) + ]; + } + + private function resultArray($value) + { + return $value instanceof ResultInterface + ? [ + 'instance' => spl_object_hash($value), + 'data' => $value->toArray() + ] : $value; + } + + private function exceptionArray($e) + { + if (!($e instanceof \Exception)) { + return $e; + } + + $result = [ + 'instance' => spl_object_hash($e), + 'class' => get_class($e), + 'message' => $e->getMessage(), + 'file' => $e->getFile(), + 'line' => $e->getLine(), + 'trace' => $e->getTraceAsString(), + ]; + + if ($e instanceof AwsException) { + $result += [ + 'type' => $e->getAwsErrorType(), + 'code' => $e->getAwsErrorCode(), + 'requestId' => $e->getAwsRequestId(), + 'statusCode' => $e->getStatusCode(), + 'result' => $this->resultArray($e->getResult()), + 'request' => $this->requestArray($e->getRequest()), + 'response' => $this->responseArray($e->getResponse()), + ]; + } + + return $result; + } + + private function compareArray($a, $b, $path, array &$diff) + { + if ($a === $b) { + return; + } + + if (is_array($a)) { + $b = (array) $b; + $keys = array_unique(array_merge(array_keys($a), array_keys($b))); + foreach ($keys as $k) { + if (!array_key_exists($k, $a)) { + $this->compareArray(null, $b[$k], "{$path}.{$k}", $diff); + } elseif (!array_key_exists($k, $b)) { + $this->compareArray($a[$k], null, "{$path}.{$k}", $diff); + } else { + $this->compareArray($a[$k], $b[$k], "{$path}.{$k}", $diff); + } + } + } elseif ($a !== null && $b === null) { + $diff[] = "{$path} was unset"; + } elseif ($a === null && $b !== null) { + $diff[] = sprintf("%s was set to %s", $path, $this->str($b)); + } else { + $diff[] = sprintf("%s changed from %s to %s", $path, $this->str($a), $this->str($b)); + } + } + + private function str($value) + { + if (is_scalar($value)) { + return (string) $value; + } + + if ($value instanceof \Exception) { + $value = $this->exceptionArray($value); + } + + ob_start(); + var_dump($value); + return ob_get_clean(); + } + + private function streamStr(StreamInterface $body) + { + return $body->getSize() < $this->config['stream_size'] + ? (string) $body + : 'stream(size=' . $body->getSize() . ')'; + } + + private function createHttpDebug(CommandInterface $command) + { + if ($this->config['http'] && !isset($command['@http']['debug'])) { + $command['@http']['debug'] = fopen('php://temp', 'w+'); + } + } + + private function flushHttpDebug(CommandInterface $command) + { + if ($res = $command['@http']['debug']) { + if (is_resource($res)) { + rewind($res); + $this->write(stream_get_contents($res)); + fclose($res); + } + $command['@http']['debug'] = null; + } + } + + private function write($value) + { + if ($this->config['scrub_auth']) { + foreach ($this->config['auth_strings'] as $pattern => $replacement) { + $value = preg_replace_callback( + $pattern, + function ($matches) use ($replacement) { + return $replacement; + }, + $value + ); + } + } + + call_user_func($this->config['logfn'], $value); + } + + private function redactHeaders(array $headers) + { + if ($this->config['scrub_auth']) { + $headers = $this->config['auth_headers'] + $headers; + } + + return $headers; + } + + /** + * @param CommandInterface $cmd + * @return array + */ + private function getRedactedArray(CommandInterface $cmd) + { + if (!isset($this->service["shapes"])) { + return $cmd->toArray(); + } + $shapes = $this->service["shapes"]; + $cmdArray = $cmd->toArray(); + $iterator = new RecursiveIteratorIterator( + new RecursiveArrayIterator($cmdArray), + RecursiveIteratorIterator::SELF_FIRST + ); + foreach ($iterator as $parameter => $value) { + if (isset($shapes[$parameter]['sensitive']) && + $shapes[$parameter]['sensitive'] === true + ) { + $redactedValue = is_string($value) ? "[{$parameter}]" : ["[{$parameter}]"]; + $currentDepth = $iterator->getDepth(); + for ($subDepth = $currentDepth; $subDepth >= 0; $subDepth--) { + $subIterator = $iterator->getSubIterator($subDepth); + $subIterator->offsetSet( + $subIterator->key(), + ($subDepth === $currentDepth + ? $redactedValue + : $iterator->getSubIterator(($subDepth+1))->getArrayCopy() + ) + ); + } + } + } + return $iterator->getArrayCopy(); + } +} diff --git a/3rdparty/aws/aws-sdk-php/src/UserAgentMiddleware.php b/3rdparty/aws/aws-sdk-php/src/UserAgentMiddleware.php new file mode 100644 index 00000000..afccc978 --- /dev/null +++ b/3rdparty/aws/aws-sdk-php/src/UserAgentMiddleware.php @@ -0,0 +1,265 @@ +nextHandler = $nextHandler; + $this->args = $args; + } + + /** + * When invoked, its injects the user agent header into the + * request headers. + * + * @param CommandInterface $command + * @param RequestInterface $request + * + * @return mixed + */ + public function __invoke(CommandInterface $command, RequestInterface $request) + { + $handler = $this->nextHandler; + $this->metricsBuilder = MetricsBuilder::fromCommand($command); + $request = $this->requestWithUserAgentHeader($request); + + return $handler($command, $request); + } + + /** + * Builds the user agent header value, and injects it into the request + * headers. Then, it returns the mutated request. + * + * @param RequestInterface $request + * + * @return RequestInterface + */ + private function requestWithUserAgentHeader(RequestInterface $request): RequestInterface + { + $uaAppend = $this->args['ua_append'] ?? []; + $userAgentValue = array_merge( + $this->buildUserAgentValue(), + $uaAppend + ); + // It includes the user agent values just for the User-Agent header. + // The reason is that the SEP does not mention appending the + // metrics into the X-Amz-User-Agent header. + return $request->withHeader( + 'User-Agent', + implode(' ', array_merge( + $userAgentValue, + $request->getHeader('User-Agent') + )) + ); + } + + /** + * Builds the different user agent values. + * + * @return array + */ + private function buildUserAgentValue(): array + { + $userAgentValue = []; + foreach (self::$userAgentFnList as $fn) { + $val = $this->{$fn}(); + if (!empty($val)) { + $userAgentValue[] = $val; + } + } + + return $userAgentValue; + } + + /** + * Returns the user agent value for SDK version. + * + * @return string + */ + private function getSdkVersion(): string + { + return 'aws-sdk-php/' . Sdk::VERSION; + } + + /** + * Returns the user agent value for the agent version. + * + * @return string + */ + private function getUserAgentVersion(): string + { + return 'ua/' . self::AGENT_VERSION; + } + + /** + * Returns the user agent value for the hhvm version, but just + * when it is defined. + * + * @return string + */ + private function getHhvmVersion(): string + { + if (defined('HHVM_VERSION')) { + return 'HHVM/' . HHVM_VERSION; + } + + return ""; + } + + /** + * Returns the user agent value for the os version. + * + * @return string + */ + private function getOsName(): string + { + $disabledFunctions = explode(',', ini_get('disable_functions')); + if (function_exists('php_uname') + && !in_array('php_uname', $disabledFunctions, true) + ) { + $osName = "OS/" . php_uname('s') . '#' . php_uname('r'); + if (!empty($osName)) { + return $osName; + } + } + + return ""; + } + + /** + * Returns the user agent value for the php language used. + * + * @return string + */ + private function getLangVersion(): string + { + return 'lang/php#' . phpversion(); + } + + /** + * Returns the user agent value for the execution env. + * + * @return string + */ + private function getExecEnv(): string + { + if ($executionEnvironment = getenv('AWS_EXECUTION_ENV')) { + return $executionEnvironment; + } + + return ""; + } + + /** + * Returns the user agent value for endpoint discovery as cfg. + * This feature is deprecated. + * + * @return string + */ + private function getEndpointDiscovery(): string + { + $args = $this->args; + if (isset($args['endpoint_discovery'])) { + if (($args['endpoint_discovery'] instanceof Configuration + && $args['endpoint_discovery']->isEnabled()) + ) { + return 'cfg/endpoint-discovery'; + } elseif (is_array($args['endpoint_discovery']) + && isset($args['endpoint_discovery']['enabled']) + && $args['endpoint_discovery']['enabled'] + ) { + return 'cfg/endpoint-discovery'; + } + } + + return ""; + } + + /** + * Returns the user agent value for app id, but just when an + * app id was provided as a client argument. + * + * @return string + */ + private function getAppId(): string + { + if (empty($this->args['app_id'])) { + return ""; + } + + return 'app/' . $this->args['app_id']; + } + + /** + * Returns the user agent value for metrics. + * + * @return string + */ + private function getMetrics(): string + { + // Resolve first metrics related to client arguments. + $this->metricsBuilder->resolveAndAppendFromArgs($this->args); + // Build the metrics. + $metricsEncoded = $this->metricsBuilder->build(); + if (empty($metricsEncoded)) { + return ""; + } + + return "m/" . $metricsEncoded; + } +} diff --git a/3rdparty/aws/aws-sdk-php/src/Waiter.php b/3rdparty/aws/aws-sdk-php/src/Waiter.php new file mode 100644 index 00000000..59abdcca --- /dev/null +++ b/3rdparty/aws/aws-sdk-php/src/Waiter.php @@ -0,0 +1,285 @@ + 0, 'before' => null]; + + /** @var array Required configuration options. */ + private static $required = [ + 'acceptors', + 'delay', + 'maxAttempts', + 'operation', + ]; + + /** + * The array of configuration options include: + * + * - acceptors: (array) Array of acceptor options + * - delay: (int) Number of seconds to delay between attempts + * - maxAttempts: (int) Maximum number of attempts before failing + * - operation: (string) Name of the API operation to use for polling + * - before: (callable) Invoked before attempts. Accepts command and tries. + * + * @param AwsClientInterface $client Client used to execute commands. + * @param string $name Waiter name. + * @param array $args Command arguments. + * @param array $config Waiter config that overrides defaults. + * + * @throws \InvalidArgumentException if the configuration is incomplete. + */ + public function __construct( + AwsClientInterface $client, + $name, + array $args = [], + array $config = [] + ) { + $this->client = $client; + $this->name = $name; + $this->args = $args; + + // Prepare and validate config. + $this->config = $config + self::$defaults; + foreach (self::$required as $key) { + if (!isset($this->config[$key])) { + throw new \InvalidArgumentException( + 'The provided waiter configuration was incomplete.' + ); + } + } + if ($this->config['before'] && !is_callable($this->config['before'])) { + throw new \InvalidArgumentException( + 'The provided "before" callback is not callable.' + ); + } + MetricsBuilder::appendMetricsCaptureMiddleware( + $this->client->getHandlerList(), + MetricsBuilder::WAITER + ); + } + + /** + * @return Coroutine + */ + public function promise(): PromiseInterface + { + return Coroutine::of(function () { + $name = $this->config['operation']; + for ($state = 'retry', $attempt = 1; $state === 'retry'; $attempt++) { + // Execute the operation. + $args = $this->getArgsForAttempt($attempt); + $command = $this->client->getCommand($name, $args); + try { + if ($this->config['before']) { + $this->config['before']($command, $attempt); + } + $result = (yield $this->client->executeAsync($command)); + } catch (AwsException $e) { + $result = $e; + } + + // Determine the waiter's state and what to do next. + $state = $this->determineState($result); + if ($state === 'success') { + yield $command; + } elseif ($state === 'failed') { + $msg = "The {$this->name} waiter entered a failure state."; + if ($result instanceof \Exception) { + $msg .= ' Reason: ' . $result->getMessage(); + } + yield new RejectedPromise(new \RuntimeException($msg)); + } elseif ($state === 'retry' + && $attempt >= $this->config['maxAttempts'] + ) { + $state = 'failed'; + yield new RejectedPromise(new \RuntimeException( + "The {$this->name} waiter failed after attempt #{$attempt}." + )); + } + } + }); + } + + /** + * Gets the operation arguments for the attempt, including the delay. + * + * @param $attempt Number of the current attempt. + * + * @return mixed integer + */ + private function getArgsForAttempt($attempt) + { + $args = $this->args; + + // Determine the delay. + $delay = ($attempt === 1) + ? $this->config['initDelay'] + : $this->config['delay']; + if (is_callable($delay)) { + $delay = $delay($attempt); + } + + // Set the delay. (Note: handlers except delay in milliseconds.) + if (!isset($args['@http'])) { + $args['@http'] = []; + } + $args['@http']['delay'] = $delay * 1000; + + return $args; + } + + /** + * Determines the state of the waiter attempt, based on the result of + * polling the resource. A waiter can have the state of "success", "failed", + * or "retry". + * + * @param mixed $result + * + * @return string Will be "success", "failed", or "retry" + */ + private function determineState($result) + { + foreach ($this->config['acceptors'] as $acceptor) { + $matcher = 'matches' . ucfirst($acceptor['matcher']); + if ($this->{$matcher}($result, $acceptor)) { + return $acceptor['state']; + } + } + + return $result instanceof \Exception ? 'failed' : 'retry'; + } + + /** + * @param Result $result Result or exception. + * @param array $acceptor Acceptor configuration being checked. + * + * @return bool + */ + private function matchesPath($result, array $acceptor) + { + return $result instanceof ResultInterface + && $acceptor['expected'] === $result->search($acceptor['argument']); + } + + /** + * @param Result $result Result or exception. + * @param array $acceptor Acceptor configuration being checked. + * + * @return bool + */ + private function matchesPathAll($result, array $acceptor) + { + if (!($result instanceof ResultInterface)) { + return false; + } + + $actuals = $result->search($acceptor['argument']) ?: []; + // If is empty or not evaluates to an array it must return false. + if (empty($actuals) || !is_array($actuals)) { + return false; + } + + foreach ($actuals as $actual) { + if ($actual != $acceptor['expected']) { + return false; + } + } + + return true; + } + + /** + * @param Result $result Result or exception. + * @param array $acceptor Acceptor configuration being checked. + * + * @return bool + */ + private function matchesPathAny($result, array $acceptor) + { + if (!($result instanceof ResultInterface)) { + return false; + } + + $actuals = $result->search($acceptor['argument']) ?: []; + // If is empty or not evaluates to an array it must return false. + if (empty($actuals) || !is_array($actuals)) { + return false; + } + + return in_array($acceptor['expected'], $actuals); + } + + /** + * @param Result $result Result or exception. + * @param array $acceptor Acceptor configuration being checked. + * + * @return bool + */ + private function matchesStatus($result, array $acceptor) + { + if ($result instanceof ResultInterface) { + return $acceptor['expected'] == $result['@metadata']['statusCode']; + } + + if ($result instanceof AwsException && $response = $result->getResponse()) { + return $acceptor['expected'] == $response->getStatusCode(); + } + + return false; + } + + /** + * @param Result $result Result or exception. + * @param array $acceptor Acceptor configuration being checked. + * + * @return bool + */ + private function matchesError($result, array $acceptor) + { + // If expected is true then the $result should be an instance of + // AwsException, otherwise it should not. + if (isset($acceptor['expected']) && is_bool($acceptor['expected'])) { + return $acceptor['expected'] === ($result instanceof AwsException); + } + + if ($result instanceof AwsException) { + return $result->isConnectionError() + || $result->getAwsErrorCode() == $acceptor['expected']; + } + + return false; + } +} diff --git a/3rdparty/aws/aws-sdk-php/src/WrappedHttpHandler.php b/3rdparty/aws/aws-sdk-php/src/WrappedHttpHandler.php new file mode 100644 index 00000000..1c602de4 --- /dev/null +++ b/3rdparty/aws/aws-sdk-php/src/WrappedHttpHandler.php @@ -0,0 +1,208 @@ +httpHandler = $httpHandler; + $this->parser = $parser; + $this->errorParser = $errorParser; + $this->exceptionClass = $exceptionClass; + $this->collectStats = $collectStats; + } + + /** + * Calls the simpler HTTP specific handler and wraps the returned promise + * with AWS specific values (e.g., a result object or AWS exception). + * + * @param CommandInterface $command Command being executed. + * @param RequestInterface $request Request to send. + * + * @return Promise\PromiseInterface + */ + public function __invoke( + CommandInterface $command, + RequestInterface $request + ) { + $fn = $this->httpHandler; + $options = $command['@http'] ?: []; + $stats = []; + if ($this->collectStats || !empty($options['collect_stats'])) { + $options['http_stats_receiver'] = static function ( + array $transferStats + ) use (&$stats) { + $stats = $transferStats; + }; + } elseif (isset($options['http_stats_receiver'])) { + throw new \InvalidArgumentException('Providing a custom HTTP stats' + . ' receiver to Aws\WrappedHttpHandler is not supported.'); + } + + return Promise\Create::promiseFor($fn($request, $options)) + ->then( + function ( + ResponseInterface $res + ) use ($command, $request, &$stats) { + return $this->parseResponse($command, $request, $res, $stats); + }, + function ($err) use ($request, $command, &$stats) { + if (is_array($err)) { + $err = $this->parseError( + $err, + $request, + $command, + $stats + ); + } + return new Promise\RejectedPromise($err); + } + ); + } + + /** + * @param CommandInterface $command + * @param RequestInterface $request + * @param ResponseInterface $response + * @param array $stats + * + * @return ResultInterface + */ + private function parseResponse( + CommandInterface $command, + RequestInterface $request, + ResponseInterface $response, + array $stats + ) { + $parser = $this->parser; + $status = $response->getStatusCode(); + $result = $status < 300 + ? $parser($command, $response) + : new Result(); + + $metadata = [ + 'statusCode' => $status, + 'effectiveUri' => (string) $request->getUri(), + 'headers' => [], + 'transferStats' => [], + ]; + if (!empty($stats)) { + $metadata['transferStats']['http'] = [$stats]; + } + + // Bring headers into the metadata array. + foreach ($response->getHeaders() as $name => $values) { + $metadata['headers'][strtolower($name)] = $values[0]; + } + + $result['@metadata'] = $metadata; + + return $result; + } + + /** + * Parses a rejection into an AWS error. + * + * @param array $err Rejection error array. + * @param RequestInterface $request Request that was sent. + * @param CommandInterface $command Command being sent. + * @param array $stats Transfer statistics + * + * @return \Exception + */ + private function parseError( + array $err, + RequestInterface $request, + CommandInterface $command, + array $stats + ) { + if (!isset($err['exception'])) { + throw new \RuntimeException('The HTTP handler was rejected without an "exception" key value pair.'); + } + + $serviceError = "AWS HTTP error: " . $err['exception']->getMessage(); + + if (!isset($err['response'])) { + $parts = ['response' => null]; + } else { + try { + $parts = call_user_func( + $this->errorParser, + $err['response'], + $command + ); + $serviceError .= " {$parts['code']} ({$parts['type']}): " + . "{$parts['message']} - " . $err['response']->getBody(); + } catch (ParserException $e) { + $parts = []; + $serviceError .= ' Unable to parse error information from ' + . "response - {$e->getMessage()}"; + } + + $parts['response'] = $err['response']; + } + + $parts['exception'] = $err['exception']; + $parts['request'] = $request; + $parts['connection_error'] = !empty($err['connection_error']); + $parts['transfer_stats'] = $stats; + + return new $this->exceptionClass( + sprintf( + 'Error executing "%s" on "%s"; %s', + $command->getName(), + $request->getUri(), + $serviceError + ), + $command, + $parts, + $err['exception'] + ); + } +} diff --git a/3rdparty/aws/aws-sdk-php/src/functions.php b/3rdparty/aws/aws-sdk-php/src/functions.php new file mode 100644 index 00000000..166e7493 --- /dev/null +++ b/3rdparty/aws/aws-sdk-php/src/functions.php @@ -0,0 +1,573 @@ + true, '..' => true]; + $pathLen = strlen($path) + 1; + $iterator = dir_iterator($path, $context); + $queue = []; + do { + while ($iterator->valid()) { + $file = $iterator->current(); + $iterator->next(); + if (isset($invalid[basename($file)])) { + continue; + } + $fullPath = "{$path}/{$file}"; + yield $fullPath; + if (is_dir($fullPath)) { + $queue[] = $iterator; + $iterator = map( + dir_iterator($fullPath, $context), + function ($file) use ($fullPath, $pathLen) { + return substr("{$fullPath}/{$file}", $pathLen); + } + ); + continue; + } + } + $iterator = array_pop($queue); + } while ($iterator); +} + +//----------------------------------------------------------------------------- +// Misc. functions. +//----------------------------------------------------------------------------- + +/** + * Debug function used to describe the provided value type and class. + * + * @param mixed $input + * + * @return string Returns a string containing the type of the variable and + * if a class is provided, the class name. + */ +function describe_type($input) +{ + switch (gettype($input)) { + case 'object': + return 'object(' . get_class($input) . ')'; + case 'array': + return 'array(' . count($input) . ')'; + default: + ob_start(); + var_dump($input); + // normalize float vs double + return str_replace('double(', 'float(', rtrim(ob_get_clean())); + } +} + +/** + * Creates a default HTTP handler based on the available clients. + * + * @return callable + */ +function default_http_handler() +{ + return new \Aws\Handler\Guzzle\GuzzleHandler(); +} + +/** + * Gets the default user agent string depending on the Guzzle version + * + * @return string + */ +function default_user_agent() +{ + return Utils::defaultUserAgent(); +} + +/** + * Serialize a request for a command but do not send it. + * + * Returns a promise that is fulfilled with the serialized request. + * + * @param CommandInterface $command Command to serialize. + * + * @return RequestInterface + * @throws \RuntimeException + */ +function serialize(CommandInterface $command) +{ + $request = null; + $handlerList = $command->getHandlerList(); + + // Return a mock result. + $handlerList->setHandler( + function (CommandInterface $_, RequestInterface $r) use (&$request) { + $request = $r; + return new FulfilledPromise(new Result([])); + } + ); + + call_user_func($handlerList->resolve(), $command)->wait(); + if (!$request instanceof RequestInterface) { + throw new \RuntimeException( + 'Calling handler did not serialize request' + ); + } + + return $request; +} + +/** + * Retrieves data for a service from the SDK's service manifest file. + * + * Manifest data is stored statically, so it does not need to be loaded more + * than once per process. The JSON data is also cached in opcache. + * + * @param string $service Case-insensitive namespace or endpoint prefix of the + * service for which you are retrieving manifest data. + * + * @return array + * @throws \InvalidArgumentException if the service is not supported. + */ +function manifest($service = null) +{ + // Load the manifest and create aliases for lowercased namespaces + static $manifest = []; + static $aliases = []; + if (empty($manifest)) { + $manifest = load_compiled_json(__DIR__ . '/data/manifest.json'); + foreach ($manifest as $endpoint => $info) { + $alias = strtolower($info['namespace']); + if ($alias !== $endpoint) { + $aliases[$alias] = $endpoint; + } + } + } + + // If no service specified, then return the whole manifest. + if ($service === null) { + return $manifest; + } + + // Look up the service's info in the manifest data. + $service = strtolower($service); + if (isset($manifest[$service])) { + return $manifest[$service] + ['endpoint' => $service]; + } + + if (isset($aliases[$service])) { + return manifest($aliases[$service]); + } + + throw new \InvalidArgumentException( + "The service \"{$service}\" is not provided by the AWS SDK for PHP." + ); +} + +/** + * Checks if supplied parameter is a valid hostname + * + * @param string $hostname + * @return bool + */ +function is_valid_hostname($hostname) +{ + return ( + preg_match("/^([a-z\d](-*[a-z\d])*)(\.([a-z\d](-*[a-z\d])*))*\.?$/i", $hostname) + && preg_match("/^.{1,253}$/", $hostname) + && preg_match("/^[^\.]{1,63}(\.[^\.]{0,63})*$/", $hostname) + ); +} + +/** + * Checks if supplied parameter is a valid host label + * + * @param $label + * @return bool + */ +function is_valid_hostlabel($label) +{ + return preg_match("/^(?!-)[a-zA-Z0-9-]{1,63}(? +* +* For the full copyright and license information, please view the LICENSE +* file that was distributed with this source code. +*/ + +namespace bantu\IniGetWrapper; + +/** +* Wrapper class around built-in ini_get() function. +* +* Provides easier handling of the different interpretations of ini values. +*/ +class IniGetWrapper +{ + /** + * Simple wrapper around ini_get() + * See http://php.net/manual/en/function.ini-get.php + * + * @param string $varname The configuration option name. + * @return null|string Null if configuration option does not exist. + * The configuration option value (string) otherwise. + */ + public function get($varname) + { + $value = $this->getPhp($varname); + return $value === false ? null : $value; + } + + /** + * Gets the configuration option value as a trimmed string. + * + * @param string $varname The configuration option name. + * @return null|string Null if configuration option does not exist. + * The configuration option value (string) otherwise. + */ + public function getString($varname) + { + $value = $this->get($varname); + return $value === null ? null : trim($value); + } + + /** + * Gets configuration option value as a boolean. + * Interprets the string value 'off' as false. + * + * @param string $varname The configuration option name. + * @return null|bool Null if configuration option does not exist. + * False if configuration option is disabled. + * True otherwise. + */ + public function getBool($varname) + { + $value = $this->getString($varname); + return $value === null ? null : $value && strtolower($value) !== 'off'; + } + + /** + * Gets configuration option value as an integer. + * + * @param string $varname The configuration option name. + * @return null|int|float Null if configuration option does not exist or is not numeric. + * The configuration option value (integer or float) otherwise. + */ + public function getNumeric($varname) + { + $value = $this->getString($varname); + return is_numeric($value) ? $value + 0 : null; + } + + /** + * Gets configuration option value in bytes. + * Converts strings like '128M' to bytes (integer or float). + * + * @param string $varname The configuration option name. + * @return null|int|float Null if configuration option does not exist or is not well-formed. + * The configuration option value as bytes (integer or float) otherwise. + */ + public function getBytes($varname) + { + $value = $this->getString($varname); + + if ($value === null) { + return null; + } + + if (is_numeric($value)) { + // Already in bytes. + return $value + 0; + } + + if (strlen($value) < 2 || strlen($value) < 3 && $value[0] === '-') { + // Either a single character + // or two characters where the first one is a minus. + return null; + } + + // Split string into numeric value and unit. + $value_numeric = substr($value, 0, -1); + if (!is_numeric($value_numeric)) { + return null; + } + + switch (strtolower($value[strlen($value) - 1])) { + case 'g': + $value_numeric *= 1024; + // no break + case 'm': + $value_numeric *= 1024; + // no break + case 'k': + $value_numeric *= 1024; + break; + + default: + // It's not already in bytes (and thus numeric) + // and does not carry a unit. + return null; + } + + return $value_numeric; + } + + /** + * Gets configuration option value as a list (array). + * Converts comma-separated string into list (array). + * + * @param string $varname The configuration option name. + * @return null|array Null if configuration option does not exist. + * The configuration option value as a list (array) otherwise. + */ + public function getList($varname) + { + $value = $this->getString($varname); + return $value === null ? null : explode(',', $value); + } + + /** + * Checks whether a list contains a given element (string). + * + * @param string $varname The configuration option name. + * @param string $needle The element to check whether it is contained in the list. + * @return null|bool Null if configuration option does not exist. + * Whether $needle is contained in the list otherwise. + */ + public function listContains($varname, $needle) + { + $list = $this->getList($varname); + return $list === null ? null : in_array($needle, $list, true); + } + + /** + * @param string $varname The configuration option name. + */ + protected function getPhp($varname) + { + return ini_get($varname); + } +} diff --git a/3rdparty/brick/math/LICENSE b/3rdparty/brick/math/LICENSE new file mode 100644 index 00000000..f9b724f0 --- /dev/null +++ b/3rdparty/brick/math/LICENSE @@ -0,0 +1,20 @@ +The MIT License (MIT) + +Copyright (c) 2013-present Benjamin Morel + +Permission is hereby granted, free of charge, to any person obtaining a copy of +this software and associated documentation files (the "Software"), to deal in +the Software without restriction, including without limitation the rights to +use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of +the Software, and to permit persons to whom the Software is furnished to do so, +subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS +FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR +COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER +IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN +CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. diff --git a/3rdparty/brick/math/src/BigDecimal.php b/3rdparty/brick/math/src/BigDecimal.php new file mode 100644 index 00000000..31d22ab3 --- /dev/null +++ b/3rdparty/brick/math/src/BigDecimal.php @@ -0,0 +1,754 @@ +value = $value; + $this->scale = $scale; + } + + /** + * @psalm-pure + */ + protected static function from(BigNumber $number): static + { + return $number->toBigDecimal(); + } + + /** + * Creates a BigDecimal from an unscaled value and a scale. + * + * Example: `(12345, 3)` will result in the BigDecimal `12.345`. + * + * @param BigNumber|int|float|string $value The unscaled value. Must be convertible to a BigInteger. + * @param int $scale The scale of the number, positive or zero. + * + * @throws \InvalidArgumentException If the scale is negative. + * + * @psalm-pure + */ + public static function ofUnscaledValue(BigNumber|int|float|string $value, int $scale = 0) : BigDecimal + { + if ($scale < 0) { + throw new \InvalidArgumentException('The scale cannot be negative.'); + } + + return new BigDecimal((string) BigInteger::of($value), $scale); + } + + /** + * Returns a BigDecimal representing zero, with a scale of zero. + * + * @psalm-pure + */ + public static function zero() : BigDecimal + { + /** + * @psalm-suppress ImpureStaticVariable + * @var BigDecimal|null $zero + */ + static $zero; + + if ($zero === null) { + $zero = new BigDecimal('0'); + } + + return $zero; + } + + /** + * Returns a BigDecimal representing one, with a scale of zero. + * + * @psalm-pure + */ + public static function one() : BigDecimal + { + /** + * @psalm-suppress ImpureStaticVariable + * @var BigDecimal|null $one + */ + static $one; + + if ($one === null) { + $one = new BigDecimal('1'); + } + + return $one; + } + + /** + * Returns a BigDecimal representing ten, with a scale of zero. + * + * @psalm-pure + */ + public static function ten() : BigDecimal + { + /** + * @psalm-suppress ImpureStaticVariable + * @var BigDecimal|null $ten + */ + static $ten; + + if ($ten === null) { + $ten = new BigDecimal('10'); + } + + return $ten; + } + + /** + * Returns the sum of this number and the given one. + * + * The result has a scale of `max($this->scale, $that->scale)`. + * + * @param BigNumber|int|float|string $that The number to add. Must be convertible to a BigDecimal. + * + * @throws MathException If the number is not valid, or is not convertible to a BigDecimal. + */ + public function plus(BigNumber|int|float|string $that) : BigDecimal + { + $that = BigDecimal::of($that); + + if ($that->value === '0' && $that->scale <= $this->scale) { + return $this; + } + + if ($this->value === '0' && $this->scale <= $that->scale) { + return $that; + } + + [$a, $b] = $this->scaleValues($this, $that); + + $value = Calculator::get()->add($a, $b); + $scale = $this->scale > $that->scale ? $this->scale : $that->scale; + + return new BigDecimal($value, $scale); + } + + /** + * Returns the difference of this number and the given one. + * + * The result has a scale of `max($this->scale, $that->scale)`. + * + * @param BigNumber|int|float|string $that The number to subtract. Must be convertible to a BigDecimal. + * + * @throws MathException If the number is not valid, or is not convertible to a BigDecimal. + */ + public function minus(BigNumber|int|float|string $that) : BigDecimal + { + $that = BigDecimal::of($that); + + if ($that->value === '0' && $that->scale <= $this->scale) { + return $this; + } + + [$a, $b] = $this->scaleValues($this, $that); + + $value = Calculator::get()->sub($a, $b); + $scale = $this->scale > $that->scale ? $this->scale : $that->scale; + + return new BigDecimal($value, $scale); + } + + /** + * Returns the product of this number and the given one. + * + * The result has a scale of `$this->scale + $that->scale`. + * + * @param BigNumber|int|float|string $that The multiplier. Must be convertible to a BigDecimal. + * + * @throws MathException If the multiplier is not a valid number, or is not convertible to a BigDecimal. + */ + public function multipliedBy(BigNumber|int|float|string $that) : BigDecimal + { + $that = BigDecimal::of($that); + + if ($that->value === '1' && $that->scale === 0) { + return $this; + } + + if ($this->value === '1' && $this->scale === 0) { + return $that; + } + + $value = Calculator::get()->mul($this->value, $that->value); + $scale = $this->scale + $that->scale; + + return new BigDecimal($value, $scale); + } + + /** + * Returns the result of the division of this number by the given one, at the given scale. + * + * @param BigNumber|int|float|string $that The divisor. + * @param int|null $scale The desired scale, or null to use the scale of this number. + * @param RoundingMode $roundingMode An optional rounding mode, defaults to UNNECESSARY. + * + * @throws \InvalidArgumentException If the scale or rounding mode is invalid. + * @throws MathException If the number is invalid, is zero, or rounding was necessary. + */ + public function dividedBy(BigNumber|int|float|string $that, ?int $scale = null, RoundingMode $roundingMode = RoundingMode::UNNECESSARY) : BigDecimal + { + $that = BigDecimal::of($that); + + if ($that->isZero()) { + throw DivisionByZeroException::divisionByZero(); + } + + if ($scale === null) { + $scale = $this->scale; + } elseif ($scale < 0) { + throw new \InvalidArgumentException('Scale cannot be negative.'); + } + + if ($that->value === '1' && $that->scale === 0 && $scale === $this->scale) { + return $this; + } + + $p = $this->valueWithMinScale($that->scale + $scale); + $q = $that->valueWithMinScale($this->scale - $scale); + + $result = Calculator::get()->divRound($p, $q, $roundingMode); + + return new BigDecimal($result, $scale); + } + + /** + * Returns the exact result of the division of this number by the given one. + * + * The scale of the result is automatically calculated to fit all the fraction digits. + * + * @param BigNumber|int|float|string $that The divisor. Must be convertible to a BigDecimal. + * + * @throws MathException If the divisor is not a valid number, is not convertible to a BigDecimal, is zero, + * or the result yields an infinite number of digits. + */ + public function exactlyDividedBy(BigNumber|int|float|string $that) : BigDecimal + { + $that = BigDecimal::of($that); + + if ($that->value === '0') { + throw DivisionByZeroException::divisionByZero(); + } + + [, $b] = $this->scaleValues($this, $that); + + $d = \rtrim($b, '0'); + $scale = \strlen($b) - \strlen($d); + + $calculator = Calculator::get(); + + foreach ([5, 2] as $prime) { + for (;;) { + $lastDigit = (int) $d[-1]; + + if ($lastDigit % $prime !== 0) { + break; + } + + $d = $calculator->divQ($d, (string) $prime); + $scale++; + } + } + + return $this->dividedBy($that, $scale)->stripTrailingZeros(); + } + + /** + * Returns this number exponentiated to the given value. + * + * The result has a scale of `$this->scale * $exponent`. + * + * @throws \InvalidArgumentException If the exponent is not in the range 0 to 1,000,000. + */ + public function power(int $exponent) : BigDecimal + { + if ($exponent === 0) { + return BigDecimal::one(); + } + + if ($exponent === 1) { + return $this; + } + + if ($exponent < 0 || $exponent > Calculator::MAX_POWER) { + throw new \InvalidArgumentException(\sprintf( + 'The exponent %d is not in the range 0 to %d.', + $exponent, + Calculator::MAX_POWER + )); + } + + return new BigDecimal(Calculator::get()->pow($this->value, $exponent), $this->scale * $exponent); + } + + /** + * Returns the quotient of the division of this number by the given one. + * + * The quotient has a scale of `0`. + * + * @param BigNumber|int|float|string $that The divisor. Must be convertible to a BigDecimal. + * + * @throws MathException If the divisor is not a valid decimal number, or is zero. + */ + public function quotient(BigNumber|int|float|string $that) : BigDecimal + { + $that = BigDecimal::of($that); + + if ($that->isZero()) { + throw DivisionByZeroException::divisionByZero(); + } + + $p = $this->valueWithMinScale($that->scale); + $q = $that->valueWithMinScale($this->scale); + + $quotient = Calculator::get()->divQ($p, $q); + + return new BigDecimal($quotient, 0); + } + + /** + * Returns the remainder of the division of this number by the given one. + * + * The remainder has a scale of `max($this->scale, $that->scale)`. + * + * @param BigNumber|int|float|string $that The divisor. Must be convertible to a BigDecimal. + * + * @throws MathException If the divisor is not a valid decimal number, or is zero. + */ + public function remainder(BigNumber|int|float|string $that) : BigDecimal + { + $that = BigDecimal::of($that); + + if ($that->isZero()) { + throw DivisionByZeroException::divisionByZero(); + } + + $p = $this->valueWithMinScale($that->scale); + $q = $that->valueWithMinScale($this->scale); + + $remainder = Calculator::get()->divR($p, $q); + + $scale = $this->scale > $that->scale ? $this->scale : $that->scale; + + return new BigDecimal($remainder, $scale); + } + + /** + * Returns the quotient and remainder of the division of this number by the given one. + * + * The quotient has a scale of `0`, and the remainder has a scale of `max($this->scale, $that->scale)`. + * + * @param BigNumber|int|float|string $that The divisor. Must be convertible to a BigDecimal. + * + * @return BigDecimal[] An array containing the quotient and the remainder. + * + * @psalm-return array{BigDecimal, BigDecimal} + * + * @throws MathException If the divisor is not a valid decimal number, or is zero. + */ + public function quotientAndRemainder(BigNumber|int|float|string $that) : array + { + $that = BigDecimal::of($that); + + if ($that->isZero()) { + throw DivisionByZeroException::divisionByZero(); + } + + $p = $this->valueWithMinScale($that->scale); + $q = $that->valueWithMinScale($this->scale); + + [$quotient, $remainder] = Calculator::get()->divQR($p, $q); + + $scale = $this->scale > $that->scale ? $this->scale : $that->scale; + + $quotient = new BigDecimal($quotient, 0); + $remainder = new BigDecimal($remainder, $scale); + + return [$quotient, $remainder]; + } + + /** + * Returns the square root of this number, rounded down to the given number of decimals. + * + * @throws \InvalidArgumentException If the scale is negative. + * @throws NegativeNumberException If this number is negative. + */ + public function sqrt(int $scale) : BigDecimal + { + if ($scale < 0) { + throw new \InvalidArgumentException('Scale cannot be negative.'); + } + + if ($this->value === '0') { + return new BigDecimal('0', $scale); + } + + if ($this->value[0] === '-') { + throw new NegativeNumberException('Cannot calculate the square root of a negative number.'); + } + + $value = $this->value; + $addDigits = 2 * $scale - $this->scale; + + if ($addDigits > 0) { + // add zeros + $value .= \str_repeat('0', $addDigits); + } elseif ($addDigits < 0) { + // trim digits + if (-$addDigits >= \strlen($this->value)) { + // requesting a scale too low, will always yield a zero result + return new BigDecimal('0', $scale); + } + + $value = \substr($value, 0, $addDigits); + } + + $value = Calculator::get()->sqrt($value); + + return new BigDecimal($value, $scale); + } + + /** + * Returns a copy of this BigDecimal with the decimal point moved $n places to the left. + */ + public function withPointMovedLeft(int $n) : BigDecimal + { + if ($n === 0) { + return $this; + } + + if ($n < 0) { + return $this->withPointMovedRight(-$n); + } + + return new BigDecimal($this->value, $this->scale + $n); + } + + /** + * Returns a copy of this BigDecimal with the decimal point moved $n places to the right. + */ + public function withPointMovedRight(int $n) : BigDecimal + { + if ($n === 0) { + return $this; + } + + if ($n < 0) { + return $this->withPointMovedLeft(-$n); + } + + $value = $this->value; + $scale = $this->scale - $n; + + if ($scale < 0) { + if ($value !== '0') { + $value .= \str_repeat('0', -$scale); + } + $scale = 0; + } + + return new BigDecimal($value, $scale); + } + + /** + * Returns a copy of this BigDecimal with any trailing zeros removed from the fractional part. + */ + public function stripTrailingZeros() : BigDecimal + { + if ($this->scale === 0) { + return $this; + } + + $trimmedValue = \rtrim($this->value, '0'); + + if ($trimmedValue === '') { + return BigDecimal::zero(); + } + + $trimmableZeros = \strlen($this->value) - \strlen($trimmedValue); + + if ($trimmableZeros === 0) { + return $this; + } + + if ($trimmableZeros > $this->scale) { + $trimmableZeros = $this->scale; + } + + $value = \substr($this->value, 0, -$trimmableZeros); + $scale = $this->scale - $trimmableZeros; + + return new BigDecimal($value, $scale); + } + + /** + * Returns the absolute value of this number. + */ + public function abs() : BigDecimal + { + return $this->isNegative() ? $this->negated() : $this; + } + + /** + * Returns the negated value of this number. + */ + public function negated() : BigDecimal + { + return new BigDecimal(Calculator::get()->neg($this->value), $this->scale); + } + + public function compareTo(BigNumber|int|float|string $that) : int + { + $that = BigNumber::of($that); + + if ($that instanceof BigInteger) { + $that = $that->toBigDecimal(); + } + + if ($that instanceof BigDecimal) { + [$a, $b] = $this->scaleValues($this, $that); + + return Calculator::get()->cmp($a, $b); + } + + return - $that->compareTo($this); + } + + public function getSign() : int + { + return ($this->value === '0') ? 0 : (($this->value[0] === '-') ? -1 : 1); + } + + public function getUnscaledValue() : BigInteger + { + return self::newBigInteger($this->value); + } + + public function getScale() : int + { + return $this->scale; + } + + /** + * Returns a string representing the integral part of this decimal number. + * + * Example: `-123.456` => `-123`. + */ + public function getIntegralPart() : string + { + if ($this->scale === 0) { + return $this->value; + } + + $value = $this->getUnscaledValueWithLeadingZeros(); + + return \substr($value, 0, -$this->scale); + } + + /** + * Returns a string representing the fractional part of this decimal number. + * + * If the scale is zero, an empty string is returned. + * + * Examples: `-123.456` => '456', `123` => ''. + */ + public function getFractionalPart() : string + { + if ($this->scale === 0) { + return ''; + } + + $value = $this->getUnscaledValueWithLeadingZeros(); + + return \substr($value, -$this->scale); + } + + /** + * Returns whether this decimal number has a non-zero fractional part. + */ + public function hasNonZeroFractionalPart() : bool + { + return $this->getFractionalPart() !== \str_repeat('0', $this->scale); + } + + public function toBigInteger() : BigInteger + { + $zeroScaleDecimal = $this->scale === 0 ? $this : $this->dividedBy(1, 0); + + return self::newBigInteger($zeroScaleDecimal->value); + } + + public function toBigDecimal() : BigDecimal + { + return $this; + } + + public function toBigRational() : BigRational + { + $numerator = self::newBigInteger($this->value); + $denominator = self::newBigInteger('1' . \str_repeat('0', $this->scale)); + + return self::newBigRational($numerator, $denominator, false); + } + + public function toScale(int $scale, RoundingMode $roundingMode = RoundingMode::UNNECESSARY) : BigDecimal + { + if ($scale === $this->scale) { + return $this; + } + + return $this->dividedBy(BigDecimal::one(), $scale, $roundingMode); + } + + public function toInt() : int + { + return $this->toBigInteger()->toInt(); + } + + public function toFloat() : float + { + return (float) (string) $this; + } + + public function __toString() : string + { + if ($this->scale === 0) { + return $this->value; + } + + $value = $this->getUnscaledValueWithLeadingZeros(); + + return \substr($value, 0, -$this->scale) . '.' . \substr($value, -$this->scale); + } + + /** + * This method is required for serializing the object and SHOULD NOT be accessed directly. + * + * @internal + * + * @return array{value: string, scale: int} + */ + public function __serialize(): array + { + return ['value' => $this->value, 'scale' => $this->scale]; + } + + /** + * This method is only here to allow unserializing the object and cannot be accessed directly. + * + * @internal + * @psalm-suppress RedundantPropertyInitializationCheck + * + * @param array{value: string, scale: int} $data + * + * @throws \LogicException + */ + public function __unserialize(array $data): void + { + if (isset($this->value)) { + throw new \LogicException('__unserialize() is an internal function, it must not be called directly.'); + } + + $this->value = $data['value']; + $this->scale = $data['scale']; + } + + /** + * Puts the internal values of the given decimal numbers on the same scale. + * + * @return array{string, string} The scaled integer values of $x and $y. + */ + private function scaleValues(BigDecimal $x, BigDecimal $y) : array + { + $a = $x->value; + $b = $y->value; + + if ($b !== '0' && $x->scale > $y->scale) { + $b .= \str_repeat('0', $x->scale - $y->scale); + } elseif ($a !== '0' && $x->scale < $y->scale) { + $a .= \str_repeat('0', $y->scale - $x->scale); + } + + return [$a, $b]; + } + + private function valueWithMinScale(int $scale) : string + { + $value = $this->value; + + if ($this->value !== '0' && $scale > $this->scale) { + $value .= \str_repeat('0', $scale - $this->scale); + } + + return $value; + } + + /** + * Adds leading zeros if necessary to the unscaled value to represent the full decimal number. + */ + private function getUnscaledValueWithLeadingZeros() : string + { + $value = $this->value; + $targetLength = $this->scale + 1; + $negative = ($value[0] === '-'); + $length = \strlen($value); + + if ($negative) { + $length--; + } + + if ($length >= $targetLength) { + return $this->value; + } + + if ($negative) { + $value = \substr($value, 1); + } + + $value = \str_pad($value, $targetLength, '0', STR_PAD_LEFT); + + if ($negative) { + $value = '-' . $value; + } + + return $value; + } +} diff --git a/3rdparty/brick/math/src/BigInteger.php b/3rdparty/brick/math/src/BigInteger.php new file mode 100644 index 00000000..73dcc89a --- /dev/null +++ b/3rdparty/brick/math/src/BigInteger.php @@ -0,0 +1,1051 @@ +value = $value; + } + + /** + * @psalm-pure + */ + protected static function from(BigNumber $number): static + { + return $number->toBigInteger(); + } + + /** + * Creates a number from a string in a given base. + * + * The string can optionally be prefixed with the `+` or `-` sign. + * + * Bases greater than 36 are not supported by this method, as there is no clear consensus on which of the lowercase + * or uppercase characters should come first. Instead, this method accepts any base up to 36, and does not + * differentiate lowercase and uppercase characters, which are considered equal. + * + * For bases greater than 36, and/or custom alphabets, use the fromArbitraryBase() method. + * + * @param string $number The number to convert, in the given base. + * @param int $base The base of the number, between 2 and 36. + * + * @throws NumberFormatException If the number is empty, or contains invalid chars for the given base. + * @throws \InvalidArgumentException If the base is out of range. + * + * @psalm-pure + */ + public static function fromBase(string $number, int $base) : BigInteger + { + if ($number === '') { + throw new NumberFormatException('The number cannot be empty.'); + } + + if ($base < 2 || $base > 36) { + throw new \InvalidArgumentException(\sprintf('Base %d is not in range 2 to 36.', $base)); + } + + if ($number[0] === '-') { + $sign = '-'; + $number = \substr($number, 1); + } elseif ($number[0] === '+') { + $sign = ''; + $number = \substr($number, 1); + } else { + $sign = ''; + } + + if ($number === '') { + throw new NumberFormatException('The number cannot be empty.'); + } + + $number = \ltrim($number, '0'); + + if ($number === '') { + // The result will be the same in any base, avoid further calculation. + return BigInteger::zero(); + } + + if ($number === '1') { + // The result will be the same in any base, avoid further calculation. + return new BigInteger($sign . '1'); + } + + $pattern = '/[^' . \substr(Calculator::ALPHABET, 0, $base) . ']/'; + + if (\preg_match($pattern, \strtolower($number), $matches) === 1) { + throw new NumberFormatException(\sprintf('"%s" is not a valid character in base %d.', $matches[0], $base)); + } + + if ($base === 10) { + // The number is usable as is, avoid further calculation. + return new BigInteger($sign . $number); + } + + $result = Calculator::get()->fromBase($number, $base); + + return new BigInteger($sign . $result); + } + + /** + * Parses a string containing an integer in an arbitrary base, using a custom alphabet. + * + * Because this method accepts an alphabet with any character, including dash, it does not handle negative numbers. + * + * @param string $number The number to parse. + * @param string $alphabet The alphabet, for example '01' for base 2, or '01234567' for base 8. + * + * @throws NumberFormatException If the given number is empty or contains invalid chars for the given alphabet. + * @throws \InvalidArgumentException If the alphabet does not contain at least 2 chars. + * + * @psalm-pure + */ + public static function fromArbitraryBase(string $number, string $alphabet) : BigInteger + { + if ($number === '') { + throw new NumberFormatException('The number cannot be empty.'); + } + + $base = \strlen($alphabet); + + if ($base < 2) { + throw new \InvalidArgumentException('The alphabet must contain at least 2 chars.'); + } + + $pattern = '/[^' . \preg_quote($alphabet, '/') . ']/'; + + if (\preg_match($pattern, $number, $matches) === 1) { + throw NumberFormatException::charNotInAlphabet($matches[0]); + } + + $number = Calculator::get()->fromArbitraryBase($number, $alphabet, $base); + + return new BigInteger($number); + } + + /** + * Translates a string of bytes containing the binary representation of a BigInteger into a BigInteger. + * + * The input string is assumed to be in big-endian byte-order: the most significant byte is in the zeroth element. + * + * If `$signed` is true, the input is assumed to be in two's-complement representation, and the leading bit is + * interpreted as a sign bit. If `$signed` is false, the input is interpreted as an unsigned number, and the + * resulting BigInteger will always be positive or zero. + * + * This method can be used to retrieve a number exported by `toBytes()`, as long as the `$signed` flags match. + * + * @param string $value The byte string. + * @param bool $signed Whether to interpret as a signed number in two's-complement representation with a leading + * sign bit. + * + * @throws NumberFormatException If the string is empty. + */ + public static function fromBytes(string $value, bool $signed = true) : BigInteger + { + if ($value === '') { + throw new NumberFormatException('The byte string must not be empty.'); + } + + $twosComplement = false; + + if ($signed) { + $x = \ord($value[0]); + + if (($twosComplement = ($x >= 0x80))) { + $value = ~$value; + } + } + + $number = self::fromBase(\bin2hex($value), 16); + + if ($twosComplement) { + return $number->plus(1)->negated(); + } + + return $number; + } + + /** + * Generates a pseudo-random number in the range 0 to 2^numBits - 1. + * + * Using the default random bytes generator, this method is suitable for cryptographic use. + * + * @psalm-param (callable(int): string)|null $randomBytesGenerator + * + * @param int $numBits The number of bits. + * @param callable|null $randomBytesGenerator A function that accepts a number of bytes as an integer, and returns a + * string of random bytes of the given length. Defaults to the + * `random_bytes()` function. + * + * @throws \InvalidArgumentException If $numBits is negative. + */ + public static function randomBits(int $numBits, ?callable $randomBytesGenerator = null) : BigInteger + { + if ($numBits < 0) { + throw new \InvalidArgumentException('The number of bits cannot be negative.'); + } + + if ($numBits === 0) { + return BigInteger::zero(); + } + + if ($randomBytesGenerator === null) { + $randomBytesGenerator = random_bytes(...); + } + + /** @var int<1, max> $byteLength */ + $byteLength = \intdiv($numBits - 1, 8) + 1; + + $extraBits = ($byteLength * 8 - $numBits); + $bitmask = \chr(0xFF >> $extraBits); + + $randomBytes = $randomBytesGenerator($byteLength); + $randomBytes[0] = $randomBytes[0] & $bitmask; + + return self::fromBytes($randomBytes, false); + } + + /** + * Generates a pseudo-random number between `$min` and `$max`. + * + * Using the default random bytes generator, this method is suitable for cryptographic use. + * + * @psalm-param (callable(int): string)|null $randomBytesGenerator + * + * @param BigNumber|int|float|string $min The lower bound. Must be convertible to a BigInteger. + * @param BigNumber|int|float|string $max The upper bound. Must be convertible to a BigInteger. + * @param callable|null $randomBytesGenerator A function that accepts a number of bytes as an integer, + * and returns a string of random bytes of the given length. + * Defaults to the `random_bytes()` function. + * + * @throws MathException If one of the parameters cannot be converted to a BigInteger, + * or `$min` is greater than `$max`. + */ + public static function randomRange( + BigNumber|int|float|string $min, + BigNumber|int|float|string $max, + ?callable $randomBytesGenerator = null + ) : BigInteger { + $min = BigInteger::of($min); + $max = BigInteger::of($max); + + if ($min->isGreaterThan($max)) { + throw new MathException('$min cannot be greater than $max.'); + } + + if ($min->isEqualTo($max)) { + return $min; + } + + $diff = $max->minus($min); + $bitLength = $diff->getBitLength(); + + // try until the number is in range (50% to 100% chance of success) + do { + $randomNumber = self::randomBits($bitLength, $randomBytesGenerator); + } while ($randomNumber->isGreaterThan($diff)); + + return $randomNumber->plus($min); + } + + /** + * Returns a BigInteger representing zero. + * + * @psalm-pure + */ + public static function zero() : BigInteger + { + /** + * @psalm-suppress ImpureStaticVariable + * @var BigInteger|null $zero + */ + static $zero; + + if ($zero === null) { + $zero = new BigInteger('0'); + } + + return $zero; + } + + /** + * Returns a BigInteger representing one. + * + * @psalm-pure + */ + public static function one() : BigInteger + { + /** + * @psalm-suppress ImpureStaticVariable + * @var BigInteger|null $one + */ + static $one; + + if ($one === null) { + $one = new BigInteger('1'); + } + + return $one; + } + + /** + * Returns a BigInteger representing ten. + * + * @psalm-pure + */ + public static function ten() : BigInteger + { + /** + * @psalm-suppress ImpureStaticVariable + * @var BigInteger|null $ten + */ + static $ten; + + if ($ten === null) { + $ten = new BigInteger('10'); + } + + return $ten; + } + + public static function gcdMultiple(BigInteger $a, BigInteger ...$n): BigInteger + { + $result = $a; + + foreach ($n as $next) { + $result = $result->gcd($next); + + if ($result->isEqualTo(1)) { + return $result; + } + } + + return $result; + } + + /** + * Returns the sum of this number and the given one. + * + * @param BigNumber|int|float|string $that The number to add. Must be convertible to a BigInteger. + * + * @throws MathException If the number is not valid, or is not convertible to a BigInteger. + */ + public function plus(BigNumber|int|float|string $that) : BigInteger + { + $that = BigInteger::of($that); + + if ($that->value === '0') { + return $this; + } + + if ($this->value === '0') { + return $that; + } + + $value = Calculator::get()->add($this->value, $that->value); + + return new BigInteger($value); + } + + /** + * Returns the difference of this number and the given one. + * + * @param BigNumber|int|float|string $that The number to subtract. Must be convertible to a BigInteger. + * + * @throws MathException If the number is not valid, or is not convertible to a BigInteger. + */ + public function minus(BigNumber|int|float|string $that) : BigInteger + { + $that = BigInteger::of($that); + + if ($that->value === '0') { + return $this; + } + + $value = Calculator::get()->sub($this->value, $that->value); + + return new BigInteger($value); + } + + /** + * Returns the product of this number and the given one. + * + * @param BigNumber|int|float|string $that The multiplier. Must be convertible to a BigInteger. + * + * @throws MathException If the multiplier is not a valid number, or is not convertible to a BigInteger. + */ + public function multipliedBy(BigNumber|int|float|string $that) : BigInteger + { + $that = BigInteger::of($that); + + if ($that->value === '1') { + return $this; + } + + if ($this->value === '1') { + return $that; + } + + $value = Calculator::get()->mul($this->value, $that->value); + + return new BigInteger($value); + } + + /** + * Returns the result of the division of this number by the given one. + * + * @param BigNumber|int|float|string $that The divisor. Must be convertible to a BigInteger. + * @param RoundingMode $roundingMode An optional rounding mode, defaults to UNNECESSARY. + * + * @throws MathException If the divisor is not a valid number, is not convertible to a BigInteger, is zero, + * or RoundingMode::UNNECESSARY is used and the remainder is not zero. + */ + public function dividedBy(BigNumber|int|float|string $that, RoundingMode $roundingMode = RoundingMode::UNNECESSARY) : BigInteger + { + $that = BigInteger::of($that); + + if ($that->value === '1') { + return $this; + } + + if ($that->value === '0') { + throw DivisionByZeroException::divisionByZero(); + } + + $result = Calculator::get()->divRound($this->value, $that->value, $roundingMode); + + return new BigInteger($result); + } + + /** + * Returns this number exponentiated to the given value. + * + * @throws \InvalidArgumentException If the exponent is not in the range 0 to 1,000,000. + */ + public function power(int $exponent) : BigInteger + { + if ($exponent === 0) { + return BigInteger::one(); + } + + if ($exponent === 1) { + return $this; + } + + if ($exponent < 0 || $exponent > Calculator::MAX_POWER) { + throw new \InvalidArgumentException(\sprintf( + 'The exponent %d is not in the range 0 to %d.', + $exponent, + Calculator::MAX_POWER + )); + } + + return new BigInteger(Calculator::get()->pow($this->value, $exponent)); + } + + /** + * Returns the quotient of the division of this number by the given one. + * + * @param BigNumber|int|float|string $that The divisor. Must be convertible to a BigInteger. + * + * @throws DivisionByZeroException If the divisor is zero. + */ + public function quotient(BigNumber|int|float|string $that) : BigInteger + { + $that = BigInteger::of($that); + + if ($that->value === '1') { + return $this; + } + + if ($that->value === '0') { + throw DivisionByZeroException::divisionByZero(); + } + + $quotient = Calculator::get()->divQ($this->value, $that->value); + + return new BigInteger($quotient); + } + + /** + * Returns the remainder of the division of this number by the given one. + * + * The remainder, when non-zero, has the same sign as the dividend. + * + * @param BigNumber|int|float|string $that The divisor. Must be convertible to a BigInteger. + * + * @throws DivisionByZeroException If the divisor is zero. + */ + public function remainder(BigNumber|int|float|string $that) : BigInteger + { + $that = BigInteger::of($that); + + if ($that->value === '1') { + return BigInteger::zero(); + } + + if ($that->value === '0') { + throw DivisionByZeroException::divisionByZero(); + } + + $remainder = Calculator::get()->divR($this->value, $that->value); + + return new BigInteger($remainder); + } + + /** + * Returns the quotient and remainder of the division of this number by the given one. + * + * @param BigNumber|int|float|string $that The divisor. Must be convertible to a BigInteger. + * + * @return BigInteger[] An array containing the quotient and the remainder. + * + * @psalm-return array{BigInteger, BigInteger} + * + * @throws DivisionByZeroException If the divisor is zero. + */ + public function quotientAndRemainder(BigNumber|int|float|string $that) : array + { + $that = BigInteger::of($that); + + if ($that->value === '0') { + throw DivisionByZeroException::divisionByZero(); + } + + [$quotient, $remainder] = Calculator::get()->divQR($this->value, $that->value); + + return [ + new BigInteger($quotient), + new BigInteger($remainder) + ]; + } + + /** + * Returns the modulo of this number and the given one. + * + * The modulo operation yields the same result as the remainder operation when both operands are of the same sign, + * and may differ when signs are different. + * + * The result of the modulo operation, when non-zero, has the same sign as the divisor. + * + * @param BigNumber|int|float|string $that The divisor. Must be convertible to a BigInteger. + * + * @throws DivisionByZeroException If the divisor is zero. + */ + public function mod(BigNumber|int|float|string $that) : BigInteger + { + $that = BigInteger::of($that); + + if ($that->value === '0') { + throw DivisionByZeroException::modulusMustNotBeZero(); + } + + $value = Calculator::get()->mod($this->value, $that->value); + + return new BigInteger($value); + } + + /** + * Returns the modular multiplicative inverse of this BigInteger modulo $m. + * + * @throws DivisionByZeroException If $m is zero. + * @throws NegativeNumberException If $m is negative. + * @throws MathException If this BigInteger has no multiplicative inverse mod m (that is, this BigInteger + * is not relatively prime to m). + */ + public function modInverse(BigInteger $m) : BigInteger + { + if ($m->value === '0') { + throw DivisionByZeroException::modulusMustNotBeZero(); + } + + if ($m->isNegative()) { + throw new NegativeNumberException('Modulus must not be negative.'); + } + + if ($m->value === '1') { + return BigInteger::zero(); + } + + $value = Calculator::get()->modInverse($this->value, $m->value); + + if ($value === null) { + throw new MathException('Unable to compute the modInverse for the given modulus.'); + } + + return new BigInteger($value); + } + + /** + * Returns this number raised into power with modulo. + * + * This operation only works on positive numbers. + * + * @param BigNumber|int|float|string $exp The exponent. Must be positive or zero. + * @param BigNumber|int|float|string $mod The modulus. Must be strictly positive. + * + * @throws NegativeNumberException If any of the operands is negative. + * @throws DivisionByZeroException If the modulus is zero. + */ + public function modPow(BigNumber|int|float|string $exp, BigNumber|int|float|string $mod) : BigInteger + { + $exp = BigInteger::of($exp); + $mod = BigInteger::of($mod); + + if ($this->isNegative() || $exp->isNegative() || $mod->isNegative()) { + throw new NegativeNumberException('The operands cannot be negative.'); + } + + if ($mod->isZero()) { + throw DivisionByZeroException::modulusMustNotBeZero(); + } + + $result = Calculator::get()->modPow($this->value, $exp->value, $mod->value); + + return new BigInteger($result); + } + + /** + * Returns the greatest common divisor of this number and the given one. + * + * The GCD is always positive, unless both operands are zero, in which case it is zero. + * + * @param BigNumber|int|float|string $that The operand. Must be convertible to an integer number. + */ + public function gcd(BigNumber|int|float|string $that) : BigInteger + { + $that = BigInteger::of($that); + + if ($that->value === '0' && $this->value[0] !== '-') { + return $this; + } + + if ($this->value === '0' && $that->value[0] !== '-') { + return $that; + } + + $value = Calculator::get()->gcd($this->value, $that->value); + + return new BigInteger($value); + } + + /** + * Returns the integer square root number of this number, rounded down. + * + * The result is the largest x such that x² ≤ n. + * + * @throws NegativeNumberException If this number is negative. + */ + public function sqrt() : BigInteger + { + if ($this->value[0] === '-') { + throw new NegativeNumberException('Cannot calculate the square root of a negative number.'); + } + + $value = Calculator::get()->sqrt($this->value); + + return new BigInteger($value); + } + + /** + * Returns the absolute value of this number. + */ + public function abs() : BigInteger + { + return $this->isNegative() ? $this->negated() : $this; + } + + /** + * Returns the inverse of this number. + */ + public function negated() : BigInteger + { + return new BigInteger(Calculator::get()->neg($this->value)); + } + + /** + * Returns the integer bitwise-and combined with another integer. + * + * This method returns a negative BigInteger if and only if both operands are negative. + * + * @param BigNumber|int|float|string $that The operand. Must be convertible to an integer number. + */ + public function and(BigNumber|int|float|string $that) : BigInteger + { + $that = BigInteger::of($that); + + return new BigInteger(Calculator::get()->and($this->value, $that->value)); + } + + /** + * Returns the integer bitwise-or combined with another integer. + * + * This method returns a negative BigInteger if and only if either of the operands is negative. + * + * @param BigNumber|int|float|string $that The operand. Must be convertible to an integer number. + */ + public function or(BigNumber|int|float|string $that) : BigInteger + { + $that = BigInteger::of($that); + + return new BigInteger(Calculator::get()->or($this->value, $that->value)); + } + + /** + * Returns the integer bitwise-xor combined with another integer. + * + * This method returns a negative BigInteger if and only if exactly one of the operands is negative. + * + * @param BigNumber|int|float|string $that The operand. Must be convertible to an integer number. + */ + public function xor(BigNumber|int|float|string $that) : BigInteger + { + $that = BigInteger::of($that); + + return new BigInteger(Calculator::get()->xor($this->value, $that->value)); + } + + /** + * Returns the bitwise-not of this BigInteger. + */ + public function not() : BigInteger + { + return $this->negated()->minus(1); + } + + /** + * Returns the integer left shifted by a given number of bits. + */ + public function shiftedLeft(int $distance) : BigInteger + { + if ($distance === 0) { + return $this; + } + + if ($distance < 0) { + return $this->shiftedRight(- $distance); + } + + return $this->multipliedBy(BigInteger::of(2)->power($distance)); + } + + /** + * Returns the integer right shifted by a given number of bits. + */ + public function shiftedRight(int $distance) : BigInteger + { + if ($distance === 0) { + return $this; + } + + if ($distance < 0) { + return $this->shiftedLeft(- $distance); + } + + $operand = BigInteger::of(2)->power($distance); + + if ($this->isPositiveOrZero()) { + return $this->quotient($operand); + } + + return $this->dividedBy($operand, RoundingMode::UP); + } + + /** + * Returns the number of bits in the minimal two's-complement representation of this BigInteger, excluding a sign bit. + * + * For positive BigIntegers, this is equivalent to the number of bits in the ordinary binary representation. + * Computes (ceil(log2(this < 0 ? -this : this+1))). + */ + public function getBitLength() : int + { + if ($this->value === '0') { + return 0; + } + + if ($this->isNegative()) { + return $this->abs()->minus(1)->getBitLength(); + } + + return \strlen($this->toBase(2)); + } + + /** + * Returns the index of the rightmost (lowest-order) one bit in this BigInteger. + * + * Returns -1 if this BigInteger contains no one bits. + */ + public function getLowestSetBit() : int + { + $n = $this; + $bitLength = $this->getBitLength(); + + for ($i = 0; $i <= $bitLength; $i++) { + if ($n->isOdd()) { + return $i; + } + + $n = $n->shiftedRight(1); + } + + return -1; + } + + /** + * Returns whether this number is even. + */ + public function isEven() : bool + { + return \in_array($this->value[-1], ['0', '2', '4', '6', '8'], true); + } + + /** + * Returns whether this number is odd. + */ + public function isOdd() : bool + { + return \in_array($this->value[-1], ['1', '3', '5', '7', '9'], true); + } + + /** + * Returns true if and only if the designated bit is set. + * + * Computes ((this & (1<shiftedRight($n)->isOdd(); + } + + public function compareTo(BigNumber|int|float|string $that) : int + { + $that = BigNumber::of($that); + + if ($that instanceof BigInteger) { + return Calculator::get()->cmp($this->value, $that->value); + } + + return - $that->compareTo($this); + } + + public function getSign() : int + { + return ($this->value === '0') ? 0 : (($this->value[0] === '-') ? -1 : 1); + } + + public function toBigInteger() : BigInteger + { + return $this; + } + + public function toBigDecimal() : BigDecimal + { + return self::newBigDecimal($this->value); + } + + public function toBigRational() : BigRational + { + return self::newBigRational($this, BigInteger::one(), false); + } + + public function toScale(int $scale, RoundingMode $roundingMode = RoundingMode::UNNECESSARY) : BigDecimal + { + return $this->toBigDecimal()->toScale($scale, $roundingMode); + } + + public function toInt() : int + { + $intValue = (int) $this->value; + + if ($this->value !== (string) $intValue) { + throw IntegerOverflowException::toIntOverflow($this); + } + + return $intValue; + } + + public function toFloat() : float + { + return (float) $this->value; + } + + /** + * Returns a string representation of this number in the given base. + * + * The output will always be lowercase for bases greater than 10. + * + * @throws \InvalidArgumentException If the base is out of range. + */ + public function toBase(int $base) : string + { + if ($base === 10) { + return $this->value; + } + + if ($base < 2 || $base > 36) { + throw new \InvalidArgumentException(\sprintf('Base %d is out of range [2, 36]', $base)); + } + + return Calculator::get()->toBase($this->value, $base); + } + + /** + * Returns a string representation of this number in an arbitrary base with a custom alphabet. + * + * Because this method accepts an alphabet with any character, including dash, it does not handle negative numbers; + * a NegativeNumberException will be thrown when attempting to call this method on a negative number. + * + * @param string $alphabet The alphabet, for example '01' for base 2, or '01234567' for base 8. + * + * @throws NegativeNumberException If this number is negative. + * @throws \InvalidArgumentException If the given alphabet does not contain at least 2 chars. + */ + public function toArbitraryBase(string $alphabet) : string + { + $base = \strlen($alphabet); + + if ($base < 2) { + throw new \InvalidArgumentException('The alphabet must contain at least 2 chars.'); + } + + if ($this->value[0] === '-') { + throw new NegativeNumberException(__FUNCTION__ . '() does not support negative numbers.'); + } + + return Calculator::get()->toArbitraryBase($this->value, $alphabet, $base); + } + + /** + * Returns a string of bytes containing the binary representation of this BigInteger. + * + * The string is in big-endian byte-order: the most significant byte is in the zeroth element. + * + * If `$signed` is true, the output will be in two's-complement representation, and a sign bit will be prepended to + * the output. If `$signed` is false, no sign bit will be prepended, and this method will throw an exception if the + * number is negative. + * + * The string will contain the minimum number of bytes required to represent this BigInteger, including a sign bit + * if `$signed` is true. + * + * This representation is compatible with the `fromBytes()` factory method, as long as the `$signed` flags match. + * + * @param bool $signed Whether to output a signed number in two's-complement representation with a leading sign bit. + * + * @throws NegativeNumberException If $signed is false, and the number is negative. + */ + public function toBytes(bool $signed = true) : string + { + if (! $signed && $this->isNegative()) { + throw new NegativeNumberException('Cannot convert a negative number to a byte string when $signed is false.'); + } + + $hex = $this->abs()->toBase(16); + + if (\strlen($hex) % 2 !== 0) { + $hex = '0' . $hex; + } + + $baseHexLength = \strlen($hex); + + if ($signed) { + if ($this->isNegative()) { + $bin = \hex2bin($hex); + assert($bin !== false); + + $hex = \bin2hex(~$bin); + $hex = self::fromBase($hex, 16)->plus(1)->toBase(16); + + $hexLength = \strlen($hex); + + if ($hexLength < $baseHexLength) { + $hex = \str_repeat('0', $baseHexLength - $hexLength) . $hex; + } + + if ($hex[0] < '8') { + $hex = 'FF' . $hex; + } + } else { + if ($hex[0] >= '8') { + $hex = '00' . $hex; + } + } + } + + return \hex2bin($hex); + } + + public function __toString() : string + { + return $this->value; + } + + /** + * This method is required for serializing the object and SHOULD NOT be accessed directly. + * + * @internal + * + * @return array{value: string} + */ + public function __serialize(): array + { + return ['value' => $this->value]; + } + + /** + * This method is only here to allow unserializing the object and cannot be accessed directly. + * + * @internal + * @psalm-suppress RedundantPropertyInitializationCheck + * + * @param array{value: string} $data + * + * @throws \LogicException + */ + public function __unserialize(array $data): void + { + if (isset($this->value)) { + throw new \LogicException('__unserialize() is an internal function, it must not be called directly.'); + } + + $this->value = $data['value']; + } +} diff --git a/3rdparty/brick/math/src/BigNumber.php b/3rdparty/brick/math/src/BigNumber.php new file mode 100644 index 00000000..5a0df783 --- /dev/null +++ b/3rdparty/brick/math/src/BigNumber.php @@ -0,0 +1,509 @@ +[\-\+])?' . + '(?[0-9]+)?' . + '(?\.)?' . + '(?[0-9]+)?' . + '(?:[eE](?[\-\+]?[0-9]+))?' . + '$/'; + + /** + * The regular expression used to parse rational numbers. + */ + private const PARSE_REGEXP_RATIONAL = + '/^' . + '(?[\-\+])?' . + '(?[0-9]+)' . + '\/?' . + '(?[0-9]+)' . + '$/'; + + /** + * Creates a BigNumber of the given value. + * + * The concrete return type is dependent on the given value, with the following rules: + * + * - BigNumber instances are returned as is + * - integer numbers are returned as BigInteger + * - floating point numbers are converted to a string then parsed as such + * - strings containing a `/` character are returned as BigRational + * - strings containing a `.` character or using an exponential notation are returned as BigDecimal + * - strings containing only digits with an optional leading `+` or `-` sign are returned as BigInteger + * + * @throws NumberFormatException If the format of the number is not valid. + * @throws DivisionByZeroException If the value represents a rational number with a denominator of zero. + * + * @psalm-pure + */ + final public static function of(BigNumber|int|float|string $value) : static + { + $value = self::_of($value); + + if (static::class === BigNumber::class) { + // https://github.com/vimeo/psalm/issues/10309 + assert($value instanceof static); + + return $value; + } + + return static::from($value); + } + + /** + * @psalm-pure + */ + private static function _of(BigNumber|int|float|string $value) : BigNumber + { + if ($value instanceof BigNumber) { + return $value; + } + + if (\is_int($value)) { + return new BigInteger((string) $value); + } + + if (is_float($value)) { + $value = (string) $value; + } + + if (str_contains($value, '/')) { + // Rational number + if (\preg_match(self::PARSE_REGEXP_RATIONAL, $value, $matches, PREG_UNMATCHED_AS_NULL) !== 1) { + throw NumberFormatException::invalidFormat($value); + } + + $sign = $matches['sign']; + $numerator = $matches['numerator']; + $denominator = $matches['denominator']; + + assert($numerator !== null); + assert($denominator !== null); + + $numerator = self::cleanUp($sign, $numerator); + $denominator = self::cleanUp(null, $denominator); + + if ($denominator === '0') { + throw DivisionByZeroException::denominatorMustNotBeZero(); + } + + return new BigRational( + new BigInteger($numerator), + new BigInteger($denominator), + false + ); + } else { + // Integer or decimal number + if (\preg_match(self::PARSE_REGEXP_NUMERICAL, $value, $matches, PREG_UNMATCHED_AS_NULL) !== 1) { + throw NumberFormatException::invalidFormat($value); + } + + $sign = $matches['sign']; + $point = $matches['point']; + $integral = $matches['integral']; + $fractional = $matches['fractional']; + $exponent = $matches['exponent']; + + if ($integral === null && $fractional === null) { + throw NumberFormatException::invalidFormat($value); + } + + if ($integral === null) { + $integral = '0'; + } + + if ($point !== null || $exponent !== null) { + $fractional = ($fractional ?? ''); + $exponent = ($exponent !== null) ? (int)$exponent : 0; + + if ($exponent === PHP_INT_MIN || $exponent === PHP_INT_MAX) { + throw new NumberFormatException('Exponent too large.'); + } + + $unscaledValue = self::cleanUp($sign, $integral . $fractional); + + $scale = \strlen($fractional) - $exponent; + + if ($scale < 0) { + if ($unscaledValue !== '0') { + $unscaledValue .= \str_repeat('0', -$scale); + } + $scale = 0; + } + + return new BigDecimal($unscaledValue, $scale); + } + + $integral = self::cleanUp($sign, $integral); + + return new BigInteger($integral); + } + } + + /** + * Overridden by subclasses to convert a BigNumber to an instance of the subclass. + * + * @throws MathException If the value cannot be converted. + * + * @psalm-pure + */ + abstract protected static function from(BigNumber $number): static; + + /** + * Proxy method to access BigInteger's protected constructor from sibling classes. + * + * @internal + * @psalm-pure + */ + final protected function newBigInteger(string $value) : BigInteger + { + return new BigInteger($value); + } + + /** + * Proxy method to access BigDecimal's protected constructor from sibling classes. + * + * @internal + * @psalm-pure + */ + final protected function newBigDecimal(string $value, int $scale = 0) : BigDecimal + { + return new BigDecimal($value, $scale); + } + + /** + * Proxy method to access BigRational's protected constructor from sibling classes. + * + * @internal + * @psalm-pure + */ + final protected function newBigRational(BigInteger $numerator, BigInteger $denominator, bool $checkDenominator) : BigRational + { + return new BigRational($numerator, $denominator, $checkDenominator); + } + + /** + * Returns the minimum of the given values. + * + * @param BigNumber|int|float|string ...$values The numbers to compare. All the numbers need to be convertible + * to an instance of the class this method is called on. + * + * @throws \InvalidArgumentException If no values are given. + * @throws MathException If an argument is not valid. + * + * @psalm-pure + */ + final public static function min(BigNumber|int|float|string ...$values) : static + { + $min = null; + + foreach ($values as $value) { + $value = static::of($value); + + if ($min === null || $value->isLessThan($min)) { + $min = $value; + } + } + + if ($min === null) { + throw new \InvalidArgumentException(__METHOD__ . '() expects at least one value.'); + } + + return $min; + } + + /** + * Returns the maximum of the given values. + * + * @param BigNumber|int|float|string ...$values The numbers to compare. All the numbers need to be convertible + * to an instance of the class this method is called on. + * + * @throws \InvalidArgumentException If no values are given. + * @throws MathException If an argument is not valid. + * + * @psalm-pure + */ + final public static function max(BigNumber|int|float|string ...$values) : static + { + $max = null; + + foreach ($values as $value) { + $value = static::of($value); + + if ($max === null || $value->isGreaterThan($max)) { + $max = $value; + } + } + + if ($max === null) { + throw new \InvalidArgumentException(__METHOD__ . '() expects at least one value.'); + } + + return $max; + } + + /** + * Returns the sum of the given values. + * + * @param BigNumber|int|float|string ...$values The numbers to add. All the numbers need to be convertible + * to an instance of the class this method is called on. + * + * @throws \InvalidArgumentException If no values are given. + * @throws MathException If an argument is not valid. + * + * @psalm-pure + */ + final public static function sum(BigNumber|int|float|string ...$values) : static + { + /** @var static|null $sum */ + $sum = null; + + foreach ($values as $value) { + $value = static::of($value); + + $sum = $sum === null ? $value : self::add($sum, $value); + } + + if ($sum === null) { + throw new \InvalidArgumentException(__METHOD__ . '() expects at least one value.'); + } + + return $sum; + } + + /** + * Adds two BigNumber instances in the correct order to avoid a RoundingNecessaryException. + * + * @todo This could be better resolved by creating an abstract protected method in BigNumber, and leaving to + * concrete classes the responsibility to perform the addition themselves or delegate it to the given number, + * depending on their ability to perform the operation. This will also require a version bump because we're + * potentially breaking custom BigNumber implementations (if any...) + * + * @psalm-pure + */ + private static function add(BigNumber $a, BigNumber $b) : BigNumber + { + if ($a instanceof BigRational) { + return $a->plus($b); + } + + if ($b instanceof BigRational) { + return $b->plus($a); + } + + if ($a instanceof BigDecimal) { + return $a->plus($b); + } + + if ($b instanceof BigDecimal) { + return $b->plus($a); + } + + /** @var BigInteger $a */ + + return $a->plus($b); + } + + /** + * Removes optional leading zeros and applies sign. + * + * @param string|null $sign The sign, '+' or '-', optional. Null is allowed for convenience and treated as '+'. + * @param string $number The number, validated as a non-empty string of digits. + * + * @psalm-pure + */ + private static function cleanUp(string|null $sign, string $number) : string + { + $number = \ltrim($number, '0'); + + if ($number === '') { + return '0'; + } + + return $sign === '-' ? '-' . $number : $number; + } + + /** + * Checks if this number is equal to the given one. + */ + final public function isEqualTo(BigNumber|int|float|string $that) : bool + { + return $this->compareTo($that) === 0; + } + + /** + * Checks if this number is strictly lower than the given one. + */ + final public function isLessThan(BigNumber|int|float|string $that) : bool + { + return $this->compareTo($that) < 0; + } + + /** + * Checks if this number is lower than or equal to the given one. + */ + final public function isLessThanOrEqualTo(BigNumber|int|float|string $that) : bool + { + return $this->compareTo($that) <= 0; + } + + /** + * Checks if this number is strictly greater than the given one. + */ + final public function isGreaterThan(BigNumber|int|float|string $that) : bool + { + return $this->compareTo($that) > 0; + } + + /** + * Checks if this number is greater than or equal to the given one. + */ + final public function isGreaterThanOrEqualTo(BigNumber|int|float|string $that) : bool + { + return $this->compareTo($that) >= 0; + } + + /** + * Checks if this number equals zero. + */ + final public function isZero() : bool + { + return $this->getSign() === 0; + } + + /** + * Checks if this number is strictly negative. + */ + final public function isNegative() : bool + { + return $this->getSign() < 0; + } + + /** + * Checks if this number is negative or zero. + */ + final public function isNegativeOrZero() : bool + { + return $this->getSign() <= 0; + } + + /** + * Checks if this number is strictly positive. + */ + final public function isPositive() : bool + { + return $this->getSign() > 0; + } + + /** + * Checks if this number is positive or zero. + */ + final public function isPositiveOrZero() : bool + { + return $this->getSign() >= 0; + } + + /** + * Returns the sign of this number. + * + * @psalm-return -1|0|1 + * + * @return int -1 if the number is negative, 0 if zero, 1 if positive. + */ + abstract public function getSign() : int; + + /** + * Compares this number to the given one. + * + * @psalm-return -1|0|1 + * + * @return int -1 if `$this` is lower than, 0 if equal to, 1 if greater than `$that`. + * + * @throws MathException If the number is not valid. + */ + abstract public function compareTo(BigNumber|int|float|string $that) : int; + + /** + * Converts this number to a BigInteger. + * + * @throws RoundingNecessaryException If this number cannot be converted to a BigInteger without rounding. + */ + abstract public function toBigInteger() : BigInteger; + + /** + * Converts this number to a BigDecimal. + * + * @throws RoundingNecessaryException If this number cannot be converted to a BigDecimal without rounding. + */ + abstract public function toBigDecimal() : BigDecimal; + + /** + * Converts this number to a BigRational. + */ + abstract public function toBigRational() : BigRational; + + /** + * Converts this number to a BigDecimal with the given scale, using rounding if necessary. + * + * @param int $scale The scale of the resulting `BigDecimal`. + * @param RoundingMode $roundingMode An optional rounding mode, defaults to UNNECESSARY. + * + * @throws RoundingNecessaryException If this number cannot be converted to the given scale without rounding. + * This only applies when RoundingMode::UNNECESSARY is used. + */ + abstract public function toScale(int $scale, RoundingMode $roundingMode = RoundingMode::UNNECESSARY) : BigDecimal; + + /** + * Returns the exact value of this number as a native integer. + * + * If this number cannot be converted to a native integer without losing precision, an exception is thrown. + * Note that the acceptable range for an integer depends on the platform and differs for 32-bit and 64-bit. + * + * @throws MathException If this number cannot be exactly converted to a native integer. + */ + abstract public function toInt() : int; + + /** + * Returns an approximation of this number as a floating-point value. + * + * Note that this method can discard information as the precision of a floating-point value + * is inherently limited. + * + * If the number is greater than the largest representable floating point number, positive infinity is returned. + * If the number is less than the smallest representable floating point number, negative infinity is returned. + */ + abstract public function toFloat() : float; + + /** + * Returns a string representation of this number. + * + * The output of this method can be parsed by the `of()` factory method; + * this will yield an object equal to this one, without any information loss. + */ + abstract public function __toString() : string; + + final public function jsonSerialize() : string + { + return $this->__toString(); + } +} diff --git a/3rdparty/brick/math/src/BigRational.php b/3rdparty/brick/math/src/BigRational.php new file mode 100644 index 00000000..fc3060ed --- /dev/null +++ b/3rdparty/brick/math/src/BigRational.php @@ -0,0 +1,413 @@ +isZero()) { + throw DivisionByZeroException::denominatorMustNotBeZero(); + } + + if ($denominator->isNegative()) { + $numerator = $numerator->negated(); + $denominator = $denominator->negated(); + } + } + + $this->numerator = $numerator; + $this->denominator = $denominator; + } + + /** + * @psalm-pure + */ + protected static function from(BigNumber $number): static + { + return $number->toBigRational(); + } + + /** + * Creates a BigRational out of a numerator and a denominator. + * + * If the denominator is negative, the signs of both the numerator and the denominator + * will be inverted to ensure that the denominator is always positive. + * + * @param BigNumber|int|float|string $numerator The numerator. Must be convertible to a BigInteger. + * @param BigNumber|int|float|string $denominator The denominator. Must be convertible to a BigInteger. + * + * @throws NumberFormatException If an argument does not represent a valid number. + * @throws RoundingNecessaryException If an argument represents a non-integer number. + * @throws DivisionByZeroException If the denominator is zero. + * + * @psalm-pure + */ + public static function nd( + BigNumber|int|float|string $numerator, + BigNumber|int|float|string $denominator, + ) : BigRational { + $numerator = BigInteger::of($numerator); + $denominator = BigInteger::of($denominator); + + return new BigRational($numerator, $denominator, true); + } + + /** + * Returns a BigRational representing zero. + * + * @psalm-pure + */ + public static function zero() : BigRational + { + /** + * @psalm-suppress ImpureStaticVariable + * @var BigRational|null $zero + */ + static $zero; + + if ($zero === null) { + $zero = new BigRational(BigInteger::zero(), BigInteger::one(), false); + } + + return $zero; + } + + /** + * Returns a BigRational representing one. + * + * @psalm-pure + */ + public static function one() : BigRational + { + /** + * @psalm-suppress ImpureStaticVariable + * @var BigRational|null $one + */ + static $one; + + if ($one === null) { + $one = new BigRational(BigInteger::one(), BigInteger::one(), false); + } + + return $one; + } + + /** + * Returns a BigRational representing ten. + * + * @psalm-pure + */ + public static function ten() : BigRational + { + /** + * @psalm-suppress ImpureStaticVariable + * @var BigRational|null $ten + */ + static $ten; + + if ($ten === null) { + $ten = new BigRational(BigInteger::ten(), BigInteger::one(), false); + } + + return $ten; + } + + public function getNumerator() : BigInteger + { + return $this->numerator; + } + + public function getDenominator() : BigInteger + { + return $this->denominator; + } + + /** + * Returns the quotient of the division of the numerator by the denominator. + */ + public function quotient() : BigInteger + { + return $this->numerator->quotient($this->denominator); + } + + /** + * Returns the remainder of the division of the numerator by the denominator. + */ + public function remainder() : BigInteger + { + return $this->numerator->remainder($this->denominator); + } + + /** + * Returns the quotient and remainder of the division of the numerator by the denominator. + * + * @return BigInteger[] + * + * @psalm-return array{BigInteger, BigInteger} + */ + public function quotientAndRemainder() : array + { + return $this->numerator->quotientAndRemainder($this->denominator); + } + + /** + * Returns the sum of this number and the given one. + * + * @param BigNumber|int|float|string $that The number to add. + * + * @throws MathException If the number is not valid. + */ + public function plus(BigNumber|int|float|string $that) : BigRational + { + $that = BigRational::of($that); + + $numerator = $this->numerator->multipliedBy($that->denominator); + $numerator = $numerator->plus($that->numerator->multipliedBy($this->denominator)); + $denominator = $this->denominator->multipliedBy($that->denominator); + + return new BigRational($numerator, $denominator, false); + } + + /** + * Returns the difference of this number and the given one. + * + * @param BigNumber|int|float|string $that The number to subtract. + * + * @throws MathException If the number is not valid. + */ + public function minus(BigNumber|int|float|string $that) : BigRational + { + $that = BigRational::of($that); + + $numerator = $this->numerator->multipliedBy($that->denominator); + $numerator = $numerator->minus($that->numerator->multipliedBy($this->denominator)); + $denominator = $this->denominator->multipliedBy($that->denominator); + + return new BigRational($numerator, $denominator, false); + } + + /** + * Returns the product of this number and the given one. + * + * @param BigNumber|int|float|string $that The multiplier. + * + * @throws MathException If the multiplier is not a valid number. + */ + public function multipliedBy(BigNumber|int|float|string $that) : BigRational + { + $that = BigRational::of($that); + + $numerator = $this->numerator->multipliedBy($that->numerator); + $denominator = $this->denominator->multipliedBy($that->denominator); + + return new BigRational($numerator, $denominator, false); + } + + /** + * Returns the result of the division of this number by the given one. + * + * @param BigNumber|int|float|string $that The divisor. + * + * @throws MathException If the divisor is not a valid number, or is zero. + */ + public function dividedBy(BigNumber|int|float|string $that) : BigRational + { + $that = BigRational::of($that); + + $numerator = $this->numerator->multipliedBy($that->denominator); + $denominator = $this->denominator->multipliedBy($that->numerator); + + return new BigRational($numerator, $denominator, true); + } + + /** + * Returns this number exponentiated to the given value. + * + * @throws \InvalidArgumentException If the exponent is not in the range 0 to 1,000,000. + */ + public function power(int $exponent) : BigRational + { + if ($exponent === 0) { + $one = BigInteger::one(); + + return new BigRational($one, $one, false); + } + + if ($exponent === 1) { + return $this; + } + + return new BigRational( + $this->numerator->power($exponent), + $this->denominator->power($exponent), + false + ); + } + + /** + * Returns the reciprocal of this BigRational. + * + * The reciprocal has the numerator and denominator swapped. + * + * @throws DivisionByZeroException If the numerator is zero. + */ + public function reciprocal() : BigRational + { + return new BigRational($this->denominator, $this->numerator, true); + } + + /** + * Returns the absolute value of this BigRational. + */ + public function abs() : BigRational + { + return new BigRational($this->numerator->abs(), $this->denominator, false); + } + + /** + * Returns the negated value of this BigRational. + */ + public function negated() : BigRational + { + return new BigRational($this->numerator->negated(), $this->denominator, false); + } + + /** + * Returns the simplified value of this BigRational. + */ + public function simplified() : BigRational + { + $gcd = $this->numerator->gcd($this->denominator); + + $numerator = $this->numerator->quotient($gcd); + $denominator = $this->denominator->quotient($gcd); + + return new BigRational($numerator, $denominator, false); + } + + public function compareTo(BigNumber|int|float|string $that) : int + { + return $this->minus($that)->getSign(); + } + + public function getSign() : int + { + return $this->numerator->getSign(); + } + + public function toBigInteger() : BigInteger + { + $simplified = $this->simplified(); + + if (! $simplified->denominator->isEqualTo(1)) { + throw new RoundingNecessaryException('This rational number cannot be represented as an integer value without rounding.'); + } + + return $simplified->numerator; + } + + public function toBigDecimal() : BigDecimal + { + return $this->numerator->toBigDecimal()->exactlyDividedBy($this->denominator); + } + + public function toBigRational() : BigRational + { + return $this; + } + + public function toScale(int $scale, RoundingMode $roundingMode = RoundingMode::UNNECESSARY) : BigDecimal + { + return $this->numerator->toBigDecimal()->dividedBy($this->denominator, $scale, $roundingMode); + } + + public function toInt() : int + { + return $this->toBigInteger()->toInt(); + } + + public function toFloat() : float + { + $simplified = $this->simplified(); + return $simplified->numerator->toFloat() / $simplified->denominator->toFloat(); + } + + public function __toString() : string + { + $numerator = (string) $this->numerator; + $denominator = (string) $this->denominator; + + if ($denominator === '1') { + return $numerator; + } + + return $this->numerator . '/' . $this->denominator; + } + + /** + * This method is required for serializing the object and SHOULD NOT be accessed directly. + * + * @internal + * + * @return array{numerator: BigInteger, denominator: BigInteger} + */ + public function __serialize(): array + { + return ['numerator' => $this->numerator, 'denominator' => $this->denominator]; + } + + /** + * This method is only here to allow unserializing the object and cannot be accessed directly. + * + * @internal + * @psalm-suppress RedundantPropertyInitializationCheck + * + * @param array{numerator: BigInteger, denominator: BigInteger} $data + * + * @throws \LogicException + */ + public function __unserialize(array $data): void + { + if (isset($this->numerator)) { + throw new \LogicException('__unserialize() is an internal function, it must not be called directly.'); + } + + $this->numerator = $data['numerator']; + $this->denominator = $data['denominator']; + } +} diff --git a/3rdparty/brick/math/src/Exception/DivisionByZeroException.php b/3rdparty/brick/math/src/Exception/DivisionByZeroException.php new file mode 100644 index 00000000..ce7769ac --- /dev/null +++ b/3rdparty/brick/math/src/Exception/DivisionByZeroException.php @@ -0,0 +1,35 @@ + 126) { + $char = \strtoupper(\dechex($ord)); + + if ($ord < 10) { + $char = '0' . $char; + } + } else { + $char = '"' . $char . '"'; + } + + return new self(\sprintf('Char %s is not a valid character in the given alphabet.', $char)); + } +} diff --git a/3rdparty/brick/math/src/Exception/RoundingNecessaryException.php b/3rdparty/brick/math/src/Exception/RoundingNecessaryException.php new file mode 100644 index 00000000..57bfcd84 --- /dev/null +++ b/3rdparty/brick/math/src/Exception/RoundingNecessaryException.php @@ -0,0 +1,19 @@ +init($a, $b); + + if ($aNeg && ! $bNeg) { + return -1; + } + + if ($bNeg && ! $aNeg) { + return 1; + } + + $aLen = \strlen($aDig); + $bLen = \strlen($bDig); + + if ($aLen < $bLen) { + $result = -1; + } elseif ($aLen > $bLen) { + $result = 1; + } else { + $result = $aDig <=> $bDig; + } + + return $aNeg ? -$result : $result; + } + + /** + * Adds two numbers. + */ + abstract public function add(string $a, string $b) : string; + + /** + * Subtracts two numbers. + */ + abstract public function sub(string $a, string $b) : string; + + /** + * Multiplies two numbers. + */ + abstract public function mul(string $a, string $b) : string; + + /** + * Returns the quotient of the division of two numbers. + * + * @param string $a The dividend. + * @param string $b The divisor, must not be zero. + * + * @return string The quotient. + */ + abstract public function divQ(string $a, string $b) : string; + + /** + * Returns the remainder of the division of two numbers. + * + * @param string $a The dividend. + * @param string $b The divisor, must not be zero. + * + * @return string The remainder. + */ + abstract public function divR(string $a, string $b) : string; + + /** + * Returns the quotient and remainder of the division of two numbers. + * + * @param string $a The dividend. + * @param string $b The divisor, must not be zero. + * + * @return array{string, string} An array containing the quotient and remainder. + */ + abstract public function divQR(string $a, string $b) : array; + + /** + * Exponentiates a number. + * + * @param string $a The base number. + * @param int $e The exponent, validated as an integer between 0 and MAX_POWER. + * + * @return string The power. + */ + abstract public function pow(string $a, int $e) : string; + + /** + * @param string $b The modulus; must not be zero. + */ + public function mod(string $a, string $b) : string + { + return $this->divR($this->add($this->divR($a, $b), $b), $b); + } + + /** + * Returns the modular multiplicative inverse of $x modulo $m. + * + * If $x has no multiplicative inverse mod m, this method must return null. + * + * This method can be overridden by the concrete implementation if the underlying library has built-in support. + * + * @param string $m The modulus; must not be negative or zero. + */ + public function modInverse(string $x, string $m) : ?string + { + if ($m === '1') { + return '0'; + } + + $modVal = $x; + + if ($x[0] === '-' || ($this->cmp($this->abs($x), $m) >= 0)) { + $modVal = $this->mod($x, $m); + } + + [$g, $x] = $this->gcdExtended($modVal, $m); + + if ($g !== '1') { + return null; + } + + return $this->mod($this->add($this->mod($x, $m), $m), $m); + } + + /** + * Raises a number into power with modulo. + * + * @param string $base The base number; must be positive or zero. + * @param string $exp The exponent; must be positive or zero. + * @param string $mod The modulus; must be strictly positive. + */ + abstract public function modPow(string $base, string $exp, string $mod) : string; + + /** + * Returns the greatest common divisor of the two numbers. + * + * This method can be overridden by the concrete implementation if the underlying library + * has built-in support for GCD calculations. + * + * @return string The GCD, always positive, or zero if both arguments are zero. + */ + public function gcd(string $a, string $b) : string + { + if ($a === '0') { + return $this->abs($b); + } + + if ($b === '0') { + return $this->abs($a); + } + + return $this->gcd($b, $this->divR($a, $b)); + } + + /** + * @return array{string, string, string} GCD, X, Y + */ + private function gcdExtended(string $a, string $b) : array + { + if ($a === '0') { + return [$b, '0', '1']; + } + + [$gcd, $x1, $y1] = $this->gcdExtended($this->mod($b, $a), $a); + + $x = $this->sub($y1, $this->mul($this->divQ($b, $a), $x1)); + $y = $x1; + + return [$gcd, $x, $y]; + } + + /** + * Returns the square root of the given number, rounded down. + * + * The result is the largest x such that x² ≤ n. + * The input MUST NOT be negative. + */ + abstract public function sqrt(string $n) : string; + + /** + * Converts a number from an arbitrary base. + * + * This method can be overridden by the concrete implementation if the underlying library + * has built-in support for base conversion. + * + * @param string $number The number, positive or zero, non-empty, case-insensitively validated for the given base. + * @param int $base The base of the number, validated from 2 to 36. + * + * @return string The converted number, following the Calculator conventions. + */ + public function fromBase(string $number, int $base) : string + { + return $this->fromArbitraryBase(\strtolower($number), self::ALPHABET, $base); + } + + /** + * Converts a number to an arbitrary base. + * + * This method can be overridden by the concrete implementation if the underlying library + * has built-in support for base conversion. + * + * @param string $number The number to convert, following the Calculator conventions. + * @param int $base The base to convert to, validated from 2 to 36. + * + * @return string The converted number, lowercase. + */ + public function toBase(string $number, int $base) : string + { + $negative = ($number[0] === '-'); + + if ($negative) { + $number = \substr($number, 1); + } + + $number = $this->toArbitraryBase($number, self::ALPHABET, $base); + + if ($negative) { + return '-' . $number; + } + + return $number; + } + + /** + * Converts a non-negative number in an arbitrary base using a custom alphabet, to base 10. + * + * @param string $number The number to convert, validated as a non-empty string, + * containing only chars in the given alphabet/base. + * @param string $alphabet The alphabet that contains every digit, validated as 2 chars minimum. + * @param int $base The base of the number, validated from 2 to alphabet length. + * + * @return string The number in base 10, following the Calculator conventions. + */ + final public function fromArbitraryBase(string $number, string $alphabet, int $base) : string + { + // remove leading "zeros" + $number = \ltrim($number, $alphabet[0]); + + if ($number === '') { + return '0'; + } + + // optimize for "one" + if ($number === $alphabet[1]) { + return '1'; + } + + $result = '0'; + $power = '1'; + + $base = (string) $base; + + for ($i = \strlen($number) - 1; $i >= 0; $i--) { + $index = \strpos($alphabet, $number[$i]); + + if ($index !== 0) { + $result = $this->add($result, ($index === 1) + ? $power + : $this->mul($power, (string) $index) + ); + } + + if ($i !== 0) { + $power = $this->mul($power, $base); + } + } + + return $result; + } + + /** + * Converts a non-negative number to an arbitrary base using a custom alphabet. + * + * @param string $number The number to convert, positive or zero, following the Calculator conventions. + * @param string $alphabet The alphabet that contains every digit, validated as 2 chars minimum. + * @param int $base The base to convert to, validated from 2 to alphabet length. + * + * @return string The converted number in the given alphabet. + */ + final public function toArbitraryBase(string $number, string $alphabet, int $base) : string + { + if ($number === '0') { + return $alphabet[0]; + } + + $base = (string) $base; + $result = ''; + + while ($number !== '0') { + [$number, $remainder] = $this->divQR($number, $base); + $remainder = (int) $remainder; + + $result .= $alphabet[$remainder]; + } + + return \strrev($result); + } + + /** + * Performs a rounded division. + * + * Rounding is performed when the remainder of the division is not zero. + * + * @param string $a The dividend. + * @param string $b The divisor, must not be zero. + * @param RoundingMode $roundingMode The rounding mode. + * + * @throws \InvalidArgumentException If the rounding mode is invalid. + * @throws RoundingNecessaryException If RoundingMode::UNNECESSARY is provided but rounding is necessary. + * + * @psalm-suppress ImpureFunctionCall + */ + final public function divRound(string $a, string $b, RoundingMode $roundingMode) : string + { + [$quotient, $remainder] = $this->divQR($a, $b); + + $hasDiscardedFraction = ($remainder !== '0'); + $isPositiveOrZero = ($a[0] === '-') === ($b[0] === '-'); + + $discardedFractionSign = function() use ($remainder, $b) : int { + $r = $this->abs($this->mul($remainder, '2')); + $b = $this->abs($b); + + return $this->cmp($r, $b); + }; + + $increment = false; + + switch ($roundingMode) { + case RoundingMode::UNNECESSARY: + if ($hasDiscardedFraction) { + throw RoundingNecessaryException::roundingNecessary(); + } + break; + + case RoundingMode::UP: + $increment = $hasDiscardedFraction; + break; + + case RoundingMode::DOWN: + break; + + case RoundingMode::CEILING: + $increment = $hasDiscardedFraction && $isPositiveOrZero; + break; + + case RoundingMode::FLOOR: + $increment = $hasDiscardedFraction && ! $isPositiveOrZero; + break; + + case RoundingMode::HALF_UP: + $increment = $discardedFractionSign() >= 0; + break; + + case RoundingMode::HALF_DOWN: + $increment = $discardedFractionSign() > 0; + break; + + case RoundingMode::HALF_CEILING: + $increment = $isPositiveOrZero ? $discardedFractionSign() >= 0 : $discardedFractionSign() > 0; + break; + + case RoundingMode::HALF_FLOOR: + $increment = $isPositiveOrZero ? $discardedFractionSign() > 0 : $discardedFractionSign() >= 0; + break; + + case RoundingMode::HALF_EVEN: + $lastDigit = (int) $quotient[-1]; + $lastDigitIsEven = ($lastDigit % 2 === 0); + $increment = $lastDigitIsEven ? $discardedFractionSign() > 0 : $discardedFractionSign() >= 0; + break; + + default: + throw new \InvalidArgumentException('Invalid rounding mode.'); + } + + if ($increment) { + return $this->add($quotient, $isPositiveOrZero ? '1' : '-1'); + } + + return $quotient; + } + + /** + * Calculates bitwise AND of two numbers. + * + * This method can be overridden by the concrete implementation if the underlying library + * has built-in support for bitwise operations. + */ + public function and(string $a, string $b) : string + { + return $this->bitwise('and', $a, $b); + } + + /** + * Calculates bitwise OR of two numbers. + * + * This method can be overridden by the concrete implementation if the underlying library + * has built-in support for bitwise operations. + */ + public function or(string $a, string $b) : string + { + return $this->bitwise('or', $a, $b); + } + + /** + * Calculates bitwise XOR of two numbers. + * + * This method can be overridden by the concrete implementation if the underlying library + * has built-in support for bitwise operations. + */ + public function xor(string $a, string $b) : string + { + return $this->bitwise('xor', $a, $b); + } + + /** + * Performs a bitwise operation on a decimal number. + * + * @param 'and'|'or'|'xor' $operator The operator to use. + * @param string $a The left operand. + * @param string $b The right operand. + */ + private function bitwise(string $operator, string $a, string $b) : string + { + [$aNeg, $bNeg, $aDig, $bDig] = $this->init($a, $b); + + $aBin = $this->toBinary($aDig); + $bBin = $this->toBinary($bDig); + + $aLen = \strlen($aBin); + $bLen = \strlen($bBin); + + if ($aLen > $bLen) { + $bBin = \str_repeat("\x00", $aLen - $bLen) . $bBin; + } elseif ($bLen > $aLen) { + $aBin = \str_repeat("\x00", $bLen - $aLen) . $aBin; + } + + if ($aNeg) { + $aBin = $this->twosComplement($aBin); + } + if ($bNeg) { + $bBin = $this->twosComplement($bBin); + } + + $value = match ($operator) { + 'and' => $aBin & $bBin, + 'or' => $aBin | $bBin, + 'xor' => $aBin ^ $bBin, + }; + + $negative = match ($operator) { + 'and' => $aNeg and $bNeg, + 'or' => $aNeg or $bNeg, + 'xor' => $aNeg xor $bNeg, + }; + + if ($negative) { + $value = $this->twosComplement($value); + } + + $result = $this->toDecimal($value); + + return $negative ? $this->neg($result) : $result; + } + + /** + * @param string $number A positive, binary number. + */ + private function twosComplement(string $number) : string + { + $xor = \str_repeat("\xff", \strlen($number)); + + $number ^= $xor; + + for ($i = \strlen($number) - 1; $i >= 0; $i--) { + $byte = \ord($number[$i]); + + if (++$byte !== 256) { + $number[$i] = \chr($byte); + break; + } + + $number[$i] = "\x00"; + + if ($i === 0) { + $number = "\x01" . $number; + } + } + + return $number; + } + + /** + * Converts a decimal number to a binary string. + * + * @param string $number The number to convert, positive or zero, only digits. + */ + private function toBinary(string $number) : string + { + $result = ''; + + while ($number !== '0') { + [$number, $remainder] = $this->divQR($number, '256'); + $result .= \chr((int) $remainder); + } + + return \strrev($result); + } + + /** + * Returns the positive decimal representation of a binary number. + * + * @param string $bytes The bytes representing the number. + */ + private function toDecimal(string $bytes) : string + { + $result = '0'; + $power = '1'; + + for ($i = \strlen($bytes) - 1; $i >= 0; $i--) { + $index = \ord($bytes[$i]); + + if ($index !== 0) { + $result = $this->add($result, ($index === 1) + ? $power + : $this->mul($power, (string) $index) + ); + } + + if ($i !== 0) { + $power = $this->mul($power, '256'); + } + } + + return $result; + } +} diff --git a/3rdparty/brick/math/src/Internal/Calculator/BcMathCalculator.php b/3rdparty/brick/math/src/Internal/Calculator/BcMathCalculator.php new file mode 100644 index 00000000..067085e2 --- /dev/null +++ b/3rdparty/brick/math/src/Internal/Calculator/BcMathCalculator.php @@ -0,0 +1,65 @@ +maxDigits = match (PHP_INT_SIZE) { + 4 => 9, + 8 => 18, + default => throw new \RuntimeException('The platform is not 32-bit or 64-bit as expected.') + }; + } + + public function add(string $a, string $b) : string + { + /** + * @psalm-var numeric-string $a + * @psalm-var numeric-string $b + */ + $result = $a + $b; + + if (is_int($result)) { + return (string) $result; + } + + if ($a === '0') { + return $b; + } + + if ($b === '0') { + return $a; + } + + [$aNeg, $bNeg, $aDig, $bDig] = $this->init($a, $b); + + $result = $aNeg === $bNeg ? $this->doAdd($aDig, $bDig) : $this->doSub($aDig, $bDig); + + if ($aNeg) { + $result = $this->neg($result); + } + + return $result; + } + + public function sub(string $a, string $b) : string + { + return $this->add($a, $this->neg($b)); + } + + public function mul(string $a, string $b) : string + { + /** + * @psalm-var numeric-string $a + * @psalm-var numeric-string $b + */ + $result = $a * $b; + + if (is_int($result)) { + return (string) $result; + } + + if ($a === '0' || $b === '0') { + return '0'; + } + + if ($a === '1') { + return $b; + } + + if ($b === '1') { + return $a; + } + + if ($a === '-1') { + return $this->neg($b); + } + + if ($b === '-1') { + return $this->neg($a); + } + + [$aNeg, $bNeg, $aDig, $bDig] = $this->init($a, $b); + + $result = $this->doMul($aDig, $bDig); + + if ($aNeg !== $bNeg) { + $result = $this->neg($result); + } + + return $result; + } + + public function divQ(string $a, string $b) : string + { + return $this->divQR($a, $b)[0]; + } + + public function divR(string $a, string $b): string + { + return $this->divQR($a, $b)[1]; + } + + public function divQR(string $a, string $b) : array + { + if ($a === '0') { + return ['0', '0']; + } + + if ($a === $b) { + return ['1', '0']; + } + + if ($b === '1') { + return [$a, '0']; + } + + if ($b === '-1') { + return [$this->neg($a), '0']; + } + + /** @psalm-var numeric-string $a */ + $na = $a * 1; // cast to number + + if (is_int($na)) { + /** @psalm-var numeric-string $b */ + $nb = $b * 1; + + if (is_int($nb)) { + // the only division that may overflow is PHP_INT_MIN / -1, + // which cannot happen here as we've already handled a divisor of -1 above. + $q = intdiv($na, $nb); + $r = $na % $nb; + + return [ + (string) $q, + (string) $r + ]; + } + } + + [$aNeg, $bNeg, $aDig, $bDig] = $this->init($a, $b); + + [$q, $r] = $this->doDiv($aDig, $bDig); + + if ($aNeg !== $bNeg) { + $q = $this->neg($q); + } + + if ($aNeg) { + $r = $this->neg($r); + } + + return [$q, $r]; + } + + public function pow(string $a, int $e) : string + { + if ($e === 0) { + return '1'; + } + + if ($e === 1) { + return $a; + } + + $odd = $e % 2; + $e -= $odd; + + $aa = $this->mul($a, $a); + + /** @psalm-suppress PossiblyInvalidArgument We're sure that $e / 2 is an int now */ + $result = $this->pow($aa, $e / 2); + + if ($odd === 1) { + $result = $this->mul($result, $a); + } + + return $result; + } + + /** + * Algorithm from: https://www.geeksforgeeks.org/modular-exponentiation-power-in-modular-arithmetic/ + */ + public function modPow(string $base, string $exp, string $mod) : string + { + // special case: the algorithm below fails with 0 power 0 mod 1 (returns 1 instead of 0) + if ($base === '0' && $exp === '0' && $mod === '1') { + return '0'; + } + + // special case: the algorithm below fails with power 0 mod 1 (returns 1 instead of 0) + if ($exp === '0' && $mod === '1') { + return '0'; + } + + $x = $base; + + $res = '1'; + + // numbers are positive, so we can use remainder instead of modulo + $x = $this->divR($x, $mod); + + while ($exp !== '0') { + if (in_array($exp[-1], ['1', '3', '5', '7', '9'])) { // odd + $res = $this->divR($this->mul($res, $x), $mod); + } + + $exp = $this->divQ($exp, '2'); + $x = $this->divR($this->mul($x, $x), $mod); + } + + return $res; + } + + /** + * Adapted from https://cp-algorithms.com/num_methods/roots_newton.html + */ + public function sqrt(string $n) : string + { + if ($n === '0') { + return '0'; + } + + // initial approximation + $x = \str_repeat('9', \intdiv(\strlen($n), 2) ?: 1); + + $decreased = false; + + for (;;) { + $nx = $this->divQ($this->add($x, $this->divQ($n, $x)), '2'); + + if ($x === $nx || $this->cmp($nx, $x) > 0 && $decreased) { + break; + } + + $decreased = $this->cmp($nx, $x) < 0; + $x = $nx; + } + + return $x; + } + + /** + * Performs the addition of two non-signed large integers. + */ + private function doAdd(string $a, string $b) : string + { + [$a, $b, $length] = $this->pad($a, $b); + + $carry = 0; + $result = ''; + + for ($i = $length - $this->maxDigits;; $i -= $this->maxDigits) { + $blockLength = $this->maxDigits; + + if ($i < 0) { + $blockLength += $i; + /** @psalm-suppress LoopInvalidation */ + $i = 0; + } + + /** @psalm-var numeric-string $blockA */ + $blockA = \substr($a, $i, $blockLength); + + /** @psalm-var numeric-string $blockB */ + $blockB = \substr($b, $i, $blockLength); + + $sum = (string) ($blockA + $blockB + $carry); + $sumLength = \strlen($sum); + + if ($sumLength > $blockLength) { + $sum = \substr($sum, 1); + $carry = 1; + } else { + if ($sumLength < $blockLength) { + $sum = \str_repeat('0', $blockLength - $sumLength) . $sum; + } + $carry = 0; + } + + $result = $sum . $result; + + if ($i === 0) { + break; + } + } + + if ($carry === 1) { + $result = '1' . $result; + } + + return $result; + } + + /** + * Performs the subtraction of two non-signed large integers. + */ + private function doSub(string $a, string $b) : string + { + if ($a === $b) { + return '0'; + } + + // Ensure that we always subtract to a positive result: biggest minus smallest. + $cmp = $this->doCmp($a, $b); + + $invert = ($cmp === -1); + + if ($invert) { + $c = $a; + $a = $b; + $b = $c; + } + + [$a, $b, $length] = $this->pad($a, $b); + + $carry = 0; + $result = ''; + + $complement = 10 ** $this->maxDigits; + + for ($i = $length - $this->maxDigits;; $i -= $this->maxDigits) { + $blockLength = $this->maxDigits; + + if ($i < 0) { + $blockLength += $i; + /** @psalm-suppress LoopInvalidation */ + $i = 0; + } + + /** @psalm-var numeric-string $blockA */ + $blockA = \substr($a, $i, $blockLength); + + /** @psalm-var numeric-string $blockB */ + $blockB = \substr($b, $i, $blockLength); + + $sum = $blockA - $blockB - $carry; + + if ($sum < 0) { + $sum += $complement; + $carry = 1; + } else { + $carry = 0; + } + + $sum = (string) $sum; + $sumLength = \strlen($sum); + + if ($sumLength < $blockLength) { + $sum = \str_repeat('0', $blockLength - $sumLength) . $sum; + } + + $result = $sum . $result; + + if ($i === 0) { + break; + } + } + + // Carry cannot be 1 when the loop ends, as a > b + assert($carry === 0); + + $result = \ltrim($result, '0'); + + if ($invert) { + $result = $this->neg($result); + } + + return $result; + } + + /** + * Performs the multiplication of two non-signed large integers. + */ + private function doMul(string $a, string $b) : string + { + $x = \strlen($a); + $y = \strlen($b); + + $maxDigits = \intdiv($this->maxDigits, 2); + $complement = 10 ** $maxDigits; + + $result = '0'; + + for ($i = $x - $maxDigits;; $i -= $maxDigits) { + $blockALength = $maxDigits; + + if ($i < 0) { + $blockALength += $i; + /** @psalm-suppress LoopInvalidation */ + $i = 0; + } + + $blockA = (int) \substr($a, $i, $blockALength); + + $line = ''; + $carry = 0; + + for ($j = $y - $maxDigits;; $j -= $maxDigits) { + $blockBLength = $maxDigits; + + if ($j < 0) { + $blockBLength += $j; + /** @psalm-suppress LoopInvalidation */ + $j = 0; + } + + $blockB = (int) \substr($b, $j, $blockBLength); + + $mul = $blockA * $blockB + $carry; + $value = $mul % $complement; + $carry = ($mul - $value) / $complement; + + $value = (string) $value; + $value = \str_pad($value, $maxDigits, '0', STR_PAD_LEFT); + + $line = $value . $line; + + if ($j === 0) { + break; + } + } + + if ($carry !== 0) { + $line = $carry . $line; + } + + $line = \ltrim($line, '0'); + + if ($line !== '') { + $line .= \str_repeat('0', $x - $blockALength - $i); + $result = $this->add($result, $line); + } + + if ($i === 0) { + break; + } + } + + return $result; + } + + /** + * Performs the division of two non-signed large integers. + * + * @return string[] The quotient and remainder. + */ + private function doDiv(string $a, string $b) : array + { + $cmp = $this->doCmp($a, $b); + + if ($cmp === -1) { + return ['0', $a]; + } + + $x = \strlen($a); + $y = \strlen($b); + + // we now know that a >= b && x >= y + + $q = '0'; // quotient + $r = $a; // remainder + $z = $y; // focus length, always $y or $y+1 + + for (;;) { + $focus = \substr($a, 0, $z); + + $cmp = $this->doCmp($focus, $b); + + if ($cmp === -1) { + if ($z === $x) { // remainder < dividend + break; + } + + $z++; + } + + $zeros = \str_repeat('0', $x - $z); + + $q = $this->add($q, '1' . $zeros); + $a = $this->sub($a, $b . $zeros); + + $r = $a; + + if ($r === '0') { // remainder == 0 + break; + } + + $x = \strlen($a); + + if ($x < $y) { // remainder < dividend + break; + } + + $z = $y; + } + + return [$q, $r]; + } + + /** + * Compares two non-signed large numbers. + * + * @psalm-return -1|0|1 + */ + private function doCmp(string $a, string $b) : int + { + $x = \strlen($a); + $y = \strlen($b); + + $cmp = $x <=> $y; + + if ($cmp !== 0) { + return $cmp; + } + + return \strcmp($a, $b) <=> 0; // enforce -1|0|1 + } + + /** + * Pads the left of one of the given numbers with zeros if necessary to make both numbers the same length. + * + * The numbers must only consist of digits, without leading minus sign. + * + * @return array{string, string, int} + */ + private function pad(string $a, string $b) : array + { + $x = \strlen($a); + $y = \strlen($b); + + if ($x > $y) { + $b = \str_repeat('0', $x - $y) . $b; + + return [$a, $b, $x]; + } + + if ($x < $y) { + $a = \str_repeat('0', $y - $x) . $a; + + return [$a, $b, $y]; + } + + return [$a, $b, $x]; + } +} diff --git a/3rdparty/brick/math/src/RoundingMode.php b/3rdparty/brick/math/src/RoundingMode.php new file mode 100644 index 00000000..e8ee6a8b --- /dev/null +++ b/3rdparty/brick/math/src/RoundingMode.php @@ -0,0 +1,98 @@ += 0.5; otherwise, behaves as for DOWN. + * Note that this is the rounding mode commonly taught at school. + */ + case HALF_UP; + + /** + * Rounds towards "nearest neighbor" unless both neighbors are equidistant, in which case round down. + * + * Behaves as for UP if the discarded fraction is > 0.5; otherwise, behaves as for DOWN. + */ + case HALF_DOWN; + + /** + * Rounds towards "nearest neighbor" unless both neighbors are equidistant, in which case round towards positive infinity. + * + * If the result is positive, behaves as for HALF_UP; if negative, behaves as for HALF_DOWN. + */ + case HALF_CEILING; + + /** + * Rounds towards "nearest neighbor" unless both neighbors are equidistant, in which case round towards negative infinity. + * + * If the result is positive, behaves as for HALF_DOWN; if negative, behaves as for HALF_UP. + */ + case HALF_FLOOR; + + /** + * Rounds towards the "nearest neighbor" unless both neighbors are equidistant, in which case rounds towards the even neighbor. + * + * Behaves as for HALF_UP if the digit to the left of the discarded fraction is odd; + * behaves as for HALF_DOWN if it's even. + * + * Note that this is the rounding mode that statistically minimizes + * cumulative error when applied repeatedly over a sequence of calculations. + * It is sometimes known as "Banker's rounding", and is chiefly used in the USA. + */ + case HALF_EVEN; +} diff --git a/3rdparty/composer.json b/3rdparty/composer.json new file mode 100644 index 00000000..5dc7de22 --- /dev/null +++ b/3rdparty/composer.json @@ -0,0 +1,85 @@ +{ + "name": "f7cloud/3rdparty", + "version": "dev-stable32", + "description": "All 3rdparty components", + "license": "MIT", + "config": { + "vendor-dir": ".", + "preferred-install": "dist", + "optimize-autoloader": true, + "classmap-authoritative": true, + "platform": { + "php": "8.1.0" + }, + "sort-packages": true, + "allow-plugins": { + "cweagans/composer-patches": true + } + }, + "require": { + "php": "^8.1", + "ext-ctype": "*", + "ext-mbstring": "*", + "aws/aws-sdk-php": "^3.349", + "bantu/ini-get-wrapper": "v1.0.1", + "cweagans/composer-patches": "^1.7", + "deepdiver/zipstreamer": "^2.0.3", + "deepdiver1975/tarstreamer": "^2.1.0", + "doctrine/dbal": "^3.10.2", + "egulias/email-validator": "^4.0.4", + "fusonic/opengraph": "^3.0.0", + "giggsey/libphonenumber-for-php-lite": "^9.0.17", + "guzzlehttp/guzzle": "^7.9.3", + "icewind/searchdav": "^3.2.0", + "icewind/smb": "^3.7", + "icewind/streams": "^0.7.8", + "kornrunner/blurhash": "^1.2", + "laravel/serializable-closure": "^2.0.4", + "mexitek/phpcolors": "^1.0", + "microsoft/azure-storage-blob": "^1.5.4", + "mlocati/ip-lib": "^1.20", + "f7cloud/lognormalizer": "^1.0", + "pear/archive_tar": "^1.4.9", + "pear/pear-core-minimal": "^1.10", + "php-http/guzzle7-adapter": "^1.1.0", + "php-opencloud/openstack": "^3.14", + "phpseclib/phpseclib": "^2.0.45", + "pimple/pimple": "^3.5.0", + "psr/clock": "^1.0", + "psr/container": "^2.0.2", + "psr/event-dispatcher": "^1.0", + "psr/log": "^3.0.2", + "punic/punic": "^3.8", + "sabre/dav": "^4.7.0", + "stecman/symfony-console-completion": "^0.14.0", + "symfony/console": "^6.4.12", + "symfony/event-dispatcher": "^6.4.8", + "symfony/http-foundation": "^6.4.29", + "symfony/mailer": "^6.4", + "symfony/polyfill-intl-normalizer": "^1.32.0", + "symfony/polyfill-php82": "^1.32.0", + "symfony/polyfill-php83": "^1.32.0", + "symfony/polyfill-php84": "^1.32.0", + "symfony/process": "^6.4.15", + "symfony/routing": "^6.4.12", + "symfony/translation": "^6.4.4", + "wapmorgan/mp3info": "^0.1.1", + "web-auth/webauthn-lib": "^4.9.1" + }, + "replace": { + "symfony/polyfill-php80": "*", + "symfony/polyfill-php81": "*", + "symfony/polyfill-ctype": "*", + "symfony/polyfill-mbstring": "*" + }, + "scripts": { + "lint": "find . -name \\*.php -print0 | xargs -0 -n1 php -l", + "pre-autoload-dump": "Aws\\Script\\Composer\\Composer::removeUnusedServices" + }, + "extra": { + "patches-file": "composer.patches.json", + "aws/aws-sdk-php": [ + "S3" + ] + } +} diff --git a/3rdparty/composer.lock b/3rdparty/composer.lock new file mode 100644 index 00000000..bf10b448 --- /dev/null +++ b/3rdparty/composer.lock @@ -0,0 +1,6222 @@ +{ + "_readme": [ + "This file locks the dependencies of your project to a known state", + "Read more about it at https://getcomposer.org/doc/01-basic-usage.md#installing-dependencies", + "This file is @generated automatically" + ], + "content-hash": "cdeb35c7c280b19b222049b8b7a750a9", + "packages": [ + { + "name": "aws/aws-crt-php", + "version": "v1.2.7", + "source": { + "type": "git", + "url": "https://github.com/awslabs/aws-crt-php.git", + "reference": "d71d9906c7bb63a28295447ba12e74723bd3730e" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/awslabs/aws-crt-php/zipball/d71d9906c7bb63a28295447ba12e74723bd3730e", + "reference": "d71d9906c7bb63a28295447ba12e74723bd3730e", + "shasum": "" + }, + "require": { + "php": ">=5.5" + }, + "require-dev": { + "phpunit/phpunit": "^4.8.35||^5.6.3||^9.5", + "yoast/phpunit-polyfills": "^1.0" + }, + "suggest": { + "ext-awscrt": "Make sure you install awscrt native extension to use any of the functionality." + }, + "type": "library", + "autoload": { + "classmap": [ + "src/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "Apache-2.0" + ], + "authors": [ + { + "name": "AWS SDK Common Runtime Team", + "email": "aws-sdk-common-runtime@amazon.com" + } + ], + "description": "AWS Common Runtime for PHP", + "homepage": "https://github.com/awslabs/aws-crt-php", + "keywords": [ + "amazon", + "aws", + "crt", + "sdk" + ], + "support": { + "issues": "https://github.com/awslabs/aws-crt-php/issues", + "source": "https://github.com/awslabs/aws-crt-php/tree/v1.2.7" + }, + "time": "2024-10-18T22:15:13+00:00" + }, + { + "name": "aws/aws-sdk-php", + "version": "3.349.3", + "source": { + "type": "git", + "url": "https://github.com/aws/aws-sdk-php.git", + "reference": "b2d4718786398f47626add9c29840fc416175ef2" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/aws/aws-sdk-php/zipball/b2d4718786398f47626add9c29840fc416175ef2", + "reference": "b2d4718786398f47626add9c29840fc416175ef2", + "shasum": "" + }, + "require": { + "aws/aws-crt-php": "^1.2.3", + "ext-json": "*", + "ext-pcre": "*", + "ext-simplexml": "*", + "guzzlehttp/guzzle": "^7.4.5", + "guzzlehttp/promises": "^2.0", + "guzzlehttp/psr7": "^2.4.5", + "mtdowling/jmespath.php": "^2.8.0", + "php": ">=8.1", + "psr/http-message": "^2.0" + }, + "require-dev": { + "andrewsville/php-token-reflection": "^1.4", + "aws/aws-php-sns-message-validator": "~1.0", + "behat/behat": "~3.0", + "composer/composer": "^2.7.8", + "dms/phpunit-arraysubset-asserts": "^0.4.0", + "doctrine/cache": "~1.4", + "ext-dom": "*", + "ext-openssl": "*", + "ext-pcntl": "*", + "ext-sockets": "*", + "phpunit/phpunit": "^5.6.3 || ^8.5 || ^9.5", + "psr/cache": "^2.0 || ^3.0", + "psr/simple-cache": "^2.0 || ^3.0", + "sebastian/comparator": "^1.2.3 || ^4.0 || ^5.0", + "symfony/filesystem": "^v6.4.0 || ^v7.1.0", + "yoast/phpunit-polyfills": "^2.0" + }, + "suggest": { + "aws/aws-php-sns-message-validator": "To validate incoming SNS notifications", + "doctrine/cache": "To use the DoctrineCacheAdapter", + "ext-curl": "To send requests using cURL", + "ext-openssl": "Allows working with CloudFront private distributions and verifying received SNS messages", + "ext-sockets": "To use client-side monitoring" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "3.0-dev" + } + }, + "autoload": { + "files": [ + "src/functions.php" + ], + "psr-4": { + "Aws\\": "src/" + }, + "exclude-from-classmap": [ + "src/data/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "Apache-2.0" + ], + "authors": [ + { + "name": "Amazon Web Services", + "homepage": "http://aws.amazon.com" + } + ], + "description": "AWS SDK for PHP - Use Amazon Web Services in your PHP project", + "homepage": "http://aws.amazon.com/sdkforphp", + "keywords": [ + "amazon", + "aws", + "cloud", + "dynamodb", + "ec2", + "glacier", + "s3", + "sdk" + ], + "support": { + "forum": "https://github.com/aws/aws-sdk-php/discussions", + "issues": "https://github.com/aws/aws-sdk-php/issues", + "source": "https://github.com/aws/aws-sdk-php/tree/3.349.3" + }, + "time": "2025-07-09T18:10:17+00:00" + }, + { + "name": "bantu/ini-get-wrapper", + "version": "v1.0.1", + "source": { + "type": "git", + "url": "https://github.com/bantuXorg/php-ini-get-wrapper.git", + "reference": "4770c7feab370c62e23db4f31c112b7c6d90aee2" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/bantuXorg/php-ini-get-wrapper/zipball/4770c7feab370c62e23db4f31c112b7c6d90aee2", + "reference": "4770c7feab370c62e23db4f31c112b7c6d90aee2", + "shasum": "" + }, + "require-dev": { + "phpunit/phpunit": "3.7.*" + }, + "type": "library", + "autoload": { + "psr-4": { + "bantu\\IniGetWrapper\\": "src/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "description": "Convenience wrapper around ini_get()", + "support": { + "issues": "https://github.com/bantuXorg/php-ini-get-wrapper/issues", + "source": "https://github.com/bantuXorg/php-ini-get-wrapper/tree/v1.0.1" + }, + "time": "2014-09-15T13:12:35+00:00" + }, + { + "name": "brick/math", + "version": "0.12.1", + "source": { + "type": "git", + "url": "https://github.com/brick/math.git", + "reference": "f510c0a40911935b77b86859eb5223d58d660df1" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/brick/math/zipball/f510c0a40911935b77b86859eb5223d58d660df1", + "reference": "f510c0a40911935b77b86859eb5223d58d660df1", + "shasum": "" + }, + "require": { + "php": "^8.1" + }, + "require-dev": { + "php-coveralls/php-coveralls": "^2.2", + "phpunit/phpunit": "^10.1", + "vimeo/psalm": "5.16.0" + }, + "type": "library", + "autoload": { + "psr-4": { + "Brick\\Math\\": "src/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "description": "Arbitrary-precision arithmetic library", + "keywords": [ + "Arbitrary-precision", + "BigInteger", + "BigRational", + "arithmetic", + "bigdecimal", + "bignum", + "bignumber", + "brick", + "decimal", + "integer", + "math", + "mathematics", + "rational" + ], + "support": { + "issues": "https://github.com/brick/math/issues", + "source": "https://github.com/brick/math/tree/0.12.1" + }, + "funding": [ + { + "url": "https://github.com/BenMorel", + "type": "github" + } + ], + "time": "2023-11-29T23:19:16+00:00" + }, + { + "name": "cweagans/composer-patches", + "version": "1.7.3", + "source": { + "type": "git", + "url": "https://github.com/cweagans/composer-patches.git", + "reference": "e190d4466fe2b103a55467dfa83fc2fecfcaf2db" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/cweagans/composer-patches/zipball/e190d4466fe2b103a55467dfa83fc2fecfcaf2db", + "reference": "e190d4466fe2b103a55467dfa83fc2fecfcaf2db", + "shasum": "" + }, + "require": { + "composer-plugin-api": "^1.0 || ^2.0", + "php": ">=5.3.0" + }, + "require-dev": { + "composer/composer": "~1.0 || ~2.0", + "phpunit/phpunit": "~4.6" + }, + "type": "composer-plugin", + "extra": { + "class": "cweagans\\Composer\\Patches" + }, + "autoload": { + "psr-4": { + "cweagans\\Composer\\": "src" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "BSD-3-Clause" + ], + "authors": [ + { + "name": "Cameron Eagans", + "email": "me@cweagans.net" + } + ], + "description": "Provides a way to patch Composer packages.", + "support": { + "issues": "https://github.com/cweagans/composer-patches/issues", + "source": "https://github.com/cweagans/composer-patches/tree/1.7.3" + }, + "time": "2022-12-20T22:53:13+00:00" + }, + { + "name": "deepdiver/zipstreamer", + "version": "v2.0.3", + "source": { + "type": "git", + "url": "https://github.com/DeepDiver1975/PHPZipStreamer.git", + "reference": "b9d1f53453a5736285facb723252ea2169dc472e" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/DeepDiver1975/PHPZipStreamer/zipball/b9d1f53453a5736285facb723252ea2169dc472e", + "reference": "b9d1f53453a5736285facb723252ea2169dc472e", + "shasum": "" + }, + "require": { + "ext-mbstring": "*", + "php": ">=7.1" + }, + "require-dev": { + "ext-xdebug": "*", + "ext-zlib": "*", + "phpunit/phpunit": "^7 || ^8" + }, + "suggest": { + "ext-http": ">=0.10" + }, + "type": "library", + "autoload": { + "psr-4": { + "ZipStreamer\\": "src/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "GPL-3.0-or-later" + ], + "authors": [ + { + "name": "Nicolai Ehemann", + "email": "en@enlightened.de", + "role": "Author/Maintainer" + }, + { + "name": "André Rothe", + "email": "arothe@zks.uni-leipzig.de", + "role": "Contributor" + }, + { + "name": "Lukas Reschke", + "email": "lukas@owncloud.com", + "role": "Contributor" + }, + { + "name": "Thomas Müller", + "email": "thomas.mueller@tmit.eu", + "role": "Contributor" + }, + { + "name": "Roeland Jago Douma", + "email": "roeland@famdouma.nl", + "role": "Contributor" + } + ], + "description": "Stream zip files without i/o overhead", + "homepage": "https://github.com/DeepDiver1975/PHPZipStreamer", + "keywords": [ + "stream", + "zip" + ], + "support": { + "issues": "https://github.com/DeepDiver1975/PHPZipStreamer/issues", + "source": "https://github.com/DeepDiver1975/PHPZipStreamer/tree/v2.0.3" + }, + "funding": [ + { + "url": "https://github.com/DeepDiver1975", + "type": "github" + } + ], + "time": "2024-03-13T14:30:52+00:00" + }, + { + "name": "deepdiver1975/tarstreamer", + "version": "v2.1.0", + "source": { + "type": "git", + "url": "https://github.com/owncloud/TarStreamer.git", + "reference": "163052d7a076fd3dd54d4f50e1ff2705b72604db" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/owncloud/TarStreamer/zipball/163052d7a076fd3dd54d4f50e1ff2705b72604db", + "reference": "163052d7a076fd3dd54d4f50e1ff2705b72604db", + "shasum": "" + }, + "require": { + "php": ">=7.1" + }, + "require-dev": { + "bamarni/composer-bin-plugin": "^1.5", + "pear/archive_tar": "~1.4", + "pear/pear-core-minimal": "v1.10.13", + "phpunit/phpunit": "^7.5|^8.5|^9.6" + }, + "type": "library", + "extra": { + "bamarni-bin": { + "bin-links": false + } + }, + "autoload": { + "psr-4": { + "ownCloud\\TarStreamer\\": "src/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "description": "A library for dynamically streaming dynamic tar files without the need to have the complete file stored on the server.", + "homepage": "https://github.com/owncloud/TarStreamer", + "keywords": [ + "archive", + "php", + "stream", + "tar" + ], + "support": { + "issues": "https://github.com/owncloud/TarStreamer/issues", + "source": "https://github.com/owncloud/TarStreamer/tree/v2.1.0" + }, + "time": "2023-06-16T08:01:55+00:00" + }, + { + "name": "doctrine/dbal", + "version": "3.10.2", + "source": { + "type": "git", + "url": "https://github.com/doctrine/dbal.git", + "reference": "c6c16cf787eaba3112203dfcd715fa2059c62282" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/doctrine/dbal/zipball/c6c16cf787eaba3112203dfcd715fa2059c62282", + "reference": "c6c16cf787eaba3112203dfcd715fa2059c62282", + "shasum": "" + }, + "require": { + "composer-runtime-api": "^2", + "doctrine/deprecations": "^0.5.3|^1", + "doctrine/event-manager": "^1|^2", + "php": "^7.4 || ^8.0", + "psr/cache": "^1|^2|^3", + "psr/log": "^1|^2|^3" + }, + "conflict": { + "doctrine/cache": "< 1.11" + }, + "require-dev": { + "doctrine/cache": "^1.11|^2.0", + "doctrine/coding-standard": "13.0.1", + "fig/log-test": "^1", + "jetbrains/phpstorm-stubs": "2023.1", + "phpstan/phpstan": "2.1.22", + "phpstan/phpstan-strict-rules": "^2", + "phpunit/phpunit": "9.6.23", + "slevomat/coding-standard": "8.16.2", + "squizlabs/php_codesniffer": "3.13.1", + "symfony/cache": "^5.4|^6.0|^7.0", + "symfony/console": "^4.4|^5.4|^6.0|^7.0" + }, + "suggest": { + "symfony/console": "For helpful console commands such as SQL execution and import of files." + }, + "bin": [ + "bin/doctrine-dbal" + ], + "type": "library", + "autoload": { + "psr-4": { + "Doctrine\\DBAL\\": "src" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Guilherme Blanco", + "email": "guilhermeblanco@gmail.com" + }, + { + "name": "Roman Borschel", + "email": "roman@code-factory.org" + }, + { + "name": "Benjamin Eberlei", + "email": "kontakt@beberlei.de" + }, + { + "name": "Jonathan Wage", + "email": "jonwage@gmail.com" + } + ], + "description": "Powerful PHP database abstraction layer (DBAL) with many features for database schema introspection and management.", + "homepage": "https://www.doctrine-project.org/projects/dbal.html", + "keywords": [ + "abstraction", + "database", + "db2", + "dbal", + "mariadb", + "mssql", + "mysql", + "oci8", + "oracle", + "pdo", + "pgsql", + "postgresql", + "queryobject", + "sasql", + "sql", + "sqlite", + "sqlserver", + "sqlsrv" + ], + "support": { + "issues": "https://github.com/doctrine/dbal/issues", + "source": "https://github.com/doctrine/dbal/tree/3.10.2" + }, + "funding": [ + { + "url": "https://www.doctrine-project.org/sponsorship.html", + "type": "custom" + }, + { + "url": "https://www.patreon.com/phpdoctrine", + "type": "patreon" + }, + { + "url": "https://tidelift.com/funding/github/packagist/doctrine%2Fdbal", + "type": "tidelift" + } + ], + "time": "2025-09-04T23:51:27+00:00" + }, + { + "name": "doctrine/deprecations", + "version": "1.1.5", + "source": { + "type": "git", + "url": "https://github.com/doctrine/deprecations.git", + "reference": "459c2f5dd3d6a4633d3b5f46ee2b1c40f57d3f38" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/doctrine/deprecations/zipball/459c2f5dd3d6a4633d3b5f46ee2b1c40f57d3f38", + "reference": "459c2f5dd3d6a4633d3b5f46ee2b1c40f57d3f38", + "shasum": "" + }, + "require": { + "php": "^7.1 || ^8.0" + }, + "conflict": { + "phpunit/phpunit": "<=7.5 || >=13" + }, + "require-dev": { + "doctrine/coding-standard": "^9 || ^12 || ^13", + "phpstan/phpstan": "1.4.10 || 2.1.11", + "phpstan/phpstan-phpunit": "^1.0 || ^2", + "phpunit/phpunit": "^7.5 || ^8.5 || ^9.6 || ^10.5 || ^11.5 || ^12", + "psr/log": "^1 || ^2 || ^3" + }, + "suggest": { + "psr/log": "Allows logging deprecations via PSR-3 logger implementation" + }, + "type": "library", + "autoload": { + "psr-4": { + "Doctrine\\Deprecations\\": "src" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "description": "A small layer on top of trigger_error(E_USER_DEPRECATED) or PSR-3 logging with options to disable all deprecations or selectively for packages.", + "homepage": "https://www.doctrine-project.org/", + "support": { + "issues": "https://github.com/doctrine/deprecations/issues", + "source": "https://github.com/doctrine/deprecations/tree/1.1.5" + }, + "time": "2025-04-07T20:06:18+00:00" + }, + { + "name": "doctrine/event-manager", + "version": "2.0.1", + "source": { + "type": "git", + "url": "https://github.com/doctrine/event-manager.git", + "reference": "b680156fa328f1dfd874fd48c7026c41570b9c6e" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/doctrine/event-manager/zipball/b680156fa328f1dfd874fd48c7026c41570b9c6e", + "reference": "b680156fa328f1dfd874fd48c7026c41570b9c6e", + "shasum": "" + }, + "require": { + "php": "^8.1" + }, + "conflict": { + "doctrine/common": "<2.9" + }, + "require-dev": { + "doctrine/coding-standard": "^12", + "phpstan/phpstan": "^1.8.8", + "phpunit/phpunit": "^10.5", + "vimeo/psalm": "^5.24" + }, + "type": "library", + "autoload": { + "psr-4": { + "Doctrine\\Common\\": "src" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Guilherme Blanco", + "email": "guilhermeblanco@gmail.com" + }, + { + "name": "Roman Borschel", + "email": "roman@code-factory.org" + }, + { + "name": "Benjamin Eberlei", + "email": "kontakt@beberlei.de" + }, + { + "name": "Jonathan Wage", + "email": "jonwage@gmail.com" + }, + { + "name": "Johannes Schmitt", + "email": "schmittjoh@gmail.com" + }, + { + "name": "Marco Pivetta", + "email": "ocramius@gmail.com" + } + ], + "description": "The Doctrine Event Manager is a simple PHP event system that was built to be used with the various Doctrine projects.", + "homepage": "https://www.doctrine-project.org/projects/event-manager.html", + "keywords": [ + "event", + "event dispatcher", + "event manager", + "event system", + "events" + ], + "support": { + "issues": "https://github.com/doctrine/event-manager/issues", + "source": "https://github.com/doctrine/event-manager/tree/2.0.1" + }, + "funding": [ + { + "url": "https://www.doctrine-project.org/sponsorship.html", + "type": "custom" + }, + { + "url": "https://www.patreon.com/phpdoctrine", + "type": "patreon" + }, + { + "url": "https://tidelift.com/funding/github/packagist/doctrine%2Fevent-manager", + "type": "tidelift" + } + ], + "time": "2024-05-22T20:47:39+00:00" + }, + { + "name": "doctrine/lexer", + "version": "3.0.1", + "source": { + "type": "git", + "url": "https://github.com/doctrine/lexer.git", + "reference": "31ad66abc0fc9e1a1f2d9bc6a42668d2fbbcd6dd" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/doctrine/lexer/zipball/31ad66abc0fc9e1a1f2d9bc6a42668d2fbbcd6dd", + "reference": "31ad66abc0fc9e1a1f2d9bc6a42668d2fbbcd6dd", + "shasum": "" + }, + "require": { + "php": "^8.1" + }, + "require-dev": { + "doctrine/coding-standard": "^12", + "phpstan/phpstan": "^1.10", + "phpunit/phpunit": "^10.5", + "psalm/plugin-phpunit": "^0.18.3", + "vimeo/psalm": "^5.21" + }, + "type": "library", + "autoload": { + "psr-4": { + "Doctrine\\Common\\Lexer\\": "src" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Guilherme Blanco", + "email": "guilhermeblanco@gmail.com" + }, + { + "name": "Roman Borschel", + "email": "roman@code-factory.org" + }, + { + "name": "Johannes Schmitt", + "email": "schmittjoh@gmail.com" + } + ], + "description": "PHP Doctrine Lexer parser library that can be used in Top-Down, Recursive Descent Parsers.", + "homepage": "https://www.doctrine-project.org/projects/lexer.html", + "keywords": [ + "annotations", + "docblock", + "lexer", + "parser", + "php" + ], + "support": { + "issues": "https://github.com/doctrine/lexer/issues", + "source": "https://github.com/doctrine/lexer/tree/3.0.1" + }, + "funding": [ + { + "url": "https://www.doctrine-project.org/sponsorship.html", + "type": "custom" + }, + { + "url": "https://www.patreon.com/phpdoctrine", + "type": "patreon" + }, + { + "url": "https://tidelift.com/funding/github/packagist/doctrine%2Flexer", + "type": "tidelift" + } + ], + "time": "2024-02-05T11:56:58+00:00" + }, + { + "name": "egulias/email-validator", + "version": "4.0.4", + "source": { + "type": "git", + "url": "https://github.com/egulias/EmailValidator.git", + "reference": "d42c8731f0624ad6bdc8d3e5e9a4524f68801cfa" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/egulias/EmailValidator/zipball/d42c8731f0624ad6bdc8d3e5e9a4524f68801cfa", + "reference": "d42c8731f0624ad6bdc8d3e5e9a4524f68801cfa", + "shasum": "" + }, + "require": { + "doctrine/lexer": "^2.0 || ^3.0", + "php": ">=8.1", + "symfony/polyfill-intl-idn": "^1.26" + }, + "require-dev": { + "phpunit/phpunit": "^10.2", + "vimeo/psalm": "^5.12" + }, + "suggest": { + "ext-intl": "PHP Internationalization Libraries are required to use the SpoofChecking validation" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "4.0.x-dev" + } + }, + "autoload": { + "psr-4": { + "Egulias\\EmailValidator\\": "src" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Eduardo Gulias Davis" + } + ], + "description": "A library for validating emails against several RFCs", + "homepage": "https://github.com/egulias/EmailValidator", + "keywords": [ + "email", + "emailvalidation", + "emailvalidator", + "validation", + "validator" + ], + "support": { + "issues": "https://github.com/egulias/EmailValidator/issues", + "source": "https://github.com/egulias/EmailValidator/tree/4.0.4" + }, + "funding": [ + { + "url": "https://github.com/egulias", + "type": "github" + } + ], + "time": "2025-03-06T22:45:56+00:00" + }, + { + "name": "fusonic/opengraph", + "version": "v3.0.0", + "source": { + "type": "git", + "url": "https://github.com/fusonic/opengraph.git", + "reference": "2daa6dce84f23b1bb6d66bf03b3e9371c39cd378" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/fusonic/opengraph/zipball/2daa6dce84f23b1bb6d66bf03b3e9371c39cd378", + "reference": "2daa6dce84f23b1bb6d66bf03b3e9371c39cd378", + "shasum": "" + }, + "require": { + "ext-dom": "*", + "php": "^8.1", + "psr/http-client": "^1.0", + "psr/http-factory": "^1.0", + "symfony/css-selector": "^5.4 || ^6.4 || ^7.1", + "symfony/dom-crawler": "^5.4 || ^6.4 || ^7.1" + }, + "require-dev": { + "friendsofphp/php-cs-fixer": "^3.65", + "nyholm/psr7": "^1.8", + "phpstan/phpstan": "^2.0", + "phpstan/phpstan-deprecation-rules": "^2.0", + "phpstan/phpstan-phpunit": "^2.0", + "phpstan/phpstan-strict-rules": "^2.0", + "phpunit/phpunit": "^10.5 || ^11.4", + "symfony/http-client": "^5.4 || ^6.4 || ^7.1" + }, + "suggest": { + "nyholm/psr7": "^1.8", + "symfony/http-client": "^5.4 || ^6.4 || ^7.1" + }, + "type": "library", + "autoload": { + "psr-4": { + "Fusonic\\OpenGraph\\": "src/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Fusonic", + "homepage": "https://www.fusonic.net" + } + ], + "description": "PHP library for consuming and publishing Open Graph resources.", + "homepage": "https://github.com/fusonic/opengraph", + "keywords": [ + "opengraph" + ], + "support": { + "issues": "https://github.com/fusonic/opengraph/issues", + "source": "https://github.com/fusonic/opengraph/tree/v3.0.0" + }, + "time": "2025-01-13T07:23:24+00:00" + }, + { + "name": "giggsey/libphonenumber-for-php-lite", + "version": "9.0.17", + "source": { + "type": "git", + "url": "https://github.com/giggsey/libphonenumber-for-php-lite.git", + "reference": "430a602c6e5a03932b732226daf64aeeab5c6c65" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/giggsey/libphonenumber-for-php-lite/zipball/430a602c6e5a03932b732226daf64aeeab5c6c65", + "reference": "430a602c6e5a03932b732226daf64aeeab5c6c65", + "shasum": "" + }, + "require": { + "php": "^8.1", + "symfony/polyfill-mbstring": "^1.17" + }, + "conflict": { + "giggsey/libphonenumber-for-php": "*" + }, + "require-dev": { + "ext-dom": "*", + "friendsofphp/php-cs-fixer": "^3.71", + "infection/infection": "^0.29|^0.31.0", + "nette/php-generator": "^4.1", + "php-coveralls/php-coveralls": "^2.7", + "phpstan/extension-installer": "^1.4.3", + "phpstan/phpstan": "^2.1.7", + "phpstan/phpstan-deprecation-rules": "^2.0.1", + "phpstan/phpstan-phpunit": "^2.0.4", + "phpstan/phpstan-strict-rules": "^2.0.3", + "phpunit/phpunit": "^10.5.45", + "symfony/console": "^6.4", + "symfony/filesystem": "^6.4", + "symfony/process": "^6.4" + }, + "suggest": { + "giggsey/libphonenumber-for-php": "Use libphonenumber-for-php for geocoding, carriers, timezones and matching" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "9.x-dev" + } + }, + "autoload": { + "psr-4": { + "libphonenumber\\": "src/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "Apache-2.0" + ], + "authors": [ + { + "name": "Joshua Gigg", + "email": "giggsey@gmail.com", + "homepage": "https://giggsey.com/" + } + ], + "description": "A lite version of giggsey/libphonenumber-for-php, which is a PHP Port of Google's libphonenumber", + "homepage": "https://github.com/giggsey/libphonenumber-for-php-lite", + "keywords": [ + "geocoding", + "geolocation", + "libphonenumber", + "mobile", + "phonenumber", + "validation" + ], + "support": { + "issues": "https://github.com/giggsey/libphonenumber-for-php-lite/issues", + "source": "https://github.com/giggsey/libphonenumber-for-php-lite" + }, + "time": "2025-10-24T07:09:56+00:00" + }, + { + "name": "guzzlehttp/guzzle", + "version": "7.9.3", + "source": { + "type": "git", + "url": "https://github.com/guzzle/guzzle.git", + "reference": "7b2f29fe81dc4da0ca0ea7d42107a0845946ea77" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/guzzle/guzzle/zipball/7b2f29fe81dc4da0ca0ea7d42107a0845946ea77", + "reference": "7b2f29fe81dc4da0ca0ea7d42107a0845946ea77", + "shasum": "" + }, + "require": { + "ext-json": "*", + "guzzlehttp/promises": "^1.5.3 || ^2.0.3", + "guzzlehttp/psr7": "^2.7.0", + "php": "^7.2.5 || ^8.0", + "psr/http-client": "^1.0", + "symfony/deprecation-contracts": "^2.2 || ^3.0" + }, + "provide": { + "psr/http-client-implementation": "1.0" + }, + "require-dev": { + "bamarni/composer-bin-plugin": "^1.8.2", + "ext-curl": "*", + "guzzle/client-integration-tests": "3.0.2", + "php-http/message-factory": "^1.1", + "phpunit/phpunit": "^8.5.39 || ^9.6.20", + "psr/log": "^1.1 || ^2.0 || ^3.0" + }, + "suggest": { + "ext-curl": "Required for CURL handler support", + "ext-intl": "Required for Internationalized Domain Name (IDN) support", + "psr/log": "Required for using the Log middleware" + }, + "type": "library", + "extra": { + "bamarni-bin": { + "bin-links": true, + "forward-command": false + } + }, + "autoload": { + "files": [ + "src/functions_include.php" + ], + "psr-4": { + "GuzzleHttp\\": "src/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Graham Campbell", + "email": "hello@gjcampbell.co.uk", + "homepage": "https://github.com/GrahamCampbell" + }, + { + "name": "Michael Dowling", + "email": "mtdowling@gmail.com", + "homepage": "https://github.com/mtdowling" + }, + { + "name": "Jeremy Lindblom", + "email": "jeremeamia@gmail.com", + "homepage": "https://github.com/jeremeamia" + }, + { + "name": "George Mponos", + "email": "gmponos@gmail.com", + "homepage": "https://github.com/gmponos" + }, + { + "name": "Tobias Nyholm", + "email": "tobias.nyholm@gmail.com", + "homepage": "https://github.com/Nyholm" + }, + { + "name": "Márk Sági-Kazár", + "email": "mark.sagikazar@gmail.com", + "homepage": "https://github.com/sagikazarmark" + }, + { + "name": "Tobias Schultze", + "email": "webmaster@tubo-world.de", + "homepage": "https://github.com/Tobion" + } + ], + "description": "Guzzle is a PHP HTTP client library", + "keywords": [ + "client", + "curl", + "framework", + "http", + "http client", + "psr-18", + "psr-7", + "rest", + "web service" + ], + "support": { + "issues": "https://github.com/guzzle/guzzle/issues", + "source": "https://github.com/guzzle/guzzle/tree/7.9.3" + }, + "funding": [ + { + "url": "https://github.com/GrahamCampbell", + "type": "github" + }, + { + "url": "https://github.com/Nyholm", + "type": "github" + }, + { + "url": "https://tidelift.com/funding/github/packagist/guzzlehttp/guzzle", + "type": "tidelift" + } + ], + "time": "2025-03-27T13:37:11+00:00" + }, + { + "name": "guzzlehttp/promises", + "version": "2.2.0", + "source": { + "type": "git", + "url": "https://github.com/guzzle/promises.git", + "reference": "7c69f28996b0a6920945dd20b3857e499d9ca96c" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/guzzle/promises/zipball/7c69f28996b0a6920945dd20b3857e499d9ca96c", + "reference": "7c69f28996b0a6920945dd20b3857e499d9ca96c", + "shasum": "" + }, + "require": { + "php": "^7.2.5 || ^8.0" + }, + "require-dev": { + "bamarni/composer-bin-plugin": "^1.8.2", + "phpunit/phpunit": "^8.5.39 || ^9.6.20" + }, + "type": "library", + "extra": { + "bamarni-bin": { + "bin-links": true, + "forward-command": false + } + }, + "autoload": { + "psr-4": { + "GuzzleHttp\\Promise\\": "src/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Graham Campbell", + "email": "hello@gjcampbell.co.uk", + "homepage": "https://github.com/GrahamCampbell" + }, + { + "name": "Michael Dowling", + "email": "mtdowling@gmail.com", + "homepage": "https://github.com/mtdowling" + }, + { + "name": "Tobias Nyholm", + "email": "tobias.nyholm@gmail.com", + "homepage": "https://github.com/Nyholm" + }, + { + "name": "Tobias Schultze", + "email": "webmaster@tubo-world.de", + "homepage": "https://github.com/Tobion" + } + ], + "description": "Guzzle promises library", + "keywords": [ + "promise" + ], + "support": { + "issues": "https://github.com/guzzle/promises/issues", + "source": "https://github.com/guzzle/promises/tree/2.2.0" + }, + "funding": [ + { + "url": "https://github.com/GrahamCampbell", + "type": "github" + }, + { + "url": "https://github.com/Nyholm", + "type": "github" + }, + { + "url": "https://tidelift.com/funding/github/packagist/guzzlehttp/promises", + "type": "tidelift" + } + ], + "time": "2025-03-27T13:27:01+00:00" + }, + { + "name": "guzzlehttp/psr7", + "version": "2.7.1", + "source": { + "type": "git", + "url": "https://github.com/guzzle/psr7.git", + "reference": "c2270caaabe631b3b44c85f99e5a04bbb8060d16" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/guzzle/psr7/zipball/c2270caaabe631b3b44c85f99e5a04bbb8060d16", + "reference": "c2270caaabe631b3b44c85f99e5a04bbb8060d16", + "shasum": "" + }, + "require": { + "php": "^7.2.5 || ^8.0", + "psr/http-factory": "^1.0", + "psr/http-message": "^1.1 || ^2.0", + "ralouphie/getallheaders": "^3.0" + }, + "provide": { + "psr/http-factory-implementation": "1.0", + "psr/http-message-implementation": "1.0" + }, + "require-dev": { + "bamarni/composer-bin-plugin": "^1.8.2", + "http-interop/http-factory-tests": "0.9.0", + "phpunit/phpunit": "^8.5.39 || ^9.6.20" + }, + "suggest": { + "laminas/laminas-httphandlerrunner": "Emit PSR-7 responses" + }, + "type": "library", + "extra": { + "bamarni-bin": { + "bin-links": true, + "forward-command": false + } + }, + "autoload": { + "psr-4": { + "GuzzleHttp\\Psr7\\": "src/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Graham Campbell", + "email": "hello@gjcampbell.co.uk", + "homepage": "https://github.com/GrahamCampbell" + }, + { + "name": "Michael Dowling", + "email": "mtdowling@gmail.com", + "homepage": "https://github.com/mtdowling" + }, + { + "name": "George Mponos", + "email": "gmponos@gmail.com", + "homepage": "https://github.com/gmponos" + }, + { + "name": "Tobias Nyholm", + "email": "tobias.nyholm@gmail.com", + "homepage": "https://github.com/Nyholm" + }, + { + "name": "Márk Sági-Kazár", + "email": "mark.sagikazar@gmail.com", + "homepage": "https://github.com/sagikazarmark" + }, + { + "name": "Tobias Schultze", + "email": "webmaster@tubo-world.de", + "homepage": "https://github.com/Tobion" + }, + { + "name": "Márk Sági-Kazár", + "email": "mark.sagikazar@gmail.com", + "homepage": "https://sagikazarmark.hu" + } + ], + "description": "PSR-7 message implementation that also provides common utility methods", + "keywords": [ + "http", + "message", + "psr-7", + "request", + "response", + "stream", + "uri", + "url" + ], + "support": { + "issues": "https://github.com/guzzle/psr7/issues", + "source": "https://github.com/guzzle/psr7/tree/2.7.1" + }, + "funding": [ + { + "url": "https://github.com/GrahamCampbell", + "type": "github" + }, + { + "url": "https://github.com/Nyholm", + "type": "github" + }, + { + "url": "https://tidelift.com/funding/github/packagist/guzzlehttp/psr7", + "type": "tidelift" + } + ], + "time": "2025-03-27T12:30:47+00:00" + }, + { + "name": "guzzlehttp/uri-template", + "version": "v1.0.4", + "source": { + "type": "git", + "url": "https://github.com/guzzle/uri-template.git", + "reference": "30e286560c137526eccd4ce21b2de477ab0676d2" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/guzzle/uri-template/zipball/30e286560c137526eccd4ce21b2de477ab0676d2", + "reference": "30e286560c137526eccd4ce21b2de477ab0676d2", + "shasum": "" + }, + "require": { + "php": "^7.2.5 || ^8.0", + "symfony/polyfill-php80": "^1.24" + }, + "require-dev": { + "bamarni/composer-bin-plugin": "^1.8.2", + "phpunit/phpunit": "^8.5.36 || ^9.6.15", + "uri-template/tests": "1.0.0" + }, + "type": "library", + "extra": { + "bamarni-bin": { + "bin-links": true, + "forward-command": false + } + }, + "autoload": { + "psr-4": { + "GuzzleHttp\\UriTemplate\\": "src" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Graham Campbell", + "email": "hello@gjcampbell.co.uk", + "homepage": "https://github.com/GrahamCampbell" + }, + { + "name": "Michael Dowling", + "email": "mtdowling@gmail.com", + "homepage": "https://github.com/mtdowling" + }, + { + "name": "George Mponos", + "email": "gmponos@gmail.com", + "homepage": "https://github.com/gmponos" + }, + { + "name": "Tobias Nyholm", + "email": "tobias.nyholm@gmail.com", + "homepage": "https://github.com/Nyholm" + } + ], + "description": "A polyfill class for uri_template of PHP", + "keywords": [ + "guzzlehttp", + "uri-template" + ], + "support": { + "issues": "https://github.com/guzzle/uri-template/issues", + "source": "https://github.com/guzzle/uri-template/tree/v1.0.4" + }, + "funding": [ + { + "url": "https://github.com/GrahamCampbell", + "type": "github" + }, + { + "url": "https://github.com/Nyholm", + "type": "github" + }, + { + "url": "https://tidelift.com/funding/github/packagist/guzzlehttp/uri-template", + "type": "tidelift" + } + ], + "time": "2025-02-03T10:55:03+00:00" + }, + { + "name": "icewind/searchdav", + "version": "v3.2.0", + "source": { + "type": "git", + "url": "https://codeberg.org/icewind/SearchDAV", + "reference": "3865288b6962de33086d4e02ec3584610c32b773" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/icewind1991/SearchDAV/zipball/3865288b6962de33086d4e02ec3584610c32b773", + "reference": "3865288b6962de33086d4e02ec3584610c32b773", + "shasum": "" + }, + "require": { + "php": ">=7.3 || >=8.0", + "sabre/dav": "^4.0.0" + }, + "require-dev": { + "friendsofphp/php-cs-fixer": "^2", + "php-parallel-lint/php-parallel-lint": "^1.0", + "phpstan/phpstan": "^0.12", + "phpunit/phpunit": "^8", + "psalm/phar": "^4.3" + }, + "type": "library", + "autoload": { + "psr-4": { + "SearchDAV\\": "src/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "AGPL-3.0-or-later" + ], + "authors": [ + { + "name": "Robin Appelman", + "email": "robin@icewind.nl" + } + ], + "description": "sabre/dav plugin to implement rfc5323 SEARCH", + "support": { + "issues": "https://github.com/icewind1991/SearchDAV/issues", + "source": "https://github.com/icewind1991/SearchDAV/tree/v3.2.0" + }, + "time": "2024-11-08T15:54:16+00:00" + }, + { + "name": "icewind/smb", + "version": "v3.7.0", + "source": { + "type": "git", + "url": "https://codeberg.org/icewind/SMB", + "reference": "e6904cbe75f678335092f4861c60c656b1a99e84" + }, + "require": { + "icewind/streams": ">=0.7.3", + "php": ">=7.2" + }, + "require-dev": { + "friendsofphp/php-cs-fixer": "^2.16", + "phpstan/phpstan": "^0.12.57", + "phpunit/phpunit": "^8.5|^9.3.8", + "psalm/phar": "^4.3" + }, + "type": "library", + "autoload": { + "psr-4": { + "Icewind\\SMB\\": "src/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Robin Appelman", + "email": "icewind@owncloud.com" + } + ], + "description": "php wrapper for smbclient and libsmbclient-php", + "time": "2024-11-11T14:08:34+00:00" + }, + { + "name": "icewind/streams", + "version": "v0.7.8", + "source": { + "type": "git", + "url": "https://codeberg.org/icewind/streams", + "reference": "cb2bd3ed41b516efb97e06e8da35a12ef58ba48b" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/icewind1991/Streams/zipball/cb2bd3ed41b516efb97e06e8da35a12ef58ba48b", + "reference": "cb2bd3ed41b516efb97e06e8da35a12ef58ba48b", + "shasum": "" + }, + "require": { + "php": ">=7.1" + }, + "require-dev": { + "friendsofphp/php-cs-fixer": "^2", + "phpstan/phpstan": "^0.12", + "phpunit/phpunit": "^9" + }, + "type": "library", + "autoload": { + "psr-4": { + "Icewind\\Streams\\": "src/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Robin Appelman", + "email": "icewind@owncloud.com" + } + ], + "description": "A set of generic stream wrappers", + "support": { + "issues": "https://github.com/icewind1991/Streams/issues", + "source": "https://github.com/icewind1991/Streams/tree/v0.7.8" + }, + "time": "2024-12-05T14:36:22+00:00" + }, + { + "name": "justinrainbow/json-schema", + "version": "6.4.2", + "source": { + "type": "git", + "url": "https://github.com/jsonrainbow/json-schema.git", + "reference": "ce1fd2d47799bb60668643bc6220f6278a4c1d02" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/jsonrainbow/json-schema/zipball/ce1fd2d47799bb60668643bc6220f6278a4c1d02", + "reference": "ce1fd2d47799bb60668643bc6220f6278a4c1d02", + "shasum": "" + }, + "require": { + "ext-json": "*", + "marc-mabe/php-enum": "^4.0", + "php": "^7.2 || ^8.0" + }, + "require-dev": { + "friendsofphp/php-cs-fixer": "3.3.0", + "json-schema/json-schema-test-suite": "1.2.0", + "marc-mabe/php-enum-phpstan": "^2.0", + "phpspec/prophecy": "^1.19", + "phpstan/phpstan": "^1.12", + "phpunit/phpunit": "^8.5" + }, + "bin": [ + "bin/validate-json" + ], + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "6.x-dev" + } + }, + "autoload": { + "psr-4": { + "JsonSchema\\": "src/JsonSchema/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Bruno Prieto Reis", + "email": "bruno.p.reis@gmail.com" + }, + { + "name": "Justin Rainbow", + "email": "justin.rainbow@gmail.com" + }, + { + "name": "Igor Wiedler", + "email": "igor@wiedler.ch" + }, + { + "name": "Robert Schönthal", + "email": "seroscho@googlemail.com" + } + ], + "description": "A library to validate a json schema.", + "homepage": "https://github.com/jsonrainbow/json-schema", + "keywords": [ + "json", + "schema" + ], + "support": { + "issues": "https://github.com/jsonrainbow/json-schema/issues", + "source": "https://github.com/jsonrainbow/json-schema/tree/6.4.2" + }, + "time": "2025-06-03T18:27:04+00:00" + }, + { + "name": "kornrunner/blurhash", + "version": "v1.2.2", + "source": { + "type": "git", + "url": "https://github.com/kornrunner/php-blurhash.git", + "reference": "bc8a4596cb0a49874f0158696a382ab3933fefe4" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/kornrunner/php-blurhash/zipball/bc8a4596cb0a49874f0158696a382ab3933fefe4", + "reference": "bc8a4596cb0a49874f0158696a382ab3933fefe4", + "shasum": "" + }, + "require": { + "php": "^7.3|^8.0" + }, + "require-dev": { + "ext-gd": "*", + "ocramius/package-versions": "^1.4|^2.0", + "phpstan/phpstan": "^0.12", + "phpunit/phpunit": "^9", + "vimeo/psalm": "^4.3" + }, + "type": "library", + "autoload": { + "psr-4": { + "kornrunner\\Blurhash\\": "src" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Boris Momčilović", + "email": "boris.momcilovic@gmail.com" + } + ], + "description": "Pure PHP implementation of Blurhash", + "homepage": "https://github.com/kornrunner/php-blurhash", + "support": { + "issues": "https://github.com/kornrunner/php-blurhash/issues", + "source": "https://github.com/kornrunner/php-blurhash.git" + }, + "time": "2022-07-13T19:38:39+00:00" + }, + { + "name": "laravel/serializable-closure", + "version": "v2.0.4", + "source": { + "type": "git", + "url": "https://github.com/laravel/serializable-closure.git", + "reference": "b352cf0534aa1ae6b4d825d1e762e35d43f8a841" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/laravel/serializable-closure/zipball/b352cf0534aa1ae6b4d825d1e762e35d43f8a841", + "reference": "b352cf0534aa1ae6b4d825d1e762e35d43f8a841", + "shasum": "" + }, + "require": { + "php": "^8.1" + }, + "require-dev": { + "illuminate/support": "^10.0|^11.0|^12.0", + "nesbot/carbon": "^2.67|^3.0", + "pestphp/pest": "^2.36|^3.0", + "phpstan/phpstan": "^2.0", + "symfony/var-dumper": "^6.2.0|^7.0.0" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "2.x-dev" + } + }, + "autoload": { + "psr-4": { + "Laravel\\SerializableClosure\\": "src/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Taylor Otwell", + "email": "taylor@laravel.com" + }, + { + "name": "Nuno Maduro", + "email": "nuno@laravel.com" + } + ], + "description": "Laravel Serializable Closure provides an easy and secure way to serialize closures in PHP.", + "keywords": [ + "closure", + "laravel", + "serializable" + ], + "support": { + "issues": "https://github.com/laravel/serializable-closure/issues", + "source": "https://github.com/laravel/serializable-closure" + }, + "time": "2025-03-19T13:51:03+00:00" + }, + { + "name": "lcobucci/clock", + "version": "3.0.0", + "source": { + "type": "git", + "url": "https://github.com/lcobucci/clock.git", + "reference": "039ef98c6b57b101d10bd11d8fdfda12cbd996dc" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/lcobucci/clock/zipball/039ef98c6b57b101d10bd11d8fdfda12cbd996dc", + "reference": "039ef98c6b57b101d10bd11d8fdfda12cbd996dc", + "shasum": "" + }, + "require": { + "php": "~8.1.0 || ~8.2.0", + "psr/clock": "^1.0" + }, + "provide": { + "psr/clock-implementation": "1.0" + }, + "require-dev": { + "infection/infection": "^0.26", + "lcobucci/coding-standard": "^9.0", + "phpstan/extension-installer": "^1.2", + "phpstan/phpstan": "^1.9.4", + "phpstan/phpstan-deprecation-rules": "^1.1.1", + "phpstan/phpstan-phpunit": "^1.3.2", + "phpstan/phpstan-strict-rules": "^1.4.4", + "phpunit/phpunit": "^9.5.27" + }, + "type": "library", + "autoload": { + "psr-4": { + "Lcobucci\\Clock\\": "src" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Luís Cobucci", + "email": "lcobucci@gmail.com" + } + ], + "description": "Yet another clock abstraction", + "support": { + "issues": "https://github.com/lcobucci/clock/issues", + "source": "https://github.com/lcobucci/clock/tree/3.0.0" + }, + "funding": [ + { + "url": "https://github.com/lcobucci", + "type": "github" + }, + { + "url": "https://www.patreon.com/lcobucci", + "type": "patreon" + } + ], + "time": "2022-12-19T15:00:24+00:00" + }, + { + "name": "marc-mabe/php-enum", + "version": "v4.7.1", + "source": { + "type": "git", + "url": "https://github.com/marc-mabe/php-enum.git", + "reference": "7159809e5cfa041dca28e61f7f7ae58063aae8ed" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/marc-mabe/php-enum/zipball/7159809e5cfa041dca28e61f7f7ae58063aae8ed", + "reference": "7159809e5cfa041dca28e61f7f7ae58063aae8ed", + "shasum": "" + }, + "require": { + "ext-reflection": "*", + "php": "^7.1 | ^8.0" + }, + "require-dev": { + "phpbench/phpbench": "^0.16.10 || ^1.0.4", + "phpstan/phpstan": "^1.3.1", + "phpunit/phpunit": "^7.5.20 | ^8.5.22 | ^9.5.11", + "vimeo/psalm": "^4.17.0 | ^5.26.1" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-3.x": "3.2-dev", + "dev-master": "4.7-dev" + } + }, + "autoload": { + "psr-4": { + "MabeEnum\\": "src/" + }, + "classmap": [ + "stubs/Stringable.php" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "BSD-3-Clause" + ], + "authors": [ + { + "name": "Marc Bennewitz", + "email": "dev@mabe.berlin", + "homepage": "https://mabe.berlin/", + "role": "Lead" + } + ], + "description": "Simple and fast implementation of enumerations with native PHP", + "homepage": "https://github.com/marc-mabe/php-enum", + "keywords": [ + "enum", + "enum-map", + "enum-set", + "enumeration", + "enumerator", + "enummap", + "enumset", + "map", + "set", + "type", + "type-hint", + "typehint" + ], + "support": { + "issues": "https://github.com/marc-mabe/php-enum/issues", + "source": "https://github.com/marc-mabe/php-enum/tree/v4.7.1" + }, + "time": "2024-11-28T04:54:44+00:00" + }, + { + "name": "masterminds/html5", + "version": "2.9.0", + "source": { + "type": "git", + "url": "https://github.com/Masterminds/html5-php.git", + "reference": "f5ac2c0b0a2eefca70b2ce32a5809992227e75a6" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/Masterminds/html5-php/zipball/f5ac2c0b0a2eefca70b2ce32a5809992227e75a6", + "reference": "f5ac2c0b0a2eefca70b2ce32a5809992227e75a6", + "shasum": "" + }, + "require": { + "ext-dom": "*", + "php": ">=5.3.0" + }, + "require-dev": { + "phpunit/phpunit": "^4.8.35 || ^5.7.21 || ^6 || ^7 || ^8 || ^9" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "2.7-dev" + } + }, + "autoload": { + "psr-4": { + "Masterminds\\": "src" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Matt Butcher", + "email": "technosophos@gmail.com" + }, + { + "name": "Matt Farina", + "email": "matt@mattfarina.com" + }, + { + "name": "Asmir Mustafic", + "email": "goetas@gmail.com" + } + ], + "description": "An HTML5 parser and serializer.", + "homepage": "http://masterminds.github.io/html5-php", + "keywords": [ + "HTML5", + "dom", + "html", + "parser", + "querypath", + "serializer", + "xml" + ], + "support": { + "issues": "https://github.com/Masterminds/html5-php/issues", + "source": "https://github.com/Masterminds/html5-php/tree/2.9.0" + }, + "time": "2024-03-31T07:05:07+00:00" + }, + { + "name": "mexitek/phpcolors", + "version": "v1.0.4", + "source": { + "type": "git", + "url": "https://github.com/mexitek/phpColors.git", + "reference": "4043974240ca7dc3c2bec3c158588148b605b206" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/mexitek/phpColors/zipball/4043974240ca7dc3c2bec3c158588148b605b206", + "reference": "4043974240ca7dc3c2bec3c158588148b605b206", + "shasum": "" + }, + "require": { + "php": "^7.2|^8.0" + }, + "require-dev": { + "nette/tester": "^2.3", + "squizlabs/php_codesniffer": "^3.5" + }, + "type": "library", + "autoload": { + "classmap": [ + "src" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Arlo Carreon", + "homepage": "http://arlocarreon.com", + "role": "creator" + } + ], + "description": "A series of methods that let you manipulate colors. Just incase you ever need different shades of one color on the fly.", + "homepage": "http://mexitek.github.com/phpColors/", + "keywords": [ + "color", + "css", + "design", + "frontend", + "ui" + ], + "support": { + "issues": "https://github.com/mexitek/phpColors/issues", + "source": "https://github.com/mexitek/phpColors" + }, + "time": "2021-11-26T13:19:08+00:00" + }, + { + "name": "microsoft/azure-storage-blob", + "version": "1.5.4", + "source": { + "type": "git", + "url": "https://github.com/Azure/azure-storage-blob-php.git", + "reference": "1023ce1dbf062351a32ca5ec72ad1fd4a504f1bf" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/Azure/azure-storage-blob-php/zipball/1023ce1dbf062351a32ca5ec72ad1fd4a504f1bf", + "reference": "1023ce1dbf062351a32ca5ec72ad1fd4a504f1bf", + "shasum": "" + }, + "require": { + "microsoft/azure-storage-common": "~1.5", + "php": ">=5.6.0" + }, + "type": "library", + "autoload": { + "psr-4": { + "MicrosoftAzure\\Storage\\Blob\\": "src/Blob" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Azure Storage PHP Client Library", + "email": "dmsh@microsoft.com" + } + ], + "description": "This project provides a set of PHP client libraries that make it easy to access Microsoft Azure Storage Blob APIs.", + "keywords": [ + "azure", + "blob", + "php", + "sdk", + "storage" + ], + "support": { + "issues": "https://github.com/Azure/azure-storage-blob-php/issues", + "source": "https://github.com/Azure/azure-storage-blob-php/tree/v1.5.4" + }, + "time": "2022-09-02T02:13:06+00:00" + }, + { + "name": "microsoft/azure-storage-common", + "version": "1.5.2", + "source": { + "type": "git", + "url": "https://github.com/Azure/azure-storage-common-php.git", + "reference": "8ca7b1bf4c9ca7c663e75a02a0035b05b37196a0" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/Azure/azure-storage-common-php/zipball/8ca7b1bf4c9ca7c663e75a02a0035b05b37196a0", + "reference": "8ca7b1bf4c9ca7c663e75a02a0035b05b37196a0", + "shasum": "" + }, + "require": { + "guzzlehttp/guzzle": "~6.0|^7.0", + "php": ">=5.6.0" + }, + "type": "library", + "autoload": { + "psr-4": { + "MicrosoftAzure\\Storage\\Common\\": "src/Common" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Azure Storage PHP Client Library", + "email": "dmsh@microsoft.com" + } + ], + "description": "This project provides a set of common code shared by Azure Storage Blob, Table, Queue and File PHP client libraries.", + "keywords": [ + "azure", + "common", + "php", + "sdk", + "storage" + ], + "support": { + "issues": "https://github.com/Azure/azure-storage-common-php/issues", + "source": "https://github.com/Azure/azure-storage-common-php/tree/v1.5.2" + }, + "time": "2021-10-09T03:03:47+00:00" + }, + { + "name": "mlocati/ip-lib", + "version": "1.20.0", + "source": { + "type": "git", + "url": "https://github.com/mlocati/ip-lib.git", + "reference": "fd45fc3bf08ed6c7e665e2e70562082ac954afd4" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/mlocati/ip-lib/zipball/fd45fc3bf08ed6c7e665e2e70562082ac954afd4", + "reference": "fd45fc3bf08ed6c7e665e2e70562082ac954afd4", + "shasum": "" + }, + "require": { + "php": ">=5.3.3" + }, + "require-dev": { + "ext-pdo_sqlite": "*", + "phpunit/phpunit": "^4.8 || ^5.7 || ^6.5 || ^7.5 || ^8.5 || ^9.5" + }, + "type": "library", + "autoload": { + "psr-4": { + "IPLib\\": "src/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Michele Locati", + "email": "mlocati@gmail.com", + "homepage": "https://github.com/mlocati", + "role": "Author" + } + ], + "description": "Handle IPv4, IPv6 addresses and ranges", + "homepage": "https://github.com/mlocati/ip-lib", + "keywords": [ + "IP", + "address", + "addresses", + "ipv4", + "ipv6", + "manage", + "managing", + "matching", + "network", + "networking", + "range", + "subnet" + ], + "support": { + "issues": "https://github.com/mlocati/ip-lib/issues", + "source": "https://github.com/mlocati/ip-lib/tree/1.20.0" + }, + "funding": [ + { + "url": "https://github.com/sponsors/mlocati", + "type": "github" + }, + { + "url": "https://paypal.me/mlocati", + "type": "other" + } + ], + "time": "2025-02-04T17:30:58+00:00" + }, + { + "name": "mtdowling/jmespath.php", + "version": "2.8.0", + "source": { + "type": "git", + "url": "https://github.com/jmespath/jmespath.php.git", + "reference": "a2a865e05d5f420b50cc2f85bb78d565db12a6bc" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/jmespath/jmespath.php/zipball/a2a865e05d5f420b50cc2f85bb78d565db12a6bc", + "reference": "a2a865e05d5f420b50cc2f85bb78d565db12a6bc", + "shasum": "" + }, + "require": { + "php": "^7.2.5 || ^8.0", + "symfony/polyfill-mbstring": "^1.17" + }, + "require-dev": { + "composer/xdebug-handler": "^3.0.3", + "phpunit/phpunit": "^8.5.33" + }, + "bin": [ + "bin/jp.php" + ], + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "2.8-dev" + } + }, + "autoload": { + "files": [ + "src/JmesPath.php" + ], + "psr-4": { + "JmesPath\\": "src/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Graham Campbell", + "email": "hello@gjcampbell.co.uk", + "homepage": "https://github.com/GrahamCampbell" + }, + { + "name": "Michael Dowling", + "email": "mtdowling@gmail.com", + "homepage": "https://github.com/mtdowling" + } + ], + "description": "Declaratively specify how to extract elements from a JSON document", + "keywords": [ + "json", + "jsonpath" + ], + "support": { + "issues": "https://github.com/jmespath/jmespath.php/issues", + "source": "https://github.com/jmespath/jmespath.php/tree/2.8.0" + }, + "time": "2024-09-04T18:46:31+00:00" + }, + { + "name": "f7cloud/lognormalizer", + "version": "v1.0.0", + "source": { + "type": "git", + "url": "https://github.com/nextcloud/lognormalizer.git", + "reference": "87445d69225c247aaff64643b1fc83c6d6df741f" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/nextcloud/lognormalizer/zipball/87445d69225c247aaff64643b1fc83c6d6df741f", + "reference": "87445d69225c247aaff64643b1fc83c6d6df741f", + "shasum": "" + }, + "require": { + "ext-json": "*", + "php": ">=7.2.0" + }, + "require-dev": { + "phpunit/phpunit": "8.*" + }, + "type": "library", + "autoload": { + "psr-4": { + "F7cloud\\LogNormalizer\\": "src/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "AGPL-3.0" + ], + "authors": [ + { + "name": "Christoph Wurst", + "email": "christoph@winzerhof-wurst.at" + }, + { + "name": "Olivier Paroz", + "email": "dev-lognormalizer@interfasys.ch", + "homepage": "http://www.interfasys.ch", + "role": "Developer" + }, + { + "name": "Jordi Boggiano", + "email": "j.boggiano@seld.be", + "role": "Developer" + } + ], + "description": "Parses variables and converts them to string so that they can be logged", + "homepage": "https://github.com/interfasys/lognormalizer", + "keywords": [ + "log", + "normalizer" + ], + "support": { + "issues": "https://github.com/nextcloud/lognormalizer/issues", + "source": "https://github.com/nextcloud/lognormalizer/tree/v1.0.0" + }, + "time": "2020-12-02T09:34:47+00:00" + }, + { + "name": "paragonie/constant_time_encoding", + "version": "v2.6.3", + "source": { + "type": "git", + "url": "https://github.com/paragonie/constant_time_encoding.git", + "reference": "58c3f47f650c94ec05a151692652a868995d2938" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/paragonie/constant_time_encoding/zipball/58c3f47f650c94ec05a151692652a868995d2938", + "reference": "58c3f47f650c94ec05a151692652a868995d2938", + "shasum": "" + }, + "require": { + "php": "^7|^8" + }, + "require-dev": { + "phpunit/phpunit": "^6|^7|^8|^9", + "vimeo/psalm": "^1|^2|^3|^4" + }, + "type": "library", + "autoload": { + "psr-4": { + "ParagonIE\\ConstantTime\\": "src/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Paragon Initiative Enterprises", + "email": "security@paragonie.com", + "homepage": "https://paragonie.com", + "role": "Maintainer" + }, + { + "name": "Steve 'Sc00bz' Thomas", + "email": "steve@tobtu.com", + "homepage": "https://www.tobtu.com", + "role": "Original Developer" + } + ], + "description": "Constant-time Implementations of RFC 4648 Encoding (Base-64, Base-32, Base-16)", + "keywords": [ + "base16", + "base32", + "base32_decode", + "base32_encode", + "base64", + "base64_decode", + "base64_encode", + "bin2hex", + "encoding", + "hex", + "hex2bin", + "rfc4648" + ], + "support": { + "email": "info@paragonie.com", + "issues": "https://github.com/paragonie/constant_time_encoding/issues", + "source": "https://github.com/paragonie/constant_time_encoding" + }, + "time": "2022-06-14T06:56:20+00:00" + }, + { + "name": "pear/archive_tar", + "version": "1.5.0", + "source": { + "type": "git", + "url": "https://github.com/pear/Archive_Tar.git", + "reference": "b439c859564f5cbb0f64ad6002d0afe84a889602" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/pear/Archive_Tar/zipball/b439c859564f5cbb0f64ad6002d0afe84a889602", + "reference": "b439c859564f5cbb0f64ad6002d0afe84a889602", + "shasum": "" + }, + "require": { + "pear/pear-core-minimal": "^1.10.0alpha2", + "php": ">=5.2.0" + }, + "require-dev": { + "phpunit/phpunit": "*" + }, + "suggest": { + "ext-bz2": "Bz2 compression support.", + "ext-xz": "Lzma2 compression support.", + "ext-zlib": "Gzip compression support." + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "1.4.x-dev" + } + }, + "autoload": { + "psr-0": { + "Archive_Tar": "" + } + }, + "notification-url": "https://packagist.org/downloads/", + "include-path": [ + "./" + ], + "license": [ + "BSD-2-Clause" + ], + "authors": [ + { + "name": "Vincent Blavet", + "email": "vincent@phpconcept.net" + }, + { + "name": "Greg Beaver", + "email": "greg@chiaraquartet.net" + }, + { + "name": "Michiel Rook", + "email": "mrook@php.net" + } + ], + "description": "Tar file management class with compression support (gzip, bzip2, lzma2)", + "homepage": "https://github.com/pear/Archive_Tar", + "keywords": [ + "archive", + "tar" + ], + "support": { + "issues": "http://pear.php.net/bugs/search.php?cmd=display&package_name[]=Archive_Tar", + "source": "https://github.com/pear/Archive_Tar" + }, + "time": "2024-03-16T16:21:40+00:00" + }, + { + "name": "pear/console_getopt", + "version": "v1.4.3", + "source": { + "type": "git", + "url": "https://github.com/pear/Console_Getopt.git", + "reference": "a41f8d3e668987609178c7c4a9fe48fecac53fa0" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/pear/Console_Getopt/zipball/a41f8d3e668987609178c7c4a9fe48fecac53fa0", + "reference": "a41f8d3e668987609178c7c4a9fe48fecac53fa0", + "shasum": "" + }, + "type": "library", + "autoload": { + "psr-0": { + "Console": "./" + } + }, + "notification-url": "https://packagist.org/downloads/", + "include-path": [ + "./" + ], + "license": [ + "BSD-2-Clause" + ], + "authors": [ + { + "name": "Andrei Zmievski", + "email": "andrei@php.net", + "role": "Lead" + }, + { + "name": "Stig Bakken", + "email": "stig@php.net", + "role": "Developer" + }, + { + "name": "Greg Beaver", + "email": "cellog@php.net", + "role": "Helper" + } + ], + "description": "More info available on: http://pear.php.net/package/Console_Getopt", + "support": { + "issues": "http://pear.php.net/bugs/search.php?cmd=display&package_name[]=Console_Getopt", + "source": "https://github.com/pear/Console_Getopt" + }, + "time": "2019-11-20T18:27:48+00:00" + }, + { + "name": "pear/pear-core-minimal", + "version": "v1.10.16", + "source": { + "type": "git", + "url": "https://github.com/pear/pear-core-minimal.git", + "reference": "c0f51b45f50683bf5bbf558036854ebc9b54d033" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/pear/pear-core-minimal/zipball/c0f51b45f50683bf5bbf558036854ebc9b54d033", + "reference": "c0f51b45f50683bf5bbf558036854ebc9b54d033", + "shasum": "" + }, + "require": { + "pear/console_getopt": "~1.4", + "pear/pear_exception": "~1.0", + "php": ">=5.4" + }, + "replace": { + "rsky/pear-core-min": "self.version" + }, + "type": "library", + "autoload": { + "classmap": [ + "src/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "include-path": [ + "src/" + ], + "license": [ + "BSD-3-Clause" + ], + "authors": [ + { + "name": "Christian Weiske", + "email": "cweiske@php.net", + "role": "Lead" + } + ], + "description": "Minimal set of PEAR core files to be used as composer dependency", + "support": { + "issues": "http://pear.php.net/bugs/search.php?cmd=display&package_name[]=PEAR", + "source": "https://github.com/pear/pear-core-minimal" + }, + "time": "2024-11-24T22:27:58+00:00" + }, + { + "name": "pear/pear_exception", + "version": "v1.0.2", + "source": { + "type": "git", + "url": "https://github.com/pear/PEAR_Exception.git", + "reference": "b14fbe2ddb0b9f94f5b24cf08783d599f776fff0" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/pear/PEAR_Exception/zipball/b14fbe2ddb0b9f94f5b24cf08783d599f776fff0", + "reference": "b14fbe2ddb0b9f94f5b24cf08783d599f776fff0", + "shasum": "" + }, + "require": { + "php": ">=5.2.0" + }, + "require-dev": { + "phpunit/phpunit": "<9" + }, + "type": "class", + "extra": { + "branch-alias": { + "dev-master": "1.0.x-dev" + } + }, + "autoload": { + "classmap": [ + "PEAR/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "include-path": [ + "." + ], + "license": [ + "BSD-2-Clause" + ], + "authors": [ + { + "name": "Helgi Thormar", + "email": "dufuz@php.net" + }, + { + "name": "Greg Beaver", + "email": "cellog@php.net" + } + ], + "description": "The PEAR Exception base class.", + "homepage": "https://github.com/pear/PEAR_Exception", + "keywords": [ + "exception" + ], + "support": { + "issues": "http://pear.php.net/bugs/search.php?cmd=display&package_name[]=PEAR_Exception", + "source": "https://github.com/pear/PEAR_Exception" + }, + "time": "2021-03-21T15:43:46+00:00" + }, + { + "name": "php-http/guzzle7-adapter", + "version": "1.1.0", + "source": { + "type": "git", + "url": "https://github.com/php-http/guzzle7-adapter.git", + "reference": "03a415fde709c2f25539790fecf4d9a31bc3d0eb" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/php-http/guzzle7-adapter/zipball/03a415fde709c2f25539790fecf4d9a31bc3d0eb", + "reference": "03a415fde709c2f25539790fecf4d9a31bc3d0eb", + "shasum": "" + }, + "require": { + "guzzlehttp/guzzle": "^7.0", + "php": "^7.3 | ^8.0", + "php-http/httplug": "^2.0", + "psr/http-client": "^1.0" + }, + "provide": { + "php-http/async-client-implementation": "1.0", + "php-http/client-implementation": "1.0", + "psr/http-client-implementation": "1.0" + }, + "require-dev": { + "php-http/client-integration-tests": "^3.0", + "php-http/message-factory": "^1.1", + "phpspec/prophecy-phpunit": "^2.0", + "phpunit/phpunit": "^8.0|^9.3" + }, + "type": "library", + "autoload": { + "psr-4": { + "Http\\Adapter\\Guzzle7\\": "src/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Tobias Nyholm", + "email": "tobias.nyholm@gmail.com" + } + ], + "description": "Guzzle 7 HTTP Adapter", + "homepage": "http://httplug.io", + "keywords": [ + "Guzzle", + "http" + ], + "support": { + "issues": "https://github.com/php-http/guzzle7-adapter/issues", + "source": "https://github.com/php-http/guzzle7-adapter/tree/1.1.0" + }, + "time": "2024-11-26T11:14:36+00:00" + }, + { + "name": "php-http/httplug", + "version": "2.4.1", + "source": { + "type": "git", + "url": "https://github.com/php-http/httplug.git", + "reference": "5cad731844891a4c282f3f3e1b582c46839d22f4" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/php-http/httplug/zipball/5cad731844891a4c282f3f3e1b582c46839d22f4", + "reference": "5cad731844891a4c282f3f3e1b582c46839d22f4", + "shasum": "" + }, + "require": { + "php": "^7.1 || ^8.0", + "php-http/promise": "^1.1", + "psr/http-client": "^1.0", + "psr/http-message": "^1.0 || ^2.0" + }, + "require-dev": { + "friends-of-phpspec/phpspec-code-coverage": "^4.1 || ^5.0 || ^6.0", + "phpspec/phpspec": "^5.1 || ^6.0 || ^7.0" + }, + "type": "library", + "autoload": { + "psr-4": { + "Http\\Client\\": "src/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Eric GELOEN", + "email": "geloen.eric@gmail.com" + }, + { + "name": "Márk Sági-Kazár", + "email": "mark.sagikazar@gmail.com", + "homepage": "https://sagikazarmark.hu" + } + ], + "description": "HTTPlug, the HTTP client abstraction for PHP", + "homepage": "http://httplug.io", + "keywords": [ + "client", + "http" + ], + "support": { + "issues": "https://github.com/php-http/httplug/issues", + "source": "https://github.com/php-http/httplug/tree/2.4.1" + }, + "time": "2024-09-23T11:39:58+00:00" + }, + { + "name": "php-http/promise", + "version": "1.3.1", + "source": { + "type": "git", + "url": "https://github.com/php-http/promise.git", + "reference": "fc85b1fba37c169a69a07ef0d5a8075770cc1f83" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/php-http/promise/zipball/fc85b1fba37c169a69a07ef0d5a8075770cc1f83", + "reference": "fc85b1fba37c169a69a07ef0d5a8075770cc1f83", + "shasum": "" + }, + "require": { + "php": "^7.1 || ^8.0" + }, + "require-dev": { + "friends-of-phpspec/phpspec-code-coverage": "^4.3.2 || ^6.3", + "phpspec/phpspec": "^5.1.2 || ^6.2 || ^7.4" + }, + "type": "library", + "autoload": { + "psr-4": { + "Http\\Promise\\": "src/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Joel Wurtz", + "email": "joel.wurtz@gmail.com" + }, + { + "name": "Márk Sági-Kazár", + "email": "mark.sagikazar@gmail.com" + } + ], + "description": "Promise used for asynchronous HTTP requests", + "homepage": "http://httplug.io", + "keywords": [ + "promise" + ], + "support": { + "issues": "https://github.com/php-http/promise/issues", + "source": "https://github.com/php-http/promise/tree/1.3.1" + }, + "time": "2024-03-15T13:55:21+00:00" + }, + { + "name": "php-opencloud/openstack", + "version": "v3.14.0", + "source": { + "type": "git", + "url": "https://github.com/php-opencloud/openstack.git", + "reference": "b92ea5581ca91779b88f08b1b44e6ca880b34fc3" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/php-opencloud/openstack/zipball/b92ea5581ca91779b88f08b1b44e6ca880b34fc3", + "reference": "b92ea5581ca91779b88f08b1b44e6ca880b34fc3", + "shasum": "" + }, + "require": { + "guzzlehttp/guzzle": "^7.0", + "guzzlehttp/psr7": ">=1.7", + "guzzlehttp/uri-template": "^0.2 || ^1.0", + "justinrainbow/json-schema": "^5.2 || ^6.0", + "php": "^7.2.5 || ^8.0" + }, + "require-dev": { + "ergebnis/composer-normalize": "^2.0", + "ext-json": "*", + "friendsofphp/php-cs-fixer": "^3", + "php-coveralls/php-coveralls": "^2.0", + "php-parallel-lint/php-parallel-lint": "^1.2", + "phpspec/prophecy": "^1.17", + "phpunit/phpunit": ">=8.5.23 <9.0", + "psr/log": "^1.0" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "3.0-dev" + } + }, + "autoload": { + "psr-4": { + "OpenStack\\": "src/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "Apache-2.0" + ], + "authors": [ + { + "name": "Jamie Hannaford", + "email": "jamie.hannaford@rackspace.com", + "homepage": "https://github.com/jamiehannaford" + }, + { + "name": "Ha Phan", + "email": "thanhha.work@gmail.com", + "homepage": "https://github.com/haphan" + }, + { + "name": "Konstantin Babushkin", + "email": "koka@idwrx.com", + "homepage": "https://github.com/k0ka" + } + ], + "description": "PHP SDK for OpenStack APIs. Supports BlockStorage, Compute, Identity, Images, Networking and Metric Gnocchi", + "homepage": "https://github.com/php-opencloud/openstack", + "keywords": [ + "Openstack", + "api", + "php", + "sdk" + ], + "support": { + "issues": "https://github.com/php-opencloud/openstack/issues", + "source": "https://github.com/php-opencloud/openstack/tree/v3.14.0" + }, + "time": "2025-05-30T09:26:42+00:00" + }, + { + "name": "phpseclib/phpseclib", + "version": "2.0.47", + "source": { + "type": "git", + "url": "https://github.com/phpseclib/phpseclib.git", + "reference": "b7d7d90ee7df7f33a664b4aea32d50a305d35adb" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/phpseclib/phpseclib/zipball/b7d7d90ee7df7f33a664b4aea32d50a305d35adb", + "reference": "b7d7d90ee7df7f33a664b4aea32d50a305d35adb", + "shasum": "" + }, + "require": { + "php": ">=5.3.3" + }, + "require-dev": { + "phing/phing": "~2.7", + "phpunit/phpunit": "^4.8.35|^5.7|^6.0|^9.4", + "squizlabs/php_codesniffer": "~2.0" + }, + "suggest": { + "ext-gmp": "Install the GMP (GNU Multiple Precision) extension in order to speed up arbitrary precision integer arithmetic operations.", + "ext-libsodium": "SSH2/SFTP can make use of some algorithms provided by the libsodium-php extension.", + "ext-mcrypt": "Install the Mcrypt extension in order to speed up a few other cryptographic operations.", + "ext-openssl": "Install the OpenSSL extension in order to speed up a wide variety of cryptographic operations.", + "ext-xml": "Install the XML extension to load XML formatted public keys." + }, + "type": "library", + "autoload": { + "files": [ + "phpseclib/bootstrap.php" + ], + "psr-4": { + "phpseclib\\": "phpseclib/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Jim Wigginton", + "email": "terrafrost@php.net", + "role": "Lead Developer" + }, + { + "name": "Patrick Monnerat", + "email": "pm@datasphere.ch", + "role": "Developer" + }, + { + "name": "Andreas Fischer", + "email": "bantu@phpbb.com", + "role": "Developer" + }, + { + "name": "Hans-Jürgen Petrich", + "email": "petrich@tronic-media.com", + "role": "Developer" + }, + { + "name": "Graham Campbell", + "email": "graham@alt-three.com", + "role": "Developer" + } + ], + "description": "PHP Secure Communications Library - Pure-PHP implementations of RSA, AES, SSH2, SFTP, X.509 etc.", + "homepage": "http://phpseclib.sourceforge.net", + "keywords": [ + "BigInteger", + "aes", + "asn.1", + "asn1", + "blowfish", + "crypto", + "cryptography", + "encryption", + "rsa", + "security", + "sftp", + "signature", + "signing", + "ssh", + "twofish", + "x.509", + "x509" + ], + "support": { + "issues": "https://github.com/phpseclib/phpseclib/issues", + "source": "https://github.com/phpseclib/phpseclib/tree/2.0.47" + }, + "funding": [ + { + "url": "https://github.com/terrafrost", + "type": "github" + }, + { + "url": "https://www.patreon.com/phpseclib", + "type": "patreon" + }, + { + "url": "https://tidelift.com/funding/github/packagist/phpseclib/phpseclib", + "type": "tidelift" + } + ], + "time": "2024-02-26T04:55:38+00:00" + }, + { + "name": "pimple/pimple", + "version": "v3.5.0", + "source": { + "type": "git", + "url": "https://github.com/silexphp/Pimple.git", + "reference": "a94b3a4db7fb774b3d78dad2315ddc07629e1bed" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/silexphp/Pimple/zipball/a94b3a4db7fb774b3d78dad2315ddc07629e1bed", + "reference": "a94b3a4db7fb774b3d78dad2315ddc07629e1bed", + "shasum": "" + }, + "require": { + "php": ">=7.2.5", + "psr/container": "^1.1 || ^2.0" + }, + "require-dev": { + "symfony/phpunit-bridge": "^5.4@dev" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "3.4.x-dev" + } + }, + "autoload": { + "psr-0": { + "Pimple": "src/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Fabien Potencier", + "email": "fabien@symfony.com" + } + ], + "description": "Pimple, a simple Dependency Injection Container", + "homepage": "https://pimple.symfony.com", + "keywords": [ + "container", + "dependency injection" + ], + "support": { + "source": "https://github.com/silexphp/Pimple/tree/v3.5.0" + }, + "time": "2021-10-28T11:13:42+00:00" + }, + { + "name": "psr/cache", + "version": "3.0.0", + "source": { + "type": "git", + "url": "https://github.com/php-fig/cache.git", + "reference": "aa5030cfa5405eccfdcb1083ce040c2cb8d253bf" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/php-fig/cache/zipball/aa5030cfa5405eccfdcb1083ce040c2cb8d253bf", + "reference": "aa5030cfa5405eccfdcb1083ce040c2cb8d253bf", + "shasum": "" + }, + "require": { + "php": ">=8.0.0" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "1.0.x-dev" + } + }, + "autoload": { + "psr-4": { + "Psr\\Cache\\": "src/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "PHP-FIG", + "homepage": "https://www.php-fig.org/" + } + ], + "description": "Common interface for caching libraries", + "keywords": [ + "cache", + "psr", + "psr-6" + ], + "support": { + "source": "https://github.com/php-fig/cache/tree/3.0.0" + }, + "time": "2021-02-03T23:26:27+00:00" + }, + { + "name": "psr/clock", + "version": "1.0.0", + "source": { + "type": "git", + "url": "https://github.com/php-fig/clock.git", + "reference": "e41a24703d4560fd0acb709162f73b8adfc3aa0d" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/php-fig/clock/zipball/e41a24703d4560fd0acb709162f73b8adfc3aa0d", + "reference": "e41a24703d4560fd0acb709162f73b8adfc3aa0d", + "shasum": "" + }, + "require": { + "php": "^7.0 || ^8.0" + }, + "type": "library", + "autoload": { + "psr-4": { + "Psr\\Clock\\": "src/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "PHP-FIG", + "homepage": "https://www.php-fig.org/" + } + ], + "description": "Common interface for reading the clock.", + "homepage": "https://github.com/php-fig/clock", + "keywords": [ + "clock", + "now", + "psr", + "psr-20", + "time" + ], + "support": { + "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" + }, + { + "name": "psr/container", + "version": "2.0.2", + "source": { + "type": "git", + "url": "https://github.com/php-fig/container.git", + "reference": "c71ecc56dfe541dbd90c5360474fbc405f8d5963" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/php-fig/container/zipball/c71ecc56dfe541dbd90c5360474fbc405f8d5963", + "reference": "c71ecc56dfe541dbd90c5360474fbc405f8d5963", + "shasum": "" + }, + "require": { + "php": ">=7.4.0" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "2.0.x-dev" + } + }, + "autoload": { + "psr-4": { + "Psr\\Container\\": "src/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "PHP-FIG", + "homepage": "https://www.php-fig.org/" + } + ], + "description": "Common Container Interface (PHP FIG PSR-11)", + "homepage": "https://github.com/php-fig/container", + "keywords": [ + "PSR-11", + "container", + "container-interface", + "container-interop", + "psr" + ], + "support": { + "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" + }, + { + "name": "psr/event-dispatcher", + "version": "1.0.0", + "source": { + "type": "git", + "url": "https://github.com/php-fig/event-dispatcher.git", + "reference": "dbefd12671e8a14ec7f180cab83036ed26714bb0" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/php-fig/event-dispatcher/zipball/dbefd12671e8a14ec7f180cab83036ed26714bb0", + "reference": "dbefd12671e8a14ec7f180cab83036ed26714bb0", + "shasum": "" + }, + "require": { + "php": ">=7.2.0" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "1.0.x-dev" + } + }, + "autoload": { + "psr-4": { + "Psr\\EventDispatcher\\": "src/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "PHP-FIG", + "homepage": "http://www.php-fig.org/" + } + ], + "description": "Standard interfaces for event handling.", + "keywords": [ + "events", + "psr", + "psr-14" + ], + "support": { + "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" + }, + { + "name": "psr/http-client", + "version": "1.0.3", + "source": { + "type": "git", + "url": "https://github.com/php-fig/http-client.git", + "reference": "bb5906edc1c324c9a05aa0873d40117941e5fa90" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/php-fig/http-client/zipball/bb5906edc1c324c9a05aa0873d40117941e5fa90", + "reference": "bb5906edc1c324c9a05aa0873d40117941e5fa90", + "shasum": "" + }, + "require": { + "php": "^7.0 || ^8.0", + "psr/http-message": "^1.0 || ^2.0" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "1.0.x-dev" + } + }, + "autoload": { + "psr-4": { + "Psr\\Http\\Client\\": "src/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "PHP-FIG", + "homepage": "https://www.php-fig.org/" + } + ], + "description": "Common interface for HTTP clients", + "homepage": "https://github.com/php-fig/http-client", + "keywords": [ + "http", + "http-client", + "psr", + "psr-18" + ], + "support": { + "source": "https://github.com/php-fig/http-client" + }, + "time": "2023-09-23T14:17:50+00:00" + }, + { + "name": "psr/http-factory", + "version": "1.1.0", + "source": { + "type": "git", + "url": "https://github.com/php-fig/http-factory.git", + "reference": "2b4765fddfe3b508ac62f829e852b1501d3f6e8a" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/php-fig/http-factory/zipball/2b4765fddfe3b508ac62f829e852b1501d3f6e8a", + "reference": "2b4765fddfe3b508ac62f829e852b1501d3f6e8a", + "shasum": "" + }, + "require": { + "php": ">=7.1", + "psr/http-message": "^1.0 || ^2.0" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "1.0.x-dev" + } + }, + "autoload": { + "psr-4": { + "Psr\\Http\\Message\\": "src/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "PHP-FIG", + "homepage": "https://www.php-fig.org/" + } + ], + "description": "PSR-17: Common interfaces for PSR-7 HTTP message factories", + "keywords": [ + "factory", + "http", + "message", + "psr", + "psr-17", + "psr-7", + "request", + "response" + ], + "support": { + "source": "https://github.com/php-fig/http-factory" + }, + "time": "2024-04-15T12:06:14+00:00" + }, + { + "name": "psr/http-message", + "version": "2.0", + "source": { + "type": "git", + "url": "https://github.com/php-fig/http-message.git", + "reference": "402d35bcb92c70c026d1a6a9883f06b2ead23d71" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/php-fig/http-message/zipball/402d35bcb92c70c026d1a6a9883f06b2ead23d71", + "reference": "402d35bcb92c70c026d1a6a9883f06b2ead23d71", + "shasum": "" + }, + "require": { + "php": "^7.2 || ^8.0" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "2.0.x-dev" + } + }, + "autoload": { + "psr-4": { + "Psr\\Http\\Message\\": "src/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "PHP-FIG", + "homepage": "https://www.php-fig.org/" + } + ], + "description": "Common interface for HTTP messages", + "homepage": "https://github.com/php-fig/http-message", + "keywords": [ + "http", + "http-message", + "psr", + "psr-7", + "request", + "response" + ], + "support": { + "source": "https://github.com/php-fig/http-message/tree/2.0" + }, + "time": "2023-04-04T09:54:51+00:00" + }, + { + "name": "psr/log", + "version": "3.0.2", + "source": { + "type": "git", + "url": "https://github.com/php-fig/log.git", + "reference": "f16e1d5863e37f8d8c2a01719f5b34baa2b714d3" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/php-fig/log/zipball/f16e1d5863e37f8d8c2a01719f5b34baa2b714d3", + "reference": "f16e1d5863e37f8d8c2a01719f5b34baa2b714d3", + "shasum": "" + }, + "require": { + "php": ">=8.0.0" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "3.x-dev" + } + }, + "autoload": { + "psr-4": { + "Psr\\Log\\": "src" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "PHP-FIG", + "homepage": "https://www.php-fig.org/" + } + ], + "description": "Common interface for logging libraries", + "homepage": "https://github.com/php-fig/log", + "keywords": [ + "log", + "psr", + "psr-3" + ], + "support": { + "source": "https://github.com/php-fig/log/tree/3.0.2" + }, + "time": "2024-09-11T13:17:53+00:00" + }, + { + "name": "punic/punic", + "version": "3.8.1", + "source": { + "type": "git", + "url": "https://github.com/punic/punic.git", + "reference": "142707201a246a9c2ea909605cd56177af87f961" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/punic/punic/zipball/142707201a246a9c2ea909605cd56177af87f961", + "reference": "142707201a246a9c2ea909605cd56177af87f961", + "shasum": "" + }, + "require": { + "php": ">=5.3" + }, + "replace": { + "punic/calendar": "*", + "punic/common": "*" + }, + "require-dev": { + "phpunit/phpunit": "^4.8 || ^5.7 || ^6.5 || ^7.4 || ^8.5 || ^9.5" + }, + "bin": [ + "bin/punic-data" + ], + "type": "library", + "autoload": { + "psr-4": { + "Punic\\": "src/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Michele Locati", + "email": "mlocati@gmail.com", + "role": "Developer" + }, + { + "name": "Remo Laubacher", + "email": "remo.laubacher@gmail.com", + "role": "Collaborator, motivator and perfectionist supporter" + }, + { + "name": "Christian Schmidt", + "email": "github@chsc.dk", + "role": "Developer" + } + ], + "description": "PHP-Unicode CLDR", + "homepage": "https://github.com/punic/punic", + "keywords": [ + "calendar", + "cldr", + "date", + "date-time", + "i18n", + "internationalization", + "l10n", + "localization", + "php", + "time", + "translate", + "translations", + "unicode" + ], + "support": { + "issues": "https://github.com/punic/punic/issues", + "source": "https://github.com/punic/punic/tree/3.8.1" + }, + "funding": [ + { + "url": "https://paypal.me/mlocati", + "type": "custom" + }, + { + "url": "https://github.com/mlocati", + "type": "github" + } + ], + "time": "2023-03-29T06:56:57+00:00" + }, + { + "name": "ralouphie/getallheaders", + "version": "3.0.3", + "source": { + "type": "git", + "url": "https://github.com/ralouphie/getallheaders.git", + "reference": "120b605dfeb996808c31b6477290a714d356e822" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/ralouphie/getallheaders/zipball/120b605dfeb996808c31b6477290a714d356e822", + "reference": "120b605dfeb996808c31b6477290a714d356e822", + "shasum": "" + }, + "require": { + "php": ">=5.6" + }, + "require-dev": { + "php-coveralls/php-coveralls": "^2.1", + "phpunit/phpunit": "^5 || ^6.5" + }, + "type": "library", + "autoload": { + "files": [ + "src/getallheaders.php" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Ralph Khattar", + "email": "ralph.khattar@gmail.com" + } + ], + "description": "A polyfill for getallheaders.", + "support": { + "issues": "https://github.com/ralouphie/getallheaders/issues", + "source": "https://github.com/ralouphie/getallheaders/tree/develop" + }, + "time": "2019-03-08T08:55:37+00:00" + }, + { + "name": "sabre/dav", + "version": "4.7.0", + "source": { + "type": "git", + "url": "https://github.com/sabre-io/dav.git", + "reference": "074373bcd689a30bcf5aaa6bbb20a3395964ce7a" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/sabre-io/dav/zipball/074373bcd689a30bcf5aaa6bbb20a3395964ce7a", + "reference": "074373bcd689a30bcf5aaa6bbb20a3395964ce7a", + "shasum": "" + }, + "require": { + "ext-ctype": "*", + "ext-date": "*", + "ext-dom": "*", + "ext-iconv": "*", + "ext-json": "*", + "ext-mbstring": "*", + "ext-pcre": "*", + "ext-simplexml": "*", + "ext-spl": "*", + "lib-libxml": ">=2.7.0", + "php": "^7.1.0 || ^8.0", + "psr/log": "^1.0 || ^2.0 || ^3.0", + "sabre/event": "^5.0", + "sabre/http": "^5.0.5", + "sabre/uri": "^2.0", + "sabre/vobject": "^4.2.1", + "sabre/xml": "^2.0.1" + }, + "require-dev": { + "friendsofphp/php-cs-fixer": "^2.19", + "monolog/monolog": "^1.27 || ^2.0", + "phpstan/phpstan": "^0.12 || ^1.0", + "phpstan/phpstan-phpunit": "^1.0", + "phpunit/phpunit": "^7.5 || ^8.5 || ^9.6" + }, + "suggest": { + "ext-curl": "*", + "ext-imap": "*", + "ext-pdo": "*" + }, + "bin": [ + "bin/sabredav", + "bin/naturalselection" + ], + "type": "library", + "autoload": { + "psr-4": { + "Sabre\\": "lib/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "BSD-3-Clause" + ], + "authors": [ + { + "name": "Evert Pot", + "email": "me@evertpot.com", + "homepage": "http://evertpot.com/", + "role": "Developer" + } + ], + "description": "WebDAV Framework for PHP", + "homepage": "http://sabre.io/", + "keywords": [ + "CalDAV", + "CardDAV", + "WebDAV", + "framework", + "iCalendar" + ], + "support": { + "forum": "https://groups.google.com/group/sabredav-discuss", + "issues": "https://github.com/sabre-io/dav/issues", + "source": "https://github.com/fruux/sabre-dav" + }, + "time": "2024-10-29T11:46:02+00:00" + }, + { + "name": "sabre/event", + "version": "5.1.7", + "source": { + "type": "git", + "url": "https://github.com/sabre-io/event.git", + "reference": "86d57e305c272898ba3c28e9bd3d65d5464587c2" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/sabre-io/event/zipball/86d57e305c272898ba3c28e9bd3d65d5464587c2", + "reference": "86d57e305c272898ba3c28e9bd3d65d5464587c2", + "shasum": "" + }, + "require": { + "php": "^7.1 || ^8.0" + }, + "require-dev": { + "friendsofphp/php-cs-fixer": "~2.17.1||^3.63", + "phpstan/phpstan": "^0.12", + "phpunit/phpunit": "^7.5 || ^8.5 || ^9.6" + }, + "type": "library", + "autoload": { + "files": [ + "lib/coroutine.php", + "lib/Loop/functions.php", + "lib/Promise/functions.php" + ], + "psr-4": { + "Sabre\\Event\\": "lib/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "BSD-3-Clause" + ], + "authors": [ + { + "name": "Evert Pot", + "email": "me@evertpot.com", + "homepage": "http://evertpot.com/", + "role": "Developer" + } + ], + "description": "sabre/event is a library for lightweight event-based programming", + "homepage": "http://sabre.io/event/", + "keywords": [ + "EventEmitter", + "async", + "coroutine", + "eventloop", + "events", + "hooks", + "plugin", + "promise", + "reactor", + "signal" + ], + "support": { + "forum": "https://groups.google.com/group/sabredav-discuss", + "issues": "https://github.com/sabre-io/event/issues", + "source": "https://github.com/fruux/sabre-event" + }, + "time": "2024-08-27T11:23:05+00:00" + }, + { + "name": "sabre/http", + "version": "5.1.12", + "source": { + "type": "git", + "url": "https://github.com/sabre-io/http.git", + "reference": "dedff73f3995578bc942fa4c8484190cac14f139" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/sabre-io/http/zipball/dedff73f3995578bc942fa4c8484190cac14f139", + "reference": "dedff73f3995578bc942fa4c8484190cac14f139", + "shasum": "" + }, + "require": { + "ext-ctype": "*", + "ext-curl": "*", + "ext-mbstring": "*", + "php": "^7.1 || ^8.0", + "sabre/event": ">=4.0 <6.0", + "sabre/uri": "^2.0" + }, + "require-dev": { + "friendsofphp/php-cs-fixer": "~2.17.1||^3.63", + "phpstan/phpstan": "^0.12", + "phpunit/phpunit": "^7.5 || ^8.5 || ^9.6" + }, + "suggest": { + "ext-curl": " to make http requests with the Client class" + }, + "type": "library", + "autoload": { + "files": [ + "lib/functions.php" + ], + "psr-4": { + "Sabre\\HTTP\\": "lib/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "BSD-3-Clause" + ], + "authors": [ + { + "name": "Evert Pot", + "email": "me@evertpot.com", + "homepage": "http://evertpot.com/", + "role": "Developer" + } + ], + "description": "The sabre/http library provides utilities for dealing with http requests and responses. ", + "homepage": "https://github.com/fruux/sabre-http", + "keywords": [ + "http" + ], + "support": { + "forum": "https://groups.google.com/group/sabredav-discuss", + "issues": "https://github.com/sabre-io/http/issues", + "source": "https://github.com/fruux/sabre-http" + }, + "time": "2024-08-27T16:07:41+00:00" + }, + { + "name": "sabre/uri", + "version": "2.3.4", + "source": { + "type": "git", + "url": "https://github.com/sabre-io/uri.git", + "reference": "b76524c22de90d80ca73143680a8e77b1266c291" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/sabre-io/uri/zipball/b76524c22de90d80ca73143680a8e77b1266c291", + "reference": "b76524c22de90d80ca73143680a8e77b1266c291", + "shasum": "" + }, + "require": { + "php": "^7.4 || ^8.0" + }, + "require-dev": { + "friendsofphp/php-cs-fixer": "^3.63", + "phpstan/extension-installer": "^1.4", + "phpstan/phpstan": "^1.12", + "phpstan/phpstan-phpunit": "^1.4", + "phpstan/phpstan-strict-rules": "^1.6", + "phpunit/phpunit": "^9.6" + }, + "type": "library", + "autoload": { + "files": [ + "lib/functions.php" + ], + "psr-4": { + "Sabre\\Uri\\": "lib/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "BSD-3-Clause" + ], + "authors": [ + { + "name": "Evert Pot", + "email": "me@evertpot.com", + "homepage": "http://evertpot.com/", + "role": "Developer" + } + ], + "description": "Functions for making sense out of URIs.", + "homepage": "http://sabre.io/uri/", + "keywords": [ + "rfc3986", + "uri", + "url" + ], + "support": { + "forum": "https://groups.google.com/group/sabredav-discuss", + "issues": "https://github.com/sabre-io/uri/issues", + "source": "https://github.com/fruux/sabre-uri" + }, + "time": "2024-08-27T12:18:16+00:00" + }, + { + "name": "sabre/vobject", + "version": "4.5.6", + "source": { + "type": "git", + "url": "https://github.com/sabre-io/vobject.git", + "reference": "900266bb3bd448a9f7f41f82344ad0aba237cb27" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/sabre-io/vobject/zipball/900266bb3bd448a9f7f41f82344ad0aba237cb27", + "reference": "900266bb3bd448a9f7f41f82344ad0aba237cb27", + "shasum": "" + }, + "require": { + "ext-mbstring": "*", + "php": "^7.1 || ^8.0", + "sabre/xml": "^2.1 || ^3.0 || ^4.0" + }, + "require-dev": { + "friendsofphp/php-cs-fixer": "~2.17.1", + "phpstan/phpstan": "^0.12 || ^1.11", + "phpunit/php-invoker": "^2.0 || ^3.1", + "phpunit/phpunit": "^7.5 || ^8.5 || ^9.6" + }, + "suggest": { + "hoa/bench": "If you would like to run the benchmark scripts" + }, + "bin": [ + "bin/vobject", + "bin/generate_vcards" + ], + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "4.0.x-dev" + } + }, + "autoload": { + "psr-4": { + "Sabre\\VObject\\": "lib/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "BSD-3-Clause" + ], + "authors": [ + { + "name": "Evert Pot", + "email": "me@evertpot.com", + "homepage": "http://evertpot.com/", + "role": "Developer" + }, + { + "name": "Dominik Tobschall", + "email": "dominik@fruux.com", + "homepage": "http://tobschall.de/", + "role": "Developer" + }, + { + "name": "Ivan Enderlin", + "email": "ivan.enderlin@hoa-project.net", + "homepage": "http://mnt.io/", + "role": "Developer" + } + ], + "description": "The VObject library for PHP allows you to easily parse and manipulate iCalendar and vCard objects", + "homepage": "http://sabre.io/vobject/", + "keywords": [ + "availability", + "freebusy", + "iCalendar", + "ical", + "ics", + "jCal", + "jCard", + "recurrence", + "rfc2425", + "rfc2426", + "rfc2739", + "rfc4770", + "rfc5545", + "rfc5546", + "rfc6321", + "rfc6350", + "rfc6351", + "rfc6474", + "rfc6638", + "rfc6715", + "rfc6868", + "vCalendar", + "vCard", + "vcf", + "xCal", + "xCard" + ], + "support": { + "forum": "https://groups.google.com/group/sabredav-discuss", + "issues": "https://github.com/sabre-io/vobject/issues", + "source": "https://github.com/fruux/sabre-vobject" + }, + "time": "2024-10-14T11:53:54+00:00" + }, + { + "name": "sabre/xml", + "version": "2.2.11", + "source": { + "type": "git", + "url": "https://github.com/sabre-io/xml.git", + "reference": "01a7927842abf3e10df3d9c2d9b0cc9d813a3fcc" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/sabre-io/xml/zipball/01a7927842abf3e10df3d9c2d9b0cc9d813a3fcc", + "reference": "01a7927842abf3e10df3d9c2d9b0cc9d813a3fcc", + "shasum": "" + }, + "require": { + "ext-dom": "*", + "ext-xmlreader": "*", + "ext-xmlwriter": "*", + "lib-libxml": ">=2.6.20", + "php": "^7.1 || ^8.0", + "sabre/uri": ">=1.0,<3.0.0" + }, + "require-dev": { + "friendsofphp/php-cs-fixer": "~2.17.1||3.63.2", + "phpstan/phpstan": "^0.12", + "phpunit/phpunit": "^7.5 || ^8.5 || ^9.6" + }, + "type": "library", + "autoload": { + "files": [ + "lib/Deserializer/functions.php", + "lib/Serializer/functions.php" + ], + "psr-4": { + "Sabre\\Xml\\": "lib/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "BSD-3-Clause" + ], + "authors": [ + { + "name": "Evert Pot", + "email": "me@evertpot.com", + "homepage": "http://evertpot.com/", + "role": "Developer" + }, + { + "name": "Markus Staab", + "email": "markus.staab@redaxo.de", + "role": "Developer" + } + ], + "description": "sabre/xml is an XML library that you may not hate.", + "homepage": "https://sabre.io/xml/", + "keywords": [ + "XMLReader", + "XMLWriter", + "dom", + "xml" + ], + "support": { + "forum": "https://groups.google.com/group/sabredav-discuss", + "issues": "https://github.com/sabre-io/xml/issues", + "source": "https://github.com/fruux/sabre-xml" + }, + "time": "2024-09-06T07:37:46+00:00" + }, + { + "name": "spomky-labs/cbor-php", + "version": "3.0.4", + "source": { + "type": "git", + "url": "https://github.com/Spomky-Labs/cbor-php.git", + "reference": "658ed12a85a6b31fa312b89cd92f3a4ce6df4c6b" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/Spomky-Labs/cbor-php/zipball/658ed12a85a6b31fa312b89cd92f3a4ce6df4c6b", + "reference": "658ed12a85a6b31fa312b89cd92f3a4ce6df4c6b", + "shasum": "" + }, + "require": { + "brick/math": "^0.9|^0.10|^0.11|^0.12", + "ext-mbstring": "*", + "php": ">=8.0" + }, + "require-dev": { + "ekino/phpstan-banned-code": "^1.0", + "ext-json": "*", + "infection/infection": "^0.27", + "php-parallel-lint/php-parallel-lint": "^1.3", + "phpstan/extension-installer": "^1.1", + "phpstan/phpstan": "^1.0", + "phpstan/phpstan-beberlei-assert": "^1.0", + "phpstan/phpstan-deprecation-rules": "^1.0", + "phpstan/phpstan-phpunit": "^1.0", + "phpstan/phpstan-strict-rules": "^1.0", + "phpunit/phpunit": "^10.1", + "qossmic/deptrac-shim": "^1.0", + "rector/rector": "^0.19", + "roave/security-advisories": "dev-latest", + "symfony/var-dumper": "^6.0|^7.0", + "symplify/easy-coding-standard": "^12.0" + }, + "suggest": { + "ext-bcmath": "GMP or BCMath extensions will drastically improve the library performance. BCMath extension needed to handle the Big Float and Decimal Fraction Tags", + "ext-gmp": "GMP or BCMath extensions will drastically improve the library performance" + }, + "type": "library", + "autoload": { + "psr-4": { + "CBOR\\": "src/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Florent Morselli", + "homepage": "https://github.com/Spomky" + }, + { + "name": "All contributors", + "homepage": "https://github.com/Spomky-Labs/cbor-php/contributors" + } + ], + "description": "CBOR Encoder/Decoder for PHP", + "keywords": [ + "Concise Binary Object Representation", + "RFC7049", + "cbor" + ], + "support": { + "issues": "https://github.com/Spomky-Labs/cbor-php/issues", + "source": "https://github.com/Spomky-Labs/cbor-php/tree/3.0.4" + }, + "funding": [ + { + "url": "https://github.com/Spomky", + "type": "github" + }, + { + "url": "https://www.patreon.com/FlorentMorselli", + "type": "patreon" + } + ], + "time": "2024-01-29T20:33:48+00:00" + }, + { + "name": "spomky-labs/pki-framework", + "version": "1.2.1", + "source": { + "type": "git", + "url": "https://github.com/Spomky-Labs/pki-framework.git", + "reference": "0b10c8b53366729417d6226ae89a665f9e2d61b6" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/Spomky-Labs/pki-framework/zipball/0b10c8b53366729417d6226ae89a665f9e2d61b6", + "reference": "0b10c8b53366729417d6226ae89a665f9e2d61b6", + "shasum": "" + }, + "require": { + "brick/math": "^0.10|^0.11|^0.12", + "ext-mbstring": "*", + "php": ">=8.1" + }, + "require-dev": { + "ekino/phpstan-banned-code": "^1.0", + "ext-gmp": "*", + "ext-openssl": "*", + "infection/infection": "^0.28", + "php-parallel-lint/php-parallel-lint": "^1.3", + "phpstan/extension-installer": "^1.3", + "phpstan/phpstan": "^1.8", + "phpstan/phpstan-beberlei-assert": "^1.0", + "phpstan/phpstan-deprecation-rules": "^1.0", + "phpstan/phpstan-phpunit": "^1.1", + "phpstan/phpstan-strict-rules": "^1.3", + "phpunit/phpunit": "^10.1|^11.0", + "rector/rector": "^1.0", + "roave/security-advisories": "dev-latest", + "symfony/phpunit-bridge": "^6.4|^7.0", + "symfony/string": "^6.4|^7.0", + "symfony/var-dumper": "^6.4|^7.0", + "symplify/easy-coding-standard": "^12.0" + }, + "suggest": { + "ext-bcmath": "For better performance (or GMP)", + "ext-gmp": "For better performance (or BCMath)", + "ext-openssl": "For OpenSSL based cyphering" + }, + "type": "library", + "autoload": { + "psr-4": { + "SpomkyLabs\\Pki\\": "src/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Joni Eskelinen", + "email": "jonieske@gmail.com", + "role": "Original developer" + }, + { + "name": "Florent Morselli", + "email": "florent.morselli@spomky-labs.com", + "role": "Spomky-Labs PKI Framework developer" + } + ], + "description": "A PHP framework for managing Public Key Infrastructures. It comprises X.509 public key certificates, attribute certificates, certification requests and certification path validation.", + "homepage": "https://github.com/spomky-labs/pki-framework", + "keywords": [ + "DER", + "Private Key", + "ac", + "algorithm identifier", + "asn.1", + "asn1", + "attribute certificate", + "certificate", + "certification request", + "cryptography", + "csr", + "decrypt", + "ec", + "encrypt", + "pem", + "pkcs", + "public key", + "rsa", + "sign", + "signature", + "verify", + "x.509", + "x.690", + "x509", + "x690" + ], + "support": { + "issues": "https://github.com/Spomky-Labs/pki-framework/issues", + "source": "https://github.com/Spomky-Labs/pki-framework/tree/1.2.1" + }, + "funding": [ + { + "url": "https://github.com/Spomky", + "type": "github" + }, + { + "url": "https://www.patreon.com/FlorentMorselli", + "type": "patreon" + } + ], + "time": "2024-03-30T18:03:49+00:00" + }, + { + "name": "stecman/symfony-console-completion", + "version": "v0.14.0", + "source": { + "type": "git", + "url": "https://github.com/stecman/symfony-console-completion.git", + "reference": "93b88586e50c05b7375a547031f535bf1ea970f5" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/stecman/symfony-console-completion/zipball/93b88586e50c05b7375a547031f535bf1ea970f5", + "reference": "93b88586e50c05b7375a547031f535bf1ea970f5", + "shasum": "" + }, + "require": { + "php": ">=8.1", + "symfony/console": "~6.4 || ^7.1" + }, + "require-dev": { + "dms/phpunit-arraysubset-asserts": "^0.4.0", + "phpunit/phpunit": "^9.5" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "0.14.x-dev" + } + }, + "autoload": { + "psr-4": { + "Stecman\\Component\\Symfony\\Console\\BashCompletion\\": "src/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Stephen Holdaway", + "email": "stephen@stecman.co.nz" + } + ], + "description": "Automatic BASH completion for Symfony Console Component based applications.", + "support": { + "issues": "https://github.com/stecman/symfony-console-completion/issues", + "source": "https://github.com/stecman/symfony-console-completion/tree/v0.14.0" + }, + "time": "2024-11-09T03:07:26+00:00" + }, + { + "name": "symfony/console", + "version": "v6.4.17", + "source": { + "type": "git", + "url": "https://github.com/symfony/console.git", + "reference": "799445db3f15768ecc382ac5699e6da0520a0a04" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/symfony/console/zipball/799445db3f15768ecc382ac5699e6da0520a0a04", + "reference": "799445db3f15768ecc382ac5699e6da0520a0a04", + "shasum": "" + }, + "require": { + "php": ">=8.1", + "symfony/deprecation-contracts": "^2.5|^3", + "symfony/polyfill-mbstring": "~1.0", + "symfony/service-contracts": "^2.5|^3", + "symfony/string": "^5.4|^6.0|^7.0" + }, + "conflict": { + "symfony/dependency-injection": "<5.4", + "symfony/dotenv": "<5.4", + "symfony/event-dispatcher": "<5.4", + "symfony/lock": "<5.4", + "symfony/process": "<5.4" + }, + "provide": { + "psr/log-implementation": "1.0|2.0|3.0" + }, + "require-dev": { + "psr/log": "^1|^2|^3", + "symfony/config": "^5.4|^6.0|^7.0", + "symfony/dependency-injection": "^5.4|^6.0|^7.0", + "symfony/event-dispatcher": "^5.4|^6.0|^7.0", + "symfony/http-foundation": "^6.4|^7.0", + "symfony/http-kernel": "^6.4|^7.0", + "symfony/lock": "^5.4|^6.0|^7.0", + "symfony/messenger": "^5.4|^6.0|^7.0", + "symfony/process": "^5.4|^6.0|^7.0", + "symfony/stopwatch": "^5.4|^6.0|^7.0", + "symfony/var-dumper": "^5.4|^6.0|^7.0" + }, + "type": "library", + "autoload": { + "psr-4": { + "Symfony\\Component\\Console\\": "" + }, + "exclude-from-classmap": [ + "/Tests/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Fabien Potencier", + "email": "fabien@symfony.com" + }, + { + "name": "Symfony Community", + "homepage": "https://symfony.com/contributors" + } + ], + "description": "Eases the creation of beautiful and testable command line interfaces", + "homepage": "https://symfony.com", + "keywords": [ + "cli", + "command-line", + "console", + "terminal" + ], + "support": { + "source": "https://github.com/symfony/console/tree/v6.4.17" + }, + "funding": [ + { + "url": "https://symfony.com/sponsor", + "type": "custom" + }, + { + "url": "https://github.com/fabpot", + "type": "github" + }, + { + "url": "https://tidelift.com/funding/github/packagist/symfony/symfony", + "type": "tidelift" + } + ], + "time": "2024-12-07T12:07:30+00:00" + }, + { + "name": "symfony/css-selector", + "version": "v6.4.13", + "source": { + "type": "git", + "url": "https://github.com/symfony/css-selector.git", + "reference": "cb23e97813c5837a041b73a6d63a9ddff0778f5e" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/symfony/css-selector/zipball/cb23e97813c5837a041b73a6d63a9ddff0778f5e", + "reference": "cb23e97813c5837a041b73a6d63a9ddff0778f5e", + "shasum": "" + }, + "require": { + "php": ">=8.1" + }, + "type": "library", + "autoload": { + "psr-4": { + "Symfony\\Component\\CssSelector\\": "" + }, + "exclude-from-classmap": [ + "/Tests/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Fabien Potencier", + "email": "fabien@symfony.com" + }, + { + "name": "Jean-François Simon", + "email": "jeanfrancois.simon@sensiolabs.com" + }, + { + "name": "Symfony Community", + "homepage": "https://symfony.com/contributors" + } + ], + "description": "Converts CSS selectors to XPath expressions", + "homepage": "https://symfony.com", + "support": { + "source": "https://github.com/symfony/css-selector/tree/v6.4.13" + }, + "funding": [ + { + "url": "https://symfony.com/sponsor", + "type": "custom" + }, + { + "url": "https://github.com/fabpot", + "type": "github" + }, + { + "url": "https://tidelift.com/funding/github/packagist/symfony/symfony", + "type": "tidelift" + } + ], + "time": "2024-09-25T14:18:03+00:00" + }, + { + "name": "symfony/deprecation-contracts", + "version": "v3.6.0", + "source": { + "type": "git", + "url": "https://github.com/symfony/deprecation-contracts.git", + "reference": "63afe740e99a13ba87ec199bb07bbdee937a5b62" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/symfony/deprecation-contracts/zipball/63afe740e99a13ba87ec199bb07bbdee937a5b62", + "reference": "63afe740e99a13ba87ec199bb07bbdee937a5b62", + "shasum": "" + }, + "require": { + "php": ">=8.1" + }, + "type": "library", + "extra": { + "thanks": { + "url": "https://github.com/symfony/contracts", + "name": "symfony/contracts" + }, + "branch-alias": { + "dev-main": "3.6-dev" + } + }, + "autoload": { + "files": [ + "function.php" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Nicolas Grekas", + "email": "p@tchwork.com" + }, + { + "name": "Symfony Community", + "homepage": "https://symfony.com/contributors" + } + ], + "description": "A generic function and convention to trigger deprecation notices", + "homepage": "https://symfony.com", + "support": { + "source": "https://github.com/symfony/deprecation-contracts/tree/v3.6.0" + }, + "funding": [ + { + "url": "https://symfony.com/sponsor", + "type": "custom" + }, + { + "url": "https://github.com/fabpot", + "type": "github" + }, + { + "url": "https://tidelift.com/funding/github/packagist/symfony/symfony", + "type": "tidelift" + } + ], + "time": "2024-09-25T14:21:43+00:00" + }, + { + "name": "symfony/dom-crawler", + "version": "v6.4.23", + "source": { + "type": "git", + "url": "https://github.com/symfony/dom-crawler.git", + "reference": "22210aacb35dbadd772325d759d17bce2374a84d" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/symfony/dom-crawler/zipball/22210aacb35dbadd772325d759d17bce2374a84d", + "reference": "22210aacb35dbadd772325d759d17bce2374a84d", + "shasum": "" + }, + "require": { + "masterminds/html5": "^2.6", + "php": ">=8.1", + "symfony/polyfill-ctype": "~1.8", + "symfony/polyfill-mbstring": "~1.0" + }, + "require-dev": { + "symfony/css-selector": "^5.4|^6.0|^7.0" + }, + "type": "library", + "autoload": { + "psr-4": { + "Symfony\\Component\\DomCrawler\\": "" + }, + "exclude-from-classmap": [ + "/Tests/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Fabien Potencier", + "email": "fabien@symfony.com" + }, + { + "name": "Symfony Community", + "homepage": "https://symfony.com/contributors" + } + ], + "description": "Eases DOM navigation for HTML and XML documents", + "homepage": "https://symfony.com", + "support": { + "source": "https://github.com/symfony/dom-crawler/tree/v6.4.23" + }, + "funding": [ + { + "url": "https://symfony.com/sponsor", + "type": "custom" + }, + { + "url": "https://github.com/fabpot", + "type": "github" + }, + { + "url": "https://tidelift.com/funding/github/packagist/symfony/symfony", + "type": "tidelift" + } + ], + "time": "2025-06-13T12:10:00+00:00" + }, + { + "name": "symfony/event-dispatcher", + "version": "v6.4.8", + "source": { + "type": "git", + "url": "https://github.com/symfony/event-dispatcher.git", + "reference": "8d7507f02b06e06815e56bb39aa0128e3806208b" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/symfony/event-dispatcher/zipball/8d7507f02b06e06815e56bb39aa0128e3806208b", + "reference": "8d7507f02b06e06815e56bb39aa0128e3806208b", + "shasum": "" + }, + "require": { + "php": ">=8.1", + "symfony/event-dispatcher-contracts": "^2.5|^3" + }, + "conflict": { + "symfony/dependency-injection": "<5.4", + "symfony/service-contracts": "<2.5" + }, + "provide": { + "psr/event-dispatcher-implementation": "1.0", + "symfony/event-dispatcher-implementation": "2.0|3.0" + }, + "require-dev": { + "psr/log": "^1|^2|^3", + "symfony/config": "^5.4|^6.0|^7.0", + "symfony/dependency-injection": "^5.4|^6.0|^7.0", + "symfony/error-handler": "^5.4|^6.0|^7.0", + "symfony/expression-language": "^5.4|^6.0|^7.0", + "symfony/http-foundation": "^5.4|^6.0|^7.0", + "symfony/service-contracts": "^2.5|^3", + "symfony/stopwatch": "^5.4|^6.0|^7.0" + }, + "type": "library", + "autoload": { + "psr-4": { + "Symfony\\Component\\EventDispatcher\\": "" + }, + "exclude-from-classmap": [ + "/Tests/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Fabien Potencier", + "email": "fabien@symfony.com" + }, + { + "name": "Symfony Community", + "homepage": "https://symfony.com/contributors" + } + ], + "description": "Provides tools that allow your application components to communicate with each other by dispatching events and listening to them", + "homepage": "https://symfony.com", + "support": { + "source": "https://github.com/symfony/event-dispatcher/tree/v6.4.8" + }, + "funding": [ + { + "url": "https://symfony.com/sponsor", + "type": "custom" + }, + { + "url": "https://github.com/fabpot", + "type": "github" + }, + { + "url": "https://tidelift.com/funding/github/packagist/symfony/symfony", + "type": "tidelift" + } + ], + "time": "2024-05-31T14:49:08+00:00" + }, + { + "name": "symfony/event-dispatcher-contracts", + "version": "v3.5.0", + "source": { + "type": "git", + "url": "https://github.com/symfony/event-dispatcher-contracts.git", + "reference": "8f93aec25d41b72493c6ddff14e916177c9efc50" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/symfony/event-dispatcher-contracts/zipball/8f93aec25d41b72493c6ddff14e916177c9efc50", + "reference": "8f93aec25d41b72493c6ddff14e916177c9efc50", + "shasum": "" + }, + "require": { + "php": ">=8.1", + "psr/event-dispatcher": "^1" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-main": "3.5-dev" + }, + "thanks": { + "name": "symfony/contracts", + "url": "https://github.com/symfony/contracts" + } + }, + "autoload": { + "psr-4": { + "Symfony\\Contracts\\EventDispatcher\\": "" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Nicolas Grekas", + "email": "p@tchwork.com" + }, + { + "name": "Symfony Community", + "homepage": "https://symfony.com/contributors" + } + ], + "description": "Generic abstractions related to dispatching event", + "homepage": "https://symfony.com", + "keywords": [ + "abstractions", + "contracts", + "decoupling", + "interfaces", + "interoperability", + "standards" + ], + "support": { + "source": "https://github.com/symfony/event-dispatcher-contracts/tree/v3.5.0" + }, + "funding": [ + { + "url": "https://symfony.com/sponsor", + "type": "custom" + }, + { + "url": "https://github.com/fabpot", + "type": "github" + }, + { + "url": "https://tidelift.com/funding/github/packagist/symfony/symfony", + "type": "tidelift" + } + ], + "time": "2024-04-18T09:32:20+00:00" + }, + { + "name": "symfony/http-foundation", + "version": "v6.4.29", + "source": { + "type": "git", + "url": "https://github.com/symfony/http-foundation.git", + "reference": "b03d11e015552a315714c127d8d1e0f9e970ec88" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/symfony/http-foundation/zipball/b03d11e015552a315714c127d8d1e0f9e970ec88", + "reference": "b03d11e015552a315714c127d8d1e0f9e970ec88", + "shasum": "" + }, + "require": { + "php": ">=8.1", + "symfony/deprecation-contracts": "^2.5|^3", + "symfony/polyfill-mbstring": "~1.1", + "symfony/polyfill-php83": "^1.27" + }, + "conflict": { + "symfony/cache": "<6.4.12|>=7.0,<7.1.5" + }, + "require-dev": { + "doctrine/dbal": "^2.13.1|^3|^4", + "predis/predis": "^1.1|^2.0", + "symfony/cache": "^6.4.12|^7.1.5", + "symfony/dependency-injection": "^5.4|^6.0|^7.0", + "symfony/expression-language": "^5.4|^6.0|^7.0", + "symfony/http-kernel": "^5.4.12|^6.0.12|^6.1.4|^7.0", + "symfony/mime": "^5.4|^6.0|^7.0", + "symfony/rate-limiter": "^5.4|^6.0|^7.0" + }, + "type": "library", + "autoload": { + "psr-4": { + "Symfony\\Component\\HttpFoundation\\": "" + }, + "exclude-from-classmap": [ + "/Tests/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Fabien Potencier", + "email": "fabien@symfony.com" + }, + { + "name": "Symfony Community", + "homepage": "https://symfony.com/contributors" + } + ], + "description": "Defines an object-oriented layer for the HTTP specification", + "homepage": "https://symfony.com", + "support": { + "source": "https://github.com/symfony/http-foundation/tree/v6.4.29" + }, + "funding": [ + { + "url": "https://symfony.com/sponsor", + "type": "custom" + }, + { + "url": "https://github.com/fabpot", + "type": "github" + }, + { + "url": "https://github.com/nicolas-grekas", + "type": "github" + }, + { + "url": "https://tidelift.com/funding/github/packagist/symfony/symfony", + "type": "tidelift" + } + ], + "time": "2025-11-08T16:40:12+00:00" + }, + { + "name": "symfony/mailer", + "version": "v6.4.12", + "source": { + "type": "git", + "url": "https://github.com/symfony/mailer.git", + "reference": "b6a25408c569ae2366b3f663a4edad19420a9c26" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/symfony/mailer/zipball/b6a25408c569ae2366b3f663a4edad19420a9c26", + "reference": "b6a25408c569ae2366b3f663a4edad19420a9c26", + "shasum": "" + }, + "require": { + "egulias/email-validator": "^2.1.10|^3|^4", + "php": ">=8.1", + "psr/event-dispatcher": "^1", + "psr/log": "^1|^2|^3", + "symfony/event-dispatcher": "^5.4|^6.0|^7.0", + "symfony/mime": "^6.2|^7.0", + "symfony/service-contracts": "^2.5|^3" + }, + "conflict": { + "symfony/http-client-contracts": "<2.5", + "symfony/http-kernel": "<5.4", + "symfony/messenger": "<6.2", + "symfony/mime": "<6.2", + "symfony/twig-bridge": "<6.2.1" + }, + "require-dev": { + "symfony/console": "^5.4|^6.0|^7.0", + "symfony/http-client": "^5.4|^6.0|^7.0", + "symfony/messenger": "^6.2|^7.0", + "symfony/twig-bridge": "^6.2|^7.0" + }, + "type": "library", + "autoload": { + "psr-4": { + "Symfony\\Component\\Mailer\\": "" + }, + "exclude-from-classmap": [ + "/Tests/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Fabien Potencier", + "email": "fabien@symfony.com" + }, + { + "name": "Symfony Community", + "homepage": "https://symfony.com/contributors" + } + ], + "description": "Helps sending emails", + "homepage": "https://symfony.com", + "support": { + "source": "https://github.com/symfony/mailer/tree/v6.4.12" + }, + "funding": [ + { + "url": "https://symfony.com/sponsor", + "type": "custom" + }, + { + "url": "https://github.com/fabpot", + "type": "github" + }, + { + "url": "https://tidelift.com/funding/github/packagist/symfony/symfony", + "type": "tidelift" + } + ], + "time": "2024-09-08T12:30:05+00:00" + }, + { + "name": "symfony/mime", + "version": "v6.4.12", + "source": { + "type": "git", + "url": "https://github.com/symfony/mime.git", + "reference": "abe16ee7790b16aa525877419deb0f113953f0e1" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/symfony/mime/zipball/abe16ee7790b16aa525877419deb0f113953f0e1", + "reference": "abe16ee7790b16aa525877419deb0f113953f0e1", + "shasum": "" + }, + "require": { + "php": ">=8.1", + "symfony/deprecation-contracts": "^2.5|^3", + "symfony/polyfill-intl-idn": "^1.10", + "symfony/polyfill-mbstring": "^1.0" + }, + "conflict": { + "egulias/email-validator": "~3.0.0", + "phpdocumentor/reflection-docblock": "<3.2.2", + "phpdocumentor/type-resolver": "<1.4.0", + "symfony/mailer": "<5.4", + "symfony/serializer": "<6.4.3|>7.0,<7.0.3" + }, + "require-dev": { + "egulias/email-validator": "^2.1.10|^3.1|^4", + "league/html-to-markdown": "^5.0", + "phpdocumentor/reflection-docblock": "^3.0|^4.0|^5.0", + "symfony/dependency-injection": "^5.4|^6.0|^7.0", + "symfony/process": "^5.4|^6.4|^7.0", + "symfony/property-access": "^5.4|^6.0|^7.0", + "symfony/property-info": "^5.4|^6.0|^7.0", + "symfony/serializer": "^6.4.3|^7.0.3" + }, + "type": "library", + "autoload": { + "psr-4": { + "Symfony\\Component\\Mime\\": "" + }, + "exclude-from-classmap": [ + "/Tests/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Fabien Potencier", + "email": "fabien@symfony.com" + }, + { + "name": "Symfony Community", + "homepage": "https://symfony.com/contributors" + } + ], + "description": "Allows manipulating MIME messages", + "homepage": "https://symfony.com", + "keywords": [ + "mime", + "mime-type" + ], + "support": { + "source": "https://github.com/symfony/mime/tree/v6.4.12" + }, + "funding": [ + { + "url": "https://symfony.com/sponsor", + "type": "custom" + }, + { + "url": "https://github.com/fabpot", + "type": "github" + }, + { + "url": "https://tidelift.com/funding/github/packagist/symfony/symfony", + "type": "tidelift" + } + ], + "time": "2024-09-20T08:18:25+00:00" + }, + { + "name": "symfony/polyfill-intl-grapheme", + "version": "v1.32.0", + "source": { + "type": "git", + "url": "https://github.com/symfony/polyfill-intl-grapheme.git", + "reference": "b9123926e3b7bc2f98c02ad54f6a4b02b91a8abe" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/symfony/polyfill-intl-grapheme/zipball/b9123926e3b7bc2f98c02ad54f6a4b02b91a8abe", + "reference": "b9123926e3b7bc2f98c02ad54f6a4b02b91a8abe", + "shasum": "" + }, + "require": { + "php": ">=7.2" + }, + "suggest": { + "ext-intl": "For best performance" + }, + "type": "library", + "extra": { + "thanks": { + "url": "https://github.com/symfony/polyfill", + "name": "symfony/polyfill" + } + }, + "autoload": { + "files": [ + "bootstrap.php" + ], + "psr-4": { + "Symfony\\Polyfill\\Intl\\Grapheme\\": "" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Nicolas Grekas", + "email": "p@tchwork.com" + }, + { + "name": "Symfony Community", + "homepage": "https://symfony.com/contributors" + } + ], + "description": "Symfony polyfill for intl's grapheme_* functions", + "homepage": "https://symfony.com", + "keywords": [ + "compatibility", + "grapheme", + "intl", + "polyfill", + "portable", + "shim" + ], + "support": { + "source": "https://github.com/symfony/polyfill-intl-grapheme/tree/v1.32.0" + }, + "funding": [ + { + "url": "https://symfony.com/sponsor", + "type": "custom" + }, + { + "url": "https://github.com/fabpot", + "type": "github" + }, + { + "url": "https://tidelift.com/funding/github/packagist/symfony/symfony", + "type": "tidelift" + } + ], + "time": "2024-09-09T11:45:10+00:00" + }, + { + "name": "symfony/polyfill-intl-idn", + "version": "v1.32.0", + "source": { + "type": "git", + "url": "https://github.com/symfony/polyfill-intl-idn.git", + "reference": "9614ac4d8061dc257ecc64cba1b140873dce8ad3" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/symfony/polyfill-intl-idn/zipball/9614ac4d8061dc257ecc64cba1b140873dce8ad3", + "reference": "9614ac4d8061dc257ecc64cba1b140873dce8ad3", + "shasum": "" + }, + "require": { + "php": ">=7.2", + "symfony/polyfill-intl-normalizer": "^1.10" + }, + "suggest": { + "ext-intl": "For best performance" + }, + "type": "library", + "extra": { + "thanks": { + "url": "https://github.com/symfony/polyfill", + "name": "symfony/polyfill" + } + }, + "autoload": { + "files": [ + "bootstrap.php" + ], + "psr-4": { + "Symfony\\Polyfill\\Intl\\Idn\\": "" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Laurent Bassin", + "email": "laurent@bassin.info" + }, + { + "name": "Trevor Rowbotham", + "email": "trevor.rowbotham@pm.me" + }, + { + "name": "Symfony Community", + "homepage": "https://symfony.com/contributors" + } + ], + "description": "Symfony polyfill for intl's idn_to_ascii and idn_to_utf8 functions", + "homepage": "https://symfony.com", + "keywords": [ + "compatibility", + "idn", + "intl", + "polyfill", + "portable", + "shim" + ], + "support": { + "source": "https://github.com/symfony/polyfill-intl-idn/tree/v1.32.0" + }, + "funding": [ + { + "url": "https://symfony.com/sponsor", + "type": "custom" + }, + { + "url": "https://github.com/fabpot", + "type": "github" + }, + { + "url": "https://tidelift.com/funding/github/packagist/symfony/symfony", + "type": "tidelift" + } + ], + "time": "2024-09-10T14:38:51+00:00" + }, + { + "name": "symfony/polyfill-intl-normalizer", + "version": "v1.32.0", + "source": { + "type": "git", + "url": "https://github.com/symfony/polyfill-intl-normalizer.git", + "reference": "3833d7255cc303546435cb650316bff708a1c75c" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/symfony/polyfill-intl-normalizer/zipball/3833d7255cc303546435cb650316bff708a1c75c", + "reference": "3833d7255cc303546435cb650316bff708a1c75c", + "shasum": "" + }, + "require": { + "php": ">=7.2" + }, + "suggest": { + "ext-intl": "For best performance" + }, + "type": "library", + "extra": { + "thanks": { + "url": "https://github.com/symfony/polyfill", + "name": "symfony/polyfill" + } + }, + "autoload": { + "files": [ + "bootstrap.php" + ], + "psr-4": { + "Symfony\\Polyfill\\Intl\\Normalizer\\": "" + }, + "classmap": [ + "Resources/stubs" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Nicolas Grekas", + "email": "p@tchwork.com" + }, + { + "name": "Symfony Community", + "homepage": "https://symfony.com/contributors" + } + ], + "description": "Symfony polyfill for intl's Normalizer class and related functions", + "homepage": "https://symfony.com", + "keywords": [ + "compatibility", + "intl", + "normalizer", + "polyfill", + "portable", + "shim" + ], + "support": { + "source": "https://github.com/symfony/polyfill-intl-normalizer/tree/v1.32.0" + }, + "funding": [ + { + "url": "https://symfony.com/sponsor", + "type": "custom" + }, + { + "url": "https://github.com/fabpot", + "type": "github" + }, + { + "url": "https://tidelift.com/funding/github/packagist/symfony/symfony", + "type": "tidelift" + } + ], + "time": "2024-09-09T11:45:10+00:00" + }, + { + "name": "symfony/polyfill-php82", + "version": "v1.32.0", + "source": { + "type": "git", + "url": "https://github.com/symfony/polyfill-php82.git", + "reference": "5d2ed36f7734637dacc025f179698031951b1692" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/symfony/polyfill-php82/zipball/5d2ed36f7734637dacc025f179698031951b1692", + "reference": "5d2ed36f7734637dacc025f179698031951b1692", + "shasum": "" + }, + "require": { + "php": ">=7.2" + }, + "type": "library", + "extra": { + "thanks": { + "url": "https://github.com/symfony/polyfill", + "name": "symfony/polyfill" + } + }, + "autoload": { + "files": [ + "bootstrap.php" + ], + "psr-4": { + "Symfony\\Polyfill\\Php82\\": "" + }, + "classmap": [ + "Resources/stubs" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Nicolas Grekas", + "email": "p@tchwork.com" + }, + { + "name": "Symfony Community", + "homepage": "https://symfony.com/contributors" + } + ], + "description": "Symfony polyfill backporting some PHP 8.2+ features to lower PHP versions", + "homepage": "https://symfony.com", + "keywords": [ + "compatibility", + "polyfill", + "portable", + "shim" + ], + "support": { + "source": "https://github.com/symfony/polyfill-php82/tree/v1.32.0" + }, + "funding": [ + { + "url": "https://symfony.com/sponsor", + "type": "custom" + }, + { + "url": "https://github.com/fabpot", + "type": "github" + }, + { + "url": "https://tidelift.com/funding/github/packagist/symfony/symfony", + "type": "tidelift" + } + ], + "time": "2024-09-09T11:45:10+00:00" + }, + { + "name": "symfony/polyfill-php83", + "version": "v1.32.0", + "source": { + "type": "git", + "url": "https://github.com/symfony/polyfill-php83.git", + "reference": "2fb86d65e2d424369ad2905e83b236a8805ba491" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/symfony/polyfill-php83/zipball/2fb86d65e2d424369ad2905e83b236a8805ba491", + "reference": "2fb86d65e2d424369ad2905e83b236a8805ba491", + "shasum": "" + }, + "require": { + "php": ">=7.2" + }, + "type": "library", + "extra": { + "thanks": { + "url": "https://github.com/symfony/polyfill", + "name": "symfony/polyfill" + } + }, + "autoload": { + "files": [ + "bootstrap.php" + ], + "psr-4": { + "Symfony\\Polyfill\\Php83\\": "" + }, + "classmap": [ + "Resources/stubs" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Nicolas Grekas", + "email": "p@tchwork.com" + }, + { + "name": "Symfony Community", + "homepage": "https://symfony.com/contributors" + } + ], + "description": "Symfony polyfill backporting some PHP 8.3+ features to lower PHP versions", + "homepage": "https://symfony.com", + "keywords": [ + "compatibility", + "polyfill", + "portable", + "shim" + ], + "support": { + "source": "https://github.com/symfony/polyfill-php83/tree/v1.32.0" + }, + "funding": [ + { + "url": "https://symfony.com/sponsor", + "type": "custom" + }, + { + "url": "https://github.com/fabpot", + "type": "github" + }, + { + "url": "https://tidelift.com/funding/github/packagist/symfony/symfony", + "type": "tidelift" + } + ], + "time": "2024-09-09T11:45:10+00:00" + }, + { + "name": "symfony/polyfill-php84", + "version": "v1.32.0", + "source": { + "type": "git", + "url": "https://github.com/symfony/polyfill-php84.git", + "reference": "000df7860439609837bbe28670b0be15783b7fbf" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/symfony/polyfill-php84/zipball/000df7860439609837bbe28670b0be15783b7fbf", + "reference": "000df7860439609837bbe28670b0be15783b7fbf", + "shasum": "" + }, + "require": { + "php": ">=7.2" + }, + "type": "library", + "extra": { + "thanks": { + "url": "https://github.com/symfony/polyfill", + "name": "symfony/polyfill" + } + }, + "autoload": { + "files": [ + "bootstrap.php" + ], + "psr-4": { + "Symfony\\Polyfill\\Php84\\": "" + }, + "classmap": [ + "Resources/stubs" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Nicolas Grekas", + "email": "p@tchwork.com" + }, + { + "name": "Symfony Community", + "homepage": "https://symfony.com/contributors" + } + ], + "description": "Symfony polyfill backporting some PHP 8.4+ features to lower PHP versions", + "homepage": "https://symfony.com", + "keywords": [ + "compatibility", + "polyfill", + "portable", + "shim" + ], + "support": { + "source": "https://github.com/symfony/polyfill-php84/tree/v1.32.0" + }, + "funding": [ + { + "url": "https://symfony.com/sponsor", + "type": "custom" + }, + { + "url": "https://github.com/fabpot", + "type": "github" + }, + { + "url": "https://tidelift.com/funding/github/packagist/symfony/symfony", + "type": "tidelift" + } + ], + "time": "2025-02-20T12:04:08+00:00" + }, + { + "name": "symfony/polyfill-uuid", + "version": "v1.29.0", + "source": { + "type": "git", + "url": "https://github.com/symfony/polyfill-uuid.git", + "reference": "3abdd21b0ceaa3000ee950097bc3cf9efc137853" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/symfony/polyfill-uuid/zipball/3abdd21b0ceaa3000ee950097bc3cf9efc137853", + "reference": "3abdd21b0ceaa3000ee950097bc3cf9efc137853", + "shasum": "" + }, + "require": { + "php": ">=7.1" + }, + "provide": { + "ext-uuid": "*" + }, + "suggest": { + "ext-uuid": "For best performance" + }, + "type": "library", + "extra": { + "thanks": { + "name": "symfony/polyfill", + "url": "https://github.com/symfony/polyfill" + } + }, + "autoload": { + "files": [ + "bootstrap.php" + ], + "psr-4": { + "Symfony\\Polyfill\\Uuid\\": "" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Grégoire Pineau", + "email": "lyrixx@lyrixx.info" + }, + { + "name": "Symfony Community", + "homepage": "https://symfony.com/contributors" + } + ], + "description": "Symfony polyfill for uuid functions", + "homepage": "https://symfony.com", + "keywords": [ + "compatibility", + "polyfill", + "portable", + "uuid" + ], + "support": { + "source": "https://github.com/symfony/polyfill-uuid/tree/v1.29.0" + }, + "funding": [ + { + "url": "https://symfony.com/sponsor", + "type": "custom" + }, + { + "url": "https://github.com/fabpot", + "type": "github" + }, + { + "url": "https://tidelift.com/funding/github/packagist/symfony/symfony", + "type": "tidelift" + } + ], + "time": "2024-01-29T20:11:03+00:00" + }, + { + "name": "symfony/process", + "version": "v6.4.15", + "source": { + "type": "git", + "url": "https://github.com/symfony/process.git", + "reference": "3cb242f059c14ae08591c5c4087d1fe443564392" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/symfony/process/zipball/3cb242f059c14ae08591c5c4087d1fe443564392", + "reference": "3cb242f059c14ae08591c5c4087d1fe443564392", + "shasum": "" + }, + "require": { + "php": ">=8.1" + }, + "type": "library", + "autoload": { + "psr-4": { + "Symfony\\Component\\Process\\": "" + }, + "exclude-from-classmap": [ + "/Tests/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Fabien Potencier", + "email": "fabien@symfony.com" + }, + { + "name": "Symfony Community", + "homepage": "https://symfony.com/contributors" + } + ], + "description": "Executes commands in sub-processes", + "homepage": "https://symfony.com", + "support": { + "source": "https://github.com/symfony/process/tree/v6.4.15" + }, + "funding": [ + { + "url": "https://symfony.com/sponsor", + "type": "custom" + }, + { + "url": "https://github.com/fabpot", + "type": "github" + }, + { + "url": "https://tidelift.com/funding/github/packagist/symfony/symfony", + "type": "tidelift" + } + ], + "time": "2024-11-06T14:19:14+00:00" + }, + { + "name": "symfony/routing", + "version": "v6.4.12", + "source": { + "type": "git", + "url": "https://github.com/symfony/routing.git", + "reference": "a7c8036bd159486228dc9be3e846a00a0dda9f9f" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/symfony/routing/zipball/a7c8036bd159486228dc9be3e846a00a0dda9f9f", + "reference": "a7c8036bd159486228dc9be3e846a00a0dda9f9f", + "shasum": "" + }, + "require": { + "php": ">=8.1", + "symfony/deprecation-contracts": "^2.5|^3" + }, + "conflict": { + "doctrine/annotations": "<1.12", + "symfony/config": "<6.2", + "symfony/dependency-injection": "<5.4", + "symfony/yaml": "<5.4" + }, + "require-dev": { + "doctrine/annotations": "^1.12|^2", + "psr/log": "^1|^2|^3", + "symfony/config": "^6.2|^7.0", + "symfony/dependency-injection": "^5.4|^6.0|^7.0", + "symfony/expression-language": "^5.4|^6.0|^7.0", + "symfony/http-foundation": "^5.4|^6.0|^7.0", + "symfony/yaml": "^5.4|^6.0|^7.0" + }, + "type": "library", + "autoload": { + "psr-4": { + "Symfony\\Component\\Routing\\": "" + }, + "exclude-from-classmap": [ + "/Tests/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Fabien Potencier", + "email": "fabien@symfony.com" + }, + { + "name": "Symfony Community", + "homepage": "https://symfony.com/contributors" + } + ], + "description": "Maps an HTTP request to a set of configuration variables", + "homepage": "https://symfony.com", + "keywords": [ + "router", + "routing", + "uri", + "url" + ], + "support": { + "source": "https://github.com/symfony/routing/tree/v6.4.12" + }, + "funding": [ + { + "url": "https://symfony.com/sponsor", + "type": "custom" + }, + { + "url": "https://github.com/fabpot", + "type": "github" + }, + { + "url": "https://tidelift.com/funding/github/packagist/symfony/symfony", + "type": "tidelift" + } + ], + "time": "2024-09-20T08:32:26+00:00" + }, + { + "name": "symfony/service-contracts", + "version": "v3.5.1", + "source": { + "type": "git", + "url": "https://github.com/symfony/service-contracts.git", + "reference": "e53260aabf78fb3d63f8d79d69ece59f80d5eda0" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/symfony/service-contracts/zipball/e53260aabf78fb3d63f8d79d69ece59f80d5eda0", + "reference": "e53260aabf78fb3d63f8d79d69ece59f80d5eda0", + "shasum": "" + }, + "require": { + "php": ">=8.1", + "psr/container": "^1.1|^2.0", + "symfony/deprecation-contracts": "^2.5|^3" + }, + "conflict": { + "ext-psr": "<1.1|>=2" + }, + "type": "library", + "extra": { + "thanks": { + "url": "https://github.com/symfony/contracts", + "name": "symfony/contracts" + }, + "branch-alias": { + "dev-main": "3.5-dev" + } + }, + "autoload": { + "psr-4": { + "Symfony\\Contracts\\Service\\": "" + }, + "exclude-from-classmap": [ + "/Test/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Nicolas Grekas", + "email": "p@tchwork.com" + }, + { + "name": "Symfony Community", + "homepage": "https://symfony.com/contributors" + } + ], + "description": "Generic abstractions related to writing services", + "homepage": "https://symfony.com", + "keywords": [ + "abstractions", + "contracts", + "decoupling", + "interfaces", + "interoperability", + "standards" + ], + "support": { + "source": "https://github.com/symfony/service-contracts/tree/v3.5.1" + }, + "funding": [ + { + "url": "https://symfony.com/sponsor", + "type": "custom" + }, + { + "url": "https://github.com/fabpot", + "type": "github" + }, + { + "url": "https://tidelift.com/funding/github/packagist/symfony/symfony", + "type": "tidelift" + } + ], + "time": "2024-09-25T14:20:29+00:00" + }, + { + "name": "symfony/string", + "version": "v6.4.15", + "source": { + "type": "git", + "url": "https://github.com/symfony/string.git", + "reference": "73a5e66ea2e1677c98d4449177c5a9cf9d8b4c6f" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/symfony/string/zipball/73a5e66ea2e1677c98d4449177c5a9cf9d8b4c6f", + "reference": "73a5e66ea2e1677c98d4449177c5a9cf9d8b4c6f", + "shasum": "" + }, + "require": { + "php": ">=8.1", + "symfony/polyfill-ctype": "~1.8", + "symfony/polyfill-intl-grapheme": "~1.0", + "symfony/polyfill-intl-normalizer": "~1.0", + "symfony/polyfill-mbstring": "~1.0" + }, + "conflict": { + "symfony/translation-contracts": "<2.5" + }, + "require-dev": { + "symfony/error-handler": "^5.4|^6.0|^7.0", + "symfony/http-client": "^5.4|^6.0|^7.0", + "symfony/intl": "^6.2|^7.0", + "symfony/translation-contracts": "^2.5|^3.0", + "symfony/var-exporter": "^5.4|^6.0|^7.0" + }, + "type": "library", + "autoload": { + "files": [ + "Resources/functions.php" + ], + "psr-4": { + "Symfony\\Component\\String\\": "" + }, + "exclude-from-classmap": [ + "/Tests/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Nicolas Grekas", + "email": "p@tchwork.com" + }, + { + "name": "Symfony Community", + "homepage": "https://symfony.com/contributors" + } + ], + "description": "Provides an object-oriented API to strings and deals with bytes, UTF-8 code points and grapheme clusters in a unified way", + "homepage": "https://symfony.com", + "keywords": [ + "grapheme", + "i18n", + "string", + "unicode", + "utf-8", + "utf8" + ], + "support": { + "source": "https://github.com/symfony/string/tree/v6.4.15" + }, + "funding": [ + { + "url": "https://symfony.com/sponsor", + "type": "custom" + }, + { + "url": "https://github.com/fabpot", + "type": "github" + }, + { + "url": "https://tidelift.com/funding/github/packagist/symfony/symfony", + "type": "tidelift" + } + ], + "time": "2024-11-13T13:31:12+00:00" + }, + { + "name": "symfony/translation", + "version": "v6.4.4", + "source": { + "type": "git", + "url": "https://github.com/symfony/translation.git", + "reference": "bce6a5a78e94566641b2594d17e48b0da3184a8e" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/symfony/translation/zipball/bce6a5a78e94566641b2594d17e48b0da3184a8e", + "reference": "bce6a5a78e94566641b2594d17e48b0da3184a8e", + "shasum": "" + }, + "require": { + "php": ">=8.1", + "symfony/deprecation-contracts": "^2.5|^3", + "symfony/polyfill-mbstring": "~1.0", + "symfony/translation-contracts": "^2.5|^3.0" + }, + "conflict": { + "symfony/config": "<5.4", + "symfony/console": "<5.4", + "symfony/dependency-injection": "<5.4", + "symfony/http-client-contracts": "<2.5", + "symfony/http-kernel": "<5.4", + "symfony/service-contracts": "<2.5", + "symfony/twig-bundle": "<5.4", + "symfony/yaml": "<5.4" + }, + "provide": { + "symfony/translation-implementation": "2.3|3.0" + }, + "require-dev": { + "nikic/php-parser": "^4.18|^5.0", + "psr/log": "^1|^2|^3", + "symfony/config": "^5.4|^6.0|^7.0", + "symfony/console": "^5.4|^6.0|^7.0", + "symfony/dependency-injection": "^5.4|^6.0|^7.0", + "symfony/finder": "^5.4|^6.0|^7.0", + "symfony/http-client-contracts": "^2.5|^3.0", + "symfony/http-kernel": "^5.4|^6.0|^7.0", + "symfony/intl": "^5.4|^6.0|^7.0", + "symfony/polyfill-intl-icu": "^1.21", + "symfony/routing": "^5.4|^6.0|^7.0", + "symfony/service-contracts": "^2.5|^3", + "symfony/yaml": "^5.4|^6.0|^7.0" + }, + "type": "library", + "autoload": { + "files": [ + "Resources/functions.php" + ], + "psr-4": { + "Symfony\\Component\\Translation\\": "" + }, + "exclude-from-classmap": [ + "/Tests/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Fabien Potencier", + "email": "fabien@symfony.com" + }, + { + "name": "Symfony Community", + "homepage": "https://symfony.com/contributors" + } + ], + "description": "Provides tools to internationalize your application", + "homepage": "https://symfony.com", + "support": { + "source": "https://github.com/symfony/translation/tree/v6.4.4" + }, + "funding": [ + { + "url": "https://symfony.com/sponsor", + "type": "custom" + }, + { + "url": "https://github.com/fabpot", + "type": "github" + }, + { + "url": "https://tidelift.com/funding/github/packagist/symfony/symfony", + "type": "tidelift" + } + ], + "time": "2024-02-20T13:16:58+00:00" + }, + { + "name": "symfony/translation-contracts", + "version": "v3.4.2", + "source": { + "type": "git", + "url": "https://github.com/symfony/translation-contracts.git", + "reference": "43810bdb2ddb5400e5c5e778e27b210a0ca83b6b" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/symfony/translation-contracts/zipball/43810bdb2ddb5400e5c5e778e27b210a0ca83b6b", + "reference": "43810bdb2ddb5400e5c5e778e27b210a0ca83b6b", + "shasum": "" + }, + "require": { + "php": ">=8.1" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-main": "3.4-dev" + }, + "thanks": { + "name": "symfony/contracts", + "url": "https://github.com/symfony/contracts" + } + }, + "autoload": { + "psr-4": { + "Symfony\\Contracts\\Translation\\": "" + }, + "exclude-from-classmap": [ + "/Test/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Nicolas Grekas", + "email": "p@tchwork.com" + }, + { + "name": "Symfony Community", + "homepage": "https://symfony.com/contributors" + } + ], + "description": "Generic abstractions related to translation", + "homepage": "https://symfony.com", + "keywords": [ + "abstractions", + "contracts", + "decoupling", + "interfaces", + "interoperability", + "standards" + ], + "support": { + "source": "https://github.com/symfony/translation-contracts/tree/v3.4.2" + }, + "funding": [ + { + "url": "https://symfony.com/sponsor", + "type": "custom" + }, + { + "url": "https://github.com/fabpot", + "type": "github" + }, + { + "url": "https://tidelift.com/funding/github/packagist/symfony/symfony", + "type": "tidelift" + } + ], + "time": "2024-01-23T14:51:35+00:00" + }, + { + "name": "symfony/uid", + "version": "v6.4.3", + "source": { + "type": "git", + "url": "https://github.com/symfony/uid.git", + "reference": "1d31267211cc3a2fff32bcfc7c1818dac41b6fc0" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/symfony/uid/zipball/1d31267211cc3a2fff32bcfc7c1818dac41b6fc0", + "reference": "1d31267211cc3a2fff32bcfc7c1818dac41b6fc0", + "shasum": "" + }, + "require": { + "php": ">=8.1", + "symfony/polyfill-uuid": "^1.15" + }, + "require-dev": { + "symfony/console": "^5.4|^6.0|^7.0" + }, + "type": "library", + "autoload": { + "psr-4": { + "Symfony\\Component\\Uid\\": "" + }, + "exclude-from-classmap": [ + "/Tests/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Grégoire Pineau", + "email": "lyrixx@lyrixx.info" + }, + { + "name": "Nicolas Grekas", + "email": "p@tchwork.com" + }, + { + "name": "Symfony Community", + "homepage": "https://symfony.com/contributors" + } + ], + "description": "Provides an object-oriented API to generate and represent UIDs", + "homepage": "https://symfony.com", + "keywords": [ + "UID", + "ulid", + "uuid" + ], + "support": { + "source": "https://github.com/symfony/uid/tree/v6.4.3" + }, + "funding": [ + { + "url": "https://symfony.com/sponsor", + "type": "custom" + }, + { + "url": "https://github.com/fabpot", + "type": "github" + }, + { + "url": "https://tidelift.com/funding/github/packagist/symfony/symfony", + "type": "tidelift" + } + ], + "time": "2024-01-23T14:51:35+00:00" + }, + { + "name": "wapmorgan/mp3info", + "version": "0.1.1", + "source": { + "type": "git", + "url": "https://github.com/wapmorgan/Mp3Info.git", + "reference": "e532c2e1997874f9c672c7810ce90ef15004ef94" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/wapmorgan/Mp3Info/zipball/e532c2e1997874f9c672c7810ce90ef15004ef94", + "reference": "e532c2e1997874f9c672c7810ce90ef15004ef94", + "shasum": "" + }, + "require": { + "ext-mbstring": "*", + "php": ">=5.4.0" + }, + "bin": [ + "bin/mp3scan" + ], + "type": "library", + "autoload": { + "psr-4": { + "wapmorgan\\Mp3Info\\": "src/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "LGPL-3.0" + ], + "description": "The fastest php library to extract mp3 tags & meta information.", + "keywords": [ + "audio", + "id3", + "id3v1", + "id3v2", + "mp3", + "mpeg" + ], + "support": { + "issues": "https://github.com/wapmorgan/Mp3Info/issues", + "source": "https://github.com/wapmorgan/Mp3Info/tree/0.1.1" + }, + "time": "2025-04-01T20:51:11+00:00" + }, + { + "name": "web-auth/cose-lib", + "version": "4.3.0", + "source": { + "type": "git", + "url": "https://github.com/web-auth/cose-lib.git", + "reference": "e5c417b3b90e06c84638a18d350e438d760cb955" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/web-auth/cose-lib/zipball/e5c417b3b90e06c84638a18d350e438d760cb955", + "reference": "e5c417b3b90e06c84638a18d350e438d760cb955", + "shasum": "" + }, + "require": { + "brick/math": "^0.9|^0.10|^0.11|^0.12", + "ext-json": "*", + "ext-mbstring": "*", + "ext-openssl": "*", + "php": ">=8.1", + "spomky-labs/pki-framework": "^1.0" + }, + "require-dev": { + "ekino/phpstan-banned-code": "^1.0", + "infection/infection": "^0.27", + "php-parallel-lint/php-parallel-lint": "^1.3", + "phpstan/extension-installer": "^1.3", + "phpstan/phpstan": "^1.7", + "phpstan/phpstan-deprecation-rules": "^1.0", + "phpstan/phpstan-phpunit": "^1.1", + "phpstan/phpstan-strict-rules": "^1.2", + "phpunit/phpunit": "^10.1", + "qossmic/deptrac-shim": "^1.0", + "rector/rector": "^0.19", + "symfony/phpunit-bridge": "^6.4|^7.0", + "symplify/easy-coding-standard": "^12.0" + }, + "suggest": { + "ext-bcmath": "For better performance, please install either GMP (recommended) or BCMath extension", + "ext-gmp": "For better performance, please install either GMP (recommended) or BCMath extension" + }, + "type": "library", + "autoload": { + "psr-4": { + "Cose\\": "src/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Florent Morselli", + "homepage": "https://github.com/Spomky" + }, + { + "name": "All contributors", + "homepage": "https://github.com/web-auth/cose/contributors" + } + ], + "description": "CBOR Object Signing and Encryption (COSE) For PHP", + "homepage": "https://github.com/web-auth", + "keywords": [ + "COSE", + "RFC8152" + ], + "support": { + "issues": "https://github.com/web-auth/cose-lib/issues", + "source": "https://github.com/web-auth/cose-lib/tree/4.3.0" + }, + "funding": [ + { + "url": "https://github.com/Spomky", + "type": "github" + }, + { + "url": "https://www.patreon.com/FlorentMorselli", + "type": "patreon" + } + ], + "time": "2024-02-05T21:00:39+00:00" + }, + { + "name": "web-auth/webauthn-lib", + "version": "4.9.1", + "source": { + "type": "git", + "url": "https://github.com/web-auth/webauthn-lib.git", + "reference": "fd7a0943c663b325e92ad562c2bcc943e77beeac" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/web-auth/webauthn-lib/zipball/fd7a0943c663b325e92ad562c2bcc943e77beeac", + "reference": "fd7a0943c663b325e92ad562c2bcc943e77beeac", + "shasum": "" + }, + "require": { + "ext-json": "*", + "ext-mbstring": "*", + "ext-openssl": "*", + "lcobucci/clock": "^2.2|^3.0", + "paragonie/constant_time_encoding": "^2.6|^3.0", + "php": ">=8.1", + "psr/clock": "^1.0", + "psr/event-dispatcher": "^1.0", + "psr/http-client": "^1.0", + "psr/http-factory": "^1.0", + "psr/log": "^1.0|^2.0|^3.0", + "spomky-labs/cbor-php": "^3.0", + "spomky-labs/pki-framework": "^1.0", + "symfony/deprecation-contracts": "^3.2", + "symfony/uid": "^6.1|^7.0", + "web-auth/cose-lib": "^4.2.3" + }, + "suggest": { + "phpdocumentor/reflection-docblock": "As of 4.5.x, the phpdocumentor/reflection-docblock component will become mandatory for converting objects such as the Metadata Statement", + "psr/clock-implementation": "As of 4.5.x, the PSR Clock implementation will replace lcobucci/clock", + "psr/log-implementation": "Recommended to receive logs from the library", + "symfony/event-dispatcher": "Recommended to use dispatched events", + "symfony/property-access": "As of 4.5.x, the symfony/serializer component will become mandatory for converting objects such as the Metadata Statement", + "symfony/property-info": "As of 4.5.x, the symfony/serializer component will become mandatory for converting objects such as the Metadata Statement", + "symfony/serializer": "As of 4.5.x, the symfony/serializer component will become mandatory for converting objects such as the Metadata Statement", + "web-token/jwt-library": "Mandatory for fetching Metadata Statement from distant sources" + }, + "type": "library", + "extra": { + "thanks": { + "name": "web-auth/webauthn-framework", + "url": "https://github.com/web-auth/webauthn-framework" + } + }, + "autoload": { + "psr-4": { + "Webauthn\\": "src/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Florent Morselli", + "homepage": "https://github.com/Spomky" + }, + { + "name": "All contributors", + "homepage": "https://github.com/web-auth/webauthn-library/contributors" + } + ], + "description": "FIDO2/Webauthn Support For PHP", + "homepage": "https://github.com/web-auth", + "keywords": [ + "FIDO2", + "fido", + "webauthn" + ], + "support": { + "source": "https://github.com/web-auth/webauthn-lib/tree/4.9.1" + }, + "funding": [ + { + "url": "https://github.com/Spomky", + "type": "github" + }, + { + "url": "https://www.patreon.com/FlorentMorselli", + "type": "patreon" + } + ], + "time": "2024-07-16T18:36:36+00:00" + } + ], + "packages-dev": [], + "aliases": [], + "minimum-stability": "stable", + "stability-flags": {}, + "prefer-stable": false, + "prefer-lowest": false, + "platform": { + "php": "^8.1", + "ext-ctype": "*", + "ext-mbstring": "*" + }, + "platform-dev": {}, + "platform-overrides": { + "php": "8.1.0" + }, + "plugin-api-version": "2.6.0" +} diff --git a/3rdparty/composer.patches.json b/3rdparty/composer.patches.json new file mode 100644 index 00000000..18dbe496 --- /dev/null +++ b/3rdparty/composer.patches.json @@ -0,0 +1,8 @@ +{ + "patches": { + "wapmorgan/mp3info": { + "Break frame parsing on invalid frame": ".patches/mp3info-break-frame-parsing.patch", + "fix incorrect lookup for mpeg header": ".patches/mp3info-fix-incorrect-lookup-for-mpeg-header.patch" + } + } +} diff --git a/3rdparty/composer/ClassLoader.php b/3rdparty/composer/ClassLoader.php new file mode 100644 index 00000000..7824d8f7 --- /dev/null +++ b/3rdparty/composer/ClassLoader.php @@ -0,0 +1,579 @@ + + * Jordi Boggiano + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Composer\Autoload; + +/** + * ClassLoader implements a PSR-0, PSR-4 and classmap class loader. + * + * $loader = new \Composer\Autoload\ClassLoader(); + * + * // register classes with namespaces + * $loader->add('Symfony\Component', __DIR__.'/component'); + * $loader->add('Symfony', __DIR__.'/framework'); + * + * // activate the autoloader + * $loader->register(); + * + * // to enable searching the include path (eg. for PEAR packages) + * $loader->setUseIncludePath(true); + * + * In this example, if you try to use a class in the Symfony\Component + * namespace or one of its children (Symfony\Component\Console for instance), + * the autoloader will first look for the class under the component/ + * directory, and it will then fallback to the framework/ directory if not + * found before giving up. + * + * This class is loosely based on the Symfony UniversalClassLoader. + * + * @author Fabien Potencier + * @author Jordi Boggiano + * @see https://www.php-fig.org/psr/psr-0/ + * @see https://www.php-fig.org/psr/psr-4/ + */ +class ClassLoader +{ + /** @var \Closure(string):void */ + private static $includeFile; + + /** @var string|null */ + private $vendorDir; + + // PSR-4 + /** + * @var array> + */ + private $prefixLengthsPsr4 = array(); + /** + * @var array> + */ + private $prefixDirsPsr4 = array(); + /** + * @var list + */ + private $fallbackDirsPsr4 = array(); + + // PSR-0 + /** + * List of PSR-0 prefixes + * + * Structured as array('F (first letter)' => array('Foo\Bar (full prefix)' => array('path', 'path2'))) + * + * @var array>> + */ + private $prefixesPsr0 = array(); + /** + * @var list + */ + private $fallbackDirsPsr0 = array(); + + /** @var bool */ + private $useIncludePath = false; + + /** + * @var array + */ + private $classMap = array(); + + /** @var bool */ + private $classMapAuthoritative = false; + + /** + * @var array + */ + private $missingClasses = array(); + + /** @var string|null */ + private $apcuPrefix; + + /** + * @var array + */ + private static $registeredLoaders = array(); + + /** + * @param string|null $vendorDir + */ + public function __construct($vendorDir = null) + { + $this->vendorDir = $vendorDir; + self::initializeIncludeClosure(); + } + + /** + * @return array> + */ + public function getPrefixes() + { + if (!empty($this->prefixesPsr0)) { + return call_user_func_array('array_merge', array_values($this->prefixesPsr0)); + } + + return array(); + } + + /** + * @return array> + */ + public function getPrefixesPsr4() + { + return $this->prefixDirsPsr4; + } + + /** + * @return list + */ + public function getFallbackDirs() + { + return $this->fallbackDirsPsr0; + } + + /** + * @return list + */ + public function getFallbackDirsPsr4() + { + return $this->fallbackDirsPsr4; + } + + /** + * @return array Array of classname => path + */ + public function getClassMap() + { + return $this->classMap; + } + + /** + * @param array $classMap Class to filename map + * + * @return void + */ + public function addClassMap(array $classMap) + { + if ($this->classMap) { + $this->classMap = array_merge($this->classMap, $classMap); + } else { + $this->classMap = $classMap; + } + } + + /** + * Registers a set of PSR-0 directories for a given prefix, either + * appending or prepending to the ones previously set for this prefix. + * + * @param string $prefix The prefix + * @param list|string $paths The PSR-0 root directories + * @param bool $prepend Whether to prepend the directories + * + * @return void + */ + public function add($prefix, $paths, $prepend = false) + { + $paths = (array) $paths; + if (!$prefix) { + if ($prepend) { + $this->fallbackDirsPsr0 = array_merge( + $paths, + $this->fallbackDirsPsr0 + ); + } else { + $this->fallbackDirsPsr0 = array_merge( + $this->fallbackDirsPsr0, + $paths + ); + } + + return; + } + + $first = $prefix[0]; + if (!isset($this->prefixesPsr0[$first][$prefix])) { + $this->prefixesPsr0[$first][$prefix] = $paths; + + return; + } + if ($prepend) { + $this->prefixesPsr0[$first][$prefix] = array_merge( + $paths, + $this->prefixesPsr0[$first][$prefix] + ); + } else { + $this->prefixesPsr0[$first][$prefix] = array_merge( + $this->prefixesPsr0[$first][$prefix], + $paths + ); + } + } + + /** + * Registers a set of PSR-4 directories for a given namespace, either + * appending or prepending to the ones previously set for this namespace. + * + * @param string $prefix The prefix/namespace, with trailing '\\' + * @param list|string $paths The PSR-4 base directories + * @param bool $prepend Whether to prepend the directories + * + * @throws \InvalidArgumentException + * + * @return void + */ + public function addPsr4($prefix, $paths, $prepend = false) + { + $paths = (array) $paths; + if (!$prefix) { + // Register directories for the root namespace. + if ($prepend) { + $this->fallbackDirsPsr4 = array_merge( + $paths, + $this->fallbackDirsPsr4 + ); + } else { + $this->fallbackDirsPsr4 = array_merge( + $this->fallbackDirsPsr4, + $paths + ); + } + } elseif (!isset($this->prefixDirsPsr4[$prefix])) { + // Register directories for a new namespace. + $length = strlen($prefix); + if ('\\' !== $prefix[$length - 1]) { + throw new \InvalidArgumentException("A non-empty PSR-4 prefix must end with a namespace separator."); + } + $this->prefixLengthsPsr4[$prefix[0]][$prefix] = $length; + $this->prefixDirsPsr4[$prefix] = $paths; + } elseif ($prepend) { + // Prepend directories for an already registered namespace. + $this->prefixDirsPsr4[$prefix] = array_merge( + $paths, + $this->prefixDirsPsr4[$prefix] + ); + } else { + // Append directories for an already registered namespace. + $this->prefixDirsPsr4[$prefix] = array_merge( + $this->prefixDirsPsr4[$prefix], + $paths + ); + } + } + + /** + * Registers a set of PSR-0 directories for a given prefix, + * replacing any others previously set for this prefix. + * + * @param string $prefix The prefix + * @param list|string $paths The PSR-0 base directories + * + * @return void + */ + public function set($prefix, $paths) + { + if (!$prefix) { + $this->fallbackDirsPsr0 = (array) $paths; + } else { + $this->prefixesPsr0[$prefix[0]][$prefix] = (array) $paths; + } + } + + /** + * Registers a set of PSR-4 directories for a given namespace, + * replacing any others previously set for this namespace. + * + * @param string $prefix The prefix/namespace, with trailing '\\' + * @param list|string $paths The PSR-4 base directories + * + * @throws \InvalidArgumentException + * + * @return void + */ + public function setPsr4($prefix, $paths) + { + if (!$prefix) { + $this->fallbackDirsPsr4 = (array) $paths; + } else { + $length = strlen($prefix); + if ('\\' !== $prefix[$length - 1]) { + throw new \InvalidArgumentException("A non-empty PSR-4 prefix must end with a namespace separator."); + } + $this->prefixLengthsPsr4[$prefix[0]][$prefix] = $length; + $this->prefixDirsPsr4[$prefix] = (array) $paths; + } + } + + /** + * Turns on searching the include path for class files. + * + * @param bool $useIncludePath + * + * @return void + */ + public function setUseIncludePath($useIncludePath) + { + $this->useIncludePath = $useIncludePath; + } + + /** + * Can be used to check if the autoloader uses the include path to check + * for classes. + * + * @return bool + */ + public function getUseIncludePath() + { + return $this->useIncludePath; + } + + /** + * Turns off searching the prefix and fallback directories for classes + * that have not been registered with the class map. + * + * @param bool $classMapAuthoritative + * + * @return void + */ + public function setClassMapAuthoritative($classMapAuthoritative) + { + $this->classMapAuthoritative = $classMapAuthoritative; + } + + /** + * Should class lookup fail if not found in the current class map? + * + * @return bool + */ + public function isClassMapAuthoritative() + { + return $this->classMapAuthoritative; + } + + /** + * APCu prefix to use to cache found/not-found classes, if the extension is enabled. + * + * @param string|null $apcuPrefix + * + * @return void + */ + public function setApcuPrefix($apcuPrefix) + { + $this->apcuPrefix = function_exists('apcu_fetch') && filter_var(ini_get('apc.enabled'), FILTER_VALIDATE_BOOLEAN) ? $apcuPrefix : null; + } + + /** + * The APCu prefix in use, or null if APCu caching is not enabled. + * + * @return string|null + */ + public function getApcuPrefix() + { + return $this->apcuPrefix; + } + + /** + * Registers this instance as an autoloader. + * + * @param bool $prepend Whether to prepend the autoloader or not + * + * @return void + */ + public function register($prepend = false) + { + spl_autoload_register(array($this, 'loadClass'), true, $prepend); + + if (null === $this->vendorDir) { + return; + } + + if ($prepend) { + self::$registeredLoaders = array($this->vendorDir => $this) + self::$registeredLoaders; + } else { + unset(self::$registeredLoaders[$this->vendorDir]); + self::$registeredLoaders[$this->vendorDir] = $this; + } + } + + /** + * Unregisters this instance as an autoloader. + * + * @return void + */ + public function unregister() + { + spl_autoload_unregister(array($this, 'loadClass')); + + if (null !== $this->vendorDir) { + unset(self::$registeredLoaders[$this->vendorDir]); + } + } + + /** + * Loads the given class or interface. + * + * @param string $class The name of the class + * @return true|null True if loaded, null otherwise + */ + public function loadClass($class) + { + if ($file = $this->findFile($class)) { + $includeFile = self::$includeFile; + $includeFile($file); + + return true; + } + + return null; + } + + /** + * Finds the path to the file where the class is defined. + * + * @param string $class The name of the class + * + * @return string|false The path if found, false otherwise + */ + public function findFile($class) + { + // class map lookup + if (isset($this->classMap[$class])) { + return $this->classMap[$class]; + } + if ($this->classMapAuthoritative || isset($this->missingClasses[$class])) { + return false; + } + if (null !== $this->apcuPrefix) { + $file = apcu_fetch($this->apcuPrefix.$class, $hit); + if ($hit) { + return $file; + } + } + + $file = $this->findFileWithExtension($class, '.php'); + + // Search for Hack files if we are running on HHVM + if (false === $file && defined('HHVM_VERSION')) { + $file = $this->findFileWithExtension($class, '.hh'); + } + + if (null !== $this->apcuPrefix) { + apcu_add($this->apcuPrefix.$class, $file); + } + + if (false === $file) { + // Remember that this class does not exist. + $this->missingClasses[$class] = true; + } + + return $file; + } + + /** + * Returns the currently registered loaders keyed by their corresponding vendor directories. + * + * @return array + */ + public static function getRegisteredLoaders() + { + return self::$registeredLoaders; + } + + /** + * @param string $class + * @param string $ext + * @return string|false + */ + private function findFileWithExtension($class, $ext) + { + // PSR-4 lookup + $logicalPathPsr4 = strtr($class, '\\', DIRECTORY_SEPARATOR) . $ext; + + $first = $class[0]; + if (isset($this->prefixLengthsPsr4[$first])) { + $subPath = $class; + while (false !== $lastPos = strrpos($subPath, '\\')) { + $subPath = substr($subPath, 0, $lastPos); + $search = $subPath . '\\'; + if (isset($this->prefixDirsPsr4[$search])) { + $pathEnd = DIRECTORY_SEPARATOR . substr($logicalPathPsr4, $lastPos + 1); + foreach ($this->prefixDirsPsr4[$search] as $dir) { + if (file_exists($file = $dir . $pathEnd)) { + return $file; + } + } + } + } + } + + // PSR-4 fallback dirs + foreach ($this->fallbackDirsPsr4 as $dir) { + if (file_exists($file = $dir . DIRECTORY_SEPARATOR . $logicalPathPsr4)) { + return $file; + } + } + + // PSR-0 lookup + if (false !== $pos = strrpos($class, '\\')) { + // namespaced class name + $logicalPathPsr0 = substr($logicalPathPsr4, 0, $pos + 1) + . strtr(substr($logicalPathPsr4, $pos + 1), '_', DIRECTORY_SEPARATOR); + } else { + // PEAR-like class name + $logicalPathPsr0 = strtr($class, '_', DIRECTORY_SEPARATOR) . $ext; + } + + if (isset($this->prefixesPsr0[$first])) { + foreach ($this->prefixesPsr0[$first] as $prefix => $dirs) { + if (0 === strpos($class, $prefix)) { + foreach ($dirs as $dir) { + if (file_exists($file = $dir . DIRECTORY_SEPARATOR . $logicalPathPsr0)) { + return $file; + } + } + } + } + } + + // PSR-0 fallback dirs + foreach ($this->fallbackDirsPsr0 as $dir) { + if (file_exists($file = $dir . DIRECTORY_SEPARATOR . $logicalPathPsr0)) { + return $file; + } + } + + // PSR-0 include paths. + if ($this->useIncludePath && $file = stream_resolve_include_path($logicalPathPsr0)) { + return $file; + } + + return false; + } + + /** + * @return void + */ + private static function initializeIncludeClosure() + { + if (self::$includeFile !== null) { + return; + } + + /** + * Scope isolated include. + * + * Prevents access to $this/self from included files. + * + * @param string $file + * @return void + */ + self::$includeFile = \Closure::bind(static function($file) { + include $file; + }, null, null); + } +} diff --git a/3rdparty/composer/InstalledVersions.php b/3rdparty/composer/InstalledVersions.php new file mode 100644 index 00000000..2052022f --- /dev/null +++ b/3rdparty/composer/InstalledVersions.php @@ -0,0 +1,396 @@ + + * Jordi Boggiano + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Composer; + +use Composer\Autoload\ClassLoader; +use Composer\Semver\VersionParser; + +/** + * This class is copied in every Composer installed project and available to all + * + * See also https://getcomposer.org/doc/07-runtime.md#installed-versions + * + * To require its presence, you can require `composer-runtime-api ^2.0` + * + * @final + */ +class InstalledVersions +{ + /** + * @var string|null if set (by reflection by Composer), this should be set to the path where this class is being copied to + * @internal + */ + private static $selfDir = null; + + /** + * @var mixed[]|null + * @psalm-var array{root: array{name: string, pretty_version: string, version: string, reference: string|null, type: string, install_path: string, aliases: string[], dev: bool}, versions: array}|array{}|null + */ + private static $installed; + + /** + * @var bool + */ + private static $installedIsLocalDir; + + /** + * @var bool|null + */ + private static $canGetVendors; + + /** + * @var array[] + * @psalm-var array}> + */ + private static $installedByVendor = array(); + + /** + * Returns a list of all package names which are present, either by being installed, replaced or provided + * + * @return string[] + * @psalm-return list + */ + public static function getInstalledPackages() + { + $packages = array(); + foreach (self::getInstalled() as $installed) { + $packages[] = array_keys($installed['versions']); + } + + if (1 === \count($packages)) { + return $packages[0]; + } + + return array_keys(array_flip(\call_user_func_array('array_merge', $packages))); + } + + /** + * Returns a list of all package names with a specific type e.g. 'library' + * + * @param string $type + * @return string[] + * @psalm-return list + */ + public static function getInstalledPackagesByType($type) + { + $packagesByType = array(); + + foreach (self::getInstalled() as $installed) { + foreach ($installed['versions'] as $name => $package) { + if (isset($package['type']) && $package['type'] === $type) { + $packagesByType[] = $name; + } + } + } + + return $packagesByType; + } + + /** + * Checks whether the given package is installed + * + * This also returns true if the package name is provided or replaced by another package + * + * @param string $packageName + * @param bool $includeDevRequirements + * @return bool + */ + public static function isInstalled($packageName, $includeDevRequirements = true) + { + foreach (self::getInstalled() as $installed) { + if (isset($installed['versions'][$packageName])) { + return $includeDevRequirements || !isset($installed['versions'][$packageName]['dev_requirement']) || $installed['versions'][$packageName]['dev_requirement'] === false; + } + } + + return false; + } + + /** + * Checks whether the given package satisfies a version constraint + * + * e.g. If you want to know whether version 2.3+ of package foo/bar is installed, you would call: + * + * Composer\InstalledVersions::satisfies(new VersionParser, 'foo/bar', '^2.3') + * + * @param VersionParser $parser Install composer/semver to have access to this class and functionality + * @param string $packageName + * @param string|null $constraint A version constraint to check for, if you pass one you have to make sure composer/semver is required by your package + * @return bool + */ + public static function satisfies(VersionParser $parser, $packageName, $constraint) + { + $constraint = $parser->parseConstraints((string) $constraint); + $provided = $parser->parseConstraints(self::getVersionRanges($packageName)); + + return $provided->matches($constraint); + } + + /** + * Returns a version constraint representing all the range(s) which are installed for a given package + * + * It is easier to use this via isInstalled() with the $constraint argument if you need to check + * whether a given version of a package is installed, and not just whether it exists + * + * @param string $packageName + * @return string Version constraint usable with composer/semver + */ + public static function getVersionRanges($packageName) + { + foreach (self::getInstalled() as $installed) { + if (!isset($installed['versions'][$packageName])) { + continue; + } + + $ranges = array(); + if (isset($installed['versions'][$packageName]['pretty_version'])) { + $ranges[] = $installed['versions'][$packageName]['pretty_version']; + } + if (array_key_exists('aliases', $installed['versions'][$packageName])) { + $ranges = array_merge($ranges, $installed['versions'][$packageName]['aliases']); + } + if (array_key_exists('replaced', $installed['versions'][$packageName])) { + $ranges = array_merge($ranges, $installed['versions'][$packageName]['replaced']); + } + if (array_key_exists('provided', $installed['versions'][$packageName])) { + $ranges = array_merge($ranges, $installed['versions'][$packageName]['provided']); + } + + return implode(' || ', $ranges); + } + + throw new \OutOfBoundsException('Package "' . $packageName . '" is not installed'); + } + + /** + * @param string $packageName + * @return string|null If the package is being replaced or provided but is not really installed, null will be returned as version, use satisfies or getVersionRanges if you need to know if a given version is present + */ + public static function getVersion($packageName) + { + foreach (self::getInstalled() as $installed) { + if (!isset($installed['versions'][$packageName])) { + continue; + } + + if (!isset($installed['versions'][$packageName]['version'])) { + return null; + } + + return $installed['versions'][$packageName]['version']; + } + + throw new \OutOfBoundsException('Package "' . $packageName . '" is not installed'); + } + + /** + * @param string $packageName + * @return string|null If the package is being replaced or provided but is not really installed, null will be returned as version, use satisfies or getVersionRanges if you need to know if a given version is present + */ + public static function getPrettyVersion($packageName) + { + foreach (self::getInstalled() as $installed) { + if (!isset($installed['versions'][$packageName])) { + continue; + } + + if (!isset($installed['versions'][$packageName]['pretty_version'])) { + return null; + } + + return $installed['versions'][$packageName]['pretty_version']; + } + + throw new \OutOfBoundsException('Package "' . $packageName . '" is not installed'); + } + + /** + * @param string $packageName + * @return string|null If the package is being replaced or provided but is not really installed, null will be returned as reference + */ + public static function getReference($packageName) + { + foreach (self::getInstalled() as $installed) { + if (!isset($installed['versions'][$packageName])) { + continue; + } + + if (!isset($installed['versions'][$packageName]['reference'])) { + return null; + } + + return $installed['versions'][$packageName]['reference']; + } + + throw new \OutOfBoundsException('Package "' . $packageName . '" is not installed'); + } + + /** + * @param string $packageName + * @return string|null If the package is being replaced or provided but is not really installed, null will be returned as install path. Packages of type metapackages also have a null install path. + */ + public static function getInstallPath($packageName) + { + foreach (self::getInstalled() as $installed) { + if (!isset($installed['versions'][$packageName])) { + continue; + } + + return isset($installed['versions'][$packageName]['install_path']) ? $installed['versions'][$packageName]['install_path'] : null; + } + + throw new \OutOfBoundsException('Package "' . $packageName . '" is not installed'); + } + + /** + * @return array + * @psalm-return array{name: string, pretty_version: string, version: string, reference: string|null, type: string, install_path: string, aliases: string[], dev: bool} + */ + public static function getRootPackage() + { + $installed = self::getInstalled(); + + return $installed[0]['root']; + } + + /** + * Returns the raw installed.php data for custom implementations + * + * @deprecated Use getAllRawData() instead which returns all datasets for all autoloaders present in the process. getRawData only returns the first dataset loaded, which may not be what you expect. + * @return array[] + * @psalm-return array{root: array{name: string, pretty_version: string, version: string, reference: string|null, type: string, install_path: string, aliases: string[], dev: bool}, versions: array} + */ + public static function getRawData() + { + @trigger_error('getRawData only returns the first dataset loaded, which may not be what you expect. Use getAllRawData() instead which returns all datasets for all autoloaders present in the process.', E_USER_DEPRECATED); + + if (null === self::$installed) { + // only require the installed.php file if this file is loaded from its dumped location, + // and not from its source location in the composer/composer package, see https://github.com/composer/composer/issues/9937 + if (substr(__DIR__, -8, 1) !== 'C') { + self::$installed = include __DIR__ . '/installed.php'; + } else { + self::$installed = array(); + } + } + + return self::$installed; + } + + /** + * Returns the raw data of all installed.php which are currently loaded for custom implementations + * + * @return array[] + * @psalm-return list}> + */ + public static function getAllRawData() + { + return self::getInstalled(); + } + + /** + * Lets you reload the static array from another file + * + * This is only useful for complex integrations in which a project needs to use + * this class but then also needs to execute another project's autoloader in process, + * and wants to ensure both projects have access to their version of installed.php. + * + * A typical case would be PHPUnit, where it would need to make sure it reads all + * the data it needs from this class, then call reload() with + * `require $CWD/vendor/composer/installed.php` (or similar) as input to make sure + * the project in which it runs can then also use this class safely, without + * interference between PHPUnit's dependencies and the project's dependencies. + * + * @param array[] $data A vendor/composer/installed.php data set + * @return void + * + * @psalm-param array{root: array{name: string, pretty_version: string, version: string, reference: string|null, type: string, install_path: string, aliases: string[], dev: bool}, versions: array} $data + */ + public static function reload($data) + { + self::$installed = $data; + self::$installedByVendor = array(); + + // when using reload, we disable the duplicate protection to ensure that self::$installed data is + // always returned, but we cannot know whether it comes from the installed.php in __DIR__ or not, + // so we have to assume it does not, and that may result in duplicate data being returned when listing + // all installed packages for example + self::$installedIsLocalDir = false; + } + + /** + * @return string + */ + private static function getSelfDir() + { + if (self::$selfDir === null) { + self::$selfDir = strtr(__DIR__, '\\', '/'); + } + + return self::$selfDir; + } + + /** + * @return array[] + * @psalm-return list}> + */ + private static function getInstalled() + { + if (null === self::$canGetVendors) { + self::$canGetVendors = method_exists('Composer\Autoload\ClassLoader', 'getRegisteredLoaders'); + } + + $installed = array(); + $copiedLocalDir = false; + + if (self::$canGetVendors) { + $selfDir = self::getSelfDir(); + foreach (ClassLoader::getRegisteredLoaders() as $vendorDir => $loader) { + $vendorDir = strtr($vendorDir, '\\', '/'); + if (isset(self::$installedByVendor[$vendorDir])) { + $installed[] = self::$installedByVendor[$vendorDir]; + } elseif (is_file($vendorDir.'/composer/installed.php')) { + /** @var array{root: array{name: string, pretty_version: string, version: string, reference: string|null, type: string, install_path: string, aliases: string[], dev: bool}, versions: array} $required */ + $required = require $vendorDir.'/composer/installed.php'; + self::$installedByVendor[$vendorDir] = $required; + $installed[] = $required; + if (self::$installed === null && $vendorDir.'/composer' === $selfDir) { + self::$installed = $required; + self::$installedIsLocalDir = true; + } + } + if (self::$installedIsLocalDir && $vendorDir.'/composer' === $selfDir) { + $copiedLocalDir = true; + } + } + } + + if (null === self::$installed) { + // only require the installed.php file if this file is loaded from its dumped location, + // and not from its source location in the composer/composer package, see https://github.com/composer/composer/issues/9937 + if (substr(__DIR__, -8, 1) !== 'C') { + /** @var array{root: array{name: string, pretty_version: string, version: string, reference: string|null, type: string, install_path: string, aliases: string[], dev: bool}, versions: array} $required */ + $required = require __DIR__ . '/installed.php'; + self::$installed = $required; + } else { + self::$installed = array(); + } + } + + if (self::$installed !== array() && !$copiedLocalDir) { + $installed[] = self::$installed; + } + + return $installed; + } +} diff --git a/3rdparty/composer/LICENSE b/3rdparty/composer/LICENSE new file mode 100644 index 00000000..f27399a0 --- /dev/null +++ b/3rdparty/composer/LICENSE @@ -0,0 +1,21 @@ + +Copyright (c) Nils Adermann, Jordi Boggiano + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is furnished +to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +THE SOFTWARE. + diff --git a/3rdparty/composer/autoload_classmap.php b/3rdparty/composer/autoload_classmap.php new file mode 100644 index 00000000..1c73f1a2 --- /dev/null +++ b/3rdparty/composer/autoload_classmap.php @@ -0,0 +1,3656 @@ + $vendorDir . '/aws/aws-crt-php/src/AWS/CRT/Auth/AwsCredentials.php', + 'AWS\\CRT\\Auth\\CredentialsProvider' => $vendorDir . '/aws/aws-crt-php/src/AWS/CRT/Auth/CredentialsProvider.php', + 'AWS\\CRT\\Auth\\Signable' => $vendorDir . '/aws/aws-crt-php/src/AWS/CRT/Auth/Signable.php', + 'AWS\\CRT\\Auth\\SignatureType' => $vendorDir . '/aws/aws-crt-php/src/AWS/CRT/Auth/SignatureType.php', + 'AWS\\CRT\\Auth\\SignedBodyHeaderType' => $vendorDir . '/aws/aws-crt-php/src/AWS/CRT/Auth/SignedBodyHeaderType.php', + 'AWS\\CRT\\Auth\\Signing' => $vendorDir . '/aws/aws-crt-php/src/AWS/CRT/Auth/Signing.php', + 'AWS\\CRT\\Auth\\SigningAlgorithm' => $vendorDir . '/aws/aws-crt-php/src/AWS/CRT/Auth/SigningAlgorithm.php', + 'AWS\\CRT\\Auth\\SigningConfigAWS' => $vendorDir . '/aws/aws-crt-php/src/AWS/CRT/Auth/SigningConfigAWS.php', + 'AWS\\CRT\\Auth\\SigningResult' => $vendorDir . '/aws/aws-crt-php/src/AWS/CRT/Auth/SigningResult.php', + 'AWS\\CRT\\Auth\\StaticCredentialsProvider' => $vendorDir . '/aws/aws-crt-php/src/AWS/CRT/Auth/StaticCredentialsProvider.php', + 'AWS\\CRT\\CRT' => $vendorDir . '/aws/aws-crt-php/src/AWS/CRT/CRT.php', + 'AWS\\CRT\\HTTP\\Headers' => $vendorDir . '/aws/aws-crt-php/src/AWS/CRT/HTTP/Headers.php', + 'AWS\\CRT\\HTTP\\Message' => $vendorDir . '/aws/aws-crt-php/src/AWS/CRT/HTTP/Message.php', + 'AWS\\CRT\\HTTP\\Request' => $vendorDir . '/aws/aws-crt-php/src/AWS/CRT/HTTP/Request.php', + 'AWS\\CRT\\HTTP\\Response' => $vendorDir . '/aws/aws-crt-php/src/AWS/CRT/HTTP/Response.php', + 'AWS\\CRT\\IO\\EventLoopGroup' => $vendorDir . '/aws/aws-crt-php/src/AWS/CRT/IO/EventLoopGroup.php', + 'AWS\\CRT\\IO\\InputStream' => $vendorDir . '/aws/aws-crt-php/src/AWS/CRT/IO/InputStream.php', + 'AWS\\CRT\\Internal\\Encoding' => $vendorDir . '/aws/aws-crt-php/src/AWS/CRT/Internal/Encoding.php', + 'AWS\\CRT\\Internal\\Extension' => $vendorDir . '/aws/aws-crt-php/src/AWS/CRT/Internal/Extension.php', + 'AWS\\CRT\\Log' => $vendorDir . '/aws/aws-crt-php/src/AWS/CRT/Log.php', + 'AWS\\CRT\\NativeResource' => $vendorDir . '/aws/aws-crt-php/src/AWS/CRT/NativeResource.php', + 'AWS\\CRT\\OptionValue' => $vendorDir . '/aws/aws-crt-php/src/AWS/CRT/Options.php', + 'AWS\\CRT\\Options' => $vendorDir . '/aws/aws-crt-php/src/AWS/CRT/Options.php', + 'AllowDynamicProperties' => $vendorDir . '/symfony/polyfill-php82/Resources/stubs/AllowDynamicProperties.php', + 'Archive_Tar' => $vendorDir . '/pear/archive_tar/Archive/Tar.php', + 'Aws\\AbstractConfigurationProvider' => $vendorDir . '/aws/aws-sdk-php/src/AbstractConfigurationProvider.php', + 'Aws\\Api\\AbstractModel' => $vendorDir . '/aws/aws-sdk-php/src/Api/AbstractModel.php', + 'Aws\\Api\\ApiProvider' => $vendorDir . '/aws/aws-sdk-php/src/Api/ApiProvider.php', + 'Aws\\Api\\DateTimeResult' => $vendorDir . '/aws/aws-sdk-php/src/Api/DateTimeResult.php', + 'Aws\\Api\\DocModel' => $vendorDir . '/aws/aws-sdk-php/src/Api/DocModel.php', + 'Aws\\Api\\ErrorParser\\AbstractErrorParser' => $vendorDir . '/aws/aws-sdk-php/src/Api/ErrorParser/AbstractErrorParser.php', + 'Aws\\Api\\ErrorParser\\JsonParserTrait' => $vendorDir . '/aws/aws-sdk-php/src/Api/ErrorParser/JsonParserTrait.php', + 'Aws\\Api\\ErrorParser\\JsonRpcErrorParser' => $vendorDir . '/aws/aws-sdk-php/src/Api/ErrorParser/JsonRpcErrorParser.php', + 'Aws\\Api\\ErrorParser\\RestJsonErrorParser' => $vendorDir . '/aws/aws-sdk-php/src/Api/ErrorParser/RestJsonErrorParser.php', + 'Aws\\Api\\ErrorParser\\XmlErrorParser' => $vendorDir . '/aws/aws-sdk-php/src/Api/ErrorParser/XmlErrorParser.php', + 'Aws\\Api\\ListShape' => $vendorDir . '/aws/aws-sdk-php/src/Api/ListShape.php', + 'Aws\\Api\\MapShape' => $vendorDir . '/aws/aws-sdk-php/src/Api/MapShape.php', + 'Aws\\Api\\Operation' => $vendorDir . '/aws/aws-sdk-php/src/Api/Operation.php', + 'Aws\\Api\\Parser\\AbstractParser' => $vendorDir . '/aws/aws-sdk-php/src/Api/Parser/AbstractParser.php', + 'Aws\\Api\\Parser\\AbstractRestParser' => $vendorDir . '/aws/aws-sdk-php/src/Api/Parser/AbstractRestParser.php', + 'Aws\\Api\\Parser\\Crc32ValidatingParser' => $vendorDir . '/aws/aws-sdk-php/src/Api/Parser/Crc32ValidatingParser.php', + 'Aws\\Api\\Parser\\DecodingEventStreamIterator' => $vendorDir . '/aws/aws-sdk-php/src/Api/Parser/DecodingEventStreamIterator.php', + 'Aws\\Api\\Parser\\EventParsingIterator' => $vendorDir . '/aws/aws-sdk-php/src/Api/Parser/EventParsingIterator.php', + 'Aws\\Api\\Parser\\Exception\\ParserException' => $vendorDir . '/aws/aws-sdk-php/src/Api/Parser/Exception/ParserException.php', + 'Aws\\Api\\Parser\\JsonParser' => $vendorDir . '/aws/aws-sdk-php/src/Api/Parser/JsonParser.php', + 'Aws\\Api\\Parser\\JsonRpcParser' => $vendorDir . '/aws/aws-sdk-php/src/Api/Parser/JsonRpcParser.php', + 'Aws\\Api\\Parser\\MetadataParserTrait' => $vendorDir . '/aws/aws-sdk-php/src/Api/Parser/MetadataParserTrait.php', + 'Aws\\Api\\Parser\\NonSeekableStreamDecodingEventStreamIterator' => $vendorDir . '/aws/aws-sdk-php/src/Api/Parser/NonSeekableStreamDecodingEventStreamIterator.php', + 'Aws\\Api\\Parser\\PayloadParserTrait' => $vendorDir . '/aws/aws-sdk-php/src/Api/Parser/PayloadParserTrait.php', + 'Aws\\Api\\Parser\\QueryParser' => $vendorDir . '/aws/aws-sdk-php/src/Api/Parser/QueryParser.php', + 'Aws\\Api\\Parser\\RestJsonParser' => $vendorDir . '/aws/aws-sdk-php/src/Api/Parser/RestJsonParser.php', + 'Aws\\Api\\Parser\\RestXmlParser' => $vendorDir . '/aws/aws-sdk-php/src/Api/Parser/RestXmlParser.php', + 'Aws\\Api\\Parser\\XmlParser' => $vendorDir . '/aws/aws-sdk-php/src/Api/Parser/XmlParser.php', + 'Aws\\Api\\Serializer\\Ec2ParamBuilder' => $vendorDir . '/aws/aws-sdk-php/src/Api/Serializer/Ec2ParamBuilder.php', + 'Aws\\Api\\Serializer\\JsonBody' => $vendorDir . '/aws/aws-sdk-php/src/Api/Serializer/JsonBody.php', + 'Aws\\Api\\Serializer\\JsonRpcSerializer' => $vendorDir . '/aws/aws-sdk-php/src/Api/Serializer/JsonRpcSerializer.php', + 'Aws\\Api\\Serializer\\QueryParamBuilder' => $vendorDir . '/aws/aws-sdk-php/src/Api/Serializer/QueryParamBuilder.php', + 'Aws\\Api\\Serializer\\QuerySerializer' => $vendorDir . '/aws/aws-sdk-php/src/Api/Serializer/QuerySerializer.php', + 'Aws\\Api\\Serializer\\RestJsonSerializer' => $vendorDir . '/aws/aws-sdk-php/src/Api/Serializer/RestJsonSerializer.php', + 'Aws\\Api\\Serializer\\RestSerializer' => $vendorDir . '/aws/aws-sdk-php/src/Api/Serializer/RestSerializer.php', + 'Aws\\Api\\Serializer\\RestXmlSerializer' => $vendorDir . '/aws/aws-sdk-php/src/Api/Serializer/RestXmlSerializer.php', + 'Aws\\Api\\Serializer\\XmlBody' => $vendorDir . '/aws/aws-sdk-php/src/Api/Serializer/XmlBody.php', + 'Aws\\Api\\Service' => $vendorDir . '/aws/aws-sdk-php/src/Api/Service.php', + 'Aws\\Api\\Shape' => $vendorDir . '/aws/aws-sdk-php/src/Api/Shape.php', + 'Aws\\Api\\ShapeMap' => $vendorDir . '/aws/aws-sdk-php/src/Api/ShapeMap.php', + 'Aws\\Api\\StructureShape' => $vendorDir . '/aws/aws-sdk-php/src/Api/StructureShape.php', + 'Aws\\Api\\SupportedProtocols' => $vendorDir . '/aws/aws-sdk-php/src/Api/SupportedProtocols.php', + 'Aws\\Api\\TimestampShape' => $vendorDir . '/aws/aws-sdk-php/src/Api/TimestampShape.php', + 'Aws\\Api\\Validator' => $vendorDir . '/aws/aws-sdk-php/src/Api/Validator.php', + 'Aws\\Arn\\AccessPointArn' => $vendorDir . '/aws/aws-sdk-php/src/Arn/AccessPointArn.php', + 'Aws\\Arn\\AccessPointArnInterface' => $vendorDir . '/aws/aws-sdk-php/src/Arn/AccessPointArnInterface.php', + 'Aws\\Arn\\Arn' => $vendorDir . '/aws/aws-sdk-php/src/Arn/Arn.php', + 'Aws\\Arn\\ArnInterface' => $vendorDir . '/aws/aws-sdk-php/src/Arn/ArnInterface.php', + 'Aws\\Arn\\ArnParser' => $vendorDir . '/aws/aws-sdk-php/src/Arn/ArnParser.php', + 'Aws\\Arn\\Exception\\InvalidArnException' => $vendorDir . '/aws/aws-sdk-php/src/Arn/Exception/InvalidArnException.php', + 'Aws\\Arn\\ObjectLambdaAccessPointArn' => $vendorDir . '/aws/aws-sdk-php/src/Arn/ObjectLambdaAccessPointArn.php', + 'Aws\\Arn\\ResourceTypeAndIdTrait' => $vendorDir . '/aws/aws-sdk-php/src/Arn/ResourceTypeAndIdTrait.php', + 'Aws\\Arn\\S3\\AccessPointArn' => $vendorDir . '/aws/aws-sdk-php/src/Arn/S3/AccessPointArn.php', + 'Aws\\Arn\\S3\\BucketArnInterface' => $vendorDir . '/aws/aws-sdk-php/src/Arn/S3/BucketArnInterface.php', + 'Aws\\Arn\\S3\\MultiRegionAccessPointArn' => $vendorDir . '/aws/aws-sdk-php/src/Arn/S3/MultiRegionAccessPointArn.php', + 'Aws\\Arn\\S3\\OutpostsAccessPointArn' => $vendorDir . '/aws/aws-sdk-php/src/Arn/S3/OutpostsAccessPointArn.php', + 'Aws\\Arn\\S3\\OutpostsArnInterface' => $vendorDir . '/aws/aws-sdk-php/src/Arn/S3/OutpostsArnInterface.php', + 'Aws\\Arn\\S3\\OutpostsBucketArn' => $vendorDir . '/aws/aws-sdk-php/src/Arn/S3/OutpostsBucketArn.php', + 'Aws\\Auth\\AuthSchemeResolver' => $vendorDir . '/aws/aws-sdk-php/src/Auth/AuthSchemeResolver.php', + 'Aws\\Auth\\AuthSchemeResolverInterface' => $vendorDir . '/aws/aws-sdk-php/src/Auth/AuthSchemeResolverInterface.php', + 'Aws\\Auth\\AuthSelectionMiddleware' => $vendorDir . '/aws/aws-sdk-php/src/Auth/AuthSelectionMiddleware.php', + 'Aws\\Auth\\Exception\\UnresolvedAuthSchemeException' => $vendorDir . '/aws/aws-sdk-php/src/Auth/Exception/UnresolvedAuthSchemeException.php', + 'Aws\\AwsClient' => $vendorDir . '/aws/aws-sdk-php/src/AwsClient.php', + 'Aws\\AwsClientInterface' => $vendorDir . '/aws/aws-sdk-php/src/AwsClientInterface.php', + 'Aws\\AwsClientTrait' => $vendorDir . '/aws/aws-sdk-php/src/AwsClientTrait.php', + 'Aws\\CacheInterface' => $vendorDir . '/aws/aws-sdk-php/src/CacheInterface.php', + 'Aws\\ClientResolver' => $vendorDir . '/aws/aws-sdk-php/src/ClientResolver.php', + 'Aws\\ClientSideMonitoring\\AbstractMonitoringMiddleware' => $vendorDir . '/aws/aws-sdk-php/src/ClientSideMonitoring/AbstractMonitoringMiddleware.php', + 'Aws\\ClientSideMonitoring\\ApiCallAttemptMonitoringMiddleware' => $vendorDir . '/aws/aws-sdk-php/src/ClientSideMonitoring/ApiCallAttemptMonitoringMiddleware.php', + 'Aws\\ClientSideMonitoring\\ApiCallMonitoringMiddleware' => $vendorDir . '/aws/aws-sdk-php/src/ClientSideMonitoring/ApiCallMonitoringMiddleware.php', + 'Aws\\ClientSideMonitoring\\Configuration' => $vendorDir . '/aws/aws-sdk-php/src/ClientSideMonitoring/Configuration.php', + 'Aws\\ClientSideMonitoring\\ConfigurationInterface' => $vendorDir . '/aws/aws-sdk-php/src/ClientSideMonitoring/ConfigurationInterface.php', + 'Aws\\ClientSideMonitoring\\ConfigurationProvider' => $vendorDir . '/aws/aws-sdk-php/src/ClientSideMonitoring/ConfigurationProvider.php', + 'Aws\\ClientSideMonitoring\\Exception\\ConfigurationException' => $vendorDir . '/aws/aws-sdk-php/src/ClientSideMonitoring/Exception/ConfigurationException.php', + 'Aws\\ClientSideMonitoring\\MonitoringMiddlewareInterface' => $vendorDir . '/aws/aws-sdk-php/src/ClientSideMonitoring/MonitoringMiddlewareInterface.php', + 'Aws\\Command' => $vendorDir . '/aws/aws-sdk-php/src/Command.php', + 'Aws\\CommandInterface' => $vendorDir . '/aws/aws-sdk-php/src/CommandInterface.php', + 'Aws\\CommandPool' => $vendorDir . '/aws/aws-sdk-php/src/CommandPool.php', + 'Aws\\ConfigurationProviderInterface' => $vendorDir . '/aws/aws-sdk-php/src/ConfigurationProviderInterface.php', + 'Aws\\Configuration\\ConfigurationResolver' => $vendorDir . '/aws/aws-sdk-php/src/Configuration/ConfigurationResolver.php', + 'Aws\\Credentials\\AssumeRoleCredentialProvider' => $vendorDir . '/aws/aws-sdk-php/src/Credentials/AssumeRoleCredentialProvider.php', + 'Aws\\Credentials\\AssumeRoleWithWebIdentityCredentialProvider' => $vendorDir . '/aws/aws-sdk-php/src/Credentials/AssumeRoleWithWebIdentityCredentialProvider.php', + 'Aws\\Credentials\\CredentialProvider' => $vendorDir . '/aws/aws-sdk-php/src/Credentials/CredentialProvider.php', + 'Aws\\Credentials\\CredentialSources' => $vendorDir . '/aws/aws-sdk-php/src/Credentials/CredentialSources.php', + 'Aws\\Credentials\\Credentials' => $vendorDir . '/aws/aws-sdk-php/src/Credentials/Credentials.php', + 'Aws\\Credentials\\CredentialsInterface' => $vendorDir . '/aws/aws-sdk-php/src/Credentials/CredentialsInterface.php', + 'Aws\\Credentials\\CredentialsUtils' => $vendorDir . '/aws/aws-sdk-php/src/Credentials/CredentialsUtils.php', + 'Aws\\Credentials\\EcsCredentialProvider' => $vendorDir . '/aws/aws-sdk-php/src/Credentials/EcsCredentialProvider.php', + 'Aws\\Credentials\\InstanceProfileProvider' => $vendorDir . '/aws/aws-sdk-php/src/Credentials/InstanceProfileProvider.php', + 'Aws\\Crypto\\AbstractCryptoClient' => $vendorDir . '/aws/aws-sdk-php/src/Crypto/AbstractCryptoClient.php', + 'Aws\\Crypto\\AbstractCryptoClientV2' => $vendorDir . '/aws/aws-sdk-php/src/Crypto/AbstractCryptoClientV2.php', + 'Aws\\Crypto\\AesDecryptingStream' => $vendorDir . '/aws/aws-sdk-php/src/Crypto/AesDecryptingStream.php', + 'Aws\\Crypto\\AesEncryptingStream' => $vendorDir . '/aws/aws-sdk-php/src/Crypto/AesEncryptingStream.php', + 'Aws\\Crypto\\AesGcmDecryptingStream' => $vendorDir . '/aws/aws-sdk-php/src/Crypto/AesGcmDecryptingStream.php', + 'Aws\\Crypto\\AesGcmEncryptingStream' => $vendorDir . '/aws/aws-sdk-php/src/Crypto/AesGcmEncryptingStream.php', + 'Aws\\Crypto\\AesStreamInterface' => $vendorDir . '/aws/aws-sdk-php/src/Crypto/AesStreamInterface.php', + 'Aws\\Crypto\\AesStreamInterfaceV2' => $vendorDir . '/aws/aws-sdk-php/src/Crypto/AesStreamInterfaceV2.php', + 'Aws\\Crypto\\Cipher\\Cbc' => $vendorDir . '/aws/aws-sdk-php/src/Crypto/Cipher/Cbc.php', + 'Aws\\Crypto\\Cipher\\CipherBuilderTrait' => $vendorDir . '/aws/aws-sdk-php/src/Crypto/Cipher/CipherBuilderTrait.php', + 'Aws\\Crypto\\Cipher\\CipherMethod' => $vendorDir . '/aws/aws-sdk-php/src/Crypto/Cipher/CipherMethod.php', + 'Aws\\Crypto\\DecryptionTrait' => $vendorDir . '/aws/aws-sdk-php/src/Crypto/DecryptionTrait.php', + 'Aws\\Crypto\\DecryptionTraitV2' => $vendorDir . '/aws/aws-sdk-php/src/Crypto/DecryptionTraitV2.php', + 'Aws\\Crypto\\EncryptionTrait' => $vendorDir . '/aws/aws-sdk-php/src/Crypto/EncryptionTrait.php', + 'Aws\\Crypto\\EncryptionTraitV2' => $vendorDir . '/aws/aws-sdk-php/src/Crypto/EncryptionTraitV2.php', + 'Aws\\Crypto\\KmsMaterialsProvider' => $vendorDir . '/aws/aws-sdk-php/src/Crypto/KmsMaterialsProvider.php', + 'Aws\\Crypto\\KmsMaterialsProviderV2' => $vendorDir . '/aws/aws-sdk-php/src/Crypto/KmsMaterialsProviderV2.php', + 'Aws\\Crypto\\MaterialsProvider' => $vendorDir . '/aws/aws-sdk-php/src/Crypto/MaterialsProvider.php', + 'Aws\\Crypto\\MaterialsProviderInterface' => $vendorDir . '/aws/aws-sdk-php/src/Crypto/MaterialsProviderInterface.php', + 'Aws\\Crypto\\MaterialsProviderInterfaceV2' => $vendorDir . '/aws/aws-sdk-php/src/Crypto/MaterialsProviderInterfaceV2.php', + 'Aws\\Crypto\\MaterialsProviderV2' => $vendorDir . '/aws/aws-sdk-php/src/Crypto/MaterialsProviderV2.php', + 'Aws\\Crypto\\MetadataEnvelope' => $vendorDir . '/aws/aws-sdk-php/src/Crypto/MetadataEnvelope.php', + 'Aws\\Crypto\\MetadataStrategyInterface' => $vendorDir . '/aws/aws-sdk-php/src/Crypto/MetadataStrategyInterface.php', + 'Aws\\DefaultsMode\\Configuration' => $vendorDir . '/aws/aws-sdk-php/src/DefaultsMode/Configuration.php', + 'Aws\\DefaultsMode\\ConfigurationInterface' => $vendorDir . '/aws/aws-sdk-php/src/DefaultsMode/ConfigurationInterface.php', + 'Aws\\DefaultsMode\\ConfigurationProvider' => $vendorDir . '/aws/aws-sdk-php/src/DefaultsMode/ConfigurationProvider.php', + 'Aws\\DefaultsMode\\Exception\\ConfigurationException' => $vendorDir . '/aws/aws-sdk-php/src/DefaultsMode/Exception/ConfigurationException.php', + 'Aws\\DoctrineCacheAdapter' => $vendorDir . '/aws/aws-sdk-php/src/DoctrineCacheAdapter.php', + 'Aws\\EndpointDiscovery\\Configuration' => $vendorDir . '/aws/aws-sdk-php/src/EndpointDiscovery/Configuration.php', + 'Aws\\EndpointDiscovery\\ConfigurationInterface' => $vendorDir . '/aws/aws-sdk-php/src/EndpointDiscovery/ConfigurationInterface.php', + 'Aws\\EndpointDiscovery\\ConfigurationProvider' => $vendorDir . '/aws/aws-sdk-php/src/EndpointDiscovery/ConfigurationProvider.php', + 'Aws\\EndpointDiscovery\\EndpointDiscoveryMiddleware' => $vendorDir . '/aws/aws-sdk-php/src/EndpointDiscovery/EndpointDiscoveryMiddleware.php', + 'Aws\\EndpointDiscovery\\EndpointList' => $vendorDir . '/aws/aws-sdk-php/src/EndpointDiscovery/EndpointList.php', + 'Aws\\EndpointDiscovery\\Exception\\ConfigurationException' => $vendorDir . '/aws/aws-sdk-php/src/EndpointDiscovery/Exception/ConfigurationException.php', + 'Aws\\EndpointParameterMiddleware' => $vendorDir . '/aws/aws-sdk-php/src/EndpointParameterMiddleware.php', + 'Aws\\EndpointV2\\EndpointDefinitionProvider' => $vendorDir . '/aws/aws-sdk-php/src/EndpointV2/EndpointDefinitionProvider.php', + 'Aws\\EndpointV2\\EndpointProviderV2' => $vendorDir . '/aws/aws-sdk-php/src/EndpointV2/EndpointProviderV2.php', + 'Aws\\EndpointV2\\EndpointV2Middleware' => $vendorDir . '/aws/aws-sdk-php/src/EndpointV2/EndpointV2Middleware.php', + 'Aws\\EndpointV2\\EndpointV2SerializerTrait' => $vendorDir . '/aws/aws-sdk-php/src/EndpointV2/EndpointV2SerializerTrait.php', + 'Aws\\EndpointV2\\Rule\\AbstractRule' => $vendorDir . '/aws/aws-sdk-php/src/EndpointV2/Rule/AbstractRule.php', + 'Aws\\EndpointV2\\Rule\\EndpointRule' => $vendorDir . '/aws/aws-sdk-php/src/EndpointV2/Rule/EndpointRule.php', + 'Aws\\EndpointV2\\Rule\\ErrorRule' => $vendorDir . '/aws/aws-sdk-php/src/EndpointV2/Rule/ErrorRule.php', + 'Aws\\EndpointV2\\Rule\\RuleCreator' => $vendorDir . '/aws/aws-sdk-php/src/EndpointV2/Rule/RuleCreator.php', + 'Aws\\EndpointV2\\Rule\\TreeRule' => $vendorDir . '/aws/aws-sdk-php/src/EndpointV2/Rule/TreeRule.php', + 'Aws\\EndpointV2\\Ruleset\\Ruleset' => $vendorDir . '/aws/aws-sdk-php/src/EndpointV2/Ruleset/Ruleset.php', + 'Aws\\EndpointV2\\Ruleset\\RulesetEndpoint' => $vendorDir . '/aws/aws-sdk-php/src/EndpointV2/Ruleset/RulesetEndpoint.php', + 'Aws\\EndpointV2\\Ruleset\\RulesetParameter' => $vendorDir . '/aws/aws-sdk-php/src/EndpointV2/Ruleset/RulesetParameter.php', + 'Aws\\EndpointV2\\Ruleset\\RulesetStandardLibrary' => $vendorDir . '/aws/aws-sdk-php/src/EndpointV2/Ruleset/RulesetStandardLibrary.php', + 'Aws\\Endpoint\\EndpointProvider' => $vendorDir . '/aws/aws-sdk-php/src/Endpoint/EndpointProvider.php', + 'Aws\\Endpoint\\Partition' => $vendorDir . '/aws/aws-sdk-php/src/Endpoint/Partition.php', + 'Aws\\Endpoint\\PartitionEndpointProvider' => $vendorDir . '/aws/aws-sdk-php/src/Endpoint/PartitionEndpointProvider.php', + 'Aws\\Endpoint\\PartitionInterface' => $vendorDir . '/aws/aws-sdk-php/src/Endpoint/PartitionInterface.php', + 'Aws\\Endpoint\\PatternEndpointProvider' => $vendorDir . '/aws/aws-sdk-php/src/Endpoint/PatternEndpointProvider.php', + 'Aws\\Endpoint\\UseDualstackEndpoint\\Configuration' => $vendorDir . '/aws/aws-sdk-php/src/Endpoint/UseDualstackEndpoint/Configuration.php', + 'Aws\\Endpoint\\UseDualstackEndpoint\\ConfigurationInterface' => $vendorDir . '/aws/aws-sdk-php/src/Endpoint/UseDualstackEndpoint/ConfigurationInterface.php', + 'Aws\\Endpoint\\UseDualstackEndpoint\\ConfigurationProvider' => $vendorDir . '/aws/aws-sdk-php/src/Endpoint/UseDualstackEndpoint/ConfigurationProvider.php', + 'Aws\\Endpoint\\UseDualstackEndpoint\\Exception\\ConfigurationException' => $vendorDir . '/aws/aws-sdk-php/src/Endpoint/UseDualstackEndpoint/Exception/ConfigurationException.php', + 'Aws\\Endpoint\\UseFipsEndpoint\\Configuration' => $vendorDir . '/aws/aws-sdk-php/src/Endpoint/UseFipsEndpoint/Configuration.php', + 'Aws\\Endpoint\\UseFipsEndpoint\\ConfigurationInterface' => $vendorDir . '/aws/aws-sdk-php/src/Endpoint/UseFipsEndpoint/ConfigurationInterface.php', + 'Aws\\Endpoint\\UseFipsEndpoint\\ConfigurationProvider' => $vendorDir . '/aws/aws-sdk-php/src/Endpoint/UseFipsEndpoint/ConfigurationProvider.php', + 'Aws\\Endpoint\\UseFipsEndpoint\\Exception\\ConfigurationException' => $vendorDir . '/aws/aws-sdk-php/src/Endpoint/UseFipsEndpoint/Exception/ConfigurationException.php', + 'Aws\\Exception\\AwsException' => $vendorDir . '/aws/aws-sdk-php/src/Exception/AwsException.php', + 'Aws\\Exception\\CommonRuntimeException' => $vendorDir . '/aws/aws-sdk-php/src/Exception/CommonRuntimeException.php', + 'Aws\\Exception\\CouldNotCreateChecksumException' => $vendorDir . '/aws/aws-sdk-php/src/Exception/CouldNotCreateChecksumException.php', + 'Aws\\Exception\\CredentialsException' => $vendorDir . '/aws/aws-sdk-php/src/Exception/CredentialsException.php', + 'Aws\\Exception\\CryptoException' => $vendorDir . '/aws/aws-sdk-php/src/Exception/CryptoException.php', + 'Aws\\Exception\\CryptoPolyfillException' => $vendorDir . '/aws/aws-sdk-php/src/Exception/CryptoPolyfillException.php', + 'Aws\\Exception\\EventStreamDataException' => $vendorDir . '/aws/aws-sdk-php/src/Exception/EventStreamDataException.php', + 'Aws\\Exception\\IncalculablePayloadException' => $vendorDir . '/aws/aws-sdk-php/src/Exception/IncalculablePayloadException.php', + 'Aws\\Exception\\InvalidJsonException' => $vendorDir . '/aws/aws-sdk-php/src/Exception/InvalidJsonException.php', + 'Aws\\Exception\\InvalidRegionException' => $vendorDir . '/aws/aws-sdk-php/src/Exception/InvalidRegionException.php', + 'Aws\\Exception\\MultipartUploadException' => $vendorDir . '/aws/aws-sdk-php/src/Exception/MultipartUploadException.php', + 'Aws\\Exception\\TokenException' => $vendorDir . '/aws/aws-sdk-php/src/Exception/TokenException.php', + 'Aws\\Exception\\UnresolvedApiException' => $vendorDir . '/aws/aws-sdk-php/src/Exception/UnresolvedApiException.php', + 'Aws\\Exception\\UnresolvedEndpointException' => $vendorDir . '/aws/aws-sdk-php/src/Exception/UnresolvedEndpointException.php', + 'Aws\\Exception\\UnresolvedSignatureException' => $vendorDir . '/aws/aws-sdk-php/src/Exception/UnresolvedSignatureException.php', + 'Aws\\HandlerList' => $vendorDir . '/aws/aws-sdk-php/src/HandlerList.php', + 'Aws\\Handler\\Guzzle\\GuzzleHandler' => $vendorDir . '/aws/aws-sdk-php/src/Handler/Guzzle/GuzzleHandler.php', + 'Aws\\HasDataTrait' => $vendorDir . '/aws/aws-sdk-php/src/HasDataTrait.php', + 'Aws\\HasMonitoringEventsTrait' => $vendorDir . '/aws/aws-sdk-php/src/HasMonitoringEventsTrait.php', + 'Aws\\HashInterface' => $vendorDir . '/aws/aws-sdk-php/src/HashInterface.php', + 'Aws\\HashingStream' => $vendorDir . '/aws/aws-sdk-php/src/HashingStream.php', + 'Aws\\History' => $vendorDir . '/aws/aws-sdk-php/src/History.php', + 'Aws\\IdempotencyTokenMiddleware' => $vendorDir . '/aws/aws-sdk-php/src/IdempotencyTokenMiddleware.php', + 'Aws\\Identity\\AwsCredentialIdentity' => $vendorDir . '/aws/aws-sdk-php/src/Identity/AwsCredentialIdentity.php', + 'Aws\\Identity\\BearerTokenIdentity' => $vendorDir . '/aws/aws-sdk-php/src/Identity/BearerTokenIdentity.php', + 'Aws\\Identity\\IdentityInterface' => $vendorDir . '/aws/aws-sdk-php/src/Identity/IdentityInterface.php', + 'Aws\\Identity\\S3\\S3ExpressIdentity' => $vendorDir . '/aws/aws-sdk-php/src/Identity/S3/S3ExpressIdentity.php', + 'Aws\\Identity\\S3\\S3ExpressIdentityProvider' => $vendorDir . '/aws/aws-sdk-php/src/Identity/S3/S3ExpressIdentityProvider.php', + 'Aws\\InputValidationMiddleware' => $vendorDir . '/aws/aws-sdk-php/src/InputValidationMiddleware.php', + 'Aws\\JsonCompiler' => $vendorDir . '/aws/aws-sdk-php/src/JsonCompiler.php', + 'Aws\\Kms\\Exception\\KmsException' => $vendorDir . '/aws/aws-sdk-php/src/Kms/Exception/KmsException.php', + 'Aws\\Kms\\KmsClient' => $vendorDir . '/aws/aws-sdk-php/src/Kms/KmsClient.php', + 'Aws\\LruArrayCache' => $vendorDir . '/aws/aws-sdk-php/src/LruArrayCache.php', + 'Aws\\MetricsBuilder' => $vendorDir . '/aws/aws-sdk-php/src/MetricsBuilder.php', + 'Aws\\Middleware' => $vendorDir . '/aws/aws-sdk-php/src/Middleware.php', + 'Aws\\MockHandler' => $vendorDir . '/aws/aws-sdk-php/src/MockHandler.php', + 'Aws\\MonitoringEventsInterface' => $vendorDir . '/aws/aws-sdk-php/src/MonitoringEventsInterface.php', + 'Aws\\MultiRegionClient' => $vendorDir . '/aws/aws-sdk-php/src/MultiRegionClient.php', + 'Aws\\Multipart\\AbstractUploadManager' => $vendorDir . '/aws/aws-sdk-php/src/Multipart/AbstractUploadManager.php', + 'Aws\\Multipart\\AbstractUploader' => $vendorDir . '/aws/aws-sdk-php/src/Multipart/AbstractUploader.php', + 'Aws\\Multipart\\UploadState' => $vendorDir . '/aws/aws-sdk-php/src/Multipart/UploadState.php', + 'Aws\\PhpHash' => $vendorDir . '/aws/aws-sdk-php/src/PhpHash.php', + 'Aws\\PresignUrlMiddleware' => $vendorDir . '/aws/aws-sdk-php/src/PresignUrlMiddleware.php', + 'Aws\\Psr16CacheAdapter' => $vendorDir . '/aws/aws-sdk-php/src/Psr16CacheAdapter.php', + 'Aws\\PsrCacheAdapter' => $vendorDir . '/aws/aws-sdk-php/src/PsrCacheAdapter.php', + 'Aws\\QueryCompatibleInputMiddleware' => $vendorDir . '/aws/aws-sdk-php/src/QueryCompatibleInputMiddleware.php', + 'Aws\\RequestCompressionMiddleware' => $vendorDir . '/aws/aws-sdk-php/src/RequestCompressionMiddleware.php', + 'Aws\\ResponseContainerInterface' => $vendorDir . '/aws/aws-sdk-php/src/ResponseContainerInterface.php', + 'Aws\\Result' => $vendorDir . '/aws/aws-sdk-php/src/Result.php', + 'Aws\\ResultInterface' => $vendorDir . '/aws/aws-sdk-php/src/ResultInterface.php', + 'Aws\\ResultPaginator' => $vendorDir . '/aws/aws-sdk-php/src/ResultPaginator.php', + 'Aws\\RetryMiddleware' => $vendorDir . '/aws/aws-sdk-php/src/RetryMiddleware.php', + 'Aws\\RetryMiddlewareV2' => $vendorDir . '/aws/aws-sdk-php/src/RetryMiddlewareV2.php', + 'Aws\\Retry\\Configuration' => $vendorDir . '/aws/aws-sdk-php/src/Retry/Configuration.php', + 'Aws\\Retry\\ConfigurationInterface' => $vendorDir . '/aws/aws-sdk-php/src/Retry/ConfigurationInterface.php', + 'Aws\\Retry\\ConfigurationProvider' => $vendorDir . '/aws/aws-sdk-php/src/Retry/ConfigurationProvider.php', + 'Aws\\Retry\\Exception\\ConfigurationException' => $vendorDir . '/aws/aws-sdk-php/src/Retry/Exception/ConfigurationException.php', + 'Aws\\Retry\\QuotaManager' => $vendorDir . '/aws/aws-sdk-php/src/Retry/QuotaManager.php', + 'Aws\\Retry\\RateLimiter' => $vendorDir . '/aws/aws-sdk-php/src/Retry/RateLimiter.php', + 'Aws\\Retry\\RetryHelperTrait' => $vendorDir . '/aws/aws-sdk-php/src/Retry/RetryHelperTrait.php', + 'Aws\\S3\\AmbiguousSuccessParser' => $vendorDir . '/aws/aws-sdk-php/src/S3/AmbiguousSuccessParser.php', + 'Aws\\S3\\ApplyChecksumMiddleware' => $vendorDir . '/aws/aws-sdk-php/src/S3/ApplyChecksumMiddleware.php', + 'Aws\\S3\\BatchDelete' => $vendorDir . '/aws/aws-sdk-php/src/S3/BatchDelete.php', + 'Aws\\S3\\BucketEndpointArnMiddleware' => $vendorDir . '/aws/aws-sdk-php/src/S3/BucketEndpointArnMiddleware.php', + 'Aws\\S3\\BucketEndpointMiddleware' => $vendorDir . '/aws/aws-sdk-php/src/S3/BucketEndpointMiddleware.php', + 'Aws\\S3\\CalculatesChecksumTrait' => $vendorDir . '/aws/aws-sdk-php/src/S3/CalculatesChecksumTrait.php', + 'Aws\\S3\\Crypto\\CryptoParamsTrait' => $vendorDir . '/aws/aws-sdk-php/src/S3/Crypto/CryptoParamsTrait.php', + 'Aws\\S3\\Crypto\\CryptoParamsTraitV2' => $vendorDir . '/aws/aws-sdk-php/src/S3/Crypto/CryptoParamsTraitV2.php', + 'Aws\\S3\\Crypto\\HeadersMetadataStrategy' => $vendorDir . '/aws/aws-sdk-php/src/S3/Crypto/HeadersMetadataStrategy.php', + 'Aws\\S3\\Crypto\\InstructionFileMetadataStrategy' => $vendorDir . '/aws/aws-sdk-php/src/S3/Crypto/InstructionFileMetadataStrategy.php', + 'Aws\\S3\\Crypto\\S3EncryptionClient' => $vendorDir . '/aws/aws-sdk-php/src/S3/Crypto/S3EncryptionClient.php', + 'Aws\\S3\\Crypto\\S3EncryptionClientV2' => $vendorDir . '/aws/aws-sdk-php/src/S3/Crypto/S3EncryptionClientV2.php', + 'Aws\\S3\\Crypto\\S3EncryptionMultipartUploader' => $vendorDir . '/aws/aws-sdk-php/src/S3/Crypto/S3EncryptionMultipartUploader.php', + 'Aws\\S3\\Crypto\\S3EncryptionMultipartUploaderV2' => $vendorDir . '/aws/aws-sdk-php/src/S3/Crypto/S3EncryptionMultipartUploaderV2.php', + 'Aws\\S3\\Crypto\\UserAgentTrait' => $vendorDir . '/aws/aws-sdk-php/src/S3/Crypto/UserAgentTrait.php', + 'Aws\\S3\\EndpointRegionHelperTrait' => $vendorDir . '/aws/aws-sdk-php/src/S3/EndpointRegionHelperTrait.php', + 'Aws\\S3\\Exception\\DeleteMultipleObjectsException' => $vendorDir . '/aws/aws-sdk-php/src/S3/Exception/DeleteMultipleObjectsException.php', + 'Aws\\S3\\Exception\\PermanentRedirectException' => $vendorDir . '/aws/aws-sdk-php/src/S3/Exception/PermanentRedirectException.php', + 'Aws\\S3\\Exception\\S3Exception' => $vendorDir . '/aws/aws-sdk-php/src/S3/Exception/S3Exception.php', + 'Aws\\S3\\Exception\\S3MultipartUploadException' => $vendorDir . '/aws/aws-sdk-php/src/S3/Exception/S3MultipartUploadException.php', + 'Aws\\S3\\ExpiresParsingMiddleware' => $vendorDir . '/aws/aws-sdk-php/src/S3/ExpiresParsingMiddleware.php', + 'Aws\\S3\\GetBucketLocationParser' => $vendorDir . '/aws/aws-sdk-php/src/S3/GetBucketLocationParser.php', + 'Aws\\S3\\MultipartCopy' => $vendorDir . '/aws/aws-sdk-php/src/S3/MultipartCopy.php', + 'Aws\\S3\\MultipartUploader' => $vendorDir . '/aws/aws-sdk-php/src/S3/MultipartUploader.php', + 'Aws\\S3\\MultipartUploadingTrait' => $vendorDir . '/aws/aws-sdk-php/src/S3/MultipartUploadingTrait.php', + 'Aws\\S3\\ObjectCopier' => $vendorDir . '/aws/aws-sdk-php/src/S3/ObjectCopier.php', + 'Aws\\S3\\ObjectUploader' => $vendorDir . '/aws/aws-sdk-php/src/S3/ObjectUploader.php', + 'Aws\\S3\\Parser\\GetBucketLocationResultMutator' => $vendorDir . '/aws/aws-sdk-php/src/S3/Parser/GetBucketLocationResultMutator.php', + 'Aws\\S3\\Parser\\S3Parser' => $vendorDir . '/aws/aws-sdk-php/src/S3/Parser/S3Parser.php', + 'Aws\\S3\\Parser\\S3ResultMutator' => $vendorDir . '/aws/aws-sdk-php/src/S3/Parser/S3ResultMutator.php', + 'Aws\\S3\\Parser\\ValidateResponseChecksumResultMutator' => $vendorDir . '/aws/aws-sdk-php/src/S3/Parser/ValidateResponseChecksumResultMutator.php', + 'Aws\\S3\\PermanentRedirectMiddleware' => $vendorDir . '/aws/aws-sdk-php/src/S3/PermanentRedirectMiddleware.php', + 'Aws\\S3\\PostObject' => $vendorDir . '/aws/aws-sdk-php/src/S3/PostObject.php', + 'Aws\\S3\\PostObjectV4' => $vendorDir . '/aws/aws-sdk-php/src/S3/PostObjectV4.php', + 'Aws\\S3\\PutObjectUrlMiddleware' => $vendorDir . '/aws/aws-sdk-php/src/S3/PutObjectUrlMiddleware.php', + 'Aws\\S3\\RegionalEndpoint\\Configuration' => $vendorDir . '/aws/aws-sdk-php/src/S3/RegionalEndpoint/Configuration.php', + 'Aws\\S3\\RegionalEndpoint\\ConfigurationInterface' => $vendorDir . '/aws/aws-sdk-php/src/S3/RegionalEndpoint/ConfigurationInterface.php', + 'Aws\\S3\\RegionalEndpoint\\ConfigurationProvider' => $vendorDir . '/aws/aws-sdk-php/src/S3/RegionalEndpoint/ConfigurationProvider.php', + 'Aws\\S3\\RegionalEndpoint\\Exception\\ConfigurationException' => $vendorDir . '/aws/aws-sdk-php/src/S3/RegionalEndpoint/Exception/ConfigurationException.php', + 'Aws\\S3\\RetryableMalformedResponseParser' => $vendorDir . '/aws/aws-sdk-php/src/S3/RetryableMalformedResponseParser.php', + 'Aws\\S3\\S3Client' => $vendorDir . '/aws/aws-sdk-php/src/S3/S3Client.php', + 'Aws\\S3\\S3ClientInterface' => $vendorDir . '/aws/aws-sdk-php/src/S3/S3ClientInterface.php', + 'Aws\\S3\\S3ClientTrait' => $vendorDir . '/aws/aws-sdk-php/src/S3/S3ClientTrait.php', + 'Aws\\S3\\S3EndpointMiddleware' => $vendorDir . '/aws/aws-sdk-php/src/S3/S3EndpointMiddleware.php', + 'Aws\\S3\\S3MultiRegionClient' => $vendorDir . '/aws/aws-sdk-php/src/S3/S3MultiRegionClient.php', + 'Aws\\S3\\S3UriParser' => $vendorDir . '/aws/aws-sdk-php/src/S3/S3UriParser.php', + 'Aws\\S3\\SSECMiddleware' => $vendorDir . '/aws/aws-sdk-php/src/S3/SSECMiddleware.php', + 'Aws\\S3\\StreamWrapper' => $vendorDir . '/aws/aws-sdk-php/src/S3/StreamWrapper.php', + 'Aws\\S3\\Transfer' => $vendorDir . '/aws/aws-sdk-php/src/S3/Transfer.php', + 'Aws\\S3\\UseArnRegion\\Configuration' => $vendorDir . '/aws/aws-sdk-php/src/S3/UseArnRegion/Configuration.php', + 'Aws\\S3\\UseArnRegion\\ConfigurationInterface' => $vendorDir . '/aws/aws-sdk-php/src/S3/UseArnRegion/ConfigurationInterface.php', + 'Aws\\S3\\UseArnRegion\\ConfigurationProvider' => $vendorDir . '/aws/aws-sdk-php/src/S3/UseArnRegion/ConfigurationProvider.php', + 'Aws\\S3\\UseArnRegion\\Exception\\ConfigurationException' => $vendorDir . '/aws/aws-sdk-php/src/S3/UseArnRegion/Exception/ConfigurationException.php', + 'Aws\\S3\\ValidateResponseChecksumParser' => $vendorDir . '/aws/aws-sdk-php/src/S3/ValidateResponseChecksumParser.php', + 'Aws\\SSOOIDC\\Exception\\SSOOIDCException' => $vendorDir . '/aws/aws-sdk-php/src/SSOOIDC/Exception/SSOOIDCException.php', + 'Aws\\SSOOIDC\\SSOOIDCClient' => $vendorDir . '/aws/aws-sdk-php/src/SSOOIDC/SSOOIDCClient.php', + 'Aws\\SSO\\Exception\\SSOException' => $vendorDir . '/aws/aws-sdk-php/src/SSO/Exception/SSOException.php', + 'Aws\\SSO\\SSOClient' => $vendorDir . '/aws/aws-sdk-php/src/SSO/SSOClient.php', + 'Aws\\Script\\Composer\\Composer' => $vendorDir . '/aws/aws-sdk-php/src/Script/Composer/Composer.php', + 'Aws\\Sdk' => $vendorDir . '/aws/aws-sdk-php/src/Sdk.php', + 'Aws\\Signature\\AnonymousSignature' => $vendorDir . '/aws/aws-sdk-php/src/Signature/AnonymousSignature.php', + 'Aws\\Signature\\S3ExpressSignature' => $vendorDir . '/aws/aws-sdk-php/src/Signature/S3ExpressSignature.php', + 'Aws\\Signature\\S3SignatureV4' => $vendorDir . '/aws/aws-sdk-php/src/Signature/S3SignatureV4.php', + 'Aws\\Signature\\SignatureInterface' => $vendorDir . '/aws/aws-sdk-php/src/Signature/SignatureInterface.php', + 'Aws\\Signature\\SignatureProvider' => $vendorDir . '/aws/aws-sdk-php/src/Signature/SignatureProvider.php', + 'Aws\\Signature\\SignatureTrait' => $vendorDir . '/aws/aws-sdk-php/src/Signature/SignatureTrait.php', + 'Aws\\Signature\\SignatureV4' => $vendorDir . '/aws/aws-sdk-php/src/Signature/SignatureV4.php', + 'Aws\\StreamRequestPayloadMiddleware' => $vendorDir . '/aws/aws-sdk-php/src/StreamRequestPayloadMiddleware.php', + 'Aws\\Sts\\Exception\\StsException' => $vendorDir . '/aws/aws-sdk-php/src/Sts/Exception/StsException.php', + 'Aws\\Sts\\RegionalEndpoints\\Configuration' => $vendorDir . '/aws/aws-sdk-php/src/Sts/RegionalEndpoints/Configuration.php', + 'Aws\\Sts\\RegionalEndpoints\\ConfigurationInterface' => $vendorDir . '/aws/aws-sdk-php/src/Sts/RegionalEndpoints/ConfigurationInterface.php', + 'Aws\\Sts\\RegionalEndpoints\\ConfigurationProvider' => $vendorDir . '/aws/aws-sdk-php/src/Sts/RegionalEndpoints/ConfigurationProvider.php', + 'Aws\\Sts\\RegionalEndpoints\\Exception\\ConfigurationException' => $vendorDir . '/aws/aws-sdk-php/src/Sts/RegionalEndpoints/Exception/ConfigurationException.php', + 'Aws\\Sts\\StsClient' => $vendorDir . '/aws/aws-sdk-php/src/Sts/StsClient.php', + 'Aws\\Token\\BearerTokenAuthorization' => $vendorDir . '/aws/aws-sdk-php/src/Token/BearerTokenAuthorization.php', + 'Aws\\Token\\ParsesIniTrait' => $vendorDir . '/aws/aws-sdk-php/src/Token/ParsesIniTrait.php', + 'Aws\\Token\\RefreshableTokenProviderInterface' => $vendorDir . '/aws/aws-sdk-php/src/Token/RefreshableTokenProviderInterface.php', + 'Aws\\Token\\SsoToken' => $vendorDir . '/aws/aws-sdk-php/src/Token/SsoToken.php', + 'Aws\\Token\\SsoTokenProvider' => $vendorDir . '/aws/aws-sdk-php/src/Token/SsoTokenProvider.php', + 'Aws\\Token\\Token' => $vendorDir . '/aws/aws-sdk-php/src/Token/Token.php', + 'Aws\\Token\\TokenAuthorization' => $vendorDir . '/aws/aws-sdk-php/src/Token/TokenAuthorization.php', + 'Aws\\Token\\TokenInterface' => $vendorDir . '/aws/aws-sdk-php/src/Token/TokenInterface.php', + 'Aws\\Token\\TokenProvider' => $vendorDir . '/aws/aws-sdk-php/src/Token/TokenProvider.php', + 'Aws\\TraceMiddleware' => $vendorDir . '/aws/aws-sdk-php/src/TraceMiddleware.php', + 'Aws\\UserAgentMiddleware' => $vendorDir . '/aws/aws-sdk-php/src/UserAgentMiddleware.php', + 'Aws\\Waiter' => $vendorDir . '/aws/aws-sdk-php/src/Waiter.php', + 'Aws\\WrappedHttpHandler' => $vendorDir . '/aws/aws-sdk-php/src/WrappedHttpHandler.php', + 'Brick\\Math\\BigDecimal' => $vendorDir . '/brick/math/src/BigDecimal.php', + 'Brick\\Math\\BigInteger' => $vendorDir . '/brick/math/src/BigInteger.php', + 'Brick\\Math\\BigNumber' => $vendorDir . '/brick/math/src/BigNumber.php', + 'Brick\\Math\\BigRational' => $vendorDir . '/brick/math/src/BigRational.php', + 'Brick\\Math\\Exception\\DivisionByZeroException' => $vendorDir . '/brick/math/src/Exception/DivisionByZeroException.php', + 'Brick\\Math\\Exception\\IntegerOverflowException' => $vendorDir . '/brick/math/src/Exception/IntegerOverflowException.php', + 'Brick\\Math\\Exception\\MathException' => $vendorDir . '/brick/math/src/Exception/MathException.php', + 'Brick\\Math\\Exception\\NegativeNumberException' => $vendorDir . '/brick/math/src/Exception/NegativeNumberException.php', + 'Brick\\Math\\Exception\\NumberFormatException' => $vendorDir . '/brick/math/src/Exception/NumberFormatException.php', + 'Brick\\Math\\Exception\\RoundingNecessaryException' => $vendorDir . '/brick/math/src/Exception/RoundingNecessaryException.php', + 'Brick\\Math\\Internal\\Calculator' => $vendorDir . '/brick/math/src/Internal/Calculator.php', + 'Brick\\Math\\Internal\\Calculator\\BcMathCalculator' => $vendorDir . '/brick/math/src/Internal/Calculator/BcMathCalculator.php', + 'Brick\\Math\\Internal\\Calculator\\GmpCalculator' => $vendorDir . '/brick/math/src/Internal/Calculator/GmpCalculator.php', + 'Brick\\Math\\Internal\\Calculator\\NativeCalculator' => $vendorDir . '/brick/math/src/Internal/Calculator/NativeCalculator.php', + 'Brick\\Math\\RoundingMode' => $vendorDir . '/brick/math/src/RoundingMode.php', + 'CBOR\\AbstractCBORObject' => $vendorDir . '/spomky-labs/cbor-php/src/AbstractCBORObject.php', + 'CBOR\\ByteStringObject' => $vendorDir . '/spomky-labs/cbor-php/src/ByteStringObject.php', + 'CBOR\\CBORObject' => $vendorDir . '/spomky-labs/cbor-php/src/CBORObject.php', + 'CBOR\\Decoder' => $vendorDir . '/spomky-labs/cbor-php/src/Decoder.php', + 'CBOR\\DecoderInterface' => $vendorDir . '/spomky-labs/cbor-php/src/DecoderInterface.php', + 'CBOR\\IndefiniteLengthByteStringObject' => $vendorDir . '/spomky-labs/cbor-php/src/IndefiniteLengthByteStringObject.php', + 'CBOR\\IndefiniteLengthListObject' => $vendorDir . '/spomky-labs/cbor-php/src/IndefiniteLengthListObject.php', + 'CBOR\\IndefiniteLengthMapObject' => $vendorDir . '/spomky-labs/cbor-php/src/IndefiniteLengthMapObject.php', + 'CBOR\\IndefiniteLengthTextStringObject' => $vendorDir . '/spomky-labs/cbor-php/src/IndefiniteLengthTextStringObject.php', + 'CBOR\\LengthCalculator' => $vendorDir . '/spomky-labs/cbor-php/src/LengthCalculator.php', + 'CBOR\\ListObject' => $vendorDir . '/spomky-labs/cbor-php/src/ListObject.php', + 'CBOR\\MapItem' => $vendorDir . '/spomky-labs/cbor-php/src/MapItem.php', + 'CBOR\\MapObject' => $vendorDir . '/spomky-labs/cbor-php/src/MapObject.php', + 'CBOR\\NegativeIntegerObject' => $vendorDir . '/spomky-labs/cbor-php/src/NegativeIntegerObject.php', + 'CBOR\\Normalizable' => $vendorDir . '/spomky-labs/cbor-php/src/Normalizable.php', + 'CBOR\\OtherObject' => $vendorDir . '/spomky-labs/cbor-php/src/OtherObject.php', + 'CBOR\\OtherObject\\BreakObject' => $vendorDir . '/spomky-labs/cbor-php/src/OtherObject/BreakObject.php', + 'CBOR\\OtherObject\\DoublePrecisionFloatObject' => $vendorDir . '/spomky-labs/cbor-php/src/OtherObject/DoublePrecisionFloatObject.php', + 'CBOR\\OtherObject\\FalseObject' => $vendorDir . '/spomky-labs/cbor-php/src/OtherObject/FalseObject.php', + 'CBOR\\OtherObject\\GenericObject' => $vendorDir . '/spomky-labs/cbor-php/src/OtherObject/GenericObject.php', + 'CBOR\\OtherObject\\HalfPrecisionFloatObject' => $vendorDir . '/spomky-labs/cbor-php/src/OtherObject/HalfPrecisionFloatObject.php', + 'CBOR\\OtherObject\\NullObject' => $vendorDir . '/spomky-labs/cbor-php/src/OtherObject/NullObject.php', + 'CBOR\\OtherObject\\OtherObjectInterface' => $vendorDir . '/spomky-labs/cbor-php/src/OtherObject/OtherObjectInterface.php', + 'CBOR\\OtherObject\\OtherObjectManager' => $vendorDir . '/spomky-labs/cbor-php/src/OtherObject/OtherObjectManager.php', + 'CBOR\\OtherObject\\OtherObjectManagerInterface' => $vendorDir . '/spomky-labs/cbor-php/src/OtherObject/OtherObjectManagerInterface.php', + 'CBOR\\OtherObject\\SimpleObject' => $vendorDir . '/spomky-labs/cbor-php/src/OtherObject/SimpleObject.php', + 'CBOR\\OtherObject\\SinglePrecisionFloatObject' => $vendorDir . '/spomky-labs/cbor-php/src/OtherObject/SinglePrecisionFloatObject.php', + 'CBOR\\OtherObject\\TrueObject' => $vendorDir . '/spomky-labs/cbor-php/src/OtherObject/TrueObject.php', + 'CBOR\\OtherObject\\UndefinedObject' => $vendorDir . '/spomky-labs/cbor-php/src/OtherObject/UndefinedObject.php', + 'CBOR\\Stream' => $vendorDir . '/spomky-labs/cbor-php/src/Stream.php', + 'CBOR\\StringStream' => $vendorDir . '/spomky-labs/cbor-php/src/StringStream.php', + 'CBOR\\Tag' => $vendorDir . '/spomky-labs/cbor-php/src/Tag.php', + 'CBOR\\Tag\\Base16EncodingTag' => $vendorDir . '/spomky-labs/cbor-php/src/Tag/Base16EncodingTag.php', + 'CBOR\\Tag\\Base64EncodingTag' => $vendorDir . '/spomky-labs/cbor-php/src/Tag/Base64EncodingTag.php', + 'CBOR\\Tag\\Base64Tag' => $vendorDir . '/spomky-labs/cbor-php/src/Tag/Base64Tag.php', + 'CBOR\\Tag\\Base64UrlEncodingTag' => $vendorDir . '/spomky-labs/cbor-php/src/Tag/Base64UrlEncodingTag.php', + 'CBOR\\Tag\\Base64UrlTag' => $vendorDir . '/spomky-labs/cbor-php/src/Tag/Base64UrlTag.php', + 'CBOR\\Tag\\BigFloatTag' => $vendorDir . '/spomky-labs/cbor-php/src/Tag/BigFloatTag.php', + 'CBOR\\Tag\\CBOREncodingTag' => $vendorDir . '/spomky-labs/cbor-php/src/Tag/CBOREncodingTag.php', + 'CBOR\\Tag\\CBORTag' => $vendorDir . '/spomky-labs/cbor-php/src/Tag/CBORTag.php', + 'CBOR\\Tag\\DatetimeTag' => $vendorDir . '/spomky-labs/cbor-php/src/Tag/DatetimeTag.php', + 'CBOR\\Tag\\DecimalFractionTag' => $vendorDir . '/spomky-labs/cbor-php/src/Tag/DecimalFractionTag.php', + 'CBOR\\Tag\\GenericTag' => $vendorDir . '/spomky-labs/cbor-php/src/Tag/GenericTag.php', + 'CBOR\\Tag\\MimeTag' => $vendorDir . '/spomky-labs/cbor-php/src/Tag/MimeTag.php', + 'CBOR\\Tag\\NegativeBigIntegerTag' => $vendorDir . '/spomky-labs/cbor-php/src/Tag/NegativeBigIntegerTag.php', + 'CBOR\\Tag\\TagInterface' => $vendorDir . '/spomky-labs/cbor-php/src/Tag/TagInterface.php', + 'CBOR\\Tag\\TagManager' => $vendorDir . '/spomky-labs/cbor-php/src/Tag/TagManager.php', + 'CBOR\\Tag\\TagManagerInterface' => $vendorDir . '/spomky-labs/cbor-php/src/Tag/TagManagerInterface.php', + 'CBOR\\Tag\\TimestampTag' => $vendorDir . '/spomky-labs/cbor-php/src/Tag/TimestampTag.php', + 'CBOR\\Tag\\UnsignedBigIntegerTag' => $vendorDir . '/spomky-labs/cbor-php/src/Tag/UnsignedBigIntegerTag.php', + 'CBOR\\Tag\\UriTag' => $vendorDir . '/spomky-labs/cbor-php/src/Tag/UriTag.php', + 'CBOR\\TextStringObject' => $vendorDir . '/spomky-labs/cbor-php/src/TextStringObject.php', + 'CBOR\\UnsignedIntegerObject' => $vendorDir . '/spomky-labs/cbor-php/src/UnsignedIntegerObject.php', + 'CBOR\\Utils' => $vendorDir . '/spomky-labs/cbor-php/src/Utils.php', + 'Composer\\InstalledVersions' => $vendorDir . '/composer/InstalledVersions.php', + 'Console_Getopt' => $vendorDir . '/pear/console_getopt/Console/Getopt.php', + 'Cose\\Algorithm\\Algorithm' => $vendorDir . '/web-auth/cose-lib/src/Algorithm/Algorithm.php', + 'Cose\\Algorithm\\Mac\\HS256' => $vendorDir . '/web-auth/cose-lib/src/Algorithm/Mac/HS256.php', + 'Cose\\Algorithm\\Mac\\HS256Truncated64' => $vendorDir . '/web-auth/cose-lib/src/Algorithm/Mac/HS256Truncated64.php', + 'Cose\\Algorithm\\Mac\\HS384' => $vendorDir . '/web-auth/cose-lib/src/Algorithm/Mac/HS384.php', + 'Cose\\Algorithm\\Mac\\HS512' => $vendorDir . '/web-auth/cose-lib/src/Algorithm/Mac/HS512.php', + 'Cose\\Algorithm\\Mac\\Hmac' => $vendorDir . '/web-auth/cose-lib/src/Algorithm/Mac/Hmac.php', + 'Cose\\Algorithm\\Mac\\Mac' => $vendorDir . '/web-auth/cose-lib/src/Algorithm/Mac/Mac.php', + 'Cose\\Algorithm\\Manager' => $vendorDir . '/web-auth/cose-lib/src/Algorithm/Manager.php', + 'Cose\\Algorithm\\ManagerFactory' => $vendorDir . '/web-auth/cose-lib/src/Algorithm/ManagerFactory.php', + 'Cose\\Algorithm\\Signature\\ECDSA\\ECDSA' => $vendorDir . '/web-auth/cose-lib/src/Algorithm/Signature/ECDSA/ECDSA.php', + 'Cose\\Algorithm\\Signature\\ECDSA\\ECSignature' => $vendorDir . '/web-auth/cose-lib/src/Algorithm/Signature/ECDSA/ECSignature.php', + 'Cose\\Algorithm\\Signature\\ECDSA\\ES256' => $vendorDir . '/web-auth/cose-lib/src/Algorithm/Signature/ECDSA/ES256.php', + 'Cose\\Algorithm\\Signature\\ECDSA\\ES256K' => $vendorDir . '/web-auth/cose-lib/src/Algorithm/Signature/ECDSA/ES256K.php', + 'Cose\\Algorithm\\Signature\\ECDSA\\ES384' => $vendorDir . '/web-auth/cose-lib/src/Algorithm/Signature/ECDSA/ES384.php', + 'Cose\\Algorithm\\Signature\\ECDSA\\ES512' => $vendorDir . '/web-auth/cose-lib/src/Algorithm/Signature/ECDSA/ES512.php', + 'Cose\\Algorithm\\Signature\\EdDSA\\Ed25519' => $vendorDir . '/web-auth/cose-lib/src/Algorithm/Signature/EdDSA/Ed25519.php', + 'Cose\\Algorithm\\Signature\\EdDSA\\Ed256' => $vendorDir . '/web-auth/cose-lib/src/Algorithm/Signature/EdDSA/Ed256.php', + 'Cose\\Algorithm\\Signature\\EdDSA\\Ed512' => $vendorDir . '/web-auth/cose-lib/src/Algorithm/Signature/EdDSA/Ed512.php', + 'Cose\\Algorithm\\Signature\\EdDSA\\EdDSA' => $vendorDir . '/web-auth/cose-lib/src/Algorithm/Signature/EdDSA/EdDSA.php', + 'Cose\\Algorithm\\Signature\\RSA\\PS256' => $vendorDir . '/web-auth/cose-lib/src/Algorithm/Signature/RSA/PS256.php', + 'Cose\\Algorithm\\Signature\\RSA\\PS384' => $vendorDir . '/web-auth/cose-lib/src/Algorithm/Signature/RSA/PS384.php', + 'Cose\\Algorithm\\Signature\\RSA\\PS512' => $vendorDir . '/web-auth/cose-lib/src/Algorithm/Signature/RSA/PS512.php', + 'Cose\\Algorithm\\Signature\\RSA\\PSSRSA' => $vendorDir . '/web-auth/cose-lib/src/Algorithm/Signature/RSA/PSSRSA.php', + 'Cose\\Algorithm\\Signature\\RSA\\RS1' => $vendorDir . '/web-auth/cose-lib/src/Algorithm/Signature/RSA/RS1.php', + 'Cose\\Algorithm\\Signature\\RSA\\RS256' => $vendorDir . '/web-auth/cose-lib/src/Algorithm/Signature/RSA/RS256.php', + 'Cose\\Algorithm\\Signature\\RSA\\RS384' => $vendorDir . '/web-auth/cose-lib/src/Algorithm/Signature/RSA/RS384.php', + 'Cose\\Algorithm\\Signature\\RSA\\RS512' => $vendorDir . '/web-auth/cose-lib/src/Algorithm/Signature/RSA/RS512.php', + 'Cose\\Algorithm\\Signature\\RSA\\RSA' => $vendorDir . '/web-auth/cose-lib/src/Algorithm/Signature/RSA/RSA.php', + 'Cose\\Algorithm\\Signature\\Signature' => $vendorDir . '/web-auth/cose-lib/src/Algorithm/Signature/Signature.php', + 'Cose\\Algorithms' => $vendorDir . '/web-auth/cose-lib/src/Algorithms.php', + 'Cose\\BigInteger' => $vendorDir . '/web-auth/cose-lib/src/BigInteger.php', + 'Cose\\Hash' => $vendorDir . '/web-auth/cose-lib/src/Hash.php', + 'Cose\\Key\\Ec2Key' => $vendorDir . '/web-auth/cose-lib/src/Key/Ec2Key.php', + 'Cose\\Key\\Key' => $vendorDir . '/web-auth/cose-lib/src/Key/Key.php', + 'Cose\\Key\\OkpKey' => $vendorDir . '/web-auth/cose-lib/src/Key/OkpKey.php', + 'Cose\\Key\\RsaKey' => $vendorDir . '/web-auth/cose-lib/src/Key/RsaKey.php', + 'Cose\\Key\\SymmetricKey' => $vendorDir . '/web-auth/cose-lib/src/Key/SymmetricKey.php', + 'DateError' => $vendorDir . '/symfony/polyfill-php83/Resources/stubs/DateError.php', + 'DateException' => $vendorDir . '/symfony/polyfill-php83/Resources/stubs/DateException.php', + 'DateInvalidOperationException' => $vendorDir . '/symfony/polyfill-php83/Resources/stubs/DateInvalidOperationException.php', + 'DateInvalidTimeZoneException' => $vendorDir . '/symfony/polyfill-php83/Resources/stubs/DateInvalidTimeZoneException.php', + 'DateMalformedIntervalStringException' => $vendorDir . '/symfony/polyfill-php83/Resources/stubs/DateMalformedIntervalStringException.php', + 'DateMalformedPeriodStringException' => $vendorDir . '/symfony/polyfill-php83/Resources/stubs/DateMalformedPeriodStringException.php', + 'DateMalformedStringException' => $vendorDir . '/symfony/polyfill-php83/Resources/stubs/DateMalformedStringException.php', + 'DateObjectError' => $vendorDir . '/symfony/polyfill-php83/Resources/stubs/DateObjectError.php', + 'DateRangeError' => $vendorDir . '/symfony/polyfill-php83/Resources/stubs/DateRangeError.php', + 'Deprecated' => $vendorDir . '/symfony/polyfill-php84/Resources/stubs/Deprecated.php', + 'Doctrine\\Common\\EventArgs' => $vendorDir . '/doctrine/event-manager/src/EventArgs.php', + 'Doctrine\\Common\\EventManager' => $vendorDir . '/doctrine/event-manager/src/EventManager.php', + 'Doctrine\\Common\\EventSubscriber' => $vendorDir . '/doctrine/event-manager/src/EventSubscriber.php', + 'Doctrine\\Common\\Lexer\\AbstractLexer' => $vendorDir . '/doctrine/lexer/src/AbstractLexer.php', + 'Doctrine\\Common\\Lexer\\Token' => $vendorDir . '/doctrine/lexer/src/Token.php', + 'Doctrine\\DBAL\\ArrayParameterType' => $vendorDir . '/doctrine/dbal/src/ArrayParameterType.php', + 'Doctrine\\DBAL\\ArrayParameters\\Exception' => $vendorDir . '/doctrine/dbal/src/ArrayParameters/Exception.php', + 'Doctrine\\DBAL\\ArrayParameters\\Exception\\MissingNamedParameter' => $vendorDir . '/doctrine/dbal/src/ArrayParameters/Exception/MissingNamedParameter.php', + 'Doctrine\\DBAL\\ArrayParameters\\Exception\\MissingPositionalParameter' => $vendorDir . '/doctrine/dbal/src/ArrayParameters/Exception/MissingPositionalParameter.php', + 'Doctrine\\DBAL\\Cache\\ArrayResult' => $vendorDir . '/doctrine/dbal/src/Cache/ArrayResult.php', + 'Doctrine\\DBAL\\Cache\\CacheException' => $vendorDir . '/doctrine/dbal/src/Cache/CacheException.php', + 'Doctrine\\DBAL\\Cache\\QueryCacheProfile' => $vendorDir . '/doctrine/dbal/src/Cache/QueryCacheProfile.php', + 'Doctrine\\DBAL\\ColumnCase' => $vendorDir . '/doctrine/dbal/src/ColumnCase.php', + 'Doctrine\\DBAL\\Configuration' => $vendorDir . '/doctrine/dbal/src/Configuration.php', + 'Doctrine\\DBAL\\Connection' => $vendorDir . '/doctrine/dbal/src/Connection.php', + 'Doctrine\\DBAL\\ConnectionException' => $vendorDir . '/doctrine/dbal/src/ConnectionException.php', + 'Doctrine\\DBAL\\Connections\\PrimaryReadReplicaConnection' => $vendorDir . '/doctrine/dbal/src/Connections/PrimaryReadReplicaConnection.php', + 'Doctrine\\DBAL\\Driver' => $vendorDir . '/doctrine/dbal/src/Driver.php', + 'Doctrine\\DBAL\\DriverManager' => $vendorDir . '/doctrine/dbal/src/DriverManager.php', + 'Doctrine\\DBAL\\Driver\\API\\ExceptionConverter' => $vendorDir . '/doctrine/dbal/src/Driver/API/ExceptionConverter.php', + 'Doctrine\\DBAL\\Driver\\API\\IBMDB2\\ExceptionConverter' => $vendorDir . '/doctrine/dbal/src/Driver/API/IBMDB2/ExceptionConverter.php', + 'Doctrine\\DBAL\\Driver\\API\\MySQL\\ExceptionConverter' => $vendorDir . '/doctrine/dbal/src/Driver/API/MySQL/ExceptionConverter.php', + 'Doctrine\\DBAL\\Driver\\API\\OCI\\ExceptionConverter' => $vendorDir . '/doctrine/dbal/src/Driver/API/OCI/ExceptionConverter.php', + 'Doctrine\\DBAL\\Driver\\API\\PostgreSQL\\ExceptionConverter' => $vendorDir . '/doctrine/dbal/src/Driver/API/PostgreSQL/ExceptionConverter.php', + 'Doctrine\\DBAL\\Driver\\API\\SQLSrv\\ExceptionConverter' => $vendorDir . '/doctrine/dbal/src/Driver/API/SQLSrv/ExceptionConverter.php', + 'Doctrine\\DBAL\\Driver\\API\\SQLite\\ExceptionConverter' => $vendorDir . '/doctrine/dbal/src/Driver/API/SQLite/ExceptionConverter.php', + 'Doctrine\\DBAL\\Driver\\API\\SQLite\\UserDefinedFunctions' => $vendorDir . '/doctrine/dbal/src/Driver/API/SQLite/UserDefinedFunctions.php', + 'Doctrine\\DBAL\\Driver\\AbstractDB2Driver' => $vendorDir . '/doctrine/dbal/src/Driver/AbstractDB2Driver.php', + 'Doctrine\\DBAL\\Driver\\AbstractException' => $vendorDir . '/doctrine/dbal/src/Driver/AbstractException.php', + 'Doctrine\\DBAL\\Driver\\AbstractMySQLDriver' => $vendorDir . '/doctrine/dbal/src/Driver/AbstractMySQLDriver.php', + 'Doctrine\\DBAL\\Driver\\AbstractOracleDriver' => $vendorDir . '/doctrine/dbal/src/Driver/AbstractOracleDriver.php', + 'Doctrine\\DBAL\\Driver\\AbstractOracleDriver\\EasyConnectString' => $vendorDir . '/doctrine/dbal/src/Driver/AbstractOracleDriver/EasyConnectString.php', + 'Doctrine\\DBAL\\Driver\\AbstractPostgreSQLDriver' => $vendorDir . '/doctrine/dbal/src/Driver/AbstractPostgreSQLDriver.php', + 'Doctrine\\DBAL\\Driver\\AbstractSQLServerDriver' => $vendorDir . '/doctrine/dbal/src/Driver/AbstractSQLServerDriver.php', + 'Doctrine\\DBAL\\Driver\\AbstractSQLServerDriver\\Exception\\PortWithoutHost' => $vendorDir . '/doctrine/dbal/src/Driver/AbstractSQLServerDriver/Exception/PortWithoutHost.php', + 'Doctrine\\DBAL\\Driver\\AbstractSQLiteDriver' => $vendorDir . '/doctrine/dbal/src/Driver/AbstractSQLiteDriver.php', + 'Doctrine\\DBAL\\Driver\\AbstractSQLiteDriver\\Middleware\\EnableForeignKeys' => $vendorDir . '/doctrine/dbal/src/Driver/AbstractSQLiteDriver/Middleware/EnableForeignKeys.php', + 'Doctrine\\DBAL\\Driver\\Connection' => $vendorDir . '/doctrine/dbal/src/Driver/Connection.php', + 'Doctrine\\DBAL\\Driver\\Exception' => $vendorDir . '/doctrine/dbal/src/Driver/Exception.php', + 'Doctrine\\DBAL\\Driver\\Exception\\UnknownParameterType' => $vendorDir . '/doctrine/dbal/src/Driver/Exception/UnknownParameterType.php', + 'Doctrine\\DBAL\\Driver\\FetchUtils' => $vendorDir . '/doctrine/dbal/src/Driver/FetchUtils.php', + 'Doctrine\\DBAL\\Driver\\IBMDB2\\Connection' => $vendorDir . '/doctrine/dbal/src/Driver/IBMDB2/Connection.php', + 'Doctrine\\DBAL\\Driver\\IBMDB2\\DataSourceName' => $vendorDir . '/doctrine/dbal/src/Driver/IBMDB2/DataSourceName.php', + 'Doctrine\\DBAL\\Driver\\IBMDB2\\Driver' => $vendorDir . '/doctrine/dbal/src/Driver/IBMDB2/Driver.php', + 'Doctrine\\DBAL\\Driver\\IBMDB2\\Exception\\CannotCopyStreamToStream' => $vendorDir . '/doctrine/dbal/src/Driver/IBMDB2/Exception/CannotCopyStreamToStream.php', + 'Doctrine\\DBAL\\Driver\\IBMDB2\\Exception\\CannotCreateTemporaryFile' => $vendorDir . '/doctrine/dbal/src/Driver/IBMDB2/Exception/CannotCreateTemporaryFile.php', + 'Doctrine\\DBAL\\Driver\\IBMDB2\\Exception\\ConnectionError' => $vendorDir . '/doctrine/dbal/src/Driver/IBMDB2/Exception/ConnectionError.php', + 'Doctrine\\DBAL\\Driver\\IBMDB2\\Exception\\ConnectionFailed' => $vendorDir . '/doctrine/dbal/src/Driver/IBMDB2/Exception/ConnectionFailed.php', + 'Doctrine\\DBAL\\Driver\\IBMDB2\\Exception\\Factory' => $vendorDir . '/doctrine/dbal/src/Driver/IBMDB2/Exception/Factory.php', + 'Doctrine\\DBAL\\Driver\\IBMDB2\\Exception\\PrepareFailed' => $vendorDir . '/doctrine/dbal/src/Driver/IBMDB2/Exception/PrepareFailed.php', + 'Doctrine\\DBAL\\Driver\\IBMDB2\\Exception\\StatementError' => $vendorDir . '/doctrine/dbal/src/Driver/IBMDB2/Exception/StatementError.php', + 'Doctrine\\DBAL\\Driver\\IBMDB2\\Result' => $vendorDir . '/doctrine/dbal/src/Driver/IBMDB2/Result.php', + 'Doctrine\\DBAL\\Driver\\IBMDB2\\Statement' => $vendorDir . '/doctrine/dbal/src/Driver/IBMDB2/Statement.php', + 'Doctrine\\DBAL\\Driver\\Middleware' => $vendorDir . '/doctrine/dbal/src/Driver/Middleware.php', + 'Doctrine\\DBAL\\Driver\\Middleware\\AbstractConnectionMiddleware' => $vendorDir . '/doctrine/dbal/src/Driver/Middleware/AbstractConnectionMiddleware.php', + 'Doctrine\\DBAL\\Driver\\Middleware\\AbstractDriverMiddleware' => $vendorDir . '/doctrine/dbal/src/Driver/Middleware/AbstractDriverMiddleware.php', + 'Doctrine\\DBAL\\Driver\\Middleware\\AbstractResultMiddleware' => $vendorDir . '/doctrine/dbal/src/Driver/Middleware/AbstractResultMiddleware.php', + 'Doctrine\\DBAL\\Driver\\Middleware\\AbstractStatementMiddleware' => $vendorDir . '/doctrine/dbal/src/Driver/Middleware/AbstractStatementMiddleware.php', + 'Doctrine\\DBAL\\Driver\\Mysqli\\Connection' => $vendorDir . '/doctrine/dbal/src/Driver/Mysqli/Connection.php', + 'Doctrine\\DBAL\\Driver\\Mysqli\\Driver' => $vendorDir . '/doctrine/dbal/src/Driver/Mysqli/Driver.php', + 'Doctrine\\DBAL\\Driver\\Mysqli\\Exception\\ConnectionError' => $vendorDir . '/doctrine/dbal/src/Driver/Mysqli/Exception/ConnectionError.php', + 'Doctrine\\DBAL\\Driver\\Mysqli\\Exception\\ConnectionFailed' => $vendorDir . '/doctrine/dbal/src/Driver/Mysqli/Exception/ConnectionFailed.php', + 'Doctrine\\DBAL\\Driver\\Mysqli\\Exception\\FailedReadingStreamOffset' => $vendorDir . '/doctrine/dbal/src/Driver/Mysqli/Exception/FailedReadingStreamOffset.php', + 'Doctrine\\DBAL\\Driver\\Mysqli\\Exception\\HostRequired' => $vendorDir . '/doctrine/dbal/src/Driver/Mysqli/Exception/HostRequired.php', + 'Doctrine\\DBAL\\Driver\\Mysqli\\Exception\\InvalidCharset' => $vendorDir . '/doctrine/dbal/src/Driver/Mysqli/Exception/InvalidCharset.php', + 'Doctrine\\DBAL\\Driver\\Mysqli\\Exception\\InvalidOption' => $vendorDir . '/doctrine/dbal/src/Driver/Mysqli/Exception/InvalidOption.php', + 'Doctrine\\DBAL\\Driver\\Mysqli\\Exception\\NonStreamResourceUsedAsLargeObject' => $vendorDir . '/doctrine/dbal/src/Driver/Mysqli/Exception/NonStreamResourceUsedAsLargeObject.php', + 'Doctrine\\DBAL\\Driver\\Mysqli\\Exception\\StatementError' => $vendorDir . '/doctrine/dbal/src/Driver/Mysqli/Exception/StatementError.php', + 'Doctrine\\DBAL\\Driver\\Mysqli\\Initializer' => $vendorDir . '/doctrine/dbal/src/Driver/Mysqli/Initializer.php', + 'Doctrine\\DBAL\\Driver\\Mysqli\\Initializer\\Charset' => $vendorDir . '/doctrine/dbal/src/Driver/Mysqli/Initializer/Charset.php', + 'Doctrine\\DBAL\\Driver\\Mysqli\\Initializer\\Options' => $vendorDir . '/doctrine/dbal/src/Driver/Mysqli/Initializer/Options.php', + 'Doctrine\\DBAL\\Driver\\Mysqli\\Initializer\\Secure' => $vendorDir . '/doctrine/dbal/src/Driver/Mysqli/Initializer/Secure.php', + 'Doctrine\\DBAL\\Driver\\Mysqli\\Result' => $vendorDir . '/doctrine/dbal/src/Driver/Mysqli/Result.php', + 'Doctrine\\DBAL\\Driver\\Mysqli\\Statement' => $vendorDir . '/doctrine/dbal/src/Driver/Mysqli/Statement.php', + 'Doctrine\\DBAL\\Driver\\OCI8\\Connection' => $vendorDir . '/doctrine/dbal/src/Driver/OCI8/Connection.php', + 'Doctrine\\DBAL\\Driver\\OCI8\\ConvertPositionalToNamedPlaceholders' => $vendorDir . '/doctrine/dbal/src/Driver/OCI8/ConvertPositionalToNamedPlaceholders.php', + 'Doctrine\\DBAL\\Driver\\OCI8\\Driver' => $vendorDir . '/doctrine/dbal/src/Driver/OCI8/Driver.php', + 'Doctrine\\DBAL\\Driver\\OCI8\\Exception\\ConnectionFailed' => $vendorDir . '/doctrine/dbal/src/Driver/OCI8/Exception/ConnectionFailed.php', + 'Doctrine\\DBAL\\Driver\\OCI8\\Exception\\Error' => $vendorDir . '/doctrine/dbal/src/Driver/OCI8/Exception/Error.php', + 'Doctrine\\DBAL\\Driver\\OCI8\\Exception\\InvalidConfiguration' => $vendorDir . '/doctrine/dbal/src/Driver/OCI8/Exception/InvalidConfiguration.php', + 'Doctrine\\DBAL\\Driver\\OCI8\\Exception\\NonTerminatedStringLiteral' => $vendorDir . '/doctrine/dbal/src/Driver/OCI8/Exception/NonTerminatedStringLiteral.php', + 'Doctrine\\DBAL\\Driver\\OCI8\\Exception\\SequenceDoesNotExist' => $vendorDir . '/doctrine/dbal/src/Driver/OCI8/Exception/SequenceDoesNotExist.php', + 'Doctrine\\DBAL\\Driver\\OCI8\\Exception\\UnknownParameterIndex' => $vendorDir . '/doctrine/dbal/src/Driver/OCI8/Exception/UnknownParameterIndex.php', + 'Doctrine\\DBAL\\Driver\\OCI8\\ExecutionMode' => $vendorDir . '/doctrine/dbal/src/Driver/OCI8/ExecutionMode.php', + 'Doctrine\\DBAL\\Driver\\OCI8\\Middleware\\InitializeSession' => $vendorDir . '/doctrine/dbal/src/Driver/OCI8/Middleware/InitializeSession.php', + 'Doctrine\\DBAL\\Driver\\OCI8\\Result' => $vendorDir . '/doctrine/dbal/src/Driver/OCI8/Result.php', + 'Doctrine\\DBAL\\Driver\\OCI8\\Statement' => $vendorDir . '/doctrine/dbal/src/Driver/OCI8/Statement.php', + 'Doctrine\\DBAL\\Driver\\PDO\\Connection' => $vendorDir . '/doctrine/dbal/src/Driver/PDO/Connection.php', + 'Doctrine\\DBAL\\Driver\\PDO\\Exception' => $vendorDir . '/doctrine/dbal/src/Driver/PDO/Exception.php', + 'Doctrine\\DBAL\\Driver\\PDO\\MySQL\\Driver' => $vendorDir . '/doctrine/dbal/src/Driver/PDO/MySQL/Driver.php', + 'Doctrine\\DBAL\\Driver\\PDO\\OCI\\Driver' => $vendorDir . '/doctrine/dbal/src/Driver/PDO/OCI/Driver.php', + 'Doctrine\\DBAL\\Driver\\PDO\\PDOConnect' => $vendorDir . '/doctrine/dbal/src/Driver/PDO/PDOConnect.php', + 'Doctrine\\DBAL\\Driver\\PDO\\PDOException' => $vendorDir . '/doctrine/dbal/src/Driver/PDO/PDOException.php', + 'Doctrine\\DBAL\\Driver\\PDO\\ParameterTypeMap' => $vendorDir . '/doctrine/dbal/src/Driver/PDO/ParameterTypeMap.php', + 'Doctrine\\DBAL\\Driver\\PDO\\PgSQL\\Driver' => $vendorDir . '/doctrine/dbal/src/Driver/PDO/PgSQL/Driver.php', + 'Doctrine\\DBAL\\Driver\\PDO\\Result' => $vendorDir . '/doctrine/dbal/src/Driver/PDO/Result.php', + 'Doctrine\\DBAL\\Driver\\PDO\\SQLSrv\\Connection' => $vendorDir . '/doctrine/dbal/src/Driver/PDO/SQLSrv/Connection.php', + 'Doctrine\\DBAL\\Driver\\PDO\\SQLSrv\\Driver' => $vendorDir . '/doctrine/dbal/src/Driver/PDO/SQLSrv/Driver.php', + 'Doctrine\\DBAL\\Driver\\PDO\\SQLSrv\\Statement' => $vendorDir . '/doctrine/dbal/src/Driver/PDO/SQLSrv/Statement.php', + 'Doctrine\\DBAL\\Driver\\PDO\\SQLite\\Driver' => $vendorDir . '/doctrine/dbal/src/Driver/PDO/SQLite/Driver.php', + 'Doctrine\\DBAL\\Driver\\PDO\\Statement' => $vendorDir . '/doctrine/dbal/src/Driver/PDO/Statement.php', + 'Doctrine\\DBAL\\Driver\\PgSQL\\Connection' => $vendorDir . '/doctrine/dbal/src/Driver/PgSQL/Connection.php', + 'Doctrine\\DBAL\\Driver\\PgSQL\\ConvertParameters' => $vendorDir . '/doctrine/dbal/src/Driver/PgSQL/ConvertParameters.php', + 'Doctrine\\DBAL\\Driver\\PgSQL\\Driver' => $vendorDir . '/doctrine/dbal/src/Driver/PgSQL/Driver.php', + 'Doctrine\\DBAL\\Driver\\PgSQL\\Exception' => $vendorDir . '/doctrine/dbal/src/Driver/PgSQL/Exception.php', + 'Doctrine\\DBAL\\Driver\\PgSQL\\Exception\\UnexpectedValue' => $vendorDir . '/doctrine/dbal/src/Driver/PgSQL/Exception/UnexpectedValue.php', + 'Doctrine\\DBAL\\Driver\\PgSQL\\Exception\\UnknownParameter' => $vendorDir . '/doctrine/dbal/src/Driver/PgSQL/Exception/UnknownParameter.php', + 'Doctrine\\DBAL\\Driver\\PgSQL\\Result' => $vendorDir . '/doctrine/dbal/src/Driver/PgSQL/Result.php', + 'Doctrine\\DBAL\\Driver\\PgSQL\\Statement' => $vendorDir . '/doctrine/dbal/src/Driver/PgSQL/Statement.php', + 'Doctrine\\DBAL\\Driver\\Result' => $vendorDir . '/doctrine/dbal/src/Driver/Result.php', + 'Doctrine\\DBAL\\Driver\\SQLSrv\\Connection' => $vendorDir . '/doctrine/dbal/src/Driver/SQLSrv/Connection.php', + 'Doctrine\\DBAL\\Driver\\SQLSrv\\Driver' => $vendorDir . '/doctrine/dbal/src/Driver/SQLSrv/Driver.php', + 'Doctrine\\DBAL\\Driver\\SQLSrv\\Exception\\Error' => $vendorDir . '/doctrine/dbal/src/Driver/SQLSrv/Exception/Error.php', + 'Doctrine\\DBAL\\Driver\\SQLSrv\\Result' => $vendorDir . '/doctrine/dbal/src/Driver/SQLSrv/Result.php', + 'Doctrine\\DBAL\\Driver\\SQLSrv\\Statement' => $vendorDir . '/doctrine/dbal/src/Driver/SQLSrv/Statement.php', + 'Doctrine\\DBAL\\Driver\\SQLite3\\Connection' => $vendorDir . '/doctrine/dbal/src/Driver/SQLite3/Connection.php', + 'Doctrine\\DBAL\\Driver\\SQLite3\\Driver' => $vendorDir . '/doctrine/dbal/src/Driver/SQLite3/Driver.php', + 'Doctrine\\DBAL\\Driver\\SQLite3\\Exception' => $vendorDir . '/doctrine/dbal/src/Driver/SQLite3/Exception.php', + 'Doctrine\\DBAL\\Driver\\SQLite3\\Result' => $vendorDir . '/doctrine/dbal/src/Driver/SQLite3/Result.php', + 'Doctrine\\DBAL\\Driver\\SQLite3\\Statement' => $vendorDir . '/doctrine/dbal/src/Driver/SQLite3/Statement.php', + 'Doctrine\\DBAL\\Driver\\ServerInfoAwareConnection' => $vendorDir . '/doctrine/dbal/src/Driver/ServerInfoAwareConnection.php', + 'Doctrine\\DBAL\\Driver\\Statement' => $vendorDir . '/doctrine/dbal/src/Driver/Statement.php', + 'Doctrine\\DBAL\\Event\\ConnectionEventArgs' => $vendorDir . '/doctrine/dbal/src/Event/ConnectionEventArgs.php', + 'Doctrine\\DBAL\\Event\\Listeners\\OracleSessionInit' => $vendorDir . '/doctrine/dbal/src/Event/Listeners/OracleSessionInit.php', + 'Doctrine\\DBAL\\Event\\Listeners\\SQLSessionInit' => $vendorDir . '/doctrine/dbal/src/Event/Listeners/SQLSessionInit.php', + 'Doctrine\\DBAL\\Event\\Listeners\\SQLiteSessionInit' => $vendorDir . '/doctrine/dbal/src/Event/Listeners/SQLiteSessionInit.php', + 'Doctrine\\DBAL\\Event\\SchemaAlterTableAddColumnEventArgs' => $vendorDir . '/doctrine/dbal/src/Event/SchemaAlterTableAddColumnEventArgs.php', + 'Doctrine\\DBAL\\Event\\SchemaAlterTableChangeColumnEventArgs' => $vendorDir . '/doctrine/dbal/src/Event/SchemaAlterTableChangeColumnEventArgs.php', + 'Doctrine\\DBAL\\Event\\SchemaAlterTableEventArgs' => $vendorDir . '/doctrine/dbal/src/Event/SchemaAlterTableEventArgs.php', + 'Doctrine\\DBAL\\Event\\SchemaAlterTableRemoveColumnEventArgs' => $vendorDir . '/doctrine/dbal/src/Event/SchemaAlterTableRemoveColumnEventArgs.php', + 'Doctrine\\DBAL\\Event\\SchemaAlterTableRenameColumnEventArgs' => $vendorDir . '/doctrine/dbal/src/Event/SchemaAlterTableRenameColumnEventArgs.php', + 'Doctrine\\DBAL\\Event\\SchemaColumnDefinitionEventArgs' => $vendorDir . '/doctrine/dbal/src/Event/SchemaColumnDefinitionEventArgs.php', + 'Doctrine\\DBAL\\Event\\SchemaCreateTableColumnEventArgs' => $vendorDir . '/doctrine/dbal/src/Event/SchemaCreateTableColumnEventArgs.php', + 'Doctrine\\DBAL\\Event\\SchemaCreateTableEventArgs' => $vendorDir . '/doctrine/dbal/src/Event/SchemaCreateTableEventArgs.php', + 'Doctrine\\DBAL\\Event\\SchemaDropTableEventArgs' => $vendorDir . '/doctrine/dbal/src/Event/SchemaDropTableEventArgs.php', + 'Doctrine\\DBAL\\Event\\SchemaEventArgs' => $vendorDir . '/doctrine/dbal/src/Event/SchemaEventArgs.php', + 'Doctrine\\DBAL\\Event\\SchemaIndexDefinitionEventArgs' => $vendorDir . '/doctrine/dbal/src/Event/SchemaIndexDefinitionEventArgs.php', + 'Doctrine\\DBAL\\Event\\TransactionBeginEventArgs' => $vendorDir . '/doctrine/dbal/src/Event/TransactionBeginEventArgs.php', + 'Doctrine\\DBAL\\Event\\TransactionCommitEventArgs' => $vendorDir . '/doctrine/dbal/src/Event/TransactionCommitEventArgs.php', + 'Doctrine\\DBAL\\Event\\TransactionEventArgs' => $vendorDir . '/doctrine/dbal/src/Event/TransactionEventArgs.php', + 'Doctrine\\DBAL\\Event\\TransactionRollBackEventArgs' => $vendorDir . '/doctrine/dbal/src/Event/TransactionRollBackEventArgs.php', + 'Doctrine\\DBAL\\Events' => $vendorDir . '/doctrine/dbal/src/Events.php', + 'Doctrine\\DBAL\\Exception' => $vendorDir . '/doctrine/dbal/src/Exception.php', + 'Doctrine\\DBAL\\Exception\\ConnectionException' => $vendorDir . '/doctrine/dbal/src/Exception/ConnectionException.php', + 'Doctrine\\DBAL\\Exception\\ConnectionLost' => $vendorDir . '/doctrine/dbal/src/Exception/ConnectionLost.php', + 'Doctrine\\DBAL\\Exception\\ConstraintViolationException' => $vendorDir . '/doctrine/dbal/src/Exception/ConstraintViolationException.php', + 'Doctrine\\DBAL\\Exception\\DatabaseDoesNotExist' => $vendorDir . '/doctrine/dbal/src/Exception/DatabaseDoesNotExist.php', + 'Doctrine\\DBAL\\Exception\\DatabaseObjectExistsException' => $vendorDir . '/doctrine/dbal/src/Exception/DatabaseObjectExistsException.php', + 'Doctrine\\DBAL\\Exception\\DatabaseObjectNotFoundException' => $vendorDir . '/doctrine/dbal/src/Exception/DatabaseObjectNotFoundException.php', + 'Doctrine\\DBAL\\Exception\\DatabaseRequired' => $vendorDir . '/doctrine/dbal/src/Exception/DatabaseRequired.php', + 'Doctrine\\DBAL\\Exception\\DeadlockException' => $vendorDir . '/doctrine/dbal/src/Exception/DeadlockException.php', + 'Doctrine\\DBAL\\Exception\\DriverException' => $vendorDir . '/doctrine/dbal/src/Exception/DriverException.php', + 'Doctrine\\DBAL\\Exception\\ForeignKeyConstraintViolationException' => $vendorDir . '/doctrine/dbal/src/Exception/ForeignKeyConstraintViolationException.php', + 'Doctrine\\DBAL\\Exception\\InvalidArgumentException' => $vendorDir . '/doctrine/dbal/src/Exception/InvalidArgumentException.php', + 'Doctrine\\DBAL\\Exception\\InvalidFieldNameException' => $vendorDir . '/doctrine/dbal/src/Exception/InvalidFieldNameException.php', + 'Doctrine\\DBAL\\Exception\\InvalidLockMode' => $vendorDir . '/doctrine/dbal/src/Exception/InvalidLockMode.php', + 'Doctrine\\DBAL\\Exception\\LockWaitTimeoutException' => $vendorDir . '/doctrine/dbal/src/Exception/LockWaitTimeoutException.php', + 'Doctrine\\DBAL\\Exception\\MalformedDsnException' => $vendorDir . '/doctrine/dbal/src/Exception/MalformedDsnException.php', + 'Doctrine\\DBAL\\Exception\\NoKeyValue' => $vendorDir . '/doctrine/dbal/src/Exception/NoKeyValue.php', + 'Doctrine\\DBAL\\Exception\\NonUniqueFieldNameException' => $vendorDir . '/doctrine/dbal/src/Exception/NonUniqueFieldNameException.php', + 'Doctrine\\DBAL\\Exception\\NotNullConstraintViolationException' => $vendorDir . '/doctrine/dbal/src/Exception/NotNullConstraintViolationException.php', + 'Doctrine\\DBAL\\Exception\\ReadOnlyException' => $vendorDir . '/doctrine/dbal/src/Exception/ReadOnlyException.php', + 'Doctrine\\DBAL\\Exception\\RetryableException' => $vendorDir . '/doctrine/dbal/src/Exception/RetryableException.php', + 'Doctrine\\DBAL\\Exception\\SchemaDoesNotExist' => $vendorDir . '/doctrine/dbal/src/Exception/SchemaDoesNotExist.php', + 'Doctrine\\DBAL\\Exception\\ServerException' => $vendorDir . '/doctrine/dbal/src/Exception/ServerException.php', + 'Doctrine\\DBAL\\Exception\\SyntaxErrorException' => $vendorDir . '/doctrine/dbal/src/Exception/SyntaxErrorException.php', + 'Doctrine\\DBAL\\Exception\\TableExistsException' => $vendorDir . '/doctrine/dbal/src/Exception/TableExistsException.php', + 'Doctrine\\DBAL\\Exception\\TableNotFoundException' => $vendorDir . '/doctrine/dbal/src/Exception/TableNotFoundException.php', + 'Doctrine\\DBAL\\Exception\\TransactionRolledBack' => $vendorDir . '/doctrine/dbal/src/Exception/TransactionRolledBack.php', + 'Doctrine\\DBAL\\Exception\\UniqueConstraintViolationException' => $vendorDir . '/doctrine/dbal/src/Exception/UniqueConstraintViolationException.php', + 'Doctrine\\DBAL\\ExpandArrayParameters' => $vendorDir . '/doctrine/dbal/src/ExpandArrayParameters.php', + 'Doctrine\\DBAL\\FetchMode' => $vendorDir . '/doctrine/dbal/src/FetchMode.php', + 'Doctrine\\DBAL\\Id\\TableGenerator' => $vendorDir . '/doctrine/dbal/src/Id/TableGenerator.php', + 'Doctrine\\DBAL\\Id\\TableGeneratorSchemaVisitor' => $vendorDir . '/doctrine/dbal/src/Id/TableGeneratorSchemaVisitor.php', + 'Doctrine\\DBAL\\LockMode' => $vendorDir . '/doctrine/dbal/src/LockMode.php', + 'Doctrine\\DBAL\\Logging\\Connection' => $vendorDir . '/doctrine/dbal/src/Logging/Connection.php', + 'Doctrine\\DBAL\\Logging\\DebugStack' => $vendorDir . '/doctrine/dbal/src/Logging/DebugStack.php', + 'Doctrine\\DBAL\\Logging\\Driver' => $vendorDir . '/doctrine/dbal/src/Logging/Driver.php', + 'Doctrine\\DBAL\\Logging\\LoggerChain' => $vendorDir . '/doctrine/dbal/src/Logging/LoggerChain.php', + 'Doctrine\\DBAL\\Logging\\Middleware' => $vendorDir . '/doctrine/dbal/src/Logging/Middleware.php', + 'Doctrine\\DBAL\\Logging\\SQLLogger' => $vendorDir . '/doctrine/dbal/src/Logging/SQLLogger.php', + 'Doctrine\\DBAL\\Logging\\Statement' => $vendorDir . '/doctrine/dbal/src/Logging/Statement.php', + 'Doctrine\\DBAL\\ParameterType' => $vendorDir . '/doctrine/dbal/src/ParameterType.php', + 'Doctrine\\DBAL\\Platforms\\AbstractMySQLPlatform' => $vendorDir . '/doctrine/dbal/src/Platforms/AbstractMySQLPlatform.php', + 'Doctrine\\DBAL\\Platforms\\AbstractPlatform' => $vendorDir . '/doctrine/dbal/src/Platforms/AbstractPlatform.php', + 'Doctrine\\DBAL\\Platforms\\DB2111Platform' => $vendorDir . '/doctrine/dbal/src/Platforms/DB2111Platform.php', + 'Doctrine\\DBAL\\Platforms\\DB2Platform' => $vendorDir . '/doctrine/dbal/src/Platforms/DB2Platform.php', + 'Doctrine\\DBAL\\Platforms\\DateIntervalUnit' => $vendorDir . '/doctrine/dbal/src/Platforms/DateIntervalUnit.php', + 'Doctrine\\DBAL\\Platforms\\Keywords\\DB2Keywords' => $vendorDir . '/doctrine/dbal/src/Platforms/Keywords/DB2Keywords.php', + 'Doctrine\\DBAL\\Platforms\\Keywords\\KeywordList' => $vendorDir . '/doctrine/dbal/src/Platforms/Keywords/KeywordList.php', + 'Doctrine\\DBAL\\Platforms\\Keywords\\MariaDBKeywords' => $vendorDir . '/doctrine/dbal/src/Platforms/Keywords/MariaDBKeywords.php', + 'Doctrine\\DBAL\\Platforms\\Keywords\\MariaDb102Keywords' => $vendorDir . '/doctrine/dbal/src/Platforms/Keywords/MariaDb102Keywords.php', + 'Doctrine\\DBAL\\Platforms\\Keywords\\MariaDb117Keywords' => $vendorDir . '/doctrine/dbal/src/Platforms/Keywords/MariaDb117Keywords.php', + 'Doctrine\\DBAL\\Platforms\\Keywords\\MySQL57Keywords' => $vendorDir . '/doctrine/dbal/src/Platforms/Keywords/MySQL57Keywords.php', + 'Doctrine\\DBAL\\Platforms\\Keywords\\MySQL80Keywords' => $vendorDir . '/doctrine/dbal/src/Platforms/Keywords/MySQL80Keywords.php', + 'Doctrine\\DBAL\\Platforms\\Keywords\\MySQL84Keywords' => $vendorDir . '/doctrine/dbal/src/Platforms/Keywords/MySQL84Keywords.php', + 'Doctrine\\DBAL\\Platforms\\Keywords\\MySQLKeywords' => $vendorDir . '/doctrine/dbal/src/Platforms/Keywords/MySQLKeywords.php', + 'Doctrine\\DBAL\\Platforms\\Keywords\\OracleKeywords' => $vendorDir . '/doctrine/dbal/src/Platforms/Keywords/OracleKeywords.php', + 'Doctrine\\DBAL\\Platforms\\Keywords\\PostgreSQL100Keywords' => $vendorDir . '/doctrine/dbal/src/Platforms/Keywords/PostgreSQL100Keywords.php', + 'Doctrine\\DBAL\\Platforms\\Keywords\\PostgreSQL94Keywords' => $vendorDir . '/doctrine/dbal/src/Platforms/Keywords/PostgreSQL94Keywords.php', + 'Doctrine\\DBAL\\Platforms\\Keywords\\PostgreSQLKeywords' => $vendorDir . '/doctrine/dbal/src/Platforms/Keywords/PostgreSQLKeywords.php', + 'Doctrine\\DBAL\\Platforms\\Keywords\\ReservedKeywordsValidator' => $vendorDir . '/doctrine/dbal/src/Platforms/Keywords/ReservedKeywordsValidator.php', + 'Doctrine\\DBAL\\Platforms\\Keywords\\SQLServer2012Keywords' => $vendorDir . '/doctrine/dbal/src/Platforms/Keywords/SQLServer2012Keywords.php', + 'Doctrine\\DBAL\\Platforms\\Keywords\\SQLServerKeywords' => $vendorDir . '/doctrine/dbal/src/Platforms/Keywords/SQLServerKeywords.php', + 'Doctrine\\DBAL\\Platforms\\Keywords\\SQLiteKeywords' => $vendorDir . '/doctrine/dbal/src/Platforms/Keywords/SQLiteKeywords.php', + 'Doctrine\\DBAL\\Platforms\\MariaDBPlatform' => $vendorDir . '/doctrine/dbal/src/Platforms/MariaDBPlatform.php', + 'Doctrine\\DBAL\\Platforms\\MariaDb1010Platform' => $vendorDir . '/doctrine/dbal/src/Platforms/MariaDb1010Platform.php', + 'Doctrine\\DBAL\\Platforms\\MariaDb1027Platform' => $vendorDir . '/doctrine/dbal/src/Platforms/MariaDb1027Platform.php', + 'Doctrine\\DBAL\\Platforms\\MariaDb1043Platform' => $vendorDir . '/doctrine/dbal/src/Platforms/MariaDb1043Platform.php', + 'Doctrine\\DBAL\\Platforms\\MariaDb1052Platform' => $vendorDir . '/doctrine/dbal/src/Platforms/MariaDb1052Platform.php', + 'Doctrine\\DBAL\\Platforms\\MariaDb1060Platform' => $vendorDir . '/doctrine/dbal/src/Platforms/MariaDb1060Platform.php', + 'Doctrine\\DBAL\\Platforms\\MariaDb110700Platform' => $vendorDir . '/doctrine/dbal/src/Platforms/MariaDb110700Platform.php', + 'Doctrine\\DBAL\\Platforms\\MySQL57Platform' => $vendorDir . '/doctrine/dbal/src/Platforms/MySQL57Platform.php', + 'Doctrine\\DBAL\\Platforms\\MySQL80Platform' => $vendorDir . '/doctrine/dbal/src/Platforms/MySQL80Platform.php', + 'Doctrine\\DBAL\\Platforms\\MySQL84Platform' => $vendorDir . '/doctrine/dbal/src/Platforms/MySQL84Platform.php', + 'Doctrine\\DBAL\\Platforms\\MySQLPlatform' => $vendorDir . '/doctrine/dbal/src/Platforms/MySQLPlatform.php', + 'Doctrine\\DBAL\\Platforms\\MySQL\\CollationMetadataProvider' => $vendorDir . '/doctrine/dbal/src/Platforms/MySQL/CollationMetadataProvider.php', + 'Doctrine\\DBAL\\Platforms\\MySQL\\CollationMetadataProvider\\CachingCollationMetadataProvider' => $vendorDir . '/doctrine/dbal/src/Platforms/MySQL/CollationMetadataProvider/CachingCollationMetadataProvider.php', + 'Doctrine\\DBAL\\Platforms\\MySQL\\CollationMetadataProvider\\ConnectionCollationMetadataProvider' => $vendorDir . '/doctrine/dbal/src/Platforms/MySQL/CollationMetadataProvider/ConnectionCollationMetadataProvider.php', + 'Doctrine\\DBAL\\Platforms\\MySQL\\Comparator' => $vendorDir . '/doctrine/dbal/src/Platforms/MySQL/Comparator.php', + 'Doctrine\\DBAL\\Platforms\\OraclePlatform' => $vendorDir . '/doctrine/dbal/src/Platforms/OraclePlatform.php', + 'Doctrine\\DBAL\\Platforms\\PostgreSQL100Platform' => $vendorDir . '/doctrine/dbal/src/Platforms/PostgreSQL100Platform.php', + 'Doctrine\\DBAL\\Platforms\\PostgreSQL120Platform' => $vendorDir . '/doctrine/dbal/src/Platforms/PostgreSQL120Platform.php', + 'Doctrine\\DBAL\\Platforms\\PostgreSQL94Platform' => $vendorDir . '/doctrine/dbal/src/Platforms/PostgreSQL94Platform.php', + 'Doctrine\\DBAL\\Platforms\\PostgreSQLPlatform' => $vendorDir . '/doctrine/dbal/src/Platforms/PostgreSQLPlatform.php', + 'Doctrine\\DBAL\\Platforms\\SQLServer2012Platform' => $vendorDir . '/doctrine/dbal/src/Platforms/SQLServer2012Platform.php', + 'Doctrine\\DBAL\\Platforms\\SQLServerPlatform' => $vendorDir . '/doctrine/dbal/src/Platforms/SQLServerPlatform.php', + 'Doctrine\\DBAL\\Platforms\\SQLServer\\Comparator' => $vendorDir . '/doctrine/dbal/src/Platforms/SQLServer/Comparator.php', + 'Doctrine\\DBAL\\Platforms\\SQLServer\\SQL\\Builder\\SQLServerSelectSQLBuilder' => $vendorDir . '/doctrine/dbal/src/Platforms/SQLServer/SQL/Builder/SQLServerSelectSQLBuilder.php', + 'Doctrine\\DBAL\\Platforms\\SQLite\\Comparator' => $vendorDir . '/doctrine/dbal/src/Platforms/SQLite/Comparator.php', + 'Doctrine\\DBAL\\Platforms\\SqlitePlatform' => $vendorDir . '/doctrine/dbal/src/Platforms/SqlitePlatform.php', + 'Doctrine\\DBAL\\Platforms\\TrimMode' => $vendorDir . '/doctrine/dbal/src/Platforms/TrimMode.php', + 'Doctrine\\DBAL\\Portability\\Connection' => $vendorDir . '/doctrine/dbal/src/Portability/Connection.php', + 'Doctrine\\DBAL\\Portability\\Converter' => $vendorDir . '/doctrine/dbal/src/Portability/Converter.php', + 'Doctrine\\DBAL\\Portability\\Driver' => $vendorDir . '/doctrine/dbal/src/Portability/Driver.php', + 'Doctrine\\DBAL\\Portability\\Middleware' => $vendorDir . '/doctrine/dbal/src/Portability/Middleware.php', + 'Doctrine\\DBAL\\Portability\\OptimizeFlags' => $vendorDir . '/doctrine/dbal/src/Portability/OptimizeFlags.php', + 'Doctrine\\DBAL\\Portability\\Result' => $vendorDir . '/doctrine/dbal/src/Portability/Result.php', + 'Doctrine\\DBAL\\Portability\\Statement' => $vendorDir . '/doctrine/dbal/src/Portability/Statement.php', + 'Doctrine\\DBAL\\Query' => $vendorDir . '/doctrine/dbal/src/Query.php', + 'Doctrine\\DBAL\\Query\\Expression\\CompositeExpression' => $vendorDir . '/doctrine/dbal/src/Query/Expression/CompositeExpression.php', + 'Doctrine\\DBAL\\Query\\Expression\\ExpressionBuilder' => $vendorDir . '/doctrine/dbal/src/Query/Expression/ExpressionBuilder.php', + 'Doctrine\\DBAL\\Query\\ForUpdate' => $vendorDir . '/doctrine/dbal/src/Query/ForUpdate.php', + 'Doctrine\\DBAL\\Query\\ForUpdate\\ConflictResolutionMode' => $vendorDir . '/doctrine/dbal/src/Query/ForUpdate/ConflictResolutionMode.php', + 'Doctrine\\DBAL\\Query\\Limit' => $vendorDir . '/doctrine/dbal/src/Query/Limit.php', + 'Doctrine\\DBAL\\Query\\QueryBuilder' => $vendorDir . '/doctrine/dbal/src/Query/QueryBuilder.php', + 'Doctrine\\DBAL\\Query\\QueryException' => $vendorDir . '/doctrine/dbal/src/Query/QueryException.php', + 'Doctrine\\DBAL\\Query\\SelectQuery' => $vendorDir . '/doctrine/dbal/src/Query/SelectQuery.php', + 'Doctrine\\DBAL\\Result' => $vendorDir . '/doctrine/dbal/src/Result.php', + 'Doctrine\\DBAL\\SQL\\Builder\\CreateSchemaObjectsSQLBuilder' => $vendorDir . '/doctrine/dbal/src/SQL/Builder/CreateSchemaObjectsSQLBuilder.php', + 'Doctrine\\DBAL\\SQL\\Builder\\DefaultSelectSQLBuilder' => $vendorDir . '/doctrine/dbal/src/SQL/Builder/DefaultSelectSQLBuilder.php', + 'Doctrine\\DBAL\\SQL\\Builder\\DropSchemaObjectsSQLBuilder' => $vendorDir . '/doctrine/dbal/src/SQL/Builder/DropSchemaObjectsSQLBuilder.php', + 'Doctrine\\DBAL\\SQL\\Builder\\SelectSQLBuilder' => $vendorDir . '/doctrine/dbal/src/SQL/Builder/SelectSQLBuilder.php', + 'Doctrine\\DBAL\\SQL\\Parser' => $vendorDir . '/doctrine/dbal/src/SQL/Parser.php', + 'Doctrine\\DBAL\\SQL\\Parser\\Exception' => $vendorDir . '/doctrine/dbal/src/SQL/Parser/Exception.php', + 'Doctrine\\DBAL\\SQL\\Parser\\Exception\\RegularExpressionError' => $vendorDir . '/doctrine/dbal/src/SQL/Parser/Exception/RegularExpressionError.php', + 'Doctrine\\DBAL\\SQL\\Parser\\Visitor' => $vendorDir . '/doctrine/dbal/src/SQL/Parser/Visitor.php', + 'Doctrine\\DBAL\\Schema\\AbstractAsset' => $vendorDir . '/doctrine/dbal/src/Schema/AbstractAsset.php', + 'Doctrine\\DBAL\\Schema\\AbstractSchemaManager' => $vendorDir . '/doctrine/dbal/src/Schema/AbstractSchemaManager.php', + 'Doctrine\\DBAL\\Schema\\Column' => $vendorDir . '/doctrine/dbal/src/Schema/Column.php', + 'Doctrine\\DBAL\\Schema\\ColumnDiff' => $vendorDir . '/doctrine/dbal/src/Schema/ColumnDiff.php', + 'Doctrine\\DBAL\\Schema\\Comparator' => $vendorDir . '/doctrine/dbal/src/Schema/Comparator.php', + 'Doctrine\\DBAL\\Schema\\Constraint' => $vendorDir . '/doctrine/dbal/src/Schema/Constraint.php', + 'Doctrine\\DBAL\\Schema\\DB2SchemaManager' => $vendorDir . '/doctrine/dbal/src/Schema/DB2SchemaManager.php', + 'Doctrine\\DBAL\\Schema\\DefaultSchemaManagerFactory' => $vendorDir . '/doctrine/dbal/src/Schema/DefaultSchemaManagerFactory.php', + 'Doctrine\\DBAL\\Schema\\Exception\\ColumnAlreadyExists' => $vendorDir . '/doctrine/dbal/src/Schema/Exception/ColumnAlreadyExists.php', + 'Doctrine\\DBAL\\Schema\\Exception\\ColumnDoesNotExist' => $vendorDir . '/doctrine/dbal/src/Schema/Exception/ColumnDoesNotExist.php', + 'Doctrine\\DBAL\\Schema\\Exception\\ForeignKeyDoesNotExist' => $vendorDir . '/doctrine/dbal/src/Schema/Exception/ForeignKeyDoesNotExist.php', + 'Doctrine\\DBAL\\Schema\\Exception\\IndexAlreadyExists' => $vendorDir . '/doctrine/dbal/src/Schema/Exception/IndexAlreadyExists.php', + 'Doctrine\\DBAL\\Schema\\Exception\\IndexDoesNotExist' => $vendorDir . '/doctrine/dbal/src/Schema/Exception/IndexDoesNotExist.php', + 'Doctrine\\DBAL\\Schema\\Exception\\IndexNameInvalid' => $vendorDir . '/doctrine/dbal/src/Schema/Exception/IndexNameInvalid.php', + 'Doctrine\\DBAL\\Schema\\Exception\\InvalidTableName' => $vendorDir . '/doctrine/dbal/src/Schema/Exception/InvalidTableName.php', + 'Doctrine\\DBAL\\Schema\\Exception\\NamedForeignKeyRequired' => $vendorDir . '/doctrine/dbal/src/Schema/Exception/NamedForeignKeyRequired.php', + 'Doctrine\\DBAL\\Schema\\Exception\\NamespaceAlreadyExists' => $vendorDir . '/doctrine/dbal/src/Schema/Exception/NamespaceAlreadyExists.php', + 'Doctrine\\DBAL\\Schema\\Exception\\SequenceAlreadyExists' => $vendorDir . '/doctrine/dbal/src/Schema/Exception/SequenceAlreadyExists.php', + 'Doctrine\\DBAL\\Schema\\Exception\\SequenceDoesNotExist' => $vendorDir . '/doctrine/dbal/src/Schema/Exception/SequenceDoesNotExist.php', + 'Doctrine\\DBAL\\Schema\\Exception\\TableAlreadyExists' => $vendorDir . '/doctrine/dbal/src/Schema/Exception/TableAlreadyExists.php', + 'Doctrine\\DBAL\\Schema\\Exception\\TableDoesNotExist' => $vendorDir . '/doctrine/dbal/src/Schema/Exception/TableDoesNotExist.php', + 'Doctrine\\DBAL\\Schema\\Exception\\UniqueConstraintDoesNotExist' => $vendorDir . '/doctrine/dbal/src/Schema/Exception/UniqueConstraintDoesNotExist.php', + 'Doctrine\\DBAL\\Schema\\Exception\\UnknownColumnOption' => $vendorDir . '/doctrine/dbal/src/Schema/Exception/UnknownColumnOption.php', + 'Doctrine\\DBAL\\Schema\\ForeignKeyConstraint' => $vendorDir . '/doctrine/dbal/src/Schema/ForeignKeyConstraint.php', + 'Doctrine\\DBAL\\Schema\\Identifier' => $vendorDir . '/doctrine/dbal/src/Schema/Identifier.php', + 'Doctrine\\DBAL\\Schema\\Index' => $vendorDir . '/doctrine/dbal/src/Schema/Index.php', + 'Doctrine\\DBAL\\Schema\\LegacySchemaManagerFactory' => $vendorDir . '/doctrine/dbal/src/Schema/LegacySchemaManagerFactory.php', + 'Doctrine\\DBAL\\Schema\\MySQLSchemaManager' => $vendorDir . '/doctrine/dbal/src/Schema/MySQLSchemaManager.php', + 'Doctrine\\DBAL\\Schema\\OracleSchemaManager' => $vendorDir . '/doctrine/dbal/src/Schema/OracleSchemaManager.php', + 'Doctrine\\DBAL\\Schema\\PostgreSQLSchemaManager' => $vendorDir . '/doctrine/dbal/src/Schema/PostgreSQLSchemaManager.php', + 'Doctrine\\DBAL\\Schema\\SQLServerSchemaManager' => $vendorDir . '/doctrine/dbal/src/Schema/SQLServerSchemaManager.php', + 'Doctrine\\DBAL\\Schema\\Schema' => $vendorDir . '/doctrine/dbal/src/Schema/Schema.php', + 'Doctrine\\DBAL\\Schema\\SchemaConfig' => $vendorDir . '/doctrine/dbal/src/Schema/SchemaConfig.php', + 'Doctrine\\DBAL\\Schema\\SchemaDiff' => $vendorDir . '/doctrine/dbal/src/Schema/SchemaDiff.php', + 'Doctrine\\DBAL\\Schema\\SchemaException' => $vendorDir . '/doctrine/dbal/src/Schema/SchemaException.php', + 'Doctrine\\DBAL\\Schema\\SchemaManagerFactory' => $vendorDir . '/doctrine/dbal/src/Schema/SchemaManagerFactory.php', + 'Doctrine\\DBAL\\Schema\\Sequence' => $vendorDir . '/doctrine/dbal/src/Schema/Sequence.php', + 'Doctrine\\DBAL\\Schema\\SqliteSchemaManager' => $vendorDir . '/doctrine/dbal/src/Schema/SqliteSchemaManager.php', + 'Doctrine\\DBAL\\Schema\\Table' => $vendorDir . '/doctrine/dbal/src/Schema/Table.php', + 'Doctrine\\DBAL\\Schema\\TableDiff' => $vendorDir . '/doctrine/dbal/src/Schema/TableDiff.php', + 'Doctrine\\DBAL\\Schema\\UniqueConstraint' => $vendorDir . '/doctrine/dbal/src/Schema/UniqueConstraint.php', + 'Doctrine\\DBAL\\Schema\\View' => $vendorDir . '/doctrine/dbal/src/Schema/View.php', + 'Doctrine\\DBAL\\Schema\\Visitor\\AbstractVisitor' => $vendorDir . '/doctrine/dbal/src/Schema/Visitor/AbstractVisitor.php', + 'Doctrine\\DBAL\\Schema\\Visitor\\CreateSchemaSqlCollector' => $vendorDir . '/doctrine/dbal/src/Schema/Visitor/CreateSchemaSqlCollector.php', + 'Doctrine\\DBAL\\Schema\\Visitor\\DropSchemaSqlCollector' => $vendorDir . '/doctrine/dbal/src/Schema/Visitor/DropSchemaSqlCollector.php', + 'Doctrine\\DBAL\\Schema\\Visitor\\Graphviz' => $vendorDir . '/doctrine/dbal/src/Schema/Visitor/Graphviz.php', + 'Doctrine\\DBAL\\Schema\\Visitor\\NamespaceVisitor' => $vendorDir . '/doctrine/dbal/src/Schema/Visitor/NamespaceVisitor.php', + 'Doctrine\\DBAL\\Schema\\Visitor\\RemoveNamespacedAssets' => $vendorDir . '/doctrine/dbal/src/Schema/Visitor/RemoveNamespacedAssets.php', + 'Doctrine\\DBAL\\Schema\\Visitor\\Visitor' => $vendorDir . '/doctrine/dbal/src/Schema/Visitor/Visitor.php', + 'Doctrine\\DBAL\\Statement' => $vendorDir . '/doctrine/dbal/src/Statement.php', + 'Doctrine\\DBAL\\Tools\\Console\\Command\\CommandCompatibility' => $vendorDir . '/doctrine/dbal/src/Tools/Console/Command/CommandCompatibility.php', + 'Doctrine\\DBAL\\Tools\\Console\\Command\\ReservedWordsCommand' => $vendorDir . '/doctrine/dbal/src/Tools/Console/Command/ReservedWordsCommand.php', + 'Doctrine\\DBAL\\Tools\\Console\\Command\\RunSqlCommand' => $vendorDir . '/doctrine/dbal/src/Tools/Console/Command/RunSqlCommand.php', + 'Doctrine\\DBAL\\Tools\\Console\\ConnectionNotFound' => $vendorDir . '/doctrine/dbal/src/Tools/Console/ConnectionNotFound.php', + 'Doctrine\\DBAL\\Tools\\Console\\ConnectionProvider' => $vendorDir . '/doctrine/dbal/src/Tools/Console/ConnectionProvider.php', + 'Doctrine\\DBAL\\Tools\\Console\\ConnectionProvider\\SingleConnectionProvider' => $vendorDir . '/doctrine/dbal/src/Tools/Console/ConnectionProvider/SingleConnectionProvider.php', + 'Doctrine\\DBAL\\Tools\\Console\\ConsoleRunner' => $vendorDir . '/doctrine/dbal/src/Tools/Console/ConsoleRunner.php', + 'Doctrine\\DBAL\\Tools\\DsnParser' => $vendorDir . '/doctrine/dbal/src/Tools/DsnParser.php', + 'Doctrine\\DBAL\\TransactionIsolationLevel' => $vendorDir . '/doctrine/dbal/src/TransactionIsolationLevel.php', + 'Doctrine\\DBAL\\Types\\ArrayType' => $vendorDir . '/doctrine/dbal/src/Types/ArrayType.php', + 'Doctrine\\DBAL\\Types\\AsciiStringType' => $vendorDir . '/doctrine/dbal/src/Types/AsciiStringType.php', + 'Doctrine\\DBAL\\Types\\BigIntType' => $vendorDir . '/doctrine/dbal/src/Types/BigIntType.php', + 'Doctrine\\DBAL\\Types\\BinaryType' => $vendorDir . '/doctrine/dbal/src/Types/BinaryType.php', + 'Doctrine\\DBAL\\Types\\BlobType' => $vendorDir . '/doctrine/dbal/src/Types/BlobType.php', + 'Doctrine\\DBAL\\Types\\BooleanType' => $vendorDir . '/doctrine/dbal/src/Types/BooleanType.php', + 'Doctrine\\DBAL\\Types\\ConversionException' => $vendorDir . '/doctrine/dbal/src/Types/ConversionException.php', + 'Doctrine\\DBAL\\Types\\DateImmutableType' => $vendorDir . '/doctrine/dbal/src/Types/DateImmutableType.php', + 'Doctrine\\DBAL\\Types\\DateIntervalType' => $vendorDir . '/doctrine/dbal/src/Types/DateIntervalType.php', + 'Doctrine\\DBAL\\Types\\DateTimeImmutableType' => $vendorDir . '/doctrine/dbal/src/Types/DateTimeImmutableType.php', + 'Doctrine\\DBAL\\Types\\DateTimeType' => $vendorDir . '/doctrine/dbal/src/Types/DateTimeType.php', + 'Doctrine\\DBAL\\Types\\DateTimeTzImmutableType' => $vendorDir . '/doctrine/dbal/src/Types/DateTimeTzImmutableType.php', + 'Doctrine\\DBAL\\Types\\DateTimeTzType' => $vendorDir . '/doctrine/dbal/src/Types/DateTimeTzType.php', + 'Doctrine\\DBAL\\Types\\DateType' => $vendorDir . '/doctrine/dbal/src/Types/DateType.php', + 'Doctrine\\DBAL\\Types\\DecimalType' => $vendorDir . '/doctrine/dbal/src/Types/DecimalType.php', + 'Doctrine\\DBAL\\Types\\FloatType' => $vendorDir . '/doctrine/dbal/src/Types/FloatType.php', + 'Doctrine\\DBAL\\Types\\GuidType' => $vendorDir . '/doctrine/dbal/src/Types/GuidType.php', + 'Doctrine\\DBAL\\Types\\IntegerType' => $vendorDir . '/doctrine/dbal/src/Types/IntegerType.php', + 'Doctrine\\DBAL\\Types\\JsonType' => $vendorDir . '/doctrine/dbal/src/Types/JsonType.php', + 'Doctrine\\DBAL\\Types\\ObjectType' => $vendorDir . '/doctrine/dbal/src/Types/ObjectType.php', + 'Doctrine\\DBAL\\Types\\PhpDateTimeMappingType' => $vendorDir . '/doctrine/dbal/src/Types/PhpDateTimeMappingType.php', + 'Doctrine\\DBAL\\Types\\PhpIntegerMappingType' => $vendorDir . '/doctrine/dbal/src/Types/PhpIntegerMappingType.php', + 'Doctrine\\DBAL\\Types\\SimpleArrayType' => $vendorDir . '/doctrine/dbal/src/Types/SimpleArrayType.php', + 'Doctrine\\DBAL\\Types\\SmallIntType' => $vendorDir . '/doctrine/dbal/src/Types/SmallIntType.php', + 'Doctrine\\DBAL\\Types\\StringType' => $vendorDir . '/doctrine/dbal/src/Types/StringType.php', + 'Doctrine\\DBAL\\Types\\TextType' => $vendorDir . '/doctrine/dbal/src/Types/TextType.php', + 'Doctrine\\DBAL\\Types\\TimeImmutableType' => $vendorDir . '/doctrine/dbal/src/Types/TimeImmutableType.php', + 'Doctrine\\DBAL\\Types\\TimeType' => $vendorDir . '/doctrine/dbal/src/Types/TimeType.php', + 'Doctrine\\DBAL\\Types\\Type' => $vendorDir . '/doctrine/dbal/src/Types/Type.php', + 'Doctrine\\DBAL\\Types\\TypeRegistry' => $vendorDir . '/doctrine/dbal/src/Types/TypeRegistry.php', + 'Doctrine\\DBAL\\Types\\Types' => $vendorDir . '/doctrine/dbal/src/Types/Types.php', + 'Doctrine\\DBAL\\Types\\VarDateTimeImmutableType' => $vendorDir . '/doctrine/dbal/src/Types/VarDateTimeImmutableType.php', + 'Doctrine\\DBAL\\Types\\VarDateTimeType' => $vendorDir . '/doctrine/dbal/src/Types/VarDateTimeType.php', + 'Doctrine\\DBAL\\VersionAwarePlatformDriver' => $vendorDir . '/doctrine/dbal/src/VersionAwarePlatformDriver.php', + 'Doctrine\\Deprecations\\Deprecation' => $vendorDir . '/doctrine/deprecations/src/Deprecation.php', + 'Doctrine\\Deprecations\\PHPUnit\\VerifyDeprecations' => $vendorDir . '/doctrine/deprecations/src/PHPUnit/VerifyDeprecations.php', + 'Egulias\\EmailValidator\\EmailLexer' => $vendorDir . '/egulias/email-validator/src/EmailLexer.php', + 'Egulias\\EmailValidator\\EmailParser' => $vendorDir . '/egulias/email-validator/src/EmailParser.php', + 'Egulias\\EmailValidator\\EmailValidator' => $vendorDir . '/egulias/email-validator/src/EmailValidator.php', + 'Egulias\\EmailValidator\\MessageIDParser' => $vendorDir . '/egulias/email-validator/src/MessageIDParser.php', + 'Egulias\\EmailValidator\\Parser' => $vendorDir . '/egulias/email-validator/src/Parser.php', + 'Egulias\\EmailValidator\\Parser\\Comment' => $vendorDir . '/egulias/email-validator/src/Parser/Comment.php', + 'Egulias\\EmailValidator\\Parser\\CommentStrategy\\CommentStrategy' => $vendorDir . '/egulias/email-validator/src/Parser/CommentStrategy/CommentStrategy.php', + 'Egulias\\EmailValidator\\Parser\\CommentStrategy\\DomainComment' => $vendorDir . '/egulias/email-validator/src/Parser/CommentStrategy/DomainComment.php', + 'Egulias\\EmailValidator\\Parser\\CommentStrategy\\LocalComment' => $vendorDir . '/egulias/email-validator/src/Parser/CommentStrategy/LocalComment.php', + 'Egulias\\EmailValidator\\Parser\\DomainLiteral' => $vendorDir . '/egulias/email-validator/src/Parser/DomainLiteral.php', + 'Egulias\\EmailValidator\\Parser\\DomainPart' => $vendorDir . '/egulias/email-validator/src/Parser/DomainPart.php', + 'Egulias\\EmailValidator\\Parser\\DoubleQuote' => $vendorDir . '/egulias/email-validator/src/Parser/DoubleQuote.php', + 'Egulias\\EmailValidator\\Parser\\FoldingWhiteSpace' => $vendorDir . '/egulias/email-validator/src/Parser/FoldingWhiteSpace.php', + 'Egulias\\EmailValidator\\Parser\\IDLeftPart' => $vendorDir . '/egulias/email-validator/src/Parser/IDLeftPart.php', + 'Egulias\\EmailValidator\\Parser\\IDRightPart' => $vendorDir . '/egulias/email-validator/src/Parser/IDRightPart.php', + 'Egulias\\EmailValidator\\Parser\\LocalPart' => $vendorDir . '/egulias/email-validator/src/Parser/LocalPart.php', + 'Egulias\\EmailValidator\\Parser\\PartParser' => $vendorDir . '/egulias/email-validator/src/Parser/PartParser.php', + 'Egulias\\EmailValidator\\Result\\InvalidEmail' => $vendorDir . '/egulias/email-validator/src/Result/InvalidEmail.php', + 'Egulias\\EmailValidator\\Result\\MultipleErrors' => $vendorDir . '/egulias/email-validator/src/Result/MultipleErrors.php', + 'Egulias\\EmailValidator\\Result\\Reason\\AtextAfterCFWS' => $vendorDir . '/egulias/email-validator/src/Result/Reason/AtextAfterCFWS.php', + 'Egulias\\EmailValidator\\Result\\Reason\\CRLFAtTheEnd' => $vendorDir . '/egulias/email-validator/src/Result/Reason/CRLFAtTheEnd.php', + 'Egulias\\EmailValidator\\Result\\Reason\\CRLFX2' => $vendorDir . '/egulias/email-validator/src/Result/Reason/CRLFX2.php', + 'Egulias\\EmailValidator\\Result\\Reason\\CRNoLF' => $vendorDir . '/egulias/email-validator/src/Result/Reason/CRNoLF.php', + 'Egulias\\EmailValidator\\Result\\Reason\\CharNotAllowed' => $vendorDir . '/egulias/email-validator/src/Result/Reason/CharNotAllowed.php', + 'Egulias\\EmailValidator\\Result\\Reason\\CommaInDomain' => $vendorDir . '/egulias/email-validator/src/Result/Reason/CommaInDomain.php', + 'Egulias\\EmailValidator\\Result\\Reason\\CommentsInIDRight' => $vendorDir . '/egulias/email-validator/src/Result/Reason/CommentsInIDRight.php', + 'Egulias\\EmailValidator\\Result\\Reason\\ConsecutiveAt' => $vendorDir . '/egulias/email-validator/src/Result/Reason/ConsecutiveAt.php', + 'Egulias\\EmailValidator\\Result\\Reason\\ConsecutiveDot' => $vendorDir . '/egulias/email-validator/src/Result/Reason/ConsecutiveDot.php', + 'Egulias\\EmailValidator\\Result\\Reason\\DetailedReason' => $vendorDir . '/egulias/email-validator/src/Result/Reason/DetailedReason.php', + 'Egulias\\EmailValidator\\Result\\Reason\\DomainAcceptsNoMail' => $vendorDir . '/egulias/email-validator/src/Result/Reason/DomainAcceptsNoMail.php', + 'Egulias\\EmailValidator\\Result\\Reason\\DomainHyphened' => $vendorDir . '/egulias/email-validator/src/Result/Reason/DomainHyphened.php', + 'Egulias\\EmailValidator\\Result\\Reason\\DomainTooLong' => $vendorDir . '/egulias/email-validator/src/Result/Reason/DomainTooLong.php', + 'Egulias\\EmailValidator\\Result\\Reason\\DotAtEnd' => $vendorDir . '/egulias/email-validator/src/Result/Reason/DotAtEnd.php', + 'Egulias\\EmailValidator\\Result\\Reason\\DotAtStart' => $vendorDir . '/egulias/email-validator/src/Result/Reason/DotAtStart.php', + 'Egulias\\EmailValidator\\Result\\Reason\\EmptyReason' => $vendorDir . '/egulias/email-validator/src/Result/Reason/EmptyReason.php', + 'Egulias\\EmailValidator\\Result\\Reason\\ExceptionFound' => $vendorDir . '/egulias/email-validator/src/Result/Reason/ExceptionFound.php', + 'Egulias\\EmailValidator\\Result\\Reason\\ExpectingATEXT' => $vendorDir . '/egulias/email-validator/src/Result/Reason/ExpectingATEXT.php', + 'Egulias\\EmailValidator\\Result\\Reason\\ExpectingCTEXT' => $vendorDir . '/egulias/email-validator/src/Result/Reason/ExpectingCTEXT.php', + 'Egulias\\EmailValidator\\Result\\Reason\\ExpectingDTEXT' => $vendorDir . '/egulias/email-validator/src/Result/Reason/ExpectingDTEXT.php', + 'Egulias\\EmailValidator\\Result\\Reason\\ExpectingDomainLiteralClose' => $vendorDir . '/egulias/email-validator/src/Result/Reason/ExpectingDomainLiteralClose.php', + 'Egulias\\EmailValidator\\Result\\Reason\\LabelTooLong' => $vendorDir . '/egulias/email-validator/src/Result/Reason/LabelTooLong.php', + 'Egulias\\EmailValidator\\Result\\Reason\\LocalOrReservedDomain' => $vendorDir . '/egulias/email-validator/src/Result/Reason/LocalOrReservedDomain.php', + 'Egulias\\EmailValidator\\Result\\Reason\\NoDNSRecord' => $vendorDir . '/egulias/email-validator/src/Result/Reason/NoDNSRecord.php', + 'Egulias\\EmailValidator\\Result\\Reason\\NoDomainPart' => $vendorDir . '/egulias/email-validator/src/Result/Reason/NoDomainPart.php', + 'Egulias\\EmailValidator\\Result\\Reason\\NoLocalPart' => $vendorDir . '/egulias/email-validator/src/Result/Reason/NoLocalPart.php', + 'Egulias\\EmailValidator\\Result\\Reason\\RFCWarnings' => $vendorDir . '/egulias/email-validator/src/Result/Reason/RFCWarnings.php', + 'Egulias\\EmailValidator\\Result\\Reason\\Reason' => $vendorDir . '/egulias/email-validator/src/Result/Reason/Reason.php', + 'Egulias\\EmailValidator\\Result\\Reason\\SpoofEmail' => $vendorDir . '/egulias/email-validator/src/Result/Reason/SpoofEmail.php', + 'Egulias\\EmailValidator\\Result\\Reason\\UnOpenedComment' => $vendorDir . '/egulias/email-validator/src/Result/Reason/UnOpenedComment.php', + 'Egulias\\EmailValidator\\Result\\Reason\\UnableToGetDNSRecord' => $vendorDir . '/egulias/email-validator/src/Result/Reason/UnableToGetDNSRecord.php', + 'Egulias\\EmailValidator\\Result\\Reason\\UnclosedComment' => $vendorDir . '/egulias/email-validator/src/Result/Reason/UnclosedComment.php', + 'Egulias\\EmailValidator\\Result\\Reason\\UnclosedQuotedString' => $vendorDir . '/egulias/email-validator/src/Result/Reason/UnclosedQuotedString.php', + 'Egulias\\EmailValidator\\Result\\Reason\\UnusualElements' => $vendorDir . '/egulias/email-validator/src/Result/Reason/UnusualElements.php', + 'Egulias\\EmailValidator\\Result\\Result' => $vendorDir . '/egulias/email-validator/src/Result/Result.php', + 'Egulias\\EmailValidator\\Result\\SpoofEmail' => $vendorDir . '/egulias/email-validator/src/Result/SpoofEmail.php', + 'Egulias\\EmailValidator\\Result\\ValidEmail' => $vendorDir . '/egulias/email-validator/src/Result/ValidEmail.php', + 'Egulias\\EmailValidator\\Validation\\DNSCheckValidation' => $vendorDir . '/egulias/email-validator/src/Validation/DNSCheckValidation.php', + 'Egulias\\EmailValidator\\Validation\\DNSGetRecordWrapper' => $vendorDir . '/egulias/email-validator/src/Validation/DNSGetRecordWrapper.php', + 'Egulias\\EmailValidator\\Validation\\DNSRecords' => $vendorDir . '/egulias/email-validator/src/Validation/DNSRecords.php', + 'Egulias\\EmailValidator\\Validation\\EmailValidation' => $vendorDir . '/egulias/email-validator/src/Validation/EmailValidation.php', + 'Egulias\\EmailValidator\\Validation\\Exception\\EmptyValidationList' => $vendorDir . '/egulias/email-validator/src/Validation/Exception/EmptyValidationList.php', + 'Egulias\\EmailValidator\\Validation\\Extra\\SpoofCheckValidation' => $vendorDir . '/egulias/email-validator/src/Validation/Extra/SpoofCheckValidation.php', + 'Egulias\\EmailValidator\\Validation\\MessageIDValidation' => $vendorDir . '/egulias/email-validator/src/Validation/MessageIDValidation.php', + 'Egulias\\EmailValidator\\Validation\\MultipleValidationWithAnd' => $vendorDir . '/egulias/email-validator/src/Validation/MultipleValidationWithAnd.php', + 'Egulias\\EmailValidator\\Validation\\NoRFCWarningsValidation' => $vendorDir . '/egulias/email-validator/src/Validation/NoRFCWarningsValidation.php', + 'Egulias\\EmailValidator\\Validation\\RFCValidation' => $vendorDir . '/egulias/email-validator/src/Validation/RFCValidation.php', + 'Egulias\\EmailValidator\\Warning\\AddressLiteral' => $vendorDir . '/egulias/email-validator/src/Warning/AddressLiteral.php', + 'Egulias\\EmailValidator\\Warning\\CFWSNearAt' => $vendorDir . '/egulias/email-validator/src/Warning/CFWSNearAt.php', + 'Egulias\\EmailValidator\\Warning\\CFWSWithFWS' => $vendorDir . '/egulias/email-validator/src/Warning/CFWSWithFWS.php', + 'Egulias\\EmailValidator\\Warning\\Comment' => $vendorDir . '/egulias/email-validator/src/Warning/Comment.php', + 'Egulias\\EmailValidator\\Warning\\DeprecatedComment' => $vendorDir . '/egulias/email-validator/src/Warning/DeprecatedComment.php', + 'Egulias\\EmailValidator\\Warning\\DomainLiteral' => $vendorDir . '/egulias/email-validator/src/Warning/DomainLiteral.php', + 'Egulias\\EmailValidator\\Warning\\EmailTooLong' => $vendorDir . '/egulias/email-validator/src/Warning/EmailTooLong.php', + 'Egulias\\EmailValidator\\Warning\\IPV6BadChar' => $vendorDir . '/egulias/email-validator/src/Warning/IPV6BadChar.php', + 'Egulias\\EmailValidator\\Warning\\IPV6ColonEnd' => $vendorDir . '/egulias/email-validator/src/Warning/IPV6ColonEnd.php', + 'Egulias\\EmailValidator\\Warning\\IPV6ColonStart' => $vendorDir . '/egulias/email-validator/src/Warning/IPV6ColonStart.php', + 'Egulias\\EmailValidator\\Warning\\IPV6Deprecated' => $vendorDir . '/egulias/email-validator/src/Warning/IPV6Deprecated.php', + 'Egulias\\EmailValidator\\Warning\\IPV6DoubleColon' => $vendorDir . '/egulias/email-validator/src/Warning/IPV6DoubleColon.php', + 'Egulias\\EmailValidator\\Warning\\IPV6GroupCount' => $vendorDir . '/egulias/email-validator/src/Warning/IPV6GroupCount.php', + 'Egulias\\EmailValidator\\Warning\\IPV6MaxGroups' => $vendorDir . '/egulias/email-validator/src/Warning/IPV6MaxGroups.php', + 'Egulias\\EmailValidator\\Warning\\LocalTooLong' => $vendorDir . '/egulias/email-validator/src/Warning/LocalTooLong.php', + 'Egulias\\EmailValidator\\Warning\\NoDNSMXRecord' => $vendorDir . '/egulias/email-validator/src/Warning/NoDNSMXRecord.php', + 'Egulias\\EmailValidator\\Warning\\ObsoleteDTEXT' => $vendorDir . '/egulias/email-validator/src/Warning/ObsoleteDTEXT.php', + 'Egulias\\EmailValidator\\Warning\\QuotedPart' => $vendorDir . '/egulias/email-validator/src/Warning/QuotedPart.php', + 'Egulias\\EmailValidator\\Warning\\QuotedString' => $vendorDir . '/egulias/email-validator/src/Warning/QuotedString.php', + 'Egulias\\EmailValidator\\Warning\\TLD' => $vendorDir . '/egulias/email-validator/src/Warning/TLD.php', + 'Egulias\\EmailValidator\\Warning\\Warning' => $vendorDir . '/egulias/email-validator/src/Warning/Warning.php', + 'Fusonic\\OpenGraph\\Consumer' => $vendorDir . '/fusonic/opengraph/src/Consumer.php', + 'Fusonic\\OpenGraph\\Elements\\Audio' => $vendorDir . '/fusonic/opengraph/src/Elements/Audio.php', + 'Fusonic\\OpenGraph\\Elements\\ElementBase' => $vendorDir . '/fusonic/opengraph/src/Elements/ElementBase.php', + 'Fusonic\\OpenGraph\\Elements\\Image' => $vendorDir . '/fusonic/opengraph/src/Elements/Image.php', + 'Fusonic\\OpenGraph\\Elements\\Video' => $vendorDir . '/fusonic/opengraph/src/Elements/Video.php', + 'Fusonic\\OpenGraph\\Objects\\ObjectBase' => $vendorDir . '/fusonic/opengraph/src/Objects/ObjectBase.php', + 'Fusonic\\OpenGraph\\Objects\\Website' => $vendorDir . '/fusonic/opengraph/src/Objects/Website.php', + 'Fusonic\\OpenGraph\\Property' => $vendorDir . '/fusonic/opengraph/src/Property.php', + 'Fusonic\\OpenGraph\\Publisher' => $vendorDir . '/fusonic/opengraph/src/Publisher.php', + 'GuzzleHttp\\BodySummarizer' => $vendorDir . '/guzzlehttp/guzzle/src/BodySummarizer.php', + 'GuzzleHttp\\BodySummarizerInterface' => $vendorDir . '/guzzlehttp/guzzle/src/BodySummarizerInterface.php', + 'GuzzleHttp\\Client' => $vendorDir . '/guzzlehttp/guzzle/src/Client.php', + 'GuzzleHttp\\ClientInterface' => $vendorDir . '/guzzlehttp/guzzle/src/ClientInterface.php', + 'GuzzleHttp\\ClientTrait' => $vendorDir . '/guzzlehttp/guzzle/src/ClientTrait.php', + 'GuzzleHttp\\Cookie\\CookieJar' => $vendorDir . '/guzzlehttp/guzzle/src/Cookie/CookieJar.php', + 'GuzzleHttp\\Cookie\\CookieJarInterface' => $vendorDir . '/guzzlehttp/guzzle/src/Cookie/CookieJarInterface.php', + 'GuzzleHttp\\Cookie\\FileCookieJar' => $vendorDir . '/guzzlehttp/guzzle/src/Cookie/FileCookieJar.php', + 'GuzzleHttp\\Cookie\\SessionCookieJar' => $vendorDir . '/guzzlehttp/guzzle/src/Cookie/SessionCookieJar.php', + 'GuzzleHttp\\Cookie\\SetCookie' => $vendorDir . '/guzzlehttp/guzzle/src/Cookie/SetCookie.php', + 'GuzzleHttp\\Exception\\BadResponseException' => $vendorDir . '/guzzlehttp/guzzle/src/Exception/BadResponseException.php', + 'GuzzleHttp\\Exception\\ClientException' => $vendorDir . '/guzzlehttp/guzzle/src/Exception/ClientException.php', + 'GuzzleHttp\\Exception\\ConnectException' => $vendorDir . '/guzzlehttp/guzzle/src/Exception/ConnectException.php', + 'GuzzleHttp\\Exception\\GuzzleException' => $vendorDir . '/guzzlehttp/guzzle/src/Exception/GuzzleException.php', + 'GuzzleHttp\\Exception\\InvalidArgumentException' => $vendorDir . '/guzzlehttp/guzzle/src/Exception/InvalidArgumentException.php', + 'GuzzleHttp\\Exception\\RequestException' => $vendorDir . '/guzzlehttp/guzzle/src/Exception/RequestException.php', + 'GuzzleHttp\\Exception\\ServerException' => $vendorDir . '/guzzlehttp/guzzle/src/Exception/ServerException.php', + 'GuzzleHttp\\Exception\\TooManyRedirectsException' => $vendorDir . '/guzzlehttp/guzzle/src/Exception/TooManyRedirectsException.php', + 'GuzzleHttp\\Exception\\TransferException' => $vendorDir . '/guzzlehttp/guzzle/src/Exception/TransferException.php', + 'GuzzleHttp\\HandlerStack' => $vendorDir . '/guzzlehttp/guzzle/src/HandlerStack.php', + 'GuzzleHttp\\Handler\\CurlFactory' => $vendorDir . '/guzzlehttp/guzzle/src/Handler/CurlFactory.php', + 'GuzzleHttp\\Handler\\CurlFactoryInterface' => $vendorDir . '/guzzlehttp/guzzle/src/Handler/CurlFactoryInterface.php', + 'GuzzleHttp\\Handler\\CurlHandler' => $vendorDir . '/guzzlehttp/guzzle/src/Handler/CurlHandler.php', + 'GuzzleHttp\\Handler\\CurlMultiHandler' => $vendorDir . '/guzzlehttp/guzzle/src/Handler/CurlMultiHandler.php', + 'GuzzleHttp\\Handler\\EasyHandle' => $vendorDir . '/guzzlehttp/guzzle/src/Handler/EasyHandle.php', + 'GuzzleHttp\\Handler\\HeaderProcessor' => $vendorDir . '/guzzlehttp/guzzle/src/Handler/HeaderProcessor.php', + 'GuzzleHttp\\Handler\\MockHandler' => $vendorDir . '/guzzlehttp/guzzle/src/Handler/MockHandler.php', + 'GuzzleHttp\\Handler\\Proxy' => $vendorDir . '/guzzlehttp/guzzle/src/Handler/Proxy.php', + 'GuzzleHttp\\Handler\\StreamHandler' => $vendorDir . '/guzzlehttp/guzzle/src/Handler/StreamHandler.php', + 'GuzzleHttp\\MessageFormatter' => $vendorDir . '/guzzlehttp/guzzle/src/MessageFormatter.php', + 'GuzzleHttp\\MessageFormatterInterface' => $vendorDir . '/guzzlehttp/guzzle/src/MessageFormatterInterface.php', + 'GuzzleHttp\\Middleware' => $vendorDir . '/guzzlehttp/guzzle/src/Middleware.php', + 'GuzzleHttp\\Pool' => $vendorDir . '/guzzlehttp/guzzle/src/Pool.php', + 'GuzzleHttp\\PrepareBodyMiddleware' => $vendorDir . '/guzzlehttp/guzzle/src/PrepareBodyMiddleware.php', + 'GuzzleHttp\\Promise\\AggregateException' => $vendorDir . '/guzzlehttp/promises/src/AggregateException.php', + 'GuzzleHttp\\Promise\\CancellationException' => $vendorDir . '/guzzlehttp/promises/src/CancellationException.php', + 'GuzzleHttp\\Promise\\Coroutine' => $vendorDir . '/guzzlehttp/promises/src/Coroutine.php', + 'GuzzleHttp\\Promise\\Create' => $vendorDir . '/guzzlehttp/promises/src/Create.php', + 'GuzzleHttp\\Promise\\Each' => $vendorDir . '/guzzlehttp/promises/src/Each.php', + 'GuzzleHttp\\Promise\\EachPromise' => $vendorDir . '/guzzlehttp/promises/src/EachPromise.php', + 'GuzzleHttp\\Promise\\FulfilledPromise' => $vendorDir . '/guzzlehttp/promises/src/FulfilledPromise.php', + 'GuzzleHttp\\Promise\\Is' => $vendorDir . '/guzzlehttp/promises/src/Is.php', + 'GuzzleHttp\\Promise\\Promise' => $vendorDir . '/guzzlehttp/promises/src/Promise.php', + 'GuzzleHttp\\Promise\\PromiseInterface' => $vendorDir . '/guzzlehttp/promises/src/PromiseInterface.php', + 'GuzzleHttp\\Promise\\PromisorInterface' => $vendorDir . '/guzzlehttp/promises/src/PromisorInterface.php', + 'GuzzleHttp\\Promise\\RejectedPromise' => $vendorDir . '/guzzlehttp/promises/src/RejectedPromise.php', + 'GuzzleHttp\\Promise\\RejectionException' => $vendorDir . '/guzzlehttp/promises/src/RejectionException.php', + 'GuzzleHttp\\Promise\\TaskQueue' => $vendorDir . '/guzzlehttp/promises/src/TaskQueue.php', + 'GuzzleHttp\\Promise\\TaskQueueInterface' => $vendorDir . '/guzzlehttp/promises/src/TaskQueueInterface.php', + 'GuzzleHttp\\Promise\\Utils' => $vendorDir . '/guzzlehttp/promises/src/Utils.php', + 'GuzzleHttp\\Psr7\\AppendStream' => $vendorDir . '/guzzlehttp/psr7/src/AppendStream.php', + 'GuzzleHttp\\Psr7\\BufferStream' => $vendorDir . '/guzzlehttp/psr7/src/BufferStream.php', + 'GuzzleHttp\\Psr7\\CachingStream' => $vendorDir . '/guzzlehttp/psr7/src/CachingStream.php', + 'GuzzleHttp\\Psr7\\DroppingStream' => $vendorDir . '/guzzlehttp/psr7/src/DroppingStream.php', + 'GuzzleHttp\\Psr7\\Exception\\MalformedUriException' => $vendorDir . '/guzzlehttp/psr7/src/Exception/MalformedUriException.php', + 'GuzzleHttp\\Psr7\\FnStream' => $vendorDir . '/guzzlehttp/psr7/src/FnStream.php', + 'GuzzleHttp\\Psr7\\Header' => $vendorDir . '/guzzlehttp/psr7/src/Header.php', + 'GuzzleHttp\\Psr7\\HttpFactory' => $vendorDir . '/guzzlehttp/psr7/src/HttpFactory.php', + 'GuzzleHttp\\Psr7\\InflateStream' => $vendorDir . '/guzzlehttp/psr7/src/InflateStream.php', + 'GuzzleHttp\\Psr7\\LazyOpenStream' => $vendorDir . '/guzzlehttp/psr7/src/LazyOpenStream.php', + 'GuzzleHttp\\Psr7\\LimitStream' => $vendorDir . '/guzzlehttp/psr7/src/LimitStream.php', + 'GuzzleHttp\\Psr7\\Message' => $vendorDir . '/guzzlehttp/psr7/src/Message.php', + 'GuzzleHttp\\Psr7\\MessageTrait' => $vendorDir . '/guzzlehttp/psr7/src/MessageTrait.php', + 'GuzzleHttp\\Psr7\\MimeType' => $vendorDir . '/guzzlehttp/psr7/src/MimeType.php', + 'GuzzleHttp\\Psr7\\MultipartStream' => $vendorDir . '/guzzlehttp/psr7/src/MultipartStream.php', + 'GuzzleHttp\\Psr7\\NoSeekStream' => $vendorDir . '/guzzlehttp/psr7/src/NoSeekStream.php', + 'GuzzleHttp\\Psr7\\PumpStream' => $vendorDir . '/guzzlehttp/psr7/src/PumpStream.php', + 'GuzzleHttp\\Psr7\\Query' => $vendorDir . '/guzzlehttp/psr7/src/Query.php', + 'GuzzleHttp\\Psr7\\Request' => $vendorDir . '/guzzlehttp/psr7/src/Request.php', + 'GuzzleHttp\\Psr7\\Response' => $vendorDir . '/guzzlehttp/psr7/src/Response.php', + 'GuzzleHttp\\Psr7\\Rfc7230' => $vendorDir . '/guzzlehttp/psr7/src/Rfc7230.php', + 'GuzzleHttp\\Psr7\\ServerRequest' => $vendorDir . '/guzzlehttp/psr7/src/ServerRequest.php', + 'GuzzleHttp\\Psr7\\Stream' => $vendorDir . '/guzzlehttp/psr7/src/Stream.php', + 'GuzzleHttp\\Psr7\\StreamDecoratorTrait' => $vendorDir . '/guzzlehttp/psr7/src/StreamDecoratorTrait.php', + 'GuzzleHttp\\Psr7\\StreamWrapper' => $vendorDir . '/guzzlehttp/psr7/src/StreamWrapper.php', + 'GuzzleHttp\\Psr7\\UploadedFile' => $vendorDir . '/guzzlehttp/psr7/src/UploadedFile.php', + 'GuzzleHttp\\Psr7\\Uri' => $vendorDir . '/guzzlehttp/psr7/src/Uri.php', + 'GuzzleHttp\\Psr7\\UriComparator' => $vendorDir . '/guzzlehttp/psr7/src/UriComparator.php', + 'GuzzleHttp\\Psr7\\UriNormalizer' => $vendorDir . '/guzzlehttp/psr7/src/UriNormalizer.php', + 'GuzzleHttp\\Psr7\\UriResolver' => $vendorDir . '/guzzlehttp/psr7/src/UriResolver.php', + 'GuzzleHttp\\Psr7\\Utils' => $vendorDir . '/guzzlehttp/psr7/src/Utils.php', + 'GuzzleHttp\\RedirectMiddleware' => $vendorDir . '/guzzlehttp/guzzle/src/RedirectMiddleware.php', + 'GuzzleHttp\\RequestOptions' => $vendorDir . '/guzzlehttp/guzzle/src/RequestOptions.php', + 'GuzzleHttp\\RetryMiddleware' => $vendorDir . '/guzzlehttp/guzzle/src/RetryMiddleware.php', + 'GuzzleHttp\\TransferStats' => $vendorDir . '/guzzlehttp/guzzle/src/TransferStats.php', + 'GuzzleHttp\\UriTemplate\\UriTemplate' => $vendorDir . '/guzzlehttp/uri-template/src/UriTemplate.php', + 'GuzzleHttp\\Utils' => $vendorDir . '/guzzlehttp/guzzle/src/Utils.php', + 'Http\\Adapter\\Guzzle7\\Client' => $vendorDir . '/php-http/guzzle7-adapter/src/Client.php', + 'Http\\Adapter\\Guzzle7\\Exception\\UnexpectedValueException' => $vendorDir . '/php-http/guzzle7-adapter/src/Exception/UnexpectedValueException.php', + 'Http\\Adapter\\Guzzle7\\Promise' => $vendorDir . '/php-http/guzzle7-adapter/src/Promise.php', + 'Http\\Client\\Exception' => $vendorDir . '/php-http/httplug/src/Exception.php', + 'Http\\Client\\Exception\\HttpException' => $vendorDir . '/php-http/httplug/src/Exception/HttpException.php', + 'Http\\Client\\Exception\\NetworkException' => $vendorDir . '/php-http/httplug/src/Exception/NetworkException.php', + 'Http\\Client\\Exception\\RequestAwareTrait' => $vendorDir . '/php-http/httplug/src/Exception/RequestAwareTrait.php', + 'Http\\Client\\Exception\\RequestException' => $vendorDir . '/php-http/httplug/src/Exception/RequestException.php', + 'Http\\Client\\Exception\\TransferException' => $vendorDir . '/php-http/httplug/src/Exception/TransferException.php', + 'Http\\Client\\HttpAsyncClient' => $vendorDir . '/php-http/httplug/src/HttpAsyncClient.php', + 'Http\\Client\\HttpClient' => $vendorDir . '/php-http/httplug/src/HttpClient.php', + 'Http\\Client\\Promise\\HttpFulfilledPromise' => $vendorDir . '/php-http/httplug/src/Promise/HttpFulfilledPromise.php', + 'Http\\Client\\Promise\\HttpRejectedPromise' => $vendorDir . '/php-http/httplug/src/Promise/HttpRejectedPromise.php', + 'Http\\Promise\\FulfilledPromise' => $vendorDir . '/php-http/promise/src/FulfilledPromise.php', + 'Http\\Promise\\Promise' => $vendorDir . '/php-http/promise/src/Promise.php', + 'Http\\Promise\\RejectedPromise' => $vendorDir . '/php-http/promise/src/RejectedPromise.php', + 'IPLib\\Address\\AddressInterface' => $vendorDir . '/mlocati/ip-lib/src/Address/AddressInterface.php', + 'IPLib\\Address\\AssignedRange' => $vendorDir . '/mlocati/ip-lib/src/Address/AssignedRange.php', + 'IPLib\\Address\\IPv4' => $vendorDir . '/mlocati/ip-lib/src/Address/IPv4.php', + 'IPLib\\Address\\IPv6' => $vendorDir . '/mlocati/ip-lib/src/Address/IPv6.php', + 'IPLib\\Address\\Type' => $vendorDir . '/mlocati/ip-lib/src/Address/Type.php', + 'IPLib\\Factory' => $vendorDir . '/mlocati/ip-lib/src/Factory.php', + 'IPLib\\ParseStringFlag' => $vendorDir . '/mlocati/ip-lib/src/ParseStringFlag.php', + 'IPLib\\Range\\AbstractRange' => $vendorDir . '/mlocati/ip-lib/src/Range/AbstractRange.php', + 'IPLib\\Range\\Pattern' => $vendorDir . '/mlocati/ip-lib/src/Range/Pattern.php', + 'IPLib\\Range\\RangeInterface' => $vendorDir . '/mlocati/ip-lib/src/Range/RangeInterface.php', + 'IPLib\\Range\\Single' => $vendorDir . '/mlocati/ip-lib/src/Range/Single.php', + 'IPLib\\Range\\Subnet' => $vendorDir . '/mlocati/ip-lib/src/Range/Subnet.php', + 'IPLib\\Range\\Type' => $vendorDir . '/mlocati/ip-lib/src/Range/Type.php', + 'IPLib\\Service\\BinaryMath' => $vendorDir . '/mlocati/ip-lib/src/Service/BinaryMath.php', + 'IPLib\\Service\\RangesFromBoundaryCalculator' => $vendorDir . '/mlocati/ip-lib/src/Service/RangesFromBoundaryCalculator.php', + 'IPLib\\Service\\UnsignedIntegerMath' => $vendorDir . '/mlocati/ip-lib/src/Service/UnsignedIntegerMath.php', + 'Icewind\\SMB\\ACL' => $vendorDir . '/icewind/smb/src/ACL.php', + 'Icewind\\SMB\\AbstractServer' => $vendorDir . '/icewind/smb/src/AbstractServer.php', + 'Icewind\\SMB\\AbstractShare' => $vendorDir . '/icewind/smb/src/AbstractShare.php', + 'Icewind\\SMB\\AnonymousAuth' => $vendorDir . '/icewind/smb/src/AnonymousAuth.php', + 'Icewind\\SMB\\BasicAuth' => $vendorDir . '/icewind/smb/src/BasicAuth.php', + 'Icewind\\SMB\\Change' => $vendorDir . '/icewind/smb/src/Change.php', + 'Icewind\\SMB\\Exception\\AccessDeniedException' => $vendorDir . '/icewind/smb/src/Exception/AccessDeniedException.php', + 'Icewind\\SMB\\Exception\\AlreadyExistsException' => $vendorDir . '/icewind/smb/src/Exception/AlreadyExistsException.php', + 'Icewind\\SMB\\Exception\\AuthenticationException' => $vendorDir . '/icewind/smb/src/Exception/AuthenticationException.php', + 'Icewind\\SMB\\Exception\\ConnectException' => $vendorDir . '/icewind/smb/src/Exception/ConnectException.php', + 'Icewind\\SMB\\Exception\\ConnectionAbortedException' => $vendorDir . '/icewind/smb/src/Exception/ConnectionAbortedException.php', + 'Icewind\\SMB\\Exception\\ConnectionException' => $vendorDir . '/icewind/smb/src/Exception/ConnectionException.php', + 'Icewind\\SMB\\Exception\\ConnectionRefusedException' => $vendorDir . '/icewind/smb/src/Exception/ConnectionRefusedException.php', + 'Icewind\\SMB\\Exception\\ConnectionResetException' => $vendorDir . '/icewind/smb/src/Exception/ConnectionResetException.php', + 'Icewind\\SMB\\Exception\\DependencyException' => $vendorDir . '/icewind/smb/src/Exception/DependencyException.php', + 'Icewind\\SMB\\Exception\\Exception' => $vendorDir . '/icewind/smb/src/Exception/Exception.php', + 'Icewind\\SMB\\Exception\\FileInUseException' => $vendorDir . '/icewind/smb/src/Exception/FileInUseException.php', + 'Icewind\\SMB\\Exception\\ForbiddenException' => $vendorDir . '/icewind/smb/src/Exception/ForbiddenException.php', + 'Icewind\\SMB\\Exception\\HostDownException' => $vendorDir . '/icewind/smb/src/Exception/HostDownException.php', + 'Icewind\\SMB\\Exception\\InvalidArgumentException' => $vendorDir . '/icewind/smb/src/Exception/InvalidArgumentException.php', + 'Icewind\\SMB\\Exception\\InvalidHostException' => $vendorDir . '/icewind/smb/src/Exception/InvalidHostException.php', + 'Icewind\\SMB\\Exception\\InvalidParameterException' => $vendorDir . '/icewind/smb/src/Exception/InvalidParameterException.php', + 'Icewind\\SMB\\Exception\\InvalidPathException' => $vendorDir . '/icewind/smb/src/Exception/InvalidPathException.php', + 'Icewind\\SMB\\Exception\\InvalidRequestException' => $vendorDir . '/icewind/smb/src/Exception/InvalidRequestException.php', + 'Icewind\\SMB\\Exception\\InvalidResourceException' => $vendorDir . '/icewind/smb/src/Exception/InvalidResourceException.php', + 'Icewind\\SMB\\Exception\\InvalidTicket' => $vendorDir . '/icewind/smb/src/Exception/InvalidTicket.php', + 'Icewind\\SMB\\Exception\\InvalidTypeException' => $vendorDir . '/icewind/smb/src/Exception/InvalidTypeException.php', + 'Icewind\\SMB\\Exception\\NoLoginServerException' => $vendorDir . '/icewind/smb/src/Exception/NoLoginServerException.php', + 'Icewind\\SMB\\Exception\\NoRouteToHostException' => $vendorDir . '/icewind/smb/src/Exception/NoRouteToHostException.php', + 'Icewind\\SMB\\Exception\\NotEmptyException' => $vendorDir . '/icewind/smb/src/Exception/NotEmptyException.php', + 'Icewind\\SMB\\Exception\\NotFoundException' => $vendorDir . '/icewind/smb/src/Exception/NotFoundException.php', + 'Icewind\\SMB\\Exception\\OutOfSpaceException' => $vendorDir . '/icewind/smb/src/Exception/OutOfSpaceException.php', + 'Icewind\\SMB\\Exception\\RevisionMismatchException' => $vendorDir . '/icewind/smb/src/Exception/RevisionMismatchException.php', + 'Icewind\\SMB\\Exception\\TimedOutException' => $vendorDir . '/icewind/smb/src/Exception/TimedOutException.php', + 'Icewind\\SMB\\IAuth' => $vendorDir . '/icewind/smb/src/IAuth.php', + 'Icewind\\SMB\\IFileInfo' => $vendorDir . '/icewind/smb/src/IFileInfo.php', + 'Icewind\\SMB\\INotifyHandler' => $vendorDir . '/icewind/smb/src/INotifyHandler.php', + 'Icewind\\SMB\\IOptions' => $vendorDir . '/icewind/smb/src/IOptions.php', + 'Icewind\\SMB\\IServer' => $vendorDir . '/icewind/smb/src/IServer.php', + 'Icewind\\SMB\\IShare' => $vendorDir . '/icewind/smb/src/IShare.php', + 'Icewind\\SMB\\ISystem' => $vendorDir . '/icewind/smb/src/ISystem.php', + 'Icewind\\SMB\\ITimeZoneProvider' => $vendorDir . '/icewind/smb/src/ITimeZoneProvider.php', + 'Icewind\\SMB\\KerberosApacheAuth' => $vendorDir . '/icewind/smb/src/KerberosApacheAuth.php', + 'Icewind\\SMB\\KerberosAuth' => $vendorDir . '/icewind/smb/src/KerberosAuth.php', + 'Icewind\\SMB\\KerberosTicket' => $vendorDir . '/icewind/smb/src/KerberosTicket.php', + 'Icewind\\SMB\\Native\\NativeFileInfo' => $vendorDir . '/icewind/smb/src/Native/NativeFileInfo.php', + 'Icewind\\SMB\\Native\\NativeReadStream' => $vendorDir . '/icewind/smb/src/Native/NativeReadStream.php', + 'Icewind\\SMB\\Native\\NativeServer' => $vendorDir . '/icewind/smb/src/Native/NativeServer.php', + 'Icewind\\SMB\\Native\\NativeShare' => $vendorDir . '/icewind/smb/src/Native/NativeShare.php', + 'Icewind\\SMB\\Native\\NativeState' => $vendorDir . '/icewind/smb/src/Native/NativeState.php', + 'Icewind\\SMB\\Native\\NativeStream' => $vendorDir . '/icewind/smb/src/Native/NativeStream.php', + 'Icewind\\SMB\\Native\\NativeWriteStream' => $vendorDir . '/icewind/smb/src/Native/NativeWriteStream.php', + 'Icewind\\SMB\\Options' => $vendorDir . '/icewind/smb/src/Options.php', + 'Icewind\\SMB\\ServerFactory' => $vendorDir . '/icewind/smb/src/ServerFactory.php', + 'Icewind\\SMB\\StringBuffer' => $vendorDir . '/icewind/smb/src/StringBuffer.php', + 'Icewind\\SMB\\System' => $vendorDir . '/icewind/smb/src/System.php', + 'Icewind\\SMB\\TimeZoneProvider' => $vendorDir . '/icewind/smb/src/TimeZoneProvider.php', + 'Icewind\\SMB\\Wrapped\\Connection' => $vendorDir . '/icewind/smb/src/Wrapped/Connection.php', + 'Icewind\\SMB\\Wrapped\\ErrorCodes' => $vendorDir . '/icewind/smb/src/Wrapped/ErrorCodes.php', + 'Icewind\\SMB\\Wrapped\\FileInfo' => $vendorDir . '/icewind/smb/src/Wrapped/FileInfo.php', + 'Icewind\\SMB\\Wrapped\\NotifyHandler' => $vendorDir . '/icewind/smb/src/Wrapped/NotifyHandler.php', + 'Icewind\\SMB\\Wrapped\\Parser' => $vendorDir . '/icewind/smb/src/Wrapped/Parser.php', + 'Icewind\\SMB\\Wrapped\\RawConnection' => $vendorDir . '/icewind/smb/src/Wrapped/RawConnection.php', + 'Icewind\\SMB\\Wrapped\\Server' => $vendorDir . '/icewind/smb/src/Wrapped/Server.php', + 'Icewind\\SMB\\Wrapped\\Share' => $vendorDir . '/icewind/smb/src/Wrapped/Share.php', + 'Icewind\\Streams\\CallbackWrapper' => $vendorDir . '/icewind/streams/src/CallbackWrapper.php', + 'Icewind\\Streams\\CountWrapper' => $vendorDir . '/icewind/streams/src/CountWrapper.php', + 'Icewind\\Streams\\Directory' => $vendorDir . '/icewind/streams/src/Directory.php', + 'Icewind\\Streams\\DirectoryFilter' => $vendorDir . '/icewind/streams/src/DirectoryFilter.php', + 'Icewind\\Streams\\DirectoryWrapper' => $vendorDir . '/icewind/streams/src/DirectoryWrapper.php', + 'Icewind\\Streams\\File' => $vendorDir . '/icewind/streams/src/File.php', + 'Icewind\\Streams\\HashWrapper' => $vendorDir . '/icewind/streams/src/HashWrapper.php', + 'Icewind\\Streams\\IteratorDirectory' => $vendorDir . '/icewind/streams/src/IteratorDirectory.php', + 'Icewind\\Streams\\NullWrapper' => $vendorDir . '/icewind/streams/src/NullWrapper.php', + 'Icewind\\Streams\\Path' => $vendorDir . '/icewind/streams/src/Path.php', + 'Icewind\\Streams\\PathWrapper' => $vendorDir . '/icewind/streams/src/PathWrapper.php', + 'Icewind\\Streams\\ReadHashWrapper' => $vendorDir . '/icewind/streams/src/ReadHashWrapper.php', + 'Icewind\\Streams\\RetryWrapper' => $vendorDir . '/icewind/streams/src/RetryWrapper.php', + 'Icewind\\Streams\\SeekableWrapper' => $vendorDir . '/icewind/streams/src/SeekableWrapper.php', + 'Icewind\\Streams\\Url' => $vendorDir . '/icewind/streams/src/Url.php', + 'Icewind\\Streams\\UrlCallback' => $vendorDir . '/icewind/streams/src/UrlCallback.php', + 'Icewind\\Streams\\Wrapper' => $vendorDir . '/icewind/streams/src/Wrapper.php', + 'Icewind\\Streams\\WrapperHandler' => $vendorDir . '/icewind/streams/src/WrapperHandler.php', + 'Icewind\\Streams\\WriteHashWrapper' => $vendorDir . '/icewind/streams/src/WriteHashWrapper.php', + 'JmesPath\\AstRuntime' => $vendorDir . '/mtdowling/jmespath.php/src/AstRuntime.php', + 'JmesPath\\CompilerRuntime' => $vendorDir . '/mtdowling/jmespath.php/src/CompilerRuntime.php', + 'JmesPath\\DebugRuntime' => $vendorDir . '/mtdowling/jmespath.php/src/DebugRuntime.php', + 'JmesPath\\Env' => $vendorDir . '/mtdowling/jmespath.php/src/Env.php', + 'JmesPath\\FnDispatcher' => $vendorDir . '/mtdowling/jmespath.php/src/FnDispatcher.php', + 'JmesPath\\Lexer' => $vendorDir . '/mtdowling/jmespath.php/src/Lexer.php', + 'JmesPath\\Parser' => $vendorDir . '/mtdowling/jmespath.php/src/Parser.php', + 'JmesPath\\SyntaxErrorException' => $vendorDir . '/mtdowling/jmespath.php/src/SyntaxErrorException.php', + 'JmesPath\\TreeCompiler' => $vendorDir . '/mtdowling/jmespath.php/src/TreeCompiler.php', + 'JmesPath\\TreeInterpreter' => $vendorDir . '/mtdowling/jmespath.php/src/TreeInterpreter.php', + 'JmesPath\\Utils' => $vendorDir . '/mtdowling/jmespath.php/src/Utils.php', + 'JsonSchema\\ConstraintError' => $vendorDir . '/justinrainbow/json-schema/src/JsonSchema/ConstraintError.php', + 'JsonSchema\\Constraints\\BaseConstraint' => $vendorDir . '/justinrainbow/json-schema/src/JsonSchema/Constraints/BaseConstraint.php', + 'JsonSchema\\Constraints\\CollectionConstraint' => $vendorDir . '/justinrainbow/json-schema/src/JsonSchema/Constraints/CollectionConstraint.php', + 'JsonSchema\\Constraints\\ConstConstraint' => $vendorDir . '/justinrainbow/json-schema/src/JsonSchema/Constraints/ConstConstraint.php', + 'JsonSchema\\Constraints\\Constraint' => $vendorDir . '/justinrainbow/json-schema/src/JsonSchema/Constraints/Constraint.php', + 'JsonSchema\\Constraints\\ConstraintInterface' => $vendorDir . '/justinrainbow/json-schema/src/JsonSchema/Constraints/ConstraintInterface.php', + 'JsonSchema\\Constraints\\EnumConstraint' => $vendorDir . '/justinrainbow/json-schema/src/JsonSchema/Constraints/EnumConstraint.php', + 'JsonSchema\\Constraints\\Factory' => $vendorDir . '/justinrainbow/json-schema/src/JsonSchema/Constraints/Factory.php', + 'JsonSchema\\Constraints\\FormatConstraint' => $vendorDir . '/justinrainbow/json-schema/src/JsonSchema/Constraints/FormatConstraint.php', + 'JsonSchema\\Constraints\\NumberConstraint' => $vendorDir . '/justinrainbow/json-schema/src/JsonSchema/Constraints/NumberConstraint.php', + 'JsonSchema\\Constraints\\ObjectConstraint' => $vendorDir . '/justinrainbow/json-schema/src/JsonSchema/Constraints/ObjectConstraint.php', + 'JsonSchema\\Constraints\\SchemaConstraint' => $vendorDir . '/justinrainbow/json-schema/src/JsonSchema/Constraints/SchemaConstraint.php', + 'JsonSchema\\Constraints\\StringConstraint' => $vendorDir . '/justinrainbow/json-schema/src/JsonSchema/Constraints/StringConstraint.php', + 'JsonSchema\\Constraints\\TypeCheck\\LooseTypeCheck' => $vendorDir . '/justinrainbow/json-schema/src/JsonSchema/Constraints/TypeCheck/LooseTypeCheck.php', + 'JsonSchema\\Constraints\\TypeCheck\\StrictTypeCheck' => $vendorDir . '/justinrainbow/json-schema/src/JsonSchema/Constraints/TypeCheck/StrictTypeCheck.php', + 'JsonSchema\\Constraints\\TypeCheck\\TypeCheckInterface' => $vendorDir . '/justinrainbow/json-schema/src/JsonSchema/Constraints/TypeCheck/TypeCheckInterface.php', + 'JsonSchema\\Constraints\\TypeConstraint' => $vendorDir . '/justinrainbow/json-schema/src/JsonSchema/Constraints/TypeConstraint.php', + 'JsonSchema\\Constraints\\UndefinedConstraint' => $vendorDir . '/justinrainbow/json-schema/src/JsonSchema/Constraints/UndefinedConstraint.php', + 'JsonSchema\\Entity\\JsonPointer' => $vendorDir . '/justinrainbow/json-schema/src/JsonSchema/Entity/JsonPointer.php', + 'JsonSchema\\Enum' => $vendorDir . '/justinrainbow/json-schema/src/JsonSchema/Enum.php', + 'JsonSchema\\Exception\\ExceptionInterface' => $vendorDir . '/justinrainbow/json-schema/src/JsonSchema/Exception/ExceptionInterface.php', + 'JsonSchema\\Exception\\InvalidArgumentException' => $vendorDir . '/justinrainbow/json-schema/src/JsonSchema/Exception/InvalidArgumentException.php', + 'JsonSchema\\Exception\\InvalidConfigException' => $vendorDir . '/justinrainbow/json-schema/src/JsonSchema/Exception/InvalidConfigException.php', + 'JsonSchema\\Exception\\InvalidSchemaException' => $vendorDir . '/justinrainbow/json-schema/src/JsonSchema/Exception/InvalidSchemaException.php', + 'JsonSchema\\Exception\\InvalidSchemaMediaTypeException' => $vendorDir . '/justinrainbow/json-schema/src/JsonSchema/Exception/InvalidSchemaMediaTypeException.php', + 'JsonSchema\\Exception\\InvalidSourceUriException' => $vendorDir . '/justinrainbow/json-schema/src/JsonSchema/Exception/InvalidSourceUriException.php', + 'JsonSchema\\Exception\\JsonDecodingException' => $vendorDir . '/justinrainbow/json-schema/src/JsonSchema/Exception/JsonDecodingException.php', + 'JsonSchema\\Exception\\ResourceNotFoundException' => $vendorDir . '/justinrainbow/json-schema/src/JsonSchema/Exception/ResourceNotFoundException.php', + 'JsonSchema\\Exception\\RuntimeException' => $vendorDir . '/justinrainbow/json-schema/src/JsonSchema/Exception/RuntimeException.php', + 'JsonSchema\\Exception\\UnresolvableJsonPointerException' => $vendorDir . '/justinrainbow/json-schema/src/JsonSchema/Exception/UnresolvableJsonPointerException.php', + 'JsonSchema\\Exception\\UriResolverException' => $vendorDir . '/justinrainbow/json-schema/src/JsonSchema/Exception/UriResolverException.php', + 'JsonSchema\\Exception\\ValidationException' => $vendorDir . '/justinrainbow/json-schema/src/JsonSchema/Exception/ValidationException.php', + 'JsonSchema\\Iterator\\ObjectIterator' => $vendorDir . '/justinrainbow/json-schema/src/JsonSchema/Iterator/ObjectIterator.php', + 'JsonSchema\\Rfc3339' => $vendorDir . '/justinrainbow/json-schema/src/JsonSchema/Rfc3339.php', + 'JsonSchema\\SchemaStorage' => $vendorDir . '/justinrainbow/json-schema/src/JsonSchema/SchemaStorage.php', + 'JsonSchema\\SchemaStorageInterface' => $vendorDir . '/justinrainbow/json-schema/src/JsonSchema/SchemaStorageInterface.php', + 'JsonSchema\\Tool\\DeepComparer' => $vendorDir . '/justinrainbow/json-schema/src/JsonSchema/Tool/DeepComparer.php', + 'JsonSchema\\Tool\\DeepCopy' => $vendorDir . '/justinrainbow/json-schema/src/JsonSchema/Tool/DeepCopy.php', + 'JsonSchema\\Tool\\Validator\\RelativeReferenceValidator' => $vendorDir . '/justinrainbow/json-schema/src/JsonSchema/Tool/Validator/RelativeReferenceValidator.php', + 'JsonSchema\\Tool\\Validator\\UriValidator' => $vendorDir . '/justinrainbow/json-schema/src/JsonSchema/Tool/Validator/UriValidator.php', + 'JsonSchema\\UriResolverInterface' => $vendorDir . '/justinrainbow/json-schema/src/JsonSchema/UriResolverInterface.php', + 'JsonSchema\\UriRetrieverInterface' => $vendorDir . '/justinrainbow/json-schema/src/JsonSchema/UriRetrieverInterface.php', + 'JsonSchema\\Uri\\Retrievers\\AbstractRetriever' => $vendorDir . '/justinrainbow/json-schema/src/JsonSchema/Uri/Retrievers/AbstractRetriever.php', + 'JsonSchema\\Uri\\Retrievers\\Curl' => $vendorDir . '/justinrainbow/json-schema/src/JsonSchema/Uri/Retrievers/Curl.php', + 'JsonSchema\\Uri\\Retrievers\\FileGetContents' => $vendorDir . '/justinrainbow/json-schema/src/JsonSchema/Uri/Retrievers/FileGetContents.php', + 'JsonSchema\\Uri\\Retrievers\\PredefinedArray' => $vendorDir . '/justinrainbow/json-schema/src/JsonSchema/Uri/Retrievers/PredefinedArray.php', + 'JsonSchema\\Uri\\Retrievers\\UriRetrieverInterface' => $vendorDir . '/justinrainbow/json-schema/src/JsonSchema/Uri/Retrievers/UriRetrieverInterface.php', + 'JsonSchema\\Uri\\UriResolver' => $vendorDir . '/justinrainbow/json-schema/src/JsonSchema/Uri/UriResolver.php', + 'JsonSchema\\Uri\\UriRetriever' => $vendorDir . '/justinrainbow/json-schema/src/JsonSchema/Uri/UriRetriever.php', + 'JsonSchema\\Validator' => $vendorDir . '/justinrainbow/json-schema/src/JsonSchema/Validator.php', + 'Laravel\\SerializableClosure\\Contracts\\Serializable' => $vendorDir . '/laravel/serializable-closure/src/Contracts/Serializable.php', + 'Laravel\\SerializableClosure\\Contracts\\Signer' => $vendorDir . '/laravel/serializable-closure/src/Contracts/Signer.php', + 'Laravel\\SerializableClosure\\Exceptions\\InvalidSignatureException' => $vendorDir . '/laravel/serializable-closure/src/Exceptions/InvalidSignatureException.php', + 'Laravel\\SerializableClosure\\Exceptions\\MissingSecretKeyException' => $vendorDir . '/laravel/serializable-closure/src/Exceptions/MissingSecretKeyException.php', + 'Laravel\\SerializableClosure\\Exceptions\\PhpVersionNotSupportedException' => $vendorDir . '/laravel/serializable-closure/src/Exceptions/PhpVersionNotSupportedException.php', + 'Laravel\\SerializableClosure\\SerializableClosure' => $vendorDir . '/laravel/serializable-closure/src/SerializableClosure.php', + 'Laravel\\SerializableClosure\\Serializers\\Native' => $vendorDir . '/laravel/serializable-closure/src/Serializers/Native.php', + 'Laravel\\SerializableClosure\\Serializers\\Signed' => $vendorDir . '/laravel/serializable-closure/src/Serializers/Signed.php', + 'Laravel\\SerializableClosure\\Signers\\Hmac' => $vendorDir . '/laravel/serializable-closure/src/Signers/Hmac.php', + 'Laravel\\SerializableClosure\\Support\\ClosureScope' => $vendorDir . '/laravel/serializable-closure/src/Support/ClosureScope.php', + 'Laravel\\SerializableClosure\\Support\\ClosureStream' => $vendorDir . '/laravel/serializable-closure/src/Support/ClosureStream.php', + 'Laravel\\SerializableClosure\\Support\\ReflectionClosure' => $vendorDir . '/laravel/serializable-closure/src/Support/ReflectionClosure.php', + 'Laravel\\SerializableClosure\\Support\\SelfReference' => $vendorDir . '/laravel/serializable-closure/src/Support/SelfReference.php', + 'Laravel\\SerializableClosure\\UnsignedSerializableClosure' => $vendorDir . '/laravel/serializable-closure/src/UnsignedSerializableClosure.php', + 'Lcobucci\\Clock\\Clock' => $vendorDir . '/lcobucci/clock/src/Clock.php', + 'Lcobucci\\Clock\\FrozenClock' => $vendorDir . '/lcobucci/clock/src/FrozenClock.php', + 'Lcobucci\\Clock\\SystemClock' => $vendorDir . '/lcobucci/clock/src/SystemClock.php', + 'MabeEnum\\Enum' => $vendorDir . '/marc-mabe/php-enum/src/Enum.php', + 'MabeEnum\\EnumMap' => $vendorDir . '/marc-mabe/php-enum/src/EnumMap.php', + 'MabeEnum\\EnumSerializableTrait' => $vendorDir . '/marc-mabe/php-enum/src/EnumSerializableTrait.php', + 'MabeEnum\\EnumSet' => $vendorDir . '/marc-mabe/php-enum/src/EnumSet.php', + 'Masterminds\\HTML5' => $vendorDir . '/masterminds/html5/src/HTML5.php', + 'Masterminds\\HTML5\\Elements' => $vendorDir . '/masterminds/html5/src/HTML5/Elements.php', + 'Masterminds\\HTML5\\Entities' => $vendorDir . '/masterminds/html5/src/HTML5/Entities.php', + 'Masterminds\\HTML5\\Exception' => $vendorDir . '/masterminds/html5/src/HTML5/Exception.php', + 'Masterminds\\HTML5\\InstructionProcessor' => $vendorDir . '/masterminds/html5/src/HTML5/InstructionProcessor.php', + 'Masterminds\\HTML5\\Parser\\CharacterReference' => $vendorDir . '/masterminds/html5/src/HTML5/Parser/CharacterReference.php', + 'Masterminds\\HTML5\\Parser\\DOMTreeBuilder' => $vendorDir . '/masterminds/html5/src/HTML5/Parser/DOMTreeBuilder.php', + 'Masterminds\\HTML5\\Parser\\EventHandler' => $vendorDir . '/masterminds/html5/src/HTML5/Parser/EventHandler.php', + 'Masterminds\\HTML5\\Parser\\FileInputStream' => $vendorDir . '/masterminds/html5/src/HTML5/Parser/FileInputStream.php', + 'Masterminds\\HTML5\\Parser\\InputStream' => $vendorDir . '/masterminds/html5/src/HTML5/Parser/InputStream.php', + 'Masterminds\\HTML5\\Parser\\ParseError' => $vendorDir . '/masterminds/html5/src/HTML5/Parser/ParseError.php', + 'Masterminds\\HTML5\\Parser\\Scanner' => $vendorDir . '/masterminds/html5/src/HTML5/Parser/Scanner.php', + 'Masterminds\\HTML5\\Parser\\StringInputStream' => $vendorDir . '/masterminds/html5/src/HTML5/Parser/StringInputStream.php', + 'Masterminds\\HTML5\\Parser\\Tokenizer' => $vendorDir . '/masterminds/html5/src/HTML5/Parser/Tokenizer.php', + 'Masterminds\\HTML5\\Parser\\TreeBuildingRules' => $vendorDir . '/masterminds/html5/src/HTML5/Parser/TreeBuildingRules.php', + 'Masterminds\\HTML5\\Parser\\UTF8Utils' => $vendorDir . '/masterminds/html5/src/HTML5/Parser/UTF8Utils.php', + 'Masterminds\\HTML5\\Serializer\\HTML5Entities' => $vendorDir . '/masterminds/html5/src/HTML5/Serializer/HTML5Entities.php', + 'Masterminds\\HTML5\\Serializer\\OutputRules' => $vendorDir . '/masterminds/html5/src/HTML5/Serializer/OutputRules.php', + 'Masterminds\\HTML5\\Serializer\\RulesInterface' => $vendorDir . '/masterminds/html5/src/HTML5/Serializer/RulesInterface.php', + 'Masterminds\\HTML5\\Serializer\\Traverser' => $vendorDir . '/masterminds/html5/src/HTML5/Serializer/Traverser.php', + 'Mexitek\\PHPColors\\Color' => $vendorDir . '/mexitek/phpcolors/src/Mexitek/PHPColors/Color.php', + 'MicrosoftAzure\\Storage\\Blob\\BlobRestProxy' => $vendorDir . '/microsoft/azure-storage-blob/src/Blob/BlobRestProxy.php', + 'MicrosoftAzure\\Storage\\Blob\\BlobSharedAccessSignatureHelper' => $vendorDir . '/microsoft/azure-storage-blob/src/Blob/BlobSharedAccessSignatureHelper.php', + 'MicrosoftAzure\\Storage\\Blob\\Internal\\BlobResources' => $vendorDir . '/microsoft/azure-storage-blob/src/Blob/Internal/BlobResources.php', + 'MicrosoftAzure\\Storage\\Blob\\Internal\\IBlob' => $vendorDir . '/microsoft/azure-storage-blob/src/Blob/Internal/IBlob.php', + 'MicrosoftAzure\\Storage\\Blob\\Models\\AccessCondition' => $vendorDir . '/microsoft/azure-storage-blob/src/Blob/Models/AccessCondition.php', + 'MicrosoftAzure\\Storage\\Blob\\Models\\AccessTierTrait' => $vendorDir . '/microsoft/azure-storage-blob/src/Blob/Models/AccessTierTrait.php', + 'MicrosoftAzure\\Storage\\Blob\\Models\\AppendBlockOptions' => $vendorDir . '/microsoft/azure-storage-blob/src/Blob/Models/AppendBlockOptions.php', + 'MicrosoftAzure\\Storage\\Blob\\Models\\AppendBlockResult' => $vendorDir . '/microsoft/azure-storage-blob/src/Blob/Models/AppendBlockResult.php', + 'MicrosoftAzure\\Storage\\Blob\\Models\\Blob' => $vendorDir . '/microsoft/azure-storage-blob/src/Blob/Models/Blob.php', + 'MicrosoftAzure\\Storage\\Blob\\Models\\BlobAccessPolicy' => $vendorDir . '/microsoft/azure-storage-blob/src/Blob/Models/BlobAccessPolicy.php', + 'MicrosoftAzure\\Storage\\Blob\\Models\\BlobBlockType' => $vendorDir . '/microsoft/azure-storage-blob/src/Blob/Models/BlobBlockType.php', + 'MicrosoftAzure\\Storage\\Blob\\Models\\BlobPrefix' => $vendorDir . '/microsoft/azure-storage-blob/src/Blob/Models/BlobPrefix.php', + 'MicrosoftAzure\\Storage\\Blob\\Models\\BlobProperties' => $vendorDir . '/microsoft/azure-storage-blob/src/Blob/Models/BlobProperties.php', + 'MicrosoftAzure\\Storage\\Blob\\Models\\BlobServiceOptions' => $vendorDir . '/microsoft/azure-storage-blob/src/Blob/Models/BlobServiceOptions.php', + 'MicrosoftAzure\\Storage\\Blob\\Models\\BlobType' => $vendorDir . '/microsoft/azure-storage-blob/src/Blob/Models/BlobType.php', + 'MicrosoftAzure\\Storage\\Blob\\Models\\Block' => $vendorDir . '/microsoft/azure-storage-blob/src/Blob/Models/Block.php', + 'MicrosoftAzure\\Storage\\Blob\\Models\\BlockList' => $vendorDir . '/microsoft/azure-storage-blob/src/Blob/Models/BlockList.php', + 'MicrosoftAzure\\Storage\\Blob\\Models\\BreakLeaseResult' => $vendorDir . '/microsoft/azure-storage-blob/src/Blob/Models/BreakLeaseResult.php', + 'MicrosoftAzure\\Storage\\Blob\\Models\\CommitBlobBlocksOptions' => $vendorDir . '/microsoft/azure-storage-blob/src/Blob/Models/CommitBlobBlocksOptions.php', + 'MicrosoftAzure\\Storage\\Blob\\Models\\Container' => $vendorDir . '/microsoft/azure-storage-blob/src/Blob/Models/Container.php', + 'MicrosoftAzure\\Storage\\Blob\\Models\\ContainerACL' => $vendorDir . '/microsoft/azure-storage-blob/src/Blob/Models/ContainerACL.php', + 'MicrosoftAzure\\Storage\\Blob\\Models\\ContainerAccessPolicy' => $vendorDir . '/microsoft/azure-storage-blob/src/Blob/Models/ContainerAccessPolicy.php', + 'MicrosoftAzure\\Storage\\Blob\\Models\\ContainerProperties' => $vendorDir . '/microsoft/azure-storage-blob/src/Blob/Models/ContainerProperties.php', + 'MicrosoftAzure\\Storage\\Blob\\Models\\CopyBlobFromURLOptions' => $vendorDir . '/microsoft/azure-storage-blob/src/Blob/Models/CopyBlobFromURLOptions.php', + 'MicrosoftAzure\\Storage\\Blob\\Models\\CopyBlobOptions' => $vendorDir . '/microsoft/azure-storage-blob/src/Blob/Models/CopyBlobOptions.php', + 'MicrosoftAzure\\Storage\\Blob\\Models\\CopyBlobResult' => $vendorDir . '/microsoft/azure-storage-blob/src/Blob/Models/CopyBlobResult.php', + 'MicrosoftAzure\\Storage\\Blob\\Models\\CopyState' => $vendorDir . '/microsoft/azure-storage-blob/src/Blob/Models/CopyState.php', + 'MicrosoftAzure\\Storage\\Blob\\Models\\CreateBlobBlockOptions' => $vendorDir . '/microsoft/azure-storage-blob/src/Blob/Models/CreateBlobBlockOptions.php', + 'MicrosoftAzure\\Storage\\Blob\\Models\\CreateBlobOptions' => $vendorDir . '/microsoft/azure-storage-blob/src/Blob/Models/CreateBlobOptions.php', + 'MicrosoftAzure\\Storage\\Blob\\Models\\CreateBlobPagesOptions' => $vendorDir . '/microsoft/azure-storage-blob/src/Blob/Models/CreateBlobPagesOptions.php', + 'MicrosoftAzure\\Storage\\Blob\\Models\\CreateBlobPagesResult' => $vendorDir . '/microsoft/azure-storage-blob/src/Blob/Models/CreateBlobPagesResult.php', + 'MicrosoftAzure\\Storage\\Blob\\Models\\CreateBlobSnapshotOptions' => $vendorDir . '/microsoft/azure-storage-blob/src/Blob/Models/CreateBlobSnapshotOptions.php', + 'MicrosoftAzure\\Storage\\Blob\\Models\\CreateBlobSnapshotResult' => $vendorDir . '/microsoft/azure-storage-blob/src/Blob/Models/CreateBlobSnapshotResult.php', + 'MicrosoftAzure\\Storage\\Blob\\Models\\CreateBlockBlobOptions' => $vendorDir . '/microsoft/azure-storage-blob/src/Blob/Models/CreateBlockBlobOptions.php', + 'MicrosoftAzure\\Storage\\Blob\\Models\\CreateContainerOptions' => $vendorDir . '/microsoft/azure-storage-blob/src/Blob/Models/CreateContainerOptions.php', + 'MicrosoftAzure\\Storage\\Blob\\Models\\CreatePageBlobFromContentOptions' => $vendorDir . '/microsoft/azure-storage-blob/src/Blob/Models/CreatePageBlobFromContentOptions.php', + 'MicrosoftAzure\\Storage\\Blob\\Models\\CreatePageBlobOptions' => $vendorDir . '/microsoft/azure-storage-blob/src/Blob/Models/CreatePageBlobOptions.php', + 'MicrosoftAzure\\Storage\\Blob\\Models\\DeleteBlobOptions' => $vendorDir . '/microsoft/azure-storage-blob/src/Blob/Models/DeleteBlobOptions.php', + 'MicrosoftAzure\\Storage\\Blob\\Models\\GetBlobMetadataOptions' => $vendorDir . '/microsoft/azure-storage-blob/src/Blob/Models/GetBlobMetadataOptions.php', + 'MicrosoftAzure\\Storage\\Blob\\Models\\GetBlobMetadataResult' => $vendorDir . '/microsoft/azure-storage-blob/src/Blob/Models/GetBlobMetadataResult.php', + 'MicrosoftAzure\\Storage\\Blob\\Models\\GetBlobOptions' => $vendorDir . '/microsoft/azure-storage-blob/src/Blob/Models/GetBlobOptions.php', + 'MicrosoftAzure\\Storage\\Blob\\Models\\GetBlobPropertiesOptions' => $vendorDir . '/microsoft/azure-storage-blob/src/Blob/Models/GetBlobPropertiesOptions.php', + 'MicrosoftAzure\\Storage\\Blob\\Models\\GetBlobPropertiesResult' => $vendorDir . '/microsoft/azure-storage-blob/src/Blob/Models/GetBlobPropertiesResult.php', + 'MicrosoftAzure\\Storage\\Blob\\Models\\GetBlobResult' => $vendorDir . '/microsoft/azure-storage-blob/src/Blob/Models/GetBlobResult.php', + 'MicrosoftAzure\\Storage\\Blob\\Models\\GetContainerACLResult' => $vendorDir . '/microsoft/azure-storage-blob/src/Blob/Models/GetContainerACLResult.php', + 'MicrosoftAzure\\Storage\\Blob\\Models\\GetContainerPropertiesResult' => $vendorDir . '/microsoft/azure-storage-blob/src/Blob/Models/GetContainerPropertiesResult.php', + 'MicrosoftAzure\\Storage\\Blob\\Models\\LeaseMode' => $vendorDir . '/microsoft/azure-storage-blob/src/Blob/Models/LeaseMode.php', + 'MicrosoftAzure\\Storage\\Blob\\Models\\LeaseResult' => $vendorDir . '/microsoft/azure-storage-blob/src/Blob/Models/LeaseResult.php', + 'MicrosoftAzure\\Storage\\Blob\\Models\\ListBlobBlocksOptions' => $vendorDir . '/microsoft/azure-storage-blob/src/Blob/Models/ListBlobBlocksOptions.php', + 'MicrosoftAzure\\Storage\\Blob\\Models\\ListBlobBlocksResult' => $vendorDir . '/microsoft/azure-storage-blob/src/Blob/Models/ListBlobBlocksResult.php', + 'MicrosoftAzure\\Storage\\Blob\\Models\\ListBlobsOptions' => $vendorDir . '/microsoft/azure-storage-blob/src/Blob/Models/ListBlobsOptions.php', + 'MicrosoftAzure\\Storage\\Blob\\Models\\ListBlobsResult' => $vendorDir . '/microsoft/azure-storage-blob/src/Blob/Models/ListBlobsResult.php', + 'MicrosoftAzure\\Storage\\Blob\\Models\\ListContainersOptions' => $vendorDir . '/microsoft/azure-storage-blob/src/Blob/Models/ListContainersOptions.php', + 'MicrosoftAzure\\Storage\\Blob\\Models\\ListContainersResult' => $vendorDir . '/microsoft/azure-storage-blob/src/Blob/Models/ListContainersResult.php', + 'MicrosoftAzure\\Storage\\Blob\\Models\\ListPageBlobRangesDiffResult' => $vendorDir . '/microsoft/azure-storage-blob/src/Blob/Models/ListPageBlobRangesDiffResult.php', + 'MicrosoftAzure\\Storage\\Blob\\Models\\ListPageBlobRangesOptions' => $vendorDir . '/microsoft/azure-storage-blob/src/Blob/Models/ListPageBlobRangesOptions.php', + 'MicrosoftAzure\\Storage\\Blob\\Models\\ListPageBlobRangesResult' => $vendorDir . '/microsoft/azure-storage-blob/src/Blob/Models/ListPageBlobRangesResult.php', + 'MicrosoftAzure\\Storage\\Blob\\Models\\PageWriteOption' => $vendorDir . '/microsoft/azure-storage-blob/src/Blob/Models/PageWriteOption.php', + 'MicrosoftAzure\\Storage\\Blob\\Models\\PublicAccessType' => $vendorDir . '/microsoft/azure-storage-blob/src/Blob/Models/PublicAccessType.php', + 'MicrosoftAzure\\Storage\\Blob\\Models\\PutBlobResult' => $vendorDir . '/microsoft/azure-storage-blob/src/Blob/Models/PutBlobResult.php', + 'MicrosoftAzure\\Storage\\Blob\\Models\\PutBlockResult' => $vendorDir . '/microsoft/azure-storage-blob/src/Blob/Models/PutBlockResult.php', + 'MicrosoftAzure\\Storage\\Blob\\Models\\SetBlobMetadataResult' => $vendorDir . '/microsoft/azure-storage-blob/src/Blob/Models/SetBlobMetadataResult.php', + 'MicrosoftAzure\\Storage\\Blob\\Models\\SetBlobPropertiesOptions' => $vendorDir . '/microsoft/azure-storage-blob/src/Blob/Models/SetBlobPropertiesOptions.php', + 'MicrosoftAzure\\Storage\\Blob\\Models\\SetBlobPropertiesResult' => $vendorDir . '/microsoft/azure-storage-blob/src/Blob/Models/SetBlobPropertiesResult.php', + 'MicrosoftAzure\\Storage\\Blob\\Models\\SetBlobTierOptions' => $vendorDir . '/microsoft/azure-storage-blob/src/Blob/Models/SetBlobTierOptions.php', + 'MicrosoftAzure\\Storage\\Blob\\Models\\UndeleteBlobOptions' => $vendorDir . '/microsoft/azure-storage-blob/src/Blob/Models/UndeleteBlobOptions.php', + 'MicrosoftAzure\\Storage\\Common\\CloudConfigurationManager' => $vendorDir . '/microsoft/azure-storage-common/src/Common/CloudConfigurationManager.php', + 'MicrosoftAzure\\Storage\\Common\\Exceptions\\InvalidArgumentTypeException' => $vendorDir . '/microsoft/azure-storage-common/src/Common/Exceptions/InvalidArgumentTypeException.php', + 'MicrosoftAzure\\Storage\\Common\\Exceptions\\ServiceException' => $vendorDir . '/microsoft/azure-storage-common/src/Common/Exceptions/ServiceException.php', + 'MicrosoftAzure\\Storage\\Common\\Internal\\ACLBase' => $vendorDir . '/microsoft/azure-storage-common/src/Common/Internal/ACLBase.php', + 'MicrosoftAzure\\Storage\\Common\\Internal\\Authentication\\IAuthScheme' => $vendorDir . '/microsoft/azure-storage-common/src/Common/Internal/Authentication/IAuthScheme.php', + 'MicrosoftAzure\\Storage\\Common\\Internal\\Authentication\\SharedAccessSignatureAuthScheme' => $vendorDir . '/microsoft/azure-storage-common/src/Common/Internal/Authentication/SharedAccessSignatureAuthScheme.php', + 'MicrosoftAzure\\Storage\\Common\\Internal\\Authentication\\SharedKeyAuthScheme' => $vendorDir . '/microsoft/azure-storage-common/src/Common/Internal/Authentication/SharedKeyAuthScheme.php', + 'MicrosoftAzure\\Storage\\Common\\Internal\\Authentication\\TokenAuthScheme' => $vendorDir . '/microsoft/azure-storage-common/src/Common/Internal/Authentication/TokenAuthScheme.php', + 'MicrosoftAzure\\Storage\\Common\\Internal\\ConnectionStringParser' => $vendorDir . '/microsoft/azure-storage-common/src/Common/Internal/ConnectionStringParser.php', + 'MicrosoftAzure\\Storage\\Common\\Internal\\ConnectionStringSource' => $vendorDir . '/microsoft/azure-storage-common/src/Common/Internal/ConnectionStringSource.php', + 'MicrosoftAzure\\Storage\\Common\\Internal\\Http\\HttpCallContext' => $vendorDir . '/microsoft/azure-storage-common/src/Common/Internal/Http/HttpCallContext.php', + 'MicrosoftAzure\\Storage\\Common\\Internal\\Http\\HttpFormatter' => $vendorDir . '/microsoft/azure-storage-common/src/Common/Internal/Http/HttpFormatter.php', + 'MicrosoftAzure\\Storage\\Common\\Internal\\MetadataTrait' => $vendorDir . '/microsoft/azure-storage-common/src/Common/Internal/MetadataTrait.php', + 'MicrosoftAzure\\Storage\\Common\\Internal\\Middlewares\\CommonRequestMiddleware' => $vendorDir . '/microsoft/azure-storage-common/src/Common/Internal/Middlewares/CommonRequestMiddleware.php', + 'MicrosoftAzure\\Storage\\Common\\Internal\\Resources' => $vendorDir . '/microsoft/azure-storage-common/src/Common/Internal/Resources.php', + 'MicrosoftAzure\\Storage\\Common\\Internal\\RestProxy' => $vendorDir . '/microsoft/azure-storage-common/src/Common/Internal/RestProxy.php', + 'MicrosoftAzure\\Storage\\Common\\Internal\\Serialization\\ISerializer' => $vendorDir . '/microsoft/azure-storage-common/src/Common/Internal/Serialization/ISerializer.php', + 'MicrosoftAzure\\Storage\\Common\\Internal\\Serialization\\JsonSerializer' => $vendorDir . '/microsoft/azure-storage-common/src/Common/Internal/Serialization/JsonSerializer.php', + 'MicrosoftAzure\\Storage\\Common\\Internal\\Serialization\\MessageSerializer' => $vendorDir . '/microsoft/azure-storage-common/src/Common/Internal/Serialization/MessageSerializer.php', + 'MicrosoftAzure\\Storage\\Common\\Internal\\Serialization\\XmlSerializer' => $vendorDir . '/microsoft/azure-storage-common/src/Common/Internal/Serialization/XmlSerializer.php', + 'MicrosoftAzure\\Storage\\Common\\Internal\\ServiceRestProxy' => $vendorDir . '/microsoft/azure-storage-common/src/Common/Internal/ServiceRestProxy.php', + 'MicrosoftAzure\\Storage\\Common\\Internal\\ServiceRestTrait' => $vendorDir . '/microsoft/azure-storage-common/src/Common/Internal/ServiceRestTrait.php', + 'MicrosoftAzure\\Storage\\Common\\Internal\\ServiceSettings' => $vendorDir . '/microsoft/azure-storage-common/src/Common/Internal/ServiceSettings.php', + 'MicrosoftAzure\\Storage\\Common\\Internal\\StorageServiceSettings' => $vendorDir . '/microsoft/azure-storage-common/src/Common/Internal/StorageServiceSettings.php', + 'MicrosoftAzure\\Storage\\Common\\Internal\\Utilities' => $vendorDir . '/microsoft/azure-storage-common/src/Common/Internal/Utilities.php', + 'MicrosoftAzure\\Storage\\Common\\Internal\\Validate' => $vendorDir . '/microsoft/azure-storage-common/src/Common/Internal/Validate.php', + 'MicrosoftAzure\\Storage\\Common\\LocationMode' => $vendorDir . '/microsoft/azure-storage-common/src/Common/LocationMode.php', + 'MicrosoftAzure\\Storage\\Common\\Logger' => $vendorDir . '/microsoft/azure-storage-common/src/Common/Logger.php', + 'MicrosoftAzure\\Storage\\Common\\MarkerContinuationTokenTrait' => $vendorDir . '/microsoft/azure-storage-common/src/Common/MarkerContinuationTokenTrait.php', + 'MicrosoftAzure\\Storage\\Common\\Middlewares\\HistoryMiddleware' => $vendorDir . '/microsoft/azure-storage-common/src/Common/Middlewares/HistoryMiddleware.php', + 'MicrosoftAzure\\Storage\\Common\\Middlewares\\IMiddleware' => $vendorDir . '/microsoft/azure-storage-common/src/Common/Middlewares/IMiddleware.php', + 'MicrosoftAzure\\Storage\\Common\\Middlewares\\MiddlewareBase' => $vendorDir . '/microsoft/azure-storage-common/src/Common/Middlewares/MiddlewareBase.php', + 'MicrosoftAzure\\Storage\\Common\\Middlewares\\MiddlewareStack' => $vendorDir . '/microsoft/azure-storage-common/src/Common/Middlewares/MiddlewareStack.php', + 'MicrosoftAzure\\Storage\\Common\\Middlewares\\RetryMiddleware' => $vendorDir . '/microsoft/azure-storage-common/src/Common/Middlewares/RetryMiddleware.php', + 'MicrosoftAzure\\Storage\\Common\\Middlewares\\RetryMiddlewareFactory' => $vendorDir . '/microsoft/azure-storage-common/src/Common/Middlewares/RetryMiddlewareFactory.php', + 'MicrosoftAzure\\Storage\\Common\\Models\\AccessPolicy' => $vendorDir . '/microsoft/azure-storage-common/src/Common/Models/AccessPolicy.php', + 'MicrosoftAzure\\Storage\\Common\\Models\\CORS' => $vendorDir . '/microsoft/azure-storage-common/src/Common/Models/CORS.php', + 'MicrosoftAzure\\Storage\\Common\\Models\\ContinuationToken' => $vendorDir . '/microsoft/azure-storage-common/src/Common/Models/ContinuationToken.php', + 'MicrosoftAzure\\Storage\\Common\\Models\\GetServicePropertiesResult' => $vendorDir . '/microsoft/azure-storage-common/src/Common/Models/GetServicePropertiesResult.php', + 'MicrosoftAzure\\Storage\\Common\\Models\\GetServiceStatsResult' => $vendorDir . '/microsoft/azure-storage-common/src/Common/Models/GetServiceStatsResult.php', + 'MicrosoftAzure\\Storage\\Common\\Models\\Logging' => $vendorDir . '/microsoft/azure-storage-common/src/Common/Models/Logging.php', + 'MicrosoftAzure\\Storage\\Common\\Models\\MarkerContinuationToken' => $vendorDir . '/microsoft/azure-storage-common/src/Common/Models/MarkerContinuationToken.php', + 'MicrosoftAzure\\Storage\\Common\\Models\\Metrics' => $vendorDir . '/microsoft/azure-storage-common/src/Common/Models/Metrics.php', + 'MicrosoftAzure\\Storage\\Common\\Models\\Range' => $vendorDir . '/microsoft/azure-storage-common/src/Common/Models/Range.php', + 'MicrosoftAzure\\Storage\\Common\\Models\\RangeDiff' => $vendorDir . '/microsoft/azure-storage-common/src/Common/Models/RangeDiff.php', + 'MicrosoftAzure\\Storage\\Common\\Models\\RetentionPolicy' => $vendorDir . '/microsoft/azure-storage-common/src/Common/Models/RetentionPolicy.php', + 'MicrosoftAzure\\Storage\\Common\\Models\\ServiceOptions' => $vendorDir . '/microsoft/azure-storage-common/src/Common/Models/ServiceOptions.php', + 'MicrosoftAzure\\Storage\\Common\\Models\\ServiceProperties' => $vendorDir . '/microsoft/azure-storage-common/src/Common/Models/ServiceProperties.php', + 'MicrosoftAzure\\Storage\\Common\\Models\\SignedIdentifier' => $vendorDir . '/microsoft/azure-storage-common/src/Common/Models/SignedIdentifier.php', + 'MicrosoftAzure\\Storage\\Common\\Models\\TransactionalMD5Trait' => $vendorDir . '/microsoft/azure-storage-common/src/Common/Models/TransactionalMD5Trait.php', + 'MicrosoftAzure\\Storage\\Common\\SharedAccessSignatureHelper' => $vendorDir . '/microsoft/azure-storage-common/src/Common/SharedAccessSignatureHelper.php', + 'F7cloud\\LogNormalizer\\Normalizer' => $vendorDir . '/f7cloud/lognormalizer/src/Normalizer.php', + 'Normalizer' => $vendorDir . '/symfony/polyfill-intl-normalizer/Resources/stubs/Normalizer.php', + 'OS_Guess' => $vendorDir . '/pear/pear-core-minimal/src/OS/Guess.php', + 'OpenStack\\BlockStorage\\v2\\Api' => $vendorDir . '/php-opencloud/openstack/src/BlockStorage/v2/Api.php', + 'OpenStack\\BlockStorage\\v2\\Models\\QuotaSet' => $vendorDir . '/php-opencloud/openstack/src/BlockStorage/v2/Models/QuotaSet.php', + 'OpenStack\\BlockStorage\\v2\\Models\\Snapshot' => $vendorDir . '/php-opencloud/openstack/src/BlockStorage/v2/Models/Snapshot.php', + 'OpenStack\\BlockStorage\\v2\\Models\\Volume' => $vendorDir . '/php-opencloud/openstack/src/BlockStorage/v2/Models/Volume.php', + 'OpenStack\\BlockStorage\\v2\\Models\\VolumeAttachment' => $vendorDir . '/php-opencloud/openstack/src/BlockStorage/v2/Models/VolumeAttachment.php', + 'OpenStack\\BlockStorage\\v2\\Models\\VolumeType' => $vendorDir . '/php-opencloud/openstack/src/BlockStorage/v2/Models/VolumeType.php', + 'OpenStack\\BlockStorage\\v2\\Params' => $vendorDir . '/php-opencloud/openstack/src/BlockStorage/v2/Params.php', + 'OpenStack\\BlockStorage\\v2\\Service' => $vendorDir . '/php-opencloud/openstack/src/BlockStorage/v2/Service.php', + 'OpenStack\\BlockStorage\\v3\\Api' => $vendorDir . '/php-opencloud/openstack/src/BlockStorage/v3/Api.php', + 'OpenStack\\BlockStorage\\v3\\Service' => $vendorDir . '/php-opencloud/openstack/src/BlockStorage/v3/Service.php', + 'OpenStack\\Common\\Api\\AbstractApi' => $vendorDir . '/php-opencloud/openstack/src/Common/Api/AbstractApi.php', + 'OpenStack\\Common\\Api\\AbstractParams' => $vendorDir . '/php-opencloud/openstack/src/Common/Api/AbstractParams.php', + 'OpenStack\\Common\\Api\\ApiInterface' => $vendorDir . '/php-opencloud/openstack/src/Common/Api/ApiInterface.php', + 'OpenStack\\Common\\Api\\Operation' => $vendorDir . '/php-opencloud/openstack/src/Common/Api/Operation.php', + 'OpenStack\\Common\\Api\\OperatorInterface' => $vendorDir . '/php-opencloud/openstack/src/Common/Api/OperatorInterface.php', + 'OpenStack\\Common\\Api\\OperatorTrait' => $vendorDir . '/php-opencloud/openstack/src/Common/Api/OperatorTrait.php', + 'OpenStack\\Common\\Api\\Parameter' => $vendorDir . '/php-opencloud/openstack/src/Common/Api/Parameter.php', + 'OpenStack\\Common\\ArrayAccessTrait' => $vendorDir . '/php-opencloud/openstack/src/Common/ArrayAccessTrait.php', + 'OpenStack\\Common\\Auth\\AuthHandler' => $vendorDir . '/php-opencloud/openstack/src/Common/Auth/AuthHandler.php', + 'OpenStack\\Common\\Auth\\Catalog' => $vendorDir . '/php-opencloud/openstack/src/Common/Auth/Catalog.php', + 'OpenStack\\Common\\Auth\\IdentityService' => $vendorDir . '/php-opencloud/openstack/src/Common/Auth/IdentityService.php', + 'OpenStack\\Common\\Auth\\Token' => $vendorDir . '/php-opencloud/openstack/src/Common/Auth/Token.php', + 'OpenStack\\Common\\Error\\BadResponseError' => $vendorDir . '/php-opencloud/openstack/src/Common/Error/BadResponseError.php', + 'OpenStack\\Common\\Error\\BaseError' => $vendorDir . '/php-opencloud/openstack/src/Common/Error/BaseError.php', + 'OpenStack\\Common\\Error\\Builder' => $vendorDir . '/php-opencloud/openstack/src/Common/Error/Builder.php', + 'OpenStack\\Common\\Error\\NotImplementedError' => $vendorDir . '/php-opencloud/openstack/src/Common/Error/NotImplementedError.php', + 'OpenStack\\Common\\Error\\UserInputError' => $vendorDir . '/php-opencloud/openstack/src/Common/Error/UserInputError.php', + 'OpenStack\\Common\\HydratorStrategyTrait' => $vendorDir . '/php-opencloud/openstack/src/Common/HydratorStrategyTrait.php', + 'OpenStack\\Common\\JsonPath' => $vendorDir . '/php-opencloud/openstack/src/Common/JsonPath.php', + 'OpenStack\\Common\\JsonSchema\\JsonPatch' => $vendorDir . '/php-opencloud/openstack/src/Common/JsonSchema/JsonPatch.php', + 'OpenStack\\Common\\JsonSchema\\Schema' => $vendorDir . '/php-opencloud/openstack/src/Common/JsonSchema/Schema.php', + 'OpenStack\\Common\\Resource\\AbstractResource' => $vendorDir . '/php-opencloud/openstack/src/Common/Resource/AbstractResource.php', + 'OpenStack\\Common\\Resource\\Alias' => $vendorDir . '/php-opencloud/openstack/src/Common/Resource/Alias.php', + 'OpenStack\\Common\\Resource\\Creatable' => $vendorDir . '/php-opencloud/openstack/src/Common/Resource/Creatable.php', + 'OpenStack\\Common\\Resource\\Deletable' => $vendorDir . '/php-opencloud/openstack/src/Common/Resource/Deletable.php', + 'OpenStack\\Common\\Resource\\HasMetadata' => $vendorDir . '/php-opencloud/openstack/src/Common/Resource/HasMetadata.php', + 'OpenStack\\Common\\Resource\\HasWaiterTrait' => $vendorDir . '/php-opencloud/openstack/src/Common/Resource/HasWaiterTrait.php', + 'OpenStack\\Common\\Resource\\Iterator' => $vendorDir . '/php-opencloud/openstack/src/Common/Resource/Iterator.php', + 'OpenStack\\Common\\Resource\\Listable' => $vendorDir . '/php-opencloud/openstack/src/Common/Resource/Listable.php', + 'OpenStack\\Common\\Resource\\OperatorResource' => $vendorDir . '/php-opencloud/openstack/src/Common/Resource/OperatorResource.php', + 'OpenStack\\Common\\Resource\\ResourceInterface' => $vendorDir . '/php-opencloud/openstack/src/Common/Resource/ResourceInterface.php', + 'OpenStack\\Common\\Resource\\Retrievable' => $vendorDir . '/php-opencloud/openstack/src/Common/Resource/Retrievable.php', + 'OpenStack\\Common\\Resource\\Updateable' => $vendorDir . '/php-opencloud/openstack/src/Common/Resource/Updateable.php', + 'OpenStack\\Common\\Service\\AbstractService' => $vendorDir . '/php-opencloud/openstack/src/Common/Service/AbstractService.php', + 'OpenStack\\Common\\Service\\Builder' => $vendorDir . '/php-opencloud/openstack/src/Common/Service/Builder.php', + 'OpenStack\\Common\\Service\\ServiceInterface' => $vendorDir . '/php-opencloud/openstack/src/Common/Service/ServiceInterface.php', + 'OpenStack\\Common\\Transport\\HandlerStack' => $vendorDir . '/php-opencloud/openstack/src/Common/Transport/HandlerStack.php', + 'OpenStack\\Common\\Transport\\HandlerStackFactory' => $vendorDir . '/php-opencloud/openstack/src/Common/Transport/HandlerStackFactory.php', + 'OpenStack\\Common\\Transport\\JsonSerializer' => $vendorDir . '/php-opencloud/openstack/src/Common/Transport/JsonSerializer.php', + 'OpenStack\\Common\\Transport\\Middleware' => $vendorDir . '/php-opencloud/openstack/src/Common/Transport/Middleware.php', + 'OpenStack\\Common\\Transport\\RequestSerializer' => $vendorDir . '/php-opencloud/openstack/src/Common/Transport/RequestSerializer.php', + 'OpenStack\\Common\\Transport\\Serializable' => $vendorDir . '/php-opencloud/openstack/src/Common/Transport/Serializable.php', + 'OpenStack\\Common\\Transport\\Utils' => $vendorDir . '/php-opencloud/openstack/src/Common/Transport/Utils.php', + 'OpenStack\\Compute\\v2\\Api' => $vendorDir . '/php-opencloud/openstack/src/Compute/v2/Api.php', + 'OpenStack\\Compute\\v2\\Enum' => $vendorDir . '/php-opencloud/openstack/src/Compute/v2/Enum.php', + 'OpenStack\\Compute\\v2\\Models\\AvailabilityZone' => $vendorDir . '/php-opencloud/openstack/src/Compute/v2/Models/AvailabilityZone.php', + 'OpenStack\\Compute\\v2\\Models\\Fault' => $vendorDir . '/php-opencloud/openstack/src/Compute/v2/Models/Fault.php', + 'OpenStack\\Compute\\v2\\Models\\Flavor' => $vendorDir . '/php-opencloud/openstack/src/Compute/v2/Models/Flavor.php', + 'OpenStack\\Compute\\v2\\Models\\Host' => $vendorDir . '/php-opencloud/openstack/src/Compute/v2/Models/Host.php', + 'OpenStack\\Compute\\v2\\Models\\Hypervisor' => $vendorDir . '/php-opencloud/openstack/src/Compute/v2/Models/Hypervisor.php', + 'OpenStack\\Compute\\v2\\Models\\HypervisorStatistic' => $vendorDir . '/php-opencloud/openstack/src/Compute/v2/Models/HypervisorStatistic.php', + 'OpenStack\\Compute\\v2\\Models\\Image' => $vendorDir . '/php-opencloud/openstack/src/Compute/v2/Models/Image.php', + 'OpenStack\\Compute\\v2\\Models\\Keypair' => $vendorDir . '/php-opencloud/openstack/src/Compute/v2/Models/Keypair.php', + 'OpenStack\\Compute\\v2\\Models\\Limit' => $vendorDir . '/php-opencloud/openstack/src/Compute/v2/Models/Limit.php', + 'OpenStack\\Compute\\v2\\Models\\QuotaSet' => $vendorDir . '/php-opencloud/openstack/src/Compute/v2/Models/QuotaSet.php', + 'OpenStack\\Compute\\v2\\Models\\Server' => $vendorDir . '/php-opencloud/openstack/src/Compute/v2/Models/Server.php', + 'OpenStack\\Compute\\v2\\Params' => $vendorDir . '/php-opencloud/openstack/src/Compute/v2/Params.php', + 'OpenStack\\Compute\\v2\\Service' => $vendorDir . '/php-opencloud/openstack/src/Compute/v2/Service.php', + 'OpenStack\\Identity\\v2\\Api' => $vendorDir . '/php-opencloud/openstack/src/Identity/v2/Api.php', + 'OpenStack\\Identity\\v2\\Models\\Catalog' => $vendorDir . '/php-opencloud/openstack/src/Identity/v2/Models/Catalog.php', + 'OpenStack\\Identity\\v2\\Models\\Endpoint' => $vendorDir . '/php-opencloud/openstack/src/Identity/v2/Models/Endpoint.php', + 'OpenStack\\Identity\\v2\\Models\\Entry' => $vendorDir . '/php-opencloud/openstack/src/Identity/v2/Models/Entry.php', + 'OpenStack\\Identity\\v2\\Models\\Tenant' => $vendorDir . '/php-opencloud/openstack/src/Identity/v2/Models/Tenant.php', + 'OpenStack\\Identity\\v2\\Models\\Token' => $vendorDir . '/php-opencloud/openstack/src/Identity/v2/Models/Token.php', + 'OpenStack\\Identity\\v2\\Service' => $vendorDir . '/php-opencloud/openstack/src/Identity/v2/Service.php', + 'OpenStack\\Identity\\v3\\Api' => $vendorDir . '/php-opencloud/openstack/src/Identity/v3/Api.php', + 'OpenStack\\Identity\\v3\\Enum' => $vendorDir . '/php-opencloud/openstack/src/Identity/v3/Enum.php', + 'OpenStack\\Identity\\v3\\Models\\ApplicationCredential' => $vendorDir . '/php-opencloud/openstack/src/Identity/v3/Models/ApplicationCredential.php', + 'OpenStack\\Identity\\v3\\Models\\Assignment' => $vendorDir . '/php-opencloud/openstack/src/Identity/v3/Models/Assignment.php', + 'OpenStack\\Identity\\v3\\Models\\Catalog' => $vendorDir . '/php-opencloud/openstack/src/Identity/v3/Models/Catalog.php', + 'OpenStack\\Identity\\v3\\Models\\Credential' => $vendorDir . '/php-opencloud/openstack/src/Identity/v3/Models/Credential.php', + 'OpenStack\\Identity\\v3\\Models\\Domain' => $vendorDir . '/php-opencloud/openstack/src/Identity/v3/Models/Domain.php', + 'OpenStack\\Identity\\v3\\Models\\Endpoint' => $vendorDir . '/php-opencloud/openstack/src/Identity/v3/Models/Endpoint.php', + 'OpenStack\\Identity\\v3\\Models\\Group' => $vendorDir . '/php-opencloud/openstack/src/Identity/v3/Models/Group.php', + 'OpenStack\\Identity\\v3\\Models\\Policy' => $vendorDir . '/php-opencloud/openstack/src/Identity/v3/Models/Policy.php', + 'OpenStack\\Identity\\v3\\Models\\Project' => $vendorDir . '/php-opencloud/openstack/src/Identity/v3/Models/Project.php', + 'OpenStack\\Identity\\v3\\Models\\Role' => $vendorDir . '/php-opencloud/openstack/src/Identity/v3/Models/Role.php', + 'OpenStack\\Identity\\v3\\Models\\Service' => $vendorDir . '/php-opencloud/openstack/src/Identity/v3/Models/Service.php', + 'OpenStack\\Identity\\v3\\Models\\Token' => $vendorDir . '/php-opencloud/openstack/src/Identity/v3/Models/Token.php', + 'OpenStack\\Identity\\v3\\Models\\User' => $vendorDir . '/php-opencloud/openstack/src/Identity/v3/Models/User.php', + 'OpenStack\\Identity\\v3\\Params' => $vendorDir . '/php-opencloud/openstack/src/Identity/v3/Params.php', + 'OpenStack\\Identity\\v3\\Service' => $vendorDir . '/php-opencloud/openstack/src/Identity/v3/Service.php', + 'OpenStack\\Images\\v2\\Api' => $vendorDir . '/php-opencloud/openstack/src/Images/v2/Api.php', + 'OpenStack\\Images\\v2\\JsonPatch' => $vendorDir . '/php-opencloud/openstack/src/Images/v2/JsonPatch.php', + 'OpenStack\\Images\\v2\\Models\\Image' => $vendorDir . '/php-opencloud/openstack/src/Images/v2/Models/Image.php', + 'OpenStack\\Images\\v2\\Models\\Member' => $vendorDir . '/php-opencloud/openstack/src/Images/v2/Models/Member.php', + 'OpenStack\\Images\\v2\\Models\\Schema' => $vendorDir . '/php-opencloud/openstack/src/Images/v2/Models/Schema.php', + 'OpenStack\\Images\\v2\\Params' => $vendorDir . '/php-opencloud/openstack/src/Images/v2/Params.php', + 'OpenStack\\Images\\v2\\Service' => $vendorDir . '/php-opencloud/openstack/src/Images/v2/Service.php', + 'OpenStack\\Metric\\v1\\Gnocchi\\Api' => $vendorDir . '/php-opencloud/openstack/src/Metric/v1/Gnocchi/Api.php', + 'OpenStack\\Metric\\v1\\Gnocchi\\Models\\Metric' => $vendorDir . '/php-opencloud/openstack/src/Metric/v1/Gnocchi/Models/Metric.php', + 'OpenStack\\Metric\\v1\\Gnocchi\\Models\\Resource' => $vendorDir . '/php-opencloud/openstack/src/Metric/v1/Gnocchi/Models/Resource.php', + 'OpenStack\\Metric\\v1\\Gnocchi\\Models\\ResourceType' => $vendorDir . '/php-opencloud/openstack/src/Metric/v1/Gnocchi/Models/ResourceType.php', + 'OpenStack\\Metric\\v1\\Gnocchi\\Params' => $vendorDir . '/php-opencloud/openstack/src/Metric/v1/Gnocchi/Params.php', + 'OpenStack\\Metric\\v1\\Gnocchi\\Service' => $vendorDir . '/php-opencloud/openstack/src/Metric/v1/Gnocchi/Service.php', + 'OpenStack\\Networking\\v2\\Api' => $vendorDir . '/php-opencloud/openstack/src/Networking/v2/Api.php', + 'OpenStack\\Networking\\v2\\Extensions\\Layer3\\Api' => $vendorDir . '/php-opencloud/openstack/src/Networking/v2/Extensions/Layer3/Api.php', + 'OpenStack\\Networking\\v2\\Extensions\\Layer3\\ApiTrait' => $vendorDir . '/php-opencloud/openstack/src/Networking/v2/Extensions/Layer3/ApiTrait.php', + 'OpenStack\\Networking\\v2\\Extensions\\Layer3\\Models\\FixedIp' => $vendorDir . '/php-opencloud/openstack/src/Networking/v2/Extensions/Layer3/Models/FixedIp.php', + 'OpenStack\\Networking\\v2\\Extensions\\Layer3\\Models\\FloatingIp' => $vendorDir . '/php-opencloud/openstack/src/Networking/v2/Extensions/Layer3/Models/FloatingIp.php', + 'OpenStack\\Networking\\v2\\Extensions\\Layer3\\Models\\GatewayInfo' => $vendorDir . '/php-opencloud/openstack/src/Networking/v2/Extensions/Layer3/Models/GatewayInfo.php', + 'OpenStack\\Networking\\v2\\Extensions\\Layer3\\Models\\Router' => $vendorDir . '/php-opencloud/openstack/src/Networking/v2/Extensions/Layer3/Models/Router.php', + 'OpenStack\\Networking\\v2\\Extensions\\Layer3\\Params' => $vendorDir . '/php-opencloud/openstack/src/Networking/v2/Extensions/Layer3/Params.php', + 'OpenStack\\Networking\\v2\\Extensions\\Layer3\\ParamsTrait' => $vendorDir . '/php-opencloud/openstack/src/Networking/v2/Extensions/Layer3/ParamsTrait.php', + 'OpenStack\\Networking\\v2\\Extensions\\Layer3\\Service' => $vendorDir . '/php-opencloud/openstack/src/Networking/v2/Extensions/Layer3/Service.php', + 'OpenStack\\Networking\\v2\\Extensions\\Layer3\\ServiceTrait' => $vendorDir . '/php-opencloud/openstack/src/Networking/v2/Extensions/Layer3/ServiceTrait.php', + 'OpenStack\\Networking\\v2\\Extensions\\SecurityGroups\\Api' => $vendorDir . '/php-opencloud/openstack/src/Networking/v2/Extensions/SecurityGroups/Api.php', + 'OpenStack\\Networking\\v2\\Extensions\\SecurityGroups\\ApiTrait' => $vendorDir . '/php-opencloud/openstack/src/Networking/v2/Extensions/SecurityGroups/ApiTrait.php', + 'OpenStack\\Networking\\v2\\Extensions\\SecurityGroups\\Models\\SecurityGroup' => $vendorDir . '/php-opencloud/openstack/src/Networking/v2/Extensions/SecurityGroups/Models/SecurityGroup.php', + 'OpenStack\\Networking\\v2\\Extensions\\SecurityGroups\\Models\\SecurityGroupRule' => $vendorDir . '/php-opencloud/openstack/src/Networking/v2/Extensions/SecurityGroups/Models/SecurityGroupRule.php', + 'OpenStack\\Networking\\v2\\Extensions\\SecurityGroups\\Params' => $vendorDir . '/php-opencloud/openstack/src/Networking/v2/Extensions/SecurityGroups/Params.php', + 'OpenStack\\Networking\\v2\\Extensions\\SecurityGroups\\ParamsTrait' => $vendorDir . '/php-opencloud/openstack/src/Networking/v2/Extensions/SecurityGroups/ParamsTrait.php', + 'OpenStack\\Networking\\v2\\Extensions\\SecurityGroups\\Service' => $vendorDir . '/php-opencloud/openstack/src/Networking/v2/Extensions/SecurityGroups/Service.php', + 'OpenStack\\Networking\\v2\\Extensions\\SecurityGroups\\ServiceTrait' => $vendorDir . '/php-opencloud/openstack/src/Networking/v2/Extensions/SecurityGroups/ServiceTrait.php', + 'OpenStack\\Networking\\v2\\Models\\InterfaceAttachment' => $vendorDir . '/php-opencloud/openstack/src/Networking/v2/Models/InterfaceAttachment.php', + 'OpenStack\\Networking\\v2\\Models\\LoadBalancer' => $vendorDir . '/php-opencloud/openstack/src/Networking/v2/Models/LoadBalancer.php', + 'OpenStack\\Networking\\v2\\Models\\LoadBalancerHealthMonitor' => $vendorDir . '/php-opencloud/openstack/src/Networking/v2/Models/LoadBalancerHealthMonitor.php', + 'OpenStack\\Networking\\v2\\Models\\LoadBalancerListener' => $vendorDir . '/php-opencloud/openstack/src/Networking/v2/Models/LoadBalancerListener.php', + 'OpenStack\\Networking\\v2\\Models\\LoadBalancerMember' => $vendorDir . '/php-opencloud/openstack/src/Networking/v2/Models/LoadBalancerMember.php', + 'OpenStack\\Networking\\v2\\Models\\LoadBalancerPool' => $vendorDir . '/php-opencloud/openstack/src/Networking/v2/Models/LoadBalancerPool.php', + 'OpenStack\\Networking\\v2\\Models\\LoadBalancerStat' => $vendorDir . '/php-opencloud/openstack/src/Networking/v2/Models/LoadBalancerStat.php', + 'OpenStack\\Networking\\v2\\Models\\LoadBalancerStatus' => $vendorDir . '/php-opencloud/openstack/src/Networking/v2/Models/LoadBalancerStatus.php', + 'OpenStack\\Networking\\v2\\Models\\Network' => $vendorDir . '/php-opencloud/openstack/src/Networking/v2/Models/Network.php', + 'OpenStack\\Networking\\v2\\Models\\Port' => $vendorDir . '/php-opencloud/openstack/src/Networking/v2/Models/Port.php', + 'OpenStack\\Networking\\v2\\Models\\Quota' => $vendorDir . '/php-opencloud/openstack/src/Networking/v2/Models/Quota.php', + 'OpenStack\\Networking\\v2\\Models\\Subnet' => $vendorDir . '/php-opencloud/openstack/src/Networking/v2/Models/Subnet.php', + 'OpenStack\\Networking\\v2\\Params' => $vendorDir . '/php-opencloud/openstack/src/Networking/v2/Params.php', + 'OpenStack\\Networking\\v2\\Service' => $vendorDir . '/php-opencloud/openstack/src/Networking/v2/Service.php', + 'OpenStack\\ObjectStore\\v1\\Api' => $vendorDir . '/php-opencloud/openstack/src/ObjectStore/v1/Api.php', + 'OpenStack\\ObjectStore\\v1\\Models\\Account' => $vendorDir . '/php-opencloud/openstack/src/ObjectStore/v1/Models/Account.php', + 'OpenStack\\ObjectStore\\v1\\Models\\Container' => $vendorDir . '/php-opencloud/openstack/src/ObjectStore/v1/Models/Container.php', + 'OpenStack\\ObjectStore\\v1\\Models\\MetadataTrait' => $vendorDir . '/php-opencloud/openstack/src/ObjectStore/v1/Models/MetadataTrait.php', + 'OpenStack\\ObjectStore\\v1\\Models\\StorageObject' => $vendorDir . '/php-opencloud/openstack/src/ObjectStore/v1/Models/StorageObject.php', + 'OpenStack\\ObjectStore\\v1\\Params' => $vendorDir . '/php-opencloud/openstack/src/ObjectStore/v1/Params.php', + 'OpenStack\\ObjectStore\\v1\\Service' => $vendorDir . '/php-opencloud/openstack/src/ObjectStore/v1/Service.php', + 'OpenStack\\OpenStack' => $vendorDir . '/php-opencloud/openstack/src/OpenStack.php', + 'Override' => $vendorDir . '/symfony/polyfill-php83/Resources/stubs/Override.php', + 'PEAR' => $vendorDir . '/pear/pear-core-minimal/src/PEAR.php', + 'PEAR_Error' => $vendorDir . '/pear/pear-core-minimal/src/PEAR.php', + 'PEAR_ErrorStack' => $vendorDir . '/pear/pear-core-minimal/src/PEAR/ErrorStack.php', + 'PEAR_Exception' => $vendorDir . '/pear/pear_exception/PEAR/Exception.php', + 'ParagonIE\\ConstantTime\\Base32' => $vendorDir . '/paragonie/constant_time_encoding/src/Base32.php', + 'ParagonIE\\ConstantTime\\Base32Hex' => $vendorDir . '/paragonie/constant_time_encoding/src/Base32Hex.php', + 'ParagonIE\\ConstantTime\\Base64' => $vendorDir . '/paragonie/constant_time_encoding/src/Base64.php', + 'ParagonIE\\ConstantTime\\Base64DotSlash' => $vendorDir . '/paragonie/constant_time_encoding/src/Base64DotSlash.php', + 'ParagonIE\\ConstantTime\\Base64DotSlashOrdered' => $vendorDir . '/paragonie/constant_time_encoding/src/Base64DotSlashOrdered.php', + 'ParagonIE\\ConstantTime\\Base64UrlSafe' => $vendorDir . '/paragonie/constant_time_encoding/src/Base64UrlSafe.php', + 'ParagonIE\\ConstantTime\\Binary' => $vendorDir . '/paragonie/constant_time_encoding/src/Binary.php', + 'ParagonIE\\ConstantTime\\EncoderInterface' => $vendorDir . '/paragonie/constant_time_encoding/src/EncoderInterface.php', + 'ParagonIE\\ConstantTime\\Encoding' => $vendorDir . '/paragonie/constant_time_encoding/src/Encoding.php', + 'ParagonIE\\ConstantTime\\Hex' => $vendorDir . '/paragonie/constant_time_encoding/src/Hex.php', + 'ParagonIE\\ConstantTime\\RFC4648' => $vendorDir . '/paragonie/constant_time_encoding/src/RFC4648.php', + 'Pimple\\Container' => $vendorDir . '/pimple/pimple/src/Pimple/Container.php', + 'Pimple\\Exception\\ExpectedInvokableException' => $vendorDir . '/pimple/pimple/src/Pimple/Exception/ExpectedInvokableException.php', + 'Pimple\\Exception\\FrozenServiceException' => $vendorDir . '/pimple/pimple/src/Pimple/Exception/FrozenServiceException.php', + 'Pimple\\Exception\\InvalidServiceIdentifierException' => $vendorDir . '/pimple/pimple/src/Pimple/Exception/InvalidServiceIdentifierException.php', + 'Pimple\\Exception\\UnknownIdentifierException' => $vendorDir . '/pimple/pimple/src/Pimple/Exception/UnknownIdentifierException.php', + 'Pimple\\Psr11\\Container' => $vendorDir . '/pimple/pimple/src/Pimple/Psr11/Container.php', + 'Pimple\\Psr11\\ServiceLocator' => $vendorDir . '/pimple/pimple/src/Pimple/Psr11/ServiceLocator.php', + 'Pimple\\ServiceIterator' => $vendorDir . '/pimple/pimple/src/Pimple/ServiceIterator.php', + 'Pimple\\ServiceProviderInterface' => $vendorDir . '/pimple/pimple/src/Pimple/ServiceProviderInterface.php', + 'Psr\\Cache\\CacheException' => $vendorDir . '/psr/cache/src/CacheException.php', + 'Psr\\Cache\\CacheItemInterface' => $vendorDir . '/psr/cache/src/CacheItemInterface.php', + 'Psr\\Cache\\CacheItemPoolInterface' => $vendorDir . '/psr/cache/src/CacheItemPoolInterface.php', + 'Psr\\Cache\\InvalidArgumentException' => $vendorDir . '/psr/cache/src/InvalidArgumentException.php', + 'Psr\\Clock\\ClockInterface' => $vendorDir . '/psr/clock/src/ClockInterface.php', + 'Psr\\Container\\ContainerExceptionInterface' => $vendorDir . '/psr/container/src/ContainerExceptionInterface.php', + 'Psr\\Container\\ContainerInterface' => $vendorDir . '/psr/container/src/ContainerInterface.php', + 'Psr\\Container\\NotFoundExceptionInterface' => $vendorDir . '/psr/container/src/NotFoundExceptionInterface.php', + 'Psr\\EventDispatcher\\EventDispatcherInterface' => $vendorDir . '/psr/event-dispatcher/src/EventDispatcherInterface.php', + 'Psr\\EventDispatcher\\ListenerProviderInterface' => $vendorDir . '/psr/event-dispatcher/src/ListenerProviderInterface.php', + 'Psr\\EventDispatcher\\StoppableEventInterface' => $vendorDir . '/psr/event-dispatcher/src/StoppableEventInterface.php', + 'Psr\\Http\\Client\\ClientExceptionInterface' => $vendorDir . '/psr/http-client/src/ClientExceptionInterface.php', + 'Psr\\Http\\Client\\ClientInterface' => $vendorDir . '/psr/http-client/src/ClientInterface.php', + 'Psr\\Http\\Client\\NetworkExceptionInterface' => $vendorDir . '/psr/http-client/src/NetworkExceptionInterface.php', + 'Psr\\Http\\Client\\RequestExceptionInterface' => $vendorDir . '/psr/http-client/src/RequestExceptionInterface.php', + 'Psr\\Http\\Message\\MessageInterface' => $vendorDir . '/psr/http-message/src/MessageInterface.php', + 'Psr\\Http\\Message\\RequestFactoryInterface' => $vendorDir . '/psr/http-factory/src/RequestFactoryInterface.php', + 'Psr\\Http\\Message\\RequestInterface' => $vendorDir . '/psr/http-message/src/RequestInterface.php', + 'Psr\\Http\\Message\\ResponseFactoryInterface' => $vendorDir . '/psr/http-factory/src/ResponseFactoryInterface.php', + 'Psr\\Http\\Message\\ResponseInterface' => $vendorDir . '/psr/http-message/src/ResponseInterface.php', + 'Psr\\Http\\Message\\ServerRequestFactoryInterface' => $vendorDir . '/psr/http-factory/src/ServerRequestFactoryInterface.php', + 'Psr\\Http\\Message\\ServerRequestInterface' => $vendorDir . '/psr/http-message/src/ServerRequestInterface.php', + 'Psr\\Http\\Message\\StreamFactoryInterface' => $vendorDir . '/psr/http-factory/src/StreamFactoryInterface.php', + 'Psr\\Http\\Message\\StreamInterface' => $vendorDir . '/psr/http-message/src/StreamInterface.php', + 'Psr\\Http\\Message\\UploadedFileFactoryInterface' => $vendorDir . '/psr/http-factory/src/UploadedFileFactoryInterface.php', + 'Psr\\Http\\Message\\UploadedFileInterface' => $vendorDir . '/psr/http-message/src/UploadedFileInterface.php', + 'Psr\\Http\\Message\\UriFactoryInterface' => $vendorDir . '/psr/http-factory/src/UriFactoryInterface.php', + 'Psr\\Http\\Message\\UriInterface' => $vendorDir . '/psr/http-message/src/UriInterface.php', + 'Psr\\Log\\AbstractLogger' => $vendorDir . '/psr/log/src/AbstractLogger.php', + 'Psr\\Log\\InvalidArgumentException' => $vendorDir . '/psr/log/src/InvalidArgumentException.php', + 'Psr\\Log\\LogLevel' => $vendorDir . '/psr/log/src/LogLevel.php', + 'Psr\\Log\\LoggerAwareInterface' => $vendorDir . '/psr/log/src/LoggerAwareInterface.php', + 'Psr\\Log\\LoggerAwareTrait' => $vendorDir . '/psr/log/src/LoggerAwareTrait.php', + 'Psr\\Log\\LoggerInterface' => $vendorDir . '/psr/log/src/LoggerInterface.php', + 'Psr\\Log\\LoggerTrait' => $vendorDir . '/psr/log/src/LoggerTrait.php', + 'Psr\\Log\\NullLogger' => $vendorDir . '/psr/log/src/NullLogger.php', + 'Punic\\Calendar' => $vendorDir . '/punic/punic/src/Calendar.php', + 'Punic\\Comparer' => $vendorDir . '/punic/punic/src/Comparer.php', + 'Punic\\Currency' => $vendorDir . '/punic/punic/src/Currency.php', + 'Punic\\Data' => $vendorDir . '/punic/punic/src/Data.php', + 'Punic\\Exception' => $vendorDir . '/punic/punic/src/Exception.php', + 'Punic\\Exception\\BadArgumentType' => $vendorDir . '/punic/punic/src/Exception/BadArgumentType.php', + 'Punic\\Exception\\BadDataFileContents' => $vendorDir . '/punic/punic/src/Exception/BadDataFileContents.php', + 'Punic\\Exception\\DataFileNotFound' => $vendorDir . '/punic/punic/src/Exception/DataFileNotFound.php', + 'Punic\\Exception\\DataFileNotReadable' => $vendorDir . '/punic/punic/src/Exception/DataFileNotReadable.php', + 'Punic\\Exception\\DataFolderNotFound' => $vendorDir . '/punic/punic/src/Exception/DataFolderNotFound.php', + 'Punic\\Exception\\InvalidDataFile' => $vendorDir . '/punic/punic/src/Exception/InvalidDataFile.php', + 'Punic\\Exception\\InvalidLocale' => $vendorDir . '/punic/punic/src/Exception/InvalidLocale.php', + 'Punic\\Exception\\InvalidOverride' => $vendorDir . '/punic/punic/src/Exception/InvalidOverride.php', + 'Punic\\Exception\\NotImplemented' => $vendorDir . '/punic/punic/src/Exception/NotImplemented.php', + 'Punic\\Exception\\ValueNotInList' => $vendorDir . '/punic/punic/src/Exception/ValueNotInList.php', + 'Punic\\Language' => $vendorDir . '/punic/punic/src/Language.php', + 'Punic\\Misc' => $vendorDir . '/punic/punic/src/Misc.php', + 'Punic\\Number' => $vendorDir . '/punic/punic/src/Number.php', + 'Punic\\Phone' => $vendorDir . '/punic/punic/src/Phone.php', + 'Punic\\Plural' => $vendorDir . '/punic/punic/src/Plural.php', + 'Punic\\Script' => $vendorDir . '/punic/punic/src/Script.php', + 'Punic\\Territory' => $vendorDir . '/punic/punic/src/Territory.php', + 'Punic\\Unit' => $vendorDir . '/punic/punic/src/Unit.php', + 'Random\\BrokenRandomEngineError' => $vendorDir . '/symfony/polyfill-php82/Resources/stubs/Random/BrokenRandomEngineError.php', + 'Random\\CryptoSafeEngine' => $vendorDir . '/symfony/polyfill-php82/Resources/stubs/Random/CryptoSafeEngine.php', + 'Random\\Engine' => $vendorDir . '/symfony/polyfill-php82/Resources/stubs/Random/Engine.php', + 'Random\\Engine\\Secure' => $vendorDir . '/symfony/polyfill-php82/Resources/stubs/Random/Engine/Secure.php', + 'Random\\RandomError' => $vendorDir . '/symfony/polyfill-php82/Resources/stubs/Random/RandomError.php', + 'Random\\RandomException' => $vendorDir . '/symfony/polyfill-php82/Resources/stubs/Random/RandomException.php', + 'SQLite3Exception' => $vendorDir . '/symfony/polyfill-php83/Resources/stubs/SQLite3Exception.php', + 'Sabre\\CalDAV\\Backend\\AbstractBackend' => $vendorDir . '/sabre/dav/lib/CalDAV/Backend/AbstractBackend.php', + 'Sabre\\CalDAV\\Backend\\BackendInterface' => $vendorDir . '/sabre/dav/lib/CalDAV/Backend/BackendInterface.php', + 'Sabre\\CalDAV\\Backend\\NotificationSupport' => $vendorDir . '/sabre/dav/lib/CalDAV/Backend/NotificationSupport.php', + 'Sabre\\CalDAV\\Backend\\PDO' => $vendorDir . '/sabre/dav/lib/CalDAV/Backend/PDO.php', + 'Sabre\\CalDAV\\Backend\\SchedulingSupport' => $vendorDir . '/sabre/dav/lib/CalDAV/Backend/SchedulingSupport.php', + 'Sabre\\CalDAV\\Backend\\SharingSupport' => $vendorDir . '/sabre/dav/lib/CalDAV/Backend/SharingSupport.php', + 'Sabre\\CalDAV\\Backend\\SimplePDO' => $vendorDir . '/sabre/dav/lib/CalDAV/Backend/SimplePDO.php', + 'Sabre\\CalDAV\\Backend\\SubscriptionSupport' => $vendorDir . '/sabre/dav/lib/CalDAV/Backend/SubscriptionSupport.php', + 'Sabre\\CalDAV\\Backend\\SyncSupport' => $vendorDir . '/sabre/dav/lib/CalDAV/Backend/SyncSupport.php', + 'Sabre\\CalDAV\\Calendar' => $vendorDir . '/sabre/dav/lib/CalDAV/Calendar.php', + 'Sabre\\CalDAV\\CalendarHome' => $vendorDir . '/sabre/dav/lib/CalDAV/CalendarHome.php', + 'Sabre\\CalDAV\\CalendarObject' => $vendorDir . '/sabre/dav/lib/CalDAV/CalendarObject.php', + 'Sabre\\CalDAV\\CalendarQueryValidator' => $vendorDir . '/sabre/dav/lib/CalDAV/CalendarQueryValidator.php', + 'Sabre\\CalDAV\\CalendarRoot' => $vendorDir . '/sabre/dav/lib/CalDAV/CalendarRoot.php', + 'Sabre\\CalDAV\\Exception\\InvalidComponentType' => $vendorDir . '/sabre/dav/lib/CalDAV/Exception/InvalidComponentType.php', + 'Sabre\\CalDAV\\ICSExportPlugin' => $vendorDir . '/sabre/dav/lib/CalDAV/ICSExportPlugin.php', + 'Sabre\\CalDAV\\ICalendar' => $vendorDir . '/sabre/dav/lib/CalDAV/ICalendar.php', + 'Sabre\\CalDAV\\ICalendarObject' => $vendorDir . '/sabre/dav/lib/CalDAV/ICalendarObject.php', + 'Sabre\\CalDAV\\ICalendarObjectContainer' => $vendorDir . '/sabre/dav/lib/CalDAV/ICalendarObjectContainer.php', + 'Sabre\\CalDAV\\ISharedCalendar' => $vendorDir . '/sabre/dav/lib/CalDAV/ISharedCalendar.php', + 'Sabre\\CalDAV\\Notifications\\Collection' => $vendorDir . '/sabre/dav/lib/CalDAV/Notifications/Collection.php', + 'Sabre\\CalDAV\\Notifications\\ICollection' => $vendorDir . '/sabre/dav/lib/CalDAV/Notifications/ICollection.php', + 'Sabre\\CalDAV\\Notifications\\INode' => $vendorDir . '/sabre/dav/lib/CalDAV/Notifications/INode.php', + 'Sabre\\CalDAV\\Notifications\\Node' => $vendorDir . '/sabre/dav/lib/CalDAV/Notifications/Node.php', + 'Sabre\\CalDAV\\Notifications\\Plugin' => $vendorDir . '/sabre/dav/lib/CalDAV/Notifications/Plugin.php', + 'Sabre\\CalDAV\\Plugin' => $vendorDir . '/sabre/dav/lib/CalDAV/Plugin.php', + 'Sabre\\CalDAV\\Principal\\Collection' => $vendorDir . '/sabre/dav/lib/CalDAV/Principal/Collection.php', + 'Sabre\\CalDAV\\Principal\\IProxyRead' => $vendorDir . '/sabre/dav/lib/CalDAV/Principal/IProxyRead.php', + 'Sabre\\CalDAV\\Principal\\IProxyWrite' => $vendorDir . '/sabre/dav/lib/CalDAV/Principal/IProxyWrite.php', + 'Sabre\\CalDAV\\Principal\\ProxyRead' => $vendorDir . '/sabre/dav/lib/CalDAV/Principal/ProxyRead.php', + 'Sabre\\CalDAV\\Principal\\ProxyWrite' => $vendorDir . '/sabre/dav/lib/CalDAV/Principal/ProxyWrite.php', + 'Sabre\\CalDAV\\Principal\\User' => $vendorDir . '/sabre/dav/lib/CalDAV/Principal/User.php', + 'Sabre\\CalDAV\\Schedule\\IInbox' => $vendorDir . '/sabre/dav/lib/CalDAV/Schedule/IInbox.php', + 'Sabre\\CalDAV\\Schedule\\IMipPlugin' => $vendorDir . '/sabre/dav/lib/CalDAV/Schedule/IMipPlugin.php', + 'Sabre\\CalDAV\\Schedule\\IOutbox' => $vendorDir . '/sabre/dav/lib/CalDAV/Schedule/IOutbox.php', + 'Sabre\\CalDAV\\Schedule\\ISchedulingObject' => $vendorDir . '/sabre/dav/lib/CalDAV/Schedule/ISchedulingObject.php', + 'Sabre\\CalDAV\\Schedule\\Inbox' => $vendorDir . '/sabre/dav/lib/CalDAV/Schedule/Inbox.php', + 'Sabre\\CalDAV\\Schedule\\Outbox' => $vendorDir . '/sabre/dav/lib/CalDAV/Schedule/Outbox.php', + 'Sabre\\CalDAV\\Schedule\\Plugin' => $vendorDir . '/sabre/dav/lib/CalDAV/Schedule/Plugin.php', + 'Sabre\\CalDAV\\Schedule\\SchedulingObject' => $vendorDir . '/sabre/dav/lib/CalDAV/Schedule/SchedulingObject.php', + 'Sabre\\CalDAV\\SharedCalendar' => $vendorDir . '/sabre/dav/lib/CalDAV/SharedCalendar.php', + 'Sabre\\CalDAV\\SharingPlugin' => $vendorDir . '/sabre/dav/lib/CalDAV/SharingPlugin.php', + 'Sabre\\CalDAV\\Subscriptions\\ISubscription' => $vendorDir . '/sabre/dav/lib/CalDAV/Subscriptions/ISubscription.php', + 'Sabre\\CalDAV\\Subscriptions\\Plugin' => $vendorDir . '/sabre/dav/lib/CalDAV/Subscriptions/Plugin.php', + 'Sabre\\CalDAV\\Subscriptions\\Subscription' => $vendorDir . '/sabre/dav/lib/CalDAV/Subscriptions/Subscription.php', + 'Sabre\\CalDAV\\Xml\\Filter\\CalendarData' => $vendorDir . '/sabre/dav/lib/CalDAV/Xml/Filter/CalendarData.php', + 'Sabre\\CalDAV\\Xml\\Filter\\CompFilter' => $vendorDir . '/sabre/dav/lib/CalDAV/Xml/Filter/CompFilter.php', + 'Sabre\\CalDAV\\Xml\\Filter\\ParamFilter' => $vendorDir . '/sabre/dav/lib/CalDAV/Xml/Filter/ParamFilter.php', + 'Sabre\\CalDAV\\Xml\\Filter\\PropFilter' => $vendorDir . '/sabre/dav/lib/CalDAV/Xml/Filter/PropFilter.php', + 'Sabre\\CalDAV\\Xml\\Notification\\Invite' => $vendorDir . '/sabre/dav/lib/CalDAV/Xml/Notification/Invite.php', + 'Sabre\\CalDAV\\Xml\\Notification\\InviteReply' => $vendorDir . '/sabre/dav/lib/CalDAV/Xml/Notification/InviteReply.php', + 'Sabre\\CalDAV\\Xml\\Notification\\NotificationInterface' => $vendorDir . '/sabre/dav/lib/CalDAV/Xml/Notification/NotificationInterface.php', + 'Sabre\\CalDAV\\Xml\\Notification\\SystemStatus' => $vendorDir . '/sabre/dav/lib/CalDAV/Xml/Notification/SystemStatus.php', + 'Sabre\\CalDAV\\Xml\\Property\\AllowedSharingModes' => $vendorDir . '/sabre/dav/lib/CalDAV/Xml/Property/AllowedSharingModes.php', + 'Sabre\\CalDAV\\Xml\\Property\\EmailAddressSet' => $vendorDir . '/sabre/dav/lib/CalDAV/Xml/Property/EmailAddressSet.php', + 'Sabre\\CalDAV\\Xml\\Property\\Invite' => $vendorDir . '/sabre/dav/lib/CalDAV/Xml/Property/Invite.php', + 'Sabre\\CalDAV\\Xml\\Property\\ScheduleCalendarTransp' => $vendorDir . '/sabre/dav/lib/CalDAV/Xml/Property/ScheduleCalendarTransp.php', + 'Sabre\\CalDAV\\Xml\\Property\\SupportedCalendarComponentSet' => $vendorDir . '/sabre/dav/lib/CalDAV/Xml/Property/SupportedCalendarComponentSet.php', + 'Sabre\\CalDAV\\Xml\\Property\\SupportedCalendarData' => $vendorDir . '/sabre/dav/lib/CalDAV/Xml/Property/SupportedCalendarData.php', + 'Sabre\\CalDAV\\Xml\\Property\\SupportedCollationSet' => $vendorDir . '/sabre/dav/lib/CalDAV/Xml/Property/SupportedCollationSet.php', + 'Sabre\\CalDAV\\Xml\\Request\\CalendarMultiGetReport' => $vendorDir . '/sabre/dav/lib/CalDAV/Xml/Request/CalendarMultiGetReport.php', + 'Sabre\\CalDAV\\Xml\\Request\\CalendarQueryReport' => $vendorDir . '/sabre/dav/lib/CalDAV/Xml/Request/CalendarQueryReport.php', + 'Sabre\\CalDAV\\Xml\\Request\\FreeBusyQueryReport' => $vendorDir . '/sabre/dav/lib/CalDAV/Xml/Request/FreeBusyQueryReport.php', + 'Sabre\\CalDAV\\Xml\\Request\\InviteReply' => $vendorDir . '/sabre/dav/lib/CalDAV/Xml/Request/InviteReply.php', + 'Sabre\\CalDAV\\Xml\\Request\\MkCalendar' => $vendorDir . '/sabre/dav/lib/CalDAV/Xml/Request/MkCalendar.php', + 'Sabre\\CalDAV\\Xml\\Request\\Share' => $vendorDir . '/sabre/dav/lib/CalDAV/Xml/Request/Share.php', + 'Sabre\\CardDAV\\AddressBook' => $vendorDir . '/sabre/dav/lib/CardDAV/AddressBook.php', + 'Sabre\\CardDAV\\AddressBookHome' => $vendorDir . '/sabre/dav/lib/CardDAV/AddressBookHome.php', + 'Sabre\\CardDAV\\AddressBookRoot' => $vendorDir . '/sabre/dav/lib/CardDAV/AddressBookRoot.php', + 'Sabre\\CardDAV\\Backend\\AbstractBackend' => $vendorDir . '/sabre/dav/lib/CardDAV/Backend/AbstractBackend.php', + 'Sabre\\CardDAV\\Backend\\BackendInterface' => $vendorDir . '/sabre/dav/lib/CardDAV/Backend/BackendInterface.php', + 'Sabre\\CardDAV\\Backend\\PDO' => $vendorDir . '/sabre/dav/lib/CardDAV/Backend/PDO.php', + 'Sabre\\CardDAV\\Backend\\SyncSupport' => $vendorDir . '/sabre/dav/lib/CardDAV/Backend/SyncSupport.php', + 'Sabre\\CardDAV\\Card' => $vendorDir . '/sabre/dav/lib/CardDAV/Card.php', + 'Sabre\\CardDAV\\IAddressBook' => $vendorDir . '/sabre/dav/lib/CardDAV/IAddressBook.php', + 'Sabre\\CardDAV\\ICard' => $vendorDir . '/sabre/dav/lib/CardDAV/ICard.php', + 'Sabre\\CardDAV\\IDirectory' => $vendorDir . '/sabre/dav/lib/CardDAV/IDirectory.php', + 'Sabre\\CardDAV\\Plugin' => $vendorDir . '/sabre/dav/lib/CardDAV/Plugin.php', + 'Sabre\\CardDAV\\VCFExportPlugin' => $vendorDir . '/sabre/dav/lib/CardDAV/VCFExportPlugin.php', + 'Sabre\\CardDAV\\Xml\\Filter\\AddressData' => $vendorDir . '/sabre/dav/lib/CardDAV/Xml/Filter/AddressData.php', + 'Sabre\\CardDAV\\Xml\\Filter\\ParamFilter' => $vendorDir . '/sabre/dav/lib/CardDAV/Xml/Filter/ParamFilter.php', + 'Sabre\\CardDAV\\Xml\\Filter\\PropFilter' => $vendorDir . '/sabre/dav/lib/CardDAV/Xml/Filter/PropFilter.php', + 'Sabre\\CardDAV\\Xml\\Property\\SupportedAddressData' => $vendorDir . '/sabre/dav/lib/CardDAV/Xml/Property/SupportedAddressData.php', + 'Sabre\\CardDAV\\Xml\\Property\\SupportedCollationSet' => $vendorDir . '/sabre/dav/lib/CardDAV/Xml/Property/SupportedCollationSet.php', + 'Sabre\\CardDAV\\Xml\\Request\\AddressBookMultiGetReport' => $vendorDir . '/sabre/dav/lib/CardDAV/Xml/Request/AddressBookMultiGetReport.php', + 'Sabre\\CardDAV\\Xml\\Request\\AddressBookQueryReport' => $vendorDir . '/sabre/dav/lib/CardDAV/Xml/Request/AddressBookQueryReport.php', + 'Sabre\\DAVACL\\ACLTrait' => $vendorDir . '/sabre/dav/lib/DAVACL/ACLTrait.php', + 'Sabre\\DAVACL\\AbstractPrincipalCollection' => $vendorDir . '/sabre/dav/lib/DAVACL/AbstractPrincipalCollection.php', + 'Sabre\\DAVACL\\Exception\\AceConflict' => $vendorDir . '/sabre/dav/lib/DAVACL/Exception/AceConflict.php', + 'Sabre\\DAVACL\\Exception\\NeedPrivileges' => $vendorDir . '/sabre/dav/lib/DAVACL/Exception/NeedPrivileges.php', + 'Sabre\\DAVACL\\Exception\\NoAbstract' => $vendorDir . '/sabre/dav/lib/DAVACL/Exception/NoAbstract.php', + 'Sabre\\DAVACL\\Exception\\NotRecognizedPrincipal' => $vendorDir . '/sabre/dav/lib/DAVACL/Exception/NotRecognizedPrincipal.php', + 'Sabre\\DAVACL\\Exception\\NotSupportedPrivilege' => $vendorDir . '/sabre/dav/lib/DAVACL/Exception/NotSupportedPrivilege.php', + 'Sabre\\DAVACL\\FS\\Collection' => $vendorDir . '/sabre/dav/lib/DAVACL/FS/Collection.php', + 'Sabre\\DAVACL\\FS\\File' => $vendorDir . '/sabre/dav/lib/DAVACL/FS/File.php', + 'Sabre\\DAVACL\\FS\\HomeCollection' => $vendorDir . '/sabre/dav/lib/DAVACL/FS/HomeCollection.php', + 'Sabre\\DAVACL\\IACL' => $vendorDir . '/sabre/dav/lib/DAVACL/IACL.php', + 'Sabre\\DAVACL\\IPrincipal' => $vendorDir . '/sabre/dav/lib/DAVACL/IPrincipal.php', + 'Sabre\\DAVACL\\IPrincipalCollection' => $vendorDir . '/sabre/dav/lib/DAVACL/IPrincipalCollection.php', + 'Sabre\\DAVACL\\Plugin' => $vendorDir . '/sabre/dav/lib/DAVACL/Plugin.php', + 'Sabre\\DAVACL\\Principal' => $vendorDir . '/sabre/dav/lib/DAVACL/Principal.php', + 'Sabre\\DAVACL\\PrincipalBackend\\AbstractBackend' => $vendorDir . '/sabre/dav/lib/DAVACL/PrincipalBackend/AbstractBackend.php', + 'Sabre\\DAVACL\\PrincipalBackend\\BackendInterface' => $vendorDir . '/sabre/dav/lib/DAVACL/PrincipalBackend/BackendInterface.php', + 'Sabre\\DAVACL\\PrincipalBackend\\CreatePrincipalSupport' => $vendorDir . '/sabre/dav/lib/DAVACL/PrincipalBackend/CreatePrincipalSupport.php', + 'Sabre\\DAVACL\\PrincipalBackend\\PDO' => $vendorDir . '/sabre/dav/lib/DAVACL/PrincipalBackend/PDO.php', + 'Sabre\\DAVACL\\PrincipalCollection' => $vendorDir . '/sabre/dav/lib/DAVACL/PrincipalCollection.php', + 'Sabre\\DAVACL\\Xml\\Property\\Acl' => $vendorDir . '/sabre/dav/lib/DAVACL/Xml/Property/Acl.php', + 'Sabre\\DAVACL\\Xml\\Property\\AclRestrictions' => $vendorDir . '/sabre/dav/lib/DAVACL/Xml/Property/AclRestrictions.php', + 'Sabre\\DAVACL\\Xml\\Property\\CurrentUserPrivilegeSet' => $vendorDir . '/sabre/dav/lib/DAVACL/Xml/Property/CurrentUserPrivilegeSet.php', + 'Sabre\\DAVACL\\Xml\\Property\\Principal' => $vendorDir . '/sabre/dav/lib/DAVACL/Xml/Property/Principal.php', + 'Sabre\\DAVACL\\Xml\\Property\\SupportedPrivilegeSet' => $vendorDir . '/sabre/dav/lib/DAVACL/Xml/Property/SupportedPrivilegeSet.php', + 'Sabre\\DAVACL\\Xml\\Request\\AclPrincipalPropSetReport' => $vendorDir . '/sabre/dav/lib/DAVACL/Xml/Request/AclPrincipalPropSetReport.php', + 'Sabre\\DAVACL\\Xml\\Request\\ExpandPropertyReport' => $vendorDir . '/sabre/dav/lib/DAVACL/Xml/Request/ExpandPropertyReport.php', + 'Sabre\\DAVACL\\Xml\\Request\\PrincipalMatchReport' => $vendorDir . '/sabre/dav/lib/DAVACL/Xml/Request/PrincipalMatchReport.php', + 'Sabre\\DAVACL\\Xml\\Request\\PrincipalPropertySearchReport' => $vendorDir . '/sabre/dav/lib/DAVACL/Xml/Request/PrincipalPropertySearchReport.php', + 'Sabre\\DAVACL\\Xml\\Request\\PrincipalSearchPropertySetReport' => $vendorDir . '/sabre/dav/lib/DAVACL/Xml/Request/PrincipalSearchPropertySetReport.php', + 'Sabre\\DAV\\Auth\\Backend\\AbstractBasic' => $vendorDir . '/sabre/dav/lib/DAV/Auth/Backend/AbstractBasic.php', + 'Sabre\\DAV\\Auth\\Backend\\AbstractBearer' => $vendorDir . '/sabre/dav/lib/DAV/Auth/Backend/AbstractBearer.php', + 'Sabre\\DAV\\Auth\\Backend\\AbstractDigest' => $vendorDir . '/sabre/dav/lib/DAV/Auth/Backend/AbstractDigest.php', + 'Sabre\\DAV\\Auth\\Backend\\Apache' => $vendorDir . '/sabre/dav/lib/DAV/Auth/Backend/Apache.php', + 'Sabre\\DAV\\Auth\\Backend\\BackendInterface' => $vendorDir . '/sabre/dav/lib/DAV/Auth/Backend/BackendInterface.php', + 'Sabre\\DAV\\Auth\\Backend\\BasicCallBack' => $vendorDir . '/sabre/dav/lib/DAV/Auth/Backend/BasicCallBack.php', + 'Sabre\\DAV\\Auth\\Backend\\File' => $vendorDir . '/sabre/dav/lib/DAV/Auth/Backend/File.php', + 'Sabre\\DAV\\Auth\\Backend\\IMAP' => $vendorDir . '/sabre/dav/lib/DAV/Auth/Backend/IMAP.php', + 'Sabre\\DAV\\Auth\\Backend\\PDO' => $vendorDir . '/sabre/dav/lib/DAV/Auth/Backend/PDO.php', + 'Sabre\\DAV\\Auth\\Backend\\PDOBasicAuth' => $vendorDir . '/sabre/dav/lib/DAV/Auth/Backend/PDOBasicAuth.php', + 'Sabre\\DAV\\Auth\\Plugin' => $vendorDir . '/sabre/dav/lib/DAV/Auth/Plugin.php', + 'Sabre\\DAV\\Browser\\GuessContentType' => $vendorDir . '/sabre/dav/lib/DAV/Browser/GuessContentType.php', + 'Sabre\\DAV\\Browser\\HtmlOutput' => $vendorDir . '/sabre/dav/lib/DAV/Browser/HtmlOutput.php', + 'Sabre\\DAV\\Browser\\HtmlOutputHelper' => $vendorDir . '/sabre/dav/lib/DAV/Browser/HtmlOutputHelper.php', + 'Sabre\\DAV\\Browser\\MapGetToPropFind' => $vendorDir . '/sabre/dav/lib/DAV/Browser/MapGetToPropFind.php', + 'Sabre\\DAV\\Browser\\Plugin' => $vendorDir . '/sabre/dav/lib/DAV/Browser/Plugin.php', + 'Sabre\\DAV\\Browser\\PropFindAll' => $vendorDir . '/sabre/dav/lib/DAV/Browser/PropFindAll.php', + 'Sabre\\DAV\\Client' => $vendorDir . '/sabre/dav/lib/DAV/Client.php', + 'Sabre\\DAV\\Collection' => $vendorDir . '/sabre/dav/lib/DAV/Collection.php', + 'Sabre\\DAV\\CorePlugin' => $vendorDir . '/sabre/dav/lib/DAV/CorePlugin.php', + 'Sabre\\DAV\\Exception' => $vendorDir . '/sabre/dav/lib/DAV/Exception.php', + 'Sabre\\DAV\\Exception\\BadRequest' => $vendorDir . '/sabre/dav/lib/DAV/Exception/BadRequest.php', + 'Sabre\\DAV\\Exception\\Conflict' => $vendorDir . '/sabre/dav/lib/DAV/Exception/Conflict.php', + 'Sabre\\DAV\\Exception\\ConflictingLock' => $vendorDir . '/sabre/dav/lib/DAV/Exception/ConflictingLock.php', + 'Sabre\\DAV\\Exception\\Forbidden' => $vendorDir . '/sabre/dav/lib/DAV/Exception/Forbidden.php', + 'Sabre\\DAV\\Exception\\InsufficientStorage' => $vendorDir . '/sabre/dav/lib/DAV/Exception/InsufficientStorage.php', + 'Sabre\\DAV\\Exception\\InvalidResourceType' => $vendorDir . '/sabre/dav/lib/DAV/Exception/InvalidResourceType.php', + 'Sabre\\DAV\\Exception\\InvalidSyncToken' => $vendorDir . '/sabre/dav/lib/DAV/Exception/InvalidSyncToken.php', + 'Sabre\\DAV\\Exception\\LengthRequired' => $vendorDir . '/sabre/dav/lib/DAV/Exception/LengthRequired.php', + 'Sabre\\DAV\\Exception\\LockTokenMatchesRequestUri' => $vendorDir . '/sabre/dav/lib/DAV/Exception/LockTokenMatchesRequestUri.php', + 'Sabre\\DAV\\Exception\\Locked' => $vendorDir . '/sabre/dav/lib/DAV/Exception/Locked.php', + 'Sabre\\DAV\\Exception\\MethodNotAllowed' => $vendorDir . '/sabre/dav/lib/DAV/Exception/MethodNotAllowed.php', + 'Sabre\\DAV\\Exception\\NotAuthenticated' => $vendorDir . '/sabre/dav/lib/DAV/Exception/NotAuthenticated.php', + 'Sabre\\DAV\\Exception\\NotFound' => $vendorDir . '/sabre/dav/lib/DAV/Exception/NotFound.php', + 'Sabre\\DAV\\Exception\\NotImplemented' => $vendorDir . '/sabre/dav/lib/DAV/Exception/NotImplemented.php', + 'Sabre\\DAV\\Exception\\PaymentRequired' => $vendorDir . '/sabre/dav/lib/DAV/Exception/PaymentRequired.php', + 'Sabre\\DAV\\Exception\\PreconditionFailed' => $vendorDir . '/sabre/dav/lib/DAV/Exception/PreconditionFailed.php', + 'Sabre\\DAV\\Exception\\ReportNotSupported' => $vendorDir . '/sabre/dav/lib/DAV/Exception/ReportNotSupported.php', + 'Sabre\\DAV\\Exception\\RequestedRangeNotSatisfiable' => $vendorDir . '/sabre/dav/lib/DAV/Exception/RequestedRangeNotSatisfiable.php', + 'Sabre\\DAV\\Exception\\ServiceUnavailable' => $vendorDir . '/sabre/dav/lib/DAV/Exception/ServiceUnavailable.php', + 'Sabre\\DAV\\Exception\\TooManyMatches' => $vendorDir . '/sabre/dav/lib/DAV/Exception/TooManyMatches.php', + 'Sabre\\DAV\\Exception\\UnsupportedMediaType' => $vendorDir . '/sabre/dav/lib/DAV/Exception/UnsupportedMediaType.php', + 'Sabre\\DAV\\FSExt\\Directory' => $vendorDir . '/sabre/dav/lib/DAV/FSExt/Directory.php', + 'Sabre\\DAV\\FSExt\\File' => $vendorDir . '/sabre/dav/lib/DAV/FSExt/File.php', + 'Sabre\\DAV\\FS\\Directory' => $vendorDir . '/sabre/dav/lib/DAV/FS/Directory.php', + 'Sabre\\DAV\\FS\\File' => $vendorDir . '/sabre/dav/lib/DAV/FS/File.php', + 'Sabre\\DAV\\FS\\Node' => $vendorDir . '/sabre/dav/lib/DAV/FS/Node.php', + 'Sabre\\DAV\\File' => $vendorDir . '/sabre/dav/lib/DAV/File.php', + 'Sabre\\DAV\\ICollection' => $vendorDir . '/sabre/dav/lib/DAV/ICollection.php', + 'Sabre\\DAV\\ICopyTarget' => $vendorDir . '/sabre/dav/lib/DAV/ICopyTarget.php', + 'Sabre\\DAV\\IExtendedCollection' => $vendorDir . '/sabre/dav/lib/DAV/IExtendedCollection.php', + 'Sabre\\DAV\\IFile' => $vendorDir . '/sabre/dav/lib/DAV/IFile.php', + 'Sabre\\DAV\\IMoveTarget' => $vendorDir . '/sabre/dav/lib/DAV/IMoveTarget.php', + 'Sabre\\DAV\\IMultiGet' => $vendorDir . '/sabre/dav/lib/DAV/IMultiGet.php', + 'Sabre\\DAV\\INode' => $vendorDir . '/sabre/dav/lib/DAV/INode.php', + 'Sabre\\DAV\\INodeByPath' => $vendorDir . '/sabre/dav/lib/DAV/INodeByPath.php', + 'Sabre\\DAV\\IProperties' => $vendorDir . '/sabre/dav/lib/DAV/IProperties.php', + 'Sabre\\DAV\\IQuota' => $vendorDir . '/sabre/dav/lib/DAV/IQuota.php', + 'Sabre\\DAV\\Locks\\Backend\\AbstractBackend' => $vendorDir . '/sabre/dav/lib/DAV/Locks/Backend/AbstractBackend.php', + 'Sabre\\DAV\\Locks\\Backend\\BackendInterface' => $vendorDir . '/sabre/dav/lib/DAV/Locks/Backend/BackendInterface.php', + 'Sabre\\DAV\\Locks\\Backend\\File' => $vendorDir . '/sabre/dav/lib/DAV/Locks/Backend/File.php', + 'Sabre\\DAV\\Locks\\Backend\\PDO' => $vendorDir . '/sabre/dav/lib/DAV/Locks/Backend/PDO.php', + 'Sabre\\DAV\\Locks\\LockInfo' => $vendorDir . '/sabre/dav/lib/DAV/Locks/LockInfo.php', + 'Sabre\\DAV\\Locks\\Plugin' => $vendorDir . '/sabre/dav/lib/DAV/Locks/Plugin.php', + 'Sabre\\DAV\\MkCol' => $vendorDir . '/sabre/dav/lib/DAV/MkCol.php', + 'Sabre\\DAV\\Mount\\Plugin' => $vendorDir . '/sabre/dav/lib/DAV/Mount/Plugin.php', + 'Sabre\\DAV\\Node' => $vendorDir . '/sabre/dav/lib/DAV/Node.php', + 'Sabre\\DAV\\PartialUpdate\\IPatchSupport' => $vendorDir . '/sabre/dav/lib/DAV/PartialUpdate/IPatchSupport.php', + 'Sabre\\DAV\\PartialUpdate\\Plugin' => $vendorDir . '/sabre/dav/lib/DAV/PartialUpdate/Plugin.php', + 'Sabre\\DAV\\PropFind' => $vendorDir . '/sabre/dav/lib/DAV/PropFind.php', + 'Sabre\\DAV\\PropPatch' => $vendorDir . '/sabre/dav/lib/DAV/PropPatch.php', + 'Sabre\\DAV\\PropertyStorage\\Backend\\BackendInterface' => $vendorDir . '/sabre/dav/lib/DAV/PropertyStorage/Backend/BackendInterface.php', + 'Sabre\\DAV\\PropertyStorage\\Backend\\PDO' => $vendorDir . '/sabre/dav/lib/DAV/PropertyStorage/Backend/PDO.php', + 'Sabre\\DAV\\PropertyStorage\\Plugin' => $vendorDir . '/sabre/dav/lib/DAV/PropertyStorage/Plugin.php', + 'Sabre\\DAV\\Server' => $vendorDir . '/sabre/dav/lib/DAV/Server.php', + 'Sabre\\DAV\\ServerPlugin' => $vendorDir . '/sabre/dav/lib/DAV/ServerPlugin.php', + 'Sabre\\DAV\\Sharing\\ISharedNode' => $vendorDir . '/sabre/dav/lib/DAV/Sharing/ISharedNode.php', + 'Sabre\\DAV\\Sharing\\Plugin' => $vendorDir . '/sabre/dav/lib/DAV/Sharing/Plugin.php', + 'Sabre\\DAV\\SimpleCollection' => $vendorDir . '/sabre/dav/lib/DAV/SimpleCollection.php', + 'Sabre\\DAV\\SimpleFile' => $vendorDir . '/sabre/dav/lib/DAV/SimpleFile.php', + 'Sabre\\DAV\\StringUtil' => $vendorDir . '/sabre/dav/lib/DAV/StringUtil.php', + 'Sabre\\DAV\\Sync\\ISyncCollection' => $vendorDir . '/sabre/dav/lib/DAV/Sync/ISyncCollection.php', + 'Sabre\\DAV\\Sync\\Plugin' => $vendorDir . '/sabre/dav/lib/DAV/Sync/Plugin.php', + 'Sabre\\DAV\\TemporaryFileFilterPlugin' => $vendorDir . '/sabre/dav/lib/DAV/TemporaryFileFilterPlugin.php', + 'Sabre\\DAV\\Tree' => $vendorDir . '/sabre/dav/lib/DAV/Tree.php', + 'Sabre\\DAV\\UUIDUtil' => $vendorDir . '/sabre/dav/lib/DAV/UUIDUtil.php', + 'Sabre\\DAV\\Version' => $vendorDir . '/sabre/dav/lib/DAV/Version.php', + 'Sabre\\DAV\\Xml\\Element\\Prop' => $vendorDir . '/sabre/dav/lib/DAV/Xml/Element/Prop.php', + 'Sabre\\DAV\\Xml\\Element\\Response' => $vendorDir . '/sabre/dav/lib/DAV/Xml/Element/Response.php', + 'Sabre\\DAV\\Xml\\Element\\Sharee' => $vendorDir . '/sabre/dav/lib/DAV/Xml/Element/Sharee.php', + 'Sabre\\DAV\\Xml\\Property\\Complex' => $vendorDir . '/sabre/dav/lib/DAV/Xml/Property/Complex.php', + 'Sabre\\DAV\\Xml\\Property\\GetLastModified' => $vendorDir . '/sabre/dav/lib/DAV/Xml/Property/GetLastModified.php', + 'Sabre\\DAV\\Xml\\Property\\Href' => $vendorDir . '/sabre/dav/lib/DAV/Xml/Property/Href.php', + 'Sabre\\DAV\\Xml\\Property\\Invite' => $vendorDir . '/sabre/dav/lib/DAV/Xml/Property/Invite.php', + 'Sabre\\DAV\\Xml\\Property\\LocalHref' => $vendorDir . '/sabre/dav/lib/DAV/Xml/Property/LocalHref.php', + 'Sabre\\DAV\\Xml\\Property\\LockDiscovery' => $vendorDir . '/sabre/dav/lib/DAV/Xml/Property/LockDiscovery.php', + 'Sabre\\DAV\\Xml\\Property\\ResourceType' => $vendorDir . '/sabre/dav/lib/DAV/Xml/Property/ResourceType.php', + 'Sabre\\DAV\\Xml\\Property\\ShareAccess' => $vendorDir . '/sabre/dav/lib/DAV/Xml/Property/ShareAccess.php', + 'Sabre\\DAV\\Xml\\Property\\SupportedLock' => $vendorDir . '/sabre/dav/lib/DAV/Xml/Property/SupportedLock.php', + 'Sabre\\DAV\\Xml\\Property\\SupportedMethodSet' => $vendorDir . '/sabre/dav/lib/DAV/Xml/Property/SupportedMethodSet.php', + 'Sabre\\DAV\\Xml\\Property\\SupportedReportSet' => $vendorDir . '/sabre/dav/lib/DAV/Xml/Property/SupportedReportSet.php', + 'Sabre\\DAV\\Xml\\Request\\Lock' => $vendorDir . '/sabre/dav/lib/DAV/Xml/Request/Lock.php', + 'Sabre\\DAV\\Xml\\Request\\MkCol' => $vendorDir . '/sabre/dav/lib/DAV/Xml/Request/MkCol.php', + 'Sabre\\DAV\\Xml\\Request\\PropFind' => $vendorDir . '/sabre/dav/lib/DAV/Xml/Request/PropFind.php', + 'Sabre\\DAV\\Xml\\Request\\PropPatch' => $vendorDir . '/sabre/dav/lib/DAV/Xml/Request/PropPatch.php', + 'Sabre\\DAV\\Xml\\Request\\ShareResource' => $vendorDir . '/sabre/dav/lib/DAV/Xml/Request/ShareResource.php', + 'Sabre\\DAV\\Xml\\Request\\SyncCollectionReport' => $vendorDir . '/sabre/dav/lib/DAV/Xml/Request/SyncCollectionReport.php', + 'Sabre\\DAV\\Xml\\Response\\MultiStatus' => $vendorDir . '/sabre/dav/lib/DAV/Xml/Response/MultiStatus.php', + 'Sabre\\DAV\\Xml\\Service' => $vendorDir . '/sabre/dav/lib/DAV/Xml/Service.php', + 'Sabre\\Event\\Emitter' => $vendorDir . '/sabre/event/lib/Emitter.php', + 'Sabre\\Event\\EmitterInterface' => $vendorDir . '/sabre/event/lib/EmitterInterface.php', + 'Sabre\\Event\\EmitterTrait' => $vendorDir . '/sabre/event/lib/EmitterTrait.php', + 'Sabre\\Event\\EventEmitter' => $vendorDir . '/sabre/event/lib/EventEmitter.php', + 'Sabre\\Event\\Loop\\Loop' => $vendorDir . '/sabre/event/lib/Loop/Loop.php', + 'Sabre\\Event\\Promise' => $vendorDir . '/sabre/event/lib/Promise.php', + 'Sabre\\Event\\PromiseAlreadyResolvedException' => $vendorDir . '/sabre/event/lib/PromiseAlreadyResolvedException.php', + 'Sabre\\Event\\Version' => $vendorDir . '/sabre/event/lib/Version.php', + 'Sabre\\Event\\WildcardEmitter' => $vendorDir . '/sabre/event/lib/WildcardEmitter.php', + 'Sabre\\Event\\WildcardEmitterTrait' => $vendorDir . '/sabre/event/lib/WildcardEmitterTrait.php', + 'Sabre\\HTTP\\Auth\\AWS' => $vendorDir . '/sabre/http/lib/Auth/AWS.php', + 'Sabre\\HTTP\\Auth\\AbstractAuth' => $vendorDir . '/sabre/http/lib/Auth/AbstractAuth.php', + 'Sabre\\HTTP\\Auth\\Basic' => $vendorDir . '/sabre/http/lib/Auth/Basic.php', + 'Sabre\\HTTP\\Auth\\Bearer' => $vendorDir . '/sabre/http/lib/Auth/Bearer.php', + 'Sabre\\HTTP\\Auth\\Digest' => $vendorDir . '/sabre/http/lib/Auth/Digest.php', + 'Sabre\\HTTP\\Client' => $vendorDir . '/sabre/http/lib/Client.php', + 'Sabre\\HTTP\\ClientException' => $vendorDir . '/sabre/http/lib/ClientException.php', + 'Sabre\\HTTP\\ClientHttpException' => $vendorDir . '/sabre/http/lib/ClientHttpException.php', + 'Sabre\\HTTP\\HttpException' => $vendorDir . '/sabre/http/lib/HttpException.php', + 'Sabre\\HTTP\\Message' => $vendorDir . '/sabre/http/lib/Message.php', + 'Sabre\\HTTP\\MessageDecoratorTrait' => $vendorDir . '/sabre/http/lib/MessageDecoratorTrait.php', + 'Sabre\\HTTP\\MessageInterface' => $vendorDir . '/sabre/http/lib/MessageInterface.php', + 'Sabre\\HTTP\\Request' => $vendorDir . '/sabre/http/lib/Request.php', + 'Sabre\\HTTP\\RequestDecorator' => $vendorDir . '/sabre/http/lib/RequestDecorator.php', + 'Sabre\\HTTP\\RequestInterface' => $vendorDir . '/sabre/http/lib/RequestInterface.php', + 'Sabre\\HTTP\\Response' => $vendorDir . '/sabre/http/lib/Response.php', + 'Sabre\\HTTP\\ResponseDecorator' => $vendorDir . '/sabre/http/lib/ResponseDecorator.php', + 'Sabre\\HTTP\\ResponseInterface' => $vendorDir . '/sabre/http/lib/ResponseInterface.php', + 'Sabre\\HTTP\\Sapi' => $vendorDir . '/sabre/http/lib/Sapi.php', + 'Sabre\\HTTP\\Version' => $vendorDir . '/sabre/http/lib/Version.php', + 'Sabre\\Uri\\InvalidUriException' => $vendorDir . '/sabre/uri/lib/InvalidUriException.php', + 'Sabre\\Uri\\Version' => $vendorDir . '/sabre/uri/lib/Version.php', + 'Sabre\\VObject\\BirthdayCalendarGenerator' => $vendorDir . '/sabre/vobject/lib/BirthdayCalendarGenerator.php', + 'Sabre\\VObject\\Cli' => $vendorDir . '/sabre/vobject/lib/Cli.php', + 'Sabre\\VObject\\Component' => $vendorDir . '/sabre/vobject/lib/Component.php', + 'Sabre\\VObject\\Component\\Available' => $vendorDir . '/sabre/vobject/lib/Component/Available.php', + 'Sabre\\VObject\\Component\\VAlarm' => $vendorDir . '/sabre/vobject/lib/Component/VAlarm.php', + 'Sabre\\VObject\\Component\\VAvailability' => $vendorDir . '/sabre/vobject/lib/Component/VAvailability.php', + 'Sabre\\VObject\\Component\\VCalendar' => $vendorDir . '/sabre/vobject/lib/Component/VCalendar.php', + 'Sabre\\VObject\\Component\\VCard' => $vendorDir . '/sabre/vobject/lib/Component/VCard.php', + 'Sabre\\VObject\\Component\\VEvent' => $vendorDir . '/sabre/vobject/lib/Component/VEvent.php', + 'Sabre\\VObject\\Component\\VFreeBusy' => $vendorDir . '/sabre/vobject/lib/Component/VFreeBusy.php', + 'Sabre\\VObject\\Component\\VJournal' => $vendorDir . '/sabre/vobject/lib/Component/VJournal.php', + 'Sabre\\VObject\\Component\\VTimeZone' => $vendorDir . '/sabre/vobject/lib/Component/VTimeZone.php', + 'Sabre\\VObject\\Component\\VTodo' => $vendorDir . '/sabre/vobject/lib/Component/VTodo.php', + 'Sabre\\VObject\\DateTimeParser' => $vendorDir . '/sabre/vobject/lib/DateTimeParser.php', + 'Sabre\\VObject\\Document' => $vendorDir . '/sabre/vobject/lib/Document.php', + 'Sabre\\VObject\\ElementList' => $vendorDir . '/sabre/vobject/lib/ElementList.php', + 'Sabre\\VObject\\EofException' => $vendorDir . '/sabre/vobject/lib/EofException.php', + 'Sabre\\VObject\\FreeBusyData' => $vendorDir . '/sabre/vobject/lib/FreeBusyData.php', + 'Sabre\\VObject\\FreeBusyGenerator' => $vendorDir . '/sabre/vobject/lib/FreeBusyGenerator.php', + 'Sabre\\VObject\\ITip\\Broker' => $vendorDir . '/sabre/vobject/lib/ITip/Broker.php', + 'Sabre\\VObject\\ITip\\ITipException' => $vendorDir . '/sabre/vobject/lib/ITip/ITipException.php', + 'Sabre\\VObject\\ITip\\Message' => $vendorDir . '/sabre/vobject/lib/ITip/Message.php', + 'Sabre\\VObject\\ITip\\SameOrganizerForAllComponentsException' => $vendorDir . '/sabre/vobject/lib/ITip/SameOrganizerForAllComponentsException.php', + 'Sabre\\VObject\\InvalidDataException' => $vendorDir . '/sabre/vobject/lib/InvalidDataException.php', + 'Sabre\\VObject\\Node' => $vendorDir . '/sabre/vobject/lib/Node.php', + 'Sabre\\VObject\\PHPUnitAssertions' => $vendorDir . '/sabre/vobject/lib/PHPUnitAssertions.php', + 'Sabre\\VObject\\Parameter' => $vendorDir . '/sabre/vobject/lib/Parameter.php', + 'Sabre\\VObject\\ParseException' => $vendorDir . '/sabre/vobject/lib/ParseException.php', + 'Sabre\\VObject\\Parser\\Json' => $vendorDir . '/sabre/vobject/lib/Parser/Json.php', + 'Sabre\\VObject\\Parser\\MimeDir' => $vendorDir . '/sabre/vobject/lib/Parser/MimeDir.php', + 'Sabre\\VObject\\Parser\\Parser' => $vendorDir . '/sabre/vobject/lib/Parser/Parser.php', + 'Sabre\\VObject\\Parser\\XML' => $vendorDir . '/sabre/vobject/lib/Parser/XML.php', + 'Sabre\\VObject\\Parser\\XML\\Element\\KeyValue' => $vendorDir . '/sabre/vobject/lib/Parser/XML/Element/KeyValue.php', + 'Sabre\\VObject\\Property' => $vendorDir . '/sabre/vobject/lib/Property.php', + 'Sabre\\VObject\\Property\\Binary' => $vendorDir . '/sabre/vobject/lib/Property/Binary.php', + 'Sabre\\VObject\\Property\\Boolean' => $vendorDir . '/sabre/vobject/lib/Property/Boolean.php', + 'Sabre\\VObject\\Property\\FlatText' => $vendorDir . '/sabre/vobject/lib/Property/FlatText.php', + 'Sabre\\VObject\\Property\\FloatValue' => $vendorDir . '/sabre/vobject/lib/Property/FloatValue.php', + 'Sabre\\VObject\\Property\\ICalendar\\CalAddress' => $vendorDir . '/sabre/vobject/lib/Property/ICalendar/CalAddress.php', + 'Sabre\\VObject\\Property\\ICalendar\\Date' => $vendorDir . '/sabre/vobject/lib/Property/ICalendar/Date.php', + 'Sabre\\VObject\\Property\\ICalendar\\DateTime' => $vendorDir . '/sabre/vobject/lib/Property/ICalendar/DateTime.php', + 'Sabre\\VObject\\Property\\ICalendar\\Duration' => $vendorDir . '/sabre/vobject/lib/Property/ICalendar/Duration.php', + 'Sabre\\VObject\\Property\\ICalendar\\Period' => $vendorDir . '/sabre/vobject/lib/Property/ICalendar/Period.php', + 'Sabre\\VObject\\Property\\ICalendar\\Recur' => $vendorDir . '/sabre/vobject/lib/Property/ICalendar/Recur.php', + 'Sabre\\VObject\\Property\\IntegerValue' => $vendorDir . '/sabre/vobject/lib/Property/IntegerValue.php', + 'Sabre\\VObject\\Property\\Text' => $vendorDir . '/sabre/vobject/lib/Property/Text.php', + 'Sabre\\VObject\\Property\\Time' => $vendorDir . '/sabre/vobject/lib/Property/Time.php', + 'Sabre\\VObject\\Property\\Unknown' => $vendorDir . '/sabre/vobject/lib/Property/Unknown.php', + 'Sabre\\VObject\\Property\\Uri' => $vendorDir . '/sabre/vobject/lib/Property/Uri.php', + 'Sabre\\VObject\\Property\\UtcOffset' => $vendorDir . '/sabre/vobject/lib/Property/UtcOffset.php', + 'Sabre\\VObject\\Property\\VCard\\Date' => $vendorDir . '/sabre/vobject/lib/Property/VCard/Date.php', + 'Sabre\\VObject\\Property\\VCard\\DateAndOrTime' => $vendorDir . '/sabre/vobject/lib/Property/VCard/DateAndOrTime.php', + 'Sabre\\VObject\\Property\\VCard\\DateTime' => $vendorDir . '/sabre/vobject/lib/Property/VCard/DateTime.php', + 'Sabre\\VObject\\Property\\VCard\\LanguageTag' => $vendorDir . '/sabre/vobject/lib/Property/VCard/LanguageTag.php', + 'Sabre\\VObject\\Property\\VCard\\PhoneNumber' => $vendorDir . '/sabre/vobject/lib/Property/VCard/PhoneNumber.php', + 'Sabre\\VObject\\Property\\VCard\\TimeStamp' => $vendorDir . '/sabre/vobject/lib/Property/VCard/TimeStamp.php', + 'Sabre\\VObject\\Reader' => $vendorDir . '/sabre/vobject/lib/Reader.php', + 'Sabre\\VObject\\Recur\\EventIterator' => $vendorDir . '/sabre/vobject/lib/Recur/EventIterator.php', + 'Sabre\\VObject\\Recur\\MaxInstancesExceededException' => $vendorDir . '/sabre/vobject/lib/Recur/MaxInstancesExceededException.php', + 'Sabre\\VObject\\Recur\\NoInstancesException' => $vendorDir . '/sabre/vobject/lib/Recur/NoInstancesException.php', + 'Sabre\\VObject\\Recur\\RDateIterator' => $vendorDir . '/sabre/vobject/lib/Recur/RDateIterator.php', + 'Sabre\\VObject\\Recur\\RRuleIterator' => $vendorDir . '/sabre/vobject/lib/Recur/RRuleIterator.php', + 'Sabre\\VObject\\Settings' => $vendorDir . '/sabre/vobject/lib/Settings.php', + 'Sabre\\VObject\\Splitter\\ICalendar' => $vendorDir . '/sabre/vobject/lib/Splitter/ICalendar.php', + 'Sabre\\VObject\\Splitter\\SplitterInterface' => $vendorDir . '/sabre/vobject/lib/Splitter/SplitterInterface.php', + 'Sabre\\VObject\\Splitter\\VCard' => $vendorDir . '/sabre/vobject/lib/Splitter/VCard.php', + 'Sabre\\VObject\\StringUtil' => $vendorDir . '/sabre/vobject/lib/StringUtil.php', + 'Sabre\\VObject\\TimeZoneUtil' => $vendorDir . '/sabre/vobject/lib/TimeZoneUtil.php', + 'Sabre\\VObject\\TimezoneGuesser\\FindFromOffset' => $vendorDir . '/sabre/vobject/lib/TimezoneGuesser/FindFromOffset.php', + 'Sabre\\VObject\\TimezoneGuesser\\FindFromTimezoneIdentifier' => $vendorDir . '/sabre/vobject/lib/TimezoneGuesser/FindFromTimezoneIdentifier.php', + 'Sabre\\VObject\\TimezoneGuesser\\FindFromTimezoneMap' => $vendorDir . '/sabre/vobject/lib/TimezoneGuesser/FindFromTimezoneMap.php', + 'Sabre\\VObject\\TimezoneGuesser\\GuessFromLicEntry' => $vendorDir . '/sabre/vobject/lib/TimezoneGuesser/GuessFromLicEntry.php', + 'Sabre\\VObject\\TimezoneGuesser\\GuessFromMsTzId' => $vendorDir . '/sabre/vobject/lib/TimezoneGuesser/GuessFromMsTzId.php', + 'Sabre\\VObject\\TimezoneGuesser\\TimezoneFinder' => $vendorDir . '/sabre/vobject/lib/TimezoneGuesser/TimezoneFinder.php', + 'Sabre\\VObject\\TimezoneGuesser\\TimezoneGuesser' => $vendorDir . '/sabre/vobject/lib/TimezoneGuesser/TimezoneGuesser.php', + 'Sabre\\VObject\\UUIDUtil' => $vendorDir . '/sabre/vobject/lib/UUIDUtil.php', + 'Sabre\\VObject\\VCardConverter' => $vendorDir . '/sabre/vobject/lib/VCardConverter.php', + 'Sabre\\VObject\\Version' => $vendorDir . '/sabre/vobject/lib/Version.php', + 'Sabre\\VObject\\Writer' => $vendorDir . '/sabre/vobject/lib/Writer.php', + 'Sabre\\Xml\\ContextStackTrait' => $vendorDir . '/sabre/xml/lib/ContextStackTrait.php', + 'Sabre\\Xml\\Element' => $vendorDir . '/sabre/xml/lib/Element.php', + 'Sabre\\Xml\\Element\\Base' => $vendorDir . '/sabre/xml/lib/Element/Base.php', + 'Sabre\\Xml\\Element\\Cdata' => $vendorDir . '/sabre/xml/lib/Element/Cdata.php', + 'Sabre\\Xml\\Element\\Elements' => $vendorDir . '/sabre/xml/lib/Element/Elements.php', + 'Sabre\\Xml\\Element\\KeyValue' => $vendorDir . '/sabre/xml/lib/Element/KeyValue.php', + 'Sabre\\Xml\\Element\\Uri' => $vendorDir . '/sabre/xml/lib/Element/Uri.php', + 'Sabre\\Xml\\Element\\XmlFragment' => $vendorDir . '/sabre/xml/lib/Element/XmlFragment.php', + 'Sabre\\Xml\\LibXMLException' => $vendorDir . '/sabre/xml/lib/LibXMLException.php', + 'Sabre\\Xml\\ParseException' => $vendorDir . '/sabre/xml/lib/ParseException.php', + 'Sabre\\Xml\\Reader' => $vendorDir . '/sabre/xml/lib/Reader.php', + 'Sabre\\Xml\\Service' => $vendorDir . '/sabre/xml/lib/Service.php', + 'Sabre\\Xml\\Version' => $vendorDir . '/sabre/xml/lib/Version.php', + 'Sabre\\Xml\\Writer' => $vendorDir . '/sabre/xml/lib/Writer.php', + 'Sabre\\Xml\\XmlDeserializable' => $vendorDir . '/sabre/xml/lib/XmlDeserializable.php', + 'Sabre\\Xml\\XmlSerializable' => $vendorDir . '/sabre/xml/lib/XmlSerializable.php', + 'SearchDAV\\Backend\\ISearchBackend' => $vendorDir . '/icewind/searchdav/src/Backend/ISearchBackend.php', + 'SearchDAV\\Backend\\SearchPropertyDefinition' => $vendorDir . '/icewind/searchdav/src/Backend/SearchPropertyDefinition.php', + 'SearchDAV\\Backend\\SearchResult' => $vendorDir . '/icewind/searchdav/src/Backend/SearchResult.php', + 'SearchDAV\\DAV\\DiscoverHandler' => $vendorDir . '/icewind/searchdav/src/DAV/DiscoverHandler.php', + 'SearchDAV\\DAV\\PathHelper' => $vendorDir . '/icewind/searchdav/src/DAV/PathHelper.php', + 'SearchDAV\\DAV\\QueryParser' => $vendorDir . '/icewind/searchdav/src/DAV/QueryParser.php', + 'SearchDAV\\DAV\\SearchHandler' => $vendorDir . '/icewind/searchdav/src/DAV/SearchHandler.php', + 'SearchDAV\\DAV\\SearchPlugin' => $vendorDir . '/icewind/searchdav/src/DAV/SearchPlugin.php', + 'SearchDAV\\Query\\Limit' => $vendorDir . '/icewind/searchdav/src/Query/Limit.php', + 'SearchDAV\\Query\\Literal' => $vendorDir . '/icewind/searchdav/src/Query/Literal.php', + 'SearchDAV\\Query\\Operator' => $vendorDir . '/icewind/searchdav/src/Query/Operator.php', + 'SearchDAV\\Query\\Order' => $vendorDir . '/icewind/searchdav/src/Query/Order.php', + 'SearchDAV\\Query\\Query' => $vendorDir . '/icewind/searchdav/src/Query/Query.php', + 'SearchDAV\\Query\\Scope' => $vendorDir . '/icewind/searchdav/src/Query/Scope.php', + 'SearchDAV\\XML\\BasicSearch' => $vendorDir . '/icewind/searchdav/src/XML/BasicSearch.php', + 'SearchDAV\\XML\\BasicSearchSchema' => $vendorDir . '/icewind/searchdav/src/XML/BasicSearchSchema.php', + 'SearchDAV\\XML\\Limit' => $vendorDir . '/icewind/searchdav/src/XML/Limit.php', + 'SearchDAV\\XML\\Literal' => $vendorDir . '/icewind/searchdav/src/XML/Literal.php', + 'SearchDAV\\XML\\Operator' => $vendorDir . '/icewind/searchdav/src/XML/Operator.php', + 'SearchDAV\\XML\\Order' => $vendorDir . '/icewind/searchdav/src/XML/Order.php', + 'SearchDAV\\XML\\PropDesc' => $vendorDir . '/icewind/searchdav/src/XML/PropDesc.php', + 'SearchDAV\\XML\\QueryDiscoverResponse' => $vendorDir . '/icewind/searchdav/src/XML/QueryDiscoverResponse.php', + 'SearchDAV\\XML\\Scope' => $vendorDir . '/icewind/searchdav/src/XML/Scope.php', + 'SearchDAV\\XML\\SupportedQueryGrammar' => $vendorDir . '/icewind/searchdav/src/XML/SupportedQueryGrammar.php', + 'SensitiveParameter' => $vendorDir . '/symfony/polyfill-php82/Resources/stubs/SensitiveParameter.php', + 'SensitiveParameterValue' => $vendorDir . '/symfony/polyfill-php82/Resources/stubs/SensitiveParameterValue.php', + 'SpomkyLabs\\Pki\\ASN1\\Component\\Identifier' => $vendorDir . '/spomky-labs/pki-framework/src/ASN1/Component/Identifier.php', + 'SpomkyLabs\\Pki\\ASN1\\Component\\Length' => $vendorDir . '/spomky-labs/pki-framework/src/ASN1/Component/Length.php', + 'SpomkyLabs\\Pki\\ASN1\\DERData' => $vendorDir . '/spomky-labs/pki-framework/src/ASN1/DERData.php', + 'SpomkyLabs\\Pki\\ASN1\\Element' => $vendorDir . '/spomky-labs/pki-framework/src/ASN1/Element.php', + 'SpomkyLabs\\Pki\\ASN1\\Exception\\DecodeException' => $vendorDir . '/spomky-labs/pki-framework/src/ASN1/Exception/DecodeException.php', + 'SpomkyLabs\\Pki\\ASN1\\Feature\\ElementBase' => $vendorDir . '/spomky-labs/pki-framework/src/ASN1/Feature/ElementBase.php', + 'SpomkyLabs\\Pki\\ASN1\\Feature\\Encodable' => $vendorDir . '/spomky-labs/pki-framework/src/ASN1/Feature/Encodable.php', + 'SpomkyLabs\\Pki\\ASN1\\Feature\\Stringable' => $vendorDir . '/spomky-labs/pki-framework/src/ASN1/Feature/Stringable.php', + 'SpomkyLabs\\Pki\\ASN1\\Type\\BaseString' => $vendorDir . '/spomky-labs/pki-framework/src/ASN1/Type/BaseString.php', + 'SpomkyLabs\\Pki\\ASN1\\Type\\BaseTime' => $vendorDir . '/spomky-labs/pki-framework/src/ASN1/Type/BaseTime.php', + 'SpomkyLabs\\Pki\\ASN1\\Type\\Constructed\\ConstructedString' => $vendorDir . '/spomky-labs/pki-framework/src/ASN1/Type/Constructed/ConstructedString.php', + 'SpomkyLabs\\Pki\\ASN1\\Type\\Constructed\\Sequence' => $vendorDir . '/spomky-labs/pki-framework/src/ASN1/Type/Constructed/Sequence.php', + 'SpomkyLabs\\Pki\\ASN1\\Type\\Constructed\\Set' => $vendorDir . '/spomky-labs/pki-framework/src/ASN1/Type/Constructed/Set.php', + 'SpomkyLabs\\Pki\\ASN1\\Type\\PrimitiveString' => $vendorDir . '/spomky-labs/pki-framework/src/ASN1/Type/PrimitiveString.php', + 'SpomkyLabs\\Pki\\ASN1\\Type\\PrimitiveType' => $vendorDir . '/spomky-labs/pki-framework/src/ASN1/Type/PrimitiveType.php', + 'SpomkyLabs\\Pki\\ASN1\\Type\\Primitive\\BMPString' => $vendorDir . '/spomky-labs/pki-framework/src/ASN1/Type/Primitive/BMPString.php', + 'SpomkyLabs\\Pki\\ASN1\\Type\\Primitive\\BitString' => $vendorDir . '/spomky-labs/pki-framework/src/ASN1/Type/Primitive/BitString.php', + 'SpomkyLabs\\Pki\\ASN1\\Type\\Primitive\\Boolean' => $vendorDir . '/spomky-labs/pki-framework/src/ASN1/Type/Primitive/Boolean.php', + 'SpomkyLabs\\Pki\\ASN1\\Type\\Primitive\\CharacterString' => $vendorDir . '/spomky-labs/pki-framework/src/ASN1/Type/Primitive/CharacterString.php', + 'SpomkyLabs\\Pki\\ASN1\\Type\\Primitive\\EOC' => $vendorDir . '/spomky-labs/pki-framework/src/ASN1/Type/Primitive/EOC.php', + 'SpomkyLabs\\Pki\\ASN1\\Type\\Primitive\\Enumerated' => $vendorDir . '/spomky-labs/pki-framework/src/ASN1/Type/Primitive/Enumerated.php', + 'SpomkyLabs\\Pki\\ASN1\\Type\\Primitive\\GeneralString' => $vendorDir . '/spomky-labs/pki-framework/src/ASN1/Type/Primitive/GeneralString.php', + 'SpomkyLabs\\Pki\\ASN1\\Type\\Primitive\\GeneralizedTime' => $vendorDir . '/spomky-labs/pki-framework/src/ASN1/Type/Primitive/GeneralizedTime.php', + 'SpomkyLabs\\Pki\\ASN1\\Type\\Primitive\\GraphicString' => $vendorDir . '/spomky-labs/pki-framework/src/ASN1/Type/Primitive/GraphicString.php', + 'SpomkyLabs\\Pki\\ASN1\\Type\\Primitive\\IA5String' => $vendorDir . '/spomky-labs/pki-framework/src/ASN1/Type/Primitive/IA5String.php', + 'SpomkyLabs\\Pki\\ASN1\\Type\\Primitive\\Integer' => $vendorDir . '/spomky-labs/pki-framework/src/ASN1/Type/Primitive/Integer.php', + 'SpomkyLabs\\Pki\\ASN1\\Type\\Primitive\\NullType' => $vendorDir . '/spomky-labs/pki-framework/src/ASN1/Type/Primitive/NullType.php', + 'SpomkyLabs\\Pki\\ASN1\\Type\\Primitive\\Number' => $vendorDir . '/spomky-labs/pki-framework/src/ASN1/Type/Primitive/Number.php', + 'SpomkyLabs\\Pki\\ASN1\\Type\\Primitive\\NumericString' => $vendorDir . '/spomky-labs/pki-framework/src/ASN1/Type/Primitive/NumericString.php', + 'SpomkyLabs\\Pki\\ASN1\\Type\\Primitive\\ObjectDescriptor' => $vendorDir . '/spomky-labs/pki-framework/src/ASN1/Type/Primitive/ObjectDescriptor.php', + 'SpomkyLabs\\Pki\\ASN1\\Type\\Primitive\\ObjectIdentifier' => $vendorDir . '/spomky-labs/pki-framework/src/ASN1/Type/Primitive/ObjectIdentifier.php', + 'SpomkyLabs\\Pki\\ASN1\\Type\\Primitive\\OctetString' => $vendorDir . '/spomky-labs/pki-framework/src/ASN1/Type/Primitive/OctetString.php', + 'SpomkyLabs\\Pki\\ASN1\\Type\\Primitive\\PrintableString' => $vendorDir . '/spomky-labs/pki-framework/src/ASN1/Type/Primitive/PrintableString.php', + 'SpomkyLabs\\Pki\\ASN1\\Type\\Primitive\\Real' => $vendorDir . '/spomky-labs/pki-framework/src/ASN1/Type/Primitive/Real.php', + 'SpomkyLabs\\Pki\\ASN1\\Type\\Primitive\\RelativeOID' => $vendorDir . '/spomky-labs/pki-framework/src/ASN1/Type/Primitive/RelativeOID.php', + 'SpomkyLabs\\Pki\\ASN1\\Type\\Primitive\\T61String' => $vendorDir . '/spomky-labs/pki-framework/src/ASN1/Type/Primitive/T61String.php', + 'SpomkyLabs\\Pki\\ASN1\\Type\\Primitive\\UTCTime' => $vendorDir . '/spomky-labs/pki-framework/src/ASN1/Type/Primitive/UTCTime.php', + 'SpomkyLabs\\Pki\\ASN1\\Type\\Primitive\\UTF8String' => $vendorDir . '/spomky-labs/pki-framework/src/ASN1/Type/Primitive/UTF8String.php', + 'SpomkyLabs\\Pki\\ASN1\\Type\\Primitive\\UniversalString' => $vendorDir . '/spomky-labs/pki-framework/src/ASN1/Type/Primitive/UniversalString.php', + 'SpomkyLabs\\Pki\\ASN1\\Type\\Primitive\\VideotexString' => $vendorDir . '/spomky-labs/pki-framework/src/ASN1/Type/Primitive/VideotexString.php', + 'SpomkyLabs\\Pki\\ASN1\\Type\\Primitive\\VisibleString' => $vendorDir . '/spomky-labs/pki-framework/src/ASN1/Type/Primitive/VisibleString.php', + 'SpomkyLabs\\Pki\\ASN1\\Type\\StringType' => $vendorDir . '/spomky-labs/pki-framework/src/ASN1/Type/StringType.php', + 'SpomkyLabs\\Pki\\ASN1\\Type\\Structure' => $vendorDir . '/spomky-labs/pki-framework/src/ASN1/Type/Structure.php', + 'SpomkyLabs\\Pki\\ASN1\\Type\\TaggedType' => $vendorDir . '/spomky-labs/pki-framework/src/ASN1/Type/TaggedType.php', + 'SpomkyLabs\\Pki\\ASN1\\Type\\Tagged\\ApplicationType' => $vendorDir . '/spomky-labs/pki-framework/src/ASN1/Type/Tagged/ApplicationType.php', + 'SpomkyLabs\\Pki\\ASN1\\Type\\Tagged\\ContextSpecificType' => $vendorDir . '/spomky-labs/pki-framework/src/ASN1/Type/Tagged/ContextSpecificType.php', + 'SpomkyLabs\\Pki\\ASN1\\Type\\Tagged\\DERTaggedType' => $vendorDir . '/spomky-labs/pki-framework/src/ASN1/Type/Tagged/DERTaggedType.php', + 'SpomkyLabs\\Pki\\ASN1\\Type\\Tagged\\ExplicitTagging' => $vendorDir . '/spomky-labs/pki-framework/src/ASN1/Type/Tagged/ExplicitTagging.php', + 'SpomkyLabs\\Pki\\ASN1\\Type\\Tagged\\ExplicitlyTaggedType' => $vendorDir . '/spomky-labs/pki-framework/src/ASN1/Type/Tagged/ExplicitlyTaggedType.php', + 'SpomkyLabs\\Pki\\ASN1\\Type\\Tagged\\ImplicitTagging' => $vendorDir . '/spomky-labs/pki-framework/src/ASN1/Type/Tagged/ImplicitTagging.php', + 'SpomkyLabs\\Pki\\ASN1\\Type\\Tagged\\ImplicitlyTaggedType' => $vendorDir . '/spomky-labs/pki-framework/src/ASN1/Type/Tagged/ImplicitlyTaggedType.php', + 'SpomkyLabs\\Pki\\ASN1\\Type\\Tagged\\PrivateType' => $vendorDir . '/spomky-labs/pki-framework/src/ASN1/Type/Tagged/PrivateType.php', + 'SpomkyLabs\\Pki\\ASN1\\Type\\Tagged\\TaggedTypeWrap' => $vendorDir . '/spomky-labs/pki-framework/src/ASN1/Type/Tagged/TaggedTypeWrap.php', + 'SpomkyLabs\\Pki\\ASN1\\Type\\TimeType' => $vendorDir . '/spomky-labs/pki-framework/src/ASN1/Type/TimeType.php', + 'SpomkyLabs\\Pki\\ASN1\\Type\\UniversalClass' => $vendorDir . '/spomky-labs/pki-framework/src/ASN1/Type/UniversalClass.php', + 'SpomkyLabs\\Pki\\ASN1\\Type\\UnspecifiedType' => $vendorDir . '/spomky-labs/pki-framework/src/ASN1/Type/UnspecifiedType.php', + 'SpomkyLabs\\Pki\\ASN1\\Util\\BigInt' => $vendorDir . '/spomky-labs/pki-framework/src/ASN1/Util/BigInt.php', + 'SpomkyLabs\\Pki\\ASN1\\Util\\Flags' => $vendorDir . '/spomky-labs/pki-framework/src/ASN1/Util/Flags.php', + 'SpomkyLabs\\Pki\\CryptoBridge\\Crypto' => $vendorDir . '/spomky-labs/pki-framework/src/CryptoBridge/Crypto.php', + 'SpomkyLabs\\Pki\\CryptoBridge\\Crypto\\OpenSSLCrypto' => $vendorDir . '/spomky-labs/pki-framework/src/CryptoBridge/Crypto/OpenSSLCrypto.php', + 'SpomkyLabs\\Pki\\CryptoEncoding\\PEM' => $vendorDir . '/spomky-labs/pki-framework/src/CryptoEncoding/PEM.php', + 'SpomkyLabs\\Pki\\CryptoEncoding\\PEMBundle' => $vendorDir . '/spomky-labs/pki-framework/src/CryptoEncoding/PEMBundle.php', + 'SpomkyLabs\\Pki\\CryptoTypes\\AlgorithmIdentifier\\AlgorithmIdentifier' => $vendorDir . '/spomky-labs/pki-framework/src/CryptoTypes/AlgorithmIdentifier/AlgorithmIdentifier.php', + 'SpomkyLabs\\Pki\\CryptoTypes\\AlgorithmIdentifier\\AlgorithmIdentifierFactory' => $vendorDir . '/spomky-labs/pki-framework/src/CryptoTypes/AlgorithmIdentifier/AlgorithmIdentifierFactory.php', + 'SpomkyLabs\\Pki\\CryptoTypes\\AlgorithmIdentifier\\AlgorithmIdentifierProvider' => $vendorDir . '/spomky-labs/pki-framework/src/CryptoTypes/AlgorithmIdentifier/AlgorithmIdentifierProvider.php', + 'SpomkyLabs\\Pki\\CryptoTypes\\AlgorithmIdentifier\\Asymmetric\\ECPublicKeyAlgorithmIdentifier' => $vendorDir . '/spomky-labs/pki-framework/src/CryptoTypes/AlgorithmIdentifier/Asymmetric/ECPublicKeyAlgorithmIdentifier.php', + 'SpomkyLabs\\Pki\\CryptoTypes\\AlgorithmIdentifier\\Asymmetric\\Ed25519AlgorithmIdentifier' => $vendorDir . '/spomky-labs/pki-framework/src/CryptoTypes/AlgorithmIdentifier/Asymmetric/Ed25519AlgorithmIdentifier.php', + 'SpomkyLabs\\Pki\\CryptoTypes\\AlgorithmIdentifier\\Asymmetric\\Ed448AlgorithmIdentifier' => $vendorDir . '/spomky-labs/pki-framework/src/CryptoTypes/AlgorithmIdentifier/Asymmetric/Ed448AlgorithmIdentifier.php', + 'SpomkyLabs\\Pki\\CryptoTypes\\AlgorithmIdentifier\\Asymmetric\\RFC8410EdAlgorithmIdentifier' => $vendorDir . '/spomky-labs/pki-framework/src/CryptoTypes/AlgorithmIdentifier/Asymmetric/RFC8410EdAlgorithmIdentifier.php', + 'SpomkyLabs\\Pki\\CryptoTypes\\AlgorithmIdentifier\\Asymmetric\\RFC8410XAlgorithmIdentifier' => $vendorDir . '/spomky-labs/pki-framework/src/CryptoTypes/AlgorithmIdentifier/Asymmetric/RFC8410XAlgorithmIdentifier.php', + 'SpomkyLabs\\Pki\\CryptoTypes\\AlgorithmIdentifier\\Asymmetric\\RSAEncryptionAlgorithmIdentifier' => $vendorDir . '/spomky-labs/pki-framework/src/CryptoTypes/AlgorithmIdentifier/Asymmetric/RSAEncryptionAlgorithmIdentifier.php', + 'SpomkyLabs\\Pki\\CryptoTypes\\AlgorithmIdentifier\\Asymmetric\\RSAPSSSSAEncryptionAlgorithmIdentifier' => $vendorDir . '/spomky-labs/pki-framework/src/CryptoTypes/AlgorithmIdentifier/Asymmetric/RSAPSSSSAEncryptionAlgorithmIdentifier.php', + 'SpomkyLabs\\Pki\\CryptoTypes\\AlgorithmIdentifier\\Asymmetric\\X25519AlgorithmIdentifier' => $vendorDir . '/spomky-labs/pki-framework/src/CryptoTypes/AlgorithmIdentifier/Asymmetric/X25519AlgorithmIdentifier.php', + 'SpomkyLabs\\Pki\\CryptoTypes\\AlgorithmIdentifier\\Asymmetric\\X448AlgorithmIdentifier' => $vendorDir . '/spomky-labs/pki-framework/src/CryptoTypes/AlgorithmIdentifier/Asymmetric/X448AlgorithmIdentifier.php', + 'SpomkyLabs\\Pki\\CryptoTypes\\AlgorithmIdentifier\\Cipher\\AES128CBCAlgorithmIdentifier' => $vendorDir . '/spomky-labs/pki-framework/src/CryptoTypes/AlgorithmIdentifier/Cipher/AES128CBCAlgorithmIdentifier.php', + 'SpomkyLabs\\Pki\\CryptoTypes\\AlgorithmIdentifier\\Cipher\\AES192CBCAlgorithmIdentifier' => $vendorDir . '/spomky-labs/pki-framework/src/CryptoTypes/AlgorithmIdentifier/Cipher/AES192CBCAlgorithmIdentifier.php', + 'SpomkyLabs\\Pki\\CryptoTypes\\AlgorithmIdentifier\\Cipher\\AES256CBCAlgorithmIdentifier' => $vendorDir . '/spomky-labs/pki-framework/src/CryptoTypes/AlgorithmIdentifier/Cipher/AES256CBCAlgorithmIdentifier.php', + 'SpomkyLabs\\Pki\\CryptoTypes\\AlgorithmIdentifier\\Cipher\\AESCBCAlgorithmIdentifier' => $vendorDir . '/spomky-labs/pki-framework/src/CryptoTypes/AlgorithmIdentifier/Cipher/AESCBCAlgorithmIdentifier.php', + 'SpomkyLabs\\Pki\\CryptoTypes\\AlgorithmIdentifier\\Cipher\\BlockCipherAlgorithmIdentifier' => $vendorDir . '/spomky-labs/pki-framework/src/CryptoTypes/AlgorithmIdentifier/Cipher/BlockCipherAlgorithmIdentifier.php', + 'SpomkyLabs\\Pki\\CryptoTypes\\AlgorithmIdentifier\\Cipher\\CipherAlgorithmIdentifier' => $vendorDir . '/spomky-labs/pki-framework/src/CryptoTypes/AlgorithmIdentifier/Cipher/CipherAlgorithmIdentifier.php', + 'SpomkyLabs\\Pki\\CryptoTypes\\AlgorithmIdentifier\\Cipher\\DESCBCAlgorithmIdentifier' => $vendorDir . '/spomky-labs/pki-framework/src/CryptoTypes/AlgorithmIdentifier/Cipher/DESCBCAlgorithmIdentifier.php', + 'SpomkyLabs\\Pki\\CryptoTypes\\AlgorithmIdentifier\\Cipher\\DESEDE3CBCAlgorithmIdentifier' => $vendorDir . '/spomky-labs/pki-framework/src/CryptoTypes/AlgorithmIdentifier/Cipher/DESEDE3CBCAlgorithmIdentifier.php', + 'SpomkyLabs\\Pki\\CryptoTypes\\AlgorithmIdentifier\\Cipher\\RC2CBCAlgorithmIdentifier' => $vendorDir . '/spomky-labs/pki-framework/src/CryptoTypes/AlgorithmIdentifier/Cipher/RC2CBCAlgorithmIdentifier.php', + 'SpomkyLabs\\Pki\\CryptoTypes\\AlgorithmIdentifier\\Feature\\AlgorithmIdentifierType' => $vendorDir . '/spomky-labs/pki-framework/src/CryptoTypes/AlgorithmIdentifier/Feature/AlgorithmIdentifierType.php', + 'SpomkyLabs\\Pki\\CryptoTypes\\AlgorithmIdentifier\\Feature\\AsymmetricCryptoAlgorithmIdentifier' => $vendorDir . '/spomky-labs/pki-framework/src/CryptoTypes/AlgorithmIdentifier/Feature/AsymmetricCryptoAlgorithmIdentifier.php', + 'SpomkyLabs\\Pki\\CryptoTypes\\AlgorithmIdentifier\\Feature\\EncryptionAlgorithmIdentifier' => $vendorDir . '/spomky-labs/pki-framework/src/CryptoTypes/AlgorithmIdentifier/Feature/EncryptionAlgorithmIdentifier.php', + 'SpomkyLabs\\Pki\\CryptoTypes\\AlgorithmIdentifier\\Feature\\HashAlgorithmIdentifier' => $vendorDir . '/spomky-labs/pki-framework/src/CryptoTypes/AlgorithmIdentifier/Feature/HashAlgorithmIdentifier.php', + 'SpomkyLabs\\Pki\\CryptoTypes\\AlgorithmIdentifier\\Feature\\PRFAlgorithmIdentifier' => $vendorDir . '/spomky-labs/pki-framework/src/CryptoTypes/AlgorithmIdentifier/Feature/PRFAlgorithmIdentifier.php', + 'SpomkyLabs\\Pki\\CryptoTypes\\AlgorithmIdentifier\\Feature\\SignatureAlgorithmIdentifier' => $vendorDir . '/spomky-labs/pki-framework/src/CryptoTypes/AlgorithmIdentifier/Feature/SignatureAlgorithmIdentifier.php', + 'SpomkyLabs\\Pki\\CryptoTypes\\AlgorithmIdentifier\\GenericAlgorithmIdentifier' => $vendorDir . '/spomky-labs/pki-framework/src/CryptoTypes/AlgorithmIdentifier/GenericAlgorithmIdentifier.php', + 'SpomkyLabs\\Pki\\CryptoTypes\\AlgorithmIdentifier\\Hash\\HMACWithSHA1AlgorithmIdentifier' => $vendorDir . '/spomky-labs/pki-framework/src/CryptoTypes/AlgorithmIdentifier/Hash/HMACWithSHA1AlgorithmIdentifier.php', + 'SpomkyLabs\\Pki\\CryptoTypes\\AlgorithmIdentifier\\Hash\\HMACWithSHA224AlgorithmIdentifier' => $vendorDir . '/spomky-labs/pki-framework/src/CryptoTypes/AlgorithmIdentifier/Hash/HMACWithSHA224AlgorithmIdentifier.php', + 'SpomkyLabs\\Pki\\CryptoTypes\\AlgorithmIdentifier\\Hash\\HMACWithSHA256AlgorithmIdentifier' => $vendorDir . '/spomky-labs/pki-framework/src/CryptoTypes/AlgorithmIdentifier/Hash/HMACWithSHA256AlgorithmIdentifier.php', + 'SpomkyLabs\\Pki\\CryptoTypes\\AlgorithmIdentifier\\Hash\\HMACWithSHA384AlgorithmIdentifier' => $vendorDir . '/spomky-labs/pki-framework/src/CryptoTypes/AlgorithmIdentifier/Hash/HMACWithSHA384AlgorithmIdentifier.php', + 'SpomkyLabs\\Pki\\CryptoTypes\\AlgorithmIdentifier\\Hash\\HMACWithSHA512AlgorithmIdentifier' => $vendorDir . '/spomky-labs/pki-framework/src/CryptoTypes/AlgorithmIdentifier/Hash/HMACWithSHA512AlgorithmIdentifier.php', + 'SpomkyLabs\\Pki\\CryptoTypes\\AlgorithmIdentifier\\Hash\\MD5AlgorithmIdentifier' => $vendorDir . '/spomky-labs/pki-framework/src/CryptoTypes/AlgorithmIdentifier/Hash/MD5AlgorithmIdentifier.php', + 'SpomkyLabs\\Pki\\CryptoTypes\\AlgorithmIdentifier\\Hash\\RFC4231HMACAlgorithmIdentifier' => $vendorDir . '/spomky-labs/pki-framework/src/CryptoTypes/AlgorithmIdentifier/Hash/RFC4231HMACAlgorithmIdentifier.php', + 'SpomkyLabs\\Pki\\CryptoTypes\\AlgorithmIdentifier\\Hash\\SHA1AlgorithmIdentifier' => $vendorDir . '/spomky-labs/pki-framework/src/CryptoTypes/AlgorithmIdentifier/Hash/SHA1AlgorithmIdentifier.php', + 'SpomkyLabs\\Pki\\CryptoTypes\\AlgorithmIdentifier\\Hash\\SHA224AlgorithmIdentifier' => $vendorDir . '/spomky-labs/pki-framework/src/CryptoTypes/AlgorithmIdentifier/Hash/SHA224AlgorithmIdentifier.php', + 'SpomkyLabs\\Pki\\CryptoTypes\\AlgorithmIdentifier\\Hash\\SHA256AlgorithmIdentifier' => $vendorDir . '/spomky-labs/pki-framework/src/CryptoTypes/AlgorithmIdentifier/Hash/SHA256AlgorithmIdentifier.php', + 'SpomkyLabs\\Pki\\CryptoTypes\\AlgorithmIdentifier\\Hash\\SHA2AlgorithmIdentifier' => $vendorDir . '/spomky-labs/pki-framework/src/CryptoTypes/AlgorithmIdentifier/Hash/SHA2AlgorithmIdentifier.php', + 'SpomkyLabs\\Pki\\CryptoTypes\\AlgorithmIdentifier\\Hash\\SHA384AlgorithmIdentifier' => $vendorDir . '/spomky-labs/pki-framework/src/CryptoTypes/AlgorithmIdentifier/Hash/SHA384AlgorithmIdentifier.php', + 'SpomkyLabs\\Pki\\CryptoTypes\\AlgorithmIdentifier\\Hash\\SHA512AlgorithmIdentifier' => $vendorDir . '/spomky-labs/pki-framework/src/CryptoTypes/AlgorithmIdentifier/Hash/SHA512AlgorithmIdentifier.php', + 'SpomkyLabs\\Pki\\CryptoTypes\\AlgorithmIdentifier\\Signature\\ECDSAWithSHA1AlgorithmIdentifier' => $vendorDir . '/spomky-labs/pki-framework/src/CryptoTypes/AlgorithmIdentifier/Signature/ECDSAWithSHA1AlgorithmIdentifier.php', + 'SpomkyLabs\\Pki\\CryptoTypes\\AlgorithmIdentifier\\Signature\\ECDSAWithSHA224AlgorithmIdentifier' => $vendorDir . '/spomky-labs/pki-framework/src/CryptoTypes/AlgorithmIdentifier/Signature/ECDSAWithSHA224AlgorithmIdentifier.php', + 'SpomkyLabs\\Pki\\CryptoTypes\\AlgorithmIdentifier\\Signature\\ECDSAWithSHA256AlgorithmIdentifier' => $vendorDir . '/spomky-labs/pki-framework/src/CryptoTypes/AlgorithmIdentifier/Signature/ECDSAWithSHA256AlgorithmIdentifier.php', + 'SpomkyLabs\\Pki\\CryptoTypes\\AlgorithmIdentifier\\Signature\\ECDSAWithSHA384AlgorithmIdentifier' => $vendorDir . '/spomky-labs/pki-framework/src/CryptoTypes/AlgorithmIdentifier/Signature/ECDSAWithSHA384AlgorithmIdentifier.php', + 'SpomkyLabs\\Pki\\CryptoTypes\\AlgorithmIdentifier\\Signature\\ECDSAWithSHA512AlgorithmIdentifier' => $vendorDir . '/spomky-labs/pki-framework/src/CryptoTypes/AlgorithmIdentifier/Signature/ECDSAWithSHA512AlgorithmIdentifier.php', + 'SpomkyLabs\\Pki\\CryptoTypes\\AlgorithmIdentifier\\Signature\\ECSignatureAlgorithmIdentifier' => $vendorDir . '/spomky-labs/pki-framework/src/CryptoTypes/AlgorithmIdentifier/Signature/ECSignatureAlgorithmIdentifier.php', + 'SpomkyLabs\\Pki\\CryptoTypes\\AlgorithmIdentifier\\Signature\\MD2WithRSAEncryptionAlgorithmIdentifier' => $vendorDir . '/spomky-labs/pki-framework/src/CryptoTypes/AlgorithmIdentifier/Signature/MD2WithRSAEncryptionAlgorithmIdentifier.php', + 'SpomkyLabs\\Pki\\CryptoTypes\\AlgorithmIdentifier\\Signature\\MD4WithRSAEncryptionAlgorithmIdentifier' => $vendorDir . '/spomky-labs/pki-framework/src/CryptoTypes/AlgorithmIdentifier/Signature/MD4WithRSAEncryptionAlgorithmIdentifier.php', + 'SpomkyLabs\\Pki\\CryptoTypes\\AlgorithmIdentifier\\Signature\\MD5WithRSAEncryptionAlgorithmIdentifier' => $vendorDir . '/spomky-labs/pki-framework/src/CryptoTypes/AlgorithmIdentifier/Signature/MD5WithRSAEncryptionAlgorithmIdentifier.php', + 'SpomkyLabs\\Pki\\CryptoTypes\\AlgorithmIdentifier\\Signature\\RFC3279RSASignatureAlgorithmIdentifier' => $vendorDir . '/spomky-labs/pki-framework/src/CryptoTypes/AlgorithmIdentifier/Signature/RFC3279RSASignatureAlgorithmIdentifier.php', + 'SpomkyLabs\\Pki\\CryptoTypes\\AlgorithmIdentifier\\Signature\\RFC4055RSASignatureAlgorithmIdentifier' => $vendorDir . '/spomky-labs/pki-framework/src/CryptoTypes/AlgorithmIdentifier/Signature/RFC4055RSASignatureAlgorithmIdentifier.php', + 'SpomkyLabs\\Pki\\CryptoTypes\\AlgorithmIdentifier\\Signature\\RSASignatureAlgorithmIdentifier' => $vendorDir . '/spomky-labs/pki-framework/src/CryptoTypes/AlgorithmIdentifier/Signature/RSASignatureAlgorithmIdentifier.php', + 'SpomkyLabs\\Pki\\CryptoTypes\\AlgorithmIdentifier\\Signature\\SHA1WithRSAEncryptionAlgorithmIdentifier' => $vendorDir . '/spomky-labs/pki-framework/src/CryptoTypes/AlgorithmIdentifier/Signature/SHA1WithRSAEncryptionAlgorithmIdentifier.php', + 'SpomkyLabs\\Pki\\CryptoTypes\\AlgorithmIdentifier\\Signature\\SHA224WithRSAEncryptionAlgorithmIdentifier' => $vendorDir . '/spomky-labs/pki-framework/src/CryptoTypes/AlgorithmIdentifier/Signature/SHA224WithRSAEncryptionAlgorithmIdentifier.php', + 'SpomkyLabs\\Pki\\CryptoTypes\\AlgorithmIdentifier\\Signature\\SHA256WithRSAEncryptionAlgorithmIdentifier' => $vendorDir . '/spomky-labs/pki-framework/src/CryptoTypes/AlgorithmIdentifier/Signature/SHA256WithRSAEncryptionAlgorithmIdentifier.php', + 'SpomkyLabs\\Pki\\CryptoTypes\\AlgorithmIdentifier\\Signature\\SHA384WithRSAEncryptionAlgorithmIdentifier' => $vendorDir . '/spomky-labs/pki-framework/src/CryptoTypes/AlgorithmIdentifier/Signature/SHA384WithRSAEncryptionAlgorithmIdentifier.php', + 'SpomkyLabs\\Pki\\CryptoTypes\\AlgorithmIdentifier\\Signature\\SHA512WithRSAEncryptionAlgorithmIdentifier' => $vendorDir . '/spomky-labs/pki-framework/src/CryptoTypes/AlgorithmIdentifier/Signature/SHA512WithRSAEncryptionAlgorithmIdentifier.php', + 'SpomkyLabs\\Pki\\CryptoTypes\\AlgorithmIdentifier\\SpecificAlgorithmIdentifier' => $vendorDir . '/spomky-labs/pki-framework/src/CryptoTypes/AlgorithmIdentifier/SpecificAlgorithmIdentifier.php', + 'SpomkyLabs\\Pki\\CryptoTypes\\Asymmetric\\Attribute\\OneAsymmetricKeyAttributes' => $vendorDir . '/spomky-labs/pki-framework/src/CryptoTypes/Asymmetric/Attribute/OneAsymmetricKeyAttributes.php', + 'SpomkyLabs\\Pki\\CryptoTypes\\Asymmetric\\EC\\ECConversion' => $vendorDir . '/spomky-labs/pki-framework/src/CryptoTypes/Asymmetric/EC/ECConversion.php', + 'SpomkyLabs\\Pki\\CryptoTypes\\Asymmetric\\EC\\ECPrivateKey' => $vendorDir . '/spomky-labs/pki-framework/src/CryptoTypes/Asymmetric/EC/ECPrivateKey.php', + 'SpomkyLabs\\Pki\\CryptoTypes\\Asymmetric\\EC\\ECPublicKey' => $vendorDir . '/spomky-labs/pki-framework/src/CryptoTypes/Asymmetric/EC/ECPublicKey.php', + 'SpomkyLabs\\Pki\\CryptoTypes\\Asymmetric\\OneAsymmetricKey' => $vendorDir . '/spomky-labs/pki-framework/src/CryptoTypes/Asymmetric/OneAsymmetricKey.php', + 'SpomkyLabs\\Pki\\CryptoTypes\\Asymmetric\\PrivateKey' => $vendorDir . '/spomky-labs/pki-framework/src/CryptoTypes/Asymmetric/PrivateKey.php', + 'SpomkyLabs\\Pki\\CryptoTypes\\Asymmetric\\PrivateKeyInfo' => $vendorDir . '/spomky-labs/pki-framework/src/CryptoTypes/Asymmetric/PrivateKeyInfo.php', + 'SpomkyLabs\\Pki\\CryptoTypes\\Asymmetric\\PublicKey' => $vendorDir . '/spomky-labs/pki-framework/src/CryptoTypes/Asymmetric/PublicKey.php', + 'SpomkyLabs\\Pki\\CryptoTypes\\Asymmetric\\PublicKeyInfo' => $vendorDir . '/spomky-labs/pki-framework/src/CryptoTypes/Asymmetric/PublicKeyInfo.php', + 'SpomkyLabs\\Pki\\CryptoTypes\\Asymmetric\\RFC8410\\Curve25519\\Curve25519PrivateKey' => $vendorDir . '/spomky-labs/pki-framework/src/CryptoTypes/Asymmetric/RFC8410/Curve25519/Curve25519PrivateKey.php', + 'SpomkyLabs\\Pki\\CryptoTypes\\Asymmetric\\RFC8410\\Curve25519\\Curve25519PublicKey' => $vendorDir . '/spomky-labs/pki-framework/src/CryptoTypes/Asymmetric/RFC8410/Curve25519/Curve25519PublicKey.php', + 'SpomkyLabs\\Pki\\CryptoTypes\\Asymmetric\\RFC8410\\Curve25519\\Ed25519PrivateKey' => $vendorDir . '/spomky-labs/pki-framework/src/CryptoTypes/Asymmetric/RFC8410/Curve25519/Ed25519PrivateKey.php', + 'SpomkyLabs\\Pki\\CryptoTypes\\Asymmetric\\RFC8410\\Curve25519\\Ed25519PublicKey' => $vendorDir . '/spomky-labs/pki-framework/src/CryptoTypes/Asymmetric/RFC8410/Curve25519/Ed25519PublicKey.php', + 'SpomkyLabs\\Pki\\CryptoTypes\\Asymmetric\\RFC8410\\Curve25519\\X25519PrivateKey' => $vendorDir . '/spomky-labs/pki-framework/src/CryptoTypes/Asymmetric/RFC8410/Curve25519/X25519PrivateKey.php', + 'SpomkyLabs\\Pki\\CryptoTypes\\Asymmetric\\RFC8410\\Curve25519\\X25519PublicKey' => $vendorDir . '/spomky-labs/pki-framework/src/CryptoTypes/Asymmetric/RFC8410/Curve25519/X25519PublicKey.php', + 'SpomkyLabs\\Pki\\CryptoTypes\\Asymmetric\\RFC8410\\Curve448\\Ed448PrivateKey' => $vendorDir . '/spomky-labs/pki-framework/src/CryptoTypes/Asymmetric/RFC8410/Curve448/Ed448PrivateKey.php', + 'SpomkyLabs\\Pki\\CryptoTypes\\Asymmetric\\RFC8410\\Curve448\\Ed448PublicKey' => $vendorDir . '/spomky-labs/pki-framework/src/CryptoTypes/Asymmetric/RFC8410/Curve448/Ed448PublicKey.php', + 'SpomkyLabs\\Pki\\CryptoTypes\\Asymmetric\\RFC8410\\Curve448\\X448PrivateKey' => $vendorDir . '/spomky-labs/pki-framework/src/CryptoTypes/Asymmetric/RFC8410/Curve448/X448PrivateKey.php', + 'SpomkyLabs\\Pki\\CryptoTypes\\Asymmetric\\RFC8410\\Curve448\\X448PublicKey' => $vendorDir . '/spomky-labs/pki-framework/src/CryptoTypes/Asymmetric/RFC8410/Curve448/X448PublicKey.php', + 'SpomkyLabs\\Pki\\CryptoTypes\\Asymmetric\\RFC8410\\RFC8410PrivateKey' => $vendorDir . '/spomky-labs/pki-framework/src/CryptoTypes/Asymmetric/RFC8410/RFC8410PrivateKey.php', + 'SpomkyLabs\\Pki\\CryptoTypes\\Asymmetric\\RFC8410\\RFC8410PublicKey' => $vendorDir . '/spomky-labs/pki-framework/src/CryptoTypes/Asymmetric/RFC8410/RFC8410PublicKey.php', + 'SpomkyLabs\\Pki\\CryptoTypes\\Asymmetric\\RSA\\RSAPrivateKey' => $vendorDir . '/spomky-labs/pki-framework/src/CryptoTypes/Asymmetric/RSA/RSAPrivateKey.php', + 'SpomkyLabs\\Pki\\CryptoTypes\\Asymmetric\\RSA\\RSAPublicKey' => $vendorDir . '/spomky-labs/pki-framework/src/CryptoTypes/Asymmetric/RSA/RSAPublicKey.php', + 'SpomkyLabs\\Pki\\CryptoTypes\\Asymmetric\\RSA\\RSASSAPSSPrivateKey' => $vendorDir . '/spomky-labs/pki-framework/src/CryptoTypes/Asymmetric/RSA/RSASSAPSSPrivateKey.php', + 'SpomkyLabs\\Pki\\CryptoTypes\\Signature\\ECSignature' => $vendorDir . '/spomky-labs/pki-framework/src/CryptoTypes/Signature/ECSignature.php', + 'SpomkyLabs\\Pki\\CryptoTypes\\Signature\\Ed25519Signature' => $vendorDir . '/spomky-labs/pki-framework/src/CryptoTypes/Signature/Ed25519Signature.php', + 'SpomkyLabs\\Pki\\CryptoTypes\\Signature\\Ed448Signature' => $vendorDir . '/spomky-labs/pki-framework/src/CryptoTypes/Signature/Ed448Signature.php', + 'SpomkyLabs\\Pki\\CryptoTypes\\Signature\\GenericSignature' => $vendorDir . '/spomky-labs/pki-framework/src/CryptoTypes/Signature/GenericSignature.php', + 'SpomkyLabs\\Pki\\CryptoTypes\\Signature\\RSASignature' => $vendorDir . '/spomky-labs/pki-framework/src/CryptoTypes/Signature/RSASignature.php', + 'SpomkyLabs\\Pki\\CryptoTypes\\Signature\\Signature' => $vendorDir . '/spomky-labs/pki-framework/src/CryptoTypes/Signature/Signature.php', + 'SpomkyLabs\\Pki\\X501\\ASN1\\Attribute' => $vendorDir . '/spomky-labs/pki-framework/src/X501/ASN1/Attribute.php', + 'SpomkyLabs\\Pki\\X501\\ASN1\\AttributeType' => $vendorDir . '/spomky-labs/pki-framework/src/X501/ASN1/AttributeType.php', + 'SpomkyLabs\\Pki\\X501\\ASN1\\AttributeTypeAndValue' => $vendorDir . '/spomky-labs/pki-framework/src/X501/ASN1/AttributeTypeAndValue.php', + 'SpomkyLabs\\Pki\\X501\\ASN1\\AttributeValue\\AttributeValue' => $vendorDir . '/spomky-labs/pki-framework/src/X501/ASN1/AttributeValue/AttributeValue.php', + 'SpomkyLabs\\Pki\\X501\\ASN1\\AttributeValue\\CommonNameValue' => $vendorDir . '/spomky-labs/pki-framework/src/X501/ASN1/AttributeValue/CommonNameValue.php', + 'SpomkyLabs\\Pki\\X501\\ASN1\\AttributeValue\\CountryNameValue' => $vendorDir . '/spomky-labs/pki-framework/src/X501/ASN1/AttributeValue/CountryNameValue.php', + 'SpomkyLabs\\Pki\\X501\\ASN1\\AttributeValue\\DescriptionValue' => $vendorDir . '/spomky-labs/pki-framework/src/X501/ASN1/AttributeValue/DescriptionValue.php', + 'SpomkyLabs\\Pki\\X501\\ASN1\\AttributeValue\\Feature\\DirectoryString' => $vendorDir . '/spomky-labs/pki-framework/src/X501/ASN1/AttributeValue/Feature/DirectoryString.php', + 'SpomkyLabs\\Pki\\X501\\ASN1\\AttributeValue\\Feature\\PrintableStringValue' => $vendorDir . '/spomky-labs/pki-framework/src/X501/ASN1/AttributeValue/Feature/PrintableStringValue.php', + 'SpomkyLabs\\Pki\\X501\\ASN1\\AttributeValue\\GivenNameValue' => $vendorDir . '/spomky-labs/pki-framework/src/X501/ASN1/AttributeValue/GivenNameValue.php', + 'SpomkyLabs\\Pki\\X501\\ASN1\\AttributeValue\\LocalityNameValue' => $vendorDir . '/spomky-labs/pki-framework/src/X501/ASN1/AttributeValue/LocalityNameValue.php', + 'SpomkyLabs\\Pki\\X501\\ASN1\\AttributeValue\\NameValue' => $vendorDir . '/spomky-labs/pki-framework/src/X501/ASN1/AttributeValue/NameValue.php', + 'SpomkyLabs\\Pki\\X501\\ASN1\\AttributeValue\\OrganizationNameValue' => $vendorDir . '/spomky-labs/pki-framework/src/X501/ASN1/AttributeValue/OrganizationNameValue.php', + 'SpomkyLabs\\Pki\\X501\\ASN1\\AttributeValue\\OrganizationalUnitNameValue' => $vendorDir . '/spomky-labs/pki-framework/src/X501/ASN1/AttributeValue/OrganizationalUnitNameValue.php', + 'SpomkyLabs\\Pki\\X501\\ASN1\\AttributeValue\\PseudonymValue' => $vendorDir . '/spomky-labs/pki-framework/src/X501/ASN1/AttributeValue/PseudonymValue.php', + 'SpomkyLabs\\Pki\\X501\\ASN1\\AttributeValue\\SerialNumberValue' => $vendorDir . '/spomky-labs/pki-framework/src/X501/ASN1/AttributeValue/SerialNumberValue.php', + 'SpomkyLabs\\Pki\\X501\\ASN1\\AttributeValue\\StateOrProvinceNameValue' => $vendorDir . '/spomky-labs/pki-framework/src/X501/ASN1/AttributeValue/StateOrProvinceNameValue.php', + 'SpomkyLabs\\Pki\\X501\\ASN1\\AttributeValue\\SurnameValue' => $vendorDir . '/spomky-labs/pki-framework/src/X501/ASN1/AttributeValue/SurnameValue.php', + 'SpomkyLabs\\Pki\\X501\\ASN1\\AttributeValue\\TitleValue' => $vendorDir . '/spomky-labs/pki-framework/src/X501/ASN1/AttributeValue/TitleValue.php', + 'SpomkyLabs\\Pki\\X501\\ASN1\\AttributeValue\\UnknownAttributeValue' => $vendorDir . '/spomky-labs/pki-framework/src/X501/ASN1/AttributeValue/UnknownAttributeValue.php', + 'SpomkyLabs\\Pki\\X501\\ASN1\\Collection\\AttributeCollection' => $vendorDir . '/spomky-labs/pki-framework/src/X501/ASN1/Collection/AttributeCollection.php', + 'SpomkyLabs\\Pki\\X501\\ASN1\\Collection\\SequenceOfAttributes' => $vendorDir . '/spomky-labs/pki-framework/src/X501/ASN1/Collection/SequenceOfAttributes.php', + 'SpomkyLabs\\Pki\\X501\\ASN1\\Collection\\SetOfAttributes' => $vendorDir . '/spomky-labs/pki-framework/src/X501/ASN1/Collection/SetOfAttributes.php', + 'SpomkyLabs\\Pki\\X501\\ASN1\\Name' => $vendorDir . '/spomky-labs/pki-framework/src/X501/ASN1/Name.php', + 'SpomkyLabs\\Pki\\X501\\ASN1\\RDN' => $vendorDir . '/spomky-labs/pki-framework/src/X501/ASN1/RDN.php', + 'SpomkyLabs\\Pki\\X501\\DN\\DNParser' => $vendorDir . '/spomky-labs/pki-framework/src/X501/DN/DNParser.php', + 'SpomkyLabs\\Pki\\X501\\MatchingRule\\BinaryMatch' => $vendorDir . '/spomky-labs/pki-framework/src/X501/MatchingRule/BinaryMatch.php', + 'SpomkyLabs\\Pki\\X501\\MatchingRule\\CaseExactMatch' => $vendorDir . '/spomky-labs/pki-framework/src/X501/MatchingRule/CaseExactMatch.php', + 'SpomkyLabs\\Pki\\X501\\MatchingRule\\CaseIgnoreMatch' => $vendorDir . '/spomky-labs/pki-framework/src/X501/MatchingRule/CaseIgnoreMatch.php', + 'SpomkyLabs\\Pki\\X501\\MatchingRule\\MatchingRule' => $vendorDir . '/spomky-labs/pki-framework/src/X501/MatchingRule/MatchingRule.php', + 'SpomkyLabs\\Pki\\X501\\MatchingRule\\StringPrepMatchingRule' => $vendorDir . '/spomky-labs/pki-framework/src/X501/MatchingRule/StringPrepMatchingRule.php', + 'SpomkyLabs\\Pki\\X501\\StringPrep\\CheckBidiStep' => $vendorDir . '/spomky-labs/pki-framework/src/X501/StringPrep/CheckBidiStep.php', + 'SpomkyLabs\\Pki\\X501\\StringPrep\\InsignificantNonSubstringSpaceStep' => $vendorDir . '/spomky-labs/pki-framework/src/X501/StringPrep/InsignificantNonSubstringSpaceStep.php', + 'SpomkyLabs\\Pki\\X501\\StringPrep\\MapStep' => $vendorDir . '/spomky-labs/pki-framework/src/X501/StringPrep/MapStep.php', + 'SpomkyLabs\\Pki\\X501\\StringPrep\\NormalizeStep' => $vendorDir . '/spomky-labs/pki-framework/src/X501/StringPrep/NormalizeStep.php', + 'SpomkyLabs\\Pki\\X501\\StringPrep\\PrepareStep' => $vendorDir . '/spomky-labs/pki-framework/src/X501/StringPrep/PrepareStep.php', + 'SpomkyLabs\\Pki\\X501\\StringPrep\\ProhibitStep' => $vendorDir . '/spomky-labs/pki-framework/src/X501/StringPrep/ProhibitStep.php', + 'SpomkyLabs\\Pki\\X501\\StringPrep\\StringPreparer' => $vendorDir . '/spomky-labs/pki-framework/src/X501/StringPrep/StringPreparer.php', + 'SpomkyLabs\\Pki\\X501\\StringPrep\\TranscodeStep' => $vendorDir . '/spomky-labs/pki-framework/src/X501/StringPrep/TranscodeStep.php', + 'SpomkyLabs\\Pki\\X509\\AttributeCertificate\\AttCertIssuer' => $vendorDir . '/spomky-labs/pki-framework/src/X509/AttributeCertificate/AttCertIssuer.php', + 'SpomkyLabs\\Pki\\X509\\AttributeCertificate\\AttCertValidityPeriod' => $vendorDir . '/spomky-labs/pki-framework/src/X509/AttributeCertificate/AttCertValidityPeriod.php', + 'SpomkyLabs\\Pki\\X509\\AttributeCertificate\\AttributeCertificate' => $vendorDir . '/spomky-labs/pki-framework/src/X509/AttributeCertificate/AttributeCertificate.php', + 'SpomkyLabs\\Pki\\X509\\AttributeCertificate\\AttributeCertificateInfo' => $vendorDir . '/spomky-labs/pki-framework/src/X509/AttributeCertificate/AttributeCertificateInfo.php', + 'SpomkyLabs\\Pki\\X509\\AttributeCertificate\\Attribute\\AccessIdentityAttributeValue' => $vendorDir . '/spomky-labs/pki-framework/src/X509/AttributeCertificate/Attribute/AccessIdentityAttributeValue.php', + 'SpomkyLabs\\Pki\\X509\\AttributeCertificate\\Attribute\\AuthenticationInfoAttributeValue' => $vendorDir . '/spomky-labs/pki-framework/src/X509/AttributeCertificate/Attribute/AuthenticationInfoAttributeValue.php', + 'SpomkyLabs\\Pki\\X509\\AttributeCertificate\\Attribute\\ChargingIdentityAttributeValue' => $vendorDir . '/spomky-labs/pki-framework/src/X509/AttributeCertificate/Attribute/ChargingIdentityAttributeValue.php', + 'SpomkyLabs\\Pki\\X509\\AttributeCertificate\\Attribute\\GroupAttributeValue' => $vendorDir . '/spomky-labs/pki-framework/src/X509/AttributeCertificate/Attribute/GroupAttributeValue.php', + 'SpomkyLabs\\Pki\\X509\\AttributeCertificate\\Attribute\\IetfAttrSyntax' => $vendorDir . '/spomky-labs/pki-framework/src/X509/AttributeCertificate/Attribute/IetfAttrSyntax.php', + 'SpomkyLabs\\Pki\\X509\\AttributeCertificate\\Attribute\\IetfAttrValue' => $vendorDir . '/spomky-labs/pki-framework/src/X509/AttributeCertificate/Attribute/IetfAttrValue.php', + 'SpomkyLabs\\Pki\\X509\\AttributeCertificate\\Attribute\\RoleAttributeValue' => $vendorDir . '/spomky-labs/pki-framework/src/X509/AttributeCertificate/Attribute/RoleAttributeValue.php', + 'SpomkyLabs\\Pki\\X509\\AttributeCertificate\\Attribute\\SvceAuthInfo' => $vendorDir . '/spomky-labs/pki-framework/src/X509/AttributeCertificate/Attribute/SvceAuthInfo.php', + 'SpomkyLabs\\Pki\\X509\\AttributeCertificate\\Attributes' => $vendorDir . '/spomky-labs/pki-framework/src/X509/AttributeCertificate/Attributes.php', + 'SpomkyLabs\\Pki\\X509\\AttributeCertificate\\Holder' => $vendorDir . '/spomky-labs/pki-framework/src/X509/AttributeCertificate/Holder.php', + 'SpomkyLabs\\Pki\\X509\\AttributeCertificate\\IssuerSerial' => $vendorDir . '/spomky-labs/pki-framework/src/X509/AttributeCertificate/IssuerSerial.php', + 'SpomkyLabs\\Pki\\X509\\AttributeCertificate\\ObjectDigestInfo' => $vendorDir . '/spomky-labs/pki-framework/src/X509/AttributeCertificate/ObjectDigestInfo.php', + 'SpomkyLabs\\Pki\\X509\\AttributeCertificate\\V2Form' => $vendorDir . '/spomky-labs/pki-framework/src/X509/AttributeCertificate/V2Form.php', + 'SpomkyLabs\\Pki\\X509\\AttributeCertificate\\Validation\\ACValidationConfig' => $vendorDir . '/spomky-labs/pki-framework/src/X509/AttributeCertificate/Validation/ACValidationConfig.php', + 'SpomkyLabs\\Pki\\X509\\AttributeCertificate\\Validation\\ACValidator' => $vendorDir . '/spomky-labs/pki-framework/src/X509/AttributeCertificate/Validation/ACValidator.php', + 'SpomkyLabs\\Pki\\X509\\AttributeCertificate\\Validation\\Exception\\ACValidationException' => $vendorDir . '/spomky-labs/pki-framework/src/X509/AttributeCertificate/Validation/Exception/ACValidationException.php', + 'SpomkyLabs\\Pki\\X509\\Certificate\\Certificate' => $vendorDir . '/spomky-labs/pki-framework/src/X509/Certificate/Certificate.php', + 'SpomkyLabs\\Pki\\X509\\Certificate\\CertificateBundle' => $vendorDir . '/spomky-labs/pki-framework/src/X509/Certificate/CertificateBundle.php', + 'SpomkyLabs\\Pki\\X509\\Certificate\\CertificateChain' => $vendorDir . '/spomky-labs/pki-framework/src/X509/Certificate/CertificateChain.php', + 'SpomkyLabs\\Pki\\X509\\Certificate\\Extension\\AAControlsExtension' => $vendorDir . '/spomky-labs/pki-framework/src/X509/Certificate/Extension/AAControlsExtension.php', + 'SpomkyLabs\\Pki\\X509\\Certificate\\Extension\\AccessDescription\\AccessDescription' => $vendorDir . '/spomky-labs/pki-framework/src/X509/Certificate/Extension/AccessDescription/AccessDescription.php', + 'SpomkyLabs\\Pki\\X509\\Certificate\\Extension\\AccessDescription\\AuthorityAccessDescription' => $vendorDir . '/spomky-labs/pki-framework/src/X509/Certificate/Extension/AccessDescription/AuthorityAccessDescription.php', + 'SpomkyLabs\\Pki\\X509\\Certificate\\Extension\\AccessDescription\\SubjectAccessDescription' => $vendorDir . '/spomky-labs/pki-framework/src/X509/Certificate/Extension/AccessDescription/SubjectAccessDescription.php', + 'SpomkyLabs\\Pki\\X509\\Certificate\\Extension\\AuthorityInformationAccessExtension' => $vendorDir . '/spomky-labs/pki-framework/src/X509/Certificate/Extension/AuthorityInformationAccessExtension.php', + 'SpomkyLabs\\Pki\\X509\\Certificate\\Extension\\AuthorityKeyIdentifierExtension' => $vendorDir . '/spomky-labs/pki-framework/src/X509/Certificate/Extension/AuthorityKeyIdentifierExtension.php', + 'SpomkyLabs\\Pki\\X509\\Certificate\\Extension\\BasicConstraintsExtension' => $vendorDir . '/spomky-labs/pki-framework/src/X509/Certificate/Extension/BasicConstraintsExtension.php', + 'SpomkyLabs\\Pki\\X509\\Certificate\\Extension\\CRLDistributionPointsExtension' => $vendorDir . '/spomky-labs/pki-framework/src/X509/Certificate/Extension/CRLDistributionPointsExtension.php', + 'SpomkyLabs\\Pki\\X509\\Certificate\\Extension\\CertificatePoliciesExtension' => $vendorDir . '/spomky-labs/pki-framework/src/X509/Certificate/Extension/CertificatePoliciesExtension.php', + 'SpomkyLabs\\Pki\\X509\\Certificate\\Extension\\CertificatePolicy\\CPSQualifier' => $vendorDir . '/spomky-labs/pki-framework/src/X509/Certificate/Extension/CertificatePolicy/CPSQualifier.php', + 'SpomkyLabs\\Pki\\X509\\Certificate\\Extension\\CertificatePolicy\\DisplayText' => $vendorDir . '/spomky-labs/pki-framework/src/X509/Certificate/Extension/CertificatePolicy/DisplayText.php', + 'SpomkyLabs\\Pki\\X509\\Certificate\\Extension\\CertificatePolicy\\NoticeReference' => $vendorDir . '/spomky-labs/pki-framework/src/X509/Certificate/Extension/CertificatePolicy/NoticeReference.php', + 'SpomkyLabs\\Pki\\X509\\Certificate\\Extension\\CertificatePolicy\\PolicyInformation' => $vendorDir . '/spomky-labs/pki-framework/src/X509/Certificate/Extension/CertificatePolicy/PolicyInformation.php', + 'SpomkyLabs\\Pki\\X509\\Certificate\\Extension\\CertificatePolicy\\PolicyQualifierInfo' => $vendorDir . '/spomky-labs/pki-framework/src/X509/Certificate/Extension/CertificatePolicy/PolicyQualifierInfo.php', + 'SpomkyLabs\\Pki\\X509\\Certificate\\Extension\\CertificatePolicy\\UserNoticeQualifier' => $vendorDir . '/spomky-labs/pki-framework/src/X509/Certificate/Extension/CertificatePolicy/UserNoticeQualifier.php', + 'SpomkyLabs\\Pki\\X509\\Certificate\\Extension\\DistributionPoint\\DistributionPoint' => $vendorDir . '/spomky-labs/pki-framework/src/X509/Certificate/Extension/DistributionPoint/DistributionPoint.php', + 'SpomkyLabs\\Pki\\X509\\Certificate\\Extension\\DistributionPoint\\DistributionPointName' => $vendorDir . '/spomky-labs/pki-framework/src/X509/Certificate/Extension/DistributionPoint/DistributionPointName.php', + 'SpomkyLabs\\Pki\\X509\\Certificate\\Extension\\DistributionPoint\\FullName' => $vendorDir . '/spomky-labs/pki-framework/src/X509/Certificate/Extension/DistributionPoint/FullName.php', + 'SpomkyLabs\\Pki\\X509\\Certificate\\Extension\\DistributionPoint\\ReasonFlags' => $vendorDir . '/spomky-labs/pki-framework/src/X509/Certificate/Extension/DistributionPoint/ReasonFlags.php', + 'SpomkyLabs\\Pki\\X509\\Certificate\\Extension\\DistributionPoint\\RelativeName' => $vendorDir . '/spomky-labs/pki-framework/src/X509/Certificate/Extension/DistributionPoint/RelativeName.php', + 'SpomkyLabs\\Pki\\X509\\Certificate\\Extension\\ExtendedKeyUsageExtension' => $vendorDir . '/spomky-labs/pki-framework/src/X509/Certificate/Extension/ExtendedKeyUsageExtension.php', + 'SpomkyLabs\\Pki\\X509\\Certificate\\Extension\\Extension' => $vendorDir . '/spomky-labs/pki-framework/src/X509/Certificate/Extension/Extension.php', + 'SpomkyLabs\\Pki\\X509\\Certificate\\Extension\\FreshestCRLExtension' => $vendorDir . '/spomky-labs/pki-framework/src/X509/Certificate/Extension/FreshestCRLExtension.php', + 'SpomkyLabs\\Pki\\X509\\Certificate\\Extension\\InhibitAnyPolicyExtension' => $vendorDir . '/spomky-labs/pki-framework/src/X509/Certificate/Extension/InhibitAnyPolicyExtension.php', + 'SpomkyLabs\\Pki\\X509\\Certificate\\Extension\\IssuerAlternativeNameExtension' => $vendorDir . '/spomky-labs/pki-framework/src/X509/Certificate/Extension/IssuerAlternativeNameExtension.php', + 'SpomkyLabs\\Pki\\X509\\Certificate\\Extension\\KeyUsageExtension' => $vendorDir . '/spomky-labs/pki-framework/src/X509/Certificate/Extension/KeyUsageExtension.php', + 'SpomkyLabs\\Pki\\X509\\Certificate\\Extension\\NameConstraintsExtension' => $vendorDir . '/spomky-labs/pki-framework/src/X509/Certificate/Extension/NameConstraintsExtension.php', + 'SpomkyLabs\\Pki\\X509\\Certificate\\Extension\\NameConstraints\\GeneralSubtree' => $vendorDir . '/spomky-labs/pki-framework/src/X509/Certificate/Extension/NameConstraints/GeneralSubtree.php', + 'SpomkyLabs\\Pki\\X509\\Certificate\\Extension\\NameConstraints\\GeneralSubtrees' => $vendorDir . '/spomky-labs/pki-framework/src/X509/Certificate/Extension/NameConstraints/GeneralSubtrees.php', + 'SpomkyLabs\\Pki\\X509\\Certificate\\Extension\\NoRevocationAvailableExtension' => $vendorDir . '/spomky-labs/pki-framework/src/X509/Certificate/Extension/NoRevocationAvailableExtension.php', + 'SpomkyLabs\\Pki\\X509\\Certificate\\Extension\\PolicyConstraintsExtension' => $vendorDir . '/spomky-labs/pki-framework/src/X509/Certificate/Extension/PolicyConstraintsExtension.php', + 'SpomkyLabs\\Pki\\X509\\Certificate\\Extension\\PolicyMappingsExtension' => $vendorDir . '/spomky-labs/pki-framework/src/X509/Certificate/Extension/PolicyMappingsExtension.php', + 'SpomkyLabs\\Pki\\X509\\Certificate\\Extension\\PolicyMappings\\PolicyMapping' => $vendorDir . '/spomky-labs/pki-framework/src/X509/Certificate/Extension/PolicyMappings/PolicyMapping.php', + 'SpomkyLabs\\Pki\\X509\\Certificate\\Extension\\SubjectAlternativeNameExtension' => $vendorDir . '/spomky-labs/pki-framework/src/X509/Certificate/Extension/SubjectAlternativeNameExtension.php', + 'SpomkyLabs\\Pki\\X509\\Certificate\\Extension\\SubjectDirectoryAttributesExtension' => $vendorDir . '/spomky-labs/pki-framework/src/X509/Certificate/Extension/SubjectDirectoryAttributesExtension.php', + 'SpomkyLabs\\Pki\\X509\\Certificate\\Extension\\SubjectInformationAccessExtension' => $vendorDir . '/spomky-labs/pki-framework/src/X509/Certificate/Extension/SubjectInformationAccessExtension.php', + 'SpomkyLabs\\Pki\\X509\\Certificate\\Extension\\SubjectKeyIdentifierExtension' => $vendorDir . '/spomky-labs/pki-framework/src/X509/Certificate/Extension/SubjectKeyIdentifierExtension.php', + 'SpomkyLabs\\Pki\\X509\\Certificate\\Extension\\TargetInformationExtension' => $vendorDir . '/spomky-labs/pki-framework/src/X509/Certificate/Extension/TargetInformationExtension.php', + 'SpomkyLabs\\Pki\\X509\\Certificate\\Extension\\Target\\Target' => $vendorDir . '/spomky-labs/pki-framework/src/X509/Certificate/Extension/Target/Target.php', + 'SpomkyLabs\\Pki\\X509\\Certificate\\Extension\\Target\\TargetGroup' => $vendorDir . '/spomky-labs/pki-framework/src/X509/Certificate/Extension/Target/TargetGroup.php', + 'SpomkyLabs\\Pki\\X509\\Certificate\\Extension\\Target\\TargetName' => $vendorDir . '/spomky-labs/pki-framework/src/X509/Certificate/Extension/Target/TargetName.php', + 'SpomkyLabs\\Pki\\X509\\Certificate\\Extension\\Target\\Targets' => $vendorDir . '/spomky-labs/pki-framework/src/X509/Certificate/Extension/Target/Targets.php', + 'SpomkyLabs\\Pki\\X509\\Certificate\\Extension\\UnknownExtension' => $vendorDir . '/spomky-labs/pki-framework/src/X509/Certificate/Extension/UnknownExtension.php', + 'SpomkyLabs\\Pki\\X509\\Certificate\\Extensions' => $vendorDir . '/spomky-labs/pki-framework/src/X509/Certificate/Extensions.php', + 'SpomkyLabs\\Pki\\X509\\Certificate\\TBSCertificate' => $vendorDir . '/spomky-labs/pki-framework/src/X509/Certificate/TBSCertificate.php', + 'SpomkyLabs\\Pki\\X509\\Certificate\\Time' => $vendorDir . '/spomky-labs/pki-framework/src/X509/Certificate/Time.php', + 'SpomkyLabs\\Pki\\X509\\Certificate\\UniqueIdentifier' => $vendorDir . '/spomky-labs/pki-framework/src/X509/Certificate/UniqueIdentifier.php', + 'SpomkyLabs\\Pki\\X509\\Certificate\\Validity' => $vendorDir . '/spomky-labs/pki-framework/src/X509/Certificate/Validity.php', + 'SpomkyLabs\\Pki\\X509\\CertificationPath\\CertificationPath' => $vendorDir . '/spomky-labs/pki-framework/src/X509/CertificationPath/CertificationPath.php', + 'SpomkyLabs\\Pki\\X509\\CertificationPath\\Exception\\PathBuildingException' => $vendorDir . '/spomky-labs/pki-framework/src/X509/CertificationPath/Exception/PathBuildingException.php', + 'SpomkyLabs\\Pki\\X509\\CertificationPath\\Exception\\PathValidationException' => $vendorDir . '/spomky-labs/pki-framework/src/X509/CertificationPath/Exception/PathValidationException.php', + 'SpomkyLabs\\Pki\\X509\\CertificationPath\\PathBuilding\\CertificationPathBuilder' => $vendorDir . '/spomky-labs/pki-framework/src/X509/CertificationPath/PathBuilding/CertificationPathBuilder.php', + 'SpomkyLabs\\Pki\\X509\\CertificationPath\\PathValidation\\PathValidationConfig' => $vendorDir . '/spomky-labs/pki-framework/src/X509/CertificationPath/PathValidation/PathValidationConfig.php', + 'SpomkyLabs\\Pki\\X509\\CertificationPath\\PathValidation\\PathValidationResult' => $vendorDir . '/spomky-labs/pki-framework/src/X509/CertificationPath/PathValidation/PathValidationResult.php', + 'SpomkyLabs\\Pki\\X509\\CertificationPath\\PathValidation\\PathValidator' => $vendorDir . '/spomky-labs/pki-framework/src/X509/CertificationPath/PathValidation/PathValidator.php', + 'SpomkyLabs\\Pki\\X509\\CertificationPath\\PathValidation\\ValidatorState' => $vendorDir . '/spomky-labs/pki-framework/src/X509/CertificationPath/PathValidation/ValidatorState.php', + 'SpomkyLabs\\Pki\\X509\\CertificationPath\\Policy\\PolicyNode' => $vendorDir . '/spomky-labs/pki-framework/src/X509/CertificationPath/Policy/PolicyNode.php', + 'SpomkyLabs\\Pki\\X509\\CertificationPath\\Policy\\PolicyTree' => $vendorDir . '/spomky-labs/pki-framework/src/X509/CertificationPath/Policy/PolicyTree.php', + 'SpomkyLabs\\Pki\\X509\\CertificationRequest\\Attribute\\ExtensionRequestValue' => $vendorDir . '/spomky-labs/pki-framework/src/X509/CertificationRequest/Attribute/ExtensionRequestValue.php', + 'SpomkyLabs\\Pki\\X509\\CertificationRequest\\Attributes' => $vendorDir . '/spomky-labs/pki-framework/src/X509/CertificationRequest/Attributes.php', + 'SpomkyLabs\\Pki\\X509\\CertificationRequest\\CertificationRequest' => $vendorDir . '/spomky-labs/pki-framework/src/X509/CertificationRequest/CertificationRequest.php', + 'SpomkyLabs\\Pki\\X509\\CertificationRequest\\CertificationRequestInfo' => $vendorDir . '/spomky-labs/pki-framework/src/X509/CertificationRequest/CertificationRequestInfo.php', + 'SpomkyLabs\\Pki\\X509\\Exception\\X509ValidationException' => $vendorDir . '/spomky-labs/pki-framework/src/X509/Exception/X509ValidationException.php', + 'SpomkyLabs\\Pki\\X509\\Feature\\DateTimeHelper' => $vendorDir . '/spomky-labs/pki-framework/src/X509/Feature/DateTimeHelper.php', + 'SpomkyLabs\\Pki\\X509\\GeneralName\\DNSName' => $vendorDir . '/spomky-labs/pki-framework/src/X509/GeneralName/DNSName.php', + 'SpomkyLabs\\Pki\\X509\\GeneralName\\DirectoryName' => $vendorDir . '/spomky-labs/pki-framework/src/X509/GeneralName/DirectoryName.php', + 'SpomkyLabs\\Pki\\X509\\GeneralName\\EDIPartyName' => $vendorDir . '/spomky-labs/pki-framework/src/X509/GeneralName/EDIPartyName.php', + 'SpomkyLabs\\Pki\\X509\\GeneralName\\GeneralName' => $vendorDir . '/spomky-labs/pki-framework/src/X509/GeneralName/GeneralName.php', + 'SpomkyLabs\\Pki\\X509\\GeneralName\\GeneralNames' => $vendorDir . '/spomky-labs/pki-framework/src/X509/GeneralName/GeneralNames.php', + 'SpomkyLabs\\Pki\\X509\\GeneralName\\IPAddress' => $vendorDir . '/spomky-labs/pki-framework/src/X509/GeneralName/IPAddress.php', + 'SpomkyLabs\\Pki\\X509\\GeneralName\\IPv4Address' => $vendorDir . '/spomky-labs/pki-framework/src/X509/GeneralName/IPv4Address.php', + 'SpomkyLabs\\Pki\\X509\\GeneralName\\IPv6Address' => $vendorDir . '/spomky-labs/pki-framework/src/X509/GeneralName/IPv6Address.php', + 'SpomkyLabs\\Pki\\X509\\GeneralName\\OtherName' => $vendorDir . '/spomky-labs/pki-framework/src/X509/GeneralName/OtherName.php', + 'SpomkyLabs\\Pki\\X509\\GeneralName\\RFC822Name' => $vendorDir . '/spomky-labs/pki-framework/src/X509/GeneralName/RFC822Name.php', + 'SpomkyLabs\\Pki\\X509\\GeneralName\\RegisteredID' => $vendorDir . '/spomky-labs/pki-framework/src/X509/GeneralName/RegisteredID.php', + 'SpomkyLabs\\Pki\\X509\\GeneralName\\UniformResourceIdentifier' => $vendorDir . '/spomky-labs/pki-framework/src/X509/GeneralName/UniformResourceIdentifier.php', + 'SpomkyLabs\\Pki\\X509\\GeneralName\\X400Address' => $vendorDir . '/spomky-labs/pki-framework/src/X509/GeneralName/X400Address.php', + 'Stecman\\Component\\Symfony\\Console\\BashCompletion\\Completion' => $vendorDir . '/stecman/symfony-console-completion/src/Completion.php', + 'Stecman\\Component\\Symfony\\Console\\BashCompletion\\CompletionCommand' => $vendorDir . '/stecman/symfony-console-completion/src/CompletionCommand.php', + 'Stecman\\Component\\Symfony\\Console\\BashCompletion\\CompletionContext' => $vendorDir . '/stecman/symfony-console-completion/src/CompletionContext.php', + 'Stecman\\Component\\Symfony\\Console\\BashCompletion\\CompletionHandler' => $vendorDir . '/stecman/symfony-console-completion/src/CompletionHandler.php', + 'Stecman\\Component\\Symfony\\Console\\BashCompletion\\Completion\\CompletionAwareInterface' => $vendorDir . '/stecman/symfony-console-completion/src/Completion/CompletionAwareInterface.php', + 'Stecman\\Component\\Symfony\\Console\\BashCompletion\\Completion\\CompletionInterface' => $vendorDir . '/stecman/symfony-console-completion/src/Completion/CompletionInterface.php', + 'Stecman\\Component\\Symfony\\Console\\BashCompletion\\Completion\\ShellPathCompletion' => $vendorDir . '/stecman/symfony-console-completion/src/Completion/ShellPathCompletion.php', + 'Stecman\\Component\\Symfony\\Console\\BashCompletion\\EnvironmentCompletionContext' => $vendorDir . '/stecman/symfony-console-completion/src/EnvironmentCompletionContext.php', + 'Stecman\\Component\\Symfony\\Console\\BashCompletion\\HookFactory' => $vendorDir . '/stecman/symfony-console-completion/src/HookFactory.php', + 'Stringable' => $vendorDir . '/marc-mabe/php-enum/stubs/Stringable.php', + 'Symfony\\Component\\Console\\Application' => $vendorDir . '/symfony/console/Application.php', + 'Symfony\\Component\\Console\\Attribute\\AsCommand' => $vendorDir . '/symfony/console/Attribute/AsCommand.php', + 'Symfony\\Component\\Console\\CI\\GithubActionReporter' => $vendorDir . '/symfony/console/CI/GithubActionReporter.php', + 'Symfony\\Component\\Console\\Color' => $vendorDir . '/symfony/console/Color.php', + 'Symfony\\Component\\Console\\CommandLoader\\CommandLoaderInterface' => $vendorDir . '/symfony/console/CommandLoader/CommandLoaderInterface.php', + 'Symfony\\Component\\Console\\CommandLoader\\ContainerCommandLoader' => $vendorDir . '/symfony/console/CommandLoader/ContainerCommandLoader.php', + 'Symfony\\Component\\Console\\CommandLoader\\FactoryCommandLoader' => $vendorDir . '/symfony/console/CommandLoader/FactoryCommandLoader.php', + 'Symfony\\Component\\Console\\Command\\Command' => $vendorDir . '/symfony/console/Command/Command.php', + 'Symfony\\Component\\Console\\Command\\CompleteCommand' => $vendorDir . '/symfony/console/Command/CompleteCommand.php', + 'Symfony\\Component\\Console\\Command\\DumpCompletionCommand' => $vendorDir . '/symfony/console/Command/DumpCompletionCommand.php', + 'Symfony\\Component\\Console\\Command\\HelpCommand' => $vendorDir . '/symfony/console/Command/HelpCommand.php', + 'Symfony\\Component\\Console\\Command\\LazyCommand' => $vendorDir . '/symfony/console/Command/LazyCommand.php', + 'Symfony\\Component\\Console\\Command\\ListCommand' => $vendorDir . '/symfony/console/Command/ListCommand.php', + 'Symfony\\Component\\Console\\Command\\LockableTrait' => $vendorDir . '/symfony/console/Command/LockableTrait.php', + 'Symfony\\Component\\Console\\Command\\SignalableCommandInterface' => $vendorDir . '/symfony/console/Command/SignalableCommandInterface.php', + 'Symfony\\Component\\Console\\Command\\TraceableCommand' => $vendorDir . '/symfony/console/Command/TraceableCommand.php', + 'Symfony\\Component\\Console\\Completion\\CompletionInput' => $vendorDir . '/symfony/console/Completion/CompletionInput.php', + 'Symfony\\Component\\Console\\Completion\\CompletionSuggestions' => $vendorDir . '/symfony/console/Completion/CompletionSuggestions.php', + 'Symfony\\Component\\Console\\Completion\\Output\\BashCompletionOutput' => $vendorDir . '/symfony/console/Completion/Output/BashCompletionOutput.php', + 'Symfony\\Component\\Console\\Completion\\Output\\CompletionOutputInterface' => $vendorDir . '/symfony/console/Completion/Output/CompletionOutputInterface.php', + 'Symfony\\Component\\Console\\Completion\\Output\\FishCompletionOutput' => $vendorDir . '/symfony/console/Completion/Output/FishCompletionOutput.php', + 'Symfony\\Component\\Console\\Completion\\Output\\ZshCompletionOutput' => $vendorDir . '/symfony/console/Completion/Output/ZshCompletionOutput.php', + 'Symfony\\Component\\Console\\Completion\\Suggestion' => $vendorDir . '/symfony/console/Completion/Suggestion.php', + 'Symfony\\Component\\Console\\ConsoleEvents' => $vendorDir . '/symfony/console/ConsoleEvents.php', + 'Symfony\\Component\\Console\\Cursor' => $vendorDir . '/symfony/console/Cursor.php', + 'Symfony\\Component\\Console\\DataCollector\\CommandDataCollector' => $vendorDir . '/symfony/console/DataCollector/CommandDataCollector.php', + 'Symfony\\Component\\Console\\Debug\\CliRequest' => $vendorDir . '/symfony/console/Debug/CliRequest.php', + 'Symfony\\Component\\Console\\DependencyInjection\\AddConsoleCommandPass' => $vendorDir . '/symfony/console/DependencyInjection/AddConsoleCommandPass.php', + 'Symfony\\Component\\Console\\Descriptor\\ApplicationDescription' => $vendorDir . '/symfony/console/Descriptor/ApplicationDescription.php', + 'Symfony\\Component\\Console\\Descriptor\\Descriptor' => $vendorDir . '/symfony/console/Descriptor/Descriptor.php', + 'Symfony\\Component\\Console\\Descriptor\\DescriptorInterface' => $vendorDir . '/symfony/console/Descriptor/DescriptorInterface.php', + 'Symfony\\Component\\Console\\Descriptor\\JsonDescriptor' => $vendorDir . '/symfony/console/Descriptor/JsonDescriptor.php', + 'Symfony\\Component\\Console\\Descriptor\\MarkdownDescriptor' => $vendorDir . '/symfony/console/Descriptor/MarkdownDescriptor.php', + 'Symfony\\Component\\Console\\Descriptor\\ReStructuredTextDescriptor' => $vendorDir . '/symfony/console/Descriptor/ReStructuredTextDescriptor.php', + 'Symfony\\Component\\Console\\Descriptor\\TextDescriptor' => $vendorDir . '/symfony/console/Descriptor/TextDescriptor.php', + 'Symfony\\Component\\Console\\Descriptor\\XmlDescriptor' => $vendorDir . '/symfony/console/Descriptor/XmlDescriptor.php', + 'Symfony\\Component\\Console\\EventListener\\ErrorListener' => $vendorDir . '/symfony/console/EventListener/ErrorListener.php', + 'Symfony\\Component\\Console\\Event\\ConsoleCommandEvent' => $vendorDir . '/symfony/console/Event/ConsoleCommandEvent.php', + 'Symfony\\Component\\Console\\Event\\ConsoleErrorEvent' => $vendorDir . '/symfony/console/Event/ConsoleErrorEvent.php', + 'Symfony\\Component\\Console\\Event\\ConsoleEvent' => $vendorDir . '/symfony/console/Event/ConsoleEvent.php', + 'Symfony\\Component\\Console\\Event\\ConsoleSignalEvent' => $vendorDir . '/symfony/console/Event/ConsoleSignalEvent.php', + 'Symfony\\Component\\Console\\Event\\ConsoleTerminateEvent' => $vendorDir . '/symfony/console/Event/ConsoleTerminateEvent.php', + 'Symfony\\Component\\Console\\Exception\\CommandNotFoundException' => $vendorDir . '/symfony/console/Exception/CommandNotFoundException.php', + 'Symfony\\Component\\Console\\Exception\\ExceptionInterface' => $vendorDir . '/symfony/console/Exception/ExceptionInterface.php', + 'Symfony\\Component\\Console\\Exception\\InvalidArgumentException' => $vendorDir . '/symfony/console/Exception/InvalidArgumentException.php', + 'Symfony\\Component\\Console\\Exception\\InvalidOptionException' => $vendorDir . '/symfony/console/Exception/InvalidOptionException.php', + 'Symfony\\Component\\Console\\Exception\\LogicException' => $vendorDir . '/symfony/console/Exception/LogicException.php', + 'Symfony\\Component\\Console\\Exception\\MissingInputException' => $vendorDir . '/symfony/console/Exception/MissingInputException.php', + 'Symfony\\Component\\Console\\Exception\\NamespaceNotFoundException' => $vendorDir . '/symfony/console/Exception/NamespaceNotFoundException.php', + 'Symfony\\Component\\Console\\Exception\\RunCommandFailedException' => $vendorDir . '/symfony/console/Exception/RunCommandFailedException.php', + 'Symfony\\Component\\Console\\Exception\\RuntimeException' => $vendorDir . '/symfony/console/Exception/RuntimeException.php', + 'Symfony\\Component\\Console\\Formatter\\NullOutputFormatter' => $vendorDir . '/symfony/console/Formatter/NullOutputFormatter.php', + 'Symfony\\Component\\Console\\Formatter\\NullOutputFormatterStyle' => $vendorDir . '/symfony/console/Formatter/NullOutputFormatterStyle.php', + 'Symfony\\Component\\Console\\Formatter\\OutputFormatter' => $vendorDir . '/symfony/console/Formatter/OutputFormatter.php', + 'Symfony\\Component\\Console\\Formatter\\OutputFormatterInterface' => $vendorDir . '/symfony/console/Formatter/OutputFormatterInterface.php', + 'Symfony\\Component\\Console\\Formatter\\OutputFormatterStyle' => $vendorDir . '/symfony/console/Formatter/OutputFormatterStyle.php', + 'Symfony\\Component\\Console\\Formatter\\OutputFormatterStyleInterface' => $vendorDir . '/symfony/console/Formatter/OutputFormatterStyleInterface.php', + 'Symfony\\Component\\Console\\Formatter\\OutputFormatterStyleStack' => $vendorDir . '/symfony/console/Formatter/OutputFormatterStyleStack.php', + 'Symfony\\Component\\Console\\Formatter\\WrappableOutputFormatterInterface' => $vendorDir . '/symfony/console/Formatter/WrappableOutputFormatterInterface.php', + 'Symfony\\Component\\Console\\Helper\\DebugFormatterHelper' => $vendorDir . '/symfony/console/Helper/DebugFormatterHelper.php', + 'Symfony\\Component\\Console\\Helper\\DescriptorHelper' => $vendorDir . '/symfony/console/Helper/DescriptorHelper.php', + 'Symfony\\Component\\Console\\Helper\\Dumper' => $vendorDir . '/symfony/console/Helper/Dumper.php', + 'Symfony\\Component\\Console\\Helper\\FormatterHelper' => $vendorDir . '/symfony/console/Helper/FormatterHelper.php', + 'Symfony\\Component\\Console\\Helper\\Helper' => $vendorDir . '/symfony/console/Helper/Helper.php', + 'Symfony\\Component\\Console\\Helper\\HelperInterface' => $vendorDir . '/symfony/console/Helper/HelperInterface.php', + 'Symfony\\Component\\Console\\Helper\\HelperSet' => $vendorDir . '/symfony/console/Helper/HelperSet.php', + 'Symfony\\Component\\Console\\Helper\\InputAwareHelper' => $vendorDir . '/symfony/console/Helper/InputAwareHelper.php', + 'Symfony\\Component\\Console\\Helper\\OutputWrapper' => $vendorDir . '/symfony/console/Helper/OutputWrapper.php', + 'Symfony\\Component\\Console\\Helper\\ProcessHelper' => $vendorDir . '/symfony/console/Helper/ProcessHelper.php', + 'Symfony\\Component\\Console\\Helper\\ProgressBar' => $vendorDir . '/symfony/console/Helper/ProgressBar.php', + 'Symfony\\Component\\Console\\Helper\\ProgressIndicator' => $vendorDir . '/symfony/console/Helper/ProgressIndicator.php', + 'Symfony\\Component\\Console\\Helper\\QuestionHelper' => $vendorDir . '/symfony/console/Helper/QuestionHelper.php', + 'Symfony\\Component\\Console\\Helper\\SymfonyQuestionHelper' => $vendorDir . '/symfony/console/Helper/SymfonyQuestionHelper.php', + 'Symfony\\Component\\Console\\Helper\\Table' => $vendorDir . '/symfony/console/Helper/Table.php', + 'Symfony\\Component\\Console\\Helper\\TableCell' => $vendorDir . '/symfony/console/Helper/TableCell.php', + 'Symfony\\Component\\Console\\Helper\\TableCellStyle' => $vendorDir . '/symfony/console/Helper/TableCellStyle.php', + 'Symfony\\Component\\Console\\Helper\\TableRows' => $vendorDir . '/symfony/console/Helper/TableRows.php', + 'Symfony\\Component\\Console\\Helper\\TableSeparator' => $vendorDir . '/symfony/console/Helper/TableSeparator.php', + 'Symfony\\Component\\Console\\Helper\\TableStyle' => $vendorDir . '/symfony/console/Helper/TableStyle.php', + 'Symfony\\Component\\Console\\Input\\ArgvInput' => $vendorDir . '/symfony/console/Input/ArgvInput.php', + 'Symfony\\Component\\Console\\Input\\ArrayInput' => $vendorDir . '/symfony/console/Input/ArrayInput.php', + 'Symfony\\Component\\Console\\Input\\Input' => $vendorDir . '/symfony/console/Input/Input.php', + 'Symfony\\Component\\Console\\Input\\InputArgument' => $vendorDir . '/symfony/console/Input/InputArgument.php', + 'Symfony\\Component\\Console\\Input\\InputAwareInterface' => $vendorDir . '/symfony/console/Input/InputAwareInterface.php', + 'Symfony\\Component\\Console\\Input\\InputDefinition' => $vendorDir . '/symfony/console/Input/InputDefinition.php', + 'Symfony\\Component\\Console\\Input\\InputInterface' => $vendorDir . '/symfony/console/Input/InputInterface.php', + 'Symfony\\Component\\Console\\Input\\InputOption' => $vendorDir . '/symfony/console/Input/InputOption.php', + 'Symfony\\Component\\Console\\Input\\StreamableInputInterface' => $vendorDir . '/symfony/console/Input/StreamableInputInterface.php', + 'Symfony\\Component\\Console\\Input\\StringInput' => $vendorDir . '/symfony/console/Input/StringInput.php', + 'Symfony\\Component\\Console\\Logger\\ConsoleLogger' => $vendorDir . '/symfony/console/Logger/ConsoleLogger.php', + 'Symfony\\Component\\Console\\Messenger\\RunCommandContext' => $vendorDir . '/symfony/console/Messenger/RunCommandContext.php', + 'Symfony\\Component\\Console\\Messenger\\RunCommandMessage' => $vendorDir . '/symfony/console/Messenger/RunCommandMessage.php', + 'Symfony\\Component\\Console\\Messenger\\RunCommandMessageHandler' => $vendorDir . '/symfony/console/Messenger/RunCommandMessageHandler.php', + 'Symfony\\Component\\Console\\Output\\AnsiColorMode' => $vendorDir . '/symfony/console/Output/AnsiColorMode.php', + 'Symfony\\Component\\Console\\Output\\BufferedOutput' => $vendorDir . '/symfony/console/Output/BufferedOutput.php', + 'Symfony\\Component\\Console\\Output\\ConsoleOutput' => $vendorDir . '/symfony/console/Output/ConsoleOutput.php', + 'Symfony\\Component\\Console\\Output\\ConsoleOutputInterface' => $vendorDir . '/symfony/console/Output/ConsoleOutputInterface.php', + 'Symfony\\Component\\Console\\Output\\ConsoleSectionOutput' => $vendorDir . '/symfony/console/Output/ConsoleSectionOutput.php', + 'Symfony\\Component\\Console\\Output\\NullOutput' => $vendorDir . '/symfony/console/Output/NullOutput.php', + 'Symfony\\Component\\Console\\Output\\Output' => $vendorDir . '/symfony/console/Output/Output.php', + 'Symfony\\Component\\Console\\Output\\OutputInterface' => $vendorDir . '/symfony/console/Output/OutputInterface.php', + 'Symfony\\Component\\Console\\Output\\StreamOutput' => $vendorDir . '/symfony/console/Output/StreamOutput.php', + 'Symfony\\Component\\Console\\Output\\TrimmedBufferOutput' => $vendorDir . '/symfony/console/Output/TrimmedBufferOutput.php', + 'Symfony\\Component\\Console\\Question\\ChoiceQuestion' => $vendorDir . '/symfony/console/Question/ChoiceQuestion.php', + 'Symfony\\Component\\Console\\Question\\ConfirmationQuestion' => $vendorDir . '/symfony/console/Question/ConfirmationQuestion.php', + 'Symfony\\Component\\Console\\Question\\Question' => $vendorDir . '/symfony/console/Question/Question.php', + 'Symfony\\Component\\Console\\SignalRegistry\\SignalMap' => $vendorDir . '/symfony/console/SignalRegistry/SignalMap.php', + 'Symfony\\Component\\Console\\SignalRegistry\\SignalRegistry' => $vendorDir . '/symfony/console/SignalRegistry/SignalRegistry.php', + 'Symfony\\Component\\Console\\SingleCommandApplication' => $vendorDir . '/symfony/console/SingleCommandApplication.php', + 'Symfony\\Component\\Console\\Style\\OutputStyle' => $vendorDir . '/symfony/console/Style/OutputStyle.php', + 'Symfony\\Component\\Console\\Style\\StyleInterface' => $vendorDir . '/symfony/console/Style/StyleInterface.php', + 'Symfony\\Component\\Console\\Style\\SymfonyStyle' => $vendorDir . '/symfony/console/Style/SymfonyStyle.php', + 'Symfony\\Component\\Console\\Terminal' => $vendorDir . '/symfony/console/Terminal.php', + 'Symfony\\Component\\Console\\Tester\\ApplicationTester' => $vendorDir . '/symfony/console/Tester/ApplicationTester.php', + 'Symfony\\Component\\Console\\Tester\\CommandCompletionTester' => $vendorDir . '/symfony/console/Tester/CommandCompletionTester.php', + 'Symfony\\Component\\Console\\Tester\\CommandTester' => $vendorDir . '/symfony/console/Tester/CommandTester.php', + 'Symfony\\Component\\Console\\Tester\\Constraint\\CommandIsSuccessful' => $vendorDir . '/symfony/console/Tester/Constraint/CommandIsSuccessful.php', + 'Symfony\\Component\\Console\\Tester\\TesterTrait' => $vendorDir . '/symfony/console/Tester/TesterTrait.php', + 'Symfony\\Component\\CssSelector\\CssSelectorConverter' => $vendorDir . '/symfony/css-selector/CssSelectorConverter.php', + 'Symfony\\Component\\CssSelector\\Exception\\ExceptionInterface' => $vendorDir . '/symfony/css-selector/Exception/ExceptionInterface.php', + 'Symfony\\Component\\CssSelector\\Exception\\ExpressionErrorException' => $vendorDir . '/symfony/css-selector/Exception/ExpressionErrorException.php', + 'Symfony\\Component\\CssSelector\\Exception\\InternalErrorException' => $vendorDir . '/symfony/css-selector/Exception/InternalErrorException.php', + 'Symfony\\Component\\CssSelector\\Exception\\ParseException' => $vendorDir . '/symfony/css-selector/Exception/ParseException.php', + 'Symfony\\Component\\CssSelector\\Exception\\SyntaxErrorException' => $vendorDir . '/symfony/css-selector/Exception/SyntaxErrorException.php', + 'Symfony\\Component\\CssSelector\\Node\\AbstractNode' => $vendorDir . '/symfony/css-selector/Node/AbstractNode.php', + 'Symfony\\Component\\CssSelector\\Node\\AttributeNode' => $vendorDir . '/symfony/css-selector/Node/AttributeNode.php', + 'Symfony\\Component\\CssSelector\\Node\\ClassNode' => $vendorDir . '/symfony/css-selector/Node/ClassNode.php', + 'Symfony\\Component\\CssSelector\\Node\\CombinedSelectorNode' => $vendorDir . '/symfony/css-selector/Node/CombinedSelectorNode.php', + 'Symfony\\Component\\CssSelector\\Node\\ElementNode' => $vendorDir . '/symfony/css-selector/Node/ElementNode.php', + 'Symfony\\Component\\CssSelector\\Node\\FunctionNode' => $vendorDir . '/symfony/css-selector/Node/FunctionNode.php', + 'Symfony\\Component\\CssSelector\\Node\\HashNode' => $vendorDir . '/symfony/css-selector/Node/HashNode.php', + 'Symfony\\Component\\CssSelector\\Node\\NegationNode' => $vendorDir . '/symfony/css-selector/Node/NegationNode.php', + 'Symfony\\Component\\CssSelector\\Node\\NodeInterface' => $vendorDir . '/symfony/css-selector/Node/NodeInterface.php', + 'Symfony\\Component\\CssSelector\\Node\\PseudoNode' => $vendorDir . '/symfony/css-selector/Node/PseudoNode.php', + 'Symfony\\Component\\CssSelector\\Node\\SelectorNode' => $vendorDir . '/symfony/css-selector/Node/SelectorNode.php', + 'Symfony\\Component\\CssSelector\\Node\\Specificity' => $vendorDir . '/symfony/css-selector/Node/Specificity.php', + 'Symfony\\Component\\CssSelector\\Parser\\Handler\\CommentHandler' => $vendorDir . '/symfony/css-selector/Parser/Handler/CommentHandler.php', + 'Symfony\\Component\\CssSelector\\Parser\\Handler\\HandlerInterface' => $vendorDir . '/symfony/css-selector/Parser/Handler/HandlerInterface.php', + 'Symfony\\Component\\CssSelector\\Parser\\Handler\\HashHandler' => $vendorDir . '/symfony/css-selector/Parser/Handler/HashHandler.php', + 'Symfony\\Component\\CssSelector\\Parser\\Handler\\IdentifierHandler' => $vendorDir . '/symfony/css-selector/Parser/Handler/IdentifierHandler.php', + 'Symfony\\Component\\CssSelector\\Parser\\Handler\\NumberHandler' => $vendorDir . '/symfony/css-selector/Parser/Handler/NumberHandler.php', + 'Symfony\\Component\\CssSelector\\Parser\\Handler\\StringHandler' => $vendorDir . '/symfony/css-selector/Parser/Handler/StringHandler.php', + 'Symfony\\Component\\CssSelector\\Parser\\Handler\\WhitespaceHandler' => $vendorDir . '/symfony/css-selector/Parser/Handler/WhitespaceHandler.php', + 'Symfony\\Component\\CssSelector\\Parser\\Parser' => $vendorDir . '/symfony/css-selector/Parser/Parser.php', + 'Symfony\\Component\\CssSelector\\Parser\\ParserInterface' => $vendorDir . '/symfony/css-selector/Parser/ParserInterface.php', + 'Symfony\\Component\\CssSelector\\Parser\\Reader' => $vendorDir . '/symfony/css-selector/Parser/Reader.php', + 'Symfony\\Component\\CssSelector\\Parser\\Shortcut\\ClassParser' => $vendorDir . '/symfony/css-selector/Parser/Shortcut/ClassParser.php', + 'Symfony\\Component\\CssSelector\\Parser\\Shortcut\\ElementParser' => $vendorDir . '/symfony/css-selector/Parser/Shortcut/ElementParser.php', + 'Symfony\\Component\\CssSelector\\Parser\\Shortcut\\EmptyStringParser' => $vendorDir . '/symfony/css-selector/Parser/Shortcut/EmptyStringParser.php', + 'Symfony\\Component\\CssSelector\\Parser\\Shortcut\\HashParser' => $vendorDir . '/symfony/css-selector/Parser/Shortcut/HashParser.php', + 'Symfony\\Component\\CssSelector\\Parser\\Token' => $vendorDir . '/symfony/css-selector/Parser/Token.php', + 'Symfony\\Component\\CssSelector\\Parser\\TokenStream' => $vendorDir . '/symfony/css-selector/Parser/TokenStream.php', + 'Symfony\\Component\\CssSelector\\Parser\\Tokenizer\\Tokenizer' => $vendorDir . '/symfony/css-selector/Parser/Tokenizer/Tokenizer.php', + 'Symfony\\Component\\CssSelector\\Parser\\Tokenizer\\TokenizerEscaping' => $vendorDir . '/symfony/css-selector/Parser/Tokenizer/TokenizerEscaping.php', + 'Symfony\\Component\\CssSelector\\Parser\\Tokenizer\\TokenizerPatterns' => $vendorDir . '/symfony/css-selector/Parser/Tokenizer/TokenizerPatterns.php', + 'Symfony\\Component\\CssSelector\\XPath\\Extension\\AbstractExtension' => $vendorDir . '/symfony/css-selector/XPath/Extension/AbstractExtension.php', + 'Symfony\\Component\\CssSelector\\XPath\\Extension\\AttributeMatchingExtension' => $vendorDir . '/symfony/css-selector/XPath/Extension/AttributeMatchingExtension.php', + 'Symfony\\Component\\CssSelector\\XPath\\Extension\\CombinationExtension' => $vendorDir . '/symfony/css-selector/XPath/Extension/CombinationExtension.php', + 'Symfony\\Component\\CssSelector\\XPath\\Extension\\ExtensionInterface' => $vendorDir . '/symfony/css-selector/XPath/Extension/ExtensionInterface.php', + 'Symfony\\Component\\CssSelector\\XPath\\Extension\\FunctionExtension' => $vendorDir . '/symfony/css-selector/XPath/Extension/FunctionExtension.php', + 'Symfony\\Component\\CssSelector\\XPath\\Extension\\HtmlExtension' => $vendorDir . '/symfony/css-selector/XPath/Extension/HtmlExtension.php', + 'Symfony\\Component\\CssSelector\\XPath\\Extension\\NodeExtension' => $vendorDir . '/symfony/css-selector/XPath/Extension/NodeExtension.php', + 'Symfony\\Component\\CssSelector\\XPath\\Extension\\PseudoClassExtension' => $vendorDir . '/symfony/css-selector/XPath/Extension/PseudoClassExtension.php', + 'Symfony\\Component\\CssSelector\\XPath\\Translator' => $vendorDir . '/symfony/css-selector/XPath/Translator.php', + 'Symfony\\Component\\CssSelector\\XPath\\TranslatorInterface' => $vendorDir . '/symfony/css-selector/XPath/TranslatorInterface.php', + 'Symfony\\Component\\CssSelector\\XPath\\XPathExpr' => $vendorDir . '/symfony/css-selector/XPath/XPathExpr.php', + 'Symfony\\Component\\DomCrawler\\AbstractUriElement' => $vendorDir . '/symfony/dom-crawler/AbstractUriElement.php', + 'Symfony\\Component\\DomCrawler\\Crawler' => $vendorDir . '/symfony/dom-crawler/Crawler.php', + 'Symfony\\Component\\DomCrawler\\Field\\ChoiceFormField' => $vendorDir . '/symfony/dom-crawler/Field/ChoiceFormField.php', + 'Symfony\\Component\\DomCrawler\\Field\\FileFormField' => $vendorDir . '/symfony/dom-crawler/Field/FileFormField.php', + 'Symfony\\Component\\DomCrawler\\Field\\FormField' => $vendorDir . '/symfony/dom-crawler/Field/FormField.php', + 'Symfony\\Component\\DomCrawler\\Field\\InputFormField' => $vendorDir . '/symfony/dom-crawler/Field/InputFormField.php', + 'Symfony\\Component\\DomCrawler\\Field\\TextareaFormField' => $vendorDir . '/symfony/dom-crawler/Field/TextareaFormField.php', + 'Symfony\\Component\\DomCrawler\\Form' => $vendorDir . '/symfony/dom-crawler/Form.php', + 'Symfony\\Component\\DomCrawler\\FormFieldRegistry' => $vendorDir . '/symfony/dom-crawler/FormFieldRegistry.php', + 'Symfony\\Component\\DomCrawler\\Image' => $vendorDir . '/symfony/dom-crawler/Image.php', + 'Symfony\\Component\\DomCrawler\\Link' => $vendorDir . '/symfony/dom-crawler/Link.php', + 'Symfony\\Component\\DomCrawler\\UriResolver' => $vendorDir . '/symfony/dom-crawler/UriResolver.php', + 'Symfony\\Component\\EventDispatcher\\Attribute\\AsEventListener' => $vendorDir . '/symfony/event-dispatcher/Attribute/AsEventListener.php', + 'Symfony\\Component\\EventDispatcher\\Debug\\TraceableEventDispatcher' => $vendorDir . '/symfony/event-dispatcher/Debug/TraceableEventDispatcher.php', + 'Symfony\\Component\\EventDispatcher\\Debug\\WrappedListener' => $vendorDir . '/symfony/event-dispatcher/Debug/WrappedListener.php', + 'Symfony\\Component\\EventDispatcher\\DependencyInjection\\AddEventAliasesPass' => $vendorDir . '/symfony/event-dispatcher/DependencyInjection/AddEventAliasesPass.php', + 'Symfony\\Component\\EventDispatcher\\DependencyInjection\\RegisterListenersPass' => $vendorDir . '/symfony/event-dispatcher/DependencyInjection/RegisterListenersPass.php', + 'Symfony\\Component\\EventDispatcher\\EventDispatcher' => $vendorDir . '/symfony/event-dispatcher/EventDispatcher.php', + 'Symfony\\Component\\EventDispatcher\\EventDispatcherInterface' => $vendorDir . '/symfony/event-dispatcher/EventDispatcherInterface.php', + 'Symfony\\Component\\EventDispatcher\\EventSubscriberInterface' => $vendorDir . '/symfony/event-dispatcher/EventSubscriberInterface.php', + 'Symfony\\Component\\EventDispatcher\\GenericEvent' => $vendorDir . '/symfony/event-dispatcher/GenericEvent.php', + 'Symfony\\Component\\EventDispatcher\\ImmutableEventDispatcher' => $vendorDir . '/symfony/event-dispatcher/ImmutableEventDispatcher.php', + 'Symfony\\Component\\HttpFoundation\\AcceptHeader' => $vendorDir . '/symfony/http-foundation/AcceptHeader.php', + 'Symfony\\Component\\HttpFoundation\\AcceptHeaderItem' => $vendorDir . '/symfony/http-foundation/AcceptHeaderItem.php', + 'Symfony\\Component\\HttpFoundation\\BinaryFileResponse' => $vendorDir . '/symfony/http-foundation/BinaryFileResponse.php', + 'Symfony\\Component\\HttpFoundation\\ChainRequestMatcher' => $vendorDir . '/symfony/http-foundation/ChainRequestMatcher.php', + 'Symfony\\Component\\HttpFoundation\\Cookie' => $vendorDir . '/symfony/http-foundation/Cookie.php', + 'Symfony\\Component\\HttpFoundation\\Exception\\BadRequestException' => $vendorDir . '/symfony/http-foundation/Exception/BadRequestException.php', + 'Symfony\\Component\\HttpFoundation\\Exception\\ConflictingHeadersException' => $vendorDir . '/symfony/http-foundation/Exception/ConflictingHeadersException.php', + 'Symfony\\Component\\HttpFoundation\\Exception\\JsonException' => $vendorDir . '/symfony/http-foundation/Exception/JsonException.php', + 'Symfony\\Component\\HttpFoundation\\Exception\\RequestExceptionInterface' => $vendorDir . '/symfony/http-foundation/Exception/RequestExceptionInterface.php', + 'Symfony\\Component\\HttpFoundation\\Exception\\SessionNotFoundException' => $vendorDir . '/symfony/http-foundation/Exception/SessionNotFoundException.php', + 'Symfony\\Component\\HttpFoundation\\Exception\\SuspiciousOperationException' => $vendorDir . '/symfony/http-foundation/Exception/SuspiciousOperationException.php', + 'Symfony\\Component\\HttpFoundation\\Exception\\UnexpectedValueException' => $vendorDir . '/symfony/http-foundation/Exception/UnexpectedValueException.php', + 'Symfony\\Component\\HttpFoundation\\ExpressionRequestMatcher' => $vendorDir . '/symfony/http-foundation/ExpressionRequestMatcher.php', + 'Symfony\\Component\\HttpFoundation\\FileBag' => $vendorDir . '/symfony/http-foundation/FileBag.php', + 'Symfony\\Component\\HttpFoundation\\File\\Exception\\AccessDeniedException' => $vendorDir . '/symfony/http-foundation/File/Exception/AccessDeniedException.php', + 'Symfony\\Component\\HttpFoundation\\File\\Exception\\CannotWriteFileException' => $vendorDir . '/symfony/http-foundation/File/Exception/CannotWriteFileException.php', + 'Symfony\\Component\\HttpFoundation\\File\\Exception\\ExtensionFileException' => $vendorDir . '/symfony/http-foundation/File/Exception/ExtensionFileException.php', + 'Symfony\\Component\\HttpFoundation\\File\\Exception\\FileException' => $vendorDir . '/symfony/http-foundation/File/Exception/FileException.php', + 'Symfony\\Component\\HttpFoundation\\File\\Exception\\FileNotFoundException' => $vendorDir . '/symfony/http-foundation/File/Exception/FileNotFoundException.php', + 'Symfony\\Component\\HttpFoundation\\File\\Exception\\FormSizeFileException' => $vendorDir . '/symfony/http-foundation/File/Exception/FormSizeFileException.php', + 'Symfony\\Component\\HttpFoundation\\File\\Exception\\IniSizeFileException' => $vendorDir . '/symfony/http-foundation/File/Exception/IniSizeFileException.php', + 'Symfony\\Component\\HttpFoundation\\File\\Exception\\NoFileException' => $vendorDir . '/symfony/http-foundation/File/Exception/NoFileException.php', + 'Symfony\\Component\\HttpFoundation\\File\\Exception\\NoTmpDirFileException' => $vendorDir . '/symfony/http-foundation/File/Exception/NoTmpDirFileException.php', + 'Symfony\\Component\\HttpFoundation\\File\\Exception\\PartialFileException' => $vendorDir . '/symfony/http-foundation/File/Exception/PartialFileException.php', + 'Symfony\\Component\\HttpFoundation\\File\\Exception\\UnexpectedTypeException' => $vendorDir . '/symfony/http-foundation/File/Exception/UnexpectedTypeException.php', + 'Symfony\\Component\\HttpFoundation\\File\\Exception\\UploadException' => $vendorDir . '/symfony/http-foundation/File/Exception/UploadException.php', + 'Symfony\\Component\\HttpFoundation\\File\\File' => $vendorDir . '/symfony/http-foundation/File/File.php', + 'Symfony\\Component\\HttpFoundation\\File\\Stream' => $vendorDir . '/symfony/http-foundation/File/Stream.php', + 'Symfony\\Component\\HttpFoundation\\File\\UploadedFile' => $vendorDir . '/symfony/http-foundation/File/UploadedFile.php', + 'Symfony\\Component\\HttpFoundation\\HeaderBag' => $vendorDir . '/symfony/http-foundation/HeaderBag.php', + 'Symfony\\Component\\HttpFoundation\\HeaderUtils' => $vendorDir . '/symfony/http-foundation/HeaderUtils.php', + 'Symfony\\Component\\HttpFoundation\\InputBag' => $vendorDir . '/symfony/http-foundation/InputBag.php', + 'Symfony\\Component\\HttpFoundation\\IpUtils' => $vendorDir . '/symfony/http-foundation/IpUtils.php', + 'Symfony\\Component\\HttpFoundation\\JsonResponse' => $vendorDir . '/symfony/http-foundation/JsonResponse.php', + 'Symfony\\Component\\HttpFoundation\\ParameterBag' => $vendorDir . '/symfony/http-foundation/ParameterBag.php', + 'Symfony\\Component\\HttpFoundation\\RateLimiter\\AbstractRequestRateLimiter' => $vendorDir . '/symfony/http-foundation/RateLimiter/AbstractRequestRateLimiter.php', + 'Symfony\\Component\\HttpFoundation\\RateLimiter\\PeekableRequestRateLimiterInterface' => $vendorDir . '/symfony/http-foundation/RateLimiter/PeekableRequestRateLimiterInterface.php', + 'Symfony\\Component\\HttpFoundation\\RateLimiter\\RequestRateLimiterInterface' => $vendorDir . '/symfony/http-foundation/RateLimiter/RequestRateLimiterInterface.php', + 'Symfony\\Component\\HttpFoundation\\RedirectResponse' => $vendorDir . '/symfony/http-foundation/RedirectResponse.php', + 'Symfony\\Component\\HttpFoundation\\Request' => $vendorDir . '/symfony/http-foundation/Request.php', + 'Symfony\\Component\\HttpFoundation\\RequestMatcher' => $vendorDir . '/symfony/http-foundation/RequestMatcher.php', + 'Symfony\\Component\\HttpFoundation\\RequestMatcherInterface' => $vendorDir . '/symfony/http-foundation/RequestMatcherInterface.php', + 'Symfony\\Component\\HttpFoundation\\RequestMatcher\\AttributesRequestMatcher' => $vendorDir . '/symfony/http-foundation/RequestMatcher/AttributesRequestMatcher.php', + 'Symfony\\Component\\HttpFoundation\\RequestMatcher\\ExpressionRequestMatcher' => $vendorDir . '/symfony/http-foundation/RequestMatcher/ExpressionRequestMatcher.php', + 'Symfony\\Component\\HttpFoundation\\RequestMatcher\\HostRequestMatcher' => $vendorDir . '/symfony/http-foundation/RequestMatcher/HostRequestMatcher.php', + 'Symfony\\Component\\HttpFoundation\\RequestMatcher\\IpsRequestMatcher' => $vendorDir . '/symfony/http-foundation/RequestMatcher/IpsRequestMatcher.php', + 'Symfony\\Component\\HttpFoundation\\RequestMatcher\\IsJsonRequestMatcher' => $vendorDir . '/symfony/http-foundation/RequestMatcher/IsJsonRequestMatcher.php', + 'Symfony\\Component\\HttpFoundation\\RequestMatcher\\MethodRequestMatcher' => $vendorDir . '/symfony/http-foundation/RequestMatcher/MethodRequestMatcher.php', + 'Symfony\\Component\\HttpFoundation\\RequestMatcher\\PathRequestMatcher' => $vendorDir . '/symfony/http-foundation/RequestMatcher/PathRequestMatcher.php', + 'Symfony\\Component\\HttpFoundation\\RequestMatcher\\PortRequestMatcher' => $vendorDir . '/symfony/http-foundation/RequestMatcher/PortRequestMatcher.php', + 'Symfony\\Component\\HttpFoundation\\RequestMatcher\\SchemeRequestMatcher' => $vendorDir . '/symfony/http-foundation/RequestMatcher/SchemeRequestMatcher.php', + 'Symfony\\Component\\HttpFoundation\\RequestStack' => $vendorDir . '/symfony/http-foundation/RequestStack.php', + 'Symfony\\Component\\HttpFoundation\\Response' => $vendorDir . '/symfony/http-foundation/Response.php', + 'Symfony\\Component\\HttpFoundation\\ResponseHeaderBag' => $vendorDir . '/symfony/http-foundation/ResponseHeaderBag.php', + 'Symfony\\Component\\HttpFoundation\\ServerBag' => $vendorDir . '/symfony/http-foundation/ServerBag.php', + 'Symfony\\Component\\HttpFoundation\\Session\\Attribute\\AttributeBag' => $vendorDir . '/symfony/http-foundation/Session/Attribute/AttributeBag.php', + 'Symfony\\Component\\HttpFoundation\\Session\\Attribute\\AttributeBagInterface' => $vendorDir . '/symfony/http-foundation/Session/Attribute/AttributeBagInterface.php', + 'Symfony\\Component\\HttpFoundation\\Session\\FlashBagAwareSessionInterface' => $vendorDir . '/symfony/http-foundation/Session/FlashBagAwareSessionInterface.php', + 'Symfony\\Component\\HttpFoundation\\Session\\Flash\\AutoExpireFlashBag' => $vendorDir . '/symfony/http-foundation/Session/Flash/AutoExpireFlashBag.php', + 'Symfony\\Component\\HttpFoundation\\Session\\Flash\\FlashBag' => $vendorDir . '/symfony/http-foundation/Session/Flash/FlashBag.php', + 'Symfony\\Component\\HttpFoundation\\Session\\Flash\\FlashBagInterface' => $vendorDir . '/symfony/http-foundation/Session/Flash/FlashBagInterface.php', + 'Symfony\\Component\\HttpFoundation\\Session\\Session' => $vendorDir . '/symfony/http-foundation/Session/Session.php', + 'Symfony\\Component\\HttpFoundation\\Session\\SessionBagInterface' => $vendorDir . '/symfony/http-foundation/Session/SessionBagInterface.php', + 'Symfony\\Component\\HttpFoundation\\Session\\SessionBagProxy' => $vendorDir . '/symfony/http-foundation/Session/SessionBagProxy.php', + 'Symfony\\Component\\HttpFoundation\\Session\\SessionFactory' => $vendorDir . '/symfony/http-foundation/Session/SessionFactory.php', + 'Symfony\\Component\\HttpFoundation\\Session\\SessionFactoryInterface' => $vendorDir . '/symfony/http-foundation/Session/SessionFactoryInterface.php', + 'Symfony\\Component\\HttpFoundation\\Session\\SessionInterface' => $vendorDir . '/symfony/http-foundation/Session/SessionInterface.php', + 'Symfony\\Component\\HttpFoundation\\Session\\SessionUtils' => $vendorDir . '/symfony/http-foundation/Session/SessionUtils.php', + 'Symfony\\Component\\HttpFoundation\\Session\\Storage\\Handler\\AbstractSessionHandler' => $vendorDir . '/symfony/http-foundation/Session/Storage/Handler/AbstractSessionHandler.php', + 'Symfony\\Component\\HttpFoundation\\Session\\Storage\\Handler\\IdentityMarshaller' => $vendorDir . '/symfony/http-foundation/Session/Storage/Handler/IdentityMarshaller.php', + 'Symfony\\Component\\HttpFoundation\\Session\\Storage\\Handler\\MarshallingSessionHandler' => $vendorDir . '/symfony/http-foundation/Session/Storage/Handler/MarshallingSessionHandler.php', + 'Symfony\\Component\\HttpFoundation\\Session\\Storage\\Handler\\MemcachedSessionHandler' => $vendorDir . '/symfony/http-foundation/Session/Storage/Handler/MemcachedSessionHandler.php', + 'Symfony\\Component\\HttpFoundation\\Session\\Storage\\Handler\\MigratingSessionHandler' => $vendorDir . '/symfony/http-foundation/Session/Storage/Handler/MigratingSessionHandler.php', + 'Symfony\\Component\\HttpFoundation\\Session\\Storage\\Handler\\MongoDbSessionHandler' => $vendorDir . '/symfony/http-foundation/Session/Storage/Handler/MongoDbSessionHandler.php', + 'Symfony\\Component\\HttpFoundation\\Session\\Storage\\Handler\\NativeFileSessionHandler' => $vendorDir . '/symfony/http-foundation/Session/Storage/Handler/NativeFileSessionHandler.php', + 'Symfony\\Component\\HttpFoundation\\Session\\Storage\\Handler\\NullSessionHandler' => $vendorDir . '/symfony/http-foundation/Session/Storage/Handler/NullSessionHandler.php', + 'Symfony\\Component\\HttpFoundation\\Session\\Storage\\Handler\\PdoSessionHandler' => $vendorDir . '/symfony/http-foundation/Session/Storage/Handler/PdoSessionHandler.php', + 'Symfony\\Component\\HttpFoundation\\Session\\Storage\\Handler\\RedisSessionHandler' => $vendorDir . '/symfony/http-foundation/Session/Storage/Handler/RedisSessionHandler.php', + 'Symfony\\Component\\HttpFoundation\\Session\\Storage\\Handler\\SessionHandlerFactory' => $vendorDir . '/symfony/http-foundation/Session/Storage/Handler/SessionHandlerFactory.php', + 'Symfony\\Component\\HttpFoundation\\Session\\Storage\\Handler\\StrictSessionHandler' => $vendorDir . '/symfony/http-foundation/Session/Storage/Handler/StrictSessionHandler.php', + 'Symfony\\Component\\HttpFoundation\\Session\\Storage\\MetadataBag' => $vendorDir . '/symfony/http-foundation/Session/Storage/MetadataBag.php', + 'Symfony\\Component\\HttpFoundation\\Session\\Storage\\MockArraySessionStorage' => $vendorDir . '/symfony/http-foundation/Session/Storage/MockArraySessionStorage.php', + 'Symfony\\Component\\HttpFoundation\\Session\\Storage\\MockFileSessionStorage' => $vendorDir . '/symfony/http-foundation/Session/Storage/MockFileSessionStorage.php', + 'Symfony\\Component\\HttpFoundation\\Session\\Storage\\MockFileSessionStorageFactory' => $vendorDir . '/symfony/http-foundation/Session/Storage/MockFileSessionStorageFactory.php', + 'Symfony\\Component\\HttpFoundation\\Session\\Storage\\NativeSessionStorage' => $vendorDir . '/symfony/http-foundation/Session/Storage/NativeSessionStorage.php', + 'Symfony\\Component\\HttpFoundation\\Session\\Storage\\NativeSessionStorageFactory' => $vendorDir . '/symfony/http-foundation/Session/Storage/NativeSessionStorageFactory.php', + 'Symfony\\Component\\HttpFoundation\\Session\\Storage\\PhpBridgeSessionStorage' => $vendorDir . '/symfony/http-foundation/Session/Storage/PhpBridgeSessionStorage.php', + 'Symfony\\Component\\HttpFoundation\\Session\\Storage\\PhpBridgeSessionStorageFactory' => $vendorDir . '/symfony/http-foundation/Session/Storage/PhpBridgeSessionStorageFactory.php', + 'Symfony\\Component\\HttpFoundation\\Session\\Storage\\Proxy\\AbstractProxy' => $vendorDir . '/symfony/http-foundation/Session/Storage/Proxy/AbstractProxy.php', + 'Symfony\\Component\\HttpFoundation\\Session\\Storage\\Proxy\\SessionHandlerProxy' => $vendorDir . '/symfony/http-foundation/Session/Storage/Proxy/SessionHandlerProxy.php', + 'Symfony\\Component\\HttpFoundation\\Session\\Storage\\SessionStorageFactoryInterface' => $vendorDir . '/symfony/http-foundation/Session/Storage/SessionStorageFactoryInterface.php', + 'Symfony\\Component\\HttpFoundation\\Session\\Storage\\SessionStorageInterface' => $vendorDir . '/symfony/http-foundation/Session/Storage/SessionStorageInterface.php', + 'Symfony\\Component\\HttpFoundation\\StreamedJsonResponse' => $vendorDir . '/symfony/http-foundation/StreamedJsonResponse.php', + 'Symfony\\Component\\HttpFoundation\\StreamedResponse' => $vendorDir . '/symfony/http-foundation/StreamedResponse.php', + 'Symfony\\Component\\HttpFoundation\\UriSigner' => $vendorDir . '/symfony/http-foundation/UriSigner.php', + 'Symfony\\Component\\HttpFoundation\\UrlHelper' => $vendorDir . '/symfony/http-foundation/UrlHelper.php', + 'Symfony\\Component\\Mailer\\Command\\MailerTestCommand' => $vendorDir . '/symfony/mailer/Command/MailerTestCommand.php', + 'Symfony\\Component\\Mailer\\DataCollector\\MessageDataCollector' => $vendorDir . '/symfony/mailer/DataCollector/MessageDataCollector.php', + 'Symfony\\Component\\Mailer\\DelayedEnvelope' => $vendorDir . '/symfony/mailer/DelayedEnvelope.php', + 'Symfony\\Component\\Mailer\\Envelope' => $vendorDir . '/symfony/mailer/Envelope.php', + 'Symfony\\Component\\Mailer\\EventListener\\EnvelopeListener' => $vendorDir . '/symfony/mailer/EventListener/EnvelopeListener.php', + 'Symfony\\Component\\Mailer\\EventListener\\MessageListener' => $vendorDir . '/symfony/mailer/EventListener/MessageListener.php', + 'Symfony\\Component\\Mailer\\EventListener\\MessageLoggerListener' => $vendorDir . '/symfony/mailer/EventListener/MessageLoggerListener.php', + 'Symfony\\Component\\Mailer\\EventListener\\MessengerTransportListener' => $vendorDir . '/symfony/mailer/EventListener/MessengerTransportListener.php', + 'Symfony\\Component\\Mailer\\Event\\FailedMessageEvent' => $vendorDir . '/symfony/mailer/Event/FailedMessageEvent.php', + 'Symfony\\Component\\Mailer\\Event\\MessageEvent' => $vendorDir . '/symfony/mailer/Event/MessageEvent.php', + 'Symfony\\Component\\Mailer\\Event\\MessageEvents' => $vendorDir . '/symfony/mailer/Event/MessageEvents.php', + 'Symfony\\Component\\Mailer\\Event\\SentMessageEvent' => $vendorDir . '/symfony/mailer/Event/SentMessageEvent.php', + 'Symfony\\Component\\Mailer\\Exception\\ExceptionInterface' => $vendorDir . '/symfony/mailer/Exception/ExceptionInterface.php', + 'Symfony\\Component\\Mailer\\Exception\\HttpTransportException' => $vendorDir . '/symfony/mailer/Exception/HttpTransportException.php', + 'Symfony\\Component\\Mailer\\Exception\\IncompleteDsnException' => $vendorDir . '/symfony/mailer/Exception/IncompleteDsnException.php', + 'Symfony\\Component\\Mailer\\Exception\\InvalidArgumentException' => $vendorDir . '/symfony/mailer/Exception/InvalidArgumentException.php', + 'Symfony\\Component\\Mailer\\Exception\\LogicException' => $vendorDir . '/symfony/mailer/Exception/LogicException.php', + 'Symfony\\Component\\Mailer\\Exception\\RuntimeException' => $vendorDir . '/symfony/mailer/Exception/RuntimeException.php', + 'Symfony\\Component\\Mailer\\Exception\\TransportException' => $vendorDir . '/symfony/mailer/Exception/TransportException.php', + 'Symfony\\Component\\Mailer\\Exception\\TransportExceptionInterface' => $vendorDir . '/symfony/mailer/Exception/TransportExceptionInterface.php', + 'Symfony\\Component\\Mailer\\Exception\\UnexpectedResponseException' => $vendorDir . '/symfony/mailer/Exception/UnexpectedResponseException.php', + 'Symfony\\Component\\Mailer\\Exception\\UnsupportedSchemeException' => $vendorDir . '/symfony/mailer/Exception/UnsupportedSchemeException.php', + 'Symfony\\Component\\Mailer\\Header\\MetadataHeader' => $vendorDir . '/symfony/mailer/Header/MetadataHeader.php', + 'Symfony\\Component\\Mailer\\Header\\TagHeader' => $vendorDir . '/symfony/mailer/Header/TagHeader.php', + 'Symfony\\Component\\Mailer\\Mailer' => $vendorDir . '/symfony/mailer/Mailer.php', + 'Symfony\\Component\\Mailer\\MailerInterface' => $vendorDir . '/symfony/mailer/MailerInterface.php', + 'Symfony\\Component\\Mailer\\Messenger\\MessageHandler' => $vendorDir . '/symfony/mailer/Messenger/MessageHandler.php', + 'Symfony\\Component\\Mailer\\Messenger\\SendEmailMessage' => $vendorDir . '/symfony/mailer/Messenger/SendEmailMessage.php', + 'Symfony\\Component\\Mailer\\SentMessage' => $vendorDir . '/symfony/mailer/SentMessage.php', + 'Symfony\\Component\\Mailer\\Transport' => $vendorDir . '/symfony/mailer/Transport.php', + 'Symfony\\Component\\Mailer\\Transport\\AbstractApiTransport' => $vendorDir . '/symfony/mailer/Transport/AbstractApiTransport.php', + 'Symfony\\Component\\Mailer\\Transport\\AbstractHttpTransport' => $vendorDir . '/symfony/mailer/Transport/AbstractHttpTransport.php', + 'Symfony\\Component\\Mailer\\Transport\\AbstractTransport' => $vendorDir . '/symfony/mailer/Transport/AbstractTransport.php', + 'Symfony\\Component\\Mailer\\Transport\\AbstractTransportFactory' => $vendorDir . '/symfony/mailer/Transport/AbstractTransportFactory.php', + 'Symfony\\Component\\Mailer\\Transport\\Dsn' => $vendorDir . '/symfony/mailer/Transport/Dsn.php', + 'Symfony\\Component\\Mailer\\Transport\\FailoverTransport' => $vendorDir . '/symfony/mailer/Transport/FailoverTransport.php', + 'Symfony\\Component\\Mailer\\Transport\\NativeTransportFactory' => $vendorDir . '/symfony/mailer/Transport/NativeTransportFactory.php', + 'Symfony\\Component\\Mailer\\Transport\\NullTransport' => $vendorDir . '/symfony/mailer/Transport/NullTransport.php', + 'Symfony\\Component\\Mailer\\Transport\\NullTransportFactory' => $vendorDir . '/symfony/mailer/Transport/NullTransportFactory.php', + 'Symfony\\Component\\Mailer\\Transport\\RoundRobinTransport' => $vendorDir . '/symfony/mailer/Transport/RoundRobinTransport.php', + 'Symfony\\Component\\Mailer\\Transport\\SendmailTransport' => $vendorDir . '/symfony/mailer/Transport/SendmailTransport.php', + 'Symfony\\Component\\Mailer\\Transport\\SendmailTransportFactory' => $vendorDir . '/symfony/mailer/Transport/SendmailTransportFactory.php', + 'Symfony\\Component\\Mailer\\Transport\\Smtp\\Auth\\AuthenticatorInterface' => $vendorDir . '/symfony/mailer/Transport/Smtp/Auth/AuthenticatorInterface.php', + 'Symfony\\Component\\Mailer\\Transport\\Smtp\\Auth\\CramMd5Authenticator' => $vendorDir . '/symfony/mailer/Transport/Smtp/Auth/CramMd5Authenticator.php', + 'Symfony\\Component\\Mailer\\Transport\\Smtp\\Auth\\LoginAuthenticator' => $vendorDir . '/symfony/mailer/Transport/Smtp/Auth/LoginAuthenticator.php', + 'Symfony\\Component\\Mailer\\Transport\\Smtp\\Auth\\PlainAuthenticator' => $vendorDir . '/symfony/mailer/Transport/Smtp/Auth/PlainAuthenticator.php', + 'Symfony\\Component\\Mailer\\Transport\\Smtp\\Auth\\XOAuth2Authenticator' => $vendorDir . '/symfony/mailer/Transport/Smtp/Auth/XOAuth2Authenticator.php', + 'Symfony\\Component\\Mailer\\Transport\\Smtp\\EsmtpTransport' => $vendorDir . '/symfony/mailer/Transport/Smtp/EsmtpTransport.php', + 'Symfony\\Component\\Mailer\\Transport\\Smtp\\EsmtpTransportFactory' => $vendorDir . '/symfony/mailer/Transport/Smtp/EsmtpTransportFactory.php', + 'Symfony\\Component\\Mailer\\Transport\\Smtp\\SmtpTransport' => $vendorDir . '/symfony/mailer/Transport/Smtp/SmtpTransport.php', + 'Symfony\\Component\\Mailer\\Transport\\Smtp\\Stream\\AbstractStream' => $vendorDir . '/symfony/mailer/Transport/Smtp/Stream/AbstractStream.php', + 'Symfony\\Component\\Mailer\\Transport\\Smtp\\Stream\\ProcessStream' => $vendorDir . '/symfony/mailer/Transport/Smtp/Stream/ProcessStream.php', + 'Symfony\\Component\\Mailer\\Transport\\Smtp\\Stream\\SocketStream' => $vendorDir . '/symfony/mailer/Transport/Smtp/Stream/SocketStream.php', + 'Symfony\\Component\\Mailer\\Transport\\TransportFactoryInterface' => $vendorDir . '/symfony/mailer/Transport/TransportFactoryInterface.php', + 'Symfony\\Component\\Mailer\\Transport\\TransportInterface' => $vendorDir . '/symfony/mailer/Transport/TransportInterface.php', + 'Symfony\\Component\\Mailer\\Transport\\Transports' => $vendorDir . '/symfony/mailer/Transport/Transports.php', + 'Symfony\\Component\\Mime\\Address' => $vendorDir . '/symfony/mime/Address.php', + 'Symfony\\Component\\Mime\\BodyRendererInterface' => $vendorDir . '/symfony/mime/BodyRendererInterface.php', + 'Symfony\\Component\\Mime\\CharacterStream' => $vendorDir . '/symfony/mime/CharacterStream.php', + 'Symfony\\Component\\Mime\\Crypto\\DkimOptions' => $vendorDir . '/symfony/mime/Crypto/DkimOptions.php', + 'Symfony\\Component\\Mime\\Crypto\\DkimSigner' => $vendorDir . '/symfony/mime/Crypto/DkimSigner.php', + 'Symfony\\Component\\Mime\\Crypto\\SMime' => $vendorDir . '/symfony/mime/Crypto/SMime.php', + 'Symfony\\Component\\Mime\\Crypto\\SMimeEncrypter' => $vendorDir . '/symfony/mime/Crypto/SMimeEncrypter.php', + 'Symfony\\Component\\Mime\\Crypto\\SMimeSigner' => $vendorDir . '/symfony/mime/Crypto/SMimeSigner.php', + 'Symfony\\Component\\Mime\\DependencyInjection\\AddMimeTypeGuesserPass' => $vendorDir . '/symfony/mime/DependencyInjection/AddMimeTypeGuesserPass.php', + 'Symfony\\Component\\Mime\\DraftEmail' => $vendorDir . '/symfony/mime/DraftEmail.php', + 'Symfony\\Component\\Mime\\Email' => $vendorDir . '/symfony/mime/Email.php', + 'Symfony\\Component\\Mime\\Encoder\\AddressEncoderInterface' => $vendorDir . '/symfony/mime/Encoder/AddressEncoderInterface.php', + 'Symfony\\Component\\Mime\\Encoder\\Base64ContentEncoder' => $vendorDir . '/symfony/mime/Encoder/Base64ContentEncoder.php', + 'Symfony\\Component\\Mime\\Encoder\\Base64Encoder' => $vendorDir . '/symfony/mime/Encoder/Base64Encoder.php', + 'Symfony\\Component\\Mime\\Encoder\\Base64MimeHeaderEncoder' => $vendorDir . '/symfony/mime/Encoder/Base64MimeHeaderEncoder.php', + 'Symfony\\Component\\Mime\\Encoder\\ContentEncoderInterface' => $vendorDir . '/symfony/mime/Encoder/ContentEncoderInterface.php', + 'Symfony\\Component\\Mime\\Encoder\\EightBitContentEncoder' => $vendorDir . '/symfony/mime/Encoder/EightBitContentEncoder.php', + 'Symfony\\Component\\Mime\\Encoder\\EncoderInterface' => $vendorDir . '/symfony/mime/Encoder/EncoderInterface.php', + 'Symfony\\Component\\Mime\\Encoder\\IdnAddressEncoder' => $vendorDir . '/symfony/mime/Encoder/IdnAddressEncoder.php', + 'Symfony\\Component\\Mime\\Encoder\\MimeHeaderEncoderInterface' => $vendorDir . '/symfony/mime/Encoder/MimeHeaderEncoderInterface.php', + 'Symfony\\Component\\Mime\\Encoder\\QpContentEncoder' => $vendorDir . '/symfony/mime/Encoder/QpContentEncoder.php', + 'Symfony\\Component\\Mime\\Encoder\\QpEncoder' => $vendorDir . '/symfony/mime/Encoder/QpEncoder.php', + 'Symfony\\Component\\Mime\\Encoder\\QpMimeHeaderEncoder' => $vendorDir . '/symfony/mime/Encoder/QpMimeHeaderEncoder.php', + 'Symfony\\Component\\Mime\\Encoder\\Rfc2231Encoder' => $vendorDir . '/symfony/mime/Encoder/Rfc2231Encoder.php', + 'Symfony\\Component\\Mime\\Exception\\AddressEncoderException' => $vendorDir . '/symfony/mime/Exception/AddressEncoderException.php', + 'Symfony\\Component\\Mime\\Exception\\ExceptionInterface' => $vendorDir . '/symfony/mime/Exception/ExceptionInterface.php', + 'Symfony\\Component\\Mime\\Exception\\InvalidArgumentException' => $vendorDir . '/symfony/mime/Exception/InvalidArgumentException.php', + 'Symfony\\Component\\Mime\\Exception\\LogicException' => $vendorDir . '/symfony/mime/Exception/LogicException.php', + 'Symfony\\Component\\Mime\\Exception\\RfcComplianceException' => $vendorDir . '/symfony/mime/Exception/RfcComplianceException.php', + 'Symfony\\Component\\Mime\\Exception\\RuntimeException' => $vendorDir . '/symfony/mime/Exception/RuntimeException.php', + 'Symfony\\Component\\Mime\\FileBinaryMimeTypeGuesser' => $vendorDir . '/symfony/mime/FileBinaryMimeTypeGuesser.php', + 'Symfony\\Component\\Mime\\FileinfoMimeTypeGuesser' => $vendorDir . '/symfony/mime/FileinfoMimeTypeGuesser.php', + 'Symfony\\Component\\Mime\\Header\\AbstractHeader' => $vendorDir . '/symfony/mime/Header/AbstractHeader.php', + 'Symfony\\Component\\Mime\\Header\\DateHeader' => $vendorDir . '/symfony/mime/Header/DateHeader.php', + 'Symfony\\Component\\Mime\\Header\\HeaderInterface' => $vendorDir . '/symfony/mime/Header/HeaderInterface.php', + 'Symfony\\Component\\Mime\\Header\\Headers' => $vendorDir . '/symfony/mime/Header/Headers.php', + 'Symfony\\Component\\Mime\\Header\\IdentificationHeader' => $vendorDir . '/symfony/mime/Header/IdentificationHeader.php', + 'Symfony\\Component\\Mime\\Header\\MailboxHeader' => $vendorDir . '/symfony/mime/Header/MailboxHeader.php', + 'Symfony\\Component\\Mime\\Header\\MailboxListHeader' => $vendorDir . '/symfony/mime/Header/MailboxListHeader.php', + 'Symfony\\Component\\Mime\\Header\\ParameterizedHeader' => $vendorDir . '/symfony/mime/Header/ParameterizedHeader.php', + 'Symfony\\Component\\Mime\\Header\\PathHeader' => $vendorDir . '/symfony/mime/Header/PathHeader.php', + 'Symfony\\Component\\Mime\\Header\\UnstructuredHeader' => $vendorDir . '/symfony/mime/Header/UnstructuredHeader.php', + 'Symfony\\Component\\Mime\\HtmlToTextConverter\\DefaultHtmlToTextConverter' => $vendorDir . '/symfony/mime/HtmlToTextConverter/DefaultHtmlToTextConverter.php', + 'Symfony\\Component\\Mime\\HtmlToTextConverter\\HtmlToTextConverterInterface' => $vendorDir . '/symfony/mime/HtmlToTextConverter/HtmlToTextConverterInterface.php', + 'Symfony\\Component\\Mime\\HtmlToTextConverter\\LeagueHtmlToMarkdownConverter' => $vendorDir . '/symfony/mime/HtmlToTextConverter/LeagueHtmlToMarkdownConverter.php', + 'Symfony\\Component\\Mime\\Message' => $vendorDir . '/symfony/mime/Message.php', + 'Symfony\\Component\\Mime\\MessageConverter' => $vendorDir . '/symfony/mime/MessageConverter.php', + 'Symfony\\Component\\Mime\\MimeTypeGuesserInterface' => $vendorDir . '/symfony/mime/MimeTypeGuesserInterface.php', + 'Symfony\\Component\\Mime\\MimeTypes' => $vendorDir . '/symfony/mime/MimeTypes.php', + 'Symfony\\Component\\Mime\\MimeTypesInterface' => $vendorDir . '/symfony/mime/MimeTypesInterface.php', + 'Symfony\\Component\\Mime\\Part\\AbstractMultipartPart' => $vendorDir . '/symfony/mime/Part/AbstractMultipartPart.php', + 'Symfony\\Component\\Mime\\Part\\AbstractPart' => $vendorDir . '/symfony/mime/Part/AbstractPart.php', + 'Symfony\\Component\\Mime\\Part\\DataPart' => $vendorDir . '/symfony/mime/Part/DataPart.php', + 'Symfony\\Component\\Mime\\Part\\File' => $vendorDir . '/symfony/mime/Part/File.php', + 'Symfony\\Component\\Mime\\Part\\MessagePart' => $vendorDir . '/symfony/mime/Part/MessagePart.php', + 'Symfony\\Component\\Mime\\Part\\Multipart\\AlternativePart' => $vendorDir . '/symfony/mime/Part/Multipart/AlternativePart.php', + 'Symfony\\Component\\Mime\\Part\\Multipart\\DigestPart' => $vendorDir . '/symfony/mime/Part/Multipart/DigestPart.php', + 'Symfony\\Component\\Mime\\Part\\Multipart\\FormDataPart' => $vendorDir . '/symfony/mime/Part/Multipart/FormDataPart.php', + 'Symfony\\Component\\Mime\\Part\\Multipart\\MixedPart' => $vendorDir . '/symfony/mime/Part/Multipart/MixedPart.php', + 'Symfony\\Component\\Mime\\Part\\Multipart\\RelatedPart' => $vendorDir . '/symfony/mime/Part/Multipart/RelatedPart.php', + 'Symfony\\Component\\Mime\\Part\\SMimePart' => $vendorDir . '/symfony/mime/Part/SMimePart.php', + 'Symfony\\Component\\Mime\\Part\\TextPart' => $vendorDir . '/symfony/mime/Part/TextPart.php', + 'Symfony\\Component\\Mime\\RawMessage' => $vendorDir . '/symfony/mime/RawMessage.php', + 'Symfony\\Component\\Process\\Exception\\ExceptionInterface' => $vendorDir . '/symfony/process/Exception/ExceptionInterface.php', + 'Symfony\\Component\\Process\\Exception\\InvalidArgumentException' => $vendorDir . '/symfony/process/Exception/InvalidArgumentException.php', + 'Symfony\\Component\\Process\\Exception\\LogicException' => $vendorDir . '/symfony/process/Exception/LogicException.php', + 'Symfony\\Component\\Process\\Exception\\ProcessFailedException' => $vendorDir . '/symfony/process/Exception/ProcessFailedException.php', + 'Symfony\\Component\\Process\\Exception\\ProcessSignaledException' => $vendorDir . '/symfony/process/Exception/ProcessSignaledException.php', + 'Symfony\\Component\\Process\\Exception\\ProcessTimedOutException' => $vendorDir . '/symfony/process/Exception/ProcessTimedOutException.php', + 'Symfony\\Component\\Process\\Exception\\RunProcessFailedException' => $vendorDir . '/symfony/process/Exception/RunProcessFailedException.php', + 'Symfony\\Component\\Process\\Exception\\RuntimeException' => $vendorDir . '/symfony/process/Exception/RuntimeException.php', + 'Symfony\\Component\\Process\\ExecutableFinder' => $vendorDir . '/symfony/process/ExecutableFinder.php', + 'Symfony\\Component\\Process\\InputStream' => $vendorDir . '/symfony/process/InputStream.php', + 'Symfony\\Component\\Process\\Messenger\\RunProcessContext' => $vendorDir . '/symfony/process/Messenger/RunProcessContext.php', + 'Symfony\\Component\\Process\\Messenger\\RunProcessMessage' => $vendorDir . '/symfony/process/Messenger/RunProcessMessage.php', + 'Symfony\\Component\\Process\\Messenger\\RunProcessMessageHandler' => $vendorDir . '/symfony/process/Messenger/RunProcessMessageHandler.php', + 'Symfony\\Component\\Process\\PhpExecutableFinder' => $vendorDir . '/symfony/process/PhpExecutableFinder.php', + 'Symfony\\Component\\Process\\PhpProcess' => $vendorDir . '/symfony/process/PhpProcess.php', + 'Symfony\\Component\\Process\\PhpSubprocess' => $vendorDir . '/symfony/process/PhpSubprocess.php', + 'Symfony\\Component\\Process\\Pipes\\AbstractPipes' => $vendorDir . '/symfony/process/Pipes/AbstractPipes.php', + 'Symfony\\Component\\Process\\Pipes\\PipesInterface' => $vendorDir . '/symfony/process/Pipes/PipesInterface.php', + 'Symfony\\Component\\Process\\Pipes\\UnixPipes' => $vendorDir . '/symfony/process/Pipes/UnixPipes.php', + 'Symfony\\Component\\Process\\Pipes\\WindowsPipes' => $vendorDir . '/symfony/process/Pipes/WindowsPipes.php', + 'Symfony\\Component\\Process\\Process' => $vendorDir . '/symfony/process/Process.php', + 'Symfony\\Component\\Process\\ProcessUtils' => $vendorDir . '/symfony/process/ProcessUtils.php', + 'Symfony\\Component\\Routing\\Alias' => $vendorDir . '/symfony/routing/Alias.php', + 'Symfony\\Component\\Routing\\Annotation\\Route' => $vendorDir . '/symfony/routing/Annotation/Route.php', + 'Symfony\\Component\\Routing\\Attribute\\Route' => $vendorDir . '/symfony/routing/Attribute/Route.php', + 'Symfony\\Component\\Routing\\CompiledRoute' => $vendorDir . '/symfony/routing/CompiledRoute.php', + 'Symfony\\Component\\Routing\\DependencyInjection\\AddExpressionLanguageProvidersPass' => $vendorDir . '/symfony/routing/DependencyInjection/AddExpressionLanguageProvidersPass.php', + 'Symfony\\Component\\Routing\\DependencyInjection\\RoutingResolverPass' => $vendorDir . '/symfony/routing/DependencyInjection/RoutingResolverPass.php', + 'Symfony\\Component\\Routing\\Exception\\ExceptionInterface' => $vendorDir . '/symfony/routing/Exception/ExceptionInterface.php', + 'Symfony\\Component\\Routing\\Exception\\InvalidArgumentException' => $vendorDir . '/symfony/routing/Exception/InvalidArgumentException.php', + 'Symfony\\Component\\Routing\\Exception\\InvalidParameterException' => $vendorDir . '/symfony/routing/Exception/InvalidParameterException.php', + 'Symfony\\Component\\Routing\\Exception\\MethodNotAllowedException' => $vendorDir . '/symfony/routing/Exception/MethodNotAllowedException.php', + 'Symfony\\Component\\Routing\\Exception\\MissingMandatoryParametersException' => $vendorDir . '/symfony/routing/Exception/MissingMandatoryParametersException.php', + 'Symfony\\Component\\Routing\\Exception\\NoConfigurationException' => $vendorDir . '/symfony/routing/Exception/NoConfigurationException.php', + 'Symfony\\Component\\Routing\\Exception\\ResourceNotFoundException' => $vendorDir . '/symfony/routing/Exception/ResourceNotFoundException.php', + 'Symfony\\Component\\Routing\\Exception\\RouteCircularReferenceException' => $vendorDir . '/symfony/routing/Exception/RouteCircularReferenceException.php', + 'Symfony\\Component\\Routing\\Exception\\RouteNotFoundException' => $vendorDir . '/symfony/routing/Exception/RouteNotFoundException.php', + 'Symfony\\Component\\Routing\\Exception\\RuntimeException' => $vendorDir . '/symfony/routing/Exception/RuntimeException.php', + 'Symfony\\Component\\Routing\\Generator\\CompiledUrlGenerator' => $vendorDir . '/symfony/routing/Generator/CompiledUrlGenerator.php', + 'Symfony\\Component\\Routing\\Generator\\ConfigurableRequirementsInterface' => $vendorDir . '/symfony/routing/Generator/ConfigurableRequirementsInterface.php', + 'Symfony\\Component\\Routing\\Generator\\Dumper\\CompiledUrlGeneratorDumper' => $vendorDir . '/symfony/routing/Generator/Dumper/CompiledUrlGeneratorDumper.php', + 'Symfony\\Component\\Routing\\Generator\\Dumper\\GeneratorDumper' => $vendorDir . '/symfony/routing/Generator/Dumper/GeneratorDumper.php', + 'Symfony\\Component\\Routing\\Generator\\Dumper\\GeneratorDumperInterface' => $vendorDir . '/symfony/routing/Generator/Dumper/GeneratorDumperInterface.php', + 'Symfony\\Component\\Routing\\Generator\\UrlGenerator' => $vendorDir . '/symfony/routing/Generator/UrlGenerator.php', + 'Symfony\\Component\\Routing\\Generator\\UrlGeneratorInterface' => $vendorDir . '/symfony/routing/Generator/UrlGeneratorInterface.php', + 'Symfony\\Component\\Routing\\Loader\\AnnotationClassLoader' => $vendorDir . '/symfony/routing/Loader/AnnotationClassLoader.php', + 'Symfony\\Component\\Routing\\Loader\\AnnotationDirectoryLoader' => $vendorDir . '/symfony/routing/Loader/AnnotationDirectoryLoader.php', + 'Symfony\\Component\\Routing\\Loader\\AnnotationFileLoader' => $vendorDir . '/symfony/routing/Loader/AnnotationFileLoader.php', + 'Symfony\\Component\\Routing\\Loader\\AttributeClassLoader' => $vendorDir . '/symfony/routing/Loader/AttributeClassLoader.php', + 'Symfony\\Component\\Routing\\Loader\\AttributeDirectoryLoader' => $vendorDir . '/symfony/routing/Loader/AttributeDirectoryLoader.php', + 'Symfony\\Component\\Routing\\Loader\\AttributeFileLoader' => $vendorDir . '/symfony/routing/Loader/AttributeFileLoader.php', + 'Symfony\\Component\\Routing\\Loader\\ClosureLoader' => $vendorDir . '/symfony/routing/Loader/ClosureLoader.php', + 'Symfony\\Component\\Routing\\Loader\\Configurator\\AliasConfigurator' => $vendorDir . '/symfony/routing/Loader/Configurator/AliasConfigurator.php', + 'Symfony\\Component\\Routing\\Loader\\Configurator\\CollectionConfigurator' => $vendorDir . '/symfony/routing/Loader/Configurator/CollectionConfigurator.php', + 'Symfony\\Component\\Routing\\Loader\\Configurator\\ImportConfigurator' => $vendorDir . '/symfony/routing/Loader/Configurator/ImportConfigurator.php', + 'Symfony\\Component\\Routing\\Loader\\Configurator\\RouteConfigurator' => $vendorDir . '/symfony/routing/Loader/Configurator/RouteConfigurator.php', + 'Symfony\\Component\\Routing\\Loader\\Configurator\\RoutingConfigurator' => $vendorDir . '/symfony/routing/Loader/Configurator/RoutingConfigurator.php', + 'Symfony\\Component\\Routing\\Loader\\Configurator\\Traits\\AddTrait' => $vendorDir . '/symfony/routing/Loader/Configurator/Traits/AddTrait.php', + 'Symfony\\Component\\Routing\\Loader\\Configurator\\Traits\\HostTrait' => $vendorDir . '/symfony/routing/Loader/Configurator/Traits/HostTrait.php', + 'Symfony\\Component\\Routing\\Loader\\Configurator\\Traits\\LocalizedRouteTrait' => $vendorDir . '/symfony/routing/Loader/Configurator/Traits/LocalizedRouteTrait.php', + 'Symfony\\Component\\Routing\\Loader\\Configurator\\Traits\\PrefixTrait' => $vendorDir . '/symfony/routing/Loader/Configurator/Traits/PrefixTrait.php', + 'Symfony\\Component\\Routing\\Loader\\Configurator\\Traits\\RouteTrait' => $vendorDir . '/symfony/routing/Loader/Configurator/Traits/RouteTrait.php', + 'Symfony\\Component\\Routing\\Loader\\ContainerLoader' => $vendorDir . '/symfony/routing/Loader/ContainerLoader.php', + 'Symfony\\Component\\Routing\\Loader\\DirectoryLoader' => $vendorDir . '/symfony/routing/Loader/DirectoryLoader.php', + 'Symfony\\Component\\Routing\\Loader\\GlobFileLoader' => $vendorDir . '/symfony/routing/Loader/GlobFileLoader.php', + 'Symfony\\Component\\Routing\\Loader\\ObjectLoader' => $vendorDir . '/symfony/routing/Loader/ObjectLoader.php', + 'Symfony\\Component\\Routing\\Loader\\PhpFileLoader' => $vendorDir . '/symfony/routing/Loader/PhpFileLoader.php', + 'Symfony\\Component\\Routing\\Loader\\Psr4DirectoryLoader' => $vendorDir . '/symfony/routing/Loader/Psr4DirectoryLoader.php', + 'Symfony\\Component\\Routing\\Loader\\XmlFileLoader' => $vendorDir . '/symfony/routing/Loader/XmlFileLoader.php', + 'Symfony\\Component\\Routing\\Loader\\YamlFileLoader' => $vendorDir . '/symfony/routing/Loader/YamlFileLoader.php', + 'Symfony\\Component\\Routing\\Matcher\\CompiledUrlMatcher' => $vendorDir . '/symfony/routing/Matcher/CompiledUrlMatcher.php', + 'Symfony\\Component\\Routing\\Matcher\\Dumper\\CompiledUrlMatcherDumper' => $vendorDir . '/symfony/routing/Matcher/Dumper/CompiledUrlMatcherDumper.php', + 'Symfony\\Component\\Routing\\Matcher\\Dumper\\CompiledUrlMatcherTrait' => $vendorDir . '/symfony/routing/Matcher/Dumper/CompiledUrlMatcherTrait.php', + 'Symfony\\Component\\Routing\\Matcher\\Dumper\\MatcherDumper' => $vendorDir . '/symfony/routing/Matcher/Dumper/MatcherDumper.php', + 'Symfony\\Component\\Routing\\Matcher\\Dumper\\MatcherDumperInterface' => $vendorDir . '/symfony/routing/Matcher/Dumper/MatcherDumperInterface.php', + 'Symfony\\Component\\Routing\\Matcher\\Dumper\\StaticPrefixCollection' => $vendorDir . '/symfony/routing/Matcher/Dumper/StaticPrefixCollection.php', + 'Symfony\\Component\\Routing\\Matcher\\ExpressionLanguageProvider' => $vendorDir . '/symfony/routing/Matcher/ExpressionLanguageProvider.php', + 'Symfony\\Component\\Routing\\Matcher\\RedirectableUrlMatcher' => $vendorDir . '/symfony/routing/Matcher/RedirectableUrlMatcher.php', + 'Symfony\\Component\\Routing\\Matcher\\RedirectableUrlMatcherInterface' => $vendorDir . '/symfony/routing/Matcher/RedirectableUrlMatcherInterface.php', + 'Symfony\\Component\\Routing\\Matcher\\RequestMatcherInterface' => $vendorDir . '/symfony/routing/Matcher/RequestMatcherInterface.php', + 'Symfony\\Component\\Routing\\Matcher\\TraceableUrlMatcher' => $vendorDir . '/symfony/routing/Matcher/TraceableUrlMatcher.php', + 'Symfony\\Component\\Routing\\Matcher\\UrlMatcher' => $vendorDir . '/symfony/routing/Matcher/UrlMatcher.php', + 'Symfony\\Component\\Routing\\Matcher\\UrlMatcherInterface' => $vendorDir . '/symfony/routing/Matcher/UrlMatcherInterface.php', + 'Symfony\\Component\\Routing\\RequestContext' => $vendorDir . '/symfony/routing/RequestContext.php', + 'Symfony\\Component\\Routing\\RequestContextAwareInterface' => $vendorDir . '/symfony/routing/RequestContextAwareInterface.php', + 'Symfony\\Component\\Routing\\Requirement\\EnumRequirement' => $vendorDir . '/symfony/routing/Requirement/EnumRequirement.php', + 'Symfony\\Component\\Routing\\Requirement\\Requirement' => $vendorDir . '/symfony/routing/Requirement/Requirement.php', + 'Symfony\\Component\\Routing\\Route' => $vendorDir . '/symfony/routing/Route.php', + 'Symfony\\Component\\Routing\\RouteCollection' => $vendorDir . '/symfony/routing/RouteCollection.php', + 'Symfony\\Component\\Routing\\RouteCompiler' => $vendorDir . '/symfony/routing/RouteCompiler.php', + 'Symfony\\Component\\Routing\\RouteCompilerInterface' => $vendorDir . '/symfony/routing/RouteCompilerInterface.php', + 'Symfony\\Component\\Routing\\Router' => $vendorDir . '/symfony/routing/Router.php', + 'Symfony\\Component\\Routing\\RouterInterface' => $vendorDir . '/symfony/routing/RouterInterface.php', + 'Symfony\\Component\\String\\AbstractString' => $vendorDir . '/symfony/string/AbstractString.php', + 'Symfony\\Component\\String\\AbstractUnicodeString' => $vendorDir . '/symfony/string/AbstractUnicodeString.php', + 'Symfony\\Component\\String\\ByteString' => $vendorDir . '/symfony/string/ByteString.php', + 'Symfony\\Component\\String\\CodePointString' => $vendorDir . '/symfony/string/CodePointString.php', + 'Symfony\\Component\\String\\Exception\\ExceptionInterface' => $vendorDir . '/symfony/string/Exception/ExceptionInterface.php', + 'Symfony\\Component\\String\\Exception\\InvalidArgumentException' => $vendorDir . '/symfony/string/Exception/InvalidArgumentException.php', + 'Symfony\\Component\\String\\Exception\\RuntimeException' => $vendorDir . '/symfony/string/Exception/RuntimeException.php', + 'Symfony\\Component\\String\\Inflector\\EnglishInflector' => $vendorDir . '/symfony/string/Inflector/EnglishInflector.php', + 'Symfony\\Component\\String\\Inflector\\FrenchInflector' => $vendorDir . '/symfony/string/Inflector/FrenchInflector.php', + 'Symfony\\Component\\String\\Inflector\\InflectorInterface' => $vendorDir . '/symfony/string/Inflector/InflectorInterface.php', + 'Symfony\\Component\\String\\LazyString' => $vendorDir . '/symfony/string/LazyString.php', + 'Symfony\\Component\\String\\Slugger\\AsciiSlugger' => $vendorDir . '/symfony/string/Slugger/AsciiSlugger.php', + 'Symfony\\Component\\String\\Slugger\\SluggerInterface' => $vendorDir . '/symfony/string/Slugger/SluggerInterface.php', + 'Symfony\\Component\\String\\UnicodeString' => $vendorDir . '/symfony/string/UnicodeString.php', + 'Symfony\\Component\\Translation\\CatalogueMetadataAwareInterface' => $vendorDir . '/symfony/translation/CatalogueMetadataAwareInterface.php', + 'Symfony\\Component\\Translation\\Catalogue\\AbstractOperation' => $vendorDir . '/symfony/translation/Catalogue/AbstractOperation.php', + 'Symfony\\Component\\Translation\\Catalogue\\MergeOperation' => $vendorDir . '/symfony/translation/Catalogue/MergeOperation.php', + 'Symfony\\Component\\Translation\\Catalogue\\OperationInterface' => $vendorDir . '/symfony/translation/Catalogue/OperationInterface.php', + 'Symfony\\Component\\Translation\\Catalogue\\TargetOperation' => $vendorDir . '/symfony/translation/Catalogue/TargetOperation.php', + 'Symfony\\Component\\Translation\\Command\\TranslationPullCommand' => $vendorDir . '/symfony/translation/Command/TranslationPullCommand.php', + 'Symfony\\Component\\Translation\\Command\\TranslationPushCommand' => $vendorDir . '/symfony/translation/Command/TranslationPushCommand.php', + 'Symfony\\Component\\Translation\\Command\\TranslationTrait' => $vendorDir . '/symfony/translation/Command/TranslationTrait.php', + 'Symfony\\Component\\Translation\\Command\\XliffLintCommand' => $vendorDir . '/symfony/translation/Command/XliffLintCommand.php', + 'Symfony\\Component\\Translation\\DataCollectorTranslator' => $vendorDir . '/symfony/translation/DataCollectorTranslator.php', + 'Symfony\\Component\\Translation\\DataCollector\\TranslationDataCollector' => $vendorDir . '/symfony/translation/DataCollector/TranslationDataCollector.php', + 'Symfony\\Component\\Translation\\DependencyInjection\\DataCollectorTranslatorPass' => $vendorDir . '/symfony/translation/DependencyInjection/DataCollectorTranslatorPass.php', + 'Symfony\\Component\\Translation\\DependencyInjection\\LoggingTranslatorPass' => $vendorDir . '/symfony/translation/DependencyInjection/LoggingTranslatorPass.php', + 'Symfony\\Component\\Translation\\DependencyInjection\\TranslationDumperPass' => $vendorDir . '/symfony/translation/DependencyInjection/TranslationDumperPass.php', + 'Symfony\\Component\\Translation\\DependencyInjection\\TranslationExtractorPass' => $vendorDir . '/symfony/translation/DependencyInjection/TranslationExtractorPass.php', + 'Symfony\\Component\\Translation\\DependencyInjection\\TranslatorPass' => $vendorDir . '/symfony/translation/DependencyInjection/TranslatorPass.php', + 'Symfony\\Component\\Translation\\DependencyInjection\\TranslatorPathsPass' => $vendorDir . '/symfony/translation/DependencyInjection/TranslatorPathsPass.php', + 'Symfony\\Component\\Translation\\Dumper\\CsvFileDumper' => $vendorDir . '/symfony/translation/Dumper/CsvFileDumper.php', + 'Symfony\\Component\\Translation\\Dumper\\DumperInterface' => $vendorDir . '/symfony/translation/Dumper/DumperInterface.php', + 'Symfony\\Component\\Translation\\Dumper\\FileDumper' => $vendorDir . '/symfony/translation/Dumper/FileDumper.php', + 'Symfony\\Component\\Translation\\Dumper\\IcuResFileDumper' => $vendorDir . '/symfony/translation/Dumper/IcuResFileDumper.php', + 'Symfony\\Component\\Translation\\Dumper\\IniFileDumper' => $vendorDir . '/symfony/translation/Dumper/IniFileDumper.php', + 'Symfony\\Component\\Translation\\Dumper\\JsonFileDumper' => $vendorDir . '/symfony/translation/Dumper/JsonFileDumper.php', + 'Symfony\\Component\\Translation\\Dumper\\MoFileDumper' => $vendorDir . '/symfony/translation/Dumper/MoFileDumper.php', + 'Symfony\\Component\\Translation\\Dumper\\PhpFileDumper' => $vendorDir . '/symfony/translation/Dumper/PhpFileDumper.php', + 'Symfony\\Component\\Translation\\Dumper\\PoFileDumper' => $vendorDir . '/symfony/translation/Dumper/PoFileDumper.php', + 'Symfony\\Component\\Translation\\Dumper\\QtFileDumper' => $vendorDir . '/symfony/translation/Dumper/QtFileDumper.php', + 'Symfony\\Component\\Translation\\Dumper\\XliffFileDumper' => $vendorDir . '/symfony/translation/Dumper/XliffFileDumper.php', + 'Symfony\\Component\\Translation\\Dumper\\YamlFileDumper' => $vendorDir . '/symfony/translation/Dumper/YamlFileDumper.php', + 'Symfony\\Component\\Translation\\Exception\\ExceptionInterface' => $vendorDir . '/symfony/translation/Exception/ExceptionInterface.php', + 'Symfony\\Component\\Translation\\Exception\\IncompleteDsnException' => $vendorDir . '/symfony/translation/Exception/IncompleteDsnException.php', + 'Symfony\\Component\\Translation\\Exception\\InvalidArgumentException' => $vendorDir . '/symfony/translation/Exception/InvalidArgumentException.php', + 'Symfony\\Component\\Translation\\Exception\\InvalidResourceException' => $vendorDir . '/symfony/translation/Exception/InvalidResourceException.php', + 'Symfony\\Component\\Translation\\Exception\\LogicException' => $vendorDir . '/symfony/translation/Exception/LogicException.php', + 'Symfony\\Component\\Translation\\Exception\\MissingRequiredOptionException' => $vendorDir . '/symfony/translation/Exception/MissingRequiredOptionException.php', + 'Symfony\\Component\\Translation\\Exception\\NotFoundResourceException' => $vendorDir . '/symfony/translation/Exception/NotFoundResourceException.php', + 'Symfony\\Component\\Translation\\Exception\\ProviderException' => $vendorDir . '/symfony/translation/Exception/ProviderException.php', + 'Symfony\\Component\\Translation\\Exception\\ProviderExceptionInterface' => $vendorDir . '/symfony/translation/Exception/ProviderExceptionInterface.php', + 'Symfony\\Component\\Translation\\Exception\\RuntimeException' => $vendorDir . '/symfony/translation/Exception/RuntimeException.php', + 'Symfony\\Component\\Translation\\Exception\\UnsupportedSchemeException' => $vendorDir . '/symfony/translation/Exception/UnsupportedSchemeException.php', + 'Symfony\\Component\\Translation\\Extractor\\AbstractFileExtractor' => $vendorDir . '/symfony/translation/Extractor/AbstractFileExtractor.php', + 'Symfony\\Component\\Translation\\Extractor\\ChainExtractor' => $vendorDir . '/symfony/translation/Extractor/ChainExtractor.php', + 'Symfony\\Component\\Translation\\Extractor\\ExtractorInterface' => $vendorDir . '/symfony/translation/Extractor/ExtractorInterface.php', + 'Symfony\\Component\\Translation\\Extractor\\PhpAstExtractor' => $vendorDir . '/symfony/translation/Extractor/PhpAstExtractor.php', + 'Symfony\\Component\\Translation\\Extractor\\PhpExtractor' => $vendorDir . '/symfony/translation/Extractor/PhpExtractor.php', + 'Symfony\\Component\\Translation\\Extractor\\PhpStringTokenParser' => $vendorDir . '/symfony/translation/Extractor/PhpStringTokenParser.php', + 'Symfony\\Component\\Translation\\Extractor\\Visitor\\AbstractVisitor' => $vendorDir . '/symfony/translation/Extractor/Visitor/AbstractVisitor.php', + 'Symfony\\Component\\Translation\\Extractor\\Visitor\\ConstraintVisitor' => $vendorDir . '/symfony/translation/Extractor/Visitor/ConstraintVisitor.php', + 'Symfony\\Component\\Translation\\Extractor\\Visitor\\TransMethodVisitor' => $vendorDir . '/symfony/translation/Extractor/Visitor/TransMethodVisitor.php', + 'Symfony\\Component\\Translation\\Extractor\\Visitor\\TranslatableMessageVisitor' => $vendorDir . '/symfony/translation/Extractor/Visitor/TranslatableMessageVisitor.php', + 'Symfony\\Component\\Translation\\Formatter\\IntlFormatter' => $vendorDir . '/symfony/translation/Formatter/IntlFormatter.php', + 'Symfony\\Component\\Translation\\Formatter\\IntlFormatterInterface' => $vendorDir . '/symfony/translation/Formatter/IntlFormatterInterface.php', + 'Symfony\\Component\\Translation\\Formatter\\MessageFormatter' => $vendorDir . '/symfony/translation/Formatter/MessageFormatter.php', + 'Symfony\\Component\\Translation\\Formatter\\MessageFormatterInterface' => $vendorDir . '/symfony/translation/Formatter/MessageFormatterInterface.php', + 'Symfony\\Component\\Translation\\IdentityTranslator' => $vendorDir . '/symfony/translation/IdentityTranslator.php', + 'Symfony\\Component\\Translation\\Loader\\ArrayLoader' => $vendorDir . '/symfony/translation/Loader/ArrayLoader.php', + 'Symfony\\Component\\Translation\\Loader\\CsvFileLoader' => $vendorDir . '/symfony/translation/Loader/CsvFileLoader.php', + 'Symfony\\Component\\Translation\\Loader\\FileLoader' => $vendorDir . '/symfony/translation/Loader/FileLoader.php', + 'Symfony\\Component\\Translation\\Loader\\IcuDatFileLoader' => $vendorDir . '/symfony/translation/Loader/IcuDatFileLoader.php', + 'Symfony\\Component\\Translation\\Loader\\IcuResFileLoader' => $vendorDir . '/symfony/translation/Loader/IcuResFileLoader.php', + 'Symfony\\Component\\Translation\\Loader\\IniFileLoader' => $vendorDir . '/symfony/translation/Loader/IniFileLoader.php', + 'Symfony\\Component\\Translation\\Loader\\JsonFileLoader' => $vendorDir . '/symfony/translation/Loader/JsonFileLoader.php', + 'Symfony\\Component\\Translation\\Loader\\LoaderInterface' => $vendorDir . '/symfony/translation/Loader/LoaderInterface.php', + 'Symfony\\Component\\Translation\\Loader\\MoFileLoader' => $vendorDir . '/symfony/translation/Loader/MoFileLoader.php', + 'Symfony\\Component\\Translation\\Loader\\PhpFileLoader' => $vendorDir . '/symfony/translation/Loader/PhpFileLoader.php', + 'Symfony\\Component\\Translation\\Loader\\PoFileLoader' => $vendorDir . '/symfony/translation/Loader/PoFileLoader.php', + 'Symfony\\Component\\Translation\\Loader\\QtFileLoader' => $vendorDir . '/symfony/translation/Loader/QtFileLoader.php', + 'Symfony\\Component\\Translation\\Loader\\XliffFileLoader' => $vendorDir . '/symfony/translation/Loader/XliffFileLoader.php', + 'Symfony\\Component\\Translation\\Loader\\YamlFileLoader' => $vendorDir . '/symfony/translation/Loader/YamlFileLoader.php', + 'Symfony\\Component\\Translation\\LocaleSwitcher' => $vendorDir . '/symfony/translation/LocaleSwitcher.php', + 'Symfony\\Component\\Translation\\LoggingTranslator' => $vendorDir . '/symfony/translation/LoggingTranslator.php', + 'Symfony\\Component\\Translation\\MessageCatalogue' => $vendorDir . '/symfony/translation/MessageCatalogue.php', + 'Symfony\\Component\\Translation\\MessageCatalogueInterface' => $vendorDir . '/symfony/translation/MessageCatalogueInterface.php', + 'Symfony\\Component\\Translation\\MetadataAwareInterface' => $vendorDir . '/symfony/translation/MetadataAwareInterface.php', + 'Symfony\\Component\\Translation\\Provider\\AbstractProviderFactory' => $vendorDir . '/symfony/translation/Provider/AbstractProviderFactory.php', + 'Symfony\\Component\\Translation\\Provider\\Dsn' => $vendorDir . '/symfony/translation/Provider/Dsn.php', + 'Symfony\\Component\\Translation\\Provider\\FilteringProvider' => $vendorDir . '/symfony/translation/Provider/FilteringProvider.php', + 'Symfony\\Component\\Translation\\Provider\\NullProvider' => $vendorDir . '/symfony/translation/Provider/NullProvider.php', + 'Symfony\\Component\\Translation\\Provider\\NullProviderFactory' => $vendorDir . '/symfony/translation/Provider/NullProviderFactory.php', + 'Symfony\\Component\\Translation\\Provider\\ProviderFactoryInterface' => $vendorDir . '/symfony/translation/Provider/ProviderFactoryInterface.php', + 'Symfony\\Component\\Translation\\Provider\\ProviderInterface' => $vendorDir . '/symfony/translation/Provider/ProviderInterface.php', + 'Symfony\\Component\\Translation\\Provider\\TranslationProviderCollection' => $vendorDir . '/symfony/translation/Provider/TranslationProviderCollection.php', + 'Symfony\\Component\\Translation\\Provider\\TranslationProviderCollectionFactory' => $vendorDir . '/symfony/translation/Provider/TranslationProviderCollectionFactory.php', + 'Symfony\\Component\\Translation\\PseudoLocalizationTranslator' => $vendorDir . '/symfony/translation/PseudoLocalizationTranslator.php', + 'Symfony\\Component\\Translation\\Reader\\TranslationReader' => $vendorDir . '/symfony/translation/Reader/TranslationReader.php', + 'Symfony\\Component\\Translation\\Reader\\TranslationReaderInterface' => $vendorDir . '/symfony/translation/Reader/TranslationReaderInterface.php', + 'Symfony\\Component\\Translation\\TranslatableMessage' => $vendorDir . '/symfony/translation/TranslatableMessage.php', + 'Symfony\\Component\\Translation\\Translator' => $vendorDir . '/symfony/translation/Translator.php', + 'Symfony\\Component\\Translation\\TranslatorBag' => $vendorDir . '/symfony/translation/TranslatorBag.php', + 'Symfony\\Component\\Translation\\TranslatorBagInterface' => $vendorDir . '/symfony/translation/TranslatorBagInterface.php', + 'Symfony\\Component\\Translation\\Util\\ArrayConverter' => $vendorDir . '/symfony/translation/Util/ArrayConverter.php', + 'Symfony\\Component\\Translation\\Util\\XliffUtils' => $vendorDir . '/symfony/translation/Util/XliffUtils.php', + 'Symfony\\Component\\Translation\\Writer\\TranslationWriter' => $vendorDir . '/symfony/translation/Writer/TranslationWriter.php', + 'Symfony\\Component\\Translation\\Writer\\TranslationWriterInterface' => $vendorDir . '/symfony/translation/Writer/TranslationWriterInterface.php', + 'Symfony\\Component\\Uid\\AbstractUid' => $vendorDir . '/symfony/uid/AbstractUid.php', + 'Symfony\\Component\\Uid\\BinaryUtil' => $vendorDir . '/symfony/uid/BinaryUtil.php', + 'Symfony\\Component\\Uid\\Command\\GenerateUlidCommand' => $vendorDir . '/symfony/uid/Command/GenerateUlidCommand.php', + 'Symfony\\Component\\Uid\\Command\\GenerateUuidCommand' => $vendorDir . '/symfony/uid/Command/GenerateUuidCommand.php', + 'Symfony\\Component\\Uid\\Command\\InspectUlidCommand' => $vendorDir . '/symfony/uid/Command/InspectUlidCommand.php', + 'Symfony\\Component\\Uid\\Command\\InspectUuidCommand' => $vendorDir . '/symfony/uid/Command/InspectUuidCommand.php', + 'Symfony\\Component\\Uid\\Factory\\NameBasedUuidFactory' => $vendorDir . '/symfony/uid/Factory/NameBasedUuidFactory.php', + 'Symfony\\Component\\Uid\\Factory\\RandomBasedUuidFactory' => $vendorDir . '/symfony/uid/Factory/RandomBasedUuidFactory.php', + 'Symfony\\Component\\Uid\\Factory\\TimeBasedUuidFactory' => $vendorDir . '/symfony/uid/Factory/TimeBasedUuidFactory.php', + 'Symfony\\Component\\Uid\\Factory\\UlidFactory' => $vendorDir . '/symfony/uid/Factory/UlidFactory.php', + 'Symfony\\Component\\Uid\\Factory\\UuidFactory' => $vendorDir . '/symfony/uid/Factory/UuidFactory.php', + 'Symfony\\Component\\Uid\\MaxUlid' => $vendorDir . '/symfony/uid/MaxUlid.php', + 'Symfony\\Component\\Uid\\MaxUuid' => $vendorDir . '/symfony/uid/MaxUuid.php', + 'Symfony\\Component\\Uid\\NilUlid' => $vendorDir . '/symfony/uid/NilUlid.php', + 'Symfony\\Component\\Uid\\NilUuid' => $vendorDir . '/symfony/uid/NilUuid.php', + 'Symfony\\Component\\Uid\\TimeBasedUidInterface' => $vendorDir . '/symfony/uid/TimeBasedUidInterface.php', + 'Symfony\\Component\\Uid\\Ulid' => $vendorDir . '/symfony/uid/Ulid.php', + 'Symfony\\Component\\Uid\\Uuid' => $vendorDir . '/symfony/uid/Uuid.php', + 'Symfony\\Component\\Uid\\UuidV1' => $vendorDir . '/symfony/uid/UuidV1.php', + 'Symfony\\Component\\Uid\\UuidV3' => $vendorDir . '/symfony/uid/UuidV3.php', + 'Symfony\\Component\\Uid\\UuidV4' => $vendorDir . '/symfony/uid/UuidV4.php', + 'Symfony\\Component\\Uid\\UuidV5' => $vendorDir . '/symfony/uid/UuidV5.php', + 'Symfony\\Component\\Uid\\UuidV6' => $vendorDir . '/symfony/uid/UuidV6.php', + 'Symfony\\Component\\Uid\\UuidV7' => $vendorDir . '/symfony/uid/UuidV7.php', + 'Symfony\\Component\\Uid\\UuidV8' => $vendorDir . '/symfony/uid/UuidV8.php', + 'Symfony\\Contracts\\EventDispatcher\\Event' => $vendorDir . '/symfony/event-dispatcher-contracts/Event.php', + 'Symfony\\Contracts\\EventDispatcher\\EventDispatcherInterface' => $vendorDir . '/symfony/event-dispatcher-contracts/EventDispatcherInterface.php', + 'Symfony\\Contracts\\Service\\Attribute\\Required' => $vendorDir . '/symfony/service-contracts/Attribute/Required.php', + 'Symfony\\Contracts\\Service\\Attribute\\SubscribedService' => $vendorDir . '/symfony/service-contracts/Attribute/SubscribedService.php', + 'Symfony\\Contracts\\Service\\ResetInterface' => $vendorDir . '/symfony/service-contracts/ResetInterface.php', + 'Symfony\\Contracts\\Service\\ServiceCollectionInterface' => $vendorDir . '/symfony/service-contracts/ServiceCollectionInterface.php', + 'Symfony\\Contracts\\Service\\ServiceLocatorTrait' => $vendorDir . '/symfony/service-contracts/ServiceLocatorTrait.php', + 'Symfony\\Contracts\\Service\\ServiceMethodsSubscriberTrait' => $vendorDir . '/symfony/service-contracts/ServiceMethodsSubscriberTrait.php', + 'Symfony\\Contracts\\Service\\ServiceProviderInterface' => $vendorDir . '/symfony/service-contracts/ServiceProviderInterface.php', + 'Symfony\\Contracts\\Service\\ServiceSubscriberInterface' => $vendorDir . '/symfony/service-contracts/ServiceSubscriberInterface.php', + 'Symfony\\Contracts\\Service\\ServiceSubscriberTrait' => $vendorDir . '/symfony/service-contracts/ServiceSubscriberTrait.php', + 'Symfony\\Contracts\\Translation\\LocaleAwareInterface' => $vendorDir . '/symfony/translation-contracts/LocaleAwareInterface.php', + 'Symfony\\Contracts\\Translation\\TranslatableInterface' => $vendorDir . '/symfony/translation-contracts/TranslatableInterface.php', + 'Symfony\\Contracts\\Translation\\TranslatorInterface' => $vendorDir . '/symfony/translation-contracts/TranslatorInterface.php', + 'Symfony\\Contracts\\Translation\\TranslatorTrait' => $vendorDir . '/symfony/translation-contracts/TranslatorTrait.php', + 'Symfony\\Polyfill\\Intl\\Grapheme\\Grapheme' => $vendorDir . '/symfony/polyfill-intl-grapheme/Grapheme.php', + 'Symfony\\Polyfill\\Intl\\Idn\\Idn' => $vendorDir . '/symfony/polyfill-intl-idn/Idn.php', + 'Symfony\\Polyfill\\Intl\\Idn\\Info' => $vendorDir . '/symfony/polyfill-intl-idn/Info.php', + 'Symfony\\Polyfill\\Intl\\Idn\\Resources\\unidata\\DisallowedRanges' => $vendorDir . '/symfony/polyfill-intl-idn/Resources/unidata/DisallowedRanges.php', + 'Symfony\\Polyfill\\Intl\\Idn\\Resources\\unidata\\Regex' => $vendorDir . '/symfony/polyfill-intl-idn/Resources/unidata/Regex.php', + 'Symfony\\Polyfill\\Intl\\Normalizer\\Normalizer' => $vendorDir . '/symfony/polyfill-intl-normalizer/Normalizer.php', + 'Symfony\\Polyfill\\Php82\\NoDynamicProperties' => $vendorDir . '/symfony/polyfill-php82/NoDynamicProperties.php', + 'Symfony\\Polyfill\\Php82\\Php82' => $vendorDir . '/symfony/polyfill-php82/Php82.php', + 'Symfony\\Polyfill\\Php82\\Random\\Engine\\Secure' => $vendorDir . '/symfony/polyfill-php82/Random/Engine/Secure.php', + 'Symfony\\Polyfill\\Php82\\SensitiveParameterValue' => $vendorDir . '/symfony/polyfill-php82/SensitiveParameterValue.php', + 'Symfony\\Polyfill\\Php83\\Php83' => $vendorDir . '/symfony/polyfill-php83/Php83.php', + 'Symfony\\Polyfill\\Php84\\Php84' => $vendorDir . '/symfony/polyfill-php84/Php84.php', + 'Symfony\\Polyfill\\Uuid\\Uuid' => $vendorDir . '/symfony/polyfill-uuid/Uuid.php', + 'System' => $vendorDir . '/pear/pear-core-minimal/src/System.php', + 'Webauthn\\AttestationStatement\\AndroidKeyAttestationStatementSupport' => $vendorDir . '/web-auth/webauthn-lib/src/AttestationStatement/AndroidKeyAttestationStatementSupport.php', + 'Webauthn\\AttestationStatement\\AndroidSafetyNetAttestationStatementSupport' => $vendorDir . '/web-auth/webauthn-lib/src/AttestationStatement/AndroidSafetyNetAttestationStatementSupport.php', + 'Webauthn\\AttestationStatement\\AppleAttestationStatementSupport' => $vendorDir . '/web-auth/webauthn-lib/src/AttestationStatement/AppleAttestationStatementSupport.php', + 'Webauthn\\AttestationStatement\\AttestationObject' => $vendorDir . '/web-auth/webauthn-lib/src/AttestationStatement/AttestationObject.php', + 'Webauthn\\AttestationStatement\\AttestationObjectLoader' => $vendorDir . '/web-auth/webauthn-lib/src/AttestationStatement/AttestationObjectLoader.php', + 'Webauthn\\AttestationStatement\\AttestationStatement' => $vendorDir . '/web-auth/webauthn-lib/src/AttestationStatement/AttestationStatement.php', + 'Webauthn\\AttestationStatement\\AttestationStatementSupport' => $vendorDir . '/web-auth/webauthn-lib/src/AttestationStatement/AttestationStatementSupport.php', + 'Webauthn\\AttestationStatement\\AttestationStatementSupportManager' => $vendorDir . '/web-auth/webauthn-lib/src/AttestationStatement/AttestationStatementSupportManager.php', + 'Webauthn\\AttestationStatement\\FidoU2FAttestationStatementSupport' => $vendorDir . '/web-auth/webauthn-lib/src/AttestationStatement/FidoU2FAttestationStatementSupport.php', + 'Webauthn\\AttestationStatement\\NoneAttestationStatementSupport' => $vendorDir . '/web-auth/webauthn-lib/src/AttestationStatement/NoneAttestationStatementSupport.php', + 'Webauthn\\AttestationStatement\\PackedAttestationStatementSupport' => $vendorDir . '/web-auth/webauthn-lib/src/AttestationStatement/PackedAttestationStatementSupport.php', + 'Webauthn\\AttestationStatement\\TPMAttestationStatementSupport' => $vendorDir . '/web-auth/webauthn-lib/src/AttestationStatement/TPMAttestationStatementSupport.php', + 'Webauthn\\AttestedCredentialData' => $vendorDir . '/web-auth/webauthn-lib/src/AttestedCredentialData.php', + 'Webauthn\\AuthenticationExtensions\\AuthenticationExtension' => $vendorDir . '/web-auth/webauthn-lib/src/AuthenticationExtensions/AuthenticationExtension.php', + 'Webauthn\\AuthenticationExtensions\\AuthenticationExtensions' => $vendorDir . '/web-auth/webauthn-lib/src/AuthenticationExtensions/AuthenticationExtensions.php', + 'Webauthn\\AuthenticationExtensions\\AuthenticationExtensionsClientInputs' => $vendorDir . '/web-auth/webauthn-lib/src/AuthenticationExtensions/AuthenticationExtensionsClientInputs.php', + 'Webauthn\\AuthenticationExtensions\\AuthenticationExtensionsClientOutputs' => $vendorDir . '/web-auth/webauthn-lib/src/AuthenticationExtensions/AuthenticationExtensionsClientOutputs.php', + 'Webauthn\\AuthenticationExtensions\\AuthenticationExtensionsClientOutputsLoader' => $vendorDir . '/web-auth/webauthn-lib/src/AuthenticationExtensions/AuthenticationExtensionsClientOutputsLoader.php', + 'Webauthn\\AuthenticationExtensions\\ExtensionOutputChecker' => $vendorDir . '/web-auth/webauthn-lib/src/AuthenticationExtensions/ExtensionOutputChecker.php', + 'Webauthn\\AuthenticationExtensions\\ExtensionOutputCheckerHandler' => $vendorDir . '/web-auth/webauthn-lib/src/AuthenticationExtensions/ExtensionOutputCheckerHandler.php', + 'Webauthn\\AuthenticationExtensions\\ExtensionOutputError' => $vendorDir . '/web-auth/webauthn-lib/src/AuthenticationExtensions/ExtensionOutputError.php', + 'Webauthn\\AuthenticatorAssertionResponse' => $vendorDir . '/web-auth/webauthn-lib/src/AuthenticatorAssertionResponse.php', + 'Webauthn\\AuthenticatorAssertionResponseValidator' => $vendorDir . '/web-auth/webauthn-lib/src/AuthenticatorAssertionResponseValidator.php', + 'Webauthn\\AuthenticatorAttestationResponse' => $vendorDir . '/web-auth/webauthn-lib/src/AuthenticatorAttestationResponse.php', + 'Webauthn\\AuthenticatorAttestationResponseValidator' => $vendorDir . '/web-auth/webauthn-lib/src/AuthenticatorAttestationResponseValidator.php', + 'Webauthn\\AuthenticatorData' => $vendorDir . '/web-auth/webauthn-lib/src/AuthenticatorData.php', + 'Webauthn\\AuthenticatorDataLoader' => $vendorDir . '/web-auth/webauthn-lib/src/AuthenticatorDataLoader.php', + 'Webauthn\\AuthenticatorResponse' => $vendorDir . '/web-auth/webauthn-lib/src/AuthenticatorResponse.php', + 'Webauthn\\AuthenticatorSelectionCriteria' => $vendorDir . '/web-auth/webauthn-lib/src/AuthenticatorSelectionCriteria.php', + 'Webauthn\\CeremonyStep\\CeremonyStep' => $vendorDir . '/web-auth/webauthn-lib/src/CeremonyStep/CeremonyStep.php', + 'Webauthn\\CeremonyStep\\CeremonyStepManager' => $vendorDir . '/web-auth/webauthn-lib/src/CeremonyStep/CeremonyStepManager.php', + 'Webauthn\\CeremonyStep\\CeremonyStepManagerFactory' => $vendorDir . '/web-auth/webauthn-lib/src/CeremonyStep/CeremonyStepManagerFactory.php', + 'Webauthn\\CeremonyStep\\CheckAlgorithm' => $vendorDir . '/web-auth/webauthn-lib/src/CeremonyStep/CheckAlgorithm.php', + 'Webauthn\\CeremonyStep\\CheckAllowedCredentialList' => $vendorDir . '/web-auth/webauthn-lib/src/CeremonyStep/CheckAllowedCredentialList.php', + 'Webauthn\\CeremonyStep\\CheckAttestationFormatIsKnownAndValid' => $vendorDir . '/web-auth/webauthn-lib/src/CeremonyStep/CheckAttestationFormatIsKnownAndValid.php', + 'Webauthn\\CeremonyStep\\CheckBackupBitsAreConsistent' => $vendorDir . '/web-auth/webauthn-lib/src/CeremonyStep/CheckBackupBitsAreConsistent.php', + 'Webauthn\\CeremonyStep\\CheckChallenge' => $vendorDir . '/web-auth/webauthn-lib/src/CeremonyStep/CheckChallenge.php', + 'Webauthn\\CeremonyStep\\CheckClientDataCollectorType' => $vendorDir . '/web-auth/webauthn-lib/src/CeremonyStep/CheckClientDataCollectorType.php', + 'Webauthn\\CeremonyStep\\CheckCounter' => $vendorDir . '/web-auth/webauthn-lib/src/CeremonyStep/CheckCounter.php', + 'Webauthn\\CeremonyStep\\CheckCredentialId' => $vendorDir . '/web-auth/webauthn-lib/src/CeremonyStep/CheckCredentialId.php', + 'Webauthn\\CeremonyStep\\CheckExtensions' => $vendorDir . '/web-auth/webauthn-lib/src/CeremonyStep/CheckExtensions.php', + 'Webauthn\\CeremonyStep\\CheckHasAttestedCredentialData' => $vendorDir . '/web-auth/webauthn-lib/src/CeremonyStep/CheckHasAttestedCredentialData.php', + 'Webauthn\\CeremonyStep\\CheckMetadataStatement' => $vendorDir . '/web-auth/webauthn-lib/src/CeremonyStep/CheckMetadataStatement.php', + 'Webauthn\\CeremonyStep\\CheckOrigin' => $vendorDir . '/web-auth/webauthn-lib/src/CeremonyStep/CheckOrigin.php', + 'Webauthn\\CeremonyStep\\CheckRelyingPartyIdIdHash' => $vendorDir . '/web-auth/webauthn-lib/src/CeremonyStep/CheckRelyingPartyIdIdHash.php', + 'Webauthn\\CeremonyStep\\CheckSignature' => $vendorDir . '/web-auth/webauthn-lib/src/CeremonyStep/CheckSignature.php', + 'Webauthn\\CeremonyStep\\CheckTopOrigin' => $vendorDir . '/web-auth/webauthn-lib/src/CeremonyStep/CheckTopOrigin.php', + 'Webauthn\\CeremonyStep\\CheckUserHandle' => $vendorDir . '/web-auth/webauthn-lib/src/CeremonyStep/CheckUserHandle.php', + 'Webauthn\\CeremonyStep\\CheckUserVerification' => $vendorDir . '/web-auth/webauthn-lib/src/CeremonyStep/CheckUserVerification.php', + 'Webauthn\\CeremonyStep\\CheckUserWasPresent' => $vendorDir . '/web-auth/webauthn-lib/src/CeremonyStep/CheckUserWasPresent.php', + 'Webauthn\\CeremonyStep\\HostTopOriginValidator' => $vendorDir . '/web-auth/webauthn-lib/src/CeremonyStep/HostTopOriginValidator.php', + 'Webauthn\\CeremonyStep\\TopOriginValidator' => $vendorDir . '/web-auth/webauthn-lib/src/CeremonyStep/TopOriginValidator.php', + 'Webauthn\\CertificateChainChecker\\CertificateChainChecker' => $vendorDir . '/web-auth/webauthn-lib/src/CertificateChainChecker/CertificateChainChecker.php', + 'Webauthn\\CertificateChainChecker\\PhpCertificateChainChecker' => $vendorDir . '/web-auth/webauthn-lib/src/CertificateChainChecker/PhpCertificateChainChecker.php', + 'Webauthn\\CertificateToolbox' => $vendorDir . '/web-auth/webauthn-lib/src/CertificateToolbox.php', + 'Webauthn\\ClientDataCollector\\ClientDataCollector' => $vendorDir . '/web-auth/webauthn-lib/src/ClientDataCollector/ClientDataCollector.php', + 'Webauthn\\ClientDataCollector\\ClientDataCollectorManager' => $vendorDir . '/web-auth/webauthn-lib/src/ClientDataCollector/ClientDataCollectorManager.php', + 'Webauthn\\ClientDataCollector\\WebauthnAuthenticationCollector' => $vendorDir . '/web-auth/webauthn-lib/src/ClientDataCollector/WebauthnAuthenticationCollector.php', + 'Webauthn\\CollectedClientData' => $vendorDir . '/web-auth/webauthn-lib/src/CollectedClientData.php', + 'Webauthn\\Counter\\CounterChecker' => $vendorDir . '/web-auth/webauthn-lib/src/Counter/CounterChecker.php', + 'Webauthn\\Counter\\ThrowExceptionIfInvalid' => $vendorDir . '/web-auth/webauthn-lib/src/Counter/ThrowExceptionIfInvalid.php', + 'Webauthn\\Credential' => $vendorDir . '/web-auth/webauthn-lib/src/Credential.php', + 'Webauthn\\Denormalizer\\AttestationObjectDenormalizer' => $vendorDir . '/web-auth/webauthn-lib/src/Denormalizer/AttestationObjectDenormalizer.php', + 'Webauthn\\Denormalizer\\AttestationStatementDenormalizer' => $vendorDir . '/web-auth/webauthn-lib/src/Denormalizer/AttestationStatementDenormalizer.php', + 'Webauthn\\Denormalizer\\AttestedCredentialDataNormalizer' => $vendorDir . '/web-auth/webauthn-lib/src/Denormalizer/AttestedCredentialDataNormalizer.php', + 'Webauthn\\Denormalizer\\AuthenticationExtensionNormalizer' => $vendorDir . '/web-auth/webauthn-lib/src/Denormalizer/AuthenticationExtensionNormalizer.php', + 'Webauthn\\Denormalizer\\AuthenticationExtensionsDenormalizer' => $vendorDir . '/web-auth/webauthn-lib/src/Denormalizer/AuthenticationExtensionsDenormalizer.php', + 'Webauthn\\Denormalizer\\AuthenticatorAssertionResponseDenormalizer' => $vendorDir . '/web-auth/webauthn-lib/src/Denormalizer/AuthenticatorAssertionResponseDenormalizer.php', + 'Webauthn\\Denormalizer\\AuthenticatorAttestationResponseDenormalizer' => $vendorDir . '/web-auth/webauthn-lib/src/Denormalizer/AuthenticatorAttestationResponseDenormalizer.php', + 'Webauthn\\Denormalizer\\AuthenticatorDataDenormalizer' => $vendorDir . '/web-auth/webauthn-lib/src/Denormalizer/AuthenticatorDataDenormalizer.php', + 'Webauthn\\Denormalizer\\AuthenticatorResponseDenormalizer' => $vendorDir . '/web-auth/webauthn-lib/src/Denormalizer/AuthenticatorResponseDenormalizer.php', + 'Webauthn\\Denormalizer\\CollectedClientDataDenormalizer' => $vendorDir . '/web-auth/webauthn-lib/src/Denormalizer/CollectedClientDataDenormalizer.php', + 'Webauthn\\Denormalizer\\ExtensionDescriptorDenormalizer' => $vendorDir . '/web-auth/webauthn-lib/src/Denormalizer/ExtensionDescriptorDenormalizer.php', + 'Webauthn\\Denormalizer\\PublicKeyCredentialDenormalizer' => $vendorDir . '/web-auth/webauthn-lib/src/Denormalizer/PublicKeyCredentialDenormalizer.php', + 'Webauthn\\Denormalizer\\PublicKeyCredentialDescriptorNormalizer' => $vendorDir . '/web-auth/webauthn-lib/src/Denormalizer/PublicKeyCredentialDescriptorNormalizer.php', + 'Webauthn\\Denormalizer\\PublicKeyCredentialOptionsDenormalizer' => $vendorDir . '/web-auth/webauthn-lib/src/Denormalizer/PublicKeyCredentialOptionsDenormalizer.php', + 'Webauthn\\Denormalizer\\PublicKeyCredentialParametersDenormalizer' => $vendorDir . '/web-auth/webauthn-lib/src/Denormalizer/PublicKeyCredentialParametersDenormalizer.php', + 'Webauthn\\Denormalizer\\PublicKeyCredentialSourceDenormalizer' => $vendorDir . '/web-auth/webauthn-lib/src/Denormalizer/PublicKeyCredentialSourceDenormalizer.php', + 'Webauthn\\Denormalizer\\PublicKeyCredentialUserEntityDenormalizer' => $vendorDir . '/web-auth/webauthn-lib/src/Denormalizer/PublicKeyCredentialUserEntityDenormalizer.php', + 'Webauthn\\Denormalizer\\TrustPathDenormalizer' => $vendorDir . '/web-auth/webauthn-lib/src/Denormalizer/TrustPathDenormalizer.php', + 'Webauthn\\Denormalizer\\VerificationMethodANDCombinationsDenormalizer' => $vendorDir . '/web-auth/webauthn-lib/src/Denormalizer/VerificationMethodANDCombinationsDenormalizer.php', + 'Webauthn\\Denormalizer\\WebauthnSerializerFactory' => $vendorDir . '/web-auth/webauthn-lib/src/Denormalizer/WebauthnSerializerFactory.php', + 'Webauthn\\Event\\AttestationObjectLoaded' => $vendorDir . '/web-auth/webauthn-lib/src/Event/AttestationObjectLoaded.php', + 'Webauthn\\Event\\AttestationStatementLoaded' => $vendorDir . '/web-auth/webauthn-lib/src/Event/AttestationStatementLoaded.php', + 'Webauthn\\Event\\AuthenticatorAssertionResponseValidationFailedEvent' => $vendorDir . '/web-auth/webauthn-lib/src/Event/AuthenticatorAssertionResponseValidationFailedEvent.php', + 'Webauthn\\Event\\AuthenticatorAssertionResponseValidationSucceededEvent' => $vendorDir . '/web-auth/webauthn-lib/src/Event/AuthenticatorAssertionResponseValidationSucceededEvent.php', + 'Webauthn\\Event\\AuthenticatorAttestationResponseValidationFailedEvent' => $vendorDir . '/web-auth/webauthn-lib/src/Event/AuthenticatorAttestationResponseValidationFailedEvent.php', + 'Webauthn\\Event\\AuthenticatorAttestationResponseValidationSucceededEvent' => $vendorDir . '/web-auth/webauthn-lib/src/Event/AuthenticatorAttestationResponseValidationSucceededEvent.php', + 'Webauthn\\Event\\BeforeCertificateChainValidation' => $vendorDir . '/web-auth/webauthn-lib/src/Event/BeforeCertificateChainValidation.php', + 'Webauthn\\Event\\CanDispatchEvents' => $vendorDir . '/web-auth/webauthn-lib/src/Event/CanDispatchEvents.php', + 'Webauthn\\Event\\CertificateChainValidationFailed' => $vendorDir . '/web-auth/webauthn-lib/src/Event/CertificateChainValidationFailed.php', + 'Webauthn\\Event\\CertificateChainValidationSucceeded' => $vendorDir . '/web-auth/webauthn-lib/src/Event/CertificateChainValidationSucceeded.php', + 'Webauthn\\Event\\MetadataStatementFound' => $vendorDir . '/web-auth/webauthn-lib/src/Event/MetadataStatementFound.php', + 'Webauthn\\Event\\NullEventDispatcher' => $vendorDir . '/web-auth/webauthn-lib/src/Event/NullEventDispatcher.php', + 'Webauthn\\Event\\WebauthnEvent' => $vendorDir . '/web-auth/webauthn-lib/src/Event/WebauthnEvent.php', + 'Webauthn\\Exception\\AttestationStatementException' => $vendorDir . '/web-auth/webauthn-lib/src/Exception/AttestationStatementException.php', + 'Webauthn\\Exception\\AttestationStatementLoadingException' => $vendorDir . '/web-auth/webauthn-lib/src/Exception/AttestationStatementLoadingException.php', + 'Webauthn\\Exception\\AttestationStatementVerificationException' => $vendorDir . '/web-auth/webauthn-lib/src/Exception/AttestationStatementVerificationException.php', + 'Webauthn\\Exception\\AuthenticationExtensionException' => $vendorDir . '/web-auth/webauthn-lib/src/Exception/AuthenticationExtensionException.php', + 'Webauthn\\Exception\\AuthenticatorResponseVerificationException' => $vendorDir . '/web-auth/webauthn-lib/src/Exception/AuthenticatorResponseVerificationException.php', + 'Webauthn\\Exception\\CertificateChainException' => $vendorDir . '/web-auth/webauthn-lib/src/Exception/CertificateChainException.php', + 'Webauthn\\Exception\\CertificateException' => $vendorDir . '/web-auth/webauthn-lib/src/Exception/CertificateException.php', + 'Webauthn\\Exception\\CertificateRevocationListException' => $vendorDir . '/web-auth/webauthn-lib/src/Exception/CertificateRevocationListException.php', + 'Webauthn\\Exception\\CounterException' => $vendorDir . '/web-auth/webauthn-lib/src/Exception/CounterException.php', + 'Webauthn\\Exception\\ExpiredCertificateException' => $vendorDir . '/web-auth/webauthn-lib/src/Exception/ExpiredCertificateException.php', + 'Webauthn\\Exception\\InvalidAttestationStatementException' => $vendorDir . '/web-auth/webauthn-lib/src/Exception/InvalidAttestationStatementException.php', + 'Webauthn\\Exception\\InvalidCertificateException' => $vendorDir . '/web-auth/webauthn-lib/src/Exception/InvalidCertificateException.php', + 'Webauthn\\Exception\\InvalidDataException' => $vendorDir . '/web-auth/webauthn-lib/src/Exception/InvalidDataException.php', + 'Webauthn\\Exception\\InvalidTrustPathException' => $vendorDir . '/web-auth/webauthn-lib/src/Exception/InvalidTrustPathException.php', + 'Webauthn\\Exception\\InvalidUserHandleException' => $vendorDir . '/web-auth/webauthn-lib/src/Exception/InvalidUserHandleException.php', + 'Webauthn\\Exception\\MetadataServiceException' => $vendorDir . '/web-auth/webauthn-lib/src/Exception/MetadataServiceException.php', + 'Webauthn\\Exception\\MetadataStatementException' => $vendorDir . '/web-auth/webauthn-lib/src/Exception/MetadataStatementException.php', + 'Webauthn\\Exception\\MetadataStatementLoadingException' => $vendorDir . '/web-auth/webauthn-lib/src/Exception/MetadataStatementLoadingException.php', + 'Webauthn\\Exception\\MissingMetadataStatementException' => $vendorDir . '/web-auth/webauthn-lib/src/Exception/MissingMetadataStatementException.php', + 'Webauthn\\Exception\\RevokedCertificateException' => $vendorDir . '/web-auth/webauthn-lib/src/Exception/RevokedCertificateException.php', + 'Webauthn\\Exception\\UnsupportedFeatureException' => $vendorDir . '/web-auth/webauthn-lib/src/Exception/UnsupportedFeatureException.php', + 'Webauthn\\Exception\\WebauthnException' => $vendorDir . '/web-auth/webauthn-lib/src/Exception/WebauthnException.php', + 'Webauthn\\FakeCredentialGenerator' => $vendorDir . '/web-auth/webauthn-lib/src/FakeCredentialGenerator.php', + 'Webauthn\\MetadataService\\CanLogData' => $vendorDir . '/web-auth/webauthn-lib/src/MetadataService/CanLogData.php', + 'Webauthn\\MetadataService\\CertificateChain\\CertificateChainValidator' => $vendorDir . '/web-auth/webauthn-lib/src/MetadataService/CertificateChain/CertificateChainValidator.php', + 'Webauthn\\MetadataService\\CertificateChain\\CertificateToolbox' => $vendorDir . '/web-auth/webauthn-lib/src/MetadataService/CertificateChain/CertificateToolbox.php', + 'Webauthn\\MetadataService\\CertificateChain\\PhpCertificateChainValidator' => $vendorDir . '/web-auth/webauthn-lib/src/MetadataService/CertificateChain/PhpCertificateChainValidator.php', + 'Webauthn\\MetadataService\\Denormalizer\\ExtensionDescriptorDenormalizer' => $vendorDir . '/web-auth/webauthn-lib/src/MetadataService/Denormalizer/ExtensionDescriptorDenormalizer.php', + 'Webauthn\\MetadataService\\Denormalizer\\MetadataStatementSerializerFactory' => $vendorDir . '/web-auth/webauthn-lib/src/MetadataService/Denormalizer/MetadataStatementSerializerFactory.php', + 'Webauthn\\MetadataService\\Event\\BeforeCertificateChainValidation' => $vendorDir . '/web-auth/webauthn-lib/src/MetadataService/Event/BeforeCertificateChainValidation.php', + 'Webauthn\\MetadataService\\Event\\CanDispatchEvents' => $vendorDir . '/web-auth/webauthn-lib/src/MetadataService/Event/CanDispatchEvents.php', + 'Webauthn\\MetadataService\\Event\\CertificateChainValidationFailed' => $vendorDir . '/web-auth/webauthn-lib/src/MetadataService/Event/CertificateChainValidationFailed.php', + 'Webauthn\\MetadataService\\Event\\CertificateChainValidationSucceeded' => $vendorDir . '/web-auth/webauthn-lib/src/MetadataService/Event/CertificateChainValidationSucceeded.php', + 'Webauthn\\MetadataService\\Event\\MetadataStatementFound' => $vendorDir . '/web-auth/webauthn-lib/src/MetadataService/Event/MetadataStatementFound.php', + 'Webauthn\\MetadataService\\Event\\NullEventDispatcher' => $vendorDir . '/web-auth/webauthn-lib/src/MetadataService/Event/NullEventDispatcher.php', + 'Webauthn\\MetadataService\\Event\\WebauthnEvent' => $vendorDir . '/web-auth/webauthn-lib/src/MetadataService/Event/WebauthnEvent.php', + 'Webauthn\\MetadataService\\Exception\\CertificateChainException' => $vendorDir . '/web-auth/webauthn-lib/src/MetadataService/Exception/CertificateChainException.php', + 'Webauthn\\MetadataService\\Exception\\CertificateException' => $vendorDir . '/web-auth/webauthn-lib/src/MetadataService/Exception/CertificateException.php', + 'Webauthn\\MetadataService\\Exception\\CertificateRevocationListException' => $vendorDir . '/web-auth/webauthn-lib/src/MetadataService/Exception/CertificateRevocationListException.php', + 'Webauthn\\MetadataService\\Exception\\ExpiredCertificateException' => $vendorDir . '/web-auth/webauthn-lib/src/MetadataService/Exception/ExpiredCertificateException.php', + 'Webauthn\\MetadataService\\Exception\\InvalidCertificateException' => $vendorDir . '/web-auth/webauthn-lib/src/MetadataService/Exception/InvalidCertificateException.php', + 'Webauthn\\MetadataService\\Exception\\MetadataServiceException' => $vendorDir . '/web-auth/webauthn-lib/src/MetadataService/Exception/MetadataServiceException.php', + 'Webauthn\\MetadataService\\Exception\\MetadataStatementException' => $vendorDir . '/web-auth/webauthn-lib/src/MetadataService/Exception/MetadataStatementException.php', + 'Webauthn\\MetadataService\\Exception\\MetadataStatementLoadingException' => $vendorDir . '/web-auth/webauthn-lib/src/MetadataService/Exception/MetadataStatementLoadingException.php', + 'Webauthn\\MetadataService\\Exception\\MissingMetadataStatementException' => $vendorDir . '/web-auth/webauthn-lib/src/MetadataService/Exception/MissingMetadataStatementException.php', + 'Webauthn\\MetadataService\\Exception\\RevokedCertificateException' => $vendorDir . '/web-auth/webauthn-lib/src/MetadataService/Exception/RevokedCertificateException.php', + 'Webauthn\\MetadataService\\MetadataStatementRepository' => $vendorDir . '/web-auth/webauthn-lib/src/MetadataService/MetadataStatementRepository.php', + 'Webauthn\\MetadataService\\Psr18HttpClient' => $vendorDir . '/web-auth/webauthn-lib/src/MetadataService/Psr18HttpClient.php', + 'Webauthn\\MetadataService\\Service\\ChainedMetadataServices' => $vendorDir . '/web-auth/webauthn-lib/src/MetadataService/Service/ChainedMetadataServices.php', + 'Webauthn\\MetadataService\\Service\\DistantResourceMetadataService' => $vendorDir . '/web-auth/webauthn-lib/src/MetadataService/Service/DistantResourceMetadataService.php', + 'Webauthn\\MetadataService\\Service\\FidoAllianceCompliantMetadataService' => $vendorDir . '/web-auth/webauthn-lib/src/MetadataService/Service/FidoAllianceCompliantMetadataService.php', + 'Webauthn\\MetadataService\\Service\\FolderResourceMetadataService' => $vendorDir . '/web-auth/webauthn-lib/src/MetadataService/Service/FolderResourceMetadataService.php', + 'Webauthn\\MetadataService\\Service\\InMemoryMetadataService' => $vendorDir . '/web-auth/webauthn-lib/src/MetadataService/Service/InMemoryMetadataService.php', + 'Webauthn\\MetadataService\\Service\\JsonMetadataService' => $vendorDir . '/web-auth/webauthn-lib/src/MetadataService/Service/JsonMetadataService.php', + 'Webauthn\\MetadataService\\Service\\LocalResourceMetadataService' => $vendorDir . '/web-auth/webauthn-lib/src/MetadataService/Service/LocalResourceMetadataService.php', + 'Webauthn\\MetadataService\\Service\\MetadataBLOBPayload' => $vendorDir . '/web-auth/webauthn-lib/src/MetadataService/Service/MetadataBLOBPayload.php', + 'Webauthn\\MetadataService\\Service\\MetadataBLOBPayloadEntry' => $vendorDir . '/web-auth/webauthn-lib/src/MetadataService/Service/MetadataBLOBPayloadEntry.php', + 'Webauthn\\MetadataService\\Service\\MetadataService' => $vendorDir . '/web-auth/webauthn-lib/src/MetadataService/Service/MetadataService.php', + 'Webauthn\\MetadataService\\Service\\StringMetadataService' => $vendorDir . '/web-auth/webauthn-lib/src/MetadataService/Service/StringMetadataService.php', + 'Webauthn\\MetadataService\\Statement\\AbstractDescriptor' => $vendorDir . '/web-auth/webauthn-lib/src/MetadataService/Statement/AbstractDescriptor.php', + 'Webauthn\\MetadataService\\Statement\\AlternativeDescriptions' => $vendorDir . '/web-auth/webauthn-lib/src/MetadataService/Statement/AlternativeDescriptions.php', + 'Webauthn\\MetadataService\\Statement\\AuthenticatorGetInfo' => $vendorDir . '/web-auth/webauthn-lib/src/MetadataService/Statement/AuthenticatorGetInfo.php', + 'Webauthn\\MetadataService\\Statement\\AuthenticatorStatus' => $vendorDir . '/web-auth/webauthn-lib/src/MetadataService/Statement/AuthenticatorStatus.php', + 'Webauthn\\MetadataService\\Statement\\BiometricAccuracyDescriptor' => $vendorDir . '/web-auth/webauthn-lib/src/MetadataService/Statement/BiometricAccuracyDescriptor.php', + 'Webauthn\\MetadataService\\Statement\\BiometricStatusReport' => $vendorDir . '/web-auth/webauthn-lib/src/MetadataService/Statement/BiometricStatusReport.php', + 'Webauthn\\MetadataService\\Statement\\CodeAccuracyDescriptor' => $vendorDir . '/web-auth/webauthn-lib/src/MetadataService/Statement/CodeAccuracyDescriptor.php', + 'Webauthn\\MetadataService\\Statement\\DisplayPNGCharacteristicsDescriptor' => $vendorDir . '/web-auth/webauthn-lib/src/MetadataService/Statement/DisplayPNGCharacteristicsDescriptor.php', + 'Webauthn\\MetadataService\\Statement\\EcdaaTrustAnchor' => $vendorDir . '/web-auth/webauthn-lib/src/MetadataService/Statement/EcdaaTrustAnchor.php', + 'Webauthn\\MetadataService\\Statement\\ExtensionDescriptor' => $vendorDir . '/web-auth/webauthn-lib/src/MetadataService/Statement/ExtensionDescriptor.php', + 'Webauthn\\MetadataService\\Statement\\MetadataStatement' => $vendorDir . '/web-auth/webauthn-lib/src/MetadataService/Statement/MetadataStatement.php', + 'Webauthn\\MetadataService\\Statement\\PatternAccuracyDescriptor' => $vendorDir . '/web-auth/webauthn-lib/src/MetadataService/Statement/PatternAccuracyDescriptor.php', + 'Webauthn\\MetadataService\\Statement\\RgbPaletteEntry' => $vendorDir . '/web-auth/webauthn-lib/src/MetadataService/Statement/RgbPaletteEntry.php', + 'Webauthn\\MetadataService\\Statement\\RogueListEntry' => $vendorDir . '/web-auth/webauthn-lib/src/MetadataService/Statement/RogueListEntry.php', + 'Webauthn\\MetadataService\\Statement\\StatusReport' => $vendorDir . '/web-auth/webauthn-lib/src/MetadataService/Statement/StatusReport.php', + 'Webauthn\\MetadataService\\Statement\\VerificationMethodANDCombinations' => $vendorDir . '/web-auth/webauthn-lib/src/MetadataService/Statement/VerificationMethodANDCombinations.php', + 'Webauthn\\MetadataService\\Statement\\VerificationMethodDescriptor' => $vendorDir . '/web-auth/webauthn-lib/src/MetadataService/Statement/VerificationMethodDescriptor.php', + 'Webauthn\\MetadataService\\Statement\\Version' => $vendorDir . '/web-auth/webauthn-lib/src/MetadataService/Statement/Version.php', + 'Webauthn\\MetadataService\\StatusReportRepository' => $vendorDir . '/web-auth/webauthn-lib/src/MetadataService/StatusReportRepository.php', + 'Webauthn\\MetadataService\\ValueFilter' => $vendorDir . '/web-auth/webauthn-lib/src/MetadataService/ValueFilter.php', + 'Webauthn\\PublicKeyCredential' => $vendorDir . '/web-auth/webauthn-lib/src/PublicKeyCredential.php', + 'Webauthn\\PublicKeyCredentialCreationOptions' => $vendorDir . '/web-auth/webauthn-lib/src/PublicKeyCredentialCreationOptions.php', + 'Webauthn\\PublicKeyCredentialDescriptor' => $vendorDir . '/web-auth/webauthn-lib/src/PublicKeyCredentialDescriptor.php', + 'Webauthn\\PublicKeyCredentialDescriptorCollection' => $vendorDir . '/web-auth/webauthn-lib/src/PublicKeyCredentialDescriptorCollection.php', + 'Webauthn\\PublicKeyCredentialEntity' => $vendorDir . '/web-auth/webauthn-lib/src/PublicKeyCredentialEntity.php', + 'Webauthn\\PublicKeyCredentialLoader' => $vendorDir . '/web-auth/webauthn-lib/src/PublicKeyCredentialLoader.php', + 'Webauthn\\PublicKeyCredentialOptions' => $vendorDir . '/web-auth/webauthn-lib/src/PublicKeyCredentialOptions.php', + 'Webauthn\\PublicKeyCredentialParameters' => $vendorDir . '/web-auth/webauthn-lib/src/PublicKeyCredentialParameters.php', + 'Webauthn\\PublicKeyCredentialRequestOptions' => $vendorDir . '/web-auth/webauthn-lib/src/PublicKeyCredentialRequestOptions.php', + 'Webauthn\\PublicKeyCredentialRpEntity' => $vendorDir . '/web-auth/webauthn-lib/src/PublicKeyCredentialRpEntity.php', + 'Webauthn\\PublicKeyCredentialSource' => $vendorDir . '/web-auth/webauthn-lib/src/PublicKeyCredentialSource.php', + 'Webauthn\\PublicKeyCredentialSourceRepository' => $vendorDir . '/web-auth/webauthn-lib/src/PublicKeyCredentialSourceRepository.php', + 'Webauthn\\PublicKeyCredentialUserEntity' => $vendorDir . '/web-auth/webauthn-lib/src/PublicKeyCredentialUserEntity.php', + 'Webauthn\\SimpleFakeCredentialGenerator' => $vendorDir . '/web-auth/webauthn-lib/src/SimpleFakeCredentialGenerator.php', + 'Webauthn\\StringStream' => $vendorDir . '/web-auth/webauthn-lib/src/StringStream.php', + 'Webauthn\\TokenBinding\\IgnoreTokenBindingHandler' => $vendorDir . '/web-auth/webauthn-lib/src/TokenBinding/IgnoreTokenBindingHandler.php', + 'Webauthn\\TokenBinding\\SecTokenBindingHandler' => $vendorDir . '/web-auth/webauthn-lib/src/TokenBinding/SecTokenBindingHandler.php', + 'Webauthn\\TokenBinding\\TokenBinding' => $vendorDir . '/web-auth/webauthn-lib/src/TokenBinding/TokenBinding.php', + 'Webauthn\\TokenBinding\\TokenBindingHandler' => $vendorDir . '/web-auth/webauthn-lib/src/TokenBinding/TokenBindingHandler.php', + 'Webauthn\\TokenBinding\\TokenBindingNotSupportedHandler' => $vendorDir . '/web-auth/webauthn-lib/src/TokenBinding/TokenBindingNotSupportedHandler.php', + 'Webauthn\\TrustPath\\CertificateTrustPath' => $vendorDir . '/web-auth/webauthn-lib/src/TrustPath/CertificateTrustPath.php', + 'Webauthn\\TrustPath\\EcdaaKeyIdTrustPath' => $vendorDir . '/web-auth/webauthn-lib/src/TrustPath/EcdaaKeyIdTrustPath.php', + 'Webauthn\\TrustPath\\EmptyTrustPath' => $vendorDir . '/web-auth/webauthn-lib/src/TrustPath/EmptyTrustPath.php', + 'Webauthn\\TrustPath\\TrustPath' => $vendorDir . '/web-auth/webauthn-lib/src/TrustPath/TrustPath.php', + 'Webauthn\\TrustPath\\TrustPathLoader' => $vendorDir . '/web-auth/webauthn-lib/src/TrustPath/TrustPathLoader.php', + 'Webauthn\\U2FPublicKey' => $vendorDir . '/web-auth/webauthn-lib/src/U2FPublicKey.php', + 'Webauthn\\Util\\Base64' => $vendorDir . '/web-auth/webauthn-lib/src/Util/Base64.php', + 'Webauthn\\Util\\CoseSignatureFixer' => $vendorDir . '/web-auth/webauthn-lib/src/Util/CoseSignatureFixer.php', + 'ZipStreamer\\COMPR' => $vendorDir . '/deepdiver/zipstreamer/src/COMPR.php', + 'ZipStreamer\\Count64' => $vendorDir . '/deepdiver/zipstreamer/src/Count64.php', + 'ZipStreamer\\Lib\\Count64Base' => $vendorDir . '/deepdiver/zipstreamer/src/Lib/Count64Base.php', + 'ZipStreamer\\Lib\\Count64_32' => $vendorDir . '/deepdiver/zipstreamer/src/Lib/Count64_32.php', + 'ZipStreamer\\Lib\\Count64_64' => $vendorDir . '/deepdiver/zipstreamer/src/Lib/Count64_64.php', + 'ZipStreamer\\ZipStreamer' => $vendorDir . '/deepdiver/zipstreamer/src/ZipStreamer.php', + 'bantu\\IniGetWrapper\\IniGetWrapper' => $vendorDir . '/bantu/ini-get-wrapper/src/IniGetWrapper.php', + 'cweagans\\Composer\\PatchEvent' => $vendorDir . '/cweagans/composer-patches/src/PatchEvent.php', + 'cweagans\\Composer\\PatchEvents' => $vendorDir . '/cweagans/composer-patches/src/PatchEvents.php', + 'cweagans\\Composer\\Patches' => $vendorDir . '/cweagans/composer-patches/src/Patches.php', + 'kornrunner\\Blurhash\\AC' => $vendorDir . '/kornrunner/blurhash/src/AC.php', + 'kornrunner\\Blurhash\\Base83' => $vendorDir . '/kornrunner/blurhash/src/Base83.php', + 'kornrunner\\Blurhash\\Blurhash' => $vendorDir . '/kornrunner/blurhash/src/Blurhash.php', + 'kornrunner\\Blurhash\\Color' => $vendorDir . '/kornrunner/blurhash/src/Color.php', + 'kornrunner\\Blurhash\\DC' => $vendorDir . '/kornrunner/blurhash/src/DC.php', + 'libphonenumber\\CountryCodeSource' => $vendorDir . '/giggsey/libphonenumber-for-php-lite/src/CountryCodeSource.php', + 'libphonenumber\\CountryCodeToRegionCodeMap' => $vendorDir . '/giggsey/libphonenumber-for-php-lite/src/CountryCodeToRegionCodeMap.php', + 'libphonenumber\\MatchType' => $vendorDir . '/giggsey/libphonenumber-for-php-lite/src/MatchType.php', + 'libphonenumber\\Matcher' => $vendorDir . '/giggsey/libphonenumber-for-php-lite/src/Matcher.php', + 'libphonenumber\\MatcherAPIInterface' => $vendorDir . '/giggsey/libphonenumber-for-php-lite/src/MatcherAPIInterface.php', + 'libphonenumber\\MetadataSourceInterface' => $vendorDir . '/giggsey/libphonenumber-for-php-lite/src/MetadataSourceInterface.php', + 'libphonenumber\\MultiFileMetadataSourceImpl' => $vendorDir . '/giggsey/libphonenumber-for-php-lite/src/MultiFileMetadataSourceImpl.php', + 'libphonenumber\\NumberFormat' => $vendorDir . '/giggsey/libphonenumber-for-php-lite/src/NumberFormat.php', + 'libphonenumber\\NumberParseException' => $vendorDir . '/giggsey/libphonenumber-for-php-lite/src/NumberParseException.php', + 'libphonenumber\\PhoneMetadata' => $vendorDir . '/giggsey/libphonenumber-for-php-lite/src/PhoneMetadata.php', + 'libphonenumber\\PhoneNumber' => $vendorDir . '/giggsey/libphonenumber-for-php-lite/src/PhoneNumber.php', + 'libphonenumber\\PhoneNumberDesc' => $vendorDir . '/giggsey/libphonenumber-for-php-lite/src/PhoneNumberDesc.php', + 'libphonenumber\\PhoneNumberFormat' => $vendorDir . '/giggsey/libphonenumber-for-php-lite/src/PhoneNumberFormat.php', + 'libphonenumber\\PhoneNumberMatch' => $vendorDir . '/giggsey/libphonenumber-for-php-lite/src/PhoneNumberMatch.php', + 'libphonenumber\\PhoneNumberType' => $vendorDir . '/giggsey/libphonenumber-for-php-lite/src/PhoneNumberType.php', + 'libphonenumber\\PhoneNumberUtil' => $vendorDir . '/giggsey/libphonenumber-for-php-lite/src/PhoneNumberUtil.php', + 'libphonenumber\\RegexBasedMatcher' => $vendorDir . '/giggsey/libphonenumber-for-php-lite/src/RegexBasedMatcher.php', + 'libphonenumber\\ShortNumberCost' => $vendorDir . '/giggsey/libphonenumber-for-php-lite/src/ShortNumberCost.php', + 'libphonenumber\\ShortNumberInfo' => $vendorDir . '/giggsey/libphonenumber-for-php-lite/src/ShortNumberInfo.php', + 'libphonenumber\\ShortNumbersRegionCodeSet' => $vendorDir . '/giggsey/libphonenumber-for-php-lite/src/ShortNumbersRegionCodeSet.php', + 'libphonenumber\\ValidationResult' => $vendorDir . '/giggsey/libphonenumber-for-php-lite/src/ValidationResult.php', + 'libphonenumber\\data\\PhoneNumberMetadata_800' => $vendorDir . '/giggsey/libphonenumber-for-php-lite/src/data/PhoneNumberMetadata_800.php', + 'libphonenumber\\data\\PhoneNumberMetadata_808' => $vendorDir . '/giggsey/libphonenumber-for-php-lite/src/data/PhoneNumberMetadata_808.php', + 'libphonenumber\\data\\PhoneNumberMetadata_870' => $vendorDir . '/giggsey/libphonenumber-for-php-lite/src/data/PhoneNumberMetadata_870.php', + 'libphonenumber\\data\\PhoneNumberMetadata_878' => $vendorDir . '/giggsey/libphonenumber-for-php-lite/src/data/PhoneNumberMetadata_878.php', + 'libphonenumber\\data\\PhoneNumberMetadata_881' => $vendorDir . '/giggsey/libphonenumber-for-php-lite/src/data/PhoneNumberMetadata_881.php', + 'libphonenumber\\data\\PhoneNumberMetadata_882' => $vendorDir . '/giggsey/libphonenumber-for-php-lite/src/data/PhoneNumberMetadata_882.php', + 'libphonenumber\\data\\PhoneNumberMetadata_883' => $vendorDir . '/giggsey/libphonenumber-for-php-lite/src/data/PhoneNumberMetadata_883.php', + 'libphonenumber\\data\\PhoneNumberMetadata_888' => $vendorDir . '/giggsey/libphonenumber-for-php-lite/src/data/PhoneNumberMetadata_888.php', + 'libphonenumber\\data\\PhoneNumberMetadata_979' => $vendorDir . '/giggsey/libphonenumber-for-php-lite/src/data/PhoneNumberMetadata_979.php', + 'libphonenumber\\data\\PhoneNumberMetadata_AC' => $vendorDir . '/giggsey/libphonenumber-for-php-lite/src/data/PhoneNumberMetadata_AC.php', + 'libphonenumber\\data\\PhoneNumberMetadata_AD' => $vendorDir . '/giggsey/libphonenumber-for-php-lite/src/data/PhoneNumberMetadata_AD.php', + 'libphonenumber\\data\\PhoneNumberMetadata_AE' => $vendorDir . '/giggsey/libphonenumber-for-php-lite/src/data/PhoneNumberMetadata_AE.php', + 'libphonenumber\\data\\PhoneNumberMetadata_AF' => $vendorDir . '/giggsey/libphonenumber-for-php-lite/src/data/PhoneNumberMetadata_AF.php', + 'libphonenumber\\data\\PhoneNumberMetadata_AG' => $vendorDir . '/giggsey/libphonenumber-for-php-lite/src/data/PhoneNumberMetadata_AG.php', + 'libphonenumber\\data\\PhoneNumberMetadata_AI' => $vendorDir . '/giggsey/libphonenumber-for-php-lite/src/data/PhoneNumberMetadata_AI.php', + 'libphonenumber\\data\\PhoneNumberMetadata_AL' => $vendorDir . '/giggsey/libphonenumber-for-php-lite/src/data/PhoneNumberMetadata_AL.php', + 'libphonenumber\\data\\PhoneNumberMetadata_AM' => $vendorDir . '/giggsey/libphonenumber-for-php-lite/src/data/PhoneNumberMetadata_AM.php', + 'libphonenumber\\data\\PhoneNumberMetadata_AO' => $vendorDir . '/giggsey/libphonenumber-for-php-lite/src/data/PhoneNumberMetadata_AO.php', + 'libphonenumber\\data\\PhoneNumberMetadata_AR' => $vendorDir . '/giggsey/libphonenumber-for-php-lite/src/data/PhoneNumberMetadata_AR.php', + 'libphonenumber\\data\\PhoneNumberMetadata_AS' => $vendorDir . '/giggsey/libphonenumber-for-php-lite/src/data/PhoneNumberMetadata_AS.php', + 'libphonenumber\\data\\PhoneNumberMetadata_AT' => $vendorDir . '/giggsey/libphonenumber-for-php-lite/src/data/PhoneNumberMetadata_AT.php', + 'libphonenumber\\data\\PhoneNumberMetadata_AU' => $vendorDir . '/giggsey/libphonenumber-for-php-lite/src/data/PhoneNumberMetadata_AU.php', + 'libphonenumber\\data\\PhoneNumberMetadata_AW' => $vendorDir . '/giggsey/libphonenumber-for-php-lite/src/data/PhoneNumberMetadata_AW.php', + 'libphonenumber\\data\\PhoneNumberMetadata_AX' => $vendorDir . '/giggsey/libphonenumber-for-php-lite/src/data/PhoneNumberMetadata_AX.php', + 'libphonenumber\\data\\PhoneNumberMetadata_AZ' => $vendorDir . '/giggsey/libphonenumber-for-php-lite/src/data/PhoneNumberMetadata_AZ.php', + 'libphonenumber\\data\\PhoneNumberMetadata_BA' => $vendorDir . '/giggsey/libphonenumber-for-php-lite/src/data/PhoneNumberMetadata_BA.php', + 'libphonenumber\\data\\PhoneNumberMetadata_BB' => $vendorDir . '/giggsey/libphonenumber-for-php-lite/src/data/PhoneNumberMetadata_BB.php', + 'libphonenumber\\data\\PhoneNumberMetadata_BD' => $vendorDir . '/giggsey/libphonenumber-for-php-lite/src/data/PhoneNumberMetadata_BD.php', + 'libphonenumber\\data\\PhoneNumberMetadata_BE' => $vendorDir . '/giggsey/libphonenumber-for-php-lite/src/data/PhoneNumberMetadata_BE.php', + 'libphonenumber\\data\\PhoneNumberMetadata_BF' => $vendorDir . '/giggsey/libphonenumber-for-php-lite/src/data/PhoneNumberMetadata_BF.php', + 'libphonenumber\\data\\PhoneNumberMetadata_BG' => $vendorDir . '/giggsey/libphonenumber-for-php-lite/src/data/PhoneNumberMetadata_BG.php', + 'libphonenumber\\data\\PhoneNumberMetadata_BH' => $vendorDir . '/giggsey/libphonenumber-for-php-lite/src/data/PhoneNumberMetadata_BH.php', + 'libphonenumber\\data\\PhoneNumberMetadata_BI' => $vendorDir . '/giggsey/libphonenumber-for-php-lite/src/data/PhoneNumberMetadata_BI.php', + 'libphonenumber\\data\\PhoneNumberMetadata_BJ' => $vendorDir . '/giggsey/libphonenumber-for-php-lite/src/data/PhoneNumberMetadata_BJ.php', + 'libphonenumber\\data\\PhoneNumberMetadata_BL' => $vendorDir . '/giggsey/libphonenumber-for-php-lite/src/data/PhoneNumberMetadata_BL.php', + 'libphonenumber\\data\\PhoneNumberMetadata_BM' => $vendorDir . '/giggsey/libphonenumber-for-php-lite/src/data/PhoneNumberMetadata_BM.php', + 'libphonenumber\\data\\PhoneNumberMetadata_BN' => $vendorDir . '/giggsey/libphonenumber-for-php-lite/src/data/PhoneNumberMetadata_BN.php', + 'libphonenumber\\data\\PhoneNumberMetadata_BO' => $vendorDir . '/giggsey/libphonenumber-for-php-lite/src/data/PhoneNumberMetadata_BO.php', + 'libphonenumber\\data\\PhoneNumberMetadata_BQ' => $vendorDir . '/giggsey/libphonenumber-for-php-lite/src/data/PhoneNumberMetadata_BQ.php', + 'libphonenumber\\data\\PhoneNumberMetadata_BR' => $vendorDir . '/giggsey/libphonenumber-for-php-lite/src/data/PhoneNumberMetadata_BR.php', + 'libphonenumber\\data\\PhoneNumberMetadata_BS' => $vendorDir . '/giggsey/libphonenumber-for-php-lite/src/data/PhoneNumberMetadata_BS.php', + 'libphonenumber\\data\\PhoneNumberMetadata_BT' => $vendorDir . '/giggsey/libphonenumber-for-php-lite/src/data/PhoneNumberMetadata_BT.php', + 'libphonenumber\\data\\PhoneNumberMetadata_BW' => $vendorDir . '/giggsey/libphonenumber-for-php-lite/src/data/PhoneNumberMetadata_BW.php', + 'libphonenumber\\data\\PhoneNumberMetadata_BY' => $vendorDir . '/giggsey/libphonenumber-for-php-lite/src/data/PhoneNumberMetadata_BY.php', + 'libphonenumber\\data\\PhoneNumberMetadata_BZ' => $vendorDir . '/giggsey/libphonenumber-for-php-lite/src/data/PhoneNumberMetadata_BZ.php', + 'libphonenumber\\data\\PhoneNumberMetadata_CA' => $vendorDir . '/giggsey/libphonenumber-for-php-lite/src/data/PhoneNumberMetadata_CA.php', + 'libphonenumber\\data\\PhoneNumberMetadata_CC' => $vendorDir . '/giggsey/libphonenumber-for-php-lite/src/data/PhoneNumberMetadata_CC.php', + 'libphonenumber\\data\\PhoneNumberMetadata_CD' => $vendorDir . '/giggsey/libphonenumber-for-php-lite/src/data/PhoneNumberMetadata_CD.php', + 'libphonenumber\\data\\PhoneNumberMetadata_CF' => $vendorDir . '/giggsey/libphonenumber-for-php-lite/src/data/PhoneNumberMetadata_CF.php', + 'libphonenumber\\data\\PhoneNumberMetadata_CG' => $vendorDir . '/giggsey/libphonenumber-for-php-lite/src/data/PhoneNumberMetadata_CG.php', + 'libphonenumber\\data\\PhoneNumberMetadata_CH' => $vendorDir . '/giggsey/libphonenumber-for-php-lite/src/data/PhoneNumberMetadata_CH.php', + 'libphonenumber\\data\\PhoneNumberMetadata_CI' => $vendorDir . '/giggsey/libphonenumber-for-php-lite/src/data/PhoneNumberMetadata_CI.php', + 'libphonenumber\\data\\PhoneNumberMetadata_CK' => $vendorDir . '/giggsey/libphonenumber-for-php-lite/src/data/PhoneNumberMetadata_CK.php', + 'libphonenumber\\data\\PhoneNumberMetadata_CL' => $vendorDir . '/giggsey/libphonenumber-for-php-lite/src/data/PhoneNumberMetadata_CL.php', + 'libphonenumber\\data\\PhoneNumberMetadata_CM' => $vendorDir . '/giggsey/libphonenumber-for-php-lite/src/data/PhoneNumberMetadata_CM.php', + 'libphonenumber\\data\\PhoneNumberMetadata_CN' => $vendorDir . '/giggsey/libphonenumber-for-php-lite/src/data/PhoneNumberMetadata_CN.php', + 'libphonenumber\\data\\PhoneNumberMetadata_CO' => $vendorDir . '/giggsey/libphonenumber-for-php-lite/src/data/PhoneNumberMetadata_CO.php', + 'libphonenumber\\data\\PhoneNumberMetadata_CR' => $vendorDir . '/giggsey/libphonenumber-for-php-lite/src/data/PhoneNumberMetadata_CR.php', + 'libphonenumber\\data\\PhoneNumberMetadata_CU' => $vendorDir . '/giggsey/libphonenumber-for-php-lite/src/data/PhoneNumberMetadata_CU.php', + 'libphonenumber\\data\\PhoneNumberMetadata_CV' => $vendorDir . '/giggsey/libphonenumber-for-php-lite/src/data/PhoneNumberMetadata_CV.php', + 'libphonenumber\\data\\PhoneNumberMetadata_CW' => $vendorDir . '/giggsey/libphonenumber-for-php-lite/src/data/PhoneNumberMetadata_CW.php', + 'libphonenumber\\data\\PhoneNumberMetadata_CX' => $vendorDir . '/giggsey/libphonenumber-for-php-lite/src/data/PhoneNumberMetadata_CX.php', + 'libphonenumber\\data\\PhoneNumberMetadata_CY' => $vendorDir . '/giggsey/libphonenumber-for-php-lite/src/data/PhoneNumberMetadata_CY.php', + 'libphonenumber\\data\\PhoneNumberMetadata_CZ' => $vendorDir . '/giggsey/libphonenumber-for-php-lite/src/data/PhoneNumberMetadata_CZ.php', + 'libphonenumber\\data\\PhoneNumberMetadata_DE' => $vendorDir . '/giggsey/libphonenumber-for-php-lite/src/data/PhoneNumberMetadata_DE.php', + 'libphonenumber\\data\\PhoneNumberMetadata_DJ' => $vendorDir . '/giggsey/libphonenumber-for-php-lite/src/data/PhoneNumberMetadata_DJ.php', + 'libphonenumber\\data\\PhoneNumberMetadata_DK' => $vendorDir . '/giggsey/libphonenumber-for-php-lite/src/data/PhoneNumberMetadata_DK.php', + 'libphonenumber\\data\\PhoneNumberMetadata_DM' => $vendorDir . '/giggsey/libphonenumber-for-php-lite/src/data/PhoneNumberMetadata_DM.php', + 'libphonenumber\\data\\PhoneNumberMetadata_DO' => $vendorDir . '/giggsey/libphonenumber-for-php-lite/src/data/PhoneNumberMetadata_DO.php', + 'libphonenumber\\data\\PhoneNumberMetadata_DZ' => $vendorDir . '/giggsey/libphonenumber-for-php-lite/src/data/PhoneNumberMetadata_DZ.php', + 'libphonenumber\\data\\PhoneNumberMetadata_EC' => $vendorDir . '/giggsey/libphonenumber-for-php-lite/src/data/PhoneNumberMetadata_EC.php', + 'libphonenumber\\data\\PhoneNumberMetadata_EE' => $vendorDir . '/giggsey/libphonenumber-for-php-lite/src/data/PhoneNumberMetadata_EE.php', + 'libphonenumber\\data\\PhoneNumberMetadata_EG' => $vendorDir . '/giggsey/libphonenumber-for-php-lite/src/data/PhoneNumberMetadata_EG.php', + 'libphonenumber\\data\\PhoneNumberMetadata_EH' => $vendorDir . '/giggsey/libphonenumber-for-php-lite/src/data/PhoneNumberMetadata_EH.php', + 'libphonenumber\\data\\PhoneNumberMetadata_ER' => $vendorDir . '/giggsey/libphonenumber-for-php-lite/src/data/PhoneNumberMetadata_ER.php', + 'libphonenumber\\data\\PhoneNumberMetadata_ES' => $vendorDir . '/giggsey/libphonenumber-for-php-lite/src/data/PhoneNumberMetadata_ES.php', + 'libphonenumber\\data\\PhoneNumberMetadata_ET' => $vendorDir . '/giggsey/libphonenumber-for-php-lite/src/data/PhoneNumberMetadata_ET.php', + 'libphonenumber\\data\\PhoneNumberMetadata_FI' => $vendorDir . '/giggsey/libphonenumber-for-php-lite/src/data/PhoneNumberMetadata_FI.php', + 'libphonenumber\\data\\PhoneNumberMetadata_FJ' => $vendorDir . '/giggsey/libphonenumber-for-php-lite/src/data/PhoneNumberMetadata_FJ.php', + 'libphonenumber\\data\\PhoneNumberMetadata_FK' => $vendorDir . '/giggsey/libphonenumber-for-php-lite/src/data/PhoneNumberMetadata_FK.php', + 'libphonenumber\\data\\PhoneNumberMetadata_FM' => $vendorDir . '/giggsey/libphonenumber-for-php-lite/src/data/PhoneNumberMetadata_FM.php', + 'libphonenumber\\data\\PhoneNumberMetadata_FO' => $vendorDir . '/giggsey/libphonenumber-for-php-lite/src/data/PhoneNumberMetadata_FO.php', + 'libphonenumber\\data\\PhoneNumberMetadata_FR' => $vendorDir . '/giggsey/libphonenumber-for-php-lite/src/data/PhoneNumberMetadata_FR.php', + 'libphonenumber\\data\\PhoneNumberMetadata_GA' => $vendorDir . '/giggsey/libphonenumber-for-php-lite/src/data/PhoneNumberMetadata_GA.php', + 'libphonenumber\\data\\PhoneNumberMetadata_GB' => $vendorDir . '/giggsey/libphonenumber-for-php-lite/src/data/PhoneNumberMetadata_GB.php', + 'libphonenumber\\data\\PhoneNumberMetadata_GD' => $vendorDir . '/giggsey/libphonenumber-for-php-lite/src/data/PhoneNumberMetadata_GD.php', + 'libphonenumber\\data\\PhoneNumberMetadata_GE' => $vendorDir . '/giggsey/libphonenumber-for-php-lite/src/data/PhoneNumberMetadata_GE.php', + 'libphonenumber\\data\\PhoneNumberMetadata_GF' => $vendorDir . '/giggsey/libphonenumber-for-php-lite/src/data/PhoneNumberMetadata_GF.php', + 'libphonenumber\\data\\PhoneNumberMetadata_GG' => $vendorDir . '/giggsey/libphonenumber-for-php-lite/src/data/PhoneNumberMetadata_GG.php', + 'libphonenumber\\data\\PhoneNumberMetadata_GH' => $vendorDir . '/giggsey/libphonenumber-for-php-lite/src/data/PhoneNumberMetadata_GH.php', + 'libphonenumber\\data\\PhoneNumberMetadata_GI' => $vendorDir . '/giggsey/libphonenumber-for-php-lite/src/data/PhoneNumberMetadata_GI.php', + 'libphonenumber\\data\\PhoneNumberMetadata_GL' => $vendorDir . '/giggsey/libphonenumber-for-php-lite/src/data/PhoneNumberMetadata_GL.php', + 'libphonenumber\\data\\PhoneNumberMetadata_GM' => $vendorDir . '/giggsey/libphonenumber-for-php-lite/src/data/PhoneNumberMetadata_GM.php', + 'libphonenumber\\data\\PhoneNumberMetadata_GN' => $vendorDir . '/giggsey/libphonenumber-for-php-lite/src/data/PhoneNumberMetadata_GN.php', + 'libphonenumber\\data\\PhoneNumberMetadata_GP' => $vendorDir . '/giggsey/libphonenumber-for-php-lite/src/data/PhoneNumberMetadata_GP.php', + 'libphonenumber\\data\\PhoneNumberMetadata_GQ' => $vendorDir . '/giggsey/libphonenumber-for-php-lite/src/data/PhoneNumberMetadata_GQ.php', + 'libphonenumber\\data\\PhoneNumberMetadata_GR' => $vendorDir . '/giggsey/libphonenumber-for-php-lite/src/data/PhoneNumberMetadata_GR.php', + 'libphonenumber\\data\\PhoneNumberMetadata_GT' => $vendorDir . '/giggsey/libphonenumber-for-php-lite/src/data/PhoneNumberMetadata_GT.php', + 'libphonenumber\\data\\PhoneNumberMetadata_GU' => $vendorDir . '/giggsey/libphonenumber-for-php-lite/src/data/PhoneNumberMetadata_GU.php', + 'libphonenumber\\data\\PhoneNumberMetadata_GW' => $vendorDir . '/giggsey/libphonenumber-for-php-lite/src/data/PhoneNumberMetadata_GW.php', + 'libphonenumber\\data\\PhoneNumberMetadata_GY' => $vendorDir . '/giggsey/libphonenumber-for-php-lite/src/data/PhoneNumberMetadata_GY.php', + 'libphonenumber\\data\\PhoneNumberMetadata_HK' => $vendorDir . '/giggsey/libphonenumber-for-php-lite/src/data/PhoneNumberMetadata_HK.php', + 'libphonenumber\\data\\PhoneNumberMetadata_HN' => $vendorDir . '/giggsey/libphonenumber-for-php-lite/src/data/PhoneNumberMetadata_HN.php', + 'libphonenumber\\data\\PhoneNumberMetadata_HR' => $vendorDir . '/giggsey/libphonenumber-for-php-lite/src/data/PhoneNumberMetadata_HR.php', + 'libphonenumber\\data\\PhoneNumberMetadata_HT' => $vendorDir . '/giggsey/libphonenumber-for-php-lite/src/data/PhoneNumberMetadata_HT.php', + 'libphonenumber\\data\\PhoneNumberMetadata_HU' => $vendorDir . '/giggsey/libphonenumber-for-php-lite/src/data/PhoneNumberMetadata_HU.php', + 'libphonenumber\\data\\PhoneNumberMetadata_ID' => $vendorDir . '/giggsey/libphonenumber-for-php-lite/src/data/PhoneNumberMetadata_ID.php', + 'libphonenumber\\data\\PhoneNumberMetadata_IE' => $vendorDir . '/giggsey/libphonenumber-for-php-lite/src/data/PhoneNumberMetadata_IE.php', + 'libphonenumber\\data\\PhoneNumberMetadata_IL' => $vendorDir . '/giggsey/libphonenumber-for-php-lite/src/data/PhoneNumberMetadata_IL.php', + 'libphonenumber\\data\\PhoneNumberMetadata_IM' => $vendorDir . '/giggsey/libphonenumber-for-php-lite/src/data/PhoneNumberMetadata_IM.php', + 'libphonenumber\\data\\PhoneNumberMetadata_IN' => $vendorDir . '/giggsey/libphonenumber-for-php-lite/src/data/PhoneNumberMetadata_IN.php', + 'libphonenumber\\data\\PhoneNumberMetadata_IO' => $vendorDir . '/giggsey/libphonenumber-for-php-lite/src/data/PhoneNumberMetadata_IO.php', + 'libphonenumber\\data\\PhoneNumberMetadata_IQ' => $vendorDir . '/giggsey/libphonenumber-for-php-lite/src/data/PhoneNumberMetadata_IQ.php', + 'libphonenumber\\data\\PhoneNumberMetadata_IR' => $vendorDir . '/giggsey/libphonenumber-for-php-lite/src/data/PhoneNumberMetadata_IR.php', + 'libphonenumber\\data\\PhoneNumberMetadata_IS' => $vendorDir . '/giggsey/libphonenumber-for-php-lite/src/data/PhoneNumberMetadata_IS.php', + 'libphonenumber\\data\\PhoneNumberMetadata_IT' => $vendorDir . '/giggsey/libphonenumber-for-php-lite/src/data/PhoneNumberMetadata_IT.php', + 'libphonenumber\\data\\PhoneNumberMetadata_JE' => $vendorDir . '/giggsey/libphonenumber-for-php-lite/src/data/PhoneNumberMetadata_JE.php', + 'libphonenumber\\data\\PhoneNumberMetadata_JM' => $vendorDir . '/giggsey/libphonenumber-for-php-lite/src/data/PhoneNumberMetadata_JM.php', + 'libphonenumber\\data\\PhoneNumberMetadata_JO' => $vendorDir . '/giggsey/libphonenumber-for-php-lite/src/data/PhoneNumberMetadata_JO.php', + 'libphonenumber\\data\\PhoneNumberMetadata_JP' => $vendorDir . '/giggsey/libphonenumber-for-php-lite/src/data/PhoneNumberMetadata_JP.php', + 'libphonenumber\\data\\PhoneNumberMetadata_KE' => $vendorDir . '/giggsey/libphonenumber-for-php-lite/src/data/PhoneNumberMetadata_KE.php', + 'libphonenumber\\data\\PhoneNumberMetadata_KG' => $vendorDir . '/giggsey/libphonenumber-for-php-lite/src/data/PhoneNumberMetadata_KG.php', + 'libphonenumber\\data\\PhoneNumberMetadata_KH' => $vendorDir . '/giggsey/libphonenumber-for-php-lite/src/data/PhoneNumberMetadata_KH.php', + 'libphonenumber\\data\\PhoneNumberMetadata_KI' => $vendorDir . '/giggsey/libphonenumber-for-php-lite/src/data/PhoneNumberMetadata_KI.php', + 'libphonenumber\\data\\PhoneNumberMetadata_KM' => $vendorDir . '/giggsey/libphonenumber-for-php-lite/src/data/PhoneNumberMetadata_KM.php', + 'libphonenumber\\data\\PhoneNumberMetadata_KN' => $vendorDir . '/giggsey/libphonenumber-for-php-lite/src/data/PhoneNumberMetadata_KN.php', + 'libphonenumber\\data\\PhoneNumberMetadata_KP' => $vendorDir . '/giggsey/libphonenumber-for-php-lite/src/data/PhoneNumberMetadata_KP.php', + 'libphonenumber\\data\\PhoneNumberMetadata_KR' => $vendorDir . '/giggsey/libphonenumber-for-php-lite/src/data/PhoneNumberMetadata_KR.php', + 'libphonenumber\\data\\PhoneNumberMetadata_KW' => $vendorDir . '/giggsey/libphonenumber-for-php-lite/src/data/PhoneNumberMetadata_KW.php', + 'libphonenumber\\data\\PhoneNumberMetadata_KY' => $vendorDir . '/giggsey/libphonenumber-for-php-lite/src/data/PhoneNumberMetadata_KY.php', + 'libphonenumber\\data\\PhoneNumberMetadata_KZ' => $vendorDir . '/giggsey/libphonenumber-for-php-lite/src/data/PhoneNumberMetadata_KZ.php', + 'libphonenumber\\data\\PhoneNumberMetadata_LA' => $vendorDir . '/giggsey/libphonenumber-for-php-lite/src/data/PhoneNumberMetadata_LA.php', + 'libphonenumber\\data\\PhoneNumberMetadata_LB' => $vendorDir . '/giggsey/libphonenumber-for-php-lite/src/data/PhoneNumberMetadata_LB.php', + 'libphonenumber\\data\\PhoneNumberMetadata_LC' => $vendorDir . '/giggsey/libphonenumber-for-php-lite/src/data/PhoneNumberMetadata_LC.php', + 'libphonenumber\\data\\PhoneNumberMetadata_LI' => $vendorDir . '/giggsey/libphonenumber-for-php-lite/src/data/PhoneNumberMetadata_LI.php', + 'libphonenumber\\data\\PhoneNumberMetadata_LK' => $vendorDir . '/giggsey/libphonenumber-for-php-lite/src/data/PhoneNumberMetadata_LK.php', + 'libphonenumber\\data\\PhoneNumberMetadata_LR' => $vendorDir . '/giggsey/libphonenumber-for-php-lite/src/data/PhoneNumberMetadata_LR.php', + 'libphonenumber\\data\\PhoneNumberMetadata_LS' => $vendorDir . '/giggsey/libphonenumber-for-php-lite/src/data/PhoneNumberMetadata_LS.php', + 'libphonenumber\\data\\PhoneNumberMetadata_LT' => $vendorDir . '/giggsey/libphonenumber-for-php-lite/src/data/PhoneNumberMetadata_LT.php', + 'libphonenumber\\data\\PhoneNumberMetadata_LU' => $vendorDir . '/giggsey/libphonenumber-for-php-lite/src/data/PhoneNumberMetadata_LU.php', + 'libphonenumber\\data\\PhoneNumberMetadata_LV' => $vendorDir . '/giggsey/libphonenumber-for-php-lite/src/data/PhoneNumberMetadata_LV.php', + 'libphonenumber\\data\\PhoneNumberMetadata_LY' => $vendorDir . '/giggsey/libphonenumber-for-php-lite/src/data/PhoneNumberMetadata_LY.php', + 'libphonenumber\\data\\PhoneNumberMetadata_MA' => $vendorDir . '/giggsey/libphonenumber-for-php-lite/src/data/PhoneNumberMetadata_MA.php', + 'libphonenumber\\data\\PhoneNumberMetadata_MC' => $vendorDir . '/giggsey/libphonenumber-for-php-lite/src/data/PhoneNumberMetadata_MC.php', + 'libphonenumber\\data\\PhoneNumberMetadata_MD' => $vendorDir . '/giggsey/libphonenumber-for-php-lite/src/data/PhoneNumberMetadata_MD.php', + 'libphonenumber\\data\\PhoneNumberMetadata_ME' => $vendorDir . '/giggsey/libphonenumber-for-php-lite/src/data/PhoneNumberMetadata_ME.php', + 'libphonenumber\\data\\PhoneNumberMetadata_MF' => $vendorDir . '/giggsey/libphonenumber-for-php-lite/src/data/PhoneNumberMetadata_MF.php', + 'libphonenumber\\data\\PhoneNumberMetadata_MG' => $vendorDir . '/giggsey/libphonenumber-for-php-lite/src/data/PhoneNumberMetadata_MG.php', + 'libphonenumber\\data\\PhoneNumberMetadata_MH' => $vendorDir . '/giggsey/libphonenumber-for-php-lite/src/data/PhoneNumberMetadata_MH.php', + 'libphonenumber\\data\\PhoneNumberMetadata_MK' => $vendorDir . '/giggsey/libphonenumber-for-php-lite/src/data/PhoneNumberMetadata_MK.php', + 'libphonenumber\\data\\PhoneNumberMetadata_ML' => $vendorDir . '/giggsey/libphonenumber-for-php-lite/src/data/PhoneNumberMetadata_ML.php', + 'libphonenumber\\data\\PhoneNumberMetadata_MM' => $vendorDir . '/giggsey/libphonenumber-for-php-lite/src/data/PhoneNumberMetadata_MM.php', + 'libphonenumber\\data\\PhoneNumberMetadata_MN' => $vendorDir . '/giggsey/libphonenumber-for-php-lite/src/data/PhoneNumberMetadata_MN.php', + 'libphonenumber\\data\\PhoneNumberMetadata_MO' => $vendorDir . '/giggsey/libphonenumber-for-php-lite/src/data/PhoneNumberMetadata_MO.php', + 'libphonenumber\\data\\PhoneNumberMetadata_MP' => $vendorDir . '/giggsey/libphonenumber-for-php-lite/src/data/PhoneNumberMetadata_MP.php', + 'libphonenumber\\data\\PhoneNumberMetadata_MQ' => $vendorDir . '/giggsey/libphonenumber-for-php-lite/src/data/PhoneNumberMetadata_MQ.php', + 'libphonenumber\\data\\PhoneNumberMetadata_MR' => $vendorDir . '/giggsey/libphonenumber-for-php-lite/src/data/PhoneNumberMetadata_MR.php', + 'libphonenumber\\data\\PhoneNumberMetadata_MS' => $vendorDir . '/giggsey/libphonenumber-for-php-lite/src/data/PhoneNumberMetadata_MS.php', + 'libphonenumber\\data\\PhoneNumberMetadata_MT' => $vendorDir . '/giggsey/libphonenumber-for-php-lite/src/data/PhoneNumberMetadata_MT.php', + 'libphonenumber\\data\\PhoneNumberMetadata_MU' => $vendorDir . '/giggsey/libphonenumber-for-php-lite/src/data/PhoneNumberMetadata_MU.php', + 'libphonenumber\\data\\PhoneNumberMetadata_MV' => $vendorDir . '/giggsey/libphonenumber-for-php-lite/src/data/PhoneNumberMetadata_MV.php', + 'libphonenumber\\data\\PhoneNumberMetadata_MW' => $vendorDir . '/giggsey/libphonenumber-for-php-lite/src/data/PhoneNumberMetadata_MW.php', + 'libphonenumber\\data\\PhoneNumberMetadata_MX' => $vendorDir . '/giggsey/libphonenumber-for-php-lite/src/data/PhoneNumberMetadata_MX.php', + 'libphonenumber\\data\\PhoneNumberMetadata_MY' => $vendorDir . '/giggsey/libphonenumber-for-php-lite/src/data/PhoneNumberMetadata_MY.php', + 'libphonenumber\\data\\PhoneNumberMetadata_MZ' => $vendorDir . '/giggsey/libphonenumber-for-php-lite/src/data/PhoneNumberMetadata_MZ.php', + 'libphonenumber\\data\\PhoneNumberMetadata_NA' => $vendorDir . '/giggsey/libphonenumber-for-php-lite/src/data/PhoneNumberMetadata_NA.php', + 'libphonenumber\\data\\PhoneNumberMetadata_NC' => $vendorDir . '/giggsey/libphonenumber-for-php-lite/src/data/PhoneNumberMetadata_NC.php', + 'libphonenumber\\data\\PhoneNumberMetadata_NE' => $vendorDir . '/giggsey/libphonenumber-for-php-lite/src/data/PhoneNumberMetadata_NE.php', + 'libphonenumber\\data\\PhoneNumberMetadata_NF' => $vendorDir . '/giggsey/libphonenumber-for-php-lite/src/data/PhoneNumberMetadata_NF.php', + 'libphonenumber\\data\\PhoneNumberMetadata_NG' => $vendorDir . '/giggsey/libphonenumber-for-php-lite/src/data/PhoneNumberMetadata_NG.php', + 'libphonenumber\\data\\PhoneNumberMetadata_NI' => $vendorDir . '/giggsey/libphonenumber-for-php-lite/src/data/PhoneNumberMetadata_NI.php', + 'libphonenumber\\data\\PhoneNumberMetadata_NL' => $vendorDir . '/giggsey/libphonenumber-for-php-lite/src/data/PhoneNumberMetadata_NL.php', + 'libphonenumber\\data\\PhoneNumberMetadata_NO' => $vendorDir . '/giggsey/libphonenumber-for-php-lite/src/data/PhoneNumberMetadata_NO.php', + 'libphonenumber\\data\\PhoneNumberMetadata_NP' => $vendorDir . '/giggsey/libphonenumber-for-php-lite/src/data/PhoneNumberMetadata_NP.php', + 'libphonenumber\\data\\PhoneNumberMetadata_NR' => $vendorDir . '/giggsey/libphonenumber-for-php-lite/src/data/PhoneNumberMetadata_NR.php', + 'libphonenumber\\data\\PhoneNumberMetadata_NU' => $vendorDir . '/giggsey/libphonenumber-for-php-lite/src/data/PhoneNumberMetadata_NU.php', + 'libphonenumber\\data\\PhoneNumberMetadata_NZ' => $vendorDir . '/giggsey/libphonenumber-for-php-lite/src/data/PhoneNumberMetadata_NZ.php', + 'libphonenumber\\data\\PhoneNumberMetadata_OM' => $vendorDir . '/giggsey/libphonenumber-for-php-lite/src/data/PhoneNumberMetadata_OM.php', + 'libphonenumber\\data\\PhoneNumberMetadata_PA' => $vendorDir . '/giggsey/libphonenumber-for-php-lite/src/data/PhoneNumberMetadata_PA.php', + 'libphonenumber\\data\\PhoneNumberMetadata_PE' => $vendorDir . '/giggsey/libphonenumber-for-php-lite/src/data/PhoneNumberMetadata_PE.php', + 'libphonenumber\\data\\PhoneNumberMetadata_PF' => $vendorDir . '/giggsey/libphonenumber-for-php-lite/src/data/PhoneNumberMetadata_PF.php', + 'libphonenumber\\data\\PhoneNumberMetadata_PG' => $vendorDir . '/giggsey/libphonenumber-for-php-lite/src/data/PhoneNumberMetadata_PG.php', + 'libphonenumber\\data\\PhoneNumberMetadata_PH' => $vendorDir . '/giggsey/libphonenumber-for-php-lite/src/data/PhoneNumberMetadata_PH.php', + 'libphonenumber\\data\\PhoneNumberMetadata_PK' => $vendorDir . '/giggsey/libphonenumber-for-php-lite/src/data/PhoneNumberMetadata_PK.php', + 'libphonenumber\\data\\PhoneNumberMetadata_PL' => $vendorDir . '/giggsey/libphonenumber-for-php-lite/src/data/PhoneNumberMetadata_PL.php', + 'libphonenumber\\data\\PhoneNumberMetadata_PM' => $vendorDir . '/giggsey/libphonenumber-for-php-lite/src/data/PhoneNumberMetadata_PM.php', + 'libphonenumber\\data\\PhoneNumberMetadata_PR' => $vendorDir . '/giggsey/libphonenumber-for-php-lite/src/data/PhoneNumberMetadata_PR.php', + 'libphonenumber\\data\\PhoneNumberMetadata_PS' => $vendorDir . '/giggsey/libphonenumber-for-php-lite/src/data/PhoneNumberMetadata_PS.php', + 'libphonenumber\\data\\PhoneNumberMetadata_PT' => $vendorDir . '/giggsey/libphonenumber-for-php-lite/src/data/PhoneNumberMetadata_PT.php', + 'libphonenumber\\data\\PhoneNumberMetadata_PW' => $vendorDir . '/giggsey/libphonenumber-for-php-lite/src/data/PhoneNumberMetadata_PW.php', + 'libphonenumber\\data\\PhoneNumberMetadata_PY' => $vendorDir . '/giggsey/libphonenumber-for-php-lite/src/data/PhoneNumberMetadata_PY.php', + 'libphonenumber\\data\\PhoneNumberMetadata_QA' => $vendorDir . '/giggsey/libphonenumber-for-php-lite/src/data/PhoneNumberMetadata_QA.php', + 'libphonenumber\\data\\PhoneNumberMetadata_RE' => $vendorDir . '/giggsey/libphonenumber-for-php-lite/src/data/PhoneNumberMetadata_RE.php', + 'libphonenumber\\data\\PhoneNumberMetadata_RO' => $vendorDir . '/giggsey/libphonenumber-for-php-lite/src/data/PhoneNumberMetadata_RO.php', + 'libphonenumber\\data\\PhoneNumberMetadata_RS' => $vendorDir . '/giggsey/libphonenumber-for-php-lite/src/data/PhoneNumberMetadata_RS.php', + 'libphonenumber\\data\\PhoneNumberMetadata_RU' => $vendorDir . '/giggsey/libphonenumber-for-php-lite/src/data/PhoneNumberMetadata_RU.php', + 'libphonenumber\\data\\PhoneNumberMetadata_RW' => $vendorDir . '/giggsey/libphonenumber-for-php-lite/src/data/PhoneNumberMetadata_RW.php', + 'libphonenumber\\data\\PhoneNumberMetadata_SA' => $vendorDir . '/giggsey/libphonenumber-for-php-lite/src/data/PhoneNumberMetadata_SA.php', + 'libphonenumber\\data\\PhoneNumberMetadata_SB' => $vendorDir . '/giggsey/libphonenumber-for-php-lite/src/data/PhoneNumberMetadata_SB.php', + 'libphonenumber\\data\\PhoneNumberMetadata_SC' => $vendorDir . '/giggsey/libphonenumber-for-php-lite/src/data/PhoneNumberMetadata_SC.php', + 'libphonenumber\\data\\PhoneNumberMetadata_SD' => $vendorDir . '/giggsey/libphonenumber-for-php-lite/src/data/PhoneNumberMetadata_SD.php', + 'libphonenumber\\data\\PhoneNumberMetadata_SE' => $vendorDir . '/giggsey/libphonenumber-for-php-lite/src/data/PhoneNumberMetadata_SE.php', + 'libphonenumber\\data\\PhoneNumberMetadata_SG' => $vendorDir . '/giggsey/libphonenumber-for-php-lite/src/data/PhoneNumberMetadata_SG.php', + 'libphonenumber\\data\\PhoneNumberMetadata_SH' => $vendorDir . '/giggsey/libphonenumber-for-php-lite/src/data/PhoneNumberMetadata_SH.php', + 'libphonenumber\\data\\PhoneNumberMetadata_SI' => $vendorDir . '/giggsey/libphonenumber-for-php-lite/src/data/PhoneNumberMetadata_SI.php', + 'libphonenumber\\data\\PhoneNumberMetadata_SJ' => $vendorDir . '/giggsey/libphonenumber-for-php-lite/src/data/PhoneNumberMetadata_SJ.php', + 'libphonenumber\\data\\PhoneNumberMetadata_SK' => $vendorDir . '/giggsey/libphonenumber-for-php-lite/src/data/PhoneNumberMetadata_SK.php', + 'libphonenumber\\data\\PhoneNumberMetadata_SL' => $vendorDir . '/giggsey/libphonenumber-for-php-lite/src/data/PhoneNumberMetadata_SL.php', + 'libphonenumber\\data\\PhoneNumberMetadata_SM' => $vendorDir . '/giggsey/libphonenumber-for-php-lite/src/data/PhoneNumberMetadata_SM.php', + 'libphonenumber\\data\\PhoneNumberMetadata_SN' => $vendorDir . '/giggsey/libphonenumber-for-php-lite/src/data/PhoneNumberMetadata_SN.php', + 'libphonenumber\\data\\PhoneNumberMetadata_SO' => $vendorDir . '/giggsey/libphonenumber-for-php-lite/src/data/PhoneNumberMetadata_SO.php', + 'libphonenumber\\data\\PhoneNumberMetadata_SR' => $vendorDir . '/giggsey/libphonenumber-for-php-lite/src/data/PhoneNumberMetadata_SR.php', + 'libphonenumber\\data\\PhoneNumberMetadata_SS' => $vendorDir . '/giggsey/libphonenumber-for-php-lite/src/data/PhoneNumberMetadata_SS.php', + 'libphonenumber\\data\\PhoneNumberMetadata_ST' => $vendorDir . '/giggsey/libphonenumber-for-php-lite/src/data/PhoneNumberMetadata_ST.php', + 'libphonenumber\\data\\PhoneNumberMetadata_SV' => $vendorDir . '/giggsey/libphonenumber-for-php-lite/src/data/PhoneNumberMetadata_SV.php', + 'libphonenumber\\data\\PhoneNumberMetadata_SX' => $vendorDir . '/giggsey/libphonenumber-for-php-lite/src/data/PhoneNumberMetadata_SX.php', + 'libphonenumber\\data\\PhoneNumberMetadata_SY' => $vendorDir . '/giggsey/libphonenumber-for-php-lite/src/data/PhoneNumberMetadata_SY.php', + 'libphonenumber\\data\\PhoneNumberMetadata_SZ' => $vendorDir . '/giggsey/libphonenumber-for-php-lite/src/data/PhoneNumberMetadata_SZ.php', + 'libphonenumber\\data\\PhoneNumberMetadata_TA' => $vendorDir . '/giggsey/libphonenumber-for-php-lite/src/data/PhoneNumberMetadata_TA.php', + 'libphonenumber\\data\\PhoneNumberMetadata_TC' => $vendorDir . '/giggsey/libphonenumber-for-php-lite/src/data/PhoneNumberMetadata_TC.php', + 'libphonenumber\\data\\PhoneNumberMetadata_TD' => $vendorDir . '/giggsey/libphonenumber-for-php-lite/src/data/PhoneNumberMetadata_TD.php', + 'libphonenumber\\data\\PhoneNumberMetadata_TG' => $vendorDir . '/giggsey/libphonenumber-for-php-lite/src/data/PhoneNumberMetadata_TG.php', + 'libphonenumber\\data\\PhoneNumberMetadata_TH' => $vendorDir . '/giggsey/libphonenumber-for-php-lite/src/data/PhoneNumberMetadata_TH.php', + 'libphonenumber\\data\\PhoneNumberMetadata_TJ' => $vendorDir . '/giggsey/libphonenumber-for-php-lite/src/data/PhoneNumberMetadata_TJ.php', + 'libphonenumber\\data\\PhoneNumberMetadata_TK' => $vendorDir . '/giggsey/libphonenumber-for-php-lite/src/data/PhoneNumberMetadata_TK.php', + 'libphonenumber\\data\\PhoneNumberMetadata_TL' => $vendorDir . '/giggsey/libphonenumber-for-php-lite/src/data/PhoneNumberMetadata_TL.php', + 'libphonenumber\\data\\PhoneNumberMetadata_TM' => $vendorDir . '/giggsey/libphonenumber-for-php-lite/src/data/PhoneNumberMetadata_TM.php', + 'libphonenumber\\data\\PhoneNumberMetadata_TN' => $vendorDir . '/giggsey/libphonenumber-for-php-lite/src/data/PhoneNumberMetadata_TN.php', + 'libphonenumber\\data\\PhoneNumberMetadata_TO' => $vendorDir . '/giggsey/libphonenumber-for-php-lite/src/data/PhoneNumberMetadata_TO.php', + 'libphonenumber\\data\\PhoneNumberMetadata_TR' => $vendorDir . '/giggsey/libphonenumber-for-php-lite/src/data/PhoneNumberMetadata_TR.php', + 'libphonenumber\\data\\PhoneNumberMetadata_TT' => $vendorDir . '/giggsey/libphonenumber-for-php-lite/src/data/PhoneNumberMetadata_TT.php', + 'libphonenumber\\data\\PhoneNumberMetadata_TV' => $vendorDir . '/giggsey/libphonenumber-for-php-lite/src/data/PhoneNumberMetadata_TV.php', + 'libphonenumber\\data\\PhoneNumberMetadata_TW' => $vendorDir . '/giggsey/libphonenumber-for-php-lite/src/data/PhoneNumberMetadata_TW.php', + 'libphonenumber\\data\\PhoneNumberMetadata_TZ' => $vendorDir . '/giggsey/libphonenumber-for-php-lite/src/data/PhoneNumberMetadata_TZ.php', + 'libphonenumber\\data\\PhoneNumberMetadata_UA' => $vendorDir . '/giggsey/libphonenumber-for-php-lite/src/data/PhoneNumberMetadata_UA.php', + 'libphonenumber\\data\\PhoneNumberMetadata_UG' => $vendorDir . '/giggsey/libphonenumber-for-php-lite/src/data/PhoneNumberMetadata_UG.php', + 'libphonenumber\\data\\PhoneNumberMetadata_US' => $vendorDir . '/giggsey/libphonenumber-for-php-lite/src/data/PhoneNumberMetadata_US.php', + 'libphonenumber\\data\\PhoneNumberMetadata_UY' => $vendorDir . '/giggsey/libphonenumber-for-php-lite/src/data/PhoneNumberMetadata_UY.php', + 'libphonenumber\\data\\PhoneNumberMetadata_UZ' => $vendorDir . '/giggsey/libphonenumber-for-php-lite/src/data/PhoneNumberMetadata_UZ.php', + 'libphonenumber\\data\\PhoneNumberMetadata_VA' => $vendorDir . '/giggsey/libphonenumber-for-php-lite/src/data/PhoneNumberMetadata_VA.php', + 'libphonenumber\\data\\PhoneNumberMetadata_VC' => $vendorDir . '/giggsey/libphonenumber-for-php-lite/src/data/PhoneNumberMetadata_VC.php', + 'libphonenumber\\data\\PhoneNumberMetadata_VE' => $vendorDir . '/giggsey/libphonenumber-for-php-lite/src/data/PhoneNumberMetadata_VE.php', + 'libphonenumber\\data\\PhoneNumberMetadata_VG' => $vendorDir . '/giggsey/libphonenumber-for-php-lite/src/data/PhoneNumberMetadata_VG.php', + 'libphonenumber\\data\\PhoneNumberMetadata_VI' => $vendorDir . '/giggsey/libphonenumber-for-php-lite/src/data/PhoneNumberMetadata_VI.php', + 'libphonenumber\\data\\PhoneNumberMetadata_VN' => $vendorDir . '/giggsey/libphonenumber-for-php-lite/src/data/PhoneNumberMetadata_VN.php', + 'libphonenumber\\data\\PhoneNumberMetadata_VU' => $vendorDir . '/giggsey/libphonenumber-for-php-lite/src/data/PhoneNumberMetadata_VU.php', + 'libphonenumber\\data\\PhoneNumberMetadata_WF' => $vendorDir . '/giggsey/libphonenumber-for-php-lite/src/data/PhoneNumberMetadata_WF.php', + 'libphonenumber\\data\\PhoneNumberMetadata_WS' => $vendorDir . '/giggsey/libphonenumber-for-php-lite/src/data/PhoneNumberMetadata_WS.php', + 'libphonenumber\\data\\PhoneNumberMetadata_XK' => $vendorDir . '/giggsey/libphonenumber-for-php-lite/src/data/PhoneNumberMetadata_XK.php', + 'libphonenumber\\data\\PhoneNumberMetadata_YE' => $vendorDir . '/giggsey/libphonenumber-for-php-lite/src/data/PhoneNumberMetadata_YE.php', + 'libphonenumber\\data\\PhoneNumberMetadata_YT' => $vendorDir . '/giggsey/libphonenumber-for-php-lite/src/data/PhoneNumberMetadata_YT.php', + 'libphonenumber\\data\\PhoneNumberMetadata_ZA' => $vendorDir . '/giggsey/libphonenumber-for-php-lite/src/data/PhoneNumberMetadata_ZA.php', + 'libphonenumber\\data\\PhoneNumberMetadata_ZM' => $vendorDir . '/giggsey/libphonenumber-for-php-lite/src/data/PhoneNumberMetadata_ZM.php', + 'libphonenumber\\data\\PhoneNumberMetadata_ZW' => $vendorDir . '/giggsey/libphonenumber-for-php-lite/src/data/PhoneNumberMetadata_ZW.php', + 'libphonenumber\\data\\ShortNumberMetadata_AC' => $vendorDir . '/giggsey/libphonenumber-for-php-lite/src/data/ShortNumberMetadata_AC.php', + 'libphonenumber\\data\\ShortNumberMetadata_AD' => $vendorDir . '/giggsey/libphonenumber-for-php-lite/src/data/ShortNumberMetadata_AD.php', + 'libphonenumber\\data\\ShortNumberMetadata_AE' => $vendorDir . '/giggsey/libphonenumber-for-php-lite/src/data/ShortNumberMetadata_AE.php', + 'libphonenumber\\data\\ShortNumberMetadata_AF' => $vendorDir . '/giggsey/libphonenumber-for-php-lite/src/data/ShortNumberMetadata_AF.php', + 'libphonenumber\\data\\ShortNumberMetadata_AG' => $vendorDir . '/giggsey/libphonenumber-for-php-lite/src/data/ShortNumberMetadata_AG.php', + 'libphonenumber\\data\\ShortNumberMetadata_AI' => $vendorDir . '/giggsey/libphonenumber-for-php-lite/src/data/ShortNumberMetadata_AI.php', + 'libphonenumber\\data\\ShortNumberMetadata_AL' => $vendorDir . '/giggsey/libphonenumber-for-php-lite/src/data/ShortNumberMetadata_AL.php', + 'libphonenumber\\data\\ShortNumberMetadata_AM' => $vendorDir . '/giggsey/libphonenumber-for-php-lite/src/data/ShortNumberMetadata_AM.php', + 'libphonenumber\\data\\ShortNumberMetadata_AO' => $vendorDir . '/giggsey/libphonenumber-for-php-lite/src/data/ShortNumberMetadata_AO.php', + 'libphonenumber\\data\\ShortNumberMetadata_AR' => $vendorDir . '/giggsey/libphonenumber-for-php-lite/src/data/ShortNumberMetadata_AR.php', + 'libphonenumber\\data\\ShortNumberMetadata_AS' => $vendorDir . '/giggsey/libphonenumber-for-php-lite/src/data/ShortNumberMetadata_AS.php', + 'libphonenumber\\data\\ShortNumberMetadata_AT' => $vendorDir . '/giggsey/libphonenumber-for-php-lite/src/data/ShortNumberMetadata_AT.php', + 'libphonenumber\\data\\ShortNumberMetadata_AU' => $vendorDir . '/giggsey/libphonenumber-for-php-lite/src/data/ShortNumberMetadata_AU.php', + 'libphonenumber\\data\\ShortNumberMetadata_AW' => $vendorDir . '/giggsey/libphonenumber-for-php-lite/src/data/ShortNumberMetadata_AW.php', + 'libphonenumber\\data\\ShortNumberMetadata_AX' => $vendorDir . '/giggsey/libphonenumber-for-php-lite/src/data/ShortNumberMetadata_AX.php', + 'libphonenumber\\data\\ShortNumberMetadata_AZ' => $vendorDir . '/giggsey/libphonenumber-for-php-lite/src/data/ShortNumberMetadata_AZ.php', + 'libphonenumber\\data\\ShortNumberMetadata_BA' => $vendorDir . '/giggsey/libphonenumber-for-php-lite/src/data/ShortNumberMetadata_BA.php', + 'libphonenumber\\data\\ShortNumberMetadata_BB' => $vendorDir . '/giggsey/libphonenumber-for-php-lite/src/data/ShortNumberMetadata_BB.php', + 'libphonenumber\\data\\ShortNumberMetadata_BD' => $vendorDir . '/giggsey/libphonenumber-for-php-lite/src/data/ShortNumberMetadata_BD.php', + 'libphonenumber\\data\\ShortNumberMetadata_BE' => $vendorDir . '/giggsey/libphonenumber-for-php-lite/src/data/ShortNumberMetadata_BE.php', + 'libphonenumber\\data\\ShortNumberMetadata_BF' => $vendorDir . '/giggsey/libphonenumber-for-php-lite/src/data/ShortNumberMetadata_BF.php', + 'libphonenumber\\data\\ShortNumberMetadata_BG' => $vendorDir . '/giggsey/libphonenumber-for-php-lite/src/data/ShortNumberMetadata_BG.php', + 'libphonenumber\\data\\ShortNumberMetadata_BH' => $vendorDir . '/giggsey/libphonenumber-for-php-lite/src/data/ShortNumberMetadata_BH.php', + 'libphonenumber\\data\\ShortNumberMetadata_BI' => $vendorDir . '/giggsey/libphonenumber-for-php-lite/src/data/ShortNumberMetadata_BI.php', + 'libphonenumber\\data\\ShortNumberMetadata_BJ' => $vendorDir . '/giggsey/libphonenumber-for-php-lite/src/data/ShortNumberMetadata_BJ.php', + 'libphonenumber\\data\\ShortNumberMetadata_BL' => $vendorDir . '/giggsey/libphonenumber-for-php-lite/src/data/ShortNumberMetadata_BL.php', + 'libphonenumber\\data\\ShortNumberMetadata_BM' => $vendorDir . '/giggsey/libphonenumber-for-php-lite/src/data/ShortNumberMetadata_BM.php', + 'libphonenumber\\data\\ShortNumberMetadata_BN' => $vendorDir . '/giggsey/libphonenumber-for-php-lite/src/data/ShortNumberMetadata_BN.php', + 'libphonenumber\\data\\ShortNumberMetadata_BO' => $vendorDir . '/giggsey/libphonenumber-for-php-lite/src/data/ShortNumberMetadata_BO.php', + 'libphonenumber\\data\\ShortNumberMetadata_BQ' => $vendorDir . '/giggsey/libphonenumber-for-php-lite/src/data/ShortNumberMetadata_BQ.php', + 'libphonenumber\\data\\ShortNumberMetadata_BR' => $vendorDir . '/giggsey/libphonenumber-for-php-lite/src/data/ShortNumberMetadata_BR.php', + 'libphonenumber\\data\\ShortNumberMetadata_BS' => $vendorDir . '/giggsey/libphonenumber-for-php-lite/src/data/ShortNumberMetadata_BS.php', + 'libphonenumber\\data\\ShortNumberMetadata_BT' => $vendorDir . '/giggsey/libphonenumber-for-php-lite/src/data/ShortNumberMetadata_BT.php', + 'libphonenumber\\data\\ShortNumberMetadata_BW' => $vendorDir . '/giggsey/libphonenumber-for-php-lite/src/data/ShortNumberMetadata_BW.php', + 'libphonenumber\\data\\ShortNumberMetadata_BY' => $vendorDir . '/giggsey/libphonenumber-for-php-lite/src/data/ShortNumberMetadata_BY.php', + 'libphonenumber\\data\\ShortNumberMetadata_BZ' => $vendorDir . '/giggsey/libphonenumber-for-php-lite/src/data/ShortNumberMetadata_BZ.php', + 'libphonenumber\\data\\ShortNumberMetadata_CA' => $vendorDir . '/giggsey/libphonenumber-for-php-lite/src/data/ShortNumberMetadata_CA.php', + 'libphonenumber\\data\\ShortNumberMetadata_CC' => $vendorDir . '/giggsey/libphonenumber-for-php-lite/src/data/ShortNumberMetadata_CC.php', + 'libphonenumber\\data\\ShortNumberMetadata_CD' => $vendorDir . '/giggsey/libphonenumber-for-php-lite/src/data/ShortNumberMetadata_CD.php', + 'libphonenumber\\data\\ShortNumberMetadata_CF' => $vendorDir . '/giggsey/libphonenumber-for-php-lite/src/data/ShortNumberMetadata_CF.php', + 'libphonenumber\\data\\ShortNumberMetadata_CG' => $vendorDir . '/giggsey/libphonenumber-for-php-lite/src/data/ShortNumberMetadata_CG.php', + 'libphonenumber\\data\\ShortNumberMetadata_CH' => $vendorDir . '/giggsey/libphonenumber-for-php-lite/src/data/ShortNumberMetadata_CH.php', + 'libphonenumber\\data\\ShortNumberMetadata_CI' => $vendorDir . '/giggsey/libphonenumber-for-php-lite/src/data/ShortNumberMetadata_CI.php', + 'libphonenumber\\data\\ShortNumberMetadata_CK' => $vendorDir . '/giggsey/libphonenumber-for-php-lite/src/data/ShortNumberMetadata_CK.php', + 'libphonenumber\\data\\ShortNumberMetadata_CL' => $vendorDir . '/giggsey/libphonenumber-for-php-lite/src/data/ShortNumberMetadata_CL.php', + 'libphonenumber\\data\\ShortNumberMetadata_CM' => $vendorDir . '/giggsey/libphonenumber-for-php-lite/src/data/ShortNumberMetadata_CM.php', + 'libphonenumber\\data\\ShortNumberMetadata_CN' => $vendorDir . '/giggsey/libphonenumber-for-php-lite/src/data/ShortNumberMetadata_CN.php', + 'libphonenumber\\data\\ShortNumberMetadata_CO' => $vendorDir . '/giggsey/libphonenumber-for-php-lite/src/data/ShortNumberMetadata_CO.php', + 'libphonenumber\\data\\ShortNumberMetadata_CR' => $vendorDir . '/giggsey/libphonenumber-for-php-lite/src/data/ShortNumberMetadata_CR.php', + 'libphonenumber\\data\\ShortNumberMetadata_CU' => $vendorDir . '/giggsey/libphonenumber-for-php-lite/src/data/ShortNumberMetadata_CU.php', + 'libphonenumber\\data\\ShortNumberMetadata_CV' => $vendorDir . '/giggsey/libphonenumber-for-php-lite/src/data/ShortNumberMetadata_CV.php', + 'libphonenumber\\data\\ShortNumberMetadata_CW' => $vendorDir . '/giggsey/libphonenumber-for-php-lite/src/data/ShortNumberMetadata_CW.php', + 'libphonenumber\\data\\ShortNumberMetadata_CX' => $vendorDir . '/giggsey/libphonenumber-for-php-lite/src/data/ShortNumberMetadata_CX.php', + 'libphonenumber\\data\\ShortNumberMetadata_CY' => $vendorDir . '/giggsey/libphonenumber-for-php-lite/src/data/ShortNumberMetadata_CY.php', + 'libphonenumber\\data\\ShortNumberMetadata_CZ' => $vendorDir . '/giggsey/libphonenumber-for-php-lite/src/data/ShortNumberMetadata_CZ.php', + 'libphonenumber\\data\\ShortNumberMetadata_DE' => $vendorDir . '/giggsey/libphonenumber-for-php-lite/src/data/ShortNumberMetadata_DE.php', + 'libphonenumber\\data\\ShortNumberMetadata_DJ' => $vendorDir . '/giggsey/libphonenumber-for-php-lite/src/data/ShortNumberMetadata_DJ.php', + 'libphonenumber\\data\\ShortNumberMetadata_DK' => $vendorDir . '/giggsey/libphonenumber-for-php-lite/src/data/ShortNumberMetadata_DK.php', + 'libphonenumber\\data\\ShortNumberMetadata_DM' => $vendorDir . '/giggsey/libphonenumber-for-php-lite/src/data/ShortNumberMetadata_DM.php', + 'libphonenumber\\data\\ShortNumberMetadata_DO' => $vendorDir . '/giggsey/libphonenumber-for-php-lite/src/data/ShortNumberMetadata_DO.php', + 'libphonenumber\\data\\ShortNumberMetadata_DZ' => $vendorDir . '/giggsey/libphonenumber-for-php-lite/src/data/ShortNumberMetadata_DZ.php', + 'libphonenumber\\data\\ShortNumberMetadata_EC' => $vendorDir . '/giggsey/libphonenumber-for-php-lite/src/data/ShortNumberMetadata_EC.php', + 'libphonenumber\\data\\ShortNumberMetadata_EE' => $vendorDir . '/giggsey/libphonenumber-for-php-lite/src/data/ShortNumberMetadata_EE.php', + 'libphonenumber\\data\\ShortNumberMetadata_EG' => $vendorDir . '/giggsey/libphonenumber-for-php-lite/src/data/ShortNumberMetadata_EG.php', + 'libphonenumber\\data\\ShortNumberMetadata_EH' => $vendorDir . '/giggsey/libphonenumber-for-php-lite/src/data/ShortNumberMetadata_EH.php', + 'libphonenumber\\data\\ShortNumberMetadata_ER' => $vendorDir . '/giggsey/libphonenumber-for-php-lite/src/data/ShortNumberMetadata_ER.php', + 'libphonenumber\\data\\ShortNumberMetadata_ES' => $vendorDir . '/giggsey/libphonenumber-for-php-lite/src/data/ShortNumberMetadata_ES.php', + 'libphonenumber\\data\\ShortNumberMetadata_ET' => $vendorDir . '/giggsey/libphonenumber-for-php-lite/src/data/ShortNumberMetadata_ET.php', + 'libphonenumber\\data\\ShortNumberMetadata_FI' => $vendorDir . '/giggsey/libphonenumber-for-php-lite/src/data/ShortNumberMetadata_FI.php', + 'libphonenumber\\data\\ShortNumberMetadata_FJ' => $vendorDir . '/giggsey/libphonenumber-for-php-lite/src/data/ShortNumberMetadata_FJ.php', + 'libphonenumber\\data\\ShortNumberMetadata_FK' => $vendorDir . '/giggsey/libphonenumber-for-php-lite/src/data/ShortNumberMetadata_FK.php', + 'libphonenumber\\data\\ShortNumberMetadata_FM' => $vendorDir . '/giggsey/libphonenumber-for-php-lite/src/data/ShortNumberMetadata_FM.php', + 'libphonenumber\\data\\ShortNumberMetadata_FO' => $vendorDir . '/giggsey/libphonenumber-for-php-lite/src/data/ShortNumberMetadata_FO.php', + 'libphonenumber\\data\\ShortNumberMetadata_FR' => $vendorDir . '/giggsey/libphonenumber-for-php-lite/src/data/ShortNumberMetadata_FR.php', + 'libphonenumber\\data\\ShortNumberMetadata_GA' => $vendorDir . '/giggsey/libphonenumber-for-php-lite/src/data/ShortNumberMetadata_GA.php', + 'libphonenumber\\data\\ShortNumberMetadata_GB' => $vendorDir . '/giggsey/libphonenumber-for-php-lite/src/data/ShortNumberMetadata_GB.php', + 'libphonenumber\\data\\ShortNumberMetadata_GD' => $vendorDir . '/giggsey/libphonenumber-for-php-lite/src/data/ShortNumberMetadata_GD.php', + 'libphonenumber\\data\\ShortNumberMetadata_GE' => $vendorDir . '/giggsey/libphonenumber-for-php-lite/src/data/ShortNumberMetadata_GE.php', + 'libphonenumber\\data\\ShortNumberMetadata_GF' => $vendorDir . '/giggsey/libphonenumber-for-php-lite/src/data/ShortNumberMetadata_GF.php', + 'libphonenumber\\data\\ShortNumberMetadata_GG' => $vendorDir . '/giggsey/libphonenumber-for-php-lite/src/data/ShortNumberMetadata_GG.php', + 'libphonenumber\\data\\ShortNumberMetadata_GH' => $vendorDir . '/giggsey/libphonenumber-for-php-lite/src/data/ShortNumberMetadata_GH.php', + 'libphonenumber\\data\\ShortNumberMetadata_GI' => $vendorDir . '/giggsey/libphonenumber-for-php-lite/src/data/ShortNumberMetadata_GI.php', + 'libphonenumber\\data\\ShortNumberMetadata_GL' => $vendorDir . '/giggsey/libphonenumber-for-php-lite/src/data/ShortNumberMetadata_GL.php', + 'libphonenumber\\data\\ShortNumberMetadata_GM' => $vendorDir . '/giggsey/libphonenumber-for-php-lite/src/data/ShortNumberMetadata_GM.php', + 'libphonenumber\\data\\ShortNumberMetadata_GN' => $vendorDir . '/giggsey/libphonenumber-for-php-lite/src/data/ShortNumberMetadata_GN.php', + 'libphonenumber\\data\\ShortNumberMetadata_GP' => $vendorDir . '/giggsey/libphonenumber-for-php-lite/src/data/ShortNumberMetadata_GP.php', + 'libphonenumber\\data\\ShortNumberMetadata_GR' => $vendorDir . '/giggsey/libphonenumber-for-php-lite/src/data/ShortNumberMetadata_GR.php', + 'libphonenumber\\data\\ShortNumberMetadata_GT' => $vendorDir . '/giggsey/libphonenumber-for-php-lite/src/data/ShortNumberMetadata_GT.php', + 'libphonenumber\\data\\ShortNumberMetadata_GU' => $vendorDir . '/giggsey/libphonenumber-for-php-lite/src/data/ShortNumberMetadata_GU.php', + 'libphonenumber\\data\\ShortNumberMetadata_GW' => $vendorDir . '/giggsey/libphonenumber-for-php-lite/src/data/ShortNumberMetadata_GW.php', + 'libphonenumber\\data\\ShortNumberMetadata_GY' => $vendorDir . '/giggsey/libphonenumber-for-php-lite/src/data/ShortNumberMetadata_GY.php', + 'libphonenumber\\data\\ShortNumberMetadata_HK' => $vendorDir . '/giggsey/libphonenumber-for-php-lite/src/data/ShortNumberMetadata_HK.php', + 'libphonenumber\\data\\ShortNumberMetadata_HN' => $vendorDir . '/giggsey/libphonenumber-for-php-lite/src/data/ShortNumberMetadata_HN.php', + 'libphonenumber\\data\\ShortNumberMetadata_HR' => $vendorDir . '/giggsey/libphonenumber-for-php-lite/src/data/ShortNumberMetadata_HR.php', + 'libphonenumber\\data\\ShortNumberMetadata_HT' => $vendorDir . '/giggsey/libphonenumber-for-php-lite/src/data/ShortNumberMetadata_HT.php', + 'libphonenumber\\data\\ShortNumberMetadata_HU' => $vendorDir . '/giggsey/libphonenumber-for-php-lite/src/data/ShortNumberMetadata_HU.php', + 'libphonenumber\\data\\ShortNumberMetadata_ID' => $vendorDir . '/giggsey/libphonenumber-for-php-lite/src/data/ShortNumberMetadata_ID.php', + 'libphonenumber\\data\\ShortNumberMetadata_IE' => $vendorDir . '/giggsey/libphonenumber-for-php-lite/src/data/ShortNumberMetadata_IE.php', + 'libphonenumber\\data\\ShortNumberMetadata_IL' => $vendorDir . '/giggsey/libphonenumber-for-php-lite/src/data/ShortNumberMetadata_IL.php', + 'libphonenumber\\data\\ShortNumberMetadata_IM' => $vendorDir . '/giggsey/libphonenumber-for-php-lite/src/data/ShortNumberMetadata_IM.php', + 'libphonenumber\\data\\ShortNumberMetadata_IN' => $vendorDir . '/giggsey/libphonenumber-for-php-lite/src/data/ShortNumberMetadata_IN.php', + 'libphonenumber\\data\\ShortNumberMetadata_IQ' => $vendorDir . '/giggsey/libphonenumber-for-php-lite/src/data/ShortNumberMetadata_IQ.php', + 'libphonenumber\\data\\ShortNumberMetadata_IR' => $vendorDir . '/giggsey/libphonenumber-for-php-lite/src/data/ShortNumberMetadata_IR.php', + 'libphonenumber\\data\\ShortNumberMetadata_IS' => $vendorDir . '/giggsey/libphonenumber-for-php-lite/src/data/ShortNumberMetadata_IS.php', + 'libphonenumber\\data\\ShortNumberMetadata_IT' => $vendorDir . '/giggsey/libphonenumber-for-php-lite/src/data/ShortNumberMetadata_IT.php', + 'libphonenumber\\data\\ShortNumberMetadata_JE' => $vendorDir . '/giggsey/libphonenumber-for-php-lite/src/data/ShortNumberMetadata_JE.php', + 'libphonenumber\\data\\ShortNumberMetadata_JM' => $vendorDir . '/giggsey/libphonenumber-for-php-lite/src/data/ShortNumberMetadata_JM.php', + 'libphonenumber\\data\\ShortNumberMetadata_JO' => $vendorDir . '/giggsey/libphonenumber-for-php-lite/src/data/ShortNumberMetadata_JO.php', + 'libphonenumber\\data\\ShortNumberMetadata_JP' => $vendorDir . '/giggsey/libphonenumber-for-php-lite/src/data/ShortNumberMetadata_JP.php', + 'libphonenumber\\data\\ShortNumberMetadata_KE' => $vendorDir . '/giggsey/libphonenumber-for-php-lite/src/data/ShortNumberMetadata_KE.php', + 'libphonenumber\\data\\ShortNumberMetadata_KG' => $vendorDir . '/giggsey/libphonenumber-for-php-lite/src/data/ShortNumberMetadata_KG.php', + 'libphonenumber\\data\\ShortNumberMetadata_KH' => $vendorDir . '/giggsey/libphonenumber-for-php-lite/src/data/ShortNumberMetadata_KH.php', + 'libphonenumber\\data\\ShortNumberMetadata_KI' => $vendorDir . '/giggsey/libphonenumber-for-php-lite/src/data/ShortNumberMetadata_KI.php', + 'libphonenumber\\data\\ShortNumberMetadata_KM' => $vendorDir . '/giggsey/libphonenumber-for-php-lite/src/data/ShortNumberMetadata_KM.php', + 'libphonenumber\\data\\ShortNumberMetadata_KN' => $vendorDir . '/giggsey/libphonenumber-for-php-lite/src/data/ShortNumberMetadata_KN.php', + 'libphonenumber\\data\\ShortNumberMetadata_KP' => $vendorDir . '/giggsey/libphonenumber-for-php-lite/src/data/ShortNumberMetadata_KP.php', + 'libphonenumber\\data\\ShortNumberMetadata_KR' => $vendorDir . '/giggsey/libphonenumber-for-php-lite/src/data/ShortNumberMetadata_KR.php', + 'libphonenumber\\data\\ShortNumberMetadata_KW' => $vendorDir . '/giggsey/libphonenumber-for-php-lite/src/data/ShortNumberMetadata_KW.php', + 'libphonenumber\\data\\ShortNumberMetadata_KY' => $vendorDir . '/giggsey/libphonenumber-for-php-lite/src/data/ShortNumberMetadata_KY.php', + 'libphonenumber\\data\\ShortNumberMetadata_KZ' => $vendorDir . '/giggsey/libphonenumber-for-php-lite/src/data/ShortNumberMetadata_KZ.php', + 'libphonenumber\\data\\ShortNumberMetadata_LA' => $vendorDir . '/giggsey/libphonenumber-for-php-lite/src/data/ShortNumberMetadata_LA.php', + 'libphonenumber\\data\\ShortNumberMetadata_LB' => $vendorDir . '/giggsey/libphonenumber-for-php-lite/src/data/ShortNumberMetadata_LB.php', + 'libphonenumber\\data\\ShortNumberMetadata_LC' => $vendorDir . '/giggsey/libphonenumber-for-php-lite/src/data/ShortNumberMetadata_LC.php', + 'libphonenumber\\data\\ShortNumberMetadata_LI' => $vendorDir . '/giggsey/libphonenumber-for-php-lite/src/data/ShortNumberMetadata_LI.php', + 'libphonenumber\\data\\ShortNumberMetadata_LK' => $vendorDir . '/giggsey/libphonenumber-for-php-lite/src/data/ShortNumberMetadata_LK.php', + 'libphonenumber\\data\\ShortNumberMetadata_LR' => $vendorDir . '/giggsey/libphonenumber-for-php-lite/src/data/ShortNumberMetadata_LR.php', + 'libphonenumber\\data\\ShortNumberMetadata_LS' => $vendorDir . '/giggsey/libphonenumber-for-php-lite/src/data/ShortNumberMetadata_LS.php', + 'libphonenumber\\data\\ShortNumberMetadata_LT' => $vendorDir . '/giggsey/libphonenumber-for-php-lite/src/data/ShortNumberMetadata_LT.php', + 'libphonenumber\\data\\ShortNumberMetadata_LU' => $vendorDir . '/giggsey/libphonenumber-for-php-lite/src/data/ShortNumberMetadata_LU.php', + 'libphonenumber\\data\\ShortNumberMetadata_LV' => $vendorDir . '/giggsey/libphonenumber-for-php-lite/src/data/ShortNumberMetadata_LV.php', + 'libphonenumber\\data\\ShortNumberMetadata_LY' => $vendorDir . '/giggsey/libphonenumber-for-php-lite/src/data/ShortNumberMetadata_LY.php', + 'libphonenumber\\data\\ShortNumberMetadata_MA' => $vendorDir . '/giggsey/libphonenumber-for-php-lite/src/data/ShortNumberMetadata_MA.php', + 'libphonenumber\\data\\ShortNumberMetadata_MC' => $vendorDir . '/giggsey/libphonenumber-for-php-lite/src/data/ShortNumberMetadata_MC.php', + 'libphonenumber\\data\\ShortNumberMetadata_MD' => $vendorDir . '/giggsey/libphonenumber-for-php-lite/src/data/ShortNumberMetadata_MD.php', + 'libphonenumber\\data\\ShortNumberMetadata_ME' => $vendorDir . '/giggsey/libphonenumber-for-php-lite/src/data/ShortNumberMetadata_ME.php', + 'libphonenumber\\data\\ShortNumberMetadata_MF' => $vendorDir . '/giggsey/libphonenumber-for-php-lite/src/data/ShortNumberMetadata_MF.php', + 'libphonenumber\\data\\ShortNumberMetadata_MG' => $vendorDir . '/giggsey/libphonenumber-for-php-lite/src/data/ShortNumberMetadata_MG.php', + 'libphonenumber\\data\\ShortNumberMetadata_MH' => $vendorDir . '/giggsey/libphonenumber-for-php-lite/src/data/ShortNumberMetadata_MH.php', + 'libphonenumber\\data\\ShortNumberMetadata_MK' => $vendorDir . '/giggsey/libphonenumber-for-php-lite/src/data/ShortNumberMetadata_MK.php', + 'libphonenumber\\data\\ShortNumberMetadata_ML' => $vendorDir . '/giggsey/libphonenumber-for-php-lite/src/data/ShortNumberMetadata_ML.php', + 'libphonenumber\\data\\ShortNumberMetadata_MM' => $vendorDir . '/giggsey/libphonenumber-for-php-lite/src/data/ShortNumberMetadata_MM.php', + 'libphonenumber\\data\\ShortNumberMetadata_MN' => $vendorDir . '/giggsey/libphonenumber-for-php-lite/src/data/ShortNumberMetadata_MN.php', + 'libphonenumber\\data\\ShortNumberMetadata_MO' => $vendorDir . '/giggsey/libphonenumber-for-php-lite/src/data/ShortNumberMetadata_MO.php', + 'libphonenumber\\data\\ShortNumberMetadata_MP' => $vendorDir . '/giggsey/libphonenumber-for-php-lite/src/data/ShortNumberMetadata_MP.php', + 'libphonenumber\\data\\ShortNumberMetadata_MQ' => $vendorDir . '/giggsey/libphonenumber-for-php-lite/src/data/ShortNumberMetadata_MQ.php', + 'libphonenumber\\data\\ShortNumberMetadata_MR' => $vendorDir . '/giggsey/libphonenumber-for-php-lite/src/data/ShortNumberMetadata_MR.php', + 'libphonenumber\\data\\ShortNumberMetadata_MS' => $vendorDir . '/giggsey/libphonenumber-for-php-lite/src/data/ShortNumberMetadata_MS.php', + 'libphonenumber\\data\\ShortNumberMetadata_MT' => $vendorDir . '/giggsey/libphonenumber-for-php-lite/src/data/ShortNumberMetadata_MT.php', + 'libphonenumber\\data\\ShortNumberMetadata_MU' => $vendorDir . '/giggsey/libphonenumber-for-php-lite/src/data/ShortNumberMetadata_MU.php', + 'libphonenumber\\data\\ShortNumberMetadata_MV' => $vendorDir . '/giggsey/libphonenumber-for-php-lite/src/data/ShortNumberMetadata_MV.php', + 'libphonenumber\\data\\ShortNumberMetadata_MW' => $vendorDir . '/giggsey/libphonenumber-for-php-lite/src/data/ShortNumberMetadata_MW.php', + 'libphonenumber\\data\\ShortNumberMetadata_MX' => $vendorDir . '/giggsey/libphonenumber-for-php-lite/src/data/ShortNumberMetadata_MX.php', + 'libphonenumber\\data\\ShortNumberMetadata_MY' => $vendorDir . '/giggsey/libphonenumber-for-php-lite/src/data/ShortNumberMetadata_MY.php', + 'libphonenumber\\data\\ShortNumberMetadata_MZ' => $vendorDir . '/giggsey/libphonenumber-for-php-lite/src/data/ShortNumberMetadata_MZ.php', + 'libphonenumber\\data\\ShortNumberMetadata_NA' => $vendorDir . '/giggsey/libphonenumber-for-php-lite/src/data/ShortNumberMetadata_NA.php', + 'libphonenumber\\data\\ShortNumberMetadata_NC' => $vendorDir . '/giggsey/libphonenumber-for-php-lite/src/data/ShortNumberMetadata_NC.php', + 'libphonenumber\\data\\ShortNumberMetadata_NE' => $vendorDir . '/giggsey/libphonenumber-for-php-lite/src/data/ShortNumberMetadata_NE.php', + 'libphonenumber\\data\\ShortNumberMetadata_NF' => $vendorDir . '/giggsey/libphonenumber-for-php-lite/src/data/ShortNumberMetadata_NF.php', + 'libphonenumber\\data\\ShortNumberMetadata_NG' => $vendorDir . '/giggsey/libphonenumber-for-php-lite/src/data/ShortNumberMetadata_NG.php', + 'libphonenumber\\data\\ShortNumberMetadata_NI' => $vendorDir . '/giggsey/libphonenumber-for-php-lite/src/data/ShortNumberMetadata_NI.php', + 'libphonenumber\\data\\ShortNumberMetadata_NL' => $vendorDir . '/giggsey/libphonenumber-for-php-lite/src/data/ShortNumberMetadata_NL.php', + 'libphonenumber\\data\\ShortNumberMetadata_NO' => $vendorDir . '/giggsey/libphonenumber-for-php-lite/src/data/ShortNumberMetadata_NO.php', + 'libphonenumber\\data\\ShortNumberMetadata_NP' => $vendorDir . '/giggsey/libphonenumber-for-php-lite/src/data/ShortNumberMetadata_NP.php', + 'libphonenumber\\data\\ShortNumberMetadata_NR' => $vendorDir . '/giggsey/libphonenumber-for-php-lite/src/data/ShortNumberMetadata_NR.php', + 'libphonenumber\\data\\ShortNumberMetadata_NU' => $vendorDir . '/giggsey/libphonenumber-for-php-lite/src/data/ShortNumberMetadata_NU.php', + 'libphonenumber\\data\\ShortNumberMetadata_NZ' => $vendorDir . '/giggsey/libphonenumber-for-php-lite/src/data/ShortNumberMetadata_NZ.php', + 'libphonenumber\\data\\ShortNumberMetadata_OM' => $vendorDir . '/giggsey/libphonenumber-for-php-lite/src/data/ShortNumberMetadata_OM.php', + 'libphonenumber\\data\\ShortNumberMetadata_PA' => $vendorDir . '/giggsey/libphonenumber-for-php-lite/src/data/ShortNumberMetadata_PA.php', + 'libphonenumber\\data\\ShortNumberMetadata_PE' => $vendorDir . '/giggsey/libphonenumber-for-php-lite/src/data/ShortNumberMetadata_PE.php', + 'libphonenumber\\data\\ShortNumberMetadata_PF' => $vendorDir . '/giggsey/libphonenumber-for-php-lite/src/data/ShortNumberMetadata_PF.php', + 'libphonenumber\\data\\ShortNumberMetadata_PG' => $vendorDir . '/giggsey/libphonenumber-for-php-lite/src/data/ShortNumberMetadata_PG.php', + 'libphonenumber\\data\\ShortNumberMetadata_PH' => $vendorDir . '/giggsey/libphonenumber-for-php-lite/src/data/ShortNumberMetadata_PH.php', + 'libphonenumber\\data\\ShortNumberMetadata_PK' => $vendorDir . '/giggsey/libphonenumber-for-php-lite/src/data/ShortNumberMetadata_PK.php', + 'libphonenumber\\data\\ShortNumberMetadata_PL' => $vendorDir . '/giggsey/libphonenumber-for-php-lite/src/data/ShortNumberMetadata_PL.php', + 'libphonenumber\\data\\ShortNumberMetadata_PM' => $vendorDir . '/giggsey/libphonenumber-for-php-lite/src/data/ShortNumberMetadata_PM.php', + 'libphonenumber\\data\\ShortNumberMetadata_PR' => $vendorDir . '/giggsey/libphonenumber-for-php-lite/src/data/ShortNumberMetadata_PR.php', + 'libphonenumber\\data\\ShortNumberMetadata_PS' => $vendorDir . '/giggsey/libphonenumber-for-php-lite/src/data/ShortNumberMetadata_PS.php', + 'libphonenumber\\data\\ShortNumberMetadata_PT' => $vendorDir . '/giggsey/libphonenumber-for-php-lite/src/data/ShortNumberMetadata_PT.php', + 'libphonenumber\\data\\ShortNumberMetadata_PW' => $vendorDir . '/giggsey/libphonenumber-for-php-lite/src/data/ShortNumberMetadata_PW.php', + 'libphonenumber\\data\\ShortNumberMetadata_PY' => $vendorDir . '/giggsey/libphonenumber-for-php-lite/src/data/ShortNumberMetadata_PY.php', + 'libphonenumber\\data\\ShortNumberMetadata_QA' => $vendorDir . '/giggsey/libphonenumber-for-php-lite/src/data/ShortNumberMetadata_QA.php', + 'libphonenumber\\data\\ShortNumberMetadata_RE' => $vendorDir . '/giggsey/libphonenumber-for-php-lite/src/data/ShortNumberMetadata_RE.php', + 'libphonenumber\\data\\ShortNumberMetadata_RO' => $vendorDir . '/giggsey/libphonenumber-for-php-lite/src/data/ShortNumberMetadata_RO.php', + 'libphonenumber\\data\\ShortNumberMetadata_RS' => $vendorDir . '/giggsey/libphonenumber-for-php-lite/src/data/ShortNumberMetadata_RS.php', + 'libphonenumber\\data\\ShortNumberMetadata_RU' => $vendorDir . '/giggsey/libphonenumber-for-php-lite/src/data/ShortNumberMetadata_RU.php', + 'libphonenumber\\data\\ShortNumberMetadata_RW' => $vendorDir . '/giggsey/libphonenumber-for-php-lite/src/data/ShortNumberMetadata_RW.php', + 'libphonenumber\\data\\ShortNumberMetadata_SA' => $vendorDir . '/giggsey/libphonenumber-for-php-lite/src/data/ShortNumberMetadata_SA.php', + 'libphonenumber\\data\\ShortNumberMetadata_SB' => $vendorDir . '/giggsey/libphonenumber-for-php-lite/src/data/ShortNumberMetadata_SB.php', + 'libphonenumber\\data\\ShortNumberMetadata_SC' => $vendorDir . '/giggsey/libphonenumber-for-php-lite/src/data/ShortNumberMetadata_SC.php', + 'libphonenumber\\data\\ShortNumberMetadata_SD' => $vendorDir . '/giggsey/libphonenumber-for-php-lite/src/data/ShortNumberMetadata_SD.php', + 'libphonenumber\\data\\ShortNumberMetadata_SE' => $vendorDir . '/giggsey/libphonenumber-for-php-lite/src/data/ShortNumberMetadata_SE.php', + 'libphonenumber\\data\\ShortNumberMetadata_SG' => $vendorDir . '/giggsey/libphonenumber-for-php-lite/src/data/ShortNumberMetadata_SG.php', + 'libphonenumber\\data\\ShortNumberMetadata_SH' => $vendorDir . '/giggsey/libphonenumber-for-php-lite/src/data/ShortNumberMetadata_SH.php', + 'libphonenumber\\data\\ShortNumberMetadata_SI' => $vendorDir . '/giggsey/libphonenumber-for-php-lite/src/data/ShortNumberMetadata_SI.php', + 'libphonenumber\\data\\ShortNumberMetadata_SJ' => $vendorDir . '/giggsey/libphonenumber-for-php-lite/src/data/ShortNumberMetadata_SJ.php', + 'libphonenumber\\data\\ShortNumberMetadata_SK' => $vendorDir . '/giggsey/libphonenumber-for-php-lite/src/data/ShortNumberMetadata_SK.php', + 'libphonenumber\\data\\ShortNumberMetadata_SL' => $vendorDir . '/giggsey/libphonenumber-for-php-lite/src/data/ShortNumberMetadata_SL.php', + 'libphonenumber\\data\\ShortNumberMetadata_SM' => $vendorDir . '/giggsey/libphonenumber-for-php-lite/src/data/ShortNumberMetadata_SM.php', + 'libphonenumber\\data\\ShortNumberMetadata_SN' => $vendorDir . '/giggsey/libphonenumber-for-php-lite/src/data/ShortNumberMetadata_SN.php', + 'libphonenumber\\data\\ShortNumberMetadata_SO' => $vendorDir . '/giggsey/libphonenumber-for-php-lite/src/data/ShortNumberMetadata_SO.php', + 'libphonenumber\\data\\ShortNumberMetadata_SR' => $vendorDir . '/giggsey/libphonenumber-for-php-lite/src/data/ShortNumberMetadata_SR.php', + 'libphonenumber\\data\\ShortNumberMetadata_SS' => $vendorDir . '/giggsey/libphonenumber-for-php-lite/src/data/ShortNumberMetadata_SS.php', + 'libphonenumber\\data\\ShortNumberMetadata_ST' => $vendorDir . '/giggsey/libphonenumber-for-php-lite/src/data/ShortNumberMetadata_ST.php', + 'libphonenumber\\data\\ShortNumberMetadata_SV' => $vendorDir . '/giggsey/libphonenumber-for-php-lite/src/data/ShortNumberMetadata_SV.php', + 'libphonenumber\\data\\ShortNumberMetadata_SX' => $vendorDir . '/giggsey/libphonenumber-for-php-lite/src/data/ShortNumberMetadata_SX.php', + 'libphonenumber\\data\\ShortNumberMetadata_SY' => $vendorDir . '/giggsey/libphonenumber-for-php-lite/src/data/ShortNumberMetadata_SY.php', + 'libphonenumber\\data\\ShortNumberMetadata_SZ' => $vendorDir . '/giggsey/libphonenumber-for-php-lite/src/data/ShortNumberMetadata_SZ.php', + 'libphonenumber\\data\\ShortNumberMetadata_TC' => $vendorDir . '/giggsey/libphonenumber-for-php-lite/src/data/ShortNumberMetadata_TC.php', + 'libphonenumber\\data\\ShortNumberMetadata_TD' => $vendorDir . '/giggsey/libphonenumber-for-php-lite/src/data/ShortNumberMetadata_TD.php', + 'libphonenumber\\data\\ShortNumberMetadata_TG' => $vendorDir . '/giggsey/libphonenumber-for-php-lite/src/data/ShortNumberMetadata_TG.php', + 'libphonenumber\\data\\ShortNumberMetadata_TH' => $vendorDir . '/giggsey/libphonenumber-for-php-lite/src/data/ShortNumberMetadata_TH.php', + 'libphonenumber\\data\\ShortNumberMetadata_TJ' => $vendorDir . '/giggsey/libphonenumber-for-php-lite/src/data/ShortNumberMetadata_TJ.php', + 'libphonenumber\\data\\ShortNumberMetadata_TL' => $vendorDir . '/giggsey/libphonenumber-for-php-lite/src/data/ShortNumberMetadata_TL.php', + 'libphonenumber\\data\\ShortNumberMetadata_TM' => $vendorDir . '/giggsey/libphonenumber-for-php-lite/src/data/ShortNumberMetadata_TM.php', + 'libphonenumber\\data\\ShortNumberMetadata_TN' => $vendorDir . '/giggsey/libphonenumber-for-php-lite/src/data/ShortNumberMetadata_TN.php', + 'libphonenumber\\data\\ShortNumberMetadata_TO' => $vendorDir . '/giggsey/libphonenumber-for-php-lite/src/data/ShortNumberMetadata_TO.php', + 'libphonenumber\\data\\ShortNumberMetadata_TR' => $vendorDir . '/giggsey/libphonenumber-for-php-lite/src/data/ShortNumberMetadata_TR.php', + 'libphonenumber\\data\\ShortNumberMetadata_TT' => $vendorDir . '/giggsey/libphonenumber-for-php-lite/src/data/ShortNumberMetadata_TT.php', + 'libphonenumber\\data\\ShortNumberMetadata_TV' => $vendorDir . '/giggsey/libphonenumber-for-php-lite/src/data/ShortNumberMetadata_TV.php', + 'libphonenumber\\data\\ShortNumberMetadata_TW' => $vendorDir . '/giggsey/libphonenumber-for-php-lite/src/data/ShortNumberMetadata_TW.php', + 'libphonenumber\\data\\ShortNumberMetadata_TZ' => $vendorDir . '/giggsey/libphonenumber-for-php-lite/src/data/ShortNumberMetadata_TZ.php', + 'libphonenumber\\data\\ShortNumberMetadata_UA' => $vendorDir . '/giggsey/libphonenumber-for-php-lite/src/data/ShortNumberMetadata_UA.php', + 'libphonenumber\\data\\ShortNumberMetadata_UG' => $vendorDir . '/giggsey/libphonenumber-for-php-lite/src/data/ShortNumberMetadata_UG.php', + 'libphonenumber\\data\\ShortNumberMetadata_US' => $vendorDir . '/giggsey/libphonenumber-for-php-lite/src/data/ShortNumberMetadata_US.php', + 'libphonenumber\\data\\ShortNumberMetadata_UY' => $vendorDir . '/giggsey/libphonenumber-for-php-lite/src/data/ShortNumberMetadata_UY.php', + 'libphonenumber\\data\\ShortNumberMetadata_UZ' => $vendorDir . '/giggsey/libphonenumber-for-php-lite/src/data/ShortNumberMetadata_UZ.php', + 'libphonenumber\\data\\ShortNumberMetadata_VA' => $vendorDir . '/giggsey/libphonenumber-for-php-lite/src/data/ShortNumberMetadata_VA.php', + 'libphonenumber\\data\\ShortNumberMetadata_VC' => $vendorDir . '/giggsey/libphonenumber-for-php-lite/src/data/ShortNumberMetadata_VC.php', + 'libphonenumber\\data\\ShortNumberMetadata_VE' => $vendorDir . '/giggsey/libphonenumber-for-php-lite/src/data/ShortNumberMetadata_VE.php', + 'libphonenumber\\data\\ShortNumberMetadata_VG' => $vendorDir . '/giggsey/libphonenumber-for-php-lite/src/data/ShortNumberMetadata_VG.php', + 'libphonenumber\\data\\ShortNumberMetadata_VI' => $vendorDir . '/giggsey/libphonenumber-for-php-lite/src/data/ShortNumberMetadata_VI.php', + 'libphonenumber\\data\\ShortNumberMetadata_VN' => $vendorDir . '/giggsey/libphonenumber-for-php-lite/src/data/ShortNumberMetadata_VN.php', + 'libphonenumber\\data\\ShortNumberMetadata_VU' => $vendorDir . '/giggsey/libphonenumber-for-php-lite/src/data/ShortNumberMetadata_VU.php', + 'libphonenumber\\data\\ShortNumberMetadata_WF' => $vendorDir . '/giggsey/libphonenumber-for-php-lite/src/data/ShortNumberMetadata_WF.php', + 'libphonenumber\\data\\ShortNumberMetadata_WS' => $vendorDir . '/giggsey/libphonenumber-for-php-lite/src/data/ShortNumberMetadata_WS.php', + 'libphonenumber\\data\\ShortNumberMetadata_XK' => $vendorDir . '/giggsey/libphonenumber-for-php-lite/src/data/ShortNumberMetadata_XK.php', + 'libphonenumber\\data\\ShortNumberMetadata_YE' => $vendorDir . '/giggsey/libphonenumber-for-php-lite/src/data/ShortNumberMetadata_YE.php', + 'libphonenumber\\data\\ShortNumberMetadata_YT' => $vendorDir . '/giggsey/libphonenumber-for-php-lite/src/data/ShortNumberMetadata_YT.php', + 'libphonenumber\\data\\ShortNumberMetadata_ZA' => $vendorDir . '/giggsey/libphonenumber-for-php-lite/src/data/ShortNumberMetadata_ZA.php', + 'libphonenumber\\data\\ShortNumberMetadata_ZM' => $vendorDir . '/giggsey/libphonenumber-for-php-lite/src/data/ShortNumberMetadata_ZM.php', + 'libphonenumber\\data\\ShortNumberMetadata_ZW' => $vendorDir . '/giggsey/libphonenumber-for-php-lite/src/data/ShortNumberMetadata_ZW.php', + 'ownCloud\\TarStreamer\\TarHeader' => $vendorDir . '/deepdiver1975/tarstreamer/src/TarHeader.php', + 'ownCloud\\TarStreamer\\TarStreamer' => $vendorDir . '/deepdiver1975/tarstreamer/src/TarStreamer.php', + 'phpseclib\\Crypt\\AES' => $vendorDir . '/phpseclib/phpseclib/phpseclib/Crypt/AES.php', + 'phpseclib\\Crypt\\Base' => $vendorDir . '/phpseclib/phpseclib/phpseclib/Crypt/Base.php', + 'phpseclib\\Crypt\\Blowfish' => $vendorDir . '/phpseclib/phpseclib/phpseclib/Crypt/Blowfish.php', + 'phpseclib\\Crypt\\DES' => $vendorDir . '/phpseclib/phpseclib/phpseclib/Crypt/DES.php', + 'phpseclib\\Crypt\\Hash' => $vendorDir . '/phpseclib/phpseclib/phpseclib/Crypt/Hash.php', + 'phpseclib\\Crypt\\RC2' => $vendorDir . '/phpseclib/phpseclib/phpseclib/Crypt/RC2.php', + 'phpseclib\\Crypt\\RC4' => $vendorDir . '/phpseclib/phpseclib/phpseclib/Crypt/RC4.php', + 'phpseclib\\Crypt\\RSA' => $vendorDir . '/phpseclib/phpseclib/phpseclib/Crypt/RSA.php', + 'phpseclib\\Crypt\\Random' => $vendorDir . '/phpseclib/phpseclib/phpseclib/Crypt/Random.php', + 'phpseclib\\Crypt\\Rijndael' => $vendorDir . '/phpseclib/phpseclib/phpseclib/Crypt/Rijndael.php', + 'phpseclib\\Crypt\\TripleDES' => $vendorDir . '/phpseclib/phpseclib/phpseclib/Crypt/TripleDES.php', + 'phpseclib\\Crypt\\Twofish' => $vendorDir . '/phpseclib/phpseclib/phpseclib/Crypt/Twofish.php', + 'phpseclib\\File\\ANSI' => $vendorDir . '/phpseclib/phpseclib/phpseclib/File/ANSI.php', + 'phpseclib\\File\\ASN1' => $vendorDir . '/phpseclib/phpseclib/phpseclib/File/ASN1.php', + 'phpseclib\\File\\ASN1\\Element' => $vendorDir . '/phpseclib/phpseclib/phpseclib/File/ASN1/Element.php', + 'phpseclib\\File\\X509' => $vendorDir . '/phpseclib/phpseclib/phpseclib/File/X509.php', + 'phpseclib\\Math\\BigInteger' => $vendorDir . '/phpseclib/phpseclib/phpseclib/Math/BigInteger.php', + 'phpseclib\\Net\\SCP' => $vendorDir . '/phpseclib/phpseclib/phpseclib/Net/SCP.php', + 'phpseclib\\Net\\SFTP' => $vendorDir . '/phpseclib/phpseclib/phpseclib/Net/SFTP.php', + 'phpseclib\\Net\\SFTP\\Stream' => $vendorDir . '/phpseclib/phpseclib/phpseclib/Net/SFTP/Stream.php', + 'phpseclib\\Net\\SSH1' => $vendorDir . '/phpseclib/phpseclib/phpseclib/Net/SSH1.php', + 'phpseclib\\Net\\SSH2' => $vendorDir . '/phpseclib/phpseclib/phpseclib/Net/SSH2.php', + 'phpseclib\\System\\SSH\\Agent' => $vendorDir . '/phpseclib/phpseclib/phpseclib/System/SSH/Agent.php', + 'phpseclib\\System\\SSH\\Agent\\Identity' => $vendorDir . '/phpseclib/phpseclib/phpseclib/System/SSH/Agent/Identity.php', + 'wapmorgan\\Mp3Info\\Mp3Info' => $vendorDir . '/wapmorgan/mp3info/src/Mp3Info.php', +); diff --git a/3rdparty/composer/autoload_files.php b/3rdparty/composer/autoload_files.php new file mode 100644 index 00000000..cd7dda71 --- /dev/null +++ b/3rdparty/composer/autoload_files.php @@ -0,0 +1,31 @@ + $vendorDir . '/symfony/deprecation-contracts/function.php', + '383eaff206634a77a1be54e64e6459c7' => $vendorDir . '/sabre/uri/lib/functions.php', + '7b11c4dc42b3b3023073cb14e519683c' => $vendorDir . '/ralouphie/getallheaders/src/getallheaders.php', + 'e69f7f6ee287b969198c3c9d6777bd38' => $vendorDir . '/symfony/polyfill-intl-normalizer/bootstrap.php', + '37a3dc5111fe8f707ab4c132ef1dbc62' => $vendorDir . '/guzzlehttp/guzzle/src/functions_include.php', + '2b9d0f43f9552984cfa82fee95491826' => $vendorDir . '/sabre/event/lib/coroutine.php', + 'd81bab31d3feb45bfe2f283ea3c8fdf7' => $vendorDir . '/sabre/event/lib/Loop/functions.php', + 'a1cce3d26cc15c00fcd0b3354bd72c88' => $vendorDir . '/sabre/event/lib/Promise/functions.php', + '3569eecfeed3bcf0bad3c998a494ecb8' => $vendorDir . '/sabre/xml/lib/Deserializer/functions.php', + '93aa591bc4ca510c520999e34229ee79' => $vendorDir . '/sabre/xml/lib/Serializer/functions.php', + 'f598d06aa772fa33d905e87be6398fb1' => $vendorDir . '/symfony/polyfill-intl-idn/bootstrap.php', + '8825ede83f2f289127722d4e842cf7e8' => $vendorDir . '/symfony/polyfill-intl-grapheme/bootstrap.php', + 'ebdb698ed4152ae445614b69b5e4bb6a' => $vendorDir . '/sabre/http/lib/functions.php', + '09f6b20656683369174dd6fa83b7e5fb' => $vendorDir . '/symfony/polyfill-uuid/bootstrap.php', + 'b6b991a57620e2fb6b2f66f03fe9ddc2' => $vendorDir . '/symfony/string/Resources/functions.php', + 'b067bc7112e384b61c701452d53a14a8' => $vendorDir . '/mtdowling/jmespath.php/src/JmesPath.php', + '662a729f963d39afe703c9d9b7ab4a8c' => $vendorDir . '/symfony/polyfill-php83/bootstrap.php', + '8a9dc1de0ca7e01f3e08231539562f61' => $vendorDir . '/aws/aws-sdk-php/src/functions.php', + 'decc78cc4436b1292c6c0d151b19445c' => $vendorDir . '/phpseclib/phpseclib/phpseclib/bootstrap.php', + '5897ea0ac4cccf14d323035e65887801' => $vendorDir . '/symfony/polyfill-php82/bootstrap.php', + '9d2b9fc6db0f153a0a149fefb182415e' => $vendorDir . '/symfony/polyfill-php84/bootstrap.php', + 'a1105708a18b76903365ca1c4aa61b02' => $vendorDir . '/symfony/translation/Resources/functions.php', +); diff --git a/3rdparty/composer/autoload_namespaces.php b/3rdparty/composer/autoload_namespaces.php new file mode 100644 index 00000000..59e9e08d --- /dev/null +++ b/3rdparty/composer/autoload_namespaces.php @@ -0,0 +1,12 @@ + array($vendorDir . '/pimple/pimple/src'), + 'Console' => array($vendorDir . '/pear/console_getopt'), + 'Archive_Tar' => array($vendorDir . '/pear/archive_tar'), +); diff --git a/3rdparty/composer/autoload_psr4.php b/3rdparty/composer/autoload_psr4.php new file mode 100644 index 00000000..0dab79b9 --- /dev/null +++ b/3rdparty/composer/autoload_psr4.php @@ -0,0 +1,88 @@ + array($vendorDir . '/wapmorgan/mp3info/src'), + 'phpseclib\\' => array($vendorDir . '/phpseclib/phpseclib/phpseclib'), + 'ownCloud\\TarStreamer\\' => array($vendorDir . '/deepdiver1975/tarstreamer/src'), + 'libphonenumber\\' => array($vendorDir . '/giggsey/libphonenumber-for-php-lite/src'), + 'kornrunner\\Blurhash\\' => array($vendorDir . '/kornrunner/blurhash/src'), + 'cweagans\\Composer\\' => array($vendorDir . '/cweagans/composer-patches/src'), + 'bantu\\IniGetWrapper\\' => array($vendorDir . '/bantu/ini-get-wrapper/src'), + 'ZipStreamer\\' => array($vendorDir . '/deepdiver/zipstreamer/src'), + 'Webauthn\\' => array($vendorDir . '/web-auth/webauthn-lib/src'), + 'Symfony\\Polyfill\\Uuid\\' => array($vendorDir . '/symfony/polyfill-uuid'), + 'Symfony\\Polyfill\\Php84\\' => array($vendorDir . '/symfony/polyfill-php84'), + 'Symfony\\Polyfill\\Php83\\' => array($vendorDir . '/symfony/polyfill-php83'), + 'Symfony\\Polyfill\\Php82\\' => array($vendorDir . '/symfony/polyfill-php82'), + 'Symfony\\Polyfill\\Intl\\Normalizer\\' => array($vendorDir . '/symfony/polyfill-intl-normalizer'), + 'Symfony\\Polyfill\\Intl\\Idn\\' => array($vendorDir . '/symfony/polyfill-intl-idn'), + 'Symfony\\Polyfill\\Intl\\Grapheme\\' => array($vendorDir . '/symfony/polyfill-intl-grapheme'), + 'Symfony\\Contracts\\Translation\\' => array($vendorDir . '/symfony/translation-contracts'), + 'Symfony\\Contracts\\Service\\' => array($vendorDir . '/symfony/service-contracts'), + 'Symfony\\Contracts\\EventDispatcher\\' => array($vendorDir . '/symfony/event-dispatcher-contracts'), + 'Symfony\\Component\\Uid\\' => array($vendorDir . '/symfony/uid'), + 'Symfony\\Component\\Translation\\' => array($vendorDir . '/symfony/translation'), + 'Symfony\\Component\\String\\' => array($vendorDir . '/symfony/string'), + 'Symfony\\Component\\Routing\\' => array($vendorDir . '/symfony/routing'), + 'Symfony\\Component\\Process\\' => array($vendorDir . '/symfony/process'), + 'Symfony\\Component\\Mime\\' => array($vendorDir . '/symfony/mime'), + 'Symfony\\Component\\Mailer\\' => array($vendorDir . '/symfony/mailer'), + 'Symfony\\Component\\HttpFoundation\\' => array($vendorDir . '/symfony/http-foundation'), + 'Symfony\\Component\\EventDispatcher\\' => array($vendorDir . '/symfony/event-dispatcher'), + 'Symfony\\Component\\DomCrawler\\' => array($vendorDir . '/symfony/dom-crawler'), + 'Symfony\\Component\\CssSelector\\' => array($vendorDir . '/symfony/css-selector'), + 'Symfony\\Component\\Console\\' => array($vendorDir . '/symfony/console'), + 'Stecman\\Component\\Symfony\\Console\\BashCompletion\\' => array($vendorDir . '/stecman/symfony-console-completion/src'), + 'SpomkyLabs\\Pki\\' => array($vendorDir . '/spomky-labs/pki-framework/src'), + 'SearchDAV\\' => array($vendorDir . '/icewind/searchdav/src'), + 'Sabre\\Xml\\' => array($vendorDir . '/sabre/xml/lib'), + 'Sabre\\VObject\\' => array($vendorDir . '/sabre/vobject/lib'), + 'Sabre\\Uri\\' => array($vendorDir . '/sabre/uri/lib'), + 'Sabre\\HTTP\\' => array($vendorDir . '/sabre/http/lib'), + 'Sabre\\Event\\' => array($vendorDir . '/sabre/event/lib'), + 'Sabre\\' => array($vendorDir . '/sabre/dav/lib'), + 'Punic\\' => array($vendorDir . '/punic/punic/src'), + 'Psr\\Log\\' => array($vendorDir . '/psr/log/src'), + 'Psr\\Http\\Message\\' => array($vendorDir . '/psr/http-factory/src', $vendorDir . '/psr/http-message/src'), + 'Psr\\Http\\Client\\' => array($vendorDir . '/psr/http-client/src'), + 'Psr\\EventDispatcher\\' => array($vendorDir . '/psr/event-dispatcher/src'), + 'Psr\\Container\\' => array($vendorDir . '/psr/container/src'), + 'Psr\\Clock\\' => array($vendorDir . '/psr/clock/src'), + 'Psr\\Cache\\' => array($vendorDir . '/psr/cache/src'), + 'ParagonIE\\ConstantTime\\' => array($vendorDir . '/paragonie/constant_time_encoding/src'), + 'OpenStack\\' => array($vendorDir . '/php-opencloud/openstack/src'), + 'F7cloud\\LogNormalizer\\' => array($vendorDir . '/f7cloud/lognormalizer/src'), + 'MicrosoftAzure\\Storage\\Common\\' => array($vendorDir . '/microsoft/azure-storage-common/src/Common'), + 'MicrosoftAzure\\Storage\\Blob\\' => array($vendorDir . '/microsoft/azure-storage-blob/src/Blob'), + 'Masterminds\\' => array($vendorDir . '/masterminds/html5/src'), + 'MabeEnum\\' => array($vendorDir . '/marc-mabe/php-enum/src'), + 'Lcobucci\\Clock\\' => array($vendorDir . '/lcobucci/clock/src'), + 'Laravel\\SerializableClosure\\' => array($vendorDir . '/laravel/serializable-closure/src'), + 'JsonSchema\\' => array($vendorDir . '/justinrainbow/json-schema/src/JsonSchema'), + 'JmesPath\\' => array($vendorDir . '/mtdowling/jmespath.php/src'), + 'Icewind\\Streams\\' => array($vendorDir . '/icewind/streams/src'), + 'Icewind\\SMB\\' => array($vendorDir . '/icewind/smb/src'), + 'IPLib\\' => array($vendorDir . '/mlocati/ip-lib/src'), + 'Http\\Promise\\' => array($vendorDir . '/php-http/promise/src'), + 'Http\\Client\\' => array($vendorDir . '/php-http/httplug/src'), + 'Http\\Adapter\\Guzzle7\\' => array($vendorDir . '/php-http/guzzle7-adapter/src'), + 'GuzzleHttp\\UriTemplate\\' => array($vendorDir . '/guzzlehttp/uri-template/src'), + 'GuzzleHttp\\Psr7\\' => array($vendorDir . '/guzzlehttp/psr7/src'), + 'GuzzleHttp\\Promise\\' => array($vendorDir . '/guzzlehttp/promises/src'), + 'GuzzleHttp\\' => array($vendorDir . '/guzzlehttp/guzzle/src'), + 'Fusonic\\OpenGraph\\' => array($vendorDir . '/fusonic/opengraph/src'), + 'Egulias\\EmailValidator\\' => array($vendorDir . '/egulias/email-validator/src'), + 'Doctrine\\Deprecations\\' => array($vendorDir . '/doctrine/deprecations/src'), + 'Doctrine\\DBAL\\' => array($vendorDir . '/doctrine/dbal/src'), + 'Doctrine\\Common\\Lexer\\' => array($vendorDir . '/doctrine/lexer/src'), + 'Doctrine\\Common\\' => array($vendorDir . '/doctrine/event-manager/src'), + 'Cose\\' => array($vendorDir . '/web-auth/cose-lib/src'), + 'CBOR\\' => array($vendorDir . '/spomky-labs/cbor-php/src'), + 'Brick\\Math\\' => array($vendorDir . '/brick/math/src'), + 'Aws\\' => array($vendorDir . '/aws/aws-sdk-php/src'), +); diff --git a/3rdparty/composer/autoload_real.php b/3rdparty/composer/autoload_real.php new file mode 100644 index 00000000..149de3c3 --- /dev/null +++ b/3rdparty/composer/autoload_real.php @@ -0,0 +1,55 @@ +setClassMapAuthoritative(true); + $loader->register(true); + + $filesToLoad = \Composer\Autoload\ComposerStaticInit2f23f73bc0cc116b4b1eee1521aa8652::$files; + $requireFile = \Closure::bind(static function ($fileIdentifier, $file) { + if (empty($GLOBALS['__composer_autoload_files'][$fileIdentifier])) { + $GLOBALS['__composer_autoload_files'][$fileIdentifier] = true; + + require $file; + } + }, null, null); + foreach ($filesToLoad as $fileIdentifier => $file) { + $requireFile($fileIdentifier, $file); + } + + return $loader; + } +} diff --git a/3rdparty/composer/autoload_static.php b/3rdparty/composer/autoload_static.php new file mode 100644 index 00000000..60cf4e38 --- /dev/null +++ b/3rdparty/composer/autoload_static.php @@ -0,0 +1,4195 @@ + __DIR__ . '/..' . '/symfony/deprecation-contracts/function.php', + '383eaff206634a77a1be54e64e6459c7' => __DIR__ . '/..' . '/sabre/uri/lib/functions.php', + '7b11c4dc42b3b3023073cb14e519683c' => __DIR__ . '/..' . '/ralouphie/getallheaders/src/getallheaders.php', + 'e69f7f6ee287b969198c3c9d6777bd38' => __DIR__ . '/..' . '/symfony/polyfill-intl-normalizer/bootstrap.php', + '37a3dc5111fe8f707ab4c132ef1dbc62' => __DIR__ . '/..' . '/guzzlehttp/guzzle/src/functions_include.php', + '2b9d0f43f9552984cfa82fee95491826' => __DIR__ . '/..' . '/sabre/event/lib/coroutine.php', + 'd81bab31d3feb45bfe2f283ea3c8fdf7' => __DIR__ . '/..' . '/sabre/event/lib/Loop/functions.php', + 'a1cce3d26cc15c00fcd0b3354bd72c88' => __DIR__ . '/..' . '/sabre/event/lib/Promise/functions.php', + '3569eecfeed3bcf0bad3c998a494ecb8' => __DIR__ . '/..' . '/sabre/xml/lib/Deserializer/functions.php', + '93aa591bc4ca510c520999e34229ee79' => __DIR__ . '/..' . '/sabre/xml/lib/Serializer/functions.php', + 'f598d06aa772fa33d905e87be6398fb1' => __DIR__ . '/..' . '/symfony/polyfill-intl-idn/bootstrap.php', + '8825ede83f2f289127722d4e842cf7e8' => __DIR__ . '/..' . '/symfony/polyfill-intl-grapheme/bootstrap.php', + 'ebdb698ed4152ae445614b69b5e4bb6a' => __DIR__ . '/..' . '/sabre/http/lib/functions.php', + '09f6b20656683369174dd6fa83b7e5fb' => __DIR__ . '/..' . '/symfony/polyfill-uuid/bootstrap.php', + 'b6b991a57620e2fb6b2f66f03fe9ddc2' => __DIR__ . '/..' . '/symfony/string/Resources/functions.php', + 'b067bc7112e384b61c701452d53a14a8' => __DIR__ . '/..' . '/mtdowling/jmespath.php/src/JmesPath.php', + '662a729f963d39afe703c9d9b7ab4a8c' => __DIR__ . '/..' . '/symfony/polyfill-php83/bootstrap.php', + '8a9dc1de0ca7e01f3e08231539562f61' => __DIR__ . '/..' . '/aws/aws-sdk-php/src/functions.php', + 'decc78cc4436b1292c6c0d151b19445c' => __DIR__ . '/..' . '/phpseclib/phpseclib/phpseclib/bootstrap.php', + '5897ea0ac4cccf14d323035e65887801' => __DIR__ . '/..' . '/symfony/polyfill-php82/bootstrap.php', + '9d2b9fc6db0f153a0a149fefb182415e' => __DIR__ . '/..' . '/symfony/polyfill-php84/bootstrap.php', + 'a1105708a18b76903365ca1c4aa61b02' => __DIR__ . '/..' . '/symfony/translation/Resources/functions.php', + ); + + public static $prefixLengthsPsr4 = array ( + 'w' => + array ( + 'wapmorgan\\Mp3Info\\' => 18, + ), + 'p' => + array ( + 'phpseclib\\' => 10, + ), + 'o' => + array ( + 'ownCloud\\TarStreamer\\' => 21, + ), + 'l' => + array ( + 'libphonenumber\\' => 15, + ), + 'k' => + array ( + 'kornrunner\\Blurhash\\' => 20, + ), + 'c' => + array ( + 'cweagans\\Composer\\' => 18, + ), + 'b' => + array ( + 'bantu\\IniGetWrapper\\' => 20, + ), + 'Z' => + array ( + 'ZipStreamer\\' => 12, + ), + 'W' => + array ( + 'Webauthn\\' => 9, + ), + 'S' => + array ( + 'Symfony\\Polyfill\\Uuid\\' => 22, + 'Symfony\\Polyfill\\Php84\\' => 23, + 'Symfony\\Polyfill\\Php83\\' => 23, + 'Symfony\\Polyfill\\Php82\\' => 23, + 'Symfony\\Polyfill\\Intl\\Normalizer\\' => 33, + 'Symfony\\Polyfill\\Intl\\Idn\\' => 26, + 'Symfony\\Polyfill\\Intl\\Grapheme\\' => 31, + 'Symfony\\Contracts\\Translation\\' => 30, + 'Symfony\\Contracts\\Service\\' => 26, + 'Symfony\\Contracts\\EventDispatcher\\' => 34, + 'Symfony\\Component\\Uid\\' => 22, + 'Symfony\\Component\\Translation\\' => 30, + 'Symfony\\Component\\String\\' => 25, + 'Symfony\\Component\\Routing\\' => 26, + 'Symfony\\Component\\Process\\' => 26, + 'Symfony\\Component\\Mime\\' => 23, + 'Symfony\\Component\\Mailer\\' => 25, + 'Symfony\\Component\\HttpFoundation\\' => 33, + 'Symfony\\Component\\EventDispatcher\\' => 34, + 'Symfony\\Component\\DomCrawler\\' => 29, + 'Symfony\\Component\\CssSelector\\' => 30, + 'Symfony\\Component\\Console\\' => 26, + 'Stecman\\Component\\Symfony\\Console\\BashCompletion\\' => 49, + 'SpomkyLabs\\Pki\\' => 15, + 'SearchDAV\\' => 10, + 'Sabre\\Xml\\' => 10, + 'Sabre\\VObject\\' => 14, + 'Sabre\\Uri\\' => 10, + 'Sabre\\HTTP\\' => 11, + 'Sabre\\Event\\' => 12, + 'Sabre\\' => 6, + ), + 'P' => + array ( + 'Punic\\' => 6, + 'Psr\\Log\\' => 8, + 'Psr\\Http\\Message\\' => 17, + 'Psr\\Http\\Client\\' => 16, + 'Psr\\EventDispatcher\\' => 20, + 'Psr\\Container\\' => 14, + 'Psr\\Clock\\' => 10, + 'Psr\\Cache\\' => 10, + 'ParagonIE\\ConstantTime\\' => 23, + ), + 'O' => + array ( + 'OpenStack\\' => 10, + ), + 'N' => + array ( + 'F7cloud\\LogNormalizer\\' => 24, + ), + 'M' => + array ( + 'MicrosoftAzure\\Storage\\Common\\' => 30, + 'MicrosoftAzure\\Storage\\Blob\\' => 28, + 'Masterminds\\' => 12, + 'MabeEnum\\' => 9, + ), + 'L' => + array ( + 'Lcobucci\\Clock\\' => 15, + 'Laravel\\SerializableClosure\\' => 28, + ), + 'J' => + array ( + 'JsonSchema\\' => 11, + 'JmesPath\\' => 9, + ), + 'I' => + array ( + 'Icewind\\Streams\\' => 16, + 'Icewind\\SMB\\' => 12, + 'IPLib\\' => 6, + ), + 'H' => + array ( + 'Http\\Promise\\' => 13, + 'Http\\Client\\' => 12, + 'Http\\Adapter\\Guzzle7\\' => 21, + ), + 'G' => + array ( + 'GuzzleHttp\\UriTemplate\\' => 23, + 'GuzzleHttp\\Psr7\\' => 16, + 'GuzzleHttp\\Promise\\' => 19, + 'GuzzleHttp\\' => 11, + ), + 'F' => + array ( + 'Fusonic\\OpenGraph\\' => 18, + ), + 'E' => + array ( + 'Egulias\\EmailValidator\\' => 23, + ), + 'D' => + array ( + 'Doctrine\\Deprecations\\' => 22, + 'Doctrine\\DBAL\\' => 14, + 'Doctrine\\Common\\Lexer\\' => 22, + 'Doctrine\\Common\\' => 16, + ), + 'C' => + array ( + 'Cose\\' => 5, + 'CBOR\\' => 5, + ), + 'B' => + array ( + 'Brick\\Math\\' => 11, + ), + 'A' => + array ( + 'Aws\\' => 4, + ), + ); + + public static $prefixDirsPsr4 = array ( + 'wapmorgan\\Mp3Info\\' => + array ( + 0 => __DIR__ . '/..' . '/wapmorgan/mp3info/src', + ), + 'phpseclib\\' => + array ( + 0 => __DIR__ . '/..' . '/phpseclib/phpseclib/phpseclib', + ), + 'ownCloud\\TarStreamer\\' => + array ( + 0 => __DIR__ . '/..' . '/deepdiver1975/tarstreamer/src', + ), + 'libphonenumber\\' => + array ( + 0 => __DIR__ . '/..' . '/giggsey/libphonenumber-for-php-lite/src', + ), + 'kornrunner\\Blurhash\\' => + array ( + 0 => __DIR__ . '/..' . '/kornrunner/blurhash/src', + ), + 'cweagans\\Composer\\' => + array ( + 0 => __DIR__ . '/..' . '/cweagans/composer-patches/src', + ), + 'bantu\\IniGetWrapper\\' => + array ( + 0 => __DIR__ . '/..' . '/bantu/ini-get-wrapper/src', + ), + 'ZipStreamer\\' => + array ( + 0 => __DIR__ . '/..' . '/deepdiver/zipstreamer/src', + ), + 'Webauthn\\' => + array ( + 0 => __DIR__ . '/..' . '/web-auth/webauthn-lib/src', + ), + 'Symfony\\Polyfill\\Uuid\\' => + array ( + 0 => __DIR__ . '/..' . '/symfony/polyfill-uuid', + ), + 'Symfony\\Polyfill\\Php84\\' => + array ( + 0 => __DIR__ . '/..' . '/symfony/polyfill-php84', + ), + 'Symfony\\Polyfill\\Php83\\' => + array ( + 0 => __DIR__ . '/..' . '/symfony/polyfill-php83', + ), + 'Symfony\\Polyfill\\Php82\\' => + array ( + 0 => __DIR__ . '/..' . '/symfony/polyfill-php82', + ), + 'Symfony\\Polyfill\\Intl\\Normalizer\\' => + array ( + 0 => __DIR__ . '/..' . '/symfony/polyfill-intl-normalizer', + ), + 'Symfony\\Polyfill\\Intl\\Idn\\' => + array ( + 0 => __DIR__ . '/..' . '/symfony/polyfill-intl-idn', + ), + 'Symfony\\Polyfill\\Intl\\Grapheme\\' => + array ( + 0 => __DIR__ . '/..' . '/symfony/polyfill-intl-grapheme', + ), + 'Symfony\\Contracts\\Translation\\' => + array ( + 0 => __DIR__ . '/..' . '/symfony/translation-contracts', + ), + 'Symfony\\Contracts\\Service\\' => + array ( + 0 => __DIR__ . '/..' . '/symfony/service-contracts', + ), + 'Symfony\\Contracts\\EventDispatcher\\' => + array ( + 0 => __DIR__ . '/..' . '/symfony/event-dispatcher-contracts', + ), + 'Symfony\\Component\\Uid\\' => + array ( + 0 => __DIR__ . '/..' . '/symfony/uid', + ), + 'Symfony\\Component\\Translation\\' => + array ( + 0 => __DIR__ . '/..' . '/symfony/translation', + ), + 'Symfony\\Component\\String\\' => + array ( + 0 => __DIR__ . '/..' . '/symfony/string', + ), + 'Symfony\\Component\\Routing\\' => + array ( + 0 => __DIR__ . '/..' . '/symfony/routing', + ), + 'Symfony\\Component\\Process\\' => + array ( + 0 => __DIR__ . '/..' . '/symfony/process', + ), + 'Symfony\\Component\\Mime\\' => + array ( + 0 => __DIR__ . '/..' . '/symfony/mime', + ), + 'Symfony\\Component\\Mailer\\' => + array ( + 0 => __DIR__ . '/..' . '/symfony/mailer', + ), + 'Symfony\\Component\\HttpFoundation\\' => + array ( + 0 => __DIR__ . '/..' . '/symfony/http-foundation', + ), + 'Symfony\\Component\\EventDispatcher\\' => + array ( + 0 => __DIR__ . '/..' . '/symfony/event-dispatcher', + ), + 'Symfony\\Component\\DomCrawler\\' => + array ( + 0 => __DIR__ . '/..' . '/symfony/dom-crawler', + ), + 'Symfony\\Component\\CssSelector\\' => + array ( + 0 => __DIR__ . '/..' . '/symfony/css-selector', + ), + 'Symfony\\Component\\Console\\' => + array ( + 0 => __DIR__ . '/..' . '/symfony/console', + ), + 'Stecman\\Component\\Symfony\\Console\\BashCompletion\\' => + array ( + 0 => __DIR__ . '/..' . '/stecman/symfony-console-completion/src', + ), + 'SpomkyLabs\\Pki\\' => + array ( + 0 => __DIR__ . '/..' . '/spomky-labs/pki-framework/src', + ), + 'SearchDAV\\' => + array ( + 0 => __DIR__ . '/..' . '/icewind/searchdav/src', + ), + 'Sabre\\Xml\\' => + array ( + 0 => __DIR__ . '/..' . '/sabre/xml/lib', + ), + 'Sabre\\VObject\\' => + array ( + 0 => __DIR__ . '/..' . '/sabre/vobject/lib', + ), + 'Sabre\\Uri\\' => + array ( + 0 => __DIR__ . '/..' . '/sabre/uri/lib', + ), + 'Sabre\\HTTP\\' => + array ( + 0 => __DIR__ . '/..' . '/sabre/http/lib', + ), + 'Sabre\\Event\\' => + array ( + 0 => __DIR__ . '/..' . '/sabre/event/lib', + ), + 'Sabre\\' => + array ( + 0 => __DIR__ . '/..' . '/sabre/dav/lib', + ), + 'Punic\\' => + array ( + 0 => __DIR__ . '/..' . '/punic/punic/src', + ), + 'Psr\\Log\\' => + array ( + 0 => __DIR__ . '/..' . '/psr/log/src', + ), + 'Psr\\Http\\Message\\' => + array ( + 0 => __DIR__ . '/..' . '/psr/http-factory/src', + 1 => __DIR__ . '/..' . '/psr/http-message/src', + ), + 'Psr\\Http\\Client\\' => + array ( + 0 => __DIR__ . '/..' . '/psr/http-client/src', + ), + 'Psr\\EventDispatcher\\' => + array ( + 0 => __DIR__ . '/..' . '/psr/event-dispatcher/src', + ), + 'Psr\\Container\\' => + array ( + 0 => __DIR__ . '/..' . '/psr/container/src', + ), + 'Psr\\Clock\\' => + array ( + 0 => __DIR__ . '/..' . '/psr/clock/src', + ), + 'Psr\\Cache\\' => + array ( + 0 => __DIR__ . '/..' . '/psr/cache/src', + ), + 'ParagonIE\\ConstantTime\\' => + array ( + 0 => __DIR__ . '/..' . '/paragonie/constant_time_encoding/src', + ), + 'OpenStack\\' => + array ( + 0 => __DIR__ . '/..' . '/php-opencloud/openstack/src', + ), + 'F7cloud\\LogNormalizer\\' => + array ( + 0 => __DIR__ . '/..' . '/f7cloud/lognormalizer/src', + ), + 'MicrosoftAzure\\Storage\\Common\\' => + array ( + 0 => __DIR__ . '/..' . '/microsoft/azure-storage-common/src/Common', + ), + 'MicrosoftAzure\\Storage\\Blob\\' => + array ( + 0 => __DIR__ . '/..' . '/microsoft/azure-storage-blob/src/Blob', + ), + 'Masterminds\\' => + array ( + 0 => __DIR__ . '/..' . '/masterminds/html5/src', + ), + 'MabeEnum\\' => + array ( + 0 => __DIR__ . '/..' . '/marc-mabe/php-enum/src', + ), + 'Lcobucci\\Clock\\' => + array ( + 0 => __DIR__ . '/..' . '/lcobucci/clock/src', + ), + 'Laravel\\SerializableClosure\\' => + array ( + 0 => __DIR__ . '/..' . '/laravel/serializable-closure/src', + ), + 'JsonSchema\\' => + array ( + 0 => __DIR__ . '/..' . '/justinrainbow/json-schema/src/JsonSchema', + ), + 'JmesPath\\' => + array ( + 0 => __DIR__ . '/..' . '/mtdowling/jmespath.php/src', + ), + 'Icewind\\Streams\\' => + array ( + 0 => __DIR__ . '/..' . '/icewind/streams/src', + ), + 'Icewind\\SMB\\' => + array ( + 0 => __DIR__ . '/..' . '/icewind/smb/src', + ), + 'IPLib\\' => + array ( + 0 => __DIR__ . '/..' . '/mlocati/ip-lib/src', + ), + 'Http\\Promise\\' => + array ( + 0 => __DIR__ . '/..' . '/php-http/promise/src', + ), + 'Http\\Client\\' => + array ( + 0 => __DIR__ . '/..' . '/php-http/httplug/src', + ), + 'Http\\Adapter\\Guzzle7\\' => + array ( + 0 => __DIR__ . '/..' . '/php-http/guzzle7-adapter/src', + ), + 'GuzzleHttp\\UriTemplate\\' => + array ( + 0 => __DIR__ . '/..' . '/guzzlehttp/uri-template/src', + ), + 'GuzzleHttp\\Psr7\\' => + array ( + 0 => __DIR__ . '/..' . '/guzzlehttp/psr7/src', + ), + 'GuzzleHttp\\Promise\\' => + array ( + 0 => __DIR__ . '/..' . '/guzzlehttp/promises/src', + ), + 'GuzzleHttp\\' => + array ( + 0 => __DIR__ . '/..' . '/guzzlehttp/guzzle/src', + ), + 'Fusonic\\OpenGraph\\' => + array ( + 0 => __DIR__ . '/..' . '/fusonic/opengraph/src', + ), + 'Egulias\\EmailValidator\\' => + array ( + 0 => __DIR__ . '/..' . '/egulias/email-validator/src', + ), + 'Doctrine\\Deprecations\\' => + array ( + 0 => __DIR__ . '/..' . '/doctrine/deprecations/src', + ), + 'Doctrine\\DBAL\\' => + array ( + 0 => __DIR__ . '/..' . '/doctrine/dbal/src', + ), + 'Doctrine\\Common\\Lexer\\' => + array ( + 0 => __DIR__ . '/..' . '/doctrine/lexer/src', + ), + 'Doctrine\\Common\\' => + array ( + 0 => __DIR__ . '/..' . '/doctrine/event-manager/src', + ), + 'Cose\\' => + array ( + 0 => __DIR__ . '/..' . '/web-auth/cose-lib/src', + ), + 'CBOR\\' => + array ( + 0 => __DIR__ . '/..' . '/spomky-labs/cbor-php/src', + ), + 'Brick\\Math\\' => + array ( + 0 => __DIR__ . '/..' . '/brick/math/src', + ), + 'Aws\\' => + array ( + 0 => __DIR__ . '/..' . '/aws/aws-sdk-php/src', + ), + ); + + public static $prefixesPsr0 = array ( + 'P' => + array ( + 'Pimple' => + array ( + 0 => __DIR__ . '/..' . '/pimple/pimple/src', + ), + ), + 'C' => + array ( + 'Console' => + array ( + 0 => __DIR__ . '/..' . '/pear/console_getopt', + ), + ), + 'A' => + array ( + 'Archive_Tar' => + array ( + 0 => __DIR__ . '/..' . '/pear/archive_tar', + ), + ), + ); + + public static $classMap = array ( + 'AWS\\CRT\\Auth\\AwsCredentials' => __DIR__ . '/..' . '/aws/aws-crt-php/src/AWS/CRT/Auth/AwsCredentials.php', + 'AWS\\CRT\\Auth\\CredentialsProvider' => __DIR__ . '/..' . '/aws/aws-crt-php/src/AWS/CRT/Auth/CredentialsProvider.php', + 'AWS\\CRT\\Auth\\Signable' => __DIR__ . '/..' . '/aws/aws-crt-php/src/AWS/CRT/Auth/Signable.php', + 'AWS\\CRT\\Auth\\SignatureType' => __DIR__ . '/..' . '/aws/aws-crt-php/src/AWS/CRT/Auth/SignatureType.php', + 'AWS\\CRT\\Auth\\SignedBodyHeaderType' => __DIR__ . '/..' . '/aws/aws-crt-php/src/AWS/CRT/Auth/SignedBodyHeaderType.php', + 'AWS\\CRT\\Auth\\Signing' => __DIR__ . '/..' . '/aws/aws-crt-php/src/AWS/CRT/Auth/Signing.php', + 'AWS\\CRT\\Auth\\SigningAlgorithm' => __DIR__ . '/..' . '/aws/aws-crt-php/src/AWS/CRT/Auth/SigningAlgorithm.php', + 'AWS\\CRT\\Auth\\SigningConfigAWS' => __DIR__ . '/..' . '/aws/aws-crt-php/src/AWS/CRT/Auth/SigningConfigAWS.php', + 'AWS\\CRT\\Auth\\SigningResult' => __DIR__ . '/..' . '/aws/aws-crt-php/src/AWS/CRT/Auth/SigningResult.php', + 'AWS\\CRT\\Auth\\StaticCredentialsProvider' => __DIR__ . '/..' . '/aws/aws-crt-php/src/AWS/CRT/Auth/StaticCredentialsProvider.php', + 'AWS\\CRT\\CRT' => __DIR__ . '/..' . '/aws/aws-crt-php/src/AWS/CRT/CRT.php', + 'AWS\\CRT\\HTTP\\Headers' => __DIR__ . '/..' . '/aws/aws-crt-php/src/AWS/CRT/HTTP/Headers.php', + 'AWS\\CRT\\HTTP\\Message' => __DIR__ . '/..' . '/aws/aws-crt-php/src/AWS/CRT/HTTP/Message.php', + 'AWS\\CRT\\HTTP\\Request' => __DIR__ . '/..' . '/aws/aws-crt-php/src/AWS/CRT/HTTP/Request.php', + 'AWS\\CRT\\HTTP\\Response' => __DIR__ . '/..' . '/aws/aws-crt-php/src/AWS/CRT/HTTP/Response.php', + 'AWS\\CRT\\IO\\EventLoopGroup' => __DIR__ . '/..' . '/aws/aws-crt-php/src/AWS/CRT/IO/EventLoopGroup.php', + 'AWS\\CRT\\IO\\InputStream' => __DIR__ . '/..' . '/aws/aws-crt-php/src/AWS/CRT/IO/InputStream.php', + 'AWS\\CRT\\Internal\\Encoding' => __DIR__ . '/..' . '/aws/aws-crt-php/src/AWS/CRT/Internal/Encoding.php', + 'AWS\\CRT\\Internal\\Extension' => __DIR__ . '/..' . '/aws/aws-crt-php/src/AWS/CRT/Internal/Extension.php', + 'AWS\\CRT\\Log' => __DIR__ . '/..' . '/aws/aws-crt-php/src/AWS/CRT/Log.php', + 'AWS\\CRT\\NativeResource' => __DIR__ . '/..' . '/aws/aws-crt-php/src/AWS/CRT/NativeResource.php', + 'AWS\\CRT\\OptionValue' => __DIR__ . '/..' . '/aws/aws-crt-php/src/AWS/CRT/Options.php', + 'AWS\\CRT\\Options' => __DIR__ . '/..' . '/aws/aws-crt-php/src/AWS/CRT/Options.php', + 'AllowDynamicProperties' => __DIR__ . '/..' . '/symfony/polyfill-php82/Resources/stubs/AllowDynamicProperties.php', + 'Archive_Tar' => __DIR__ . '/..' . '/pear/archive_tar/Archive/Tar.php', + 'Aws\\AbstractConfigurationProvider' => __DIR__ . '/..' . '/aws/aws-sdk-php/src/AbstractConfigurationProvider.php', + 'Aws\\Api\\AbstractModel' => __DIR__ . '/..' . '/aws/aws-sdk-php/src/Api/AbstractModel.php', + 'Aws\\Api\\ApiProvider' => __DIR__ . '/..' . '/aws/aws-sdk-php/src/Api/ApiProvider.php', + 'Aws\\Api\\DateTimeResult' => __DIR__ . '/..' . '/aws/aws-sdk-php/src/Api/DateTimeResult.php', + 'Aws\\Api\\DocModel' => __DIR__ . '/..' . '/aws/aws-sdk-php/src/Api/DocModel.php', + 'Aws\\Api\\ErrorParser\\AbstractErrorParser' => __DIR__ . '/..' . '/aws/aws-sdk-php/src/Api/ErrorParser/AbstractErrorParser.php', + 'Aws\\Api\\ErrorParser\\JsonParserTrait' => __DIR__ . '/..' . '/aws/aws-sdk-php/src/Api/ErrorParser/JsonParserTrait.php', + 'Aws\\Api\\ErrorParser\\JsonRpcErrorParser' => __DIR__ . '/..' . '/aws/aws-sdk-php/src/Api/ErrorParser/JsonRpcErrorParser.php', + 'Aws\\Api\\ErrorParser\\RestJsonErrorParser' => __DIR__ . '/..' . '/aws/aws-sdk-php/src/Api/ErrorParser/RestJsonErrorParser.php', + 'Aws\\Api\\ErrorParser\\XmlErrorParser' => __DIR__ . '/..' . '/aws/aws-sdk-php/src/Api/ErrorParser/XmlErrorParser.php', + 'Aws\\Api\\ListShape' => __DIR__ . '/..' . '/aws/aws-sdk-php/src/Api/ListShape.php', + 'Aws\\Api\\MapShape' => __DIR__ . '/..' . '/aws/aws-sdk-php/src/Api/MapShape.php', + 'Aws\\Api\\Operation' => __DIR__ . '/..' . '/aws/aws-sdk-php/src/Api/Operation.php', + 'Aws\\Api\\Parser\\AbstractParser' => __DIR__ . '/..' . '/aws/aws-sdk-php/src/Api/Parser/AbstractParser.php', + 'Aws\\Api\\Parser\\AbstractRestParser' => __DIR__ . '/..' . '/aws/aws-sdk-php/src/Api/Parser/AbstractRestParser.php', + 'Aws\\Api\\Parser\\Crc32ValidatingParser' => __DIR__ . '/..' . '/aws/aws-sdk-php/src/Api/Parser/Crc32ValidatingParser.php', + 'Aws\\Api\\Parser\\DecodingEventStreamIterator' => __DIR__ . '/..' . '/aws/aws-sdk-php/src/Api/Parser/DecodingEventStreamIterator.php', + 'Aws\\Api\\Parser\\EventParsingIterator' => __DIR__ . '/..' . '/aws/aws-sdk-php/src/Api/Parser/EventParsingIterator.php', + 'Aws\\Api\\Parser\\Exception\\ParserException' => __DIR__ . '/..' . '/aws/aws-sdk-php/src/Api/Parser/Exception/ParserException.php', + 'Aws\\Api\\Parser\\JsonParser' => __DIR__ . '/..' . '/aws/aws-sdk-php/src/Api/Parser/JsonParser.php', + 'Aws\\Api\\Parser\\JsonRpcParser' => __DIR__ . '/..' . '/aws/aws-sdk-php/src/Api/Parser/JsonRpcParser.php', + 'Aws\\Api\\Parser\\MetadataParserTrait' => __DIR__ . '/..' . '/aws/aws-sdk-php/src/Api/Parser/MetadataParserTrait.php', + 'Aws\\Api\\Parser\\NonSeekableStreamDecodingEventStreamIterator' => __DIR__ . '/..' . '/aws/aws-sdk-php/src/Api/Parser/NonSeekableStreamDecodingEventStreamIterator.php', + 'Aws\\Api\\Parser\\PayloadParserTrait' => __DIR__ . '/..' . '/aws/aws-sdk-php/src/Api/Parser/PayloadParserTrait.php', + 'Aws\\Api\\Parser\\QueryParser' => __DIR__ . '/..' . '/aws/aws-sdk-php/src/Api/Parser/QueryParser.php', + 'Aws\\Api\\Parser\\RestJsonParser' => __DIR__ . '/..' . '/aws/aws-sdk-php/src/Api/Parser/RestJsonParser.php', + 'Aws\\Api\\Parser\\RestXmlParser' => __DIR__ . '/..' . '/aws/aws-sdk-php/src/Api/Parser/RestXmlParser.php', + 'Aws\\Api\\Parser\\XmlParser' => __DIR__ . '/..' . '/aws/aws-sdk-php/src/Api/Parser/XmlParser.php', + 'Aws\\Api\\Serializer\\Ec2ParamBuilder' => __DIR__ . '/..' . '/aws/aws-sdk-php/src/Api/Serializer/Ec2ParamBuilder.php', + 'Aws\\Api\\Serializer\\JsonBody' => __DIR__ . '/..' . '/aws/aws-sdk-php/src/Api/Serializer/JsonBody.php', + 'Aws\\Api\\Serializer\\JsonRpcSerializer' => __DIR__ . '/..' . '/aws/aws-sdk-php/src/Api/Serializer/JsonRpcSerializer.php', + 'Aws\\Api\\Serializer\\QueryParamBuilder' => __DIR__ . '/..' . '/aws/aws-sdk-php/src/Api/Serializer/QueryParamBuilder.php', + 'Aws\\Api\\Serializer\\QuerySerializer' => __DIR__ . '/..' . '/aws/aws-sdk-php/src/Api/Serializer/QuerySerializer.php', + 'Aws\\Api\\Serializer\\RestJsonSerializer' => __DIR__ . '/..' . '/aws/aws-sdk-php/src/Api/Serializer/RestJsonSerializer.php', + 'Aws\\Api\\Serializer\\RestSerializer' => __DIR__ . '/..' . '/aws/aws-sdk-php/src/Api/Serializer/RestSerializer.php', + 'Aws\\Api\\Serializer\\RestXmlSerializer' => __DIR__ . '/..' . '/aws/aws-sdk-php/src/Api/Serializer/RestXmlSerializer.php', + 'Aws\\Api\\Serializer\\XmlBody' => __DIR__ . '/..' . '/aws/aws-sdk-php/src/Api/Serializer/XmlBody.php', + 'Aws\\Api\\Service' => __DIR__ . '/..' . '/aws/aws-sdk-php/src/Api/Service.php', + 'Aws\\Api\\Shape' => __DIR__ . '/..' . '/aws/aws-sdk-php/src/Api/Shape.php', + 'Aws\\Api\\ShapeMap' => __DIR__ . '/..' . '/aws/aws-sdk-php/src/Api/ShapeMap.php', + 'Aws\\Api\\StructureShape' => __DIR__ . '/..' . '/aws/aws-sdk-php/src/Api/StructureShape.php', + 'Aws\\Api\\SupportedProtocols' => __DIR__ . '/..' . '/aws/aws-sdk-php/src/Api/SupportedProtocols.php', + 'Aws\\Api\\TimestampShape' => __DIR__ . '/..' . '/aws/aws-sdk-php/src/Api/TimestampShape.php', + 'Aws\\Api\\Validator' => __DIR__ . '/..' . '/aws/aws-sdk-php/src/Api/Validator.php', + 'Aws\\Arn\\AccessPointArn' => __DIR__ . '/..' . '/aws/aws-sdk-php/src/Arn/AccessPointArn.php', + 'Aws\\Arn\\AccessPointArnInterface' => __DIR__ . '/..' . '/aws/aws-sdk-php/src/Arn/AccessPointArnInterface.php', + 'Aws\\Arn\\Arn' => __DIR__ . '/..' . '/aws/aws-sdk-php/src/Arn/Arn.php', + 'Aws\\Arn\\ArnInterface' => __DIR__ . '/..' . '/aws/aws-sdk-php/src/Arn/ArnInterface.php', + 'Aws\\Arn\\ArnParser' => __DIR__ . '/..' . '/aws/aws-sdk-php/src/Arn/ArnParser.php', + 'Aws\\Arn\\Exception\\InvalidArnException' => __DIR__ . '/..' . '/aws/aws-sdk-php/src/Arn/Exception/InvalidArnException.php', + 'Aws\\Arn\\ObjectLambdaAccessPointArn' => __DIR__ . '/..' . '/aws/aws-sdk-php/src/Arn/ObjectLambdaAccessPointArn.php', + 'Aws\\Arn\\ResourceTypeAndIdTrait' => __DIR__ . '/..' . '/aws/aws-sdk-php/src/Arn/ResourceTypeAndIdTrait.php', + 'Aws\\Arn\\S3\\AccessPointArn' => __DIR__ . '/..' . '/aws/aws-sdk-php/src/Arn/S3/AccessPointArn.php', + 'Aws\\Arn\\S3\\BucketArnInterface' => __DIR__ . '/..' . '/aws/aws-sdk-php/src/Arn/S3/BucketArnInterface.php', + 'Aws\\Arn\\S3\\MultiRegionAccessPointArn' => __DIR__ . '/..' . '/aws/aws-sdk-php/src/Arn/S3/MultiRegionAccessPointArn.php', + 'Aws\\Arn\\S3\\OutpostsAccessPointArn' => __DIR__ . '/..' . '/aws/aws-sdk-php/src/Arn/S3/OutpostsAccessPointArn.php', + 'Aws\\Arn\\S3\\OutpostsArnInterface' => __DIR__ . '/..' . '/aws/aws-sdk-php/src/Arn/S3/OutpostsArnInterface.php', + 'Aws\\Arn\\S3\\OutpostsBucketArn' => __DIR__ . '/..' . '/aws/aws-sdk-php/src/Arn/S3/OutpostsBucketArn.php', + 'Aws\\Auth\\AuthSchemeResolver' => __DIR__ . '/..' . '/aws/aws-sdk-php/src/Auth/AuthSchemeResolver.php', + 'Aws\\Auth\\AuthSchemeResolverInterface' => __DIR__ . '/..' . '/aws/aws-sdk-php/src/Auth/AuthSchemeResolverInterface.php', + 'Aws\\Auth\\AuthSelectionMiddleware' => __DIR__ . '/..' . '/aws/aws-sdk-php/src/Auth/AuthSelectionMiddleware.php', + 'Aws\\Auth\\Exception\\UnresolvedAuthSchemeException' => __DIR__ . '/..' . '/aws/aws-sdk-php/src/Auth/Exception/UnresolvedAuthSchemeException.php', + 'Aws\\AwsClient' => __DIR__ . '/..' . '/aws/aws-sdk-php/src/AwsClient.php', + 'Aws\\AwsClientInterface' => __DIR__ . '/..' . '/aws/aws-sdk-php/src/AwsClientInterface.php', + 'Aws\\AwsClientTrait' => __DIR__ . '/..' . '/aws/aws-sdk-php/src/AwsClientTrait.php', + 'Aws\\CacheInterface' => __DIR__ . '/..' . '/aws/aws-sdk-php/src/CacheInterface.php', + 'Aws\\ClientResolver' => __DIR__ . '/..' . '/aws/aws-sdk-php/src/ClientResolver.php', + 'Aws\\ClientSideMonitoring\\AbstractMonitoringMiddleware' => __DIR__ . '/..' . '/aws/aws-sdk-php/src/ClientSideMonitoring/AbstractMonitoringMiddleware.php', + 'Aws\\ClientSideMonitoring\\ApiCallAttemptMonitoringMiddleware' => __DIR__ . '/..' . '/aws/aws-sdk-php/src/ClientSideMonitoring/ApiCallAttemptMonitoringMiddleware.php', + 'Aws\\ClientSideMonitoring\\ApiCallMonitoringMiddleware' => __DIR__ . '/..' . '/aws/aws-sdk-php/src/ClientSideMonitoring/ApiCallMonitoringMiddleware.php', + 'Aws\\ClientSideMonitoring\\Configuration' => __DIR__ . '/..' . '/aws/aws-sdk-php/src/ClientSideMonitoring/Configuration.php', + 'Aws\\ClientSideMonitoring\\ConfigurationInterface' => __DIR__ . '/..' . '/aws/aws-sdk-php/src/ClientSideMonitoring/ConfigurationInterface.php', + 'Aws\\ClientSideMonitoring\\ConfigurationProvider' => __DIR__ . '/..' . '/aws/aws-sdk-php/src/ClientSideMonitoring/ConfigurationProvider.php', + 'Aws\\ClientSideMonitoring\\Exception\\ConfigurationException' => __DIR__ . '/..' . '/aws/aws-sdk-php/src/ClientSideMonitoring/Exception/ConfigurationException.php', + 'Aws\\ClientSideMonitoring\\MonitoringMiddlewareInterface' => __DIR__ . '/..' . '/aws/aws-sdk-php/src/ClientSideMonitoring/MonitoringMiddlewareInterface.php', + 'Aws\\Command' => __DIR__ . '/..' . '/aws/aws-sdk-php/src/Command.php', + 'Aws\\CommandInterface' => __DIR__ . '/..' . '/aws/aws-sdk-php/src/CommandInterface.php', + 'Aws\\CommandPool' => __DIR__ . '/..' . '/aws/aws-sdk-php/src/CommandPool.php', + 'Aws\\ConfigurationProviderInterface' => __DIR__ . '/..' . '/aws/aws-sdk-php/src/ConfigurationProviderInterface.php', + 'Aws\\Configuration\\ConfigurationResolver' => __DIR__ . '/..' . '/aws/aws-sdk-php/src/Configuration/ConfigurationResolver.php', + 'Aws\\Credentials\\AssumeRoleCredentialProvider' => __DIR__ . '/..' . '/aws/aws-sdk-php/src/Credentials/AssumeRoleCredentialProvider.php', + 'Aws\\Credentials\\AssumeRoleWithWebIdentityCredentialProvider' => __DIR__ . '/..' . '/aws/aws-sdk-php/src/Credentials/AssumeRoleWithWebIdentityCredentialProvider.php', + 'Aws\\Credentials\\CredentialProvider' => __DIR__ . '/..' . '/aws/aws-sdk-php/src/Credentials/CredentialProvider.php', + 'Aws\\Credentials\\CredentialSources' => __DIR__ . '/..' . '/aws/aws-sdk-php/src/Credentials/CredentialSources.php', + 'Aws\\Credentials\\Credentials' => __DIR__ . '/..' . '/aws/aws-sdk-php/src/Credentials/Credentials.php', + 'Aws\\Credentials\\CredentialsInterface' => __DIR__ . '/..' . '/aws/aws-sdk-php/src/Credentials/CredentialsInterface.php', + 'Aws\\Credentials\\CredentialsUtils' => __DIR__ . '/..' . '/aws/aws-sdk-php/src/Credentials/CredentialsUtils.php', + 'Aws\\Credentials\\EcsCredentialProvider' => __DIR__ . '/..' . '/aws/aws-sdk-php/src/Credentials/EcsCredentialProvider.php', + 'Aws\\Credentials\\InstanceProfileProvider' => __DIR__ . '/..' . '/aws/aws-sdk-php/src/Credentials/InstanceProfileProvider.php', + 'Aws\\Crypto\\AbstractCryptoClient' => __DIR__ . '/..' . '/aws/aws-sdk-php/src/Crypto/AbstractCryptoClient.php', + 'Aws\\Crypto\\AbstractCryptoClientV2' => __DIR__ . '/..' . '/aws/aws-sdk-php/src/Crypto/AbstractCryptoClientV2.php', + 'Aws\\Crypto\\AesDecryptingStream' => __DIR__ . '/..' . '/aws/aws-sdk-php/src/Crypto/AesDecryptingStream.php', + 'Aws\\Crypto\\AesEncryptingStream' => __DIR__ . '/..' . '/aws/aws-sdk-php/src/Crypto/AesEncryptingStream.php', + 'Aws\\Crypto\\AesGcmDecryptingStream' => __DIR__ . '/..' . '/aws/aws-sdk-php/src/Crypto/AesGcmDecryptingStream.php', + 'Aws\\Crypto\\AesGcmEncryptingStream' => __DIR__ . '/..' . '/aws/aws-sdk-php/src/Crypto/AesGcmEncryptingStream.php', + 'Aws\\Crypto\\AesStreamInterface' => __DIR__ . '/..' . '/aws/aws-sdk-php/src/Crypto/AesStreamInterface.php', + 'Aws\\Crypto\\AesStreamInterfaceV2' => __DIR__ . '/..' . '/aws/aws-sdk-php/src/Crypto/AesStreamInterfaceV2.php', + 'Aws\\Crypto\\Cipher\\Cbc' => __DIR__ . '/..' . '/aws/aws-sdk-php/src/Crypto/Cipher/Cbc.php', + 'Aws\\Crypto\\Cipher\\CipherBuilderTrait' => __DIR__ . '/..' . '/aws/aws-sdk-php/src/Crypto/Cipher/CipherBuilderTrait.php', + 'Aws\\Crypto\\Cipher\\CipherMethod' => __DIR__ . '/..' . '/aws/aws-sdk-php/src/Crypto/Cipher/CipherMethod.php', + 'Aws\\Crypto\\DecryptionTrait' => __DIR__ . '/..' . '/aws/aws-sdk-php/src/Crypto/DecryptionTrait.php', + 'Aws\\Crypto\\DecryptionTraitV2' => __DIR__ . '/..' . '/aws/aws-sdk-php/src/Crypto/DecryptionTraitV2.php', + 'Aws\\Crypto\\EncryptionTrait' => __DIR__ . '/..' . '/aws/aws-sdk-php/src/Crypto/EncryptionTrait.php', + 'Aws\\Crypto\\EncryptionTraitV2' => __DIR__ . '/..' . '/aws/aws-sdk-php/src/Crypto/EncryptionTraitV2.php', + 'Aws\\Crypto\\KmsMaterialsProvider' => __DIR__ . '/..' . '/aws/aws-sdk-php/src/Crypto/KmsMaterialsProvider.php', + 'Aws\\Crypto\\KmsMaterialsProviderV2' => __DIR__ . '/..' . '/aws/aws-sdk-php/src/Crypto/KmsMaterialsProviderV2.php', + 'Aws\\Crypto\\MaterialsProvider' => __DIR__ . '/..' . '/aws/aws-sdk-php/src/Crypto/MaterialsProvider.php', + 'Aws\\Crypto\\MaterialsProviderInterface' => __DIR__ . '/..' . '/aws/aws-sdk-php/src/Crypto/MaterialsProviderInterface.php', + 'Aws\\Crypto\\MaterialsProviderInterfaceV2' => __DIR__ . '/..' . '/aws/aws-sdk-php/src/Crypto/MaterialsProviderInterfaceV2.php', + 'Aws\\Crypto\\MaterialsProviderV2' => __DIR__ . '/..' . '/aws/aws-sdk-php/src/Crypto/MaterialsProviderV2.php', + 'Aws\\Crypto\\MetadataEnvelope' => __DIR__ . '/..' . '/aws/aws-sdk-php/src/Crypto/MetadataEnvelope.php', + 'Aws\\Crypto\\MetadataStrategyInterface' => __DIR__ . '/..' . '/aws/aws-sdk-php/src/Crypto/MetadataStrategyInterface.php', + 'Aws\\DefaultsMode\\Configuration' => __DIR__ . '/..' . '/aws/aws-sdk-php/src/DefaultsMode/Configuration.php', + 'Aws\\DefaultsMode\\ConfigurationInterface' => __DIR__ . '/..' . '/aws/aws-sdk-php/src/DefaultsMode/ConfigurationInterface.php', + 'Aws\\DefaultsMode\\ConfigurationProvider' => __DIR__ . '/..' . '/aws/aws-sdk-php/src/DefaultsMode/ConfigurationProvider.php', + 'Aws\\DefaultsMode\\Exception\\ConfigurationException' => __DIR__ . '/..' . '/aws/aws-sdk-php/src/DefaultsMode/Exception/ConfigurationException.php', + 'Aws\\DoctrineCacheAdapter' => __DIR__ . '/..' . '/aws/aws-sdk-php/src/DoctrineCacheAdapter.php', + 'Aws\\EndpointDiscovery\\Configuration' => __DIR__ . '/..' . '/aws/aws-sdk-php/src/EndpointDiscovery/Configuration.php', + 'Aws\\EndpointDiscovery\\ConfigurationInterface' => __DIR__ . '/..' . '/aws/aws-sdk-php/src/EndpointDiscovery/ConfigurationInterface.php', + 'Aws\\EndpointDiscovery\\ConfigurationProvider' => __DIR__ . '/..' . '/aws/aws-sdk-php/src/EndpointDiscovery/ConfigurationProvider.php', + 'Aws\\EndpointDiscovery\\EndpointDiscoveryMiddleware' => __DIR__ . '/..' . '/aws/aws-sdk-php/src/EndpointDiscovery/EndpointDiscoveryMiddleware.php', + 'Aws\\EndpointDiscovery\\EndpointList' => __DIR__ . '/..' . '/aws/aws-sdk-php/src/EndpointDiscovery/EndpointList.php', + 'Aws\\EndpointDiscovery\\Exception\\ConfigurationException' => __DIR__ . '/..' . '/aws/aws-sdk-php/src/EndpointDiscovery/Exception/ConfigurationException.php', + 'Aws\\EndpointParameterMiddleware' => __DIR__ . '/..' . '/aws/aws-sdk-php/src/EndpointParameterMiddleware.php', + 'Aws\\EndpointV2\\EndpointDefinitionProvider' => __DIR__ . '/..' . '/aws/aws-sdk-php/src/EndpointV2/EndpointDefinitionProvider.php', + 'Aws\\EndpointV2\\EndpointProviderV2' => __DIR__ . '/..' . '/aws/aws-sdk-php/src/EndpointV2/EndpointProviderV2.php', + 'Aws\\EndpointV2\\EndpointV2Middleware' => __DIR__ . '/..' . '/aws/aws-sdk-php/src/EndpointV2/EndpointV2Middleware.php', + 'Aws\\EndpointV2\\EndpointV2SerializerTrait' => __DIR__ . '/..' . '/aws/aws-sdk-php/src/EndpointV2/EndpointV2SerializerTrait.php', + 'Aws\\EndpointV2\\Rule\\AbstractRule' => __DIR__ . '/..' . '/aws/aws-sdk-php/src/EndpointV2/Rule/AbstractRule.php', + 'Aws\\EndpointV2\\Rule\\EndpointRule' => __DIR__ . '/..' . '/aws/aws-sdk-php/src/EndpointV2/Rule/EndpointRule.php', + 'Aws\\EndpointV2\\Rule\\ErrorRule' => __DIR__ . '/..' . '/aws/aws-sdk-php/src/EndpointV2/Rule/ErrorRule.php', + 'Aws\\EndpointV2\\Rule\\RuleCreator' => __DIR__ . '/..' . '/aws/aws-sdk-php/src/EndpointV2/Rule/RuleCreator.php', + 'Aws\\EndpointV2\\Rule\\TreeRule' => __DIR__ . '/..' . '/aws/aws-sdk-php/src/EndpointV2/Rule/TreeRule.php', + 'Aws\\EndpointV2\\Ruleset\\Ruleset' => __DIR__ . '/..' . '/aws/aws-sdk-php/src/EndpointV2/Ruleset/Ruleset.php', + 'Aws\\EndpointV2\\Ruleset\\RulesetEndpoint' => __DIR__ . '/..' . '/aws/aws-sdk-php/src/EndpointV2/Ruleset/RulesetEndpoint.php', + 'Aws\\EndpointV2\\Ruleset\\RulesetParameter' => __DIR__ . '/..' . '/aws/aws-sdk-php/src/EndpointV2/Ruleset/RulesetParameter.php', + 'Aws\\EndpointV2\\Ruleset\\RulesetStandardLibrary' => __DIR__ . '/..' . '/aws/aws-sdk-php/src/EndpointV2/Ruleset/RulesetStandardLibrary.php', + 'Aws\\Endpoint\\EndpointProvider' => __DIR__ . '/..' . '/aws/aws-sdk-php/src/Endpoint/EndpointProvider.php', + 'Aws\\Endpoint\\Partition' => __DIR__ . '/..' . '/aws/aws-sdk-php/src/Endpoint/Partition.php', + 'Aws\\Endpoint\\PartitionEndpointProvider' => __DIR__ . '/..' . '/aws/aws-sdk-php/src/Endpoint/PartitionEndpointProvider.php', + 'Aws\\Endpoint\\PartitionInterface' => __DIR__ . '/..' . '/aws/aws-sdk-php/src/Endpoint/PartitionInterface.php', + 'Aws\\Endpoint\\PatternEndpointProvider' => __DIR__ . '/..' . '/aws/aws-sdk-php/src/Endpoint/PatternEndpointProvider.php', + 'Aws\\Endpoint\\UseDualstackEndpoint\\Configuration' => __DIR__ . '/..' . '/aws/aws-sdk-php/src/Endpoint/UseDualstackEndpoint/Configuration.php', + 'Aws\\Endpoint\\UseDualstackEndpoint\\ConfigurationInterface' => __DIR__ . '/..' . '/aws/aws-sdk-php/src/Endpoint/UseDualstackEndpoint/ConfigurationInterface.php', + 'Aws\\Endpoint\\UseDualstackEndpoint\\ConfigurationProvider' => __DIR__ . '/..' . '/aws/aws-sdk-php/src/Endpoint/UseDualstackEndpoint/ConfigurationProvider.php', + 'Aws\\Endpoint\\UseDualstackEndpoint\\Exception\\ConfigurationException' => __DIR__ . '/..' . '/aws/aws-sdk-php/src/Endpoint/UseDualstackEndpoint/Exception/ConfigurationException.php', + 'Aws\\Endpoint\\UseFipsEndpoint\\Configuration' => __DIR__ . '/..' . '/aws/aws-sdk-php/src/Endpoint/UseFipsEndpoint/Configuration.php', + 'Aws\\Endpoint\\UseFipsEndpoint\\ConfigurationInterface' => __DIR__ . '/..' . '/aws/aws-sdk-php/src/Endpoint/UseFipsEndpoint/ConfigurationInterface.php', + 'Aws\\Endpoint\\UseFipsEndpoint\\ConfigurationProvider' => __DIR__ . '/..' . '/aws/aws-sdk-php/src/Endpoint/UseFipsEndpoint/ConfigurationProvider.php', + 'Aws\\Endpoint\\UseFipsEndpoint\\Exception\\ConfigurationException' => __DIR__ . '/..' . '/aws/aws-sdk-php/src/Endpoint/UseFipsEndpoint/Exception/ConfigurationException.php', + 'Aws\\Exception\\AwsException' => __DIR__ . '/..' . '/aws/aws-sdk-php/src/Exception/AwsException.php', + 'Aws\\Exception\\CommonRuntimeException' => __DIR__ . '/..' . '/aws/aws-sdk-php/src/Exception/CommonRuntimeException.php', + 'Aws\\Exception\\CouldNotCreateChecksumException' => __DIR__ . '/..' . '/aws/aws-sdk-php/src/Exception/CouldNotCreateChecksumException.php', + 'Aws\\Exception\\CredentialsException' => __DIR__ . '/..' . '/aws/aws-sdk-php/src/Exception/CredentialsException.php', + 'Aws\\Exception\\CryptoException' => __DIR__ . '/..' . '/aws/aws-sdk-php/src/Exception/CryptoException.php', + 'Aws\\Exception\\CryptoPolyfillException' => __DIR__ . '/..' . '/aws/aws-sdk-php/src/Exception/CryptoPolyfillException.php', + 'Aws\\Exception\\EventStreamDataException' => __DIR__ . '/..' . '/aws/aws-sdk-php/src/Exception/EventStreamDataException.php', + 'Aws\\Exception\\IncalculablePayloadException' => __DIR__ . '/..' . '/aws/aws-sdk-php/src/Exception/IncalculablePayloadException.php', + 'Aws\\Exception\\InvalidJsonException' => __DIR__ . '/..' . '/aws/aws-sdk-php/src/Exception/InvalidJsonException.php', + 'Aws\\Exception\\InvalidRegionException' => __DIR__ . '/..' . '/aws/aws-sdk-php/src/Exception/InvalidRegionException.php', + 'Aws\\Exception\\MultipartUploadException' => __DIR__ . '/..' . '/aws/aws-sdk-php/src/Exception/MultipartUploadException.php', + 'Aws\\Exception\\TokenException' => __DIR__ . '/..' . '/aws/aws-sdk-php/src/Exception/TokenException.php', + 'Aws\\Exception\\UnresolvedApiException' => __DIR__ . '/..' . '/aws/aws-sdk-php/src/Exception/UnresolvedApiException.php', + 'Aws\\Exception\\UnresolvedEndpointException' => __DIR__ . '/..' . '/aws/aws-sdk-php/src/Exception/UnresolvedEndpointException.php', + 'Aws\\Exception\\UnresolvedSignatureException' => __DIR__ . '/..' . '/aws/aws-sdk-php/src/Exception/UnresolvedSignatureException.php', + 'Aws\\HandlerList' => __DIR__ . '/..' . '/aws/aws-sdk-php/src/HandlerList.php', + 'Aws\\Handler\\Guzzle\\GuzzleHandler' => __DIR__ . '/..' . '/aws/aws-sdk-php/src/Handler/Guzzle/GuzzleHandler.php', + 'Aws\\HasDataTrait' => __DIR__ . '/..' . '/aws/aws-sdk-php/src/HasDataTrait.php', + 'Aws\\HasMonitoringEventsTrait' => __DIR__ . '/..' . '/aws/aws-sdk-php/src/HasMonitoringEventsTrait.php', + 'Aws\\HashInterface' => __DIR__ . '/..' . '/aws/aws-sdk-php/src/HashInterface.php', + 'Aws\\HashingStream' => __DIR__ . '/..' . '/aws/aws-sdk-php/src/HashingStream.php', + 'Aws\\History' => __DIR__ . '/..' . '/aws/aws-sdk-php/src/History.php', + 'Aws\\IdempotencyTokenMiddleware' => __DIR__ . '/..' . '/aws/aws-sdk-php/src/IdempotencyTokenMiddleware.php', + 'Aws\\Identity\\AwsCredentialIdentity' => __DIR__ . '/..' . '/aws/aws-sdk-php/src/Identity/AwsCredentialIdentity.php', + 'Aws\\Identity\\BearerTokenIdentity' => __DIR__ . '/..' . '/aws/aws-sdk-php/src/Identity/BearerTokenIdentity.php', + 'Aws\\Identity\\IdentityInterface' => __DIR__ . '/..' . '/aws/aws-sdk-php/src/Identity/IdentityInterface.php', + 'Aws\\Identity\\S3\\S3ExpressIdentity' => __DIR__ . '/..' . '/aws/aws-sdk-php/src/Identity/S3/S3ExpressIdentity.php', + 'Aws\\Identity\\S3\\S3ExpressIdentityProvider' => __DIR__ . '/..' . '/aws/aws-sdk-php/src/Identity/S3/S3ExpressIdentityProvider.php', + 'Aws\\InputValidationMiddleware' => __DIR__ . '/..' . '/aws/aws-sdk-php/src/InputValidationMiddleware.php', + 'Aws\\JsonCompiler' => __DIR__ . '/..' . '/aws/aws-sdk-php/src/JsonCompiler.php', + 'Aws\\Kms\\Exception\\KmsException' => __DIR__ . '/..' . '/aws/aws-sdk-php/src/Kms/Exception/KmsException.php', + 'Aws\\Kms\\KmsClient' => __DIR__ . '/..' . '/aws/aws-sdk-php/src/Kms/KmsClient.php', + 'Aws\\LruArrayCache' => __DIR__ . '/..' . '/aws/aws-sdk-php/src/LruArrayCache.php', + 'Aws\\MetricsBuilder' => __DIR__ . '/..' . '/aws/aws-sdk-php/src/MetricsBuilder.php', + 'Aws\\Middleware' => __DIR__ . '/..' . '/aws/aws-sdk-php/src/Middleware.php', + 'Aws\\MockHandler' => __DIR__ . '/..' . '/aws/aws-sdk-php/src/MockHandler.php', + 'Aws\\MonitoringEventsInterface' => __DIR__ . '/..' . '/aws/aws-sdk-php/src/MonitoringEventsInterface.php', + 'Aws\\MultiRegionClient' => __DIR__ . '/..' . '/aws/aws-sdk-php/src/MultiRegionClient.php', + 'Aws\\Multipart\\AbstractUploadManager' => __DIR__ . '/..' . '/aws/aws-sdk-php/src/Multipart/AbstractUploadManager.php', + 'Aws\\Multipart\\AbstractUploader' => __DIR__ . '/..' . '/aws/aws-sdk-php/src/Multipart/AbstractUploader.php', + 'Aws\\Multipart\\UploadState' => __DIR__ . '/..' . '/aws/aws-sdk-php/src/Multipart/UploadState.php', + 'Aws\\PhpHash' => __DIR__ . '/..' . '/aws/aws-sdk-php/src/PhpHash.php', + 'Aws\\PresignUrlMiddleware' => __DIR__ . '/..' . '/aws/aws-sdk-php/src/PresignUrlMiddleware.php', + 'Aws\\Psr16CacheAdapter' => __DIR__ . '/..' . '/aws/aws-sdk-php/src/Psr16CacheAdapter.php', + 'Aws\\PsrCacheAdapter' => __DIR__ . '/..' . '/aws/aws-sdk-php/src/PsrCacheAdapter.php', + 'Aws\\QueryCompatibleInputMiddleware' => __DIR__ . '/..' . '/aws/aws-sdk-php/src/QueryCompatibleInputMiddleware.php', + 'Aws\\RequestCompressionMiddleware' => __DIR__ . '/..' . '/aws/aws-sdk-php/src/RequestCompressionMiddleware.php', + 'Aws\\ResponseContainerInterface' => __DIR__ . '/..' . '/aws/aws-sdk-php/src/ResponseContainerInterface.php', + 'Aws\\Result' => __DIR__ . '/..' . '/aws/aws-sdk-php/src/Result.php', + 'Aws\\ResultInterface' => __DIR__ . '/..' . '/aws/aws-sdk-php/src/ResultInterface.php', + 'Aws\\ResultPaginator' => __DIR__ . '/..' . '/aws/aws-sdk-php/src/ResultPaginator.php', + 'Aws\\RetryMiddleware' => __DIR__ . '/..' . '/aws/aws-sdk-php/src/RetryMiddleware.php', + 'Aws\\RetryMiddlewareV2' => __DIR__ . '/..' . '/aws/aws-sdk-php/src/RetryMiddlewareV2.php', + 'Aws\\Retry\\Configuration' => __DIR__ . '/..' . '/aws/aws-sdk-php/src/Retry/Configuration.php', + 'Aws\\Retry\\ConfigurationInterface' => __DIR__ . '/..' . '/aws/aws-sdk-php/src/Retry/ConfigurationInterface.php', + 'Aws\\Retry\\ConfigurationProvider' => __DIR__ . '/..' . '/aws/aws-sdk-php/src/Retry/ConfigurationProvider.php', + 'Aws\\Retry\\Exception\\ConfigurationException' => __DIR__ . '/..' . '/aws/aws-sdk-php/src/Retry/Exception/ConfigurationException.php', + 'Aws\\Retry\\QuotaManager' => __DIR__ . '/..' . '/aws/aws-sdk-php/src/Retry/QuotaManager.php', + 'Aws\\Retry\\RateLimiter' => __DIR__ . '/..' . '/aws/aws-sdk-php/src/Retry/RateLimiter.php', + 'Aws\\Retry\\RetryHelperTrait' => __DIR__ . '/..' . '/aws/aws-sdk-php/src/Retry/RetryHelperTrait.php', + 'Aws\\S3\\AmbiguousSuccessParser' => __DIR__ . '/..' . '/aws/aws-sdk-php/src/S3/AmbiguousSuccessParser.php', + 'Aws\\S3\\ApplyChecksumMiddleware' => __DIR__ . '/..' . '/aws/aws-sdk-php/src/S3/ApplyChecksumMiddleware.php', + 'Aws\\S3\\BatchDelete' => __DIR__ . '/..' . '/aws/aws-sdk-php/src/S3/BatchDelete.php', + 'Aws\\S3\\BucketEndpointArnMiddleware' => __DIR__ . '/..' . '/aws/aws-sdk-php/src/S3/BucketEndpointArnMiddleware.php', + 'Aws\\S3\\BucketEndpointMiddleware' => __DIR__ . '/..' . '/aws/aws-sdk-php/src/S3/BucketEndpointMiddleware.php', + 'Aws\\S3\\CalculatesChecksumTrait' => __DIR__ . '/..' . '/aws/aws-sdk-php/src/S3/CalculatesChecksumTrait.php', + 'Aws\\S3\\Crypto\\CryptoParamsTrait' => __DIR__ . '/..' . '/aws/aws-sdk-php/src/S3/Crypto/CryptoParamsTrait.php', + 'Aws\\S3\\Crypto\\CryptoParamsTraitV2' => __DIR__ . '/..' . '/aws/aws-sdk-php/src/S3/Crypto/CryptoParamsTraitV2.php', + 'Aws\\S3\\Crypto\\HeadersMetadataStrategy' => __DIR__ . '/..' . '/aws/aws-sdk-php/src/S3/Crypto/HeadersMetadataStrategy.php', + 'Aws\\S3\\Crypto\\InstructionFileMetadataStrategy' => __DIR__ . '/..' . '/aws/aws-sdk-php/src/S3/Crypto/InstructionFileMetadataStrategy.php', + 'Aws\\S3\\Crypto\\S3EncryptionClient' => __DIR__ . '/..' . '/aws/aws-sdk-php/src/S3/Crypto/S3EncryptionClient.php', + 'Aws\\S3\\Crypto\\S3EncryptionClientV2' => __DIR__ . '/..' . '/aws/aws-sdk-php/src/S3/Crypto/S3EncryptionClientV2.php', + 'Aws\\S3\\Crypto\\S3EncryptionMultipartUploader' => __DIR__ . '/..' . '/aws/aws-sdk-php/src/S3/Crypto/S3EncryptionMultipartUploader.php', + 'Aws\\S3\\Crypto\\S3EncryptionMultipartUploaderV2' => __DIR__ . '/..' . '/aws/aws-sdk-php/src/S3/Crypto/S3EncryptionMultipartUploaderV2.php', + 'Aws\\S3\\Crypto\\UserAgentTrait' => __DIR__ . '/..' . '/aws/aws-sdk-php/src/S3/Crypto/UserAgentTrait.php', + 'Aws\\S3\\EndpointRegionHelperTrait' => __DIR__ . '/..' . '/aws/aws-sdk-php/src/S3/EndpointRegionHelperTrait.php', + 'Aws\\S3\\Exception\\DeleteMultipleObjectsException' => __DIR__ . '/..' . '/aws/aws-sdk-php/src/S3/Exception/DeleteMultipleObjectsException.php', + 'Aws\\S3\\Exception\\PermanentRedirectException' => __DIR__ . '/..' . '/aws/aws-sdk-php/src/S3/Exception/PermanentRedirectException.php', + 'Aws\\S3\\Exception\\S3Exception' => __DIR__ . '/..' . '/aws/aws-sdk-php/src/S3/Exception/S3Exception.php', + 'Aws\\S3\\Exception\\S3MultipartUploadException' => __DIR__ . '/..' . '/aws/aws-sdk-php/src/S3/Exception/S3MultipartUploadException.php', + 'Aws\\S3\\ExpiresParsingMiddleware' => __DIR__ . '/..' . '/aws/aws-sdk-php/src/S3/ExpiresParsingMiddleware.php', + 'Aws\\S3\\GetBucketLocationParser' => __DIR__ . '/..' . '/aws/aws-sdk-php/src/S3/GetBucketLocationParser.php', + 'Aws\\S3\\MultipartCopy' => __DIR__ . '/..' . '/aws/aws-sdk-php/src/S3/MultipartCopy.php', + 'Aws\\S3\\MultipartUploader' => __DIR__ . '/..' . '/aws/aws-sdk-php/src/S3/MultipartUploader.php', + 'Aws\\S3\\MultipartUploadingTrait' => __DIR__ . '/..' . '/aws/aws-sdk-php/src/S3/MultipartUploadingTrait.php', + 'Aws\\S3\\ObjectCopier' => __DIR__ . '/..' . '/aws/aws-sdk-php/src/S3/ObjectCopier.php', + 'Aws\\S3\\ObjectUploader' => __DIR__ . '/..' . '/aws/aws-sdk-php/src/S3/ObjectUploader.php', + 'Aws\\S3\\Parser\\GetBucketLocationResultMutator' => __DIR__ . '/..' . '/aws/aws-sdk-php/src/S3/Parser/GetBucketLocationResultMutator.php', + 'Aws\\S3\\Parser\\S3Parser' => __DIR__ . '/..' . '/aws/aws-sdk-php/src/S3/Parser/S3Parser.php', + 'Aws\\S3\\Parser\\S3ResultMutator' => __DIR__ . '/..' . '/aws/aws-sdk-php/src/S3/Parser/S3ResultMutator.php', + 'Aws\\S3\\Parser\\ValidateResponseChecksumResultMutator' => __DIR__ . '/..' . '/aws/aws-sdk-php/src/S3/Parser/ValidateResponseChecksumResultMutator.php', + 'Aws\\S3\\PermanentRedirectMiddleware' => __DIR__ . '/..' . '/aws/aws-sdk-php/src/S3/PermanentRedirectMiddleware.php', + 'Aws\\S3\\PostObject' => __DIR__ . '/..' . '/aws/aws-sdk-php/src/S3/PostObject.php', + 'Aws\\S3\\PostObjectV4' => __DIR__ . '/..' . '/aws/aws-sdk-php/src/S3/PostObjectV4.php', + 'Aws\\S3\\PutObjectUrlMiddleware' => __DIR__ . '/..' . '/aws/aws-sdk-php/src/S3/PutObjectUrlMiddleware.php', + 'Aws\\S3\\RegionalEndpoint\\Configuration' => __DIR__ . '/..' . '/aws/aws-sdk-php/src/S3/RegionalEndpoint/Configuration.php', + 'Aws\\S3\\RegionalEndpoint\\ConfigurationInterface' => __DIR__ . '/..' . '/aws/aws-sdk-php/src/S3/RegionalEndpoint/ConfigurationInterface.php', + 'Aws\\S3\\RegionalEndpoint\\ConfigurationProvider' => __DIR__ . '/..' . '/aws/aws-sdk-php/src/S3/RegionalEndpoint/ConfigurationProvider.php', + 'Aws\\S3\\RegionalEndpoint\\Exception\\ConfigurationException' => __DIR__ . '/..' . '/aws/aws-sdk-php/src/S3/RegionalEndpoint/Exception/ConfigurationException.php', + 'Aws\\S3\\RetryableMalformedResponseParser' => __DIR__ . '/..' . '/aws/aws-sdk-php/src/S3/RetryableMalformedResponseParser.php', + 'Aws\\S3\\S3Client' => __DIR__ . '/..' . '/aws/aws-sdk-php/src/S3/S3Client.php', + 'Aws\\S3\\S3ClientInterface' => __DIR__ . '/..' . '/aws/aws-sdk-php/src/S3/S3ClientInterface.php', + 'Aws\\S3\\S3ClientTrait' => __DIR__ . '/..' . '/aws/aws-sdk-php/src/S3/S3ClientTrait.php', + 'Aws\\S3\\S3EndpointMiddleware' => __DIR__ . '/..' . '/aws/aws-sdk-php/src/S3/S3EndpointMiddleware.php', + 'Aws\\S3\\S3MultiRegionClient' => __DIR__ . '/..' . '/aws/aws-sdk-php/src/S3/S3MultiRegionClient.php', + 'Aws\\S3\\S3UriParser' => __DIR__ . '/..' . '/aws/aws-sdk-php/src/S3/S3UriParser.php', + 'Aws\\S3\\SSECMiddleware' => __DIR__ . '/..' . '/aws/aws-sdk-php/src/S3/SSECMiddleware.php', + 'Aws\\S3\\StreamWrapper' => __DIR__ . '/..' . '/aws/aws-sdk-php/src/S3/StreamWrapper.php', + 'Aws\\S3\\Transfer' => __DIR__ . '/..' . '/aws/aws-sdk-php/src/S3/Transfer.php', + 'Aws\\S3\\UseArnRegion\\Configuration' => __DIR__ . '/..' . '/aws/aws-sdk-php/src/S3/UseArnRegion/Configuration.php', + 'Aws\\S3\\UseArnRegion\\ConfigurationInterface' => __DIR__ . '/..' . '/aws/aws-sdk-php/src/S3/UseArnRegion/ConfigurationInterface.php', + 'Aws\\S3\\UseArnRegion\\ConfigurationProvider' => __DIR__ . '/..' . '/aws/aws-sdk-php/src/S3/UseArnRegion/ConfigurationProvider.php', + 'Aws\\S3\\UseArnRegion\\Exception\\ConfigurationException' => __DIR__ . '/..' . '/aws/aws-sdk-php/src/S3/UseArnRegion/Exception/ConfigurationException.php', + 'Aws\\S3\\ValidateResponseChecksumParser' => __DIR__ . '/..' . '/aws/aws-sdk-php/src/S3/ValidateResponseChecksumParser.php', + 'Aws\\SSOOIDC\\Exception\\SSOOIDCException' => __DIR__ . '/..' . '/aws/aws-sdk-php/src/SSOOIDC/Exception/SSOOIDCException.php', + 'Aws\\SSOOIDC\\SSOOIDCClient' => __DIR__ . '/..' . '/aws/aws-sdk-php/src/SSOOIDC/SSOOIDCClient.php', + 'Aws\\SSO\\Exception\\SSOException' => __DIR__ . '/..' . '/aws/aws-sdk-php/src/SSO/Exception/SSOException.php', + 'Aws\\SSO\\SSOClient' => __DIR__ . '/..' . '/aws/aws-sdk-php/src/SSO/SSOClient.php', + 'Aws\\Script\\Composer\\Composer' => __DIR__ . '/..' . '/aws/aws-sdk-php/src/Script/Composer/Composer.php', + 'Aws\\Sdk' => __DIR__ . '/..' . '/aws/aws-sdk-php/src/Sdk.php', + 'Aws\\Signature\\AnonymousSignature' => __DIR__ . '/..' . '/aws/aws-sdk-php/src/Signature/AnonymousSignature.php', + 'Aws\\Signature\\S3ExpressSignature' => __DIR__ . '/..' . '/aws/aws-sdk-php/src/Signature/S3ExpressSignature.php', + 'Aws\\Signature\\S3SignatureV4' => __DIR__ . '/..' . '/aws/aws-sdk-php/src/Signature/S3SignatureV4.php', + 'Aws\\Signature\\SignatureInterface' => __DIR__ . '/..' . '/aws/aws-sdk-php/src/Signature/SignatureInterface.php', + 'Aws\\Signature\\SignatureProvider' => __DIR__ . '/..' . '/aws/aws-sdk-php/src/Signature/SignatureProvider.php', + 'Aws\\Signature\\SignatureTrait' => __DIR__ . '/..' . '/aws/aws-sdk-php/src/Signature/SignatureTrait.php', + 'Aws\\Signature\\SignatureV4' => __DIR__ . '/..' . '/aws/aws-sdk-php/src/Signature/SignatureV4.php', + 'Aws\\StreamRequestPayloadMiddleware' => __DIR__ . '/..' . '/aws/aws-sdk-php/src/StreamRequestPayloadMiddleware.php', + 'Aws\\Sts\\Exception\\StsException' => __DIR__ . '/..' . '/aws/aws-sdk-php/src/Sts/Exception/StsException.php', + 'Aws\\Sts\\RegionalEndpoints\\Configuration' => __DIR__ . '/..' . '/aws/aws-sdk-php/src/Sts/RegionalEndpoints/Configuration.php', + 'Aws\\Sts\\RegionalEndpoints\\ConfigurationInterface' => __DIR__ . '/..' . '/aws/aws-sdk-php/src/Sts/RegionalEndpoints/ConfigurationInterface.php', + 'Aws\\Sts\\RegionalEndpoints\\ConfigurationProvider' => __DIR__ . '/..' . '/aws/aws-sdk-php/src/Sts/RegionalEndpoints/ConfigurationProvider.php', + 'Aws\\Sts\\RegionalEndpoints\\Exception\\ConfigurationException' => __DIR__ . '/..' . '/aws/aws-sdk-php/src/Sts/RegionalEndpoints/Exception/ConfigurationException.php', + 'Aws\\Sts\\StsClient' => __DIR__ . '/..' . '/aws/aws-sdk-php/src/Sts/StsClient.php', + 'Aws\\Token\\BearerTokenAuthorization' => __DIR__ . '/..' . '/aws/aws-sdk-php/src/Token/BearerTokenAuthorization.php', + 'Aws\\Token\\ParsesIniTrait' => __DIR__ . '/..' . '/aws/aws-sdk-php/src/Token/ParsesIniTrait.php', + 'Aws\\Token\\RefreshableTokenProviderInterface' => __DIR__ . '/..' . '/aws/aws-sdk-php/src/Token/RefreshableTokenProviderInterface.php', + 'Aws\\Token\\SsoToken' => __DIR__ . '/..' . '/aws/aws-sdk-php/src/Token/SsoToken.php', + 'Aws\\Token\\SsoTokenProvider' => __DIR__ . '/..' . '/aws/aws-sdk-php/src/Token/SsoTokenProvider.php', + 'Aws\\Token\\Token' => __DIR__ . '/..' . '/aws/aws-sdk-php/src/Token/Token.php', + 'Aws\\Token\\TokenAuthorization' => __DIR__ . '/..' . '/aws/aws-sdk-php/src/Token/TokenAuthorization.php', + 'Aws\\Token\\TokenInterface' => __DIR__ . '/..' . '/aws/aws-sdk-php/src/Token/TokenInterface.php', + 'Aws\\Token\\TokenProvider' => __DIR__ . '/..' . '/aws/aws-sdk-php/src/Token/TokenProvider.php', + 'Aws\\TraceMiddleware' => __DIR__ . '/..' . '/aws/aws-sdk-php/src/TraceMiddleware.php', + 'Aws\\UserAgentMiddleware' => __DIR__ . '/..' . '/aws/aws-sdk-php/src/UserAgentMiddleware.php', + 'Aws\\Waiter' => __DIR__ . '/..' . '/aws/aws-sdk-php/src/Waiter.php', + 'Aws\\WrappedHttpHandler' => __DIR__ . '/..' . '/aws/aws-sdk-php/src/WrappedHttpHandler.php', + 'Brick\\Math\\BigDecimal' => __DIR__ . '/..' . '/brick/math/src/BigDecimal.php', + 'Brick\\Math\\BigInteger' => __DIR__ . '/..' . '/brick/math/src/BigInteger.php', + 'Brick\\Math\\BigNumber' => __DIR__ . '/..' . '/brick/math/src/BigNumber.php', + 'Brick\\Math\\BigRational' => __DIR__ . '/..' . '/brick/math/src/BigRational.php', + 'Brick\\Math\\Exception\\DivisionByZeroException' => __DIR__ . '/..' . '/brick/math/src/Exception/DivisionByZeroException.php', + 'Brick\\Math\\Exception\\IntegerOverflowException' => __DIR__ . '/..' . '/brick/math/src/Exception/IntegerOverflowException.php', + 'Brick\\Math\\Exception\\MathException' => __DIR__ . '/..' . '/brick/math/src/Exception/MathException.php', + 'Brick\\Math\\Exception\\NegativeNumberException' => __DIR__ . '/..' . '/brick/math/src/Exception/NegativeNumberException.php', + 'Brick\\Math\\Exception\\NumberFormatException' => __DIR__ . '/..' . '/brick/math/src/Exception/NumberFormatException.php', + 'Brick\\Math\\Exception\\RoundingNecessaryException' => __DIR__ . '/..' . '/brick/math/src/Exception/RoundingNecessaryException.php', + 'Brick\\Math\\Internal\\Calculator' => __DIR__ . '/..' . '/brick/math/src/Internal/Calculator.php', + 'Brick\\Math\\Internal\\Calculator\\BcMathCalculator' => __DIR__ . '/..' . '/brick/math/src/Internal/Calculator/BcMathCalculator.php', + 'Brick\\Math\\Internal\\Calculator\\GmpCalculator' => __DIR__ . '/..' . '/brick/math/src/Internal/Calculator/GmpCalculator.php', + 'Brick\\Math\\Internal\\Calculator\\NativeCalculator' => __DIR__ . '/..' . '/brick/math/src/Internal/Calculator/NativeCalculator.php', + 'Brick\\Math\\RoundingMode' => __DIR__ . '/..' . '/brick/math/src/RoundingMode.php', + 'CBOR\\AbstractCBORObject' => __DIR__ . '/..' . '/spomky-labs/cbor-php/src/AbstractCBORObject.php', + 'CBOR\\ByteStringObject' => __DIR__ . '/..' . '/spomky-labs/cbor-php/src/ByteStringObject.php', + 'CBOR\\CBORObject' => __DIR__ . '/..' . '/spomky-labs/cbor-php/src/CBORObject.php', + 'CBOR\\Decoder' => __DIR__ . '/..' . '/spomky-labs/cbor-php/src/Decoder.php', + 'CBOR\\DecoderInterface' => __DIR__ . '/..' . '/spomky-labs/cbor-php/src/DecoderInterface.php', + 'CBOR\\IndefiniteLengthByteStringObject' => __DIR__ . '/..' . '/spomky-labs/cbor-php/src/IndefiniteLengthByteStringObject.php', + 'CBOR\\IndefiniteLengthListObject' => __DIR__ . '/..' . '/spomky-labs/cbor-php/src/IndefiniteLengthListObject.php', + 'CBOR\\IndefiniteLengthMapObject' => __DIR__ . '/..' . '/spomky-labs/cbor-php/src/IndefiniteLengthMapObject.php', + 'CBOR\\IndefiniteLengthTextStringObject' => __DIR__ . '/..' . '/spomky-labs/cbor-php/src/IndefiniteLengthTextStringObject.php', + 'CBOR\\LengthCalculator' => __DIR__ . '/..' . '/spomky-labs/cbor-php/src/LengthCalculator.php', + 'CBOR\\ListObject' => __DIR__ . '/..' . '/spomky-labs/cbor-php/src/ListObject.php', + 'CBOR\\MapItem' => __DIR__ . '/..' . '/spomky-labs/cbor-php/src/MapItem.php', + 'CBOR\\MapObject' => __DIR__ . '/..' . '/spomky-labs/cbor-php/src/MapObject.php', + 'CBOR\\NegativeIntegerObject' => __DIR__ . '/..' . '/spomky-labs/cbor-php/src/NegativeIntegerObject.php', + 'CBOR\\Normalizable' => __DIR__ . '/..' . '/spomky-labs/cbor-php/src/Normalizable.php', + 'CBOR\\OtherObject' => __DIR__ . '/..' . '/spomky-labs/cbor-php/src/OtherObject.php', + 'CBOR\\OtherObject\\BreakObject' => __DIR__ . '/..' . '/spomky-labs/cbor-php/src/OtherObject/BreakObject.php', + 'CBOR\\OtherObject\\DoublePrecisionFloatObject' => __DIR__ . '/..' . '/spomky-labs/cbor-php/src/OtherObject/DoublePrecisionFloatObject.php', + 'CBOR\\OtherObject\\FalseObject' => __DIR__ . '/..' . '/spomky-labs/cbor-php/src/OtherObject/FalseObject.php', + 'CBOR\\OtherObject\\GenericObject' => __DIR__ . '/..' . '/spomky-labs/cbor-php/src/OtherObject/GenericObject.php', + 'CBOR\\OtherObject\\HalfPrecisionFloatObject' => __DIR__ . '/..' . '/spomky-labs/cbor-php/src/OtherObject/HalfPrecisionFloatObject.php', + 'CBOR\\OtherObject\\NullObject' => __DIR__ . '/..' . '/spomky-labs/cbor-php/src/OtherObject/NullObject.php', + 'CBOR\\OtherObject\\OtherObjectInterface' => __DIR__ . '/..' . '/spomky-labs/cbor-php/src/OtherObject/OtherObjectInterface.php', + 'CBOR\\OtherObject\\OtherObjectManager' => __DIR__ . '/..' . '/spomky-labs/cbor-php/src/OtherObject/OtherObjectManager.php', + 'CBOR\\OtherObject\\OtherObjectManagerInterface' => __DIR__ . '/..' . '/spomky-labs/cbor-php/src/OtherObject/OtherObjectManagerInterface.php', + 'CBOR\\OtherObject\\SimpleObject' => __DIR__ . '/..' . '/spomky-labs/cbor-php/src/OtherObject/SimpleObject.php', + 'CBOR\\OtherObject\\SinglePrecisionFloatObject' => __DIR__ . '/..' . '/spomky-labs/cbor-php/src/OtherObject/SinglePrecisionFloatObject.php', + 'CBOR\\OtherObject\\TrueObject' => __DIR__ . '/..' . '/spomky-labs/cbor-php/src/OtherObject/TrueObject.php', + 'CBOR\\OtherObject\\UndefinedObject' => __DIR__ . '/..' . '/spomky-labs/cbor-php/src/OtherObject/UndefinedObject.php', + 'CBOR\\Stream' => __DIR__ . '/..' . '/spomky-labs/cbor-php/src/Stream.php', + 'CBOR\\StringStream' => __DIR__ . '/..' . '/spomky-labs/cbor-php/src/StringStream.php', + 'CBOR\\Tag' => __DIR__ . '/..' . '/spomky-labs/cbor-php/src/Tag.php', + 'CBOR\\Tag\\Base16EncodingTag' => __DIR__ . '/..' . '/spomky-labs/cbor-php/src/Tag/Base16EncodingTag.php', + 'CBOR\\Tag\\Base64EncodingTag' => __DIR__ . '/..' . '/spomky-labs/cbor-php/src/Tag/Base64EncodingTag.php', + 'CBOR\\Tag\\Base64Tag' => __DIR__ . '/..' . '/spomky-labs/cbor-php/src/Tag/Base64Tag.php', + 'CBOR\\Tag\\Base64UrlEncodingTag' => __DIR__ . '/..' . '/spomky-labs/cbor-php/src/Tag/Base64UrlEncodingTag.php', + 'CBOR\\Tag\\Base64UrlTag' => __DIR__ . '/..' . '/spomky-labs/cbor-php/src/Tag/Base64UrlTag.php', + 'CBOR\\Tag\\BigFloatTag' => __DIR__ . '/..' . '/spomky-labs/cbor-php/src/Tag/BigFloatTag.php', + 'CBOR\\Tag\\CBOREncodingTag' => __DIR__ . '/..' . '/spomky-labs/cbor-php/src/Tag/CBOREncodingTag.php', + 'CBOR\\Tag\\CBORTag' => __DIR__ . '/..' . '/spomky-labs/cbor-php/src/Tag/CBORTag.php', + 'CBOR\\Tag\\DatetimeTag' => __DIR__ . '/..' . '/spomky-labs/cbor-php/src/Tag/DatetimeTag.php', + 'CBOR\\Tag\\DecimalFractionTag' => __DIR__ . '/..' . '/spomky-labs/cbor-php/src/Tag/DecimalFractionTag.php', + 'CBOR\\Tag\\GenericTag' => __DIR__ . '/..' . '/spomky-labs/cbor-php/src/Tag/GenericTag.php', + 'CBOR\\Tag\\MimeTag' => __DIR__ . '/..' . '/spomky-labs/cbor-php/src/Tag/MimeTag.php', + 'CBOR\\Tag\\NegativeBigIntegerTag' => __DIR__ . '/..' . '/spomky-labs/cbor-php/src/Tag/NegativeBigIntegerTag.php', + 'CBOR\\Tag\\TagInterface' => __DIR__ . '/..' . '/spomky-labs/cbor-php/src/Tag/TagInterface.php', + 'CBOR\\Tag\\TagManager' => __DIR__ . '/..' . '/spomky-labs/cbor-php/src/Tag/TagManager.php', + 'CBOR\\Tag\\TagManagerInterface' => __DIR__ . '/..' . '/spomky-labs/cbor-php/src/Tag/TagManagerInterface.php', + 'CBOR\\Tag\\TimestampTag' => __DIR__ . '/..' . '/spomky-labs/cbor-php/src/Tag/TimestampTag.php', + 'CBOR\\Tag\\UnsignedBigIntegerTag' => __DIR__ . '/..' . '/spomky-labs/cbor-php/src/Tag/UnsignedBigIntegerTag.php', + 'CBOR\\Tag\\UriTag' => __DIR__ . '/..' . '/spomky-labs/cbor-php/src/Tag/UriTag.php', + 'CBOR\\TextStringObject' => __DIR__ . '/..' . '/spomky-labs/cbor-php/src/TextStringObject.php', + 'CBOR\\UnsignedIntegerObject' => __DIR__ . '/..' . '/spomky-labs/cbor-php/src/UnsignedIntegerObject.php', + 'CBOR\\Utils' => __DIR__ . '/..' . '/spomky-labs/cbor-php/src/Utils.php', + 'Composer\\InstalledVersions' => __DIR__ . '/..' . '/composer/InstalledVersions.php', + 'Console_Getopt' => __DIR__ . '/..' . '/pear/console_getopt/Console/Getopt.php', + 'Cose\\Algorithm\\Algorithm' => __DIR__ . '/..' . '/web-auth/cose-lib/src/Algorithm/Algorithm.php', + 'Cose\\Algorithm\\Mac\\HS256' => __DIR__ . '/..' . '/web-auth/cose-lib/src/Algorithm/Mac/HS256.php', + 'Cose\\Algorithm\\Mac\\HS256Truncated64' => __DIR__ . '/..' . '/web-auth/cose-lib/src/Algorithm/Mac/HS256Truncated64.php', + 'Cose\\Algorithm\\Mac\\HS384' => __DIR__ . '/..' . '/web-auth/cose-lib/src/Algorithm/Mac/HS384.php', + 'Cose\\Algorithm\\Mac\\HS512' => __DIR__ . '/..' . '/web-auth/cose-lib/src/Algorithm/Mac/HS512.php', + 'Cose\\Algorithm\\Mac\\Hmac' => __DIR__ . '/..' . '/web-auth/cose-lib/src/Algorithm/Mac/Hmac.php', + 'Cose\\Algorithm\\Mac\\Mac' => __DIR__ . '/..' . '/web-auth/cose-lib/src/Algorithm/Mac/Mac.php', + 'Cose\\Algorithm\\Manager' => __DIR__ . '/..' . '/web-auth/cose-lib/src/Algorithm/Manager.php', + 'Cose\\Algorithm\\ManagerFactory' => __DIR__ . '/..' . '/web-auth/cose-lib/src/Algorithm/ManagerFactory.php', + 'Cose\\Algorithm\\Signature\\ECDSA\\ECDSA' => __DIR__ . '/..' . '/web-auth/cose-lib/src/Algorithm/Signature/ECDSA/ECDSA.php', + 'Cose\\Algorithm\\Signature\\ECDSA\\ECSignature' => __DIR__ . '/..' . '/web-auth/cose-lib/src/Algorithm/Signature/ECDSA/ECSignature.php', + 'Cose\\Algorithm\\Signature\\ECDSA\\ES256' => __DIR__ . '/..' . '/web-auth/cose-lib/src/Algorithm/Signature/ECDSA/ES256.php', + 'Cose\\Algorithm\\Signature\\ECDSA\\ES256K' => __DIR__ . '/..' . '/web-auth/cose-lib/src/Algorithm/Signature/ECDSA/ES256K.php', + 'Cose\\Algorithm\\Signature\\ECDSA\\ES384' => __DIR__ . '/..' . '/web-auth/cose-lib/src/Algorithm/Signature/ECDSA/ES384.php', + 'Cose\\Algorithm\\Signature\\ECDSA\\ES512' => __DIR__ . '/..' . '/web-auth/cose-lib/src/Algorithm/Signature/ECDSA/ES512.php', + 'Cose\\Algorithm\\Signature\\EdDSA\\Ed25519' => __DIR__ . '/..' . '/web-auth/cose-lib/src/Algorithm/Signature/EdDSA/Ed25519.php', + 'Cose\\Algorithm\\Signature\\EdDSA\\Ed256' => __DIR__ . '/..' . '/web-auth/cose-lib/src/Algorithm/Signature/EdDSA/Ed256.php', + 'Cose\\Algorithm\\Signature\\EdDSA\\Ed512' => __DIR__ . '/..' . '/web-auth/cose-lib/src/Algorithm/Signature/EdDSA/Ed512.php', + 'Cose\\Algorithm\\Signature\\EdDSA\\EdDSA' => __DIR__ . '/..' . '/web-auth/cose-lib/src/Algorithm/Signature/EdDSA/EdDSA.php', + 'Cose\\Algorithm\\Signature\\RSA\\PS256' => __DIR__ . '/..' . '/web-auth/cose-lib/src/Algorithm/Signature/RSA/PS256.php', + 'Cose\\Algorithm\\Signature\\RSA\\PS384' => __DIR__ . '/..' . '/web-auth/cose-lib/src/Algorithm/Signature/RSA/PS384.php', + 'Cose\\Algorithm\\Signature\\RSA\\PS512' => __DIR__ . '/..' . '/web-auth/cose-lib/src/Algorithm/Signature/RSA/PS512.php', + 'Cose\\Algorithm\\Signature\\RSA\\PSSRSA' => __DIR__ . '/..' . '/web-auth/cose-lib/src/Algorithm/Signature/RSA/PSSRSA.php', + 'Cose\\Algorithm\\Signature\\RSA\\RS1' => __DIR__ . '/..' . '/web-auth/cose-lib/src/Algorithm/Signature/RSA/RS1.php', + 'Cose\\Algorithm\\Signature\\RSA\\RS256' => __DIR__ . '/..' . '/web-auth/cose-lib/src/Algorithm/Signature/RSA/RS256.php', + 'Cose\\Algorithm\\Signature\\RSA\\RS384' => __DIR__ . '/..' . '/web-auth/cose-lib/src/Algorithm/Signature/RSA/RS384.php', + 'Cose\\Algorithm\\Signature\\RSA\\RS512' => __DIR__ . '/..' . '/web-auth/cose-lib/src/Algorithm/Signature/RSA/RS512.php', + 'Cose\\Algorithm\\Signature\\RSA\\RSA' => __DIR__ . '/..' . '/web-auth/cose-lib/src/Algorithm/Signature/RSA/RSA.php', + 'Cose\\Algorithm\\Signature\\Signature' => __DIR__ . '/..' . '/web-auth/cose-lib/src/Algorithm/Signature/Signature.php', + 'Cose\\Algorithms' => __DIR__ . '/..' . '/web-auth/cose-lib/src/Algorithms.php', + 'Cose\\BigInteger' => __DIR__ . '/..' . '/web-auth/cose-lib/src/BigInteger.php', + 'Cose\\Hash' => __DIR__ . '/..' . '/web-auth/cose-lib/src/Hash.php', + 'Cose\\Key\\Ec2Key' => __DIR__ . '/..' . '/web-auth/cose-lib/src/Key/Ec2Key.php', + 'Cose\\Key\\Key' => __DIR__ . '/..' . '/web-auth/cose-lib/src/Key/Key.php', + 'Cose\\Key\\OkpKey' => __DIR__ . '/..' . '/web-auth/cose-lib/src/Key/OkpKey.php', + 'Cose\\Key\\RsaKey' => __DIR__ . '/..' . '/web-auth/cose-lib/src/Key/RsaKey.php', + 'Cose\\Key\\SymmetricKey' => __DIR__ . '/..' . '/web-auth/cose-lib/src/Key/SymmetricKey.php', + 'DateError' => __DIR__ . '/..' . '/symfony/polyfill-php83/Resources/stubs/DateError.php', + 'DateException' => __DIR__ . '/..' . '/symfony/polyfill-php83/Resources/stubs/DateException.php', + 'DateInvalidOperationException' => __DIR__ . '/..' . '/symfony/polyfill-php83/Resources/stubs/DateInvalidOperationException.php', + 'DateInvalidTimeZoneException' => __DIR__ . '/..' . '/symfony/polyfill-php83/Resources/stubs/DateInvalidTimeZoneException.php', + 'DateMalformedIntervalStringException' => __DIR__ . '/..' . '/symfony/polyfill-php83/Resources/stubs/DateMalformedIntervalStringException.php', + 'DateMalformedPeriodStringException' => __DIR__ . '/..' . '/symfony/polyfill-php83/Resources/stubs/DateMalformedPeriodStringException.php', + 'DateMalformedStringException' => __DIR__ . '/..' . '/symfony/polyfill-php83/Resources/stubs/DateMalformedStringException.php', + 'DateObjectError' => __DIR__ . '/..' . '/symfony/polyfill-php83/Resources/stubs/DateObjectError.php', + 'DateRangeError' => __DIR__ . '/..' . '/symfony/polyfill-php83/Resources/stubs/DateRangeError.php', + 'Deprecated' => __DIR__ . '/..' . '/symfony/polyfill-php84/Resources/stubs/Deprecated.php', + 'Doctrine\\Common\\EventArgs' => __DIR__ . '/..' . '/doctrine/event-manager/src/EventArgs.php', + 'Doctrine\\Common\\EventManager' => __DIR__ . '/..' . '/doctrine/event-manager/src/EventManager.php', + 'Doctrine\\Common\\EventSubscriber' => __DIR__ . '/..' . '/doctrine/event-manager/src/EventSubscriber.php', + 'Doctrine\\Common\\Lexer\\AbstractLexer' => __DIR__ . '/..' . '/doctrine/lexer/src/AbstractLexer.php', + 'Doctrine\\Common\\Lexer\\Token' => __DIR__ . '/..' . '/doctrine/lexer/src/Token.php', + 'Doctrine\\DBAL\\ArrayParameterType' => __DIR__ . '/..' . '/doctrine/dbal/src/ArrayParameterType.php', + 'Doctrine\\DBAL\\ArrayParameters\\Exception' => __DIR__ . '/..' . '/doctrine/dbal/src/ArrayParameters/Exception.php', + 'Doctrine\\DBAL\\ArrayParameters\\Exception\\MissingNamedParameter' => __DIR__ . '/..' . '/doctrine/dbal/src/ArrayParameters/Exception/MissingNamedParameter.php', + 'Doctrine\\DBAL\\ArrayParameters\\Exception\\MissingPositionalParameter' => __DIR__ . '/..' . '/doctrine/dbal/src/ArrayParameters/Exception/MissingPositionalParameter.php', + 'Doctrine\\DBAL\\Cache\\ArrayResult' => __DIR__ . '/..' . '/doctrine/dbal/src/Cache/ArrayResult.php', + 'Doctrine\\DBAL\\Cache\\CacheException' => __DIR__ . '/..' . '/doctrine/dbal/src/Cache/CacheException.php', + 'Doctrine\\DBAL\\Cache\\QueryCacheProfile' => __DIR__ . '/..' . '/doctrine/dbal/src/Cache/QueryCacheProfile.php', + 'Doctrine\\DBAL\\ColumnCase' => __DIR__ . '/..' . '/doctrine/dbal/src/ColumnCase.php', + 'Doctrine\\DBAL\\Configuration' => __DIR__ . '/..' . '/doctrine/dbal/src/Configuration.php', + 'Doctrine\\DBAL\\Connection' => __DIR__ . '/..' . '/doctrine/dbal/src/Connection.php', + 'Doctrine\\DBAL\\ConnectionException' => __DIR__ . '/..' . '/doctrine/dbal/src/ConnectionException.php', + 'Doctrine\\DBAL\\Connections\\PrimaryReadReplicaConnection' => __DIR__ . '/..' . '/doctrine/dbal/src/Connections/PrimaryReadReplicaConnection.php', + 'Doctrine\\DBAL\\Driver' => __DIR__ . '/..' . '/doctrine/dbal/src/Driver.php', + 'Doctrine\\DBAL\\DriverManager' => __DIR__ . '/..' . '/doctrine/dbal/src/DriverManager.php', + 'Doctrine\\DBAL\\Driver\\API\\ExceptionConverter' => __DIR__ . '/..' . '/doctrine/dbal/src/Driver/API/ExceptionConverter.php', + 'Doctrine\\DBAL\\Driver\\API\\IBMDB2\\ExceptionConverter' => __DIR__ . '/..' . '/doctrine/dbal/src/Driver/API/IBMDB2/ExceptionConverter.php', + 'Doctrine\\DBAL\\Driver\\API\\MySQL\\ExceptionConverter' => __DIR__ . '/..' . '/doctrine/dbal/src/Driver/API/MySQL/ExceptionConverter.php', + 'Doctrine\\DBAL\\Driver\\API\\OCI\\ExceptionConverter' => __DIR__ . '/..' . '/doctrine/dbal/src/Driver/API/OCI/ExceptionConverter.php', + 'Doctrine\\DBAL\\Driver\\API\\PostgreSQL\\ExceptionConverter' => __DIR__ . '/..' . '/doctrine/dbal/src/Driver/API/PostgreSQL/ExceptionConverter.php', + 'Doctrine\\DBAL\\Driver\\API\\SQLSrv\\ExceptionConverter' => __DIR__ . '/..' . '/doctrine/dbal/src/Driver/API/SQLSrv/ExceptionConverter.php', + 'Doctrine\\DBAL\\Driver\\API\\SQLite\\ExceptionConverter' => __DIR__ . '/..' . '/doctrine/dbal/src/Driver/API/SQLite/ExceptionConverter.php', + 'Doctrine\\DBAL\\Driver\\API\\SQLite\\UserDefinedFunctions' => __DIR__ . '/..' . '/doctrine/dbal/src/Driver/API/SQLite/UserDefinedFunctions.php', + 'Doctrine\\DBAL\\Driver\\AbstractDB2Driver' => __DIR__ . '/..' . '/doctrine/dbal/src/Driver/AbstractDB2Driver.php', + 'Doctrine\\DBAL\\Driver\\AbstractException' => __DIR__ . '/..' . '/doctrine/dbal/src/Driver/AbstractException.php', + 'Doctrine\\DBAL\\Driver\\AbstractMySQLDriver' => __DIR__ . '/..' . '/doctrine/dbal/src/Driver/AbstractMySQLDriver.php', + 'Doctrine\\DBAL\\Driver\\AbstractOracleDriver' => __DIR__ . '/..' . '/doctrine/dbal/src/Driver/AbstractOracleDriver.php', + 'Doctrine\\DBAL\\Driver\\AbstractOracleDriver\\EasyConnectString' => __DIR__ . '/..' . '/doctrine/dbal/src/Driver/AbstractOracleDriver/EasyConnectString.php', + 'Doctrine\\DBAL\\Driver\\AbstractPostgreSQLDriver' => __DIR__ . '/..' . '/doctrine/dbal/src/Driver/AbstractPostgreSQLDriver.php', + 'Doctrine\\DBAL\\Driver\\AbstractSQLServerDriver' => __DIR__ . '/..' . '/doctrine/dbal/src/Driver/AbstractSQLServerDriver.php', + 'Doctrine\\DBAL\\Driver\\AbstractSQLServerDriver\\Exception\\PortWithoutHost' => __DIR__ . '/..' . '/doctrine/dbal/src/Driver/AbstractSQLServerDriver/Exception/PortWithoutHost.php', + 'Doctrine\\DBAL\\Driver\\AbstractSQLiteDriver' => __DIR__ . '/..' . '/doctrine/dbal/src/Driver/AbstractSQLiteDriver.php', + 'Doctrine\\DBAL\\Driver\\AbstractSQLiteDriver\\Middleware\\EnableForeignKeys' => __DIR__ . '/..' . '/doctrine/dbal/src/Driver/AbstractSQLiteDriver/Middleware/EnableForeignKeys.php', + 'Doctrine\\DBAL\\Driver\\Connection' => __DIR__ . '/..' . '/doctrine/dbal/src/Driver/Connection.php', + 'Doctrine\\DBAL\\Driver\\Exception' => __DIR__ . '/..' . '/doctrine/dbal/src/Driver/Exception.php', + 'Doctrine\\DBAL\\Driver\\Exception\\UnknownParameterType' => __DIR__ . '/..' . '/doctrine/dbal/src/Driver/Exception/UnknownParameterType.php', + 'Doctrine\\DBAL\\Driver\\FetchUtils' => __DIR__ . '/..' . '/doctrine/dbal/src/Driver/FetchUtils.php', + 'Doctrine\\DBAL\\Driver\\IBMDB2\\Connection' => __DIR__ . '/..' . '/doctrine/dbal/src/Driver/IBMDB2/Connection.php', + 'Doctrine\\DBAL\\Driver\\IBMDB2\\DataSourceName' => __DIR__ . '/..' . '/doctrine/dbal/src/Driver/IBMDB2/DataSourceName.php', + 'Doctrine\\DBAL\\Driver\\IBMDB2\\Driver' => __DIR__ . '/..' . '/doctrine/dbal/src/Driver/IBMDB2/Driver.php', + 'Doctrine\\DBAL\\Driver\\IBMDB2\\Exception\\CannotCopyStreamToStream' => __DIR__ . '/..' . '/doctrine/dbal/src/Driver/IBMDB2/Exception/CannotCopyStreamToStream.php', + 'Doctrine\\DBAL\\Driver\\IBMDB2\\Exception\\CannotCreateTemporaryFile' => __DIR__ . '/..' . '/doctrine/dbal/src/Driver/IBMDB2/Exception/CannotCreateTemporaryFile.php', + 'Doctrine\\DBAL\\Driver\\IBMDB2\\Exception\\ConnectionError' => __DIR__ . '/..' . '/doctrine/dbal/src/Driver/IBMDB2/Exception/ConnectionError.php', + 'Doctrine\\DBAL\\Driver\\IBMDB2\\Exception\\ConnectionFailed' => __DIR__ . '/..' . '/doctrine/dbal/src/Driver/IBMDB2/Exception/ConnectionFailed.php', + 'Doctrine\\DBAL\\Driver\\IBMDB2\\Exception\\Factory' => __DIR__ . '/..' . '/doctrine/dbal/src/Driver/IBMDB2/Exception/Factory.php', + 'Doctrine\\DBAL\\Driver\\IBMDB2\\Exception\\PrepareFailed' => __DIR__ . '/..' . '/doctrine/dbal/src/Driver/IBMDB2/Exception/PrepareFailed.php', + 'Doctrine\\DBAL\\Driver\\IBMDB2\\Exception\\StatementError' => __DIR__ . '/..' . '/doctrine/dbal/src/Driver/IBMDB2/Exception/StatementError.php', + 'Doctrine\\DBAL\\Driver\\IBMDB2\\Result' => __DIR__ . '/..' . '/doctrine/dbal/src/Driver/IBMDB2/Result.php', + 'Doctrine\\DBAL\\Driver\\IBMDB2\\Statement' => __DIR__ . '/..' . '/doctrine/dbal/src/Driver/IBMDB2/Statement.php', + 'Doctrine\\DBAL\\Driver\\Middleware' => __DIR__ . '/..' . '/doctrine/dbal/src/Driver/Middleware.php', + 'Doctrine\\DBAL\\Driver\\Middleware\\AbstractConnectionMiddleware' => __DIR__ . '/..' . '/doctrine/dbal/src/Driver/Middleware/AbstractConnectionMiddleware.php', + 'Doctrine\\DBAL\\Driver\\Middleware\\AbstractDriverMiddleware' => __DIR__ . '/..' . '/doctrine/dbal/src/Driver/Middleware/AbstractDriverMiddleware.php', + 'Doctrine\\DBAL\\Driver\\Middleware\\AbstractResultMiddleware' => __DIR__ . '/..' . '/doctrine/dbal/src/Driver/Middleware/AbstractResultMiddleware.php', + 'Doctrine\\DBAL\\Driver\\Middleware\\AbstractStatementMiddleware' => __DIR__ . '/..' . '/doctrine/dbal/src/Driver/Middleware/AbstractStatementMiddleware.php', + 'Doctrine\\DBAL\\Driver\\Mysqli\\Connection' => __DIR__ . '/..' . '/doctrine/dbal/src/Driver/Mysqli/Connection.php', + 'Doctrine\\DBAL\\Driver\\Mysqli\\Driver' => __DIR__ . '/..' . '/doctrine/dbal/src/Driver/Mysqli/Driver.php', + 'Doctrine\\DBAL\\Driver\\Mysqli\\Exception\\ConnectionError' => __DIR__ . '/..' . '/doctrine/dbal/src/Driver/Mysqli/Exception/ConnectionError.php', + 'Doctrine\\DBAL\\Driver\\Mysqli\\Exception\\ConnectionFailed' => __DIR__ . '/..' . '/doctrine/dbal/src/Driver/Mysqli/Exception/ConnectionFailed.php', + 'Doctrine\\DBAL\\Driver\\Mysqli\\Exception\\FailedReadingStreamOffset' => __DIR__ . '/..' . '/doctrine/dbal/src/Driver/Mysqli/Exception/FailedReadingStreamOffset.php', + 'Doctrine\\DBAL\\Driver\\Mysqli\\Exception\\HostRequired' => __DIR__ . '/..' . '/doctrine/dbal/src/Driver/Mysqli/Exception/HostRequired.php', + 'Doctrine\\DBAL\\Driver\\Mysqli\\Exception\\InvalidCharset' => __DIR__ . '/..' . '/doctrine/dbal/src/Driver/Mysqli/Exception/InvalidCharset.php', + 'Doctrine\\DBAL\\Driver\\Mysqli\\Exception\\InvalidOption' => __DIR__ . '/..' . '/doctrine/dbal/src/Driver/Mysqli/Exception/InvalidOption.php', + 'Doctrine\\DBAL\\Driver\\Mysqli\\Exception\\NonStreamResourceUsedAsLargeObject' => __DIR__ . '/..' . '/doctrine/dbal/src/Driver/Mysqli/Exception/NonStreamResourceUsedAsLargeObject.php', + 'Doctrine\\DBAL\\Driver\\Mysqli\\Exception\\StatementError' => __DIR__ . '/..' . '/doctrine/dbal/src/Driver/Mysqli/Exception/StatementError.php', + 'Doctrine\\DBAL\\Driver\\Mysqli\\Initializer' => __DIR__ . '/..' . '/doctrine/dbal/src/Driver/Mysqli/Initializer.php', + 'Doctrine\\DBAL\\Driver\\Mysqli\\Initializer\\Charset' => __DIR__ . '/..' . '/doctrine/dbal/src/Driver/Mysqli/Initializer/Charset.php', + 'Doctrine\\DBAL\\Driver\\Mysqli\\Initializer\\Options' => __DIR__ . '/..' . '/doctrine/dbal/src/Driver/Mysqli/Initializer/Options.php', + 'Doctrine\\DBAL\\Driver\\Mysqli\\Initializer\\Secure' => __DIR__ . '/..' . '/doctrine/dbal/src/Driver/Mysqli/Initializer/Secure.php', + 'Doctrine\\DBAL\\Driver\\Mysqli\\Result' => __DIR__ . '/..' . '/doctrine/dbal/src/Driver/Mysqli/Result.php', + 'Doctrine\\DBAL\\Driver\\Mysqli\\Statement' => __DIR__ . '/..' . '/doctrine/dbal/src/Driver/Mysqli/Statement.php', + 'Doctrine\\DBAL\\Driver\\OCI8\\Connection' => __DIR__ . '/..' . '/doctrine/dbal/src/Driver/OCI8/Connection.php', + 'Doctrine\\DBAL\\Driver\\OCI8\\ConvertPositionalToNamedPlaceholders' => __DIR__ . '/..' . '/doctrine/dbal/src/Driver/OCI8/ConvertPositionalToNamedPlaceholders.php', + 'Doctrine\\DBAL\\Driver\\OCI8\\Driver' => __DIR__ . '/..' . '/doctrine/dbal/src/Driver/OCI8/Driver.php', + 'Doctrine\\DBAL\\Driver\\OCI8\\Exception\\ConnectionFailed' => __DIR__ . '/..' . '/doctrine/dbal/src/Driver/OCI8/Exception/ConnectionFailed.php', + 'Doctrine\\DBAL\\Driver\\OCI8\\Exception\\Error' => __DIR__ . '/..' . '/doctrine/dbal/src/Driver/OCI8/Exception/Error.php', + 'Doctrine\\DBAL\\Driver\\OCI8\\Exception\\InvalidConfiguration' => __DIR__ . '/..' . '/doctrine/dbal/src/Driver/OCI8/Exception/InvalidConfiguration.php', + 'Doctrine\\DBAL\\Driver\\OCI8\\Exception\\NonTerminatedStringLiteral' => __DIR__ . '/..' . '/doctrine/dbal/src/Driver/OCI8/Exception/NonTerminatedStringLiteral.php', + 'Doctrine\\DBAL\\Driver\\OCI8\\Exception\\SequenceDoesNotExist' => __DIR__ . '/..' . '/doctrine/dbal/src/Driver/OCI8/Exception/SequenceDoesNotExist.php', + 'Doctrine\\DBAL\\Driver\\OCI8\\Exception\\UnknownParameterIndex' => __DIR__ . '/..' . '/doctrine/dbal/src/Driver/OCI8/Exception/UnknownParameterIndex.php', + 'Doctrine\\DBAL\\Driver\\OCI8\\ExecutionMode' => __DIR__ . '/..' . '/doctrine/dbal/src/Driver/OCI8/ExecutionMode.php', + 'Doctrine\\DBAL\\Driver\\OCI8\\Middleware\\InitializeSession' => __DIR__ . '/..' . '/doctrine/dbal/src/Driver/OCI8/Middleware/InitializeSession.php', + 'Doctrine\\DBAL\\Driver\\OCI8\\Result' => __DIR__ . '/..' . '/doctrine/dbal/src/Driver/OCI8/Result.php', + 'Doctrine\\DBAL\\Driver\\OCI8\\Statement' => __DIR__ . '/..' . '/doctrine/dbal/src/Driver/OCI8/Statement.php', + 'Doctrine\\DBAL\\Driver\\PDO\\Connection' => __DIR__ . '/..' . '/doctrine/dbal/src/Driver/PDO/Connection.php', + 'Doctrine\\DBAL\\Driver\\PDO\\Exception' => __DIR__ . '/..' . '/doctrine/dbal/src/Driver/PDO/Exception.php', + 'Doctrine\\DBAL\\Driver\\PDO\\MySQL\\Driver' => __DIR__ . '/..' . '/doctrine/dbal/src/Driver/PDO/MySQL/Driver.php', + 'Doctrine\\DBAL\\Driver\\PDO\\OCI\\Driver' => __DIR__ . '/..' . '/doctrine/dbal/src/Driver/PDO/OCI/Driver.php', + 'Doctrine\\DBAL\\Driver\\PDO\\PDOConnect' => __DIR__ . '/..' . '/doctrine/dbal/src/Driver/PDO/PDOConnect.php', + 'Doctrine\\DBAL\\Driver\\PDO\\PDOException' => __DIR__ . '/..' . '/doctrine/dbal/src/Driver/PDO/PDOException.php', + 'Doctrine\\DBAL\\Driver\\PDO\\ParameterTypeMap' => __DIR__ . '/..' . '/doctrine/dbal/src/Driver/PDO/ParameterTypeMap.php', + 'Doctrine\\DBAL\\Driver\\PDO\\PgSQL\\Driver' => __DIR__ . '/..' . '/doctrine/dbal/src/Driver/PDO/PgSQL/Driver.php', + 'Doctrine\\DBAL\\Driver\\PDO\\Result' => __DIR__ . '/..' . '/doctrine/dbal/src/Driver/PDO/Result.php', + 'Doctrine\\DBAL\\Driver\\PDO\\SQLSrv\\Connection' => __DIR__ . '/..' . '/doctrine/dbal/src/Driver/PDO/SQLSrv/Connection.php', + 'Doctrine\\DBAL\\Driver\\PDO\\SQLSrv\\Driver' => __DIR__ . '/..' . '/doctrine/dbal/src/Driver/PDO/SQLSrv/Driver.php', + 'Doctrine\\DBAL\\Driver\\PDO\\SQLSrv\\Statement' => __DIR__ . '/..' . '/doctrine/dbal/src/Driver/PDO/SQLSrv/Statement.php', + 'Doctrine\\DBAL\\Driver\\PDO\\SQLite\\Driver' => __DIR__ . '/..' . '/doctrine/dbal/src/Driver/PDO/SQLite/Driver.php', + 'Doctrine\\DBAL\\Driver\\PDO\\Statement' => __DIR__ . '/..' . '/doctrine/dbal/src/Driver/PDO/Statement.php', + 'Doctrine\\DBAL\\Driver\\PgSQL\\Connection' => __DIR__ . '/..' . '/doctrine/dbal/src/Driver/PgSQL/Connection.php', + 'Doctrine\\DBAL\\Driver\\PgSQL\\ConvertParameters' => __DIR__ . '/..' . '/doctrine/dbal/src/Driver/PgSQL/ConvertParameters.php', + 'Doctrine\\DBAL\\Driver\\PgSQL\\Driver' => __DIR__ . '/..' . '/doctrine/dbal/src/Driver/PgSQL/Driver.php', + 'Doctrine\\DBAL\\Driver\\PgSQL\\Exception' => __DIR__ . '/..' . '/doctrine/dbal/src/Driver/PgSQL/Exception.php', + 'Doctrine\\DBAL\\Driver\\PgSQL\\Exception\\UnexpectedValue' => __DIR__ . '/..' . '/doctrine/dbal/src/Driver/PgSQL/Exception/UnexpectedValue.php', + 'Doctrine\\DBAL\\Driver\\PgSQL\\Exception\\UnknownParameter' => __DIR__ . '/..' . '/doctrine/dbal/src/Driver/PgSQL/Exception/UnknownParameter.php', + 'Doctrine\\DBAL\\Driver\\PgSQL\\Result' => __DIR__ . '/..' . '/doctrine/dbal/src/Driver/PgSQL/Result.php', + 'Doctrine\\DBAL\\Driver\\PgSQL\\Statement' => __DIR__ . '/..' . '/doctrine/dbal/src/Driver/PgSQL/Statement.php', + 'Doctrine\\DBAL\\Driver\\Result' => __DIR__ . '/..' . '/doctrine/dbal/src/Driver/Result.php', + 'Doctrine\\DBAL\\Driver\\SQLSrv\\Connection' => __DIR__ . '/..' . '/doctrine/dbal/src/Driver/SQLSrv/Connection.php', + 'Doctrine\\DBAL\\Driver\\SQLSrv\\Driver' => __DIR__ . '/..' . '/doctrine/dbal/src/Driver/SQLSrv/Driver.php', + 'Doctrine\\DBAL\\Driver\\SQLSrv\\Exception\\Error' => __DIR__ . '/..' . '/doctrine/dbal/src/Driver/SQLSrv/Exception/Error.php', + 'Doctrine\\DBAL\\Driver\\SQLSrv\\Result' => __DIR__ . '/..' . '/doctrine/dbal/src/Driver/SQLSrv/Result.php', + 'Doctrine\\DBAL\\Driver\\SQLSrv\\Statement' => __DIR__ . '/..' . '/doctrine/dbal/src/Driver/SQLSrv/Statement.php', + 'Doctrine\\DBAL\\Driver\\SQLite3\\Connection' => __DIR__ . '/..' . '/doctrine/dbal/src/Driver/SQLite3/Connection.php', + 'Doctrine\\DBAL\\Driver\\SQLite3\\Driver' => __DIR__ . '/..' . '/doctrine/dbal/src/Driver/SQLite3/Driver.php', + 'Doctrine\\DBAL\\Driver\\SQLite3\\Exception' => __DIR__ . '/..' . '/doctrine/dbal/src/Driver/SQLite3/Exception.php', + 'Doctrine\\DBAL\\Driver\\SQLite3\\Result' => __DIR__ . '/..' . '/doctrine/dbal/src/Driver/SQLite3/Result.php', + 'Doctrine\\DBAL\\Driver\\SQLite3\\Statement' => __DIR__ . '/..' . '/doctrine/dbal/src/Driver/SQLite3/Statement.php', + 'Doctrine\\DBAL\\Driver\\ServerInfoAwareConnection' => __DIR__ . '/..' . '/doctrine/dbal/src/Driver/ServerInfoAwareConnection.php', + 'Doctrine\\DBAL\\Driver\\Statement' => __DIR__ . '/..' . '/doctrine/dbal/src/Driver/Statement.php', + 'Doctrine\\DBAL\\Event\\ConnectionEventArgs' => __DIR__ . '/..' . '/doctrine/dbal/src/Event/ConnectionEventArgs.php', + 'Doctrine\\DBAL\\Event\\Listeners\\OracleSessionInit' => __DIR__ . '/..' . '/doctrine/dbal/src/Event/Listeners/OracleSessionInit.php', + 'Doctrine\\DBAL\\Event\\Listeners\\SQLSessionInit' => __DIR__ . '/..' . '/doctrine/dbal/src/Event/Listeners/SQLSessionInit.php', + 'Doctrine\\DBAL\\Event\\Listeners\\SQLiteSessionInit' => __DIR__ . '/..' . '/doctrine/dbal/src/Event/Listeners/SQLiteSessionInit.php', + 'Doctrine\\DBAL\\Event\\SchemaAlterTableAddColumnEventArgs' => __DIR__ . '/..' . '/doctrine/dbal/src/Event/SchemaAlterTableAddColumnEventArgs.php', + 'Doctrine\\DBAL\\Event\\SchemaAlterTableChangeColumnEventArgs' => __DIR__ . '/..' . '/doctrine/dbal/src/Event/SchemaAlterTableChangeColumnEventArgs.php', + 'Doctrine\\DBAL\\Event\\SchemaAlterTableEventArgs' => __DIR__ . '/..' . '/doctrine/dbal/src/Event/SchemaAlterTableEventArgs.php', + 'Doctrine\\DBAL\\Event\\SchemaAlterTableRemoveColumnEventArgs' => __DIR__ . '/..' . '/doctrine/dbal/src/Event/SchemaAlterTableRemoveColumnEventArgs.php', + 'Doctrine\\DBAL\\Event\\SchemaAlterTableRenameColumnEventArgs' => __DIR__ . '/..' . '/doctrine/dbal/src/Event/SchemaAlterTableRenameColumnEventArgs.php', + 'Doctrine\\DBAL\\Event\\SchemaColumnDefinitionEventArgs' => __DIR__ . '/..' . '/doctrine/dbal/src/Event/SchemaColumnDefinitionEventArgs.php', + 'Doctrine\\DBAL\\Event\\SchemaCreateTableColumnEventArgs' => __DIR__ . '/..' . '/doctrine/dbal/src/Event/SchemaCreateTableColumnEventArgs.php', + 'Doctrine\\DBAL\\Event\\SchemaCreateTableEventArgs' => __DIR__ . '/..' . '/doctrine/dbal/src/Event/SchemaCreateTableEventArgs.php', + 'Doctrine\\DBAL\\Event\\SchemaDropTableEventArgs' => __DIR__ . '/..' . '/doctrine/dbal/src/Event/SchemaDropTableEventArgs.php', + 'Doctrine\\DBAL\\Event\\SchemaEventArgs' => __DIR__ . '/..' . '/doctrine/dbal/src/Event/SchemaEventArgs.php', + 'Doctrine\\DBAL\\Event\\SchemaIndexDefinitionEventArgs' => __DIR__ . '/..' . '/doctrine/dbal/src/Event/SchemaIndexDefinitionEventArgs.php', + 'Doctrine\\DBAL\\Event\\TransactionBeginEventArgs' => __DIR__ . '/..' . '/doctrine/dbal/src/Event/TransactionBeginEventArgs.php', + 'Doctrine\\DBAL\\Event\\TransactionCommitEventArgs' => __DIR__ . '/..' . '/doctrine/dbal/src/Event/TransactionCommitEventArgs.php', + 'Doctrine\\DBAL\\Event\\TransactionEventArgs' => __DIR__ . '/..' . '/doctrine/dbal/src/Event/TransactionEventArgs.php', + 'Doctrine\\DBAL\\Event\\TransactionRollBackEventArgs' => __DIR__ . '/..' . '/doctrine/dbal/src/Event/TransactionRollBackEventArgs.php', + 'Doctrine\\DBAL\\Events' => __DIR__ . '/..' . '/doctrine/dbal/src/Events.php', + 'Doctrine\\DBAL\\Exception' => __DIR__ . '/..' . '/doctrine/dbal/src/Exception.php', + 'Doctrine\\DBAL\\Exception\\ConnectionException' => __DIR__ . '/..' . '/doctrine/dbal/src/Exception/ConnectionException.php', + 'Doctrine\\DBAL\\Exception\\ConnectionLost' => __DIR__ . '/..' . '/doctrine/dbal/src/Exception/ConnectionLost.php', + 'Doctrine\\DBAL\\Exception\\ConstraintViolationException' => __DIR__ . '/..' . '/doctrine/dbal/src/Exception/ConstraintViolationException.php', + 'Doctrine\\DBAL\\Exception\\DatabaseDoesNotExist' => __DIR__ . '/..' . '/doctrine/dbal/src/Exception/DatabaseDoesNotExist.php', + 'Doctrine\\DBAL\\Exception\\DatabaseObjectExistsException' => __DIR__ . '/..' . '/doctrine/dbal/src/Exception/DatabaseObjectExistsException.php', + 'Doctrine\\DBAL\\Exception\\DatabaseObjectNotFoundException' => __DIR__ . '/..' . '/doctrine/dbal/src/Exception/DatabaseObjectNotFoundException.php', + 'Doctrine\\DBAL\\Exception\\DatabaseRequired' => __DIR__ . '/..' . '/doctrine/dbal/src/Exception/DatabaseRequired.php', + 'Doctrine\\DBAL\\Exception\\DeadlockException' => __DIR__ . '/..' . '/doctrine/dbal/src/Exception/DeadlockException.php', + 'Doctrine\\DBAL\\Exception\\DriverException' => __DIR__ . '/..' . '/doctrine/dbal/src/Exception/DriverException.php', + 'Doctrine\\DBAL\\Exception\\ForeignKeyConstraintViolationException' => __DIR__ . '/..' . '/doctrine/dbal/src/Exception/ForeignKeyConstraintViolationException.php', + 'Doctrine\\DBAL\\Exception\\InvalidArgumentException' => __DIR__ . '/..' . '/doctrine/dbal/src/Exception/InvalidArgumentException.php', + 'Doctrine\\DBAL\\Exception\\InvalidFieldNameException' => __DIR__ . '/..' . '/doctrine/dbal/src/Exception/InvalidFieldNameException.php', + 'Doctrine\\DBAL\\Exception\\InvalidLockMode' => __DIR__ . '/..' . '/doctrine/dbal/src/Exception/InvalidLockMode.php', + 'Doctrine\\DBAL\\Exception\\LockWaitTimeoutException' => __DIR__ . '/..' . '/doctrine/dbal/src/Exception/LockWaitTimeoutException.php', + 'Doctrine\\DBAL\\Exception\\MalformedDsnException' => __DIR__ . '/..' . '/doctrine/dbal/src/Exception/MalformedDsnException.php', + 'Doctrine\\DBAL\\Exception\\NoKeyValue' => __DIR__ . '/..' . '/doctrine/dbal/src/Exception/NoKeyValue.php', + 'Doctrine\\DBAL\\Exception\\NonUniqueFieldNameException' => __DIR__ . '/..' . '/doctrine/dbal/src/Exception/NonUniqueFieldNameException.php', + 'Doctrine\\DBAL\\Exception\\NotNullConstraintViolationException' => __DIR__ . '/..' . '/doctrine/dbal/src/Exception/NotNullConstraintViolationException.php', + 'Doctrine\\DBAL\\Exception\\ReadOnlyException' => __DIR__ . '/..' . '/doctrine/dbal/src/Exception/ReadOnlyException.php', + 'Doctrine\\DBAL\\Exception\\RetryableException' => __DIR__ . '/..' . '/doctrine/dbal/src/Exception/RetryableException.php', + 'Doctrine\\DBAL\\Exception\\SchemaDoesNotExist' => __DIR__ . '/..' . '/doctrine/dbal/src/Exception/SchemaDoesNotExist.php', + 'Doctrine\\DBAL\\Exception\\ServerException' => __DIR__ . '/..' . '/doctrine/dbal/src/Exception/ServerException.php', + 'Doctrine\\DBAL\\Exception\\SyntaxErrorException' => __DIR__ . '/..' . '/doctrine/dbal/src/Exception/SyntaxErrorException.php', + 'Doctrine\\DBAL\\Exception\\TableExistsException' => __DIR__ . '/..' . '/doctrine/dbal/src/Exception/TableExistsException.php', + 'Doctrine\\DBAL\\Exception\\TableNotFoundException' => __DIR__ . '/..' . '/doctrine/dbal/src/Exception/TableNotFoundException.php', + 'Doctrine\\DBAL\\Exception\\TransactionRolledBack' => __DIR__ . '/..' . '/doctrine/dbal/src/Exception/TransactionRolledBack.php', + 'Doctrine\\DBAL\\Exception\\UniqueConstraintViolationException' => __DIR__ . '/..' . '/doctrine/dbal/src/Exception/UniqueConstraintViolationException.php', + 'Doctrine\\DBAL\\ExpandArrayParameters' => __DIR__ . '/..' . '/doctrine/dbal/src/ExpandArrayParameters.php', + 'Doctrine\\DBAL\\FetchMode' => __DIR__ . '/..' . '/doctrine/dbal/src/FetchMode.php', + 'Doctrine\\DBAL\\Id\\TableGenerator' => __DIR__ . '/..' . '/doctrine/dbal/src/Id/TableGenerator.php', + 'Doctrine\\DBAL\\Id\\TableGeneratorSchemaVisitor' => __DIR__ . '/..' . '/doctrine/dbal/src/Id/TableGeneratorSchemaVisitor.php', + 'Doctrine\\DBAL\\LockMode' => __DIR__ . '/..' . '/doctrine/dbal/src/LockMode.php', + 'Doctrine\\DBAL\\Logging\\Connection' => __DIR__ . '/..' . '/doctrine/dbal/src/Logging/Connection.php', + 'Doctrine\\DBAL\\Logging\\DebugStack' => __DIR__ . '/..' . '/doctrine/dbal/src/Logging/DebugStack.php', + 'Doctrine\\DBAL\\Logging\\Driver' => __DIR__ . '/..' . '/doctrine/dbal/src/Logging/Driver.php', + 'Doctrine\\DBAL\\Logging\\LoggerChain' => __DIR__ . '/..' . '/doctrine/dbal/src/Logging/LoggerChain.php', + 'Doctrine\\DBAL\\Logging\\Middleware' => __DIR__ . '/..' . '/doctrine/dbal/src/Logging/Middleware.php', + 'Doctrine\\DBAL\\Logging\\SQLLogger' => __DIR__ . '/..' . '/doctrine/dbal/src/Logging/SQLLogger.php', + 'Doctrine\\DBAL\\Logging\\Statement' => __DIR__ . '/..' . '/doctrine/dbal/src/Logging/Statement.php', + 'Doctrine\\DBAL\\ParameterType' => __DIR__ . '/..' . '/doctrine/dbal/src/ParameterType.php', + 'Doctrine\\DBAL\\Platforms\\AbstractMySQLPlatform' => __DIR__ . '/..' . '/doctrine/dbal/src/Platforms/AbstractMySQLPlatform.php', + 'Doctrine\\DBAL\\Platforms\\AbstractPlatform' => __DIR__ . '/..' . '/doctrine/dbal/src/Platforms/AbstractPlatform.php', + 'Doctrine\\DBAL\\Platforms\\DB2111Platform' => __DIR__ . '/..' . '/doctrine/dbal/src/Platforms/DB2111Platform.php', + 'Doctrine\\DBAL\\Platforms\\DB2Platform' => __DIR__ . '/..' . '/doctrine/dbal/src/Platforms/DB2Platform.php', + 'Doctrine\\DBAL\\Platforms\\DateIntervalUnit' => __DIR__ . '/..' . '/doctrine/dbal/src/Platforms/DateIntervalUnit.php', + 'Doctrine\\DBAL\\Platforms\\Keywords\\DB2Keywords' => __DIR__ . '/..' . '/doctrine/dbal/src/Platforms/Keywords/DB2Keywords.php', + 'Doctrine\\DBAL\\Platforms\\Keywords\\KeywordList' => __DIR__ . '/..' . '/doctrine/dbal/src/Platforms/Keywords/KeywordList.php', + 'Doctrine\\DBAL\\Platforms\\Keywords\\MariaDBKeywords' => __DIR__ . '/..' . '/doctrine/dbal/src/Platforms/Keywords/MariaDBKeywords.php', + 'Doctrine\\DBAL\\Platforms\\Keywords\\MariaDb102Keywords' => __DIR__ . '/..' . '/doctrine/dbal/src/Platforms/Keywords/MariaDb102Keywords.php', + 'Doctrine\\DBAL\\Platforms\\Keywords\\MariaDb117Keywords' => __DIR__ . '/..' . '/doctrine/dbal/src/Platforms/Keywords/MariaDb117Keywords.php', + 'Doctrine\\DBAL\\Platforms\\Keywords\\MySQL57Keywords' => __DIR__ . '/..' . '/doctrine/dbal/src/Platforms/Keywords/MySQL57Keywords.php', + 'Doctrine\\DBAL\\Platforms\\Keywords\\MySQL80Keywords' => __DIR__ . '/..' . '/doctrine/dbal/src/Platforms/Keywords/MySQL80Keywords.php', + 'Doctrine\\DBAL\\Platforms\\Keywords\\MySQL84Keywords' => __DIR__ . '/..' . '/doctrine/dbal/src/Platforms/Keywords/MySQL84Keywords.php', + 'Doctrine\\DBAL\\Platforms\\Keywords\\MySQLKeywords' => __DIR__ . '/..' . '/doctrine/dbal/src/Platforms/Keywords/MySQLKeywords.php', + 'Doctrine\\DBAL\\Platforms\\Keywords\\OracleKeywords' => __DIR__ . '/..' . '/doctrine/dbal/src/Platforms/Keywords/OracleKeywords.php', + 'Doctrine\\DBAL\\Platforms\\Keywords\\PostgreSQL100Keywords' => __DIR__ . '/..' . '/doctrine/dbal/src/Platforms/Keywords/PostgreSQL100Keywords.php', + 'Doctrine\\DBAL\\Platforms\\Keywords\\PostgreSQL94Keywords' => __DIR__ . '/..' . '/doctrine/dbal/src/Platforms/Keywords/PostgreSQL94Keywords.php', + 'Doctrine\\DBAL\\Platforms\\Keywords\\PostgreSQLKeywords' => __DIR__ . '/..' . '/doctrine/dbal/src/Platforms/Keywords/PostgreSQLKeywords.php', + 'Doctrine\\DBAL\\Platforms\\Keywords\\ReservedKeywordsValidator' => __DIR__ . '/..' . '/doctrine/dbal/src/Platforms/Keywords/ReservedKeywordsValidator.php', + 'Doctrine\\DBAL\\Platforms\\Keywords\\SQLServer2012Keywords' => __DIR__ . '/..' . '/doctrine/dbal/src/Platforms/Keywords/SQLServer2012Keywords.php', + 'Doctrine\\DBAL\\Platforms\\Keywords\\SQLServerKeywords' => __DIR__ . '/..' . '/doctrine/dbal/src/Platforms/Keywords/SQLServerKeywords.php', + 'Doctrine\\DBAL\\Platforms\\Keywords\\SQLiteKeywords' => __DIR__ . '/..' . '/doctrine/dbal/src/Platforms/Keywords/SQLiteKeywords.php', + 'Doctrine\\DBAL\\Platforms\\MariaDBPlatform' => __DIR__ . '/..' . '/doctrine/dbal/src/Platforms/MariaDBPlatform.php', + 'Doctrine\\DBAL\\Platforms\\MariaDb1010Platform' => __DIR__ . '/..' . '/doctrine/dbal/src/Platforms/MariaDb1010Platform.php', + 'Doctrine\\DBAL\\Platforms\\MariaDb1027Platform' => __DIR__ . '/..' . '/doctrine/dbal/src/Platforms/MariaDb1027Platform.php', + 'Doctrine\\DBAL\\Platforms\\MariaDb1043Platform' => __DIR__ . '/..' . '/doctrine/dbal/src/Platforms/MariaDb1043Platform.php', + 'Doctrine\\DBAL\\Platforms\\MariaDb1052Platform' => __DIR__ . '/..' . '/doctrine/dbal/src/Platforms/MariaDb1052Platform.php', + 'Doctrine\\DBAL\\Platforms\\MariaDb1060Platform' => __DIR__ . '/..' . '/doctrine/dbal/src/Platforms/MariaDb1060Platform.php', + 'Doctrine\\DBAL\\Platforms\\MariaDb110700Platform' => __DIR__ . '/..' . '/doctrine/dbal/src/Platforms/MariaDb110700Platform.php', + 'Doctrine\\DBAL\\Platforms\\MySQL57Platform' => __DIR__ . '/..' . '/doctrine/dbal/src/Platforms/MySQL57Platform.php', + 'Doctrine\\DBAL\\Platforms\\MySQL80Platform' => __DIR__ . '/..' . '/doctrine/dbal/src/Platforms/MySQL80Platform.php', + 'Doctrine\\DBAL\\Platforms\\MySQL84Platform' => __DIR__ . '/..' . '/doctrine/dbal/src/Platforms/MySQL84Platform.php', + 'Doctrine\\DBAL\\Platforms\\MySQLPlatform' => __DIR__ . '/..' . '/doctrine/dbal/src/Platforms/MySQLPlatform.php', + 'Doctrine\\DBAL\\Platforms\\MySQL\\CollationMetadataProvider' => __DIR__ . '/..' . '/doctrine/dbal/src/Platforms/MySQL/CollationMetadataProvider.php', + 'Doctrine\\DBAL\\Platforms\\MySQL\\CollationMetadataProvider\\CachingCollationMetadataProvider' => __DIR__ . '/..' . '/doctrine/dbal/src/Platforms/MySQL/CollationMetadataProvider/CachingCollationMetadataProvider.php', + 'Doctrine\\DBAL\\Platforms\\MySQL\\CollationMetadataProvider\\ConnectionCollationMetadataProvider' => __DIR__ . '/..' . '/doctrine/dbal/src/Platforms/MySQL/CollationMetadataProvider/ConnectionCollationMetadataProvider.php', + 'Doctrine\\DBAL\\Platforms\\MySQL\\Comparator' => __DIR__ . '/..' . '/doctrine/dbal/src/Platforms/MySQL/Comparator.php', + 'Doctrine\\DBAL\\Platforms\\OraclePlatform' => __DIR__ . '/..' . '/doctrine/dbal/src/Platforms/OraclePlatform.php', + 'Doctrine\\DBAL\\Platforms\\PostgreSQL100Platform' => __DIR__ . '/..' . '/doctrine/dbal/src/Platforms/PostgreSQL100Platform.php', + 'Doctrine\\DBAL\\Platforms\\PostgreSQL120Platform' => __DIR__ . '/..' . '/doctrine/dbal/src/Platforms/PostgreSQL120Platform.php', + 'Doctrine\\DBAL\\Platforms\\PostgreSQL94Platform' => __DIR__ . '/..' . '/doctrine/dbal/src/Platforms/PostgreSQL94Platform.php', + 'Doctrine\\DBAL\\Platforms\\PostgreSQLPlatform' => __DIR__ . '/..' . '/doctrine/dbal/src/Platforms/PostgreSQLPlatform.php', + 'Doctrine\\DBAL\\Platforms\\SQLServer2012Platform' => __DIR__ . '/..' . '/doctrine/dbal/src/Platforms/SQLServer2012Platform.php', + 'Doctrine\\DBAL\\Platforms\\SQLServerPlatform' => __DIR__ . '/..' . '/doctrine/dbal/src/Platforms/SQLServerPlatform.php', + 'Doctrine\\DBAL\\Platforms\\SQLServer\\Comparator' => __DIR__ . '/..' . '/doctrine/dbal/src/Platforms/SQLServer/Comparator.php', + 'Doctrine\\DBAL\\Platforms\\SQLServer\\SQL\\Builder\\SQLServerSelectSQLBuilder' => __DIR__ . '/..' . '/doctrine/dbal/src/Platforms/SQLServer/SQL/Builder/SQLServerSelectSQLBuilder.php', + 'Doctrine\\DBAL\\Platforms\\SQLite\\Comparator' => __DIR__ . '/..' . '/doctrine/dbal/src/Platforms/SQLite/Comparator.php', + 'Doctrine\\DBAL\\Platforms\\SqlitePlatform' => __DIR__ . '/..' . '/doctrine/dbal/src/Platforms/SqlitePlatform.php', + 'Doctrine\\DBAL\\Platforms\\TrimMode' => __DIR__ . '/..' . '/doctrine/dbal/src/Platforms/TrimMode.php', + 'Doctrine\\DBAL\\Portability\\Connection' => __DIR__ . '/..' . '/doctrine/dbal/src/Portability/Connection.php', + 'Doctrine\\DBAL\\Portability\\Converter' => __DIR__ . '/..' . '/doctrine/dbal/src/Portability/Converter.php', + 'Doctrine\\DBAL\\Portability\\Driver' => __DIR__ . '/..' . '/doctrine/dbal/src/Portability/Driver.php', + 'Doctrine\\DBAL\\Portability\\Middleware' => __DIR__ . '/..' . '/doctrine/dbal/src/Portability/Middleware.php', + 'Doctrine\\DBAL\\Portability\\OptimizeFlags' => __DIR__ . '/..' . '/doctrine/dbal/src/Portability/OptimizeFlags.php', + 'Doctrine\\DBAL\\Portability\\Result' => __DIR__ . '/..' . '/doctrine/dbal/src/Portability/Result.php', + 'Doctrine\\DBAL\\Portability\\Statement' => __DIR__ . '/..' . '/doctrine/dbal/src/Portability/Statement.php', + 'Doctrine\\DBAL\\Query' => __DIR__ . '/..' . '/doctrine/dbal/src/Query.php', + 'Doctrine\\DBAL\\Query\\Expression\\CompositeExpression' => __DIR__ . '/..' . '/doctrine/dbal/src/Query/Expression/CompositeExpression.php', + 'Doctrine\\DBAL\\Query\\Expression\\ExpressionBuilder' => __DIR__ . '/..' . '/doctrine/dbal/src/Query/Expression/ExpressionBuilder.php', + 'Doctrine\\DBAL\\Query\\ForUpdate' => __DIR__ . '/..' . '/doctrine/dbal/src/Query/ForUpdate.php', + 'Doctrine\\DBAL\\Query\\ForUpdate\\ConflictResolutionMode' => __DIR__ . '/..' . '/doctrine/dbal/src/Query/ForUpdate/ConflictResolutionMode.php', + 'Doctrine\\DBAL\\Query\\Limit' => __DIR__ . '/..' . '/doctrine/dbal/src/Query/Limit.php', + 'Doctrine\\DBAL\\Query\\QueryBuilder' => __DIR__ . '/..' . '/doctrine/dbal/src/Query/QueryBuilder.php', + 'Doctrine\\DBAL\\Query\\QueryException' => __DIR__ . '/..' . '/doctrine/dbal/src/Query/QueryException.php', + 'Doctrine\\DBAL\\Query\\SelectQuery' => __DIR__ . '/..' . '/doctrine/dbal/src/Query/SelectQuery.php', + 'Doctrine\\DBAL\\Result' => __DIR__ . '/..' . '/doctrine/dbal/src/Result.php', + 'Doctrine\\DBAL\\SQL\\Builder\\CreateSchemaObjectsSQLBuilder' => __DIR__ . '/..' . '/doctrine/dbal/src/SQL/Builder/CreateSchemaObjectsSQLBuilder.php', + 'Doctrine\\DBAL\\SQL\\Builder\\DefaultSelectSQLBuilder' => __DIR__ . '/..' . '/doctrine/dbal/src/SQL/Builder/DefaultSelectSQLBuilder.php', + 'Doctrine\\DBAL\\SQL\\Builder\\DropSchemaObjectsSQLBuilder' => __DIR__ . '/..' . '/doctrine/dbal/src/SQL/Builder/DropSchemaObjectsSQLBuilder.php', + 'Doctrine\\DBAL\\SQL\\Builder\\SelectSQLBuilder' => __DIR__ . '/..' . '/doctrine/dbal/src/SQL/Builder/SelectSQLBuilder.php', + 'Doctrine\\DBAL\\SQL\\Parser' => __DIR__ . '/..' . '/doctrine/dbal/src/SQL/Parser.php', + 'Doctrine\\DBAL\\SQL\\Parser\\Exception' => __DIR__ . '/..' . '/doctrine/dbal/src/SQL/Parser/Exception.php', + 'Doctrine\\DBAL\\SQL\\Parser\\Exception\\RegularExpressionError' => __DIR__ . '/..' . '/doctrine/dbal/src/SQL/Parser/Exception/RegularExpressionError.php', + 'Doctrine\\DBAL\\SQL\\Parser\\Visitor' => __DIR__ . '/..' . '/doctrine/dbal/src/SQL/Parser/Visitor.php', + 'Doctrine\\DBAL\\Schema\\AbstractAsset' => __DIR__ . '/..' . '/doctrine/dbal/src/Schema/AbstractAsset.php', + 'Doctrine\\DBAL\\Schema\\AbstractSchemaManager' => __DIR__ . '/..' . '/doctrine/dbal/src/Schema/AbstractSchemaManager.php', + 'Doctrine\\DBAL\\Schema\\Column' => __DIR__ . '/..' . '/doctrine/dbal/src/Schema/Column.php', + 'Doctrine\\DBAL\\Schema\\ColumnDiff' => __DIR__ . '/..' . '/doctrine/dbal/src/Schema/ColumnDiff.php', + 'Doctrine\\DBAL\\Schema\\Comparator' => __DIR__ . '/..' . '/doctrine/dbal/src/Schema/Comparator.php', + 'Doctrine\\DBAL\\Schema\\Constraint' => __DIR__ . '/..' . '/doctrine/dbal/src/Schema/Constraint.php', + 'Doctrine\\DBAL\\Schema\\DB2SchemaManager' => __DIR__ . '/..' . '/doctrine/dbal/src/Schema/DB2SchemaManager.php', + 'Doctrine\\DBAL\\Schema\\DefaultSchemaManagerFactory' => __DIR__ . '/..' . '/doctrine/dbal/src/Schema/DefaultSchemaManagerFactory.php', + 'Doctrine\\DBAL\\Schema\\Exception\\ColumnAlreadyExists' => __DIR__ . '/..' . '/doctrine/dbal/src/Schema/Exception/ColumnAlreadyExists.php', + 'Doctrine\\DBAL\\Schema\\Exception\\ColumnDoesNotExist' => __DIR__ . '/..' . '/doctrine/dbal/src/Schema/Exception/ColumnDoesNotExist.php', + 'Doctrine\\DBAL\\Schema\\Exception\\ForeignKeyDoesNotExist' => __DIR__ . '/..' . '/doctrine/dbal/src/Schema/Exception/ForeignKeyDoesNotExist.php', + 'Doctrine\\DBAL\\Schema\\Exception\\IndexAlreadyExists' => __DIR__ . '/..' . '/doctrine/dbal/src/Schema/Exception/IndexAlreadyExists.php', + 'Doctrine\\DBAL\\Schema\\Exception\\IndexDoesNotExist' => __DIR__ . '/..' . '/doctrine/dbal/src/Schema/Exception/IndexDoesNotExist.php', + 'Doctrine\\DBAL\\Schema\\Exception\\IndexNameInvalid' => __DIR__ . '/..' . '/doctrine/dbal/src/Schema/Exception/IndexNameInvalid.php', + 'Doctrine\\DBAL\\Schema\\Exception\\InvalidTableName' => __DIR__ . '/..' . '/doctrine/dbal/src/Schema/Exception/InvalidTableName.php', + 'Doctrine\\DBAL\\Schema\\Exception\\NamedForeignKeyRequired' => __DIR__ . '/..' . '/doctrine/dbal/src/Schema/Exception/NamedForeignKeyRequired.php', + 'Doctrine\\DBAL\\Schema\\Exception\\NamespaceAlreadyExists' => __DIR__ . '/..' . '/doctrine/dbal/src/Schema/Exception/NamespaceAlreadyExists.php', + 'Doctrine\\DBAL\\Schema\\Exception\\SequenceAlreadyExists' => __DIR__ . '/..' . '/doctrine/dbal/src/Schema/Exception/SequenceAlreadyExists.php', + 'Doctrine\\DBAL\\Schema\\Exception\\SequenceDoesNotExist' => __DIR__ . '/..' . '/doctrine/dbal/src/Schema/Exception/SequenceDoesNotExist.php', + 'Doctrine\\DBAL\\Schema\\Exception\\TableAlreadyExists' => __DIR__ . '/..' . '/doctrine/dbal/src/Schema/Exception/TableAlreadyExists.php', + 'Doctrine\\DBAL\\Schema\\Exception\\TableDoesNotExist' => __DIR__ . '/..' . '/doctrine/dbal/src/Schema/Exception/TableDoesNotExist.php', + 'Doctrine\\DBAL\\Schema\\Exception\\UniqueConstraintDoesNotExist' => __DIR__ . '/..' . '/doctrine/dbal/src/Schema/Exception/UniqueConstraintDoesNotExist.php', + 'Doctrine\\DBAL\\Schema\\Exception\\UnknownColumnOption' => __DIR__ . '/..' . '/doctrine/dbal/src/Schema/Exception/UnknownColumnOption.php', + 'Doctrine\\DBAL\\Schema\\ForeignKeyConstraint' => __DIR__ . '/..' . '/doctrine/dbal/src/Schema/ForeignKeyConstraint.php', + 'Doctrine\\DBAL\\Schema\\Identifier' => __DIR__ . '/..' . '/doctrine/dbal/src/Schema/Identifier.php', + 'Doctrine\\DBAL\\Schema\\Index' => __DIR__ . '/..' . '/doctrine/dbal/src/Schema/Index.php', + 'Doctrine\\DBAL\\Schema\\LegacySchemaManagerFactory' => __DIR__ . '/..' . '/doctrine/dbal/src/Schema/LegacySchemaManagerFactory.php', + 'Doctrine\\DBAL\\Schema\\MySQLSchemaManager' => __DIR__ . '/..' . '/doctrine/dbal/src/Schema/MySQLSchemaManager.php', + 'Doctrine\\DBAL\\Schema\\OracleSchemaManager' => __DIR__ . '/..' . '/doctrine/dbal/src/Schema/OracleSchemaManager.php', + 'Doctrine\\DBAL\\Schema\\PostgreSQLSchemaManager' => __DIR__ . '/..' . '/doctrine/dbal/src/Schema/PostgreSQLSchemaManager.php', + 'Doctrine\\DBAL\\Schema\\SQLServerSchemaManager' => __DIR__ . '/..' . '/doctrine/dbal/src/Schema/SQLServerSchemaManager.php', + 'Doctrine\\DBAL\\Schema\\Schema' => __DIR__ . '/..' . '/doctrine/dbal/src/Schema/Schema.php', + 'Doctrine\\DBAL\\Schema\\SchemaConfig' => __DIR__ . '/..' . '/doctrine/dbal/src/Schema/SchemaConfig.php', + 'Doctrine\\DBAL\\Schema\\SchemaDiff' => __DIR__ . '/..' . '/doctrine/dbal/src/Schema/SchemaDiff.php', + 'Doctrine\\DBAL\\Schema\\SchemaException' => __DIR__ . '/..' . '/doctrine/dbal/src/Schema/SchemaException.php', + 'Doctrine\\DBAL\\Schema\\SchemaManagerFactory' => __DIR__ . '/..' . '/doctrine/dbal/src/Schema/SchemaManagerFactory.php', + 'Doctrine\\DBAL\\Schema\\Sequence' => __DIR__ . '/..' . '/doctrine/dbal/src/Schema/Sequence.php', + 'Doctrine\\DBAL\\Schema\\SqliteSchemaManager' => __DIR__ . '/..' . '/doctrine/dbal/src/Schema/SqliteSchemaManager.php', + 'Doctrine\\DBAL\\Schema\\Table' => __DIR__ . '/..' . '/doctrine/dbal/src/Schema/Table.php', + 'Doctrine\\DBAL\\Schema\\TableDiff' => __DIR__ . '/..' . '/doctrine/dbal/src/Schema/TableDiff.php', + 'Doctrine\\DBAL\\Schema\\UniqueConstraint' => __DIR__ . '/..' . '/doctrine/dbal/src/Schema/UniqueConstraint.php', + 'Doctrine\\DBAL\\Schema\\View' => __DIR__ . '/..' . '/doctrine/dbal/src/Schema/View.php', + 'Doctrine\\DBAL\\Schema\\Visitor\\AbstractVisitor' => __DIR__ . '/..' . '/doctrine/dbal/src/Schema/Visitor/AbstractVisitor.php', + 'Doctrine\\DBAL\\Schema\\Visitor\\CreateSchemaSqlCollector' => __DIR__ . '/..' . '/doctrine/dbal/src/Schema/Visitor/CreateSchemaSqlCollector.php', + 'Doctrine\\DBAL\\Schema\\Visitor\\DropSchemaSqlCollector' => __DIR__ . '/..' . '/doctrine/dbal/src/Schema/Visitor/DropSchemaSqlCollector.php', + 'Doctrine\\DBAL\\Schema\\Visitor\\Graphviz' => __DIR__ . '/..' . '/doctrine/dbal/src/Schema/Visitor/Graphviz.php', + 'Doctrine\\DBAL\\Schema\\Visitor\\NamespaceVisitor' => __DIR__ . '/..' . '/doctrine/dbal/src/Schema/Visitor/NamespaceVisitor.php', + 'Doctrine\\DBAL\\Schema\\Visitor\\RemoveNamespacedAssets' => __DIR__ . '/..' . '/doctrine/dbal/src/Schema/Visitor/RemoveNamespacedAssets.php', + 'Doctrine\\DBAL\\Schema\\Visitor\\Visitor' => __DIR__ . '/..' . '/doctrine/dbal/src/Schema/Visitor/Visitor.php', + 'Doctrine\\DBAL\\Statement' => __DIR__ . '/..' . '/doctrine/dbal/src/Statement.php', + 'Doctrine\\DBAL\\Tools\\Console\\Command\\CommandCompatibility' => __DIR__ . '/..' . '/doctrine/dbal/src/Tools/Console/Command/CommandCompatibility.php', + 'Doctrine\\DBAL\\Tools\\Console\\Command\\ReservedWordsCommand' => __DIR__ . '/..' . '/doctrine/dbal/src/Tools/Console/Command/ReservedWordsCommand.php', + 'Doctrine\\DBAL\\Tools\\Console\\Command\\RunSqlCommand' => __DIR__ . '/..' . '/doctrine/dbal/src/Tools/Console/Command/RunSqlCommand.php', + 'Doctrine\\DBAL\\Tools\\Console\\ConnectionNotFound' => __DIR__ . '/..' . '/doctrine/dbal/src/Tools/Console/ConnectionNotFound.php', + 'Doctrine\\DBAL\\Tools\\Console\\ConnectionProvider' => __DIR__ . '/..' . '/doctrine/dbal/src/Tools/Console/ConnectionProvider.php', + 'Doctrine\\DBAL\\Tools\\Console\\ConnectionProvider\\SingleConnectionProvider' => __DIR__ . '/..' . '/doctrine/dbal/src/Tools/Console/ConnectionProvider/SingleConnectionProvider.php', + 'Doctrine\\DBAL\\Tools\\Console\\ConsoleRunner' => __DIR__ . '/..' . '/doctrine/dbal/src/Tools/Console/ConsoleRunner.php', + 'Doctrine\\DBAL\\Tools\\DsnParser' => __DIR__ . '/..' . '/doctrine/dbal/src/Tools/DsnParser.php', + 'Doctrine\\DBAL\\TransactionIsolationLevel' => __DIR__ . '/..' . '/doctrine/dbal/src/TransactionIsolationLevel.php', + 'Doctrine\\DBAL\\Types\\ArrayType' => __DIR__ . '/..' . '/doctrine/dbal/src/Types/ArrayType.php', + 'Doctrine\\DBAL\\Types\\AsciiStringType' => __DIR__ . '/..' . '/doctrine/dbal/src/Types/AsciiStringType.php', + 'Doctrine\\DBAL\\Types\\BigIntType' => __DIR__ . '/..' . '/doctrine/dbal/src/Types/BigIntType.php', + 'Doctrine\\DBAL\\Types\\BinaryType' => __DIR__ . '/..' . '/doctrine/dbal/src/Types/BinaryType.php', + 'Doctrine\\DBAL\\Types\\BlobType' => __DIR__ . '/..' . '/doctrine/dbal/src/Types/BlobType.php', + 'Doctrine\\DBAL\\Types\\BooleanType' => __DIR__ . '/..' . '/doctrine/dbal/src/Types/BooleanType.php', + 'Doctrine\\DBAL\\Types\\ConversionException' => __DIR__ . '/..' . '/doctrine/dbal/src/Types/ConversionException.php', + 'Doctrine\\DBAL\\Types\\DateImmutableType' => __DIR__ . '/..' . '/doctrine/dbal/src/Types/DateImmutableType.php', + 'Doctrine\\DBAL\\Types\\DateIntervalType' => __DIR__ . '/..' . '/doctrine/dbal/src/Types/DateIntervalType.php', + 'Doctrine\\DBAL\\Types\\DateTimeImmutableType' => __DIR__ . '/..' . '/doctrine/dbal/src/Types/DateTimeImmutableType.php', + 'Doctrine\\DBAL\\Types\\DateTimeType' => __DIR__ . '/..' . '/doctrine/dbal/src/Types/DateTimeType.php', + 'Doctrine\\DBAL\\Types\\DateTimeTzImmutableType' => __DIR__ . '/..' . '/doctrine/dbal/src/Types/DateTimeTzImmutableType.php', + 'Doctrine\\DBAL\\Types\\DateTimeTzType' => __DIR__ . '/..' . '/doctrine/dbal/src/Types/DateTimeTzType.php', + 'Doctrine\\DBAL\\Types\\DateType' => __DIR__ . '/..' . '/doctrine/dbal/src/Types/DateType.php', + 'Doctrine\\DBAL\\Types\\DecimalType' => __DIR__ . '/..' . '/doctrine/dbal/src/Types/DecimalType.php', + 'Doctrine\\DBAL\\Types\\FloatType' => __DIR__ . '/..' . '/doctrine/dbal/src/Types/FloatType.php', + 'Doctrine\\DBAL\\Types\\GuidType' => __DIR__ . '/..' . '/doctrine/dbal/src/Types/GuidType.php', + 'Doctrine\\DBAL\\Types\\IntegerType' => __DIR__ . '/..' . '/doctrine/dbal/src/Types/IntegerType.php', + 'Doctrine\\DBAL\\Types\\JsonType' => __DIR__ . '/..' . '/doctrine/dbal/src/Types/JsonType.php', + 'Doctrine\\DBAL\\Types\\ObjectType' => __DIR__ . '/..' . '/doctrine/dbal/src/Types/ObjectType.php', + 'Doctrine\\DBAL\\Types\\PhpDateTimeMappingType' => __DIR__ . '/..' . '/doctrine/dbal/src/Types/PhpDateTimeMappingType.php', + 'Doctrine\\DBAL\\Types\\PhpIntegerMappingType' => __DIR__ . '/..' . '/doctrine/dbal/src/Types/PhpIntegerMappingType.php', + 'Doctrine\\DBAL\\Types\\SimpleArrayType' => __DIR__ . '/..' . '/doctrine/dbal/src/Types/SimpleArrayType.php', + 'Doctrine\\DBAL\\Types\\SmallIntType' => __DIR__ . '/..' . '/doctrine/dbal/src/Types/SmallIntType.php', + 'Doctrine\\DBAL\\Types\\StringType' => __DIR__ . '/..' . '/doctrine/dbal/src/Types/StringType.php', + 'Doctrine\\DBAL\\Types\\TextType' => __DIR__ . '/..' . '/doctrine/dbal/src/Types/TextType.php', + 'Doctrine\\DBAL\\Types\\TimeImmutableType' => __DIR__ . '/..' . '/doctrine/dbal/src/Types/TimeImmutableType.php', + 'Doctrine\\DBAL\\Types\\TimeType' => __DIR__ . '/..' . '/doctrine/dbal/src/Types/TimeType.php', + 'Doctrine\\DBAL\\Types\\Type' => __DIR__ . '/..' . '/doctrine/dbal/src/Types/Type.php', + 'Doctrine\\DBAL\\Types\\TypeRegistry' => __DIR__ . '/..' . '/doctrine/dbal/src/Types/TypeRegistry.php', + 'Doctrine\\DBAL\\Types\\Types' => __DIR__ . '/..' . '/doctrine/dbal/src/Types/Types.php', + 'Doctrine\\DBAL\\Types\\VarDateTimeImmutableType' => __DIR__ . '/..' . '/doctrine/dbal/src/Types/VarDateTimeImmutableType.php', + 'Doctrine\\DBAL\\Types\\VarDateTimeType' => __DIR__ . '/..' . '/doctrine/dbal/src/Types/VarDateTimeType.php', + 'Doctrine\\DBAL\\VersionAwarePlatformDriver' => __DIR__ . '/..' . '/doctrine/dbal/src/VersionAwarePlatformDriver.php', + 'Doctrine\\Deprecations\\Deprecation' => __DIR__ . '/..' . '/doctrine/deprecations/src/Deprecation.php', + 'Doctrine\\Deprecations\\PHPUnit\\VerifyDeprecations' => __DIR__ . '/..' . '/doctrine/deprecations/src/PHPUnit/VerifyDeprecations.php', + 'Egulias\\EmailValidator\\EmailLexer' => __DIR__ . '/..' . '/egulias/email-validator/src/EmailLexer.php', + 'Egulias\\EmailValidator\\EmailParser' => __DIR__ . '/..' . '/egulias/email-validator/src/EmailParser.php', + 'Egulias\\EmailValidator\\EmailValidator' => __DIR__ . '/..' . '/egulias/email-validator/src/EmailValidator.php', + 'Egulias\\EmailValidator\\MessageIDParser' => __DIR__ . '/..' . '/egulias/email-validator/src/MessageIDParser.php', + 'Egulias\\EmailValidator\\Parser' => __DIR__ . '/..' . '/egulias/email-validator/src/Parser.php', + 'Egulias\\EmailValidator\\Parser\\Comment' => __DIR__ . '/..' . '/egulias/email-validator/src/Parser/Comment.php', + 'Egulias\\EmailValidator\\Parser\\CommentStrategy\\CommentStrategy' => __DIR__ . '/..' . '/egulias/email-validator/src/Parser/CommentStrategy/CommentStrategy.php', + 'Egulias\\EmailValidator\\Parser\\CommentStrategy\\DomainComment' => __DIR__ . '/..' . '/egulias/email-validator/src/Parser/CommentStrategy/DomainComment.php', + 'Egulias\\EmailValidator\\Parser\\CommentStrategy\\LocalComment' => __DIR__ . '/..' . '/egulias/email-validator/src/Parser/CommentStrategy/LocalComment.php', + 'Egulias\\EmailValidator\\Parser\\DomainLiteral' => __DIR__ . '/..' . '/egulias/email-validator/src/Parser/DomainLiteral.php', + 'Egulias\\EmailValidator\\Parser\\DomainPart' => __DIR__ . '/..' . '/egulias/email-validator/src/Parser/DomainPart.php', + 'Egulias\\EmailValidator\\Parser\\DoubleQuote' => __DIR__ . '/..' . '/egulias/email-validator/src/Parser/DoubleQuote.php', + 'Egulias\\EmailValidator\\Parser\\FoldingWhiteSpace' => __DIR__ . '/..' . '/egulias/email-validator/src/Parser/FoldingWhiteSpace.php', + 'Egulias\\EmailValidator\\Parser\\IDLeftPart' => __DIR__ . '/..' . '/egulias/email-validator/src/Parser/IDLeftPart.php', + 'Egulias\\EmailValidator\\Parser\\IDRightPart' => __DIR__ . '/..' . '/egulias/email-validator/src/Parser/IDRightPart.php', + 'Egulias\\EmailValidator\\Parser\\LocalPart' => __DIR__ . '/..' . '/egulias/email-validator/src/Parser/LocalPart.php', + 'Egulias\\EmailValidator\\Parser\\PartParser' => __DIR__ . '/..' . '/egulias/email-validator/src/Parser/PartParser.php', + 'Egulias\\EmailValidator\\Result\\InvalidEmail' => __DIR__ . '/..' . '/egulias/email-validator/src/Result/InvalidEmail.php', + 'Egulias\\EmailValidator\\Result\\MultipleErrors' => __DIR__ . '/..' . '/egulias/email-validator/src/Result/MultipleErrors.php', + 'Egulias\\EmailValidator\\Result\\Reason\\AtextAfterCFWS' => __DIR__ . '/..' . '/egulias/email-validator/src/Result/Reason/AtextAfterCFWS.php', + 'Egulias\\EmailValidator\\Result\\Reason\\CRLFAtTheEnd' => __DIR__ . '/..' . '/egulias/email-validator/src/Result/Reason/CRLFAtTheEnd.php', + 'Egulias\\EmailValidator\\Result\\Reason\\CRLFX2' => __DIR__ . '/..' . '/egulias/email-validator/src/Result/Reason/CRLFX2.php', + 'Egulias\\EmailValidator\\Result\\Reason\\CRNoLF' => __DIR__ . '/..' . '/egulias/email-validator/src/Result/Reason/CRNoLF.php', + 'Egulias\\EmailValidator\\Result\\Reason\\CharNotAllowed' => __DIR__ . '/..' . '/egulias/email-validator/src/Result/Reason/CharNotAllowed.php', + 'Egulias\\EmailValidator\\Result\\Reason\\CommaInDomain' => __DIR__ . '/..' . '/egulias/email-validator/src/Result/Reason/CommaInDomain.php', + 'Egulias\\EmailValidator\\Result\\Reason\\CommentsInIDRight' => __DIR__ . '/..' . '/egulias/email-validator/src/Result/Reason/CommentsInIDRight.php', + 'Egulias\\EmailValidator\\Result\\Reason\\ConsecutiveAt' => __DIR__ . '/..' . '/egulias/email-validator/src/Result/Reason/ConsecutiveAt.php', + 'Egulias\\EmailValidator\\Result\\Reason\\ConsecutiveDot' => __DIR__ . '/..' . '/egulias/email-validator/src/Result/Reason/ConsecutiveDot.php', + 'Egulias\\EmailValidator\\Result\\Reason\\DetailedReason' => __DIR__ . '/..' . '/egulias/email-validator/src/Result/Reason/DetailedReason.php', + 'Egulias\\EmailValidator\\Result\\Reason\\DomainAcceptsNoMail' => __DIR__ . '/..' . '/egulias/email-validator/src/Result/Reason/DomainAcceptsNoMail.php', + 'Egulias\\EmailValidator\\Result\\Reason\\DomainHyphened' => __DIR__ . '/..' . '/egulias/email-validator/src/Result/Reason/DomainHyphened.php', + 'Egulias\\EmailValidator\\Result\\Reason\\DomainTooLong' => __DIR__ . '/..' . '/egulias/email-validator/src/Result/Reason/DomainTooLong.php', + 'Egulias\\EmailValidator\\Result\\Reason\\DotAtEnd' => __DIR__ . '/..' . '/egulias/email-validator/src/Result/Reason/DotAtEnd.php', + 'Egulias\\EmailValidator\\Result\\Reason\\DotAtStart' => __DIR__ . '/..' . '/egulias/email-validator/src/Result/Reason/DotAtStart.php', + 'Egulias\\EmailValidator\\Result\\Reason\\EmptyReason' => __DIR__ . '/..' . '/egulias/email-validator/src/Result/Reason/EmptyReason.php', + 'Egulias\\EmailValidator\\Result\\Reason\\ExceptionFound' => __DIR__ . '/..' . '/egulias/email-validator/src/Result/Reason/ExceptionFound.php', + 'Egulias\\EmailValidator\\Result\\Reason\\ExpectingATEXT' => __DIR__ . '/..' . '/egulias/email-validator/src/Result/Reason/ExpectingATEXT.php', + 'Egulias\\EmailValidator\\Result\\Reason\\ExpectingCTEXT' => __DIR__ . '/..' . '/egulias/email-validator/src/Result/Reason/ExpectingCTEXT.php', + 'Egulias\\EmailValidator\\Result\\Reason\\ExpectingDTEXT' => __DIR__ . '/..' . '/egulias/email-validator/src/Result/Reason/ExpectingDTEXT.php', + 'Egulias\\EmailValidator\\Result\\Reason\\ExpectingDomainLiteralClose' => __DIR__ . '/..' . '/egulias/email-validator/src/Result/Reason/ExpectingDomainLiteralClose.php', + 'Egulias\\EmailValidator\\Result\\Reason\\LabelTooLong' => __DIR__ . '/..' . '/egulias/email-validator/src/Result/Reason/LabelTooLong.php', + 'Egulias\\EmailValidator\\Result\\Reason\\LocalOrReservedDomain' => __DIR__ . '/..' . '/egulias/email-validator/src/Result/Reason/LocalOrReservedDomain.php', + 'Egulias\\EmailValidator\\Result\\Reason\\NoDNSRecord' => __DIR__ . '/..' . '/egulias/email-validator/src/Result/Reason/NoDNSRecord.php', + 'Egulias\\EmailValidator\\Result\\Reason\\NoDomainPart' => __DIR__ . '/..' . '/egulias/email-validator/src/Result/Reason/NoDomainPart.php', + 'Egulias\\EmailValidator\\Result\\Reason\\NoLocalPart' => __DIR__ . '/..' . '/egulias/email-validator/src/Result/Reason/NoLocalPart.php', + 'Egulias\\EmailValidator\\Result\\Reason\\RFCWarnings' => __DIR__ . '/..' . '/egulias/email-validator/src/Result/Reason/RFCWarnings.php', + 'Egulias\\EmailValidator\\Result\\Reason\\Reason' => __DIR__ . '/..' . '/egulias/email-validator/src/Result/Reason/Reason.php', + 'Egulias\\EmailValidator\\Result\\Reason\\SpoofEmail' => __DIR__ . '/..' . '/egulias/email-validator/src/Result/Reason/SpoofEmail.php', + 'Egulias\\EmailValidator\\Result\\Reason\\UnOpenedComment' => __DIR__ . '/..' . '/egulias/email-validator/src/Result/Reason/UnOpenedComment.php', + 'Egulias\\EmailValidator\\Result\\Reason\\UnableToGetDNSRecord' => __DIR__ . '/..' . '/egulias/email-validator/src/Result/Reason/UnableToGetDNSRecord.php', + 'Egulias\\EmailValidator\\Result\\Reason\\UnclosedComment' => __DIR__ . '/..' . '/egulias/email-validator/src/Result/Reason/UnclosedComment.php', + 'Egulias\\EmailValidator\\Result\\Reason\\UnclosedQuotedString' => __DIR__ . '/..' . '/egulias/email-validator/src/Result/Reason/UnclosedQuotedString.php', + 'Egulias\\EmailValidator\\Result\\Reason\\UnusualElements' => __DIR__ . '/..' . '/egulias/email-validator/src/Result/Reason/UnusualElements.php', + 'Egulias\\EmailValidator\\Result\\Result' => __DIR__ . '/..' . '/egulias/email-validator/src/Result/Result.php', + 'Egulias\\EmailValidator\\Result\\SpoofEmail' => __DIR__ . '/..' . '/egulias/email-validator/src/Result/SpoofEmail.php', + 'Egulias\\EmailValidator\\Result\\ValidEmail' => __DIR__ . '/..' . '/egulias/email-validator/src/Result/ValidEmail.php', + 'Egulias\\EmailValidator\\Validation\\DNSCheckValidation' => __DIR__ . '/..' . '/egulias/email-validator/src/Validation/DNSCheckValidation.php', + 'Egulias\\EmailValidator\\Validation\\DNSGetRecordWrapper' => __DIR__ . '/..' . '/egulias/email-validator/src/Validation/DNSGetRecordWrapper.php', + 'Egulias\\EmailValidator\\Validation\\DNSRecords' => __DIR__ . '/..' . '/egulias/email-validator/src/Validation/DNSRecords.php', + 'Egulias\\EmailValidator\\Validation\\EmailValidation' => __DIR__ . '/..' . '/egulias/email-validator/src/Validation/EmailValidation.php', + 'Egulias\\EmailValidator\\Validation\\Exception\\EmptyValidationList' => __DIR__ . '/..' . '/egulias/email-validator/src/Validation/Exception/EmptyValidationList.php', + 'Egulias\\EmailValidator\\Validation\\Extra\\SpoofCheckValidation' => __DIR__ . '/..' . '/egulias/email-validator/src/Validation/Extra/SpoofCheckValidation.php', + 'Egulias\\EmailValidator\\Validation\\MessageIDValidation' => __DIR__ . '/..' . '/egulias/email-validator/src/Validation/MessageIDValidation.php', + 'Egulias\\EmailValidator\\Validation\\MultipleValidationWithAnd' => __DIR__ . '/..' . '/egulias/email-validator/src/Validation/MultipleValidationWithAnd.php', + 'Egulias\\EmailValidator\\Validation\\NoRFCWarningsValidation' => __DIR__ . '/..' . '/egulias/email-validator/src/Validation/NoRFCWarningsValidation.php', + 'Egulias\\EmailValidator\\Validation\\RFCValidation' => __DIR__ . '/..' . '/egulias/email-validator/src/Validation/RFCValidation.php', + 'Egulias\\EmailValidator\\Warning\\AddressLiteral' => __DIR__ . '/..' . '/egulias/email-validator/src/Warning/AddressLiteral.php', + 'Egulias\\EmailValidator\\Warning\\CFWSNearAt' => __DIR__ . '/..' . '/egulias/email-validator/src/Warning/CFWSNearAt.php', + 'Egulias\\EmailValidator\\Warning\\CFWSWithFWS' => __DIR__ . '/..' . '/egulias/email-validator/src/Warning/CFWSWithFWS.php', + 'Egulias\\EmailValidator\\Warning\\Comment' => __DIR__ . '/..' . '/egulias/email-validator/src/Warning/Comment.php', + 'Egulias\\EmailValidator\\Warning\\DeprecatedComment' => __DIR__ . '/..' . '/egulias/email-validator/src/Warning/DeprecatedComment.php', + 'Egulias\\EmailValidator\\Warning\\DomainLiteral' => __DIR__ . '/..' . '/egulias/email-validator/src/Warning/DomainLiteral.php', + 'Egulias\\EmailValidator\\Warning\\EmailTooLong' => __DIR__ . '/..' . '/egulias/email-validator/src/Warning/EmailTooLong.php', + 'Egulias\\EmailValidator\\Warning\\IPV6BadChar' => __DIR__ . '/..' . '/egulias/email-validator/src/Warning/IPV6BadChar.php', + 'Egulias\\EmailValidator\\Warning\\IPV6ColonEnd' => __DIR__ . '/..' . '/egulias/email-validator/src/Warning/IPV6ColonEnd.php', + 'Egulias\\EmailValidator\\Warning\\IPV6ColonStart' => __DIR__ . '/..' . '/egulias/email-validator/src/Warning/IPV6ColonStart.php', + 'Egulias\\EmailValidator\\Warning\\IPV6Deprecated' => __DIR__ . '/..' . '/egulias/email-validator/src/Warning/IPV6Deprecated.php', + 'Egulias\\EmailValidator\\Warning\\IPV6DoubleColon' => __DIR__ . '/..' . '/egulias/email-validator/src/Warning/IPV6DoubleColon.php', + 'Egulias\\EmailValidator\\Warning\\IPV6GroupCount' => __DIR__ . '/..' . '/egulias/email-validator/src/Warning/IPV6GroupCount.php', + 'Egulias\\EmailValidator\\Warning\\IPV6MaxGroups' => __DIR__ . '/..' . '/egulias/email-validator/src/Warning/IPV6MaxGroups.php', + 'Egulias\\EmailValidator\\Warning\\LocalTooLong' => __DIR__ . '/..' . '/egulias/email-validator/src/Warning/LocalTooLong.php', + 'Egulias\\EmailValidator\\Warning\\NoDNSMXRecord' => __DIR__ . '/..' . '/egulias/email-validator/src/Warning/NoDNSMXRecord.php', + 'Egulias\\EmailValidator\\Warning\\ObsoleteDTEXT' => __DIR__ . '/..' . '/egulias/email-validator/src/Warning/ObsoleteDTEXT.php', + 'Egulias\\EmailValidator\\Warning\\QuotedPart' => __DIR__ . '/..' . '/egulias/email-validator/src/Warning/QuotedPart.php', + 'Egulias\\EmailValidator\\Warning\\QuotedString' => __DIR__ . '/..' . '/egulias/email-validator/src/Warning/QuotedString.php', + 'Egulias\\EmailValidator\\Warning\\TLD' => __DIR__ . '/..' . '/egulias/email-validator/src/Warning/TLD.php', + 'Egulias\\EmailValidator\\Warning\\Warning' => __DIR__ . '/..' . '/egulias/email-validator/src/Warning/Warning.php', + 'Fusonic\\OpenGraph\\Consumer' => __DIR__ . '/..' . '/fusonic/opengraph/src/Consumer.php', + 'Fusonic\\OpenGraph\\Elements\\Audio' => __DIR__ . '/..' . '/fusonic/opengraph/src/Elements/Audio.php', + 'Fusonic\\OpenGraph\\Elements\\ElementBase' => __DIR__ . '/..' . '/fusonic/opengraph/src/Elements/ElementBase.php', + 'Fusonic\\OpenGraph\\Elements\\Image' => __DIR__ . '/..' . '/fusonic/opengraph/src/Elements/Image.php', + 'Fusonic\\OpenGraph\\Elements\\Video' => __DIR__ . '/..' . '/fusonic/opengraph/src/Elements/Video.php', + 'Fusonic\\OpenGraph\\Objects\\ObjectBase' => __DIR__ . '/..' . '/fusonic/opengraph/src/Objects/ObjectBase.php', + 'Fusonic\\OpenGraph\\Objects\\Website' => __DIR__ . '/..' . '/fusonic/opengraph/src/Objects/Website.php', + 'Fusonic\\OpenGraph\\Property' => __DIR__ . '/..' . '/fusonic/opengraph/src/Property.php', + 'Fusonic\\OpenGraph\\Publisher' => __DIR__ . '/..' . '/fusonic/opengraph/src/Publisher.php', + 'GuzzleHttp\\BodySummarizer' => __DIR__ . '/..' . '/guzzlehttp/guzzle/src/BodySummarizer.php', + 'GuzzleHttp\\BodySummarizerInterface' => __DIR__ . '/..' . '/guzzlehttp/guzzle/src/BodySummarizerInterface.php', + 'GuzzleHttp\\Client' => __DIR__ . '/..' . '/guzzlehttp/guzzle/src/Client.php', + 'GuzzleHttp\\ClientInterface' => __DIR__ . '/..' . '/guzzlehttp/guzzle/src/ClientInterface.php', + 'GuzzleHttp\\ClientTrait' => __DIR__ . '/..' . '/guzzlehttp/guzzle/src/ClientTrait.php', + 'GuzzleHttp\\Cookie\\CookieJar' => __DIR__ . '/..' . '/guzzlehttp/guzzle/src/Cookie/CookieJar.php', + 'GuzzleHttp\\Cookie\\CookieJarInterface' => __DIR__ . '/..' . '/guzzlehttp/guzzle/src/Cookie/CookieJarInterface.php', + 'GuzzleHttp\\Cookie\\FileCookieJar' => __DIR__ . '/..' . '/guzzlehttp/guzzle/src/Cookie/FileCookieJar.php', + 'GuzzleHttp\\Cookie\\SessionCookieJar' => __DIR__ . '/..' . '/guzzlehttp/guzzle/src/Cookie/SessionCookieJar.php', + 'GuzzleHttp\\Cookie\\SetCookie' => __DIR__ . '/..' . '/guzzlehttp/guzzle/src/Cookie/SetCookie.php', + 'GuzzleHttp\\Exception\\BadResponseException' => __DIR__ . '/..' . '/guzzlehttp/guzzle/src/Exception/BadResponseException.php', + 'GuzzleHttp\\Exception\\ClientException' => __DIR__ . '/..' . '/guzzlehttp/guzzle/src/Exception/ClientException.php', + 'GuzzleHttp\\Exception\\ConnectException' => __DIR__ . '/..' . '/guzzlehttp/guzzle/src/Exception/ConnectException.php', + 'GuzzleHttp\\Exception\\GuzzleException' => __DIR__ . '/..' . '/guzzlehttp/guzzle/src/Exception/GuzzleException.php', + 'GuzzleHttp\\Exception\\InvalidArgumentException' => __DIR__ . '/..' . '/guzzlehttp/guzzle/src/Exception/InvalidArgumentException.php', + 'GuzzleHttp\\Exception\\RequestException' => __DIR__ . '/..' . '/guzzlehttp/guzzle/src/Exception/RequestException.php', + 'GuzzleHttp\\Exception\\ServerException' => __DIR__ . '/..' . '/guzzlehttp/guzzle/src/Exception/ServerException.php', + 'GuzzleHttp\\Exception\\TooManyRedirectsException' => __DIR__ . '/..' . '/guzzlehttp/guzzle/src/Exception/TooManyRedirectsException.php', + 'GuzzleHttp\\Exception\\TransferException' => __DIR__ . '/..' . '/guzzlehttp/guzzle/src/Exception/TransferException.php', + 'GuzzleHttp\\HandlerStack' => __DIR__ . '/..' . '/guzzlehttp/guzzle/src/HandlerStack.php', + 'GuzzleHttp\\Handler\\CurlFactory' => __DIR__ . '/..' . '/guzzlehttp/guzzle/src/Handler/CurlFactory.php', + 'GuzzleHttp\\Handler\\CurlFactoryInterface' => __DIR__ . '/..' . '/guzzlehttp/guzzle/src/Handler/CurlFactoryInterface.php', + 'GuzzleHttp\\Handler\\CurlHandler' => __DIR__ . '/..' . '/guzzlehttp/guzzle/src/Handler/CurlHandler.php', + 'GuzzleHttp\\Handler\\CurlMultiHandler' => __DIR__ . '/..' . '/guzzlehttp/guzzle/src/Handler/CurlMultiHandler.php', + 'GuzzleHttp\\Handler\\EasyHandle' => __DIR__ . '/..' . '/guzzlehttp/guzzle/src/Handler/EasyHandle.php', + 'GuzzleHttp\\Handler\\HeaderProcessor' => __DIR__ . '/..' . '/guzzlehttp/guzzle/src/Handler/HeaderProcessor.php', + 'GuzzleHttp\\Handler\\MockHandler' => __DIR__ . '/..' . '/guzzlehttp/guzzle/src/Handler/MockHandler.php', + 'GuzzleHttp\\Handler\\Proxy' => __DIR__ . '/..' . '/guzzlehttp/guzzle/src/Handler/Proxy.php', + 'GuzzleHttp\\Handler\\StreamHandler' => __DIR__ . '/..' . '/guzzlehttp/guzzle/src/Handler/StreamHandler.php', + 'GuzzleHttp\\MessageFormatter' => __DIR__ . '/..' . '/guzzlehttp/guzzle/src/MessageFormatter.php', + 'GuzzleHttp\\MessageFormatterInterface' => __DIR__ . '/..' . '/guzzlehttp/guzzle/src/MessageFormatterInterface.php', + 'GuzzleHttp\\Middleware' => __DIR__ . '/..' . '/guzzlehttp/guzzle/src/Middleware.php', + 'GuzzleHttp\\Pool' => __DIR__ . '/..' . '/guzzlehttp/guzzle/src/Pool.php', + 'GuzzleHttp\\PrepareBodyMiddleware' => __DIR__ . '/..' . '/guzzlehttp/guzzle/src/PrepareBodyMiddleware.php', + 'GuzzleHttp\\Promise\\AggregateException' => __DIR__ . '/..' . '/guzzlehttp/promises/src/AggregateException.php', + 'GuzzleHttp\\Promise\\CancellationException' => __DIR__ . '/..' . '/guzzlehttp/promises/src/CancellationException.php', + 'GuzzleHttp\\Promise\\Coroutine' => __DIR__ . '/..' . '/guzzlehttp/promises/src/Coroutine.php', + 'GuzzleHttp\\Promise\\Create' => __DIR__ . '/..' . '/guzzlehttp/promises/src/Create.php', + 'GuzzleHttp\\Promise\\Each' => __DIR__ . '/..' . '/guzzlehttp/promises/src/Each.php', + 'GuzzleHttp\\Promise\\EachPromise' => __DIR__ . '/..' . '/guzzlehttp/promises/src/EachPromise.php', + 'GuzzleHttp\\Promise\\FulfilledPromise' => __DIR__ . '/..' . '/guzzlehttp/promises/src/FulfilledPromise.php', + 'GuzzleHttp\\Promise\\Is' => __DIR__ . '/..' . '/guzzlehttp/promises/src/Is.php', + 'GuzzleHttp\\Promise\\Promise' => __DIR__ . '/..' . '/guzzlehttp/promises/src/Promise.php', + 'GuzzleHttp\\Promise\\PromiseInterface' => __DIR__ . '/..' . '/guzzlehttp/promises/src/PromiseInterface.php', + 'GuzzleHttp\\Promise\\PromisorInterface' => __DIR__ . '/..' . '/guzzlehttp/promises/src/PromisorInterface.php', + 'GuzzleHttp\\Promise\\RejectedPromise' => __DIR__ . '/..' . '/guzzlehttp/promises/src/RejectedPromise.php', + 'GuzzleHttp\\Promise\\RejectionException' => __DIR__ . '/..' . '/guzzlehttp/promises/src/RejectionException.php', + 'GuzzleHttp\\Promise\\TaskQueue' => __DIR__ . '/..' . '/guzzlehttp/promises/src/TaskQueue.php', + 'GuzzleHttp\\Promise\\TaskQueueInterface' => __DIR__ . '/..' . '/guzzlehttp/promises/src/TaskQueueInterface.php', + 'GuzzleHttp\\Promise\\Utils' => __DIR__ . '/..' . '/guzzlehttp/promises/src/Utils.php', + 'GuzzleHttp\\Psr7\\AppendStream' => __DIR__ . '/..' . '/guzzlehttp/psr7/src/AppendStream.php', + 'GuzzleHttp\\Psr7\\BufferStream' => __DIR__ . '/..' . '/guzzlehttp/psr7/src/BufferStream.php', + 'GuzzleHttp\\Psr7\\CachingStream' => __DIR__ . '/..' . '/guzzlehttp/psr7/src/CachingStream.php', + 'GuzzleHttp\\Psr7\\DroppingStream' => __DIR__ . '/..' . '/guzzlehttp/psr7/src/DroppingStream.php', + 'GuzzleHttp\\Psr7\\Exception\\MalformedUriException' => __DIR__ . '/..' . '/guzzlehttp/psr7/src/Exception/MalformedUriException.php', + 'GuzzleHttp\\Psr7\\FnStream' => __DIR__ . '/..' . '/guzzlehttp/psr7/src/FnStream.php', + 'GuzzleHttp\\Psr7\\Header' => __DIR__ . '/..' . '/guzzlehttp/psr7/src/Header.php', + 'GuzzleHttp\\Psr7\\HttpFactory' => __DIR__ . '/..' . '/guzzlehttp/psr7/src/HttpFactory.php', + 'GuzzleHttp\\Psr7\\InflateStream' => __DIR__ . '/..' . '/guzzlehttp/psr7/src/InflateStream.php', + 'GuzzleHttp\\Psr7\\LazyOpenStream' => __DIR__ . '/..' . '/guzzlehttp/psr7/src/LazyOpenStream.php', + 'GuzzleHttp\\Psr7\\LimitStream' => __DIR__ . '/..' . '/guzzlehttp/psr7/src/LimitStream.php', + 'GuzzleHttp\\Psr7\\Message' => __DIR__ . '/..' . '/guzzlehttp/psr7/src/Message.php', + 'GuzzleHttp\\Psr7\\MessageTrait' => __DIR__ . '/..' . '/guzzlehttp/psr7/src/MessageTrait.php', + 'GuzzleHttp\\Psr7\\MimeType' => __DIR__ . '/..' . '/guzzlehttp/psr7/src/MimeType.php', + 'GuzzleHttp\\Psr7\\MultipartStream' => __DIR__ . '/..' . '/guzzlehttp/psr7/src/MultipartStream.php', + 'GuzzleHttp\\Psr7\\NoSeekStream' => __DIR__ . '/..' . '/guzzlehttp/psr7/src/NoSeekStream.php', + 'GuzzleHttp\\Psr7\\PumpStream' => __DIR__ . '/..' . '/guzzlehttp/psr7/src/PumpStream.php', + 'GuzzleHttp\\Psr7\\Query' => __DIR__ . '/..' . '/guzzlehttp/psr7/src/Query.php', + 'GuzzleHttp\\Psr7\\Request' => __DIR__ . '/..' . '/guzzlehttp/psr7/src/Request.php', + 'GuzzleHttp\\Psr7\\Response' => __DIR__ . '/..' . '/guzzlehttp/psr7/src/Response.php', + 'GuzzleHttp\\Psr7\\Rfc7230' => __DIR__ . '/..' . '/guzzlehttp/psr7/src/Rfc7230.php', + 'GuzzleHttp\\Psr7\\ServerRequest' => __DIR__ . '/..' . '/guzzlehttp/psr7/src/ServerRequest.php', + 'GuzzleHttp\\Psr7\\Stream' => __DIR__ . '/..' . '/guzzlehttp/psr7/src/Stream.php', + 'GuzzleHttp\\Psr7\\StreamDecoratorTrait' => __DIR__ . '/..' . '/guzzlehttp/psr7/src/StreamDecoratorTrait.php', + 'GuzzleHttp\\Psr7\\StreamWrapper' => __DIR__ . '/..' . '/guzzlehttp/psr7/src/StreamWrapper.php', + 'GuzzleHttp\\Psr7\\UploadedFile' => __DIR__ . '/..' . '/guzzlehttp/psr7/src/UploadedFile.php', + 'GuzzleHttp\\Psr7\\Uri' => __DIR__ . '/..' . '/guzzlehttp/psr7/src/Uri.php', + 'GuzzleHttp\\Psr7\\UriComparator' => __DIR__ . '/..' . '/guzzlehttp/psr7/src/UriComparator.php', + 'GuzzleHttp\\Psr7\\UriNormalizer' => __DIR__ . '/..' . '/guzzlehttp/psr7/src/UriNormalizer.php', + 'GuzzleHttp\\Psr7\\UriResolver' => __DIR__ . '/..' . '/guzzlehttp/psr7/src/UriResolver.php', + 'GuzzleHttp\\Psr7\\Utils' => __DIR__ . '/..' . '/guzzlehttp/psr7/src/Utils.php', + 'GuzzleHttp\\RedirectMiddleware' => __DIR__ . '/..' . '/guzzlehttp/guzzle/src/RedirectMiddleware.php', + 'GuzzleHttp\\RequestOptions' => __DIR__ . '/..' . '/guzzlehttp/guzzle/src/RequestOptions.php', + 'GuzzleHttp\\RetryMiddleware' => __DIR__ . '/..' . '/guzzlehttp/guzzle/src/RetryMiddleware.php', + 'GuzzleHttp\\TransferStats' => __DIR__ . '/..' . '/guzzlehttp/guzzle/src/TransferStats.php', + 'GuzzleHttp\\UriTemplate\\UriTemplate' => __DIR__ . '/..' . '/guzzlehttp/uri-template/src/UriTemplate.php', + 'GuzzleHttp\\Utils' => __DIR__ . '/..' . '/guzzlehttp/guzzle/src/Utils.php', + 'Http\\Adapter\\Guzzle7\\Client' => __DIR__ . '/..' . '/php-http/guzzle7-adapter/src/Client.php', + 'Http\\Adapter\\Guzzle7\\Exception\\UnexpectedValueException' => __DIR__ . '/..' . '/php-http/guzzle7-adapter/src/Exception/UnexpectedValueException.php', + 'Http\\Adapter\\Guzzle7\\Promise' => __DIR__ . '/..' . '/php-http/guzzle7-adapter/src/Promise.php', + 'Http\\Client\\Exception' => __DIR__ . '/..' . '/php-http/httplug/src/Exception.php', + 'Http\\Client\\Exception\\HttpException' => __DIR__ . '/..' . '/php-http/httplug/src/Exception/HttpException.php', + 'Http\\Client\\Exception\\NetworkException' => __DIR__ . '/..' . '/php-http/httplug/src/Exception/NetworkException.php', + 'Http\\Client\\Exception\\RequestAwareTrait' => __DIR__ . '/..' . '/php-http/httplug/src/Exception/RequestAwareTrait.php', + 'Http\\Client\\Exception\\RequestException' => __DIR__ . '/..' . '/php-http/httplug/src/Exception/RequestException.php', + 'Http\\Client\\Exception\\TransferException' => __DIR__ . '/..' . '/php-http/httplug/src/Exception/TransferException.php', + 'Http\\Client\\HttpAsyncClient' => __DIR__ . '/..' . '/php-http/httplug/src/HttpAsyncClient.php', + 'Http\\Client\\HttpClient' => __DIR__ . '/..' . '/php-http/httplug/src/HttpClient.php', + 'Http\\Client\\Promise\\HttpFulfilledPromise' => __DIR__ . '/..' . '/php-http/httplug/src/Promise/HttpFulfilledPromise.php', + 'Http\\Client\\Promise\\HttpRejectedPromise' => __DIR__ . '/..' . '/php-http/httplug/src/Promise/HttpRejectedPromise.php', + 'Http\\Promise\\FulfilledPromise' => __DIR__ . '/..' . '/php-http/promise/src/FulfilledPromise.php', + 'Http\\Promise\\Promise' => __DIR__ . '/..' . '/php-http/promise/src/Promise.php', + 'Http\\Promise\\RejectedPromise' => __DIR__ . '/..' . '/php-http/promise/src/RejectedPromise.php', + 'IPLib\\Address\\AddressInterface' => __DIR__ . '/..' . '/mlocati/ip-lib/src/Address/AddressInterface.php', + 'IPLib\\Address\\AssignedRange' => __DIR__ . '/..' . '/mlocati/ip-lib/src/Address/AssignedRange.php', + 'IPLib\\Address\\IPv4' => __DIR__ . '/..' . '/mlocati/ip-lib/src/Address/IPv4.php', + 'IPLib\\Address\\IPv6' => __DIR__ . '/..' . '/mlocati/ip-lib/src/Address/IPv6.php', + 'IPLib\\Address\\Type' => __DIR__ . '/..' . '/mlocati/ip-lib/src/Address/Type.php', + 'IPLib\\Factory' => __DIR__ . '/..' . '/mlocati/ip-lib/src/Factory.php', + 'IPLib\\ParseStringFlag' => __DIR__ . '/..' . '/mlocati/ip-lib/src/ParseStringFlag.php', + 'IPLib\\Range\\AbstractRange' => __DIR__ . '/..' . '/mlocati/ip-lib/src/Range/AbstractRange.php', + 'IPLib\\Range\\Pattern' => __DIR__ . '/..' . '/mlocati/ip-lib/src/Range/Pattern.php', + 'IPLib\\Range\\RangeInterface' => __DIR__ . '/..' . '/mlocati/ip-lib/src/Range/RangeInterface.php', + 'IPLib\\Range\\Single' => __DIR__ . '/..' . '/mlocati/ip-lib/src/Range/Single.php', + 'IPLib\\Range\\Subnet' => __DIR__ . '/..' . '/mlocati/ip-lib/src/Range/Subnet.php', + 'IPLib\\Range\\Type' => __DIR__ . '/..' . '/mlocati/ip-lib/src/Range/Type.php', + 'IPLib\\Service\\BinaryMath' => __DIR__ . '/..' . '/mlocati/ip-lib/src/Service/BinaryMath.php', + 'IPLib\\Service\\RangesFromBoundaryCalculator' => __DIR__ . '/..' . '/mlocati/ip-lib/src/Service/RangesFromBoundaryCalculator.php', + 'IPLib\\Service\\UnsignedIntegerMath' => __DIR__ . '/..' . '/mlocati/ip-lib/src/Service/UnsignedIntegerMath.php', + 'Icewind\\SMB\\ACL' => __DIR__ . '/..' . '/icewind/smb/src/ACL.php', + 'Icewind\\SMB\\AbstractServer' => __DIR__ . '/..' . '/icewind/smb/src/AbstractServer.php', + 'Icewind\\SMB\\AbstractShare' => __DIR__ . '/..' . '/icewind/smb/src/AbstractShare.php', + 'Icewind\\SMB\\AnonymousAuth' => __DIR__ . '/..' . '/icewind/smb/src/AnonymousAuth.php', + 'Icewind\\SMB\\BasicAuth' => __DIR__ . '/..' . '/icewind/smb/src/BasicAuth.php', + 'Icewind\\SMB\\Change' => __DIR__ . '/..' . '/icewind/smb/src/Change.php', + 'Icewind\\SMB\\Exception\\AccessDeniedException' => __DIR__ . '/..' . '/icewind/smb/src/Exception/AccessDeniedException.php', + 'Icewind\\SMB\\Exception\\AlreadyExistsException' => __DIR__ . '/..' . '/icewind/smb/src/Exception/AlreadyExistsException.php', + 'Icewind\\SMB\\Exception\\AuthenticationException' => __DIR__ . '/..' . '/icewind/smb/src/Exception/AuthenticationException.php', + 'Icewind\\SMB\\Exception\\ConnectException' => __DIR__ . '/..' . '/icewind/smb/src/Exception/ConnectException.php', + 'Icewind\\SMB\\Exception\\ConnectionAbortedException' => __DIR__ . '/..' . '/icewind/smb/src/Exception/ConnectionAbortedException.php', + 'Icewind\\SMB\\Exception\\ConnectionException' => __DIR__ . '/..' . '/icewind/smb/src/Exception/ConnectionException.php', + 'Icewind\\SMB\\Exception\\ConnectionRefusedException' => __DIR__ . '/..' . '/icewind/smb/src/Exception/ConnectionRefusedException.php', + 'Icewind\\SMB\\Exception\\ConnectionResetException' => __DIR__ . '/..' . '/icewind/smb/src/Exception/ConnectionResetException.php', + 'Icewind\\SMB\\Exception\\DependencyException' => __DIR__ . '/..' . '/icewind/smb/src/Exception/DependencyException.php', + 'Icewind\\SMB\\Exception\\Exception' => __DIR__ . '/..' . '/icewind/smb/src/Exception/Exception.php', + 'Icewind\\SMB\\Exception\\FileInUseException' => __DIR__ . '/..' . '/icewind/smb/src/Exception/FileInUseException.php', + 'Icewind\\SMB\\Exception\\ForbiddenException' => __DIR__ . '/..' . '/icewind/smb/src/Exception/ForbiddenException.php', + 'Icewind\\SMB\\Exception\\HostDownException' => __DIR__ . '/..' . '/icewind/smb/src/Exception/HostDownException.php', + 'Icewind\\SMB\\Exception\\InvalidArgumentException' => __DIR__ . '/..' . '/icewind/smb/src/Exception/InvalidArgumentException.php', + 'Icewind\\SMB\\Exception\\InvalidHostException' => __DIR__ . '/..' . '/icewind/smb/src/Exception/InvalidHostException.php', + 'Icewind\\SMB\\Exception\\InvalidParameterException' => __DIR__ . '/..' . '/icewind/smb/src/Exception/InvalidParameterException.php', + 'Icewind\\SMB\\Exception\\InvalidPathException' => __DIR__ . '/..' . '/icewind/smb/src/Exception/InvalidPathException.php', + 'Icewind\\SMB\\Exception\\InvalidRequestException' => __DIR__ . '/..' . '/icewind/smb/src/Exception/InvalidRequestException.php', + 'Icewind\\SMB\\Exception\\InvalidResourceException' => __DIR__ . '/..' . '/icewind/smb/src/Exception/InvalidResourceException.php', + 'Icewind\\SMB\\Exception\\InvalidTicket' => __DIR__ . '/..' . '/icewind/smb/src/Exception/InvalidTicket.php', + 'Icewind\\SMB\\Exception\\InvalidTypeException' => __DIR__ . '/..' . '/icewind/smb/src/Exception/InvalidTypeException.php', + 'Icewind\\SMB\\Exception\\NoLoginServerException' => __DIR__ . '/..' . '/icewind/smb/src/Exception/NoLoginServerException.php', + 'Icewind\\SMB\\Exception\\NoRouteToHostException' => __DIR__ . '/..' . '/icewind/smb/src/Exception/NoRouteToHostException.php', + 'Icewind\\SMB\\Exception\\NotEmptyException' => __DIR__ . '/..' . '/icewind/smb/src/Exception/NotEmptyException.php', + 'Icewind\\SMB\\Exception\\NotFoundException' => __DIR__ . '/..' . '/icewind/smb/src/Exception/NotFoundException.php', + 'Icewind\\SMB\\Exception\\OutOfSpaceException' => __DIR__ . '/..' . '/icewind/smb/src/Exception/OutOfSpaceException.php', + 'Icewind\\SMB\\Exception\\RevisionMismatchException' => __DIR__ . '/..' . '/icewind/smb/src/Exception/RevisionMismatchException.php', + 'Icewind\\SMB\\Exception\\TimedOutException' => __DIR__ . '/..' . '/icewind/smb/src/Exception/TimedOutException.php', + 'Icewind\\SMB\\IAuth' => __DIR__ . '/..' . '/icewind/smb/src/IAuth.php', + 'Icewind\\SMB\\IFileInfo' => __DIR__ . '/..' . '/icewind/smb/src/IFileInfo.php', + 'Icewind\\SMB\\INotifyHandler' => __DIR__ . '/..' . '/icewind/smb/src/INotifyHandler.php', + 'Icewind\\SMB\\IOptions' => __DIR__ . '/..' . '/icewind/smb/src/IOptions.php', + 'Icewind\\SMB\\IServer' => __DIR__ . '/..' . '/icewind/smb/src/IServer.php', + 'Icewind\\SMB\\IShare' => __DIR__ . '/..' . '/icewind/smb/src/IShare.php', + 'Icewind\\SMB\\ISystem' => __DIR__ . '/..' . '/icewind/smb/src/ISystem.php', + 'Icewind\\SMB\\ITimeZoneProvider' => __DIR__ . '/..' . '/icewind/smb/src/ITimeZoneProvider.php', + 'Icewind\\SMB\\KerberosApacheAuth' => __DIR__ . '/..' . '/icewind/smb/src/KerberosApacheAuth.php', + 'Icewind\\SMB\\KerberosAuth' => __DIR__ . '/..' . '/icewind/smb/src/KerberosAuth.php', + 'Icewind\\SMB\\KerberosTicket' => __DIR__ . '/..' . '/icewind/smb/src/KerberosTicket.php', + 'Icewind\\SMB\\Native\\NativeFileInfo' => __DIR__ . '/..' . '/icewind/smb/src/Native/NativeFileInfo.php', + 'Icewind\\SMB\\Native\\NativeReadStream' => __DIR__ . '/..' . '/icewind/smb/src/Native/NativeReadStream.php', + 'Icewind\\SMB\\Native\\NativeServer' => __DIR__ . '/..' . '/icewind/smb/src/Native/NativeServer.php', + 'Icewind\\SMB\\Native\\NativeShare' => __DIR__ . '/..' . '/icewind/smb/src/Native/NativeShare.php', + 'Icewind\\SMB\\Native\\NativeState' => __DIR__ . '/..' . '/icewind/smb/src/Native/NativeState.php', + 'Icewind\\SMB\\Native\\NativeStream' => __DIR__ . '/..' . '/icewind/smb/src/Native/NativeStream.php', + 'Icewind\\SMB\\Native\\NativeWriteStream' => __DIR__ . '/..' . '/icewind/smb/src/Native/NativeWriteStream.php', + 'Icewind\\SMB\\Options' => __DIR__ . '/..' . '/icewind/smb/src/Options.php', + 'Icewind\\SMB\\ServerFactory' => __DIR__ . '/..' . '/icewind/smb/src/ServerFactory.php', + 'Icewind\\SMB\\StringBuffer' => __DIR__ . '/..' . '/icewind/smb/src/StringBuffer.php', + 'Icewind\\SMB\\System' => __DIR__ . '/..' . '/icewind/smb/src/System.php', + 'Icewind\\SMB\\TimeZoneProvider' => __DIR__ . '/..' . '/icewind/smb/src/TimeZoneProvider.php', + 'Icewind\\SMB\\Wrapped\\Connection' => __DIR__ . '/..' . '/icewind/smb/src/Wrapped/Connection.php', + 'Icewind\\SMB\\Wrapped\\ErrorCodes' => __DIR__ . '/..' . '/icewind/smb/src/Wrapped/ErrorCodes.php', + 'Icewind\\SMB\\Wrapped\\FileInfo' => __DIR__ . '/..' . '/icewind/smb/src/Wrapped/FileInfo.php', + 'Icewind\\SMB\\Wrapped\\NotifyHandler' => __DIR__ . '/..' . '/icewind/smb/src/Wrapped/NotifyHandler.php', + 'Icewind\\SMB\\Wrapped\\Parser' => __DIR__ . '/..' . '/icewind/smb/src/Wrapped/Parser.php', + 'Icewind\\SMB\\Wrapped\\RawConnection' => __DIR__ . '/..' . '/icewind/smb/src/Wrapped/RawConnection.php', + 'Icewind\\SMB\\Wrapped\\Server' => __DIR__ . '/..' . '/icewind/smb/src/Wrapped/Server.php', + 'Icewind\\SMB\\Wrapped\\Share' => __DIR__ . '/..' . '/icewind/smb/src/Wrapped/Share.php', + 'Icewind\\Streams\\CallbackWrapper' => __DIR__ . '/..' . '/icewind/streams/src/CallbackWrapper.php', + 'Icewind\\Streams\\CountWrapper' => __DIR__ . '/..' . '/icewind/streams/src/CountWrapper.php', + 'Icewind\\Streams\\Directory' => __DIR__ . '/..' . '/icewind/streams/src/Directory.php', + 'Icewind\\Streams\\DirectoryFilter' => __DIR__ . '/..' . '/icewind/streams/src/DirectoryFilter.php', + 'Icewind\\Streams\\DirectoryWrapper' => __DIR__ . '/..' . '/icewind/streams/src/DirectoryWrapper.php', + 'Icewind\\Streams\\File' => __DIR__ . '/..' . '/icewind/streams/src/File.php', + 'Icewind\\Streams\\HashWrapper' => __DIR__ . '/..' . '/icewind/streams/src/HashWrapper.php', + 'Icewind\\Streams\\IteratorDirectory' => __DIR__ . '/..' . '/icewind/streams/src/IteratorDirectory.php', + 'Icewind\\Streams\\NullWrapper' => __DIR__ . '/..' . '/icewind/streams/src/NullWrapper.php', + 'Icewind\\Streams\\Path' => __DIR__ . '/..' . '/icewind/streams/src/Path.php', + 'Icewind\\Streams\\PathWrapper' => __DIR__ . '/..' . '/icewind/streams/src/PathWrapper.php', + 'Icewind\\Streams\\ReadHashWrapper' => __DIR__ . '/..' . '/icewind/streams/src/ReadHashWrapper.php', + 'Icewind\\Streams\\RetryWrapper' => __DIR__ . '/..' . '/icewind/streams/src/RetryWrapper.php', + 'Icewind\\Streams\\SeekableWrapper' => __DIR__ . '/..' . '/icewind/streams/src/SeekableWrapper.php', + 'Icewind\\Streams\\Url' => __DIR__ . '/..' . '/icewind/streams/src/Url.php', + 'Icewind\\Streams\\UrlCallback' => __DIR__ . '/..' . '/icewind/streams/src/UrlCallback.php', + 'Icewind\\Streams\\Wrapper' => __DIR__ . '/..' . '/icewind/streams/src/Wrapper.php', + 'Icewind\\Streams\\WrapperHandler' => __DIR__ . '/..' . '/icewind/streams/src/WrapperHandler.php', + 'Icewind\\Streams\\WriteHashWrapper' => __DIR__ . '/..' . '/icewind/streams/src/WriteHashWrapper.php', + 'JmesPath\\AstRuntime' => __DIR__ . '/..' . '/mtdowling/jmespath.php/src/AstRuntime.php', + 'JmesPath\\CompilerRuntime' => __DIR__ . '/..' . '/mtdowling/jmespath.php/src/CompilerRuntime.php', + 'JmesPath\\DebugRuntime' => __DIR__ . '/..' . '/mtdowling/jmespath.php/src/DebugRuntime.php', + 'JmesPath\\Env' => __DIR__ . '/..' . '/mtdowling/jmespath.php/src/Env.php', + 'JmesPath\\FnDispatcher' => __DIR__ . '/..' . '/mtdowling/jmespath.php/src/FnDispatcher.php', + 'JmesPath\\Lexer' => __DIR__ . '/..' . '/mtdowling/jmespath.php/src/Lexer.php', + 'JmesPath\\Parser' => __DIR__ . '/..' . '/mtdowling/jmespath.php/src/Parser.php', + 'JmesPath\\SyntaxErrorException' => __DIR__ . '/..' . '/mtdowling/jmespath.php/src/SyntaxErrorException.php', + 'JmesPath\\TreeCompiler' => __DIR__ . '/..' . '/mtdowling/jmespath.php/src/TreeCompiler.php', + 'JmesPath\\TreeInterpreter' => __DIR__ . '/..' . '/mtdowling/jmespath.php/src/TreeInterpreter.php', + 'JmesPath\\Utils' => __DIR__ . '/..' . '/mtdowling/jmespath.php/src/Utils.php', + 'JsonSchema\\ConstraintError' => __DIR__ . '/..' . '/justinrainbow/json-schema/src/JsonSchema/ConstraintError.php', + 'JsonSchema\\Constraints\\BaseConstraint' => __DIR__ . '/..' . '/justinrainbow/json-schema/src/JsonSchema/Constraints/BaseConstraint.php', + 'JsonSchema\\Constraints\\CollectionConstraint' => __DIR__ . '/..' . '/justinrainbow/json-schema/src/JsonSchema/Constraints/CollectionConstraint.php', + 'JsonSchema\\Constraints\\ConstConstraint' => __DIR__ . '/..' . '/justinrainbow/json-schema/src/JsonSchema/Constraints/ConstConstraint.php', + 'JsonSchema\\Constraints\\Constraint' => __DIR__ . '/..' . '/justinrainbow/json-schema/src/JsonSchema/Constraints/Constraint.php', + 'JsonSchema\\Constraints\\ConstraintInterface' => __DIR__ . '/..' . '/justinrainbow/json-schema/src/JsonSchema/Constraints/ConstraintInterface.php', + 'JsonSchema\\Constraints\\EnumConstraint' => __DIR__ . '/..' . '/justinrainbow/json-schema/src/JsonSchema/Constraints/EnumConstraint.php', + 'JsonSchema\\Constraints\\Factory' => __DIR__ . '/..' . '/justinrainbow/json-schema/src/JsonSchema/Constraints/Factory.php', + 'JsonSchema\\Constraints\\FormatConstraint' => __DIR__ . '/..' . '/justinrainbow/json-schema/src/JsonSchema/Constraints/FormatConstraint.php', + 'JsonSchema\\Constraints\\NumberConstraint' => __DIR__ . '/..' . '/justinrainbow/json-schema/src/JsonSchema/Constraints/NumberConstraint.php', + 'JsonSchema\\Constraints\\ObjectConstraint' => __DIR__ . '/..' . '/justinrainbow/json-schema/src/JsonSchema/Constraints/ObjectConstraint.php', + 'JsonSchema\\Constraints\\SchemaConstraint' => __DIR__ . '/..' . '/justinrainbow/json-schema/src/JsonSchema/Constraints/SchemaConstraint.php', + 'JsonSchema\\Constraints\\StringConstraint' => __DIR__ . '/..' . '/justinrainbow/json-schema/src/JsonSchema/Constraints/StringConstraint.php', + 'JsonSchema\\Constraints\\TypeCheck\\LooseTypeCheck' => __DIR__ . '/..' . '/justinrainbow/json-schema/src/JsonSchema/Constraints/TypeCheck/LooseTypeCheck.php', + 'JsonSchema\\Constraints\\TypeCheck\\StrictTypeCheck' => __DIR__ . '/..' . '/justinrainbow/json-schema/src/JsonSchema/Constraints/TypeCheck/StrictTypeCheck.php', + 'JsonSchema\\Constraints\\TypeCheck\\TypeCheckInterface' => __DIR__ . '/..' . '/justinrainbow/json-schema/src/JsonSchema/Constraints/TypeCheck/TypeCheckInterface.php', + 'JsonSchema\\Constraints\\TypeConstraint' => __DIR__ . '/..' . '/justinrainbow/json-schema/src/JsonSchema/Constraints/TypeConstraint.php', + 'JsonSchema\\Constraints\\UndefinedConstraint' => __DIR__ . '/..' . '/justinrainbow/json-schema/src/JsonSchema/Constraints/UndefinedConstraint.php', + 'JsonSchema\\Entity\\JsonPointer' => __DIR__ . '/..' . '/justinrainbow/json-schema/src/JsonSchema/Entity/JsonPointer.php', + 'JsonSchema\\Enum' => __DIR__ . '/..' . '/justinrainbow/json-schema/src/JsonSchema/Enum.php', + 'JsonSchema\\Exception\\ExceptionInterface' => __DIR__ . '/..' . '/justinrainbow/json-schema/src/JsonSchema/Exception/ExceptionInterface.php', + 'JsonSchema\\Exception\\InvalidArgumentException' => __DIR__ . '/..' . '/justinrainbow/json-schema/src/JsonSchema/Exception/InvalidArgumentException.php', + 'JsonSchema\\Exception\\InvalidConfigException' => __DIR__ . '/..' . '/justinrainbow/json-schema/src/JsonSchema/Exception/InvalidConfigException.php', + 'JsonSchema\\Exception\\InvalidSchemaException' => __DIR__ . '/..' . '/justinrainbow/json-schema/src/JsonSchema/Exception/InvalidSchemaException.php', + 'JsonSchema\\Exception\\InvalidSchemaMediaTypeException' => __DIR__ . '/..' . '/justinrainbow/json-schema/src/JsonSchema/Exception/InvalidSchemaMediaTypeException.php', + 'JsonSchema\\Exception\\InvalidSourceUriException' => __DIR__ . '/..' . '/justinrainbow/json-schema/src/JsonSchema/Exception/InvalidSourceUriException.php', + 'JsonSchema\\Exception\\JsonDecodingException' => __DIR__ . '/..' . '/justinrainbow/json-schema/src/JsonSchema/Exception/JsonDecodingException.php', + 'JsonSchema\\Exception\\ResourceNotFoundException' => __DIR__ . '/..' . '/justinrainbow/json-schema/src/JsonSchema/Exception/ResourceNotFoundException.php', + 'JsonSchema\\Exception\\RuntimeException' => __DIR__ . '/..' . '/justinrainbow/json-schema/src/JsonSchema/Exception/RuntimeException.php', + 'JsonSchema\\Exception\\UnresolvableJsonPointerException' => __DIR__ . '/..' . '/justinrainbow/json-schema/src/JsonSchema/Exception/UnresolvableJsonPointerException.php', + 'JsonSchema\\Exception\\UriResolverException' => __DIR__ . '/..' . '/justinrainbow/json-schema/src/JsonSchema/Exception/UriResolverException.php', + 'JsonSchema\\Exception\\ValidationException' => __DIR__ . '/..' . '/justinrainbow/json-schema/src/JsonSchema/Exception/ValidationException.php', + 'JsonSchema\\Iterator\\ObjectIterator' => __DIR__ . '/..' . '/justinrainbow/json-schema/src/JsonSchema/Iterator/ObjectIterator.php', + 'JsonSchema\\Rfc3339' => __DIR__ . '/..' . '/justinrainbow/json-schema/src/JsonSchema/Rfc3339.php', + 'JsonSchema\\SchemaStorage' => __DIR__ . '/..' . '/justinrainbow/json-schema/src/JsonSchema/SchemaStorage.php', + 'JsonSchema\\SchemaStorageInterface' => __DIR__ . '/..' . '/justinrainbow/json-schema/src/JsonSchema/SchemaStorageInterface.php', + 'JsonSchema\\Tool\\DeepComparer' => __DIR__ . '/..' . '/justinrainbow/json-schema/src/JsonSchema/Tool/DeepComparer.php', + 'JsonSchema\\Tool\\DeepCopy' => __DIR__ . '/..' . '/justinrainbow/json-schema/src/JsonSchema/Tool/DeepCopy.php', + 'JsonSchema\\Tool\\Validator\\RelativeReferenceValidator' => __DIR__ . '/..' . '/justinrainbow/json-schema/src/JsonSchema/Tool/Validator/RelativeReferenceValidator.php', + 'JsonSchema\\Tool\\Validator\\UriValidator' => __DIR__ . '/..' . '/justinrainbow/json-schema/src/JsonSchema/Tool/Validator/UriValidator.php', + 'JsonSchema\\UriResolverInterface' => __DIR__ . '/..' . '/justinrainbow/json-schema/src/JsonSchema/UriResolverInterface.php', + 'JsonSchema\\UriRetrieverInterface' => __DIR__ . '/..' . '/justinrainbow/json-schema/src/JsonSchema/UriRetrieverInterface.php', + 'JsonSchema\\Uri\\Retrievers\\AbstractRetriever' => __DIR__ . '/..' . '/justinrainbow/json-schema/src/JsonSchema/Uri/Retrievers/AbstractRetriever.php', + 'JsonSchema\\Uri\\Retrievers\\Curl' => __DIR__ . '/..' . '/justinrainbow/json-schema/src/JsonSchema/Uri/Retrievers/Curl.php', + 'JsonSchema\\Uri\\Retrievers\\FileGetContents' => __DIR__ . '/..' . '/justinrainbow/json-schema/src/JsonSchema/Uri/Retrievers/FileGetContents.php', + 'JsonSchema\\Uri\\Retrievers\\PredefinedArray' => __DIR__ . '/..' . '/justinrainbow/json-schema/src/JsonSchema/Uri/Retrievers/PredefinedArray.php', + 'JsonSchema\\Uri\\Retrievers\\UriRetrieverInterface' => __DIR__ . '/..' . '/justinrainbow/json-schema/src/JsonSchema/Uri/Retrievers/UriRetrieverInterface.php', + 'JsonSchema\\Uri\\UriResolver' => __DIR__ . '/..' . '/justinrainbow/json-schema/src/JsonSchema/Uri/UriResolver.php', + 'JsonSchema\\Uri\\UriRetriever' => __DIR__ . '/..' . '/justinrainbow/json-schema/src/JsonSchema/Uri/UriRetriever.php', + 'JsonSchema\\Validator' => __DIR__ . '/..' . '/justinrainbow/json-schema/src/JsonSchema/Validator.php', + 'Laravel\\SerializableClosure\\Contracts\\Serializable' => __DIR__ . '/..' . '/laravel/serializable-closure/src/Contracts/Serializable.php', + 'Laravel\\SerializableClosure\\Contracts\\Signer' => __DIR__ . '/..' . '/laravel/serializable-closure/src/Contracts/Signer.php', + 'Laravel\\SerializableClosure\\Exceptions\\InvalidSignatureException' => __DIR__ . '/..' . '/laravel/serializable-closure/src/Exceptions/InvalidSignatureException.php', + 'Laravel\\SerializableClosure\\Exceptions\\MissingSecretKeyException' => __DIR__ . '/..' . '/laravel/serializable-closure/src/Exceptions/MissingSecretKeyException.php', + 'Laravel\\SerializableClosure\\Exceptions\\PhpVersionNotSupportedException' => __DIR__ . '/..' . '/laravel/serializable-closure/src/Exceptions/PhpVersionNotSupportedException.php', + 'Laravel\\SerializableClosure\\SerializableClosure' => __DIR__ . '/..' . '/laravel/serializable-closure/src/SerializableClosure.php', + 'Laravel\\SerializableClosure\\Serializers\\Native' => __DIR__ . '/..' . '/laravel/serializable-closure/src/Serializers/Native.php', + 'Laravel\\SerializableClosure\\Serializers\\Signed' => __DIR__ . '/..' . '/laravel/serializable-closure/src/Serializers/Signed.php', + 'Laravel\\SerializableClosure\\Signers\\Hmac' => __DIR__ . '/..' . '/laravel/serializable-closure/src/Signers/Hmac.php', + 'Laravel\\SerializableClosure\\Support\\ClosureScope' => __DIR__ . '/..' . '/laravel/serializable-closure/src/Support/ClosureScope.php', + 'Laravel\\SerializableClosure\\Support\\ClosureStream' => __DIR__ . '/..' . '/laravel/serializable-closure/src/Support/ClosureStream.php', + 'Laravel\\SerializableClosure\\Support\\ReflectionClosure' => __DIR__ . '/..' . '/laravel/serializable-closure/src/Support/ReflectionClosure.php', + 'Laravel\\SerializableClosure\\Support\\SelfReference' => __DIR__ . '/..' . '/laravel/serializable-closure/src/Support/SelfReference.php', + 'Laravel\\SerializableClosure\\UnsignedSerializableClosure' => __DIR__ . '/..' . '/laravel/serializable-closure/src/UnsignedSerializableClosure.php', + 'Lcobucci\\Clock\\Clock' => __DIR__ . '/..' . '/lcobucci/clock/src/Clock.php', + 'Lcobucci\\Clock\\FrozenClock' => __DIR__ . '/..' . '/lcobucci/clock/src/FrozenClock.php', + 'Lcobucci\\Clock\\SystemClock' => __DIR__ . '/..' . '/lcobucci/clock/src/SystemClock.php', + 'MabeEnum\\Enum' => __DIR__ . '/..' . '/marc-mabe/php-enum/src/Enum.php', + 'MabeEnum\\EnumMap' => __DIR__ . '/..' . '/marc-mabe/php-enum/src/EnumMap.php', + 'MabeEnum\\EnumSerializableTrait' => __DIR__ . '/..' . '/marc-mabe/php-enum/src/EnumSerializableTrait.php', + 'MabeEnum\\EnumSet' => __DIR__ . '/..' . '/marc-mabe/php-enum/src/EnumSet.php', + 'Masterminds\\HTML5' => __DIR__ . '/..' . '/masterminds/html5/src/HTML5.php', + 'Masterminds\\HTML5\\Elements' => __DIR__ . '/..' . '/masterminds/html5/src/HTML5/Elements.php', + 'Masterminds\\HTML5\\Entities' => __DIR__ . '/..' . '/masterminds/html5/src/HTML5/Entities.php', + 'Masterminds\\HTML5\\Exception' => __DIR__ . '/..' . '/masterminds/html5/src/HTML5/Exception.php', + 'Masterminds\\HTML5\\InstructionProcessor' => __DIR__ . '/..' . '/masterminds/html5/src/HTML5/InstructionProcessor.php', + 'Masterminds\\HTML5\\Parser\\CharacterReference' => __DIR__ . '/..' . '/masterminds/html5/src/HTML5/Parser/CharacterReference.php', + 'Masterminds\\HTML5\\Parser\\DOMTreeBuilder' => __DIR__ . '/..' . '/masterminds/html5/src/HTML5/Parser/DOMTreeBuilder.php', + 'Masterminds\\HTML5\\Parser\\EventHandler' => __DIR__ . '/..' . '/masterminds/html5/src/HTML5/Parser/EventHandler.php', + 'Masterminds\\HTML5\\Parser\\FileInputStream' => __DIR__ . '/..' . '/masterminds/html5/src/HTML5/Parser/FileInputStream.php', + 'Masterminds\\HTML5\\Parser\\InputStream' => __DIR__ . '/..' . '/masterminds/html5/src/HTML5/Parser/InputStream.php', + 'Masterminds\\HTML5\\Parser\\ParseError' => __DIR__ . '/..' . '/masterminds/html5/src/HTML5/Parser/ParseError.php', + 'Masterminds\\HTML5\\Parser\\Scanner' => __DIR__ . '/..' . '/masterminds/html5/src/HTML5/Parser/Scanner.php', + 'Masterminds\\HTML5\\Parser\\StringInputStream' => __DIR__ . '/..' . '/masterminds/html5/src/HTML5/Parser/StringInputStream.php', + 'Masterminds\\HTML5\\Parser\\Tokenizer' => __DIR__ . '/..' . '/masterminds/html5/src/HTML5/Parser/Tokenizer.php', + 'Masterminds\\HTML5\\Parser\\TreeBuildingRules' => __DIR__ . '/..' . '/masterminds/html5/src/HTML5/Parser/TreeBuildingRules.php', + 'Masterminds\\HTML5\\Parser\\UTF8Utils' => __DIR__ . '/..' . '/masterminds/html5/src/HTML5/Parser/UTF8Utils.php', + 'Masterminds\\HTML5\\Serializer\\HTML5Entities' => __DIR__ . '/..' . '/masterminds/html5/src/HTML5/Serializer/HTML5Entities.php', + 'Masterminds\\HTML5\\Serializer\\OutputRules' => __DIR__ . '/..' . '/masterminds/html5/src/HTML5/Serializer/OutputRules.php', + 'Masterminds\\HTML5\\Serializer\\RulesInterface' => __DIR__ . '/..' . '/masterminds/html5/src/HTML5/Serializer/RulesInterface.php', + 'Masterminds\\HTML5\\Serializer\\Traverser' => __DIR__ . '/..' . '/masterminds/html5/src/HTML5/Serializer/Traverser.php', + 'Mexitek\\PHPColors\\Color' => __DIR__ . '/..' . '/mexitek/phpcolors/src/Mexitek/PHPColors/Color.php', + 'MicrosoftAzure\\Storage\\Blob\\BlobRestProxy' => __DIR__ . '/..' . '/microsoft/azure-storage-blob/src/Blob/BlobRestProxy.php', + 'MicrosoftAzure\\Storage\\Blob\\BlobSharedAccessSignatureHelper' => __DIR__ . '/..' . '/microsoft/azure-storage-blob/src/Blob/BlobSharedAccessSignatureHelper.php', + 'MicrosoftAzure\\Storage\\Blob\\Internal\\BlobResources' => __DIR__ . '/..' . '/microsoft/azure-storage-blob/src/Blob/Internal/BlobResources.php', + 'MicrosoftAzure\\Storage\\Blob\\Internal\\IBlob' => __DIR__ . '/..' . '/microsoft/azure-storage-blob/src/Blob/Internal/IBlob.php', + 'MicrosoftAzure\\Storage\\Blob\\Models\\AccessCondition' => __DIR__ . '/..' . '/microsoft/azure-storage-blob/src/Blob/Models/AccessCondition.php', + 'MicrosoftAzure\\Storage\\Blob\\Models\\AccessTierTrait' => __DIR__ . '/..' . '/microsoft/azure-storage-blob/src/Blob/Models/AccessTierTrait.php', + 'MicrosoftAzure\\Storage\\Blob\\Models\\AppendBlockOptions' => __DIR__ . '/..' . '/microsoft/azure-storage-blob/src/Blob/Models/AppendBlockOptions.php', + 'MicrosoftAzure\\Storage\\Blob\\Models\\AppendBlockResult' => __DIR__ . '/..' . '/microsoft/azure-storage-blob/src/Blob/Models/AppendBlockResult.php', + 'MicrosoftAzure\\Storage\\Blob\\Models\\Blob' => __DIR__ . '/..' . '/microsoft/azure-storage-blob/src/Blob/Models/Blob.php', + 'MicrosoftAzure\\Storage\\Blob\\Models\\BlobAccessPolicy' => __DIR__ . '/..' . '/microsoft/azure-storage-blob/src/Blob/Models/BlobAccessPolicy.php', + 'MicrosoftAzure\\Storage\\Blob\\Models\\BlobBlockType' => __DIR__ . '/..' . '/microsoft/azure-storage-blob/src/Blob/Models/BlobBlockType.php', + 'MicrosoftAzure\\Storage\\Blob\\Models\\BlobPrefix' => __DIR__ . '/..' . '/microsoft/azure-storage-blob/src/Blob/Models/BlobPrefix.php', + 'MicrosoftAzure\\Storage\\Blob\\Models\\BlobProperties' => __DIR__ . '/..' . '/microsoft/azure-storage-blob/src/Blob/Models/BlobProperties.php', + 'MicrosoftAzure\\Storage\\Blob\\Models\\BlobServiceOptions' => __DIR__ . '/..' . '/microsoft/azure-storage-blob/src/Blob/Models/BlobServiceOptions.php', + 'MicrosoftAzure\\Storage\\Blob\\Models\\BlobType' => __DIR__ . '/..' . '/microsoft/azure-storage-blob/src/Blob/Models/BlobType.php', + 'MicrosoftAzure\\Storage\\Blob\\Models\\Block' => __DIR__ . '/..' . '/microsoft/azure-storage-blob/src/Blob/Models/Block.php', + 'MicrosoftAzure\\Storage\\Blob\\Models\\BlockList' => __DIR__ . '/..' . '/microsoft/azure-storage-blob/src/Blob/Models/BlockList.php', + 'MicrosoftAzure\\Storage\\Blob\\Models\\BreakLeaseResult' => __DIR__ . '/..' . '/microsoft/azure-storage-blob/src/Blob/Models/BreakLeaseResult.php', + 'MicrosoftAzure\\Storage\\Blob\\Models\\CommitBlobBlocksOptions' => __DIR__ . '/..' . '/microsoft/azure-storage-blob/src/Blob/Models/CommitBlobBlocksOptions.php', + 'MicrosoftAzure\\Storage\\Blob\\Models\\Container' => __DIR__ . '/..' . '/microsoft/azure-storage-blob/src/Blob/Models/Container.php', + 'MicrosoftAzure\\Storage\\Blob\\Models\\ContainerACL' => __DIR__ . '/..' . '/microsoft/azure-storage-blob/src/Blob/Models/ContainerACL.php', + 'MicrosoftAzure\\Storage\\Blob\\Models\\ContainerAccessPolicy' => __DIR__ . '/..' . '/microsoft/azure-storage-blob/src/Blob/Models/ContainerAccessPolicy.php', + 'MicrosoftAzure\\Storage\\Blob\\Models\\ContainerProperties' => __DIR__ . '/..' . '/microsoft/azure-storage-blob/src/Blob/Models/ContainerProperties.php', + 'MicrosoftAzure\\Storage\\Blob\\Models\\CopyBlobFromURLOptions' => __DIR__ . '/..' . '/microsoft/azure-storage-blob/src/Blob/Models/CopyBlobFromURLOptions.php', + 'MicrosoftAzure\\Storage\\Blob\\Models\\CopyBlobOptions' => __DIR__ . '/..' . '/microsoft/azure-storage-blob/src/Blob/Models/CopyBlobOptions.php', + 'MicrosoftAzure\\Storage\\Blob\\Models\\CopyBlobResult' => __DIR__ . '/..' . '/microsoft/azure-storage-blob/src/Blob/Models/CopyBlobResult.php', + 'MicrosoftAzure\\Storage\\Blob\\Models\\CopyState' => __DIR__ . '/..' . '/microsoft/azure-storage-blob/src/Blob/Models/CopyState.php', + 'MicrosoftAzure\\Storage\\Blob\\Models\\CreateBlobBlockOptions' => __DIR__ . '/..' . '/microsoft/azure-storage-blob/src/Blob/Models/CreateBlobBlockOptions.php', + 'MicrosoftAzure\\Storage\\Blob\\Models\\CreateBlobOptions' => __DIR__ . '/..' . '/microsoft/azure-storage-blob/src/Blob/Models/CreateBlobOptions.php', + 'MicrosoftAzure\\Storage\\Blob\\Models\\CreateBlobPagesOptions' => __DIR__ . '/..' . '/microsoft/azure-storage-blob/src/Blob/Models/CreateBlobPagesOptions.php', + 'MicrosoftAzure\\Storage\\Blob\\Models\\CreateBlobPagesResult' => __DIR__ . '/..' . '/microsoft/azure-storage-blob/src/Blob/Models/CreateBlobPagesResult.php', + 'MicrosoftAzure\\Storage\\Blob\\Models\\CreateBlobSnapshotOptions' => __DIR__ . '/..' . '/microsoft/azure-storage-blob/src/Blob/Models/CreateBlobSnapshotOptions.php', + 'MicrosoftAzure\\Storage\\Blob\\Models\\CreateBlobSnapshotResult' => __DIR__ . '/..' . '/microsoft/azure-storage-blob/src/Blob/Models/CreateBlobSnapshotResult.php', + 'MicrosoftAzure\\Storage\\Blob\\Models\\CreateBlockBlobOptions' => __DIR__ . '/..' . '/microsoft/azure-storage-blob/src/Blob/Models/CreateBlockBlobOptions.php', + 'MicrosoftAzure\\Storage\\Blob\\Models\\CreateContainerOptions' => __DIR__ . '/..' . '/microsoft/azure-storage-blob/src/Blob/Models/CreateContainerOptions.php', + 'MicrosoftAzure\\Storage\\Blob\\Models\\CreatePageBlobFromContentOptions' => __DIR__ . '/..' . '/microsoft/azure-storage-blob/src/Blob/Models/CreatePageBlobFromContentOptions.php', + 'MicrosoftAzure\\Storage\\Blob\\Models\\CreatePageBlobOptions' => __DIR__ . '/..' . '/microsoft/azure-storage-blob/src/Blob/Models/CreatePageBlobOptions.php', + 'MicrosoftAzure\\Storage\\Blob\\Models\\DeleteBlobOptions' => __DIR__ . '/..' . '/microsoft/azure-storage-blob/src/Blob/Models/DeleteBlobOptions.php', + 'MicrosoftAzure\\Storage\\Blob\\Models\\GetBlobMetadataOptions' => __DIR__ . '/..' . '/microsoft/azure-storage-blob/src/Blob/Models/GetBlobMetadataOptions.php', + 'MicrosoftAzure\\Storage\\Blob\\Models\\GetBlobMetadataResult' => __DIR__ . '/..' . '/microsoft/azure-storage-blob/src/Blob/Models/GetBlobMetadataResult.php', + 'MicrosoftAzure\\Storage\\Blob\\Models\\GetBlobOptions' => __DIR__ . '/..' . '/microsoft/azure-storage-blob/src/Blob/Models/GetBlobOptions.php', + 'MicrosoftAzure\\Storage\\Blob\\Models\\GetBlobPropertiesOptions' => __DIR__ . '/..' . '/microsoft/azure-storage-blob/src/Blob/Models/GetBlobPropertiesOptions.php', + 'MicrosoftAzure\\Storage\\Blob\\Models\\GetBlobPropertiesResult' => __DIR__ . '/..' . '/microsoft/azure-storage-blob/src/Blob/Models/GetBlobPropertiesResult.php', + 'MicrosoftAzure\\Storage\\Blob\\Models\\GetBlobResult' => __DIR__ . '/..' . '/microsoft/azure-storage-blob/src/Blob/Models/GetBlobResult.php', + 'MicrosoftAzure\\Storage\\Blob\\Models\\GetContainerACLResult' => __DIR__ . '/..' . '/microsoft/azure-storage-blob/src/Blob/Models/GetContainerACLResult.php', + 'MicrosoftAzure\\Storage\\Blob\\Models\\GetContainerPropertiesResult' => __DIR__ . '/..' . '/microsoft/azure-storage-blob/src/Blob/Models/GetContainerPropertiesResult.php', + 'MicrosoftAzure\\Storage\\Blob\\Models\\LeaseMode' => __DIR__ . '/..' . '/microsoft/azure-storage-blob/src/Blob/Models/LeaseMode.php', + 'MicrosoftAzure\\Storage\\Blob\\Models\\LeaseResult' => __DIR__ . '/..' . '/microsoft/azure-storage-blob/src/Blob/Models/LeaseResult.php', + 'MicrosoftAzure\\Storage\\Blob\\Models\\ListBlobBlocksOptions' => __DIR__ . '/..' . '/microsoft/azure-storage-blob/src/Blob/Models/ListBlobBlocksOptions.php', + 'MicrosoftAzure\\Storage\\Blob\\Models\\ListBlobBlocksResult' => __DIR__ . '/..' . '/microsoft/azure-storage-blob/src/Blob/Models/ListBlobBlocksResult.php', + 'MicrosoftAzure\\Storage\\Blob\\Models\\ListBlobsOptions' => __DIR__ . '/..' . '/microsoft/azure-storage-blob/src/Blob/Models/ListBlobsOptions.php', + 'MicrosoftAzure\\Storage\\Blob\\Models\\ListBlobsResult' => __DIR__ . '/..' . '/microsoft/azure-storage-blob/src/Blob/Models/ListBlobsResult.php', + 'MicrosoftAzure\\Storage\\Blob\\Models\\ListContainersOptions' => __DIR__ . '/..' . '/microsoft/azure-storage-blob/src/Blob/Models/ListContainersOptions.php', + 'MicrosoftAzure\\Storage\\Blob\\Models\\ListContainersResult' => __DIR__ . '/..' . '/microsoft/azure-storage-blob/src/Blob/Models/ListContainersResult.php', + 'MicrosoftAzure\\Storage\\Blob\\Models\\ListPageBlobRangesDiffResult' => __DIR__ . '/..' . '/microsoft/azure-storage-blob/src/Blob/Models/ListPageBlobRangesDiffResult.php', + 'MicrosoftAzure\\Storage\\Blob\\Models\\ListPageBlobRangesOptions' => __DIR__ . '/..' . '/microsoft/azure-storage-blob/src/Blob/Models/ListPageBlobRangesOptions.php', + 'MicrosoftAzure\\Storage\\Blob\\Models\\ListPageBlobRangesResult' => __DIR__ . '/..' . '/microsoft/azure-storage-blob/src/Blob/Models/ListPageBlobRangesResult.php', + 'MicrosoftAzure\\Storage\\Blob\\Models\\PageWriteOption' => __DIR__ . '/..' . '/microsoft/azure-storage-blob/src/Blob/Models/PageWriteOption.php', + 'MicrosoftAzure\\Storage\\Blob\\Models\\PublicAccessType' => __DIR__ . '/..' . '/microsoft/azure-storage-blob/src/Blob/Models/PublicAccessType.php', + 'MicrosoftAzure\\Storage\\Blob\\Models\\PutBlobResult' => __DIR__ . '/..' . '/microsoft/azure-storage-blob/src/Blob/Models/PutBlobResult.php', + 'MicrosoftAzure\\Storage\\Blob\\Models\\PutBlockResult' => __DIR__ . '/..' . '/microsoft/azure-storage-blob/src/Blob/Models/PutBlockResult.php', + 'MicrosoftAzure\\Storage\\Blob\\Models\\SetBlobMetadataResult' => __DIR__ . '/..' . '/microsoft/azure-storage-blob/src/Blob/Models/SetBlobMetadataResult.php', + 'MicrosoftAzure\\Storage\\Blob\\Models\\SetBlobPropertiesOptions' => __DIR__ . '/..' . '/microsoft/azure-storage-blob/src/Blob/Models/SetBlobPropertiesOptions.php', + 'MicrosoftAzure\\Storage\\Blob\\Models\\SetBlobPropertiesResult' => __DIR__ . '/..' . '/microsoft/azure-storage-blob/src/Blob/Models/SetBlobPropertiesResult.php', + 'MicrosoftAzure\\Storage\\Blob\\Models\\SetBlobTierOptions' => __DIR__ . '/..' . '/microsoft/azure-storage-blob/src/Blob/Models/SetBlobTierOptions.php', + 'MicrosoftAzure\\Storage\\Blob\\Models\\UndeleteBlobOptions' => __DIR__ . '/..' . '/microsoft/azure-storage-blob/src/Blob/Models/UndeleteBlobOptions.php', + 'MicrosoftAzure\\Storage\\Common\\CloudConfigurationManager' => __DIR__ . '/..' . '/microsoft/azure-storage-common/src/Common/CloudConfigurationManager.php', + 'MicrosoftAzure\\Storage\\Common\\Exceptions\\InvalidArgumentTypeException' => __DIR__ . '/..' . '/microsoft/azure-storage-common/src/Common/Exceptions/InvalidArgumentTypeException.php', + 'MicrosoftAzure\\Storage\\Common\\Exceptions\\ServiceException' => __DIR__ . '/..' . '/microsoft/azure-storage-common/src/Common/Exceptions/ServiceException.php', + 'MicrosoftAzure\\Storage\\Common\\Internal\\ACLBase' => __DIR__ . '/..' . '/microsoft/azure-storage-common/src/Common/Internal/ACLBase.php', + 'MicrosoftAzure\\Storage\\Common\\Internal\\Authentication\\IAuthScheme' => __DIR__ . '/..' . '/microsoft/azure-storage-common/src/Common/Internal/Authentication/IAuthScheme.php', + 'MicrosoftAzure\\Storage\\Common\\Internal\\Authentication\\SharedAccessSignatureAuthScheme' => __DIR__ . '/..' . '/microsoft/azure-storage-common/src/Common/Internal/Authentication/SharedAccessSignatureAuthScheme.php', + 'MicrosoftAzure\\Storage\\Common\\Internal\\Authentication\\SharedKeyAuthScheme' => __DIR__ . '/..' . '/microsoft/azure-storage-common/src/Common/Internal/Authentication/SharedKeyAuthScheme.php', + 'MicrosoftAzure\\Storage\\Common\\Internal\\Authentication\\TokenAuthScheme' => __DIR__ . '/..' . '/microsoft/azure-storage-common/src/Common/Internal/Authentication/TokenAuthScheme.php', + 'MicrosoftAzure\\Storage\\Common\\Internal\\ConnectionStringParser' => __DIR__ . '/..' . '/microsoft/azure-storage-common/src/Common/Internal/ConnectionStringParser.php', + 'MicrosoftAzure\\Storage\\Common\\Internal\\ConnectionStringSource' => __DIR__ . '/..' . '/microsoft/azure-storage-common/src/Common/Internal/ConnectionStringSource.php', + 'MicrosoftAzure\\Storage\\Common\\Internal\\Http\\HttpCallContext' => __DIR__ . '/..' . '/microsoft/azure-storage-common/src/Common/Internal/Http/HttpCallContext.php', + 'MicrosoftAzure\\Storage\\Common\\Internal\\Http\\HttpFormatter' => __DIR__ . '/..' . '/microsoft/azure-storage-common/src/Common/Internal/Http/HttpFormatter.php', + 'MicrosoftAzure\\Storage\\Common\\Internal\\MetadataTrait' => __DIR__ . '/..' . '/microsoft/azure-storage-common/src/Common/Internal/MetadataTrait.php', + 'MicrosoftAzure\\Storage\\Common\\Internal\\Middlewares\\CommonRequestMiddleware' => __DIR__ . '/..' . '/microsoft/azure-storage-common/src/Common/Internal/Middlewares/CommonRequestMiddleware.php', + 'MicrosoftAzure\\Storage\\Common\\Internal\\Resources' => __DIR__ . '/..' . '/microsoft/azure-storage-common/src/Common/Internal/Resources.php', + 'MicrosoftAzure\\Storage\\Common\\Internal\\RestProxy' => __DIR__ . '/..' . '/microsoft/azure-storage-common/src/Common/Internal/RestProxy.php', + 'MicrosoftAzure\\Storage\\Common\\Internal\\Serialization\\ISerializer' => __DIR__ . '/..' . '/microsoft/azure-storage-common/src/Common/Internal/Serialization/ISerializer.php', + 'MicrosoftAzure\\Storage\\Common\\Internal\\Serialization\\JsonSerializer' => __DIR__ . '/..' . '/microsoft/azure-storage-common/src/Common/Internal/Serialization/JsonSerializer.php', + 'MicrosoftAzure\\Storage\\Common\\Internal\\Serialization\\MessageSerializer' => __DIR__ . '/..' . '/microsoft/azure-storage-common/src/Common/Internal/Serialization/MessageSerializer.php', + 'MicrosoftAzure\\Storage\\Common\\Internal\\Serialization\\XmlSerializer' => __DIR__ . '/..' . '/microsoft/azure-storage-common/src/Common/Internal/Serialization/XmlSerializer.php', + 'MicrosoftAzure\\Storage\\Common\\Internal\\ServiceRestProxy' => __DIR__ . '/..' . '/microsoft/azure-storage-common/src/Common/Internal/ServiceRestProxy.php', + 'MicrosoftAzure\\Storage\\Common\\Internal\\ServiceRestTrait' => __DIR__ . '/..' . '/microsoft/azure-storage-common/src/Common/Internal/ServiceRestTrait.php', + 'MicrosoftAzure\\Storage\\Common\\Internal\\ServiceSettings' => __DIR__ . '/..' . '/microsoft/azure-storage-common/src/Common/Internal/ServiceSettings.php', + 'MicrosoftAzure\\Storage\\Common\\Internal\\StorageServiceSettings' => __DIR__ . '/..' . '/microsoft/azure-storage-common/src/Common/Internal/StorageServiceSettings.php', + 'MicrosoftAzure\\Storage\\Common\\Internal\\Utilities' => __DIR__ . '/..' . '/microsoft/azure-storage-common/src/Common/Internal/Utilities.php', + 'MicrosoftAzure\\Storage\\Common\\Internal\\Validate' => __DIR__ . '/..' . '/microsoft/azure-storage-common/src/Common/Internal/Validate.php', + 'MicrosoftAzure\\Storage\\Common\\LocationMode' => __DIR__ . '/..' . '/microsoft/azure-storage-common/src/Common/LocationMode.php', + 'MicrosoftAzure\\Storage\\Common\\Logger' => __DIR__ . '/..' . '/microsoft/azure-storage-common/src/Common/Logger.php', + 'MicrosoftAzure\\Storage\\Common\\MarkerContinuationTokenTrait' => __DIR__ . '/..' . '/microsoft/azure-storage-common/src/Common/MarkerContinuationTokenTrait.php', + 'MicrosoftAzure\\Storage\\Common\\Middlewares\\HistoryMiddleware' => __DIR__ . '/..' . '/microsoft/azure-storage-common/src/Common/Middlewares/HistoryMiddleware.php', + 'MicrosoftAzure\\Storage\\Common\\Middlewares\\IMiddleware' => __DIR__ . '/..' . '/microsoft/azure-storage-common/src/Common/Middlewares/IMiddleware.php', + 'MicrosoftAzure\\Storage\\Common\\Middlewares\\MiddlewareBase' => __DIR__ . '/..' . '/microsoft/azure-storage-common/src/Common/Middlewares/MiddlewareBase.php', + 'MicrosoftAzure\\Storage\\Common\\Middlewares\\MiddlewareStack' => __DIR__ . '/..' . '/microsoft/azure-storage-common/src/Common/Middlewares/MiddlewareStack.php', + 'MicrosoftAzure\\Storage\\Common\\Middlewares\\RetryMiddleware' => __DIR__ . '/..' . '/microsoft/azure-storage-common/src/Common/Middlewares/RetryMiddleware.php', + 'MicrosoftAzure\\Storage\\Common\\Middlewares\\RetryMiddlewareFactory' => __DIR__ . '/..' . '/microsoft/azure-storage-common/src/Common/Middlewares/RetryMiddlewareFactory.php', + 'MicrosoftAzure\\Storage\\Common\\Models\\AccessPolicy' => __DIR__ . '/..' . '/microsoft/azure-storage-common/src/Common/Models/AccessPolicy.php', + 'MicrosoftAzure\\Storage\\Common\\Models\\CORS' => __DIR__ . '/..' . '/microsoft/azure-storage-common/src/Common/Models/CORS.php', + 'MicrosoftAzure\\Storage\\Common\\Models\\ContinuationToken' => __DIR__ . '/..' . '/microsoft/azure-storage-common/src/Common/Models/ContinuationToken.php', + 'MicrosoftAzure\\Storage\\Common\\Models\\GetServicePropertiesResult' => __DIR__ . '/..' . '/microsoft/azure-storage-common/src/Common/Models/GetServicePropertiesResult.php', + 'MicrosoftAzure\\Storage\\Common\\Models\\GetServiceStatsResult' => __DIR__ . '/..' . '/microsoft/azure-storage-common/src/Common/Models/GetServiceStatsResult.php', + 'MicrosoftAzure\\Storage\\Common\\Models\\Logging' => __DIR__ . '/..' . '/microsoft/azure-storage-common/src/Common/Models/Logging.php', + 'MicrosoftAzure\\Storage\\Common\\Models\\MarkerContinuationToken' => __DIR__ . '/..' . '/microsoft/azure-storage-common/src/Common/Models/MarkerContinuationToken.php', + 'MicrosoftAzure\\Storage\\Common\\Models\\Metrics' => __DIR__ . '/..' . '/microsoft/azure-storage-common/src/Common/Models/Metrics.php', + 'MicrosoftAzure\\Storage\\Common\\Models\\Range' => __DIR__ . '/..' . '/microsoft/azure-storage-common/src/Common/Models/Range.php', + 'MicrosoftAzure\\Storage\\Common\\Models\\RangeDiff' => __DIR__ . '/..' . '/microsoft/azure-storage-common/src/Common/Models/RangeDiff.php', + 'MicrosoftAzure\\Storage\\Common\\Models\\RetentionPolicy' => __DIR__ . '/..' . '/microsoft/azure-storage-common/src/Common/Models/RetentionPolicy.php', + 'MicrosoftAzure\\Storage\\Common\\Models\\ServiceOptions' => __DIR__ . '/..' . '/microsoft/azure-storage-common/src/Common/Models/ServiceOptions.php', + 'MicrosoftAzure\\Storage\\Common\\Models\\ServiceProperties' => __DIR__ . '/..' . '/microsoft/azure-storage-common/src/Common/Models/ServiceProperties.php', + 'MicrosoftAzure\\Storage\\Common\\Models\\SignedIdentifier' => __DIR__ . '/..' . '/microsoft/azure-storage-common/src/Common/Models/SignedIdentifier.php', + 'MicrosoftAzure\\Storage\\Common\\Models\\TransactionalMD5Trait' => __DIR__ . '/..' . '/microsoft/azure-storage-common/src/Common/Models/TransactionalMD5Trait.php', + 'MicrosoftAzure\\Storage\\Common\\SharedAccessSignatureHelper' => __DIR__ . '/..' . '/microsoft/azure-storage-common/src/Common/SharedAccessSignatureHelper.php', + 'F7cloud\\LogNormalizer\\Normalizer' => __DIR__ . '/..' . '/f7cloud/lognormalizer/src/Normalizer.php', + 'Normalizer' => __DIR__ . '/..' . '/symfony/polyfill-intl-normalizer/Resources/stubs/Normalizer.php', + 'OS_Guess' => __DIR__ . '/..' . '/pear/pear-core-minimal/src/OS/Guess.php', + 'OpenStack\\BlockStorage\\v2\\Api' => __DIR__ . '/..' . '/php-opencloud/openstack/src/BlockStorage/v2/Api.php', + 'OpenStack\\BlockStorage\\v2\\Models\\QuotaSet' => __DIR__ . '/..' . '/php-opencloud/openstack/src/BlockStorage/v2/Models/QuotaSet.php', + 'OpenStack\\BlockStorage\\v2\\Models\\Snapshot' => __DIR__ . '/..' . '/php-opencloud/openstack/src/BlockStorage/v2/Models/Snapshot.php', + 'OpenStack\\BlockStorage\\v2\\Models\\Volume' => __DIR__ . '/..' . '/php-opencloud/openstack/src/BlockStorage/v2/Models/Volume.php', + 'OpenStack\\BlockStorage\\v2\\Models\\VolumeAttachment' => __DIR__ . '/..' . '/php-opencloud/openstack/src/BlockStorage/v2/Models/VolumeAttachment.php', + 'OpenStack\\BlockStorage\\v2\\Models\\VolumeType' => __DIR__ . '/..' . '/php-opencloud/openstack/src/BlockStorage/v2/Models/VolumeType.php', + 'OpenStack\\BlockStorage\\v2\\Params' => __DIR__ . '/..' . '/php-opencloud/openstack/src/BlockStorage/v2/Params.php', + 'OpenStack\\BlockStorage\\v2\\Service' => __DIR__ . '/..' . '/php-opencloud/openstack/src/BlockStorage/v2/Service.php', + 'OpenStack\\BlockStorage\\v3\\Api' => __DIR__ . '/..' . '/php-opencloud/openstack/src/BlockStorage/v3/Api.php', + 'OpenStack\\BlockStorage\\v3\\Service' => __DIR__ . '/..' . '/php-opencloud/openstack/src/BlockStorage/v3/Service.php', + 'OpenStack\\Common\\Api\\AbstractApi' => __DIR__ . '/..' . '/php-opencloud/openstack/src/Common/Api/AbstractApi.php', + 'OpenStack\\Common\\Api\\AbstractParams' => __DIR__ . '/..' . '/php-opencloud/openstack/src/Common/Api/AbstractParams.php', + 'OpenStack\\Common\\Api\\ApiInterface' => __DIR__ . '/..' . '/php-opencloud/openstack/src/Common/Api/ApiInterface.php', + 'OpenStack\\Common\\Api\\Operation' => __DIR__ . '/..' . '/php-opencloud/openstack/src/Common/Api/Operation.php', + 'OpenStack\\Common\\Api\\OperatorInterface' => __DIR__ . '/..' . '/php-opencloud/openstack/src/Common/Api/OperatorInterface.php', + 'OpenStack\\Common\\Api\\OperatorTrait' => __DIR__ . '/..' . '/php-opencloud/openstack/src/Common/Api/OperatorTrait.php', + 'OpenStack\\Common\\Api\\Parameter' => __DIR__ . '/..' . '/php-opencloud/openstack/src/Common/Api/Parameter.php', + 'OpenStack\\Common\\ArrayAccessTrait' => __DIR__ . '/..' . '/php-opencloud/openstack/src/Common/ArrayAccessTrait.php', + 'OpenStack\\Common\\Auth\\AuthHandler' => __DIR__ . '/..' . '/php-opencloud/openstack/src/Common/Auth/AuthHandler.php', + 'OpenStack\\Common\\Auth\\Catalog' => __DIR__ . '/..' . '/php-opencloud/openstack/src/Common/Auth/Catalog.php', + 'OpenStack\\Common\\Auth\\IdentityService' => __DIR__ . '/..' . '/php-opencloud/openstack/src/Common/Auth/IdentityService.php', + 'OpenStack\\Common\\Auth\\Token' => __DIR__ . '/..' . '/php-opencloud/openstack/src/Common/Auth/Token.php', + 'OpenStack\\Common\\Error\\BadResponseError' => __DIR__ . '/..' . '/php-opencloud/openstack/src/Common/Error/BadResponseError.php', + 'OpenStack\\Common\\Error\\BaseError' => __DIR__ . '/..' . '/php-opencloud/openstack/src/Common/Error/BaseError.php', + 'OpenStack\\Common\\Error\\Builder' => __DIR__ . '/..' . '/php-opencloud/openstack/src/Common/Error/Builder.php', + 'OpenStack\\Common\\Error\\NotImplementedError' => __DIR__ . '/..' . '/php-opencloud/openstack/src/Common/Error/NotImplementedError.php', + 'OpenStack\\Common\\Error\\UserInputError' => __DIR__ . '/..' . '/php-opencloud/openstack/src/Common/Error/UserInputError.php', + 'OpenStack\\Common\\HydratorStrategyTrait' => __DIR__ . '/..' . '/php-opencloud/openstack/src/Common/HydratorStrategyTrait.php', + 'OpenStack\\Common\\JsonPath' => __DIR__ . '/..' . '/php-opencloud/openstack/src/Common/JsonPath.php', + 'OpenStack\\Common\\JsonSchema\\JsonPatch' => __DIR__ . '/..' . '/php-opencloud/openstack/src/Common/JsonSchema/JsonPatch.php', + 'OpenStack\\Common\\JsonSchema\\Schema' => __DIR__ . '/..' . '/php-opencloud/openstack/src/Common/JsonSchema/Schema.php', + 'OpenStack\\Common\\Resource\\AbstractResource' => __DIR__ . '/..' . '/php-opencloud/openstack/src/Common/Resource/AbstractResource.php', + 'OpenStack\\Common\\Resource\\Alias' => __DIR__ . '/..' . '/php-opencloud/openstack/src/Common/Resource/Alias.php', + 'OpenStack\\Common\\Resource\\Creatable' => __DIR__ . '/..' . '/php-opencloud/openstack/src/Common/Resource/Creatable.php', + 'OpenStack\\Common\\Resource\\Deletable' => __DIR__ . '/..' . '/php-opencloud/openstack/src/Common/Resource/Deletable.php', + 'OpenStack\\Common\\Resource\\HasMetadata' => __DIR__ . '/..' . '/php-opencloud/openstack/src/Common/Resource/HasMetadata.php', + 'OpenStack\\Common\\Resource\\HasWaiterTrait' => __DIR__ . '/..' . '/php-opencloud/openstack/src/Common/Resource/HasWaiterTrait.php', + 'OpenStack\\Common\\Resource\\Iterator' => __DIR__ . '/..' . '/php-opencloud/openstack/src/Common/Resource/Iterator.php', + 'OpenStack\\Common\\Resource\\Listable' => __DIR__ . '/..' . '/php-opencloud/openstack/src/Common/Resource/Listable.php', + 'OpenStack\\Common\\Resource\\OperatorResource' => __DIR__ . '/..' . '/php-opencloud/openstack/src/Common/Resource/OperatorResource.php', + 'OpenStack\\Common\\Resource\\ResourceInterface' => __DIR__ . '/..' . '/php-opencloud/openstack/src/Common/Resource/ResourceInterface.php', + 'OpenStack\\Common\\Resource\\Retrievable' => __DIR__ . '/..' . '/php-opencloud/openstack/src/Common/Resource/Retrievable.php', + 'OpenStack\\Common\\Resource\\Updateable' => __DIR__ . '/..' . '/php-opencloud/openstack/src/Common/Resource/Updateable.php', + 'OpenStack\\Common\\Service\\AbstractService' => __DIR__ . '/..' . '/php-opencloud/openstack/src/Common/Service/AbstractService.php', + 'OpenStack\\Common\\Service\\Builder' => __DIR__ . '/..' . '/php-opencloud/openstack/src/Common/Service/Builder.php', + 'OpenStack\\Common\\Service\\ServiceInterface' => __DIR__ . '/..' . '/php-opencloud/openstack/src/Common/Service/ServiceInterface.php', + 'OpenStack\\Common\\Transport\\HandlerStack' => __DIR__ . '/..' . '/php-opencloud/openstack/src/Common/Transport/HandlerStack.php', + 'OpenStack\\Common\\Transport\\HandlerStackFactory' => __DIR__ . '/..' . '/php-opencloud/openstack/src/Common/Transport/HandlerStackFactory.php', + 'OpenStack\\Common\\Transport\\JsonSerializer' => __DIR__ . '/..' . '/php-opencloud/openstack/src/Common/Transport/JsonSerializer.php', + 'OpenStack\\Common\\Transport\\Middleware' => __DIR__ . '/..' . '/php-opencloud/openstack/src/Common/Transport/Middleware.php', + 'OpenStack\\Common\\Transport\\RequestSerializer' => __DIR__ . '/..' . '/php-opencloud/openstack/src/Common/Transport/RequestSerializer.php', + 'OpenStack\\Common\\Transport\\Serializable' => __DIR__ . '/..' . '/php-opencloud/openstack/src/Common/Transport/Serializable.php', + 'OpenStack\\Common\\Transport\\Utils' => __DIR__ . '/..' . '/php-opencloud/openstack/src/Common/Transport/Utils.php', + 'OpenStack\\Compute\\v2\\Api' => __DIR__ . '/..' . '/php-opencloud/openstack/src/Compute/v2/Api.php', + 'OpenStack\\Compute\\v2\\Enum' => __DIR__ . '/..' . '/php-opencloud/openstack/src/Compute/v2/Enum.php', + 'OpenStack\\Compute\\v2\\Models\\AvailabilityZone' => __DIR__ . '/..' . '/php-opencloud/openstack/src/Compute/v2/Models/AvailabilityZone.php', + 'OpenStack\\Compute\\v2\\Models\\Fault' => __DIR__ . '/..' . '/php-opencloud/openstack/src/Compute/v2/Models/Fault.php', + 'OpenStack\\Compute\\v2\\Models\\Flavor' => __DIR__ . '/..' . '/php-opencloud/openstack/src/Compute/v2/Models/Flavor.php', + 'OpenStack\\Compute\\v2\\Models\\Host' => __DIR__ . '/..' . '/php-opencloud/openstack/src/Compute/v2/Models/Host.php', + 'OpenStack\\Compute\\v2\\Models\\Hypervisor' => __DIR__ . '/..' . '/php-opencloud/openstack/src/Compute/v2/Models/Hypervisor.php', + 'OpenStack\\Compute\\v2\\Models\\HypervisorStatistic' => __DIR__ . '/..' . '/php-opencloud/openstack/src/Compute/v2/Models/HypervisorStatistic.php', + 'OpenStack\\Compute\\v2\\Models\\Image' => __DIR__ . '/..' . '/php-opencloud/openstack/src/Compute/v2/Models/Image.php', + 'OpenStack\\Compute\\v2\\Models\\Keypair' => __DIR__ . '/..' . '/php-opencloud/openstack/src/Compute/v2/Models/Keypair.php', + 'OpenStack\\Compute\\v2\\Models\\Limit' => __DIR__ . '/..' . '/php-opencloud/openstack/src/Compute/v2/Models/Limit.php', + 'OpenStack\\Compute\\v2\\Models\\QuotaSet' => __DIR__ . '/..' . '/php-opencloud/openstack/src/Compute/v2/Models/QuotaSet.php', + 'OpenStack\\Compute\\v2\\Models\\Server' => __DIR__ . '/..' . '/php-opencloud/openstack/src/Compute/v2/Models/Server.php', + 'OpenStack\\Compute\\v2\\Params' => __DIR__ . '/..' . '/php-opencloud/openstack/src/Compute/v2/Params.php', + 'OpenStack\\Compute\\v2\\Service' => __DIR__ . '/..' . '/php-opencloud/openstack/src/Compute/v2/Service.php', + 'OpenStack\\Identity\\v2\\Api' => __DIR__ . '/..' . '/php-opencloud/openstack/src/Identity/v2/Api.php', + 'OpenStack\\Identity\\v2\\Models\\Catalog' => __DIR__ . '/..' . '/php-opencloud/openstack/src/Identity/v2/Models/Catalog.php', + 'OpenStack\\Identity\\v2\\Models\\Endpoint' => __DIR__ . '/..' . '/php-opencloud/openstack/src/Identity/v2/Models/Endpoint.php', + 'OpenStack\\Identity\\v2\\Models\\Entry' => __DIR__ . '/..' . '/php-opencloud/openstack/src/Identity/v2/Models/Entry.php', + 'OpenStack\\Identity\\v2\\Models\\Tenant' => __DIR__ . '/..' . '/php-opencloud/openstack/src/Identity/v2/Models/Tenant.php', + 'OpenStack\\Identity\\v2\\Models\\Token' => __DIR__ . '/..' . '/php-opencloud/openstack/src/Identity/v2/Models/Token.php', + 'OpenStack\\Identity\\v2\\Service' => __DIR__ . '/..' . '/php-opencloud/openstack/src/Identity/v2/Service.php', + 'OpenStack\\Identity\\v3\\Api' => __DIR__ . '/..' . '/php-opencloud/openstack/src/Identity/v3/Api.php', + 'OpenStack\\Identity\\v3\\Enum' => __DIR__ . '/..' . '/php-opencloud/openstack/src/Identity/v3/Enum.php', + 'OpenStack\\Identity\\v3\\Models\\ApplicationCredential' => __DIR__ . '/..' . '/php-opencloud/openstack/src/Identity/v3/Models/ApplicationCredential.php', + 'OpenStack\\Identity\\v3\\Models\\Assignment' => __DIR__ . '/..' . '/php-opencloud/openstack/src/Identity/v3/Models/Assignment.php', + 'OpenStack\\Identity\\v3\\Models\\Catalog' => __DIR__ . '/..' . '/php-opencloud/openstack/src/Identity/v3/Models/Catalog.php', + 'OpenStack\\Identity\\v3\\Models\\Credential' => __DIR__ . '/..' . '/php-opencloud/openstack/src/Identity/v3/Models/Credential.php', + 'OpenStack\\Identity\\v3\\Models\\Domain' => __DIR__ . '/..' . '/php-opencloud/openstack/src/Identity/v3/Models/Domain.php', + 'OpenStack\\Identity\\v3\\Models\\Endpoint' => __DIR__ . '/..' . '/php-opencloud/openstack/src/Identity/v3/Models/Endpoint.php', + 'OpenStack\\Identity\\v3\\Models\\Group' => __DIR__ . '/..' . '/php-opencloud/openstack/src/Identity/v3/Models/Group.php', + 'OpenStack\\Identity\\v3\\Models\\Policy' => __DIR__ . '/..' . '/php-opencloud/openstack/src/Identity/v3/Models/Policy.php', + 'OpenStack\\Identity\\v3\\Models\\Project' => __DIR__ . '/..' . '/php-opencloud/openstack/src/Identity/v3/Models/Project.php', + 'OpenStack\\Identity\\v3\\Models\\Role' => __DIR__ . '/..' . '/php-opencloud/openstack/src/Identity/v3/Models/Role.php', + 'OpenStack\\Identity\\v3\\Models\\Service' => __DIR__ . '/..' . '/php-opencloud/openstack/src/Identity/v3/Models/Service.php', + 'OpenStack\\Identity\\v3\\Models\\Token' => __DIR__ . '/..' . '/php-opencloud/openstack/src/Identity/v3/Models/Token.php', + 'OpenStack\\Identity\\v3\\Models\\User' => __DIR__ . '/..' . '/php-opencloud/openstack/src/Identity/v3/Models/User.php', + 'OpenStack\\Identity\\v3\\Params' => __DIR__ . '/..' . '/php-opencloud/openstack/src/Identity/v3/Params.php', + 'OpenStack\\Identity\\v3\\Service' => __DIR__ . '/..' . '/php-opencloud/openstack/src/Identity/v3/Service.php', + 'OpenStack\\Images\\v2\\Api' => __DIR__ . '/..' . '/php-opencloud/openstack/src/Images/v2/Api.php', + 'OpenStack\\Images\\v2\\JsonPatch' => __DIR__ . '/..' . '/php-opencloud/openstack/src/Images/v2/JsonPatch.php', + 'OpenStack\\Images\\v2\\Models\\Image' => __DIR__ . '/..' . '/php-opencloud/openstack/src/Images/v2/Models/Image.php', + 'OpenStack\\Images\\v2\\Models\\Member' => __DIR__ . '/..' . '/php-opencloud/openstack/src/Images/v2/Models/Member.php', + 'OpenStack\\Images\\v2\\Models\\Schema' => __DIR__ . '/..' . '/php-opencloud/openstack/src/Images/v2/Models/Schema.php', + 'OpenStack\\Images\\v2\\Params' => __DIR__ . '/..' . '/php-opencloud/openstack/src/Images/v2/Params.php', + 'OpenStack\\Images\\v2\\Service' => __DIR__ . '/..' . '/php-opencloud/openstack/src/Images/v2/Service.php', + 'OpenStack\\Metric\\v1\\Gnocchi\\Api' => __DIR__ . '/..' . '/php-opencloud/openstack/src/Metric/v1/Gnocchi/Api.php', + 'OpenStack\\Metric\\v1\\Gnocchi\\Models\\Metric' => __DIR__ . '/..' . '/php-opencloud/openstack/src/Metric/v1/Gnocchi/Models/Metric.php', + 'OpenStack\\Metric\\v1\\Gnocchi\\Models\\Resource' => __DIR__ . '/..' . '/php-opencloud/openstack/src/Metric/v1/Gnocchi/Models/Resource.php', + 'OpenStack\\Metric\\v1\\Gnocchi\\Models\\ResourceType' => __DIR__ . '/..' . '/php-opencloud/openstack/src/Metric/v1/Gnocchi/Models/ResourceType.php', + 'OpenStack\\Metric\\v1\\Gnocchi\\Params' => __DIR__ . '/..' . '/php-opencloud/openstack/src/Metric/v1/Gnocchi/Params.php', + 'OpenStack\\Metric\\v1\\Gnocchi\\Service' => __DIR__ . '/..' . '/php-opencloud/openstack/src/Metric/v1/Gnocchi/Service.php', + 'OpenStack\\Networking\\v2\\Api' => __DIR__ . '/..' . '/php-opencloud/openstack/src/Networking/v2/Api.php', + 'OpenStack\\Networking\\v2\\Extensions\\Layer3\\Api' => __DIR__ . '/..' . '/php-opencloud/openstack/src/Networking/v2/Extensions/Layer3/Api.php', + 'OpenStack\\Networking\\v2\\Extensions\\Layer3\\ApiTrait' => __DIR__ . '/..' . '/php-opencloud/openstack/src/Networking/v2/Extensions/Layer3/ApiTrait.php', + 'OpenStack\\Networking\\v2\\Extensions\\Layer3\\Models\\FixedIp' => __DIR__ . '/..' . '/php-opencloud/openstack/src/Networking/v2/Extensions/Layer3/Models/FixedIp.php', + 'OpenStack\\Networking\\v2\\Extensions\\Layer3\\Models\\FloatingIp' => __DIR__ . '/..' . '/php-opencloud/openstack/src/Networking/v2/Extensions/Layer3/Models/FloatingIp.php', + 'OpenStack\\Networking\\v2\\Extensions\\Layer3\\Models\\GatewayInfo' => __DIR__ . '/..' . '/php-opencloud/openstack/src/Networking/v2/Extensions/Layer3/Models/GatewayInfo.php', + 'OpenStack\\Networking\\v2\\Extensions\\Layer3\\Models\\Router' => __DIR__ . '/..' . '/php-opencloud/openstack/src/Networking/v2/Extensions/Layer3/Models/Router.php', + 'OpenStack\\Networking\\v2\\Extensions\\Layer3\\Params' => __DIR__ . '/..' . '/php-opencloud/openstack/src/Networking/v2/Extensions/Layer3/Params.php', + 'OpenStack\\Networking\\v2\\Extensions\\Layer3\\ParamsTrait' => __DIR__ . '/..' . '/php-opencloud/openstack/src/Networking/v2/Extensions/Layer3/ParamsTrait.php', + 'OpenStack\\Networking\\v2\\Extensions\\Layer3\\Service' => __DIR__ . '/..' . '/php-opencloud/openstack/src/Networking/v2/Extensions/Layer3/Service.php', + 'OpenStack\\Networking\\v2\\Extensions\\Layer3\\ServiceTrait' => __DIR__ . '/..' . '/php-opencloud/openstack/src/Networking/v2/Extensions/Layer3/ServiceTrait.php', + 'OpenStack\\Networking\\v2\\Extensions\\SecurityGroups\\Api' => __DIR__ . '/..' . '/php-opencloud/openstack/src/Networking/v2/Extensions/SecurityGroups/Api.php', + 'OpenStack\\Networking\\v2\\Extensions\\SecurityGroups\\ApiTrait' => __DIR__ . '/..' . '/php-opencloud/openstack/src/Networking/v2/Extensions/SecurityGroups/ApiTrait.php', + 'OpenStack\\Networking\\v2\\Extensions\\SecurityGroups\\Models\\SecurityGroup' => __DIR__ . '/..' . '/php-opencloud/openstack/src/Networking/v2/Extensions/SecurityGroups/Models/SecurityGroup.php', + 'OpenStack\\Networking\\v2\\Extensions\\SecurityGroups\\Models\\SecurityGroupRule' => __DIR__ . '/..' . '/php-opencloud/openstack/src/Networking/v2/Extensions/SecurityGroups/Models/SecurityGroupRule.php', + 'OpenStack\\Networking\\v2\\Extensions\\SecurityGroups\\Params' => __DIR__ . '/..' . '/php-opencloud/openstack/src/Networking/v2/Extensions/SecurityGroups/Params.php', + 'OpenStack\\Networking\\v2\\Extensions\\SecurityGroups\\ParamsTrait' => __DIR__ . '/..' . '/php-opencloud/openstack/src/Networking/v2/Extensions/SecurityGroups/ParamsTrait.php', + 'OpenStack\\Networking\\v2\\Extensions\\SecurityGroups\\Service' => __DIR__ . '/..' . '/php-opencloud/openstack/src/Networking/v2/Extensions/SecurityGroups/Service.php', + 'OpenStack\\Networking\\v2\\Extensions\\SecurityGroups\\ServiceTrait' => __DIR__ . '/..' . '/php-opencloud/openstack/src/Networking/v2/Extensions/SecurityGroups/ServiceTrait.php', + 'OpenStack\\Networking\\v2\\Models\\InterfaceAttachment' => __DIR__ . '/..' . '/php-opencloud/openstack/src/Networking/v2/Models/InterfaceAttachment.php', + 'OpenStack\\Networking\\v2\\Models\\LoadBalancer' => __DIR__ . '/..' . '/php-opencloud/openstack/src/Networking/v2/Models/LoadBalancer.php', + 'OpenStack\\Networking\\v2\\Models\\LoadBalancerHealthMonitor' => __DIR__ . '/..' . '/php-opencloud/openstack/src/Networking/v2/Models/LoadBalancerHealthMonitor.php', + 'OpenStack\\Networking\\v2\\Models\\LoadBalancerListener' => __DIR__ . '/..' . '/php-opencloud/openstack/src/Networking/v2/Models/LoadBalancerListener.php', + 'OpenStack\\Networking\\v2\\Models\\LoadBalancerMember' => __DIR__ . '/..' . '/php-opencloud/openstack/src/Networking/v2/Models/LoadBalancerMember.php', + 'OpenStack\\Networking\\v2\\Models\\LoadBalancerPool' => __DIR__ . '/..' . '/php-opencloud/openstack/src/Networking/v2/Models/LoadBalancerPool.php', + 'OpenStack\\Networking\\v2\\Models\\LoadBalancerStat' => __DIR__ . '/..' . '/php-opencloud/openstack/src/Networking/v2/Models/LoadBalancerStat.php', + 'OpenStack\\Networking\\v2\\Models\\LoadBalancerStatus' => __DIR__ . '/..' . '/php-opencloud/openstack/src/Networking/v2/Models/LoadBalancerStatus.php', + 'OpenStack\\Networking\\v2\\Models\\Network' => __DIR__ . '/..' . '/php-opencloud/openstack/src/Networking/v2/Models/Network.php', + 'OpenStack\\Networking\\v2\\Models\\Port' => __DIR__ . '/..' . '/php-opencloud/openstack/src/Networking/v2/Models/Port.php', + 'OpenStack\\Networking\\v2\\Models\\Quota' => __DIR__ . '/..' . '/php-opencloud/openstack/src/Networking/v2/Models/Quota.php', + 'OpenStack\\Networking\\v2\\Models\\Subnet' => __DIR__ . '/..' . '/php-opencloud/openstack/src/Networking/v2/Models/Subnet.php', + 'OpenStack\\Networking\\v2\\Params' => __DIR__ . '/..' . '/php-opencloud/openstack/src/Networking/v2/Params.php', + 'OpenStack\\Networking\\v2\\Service' => __DIR__ . '/..' . '/php-opencloud/openstack/src/Networking/v2/Service.php', + 'OpenStack\\ObjectStore\\v1\\Api' => __DIR__ . '/..' . '/php-opencloud/openstack/src/ObjectStore/v1/Api.php', + 'OpenStack\\ObjectStore\\v1\\Models\\Account' => __DIR__ . '/..' . '/php-opencloud/openstack/src/ObjectStore/v1/Models/Account.php', + 'OpenStack\\ObjectStore\\v1\\Models\\Container' => __DIR__ . '/..' . '/php-opencloud/openstack/src/ObjectStore/v1/Models/Container.php', + 'OpenStack\\ObjectStore\\v1\\Models\\MetadataTrait' => __DIR__ . '/..' . '/php-opencloud/openstack/src/ObjectStore/v1/Models/MetadataTrait.php', + 'OpenStack\\ObjectStore\\v1\\Models\\StorageObject' => __DIR__ . '/..' . '/php-opencloud/openstack/src/ObjectStore/v1/Models/StorageObject.php', + 'OpenStack\\ObjectStore\\v1\\Params' => __DIR__ . '/..' . '/php-opencloud/openstack/src/ObjectStore/v1/Params.php', + 'OpenStack\\ObjectStore\\v1\\Service' => __DIR__ . '/..' . '/php-opencloud/openstack/src/ObjectStore/v1/Service.php', + 'OpenStack\\OpenStack' => __DIR__ . '/..' . '/php-opencloud/openstack/src/OpenStack.php', + 'Override' => __DIR__ . '/..' . '/symfony/polyfill-php83/Resources/stubs/Override.php', + 'PEAR' => __DIR__ . '/..' . '/pear/pear-core-minimal/src/PEAR.php', + 'PEAR_Error' => __DIR__ . '/..' . '/pear/pear-core-minimal/src/PEAR.php', + 'PEAR_ErrorStack' => __DIR__ . '/..' . '/pear/pear-core-minimal/src/PEAR/ErrorStack.php', + 'PEAR_Exception' => __DIR__ . '/..' . '/pear/pear_exception/PEAR/Exception.php', + 'ParagonIE\\ConstantTime\\Base32' => __DIR__ . '/..' . '/paragonie/constant_time_encoding/src/Base32.php', + 'ParagonIE\\ConstantTime\\Base32Hex' => __DIR__ . '/..' . '/paragonie/constant_time_encoding/src/Base32Hex.php', + 'ParagonIE\\ConstantTime\\Base64' => __DIR__ . '/..' . '/paragonie/constant_time_encoding/src/Base64.php', + 'ParagonIE\\ConstantTime\\Base64DotSlash' => __DIR__ . '/..' . '/paragonie/constant_time_encoding/src/Base64DotSlash.php', + 'ParagonIE\\ConstantTime\\Base64DotSlashOrdered' => __DIR__ . '/..' . '/paragonie/constant_time_encoding/src/Base64DotSlashOrdered.php', + 'ParagonIE\\ConstantTime\\Base64UrlSafe' => __DIR__ . '/..' . '/paragonie/constant_time_encoding/src/Base64UrlSafe.php', + 'ParagonIE\\ConstantTime\\Binary' => __DIR__ . '/..' . '/paragonie/constant_time_encoding/src/Binary.php', + 'ParagonIE\\ConstantTime\\EncoderInterface' => __DIR__ . '/..' . '/paragonie/constant_time_encoding/src/EncoderInterface.php', + 'ParagonIE\\ConstantTime\\Encoding' => __DIR__ . '/..' . '/paragonie/constant_time_encoding/src/Encoding.php', + 'ParagonIE\\ConstantTime\\Hex' => __DIR__ . '/..' . '/paragonie/constant_time_encoding/src/Hex.php', + 'ParagonIE\\ConstantTime\\RFC4648' => __DIR__ . '/..' . '/paragonie/constant_time_encoding/src/RFC4648.php', + 'Pimple\\Container' => __DIR__ . '/..' . '/pimple/pimple/src/Pimple/Container.php', + 'Pimple\\Exception\\ExpectedInvokableException' => __DIR__ . '/..' . '/pimple/pimple/src/Pimple/Exception/ExpectedInvokableException.php', + 'Pimple\\Exception\\FrozenServiceException' => __DIR__ . '/..' . '/pimple/pimple/src/Pimple/Exception/FrozenServiceException.php', + 'Pimple\\Exception\\InvalidServiceIdentifierException' => __DIR__ . '/..' . '/pimple/pimple/src/Pimple/Exception/InvalidServiceIdentifierException.php', + 'Pimple\\Exception\\UnknownIdentifierException' => __DIR__ . '/..' . '/pimple/pimple/src/Pimple/Exception/UnknownIdentifierException.php', + 'Pimple\\Psr11\\Container' => __DIR__ . '/..' . '/pimple/pimple/src/Pimple/Psr11/Container.php', + 'Pimple\\Psr11\\ServiceLocator' => __DIR__ . '/..' . '/pimple/pimple/src/Pimple/Psr11/ServiceLocator.php', + 'Pimple\\ServiceIterator' => __DIR__ . '/..' . '/pimple/pimple/src/Pimple/ServiceIterator.php', + 'Pimple\\ServiceProviderInterface' => __DIR__ . '/..' . '/pimple/pimple/src/Pimple/ServiceProviderInterface.php', + 'Psr\\Cache\\CacheException' => __DIR__ . '/..' . '/psr/cache/src/CacheException.php', + 'Psr\\Cache\\CacheItemInterface' => __DIR__ . '/..' . '/psr/cache/src/CacheItemInterface.php', + 'Psr\\Cache\\CacheItemPoolInterface' => __DIR__ . '/..' . '/psr/cache/src/CacheItemPoolInterface.php', + 'Psr\\Cache\\InvalidArgumentException' => __DIR__ . '/..' . '/psr/cache/src/InvalidArgumentException.php', + 'Psr\\Clock\\ClockInterface' => __DIR__ . '/..' . '/psr/clock/src/ClockInterface.php', + 'Psr\\Container\\ContainerExceptionInterface' => __DIR__ . '/..' . '/psr/container/src/ContainerExceptionInterface.php', + 'Psr\\Container\\ContainerInterface' => __DIR__ . '/..' . '/psr/container/src/ContainerInterface.php', + 'Psr\\Container\\NotFoundExceptionInterface' => __DIR__ . '/..' . '/psr/container/src/NotFoundExceptionInterface.php', + 'Psr\\EventDispatcher\\EventDispatcherInterface' => __DIR__ . '/..' . '/psr/event-dispatcher/src/EventDispatcherInterface.php', + 'Psr\\EventDispatcher\\ListenerProviderInterface' => __DIR__ . '/..' . '/psr/event-dispatcher/src/ListenerProviderInterface.php', + 'Psr\\EventDispatcher\\StoppableEventInterface' => __DIR__ . '/..' . '/psr/event-dispatcher/src/StoppableEventInterface.php', + 'Psr\\Http\\Client\\ClientExceptionInterface' => __DIR__ . '/..' . '/psr/http-client/src/ClientExceptionInterface.php', + 'Psr\\Http\\Client\\ClientInterface' => __DIR__ . '/..' . '/psr/http-client/src/ClientInterface.php', + 'Psr\\Http\\Client\\NetworkExceptionInterface' => __DIR__ . '/..' . '/psr/http-client/src/NetworkExceptionInterface.php', + 'Psr\\Http\\Client\\RequestExceptionInterface' => __DIR__ . '/..' . '/psr/http-client/src/RequestExceptionInterface.php', + 'Psr\\Http\\Message\\MessageInterface' => __DIR__ . '/..' . '/psr/http-message/src/MessageInterface.php', + 'Psr\\Http\\Message\\RequestFactoryInterface' => __DIR__ . '/..' . '/psr/http-factory/src/RequestFactoryInterface.php', + 'Psr\\Http\\Message\\RequestInterface' => __DIR__ . '/..' . '/psr/http-message/src/RequestInterface.php', + 'Psr\\Http\\Message\\ResponseFactoryInterface' => __DIR__ . '/..' . '/psr/http-factory/src/ResponseFactoryInterface.php', + 'Psr\\Http\\Message\\ResponseInterface' => __DIR__ . '/..' . '/psr/http-message/src/ResponseInterface.php', + 'Psr\\Http\\Message\\ServerRequestFactoryInterface' => __DIR__ . '/..' . '/psr/http-factory/src/ServerRequestFactoryInterface.php', + 'Psr\\Http\\Message\\ServerRequestInterface' => __DIR__ . '/..' . '/psr/http-message/src/ServerRequestInterface.php', + 'Psr\\Http\\Message\\StreamFactoryInterface' => __DIR__ . '/..' . '/psr/http-factory/src/StreamFactoryInterface.php', + 'Psr\\Http\\Message\\StreamInterface' => __DIR__ . '/..' . '/psr/http-message/src/StreamInterface.php', + 'Psr\\Http\\Message\\UploadedFileFactoryInterface' => __DIR__ . '/..' . '/psr/http-factory/src/UploadedFileFactoryInterface.php', + 'Psr\\Http\\Message\\UploadedFileInterface' => __DIR__ . '/..' . '/psr/http-message/src/UploadedFileInterface.php', + 'Psr\\Http\\Message\\UriFactoryInterface' => __DIR__ . '/..' . '/psr/http-factory/src/UriFactoryInterface.php', + 'Psr\\Http\\Message\\UriInterface' => __DIR__ . '/..' . '/psr/http-message/src/UriInterface.php', + 'Psr\\Log\\AbstractLogger' => __DIR__ . '/..' . '/psr/log/src/AbstractLogger.php', + 'Psr\\Log\\InvalidArgumentException' => __DIR__ . '/..' . '/psr/log/src/InvalidArgumentException.php', + 'Psr\\Log\\LogLevel' => __DIR__ . '/..' . '/psr/log/src/LogLevel.php', + 'Psr\\Log\\LoggerAwareInterface' => __DIR__ . '/..' . '/psr/log/src/LoggerAwareInterface.php', + 'Psr\\Log\\LoggerAwareTrait' => __DIR__ . '/..' . '/psr/log/src/LoggerAwareTrait.php', + 'Psr\\Log\\LoggerInterface' => __DIR__ . '/..' . '/psr/log/src/LoggerInterface.php', + 'Psr\\Log\\LoggerTrait' => __DIR__ . '/..' . '/psr/log/src/LoggerTrait.php', + 'Psr\\Log\\NullLogger' => __DIR__ . '/..' . '/psr/log/src/NullLogger.php', + 'Punic\\Calendar' => __DIR__ . '/..' . '/punic/punic/src/Calendar.php', + 'Punic\\Comparer' => __DIR__ . '/..' . '/punic/punic/src/Comparer.php', + 'Punic\\Currency' => __DIR__ . '/..' . '/punic/punic/src/Currency.php', + 'Punic\\Data' => __DIR__ . '/..' . '/punic/punic/src/Data.php', + 'Punic\\Exception' => __DIR__ . '/..' . '/punic/punic/src/Exception.php', + 'Punic\\Exception\\BadArgumentType' => __DIR__ . '/..' . '/punic/punic/src/Exception/BadArgumentType.php', + 'Punic\\Exception\\BadDataFileContents' => __DIR__ . '/..' . '/punic/punic/src/Exception/BadDataFileContents.php', + 'Punic\\Exception\\DataFileNotFound' => __DIR__ . '/..' . '/punic/punic/src/Exception/DataFileNotFound.php', + 'Punic\\Exception\\DataFileNotReadable' => __DIR__ . '/..' . '/punic/punic/src/Exception/DataFileNotReadable.php', + 'Punic\\Exception\\DataFolderNotFound' => __DIR__ . '/..' . '/punic/punic/src/Exception/DataFolderNotFound.php', + 'Punic\\Exception\\InvalidDataFile' => __DIR__ . '/..' . '/punic/punic/src/Exception/InvalidDataFile.php', + 'Punic\\Exception\\InvalidLocale' => __DIR__ . '/..' . '/punic/punic/src/Exception/InvalidLocale.php', + 'Punic\\Exception\\InvalidOverride' => __DIR__ . '/..' . '/punic/punic/src/Exception/InvalidOverride.php', + 'Punic\\Exception\\NotImplemented' => __DIR__ . '/..' . '/punic/punic/src/Exception/NotImplemented.php', + 'Punic\\Exception\\ValueNotInList' => __DIR__ . '/..' . '/punic/punic/src/Exception/ValueNotInList.php', + 'Punic\\Language' => __DIR__ . '/..' . '/punic/punic/src/Language.php', + 'Punic\\Misc' => __DIR__ . '/..' . '/punic/punic/src/Misc.php', + 'Punic\\Number' => __DIR__ . '/..' . '/punic/punic/src/Number.php', + 'Punic\\Phone' => __DIR__ . '/..' . '/punic/punic/src/Phone.php', + 'Punic\\Plural' => __DIR__ . '/..' . '/punic/punic/src/Plural.php', + 'Punic\\Script' => __DIR__ . '/..' . '/punic/punic/src/Script.php', + 'Punic\\Territory' => __DIR__ . '/..' . '/punic/punic/src/Territory.php', + 'Punic\\Unit' => __DIR__ . '/..' . '/punic/punic/src/Unit.php', + 'Random\\BrokenRandomEngineError' => __DIR__ . '/..' . '/symfony/polyfill-php82/Resources/stubs/Random/BrokenRandomEngineError.php', + 'Random\\CryptoSafeEngine' => __DIR__ . '/..' . '/symfony/polyfill-php82/Resources/stubs/Random/CryptoSafeEngine.php', + 'Random\\Engine' => __DIR__ . '/..' . '/symfony/polyfill-php82/Resources/stubs/Random/Engine.php', + 'Random\\Engine\\Secure' => __DIR__ . '/..' . '/symfony/polyfill-php82/Resources/stubs/Random/Engine/Secure.php', + 'Random\\RandomError' => __DIR__ . '/..' . '/symfony/polyfill-php82/Resources/stubs/Random/RandomError.php', + 'Random\\RandomException' => __DIR__ . '/..' . '/symfony/polyfill-php82/Resources/stubs/Random/RandomException.php', + 'SQLite3Exception' => __DIR__ . '/..' . '/symfony/polyfill-php83/Resources/stubs/SQLite3Exception.php', + 'Sabre\\CalDAV\\Backend\\AbstractBackend' => __DIR__ . '/..' . '/sabre/dav/lib/CalDAV/Backend/AbstractBackend.php', + 'Sabre\\CalDAV\\Backend\\BackendInterface' => __DIR__ . '/..' . '/sabre/dav/lib/CalDAV/Backend/BackendInterface.php', + 'Sabre\\CalDAV\\Backend\\NotificationSupport' => __DIR__ . '/..' . '/sabre/dav/lib/CalDAV/Backend/NotificationSupport.php', + 'Sabre\\CalDAV\\Backend\\PDO' => __DIR__ . '/..' . '/sabre/dav/lib/CalDAV/Backend/PDO.php', + 'Sabre\\CalDAV\\Backend\\SchedulingSupport' => __DIR__ . '/..' . '/sabre/dav/lib/CalDAV/Backend/SchedulingSupport.php', + 'Sabre\\CalDAV\\Backend\\SharingSupport' => __DIR__ . '/..' . '/sabre/dav/lib/CalDAV/Backend/SharingSupport.php', + 'Sabre\\CalDAV\\Backend\\SimplePDO' => __DIR__ . '/..' . '/sabre/dav/lib/CalDAV/Backend/SimplePDO.php', + 'Sabre\\CalDAV\\Backend\\SubscriptionSupport' => __DIR__ . '/..' . '/sabre/dav/lib/CalDAV/Backend/SubscriptionSupport.php', + 'Sabre\\CalDAV\\Backend\\SyncSupport' => __DIR__ . '/..' . '/sabre/dav/lib/CalDAV/Backend/SyncSupport.php', + 'Sabre\\CalDAV\\Calendar' => __DIR__ . '/..' . '/sabre/dav/lib/CalDAV/Calendar.php', + 'Sabre\\CalDAV\\CalendarHome' => __DIR__ . '/..' . '/sabre/dav/lib/CalDAV/CalendarHome.php', + 'Sabre\\CalDAV\\CalendarObject' => __DIR__ . '/..' . '/sabre/dav/lib/CalDAV/CalendarObject.php', + 'Sabre\\CalDAV\\CalendarQueryValidator' => __DIR__ . '/..' . '/sabre/dav/lib/CalDAV/CalendarQueryValidator.php', + 'Sabre\\CalDAV\\CalendarRoot' => __DIR__ . '/..' . '/sabre/dav/lib/CalDAV/CalendarRoot.php', + 'Sabre\\CalDAV\\Exception\\InvalidComponentType' => __DIR__ . '/..' . '/sabre/dav/lib/CalDAV/Exception/InvalidComponentType.php', + 'Sabre\\CalDAV\\ICSExportPlugin' => __DIR__ . '/..' . '/sabre/dav/lib/CalDAV/ICSExportPlugin.php', + 'Sabre\\CalDAV\\ICalendar' => __DIR__ . '/..' . '/sabre/dav/lib/CalDAV/ICalendar.php', + 'Sabre\\CalDAV\\ICalendarObject' => __DIR__ . '/..' . '/sabre/dav/lib/CalDAV/ICalendarObject.php', + 'Sabre\\CalDAV\\ICalendarObjectContainer' => __DIR__ . '/..' . '/sabre/dav/lib/CalDAV/ICalendarObjectContainer.php', + 'Sabre\\CalDAV\\ISharedCalendar' => __DIR__ . '/..' . '/sabre/dav/lib/CalDAV/ISharedCalendar.php', + 'Sabre\\CalDAV\\Notifications\\Collection' => __DIR__ . '/..' . '/sabre/dav/lib/CalDAV/Notifications/Collection.php', + 'Sabre\\CalDAV\\Notifications\\ICollection' => __DIR__ . '/..' . '/sabre/dav/lib/CalDAV/Notifications/ICollection.php', + 'Sabre\\CalDAV\\Notifications\\INode' => __DIR__ . '/..' . '/sabre/dav/lib/CalDAV/Notifications/INode.php', + 'Sabre\\CalDAV\\Notifications\\Node' => __DIR__ . '/..' . '/sabre/dav/lib/CalDAV/Notifications/Node.php', + 'Sabre\\CalDAV\\Notifications\\Plugin' => __DIR__ . '/..' . '/sabre/dav/lib/CalDAV/Notifications/Plugin.php', + 'Sabre\\CalDAV\\Plugin' => __DIR__ . '/..' . '/sabre/dav/lib/CalDAV/Plugin.php', + 'Sabre\\CalDAV\\Principal\\Collection' => __DIR__ . '/..' . '/sabre/dav/lib/CalDAV/Principal/Collection.php', + 'Sabre\\CalDAV\\Principal\\IProxyRead' => __DIR__ . '/..' . '/sabre/dav/lib/CalDAV/Principal/IProxyRead.php', + 'Sabre\\CalDAV\\Principal\\IProxyWrite' => __DIR__ . '/..' . '/sabre/dav/lib/CalDAV/Principal/IProxyWrite.php', + 'Sabre\\CalDAV\\Principal\\ProxyRead' => __DIR__ . '/..' . '/sabre/dav/lib/CalDAV/Principal/ProxyRead.php', + 'Sabre\\CalDAV\\Principal\\ProxyWrite' => __DIR__ . '/..' . '/sabre/dav/lib/CalDAV/Principal/ProxyWrite.php', + 'Sabre\\CalDAV\\Principal\\User' => __DIR__ . '/..' . '/sabre/dav/lib/CalDAV/Principal/User.php', + 'Sabre\\CalDAV\\Schedule\\IInbox' => __DIR__ . '/..' . '/sabre/dav/lib/CalDAV/Schedule/IInbox.php', + 'Sabre\\CalDAV\\Schedule\\IMipPlugin' => __DIR__ . '/..' . '/sabre/dav/lib/CalDAV/Schedule/IMipPlugin.php', + 'Sabre\\CalDAV\\Schedule\\IOutbox' => __DIR__ . '/..' . '/sabre/dav/lib/CalDAV/Schedule/IOutbox.php', + 'Sabre\\CalDAV\\Schedule\\ISchedulingObject' => __DIR__ . '/..' . '/sabre/dav/lib/CalDAV/Schedule/ISchedulingObject.php', + 'Sabre\\CalDAV\\Schedule\\Inbox' => __DIR__ . '/..' . '/sabre/dav/lib/CalDAV/Schedule/Inbox.php', + 'Sabre\\CalDAV\\Schedule\\Outbox' => __DIR__ . '/..' . '/sabre/dav/lib/CalDAV/Schedule/Outbox.php', + 'Sabre\\CalDAV\\Schedule\\Plugin' => __DIR__ . '/..' . '/sabre/dav/lib/CalDAV/Schedule/Plugin.php', + 'Sabre\\CalDAV\\Schedule\\SchedulingObject' => __DIR__ . '/..' . '/sabre/dav/lib/CalDAV/Schedule/SchedulingObject.php', + 'Sabre\\CalDAV\\SharedCalendar' => __DIR__ . '/..' . '/sabre/dav/lib/CalDAV/SharedCalendar.php', + 'Sabre\\CalDAV\\SharingPlugin' => __DIR__ . '/..' . '/sabre/dav/lib/CalDAV/SharingPlugin.php', + 'Sabre\\CalDAV\\Subscriptions\\ISubscription' => __DIR__ . '/..' . '/sabre/dav/lib/CalDAV/Subscriptions/ISubscription.php', + 'Sabre\\CalDAV\\Subscriptions\\Plugin' => __DIR__ . '/..' . '/sabre/dav/lib/CalDAV/Subscriptions/Plugin.php', + 'Sabre\\CalDAV\\Subscriptions\\Subscription' => __DIR__ . '/..' . '/sabre/dav/lib/CalDAV/Subscriptions/Subscription.php', + 'Sabre\\CalDAV\\Xml\\Filter\\CalendarData' => __DIR__ . '/..' . '/sabre/dav/lib/CalDAV/Xml/Filter/CalendarData.php', + 'Sabre\\CalDAV\\Xml\\Filter\\CompFilter' => __DIR__ . '/..' . '/sabre/dav/lib/CalDAV/Xml/Filter/CompFilter.php', + 'Sabre\\CalDAV\\Xml\\Filter\\ParamFilter' => __DIR__ . '/..' . '/sabre/dav/lib/CalDAV/Xml/Filter/ParamFilter.php', + 'Sabre\\CalDAV\\Xml\\Filter\\PropFilter' => __DIR__ . '/..' . '/sabre/dav/lib/CalDAV/Xml/Filter/PropFilter.php', + 'Sabre\\CalDAV\\Xml\\Notification\\Invite' => __DIR__ . '/..' . '/sabre/dav/lib/CalDAV/Xml/Notification/Invite.php', + 'Sabre\\CalDAV\\Xml\\Notification\\InviteReply' => __DIR__ . '/..' . '/sabre/dav/lib/CalDAV/Xml/Notification/InviteReply.php', + 'Sabre\\CalDAV\\Xml\\Notification\\NotificationInterface' => __DIR__ . '/..' . '/sabre/dav/lib/CalDAV/Xml/Notification/NotificationInterface.php', + 'Sabre\\CalDAV\\Xml\\Notification\\SystemStatus' => __DIR__ . '/..' . '/sabre/dav/lib/CalDAV/Xml/Notification/SystemStatus.php', + 'Sabre\\CalDAV\\Xml\\Property\\AllowedSharingModes' => __DIR__ . '/..' . '/sabre/dav/lib/CalDAV/Xml/Property/AllowedSharingModes.php', + 'Sabre\\CalDAV\\Xml\\Property\\EmailAddressSet' => __DIR__ . '/..' . '/sabre/dav/lib/CalDAV/Xml/Property/EmailAddressSet.php', + 'Sabre\\CalDAV\\Xml\\Property\\Invite' => __DIR__ . '/..' . '/sabre/dav/lib/CalDAV/Xml/Property/Invite.php', + 'Sabre\\CalDAV\\Xml\\Property\\ScheduleCalendarTransp' => __DIR__ . '/..' . '/sabre/dav/lib/CalDAV/Xml/Property/ScheduleCalendarTransp.php', + 'Sabre\\CalDAV\\Xml\\Property\\SupportedCalendarComponentSet' => __DIR__ . '/..' . '/sabre/dav/lib/CalDAV/Xml/Property/SupportedCalendarComponentSet.php', + 'Sabre\\CalDAV\\Xml\\Property\\SupportedCalendarData' => __DIR__ . '/..' . '/sabre/dav/lib/CalDAV/Xml/Property/SupportedCalendarData.php', + 'Sabre\\CalDAV\\Xml\\Property\\SupportedCollationSet' => __DIR__ . '/..' . '/sabre/dav/lib/CalDAV/Xml/Property/SupportedCollationSet.php', + 'Sabre\\CalDAV\\Xml\\Request\\CalendarMultiGetReport' => __DIR__ . '/..' . '/sabre/dav/lib/CalDAV/Xml/Request/CalendarMultiGetReport.php', + 'Sabre\\CalDAV\\Xml\\Request\\CalendarQueryReport' => __DIR__ . '/..' . '/sabre/dav/lib/CalDAV/Xml/Request/CalendarQueryReport.php', + 'Sabre\\CalDAV\\Xml\\Request\\FreeBusyQueryReport' => __DIR__ . '/..' . '/sabre/dav/lib/CalDAV/Xml/Request/FreeBusyQueryReport.php', + 'Sabre\\CalDAV\\Xml\\Request\\InviteReply' => __DIR__ . '/..' . '/sabre/dav/lib/CalDAV/Xml/Request/InviteReply.php', + 'Sabre\\CalDAV\\Xml\\Request\\MkCalendar' => __DIR__ . '/..' . '/sabre/dav/lib/CalDAV/Xml/Request/MkCalendar.php', + 'Sabre\\CalDAV\\Xml\\Request\\Share' => __DIR__ . '/..' . '/sabre/dav/lib/CalDAV/Xml/Request/Share.php', + 'Sabre\\CardDAV\\AddressBook' => __DIR__ . '/..' . '/sabre/dav/lib/CardDAV/AddressBook.php', + 'Sabre\\CardDAV\\AddressBookHome' => __DIR__ . '/..' . '/sabre/dav/lib/CardDAV/AddressBookHome.php', + 'Sabre\\CardDAV\\AddressBookRoot' => __DIR__ . '/..' . '/sabre/dav/lib/CardDAV/AddressBookRoot.php', + 'Sabre\\CardDAV\\Backend\\AbstractBackend' => __DIR__ . '/..' . '/sabre/dav/lib/CardDAV/Backend/AbstractBackend.php', + 'Sabre\\CardDAV\\Backend\\BackendInterface' => __DIR__ . '/..' . '/sabre/dav/lib/CardDAV/Backend/BackendInterface.php', + 'Sabre\\CardDAV\\Backend\\PDO' => __DIR__ . '/..' . '/sabre/dav/lib/CardDAV/Backend/PDO.php', + 'Sabre\\CardDAV\\Backend\\SyncSupport' => __DIR__ . '/..' . '/sabre/dav/lib/CardDAV/Backend/SyncSupport.php', + 'Sabre\\CardDAV\\Card' => __DIR__ . '/..' . '/sabre/dav/lib/CardDAV/Card.php', + 'Sabre\\CardDAV\\IAddressBook' => __DIR__ . '/..' . '/sabre/dav/lib/CardDAV/IAddressBook.php', + 'Sabre\\CardDAV\\ICard' => __DIR__ . '/..' . '/sabre/dav/lib/CardDAV/ICard.php', + 'Sabre\\CardDAV\\IDirectory' => __DIR__ . '/..' . '/sabre/dav/lib/CardDAV/IDirectory.php', + 'Sabre\\CardDAV\\Plugin' => __DIR__ . '/..' . '/sabre/dav/lib/CardDAV/Plugin.php', + 'Sabre\\CardDAV\\VCFExportPlugin' => __DIR__ . '/..' . '/sabre/dav/lib/CardDAV/VCFExportPlugin.php', + 'Sabre\\CardDAV\\Xml\\Filter\\AddressData' => __DIR__ . '/..' . '/sabre/dav/lib/CardDAV/Xml/Filter/AddressData.php', + 'Sabre\\CardDAV\\Xml\\Filter\\ParamFilter' => __DIR__ . '/..' . '/sabre/dav/lib/CardDAV/Xml/Filter/ParamFilter.php', + 'Sabre\\CardDAV\\Xml\\Filter\\PropFilter' => __DIR__ . '/..' . '/sabre/dav/lib/CardDAV/Xml/Filter/PropFilter.php', + 'Sabre\\CardDAV\\Xml\\Property\\SupportedAddressData' => __DIR__ . '/..' . '/sabre/dav/lib/CardDAV/Xml/Property/SupportedAddressData.php', + 'Sabre\\CardDAV\\Xml\\Property\\SupportedCollationSet' => __DIR__ . '/..' . '/sabre/dav/lib/CardDAV/Xml/Property/SupportedCollationSet.php', + 'Sabre\\CardDAV\\Xml\\Request\\AddressBookMultiGetReport' => __DIR__ . '/..' . '/sabre/dav/lib/CardDAV/Xml/Request/AddressBookMultiGetReport.php', + 'Sabre\\CardDAV\\Xml\\Request\\AddressBookQueryReport' => __DIR__ . '/..' . '/sabre/dav/lib/CardDAV/Xml/Request/AddressBookQueryReport.php', + 'Sabre\\DAVACL\\ACLTrait' => __DIR__ . '/..' . '/sabre/dav/lib/DAVACL/ACLTrait.php', + 'Sabre\\DAVACL\\AbstractPrincipalCollection' => __DIR__ . '/..' . '/sabre/dav/lib/DAVACL/AbstractPrincipalCollection.php', + 'Sabre\\DAVACL\\Exception\\AceConflict' => __DIR__ . '/..' . '/sabre/dav/lib/DAVACL/Exception/AceConflict.php', + 'Sabre\\DAVACL\\Exception\\NeedPrivileges' => __DIR__ . '/..' . '/sabre/dav/lib/DAVACL/Exception/NeedPrivileges.php', + 'Sabre\\DAVACL\\Exception\\NoAbstract' => __DIR__ . '/..' . '/sabre/dav/lib/DAVACL/Exception/NoAbstract.php', + 'Sabre\\DAVACL\\Exception\\NotRecognizedPrincipal' => __DIR__ . '/..' . '/sabre/dav/lib/DAVACL/Exception/NotRecognizedPrincipal.php', + 'Sabre\\DAVACL\\Exception\\NotSupportedPrivilege' => __DIR__ . '/..' . '/sabre/dav/lib/DAVACL/Exception/NotSupportedPrivilege.php', + 'Sabre\\DAVACL\\FS\\Collection' => __DIR__ . '/..' . '/sabre/dav/lib/DAVACL/FS/Collection.php', + 'Sabre\\DAVACL\\FS\\File' => __DIR__ . '/..' . '/sabre/dav/lib/DAVACL/FS/File.php', + 'Sabre\\DAVACL\\FS\\HomeCollection' => __DIR__ . '/..' . '/sabre/dav/lib/DAVACL/FS/HomeCollection.php', + 'Sabre\\DAVACL\\IACL' => __DIR__ . '/..' . '/sabre/dav/lib/DAVACL/IACL.php', + 'Sabre\\DAVACL\\IPrincipal' => __DIR__ . '/..' . '/sabre/dav/lib/DAVACL/IPrincipal.php', + 'Sabre\\DAVACL\\IPrincipalCollection' => __DIR__ . '/..' . '/sabre/dav/lib/DAVACL/IPrincipalCollection.php', + 'Sabre\\DAVACL\\Plugin' => __DIR__ . '/..' . '/sabre/dav/lib/DAVACL/Plugin.php', + 'Sabre\\DAVACL\\Principal' => __DIR__ . '/..' . '/sabre/dav/lib/DAVACL/Principal.php', + 'Sabre\\DAVACL\\PrincipalBackend\\AbstractBackend' => __DIR__ . '/..' . '/sabre/dav/lib/DAVACL/PrincipalBackend/AbstractBackend.php', + 'Sabre\\DAVACL\\PrincipalBackend\\BackendInterface' => __DIR__ . '/..' . '/sabre/dav/lib/DAVACL/PrincipalBackend/BackendInterface.php', + 'Sabre\\DAVACL\\PrincipalBackend\\CreatePrincipalSupport' => __DIR__ . '/..' . '/sabre/dav/lib/DAVACL/PrincipalBackend/CreatePrincipalSupport.php', + 'Sabre\\DAVACL\\PrincipalBackend\\PDO' => __DIR__ . '/..' . '/sabre/dav/lib/DAVACL/PrincipalBackend/PDO.php', + 'Sabre\\DAVACL\\PrincipalCollection' => __DIR__ . '/..' . '/sabre/dav/lib/DAVACL/PrincipalCollection.php', + 'Sabre\\DAVACL\\Xml\\Property\\Acl' => __DIR__ . '/..' . '/sabre/dav/lib/DAVACL/Xml/Property/Acl.php', + 'Sabre\\DAVACL\\Xml\\Property\\AclRestrictions' => __DIR__ . '/..' . '/sabre/dav/lib/DAVACL/Xml/Property/AclRestrictions.php', + 'Sabre\\DAVACL\\Xml\\Property\\CurrentUserPrivilegeSet' => __DIR__ . '/..' . '/sabre/dav/lib/DAVACL/Xml/Property/CurrentUserPrivilegeSet.php', + 'Sabre\\DAVACL\\Xml\\Property\\Principal' => __DIR__ . '/..' . '/sabre/dav/lib/DAVACL/Xml/Property/Principal.php', + 'Sabre\\DAVACL\\Xml\\Property\\SupportedPrivilegeSet' => __DIR__ . '/..' . '/sabre/dav/lib/DAVACL/Xml/Property/SupportedPrivilegeSet.php', + 'Sabre\\DAVACL\\Xml\\Request\\AclPrincipalPropSetReport' => __DIR__ . '/..' . '/sabre/dav/lib/DAVACL/Xml/Request/AclPrincipalPropSetReport.php', + 'Sabre\\DAVACL\\Xml\\Request\\ExpandPropertyReport' => __DIR__ . '/..' . '/sabre/dav/lib/DAVACL/Xml/Request/ExpandPropertyReport.php', + 'Sabre\\DAVACL\\Xml\\Request\\PrincipalMatchReport' => __DIR__ . '/..' . '/sabre/dav/lib/DAVACL/Xml/Request/PrincipalMatchReport.php', + 'Sabre\\DAVACL\\Xml\\Request\\PrincipalPropertySearchReport' => __DIR__ . '/..' . '/sabre/dav/lib/DAVACL/Xml/Request/PrincipalPropertySearchReport.php', + 'Sabre\\DAVACL\\Xml\\Request\\PrincipalSearchPropertySetReport' => __DIR__ . '/..' . '/sabre/dav/lib/DAVACL/Xml/Request/PrincipalSearchPropertySetReport.php', + 'Sabre\\DAV\\Auth\\Backend\\AbstractBasic' => __DIR__ . '/..' . '/sabre/dav/lib/DAV/Auth/Backend/AbstractBasic.php', + 'Sabre\\DAV\\Auth\\Backend\\AbstractBearer' => __DIR__ . '/..' . '/sabre/dav/lib/DAV/Auth/Backend/AbstractBearer.php', + 'Sabre\\DAV\\Auth\\Backend\\AbstractDigest' => __DIR__ . '/..' . '/sabre/dav/lib/DAV/Auth/Backend/AbstractDigest.php', + 'Sabre\\DAV\\Auth\\Backend\\Apache' => __DIR__ . '/..' . '/sabre/dav/lib/DAV/Auth/Backend/Apache.php', + 'Sabre\\DAV\\Auth\\Backend\\BackendInterface' => __DIR__ . '/..' . '/sabre/dav/lib/DAV/Auth/Backend/BackendInterface.php', + 'Sabre\\DAV\\Auth\\Backend\\BasicCallBack' => __DIR__ . '/..' . '/sabre/dav/lib/DAV/Auth/Backend/BasicCallBack.php', + 'Sabre\\DAV\\Auth\\Backend\\File' => __DIR__ . '/..' . '/sabre/dav/lib/DAV/Auth/Backend/File.php', + 'Sabre\\DAV\\Auth\\Backend\\IMAP' => __DIR__ . '/..' . '/sabre/dav/lib/DAV/Auth/Backend/IMAP.php', + 'Sabre\\DAV\\Auth\\Backend\\PDO' => __DIR__ . '/..' . '/sabre/dav/lib/DAV/Auth/Backend/PDO.php', + 'Sabre\\DAV\\Auth\\Backend\\PDOBasicAuth' => __DIR__ . '/..' . '/sabre/dav/lib/DAV/Auth/Backend/PDOBasicAuth.php', + 'Sabre\\DAV\\Auth\\Plugin' => __DIR__ . '/..' . '/sabre/dav/lib/DAV/Auth/Plugin.php', + 'Sabre\\DAV\\Browser\\GuessContentType' => __DIR__ . '/..' . '/sabre/dav/lib/DAV/Browser/GuessContentType.php', + 'Sabre\\DAV\\Browser\\HtmlOutput' => __DIR__ . '/..' . '/sabre/dav/lib/DAV/Browser/HtmlOutput.php', + 'Sabre\\DAV\\Browser\\HtmlOutputHelper' => __DIR__ . '/..' . '/sabre/dav/lib/DAV/Browser/HtmlOutputHelper.php', + 'Sabre\\DAV\\Browser\\MapGetToPropFind' => __DIR__ . '/..' . '/sabre/dav/lib/DAV/Browser/MapGetToPropFind.php', + 'Sabre\\DAV\\Browser\\Plugin' => __DIR__ . '/..' . '/sabre/dav/lib/DAV/Browser/Plugin.php', + 'Sabre\\DAV\\Browser\\PropFindAll' => __DIR__ . '/..' . '/sabre/dav/lib/DAV/Browser/PropFindAll.php', + 'Sabre\\DAV\\Client' => __DIR__ . '/..' . '/sabre/dav/lib/DAV/Client.php', + 'Sabre\\DAV\\Collection' => __DIR__ . '/..' . '/sabre/dav/lib/DAV/Collection.php', + 'Sabre\\DAV\\CorePlugin' => __DIR__ . '/..' . '/sabre/dav/lib/DAV/CorePlugin.php', + 'Sabre\\DAV\\Exception' => __DIR__ . '/..' . '/sabre/dav/lib/DAV/Exception.php', + 'Sabre\\DAV\\Exception\\BadRequest' => __DIR__ . '/..' . '/sabre/dav/lib/DAV/Exception/BadRequest.php', + 'Sabre\\DAV\\Exception\\Conflict' => __DIR__ . '/..' . '/sabre/dav/lib/DAV/Exception/Conflict.php', + 'Sabre\\DAV\\Exception\\ConflictingLock' => __DIR__ . '/..' . '/sabre/dav/lib/DAV/Exception/ConflictingLock.php', + 'Sabre\\DAV\\Exception\\Forbidden' => __DIR__ . '/..' . '/sabre/dav/lib/DAV/Exception/Forbidden.php', + 'Sabre\\DAV\\Exception\\InsufficientStorage' => __DIR__ . '/..' . '/sabre/dav/lib/DAV/Exception/InsufficientStorage.php', + 'Sabre\\DAV\\Exception\\InvalidResourceType' => __DIR__ . '/..' . '/sabre/dav/lib/DAV/Exception/InvalidResourceType.php', + 'Sabre\\DAV\\Exception\\InvalidSyncToken' => __DIR__ . '/..' . '/sabre/dav/lib/DAV/Exception/InvalidSyncToken.php', + 'Sabre\\DAV\\Exception\\LengthRequired' => __DIR__ . '/..' . '/sabre/dav/lib/DAV/Exception/LengthRequired.php', + 'Sabre\\DAV\\Exception\\LockTokenMatchesRequestUri' => __DIR__ . '/..' . '/sabre/dav/lib/DAV/Exception/LockTokenMatchesRequestUri.php', + 'Sabre\\DAV\\Exception\\Locked' => __DIR__ . '/..' . '/sabre/dav/lib/DAV/Exception/Locked.php', + 'Sabre\\DAV\\Exception\\MethodNotAllowed' => __DIR__ . '/..' . '/sabre/dav/lib/DAV/Exception/MethodNotAllowed.php', + 'Sabre\\DAV\\Exception\\NotAuthenticated' => __DIR__ . '/..' . '/sabre/dav/lib/DAV/Exception/NotAuthenticated.php', + 'Sabre\\DAV\\Exception\\NotFound' => __DIR__ . '/..' . '/sabre/dav/lib/DAV/Exception/NotFound.php', + 'Sabre\\DAV\\Exception\\NotImplemented' => __DIR__ . '/..' . '/sabre/dav/lib/DAV/Exception/NotImplemented.php', + 'Sabre\\DAV\\Exception\\PaymentRequired' => __DIR__ . '/..' . '/sabre/dav/lib/DAV/Exception/PaymentRequired.php', + 'Sabre\\DAV\\Exception\\PreconditionFailed' => __DIR__ . '/..' . '/sabre/dav/lib/DAV/Exception/PreconditionFailed.php', + 'Sabre\\DAV\\Exception\\ReportNotSupported' => __DIR__ . '/..' . '/sabre/dav/lib/DAV/Exception/ReportNotSupported.php', + 'Sabre\\DAV\\Exception\\RequestedRangeNotSatisfiable' => __DIR__ . '/..' . '/sabre/dav/lib/DAV/Exception/RequestedRangeNotSatisfiable.php', + 'Sabre\\DAV\\Exception\\ServiceUnavailable' => __DIR__ . '/..' . '/sabre/dav/lib/DAV/Exception/ServiceUnavailable.php', + 'Sabre\\DAV\\Exception\\TooManyMatches' => __DIR__ . '/..' . '/sabre/dav/lib/DAV/Exception/TooManyMatches.php', + 'Sabre\\DAV\\Exception\\UnsupportedMediaType' => __DIR__ . '/..' . '/sabre/dav/lib/DAV/Exception/UnsupportedMediaType.php', + 'Sabre\\DAV\\FSExt\\Directory' => __DIR__ . '/..' . '/sabre/dav/lib/DAV/FSExt/Directory.php', + 'Sabre\\DAV\\FSExt\\File' => __DIR__ . '/..' . '/sabre/dav/lib/DAV/FSExt/File.php', + 'Sabre\\DAV\\FS\\Directory' => __DIR__ . '/..' . '/sabre/dav/lib/DAV/FS/Directory.php', + 'Sabre\\DAV\\FS\\File' => __DIR__ . '/..' . '/sabre/dav/lib/DAV/FS/File.php', + 'Sabre\\DAV\\FS\\Node' => __DIR__ . '/..' . '/sabre/dav/lib/DAV/FS/Node.php', + 'Sabre\\DAV\\File' => __DIR__ . '/..' . '/sabre/dav/lib/DAV/File.php', + 'Sabre\\DAV\\ICollection' => __DIR__ . '/..' . '/sabre/dav/lib/DAV/ICollection.php', + 'Sabre\\DAV\\ICopyTarget' => __DIR__ . '/..' . '/sabre/dav/lib/DAV/ICopyTarget.php', + 'Sabre\\DAV\\IExtendedCollection' => __DIR__ . '/..' . '/sabre/dav/lib/DAV/IExtendedCollection.php', + 'Sabre\\DAV\\IFile' => __DIR__ . '/..' . '/sabre/dav/lib/DAV/IFile.php', + 'Sabre\\DAV\\IMoveTarget' => __DIR__ . '/..' . '/sabre/dav/lib/DAV/IMoveTarget.php', + 'Sabre\\DAV\\IMultiGet' => __DIR__ . '/..' . '/sabre/dav/lib/DAV/IMultiGet.php', + 'Sabre\\DAV\\INode' => __DIR__ . '/..' . '/sabre/dav/lib/DAV/INode.php', + 'Sabre\\DAV\\INodeByPath' => __DIR__ . '/..' . '/sabre/dav/lib/DAV/INodeByPath.php', + 'Sabre\\DAV\\IProperties' => __DIR__ . '/..' . '/sabre/dav/lib/DAV/IProperties.php', + 'Sabre\\DAV\\IQuota' => __DIR__ . '/..' . '/sabre/dav/lib/DAV/IQuota.php', + 'Sabre\\DAV\\Locks\\Backend\\AbstractBackend' => __DIR__ . '/..' . '/sabre/dav/lib/DAV/Locks/Backend/AbstractBackend.php', + 'Sabre\\DAV\\Locks\\Backend\\BackendInterface' => __DIR__ . '/..' . '/sabre/dav/lib/DAV/Locks/Backend/BackendInterface.php', + 'Sabre\\DAV\\Locks\\Backend\\File' => __DIR__ . '/..' . '/sabre/dav/lib/DAV/Locks/Backend/File.php', + 'Sabre\\DAV\\Locks\\Backend\\PDO' => __DIR__ . '/..' . '/sabre/dav/lib/DAV/Locks/Backend/PDO.php', + 'Sabre\\DAV\\Locks\\LockInfo' => __DIR__ . '/..' . '/sabre/dav/lib/DAV/Locks/LockInfo.php', + 'Sabre\\DAV\\Locks\\Plugin' => __DIR__ . '/..' . '/sabre/dav/lib/DAV/Locks/Plugin.php', + 'Sabre\\DAV\\MkCol' => __DIR__ . '/..' . '/sabre/dav/lib/DAV/MkCol.php', + 'Sabre\\DAV\\Mount\\Plugin' => __DIR__ . '/..' . '/sabre/dav/lib/DAV/Mount/Plugin.php', + 'Sabre\\DAV\\Node' => __DIR__ . '/..' . '/sabre/dav/lib/DAV/Node.php', + 'Sabre\\DAV\\PartialUpdate\\IPatchSupport' => __DIR__ . '/..' . '/sabre/dav/lib/DAV/PartialUpdate/IPatchSupport.php', + 'Sabre\\DAV\\PartialUpdate\\Plugin' => __DIR__ . '/..' . '/sabre/dav/lib/DAV/PartialUpdate/Plugin.php', + 'Sabre\\DAV\\PropFind' => __DIR__ . '/..' . '/sabre/dav/lib/DAV/PropFind.php', + 'Sabre\\DAV\\PropPatch' => __DIR__ . '/..' . '/sabre/dav/lib/DAV/PropPatch.php', + 'Sabre\\DAV\\PropertyStorage\\Backend\\BackendInterface' => __DIR__ . '/..' . '/sabre/dav/lib/DAV/PropertyStorage/Backend/BackendInterface.php', + 'Sabre\\DAV\\PropertyStorage\\Backend\\PDO' => __DIR__ . '/..' . '/sabre/dav/lib/DAV/PropertyStorage/Backend/PDO.php', + 'Sabre\\DAV\\PropertyStorage\\Plugin' => __DIR__ . '/..' . '/sabre/dav/lib/DAV/PropertyStorage/Plugin.php', + 'Sabre\\DAV\\Server' => __DIR__ . '/..' . '/sabre/dav/lib/DAV/Server.php', + 'Sabre\\DAV\\ServerPlugin' => __DIR__ . '/..' . '/sabre/dav/lib/DAV/ServerPlugin.php', + 'Sabre\\DAV\\Sharing\\ISharedNode' => __DIR__ . '/..' . '/sabre/dav/lib/DAV/Sharing/ISharedNode.php', + 'Sabre\\DAV\\Sharing\\Plugin' => __DIR__ . '/..' . '/sabre/dav/lib/DAV/Sharing/Plugin.php', + 'Sabre\\DAV\\SimpleCollection' => __DIR__ . '/..' . '/sabre/dav/lib/DAV/SimpleCollection.php', + 'Sabre\\DAV\\SimpleFile' => __DIR__ . '/..' . '/sabre/dav/lib/DAV/SimpleFile.php', + 'Sabre\\DAV\\StringUtil' => __DIR__ . '/..' . '/sabre/dav/lib/DAV/StringUtil.php', + 'Sabre\\DAV\\Sync\\ISyncCollection' => __DIR__ . '/..' . '/sabre/dav/lib/DAV/Sync/ISyncCollection.php', + 'Sabre\\DAV\\Sync\\Plugin' => __DIR__ . '/..' . '/sabre/dav/lib/DAV/Sync/Plugin.php', + 'Sabre\\DAV\\TemporaryFileFilterPlugin' => __DIR__ . '/..' . '/sabre/dav/lib/DAV/TemporaryFileFilterPlugin.php', + 'Sabre\\DAV\\Tree' => __DIR__ . '/..' . '/sabre/dav/lib/DAV/Tree.php', + 'Sabre\\DAV\\UUIDUtil' => __DIR__ . '/..' . '/sabre/dav/lib/DAV/UUIDUtil.php', + 'Sabre\\DAV\\Version' => __DIR__ . '/..' . '/sabre/dav/lib/DAV/Version.php', + 'Sabre\\DAV\\Xml\\Element\\Prop' => __DIR__ . '/..' . '/sabre/dav/lib/DAV/Xml/Element/Prop.php', + 'Sabre\\DAV\\Xml\\Element\\Response' => __DIR__ . '/..' . '/sabre/dav/lib/DAV/Xml/Element/Response.php', + 'Sabre\\DAV\\Xml\\Element\\Sharee' => __DIR__ . '/..' . '/sabre/dav/lib/DAV/Xml/Element/Sharee.php', + 'Sabre\\DAV\\Xml\\Property\\Complex' => __DIR__ . '/..' . '/sabre/dav/lib/DAV/Xml/Property/Complex.php', + 'Sabre\\DAV\\Xml\\Property\\GetLastModified' => __DIR__ . '/..' . '/sabre/dav/lib/DAV/Xml/Property/GetLastModified.php', + 'Sabre\\DAV\\Xml\\Property\\Href' => __DIR__ . '/..' . '/sabre/dav/lib/DAV/Xml/Property/Href.php', + 'Sabre\\DAV\\Xml\\Property\\Invite' => __DIR__ . '/..' . '/sabre/dav/lib/DAV/Xml/Property/Invite.php', + 'Sabre\\DAV\\Xml\\Property\\LocalHref' => __DIR__ . '/..' . '/sabre/dav/lib/DAV/Xml/Property/LocalHref.php', + 'Sabre\\DAV\\Xml\\Property\\LockDiscovery' => __DIR__ . '/..' . '/sabre/dav/lib/DAV/Xml/Property/LockDiscovery.php', + 'Sabre\\DAV\\Xml\\Property\\ResourceType' => __DIR__ . '/..' . '/sabre/dav/lib/DAV/Xml/Property/ResourceType.php', + 'Sabre\\DAV\\Xml\\Property\\ShareAccess' => __DIR__ . '/..' . '/sabre/dav/lib/DAV/Xml/Property/ShareAccess.php', + 'Sabre\\DAV\\Xml\\Property\\SupportedLock' => __DIR__ . '/..' . '/sabre/dav/lib/DAV/Xml/Property/SupportedLock.php', + 'Sabre\\DAV\\Xml\\Property\\SupportedMethodSet' => __DIR__ . '/..' . '/sabre/dav/lib/DAV/Xml/Property/SupportedMethodSet.php', + 'Sabre\\DAV\\Xml\\Property\\SupportedReportSet' => __DIR__ . '/..' . '/sabre/dav/lib/DAV/Xml/Property/SupportedReportSet.php', + 'Sabre\\DAV\\Xml\\Request\\Lock' => __DIR__ . '/..' . '/sabre/dav/lib/DAV/Xml/Request/Lock.php', + 'Sabre\\DAV\\Xml\\Request\\MkCol' => __DIR__ . '/..' . '/sabre/dav/lib/DAV/Xml/Request/MkCol.php', + 'Sabre\\DAV\\Xml\\Request\\PropFind' => __DIR__ . '/..' . '/sabre/dav/lib/DAV/Xml/Request/PropFind.php', + 'Sabre\\DAV\\Xml\\Request\\PropPatch' => __DIR__ . '/..' . '/sabre/dav/lib/DAV/Xml/Request/PropPatch.php', + 'Sabre\\DAV\\Xml\\Request\\ShareResource' => __DIR__ . '/..' . '/sabre/dav/lib/DAV/Xml/Request/ShareResource.php', + 'Sabre\\DAV\\Xml\\Request\\SyncCollectionReport' => __DIR__ . '/..' . '/sabre/dav/lib/DAV/Xml/Request/SyncCollectionReport.php', + 'Sabre\\DAV\\Xml\\Response\\MultiStatus' => __DIR__ . '/..' . '/sabre/dav/lib/DAV/Xml/Response/MultiStatus.php', + 'Sabre\\DAV\\Xml\\Service' => __DIR__ . '/..' . '/sabre/dav/lib/DAV/Xml/Service.php', + 'Sabre\\Event\\Emitter' => __DIR__ . '/..' . '/sabre/event/lib/Emitter.php', + 'Sabre\\Event\\EmitterInterface' => __DIR__ . '/..' . '/sabre/event/lib/EmitterInterface.php', + 'Sabre\\Event\\EmitterTrait' => __DIR__ . '/..' . '/sabre/event/lib/EmitterTrait.php', + 'Sabre\\Event\\EventEmitter' => __DIR__ . '/..' . '/sabre/event/lib/EventEmitter.php', + 'Sabre\\Event\\Loop\\Loop' => __DIR__ . '/..' . '/sabre/event/lib/Loop/Loop.php', + 'Sabre\\Event\\Promise' => __DIR__ . '/..' . '/sabre/event/lib/Promise.php', + 'Sabre\\Event\\PromiseAlreadyResolvedException' => __DIR__ . '/..' . '/sabre/event/lib/PromiseAlreadyResolvedException.php', + 'Sabre\\Event\\Version' => __DIR__ . '/..' . '/sabre/event/lib/Version.php', + 'Sabre\\Event\\WildcardEmitter' => __DIR__ . '/..' . '/sabre/event/lib/WildcardEmitter.php', + 'Sabre\\Event\\WildcardEmitterTrait' => __DIR__ . '/..' . '/sabre/event/lib/WildcardEmitterTrait.php', + 'Sabre\\HTTP\\Auth\\AWS' => __DIR__ . '/..' . '/sabre/http/lib/Auth/AWS.php', + 'Sabre\\HTTP\\Auth\\AbstractAuth' => __DIR__ . '/..' . '/sabre/http/lib/Auth/AbstractAuth.php', + 'Sabre\\HTTP\\Auth\\Basic' => __DIR__ . '/..' . '/sabre/http/lib/Auth/Basic.php', + 'Sabre\\HTTP\\Auth\\Bearer' => __DIR__ . '/..' . '/sabre/http/lib/Auth/Bearer.php', + 'Sabre\\HTTP\\Auth\\Digest' => __DIR__ . '/..' . '/sabre/http/lib/Auth/Digest.php', + 'Sabre\\HTTP\\Client' => __DIR__ . '/..' . '/sabre/http/lib/Client.php', + 'Sabre\\HTTP\\ClientException' => __DIR__ . '/..' . '/sabre/http/lib/ClientException.php', + 'Sabre\\HTTP\\ClientHttpException' => __DIR__ . '/..' . '/sabre/http/lib/ClientHttpException.php', + 'Sabre\\HTTP\\HttpException' => __DIR__ . '/..' . '/sabre/http/lib/HttpException.php', + 'Sabre\\HTTP\\Message' => __DIR__ . '/..' . '/sabre/http/lib/Message.php', + 'Sabre\\HTTP\\MessageDecoratorTrait' => __DIR__ . '/..' . '/sabre/http/lib/MessageDecoratorTrait.php', + 'Sabre\\HTTP\\MessageInterface' => __DIR__ . '/..' . '/sabre/http/lib/MessageInterface.php', + 'Sabre\\HTTP\\Request' => __DIR__ . '/..' . '/sabre/http/lib/Request.php', + 'Sabre\\HTTP\\RequestDecorator' => __DIR__ . '/..' . '/sabre/http/lib/RequestDecorator.php', + 'Sabre\\HTTP\\RequestInterface' => __DIR__ . '/..' . '/sabre/http/lib/RequestInterface.php', + 'Sabre\\HTTP\\Response' => __DIR__ . '/..' . '/sabre/http/lib/Response.php', + 'Sabre\\HTTP\\ResponseDecorator' => __DIR__ . '/..' . '/sabre/http/lib/ResponseDecorator.php', + 'Sabre\\HTTP\\ResponseInterface' => __DIR__ . '/..' . '/sabre/http/lib/ResponseInterface.php', + 'Sabre\\HTTP\\Sapi' => __DIR__ . '/..' . '/sabre/http/lib/Sapi.php', + 'Sabre\\HTTP\\Version' => __DIR__ . '/..' . '/sabre/http/lib/Version.php', + 'Sabre\\Uri\\InvalidUriException' => __DIR__ . '/..' . '/sabre/uri/lib/InvalidUriException.php', + 'Sabre\\Uri\\Version' => __DIR__ . '/..' . '/sabre/uri/lib/Version.php', + 'Sabre\\VObject\\BirthdayCalendarGenerator' => __DIR__ . '/..' . '/sabre/vobject/lib/BirthdayCalendarGenerator.php', + 'Sabre\\VObject\\Cli' => __DIR__ . '/..' . '/sabre/vobject/lib/Cli.php', + 'Sabre\\VObject\\Component' => __DIR__ . '/..' . '/sabre/vobject/lib/Component.php', + 'Sabre\\VObject\\Component\\Available' => __DIR__ . '/..' . '/sabre/vobject/lib/Component/Available.php', + 'Sabre\\VObject\\Component\\VAlarm' => __DIR__ . '/..' . '/sabre/vobject/lib/Component/VAlarm.php', + 'Sabre\\VObject\\Component\\VAvailability' => __DIR__ . '/..' . '/sabre/vobject/lib/Component/VAvailability.php', + 'Sabre\\VObject\\Component\\VCalendar' => __DIR__ . '/..' . '/sabre/vobject/lib/Component/VCalendar.php', + 'Sabre\\VObject\\Component\\VCard' => __DIR__ . '/..' . '/sabre/vobject/lib/Component/VCard.php', + 'Sabre\\VObject\\Component\\VEvent' => __DIR__ . '/..' . '/sabre/vobject/lib/Component/VEvent.php', + 'Sabre\\VObject\\Component\\VFreeBusy' => __DIR__ . '/..' . '/sabre/vobject/lib/Component/VFreeBusy.php', + 'Sabre\\VObject\\Component\\VJournal' => __DIR__ . '/..' . '/sabre/vobject/lib/Component/VJournal.php', + 'Sabre\\VObject\\Component\\VTimeZone' => __DIR__ . '/..' . '/sabre/vobject/lib/Component/VTimeZone.php', + 'Sabre\\VObject\\Component\\VTodo' => __DIR__ . '/..' . '/sabre/vobject/lib/Component/VTodo.php', + 'Sabre\\VObject\\DateTimeParser' => __DIR__ . '/..' . '/sabre/vobject/lib/DateTimeParser.php', + 'Sabre\\VObject\\Document' => __DIR__ . '/..' . '/sabre/vobject/lib/Document.php', + 'Sabre\\VObject\\ElementList' => __DIR__ . '/..' . '/sabre/vobject/lib/ElementList.php', + 'Sabre\\VObject\\EofException' => __DIR__ . '/..' . '/sabre/vobject/lib/EofException.php', + 'Sabre\\VObject\\FreeBusyData' => __DIR__ . '/..' . '/sabre/vobject/lib/FreeBusyData.php', + 'Sabre\\VObject\\FreeBusyGenerator' => __DIR__ . '/..' . '/sabre/vobject/lib/FreeBusyGenerator.php', + 'Sabre\\VObject\\ITip\\Broker' => __DIR__ . '/..' . '/sabre/vobject/lib/ITip/Broker.php', + 'Sabre\\VObject\\ITip\\ITipException' => __DIR__ . '/..' . '/sabre/vobject/lib/ITip/ITipException.php', + 'Sabre\\VObject\\ITip\\Message' => __DIR__ . '/..' . '/sabre/vobject/lib/ITip/Message.php', + 'Sabre\\VObject\\ITip\\SameOrganizerForAllComponentsException' => __DIR__ . '/..' . '/sabre/vobject/lib/ITip/SameOrganizerForAllComponentsException.php', + 'Sabre\\VObject\\InvalidDataException' => __DIR__ . '/..' . '/sabre/vobject/lib/InvalidDataException.php', + 'Sabre\\VObject\\Node' => __DIR__ . '/..' . '/sabre/vobject/lib/Node.php', + 'Sabre\\VObject\\PHPUnitAssertions' => __DIR__ . '/..' . '/sabre/vobject/lib/PHPUnitAssertions.php', + 'Sabre\\VObject\\Parameter' => __DIR__ . '/..' . '/sabre/vobject/lib/Parameter.php', + 'Sabre\\VObject\\ParseException' => __DIR__ . '/..' . '/sabre/vobject/lib/ParseException.php', + 'Sabre\\VObject\\Parser\\Json' => __DIR__ . '/..' . '/sabre/vobject/lib/Parser/Json.php', + 'Sabre\\VObject\\Parser\\MimeDir' => __DIR__ . '/..' . '/sabre/vobject/lib/Parser/MimeDir.php', + 'Sabre\\VObject\\Parser\\Parser' => __DIR__ . '/..' . '/sabre/vobject/lib/Parser/Parser.php', + 'Sabre\\VObject\\Parser\\XML' => __DIR__ . '/..' . '/sabre/vobject/lib/Parser/XML.php', + 'Sabre\\VObject\\Parser\\XML\\Element\\KeyValue' => __DIR__ . '/..' . '/sabre/vobject/lib/Parser/XML/Element/KeyValue.php', + 'Sabre\\VObject\\Property' => __DIR__ . '/..' . '/sabre/vobject/lib/Property.php', + 'Sabre\\VObject\\Property\\Binary' => __DIR__ . '/..' . '/sabre/vobject/lib/Property/Binary.php', + 'Sabre\\VObject\\Property\\Boolean' => __DIR__ . '/..' . '/sabre/vobject/lib/Property/Boolean.php', + 'Sabre\\VObject\\Property\\FlatText' => __DIR__ . '/..' . '/sabre/vobject/lib/Property/FlatText.php', + 'Sabre\\VObject\\Property\\FloatValue' => __DIR__ . '/..' . '/sabre/vobject/lib/Property/FloatValue.php', + 'Sabre\\VObject\\Property\\ICalendar\\CalAddress' => __DIR__ . '/..' . '/sabre/vobject/lib/Property/ICalendar/CalAddress.php', + 'Sabre\\VObject\\Property\\ICalendar\\Date' => __DIR__ . '/..' . '/sabre/vobject/lib/Property/ICalendar/Date.php', + 'Sabre\\VObject\\Property\\ICalendar\\DateTime' => __DIR__ . '/..' . '/sabre/vobject/lib/Property/ICalendar/DateTime.php', + 'Sabre\\VObject\\Property\\ICalendar\\Duration' => __DIR__ . '/..' . '/sabre/vobject/lib/Property/ICalendar/Duration.php', + 'Sabre\\VObject\\Property\\ICalendar\\Period' => __DIR__ . '/..' . '/sabre/vobject/lib/Property/ICalendar/Period.php', + 'Sabre\\VObject\\Property\\ICalendar\\Recur' => __DIR__ . '/..' . '/sabre/vobject/lib/Property/ICalendar/Recur.php', + 'Sabre\\VObject\\Property\\IntegerValue' => __DIR__ . '/..' . '/sabre/vobject/lib/Property/IntegerValue.php', + 'Sabre\\VObject\\Property\\Text' => __DIR__ . '/..' . '/sabre/vobject/lib/Property/Text.php', + 'Sabre\\VObject\\Property\\Time' => __DIR__ . '/..' . '/sabre/vobject/lib/Property/Time.php', + 'Sabre\\VObject\\Property\\Unknown' => __DIR__ . '/..' . '/sabre/vobject/lib/Property/Unknown.php', + 'Sabre\\VObject\\Property\\Uri' => __DIR__ . '/..' . '/sabre/vobject/lib/Property/Uri.php', + 'Sabre\\VObject\\Property\\UtcOffset' => __DIR__ . '/..' . '/sabre/vobject/lib/Property/UtcOffset.php', + 'Sabre\\VObject\\Property\\VCard\\Date' => __DIR__ . '/..' . '/sabre/vobject/lib/Property/VCard/Date.php', + 'Sabre\\VObject\\Property\\VCard\\DateAndOrTime' => __DIR__ . '/..' . '/sabre/vobject/lib/Property/VCard/DateAndOrTime.php', + 'Sabre\\VObject\\Property\\VCard\\DateTime' => __DIR__ . '/..' . '/sabre/vobject/lib/Property/VCard/DateTime.php', + 'Sabre\\VObject\\Property\\VCard\\LanguageTag' => __DIR__ . '/..' . '/sabre/vobject/lib/Property/VCard/LanguageTag.php', + 'Sabre\\VObject\\Property\\VCard\\PhoneNumber' => __DIR__ . '/..' . '/sabre/vobject/lib/Property/VCard/PhoneNumber.php', + 'Sabre\\VObject\\Property\\VCard\\TimeStamp' => __DIR__ . '/..' . '/sabre/vobject/lib/Property/VCard/TimeStamp.php', + 'Sabre\\VObject\\Reader' => __DIR__ . '/..' . '/sabre/vobject/lib/Reader.php', + 'Sabre\\VObject\\Recur\\EventIterator' => __DIR__ . '/..' . '/sabre/vobject/lib/Recur/EventIterator.php', + 'Sabre\\VObject\\Recur\\MaxInstancesExceededException' => __DIR__ . '/..' . '/sabre/vobject/lib/Recur/MaxInstancesExceededException.php', + 'Sabre\\VObject\\Recur\\NoInstancesException' => __DIR__ . '/..' . '/sabre/vobject/lib/Recur/NoInstancesException.php', + 'Sabre\\VObject\\Recur\\RDateIterator' => __DIR__ . '/..' . '/sabre/vobject/lib/Recur/RDateIterator.php', + 'Sabre\\VObject\\Recur\\RRuleIterator' => __DIR__ . '/..' . '/sabre/vobject/lib/Recur/RRuleIterator.php', + 'Sabre\\VObject\\Settings' => __DIR__ . '/..' . '/sabre/vobject/lib/Settings.php', + 'Sabre\\VObject\\Splitter\\ICalendar' => __DIR__ . '/..' . '/sabre/vobject/lib/Splitter/ICalendar.php', + 'Sabre\\VObject\\Splitter\\SplitterInterface' => __DIR__ . '/..' . '/sabre/vobject/lib/Splitter/SplitterInterface.php', + 'Sabre\\VObject\\Splitter\\VCard' => __DIR__ . '/..' . '/sabre/vobject/lib/Splitter/VCard.php', + 'Sabre\\VObject\\StringUtil' => __DIR__ . '/..' . '/sabre/vobject/lib/StringUtil.php', + 'Sabre\\VObject\\TimeZoneUtil' => __DIR__ . '/..' . '/sabre/vobject/lib/TimeZoneUtil.php', + 'Sabre\\VObject\\TimezoneGuesser\\FindFromOffset' => __DIR__ . '/..' . '/sabre/vobject/lib/TimezoneGuesser/FindFromOffset.php', + 'Sabre\\VObject\\TimezoneGuesser\\FindFromTimezoneIdentifier' => __DIR__ . '/..' . '/sabre/vobject/lib/TimezoneGuesser/FindFromTimezoneIdentifier.php', + 'Sabre\\VObject\\TimezoneGuesser\\FindFromTimezoneMap' => __DIR__ . '/..' . '/sabre/vobject/lib/TimezoneGuesser/FindFromTimezoneMap.php', + 'Sabre\\VObject\\TimezoneGuesser\\GuessFromLicEntry' => __DIR__ . '/..' . '/sabre/vobject/lib/TimezoneGuesser/GuessFromLicEntry.php', + 'Sabre\\VObject\\TimezoneGuesser\\GuessFromMsTzId' => __DIR__ . '/..' . '/sabre/vobject/lib/TimezoneGuesser/GuessFromMsTzId.php', + 'Sabre\\VObject\\TimezoneGuesser\\TimezoneFinder' => __DIR__ . '/..' . '/sabre/vobject/lib/TimezoneGuesser/TimezoneFinder.php', + 'Sabre\\VObject\\TimezoneGuesser\\TimezoneGuesser' => __DIR__ . '/..' . '/sabre/vobject/lib/TimezoneGuesser/TimezoneGuesser.php', + 'Sabre\\VObject\\UUIDUtil' => __DIR__ . '/..' . '/sabre/vobject/lib/UUIDUtil.php', + 'Sabre\\VObject\\VCardConverter' => __DIR__ . '/..' . '/sabre/vobject/lib/VCardConverter.php', + 'Sabre\\VObject\\Version' => __DIR__ . '/..' . '/sabre/vobject/lib/Version.php', + 'Sabre\\VObject\\Writer' => __DIR__ . '/..' . '/sabre/vobject/lib/Writer.php', + 'Sabre\\Xml\\ContextStackTrait' => __DIR__ . '/..' . '/sabre/xml/lib/ContextStackTrait.php', + 'Sabre\\Xml\\Element' => __DIR__ . '/..' . '/sabre/xml/lib/Element.php', + 'Sabre\\Xml\\Element\\Base' => __DIR__ . '/..' . '/sabre/xml/lib/Element/Base.php', + 'Sabre\\Xml\\Element\\Cdata' => __DIR__ . '/..' . '/sabre/xml/lib/Element/Cdata.php', + 'Sabre\\Xml\\Element\\Elements' => __DIR__ . '/..' . '/sabre/xml/lib/Element/Elements.php', + 'Sabre\\Xml\\Element\\KeyValue' => __DIR__ . '/..' . '/sabre/xml/lib/Element/KeyValue.php', + 'Sabre\\Xml\\Element\\Uri' => __DIR__ . '/..' . '/sabre/xml/lib/Element/Uri.php', + 'Sabre\\Xml\\Element\\XmlFragment' => __DIR__ . '/..' . '/sabre/xml/lib/Element/XmlFragment.php', + 'Sabre\\Xml\\LibXMLException' => __DIR__ . '/..' . '/sabre/xml/lib/LibXMLException.php', + 'Sabre\\Xml\\ParseException' => __DIR__ . '/..' . '/sabre/xml/lib/ParseException.php', + 'Sabre\\Xml\\Reader' => __DIR__ . '/..' . '/sabre/xml/lib/Reader.php', + 'Sabre\\Xml\\Service' => __DIR__ . '/..' . '/sabre/xml/lib/Service.php', + 'Sabre\\Xml\\Version' => __DIR__ . '/..' . '/sabre/xml/lib/Version.php', + 'Sabre\\Xml\\Writer' => __DIR__ . '/..' . '/sabre/xml/lib/Writer.php', + 'Sabre\\Xml\\XmlDeserializable' => __DIR__ . '/..' . '/sabre/xml/lib/XmlDeserializable.php', + 'Sabre\\Xml\\XmlSerializable' => __DIR__ . '/..' . '/sabre/xml/lib/XmlSerializable.php', + 'SearchDAV\\Backend\\ISearchBackend' => __DIR__ . '/..' . '/icewind/searchdav/src/Backend/ISearchBackend.php', + 'SearchDAV\\Backend\\SearchPropertyDefinition' => __DIR__ . '/..' . '/icewind/searchdav/src/Backend/SearchPropertyDefinition.php', + 'SearchDAV\\Backend\\SearchResult' => __DIR__ . '/..' . '/icewind/searchdav/src/Backend/SearchResult.php', + 'SearchDAV\\DAV\\DiscoverHandler' => __DIR__ . '/..' . '/icewind/searchdav/src/DAV/DiscoverHandler.php', + 'SearchDAV\\DAV\\PathHelper' => __DIR__ . '/..' . '/icewind/searchdav/src/DAV/PathHelper.php', + 'SearchDAV\\DAV\\QueryParser' => __DIR__ . '/..' . '/icewind/searchdav/src/DAV/QueryParser.php', + 'SearchDAV\\DAV\\SearchHandler' => __DIR__ . '/..' . '/icewind/searchdav/src/DAV/SearchHandler.php', + 'SearchDAV\\DAV\\SearchPlugin' => __DIR__ . '/..' . '/icewind/searchdav/src/DAV/SearchPlugin.php', + 'SearchDAV\\Query\\Limit' => __DIR__ . '/..' . '/icewind/searchdav/src/Query/Limit.php', + 'SearchDAV\\Query\\Literal' => __DIR__ . '/..' . '/icewind/searchdav/src/Query/Literal.php', + 'SearchDAV\\Query\\Operator' => __DIR__ . '/..' . '/icewind/searchdav/src/Query/Operator.php', + 'SearchDAV\\Query\\Order' => __DIR__ . '/..' . '/icewind/searchdav/src/Query/Order.php', + 'SearchDAV\\Query\\Query' => __DIR__ . '/..' . '/icewind/searchdav/src/Query/Query.php', + 'SearchDAV\\Query\\Scope' => __DIR__ . '/..' . '/icewind/searchdav/src/Query/Scope.php', + 'SearchDAV\\XML\\BasicSearch' => __DIR__ . '/..' . '/icewind/searchdav/src/XML/BasicSearch.php', + 'SearchDAV\\XML\\BasicSearchSchema' => __DIR__ . '/..' . '/icewind/searchdav/src/XML/BasicSearchSchema.php', + 'SearchDAV\\XML\\Limit' => __DIR__ . '/..' . '/icewind/searchdav/src/XML/Limit.php', + 'SearchDAV\\XML\\Literal' => __DIR__ . '/..' . '/icewind/searchdav/src/XML/Literal.php', + 'SearchDAV\\XML\\Operator' => __DIR__ . '/..' . '/icewind/searchdav/src/XML/Operator.php', + 'SearchDAV\\XML\\Order' => __DIR__ . '/..' . '/icewind/searchdav/src/XML/Order.php', + 'SearchDAV\\XML\\PropDesc' => __DIR__ . '/..' . '/icewind/searchdav/src/XML/PropDesc.php', + 'SearchDAV\\XML\\QueryDiscoverResponse' => __DIR__ . '/..' . '/icewind/searchdav/src/XML/QueryDiscoverResponse.php', + 'SearchDAV\\XML\\Scope' => __DIR__ . '/..' . '/icewind/searchdav/src/XML/Scope.php', + 'SearchDAV\\XML\\SupportedQueryGrammar' => __DIR__ . '/..' . '/icewind/searchdav/src/XML/SupportedQueryGrammar.php', + 'SensitiveParameter' => __DIR__ . '/..' . '/symfony/polyfill-php82/Resources/stubs/SensitiveParameter.php', + 'SensitiveParameterValue' => __DIR__ . '/..' . '/symfony/polyfill-php82/Resources/stubs/SensitiveParameterValue.php', + 'SpomkyLabs\\Pki\\ASN1\\Component\\Identifier' => __DIR__ . '/..' . '/spomky-labs/pki-framework/src/ASN1/Component/Identifier.php', + 'SpomkyLabs\\Pki\\ASN1\\Component\\Length' => __DIR__ . '/..' . '/spomky-labs/pki-framework/src/ASN1/Component/Length.php', + 'SpomkyLabs\\Pki\\ASN1\\DERData' => __DIR__ . '/..' . '/spomky-labs/pki-framework/src/ASN1/DERData.php', + 'SpomkyLabs\\Pki\\ASN1\\Element' => __DIR__ . '/..' . '/spomky-labs/pki-framework/src/ASN1/Element.php', + 'SpomkyLabs\\Pki\\ASN1\\Exception\\DecodeException' => __DIR__ . '/..' . '/spomky-labs/pki-framework/src/ASN1/Exception/DecodeException.php', + 'SpomkyLabs\\Pki\\ASN1\\Feature\\ElementBase' => __DIR__ . '/..' . '/spomky-labs/pki-framework/src/ASN1/Feature/ElementBase.php', + 'SpomkyLabs\\Pki\\ASN1\\Feature\\Encodable' => __DIR__ . '/..' . '/spomky-labs/pki-framework/src/ASN1/Feature/Encodable.php', + 'SpomkyLabs\\Pki\\ASN1\\Feature\\Stringable' => __DIR__ . '/..' . '/spomky-labs/pki-framework/src/ASN1/Feature/Stringable.php', + 'SpomkyLabs\\Pki\\ASN1\\Type\\BaseString' => __DIR__ . '/..' . '/spomky-labs/pki-framework/src/ASN1/Type/BaseString.php', + 'SpomkyLabs\\Pki\\ASN1\\Type\\BaseTime' => __DIR__ . '/..' . '/spomky-labs/pki-framework/src/ASN1/Type/BaseTime.php', + 'SpomkyLabs\\Pki\\ASN1\\Type\\Constructed\\ConstructedString' => __DIR__ . '/..' . '/spomky-labs/pki-framework/src/ASN1/Type/Constructed/ConstructedString.php', + 'SpomkyLabs\\Pki\\ASN1\\Type\\Constructed\\Sequence' => __DIR__ . '/..' . '/spomky-labs/pki-framework/src/ASN1/Type/Constructed/Sequence.php', + 'SpomkyLabs\\Pki\\ASN1\\Type\\Constructed\\Set' => __DIR__ . '/..' . '/spomky-labs/pki-framework/src/ASN1/Type/Constructed/Set.php', + 'SpomkyLabs\\Pki\\ASN1\\Type\\PrimitiveString' => __DIR__ . '/..' . '/spomky-labs/pki-framework/src/ASN1/Type/PrimitiveString.php', + 'SpomkyLabs\\Pki\\ASN1\\Type\\PrimitiveType' => __DIR__ . '/..' . '/spomky-labs/pki-framework/src/ASN1/Type/PrimitiveType.php', + 'SpomkyLabs\\Pki\\ASN1\\Type\\Primitive\\BMPString' => __DIR__ . '/..' . '/spomky-labs/pki-framework/src/ASN1/Type/Primitive/BMPString.php', + 'SpomkyLabs\\Pki\\ASN1\\Type\\Primitive\\BitString' => __DIR__ . '/..' . '/spomky-labs/pki-framework/src/ASN1/Type/Primitive/BitString.php', + 'SpomkyLabs\\Pki\\ASN1\\Type\\Primitive\\Boolean' => __DIR__ . '/..' . '/spomky-labs/pki-framework/src/ASN1/Type/Primitive/Boolean.php', + 'SpomkyLabs\\Pki\\ASN1\\Type\\Primitive\\CharacterString' => __DIR__ . '/..' . '/spomky-labs/pki-framework/src/ASN1/Type/Primitive/CharacterString.php', + 'SpomkyLabs\\Pki\\ASN1\\Type\\Primitive\\EOC' => __DIR__ . '/..' . '/spomky-labs/pki-framework/src/ASN1/Type/Primitive/EOC.php', + 'SpomkyLabs\\Pki\\ASN1\\Type\\Primitive\\Enumerated' => __DIR__ . '/..' . '/spomky-labs/pki-framework/src/ASN1/Type/Primitive/Enumerated.php', + 'SpomkyLabs\\Pki\\ASN1\\Type\\Primitive\\GeneralString' => __DIR__ . '/..' . '/spomky-labs/pki-framework/src/ASN1/Type/Primitive/GeneralString.php', + 'SpomkyLabs\\Pki\\ASN1\\Type\\Primitive\\GeneralizedTime' => __DIR__ . '/..' . '/spomky-labs/pki-framework/src/ASN1/Type/Primitive/GeneralizedTime.php', + 'SpomkyLabs\\Pki\\ASN1\\Type\\Primitive\\GraphicString' => __DIR__ . '/..' . '/spomky-labs/pki-framework/src/ASN1/Type/Primitive/GraphicString.php', + 'SpomkyLabs\\Pki\\ASN1\\Type\\Primitive\\IA5String' => __DIR__ . '/..' . '/spomky-labs/pki-framework/src/ASN1/Type/Primitive/IA5String.php', + 'SpomkyLabs\\Pki\\ASN1\\Type\\Primitive\\Integer' => __DIR__ . '/..' . '/spomky-labs/pki-framework/src/ASN1/Type/Primitive/Integer.php', + 'SpomkyLabs\\Pki\\ASN1\\Type\\Primitive\\NullType' => __DIR__ . '/..' . '/spomky-labs/pki-framework/src/ASN1/Type/Primitive/NullType.php', + 'SpomkyLabs\\Pki\\ASN1\\Type\\Primitive\\Number' => __DIR__ . '/..' . '/spomky-labs/pki-framework/src/ASN1/Type/Primitive/Number.php', + 'SpomkyLabs\\Pki\\ASN1\\Type\\Primitive\\NumericString' => __DIR__ . '/..' . '/spomky-labs/pki-framework/src/ASN1/Type/Primitive/NumericString.php', + 'SpomkyLabs\\Pki\\ASN1\\Type\\Primitive\\ObjectDescriptor' => __DIR__ . '/..' . '/spomky-labs/pki-framework/src/ASN1/Type/Primitive/ObjectDescriptor.php', + 'SpomkyLabs\\Pki\\ASN1\\Type\\Primitive\\ObjectIdentifier' => __DIR__ . '/..' . '/spomky-labs/pki-framework/src/ASN1/Type/Primitive/ObjectIdentifier.php', + 'SpomkyLabs\\Pki\\ASN1\\Type\\Primitive\\OctetString' => __DIR__ . '/..' . '/spomky-labs/pki-framework/src/ASN1/Type/Primitive/OctetString.php', + 'SpomkyLabs\\Pki\\ASN1\\Type\\Primitive\\PrintableString' => __DIR__ . '/..' . '/spomky-labs/pki-framework/src/ASN1/Type/Primitive/PrintableString.php', + 'SpomkyLabs\\Pki\\ASN1\\Type\\Primitive\\Real' => __DIR__ . '/..' . '/spomky-labs/pki-framework/src/ASN1/Type/Primitive/Real.php', + 'SpomkyLabs\\Pki\\ASN1\\Type\\Primitive\\RelativeOID' => __DIR__ . '/..' . '/spomky-labs/pki-framework/src/ASN1/Type/Primitive/RelativeOID.php', + 'SpomkyLabs\\Pki\\ASN1\\Type\\Primitive\\T61String' => __DIR__ . '/..' . '/spomky-labs/pki-framework/src/ASN1/Type/Primitive/T61String.php', + 'SpomkyLabs\\Pki\\ASN1\\Type\\Primitive\\UTCTime' => __DIR__ . '/..' . '/spomky-labs/pki-framework/src/ASN1/Type/Primitive/UTCTime.php', + 'SpomkyLabs\\Pki\\ASN1\\Type\\Primitive\\UTF8String' => __DIR__ . '/..' . '/spomky-labs/pki-framework/src/ASN1/Type/Primitive/UTF8String.php', + 'SpomkyLabs\\Pki\\ASN1\\Type\\Primitive\\UniversalString' => __DIR__ . '/..' . '/spomky-labs/pki-framework/src/ASN1/Type/Primitive/UniversalString.php', + 'SpomkyLabs\\Pki\\ASN1\\Type\\Primitive\\VideotexString' => __DIR__ . '/..' . '/spomky-labs/pki-framework/src/ASN1/Type/Primitive/VideotexString.php', + 'SpomkyLabs\\Pki\\ASN1\\Type\\Primitive\\VisibleString' => __DIR__ . '/..' . '/spomky-labs/pki-framework/src/ASN1/Type/Primitive/VisibleString.php', + 'SpomkyLabs\\Pki\\ASN1\\Type\\StringType' => __DIR__ . '/..' . '/spomky-labs/pki-framework/src/ASN1/Type/StringType.php', + 'SpomkyLabs\\Pki\\ASN1\\Type\\Structure' => __DIR__ . '/..' . '/spomky-labs/pki-framework/src/ASN1/Type/Structure.php', + 'SpomkyLabs\\Pki\\ASN1\\Type\\TaggedType' => __DIR__ . '/..' . '/spomky-labs/pki-framework/src/ASN1/Type/TaggedType.php', + 'SpomkyLabs\\Pki\\ASN1\\Type\\Tagged\\ApplicationType' => __DIR__ . '/..' . '/spomky-labs/pki-framework/src/ASN1/Type/Tagged/ApplicationType.php', + 'SpomkyLabs\\Pki\\ASN1\\Type\\Tagged\\ContextSpecificType' => __DIR__ . '/..' . '/spomky-labs/pki-framework/src/ASN1/Type/Tagged/ContextSpecificType.php', + 'SpomkyLabs\\Pki\\ASN1\\Type\\Tagged\\DERTaggedType' => __DIR__ . '/..' . '/spomky-labs/pki-framework/src/ASN1/Type/Tagged/DERTaggedType.php', + 'SpomkyLabs\\Pki\\ASN1\\Type\\Tagged\\ExplicitTagging' => __DIR__ . '/..' . '/spomky-labs/pki-framework/src/ASN1/Type/Tagged/ExplicitTagging.php', + 'SpomkyLabs\\Pki\\ASN1\\Type\\Tagged\\ExplicitlyTaggedType' => __DIR__ . '/..' . '/spomky-labs/pki-framework/src/ASN1/Type/Tagged/ExplicitlyTaggedType.php', + 'SpomkyLabs\\Pki\\ASN1\\Type\\Tagged\\ImplicitTagging' => __DIR__ . '/..' . '/spomky-labs/pki-framework/src/ASN1/Type/Tagged/ImplicitTagging.php', + 'SpomkyLabs\\Pki\\ASN1\\Type\\Tagged\\ImplicitlyTaggedType' => __DIR__ . '/..' . '/spomky-labs/pki-framework/src/ASN1/Type/Tagged/ImplicitlyTaggedType.php', + 'SpomkyLabs\\Pki\\ASN1\\Type\\Tagged\\PrivateType' => __DIR__ . '/..' . '/spomky-labs/pki-framework/src/ASN1/Type/Tagged/PrivateType.php', + 'SpomkyLabs\\Pki\\ASN1\\Type\\Tagged\\TaggedTypeWrap' => __DIR__ . '/..' . '/spomky-labs/pki-framework/src/ASN1/Type/Tagged/TaggedTypeWrap.php', + 'SpomkyLabs\\Pki\\ASN1\\Type\\TimeType' => __DIR__ . '/..' . '/spomky-labs/pki-framework/src/ASN1/Type/TimeType.php', + 'SpomkyLabs\\Pki\\ASN1\\Type\\UniversalClass' => __DIR__ . '/..' . '/spomky-labs/pki-framework/src/ASN1/Type/UniversalClass.php', + 'SpomkyLabs\\Pki\\ASN1\\Type\\UnspecifiedType' => __DIR__ . '/..' . '/spomky-labs/pki-framework/src/ASN1/Type/UnspecifiedType.php', + 'SpomkyLabs\\Pki\\ASN1\\Util\\BigInt' => __DIR__ . '/..' . '/spomky-labs/pki-framework/src/ASN1/Util/BigInt.php', + 'SpomkyLabs\\Pki\\ASN1\\Util\\Flags' => __DIR__ . '/..' . '/spomky-labs/pki-framework/src/ASN1/Util/Flags.php', + 'SpomkyLabs\\Pki\\CryptoBridge\\Crypto' => __DIR__ . '/..' . '/spomky-labs/pki-framework/src/CryptoBridge/Crypto.php', + 'SpomkyLabs\\Pki\\CryptoBridge\\Crypto\\OpenSSLCrypto' => __DIR__ . '/..' . '/spomky-labs/pki-framework/src/CryptoBridge/Crypto/OpenSSLCrypto.php', + 'SpomkyLabs\\Pki\\CryptoEncoding\\PEM' => __DIR__ . '/..' . '/spomky-labs/pki-framework/src/CryptoEncoding/PEM.php', + 'SpomkyLabs\\Pki\\CryptoEncoding\\PEMBundle' => __DIR__ . '/..' . '/spomky-labs/pki-framework/src/CryptoEncoding/PEMBundle.php', + 'SpomkyLabs\\Pki\\CryptoTypes\\AlgorithmIdentifier\\AlgorithmIdentifier' => __DIR__ . '/..' . '/spomky-labs/pki-framework/src/CryptoTypes/AlgorithmIdentifier/AlgorithmIdentifier.php', + 'SpomkyLabs\\Pki\\CryptoTypes\\AlgorithmIdentifier\\AlgorithmIdentifierFactory' => __DIR__ . '/..' . '/spomky-labs/pki-framework/src/CryptoTypes/AlgorithmIdentifier/AlgorithmIdentifierFactory.php', + 'SpomkyLabs\\Pki\\CryptoTypes\\AlgorithmIdentifier\\AlgorithmIdentifierProvider' => __DIR__ . '/..' . '/spomky-labs/pki-framework/src/CryptoTypes/AlgorithmIdentifier/AlgorithmIdentifierProvider.php', + 'SpomkyLabs\\Pki\\CryptoTypes\\AlgorithmIdentifier\\Asymmetric\\ECPublicKeyAlgorithmIdentifier' => __DIR__ . '/..' . '/spomky-labs/pki-framework/src/CryptoTypes/AlgorithmIdentifier/Asymmetric/ECPublicKeyAlgorithmIdentifier.php', + 'SpomkyLabs\\Pki\\CryptoTypes\\AlgorithmIdentifier\\Asymmetric\\Ed25519AlgorithmIdentifier' => __DIR__ . '/..' . '/spomky-labs/pki-framework/src/CryptoTypes/AlgorithmIdentifier/Asymmetric/Ed25519AlgorithmIdentifier.php', + 'SpomkyLabs\\Pki\\CryptoTypes\\AlgorithmIdentifier\\Asymmetric\\Ed448AlgorithmIdentifier' => __DIR__ . '/..' . '/spomky-labs/pki-framework/src/CryptoTypes/AlgorithmIdentifier/Asymmetric/Ed448AlgorithmIdentifier.php', + 'SpomkyLabs\\Pki\\CryptoTypes\\AlgorithmIdentifier\\Asymmetric\\RFC8410EdAlgorithmIdentifier' => __DIR__ . '/..' . '/spomky-labs/pki-framework/src/CryptoTypes/AlgorithmIdentifier/Asymmetric/RFC8410EdAlgorithmIdentifier.php', + 'SpomkyLabs\\Pki\\CryptoTypes\\AlgorithmIdentifier\\Asymmetric\\RFC8410XAlgorithmIdentifier' => __DIR__ . '/..' . '/spomky-labs/pki-framework/src/CryptoTypes/AlgorithmIdentifier/Asymmetric/RFC8410XAlgorithmIdentifier.php', + 'SpomkyLabs\\Pki\\CryptoTypes\\AlgorithmIdentifier\\Asymmetric\\RSAEncryptionAlgorithmIdentifier' => __DIR__ . '/..' . '/spomky-labs/pki-framework/src/CryptoTypes/AlgorithmIdentifier/Asymmetric/RSAEncryptionAlgorithmIdentifier.php', + 'SpomkyLabs\\Pki\\CryptoTypes\\AlgorithmIdentifier\\Asymmetric\\RSAPSSSSAEncryptionAlgorithmIdentifier' => __DIR__ . '/..' . '/spomky-labs/pki-framework/src/CryptoTypes/AlgorithmIdentifier/Asymmetric/RSAPSSSSAEncryptionAlgorithmIdentifier.php', + 'SpomkyLabs\\Pki\\CryptoTypes\\AlgorithmIdentifier\\Asymmetric\\X25519AlgorithmIdentifier' => __DIR__ . '/..' . '/spomky-labs/pki-framework/src/CryptoTypes/AlgorithmIdentifier/Asymmetric/X25519AlgorithmIdentifier.php', + 'SpomkyLabs\\Pki\\CryptoTypes\\AlgorithmIdentifier\\Asymmetric\\X448AlgorithmIdentifier' => __DIR__ . '/..' . '/spomky-labs/pki-framework/src/CryptoTypes/AlgorithmIdentifier/Asymmetric/X448AlgorithmIdentifier.php', + 'SpomkyLabs\\Pki\\CryptoTypes\\AlgorithmIdentifier\\Cipher\\AES128CBCAlgorithmIdentifier' => __DIR__ . '/..' . '/spomky-labs/pki-framework/src/CryptoTypes/AlgorithmIdentifier/Cipher/AES128CBCAlgorithmIdentifier.php', + 'SpomkyLabs\\Pki\\CryptoTypes\\AlgorithmIdentifier\\Cipher\\AES192CBCAlgorithmIdentifier' => __DIR__ . '/..' . '/spomky-labs/pki-framework/src/CryptoTypes/AlgorithmIdentifier/Cipher/AES192CBCAlgorithmIdentifier.php', + 'SpomkyLabs\\Pki\\CryptoTypes\\AlgorithmIdentifier\\Cipher\\AES256CBCAlgorithmIdentifier' => __DIR__ . '/..' . '/spomky-labs/pki-framework/src/CryptoTypes/AlgorithmIdentifier/Cipher/AES256CBCAlgorithmIdentifier.php', + 'SpomkyLabs\\Pki\\CryptoTypes\\AlgorithmIdentifier\\Cipher\\AESCBCAlgorithmIdentifier' => __DIR__ . '/..' . '/spomky-labs/pki-framework/src/CryptoTypes/AlgorithmIdentifier/Cipher/AESCBCAlgorithmIdentifier.php', + 'SpomkyLabs\\Pki\\CryptoTypes\\AlgorithmIdentifier\\Cipher\\BlockCipherAlgorithmIdentifier' => __DIR__ . '/..' . '/spomky-labs/pki-framework/src/CryptoTypes/AlgorithmIdentifier/Cipher/BlockCipherAlgorithmIdentifier.php', + 'SpomkyLabs\\Pki\\CryptoTypes\\AlgorithmIdentifier\\Cipher\\CipherAlgorithmIdentifier' => __DIR__ . '/..' . '/spomky-labs/pki-framework/src/CryptoTypes/AlgorithmIdentifier/Cipher/CipherAlgorithmIdentifier.php', + 'SpomkyLabs\\Pki\\CryptoTypes\\AlgorithmIdentifier\\Cipher\\DESCBCAlgorithmIdentifier' => __DIR__ . '/..' . '/spomky-labs/pki-framework/src/CryptoTypes/AlgorithmIdentifier/Cipher/DESCBCAlgorithmIdentifier.php', + 'SpomkyLabs\\Pki\\CryptoTypes\\AlgorithmIdentifier\\Cipher\\DESEDE3CBCAlgorithmIdentifier' => __DIR__ . '/..' . '/spomky-labs/pki-framework/src/CryptoTypes/AlgorithmIdentifier/Cipher/DESEDE3CBCAlgorithmIdentifier.php', + 'SpomkyLabs\\Pki\\CryptoTypes\\AlgorithmIdentifier\\Cipher\\RC2CBCAlgorithmIdentifier' => __DIR__ . '/..' . '/spomky-labs/pki-framework/src/CryptoTypes/AlgorithmIdentifier/Cipher/RC2CBCAlgorithmIdentifier.php', + 'SpomkyLabs\\Pki\\CryptoTypes\\AlgorithmIdentifier\\Feature\\AlgorithmIdentifierType' => __DIR__ . '/..' . '/spomky-labs/pki-framework/src/CryptoTypes/AlgorithmIdentifier/Feature/AlgorithmIdentifierType.php', + 'SpomkyLabs\\Pki\\CryptoTypes\\AlgorithmIdentifier\\Feature\\AsymmetricCryptoAlgorithmIdentifier' => __DIR__ . '/..' . '/spomky-labs/pki-framework/src/CryptoTypes/AlgorithmIdentifier/Feature/AsymmetricCryptoAlgorithmIdentifier.php', + 'SpomkyLabs\\Pki\\CryptoTypes\\AlgorithmIdentifier\\Feature\\EncryptionAlgorithmIdentifier' => __DIR__ . '/..' . '/spomky-labs/pki-framework/src/CryptoTypes/AlgorithmIdentifier/Feature/EncryptionAlgorithmIdentifier.php', + 'SpomkyLabs\\Pki\\CryptoTypes\\AlgorithmIdentifier\\Feature\\HashAlgorithmIdentifier' => __DIR__ . '/..' . '/spomky-labs/pki-framework/src/CryptoTypes/AlgorithmIdentifier/Feature/HashAlgorithmIdentifier.php', + 'SpomkyLabs\\Pki\\CryptoTypes\\AlgorithmIdentifier\\Feature\\PRFAlgorithmIdentifier' => __DIR__ . '/..' . '/spomky-labs/pki-framework/src/CryptoTypes/AlgorithmIdentifier/Feature/PRFAlgorithmIdentifier.php', + 'SpomkyLabs\\Pki\\CryptoTypes\\AlgorithmIdentifier\\Feature\\SignatureAlgorithmIdentifier' => __DIR__ . '/..' . '/spomky-labs/pki-framework/src/CryptoTypes/AlgorithmIdentifier/Feature/SignatureAlgorithmIdentifier.php', + 'SpomkyLabs\\Pki\\CryptoTypes\\AlgorithmIdentifier\\GenericAlgorithmIdentifier' => __DIR__ . '/..' . '/spomky-labs/pki-framework/src/CryptoTypes/AlgorithmIdentifier/GenericAlgorithmIdentifier.php', + 'SpomkyLabs\\Pki\\CryptoTypes\\AlgorithmIdentifier\\Hash\\HMACWithSHA1AlgorithmIdentifier' => __DIR__ . '/..' . '/spomky-labs/pki-framework/src/CryptoTypes/AlgorithmIdentifier/Hash/HMACWithSHA1AlgorithmIdentifier.php', + 'SpomkyLabs\\Pki\\CryptoTypes\\AlgorithmIdentifier\\Hash\\HMACWithSHA224AlgorithmIdentifier' => __DIR__ . '/..' . '/spomky-labs/pki-framework/src/CryptoTypes/AlgorithmIdentifier/Hash/HMACWithSHA224AlgorithmIdentifier.php', + 'SpomkyLabs\\Pki\\CryptoTypes\\AlgorithmIdentifier\\Hash\\HMACWithSHA256AlgorithmIdentifier' => __DIR__ . '/..' . '/spomky-labs/pki-framework/src/CryptoTypes/AlgorithmIdentifier/Hash/HMACWithSHA256AlgorithmIdentifier.php', + 'SpomkyLabs\\Pki\\CryptoTypes\\AlgorithmIdentifier\\Hash\\HMACWithSHA384AlgorithmIdentifier' => __DIR__ . '/..' . '/spomky-labs/pki-framework/src/CryptoTypes/AlgorithmIdentifier/Hash/HMACWithSHA384AlgorithmIdentifier.php', + 'SpomkyLabs\\Pki\\CryptoTypes\\AlgorithmIdentifier\\Hash\\HMACWithSHA512AlgorithmIdentifier' => __DIR__ . '/..' . '/spomky-labs/pki-framework/src/CryptoTypes/AlgorithmIdentifier/Hash/HMACWithSHA512AlgorithmIdentifier.php', + 'SpomkyLabs\\Pki\\CryptoTypes\\AlgorithmIdentifier\\Hash\\MD5AlgorithmIdentifier' => __DIR__ . '/..' . '/spomky-labs/pki-framework/src/CryptoTypes/AlgorithmIdentifier/Hash/MD5AlgorithmIdentifier.php', + 'SpomkyLabs\\Pki\\CryptoTypes\\AlgorithmIdentifier\\Hash\\RFC4231HMACAlgorithmIdentifier' => __DIR__ . '/..' . '/spomky-labs/pki-framework/src/CryptoTypes/AlgorithmIdentifier/Hash/RFC4231HMACAlgorithmIdentifier.php', + 'SpomkyLabs\\Pki\\CryptoTypes\\AlgorithmIdentifier\\Hash\\SHA1AlgorithmIdentifier' => __DIR__ . '/..' . '/spomky-labs/pki-framework/src/CryptoTypes/AlgorithmIdentifier/Hash/SHA1AlgorithmIdentifier.php', + 'SpomkyLabs\\Pki\\CryptoTypes\\AlgorithmIdentifier\\Hash\\SHA224AlgorithmIdentifier' => __DIR__ . '/..' . '/spomky-labs/pki-framework/src/CryptoTypes/AlgorithmIdentifier/Hash/SHA224AlgorithmIdentifier.php', + 'SpomkyLabs\\Pki\\CryptoTypes\\AlgorithmIdentifier\\Hash\\SHA256AlgorithmIdentifier' => __DIR__ . '/..' . '/spomky-labs/pki-framework/src/CryptoTypes/AlgorithmIdentifier/Hash/SHA256AlgorithmIdentifier.php', + 'SpomkyLabs\\Pki\\CryptoTypes\\AlgorithmIdentifier\\Hash\\SHA2AlgorithmIdentifier' => __DIR__ . '/..' . '/spomky-labs/pki-framework/src/CryptoTypes/AlgorithmIdentifier/Hash/SHA2AlgorithmIdentifier.php', + 'SpomkyLabs\\Pki\\CryptoTypes\\AlgorithmIdentifier\\Hash\\SHA384AlgorithmIdentifier' => __DIR__ . '/..' . '/spomky-labs/pki-framework/src/CryptoTypes/AlgorithmIdentifier/Hash/SHA384AlgorithmIdentifier.php', + 'SpomkyLabs\\Pki\\CryptoTypes\\AlgorithmIdentifier\\Hash\\SHA512AlgorithmIdentifier' => __DIR__ . '/..' . '/spomky-labs/pki-framework/src/CryptoTypes/AlgorithmIdentifier/Hash/SHA512AlgorithmIdentifier.php', + 'SpomkyLabs\\Pki\\CryptoTypes\\AlgorithmIdentifier\\Signature\\ECDSAWithSHA1AlgorithmIdentifier' => __DIR__ . '/..' . '/spomky-labs/pki-framework/src/CryptoTypes/AlgorithmIdentifier/Signature/ECDSAWithSHA1AlgorithmIdentifier.php', + 'SpomkyLabs\\Pki\\CryptoTypes\\AlgorithmIdentifier\\Signature\\ECDSAWithSHA224AlgorithmIdentifier' => __DIR__ . '/..' . '/spomky-labs/pki-framework/src/CryptoTypes/AlgorithmIdentifier/Signature/ECDSAWithSHA224AlgorithmIdentifier.php', + 'SpomkyLabs\\Pki\\CryptoTypes\\AlgorithmIdentifier\\Signature\\ECDSAWithSHA256AlgorithmIdentifier' => __DIR__ . '/..' . '/spomky-labs/pki-framework/src/CryptoTypes/AlgorithmIdentifier/Signature/ECDSAWithSHA256AlgorithmIdentifier.php', + 'SpomkyLabs\\Pki\\CryptoTypes\\AlgorithmIdentifier\\Signature\\ECDSAWithSHA384AlgorithmIdentifier' => __DIR__ . '/..' . '/spomky-labs/pki-framework/src/CryptoTypes/AlgorithmIdentifier/Signature/ECDSAWithSHA384AlgorithmIdentifier.php', + 'SpomkyLabs\\Pki\\CryptoTypes\\AlgorithmIdentifier\\Signature\\ECDSAWithSHA512AlgorithmIdentifier' => __DIR__ . '/..' . '/spomky-labs/pki-framework/src/CryptoTypes/AlgorithmIdentifier/Signature/ECDSAWithSHA512AlgorithmIdentifier.php', + 'SpomkyLabs\\Pki\\CryptoTypes\\AlgorithmIdentifier\\Signature\\ECSignatureAlgorithmIdentifier' => __DIR__ . '/..' . '/spomky-labs/pki-framework/src/CryptoTypes/AlgorithmIdentifier/Signature/ECSignatureAlgorithmIdentifier.php', + 'SpomkyLabs\\Pki\\CryptoTypes\\AlgorithmIdentifier\\Signature\\MD2WithRSAEncryptionAlgorithmIdentifier' => __DIR__ . '/..' . '/spomky-labs/pki-framework/src/CryptoTypes/AlgorithmIdentifier/Signature/MD2WithRSAEncryptionAlgorithmIdentifier.php', + 'SpomkyLabs\\Pki\\CryptoTypes\\AlgorithmIdentifier\\Signature\\MD4WithRSAEncryptionAlgorithmIdentifier' => __DIR__ . '/..' . '/spomky-labs/pki-framework/src/CryptoTypes/AlgorithmIdentifier/Signature/MD4WithRSAEncryptionAlgorithmIdentifier.php', + 'SpomkyLabs\\Pki\\CryptoTypes\\AlgorithmIdentifier\\Signature\\MD5WithRSAEncryptionAlgorithmIdentifier' => __DIR__ . '/..' . '/spomky-labs/pki-framework/src/CryptoTypes/AlgorithmIdentifier/Signature/MD5WithRSAEncryptionAlgorithmIdentifier.php', + 'SpomkyLabs\\Pki\\CryptoTypes\\AlgorithmIdentifier\\Signature\\RFC3279RSASignatureAlgorithmIdentifier' => __DIR__ . '/..' . '/spomky-labs/pki-framework/src/CryptoTypes/AlgorithmIdentifier/Signature/RFC3279RSASignatureAlgorithmIdentifier.php', + 'SpomkyLabs\\Pki\\CryptoTypes\\AlgorithmIdentifier\\Signature\\RFC4055RSASignatureAlgorithmIdentifier' => __DIR__ . '/..' . '/spomky-labs/pki-framework/src/CryptoTypes/AlgorithmIdentifier/Signature/RFC4055RSASignatureAlgorithmIdentifier.php', + 'SpomkyLabs\\Pki\\CryptoTypes\\AlgorithmIdentifier\\Signature\\RSASignatureAlgorithmIdentifier' => __DIR__ . '/..' . '/spomky-labs/pki-framework/src/CryptoTypes/AlgorithmIdentifier/Signature/RSASignatureAlgorithmIdentifier.php', + 'SpomkyLabs\\Pki\\CryptoTypes\\AlgorithmIdentifier\\Signature\\SHA1WithRSAEncryptionAlgorithmIdentifier' => __DIR__ . '/..' . '/spomky-labs/pki-framework/src/CryptoTypes/AlgorithmIdentifier/Signature/SHA1WithRSAEncryptionAlgorithmIdentifier.php', + 'SpomkyLabs\\Pki\\CryptoTypes\\AlgorithmIdentifier\\Signature\\SHA224WithRSAEncryptionAlgorithmIdentifier' => __DIR__ . '/..' . '/spomky-labs/pki-framework/src/CryptoTypes/AlgorithmIdentifier/Signature/SHA224WithRSAEncryptionAlgorithmIdentifier.php', + 'SpomkyLabs\\Pki\\CryptoTypes\\AlgorithmIdentifier\\Signature\\SHA256WithRSAEncryptionAlgorithmIdentifier' => __DIR__ . '/..' . '/spomky-labs/pki-framework/src/CryptoTypes/AlgorithmIdentifier/Signature/SHA256WithRSAEncryptionAlgorithmIdentifier.php', + 'SpomkyLabs\\Pki\\CryptoTypes\\AlgorithmIdentifier\\Signature\\SHA384WithRSAEncryptionAlgorithmIdentifier' => __DIR__ . '/..' . '/spomky-labs/pki-framework/src/CryptoTypes/AlgorithmIdentifier/Signature/SHA384WithRSAEncryptionAlgorithmIdentifier.php', + 'SpomkyLabs\\Pki\\CryptoTypes\\AlgorithmIdentifier\\Signature\\SHA512WithRSAEncryptionAlgorithmIdentifier' => __DIR__ . '/..' . '/spomky-labs/pki-framework/src/CryptoTypes/AlgorithmIdentifier/Signature/SHA512WithRSAEncryptionAlgorithmIdentifier.php', + 'SpomkyLabs\\Pki\\CryptoTypes\\AlgorithmIdentifier\\SpecificAlgorithmIdentifier' => __DIR__ . '/..' . '/spomky-labs/pki-framework/src/CryptoTypes/AlgorithmIdentifier/SpecificAlgorithmIdentifier.php', + 'SpomkyLabs\\Pki\\CryptoTypes\\Asymmetric\\Attribute\\OneAsymmetricKeyAttributes' => __DIR__ . '/..' . '/spomky-labs/pki-framework/src/CryptoTypes/Asymmetric/Attribute/OneAsymmetricKeyAttributes.php', + 'SpomkyLabs\\Pki\\CryptoTypes\\Asymmetric\\EC\\ECConversion' => __DIR__ . '/..' . '/spomky-labs/pki-framework/src/CryptoTypes/Asymmetric/EC/ECConversion.php', + 'SpomkyLabs\\Pki\\CryptoTypes\\Asymmetric\\EC\\ECPrivateKey' => __DIR__ . '/..' . '/spomky-labs/pki-framework/src/CryptoTypes/Asymmetric/EC/ECPrivateKey.php', + 'SpomkyLabs\\Pki\\CryptoTypes\\Asymmetric\\EC\\ECPublicKey' => __DIR__ . '/..' . '/spomky-labs/pki-framework/src/CryptoTypes/Asymmetric/EC/ECPublicKey.php', + 'SpomkyLabs\\Pki\\CryptoTypes\\Asymmetric\\OneAsymmetricKey' => __DIR__ . '/..' . '/spomky-labs/pki-framework/src/CryptoTypes/Asymmetric/OneAsymmetricKey.php', + 'SpomkyLabs\\Pki\\CryptoTypes\\Asymmetric\\PrivateKey' => __DIR__ . '/..' . '/spomky-labs/pki-framework/src/CryptoTypes/Asymmetric/PrivateKey.php', + 'SpomkyLabs\\Pki\\CryptoTypes\\Asymmetric\\PrivateKeyInfo' => __DIR__ . '/..' . '/spomky-labs/pki-framework/src/CryptoTypes/Asymmetric/PrivateKeyInfo.php', + 'SpomkyLabs\\Pki\\CryptoTypes\\Asymmetric\\PublicKey' => __DIR__ . '/..' . '/spomky-labs/pki-framework/src/CryptoTypes/Asymmetric/PublicKey.php', + 'SpomkyLabs\\Pki\\CryptoTypes\\Asymmetric\\PublicKeyInfo' => __DIR__ . '/..' . '/spomky-labs/pki-framework/src/CryptoTypes/Asymmetric/PublicKeyInfo.php', + 'SpomkyLabs\\Pki\\CryptoTypes\\Asymmetric\\RFC8410\\Curve25519\\Curve25519PrivateKey' => __DIR__ . '/..' . '/spomky-labs/pki-framework/src/CryptoTypes/Asymmetric/RFC8410/Curve25519/Curve25519PrivateKey.php', + 'SpomkyLabs\\Pki\\CryptoTypes\\Asymmetric\\RFC8410\\Curve25519\\Curve25519PublicKey' => __DIR__ . '/..' . '/spomky-labs/pki-framework/src/CryptoTypes/Asymmetric/RFC8410/Curve25519/Curve25519PublicKey.php', + 'SpomkyLabs\\Pki\\CryptoTypes\\Asymmetric\\RFC8410\\Curve25519\\Ed25519PrivateKey' => __DIR__ . '/..' . '/spomky-labs/pki-framework/src/CryptoTypes/Asymmetric/RFC8410/Curve25519/Ed25519PrivateKey.php', + 'SpomkyLabs\\Pki\\CryptoTypes\\Asymmetric\\RFC8410\\Curve25519\\Ed25519PublicKey' => __DIR__ . '/..' . '/spomky-labs/pki-framework/src/CryptoTypes/Asymmetric/RFC8410/Curve25519/Ed25519PublicKey.php', + 'SpomkyLabs\\Pki\\CryptoTypes\\Asymmetric\\RFC8410\\Curve25519\\X25519PrivateKey' => __DIR__ . '/..' . '/spomky-labs/pki-framework/src/CryptoTypes/Asymmetric/RFC8410/Curve25519/X25519PrivateKey.php', + 'SpomkyLabs\\Pki\\CryptoTypes\\Asymmetric\\RFC8410\\Curve25519\\X25519PublicKey' => __DIR__ . '/..' . '/spomky-labs/pki-framework/src/CryptoTypes/Asymmetric/RFC8410/Curve25519/X25519PublicKey.php', + 'SpomkyLabs\\Pki\\CryptoTypes\\Asymmetric\\RFC8410\\Curve448\\Ed448PrivateKey' => __DIR__ . '/..' . '/spomky-labs/pki-framework/src/CryptoTypes/Asymmetric/RFC8410/Curve448/Ed448PrivateKey.php', + 'SpomkyLabs\\Pki\\CryptoTypes\\Asymmetric\\RFC8410\\Curve448\\Ed448PublicKey' => __DIR__ . '/..' . '/spomky-labs/pki-framework/src/CryptoTypes/Asymmetric/RFC8410/Curve448/Ed448PublicKey.php', + 'SpomkyLabs\\Pki\\CryptoTypes\\Asymmetric\\RFC8410\\Curve448\\X448PrivateKey' => __DIR__ . '/..' . '/spomky-labs/pki-framework/src/CryptoTypes/Asymmetric/RFC8410/Curve448/X448PrivateKey.php', + 'SpomkyLabs\\Pki\\CryptoTypes\\Asymmetric\\RFC8410\\Curve448\\X448PublicKey' => __DIR__ . '/..' . '/spomky-labs/pki-framework/src/CryptoTypes/Asymmetric/RFC8410/Curve448/X448PublicKey.php', + 'SpomkyLabs\\Pki\\CryptoTypes\\Asymmetric\\RFC8410\\RFC8410PrivateKey' => __DIR__ . '/..' . '/spomky-labs/pki-framework/src/CryptoTypes/Asymmetric/RFC8410/RFC8410PrivateKey.php', + 'SpomkyLabs\\Pki\\CryptoTypes\\Asymmetric\\RFC8410\\RFC8410PublicKey' => __DIR__ . '/..' . '/spomky-labs/pki-framework/src/CryptoTypes/Asymmetric/RFC8410/RFC8410PublicKey.php', + 'SpomkyLabs\\Pki\\CryptoTypes\\Asymmetric\\RSA\\RSAPrivateKey' => __DIR__ . '/..' . '/spomky-labs/pki-framework/src/CryptoTypes/Asymmetric/RSA/RSAPrivateKey.php', + 'SpomkyLabs\\Pki\\CryptoTypes\\Asymmetric\\RSA\\RSAPublicKey' => __DIR__ . '/..' . '/spomky-labs/pki-framework/src/CryptoTypes/Asymmetric/RSA/RSAPublicKey.php', + 'SpomkyLabs\\Pki\\CryptoTypes\\Asymmetric\\RSA\\RSASSAPSSPrivateKey' => __DIR__ . '/..' . '/spomky-labs/pki-framework/src/CryptoTypes/Asymmetric/RSA/RSASSAPSSPrivateKey.php', + 'SpomkyLabs\\Pki\\CryptoTypes\\Signature\\ECSignature' => __DIR__ . '/..' . '/spomky-labs/pki-framework/src/CryptoTypes/Signature/ECSignature.php', + 'SpomkyLabs\\Pki\\CryptoTypes\\Signature\\Ed25519Signature' => __DIR__ . '/..' . '/spomky-labs/pki-framework/src/CryptoTypes/Signature/Ed25519Signature.php', + 'SpomkyLabs\\Pki\\CryptoTypes\\Signature\\Ed448Signature' => __DIR__ . '/..' . '/spomky-labs/pki-framework/src/CryptoTypes/Signature/Ed448Signature.php', + 'SpomkyLabs\\Pki\\CryptoTypes\\Signature\\GenericSignature' => __DIR__ . '/..' . '/spomky-labs/pki-framework/src/CryptoTypes/Signature/GenericSignature.php', + 'SpomkyLabs\\Pki\\CryptoTypes\\Signature\\RSASignature' => __DIR__ . '/..' . '/spomky-labs/pki-framework/src/CryptoTypes/Signature/RSASignature.php', + 'SpomkyLabs\\Pki\\CryptoTypes\\Signature\\Signature' => __DIR__ . '/..' . '/spomky-labs/pki-framework/src/CryptoTypes/Signature/Signature.php', + 'SpomkyLabs\\Pki\\X501\\ASN1\\Attribute' => __DIR__ . '/..' . '/spomky-labs/pki-framework/src/X501/ASN1/Attribute.php', + 'SpomkyLabs\\Pki\\X501\\ASN1\\AttributeType' => __DIR__ . '/..' . '/spomky-labs/pki-framework/src/X501/ASN1/AttributeType.php', + 'SpomkyLabs\\Pki\\X501\\ASN1\\AttributeTypeAndValue' => __DIR__ . '/..' . '/spomky-labs/pki-framework/src/X501/ASN1/AttributeTypeAndValue.php', + 'SpomkyLabs\\Pki\\X501\\ASN1\\AttributeValue\\AttributeValue' => __DIR__ . '/..' . '/spomky-labs/pki-framework/src/X501/ASN1/AttributeValue/AttributeValue.php', + 'SpomkyLabs\\Pki\\X501\\ASN1\\AttributeValue\\CommonNameValue' => __DIR__ . '/..' . '/spomky-labs/pki-framework/src/X501/ASN1/AttributeValue/CommonNameValue.php', + 'SpomkyLabs\\Pki\\X501\\ASN1\\AttributeValue\\CountryNameValue' => __DIR__ . '/..' . '/spomky-labs/pki-framework/src/X501/ASN1/AttributeValue/CountryNameValue.php', + 'SpomkyLabs\\Pki\\X501\\ASN1\\AttributeValue\\DescriptionValue' => __DIR__ . '/..' . '/spomky-labs/pki-framework/src/X501/ASN1/AttributeValue/DescriptionValue.php', + 'SpomkyLabs\\Pki\\X501\\ASN1\\AttributeValue\\Feature\\DirectoryString' => __DIR__ . '/..' . '/spomky-labs/pki-framework/src/X501/ASN1/AttributeValue/Feature/DirectoryString.php', + 'SpomkyLabs\\Pki\\X501\\ASN1\\AttributeValue\\Feature\\PrintableStringValue' => __DIR__ . '/..' . '/spomky-labs/pki-framework/src/X501/ASN1/AttributeValue/Feature/PrintableStringValue.php', + 'SpomkyLabs\\Pki\\X501\\ASN1\\AttributeValue\\GivenNameValue' => __DIR__ . '/..' . '/spomky-labs/pki-framework/src/X501/ASN1/AttributeValue/GivenNameValue.php', + 'SpomkyLabs\\Pki\\X501\\ASN1\\AttributeValue\\LocalityNameValue' => __DIR__ . '/..' . '/spomky-labs/pki-framework/src/X501/ASN1/AttributeValue/LocalityNameValue.php', + 'SpomkyLabs\\Pki\\X501\\ASN1\\AttributeValue\\NameValue' => __DIR__ . '/..' . '/spomky-labs/pki-framework/src/X501/ASN1/AttributeValue/NameValue.php', + 'SpomkyLabs\\Pki\\X501\\ASN1\\AttributeValue\\OrganizationNameValue' => __DIR__ . '/..' . '/spomky-labs/pki-framework/src/X501/ASN1/AttributeValue/OrganizationNameValue.php', + 'SpomkyLabs\\Pki\\X501\\ASN1\\AttributeValue\\OrganizationalUnitNameValue' => __DIR__ . '/..' . '/spomky-labs/pki-framework/src/X501/ASN1/AttributeValue/OrganizationalUnitNameValue.php', + 'SpomkyLabs\\Pki\\X501\\ASN1\\AttributeValue\\PseudonymValue' => __DIR__ . '/..' . '/spomky-labs/pki-framework/src/X501/ASN1/AttributeValue/PseudonymValue.php', + 'SpomkyLabs\\Pki\\X501\\ASN1\\AttributeValue\\SerialNumberValue' => __DIR__ . '/..' . '/spomky-labs/pki-framework/src/X501/ASN1/AttributeValue/SerialNumberValue.php', + 'SpomkyLabs\\Pki\\X501\\ASN1\\AttributeValue\\StateOrProvinceNameValue' => __DIR__ . '/..' . '/spomky-labs/pki-framework/src/X501/ASN1/AttributeValue/StateOrProvinceNameValue.php', + 'SpomkyLabs\\Pki\\X501\\ASN1\\AttributeValue\\SurnameValue' => __DIR__ . '/..' . '/spomky-labs/pki-framework/src/X501/ASN1/AttributeValue/SurnameValue.php', + 'SpomkyLabs\\Pki\\X501\\ASN1\\AttributeValue\\TitleValue' => __DIR__ . '/..' . '/spomky-labs/pki-framework/src/X501/ASN1/AttributeValue/TitleValue.php', + 'SpomkyLabs\\Pki\\X501\\ASN1\\AttributeValue\\UnknownAttributeValue' => __DIR__ . '/..' . '/spomky-labs/pki-framework/src/X501/ASN1/AttributeValue/UnknownAttributeValue.php', + 'SpomkyLabs\\Pki\\X501\\ASN1\\Collection\\AttributeCollection' => __DIR__ . '/..' . '/spomky-labs/pki-framework/src/X501/ASN1/Collection/AttributeCollection.php', + 'SpomkyLabs\\Pki\\X501\\ASN1\\Collection\\SequenceOfAttributes' => __DIR__ . '/..' . '/spomky-labs/pki-framework/src/X501/ASN1/Collection/SequenceOfAttributes.php', + 'SpomkyLabs\\Pki\\X501\\ASN1\\Collection\\SetOfAttributes' => __DIR__ . '/..' . '/spomky-labs/pki-framework/src/X501/ASN1/Collection/SetOfAttributes.php', + 'SpomkyLabs\\Pki\\X501\\ASN1\\Name' => __DIR__ . '/..' . '/spomky-labs/pki-framework/src/X501/ASN1/Name.php', + 'SpomkyLabs\\Pki\\X501\\ASN1\\RDN' => __DIR__ . '/..' . '/spomky-labs/pki-framework/src/X501/ASN1/RDN.php', + 'SpomkyLabs\\Pki\\X501\\DN\\DNParser' => __DIR__ . '/..' . '/spomky-labs/pki-framework/src/X501/DN/DNParser.php', + 'SpomkyLabs\\Pki\\X501\\MatchingRule\\BinaryMatch' => __DIR__ . '/..' . '/spomky-labs/pki-framework/src/X501/MatchingRule/BinaryMatch.php', + 'SpomkyLabs\\Pki\\X501\\MatchingRule\\CaseExactMatch' => __DIR__ . '/..' . '/spomky-labs/pki-framework/src/X501/MatchingRule/CaseExactMatch.php', + 'SpomkyLabs\\Pki\\X501\\MatchingRule\\CaseIgnoreMatch' => __DIR__ . '/..' . '/spomky-labs/pki-framework/src/X501/MatchingRule/CaseIgnoreMatch.php', + 'SpomkyLabs\\Pki\\X501\\MatchingRule\\MatchingRule' => __DIR__ . '/..' . '/spomky-labs/pki-framework/src/X501/MatchingRule/MatchingRule.php', + 'SpomkyLabs\\Pki\\X501\\MatchingRule\\StringPrepMatchingRule' => __DIR__ . '/..' . '/spomky-labs/pki-framework/src/X501/MatchingRule/StringPrepMatchingRule.php', + 'SpomkyLabs\\Pki\\X501\\StringPrep\\CheckBidiStep' => __DIR__ . '/..' . '/spomky-labs/pki-framework/src/X501/StringPrep/CheckBidiStep.php', + 'SpomkyLabs\\Pki\\X501\\StringPrep\\InsignificantNonSubstringSpaceStep' => __DIR__ . '/..' . '/spomky-labs/pki-framework/src/X501/StringPrep/InsignificantNonSubstringSpaceStep.php', + 'SpomkyLabs\\Pki\\X501\\StringPrep\\MapStep' => __DIR__ . '/..' . '/spomky-labs/pki-framework/src/X501/StringPrep/MapStep.php', + 'SpomkyLabs\\Pki\\X501\\StringPrep\\NormalizeStep' => __DIR__ . '/..' . '/spomky-labs/pki-framework/src/X501/StringPrep/NormalizeStep.php', + 'SpomkyLabs\\Pki\\X501\\StringPrep\\PrepareStep' => __DIR__ . '/..' . '/spomky-labs/pki-framework/src/X501/StringPrep/PrepareStep.php', + 'SpomkyLabs\\Pki\\X501\\StringPrep\\ProhibitStep' => __DIR__ . '/..' . '/spomky-labs/pki-framework/src/X501/StringPrep/ProhibitStep.php', + 'SpomkyLabs\\Pki\\X501\\StringPrep\\StringPreparer' => __DIR__ . '/..' . '/spomky-labs/pki-framework/src/X501/StringPrep/StringPreparer.php', + 'SpomkyLabs\\Pki\\X501\\StringPrep\\TranscodeStep' => __DIR__ . '/..' . '/spomky-labs/pki-framework/src/X501/StringPrep/TranscodeStep.php', + 'SpomkyLabs\\Pki\\X509\\AttributeCertificate\\AttCertIssuer' => __DIR__ . '/..' . '/spomky-labs/pki-framework/src/X509/AttributeCertificate/AttCertIssuer.php', + 'SpomkyLabs\\Pki\\X509\\AttributeCertificate\\AttCertValidityPeriod' => __DIR__ . '/..' . '/spomky-labs/pki-framework/src/X509/AttributeCertificate/AttCertValidityPeriod.php', + 'SpomkyLabs\\Pki\\X509\\AttributeCertificate\\AttributeCertificate' => __DIR__ . '/..' . '/spomky-labs/pki-framework/src/X509/AttributeCertificate/AttributeCertificate.php', + 'SpomkyLabs\\Pki\\X509\\AttributeCertificate\\AttributeCertificateInfo' => __DIR__ . '/..' . '/spomky-labs/pki-framework/src/X509/AttributeCertificate/AttributeCertificateInfo.php', + 'SpomkyLabs\\Pki\\X509\\AttributeCertificate\\Attribute\\AccessIdentityAttributeValue' => __DIR__ . '/..' . '/spomky-labs/pki-framework/src/X509/AttributeCertificate/Attribute/AccessIdentityAttributeValue.php', + 'SpomkyLabs\\Pki\\X509\\AttributeCertificate\\Attribute\\AuthenticationInfoAttributeValue' => __DIR__ . '/..' . '/spomky-labs/pki-framework/src/X509/AttributeCertificate/Attribute/AuthenticationInfoAttributeValue.php', + 'SpomkyLabs\\Pki\\X509\\AttributeCertificate\\Attribute\\ChargingIdentityAttributeValue' => __DIR__ . '/..' . '/spomky-labs/pki-framework/src/X509/AttributeCertificate/Attribute/ChargingIdentityAttributeValue.php', + 'SpomkyLabs\\Pki\\X509\\AttributeCertificate\\Attribute\\GroupAttributeValue' => __DIR__ . '/..' . '/spomky-labs/pki-framework/src/X509/AttributeCertificate/Attribute/GroupAttributeValue.php', + 'SpomkyLabs\\Pki\\X509\\AttributeCertificate\\Attribute\\IetfAttrSyntax' => __DIR__ . '/..' . '/spomky-labs/pki-framework/src/X509/AttributeCertificate/Attribute/IetfAttrSyntax.php', + 'SpomkyLabs\\Pki\\X509\\AttributeCertificate\\Attribute\\IetfAttrValue' => __DIR__ . '/..' . '/spomky-labs/pki-framework/src/X509/AttributeCertificate/Attribute/IetfAttrValue.php', + 'SpomkyLabs\\Pki\\X509\\AttributeCertificate\\Attribute\\RoleAttributeValue' => __DIR__ . '/..' . '/spomky-labs/pki-framework/src/X509/AttributeCertificate/Attribute/RoleAttributeValue.php', + 'SpomkyLabs\\Pki\\X509\\AttributeCertificate\\Attribute\\SvceAuthInfo' => __DIR__ . '/..' . '/spomky-labs/pki-framework/src/X509/AttributeCertificate/Attribute/SvceAuthInfo.php', + 'SpomkyLabs\\Pki\\X509\\AttributeCertificate\\Attributes' => __DIR__ . '/..' . '/spomky-labs/pki-framework/src/X509/AttributeCertificate/Attributes.php', + 'SpomkyLabs\\Pki\\X509\\AttributeCertificate\\Holder' => __DIR__ . '/..' . '/spomky-labs/pki-framework/src/X509/AttributeCertificate/Holder.php', + 'SpomkyLabs\\Pki\\X509\\AttributeCertificate\\IssuerSerial' => __DIR__ . '/..' . '/spomky-labs/pki-framework/src/X509/AttributeCertificate/IssuerSerial.php', + 'SpomkyLabs\\Pki\\X509\\AttributeCertificate\\ObjectDigestInfo' => __DIR__ . '/..' . '/spomky-labs/pki-framework/src/X509/AttributeCertificate/ObjectDigestInfo.php', + 'SpomkyLabs\\Pki\\X509\\AttributeCertificate\\V2Form' => __DIR__ . '/..' . '/spomky-labs/pki-framework/src/X509/AttributeCertificate/V2Form.php', + 'SpomkyLabs\\Pki\\X509\\AttributeCertificate\\Validation\\ACValidationConfig' => __DIR__ . '/..' . '/spomky-labs/pki-framework/src/X509/AttributeCertificate/Validation/ACValidationConfig.php', + 'SpomkyLabs\\Pki\\X509\\AttributeCertificate\\Validation\\ACValidator' => __DIR__ . '/..' . '/spomky-labs/pki-framework/src/X509/AttributeCertificate/Validation/ACValidator.php', + 'SpomkyLabs\\Pki\\X509\\AttributeCertificate\\Validation\\Exception\\ACValidationException' => __DIR__ . '/..' . '/spomky-labs/pki-framework/src/X509/AttributeCertificate/Validation/Exception/ACValidationException.php', + 'SpomkyLabs\\Pki\\X509\\Certificate\\Certificate' => __DIR__ . '/..' . '/spomky-labs/pki-framework/src/X509/Certificate/Certificate.php', + 'SpomkyLabs\\Pki\\X509\\Certificate\\CertificateBundle' => __DIR__ . '/..' . '/spomky-labs/pki-framework/src/X509/Certificate/CertificateBundle.php', + 'SpomkyLabs\\Pki\\X509\\Certificate\\CertificateChain' => __DIR__ . '/..' . '/spomky-labs/pki-framework/src/X509/Certificate/CertificateChain.php', + 'SpomkyLabs\\Pki\\X509\\Certificate\\Extension\\AAControlsExtension' => __DIR__ . '/..' . '/spomky-labs/pki-framework/src/X509/Certificate/Extension/AAControlsExtension.php', + 'SpomkyLabs\\Pki\\X509\\Certificate\\Extension\\AccessDescription\\AccessDescription' => __DIR__ . '/..' . '/spomky-labs/pki-framework/src/X509/Certificate/Extension/AccessDescription/AccessDescription.php', + 'SpomkyLabs\\Pki\\X509\\Certificate\\Extension\\AccessDescription\\AuthorityAccessDescription' => __DIR__ . '/..' . '/spomky-labs/pki-framework/src/X509/Certificate/Extension/AccessDescription/AuthorityAccessDescription.php', + 'SpomkyLabs\\Pki\\X509\\Certificate\\Extension\\AccessDescription\\SubjectAccessDescription' => __DIR__ . '/..' . '/spomky-labs/pki-framework/src/X509/Certificate/Extension/AccessDescription/SubjectAccessDescription.php', + 'SpomkyLabs\\Pki\\X509\\Certificate\\Extension\\AuthorityInformationAccessExtension' => __DIR__ . '/..' . '/spomky-labs/pki-framework/src/X509/Certificate/Extension/AuthorityInformationAccessExtension.php', + 'SpomkyLabs\\Pki\\X509\\Certificate\\Extension\\AuthorityKeyIdentifierExtension' => __DIR__ . '/..' . '/spomky-labs/pki-framework/src/X509/Certificate/Extension/AuthorityKeyIdentifierExtension.php', + 'SpomkyLabs\\Pki\\X509\\Certificate\\Extension\\BasicConstraintsExtension' => __DIR__ . '/..' . '/spomky-labs/pki-framework/src/X509/Certificate/Extension/BasicConstraintsExtension.php', + 'SpomkyLabs\\Pki\\X509\\Certificate\\Extension\\CRLDistributionPointsExtension' => __DIR__ . '/..' . '/spomky-labs/pki-framework/src/X509/Certificate/Extension/CRLDistributionPointsExtension.php', + 'SpomkyLabs\\Pki\\X509\\Certificate\\Extension\\CertificatePoliciesExtension' => __DIR__ . '/..' . '/spomky-labs/pki-framework/src/X509/Certificate/Extension/CertificatePoliciesExtension.php', + 'SpomkyLabs\\Pki\\X509\\Certificate\\Extension\\CertificatePolicy\\CPSQualifier' => __DIR__ . '/..' . '/spomky-labs/pki-framework/src/X509/Certificate/Extension/CertificatePolicy/CPSQualifier.php', + 'SpomkyLabs\\Pki\\X509\\Certificate\\Extension\\CertificatePolicy\\DisplayText' => __DIR__ . '/..' . '/spomky-labs/pki-framework/src/X509/Certificate/Extension/CertificatePolicy/DisplayText.php', + 'SpomkyLabs\\Pki\\X509\\Certificate\\Extension\\CertificatePolicy\\NoticeReference' => __DIR__ . '/..' . '/spomky-labs/pki-framework/src/X509/Certificate/Extension/CertificatePolicy/NoticeReference.php', + 'SpomkyLabs\\Pki\\X509\\Certificate\\Extension\\CertificatePolicy\\PolicyInformation' => __DIR__ . '/..' . '/spomky-labs/pki-framework/src/X509/Certificate/Extension/CertificatePolicy/PolicyInformation.php', + 'SpomkyLabs\\Pki\\X509\\Certificate\\Extension\\CertificatePolicy\\PolicyQualifierInfo' => __DIR__ . '/..' . '/spomky-labs/pki-framework/src/X509/Certificate/Extension/CertificatePolicy/PolicyQualifierInfo.php', + 'SpomkyLabs\\Pki\\X509\\Certificate\\Extension\\CertificatePolicy\\UserNoticeQualifier' => __DIR__ . '/..' . '/spomky-labs/pki-framework/src/X509/Certificate/Extension/CertificatePolicy/UserNoticeQualifier.php', + 'SpomkyLabs\\Pki\\X509\\Certificate\\Extension\\DistributionPoint\\DistributionPoint' => __DIR__ . '/..' . '/spomky-labs/pki-framework/src/X509/Certificate/Extension/DistributionPoint/DistributionPoint.php', + 'SpomkyLabs\\Pki\\X509\\Certificate\\Extension\\DistributionPoint\\DistributionPointName' => __DIR__ . '/..' . '/spomky-labs/pki-framework/src/X509/Certificate/Extension/DistributionPoint/DistributionPointName.php', + 'SpomkyLabs\\Pki\\X509\\Certificate\\Extension\\DistributionPoint\\FullName' => __DIR__ . '/..' . '/spomky-labs/pki-framework/src/X509/Certificate/Extension/DistributionPoint/FullName.php', + 'SpomkyLabs\\Pki\\X509\\Certificate\\Extension\\DistributionPoint\\ReasonFlags' => __DIR__ . '/..' . '/spomky-labs/pki-framework/src/X509/Certificate/Extension/DistributionPoint/ReasonFlags.php', + 'SpomkyLabs\\Pki\\X509\\Certificate\\Extension\\DistributionPoint\\RelativeName' => __DIR__ . '/..' . '/spomky-labs/pki-framework/src/X509/Certificate/Extension/DistributionPoint/RelativeName.php', + 'SpomkyLabs\\Pki\\X509\\Certificate\\Extension\\ExtendedKeyUsageExtension' => __DIR__ . '/..' . '/spomky-labs/pki-framework/src/X509/Certificate/Extension/ExtendedKeyUsageExtension.php', + 'SpomkyLabs\\Pki\\X509\\Certificate\\Extension\\Extension' => __DIR__ . '/..' . '/spomky-labs/pki-framework/src/X509/Certificate/Extension/Extension.php', + 'SpomkyLabs\\Pki\\X509\\Certificate\\Extension\\FreshestCRLExtension' => __DIR__ . '/..' . '/spomky-labs/pki-framework/src/X509/Certificate/Extension/FreshestCRLExtension.php', + 'SpomkyLabs\\Pki\\X509\\Certificate\\Extension\\InhibitAnyPolicyExtension' => __DIR__ . '/..' . '/spomky-labs/pki-framework/src/X509/Certificate/Extension/InhibitAnyPolicyExtension.php', + 'SpomkyLabs\\Pki\\X509\\Certificate\\Extension\\IssuerAlternativeNameExtension' => __DIR__ . '/..' . '/spomky-labs/pki-framework/src/X509/Certificate/Extension/IssuerAlternativeNameExtension.php', + 'SpomkyLabs\\Pki\\X509\\Certificate\\Extension\\KeyUsageExtension' => __DIR__ . '/..' . '/spomky-labs/pki-framework/src/X509/Certificate/Extension/KeyUsageExtension.php', + 'SpomkyLabs\\Pki\\X509\\Certificate\\Extension\\NameConstraintsExtension' => __DIR__ . '/..' . '/spomky-labs/pki-framework/src/X509/Certificate/Extension/NameConstraintsExtension.php', + 'SpomkyLabs\\Pki\\X509\\Certificate\\Extension\\NameConstraints\\GeneralSubtree' => __DIR__ . '/..' . '/spomky-labs/pki-framework/src/X509/Certificate/Extension/NameConstraints/GeneralSubtree.php', + 'SpomkyLabs\\Pki\\X509\\Certificate\\Extension\\NameConstraints\\GeneralSubtrees' => __DIR__ . '/..' . '/spomky-labs/pki-framework/src/X509/Certificate/Extension/NameConstraints/GeneralSubtrees.php', + 'SpomkyLabs\\Pki\\X509\\Certificate\\Extension\\NoRevocationAvailableExtension' => __DIR__ . '/..' . '/spomky-labs/pki-framework/src/X509/Certificate/Extension/NoRevocationAvailableExtension.php', + 'SpomkyLabs\\Pki\\X509\\Certificate\\Extension\\PolicyConstraintsExtension' => __DIR__ . '/..' . '/spomky-labs/pki-framework/src/X509/Certificate/Extension/PolicyConstraintsExtension.php', + 'SpomkyLabs\\Pki\\X509\\Certificate\\Extension\\PolicyMappingsExtension' => __DIR__ . '/..' . '/spomky-labs/pki-framework/src/X509/Certificate/Extension/PolicyMappingsExtension.php', + 'SpomkyLabs\\Pki\\X509\\Certificate\\Extension\\PolicyMappings\\PolicyMapping' => __DIR__ . '/..' . '/spomky-labs/pki-framework/src/X509/Certificate/Extension/PolicyMappings/PolicyMapping.php', + 'SpomkyLabs\\Pki\\X509\\Certificate\\Extension\\SubjectAlternativeNameExtension' => __DIR__ . '/..' . '/spomky-labs/pki-framework/src/X509/Certificate/Extension/SubjectAlternativeNameExtension.php', + 'SpomkyLabs\\Pki\\X509\\Certificate\\Extension\\SubjectDirectoryAttributesExtension' => __DIR__ . '/..' . '/spomky-labs/pki-framework/src/X509/Certificate/Extension/SubjectDirectoryAttributesExtension.php', + 'SpomkyLabs\\Pki\\X509\\Certificate\\Extension\\SubjectInformationAccessExtension' => __DIR__ . '/..' . '/spomky-labs/pki-framework/src/X509/Certificate/Extension/SubjectInformationAccessExtension.php', + 'SpomkyLabs\\Pki\\X509\\Certificate\\Extension\\SubjectKeyIdentifierExtension' => __DIR__ . '/..' . '/spomky-labs/pki-framework/src/X509/Certificate/Extension/SubjectKeyIdentifierExtension.php', + 'SpomkyLabs\\Pki\\X509\\Certificate\\Extension\\TargetInformationExtension' => __DIR__ . '/..' . '/spomky-labs/pki-framework/src/X509/Certificate/Extension/TargetInformationExtension.php', + 'SpomkyLabs\\Pki\\X509\\Certificate\\Extension\\Target\\Target' => __DIR__ . '/..' . '/spomky-labs/pki-framework/src/X509/Certificate/Extension/Target/Target.php', + 'SpomkyLabs\\Pki\\X509\\Certificate\\Extension\\Target\\TargetGroup' => __DIR__ . '/..' . '/spomky-labs/pki-framework/src/X509/Certificate/Extension/Target/TargetGroup.php', + 'SpomkyLabs\\Pki\\X509\\Certificate\\Extension\\Target\\TargetName' => __DIR__ . '/..' . '/spomky-labs/pki-framework/src/X509/Certificate/Extension/Target/TargetName.php', + 'SpomkyLabs\\Pki\\X509\\Certificate\\Extension\\Target\\Targets' => __DIR__ . '/..' . '/spomky-labs/pki-framework/src/X509/Certificate/Extension/Target/Targets.php', + 'SpomkyLabs\\Pki\\X509\\Certificate\\Extension\\UnknownExtension' => __DIR__ . '/..' . '/spomky-labs/pki-framework/src/X509/Certificate/Extension/UnknownExtension.php', + 'SpomkyLabs\\Pki\\X509\\Certificate\\Extensions' => __DIR__ . '/..' . '/spomky-labs/pki-framework/src/X509/Certificate/Extensions.php', + 'SpomkyLabs\\Pki\\X509\\Certificate\\TBSCertificate' => __DIR__ . '/..' . '/spomky-labs/pki-framework/src/X509/Certificate/TBSCertificate.php', + 'SpomkyLabs\\Pki\\X509\\Certificate\\Time' => __DIR__ . '/..' . '/spomky-labs/pki-framework/src/X509/Certificate/Time.php', + 'SpomkyLabs\\Pki\\X509\\Certificate\\UniqueIdentifier' => __DIR__ . '/..' . '/spomky-labs/pki-framework/src/X509/Certificate/UniqueIdentifier.php', + 'SpomkyLabs\\Pki\\X509\\Certificate\\Validity' => __DIR__ . '/..' . '/spomky-labs/pki-framework/src/X509/Certificate/Validity.php', + 'SpomkyLabs\\Pki\\X509\\CertificationPath\\CertificationPath' => __DIR__ . '/..' . '/spomky-labs/pki-framework/src/X509/CertificationPath/CertificationPath.php', + 'SpomkyLabs\\Pki\\X509\\CertificationPath\\Exception\\PathBuildingException' => __DIR__ . '/..' . '/spomky-labs/pki-framework/src/X509/CertificationPath/Exception/PathBuildingException.php', + 'SpomkyLabs\\Pki\\X509\\CertificationPath\\Exception\\PathValidationException' => __DIR__ . '/..' . '/spomky-labs/pki-framework/src/X509/CertificationPath/Exception/PathValidationException.php', + 'SpomkyLabs\\Pki\\X509\\CertificationPath\\PathBuilding\\CertificationPathBuilder' => __DIR__ . '/..' . '/spomky-labs/pki-framework/src/X509/CertificationPath/PathBuilding/CertificationPathBuilder.php', + 'SpomkyLabs\\Pki\\X509\\CertificationPath\\PathValidation\\PathValidationConfig' => __DIR__ . '/..' . '/spomky-labs/pki-framework/src/X509/CertificationPath/PathValidation/PathValidationConfig.php', + 'SpomkyLabs\\Pki\\X509\\CertificationPath\\PathValidation\\PathValidationResult' => __DIR__ . '/..' . '/spomky-labs/pki-framework/src/X509/CertificationPath/PathValidation/PathValidationResult.php', + 'SpomkyLabs\\Pki\\X509\\CertificationPath\\PathValidation\\PathValidator' => __DIR__ . '/..' . '/spomky-labs/pki-framework/src/X509/CertificationPath/PathValidation/PathValidator.php', + 'SpomkyLabs\\Pki\\X509\\CertificationPath\\PathValidation\\ValidatorState' => __DIR__ . '/..' . '/spomky-labs/pki-framework/src/X509/CertificationPath/PathValidation/ValidatorState.php', + 'SpomkyLabs\\Pki\\X509\\CertificationPath\\Policy\\PolicyNode' => __DIR__ . '/..' . '/spomky-labs/pki-framework/src/X509/CertificationPath/Policy/PolicyNode.php', + 'SpomkyLabs\\Pki\\X509\\CertificationPath\\Policy\\PolicyTree' => __DIR__ . '/..' . '/spomky-labs/pki-framework/src/X509/CertificationPath/Policy/PolicyTree.php', + 'SpomkyLabs\\Pki\\X509\\CertificationRequest\\Attribute\\ExtensionRequestValue' => __DIR__ . '/..' . '/spomky-labs/pki-framework/src/X509/CertificationRequest/Attribute/ExtensionRequestValue.php', + 'SpomkyLabs\\Pki\\X509\\CertificationRequest\\Attributes' => __DIR__ . '/..' . '/spomky-labs/pki-framework/src/X509/CertificationRequest/Attributes.php', + 'SpomkyLabs\\Pki\\X509\\CertificationRequest\\CertificationRequest' => __DIR__ . '/..' . '/spomky-labs/pki-framework/src/X509/CertificationRequest/CertificationRequest.php', + 'SpomkyLabs\\Pki\\X509\\CertificationRequest\\CertificationRequestInfo' => __DIR__ . '/..' . '/spomky-labs/pki-framework/src/X509/CertificationRequest/CertificationRequestInfo.php', + 'SpomkyLabs\\Pki\\X509\\Exception\\X509ValidationException' => __DIR__ . '/..' . '/spomky-labs/pki-framework/src/X509/Exception/X509ValidationException.php', + 'SpomkyLabs\\Pki\\X509\\Feature\\DateTimeHelper' => __DIR__ . '/..' . '/spomky-labs/pki-framework/src/X509/Feature/DateTimeHelper.php', + 'SpomkyLabs\\Pki\\X509\\GeneralName\\DNSName' => __DIR__ . '/..' . '/spomky-labs/pki-framework/src/X509/GeneralName/DNSName.php', + 'SpomkyLabs\\Pki\\X509\\GeneralName\\DirectoryName' => __DIR__ . '/..' . '/spomky-labs/pki-framework/src/X509/GeneralName/DirectoryName.php', + 'SpomkyLabs\\Pki\\X509\\GeneralName\\EDIPartyName' => __DIR__ . '/..' . '/spomky-labs/pki-framework/src/X509/GeneralName/EDIPartyName.php', + 'SpomkyLabs\\Pki\\X509\\GeneralName\\GeneralName' => __DIR__ . '/..' . '/spomky-labs/pki-framework/src/X509/GeneralName/GeneralName.php', + 'SpomkyLabs\\Pki\\X509\\GeneralName\\GeneralNames' => __DIR__ . '/..' . '/spomky-labs/pki-framework/src/X509/GeneralName/GeneralNames.php', + 'SpomkyLabs\\Pki\\X509\\GeneralName\\IPAddress' => __DIR__ . '/..' . '/spomky-labs/pki-framework/src/X509/GeneralName/IPAddress.php', + 'SpomkyLabs\\Pki\\X509\\GeneralName\\IPv4Address' => __DIR__ . '/..' . '/spomky-labs/pki-framework/src/X509/GeneralName/IPv4Address.php', + 'SpomkyLabs\\Pki\\X509\\GeneralName\\IPv6Address' => __DIR__ . '/..' . '/spomky-labs/pki-framework/src/X509/GeneralName/IPv6Address.php', + 'SpomkyLabs\\Pki\\X509\\GeneralName\\OtherName' => __DIR__ . '/..' . '/spomky-labs/pki-framework/src/X509/GeneralName/OtherName.php', + 'SpomkyLabs\\Pki\\X509\\GeneralName\\RFC822Name' => __DIR__ . '/..' . '/spomky-labs/pki-framework/src/X509/GeneralName/RFC822Name.php', + 'SpomkyLabs\\Pki\\X509\\GeneralName\\RegisteredID' => __DIR__ . '/..' . '/spomky-labs/pki-framework/src/X509/GeneralName/RegisteredID.php', + 'SpomkyLabs\\Pki\\X509\\GeneralName\\UniformResourceIdentifier' => __DIR__ . '/..' . '/spomky-labs/pki-framework/src/X509/GeneralName/UniformResourceIdentifier.php', + 'SpomkyLabs\\Pki\\X509\\GeneralName\\X400Address' => __DIR__ . '/..' . '/spomky-labs/pki-framework/src/X509/GeneralName/X400Address.php', + 'Stecman\\Component\\Symfony\\Console\\BashCompletion\\Completion' => __DIR__ . '/..' . '/stecman/symfony-console-completion/src/Completion.php', + 'Stecman\\Component\\Symfony\\Console\\BashCompletion\\CompletionCommand' => __DIR__ . '/..' . '/stecman/symfony-console-completion/src/CompletionCommand.php', + 'Stecman\\Component\\Symfony\\Console\\BashCompletion\\CompletionContext' => __DIR__ . '/..' . '/stecman/symfony-console-completion/src/CompletionContext.php', + 'Stecman\\Component\\Symfony\\Console\\BashCompletion\\CompletionHandler' => __DIR__ . '/..' . '/stecman/symfony-console-completion/src/CompletionHandler.php', + 'Stecman\\Component\\Symfony\\Console\\BashCompletion\\Completion\\CompletionAwareInterface' => __DIR__ . '/..' . '/stecman/symfony-console-completion/src/Completion/CompletionAwareInterface.php', + 'Stecman\\Component\\Symfony\\Console\\BashCompletion\\Completion\\CompletionInterface' => __DIR__ . '/..' . '/stecman/symfony-console-completion/src/Completion/CompletionInterface.php', + 'Stecman\\Component\\Symfony\\Console\\BashCompletion\\Completion\\ShellPathCompletion' => __DIR__ . '/..' . '/stecman/symfony-console-completion/src/Completion/ShellPathCompletion.php', + 'Stecman\\Component\\Symfony\\Console\\BashCompletion\\EnvironmentCompletionContext' => __DIR__ . '/..' . '/stecman/symfony-console-completion/src/EnvironmentCompletionContext.php', + 'Stecman\\Component\\Symfony\\Console\\BashCompletion\\HookFactory' => __DIR__ . '/..' . '/stecman/symfony-console-completion/src/HookFactory.php', + 'Stringable' => __DIR__ . '/..' . '/marc-mabe/php-enum/stubs/Stringable.php', + 'Symfony\\Component\\Console\\Application' => __DIR__ . '/..' . '/symfony/console/Application.php', + 'Symfony\\Component\\Console\\Attribute\\AsCommand' => __DIR__ . '/..' . '/symfony/console/Attribute/AsCommand.php', + 'Symfony\\Component\\Console\\CI\\GithubActionReporter' => __DIR__ . '/..' . '/symfony/console/CI/GithubActionReporter.php', + 'Symfony\\Component\\Console\\Color' => __DIR__ . '/..' . '/symfony/console/Color.php', + 'Symfony\\Component\\Console\\CommandLoader\\CommandLoaderInterface' => __DIR__ . '/..' . '/symfony/console/CommandLoader/CommandLoaderInterface.php', + 'Symfony\\Component\\Console\\CommandLoader\\ContainerCommandLoader' => __DIR__ . '/..' . '/symfony/console/CommandLoader/ContainerCommandLoader.php', + 'Symfony\\Component\\Console\\CommandLoader\\FactoryCommandLoader' => __DIR__ . '/..' . '/symfony/console/CommandLoader/FactoryCommandLoader.php', + 'Symfony\\Component\\Console\\Command\\Command' => __DIR__ . '/..' . '/symfony/console/Command/Command.php', + 'Symfony\\Component\\Console\\Command\\CompleteCommand' => __DIR__ . '/..' . '/symfony/console/Command/CompleteCommand.php', + 'Symfony\\Component\\Console\\Command\\DumpCompletionCommand' => __DIR__ . '/..' . '/symfony/console/Command/DumpCompletionCommand.php', + 'Symfony\\Component\\Console\\Command\\HelpCommand' => __DIR__ . '/..' . '/symfony/console/Command/HelpCommand.php', + 'Symfony\\Component\\Console\\Command\\LazyCommand' => __DIR__ . '/..' . '/symfony/console/Command/LazyCommand.php', + 'Symfony\\Component\\Console\\Command\\ListCommand' => __DIR__ . '/..' . '/symfony/console/Command/ListCommand.php', + 'Symfony\\Component\\Console\\Command\\LockableTrait' => __DIR__ . '/..' . '/symfony/console/Command/LockableTrait.php', + 'Symfony\\Component\\Console\\Command\\SignalableCommandInterface' => __DIR__ . '/..' . '/symfony/console/Command/SignalableCommandInterface.php', + 'Symfony\\Component\\Console\\Command\\TraceableCommand' => __DIR__ . '/..' . '/symfony/console/Command/TraceableCommand.php', + 'Symfony\\Component\\Console\\Completion\\CompletionInput' => __DIR__ . '/..' . '/symfony/console/Completion/CompletionInput.php', + 'Symfony\\Component\\Console\\Completion\\CompletionSuggestions' => __DIR__ . '/..' . '/symfony/console/Completion/CompletionSuggestions.php', + 'Symfony\\Component\\Console\\Completion\\Output\\BashCompletionOutput' => __DIR__ . '/..' . '/symfony/console/Completion/Output/BashCompletionOutput.php', + 'Symfony\\Component\\Console\\Completion\\Output\\CompletionOutputInterface' => __DIR__ . '/..' . '/symfony/console/Completion/Output/CompletionOutputInterface.php', + 'Symfony\\Component\\Console\\Completion\\Output\\FishCompletionOutput' => __DIR__ . '/..' . '/symfony/console/Completion/Output/FishCompletionOutput.php', + 'Symfony\\Component\\Console\\Completion\\Output\\ZshCompletionOutput' => __DIR__ . '/..' . '/symfony/console/Completion/Output/ZshCompletionOutput.php', + 'Symfony\\Component\\Console\\Completion\\Suggestion' => __DIR__ . '/..' . '/symfony/console/Completion/Suggestion.php', + 'Symfony\\Component\\Console\\ConsoleEvents' => __DIR__ . '/..' . '/symfony/console/ConsoleEvents.php', + 'Symfony\\Component\\Console\\Cursor' => __DIR__ . '/..' . '/symfony/console/Cursor.php', + 'Symfony\\Component\\Console\\DataCollector\\CommandDataCollector' => __DIR__ . '/..' . '/symfony/console/DataCollector/CommandDataCollector.php', + 'Symfony\\Component\\Console\\Debug\\CliRequest' => __DIR__ . '/..' . '/symfony/console/Debug/CliRequest.php', + 'Symfony\\Component\\Console\\DependencyInjection\\AddConsoleCommandPass' => __DIR__ . '/..' . '/symfony/console/DependencyInjection/AddConsoleCommandPass.php', + 'Symfony\\Component\\Console\\Descriptor\\ApplicationDescription' => __DIR__ . '/..' . '/symfony/console/Descriptor/ApplicationDescription.php', + 'Symfony\\Component\\Console\\Descriptor\\Descriptor' => __DIR__ . '/..' . '/symfony/console/Descriptor/Descriptor.php', + 'Symfony\\Component\\Console\\Descriptor\\DescriptorInterface' => __DIR__ . '/..' . '/symfony/console/Descriptor/DescriptorInterface.php', + 'Symfony\\Component\\Console\\Descriptor\\JsonDescriptor' => __DIR__ . '/..' . '/symfony/console/Descriptor/JsonDescriptor.php', + 'Symfony\\Component\\Console\\Descriptor\\MarkdownDescriptor' => __DIR__ . '/..' . '/symfony/console/Descriptor/MarkdownDescriptor.php', + 'Symfony\\Component\\Console\\Descriptor\\ReStructuredTextDescriptor' => __DIR__ . '/..' . '/symfony/console/Descriptor/ReStructuredTextDescriptor.php', + 'Symfony\\Component\\Console\\Descriptor\\TextDescriptor' => __DIR__ . '/..' . '/symfony/console/Descriptor/TextDescriptor.php', + 'Symfony\\Component\\Console\\Descriptor\\XmlDescriptor' => __DIR__ . '/..' . '/symfony/console/Descriptor/XmlDescriptor.php', + 'Symfony\\Component\\Console\\EventListener\\ErrorListener' => __DIR__ . '/..' . '/symfony/console/EventListener/ErrorListener.php', + 'Symfony\\Component\\Console\\Event\\ConsoleCommandEvent' => __DIR__ . '/..' . '/symfony/console/Event/ConsoleCommandEvent.php', + 'Symfony\\Component\\Console\\Event\\ConsoleErrorEvent' => __DIR__ . '/..' . '/symfony/console/Event/ConsoleErrorEvent.php', + 'Symfony\\Component\\Console\\Event\\ConsoleEvent' => __DIR__ . '/..' . '/symfony/console/Event/ConsoleEvent.php', + 'Symfony\\Component\\Console\\Event\\ConsoleSignalEvent' => __DIR__ . '/..' . '/symfony/console/Event/ConsoleSignalEvent.php', + 'Symfony\\Component\\Console\\Event\\ConsoleTerminateEvent' => __DIR__ . '/..' . '/symfony/console/Event/ConsoleTerminateEvent.php', + 'Symfony\\Component\\Console\\Exception\\CommandNotFoundException' => __DIR__ . '/..' . '/symfony/console/Exception/CommandNotFoundException.php', + 'Symfony\\Component\\Console\\Exception\\ExceptionInterface' => __DIR__ . '/..' . '/symfony/console/Exception/ExceptionInterface.php', + 'Symfony\\Component\\Console\\Exception\\InvalidArgumentException' => __DIR__ . '/..' . '/symfony/console/Exception/InvalidArgumentException.php', + 'Symfony\\Component\\Console\\Exception\\InvalidOptionException' => __DIR__ . '/..' . '/symfony/console/Exception/InvalidOptionException.php', + 'Symfony\\Component\\Console\\Exception\\LogicException' => __DIR__ . '/..' . '/symfony/console/Exception/LogicException.php', + 'Symfony\\Component\\Console\\Exception\\MissingInputException' => __DIR__ . '/..' . '/symfony/console/Exception/MissingInputException.php', + 'Symfony\\Component\\Console\\Exception\\NamespaceNotFoundException' => __DIR__ . '/..' . '/symfony/console/Exception/NamespaceNotFoundException.php', + 'Symfony\\Component\\Console\\Exception\\RunCommandFailedException' => __DIR__ . '/..' . '/symfony/console/Exception/RunCommandFailedException.php', + 'Symfony\\Component\\Console\\Exception\\RuntimeException' => __DIR__ . '/..' . '/symfony/console/Exception/RuntimeException.php', + 'Symfony\\Component\\Console\\Formatter\\NullOutputFormatter' => __DIR__ . '/..' . '/symfony/console/Formatter/NullOutputFormatter.php', + 'Symfony\\Component\\Console\\Formatter\\NullOutputFormatterStyle' => __DIR__ . '/..' . '/symfony/console/Formatter/NullOutputFormatterStyle.php', + 'Symfony\\Component\\Console\\Formatter\\OutputFormatter' => __DIR__ . '/..' . '/symfony/console/Formatter/OutputFormatter.php', + 'Symfony\\Component\\Console\\Formatter\\OutputFormatterInterface' => __DIR__ . '/..' . '/symfony/console/Formatter/OutputFormatterInterface.php', + 'Symfony\\Component\\Console\\Formatter\\OutputFormatterStyle' => __DIR__ . '/..' . '/symfony/console/Formatter/OutputFormatterStyle.php', + 'Symfony\\Component\\Console\\Formatter\\OutputFormatterStyleInterface' => __DIR__ . '/..' . '/symfony/console/Formatter/OutputFormatterStyleInterface.php', + 'Symfony\\Component\\Console\\Formatter\\OutputFormatterStyleStack' => __DIR__ . '/..' . '/symfony/console/Formatter/OutputFormatterStyleStack.php', + 'Symfony\\Component\\Console\\Formatter\\WrappableOutputFormatterInterface' => __DIR__ . '/..' . '/symfony/console/Formatter/WrappableOutputFormatterInterface.php', + 'Symfony\\Component\\Console\\Helper\\DebugFormatterHelper' => __DIR__ . '/..' . '/symfony/console/Helper/DebugFormatterHelper.php', + 'Symfony\\Component\\Console\\Helper\\DescriptorHelper' => __DIR__ . '/..' . '/symfony/console/Helper/DescriptorHelper.php', + 'Symfony\\Component\\Console\\Helper\\Dumper' => __DIR__ . '/..' . '/symfony/console/Helper/Dumper.php', + 'Symfony\\Component\\Console\\Helper\\FormatterHelper' => __DIR__ . '/..' . '/symfony/console/Helper/FormatterHelper.php', + 'Symfony\\Component\\Console\\Helper\\Helper' => __DIR__ . '/..' . '/symfony/console/Helper/Helper.php', + 'Symfony\\Component\\Console\\Helper\\HelperInterface' => __DIR__ . '/..' . '/symfony/console/Helper/HelperInterface.php', + 'Symfony\\Component\\Console\\Helper\\HelperSet' => __DIR__ . '/..' . '/symfony/console/Helper/HelperSet.php', + 'Symfony\\Component\\Console\\Helper\\InputAwareHelper' => __DIR__ . '/..' . '/symfony/console/Helper/InputAwareHelper.php', + 'Symfony\\Component\\Console\\Helper\\OutputWrapper' => __DIR__ . '/..' . '/symfony/console/Helper/OutputWrapper.php', + 'Symfony\\Component\\Console\\Helper\\ProcessHelper' => __DIR__ . '/..' . '/symfony/console/Helper/ProcessHelper.php', + 'Symfony\\Component\\Console\\Helper\\ProgressBar' => __DIR__ . '/..' . '/symfony/console/Helper/ProgressBar.php', + 'Symfony\\Component\\Console\\Helper\\ProgressIndicator' => __DIR__ . '/..' . '/symfony/console/Helper/ProgressIndicator.php', + 'Symfony\\Component\\Console\\Helper\\QuestionHelper' => __DIR__ . '/..' . '/symfony/console/Helper/QuestionHelper.php', + 'Symfony\\Component\\Console\\Helper\\SymfonyQuestionHelper' => __DIR__ . '/..' . '/symfony/console/Helper/SymfonyQuestionHelper.php', + 'Symfony\\Component\\Console\\Helper\\Table' => __DIR__ . '/..' . '/symfony/console/Helper/Table.php', + 'Symfony\\Component\\Console\\Helper\\TableCell' => __DIR__ . '/..' . '/symfony/console/Helper/TableCell.php', + 'Symfony\\Component\\Console\\Helper\\TableCellStyle' => __DIR__ . '/..' . '/symfony/console/Helper/TableCellStyle.php', + 'Symfony\\Component\\Console\\Helper\\TableRows' => __DIR__ . '/..' . '/symfony/console/Helper/TableRows.php', + 'Symfony\\Component\\Console\\Helper\\TableSeparator' => __DIR__ . '/..' . '/symfony/console/Helper/TableSeparator.php', + 'Symfony\\Component\\Console\\Helper\\TableStyle' => __DIR__ . '/..' . '/symfony/console/Helper/TableStyle.php', + 'Symfony\\Component\\Console\\Input\\ArgvInput' => __DIR__ . '/..' . '/symfony/console/Input/ArgvInput.php', + 'Symfony\\Component\\Console\\Input\\ArrayInput' => __DIR__ . '/..' . '/symfony/console/Input/ArrayInput.php', + 'Symfony\\Component\\Console\\Input\\Input' => __DIR__ . '/..' . '/symfony/console/Input/Input.php', + 'Symfony\\Component\\Console\\Input\\InputArgument' => __DIR__ . '/..' . '/symfony/console/Input/InputArgument.php', + 'Symfony\\Component\\Console\\Input\\InputAwareInterface' => __DIR__ . '/..' . '/symfony/console/Input/InputAwareInterface.php', + 'Symfony\\Component\\Console\\Input\\InputDefinition' => __DIR__ . '/..' . '/symfony/console/Input/InputDefinition.php', + 'Symfony\\Component\\Console\\Input\\InputInterface' => __DIR__ . '/..' . '/symfony/console/Input/InputInterface.php', + 'Symfony\\Component\\Console\\Input\\InputOption' => __DIR__ . '/..' . '/symfony/console/Input/InputOption.php', + 'Symfony\\Component\\Console\\Input\\StreamableInputInterface' => __DIR__ . '/..' . '/symfony/console/Input/StreamableInputInterface.php', + 'Symfony\\Component\\Console\\Input\\StringInput' => __DIR__ . '/..' . '/symfony/console/Input/StringInput.php', + 'Symfony\\Component\\Console\\Logger\\ConsoleLogger' => __DIR__ . '/..' . '/symfony/console/Logger/ConsoleLogger.php', + 'Symfony\\Component\\Console\\Messenger\\RunCommandContext' => __DIR__ . '/..' . '/symfony/console/Messenger/RunCommandContext.php', + 'Symfony\\Component\\Console\\Messenger\\RunCommandMessage' => __DIR__ . '/..' . '/symfony/console/Messenger/RunCommandMessage.php', + 'Symfony\\Component\\Console\\Messenger\\RunCommandMessageHandler' => __DIR__ . '/..' . '/symfony/console/Messenger/RunCommandMessageHandler.php', + 'Symfony\\Component\\Console\\Output\\AnsiColorMode' => __DIR__ . '/..' . '/symfony/console/Output/AnsiColorMode.php', + 'Symfony\\Component\\Console\\Output\\BufferedOutput' => __DIR__ . '/..' . '/symfony/console/Output/BufferedOutput.php', + 'Symfony\\Component\\Console\\Output\\ConsoleOutput' => __DIR__ . '/..' . '/symfony/console/Output/ConsoleOutput.php', + 'Symfony\\Component\\Console\\Output\\ConsoleOutputInterface' => __DIR__ . '/..' . '/symfony/console/Output/ConsoleOutputInterface.php', + 'Symfony\\Component\\Console\\Output\\ConsoleSectionOutput' => __DIR__ . '/..' . '/symfony/console/Output/ConsoleSectionOutput.php', + 'Symfony\\Component\\Console\\Output\\NullOutput' => __DIR__ . '/..' . '/symfony/console/Output/NullOutput.php', + 'Symfony\\Component\\Console\\Output\\Output' => __DIR__ . '/..' . '/symfony/console/Output/Output.php', + 'Symfony\\Component\\Console\\Output\\OutputInterface' => __DIR__ . '/..' . '/symfony/console/Output/OutputInterface.php', + 'Symfony\\Component\\Console\\Output\\StreamOutput' => __DIR__ . '/..' . '/symfony/console/Output/StreamOutput.php', + 'Symfony\\Component\\Console\\Output\\TrimmedBufferOutput' => __DIR__ . '/..' . '/symfony/console/Output/TrimmedBufferOutput.php', + 'Symfony\\Component\\Console\\Question\\ChoiceQuestion' => __DIR__ . '/..' . '/symfony/console/Question/ChoiceQuestion.php', + 'Symfony\\Component\\Console\\Question\\ConfirmationQuestion' => __DIR__ . '/..' . '/symfony/console/Question/ConfirmationQuestion.php', + 'Symfony\\Component\\Console\\Question\\Question' => __DIR__ . '/..' . '/symfony/console/Question/Question.php', + 'Symfony\\Component\\Console\\SignalRegistry\\SignalMap' => __DIR__ . '/..' . '/symfony/console/SignalRegistry/SignalMap.php', + 'Symfony\\Component\\Console\\SignalRegistry\\SignalRegistry' => __DIR__ . '/..' . '/symfony/console/SignalRegistry/SignalRegistry.php', + 'Symfony\\Component\\Console\\SingleCommandApplication' => __DIR__ . '/..' . '/symfony/console/SingleCommandApplication.php', + 'Symfony\\Component\\Console\\Style\\OutputStyle' => __DIR__ . '/..' . '/symfony/console/Style/OutputStyle.php', + 'Symfony\\Component\\Console\\Style\\StyleInterface' => __DIR__ . '/..' . '/symfony/console/Style/StyleInterface.php', + 'Symfony\\Component\\Console\\Style\\SymfonyStyle' => __DIR__ . '/..' . '/symfony/console/Style/SymfonyStyle.php', + 'Symfony\\Component\\Console\\Terminal' => __DIR__ . '/..' . '/symfony/console/Terminal.php', + 'Symfony\\Component\\Console\\Tester\\ApplicationTester' => __DIR__ . '/..' . '/symfony/console/Tester/ApplicationTester.php', + 'Symfony\\Component\\Console\\Tester\\CommandCompletionTester' => __DIR__ . '/..' . '/symfony/console/Tester/CommandCompletionTester.php', + 'Symfony\\Component\\Console\\Tester\\CommandTester' => __DIR__ . '/..' . '/symfony/console/Tester/CommandTester.php', + 'Symfony\\Component\\Console\\Tester\\Constraint\\CommandIsSuccessful' => __DIR__ . '/..' . '/symfony/console/Tester/Constraint/CommandIsSuccessful.php', + 'Symfony\\Component\\Console\\Tester\\TesterTrait' => __DIR__ . '/..' . '/symfony/console/Tester/TesterTrait.php', + 'Symfony\\Component\\CssSelector\\CssSelectorConverter' => __DIR__ . '/..' . '/symfony/css-selector/CssSelectorConverter.php', + 'Symfony\\Component\\CssSelector\\Exception\\ExceptionInterface' => __DIR__ . '/..' . '/symfony/css-selector/Exception/ExceptionInterface.php', + 'Symfony\\Component\\CssSelector\\Exception\\ExpressionErrorException' => __DIR__ . '/..' . '/symfony/css-selector/Exception/ExpressionErrorException.php', + 'Symfony\\Component\\CssSelector\\Exception\\InternalErrorException' => __DIR__ . '/..' . '/symfony/css-selector/Exception/InternalErrorException.php', + 'Symfony\\Component\\CssSelector\\Exception\\ParseException' => __DIR__ . '/..' . '/symfony/css-selector/Exception/ParseException.php', + 'Symfony\\Component\\CssSelector\\Exception\\SyntaxErrorException' => __DIR__ . '/..' . '/symfony/css-selector/Exception/SyntaxErrorException.php', + 'Symfony\\Component\\CssSelector\\Node\\AbstractNode' => __DIR__ . '/..' . '/symfony/css-selector/Node/AbstractNode.php', + 'Symfony\\Component\\CssSelector\\Node\\AttributeNode' => __DIR__ . '/..' . '/symfony/css-selector/Node/AttributeNode.php', + 'Symfony\\Component\\CssSelector\\Node\\ClassNode' => __DIR__ . '/..' . '/symfony/css-selector/Node/ClassNode.php', + 'Symfony\\Component\\CssSelector\\Node\\CombinedSelectorNode' => __DIR__ . '/..' . '/symfony/css-selector/Node/CombinedSelectorNode.php', + 'Symfony\\Component\\CssSelector\\Node\\ElementNode' => __DIR__ . '/..' . '/symfony/css-selector/Node/ElementNode.php', + 'Symfony\\Component\\CssSelector\\Node\\FunctionNode' => __DIR__ . '/..' . '/symfony/css-selector/Node/FunctionNode.php', + 'Symfony\\Component\\CssSelector\\Node\\HashNode' => __DIR__ . '/..' . '/symfony/css-selector/Node/HashNode.php', + 'Symfony\\Component\\CssSelector\\Node\\NegationNode' => __DIR__ . '/..' . '/symfony/css-selector/Node/NegationNode.php', + 'Symfony\\Component\\CssSelector\\Node\\NodeInterface' => __DIR__ . '/..' . '/symfony/css-selector/Node/NodeInterface.php', + 'Symfony\\Component\\CssSelector\\Node\\PseudoNode' => __DIR__ . '/..' . '/symfony/css-selector/Node/PseudoNode.php', + 'Symfony\\Component\\CssSelector\\Node\\SelectorNode' => __DIR__ . '/..' . '/symfony/css-selector/Node/SelectorNode.php', + 'Symfony\\Component\\CssSelector\\Node\\Specificity' => __DIR__ . '/..' . '/symfony/css-selector/Node/Specificity.php', + 'Symfony\\Component\\CssSelector\\Parser\\Handler\\CommentHandler' => __DIR__ . '/..' . '/symfony/css-selector/Parser/Handler/CommentHandler.php', + 'Symfony\\Component\\CssSelector\\Parser\\Handler\\HandlerInterface' => __DIR__ . '/..' . '/symfony/css-selector/Parser/Handler/HandlerInterface.php', + 'Symfony\\Component\\CssSelector\\Parser\\Handler\\HashHandler' => __DIR__ . '/..' . '/symfony/css-selector/Parser/Handler/HashHandler.php', + 'Symfony\\Component\\CssSelector\\Parser\\Handler\\IdentifierHandler' => __DIR__ . '/..' . '/symfony/css-selector/Parser/Handler/IdentifierHandler.php', + 'Symfony\\Component\\CssSelector\\Parser\\Handler\\NumberHandler' => __DIR__ . '/..' . '/symfony/css-selector/Parser/Handler/NumberHandler.php', + 'Symfony\\Component\\CssSelector\\Parser\\Handler\\StringHandler' => __DIR__ . '/..' . '/symfony/css-selector/Parser/Handler/StringHandler.php', + 'Symfony\\Component\\CssSelector\\Parser\\Handler\\WhitespaceHandler' => __DIR__ . '/..' . '/symfony/css-selector/Parser/Handler/WhitespaceHandler.php', + 'Symfony\\Component\\CssSelector\\Parser\\Parser' => __DIR__ . '/..' . '/symfony/css-selector/Parser/Parser.php', + 'Symfony\\Component\\CssSelector\\Parser\\ParserInterface' => __DIR__ . '/..' . '/symfony/css-selector/Parser/ParserInterface.php', + 'Symfony\\Component\\CssSelector\\Parser\\Reader' => __DIR__ . '/..' . '/symfony/css-selector/Parser/Reader.php', + 'Symfony\\Component\\CssSelector\\Parser\\Shortcut\\ClassParser' => __DIR__ . '/..' . '/symfony/css-selector/Parser/Shortcut/ClassParser.php', + 'Symfony\\Component\\CssSelector\\Parser\\Shortcut\\ElementParser' => __DIR__ . '/..' . '/symfony/css-selector/Parser/Shortcut/ElementParser.php', + 'Symfony\\Component\\CssSelector\\Parser\\Shortcut\\EmptyStringParser' => __DIR__ . '/..' . '/symfony/css-selector/Parser/Shortcut/EmptyStringParser.php', + 'Symfony\\Component\\CssSelector\\Parser\\Shortcut\\HashParser' => __DIR__ . '/..' . '/symfony/css-selector/Parser/Shortcut/HashParser.php', + 'Symfony\\Component\\CssSelector\\Parser\\Token' => __DIR__ . '/..' . '/symfony/css-selector/Parser/Token.php', + 'Symfony\\Component\\CssSelector\\Parser\\TokenStream' => __DIR__ . '/..' . '/symfony/css-selector/Parser/TokenStream.php', + 'Symfony\\Component\\CssSelector\\Parser\\Tokenizer\\Tokenizer' => __DIR__ . '/..' . '/symfony/css-selector/Parser/Tokenizer/Tokenizer.php', + 'Symfony\\Component\\CssSelector\\Parser\\Tokenizer\\TokenizerEscaping' => __DIR__ . '/..' . '/symfony/css-selector/Parser/Tokenizer/TokenizerEscaping.php', + 'Symfony\\Component\\CssSelector\\Parser\\Tokenizer\\TokenizerPatterns' => __DIR__ . '/..' . '/symfony/css-selector/Parser/Tokenizer/TokenizerPatterns.php', + 'Symfony\\Component\\CssSelector\\XPath\\Extension\\AbstractExtension' => __DIR__ . '/..' . '/symfony/css-selector/XPath/Extension/AbstractExtension.php', + 'Symfony\\Component\\CssSelector\\XPath\\Extension\\AttributeMatchingExtension' => __DIR__ . '/..' . '/symfony/css-selector/XPath/Extension/AttributeMatchingExtension.php', + 'Symfony\\Component\\CssSelector\\XPath\\Extension\\CombinationExtension' => __DIR__ . '/..' . '/symfony/css-selector/XPath/Extension/CombinationExtension.php', + 'Symfony\\Component\\CssSelector\\XPath\\Extension\\ExtensionInterface' => __DIR__ . '/..' . '/symfony/css-selector/XPath/Extension/ExtensionInterface.php', + 'Symfony\\Component\\CssSelector\\XPath\\Extension\\FunctionExtension' => __DIR__ . '/..' . '/symfony/css-selector/XPath/Extension/FunctionExtension.php', + 'Symfony\\Component\\CssSelector\\XPath\\Extension\\HtmlExtension' => __DIR__ . '/..' . '/symfony/css-selector/XPath/Extension/HtmlExtension.php', + 'Symfony\\Component\\CssSelector\\XPath\\Extension\\NodeExtension' => __DIR__ . '/..' . '/symfony/css-selector/XPath/Extension/NodeExtension.php', + 'Symfony\\Component\\CssSelector\\XPath\\Extension\\PseudoClassExtension' => __DIR__ . '/..' . '/symfony/css-selector/XPath/Extension/PseudoClassExtension.php', + 'Symfony\\Component\\CssSelector\\XPath\\Translator' => __DIR__ . '/..' . '/symfony/css-selector/XPath/Translator.php', + 'Symfony\\Component\\CssSelector\\XPath\\TranslatorInterface' => __DIR__ . '/..' . '/symfony/css-selector/XPath/TranslatorInterface.php', + 'Symfony\\Component\\CssSelector\\XPath\\XPathExpr' => __DIR__ . '/..' . '/symfony/css-selector/XPath/XPathExpr.php', + 'Symfony\\Component\\DomCrawler\\AbstractUriElement' => __DIR__ . '/..' . '/symfony/dom-crawler/AbstractUriElement.php', + 'Symfony\\Component\\DomCrawler\\Crawler' => __DIR__ . '/..' . '/symfony/dom-crawler/Crawler.php', + 'Symfony\\Component\\DomCrawler\\Field\\ChoiceFormField' => __DIR__ . '/..' . '/symfony/dom-crawler/Field/ChoiceFormField.php', + 'Symfony\\Component\\DomCrawler\\Field\\FileFormField' => __DIR__ . '/..' . '/symfony/dom-crawler/Field/FileFormField.php', + 'Symfony\\Component\\DomCrawler\\Field\\FormField' => __DIR__ . '/..' . '/symfony/dom-crawler/Field/FormField.php', + 'Symfony\\Component\\DomCrawler\\Field\\InputFormField' => __DIR__ . '/..' . '/symfony/dom-crawler/Field/InputFormField.php', + 'Symfony\\Component\\DomCrawler\\Field\\TextareaFormField' => __DIR__ . '/..' . '/symfony/dom-crawler/Field/TextareaFormField.php', + 'Symfony\\Component\\DomCrawler\\Form' => __DIR__ . '/..' . '/symfony/dom-crawler/Form.php', + 'Symfony\\Component\\DomCrawler\\FormFieldRegistry' => __DIR__ . '/..' . '/symfony/dom-crawler/FormFieldRegistry.php', + 'Symfony\\Component\\DomCrawler\\Image' => __DIR__ . '/..' . '/symfony/dom-crawler/Image.php', + 'Symfony\\Component\\DomCrawler\\Link' => __DIR__ . '/..' . '/symfony/dom-crawler/Link.php', + 'Symfony\\Component\\DomCrawler\\UriResolver' => __DIR__ . '/..' . '/symfony/dom-crawler/UriResolver.php', + 'Symfony\\Component\\EventDispatcher\\Attribute\\AsEventListener' => __DIR__ . '/..' . '/symfony/event-dispatcher/Attribute/AsEventListener.php', + 'Symfony\\Component\\EventDispatcher\\Debug\\TraceableEventDispatcher' => __DIR__ . '/..' . '/symfony/event-dispatcher/Debug/TraceableEventDispatcher.php', + 'Symfony\\Component\\EventDispatcher\\Debug\\WrappedListener' => __DIR__ . '/..' . '/symfony/event-dispatcher/Debug/WrappedListener.php', + 'Symfony\\Component\\EventDispatcher\\DependencyInjection\\AddEventAliasesPass' => __DIR__ . '/..' . '/symfony/event-dispatcher/DependencyInjection/AddEventAliasesPass.php', + 'Symfony\\Component\\EventDispatcher\\DependencyInjection\\RegisterListenersPass' => __DIR__ . '/..' . '/symfony/event-dispatcher/DependencyInjection/RegisterListenersPass.php', + 'Symfony\\Component\\EventDispatcher\\EventDispatcher' => __DIR__ . '/..' . '/symfony/event-dispatcher/EventDispatcher.php', + 'Symfony\\Component\\EventDispatcher\\EventDispatcherInterface' => __DIR__ . '/..' . '/symfony/event-dispatcher/EventDispatcherInterface.php', + 'Symfony\\Component\\EventDispatcher\\EventSubscriberInterface' => __DIR__ . '/..' . '/symfony/event-dispatcher/EventSubscriberInterface.php', + 'Symfony\\Component\\EventDispatcher\\GenericEvent' => __DIR__ . '/..' . '/symfony/event-dispatcher/GenericEvent.php', + 'Symfony\\Component\\EventDispatcher\\ImmutableEventDispatcher' => __DIR__ . '/..' . '/symfony/event-dispatcher/ImmutableEventDispatcher.php', + 'Symfony\\Component\\HttpFoundation\\AcceptHeader' => __DIR__ . '/..' . '/symfony/http-foundation/AcceptHeader.php', + 'Symfony\\Component\\HttpFoundation\\AcceptHeaderItem' => __DIR__ . '/..' . '/symfony/http-foundation/AcceptHeaderItem.php', + 'Symfony\\Component\\HttpFoundation\\BinaryFileResponse' => __DIR__ . '/..' . '/symfony/http-foundation/BinaryFileResponse.php', + 'Symfony\\Component\\HttpFoundation\\ChainRequestMatcher' => __DIR__ . '/..' . '/symfony/http-foundation/ChainRequestMatcher.php', + 'Symfony\\Component\\HttpFoundation\\Cookie' => __DIR__ . '/..' . '/symfony/http-foundation/Cookie.php', + 'Symfony\\Component\\HttpFoundation\\Exception\\BadRequestException' => __DIR__ . '/..' . '/symfony/http-foundation/Exception/BadRequestException.php', + 'Symfony\\Component\\HttpFoundation\\Exception\\ConflictingHeadersException' => __DIR__ . '/..' . '/symfony/http-foundation/Exception/ConflictingHeadersException.php', + 'Symfony\\Component\\HttpFoundation\\Exception\\JsonException' => __DIR__ . '/..' . '/symfony/http-foundation/Exception/JsonException.php', + 'Symfony\\Component\\HttpFoundation\\Exception\\RequestExceptionInterface' => __DIR__ . '/..' . '/symfony/http-foundation/Exception/RequestExceptionInterface.php', + 'Symfony\\Component\\HttpFoundation\\Exception\\SessionNotFoundException' => __DIR__ . '/..' . '/symfony/http-foundation/Exception/SessionNotFoundException.php', + 'Symfony\\Component\\HttpFoundation\\Exception\\SuspiciousOperationException' => __DIR__ . '/..' . '/symfony/http-foundation/Exception/SuspiciousOperationException.php', + 'Symfony\\Component\\HttpFoundation\\Exception\\UnexpectedValueException' => __DIR__ . '/..' . '/symfony/http-foundation/Exception/UnexpectedValueException.php', + 'Symfony\\Component\\HttpFoundation\\ExpressionRequestMatcher' => __DIR__ . '/..' . '/symfony/http-foundation/ExpressionRequestMatcher.php', + 'Symfony\\Component\\HttpFoundation\\FileBag' => __DIR__ . '/..' . '/symfony/http-foundation/FileBag.php', + 'Symfony\\Component\\HttpFoundation\\File\\Exception\\AccessDeniedException' => __DIR__ . '/..' . '/symfony/http-foundation/File/Exception/AccessDeniedException.php', + 'Symfony\\Component\\HttpFoundation\\File\\Exception\\CannotWriteFileException' => __DIR__ . '/..' . '/symfony/http-foundation/File/Exception/CannotWriteFileException.php', + 'Symfony\\Component\\HttpFoundation\\File\\Exception\\ExtensionFileException' => __DIR__ . '/..' . '/symfony/http-foundation/File/Exception/ExtensionFileException.php', + 'Symfony\\Component\\HttpFoundation\\File\\Exception\\FileException' => __DIR__ . '/..' . '/symfony/http-foundation/File/Exception/FileException.php', + 'Symfony\\Component\\HttpFoundation\\File\\Exception\\FileNotFoundException' => __DIR__ . '/..' . '/symfony/http-foundation/File/Exception/FileNotFoundException.php', + 'Symfony\\Component\\HttpFoundation\\File\\Exception\\FormSizeFileException' => __DIR__ . '/..' . '/symfony/http-foundation/File/Exception/FormSizeFileException.php', + 'Symfony\\Component\\HttpFoundation\\File\\Exception\\IniSizeFileException' => __DIR__ . '/..' . '/symfony/http-foundation/File/Exception/IniSizeFileException.php', + 'Symfony\\Component\\HttpFoundation\\File\\Exception\\NoFileException' => __DIR__ . '/..' . '/symfony/http-foundation/File/Exception/NoFileException.php', + 'Symfony\\Component\\HttpFoundation\\File\\Exception\\NoTmpDirFileException' => __DIR__ . '/..' . '/symfony/http-foundation/File/Exception/NoTmpDirFileException.php', + 'Symfony\\Component\\HttpFoundation\\File\\Exception\\PartialFileException' => __DIR__ . '/..' . '/symfony/http-foundation/File/Exception/PartialFileException.php', + 'Symfony\\Component\\HttpFoundation\\File\\Exception\\UnexpectedTypeException' => __DIR__ . '/..' . '/symfony/http-foundation/File/Exception/UnexpectedTypeException.php', + 'Symfony\\Component\\HttpFoundation\\File\\Exception\\UploadException' => __DIR__ . '/..' . '/symfony/http-foundation/File/Exception/UploadException.php', + 'Symfony\\Component\\HttpFoundation\\File\\File' => __DIR__ . '/..' . '/symfony/http-foundation/File/File.php', + 'Symfony\\Component\\HttpFoundation\\File\\Stream' => __DIR__ . '/..' . '/symfony/http-foundation/File/Stream.php', + 'Symfony\\Component\\HttpFoundation\\File\\UploadedFile' => __DIR__ . '/..' . '/symfony/http-foundation/File/UploadedFile.php', + 'Symfony\\Component\\HttpFoundation\\HeaderBag' => __DIR__ . '/..' . '/symfony/http-foundation/HeaderBag.php', + 'Symfony\\Component\\HttpFoundation\\HeaderUtils' => __DIR__ . '/..' . '/symfony/http-foundation/HeaderUtils.php', + 'Symfony\\Component\\HttpFoundation\\InputBag' => __DIR__ . '/..' . '/symfony/http-foundation/InputBag.php', + 'Symfony\\Component\\HttpFoundation\\IpUtils' => __DIR__ . '/..' . '/symfony/http-foundation/IpUtils.php', + 'Symfony\\Component\\HttpFoundation\\JsonResponse' => __DIR__ . '/..' . '/symfony/http-foundation/JsonResponse.php', + 'Symfony\\Component\\HttpFoundation\\ParameterBag' => __DIR__ . '/..' . '/symfony/http-foundation/ParameterBag.php', + 'Symfony\\Component\\HttpFoundation\\RateLimiter\\AbstractRequestRateLimiter' => __DIR__ . '/..' . '/symfony/http-foundation/RateLimiter/AbstractRequestRateLimiter.php', + 'Symfony\\Component\\HttpFoundation\\RateLimiter\\PeekableRequestRateLimiterInterface' => __DIR__ . '/..' . '/symfony/http-foundation/RateLimiter/PeekableRequestRateLimiterInterface.php', + 'Symfony\\Component\\HttpFoundation\\RateLimiter\\RequestRateLimiterInterface' => __DIR__ . '/..' . '/symfony/http-foundation/RateLimiter/RequestRateLimiterInterface.php', + 'Symfony\\Component\\HttpFoundation\\RedirectResponse' => __DIR__ . '/..' . '/symfony/http-foundation/RedirectResponse.php', + 'Symfony\\Component\\HttpFoundation\\Request' => __DIR__ . '/..' . '/symfony/http-foundation/Request.php', + 'Symfony\\Component\\HttpFoundation\\RequestMatcher' => __DIR__ . '/..' . '/symfony/http-foundation/RequestMatcher.php', + 'Symfony\\Component\\HttpFoundation\\RequestMatcherInterface' => __DIR__ . '/..' . '/symfony/http-foundation/RequestMatcherInterface.php', + 'Symfony\\Component\\HttpFoundation\\RequestMatcher\\AttributesRequestMatcher' => __DIR__ . '/..' . '/symfony/http-foundation/RequestMatcher/AttributesRequestMatcher.php', + 'Symfony\\Component\\HttpFoundation\\RequestMatcher\\ExpressionRequestMatcher' => __DIR__ . '/..' . '/symfony/http-foundation/RequestMatcher/ExpressionRequestMatcher.php', + 'Symfony\\Component\\HttpFoundation\\RequestMatcher\\HostRequestMatcher' => __DIR__ . '/..' . '/symfony/http-foundation/RequestMatcher/HostRequestMatcher.php', + 'Symfony\\Component\\HttpFoundation\\RequestMatcher\\IpsRequestMatcher' => __DIR__ . '/..' . '/symfony/http-foundation/RequestMatcher/IpsRequestMatcher.php', + 'Symfony\\Component\\HttpFoundation\\RequestMatcher\\IsJsonRequestMatcher' => __DIR__ . '/..' . '/symfony/http-foundation/RequestMatcher/IsJsonRequestMatcher.php', + 'Symfony\\Component\\HttpFoundation\\RequestMatcher\\MethodRequestMatcher' => __DIR__ . '/..' . '/symfony/http-foundation/RequestMatcher/MethodRequestMatcher.php', + 'Symfony\\Component\\HttpFoundation\\RequestMatcher\\PathRequestMatcher' => __DIR__ . '/..' . '/symfony/http-foundation/RequestMatcher/PathRequestMatcher.php', + 'Symfony\\Component\\HttpFoundation\\RequestMatcher\\PortRequestMatcher' => __DIR__ . '/..' . '/symfony/http-foundation/RequestMatcher/PortRequestMatcher.php', + 'Symfony\\Component\\HttpFoundation\\RequestMatcher\\SchemeRequestMatcher' => __DIR__ . '/..' . '/symfony/http-foundation/RequestMatcher/SchemeRequestMatcher.php', + 'Symfony\\Component\\HttpFoundation\\RequestStack' => __DIR__ . '/..' . '/symfony/http-foundation/RequestStack.php', + 'Symfony\\Component\\HttpFoundation\\Response' => __DIR__ . '/..' . '/symfony/http-foundation/Response.php', + 'Symfony\\Component\\HttpFoundation\\ResponseHeaderBag' => __DIR__ . '/..' . '/symfony/http-foundation/ResponseHeaderBag.php', + 'Symfony\\Component\\HttpFoundation\\ServerBag' => __DIR__ . '/..' . '/symfony/http-foundation/ServerBag.php', + 'Symfony\\Component\\HttpFoundation\\Session\\Attribute\\AttributeBag' => __DIR__ . '/..' . '/symfony/http-foundation/Session/Attribute/AttributeBag.php', + 'Symfony\\Component\\HttpFoundation\\Session\\Attribute\\AttributeBagInterface' => __DIR__ . '/..' . '/symfony/http-foundation/Session/Attribute/AttributeBagInterface.php', + 'Symfony\\Component\\HttpFoundation\\Session\\FlashBagAwareSessionInterface' => __DIR__ . '/..' . '/symfony/http-foundation/Session/FlashBagAwareSessionInterface.php', + 'Symfony\\Component\\HttpFoundation\\Session\\Flash\\AutoExpireFlashBag' => __DIR__ . '/..' . '/symfony/http-foundation/Session/Flash/AutoExpireFlashBag.php', + 'Symfony\\Component\\HttpFoundation\\Session\\Flash\\FlashBag' => __DIR__ . '/..' . '/symfony/http-foundation/Session/Flash/FlashBag.php', + 'Symfony\\Component\\HttpFoundation\\Session\\Flash\\FlashBagInterface' => __DIR__ . '/..' . '/symfony/http-foundation/Session/Flash/FlashBagInterface.php', + 'Symfony\\Component\\HttpFoundation\\Session\\Session' => __DIR__ . '/..' . '/symfony/http-foundation/Session/Session.php', + 'Symfony\\Component\\HttpFoundation\\Session\\SessionBagInterface' => __DIR__ . '/..' . '/symfony/http-foundation/Session/SessionBagInterface.php', + 'Symfony\\Component\\HttpFoundation\\Session\\SessionBagProxy' => __DIR__ . '/..' . '/symfony/http-foundation/Session/SessionBagProxy.php', + 'Symfony\\Component\\HttpFoundation\\Session\\SessionFactory' => __DIR__ . '/..' . '/symfony/http-foundation/Session/SessionFactory.php', + 'Symfony\\Component\\HttpFoundation\\Session\\SessionFactoryInterface' => __DIR__ . '/..' . '/symfony/http-foundation/Session/SessionFactoryInterface.php', + 'Symfony\\Component\\HttpFoundation\\Session\\SessionInterface' => __DIR__ . '/..' . '/symfony/http-foundation/Session/SessionInterface.php', + 'Symfony\\Component\\HttpFoundation\\Session\\SessionUtils' => __DIR__ . '/..' . '/symfony/http-foundation/Session/SessionUtils.php', + 'Symfony\\Component\\HttpFoundation\\Session\\Storage\\Handler\\AbstractSessionHandler' => __DIR__ . '/..' . '/symfony/http-foundation/Session/Storage/Handler/AbstractSessionHandler.php', + 'Symfony\\Component\\HttpFoundation\\Session\\Storage\\Handler\\IdentityMarshaller' => __DIR__ . '/..' . '/symfony/http-foundation/Session/Storage/Handler/IdentityMarshaller.php', + 'Symfony\\Component\\HttpFoundation\\Session\\Storage\\Handler\\MarshallingSessionHandler' => __DIR__ . '/..' . '/symfony/http-foundation/Session/Storage/Handler/MarshallingSessionHandler.php', + 'Symfony\\Component\\HttpFoundation\\Session\\Storage\\Handler\\MemcachedSessionHandler' => __DIR__ . '/..' . '/symfony/http-foundation/Session/Storage/Handler/MemcachedSessionHandler.php', + 'Symfony\\Component\\HttpFoundation\\Session\\Storage\\Handler\\MigratingSessionHandler' => __DIR__ . '/..' . '/symfony/http-foundation/Session/Storage/Handler/MigratingSessionHandler.php', + 'Symfony\\Component\\HttpFoundation\\Session\\Storage\\Handler\\MongoDbSessionHandler' => __DIR__ . '/..' . '/symfony/http-foundation/Session/Storage/Handler/MongoDbSessionHandler.php', + 'Symfony\\Component\\HttpFoundation\\Session\\Storage\\Handler\\NativeFileSessionHandler' => __DIR__ . '/..' . '/symfony/http-foundation/Session/Storage/Handler/NativeFileSessionHandler.php', + 'Symfony\\Component\\HttpFoundation\\Session\\Storage\\Handler\\NullSessionHandler' => __DIR__ . '/..' . '/symfony/http-foundation/Session/Storage/Handler/NullSessionHandler.php', + 'Symfony\\Component\\HttpFoundation\\Session\\Storage\\Handler\\PdoSessionHandler' => __DIR__ . '/..' . '/symfony/http-foundation/Session/Storage/Handler/PdoSessionHandler.php', + 'Symfony\\Component\\HttpFoundation\\Session\\Storage\\Handler\\RedisSessionHandler' => __DIR__ . '/..' . '/symfony/http-foundation/Session/Storage/Handler/RedisSessionHandler.php', + 'Symfony\\Component\\HttpFoundation\\Session\\Storage\\Handler\\SessionHandlerFactory' => __DIR__ . '/..' . '/symfony/http-foundation/Session/Storage/Handler/SessionHandlerFactory.php', + 'Symfony\\Component\\HttpFoundation\\Session\\Storage\\Handler\\StrictSessionHandler' => __DIR__ . '/..' . '/symfony/http-foundation/Session/Storage/Handler/StrictSessionHandler.php', + 'Symfony\\Component\\HttpFoundation\\Session\\Storage\\MetadataBag' => __DIR__ . '/..' . '/symfony/http-foundation/Session/Storage/MetadataBag.php', + 'Symfony\\Component\\HttpFoundation\\Session\\Storage\\MockArraySessionStorage' => __DIR__ . '/..' . '/symfony/http-foundation/Session/Storage/MockArraySessionStorage.php', + 'Symfony\\Component\\HttpFoundation\\Session\\Storage\\MockFileSessionStorage' => __DIR__ . '/..' . '/symfony/http-foundation/Session/Storage/MockFileSessionStorage.php', + 'Symfony\\Component\\HttpFoundation\\Session\\Storage\\MockFileSessionStorageFactory' => __DIR__ . '/..' . '/symfony/http-foundation/Session/Storage/MockFileSessionStorageFactory.php', + 'Symfony\\Component\\HttpFoundation\\Session\\Storage\\NativeSessionStorage' => __DIR__ . '/..' . '/symfony/http-foundation/Session/Storage/NativeSessionStorage.php', + 'Symfony\\Component\\HttpFoundation\\Session\\Storage\\NativeSessionStorageFactory' => __DIR__ . '/..' . '/symfony/http-foundation/Session/Storage/NativeSessionStorageFactory.php', + 'Symfony\\Component\\HttpFoundation\\Session\\Storage\\PhpBridgeSessionStorage' => __DIR__ . '/..' . '/symfony/http-foundation/Session/Storage/PhpBridgeSessionStorage.php', + 'Symfony\\Component\\HttpFoundation\\Session\\Storage\\PhpBridgeSessionStorageFactory' => __DIR__ . '/..' . '/symfony/http-foundation/Session/Storage/PhpBridgeSessionStorageFactory.php', + 'Symfony\\Component\\HttpFoundation\\Session\\Storage\\Proxy\\AbstractProxy' => __DIR__ . '/..' . '/symfony/http-foundation/Session/Storage/Proxy/AbstractProxy.php', + 'Symfony\\Component\\HttpFoundation\\Session\\Storage\\Proxy\\SessionHandlerProxy' => __DIR__ . '/..' . '/symfony/http-foundation/Session/Storage/Proxy/SessionHandlerProxy.php', + 'Symfony\\Component\\HttpFoundation\\Session\\Storage\\SessionStorageFactoryInterface' => __DIR__ . '/..' . '/symfony/http-foundation/Session/Storage/SessionStorageFactoryInterface.php', + 'Symfony\\Component\\HttpFoundation\\Session\\Storage\\SessionStorageInterface' => __DIR__ . '/..' . '/symfony/http-foundation/Session/Storage/SessionStorageInterface.php', + 'Symfony\\Component\\HttpFoundation\\StreamedJsonResponse' => __DIR__ . '/..' . '/symfony/http-foundation/StreamedJsonResponse.php', + 'Symfony\\Component\\HttpFoundation\\StreamedResponse' => __DIR__ . '/..' . '/symfony/http-foundation/StreamedResponse.php', + 'Symfony\\Component\\HttpFoundation\\UriSigner' => __DIR__ . '/..' . '/symfony/http-foundation/UriSigner.php', + 'Symfony\\Component\\HttpFoundation\\UrlHelper' => __DIR__ . '/..' . '/symfony/http-foundation/UrlHelper.php', + 'Symfony\\Component\\Mailer\\Command\\MailerTestCommand' => __DIR__ . '/..' . '/symfony/mailer/Command/MailerTestCommand.php', + 'Symfony\\Component\\Mailer\\DataCollector\\MessageDataCollector' => __DIR__ . '/..' . '/symfony/mailer/DataCollector/MessageDataCollector.php', + 'Symfony\\Component\\Mailer\\DelayedEnvelope' => __DIR__ . '/..' . '/symfony/mailer/DelayedEnvelope.php', + 'Symfony\\Component\\Mailer\\Envelope' => __DIR__ . '/..' . '/symfony/mailer/Envelope.php', + 'Symfony\\Component\\Mailer\\EventListener\\EnvelopeListener' => __DIR__ . '/..' . '/symfony/mailer/EventListener/EnvelopeListener.php', + 'Symfony\\Component\\Mailer\\EventListener\\MessageListener' => __DIR__ . '/..' . '/symfony/mailer/EventListener/MessageListener.php', + 'Symfony\\Component\\Mailer\\EventListener\\MessageLoggerListener' => __DIR__ . '/..' . '/symfony/mailer/EventListener/MessageLoggerListener.php', + 'Symfony\\Component\\Mailer\\EventListener\\MessengerTransportListener' => __DIR__ . '/..' . '/symfony/mailer/EventListener/MessengerTransportListener.php', + 'Symfony\\Component\\Mailer\\Event\\FailedMessageEvent' => __DIR__ . '/..' . '/symfony/mailer/Event/FailedMessageEvent.php', + 'Symfony\\Component\\Mailer\\Event\\MessageEvent' => __DIR__ . '/..' . '/symfony/mailer/Event/MessageEvent.php', + 'Symfony\\Component\\Mailer\\Event\\MessageEvents' => __DIR__ . '/..' . '/symfony/mailer/Event/MessageEvents.php', + 'Symfony\\Component\\Mailer\\Event\\SentMessageEvent' => __DIR__ . '/..' . '/symfony/mailer/Event/SentMessageEvent.php', + 'Symfony\\Component\\Mailer\\Exception\\ExceptionInterface' => __DIR__ . '/..' . '/symfony/mailer/Exception/ExceptionInterface.php', + 'Symfony\\Component\\Mailer\\Exception\\HttpTransportException' => __DIR__ . '/..' . '/symfony/mailer/Exception/HttpTransportException.php', + 'Symfony\\Component\\Mailer\\Exception\\IncompleteDsnException' => __DIR__ . '/..' . '/symfony/mailer/Exception/IncompleteDsnException.php', + 'Symfony\\Component\\Mailer\\Exception\\InvalidArgumentException' => __DIR__ . '/..' . '/symfony/mailer/Exception/InvalidArgumentException.php', + 'Symfony\\Component\\Mailer\\Exception\\LogicException' => __DIR__ . '/..' . '/symfony/mailer/Exception/LogicException.php', + 'Symfony\\Component\\Mailer\\Exception\\RuntimeException' => __DIR__ . '/..' . '/symfony/mailer/Exception/RuntimeException.php', + 'Symfony\\Component\\Mailer\\Exception\\TransportException' => __DIR__ . '/..' . '/symfony/mailer/Exception/TransportException.php', + 'Symfony\\Component\\Mailer\\Exception\\TransportExceptionInterface' => __DIR__ . '/..' . '/symfony/mailer/Exception/TransportExceptionInterface.php', + 'Symfony\\Component\\Mailer\\Exception\\UnexpectedResponseException' => __DIR__ . '/..' . '/symfony/mailer/Exception/UnexpectedResponseException.php', + 'Symfony\\Component\\Mailer\\Exception\\UnsupportedSchemeException' => __DIR__ . '/..' . '/symfony/mailer/Exception/UnsupportedSchemeException.php', + 'Symfony\\Component\\Mailer\\Header\\MetadataHeader' => __DIR__ . '/..' . '/symfony/mailer/Header/MetadataHeader.php', + 'Symfony\\Component\\Mailer\\Header\\TagHeader' => __DIR__ . '/..' . '/symfony/mailer/Header/TagHeader.php', + 'Symfony\\Component\\Mailer\\Mailer' => __DIR__ . '/..' . '/symfony/mailer/Mailer.php', + 'Symfony\\Component\\Mailer\\MailerInterface' => __DIR__ . '/..' . '/symfony/mailer/MailerInterface.php', + 'Symfony\\Component\\Mailer\\Messenger\\MessageHandler' => __DIR__ . '/..' . '/symfony/mailer/Messenger/MessageHandler.php', + 'Symfony\\Component\\Mailer\\Messenger\\SendEmailMessage' => __DIR__ . '/..' . '/symfony/mailer/Messenger/SendEmailMessage.php', + 'Symfony\\Component\\Mailer\\SentMessage' => __DIR__ . '/..' . '/symfony/mailer/SentMessage.php', + 'Symfony\\Component\\Mailer\\Transport' => __DIR__ . '/..' . '/symfony/mailer/Transport.php', + 'Symfony\\Component\\Mailer\\Transport\\AbstractApiTransport' => __DIR__ . '/..' . '/symfony/mailer/Transport/AbstractApiTransport.php', + 'Symfony\\Component\\Mailer\\Transport\\AbstractHttpTransport' => __DIR__ . '/..' . '/symfony/mailer/Transport/AbstractHttpTransport.php', + 'Symfony\\Component\\Mailer\\Transport\\AbstractTransport' => __DIR__ . '/..' . '/symfony/mailer/Transport/AbstractTransport.php', + 'Symfony\\Component\\Mailer\\Transport\\AbstractTransportFactory' => __DIR__ . '/..' . '/symfony/mailer/Transport/AbstractTransportFactory.php', + 'Symfony\\Component\\Mailer\\Transport\\Dsn' => __DIR__ . '/..' . '/symfony/mailer/Transport/Dsn.php', + 'Symfony\\Component\\Mailer\\Transport\\FailoverTransport' => __DIR__ . '/..' . '/symfony/mailer/Transport/FailoverTransport.php', + 'Symfony\\Component\\Mailer\\Transport\\NativeTransportFactory' => __DIR__ . '/..' . '/symfony/mailer/Transport/NativeTransportFactory.php', + 'Symfony\\Component\\Mailer\\Transport\\NullTransport' => __DIR__ . '/..' . '/symfony/mailer/Transport/NullTransport.php', + 'Symfony\\Component\\Mailer\\Transport\\NullTransportFactory' => __DIR__ . '/..' . '/symfony/mailer/Transport/NullTransportFactory.php', + 'Symfony\\Component\\Mailer\\Transport\\RoundRobinTransport' => __DIR__ . '/..' . '/symfony/mailer/Transport/RoundRobinTransport.php', + 'Symfony\\Component\\Mailer\\Transport\\SendmailTransport' => __DIR__ . '/..' . '/symfony/mailer/Transport/SendmailTransport.php', + 'Symfony\\Component\\Mailer\\Transport\\SendmailTransportFactory' => __DIR__ . '/..' . '/symfony/mailer/Transport/SendmailTransportFactory.php', + 'Symfony\\Component\\Mailer\\Transport\\Smtp\\Auth\\AuthenticatorInterface' => __DIR__ . '/..' . '/symfony/mailer/Transport/Smtp/Auth/AuthenticatorInterface.php', + 'Symfony\\Component\\Mailer\\Transport\\Smtp\\Auth\\CramMd5Authenticator' => __DIR__ . '/..' . '/symfony/mailer/Transport/Smtp/Auth/CramMd5Authenticator.php', + 'Symfony\\Component\\Mailer\\Transport\\Smtp\\Auth\\LoginAuthenticator' => __DIR__ . '/..' . '/symfony/mailer/Transport/Smtp/Auth/LoginAuthenticator.php', + 'Symfony\\Component\\Mailer\\Transport\\Smtp\\Auth\\PlainAuthenticator' => __DIR__ . '/..' . '/symfony/mailer/Transport/Smtp/Auth/PlainAuthenticator.php', + 'Symfony\\Component\\Mailer\\Transport\\Smtp\\Auth\\XOAuth2Authenticator' => __DIR__ . '/..' . '/symfony/mailer/Transport/Smtp/Auth/XOAuth2Authenticator.php', + 'Symfony\\Component\\Mailer\\Transport\\Smtp\\EsmtpTransport' => __DIR__ . '/..' . '/symfony/mailer/Transport/Smtp/EsmtpTransport.php', + 'Symfony\\Component\\Mailer\\Transport\\Smtp\\EsmtpTransportFactory' => __DIR__ . '/..' . '/symfony/mailer/Transport/Smtp/EsmtpTransportFactory.php', + 'Symfony\\Component\\Mailer\\Transport\\Smtp\\SmtpTransport' => __DIR__ . '/..' . '/symfony/mailer/Transport/Smtp/SmtpTransport.php', + 'Symfony\\Component\\Mailer\\Transport\\Smtp\\Stream\\AbstractStream' => __DIR__ . '/..' . '/symfony/mailer/Transport/Smtp/Stream/AbstractStream.php', + 'Symfony\\Component\\Mailer\\Transport\\Smtp\\Stream\\ProcessStream' => __DIR__ . '/..' . '/symfony/mailer/Transport/Smtp/Stream/ProcessStream.php', + 'Symfony\\Component\\Mailer\\Transport\\Smtp\\Stream\\SocketStream' => __DIR__ . '/..' . '/symfony/mailer/Transport/Smtp/Stream/SocketStream.php', + 'Symfony\\Component\\Mailer\\Transport\\TransportFactoryInterface' => __DIR__ . '/..' . '/symfony/mailer/Transport/TransportFactoryInterface.php', + 'Symfony\\Component\\Mailer\\Transport\\TransportInterface' => __DIR__ . '/..' . '/symfony/mailer/Transport/TransportInterface.php', + 'Symfony\\Component\\Mailer\\Transport\\Transports' => __DIR__ . '/..' . '/symfony/mailer/Transport/Transports.php', + 'Symfony\\Component\\Mime\\Address' => __DIR__ . '/..' . '/symfony/mime/Address.php', + 'Symfony\\Component\\Mime\\BodyRendererInterface' => __DIR__ . '/..' . '/symfony/mime/BodyRendererInterface.php', + 'Symfony\\Component\\Mime\\CharacterStream' => __DIR__ . '/..' . '/symfony/mime/CharacterStream.php', + 'Symfony\\Component\\Mime\\Crypto\\DkimOptions' => __DIR__ . '/..' . '/symfony/mime/Crypto/DkimOptions.php', + 'Symfony\\Component\\Mime\\Crypto\\DkimSigner' => __DIR__ . '/..' . '/symfony/mime/Crypto/DkimSigner.php', + 'Symfony\\Component\\Mime\\Crypto\\SMime' => __DIR__ . '/..' . '/symfony/mime/Crypto/SMime.php', + 'Symfony\\Component\\Mime\\Crypto\\SMimeEncrypter' => __DIR__ . '/..' . '/symfony/mime/Crypto/SMimeEncrypter.php', + 'Symfony\\Component\\Mime\\Crypto\\SMimeSigner' => __DIR__ . '/..' . '/symfony/mime/Crypto/SMimeSigner.php', + 'Symfony\\Component\\Mime\\DependencyInjection\\AddMimeTypeGuesserPass' => __DIR__ . '/..' . '/symfony/mime/DependencyInjection/AddMimeTypeGuesserPass.php', + 'Symfony\\Component\\Mime\\DraftEmail' => __DIR__ . '/..' . '/symfony/mime/DraftEmail.php', + 'Symfony\\Component\\Mime\\Email' => __DIR__ . '/..' . '/symfony/mime/Email.php', + 'Symfony\\Component\\Mime\\Encoder\\AddressEncoderInterface' => __DIR__ . '/..' . '/symfony/mime/Encoder/AddressEncoderInterface.php', + 'Symfony\\Component\\Mime\\Encoder\\Base64ContentEncoder' => __DIR__ . '/..' . '/symfony/mime/Encoder/Base64ContentEncoder.php', + 'Symfony\\Component\\Mime\\Encoder\\Base64Encoder' => __DIR__ . '/..' . '/symfony/mime/Encoder/Base64Encoder.php', + 'Symfony\\Component\\Mime\\Encoder\\Base64MimeHeaderEncoder' => __DIR__ . '/..' . '/symfony/mime/Encoder/Base64MimeHeaderEncoder.php', + 'Symfony\\Component\\Mime\\Encoder\\ContentEncoderInterface' => __DIR__ . '/..' . '/symfony/mime/Encoder/ContentEncoderInterface.php', + 'Symfony\\Component\\Mime\\Encoder\\EightBitContentEncoder' => __DIR__ . '/..' . '/symfony/mime/Encoder/EightBitContentEncoder.php', + 'Symfony\\Component\\Mime\\Encoder\\EncoderInterface' => __DIR__ . '/..' . '/symfony/mime/Encoder/EncoderInterface.php', + 'Symfony\\Component\\Mime\\Encoder\\IdnAddressEncoder' => __DIR__ . '/..' . '/symfony/mime/Encoder/IdnAddressEncoder.php', + 'Symfony\\Component\\Mime\\Encoder\\MimeHeaderEncoderInterface' => __DIR__ . '/..' . '/symfony/mime/Encoder/MimeHeaderEncoderInterface.php', + 'Symfony\\Component\\Mime\\Encoder\\QpContentEncoder' => __DIR__ . '/..' . '/symfony/mime/Encoder/QpContentEncoder.php', + 'Symfony\\Component\\Mime\\Encoder\\QpEncoder' => __DIR__ . '/..' . '/symfony/mime/Encoder/QpEncoder.php', + 'Symfony\\Component\\Mime\\Encoder\\QpMimeHeaderEncoder' => __DIR__ . '/..' . '/symfony/mime/Encoder/QpMimeHeaderEncoder.php', + 'Symfony\\Component\\Mime\\Encoder\\Rfc2231Encoder' => __DIR__ . '/..' . '/symfony/mime/Encoder/Rfc2231Encoder.php', + 'Symfony\\Component\\Mime\\Exception\\AddressEncoderException' => __DIR__ . '/..' . '/symfony/mime/Exception/AddressEncoderException.php', + 'Symfony\\Component\\Mime\\Exception\\ExceptionInterface' => __DIR__ . '/..' . '/symfony/mime/Exception/ExceptionInterface.php', + 'Symfony\\Component\\Mime\\Exception\\InvalidArgumentException' => __DIR__ . '/..' . '/symfony/mime/Exception/InvalidArgumentException.php', + 'Symfony\\Component\\Mime\\Exception\\LogicException' => __DIR__ . '/..' . '/symfony/mime/Exception/LogicException.php', + 'Symfony\\Component\\Mime\\Exception\\RfcComplianceException' => __DIR__ . '/..' . '/symfony/mime/Exception/RfcComplianceException.php', + 'Symfony\\Component\\Mime\\Exception\\RuntimeException' => __DIR__ . '/..' . '/symfony/mime/Exception/RuntimeException.php', + 'Symfony\\Component\\Mime\\FileBinaryMimeTypeGuesser' => __DIR__ . '/..' . '/symfony/mime/FileBinaryMimeTypeGuesser.php', + 'Symfony\\Component\\Mime\\FileinfoMimeTypeGuesser' => __DIR__ . '/..' . '/symfony/mime/FileinfoMimeTypeGuesser.php', + 'Symfony\\Component\\Mime\\Header\\AbstractHeader' => __DIR__ . '/..' . '/symfony/mime/Header/AbstractHeader.php', + 'Symfony\\Component\\Mime\\Header\\DateHeader' => __DIR__ . '/..' . '/symfony/mime/Header/DateHeader.php', + 'Symfony\\Component\\Mime\\Header\\HeaderInterface' => __DIR__ . '/..' . '/symfony/mime/Header/HeaderInterface.php', + 'Symfony\\Component\\Mime\\Header\\Headers' => __DIR__ . '/..' . '/symfony/mime/Header/Headers.php', + 'Symfony\\Component\\Mime\\Header\\IdentificationHeader' => __DIR__ . '/..' . '/symfony/mime/Header/IdentificationHeader.php', + 'Symfony\\Component\\Mime\\Header\\MailboxHeader' => __DIR__ . '/..' . '/symfony/mime/Header/MailboxHeader.php', + 'Symfony\\Component\\Mime\\Header\\MailboxListHeader' => __DIR__ . '/..' . '/symfony/mime/Header/MailboxListHeader.php', + 'Symfony\\Component\\Mime\\Header\\ParameterizedHeader' => __DIR__ . '/..' . '/symfony/mime/Header/ParameterizedHeader.php', + 'Symfony\\Component\\Mime\\Header\\PathHeader' => __DIR__ . '/..' . '/symfony/mime/Header/PathHeader.php', + 'Symfony\\Component\\Mime\\Header\\UnstructuredHeader' => __DIR__ . '/..' . '/symfony/mime/Header/UnstructuredHeader.php', + 'Symfony\\Component\\Mime\\HtmlToTextConverter\\DefaultHtmlToTextConverter' => __DIR__ . '/..' . '/symfony/mime/HtmlToTextConverter/DefaultHtmlToTextConverter.php', + 'Symfony\\Component\\Mime\\HtmlToTextConverter\\HtmlToTextConverterInterface' => __DIR__ . '/..' . '/symfony/mime/HtmlToTextConverter/HtmlToTextConverterInterface.php', + 'Symfony\\Component\\Mime\\HtmlToTextConverter\\LeagueHtmlToMarkdownConverter' => __DIR__ . '/..' . '/symfony/mime/HtmlToTextConverter/LeagueHtmlToMarkdownConverter.php', + 'Symfony\\Component\\Mime\\Message' => __DIR__ . '/..' . '/symfony/mime/Message.php', + 'Symfony\\Component\\Mime\\MessageConverter' => __DIR__ . '/..' . '/symfony/mime/MessageConverter.php', + 'Symfony\\Component\\Mime\\MimeTypeGuesserInterface' => __DIR__ . '/..' . '/symfony/mime/MimeTypeGuesserInterface.php', + 'Symfony\\Component\\Mime\\MimeTypes' => __DIR__ . '/..' . '/symfony/mime/MimeTypes.php', + 'Symfony\\Component\\Mime\\MimeTypesInterface' => __DIR__ . '/..' . '/symfony/mime/MimeTypesInterface.php', + 'Symfony\\Component\\Mime\\Part\\AbstractMultipartPart' => __DIR__ . '/..' . '/symfony/mime/Part/AbstractMultipartPart.php', + 'Symfony\\Component\\Mime\\Part\\AbstractPart' => __DIR__ . '/..' . '/symfony/mime/Part/AbstractPart.php', + 'Symfony\\Component\\Mime\\Part\\DataPart' => __DIR__ . '/..' . '/symfony/mime/Part/DataPart.php', + 'Symfony\\Component\\Mime\\Part\\File' => __DIR__ . '/..' . '/symfony/mime/Part/File.php', + 'Symfony\\Component\\Mime\\Part\\MessagePart' => __DIR__ . '/..' . '/symfony/mime/Part/MessagePart.php', + 'Symfony\\Component\\Mime\\Part\\Multipart\\AlternativePart' => __DIR__ . '/..' . '/symfony/mime/Part/Multipart/AlternativePart.php', + 'Symfony\\Component\\Mime\\Part\\Multipart\\DigestPart' => __DIR__ . '/..' . '/symfony/mime/Part/Multipart/DigestPart.php', + 'Symfony\\Component\\Mime\\Part\\Multipart\\FormDataPart' => __DIR__ . '/..' . '/symfony/mime/Part/Multipart/FormDataPart.php', + 'Symfony\\Component\\Mime\\Part\\Multipart\\MixedPart' => __DIR__ . '/..' . '/symfony/mime/Part/Multipart/MixedPart.php', + 'Symfony\\Component\\Mime\\Part\\Multipart\\RelatedPart' => __DIR__ . '/..' . '/symfony/mime/Part/Multipart/RelatedPart.php', + 'Symfony\\Component\\Mime\\Part\\SMimePart' => __DIR__ . '/..' . '/symfony/mime/Part/SMimePart.php', + 'Symfony\\Component\\Mime\\Part\\TextPart' => __DIR__ . '/..' . '/symfony/mime/Part/TextPart.php', + 'Symfony\\Component\\Mime\\RawMessage' => __DIR__ . '/..' . '/symfony/mime/RawMessage.php', + 'Symfony\\Component\\Process\\Exception\\ExceptionInterface' => __DIR__ . '/..' . '/symfony/process/Exception/ExceptionInterface.php', + 'Symfony\\Component\\Process\\Exception\\InvalidArgumentException' => __DIR__ . '/..' . '/symfony/process/Exception/InvalidArgumentException.php', + 'Symfony\\Component\\Process\\Exception\\LogicException' => __DIR__ . '/..' . '/symfony/process/Exception/LogicException.php', + 'Symfony\\Component\\Process\\Exception\\ProcessFailedException' => __DIR__ . '/..' . '/symfony/process/Exception/ProcessFailedException.php', + 'Symfony\\Component\\Process\\Exception\\ProcessSignaledException' => __DIR__ . '/..' . '/symfony/process/Exception/ProcessSignaledException.php', + 'Symfony\\Component\\Process\\Exception\\ProcessTimedOutException' => __DIR__ . '/..' . '/symfony/process/Exception/ProcessTimedOutException.php', + 'Symfony\\Component\\Process\\Exception\\RunProcessFailedException' => __DIR__ . '/..' . '/symfony/process/Exception/RunProcessFailedException.php', + 'Symfony\\Component\\Process\\Exception\\RuntimeException' => __DIR__ . '/..' . '/symfony/process/Exception/RuntimeException.php', + 'Symfony\\Component\\Process\\ExecutableFinder' => __DIR__ . '/..' . '/symfony/process/ExecutableFinder.php', + 'Symfony\\Component\\Process\\InputStream' => __DIR__ . '/..' . '/symfony/process/InputStream.php', + 'Symfony\\Component\\Process\\Messenger\\RunProcessContext' => __DIR__ . '/..' . '/symfony/process/Messenger/RunProcessContext.php', + 'Symfony\\Component\\Process\\Messenger\\RunProcessMessage' => __DIR__ . '/..' . '/symfony/process/Messenger/RunProcessMessage.php', + 'Symfony\\Component\\Process\\Messenger\\RunProcessMessageHandler' => __DIR__ . '/..' . '/symfony/process/Messenger/RunProcessMessageHandler.php', + 'Symfony\\Component\\Process\\PhpExecutableFinder' => __DIR__ . '/..' . '/symfony/process/PhpExecutableFinder.php', + 'Symfony\\Component\\Process\\PhpProcess' => __DIR__ . '/..' . '/symfony/process/PhpProcess.php', + 'Symfony\\Component\\Process\\PhpSubprocess' => __DIR__ . '/..' . '/symfony/process/PhpSubprocess.php', + 'Symfony\\Component\\Process\\Pipes\\AbstractPipes' => __DIR__ . '/..' . '/symfony/process/Pipes/AbstractPipes.php', + 'Symfony\\Component\\Process\\Pipes\\PipesInterface' => __DIR__ . '/..' . '/symfony/process/Pipes/PipesInterface.php', + 'Symfony\\Component\\Process\\Pipes\\UnixPipes' => __DIR__ . '/..' . '/symfony/process/Pipes/UnixPipes.php', + 'Symfony\\Component\\Process\\Pipes\\WindowsPipes' => __DIR__ . '/..' . '/symfony/process/Pipes/WindowsPipes.php', + 'Symfony\\Component\\Process\\Process' => __DIR__ . '/..' . '/symfony/process/Process.php', + 'Symfony\\Component\\Process\\ProcessUtils' => __DIR__ . '/..' . '/symfony/process/ProcessUtils.php', + 'Symfony\\Component\\Routing\\Alias' => __DIR__ . '/..' . '/symfony/routing/Alias.php', + 'Symfony\\Component\\Routing\\Annotation\\Route' => __DIR__ . '/..' . '/symfony/routing/Annotation/Route.php', + 'Symfony\\Component\\Routing\\Attribute\\Route' => __DIR__ . '/..' . '/symfony/routing/Attribute/Route.php', + 'Symfony\\Component\\Routing\\CompiledRoute' => __DIR__ . '/..' . '/symfony/routing/CompiledRoute.php', + 'Symfony\\Component\\Routing\\DependencyInjection\\AddExpressionLanguageProvidersPass' => __DIR__ . '/..' . '/symfony/routing/DependencyInjection/AddExpressionLanguageProvidersPass.php', + 'Symfony\\Component\\Routing\\DependencyInjection\\RoutingResolverPass' => __DIR__ . '/..' . '/symfony/routing/DependencyInjection/RoutingResolverPass.php', + 'Symfony\\Component\\Routing\\Exception\\ExceptionInterface' => __DIR__ . '/..' . '/symfony/routing/Exception/ExceptionInterface.php', + 'Symfony\\Component\\Routing\\Exception\\InvalidArgumentException' => __DIR__ . '/..' . '/symfony/routing/Exception/InvalidArgumentException.php', + 'Symfony\\Component\\Routing\\Exception\\InvalidParameterException' => __DIR__ . '/..' . '/symfony/routing/Exception/InvalidParameterException.php', + 'Symfony\\Component\\Routing\\Exception\\MethodNotAllowedException' => __DIR__ . '/..' . '/symfony/routing/Exception/MethodNotAllowedException.php', + 'Symfony\\Component\\Routing\\Exception\\MissingMandatoryParametersException' => __DIR__ . '/..' . '/symfony/routing/Exception/MissingMandatoryParametersException.php', + 'Symfony\\Component\\Routing\\Exception\\NoConfigurationException' => __DIR__ . '/..' . '/symfony/routing/Exception/NoConfigurationException.php', + 'Symfony\\Component\\Routing\\Exception\\ResourceNotFoundException' => __DIR__ . '/..' . '/symfony/routing/Exception/ResourceNotFoundException.php', + 'Symfony\\Component\\Routing\\Exception\\RouteCircularReferenceException' => __DIR__ . '/..' . '/symfony/routing/Exception/RouteCircularReferenceException.php', + 'Symfony\\Component\\Routing\\Exception\\RouteNotFoundException' => __DIR__ . '/..' . '/symfony/routing/Exception/RouteNotFoundException.php', + 'Symfony\\Component\\Routing\\Exception\\RuntimeException' => __DIR__ . '/..' . '/symfony/routing/Exception/RuntimeException.php', + 'Symfony\\Component\\Routing\\Generator\\CompiledUrlGenerator' => __DIR__ . '/..' . '/symfony/routing/Generator/CompiledUrlGenerator.php', + 'Symfony\\Component\\Routing\\Generator\\ConfigurableRequirementsInterface' => __DIR__ . '/..' . '/symfony/routing/Generator/ConfigurableRequirementsInterface.php', + 'Symfony\\Component\\Routing\\Generator\\Dumper\\CompiledUrlGeneratorDumper' => __DIR__ . '/..' . '/symfony/routing/Generator/Dumper/CompiledUrlGeneratorDumper.php', + 'Symfony\\Component\\Routing\\Generator\\Dumper\\GeneratorDumper' => __DIR__ . '/..' . '/symfony/routing/Generator/Dumper/GeneratorDumper.php', + 'Symfony\\Component\\Routing\\Generator\\Dumper\\GeneratorDumperInterface' => __DIR__ . '/..' . '/symfony/routing/Generator/Dumper/GeneratorDumperInterface.php', + 'Symfony\\Component\\Routing\\Generator\\UrlGenerator' => __DIR__ . '/..' . '/symfony/routing/Generator/UrlGenerator.php', + 'Symfony\\Component\\Routing\\Generator\\UrlGeneratorInterface' => __DIR__ . '/..' . '/symfony/routing/Generator/UrlGeneratorInterface.php', + 'Symfony\\Component\\Routing\\Loader\\AnnotationClassLoader' => __DIR__ . '/..' . '/symfony/routing/Loader/AnnotationClassLoader.php', + 'Symfony\\Component\\Routing\\Loader\\AnnotationDirectoryLoader' => __DIR__ . '/..' . '/symfony/routing/Loader/AnnotationDirectoryLoader.php', + 'Symfony\\Component\\Routing\\Loader\\AnnotationFileLoader' => __DIR__ . '/..' . '/symfony/routing/Loader/AnnotationFileLoader.php', + 'Symfony\\Component\\Routing\\Loader\\AttributeClassLoader' => __DIR__ . '/..' . '/symfony/routing/Loader/AttributeClassLoader.php', + 'Symfony\\Component\\Routing\\Loader\\AttributeDirectoryLoader' => __DIR__ . '/..' . '/symfony/routing/Loader/AttributeDirectoryLoader.php', + 'Symfony\\Component\\Routing\\Loader\\AttributeFileLoader' => __DIR__ . '/..' . '/symfony/routing/Loader/AttributeFileLoader.php', + 'Symfony\\Component\\Routing\\Loader\\ClosureLoader' => __DIR__ . '/..' . '/symfony/routing/Loader/ClosureLoader.php', + 'Symfony\\Component\\Routing\\Loader\\Configurator\\AliasConfigurator' => __DIR__ . '/..' . '/symfony/routing/Loader/Configurator/AliasConfigurator.php', + 'Symfony\\Component\\Routing\\Loader\\Configurator\\CollectionConfigurator' => __DIR__ . '/..' . '/symfony/routing/Loader/Configurator/CollectionConfigurator.php', + 'Symfony\\Component\\Routing\\Loader\\Configurator\\ImportConfigurator' => __DIR__ . '/..' . '/symfony/routing/Loader/Configurator/ImportConfigurator.php', + 'Symfony\\Component\\Routing\\Loader\\Configurator\\RouteConfigurator' => __DIR__ . '/..' . '/symfony/routing/Loader/Configurator/RouteConfigurator.php', + 'Symfony\\Component\\Routing\\Loader\\Configurator\\RoutingConfigurator' => __DIR__ . '/..' . '/symfony/routing/Loader/Configurator/RoutingConfigurator.php', + 'Symfony\\Component\\Routing\\Loader\\Configurator\\Traits\\AddTrait' => __DIR__ . '/..' . '/symfony/routing/Loader/Configurator/Traits/AddTrait.php', + 'Symfony\\Component\\Routing\\Loader\\Configurator\\Traits\\HostTrait' => __DIR__ . '/..' . '/symfony/routing/Loader/Configurator/Traits/HostTrait.php', + 'Symfony\\Component\\Routing\\Loader\\Configurator\\Traits\\LocalizedRouteTrait' => __DIR__ . '/..' . '/symfony/routing/Loader/Configurator/Traits/LocalizedRouteTrait.php', + 'Symfony\\Component\\Routing\\Loader\\Configurator\\Traits\\PrefixTrait' => __DIR__ . '/..' . '/symfony/routing/Loader/Configurator/Traits/PrefixTrait.php', + 'Symfony\\Component\\Routing\\Loader\\Configurator\\Traits\\RouteTrait' => __DIR__ . '/..' . '/symfony/routing/Loader/Configurator/Traits/RouteTrait.php', + 'Symfony\\Component\\Routing\\Loader\\ContainerLoader' => __DIR__ . '/..' . '/symfony/routing/Loader/ContainerLoader.php', + 'Symfony\\Component\\Routing\\Loader\\DirectoryLoader' => __DIR__ . '/..' . '/symfony/routing/Loader/DirectoryLoader.php', + 'Symfony\\Component\\Routing\\Loader\\GlobFileLoader' => __DIR__ . '/..' . '/symfony/routing/Loader/GlobFileLoader.php', + 'Symfony\\Component\\Routing\\Loader\\ObjectLoader' => __DIR__ . '/..' . '/symfony/routing/Loader/ObjectLoader.php', + 'Symfony\\Component\\Routing\\Loader\\PhpFileLoader' => __DIR__ . '/..' . '/symfony/routing/Loader/PhpFileLoader.php', + 'Symfony\\Component\\Routing\\Loader\\Psr4DirectoryLoader' => __DIR__ . '/..' . '/symfony/routing/Loader/Psr4DirectoryLoader.php', + 'Symfony\\Component\\Routing\\Loader\\XmlFileLoader' => __DIR__ . '/..' . '/symfony/routing/Loader/XmlFileLoader.php', + 'Symfony\\Component\\Routing\\Loader\\YamlFileLoader' => __DIR__ . '/..' . '/symfony/routing/Loader/YamlFileLoader.php', + 'Symfony\\Component\\Routing\\Matcher\\CompiledUrlMatcher' => __DIR__ . '/..' . '/symfony/routing/Matcher/CompiledUrlMatcher.php', + 'Symfony\\Component\\Routing\\Matcher\\Dumper\\CompiledUrlMatcherDumper' => __DIR__ . '/..' . '/symfony/routing/Matcher/Dumper/CompiledUrlMatcherDumper.php', + 'Symfony\\Component\\Routing\\Matcher\\Dumper\\CompiledUrlMatcherTrait' => __DIR__ . '/..' . '/symfony/routing/Matcher/Dumper/CompiledUrlMatcherTrait.php', + 'Symfony\\Component\\Routing\\Matcher\\Dumper\\MatcherDumper' => __DIR__ . '/..' . '/symfony/routing/Matcher/Dumper/MatcherDumper.php', + 'Symfony\\Component\\Routing\\Matcher\\Dumper\\MatcherDumperInterface' => __DIR__ . '/..' . '/symfony/routing/Matcher/Dumper/MatcherDumperInterface.php', + 'Symfony\\Component\\Routing\\Matcher\\Dumper\\StaticPrefixCollection' => __DIR__ . '/..' . '/symfony/routing/Matcher/Dumper/StaticPrefixCollection.php', + 'Symfony\\Component\\Routing\\Matcher\\ExpressionLanguageProvider' => __DIR__ . '/..' . '/symfony/routing/Matcher/ExpressionLanguageProvider.php', + 'Symfony\\Component\\Routing\\Matcher\\RedirectableUrlMatcher' => __DIR__ . '/..' . '/symfony/routing/Matcher/RedirectableUrlMatcher.php', + 'Symfony\\Component\\Routing\\Matcher\\RedirectableUrlMatcherInterface' => __DIR__ . '/..' . '/symfony/routing/Matcher/RedirectableUrlMatcherInterface.php', + 'Symfony\\Component\\Routing\\Matcher\\RequestMatcherInterface' => __DIR__ . '/..' . '/symfony/routing/Matcher/RequestMatcherInterface.php', + 'Symfony\\Component\\Routing\\Matcher\\TraceableUrlMatcher' => __DIR__ . '/..' . '/symfony/routing/Matcher/TraceableUrlMatcher.php', + 'Symfony\\Component\\Routing\\Matcher\\UrlMatcher' => __DIR__ . '/..' . '/symfony/routing/Matcher/UrlMatcher.php', + 'Symfony\\Component\\Routing\\Matcher\\UrlMatcherInterface' => __DIR__ . '/..' . '/symfony/routing/Matcher/UrlMatcherInterface.php', + 'Symfony\\Component\\Routing\\RequestContext' => __DIR__ . '/..' . '/symfony/routing/RequestContext.php', + 'Symfony\\Component\\Routing\\RequestContextAwareInterface' => __DIR__ . '/..' . '/symfony/routing/RequestContextAwareInterface.php', + 'Symfony\\Component\\Routing\\Requirement\\EnumRequirement' => __DIR__ . '/..' . '/symfony/routing/Requirement/EnumRequirement.php', + 'Symfony\\Component\\Routing\\Requirement\\Requirement' => __DIR__ . '/..' . '/symfony/routing/Requirement/Requirement.php', + 'Symfony\\Component\\Routing\\Route' => __DIR__ . '/..' . '/symfony/routing/Route.php', + 'Symfony\\Component\\Routing\\RouteCollection' => __DIR__ . '/..' . '/symfony/routing/RouteCollection.php', + 'Symfony\\Component\\Routing\\RouteCompiler' => __DIR__ . '/..' . '/symfony/routing/RouteCompiler.php', + 'Symfony\\Component\\Routing\\RouteCompilerInterface' => __DIR__ . '/..' . '/symfony/routing/RouteCompilerInterface.php', + 'Symfony\\Component\\Routing\\Router' => __DIR__ . '/..' . '/symfony/routing/Router.php', + 'Symfony\\Component\\Routing\\RouterInterface' => __DIR__ . '/..' . '/symfony/routing/RouterInterface.php', + 'Symfony\\Component\\String\\AbstractString' => __DIR__ . '/..' . '/symfony/string/AbstractString.php', + 'Symfony\\Component\\String\\AbstractUnicodeString' => __DIR__ . '/..' . '/symfony/string/AbstractUnicodeString.php', + 'Symfony\\Component\\String\\ByteString' => __DIR__ . '/..' . '/symfony/string/ByteString.php', + 'Symfony\\Component\\String\\CodePointString' => __DIR__ . '/..' . '/symfony/string/CodePointString.php', + 'Symfony\\Component\\String\\Exception\\ExceptionInterface' => __DIR__ . '/..' . '/symfony/string/Exception/ExceptionInterface.php', + 'Symfony\\Component\\String\\Exception\\InvalidArgumentException' => __DIR__ . '/..' . '/symfony/string/Exception/InvalidArgumentException.php', + 'Symfony\\Component\\String\\Exception\\RuntimeException' => __DIR__ . '/..' . '/symfony/string/Exception/RuntimeException.php', + 'Symfony\\Component\\String\\Inflector\\EnglishInflector' => __DIR__ . '/..' . '/symfony/string/Inflector/EnglishInflector.php', + 'Symfony\\Component\\String\\Inflector\\FrenchInflector' => __DIR__ . '/..' . '/symfony/string/Inflector/FrenchInflector.php', + 'Symfony\\Component\\String\\Inflector\\InflectorInterface' => __DIR__ . '/..' . '/symfony/string/Inflector/InflectorInterface.php', + 'Symfony\\Component\\String\\LazyString' => __DIR__ . '/..' . '/symfony/string/LazyString.php', + 'Symfony\\Component\\String\\Slugger\\AsciiSlugger' => __DIR__ . '/..' . '/symfony/string/Slugger/AsciiSlugger.php', + 'Symfony\\Component\\String\\Slugger\\SluggerInterface' => __DIR__ . '/..' . '/symfony/string/Slugger/SluggerInterface.php', + 'Symfony\\Component\\String\\UnicodeString' => __DIR__ . '/..' . '/symfony/string/UnicodeString.php', + 'Symfony\\Component\\Translation\\CatalogueMetadataAwareInterface' => __DIR__ . '/..' . '/symfony/translation/CatalogueMetadataAwareInterface.php', + 'Symfony\\Component\\Translation\\Catalogue\\AbstractOperation' => __DIR__ . '/..' . '/symfony/translation/Catalogue/AbstractOperation.php', + 'Symfony\\Component\\Translation\\Catalogue\\MergeOperation' => __DIR__ . '/..' . '/symfony/translation/Catalogue/MergeOperation.php', + 'Symfony\\Component\\Translation\\Catalogue\\OperationInterface' => __DIR__ . '/..' . '/symfony/translation/Catalogue/OperationInterface.php', + 'Symfony\\Component\\Translation\\Catalogue\\TargetOperation' => __DIR__ . '/..' . '/symfony/translation/Catalogue/TargetOperation.php', + 'Symfony\\Component\\Translation\\Command\\TranslationPullCommand' => __DIR__ . '/..' . '/symfony/translation/Command/TranslationPullCommand.php', + 'Symfony\\Component\\Translation\\Command\\TranslationPushCommand' => __DIR__ . '/..' . '/symfony/translation/Command/TranslationPushCommand.php', + 'Symfony\\Component\\Translation\\Command\\TranslationTrait' => __DIR__ . '/..' . '/symfony/translation/Command/TranslationTrait.php', + 'Symfony\\Component\\Translation\\Command\\XliffLintCommand' => __DIR__ . '/..' . '/symfony/translation/Command/XliffLintCommand.php', + 'Symfony\\Component\\Translation\\DataCollectorTranslator' => __DIR__ . '/..' . '/symfony/translation/DataCollectorTranslator.php', + 'Symfony\\Component\\Translation\\DataCollector\\TranslationDataCollector' => __DIR__ . '/..' . '/symfony/translation/DataCollector/TranslationDataCollector.php', + 'Symfony\\Component\\Translation\\DependencyInjection\\DataCollectorTranslatorPass' => __DIR__ . '/..' . '/symfony/translation/DependencyInjection/DataCollectorTranslatorPass.php', + 'Symfony\\Component\\Translation\\DependencyInjection\\LoggingTranslatorPass' => __DIR__ . '/..' . '/symfony/translation/DependencyInjection/LoggingTranslatorPass.php', + 'Symfony\\Component\\Translation\\DependencyInjection\\TranslationDumperPass' => __DIR__ . '/..' . '/symfony/translation/DependencyInjection/TranslationDumperPass.php', + 'Symfony\\Component\\Translation\\DependencyInjection\\TranslationExtractorPass' => __DIR__ . '/..' . '/symfony/translation/DependencyInjection/TranslationExtractorPass.php', + 'Symfony\\Component\\Translation\\DependencyInjection\\TranslatorPass' => __DIR__ . '/..' . '/symfony/translation/DependencyInjection/TranslatorPass.php', + 'Symfony\\Component\\Translation\\DependencyInjection\\TranslatorPathsPass' => __DIR__ . '/..' . '/symfony/translation/DependencyInjection/TranslatorPathsPass.php', + 'Symfony\\Component\\Translation\\Dumper\\CsvFileDumper' => __DIR__ . '/..' . '/symfony/translation/Dumper/CsvFileDumper.php', + 'Symfony\\Component\\Translation\\Dumper\\DumperInterface' => __DIR__ . '/..' . '/symfony/translation/Dumper/DumperInterface.php', + 'Symfony\\Component\\Translation\\Dumper\\FileDumper' => __DIR__ . '/..' . '/symfony/translation/Dumper/FileDumper.php', + 'Symfony\\Component\\Translation\\Dumper\\IcuResFileDumper' => __DIR__ . '/..' . '/symfony/translation/Dumper/IcuResFileDumper.php', + 'Symfony\\Component\\Translation\\Dumper\\IniFileDumper' => __DIR__ . '/..' . '/symfony/translation/Dumper/IniFileDumper.php', + 'Symfony\\Component\\Translation\\Dumper\\JsonFileDumper' => __DIR__ . '/..' . '/symfony/translation/Dumper/JsonFileDumper.php', + 'Symfony\\Component\\Translation\\Dumper\\MoFileDumper' => __DIR__ . '/..' . '/symfony/translation/Dumper/MoFileDumper.php', + 'Symfony\\Component\\Translation\\Dumper\\PhpFileDumper' => __DIR__ . '/..' . '/symfony/translation/Dumper/PhpFileDumper.php', + 'Symfony\\Component\\Translation\\Dumper\\PoFileDumper' => __DIR__ . '/..' . '/symfony/translation/Dumper/PoFileDumper.php', + 'Symfony\\Component\\Translation\\Dumper\\QtFileDumper' => __DIR__ . '/..' . '/symfony/translation/Dumper/QtFileDumper.php', + 'Symfony\\Component\\Translation\\Dumper\\XliffFileDumper' => __DIR__ . '/..' . '/symfony/translation/Dumper/XliffFileDumper.php', + 'Symfony\\Component\\Translation\\Dumper\\YamlFileDumper' => __DIR__ . '/..' . '/symfony/translation/Dumper/YamlFileDumper.php', + 'Symfony\\Component\\Translation\\Exception\\ExceptionInterface' => __DIR__ . '/..' . '/symfony/translation/Exception/ExceptionInterface.php', + 'Symfony\\Component\\Translation\\Exception\\IncompleteDsnException' => __DIR__ . '/..' . '/symfony/translation/Exception/IncompleteDsnException.php', + 'Symfony\\Component\\Translation\\Exception\\InvalidArgumentException' => __DIR__ . '/..' . '/symfony/translation/Exception/InvalidArgumentException.php', + 'Symfony\\Component\\Translation\\Exception\\InvalidResourceException' => __DIR__ . '/..' . '/symfony/translation/Exception/InvalidResourceException.php', + 'Symfony\\Component\\Translation\\Exception\\LogicException' => __DIR__ . '/..' . '/symfony/translation/Exception/LogicException.php', + 'Symfony\\Component\\Translation\\Exception\\MissingRequiredOptionException' => __DIR__ . '/..' . '/symfony/translation/Exception/MissingRequiredOptionException.php', + 'Symfony\\Component\\Translation\\Exception\\NotFoundResourceException' => __DIR__ . '/..' . '/symfony/translation/Exception/NotFoundResourceException.php', + 'Symfony\\Component\\Translation\\Exception\\ProviderException' => __DIR__ . '/..' . '/symfony/translation/Exception/ProviderException.php', + 'Symfony\\Component\\Translation\\Exception\\ProviderExceptionInterface' => __DIR__ . '/..' . '/symfony/translation/Exception/ProviderExceptionInterface.php', + 'Symfony\\Component\\Translation\\Exception\\RuntimeException' => __DIR__ . '/..' . '/symfony/translation/Exception/RuntimeException.php', + 'Symfony\\Component\\Translation\\Exception\\UnsupportedSchemeException' => __DIR__ . '/..' . '/symfony/translation/Exception/UnsupportedSchemeException.php', + 'Symfony\\Component\\Translation\\Extractor\\AbstractFileExtractor' => __DIR__ . '/..' . '/symfony/translation/Extractor/AbstractFileExtractor.php', + 'Symfony\\Component\\Translation\\Extractor\\ChainExtractor' => __DIR__ . '/..' . '/symfony/translation/Extractor/ChainExtractor.php', + 'Symfony\\Component\\Translation\\Extractor\\ExtractorInterface' => __DIR__ . '/..' . '/symfony/translation/Extractor/ExtractorInterface.php', + 'Symfony\\Component\\Translation\\Extractor\\PhpAstExtractor' => __DIR__ . '/..' . '/symfony/translation/Extractor/PhpAstExtractor.php', + 'Symfony\\Component\\Translation\\Extractor\\PhpExtractor' => __DIR__ . '/..' . '/symfony/translation/Extractor/PhpExtractor.php', + 'Symfony\\Component\\Translation\\Extractor\\PhpStringTokenParser' => __DIR__ . '/..' . '/symfony/translation/Extractor/PhpStringTokenParser.php', + 'Symfony\\Component\\Translation\\Extractor\\Visitor\\AbstractVisitor' => __DIR__ . '/..' . '/symfony/translation/Extractor/Visitor/AbstractVisitor.php', + 'Symfony\\Component\\Translation\\Extractor\\Visitor\\ConstraintVisitor' => __DIR__ . '/..' . '/symfony/translation/Extractor/Visitor/ConstraintVisitor.php', + 'Symfony\\Component\\Translation\\Extractor\\Visitor\\TransMethodVisitor' => __DIR__ . '/..' . '/symfony/translation/Extractor/Visitor/TransMethodVisitor.php', + 'Symfony\\Component\\Translation\\Extractor\\Visitor\\TranslatableMessageVisitor' => __DIR__ . '/..' . '/symfony/translation/Extractor/Visitor/TranslatableMessageVisitor.php', + 'Symfony\\Component\\Translation\\Formatter\\IntlFormatter' => __DIR__ . '/..' . '/symfony/translation/Formatter/IntlFormatter.php', + 'Symfony\\Component\\Translation\\Formatter\\IntlFormatterInterface' => __DIR__ . '/..' . '/symfony/translation/Formatter/IntlFormatterInterface.php', + 'Symfony\\Component\\Translation\\Formatter\\MessageFormatter' => __DIR__ . '/..' . '/symfony/translation/Formatter/MessageFormatter.php', + 'Symfony\\Component\\Translation\\Formatter\\MessageFormatterInterface' => __DIR__ . '/..' . '/symfony/translation/Formatter/MessageFormatterInterface.php', + 'Symfony\\Component\\Translation\\IdentityTranslator' => __DIR__ . '/..' . '/symfony/translation/IdentityTranslator.php', + 'Symfony\\Component\\Translation\\Loader\\ArrayLoader' => __DIR__ . '/..' . '/symfony/translation/Loader/ArrayLoader.php', + 'Symfony\\Component\\Translation\\Loader\\CsvFileLoader' => __DIR__ . '/..' . '/symfony/translation/Loader/CsvFileLoader.php', + 'Symfony\\Component\\Translation\\Loader\\FileLoader' => __DIR__ . '/..' . '/symfony/translation/Loader/FileLoader.php', + 'Symfony\\Component\\Translation\\Loader\\IcuDatFileLoader' => __DIR__ . '/..' . '/symfony/translation/Loader/IcuDatFileLoader.php', + 'Symfony\\Component\\Translation\\Loader\\IcuResFileLoader' => __DIR__ . '/..' . '/symfony/translation/Loader/IcuResFileLoader.php', + 'Symfony\\Component\\Translation\\Loader\\IniFileLoader' => __DIR__ . '/..' . '/symfony/translation/Loader/IniFileLoader.php', + 'Symfony\\Component\\Translation\\Loader\\JsonFileLoader' => __DIR__ . '/..' . '/symfony/translation/Loader/JsonFileLoader.php', + 'Symfony\\Component\\Translation\\Loader\\LoaderInterface' => __DIR__ . '/..' . '/symfony/translation/Loader/LoaderInterface.php', + 'Symfony\\Component\\Translation\\Loader\\MoFileLoader' => __DIR__ . '/..' . '/symfony/translation/Loader/MoFileLoader.php', + 'Symfony\\Component\\Translation\\Loader\\PhpFileLoader' => __DIR__ . '/..' . '/symfony/translation/Loader/PhpFileLoader.php', + 'Symfony\\Component\\Translation\\Loader\\PoFileLoader' => __DIR__ . '/..' . '/symfony/translation/Loader/PoFileLoader.php', + 'Symfony\\Component\\Translation\\Loader\\QtFileLoader' => __DIR__ . '/..' . '/symfony/translation/Loader/QtFileLoader.php', + 'Symfony\\Component\\Translation\\Loader\\XliffFileLoader' => __DIR__ . '/..' . '/symfony/translation/Loader/XliffFileLoader.php', + 'Symfony\\Component\\Translation\\Loader\\YamlFileLoader' => __DIR__ . '/..' . '/symfony/translation/Loader/YamlFileLoader.php', + 'Symfony\\Component\\Translation\\LocaleSwitcher' => __DIR__ . '/..' . '/symfony/translation/LocaleSwitcher.php', + 'Symfony\\Component\\Translation\\LoggingTranslator' => __DIR__ . '/..' . '/symfony/translation/LoggingTranslator.php', + 'Symfony\\Component\\Translation\\MessageCatalogue' => __DIR__ . '/..' . '/symfony/translation/MessageCatalogue.php', + 'Symfony\\Component\\Translation\\MessageCatalogueInterface' => __DIR__ . '/..' . '/symfony/translation/MessageCatalogueInterface.php', + 'Symfony\\Component\\Translation\\MetadataAwareInterface' => __DIR__ . '/..' . '/symfony/translation/MetadataAwareInterface.php', + 'Symfony\\Component\\Translation\\Provider\\AbstractProviderFactory' => __DIR__ . '/..' . '/symfony/translation/Provider/AbstractProviderFactory.php', + 'Symfony\\Component\\Translation\\Provider\\Dsn' => __DIR__ . '/..' . '/symfony/translation/Provider/Dsn.php', + 'Symfony\\Component\\Translation\\Provider\\FilteringProvider' => __DIR__ . '/..' . '/symfony/translation/Provider/FilteringProvider.php', + 'Symfony\\Component\\Translation\\Provider\\NullProvider' => __DIR__ . '/..' . '/symfony/translation/Provider/NullProvider.php', + 'Symfony\\Component\\Translation\\Provider\\NullProviderFactory' => __DIR__ . '/..' . '/symfony/translation/Provider/NullProviderFactory.php', + 'Symfony\\Component\\Translation\\Provider\\ProviderFactoryInterface' => __DIR__ . '/..' . '/symfony/translation/Provider/ProviderFactoryInterface.php', + 'Symfony\\Component\\Translation\\Provider\\ProviderInterface' => __DIR__ . '/..' . '/symfony/translation/Provider/ProviderInterface.php', + 'Symfony\\Component\\Translation\\Provider\\TranslationProviderCollection' => __DIR__ . '/..' . '/symfony/translation/Provider/TranslationProviderCollection.php', + 'Symfony\\Component\\Translation\\Provider\\TranslationProviderCollectionFactory' => __DIR__ . '/..' . '/symfony/translation/Provider/TranslationProviderCollectionFactory.php', + 'Symfony\\Component\\Translation\\PseudoLocalizationTranslator' => __DIR__ . '/..' . '/symfony/translation/PseudoLocalizationTranslator.php', + 'Symfony\\Component\\Translation\\Reader\\TranslationReader' => __DIR__ . '/..' . '/symfony/translation/Reader/TranslationReader.php', + 'Symfony\\Component\\Translation\\Reader\\TranslationReaderInterface' => __DIR__ . '/..' . '/symfony/translation/Reader/TranslationReaderInterface.php', + 'Symfony\\Component\\Translation\\TranslatableMessage' => __DIR__ . '/..' . '/symfony/translation/TranslatableMessage.php', + 'Symfony\\Component\\Translation\\Translator' => __DIR__ . '/..' . '/symfony/translation/Translator.php', + 'Symfony\\Component\\Translation\\TranslatorBag' => __DIR__ . '/..' . '/symfony/translation/TranslatorBag.php', + 'Symfony\\Component\\Translation\\TranslatorBagInterface' => __DIR__ . '/..' . '/symfony/translation/TranslatorBagInterface.php', + 'Symfony\\Component\\Translation\\Util\\ArrayConverter' => __DIR__ . '/..' . '/symfony/translation/Util/ArrayConverter.php', + 'Symfony\\Component\\Translation\\Util\\XliffUtils' => __DIR__ . '/..' . '/symfony/translation/Util/XliffUtils.php', + 'Symfony\\Component\\Translation\\Writer\\TranslationWriter' => __DIR__ . '/..' . '/symfony/translation/Writer/TranslationWriter.php', + 'Symfony\\Component\\Translation\\Writer\\TranslationWriterInterface' => __DIR__ . '/..' . '/symfony/translation/Writer/TranslationWriterInterface.php', + 'Symfony\\Component\\Uid\\AbstractUid' => __DIR__ . '/..' . '/symfony/uid/AbstractUid.php', + 'Symfony\\Component\\Uid\\BinaryUtil' => __DIR__ . '/..' . '/symfony/uid/BinaryUtil.php', + 'Symfony\\Component\\Uid\\Command\\GenerateUlidCommand' => __DIR__ . '/..' . '/symfony/uid/Command/GenerateUlidCommand.php', + 'Symfony\\Component\\Uid\\Command\\GenerateUuidCommand' => __DIR__ . '/..' . '/symfony/uid/Command/GenerateUuidCommand.php', + 'Symfony\\Component\\Uid\\Command\\InspectUlidCommand' => __DIR__ . '/..' . '/symfony/uid/Command/InspectUlidCommand.php', + 'Symfony\\Component\\Uid\\Command\\InspectUuidCommand' => __DIR__ . '/..' . '/symfony/uid/Command/InspectUuidCommand.php', + 'Symfony\\Component\\Uid\\Factory\\NameBasedUuidFactory' => __DIR__ . '/..' . '/symfony/uid/Factory/NameBasedUuidFactory.php', + 'Symfony\\Component\\Uid\\Factory\\RandomBasedUuidFactory' => __DIR__ . '/..' . '/symfony/uid/Factory/RandomBasedUuidFactory.php', + 'Symfony\\Component\\Uid\\Factory\\TimeBasedUuidFactory' => __DIR__ . '/..' . '/symfony/uid/Factory/TimeBasedUuidFactory.php', + 'Symfony\\Component\\Uid\\Factory\\UlidFactory' => __DIR__ . '/..' . '/symfony/uid/Factory/UlidFactory.php', + 'Symfony\\Component\\Uid\\Factory\\UuidFactory' => __DIR__ . '/..' . '/symfony/uid/Factory/UuidFactory.php', + 'Symfony\\Component\\Uid\\MaxUlid' => __DIR__ . '/..' . '/symfony/uid/MaxUlid.php', + 'Symfony\\Component\\Uid\\MaxUuid' => __DIR__ . '/..' . '/symfony/uid/MaxUuid.php', + 'Symfony\\Component\\Uid\\NilUlid' => __DIR__ . '/..' . '/symfony/uid/NilUlid.php', + 'Symfony\\Component\\Uid\\NilUuid' => __DIR__ . '/..' . '/symfony/uid/NilUuid.php', + 'Symfony\\Component\\Uid\\TimeBasedUidInterface' => __DIR__ . '/..' . '/symfony/uid/TimeBasedUidInterface.php', + 'Symfony\\Component\\Uid\\Ulid' => __DIR__ . '/..' . '/symfony/uid/Ulid.php', + 'Symfony\\Component\\Uid\\Uuid' => __DIR__ . '/..' . '/symfony/uid/Uuid.php', + 'Symfony\\Component\\Uid\\UuidV1' => __DIR__ . '/..' . '/symfony/uid/UuidV1.php', + 'Symfony\\Component\\Uid\\UuidV3' => __DIR__ . '/..' . '/symfony/uid/UuidV3.php', + 'Symfony\\Component\\Uid\\UuidV4' => __DIR__ . '/..' . '/symfony/uid/UuidV4.php', + 'Symfony\\Component\\Uid\\UuidV5' => __DIR__ . '/..' . '/symfony/uid/UuidV5.php', + 'Symfony\\Component\\Uid\\UuidV6' => __DIR__ . '/..' . '/symfony/uid/UuidV6.php', + 'Symfony\\Component\\Uid\\UuidV7' => __DIR__ . '/..' . '/symfony/uid/UuidV7.php', + 'Symfony\\Component\\Uid\\UuidV8' => __DIR__ . '/..' . '/symfony/uid/UuidV8.php', + 'Symfony\\Contracts\\EventDispatcher\\Event' => __DIR__ . '/..' . '/symfony/event-dispatcher-contracts/Event.php', + 'Symfony\\Contracts\\EventDispatcher\\EventDispatcherInterface' => __DIR__ . '/..' . '/symfony/event-dispatcher-contracts/EventDispatcherInterface.php', + 'Symfony\\Contracts\\Service\\Attribute\\Required' => __DIR__ . '/..' . '/symfony/service-contracts/Attribute/Required.php', + 'Symfony\\Contracts\\Service\\Attribute\\SubscribedService' => __DIR__ . '/..' . '/symfony/service-contracts/Attribute/SubscribedService.php', + 'Symfony\\Contracts\\Service\\ResetInterface' => __DIR__ . '/..' . '/symfony/service-contracts/ResetInterface.php', + 'Symfony\\Contracts\\Service\\ServiceCollectionInterface' => __DIR__ . '/..' . '/symfony/service-contracts/ServiceCollectionInterface.php', + 'Symfony\\Contracts\\Service\\ServiceLocatorTrait' => __DIR__ . '/..' . '/symfony/service-contracts/ServiceLocatorTrait.php', + 'Symfony\\Contracts\\Service\\ServiceMethodsSubscriberTrait' => __DIR__ . '/..' . '/symfony/service-contracts/ServiceMethodsSubscriberTrait.php', + 'Symfony\\Contracts\\Service\\ServiceProviderInterface' => __DIR__ . '/..' . '/symfony/service-contracts/ServiceProviderInterface.php', + 'Symfony\\Contracts\\Service\\ServiceSubscriberInterface' => __DIR__ . '/..' . '/symfony/service-contracts/ServiceSubscriberInterface.php', + 'Symfony\\Contracts\\Service\\ServiceSubscriberTrait' => __DIR__ . '/..' . '/symfony/service-contracts/ServiceSubscriberTrait.php', + 'Symfony\\Contracts\\Translation\\LocaleAwareInterface' => __DIR__ . '/..' . '/symfony/translation-contracts/LocaleAwareInterface.php', + 'Symfony\\Contracts\\Translation\\TranslatableInterface' => __DIR__ . '/..' . '/symfony/translation-contracts/TranslatableInterface.php', + 'Symfony\\Contracts\\Translation\\TranslatorInterface' => __DIR__ . '/..' . '/symfony/translation-contracts/TranslatorInterface.php', + 'Symfony\\Contracts\\Translation\\TranslatorTrait' => __DIR__ . '/..' . '/symfony/translation-contracts/TranslatorTrait.php', + 'Symfony\\Polyfill\\Intl\\Grapheme\\Grapheme' => __DIR__ . '/..' . '/symfony/polyfill-intl-grapheme/Grapheme.php', + 'Symfony\\Polyfill\\Intl\\Idn\\Idn' => __DIR__ . '/..' . '/symfony/polyfill-intl-idn/Idn.php', + 'Symfony\\Polyfill\\Intl\\Idn\\Info' => __DIR__ . '/..' . '/symfony/polyfill-intl-idn/Info.php', + 'Symfony\\Polyfill\\Intl\\Idn\\Resources\\unidata\\DisallowedRanges' => __DIR__ . '/..' . '/symfony/polyfill-intl-idn/Resources/unidata/DisallowedRanges.php', + 'Symfony\\Polyfill\\Intl\\Idn\\Resources\\unidata\\Regex' => __DIR__ . '/..' . '/symfony/polyfill-intl-idn/Resources/unidata/Regex.php', + 'Symfony\\Polyfill\\Intl\\Normalizer\\Normalizer' => __DIR__ . '/..' . '/symfony/polyfill-intl-normalizer/Normalizer.php', + 'Symfony\\Polyfill\\Php82\\NoDynamicProperties' => __DIR__ . '/..' . '/symfony/polyfill-php82/NoDynamicProperties.php', + 'Symfony\\Polyfill\\Php82\\Php82' => __DIR__ . '/..' . '/symfony/polyfill-php82/Php82.php', + 'Symfony\\Polyfill\\Php82\\Random\\Engine\\Secure' => __DIR__ . '/..' . '/symfony/polyfill-php82/Random/Engine/Secure.php', + 'Symfony\\Polyfill\\Php82\\SensitiveParameterValue' => __DIR__ . '/..' . '/symfony/polyfill-php82/SensitiveParameterValue.php', + 'Symfony\\Polyfill\\Php83\\Php83' => __DIR__ . '/..' . '/symfony/polyfill-php83/Php83.php', + 'Symfony\\Polyfill\\Php84\\Php84' => __DIR__ . '/..' . '/symfony/polyfill-php84/Php84.php', + 'Symfony\\Polyfill\\Uuid\\Uuid' => __DIR__ . '/..' . '/symfony/polyfill-uuid/Uuid.php', + 'System' => __DIR__ . '/..' . '/pear/pear-core-minimal/src/System.php', + 'Webauthn\\AttestationStatement\\AndroidKeyAttestationStatementSupport' => __DIR__ . '/..' . '/web-auth/webauthn-lib/src/AttestationStatement/AndroidKeyAttestationStatementSupport.php', + 'Webauthn\\AttestationStatement\\AndroidSafetyNetAttestationStatementSupport' => __DIR__ . '/..' . '/web-auth/webauthn-lib/src/AttestationStatement/AndroidSafetyNetAttestationStatementSupport.php', + 'Webauthn\\AttestationStatement\\AppleAttestationStatementSupport' => __DIR__ . '/..' . '/web-auth/webauthn-lib/src/AttestationStatement/AppleAttestationStatementSupport.php', + 'Webauthn\\AttestationStatement\\AttestationObject' => __DIR__ . '/..' . '/web-auth/webauthn-lib/src/AttestationStatement/AttestationObject.php', + 'Webauthn\\AttestationStatement\\AttestationObjectLoader' => __DIR__ . '/..' . '/web-auth/webauthn-lib/src/AttestationStatement/AttestationObjectLoader.php', + 'Webauthn\\AttestationStatement\\AttestationStatement' => __DIR__ . '/..' . '/web-auth/webauthn-lib/src/AttestationStatement/AttestationStatement.php', + 'Webauthn\\AttestationStatement\\AttestationStatementSupport' => __DIR__ . '/..' . '/web-auth/webauthn-lib/src/AttestationStatement/AttestationStatementSupport.php', + 'Webauthn\\AttestationStatement\\AttestationStatementSupportManager' => __DIR__ . '/..' . '/web-auth/webauthn-lib/src/AttestationStatement/AttestationStatementSupportManager.php', + 'Webauthn\\AttestationStatement\\FidoU2FAttestationStatementSupport' => __DIR__ . '/..' . '/web-auth/webauthn-lib/src/AttestationStatement/FidoU2FAttestationStatementSupport.php', + 'Webauthn\\AttestationStatement\\NoneAttestationStatementSupport' => __DIR__ . '/..' . '/web-auth/webauthn-lib/src/AttestationStatement/NoneAttestationStatementSupport.php', + 'Webauthn\\AttestationStatement\\PackedAttestationStatementSupport' => __DIR__ . '/..' . '/web-auth/webauthn-lib/src/AttestationStatement/PackedAttestationStatementSupport.php', + 'Webauthn\\AttestationStatement\\TPMAttestationStatementSupport' => __DIR__ . '/..' . '/web-auth/webauthn-lib/src/AttestationStatement/TPMAttestationStatementSupport.php', + 'Webauthn\\AttestedCredentialData' => __DIR__ . '/..' . '/web-auth/webauthn-lib/src/AttestedCredentialData.php', + 'Webauthn\\AuthenticationExtensions\\AuthenticationExtension' => __DIR__ . '/..' . '/web-auth/webauthn-lib/src/AuthenticationExtensions/AuthenticationExtension.php', + 'Webauthn\\AuthenticationExtensions\\AuthenticationExtensions' => __DIR__ . '/..' . '/web-auth/webauthn-lib/src/AuthenticationExtensions/AuthenticationExtensions.php', + 'Webauthn\\AuthenticationExtensions\\AuthenticationExtensionsClientInputs' => __DIR__ . '/..' . '/web-auth/webauthn-lib/src/AuthenticationExtensions/AuthenticationExtensionsClientInputs.php', + 'Webauthn\\AuthenticationExtensions\\AuthenticationExtensionsClientOutputs' => __DIR__ . '/..' . '/web-auth/webauthn-lib/src/AuthenticationExtensions/AuthenticationExtensionsClientOutputs.php', + 'Webauthn\\AuthenticationExtensions\\AuthenticationExtensionsClientOutputsLoader' => __DIR__ . '/..' . '/web-auth/webauthn-lib/src/AuthenticationExtensions/AuthenticationExtensionsClientOutputsLoader.php', + 'Webauthn\\AuthenticationExtensions\\ExtensionOutputChecker' => __DIR__ . '/..' . '/web-auth/webauthn-lib/src/AuthenticationExtensions/ExtensionOutputChecker.php', + 'Webauthn\\AuthenticationExtensions\\ExtensionOutputCheckerHandler' => __DIR__ . '/..' . '/web-auth/webauthn-lib/src/AuthenticationExtensions/ExtensionOutputCheckerHandler.php', + 'Webauthn\\AuthenticationExtensions\\ExtensionOutputError' => __DIR__ . '/..' . '/web-auth/webauthn-lib/src/AuthenticationExtensions/ExtensionOutputError.php', + 'Webauthn\\AuthenticatorAssertionResponse' => __DIR__ . '/..' . '/web-auth/webauthn-lib/src/AuthenticatorAssertionResponse.php', + 'Webauthn\\AuthenticatorAssertionResponseValidator' => __DIR__ . '/..' . '/web-auth/webauthn-lib/src/AuthenticatorAssertionResponseValidator.php', + 'Webauthn\\AuthenticatorAttestationResponse' => __DIR__ . '/..' . '/web-auth/webauthn-lib/src/AuthenticatorAttestationResponse.php', + 'Webauthn\\AuthenticatorAttestationResponseValidator' => __DIR__ . '/..' . '/web-auth/webauthn-lib/src/AuthenticatorAttestationResponseValidator.php', + 'Webauthn\\AuthenticatorData' => __DIR__ . '/..' . '/web-auth/webauthn-lib/src/AuthenticatorData.php', + 'Webauthn\\AuthenticatorDataLoader' => __DIR__ . '/..' . '/web-auth/webauthn-lib/src/AuthenticatorDataLoader.php', + 'Webauthn\\AuthenticatorResponse' => __DIR__ . '/..' . '/web-auth/webauthn-lib/src/AuthenticatorResponse.php', + 'Webauthn\\AuthenticatorSelectionCriteria' => __DIR__ . '/..' . '/web-auth/webauthn-lib/src/AuthenticatorSelectionCriteria.php', + 'Webauthn\\CeremonyStep\\CeremonyStep' => __DIR__ . '/..' . '/web-auth/webauthn-lib/src/CeremonyStep/CeremonyStep.php', + 'Webauthn\\CeremonyStep\\CeremonyStepManager' => __DIR__ . '/..' . '/web-auth/webauthn-lib/src/CeremonyStep/CeremonyStepManager.php', + 'Webauthn\\CeremonyStep\\CeremonyStepManagerFactory' => __DIR__ . '/..' . '/web-auth/webauthn-lib/src/CeremonyStep/CeremonyStepManagerFactory.php', + 'Webauthn\\CeremonyStep\\CheckAlgorithm' => __DIR__ . '/..' . '/web-auth/webauthn-lib/src/CeremonyStep/CheckAlgorithm.php', + 'Webauthn\\CeremonyStep\\CheckAllowedCredentialList' => __DIR__ . '/..' . '/web-auth/webauthn-lib/src/CeremonyStep/CheckAllowedCredentialList.php', + 'Webauthn\\CeremonyStep\\CheckAttestationFormatIsKnownAndValid' => __DIR__ . '/..' . '/web-auth/webauthn-lib/src/CeremonyStep/CheckAttestationFormatIsKnownAndValid.php', + 'Webauthn\\CeremonyStep\\CheckBackupBitsAreConsistent' => __DIR__ . '/..' . '/web-auth/webauthn-lib/src/CeremonyStep/CheckBackupBitsAreConsistent.php', + 'Webauthn\\CeremonyStep\\CheckChallenge' => __DIR__ . '/..' . '/web-auth/webauthn-lib/src/CeremonyStep/CheckChallenge.php', + 'Webauthn\\CeremonyStep\\CheckClientDataCollectorType' => __DIR__ . '/..' . '/web-auth/webauthn-lib/src/CeremonyStep/CheckClientDataCollectorType.php', + 'Webauthn\\CeremonyStep\\CheckCounter' => __DIR__ . '/..' . '/web-auth/webauthn-lib/src/CeremonyStep/CheckCounter.php', + 'Webauthn\\CeremonyStep\\CheckCredentialId' => __DIR__ . '/..' . '/web-auth/webauthn-lib/src/CeremonyStep/CheckCredentialId.php', + 'Webauthn\\CeremonyStep\\CheckExtensions' => __DIR__ . '/..' . '/web-auth/webauthn-lib/src/CeremonyStep/CheckExtensions.php', + 'Webauthn\\CeremonyStep\\CheckHasAttestedCredentialData' => __DIR__ . '/..' . '/web-auth/webauthn-lib/src/CeremonyStep/CheckHasAttestedCredentialData.php', + 'Webauthn\\CeremonyStep\\CheckMetadataStatement' => __DIR__ . '/..' . '/web-auth/webauthn-lib/src/CeremonyStep/CheckMetadataStatement.php', + 'Webauthn\\CeremonyStep\\CheckOrigin' => __DIR__ . '/..' . '/web-auth/webauthn-lib/src/CeremonyStep/CheckOrigin.php', + 'Webauthn\\CeremonyStep\\CheckRelyingPartyIdIdHash' => __DIR__ . '/..' . '/web-auth/webauthn-lib/src/CeremonyStep/CheckRelyingPartyIdIdHash.php', + 'Webauthn\\CeremonyStep\\CheckSignature' => __DIR__ . '/..' . '/web-auth/webauthn-lib/src/CeremonyStep/CheckSignature.php', + 'Webauthn\\CeremonyStep\\CheckTopOrigin' => __DIR__ . '/..' . '/web-auth/webauthn-lib/src/CeremonyStep/CheckTopOrigin.php', + 'Webauthn\\CeremonyStep\\CheckUserHandle' => __DIR__ . '/..' . '/web-auth/webauthn-lib/src/CeremonyStep/CheckUserHandle.php', + 'Webauthn\\CeremonyStep\\CheckUserVerification' => __DIR__ . '/..' . '/web-auth/webauthn-lib/src/CeremonyStep/CheckUserVerification.php', + 'Webauthn\\CeremonyStep\\CheckUserWasPresent' => __DIR__ . '/..' . '/web-auth/webauthn-lib/src/CeremonyStep/CheckUserWasPresent.php', + 'Webauthn\\CeremonyStep\\HostTopOriginValidator' => __DIR__ . '/..' . '/web-auth/webauthn-lib/src/CeremonyStep/HostTopOriginValidator.php', + 'Webauthn\\CeremonyStep\\TopOriginValidator' => __DIR__ . '/..' . '/web-auth/webauthn-lib/src/CeremonyStep/TopOriginValidator.php', + 'Webauthn\\CertificateChainChecker\\CertificateChainChecker' => __DIR__ . '/..' . '/web-auth/webauthn-lib/src/CertificateChainChecker/CertificateChainChecker.php', + 'Webauthn\\CertificateChainChecker\\PhpCertificateChainChecker' => __DIR__ . '/..' . '/web-auth/webauthn-lib/src/CertificateChainChecker/PhpCertificateChainChecker.php', + 'Webauthn\\CertificateToolbox' => __DIR__ . '/..' . '/web-auth/webauthn-lib/src/CertificateToolbox.php', + 'Webauthn\\ClientDataCollector\\ClientDataCollector' => __DIR__ . '/..' . '/web-auth/webauthn-lib/src/ClientDataCollector/ClientDataCollector.php', + 'Webauthn\\ClientDataCollector\\ClientDataCollectorManager' => __DIR__ . '/..' . '/web-auth/webauthn-lib/src/ClientDataCollector/ClientDataCollectorManager.php', + 'Webauthn\\ClientDataCollector\\WebauthnAuthenticationCollector' => __DIR__ . '/..' . '/web-auth/webauthn-lib/src/ClientDataCollector/WebauthnAuthenticationCollector.php', + 'Webauthn\\CollectedClientData' => __DIR__ . '/..' . '/web-auth/webauthn-lib/src/CollectedClientData.php', + 'Webauthn\\Counter\\CounterChecker' => __DIR__ . '/..' . '/web-auth/webauthn-lib/src/Counter/CounterChecker.php', + 'Webauthn\\Counter\\ThrowExceptionIfInvalid' => __DIR__ . '/..' . '/web-auth/webauthn-lib/src/Counter/ThrowExceptionIfInvalid.php', + 'Webauthn\\Credential' => __DIR__ . '/..' . '/web-auth/webauthn-lib/src/Credential.php', + 'Webauthn\\Denormalizer\\AttestationObjectDenormalizer' => __DIR__ . '/..' . '/web-auth/webauthn-lib/src/Denormalizer/AttestationObjectDenormalizer.php', + 'Webauthn\\Denormalizer\\AttestationStatementDenormalizer' => __DIR__ . '/..' . '/web-auth/webauthn-lib/src/Denormalizer/AttestationStatementDenormalizer.php', + 'Webauthn\\Denormalizer\\AttestedCredentialDataNormalizer' => __DIR__ . '/..' . '/web-auth/webauthn-lib/src/Denormalizer/AttestedCredentialDataNormalizer.php', + 'Webauthn\\Denormalizer\\AuthenticationExtensionNormalizer' => __DIR__ . '/..' . '/web-auth/webauthn-lib/src/Denormalizer/AuthenticationExtensionNormalizer.php', + 'Webauthn\\Denormalizer\\AuthenticationExtensionsDenormalizer' => __DIR__ . '/..' . '/web-auth/webauthn-lib/src/Denormalizer/AuthenticationExtensionsDenormalizer.php', + 'Webauthn\\Denormalizer\\AuthenticatorAssertionResponseDenormalizer' => __DIR__ . '/..' . '/web-auth/webauthn-lib/src/Denormalizer/AuthenticatorAssertionResponseDenormalizer.php', + 'Webauthn\\Denormalizer\\AuthenticatorAttestationResponseDenormalizer' => __DIR__ . '/..' . '/web-auth/webauthn-lib/src/Denormalizer/AuthenticatorAttestationResponseDenormalizer.php', + 'Webauthn\\Denormalizer\\AuthenticatorDataDenormalizer' => __DIR__ . '/..' . '/web-auth/webauthn-lib/src/Denormalizer/AuthenticatorDataDenormalizer.php', + 'Webauthn\\Denormalizer\\AuthenticatorResponseDenormalizer' => __DIR__ . '/..' . '/web-auth/webauthn-lib/src/Denormalizer/AuthenticatorResponseDenormalizer.php', + 'Webauthn\\Denormalizer\\CollectedClientDataDenormalizer' => __DIR__ . '/..' . '/web-auth/webauthn-lib/src/Denormalizer/CollectedClientDataDenormalizer.php', + 'Webauthn\\Denormalizer\\ExtensionDescriptorDenormalizer' => __DIR__ . '/..' . '/web-auth/webauthn-lib/src/Denormalizer/ExtensionDescriptorDenormalizer.php', + 'Webauthn\\Denormalizer\\PublicKeyCredentialDenormalizer' => __DIR__ . '/..' . '/web-auth/webauthn-lib/src/Denormalizer/PublicKeyCredentialDenormalizer.php', + 'Webauthn\\Denormalizer\\PublicKeyCredentialDescriptorNormalizer' => __DIR__ . '/..' . '/web-auth/webauthn-lib/src/Denormalizer/PublicKeyCredentialDescriptorNormalizer.php', + 'Webauthn\\Denormalizer\\PublicKeyCredentialOptionsDenormalizer' => __DIR__ . '/..' . '/web-auth/webauthn-lib/src/Denormalizer/PublicKeyCredentialOptionsDenormalizer.php', + 'Webauthn\\Denormalizer\\PublicKeyCredentialParametersDenormalizer' => __DIR__ . '/..' . '/web-auth/webauthn-lib/src/Denormalizer/PublicKeyCredentialParametersDenormalizer.php', + 'Webauthn\\Denormalizer\\PublicKeyCredentialSourceDenormalizer' => __DIR__ . '/..' . '/web-auth/webauthn-lib/src/Denormalizer/PublicKeyCredentialSourceDenormalizer.php', + 'Webauthn\\Denormalizer\\PublicKeyCredentialUserEntityDenormalizer' => __DIR__ . '/..' . '/web-auth/webauthn-lib/src/Denormalizer/PublicKeyCredentialUserEntityDenormalizer.php', + 'Webauthn\\Denormalizer\\TrustPathDenormalizer' => __DIR__ . '/..' . '/web-auth/webauthn-lib/src/Denormalizer/TrustPathDenormalizer.php', + 'Webauthn\\Denormalizer\\VerificationMethodANDCombinationsDenormalizer' => __DIR__ . '/..' . '/web-auth/webauthn-lib/src/Denormalizer/VerificationMethodANDCombinationsDenormalizer.php', + 'Webauthn\\Denormalizer\\WebauthnSerializerFactory' => __DIR__ . '/..' . '/web-auth/webauthn-lib/src/Denormalizer/WebauthnSerializerFactory.php', + 'Webauthn\\Event\\AttestationObjectLoaded' => __DIR__ . '/..' . '/web-auth/webauthn-lib/src/Event/AttestationObjectLoaded.php', + 'Webauthn\\Event\\AttestationStatementLoaded' => __DIR__ . '/..' . '/web-auth/webauthn-lib/src/Event/AttestationStatementLoaded.php', + 'Webauthn\\Event\\AuthenticatorAssertionResponseValidationFailedEvent' => __DIR__ . '/..' . '/web-auth/webauthn-lib/src/Event/AuthenticatorAssertionResponseValidationFailedEvent.php', + 'Webauthn\\Event\\AuthenticatorAssertionResponseValidationSucceededEvent' => __DIR__ . '/..' . '/web-auth/webauthn-lib/src/Event/AuthenticatorAssertionResponseValidationSucceededEvent.php', + 'Webauthn\\Event\\AuthenticatorAttestationResponseValidationFailedEvent' => __DIR__ . '/..' . '/web-auth/webauthn-lib/src/Event/AuthenticatorAttestationResponseValidationFailedEvent.php', + 'Webauthn\\Event\\AuthenticatorAttestationResponseValidationSucceededEvent' => __DIR__ . '/..' . '/web-auth/webauthn-lib/src/Event/AuthenticatorAttestationResponseValidationSucceededEvent.php', + 'Webauthn\\Event\\BeforeCertificateChainValidation' => __DIR__ . '/..' . '/web-auth/webauthn-lib/src/Event/BeforeCertificateChainValidation.php', + 'Webauthn\\Event\\CanDispatchEvents' => __DIR__ . '/..' . '/web-auth/webauthn-lib/src/Event/CanDispatchEvents.php', + 'Webauthn\\Event\\CertificateChainValidationFailed' => __DIR__ . '/..' . '/web-auth/webauthn-lib/src/Event/CertificateChainValidationFailed.php', + 'Webauthn\\Event\\CertificateChainValidationSucceeded' => __DIR__ . '/..' . '/web-auth/webauthn-lib/src/Event/CertificateChainValidationSucceeded.php', + 'Webauthn\\Event\\MetadataStatementFound' => __DIR__ . '/..' . '/web-auth/webauthn-lib/src/Event/MetadataStatementFound.php', + 'Webauthn\\Event\\NullEventDispatcher' => __DIR__ . '/..' . '/web-auth/webauthn-lib/src/Event/NullEventDispatcher.php', + 'Webauthn\\Event\\WebauthnEvent' => __DIR__ . '/..' . '/web-auth/webauthn-lib/src/Event/WebauthnEvent.php', + 'Webauthn\\Exception\\AttestationStatementException' => __DIR__ . '/..' . '/web-auth/webauthn-lib/src/Exception/AttestationStatementException.php', + 'Webauthn\\Exception\\AttestationStatementLoadingException' => __DIR__ . '/..' . '/web-auth/webauthn-lib/src/Exception/AttestationStatementLoadingException.php', + 'Webauthn\\Exception\\AttestationStatementVerificationException' => __DIR__ . '/..' . '/web-auth/webauthn-lib/src/Exception/AttestationStatementVerificationException.php', + 'Webauthn\\Exception\\AuthenticationExtensionException' => __DIR__ . '/..' . '/web-auth/webauthn-lib/src/Exception/AuthenticationExtensionException.php', + 'Webauthn\\Exception\\AuthenticatorResponseVerificationException' => __DIR__ . '/..' . '/web-auth/webauthn-lib/src/Exception/AuthenticatorResponseVerificationException.php', + 'Webauthn\\Exception\\CertificateChainException' => __DIR__ . '/..' . '/web-auth/webauthn-lib/src/Exception/CertificateChainException.php', + 'Webauthn\\Exception\\CertificateException' => __DIR__ . '/..' . '/web-auth/webauthn-lib/src/Exception/CertificateException.php', + 'Webauthn\\Exception\\CertificateRevocationListException' => __DIR__ . '/..' . '/web-auth/webauthn-lib/src/Exception/CertificateRevocationListException.php', + 'Webauthn\\Exception\\CounterException' => __DIR__ . '/..' . '/web-auth/webauthn-lib/src/Exception/CounterException.php', + 'Webauthn\\Exception\\ExpiredCertificateException' => __DIR__ . '/..' . '/web-auth/webauthn-lib/src/Exception/ExpiredCertificateException.php', + 'Webauthn\\Exception\\InvalidAttestationStatementException' => __DIR__ . '/..' . '/web-auth/webauthn-lib/src/Exception/InvalidAttestationStatementException.php', + 'Webauthn\\Exception\\InvalidCertificateException' => __DIR__ . '/..' . '/web-auth/webauthn-lib/src/Exception/InvalidCertificateException.php', + 'Webauthn\\Exception\\InvalidDataException' => __DIR__ . '/..' . '/web-auth/webauthn-lib/src/Exception/InvalidDataException.php', + 'Webauthn\\Exception\\InvalidTrustPathException' => __DIR__ . '/..' . '/web-auth/webauthn-lib/src/Exception/InvalidTrustPathException.php', + 'Webauthn\\Exception\\InvalidUserHandleException' => __DIR__ . '/..' . '/web-auth/webauthn-lib/src/Exception/InvalidUserHandleException.php', + 'Webauthn\\Exception\\MetadataServiceException' => __DIR__ . '/..' . '/web-auth/webauthn-lib/src/Exception/MetadataServiceException.php', + 'Webauthn\\Exception\\MetadataStatementException' => __DIR__ . '/..' . '/web-auth/webauthn-lib/src/Exception/MetadataStatementException.php', + 'Webauthn\\Exception\\MetadataStatementLoadingException' => __DIR__ . '/..' . '/web-auth/webauthn-lib/src/Exception/MetadataStatementLoadingException.php', + 'Webauthn\\Exception\\MissingMetadataStatementException' => __DIR__ . '/..' . '/web-auth/webauthn-lib/src/Exception/MissingMetadataStatementException.php', + 'Webauthn\\Exception\\RevokedCertificateException' => __DIR__ . '/..' . '/web-auth/webauthn-lib/src/Exception/RevokedCertificateException.php', + 'Webauthn\\Exception\\UnsupportedFeatureException' => __DIR__ . '/..' . '/web-auth/webauthn-lib/src/Exception/UnsupportedFeatureException.php', + 'Webauthn\\Exception\\WebauthnException' => __DIR__ . '/..' . '/web-auth/webauthn-lib/src/Exception/WebauthnException.php', + 'Webauthn\\FakeCredentialGenerator' => __DIR__ . '/..' . '/web-auth/webauthn-lib/src/FakeCredentialGenerator.php', + 'Webauthn\\MetadataService\\CanLogData' => __DIR__ . '/..' . '/web-auth/webauthn-lib/src/MetadataService/CanLogData.php', + 'Webauthn\\MetadataService\\CertificateChain\\CertificateChainValidator' => __DIR__ . '/..' . '/web-auth/webauthn-lib/src/MetadataService/CertificateChain/CertificateChainValidator.php', + 'Webauthn\\MetadataService\\CertificateChain\\CertificateToolbox' => __DIR__ . '/..' . '/web-auth/webauthn-lib/src/MetadataService/CertificateChain/CertificateToolbox.php', + 'Webauthn\\MetadataService\\CertificateChain\\PhpCertificateChainValidator' => __DIR__ . '/..' . '/web-auth/webauthn-lib/src/MetadataService/CertificateChain/PhpCertificateChainValidator.php', + 'Webauthn\\MetadataService\\Denormalizer\\ExtensionDescriptorDenormalizer' => __DIR__ . '/..' . '/web-auth/webauthn-lib/src/MetadataService/Denormalizer/ExtensionDescriptorDenormalizer.php', + 'Webauthn\\MetadataService\\Denormalizer\\MetadataStatementSerializerFactory' => __DIR__ . '/..' . '/web-auth/webauthn-lib/src/MetadataService/Denormalizer/MetadataStatementSerializerFactory.php', + 'Webauthn\\MetadataService\\Event\\BeforeCertificateChainValidation' => __DIR__ . '/..' . '/web-auth/webauthn-lib/src/MetadataService/Event/BeforeCertificateChainValidation.php', + 'Webauthn\\MetadataService\\Event\\CanDispatchEvents' => __DIR__ . '/..' . '/web-auth/webauthn-lib/src/MetadataService/Event/CanDispatchEvents.php', + 'Webauthn\\MetadataService\\Event\\CertificateChainValidationFailed' => __DIR__ . '/..' . '/web-auth/webauthn-lib/src/MetadataService/Event/CertificateChainValidationFailed.php', + 'Webauthn\\MetadataService\\Event\\CertificateChainValidationSucceeded' => __DIR__ . '/..' . '/web-auth/webauthn-lib/src/MetadataService/Event/CertificateChainValidationSucceeded.php', + 'Webauthn\\MetadataService\\Event\\MetadataStatementFound' => __DIR__ . '/..' . '/web-auth/webauthn-lib/src/MetadataService/Event/MetadataStatementFound.php', + 'Webauthn\\MetadataService\\Event\\NullEventDispatcher' => __DIR__ . '/..' . '/web-auth/webauthn-lib/src/MetadataService/Event/NullEventDispatcher.php', + 'Webauthn\\MetadataService\\Event\\WebauthnEvent' => __DIR__ . '/..' . '/web-auth/webauthn-lib/src/MetadataService/Event/WebauthnEvent.php', + 'Webauthn\\MetadataService\\Exception\\CertificateChainException' => __DIR__ . '/..' . '/web-auth/webauthn-lib/src/MetadataService/Exception/CertificateChainException.php', + 'Webauthn\\MetadataService\\Exception\\CertificateException' => __DIR__ . '/..' . '/web-auth/webauthn-lib/src/MetadataService/Exception/CertificateException.php', + 'Webauthn\\MetadataService\\Exception\\CertificateRevocationListException' => __DIR__ . '/..' . '/web-auth/webauthn-lib/src/MetadataService/Exception/CertificateRevocationListException.php', + 'Webauthn\\MetadataService\\Exception\\ExpiredCertificateException' => __DIR__ . '/..' . '/web-auth/webauthn-lib/src/MetadataService/Exception/ExpiredCertificateException.php', + 'Webauthn\\MetadataService\\Exception\\InvalidCertificateException' => __DIR__ . '/..' . '/web-auth/webauthn-lib/src/MetadataService/Exception/InvalidCertificateException.php', + 'Webauthn\\MetadataService\\Exception\\MetadataServiceException' => __DIR__ . '/..' . '/web-auth/webauthn-lib/src/MetadataService/Exception/MetadataServiceException.php', + 'Webauthn\\MetadataService\\Exception\\MetadataStatementException' => __DIR__ . '/..' . '/web-auth/webauthn-lib/src/MetadataService/Exception/MetadataStatementException.php', + 'Webauthn\\MetadataService\\Exception\\MetadataStatementLoadingException' => __DIR__ . '/..' . '/web-auth/webauthn-lib/src/MetadataService/Exception/MetadataStatementLoadingException.php', + 'Webauthn\\MetadataService\\Exception\\MissingMetadataStatementException' => __DIR__ . '/..' . '/web-auth/webauthn-lib/src/MetadataService/Exception/MissingMetadataStatementException.php', + 'Webauthn\\MetadataService\\Exception\\RevokedCertificateException' => __DIR__ . '/..' . '/web-auth/webauthn-lib/src/MetadataService/Exception/RevokedCertificateException.php', + 'Webauthn\\MetadataService\\MetadataStatementRepository' => __DIR__ . '/..' . '/web-auth/webauthn-lib/src/MetadataService/MetadataStatementRepository.php', + 'Webauthn\\MetadataService\\Psr18HttpClient' => __DIR__ . '/..' . '/web-auth/webauthn-lib/src/MetadataService/Psr18HttpClient.php', + 'Webauthn\\MetadataService\\Service\\ChainedMetadataServices' => __DIR__ . '/..' . '/web-auth/webauthn-lib/src/MetadataService/Service/ChainedMetadataServices.php', + 'Webauthn\\MetadataService\\Service\\DistantResourceMetadataService' => __DIR__ . '/..' . '/web-auth/webauthn-lib/src/MetadataService/Service/DistantResourceMetadataService.php', + 'Webauthn\\MetadataService\\Service\\FidoAllianceCompliantMetadataService' => __DIR__ . '/..' . '/web-auth/webauthn-lib/src/MetadataService/Service/FidoAllianceCompliantMetadataService.php', + 'Webauthn\\MetadataService\\Service\\FolderResourceMetadataService' => __DIR__ . '/..' . '/web-auth/webauthn-lib/src/MetadataService/Service/FolderResourceMetadataService.php', + 'Webauthn\\MetadataService\\Service\\InMemoryMetadataService' => __DIR__ . '/..' . '/web-auth/webauthn-lib/src/MetadataService/Service/InMemoryMetadataService.php', + 'Webauthn\\MetadataService\\Service\\JsonMetadataService' => __DIR__ . '/..' . '/web-auth/webauthn-lib/src/MetadataService/Service/JsonMetadataService.php', + 'Webauthn\\MetadataService\\Service\\LocalResourceMetadataService' => __DIR__ . '/..' . '/web-auth/webauthn-lib/src/MetadataService/Service/LocalResourceMetadataService.php', + 'Webauthn\\MetadataService\\Service\\MetadataBLOBPayload' => __DIR__ . '/..' . '/web-auth/webauthn-lib/src/MetadataService/Service/MetadataBLOBPayload.php', + 'Webauthn\\MetadataService\\Service\\MetadataBLOBPayloadEntry' => __DIR__ . '/..' . '/web-auth/webauthn-lib/src/MetadataService/Service/MetadataBLOBPayloadEntry.php', + 'Webauthn\\MetadataService\\Service\\MetadataService' => __DIR__ . '/..' . '/web-auth/webauthn-lib/src/MetadataService/Service/MetadataService.php', + 'Webauthn\\MetadataService\\Service\\StringMetadataService' => __DIR__ . '/..' . '/web-auth/webauthn-lib/src/MetadataService/Service/StringMetadataService.php', + 'Webauthn\\MetadataService\\Statement\\AbstractDescriptor' => __DIR__ . '/..' . '/web-auth/webauthn-lib/src/MetadataService/Statement/AbstractDescriptor.php', + 'Webauthn\\MetadataService\\Statement\\AlternativeDescriptions' => __DIR__ . '/..' . '/web-auth/webauthn-lib/src/MetadataService/Statement/AlternativeDescriptions.php', + 'Webauthn\\MetadataService\\Statement\\AuthenticatorGetInfo' => __DIR__ . '/..' . '/web-auth/webauthn-lib/src/MetadataService/Statement/AuthenticatorGetInfo.php', + 'Webauthn\\MetadataService\\Statement\\AuthenticatorStatus' => __DIR__ . '/..' . '/web-auth/webauthn-lib/src/MetadataService/Statement/AuthenticatorStatus.php', + 'Webauthn\\MetadataService\\Statement\\BiometricAccuracyDescriptor' => __DIR__ . '/..' . '/web-auth/webauthn-lib/src/MetadataService/Statement/BiometricAccuracyDescriptor.php', + 'Webauthn\\MetadataService\\Statement\\BiometricStatusReport' => __DIR__ . '/..' . '/web-auth/webauthn-lib/src/MetadataService/Statement/BiometricStatusReport.php', + 'Webauthn\\MetadataService\\Statement\\CodeAccuracyDescriptor' => __DIR__ . '/..' . '/web-auth/webauthn-lib/src/MetadataService/Statement/CodeAccuracyDescriptor.php', + 'Webauthn\\MetadataService\\Statement\\DisplayPNGCharacteristicsDescriptor' => __DIR__ . '/..' . '/web-auth/webauthn-lib/src/MetadataService/Statement/DisplayPNGCharacteristicsDescriptor.php', + 'Webauthn\\MetadataService\\Statement\\EcdaaTrustAnchor' => __DIR__ . '/..' . '/web-auth/webauthn-lib/src/MetadataService/Statement/EcdaaTrustAnchor.php', + 'Webauthn\\MetadataService\\Statement\\ExtensionDescriptor' => __DIR__ . '/..' . '/web-auth/webauthn-lib/src/MetadataService/Statement/ExtensionDescriptor.php', + 'Webauthn\\MetadataService\\Statement\\MetadataStatement' => __DIR__ . '/..' . '/web-auth/webauthn-lib/src/MetadataService/Statement/MetadataStatement.php', + 'Webauthn\\MetadataService\\Statement\\PatternAccuracyDescriptor' => __DIR__ . '/..' . '/web-auth/webauthn-lib/src/MetadataService/Statement/PatternAccuracyDescriptor.php', + 'Webauthn\\MetadataService\\Statement\\RgbPaletteEntry' => __DIR__ . '/..' . '/web-auth/webauthn-lib/src/MetadataService/Statement/RgbPaletteEntry.php', + 'Webauthn\\MetadataService\\Statement\\RogueListEntry' => __DIR__ . '/..' . '/web-auth/webauthn-lib/src/MetadataService/Statement/RogueListEntry.php', + 'Webauthn\\MetadataService\\Statement\\StatusReport' => __DIR__ . '/..' . '/web-auth/webauthn-lib/src/MetadataService/Statement/StatusReport.php', + 'Webauthn\\MetadataService\\Statement\\VerificationMethodANDCombinations' => __DIR__ . '/..' . '/web-auth/webauthn-lib/src/MetadataService/Statement/VerificationMethodANDCombinations.php', + 'Webauthn\\MetadataService\\Statement\\VerificationMethodDescriptor' => __DIR__ . '/..' . '/web-auth/webauthn-lib/src/MetadataService/Statement/VerificationMethodDescriptor.php', + 'Webauthn\\MetadataService\\Statement\\Version' => __DIR__ . '/..' . '/web-auth/webauthn-lib/src/MetadataService/Statement/Version.php', + 'Webauthn\\MetadataService\\StatusReportRepository' => __DIR__ . '/..' . '/web-auth/webauthn-lib/src/MetadataService/StatusReportRepository.php', + 'Webauthn\\MetadataService\\ValueFilter' => __DIR__ . '/..' . '/web-auth/webauthn-lib/src/MetadataService/ValueFilter.php', + 'Webauthn\\PublicKeyCredential' => __DIR__ . '/..' . '/web-auth/webauthn-lib/src/PublicKeyCredential.php', + 'Webauthn\\PublicKeyCredentialCreationOptions' => __DIR__ . '/..' . '/web-auth/webauthn-lib/src/PublicKeyCredentialCreationOptions.php', + 'Webauthn\\PublicKeyCredentialDescriptor' => __DIR__ . '/..' . '/web-auth/webauthn-lib/src/PublicKeyCredentialDescriptor.php', + 'Webauthn\\PublicKeyCredentialDescriptorCollection' => __DIR__ . '/..' . '/web-auth/webauthn-lib/src/PublicKeyCredentialDescriptorCollection.php', + 'Webauthn\\PublicKeyCredentialEntity' => __DIR__ . '/..' . '/web-auth/webauthn-lib/src/PublicKeyCredentialEntity.php', + 'Webauthn\\PublicKeyCredentialLoader' => __DIR__ . '/..' . '/web-auth/webauthn-lib/src/PublicKeyCredentialLoader.php', + 'Webauthn\\PublicKeyCredentialOptions' => __DIR__ . '/..' . '/web-auth/webauthn-lib/src/PublicKeyCredentialOptions.php', + 'Webauthn\\PublicKeyCredentialParameters' => __DIR__ . '/..' . '/web-auth/webauthn-lib/src/PublicKeyCredentialParameters.php', + 'Webauthn\\PublicKeyCredentialRequestOptions' => __DIR__ . '/..' . '/web-auth/webauthn-lib/src/PublicKeyCredentialRequestOptions.php', + 'Webauthn\\PublicKeyCredentialRpEntity' => __DIR__ . '/..' . '/web-auth/webauthn-lib/src/PublicKeyCredentialRpEntity.php', + 'Webauthn\\PublicKeyCredentialSource' => __DIR__ . '/..' . '/web-auth/webauthn-lib/src/PublicKeyCredentialSource.php', + 'Webauthn\\PublicKeyCredentialSourceRepository' => __DIR__ . '/..' . '/web-auth/webauthn-lib/src/PublicKeyCredentialSourceRepository.php', + 'Webauthn\\PublicKeyCredentialUserEntity' => __DIR__ . '/..' . '/web-auth/webauthn-lib/src/PublicKeyCredentialUserEntity.php', + 'Webauthn\\SimpleFakeCredentialGenerator' => __DIR__ . '/..' . '/web-auth/webauthn-lib/src/SimpleFakeCredentialGenerator.php', + 'Webauthn\\StringStream' => __DIR__ . '/..' . '/web-auth/webauthn-lib/src/StringStream.php', + 'Webauthn\\TokenBinding\\IgnoreTokenBindingHandler' => __DIR__ . '/..' . '/web-auth/webauthn-lib/src/TokenBinding/IgnoreTokenBindingHandler.php', + 'Webauthn\\TokenBinding\\SecTokenBindingHandler' => __DIR__ . '/..' . '/web-auth/webauthn-lib/src/TokenBinding/SecTokenBindingHandler.php', + 'Webauthn\\TokenBinding\\TokenBinding' => __DIR__ . '/..' . '/web-auth/webauthn-lib/src/TokenBinding/TokenBinding.php', + 'Webauthn\\TokenBinding\\TokenBindingHandler' => __DIR__ . '/..' . '/web-auth/webauthn-lib/src/TokenBinding/TokenBindingHandler.php', + 'Webauthn\\TokenBinding\\TokenBindingNotSupportedHandler' => __DIR__ . '/..' . '/web-auth/webauthn-lib/src/TokenBinding/TokenBindingNotSupportedHandler.php', + 'Webauthn\\TrustPath\\CertificateTrustPath' => __DIR__ . '/..' . '/web-auth/webauthn-lib/src/TrustPath/CertificateTrustPath.php', + 'Webauthn\\TrustPath\\EcdaaKeyIdTrustPath' => __DIR__ . '/..' . '/web-auth/webauthn-lib/src/TrustPath/EcdaaKeyIdTrustPath.php', + 'Webauthn\\TrustPath\\EmptyTrustPath' => __DIR__ . '/..' . '/web-auth/webauthn-lib/src/TrustPath/EmptyTrustPath.php', + 'Webauthn\\TrustPath\\TrustPath' => __DIR__ . '/..' . '/web-auth/webauthn-lib/src/TrustPath/TrustPath.php', + 'Webauthn\\TrustPath\\TrustPathLoader' => __DIR__ . '/..' . '/web-auth/webauthn-lib/src/TrustPath/TrustPathLoader.php', + 'Webauthn\\U2FPublicKey' => __DIR__ . '/..' . '/web-auth/webauthn-lib/src/U2FPublicKey.php', + 'Webauthn\\Util\\Base64' => __DIR__ . '/..' . '/web-auth/webauthn-lib/src/Util/Base64.php', + 'Webauthn\\Util\\CoseSignatureFixer' => __DIR__ . '/..' . '/web-auth/webauthn-lib/src/Util/CoseSignatureFixer.php', + 'ZipStreamer\\COMPR' => __DIR__ . '/..' . '/deepdiver/zipstreamer/src/COMPR.php', + 'ZipStreamer\\Count64' => __DIR__ . '/..' . '/deepdiver/zipstreamer/src/Count64.php', + 'ZipStreamer\\Lib\\Count64Base' => __DIR__ . '/..' . '/deepdiver/zipstreamer/src/Lib/Count64Base.php', + 'ZipStreamer\\Lib\\Count64_32' => __DIR__ . '/..' . '/deepdiver/zipstreamer/src/Lib/Count64_32.php', + 'ZipStreamer\\Lib\\Count64_64' => __DIR__ . '/..' . '/deepdiver/zipstreamer/src/Lib/Count64_64.php', + 'ZipStreamer\\ZipStreamer' => __DIR__ . '/..' . '/deepdiver/zipstreamer/src/ZipStreamer.php', + 'bantu\\IniGetWrapper\\IniGetWrapper' => __DIR__ . '/..' . '/bantu/ini-get-wrapper/src/IniGetWrapper.php', + 'cweagans\\Composer\\PatchEvent' => __DIR__ . '/..' . '/cweagans/composer-patches/src/PatchEvent.php', + 'cweagans\\Composer\\PatchEvents' => __DIR__ . '/..' . '/cweagans/composer-patches/src/PatchEvents.php', + 'cweagans\\Composer\\Patches' => __DIR__ . '/..' . '/cweagans/composer-patches/src/Patches.php', + 'kornrunner\\Blurhash\\AC' => __DIR__ . '/..' . '/kornrunner/blurhash/src/AC.php', + 'kornrunner\\Blurhash\\Base83' => __DIR__ . '/..' . '/kornrunner/blurhash/src/Base83.php', + 'kornrunner\\Blurhash\\Blurhash' => __DIR__ . '/..' . '/kornrunner/blurhash/src/Blurhash.php', + 'kornrunner\\Blurhash\\Color' => __DIR__ . '/..' . '/kornrunner/blurhash/src/Color.php', + 'kornrunner\\Blurhash\\DC' => __DIR__ . '/..' . '/kornrunner/blurhash/src/DC.php', + 'libphonenumber\\CountryCodeSource' => __DIR__ . '/..' . '/giggsey/libphonenumber-for-php-lite/src/CountryCodeSource.php', + 'libphonenumber\\CountryCodeToRegionCodeMap' => __DIR__ . '/..' . '/giggsey/libphonenumber-for-php-lite/src/CountryCodeToRegionCodeMap.php', + 'libphonenumber\\MatchType' => __DIR__ . '/..' . '/giggsey/libphonenumber-for-php-lite/src/MatchType.php', + 'libphonenumber\\Matcher' => __DIR__ . '/..' . '/giggsey/libphonenumber-for-php-lite/src/Matcher.php', + 'libphonenumber\\MatcherAPIInterface' => __DIR__ . '/..' . '/giggsey/libphonenumber-for-php-lite/src/MatcherAPIInterface.php', + 'libphonenumber\\MetadataSourceInterface' => __DIR__ . '/..' . '/giggsey/libphonenumber-for-php-lite/src/MetadataSourceInterface.php', + 'libphonenumber\\MultiFileMetadataSourceImpl' => __DIR__ . '/..' . '/giggsey/libphonenumber-for-php-lite/src/MultiFileMetadataSourceImpl.php', + 'libphonenumber\\NumberFormat' => __DIR__ . '/..' . '/giggsey/libphonenumber-for-php-lite/src/NumberFormat.php', + 'libphonenumber\\NumberParseException' => __DIR__ . '/..' . '/giggsey/libphonenumber-for-php-lite/src/NumberParseException.php', + 'libphonenumber\\PhoneMetadata' => __DIR__ . '/..' . '/giggsey/libphonenumber-for-php-lite/src/PhoneMetadata.php', + 'libphonenumber\\PhoneNumber' => __DIR__ . '/..' . '/giggsey/libphonenumber-for-php-lite/src/PhoneNumber.php', + 'libphonenumber\\PhoneNumberDesc' => __DIR__ . '/..' . '/giggsey/libphonenumber-for-php-lite/src/PhoneNumberDesc.php', + 'libphonenumber\\PhoneNumberFormat' => __DIR__ . '/..' . '/giggsey/libphonenumber-for-php-lite/src/PhoneNumberFormat.php', + 'libphonenumber\\PhoneNumberMatch' => __DIR__ . '/..' . '/giggsey/libphonenumber-for-php-lite/src/PhoneNumberMatch.php', + 'libphonenumber\\PhoneNumberType' => __DIR__ . '/..' . '/giggsey/libphonenumber-for-php-lite/src/PhoneNumberType.php', + 'libphonenumber\\PhoneNumberUtil' => __DIR__ . '/..' . '/giggsey/libphonenumber-for-php-lite/src/PhoneNumberUtil.php', + 'libphonenumber\\RegexBasedMatcher' => __DIR__ . '/..' . '/giggsey/libphonenumber-for-php-lite/src/RegexBasedMatcher.php', + 'libphonenumber\\ShortNumberCost' => __DIR__ . '/..' . '/giggsey/libphonenumber-for-php-lite/src/ShortNumberCost.php', + 'libphonenumber\\ShortNumberInfo' => __DIR__ . '/..' . '/giggsey/libphonenumber-for-php-lite/src/ShortNumberInfo.php', + 'libphonenumber\\ShortNumbersRegionCodeSet' => __DIR__ . '/..' . '/giggsey/libphonenumber-for-php-lite/src/ShortNumbersRegionCodeSet.php', + 'libphonenumber\\ValidationResult' => __DIR__ . '/..' . '/giggsey/libphonenumber-for-php-lite/src/ValidationResult.php', + 'libphonenumber\\data\\PhoneNumberMetadata_800' => __DIR__ . '/..' . '/giggsey/libphonenumber-for-php-lite/src/data/PhoneNumberMetadata_800.php', + 'libphonenumber\\data\\PhoneNumberMetadata_808' => __DIR__ . '/..' . '/giggsey/libphonenumber-for-php-lite/src/data/PhoneNumberMetadata_808.php', + 'libphonenumber\\data\\PhoneNumberMetadata_870' => __DIR__ . '/..' . '/giggsey/libphonenumber-for-php-lite/src/data/PhoneNumberMetadata_870.php', + 'libphonenumber\\data\\PhoneNumberMetadata_878' => __DIR__ . '/..' . '/giggsey/libphonenumber-for-php-lite/src/data/PhoneNumberMetadata_878.php', + 'libphonenumber\\data\\PhoneNumberMetadata_881' => __DIR__ . '/..' . '/giggsey/libphonenumber-for-php-lite/src/data/PhoneNumberMetadata_881.php', + 'libphonenumber\\data\\PhoneNumberMetadata_882' => __DIR__ . '/..' . '/giggsey/libphonenumber-for-php-lite/src/data/PhoneNumberMetadata_882.php', + 'libphonenumber\\data\\PhoneNumberMetadata_883' => __DIR__ . '/..' . '/giggsey/libphonenumber-for-php-lite/src/data/PhoneNumberMetadata_883.php', + 'libphonenumber\\data\\PhoneNumberMetadata_888' => __DIR__ . '/..' . '/giggsey/libphonenumber-for-php-lite/src/data/PhoneNumberMetadata_888.php', + 'libphonenumber\\data\\PhoneNumberMetadata_979' => __DIR__ . '/..' . '/giggsey/libphonenumber-for-php-lite/src/data/PhoneNumberMetadata_979.php', + 'libphonenumber\\data\\PhoneNumberMetadata_AC' => __DIR__ . '/..' . '/giggsey/libphonenumber-for-php-lite/src/data/PhoneNumberMetadata_AC.php', + 'libphonenumber\\data\\PhoneNumberMetadata_AD' => __DIR__ . '/..' . '/giggsey/libphonenumber-for-php-lite/src/data/PhoneNumberMetadata_AD.php', + 'libphonenumber\\data\\PhoneNumberMetadata_AE' => __DIR__ . '/..' . '/giggsey/libphonenumber-for-php-lite/src/data/PhoneNumberMetadata_AE.php', + 'libphonenumber\\data\\PhoneNumberMetadata_AF' => __DIR__ . '/..' . '/giggsey/libphonenumber-for-php-lite/src/data/PhoneNumberMetadata_AF.php', + 'libphonenumber\\data\\PhoneNumberMetadata_AG' => __DIR__ . '/..' . '/giggsey/libphonenumber-for-php-lite/src/data/PhoneNumberMetadata_AG.php', + 'libphonenumber\\data\\PhoneNumberMetadata_AI' => __DIR__ . '/..' . '/giggsey/libphonenumber-for-php-lite/src/data/PhoneNumberMetadata_AI.php', + 'libphonenumber\\data\\PhoneNumberMetadata_AL' => __DIR__ . '/..' . '/giggsey/libphonenumber-for-php-lite/src/data/PhoneNumberMetadata_AL.php', + 'libphonenumber\\data\\PhoneNumberMetadata_AM' => __DIR__ . '/..' . '/giggsey/libphonenumber-for-php-lite/src/data/PhoneNumberMetadata_AM.php', + 'libphonenumber\\data\\PhoneNumberMetadata_AO' => __DIR__ . '/..' . '/giggsey/libphonenumber-for-php-lite/src/data/PhoneNumberMetadata_AO.php', + 'libphonenumber\\data\\PhoneNumberMetadata_AR' => __DIR__ . '/..' . '/giggsey/libphonenumber-for-php-lite/src/data/PhoneNumberMetadata_AR.php', + 'libphonenumber\\data\\PhoneNumberMetadata_AS' => __DIR__ . '/..' . '/giggsey/libphonenumber-for-php-lite/src/data/PhoneNumberMetadata_AS.php', + 'libphonenumber\\data\\PhoneNumberMetadata_AT' => __DIR__ . '/..' . '/giggsey/libphonenumber-for-php-lite/src/data/PhoneNumberMetadata_AT.php', + 'libphonenumber\\data\\PhoneNumberMetadata_AU' => __DIR__ . '/..' . '/giggsey/libphonenumber-for-php-lite/src/data/PhoneNumberMetadata_AU.php', + 'libphonenumber\\data\\PhoneNumberMetadata_AW' => __DIR__ . '/..' . '/giggsey/libphonenumber-for-php-lite/src/data/PhoneNumberMetadata_AW.php', + 'libphonenumber\\data\\PhoneNumberMetadata_AX' => __DIR__ . '/..' . '/giggsey/libphonenumber-for-php-lite/src/data/PhoneNumberMetadata_AX.php', + 'libphonenumber\\data\\PhoneNumberMetadata_AZ' => __DIR__ . '/..' . '/giggsey/libphonenumber-for-php-lite/src/data/PhoneNumberMetadata_AZ.php', + 'libphonenumber\\data\\PhoneNumberMetadata_BA' => __DIR__ . '/..' . '/giggsey/libphonenumber-for-php-lite/src/data/PhoneNumberMetadata_BA.php', + 'libphonenumber\\data\\PhoneNumberMetadata_BB' => __DIR__ . '/..' . '/giggsey/libphonenumber-for-php-lite/src/data/PhoneNumberMetadata_BB.php', + 'libphonenumber\\data\\PhoneNumberMetadata_BD' => __DIR__ . '/..' . '/giggsey/libphonenumber-for-php-lite/src/data/PhoneNumberMetadata_BD.php', + 'libphonenumber\\data\\PhoneNumberMetadata_BE' => __DIR__ . '/..' . '/giggsey/libphonenumber-for-php-lite/src/data/PhoneNumberMetadata_BE.php', + 'libphonenumber\\data\\PhoneNumberMetadata_BF' => __DIR__ . '/..' . '/giggsey/libphonenumber-for-php-lite/src/data/PhoneNumberMetadata_BF.php', + 'libphonenumber\\data\\PhoneNumberMetadata_BG' => __DIR__ . '/..' . '/giggsey/libphonenumber-for-php-lite/src/data/PhoneNumberMetadata_BG.php', + 'libphonenumber\\data\\PhoneNumberMetadata_BH' => __DIR__ . '/..' . '/giggsey/libphonenumber-for-php-lite/src/data/PhoneNumberMetadata_BH.php', + 'libphonenumber\\data\\PhoneNumberMetadata_BI' => __DIR__ . '/..' . '/giggsey/libphonenumber-for-php-lite/src/data/PhoneNumberMetadata_BI.php', + 'libphonenumber\\data\\PhoneNumberMetadata_BJ' => __DIR__ . '/..' . '/giggsey/libphonenumber-for-php-lite/src/data/PhoneNumberMetadata_BJ.php', + 'libphonenumber\\data\\PhoneNumberMetadata_BL' => __DIR__ . '/..' . '/giggsey/libphonenumber-for-php-lite/src/data/PhoneNumberMetadata_BL.php', + 'libphonenumber\\data\\PhoneNumberMetadata_BM' => __DIR__ . '/..' . '/giggsey/libphonenumber-for-php-lite/src/data/PhoneNumberMetadata_BM.php', + 'libphonenumber\\data\\PhoneNumberMetadata_BN' => __DIR__ . '/..' . '/giggsey/libphonenumber-for-php-lite/src/data/PhoneNumberMetadata_BN.php', + 'libphonenumber\\data\\PhoneNumberMetadata_BO' => __DIR__ . '/..' . '/giggsey/libphonenumber-for-php-lite/src/data/PhoneNumberMetadata_BO.php', + 'libphonenumber\\data\\PhoneNumberMetadata_BQ' => __DIR__ . '/..' . '/giggsey/libphonenumber-for-php-lite/src/data/PhoneNumberMetadata_BQ.php', + 'libphonenumber\\data\\PhoneNumberMetadata_BR' => __DIR__ . '/..' . '/giggsey/libphonenumber-for-php-lite/src/data/PhoneNumberMetadata_BR.php', + 'libphonenumber\\data\\PhoneNumberMetadata_BS' => __DIR__ . '/..' . '/giggsey/libphonenumber-for-php-lite/src/data/PhoneNumberMetadata_BS.php', + 'libphonenumber\\data\\PhoneNumberMetadata_BT' => __DIR__ . '/..' . '/giggsey/libphonenumber-for-php-lite/src/data/PhoneNumberMetadata_BT.php', + 'libphonenumber\\data\\PhoneNumberMetadata_BW' => __DIR__ . '/..' . '/giggsey/libphonenumber-for-php-lite/src/data/PhoneNumberMetadata_BW.php', + 'libphonenumber\\data\\PhoneNumberMetadata_BY' => __DIR__ . '/..' . '/giggsey/libphonenumber-for-php-lite/src/data/PhoneNumberMetadata_BY.php', + 'libphonenumber\\data\\PhoneNumberMetadata_BZ' => __DIR__ . '/..' . '/giggsey/libphonenumber-for-php-lite/src/data/PhoneNumberMetadata_BZ.php', + 'libphonenumber\\data\\PhoneNumberMetadata_CA' => __DIR__ . '/..' . '/giggsey/libphonenumber-for-php-lite/src/data/PhoneNumberMetadata_CA.php', + 'libphonenumber\\data\\PhoneNumberMetadata_CC' => __DIR__ . '/..' . '/giggsey/libphonenumber-for-php-lite/src/data/PhoneNumberMetadata_CC.php', + 'libphonenumber\\data\\PhoneNumberMetadata_CD' => __DIR__ . '/..' . '/giggsey/libphonenumber-for-php-lite/src/data/PhoneNumberMetadata_CD.php', + 'libphonenumber\\data\\PhoneNumberMetadata_CF' => __DIR__ . '/..' . '/giggsey/libphonenumber-for-php-lite/src/data/PhoneNumberMetadata_CF.php', + 'libphonenumber\\data\\PhoneNumberMetadata_CG' => __DIR__ . '/..' . '/giggsey/libphonenumber-for-php-lite/src/data/PhoneNumberMetadata_CG.php', + 'libphonenumber\\data\\PhoneNumberMetadata_CH' => __DIR__ . '/..' . '/giggsey/libphonenumber-for-php-lite/src/data/PhoneNumberMetadata_CH.php', + 'libphonenumber\\data\\PhoneNumberMetadata_CI' => __DIR__ . '/..' . '/giggsey/libphonenumber-for-php-lite/src/data/PhoneNumberMetadata_CI.php', + 'libphonenumber\\data\\PhoneNumberMetadata_CK' => __DIR__ . '/..' . '/giggsey/libphonenumber-for-php-lite/src/data/PhoneNumberMetadata_CK.php', + 'libphonenumber\\data\\PhoneNumberMetadata_CL' => __DIR__ . '/..' . '/giggsey/libphonenumber-for-php-lite/src/data/PhoneNumberMetadata_CL.php', + 'libphonenumber\\data\\PhoneNumberMetadata_CM' => __DIR__ . '/..' . '/giggsey/libphonenumber-for-php-lite/src/data/PhoneNumberMetadata_CM.php', + 'libphonenumber\\data\\PhoneNumberMetadata_CN' => __DIR__ . '/..' . '/giggsey/libphonenumber-for-php-lite/src/data/PhoneNumberMetadata_CN.php', + 'libphonenumber\\data\\PhoneNumberMetadata_CO' => __DIR__ . '/..' . '/giggsey/libphonenumber-for-php-lite/src/data/PhoneNumberMetadata_CO.php', + 'libphonenumber\\data\\PhoneNumberMetadata_CR' => __DIR__ . '/..' . '/giggsey/libphonenumber-for-php-lite/src/data/PhoneNumberMetadata_CR.php', + 'libphonenumber\\data\\PhoneNumberMetadata_CU' => __DIR__ . '/..' . '/giggsey/libphonenumber-for-php-lite/src/data/PhoneNumberMetadata_CU.php', + 'libphonenumber\\data\\PhoneNumberMetadata_CV' => __DIR__ . '/..' . '/giggsey/libphonenumber-for-php-lite/src/data/PhoneNumberMetadata_CV.php', + 'libphonenumber\\data\\PhoneNumberMetadata_CW' => __DIR__ . '/..' . '/giggsey/libphonenumber-for-php-lite/src/data/PhoneNumberMetadata_CW.php', + 'libphonenumber\\data\\PhoneNumberMetadata_CX' => __DIR__ . '/..' . '/giggsey/libphonenumber-for-php-lite/src/data/PhoneNumberMetadata_CX.php', + 'libphonenumber\\data\\PhoneNumberMetadata_CY' => __DIR__ . '/..' . '/giggsey/libphonenumber-for-php-lite/src/data/PhoneNumberMetadata_CY.php', + 'libphonenumber\\data\\PhoneNumberMetadata_CZ' => __DIR__ . '/..' . '/giggsey/libphonenumber-for-php-lite/src/data/PhoneNumberMetadata_CZ.php', + 'libphonenumber\\data\\PhoneNumberMetadata_DE' => __DIR__ . '/..' . '/giggsey/libphonenumber-for-php-lite/src/data/PhoneNumberMetadata_DE.php', + 'libphonenumber\\data\\PhoneNumberMetadata_DJ' => __DIR__ . '/..' . '/giggsey/libphonenumber-for-php-lite/src/data/PhoneNumberMetadata_DJ.php', + 'libphonenumber\\data\\PhoneNumberMetadata_DK' => __DIR__ . '/..' . '/giggsey/libphonenumber-for-php-lite/src/data/PhoneNumberMetadata_DK.php', + 'libphonenumber\\data\\PhoneNumberMetadata_DM' => __DIR__ . '/..' . '/giggsey/libphonenumber-for-php-lite/src/data/PhoneNumberMetadata_DM.php', + 'libphonenumber\\data\\PhoneNumberMetadata_DO' => __DIR__ . '/..' . '/giggsey/libphonenumber-for-php-lite/src/data/PhoneNumberMetadata_DO.php', + 'libphonenumber\\data\\PhoneNumberMetadata_DZ' => __DIR__ . '/..' . '/giggsey/libphonenumber-for-php-lite/src/data/PhoneNumberMetadata_DZ.php', + 'libphonenumber\\data\\PhoneNumberMetadata_EC' => __DIR__ . '/..' . '/giggsey/libphonenumber-for-php-lite/src/data/PhoneNumberMetadata_EC.php', + 'libphonenumber\\data\\PhoneNumberMetadata_EE' => __DIR__ . '/..' . '/giggsey/libphonenumber-for-php-lite/src/data/PhoneNumberMetadata_EE.php', + 'libphonenumber\\data\\PhoneNumberMetadata_EG' => __DIR__ . '/..' . '/giggsey/libphonenumber-for-php-lite/src/data/PhoneNumberMetadata_EG.php', + 'libphonenumber\\data\\PhoneNumberMetadata_EH' => __DIR__ . '/..' . '/giggsey/libphonenumber-for-php-lite/src/data/PhoneNumberMetadata_EH.php', + 'libphonenumber\\data\\PhoneNumberMetadata_ER' => __DIR__ . '/..' . '/giggsey/libphonenumber-for-php-lite/src/data/PhoneNumberMetadata_ER.php', + 'libphonenumber\\data\\PhoneNumberMetadata_ES' => __DIR__ . '/..' . '/giggsey/libphonenumber-for-php-lite/src/data/PhoneNumberMetadata_ES.php', + 'libphonenumber\\data\\PhoneNumberMetadata_ET' => __DIR__ . '/..' . '/giggsey/libphonenumber-for-php-lite/src/data/PhoneNumberMetadata_ET.php', + 'libphonenumber\\data\\PhoneNumberMetadata_FI' => __DIR__ . '/..' . '/giggsey/libphonenumber-for-php-lite/src/data/PhoneNumberMetadata_FI.php', + 'libphonenumber\\data\\PhoneNumberMetadata_FJ' => __DIR__ . '/..' . '/giggsey/libphonenumber-for-php-lite/src/data/PhoneNumberMetadata_FJ.php', + 'libphonenumber\\data\\PhoneNumberMetadata_FK' => __DIR__ . '/..' . '/giggsey/libphonenumber-for-php-lite/src/data/PhoneNumberMetadata_FK.php', + 'libphonenumber\\data\\PhoneNumberMetadata_FM' => __DIR__ . '/..' . '/giggsey/libphonenumber-for-php-lite/src/data/PhoneNumberMetadata_FM.php', + 'libphonenumber\\data\\PhoneNumberMetadata_FO' => __DIR__ . '/..' . '/giggsey/libphonenumber-for-php-lite/src/data/PhoneNumberMetadata_FO.php', + 'libphonenumber\\data\\PhoneNumberMetadata_FR' => __DIR__ . '/..' . '/giggsey/libphonenumber-for-php-lite/src/data/PhoneNumberMetadata_FR.php', + 'libphonenumber\\data\\PhoneNumberMetadata_GA' => __DIR__ . '/..' . '/giggsey/libphonenumber-for-php-lite/src/data/PhoneNumberMetadata_GA.php', + 'libphonenumber\\data\\PhoneNumberMetadata_GB' => __DIR__ . '/..' . '/giggsey/libphonenumber-for-php-lite/src/data/PhoneNumberMetadata_GB.php', + 'libphonenumber\\data\\PhoneNumberMetadata_GD' => __DIR__ . '/..' . '/giggsey/libphonenumber-for-php-lite/src/data/PhoneNumberMetadata_GD.php', + 'libphonenumber\\data\\PhoneNumberMetadata_GE' => __DIR__ . '/..' . '/giggsey/libphonenumber-for-php-lite/src/data/PhoneNumberMetadata_GE.php', + 'libphonenumber\\data\\PhoneNumberMetadata_GF' => __DIR__ . '/..' . '/giggsey/libphonenumber-for-php-lite/src/data/PhoneNumberMetadata_GF.php', + 'libphonenumber\\data\\PhoneNumberMetadata_GG' => __DIR__ . '/..' . '/giggsey/libphonenumber-for-php-lite/src/data/PhoneNumberMetadata_GG.php', + 'libphonenumber\\data\\PhoneNumberMetadata_GH' => __DIR__ . '/..' . '/giggsey/libphonenumber-for-php-lite/src/data/PhoneNumberMetadata_GH.php', + 'libphonenumber\\data\\PhoneNumberMetadata_GI' => __DIR__ . '/..' . '/giggsey/libphonenumber-for-php-lite/src/data/PhoneNumberMetadata_GI.php', + 'libphonenumber\\data\\PhoneNumberMetadata_GL' => __DIR__ . '/..' . '/giggsey/libphonenumber-for-php-lite/src/data/PhoneNumberMetadata_GL.php', + 'libphonenumber\\data\\PhoneNumberMetadata_GM' => __DIR__ . '/..' . '/giggsey/libphonenumber-for-php-lite/src/data/PhoneNumberMetadata_GM.php', + 'libphonenumber\\data\\PhoneNumberMetadata_GN' => __DIR__ . '/..' . '/giggsey/libphonenumber-for-php-lite/src/data/PhoneNumberMetadata_GN.php', + 'libphonenumber\\data\\PhoneNumberMetadata_GP' => __DIR__ . '/..' . '/giggsey/libphonenumber-for-php-lite/src/data/PhoneNumberMetadata_GP.php', + 'libphonenumber\\data\\PhoneNumberMetadata_GQ' => __DIR__ . '/..' . '/giggsey/libphonenumber-for-php-lite/src/data/PhoneNumberMetadata_GQ.php', + 'libphonenumber\\data\\PhoneNumberMetadata_GR' => __DIR__ . '/..' . '/giggsey/libphonenumber-for-php-lite/src/data/PhoneNumberMetadata_GR.php', + 'libphonenumber\\data\\PhoneNumberMetadata_GT' => __DIR__ . '/..' . '/giggsey/libphonenumber-for-php-lite/src/data/PhoneNumberMetadata_GT.php', + 'libphonenumber\\data\\PhoneNumberMetadata_GU' => __DIR__ . '/..' . '/giggsey/libphonenumber-for-php-lite/src/data/PhoneNumberMetadata_GU.php', + 'libphonenumber\\data\\PhoneNumberMetadata_GW' => __DIR__ . '/..' . '/giggsey/libphonenumber-for-php-lite/src/data/PhoneNumberMetadata_GW.php', + 'libphonenumber\\data\\PhoneNumberMetadata_GY' => __DIR__ . '/..' . '/giggsey/libphonenumber-for-php-lite/src/data/PhoneNumberMetadata_GY.php', + 'libphonenumber\\data\\PhoneNumberMetadata_HK' => __DIR__ . '/..' . '/giggsey/libphonenumber-for-php-lite/src/data/PhoneNumberMetadata_HK.php', + 'libphonenumber\\data\\PhoneNumberMetadata_HN' => __DIR__ . '/..' . '/giggsey/libphonenumber-for-php-lite/src/data/PhoneNumberMetadata_HN.php', + 'libphonenumber\\data\\PhoneNumberMetadata_HR' => __DIR__ . '/..' . '/giggsey/libphonenumber-for-php-lite/src/data/PhoneNumberMetadata_HR.php', + 'libphonenumber\\data\\PhoneNumberMetadata_HT' => __DIR__ . '/..' . '/giggsey/libphonenumber-for-php-lite/src/data/PhoneNumberMetadata_HT.php', + 'libphonenumber\\data\\PhoneNumberMetadata_HU' => __DIR__ . '/..' . '/giggsey/libphonenumber-for-php-lite/src/data/PhoneNumberMetadata_HU.php', + 'libphonenumber\\data\\PhoneNumberMetadata_ID' => __DIR__ . '/..' . '/giggsey/libphonenumber-for-php-lite/src/data/PhoneNumberMetadata_ID.php', + 'libphonenumber\\data\\PhoneNumberMetadata_IE' => __DIR__ . '/..' . '/giggsey/libphonenumber-for-php-lite/src/data/PhoneNumberMetadata_IE.php', + 'libphonenumber\\data\\PhoneNumberMetadata_IL' => __DIR__ . '/..' . '/giggsey/libphonenumber-for-php-lite/src/data/PhoneNumberMetadata_IL.php', + 'libphonenumber\\data\\PhoneNumberMetadata_IM' => __DIR__ . '/..' . '/giggsey/libphonenumber-for-php-lite/src/data/PhoneNumberMetadata_IM.php', + 'libphonenumber\\data\\PhoneNumberMetadata_IN' => __DIR__ . '/..' . '/giggsey/libphonenumber-for-php-lite/src/data/PhoneNumberMetadata_IN.php', + 'libphonenumber\\data\\PhoneNumberMetadata_IO' => __DIR__ . '/..' . '/giggsey/libphonenumber-for-php-lite/src/data/PhoneNumberMetadata_IO.php', + 'libphonenumber\\data\\PhoneNumberMetadata_IQ' => __DIR__ . '/..' . '/giggsey/libphonenumber-for-php-lite/src/data/PhoneNumberMetadata_IQ.php', + 'libphonenumber\\data\\PhoneNumberMetadata_IR' => __DIR__ . '/..' . '/giggsey/libphonenumber-for-php-lite/src/data/PhoneNumberMetadata_IR.php', + 'libphonenumber\\data\\PhoneNumberMetadata_IS' => __DIR__ . '/..' . '/giggsey/libphonenumber-for-php-lite/src/data/PhoneNumberMetadata_IS.php', + 'libphonenumber\\data\\PhoneNumberMetadata_IT' => __DIR__ . '/..' . '/giggsey/libphonenumber-for-php-lite/src/data/PhoneNumberMetadata_IT.php', + 'libphonenumber\\data\\PhoneNumberMetadata_JE' => __DIR__ . '/..' . '/giggsey/libphonenumber-for-php-lite/src/data/PhoneNumberMetadata_JE.php', + 'libphonenumber\\data\\PhoneNumberMetadata_JM' => __DIR__ . '/..' . '/giggsey/libphonenumber-for-php-lite/src/data/PhoneNumberMetadata_JM.php', + 'libphonenumber\\data\\PhoneNumberMetadata_JO' => __DIR__ . '/..' . '/giggsey/libphonenumber-for-php-lite/src/data/PhoneNumberMetadata_JO.php', + 'libphonenumber\\data\\PhoneNumberMetadata_JP' => __DIR__ . '/..' . '/giggsey/libphonenumber-for-php-lite/src/data/PhoneNumberMetadata_JP.php', + 'libphonenumber\\data\\PhoneNumberMetadata_KE' => __DIR__ . '/..' . '/giggsey/libphonenumber-for-php-lite/src/data/PhoneNumberMetadata_KE.php', + 'libphonenumber\\data\\PhoneNumberMetadata_KG' => __DIR__ . '/..' . '/giggsey/libphonenumber-for-php-lite/src/data/PhoneNumberMetadata_KG.php', + 'libphonenumber\\data\\PhoneNumberMetadata_KH' => __DIR__ . '/..' . '/giggsey/libphonenumber-for-php-lite/src/data/PhoneNumberMetadata_KH.php', + 'libphonenumber\\data\\PhoneNumberMetadata_KI' => __DIR__ . '/..' . '/giggsey/libphonenumber-for-php-lite/src/data/PhoneNumberMetadata_KI.php', + 'libphonenumber\\data\\PhoneNumberMetadata_KM' => __DIR__ . '/..' . '/giggsey/libphonenumber-for-php-lite/src/data/PhoneNumberMetadata_KM.php', + 'libphonenumber\\data\\PhoneNumberMetadata_KN' => __DIR__ . '/..' . '/giggsey/libphonenumber-for-php-lite/src/data/PhoneNumberMetadata_KN.php', + 'libphonenumber\\data\\PhoneNumberMetadata_KP' => __DIR__ . '/..' . '/giggsey/libphonenumber-for-php-lite/src/data/PhoneNumberMetadata_KP.php', + 'libphonenumber\\data\\PhoneNumberMetadata_KR' => __DIR__ . '/..' . '/giggsey/libphonenumber-for-php-lite/src/data/PhoneNumberMetadata_KR.php', + 'libphonenumber\\data\\PhoneNumberMetadata_KW' => __DIR__ . '/..' . '/giggsey/libphonenumber-for-php-lite/src/data/PhoneNumberMetadata_KW.php', + 'libphonenumber\\data\\PhoneNumberMetadata_KY' => __DIR__ . '/..' . '/giggsey/libphonenumber-for-php-lite/src/data/PhoneNumberMetadata_KY.php', + 'libphonenumber\\data\\PhoneNumberMetadata_KZ' => __DIR__ . '/..' . '/giggsey/libphonenumber-for-php-lite/src/data/PhoneNumberMetadata_KZ.php', + 'libphonenumber\\data\\PhoneNumberMetadata_LA' => __DIR__ . '/..' . '/giggsey/libphonenumber-for-php-lite/src/data/PhoneNumberMetadata_LA.php', + 'libphonenumber\\data\\PhoneNumberMetadata_LB' => __DIR__ . '/..' . '/giggsey/libphonenumber-for-php-lite/src/data/PhoneNumberMetadata_LB.php', + 'libphonenumber\\data\\PhoneNumberMetadata_LC' => __DIR__ . '/..' . '/giggsey/libphonenumber-for-php-lite/src/data/PhoneNumberMetadata_LC.php', + 'libphonenumber\\data\\PhoneNumberMetadata_LI' => __DIR__ . '/..' . '/giggsey/libphonenumber-for-php-lite/src/data/PhoneNumberMetadata_LI.php', + 'libphonenumber\\data\\PhoneNumberMetadata_LK' => __DIR__ . '/..' . '/giggsey/libphonenumber-for-php-lite/src/data/PhoneNumberMetadata_LK.php', + 'libphonenumber\\data\\PhoneNumberMetadata_LR' => __DIR__ . '/..' . '/giggsey/libphonenumber-for-php-lite/src/data/PhoneNumberMetadata_LR.php', + 'libphonenumber\\data\\PhoneNumberMetadata_LS' => __DIR__ . '/..' . '/giggsey/libphonenumber-for-php-lite/src/data/PhoneNumberMetadata_LS.php', + 'libphonenumber\\data\\PhoneNumberMetadata_LT' => __DIR__ . '/..' . '/giggsey/libphonenumber-for-php-lite/src/data/PhoneNumberMetadata_LT.php', + 'libphonenumber\\data\\PhoneNumberMetadata_LU' => __DIR__ . '/..' . '/giggsey/libphonenumber-for-php-lite/src/data/PhoneNumberMetadata_LU.php', + 'libphonenumber\\data\\PhoneNumberMetadata_LV' => __DIR__ . '/..' . '/giggsey/libphonenumber-for-php-lite/src/data/PhoneNumberMetadata_LV.php', + 'libphonenumber\\data\\PhoneNumberMetadata_LY' => __DIR__ . '/..' . '/giggsey/libphonenumber-for-php-lite/src/data/PhoneNumberMetadata_LY.php', + 'libphonenumber\\data\\PhoneNumberMetadata_MA' => __DIR__ . '/..' . '/giggsey/libphonenumber-for-php-lite/src/data/PhoneNumberMetadata_MA.php', + 'libphonenumber\\data\\PhoneNumberMetadata_MC' => __DIR__ . '/..' . '/giggsey/libphonenumber-for-php-lite/src/data/PhoneNumberMetadata_MC.php', + 'libphonenumber\\data\\PhoneNumberMetadata_MD' => __DIR__ . '/..' . '/giggsey/libphonenumber-for-php-lite/src/data/PhoneNumberMetadata_MD.php', + 'libphonenumber\\data\\PhoneNumberMetadata_ME' => __DIR__ . '/..' . '/giggsey/libphonenumber-for-php-lite/src/data/PhoneNumberMetadata_ME.php', + 'libphonenumber\\data\\PhoneNumberMetadata_MF' => __DIR__ . '/..' . '/giggsey/libphonenumber-for-php-lite/src/data/PhoneNumberMetadata_MF.php', + 'libphonenumber\\data\\PhoneNumberMetadata_MG' => __DIR__ . '/..' . '/giggsey/libphonenumber-for-php-lite/src/data/PhoneNumberMetadata_MG.php', + 'libphonenumber\\data\\PhoneNumberMetadata_MH' => __DIR__ . '/..' . '/giggsey/libphonenumber-for-php-lite/src/data/PhoneNumberMetadata_MH.php', + 'libphonenumber\\data\\PhoneNumberMetadata_MK' => __DIR__ . '/..' . '/giggsey/libphonenumber-for-php-lite/src/data/PhoneNumberMetadata_MK.php', + 'libphonenumber\\data\\PhoneNumberMetadata_ML' => __DIR__ . '/..' . '/giggsey/libphonenumber-for-php-lite/src/data/PhoneNumberMetadata_ML.php', + 'libphonenumber\\data\\PhoneNumberMetadata_MM' => __DIR__ . '/..' . '/giggsey/libphonenumber-for-php-lite/src/data/PhoneNumberMetadata_MM.php', + 'libphonenumber\\data\\PhoneNumberMetadata_MN' => __DIR__ . '/..' . '/giggsey/libphonenumber-for-php-lite/src/data/PhoneNumberMetadata_MN.php', + 'libphonenumber\\data\\PhoneNumberMetadata_MO' => __DIR__ . '/..' . '/giggsey/libphonenumber-for-php-lite/src/data/PhoneNumberMetadata_MO.php', + 'libphonenumber\\data\\PhoneNumberMetadata_MP' => __DIR__ . '/..' . '/giggsey/libphonenumber-for-php-lite/src/data/PhoneNumberMetadata_MP.php', + 'libphonenumber\\data\\PhoneNumberMetadata_MQ' => __DIR__ . '/..' . '/giggsey/libphonenumber-for-php-lite/src/data/PhoneNumberMetadata_MQ.php', + 'libphonenumber\\data\\PhoneNumberMetadata_MR' => __DIR__ . '/..' . '/giggsey/libphonenumber-for-php-lite/src/data/PhoneNumberMetadata_MR.php', + 'libphonenumber\\data\\PhoneNumberMetadata_MS' => __DIR__ . '/..' . '/giggsey/libphonenumber-for-php-lite/src/data/PhoneNumberMetadata_MS.php', + 'libphonenumber\\data\\PhoneNumberMetadata_MT' => __DIR__ . '/..' . '/giggsey/libphonenumber-for-php-lite/src/data/PhoneNumberMetadata_MT.php', + 'libphonenumber\\data\\PhoneNumberMetadata_MU' => __DIR__ . '/..' . '/giggsey/libphonenumber-for-php-lite/src/data/PhoneNumberMetadata_MU.php', + 'libphonenumber\\data\\PhoneNumberMetadata_MV' => __DIR__ . '/..' . '/giggsey/libphonenumber-for-php-lite/src/data/PhoneNumberMetadata_MV.php', + 'libphonenumber\\data\\PhoneNumberMetadata_MW' => __DIR__ . '/..' . '/giggsey/libphonenumber-for-php-lite/src/data/PhoneNumberMetadata_MW.php', + 'libphonenumber\\data\\PhoneNumberMetadata_MX' => __DIR__ . '/..' . '/giggsey/libphonenumber-for-php-lite/src/data/PhoneNumberMetadata_MX.php', + 'libphonenumber\\data\\PhoneNumberMetadata_MY' => __DIR__ . '/..' . '/giggsey/libphonenumber-for-php-lite/src/data/PhoneNumberMetadata_MY.php', + 'libphonenumber\\data\\PhoneNumberMetadata_MZ' => __DIR__ . '/..' . '/giggsey/libphonenumber-for-php-lite/src/data/PhoneNumberMetadata_MZ.php', + 'libphonenumber\\data\\PhoneNumberMetadata_NA' => __DIR__ . '/..' . '/giggsey/libphonenumber-for-php-lite/src/data/PhoneNumberMetadata_NA.php', + 'libphonenumber\\data\\PhoneNumberMetadata_NC' => __DIR__ . '/..' . '/giggsey/libphonenumber-for-php-lite/src/data/PhoneNumberMetadata_NC.php', + 'libphonenumber\\data\\PhoneNumberMetadata_NE' => __DIR__ . '/..' . '/giggsey/libphonenumber-for-php-lite/src/data/PhoneNumberMetadata_NE.php', + 'libphonenumber\\data\\PhoneNumberMetadata_NF' => __DIR__ . '/..' . '/giggsey/libphonenumber-for-php-lite/src/data/PhoneNumberMetadata_NF.php', + 'libphonenumber\\data\\PhoneNumberMetadata_NG' => __DIR__ . '/..' . '/giggsey/libphonenumber-for-php-lite/src/data/PhoneNumberMetadata_NG.php', + 'libphonenumber\\data\\PhoneNumberMetadata_NI' => __DIR__ . '/..' . '/giggsey/libphonenumber-for-php-lite/src/data/PhoneNumberMetadata_NI.php', + 'libphonenumber\\data\\PhoneNumberMetadata_NL' => __DIR__ . '/..' . '/giggsey/libphonenumber-for-php-lite/src/data/PhoneNumberMetadata_NL.php', + 'libphonenumber\\data\\PhoneNumberMetadata_NO' => __DIR__ . '/..' . '/giggsey/libphonenumber-for-php-lite/src/data/PhoneNumberMetadata_NO.php', + 'libphonenumber\\data\\PhoneNumberMetadata_NP' => __DIR__ . '/..' . '/giggsey/libphonenumber-for-php-lite/src/data/PhoneNumberMetadata_NP.php', + 'libphonenumber\\data\\PhoneNumberMetadata_NR' => __DIR__ . '/..' . '/giggsey/libphonenumber-for-php-lite/src/data/PhoneNumberMetadata_NR.php', + 'libphonenumber\\data\\PhoneNumberMetadata_NU' => __DIR__ . '/..' . '/giggsey/libphonenumber-for-php-lite/src/data/PhoneNumberMetadata_NU.php', + 'libphonenumber\\data\\PhoneNumberMetadata_NZ' => __DIR__ . '/..' . '/giggsey/libphonenumber-for-php-lite/src/data/PhoneNumberMetadata_NZ.php', + 'libphonenumber\\data\\PhoneNumberMetadata_OM' => __DIR__ . '/..' . '/giggsey/libphonenumber-for-php-lite/src/data/PhoneNumberMetadata_OM.php', + 'libphonenumber\\data\\PhoneNumberMetadata_PA' => __DIR__ . '/..' . '/giggsey/libphonenumber-for-php-lite/src/data/PhoneNumberMetadata_PA.php', + 'libphonenumber\\data\\PhoneNumberMetadata_PE' => __DIR__ . '/..' . '/giggsey/libphonenumber-for-php-lite/src/data/PhoneNumberMetadata_PE.php', + 'libphonenumber\\data\\PhoneNumberMetadata_PF' => __DIR__ . '/..' . '/giggsey/libphonenumber-for-php-lite/src/data/PhoneNumberMetadata_PF.php', + 'libphonenumber\\data\\PhoneNumberMetadata_PG' => __DIR__ . '/..' . '/giggsey/libphonenumber-for-php-lite/src/data/PhoneNumberMetadata_PG.php', + 'libphonenumber\\data\\PhoneNumberMetadata_PH' => __DIR__ . '/..' . '/giggsey/libphonenumber-for-php-lite/src/data/PhoneNumberMetadata_PH.php', + 'libphonenumber\\data\\PhoneNumberMetadata_PK' => __DIR__ . '/..' . '/giggsey/libphonenumber-for-php-lite/src/data/PhoneNumberMetadata_PK.php', + 'libphonenumber\\data\\PhoneNumberMetadata_PL' => __DIR__ . '/..' . '/giggsey/libphonenumber-for-php-lite/src/data/PhoneNumberMetadata_PL.php', + 'libphonenumber\\data\\PhoneNumberMetadata_PM' => __DIR__ . '/..' . '/giggsey/libphonenumber-for-php-lite/src/data/PhoneNumberMetadata_PM.php', + 'libphonenumber\\data\\PhoneNumberMetadata_PR' => __DIR__ . '/..' . '/giggsey/libphonenumber-for-php-lite/src/data/PhoneNumberMetadata_PR.php', + 'libphonenumber\\data\\PhoneNumberMetadata_PS' => __DIR__ . '/..' . '/giggsey/libphonenumber-for-php-lite/src/data/PhoneNumberMetadata_PS.php', + 'libphonenumber\\data\\PhoneNumberMetadata_PT' => __DIR__ . '/..' . '/giggsey/libphonenumber-for-php-lite/src/data/PhoneNumberMetadata_PT.php', + 'libphonenumber\\data\\PhoneNumberMetadata_PW' => __DIR__ . '/..' . '/giggsey/libphonenumber-for-php-lite/src/data/PhoneNumberMetadata_PW.php', + 'libphonenumber\\data\\PhoneNumberMetadata_PY' => __DIR__ . '/..' . '/giggsey/libphonenumber-for-php-lite/src/data/PhoneNumberMetadata_PY.php', + 'libphonenumber\\data\\PhoneNumberMetadata_QA' => __DIR__ . '/..' . '/giggsey/libphonenumber-for-php-lite/src/data/PhoneNumberMetadata_QA.php', + 'libphonenumber\\data\\PhoneNumberMetadata_RE' => __DIR__ . '/..' . '/giggsey/libphonenumber-for-php-lite/src/data/PhoneNumberMetadata_RE.php', + 'libphonenumber\\data\\PhoneNumberMetadata_RO' => __DIR__ . '/..' . '/giggsey/libphonenumber-for-php-lite/src/data/PhoneNumberMetadata_RO.php', + 'libphonenumber\\data\\PhoneNumberMetadata_RS' => __DIR__ . '/..' . '/giggsey/libphonenumber-for-php-lite/src/data/PhoneNumberMetadata_RS.php', + 'libphonenumber\\data\\PhoneNumberMetadata_RU' => __DIR__ . '/..' . '/giggsey/libphonenumber-for-php-lite/src/data/PhoneNumberMetadata_RU.php', + 'libphonenumber\\data\\PhoneNumberMetadata_RW' => __DIR__ . '/..' . '/giggsey/libphonenumber-for-php-lite/src/data/PhoneNumberMetadata_RW.php', + 'libphonenumber\\data\\PhoneNumberMetadata_SA' => __DIR__ . '/..' . '/giggsey/libphonenumber-for-php-lite/src/data/PhoneNumberMetadata_SA.php', + 'libphonenumber\\data\\PhoneNumberMetadata_SB' => __DIR__ . '/..' . '/giggsey/libphonenumber-for-php-lite/src/data/PhoneNumberMetadata_SB.php', + 'libphonenumber\\data\\PhoneNumberMetadata_SC' => __DIR__ . '/..' . '/giggsey/libphonenumber-for-php-lite/src/data/PhoneNumberMetadata_SC.php', + 'libphonenumber\\data\\PhoneNumberMetadata_SD' => __DIR__ . '/..' . '/giggsey/libphonenumber-for-php-lite/src/data/PhoneNumberMetadata_SD.php', + 'libphonenumber\\data\\PhoneNumberMetadata_SE' => __DIR__ . '/..' . '/giggsey/libphonenumber-for-php-lite/src/data/PhoneNumberMetadata_SE.php', + 'libphonenumber\\data\\PhoneNumberMetadata_SG' => __DIR__ . '/..' . '/giggsey/libphonenumber-for-php-lite/src/data/PhoneNumberMetadata_SG.php', + 'libphonenumber\\data\\PhoneNumberMetadata_SH' => __DIR__ . '/..' . '/giggsey/libphonenumber-for-php-lite/src/data/PhoneNumberMetadata_SH.php', + 'libphonenumber\\data\\PhoneNumberMetadata_SI' => __DIR__ . '/..' . '/giggsey/libphonenumber-for-php-lite/src/data/PhoneNumberMetadata_SI.php', + 'libphonenumber\\data\\PhoneNumberMetadata_SJ' => __DIR__ . '/..' . '/giggsey/libphonenumber-for-php-lite/src/data/PhoneNumberMetadata_SJ.php', + 'libphonenumber\\data\\PhoneNumberMetadata_SK' => __DIR__ . '/..' . '/giggsey/libphonenumber-for-php-lite/src/data/PhoneNumberMetadata_SK.php', + 'libphonenumber\\data\\PhoneNumberMetadata_SL' => __DIR__ . '/..' . '/giggsey/libphonenumber-for-php-lite/src/data/PhoneNumberMetadata_SL.php', + 'libphonenumber\\data\\PhoneNumberMetadata_SM' => __DIR__ . '/..' . '/giggsey/libphonenumber-for-php-lite/src/data/PhoneNumberMetadata_SM.php', + 'libphonenumber\\data\\PhoneNumberMetadata_SN' => __DIR__ . '/..' . '/giggsey/libphonenumber-for-php-lite/src/data/PhoneNumberMetadata_SN.php', + 'libphonenumber\\data\\PhoneNumberMetadata_SO' => __DIR__ . '/..' . '/giggsey/libphonenumber-for-php-lite/src/data/PhoneNumberMetadata_SO.php', + 'libphonenumber\\data\\PhoneNumberMetadata_SR' => __DIR__ . '/..' . '/giggsey/libphonenumber-for-php-lite/src/data/PhoneNumberMetadata_SR.php', + 'libphonenumber\\data\\PhoneNumberMetadata_SS' => __DIR__ . '/..' . '/giggsey/libphonenumber-for-php-lite/src/data/PhoneNumberMetadata_SS.php', + 'libphonenumber\\data\\PhoneNumberMetadata_ST' => __DIR__ . '/..' . '/giggsey/libphonenumber-for-php-lite/src/data/PhoneNumberMetadata_ST.php', + 'libphonenumber\\data\\PhoneNumberMetadata_SV' => __DIR__ . '/..' . '/giggsey/libphonenumber-for-php-lite/src/data/PhoneNumberMetadata_SV.php', + 'libphonenumber\\data\\PhoneNumberMetadata_SX' => __DIR__ . '/..' . '/giggsey/libphonenumber-for-php-lite/src/data/PhoneNumberMetadata_SX.php', + 'libphonenumber\\data\\PhoneNumberMetadata_SY' => __DIR__ . '/..' . '/giggsey/libphonenumber-for-php-lite/src/data/PhoneNumberMetadata_SY.php', + 'libphonenumber\\data\\PhoneNumberMetadata_SZ' => __DIR__ . '/..' . '/giggsey/libphonenumber-for-php-lite/src/data/PhoneNumberMetadata_SZ.php', + 'libphonenumber\\data\\PhoneNumberMetadata_TA' => __DIR__ . '/..' . '/giggsey/libphonenumber-for-php-lite/src/data/PhoneNumberMetadata_TA.php', + 'libphonenumber\\data\\PhoneNumberMetadata_TC' => __DIR__ . '/..' . '/giggsey/libphonenumber-for-php-lite/src/data/PhoneNumberMetadata_TC.php', + 'libphonenumber\\data\\PhoneNumberMetadata_TD' => __DIR__ . '/..' . '/giggsey/libphonenumber-for-php-lite/src/data/PhoneNumberMetadata_TD.php', + 'libphonenumber\\data\\PhoneNumberMetadata_TG' => __DIR__ . '/..' . '/giggsey/libphonenumber-for-php-lite/src/data/PhoneNumberMetadata_TG.php', + 'libphonenumber\\data\\PhoneNumberMetadata_TH' => __DIR__ . '/..' . '/giggsey/libphonenumber-for-php-lite/src/data/PhoneNumberMetadata_TH.php', + 'libphonenumber\\data\\PhoneNumberMetadata_TJ' => __DIR__ . '/..' . '/giggsey/libphonenumber-for-php-lite/src/data/PhoneNumberMetadata_TJ.php', + 'libphonenumber\\data\\PhoneNumberMetadata_TK' => __DIR__ . '/..' . '/giggsey/libphonenumber-for-php-lite/src/data/PhoneNumberMetadata_TK.php', + 'libphonenumber\\data\\PhoneNumberMetadata_TL' => __DIR__ . '/..' . '/giggsey/libphonenumber-for-php-lite/src/data/PhoneNumberMetadata_TL.php', + 'libphonenumber\\data\\PhoneNumberMetadata_TM' => __DIR__ . '/..' . '/giggsey/libphonenumber-for-php-lite/src/data/PhoneNumberMetadata_TM.php', + 'libphonenumber\\data\\PhoneNumberMetadata_TN' => __DIR__ . '/..' . '/giggsey/libphonenumber-for-php-lite/src/data/PhoneNumberMetadata_TN.php', + 'libphonenumber\\data\\PhoneNumberMetadata_TO' => __DIR__ . '/..' . '/giggsey/libphonenumber-for-php-lite/src/data/PhoneNumberMetadata_TO.php', + 'libphonenumber\\data\\PhoneNumberMetadata_TR' => __DIR__ . '/..' . '/giggsey/libphonenumber-for-php-lite/src/data/PhoneNumberMetadata_TR.php', + 'libphonenumber\\data\\PhoneNumberMetadata_TT' => __DIR__ . '/..' . '/giggsey/libphonenumber-for-php-lite/src/data/PhoneNumberMetadata_TT.php', + 'libphonenumber\\data\\PhoneNumberMetadata_TV' => __DIR__ . '/..' . '/giggsey/libphonenumber-for-php-lite/src/data/PhoneNumberMetadata_TV.php', + 'libphonenumber\\data\\PhoneNumberMetadata_TW' => __DIR__ . '/..' . '/giggsey/libphonenumber-for-php-lite/src/data/PhoneNumberMetadata_TW.php', + 'libphonenumber\\data\\PhoneNumberMetadata_TZ' => __DIR__ . '/..' . '/giggsey/libphonenumber-for-php-lite/src/data/PhoneNumberMetadata_TZ.php', + 'libphonenumber\\data\\PhoneNumberMetadata_UA' => __DIR__ . '/..' . '/giggsey/libphonenumber-for-php-lite/src/data/PhoneNumberMetadata_UA.php', + 'libphonenumber\\data\\PhoneNumberMetadata_UG' => __DIR__ . '/..' . '/giggsey/libphonenumber-for-php-lite/src/data/PhoneNumberMetadata_UG.php', + 'libphonenumber\\data\\PhoneNumberMetadata_US' => __DIR__ . '/..' . '/giggsey/libphonenumber-for-php-lite/src/data/PhoneNumberMetadata_US.php', + 'libphonenumber\\data\\PhoneNumberMetadata_UY' => __DIR__ . '/..' . '/giggsey/libphonenumber-for-php-lite/src/data/PhoneNumberMetadata_UY.php', + 'libphonenumber\\data\\PhoneNumberMetadata_UZ' => __DIR__ . '/..' . '/giggsey/libphonenumber-for-php-lite/src/data/PhoneNumberMetadata_UZ.php', + 'libphonenumber\\data\\PhoneNumberMetadata_VA' => __DIR__ . '/..' . '/giggsey/libphonenumber-for-php-lite/src/data/PhoneNumberMetadata_VA.php', + 'libphonenumber\\data\\PhoneNumberMetadata_VC' => __DIR__ . '/..' . '/giggsey/libphonenumber-for-php-lite/src/data/PhoneNumberMetadata_VC.php', + 'libphonenumber\\data\\PhoneNumberMetadata_VE' => __DIR__ . '/..' . '/giggsey/libphonenumber-for-php-lite/src/data/PhoneNumberMetadata_VE.php', + 'libphonenumber\\data\\PhoneNumberMetadata_VG' => __DIR__ . '/..' . '/giggsey/libphonenumber-for-php-lite/src/data/PhoneNumberMetadata_VG.php', + 'libphonenumber\\data\\PhoneNumberMetadata_VI' => __DIR__ . '/..' . '/giggsey/libphonenumber-for-php-lite/src/data/PhoneNumberMetadata_VI.php', + 'libphonenumber\\data\\PhoneNumberMetadata_VN' => __DIR__ . '/..' . '/giggsey/libphonenumber-for-php-lite/src/data/PhoneNumberMetadata_VN.php', + 'libphonenumber\\data\\PhoneNumberMetadata_VU' => __DIR__ . '/..' . '/giggsey/libphonenumber-for-php-lite/src/data/PhoneNumberMetadata_VU.php', + 'libphonenumber\\data\\PhoneNumberMetadata_WF' => __DIR__ . '/..' . '/giggsey/libphonenumber-for-php-lite/src/data/PhoneNumberMetadata_WF.php', + 'libphonenumber\\data\\PhoneNumberMetadata_WS' => __DIR__ . '/..' . '/giggsey/libphonenumber-for-php-lite/src/data/PhoneNumberMetadata_WS.php', + 'libphonenumber\\data\\PhoneNumberMetadata_XK' => __DIR__ . '/..' . '/giggsey/libphonenumber-for-php-lite/src/data/PhoneNumberMetadata_XK.php', + 'libphonenumber\\data\\PhoneNumberMetadata_YE' => __DIR__ . '/..' . '/giggsey/libphonenumber-for-php-lite/src/data/PhoneNumberMetadata_YE.php', + 'libphonenumber\\data\\PhoneNumberMetadata_YT' => __DIR__ . '/..' . '/giggsey/libphonenumber-for-php-lite/src/data/PhoneNumberMetadata_YT.php', + 'libphonenumber\\data\\PhoneNumberMetadata_ZA' => __DIR__ . '/..' . '/giggsey/libphonenumber-for-php-lite/src/data/PhoneNumberMetadata_ZA.php', + 'libphonenumber\\data\\PhoneNumberMetadata_ZM' => __DIR__ . '/..' . '/giggsey/libphonenumber-for-php-lite/src/data/PhoneNumberMetadata_ZM.php', + 'libphonenumber\\data\\PhoneNumberMetadata_ZW' => __DIR__ . '/..' . '/giggsey/libphonenumber-for-php-lite/src/data/PhoneNumberMetadata_ZW.php', + 'libphonenumber\\data\\ShortNumberMetadata_AC' => __DIR__ . '/..' . '/giggsey/libphonenumber-for-php-lite/src/data/ShortNumberMetadata_AC.php', + 'libphonenumber\\data\\ShortNumberMetadata_AD' => __DIR__ . '/..' . '/giggsey/libphonenumber-for-php-lite/src/data/ShortNumberMetadata_AD.php', + 'libphonenumber\\data\\ShortNumberMetadata_AE' => __DIR__ . '/..' . '/giggsey/libphonenumber-for-php-lite/src/data/ShortNumberMetadata_AE.php', + 'libphonenumber\\data\\ShortNumberMetadata_AF' => __DIR__ . '/..' . '/giggsey/libphonenumber-for-php-lite/src/data/ShortNumberMetadata_AF.php', + 'libphonenumber\\data\\ShortNumberMetadata_AG' => __DIR__ . '/..' . '/giggsey/libphonenumber-for-php-lite/src/data/ShortNumberMetadata_AG.php', + 'libphonenumber\\data\\ShortNumberMetadata_AI' => __DIR__ . '/..' . '/giggsey/libphonenumber-for-php-lite/src/data/ShortNumberMetadata_AI.php', + 'libphonenumber\\data\\ShortNumberMetadata_AL' => __DIR__ . '/..' . '/giggsey/libphonenumber-for-php-lite/src/data/ShortNumberMetadata_AL.php', + 'libphonenumber\\data\\ShortNumberMetadata_AM' => __DIR__ . '/..' . '/giggsey/libphonenumber-for-php-lite/src/data/ShortNumberMetadata_AM.php', + 'libphonenumber\\data\\ShortNumberMetadata_AO' => __DIR__ . '/..' . '/giggsey/libphonenumber-for-php-lite/src/data/ShortNumberMetadata_AO.php', + 'libphonenumber\\data\\ShortNumberMetadata_AR' => __DIR__ . '/..' . '/giggsey/libphonenumber-for-php-lite/src/data/ShortNumberMetadata_AR.php', + 'libphonenumber\\data\\ShortNumberMetadata_AS' => __DIR__ . '/..' . '/giggsey/libphonenumber-for-php-lite/src/data/ShortNumberMetadata_AS.php', + 'libphonenumber\\data\\ShortNumberMetadata_AT' => __DIR__ . '/..' . '/giggsey/libphonenumber-for-php-lite/src/data/ShortNumberMetadata_AT.php', + 'libphonenumber\\data\\ShortNumberMetadata_AU' => __DIR__ . '/..' . '/giggsey/libphonenumber-for-php-lite/src/data/ShortNumberMetadata_AU.php', + 'libphonenumber\\data\\ShortNumberMetadata_AW' => __DIR__ . '/..' . '/giggsey/libphonenumber-for-php-lite/src/data/ShortNumberMetadata_AW.php', + 'libphonenumber\\data\\ShortNumberMetadata_AX' => __DIR__ . '/..' . '/giggsey/libphonenumber-for-php-lite/src/data/ShortNumberMetadata_AX.php', + 'libphonenumber\\data\\ShortNumberMetadata_AZ' => __DIR__ . '/..' . '/giggsey/libphonenumber-for-php-lite/src/data/ShortNumberMetadata_AZ.php', + 'libphonenumber\\data\\ShortNumberMetadata_BA' => __DIR__ . '/..' . '/giggsey/libphonenumber-for-php-lite/src/data/ShortNumberMetadata_BA.php', + 'libphonenumber\\data\\ShortNumberMetadata_BB' => __DIR__ . '/..' . '/giggsey/libphonenumber-for-php-lite/src/data/ShortNumberMetadata_BB.php', + 'libphonenumber\\data\\ShortNumberMetadata_BD' => __DIR__ . '/..' . '/giggsey/libphonenumber-for-php-lite/src/data/ShortNumberMetadata_BD.php', + 'libphonenumber\\data\\ShortNumberMetadata_BE' => __DIR__ . '/..' . '/giggsey/libphonenumber-for-php-lite/src/data/ShortNumberMetadata_BE.php', + 'libphonenumber\\data\\ShortNumberMetadata_BF' => __DIR__ . '/..' . '/giggsey/libphonenumber-for-php-lite/src/data/ShortNumberMetadata_BF.php', + 'libphonenumber\\data\\ShortNumberMetadata_BG' => __DIR__ . '/..' . '/giggsey/libphonenumber-for-php-lite/src/data/ShortNumberMetadata_BG.php', + 'libphonenumber\\data\\ShortNumberMetadata_BH' => __DIR__ . '/..' . '/giggsey/libphonenumber-for-php-lite/src/data/ShortNumberMetadata_BH.php', + 'libphonenumber\\data\\ShortNumberMetadata_BI' => __DIR__ . '/..' . '/giggsey/libphonenumber-for-php-lite/src/data/ShortNumberMetadata_BI.php', + 'libphonenumber\\data\\ShortNumberMetadata_BJ' => __DIR__ . '/..' . '/giggsey/libphonenumber-for-php-lite/src/data/ShortNumberMetadata_BJ.php', + 'libphonenumber\\data\\ShortNumberMetadata_BL' => __DIR__ . '/..' . '/giggsey/libphonenumber-for-php-lite/src/data/ShortNumberMetadata_BL.php', + 'libphonenumber\\data\\ShortNumberMetadata_BM' => __DIR__ . '/..' . '/giggsey/libphonenumber-for-php-lite/src/data/ShortNumberMetadata_BM.php', + 'libphonenumber\\data\\ShortNumberMetadata_BN' => __DIR__ . '/..' . '/giggsey/libphonenumber-for-php-lite/src/data/ShortNumberMetadata_BN.php', + 'libphonenumber\\data\\ShortNumberMetadata_BO' => __DIR__ . '/..' . '/giggsey/libphonenumber-for-php-lite/src/data/ShortNumberMetadata_BO.php', + 'libphonenumber\\data\\ShortNumberMetadata_BQ' => __DIR__ . '/..' . '/giggsey/libphonenumber-for-php-lite/src/data/ShortNumberMetadata_BQ.php', + 'libphonenumber\\data\\ShortNumberMetadata_BR' => __DIR__ . '/..' . '/giggsey/libphonenumber-for-php-lite/src/data/ShortNumberMetadata_BR.php', + 'libphonenumber\\data\\ShortNumberMetadata_BS' => __DIR__ . '/..' . '/giggsey/libphonenumber-for-php-lite/src/data/ShortNumberMetadata_BS.php', + 'libphonenumber\\data\\ShortNumberMetadata_BT' => __DIR__ . '/..' . '/giggsey/libphonenumber-for-php-lite/src/data/ShortNumberMetadata_BT.php', + 'libphonenumber\\data\\ShortNumberMetadata_BW' => __DIR__ . '/..' . '/giggsey/libphonenumber-for-php-lite/src/data/ShortNumberMetadata_BW.php', + 'libphonenumber\\data\\ShortNumberMetadata_BY' => __DIR__ . '/..' . '/giggsey/libphonenumber-for-php-lite/src/data/ShortNumberMetadata_BY.php', + 'libphonenumber\\data\\ShortNumberMetadata_BZ' => __DIR__ . '/..' . '/giggsey/libphonenumber-for-php-lite/src/data/ShortNumberMetadata_BZ.php', + 'libphonenumber\\data\\ShortNumberMetadata_CA' => __DIR__ . '/..' . '/giggsey/libphonenumber-for-php-lite/src/data/ShortNumberMetadata_CA.php', + 'libphonenumber\\data\\ShortNumberMetadata_CC' => __DIR__ . '/..' . '/giggsey/libphonenumber-for-php-lite/src/data/ShortNumberMetadata_CC.php', + 'libphonenumber\\data\\ShortNumberMetadata_CD' => __DIR__ . '/..' . '/giggsey/libphonenumber-for-php-lite/src/data/ShortNumberMetadata_CD.php', + 'libphonenumber\\data\\ShortNumberMetadata_CF' => __DIR__ . '/..' . '/giggsey/libphonenumber-for-php-lite/src/data/ShortNumberMetadata_CF.php', + 'libphonenumber\\data\\ShortNumberMetadata_CG' => __DIR__ . '/..' . '/giggsey/libphonenumber-for-php-lite/src/data/ShortNumberMetadata_CG.php', + 'libphonenumber\\data\\ShortNumberMetadata_CH' => __DIR__ . '/..' . '/giggsey/libphonenumber-for-php-lite/src/data/ShortNumberMetadata_CH.php', + 'libphonenumber\\data\\ShortNumberMetadata_CI' => __DIR__ . '/..' . '/giggsey/libphonenumber-for-php-lite/src/data/ShortNumberMetadata_CI.php', + 'libphonenumber\\data\\ShortNumberMetadata_CK' => __DIR__ . '/..' . '/giggsey/libphonenumber-for-php-lite/src/data/ShortNumberMetadata_CK.php', + 'libphonenumber\\data\\ShortNumberMetadata_CL' => __DIR__ . '/..' . '/giggsey/libphonenumber-for-php-lite/src/data/ShortNumberMetadata_CL.php', + 'libphonenumber\\data\\ShortNumberMetadata_CM' => __DIR__ . '/..' . '/giggsey/libphonenumber-for-php-lite/src/data/ShortNumberMetadata_CM.php', + 'libphonenumber\\data\\ShortNumberMetadata_CN' => __DIR__ . '/..' . '/giggsey/libphonenumber-for-php-lite/src/data/ShortNumberMetadata_CN.php', + 'libphonenumber\\data\\ShortNumberMetadata_CO' => __DIR__ . '/..' . '/giggsey/libphonenumber-for-php-lite/src/data/ShortNumberMetadata_CO.php', + 'libphonenumber\\data\\ShortNumberMetadata_CR' => __DIR__ . '/..' . '/giggsey/libphonenumber-for-php-lite/src/data/ShortNumberMetadata_CR.php', + 'libphonenumber\\data\\ShortNumberMetadata_CU' => __DIR__ . '/..' . '/giggsey/libphonenumber-for-php-lite/src/data/ShortNumberMetadata_CU.php', + 'libphonenumber\\data\\ShortNumberMetadata_CV' => __DIR__ . '/..' . '/giggsey/libphonenumber-for-php-lite/src/data/ShortNumberMetadata_CV.php', + 'libphonenumber\\data\\ShortNumberMetadata_CW' => __DIR__ . '/..' . '/giggsey/libphonenumber-for-php-lite/src/data/ShortNumberMetadata_CW.php', + 'libphonenumber\\data\\ShortNumberMetadata_CX' => __DIR__ . '/..' . '/giggsey/libphonenumber-for-php-lite/src/data/ShortNumberMetadata_CX.php', + 'libphonenumber\\data\\ShortNumberMetadata_CY' => __DIR__ . '/..' . '/giggsey/libphonenumber-for-php-lite/src/data/ShortNumberMetadata_CY.php', + 'libphonenumber\\data\\ShortNumberMetadata_CZ' => __DIR__ . '/..' . '/giggsey/libphonenumber-for-php-lite/src/data/ShortNumberMetadata_CZ.php', + 'libphonenumber\\data\\ShortNumberMetadata_DE' => __DIR__ . '/..' . '/giggsey/libphonenumber-for-php-lite/src/data/ShortNumberMetadata_DE.php', + 'libphonenumber\\data\\ShortNumberMetadata_DJ' => __DIR__ . '/..' . '/giggsey/libphonenumber-for-php-lite/src/data/ShortNumberMetadata_DJ.php', + 'libphonenumber\\data\\ShortNumberMetadata_DK' => __DIR__ . '/..' . '/giggsey/libphonenumber-for-php-lite/src/data/ShortNumberMetadata_DK.php', + 'libphonenumber\\data\\ShortNumberMetadata_DM' => __DIR__ . '/..' . '/giggsey/libphonenumber-for-php-lite/src/data/ShortNumberMetadata_DM.php', + 'libphonenumber\\data\\ShortNumberMetadata_DO' => __DIR__ . '/..' . '/giggsey/libphonenumber-for-php-lite/src/data/ShortNumberMetadata_DO.php', + 'libphonenumber\\data\\ShortNumberMetadata_DZ' => __DIR__ . '/..' . '/giggsey/libphonenumber-for-php-lite/src/data/ShortNumberMetadata_DZ.php', + 'libphonenumber\\data\\ShortNumberMetadata_EC' => __DIR__ . '/..' . '/giggsey/libphonenumber-for-php-lite/src/data/ShortNumberMetadata_EC.php', + 'libphonenumber\\data\\ShortNumberMetadata_EE' => __DIR__ . '/..' . '/giggsey/libphonenumber-for-php-lite/src/data/ShortNumberMetadata_EE.php', + 'libphonenumber\\data\\ShortNumberMetadata_EG' => __DIR__ . '/..' . '/giggsey/libphonenumber-for-php-lite/src/data/ShortNumberMetadata_EG.php', + 'libphonenumber\\data\\ShortNumberMetadata_EH' => __DIR__ . '/..' . '/giggsey/libphonenumber-for-php-lite/src/data/ShortNumberMetadata_EH.php', + 'libphonenumber\\data\\ShortNumberMetadata_ER' => __DIR__ . '/..' . '/giggsey/libphonenumber-for-php-lite/src/data/ShortNumberMetadata_ER.php', + 'libphonenumber\\data\\ShortNumberMetadata_ES' => __DIR__ . '/..' . '/giggsey/libphonenumber-for-php-lite/src/data/ShortNumberMetadata_ES.php', + 'libphonenumber\\data\\ShortNumberMetadata_ET' => __DIR__ . '/..' . '/giggsey/libphonenumber-for-php-lite/src/data/ShortNumberMetadata_ET.php', + 'libphonenumber\\data\\ShortNumberMetadata_FI' => __DIR__ . '/..' . '/giggsey/libphonenumber-for-php-lite/src/data/ShortNumberMetadata_FI.php', + 'libphonenumber\\data\\ShortNumberMetadata_FJ' => __DIR__ . '/..' . '/giggsey/libphonenumber-for-php-lite/src/data/ShortNumberMetadata_FJ.php', + 'libphonenumber\\data\\ShortNumberMetadata_FK' => __DIR__ . '/..' . '/giggsey/libphonenumber-for-php-lite/src/data/ShortNumberMetadata_FK.php', + 'libphonenumber\\data\\ShortNumberMetadata_FM' => __DIR__ . '/..' . '/giggsey/libphonenumber-for-php-lite/src/data/ShortNumberMetadata_FM.php', + 'libphonenumber\\data\\ShortNumberMetadata_FO' => __DIR__ . '/..' . '/giggsey/libphonenumber-for-php-lite/src/data/ShortNumberMetadata_FO.php', + 'libphonenumber\\data\\ShortNumberMetadata_FR' => __DIR__ . '/..' . '/giggsey/libphonenumber-for-php-lite/src/data/ShortNumberMetadata_FR.php', + 'libphonenumber\\data\\ShortNumberMetadata_GA' => __DIR__ . '/..' . '/giggsey/libphonenumber-for-php-lite/src/data/ShortNumberMetadata_GA.php', + 'libphonenumber\\data\\ShortNumberMetadata_GB' => __DIR__ . '/..' . '/giggsey/libphonenumber-for-php-lite/src/data/ShortNumberMetadata_GB.php', + 'libphonenumber\\data\\ShortNumberMetadata_GD' => __DIR__ . '/..' . '/giggsey/libphonenumber-for-php-lite/src/data/ShortNumberMetadata_GD.php', + 'libphonenumber\\data\\ShortNumberMetadata_GE' => __DIR__ . '/..' . '/giggsey/libphonenumber-for-php-lite/src/data/ShortNumberMetadata_GE.php', + 'libphonenumber\\data\\ShortNumberMetadata_GF' => __DIR__ . '/..' . '/giggsey/libphonenumber-for-php-lite/src/data/ShortNumberMetadata_GF.php', + 'libphonenumber\\data\\ShortNumberMetadata_GG' => __DIR__ . '/..' . '/giggsey/libphonenumber-for-php-lite/src/data/ShortNumberMetadata_GG.php', + 'libphonenumber\\data\\ShortNumberMetadata_GH' => __DIR__ . '/..' . '/giggsey/libphonenumber-for-php-lite/src/data/ShortNumberMetadata_GH.php', + 'libphonenumber\\data\\ShortNumberMetadata_GI' => __DIR__ . '/..' . '/giggsey/libphonenumber-for-php-lite/src/data/ShortNumberMetadata_GI.php', + 'libphonenumber\\data\\ShortNumberMetadata_GL' => __DIR__ . '/..' . '/giggsey/libphonenumber-for-php-lite/src/data/ShortNumberMetadata_GL.php', + 'libphonenumber\\data\\ShortNumberMetadata_GM' => __DIR__ . '/..' . '/giggsey/libphonenumber-for-php-lite/src/data/ShortNumberMetadata_GM.php', + 'libphonenumber\\data\\ShortNumberMetadata_GN' => __DIR__ . '/..' . '/giggsey/libphonenumber-for-php-lite/src/data/ShortNumberMetadata_GN.php', + 'libphonenumber\\data\\ShortNumberMetadata_GP' => __DIR__ . '/..' . '/giggsey/libphonenumber-for-php-lite/src/data/ShortNumberMetadata_GP.php', + 'libphonenumber\\data\\ShortNumberMetadata_GR' => __DIR__ . '/..' . '/giggsey/libphonenumber-for-php-lite/src/data/ShortNumberMetadata_GR.php', + 'libphonenumber\\data\\ShortNumberMetadata_GT' => __DIR__ . '/..' . '/giggsey/libphonenumber-for-php-lite/src/data/ShortNumberMetadata_GT.php', + 'libphonenumber\\data\\ShortNumberMetadata_GU' => __DIR__ . '/..' . '/giggsey/libphonenumber-for-php-lite/src/data/ShortNumberMetadata_GU.php', + 'libphonenumber\\data\\ShortNumberMetadata_GW' => __DIR__ . '/..' . '/giggsey/libphonenumber-for-php-lite/src/data/ShortNumberMetadata_GW.php', + 'libphonenumber\\data\\ShortNumberMetadata_GY' => __DIR__ . '/..' . '/giggsey/libphonenumber-for-php-lite/src/data/ShortNumberMetadata_GY.php', + 'libphonenumber\\data\\ShortNumberMetadata_HK' => __DIR__ . '/..' . '/giggsey/libphonenumber-for-php-lite/src/data/ShortNumberMetadata_HK.php', + 'libphonenumber\\data\\ShortNumberMetadata_HN' => __DIR__ . '/..' . '/giggsey/libphonenumber-for-php-lite/src/data/ShortNumberMetadata_HN.php', + 'libphonenumber\\data\\ShortNumberMetadata_HR' => __DIR__ . '/..' . '/giggsey/libphonenumber-for-php-lite/src/data/ShortNumberMetadata_HR.php', + 'libphonenumber\\data\\ShortNumberMetadata_HT' => __DIR__ . '/..' . '/giggsey/libphonenumber-for-php-lite/src/data/ShortNumberMetadata_HT.php', + 'libphonenumber\\data\\ShortNumberMetadata_HU' => __DIR__ . '/..' . '/giggsey/libphonenumber-for-php-lite/src/data/ShortNumberMetadata_HU.php', + 'libphonenumber\\data\\ShortNumberMetadata_ID' => __DIR__ . '/..' . '/giggsey/libphonenumber-for-php-lite/src/data/ShortNumberMetadata_ID.php', + 'libphonenumber\\data\\ShortNumberMetadata_IE' => __DIR__ . '/..' . '/giggsey/libphonenumber-for-php-lite/src/data/ShortNumberMetadata_IE.php', + 'libphonenumber\\data\\ShortNumberMetadata_IL' => __DIR__ . '/..' . '/giggsey/libphonenumber-for-php-lite/src/data/ShortNumberMetadata_IL.php', + 'libphonenumber\\data\\ShortNumberMetadata_IM' => __DIR__ . '/..' . '/giggsey/libphonenumber-for-php-lite/src/data/ShortNumberMetadata_IM.php', + 'libphonenumber\\data\\ShortNumberMetadata_IN' => __DIR__ . '/..' . '/giggsey/libphonenumber-for-php-lite/src/data/ShortNumberMetadata_IN.php', + 'libphonenumber\\data\\ShortNumberMetadata_IQ' => __DIR__ . '/..' . '/giggsey/libphonenumber-for-php-lite/src/data/ShortNumberMetadata_IQ.php', + 'libphonenumber\\data\\ShortNumberMetadata_IR' => __DIR__ . '/..' . '/giggsey/libphonenumber-for-php-lite/src/data/ShortNumberMetadata_IR.php', + 'libphonenumber\\data\\ShortNumberMetadata_IS' => __DIR__ . '/..' . '/giggsey/libphonenumber-for-php-lite/src/data/ShortNumberMetadata_IS.php', + 'libphonenumber\\data\\ShortNumberMetadata_IT' => __DIR__ . '/..' . '/giggsey/libphonenumber-for-php-lite/src/data/ShortNumberMetadata_IT.php', + 'libphonenumber\\data\\ShortNumberMetadata_JE' => __DIR__ . '/..' . '/giggsey/libphonenumber-for-php-lite/src/data/ShortNumberMetadata_JE.php', + 'libphonenumber\\data\\ShortNumberMetadata_JM' => __DIR__ . '/..' . '/giggsey/libphonenumber-for-php-lite/src/data/ShortNumberMetadata_JM.php', + 'libphonenumber\\data\\ShortNumberMetadata_JO' => __DIR__ . '/..' . '/giggsey/libphonenumber-for-php-lite/src/data/ShortNumberMetadata_JO.php', + 'libphonenumber\\data\\ShortNumberMetadata_JP' => __DIR__ . '/..' . '/giggsey/libphonenumber-for-php-lite/src/data/ShortNumberMetadata_JP.php', + 'libphonenumber\\data\\ShortNumberMetadata_KE' => __DIR__ . '/..' . '/giggsey/libphonenumber-for-php-lite/src/data/ShortNumberMetadata_KE.php', + 'libphonenumber\\data\\ShortNumberMetadata_KG' => __DIR__ . '/..' . '/giggsey/libphonenumber-for-php-lite/src/data/ShortNumberMetadata_KG.php', + 'libphonenumber\\data\\ShortNumberMetadata_KH' => __DIR__ . '/..' . '/giggsey/libphonenumber-for-php-lite/src/data/ShortNumberMetadata_KH.php', + 'libphonenumber\\data\\ShortNumberMetadata_KI' => __DIR__ . '/..' . '/giggsey/libphonenumber-for-php-lite/src/data/ShortNumberMetadata_KI.php', + 'libphonenumber\\data\\ShortNumberMetadata_KM' => __DIR__ . '/..' . '/giggsey/libphonenumber-for-php-lite/src/data/ShortNumberMetadata_KM.php', + 'libphonenumber\\data\\ShortNumberMetadata_KN' => __DIR__ . '/..' . '/giggsey/libphonenumber-for-php-lite/src/data/ShortNumberMetadata_KN.php', + 'libphonenumber\\data\\ShortNumberMetadata_KP' => __DIR__ . '/..' . '/giggsey/libphonenumber-for-php-lite/src/data/ShortNumberMetadata_KP.php', + 'libphonenumber\\data\\ShortNumberMetadata_KR' => __DIR__ . '/..' . '/giggsey/libphonenumber-for-php-lite/src/data/ShortNumberMetadata_KR.php', + 'libphonenumber\\data\\ShortNumberMetadata_KW' => __DIR__ . '/..' . '/giggsey/libphonenumber-for-php-lite/src/data/ShortNumberMetadata_KW.php', + 'libphonenumber\\data\\ShortNumberMetadata_KY' => __DIR__ . '/..' . '/giggsey/libphonenumber-for-php-lite/src/data/ShortNumberMetadata_KY.php', + 'libphonenumber\\data\\ShortNumberMetadata_KZ' => __DIR__ . '/..' . '/giggsey/libphonenumber-for-php-lite/src/data/ShortNumberMetadata_KZ.php', + 'libphonenumber\\data\\ShortNumberMetadata_LA' => __DIR__ . '/..' . '/giggsey/libphonenumber-for-php-lite/src/data/ShortNumberMetadata_LA.php', + 'libphonenumber\\data\\ShortNumberMetadata_LB' => __DIR__ . '/..' . '/giggsey/libphonenumber-for-php-lite/src/data/ShortNumberMetadata_LB.php', + 'libphonenumber\\data\\ShortNumberMetadata_LC' => __DIR__ . '/..' . '/giggsey/libphonenumber-for-php-lite/src/data/ShortNumberMetadata_LC.php', + 'libphonenumber\\data\\ShortNumberMetadata_LI' => __DIR__ . '/..' . '/giggsey/libphonenumber-for-php-lite/src/data/ShortNumberMetadata_LI.php', + 'libphonenumber\\data\\ShortNumberMetadata_LK' => __DIR__ . '/..' . '/giggsey/libphonenumber-for-php-lite/src/data/ShortNumberMetadata_LK.php', + 'libphonenumber\\data\\ShortNumberMetadata_LR' => __DIR__ . '/..' . '/giggsey/libphonenumber-for-php-lite/src/data/ShortNumberMetadata_LR.php', + 'libphonenumber\\data\\ShortNumberMetadata_LS' => __DIR__ . '/..' . '/giggsey/libphonenumber-for-php-lite/src/data/ShortNumberMetadata_LS.php', + 'libphonenumber\\data\\ShortNumberMetadata_LT' => __DIR__ . '/..' . '/giggsey/libphonenumber-for-php-lite/src/data/ShortNumberMetadata_LT.php', + 'libphonenumber\\data\\ShortNumberMetadata_LU' => __DIR__ . '/..' . '/giggsey/libphonenumber-for-php-lite/src/data/ShortNumberMetadata_LU.php', + 'libphonenumber\\data\\ShortNumberMetadata_LV' => __DIR__ . '/..' . '/giggsey/libphonenumber-for-php-lite/src/data/ShortNumberMetadata_LV.php', + 'libphonenumber\\data\\ShortNumberMetadata_LY' => __DIR__ . '/..' . '/giggsey/libphonenumber-for-php-lite/src/data/ShortNumberMetadata_LY.php', + 'libphonenumber\\data\\ShortNumberMetadata_MA' => __DIR__ . '/..' . '/giggsey/libphonenumber-for-php-lite/src/data/ShortNumberMetadata_MA.php', + 'libphonenumber\\data\\ShortNumberMetadata_MC' => __DIR__ . '/..' . '/giggsey/libphonenumber-for-php-lite/src/data/ShortNumberMetadata_MC.php', + 'libphonenumber\\data\\ShortNumberMetadata_MD' => __DIR__ . '/..' . '/giggsey/libphonenumber-for-php-lite/src/data/ShortNumberMetadata_MD.php', + 'libphonenumber\\data\\ShortNumberMetadata_ME' => __DIR__ . '/..' . '/giggsey/libphonenumber-for-php-lite/src/data/ShortNumberMetadata_ME.php', + 'libphonenumber\\data\\ShortNumberMetadata_MF' => __DIR__ . '/..' . '/giggsey/libphonenumber-for-php-lite/src/data/ShortNumberMetadata_MF.php', + 'libphonenumber\\data\\ShortNumberMetadata_MG' => __DIR__ . '/..' . '/giggsey/libphonenumber-for-php-lite/src/data/ShortNumberMetadata_MG.php', + 'libphonenumber\\data\\ShortNumberMetadata_MH' => __DIR__ . '/..' . '/giggsey/libphonenumber-for-php-lite/src/data/ShortNumberMetadata_MH.php', + 'libphonenumber\\data\\ShortNumberMetadata_MK' => __DIR__ . '/..' . '/giggsey/libphonenumber-for-php-lite/src/data/ShortNumberMetadata_MK.php', + 'libphonenumber\\data\\ShortNumberMetadata_ML' => __DIR__ . '/..' . '/giggsey/libphonenumber-for-php-lite/src/data/ShortNumberMetadata_ML.php', + 'libphonenumber\\data\\ShortNumberMetadata_MM' => __DIR__ . '/..' . '/giggsey/libphonenumber-for-php-lite/src/data/ShortNumberMetadata_MM.php', + 'libphonenumber\\data\\ShortNumberMetadata_MN' => __DIR__ . '/..' . '/giggsey/libphonenumber-for-php-lite/src/data/ShortNumberMetadata_MN.php', + 'libphonenumber\\data\\ShortNumberMetadata_MO' => __DIR__ . '/..' . '/giggsey/libphonenumber-for-php-lite/src/data/ShortNumberMetadata_MO.php', + 'libphonenumber\\data\\ShortNumberMetadata_MP' => __DIR__ . '/..' . '/giggsey/libphonenumber-for-php-lite/src/data/ShortNumberMetadata_MP.php', + 'libphonenumber\\data\\ShortNumberMetadata_MQ' => __DIR__ . '/..' . '/giggsey/libphonenumber-for-php-lite/src/data/ShortNumberMetadata_MQ.php', + 'libphonenumber\\data\\ShortNumberMetadata_MR' => __DIR__ . '/..' . '/giggsey/libphonenumber-for-php-lite/src/data/ShortNumberMetadata_MR.php', + 'libphonenumber\\data\\ShortNumberMetadata_MS' => __DIR__ . '/..' . '/giggsey/libphonenumber-for-php-lite/src/data/ShortNumberMetadata_MS.php', + 'libphonenumber\\data\\ShortNumberMetadata_MT' => __DIR__ . '/..' . '/giggsey/libphonenumber-for-php-lite/src/data/ShortNumberMetadata_MT.php', + 'libphonenumber\\data\\ShortNumberMetadata_MU' => __DIR__ . '/..' . '/giggsey/libphonenumber-for-php-lite/src/data/ShortNumberMetadata_MU.php', + 'libphonenumber\\data\\ShortNumberMetadata_MV' => __DIR__ . '/..' . '/giggsey/libphonenumber-for-php-lite/src/data/ShortNumberMetadata_MV.php', + 'libphonenumber\\data\\ShortNumberMetadata_MW' => __DIR__ . '/..' . '/giggsey/libphonenumber-for-php-lite/src/data/ShortNumberMetadata_MW.php', + 'libphonenumber\\data\\ShortNumberMetadata_MX' => __DIR__ . '/..' . '/giggsey/libphonenumber-for-php-lite/src/data/ShortNumberMetadata_MX.php', + 'libphonenumber\\data\\ShortNumberMetadata_MY' => __DIR__ . '/..' . '/giggsey/libphonenumber-for-php-lite/src/data/ShortNumberMetadata_MY.php', + 'libphonenumber\\data\\ShortNumberMetadata_MZ' => __DIR__ . '/..' . '/giggsey/libphonenumber-for-php-lite/src/data/ShortNumberMetadata_MZ.php', + 'libphonenumber\\data\\ShortNumberMetadata_NA' => __DIR__ . '/..' . '/giggsey/libphonenumber-for-php-lite/src/data/ShortNumberMetadata_NA.php', + 'libphonenumber\\data\\ShortNumberMetadata_NC' => __DIR__ . '/..' . '/giggsey/libphonenumber-for-php-lite/src/data/ShortNumberMetadata_NC.php', + 'libphonenumber\\data\\ShortNumberMetadata_NE' => __DIR__ . '/..' . '/giggsey/libphonenumber-for-php-lite/src/data/ShortNumberMetadata_NE.php', + 'libphonenumber\\data\\ShortNumberMetadata_NF' => __DIR__ . '/..' . '/giggsey/libphonenumber-for-php-lite/src/data/ShortNumberMetadata_NF.php', + 'libphonenumber\\data\\ShortNumberMetadata_NG' => __DIR__ . '/..' . '/giggsey/libphonenumber-for-php-lite/src/data/ShortNumberMetadata_NG.php', + 'libphonenumber\\data\\ShortNumberMetadata_NI' => __DIR__ . '/..' . '/giggsey/libphonenumber-for-php-lite/src/data/ShortNumberMetadata_NI.php', + 'libphonenumber\\data\\ShortNumberMetadata_NL' => __DIR__ . '/..' . '/giggsey/libphonenumber-for-php-lite/src/data/ShortNumberMetadata_NL.php', + 'libphonenumber\\data\\ShortNumberMetadata_NO' => __DIR__ . '/..' . '/giggsey/libphonenumber-for-php-lite/src/data/ShortNumberMetadata_NO.php', + 'libphonenumber\\data\\ShortNumberMetadata_NP' => __DIR__ . '/..' . '/giggsey/libphonenumber-for-php-lite/src/data/ShortNumberMetadata_NP.php', + 'libphonenumber\\data\\ShortNumberMetadata_NR' => __DIR__ . '/..' . '/giggsey/libphonenumber-for-php-lite/src/data/ShortNumberMetadata_NR.php', + 'libphonenumber\\data\\ShortNumberMetadata_NU' => __DIR__ . '/..' . '/giggsey/libphonenumber-for-php-lite/src/data/ShortNumberMetadata_NU.php', + 'libphonenumber\\data\\ShortNumberMetadata_NZ' => __DIR__ . '/..' . '/giggsey/libphonenumber-for-php-lite/src/data/ShortNumberMetadata_NZ.php', + 'libphonenumber\\data\\ShortNumberMetadata_OM' => __DIR__ . '/..' . '/giggsey/libphonenumber-for-php-lite/src/data/ShortNumberMetadata_OM.php', + 'libphonenumber\\data\\ShortNumberMetadata_PA' => __DIR__ . '/..' . '/giggsey/libphonenumber-for-php-lite/src/data/ShortNumberMetadata_PA.php', + 'libphonenumber\\data\\ShortNumberMetadata_PE' => __DIR__ . '/..' . '/giggsey/libphonenumber-for-php-lite/src/data/ShortNumberMetadata_PE.php', + 'libphonenumber\\data\\ShortNumberMetadata_PF' => __DIR__ . '/..' . '/giggsey/libphonenumber-for-php-lite/src/data/ShortNumberMetadata_PF.php', + 'libphonenumber\\data\\ShortNumberMetadata_PG' => __DIR__ . '/..' . '/giggsey/libphonenumber-for-php-lite/src/data/ShortNumberMetadata_PG.php', + 'libphonenumber\\data\\ShortNumberMetadata_PH' => __DIR__ . '/..' . '/giggsey/libphonenumber-for-php-lite/src/data/ShortNumberMetadata_PH.php', + 'libphonenumber\\data\\ShortNumberMetadata_PK' => __DIR__ . '/..' . '/giggsey/libphonenumber-for-php-lite/src/data/ShortNumberMetadata_PK.php', + 'libphonenumber\\data\\ShortNumberMetadata_PL' => __DIR__ . '/..' . '/giggsey/libphonenumber-for-php-lite/src/data/ShortNumberMetadata_PL.php', + 'libphonenumber\\data\\ShortNumberMetadata_PM' => __DIR__ . '/..' . '/giggsey/libphonenumber-for-php-lite/src/data/ShortNumberMetadata_PM.php', + 'libphonenumber\\data\\ShortNumberMetadata_PR' => __DIR__ . '/..' . '/giggsey/libphonenumber-for-php-lite/src/data/ShortNumberMetadata_PR.php', + 'libphonenumber\\data\\ShortNumberMetadata_PS' => __DIR__ . '/..' . '/giggsey/libphonenumber-for-php-lite/src/data/ShortNumberMetadata_PS.php', + 'libphonenumber\\data\\ShortNumberMetadata_PT' => __DIR__ . '/..' . '/giggsey/libphonenumber-for-php-lite/src/data/ShortNumberMetadata_PT.php', + 'libphonenumber\\data\\ShortNumberMetadata_PW' => __DIR__ . '/..' . '/giggsey/libphonenumber-for-php-lite/src/data/ShortNumberMetadata_PW.php', + 'libphonenumber\\data\\ShortNumberMetadata_PY' => __DIR__ . '/..' . '/giggsey/libphonenumber-for-php-lite/src/data/ShortNumberMetadata_PY.php', + 'libphonenumber\\data\\ShortNumberMetadata_QA' => __DIR__ . '/..' . '/giggsey/libphonenumber-for-php-lite/src/data/ShortNumberMetadata_QA.php', + 'libphonenumber\\data\\ShortNumberMetadata_RE' => __DIR__ . '/..' . '/giggsey/libphonenumber-for-php-lite/src/data/ShortNumberMetadata_RE.php', + 'libphonenumber\\data\\ShortNumberMetadata_RO' => __DIR__ . '/..' . '/giggsey/libphonenumber-for-php-lite/src/data/ShortNumberMetadata_RO.php', + 'libphonenumber\\data\\ShortNumberMetadata_RS' => __DIR__ . '/..' . '/giggsey/libphonenumber-for-php-lite/src/data/ShortNumberMetadata_RS.php', + 'libphonenumber\\data\\ShortNumberMetadata_RU' => __DIR__ . '/..' . '/giggsey/libphonenumber-for-php-lite/src/data/ShortNumberMetadata_RU.php', + 'libphonenumber\\data\\ShortNumberMetadata_RW' => __DIR__ . '/..' . '/giggsey/libphonenumber-for-php-lite/src/data/ShortNumberMetadata_RW.php', + 'libphonenumber\\data\\ShortNumberMetadata_SA' => __DIR__ . '/..' . '/giggsey/libphonenumber-for-php-lite/src/data/ShortNumberMetadata_SA.php', + 'libphonenumber\\data\\ShortNumberMetadata_SB' => __DIR__ . '/..' . '/giggsey/libphonenumber-for-php-lite/src/data/ShortNumberMetadata_SB.php', + 'libphonenumber\\data\\ShortNumberMetadata_SC' => __DIR__ . '/..' . '/giggsey/libphonenumber-for-php-lite/src/data/ShortNumberMetadata_SC.php', + 'libphonenumber\\data\\ShortNumberMetadata_SD' => __DIR__ . '/..' . '/giggsey/libphonenumber-for-php-lite/src/data/ShortNumberMetadata_SD.php', + 'libphonenumber\\data\\ShortNumberMetadata_SE' => __DIR__ . '/..' . '/giggsey/libphonenumber-for-php-lite/src/data/ShortNumberMetadata_SE.php', + 'libphonenumber\\data\\ShortNumberMetadata_SG' => __DIR__ . '/..' . '/giggsey/libphonenumber-for-php-lite/src/data/ShortNumberMetadata_SG.php', + 'libphonenumber\\data\\ShortNumberMetadata_SH' => __DIR__ . '/..' . '/giggsey/libphonenumber-for-php-lite/src/data/ShortNumberMetadata_SH.php', + 'libphonenumber\\data\\ShortNumberMetadata_SI' => __DIR__ . '/..' . '/giggsey/libphonenumber-for-php-lite/src/data/ShortNumberMetadata_SI.php', + 'libphonenumber\\data\\ShortNumberMetadata_SJ' => __DIR__ . '/..' . '/giggsey/libphonenumber-for-php-lite/src/data/ShortNumberMetadata_SJ.php', + 'libphonenumber\\data\\ShortNumberMetadata_SK' => __DIR__ . '/..' . '/giggsey/libphonenumber-for-php-lite/src/data/ShortNumberMetadata_SK.php', + 'libphonenumber\\data\\ShortNumberMetadata_SL' => __DIR__ . '/..' . '/giggsey/libphonenumber-for-php-lite/src/data/ShortNumberMetadata_SL.php', + 'libphonenumber\\data\\ShortNumberMetadata_SM' => __DIR__ . '/..' . '/giggsey/libphonenumber-for-php-lite/src/data/ShortNumberMetadata_SM.php', + 'libphonenumber\\data\\ShortNumberMetadata_SN' => __DIR__ . '/..' . '/giggsey/libphonenumber-for-php-lite/src/data/ShortNumberMetadata_SN.php', + 'libphonenumber\\data\\ShortNumberMetadata_SO' => __DIR__ . '/..' . '/giggsey/libphonenumber-for-php-lite/src/data/ShortNumberMetadata_SO.php', + 'libphonenumber\\data\\ShortNumberMetadata_SR' => __DIR__ . '/..' . '/giggsey/libphonenumber-for-php-lite/src/data/ShortNumberMetadata_SR.php', + 'libphonenumber\\data\\ShortNumberMetadata_SS' => __DIR__ . '/..' . '/giggsey/libphonenumber-for-php-lite/src/data/ShortNumberMetadata_SS.php', + 'libphonenumber\\data\\ShortNumberMetadata_ST' => __DIR__ . '/..' . '/giggsey/libphonenumber-for-php-lite/src/data/ShortNumberMetadata_ST.php', + 'libphonenumber\\data\\ShortNumberMetadata_SV' => __DIR__ . '/..' . '/giggsey/libphonenumber-for-php-lite/src/data/ShortNumberMetadata_SV.php', + 'libphonenumber\\data\\ShortNumberMetadata_SX' => __DIR__ . '/..' . '/giggsey/libphonenumber-for-php-lite/src/data/ShortNumberMetadata_SX.php', + 'libphonenumber\\data\\ShortNumberMetadata_SY' => __DIR__ . '/..' . '/giggsey/libphonenumber-for-php-lite/src/data/ShortNumberMetadata_SY.php', + 'libphonenumber\\data\\ShortNumberMetadata_SZ' => __DIR__ . '/..' . '/giggsey/libphonenumber-for-php-lite/src/data/ShortNumberMetadata_SZ.php', + 'libphonenumber\\data\\ShortNumberMetadata_TC' => __DIR__ . '/..' . '/giggsey/libphonenumber-for-php-lite/src/data/ShortNumberMetadata_TC.php', + 'libphonenumber\\data\\ShortNumberMetadata_TD' => __DIR__ . '/..' . '/giggsey/libphonenumber-for-php-lite/src/data/ShortNumberMetadata_TD.php', + 'libphonenumber\\data\\ShortNumberMetadata_TG' => __DIR__ . '/..' . '/giggsey/libphonenumber-for-php-lite/src/data/ShortNumberMetadata_TG.php', + 'libphonenumber\\data\\ShortNumberMetadata_TH' => __DIR__ . '/..' . '/giggsey/libphonenumber-for-php-lite/src/data/ShortNumberMetadata_TH.php', + 'libphonenumber\\data\\ShortNumberMetadata_TJ' => __DIR__ . '/..' . '/giggsey/libphonenumber-for-php-lite/src/data/ShortNumberMetadata_TJ.php', + 'libphonenumber\\data\\ShortNumberMetadata_TL' => __DIR__ . '/..' . '/giggsey/libphonenumber-for-php-lite/src/data/ShortNumberMetadata_TL.php', + 'libphonenumber\\data\\ShortNumberMetadata_TM' => __DIR__ . '/..' . '/giggsey/libphonenumber-for-php-lite/src/data/ShortNumberMetadata_TM.php', + 'libphonenumber\\data\\ShortNumberMetadata_TN' => __DIR__ . '/..' . '/giggsey/libphonenumber-for-php-lite/src/data/ShortNumberMetadata_TN.php', + 'libphonenumber\\data\\ShortNumberMetadata_TO' => __DIR__ . '/..' . '/giggsey/libphonenumber-for-php-lite/src/data/ShortNumberMetadata_TO.php', + 'libphonenumber\\data\\ShortNumberMetadata_TR' => __DIR__ . '/..' . '/giggsey/libphonenumber-for-php-lite/src/data/ShortNumberMetadata_TR.php', + 'libphonenumber\\data\\ShortNumberMetadata_TT' => __DIR__ . '/..' . '/giggsey/libphonenumber-for-php-lite/src/data/ShortNumberMetadata_TT.php', + 'libphonenumber\\data\\ShortNumberMetadata_TV' => __DIR__ . '/..' . '/giggsey/libphonenumber-for-php-lite/src/data/ShortNumberMetadata_TV.php', + 'libphonenumber\\data\\ShortNumberMetadata_TW' => __DIR__ . '/..' . '/giggsey/libphonenumber-for-php-lite/src/data/ShortNumberMetadata_TW.php', + 'libphonenumber\\data\\ShortNumberMetadata_TZ' => __DIR__ . '/..' . '/giggsey/libphonenumber-for-php-lite/src/data/ShortNumberMetadata_TZ.php', + 'libphonenumber\\data\\ShortNumberMetadata_UA' => __DIR__ . '/..' . '/giggsey/libphonenumber-for-php-lite/src/data/ShortNumberMetadata_UA.php', + 'libphonenumber\\data\\ShortNumberMetadata_UG' => __DIR__ . '/..' . '/giggsey/libphonenumber-for-php-lite/src/data/ShortNumberMetadata_UG.php', + 'libphonenumber\\data\\ShortNumberMetadata_US' => __DIR__ . '/..' . '/giggsey/libphonenumber-for-php-lite/src/data/ShortNumberMetadata_US.php', + 'libphonenumber\\data\\ShortNumberMetadata_UY' => __DIR__ . '/..' . '/giggsey/libphonenumber-for-php-lite/src/data/ShortNumberMetadata_UY.php', + 'libphonenumber\\data\\ShortNumberMetadata_UZ' => __DIR__ . '/..' . '/giggsey/libphonenumber-for-php-lite/src/data/ShortNumberMetadata_UZ.php', + 'libphonenumber\\data\\ShortNumberMetadata_VA' => __DIR__ . '/..' . '/giggsey/libphonenumber-for-php-lite/src/data/ShortNumberMetadata_VA.php', + 'libphonenumber\\data\\ShortNumberMetadata_VC' => __DIR__ . '/..' . '/giggsey/libphonenumber-for-php-lite/src/data/ShortNumberMetadata_VC.php', + 'libphonenumber\\data\\ShortNumberMetadata_VE' => __DIR__ . '/..' . '/giggsey/libphonenumber-for-php-lite/src/data/ShortNumberMetadata_VE.php', + 'libphonenumber\\data\\ShortNumberMetadata_VG' => __DIR__ . '/..' . '/giggsey/libphonenumber-for-php-lite/src/data/ShortNumberMetadata_VG.php', + 'libphonenumber\\data\\ShortNumberMetadata_VI' => __DIR__ . '/..' . '/giggsey/libphonenumber-for-php-lite/src/data/ShortNumberMetadata_VI.php', + 'libphonenumber\\data\\ShortNumberMetadata_VN' => __DIR__ . '/..' . '/giggsey/libphonenumber-for-php-lite/src/data/ShortNumberMetadata_VN.php', + 'libphonenumber\\data\\ShortNumberMetadata_VU' => __DIR__ . '/..' . '/giggsey/libphonenumber-for-php-lite/src/data/ShortNumberMetadata_VU.php', + 'libphonenumber\\data\\ShortNumberMetadata_WF' => __DIR__ . '/..' . '/giggsey/libphonenumber-for-php-lite/src/data/ShortNumberMetadata_WF.php', + 'libphonenumber\\data\\ShortNumberMetadata_WS' => __DIR__ . '/..' . '/giggsey/libphonenumber-for-php-lite/src/data/ShortNumberMetadata_WS.php', + 'libphonenumber\\data\\ShortNumberMetadata_XK' => __DIR__ . '/..' . '/giggsey/libphonenumber-for-php-lite/src/data/ShortNumberMetadata_XK.php', + 'libphonenumber\\data\\ShortNumberMetadata_YE' => __DIR__ . '/..' . '/giggsey/libphonenumber-for-php-lite/src/data/ShortNumberMetadata_YE.php', + 'libphonenumber\\data\\ShortNumberMetadata_YT' => __DIR__ . '/..' . '/giggsey/libphonenumber-for-php-lite/src/data/ShortNumberMetadata_YT.php', + 'libphonenumber\\data\\ShortNumberMetadata_ZA' => __DIR__ . '/..' . '/giggsey/libphonenumber-for-php-lite/src/data/ShortNumberMetadata_ZA.php', + 'libphonenumber\\data\\ShortNumberMetadata_ZM' => __DIR__ . '/..' . '/giggsey/libphonenumber-for-php-lite/src/data/ShortNumberMetadata_ZM.php', + 'libphonenumber\\data\\ShortNumberMetadata_ZW' => __DIR__ . '/..' . '/giggsey/libphonenumber-for-php-lite/src/data/ShortNumberMetadata_ZW.php', + 'ownCloud\\TarStreamer\\TarHeader' => __DIR__ . '/..' . '/deepdiver1975/tarstreamer/src/TarHeader.php', + 'ownCloud\\TarStreamer\\TarStreamer' => __DIR__ . '/..' . '/deepdiver1975/tarstreamer/src/TarStreamer.php', + 'phpseclib\\Crypt\\AES' => __DIR__ . '/..' . '/phpseclib/phpseclib/phpseclib/Crypt/AES.php', + 'phpseclib\\Crypt\\Base' => __DIR__ . '/..' . '/phpseclib/phpseclib/phpseclib/Crypt/Base.php', + 'phpseclib\\Crypt\\Blowfish' => __DIR__ . '/..' . '/phpseclib/phpseclib/phpseclib/Crypt/Blowfish.php', + 'phpseclib\\Crypt\\DES' => __DIR__ . '/..' . '/phpseclib/phpseclib/phpseclib/Crypt/DES.php', + 'phpseclib\\Crypt\\Hash' => __DIR__ . '/..' . '/phpseclib/phpseclib/phpseclib/Crypt/Hash.php', + 'phpseclib\\Crypt\\RC2' => __DIR__ . '/..' . '/phpseclib/phpseclib/phpseclib/Crypt/RC2.php', + 'phpseclib\\Crypt\\RC4' => __DIR__ . '/..' . '/phpseclib/phpseclib/phpseclib/Crypt/RC4.php', + 'phpseclib\\Crypt\\RSA' => __DIR__ . '/..' . '/phpseclib/phpseclib/phpseclib/Crypt/RSA.php', + 'phpseclib\\Crypt\\Random' => __DIR__ . '/..' . '/phpseclib/phpseclib/phpseclib/Crypt/Random.php', + 'phpseclib\\Crypt\\Rijndael' => __DIR__ . '/..' . '/phpseclib/phpseclib/phpseclib/Crypt/Rijndael.php', + 'phpseclib\\Crypt\\TripleDES' => __DIR__ . '/..' . '/phpseclib/phpseclib/phpseclib/Crypt/TripleDES.php', + 'phpseclib\\Crypt\\Twofish' => __DIR__ . '/..' . '/phpseclib/phpseclib/phpseclib/Crypt/Twofish.php', + 'phpseclib\\File\\ANSI' => __DIR__ . '/..' . '/phpseclib/phpseclib/phpseclib/File/ANSI.php', + 'phpseclib\\File\\ASN1' => __DIR__ . '/..' . '/phpseclib/phpseclib/phpseclib/File/ASN1.php', + 'phpseclib\\File\\ASN1\\Element' => __DIR__ . '/..' . '/phpseclib/phpseclib/phpseclib/File/ASN1/Element.php', + 'phpseclib\\File\\X509' => __DIR__ . '/..' . '/phpseclib/phpseclib/phpseclib/File/X509.php', + 'phpseclib\\Math\\BigInteger' => __DIR__ . '/..' . '/phpseclib/phpseclib/phpseclib/Math/BigInteger.php', + 'phpseclib\\Net\\SCP' => __DIR__ . '/..' . '/phpseclib/phpseclib/phpseclib/Net/SCP.php', + 'phpseclib\\Net\\SFTP' => __DIR__ . '/..' . '/phpseclib/phpseclib/phpseclib/Net/SFTP.php', + 'phpseclib\\Net\\SFTP\\Stream' => __DIR__ . '/..' . '/phpseclib/phpseclib/phpseclib/Net/SFTP/Stream.php', + 'phpseclib\\Net\\SSH1' => __DIR__ . '/..' . '/phpseclib/phpseclib/phpseclib/Net/SSH1.php', + 'phpseclib\\Net\\SSH2' => __DIR__ . '/..' . '/phpseclib/phpseclib/phpseclib/Net/SSH2.php', + 'phpseclib\\System\\SSH\\Agent' => __DIR__ . '/..' . '/phpseclib/phpseclib/phpseclib/System/SSH/Agent.php', + 'phpseclib\\System\\SSH\\Agent\\Identity' => __DIR__ . '/..' . '/phpseclib/phpseclib/phpseclib/System/SSH/Agent/Identity.php', + 'wapmorgan\\Mp3Info\\Mp3Info' => __DIR__ . '/..' . '/wapmorgan/mp3info/src/Mp3Info.php', + ); + + public static function getInitializer(ClassLoader $loader) + { + return \Closure::bind(function () use ($loader) { + $loader->prefixLengthsPsr4 = ComposerStaticInit2f23f73bc0cc116b4b1eee1521aa8652::$prefixLengthsPsr4; + $loader->prefixDirsPsr4 = ComposerStaticInit2f23f73bc0cc116b4b1eee1521aa8652::$prefixDirsPsr4; + $loader->prefixesPsr0 = ComposerStaticInit2f23f73bc0cc116b4b1eee1521aa8652::$prefixesPsr0; + $loader->classMap = ComposerStaticInit2f23f73bc0cc116b4b1eee1521aa8652::$classMap; + + }, null, ClassLoader::class); + } +} diff --git a/3rdparty/composer/include_paths.php b/3rdparty/composer/include_paths.php new file mode 100644 index 00000000..8c87e2c2 --- /dev/null +++ b/3rdparty/composer/include_paths.php @@ -0,0 +1,13 @@ +=5.5" + }, + "require-dev": { + "phpunit/phpunit": "^4.8.35||^5.6.3||^9.5", + "yoast/phpunit-polyfills": "^1.0" + }, + "suggest": { + "ext-awscrt": "Make sure you install awscrt native extension to use any of the functionality." + }, + "time": "2024-10-18T22:15:13+00:00", + "type": "library", + "installation-source": "dist", + "autoload": { + "classmap": [ + "src/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "Apache-2.0" + ], + "authors": [ + { + "name": "AWS SDK Common Runtime Team", + "email": "aws-sdk-common-runtime@amazon.com" + } + ], + "description": "AWS Common Runtime for PHP", + "homepage": "https://github.com/awslabs/aws-crt-php", + "keywords": [ + "amazon", + "aws", + "crt", + "sdk" + ], + "support": { + "issues": "https://github.com/awslabs/aws-crt-php/issues", + "source": "https://github.com/awslabs/aws-crt-php/tree/v1.2.7" + }, + "install-path": "../aws/aws-crt-php" + }, + { + "name": "aws/aws-sdk-php", + "version": "3.349.3", + "version_normalized": "3.349.3.0", + "source": { + "type": "git", + "url": "https://github.com/aws/aws-sdk-php.git", + "reference": "b2d4718786398f47626add9c29840fc416175ef2" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/aws/aws-sdk-php/zipball/b2d4718786398f47626add9c29840fc416175ef2", + "reference": "b2d4718786398f47626add9c29840fc416175ef2", + "shasum": "" + }, + "require": { + "aws/aws-crt-php": "^1.2.3", + "ext-json": "*", + "ext-pcre": "*", + "ext-simplexml": "*", + "guzzlehttp/guzzle": "^7.4.5", + "guzzlehttp/promises": "^2.0", + "guzzlehttp/psr7": "^2.4.5", + "mtdowling/jmespath.php": "^2.8.0", + "php": ">=8.1", + "psr/http-message": "^2.0" + }, + "require-dev": { + "andrewsville/php-token-reflection": "^1.4", + "aws/aws-php-sns-message-validator": "~1.0", + "behat/behat": "~3.0", + "composer/composer": "^2.7.8", + "dms/phpunit-arraysubset-asserts": "^0.4.0", + "doctrine/cache": "~1.4", + "ext-dom": "*", + "ext-openssl": "*", + "ext-pcntl": "*", + "ext-sockets": "*", + "phpunit/phpunit": "^5.6.3 || ^8.5 || ^9.5", + "psr/cache": "^2.0 || ^3.0", + "psr/simple-cache": "^2.0 || ^3.0", + "sebastian/comparator": "^1.2.3 || ^4.0 || ^5.0", + "symfony/filesystem": "^v6.4.0 || ^v7.1.0", + "yoast/phpunit-polyfills": "^2.0" + }, + "suggest": { + "aws/aws-php-sns-message-validator": "To validate incoming SNS notifications", + "doctrine/cache": "To use the DoctrineCacheAdapter", + "ext-curl": "To send requests using cURL", + "ext-openssl": "Allows working with CloudFront private distributions and verifying received SNS messages", + "ext-sockets": "To use client-side monitoring" + }, + "time": "2025-07-09T18:10:17+00:00", + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "3.0-dev" + } + }, + "installation-source": "dist", + "autoload": { + "files": [ + "src/functions.php" + ], + "psr-4": { + "Aws\\": "src/" + }, + "exclude-from-classmap": [ + "src/data/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "Apache-2.0" + ], + "authors": [ + { + "name": "Amazon Web Services", + "homepage": "http://aws.amazon.com" + } + ], + "description": "AWS SDK for PHP - Use Amazon Web Services in your PHP project", + "homepage": "http://aws.amazon.com/sdkforphp", + "keywords": [ + "amazon", + "aws", + "cloud", + "dynamodb", + "ec2", + "glacier", + "s3", + "sdk" + ], + "support": { + "forum": "https://github.com/aws/aws-sdk-php/discussions", + "issues": "https://github.com/aws/aws-sdk-php/issues", + "source": "https://github.com/aws/aws-sdk-php/tree/3.349.3" + }, + "install-path": "../aws/aws-sdk-php" + }, + { + "name": "bantu/ini-get-wrapper", + "version": "v1.0.1", + "version_normalized": "1.0.1.0", + "source": { + "type": "git", + "url": "https://github.com/bantuXorg/php-ini-get-wrapper.git", + "reference": "4770c7feab370c62e23db4f31c112b7c6d90aee2" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/bantuXorg/php-ini-get-wrapper/zipball/4770c7feab370c62e23db4f31c112b7c6d90aee2", + "reference": "4770c7feab370c62e23db4f31c112b7c6d90aee2", + "shasum": "" + }, + "require-dev": { + "phpunit/phpunit": "3.7.*" + }, + "time": "2014-09-15T13:12:35+00:00", + "type": "library", + "installation-source": "dist", + "autoload": { + "psr-4": { + "bantu\\IniGetWrapper\\": "src/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "description": "Convenience wrapper around ini_get()", + "support": { + "issues": "https://github.com/bantuXorg/php-ini-get-wrapper/issues", + "source": "https://github.com/bantuXorg/php-ini-get-wrapper/tree/v1.0.1" + }, + "install-path": "../bantu/ini-get-wrapper" + }, + { + "name": "brick/math", + "version": "0.12.1", + "version_normalized": "0.12.1.0", + "source": { + "type": "git", + "url": "https://github.com/brick/math.git", + "reference": "f510c0a40911935b77b86859eb5223d58d660df1" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/brick/math/zipball/f510c0a40911935b77b86859eb5223d58d660df1", + "reference": "f510c0a40911935b77b86859eb5223d58d660df1", + "shasum": "" + }, + "require": { + "php": "^8.1" + }, + "require-dev": { + "php-coveralls/php-coveralls": "^2.2", + "phpunit/phpunit": "^10.1", + "vimeo/psalm": "5.16.0" + }, + "time": "2023-11-29T23:19:16+00:00", + "type": "library", + "installation-source": "dist", + "autoload": { + "psr-4": { + "Brick\\Math\\": "src/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "description": "Arbitrary-precision arithmetic library", + "keywords": [ + "Arbitrary-precision", + "BigInteger", + "BigRational", + "arithmetic", + "bigdecimal", + "bignum", + "bignumber", + "brick", + "decimal", + "integer", + "math", + "mathematics", + "rational" + ], + "support": { + "issues": "https://github.com/brick/math/issues", + "source": "https://github.com/brick/math/tree/0.12.1" + }, + "funding": [ + { + "url": "https://github.com/BenMorel", + "type": "github" + } + ], + "install-path": "../brick/math" + }, + { + "name": "cweagans/composer-patches", + "version": "1.7.3", + "version_normalized": "1.7.3.0", + "source": { + "type": "git", + "url": "https://github.com/cweagans/composer-patches.git", + "reference": "e190d4466fe2b103a55467dfa83fc2fecfcaf2db" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/cweagans/composer-patches/zipball/e190d4466fe2b103a55467dfa83fc2fecfcaf2db", + "reference": "e190d4466fe2b103a55467dfa83fc2fecfcaf2db", + "shasum": "" + }, + "require": { + "composer-plugin-api": "^1.0 || ^2.0", + "php": ">=5.3.0" + }, + "require-dev": { + "composer/composer": "~1.0 || ~2.0", + "phpunit/phpunit": "~4.6" + }, + "time": "2022-12-20T22:53:13+00:00", + "type": "composer-plugin", + "extra": { + "class": "cweagans\\Composer\\Patches" + }, + "installation-source": "dist", + "autoload": { + "psr-4": { + "cweagans\\Composer\\": "src" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "BSD-3-Clause" + ], + "authors": [ + { + "name": "Cameron Eagans", + "email": "me@cweagans.net" + } + ], + "description": "Provides a way to patch Composer packages.", + "support": { + "issues": "https://github.com/cweagans/composer-patches/issues", + "source": "https://github.com/cweagans/composer-patches/tree/1.7.3" + }, + "install-path": "../cweagans/composer-patches" + }, + { + "name": "deepdiver/zipstreamer", + "version": "v2.0.3", + "version_normalized": "2.0.3.0", + "source": { + "type": "git", + "url": "https://github.com/DeepDiver1975/PHPZipStreamer.git", + "reference": "b9d1f53453a5736285facb723252ea2169dc472e" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/DeepDiver1975/PHPZipStreamer/zipball/b9d1f53453a5736285facb723252ea2169dc472e", + "reference": "b9d1f53453a5736285facb723252ea2169dc472e", + "shasum": "" + }, + "require": { + "ext-mbstring": "*", + "php": ">=7.1" + }, + "require-dev": { + "ext-xdebug": "*", + "ext-zlib": "*", + "phpunit/phpunit": "^7 || ^8" + }, + "suggest": { + "ext-http": ">=0.10" + }, + "time": "2024-03-13T14:30:52+00:00", + "type": "library", + "installation-source": "dist", + "autoload": { + "psr-4": { + "ZipStreamer\\": "src/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "GPL-3.0-or-later" + ], + "authors": [ + { + "name": "Nicolai Ehemann", + "email": "en@enlightened.de", + "role": "Author/Maintainer" + }, + { + "name": "André Rothe", + "email": "arothe@zks.uni-leipzig.de", + "role": "Contributor" + }, + { + "name": "Lukas Reschke", + "email": "lukas@owncloud.com", + "role": "Contributor" + }, + { + "name": "Thomas Müller", + "email": "thomas.mueller@tmit.eu", + "role": "Contributor" + }, + { + "name": "Roeland Jago Douma", + "email": "roeland@famdouma.nl", + "role": "Contributor" + } + ], + "description": "Stream zip files without i/o overhead", + "homepage": "https://github.com/DeepDiver1975/PHPZipStreamer", + "keywords": [ + "stream", + "zip" + ], + "support": { + "issues": "https://github.com/DeepDiver1975/PHPZipStreamer/issues", + "source": "https://github.com/DeepDiver1975/PHPZipStreamer/tree/v2.0.3" + }, + "funding": [ + { + "url": "https://github.com/DeepDiver1975", + "type": "github" + } + ], + "install-path": "../deepdiver/zipstreamer" + }, + { + "name": "deepdiver1975/tarstreamer", + "version": "v2.1.0", + "version_normalized": "2.1.0.0", + "source": { + "type": "git", + "url": "https://github.com/owncloud/TarStreamer.git", + "reference": "163052d7a076fd3dd54d4f50e1ff2705b72604db" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/owncloud/TarStreamer/zipball/163052d7a076fd3dd54d4f50e1ff2705b72604db", + "reference": "163052d7a076fd3dd54d4f50e1ff2705b72604db", + "shasum": "" + }, + "require": { + "php": ">=7.1" + }, + "require-dev": { + "bamarni/composer-bin-plugin": "^1.5", + "pear/archive_tar": "~1.4", + "pear/pear-core-minimal": "v1.10.13", + "phpunit/phpunit": "^7.5|^8.5|^9.6" + }, + "time": "2023-06-16T08:01:55+00:00", + "type": "library", + "extra": { + "bamarni-bin": { + "bin-links": false + } + }, + "installation-source": "dist", + "autoload": { + "psr-4": { + "ownCloud\\TarStreamer\\": "src/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "description": "A library for dynamically streaming dynamic tar files without the need to have the complete file stored on the server.", + "homepage": "https://github.com/owncloud/TarStreamer", + "keywords": [ + "archive", + "php", + "stream", + "tar" + ], + "support": { + "issues": "https://github.com/owncloud/TarStreamer/issues", + "source": "https://github.com/owncloud/TarStreamer/tree/v2.1.0" + }, + "install-path": "../deepdiver1975/tarstreamer" + }, + { + "name": "doctrine/dbal", + "version": "3.10.2", + "version_normalized": "3.10.2.0", + "source": { + "type": "git", + "url": "https://github.com/doctrine/dbal.git", + "reference": "c6c16cf787eaba3112203dfcd715fa2059c62282" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/doctrine/dbal/zipball/c6c16cf787eaba3112203dfcd715fa2059c62282", + "reference": "c6c16cf787eaba3112203dfcd715fa2059c62282", + "shasum": "" + }, + "require": { + "composer-runtime-api": "^2", + "doctrine/deprecations": "^0.5.3|^1", + "doctrine/event-manager": "^1|^2", + "php": "^7.4 || ^8.0", + "psr/cache": "^1|^2|^3", + "psr/log": "^1|^2|^3" + }, + "conflict": { + "doctrine/cache": "< 1.11" + }, + "require-dev": { + "doctrine/cache": "^1.11|^2.0", + "doctrine/coding-standard": "13.0.1", + "fig/log-test": "^1", + "jetbrains/phpstorm-stubs": "2023.1", + "phpstan/phpstan": "2.1.22", + "phpstan/phpstan-strict-rules": "^2", + "phpunit/phpunit": "9.6.23", + "slevomat/coding-standard": "8.16.2", + "squizlabs/php_codesniffer": "3.13.1", + "symfony/cache": "^5.4|^6.0|^7.0", + "symfony/console": "^4.4|^5.4|^6.0|^7.0" + }, + "suggest": { + "symfony/console": "For helpful console commands such as SQL execution and import of files." + }, + "time": "2025-09-04T23:51:27+00:00", + "bin": [ + "bin/doctrine-dbal" + ], + "type": "library", + "installation-source": "dist", + "autoload": { + "psr-4": { + "Doctrine\\DBAL\\": "src" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Guilherme Blanco", + "email": "guilhermeblanco@gmail.com" + }, + { + "name": "Roman Borschel", + "email": "roman@code-factory.org" + }, + { + "name": "Benjamin Eberlei", + "email": "kontakt@beberlei.de" + }, + { + "name": "Jonathan Wage", + "email": "jonwage@gmail.com" + } + ], + "description": "Powerful PHP database abstraction layer (DBAL) with many features for database schema introspection and management.", + "homepage": "https://www.doctrine-project.org/projects/dbal.html", + "keywords": [ + "abstraction", + "database", + "db2", + "dbal", + "mariadb", + "mssql", + "mysql", + "oci8", + "oracle", + "pdo", + "pgsql", + "postgresql", + "queryobject", + "sasql", + "sql", + "sqlite", + "sqlserver", + "sqlsrv" + ], + "support": { + "issues": "https://github.com/doctrine/dbal/issues", + "source": "https://github.com/doctrine/dbal/tree/3.10.2" + }, + "funding": [ + { + "url": "https://www.doctrine-project.org/sponsorship.html", + "type": "custom" + }, + { + "url": "https://www.patreon.com/phpdoctrine", + "type": "patreon" + }, + { + "url": "https://tidelift.com/funding/github/packagist/doctrine%2Fdbal", + "type": "tidelift" + } + ], + "install-path": "../doctrine/dbal" + }, + { + "name": "doctrine/deprecations", + "version": "1.1.5", + "version_normalized": "1.1.5.0", + "source": { + "type": "git", + "url": "https://github.com/doctrine/deprecations.git", + "reference": "459c2f5dd3d6a4633d3b5f46ee2b1c40f57d3f38" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/doctrine/deprecations/zipball/459c2f5dd3d6a4633d3b5f46ee2b1c40f57d3f38", + "reference": "459c2f5dd3d6a4633d3b5f46ee2b1c40f57d3f38", + "shasum": "" + }, + "require": { + "php": "^7.1 || ^8.0" + }, + "conflict": { + "phpunit/phpunit": "<=7.5 || >=13" + }, + "require-dev": { + "doctrine/coding-standard": "^9 || ^12 || ^13", + "phpstan/phpstan": "1.4.10 || 2.1.11", + "phpstan/phpstan-phpunit": "^1.0 || ^2", + "phpunit/phpunit": "^7.5 || ^8.5 || ^9.6 || ^10.5 || ^11.5 || ^12", + "psr/log": "^1 || ^2 || ^3" + }, + "suggest": { + "psr/log": "Allows logging deprecations via PSR-3 logger implementation" + }, + "time": "2025-04-07T20:06:18+00:00", + "type": "library", + "installation-source": "dist", + "autoload": { + "psr-4": { + "Doctrine\\Deprecations\\": "src" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "description": "A small layer on top of trigger_error(E_USER_DEPRECATED) or PSR-3 logging with options to disable all deprecations or selectively for packages.", + "homepage": "https://www.doctrine-project.org/", + "support": { + "issues": "https://github.com/doctrine/deprecations/issues", + "source": "https://github.com/doctrine/deprecations/tree/1.1.5" + }, + "install-path": "../doctrine/deprecations" + }, + { + "name": "doctrine/event-manager", + "version": "2.0.1", + "version_normalized": "2.0.1.0", + "source": { + "type": "git", + "url": "https://github.com/doctrine/event-manager.git", + "reference": "b680156fa328f1dfd874fd48c7026c41570b9c6e" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/doctrine/event-manager/zipball/b680156fa328f1dfd874fd48c7026c41570b9c6e", + "reference": "b680156fa328f1dfd874fd48c7026c41570b9c6e", + "shasum": "" + }, + "require": { + "php": "^8.1" + }, + "conflict": { + "doctrine/common": "<2.9" + }, + "require-dev": { + "doctrine/coding-standard": "^12", + "phpstan/phpstan": "^1.8.8", + "phpunit/phpunit": "^10.5", + "vimeo/psalm": "^5.24" + }, + "time": "2024-05-22T20:47:39+00:00", + "type": "library", + "installation-source": "dist", + "autoload": { + "psr-4": { + "Doctrine\\Common\\": "src" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Guilherme Blanco", + "email": "guilhermeblanco@gmail.com" + }, + { + "name": "Roman Borschel", + "email": "roman@code-factory.org" + }, + { + "name": "Benjamin Eberlei", + "email": "kontakt@beberlei.de" + }, + { + "name": "Jonathan Wage", + "email": "jonwage@gmail.com" + }, + { + "name": "Johannes Schmitt", + "email": "schmittjoh@gmail.com" + }, + { + "name": "Marco Pivetta", + "email": "ocramius@gmail.com" + } + ], + "description": "The Doctrine Event Manager is a simple PHP event system that was built to be used with the various Doctrine projects.", + "homepage": "https://www.doctrine-project.org/projects/event-manager.html", + "keywords": [ + "event", + "event dispatcher", + "event manager", + "event system", + "events" + ], + "support": { + "issues": "https://github.com/doctrine/event-manager/issues", + "source": "https://github.com/doctrine/event-manager/tree/2.0.1" + }, + "funding": [ + { + "url": "https://www.doctrine-project.org/sponsorship.html", + "type": "custom" + }, + { + "url": "https://www.patreon.com/phpdoctrine", + "type": "patreon" + }, + { + "url": "https://tidelift.com/funding/github/packagist/doctrine%2Fevent-manager", + "type": "tidelift" + } + ], + "install-path": "../doctrine/event-manager" + }, + { + "name": "doctrine/lexer", + "version": "3.0.1", + "version_normalized": "3.0.1.0", + "source": { + "type": "git", + "url": "https://github.com/doctrine/lexer.git", + "reference": "31ad66abc0fc9e1a1f2d9bc6a42668d2fbbcd6dd" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/doctrine/lexer/zipball/31ad66abc0fc9e1a1f2d9bc6a42668d2fbbcd6dd", + "reference": "31ad66abc0fc9e1a1f2d9bc6a42668d2fbbcd6dd", + "shasum": "" + }, + "require": { + "php": "^8.1" + }, + "require-dev": { + "doctrine/coding-standard": "^12", + "phpstan/phpstan": "^1.10", + "phpunit/phpunit": "^10.5", + "psalm/plugin-phpunit": "^0.18.3", + "vimeo/psalm": "^5.21" + }, + "time": "2024-02-05T11:56:58+00:00", + "type": "library", + "installation-source": "dist", + "autoload": { + "psr-4": { + "Doctrine\\Common\\Lexer\\": "src" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Guilherme Blanco", + "email": "guilhermeblanco@gmail.com" + }, + { + "name": "Roman Borschel", + "email": "roman@code-factory.org" + }, + { + "name": "Johannes Schmitt", + "email": "schmittjoh@gmail.com" + } + ], + "description": "PHP Doctrine Lexer parser library that can be used in Top-Down, Recursive Descent Parsers.", + "homepage": "https://www.doctrine-project.org/projects/lexer.html", + "keywords": [ + "annotations", + "docblock", + "lexer", + "parser", + "php" + ], + "support": { + "issues": "https://github.com/doctrine/lexer/issues", + "source": "https://github.com/doctrine/lexer/tree/3.0.1" + }, + "funding": [ + { + "url": "https://www.doctrine-project.org/sponsorship.html", + "type": "custom" + }, + { + "url": "https://www.patreon.com/phpdoctrine", + "type": "patreon" + }, + { + "url": "https://tidelift.com/funding/github/packagist/doctrine%2Flexer", + "type": "tidelift" + } + ], + "install-path": "../doctrine/lexer" + }, + { + "name": "egulias/email-validator", + "version": "4.0.4", + "version_normalized": "4.0.4.0", + "source": { + "type": "git", + "url": "https://github.com/egulias/EmailValidator.git", + "reference": "d42c8731f0624ad6bdc8d3e5e9a4524f68801cfa" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/egulias/EmailValidator/zipball/d42c8731f0624ad6bdc8d3e5e9a4524f68801cfa", + "reference": "d42c8731f0624ad6bdc8d3e5e9a4524f68801cfa", + "shasum": "" + }, + "require": { + "doctrine/lexer": "^2.0 || ^3.0", + "php": ">=8.1", + "symfony/polyfill-intl-idn": "^1.26" + }, + "require-dev": { + "phpunit/phpunit": "^10.2", + "vimeo/psalm": "^5.12" + }, + "suggest": { + "ext-intl": "PHP Internationalization Libraries are required to use the SpoofChecking validation" + }, + "time": "2025-03-06T22:45:56+00:00", + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "4.0.x-dev" + } + }, + "installation-source": "dist", + "autoload": { + "psr-4": { + "Egulias\\EmailValidator\\": "src" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Eduardo Gulias Davis" + } + ], + "description": "A library for validating emails against several RFCs", + "homepage": "https://github.com/egulias/EmailValidator", + "keywords": [ + "email", + "emailvalidation", + "emailvalidator", + "validation", + "validator" + ], + "support": { + "issues": "https://github.com/egulias/EmailValidator/issues", + "source": "https://github.com/egulias/EmailValidator/tree/4.0.4" + }, + "funding": [ + { + "url": "https://github.com/egulias", + "type": "github" + } + ], + "install-path": "../egulias/email-validator" + }, + { + "name": "fusonic/opengraph", + "version": "v3.0.0", + "version_normalized": "3.0.0.0", + "source": { + "type": "git", + "url": "https://github.com/fusonic/opengraph.git", + "reference": "2daa6dce84f23b1bb6d66bf03b3e9371c39cd378" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/fusonic/opengraph/zipball/2daa6dce84f23b1bb6d66bf03b3e9371c39cd378", + "reference": "2daa6dce84f23b1bb6d66bf03b3e9371c39cd378", + "shasum": "" + }, + "require": { + "ext-dom": "*", + "php": "^8.1", + "psr/http-client": "^1.0", + "psr/http-factory": "^1.0", + "symfony/css-selector": "^5.4 || ^6.4 || ^7.1", + "symfony/dom-crawler": "^5.4 || ^6.4 || ^7.1" + }, + "require-dev": { + "friendsofphp/php-cs-fixer": "^3.65", + "nyholm/psr7": "^1.8", + "phpstan/phpstan": "^2.0", + "phpstan/phpstan-deprecation-rules": "^2.0", + "phpstan/phpstan-phpunit": "^2.0", + "phpstan/phpstan-strict-rules": "^2.0", + "phpunit/phpunit": "^10.5 || ^11.4", + "symfony/http-client": "^5.4 || ^6.4 || ^7.1" + }, + "suggest": { + "nyholm/psr7": "^1.8", + "symfony/http-client": "^5.4 || ^6.4 || ^7.1" + }, + "time": "2025-01-13T07:23:24+00:00", + "type": "library", + "installation-source": "dist", + "autoload": { + "psr-4": { + "Fusonic\\OpenGraph\\": "src/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Fusonic", + "homepage": "https://www.fusonic.net" + } + ], + "description": "PHP library for consuming and publishing Open Graph resources.", + "homepage": "https://github.com/fusonic/opengraph", + "keywords": [ + "opengraph" + ], + "support": { + "issues": "https://github.com/fusonic/opengraph/issues", + "source": "https://github.com/fusonic/opengraph/tree/v3.0.0" + }, + "install-path": "../fusonic/opengraph" + }, + { + "name": "giggsey/libphonenumber-for-php-lite", + "version": "9.0.17", + "version_normalized": "9.0.17.0", + "source": { + "type": "git", + "url": "https://github.com/giggsey/libphonenumber-for-php-lite.git", + "reference": "430a602c6e5a03932b732226daf64aeeab5c6c65" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/giggsey/libphonenumber-for-php-lite/zipball/430a602c6e5a03932b732226daf64aeeab5c6c65", + "reference": "430a602c6e5a03932b732226daf64aeeab5c6c65", + "shasum": "" + }, + "require": { + "php": "^8.1", + "symfony/polyfill-mbstring": "^1.17" + }, + "conflict": { + "giggsey/libphonenumber-for-php": "*" + }, + "require-dev": { + "ext-dom": "*", + "friendsofphp/php-cs-fixer": "^3.71", + "infection/infection": "^0.29|^0.31.0", + "nette/php-generator": "^4.1", + "php-coveralls/php-coveralls": "^2.7", + "phpstan/extension-installer": "^1.4.3", + "phpstan/phpstan": "^2.1.7", + "phpstan/phpstan-deprecation-rules": "^2.0.1", + "phpstan/phpstan-phpunit": "^2.0.4", + "phpstan/phpstan-strict-rules": "^2.0.3", + "phpunit/phpunit": "^10.5.45", + "symfony/console": "^6.4", + "symfony/filesystem": "^6.4", + "symfony/process": "^6.4" + }, + "suggest": { + "giggsey/libphonenumber-for-php": "Use libphonenumber-for-php for geocoding, carriers, timezones and matching" + }, + "time": "2025-10-24T07:09:56+00:00", + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "9.x-dev" + } + }, + "installation-source": "dist", + "autoload": { + "psr-4": { + "libphonenumber\\": "src/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "Apache-2.0" + ], + "authors": [ + { + "name": "Joshua Gigg", + "email": "giggsey@gmail.com", + "homepage": "https://giggsey.com/" + } + ], + "description": "A lite version of giggsey/libphonenumber-for-php, which is a PHP Port of Google's libphonenumber", + "homepage": "https://github.com/giggsey/libphonenumber-for-php-lite", + "keywords": [ + "geocoding", + "geolocation", + "libphonenumber", + "mobile", + "phonenumber", + "validation" + ], + "support": { + "issues": "https://github.com/giggsey/libphonenumber-for-php-lite/issues", + "source": "https://github.com/giggsey/libphonenumber-for-php-lite" + }, + "install-path": "../giggsey/libphonenumber-for-php-lite" + }, + { + "name": "guzzlehttp/guzzle", + "version": "7.9.3", + "version_normalized": "7.9.3.0", + "source": { + "type": "git", + "url": "https://github.com/guzzle/guzzle.git", + "reference": "7b2f29fe81dc4da0ca0ea7d42107a0845946ea77" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/guzzle/guzzle/zipball/7b2f29fe81dc4da0ca0ea7d42107a0845946ea77", + "reference": "7b2f29fe81dc4da0ca0ea7d42107a0845946ea77", + "shasum": "" + }, + "require": { + "ext-json": "*", + "guzzlehttp/promises": "^1.5.3 || ^2.0.3", + "guzzlehttp/psr7": "^2.7.0", + "php": "^7.2.5 || ^8.0", + "psr/http-client": "^1.0", + "symfony/deprecation-contracts": "^2.2 || ^3.0" + }, + "provide": { + "psr/http-client-implementation": "1.0" + }, + "require-dev": { + "bamarni/composer-bin-plugin": "^1.8.2", + "ext-curl": "*", + "guzzle/client-integration-tests": "3.0.2", + "php-http/message-factory": "^1.1", + "phpunit/phpunit": "^8.5.39 || ^9.6.20", + "psr/log": "^1.1 || ^2.0 || ^3.0" + }, + "suggest": { + "ext-curl": "Required for CURL handler support", + "ext-intl": "Required for Internationalized Domain Name (IDN) support", + "psr/log": "Required for using the Log middleware" + }, + "time": "2025-03-27T13:37:11+00:00", + "type": "library", + "extra": { + "bamarni-bin": { + "bin-links": true, + "forward-command": false + } + }, + "installation-source": "dist", + "autoload": { + "files": [ + "src/functions_include.php" + ], + "psr-4": { + "GuzzleHttp\\": "src/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Graham Campbell", + "email": "hello@gjcampbell.co.uk", + "homepage": "https://github.com/GrahamCampbell" + }, + { + "name": "Michael Dowling", + "email": "mtdowling@gmail.com", + "homepage": "https://github.com/mtdowling" + }, + { + "name": "Jeremy Lindblom", + "email": "jeremeamia@gmail.com", + "homepage": "https://github.com/jeremeamia" + }, + { + "name": "George Mponos", + "email": "gmponos@gmail.com", + "homepage": "https://github.com/gmponos" + }, + { + "name": "Tobias Nyholm", + "email": "tobias.nyholm@gmail.com", + "homepage": "https://github.com/Nyholm" + }, + { + "name": "Márk Sági-Kazár", + "email": "mark.sagikazar@gmail.com", + "homepage": "https://github.com/sagikazarmark" + }, + { + "name": "Tobias Schultze", + "email": "webmaster@tubo-world.de", + "homepage": "https://github.com/Tobion" + } + ], + "description": "Guzzle is a PHP HTTP client library", + "keywords": [ + "client", + "curl", + "framework", + "http", + "http client", + "psr-18", + "psr-7", + "rest", + "web service" + ], + "support": { + "issues": "https://github.com/guzzle/guzzle/issues", + "source": "https://github.com/guzzle/guzzle/tree/7.9.3" + }, + "funding": [ + { + "url": "https://github.com/GrahamCampbell", + "type": "github" + }, + { + "url": "https://github.com/Nyholm", + "type": "github" + }, + { + "url": "https://tidelift.com/funding/github/packagist/guzzlehttp/guzzle", + "type": "tidelift" + } + ], + "install-path": "../guzzlehttp/guzzle" + }, + { + "name": "guzzlehttp/promises", + "version": "2.2.0", + "version_normalized": "2.2.0.0", + "source": { + "type": "git", + "url": "https://github.com/guzzle/promises.git", + "reference": "7c69f28996b0a6920945dd20b3857e499d9ca96c" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/guzzle/promises/zipball/7c69f28996b0a6920945dd20b3857e499d9ca96c", + "reference": "7c69f28996b0a6920945dd20b3857e499d9ca96c", + "shasum": "" + }, + "require": { + "php": "^7.2.5 || ^8.0" + }, + "require-dev": { + "bamarni/composer-bin-plugin": "^1.8.2", + "phpunit/phpunit": "^8.5.39 || ^9.6.20" + }, + "time": "2025-03-27T13:27:01+00:00", + "type": "library", + "extra": { + "bamarni-bin": { + "bin-links": true, + "forward-command": false + } + }, + "installation-source": "dist", + "autoload": { + "psr-4": { + "GuzzleHttp\\Promise\\": "src/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Graham Campbell", + "email": "hello@gjcampbell.co.uk", + "homepage": "https://github.com/GrahamCampbell" + }, + { + "name": "Michael Dowling", + "email": "mtdowling@gmail.com", + "homepage": "https://github.com/mtdowling" + }, + { + "name": "Tobias Nyholm", + "email": "tobias.nyholm@gmail.com", + "homepage": "https://github.com/Nyholm" + }, + { + "name": "Tobias Schultze", + "email": "webmaster@tubo-world.de", + "homepage": "https://github.com/Tobion" + } + ], + "description": "Guzzle promises library", + "keywords": [ + "promise" + ], + "support": { + "issues": "https://github.com/guzzle/promises/issues", + "source": "https://github.com/guzzle/promises/tree/2.2.0" + }, + "funding": [ + { + "url": "https://github.com/GrahamCampbell", + "type": "github" + }, + { + "url": "https://github.com/Nyholm", + "type": "github" + }, + { + "url": "https://tidelift.com/funding/github/packagist/guzzlehttp/promises", + "type": "tidelift" + } + ], + "install-path": "../guzzlehttp/promises" + }, + { + "name": "guzzlehttp/psr7", + "version": "2.7.1", + "version_normalized": "2.7.1.0", + "source": { + "type": "git", + "url": "https://github.com/guzzle/psr7.git", + "reference": "c2270caaabe631b3b44c85f99e5a04bbb8060d16" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/guzzle/psr7/zipball/c2270caaabe631b3b44c85f99e5a04bbb8060d16", + "reference": "c2270caaabe631b3b44c85f99e5a04bbb8060d16", + "shasum": "" + }, + "require": { + "php": "^7.2.5 || ^8.0", + "psr/http-factory": "^1.0", + "psr/http-message": "^1.1 || ^2.0", + "ralouphie/getallheaders": "^3.0" + }, + "provide": { + "psr/http-factory-implementation": "1.0", + "psr/http-message-implementation": "1.0" + }, + "require-dev": { + "bamarni/composer-bin-plugin": "^1.8.2", + "http-interop/http-factory-tests": "0.9.0", + "phpunit/phpunit": "^8.5.39 || ^9.6.20" + }, + "suggest": { + "laminas/laminas-httphandlerrunner": "Emit PSR-7 responses" + }, + "time": "2025-03-27T12:30:47+00:00", + "type": "library", + "extra": { + "bamarni-bin": { + "bin-links": true, + "forward-command": false + } + }, + "installation-source": "dist", + "autoload": { + "psr-4": { + "GuzzleHttp\\Psr7\\": "src/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Graham Campbell", + "email": "hello@gjcampbell.co.uk", + "homepage": "https://github.com/GrahamCampbell" + }, + { + "name": "Michael Dowling", + "email": "mtdowling@gmail.com", + "homepage": "https://github.com/mtdowling" + }, + { + "name": "George Mponos", + "email": "gmponos@gmail.com", + "homepage": "https://github.com/gmponos" + }, + { + "name": "Tobias Nyholm", + "email": "tobias.nyholm@gmail.com", + "homepage": "https://github.com/Nyholm" + }, + { + "name": "Márk Sági-Kazár", + "email": "mark.sagikazar@gmail.com", + "homepage": "https://github.com/sagikazarmark" + }, + { + "name": "Tobias Schultze", + "email": "webmaster@tubo-world.de", + "homepage": "https://github.com/Tobion" + }, + { + "name": "Márk Sági-Kazár", + "email": "mark.sagikazar@gmail.com", + "homepage": "https://sagikazarmark.hu" + } + ], + "description": "PSR-7 message implementation that also provides common utility methods", + "keywords": [ + "http", + "message", + "psr-7", + "request", + "response", + "stream", + "uri", + "url" + ], + "support": { + "issues": "https://github.com/guzzle/psr7/issues", + "source": "https://github.com/guzzle/psr7/tree/2.7.1" + }, + "funding": [ + { + "url": "https://github.com/GrahamCampbell", + "type": "github" + }, + { + "url": "https://github.com/Nyholm", + "type": "github" + }, + { + "url": "https://tidelift.com/funding/github/packagist/guzzlehttp/psr7", + "type": "tidelift" + } + ], + "install-path": "../guzzlehttp/psr7" + }, + { + "name": "guzzlehttp/uri-template", + "version": "v1.0.4", + "version_normalized": "1.0.4.0", + "source": { + "type": "git", + "url": "https://github.com/guzzle/uri-template.git", + "reference": "30e286560c137526eccd4ce21b2de477ab0676d2" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/guzzle/uri-template/zipball/30e286560c137526eccd4ce21b2de477ab0676d2", + "reference": "30e286560c137526eccd4ce21b2de477ab0676d2", + "shasum": "" + }, + "require": { + "php": "^7.2.5 || ^8.0", + "symfony/polyfill-php80": "^1.24" + }, + "require-dev": { + "bamarni/composer-bin-plugin": "^1.8.2", + "phpunit/phpunit": "^8.5.36 || ^9.6.15", + "uri-template/tests": "1.0.0" + }, + "time": "2025-02-03T10:55:03+00:00", + "type": "library", + "extra": { + "bamarni-bin": { + "bin-links": true, + "forward-command": false + } + }, + "installation-source": "dist", + "autoload": { + "psr-4": { + "GuzzleHttp\\UriTemplate\\": "src" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Graham Campbell", + "email": "hello@gjcampbell.co.uk", + "homepage": "https://github.com/GrahamCampbell" + }, + { + "name": "Michael Dowling", + "email": "mtdowling@gmail.com", + "homepage": "https://github.com/mtdowling" + }, + { + "name": "George Mponos", + "email": "gmponos@gmail.com", + "homepage": "https://github.com/gmponos" + }, + { + "name": "Tobias Nyholm", + "email": "tobias.nyholm@gmail.com", + "homepage": "https://github.com/Nyholm" + } + ], + "description": "A polyfill class for uri_template of PHP", + "keywords": [ + "guzzlehttp", + "uri-template" + ], + "support": { + "issues": "https://github.com/guzzle/uri-template/issues", + "source": "https://github.com/guzzle/uri-template/tree/v1.0.4" + }, + "funding": [ + { + "url": "https://github.com/GrahamCampbell", + "type": "github" + }, + { + "url": "https://github.com/Nyholm", + "type": "github" + }, + { + "url": "https://tidelift.com/funding/github/packagist/guzzlehttp/uri-template", + "type": "tidelift" + } + ], + "install-path": "../guzzlehttp/uri-template" + }, + { + "name": "icewind/searchdav", + "version": "v3.2.0", + "version_normalized": "3.2.0.0", + "source": { + "type": "git", + "url": "https://codeberg.org/icewind/SearchDAV", + "reference": "3865288b6962de33086d4e02ec3584610c32b773" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/icewind1991/SearchDAV/zipball/3865288b6962de33086d4e02ec3584610c32b773", + "reference": "3865288b6962de33086d4e02ec3584610c32b773", + "shasum": "" + }, + "require": { + "php": ">=7.3 || >=8.0", + "sabre/dav": "^4.0.0" + }, + "require-dev": { + "friendsofphp/php-cs-fixer": "^2", + "php-parallel-lint/php-parallel-lint": "^1.0", + "phpstan/phpstan": "^0.12", + "phpunit/phpunit": "^8", + "psalm/phar": "^4.3" + }, + "time": "2024-11-08T15:54:16+00:00", + "type": "library", + "installation-source": "dist", + "autoload": { + "psr-4": { + "SearchDAV\\": "src/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "AGPL-3.0-or-later" + ], + "authors": [ + { + "name": "Robin Appelman", + "email": "robin@icewind.nl" + } + ], + "description": "sabre/dav plugin to implement rfc5323 SEARCH", + "support": { + "issues": "https://github.com/icewind1991/SearchDAV/issues", + "source": "https://github.com/icewind1991/SearchDAV/tree/v3.2.0" + }, + "install-path": "../icewind/searchdav" + }, + { + "name": "icewind/smb", + "version": "v3.7.0", + "version_normalized": "3.7.0.0", + "source": { + "type": "git", + "url": "https://codeberg.org/icewind/SMB", + "reference": "e6904cbe75f678335092f4861c60c656b1a99e84" + }, + "require": { + "icewind/streams": ">=0.7.3", + "php": ">=7.2" + }, + "require-dev": { + "friendsofphp/php-cs-fixer": "^2.16", + "phpstan/phpstan": "^0.12.57", + "phpunit/phpunit": "^8.5|^9.3.8", + "psalm/phar": "^4.3" + }, + "time": "2024-11-11T14:08:34+00:00", + "type": "library", + "installation-source": "source", + "autoload": { + "psr-4": { + "Icewind\\SMB\\": "src/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Robin Appelman", + "email": "icewind@owncloud.com" + } + ], + "description": "php wrapper for smbclient and libsmbclient-php", + "install-path": "../icewind/smb" + }, + { + "name": "icewind/streams", + "version": "v0.7.8", + "version_normalized": "0.7.8.0", + "source": { + "type": "git", + "url": "https://codeberg.org/icewind/streams", + "reference": "cb2bd3ed41b516efb97e06e8da35a12ef58ba48b" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/icewind1991/Streams/zipball/cb2bd3ed41b516efb97e06e8da35a12ef58ba48b", + "reference": "cb2bd3ed41b516efb97e06e8da35a12ef58ba48b", + "shasum": "" + }, + "require": { + "php": ">=7.1" + }, + "require-dev": { + "friendsofphp/php-cs-fixer": "^2", + "phpstan/phpstan": "^0.12", + "phpunit/phpunit": "^9" + }, + "time": "2024-12-05T14:36:22+00:00", + "type": "library", + "installation-source": "dist", + "autoload": { + "psr-4": { + "Icewind\\Streams\\": "src/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Robin Appelman", + "email": "icewind@owncloud.com" + } + ], + "description": "A set of generic stream wrappers", + "support": { + "issues": "https://github.com/icewind1991/Streams/issues", + "source": "https://github.com/icewind1991/Streams/tree/v0.7.8" + }, + "install-path": "../icewind/streams" + }, + { + "name": "justinrainbow/json-schema", + "version": "6.4.2", + "version_normalized": "6.4.2.0", + "source": { + "type": "git", + "url": "https://github.com/jsonrainbow/json-schema.git", + "reference": "ce1fd2d47799bb60668643bc6220f6278a4c1d02" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/jsonrainbow/json-schema/zipball/ce1fd2d47799bb60668643bc6220f6278a4c1d02", + "reference": "ce1fd2d47799bb60668643bc6220f6278a4c1d02", + "shasum": "" + }, + "require": { + "ext-json": "*", + "marc-mabe/php-enum": "^4.0", + "php": "^7.2 || ^8.0" + }, + "require-dev": { + "friendsofphp/php-cs-fixer": "3.3.0", + "json-schema/json-schema-test-suite": "1.2.0", + "marc-mabe/php-enum-phpstan": "^2.0", + "phpspec/prophecy": "^1.19", + "phpstan/phpstan": "^1.12", + "phpunit/phpunit": "^8.5" + }, + "time": "2025-06-03T18:27:04+00:00", + "bin": [ + "bin/validate-json" + ], + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "6.x-dev" + } + }, + "installation-source": "dist", + "autoload": { + "psr-4": { + "JsonSchema\\": "src/JsonSchema/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Bruno Prieto Reis", + "email": "bruno.p.reis@gmail.com" + }, + { + "name": "Justin Rainbow", + "email": "justin.rainbow@gmail.com" + }, + { + "name": "Igor Wiedler", + "email": "igor@wiedler.ch" + }, + { + "name": "Robert Schönthal", + "email": "seroscho@googlemail.com" + } + ], + "description": "A library to validate a json schema.", + "homepage": "https://github.com/jsonrainbow/json-schema", + "keywords": [ + "json", + "schema" + ], + "support": { + "issues": "https://github.com/jsonrainbow/json-schema/issues", + "source": "https://github.com/jsonrainbow/json-schema/tree/6.4.2" + }, + "install-path": "../justinrainbow/json-schema" + }, + { + "name": "kornrunner/blurhash", + "version": "v1.2.2", + "version_normalized": "1.2.2.0", + "source": { + "type": "git", + "url": "https://github.com/kornrunner/php-blurhash.git", + "reference": "bc8a4596cb0a49874f0158696a382ab3933fefe4" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/kornrunner/php-blurhash/zipball/bc8a4596cb0a49874f0158696a382ab3933fefe4", + "reference": "bc8a4596cb0a49874f0158696a382ab3933fefe4", + "shasum": "" + }, + "require": { + "php": "^7.3|^8.0" + }, + "require-dev": { + "ext-gd": "*", + "ocramius/package-versions": "^1.4|^2.0", + "phpstan/phpstan": "^0.12", + "phpunit/phpunit": "^9", + "vimeo/psalm": "^4.3" + }, + "time": "2022-07-13T19:38:39+00:00", + "type": "library", + "installation-source": "dist", + "autoload": { + "psr-4": { + "kornrunner\\Blurhash\\": "src" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Boris Momčilović", + "email": "boris.momcilovic@gmail.com" + } + ], + "description": "Pure PHP implementation of Blurhash", + "homepage": "https://github.com/kornrunner/php-blurhash", + "support": { + "issues": "https://github.com/kornrunner/php-blurhash/issues", + "source": "https://github.com/kornrunner/php-blurhash.git" + }, + "install-path": "../kornrunner/blurhash" + }, + { + "name": "laravel/serializable-closure", + "version": "v2.0.4", + "version_normalized": "2.0.4.0", + "source": { + "type": "git", + "url": "https://github.com/laravel/serializable-closure.git", + "reference": "b352cf0534aa1ae6b4d825d1e762e35d43f8a841" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/laravel/serializable-closure/zipball/b352cf0534aa1ae6b4d825d1e762e35d43f8a841", + "reference": "b352cf0534aa1ae6b4d825d1e762e35d43f8a841", + "shasum": "" + }, + "require": { + "php": "^8.1" + }, + "require-dev": { + "illuminate/support": "^10.0|^11.0|^12.0", + "nesbot/carbon": "^2.67|^3.0", + "pestphp/pest": "^2.36|^3.0", + "phpstan/phpstan": "^2.0", + "symfony/var-dumper": "^6.2.0|^7.0.0" + }, + "time": "2025-03-19T13:51:03+00:00", + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "2.x-dev" + } + }, + "installation-source": "dist", + "autoload": { + "psr-4": { + "Laravel\\SerializableClosure\\": "src/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Taylor Otwell", + "email": "taylor@laravel.com" + }, + { + "name": "Nuno Maduro", + "email": "nuno@laravel.com" + } + ], + "description": "Laravel Serializable Closure provides an easy and secure way to serialize closures in PHP.", + "keywords": [ + "closure", + "laravel", + "serializable" + ], + "support": { + "issues": "https://github.com/laravel/serializable-closure/issues", + "source": "https://github.com/laravel/serializable-closure" + }, + "install-path": "../laravel/serializable-closure" + }, + { + "name": "lcobucci/clock", + "version": "3.0.0", + "version_normalized": "3.0.0.0", + "source": { + "type": "git", + "url": "https://github.com/lcobucci/clock.git", + "reference": "039ef98c6b57b101d10bd11d8fdfda12cbd996dc" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/lcobucci/clock/zipball/039ef98c6b57b101d10bd11d8fdfda12cbd996dc", + "reference": "039ef98c6b57b101d10bd11d8fdfda12cbd996dc", + "shasum": "" + }, + "require": { + "php": "~8.1.0 || ~8.2.0", + "psr/clock": "^1.0" + }, + "provide": { + "psr/clock-implementation": "1.0" + }, + "require-dev": { + "infection/infection": "^0.26", + "lcobucci/coding-standard": "^9.0", + "phpstan/extension-installer": "^1.2", + "phpstan/phpstan": "^1.9.4", + "phpstan/phpstan-deprecation-rules": "^1.1.1", + "phpstan/phpstan-phpunit": "^1.3.2", + "phpstan/phpstan-strict-rules": "^1.4.4", + "phpunit/phpunit": "^9.5.27" + }, + "time": "2022-12-19T15:00:24+00:00", + "type": "library", + "installation-source": "dist", + "autoload": { + "psr-4": { + "Lcobucci\\Clock\\": "src" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Luís Cobucci", + "email": "lcobucci@gmail.com" + } + ], + "description": "Yet another clock abstraction", + "support": { + "issues": "https://github.com/lcobucci/clock/issues", + "source": "https://github.com/lcobucci/clock/tree/3.0.0" + }, + "funding": [ + { + "url": "https://github.com/lcobucci", + "type": "github" + }, + { + "url": "https://www.patreon.com/lcobucci", + "type": "patreon" + } + ], + "install-path": "../lcobucci/clock" + }, + { + "name": "marc-mabe/php-enum", + "version": "v4.7.1", + "version_normalized": "4.7.1.0", + "source": { + "type": "git", + "url": "https://github.com/marc-mabe/php-enum.git", + "reference": "7159809e5cfa041dca28e61f7f7ae58063aae8ed" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/marc-mabe/php-enum/zipball/7159809e5cfa041dca28e61f7f7ae58063aae8ed", + "reference": "7159809e5cfa041dca28e61f7f7ae58063aae8ed", + "shasum": "" + }, + "require": { + "ext-reflection": "*", + "php": "^7.1 | ^8.0" + }, + "require-dev": { + "phpbench/phpbench": "^0.16.10 || ^1.0.4", + "phpstan/phpstan": "^1.3.1", + "phpunit/phpunit": "^7.5.20 | ^8.5.22 | ^9.5.11", + "vimeo/psalm": "^4.17.0 | ^5.26.1" + }, + "time": "2024-11-28T04:54:44+00:00", + "type": "library", + "extra": { + "branch-alias": { + "dev-3.x": "3.2-dev", + "dev-master": "4.7-dev" + } + }, + "installation-source": "dist", + "autoload": { + "psr-4": { + "MabeEnum\\": "src/" + }, + "classmap": [ + "stubs/Stringable.php" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "BSD-3-Clause" + ], + "authors": [ + { + "name": "Marc Bennewitz", + "email": "dev@mabe.berlin", + "homepage": "https://mabe.berlin/", + "role": "Lead" + } + ], + "description": "Simple and fast implementation of enumerations with native PHP", + "homepage": "https://github.com/marc-mabe/php-enum", + "keywords": [ + "enum", + "enum-map", + "enum-set", + "enumeration", + "enumerator", + "enummap", + "enumset", + "map", + "set", + "type", + "type-hint", + "typehint" + ], + "support": { + "issues": "https://github.com/marc-mabe/php-enum/issues", + "source": "https://github.com/marc-mabe/php-enum/tree/v4.7.1" + }, + "install-path": "../marc-mabe/php-enum" + }, + { + "name": "masterminds/html5", + "version": "2.9.0", + "version_normalized": "2.9.0.0", + "source": { + "type": "git", + "url": "https://github.com/Masterminds/html5-php.git", + "reference": "f5ac2c0b0a2eefca70b2ce32a5809992227e75a6" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/Masterminds/html5-php/zipball/f5ac2c0b0a2eefca70b2ce32a5809992227e75a6", + "reference": "f5ac2c0b0a2eefca70b2ce32a5809992227e75a6", + "shasum": "" + }, + "require": { + "ext-dom": "*", + "php": ">=5.3.0" + }, + "require-dev": { + "phpunit/phpunit": "^4.8.35 || ^5.7.21 || ^6 || ^7 || ^8 || ^9" + }, + "time": "2024-03-31T07:05:07+00:00", + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "2.7-dev" + } + }, + "installation-source": "dist", + "autoload": { + "psr-4": { + "Masterminds\\": "src" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Matt Butcher", + "email": "technosophos@gmail.com" + }, + { + "name": "Matt Farina", + "email": "matt@mattfarina.com" + }, + { + "name": "Asmir Mustafic", + "email": "goetas@gmail.com" + } + ], + "description": "An HTML5 parser and serializer.", + "homepage": "http://masterminds.github.io/html5-php", + "keywords": [ + "HTML5", + "dom", + "html", + "parser", + "querypath", + "serializer", + "xml" + ], + "support": { + "issues": "https://github.com/Masterminds/html5-php/issues", + "source": "https://github.com/Masterminds/html5-php/tree/2.9.0" + }, + "install-path": "../masterminds/html5" + }, + { + "name": "mexitek/phpcolors", + "version": "v1.0.4", + "version_normalized": "1.0.4.0", + "source": { + "type": "git", + "url": "https://github.com/mexitek/phpColors.git", + "reference": "4043974240ca7dc3c2bec3c158588148b605b206" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/mexitek/phpColors/zipball/4043974240ca7dc3c2bec3c158588148b605b206", + "reference": "4043974240ca7dc3c2bec3c158588148b605b206", + "shasum": "" + }, + "require": { + "php": "^7.2|^8.0" + }, + "require-dev": { + "nette/tester": "^2.3", + "squizlabs/php_codesniffer": "^3.5" + }, + "time": "2021-11-26T13:19:08+00:00", + "type": "library", + "installation-source": "dist", + "autoload": { + "classmap": [ + "src" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Arlo Carreon", + "homepage": "http://arlocarreon.com", + "role": "creator" + } + ], + "description": "A series of methods that let you manipulate colors. Just incase you ever need different shades of one color on the fly.", + "homepage": "http://mexitek.github.com/phpColors/", + "keywords": [ + "color", + "css", + "design", + "frontend", + "ui" + ], + "support": { + "issues": "https://github.com/mexitek/phpColors/issues", + "source": "https://github.com/mexitek/phpColors" + }, + "install-path": "../mexitek/phpcolors" + }, + { + "name": "microsoft/azure-storage-blob", + "version": "1.5.4", + "version_normalized": "1.5.4.0", + "source": { + "type": "git", + "url": "https://github.com/Azure/azure-storage-blob-php.git", + "reference": "1023ce1dbf062351a32ca5ec72ad1fd4a504f1bf" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/Azure/azure-storage-blob-php/zipball/1023ce1dbf062351a32ca5ec72ad1fd4a504f1bf", + "reference": "1023ce1dbf062351a32ca5ec72ad1fd4a504f1bf", + "shasum": "" + }, + "require": { + "microsoft/azure-storage-common": "~1.5", + "php": ">=5.6.0" + }, + "time": "2022-09-02T02:13:06+00:00", + "type": "library", + "installation-source": "dist", + "autoload": { + "psr-4": { + "MicrosoftAzure\\Storage\\Blob\\": "src/Blob" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Azure Storage PHP Client Library", + "email": "dmsh@microsoft.com" + } + ], + "description": "This project provides a set of PHP client libraries that make it easy to access Microsoft Azure Storage Blob APIs.", + "keywords": [ + "azure", + "blob", + "php", + "sdk", + "storage" + ], + "support": { + "issues": "https://github.com/Azure/azure-storage-blob-php/issues", + "source": "https://github.com/Azure/azure-storage-blob-php/tree/v1.5.4" + }, + "install-path": "../microsoft/azure-storage-blob" + }, + { + "name": "microsoft/azure-storage-common", + "version": "1.5.2", + "version_normalized": "1.5.2.0", + "source": { + "type": "git", + "url": "https://github.com/Azure/azure-storage-common-php.git", + "reference": "8ca7b1bf4c9ca7c663e75a02a0035b05b37196a0" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/Azure/azure-storage-common-php/zipball/8ca7b1bf4c9ca7c663e75a02a0035b05b37196a0", + "reference": "8ca7b1bf4c9ca7c663e75a02a0035b05b37196a0", + "shasum": "" + }, + "require": { + "guzzlehttp/guzzle": "~6.0|^7.0", + "php": ">=5.6.0" + }, + "time": "2021-10-09T03:03:47+00:00", + "type": "library", + "installation-source": "dist", + "autoload": { + "psr-4": { + "MicrosoftAzure\\Storage\\Common\\": "src/Common" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Azure Storage PHP Client Library", + "email": "dmsh@microsoft.com" + } + ], + "description": "This project provides a set of common code shared by Azure Storage Blob, Table, Queue and File PHP client libraries.", + "keywords": [ + "azure", + "common", + "php", + "sdk", + "storage" + ], + "support": { + "issues": "https://github.com/Azure/azure-storage-common-php/issues", + "source": "https://github.com/Azure/azure-storage-common-php/tree/v1.5.2" + }, + "install-path": "../microsoft/azure-storage-common" + }, + { + "name": "mlocati/ip-lib", + "version": "1.20.0", + "version_normalized": "1.20.0.0", + "source": { + "type": "git", + "url": "https://github.com/mlocati/ip-lib.git", + "reference": "fd45fc3bf08ed6c7e665e2e70562082ac954afd4" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/mlocati/ip-lib/zipball/fd45fc3bf08ed6c7e665e2e70562082ac954afd4", + "reference": "fd45fc3bf08ed6c7e665e2e70562082ac954afd4", + "shasum": "" + }, + "require": { + "php": ">=5.3.3" + }, + "require-dev": { + "ext-pdo_sqlite": "*", + "phpunit/phpunit": "^4.8 || ^5.7 || ^6.5 || ^7.5 || ^8.5 || ^9.5" + }, + "time": "2025-02-04T17:30:58+00:00", + "type": "library", + "installation-source": "dist", + "autoload": { + "psr-4": { + "IPLib\\": "src/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Michele Locati", + "email": "mlocati@gmail.com", + "homepage": "https://github.com/mlocati", + "role": "Author" + } + ], + "description": "Handle IPv4, IPv6 addresses and ranges", + "homepage": "https://github.com/mlocati/ip-lib", + "keywords": [ + "IP", + "address", + "addresses", + "ipv4", + "ipv6", + "manage", + "managing", + "matching", + "network", + "networking", + "range", + "subnet" + ], + "support": { + "issues": "https://github.com/mlocati/ip-lib/issues", + "source": "https://github.com/mlocati/ip-lib/tree/1.20.0" + }, + "funding": [ + { + "url": "https://github.com/sponsors/mlocati", + "type": "github" + }, + { + "url": "https://paypal.me/mlocati", + "type": "other" + } + ], + "install-path": "../mlocati/ip-lib" + }, + { + "name": "mtdowling/jmespath.php", + "version": "2.8.0", + "version_normalized": "2.8.0.0", + "source": { + "type": "git", + "url": "https://github.com/jmespath/jmespath.php.git", + "reference": "a2a865e05d5f420b50cc2f85bb78d565db12a6bc" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/jmespath/jmespath.php/zipball/a2a865e05d5f420b50cc2f85bb78d565db12a6bc", + "reference": "a2a865e05d5f420b50cc2f85bb78d565db12a6bc", + "shasum": "" + }, + "require": { + "php": "^7.2.5 || ^8.0", + "symfony/polyfill-mbstring": "^1.17" + }, + "require-dev": { + "composer/xdebug-handler": "^3.0.3", + "phpunit/phpunit": "^8.5.33" + }, + "time": "2024-09-04T18:46:31+00:00", + "bin": [ + "bin/jp.php" + ], + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "2.8-dev" + } + }, + "installation-source": "dist", + "autoload": { + "files": [ + "src/JmesPath.php" + ], + "psr-4": { + "JmesPath\\": "src/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Graham Campbell", + "email": "hello@gjcampbell.co.uk", + "homepage": "https://github.com/GrahamCampbell" + }, + { + "name": "Michael Dowling", + "email": "mtdowling@gmail.com", + "homepage": "https://github.com/mtdowling" + } + ], + "description": "Declaratively specify how to extract elements from a JSON document", + "keywords": [ + "json", + "jsonpath" + ], + "support": { + "issues": "https://github.com/jmespath/jmespath.php/issues", + "source": "https://github.com/jmespath/jmespath.php/tree/2.8.0" + }, + "install-path": "../mtdowling/jmespath.php" + }, + { + "name": "f7cloud/lognormalizer", + "version": "v1.0.0", + "version_normalized": "1.0.0.0", + "source": { + "type": "git", + "url": "https://github.com/nextcloud/lognormalizer.git", + "reference": "87445d69225c247aaff64643b1fc83c6d6df741f" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/nextcloud/lognormalizer/zipball/87445d69225c247aaff64643b1fc83c6d6df741f", + "reference": "87445d69225c247aaff64643b1fc83c6d6df741f", + "shasum": "" + }, + "require": { + "ext-json": "*", + "php": ">=7.2.0" + }, + "require-dev": { + "phpunit/phpunit": "8.*" + }, + "time": "2020-12-02T09:34:47+00:00", + "type": "library", + "installation-source": "dist", + "autoload": { + "psr-4": { + "F7cloud\\LogNormalizer\\": "src/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "AGPL-3.0" + ], + "authors": [ + { + "name": "Christoph Wurst", + "email": "christoph@winzerhof-wurst.at" + }, + { + "name": "Olivier Paroz", + "email": "dev-lognormalizer@interfasys.ch", + "homepage": "http://www.interfasys.ch", + "role": "Developer" + }, + { + "name": "Jordi Boggiano", + "email": "j.boggiano@seld.be", + "role": "Developer" + } + ], + "description": "Parses variables and converts them to string so that they can be logged", + "homepage": "https://github.com/interfasys/lognormalizer", + "keywords": [ + "log", + "normalizer" + ], + "support": { + "issues": "https://github.com/nextcloud/lognormalizer/issues", + "source": "https://github.com/nextcloud/lognormalizer/tree/v1.0.0" + }, + "install-path": "../f7cloud/lognormalizer" + }, + { + "name": "paragonie/constant_time_encoding", + "version": "v2.6.3", + "version_normalized": "2.6.3.0", + "source": { + "type": "git", + "url": "https://github.com/paragonie/constant_time_encoding.git", + "reference": "58c3f47f650c94ec05a151692652a868995d2938" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/paragonie/constant_time_encoding/zipball/58c3f47f650c94ec05a151692652a868995d2938", + "reference": "58c3f47f650c94ec05a151692652a868995d2938", + "shasum": "" + }, + "require": { + "php": "^7|^8" + }, + "require-dev": { + "phpunit/phpunit": "^6|^7|^8|^9", + "vimeo/psalm": "^1|^2|^3|^4" + }, + "time": "2022-06-14T06:56:20+00:00", + "type": "library", + "installation-source": "dist", + "autoload": { + "psr-4": { + "ParagonIE\\ConstantTime\\": "src/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Paragon Initiative Enterprises", + "email": "security@paragonie.com", + "homepage": "https://paragonie.com", + "role": "Maintainer" + }, + { + "name": "Steve 'Sc00bz' Thomas", + "email": "steve@tobtu.com", + "homepage": "https://www.tobtu.com", + "role": "Original Developer" + } + ], + "description": "Constant-time Implementations of RFC 4648 Encoding (Base-64, Base-32, Base-16)", + "keywords": [ + "base16", + "base32", + "base32_decode", + "base32_encode", + "base64", + "base64_decode", + "base64_encode", + "bin2hex", + "encoding", + "hex", + "hex2bin", + "rfc4648" + ], + "support": { + "email": "info@paragonie.com", + "issues": "https://github.com/paragonie/constant_time_encoding/issues", + "source": "https://github.com/paragonie/constant_time_encoding" + }, + "install-path": "../paragonie/constant_time_encoding" + }, + { + "name": "pear/archive_tar", + "version": "1.5.0", + "version_normalized": "1.5.0.0", + "source": { + "type": "git", + "url": "https://github.com/pear/Archive_Tar.git", + "reference": "b439c859564f5cbb0f64ad6002d0afe84a889602" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/pear/Archive_Tar/zipball/b439c859564f5cbb0f64ad6002d0afe84a889602", + "reference": "b439c859564f5cbb0f64ad6002d0afe84a889602", + "shasum": "" + }, + "require": { + "pear/pear-core-minimal": "^1.10.0alpha2", + "php": ">=5.2.0" + }, + "require-dev": { + "phpunit/phpunit": "*" + }, + "suggest": { + "ext-bz2": "Bz2 compression support.", + "ext-xz": "Lzma2 compression support.", + "ext-zlib": "Gzip compression support." + }, + "time": "2024-03-16T16:21:40+00:00", + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "1.4.x-dev" + } + }, + "installation-source": "dist", + "autoload": { + "psr-0": { + "Archive_Tar": "" + } + }, + "notification-url": "https://packagist.org/downloads/", + "include-path": [ + "./" + ], + "license": [ + "BSD-2-Clause" + ], + "authors": [ + { + "name": "Vincent Blavet", + "email": "vincent@phpconcept.net" + }, + { + "name": "Greg Beaver", + "email": "greg@chiaraquartet.net" + }, + { + "name": "Michiel Rook", + "email": "mrook@php.net" + } + ], + "description": "Tar file management class with compression support (gzip, bzip2, lzma2)", + "homepage": "https://github.com/pear/Archive_Tar", + "keywords": [ + "archive", + "tar" + ], + "support": { + "issues": "http://pear.php.net/bugs/search.php?cmd=display&package_name[]=Archive_Tar", + "source": "https://github.com/pear/Archive_Tar" + }, + "install-path": "../pear/archive_tar" + }, + { + "name": "pear/console_getopt", + "version": "v1.4.3", + "version_normalized": "1.4.3.0", + "source": { + "type": "git", + "url": "https://github.com/pear/Console_Getopt.git", + "reference": "a41f8d3e668987609178c7c4a9fe48fecac53fa0" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/pear/Console_Getopt/zipball/a41f8d3e668987609178c7c4a9fe48fecac53fa0", + "reference": "a41f8d3e668987609178c7c4a9fe48fecac53fa0", + "shasum": "" + }, + "time": "2019-11-20T18:27:48+00:00", + "type": "library", + "installation-source": "dist", + "autoload": { + "psr-0": { + "Console": "./" + } + }, + "notification-url": "https://packagist.org/downloads/", + "include-path": [ + "./" + ], + "license": [ + "BSD-2-Clause" + ], + "authors": [ + { + "name": "Andrei Zmievski", + "email": "andrei@php.net", + "role": "Lead" + }, + { + "name": "Stig Bakken", + "email": "stig@php.net", + "role": "Developer" + }, + { + "name": "Greg Beaver", + "email": "cellog@php.net", + "role": "Helper" + } + ], + "description": "More info available on: http://pear.php.net/package/Console_Getopt", + "support": { + "issues": "http://pear.php.net/bugs/search.php?cmd=display&package_name[]=Console_Getopt", + "source": "https://github.com/pear/Console_Getopt" + }, + "install-path": "../pear/console_getopt" + }, + { + "name": "pear/pear-core-minimal", + "version": "v1.10.16", + "version_normalized": "1.10.16.0", + "source": { + "type": "git", + "url": "https://github.com/pear/pear-core-minimal.git", + "reference": "c0f51b45f50683bf5bbf558036854ebc9b54d033" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/pear/pear-core-minimal/zipball/c0f51b45f50683bf5bbf558036854ebc9b54d033", + "reference": "c0f51b45f50683bf5bbf558036854ebc9b54d033", + "shasum": "" + }, + "require": { + "pear/console_getopt": "~1.4", + "pear/pear_exception": "~1.0", + "php": ">=5.4" + }, + "replace": { + "rsky/pear-core-min": "self.version" + }, + "time": "2024-11-24T22:27:58+00:00", + "type": "library", + "installation-source": "dist", + "autoload": { + "classmap": [ + "src/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "include-path": [ + "src/" + ], + "license": [ + "BSD-3-Clause" + ], + "authors": [ + { + "name": "Christian Weiske", + "email": "cweiske@php.net", + "role": "Lead" + } + ], + "description": "Minimal set of PEAR core files to be used as composer dependency", + "support": { + "issues": "http://pear.php.net/bugs/search.php?cmd=display&package_name[]=PEAR", + "source": "https://github.com/pear/pear-core-minimal" + }, + "install-path": "../pear/pear-core-minimal" + }, + { + "name": "pear/pear_exception", + "version": "v1.0.2", + "version_normalized": "1.0.2.0", + "source": { + "type": "git", + "url": "https://github.com/pear/PEAR_Exception.git", + "reference": "b14fbe2ddb0b9f94f5b24cf08783d599f776fff0" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/pear/PEAR_Exception/zipball/b14fbe2ddb0b9f94f5b24cf08783d599f776fff0", + "reference": "b14fbe2ddb0b9f94f5b24cf08783d599f776fff0", + "shasum": "" + }, + "require": { + "php": ">=5.2.0" + }, + "require-dev": { + "phpunit/phpunit": "<9" + }, + "time": "2021-03-21T15:43:46+00:00", + "type": "class", + "extra": { + "branch-alias": { + "dev-master": "1.0.x-dev" + } + }, + "installation-source": "dist", + "autoload": { + "classmap": [ + "PEAR/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "include-path": [ + "." + ], + "license": [ + "BSD-2-Clause" + ], + "authors": [ + { + "name": "Helgi Thormar", + "email": "dufuz@php.net" + }, + { + "name": "Greg Beaver", + "email": "cellog@php.net" + } + ], + "description": "The PEAR Exception base class.", + "homepage": "https://github.com/pear/PEAR_Exception", + "keywords": [ + "exception" + ], + "support": { + "issues": "http://pear.php.net/bugs/search.php?cmd=display&package_name[]=PEAR_Exception", + "source": "https://github.com/pear/PEAR_Exception" + }, + "install-path": "../pear/pear_exception" + }, + { + "name": "php-http/guzzle7-adapter", + "version": "1.1.0", + "version_normalized": "1.1.0.0", + "source": { + "type": "git", + "url": "https://github.com/php-http/guzzle7-adapter.git", + "reference": "03a415fde709c2f25539790fecf4d9a31bc3d0eb" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/php-http/guzzle7-adapter/zipball/03a415fde709c2f25539790fecf4d9a31bc3d0eb", + "reference": "03a415fde709c2f25539790fecf4d9a31bc3d0eb", + "shasum": "" + }, + "require": { + "guzzlehttp/guzzle": "^7.0", + "php": "^7.3 | ^8.0", + "php-http/httplug": "^2.0", + "psr/http-client": "^1.0" + }, + "provide": { + "php-http/async-client-implementation": "1.0", + "php-http/client-implementation": "1.0", + "psr/http-client-implementation": "1.0" + }, + "require-dev": { + "php-http/client-integration-tests": "^3.0", + "php-http/message-factory": "^1.1", + "phpspec/prophecy-phpunit": "^2.0", + "phpunit/phpunit": "^8.0|^9.3" + }, + "time": "2024-11-26T11:14:36+00:00", + "type": "library", + "installation-source": "dist", + "autoload": { + "psr-4": { + "Http\\Adapter\\Guzzle7\\": "src/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Tobias Nyholm", + "email": "tobias.nyholm@gmail.com" + } + ], + "description": "Guzzle 7 HTTP Adapter", + "homepage": "http://httplug.io", + "keywords": [ + "Guzzle", + "http" + ], + "support": { + "issues": "https://github.com/php-http/guzzle7-adapter/issues", + "source": "https://github.com/php-http/guzzle7-adapter/tree/1.1.0" + }, + "install-path": "../php-http/guzzle7-adapter" + }, + { + "name": "php-http/httplug", + "version": "2.4.1", + "version_normalized": "2.4.1.0", + "source": { + "type": "git", + "url": "https://github.com/php-http/httplug.git", + "reference": "5cad731844891a4c282f3f3e1b582c46839d22f4" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/php-http/httplug/zipball/5cad731844891a4c282f3f3e1b582c46839d22f4", + "reference": "5cad731844891a4c282f3f3e1b582c46839d22f4", + "shasum": "" + }, + "require": { + "php": "^7.1 || ^8.0", + "php-http/promise": "^1.1", + "psr/http-client": "^1.0", + "psr/http-message": "^1.0 || ^2.0" + }, + "require-dev": { + "friends-of-phpspec/phpspec-code-coverage": "^4.1 || ^5.0 || ^6.0", + "phpspec/phpspec": "^5.1 || ^6.0 || ^7.0" + }, + "time": "2024-09-23T11:39:58+00:00", + "type": "library", + "installation-source": "dist", + "autoload": { + "psr-4": { + "Http\\Client\\": "src/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Eric GELOEN", + "email": "geloen.eric@gmail.com" + }, + { + "name": "Márk Sági-Kazár", + "email": "mark.sagikazar@gmail.com", + "homepage": "https://sagikazarmark.hu" + } + ], + "description": "HTTPlug, the HTTP client abstraction for PHP", + "homepage": "http://httplug.io", + "keywords": [ + "client", + "http" + ], + "support": { + "issues": "https://github.com/php-http/httplug/issues", + "source": "https://github.com/php-http/httplug/tree/2.4.1" + }, + "install-path": "../php-http/httplug" + }, + { + "name": "php-http/promise", + "version": "1.3.1", + "version_normalized": "1.3.1.0", + "source": { + "type": "git", + "url": "https://github.com/php-http/promise.git", + "reference": "fc85b1fba37c169a69a07ef0d5a8075770cc1f83" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/php-http/promise/zipball/fc85b1fba37c169a69a07ef0d5a8075770cc1f83", + "reference": "fc85b1fba37c169a69a07ef0d5a8075770cc1f83", + "shasum": "" + }, + "require": { + "php": "^7.1 || ^8.0" + }, + "require-dev": { + "friends-of-phpspec/phpspec-code-coverage": "^4.3.2 || ^6.3", + "phpspec/phpspec": "^5.1.2 || ^6.2 || ^7.4" + }, + "time": "2024-03-15T13:55:21+00:00", + "type": "library", + "installation-source": "dist", + "autoload": { + "psr-4": { + "Http\\Promise\\": "src/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Joel Wurtz", + "email": "joel.wurtz@gmail.com" + }, + { + "name": "Márk Sági-Kazár", + "email": "mark.sagikazar@gmail.com" + } + ], + "description": "Promise used for asynchronous HTTP requests", + "homepage": "http://httplug.io", + "keywords": [ + "promise" + ], + "support": { + "issues": "https://github.com/php-http/promise/issues", + "source": "https://github.com/php-http/promise/tree/1.3.1" + }, + "install-path": "../php-http/promise" + }, + { + "name": "php-opencloud/openstack", + "version": "v3.14.0", + "version_normalized": "3.14.0.0", + "source": { + "type": "git", + "url": "https://github.com/php-opencloud/openstack.git", + "reference": "b92ea5581ca91779b88f08b1b44e6ca880b34fc3" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/php-opencloud/openstack/zipball/b92ea5581ca91779b88f08b1b44e6ca880b34fc3", + "reference": "b92ea5581ca91779b88f08b1b44e6ca880b34fc3", + "shasum": "" + }, + "require": { + "guzzlehttp/guzzle": "^7.0", + "guzzlehttp/psr7": ">=1.7", + "guzzlehttp/uri-template": "^0.2 || ^1.0", + "justinrainbow/json-schema": "^5.2 || ^6.0", + "php": "^7.2.5 || ^8.0" + }, + "require-dev": { + "ergebnis/composer-normalize": "^2.0", + "ext-json": "*", + "friendsofphp/php-cs-fixer": "^3", + "php-coveralls/php-coveralls": "^2.0", + "php-parallel-lint/php-parallel-lint": "^1.2", + "phpspec/prophecy": "^1.17", + "phpunit/phpunit": ">=8.5.23 <9.0", + "psr/log": "^1.0" + }, + "time": "2025-05-30T09:26:42+00:00", + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "3.0-dev" + } + }, + "installation-source": "dist", + "autoload": { + "psr-4": { + "OpenStack\\": "src/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "Apache-2.0" + ], + "authors": [ + { + "name": "Jamie Hannaford", + "email": "jamie.hannaford@rackspace.com", + "homepage": "https://github.com/jamiehannaford" + }, + { + "name": "Ha Phan", + "email": "thanhha.work@gmail.com", + "homepage": "https://github.com/haphan" + }, + { + "name": "Konstantin Babushkin", + "email": "koka@idwrx.com", + "homepage": "https://github.com/k0ka" + } + ], + "description": "PHP SDK for OpenStack APIs. Supports BlockStorage, Compute, Identity, Images, Networking and Metric Gnocchi", + "homepage": "https://github.com/php-opencloud/openstack", + "keywords": [ + "Openstack", + "api", + "php", + "sdk" + ], + "support": { + "issues": "https://github.com/php-opencloud/openstack/issues", + "source": "https://github.com/php-opencloud/openstack/tree/v3.14.0" + }, + "install-path": "../php-opencloud/openstack" + }, + { + "name": "phpseclib/phpseclib", + "version": "2.0.47", + "version_normalized": "2.0.47.0", + "source": { + "type": "git", + "url": "https://github.com/phpseclib/phpseclib.git", + "reference": "b7d7d90ee7df7f33a664b4aea32d50a305d35adb" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/phpseclib/phpseclib/zipball/b7d7d90ee7df7f33a664b4aea32d50a305d35adb", + "reference": "b7d7d90ee7df7f33a664b4aea32d50a305d35adb", + "shasum": "" + }, + "require": { + "php": ">=5.3.3" + }, + "require-dev": { + "phing/phing": "~2.7", + "phpunit/phpunit": "^4.8.35|^5.7|^6.0|^9.4", + "squizlabs/php_codesniffer": "~2.0" + }, + "suggest": { + "ext-gmp": "Install the GMP (GNU Multiple Precision) extension in order to speed up arbitrary precision integer arithmetic operations.", + "ext-libsodium": "SSH2/SFTP can make use of some algorithms provided by the libsodium-php extension.", + "ext-mcrypt": "Install the Mcrypt extension in order to speed up a few other cryptographic operations.", + "ext-openssl": "Install the OpenSSL extension in order to speed up a wide variety of cryptographic operations.", + "ext-xml": "Install the XML extension to load XML formatted public keys." + }, + "time": "2024-02-26T04:55:38+00:00", + "type": "library", + "installation-source": "dist", + "autoload": { + "files": [ + "phpseclib/bootstrap.php" + ], + "psr-4": { + "phpseclib\\": "phpseclib/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Jim Wigginton", + "email": "terrafrost@php.net", + "role": "Lead Developer" + }, + { + "name": "Patrick Monnerat", + "email": "pm@datasphere.ch", + "role": "Developer" + }, + { + "name": "Andreas Fischer", + "email": "bantu@phpbb.com", + "role": "Developer" + }, + { + "name": "Hans-Jürgen Petrich", + "email": "petrich@tronic-media.com", + "role": "Developer" + }, + { + "name": "Graham Campbell", + "email": "graham@alt-three.com", + "role": "Developer" + } + ], + "description": "PHP Secure Communications Library - Pure-PHP implementations of RSA, AES, SSH2, SFTP, X.509 etc.", + "homepage": "http://phpseclib.sourceforge.net", + "keywords": [ + "BigInteger", + "aes", + "asn.1", + "asn1", + "blowfish", + "crypto", + "cryptography", + "encryption", + "rsa", + "security", + "sftp", + "signature", + "signing", + "ssh", + "twofish", + "x.509", + "x509" + ], + "support": { + "issues": "https://github.com/phpseclib/phpseclib/issues", + "source": "https://github.com/phpseclib/phpseclib/tree/2.0.47" + }, + "funding": [ + { + "url": "https://github.com/terrafrost", + "type": "github" + }, + { + "url": "https://www.patreon.com/phpseclib", + "type": "patreon" + }, + { + "url": "https://tidelift.com/funding/github/packagist/phpseclib/phpseclib", + "type": "tidelift" + } + ], + "install-path": "../phpseclib/phpseclib" + }, + { + "name": "pimple/pimple", + "version": "v3.5.0", + "version_normalized": "3.5.0.0", + "source": { + "type": "git", + "url": "https://github.com/silexphp/Pimple.git", + "reference": "a94b3a4db7fb774b3d78dad2315ddc07629e1bed" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/silexphp/Pimple/zipball/a94b3a4db7fb774b3d78dad2315ddc07629e1bed", + "reference": "a94b3a4db7fb774b3d78dad2315ddc07629e1bed", + "shasum": "" + }, + "require": { + "php": ">=7.2.5", + "psr/container": "^1.1 || ^2.0" + }, + "require-dev": { + "symfony/phpunit-bridge": "^5.4@dev" + }, + "time": "2021-10-28T11:13:42+00:00", + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "3.4.x-dev" + } + }, + "installation-source": "dist", + "autoload": { + "psr-0": { + "Pimple": "src/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Fabien Potencier", + "email": "fabien@symfony.com" + } + ], + "description": "Pimple, a simple Dependency Injection Container", + "homepage": "https://pimple.symfony.com", + "keywords": [ + "container", + "dependency injection" + ], + "support": { + "source": "https://github.com/silexphp/Pimple/tree/v3.5.0" + }, + "install-path": "../pimple/pimple" + }, + { + "name": "psr/cache", + "version": "3.0.0", + "version_normalized": "3.0.0.0", + "source": { + "type": "git", + "url": "https://github.com/php-fig/cache.git", + "reference": "aa5030cfa5405eccfdcb1083ce040c2cb8d253bf" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/php-fig/cache/zipball/aa5030cfa5405eccfdcb1083ce040c2cb8d253bf", + "reference": "aa5030cfa5405eccfdcb1083ce040c2cb8d253bf", + "shasum": "" + }, + "require": { + "php": ">=8.0.0" + }, + "time": "2021-02-03T23:26:27+00:00", + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "1.0.x-dev" + } + }, + "installation-source": "dist", + "autoload": { + "psr-4": { + "Psr\\Cache\\": "src/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "PHP-FIG", + "homepage": "https://www.php-fig.org/" + } + ], + "description": "Common interface for caching libraries", + "keywords": [ + "cache", + "psr", + "psr-6" + ], + "support": { + "source": "https://github.com/php-fig/cache/tree/3.0.0" + }, + "install-path": "../psr/cache" + }, + { + "name": "psr/clock", + "version": "1.0.0", + "version_normalized": "1.0.0.0", + "source": { + "type": "git", + "url": "https://github.com/php-fig/clock.git", + "reference": "e41a24703d4560fd0acb709162f73b8adfc3aa0d" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/php-fig/clock/zipball/e41a24703d4560fd0acb709162f73b8adfc3aa0d", + "reference": "e41a24703d4560fd0acb709162f73b8adfc3aa0d", + "shasum": "" + }, + "require": { + "php": "^7.0 || ^8.0" + }, + "time": "2022-11-25T14:36:26+00:00", + "type": "library", + "installation-source": "dist", + "autoload": { + "psr-4": { + "Psr\\Clock\\": "src/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "PHP-FIG", + "homepage": "https://www.php-fig.org/" + } + ], + "description": "Common interface for reading the clock.", + "homepage": "https://github.com/php-fig/clock", + "keywords": [ + "clock", + "now", + "psr", + "psr-20", + "time" + ], + "support": { + "issues": "https://github.com/php-fig/clock/issues", + "source": "https://github.com/php-fig/clock/tree/1.0.0" + }, + "install-path": "../psr/clock" + }, + { + "name": "psr/container", + "version": "2.0.2", + "version_normalized": "2.0.2.0", + "source": { + "type": "git", + "url": "https://github.com/php-fig/container.git", + "reference": "c71ecc56dfe541dbd90c5360474fbc405f8d5963" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/php-fig/container/zipball/c71ecc56dfe541dbd90c5360474fbc405f8d5963", + "reference": "c71ecc56dfe541dbd90c5360474fbc405f8d5963", + "shasum": "" + }, + "require": { + "php": ">=7.4.0" + }, + "time": "2021-11-05T16:47:00+00:00", + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "2.0.x-dev" + } + }, + "installation-source": "dist", + "autoload": { + "psr-4": { + "Psr\\Container\\": "src/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "PHP-FIG", + "homepage": "https://www.php-fig.org/" + } + ], + "description": "Common Container Interface (PHP FIG PSR-11)", + "homepage": "https://github.com/php-fig/container", + "keywords": [ + "PSR-11", + "container", + "container-interface", + "container-interop", + "psr" + ], + "support": { + "issues": "https://github.com/php-fig/container/issues", + "source": "https://github.com/php-fig/container/tree/2.0.2" + }, + "install-path": "../psr/container" + }, + { + "name": "psr/event-dispatcher", + "version": "1.0.0", + "version_normalized": "1.0.0.0", + "source": { + "type": "git", + "url": "https://github.com/php-fig/event-dispatcher.git", + "reference": "dbefd12671e8a14ec7f180cab83036ed26714bb0" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/php-fig/event-dispatcher/zipball/dbefd12671e8a14ec7f180cab83036ed26714bb0", + "reference": "dbefd12671e8a14ec7f180cab83036ed26714bb0", + "shasum": "" + }, + "require": { + "php": ">=7.2.0" + }, + "time": "2019-01-08T18:20:26+00:00", + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "1.0.x-dev" + } + }, + "installation-source": "dist", + "autoload": { + "psr-4": { + "Psr\\EventDispatcher\\": "src/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "PHP-FIG", + "homepage": "http://www.php-fig.org/" + } + ], + "description": "Standard interfaces for event handling.", + "keywords": [ + "events", + "psr", + "psr-14" + ], + "support": { + "issues": "https://github.com/php-fig/event-dispatcher/issues", + "source": "https://github.com/php-fig/event-dispatcher/tree/1.0.0" + }, + "install-path": "../psr/event-dispatcher" + }, + { + "name": "psr/http-client", + "version": "1.0.3", + "version_normalized": "1.0.3.0", + "source": { + "type": "git", + "url": "https://github.com/php-fig/http-client.git", + "reference": "bb5906edc1c324c9a05aa0873d40117941e5fa90" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/php-fig/http-client/zipball/bb5906edc1c324c9a05aa0873d40117941e5fa90", + "reference": "bb5906edc1c324c9a05aa0873d40117941e5fa90", + "shasum": "" + }, + "require": { + "php": "^7.0 || ^8.0", + "psr/http-message": "^1.0 || ^2.0" + }, + "time": "2023-09-23T14:17:50+00:00", + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "1.0.x-dev" + } + }, + "installation-source": "dist", + "autoload": { + "psr-4": { + "Psr\\Http\\Client\\": "src/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "PHP-FIG", + "homepage": "https://www.php-fig.org/" + } + ], + "description": "Common interface for HTTP clients", + "homepage": "https://github.com/php-fig/http-client", + "keywords": [ + "http", + "http-client", + "psr", + "psr-18" + ], + "support": { + "source": "https://github.com/php-fig/http-client" + }, + "install-path": "../psr/http-client" + }, + { + "name": "psr/http-factory", + "version": "1.1.0", + "version_normalized": "1.1.0.0", + "source": { + "type": "git", + "url": "https://github.com/php-fig/http-factory.git", + "reference": "2b4765fddfe3b508ac62f829e852b1501d3f6e8a" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/php-fig/http-factory/zipball/2b4765fddfe3b508ac62f829e852b1501d3f6e8a", + "reference": "2b4765fddfe3b508ac62f829e852b1501d3f6e8a", + "shasum": "" + }, + "require": { + "php": ">=7.1", + "psr/http-message": "^1.0 || ^2.0" + }, + "time": "2024-04-15T12:06:14+00:00", + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "1.0.x-dev" + } + }, + "installation-source": "dist", + "autoload": { + "psr-4": { + "Psr\\Http\\Message\\": "src/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "PHP-FIG", + "homepage": "https://www.php-fig.org/" + } + ], + "description": "PSR-17: Common interfaces for PSR-7 HTTP message factories", + "keywords": [ + "factory", + "http", + "message", + "psr", + "psr-17", + "psr-7", + "request", + "response" + ], + "support": { + "source": "https://github.com/php-fig/http-factory" + }, + "install-path": "../psr/http-factory" + }, + { + "name": "psr/http-message", + "version": "2.0", + "version_normalized": "2.0.0.0", + "source": { + "type": "git", + "url": "https://github.com/php-fig/http-message.git", + "reference": "402d35bcb92c70c026d1a6a9883f06b2ead23d71" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/php-fig/http-message/zipball/402d35bcb92c70c026d1a6a9883f06b2ead23d71", + "reference": "402d35bcb92c70c026d1a6a9883f06b2ead23d71", + "shasum": "" + }, + "require": { + "php": "^7.2 || ^8.0" + }, + "time": "2023-04-04T09:54:51+00:00", + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "2.0.x-dev" + } + }, + "installation-source": "dist", + "autoload": { + "psr-4": { + "Psr\\Http\\Message\\": "src/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "PHP-FIG", + "homepage": "https://www.php-fig.org/" + } + ], + "description": "Common interface for HTTP messages", + "homepage": "https://github.com/php-fig/http-message", + "keywords": [ + "http", + "http-message", + "psr", + "psr-7", + "request", + "response" + ], + "support": { + "source": "https://github.com/php-fig/http-message/tree/2.0" + }, + "install-path": "../psr/http-message" + }, + { + "name": "psr/log", + "version": "3.0.2", + "version_normalized": "3.0.2.0", + "source": { + "type": "git", + "url": "https://github.com/php-fig/log.git", + "reference": "f16e1d5863e37f8d8c2a01719f5b34baa2b714d3" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/php-fig/log/zipball/f16e1d5863e37f8d8c2a01719f5b34baa2b714d3", + "reference": "f16e1d5863e37f8d8c2a01719f5b34baa2b714d3", + "shasum": "" + }, + "require": { + "php": ">=8.0.0" + }, + "time": "2024-09-11T13:17:53+00:00", + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "3.x-dev" + } + }, + "installation-source": "dist", + "autoload": { + "psr-4": { + "Psr\\Log\\": "src" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "PHP-FIG", + "homepage": "https://www.php-fig.org/" + } + ], + "description": "Common interface for logging libraries", + "homepage": "https://github.com/php-fig/log", + "keywords": [ + "log", + "psr", + "psr-3" + ], + "support": { + "source": "https://github.com/php-fig/log/tree/3.0.2" + }, + "install-path": "../psr/log" + }, + { + "name": "punic/punic", + "version": "3.8.1", + "version_normalized": "3.8.1.0", + "source": { + "type": "git", + "url": "https://github.com/punic/punic.git", + "reference": "142707201a246a9c2ea909605cd56177af87f961" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/punic/punic/zipball/142707201a246a9c2ea909605cd56177af87f961", + "reference": "142707201a246a9c2ea909605cd56177af87f961", + "shasum": "" + }, + "require": { + "php": ">=5.3" + }, + "replace": { + "punic/calendar": "*", + "punic/common": "*" + }, + "require-dev": { + "phpunit/phpunit": "^4.8 || ^5.7 || ^6.5 || ^7.4 || ^8.5 || ^9.5" + }, + "time": "2023-03-29T06:56:57+00:00", + "bin": [ + "bin/punic-data" + ], + "type": "library", + "installation-source": "dist", + "autoload": { + "psr-4": { + "Punic\\": "src/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Michele Locati", + "email": "mlocati@gmail.com", + "role": "Developer" + }, + { + "name": "Remo Laubacher", + "email": "remo.laubacher@gmail.com", + "role": "Collaborator, motivator and perfectionist supporter" + }, + { + "name": "Christian Schmidt", + "email": "github@chsc.dk", + "role": "Developer" + } + ], + "description": "PHP-Unicode CLDR", + "homepage": "https://github.com/punic/punic", + "keywords": [ + "calendar", + "cldr", + "date", + "date-time", + "i18n", + "internationalization", + "l10n", + "localization", + "php", + "time", + "translate", + "translations", + "unicode" + ], + "support": { + "issues": "https://github.com/punic/punic/issues", + "source": "https://github.com/punic/punic/tree/3.8.1" + }, + "funding": [ + { + "url": "https://paypal.me/mlocati", + "type": "custom" + }, + { + "url": "https://github.com/mlocati", + "type": "github" + } + ], + "install-path": "../punic/punic" + }, + { + "name": "ralouphie/getallheaders", + "version": "3.0.3", + "version_normalized": "3.0.3.0", + "source": { + "type": "git", + "url": "https://github.com/ralouphie/getallheaders.git", + "reference": "120b605dfeb996808c31b6477290a714d356e822" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/ralouphie/getallheaders/zipball/120b605dfeb996808c31b6477290a714d356e822", + "reference": "120b605dfeb996808c31b6477290a714d356e822", + "shasum": "" + }, + "require": { + "php": ">=5.6" + }, + "require-dev": { + "php-coveralls/php-coveralls": "^2.1", + "phpunit/phpunit": "^5 || ^6.5" + }, + "time": "2019-03-08T08:55:37+00:00", + "type": "library", + "installation-source": "dist", + "autoload": { + "files": [ + "src/getallheaders.php" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Ralph Khattar", + "email": "ralph.khattar@gmail.com" + } + ], + "description": "A polyfill for getallheaders.", + "support": { + "issues": "https://github.com/ralouphie/getallheaders/issues", + "source": "https://github.com/ralouphie/getallheaders/tree/develop" + }, + "install-path": "../ralouphie/getallheaders" + }, + { + "name": "sabre/dav", + "version": "4.7.0", + "version_normalized": "4.7.0.0", + "source": { + "type": "git", + "url": "https://github.com/sabre-io/dav.git", + "reference": "074373bcd689a30bcf5aaa6bbb20a3395964ce7a" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/sabre-io/dav/zipball/074373bcd689a30bcf5aaa6bbb20a3395964ce7a", + "reference": "074373bcd689a30bcf5aaa6bbb20a3395964ce7a", + "shasum": "" + }, + "require": { + "ext-ctype": "*", + "ext-date": "*", + "ext-dom": "*", + "ext-iconv": "*", + "ext-json": "*", + "ext-mbstring": "*", + "ext-pcre": "*", + "ext-simplexml": "*", + "ext-spl": "*", + "lib-libxml": ">=2.7.0", + "php": "^7.1.0 || ^8.0", + "psr/log": "^1.0 || ^2.0 || ^3.0", + "sabre/event": "^5.0", + "sabre/http": "^5.0.5", + "sabre/uri": "^2.0", + "sabre/vobject": "^4.2.1", + "sabre/xml": "^2.0.1" + }, + "require-dev": { + "friendsofphp/php-cs-fixer": "^2.19", + "monolog/monolog": "^1.27 || ^2.0", + "phpstan/phpstan": "^0.12 || ^1.0", + "phpstan/phpstan-phpunit": "^1.0", + "phpunit/phpunit": "^7.5 || ^8.5 || ^9.6" + }, + "suggest": { + "ext-curl": "*", + "ext-imap": "*", + "ext-pdo": "*" + }, + "time": "2024-10-29T11:46:02+00:00", + "bin": [ + "bin/sabredav", + "bin/naturalselection" + ], + "type": "library", + "installation-source": "dist", + "autoload": { + "psr-4": { + "Sabre\\": "lib/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "BSD-3-Clause" + ], + "authors": [ + { + "name": "Evert Pot", + "email": "me@evertpot.com", + "homepage": "http://evertpot.com/", + "role": "Developer" + } + ], + "description": "WebDAV Framework for PHP", + "homepage": "http://sabre.io/", + "keywords": [ + "CalDAV", + "CardDAV", + "WebDAV", + "framework", + "iCalendar" + ], + "support": { + "forum": "https://groups.google.com/group/sabredav-discuss", + "issues": "https://github.com/sabre-io/dav/issues", + "source": "https://github.com/fruux/sabre-dav" + }, + "install-path": "../sabre/dav" + }, + { + "name": "sabre/event", + "version": "5.1.7", + "version_normalized": "5.1.7.0", + "source": { + "type": "git", + "url": "https://github.com/sabre-io/event.git", + "reference": "86d57e305c272898ba3c28e9bd3d65d5464587c2" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/sabre-io/event/zipball/86d57e305c272898ba3c28e9bd3d65d5464587c2", + "reference": "86d57e305c272898ba3c28e9bd3d65d5464587c2", + "shasum": "" + }, + "require": { + "php": "^7.1 || ^8.0" + }, + "require-dev": { + "friendsofphp/php-cs-fixer": "~2.17.1||^3.63", + "phpstan/phpstan": "^0.12", + "phpunit/phpunit": "^7.5 || ^8.5 || ^9.6" + }, + "time": "2024-08-27T11:23:05+00:00", + "type": "library", + "installation-source": "dist", + "autoload": { + "files": [ + "lib/coroutine.php", + "lib/Loop/functions.php", + "lib/Promise/functions.php" + ], + "psr-4": { + "Sabre\\Event\\": "lib/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "BSD-3-Clause" + ], + "authors": [ + { + "name": "Evert Pot", + "email": "me@evertpot.com", + "homepage": "http://evertpot.com/", + "role": "Developer" + } + ], + "description": "sabre/event is a library for lightweight event-based programming", + "homepage": "http://sabre.io/event/", + "keywords": [ + "EventEmitter", + "async", + "coroutine", + "eventloop", + "events", + "hooks", + "plugin", + "promise", + "reactor", + "signal" + ], + "support": { + "forum": "https://groups.google.com/group/sabredav-discuss", + "issues": "https://github.com/sabre-io/event/issues", + "source": "https://github.com/fruux/sabre-event" + }, + "install-path": "../sabre/event" + }, + { + "name": "sabre/http", + "version": "5.1.12", + "version_normalized": "5.1.12.0", + "source": { + "type": "git", + "url": "https://github.com/sabre-io/http.git", + "reference": "dedff73f3995578bc942fa4c8484190cac14f139" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/sabre-io/http/zipball/dedff73f3995578bc942fa4c8484190cac14f139", + "reference": "dedff73f3995578bc942fa4c8484190cac14f139", + "shasum": "" + }, + "require": { + "ext-ctype": "*", + "ext-curl": "*", + "ext-mbstring": "*", + "php": "^7.1 || ^8.0", + "sabre/event": ">=4.0 <6.0", + "sabre/uri": "^2.0" + }, + "require-dev": { + "friendsofphp/php-cs-fixer": "~2.17.1||^3.63", + "phpstan/phpstan": "^0.12", + "phpunit/phpunit": "^7.5 || ^8.5 || ^9.6" + }, + "suggest": { + "ext-curl": " to make http requests with the Client class" + }, + "time": "2024-08-27T16:07:41+00:00", + "type": "library", + "installation-source": "dist", + "autoload": { + "files": [ + "lib/functions.php" + ], + "psr-4": { + "Sabre\\HTTP\\": "lib/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "BSD-3-Clause" + ], + "authors": [ + { + "name": "Evert Pot", + "email": "me@evertpot.com", + "homepage": "http://evertpot.com/", + "role": "Developer" + } + ], + "description": "The sabre/http library provides utilities for dealing with http requests and responses. ", + "homepage": "https://github.com/fruux/sabre-http", + "keywords": [ + "http" + ], + "support": { + "forum": "https://groups.google.com/group/sabredav-discuss", + "issues": "https://github.com/sabre-io/http/issues", + "source": "https://github.com/fruux/sabre-http" + }, + "install-path": "../sabre/http" + }, + { + "name": "sabre/uri", + "version": "2.3.4", + "version_normalized": "2.3.4.0", + "source": { + "type": "git", + "url": "https://github.com/sabre-io/uri.git", + "reference": "b76524c22de90d80ca73143680a8e77b1266c291" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/sabre-io/uri/zipball/b76524c22de90d80ca73143680a8e77b1266c291", + "reference": "b76524c22de90d80ca73143680a8e77b1266c291", + "shasum": "" + }, + "require": { + "php": "^7.4 || ^8.0" + }, + "require-dev": { + "friendsofphp/php-cs-fixer": "^3.63", + "phpstan/extension-installer": "^1.4", + "phpstan/phpstan": "^1.12", + "phpstan/phpstan-phpunit": "^1.4", + "phpstan/phpstan-strict-rules": "^1.6", + "phpunit/phpunit": "^9.6" + }, + "time": "2024-08-27T12:18:16+00:00", + "type": "library", + "installation-source": "dist", + "autoload": { + "files": [ + "lib/functions.php" + ], + "psr-4": { + "Sabre\\Uri\\": "lib/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "BSD-3-Clause" + ], + "authors": [ + { + "name": "Evert Pot", + "email": "me@evertpot.com", + "homepage": "http://evertpot.com/", + "role": "Developer" + } + ], + "description": "Functions for making sense out of URIs.", + "homepage": "http://sabre.io/uri/", + "keywords": [ + "rfc3986", + "uri", + "url" + ], + "support": { + "forum": "https://groups.google.com/group/sabredav-discuss", + "issues": "https://github.com/sabre-io/uri/issues", + "source": "https://github.com/fruux/sabre-uri" + }, + "install-path": "../sabre/uri" + }, + { + "name": "sabre/vobject", + "version": "4.5.6", + "version_normalized": "4.5.6.0", + "source": { + "type": "git", + "url": "https://github.com/sabre-io/vobject.git", + "reference": "900266bb3bd448a9f7f41f82344ad0aba237cb27" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/sabre-io/vobject/zipball/900266bb3bd448a9f7f41f82344ad0aba237cb27", + "reference": "900266bb3bd448a9f7f41f82344ad0aba237cb27", + "shasum": "" + }, + "require": { + "ext-mbstring": "*", + "php": "^7.1 || ^8.0", + "sabre/xml": "^2.1 || ^3.0 || ^4.0" + }, + "require-dev": { + "friendsofphp/php-cs-fixer": "~2.17.1", + "phpstan/phpstan": "^0.12 || ^1.11", + "phpunit/php-invoker": "^2.0 || ^3.1", + "phpunit/phpunit": "^7.5 || ^8.5 || ^9.6" + }, + "suggest": { + "hoa/bench": "If you would like to run the benchmark scripts" + }, + "time": "2024-10-14T11:53:54+00:00", + "bin": [ + "bin/vobject", + "bin/generate_vcards" + ], + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "4.0.x-dev" + } + }, + "installation-source": "dist", + "autoload": { + "psr-4": { + "Sabre\\VObject\\": "lib/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "BSD-3-Clause" + ], + "authors": [ + { + "name": "Evert Pot", + "email": "me@evertpot.com", + "homepage": "http://evertpot.com/", + "role": "Developer" + }, + { + "name": "Dominik Tobschall", + "email": "dominik@fruux.com", + "homepage": "http://tobschall.de/", + "role": "Developer" + }, + { + "name": "Ivan Enderlin", + "email": "ivan.enderlin@hoa-project.net", + "homepage": "http://mnt.io/", + "role": "Developer" + } + ], + "description": "The VObject library for PHP allows you to easily parse and manipulate iCalendar and vCard objects", + "homepage": "http://sabre.io/vobject/", + "keywords": [ + "availability", + "freebusy", + "iCalendar", + "ical", + "ics", + "jCal", + "jCard", + "recurrence", + "rfc2425", + "rfc2426", + "rfc2739", + "rfc4770", + "rfc5545", + "rfc5546", + "rfc6321", + "rfc6350", + "rfc6351", + "rfc6474", + "rfc6638", + "rfc6715", + "rfc6868", + "vCalendar", + "vCard", + "vcf", + "xCal", + "xCard" + ], + "support": { + "forum": "https://groups.google.com/group/sabredav-discuss", + "issues": "https://github.com/sabre-io/vobject/issues", + "source": "https://github.com/fruux/sabre-vobject" + }, + "install-path": "../sabre/vobject" + }, + { + "name": "sabre/xml", + "version": "2.2.11", + "version_normalized": "2.2.11.0", + "source": { + "type": "git", + "url": "https://github.com/sabre-io/xml.git", + "reference": "01a7927842abf3e10df3d9c2d9b0cc9d813a3fcc" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/sabre-io/xml/zipball/01a7927842abf3e10df3d9c2d9b0cc9d813a3fcc", + "reference": "01a7927842abf3e10df3d9c2d9b0cc9d813a3fcc", + "shasum": "" + }, + "require": { + "ext-dom": "*", + "ext-xmlreader": "*", + "ext-xmlwriter": "*", + "lib-libxml": ">=2.6.20", + "php": "^7.1 || ^8.0", + "sabre/uri": ">=1.0,<3.0.0" + }, + "require-dev": { + "friendsofphp/php-cs-fixer": "~2.17.1||3.63.2", + "phpstan/phpstan": "^0.12", + "phpunit/phpunit": "^7.5 || ^8.5 || ^9.6" + }, + "time": "2024-09-06T07:37:46+00:00", + "type": "library", + "installation-source": "dist", + "autoload": { + "files": [ + "lib/Deserializer/functions.php", + "lib/Serializer/functions.php" + ], + "psr-4": { + "Sabre\\Xml\\": "lib/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "BSD-3-Clause" + ], + "authors": [ + { + "name": "Evert Pot", + "email": "me@evertpot.com", + "homepage": "http://evertpot.com/", + "role": "Developer" + }, + { + "name": "Markus Staab", + "email": "markus.staab@redaxo.de", + "role": "Developer" + } + ], + "description": "sabre/xml is an XML library that you may not hate.", + "homepage": "https://sabre.io/xml/", + "keywords": [ + "XMLReader", + "XMLWriter", + "dom", + "xml" + ], + "support": { + "forum": "https://groups.google.com/group/sabredav-discuss", + "issues": "https://github.com/sabre-io/xml/issues", + "source": "https://github.com/fruux/sabre-xml" + }, + "install-path": "../sabre/xml" + }, + { + "name": "spomky-labs/cbor-php", + "version": "3.0.4", + "version_normalized": "3.0.4.0", + "source": { + "type": "git", + "url": "https://github.com/Spomky-Labs/cbor-php.git", + "reference": "658ed12a85a6b31fa312b89cd92f3a4ce6df4c6b" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/Spomky-Labs/cbor-php/zipball/658ed12a85a6b31fa312b89cd92f3a4ce6df4c6b", + "reference": "658ed12a85a6b31fa312b89cd92f3a4ce6df4c6b", + "shasum": "" + }, + "require": { + "brick/math": "^0.9|^0.10|^0.11|^0.12", + "ext-mbstring": "*", + "php": ">=8.0" + }, + "require-dev": { + "ekino/phpstan-banned-code": "^1.0", + "ext-json": "*", + "infection/infection": "^0.27", + "php-parallel-lint/php-parallel-lint": "^1.3", + "phpstan/extension-installer": "^1.1", + "phpstan/phpstan": "^1.0", + "phpstan/phpstan-beberlei-assert": "^1.0", + "phpstan/phpstan-deprecation-rules": "^1.0", + "phpstan/phpstan-phpunit": "^1.0", + "phpstan/phpstan-strict-rules": "^1.0", + "phpunit/phpunit": "^10.1", + "qossmic/deptrac-shim": "^1.0", + "rector/rector": "^0.19", + "roave/security-advisories": "dev-latest", + "symfony/var-dumper": "^6.0|^7.0", + "symplify/easy-coding-standard": "^12.0" + }, + "suggest": { + "ext-bcmath": "GMP or BCMath extensions will drastically improve the library performance. BCMath extension needed to handle the Big Float and Decimal Fraction Tags", + "ext-gmp": "GMP or BCMath extensions will drastically improve the library performance" + }, + "time": "2024-01-29T20:33:48+00:00", + "type": "library", + "installation-source": "dist", + "autoload": { + "psr-4": { + "CBOR\\": "src/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Florent Morselli", + "homepage": "https://github.com/Spomky" + }, + { + "name": "All contributors", + "homepage": "https://github.com/Spomky-Labs/cbor-php/contributors" + } + ], + "description": "CBOR Encoder/Decoder for PHP", + "keywords": [ + "Concise Binary Object Representation", + "RFC7049", + "cbor" + ], + "support": { + "issues": "https://github.com/Spomky-Labs/cbor-php/issues", + "source": "https://github.com/Spomky-Labs/cbor-php/tree/3.0.4" + }, + "funding": [ + { + "url": "https://github.com/Spomky", + "type": "github" + }, + { + "url": "https://www.patreon.com/FlorentMorselli", + "type": "patreon" + } + ], + "install-path": "../spomky-labs/cbor-php" + }, + { + "name": "spomky-labs/pki-framework", + "version": "1.2.1", + "version_normalized": "1.2.1.0", + "source": { + "type": "git", + "url": "https://github.com/Spomky-Labs/pki-framework.git", + "reference": "0b10c8b53366729417d6226ae89a665f9e2d61b6" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/Spomky-Labs/pki-framework/zipball/0b10c8b53366729417d6226ae89a665f9e2d61b6", + "reference": "0b10c8b53366729417d6226ae89a665f9e2d61b6", + "shasum": "" + }, + "require": { + "brick/math": "^0.10|^0.11|^0.12", + "ext-mbstring": "*", + "php": ">=8.1" + }, + "require-dev": { + "ekino/phpstan-banned-code": "^1.0", + "ext-gmp": "*", + "ext-openssl": "*", + "infection/infection": "^0.28", + "php-parallel-lint/php-parallel-lint": "^1.3", + "phpstan/extension-installer": "^1.3", + "phpstan/phpstan": "^1.8", + "phpstan/phpstan-beberlei-assert": "^1.0", + "phpstan/phpstan-deprecation-rules": "^1.0", + "phpstan/phpstan-phpunit": "^1.1", + "phpstan/phpstan-strict-rules": "^1.3", + "phpunit/phpunit": "^10.1|^11.0", + "rector/rector": "^1.0", + "roave/security-advisories": "dev-latest", + "symfony/phpunit-bridge": "^6.4|^7.0", + "symfony/string": "^6.4|^7.0", + "symfony/var-dumper": "^6.4|^7.0", + "symplify/easy-coding-standard": "^12.0" + }, + "suggest": { + "ext-bcmath": "For better performance (or GMP)", + "ext-gmp": "For better performance (or BCMath)", + "ext-openssl": "For OpenSSL based cyphering" + }, + "time": "2024-03-30T18:03:49+00:00", + "type": "library", + "installation-source": "dist", + "autoload": { + "psr-4": { + "SpomkyLabs\\Pki\\": "src/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Joni Eskelinen", + "email": "jonieske@gmail.com", + "role": "Original developer" + }, + { + "name": "Florent Morselli", + "email": "florent.morselli@spomky-labs.com", + "role": "Spomky-Labs PKI Framework developer" + } + ], + "description": "A PHP framework for managing Public Key Infrastructures. It comprises X.509 public key certificates, attribute certificates, certification requests and certification path validation.", + "homepage": "https://github.com/spomky-labs/pki-framework", + "keywords": [ + "DER", + "Private Key", + "ac", + "algorithm identifier", + "asn.1", + "asn1", + "attribute certificate", + "certificate", + "certification request", + "cryptography", + "csr", + "decrypt", + "ec", + "encrypt", + "pem", + "pkcs", + "public key", + "rsa", + "sign", + "signature", + "verify", + "x.509", + "x.690", + "x509", + "x690" + ], + "support": { + "issues": "https://github.com/Spomky-Labs/pki-framework/issues", + "source": "https://github.com/Spomky-Labs/pki-framework/tree/1.2.1" + }, + "funding": [ + { + "url": "https://github.com/Spomky", + "type": "github" + }, + { + "url": "https://www.patreon.com/FlorentMorselli", + "type": "patreon" + } + ], + "install-path": "../spomky-labs/pki-framework" + }, + { + "name": "stecman/symfony-console-completion", + "version": "v0.14.0", + "version_normalized": "0.14.0.0", + "source": { + "type": "git", + "url": "https://github.com/stecman/symfony-console-completion.git", + "reference": "93b88586e50c05b7375a547031f535bf1ea970f5" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/stecman/symfony-console-completion/zipball/93b88586e50c05b7375a547031f535bf1ea970f5", + "reference": "93b88586e50c05b7375a547031f535bf1ea970f5", + "shasum": "" + }, + "require": { + "php": ">=8.1", + "symfony/console": "~6.4 || ^7.1" + }, + "require-dev": { + "dms/phpunit-arraysubset-asserts": "^0.4.0", + "phpunit/phpunit": "^9.5" + }, + "time": "2024-11-09T03:07:26+00:00", + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "0.14.x-dev" + } + }, + "installation-source": "dist", + "autoload": { + "psr-4": { + "Stecman\\Component\\Symfony\\Console\\BashCompletion\\": "src/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Stephen Holdaway", + "email": "stephen@stecman.co.nz" + } + ], + "description": "Automatic BASH completion for Symfony Console Component based applications.", + "support": { + "issues": "https://github.com/stecman/symfony-console-completion/issues", + "source": "https://github.com/stecman/symfony-console-completion/tree/v0.14.0" + }, + "install-path": "../stecman/symfony-console-completion" + }, + { + "name": "symfony/console", + "version": "v6.4.17", + "version_normalized": "6.4.17.0", + "source": { + "type": "git", + "url": "https://github.com/symfony/console.git", + "reference": "799445db3f15768ecc382ac5699e6da0520a0a04" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/symfony/console/zipball/799445db3f15768ecc382ac5699e6da0520a0a04", + "reference": "799445db3f15768ecc382ac5699e6da0520a0a04", + "shasum": "" + }, + "require": { + "php": ">=8.1", + "symfony/deprecation-contracts": "^2.5|^3", + "symfony/polyfill-mbstring": "~1.0", + "symfony/service-contracts": "^2.5|^3", + "symfony/string": "^5.4|^6.0|^7.0" + }, + "conflict": { + "symfony/dependency-injection": "<5.4", + "symfony/dotenv": "<5.4", + "symfony/event-dispatcher": "<5.4", + "symfony/lock": "<5.4", + "symfony/process": "<5.4" + }, + "provide": { + "psr/log-implementation": "1.0|2.0|3.0" + }, + "require-dev": { + "psr/log": "^1|^2|^3", + "symfony/config": "^5.4|^6.0|^7.0", + "symfony/dependency-injection": "^5.4|^6.0|^7.0", + "symfony/event-dispatcher": "^5.4|^6.0|^7.0", + "symfony/http-foundation": "^6.4|^7.0", + "symfony/http-kernel": "^6.4|^7.0", + "symfony/lock": "^5.4|^6.0|^7.0", + "symfony/messenger": "^5.4|^6.0|^7.0", + "symfony/process": "^5.4|^6.0|^7.0", + "symfony/stopwatch": "^5.4|^6.0|^7.0", + "symfony/var-dumper": "^5.4|^6.0|^7.0" + }, + "time": "2024-12-07T12:07:30+00:00", + "type": "library", + "installation-source": "dist", + "autoload": { + "psr-4": { + "Symfony\\Component\\Console\\": "" + }, + "exclude-from-classmap": [ + "/Tests/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Fabien Potencier", + "email": "fabien@symfony.com" + }, + { + "name": "Symfony Community", + "homepage": "https://symfony.com/contributors" + } + ], + "description": "Eases the creation of beautiful and testable command line interfaces", + "homepage": "https://symfony.com", + "keywords": [ + "cli", + "command-line", + "console", + "terminal" + ], + "support": { + "source": "https://github.com/symfony/console/tree/v6.4.17" + }, + "funding": [ + { + "url": "https://symfony.com/sponsor", + "type": "custom" + }, + { + "url": "https://github.com/fabpot", + "type": "github" + }, + { + "url": "https://tidelift.com/funding/github/packagist/symfony/symfony", + "type": "tidelift" + } + ], + "install-path": "../symfony/console" + }, + { + "name": "symfony/css-selector", + "version": "v6.4.13", + "version_normalized": "6.4.13.0", + "source": { + "type": "git", + "url": "https://github.com/symfony/css-selector.git", + "reference": "cb23e97813c5837a041b73a6d63a9ddff0778f5e" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/symfony/css-selector/zipball/cb23e97813c5837a041b73a6d63a9ddff0778f5e", + "reference": "cb23e97813c5837a041b73a6d63a9ddff0778f5e", + "shasum": "" + }, + "require": { + "php": ">=8.1" + }, + "time": "2024-09-25T14:18:03+00:00", + "type": "library", + "installation-source": "dist", + "autoload": { + "psr-4": { + "Symfony\\Component\\CssSelector\\": "" + }, + "exclude-from-classmap": [ + "/Tests/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Fabien Potencier", + "email": "fabien@symfony.com" + }, + { + "name": "Jean-François Simon", + "email": "jeanfrancois.simon@sensiolabs.com" + }, + { + "name": "Symfony Community", + "homepage": "https://symfony.com/contributors" + } + ], + "description": "Converts CSS selectors to XPath expressions", + "homepage": "https://symfony.com", + "support": { + "source": "https://github.com/symfony/css-selector/tree/v6.4.13" + }, + "funding": [ + { + "url": "https://symfony.com/sponsor", + "type": "custom" + }, + { + "url": "https://github.com/fabpot", + "type": "github" + }, + { + "url": "https://tidelift.com/funding/github/packagist/symfony/symfony", + "type": "tidelift" + } + ], + "install-path": "../symfony/css-selector" + }, + { + "name": "symfony/deprecation-contracts", + "version": "v3.6.0", + "version_normalized": "3.6.0.0", + "source": { + "type": "git", + "url": "https://github.com/symfony/deprecation-contracts.git", + "reference": "63afe740e99a13ba87ec199bb07bbdee937a5b62" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/symfony/deprecation-contracts/zipball/63afe740e99a13ba87ec199bb07bbdee937a5b62", + "reference": "63afe740e99a13ba87ec199bb07bbdee937a5b62", + "shasum": "" + }, + "require": { + "php": ">=8.1" + }, + "time": "2024-09-25T14:21:43+00:00", + "type": "library", + "extra": { + "thanks": { + "url": "https://github.com/symfony/contracts", + "name": "symfony/contracts" + }, + "branch-alias": { + "dev-main": "3.6-dev" + } + }, + "installation-source": "dist", + "autoload": { + "files": [ + "function.php" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Nicolas Grekas", + "email": "p@tchwork.com" + }, + { + "name": "Symfony Community", + "homepage": "https://symfony.com/contributors" + } + ], + "description": "A generic function and convention to trigger deprecation notices", + "homepage": "https://symfony.com", + "support": { + "source": "https://github.com/symfony/deprecation-contracts/tree/v3.6.0" + }, + "funding": [ + { + "url": "https://symfony.com/sponsor", + "type": "custom" + }, + { + "url": "https://github.com/fabpot", + "type": "github" + }, + { + "url": "https://tidelift.com/funding/github/packagist/symfony/symfony", + "type": "tidelift" + } + ], + "install-path": "../symfony/deprecation-contracts" + }, + { + "name": "symfony/dom-crawler", + "version": "v6.4.23", + "version_normalized": "6.4.23.0", + "source": { + "type": "git", + "url": "https://github.com/symfony/dom-crawler.git", + "reference": "22210aacb35dbadd772325d759d17bce2374a84d" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/symfony/dom-crawler/zipball/22210aacb35dbadd772325d759d17bce2374a84d", + "reference": "22210aacb35dbadd772325d759d17bce2374a84d", + "shasum": "" + }, + "require": { + "masterminds/html5": "^2.6", + "php": ">=8.1", + "symfony/polyfill-ctype": "~1.8", + "symfony/polyfill-mbstring": "~1.0" + }, + "require-dev": { + "symfony/css-selector": "^5.4|^6.0|^7.0" + }, + "time": "2025-06-13T12:10:00+00:00", + "type": "library", + "installation-source": "dist", + "autoload": { + "psr-4": { + "Symfony\\Component\\DomCrawler\\": "" + }, + "exclude-from-classmap": [ + "/Tests/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Fabien Potencier", + "email": "fabien@symfony.com" + }, + { + "name": "Symfony Community", + "homepage": "https://symfony.com/contributors" + } + ], + "description": "Eases DOM navigation for HTML and XML documents", + "homepage": "https://symfony.com", + "support": { + "source": "https://github.com/symfony/dom-crawler/tree/v6.4.23" + }, + "funding": [ + { + "url": "https://symfony.com/sponsor", + "type": "custom" + }, + { + "url": "https://github.com/fabpot", + "type": "github" + }, + { + "url": "https://tidelift.com/funding/github/packagist/symfony/symfony", + "type": "tidelift" + } + ], + "install-path": "../symfony/dom-crawler" + }, + { + "name": "symfony/event-dispatcher", + "version": "v6.4.8", + "version_normalized": "6.4.8.0", + "source": { + "type": "git", + "url": "https://github.com/symfony/event-dispatcher.git", + "reference": "8d7507f02b06e06815e56bb39aa0128e3806208b" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/symfony/event-dispatcher/zipball/8d7507f02b06e06815e56bb39aa0128e3806208b", + "reference": "8d7507f02b06e06815e56bb39aa0128e3806208b", + "shasum": "" + }, + "require": { + "php": ">=8.1", + "symfony/event-dispatcher-contracts": "^2.5|^3" + }, + "conflict": { + "symfony/dependency-injection": "<5.4", + "symfony/service-contracts": "<2.5" + }, + "provide": { + "psr/event-dispatcher-implementation": "1.0", + "symfony/event-dispatcher-implementation": "2.0|3.0" + }, + "require-dev": { + "psr/log": "^1|^2|^3", + "symfony/config": "^5.4|^6.0|^7.0", + "symfony/dependency-injection": "^5.4|^6.0|^7.0", + "symfony/error-handler": "^5.4|^6.0|^7.0", + "symfony/expression-language": "^5.4|^6.0|^7.0", + "symfony/http-foundation": "^5.4|^6.0|^7.0", + "symfony/service-contracts": "^2.5|^3", + "symfony/stopwatch": "^5.4|^6.0|^7.0" + }, + "time": "2024-05-31T14:49:08+00:00", + "type": "library", + "installation-source": "dist", + "autoload": { + "psr-4": { + "Symfony\\Component\\EventDispatcher\\": "" + }, + "exclude-from-classmap": [ + "/Tests/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Fabien Potencier", + "email": "fabien@symfony.com" + }, + { + "name": "Symfony Community", + "homepage": "https://symfony.com/contributors" + } + ], + "description": "Provides tools that allow your application components to communicate with each other by dispatching events and listening to them", + "homepage": "https://symfony.com", + "support": { + "source": "https://github.com/symfony/event-dispatcher/tree/v6.4.8" + }, + "funding": [ + { + "url": "https://symfony.com/sponsor", + "type": "custom" + }, + { + "url": "https://github.com/fabpot", + "type": "github" + }, + { + "url": "https://tidelift.com/funding/github/packagist/symfony/symfony", + "type": "tidelift" + } + ], + "install-path": "../symfony/event-dispatcher" + }, + { + "name": "symfony/event-dispatcher-contracts", + "version": "v3.5.0", + "version_normalized": "3.5.0.0", + "source": { + "type": "git", + "url": "https://github.com/symfony/event-dispatcher-contracts.git", + "reference": "8f93aec25d41b72493c6ddff14e916177c9efc50" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/symfony/event-dispatcher-contracts/zipball/8f93aec25d41b72493c6ddff14e916177c9efc50", + "reference": "8f93aec25d41b72493c6ddff14e916177c9efc50", + "shasum": "" + }, + "require": { + "php": ">=8.1", + "psr/event-dispatcher": "^1" + }, + "time": "2024-04-18T09:32:20+00:00", + "type": "library", + "extra": { + "branch-alias": { + "dev-main": "3.5-dev" + }, + "thanks": { + "name": "symfony/contracts", + "url": "https://github.com/symfony/contracts" + } + }, + "installation-source": "dist", + "autoload": { + "psr-4": { + "Symfony\\Contracts\\EventDispatcher\\": "" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Nicolas Grekas", + "email": "p@tchwork.com" + }, + { + "name": "Symfony Community", + "homepage": "https://symfony.com/contributors" + } + ], + "description": "Generic abstractions related to dispatching event", + "homepage": "https://symfony.com", + "keywords": [ + "abstractions", + "contracts", + "decoupling", + "interfaces", + "interoperability", + "standards" + ], + "support": { + "source": "https://github.com/symfony/event-dispatcher-contracts/tree/v3.5.0" + }, + "funding": [ + { + "url": "https://symfony.com/sponsor", + "type": "custom" + }, + { + "url": "https://github.com/fabpot", + "type": "github" + }, + { + "url": "https://tidelift.com/funding/github/packagist/symfony/symfony", + "type": "tidelift" + } + ], + "install-path": "../symfony/event-dispatcher-contracts" + }, + { + "name": "symfony/http-foundation", + "version": "v6.4.29", + "version_normalized": "6.4.29.0", + "source": { + "type": "git", + "url": "https://github.com/symfony/http-foundation.git", + "reference": "b03d11e015552a315714c127d8d1e0f9e970ec88" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/symfony/http-foundation/zipball/b03d11e015552a315714c127d8d1e0f9e970ec88", + "reference": "b03d11e015552a315714c127d8d1e0f9e970ec88", + "shasum": "" + }, + "require": { + "php": ">=8.1", + "symfony/deprecation-contracts": "^2.5|^3", + "symfony/polyfill-mbstring": "~1.1", + "symfony/polyfill-php83": "^1.27" + }, + "conflict": { + "symfony/cache": "<6.4.12|>=7.0,<7.1.5" + }, + "require-dev": { + "doctrine/dbal": "^2.13.1|^3|^4", + "predis/predis": "^1.1|^2.0", + "symfony/cache": "^6.4.12|^7.1.5", + "symfony/dependency-injection": "^5.4|^6.0|^7.0", + "symfony/expression-language": "^5.4|^6.0|^7.0", + "symfony/http-kernel": "^5.4.12|^6.0.12|^6.1.4|^7.0", + "symfony/mime": "^5.4|^6.0|^7.0", + "symfony/rate-limiter": "^5.4|^6.0|^7.0" + }, + "time": "2025-11-08T16:40:12+00:00", + "type": "library", + "installation-source": "dist", + "autoload": { + "psr-4": { + "Symfony\\Component\\HttpFoundation\\": "" + }, + "exclude-from-classmap": [ + "/Tests/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Fabien Potencier", + "email": "fabien@symfony.com" + }, + { + "name": "Symfony Community", + "homepage": "https://symfony.com/contributors" + } + ], + "description": "Defines an object-oriented layer for the HTTP specification", + "homepage": "https://symfony.com", + "support": { + "source": "https://github.com/symfony/http-foundation/tree/v6.4.29" + }, + "funding": [ + { + "url": "https://symfony.com/sponsor", + "type": "custom" + }, + { + "url": "https://github.com/fabpot", + "type": "github" + }, + { + "url": "https://github.com/nicolas-grekas", + "type": "github" + }, + { + "url": "https://tidelift.com/funding/github/packagist/symfony/symfony", + "type": "tidelift" + } + ], + "install-path": "../symfony/http-foundation" + }, + { + "name": "symfony/mailer", + "version": "v6.4.12", + "version_normalized": "6.4.12.0", + "source": { + "type": "git", + "url": "https://github.com/symfony/mailer.git", + "reference": "b6a25408c569ae2366b3f663a4edad19420a9c26" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/symfony/mailer/zipball/b6a25408c569ae2366b3f663a4edad19420a9c26", + "reference": "b6a25408c569ae2366b3f663a4edad19420a9c26", + "shasum": "" + }, + "require": { + "egulias/email-validator": "^2.1.10|^3|^4", + "php": ">=8.1", + "psr/event-dispatcher": "^1", + "psr/log": "^1|^2|^3", + "symfony/event-dispatcher": "^5.4|^6.0|^7.0", + "symfony/mime": "^6.2|^7.0", + "symfony/service-contracts": "^2.5|^3" + }, + "conflict": { + "symfony/http-client-contracts": "<2.5", + "symfony/http-kernel": "<5.4", + "symfony/messenger": "<6.2", + "symfony/mime": "<6.2", + "symfony/twig-bridge": "<6.2.1" + }, + "require-dev": { + "symfony/console": "^5.4|^6.0|^7.0", + "symfony/http-client": "^5.4|^6.0|^7.0", + "symfony/messenger": "^6.2|^7.0", + "symfony/twig-bridge": "^6.2|^7.0" + }, + "time": "2024-09-08T12:30:05+00:00", + "type": "library", + "installation-source": "dist", + "autoload": { + "psr-4": { + "Symfony\\Component\\Mailer\\": "" + }, + "exclude-from-classmap": [ + "/Tests/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Fabien Potencier", + "email": "fabien@symfony.com" + }, + { + "name": "Symfony Community", + "homepage": "https://symfony.com/contributors" + } + ], + "description": "Helps sending emails", + "homepage": "https://symfony.com", + "support": { + "source": "https://github.com/symfony/mailer/tree/v6.4.12" + }, + "funding": [ + { + "url": "https://symfony.com/sponsor", + "type": "custom" + }, + { + "url": "https://github.com/fabpot", + "type": "github" + }, + { + "url": "https://tidelift.com/funding/github/packagist/symfony/symfony", + "type": "tidelift" + } + ], + "install-path": "../symfony/mailer" + }, + { + "name": "symfony/mime", + "version": "v6.4.12", + "version_normalized": "6.4.12.0", + "source": { + "type": "git", + "url": "https://github.com/symfony/mime.git", + "reference": "abe16ee7790b16aa525877419deb0f113953f0e1" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/symfony/mime/zipball/abe16ee7790b16aa525877419deb0f113953f0e1", + "reference": "abe16ee7790b16aa525877419deb0f113953f0e1", + "shasum": "" + }, + "require": { + "php": ">=8.1", + "symfony/deprecation-contracts": "^2.5|^3", + "symfony/polyfill-intl-idn": "^1.10", + "symfony/polyfill-mbstring": "^1.0" + }, + "conflict": { + "egulias/email-validator": "~3.0.0", + "phpdocumentor/reflection-docblock": "<3.2.2", + "phpdocumentor/type-resolver": "<1.4.0", + "symfony/mailer": "<5.4", + "symfony/serializer": "<6.4.3|>7.0,<7.0.3" + }, + "require-dev": { + "egulias/email-validator": "^2.1.10|^3.1|^4", + "league/html-to-markdown": "^5.0", + "phpdocumentor/reflection-docblock": "^3.0|^4.0|^5.0", + "symfony/dependency-injection": "^5.4|^6.0|^7.0", + "symfony/process": "^5.4|^6.4|^7.0", + "symfony/property-access": "^5.4|^6.0|^7.0", + "symfony/property-info": "^5.4|^6.0|^7.0", + "symfony/serializer": "^6.4.3|^7.0.3" + }, + "time": "2024-09-20T08:18:25+00:00", + "type": "library", + "installation-source": "dist", + "autoload": { + "psr-4": { + "Symfony\\Component\\Mime\\": "" + }, + "exclude-from-classmap": [ + "/Tests/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Fabien Potencier", + "email": "fabien@symfony.com" + }, + { + "name": "Symfony Community", + "homepage": "https://symfony.com/contributors" + } + ], + "description": "Allows manipulating MIME messages", + "homepage": "https://symfony.com", + "keywords": [ + "mime", + "mime-type" + ], + "support": { + "source": "https://github.com/symfony/mime/tree/v6.4.12" + }, + "funding": [ + { + "url": "https://symfony.com/sponsor", + "type": "custom" + }, + { + "url": "https://github.com/fabpot", + "type": "github" + }, + { + "url": "https://tidelift.com/funding/github/packagist/symfony/symfony", + "type": "tidelift" + } + ], + "install-path": "../symfony/mime" + }, + { + "name": "symfony/polyfill-intl-grapheme", + "version": "v1.32.0", + "version_normalized": "1.32.0.0", + "source": { + "type": "git", + "url": "https://github.com/symfony/polyfill-intl-grapheme.git", + "reference": "b9123926e3b7bc2f98c02ad54f6a4b02b91a8abe" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/symfony/polyfill-intl-grapheme/zipball/b9123926e3b7bc2f98c02ad54f6a4b02b91a8abe", + "reference": "b9123926e3b7bc2f98c02ad54f6a4b02b91a8abe", + "shasum": "" + }, + "require": { + "php": ">=7.2" + }, + "suggest": { + "ext-intl": "For best performance" + }, + "time": "2024-09-09T11:45:10+00:00", + "type": "library", + "extra": { + "thanks": { + "url": "https://github.com/symfony/polyfill", + "name": "symfony/polyfill" + } + }, + "installation-source": "dist", + "autoload": { + "files": [ + "bootstrap.php" + ], + "psr-4": { + "Symfony\\Polyfill\\Intl\\Grapheme\\": "" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Nicolas Grekas", + "email": "p@tchwork.com" + }, + { + "name": "Symfony Community", + "homepage": "https://symfony.com/contributors" + } + ], + "description": "Symfony polyfill for intl's grapheme_* functions", + "homepage": "https://symfony.com", + "keywords": [ + "compatibility", + "grapheme", + "intl", + "polyfill", + "portable", + "shim" + ], + "support": { + "source": "https://github.com/symfony/polyfill-intl-grapheme/tree/v1.32.0" + }, + "funding": [ + { + "url": "https://symfony.com/sponsor", + "type": "custom" + }, + { + "url": "https://github.com/fabpot", + "type": "github" + }, + { + "url": "https://tidelift.com/funding/github/packagist/symfony/symfony", + "type": "tidelift" + } + ], + "install-path": "../symfony/polyfill-intl-grapheme" + }, + { + "name": "symfony/polyfill-intl-idn", + "version": "v1.32.0", + "version_normalized": "1.32.0.0", + "source": { + "type": "git", + "url": "https://github.com/symfony/polyfill-intl-idn.git", + "reference": "9614ac4d8061dc257ecc64cba1b140873dce8ad3" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/symfony/polyfill-intl-idn/zipball/9614ac4d8061dc257ecc64cba1b140873dce8ad3", + "reference": "9614ac4d8061dc257ecc64cba1b140873dce8ad3", + "shasum": "" + }, + "require": { + "php": ">=7.2", + "symfony/polyfill-intl-normalizer": "^1.10" + }, + "suggest": { + "ext-intl": "For best performance" + }, + "time": "2024-09-10T14:38:51+00:00", + "type": "library", + "extra": { + "thanks": { + "url": "https://github.com/symfony/polyfill", + "name": "symfony/polyfill" + } + }, + "installation-source": "dist", + "autoload": { + "files": [ + "bootstrap.php" + ], + "psr-4": { + "Symfony\\Polyfill\\Intl\\Idn\\": "" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Laurent Bassin", + "email": "laurent@bassin.info" + }, + { + "name": "Trevor Rowbotham", + "email": "trevor.rowbotham@pm.me" + }, + { + "name": "Symfony Community", + "homepage": "https://symfony.com/contributors" + } + ], + "description": "Symfony polyfill for intl's idn_to_ascii and idn_to_utf8 functions", + "homepage": "https://symfony.com", + "keywords": [ + "compatibility", + "idn", + "intl", + "polyfill", + "portable", + "shim" + ], + "support": { + "source": "https://github.com/symfony/polyfill-intl-idn/tree/v1.32.0" + }, + "funding": [ + { + "url": "https://symfony.com/sponsor", + "type": "custom" + }, + { + "url": "https://github.com/fabpot", + "type": "github" + }, + { + "url": "https://tidelift.com/funding/github/packagist/symfony/symfony", + "type": "tidelift" + } + ], + "install-path": "../symfony/polyfill-intl-idn" + }, + { + "name": "symfony/polyfill-intl-normalizer", + "version": "v1.32.0", + "version_normalized": "1.32.0.0", + "source": { + "type": "git", + "url": "https://github.com/symfony/polyfill-intl-normalizer.git", + "reference": "3833d7255cc303546435cb650316bff708a1c75c" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/symfony/polyfill-intl-normalizer/zipball/3833d7255cc303546435cb650316bff708a1c75c", + "reference": "3833d7255cc303546435cb650316bff708a1c75c", + "shasum": "" + }, + "require": { + "php": ">=7.2" + }, + "suggest": { + "ext-intl": "For best performance" + }, + "time": "2024-09-09T11:45:10+00:00", + "type": "library", + "extra": { + "thanks": { + "url": "https://github.com/symfony/polyfill", + "name": "symfony/polyfill" + } + }, + "installation-source": "dist", + "autoload": { + "files": [ + "bootstrap.php" + ], + "psr-4": { + "Symfony\\Polyfill\\Intl\\Normalizer\\": "" + }, + "classmap": [ + "Resources/stubs" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Nicolas Grekas", + "email": "p@tchwork.com" + }, + { + "name": "Symfony Community", + "homepage": "https://symfony.com/contributors" + } + ], + "description": "Symfony polyfill for intl's Normalizer class and related functions", + "homepage": "https://symfony.com", + "keywords": [ + "compatibility", + "intl", + "normalizer", + "polyfill", + "portable", + "shim" + ], + "support": { + "source": "https://github.com/symfony/polyfill-intl-normalizer/tree/v1.32.0" + }, + "funding": [ + { + "url": "https://symfony.com/sponsor", + "type": "custom" + }, + { + "url": "https://github.com/fabpot", + "type": "github" + }, + { + "url": "https://tidelift.com/funding/github/packagist/symfony/symfony", + "type": "tidelift" + } + ], + "install-path": "../symfony/polyfill-intl-normalizer" + }, + { + "name": "symfony/polyfill-php82", + "version": "v1.32.0", + "version_normalized": "1.32.0.0", + "source": { + "type": "git", + "url": "https://github.com/symfony/polyfill-php82.git", + "reference": "5d2ed36f7734637dacc025f179698031951b1692" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/symfony/polyfill-php82/zipball/5d2ed36f7734637dacc025f179698031951b1692", + "reference": "5d2ed36f7734637dacc025f179698031951b1692", + "shasum": "" + }, + "require": { + "php": ">=7.2" + }, + "time": "2024-09-09T11:45:10+00:00", + "type": "library", + "extra": { + "thanks": { + "url": "https://github.com/symfony/polyfill", + "name": "symfony/polyfill" + } + }, + "installation-source": "dist", + "autoload": { + "files": [ + "bootstrap.php" + ], + "psr-4": { + "Symfony\\Polyfill\\Php82\\": "" + }, + "classmap": [ + "Resources/stubs" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Nicolas Grekas", + "email": "p@tchwork.com" + }, + { + "name": "Symfony Community", + "homepage": "https://symfony.com/contributors" + } + ], + "description": "Symfony polyfill backporting some PHP 8.2+ features to lower PHP versions", + "homepage": "https://symfony.com", + "keywords": [ + "compatibility", + "polyfill", + "portable", + "shim" + ], + "support": { + "source": "https://github.com/symfony/polyfill-php82/tree/v1.32.0" + }, + "funding": [ + { + "url": "https://symfony.com/sponsor", + "type": "custom" + }, + { + "url": "https://github.com/fabpot", + "type": "github" + }, + { + "url": "https://tidelift.com/funding/github/packagist/symfony/symfony", + "type": "tidelift" + } + ], + "install-path": "../symfony/polyfill-php82" + }, + { + "name": "symfony/polyfill-php83", + "version": "v1.32.0", + "version_normalized": "1.32.0.0", + "source": { + "type": "git", + "url": "https://github.com/symfony/polyfill-php83.git", + "reference": "2fb86d65e2d424369ad2905e83b236a8805ba491" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/symfony/polyfill-php83/zipball/2fb86d65e2d424369ad2905e83b236a8805ba491", + "reference": "2fb86d65e2d424369ad2905e83b236a8805ba491", + "shasum": "" + }, + "require": { + "php": ">=7.2" + }, + "time": "2024-09-09T11:45:10+00:00", + "type": "library", + "extra": { + "thanks": { + "url": "https://github.com/symfony/polyfill", + "name": "symfony/polyfill" + } + }, + "installation-source": "dist", + "autoload": { + "files": [ + "bootstrap.php" + ], + "psr-4": { + "Symfony\\Polyfill\\Php83\\": "" + }, + "classmap": [ + "Resources/stubs" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Nicolas Grekas", + "email": "p@tchwork.com" + }, + { + "name": "Symfony Community", + "homepage": "https://symfony.com/contributors" + } + ], + "description": "Symfony polyfill backporting some PHP 8.3+ features to lower PHP versions", + "homepage": "https://symfony.com", + "keywords": [ + "compatibility", + "polyfill", + "portable", + "shim" + ], + "support": { + "source": "https://github.com/symfony/polyfill-php83/tree/v1.32.0" + }, + "funding": [ + { + "url": "https://symfony.com/sponsor", + "type": "custom" + }, + { + "url": "https://github.com/fabpot", + "type": "github" + }, + { + "url": "https://tidelift.com/funding/github/packagist/symfony/symfony", + "type": "tidelift" + } + ], + "install-path": "../symfony/polyfill-php83" + }, + { + "name": "symfony/polyfill-php84", + "version": "v1.32.0", + "version_normalized": "1.32.0.0", + "source": { + "type": "git", + "url": "https://github.com/symfony/polyfill-php84.git", + "reference": "000df7860439609837bbe28670b0be15783b7fbf" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/symfony/polyfill-php84/zipball/000df7860439609837bbe28670b0be15783b7fbf", + "reference": "000df7860439609837bbe28670b0be15783b7fbf", + "shasum": "" + }, + "require": { + "php": ">=7.2" + }, + "time": "2025-02-20T12:04:08+00:00", + "type": "library", + "extra": { + "thanks": { + "url": "https://github.com/symfony/polyfill", + "name": "symfony/polyfill" + } + }, + "installation-source": "dist", + "autoload": { + "files": [ + "bootstrap.php" + ], + "psr-4": { + "Symfony\\Polyfill\\Php84\\": "" + }, + "classmap": [ + "Resources/stubs" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Nicolas Grekas", + "email": "p@tchwork.com" + }, + { + "name": "Symfony Community", + "homepage": "https://symfony.com/contributors" + } + ], + "description": "Symfony polyfill backporting some PHP 8.4+ features to lower PHP versions", + "homepage": "https://symfony.com", + "keywords": [ + "compatibility", + "polyfill", + "portable", + "shim" + ], + "support": { + "source": "https://github.com/symfony/polyfill-php84/tree/v1.32.0" + }, + "funding": [ + { + "url": "https://symfony.com/sponsor", + "type": "custom" + }, + { + "url": "https://github.com/fabpot", + "type": "github" + }, + { + "url": "https://tidelift.com/funding/github/packagist/symfony/symfony", + "type": "tidelift" + } + ], + "install-path": "../symfony/polyfill-php84" + }, + { + "name": "symfony/polyfill-uuid", + "version": "v1.29.0", + "version_normalized": "1.29.0.0", + "source": { + "type": "git", + "url": "https://github.com/symfony/polyfill-uuid.git", + "reference": "3abdd21b0ceaa3000ee950097bc3cf9efc137853" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/symfony/polyfill-uuid/zipball/3abdd21b0ceaa3000ee950097bc3cf9efc137853", + "reference": "3abdd21b0ceaa3000ee950097bc3cf9efc137853", + "shasum": "" + }, + "require": { + "php": ">=7.1" + }, + "provide": { + "ext-uuid": "*" + }, + "suggest": { + "ext-uuid": "For best performance" + }, + "time": "2024-01-29T20:11:03+00:00", + "type": "library", + "extra": { + "thanks": { + "name": "symfony/polyfill", + "url": "https://github.com/symfony/polyfill" + } + }, + "installation-source": "dist", + "autoload": { + "files": [ + "bootstrap.php" + ], + "psr-4": { + "Symfony\\Polyfill\\Uuid\\": "" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Grégoire Pineau", + "email": "lyrixx@lyrixx.info" + }, + { + "name": "Symfony Community", + "homepage": "https://symfony.com/contributors" + } + ], + "description": "Symfony polyfill for uuid functions", + "homepage": "https://symfony.com", + "keywords": [ + "compatibility", + "polyfill", + "portable", + "uuid" + ], + "support": { + "source": "https://github.com/symfony/polyfill-uuid/tree/v1.29.0" + }, + "funding": [ + { + "url": "https://symfony.com/sponsor", + "type": "custom" + }, + { + "url": "https://github.com/fabpot", + "type": "github" + }, + { + "url": "https://tidelift.com/funding/github/packagist/symfony/symfony", + "type": "tidelift" + } + ], + "install-path": "../symfony/polyfill-uuid" + }, + { + "name": "symfony/process", + "version": "v6.4.15", + "version_normalized": "6.4.15.0", + "source": { + "type": "git", + "url": "https://github.com/symfony/process.git", + "reference": "3cb242f059c14ae08591c5c4087d1fe443564392" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/symfony/process/zipball/3cb242f059c14ae08591c5c4087d1fe443564392", + "reference": "3cb242f059c14ae08591c5c4087d1fe443564392", + "shasum": "" + }, + "require": { + "php": ">=8.1" + }, + "time": "2024-11-06T14:19:14+00:00", + "type": "library", + "installation-source": "dist", + "autoload": { + "psr-4": { + "Symfony\\Component\\Process\\": "" + }, + "exclude-from-classmap": [ + "/Tests/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Fabien Potencier", + "email": "fabien@symfony.com" + }, + { + "name": "Symfony Community", + "homepage": "https://symfony.com/contributors" + } + ], + "description": "Executes commands in sub-processes", + "homepage": "https://symfony.com", + "support": { + "source": "https://github.com/symfony/process/tree/v6.4.15" + }, + "funding": [ + { + "url": "https://symfony.com/sponsor", + "type": "custom" + }, + { + "url": "https://github.com/fabpot", + "type": "github" + }, + { + "url": "https://tidelift.com/funding/github/packagist/symfony/symfony", + "type": "tidelift" + } + ], + "install-path": "../symfony/process" + }, + { + "name": "symfony/routing", + "version": "v6.4.12", + "version_normalized": "6.4.12.0", + "source": { + "type": "git", + "url": "https://github.com/symfony/routing.git", + "reference": "a7c8036bd159486228dc9be3e846a00a0dda9f9f" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/symfony/routing/zipball/a7c8036bd159486228dc9be3e846a00a0dda9f9f", + "reference": "a7c8036bd159486228dc9be3e846a00a0dda9f9f", + "shasum": "" + }, + "require": { + "php": ">=8.1", + "symfony/deprecation-contracts": "^2.5|^3" + }, + "conflict": { + "doctrine/annotations": "<1.12", + "symfony/config": "<6.2", + "symfony/dependency-injection": "<5.4", + "symfony/yaml": "<5.4" + }, + "require-dev": { + "doctrine/annotations": "^1.12|^2", + "psr/log": "^1|^2|^3", + "symfony/config": "^6.2|^7.0", + "symfony/dependency-injection": "^5.4|^6.0|^7.0", + "symfony/expression-language": "^5.4|^6.0|^7.0", + "symfony/http-foundation": "^5.4|^6.0|^7.0", + "symfony/yaml": "^5.4|^6.0|^7.0" + }, + "time": "2024-09-20T08:32:26+00:00", + "type": "library", + "installation-source": "dist", + "autoload": { + "psr-4": { + "Symfony\\Component\\Routing\\": "" + }, + "exclude-from-classmap": [ + "/Tests/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Fabien Potencier", + "email": "fabien@symfony.com" + }, + { + "name": "Symfony Community", + "homepage": "https://symfony.com/contributors" + } + ], + "description": "Maps an HTTP request to a set of configuration variables", + "homepage": "https://symfony.com", + "keywords": [ + "router", + "routing", + "uri", + "url" + ], + "support": { + "source": "https://github.com/symfony/routing/tree/v6.4.12" + }, + "funding": [ + { + "url": "https://symfony.com/sponsor", + "type": "custom" + }, + { + "url": "https://github.com/fabpot", + "type": "github" + }, + { + "url": "https://tidelift.com/funding/github/packagist/symfony/symfony", + "type": "tidelift" + } + ], + "install-path": "../symfony/routing" + }, + { + "name": "symfony/service-contracts", + "version": "v3.5.1", + "version_normalized": "3.5.1.0", + "source": { + "type": "git", + "url": "https://github.com/symfony/service-contracts.git", + "reference": "e53260aabf78fb3d63f8d79d69ece59f80d5eda0" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/symfony/service-contracts/zipball/e53260aabf78fb3d63f8d79d69ece59f80d5eda0", + "reference": "e53260aabf78fb3d63f8d79d69ece59f80d5eda0", + "shasum": "" + }, + "require": { + "php": ">=8.1", + "psr/container": "^1.1|^2.0", + "symfony/deprecation-contracts": "^2.5|^3" + }, + "conflict": { + "ext-psr": "<1.1|>=2" + }, + "time": "2024-09-25T14:20:29+00:00", + "type": "library", + "extra": { + "thanks": { + "url": "https://github.com/symfony/contracts", + "name": "symfony/contracts" + }, + "branch-alias": { + "dev-main": "3.5-dev" + } + }, + "installation-source": "dist", + "autoload": { + "psr-4": { + "Symfony\\Contracts\\Service\\": "" + }, + "exclude-from-classmap": [ + "/Test/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Nicolas Grekas", + "email": "p@tchwork.com" + }, + { + "name": "Symfony Community", + "homepage": "https://symfony.com/contributors" + } + ], + "description": "Generic abstractions related to writing services", + "homepage": "https://symfony.com", + "keywords": [ + "abstractions", + "contracts", + "decoupling", + "interfaces", + "interoperability", + "standards" + ], + "support": { + "source": "https://github.com/symfony/service-contracts/tree/v3.5.1" + }, + "funding": [ + { + "url": "https://symfony.com/sponsor", + "type": "custom" + }, + { + "url": "https://github.com/fabpot", + "type": "github" + }, + { + "url": "https://tidelift.com/funding/github/packagist/symfony/symfony", + "type": "tidelift" + } + ], + "install-path": "../symfony/service-contracts" + }, + { + "name": "symfony/string", + "version": "v6.4.15", + "version_normalized": "6.4.15.0", + "source": { + "type": "git", + "url": "https://github.com/symfony/string.git", + "reference": "73a5e66ea2e1677c98d4449177c5a9cf9d8b4c6f" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/symfony/string/zipball/73a5e66ea2e1677c98d4449177c5a9cf9d8b4c6f", + "reference": "73a5e66ea2e1677c98d4449177c5a9cf9d8b4c6f", + "shasum": "" + }, + "require": { + "php": ">=8.1", + "symfony/polyfill-ctype": "~1.8", + "symfony/polyfill-intl-grapheme": "~1.0", + "symfony/polyfill-intl-normalizer": "~1.0", + "symfony/polyfill-mbstring": "~1.0" + }, + "conflict": { + "symfony/translation-contracts": "<2.5" + }, + "require-dev": { + "symfony/error-handler": "^5.4|^6.0|^7.0", + "symfony/http-client": "^5.4|^6.0|^7.0", + "symfony/intl": "^6.2|^7.0", + "symfony/translation-contracts": "^2.5|^3.0", + "symfony/var-exporter": "^5.4|^6.0|^7.0" + }, + "time": "2024-11-13T13:31:12+00:00", + "type": "library", + "installation-source": "dist", + "autoload": { + "files": [ + "Resources/functions.php" + ], + "psr-4": { + "Symfony\\Component\\String\\": "" + }, + "exclude-from-classmap": [ + "/Tests/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Nicolas Grekas", + "email": "p@tchwork.com" + }, + { + "name": "Symfony Community", + "homepage": "https://symfony.com/contributors" + } + ], + "description": "Provides an object-oriented API to strings and deals with bytes, UTF-8 code points and grapheme clusters in a unified way", + "homepage": "https://symfony.com", + "keywords": [ + "grapheme", + "i18n", + "string", + "unicode", + "utf-8", + "utf8" + ], + "support": { + "source": "https://github.com/symfony/string/tree/v6.4.15" + }, + "funding": [ + { + "url": "https://symfony.com/sponsor", + "type": "custom" + }, + { + "url": "https://github.com/fabpot", + "type": "github" + }, + { + "url": "https://tidelift.com/funding/github/packagist/symfony/symfony", + "type": "tidelift" + } + ], + "install-path": "../symfony/string" + }, + { + "name": "symfony/translation", + "version": "v6.4.4", + "version_normalized": "6.4.4.0", + "source": { + "type": "git", + "url": "https://github.com/symfony/translation.git", + "reference": "bce6a5a78e94566641b2594d17e48b0da3184a8e" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/symfony/translation/zipball/bce6a5a78e94566641b2594d17e48b0da3184a8e", + "reference": "bce6a5a78e94566641b2594d17e48b0da3184a8e", + "shasum": "" + }, + "require": { + "php": ">=8.1", + "symfony/deprecation-contracts": "^2.5|^3", + "symfony/polyfill-mbstring": "~1.0", + "symfony/translation-contracts": "^2.5|^3.0" + }, + "conflict": { + "symfony/config": "<5.4", + "symfony/console": "<5.4", + "symfony/dependency-injection": "<5.4", + "symfony/http-client-contracts": "<2.5", + "symfony/http-kernel": "<5.4", + "symfony/service-contracts": "<2.5", + "symfony/twig-bundle": "<5.4", + "symfony/yaml": "<5.4" + }, + "provide": { + "symfony/translation-implementation": "2.3|3.0" + }, + "require-dev": { + "nikic/php-parser": "^4.18|^5.0", + "psr/log": "^1|^2|^3", + "symfony/config": "^5.4|^6.0|^7.0", + "symfony/console": "^5.4|^6.0|^7.0", + "symfony/dependency-injection": "^5.4|^6.0|^7.0", + "symfony/finder": "^5.4|^6.0|^7.0", + "symfony/http-client-contracts": "^2.5|^3.0", + "symfony/http-kernel": "^5.4|^6.0|^7.0", + "symfony/intl": "^5.4|^6.0|^7.0", + "symfony/polyfill-intl-icu": "^1.21", + "symfony/routing": "^5.4|^6.0|^7.0", + "symfony/service-contracts": "^2.5|^3", + "symfony/yaml": "^5.4|^6.0|^7.0" + }, + "time": "2024-02-20T13:16:58+00:00", + "type": "library", + "installation-source": "dist", + "autoload": { + "files": [ + "Resources/functions.php" + ], + "psr-4": { + "Symfony\\Component\\Translation\\": "" + }, + "exclude-from-classmap": [ + "/Tests/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Fabien Potencier", + "email": "fabien@symfony.com" + }, + { + "name": "Symfony Community", + "homepage": "https://symfony.com/contributors" + } + ], + "description": "Provides tools to internationalize your application", + "homepage": "https://symfony.com", + "support": { + "source": "https://github.com/symfony/translation/tree/v6.4.4" + }, + "funding": [ + { + "url": "https://symfony.com/sponsor", + "type": "custom" + }, + { + "url": "https://github.com/fabpot", + "type": "github" + }, + { + "url": "https://tidelift.com/funding/github/packagist/symfony/symfony", + "type": "tidelift" + } + ], + "install-path": "../symfony/translation" + }, + { + "name": "symfony/translation-contracts", + "version": "v3.4.2", + "version_normalized": "3.4.2.0", + "source": { + "type": "git", + "url": "https://github.com/symfony/translation-contracts.git", + "reference": "43810bdb2ddb5400e5c5e778e27b210a0ca83b6b" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/symfony/translation-contracts/zipball/43810bdb2ddb5400e5c5e778e27b210a0ca83b6b", + "reference": "43810bdb2ddb5400e5c5e778e27b210a0ca83b6b", + "shasum": "" + }, + "require": { + "php": ">=8.1" + }, + "time": "2024-01-23T14:51:35+00:00", + "type": "library", + "extra": { + "branch-alias": { + "dev-main": "3.4-dev" + }, + "thanks": { + "name": "symfony/contracts", + "url": "https://github.com/symfony/contracts" + } + }, + "installation-source": "dist", + "autoload": { + "psr-4": { + "Symfony\\Contracts\\Translation\\": "" + }, + "exclude-from-classmap": [ + "/Test/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Nicolas Grekas", + "email": "p@tchwork.com" + }, + { + "name": "Symfony Community", + "homepage": "https://symfony.com/contributors" + } + ], + "description": "Generic abstractions related to translation", + "homepage": "https://symfony.com", + "keywords": [ + "abstractions", + "contracts", + "decoupling", + "interfaces", + "interoperability", + "standards" + ], + "support": { + "source": "https://github.com/symfony/translation-contracts/tree/v3.4.2" + }, + "funding": [ + { + "url": "https://symfony.com/sponsor", + "type": "custom" + }, + { + "url": "https://github.com/fabpot", + "type": "github" + }, + { + "url": "https://tidelift.com/funding/github/packagist/symfony/symfony", + "type": "tidelift" + } + ], + "install-path": "../symfony/translation-contracts" + }, + { + "name": "symfony/uid", + "version": "v6.4.3", + "version_normalized": "6.4.3.0", + "source": { + "type": "git", + "url": "https://github.com/symfony/uid.git", + "reference": "1d31267211cc3a2fff32bcfc7c1818dac41b6fc0" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/symfony/uid/zipball/1d31267211cc3a2fff32bcfc7c1818dac41b6fc0", + "reference": "1d31267211cc3a2fff32bcfc7c1818dac41b6fc0", + "shasum": "" + }, + "require": { + "php": ">=8.1", + "symfony/polyfill-uuid": "^1.15" + }, + "require-dev": { + "symfony/console": "^5.4|^6.0|^7.0" + }, + "time": "2024-01-23T14:51:35+00:00", + "type": "library", + "installation-source": "dist", + "autoload": { + "psr-4": { + "Symfony\\Component\\Uid\\": "" + }, + "exclude-from-classmap": [ + "/Tests/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Grégoire Pineau", + "email": "lyrixx@lyrixx.info" + }, + { + "name": "Nicolas Grekas", + "email": "p@tchwork.com" + }, + { + "name": "Symfony Community", + "homepage": "https://symfony.com/contributors" + } + ], + "description": "Provides an object-oriented API to generate and represent UIDs", + "homepage": "https://symfony.com", + "keywords": [ + "UID", + "ulid", + "uuid" + ], + "support": { + "source": "https://github.com/symfony/uid/tree/v6.4.3" + }, + "funding": [ + { + "url": "https://symfony.com/sponsor", + "type": "custom" + }, + { + "url": "https://github.com/fabpot", + "type": "github" + }, + { + "url": "https://tidelift.com/funding/github/packagist/symfony/symfony", + "type": "tidelift" + } + ], + "install-path": "../symfony/uid" + }, + { + "name": "wapmorgan/mp3info", + "version": "0.1.1", + "version_normalized": "0.1.1.0", + "source": { + "type": "git", + "url": "https://github.com/wapmorgan/Mp3Info.git", + "reference": "e532c2e1997874f9c672c7810ce90ef15004ef94" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/wapmorgan/Mp3Info/zipball/e532c2e1997874f9c672c7810ce90ef15004ef94", + "reference": "e532c2e1997874f9c672c7810ce90ef15004ef94", + "shasum": "" + }, + "require": { + "ext-mbstring": "*", + "php": ">=5.4.0" + }, + "time": "2025-04-01T20:51:11+00:00", + "bin": [ + "bin/mp3scan" + ], + "type": "library", + "extra": { + "patches_applied": { + "Break frame parsing on invalid frame": ".patches/mp3info-break-frame-parsing.patch", + "fix incorrect lookup for mpeg header": ".patches/mp3info-fix-incorrect-lookup-for-mpeg-header.patch" + } + }, + "installation-source": "dist", + "autoload": { + "psr-4": { + "wapmorgan\\Mp3Info\\": "src/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "LGPL-3.0" + ], + "description": "The fastest php library to extract mp3 tags & meta information.", + "keywords": [ + "audio", + "id3", + "id3v1", + "id3v2", + "mp3", + "mpeg" + ], + "support": { + "issues": "https://github.com/wapmorgan/Mp3Info/issues", + "source": "https://github.com/wapmorgan/Mp3Info/tree/0.1.1" + }, + "install-path": "../wapmorgan/mp3info" + }, + { + "name": "web-auth/cose-lib", + "version": "4.3.0", + "version_normalized": "4.3.0.0", + "source": { + "type": "git", + "url": "https://github.com/web-auth/cose-lib.git", + "reference": "e5c417b3b90e06c84638a18d350e438d760cb955" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/web-auth/cose-lib/zipball/e5c417b3b90e06c84638a18d350e438d760cb955", + "reference": "e5c417b3b90e06c84638a18d350e438d760cb955", + "shasum": "" + }, + "require": { + "brick/math": "^0.9|^0.10|^0.11|^0.12", + "ext-json": "*", + "ext-mbstring": "*", + "ext-openssl": "*", + "php": ">=8.1", + "spomky-labs/pki-framework": "^1.0" + }, + "require-dev": { + "ekino/phpstan-banned-code": "^1.0", + "infection/infection": "^0.27", + "php-parallel-lint/php-parallel-lint": "^1.3", + "phpstan/extension-installer": "^1.3", + "phpstan/phpstan": "^1.7", + "phpstan/phpstan-deprecation-rules": "^1.0", + "phpstan/phpstan-phpunit": "^1.1", + "phpstan/phpstan-strict-rules": "^1.2", + "phpunit/phpunit": "^10.1", + "qossmic/deptrac-shim": "^1.0", + "rector/rector": "^0.19", + "symfony/phpunit-bridge": "^6.4|^7.0", + "symplify/easy-coding-standard": "^12.0" + }, + "suggest": { + "ext-bcmath": "For better performance, please install either GMP (recommended) or BCMath extension", + "ext-gmp": "For better performance, please install either GMP (recommended) or BCMath extension" + }, + "time": "2024-02-05T21:00:39+00:00", + "type": "library", + "installation-source": "dist", + "autoload": { + "psr-4": { + "Cose\\": "src/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Florent Morselli", + "homepage": "https://github.com/Spomky" + }, + { + "name": "All contributors", + "homepage": "https://github.com/web-auth/cose/contributors" + } + ], + "description": "CBOR Object Signing and Encryption (COSE) For PHP", + "homepage": "https://github.com/web-auth", + "keywords": [ + "COSE", + "RFC8152" + ], + "support": { + "issues": "https://github.com/web-auth/cose-lib/issues", + "source": "https://github.com/web-auth/cose-lib/tree/4.3.0" + }, + "funding": [ + { + "url": "https://github.com/Spomky", + "type": "github" + }, + { + "url": "https://www.patreon.com/FlorentMorselli", + "type": "patreon" + } + ], + "install-path": "../web-auth/cose-lib" + }, + { + "name": "web-auth/webauthn-lib", + "version": "4.9.1", + "version_normalized": "4.9.1.0", + "source": { + "type": "git", + "url": "https://github.com/web-auth/webauthn-lib.git", + "reference": "fd7a0943c663b325e92ad562c2bcc943e77beeac" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/web-auth/webauthn-lib/zipball/fd7a0943c663b325e92ad562c2bcc943e77beeac", + "reference": "fd7a0943c663b325e92ad562c2bcc943e77beeac", + "shasum": "" + }, + "require": { + "ext-json": "*", + "ext-mbstring": "*", + "ext-openssl": "*", + "lcobucci/clock": "^2.2|^3.0", + "paragonie/constant_time_encoding": "^2.6|^3.0", + "php": ">=8.1", + "psr/clock": "^1.0", + "psr/event-dispatcher": "^1.0", + "psr/http-client": "^1.0", + "psr/http-factory": "^1.0", + "psr/log": "^1.0|^2.0|^3.0", + "spomky-labs/cbor-php": "^3.0", + "spomky-labs/pki-framework": "^1.0", + "symfony/deprecation-contracts": "^3.2", + "symfony/uid": "^6.1|^7.0", + "web-auth/cose-lib": "^4.2.3" + }, + "suggest": { + "phpdocumentor/reflection-docblock": "As of 4.5.x, the phpdocumentor/reflection-docblock component will become mandatory for converting objects such as the Metadata Statement", + "psr/clock-implementation": "As of 4.5.x, the PSR Clock implementation will replace lcobucci/clock", + "psr/log-implementation": "Recommended to receive logs from the library", + "symfony/event-dispatcher": "Recommended to use dispatched events", + "symfony/property-access": "As of 4.5.x, the symfony/serializer component will become mandatory for converting objects such as the Metadata Statement", + "symfony/property-info": "As of 4.5.x, the symfony/serializer component will become mandatory for converting objects such as the Metadata Statement", + "symfony/serializer": "As of 4.5.x, the symfony/serializer component will become mandatory for converting objects such as the Metadata Statement", + "web-token/jwt-library": "Mandatory for fetching Metadata Statement from distant sources" + }, + "time": "2024-07-16T18:36:36+00:00", + "type": "library", + "extra": { + "thanks": { + "name": "web-auth/webauthn-framework", + "url": "https://github.com/web-auth/webauthn-framework" + } + }, + "installation-source": "dist", + "autoload": { + "psr-4": { + "Webauthn\\": "src/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Florent Morselli", + "homepage": "https://github.com/Spomky" + }, + { + "name": "All contributors", + "homepage": "https://github.com/web-auth/webauthn-library/contributors" + } + ], + "description": "FIDO2/Webauthn Support For PHP", + "homepage": "https://github.com/web-auth", + "keywords": [ + "FIDO2", + "fido", + "webauthn" + ], + "support": { + "source": "https://github.com/web-auth/webauthn-lib/tree/4.9.1" + }, + "funding": [ + { + "url": "https://github.com/Spomky", + "type": "github" + }, + { + "url": "https://www.patreon.com/FlorentMorselli", + "type": "patreon" + } + ], + "install-path": "../web-auth/webauthn-lib" + } + ], + "dev": false, + "dev-package-names": [] +} diff --git a/3rdparty/composer/installed.php b/3rdparty/composer/installed.php new file mode 100644 index 00000000..ace864f6 --- /dev/null +++ b/3rdparty/composer/installed.php @@ -0,0 +1,926 @@ + array( + 'name' => 'f7cloud/3rdparty', + 'pretty_version' => 'dev-stable32', + 'version' => 'dev-stable32', + 'reference' => null, + 'type' => 'library', + 'install_path' => __DIR__ . '/../', + 'aliases' => array(), + 'dev' => false, + ), + 'versions' => array( + 'aws/aws-crt-php' => array( + 'pretty_version' => 'v1.2.7', + 'version' => '1.2.7.0', + 'reference' => 'd71d9906c7bb63a28295447ba12e74723bd3730e', + 'type' => 'library', + 'install_path' => __DIR__ . '/../aws/aws-crt-php', + 'aliases' => array(), + 'dev_requirement' => false, + ), + 'aws/aws-sdk-php' => array( + 'pretty_version' => '3.349.3', + 'version' => '3.349.3.0', + 'reference' => 'b2d4718786398f47626add9c29840fc416175ef2', + 'type' => 'library', + 'install_path' => __DIR__ . '/../aws/aws-sdk-php', + 'aliases' => array(), + 'dev_requirement' => false, + ), + 'bantu/ini-get-wrapper' => array( + 'pretty_version' => 'v1.0.1', + 'version' => '1.0.1.0', + 'reference' => '4770c7feab370c62e23db4f31c112b7c6d90aee2', + 'type' => 'library', + 'install_path' => __DIR__ . '/../bantu/ini-get-wrapper', + 'aliases' => array(), + 'dev_requirement' => false, + ), + 'brick/math' => array( + 'pretty_version' => '0.12.1', + 'version' => '0.12.1.0', + 'reference' => 'f510c0a40911935b77b86859eb5223d58d660df1', + 'type' => 'library', + 'install_path' => __DIR__ . '/../brick/math', + 'aliases' => array(), + 'dev_requirement' => false, + ), + 'cweagans/composer-patches' => array( + 'pretty_version' => '1.7.3', + 'version' => '1.7.3.0', + 'reference' => 'e190d4466fe2b103a55467dfa83fc2fecfcaf2db', + 'type' => 'composer-plugin', + 'install_path' => __DIR__ . '/../cweagans/composer-patches', + 'aliases' => array(), + 'dev_requirement' => false, + ), + 'deepdiver/zipstreamer' => array( + 'pretty_version' => 'v2.0.3', + 'version' => '2.0.3.0', + 'reference' => 'b9d1f53453a5736285facb723252ea2169dc472e', + 'type' => 'library', + 'install_path' => __DIR__ . '/../deepdiver/zipstreamer', + 'aliases' => array(), + 'dev_requirement' => false, + ), + 'deepdiver1975/tarstreamer' => array( + 'pretty_version' => 'v2.1.0', + 'version' => '2.1.0.0', + 'reference' => '163052d7a076fd3dd54d4f50e1ff2705b72604db', + 'type' => 'library', + 'install_path' => __DIR__ . '/../deepdiver1975/tarstreamer', + 'aliases' => array(), + 'dev_requirement' => false, + ), + 'doctrine/dbal' => array( + 'pretty_version' => '3.10.2', + 'version' => '3.10.2.0', + 'reference' => 'c6c16cf787eaba3112203dfcd715fa2059c62282', + 'type' => 'library', + 'install_path' => __DIR__ . '/../doctrine/dbal', + 'aliases' => array(), + 'dev_requirement' => false, + ), + 'doctrine/deprecations' => array( + 'pretty_version' => '1.1.5', + 'version' => '1.1.5.0', + 'reference' => '459c2f5dd3d6a4633d3b5f46ee2b1c40f57d3f38', + 'type' => 'library', + 'install_path' => __DIR__ . '/../doctrine/deprecations', + 'aliases' => array(), + 'dev_requirement' => false, + ), + 'doctrine/event-manager' => array( + 'pretty_version' => '2.0.1', + 'version' => '2.0.1.0', + 'reference' => 'b680156fa328f1dfd874fd48c7026c41570b9c6e', + 'type' => 'library', + 'install_path' => __DIR__ . '/../doctrine/event-manager', + 'aliases' => array(), + 'dev_requirement' => false, + ), + 'doctrine/lexer' => array( + 'pretty_version' => '3.0.1', + 'version' => '3.0.1.0', + 'reference' => '31ad66abc0fc9e1a1f2d9bc6a42668d2fbbcd6dd', + 'type' => 'library', + 'install_path' => __DIR__ . '/../doctrine/lexer', + 'aliases' => array(), + 'dev_requirement' => false, + ), + 'egulias/email-validator' => array( + 'pretty_version' => '4.0.4', + 'version' => '4.0.4.0', + 'reference' => 'd42c8731f0624ad6bdc8d3e5e9a4524f68801cfa', + 'type' => 'library', + 'install_path' => __DIR__ . '/../egulias/email-validator', + 'aliases' => array(), + 'dev_requirement' => false, + ), + 'fusonic/opengraph' => array( + 'pretty_version' => 'v3.0.0', + 'version' => '3.0.0.0', + 'reference' => '2daa6dce84f23b1bb6d66bf03b3e9371c39cd378', + 'type' => 'library', + 'install_path' => __DIR__ . '/../fusonic/opengraph', + 'aliases' => array(), + 'dev_requirement' => false, + ), + 'giggsey/libphonenumber-for-php-lite' => array( + 'pretty_version' => '9.0.17', + 'version' => '9.0.17.0', + 'reference' => '430a602c6e5a03932b732226daf64aeeab5c6c65', + 'type' => 'library', + 'install_path' => __DIR__ . '/../giggsey/libphonenumber-for-php-lite', + 'aliases' => array(), + 'dev_requirement' => false, + ), + 'guzzlehttp/guzzle' => array( + 'pretty_version' => '7.9.3', + 'version' => '7.9.3.0', + 'reference' => '7b2f29fe81dc4da0ca0ea7d42107a0845946ea77', + 'type' => 'library', + 'install_path' => __DIR__ . '/../guzzlehttp/guzzle', + 'aliases' => array(), + 'dev_requirement' => false, + ), + 'guzzlehttp/promises' => array( + 'pretty_version' => '2.2.0', + 'version' => '2.2.0.0', + 'reference' => '7c69f28996b0a6920945dd20b3857e499d9ca96c', + 'type' => 'library', + 'install_path' => __DIR__ . '/../guzzlehttp/promises', + 'aliases' => array(), + 'dev_requirement' => false, + ), + 'guzzlehttp/psr7' => array( + 'pretty_version' => '2.7.1', + 'version' => '2.7.1.0', + 'reference' => 'c2270caaabe631b3b44c85f99e5a04bbb8060d16', + 'type' => 'library', + 'install_path' => __DIR__ . '/../guzzlehttp/psr7', + 'aliases' => array(), + 'dev_requirement' => false, + ), + 'guzzlehttp/uri-template' => array( + 'pretty_version' => 'v1.0.4', + 'version' => '1.0.4.0', + 'reference' => '30e286560c137526eccd4ce21b2de477ab0676d2', + 'type' => 'library', + 'install_path' => __DIR__ . '/../guzzlehttp/uri-template', + 'aliases' => array(), + 'dev_requirement' => false, + ), + 'icewind/searchdav' => array( + 'pretty_version' => 'v3.2.0', + 'version' => '3.2.0.0', + 'reference' => '3865288b6962de33086d4e02ec3584610c32b773', + 'type' => 'library', + 'install_path' => __DIR__ . '/../icewind/searchdav', + 'aliases' => array(), + 'dev_requirement' => false, + ), + 'icewind/smb' => array( + 'pretty_version' => 'v3.7.0', + 'version' => '3.7.0.0', + 'reference' => 'e6904cbe75f678335092f4861c60c656b1a99e84', + 'type' => 'library', + 'install_path' => __DIR__ . '/../icewind/smb', + 'aliases' => array(), + 'dev_requirement' => false, + ), + 'icewind/streams' => array( + 'pretty_version' => 'v0.7.8', + 'version' => '0.7.8.0', + 'reference' => 'cb2bd3ed41b516efb97e06e8da35a12ef58ba48b', + 'type' => 'library', + 'install_path' => __DIR__ . '/../icewind/streams', + 'aliases' => array(), + 'dev_requirement' => false, + ), + 'justinrainbow/json-schema' => array( + 'pretty_version' => '6.4.2', + 'version' => '6.4.2.0', + 'reference' => 'ce1fd2d47799bb60668643bc6220f6278a4c1d02', + 'type' => 'library', + 'install_path' => __DIR__ . '/../justinrainbow/json-schema', + 'aliases' => array(), + 'dev_requirement' => false, + ), + 'kornrunner/blurhash' => array( + 'pretty_version' => 'v1.2.2', + 'version' => '1.2.2.0', + 'reference' => 'bc8a4596cb0a49874f0158696a382ab3933fefe4', + 'type' => 'library', + 'install_path' => __DIR__ . '/../kornrunner/blurhash', + 'aliases' => array(), + 'dev_requirement' => false, + ), + 'laravel/serializable-closure' => array( + 'pretty_version' => 'v2.0.4', + 'version' => '2.0.4.0', + 'reference' => 'b352cf0534aa1ae6b4d825d1e762e35d43f8a841', + 'type' => 'library', + 'install_path' => __DIR__ . '/../laravel/serializable-closure', + 'aliases' => array(), + 'dev_requirement' => false, + ), + 'lcobucci/clock' => array( + 'pretty_version' => '3.0.0', + 'version' => '3.0.0.0', + 'reference' => '039ef98c6b57b101d10bd11d8fdfda12cbd996dc', + 'type' => 'library', + 'install_path' => __DIR__ . '/../lcobucci/clock', + 'aliases' => array(), + 'dev_requirement' => false, + ), + 'marc-mabe/php-enum' => array( + 'pretty_version' => 'v4.7.1', + 'version' => '4.7.1.0', + 'reference' => '7159809e5cfa041dca28e61f7f7ae58063aae8ed', + 'type' => 'library', + 'install_path' => __DIR__ . '/../marc-mabe/php-enum', + 'aliases' => array(), + 'dev_requirement' => false, + ), + 'masterminds/html5' => array( + 'pretty_version' => '2.9.0', + 'version' => '2.9.0.0', + 'reference' => 'f5ac2c0b0a2eefca70b2ce32a5809992227e75a6', + 'type' => 'library', + 'install_path' => __DIR__ . '/../masterminds/html5', + 'aliases' => array(), + 'dev_requirement' => false, + ), + 'mexitek/phpcolors' => array( + 'pretty_version' => 'v1.0.4', + 'version' => '1.0.4.0', + 'reference' => '4043974240ca7dc3c2bec3c158588148b605b206', + 'type' => 'library', + 'install_path' => __DIR__ . '/../mexitek/phpcolors', + 'aliases' => array(), + 'dev_requirement' => false, + ), + 'microsoft/azure-storage-blob' => array( + 'pretty_version' => '1.5.4', + 'version' => '1.5.4.0', + 'reference' => '1023ce1dbf062351a32ca5ec72ad1fd4a504f1bf', + 'type' => 'library', + 'install_path' => __DIR__ . '/../microsoft/azure-storage-blob', + 'aliases' => array(), + 'dev_requirement' => false, + ), + 'microsoft/azure-storage-common' => array( + 'pretty_version' => '1.5.2', + 'version' => '1.5.2.0', + 'reference' => '8ca7b1bf4c9ca7c663e75a02a0035b05b37196a0', + 'type' => 'library', + 'install_path' => __DIR__ . '/../microsoft/azure-storage-common', + 'aliases' => array(), + 'dev_requirement' => false, + ), + 'mlocati/ip-lib' => array( + 'pretty_version' => '1.20.0', + 'version' => '1.20.0.0', + 'reference' => 'fd45fc3bf08ed6c7e665e2e70562082ac954afd4', + 'type' => 'library', + 'install_path' => __DIR__ . '/../mlocati/ip-lib', + 'aliases' => array(), + 'dev_requirement' => false, + ), + 'mtdowling/jmespath.php' => array( + 'pretty_version' => '2.8.0', + 'version' => '2.8.0.0', + 'reference' => 'a2a865e05d5f420b50cc2f85bb78d565db12a6bc', + 'type' => 'library', + 'install_path' => __DIR__ . '/../mtdowling/jmespath.php', + 'aliases' => array(), + 'dev_requirement' => false, + ), + 'f7cloud/3rdparty' => array( + 'pretty_version' => 'dev-stable32', + 'version' => 'dev-stable32', + 'reference' => null, + 'type' => 'library', + 'install_path' => __DIR__ . '/../', + 'aliases' => array(), + 'dev_requirement' => false, + ), + 'f7cloud/lognormalizer' => array( + 'pretty_version' => 'v1.0.0', + 'version' => '1.0.0.0', + 'reference' => '87445d69225c247aaff64643b1fc83c6d6df741f', + 'type' => 'library', + 'install_path' => __DIR__ . '/../f7cloud/lognormalizer', + 'aliases' => array(), + 'dev_requirement' => false, + ), + 'paragonie/constant_time_encoding' => array( + 'pretty_version' => 'v2.6.3', + 'version' => '2.6.3.0', + 'reference' => '58c3f47f650c94ec05a151692652a868995d2938', + 'type' => 'library', + 'install_path' => __DIR__ . '/../paragonie/constant_time_encoding', + 'aliases' => array(), + 'dev_requirement' => false, + ), + 'pear/archive_tar' => array( + 'pretty_version' => '1.5.0', + 'version' => '1.5.0.0', + 'reference' => 'b439c859564f5cbb0f64ad6002d0afe84a889602', + 'type' => 'library', + 'install_path' => __DIR__ . '/../pear/archive_tar', + 'aliases' => array(), + 'dev_requirement' => false, + ), + 'pear/console_getopt' => array( + 'pretty_version' => 'v1.4.3', + 'version' => '1.4.3.0', + 'reference' => 'a41f8d3e668987609178c7c4a9fe48fecac53fa0', + 'type' => 'library', + 'install_path' => __DIR__ . '/../pear/console_getopt', + 'aliases' => array(), + 'dev_requirement' => false, + ), + 'pear/pear-core-minimal' => array( + 'pretty_version' => 'v1.10.16', + 'version' => '1.10.16.0', + 'reference' => 'c0f51b45f50683bf5bbf558036854ebc9b54d033', + 'type' => 'library', + 'install_path' => __DIR__ . '/../pear/pear-core-minimal', + 'aliases' => array(), + 'dev_requirement' => false, + ), + 'pear/pear_exception' => array( + 'pretty_version' => 'v1.0.2', + 'version' => '1.0.2.0', + 'reference' => 'b14fbe2ddb0b9f94f5b24cf08783d599f776fff0', + 'type' => 'class', + 'install_path' => __DIR__ . '/../pear/pear_exception', + 'aliases' => array(), + 'dev_requirement' => false, + ), + 'php-http/async-client-implementation' => array( + 'dev_requirement' => false, + 'provided' => array( + 0 => '1.0', + ), + ), + 'php-http/client-implementation' => array( + 'dev_requirement' => false, + 'provided' => array( + 0 => '1.0', + ), + ), + 'php-http/guzzle7-adapter' => array( + 'pretty_version' => '1.1.0', + 'version' => '1.1.0.0', + 'reference' => '03a415fde709c2f25539790fecf4d9a31bc3d0eb', + 'type' => 'library', + 'install_path' => __DIR__ . '/../php-http/guzzle7-adapter', + 'aliases' => array(), + 'dev_requirement' => false, + ), + 'php-http/httplug' => array( + 'pretty_version' => '2.4.1', + 'version' => '2.4.1.0', + 'reference' => '5cad731844891a4c282f3f3e1b582c46839d22f4', + 'type' => 'library', + 'install_path' => __DIR__ . '/../php-http/httplug', + 'aliases' => array(), + 'dev_requirement' => false, + ), + 'php-http/promise' => array( + 'pretty_version' => '1.3.1', + 'version' => '1.3.1.0', + 'reference' => 'fc85b1fba37c169a69a07ef0d5a8075770cc1f83', + 'type' => 'library', + 'install_path' => __DIR__ . '/../php-http/promise', + 'aliases' => array(), + 'dev_requirement' => false, + ), + 'php-opencloud/openstack' => array( + 'pretty_version' => 'v3.14.0', + 'version' => '3.14.0.0', + 'reference' => 'b92ea5581ca91779b88f08b1b44e6ca880b34fc3', + 'type' => 'library', + 'install_path' => __DIR__ . '/../php-opencloud/openstack', + 'aliases' => array(), + 'dev_requirement' => false, + ), + 'phpseclib/phpseclib' => array( + 'pretty_version' => '2.0.47', + 'version' => '2.0.47.0', + 'reference' => 'b7d7d90ee7df7f33a664b4aea32d50a305d35adb', + 'type' => 'library', + 'install_path' => __DIR__ . '/../phpseclib/phpseclib', + 'aliases' => array(), + 'dev_requirement' => false, + ), + 'pimple/pimple' => array( + 'pretty_version' => 'v3.5.0', + 'version' => '3.5.0.0', + 'reference' => 'a94b3a4db7fb774b3d78dad2315ddc07629e1bed', + 'type' => 'library', + 'install_path' => __DIR__ . '/../pimple/pimple', + 'aliases' => array(), + 'dev_requirement' => false, + ), + 'psr/cache' => array( + 'pretty_version' => '3.0.0', + 'version' => '3.0.0.0', + 'reference' => 'aa5030cfa5405eccfdcb1083ce040c2cb8d253bf', + 'type' => 'library', + 'install_path' => __DIR__ . '/../psr/cache', + 'aliases' => array(), + 'dev_requirement' => false, + ), + 'psr/clock' => array( + 'pretty_version' => '1.0.0', + 'version' => '1.0.0.0', + 'reference' => 'e41a24703d4560fd0acb709162f73b8adfc3aa0d', + 'type' => 'library', + 'install_path' => __DIR__ . '/../psr/clock', + 'aliases' => array(), + 'dev_requirement' => false, + ), + 'psr/clock-implementation' => array( + 'dev_requirement' => false, + 'provided' => array( + 0 => '1.0', + ), + ), + 'psr/container' => array( + 'pretty_version' => '2.0.2', + 'version' => '2.0.2.0', + 'reference' => 'c71ecc56dfe541dbd90c5360474fbc405f8d5963', + 'type' => 'library', + 'install_path' => __DIR__ . '/../psr/container', + 'aliases' => array(), + 'dev_requirement' => false, + ), + 'psr/event-dispatcher' => array( + 'pretty_version' => '1.0.0', + 'version' => '1.0.0.0', + 'reference' => 'dbefd12671e8a14ec7f180cab83036ed26714bb0', + 'type' => 'library', + 'install_path' => __DIR__ . '/../psr/event-dispatcher', + 'aliases' => array(), + 'dev_requirement' => false, + ), + 'psr/event-dispatcher-implementation' => array( + 'dev_requirement' => false, + 'provided' => array( + 0 => '1.0', + ), + ), + 'psr/http-client' => array( + 'pretty_version' => '1.0.3', + 'version' => '1.0.3.0', + 'reference' => 'bb5906edc1c324c9a05aa0873d40117941e5fa90', + 'type' => 'library', + 'install_path' => __DIR__ . '/../psr/http-client', + 'aliases' => array(), + 'dev_requirement' => false, + ), + 'psr/http-client-implementation' => array( + 'dev_requirement' => false, + 'provided' => array( + 0 => '1.0', + ), + ), + 'psr/http-factory' => array( + 'pretty_version' => '1.1.0', + 'version' => '1.1.0.0', + 'reference' => '2b4765fddfe3b508ac62f829e852b1501d3f6e8a', + 'type' => 'library', + 'install_path' => __DIR__ . '/../psr/http-factory', + 'aliases' => array(), + 'dev_requirement' => false, + ), + 'psr/http-factory-implementation' => array( + 'dev_requirement' => false, + 'provided' => array( + 0 => '1.0', + ), + ), + 'psr/http-message' => array( + 'pretty_version' => '2.0', + 'version' => '2.0.0.0', + 'reference' => '402d35bcb92c70c026d1a6a9883f06b2ead23d71', + 'type' => 'library', + 'install_path' => __DIR__ . '/../psr/http-message', + 'aliases' => array(), + 'dev_requirement' => false, + ), + 'psr/http-message-implementation' => array( + 'dev_requirement' => false, + 'provided' => array( + 0 => '1.0', + ), + ), + 'psr/log' => array( + 'pretty_version' => '3.0.2', + 'version' => '3.0.2.0', + 'reference' => 'f16e1d5863e37f8d8c2a01719f5b34baa2b714d3', + 'type' => 'library', + 'install_path' => __DIR__ . '/../psr/log', + 'aliases' => array(), + 'dev_requirement' => false, + ), + 'psr/log-implementation' => array( + 'dev_requirement' => false, + 'provided' => array( + 0 => '1.0|2.0|3.0', + ), + ), + 'punic/calendar' => array( + 'dev_requirement' => false, + 'replaced' => array( + 0 => '*', + ), + ), + 'punic/common' => array( + 'dev_requirement' => false, + 'replaced' => array( + 0 => '*', + ), + ), + 'punic/punic' => array( + 'pretty_version' => '3.8.1', + 'version' => '3.8.1.0', + 'reference' => '142707201a246a9c2ea909605cd56177af87f961', + 'type' => 'library', + 'install_path' => __DIR__ . '/../punic/punic', + 'aliases' => array(), + 'dev_requirement' => false, + ), + 'ralouphie/getallheaders' => array( + 'pretty_version' => '3.0.3', + 'version' => '3.0.3.0', + 'reference' => '120b605dfeb996808c31b6477290a714d356e822', + 'type' => 'library', + 'install_path' => __DIR__ . '/../ralouphie/getallheaders', + 'aliases' => array(), + 'dev_requirement' => false, + ), + 'rsky/pear-core-min' => array( + 'dev_requirement' => false, + 'replaced' => array( + 0 => 'v1.10.16', + ), + ), + 'sabre/dav' => array( + 'pretty_version' => '4.7.0', + 'version' => '4.7.0.0', + 'reference' => '074373bcd689a30bcf5aaa6bbb20a3395964ce7a', + 'type' => 'library', + 'install_path' => __DIR__ . '/../sabre/dav', + 'aliases' => array(), + 'dev_requirement' => false, + ), + 'sabre/event' => array( + 'pretty_version' => '5.1.7', + 'version' => '5.1.7.0', + 'reference' => '86d57e305c272898ba3c28e9bd3d65d5464587c2', + 'type' => 'library', + 'install_path' => __DIR__ . '/../sabre/event', + 'aliases' => array(), + 'dev_requirement' => false, + ), + 'sabre/http' => array( + 'pretty_version' => '5.1.12', + 'version' => '5.1.12.0', + 'reference' => 'dedff73f3995578bc942fa4c8484190cac14f139', + 'type' => 'library', + 'install_path' => __DIR__ . '/../sabre/http', + 'aliases' => array(), + 'dev_requirement' => false, + ), + 'sabre/uri' => array( + 'pretty_version' => '2.3.4', + 'version' => '2.3.4.0', + 'reference' => 'b76524c22de90d80ca73143680a8e77b1266c291', + 'type' => 'library', + 'install_path' => __DIR__ . '/../sabre/uri', + 'aliases' => array(), + 'dev_requirement' => false, + ), + 'sabre/vobject' => array( + 'pretty_version' => '4.5.6', + 'version' => '4.5.6.0', + 'reference' => '900266bb3bd448a9f7f41f82344ad0aba237cb27', + 'type' => 'library', + 'install_path' => __DIR__ . '/../sabre/vobject', + 'aliases' => array(), + 'dev_requirement' => false, + ), + 'sabre/xml' => array( + 'pretty_version' => '2.2.11', + 'version' => '2.2.11.0', + 'reference' => '01a7927842abf3e10df3d9c2d9b0cc9d813a3fcc', + 'type' => 'library', + 'install_path' => __DIR__ . '/../sabre/xml', + 'aliases' => array(), + 'dev_requirement' => false, + ), + 'spomky-labs/cbor-php' => array( + 'pretty_version' => '3.0.4', + 'version' => '3.0.4.0', + 'reference' => '658ed12a85a6b31fa312b89cd92f3a4ce6df4c6b', + 'type' => 'library', + 'install_path' => __DIR__ . '/../spomky-labs/cbor-php', + 'aliases' => array(), + 'dev_requirement' => false, + ), + 'spomky-labs/pki-framework' => array( + 'pretty_version' => '1.2.1', + 'version' => '1.2.1.0', + 'reference' => '0b10c8b53366729417d6226ae89a665f9e2d61b6', + 'type' => 'library', + 'install_path' => __DIR__ . '/../spomky-labs/pki-framework', + 'aliases' => array(), + 'dev_requirement' => false, + ), + 'stecman/symfony-console-completion' => array( + 'pretty_version' => 'v0.14.0', + 'version' => '0.14.0.0', + 'reference' => '93b88586e50c05b7375a547031f535bf1ea970f5', + 'type' => 'library', + 'install_path' => __DIR__ . '/../stecman/symfony-console-completion', + 'aliases' => array(), + 'dev_requirement' => false, + ), + 'symfony/console' => array( + 'pretty_version' => 'v6.4.17', + 'version' => '6.4.17.0', + 'reference' => '799445db3f15768ecc382ac5699e6da0520a0a04', + 'type' => 'library', + 'install_path' => __DIR__ . '/../symfony/console', + 'aliases' => array(), + 'dev_requirement' => false, + ), + 'symfony/css-selector' => array( + 'pretty_version' => 'v6.4.13', + 'version' => '6.4.13.0', + 'reference' => 'cb23e97813c5837a041b73a6d63a9ddff0778f5e', + 'type' => 'library', + 'install_path' => __DIR__ . '/../symfony/css-selector', + 'aliases' => array(), + 'dev_requirement' => false, + ), + 'symfony/deprecation-contracts' => array( + 'pretty_version' => 'v3.6.0', + 'version' => '3.6.0.0', + 'reference' => '63afe740e99a13ba87ec199bb07bbdee937a5b62', + 'type' => 'library', + 'install_path' => __DIR__ . '/../symfony/deprecation-contracts', + 'aliases' => array(), + 'dev_requirement' => false, + ), + 'symfony/dom-crawler' => array( + 'pretty_version' => 'v6.4.23', + 'version' => '6.4.23.0', + 'reference' => '22210aacb35dbadd772325d759d17bce2374a84d', + 'type' => 'library', + 'install_path' => __DIR__ . '/../symfony/dom-crawler', + 'aliases' => array(), + 'dev_requirement' => false, + ), + 'symfony/event-dispatcher' => array( + 'pretty_version' => 'v6.4.8', + 'version' => '6.4.8.0', + 'reference' => '8d7507f02b06e06815e56bb39aa0128e3806208b', + 'type' => 'library', + 'install_path' => __DIR__ . '/../symfony/event-dispatcher', + 'aliases' => array(), + 'dev_requirement' => false, + ), + 'symfony/event-dispatcher-contracts' => array( + 'pretty_version' => 'v3.5.0', + 'version' => '3.5.0.0', + 'reference' => '8f93aec25d41b72493c6ddff14e916177c9efc50', + 'type' => 'library', + 'install_path' => __DIR__ . '/../symfony/event-dispatcher-contracts', + 'aliases' => array(), + 'dev_requirement' => false, + ), + 'symfony/event-dispatcher-implementation' => array( + 'dev_requirement' => false, + 'provided' => array( + 0 => '2.0|3.0', + ), + ), + 'symfony/http-foundation' => array( + 'pretty_version' => 'v6.4.29', + 'version' => '6.4.29.0', + 'reference' => 'b03d11e015552a315714c127d8d1e0f9e970ec88', + 'type' => 'library', + 'install_path' => __DIR__ . '/../symfony/http-foundation', + 'aliases' => array(), + 'dev_requirement' => false, + ), + 'symfony/mailer' => array( + 'pretty_version' => 'v6.4.12', + 'version' => '6.4.12.0', + 'reference' => 'b6a25408c569ae2366b3f663a4edad19420a9c26', + 'type' => 'library', + 'install_path' => __DIR__ . '/../symfony/mailer', + 'aliases' => array(), + 'dev_requirement' => false, + ), + 'symfony/mime' => array( + 'pretty_version' => 'v6.4.12', + 'version' => '6.4.12.0', + 'reference' => 'abe16ee7790b16aa525877419deb0f113953f0e1', + 'type' => 'library', + 'install_path' => __DIR__ . '/../symfony/mime', + 'aliases' => array(), + 'dev_requirement' => false, + ), + 'symfony/polyfill-ctype' => array( + 'dev_requirement' => false, + 'replaced' => array( + 0 => '*', + ), + ), + 'symfony/polyfill-intl-grapheme' => array( + 'pretty_version' => 'v1.32.0', + 'version' => '1.32.0.0', + 'reference' => 'b9123926e3b7bc2f98c02ad54f6a4b02b91a8abe', + 'type' => 'library', + 'install_path' => __DIR__ . '/../symfony/polyfill-intl-grapheme', + 'aliases' => array(), + 'dev_requirement' => false, + ), + 'symfony/polyfill-intl-idn' => array( + 'pretty_version' => 'v1.32.0', + 'version' => '1.32.0.0', + 'reference' => '9614ac4d8061dc257ecc64cba1b140873dce8ad3', + 'type' => 'library', + 'install_path' => __DIR__ . '/../symfony/polyfill-intl-idn', + 'aliases' => array(), + 'dev_requirement' => false, + ), + 'symfony/polyfill-intl-normalizer' => array( + 'pretty_version' => 'v1.32.0', + 'version' => '1.32.0.0', + 'reference' => '3833d7255cc303546435cb650316bff708a1c75c', + 'type' => 'library', + 'install_path' => __DIR__ . '/../symfony/polyfill-intl-normalizer', + 'aliases' => array(), + 'dev_requirement' => false, + ), + 'symfony/polyfill-mbstring' => array( + 'dev_requirement' => false, + 'replaced' => array( + 0 => '*', + ), + ), + 'symfony/polyfill-php80' => array( + 'dev_requirement' => false, + 'replaced' => array( + 0 => '*', + ), + ), + 'symfony/polyfill-php81' => array( + 'dev_requirement' => false, + 'replaced' => array( + 0 => '*', + ), + ), + 'symfony/polyfill-php82' => array( + 'pretty_version' => 'v1.32.0', + 'version' => '1.32.0.0', + 'reference' => '5d2ed36f7734637dacc025f179698031951b1692', + 'type' => 'library', + 'install_path' => __DIR__ . '/../symfony/polyfill-php82', + 'aliases' => array(), + 'dev_requirement' => false, + ), + 'symfony/polyfill-php83' => array( + 'pretty_version' => 'v1.32.0', + 'version' => '1.32.0.0', + 'reference' => '2fb86d65e2d424369ad2905e83b236a8805ba491', + 'type' => 'library', + 'install_path' => __DIR__ . '/../symfony/polyfill-php83', + 'aliases' => array(), + 'dev_requirement' => false, + ), + 'symfony/polyfill-php84' => array( + 'pretty_version' => 'v1.32.0', + 'version' => '1.32.0.0', + 'reference' => '000df7860439609837bbe28670b0be15783b7fbf', + 'type' => 'library', + 'install_path' => __DIR__ . '/../symfony/polyfill-php84', + 'aliases' => array(), + 'dev_requirement' => false, + ), + 'symfony/polyfill-uuid' => array( + 'pretty_version' => 'v1.29.0', + 'version' => '1.29.0.0', + 'reference' => '3abdd21b0ceaa3000ee950097bc3cf9efc137853', + 'type' => 'library', + 'install_path' => __DIR__ . '/../symfony/polyfill-uuid', + 'aliases' => array(), + 'dev_requirement' => false, + ), + 'symfony/process' => array( + 'pretty_version' => 'v6.4.15', + 'version' => '6.4.15.0', + 'reference' => '3cb242f059c14ae08591c5c4087d1fe443564392', + 'type' => 'library', + 'install_path' => __DIR__ . '/../symfony/process', + 'aliases' => array(), + 'dev_requirement' => false, + ), + 'symfony/routing' => array( + 'pretty_version' => 'v6.4.12', + 'version' => '6.4.12.0', + 'reference' => 'a7c8036bd159486228dc9be3e846a00a0dda9f9f', + 'type' => 'library', + 'install_path' => __DIR__ . '/../symfony/routing', + 'aliases' => array(), + 'dev_requirement' => false, + ), + 'symfony/service-contracts' => array( + 'pretty_version' => 'v3.5.1', + 'version' => '3.5.1.0', + 'reference' => 'e53260aabf78fb3d63f8d79d69ece59f80d5eda0', + 'type' => 'library', + 'install_path' => __DIR__ . '/../symfony/service-contracts', + 'aliases' => array(), + 'dev_requirement' => false, + ), + 'symfony/string' => array( + 'pretty_version' => 'v6.4.15', + 'version' => '6.4.15.0', + 'reference' => '73a5e66ea2e1677c98d4449177c5a9cf9d8b4c6f', + 'type' => 'library', + 'install_path' => __DIR__ . '/../symfony/string', + 'aliases' => array(), + 'dev_requirement' => false, + ), + 'symfony/translation' => array( + 'pretty_version' => 'v6.4.4', + 'version' => '6.4.4.0', + 'reference' => 'bce6a5a78e94566641b2594d17e48b0da3184a8e', + 'type' => 'library', + 'install_path' => __DIR__ . '/../symfony/translation', + 'aliases' => array(), + 'dev_requirement' => false, + ), + 'symfony/translation-contracts' => array( + 'pretty_version' => 'v3.4.2', + 'version' => '3.4.2.0', + 'reference' => '43810bdb2ddb5400e5c5e778e27b210a0ca83b6b', + 'type' => 'library', + 'install_path' => __DIR__ . '/../symfony/translation-contracts', + 'aliases' => array(), + 'dev_requirement' => false, + ), + 'symfony/translation-implementation' => array( + 'dev_requirement' => false, + 'provided' => array( + 0 => '2.3|3.0', + ), + ), + 'symfony/uid' => array( + 'pretty_version' => 'v6.4.3', + 'version' => '6.4.3.0', + 'reference' => '1d31267211cc3a2fff32bcfc7c1818dac41b6fc0', + 'type' => 'library', + 'install_path' => __DIR__ . '/../symfony/uid', + 'aliases' => array(), + 'dev_requirement' => false, + ), + 'wapmorgan/mp3info' => array( + 'pretty_version' => '0.1.1', + 'version' => '0.1.1.0', + 'reference' => 'e532c2e1997874f9c672c7810ce90ef15004ef94', + 'type' => 'library', + 'install_path' => __DIR__ . '/../wapmorgan/mp3info', + 'aliases' => array(), + 'dev_requirement' => false, + ), + 'web-auth/cose-lib' => array( + 'pretty_version' => '4.3.0', + 'version' => '4.3.0.0', + 'reference' => 'e5c417b3b90e06c84638a18d350e438d760cb955', + 'type' => 'library', + 'install_path' => __DIR__ . '/../web-auth/cose-lib', + 'aliases' => array(), + 'dev_requirement' => false, + ), + 'web-auth/webauthn-lib' => array( + 'pretty_version' => '4.9.1', + 'version' => '4.9.1.0', + 'reference' => 'fd7a0943c663b325e92ad562c2bcc943e77beeac', + 'type' => 'library', + 'install_path' => __DIR__ . '/../web-auth/webauthn-lib', + 'aliases' => array(), + 'dev_requirement' => false, + ), + ), +); diff --git a/3rdparty/composer/platform_check.php b/3rdparty/composer/platform_check.php new file mode 100644 index 00000000..2beb1491 --- /dev/null +++ b/3rdparty/composer/platform_check.php @@ -0,0 +1,25 @@ += 80100)) { + $issues[] = 'Your Composer dependencies require a PHP version ">= 8.1.0". You are running ' . PHP_VERSION . '.'; +} + +if ($issues) { + if (!headers_sent()) { + header('HTTP/1.1 500 Internal Server Error'); + } + if (!ini_get('display_errors')) { + if (PHP_SAPI === 'cli' || PHP_SAPI === 'phpdbg') { + fwrite(STDERR, 'Composer detected issues in your platform:' . PHP_EOL.PHP_EOL . implode(PHP_EOL, $issues) . PHP_EOL.PHP_EOL); + } elseif (!headers_sent()) { + echo 'Composer detected issues in your platform:' . PHP_EOL.PHP_EOL . str_replace('You are running '.PHP_VERSION.'.', '', implode(PHP_EOL, $issues)) . PHP_EOL.PHP_EOL; + } + } + throw new \RuntimeException( + 'Composer detected issues in your platform: ' . implode(' ', $issues) + ); +} diff --git a/3rdparty/cweagans/composer-patches/LICENSE.md b/3rdparty/cweagans/composer-patches/LICENSE.md new file mode 100644 index 00000000..d0dad3df --- /dev/null +++ b/3rdparty/cweagans/composer-patches/LICENSE.md @@ -0,0 +1,9 @@ +Copyright 2013 Cameron Eagans + +Redistribution and use in source and binary forms, with or without modification, are permitted provided that the following conditions are met: + +1. Redistributions of source code must retain the above copyright notice, this list of conditions and the following disclaimer. +2. Redistributions in binary form must reproduce the above copyright notice, this list of conditions and the following disclaimer in the documentation and/or other materials provided with the distribution. +3. Neither the name of the copyright holder nor the names of its contributors may be used to endorse or promote products derived from this software without specific prior written permission. + +THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. diff --git a/3rdparty/cweagans/composer-patches/src/PatchEvent.php b/3rdparty/cweagans/composer-patches/src/PatchEvent.php new file mode 100644 index 00000000..31d36f89 --- /dev/null +++ b/3rdparty/cweagans/composer-patches/src/PatchEvent.php @@ -0,0 +1,70 @@ +package = $package; + $this->url = $url; + $this->description = $description; + } + + /** + * Returns the package that is patched. + * + * @return PackageInterface + */ + public function getPackage() { + return $this->package; + } + + /** + * Returns the url of the patch. + * + * @return string + */ + public function getUrl() { + return $this->url; + } + + /** + * Returns the description of the patch. + * + * @return string + */ + public function getDescription() { + return $this->description; + } + +} diff --git a/3rdparty/cweagans/composer-patches/src/PatchEvents.php b/3rdparty/cweagans/composer-patches/src/PatchEvents.php new file mode 100644 index 00000000..ecee9476 --- /dev/null +++ b/3rdparty/cweagans/composer-patches/src/PatchEvents.php @@ -0,0 +1,30 @@ +composer = $composer; + $this->io = $io; + $this->eventDispatcher = $composer->getEventDispatcher(); + $this->executor = new ProcessExecutor($this->io); + $this->patches = array(); + $this->installedPatches = array(); + } + + /** + * Returns an array of event names this subscriber wants to listen to. + */ + public static function getSubscribedEvents() { + return array( + ScriptEvents::PRE_INSTALL_CMD => array('checkPatches'), + ScriptEvents::PRE_UPDATE_CMD => array('checkPatches'), + PackageEvents::PRE_PACKAGE_INSTALL => array('gatherPatches'), + PackageEvents::PRE_PACKAGE_UPDATE => array('gatherPatches'), + // The following is a higher weight for compatibility with + // https://github.com/AydinHassan/magento-core-composer-installer and more generally for compatibility with + // every Composer plugin which deploys downloaded packages to other locations. + // In such cases you want that those plugins deploy patched files so they have to run after + // the "composer-patches" plugin. + // @see: https://github.com/cweagans/composer-patches/pull/153 + PackageEvents::POST_PACKAGE_INSTALL => array('postInstall', 10), + PackageEvents::POST_PACKAGE_UPDATE => array('postInstall', 10), + ); + } + + /** + * Before running composer install, + * @param Event $event + */ + public function checkPatches(Event $event) { + if (!$this->isPatchingEnabled()) { + return; + } + + try { + $repositoryManager = $this->composer->getRepositoryManager(); + $localRepository = $repositoryManager->getLocalRepository(); + $installationManager = $this->composer->getInstallationManager(); + $packages = $localRepository->getPackages(); + + $extra = $this->composer->getPackage()->getExtra(); + $patches_ignore = isset($extra['patches-ignore']) ? $extra['patches-ignore'] : array(); + + $tmp_patches = $this->grabPatches(); + foreach ($packages as $package) { + $extra = $package->getExtra(); + if (isset($extra['patches'])) { + if (isset($patches_ignore[$package->getName()])) { + foreach ($patches_ignore[$package->getName()] as $package_name => $patches) { + if (isset($extra['patches'][$package_name])) { + $extra['patches'][$package_name] = array_diff($extra['patches'][$package_name], $patches); + } + } + } + $this->installedPatches[$package->getName()] = $extra['patches']; + } + $patches = isset($extra['patches']) ? $extra['patches'] : array(); + $tmp_patches = $this->arrayMergeRecursiveDistinct($tmp_patches, $patches); + } + + if ($tmp_patches == FALSE) { + $this->io->write('No patches supplied.'); + return; + } + + // Remove packages for which the patch set has changed. + $promises = array(); + foreach ($packages as $package) { + if (!($package instanceof AliasPackage)) { + $package_name = $package->getName(); + $extra = $package->getExtra(); + $has_patches = isset($tmp_patches[$package_name]); + $has_applied_patches = isset($extra['patches_applied']) && count($extra['patches_applied']) > 0; + if (($has_patches && !$has_applied_patches) + || (!$has_patches && $has_applied_patches) + || ($has_patches && $has_applied_patches && $tmp_patches[$package_name] !== $extra['patches_applied'])) { + $uninstallOperation = new UninstallOperation($package, 'Removing package so it can be re-installed and re-patched.'); + $this->io->write('Removing package ' . $package_name . ' so that it can be re-installed and re-patched.'); + $promises[] = $installationManager->uninstall($localRepository, $uninstallOperation); + } + } + } + $promises = array_filter($promises); + if ($promises) { + $this->composer->getLoop()->wait($promises); + } + } + // If the Locker isn't available, then we don't need to do this. + // It's the first time packages have been installed. + catch (\LogicException $e) { + return; + } + } + + /** + * Gather patches from dependencies and store them for later use. + * + * @param PackageEvent $event + */ + public function gatherPatches(PackageEvent $event) { + // If we've already done this, then don't do it again. + if (isset($this->patches['_patchesGathered'])) { + $this->io->write('Patches already gathered. Skipping', TRUE, IOInterface::VERBOSE); + return; + } + // If patching has been disabled, bail out here. + elseif (!$this->isPatchingEnabled()) { + $this->io->write('Patching is disabled. Skipping.', TRUE, IOInterface::VERBOSE); + return; + } + + $this->patches = $this->grabPatches(); + if (empty($this->patches)) { + $this->io->write('No patches supplied.'); + } + + $extra = $this->composer->getPackage()->getExtra(); + $patches_ignore = isset($extra['patches-ignore']) ? $extra['patches-ignore'] : array(); + + // Now add all the patches from dependencies that will be installed. + $operations = $event->getOperations(); + $this->io->write('Gathering patches for dependencies. This might take a minute.'); + foreach ($operations as $operation) { + if ($operation instanceof InstallOperation || $operation instanceof UpdateOperation) { + $package = $this->getPackageFromOperation($operation); + $extra = $package->getExtra(); + if (isset($extra['patches'])) { + if (isset($patches_ignore[$package->getName()])) { + foreach ($patches_ignore[$package->getName()] as $package_name => $patches) { + if (isset($extra['patches'][$package_name])) { + $extra['patches'][$package_name] = array_diff($extra['patches'][$package_name], $patches); + } + } + } + $this->patches = $this->arrayMergeRecursiveDistinct($this->patches, $extra['patches']); + } + // Unset installed patches for this package + if(isset($this->installedPatches[$package->getName()])) { + unset($this->installedPatches[$package->getName()]); + } + } + } + + // Merge installed patches from dependencies that did not receive an update. + foreach ($this->installedPatches as $patches) { + $this->patches = $this->arrayMergeRecursiveDistinct($this->patches, $patches); + } + + // If we're in verbose mode, list the projects we're going to patch. + if ($this->io->isVerbose()) { + foreach ($this->patches as $package => $patches) { + $number = count($patches); + $this->io->write('Found ' . $number . ' patches for ' . $package . '.'); + } + } + + // Make sure we don't gather patches again. Extra keys in $this->patches + // won't hurt anything, so we'll just stash it there. + $this->patches['_patchesGathered'] = TRUE; + } + + /** + * Get the patches from root composer or external file + * @return Patches + * @throws \Exception + */ + public function grabPatches() { + // First, try to get the patches from the root composer.json. + $extra = $this->composer->getPackage()->getExtra(); + if (isset($extra['patches'])) { + $this->io->write('Gathering patches for root package.'); + $patches = $extra['patches']; + return $patches; + } + // If it's not specified there, look for a patches-file definition. + elseif (isset($extra['patches-file'])) { + $this->io->write('Gathering patches from patch file.'); + $patches = file_get_contents($extra['patches-file']); + $patches = json_decode($patches, TRUE); + $error = json_last_error(); + if ($error != 0) { + switch ($error) { + case JSON_ERROR_DEPTH: + $msg = ' - Maximum stack depth exceeded'; + break; + case JSON_ERROR_STATE_MISMATCH: + $msg = ' - Underflow or the modes mismatch'; + break; + case JSON_ERROR_CTRL_CHAR: + $msg = ' - Unexpected control character found'; + break; + case JSON_ERROR_SYNTAX: + $msg = ' - Syntax error, malformed JSON'; + break; + case JSON_ERROR_UTF8: + $msg = ' - Malformed UTF-8 characters, possibly incorrectly encoded'; + break; + default: + $msg = ' - Unknown error'; + break; + } + throw new \Exception('There was an error in the supplied patches file:' . $msg); + } + if (isset($patches['patches'])) { + $patches = $patches['patches']; + return $patches; + } + elseif(!$patches) { + throw new \Exception('There was an error in the supplied patch file'); + } + } + else { + return array(); + } + } + + /** + * @param PackageEvent $event + * @throws \Exception + */ + public function postInstall(PackageEvent $event) { + + // Check if we should exit in failure. + $extra = $this->composer->getPackage()->getExtra(); + $exitOnFailure = getenv('COMPOSER_EXIT_ON_PATCH_FAILURE') || !empty($extra['composer-exit-on-patch-failure']); + $skipReporting = getenv('COMPOSER_PATCHES_SKIP_REPORTING') || !empty($extra['composer-patches-skip-reporting']); + + // Get the package object for the current operation. + $operation = $event->getOperation(); + /** @var PackageInterface $package */ + $package = $this->getPackageFromOperation($operation); + $package_name = $package->getName(); + + if (!isset($this->patches[$package_name])) { + if ($this->io->isVerbose()) { + $this->io->write('No patches found for ' . $package_name . '.'); + } + return; + } + $this->io->write(' - Applying patches for ' . $package_name . ''); + + // Get the install path from the package object. + $manager = $event->getComposer()->getInstallationManager(); + $install_path = $manager->getInstaller($package->getType())->getInstallPath($package); + + // Set up a downloader. + $downloader = new RemoteFilesystem($this->io, $this->composer->getConfig()); + + // Track applied patches in the package info in installed.json + $localRepository = $this->composer->getRepositoryManager()->getLocalRepository(); + $localPackage = $localRepository->findPackage($package_name, $package->getVersion()); + $extra = $localPackage->getExtra(); + $extra['patches_applied'] = array(); + + foreach ($this->patches[$package_name] as $description => $url) { + $this->io->write(' ' . $url . ' (' . $description. ')'); + try { + $this->eventDispatcher->dispatch(NULL, new PatchEvent(PatchEvents::PRE_PATCH_APPLY, $package, $url, $description)); + $this->getAndApplyPatch($downloader, $install_path, $url, $package); + $this->eventDispatcher->dispatch(NULL, new PatchEvent(PatchEvents::POST_PATCH_APPLY, $package, $url, $description)); + $extra['patches_applied'][$description] = $url; + } + catch (\Exception $e) { + $this->io->write(' Could not apply patch! Skipping. The error was: ' . $e->getMessage() . ''); + if ($exitOnFailure) { + throw new \Exception("Cannot apply patch $description ($url)!"); + } + } + } + $localPackage->setExtra($extra); + + $this->io->write(''); + + if (true !== $skipReporting) { + $this->writePatchReport($this->patches[$package_name], $install_path); + } + } + + /** + * Get a Package object from an OperationInterface object. + * + * @param OperationInterface $operation + * @return PackageInterface + * @throws \Exception + */ + protected function getPackageFromOperation(OperationInterface $operation) { + if ($operation instanceof InstallOperation) { + $package = $operation->getPackage(); + } + elseif ($operation instanceof UpdateOperation) { + $package = $operation->getTargetPackage(); + } + else { + throw new \Exception('Unknown operation: ' . get_class($operation)); + } + + return $package; + } + + /** + * Apply a patch on code in the specified directory. + * + * @param RemoteFilesystem $downloader + * @param $install_path + * @param $patch_url + * @param PackageInterface $package + * @throws \Exception + */ + protected function getAndApplyPatch(RemoteFilesystem $downloader, $install_path, $patch_url, PackageInterface $package) { + + // Local patch file. + if (file_exists($patch_url)) { + $filename = realpath($patch_url); + } + else { + // Generate random (but not cryptographically so) filename. + $filename = uniqid(sys_get_temp_dir().'/') . ".patch"; + + // Download file from remote filesystem to this location. + $hostname = parse_url($patch_url, PHP_URL_HOST); + + try { + $downloader->copy($hostname, $patch_url, $filename, false); + } catch (\Exception $e) { + // In case of an exception, retry once as the download might + // have failed due to intermittent network issues. + $downloader->copy($hostname, $patch_url, $filename, false); + } + } + + // The order here is intentional. p1 is most likely to apply with git apply. + // p0 is next likely. p2 is extremely unlikely, but for some special cases, + // it might be useful. p4 is useful for Magento 2 patches + $patch_levels = array('-p1', '-p0', '-p2', '-p4'); + + // Check for specified patch level for this package. + $extra = $this->composer->getPackage()->getExtra(); + if (!empty($extra['patchLevel'][$package->getName()])){ + $patch_levels = array($extra['patchLevel'][$package->getName()]); + } + // Attempt to apply with git apply. + $patched = $this->applyPatchWithGit($install_path, $patch_levels, $filename); + + // In some rare cases, git will fail to apply a patch, fallback to using + // the 'patch' command. + if (!$patched) { + foreach ($patch_levels as $patch_level) { + // --no-backup-if-mismatch here is a hack that fixes some + // differences between how patch works on windows and unix. + if ($patched = $this->executeCommand("patch %s --no-backup-if-mismatch -d %s < %s", $patch_level, $install_path, $filename)) { + break; + } + } + } + + // Clean up the temporary patch file. + if (isset($hostname)) { + unlink($filename); + } + // If the patch *still* isn't applied, then give up and throw an Exception. + // Otherwise, let the user know it worked. + if (!$patched) { + throw new \Exception("Cannot apply patch $patch_url"); + } + } + + /** + * Checks if the root package enables patching. + * + * @return bool + * Whether patching is enabled. Defaults to TRUE. + */ + protected function isPatchingEnabled() { + $extra = $this->composer->getPackage()->getExtra(); + + if (empty($extra['patches']) && empty($extra['patches-ignore']) && !isset($extra['patches-file'])) { + // The root package has no patches of its own, so only allow patching if + // it has specifically opted in. + return isset($extra['enable-patching']) ? $extra['enable-patching'] : FALSE; + } + else { + return TRUE; + } + } + + /** + * Writes a patch report to the target directory. + * + * @param array $patches + * @param string $directory + */ + protected function writePatchReport($patches, $directory) { + $output = "This file was automatically generated by Composer Patches (https://github.com/cweagans/composer-patches)\n"; + $output .= "Patches applied to this directory:\n\n"; + foreach ($patches as $description => $url) { + $output .= $description . "\n"; + $output .= 'Source: ' . $url . "\n\n\n"; + } + file_put_contents($directory . "/PATCHES.txt", $output); + } + + /** + * Executes a shell command with escaping. + * + * @param string $cmd + * @return bool + */ + protected function executeCommand($cmd) { + // Shell-escape all arguments except the command. + $args = func_get_args(); + foreach ($args as $index => $arg) { + if ($index !== 0) { + $args[$index] = escapeshellarg($arg); + } + } + + // And replace the arguments. + $command = call_user_func_array('sprintf', $args); + $output = ''; + if ($this->io->isVerbose()) { + $this->io->write('' . $command . ''); + $io = $this->io; + $output = function ($type, $data) use ($io) { + if ($type == Process::ERR) { + $io->write('' . $data . ''); + } + else { + $io->write('' . $data . ''); + } + }; + } + return ($this->executor->execute($command, $output) == 0); + } + + /** + * Recursively merge arrays without changing data types of values. + * + * Does not change the data types of the values in the arrays. Matching keys' + * values in the second array overwrite those in the first array, as is the + * case with array_merge. + * + * @param array $array1 + * The first array. + * @param array $array2 + * The second array. + * @return array + * The merged array. + * + * @see http://php.net/manual/en/function.array-merge-recursive.php#92195 + */ + protected function arrayMergeRecursiveDistinct(array $array1, array $array2) { + $merged = $array1; + + foreach ($array2 as $key => &$value) { + if (is_array($value) && isset($merged[$key]) && is_array($merged[$key])) { + $merged[$key] = $this->arrayMergeRecursiveDistinct($merged[$key], $value); + } + else { + $merged[$key] = $value; + } + } + + return $merged; + } + + /** + * Attempts to apply a patch with git apply. + * + * @param $install_path + * @param $patch_levels + * @param $filename + * + * @return bool + * TRUE if patch was applied, FALSE otherwise. + */ + protected function applyPatchWithGit($install_path, $patch_levels, $filename) { + // Do not use git apply unless the install path is itself a git repo + // @see https://stackoverflow.com/a/27283285 + if (!is_dir($install_path . '/.git')) { + return FALSE; + } + + $patched = FALSE; + foreach ($patch_levels as $patch_level) { + if ($this->io->isVerbose()) { + $comment = 'Testing ability to patch with git apply.'; + $comment .= ' This command may produce errors that can be safely ignored.'; + $this->io->write('' . $comment . ''); + } + $checked = $this->executeCommand('git -C %s apply --check -v %s %s', $install_path, $patch_level, $filename); + $output = $this->executor->getErrorOutput(); + if (substr($output, 0, 7) == 'Skipped') { + // Git will indicate success but silently skip patches in some scenarios. + // + // @see https://github.com/cweagans/composer-patches/pull/165 + $checked = FALSE; + } + if ($checked) { + // Apply the first successful style. + $patched = $this->executeCommand('git -C %s apply %s %s', $install_path, $patch_level, $filename); + break; + } + } + return $patched; + } + + /** + * Indicates if a package has been patched. + * + * @param \Composer\Package\PackageInterface $package + * The package to check. + * + * @return bool + * TRUE if the package has been patched. + */ + public static function isPackagePatched(PackageInterface $package) { + return array_key_exists('patches_applied', $package->getExtra()); + } + + /** + * {@inheritDoc} + */ + public function deactivate(Composer $composer, IOInterface $io) + { + } + + /** + * {@inheritDoc} + */ + public function uninstall(Composer $composer, IOInterface $io) + { + } + +} diff --git a/3rdparty/deepdiver/zipstreamer/COPYING b/3rdparty/deepdiver/zipstreamer/COPYING new file mode 100644 index 00000000..20d40b6b --- /dev/null +++ b/3rdparty/deepdiver/zipstreamer/COPYING @@ -0,0 +1,674 @@ + GNU GENERAL PUBLIC LICENSE + Version 3, 29 June 2007 + + Copyright (C) 2007 Free Software Foundation, Inc. + Everyone is permitted to copy and distribute verbatim copies + of this license document, but changing it is not allowed. + + Preamble + + The GNU General Public License is a free, copyleft license for +software and other kinds of works. + + The licenses for most software and other practical works are designed +to take away your freedom to share and change the works. By contrast, +the GNU General Public License is intended to guarantee your freedom to +share and change all versions of a program--to make sure it remains free +software for all its users. We, the Free Software Foundation, use the +GNU General Public License for most of our software; it applies also to +any other work released this way by its authors. You can apply it to +your programs, too. + + When we speak of free software, we are referring to freedom, not +price. Our General Public Licenses are designed to make sure that you +have the freedom to distribute copies of free software (and charge for +them if you wish), that you receive source code or can get it if you +want it, that you can change the software or use pieces of it in new +free programs, and that you know you can do these things. + + To protect your rights, we need to prevent others from denying you +these rights or asking you to surrender the rights. Therefore, you have +certain responsibilities if you distribute copies of the software, or if +you modify it: responsibilities to respect the freedom of others. + + For example, if you distribute copies of such a program, whether +gratis or for a fee, you must pass on to the recipients the same +freedoms that you received. You must make sure that they, too, receive +or can get the source code. And you must show them these terms so they +know their rights. + + Developers that use the GNU GPL protect your rights with two steps: +(1) assert copyright on the software, and (2) offer you this License +giving you legal permission to copy, distribute and/or modify it. + + For the developers' and authors' protection, the GPL clearly explains +that there is no warranty for this free software. For both users' and +authors' sake, the GPL requires that modified versions be marked as +changed, so that their problems will not be attributed erroneously to +authors of previous versions. + + Some devices are designed to deny users access to install or run +modified versions of the software inside them, although the manufacturer +can do so. This is fundamentally incompatible with the aim of +protecting users' freedom to change the software. The systematic +pattern of such abuse occurs in the area of products for individuals to +use, which is precisely where it is most unacceptable. Therefore, we +have designed this version of the GPL to prohibit the practice for those +products. If such problems arise substantially in other domains, we +stand ready to extend this provision to those domains in future versions +of the GPL, as needed to protect the freedom of users. + + Finally, every program is threatened constantly by software patents. +States should not allow patents to restrict development and use of +software on general-purpose computers, but in those that do, we wish to +avoid the special danger that patents applied to a free program could +make it effectively proprietary. To prevent this, the GPL assures that +patents cannot be used to render the program non-free. + + The precise terms and conditions for copying, distribution and +modification follow. + + TERMS AND CONDITIONS + + 0. Definitions. + + "This License" refers to version 3 of the GNU General Public License. + + "Copyright" also means copyright-like laws that apply to other kinds of +works, such as semiconductor masks. + + "The Program" refers to any copyrightable work licensed under this +License. Each licensee is addressed as "you". "Licensees" and +"recipients" may be individuals or organizations. + + To "modify" a work means to copy from or adapt all or part of the work +in a fashion requiring copyright permission, other than the making of an +exact copy. The resulting work is called a "modified version" of the +earlier work or a work "based on" the earlier work. + + A "covered work" means either the unmodified Program or a work based +on the Program. + + To "propagate" a work means to do anything with it that, without +permission, would make you directly or secondarily liable for +infringement under applicable copyright law, except executing it on a +computer or modifying a private copy. Propagation includes copying, +distribution (with or without modification), making available to the +public, and in some countries other activities as well. + + To "convey" a work means any kind of propagation that enables other +parties to make or receive copies. Mere interaction with a user through +a computer network, with no transfer of a copy, is not conveying. + + An interactive user interface displays "Appropriate Legal Notices" +to the extent that it includes a convenient and prominently visible +feature that (1) displays an appropriate copyright notice, and (2) +tells the user that there is no warranty for the work (except to the +extent that warranties are provided), that licensees may convey the +work under this License, and how to view a copy of this License. If +the interface presents a list of user commands or options, such as a +menu, a prominent item in the list meets this criterion. + + 1. Source Code. + + The "source code" for a work means the preferred form of the work +for making modifications to it. "Object code" means any non-source +form of a work. + + A "Standard Interface" means an interface that either is an official +standard defined by a recognized standards body, or, in the case of +interfaces specified for a particular programming language, one that +is widely used among developers working in that language. + + The "System Libraries" of an executable work include anything, other +than the work as a whole, that (a) is included in the normal form of +packaging a Major Component, but which is not part of that Major +Component, and (b) serves only to enable use of the work with that +Major Component, or to implement a Standard Interface for which an +implementation is available to the public in source code form. A +"Major Component", in this context, means a major essential component +(kernel, window system, and so on) of the specific operating system +(if any) on which the executable work runs, or a compiler used to +produce the work, or an object code interpreter used to run it. + + The "Corresponding Source" for a work in object code form means all +the source code needed to generate, install, and (for an executable +work) run the object code and to modify the work, including scripts to +control those activities. However, it does not include the work's +System Libraries, or general-purpose tools or generally available free +programs which are used unmodified in performing those activities but +which are not part of the work. For example, Corresponding Source +includes interface definition files associated with source files for +the work, and the source code for shared libraries and dynamically +linked subprograms that the work is specifically designed to require, +such as by intimate data communication or control flow between those +subprograms and other parts of the work. + + The Corresponding Source need not include anything that users +can regenerate automatically from other parts of the Corresponding +Source. + + The Corresponding Source for a work in source code form is that +same work. + + 2. Basic Permissions. + + All rights granted under this License are granted for the term of +copyright on the Program, and are irrevocable provided the stated +conditions are met. This License explicitly affirms your unlimited +permission to run the unmodified Program. The output from running a +covered work is covered by this License only if the output, given its +content, constitutes a covered work. This License acknowledges your +rights of fair use or other equivalent, as provided by copyright law. + + You may make, run and propagate covered works that you do not +convey, without conditions so long as your license otherwise remains +in force. You may convey covered works to others for the sole purpose +of having them make modifications exclusively for you, or provide you +with facilities for running those works, provided that you comply with +the terms of this License in conveying all material for which you do +not control copyright. Those thus making or running the covered works +for you must do so exclusively on your behalf, under your direction +and control, on terms that prohibit them from making any copies of +your copyrighted material outside their relationship with you. + + Conveying under any other circumstances is permitted solely under +the conditions stated below. Sublicensing is not allowed; section 10 +makes it unnecessary. + + 3. Protecting Users' Legal Rights From Anti-Circumvention Law. + + No covered work shall be deemed part of an effective technological +measure under any applicable law fulfilling obligations under article +11 of the WIPO copyright treaty adopted on 20 December 1996, or +similar laws prohibiting or restricting circumvention of such +measures. + + When you convey a covered work, you waive any legal power to forbid +circumvention of technological measures to the extent such circumvention +is effected by exercising rights under this License with respect to +the covered work, and you disclaim any intention to limit operation or +modification of the work as a means of enforcing, against the work's +users, your or third parties' legal rights to forbid circumvention of +technological measures. + + 4. Conveying Verbatim Copies. + + You may convey verbatim copies of the Program's source code as you +receive it, in any medium, provided that you conspicuously and +appropriately publish on each copy an appropriate copyright notice; +keep intact all notices stating that this License and any +non-permissive terms added in accord with section 7 apply to the code; +keep intact all notices of the absence of any warranty; and give all +recipients a copy of this License along with the Program. + + You may charge any price or no price for each copy that you convey, +and you may offer support or warranty protection for a fee. + + 5. Conveying Modified Source Versions. + + You may convey a work based on the Program, or the modifications to +produce it from the Program, in the form of source code under the +terms of section 4, provided that you also meet all of these conditions: + + a) The work must carry prominent notices stating that you modified + it, and giving a relevant date. + + b) The work must carry prominent notices stating that it is + released under this License and any conditions added under section + 7. This requirement modifies the requirement in section 4 to + "keep intact all notices". + + c) You must license the entire work, as a whole, under this + License to anyone who comes into possession of a copy. This + License will therefore apply, along with any applicable section 7 + additional terms, to the whole of the work, and all its parts, + regardless of how they are packaged. This License gives no + permission to license the work in any other way, but it does not + invalidate such permission if you have separately received it. + + d) If the work has interactive user interfaces, each must display + Appropriate Legal Notices; however, if the Program has interactive + interfaces that do not display Appropriate Legal Notices, your + work need not make them do so. + + A compilation of a covered work with other separate and independent +works, which are not by their nature extensions of the covered work, +and which are not combined with it such as to form a larger program, +in or on a volume of a storage or distribution medium, is called an +"aggregate" if the compilation and its resulting copyright are not +used to limit the access or legal rights of the compilation's users +beyond what the individual works permit. Inclusion of a covered work +in an aggregate does not cause this License to apply to the other +parts of the aggregate. + + 6. Conveying Non-Source Forms. + + You may convey a covered work in object code form under the terms +of sections 4 and 5, provided that you also convey the +machine-readable Corresponding Source under the terms of this License, +in one of these ways: + + a) Convey the object code in, or embodied in, a physical product + (including a physical distribution medium), accompanied by the + Corresponding Source fixed on a durable physical medium + customarily used for software interchange. + + b) Convey the object code in, or embodied in, a physical product + (including a physical distribution medium), accompanied by a + written offer, valid for at least three years and valid for as + long as you offer spare parts or customer support for that product + model, to give anyone who possesses the object code either (1) a + copy of the Corresponding Source for all the software in the + product that is covered by this License, on a durable physical + medium customarily used for software interchange, for a price no + more than your reasonable cost of physically performing this + conveying of source, or (2) access to copy the + Corresponding Source from a network server at no charge. + + c) Convey individual copies of the object code with a copy of the + written offer to provide the Corresponding Source. This + alternative is allowed only occasionally and noncommercially, and + only if you received the object code with such an offer, in accord + with subsection 6b. + + d) Convey the object code by offering access from a designated + place (gratis or for a charge), and offer equivalent access to the + Corresponding Source in the same way through the same place at no + further charge. You need not require recipients to copy the + Corresponding Source along with the object code. If the place to + copy the object code is a network server, the Corresponding Source + may be on a different server (operated by you or a third party) + that supports equivalent copying facilities, provided you maintain + clear directions next to the object code saying where to find the + Corresponding Source. Regardless of what server hosts the + Corresponding Source, you remain obligated to ensure that it is + available for as long as needed to satisfy these requirements. + + e) Convey the object code using peer-to-peer transmission, provided + you inform other peers where the object code and Corresponding + Source of the work are being offered to the general public at no + charge under subsection 6d. + + A separable portion of the object code, whose source code is excluded +from the Corresponding Source as a System Library, need not be +included in conveying the object code work. + + A "User Product" is either (1) a "consumer product", which means any +tangible personal property which is normally used for personal, family, +or household purposes, or (2) anything designed or sold for incorporation +into a dwelling. In determining whether a product is a consumer product, +doubtful cases shall be resolved in favor of coverage. For a particular +product received by a particular user, "normally used" refers to a +typical or common use of that class of product, regardless of the status +of the particular user or of the way in which the particular user +actually uses, or expects or is expected to use, the product. A product +is a consumer product regardless of whether the product has substantial +commercial, industrial or non-consumer uses, unless such uses represent +the only significant mode of use of the product. + + "Installation Information" for a User Product means any methods, +procedures, authorization keys, or other information required to install +and execute modified versions of a covered work in that User Product from +a modified version of its Corresponding Source. The information must +suffice to ensure that the continued functioning of the modified object +code is in no case prevented or interfered with solely because +modification has been made. + + If you convey an object code work under this section in, or with, or +specifically for use in, a User Product, and the conveying occurs as +part of a transaction in which the right of possession and use of the +User Product is transferred to the recipient in perpetuity or for a +fixed term (regardless of how the transaction is characterized), the +Corresponding Source conveyed under this section must be accompanied +by the Installation Information. But this requirement does not apply +if neither you nor any third party retains the ability to install +modified object code on the User Product (for example, the work has +been installed in ROM). + + The requirement to provide Installation Information does not include a +requirement to continue to provide support service, warranty, or updates +for a work that has been modified or installed by the recipient, or for +the User Product in which it has been modified or installed. Access to a +network may be denied when the modification itself materially and +adversely affects the operation of the network or violates the rules and +protocols for communication across the network. + + Corresponding Source conveyed, and Installation Information provided, +in accord with this section must be in a format that is publicly +documented (and with an implementation available to the public in +source code form), and must require no special password or key for +unpacking, reading or copying. + + 7. Additional Terms. + + "Additional permissions" are terms that supplement the terms of this +License by making exceptions from one or more of its conditions. +Additional permissions that are applicable to the entire Program shall +be treated as though they were included in this License, to the extent +that they are valid under applicable law. If additional permissions +apply only to part of the Program, that part may be used separately +under those permissions, but the entire Program remains governed by +this License without regard to the additional permissions. + + When you convey a copy of a covered work, you may at your option +remove any additional permissions from that copy, or from any part of +it. (Additional permissions may be written to require their own +removal in certain cases when you modify the work.) You may place +additional permissions on material, added by you to a covered work, +for which you have or can give appropriate copyright permission. + + Notwithstanding any other provision of this License, for material you +add to a covered work, you may (if authorized by the copyright holders of +that material) supplement the terms of this License with terms: + + a) Disclaiming warranty or limiting liability differently from the + terms of sections 15 and 16 of this License; or + + b) Requiring preservation of specified reasonable legal notices or + author attributions in that material or in the Appropriate Legal + Notices displayed by works containing it; or + + c) Prohibiting misrepresentation of the origin of that material, or + requiring that modified versions of such material be marked in + reasonable ways as different from the original version; or + + d) Limiting the use for publicity purposes of names of licensors or + authors of the material; or + + e) Declining to grant rights under trademark law for use of some + trade names, trademarks, or service marks; or + + f) Requiring indemnification of licensors and authors of that + material by anyone who conveys the material (or modified versions of + it) with contractual assumptions of liability to the recipient, for + any liability that these contractual assumptions directly impose on + those licensors and authors. + + All other non-permissive additional terms are considered "further +restrictions" within the meaning of section 10. If the Program as you +received it, or any part of it, contains a notice stating that it is +governed by this License along with a term that is a further +restriction, you may remove that term. If a license document contains +a further restriction but permits relicensing or conveying under this +License, you may add to a covered work material governed by the terms +of that license document, provided that the further restriction does +not survive such relicensing or conveying. + + If you add terms to a covered work in accord with this section, you +must place, in the relevant source files, a statement of the +additional terms that apply to those files, or a notice indicating +where to find the applicable terms. + + Additional terms, permissive or non-permissive, may be stated in the +form of a separately written license, or stated as exceptions; +the above requirements apply either way. + + 8. Termination. + + You may not propagate or modify a covered work except as expressly +provided under this License. Any attempt otherwise to propagate or +modify it is void, and will automatically terminate your rights under +this License (including any patent licenses granted under the third +paragraph of section 11). + + However, if you cease all violation of this License, then your +license from a particular copyright holder is reinstated (a) +provisionally, unless and until the copyright holder explicitly and +finally terminates your license, and (b) permanently, if the copyright +holder fails to notify you of the violation by some reasonable means +prior to 60 days after the cessation. + + Moreover, your license from a particular copyright holder is +reinstated permanently if the copyright holder notifies you of the +violation by some reasonable means, this is the first time you have +received notice of violation of this License (for any work) from that +copyright holder, and you cure the violation prior to 30 days after +your receipt of the notice. + + Termination of your rights under this section does not terminate the +licenses of parties who have received copies or rights from you under +this License. If your rights have been terminated and not permanently +reinstated, you do not qualify to receive new licenses for the same +material under section 10. + + 9. Acceptance Not Required for Having Copies. + + You are not required to accept this License in order to receive or +run a copy of the Program. Ancillary propagation of a covered work +occurring solely as a consequence of using peer-to-peer transmission +to receive a copy likewise does not require acceptance. However, +nothing other than this License grants you permission to propagate or +modify any covered work. These actions infringe copyright if you do +not accept this License. Therefore, by modifying or propagating a +covered work, you indicate your acceptance of this License to do so. + + 10. Automatic Licensing of Downstream Recipients. + + Each time you convey a covered work, the recipient automatically +receives a license from the original licensors, to run, modify and +propagate that work, subject to this License. You are not responsible +for enforcing compliance by third parties with this License. + + An "entity transaction" is a transaction transferring control of an +organization, or substantially all assets of one, or subdividing an +organization, or merging organizations. If propagation of a covered +work results from an entity transaction, each party to that +transaction who receives a copy of the work also receives whatever +licenses to the work the party's predecessor in interest had or could +give under the previous paragraph, plus a right to possession of the +Corresponding Source of the work from the predecessor in interest, if +the predecessor has it or can get it with reasonable efforts. + + You may not impose any further restrictions on the exercise of the +rights granted or affirmed under this License. For example, you may +not impose a license fee, royalty, or other charge for exercise of +rights granted under this License, and you may not initiate litigation +(including a cross-claim or counterclaim in a lawsuit) alleging that +any patent claim is infringed by making, using, selling, offering for +sale, or importing the Program or any portion of it. + + 11. Patents. + + A "contributor" is a copyright holder who authorizes use under this +License of the Program or a work on which the Program is based. The +work thus licensed is called the contributor's "contributor version". + + A contributor's "essential patent claims" are all patent claims +owned or controlled by the contributor, whether already acquired or +hereafter acquired, that would be infringed by some manner, permitted +by this License, of making, using, or selling its contributor version, +but do not include claims that would be infringed only as a +consequence of further modification of the contributor version. For +purposes of this definition, "control" includes the right to grant +patent sublicenses in a manner consistent with the requirements of +this License. + + Each contributor grants you a non-exclusive, worldwide, royalty-free +patent license under the contributor's essential patent claims, to +make, use, sell, offer for sale, import and otherwise run, modify and +propagate the contents of its contributor version. + + In the following three paragraphs, a "patent license" is any express +agreement or commitment, however denominated, not to enforce a patent +(such as an express permission to practice a patent or covenant not to +sue for patent infringement). To "grant" such a patent license to a +party means to make such an agreement or commitment not to enforce a +patent against the party. + + If you convey a covered work, knowingly relying on a patent license, +and the Corresponding Source of the work is not available for anyone +to copy, free of charge and under the terms of this License, through a +publicly available network server or other readily accessible means, +then you must either (1) cause the Corresponding Source to be so +available, or (2) arrange to deprive yourself of the benefit of the +patent license for this particular work, or (3) arrange, in a manner +consistent with the requirements of this License, to extend the patent +license to downstream recipients. "Knowingly relying" means you have +actual knowledge that, but for the patent license, your conveying the +covered work in a country, or your recipient's use of the covered work +in a country, would infringe one or more identifiable patents in that +country that you have reason to believe are valid. + + If, pursuant to or in connection with a single transaction or +arrangement, you convey, or propagate by procuring conveyance of, a +covered work, and grant a patent license to some of the parties +receiving the covered work authorizing them to use, propagate, modify +or convey a specific copy of the covered work, then the patent license +you grant is automatically extended to all recipients of the covered +work and works based on it. + + A patent license is "discriminatory" if it does not include within +the scope of its coverage, prohibits the exercise of, or is +conditioned on the non-exercise of one or more of the rights that are +specifically granted under this License. You may not convey a covered +work if you are a party to an arrangement with a third party that is +in the business of distributing software, under which you make payment +to the third party based on the extent of your activity of conveying +the work, and under which the third party grants, to any of the +parties who would receive the covered work from you, a discriminatory +patent license (a) in connection with copies of the covered work +conveyed by you (or copies made from those copies), or (b) primarily +for and in connection with specific products or compilations that +contain the covered work, unless you entered into that arrangement, +or that patent license was granted, prior to 28 March 2007. + + Nothing in this License shall be construed as excluding or limiting +any implied license or other defenses to infringement that may +otherwise be available to you under applicable patent law. + + 12. No Surrender of Others' Freedom. + + If conditions are imposed on you (whether by court order, agreement or +otherwise) that contradict the conditions of this License, they do not +excuse you from the conditions of this License. If you cannot convey a +covered work so as to satisfy simultaneously your obligations under this +License and any other pertinent obligations, then as a consequence you may +not convey it at all. For example, if you agree to terms that obligate you +to collect a royalty for further conveying from those to whom you convey +the Program, the only way you could satisfy both those terms and this +License would be to refrain entirely from conveying the Program. + + 13. Use with the GNU Affero General Public License. + + Notwithstanding any other provision of this License, you have +permission to link or combine any covered work with a work licensed +under version 3 of the GNU Affero General Public License into a single +combined work, and to convey the resulting work. The terms of this +License will continue to apply to the part which is the covered work, +but the special requirements of the GNU Affero General Public License, +section 13, concerning interaction through a network will apply to the +combination as such. + + 14. Revised Versions of this License. + + The Free Software Foundation may publish revised and/or new versions of +the GNU General Public License from time to time. Such new versions will +be similar in spirit to the present version, but may differ in detail to +address new problems or concerns. + + Each version is given a distinguishing version number. If the +Program specifies that a certain numbered version of the GNU General +Public License "or any later version" applies to it, you have the +option of following the terms and conditions either of that numbered +version or of any later version published by the Free Software +Foundation. If the Program does not specify a version number of the +GNU General Public License, you may choose any version ever published +by the Free Software Foundation. + + If the Program specifies that a proxy can decide which future +versions of the GNU General Public License can be used, that proxy's +public statement of acceptance of a version permanently authorizes you +to choose that version for the Program. + + Later license versions may give you additional or different +permissions. However, no additional obligations are imposed on any +author or copyright holder as a result of your choosing to follow a +later version. + + 15. Disclaimer of Warranty. + + THERE IS NO WARRANTY FOR THE PROGRAM, TO THE EXTENT PERMITTED BY +APPLICABLE LAW. EXCEPT WHEN OTHERWISE STATED IN WRITING THE COPYRIGHT +HOLDERS AND/OR OTHER PARTIES PROVIDE THE PROGRAM "AS IS" WITHOUT WARRANTY +OF ANY KIND, EITHER EXPRESSED OR IMPLIED, INCLUDING, BUT NOT LIMITED TO, +THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR +PURPOSE. THE ENTIRE RISK AS TO THE QUALITY AND PERFORMANCE OF THE PROGRAM +IS WITH YOU. SHOULD THE PROGRAM PROVE DEFECTIVE, YOU ASSUME THE COST OF +ALL NECESSARY SERVICING, REPAIR OR CORRECTION. + + 16. Limitation of Liability. + + IN NO EVENT UNLESS REQUIRED BY APPLICABLE LAW OR AGREED TO IN WRITING +WILL ANY COPYRIGHT HOLDER, OR ANY OTHER PARTY WHO MODIFIES AND/OR CONVEYS +THE PROGRAM AS PERMITTED ABOVE, BE LIABLE TO YOU FOR DAMAGES, INCLUDING ANY +GENERAL, SPECIAL, INCIDENTAL OR CONSEQUENTIAL DAMAGES ARISING OUT OF THE +USE OR INABILITY TO USE THE PROGRAM (INCLUDING BUT NOT LIMITED TO LOSS OF +DATA OR DATA BEING RENDERED INACCURATE OR LOSSES SUSTAINED BY YOU OR THIRD +PARTIES OR A FAILURE OF THE PROGRAM TO OPERATE WITH ANY OTHER PROGRAMS), +EVEN IF SUCH HOLDER OR OTHER PARTY HAS BEEN ADVISED OF THE POSSIBILITY OF +SUCH DAMAGES. + + 17. Interpretation of Sections 15 and 16. + + If the disclaimer of warranty and limitation of liability provided +above cannot be given local legal effect according to their terms, +reviewing courts shall apply local law that most closely approximates +an absolute waiver of all civil liability in connection with the +Program, unless a warranty or assumption of liability accompanies a +copy of the Program in return for a fee. + + END OF TERMS AND CONDITIONS + + How to Apply These Terms to Your New Programs + + If you develop a new program, and you want it to be of the greatest +possible use to the public, the best way to achieve this is to make it +free software which everyone can redistribute and change under these terms. + + To do so, attach the following notices to the program. It is safest +to attach them to the start of each source file to most effectively +state the exclusion of warranty; and each file should have at least +the "copyright" line and a pointer to where the full notice is found. + + + Copyright (C) + + This program is free software: you can redistribute it and/or modify + it under the terms of the GNU General Public License as published by + the Free Software Foundation, either version 3 of the License, or + (at your option) any later version. + + This program is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU General Public License for more details. + + You should have received a copy of the GNU General Public License + along with this program. If not, see . + +Also add information on how to contact you by electronic and paper mail. + + If the program does terminal interaction, make it output a short +notice like this when it starts in an interactive mode: + + Copyright (C) + This program comes with ABSOLUTELY NO WARRANTY; for details type `show w'. + This is free software, and you are welcome to redistribute it + under certain conditions; type `show c' for details. + +The hypothetical commands `show w' and `show c' should show the appropriate +parts of the General Public License. Of course, your program's commands +might be different; for a GUI interface, you would use an "about box". + + You should also get your employer (if you work as a programmer) or school, +if any, to sign a "copyright disclaimer" for the program, if necessary. +For more information on this, and how to apply and follow the GNU GPL, see +. + + The GNU General Public License does not permit incorporating your program +into proprietary programs. If your program is a subroutine library, you +may consider it more useful to permit linking proprietary applications with +the library. If this is what you want to do, use the GNU Lesser General +Public License instead of this License. But first, please read +. \ No newline at end of file diff --git a/3rdparty/deepdiver/zipstreamer/MANUAL.md b/3rdparty/deepdiver/zipstreamer/MANUAL.md new file mode 100644 index 00000000..227c1359 --- /dev/null +++ b/3rdparty/deepdiver/zipstreamer/MANUAL.md @@ -0,0 +1,153 @@ +ZipStreamer Manual +================== + +This is a short manual to using ZipStreamer in a php web application. + +In short, it works as follows: a ZipStreamer object is initialized. +Afterwards, (file) streams and directory names/paths can be added to the +ZipStreamer object, which will immediately be streamed to the client (web +browser). After adding all desired files/directories, the ZipStreamer object +is finalized and the zip file is then complete. + +Example +------- +```php +require("src/ZipStreamer.php"); + +# initialize ZipStreamer object (ZipStreamer has it's own namespace) +$zip = new ZipStreamer\ZipStreamer(); + +# optionally send fitting headers - you can also send your own headers if +# desired or omit them if you want to stream to other targets but a http +# client +#$zip->sendHeaders(); + +# get a stream to a file to be added to the zip file +$stream = fopen('inputfile.txt','r'); + +# add the file to the zip stream (output is sent immediately) +$zip->addFileFromStream($stream, 'test1.txt'); + +# close the stream if you opened it yourself +fclose($stream); + +# add an empty directory to the zip file (also sent immediately) +$zip->addEmptyDir("testdirectory"); + +# finalize zip file. Nothing can be added any more. +$zip->finalize(); + +``` + +Characteristics +--------------- + +* **Performance:** ZipStreamer causes no disk i/o (aside from the input +streams, if they are created from disk), has low cpu usage (especially when +not compressing) and a low memory footprint, as the streams are read in small +chunks +* **Compatibility issues:** ZipStreamer produces 'streamed' zip files (part of +the zip standard since 1993). Some (mostly older) zip tools and Mac OS X finder +can not handle that. ZipStreamer by default uses the Zip64 extension. Some +(mostly older) zip tools and Mac OS X can not handle that, therefore it can be +disabled (see below) +* **Large output files:** With the Zip64 extension, ZipStreamer can handle +output zip files larger than 2/4 GB on both 32bit and 64bit machines +* **Large input files:** With the Zip64 extension, ZipStreamer can handle +input streams larger then 2/4 GB on both 32bit and 64bit machines. On 32bit +machines, that usually means that the LFS has to be enabled (but if the stream +source is not the filesystem, that may not even be necessary) +* **Compression:** ZipStreamer will not compress the content by default. That +means that the output zip file will be of the same size (plus a few bytes) as +the input files. However, if the pecl_http extension (>= 0.10) is available, +deflate (the zip standard) compression can be enabled and/or disabled globally +and per file. Without pecl_http extension, it is still possible to enable +deflate compression, but with compression level 0, so there is no actual +compression. + +API Documentation +----------------- + +This is the documentation of the public API of ZipStreamer. + +###Namespace ZipSteamer +####Class Zipstreamer + +#####Methods +``` +__construct(array $options) +``` + +Constructor. Initializes ZipStreamer object for immediate usage. + +Valid options for ZipStreamer are: + +* stream *outstream*: the zip file is output to (default: stdout) +* int *compress*: compression method (one of *COMPR::STORE*, +*COMPR::DEFLATE*, default *COMPR::STORE*) can be overridden for single files +* int *level*: compression level (one of *COMPR::NONE*, *COMPR::NORMAL*, +*COMPR::MAXIMUM*, *COMPR::SUPERFAST*, default *COMPR::NORMAL*) can be +overridden for single files +* zip64: boolean indicating use of Zip64 extension (default: True) + + +######Parameters + * array *$options* Optional, ZipStreamer and zip file options as key/value pairs. + +``` +sendHeaders(string $archiveName, string $contentType) +``` + +Send appropriate http headers before streaming the zip file and disable output buffering. + +This method, if used, has to be called before adding anything to the zip file. + +######Parameters +* string *$archiveName* Filename of archive to be created (optional, default 'archive.zip') +* string *$contentType* Content mime type to be set (optional, default 'application/zip') + +``` +addFileFromStream(string $stream, string $filePath, array $options) : bool +``` + +Add a file to the archive at the specified location and file name. + +######Parameters +* string *$stream* Stream to read data from +* string *$filePath* Filepath and name to be used in the archive. +* array *options* (optional) additional options. Valid options are: + * int *$timestamp* Timestamp for the file (default: current time) + * string *$comment* comment to be added for this file (default: none) + * int *compress*: compression method (override global option for this + file) + * int *level*: compression level (override global option for this file) + +######Returns +bool Success + +``` +addEmptyDir(string $directoryPath, array $options) : bool +``` + +Add an empty directory entry to the zip archive. + +######Parameters +* string *$directoryPath* Directory Path and name to be added to the archive. +* array *options* (optional) additional options. Valid options are: + * int *$timestamp* Timestamp for the dir (default: current time) + * string *$comment* comment to be added for the dir (default: none) + +######Returns +bool Success + +``` +finalize() : bool +``` + +Close the archive. + +A closed archive can no longer have new files added to it. After closing, the zip file is completely written to the output stream. + +######Returns +bool Success + diff --git a/3rdparty/deepdiver/zipstreamer/src/COMPR.php b/3rdparty/deepdiver/zipstreamer/src/COMPR.php new file mode 100644 index 00000000..664b679b --- /dev/null +++ b/3rdparty/deepdiver/zipstreamer/src/COMPR.php @@ -0,0 +1,36 @@ +. + * + * @author Nicolai Ehemann + * @author André Rothe + * @copyright Copyright (C) 2013-2015 Nicolai Ehemann and contributors + * @license GNU GPL + * @version 1.0 + */ +namespace ZipStreamer; + +class COMPR { + // compression method + const STORE = 0x0000; // 0 - The file is stored (no compression) + const DEFLATE = 0x0008; // 8 - The file is deflated + + // compression level (for deflate compression) + const NONE = 0; + const NORMAL = 1; + const MAXIMUM = 2; + const SUPERFAST = 3; +} diff --git a/3rdparty/deepdiver/zipstreamer/src/Count64.php b/3rdparty/deepdiver/zipstreamer/src/Count64.php new file mode 100644 index 00000000..1101f0d5 --- /dev/null +++ b/3rdparty/deepdiver/zipstreamer/src/Count64.php @@ -0,0 +1,147 @@ +. + * + * @author Nicolai Ehemann + * @copyright Copyright (C) 2013-2014 Nicolai Ehemann and contributors + * @license GNU GPL + */ +namespace ZipStreamer; + +use ZipStreamer\Lib\Count64_32; +use ZipStreamer\Lib\Count64_64; +use ZipStreamer\Lib\Count64Base; + +const INT64_HIGH_MAP = 0xffffffff00000000; +const INT64_LOW_MAP = 0x00000000ffffffff; +const INT_MAX_32 = 0xffffffff; + +/** + * Unsigned right shift + * + * @param int $bits integer to be shifted + * @param int $shift number of bits to be shifted + * @return int shifted integer + */ +function urShift($bits, $shift) { + if ($shift == 0) { + return $bits; + } + return ($bits >> $shift) & ~(1 << (8 * PHP_INT_SIZE - 1) >> ($shift - 1)); +} + +/** + * Pack 1 byte data into binary string + * + * @param mixed $data data + * @return string 1 byte binary string + */ +function pack8($data) { + return pack('C', $data); +} + +/** + * Pack 2 byte data into binary string, little endian format + * + * @param mixed $data data + * @return string 2 byte binary string + */ +function pack16le($data) { + return pack('v', $data); +} + +/** + * Unpack 2 byte binary string, little endian format to 2 byte data + * + * @param string $data binary string + * @return integer 2 byte data + */ +function unpack16le($data) { + $result = unpack('v', $data); + return $result[1]; +} + +/** + * Pack 4 byte data into binary string, little endian format + * + * @param mixed $data data + * @return 4 byte binary string + */ +function pack32le($data) { + return pack('V', $data); +} + +/** + * Unpack 4 byte binary string, little endian format to 4 byte data + * + * @param string $data binary string + * @return integer 4 byte data + */ +function unpack32le($data) { + $result = unpack('V', $data); + return $result[1]; +} + +/** + * Pack 8 byte data into binary string, little endian format + * + * @param mixed $data data + * @return string 8 byte binary string + */ +function pack64le($data) { + if (is_object($data)) { + if (Count64_32::class == get_class($data)) { + $value = $data->_getValue(); + $hiBytess = $value[0]; + $loBytess = $value[1]; + } else { + $hiBytess = ($data->_getValue() & INT64_HIGH_MAP) >> 32; + $loBytess = $data->_getValue() & INT64_LOW_MAP; + } + } else if (4 == PHP_INT_SIZE) { + $hiBytess = 0; + $loBytess = $data; + } else { + $hiBytess = ($data & INT64_HIGH_MAP) >> 32; + $loBytess = $data & INT64_LOW_MAP; + } + return pack('VV', $loBytess, $hiBytess); +} + +/** + * Unpack 8 byte binary string, little endian format to 8 byte data + * + * @param string $data binary string + * @return Count64Base data + */ +function unpack64le($data) { + $bytes = unpack('V2', $data); + return Count64::construct(array( + $bytes[1], + $bytes[2] + )); +} + +abstract class Count64 { + public static function construct($value = 0, $limit32Bit = False) { + if (4 == PHP_INT_SIZE) { + return new Count64_32($value, $limit32Bit); + } + + return new Count64_64($value, $limit32Bit); + } +} diff --git a/3rdparty/deepdiver/zipstreamer/src/Lib/Count64Base.php b/3rdparty/deepdiver/zipstreamer/src/Lib/Count64Base.php new file mode 100644 index 00000000..a97a047f --- /dev/null +++ b/3rdparty/deepdiver/zipstreamer/src/Lib/Count64Base.php @@ -0,0 +1,42 @@ +. + * + * @author Nicolai Ehemann + * @copyright Copyright (C) 2013-2014 Nicolai Ehemann and contributors + * @license GNU GPL + */ +namespace ZipStreamer\Lib; + +abstract class Count64Base { + protected $limit32Bit = False; + + public function __construct($value = 0, $limit32Bit = False) { + $this->limit32Bit = $limit32Bit; + $this->set($value); + } + + abstract public function set($value); + abstract public function add($value); + abstract public function getHiBytes(); + abstract public function getLoBytes(); + abstract public function _getValue(); + + const EXCEPTION_SET_INVALID_ARGUMENT = "Count64 object can only be set() to integer or Count64 values"; + const EXCEPTION_ADD_INVALID_ARGUMENT = "Count64 object can only be add()ed integer or Count64 values"; + const EXCEPTION_32BIT_OVERFLOW = "Count64 object limited to 32 bit (overflow)"; +} diff --git a/3rdparty/deepdiver/zipstreamer/src/Lib/Count64_32.php b/3rdparty/deepdiver/zipstreamer/src/Lib/Count64_32.php new file mode 100644 index 00000000..80057744 --- /dev/null +++ b/3rdparty/deepdiver/zipstreamer/src/Lib/Count64_32.php @@ -0,0 +1,96 @@ +. + * + * @author Nicolai Ehemann + * @copyright Copyright (C) 2013-2014 Nicolai Ehemann and contributors + * @license GNU GPL + */ +namespace ZipStreamer\Lib; + +class Count64_32 extends Count64Base { + private $loBytes; + private $hiBytes; + + public function getHiBytes() { + return $this->hiBytes; + } + + public function getLoBytes() { + return $this->loBytes; + } + + public function _getValue() { + return array($this->hiBytes, $this->loBytes); + } + + public function set($value) { + if (is_int($value)) { + $this->loBytes = $value; + $this->hiBytes = 0; + } else if (is_array($value) && 2 == count($value)) { + $this->loBytes = $value[0]; + if ($this->limit32Bit && 0 !== $value[1]) { + throw new \OverflowException(self::EXCEPTION_32BIT_OVERFLOW); + } + $this->hiBytes = $value[1]; + } else if (is_object($value) && __CLASS__ == get_class($value)) { + $value = $value->_getValue(); + if ($this->limit32Bit && 0 !== $value[0]) { + throw new \OverflowException(self::EXCEPTION_32BIT_OVERFLOW); + } + $this->hiBytes = $value[0]; + $this->loBytes = $value[1]; + } else { + throw new \InvalidArgumentException(self::EXCEPTION_SET_INVALID_ARGUMENT); + } + return $this; + } + + public function add($value) { + if (is_int($value)) { + $sum = (int) ($this->loBytes + $value); + // overflow! + if (($this->loBytes > -1 && $sum < $this->loBytes && $sum > -1) + || ($this->loBytes < 0 && ($sum < $this->loBytes || $sum > -1))) { + if ($this->limit32Bit) { + throw new \OverflowException(self::EXCEPTION_32BIT_OVERFLOW); + } + $this->hiBytes = (int) ($this->hiBytes + 1); + } + $this->loBytes = $sum; + } else if (is_object($value) && __CLASS__ == get_class($value)) { + $value = $value->_getValue(); + $sum = (int) ($this->loBytes + $value[1]); + if (($this->loBytes > -1 && $sum < $this->loBytes && $sum > -1) + || ($this->loBytes < 0 && ($sum < $this->loBytes || $sum > -1))) { + if ($this->limit32Bit) { + throw new \OverflowException(self::EXCEPTION_32BIT_OVERFLOW); + } + $this->hiBytes = (int) ($this->hiBytes + 1); + } + $this->loBytes = $sum; + if ($this->limit32Bit && 0 !== $value[0]) { + throw new \OverflowException(self::EXCEPTION_32BIT_OVERFLOW); + } + $this->hiBytes = (int) ($this->hiBytes + $value[0]); + } else { + throw new \InvalidArgumentException(self::EXCEPTION_ADD_INVALID_ARGUMENT); + } + return $this; + } +} diff --git a/3rdparty/deepdiver/zipstreamer/src/Lib/Count64_64.php b/3rdparty/deepdiver/zipstreamer/src/Lib/Count64_64.php new file mode 100644 index 00000000..e27021a2 --- /dev/null +++ b/3rdparty/deepdiver/zipstreamer/src/Lib/Count64_64.php @@ -0,0 +1,84 @@ +. + * + * @author Nicolai Ehemann + * @copyright Copyright (C) 2013-2014 Nicolai Ehemann and contributors + * @license GNU GPL + */ +namespace ZipStreamer\Lib; + +use function ZipStreamer\urShift; +use const ZipStreamer\INT64_LOW_MAP; +use const ZipStreamer\INT_MAX_32; + +class Count64_64 extends Count64Base { + private $value; + + public function getHiBytes() { + return urShift($this->value, 32); + } + + public function getLoBytes() { + return $this->value & INT64_LOW_MAP; + } + + public function _getValue() { + return $this->value; + } + + public function set($value) { + if (is_int($value)) { + if ($this->limit32Bit && INT_MAX_32 < $value) { + throw new \OverFlowException(self::EXCEPTION_32BIT_OVERFLOW); + } + $this->value = $value; + } else if (is_array($value) && 2 == count($value)) { + if ($this->limit32Bit && 0 !== $value[1]) { + throw new \OverFlowException(self::EXCEPTION_32BIT_OVERFLOW); + } + $this->value = $value[1]; + $this->value = $this->value << 32; + $this->value = $this->value + $value[0]; + } else if (is_object($value) && __CLASS__ == get_class($value)) { + $value = $value->_getValue(); + if ($this->limit32Bit && INT_MAX_32 < $value) { + throw new \OverFlowException(self::EXCEPTION_32BIT_OVERFLOW); + } + $this->value = $value; + + } else { + throw new \InvalidArgumentException(self::EXCEPTION_SET_INVALID_ARGUMENT); + } + return $this; + } + + public function add($value) { + if (is_int($value)) { + $sum = (int) ($this->value + $value); + } else if (is_object($value) && __CLASS__ == get_class($value)) { + $sum = (int) ($this->value + $value->_getValue()); + } else { + throw new \InvalidArgumentException(self::EXCEPTION_ADD_INVALID_ARGUMENT); + } + if ($this->limit32Bit && INT_MAX_32 < $sum) { + throw new \OverFlowException(self::EXCEPTION_32BIT_OVERFLOW); + } + $this->value = $sum; + return $this; + } +} diff --git a/3rdparty/deepdiver/zipstreamer/src/ZipStreamer.php b/3rdparty/deepdiver/zipstreamer/src/ZipStreamer.php new file mode 100644 index 00000000..e476eb62 --- /dev/null +++ b/3rdparty/deepdiver/zipstreamer/src/ZipStreamer.php @@ -0,0 +1,730 @@ +. + * + * Inspired by + * CreateZipFile by Rochak Chauhan www.rochakchauhan.com (http://www.phpclasses.org/browse/package/2322.html) + * and + * ZipStream by A. Grandt https://github.com/Grandt/PHPZip (http://www.phpclasses.org/package/6116) + * + * Unix-File attributes according to + * http://unix.stackexchange.com/questions/14705/the-zip-formats-external-file-attribute + * + * @author Nicolai Ehemann + * @author André Rothe + * @copyright Copyright (C) 2013-2015 Nicolai Ehemann and contributors + * @license GNU GPL + * @version 1.0 + */ +namespace ZipStreamer; + +class ZipStreamer { + const VERSION = "1.0"; + + const ZIP_LOCAL_FILE_HEADER = 0x04034b50; // local file header signature + const ZIP_DATA_DESCRIPTOR_HEADER = 0x08074b50; //data descriptor header signature + const ZIP_CENTRAL_FILE_HEADER = 0x02014b50; // central file header signature + const ZIP_END_OF_CENTRAL_DIRECTORY = 0x06054b50; // end of central directory record + const ZIP64_END_OF_CENTRAL_DIRECTORY = 0x06064b50; //zip64 end of central directory record + const ZIP64_END_OF_CENTRAL_DIR_LOCATOR = 0x07064b50; // zip64 end of central directory locator + + const ATTR_MADE_BY_VERSION = 0x032d; // made by version (upper byte: UNIX, lower byte v4.5) + + const STREAM_CHUNK_SIZE = 1048560; // 16 * 65535 = almost 1mb chunks, for best deflate performance + + private $extFileAttrFile; + private $extFileAttrDir; + + /** @var resource $outStream output stream zip file is written to */ + private $outStream; + /** @var boolean zip64 enabled */ + private $zip64 = True; + /** @var int compression method */ + private $compress; + /** @var int compression level */ + private $level; + + /** @var array central directory record */ + private $cdRec = array(); + /** @var int offset of next file to be added */ + private $offset; + /** @var boolean indicates zip is finalized and sent to client; no further addition possible */ + private $isFinalized = false; + /** @var bool only used for unit testing */ + public $turnOffOutputBuffering = true; + + /** + * Constructor. Initializes ZipStreamer object for immediate usage. + * @param array $options Optional, ZipStreamer and zip file options as key/value pairs. + * Valid options are: + * * outstream: stream the zip file is output to (default: stdout) + * * zip64: enabled/disable zip64 support (default: True) + * * compress: int, compression method (one of COMPR::STORE, + * COMPR::DEFLATE, default COMPR::STORE) + * can be overridden for single files + * * level: int, compression level (one of COMPR::NORMAL, + * COMPR::MAXIMUM, COMPR::SUPERFAST, default COMPR::NORMAL) + */ + function __construct($options = NULL) { + $defaultOptions = array( + 'outstream' => NULL, + 'zip64' => True, + 'compress' => COMPR::STORE, + 'level' => COMPR::NORMAL, + ); + if (is_null($options)) { + $options = array(); + } + $options = array_merge($defaultOptions, $options); + + if ($options['outstream']) { + $this->outStream = $options['outstream']; + } else { + $this->outStream = fopen('php://output', 'w'); + } + $this->zip64 = $options['zip64']; + $this->compress = $options['compress']; + $this->level = $options['level']; + $this->validateCompressionOptions($this->compress, $this->level); + //TODO: is this advisable/necessary? + if (ini_get('zlib.output_compression')) { + ini_set('zlib.output_compression', 'Off'); + } + // initialize default external file attributes + $this->extFileAttrFile = UNIX::getExtFileAttr(UNIX::S_IFREG | + UNIX::S_IRUSR | UNIX::S_IWUSR | UNIX::S_IRGRP | + UNIX::S_IROTH); + $this->extFileAttrDir = UNIX::getExtFileAttr(UNIX::S_IFDIR | + UNIX::S_IRWXU | UNIX::S_IRGRP | UNIX::S_IXGRP | + UNIX::S_IROTH | UNIX::S_IXOTH) | + DOS::getExtFileAttr(DOS::DIR); + $this->offset = Count64::construct(0, !$this->zip64); + } + + function __destruct() { + $this->isFinalized = true; + $this->cdRec = null; + } + + private function getVersionToExtract($isDir) { + if ($this->zip64) { + $version = 0x2d; // 4.5 - File uses ZIP64 format extensions + } else if ($isDir) { + $version = 0x14; // 2.0 - File is a folder (directory) + } else { + $version = 0x0a; // 1.0 - Default value + } + return $version; + } + + /** + * Send appropriate http headers before streaming the zip file and disable output buffering. + * This method, if used, has to be called before adding anything to the zip file. + * + * @param string $archiveName Filename of archive to be created (optional, default 'archive.zip') + * @param string $contentType Content mime type to be set (optional, default 'application/zip') + */ + public function sendHeaders($archiveName = 'archive.zip', $contentType = 'application/zip') { + $headerFile = null; + $headerLine = null; + if (!headers_sent($headerFile, $headerLine) + or die("

Error: Unable to send file " . + "$archiveName. HTML Headers have already been sent from " . + "$headerFile in line $headerLine" . + "

")) { + if ((ob_get_contents() === false || ob_get_contents() == '') + or die("\n

Error: Unable to send file " . + "$archiveName.epub. Output buffer " . + "already contains text (typically warnings or errors).

")) { + header('Pragma: public'); + header('Last-Modified: ' . gmdate('D, d M Y H:i:s T')); + header('Expires: 0'); + header('Accept-Ranges: bytes'); + header('Connection: Keep-Alive'); + header('Content-Type: ' . $contentType); + // Use UTF-8 filenames when not using Internet Explorer + if(isset($_SERVER['HTTP_USER_AGENT']) && strpos($_SERVER['HTTP_USER_AGENT'], 'MSIE') > 0) { + header('Content-Disposition: attachment; filename="' . rawurlencode($archiveName) . '"' ); + } else { + header( 'Content-Disposition: attachment; filename*=UTF-8\'\'' . rawurlencode($archiveName) + . '; filename="' . rawurlencode($archiveName) . '"' ); + } + header('Content-Transfer-Encoding: binary'); + } + } + $this->flush(); + // turn off output buffering + if ($this->turnOffOutputBuffering) { + @ob_end_flush(); + } + } + + /** + * Add a file to the archive at the specified location and file name. + * + * @param resource $stream Stream to read data from + * @param string $filePath Filepath and name to be used in the archive. + * @param array $options Optional, additional options + * Valid options are: + * * int timestamp: timestamp for the file (default: current time) + * * string comment: comment to be added for this file (default: none) + * * int compress: compression method (override global option for this file) + * * int level: compression level (override global option for this file) + * @return bool $success + */ + public function addFileFromStream($stream, $filePath, $options = NULL) { + if ($this->isFinalized) { + return false; + } + $defaultOptions = array( + 'timestamp' => NULL, + 'comment' => NULL, + 'compress' => $this->compress, + 'level' => $this->level, + ); + if (is_null($options)) { + $options = array(); + } + $options = array_merge($defaultOptions, $options); + $this->validateCompressionOptions($options['compress'], $options['level']); + + if (!is_resource($stream) || get_resource_type($stream) != 'stream') { + return false; + } + + $filePath = self::normalizeFilePath($filePath); + + $gpFlags = GPFLAGS::ADD; + + list($gpFlags, $lfhLength) = $this->beginFile($filePath, False, $options['comment'], $options['timestamp'], $gpFlags, $options['compress']); + list($dataLength, $gzLength, $dataCRC32) = $this->streamFileData($stream, $options['compress'], $options['level']); + + $ddLength = $this->addDataDescriptor($dataLength, $gzLength, $dataCRC32); + + // build cdRec + $this->cdRec[] = $this->buildCentralDirectoryHeader($filePath, $options['timestamp'], $gpFlags, $options['compress'], + $dataLength, $gzLength, $dataCRC32, $this->extFileAttrFile, False); + + // calc offset + $this->offset->add($ddLength)->add($lfhLength)->add($gzLength); + + return true; + } + + /** + * Add an empty directory entry to the zip archive. + * + * @param string $directoryPath Directory Path and name to be added to the archive. + * @param array $options Optional, additional options + * Valid options are: + * * int timestamp: timestamp for the file (default: current time) + * * string comment: comment to be added for this file (default: none) + * @return bool $success + */ + public function addEmptyDir($directoryPath, $options = NULL) { + if ($this->isFinalized) { + return false; + } + $defaultOptions = array( + 'timestamp' => NULL, + 'comment' => NULL, + ); + if (is_null($options)) { + $options = array(); + } + $options = array_merge($defaultOptions, $options); + + $directoryPath = self::normalizeFilePath($directoryPath) . '/'; + + if (strlen($directoryPath) > 0) { + $gpFlags = 0x0000; + $gzMethod = COMPR::STORE; // Compression type 0 = stored + + list($gpFlags, $lfhLength) = $this->beginFile($directoryPath, True, $options['comment'], $options['timestamp'], $gpFlags, $gzMethod); + // build cdRec + $this->cdRec[] = $this->buildCentralDirectoryHeader($directoryPath, $options['timestamp'], $gpFlags, $gzMethod, + Count64::construct(0, !$this->zip64), Count64::construct(0, !$this->zip64), 0, $this->extFileAttrDir, True); + + // calc offset + $this->offset->add($lfhLength); + + return true; + } + return false; + } + + /** + * Close the archive. + * A closed archive can no longer have new files added to it. After + * closing, the zip file is completely written to the output stream. + * @return bool $success + */ + public function finalize() { + if (!$this->isFinalized) { + + // print central directory + $cd = implode('', $this->cdRec); + $this->write($cd); + + if ($this->zip64) { + // print the zip64 end of central directory record + $this->write($this->buildZip64EndOfCentralDirectoryRecord(strlen($cd))); + + // print the zip64 end of central directory locator + $this->write($this->buildZip64EndOfCentralDirectoryLocator(strlen($cd))); + } + + // print end of central directory record + $this->write($this->buildEndOfCentralDirectoryRecord(strlen($cd))); + + $this->flush(); + + $this->isFinalized = true; + $cd = null; + $this->cdRec = null; + + return true; + } + return false; + } + + private function validateCompressionOptions($compress, $level) { + if (COMPR::STORE === $compress) { + } else if (COMPR::DEFLATE === $compress) { + if (COMPR::NONE !== $level + && !class_exists(DeflatePeclStream::PECL1_DEFLATE_STREAM_CLASS) + && !class_exists(DeflatePeclStream::PECL2_DEFLATE_STREAM_CLASS)) { + throw new \Exception('unable to use compression method DEFLATE with level other than NONE (requires pecl_http >= 0.10)'); + } + } else { + throw new \Exception('invalid option ' . $compress . ' (compression method)'); + } + + if (!(COMPR::NONE === $level || + COMPR::NORMAL === $level || + COMPR::MAXIMUM === $level || + COMPR::SUPERFAST === $level)) { + throw new \Exception('invalid option ' . $level . ' (compression level'); + } + } + + private function write($data) { + return fwrite($this->outStream, $data); + } + + private function flush() { + return fflush($this->outStream); + } + + private function beginFile($filePath, $isDir, $fileComment, $timestamp, $gpFlags, $gzMethod, + $dataLength = 0, $gzLength = 0, $dataCRC32 = 0) { + + $isFileUTF8 = mb_check_encoding($filePath, 'UTF-8') && !mb_check_encoding($filePath, 'ASCII'); + $isCommentUTF8 = !empty($fileComment) && mb_check_encoding($fileComment, 'UTF-8') + && !mb_check_encoding($fileComment, 'ASCII'); + + if ($isFileUTF8 || $isCommentUTF8) { + $gpFlags |= GPFLAGS::EFS; + } + + $localFileHeader = $this->buildLocalFileHeader($filePath, $timestamp, $gpFlags, $gzMethod, $dataLength, + $gzLength, $isDir, $dataCRC32); + + $this->write($localFileHeader); + + return array($gpFlags, strlen($localFileHeader)); + } + + private function streamFileData($stream, $compress, $level) { + $dataLength = Count64::construct(0, !$this->zip64); + $gzLength = Count64::construct(0, !$this->zip64); + $hashCtx = hash_init('crc32b'); + if (COMPR::DEFLATE === $compress) { + $compStream = DeflateStream::create($level); + } + + while (!feof($stream) && ($data = fread($stream, self::STREAM_CHUNK_SIZE)) !== false) { + $dataLength->add(strlen($data)); + hash_update($hashCtx, $data); + if (COMPR::DEFLATE === $compress) { + $data = $compStream->update($data); + } + $gzLength->add(strlen($data)); + $this->write($data); + + $this->flush(); + } + if (COMPR::DEFLATE === $compress) { + $data = $compStream->finish(); + $gzLength->add(strlen($data)); + $this->write($data); + + $this->flush(); + } + $crc = unpack('N', hash_final($hashCtx, true)); + return array($dataLength, $gzLength, $crc[1]); + } + + private function buildZip64ExtendedInformationField($dataLength = 0, $gzLength = 0) { + return '' + . pack16le(0x0001) // tag for this "extra" block type (ZIP64) 2 bytes (0x0001) + . pack16le(28) // size of this "extra" block 2 bytes + . pack64le($dataLength) // original uncompressed file size 8 bytes + . pack64le($gzLength) // size of compressed data 8 bytes + . pack64le($this->offset) // offset of local header record 8 bytes + . pack32le(0); // number of the disk on which this file starts 4 bytes + } + + private function buildLocalFileHeader($filePath, $timestamp, $gpFlags, + $gzMethod, $dataLength, $gzLength, $isDir = False, $dataCRC32 = 0) { + $versionToExtract = $this->getVersionToExtract($isDir); + $dosTime = self::getDosTime($timestamp); + if ($this->zip64) { + $zip64Ext = $this->buildZip64ExtendedInformationField($dataLength, $gzLength); + $dataLength = -1; + $gzLength = -1; + } else { + $zip64Ext = ''; + } + + return '' + . pack32le(self::ZIP_LOCAL_FILE_HEADER) // local file header signature 4 bytes (0x04034b50) + . pack16le($versionToExtract) // version needed to extract 2 bytes + . pack16le($gpFlags) // general purpose bit flag 2 bytes + . pack16le($gzMethod) // compression method 2 bytes + . pack32le($dosTime) // last mod file time 2 bytes + // last mod file date 2 bytes + . pack32le($dataCRC32) // crc-32 4 bytes + . pack32le($gzLength) // compressed size 4 bytes + . pack32le($dataLength) // uncompressed size 4 bytes + . pack16le(strlen($filePath)) // file name length 2 bytes + . pack16le(strlen($zip64Ext)) // extra field length 2 bytes + . $filePath // file name (variable size) + . $zip64Ext; // extra field (variable size) + } + + private function addDataDescriptor($dataLength, $gzLength, $dataCRC32) { + if ($this->zip64) { + $length = 24; + $packedGzLength = pack64le($gzLength); + $packedDataLength = pack64le($dataLength); + } else { + $length = 16; + $packedGzLength = pack32le($gzLength->getLoBytes()); + $packedDataLength = pack32le($dataLength->getLoBytes()); + } + + $this->write('' + . pack32le(self::ZIP_DATA_DESCRIPTOR_HEADER) // data descriptor header signature 4 bytes (0x08074b50) + . pack32le($dataCRC32) // crc-32 4 bytes + . $packedGzLength // compressed size 4/8 bytes (depending on zip64 enabled) + . $packedDataLength // uncompressed size 4/8 bytes (depending on zip64 enabled) + .''); + return $length; + } + + private function buildZip64EndOfCentralDirectoryRecord($cdRecLength) { + $versionToExtract = $this->getVersionToExtract(False); + $cdRecCount = count($this->cdRec); + + return '' + . pack32le(self::ZIP64_END_OF_CENTRAL_DIRECTORY) // zip64 end of central dir signature 4 bytes (0x06064b50) + . pack64le(44) // size of zip64 end of central directory + // record 8 bytes + . pack16le(self::ATTR_MADE_BY_VERSION) //version made by 2 bytes + . pack16le($versionToExtract) // version needed to extract 2 bytes + . pack32le(0) // number of this disk 4 bytes + . pack32le(0) // number of the disk with the start of the + // central directory 4 bytes + . pack64le($cdRecCount) // total number of entries in the central + // directory on this disk 8 bytes + . pack64le($cdRecCount) // total number of entries in the + // central directory 8 bytes + . pack64le($cdRecLength) // size of the central directory 8 bytes + . pack64le($this->offset) // offset of start of central directory + // with respect to the starting disk number 8 bytes + . ''; // zip64 extensible data sector (variable size) + + } + + private function buildZip64EndOfCentralDirectoryLocator($cdRecLength) { + $zip64RecStart = Count64::construct($this->offset, !$this->zip64)->add($cdRecLength); + + return '' + . pack32le(self::ZIP64_END_OF_CENTRAL_DIR_LOCATOR) // zip64 end of central dir locator signature 4 bytes (0x07064b50) + . pack32le(0) // number of the disk with the start of the + // zip64 end of central directory 4 bytes + . pack64le($zip64RecStart) // relative offset of the zip64 end of + // central directory record 8 bytes + . pack32le(1); // total number of disks 4 bytes + } + + private function buildCentralDirectoryHeader($filePath, $timestamp, $gpFlags, + $gzMethod, $dataLength, $gzLength, $dataCRC32, $extFileAttr, $isDir) { + $versionToExtract = $this->getVersionToExtract($isDir); + $dosTime = self::getDosTime($timestamp); + if ($this->zip64) { + $zip64Ext = $this->buildZip64ExtendedInformationField($dataLength, $gzLength); + $dataLength = -1; + $gzLength = -1; + $diskNo = -1; + $offset = -1; + } else { + $zip64Ext = ''; + $dataLength = $dataLength->getLoBytes(); + $gzLength = $gzLength->getLoBytes(); + $diskNo = 0; + $offset = $this->offset->getLoBytes(); + } + + return '' + . pack32le(self::ZIP_CENTRAL_FILE_HEADER) //central file header signature 4 bytes (0x02014b50) + . pack16le(self::ATTR_MADE_BY_VERSION) //version made by 2 bytes + . pack16le($versionToExtract) // version needed to extract 2 bytes + . pack16le($gpFlags) //general purpose bit flag 2 bytes + . pack16le($gzMethod) //compression method 2 bytes + . pack32le($dosTime) //last mod file time 2 bytes + //last mod file date 2 bytes + . pack32le($dataCRC32) //crc-32 4 bytes + . pack32le($gzLength) //compressed size 4 bytes + . pack32le($dataLength) //uncompressed size 4 bytes + . pack16le(strlen($filePath)) //file name length 2 bytes + . pack16le(strlen($zip64Ext)) //extra field length 2 bytes + . pack16le(0) //file comment length 2 bytes + . pack16le($diskNo) //disk number start 2 bytes + . pack16le(0) //internal file attributes 2 bytes + . pack32le($extFileAttr) //external file attributes 4 bytes + . pack32le($offset) //relative offset of local header 4 bytes + . $filePath //file name (variable size) + . $zip64Ext //extra field (variable size) + //TODO: implement? + . ''; //file comment (variable size) + } + + private function buildEndOfCentralDirectoryRecord($cdRecLength) { + if ($this->zip64) { + $diskNumber = -1; + $cdRecCount = min(count($this->cdRec), 0xffff); + $cdRecLength = -1; + $offset = -1; + } else { + $diskNumber = 0; + $cdRecCount = count($this->cdRec); + $offset = $this->offset->getLoBytes(); + } + //throw new \Exception(sprintf("zip64 %d diskno %d", $this->zip64, $diskNumber)); + + return '' + . pack32le(self::ZIP_END_OF_CENTRAL_DIRECTORY) // end of central dir signature 4 bytes (0x06064b50) + . pack16le($diskNumber) // number of this disk 2 bytes + . pack16le($diskNumber) // number of the disk with the + // start of the central directory 2 bytes + . pack16le($cdRecCount) // total number of entries in the + // central directory on this disk 2 bytes + . pack16le($cdRecCount) // total number of entries in the + // central directory 2 bytes + . pack32le($cdRecLength) // size of the central directory 4 bytes + . pack32le($offset) // offset of start of central + // directory with respect to the + // starting disk number 4 bytes + . pack16le(0) // .ZIP file comment length 2 bytes + //TODO: implement? + . ''; // .ZIP file comment (variable size) + } + + // Utility methods //////////////////////////////////////////////////////// + + private static function normalizeFilePath($filePath) { + return trim(str_replace('\\', '/', $filePath), '/'); + } + + /** + * Calculate the 2 byte dostime used in the zip entries. + * + * @param int $timestamp + * @return 2-byte encoded DOS Date + */ + public static function getDosTime($timestamp = 0) { + $timestamp = (int) $timestamp; + $oldTZ = @date_default_timezone_get(); + date_default_timezone_set('UTC'); + $date = ($timestamp == 0 ? getdate() : getdate($timestamp)); + date_default_timezone_set($oldTZ); + if ($date['year'] >= 1980) { + return (($date['mday'] + ($date['mon'] << 5) + (($date['year'] - 1980) << 9)) << 16) + | (($date['seconds'] >> 1) + ($date['minutes'] << 5) + ($date['hours'] << 11)); + } + return 0x0000; + } +} + +abstract class ExtFileAttr { + + /* + ZIP external file attributes layout + TTTTsstrwxrwxrwx0000000000ADVSHR + ^^^^____________________________ UNIX file type + ^^^_________________________ UNIX setuid, setgid, sticky + ^^^^^^^^^________________ UNIX permissions + ^^^^^^^^________ "lower-middle byte" (TODO: what is this?) + ^^^^^^^^ DOS attributes (reserved, reserved, archived, directory, volume, system, hidden, read-only + */ + + public static function getExtFileAttr($attr) { + return $attr; + } +} + +class UNIX extends ExtFileAttr { + + // Octal + const S_IFIFO = 0010000; /* named pipe (fifo) */ + const S_IFCHR = 0020000; /* character special */ + const S_IFDIR = 0040000; /* directory */ + const S_IFBLK = 0060000; /* block special */ + const S_IFREG = 0100000; /* regular */ + const S_IFLNK = 0120000; /* symbolic link */ + const S_IFSOCK = 0140000; /* socket */ + const S_ISUID = 0004000; /* set user id on execution */ + const S_ISGID = 0002000; /* set group id on execution */ + const S_ISTXT = 0001000; /* sticky bit */ + const S_IRWXU = 0000700; /* RWX mask for owner */ + const S_IRUSR = 0000400; /* R for owner */ + const S_IWUSR = 0000200; /* W for owner */ + const S_IXUSR = 0000100; /* X for owner */ + const S_IRWXG = 0000070; /* RWX mask for group */ + const S_IRGRP = 0000040; /* R for group */ + const S_IWGRP = 0000020; /* W for group */ + const S_IXGRP = 0000010; /* X for group */ + const S_IRWXO = 0000007; /* RWX mask for other */ + const S_IROTH = 0000004; /* R for other */ + const S_IWOTH = 0000002; /* W for other */ + const S_IXOTH = 0000001; /* X for other */ + const S_ISVTX = 0001000; /* save swapped text even after use */ + + public static function getExtFileAttr($attr) { + return parent::getExtFileAttr($attr) << 16; + } +} + +abstract class DeflateStream { + static public function create($level) { + if (COMPR::NONE === $level) { + return new DeflateStoreStream($level); + } else { + return new DeflatePeclStream($level); + } + } + protected function __construct($level) {} + + abstract public function update($data); + abstract public function finish(); +} + +class DeflatePeclStream extends DeflateStream { + private $peclDeflateStream; + + const PECL1_DEFLATE_STREAM_CLASS = '\HttpDeflateStream'; + const PECL2_DEFLATE_STREAM_CLASS = '\http\encoding\Stream\Deflate'; + + protected function __construct($level) { + $class = self::PECL1_DEFLATE_STREAM_CLASS; + if (!class_exists($class)) { + $class = self::PECL2_DEFLATE_STREAM_CLASS; + } + if (!class_exists($class)) { + throw new \Exception('unable to instantiate PECL deflate stream (requires pecl_http >= 0.10)'); + } + + $deflateFlags = constant($class . '::TYPE_RAW'); + switch ($level) { + case COMPR::NORMAL: + $deflateFlags |= constant($class . '::LEVEL_DEF'); + break; + case COMPR::MAXIMUM: + $deflateFlags |= constant($class . '::LEVEL_MAX'); + break; + case COMPR::SUPERFAST: + $deflateFlags |= constant($class . '::LEVEL_MIN'); + break; + } + $this->peclDeflateStream = new $class($deflateFlags); + } + + public function update($data) { + return $this->peclDeflateStream->update($data); + } + + public function finish() { + return $this->peclDeflateStream->finish(); + } +} + +class DeflateStoreStream extends DeflateStream { + const BLOCK_HEADER_NORMAL = 0x00; + const BLOCK_HEADER_FINAL = 0x01; + const BLOCK_HEADER_ERROR = 0x03; + + const MAX_UNCOMPR_BLOCK_SIZE = 0xffff; + + public function update($data) { + $result = ''; + for ($pos = 0, $len = strlen($data); $pos < $len; $pos += self::MAX_UNCOMPR_BLOCK_SIZE) { + $result .= $this->write_block(self::BLOCK_HEADER_NORMAL, substr($data, $pos, self::MAX_UNCOMPR_BLOCK_SIZE)); + } + return $result; + } + + public function finish() { + return $this->write_block(self::BLOCK_HEADER_FINAL, ''); + } + + private function write_block($header, $data) { + return '' + . pack8($header) // block header 3 bits, null padding = 1 byte + . pack16le(strlen($data)) // block data length 2 bytes + . pack16le(0xffff ^ strlen($data)) // complement of block data size 2 bytes + . $data // data + . ''; + } +} + +class DOS extends ExtFileAttr { + + const READ_ONLY = 0x1; + const HIDDEN = 0x2; + const SYSTEM = 0x4; + const VOLUME = 0x8; + const DIR = 0x10; + const ARCHIVE = 0x20; + const RESERVED1 = 0x40; + const RESERVED2 = 0x80; +} + +class GPFLAGS { + const NONE = 0x0000; // no flags set + const COMP1 = 0x0002; // compression flag 1 (compression settings, see APPNOTE for details) + const COMP2 = 0x0004; // compression flag 2 (compression settings, see APPNOTE for details) + const ADD = 0x0008; // ADD flag (sizes and crc32 are append in data descriptor) + const EFS = 0x0800; // EFS flag (UTF-8 encoded filename and/or comment) + + // compression settings for deflate/deflate64 + const DEFL_NORM = 0x0000; // normal compression (COMP1 and COMP2 not set) + const DEFL_MAX = self::COMP1; // maximum compression + const DEFL_FAST = self::COMP2; // fast compression + const DEFL_SFAST = 0x0006; // superfast compression (COMP1 and COMP2 set) +} + diff --git a/3rdparty/deepdiver1975/tarstreamer/LICENSE b/3rdparty/deepdiver1975/tarstreamer/LICENSE new file mode 100644 index 00000000..b96c27fe --- /dev/null +++ b/3rdparty/deepdiver1975/tarstreamer/LICENSE @@ -0,0 +1,20 @@ +Original work Copyright 2007-2009 Paul Duncan +Modified work Copyright 2013 Barracuda Networks, Inc. + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in +all copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +THE SOFTWARE. \ No newline at end of file diff --git a/3rdparty/deepdiver1975/tarstreamer/src/TarHeader.php b/3rdparty/deepdiver1975/tarstreamer/src/TarHeader.php new file mode 100644 index 00000000..b8ff15da --- /dev/null +++ b/3rdparty/deepdiver1975/tarstreamer/src/TarHeader.php @@ -0,0 +1,139 @@ +name = $name; + return $this; + } + + public function setSize($size) { + $this->size = $size; + return $this; + } + + public function setMtime($mtime) { + $this->mtime = $mtime; + return $this; + } + + public function setTypeflag($typeflag) { + $this->typeflag = $typeflag; + return $this; + } + + public function setPrefix($prefix) { + $this->prefix = $prefix; + return $this; + } + + public function getHeader() { + $fields = [ + ['a100', substr($this->name, 0, 100)], + ['a8', str_pad($this->mode, 7, '0', STR_PAD_LEFT)], + ['a8', decoct((int) str_pad($this->uid, 7, '0', STR_PAD_LEFT))], + ['a8', decoct((int) str_pad($this->gid, 7, '0', STR_PAD_LEFT))], + ['a12', str_pad(decoct((int)$this->size), 11, '0', STR_PAD_LEFT)], + ['a12', str_pad(decoct((int)$this->mtime), 11, '0', STR_PAD_LEFT)], + // We calculate checksum later + ['a8', ''], + ['a1', $this->typeflag], + ['a100', $this->linkname], + ['a6', $this->magic], + ['a2', $this->version], + ['a32', $this->uname], + ['a32', $this->gname], + ['a8', $this->devmajor], + ['a8', $this->devminor], + ['a155', substr($this->prefix, 0, 155)], + ['a12', $this->reserved], + ]; + + // pack fields and calculate "total" length + $header = $this->packFields($fields); + + // Compute header checksum + $checksum = str_pad(decoct($this->computeUnsignedChecksum($header)), 6, "0", STR_PAD_LEFT); + for ($i = 0; $i < 6; $i++) { + $header[(148 + $i)] = substr($checksum, $i, 1); + } + $header[154] = \chr(0); + $header[155] = \chr(32); + + return $header; + } + + /** + * Create a format string and argument list for pack(), then call pack() and return the result. + * + * @param array $fields key being the format string and value being the data to pack + * @return string binary packed data returned from pack() + */ + protected function packFields($fields) { + list($fmt, $args) = ['', []]; + + // populate format string and argument list + foreach ($fields as $field) { + $fmt .= $field[0]; + $args[] = $field[1]; + } + + // prepend format string to argument list + array_unshift($args, $fmt); + + // build output string from header and compressed data + return \call_user_func_array('pack', $args); + } + + /** + * Generate unsigned checksum of header + * + * @param string $header + * @return float|int unsigned checksum + */ + protected function computeUnsignedChecksum($header) { + $unsignedChecksum = 0; + for ($i = 0; $i < 512; $i++) { + $unsignedChecksum += \ord($header[$i]); + } + for ($i = 0; $i < 8; $i++) { + $unsignedChecksum -= \ord($header[148 + $i]); + } + $unsignedChecksum += \ord(" ") * 8; + + return $unsignedChecksum; + } +} diff --git a/3rdparty/deepdiver1975/tarstreamer/src/TarStreamer.php b/3rdparty/deepdiver1975/tarstreamer/src/TarStreamer.php new file mode 100644 index 00000000..8bbf54a3 --- /dev/null +++ b/3rdparty/deepdiver1975/tarstreamer/src/TarStreamer.php @@ -0,0 +1,267 @@ +outStream = $options['outstream']; + } else { + $this->outStream = fopen('php://output', 'w'); + // turn off output buffering + while (ob_get_level() > 0) { + ob_end_flush(); + } + } + if (isset($options['longnames'])) { + $this->longNameHeaderType = $options['longnames']; + } else { + $this->longNameHeaderType = self::LONGNAMETYPE; + } + } + + /** + * Send appropriate http headers before streaming the tar file and disable output buffering. + * This method, if used, has to be called before adding anything to the tar file. + * + * @param string $archiveName Filename of archive to be created (optional, default 'archive.tar') + * @param string $contentType Content mime type to be set (optional, default 'application/x-tar') + * @throws \Exception + */ + public function sendHeaders($archiveName = 'archive.tar', $contentType = 'application/x-tar') { + $encodedArchiveName = rawurlencode($archiveName); + if (headers_sent($headerFile, $headerLine)) { + throw new \Exception("Unable to send file $encodedArchiveName. HTML Headers have already been sent from $headerFile in line $headerLine"); + } + $buffer = ob_get_contents(); + if (!empty($buffer)) { + throw new \Exception("Unable to send file $encodedArchiveName. Output buffer already contains text (typically warnings or errors)."); + } + + $headers = [ + 'Pragma' => 'public', + 'Last-Modified' => gmdate('D, d M Y H:i:s T'), + 'Expires' => '0', + 'Accept-Ranges' => 'bytes', + 'Connection' => 'Keep-Alive', + 'Content-Type' => $contentType, + 'Cache-Control' => 'public, must-revalidate', + 'Content-Transfer-Encoding' => 'binary', + ]; + + // Use UTF-8 filenames when not using Internet Explorer + if (strpos($_SERVER['HTTP_USER_AGENT'], 'MSIE') > 0) { + header('Content-Disposition: attachment; filename="' . rawurlencode($archiveName) . '"'); + } else { + header('Content-Disposition: attachment; filename*=UTF-8\'\'' . rawurlencode($archiveName) + . '; filename="' . rawurlencode($archiveName) . '"'); + } + + foreach ($headers as $key => $value) { + header("$key: $value"); + } + } + + /** + * Add a file to the archive at the specified location and file name. + * + * @param resource $stream Stream to read data from + * @param string $filePath Filepath and name to be used in the archive. + * @param int $size + * @param array $options Optional, additional options + * Valid options are: + * * int timestamp: timestamp for the file (default: current time) + * @return bool $success + */ + public function addFileFromStream($stream, $filePath, $size, $options = []) { + if (!\is_resource($stream) || get_resource_type($stream) != 'stream') { + return false; + } + + $this->initFileStreamTransfer($filePath, self::REGTYPE, $size, $options); + + // send file blocks + while ($data = fread($stream, $this->blockSize)) { + // send data + $this->streamFilePart($data); + } + + // complete the file stream + $this->completeFileStream($size); + + return true; + } + + /** + * Explicitly adds a directory to the tar (necessary for empty directories) + * + * @param string $name Name (path) of the directory + * @param array $opt Additional options to set + * Valid options are: + * * int timestamp: timestamp for the file (default: current time) + * @return void + */ + public function addEmptyDir($name, $opt = []) { + $opt['type'] = self::DIRTYPE; + + // send header + $this->initFileStreamTransfer($name, self::DIRTYPE, 0, $opt); + + // complete the file stream + $this->completeFileStream(0); + } + + /** + * Close the archive. + * A closed archive can no longer have new files added to it. After + * closing, the file is completely written to the output stream. + * @return bool $success */ + public function finalize() { + // tar requires the end of the file have two 512 byte null blocks + $this->send(pack('a1024', '')); + + // flush the data to the output + fflush($this->outStream); + return true; + } + + /** + * Initialize a file stream + * + * @param string $name file path or just name + * @param int|string $type type of the item + * @param int $size size in bytes of the file + * @param array $opt array (optional) + * Valid options are: + * * int timestamp: timestamp for the file (default: current time) + */ + protected function initFileStreamTransfer($name, $type, $size, $opt = []) { + $dirName = (\dirname($name) == '.') ? '' : \dirname($name); + $fileName = ($type == self::DIRTYPE) ? basename($name) . '/' : basename($name); + + // handle long file names + if (\strlen($fileName) > 99 || \strlen($dirName) > 154) { + $this->writeLongName($fileName, $dirName); + } + + // process optional arguments + $time = isset($opt['timestamp']) ? $opt['timestamp'] : time(); + + $tarHeader = new TarHeader(); + $header = $tarHeader->setName($fileName) + ->setSize($size) + ->setMtime($time) + ->setTypeflag($type) + ->setPrefix($dirName) + ->getHeader() + ; + // print header + $this->send($header); + } + + protected function writeLongName($fileName, $dirName) { + $internalPath = trim($dirName . '/' . $fileName, '/'); + if ($this->longNameHeaderType === self::XHDTYPE) { + // Long names via PAX + $pax = $this->paxGenerate([ 'path' => $internalPath]); + $paxSize = \strlen($pax); + $this->initFileStreamTransfer('', self::XHDTYPE, $paxSize); + $this->streamFilePart($pax); + $this->completeFileStream($paxSize); + } else { + // long names via 'L' header + $pathSize = \strlen($internalPath); + $tarHeader = new TarHeader(); + $header = $tarHeader->setName('././@LongLink') + ->setSize($pathSize) + ->setTypeflag(self::LONGNAMETYPE) + ->getHeader() + ; + $this->send($header); + $this->streamFilePart($internalPath); + $this->completeFileStream($pathSize); + } + } + + /** + * Stream the next part of the current file stream. + * + * @param string $data raw data to send + */ + protected function streamFilePart($data) { + // send data + $this->send($data); + + // flush the data to the output + fflush($this->outStream); + } + + /** + * Complete the current file stream + * @param $size + */ + protected function completeFileStream($size) { + // ensure we pad the last block so that it is 512 bytes + if (($mod = ($size % 512)) > 0) { + $this->send(pack('a' . (512 - $mod), '')); + } + + // flush the data to the output + fflush($this->outStream); + } + + /** + * Send string, sending HTTP headers if necessary. + * + * @param string $data data to send + */ + protected function send($data) { + if ($this->needHeaders) { + $this->sendHeaders(); + } + $this->needHeaders = false; + + fwrite($this->outStream, $data); + } + + /** + * Generate a PAX string + * + * @param array $fields key value mapping + * @return string PAX formatted string + * @link http://www.freebsd.org/cgi/man.cgi?query=tar&sektion=5&manpath=FreeBSD+8-current tar / PAX spec + */ + protected function paxGenerate($fields) { + $lines = ''; + foreach ($fields as $name => $value) { + // build the line and the size + $line = ' ' . $name . '=' . $value . "\n"; + $size = \strlen((string) \strlen($line)) + \strlen($line); + + // add the line + $lines .= $size . $line; + } + + return $lines; + } +} diff --git a/3rdparty/doctrine/dbal/LICENSE b/3rdparty/doctrine/dbal/LICENSE new file mode 100644 index 00000000..e8fdec4a --- /dev/null +++ b/3rdparty/doctrine/dbal/LICENSE @@ -0,0 +1,19 @@ +Copyright (c) 2006-2018 Doctrine Project + +Permission is hereby granted, free of charge, to any person obtaining a copy of +this software and associated documentation files (the "Software"), to deal in +the Software without restriction, including without limitation the rights to +use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies +of the Software, and to permit persons to whom the Software is furnished to do +so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. diff --git a/3rdparty/doctrine/dbal/src/ArrayParameterType.php b/3rdparty/doctrine/dbal/src/ArrayParameterType.php new file mode 100644 index 00000000..d78cd5f1 --- /dev/null +++ b/3rdparty/doctrine/dbal/src/ArrayParameterType.php @@ -0,0 +1,42 @@ +> */ + private array $data; + + private int $columnCount = 0; + private int $num = 0; + + /** @param list> $data */ + public function __construct(array $data) + { + $this->data = $data; + if (count($data) === 0) { + return; + } + + $this->columnCount = count($data[0]); + } + + /** + * {@inheritDoc} + */ + public function fetchNumeric() + { + $row = $this->fetch(); + + if ($row === false) { + return false; + } + + return array_values($row); + } + + /** + * {@inheritDoc} + */ + public function fetchAssociative() + { + return $this->fetch(); + } + + /** + * {@inheritDoc} + */ + public function fetchOne() + { + $row = $this->fetch(); + + if ($row === false) { + return false; + } + + return reset($row); + } + + /** + * {@inheritDoc} + */ + public function fetchAllNumeric(): array + { + return FetchUtils::fetchAllNumeric($this); + } + + /** + * {@inheritDoc} + */ + public function fetchAllAssociative(): array + { + return FetchUtils::fetchAllAssociative($this); + } + + /** + * {@inheritDoc} + */ + public function fetchFirstColumn(): array + { + return FetchUtils::fetchFirstColumn($this); + } + + public function rowCount(): int + { + return count($this->data); + } + + public function columnCount(): int + { + return $this->columnCount; + } + + public function free(): void + { + $this->data = []; + } + + /** @return array|false */ + private function fetch() + { + if (! isset($this->data[$this->num])) { + return false; + } + + return $this->data[$this->num++]; + } +} diff --git a/3rdparty/doctrine/dbal/src/Cache/CacheException.php b/3rdparty/doctrine/dbal/src/Cache/CacheException.php new file mode 100644 index 00000000..fd10fd5d --- /dev/null +++ b/3rdparty/doctrine/dbal/src/Cache/CacheException.php @@ -0,0 +1,20 @@ +lifetime = $lifetime; + $this->cacheKey = $cacheKey; + if ($resultCache instanceof CacheItemPoolInterface) { + $this->resultCache = $resultCache; + } elseif ($resultCache instanceof Cache) { + Deprecation::trigger( + 'doctrine/dbal', + 'https://github.com/doctrine/dbal/pull/4620', + 'Passing an instance of %s to %s as $resultCache is deprecated. Pass an instance of %s instead.', + Cache::class, + __METHOD__, + CacheItemPoolInterface::class, + ); + + $this->resultCache = CacheAdapter::wrap($resultCache); + } elseif ($resultCache !== null) { + throw new TypeError(sprintf( + '$resultCache: Expected either null or an instance of %s or %s, got %s.', + CacheItemPoolInterface::class, + Cache::class, + get_class($resultCache), + )); + } + } + + public function getResultCache(): ?CacheItemPoolInterface + { + return $this->resultCache; + } + + /** + * @deprecated Use {@see getResultCache()} instead. + * + * @return Cache|null + */ + public function getResultCacheDriver() + { + Deprecation::trigger( + 'doctrine/dbal', + 'https://github.com/doctrine/dbal/pull/4620', + '%s is deprecated, call getResultCache() instead.', + __METHOD__, + ); + + if ($this->resultCache === null) { + return null; + } + + if (! class_exists(DoctrineProvider::class)) { + throw new RuntimeException(sprintf( + 'Calling %s() is not supported if the doctrine/cache package is not installed. ' + . 'Try running "composer require doctrine/cache" or migrate cache access to PSR-6.', + __METHOD__, + )); + } + + return DoctrineProvider::wrap($this->resultCache); + } + + /** @return int */ + public function getLifetime() + { + return $this->lifetime; + } + + /** + * @return string + * + * @throws CacheException + */ + public function getCacheKey() + { + if ($this->cacheKey === null) { + throw CacheException::noCacheKey(); + } + + return $this->cacheKey; + } + + /** + * Generates the real cache key from query, params, types and connection parameters. + * + * @param string $sql + * @param list|array $params + * @param array|array $types + * @param array $connectionParams + * + * @return array{string, string} + */ + public function generateCacheKeys($sql, $params, $types, array $connectionParams = []) + { + if (isset($connectionParams['password'])) { + unset($connectionParams['password']); + } + + $realCacheKey = 'query=' . $sql . + '¶ms=' . serialize($params) . + '&types=' . serialize($types) . + '&connectionParams=' . hash('sha256', serialize($connectionParams)); + + // should the key be automatically generated using the inputs or is the cache key set? + $cacheKey = $this->cacheKey ?? sha1($realCacheKey); + + return [$cacheKey, $realCacheKey]; + } + + public function setResultCache(CacheItemPoolInterface $cache): QueryCacheProfile + { + return new QueryCacheProfile($this->lifetime, $this->cacheKey, $cache); + } + + /** + * @deprecated Use {@see setResultCache()} instead. + * + * @return QueryCacheProfile + */ + public function setResultCacheDriver(Cache $cache) + { + Deprecation::trigger( + 'doctrine/dbal', + 'https://github.com/doctrine/dbal/pull/4620', + '%s is deprecated, call setResultCache() instead.', + __METHOD__, + ); + + return new QueryCacheProfile($this->lifetime, $this->cacheKey, CacheAdapter::wrap($cache)); + } + + /** + * @param string|null $cacheKey + * + * @return QueryCacheProfile + */ + public function setCacheKey($cacheKey) + { + return new QueryCacheProfile($this->lifetime, $cacheKey, $this->resultCache); + } + + /** + * @param int $lifetime + * + * @return QueryCacheProfile + */ + public function setLifetime($lifetime) + { + return new QueryCacheProfile($lifetime, $this->cacheKey, $this->resultCache); + } +} diff --git a/3rdparty/doctrine/dbal/src/ColumnCase.php b/3rdparty/doctrine/dbal/src/ColumnCase.php new file mode 100644 index 00000000..cb0dd409 --- /dev/null +++ b/3rdparty/doctrine/dbal/src/ColumnCase.php @@ -0,0 +1,28 @@ +schemaAssetsFilter = static function (): bool { + return true; + }; + } + + /** + * Sets the SQL logger to use. Defaults to NULL which means SQL logging is disabled. + * + * @deprecated Use {@see setMiddlewares()} and {@see \Doctrine\DBAL\Logging\Middleware} instead. + */ + public function setSQLLogger(?SQLLogger $logger = null): void + { + Deprecation::trigger( + 'doctrine/dbal', + 'https://github.com/doctrine/dbal/pull/4967', + '%s is deprecated, use setMiddlewares() and Logging\\Middleware instead.', + __METHOD__, + ); + + $this->sqlLogger = $logger; + } + + /** + * Gets the SQL logger that is used. + * + * @deprecated + */ + public function getSQLLogger(): ?SQLLogger + { + Deprecation::triggerIfCalledFromOutside( + 'doctrine/dbal', + 'https://github.com/doctrine/dbal/pull/4967', + '%s is deprecated.', + __METHOD__, + ); + + return $this->sqlLogger; + } + + /** + * Gets the cache driver implementation that is used for query result caching. + */ + public function getResultCache(): ?CacheItemPoolInterface + { + return $this->resultCache; + } + + /** + * Gets the cache driver implementation that is used for query result caching. + * + * @deprecated Use {@see getResultCache()} instead. + */ + public function getResultCacheImpl(): ?Cache + { + Deprecation::trigger( + 'doctrine/dbal', + 'https://github.com/doctrine/dbal/pull/4620', + '%s is deprecated, call getResultCache() instead.', + __METHOD__, + ); + + if ($this->resultCache !== null && ! interface_exists(Cache::class)) { + throw new RuntimeException(sprintf( + 'Calling %s() is not supported if the doctrine/cache package is not installed. ' + . 'Try running "composer require doctrine/cache" or migrate cache access to PSR-6.', + __METHOD__, + )); + } + + return $this->resultCacheImpl; + } + + /** + * Sets the cache driver implementation that is used for query result caching. + */ + public function setResultCache(CacheItemPoolInterface $cache): void + { + if (class_exists(DoctrineProvider::class)) { + $this->resultCacheImpl = DoctrineProvider::wrap($cache); + } + + $this->resultCache = $cache; + } + + /** + * Sets the cache driver implementation that is used for query result caching. + * + * @deprecated Use {@see setResultCache()} instead. + */ + public function setResultCacheImpl(Cache $cacheImpl): void + { + Deprecation::trigger( + 'doctrine/dbal', + 'https://github.com/doctrine/dbal/pull/4620', + '%s is deprecated, call setResultCache() instead.', + __METHOD__, + ); + + $this->resultCacheImpl = $cacheImpl; + $this->resultCache = CacheAdapter::wrap($cacheImpl); + } + + /** + * Sets the callable to use to filter schema assets. + */ + public function setSchemaAssetsFilter(?callable $callable = null): void + { + if (func_num_args() < 1) { + Deprecation::trigger( + 'doctrine/dbal', + 'https://github.com/doctrine/dbal/pull/5483', + 'Not passing an argument to %s is deprecated.', + __METHOD__, + ); + } elseif ($callable === null) { + Deprecation::trigger( + 'doctrine/dbal', + 'https://github.com/doctrine/dbal/pull/5483', + 'Using NULL as a schema asset filter is deprecated.' + . ' Use a callable that always returns true instead.', + ); + } + + $this->schemaAssetsFilter = $callable; + } + + /** + * Returns the callable to use to filter schema assets. + */ + public function getSchemaAssetsFilter(): ?callable + { + return $this->schemaAssetsFilter; + } + + /** + * Sets the default auto-commit mode for connections. + * + * If a connection is in auto-commit mode, then all its SQL statements will be executed and committed as individual + * transactions. Otherwise, its SQL statements are grouped into transactions that are terminated by a call to either + * the method commit or the method rollback. By default, new connections are in auto-commit mode. + * + * @see getAutoCommit + * + * @param bool $autoCommit True to enable auto-commit mode; false to disable it + */ + public function setAutoCommit(bool $autoCommit): void + { + $this->autoCommit = $autoCommit; + } + + /** + * Returns the default auto-commit mode for connections. + * + * @see setAutoCommit + * + * @return bool True if auto-commit mode is enabled by default for connections, false otherwise. + */ + public function getAutoCommit(): bool + { + return $this->autoCommit; + } + + /** + * @param Middleware[] $middlewares + * + * @return $this + */ + public function setMiddlewares(array $middlewares): self + { + $this->middlewares = $middlewares; + + return $this; + } + + /** @return Middleware[] */ + public function getMiddlewares(): array + { + return $this->middlewares; + } + + public function getSchemaManagerFactory(): ?SchemaManagerFactory + { + return $this->schemaManagerFactory; + } + + /** @return $this */ + public function setSchemaManagerFactory(SchemaManagerFactory $schemaManagerFactory): self + { + $this->schemaManagerFactory = $schemaManagerFactory; + + return $this; + } + + public function getDisableTypeComments(): bool + { + return $this->disableTypeComments; + } + + /** @return $this */ + public function setDisableTypeComments(bool $disableTypeComments): self + { + $this->disableTypeComments = $disableTypeComments; + + return $this; + } +} diff --git a/3rdparty/doctrine/dbal/src/Connection.php b/3rdparty/doctrine/dbal/src/Connection.php new file mode 100644 index 00000000..2902429f --- /dev/null +++ b/3rdparty/doctrine/dbal/src/Connection.php @@ -0,0 +1,2038 @@ + + * @phpstan-var Params + */ + private array $params; + + /** + * The database platform object used by the connection or NULL before it's initialized. + */ + private ?AbstractPlatform $platform = null; + + private ?ExceptionConverter $exceptionConverter = null; + private ?Parser $parser = null; + + /** + * The schema manager. + * + * @deprecated Use {@see createSchemaManager()} instead. + * + * @var AbstractSchemaManager|null + */ + protected $_schemaManager; + + /** + * The used DBAL driver. + * + * @var Driver + */ + protected $_driver; + + /** + * Flag that indicates whether the current transaction is marked for rollback only. + */ + private bool $isRollbackOnly = false; + + private SchemaManagerFactory $schemaManagerFactory; + + /** + * Initializes a new instance of the Connection class. + * + * @internal The connection can be only instantiated by the driver manager. + * + * @param array $params The connection parameters. + * @param Driver $driver The driver to use. + * @param Configuration|null $config The configuration, optional. + * @param EventManager|null $eventManager The event manager, optional. + * @phpstan-param Params $params + * + * @throws Exception + */ + public function __construct( + #[SensitiveParameter] + array $params, + Driver $driver, + ?Configuration $config = null, + ?EventManager $eventManager = null + ) { + $this->_driver = $driver; + $this->params = $params; + + // Create default config and event manager if none given + $config ??= new Configuration(); + $eventManager ??= new EventManager(); + + $this->_config = $config; + $this->_eventManager = $eventManager; + + if (isset($params['platform'])) { + if (! $params['platform'] instanceof Platforms\AbstractPlatform) { + throw Exception::invalidPlatformType($params['platform']); + } + + Deprecation::trigger( + 'doctrine/dbal', + 'https://github.com/doctrine/dbal/pull/5699', + 'The "platform" connection parameter is deprecated.' + . ' Use a driver middleware that would instantiate the platform instead.', + ); + + $this->platform = $params['platform']; + $this->platform->setEventManager($this->_eventManager); + $this->platform->setDisableTypeComments($config->getDisableTypeComments()); + } + + $this->_expr = $this->createExpressionBuilder(); + + $this->autoCommit = $config->getAutoCommit(); + + $schemaManagerFactory = $config->getSchemaManagerFactory(); + if ($schemaManagerFactory === null) { + Deprecation::trigger( + 'doctrine/dbal', + 'https://github.com/doctrine/dbal/issues/5812', + 'Not configuring a schema manager factory is deprecated.' + . ' Use %s which is going to be the default in DBAL 4.', + DefaultSchemaManagerFactory::class, + ); + + $schemaManagerFactory = new LegacySchemaManagerFactory(); + } + + $this->schemaManagerFactory = $schemaManagerFactory; + } + + /** + * Gets the parameters used during instantiation. + * + * @internal + * + * @return array + * @phpstan-return Params + */ + public function getParams() + { + return $this->params; + } + + /** + * Gets the name of the currently selected database. + * + * @return string|null The name of the database or NULL if a database is not selected. + * The platforms which don't support the concept of a database (e.g. embedded databases) + * must always return a string as an indicator of an implicitly selected database. + * + * @throws Exception + */ + public function getDatabase() + { + $platform = $this->getDatabasePlatform(); + $query = $platform->getDummySelectSQL($platform->getCurrentDatabaseExpression()); + $database = $this->fetchOne($query); + + assert(is_string($database) || $database === null); + + return $database; + } + + /** + * Gets the DBAL driver instance. + * + * @return Driver + */ + public function getDriver() + { + return $this->_driver; + } + + /** + * Gets the Configuration used by the Connection. + * + * @return Configuration + */ + public function getConfiguration() + { + return $this->_config; + } + + /** + * Gets the EventManager used by the Connection. + * + * @deprecated + * + * @return EventManager + */ + public function getEventManager() + { + Deprecation::triggerIfCalledFromOutside( + 'doctrine/dbal', + 'https://github.com/doctrine/dbal/issues/5784', + '%s is deprecated.', + __METHOD__, + ); + + return $this->_eventManager; + } + + /** + * Gets the DatabasePlatform for the connection. + * + * @return AbstractPlatform + * + * @throws Exception + */ + public function getDatabasePlatform() + { + if ($this->platform === null) { + $this->platform = $this->detectDatabasePlatform(); + $this->platform->setEventManager($this->_eventManager); + $this->platform->setDisableTypeComments($this->_config->getDisableTypeComments()); + } + + return $this->platform; + } + + /** + * Creates an expression builder for the connection. + */ + public function createExpressionBuilder(): ExpressionBuilder + { + return new ExpressionBuilder($this); + } + + /** + * Gets the ExpressionBuilder for the connection. + * + * @deprecated Use {@see createExpressionBuilder()} instead. + * + * @return ExpressionBuilder + */ + public function getExpressionBuilder() + { + Deprecation::triggerIfCalledFromOutside( + 'doctrine/dbal', + 'https://github.com/doctrine/dbal/issues/4515', + 'Connection::getExpressionBuilder() is deprecated,' + . ' use Connection::createExpressionBuilder() instead.', + ); + + return $this->_expr; + } + + /** + * Establishes the connection with the database. + * + * @internal This method will be made protected in DBAL 4.0. + * + * @return bool TRUE if the connection was successfully established, FALSE if + * the connection is already open. + * + * @throws Exception + * + * @phpstan-assert !null $this->_conn + */ + public function connect() + { + Deprecation::triggerIfCalledFromOutside( + 'doctrine/dbal', + 'https://github.com/doctrine/dbal/issues/4966', + 'Public access to Connection::connect() is deprecated.', + ); + + if ($this->_conn !== null) { + return false; + } + + try { + $this->_conn = $this->_driver->connect($this->params); + } catch (Driver\Exception $e) { + throw $this->convertException($e); + } + + if ($this->autoCommit === false) { + $this->beginTransaction(); + } + + if ($this->_eventManager->hasListeners(Events::postConnect)) { + Deprecation::trigger( + 'doctrine/dbal', + 'https://github.com/doctrine/dbal/issues/5784', + 'Subscribing to %s events is deprecated. Implement a middleware instead.', + Events::postConnect, + ); + + $eventArgs = new Event\ConnectionEventArgs($this); + $this->_eventManager->dispatchEvent(Events::postConnect, $eventArgs); + } + + return true; + } + + /** + * Detects and sets the database platform. + * + * Evaluates custom platform class and version in order to set the correct platform. + * + * @throws Exception If an invalid platform was specified for this connection. + */ + private function detectDatabasePlatform(): AbstractPlatform + { + $version = $this->getDatabasePlatformVersion(); + + if ($version !== null) { + assert($this->_driver instanceof VersionAwarePlatformDriver); + + return $this->_driver->createDatabasePlatformForVersion($version); + } + + return $this->_driver->getDatabasePlatform(); + } + + /** + * Returns the version of the related platform if applicable. + * + * Returns null if either the driver is not capable to create version + * specific platform instances, no explicit server version was specified + * or the underlying driver connection cannot determine the platform + * version without having to query it (performance reasons). + * + * @return string|null + * + * @throws Throwable + */ + private function getDatabasePlatformVersion() + { + // Driver does not support version specific platforms. + if (! $this->_driver instanceof VersionAwarePlatformDriver) { + return null; + } + + // Explicit platform version requested (supersedes auto-detection). + if (isset($this->params['serverVersion'])) { + return $this->params['serverVersion']; + } + + if (isset($this->params['primary']) && isset($this->params['primary']['serverVersion'])) { + return $this->params['primary']['serverVersion']; + } + + // If not connected, we need to connect now to determine the platform version. + if ($this->_conn === null) { + try { + $this->connect(); + } catch (Exception $originalException) { + if (! isset($this->params['dbname'])) { + throw $originalException; + } + + Deprecation::trigger( + 'doctrine/dbal', + 'https://github.com/doctrine/dbal/pull/5707', + 'Relying on a fallback connection used to determine the database platform while connecting' + . ' to a non-existing database is deprecated. Either use an existing database name in' + . ' connection parameters or omit the database name if the platform' + . ' and the server configuration allow that.', + ); + + // The database to connect to might not yet exist. + // Retry detection without database name connection parameter. + $params = $this->params; + + unset($this->params['dbname']); + + try { + $this->connect(); + } catch (Exception $fallbackException) { + // Either the platform does not support database-less connections + // or something else went wrong. + throw $originalException; + } finally { + $this->params = $params; + } + + $serverVersion = $this->getServerVersion(); + + // Close "temporary" connection to allow connecting to the real database again. + $this->close(); + + return $serverVersion; + } + } + + return $this->getServerVersion(); + } + + /** + * Returns the database server version if the underlying driver supports it. + * + * @return string|null + * + * @throws Exception + */ + private function getServerVersion() + { + $connection = $this->getWrappedConnection(); + + // Automatic platform version detection. + if ($connection instanceof ServerInfoAwareConnection) { + try { + return $connection->getServerVersion(); + } catch (Driver\Exception $e) { + throw $this->convertException($e); + } + } + + Deprecation::trigger( + 'doctrine/dbal', + 'https://github.com/doctrine/dbal/pull/4750', + 'Not implementing the ServerInfoAwareConnection interface in %s is deprecated', + get_class($connection), + ); + + // Unable to detect platform version. + return null; + } + + /** + * Returns the current auto-commit mode for this connection. + * + * @see setAutoCommit + * + * @return bool True if auto-commit mode is currently enabled for this connection, false otherwise. + */ + public function isAutoCommit() + { + return $this->autoCommit === true; + } + + /** + * Sets auto-commit mode for this connection. + * + * If a connection is in auto-commit mode, then all its SQL statements will be executed and committed as individual + * transactions. Otherwise, its SQL statements are grouped into transactions that are terminated by a call to either + * the method commit or the method rollback. By default, new connections are in auto-commit mode. + * + * NOTE: If this method is called during a transaction and the auto-commit mode is changed, the transaction is + * committed. If this method is called and the auto-commit mode is not changed, the call is a no-op. + * + * @see isAutoCommit + * + * @param bool $autoCommit True to enable auto-commit mode; false to disable it. + * + * @return void + */ + public function setAutoCommit($autoCommit) + { + $autoCommit = (bool) $autoCommit; + + // Mode not changed, no-op. + if ($autoCommit === $this->autoCommit) { + return; + } + + $this->autoCommit = $autoCommit; + + // Commit all currently active transactions if any when switching auto-commit mode. + if ($this->_conn === null || $this->transactionNestingLevel === 0) { + return; + } + + $this->commitAll(); + } + + /** + * Prepares and executes an SQL query and returns the first row of the result + * as an associative array. + * + * @param string $query SQL query + * @param list|array $params Query parameters + * @param array|array $types Parameter types + * + * @return array|false False is returned if no rows are found. + * + * @throws Exception + */ + public function fetchAssociative(string $query, array $params = [], array $types = []) + { + return $this->executeQuery($query, $params, $types)->fetchAssociative(); + } + + /** + * Prepares and executes an SQL query and returns the first row of the result + * as a numerically indexed array. + * + * @param string $query SQL query + * @param list|array $params Query parameters + * @param array|array $types Parameter types + * + * @return list|false False is returned if no rows are found. + * + * @throws Exception + */ + public function fetchNumeric(string $query, array $params = [], array $types = []) + { + return $this->executeQuery($query, $params, $types)->fetchNumeric(); + } + + /** + * Prepares and executes an SQL query and returns the value of a single column + * of the first row of the result. + * + * @param string $query SQL query + * @param list|array $params Query parameters + * @param array|array $types Parameter types + * + * @return mixed|false False is returned if no rows are found. + * + * @throws Exception + */ + public function fetchOne(string $query, array $params = [], array $types = []) + { + return $this->executeQuery($query, $params, $types)->fetchOne(); + } + + /** + * Whether an actual connection to the database is established. + * + * @return bool + */ + public function isConnected() + { + return $this->_conn !== null; + } + + /** + * Checks whether a transaction is currently active. + * + * @return bool TRUE if a transaction is currently active, FALSE otherwise. + */ + public function isTransactionActive() + { + return $this->transactionNestingLevel > 0; + } + + /** + * Adds condition based on the criteria to the query components + * + * @param array $criteria Map of key columns to their values + * @param string[] $columns Column names + * @param mixed[] $values Column values + * @param string[] $conditions Key conditions + * + * @throws Exception + */ + private function addCriteriaCondition( + array $criteria, + array &$columns, + array &$values, + array &$conditions + ): void { + $platform = $this->getDatabasePlatform(); + + foreach ($criteria as $columnName => $value) { + if ($value === null) { + $conditions[] = $platform->getIsNullExpression($columnName); + continue; + } + + $columns[] = $columnName; + $values[] = $value; + $conditions[] = $columnName . ' = ?'; + } + } + + /** + * Executes an SQL DELETE statement on a table. + * + * Table expression and columns are not escaped and are not safe for user-input. + * + * @param string $table Table name + * @param array $criteria Deletion criteria + * @param array|array $types Parameter types + * + * @return int|string The number of affected rows. + * + * @throws Exception + */ + public function delete($table, array $criteria, array $types = []) + { + if (count($criteria) === 0) { + throw InvalidArgumentException::fromEmptyCriteria(); + } + + $columns = $values = $conditions = []; + + $this->addCriteriaCondition($criteria, $columns, $values, $conditions); + + return $this->executeStatement( + 'DELETE FROM ' . $table . ' WHERE ' . implode(' AND ', $conditions), + $values, + is_string(key($types)) ? $this->extractTypeValues($columns, $types) : $types, + ); + } + + /** + * Closes the connection. + * + * @return void + */ + public function close() + { + $this->_conn = null; + $this->transactionNestingLevel = 0; + } + + /** + * Sets the transaction isolation level. + * + * @param TransactionIsolationLevel::* $level The level to set. + * + * @return int|string + * + * @throws Exception + */ + public function setTransactionIsolation($level) + { + $this->transactionIsolationLevel = $level; + + return $this->executeStatement($this->getDatabasePlatform()->getSetTransactionIsolationSQL($level)); + } + + /** + * Gets the currently active transaction isolation level. + * + * @return TransactionIsolationLevel::* The current transaction isolation level. + * + * @throws Exception + */ + public function getTransactionIsolation() + { + return $this->transactionIsolationLevel ??= $this->getDatabasePlatform()->getDefaultTransactionIsolationLevel(); + } + + /** + * Executes an SQL UPDATE statement on a table. + * + * Table expression and columns are not escaped and are not safe for user-input. + * + * @param string $table Table name + * @param array $data Column-value pairs + * @param array $criteria Update criteria + * @param array|array $types Parameter types + * + * @return int|string The number of affected rows. + * + * @throws Exception + */ + public function update($table, array $data, array $criteria, array $types = []) + { + $columns = $values = $conditions = $set = []; + + foreach ($data as $columnName => $value) { + $columns[] = $columnName; + $values[] = $value; + $set[] = $columnName . ' = ?'; + } + + $this->addCriteriaCondition($criteria, $columns, $values, $conditions); + + if (is_string(key($types))) { + $types = $this->extractTypeValues($columns, $types); + } + + $sql = 'UPDATE ' . $table . ' SET ' . implode(', ', $set) + . ' WHERE ' . implode(' AND ', $conditions); + + return $this->executeStatement($sql, $values, $types); + } + + /** + * Inserts a table row with specified data. + * + * Table expression and columns are not escaped and are not safe for user-input. + * + * @param string $table Table name + * @param array $data Column-value pairs + * @param array|array $types Parameter types + * + * @return int|string The number of affected rows. + * + * @throws Exception + */ + public function insert($table, array $data, array $types = []) + { + if (count($data) === 0) { + return $this->executeStatement('INSERT INTO ' . $table . ' () VALUES ()'); + } + + $columns = []; + $values = []; + $set = []; + + foreach ($data as $columnName => $value) { + $columns[] = $columnName; + $values[] = $value; + $set[] = '?'; + } + + return $this->executeStatement( + 'INSERT INTO ' . $table . ' (' . implode(', ', $columns) . ')' . + ' VALUES (' . implode(', ', $set) . ')', + $values, + is_string(key($types)) ? $this->extractTypeValues($columns, $types) : $types, + ); + } + + /** + * Extract ordered type list from an ordered column list and type map. + * + * @param array $columnList + * @param array|array $types + * + * @return array|array + */ + private function extractTypeValues(array $columnList, array $types): array + { + $typeValues = []; + + foreach ($columnList as $columnName) { + $typeValues[] = $types[$columnName] ?? ParameterType::STRING; + } + + return $typeValues; + } + + /** + * Quotes a string so it can be safely used as a table or column name, even if + * it is a reserved name. + * + * Delimiting style depends on the underlying database platform that is being used. + * + * NOTE: Just because you CAN use quoted identifiers does not mean + * you SHOULD use them. In general, they end up causing way more + * problems than they solve. + * + * @param string $str The name to be quoted. + * + * @return string The quoted name. + */ + public function quoteIdentifier($str) + { + return $this->getDatabasePlatform()->quoteIdentifier($str); + } + + /** + * The usage of this method is discouraged. Use prepared statements + * or {@see AbstractPlatform::quoteStringLiteral()} instead. + * + * @param mixed $value + * @param int|string|Type|null $type + * + * @return mixed + */ + public function quote($value, $type = ParameterType::STRING) + { + $connection = $this->getWrappedConnection(); + + [$value, $bindingType] = $this->getBindingInfo($value, $type); + + return $connection->quote($value, $bindingType); + } + + /** + * Prepares and executes an SQL query and returns the result as an array of numeric arrays. + * + * @param string $query SQL query + * @param list|array $params Query parameters + * @param array|array $types Parameter types + * + * @return list> + * + * @throws Exception + */ + public function fetchAllNumeric(string $query, array $params = [], array $types = []): array + { + return $this->executeQuery($query, $params, $types)->fetchAllNumeric(); + } + + /** + * Prepares and executes an SQL query and returns the result as an array of associative arrays. + * + * @param string $query SQL query + * @param list|array $params Query parameters + * @param array|array $types Parameter types + * + * @return list> + * + * @throws Exception + */ + public function fetchAllAssociative(string $query, array $params = [], array $types = []): array + { + return $this->executeQuery($query, $params, $types)->fetchAllAssociative(); + } + + /** + * Prepares and executes an SQL query and returns the result as an associative array with the keys + * mapped to the first column and the values mapped to the second column. + * + * @param string $query SQL query + * @param list|array $params Query parameters + * @param array|array $types Parameter types + * + * @return array + * + * @throws Exception + */ + public function fetchAllKeyValue(string $query, array $params = [], array $types = []): array + { + return $this->executeQuery($query, $params, $types)->fetchAllKeyValue(); + } + + /** + * Prepares and executes an SQL query and returns the result as an associative array with the keys mapped + * to the first column and the values being an associative array representing the rest of the columns + * and their values. + * + * @param string $query SQL query + * @param list|array $params Query parameters + * @param array|array $types Parameter types + * + * @return array> + * + * @throws Exception + */ + public function fetchAllAssociativeIndexed(string $query, array $params = [], array $types = []): array + { + return $this->executeQuery($query, $params, $types)->fetchAllAssociativeIndexed(); + } + + /** + * Prepares and executes an SQL query and returns the result as an array of the first column values. + * + * @param string $query SQL query + * @param list|array $params Query parameters + * @param array|array $types Parameter types + * + * @return list + * + * @throws Exception + */ + public function fetchFirstColumn(string $query, array $params = [], array $types = []): array + { + return $this->executeQuery($query, $params, $types)->fetchFirstColumn(); + } + + /** + * Prepares and executes an SQL query and returns the result as an iterator over rows represented as numeric arrays. + * + * @param string $query SQL query + * @param list|array $params Query parameters + * @param array|array $types Parameter types + * + * @return Traversable> + * + * @throws Exception + */ + public function iterateNumeric(string $query, array $params = [], array $types = []): Traversable + { + return $this->executeQuery($query, $params, $types)->iterateNumeric(); + } + + /** + * Prepares and executes an SQL query and returns the result as an iterator over rows represented + * as associative arrays. + * + * @param string $query SQL query + * @param list|array $params Query parameters + * @param array|array $types Parameter types + * + * @return Traversable> + * + * @throws Exception + */ + public function iterateAssociative(string $query, array $params = [], array $types = []): Traversable + { + return $this->executeQuery($query, $params, $types)->iterateAssociative(); + } + + /** + * Prepares and executes an SQL query and returns the result as an iterator with the keys + * mapped to the first column and the values mapped to the second column. + * + * @param string $query SQL query + * @param list|array $params Query parameters + * @param array|array $types Parameter types + * + * @return Traversable + * + * @throws Exception + */ + public function iterateKeyValue(string $query, array $params = [], array $types = []): Traversable + { + return $this->executeQuery($query, $params, $types)->iterateKeyValue(); + } + + /** + * Prepares and executes an SQL query and returns the result as an iterator with the keys mapped + * to the first column and the values being an associative array representing the rest of the columns + * and their values. + * + * @param string $query SQL query + * @param list|array $params Query parameters + * @param array|array $types Parameter types + * + * @return Traversable> + * + * @throws Exception + */ + public function iterateAssociativeIndexed(string $query, array $params = [], array $types = []): Traversable + { + return $this->executeQuery($query, $params, $types)->iterateAssociativeIndexed(); + } + + /** + * Prepares and executes an SQL query and returns the result as an iterator over the first column values. + * + * @param string $query SQL query + * @param list|array $params Query parameters + * @param array|array $types Parameter types + * + * @return Traversable + * + * @throws Exception + */ + public function iterateColumn(string $query, array $params = [], array $types = []): Traversable + { + return $this->executeQuery($query, $params, $types)->iterateColumn(); + } + + /** + * Prepares an SQL statement. + * + * @param string $sql The SQL statement to prepare. + * + * @throws Exception + */ + public function prepare(string $sql): Statement + { + $connection = $this->getWrappedConnection(); + + try { + $statement = $connection->prepare($sql); + } catch (Driver\Exception $e) { + throw $this->convertExceptionDuringQuery($e, $sql); + } + + return new Statement($this, $statement, $sql); + } + + /** + * Executes an, optionally parameterized, SQL query. + * + * If the query is parametrized, a prepared statement is used. + * If an SQLLogger is configured, the execution is logged. + * + * @param string $sql SQL query + * @param list|array $params Query parameters + * @param array|array $types Parameter types + * + * @throws Exception + */ + public function executeQuery( + string $sql, + array $params = [], + $types = [], + ?QueryCacheProfile $qcp = null + ): Result { + if ($qcp !== null) { + return $this->executeCacheQuery($sql, $params, $types, $qcp); + } + + $connection = $this->getWrappedConnection(); + + $logger = $this->_config->getSQLLogger(); + if ($logger !== null) { + $logger->startQuery($sql, $params, $types); + } + + try { + if (count($params) > 0) { + if ($this->needsArrayParameterConversion($params, $types)) { + [$sql, $params, $types] = $this->expandArrayParameters($sql, $params, $types); + } + + $stmt = $connection->prepare($sql); + + $this->bindParameters($stmt, $params, $types); + + $result = $stmt->execute(); + } else { + $result = $connection->query($sql); + } + + return new Result($result, $this); + } catch (Driver\Exception $e) { + throw $this->convertExceptionDuringQuery($e, $sql, $params, $types); + } finally { + if ($logger !== null) { + $logger->stopQuery(); + } + } + } + + /** + * Executes a caching query. + * + * @param string $sql SQL query + * @param list|array $params Query parameters + * @param array|array $types Parameter types + * + * @throws CacheException + * @throws Exception + */ + public function executeCacheQuery($sql, $params, $types, QueryCacheProfile $qcp): Result + { + $resultCache = $qcp->getResultCache() ?? $this->_config->getResultCache(); + + if ($resultCache === null) { + throw CacheException::noResultDriverConfigured(); + } + + $connectionParams = $this->params; + unset($connectionParams['platform'], $connectionParams['password'], $connectionParams['url']); + + [$cacheKey, $realKey] = $qcp->generateCacheKeys($sql, $params, $types, $connectionParams); + + $item = $resultCache->getItem($cacheKey); + + if ($item->isHit()) { + $value = $item->get(); + if (! is_array($value)) { + $value = []; + } + + if (isset($value[$realKey])) { + return new Result(new ArrayResult($value[$realKey]), $this); + } + } else { + $value = []; + } + + $data = $this->fetchAllAssociative($sql, $params, $types); + + $value[$realKey] = $data; + + $item->set($value); + + $lifetime = $qcp->getLifetime(); + if ($lifetime > 0) { + $item->expiresAfter($lifetime); + } + + $resultCache->save($item); + + return new Result(new ArrayResult($data), $this); + } + + /** + * Executes an SQL statement with the given parameters and returns the number of affected rows. + * + * Could be used for: + * - DML statements: INSERT, UPDATE, DELETE, etc. + * - DDL statements: CREATE, DROP, ALTER, etc. + * - DCL statements: GRANT, REVOKE, etc. + * - Session control statements: ALTER SESSION, SET, DECLARE, etc. + * - Other statements that don't yield a row set. + * + * This method supports PDO binding types as well as DBAL mapping types. + * + * @param string $sql SQL statement + * @param list|array $params Statement parameters + * @param array|array $types Parameter types + * + * @return int|string The number of affected rows. + * + * @throws Exception + */ + public function executeStatement($sql, array $params = [], array $types = []) + { + $connection = $this->getWrappedConnection(); + + $logger = $this->_config->getSQLLogger(); + if ($logger !== null) { + $logger->startQuery($sql, $params, $types); + } + + try { + if (count($params) > 0) { + if ($this->needsArrayParameterConversion($params, $types)) { + [$sql, $params, $types] = $this->expandArrayParameters($sql, $params, $types); + } + + $stmt = $connection->prepare($sql); + + $this->bindParameters($stmt, $params, $types); + + return $stmt->execute() + ->rowCount(); + } + + return $connection->exec($sql); + } catch (Driver\Exception $e) { + throw $this->convertExceptionDuringQuery($e, $sql, $params, $types); + } finally { + if ($logger !== null) { + $logger->stopQuery(); + } + } + } + + /** + * Returns the current transaction nesting level. + * + * @return int The nesting level. A value of 0 means there's no active transaction. + */ + public function getTransactionNestingLevel() + { + return $this->transactionNestingLevel; + } + + /** + * Returns the ID of the last inserted row, or the last value from a sequence object, + * depending on the underlying driver. + * + * Note: This method may not return a meaningful or consistent result across different drivers, + * because the underlying database may not even support the notion of AUTO_INCREMENT/IDENTITY + * columns or sequences. + * + * @param string|null $name Name of the sequence object from which the ID should be returned. + * + * @return string|int|false A string representation of the last inserted ID. + * + * @throws Exception + */ + public function lastInsertId($name = null) + { + if ($name !== null) { + Deprecation::trigger( + 'doctrine/dbal', + 'https://github.com/doctrine/dbal/issues/4687', + 'The usage of Connection::lastInsertId() with a sequence name is deprecated.', + ); + } + + try { + return $this->getWrappedConnection()->lastInsertId($name); + } catch (Driver\Exception $e) { + throw $this->convertException($e); + } + } + + /** + * Executes a function in a transaction. + * + * The function gets passed this Connection instance as an (optional) parameter. + * + * If an exception occurs during execution of the function or transaction commit, + * the transaction is rolled back and the exception re-thrown. + * + * @param Closure(self):T $func The function to execute transactionally. + * + * @return T The value returned by $func + * + * @throws Throwable + * + * @template T + */ + public function transactional(Closure $func) + { + $this->beginTransaction(); + + $successful = false; + + try { + $res = $func($this); + + $successful = true; + } finally { + if (! $successful) { + $this->rollBack(); + } + } + + $shouldRollback = true; + try { + $this->commit(); + + $shouldRollback = false; + } catch (TheDriverException $t) { + $convertedException = $this->handleDriverException($t, null); + $shouldRollback = ! ( + $convertedException instanceof TransactionRolledBack + || $convertedException instanceof UniqueConstraintViolationException + || $convertedException instanceof ForeignKeyConstraintViolationException + || $convertedException instanceof DeadlockException + ); + + throw $t; + } finally { + if ($shouldRollback) { + $this->rollBack(); + } + } + + return $res; + } + + /** + * Sets if nested transactions should use savepoints. + * + * @param bool $nestTransactionsWithSavepoints + * + * @return void + * + * @throws Exception + */ + public function setNestTransactionsWithSavepoints($nestTransactionsWithSavepoints) + { + if (! $nestTransactionsWithSavepoints) { + Deprecation::trigger( + 'doctrine/dbal', + 'https://github.com/doctrine/dbal/pull/5383', + <<<'DEPRECATION' + Nesting transactions without enabling savepoints is deprecated. + Call %s::setNestTransactionsWithSavepoints(true) to enable savepoints. + DEPRECATION, + self::class, + ); + } + + if ($this->transactionNestingLevel > 0) { + throw ConnectionException::mayNotAlterNestedTransactionWithSavepointsInTransaction(); + } + + $this->nestTransactionsWithSavepoints = (bool) $nestTransactionsWithSavepoints; + } + + /** + * Gets if nested transactions should use savepoints. + * + * @return bool + */ + public function getNestTransactionsWithSavepoints() + { + return $this->nestTransactionsWithSavepoints; + } + + /** + * Returns the savepoint name to use for nested transactions. + * + * @return string + */ + protected function _getNestedTransactionSavePointName() + { + return 'DOCTRINE_' . $this->transactionNestingLevel; + } + + /** + * @return bool + * + * @throws Exception + */ + public function beginTransaction() + { + $connection = $this->getWrappedConnection(); + + ++$this->transactionNestingLevel; + + $logger = $this->_config->getSQLLogger(); + + if ($this->transactionNestingLevel === 1) { + if ($logger !== null) { + $logger->startQuery('"START TRANSACTION"'); + } + + $connection->beginTransaction(); + + if ($logger !== null) { + $logger->stopQuery(); + } + } elseif ($this->nestTransactionsWithSavepoints) { + if ($logger !== null) { + $logger->startQuery('"SAVEPOINT"'); + } + + $this->createSavepoint($this->_getNestedTransactionSavePointName()); + if ($logger !== null) { + $logger->stopQuery(); + } + } else { + Deprecation::trigger( + 'doctrine/dbal', + 'https://github.com/doctrine/dbal/pull/5383', + <<<'DEPRECATION' + Nesting transactions without enabling savepoints is deprecated. + Call %s::setNestTransactionsWithSavepoints(true) to enable savepoints. + DEPRECATION, + self::class, + ); + } + + $eventManager = $this->getEventManager(); + + if ($eventManager->hasListeners(Events::onTransactionBegin)) { + Deprecation::trigger( + 'doctrine/dbal', + 'https://github.com/doctrine/dbal/issues/5784', + 'Subscribing to %s events is deprecated.', + Events::onTransactionBegin, + ); + + $eventManager->dispatchEvent(Events::onTransactionBegin, new TransactionBeginEventArgs($this)); + } + + return true; + } + + /** + * @return bool + * + * @throws Exception + */ + public function commit() + { + if ($this->transactionNestingLevel === 0) { + throw ConnectionException::noActiveTransaction(); + } + + if ($this->isRollbackOnly) { + throw ConnectionException::commitFailedRollbackOnly(); + } + + $result = true; + + $connection = $this->getWrappedConnection(); + + try { + if ($this->transactionNestingLevel === 1) { + $result = $this->doCommit($connection); + } elseif ($this->nestTransactionsWithSavepoints) { + $this->releaseSavepoint($this->_getNestedTransactionSavePointName()); + } + } finally { + $this->updateTransactionStateAfterCommit(); + } + + return $result; + } + + private function updateTransactionStateAfterCommit(): void + { + --$this->transactionNestingLevel; + + $eventManager = $this->getEventManager(); + + if ($eventManager->hasListeners(Events::onTransactionCommit)) { + Deprecation::trigger( + 'doctrine/dbal', + 'https://github.com/doctrine/dbal/issues/5784', + 'Subscribing to %s events is deprecated.', + Events::onTransactionCommit, + ); + + $eventManager->dispatchEvent(Events::onTransactionCommit, new TransactionCommitEventArgs($this)); + } + + if ($this->autoCommit !== false || $this->transactionNestingLevel !== 0) { + return; + } + + $this->beginTransaction(); + } + + /** + * @return bool + * + * @throws DriverException + */ + private function doCommit(DriverConnection $connection) + { + $logger = $this->_config->getSQLLogger(); + + if ($logger !== null) { + $logger->startQuery('"COMMIT"'); + } + + $result = $connection->commit(); + + if ($logger !== null) { + $logger->stopQuery(); + } + + return $result; + } + + /** + * Commits all current nesting transactions. + * + * @throws Exception + */ + private function commitAll(): void + { + while ($this->transactionNestingLevel !== 0) { + if ($this->autoCommit === false && $this->transactionNestingLevel === 1) { + // When in no auto-commit mode, the last nesting commit immediately starts a new transaction. + // Therefore we need to do the final commit here and then leave to avoid an infinite loop. + $this->commit(); + + return; + } + + $this->commit(); + } + } + + /** + * Cancels any database changes done during the current transaction. + * + * @return bool + * + * @throws Exception + */ + public function rollBack() + { + if ($this->transactionNestingLevel === 0) { + throw ConnectionException::noActiveTransaction(); + } + + $connection = $this->getWrappedConnection(); + + $logger = $this->_config->getSQLLogger(); + + if ($this->transactionNestingLevel === 1) { + if ($logger !== null) { + $logger->startQuery('"ROLLBACK"'); + } + + $this->transactionNestingLevel = 0; + $connection->rollBack(); + $this->isRollbackOnly = false; + if ($logger !== null) { + $logger->stopQuery(); + } + + if ($this->autoCommit === false) { + $this->beginTransaction(); + } + } elseif ($this->nestTransactionsWithSavepoints) { + if ($logger !== null) { + $logger->startQuery('"ROLLBACK TO SAVEPOINT"'); + } + + $this->rollbackSavepoint($this->_getNestedTransactionSavePointName()); + --$this->transactionNestingLevel; + if ($logger !== null) { + $logger->stopQuery(); + } + } else { + $this->isRollbackOnly = true; + --$this->transactionNestingLevel; + } + + $eventManager = $this->getEventManager(); + + if ($eventManager->hasListeners(Events::onTransactionRollBack)) { + Deprecation::trigger( + 'doctrine/dbal', + 'https://github.com/doctrine/dbal/issues/5784', + 'Subscribing to %s events is deprecated.', + Events::onTransactionRollBack, + ); + + $eventManager->dispatchEvent(Events::onTransactionRollBack, new TransactionRollBackEventArgs($this)); + } + + return true; + } + + /** + * Creates a new savepoint. + * + * @param string $savepoint The name of the savepoint to create. + * + * @return void + * + * @throws Exception + */ + public function createSavepoint($savepoint) + { + $platform = $this->getDatabasePlatform(); + + if (! $platform->supportsSavepoints()) { + throw ConnectionException::savepointsNotSupported(); + } + + $this->executeStatement($platform->createSavePoint($savepoint)); + } + + /** + * Releases the given savepoint. + * + * @param string $savepoint The name of the savepoint to release. + * + * @return void + * + * @throws Exception + */ + public function releaseSavepoint($savepoint) + { + $logger = $this->_config->getSQLLogger(); + + $platform = $this->getDatabasePlatform(); + + if (! $platform->supportsSavepoints()) { + throw ConnectionException::savepointsNotSupported(); + } + + if (! $platform->supportsReleaseSavepoints()) { + if ($logger !== null) { + $logger->stopQuery(); + } + + return; + } + + if ($logger !== null) { + $logger->startQuery('"RELEASE SAVEPOINT"'); + } + + $this->executeStatement($platform->releaseSavePoint($savepoint)); + + if ($logger === null) { + return; + } + + $logger->stopQuery(); + } + + /** + * Rolls back to the given savepoint. + * + * @param string $savepoint The name of the savepoint to rollback to. + * + * @return void + * + * @throws Exception + */ + public function rollbackSavepoint($savepoint) + { + $platform = $this->getDatabasePlatform(); + + if (! $platform->supportsSavepoints()) { + throw ConnectionException::savepointsNotSupported(); + } + + $this->executeStatement($platform->rollbackSavePoint($savepoint)); + } + + /** + * Gets the wrapped driver connection. + * + * @deprecated Use {@link getNativeConnection()} to access the native connection. + * + * @return DriverConnection + * + * @throws Exception + */ + public function getWrappedConnection() + { + Deprecation::triggerIfCalledFromOutside( + 'doctrine/dbal', + 'https://github.com/doctrine/dbal/issues/4966', + 'Connection::getWrappedConnection() is deprecated.' + . ' Use Connection::getNativeConnection() to access the native connection.', + ); + + $this->connect(); + + return $this->_conn; + } + + /** @return resource|object */ + public function getNativeConnection() + { + $this->connect(); + + if (! method_exists($this->_conn, 'getNativeConnection')) { + throw new LogicException(sprintf( + 'The driver connection %s does not support accessing the native connection.', + get_class($this->_conn), + )); + } + + return $this->_conn->getNativeConnection(); + } + + /** + * Creates a SchemaManager that can be used to inspect or change the + * database schema through the connection. + * + * @throws Exception + */ + public function createSchemaManager(): AbstractSchemaManager + { + return $this->schemaManagerFactory->createSchemaManager($this); + } + + /** + * Gets the SchemaManager that can be used to inspect or change the + * database schema through the connection. + * + * @deprecated Use {@see createSchemaManager()} instead. + * + * @return AbstractSchemaManager + * + * @throws Exception + */ + public function getSchemaManager() + { + Deprecation::triggerIfCalledFromOutside( + 'doctrine/dbal', + 'https://github.com/doctrine/dbal/issues/4515', + 'Connection::getSchemaManager() is deprecated, use Connection::createSchemaManager() instead.', + ); + + return $this->_schemaManager ??= $this->createSchemaManager(); + } + + /** + * Marks the current transaction so that the only possible + * outcome for the transaction to be rolled back. + * + * @return void + * + * @throws ConnectionException If no transaction is active. + */ + public function setRollbackOnly() + { + if ($this->transactionNestingLevel === 0) { + throw ConnectionException::noActiveTransaction(); + } + + $this->isRollbackOnly = true; + } + + /** + * Checks whether the current transaction is marked for rollback only. + * + * @return bool + * + * @throws ConnectionException If no transaction is active. + */ + public function isRollbackOnly() + { + if ($this->transactionNestingLevel === 0) { + throw ConnectionException::noActiveTransaction(); + } + + return $this->isRollbackOnly; + } + + /** + * Converts a given value to its database representation according to the conversion + * rules of a specific DBAL mapping type. + * + * @param mixed $value The value to convert. + * @param string $type The name of the DBAL mapping type. + * + * @return mixed The converted value. + * + * @throws Exception + */ + public function convertToDatabaseValue($value, $type) + { + return Type::getType($type)->convertToDatabaseValue($value, $this->getDatabasePlatform()); + } + + /** + * Converts a given value to its PHP representation according to the conversion + * rules of a specific DBAL mapping type. + * + * @param mixed $value The value to convert. + * @param string $type The name of the DBAL mapping type. + * + * @return mixed The converted type. + * + * @throws Exception + */ + public function convertToPHPValue($value, $type) + { + return Type::getType($type)->convertToPHPValue($value, $this->getDatabasePlatform()); + } + + /** + * Binds a set of parameters, some or all of which are typed with a PDO binding type + * or DBAL mapping type, to a given statement. + * + * @param DriverStatement $stmt Prepared statement + * @param list|array $params Statement parameters + * @param array|array $types Parameter types + * + * @throws Exception + */ + private function bindParameters(DriverStatement $stmt, array $params, array $types): void + { + // Check whether parameters are positional or named. Mixing is not allowed. + if (is_int(key($params))) { + $bindIndex = 1; + + foreach ($params as $key => $value) { + if (isset($types[$key])) { + $type = $types[$key]; + [$value, $bindingType] = $this->getBindingInfo($value, $type); + } else { + if (array_key_exists($key, $types)) { + Deprecation::trigger( + 'doctrine/dbal', + 'https://github.com/doctrine/dbal/pull/5550', + 'Using NULL as prepared statement parameter type is deprecated.' + . 'Omit or use ParameterType::STRING instead', + ); + } + + $bindingType = ParameterType::STRING; + } + + $stmt->bindValue($bindIndex, $value, $bindingType); + + ++$bindIndex; + } + } else { + // Named parameters + foreach ($params as $name => $value) { + if (isset($types[$name])) { + $type = $types[$name]; + [$value, $bindingType] = $this->getBindingInfo($value, $type); + } else { + if (array_key_exists($name, $types)) { + Deprecation::trigger( + 'doctrine/dbal', + 'https://github.com/doctrine/dbal/pull/5550', + 'Using NULL as prepared statement parameter type is deprecated.' + . 'Omit or use ParameterType::STRING instead', + ); + } + + $bindingType = ParameterType::STRING; + } + + $stmt->bindValue($name, $value, $bindingType); + } + } + } + + /** + * Gets the binding type of a given type. + * + * @param mixed $value The value to bind. + * @param int|string|Type|null $type The type to bind (PDO or DBAL). + * + * @return array{mixed, int} [0] => the (escaped) value, [1] => the binding type. + * + * @throws Exception + */ + private function getBindingInfo($value, $type): array + { + if (is_string($type)) { + $type = Type::getType($type); + } + + if ($type instanceof Type) { + $value = $type->convertToDatabaseValue($value, $this->getDatabasePlatform()); + $bindingType = $type->getBindingType(); + } else { + $bindingType = $type ?? ParameterType::STRING; + } + + return [$value, $bindingType]; + } + + /** + * Creates a new instance of a SQL query builder. + * + * @return QueryBuilder + */ + public function createQueryBuilder() + { + return new Query\QueryBuilder($this); + } + + /** + * @internal + * + * @param list|array $params + * @param array|array $types + */ + final public function convertExceptionDuringQuery( + Driver\Exception $e, + string $sql, + array $params = [], + array $types = [] + ): DriverException { + return $this->handleDriverException($e, new Query($sql, $params, $types)); + } + + /** @internal */ + final public function convertException(Driver\Exception $e): DriverException + { + return $this->handleDriverException($e, null); + } + + /** + * @param array|array $params + * @param array|array $types + * + * @return array{string, list, array} + */ + private function expandArrayParameters(string $sql, array $params, array $types): array + { + $this->parser ??= $this->getDatabasePlatform()->createSQLParser(); + $visitor = new ExpandArrayParameters($params, $types); + + $this->parser->parse($sql, $visitor); + + return [ + $visitor->getSQL(), + $visitor->getParameters(), + $visitor->getTypes(), + ]; + } + + /** + * @param array|array $params + * @param array|array $types + */ + private function needsArrayParameterConversion(array $params, array $types): bool + { + if (is_string(key($params))) { + return true; + } + + foreach ($types as $type) { + if ( + $type === ArrayParameterType::INTEGER + || $type === ArrayParameterType::STRING + || $type === ArrayParameterType::ASCII + || $type === ArrayParameterType::BINARY + ) { + return true; + } + } + + return false; + } + + private function handleDriverException( + Driver\Exception $driverException, + ?Query $query + ): DriverException { + $this->exceptionConverter ??= $this->_driver->getExceptionConverter(); + $exception = $this->exceptionConverter->convert($driverException, $query); + + if ($exception instanceof ConnectionLost) { + $this->close(); + } + + return $exception; + } + + /** + * BC layer for a wide-spread use-case of old DBAL APIs + * + * @deprecated Use {@see executeStatement()} instead + * + * @param array $params The query parameters + * @param array $types The parameter types + */ + public function executeUpdate(string $sql, array $params = [], array $types = []): int + { + Deprecation::trigger( + 'doctrine/dbal', + 'https://github.com/doctrine/dbal/pull/4163', + '%s is deprecated, please use executeStatement() instead.', + __METHOD__, + ); + + return $this->executeStatement($sql, $params, $types); + } + + /** + * BC layer for a wide-spread use-case of old DBAL APIs + * + * @deprecated Use {@see executeQuery()} instead + */ + public function query(string $sql): Result + { + Deprecation::trigger( + 'doctrine/dbal', + 'https://github.com/doctrine/dbal/pull/4163', + '%s is deprecated, please use executeQuery() instead.', + __METHOD__, + ); + + return $this->executeQuery($sql); + } + + /** + * BC layer for a wide-spread use-case of old DBAL APIs + * + * @deprecated please use {@see executeStatement()} instead + */ + public function exec(string $sql): int + { + Deprecation::trigger( + 'doctrine/dbal', + 'https://github.com/doctrine/dbal/pull/4163', + '%s is deprecated, please use executeStatement() instead.', + __METHOD__, + ); + + return $this->executeStatement($sql); + } +} diff --git a/3rdparty/doctrine/dbal/src/ConnectionException.php b/3rdparty/doctrine/dbal/src/ConnectionException.php new file mode 100644 index 00000000..49d9efe4 --- /dev/null +++ b/3rdparty/doctrine/dbal/src/ConnectionException.php @@ -0,0 +1,30 @@ +executeQuery("DELETE FROM table"); + * + * Be aware that Connection#executeQuery is a method specifically for READ + * operations only. + * + * Use Connection#executeStatement for any SQL statement that changes/updates + * state in the database (UPDATE, INSERT, DELETE or DDL statements). + * + * This connection is limited to replica operations using the + * Connection#executeQuery operation only, because it wouldn't be compatible + * with the ORM or SchemaManager code otherwise. Both use all the other + * operations in a context where writes could happen to a replica, which makes + * this restricted approach necessary. + * + * You can manually connect to the primary at any time by calling: + * + * $conn->ensureConnectedToPrimary(); + * + * Instantiation through the DriverManager looks like: + * + * @phpstan-import-type Params from DriverManager + * @example + * + * $conn = DriverManager::getConnection(array( + * 'wrapperClass' => 'Doctrine\DBAL\Connections\PrimaryReadReplicaConnection', + * 'driver' => 'pdo_mysql', + * 'primary' => array('user' => '', 'password' => '', 'host' => '', 'dbname' => ''), + * 'replica' => array( + * array('user' => 'replica1', 'password' => '', 'host' => '', 'dbname' => ''), + * array('user' => 'replica2', 'password' => '', 'host' => '', 'dbname' => ''), + * ) + * )); + * + * You can also pass 'driverOptions' and any other documented option to each of this drivers + * to pass additional information. + */ +class PrimaryReadReplicaConnection extends Connection +{ + /** + * Primary and Replica connection (one of the randomly picked replicas). + * + * @var DriverConnection[]|null[] + */ + protected $connections = ['primary' => null, 'replica' => null]; + + /** + * You can keep the replica connection and then switch back to it + * during the request if you know what you are doing. + * + * @var bool + */ + protected $keepReplica = false; + + /** + * Creates Primary Replica Connection. + * + * @internal The connection can be only instantiated by the driver manager. + * + * @param array $params + * @phpstan-param Params $params + * + * @throws Exception + * @throws InvalidArgumentException + */ + public function __construct( + array $params, + Driver $driver, + ?Configuration $config = null, + ?EventManager $eventManager = null + ) { + if (! isset($params['replica'], $params['primary'])) { + throw new InvalidArgumentException('primary or replica configuration missing'); + } + + if (count($params['replica']) === 0) { + throw new InvalidArgumentException('You have to configure at least one replica.'); + } + + if (isset($params['driver'])) { + $params['primary']['driver'] = $params['driver']; + + foreach ($params['replica'] as $replicaKey => $replica) { + $params['replica'][$replicaKey]['driver'] = $params['driver']; + } + } + + $this->keepReplica = (bool) ($params['keepReplica'] ?? false); + + parent::__construct($params, $driver, $config, $eventManager); + } + + /** + * Checks if the connection is currently towards the primary or not. + */ + public function isConnectedToPrimary(): bool + { + return $this->_conn !== null && $this->_conn === $this->connections['primary']; + } + + /** + * @param string|null $connectionName + * + * @return bool + */ + public function connect($connectionName = null) + { + if ($connectionName !== null) { + throw new InvalidArgumentException( + 'Passing a connection name as first argument is not supported anymore.' + . ' Use ensureConnectedToPrimary()/ensureConnectedToReplica() instead.', + ); + } + + return $this->performConnect(); + } + + protected function performConnect(?string $connectionName = null): bool + { + $requestedConnectionChange = ($connectionName !== null); + $connectionName = $connectionName ?? 'replica'; + + if ($connectionName !== 'replica' && $connectionName !== 'primary') { + throw new InvalidArgumentException('Invalid option to connect(), only primary or replica allowed.'); + } + + // If we have a connection open, and this is not an explicit connection + // change request, then abort right here, because we are already done. + // This prevents writes to the replica in case of "keepReplica" option enabled. + if ($this->_conn !== null && ! $requestedConnectionChange) { + return false; + } + + $forcePrimaryAsReplica = false; + + if ($this->getTransactionNestingLevel() > 0) { + $connectionName = 'primary'; + $forcePrimaryAsReplica = true; + } + + if (isset($this->connections[$connectionName])) { + $this->_conn = $this->connections[$connectionName]; + + if ($forcePrimaryAsReplica && ! $this->keepReplica) { + $this->connections['replica'] = $this->_conn; + } + + return false; + } + + if ($connectionName === 'primary') { + $this->connections['primary'] = $this->_conn = $this->connectTo($connectionName); + + // Set replica connection to primary to avoid invalid reads + if (! $this->keepReplica) { + $this->connections['replica'] = $this->connections['primary']; + } + } else { + $this->connections['replica'] = $this->_conn = $this->connectTo($connectionName); + } + + if ($this->_eventManager->hasListeners(Events::postConnect)) { + Deprecation::trigger( + 'doctrine/dbal', + 'https://github.com/doctrine/dbal/issues/5784', + 'Subscribing to %s events is deprecated. Implement a middleware instead.', + Events::postConnect, + ); + + $eventArgs = new ConnectionEventArgs($this); + $this->_eventManager->dispatchEvent(Events::postConnect, $eventArgs); + } + + return true; + } + + /** + * Connects to the primary node of the database cluster. + * + * All following statements after this will be executed against the primary node. + */ + public function ensureConnectedToPrimary(): bool + { + return $this->performConnect('primary'); + } + + /** + * Connects to a replica node of the database cluster. + * + * All following statements after this will be executed against the replica node, + * unless the keepReplica option is set to false and a primary connection + * was already opened. + */ + public function ensureConnectedToReplica(): bool + { + return $this->performConnect('replica'); + } + + /** + * Connects to a specific connection. + * + * @param string $connectionName + * + * @return DriverConnection + * + * @throws Exception + */ + protected function connectTo($connectionName) + { + $params = $this->getParams(); + + $connectionParams = $this->chooseConnectionConfiguration($connectionName, $params); + + try { + return $this->_driver->connect($connectionParams); + } catch (DriverException $e) { + throw $this->convertException($e); + } + } + + /** + * @param string $connectionName + * @param mixed[] $params + * + * @return mixed + */ + protected function chooseConnectionConfiguration( + $connectionName, + #[SensitiveParameter] + $params + ) { + if ($connectionName === 'primary') { + return $params['primary']; + } + + $config = $params['replica'][array_rand($params['replica'])]; + + if (! isset($config['charset']) && isset($params['primary']['charset'])) { + $config['charset'] = $params['primary']['charset']; + } + + return $config; + } + + /** + * {@inheritDoc} + */ + public function executeStatement($sql, array $params = [], array $types = []) + { + $this->ensureConnectedToPrimary(); + + return parent::executeStatement($sql, $params, $types); + } + + /** + * {@inheritDoc} + */ + public function beginTransaction() + { + $this->ensureConnectedToPrimary(); + + return parent::beginTransaction(); + } + + /** + * {@inheritDoc} + */ + public function commit() + { + $this->ensureConnectedToPrimary(); + + return parent::commit(); + } + + /** + * {@inheritDoc} + */ + public function rollBack() + { + $this->ensureConnectedToPrimary(); + + return parent::rollBack(); + } + + /** + * {@inheritDoc} + */ + public function close() + { + unset($this->connections['primary'], $this->connections['replica']); + + parent::close(); + + $this->_conn = null; + $this->connections = ['primary' => null, 'replica' => null]; + } + + /** + * {@inheritDoc} + */ + public function createSavepoint($savepoint) + { + $this->ensureConnectedToPrimary(); + + parent::createSavepoint($savepoint); + } + + /** + * {@inheritDoc} + */ + public function releaseSavepoint($savepoint) + { + $this->ensureConnectedToPrimary(); + + parent::releaseSavepoint($savepoint); + } + + /** + * {@inheritDoc} + */ + public function rollbackSavepoint($savepoint) + { + $this->ensureConnectedToPrimary(); + + parent::rollbackSavepoint($savepoint); + } + + public function prepare(string $sql): Statement + { + $this->ensureConnectedToPrimary(); + + return parent::prepare($sql); + } +} diff --git a/3rdparty/doctrine/dbal/src/Driver.php b/3rdparty/doctrine/dbal/src/Driver.php new file mode 100644 index 00000000..a376f069 --- /dev/null +++ b/3rdparty/doctrine/dbal/src/Driver.php @@ -0,0 +1,57 @@ + $params All connection parameters. + * @phpstan-param Params $params All connection parameters. + * + * @return DriverConnection The database connection. + * + * @throws Exception + */ + public function connect( + #[SensitiveParameter] + array $params + ); + + /** + * Gets the DatabasePlatform instance that provides all the metadata about + * the platform this driver connects to. + * + * @return AbstractPlatform The database platform. + */ + public function getDatabasePlatform(); + + /** + * Gets the SchemaManager that can be used to inspect and change the underlying + * database schema of the platform this driver connects to. + * + * @deprecated Use {@link AbstractPlatform::createSchemaManager()} instead. + * + * @return AbstractSchemaManager + */ + public function getSchemaManager(Connection $conn, AbstractPlatform $platform); + + /** + * Gets the ExceptionConverter that can be used to convert driver-level exceptions into DBAL exceptions. + */ + public function getExceptionConverter(): ExceptionConverter; +} diff --git a/3rdparty/doctrine/dbal/src/Driver/API/ExceptionConverter.php b/3rdparty/doctrine/dbal/src/Driver/API/ExceptionConverter.php new file mode 100644 index 00000000..a7bf2713 --- /dev/null +++ b/3rdparty/doctrine/dbal/src/Driver/API/ExceptionConverter.php @@ -0,0 +1,25 @@ +getCode()) { + case -104: + return new SyntaxErrorException($exception, $query); + + case -203: + return new NonUniqueFieldNameException($exception, $query); + + case -204: + return new TableNotFoundException($exception, $query); + + case -206: + return new InvalidFieldNameException($exception, $query); + + case -407: + return new NotNullConstraintViolationException($exception, $query); + + case -530: + case -531: + case -532: + case -20356: + return new ForeignKeyConstraintViolationException($exception, $query); + + case -601: + return new TableExistsException($exception, $query); + + case -803: + return new UniqueConstraintViolationException($exception, $query); + + case -1336: + case -30082: + return new ConnectionException($exception, $query); + } + + return new DriverException($exception, $query); + } +} diff --git a/3rdparty/doctrine/dbal/src/Driver/API/MySQL/ExceptionConverter.php b/3rdparty/doctrine/dbal/src/Driver/API/MySQL/ExceptionConverter.php new file mode 100644 index 00000000..10507afd --- /dev/null +++ b/3rdparty/doctrine/dbal/src/Driver/API/MySQL/ExceptionConverter.php @@ -0,0 +1,131 @@ +getCode()) { + case 1008: + return new DatabaseDoesNotExist($exception, $query); + + case 1213: + return new DeadlockException($exception, $query); + + case 1205: + return new LockWaitTimeoutException($exception, $query); + + case 1050: + return new TableExistsException($exception, $query); + + case 1051: + case 1146: + return new TableNotFoundException($exception, $query); + + case 1216: + case 1217: + case 1451: + case 1452: + case 1701: + return new ForeignKeyConstraintViolationException($exception, $query); + + case 1062: + case 1557: + case 1569: + case 1586: + return new UniqueConstraintViolationException($exception, $query); + + case 1054: + case 1166: + case 1611: + return new InvalidFieldNameException($exception, $query); + + case 1052: + case 1060: + case 1110: + return new NonUniqueFieldNameException($exception, $query); + + case 1064: + case 1149: + case 1287: + case 1341: + case 1342: + case 1343: + case 1344: + case 1382: + case 1479: + case 1541: + case 1554: + case 1626: + return new SyntaxErrorException($exception, $query); + + case 1524: + if (strpos($exception->getMessage(), 'Plugin \'mysql_native_password\' is not loaded') === false) { + break; + } + + // Workaround for MySQL 8.4 if we request an unknown user. + // https://bugs.mysql.com/bug.php?id=114876 + return new ConnectionException($exception, $query); + + case 1044: + case 1045: + case 1046: + case 1049: + case 1095: + case 1142: + case 1143: + case 1227: + case 1370: + case 1429: + case 2002: + case 2005: + case 2054: + return new ConnectionException($exception, $query); + + case 2006: + case 4031: + return new ConnectionLost($exception, $query); + + case 1048: + case 1121: + case 1138: + case 1171: + case 1252: + case 1263: + case 1364: + case 1566: + return new NotNullConstraintViolationException($exception, $query); + } + + return new DriverException($exception, $query); + } +} diff --git a/3rdparty/doctrine/dbal/src/Driver/API/OCI/ExceptionConverter.php b/3rdparty/doctrine/dbal/src/Driver/API/OCI/ExceptionConverter.php new file mode 100644 index 00000000..62a08a79 --- /dev/null +++ b/3rdparty/doctrine/dbal/src/Driver/API/OCI/ExceptionConverter.php @@ -0,0 +1,106 @@ +getCode(); + // @phpstan-ignore property.notFound, property.notFound + if ($code === 'HY000' && isset($exception->errorInfo[1], $exception->errorInfo[2])) { + $errorInfo = $exception->errorInfo; + $exception = new PDOException($errorInfo[2], $errorInfo[1]); + $exception->errorInfo = $errorInfo; + $code = $exception->getCode(); + } + + switch ($code) { + case 1: + case 2299: + case 38911: + return new UniqueConstraintViolationException($exception, $query); + + case 904: + return new InvalidFieldNameException($exception, $query); + + case 918: + case 960: + return new NonUniqueFieldNameException($exception, $query); + + case 923: + return new SyntaxErrorException($exception, $query); + + case 942: + return new TableNotFoundException($exception, $query); + + case 955: + return new TableExistsException($exception, $query); + + case 1017: + case 12545: + return new ConnectionException($exception, $query); + + case 1400: + return new NotNullConstraintViolationException($exception, $query); + + case 1918: + return new DatabaseDoesNotExist($exception, $query); + + case 2091: + //ORA-02091: transaction rolled back + //ORA-00001: unique constraint (DOCTRINE.GH3423_UNIQUE) violated + [, $causeError] = explode("\n", $exception->getMessage(), 2); + + [$causeCode] = explode(': ', $causeError, 2); + $code = (int) str_replace('ORA-', '', $causeCode); + + if ($exception instanceof PDOException) { + $why = $this->convert(new PDOException($causeError, $code, $exception), $query); + } else { + $why = $this->convert(new Error($causeError, null, $code, $exception), $query); + } + + return new TransactionRolledBack($why, $query); + + case 2289: + case 2443: + case 4080: + return new DatabaseObjectNotFoundException($exception, $query); + + case 2266: + case 2291: + case 2292: + return new ForeignKeyConstraintViolationException($exception, $query); + } + + return new DriverException($exception, $query); + } +} diff --git a/3rdparty/doctrine/dbal/src/Driver/API/PostgreSQL/ExceptionConverter.php b/3rdparty/doctrine/dbal/src/Driver/API/PostgreSQL/ExceptionConverter.php new file mode 100644 index 00000000..2baca1ee --- /dev/null +++ b/3rdparty/doctrine/dbal/src/Driver/API/PostgreSQL/ExceptionConverter.php @@ -0,0 +1,89 @@ +getSQLState()) { + case '40001': + case '40P01': + return new DeadlockException($exception, $query); + + case '0A000': + // Foreign key constraint violations during a TRUNCATE operation + // are considered "feature not supported" in PostgreSQL. + if (strpos($exception->getMessage(), 'truncate') !== false) { + return new ForeignKeyConstraintViolationException($exception, $query); + } + + break; + + case '23502': + return new NotNullConstraintViolationException($exception, $query); + + case '23503': + return new ForeignKeyConstraintViolationException($exception, $query); + + case '23505': + return new UniqueConstraintViolationException($exception, $query); + + case '3D000': + return new DatabaseDoesNotExist($exception, $query); + + case '3F000': + return new SchemaDoesNotExist($exception, $query); + + case '42601': + return new SyntaxErrorException($exception, $query); + + case '42702': + return new NonUniqueFieldNameException($exception, $query); + + case '42703': + return new InvalidFieldNameException($exception, $query); + + case '42P01': + return new TableNotFoundException($exception, $query); + + case '42P07': + return new TableExistsException($exception, $query); + + case '08006': + return new ConnectionException($exception, $query); + } + + // Prior to fixing https://bugs.php.net/bug.php?id=64705 (PHP 7.4.10), + // in some cases (mainly connection errors) the PDO exception wouldn't provide a SQLSTATE via its code. + // We have to match against the SQLSTATE in the error message in these cases. + if ($exception->getCode() === 7 && strpos($exception->getMessage(), 'SQLSTATE[08006]') !== false) { + return new ConnectionException($exception, $query); + } + + return new DriverException($exception, $query); + } +} diff --git a/3rdparty/doctrine/dbal/src/Driver/API/SQLSrv/ExceptionConverter.php b/3rdparty/doctrine/dbal/src/Driver/API/SQLSrv/ExceptionConverter.php new file mode 100644 index 00000000..d0e8e9f4 --- /dev/null +++ b/3rdparty/doctrine/dbal/src/Driver/API/SQLSrv/ExceptionConverter.php @@ -0,0 +1,69 @@ +getCode()) { + case 102: + return new SyntaxErrorException($exception, $query); + + case 207: + return new InvalidFieldNameException($exception, $query); + + case 208: + return new TableNotFoundException($exception, $query); + + case 209: + return new NonUniqueFieldNameException($exception, $query); + + case 515: + return new NotNullConstraintViolationException($exception, $query); + + case 547: + case 4712: + return new ForeignKeyConstraintViolationException($exception, $query); + + case 2601: + case 2627: + return new UniqueConstraintViolationException($exception, $query); + + case 2714: + return new TableExistsException($exception, $query); + + case 3701: + case 15151: + return new DatabaseObjectNotFoundException($exception, $query); + + case 11001: + case 18456: + return new ConnectionException($exception, $query); + } + + return new DriverException($exception, $query); + } +} diff --git a/3rdparty/doctrine/dbal/src/Driver/API/SQLite/ExceptionConverter.php b/3rdparty/doctrine/dbal/src/Driver/API/SQLite/ExceptionConverter.php new file mode 100644 index 00000000..9e67155a --- /dev/null +++ b/3rdparty/doctrine/dbal/src/Driver/API/SQLite/ExceptionConverter.php @@ -0,0 +1,85 @@ +getMessage(), 'database is locked') !== false) { + return new LockWaitTimeoutException($exception, $query); + } + + if ( + strpos($exception->getMessage(), 'must be unique') !== false || + strpos($exception->getMessage(), 'is not unique') !== false || + strpos($exception->getMessage(), 'are not unique') !== false || + strpos($exception->getMessage(), 'UNIQUE constraint failed') !== false + ) { + return new UniqueConstraintViolationException($exception, $query); + } + + if ( + strpos($exception->getMessage(), 'may not be NULL') !== false || + strpos($exception->getMessage(), 'NOT NULL constraint failed') !== false + ) { + return new NotNullConstraintViolationException($exception, $query); + } + + if (strpos($exception->getMessage(), 'no such table:') !== false) { + return new TableNotFoundException($exception, $query); + } + + if (strpos($exception->getMessage(), 'already exists') !== false) { + return new TableExistsException($exception, $query); + } + + if (strpos($exception->getMessage(), 'has no column named') !== false) { + return new InvalidFieldNameException($exception, $query); + } + + if (strpos($exception->getMessage(), 'ambiguous column name') !== false) { + return new NonUniqueFieldNameException($exception, $query); + } + + if (strpos($exception->getMessage(), 'syntax error') !== false) { + return new SyntaxErrorException($exception, $query); + } + + if (strpos($exception->getMessage(), 'attempt to write a readonly database') !== false) { + return new ReadOnlyException($exception, $query); + } + + if (strpos($exception->getMessage(), 'unable to open database file') !== false) { + return new ConnectionException($exception, $query); + } + + if (strpos($exception->getMessage(), 'FOREIGN KEY constraint failed') !== false) { + return new ForeignKeyConstraintViolationException($exception, $query); + } + + return new DriverException($exception, $query); + } +} diff --git a/3rdparty/doctrine/dbal/src/Driver/API/SQLite/UserDefinedFunctions.php b/3rdparty/doctrine/dbal/src/Driver/API/SQLite/UserDefinedFunctions.php new file mode 100644 index 00000000..3779c8ba --- /dev/null +++ b/3rdparty/doctrine/dbal/src/Driver/API/SQLite/UserDefinedFunctions.php @@ -0,0 +1,80 @@ + ['callback' => [SqlitePlatform::class, 'udfSqrt'], 'numArgs' => 1], + 'mod' => ['callback' => [SqlitePlatform::class, 'udfMod'], 'numArgs' => 2], + 'locate' => ['callback' => [SqlitePlatform::class, 'udfLocate'], 'numArgs' => -1], + ]; + + /** + * @param callable(string, callable, int): bool $callback + * @param array $additionalFunctions + */ + public static function register(callable $callback, array $additionalFunctions = []): void + { + $userDefinedFunctions = array_merge(self::DEFAULT_FUNCTIONS, $additionalFunctions); + + foreach ($userDefinedFunctions as $function => $data) { + $callback($function, $data['callback'], $data['numArgs']); + } + } + + /** + * User-defined function that implements MOD(). + * + * @param int $a + * @param int $b + */ + public static function mod($a, $b): int + { + return $a % $b; + } + + /** + * User-defined function that implements LOCATE(). + * + * @param string $str + * @param string $substr + * @param int $offset + */ + public static function locate($str, $substr, $offset = 0): int + { + Deprecation::trigger( + 'doctrine/dbal', + 'https://github.com/doctrine/dbal/pull/5749', + 'Relying on DBAL\'s emulated LOCATE() function is deprecated. ' + . 'Use INSTR() or %s::getLocateExpression() instead.', + AbstractPlatform::class, + ); + + // SQL's LOCATE function works on 1-based positions, while PHP's strpos works on 0-based positions. + // So we have to make them compatible if an offset is given. + if ($offset > 0) { + $offset -= 1; + } + + $pos = strpos($str, $substr, $offset); + + if ($pos !== false) { + return $pos + 1; + } + + return 0; + } +} diff --git a/3rdparty/doctrine/dbal/src/Driver/AbstractDB2Driver.php b/3rdparty/doctrine/dbal/src/Driver/AbstractDB2Driver.php new file mode 100644 index 00000000..81d84328 --- /dev/null +++ b/3rdparty/doctrine/dbal/src/Driver/AbstractDB2Driver.php @@ -0,0 +1,100 @@ +getVersionNumber($version), '11.1', '>=')) { + return new DB2111Platform(); + } + + Deprecation::trigger( + 'doctrine/dbal', + 'https://github.com/doctrine/dbal/pull/5156', + 'IBM DB2 < 11.1 support is deprecated and will be removed in DBAL 4.' + . ' Consider upgrading to IBM DB2 11.1 or later.', + ); + + return $this->getDatabasePlatform(); + } + + /** + * Detects IBM DB2 server version + * + * @param string $versionString Version string as returned by IBM DB2 server, i.e. 'DB2/LINUXX8664 11.5.8.0' + * + * @throws DBALException + */ + private function getVersionNumber(string $versionString): string + { + if ( + preg_match( + '/^(?:[^\s]+\s)?(?P\d+)\.(?P\d+)\.(?P\d+)/i', + $versionString, + $versionParts, + ) !== 1 + ) { + throw DBALException::invalidPlatformVersionSpecified( + $versionString, + '^(?:[^\s]+\s)?..', + ); + } + + return $versionParts['major'] . '.' . $versionParts['minor'] . '.' . $versionParts['patch']; + } +} diff --git a/3rdparty/doctrine/dbal/src/Driver/AbstractException.php b/3rdparty/doctrine/dbal/src/Driver/AbstractException.php new file mode 100644 index 00000000..9b7bff40 --- /dev/null +++ b/3rdparty/doctrine/dbal/src/Driver/AbstractException.php @@ -0,0 +1,42 @@ +sqlState = $sqlState; + } + + /** + * {@inheritDoc} + */ + public function getSQLState() + { + return $this->sqlState; + } +} diff --git a/3rdparty/doctrine/dbal/src/Driver/AbstractMySQLDriver.php b/3rdparty/doctrine/dbal/src/Driver/AbstractMySQLDriver.php new file mode 100644 index 00000000..1bb32703 --- /dev/null +++ b/3rdparty/doctrine/dbal/src/Driver/AbstractMySQLDriver.php @@ -0,0 +1,236 @@ +getMariaDbMysqlVersionNumber($version); + if (version_compare($mariaDbVersion, '11.7.0', '>=')) { + return new MariaDb110700Platform(); + } + + if (version_compare($mariaDbVersion, '10.10.0', '>=')) { + return new MariaDb1010Platform(); + } + + if (version_compare($mariaDbVersion, '10.6.0', '>=')) { + return new MariaDb1060Platform(); + } + + if (version_compare($mariaDbVersion, '10.5.2', '>=')) { + return new MariaDb1052Platform(); + } + + if (version_compare($mariaDbVersion, '10.4.3', '>=')) { + return new MariaDb1043Platform(); + } + + Deprecation::trigger( + 'doctrine/dbal', + 'https://github.com/doctrine/dbal/pull/6110', + 'Support for MariaDB < 10.4 is deprecated and will be removed in DBAL 4.' + . ' Consider upgrading to a more recent version of MariaDB.', + ); + + if (version_compare($mariaDbVersion, '10.2.7', '>=')) { + return new MariaDb1027Platform(); + } + } else { + $oracleMysqlVersion = $this->getOracleMysqlVersionNumber($version); + + if (version_compare($oracleMysqlVersion, '8.4.0', '>=')) { + if (! version_compare($version, '8.4.0', '>=')) { + Deprecation::trigger( + 'doctrine/orm', + 'https://github.com/doctrine/dbal/pull/5779', + 'Version detection logic for MySQL will change in DBAL 4. ' + . 'Please specify the version as the server reports it, e.g. "8.4.0" instead of "8.4".', + ); + } + + return new MySQL84Platform(); + } + + if (version_compare($oracleMysqlVersion, '8', '>=')) { + if (! version_compare($version, '8.0.0', '>=')) { + Deprecation::trigger( + 'doctrine/orm', + 'https://github.com/doctrine/dbal/pull/5779', + 'Version detection logic for MySQL will change in DBAL 4. ' + . 'Please specify the version as the server reports it, e.g. "8.0.31" instead of "8".', + ); + } + + return new MySQL80Platform(); + } + + if (version_compare($oracleMysqlVersion, '5.7.9', '>=')) { + if (! version_compare($version, '5.7.9', '>=')) { + Deprecation::trigger( + 'doctrine/orm', + 'https://github.com/doctrine/dbal/pull/5779', + 'Version detection logic for MySQL will change in DBAL 4. ' + . 'Please specify the version as the server reports it, e.g. "5.7.40" instead of "5.7".', + ); + } + + return new MySQL57Platform(); + } + + Deprecation::trigger( + 'doctrine/dbal', + 'https://github.com/doctrine/dbal/pull/5072', + 'MySQL 5.6 support is deprecated and will be removed in DBAL 4.' + . ' Consider upgrading to MySQL 5.7 or later.', + ); + } + + return $this->getDatabasePlatform(); + } + + /** + * Get a normalized 'version number' from the server string + * returned by Oracle MySQL servers. + * + * @param string $versionString Version string returned by the driver, i.e. '5.7.10' + * + * @throws Exception + */ + private function getOracleMysqlVersionNumber(string $versionString): string + { + if ( + preg_match( + '/^(?P\d+)(?:\.(?P\d+)(?:\.(?P\d+))?)?/', + $versionString, + $versionParts, + ) !== 1 + ) { + throw Exception::invalidPlatformVersionSpecified( + $versionString, + '..', + ); + } + + $majorVersion = $versionParts['major']; + $minorVersion = $versionParts['minor'] ?? 0; + $patchVersion = $versionParts['patch'] ?? null; + + if ($majorVersion === '5' && $minorVersion === '7') { + $patchVersion ??= '9'; + } else { + $patchVersion ??= '0'; + } + + return $majorVersion . '.' . $minorVersion . '.' . $patchVersion; + } + + /** + * Detect MariaDB server version, including hack for some mariadb distributions + * that starts with the prefix '5.5.5-' + * + * @param string $versionString Version string as returned by mariadb server, i.e. '5.5.5-Mariadb-10.0.8-xenial' + * + * @throws Exception + */ + private function getMariaDbMysqlVersionNumber(string $versionString): string + { + if (stripos($versionString, 'MariaDB') === 0) { + Deprecation::trigger( + 'doctrine/orm', + 'https://github.com/doctrine/dbal/pull/5779', + 'Version detection logic for MySQL will change in DBAL 4. ' + . 'Please specify the version as the server reports it, ' + . 'e.g. "10.9.3-MariaDB" instead of "mariadb-10.9".', + ); + } + + if ( + preg_match( + '/^(?:5\.5\.5-)?(mariadb-)?(?P\d+)\.(?P\d+)\.(?P\d+)/i', + $versionString, + $versionParts, + ) !== 1 + ) { + throw Exception::invalidPlatformVersionSpecified( + $versionString, + '^(?:5\.5\.5-)?(mariadb-)?..', + ); + } + + return $versionParts['major'] . '.' . $versionParts['minor'] . '.' . $versionParts['patch']; + } + + /** + * {@inheritDoc} + * + * @return AbstractMySQLPlatform + */ + public function getDatabasePlatform() + { + return new MySQLPlatform(); + } + + /** + * {@inheritDoc} + * + * @deprecated Use {@link AbstractMySQLPlatform::createSchemaManager()} instead. + * + * @return MySQLSchemaManager + */ + public function getSchemaManager(Connection $conn, AbstractPlatform $platform) + { + Deprecation::triggerIfCalledFromOutside( + 'doctrine/dbal', + 'https://github.com/doctrine/dbal/pull/5458', + 'AbstractMySQLDriver::getSchemaManager() is deprecated.' + . ' Use MySQLPlatform::createSchemaManager() instead.', + ); + + assert($platform instanceof AbstractMySQLPlatform); + + return new MySQLSchemaManager($conn, $platform); + } + + public function getExceptionConverter(): ExceptionConverter + { + return new MySQL\ExceptionConverter(); + } +} diff --git a/3rdparty/doctrine/dbal/src/Driver/AbstractOracleDriver.php b/3rdparty/doctrine/dbal/src/Driver/AbstractOracleDriver.php new file mode 100644 index 00000000..b0f92453 --- /dev/null +++ b/3rdparty/doctrine/dbal/src/Driver/AbstractOracleDriver.php @@ -0,0 +1,65 @@ + $params The connection parameters to return the Easy Connect String for. + * + * @return string + */ + protected function getEasyConnectString(array $params) + { + return (string) EasyConnectString::fromConnectionParameters($params); + } +} diff --git a/3rdparty/doctrine/dbal/src/Driver/AbstractOracleDriver/EasyConnectString.php b/3rdparty/doctrine/dbal/src/Driver/AbstractOracleDriver/EasyConnectString.php new file mode 100644 index 00000000..91bc6a7e --- /dev/null +++ b/3rdparty/doctrine/dbal/src/Driver/AbstractOracleDriver/EasyConnectString.php @@ -0,0 +1,116 @@ +string = $string; + } + + public function __toString(): string + { + return $this->string; + } + + /** + * Creates the object from an array representation + * + * @param mixed[] $params + */ + public static function fromArray(array $params): self + { + return new self(self::renderParams($params)); + } + + /** + * Creates the object from the given DBAL connection parameters. + * + * @param mixed[] $params + */ + public static function fromConnectionParameters(array $params): self + { + if (isset($params['connectstring'])) { + return new self($params['connectstring']); + } + + if (! isset($params['host'])) { + return new self($params['dbname'] ?? ''); + } + + $connectData = []; + + if (isset($params['servicename']) || isset($params['dbname'])) { + $serviceKey = 'SID'; + + if (isset($params['service'])) { + $serviceKey = 'SERVICE_NAME'; + } + + $serviceName = $params['servicename'] ?? $params['dbname']; + + $connectData[$serviceKey] = $serviceName; + } + + if (isset($params['instancename'])) { + $connectData['INSTANCE_NAME'] = $params['instancename']; + } + + if (! empty($params['pooled'])) { + $connectData['SERVER'] = 'POOLED'; + } + + return self::fromArray([ + 'DESCRIPTION' => [ + 'ADDRESS' => [ + 'PROTOCOL' => 'TCP', + 'HOST' => $params['host'], + 'PORT' => $params['port'] ?? 1521, + ], + 'CONNECT_DATA' => $connectData, + ], + ]); + } + + /** @param mixed[] $params */ + private static function renderParams(array $params): string + { + $chunks = []; + + foreach ($params as $key => $value) { + $string = self::renderValue($value); + + if ($string === '') { + continue; + } + + $chunks[] = sprintf('(%s=%s)', $key, $string); + } + + return implode('', $chunks); + } + + /** @param mixed $value */ + private static function renderValue($value): string + { + if (is_array($value)) { + return self::renderParams($value); + } + + return (string) $value; + } +} diff --git a/3rdparty/doctrine/dbal/src/Driver/AbstractPostgreSQLDriver.php b/3rdparty/doctrine/dbal/src/Driver/AbstractPostgreSQLDriver.php new file mode 100644 index 00000000..eba309da --- /dev/null +++ b/3rdparty/doctrine/dbal/src/Driver/AbstractPostgreSQLDriver.php @@ -0,0 +1,93 @@ +\d+)(?:\.(?P\d+)(?:\.(?P\d+))?)?/', $version, $versionParts) !== 1) { + throw Exception::invalidPlatformVersionSpecified( + $version, + '..', + ); + } + + $majorVersion = $versionParts['major']; + $minorVersion = $versionParts['minor'] ?? 0; + $patchVersion = $versionParts['patch'] ?? 0; + $version = $majorVersion . '.' . $minorVersion . '.' . $patchVersion; + + if (version_compare($version, '12.0', '>=')) { + return new PostgreSQL120Platform(); + } + + if (version_compare($version, '10.0', '>=')) { + return new PostgreSQL100Platform(); + } + + Deprecation::trigger( + 'doctrine/dbal', + 'https://github.com/doctrine/dbal/pull/5060', + 'PostgreSQL 9 support is deprecated and will be removed in DBAL 4.' + . ' Consider upgrading to Postgres 10 or later.', + ); + + return new PostgreSQL94Platform(); + } + + /** + * {@inheritDoc} + */ + public function getDatabasePlatform() + { + return new PostgreSQL94Platform(); + } + + /** + * {@inheritDoc} + * + * @deprecated Use {@link PostgreSQLPlatform::createSchemaManager()} instead. + */ + public function getSchemaManager(Connection $conn, AbstractPlatform $platform) + { + Deprecation::triggerIfCalledFromOutside( + 'doctrine/dbal', + 'https://github.com/doctrine/dbal/pull/5458', + 'AbstractPostgreSQLDriver::getSchemaManager() is deprecated.' + . ' Use PostgreSQLPlatform::createSchemaManager() instead.', + ); + + assert($platform instanceof PostgreSQLPlatform); + + return new PostgreSQLSchemaManager($conn, $platform); + } + + public function getExceptionConverter(): ExceptionConverter + { + return new PostgreSQL\ExceptionConverter(); + } +} diff --git a/3rdparty/doctrine/dbal/src/Driver/AbstractSQLServerDriver.php b/3rdparty/doctrine/dbal/src/Driver/AbstractSQLServerDriver.php new file mode 100644 index 00000000..b9a99552 --- /dev/null +++ b/3rdparty/doctrine/dbal/src/Driver/AbstractSQLServerDriver.php @@ -0,0 +1,53 @@ +exec('PRAGMA foreign_keys=ON'); + + return $connection; + } + }; + } +} diff --git a/3rdparty/doctrine/dbal/src/Driver/Connection.php b/3rdparty/doctrine/dbal/src/Driver/Connection.php new file mode 100644 index 00000000..2f460fd1 --- /dev/null +++ b/3rdparty/doctrine/dbal/src/Driver/Connection.php @@ -0,0 +1,86 @@ +fetchNumeric(); + + if ($row === false) { + return false; + } + + return $row[0]; + } + + /** + * @return list> + * + * @throws Exception + */ + public static function fetchAllNumeric(Result $result): array + { + $rows = []; + + while (($row = $result->fetchNumeric()) !== false) { + $rows[] = $row; + } + + return $rows; + } + + /** + * @return list> + * + * @throws Exception + */ + public static function fetchAllAssociative(Result $result): array + { + $rows = []; + + while (($row = $result->fetchAssociative()) !== false) { + $rows[] = $row; + } + + return $rows; + } + + /** + * @return list + * + * @throws Exception + */ + public static function fetchFirstColumn(Result $result): array + { + $rows = []; + + while (($row = $result->fetchOne()) !== false) { + $rows[] = $row; + } + + return $rows; + } +} diff --git a/3rdparty/doctrine/dbal/src/Driver/IBMDB2/Connection.php b/3rdparty/doctrine/dbal/src/Driver/IBMDB2/Connection.php new file mode 100644 index 00000000..dfb11c23 --- /dev/null +++ b/3rdparty/doctrine/dbal/src/Driver/IBMDB2/Connection.php @@ -0,0 +1,141 @@ +connection = $connection; + } + + /** + * {@inheritDoc} + */ + public function getServerVersion() + { + $serverInfo = db2_server_info($this->connection); + assert($serverInfo instanceof stdClass); + + return $serverInfo->DBMS_VER; + } + + public function prepare(string $sql): DriverStatement + { + $stmt = @db2_prepare($this->connection, $sql); + + if ($stmt === false) { + throw PrepareFailed::new(error_get_last()); + } + + return new Statement($stmt); + } + + public function query(string $sql): ResultInterface + { + return $this->prepare($sql)->execute(); + } + + /** + * {@inheritDoc} + */ + public function quote($value, $type = ParameterType::STRING) + { + $value = db2_escape_string($value); + + if ($type === ParameterType::INTEGER) { + return $value; + } + + return "'" . $value . "'"; + } + + public function exec(string $sql): int + { + $stmt = @db2_exec($this->connection, $sql); + + if ($stmt === false) { + throw StatementError::new(); + } + + return db2_num_rows($stmt); + } + + /** + * {@inheritDoc} + */ + public function lastInsertId($name = null) + { + if ($name !== null) { + Deprecation::triggerIfCalledFromOutside( + 'doctrine/dbal', + 'https://github.com/doctrine/dbal/issues/4687', + 'The usage of Connection::lastInsertId() with a sequence name is deprecated.', + ); + } + + return db2_last_insert_id($this->connection) ?? false; + } + + public function beginTransaction(): bool + { + return db2_autocommit($this->connection, DB2_AUTOCOMMIT_OFF); + } + + public function commit(): bool + { + if (! db2_commit($this->connection)) { + throw ConnectionError::new($this->connection); + } + + return db2_autocommit($this->connection, DB2_AUTOCOMMIT_ON); + } + + public function rollBack(): bool + { + if (! db2_rollback($this->connection)) { + throw ConnectionError::new($this->connection); + } + + return db2_autocommit($this->connection, DB2_AUTOCOMMIT_ON); + } + + /** @return resource */ + public function getNativeConnection() + { + return $this->connection; + } +} diff --git a/3rdparty/doctrine/dbal/src/Driver/IBMDB2/DataSourceName.php b/3rdparty/doctrine/dbal/src/Driver/IBMDB2/DataSourceName.php new file mode 100644 index 00000000..124a6f6d --- /dev/null +++ b/3rdparty/doctrine/dbal/src/Driver/IBMDB2/DataSourceName.php @@ -0,0 +1,84 @@ +string = $string; + } + + public function toString(): string + { + return $this->string; + } + + /** + * Creates the object from an array representation + * + * @param array $params + */ + public static function fromArray( + #[SensitiveParameter] + array $params + ): self { + $chunks = []; + + foreach ($params as $key => $value) { + $chunks[] = sprintf('%s=%s', $key, $value); + } + + return new self(implode(';', $chunks)); + } + + /** + * Creates the object from the given DBAL connection parameters. + * + * @param array $params + */ + public static function fromConnectionParameters( + #[SensitiveParameter] + array $params + ): self { + if (isset($params['dbname']) && strpos($params['dbname'], '=') !== false) { + return new self($params['dbname']); + } + + $dsnParams = []; + + foreach ( + [ + 'host' => 'HOSTNAME', + 'port' => 'PORT', + 'protocol' => 'PROTOCOL', + 'dbname' => 'DATABASE', + 'user' => 'UID', + 'password' => 'PWD', + ] as $dbalParam => $dsnParam + ) { + if (! isset($params[$dbalParam])) { + continue; + } + + $dsnParams[$dsnParam] = $params[$dbalParam]; + } + + return self::fromArray($dsnParams); + } +} diff --git a/3rdparty/doctrine/dbal/src/Driver/IBMDB2/Driver.php b/3rdparty/doctrine/dbal/src/Driver/IBMDB2/Driver.php new file mode 100644 index 00000000..7650db5f --- /dev/null +++ b/3rdparty/doctrine/dbal/src/Driver/IBMDB2/Driver.php @@ -0,0 +1,41 @@ +toString(); + + $username = $params['user'] ?? ''; + $password = $params['password'] ?? ''; + $driverOptions = $params['driverOptions'] ?? []; + + if (! empty($params['persistent'])) { + $connection = db2_pconnect($dataSourceName, $username, $password, $driverOptions); + } else { + $connection = db2_connect($dataSourceName, $username, $password, $driverOptions); + } + + if ($connection === false) { + throw ConnectionFailed::new(); + } + + return new Connection($connection); + } +} diff --git a/3rdparty/doctrine/dbal/src/Driver/IBMDB2/Exception/CannotCopyStreamToStream.php b/3rdparty/doctrine/dbal/src/Driver/IBMDB2/Exception/CannotCopyStreamToStream.php new file mode 100644 index 00000000..05099faf --- /dev/null +++ b/3rdparty/doctrine/dbal/src/Driver/IBMDB2/Exception/CannotCopyStreamToStream.php @@ -0,0 +1,23 @@ +statement = $statement; + } + + /** + * {@inheritDoc} + */ + public function fetchNumeric() + { + $row = @db2_fetch_array($this->statement); + + if ($row === false && db2_stmt_error($this->statement) !== '02000') { + throw StatementError::new($this->statement); + } + + return $row; + } + + /** + * {@inheritDoc} + */ + public function fetchAssociative() + { + $row = @db2_fetch_assoc($this->statement); + + if ($row === false && db2_stmt_error($this->statement) !== '02000') { + throw StatementError::new($this->statement); + } + + return $row; + } + + /** + * {@inheritDoc} + */ + public function fetchOne() + { + return FetchUtils::fetchOne($this); + } + + /** + * {@inheritDoc} + */ + public function fetchAllNumeric(): array + { + return FetchUtils::fetchAllNumeric($this); + } + + /** + * {@inheritDoc} + */ + public function fetchAllAssociative(): array + { + return FetchUtils::fetchAllAssociative($this); + } + + /** + * {@inheritDoc} + */ + public function fetchFirstColumn(): array + { + return FetchUtils::fetchFirstColumn($this); + } + + public function rowCount(): int + { + return @db2_num_rows($this->statement); + } + + public function columnCount(): int + { + $count = db2_num_fields($this->statement); + + if ($count !== false) { + return $count; + } + + return 0; + } + + public function free(): void + { + db2_free_result($this->statement); + } +} diff --git a/3rdparty/doctrine/dbal/src/Driver/IBMDB2/Statement.php b/3rdparty/doctrine/dbal/src/Driver/IBMDB2/Statement.php new file mode 100644 index 00000000..699e236d --- /dev/null +++ b/3rdparty/doctrine/dbal/src/Driver/IBMDB2/Statement.php @@ -0,0 +1,220 @@ + + */ + private array $lobs = []; + + /** + * @internal The statement can be only instantiated by its driver connection. + * + * @param resource $stmt + */ + public function __construct($stmt) + { + $this->stmt = $stmt; + } + + /** + * {@inheritDoc} + */ + public function bindValue($param, $value, $type = ParameterType::STRING): bool + { + assert(is_int($param)); + + if (func_num_args() < 3) { + Deprecation::trigger( + 'doctrine/dbal', + 'https://github.com/doctrine/dbal/pull/5558', + 'Not passing $type to Statement::bindValue() is deprecated.' + . ' Pass the type corresponding to the parameter being bound.', + ); + } + + return $this->bindParam($param, $value, $type); + } + + /** + * {@inheritDoc} + * + * @deprecated Use {@see bindValue()} instead. + */ + public function bindParam($param, &$variable, $type = ParameterType::STRING, $length = null): bool + { + Deprecation::trigger( + 'doctrine/dbal', + 'https://github.com/doctrine/dbal/pull/5563', + '%s is deprecated. Use bindValue() instead.', + __METHOD__, + ); + + assert(is_int($param)); + + if (func_num_args() < 3) { + Deprecation::trigger( + 'doctrine/dbal', + 'https://github.com/doctrine/dbal/pull/5558', + 'Not passing $type to Statement::bindParam() is deprecated.' + . ' Pass the type corresponding to the parameter being bound.', + ); + } + + switch ($type) { + case ParameterType::INTEGER: + $this->bind($param, $variable, DB2_PARAM_IN, DB2_LONG); + break; + + case ParameterType::LARGE_OBJECT: + $this->lobs[$param] = &$variable; + break; + + default: + $this->bind($param, $variable, DB2_PARAM_IN, DB2_CHAR); + break; + } + + return true; + } + + /** + * @param int $position Parameter position + * @param mixed $variable + * + * @throws Exception + */ + private function bind($position, &$variable, int $parameterType, int $dataType): void + { + $this->parameters[$position] =& $variable; + + if (! db2_bind_param($this->stmt, $position, '', $parameterType, $dataType)) { + throw StatementError::new($this->stmt); + } + } + + /** + * {@inheritDoc} + */ + public function execute($params = null): ResultInterface + { + if ($params !== null) { + Deprecation::trigger( + 'doctrine/dbal', + 'https://github.com/doctrine/dbal/pull/5556', + 'Passing $params to Statement::execute() is deprecated. Bind parameters using' + . ' Statement::bindParam() or Statement::bindValue() instead.', + ); + } + + $handles = $this->bindLobs(); + + $result = @db2_execute($this->stmt, $params ?? $this->parameters); + + foreach ($handles as $handle) { + fclose($handle); + } + + $this->lobs = []; + + if ($result === false) { + throw StatementError::new($this->stmt); + } + + return new Result($this->stmt); + } + + /** + * @return list + * + * @throws Exception + */ + private function bindLobs(): array + { + $handles = []; + + foreach ($this->lobs as $param => $value) { + if (is_resource($value)) { + $handle = $handles[] = $this->createTemporaryFile(); + $path = stream_get_meta_data($handle)['uri']; + + $this->copyStreamToStream($value, $handle); + + $this->bind($param, $path, DB2_PARAM_FILE, DB2_BINARY); + } else { + $this->bind($param, $value, DB2_PARAM_IN, DB2_CHAR); + } + + unset($value); + } + + return $handles; + } + + /** + * @return resource + * + * @throws Exception + */ + private function createTemporaryFile() + { + $handle = @tmpfile(); + + if ($handle === false) { + throw CannotCreateTemporaryFile::new(error_get_last()); + } + + return $handle; + } + + /** + * @param resource $source + * @param resource $target + * + * @throws Exception + */ + private function copyStreamToStream($source, $target): void + { + if (@stream_copy_to_stream($source, $target) === false) { + throw CannotCopyStreamToStream::new(error_get_last()); + } + } +} diff --git a/3rdparty/doctrine/dbal/src/Driver/Middleware.php b/3rdparty/doctrine/dbal/src/Driver/Middleware.php new file mode 100644 index 00000000..4629d9a8 --- /dev/null +++ b/3rdparty/doctrine/dbal/src/Driver/Middleware.php @@ -0,0 +1,12 @@ +wrappedConnection = $wrappedConnection; + } + + public function prepare(string $sql): Statement + { + return $this->wrappedConnection->prepare($sql); + } + + public function query(string $sql): Result + { + return $this->wrappedConnection->query($sql); + } + + /** + * {@inheritDoc} + */ + public function quote($value, $type = ParameterType::STRING) + { + return $this->wrappedConnection->quote($value, $type); + } + + public function exec(string $sql): int + { + return $this->wrappedConnection->exec($sql); + } + + /** + * {@inheritDoc} + */ + public function lastInsertId($name = null) + { + if ($name !== null) { + Deprecation::triggerIfCalledFromOutside( + 'doctrine/dbal', + 'https://github.com/doctrine/dbal/issues/4687', + 'The usage of Connection::lastInsertId() with a sequence name is deprecated.', + ); + } + + return $this->wrappedConnection->lastInsertId($name); + } + + /** + * {@inheritDoc} + */ + public function beginTransaction() + { + return $this->wrappedConnection->beginTransaction(); + } + + /** + * {@inheritDoc} + */ + public function commit() + { + return $this->wrappedConnection->commit(); + } + + /** + * {@inheritDoc} + */ + public function rollBack() + { + return $this->wrappedConnection->rollBack(); + } + + /** + * {@inheritDoc} + */ + public function getServerVersion() + { + if (! $this->wrappedConnection instanceof ServerInfoAwareConnection) { + throw new LogicException('The underlying connection is not a ServerInfoAwareConnection'); + } + + return $this->wrappedConnection->getServerVersion(); + } + + /** @return resource|object */ + public function getNativeConnection() + { + if (! method_exists($this->wrappedConnection, 'getNativeConnection')) { + throw new LogicException(sprintf( + 'The driver connection %s does not support accessing the native connection.', + get_class($this->wrappedConnection), + )); + } + + return $this->wrappedConnection->getNativeConnection(); + } +} diff --git a/3rdparty/doctrine/dbal/src/Driver/Middleware/AbstractDriverMiddleware.php b/3rdparty/doctrine/dbal/src/Driver/Middleware/AbstractDriverMiddleware.php new file mode 100644 index 00000000..1c9d4309 --- /dev/null +++ b/3rdparty/doctrine/dbal/src/Driver/Middleware/AbstractDriverMiddleware.php @@ -0,0 +1,73 @@ +wrappedDriver = $wrappedDriver; + } + + /** + * {@inheritDoc} + */ + public function connect( + #[SensitiveParameter] + array $params + ) { + return $this->wrappedDriver->connect($params); + } + + /** + * {@inheritDoc} + */ + public function getDatabasePlatform() + { + return $this->wrappedDriver->getDatabasePlatform(); + } + + /** + * {@inheritDoc} + * + * @deprecated Use {@link AbstractPlatform::createSchemaManager()} instead. + */ + public function getSchemaManager(Connection $conn, AbstractPlatform $platform) + { + Deprecation::triggerIfCalledFromOutside( + 'doctrine/dbal', + 'https://github.com/doctrine/dbal/pull/5458', + 'AbstractDriverMiddleware::getSchemaManager() is deprecated.' + . ' Use AbstractPlatform::createSchemaManager() instead.', + ); + + return $this->wrappedDriver->getSchemaManager($conn, $platform); + } + + public function getExceptionConverter(): ExceptionConverter + { + return $this->wrappedDriver->getExceptionConverter(); + } + + /** + * {@inheritDoc} + */ + public function createDatabasePlatformForVersion($version) + { + if ($this->wrappedDriver instanceof VersionAwarePlatformDriver) { + return $this->wrappedDriver->createDatabasePlatformForVersion($version); + } + + return $this->wrappedDriver->getDatabasePlatform(); + } +} diff --git a/3rdparty/doctrine/dbal/src/Driver/Middleware/AbstractResultMiddleware.php b/3rdparty/doctrine/dbal/src/Driver/Middleware/AbstractResultMiddleware.php new file mode 100644 index 00000000..198d39b0 --- /dev/null +++ b/3rdparty/doctrine/dbal/src/Driver/Middleware/AbstractResultMiddleware.php @@ -0,0 +1,78 @@ +wrappedResult = $result; + } + + /** + * {@inheritDoc} + */ + public function fetchNumeric() + { + return $this->wrappedResult->fetchNumeric(); + } + + /** + * {@inheritDoc} + */ + public function fetchAssociative() + { + return $this->wrappedResult->fetchAssociative(); + } + + /** + * {@inheritDoc} + */ + public function fetchOne() + { + return $this->wrappedResult->fetchOne(); + } + + /** + * {@inheritDoc} + */ + public function fetchAllNumeric(): array + { + return $this->wrappedResult->fetchAllNumeric(); + } + + /** + * {@inheritDoc} + */ + public function fetchAllAssociative(): array + { + return $this->wrappedResult->fetchAllAssociative(); + } + + /** + * {@inheritDoc} + */ + public function fetchFirstColumn(): array + { + return $this->wrappedResult->fetchFirstColumn(); + } + + public function rowCount(): int + { + return $this->wrappedResult->rowCount(); + } + + public function columnCount(): int + { + return $this->wrappedResult->columnCount(); + } + + public function free(): void + { + $this->wrappedResult->free(); + } +} diff --git a/3rdparty/doctrine/dbal/src/Driver/Middleware/AbstractStatementMiddleware.php b/3rdparty/doctrine/dbal/src/Driver/Middleware/AbstractStatementMiddleware.php new file mode 100644 index 00000000..6cd2f8f0 --- /dev/null +++ b/3rdparty/doctrine/dbal/src/Driver/Middleware/AbstractStatementMiddleware.php @@ -0,0 +1,71 @@ +wrappedStatement = $wrappedStatement; + } + + /** + * {@inheritDoc} + */ + public function bindValue($param, $value, $type = ParameterType::STRING) + { + if (func_num_args() < 3) { + Deprecation::trigger( + 'doctrine/dbal', + 'https://github.com/doctrine/dbal/pull/5558', + 'Not passing $type to Statement::bindValue() is deprecated.' + . ' Pass the type corresponding to the parameter being bound.', + ); + } + + return $this->wrappedStatement->bindValue($param, $value, $type); + } + + /** + * {@inheritDoc} + * + * @deprecated Use {@see bindValue()} instead. + */ + public function bindParam($param, &$variable, $type = ParameterType::STRING, $length = null) + { + Deprecation::trigger( + 'doctrine/dbal', + 'https://github.com/doctrine/dbal/pull/5563', + '%s is deprecated. Use bindValue() instead.', + __METHOD__, + ); + + if (func_num_args() < 3) { + Deprecation::trigger( + 'doctrine/dbal', + 'https://github.com/doctrine/dbal/pull/5558', + 'Not passing $type to Statement::bindParam() is deprecated.' + . ' Pass the type corresponding to the parameter being bound.', + ); + } + + return $this->wrappedStatement->bindParam($param, $variable, $type, $length); + } + + /** + * {@inheritDoc} + */ + public function execute($params = null): Result + { + return $this->wrappedStatement->execute($params); + } +} diff --git a/3rdparty/doctrine/dbal/src/Driver/Mysqli/Connection.php b/3rdparty/doctrine/dbal/src/Driver/Mysqli/Connection.php new file mode 100644 index 00000000..d492684c --- /dev/null +++ b/3rdparty/doctrine/dbal/src/Driver/Mysqli/Connection.php @@ -0,0 +1,141 @@ +connection = $connection; + } + + /** + * Retrieves mysqli native resource handle. + * + * Could be used if part of your application is not using DBAL. + * + * @deprecated Call {@see getNativeConnection()} instead. + */ + public function getWrappedResourceHandle(): mysqli + { + Deprecation::trigger( + 'doctrine/dbal', + 'https://github.com/doctrine/dbal/pull/5037', + '%s is deprecated, call getNativeConnection() instead.', + __METHOD__, + ); + + return $this->getNativeConnection(); + } + + public function getServerVersion(): string + { + return $this->connection->get_server_info(); + } + + public function prepare(string $sql): DriverStatement + { + try { + $stmt = $this->connection->prepare($sql); + } catch (mysqli_sql_exception $e) { + throw ConnectionError::upcast($e); + } + + if ($stmt === false) { + throw ConnectionError::new($this->connection); + } + + return new Statement($stmt); + } + + public function query(string $sql): ResultInterface + { + return $this->prepare($sql)->execute(); + } + + /** + * {@inheritDoc} + */ + public function quote($value, $type = ParameterType::STRING) + { + return "'" . $this->connection->escape_string($value) . "'"; + } + + public function exec(string $sql): int + { + try { + $result = $this->connection->query($sql); + } catch (mysqli_sql_exception $e) { + throw ConnectionError::upcast($e); + } + + if ($result === false) { + throw ConnectionError::new($this->connection); + } + + return $this->connection->affected_rows; + } + + /** + * {@inheritDoc} + */ + public function lastInsertId($name = null) + { + if ($name !== null) { + Deprecation::triggerIfCalledFromOutside( + 'doctrine/dbal', + 'https://github.com/doctrine/dbal/issues/4687', + 'The usage of Connection::lastInsertId() with a sequence name is deprecated.', + ); + } + + return $this->connection->insert_id; + } + + public function beginTransaction(): bool + { + $this->connection->begin_transaction(); + + return true; + } + + public function commit(): bool + { + try { + return $this->connection->commit(); + } catch (mysqli_sql_exception $e) { + return false; + } + } + + public function rollBack(): bool + { + try { + return $this->connection->rollback(); + } catch (mysqli_sql_exception $e) { + return false; + } + } + + public function getNativeConnection(): mysqli + { + return $this->connection; + } +} diff --git a/3rdparty/doctrine/dbal/src/Driver/Mysqli/Driver.php b/3rdparty/doctrine/dbal/src/Driver/Mysqli/Driver.php new file mode 100644 index 00000000..4f518687 --- /dev/null +++ b/3rdparty/doctrine/dbal/src/Driver/Mysqli/Driver.php @@ -0,0 +1,117 @@ +compilePreInitializers($params) as $initializer) { + $initializer->initialize($connection); + } + + try { + $success = @$connection->real_connect( + $host, + $params['user'] ?? null, + $params['password'] ?? null, + $params['dbname'] ?? null, + $params['port'] ?? null, + $params['unix_socket'] ?? null, + $params['driverOptions'][Connection::OPTION_FLAGS] ?? 0, + ); + } catch (mysqli_sql_exception $e) { + throw ConnectionFailed::upcast($e); + } + + if (! $success) { + throw ConnectionFailed::new($connection); + } + + foreach ($this->compilePostInitializers($params) as $initializer) { + $initializer->initialize($connection); + } + + return new Connection($connection); + } + + /** + * @param array $params + * + * @return Generator + */ + private function compilePreInitializers( + #[SensitiveParameter] + array $params + ): Generator { + unset($params['driverOptions'][Connection::OPTION_FLAGS]); + + if (isset($params['driverOptions']) && $params['driverOptions'] !== []) { + yield new Options($params['driverOptions']); + } + + if ( + ! isset($params['ssl_key']) && + ! isset($params['ssl_cert']) && + ! isset($params['ssl_ca']) && + ! isset($params['ssl_capath']) && + ! isset($params['ssl_cipher']) + ) { + return; + } + + yield new Secure( + $params['ssl_key'] ?? '', + $params['ssl_cert'] ?? '', + $params['ssl_ca'] ?? '', + $params['ssl_capath'] ?? '', + $params['ssl_cipher'] ?? '', + ); + } + + /** + * @param array $params + * + * @return Generator + */ + private function compilePostInitializers( + #[SensitiveParameter] + array $params + ): Generator { + if (! isset($params['charset'])) { + return; + } + + yield new Charset($params['charset']); + } +} diff --git a/3rdparty/doctrine/dbal/src/Driver/Mysqli/Exception/ConnectionError.php b/3rdparty/doctrine/dbal/src/Driver/Mysqli/Exception/ConnectionError.php new file mode 100644 index 00000000..f5755932 --- /dev/null +++ b/3rdparty/doctrine/dbal/src/Driver/Mysqli/Exception/ConnectionError.php @@ -0,0 +1,27 @@ +error, $connection->sqlstate, $connection->errno); + } + + public static function upcast(mysqli_sql_exception $exception): self + { + $p = new ReflectionProperty(mysqli_sql_exception::class, 'sqlstate'); + $p->setAccessible(true); + + return new self($exception->getMessage(), $p->getValue($exception), (int) $exception->getCode(), $exception); + } +} diff --git a/3rdparty/doctrine/dbal/src/Driver/Mysqli/Exception/ConnectionFailed.php b/3rdparty/doctrine/dbal/src/Driver/Mysqli/Exception/ConnectionFailed.php new file mode 100644 index 00000000..051f141f --- /dev/null +++ b/3rdparty/doctrine/dbal/src/Driver/Mysqli/Exception/ConnectionFailed.php @@ -0,0 +1,32 @@ +connect_error; + assert($error !== null); + + return new self($error, 'HY000', $connection->connect_errno); + } + + public static function upcast(mysqli_sql_exception $exception): self + { + $p = new ReflectionProperty(mysqli_sql_exception::class, 'sqlstate'); + $p->setAccessible(true); + + return new self($exception->getMessage(), $p->getValue($exception), (int) $exception->getCode(), $exception); + } +} diff --git a/3rdparty/doctrine/dbal/src/Driver/Mysqli/Exception/FailedReadingStreamOffset.php b/3rdparty/doctrine/dbal/src/Driver/Mysqli/Exception/FailedReadingStreamOffset.php new file mode 100644 index 00000000..f20d8bc1 --- /dev/null +++ b/3rdparty/doctrine/dbal/src/Driver/Mysqli/Exception/FailedReadingStreamOffset.php @@ -0,0 +1,18 @@ +error), + $connection->sqlstate, + $connection->errno, + ); + } + + public static function upcast(mysqli_sql_exception $exception, string $charset): self + { + $p = new ReflectionProperty(mysqli_sql_exception::class, 'sqlstate'); + $p->setAccessible(true); + + return new self( + sprintf('Failed to set charset "%s": %s', $charset, $exception->getMessage()), + $p->getValue($exception), + (int) $exception->getCode(), + $exception, + ); + } +} diff --git a/3rdparty/doctrine/dbal/src/Driver/Mysqli/Exception/InvalidOption.php b/3rdparty/doctrine/dbal/src/Driver/Mysqli/Exception/InvalidOption.php new file mode 100644 index 00000000..abbda01c --- /dev/null +++ b/3rdparty/doctrine/dbal/src/Driver/Mysqli/Exception/InvalidOption.php @@ -0,0 +1,21 @@ +error, $statement->sqlstate, $statement->errno); + } + + public static function upcast(mysqli_sql_exception $exception): self + { + $p = new ReflectionProperty(mysqli_sql_exception::class, 'sqlstate'); + $p->setAccessible(true); + + return new self($exception->getMessage(), $p->getValue($exception), (int) $exception->getCode(), $exception); + } +} diff --git a/3rdparty/doctrine/dbal/src/Driver/Mysqli/Initializer.php b/3rdparty/doctrine/dbal/src/Driver/Mysqli/Initializer.php new file mode 100644 index 00000000..efab67e2 --- /dev/null +++ b/3rdparty/doctrine/dbal/src/Driver/Mysqli/Initializer.php @@ -0,0 +1,14 @@ +charset = $charset; + } + + public function initialize(mysqli $connection): void + { + try { + $success = $connection->set_charset($this->charset); + } catch (mysqli_sql_exception $e) { + throw InvalidCharset::upcast($e, $this->charset); + } + + if ($success) { + return; + } + + throw InvalidCharset::fromCharset($connection, $this->charset); + } +} diff --git a/3rdparty/doctrine/dbal/src/Driver/Mysqli/Initializer/Options.php b/3rdparty/doctrine/dbal/src/Driver/Mysqli/Initializer/Options.php new file mode 100644 index 00000000..2e66f8d6 --- /dev/null +++ b/3rdparty/doctrine/dbal/src/Driver/Mysqli/Initializer/Options.php @@ -0,0 +1,32 @@ + */ + private array $options; + + /** @param array $options */ + public function __construct(array $options) + { + $this->options = $options; + } + + public function initialize(mysqli $connection): void + { + foreach ($this->options as $option => $value) { + if (! mysqli_options($connection, $option, $value)) { + throw InvalidOption::fromOption($option, $value); + } + } + } +} diff --git a/3rdparty/doctrine/dbal/src/Driver/Mysqli/Initializer/Secure.php b/3rdparty/doctrine/dbal/src/Driver/Mysqli/Initializer/Secure.php new file mode 100644 index 00000000..a25fcfc2 --- /dev/null +++ b/3rdparty/doctrine/dbal/src/Driver/Mysqli/Initializer/Secure.php @@ -0,0 +1,38 @@ +key = $key; + $this->cert = $cert; + $this->ca = $ca; + $this->capath = $capath; + $this->cipher = $cipher; + } + + public function initialize(mysqli $connection): void + { + $connection->ssl_set($this->key, $this->cert, $this->ca, $this->capath, $this->cipher); + } +} diff --git a/3rdparty/doctrine/dbal/src/Driver/Mysqli/Result.php b/3rdparty/doctrine/dbal/src/Driver/Mysqli/Result.php new file mode 100644 index 00000000..bfd558b4 --- /dev/null +++ b/3rdparty/doctrine/dbal/src/Driver/Mysqli/Result.php @@ -0,0 +1,191 @@ + + */ + private array $columnNames = []; + + /** @var mixed[] */ + private array $boundValues = []; + + /** + * @internal The result can be only instantiated by its driver connection or statement. + * + * @throws Exception + */ + public function __construct( + mysqli_stmt $statement, + ?Statement $statementReference = null + ) { + $this->statement = $statement; + $this->statementReference = $statementReference; + + $meta = $statement->result_metadata(); + + if ($meta === false) { + return; + } + + $this->hasColumns = true; + + $this->columnNames = array_column($meta->fetch_fields(), 'name'); + + $meta->free(); + + // Store result of every execution which has it. Otherwise it will be impossible + // to execute a new statement in case if the previous one has non-fetched rows + // @link http://dev.mysql.com/doc/refman/5.7/en/commands-out-of-sync.html + $this->statement->store_result(); + + // Bind row values _after_ storing the result. Otherwise, if mysqli is compiled with libmysql, + // it will have to allocate as much memory as it may be needed for the given column type + // (e.g. for a LONGBLOB column it's 4 gigabytes) + // @link https://bugs.php.net/bug.php?id=51386#1270673122 + // + // Make sure that the values are bound after each execution. Otherwise, if free() has been + // previously called on the result, the values are unbound making the statement unusable. + // + // It's also important that row values are bound after _each_ call to store_result(). Otherwise, + // if mysqli is compiled with libmysql, subsequently fetched string values will get truncated + // to the length of the ones fetched during the previous execution. + $this->boundValues = array_fill(0, count($this->columnNames), null); + + // The following is necessary as PHP cannot handle references to properties properly + $refs = &$this->boundValues; + + if (! $this->statement->bind_result(...$refs)) { + throw StatementError::new($this->statement); + } + } + + /** + * {@inheritDoc} + */ + public function fetchNumeric() + { + try { + $ret = $this->statement->fetch(); + } catch (mysqli_sql_exception $e) { + throw StatementError::upcast($e); + } + + if ($ret === false) { + throw StatementError::new($this->statement); + } + + if ($ret === null) { + return false; + } + + $values = []; + + foreach ($this->boundValues as $v) { + $values[] = $v; + } + + return $values; + } + + /** + * {@inheritDoc} + */ + public function fetchAssociative() + { + $values = $this->fetchNumeric(); + + if ($values === false) { + return false; + } + + return array_combine($this->columnNames, $values); + } + + /** + * {@inheritDoc} + */ + public function fetchOne() + { + return FetchUtils::fetchOne($this); + } + + /** + * {@inheritDoc} + */ + public function fetchAllNumeric(): array + { + return FetchUtils::fetchAllNumeric($this); + } + + /** + * {@inheritDoc} + */ + public function fetchAllAssociative(): array + { + return FetchUtils::fetchAllAssociative($this); + } + + /** + * {@inheritDoc} + */ + public function fetchFirstColumn(): array + { + return FetchUtils::fetchFirstColumn($this); + } + + public function rowCount(): int + { + if ($this->hasColumns) { + return $this->statement->num_rows; + } + + return $this->statement->affected_rows; + } + + public function columnCount(): int + { + return $this->statement->field_count; + } + + public function free(): void + { + $this->statement->free_result(); + } +} diff --git a/3rdparty/doctrine/dbal/src/Driver/Mysqli/Statement.php b/3rdparty/doctrine/dbal/src/Driver/Mysqli/Statement.php new file mode 100644 index 00000000..be555e3d --- /dev/null +++ b/3rdparty/doctrine/dbal/src/Driver/Mysqli/Statement.php @@ -0,0 +1,244 @@ + 's', + ParameterType::STRING => 's', + ParameterType::BINARY => 's', + ParameterType::BOOLEAN => 'i', + ParameterType::NULL => 's', + ParameterType::INTEGER => 'i', + ParameterType::LARGE_OBJECT => 'b', + ]; + + private mysqli_stmt $stmt; + + /** @var mixed[] */ + private array $boundValues; + + private string $types; + + /** + * Contains ref values for bindValue(). + * + * @var mixed[] + */ + private array $values = []; + + /** @internal The statement can be only instantiated by its driver connection. */ + public function __construct(mysqli_stmt $stmt) + { + $this->stmt = $stmt; + + $paramCount = $this->stmt->param_count; + $this->types = str_repeat('s', $paramCount); + $this->boundValues = array_fill(1, $paramCount, null); + } + + public function __destruct() + { + @$this->stmt->close(); + } + + /** + * @deprecated Use {@see bindValue()} instead. + * + * {@inheritDoc} + * + * @phpstan-assert ParameterType::* $type + */ + public function bindParam($param, &$variable, $type = ParameterType::STRING, $length = null): bool + { + Deprecation::trigger( + 'doctrine/dbal', + 'https://github.com/doctrine/dbal/pull/5563', + '%s is deprecated. Use bindValue() instead.', + __METHOD__, + ); + + assert(is_int($param)); + + if (func_num_args() < 3) { + Deprecation::trigger( + 'doctrine/dbal', + 'https://github.com/doctrine/dbal/pull/5558', + 'Not passing $type to Statement::bindParam() is deprecated.' + . ' Pass the type corresponding to the parameter being bound.', + ); + } + + if (! isset(self::PARAM_TYPE_MAP[$type])) { + throw UnknownParameterType::new($type); + } + + $this->boundValues[$param] =& $variable; + $this->types[$param - 1] = self::PARAM_TYPE_MAP[$type]; + + return true; + } + + /** + * {@inheritDoc} + * + * @phpstan-assert ParameterType::* $type + */ + public function bindValue($param, $value, $type = ParameterType::STRING): bool + { + assert(is_int($param)); + + if (func_num_args() < 3) { + Deprecation::trigger( + 'doctrine/dbal', + 'https://github.com/doctrine/dbal/pull/5558', + 'Not passing $type to Statement::bindValue() is deprecated.' + . ' Pass the type corresponding to the parameter being bound.', + ); + } + + if (! isset(self::PARAM_TYPE_MAP[$type])) { + throw UnknownParameterType::new($type); + } + + $this->values[$param] = $value; + $this->boundValues[$param] =& $this->values[$param]; + $this->types[$param - 1] = self::PARAM_TYPE_MAP[$type]; + + return true; + } + + /** + * {@inheritDoc} + */ + public function execute($params = null): ResultInterface + { + if ($params !== null) { + Deprecation::trigger( + 'doctrine/dbal', + 'https://github.com/doctrine/dbal/pull/5556', + 'Passing $params to Statement::execute() is deprecated. Bind parameters using' + . ' Statement::bindParam() or Statement::bindValue() instead.', + ); + } + + if ($params !== null && count($params) > 0) { + if (! $this->bindUntypedValues($params)) { + throw StatementError::new($this->stmt); + } + } elseif (count($this->boundValues) > 0) { + $this->bindTypedParameters(); + } + + try { + $result = $this->stmt->execute(); + } catch (mysqli_sql_exception $e) { + throw StatementError::upcast($e); + } + + if (! $result) { + throw StatementError::new($this->stmt); + } + + return new Result($this->stmt, $this); + } + + /** + * Binds parameters with known types previously bound to the statement + * + * @throws Exception + */ + private function bindTypedParameters(): void + { + $streams = $values = []; + $types = $this->types; + + foreach ($this->boundValues as $parameter => $value) { + assert(is_int($parameter)); + + if (! isset($types[$parameter - 1])) { + $types[$parameter - 1] = self::PARAM_TYPE_MAP[ParameterType::STRING]; + } + + if ($types[$parameter - 1] === self::PARAM_TYPE_MAP[ParameterType::LARGE_OBJECT]) { + if (is_resource($value)) { + if (get_resource_type($value) !== 'stream') { + throw NonStreamResourceUsedAsLargeObject::new($parameter); + } + + $streams[$parameter] = $value; + $values[$parameter] = null; + continue; + } + + $types[$parameter - 1] = self::PARAM_TYPE_MAP[ParameterType::STRING]; + } + + $values[$parameter] = $value; + } + + if (! $this->stmt->bind_param($types, ...$values)) { + throw StatementError::new($this->stmt); + } + + $this->sendLongData($streams); + } + + /** + * Handle $this->_longData after regular query parameters have been bound + * + * @param array $streams + * + * @throws Exception + */ + private function sendLongData(array $streams): void + { + foreach ($streams as $paramNr => $stream) { + while (! feof($stream)) { + $chunk = fread($stream, 8192); + + if ($chunk === false) { + throw FailedReadingStreamOffset::new($paramNr); + } + + if (! $this->stmt->send_long_data($paramNr - 1, $chunk)) { + throw StatementError::new($this->stmt); + } + } + } + } + + /** + * Binds a array of values to bound parameters. + * + * @param mixed[] $values + */ + private function bindUntypedValues(array $values): bool + { + return $this->stmt->bind_param(str_repeat('s', count($values)), ...$values); + } +} diff --git a/3rdparty/doctrine/dbal/src/Driver/OCI8/Connection.php b/3rdparty/doctrine/dbal/src/Driver/OCI8/Connection.php new file mode 100644 index 00000000..155b2d1d --- /dev/null +++ b/3rdparty/doctrine/dbal/src/Driver/OCI8/Connection.php @@ -0,0 +1,170 @@ +connection = $connection; + $this->parser = new Parser(false); + $this->executionMode = new ExecutionMode(); + } + + public function getServerVersion(): string + { + $version = oci_server_version($this->connection); + + if ($version === false) { + throw Error::new($this->connection); + } + + $result = preg_match('/\s+(\d+\.\d+\.\d+\.\d+\.\d+)\s+/', $version, $matches); + assert($result === 1); + + return $matches[1]; + } + + /** @throws Parser\Exception */ + public function prepare(string $sql): DriverStatement + { + $visitor = new ConvertPositionalToNamedPlaceholders(); + + $this->parser->parse($sql, $visitor); + + $statement = oci_parse($this->connection, $visitor->getSQL()); + assert(is_resource($statement)); + + return new Statement($this->connection, $statement, $visitor->getParameterMap(), $this->executionMode); + } + + /** + * @throws Exception + * @throws Parser\Exception + */ + public function query(string $sql): ResultInterface + { + return $this->prepare($sql)->execute(); + } + + /** + * {@inheritDoc} + */ + public function quote($value, $type = ParameterType::STRING) + { + if (is_int($value) || is_float($value)) { + return $value; + } + + $value = str_replace("'", "''", $value); + + return "'" . addcslashes($value, "\000\n\r\\\032") . "'"; + } + + /** + * @throws Exception + * @throws Parser\Exception + */ + public function exec(string $sql): int + { + return $this->prepare($sql)->execute()->rowCount(); + } + + /** + * {@inheritDoc} + * + * @param string|null $name + * + * @return int|false + * + * @throws Parser\Exception + */ + public function lastInsertId($name = null) + { + if ($name === null) { + return false; + } + + Deprecation::triggerIfCalledFromOutside( + 'doctrine/dbal', + 'https://github.com/doctrine/dbal/issues/4687', + 'The usage of Connection::lastInsertId() with a sequence name is deprecated.', + ); + + $result = $this->query('SELECT ' . $name . '.CURRVAL FROM DUAL')->fetchOne(); + + if ($result === false) { + throw SequenceDoesNotExist::new(); + } + + return (int) $result; + } + + public function beginTransaction(): bool + { + $this->executionMode->disableAutoCommit(); + + return true; + } + + public function commit(): bool + { + if (! @oci_commit($this->connection)) { + throw Error::new($this->connection); + } + + $this->executionMode->enableAutoCommit(); + + return true; + } + + public function rollBack(): bool + { + if (! oci_rollback($this->connection)) { + throw Error::new($this->connection); + } + + $this->executionMode->enableAutoCommit(); + + return true; + } + + /** @return resource */ + public function getNativeConnection() + { + return $this->connection; + } +} diff --git a/3rdparty/doctrine/dbal/src/Driver/OCI8/ConvertPositionalToNamedPlaceholders.php b/3rdparty/doctrine/dbal/src/Driver/OCI8/ConvertPositionalToNamedPlaceholders.php new file mode 100644 index 00000000..e2a11262 --- /dev/null +++ b/3rdparty/doctrine/dbal/src/Driver/OCI8/ConvertPositionalToNamedPlaceholders.php @@ -0,0 +1,56 @@ +). + * + * Oracle does not support positional parameters, hence this method converts all + * positional parameters into artificially named parameters. + * + * @internal This class is not covered by the backward compatibility promise + */ +final class ConvertPositionalToNamedPlaceholders implements Visitor +{ + /** @var list */ + private array $buffer = []; + + /** @var array */ + private array $parameterMap = []; + + public function acceptOther(string $sql): void + { + $this->buffer[] = $sql; + } + + public function acceptPositionalParameter(string $sql): void + { + $position = count($this->parameterMap) + 1; + $param = ':param' . $position; + + $this->parameterMap[$position] = $param; + + $this->buffer[] = $param; + } + + public function acceptNamedParameter(string $sql): void + { + $this->buffer[] = $sql; + } + + public function getSQL(): string + { + return implode('', $this->buffer); + } + + /** @return array */ + public function getParameterMap(): array + { + return $this->parameterMap; + } +} diff --git a/3rdparty/doctrine/dbal/src/Driver/OCI8/Driver.php b/3rdparty/doctrine/dbal/src/Driver/OCI8/Driver.php new file mode 100644 index 00000000..650a4f9a --- /dev/null +++ b/3rdparty/doctrine/dbal/src/Driver/OCI8/Driver.php @@ -0,0 +1,58 @@ +getEasyConnectString($params); + + $persistent = ! empty($params['persistent']); + $exclusive = ! empty($params['driverOptions']['exclusive']); + + if ($persistent && $exclusive) { + throw InvalidConfiguration::forPersistentAndExclusive(); + } + + if ($persistent) { + $connection = @oci_pconnect($username, $password, $connectionString, $charset, $sessionMode); + } elseif ($exclusive) { + $connection = @oci_new_connect($username, $password, $connectionString, $charset, $sessionMode); + } else { + $connection = @oci_connect($username, $password, $connectionString, $charset, $sessionMode); + } + + if ($connection === false) { + throw ConnectionFailed::new(); + } + + return new Connection($connection); + } +} diff --git a/3rdparty/doctrine/dbal/src/Driver/OCI8/Exception/ConnectionFailed.php b/3rdparty/doctrine/dbal/src/Driver/OCI8/Exception/ConnectionFailed.php new file mode 100644 index 00000000..691d1e31 --- /dev/null +++ b/3rdparty/doctrine/dbal/src/Driver/OCI8/Exception/ConnectionFailed.php @@ -0,0 +1,22 @@ +isAutoCommitEnabled = true; + } + + public function disableAutoCommit(): void + { + $this->isAutoCommitEnabled = false; + } + + public function isAutoCommitEnabled(): bool + { + return $this->isAutoCommitEnabled; + } +} diff --git a/3rdparty/doctrine/dbal/src/Driver/OCI8/Middleware/InitializeSession.php b/3rdparty/doctrine/dbal/src/Driver/OCI8/Middleware/InitializeSession.php new file mode 100644 index 00000000..3a356fb3 --- /dev/null +++ b/3rdparty/doctrine/dbal/src/Driver/OCI8/Middleware/InitializeSession.php @@ -0,0 +1,38 @@ +exec( + 'ALTER SESSION SET' + . " NLS_DATE_FORMAT = 'YYYY-MM-DD HH24:MI:SS'" + . " NLS_TIME_FORMAT = 'HH24:MI:SS'" + . " NLS_TIMESTAMP_FORMAT = 'YYYY-MM-DD HH24:MI:SS'" + . " NLS_TIMESTAMP_TZ_FORMAT = 'YYYY-MM-DD HH24:MI:SS TZH:TZM'" + . " NLS_NUMERIC_CHARACTERS = '.,'", + ); + + return $connection; + } + }; + } +} diff --git a/3rdparty/doctrine/dbal/src/Driver/OCI8/Result.php b/3rdparty/doctrine/dbal/src/Driver/OCI8/Result.php new file mode 100644 index 00000000..08add4fa --- /dev/null +++ b/3rdparty/doctrine/dbal/src/Driver/OCI8/Result.php @@ -0,0 +1,145 @@ +statement = $statement; + } + + /** + * {@inheritDoc} + */ + public function fetchNumeric() + { + return $this->fetch(OCI_NUM); + } + + /** + * {@inheritDoc} + */ + public function fetchAssociative() + { + return $this->fetch(OCI_ASSOC); + } + + /** + * {@inheritDoc} + */ + public function fetchOne() + { + return FetchUtils::fetchOne($this); + } + + /** + * {@inheritDoc} + */ + public function fetchAllNumeric(): array + { + return $this->fetchAll(OCI_NUM, OCI_FETCHSTATEMENT_BY_ROW); + } + + /** + * {@inheritDoc} + */ + public function fetchAllAssociative(): array + { + return $this->fetchAll(OCI_ASSOC, OCI_FETCHSTATEMENT_BY_ROW); + } + + /** + * {@inheritDoc} + */ + public function fetchFirstColumn(): array + { + return $this->fetchAll(OCI_NUM, OCI_FETCHSTATEMENT_BY_COLUMN)[0]; + } + + public function rowCount(): int + { + $count = oci_num_rows($this->statement); + + if ($count !== false) { + return $count; + } + + return 0; + } + + public function columnCount(): int + { + $count = oci_num_fields($this->statement); + + if ($count !== false) { + return $count; + } + + return 0; + } + + public function free(): void + { + oci_cancel($this->statement); + } + + /** + * @return mixed|false + * + * @throws Exception + */ + private function fetch(int $mode) + { + $result = oci_fetch_array($this->statement, $mode | OCI_RETURN_NULLS | OCI_RETURN_LOBS); + + if ($result === false && oci_error($this->statement) !== false) { + throw Error::new($this->statement); + } + + return $result; + } + + /** @return array */ + private function fetchAll(int $mode, int $fetchStructure): array + { + oci_fetch_all( + $this->statement, + $result, + 0, + -1, + $mode | OCI_RETURN_NULLS | $fetchStructure | OCI_RETURN_LOBS, + ); + + return $result; + } +} diff --git a/3rdparty/doctrine/dbal/src/Driver/OCI8/Statement.php b/3rdparty/doctrine/dbal/src/Driver/OCI8/Statement.php new file mode 100644 index 00000000..015a14b7 --- /dev/null +++ b/3rdparty/doctrine/dbal/src/Driver/OCI8/Statement.php @@ -0,0 +1,174 @@ + */ + private array $parameterMap; + + private ExecutionMode $executionMode; + + /** + * @internal The statement can be only instantiated by its driver connection. + * + * @param resource $connection + * @param resource $statement + * @param array $parameterMap + */ + public function __construct($connection, $statement, array $parameterMap, ExecutionMode $executionMode) + { + $this->connection = $connection; + $this->statement = $statement; + $this->parameterMap = $parameterMap; + $this->executionMode = $executionMode; + } + + /** + * {@inheritDoc} + */ + public function bindValue($param, $value, $type = ParameterType::STRING): bool + { + if (func_num_args() < 3) { + Deprecation::trigger( + 'doctrine/dbal', + 'https://github.com/doctrine/dbal/pull/5558', + 'Not passing $type to Statement::bindValue() is deprecated.' + . ' Pass the type corresponding to the parameter being bound.', + ); + } + + return $this->bindParam($param, $value, $type); + } + + /** + * {@inheritDoc} + * + * @deprecated Use {@see bindValue()} instead. + */ + public function bindParam($param, &$variable, $type = ParameterType::STRING, $length = null): bool + { + Deprecation::trigger( + 'doctrine/dbal', + 'https://github.com/doctrine/dbal/pull/5563', + '%s is deprecated. Use bindValue() instead.', + __METHOD__, + ); + + if (func_num_args() < 3) { + Deprecation::trigger( + 'doctrine/dbal', + 'https://github.com/doctrine/dbal/pull/5558', + 'Not passing $type to Statement::bindParam() is deprecated.' + . ' Pass the type corresponding to the parameter being bound.', + ); + } + + if (is_int($param)) { + if (! isset($this->parameterMap[$param])) { + throw UnknownParameterIndex::new($param); + } + + $param = $this->parameterMap[$param]; + } + + if ($type === ParameterType::LARGE_OBJECT) { + if ($variable !== null) { + $lob = oci_new_descriptor($this->connection, OCI_D_LOB); + $lob->writeTemporary($variable, OCI_TEMP_BLOB); + + $variable =& $lob; + } else { + $type = ParameterType::STRING; + } + } + + return oci_bind_by_name( + $this->statement, + $param, + $variable, + $length ?? -1, + $this->convertParameterType($type), + ); + } + + /** + * Converts DBAL parameter type to oci8 parameter type + */ + private function convertParameterType(int $type): int + { + switch ($type) { + case ParameterType::BINARY: + return OCI_B_BIN; + + case ParameterType::LARGE_OBJECT: + return OCI_B_BLOB; + + default: + return SQLT_CHR; + } + } + + /** + * {@inheritDoc} + */ + public function execute($params = null): ResultInterface + { + if ($params !== null) { + Deprecation::trigger( + 'doctrine/dbal', + 'https://github.com/doctrine/dbal/pull/5556', + 'Passing $params to Statement::execute() is deprecated. Bind parameters using' + . ' Statement::bindParam() or Statement::bindValue() instead.', + ); + + foreach ($params as $key => $val) { + if (is_int($key)) { + $this->bindValue($key + 1, $val, ParameterType::STRING); + } else { + $this->bindValue($key, $val, ParameterType::STRING); + } + } + } + + if ($this->executionMode->isAutoCommitEnabled()) { + $mode = OCI_COMMIT_ON_SUCCESS; + } else { + $mode = OCI_NO_AUTO_COMMIT; + } + + $ret = @oci_execute($this->statement, $mode); + if (! $ret) { + throw Error::new($this->statement); + } + + return new Result($this->statement); + } +} diff --git a/3rdparty/doctrine/dbal/src/Driver/PDO/Connection.php b/3rdparty/doctrine/dbal/src/Driver/PDO/Connection.php new file mode 100644 index 00000000..4320879c --- /dev/null +++ b/3rdparty/doctrine/dbal/src/Driver/PDO/Connection.php @@ -0,0 +1,158 @@ +setAttribute(PDO::ATTR_ERRMODE, PDO::ERRMODE_EXCEPTION); + + $this->connection = $connection; + } + + public function exec(string $sql): int + { + try { + $result = $this->connection->exec($sql); + + assert($result !== false); + + return $result; + } catch (PDOException $exception) { + throw Exception::new($exception); + } + } + + /** + * {@inheritDoc} + */ + public function getServerVersion() + { + return $this->connection->getAttribute(PDO::ATTR_SERVER_VERSION); + } + + /** + * {@inheritDoc} + * + * @return Statement + */ + public function prepare(string $sql): StatementInterface + { + try { + $stmt = $this->connection->prepare($sql); + assert($stmt instanceof PDOStatement); + + return new Statement($stmt); + } catch (PDOException $exception) { + throw Exception::new($exception); + } + } + + public function query(string $sql): ResultInterface + { + try { + $stmt = $this->connection->query($sql); + assert($stmt instanceof PDOStatement); + + return new Result($stmt); + } catch (PDOException $exception) { + throw Exception::new($exception); + } + } + + /** + * {@inheritDoc} + * + * @throws UnknownParameterType + * + * @phpstan-assert ParameterType::* $type + */ + public function quote($value, $type = ParameterType::STRING) + { + return $this->connection->quote($value, ParameterTypeMap::convertParamType($type)); + } + + /** + * {@inheritDoc} + */ + public function lastInsertId($name = null) + { + try { + if ($name === null) { + return $this->connection->lastInsertId(); + } + + Deprecation::triggerIfCalledFromOutside( + 'doctrine/dbal', + 'https://github.com/doctrine/dbal/issues/4687', + 'The usage of Connection::lastInsertId() with a sequence name is deprecated.', + ); + + return $this->connection->lastInsertId($name); + } catch (PDOException $exception) { + throw Exception::new($exception); + } + } + + public function beginTransaction(): bool + { + try { + return $this->connection->beginTransaction(); + } catch (PDOException $exception) { + throw DriverPDOException::new($exception); + } + } + + public function commit(): bool + { + try { + return $this->connection->commit(); + } catch (PDOException $exception) { + throw DriverPDOException::new($exception); + } + } + + public function rollBack(): bool + { + try { + return $this->connection->rollBack(); + } catch (PDOException $exception) { + throw DriverPDOException::new($exception); + } + } + + public function getNativeConnection(): PDO + { + return $this->connection; + } + + /** @deprecated Call {@see getNativeConnection()} instead. */ + public function getWrappedConnection(): PDO + { + Deprecation::triggerIfCalledFromOutside( + 'doctrine/dbal', + 'https://github.com/doctrine/dbal/pull/5037', + '%s is deprecated, call getNativeConnection() instead.', + __METHOD__, + ); + + return $this->getNativeConnection(); + } +} diff --git a/3rdparty/doctrine/dbal/src/Driver/PDO/Exception.php b/3rdparty/doctrine/dbal/src/Driver/PDO/Exception.php new file mode 100644 index 00000000..0c0d1554 --- /dev/null +++ b/3rdparty/doctrine/dbal/src/Driver/PDO/Exception.php @@ -0,0 +1,26 @@ +errorInfo !== null) { + [$sqlState, $code] = $exception->errorInfo; + + $code ??= 0; + } else { + $code = $exception->getCode(); + $sqlState = null; + } + + return new self($exception->getMessage(), $sqlState, $code, $exception); + } +} diff --git a/3rdparty/doctrine/dbal/src/Driver/PDO/MySQL/Driver.php b/3rdparty/doctrine/dbal/src/Driver/PDO/MySQL/Driver.php new file mode 100644 index 00000000..4a006daa --- /dev/null +++ b/3rdparty/doctrine/dbal/src/Driver/PDO/MySQL/Driver.php @@ -0,0 +1,79 @@ +doConnect( + $this->constructPdoDsn($safeParams), + $params['user'] ?? '', + $params['password'] ?? '', + $driverOptions, + ); + } catch (PDOException $exception) { + throw Exception::new($exception); + } + + return new Connection($pdo); + } + + /** + * Constructs the MySQL PDO DSN. + * + * @param mixed[] $params + */ + private function constructPdoDsn(array $params): string + { + $dsn = 'mysql:'; + if (isset($params['host']) && $params['host'] !== '') { + $dsn .= 'host=' . $params['host'] . ';'; + } + + if (isset($params['port'])) { + $dsn .= 'port=' . $params['port'] . ';'; + } + + if (isset($params['dbname'])) { + $dsn .= 'dbname=' . $params['dbname'] . ';'; + } + + if (isset($params['unix_socket'])) { + $dsn .= 'unix_socket=' . $params['unix_socket'] . ';'; + } + + if (isset($params['charset'])) { + $dsn .= 'charset=' . $params['charset'] . ';'; + } + + return $dsn; + } +} diff --git a/3rdparty/doctrine/dbal/src/Driver/PDO/OCI/Driver.php b/3rdparty/doctrine/dbal/src/Driver/PDO/OCI/Driver.php new file mode 100644 index 00000000..a66a5cde --- /dev/null +++ b/3rdparty/doctrine/dbal/src/Driver/PDO/OCI/Driver.php @@ -0,0 +1,64 @@ +doConnect( + $this->constructPdoDsn($params), + $params['user'] ?? '', + $params['password'] ?? '', + $driverOptions, + ); + } catch (PDOException $exception) { + throw Exception::new($exception); + } + + return new Connection($pdo); + } + + /** + * Constructs the Oracle PDO DSN. + * + * @param mixed[] $params + */ + private function constructPdoDsn(array $params): string + { + $dsn = 'oci:dbname=' . $this->getEasyConnectString($params); + + if (isset($params['charset'])) { + $dsn .= ';charset=' . $params['charset']; + } + + return $dsn; + } +} diff --git a/3rdparty/doctrine/dbal/src/Driver/PDO/PDOConnect.php b/3rdparty/doctrine/dbal/src/Driver/PDO/PDOConnect.php new file mode 100644 index 00000000..346c6cd5 --- /dev/null +++ b/3rdparty/doctrine/dbal/src/Driver/PDO/PDOConnect.php @@ -0,0 +1,27 @@ + $options */ + private function doConnect( + string $dsn, + string $username, + string $password, + array $options + ): PDO { + if (PHP_VERSION_ID < 80400) { + return new PDO($dsn, $username, $password, $options); + } + + return PDO::connect($dsn, $username, $password, $options); + } +} diff --git a/3rdparty/doctrine/dbal/src/Driver/PDO/PDOException.php b/3rdparty/doctrine/dbal/src/Driver/PDO/PDOException.php new file mode 100644 index 00000000..4b0b177d --- /dev/null +++ b/3rdparty/doctrine/dbal/src/Driver/PDO/PDOException.php @@ -0,0 +1,29 @@ +message, 0, $previous); + + $exception->errorInfo = $previous->errorInfo; + $exception->code = $previous->code; + $exception->sqlState = $previous->errorInfo[0] ?? null; + + return $exception; + } + + public function getSQLState(): ?string + { + return $this->sqlState; + } +} diff --git a/3rdparty/doctrine/dbal/src/Driver/PDO/ParameterTypeMap.php b/3rdparty/doctrine/dbal/src/Driver/PDO/ParameterTypeMap.php new file mode 100644 index 00000000..af705b03 --- /dev/null +++ b/3rdparty/doctrine/dbal/src/Driver/PDO/ParameterTypeMap.php @@ -0,0 +1,49 @@ + PDO::PARAM_NULL, + ParameterType::INTEGER => PDO::PARAM_INT, + ParameterType::STRING => PDO::PARAM_STR, + ParameterType::ASCII => PDO::PARAM_STR, + ParameterType::BINARY => PDO::PARAM_LOB, + ParameterType::LARGE_OBJECT => PDO::PARAM_LOB, + ParameterType::BOOLEAN => PDO::PARAM_BOOL, + ]; + + /** + * Converts DBAL parameter type to PDO parameter type + * + * @phpstan-return PDO::PARAM_* + * + * @throws UnknownParameterType + * + * @phpstan-assert ParameterType::* $type + */ + public static function convertParamType(int $type): int + { + if (! isset(self::PARAM_TYPE_MAP[$type])) { + throw UnknownParameterType::new($type); + } + + return self::PARAM_TYPE_MAP[$type]; + } + + private function __construct() + { + } + + private function __clone() + { + } +} diff --git a/3rdparty/doctrine/dbal/src/Driver/PDO/PgSQL/Driver.php b/3rdparty/doctrine/dbal/src/Driver/PDO/PgSQL/Driver.php new file mode 100644 index 00000000..b62c6231 --- /dev/null +++ b/3rdparty/doctrine/dbal/src/Driver/PDO/PgSQL/Driver.php @@ -0,0 +1,144 @@ +doConnect( + $this->constructPdoDsn($safeParams), + $params['user'] ?? '', + $params['password'] ?? '', + $driverOptions, + ); + } catch (PDOException $exception) { + throw Exception::new($exception); + } + + $disablePreparesAttr = PHP_VERSION_ID >= 80400 + ? Pgsql::ATTR_DISABLE_PREPARES + : PDO::PGSQL_ATTR_DISABLE_PREPARES; + if ( + ! isset($driverOptions[$disablePreparesAttr]) + || $driverOptions[$disablePreparesAttr] === true + ) { + $pdo->setAttribute($disablePreparesAttr, true); + } + + $connection = new Connection($pdo); + + /* defining client_encoding via SET NAMES to avoid inconsistent DSN support + * - passing client_encoding via the 'options' param breaks pgbouncer support + */ + if (isset($params['charset'])) { + $connection->exec('SET NAMES \'' . $params['charset'] . '\''); + } + + return $connection; + } + + /** + * Constructs the Postgres PDO DSN. + * + * @param array $params + */ + private function constructPdoDsn(array $params): string + { + $dsn = 'pgsql:'; + + if (isset($params['host']) && $params['host'] !== '') { + $dsn .= 'host=' . $params['host'] . ';'; + } + + if (isset($params['port']) && $params['port'] !== '') { + $dsn .= 'port=' . $params['port'] . ';'; + } + + if (isset($params['dbname'])) { + $dsn .= 'dbname=' . $params['dbname'] . ';'; + } elseif (isset($params['default_dbname'])) { + Deprecation::trigger( + 'doctrine/dbal', + 'https://github.com/doctrine/dbal/pull/5705', + 'The "default_dbname" connection parameter is deprecated. Use "dbname" instead.', + ); + + $dsn .= 'dbname=' . $params['default_dbname'] . ';'; + } else { + if (isset($params['user']) && $params['user'] !== 'postgres') { + Deprecation::trigger( + 'doctrine/dbal', + 'https://github.com/doctrine/dbal/pull/5705', + 'Relying on the DBAL connecting to the "postgres" database by default is deprecated.' + . ' Unless you want to have the server determine the default database for the connection,' + . ' specify the database name explicitly.', + ); + } + + // Used for temporary connections to allow operations like dropping the database currently connected to. + $dsn .= 'dbname=postgres;'; + } + + if (isset($params['sslmode'])) { + $dsn .= 'sslmode=' . $params['sslmode'] . ';'; + } + + if (isset($params['sslrootcert'])) { + $dsn .= 'sslrootcert=' . $params['sslrootcert'] . ';'; + } + + if (isset($params['sslcert'])) { + $dsn .= 'sslcert=' . $params['sslcert'] . ';'; + } + + if (isset($params['sslkey'])) { + $dsn .= 'sslkey=' . $params['sslkey'] . ';'; + } + + if (isset($params['sslcrl'])) { + $dsn .= 'sslcrl=' . $params['sslcrl'] . ';'; + } + + if (isset($params['application_name'])) { + $dsn .= 'application_name=' . $params['application_name'] . ';'; + } + + if (isset($params['gssencmode'])) { + $dsn .= 'gssencmode=' . $params['gssencmode'] . ';'; + } + + return $dsn; + } +} diff --git a/3rdparty/doctrine/dbal/src/Driver/PDO/Result.php b/3rdparty/doctrine/dbal/src/Driver/PDO/Result.php new file mode 100644 index 00000000..506fdd4a --- /dev/null +++ b/3rdparty/doctrine/dbal/src/Driver/PDO/Result.php @@ -0,0 +1,124 @@ +statement = $statement; + } + + /** + * {@inheritDoc} + */ + public function fetchNumeric() + { + return $this->fetch(PDO::FETCH_NUM); + } + + /** + * {@inheritDoc} + */ + public function fetchAssociative() + { + return $this->fetch(PDO::FETCH_ASSOC); + } + + /** + * {@inheritDoc} + */ + public function fetchOne() + { + return $this->fetch(PDO::FETCH_COLUMN); + } + + /** + * {@inheritDoc} + */ + public function fetchAllNumeric(): array + { + return $this->fetchAll(PDO::FETCH_NUM); + } + + /** + * {@inheritDoc} + */ + public function fetchAllAssociative(): array + { + return $this->fetchAll(PDO::FETCH_ASSOC); + } + + /** + * {@inheritDoc} + */ + public function fetchFirstColumn(): array + { + return $this->fetchAll(PDO::FETCH_COLUMN); + } + + public function rowCount(): int + { + try { + return $this->statement->rowCount(); + } catch (PDOException $exception) { + throw Exception::new($exception); + } + } + + public function columnCount(): int + { + try { + return $this->statement->columnCount(); + } catch (PDOException $exception) { + throw Exception::new($exception); + } + } + + public function free(): void + { + $this->statement->closeCursor(); + } + + /** + * @phpstan-param PDO::FETCH_* $mode + * + * @return mixed + * + * @throws Exception + */ + private function fetch(int $mode) + { + try { + return $this->statement->fetch($mode); + } catch (PDOException $exception) { + throw Exception::new($exception); + } + } + + /** + * @phpstan-param PDO::FETCH_* $mode + * + * @return list + * + * @throws Exception + */ + private function fetchAll(int $mode): array + { + try { + return $this->statement->fetchAll($mode); + } catch (PDOException $exception) { + throw Exception::new($exception); + } + } +} diff --git a/3rdparty/doctrine/dbal/src/Driver/PDO/SQLSrv/Connection.php b/3rdparty/doctrine/dbal/src/Driver/PDO/SQLSrv/Connection.php new file mode 100644 index 00000000..9015f555 --- /dev/null +++ b/3rdparty/doctrine/dbal/src/Driver/PDO/SQLSrv/Connection.php @@ -0,0 +1,70 @@ +connection = $connection; + } + + public function prepare(string $sql): StatementInterface + { + return new Statement( + $this->connection->prepare($sql), + ); + } + + /** + * {@inheritDoc} + */ + public function lastInsertId($name = null) + { + if ($name === null) { + return parent::lastInsertId($name); + } + + Deprecation::triggerIfCalledFromOutside( + 'doctrine/dbal', + 'https://github.com/doctrine/dbal/issues/4687', + 'The usage of Connection::lastInsertId() with a sequence name is deprecated.', + ); + + $statement = $this->prepare( + 'SELECT CONVERT(VARCHAR(MAX), current_value) FROM sys.sequences WHERE name = ?', + ); + $statement->bindValue(1, $name); + + return $statement->execute() + ->fetchOne(); + } + + public function getNativeConnection(): PDO + { + return $this->connection->getNativeConnection(); + } + + /** @deprecated Call {@see getNativeConnection()} instead. */ + public function getWrappedConnection(): PDO + { + Deprecation::trigger( + 'doctrine/dbal', + 'https://github.com/doctrine/dbal/pull/5037', + '%s is deprecated, call getNativeConnection() instead.', + __METHOD__, + ); + + return $this->connection->getWrappedConnection(); + } +} diff --git a/3rdparty/doctrine/dbal/src/Driver/PDO/SQLSrv/Driver.php b/3rdparty/doctrine/dbal/src/Driver/PDO/SQLSrv/Driver.php new file mode 100644 index 00000000..fe2064aa --- /dev/null +++ b/3rdparty/doctrine/dbal/src/Driver/PDO/SQLSrv/Driver.php @@ -0,0 +1,111 @@ + $value) { + if (is_int($option)) { + $driverOptions[$option] = $value; + } else { + $dsnOptions[$option] = $value; + } + } + } + + if (! empty($params['persistent'])) { + $driverOptions[PDO::ATTR_PERSISTENT] = true; + } + + $safeParams = $params; + unset($safeParams['password'], $safeParams['url']); + + try { + $pdo = $this->doConnect( + $this->constructDsn($safeParams, $dsnOptions), + $params['user'] ?? '', + $params['password'] ?? '', + $driverOptions, + ); + } catch (\PDOException $exception) { + throw PDOException::new($exception); + } + + return new Connection(new PDOConnection($pdo)); + } + + /** + * Constructs the Sqlsrv PDO DSN. + * + * @param mixed[] $params + * @param string[] $connectionOptions + * + * @throws Exception + */ + private function constructDsn(array $params, array $connectionOptions): string + { + $dsn = 'sqlsrv:server='; + + if (isset($params['host'])) { + $dsn .= $params['host']; + + if (isset($params['port'])) { + $dsn .= ',' . $params['port']; + } + } elseif (isset($params['port'])) { + throw PortWithoutHost::new(); + } + + if (isset($params['dbname'])) { + $connectionOptions['Database'] = $params['dbname']; + } + + if (isset($params['MultipleActiveResultSets'])) { + $connectionOptions['MultipleActiveResultSets'] = $params['MultipleActiveResultSets'] ? 'true' : 'false'; + } + + return $dsn . $this->getConnectionOptionsDsn($connectionOptions); + } + + /** + * Converts a connection options array to the DSN + * + * @param string[] $connectionOptions + */ + private function getConnectionOptionsDsn(array $connectionOptions): string + { + $connectionOptionsDsn = ''; + + foreach ($connectionOptions as $paramName => $paramValue) { + $connectionOptionsDsn .= sprintf(';%s=%s', $paramName, $paramValue); + } + + return $connectionOptionsDsn; + } +} diff --git a/3rdparty/doctrine/dbal/src/Driver/PDO/SQLSrv/Statement.php b/3rdparty/doctrine/dbal/src/Driver/PDO/SQLSrv/Statement.php new file mode 100644 index 00000000..a63ff79d --- /dev/null +++ b/3rdparty/doctrine/dbal/src/Driver/PDO/SQLSrv/Statement.php @@ -0,0 +1,109 @@ +statement = $statement; + } + + /** + * {@inheritDoc} + * + * @deprecated Use {@see bindValue()} instead. + * + * @param string|int $param + * @param mixed $variable + * @param int $type + * @param int|null $length + * @param mixed $driverOptions The usage of the argument is deprecated. + * + * @throws UnknownParameterType + * + * @phpstan-assert ParameterType::* $type + */ + public function bindParam( + $param, + &$variable, + $type = ParameterType::STRING, + $length = null, + $driverOptions = null + ): bool { + Deprecation::trigger( + 'doctrine/dbal', + 'https://github.com/doctrine/dbal/pull/5563', + '%s is deprecated. Use bindValue() instead.', + __METHOD__, + ); + + if (func_num_args() < 3) { + Deprecation::trigger( + 'doctrine/dbal', + 'https://github.com/doctrine/dbal/pull/5558', + 'Not passing $type to Statement::bindParam() is deprecated.' + . ' Pass the type corresponding to the parameter being bound.', + ); + } + + if (func_num_args() > 4) { + Deprecation::triggerIfCalledFromOutside( + 'doctrine/dbal', + 'https://github.com/doctrine/dbal/issues/4533', + 'The $driverOptions argument of Statement::bindParam() is deprecated.', + ); + } + + switch ($type) { + case ParameterType::LARGE_OBJECT: + case ParameterType::BINARY: + $driverOptions ??= PDO::SQLSRV_ENCODING_BINARY; + + break; + + case ParameterType::ASCII: + $type = ParameterType::STRING; + $length = 0; + $driverOptions = PDO::SQLSRV_ENCODING_SYSTEM; + break; + } + + return $this->statement->bindParam($param, $variable, $type, $length ?? 0, $driverOptions); + } + + /** + * @throws UnknownParameterType + * + * {@inheritDoc} + * + * @phpstan-assert ParameterType::* $type + */ + public function bindValue($param, $value, $type = ParameterType::STRING): bool + { + if (func_num_args() < 3) { + Deprecation::trigger( + 'doctrine/dbal', + 'https://github.com/doctrine/dbal/pull/5558', + 'Not passing $type to Statement::bindValue() is deprecated.' + . ' Pass the type corresponding to the parameter being bound.', + ); + } + + return $this->bindParam($param, $value, $type); + } +} diff --git a/3rdparty/doctrine/dbal/src/Driver/PDO/SQLite/Driver.php b/3rdparty/doctrine/dbal/src/Driver/PDO/SQLite/Driver.php new file mode 100644 index 00000000..2fa0e700 --- /dev/null +++ b/3rdparty/doctrine/dbal/src/Driver/PDO/SQLite/Driver.php @@ -0,0 +1,80 @@ +doConnect( + $this->constructPdoDsn(array_intersect_key($params, ['path' => true, 'memory' => true])), + $params['user'] ?? '', + $params['password'] ?? '', + $driverOptions, + ); + } catch (PDOException $exception) { + throw Exception::new($exception); + } + + UserDefinedFunctions::register( + $pdo instanceof Sqlite ? [$pdo, 'createFunction'] : [$pdo, 'sqliteCreateFunction'], + $userDefinedFunctions, + ); + + return new Connection($pdo); + } + + /** + * Constructs the Sqlite PDO DSN. + * + * @param array $params + */ + private function constructPdoDsn(array $params): string + { + $dsn = 'sqlite:'; + if (isset($params['path'])) { + $dsn .= $params['path']; + } elseif (isset($params['memory'])) { + $dsn .= ':memory:'; + } + + return $dsn; + } +} diff --git a/3rdparty/doctrine/dbal/src/Driver/PDO/Statement.php b/3rdparty/doctrine/dbal/src/Driver/PDO/Statement.php new file mode 100644 index 00000000..e631fad3 --- /dev/null +++ b/3rdparty/doctrine/dbal/src/Driver/PDO/Statement.php @@ -0,0 +1,137 @@ +stmt = $stmt; + } + + /** + * {@inheritDoc} + * + * @throws UnknownParameterType + * + * @phpstan-assert ParameterType::* $type + */ + public function bindValue($param, $value, $type = ParameterType::STRING) + { + if (func_num_args() < 3) { + Deprecation::trigger( + 'doctrine/dbal', + 'https://github.com/doctrine/dbal/pull/5558', + 'Not passing $type to Statement::bindValue() is deprecated.' + . ' Pass the type corresponding to the parameter being bound.', + ); + } + + $pdoType = ParameterTypeMap::convertParamType($type); + + try { + return $this->stmt->bindValue($param, $value, $pdoType); + } catch (PDOException $exception) { + throw Exception::new($exception); + } + } + + /** + * {@inheritDoc} + * + * @deprecated Use {@see bindValue()} instead. + * + * @param mixed $param + * @param mixed $variable + * @param int $type + * @param int|null $length + * @param mixed $driverOptions The usage of the argument is deprecated. + * + * @throws UnknownParameterType + * + * @phpstan-assert ParameterType::* $type + */ + public function bindParam( + $param, + &$variable, + $type = ParameterType::STRING, + $length = null, + $driverOptions = null + ): bool { + Deprecation::trigger( + 'doctrine/dbal', + 'https://github.com/doctrine/dbal/pull/5563', + '%s is deprecated. Use bindValue() instead.', + __METHOD__, + ); + + if (func_num_args() < 3) { + Deprecation::trigger( + 'doctrine/dbal', + 'https://github.com/doctrine/dbal/pull/5558', + 'Not passing $type to Statement::bindParam() is deprecated.' + . ' Pass the type corresponding to the parameter being bound.', + ); + } + + if (func_num_args() > 4) { + Deprecation::triggerIfCalledFromOutside( + 'doctrine/dbal', + 'https://github.com/doctrine/dbal/issues/4533', + 'The $driverOptions argument of Statement::bindParam() is deprecated.', + ); + } + + $pdoType = ParameterTypeMap::convertParamType($type); + + try { + return $this->stmt->bindParam( + $param, + $variable, + $pdoType, + $length ?? 0, + ...array_slice(func_get_args(), 4), + ); + } catch (PDOException $exception) { + throw Exception::new($exception); + } + } + + /** + * {@inheritDoc} + */ + public function execute($params = null): ResultInterface + { + if ($params !== null) { + Deprecation::trigger( + 'doctrine/dbal', + 'https://github.com/doctrine/dbal/pull/5556', + 'Passing $params to Statement::execute() is deprecated. Bind parameters using' + . ' Statement::bindParam() or Statement::bindValue() instead.', + ); + } + + try { + $this->stmt->execute($params); + } catch (PDOException $exception) { + throw Exception::new($exception); + } + + return new Result($this->stmt); + } +} diff --git a/3rdparty/doctrine/dbal/src/Driver/PgSQL/Connection.php b/3rdparty/doctrine/dbal/src/Driver/PgSQL/Connection.php new file mode 100644 index 00000000..378e8ed7 --- /dev/null +++ b/3rdparty/doctrine/dbal/src/Driver/PgSQL/Connection.php @@ -0,0 +1,161 @@ +connection = $connection; + $this->parser = new Parser(false); + } + + public function __destruct() + { + if (! isset($this->connection)) { + return; + } + + @pg_close($this->connection); + } + + public function prepare(string $sql): Statement + { + $visitor = new ConvertParameters(); + $this->parser->parse($sql, $visitor); + + $statementName = uniqid('dbal', true); + if (@pg_send_prepare($this->connection, $statementName, $visitor->getSQL()) !== true) { + throw new Exception(pg_last_error($this->connection)); + } + + $result = @pg_get_result($this->connection); + assert($result !== false); + + if ((bool) pg_result_error($result)) { + throw Exception::fromResult($result); + } + + return new Statement($this->connection, $statementName, $visitor->getParameterMap()); + } + + public function query(string $sql): Result + { + if (@pg_send_query($this->connection, $sql) !== true) { + throw new Exception(pg_last_error($this->connection)); + } + + $result = @pg_get_result($this->connection); + assert($result !== false); + + if ((bool) pg_result_error($result)) { + throw Exception::fromResult($result); + } + + return new Result($result); + } + + /** {@inheritDoc} */ + public function quote($value, $type = ParameterType::STRING) + { + if ($type === ParameterType::BINARY || $type === ParameterType::LARGE_OBJECT) { + return sprintf("'%s'", pg_escape_bytea($this->connection, $value)); + } + + return pg_escape_literal($this->connection, $value); + } + + public function exec(string $sql): int + { + return $this->query($sql)->rowCount(); + } + + /** {@inheritDoc} */ + public function lastInsertId($name = null) + { + if ($name !== null) { + Deprecation::triggerIfCalledFromOutside( + 'doctrine/dbal', + 'https://github.com/doctrine/dbal/issues/4687', + 'The usage of Connection::lastInsertId() with a sequence name is deprecated.', + ); + + return $this->query(sprintf('SELECT CURRVAL(%s)', $this->quote($name)))->fetchOne(); + } + + return $this->query('SELECT LASTVAL()')->fetchOne(); + } + + /** @return true */ + public function beginTransaction(): bool + { + $this->exec('BEGIN'); + + return true; + } + + /** @return true */ + public function commit(): bool + { + $this->exec('COMMIT'); + + return true; + } + + /** @return true */ + public function rollBack(): bool + { + $this->exec('ROLLBACK'); + + return true; + } + + public function getServerVersion(): string + { + return (string) pg_version($this->connection)['server']; + } + + /** @return PgSqlConnection|resource */ + public function getNativeConnection() + { + return $this->connection; + } +} diff --git a/3rdparty/doctrine/dbal/src/Driver/PgSQL/ConvertParameters.php b/3rdparty/doctrine/dbal/src/Driver/PgSQL/ConvertParameters.php new file mode 100644 index 00000000..795f12d2 --- /dev/null +++ b/3rdparty/doctrine/dbal/src/Driver/PgSQL/ConvertParameters.php @@ -0,0 +1,49 @@ + */ + private array $buffer = []; + + /** @var array */ + private array $parameterMap = []; + + public function acceptPositionalParameter(string $sql): void + { + $position = count($this->parameterMap) + 1; + $this->parameterMap[$position] = $position; + $this->buffer[] = '$' . $position; + } + + public function acceptNamedParameter(string $sql): void + { + $position = count($this->parameterMap) + 1; + $this->parameterMap[$sql] = $position; + $this->buffer[] = '$' . $position; + } + + public function acceptOther(string $sql): void + { + $this->buffer[] = $sql; + } + + public function getSQL(): string + { + return implode('', $this->buffer); + } + + /** @return array */ + public function getParameterMap(): array + { + return $this->parameterMap; + } +} diff --git a/3rdparty/doctrine/dbal/src/Driver/PgSQL/Driver.php b/3rdparty/doctrine/dbal/src/Driver/PgSQL/Driver.php new file mode 100644 index 00000000..1cdaee1f --- /dev/null +++ b/3rdparty/doctrine/dbal/src/Driver/PgSQL/Driver.php @@ -0,0 +1,96 @@ +constructConnectionString($params), PGSQL_CONNECT_FORCE_NEW); + } catch (ErrorException $e) { + throw new Exception($e->getMessage(), '08006', 0, $e); + } finally { + restore_error_handler(); + } + + if ($connection === false) { + throw new Exception('Unable to connect to Postgres server.'); + } + + $driverConnection = new Connection($connection); + + if (isset($params['application_name'])) { + $driverConnection->exec('SET application_name = ' . $driverConnection->quote($params['application_name'])); + } + + return $driverConnection; + } + + /** + * Constructs the Postgres connection string + * + * @param array $params + */ + private function constructConnectionString( + #[SensitiveParameter] + array $params + ): string { + // pg_connect used by Doctrine DBAL does not support [...] notation, + // but requires the host address in plain form like `aa:bb:99...` + $matches = []; + if (isset($params['host']) && preg_match('/^\[(.+)\]$/', $params['host'], $matches) === 1) { + $params['hostaddr'] = $matches[1]; + unset($params['host']); + } + + $components = array_filter( + [ + 'host' => $params['host'] ?? null, + 'hostaddr' => $params['hostaddr'] ?? null, + 'port' => $params['port'] ?? null, + 'dbname' => $params['dbname'] ?? 'postgres', + 'user' => $params['user'] ?? null, + 'password' => $params['password'] ?? null, + 'sslmode' => $params['sslmode'] ?? null, + 'gssencmode' => $params['gssencmode'] ?? null, + ], + static fn ($value) => $value !== '' && $value !== null, + ); + + return implode(' ', array_map( + static fn ($value, string $key) => sprintf("%s='%s'", $key, addslashes($value)), + array_values($components), + array_keys($components), + )); + } +} diff --git a/3rdparty/doctrine/dbal/src/Driver/PgSQL/Exception.php b/3rdparty/doctrine/dbal/src/Driver/PgSQL/Exception.php new file mode 100644 index 00000000..5e7086ba --- /dev/null +++ b/3rdparty/doctrine/dbal/src/Driver/PgSQL/Exception.php @@ -0,0 +1,26 @@ +result = $result; + } + + public function __destruct() + { + if (! isset($this->result)) { + return; + } + + $this->free(); + } + + /** {@inheritDoc} */ + public function fetchNumeric() + { + if ($this->result === null) { + return false; + } + + $row = pg_fetch_row($this->result); + if ($row === false) { + return false; + } + + return $this->mapNumericRow($row, $this->fetchNumericColumnTypes()); + } + + /** {@inheritDoc} */ + public function fetchAssociative() + { + if ($this->result === null) { + return false; + } + + $row = pg_fetch_assoc($this->result); + if ($row === false) { + return false; + } + + return $this->mapAssociativeRow($row, $this->fetchAssociativeColumnTypes()); + } + + /** {@inheritDoc} */ + public function fetchOne() + { + return FetchUtils::fetchOne($this); + } + + /** {@inheritDoc} */ + public function fetchAllNumeric(): array + { + if ($this->result === null) { + return []; + } + + $resultSet = pg_fetch_all($this->result, PGSQL_NUM); + // On PHP 7.4, pg_fetch_all() might return false for empty result sets. + if ($resultSet === false) { + return []; + } + + $types = $this->fetchNumericColumnTypes(); + + return array_map( + fn (array $row) => $this->mapNumericRow($row, $types), + $resultSet, + ); + } + + /** {@inheritDoc} */ + public function fetchAllAssociative(): array + { + if ($this->result === null) { + return []; + } + + $resultSet = pg_fetch_all($this->result, PGSQL_ASSOC); + // On PHP 7.4, pg_fetch_all() might return false for empty result sets. + if ($resultSet === false) { + return []; + } + + $types = $this->fetchAssociativeColumnTypes(); + + return array_map( + fn (array $row) => $this->mapAssociativeRow($row, $types), + $resultSet, + ); + } + + /** {@inheritDoc} */ + public function fetchFirstColumn(): array + { + if ($this->result === null) { + return []; + } + + $postgresType = pg_field_type($this->result, 0); + + return array_map( + fn ($value) => $this->mapType($postgresType, $value), + pg_fetch_all_columns($this->result), + ); + } + + public function rowCount(): int + { + if ($this->result === null) { + return 0; + } + + return pg_affected_rows($this->result); + } + + public function columnCount(): int + { + if ($this->result === null) { + return 0; + } + + return pg_num_fields($this->result); + } + + public function free(): void + { + if ($this->result === null) { + return; + } + + pg_free_result($this->result); + $this->result = null; + } + + /** @return array */ + private function fetchNumericColumnTypes(): array + { + assert($this->result !== null); + + $types = []; + $numFields = pg_num_fields($this->result); + for ($i = 0; $i < $numFields; ++$i) { + $types[$i] = pg_field_type($this->result, $i); + } + + return $types; + } + + /** @return array */ + private function fetchAssociativeColumnTypes(): array + { + assert($this->result !== null); + + $types = []; + $numFields = pg_num_fields($this->result); + for ($i = 0; $i < $numFields; ++$i) { + $types[pg_field_name($this->result, $i)] = pg_field_type($this->result, $i); + } + + return $types; + } + + /** + * @param list $row + * @param array $types + * + * @return list + */ + private function mapNumericRow(array $row, array $types): array + { + assert($this->result !== null); + + return array_map( + fn ($value, $field) => $this->mapType($types[$field], $value), + $row, + array_keys($row), + ); + } + + /** + * @param array $row + * @param array $types + * + * @return array + */ + private function mapAssociativeRow(array $row, array $types): array + { + assert($this->result !== null); + + $mappedRow = []; + foreach ($row as $field => $value) { + $mappedRow[$field] = $this->mapType($types[$field], $value); + } + + return $mappedRow; + } + + /** @return string|int|float|bool|null */ + private function mapType(string $postgresType, ?string $value) + { + if ($value === null) { + return null; + } + + switch ($postgresType) { + case 'bool': + switch ($value) { + case 't': + return true; + case 'f': + return false; + } + + throw UnexpectedValue::new($value, $postgresType); + + case 'bytea': + return hex2bin(substr($value, 2)); + + case 'float4': + case 'float8': + return (float) $value; + + case 'int2': + case 'int4': + return (int) $value; + + case 'int8': + return PHP_INT_SIZE >= 8 ? (int) $value : $value; + } + + return $value; + } +} diff --git a/3rdparty/doctrine/dbal/src/Driver/PgSQL/Statement.php b/3rdparty/doctrine/dbal/src/Driver/PgSQL/Statement.php new file mode 100644 index 00000000..43af9f1b --- /dev/null +++ b/3rdparty/doctrine/dbal/src/Driver/PgSQL/Statement.php @@ -0,0 +1,186 @@ + */ + private array $parameterMap; + + /** @var array */ + private array $parameters = []; + + /** @var array */ + private array $parameterTypes = []; + + /** + * @param PgSqlConnection|resource $connection + * @param array $parameterMap + */ + public function __construct($connection, string $name, array $parameterMap) + { + if (! is_resource($connection) && ! $connection instanceof PgSqlConnection) { + throw new TypeError(sprintf( + 'Expected connection to be a resource or an instance of %s, got %s.', + PgSqlConnection::class, + is_object($connection) ? get_class($connection) : gettype($connection), + )); + } + + $this->connection = $connection; + $this->name = $name; + $this->parameterMap = $parameterMap; + } + + public function __destruct() + { + if (! isset($this->connection)) { + return; + } + + @pg_query( + $this->connection, + 'DEALLOCATE ' . pg_escape_identifier($this->connection, $this->name), + ); + } + + /** {@inheritDoc} */ + public function bindValue($param, $value, $type = ParameterType::STRING): bool + { + if (! isset($this->parameterMap[$param])) { + throw UnknownParameter::new((string) $param); + } + + if ($value === null) { + $type = ParameterType::NULL; + } + + if ($type === ParameterType::BOOLEAN) { + $this->parameters[$this->parameterMap[$param]] = (bool) $value === false ? 'f' : 't'; + $this->parameterTypes[$this->parameterMap[$param]] = ParameterType::STRING; + } else { + $this->parameters[$this->parameterMap[$param]] = $value; + $this->parameterTypes[$this->parameterMap[$param]] = $type; + } + + return true; + } + + /** {@inheritDoc} */ + public function bindParam($param, &$variable, $type = ParameterType::STRING, $length = null): bool + { + Deprecation::trigger( + 'doctrine/dbal', + 'https://github.com/doctrine/dbal/pull/5563', + '%s is deprecated. Use bindValue() instead.', + __METHOD__, + ); + + if (func_num_args() < 3) { + Deprecation::trigger( + 'doctrine/dbal', + 'https://github.com/doctrine/dbal/pull/5558', + 'Not passing $type to Statement::bindParam() is deprecated.' + . ' Pass the type corresponding to the parameter being bound.', + ); + } + + if (func_num_args() > 4) { + Deprecation::triggerIfCalledFromOutside( + 'doctrine/dbal', + 'https://github.com/doctrine/dbal/issues/4533', + 'The $driverOptions argument of Statement::bindParam() is deprecated.', + ); + } + + if (! isset($this->parameterMap[$param])) { + throw UnknownParameter::new((string) $param); + } + + $this->parameters[$this->parameterMap[$param]] = &$variable; + $this->parameterTypes[$this->parameterMap[$param]] = $type; + + return true; + } + + /** {@inheritDoc} */ + public function execute($params = null): Result + { + if ($params !== null) { + Deprecation::trigger( + 'doctrine/dbal', + 'https://github.com/doctrine/dbal/pull/5556', + 'Passing $params to Statement::execute() is deprecated. Bind parameters using' + . ' Statement::bindParam() or Statement::bindValue() instead.', + ); + + foreach ($params as $param => $value) { + if (is_int($param)) { + $this->bindValue($param + 1, $value, ParameterType::STRING); + } else { + $this->bindValue($param, $value, ParameterType::STRING); + } + } + } + + ksort($this->parameters); + + $escapedParameters = []; + foreach ($this->parameters as $parameter => $value) { + switch ($this->parameterTypes[$parameter]) { + case ParameterType::BINARY: + case ParameterType::LARGE_OBJECT: + $escapedParameters[] = $value === null ? null : pg_escape_bytea( + $this->connection, + is_resource($value) ? stream_get_contents($value) : $value, + ); + break; + default: + $escapedParameters[] = $value; + } + } + + if (@pg_send_execute($this->connection, $this->name, $escapedParameters) !== true) { + throw new Exception(pg_last_error($this->connection)); + } + + $result = @pg_get_result($this->connection); + assert($result !== false); + + if ((bool) pg_result_error($result)) { + throw Exception::fromResult($result); + } + + return new Result($result); + } +} diff --git a/3rdparty/doctrine/dbal/src/Driver/Result.php b/3rdparty/doctrine/dbal/src/Driver/Result.php new file mode 100644 index 00000000..7843a958 --- /dev/null +++ b/3rdparty/doctrine/dbal/src/Driver/Result.php @@ -0,0 +1,93 @@ +|false + * + * @throws Exception + */ + public function fetchNumeric(); + + /** + * Returns the next row of the result as an associative array or FALSE if there are no more rows. + * + * @return array|false + * + * @throws Exception + */ + public function fetchAssociative(); + + /** + * Returns the first value of the next row of the result or FALSE if there are no more rows. + * + * @return mixed|false + * + * @throws Exception + */ + public function fetchOne(); + + /** + * Returns an array containing all of the result rows represented as numeric arrays. + * + * @return list> + * + * @throws Exception + */ + public function fetchAllNumeric(): array; + + /** + * Returns an array containing all of the result rows represented as associative arrays. + * + * @return list> + * + * @throws Exception + */ + public function fetchAllAssociative(): array; + + /** + * Returns an array containing the values of the first column of the result. + * + * @return list + * + * @throws Exception + */ + public function fetchFirstColumn(): array; + + /** + * Returns the number of rows affected by the DELETE, INSERT, or UPDATE statement that produced the result. + * + * If the statement executed a SELECT query or a similar platform-specific SQL (e.g. DESCRIBE, SHOW, etc.), + * some database drivers may return the number of rows returned by that query. However, this behaviour + * is not guaranteed for all drivers and should not be relied on in portable applications. + * + * @return int The number of rows. + * + * @throws Exception + */ + public function rowCount(): int; + + /** + * Returns the number of columns in the result + * + * @return int The number of columns in the result. If the columns cannot be counted, + * this method must return 0. + * + * @throws Exception + */ + public function columnCount(): int; + + /** + * Discards the non-fetched portion of the result, enabling the originating statement to be executed again. + */ + public function free(): void; +} diff --git a/3rdparty/doctrine/dbal/src/Driver/SQLSrv/Connection.php b/3rdparty/doctrine/dbal/src/Driver/SQLSrv/Connection.php new file mode 100644 index 00000000..16e45d11 --- /dev/null +++ b/3rdparty/doctrine/dbal/src/Driver/SQLSrv/Connection.php @@ -0,0 +1,144 @@ +connection = $connection; + } + + /** + * {@inheritDoc} + */ + public function getServerVersion() + { + $serverInfo = sqlsrv_server_info($this->connection); + + return $serverInfo['SQLServerVersion']; + } + + public function prepare(string $sql): DriverStatement + { + return new Statement($this->connection, $sql); + } + + public function query(string $sql): ResultInterface + { + return $this->prepare($sql)->execute(); + } + + /** + * {@inheritDoc} + */ + public function quote($value, $type = ParameterType::STRING) + { + if (is_int($value)) { + return $value; + } + + if (is_float($value)) { + return sprintf('%F', $value); + } + + return "'" . str_replace("'", "''", $value) . "'"; + } + + public function exec(string $sql): int + { + $stmt = sqlsrv_query($this->connection, $sql); + + if ($stmt === false) { + throw Error::new(); + } + + $rowsAffected = sqlsrv_rows_affected($stmt); + + if ($rowsAffected === false) { + throw Error::new(); + } + + return $rowsAffected; + } + + /** + * {@inheritDoc} + */ + public function lastInsertId($name = null) + { + if ($name !== null) { + Deprecation::triggerIfCalledFromOutside( + 'doctrine/dbal', + 'https://github.com/doctrine/dbal/issues/4687', + 'The usage of Connection::lastInsertId() with a sequence name is deprecated.', + ); + + $result = $this->prepare('SELECT CONVERT(VARCHAR(MAX), current_value) FROM sys.sequences WHERE name = ?') + ->execute([$name]); + } else { + $result = $this->query('SELECT @@IDENTITY'); + } + + return $result->fetchOne(); + } + + public function beginTransaction(): bool + { + if (! sqlsrv_begin_transaction($this->connection)) { + throw Error::new(); + } + + return true; + } + + public function commit(): bool + { + if (! sqlsrv_commit($this->connection)) { + throw Error::new(); + } + + return true; + } + + public function rollBack(): bool + { + if (! sqlsrv_rollback($this->connection)) { + throw Error::new(); + } + + return true; + } + + /** @return resource */ + public function getNativeConnection() + { + return $this->connection; + } +} diff --git a/3rdparty/doctrine/dbal/src/Driver/SQLSrv/Driver.php b/3rdparty/doctrine/dbal/src/Driver/SQLSrv/Driver.php new file mode 100644 index 00000000..fcbdb773 --- /dev/null +++ b/3rdparty/doctrine/dbal/src/Driver/SQLSrv/Driver.php @@ -0,0 +1,73 @@ +statement = $stmt; + } + + /** + * {@inheritDoc} + */ + public function fetchNumeric() + { + return $this->fetch(SQLSRV_FETCH_NUMERIC); + } + + /** + * {@inheritDoc} + */ + public function fetchAssociative() + { + return $this->fetch(SQLSRV_FETCH_ASSOC); + } + + /** + * {@inheritDoc} + */ + public function fetchOne() + { + return FetchUtils::fetchOne($this); + } + + /** + * {@inheritDoc} + */ + public function fetchAllNumeric(): array + { + return FetchUtils::fetchAllNumeric($this); + } + + /** + * {@inheritDoc} + */ + public function fetchAllAssociative(): array + { + return FetchUtils::fetchAllAssociative($this); + } + + /** + * {@inheritDoc} + */ + public function fetchFirstColumn(): array + { + return FetchUtils::fetchFirstColumn($this); + } + + public function rowCount(): int + { + $count = sqlsrv_rows_affected($this->statement); + + if ($count !== false) { + return $count; + } + + return 0; + } + + public function columnCount(): int + { + $count = sqlsrv_num_fields($this->statement); + + if ($count !== false) { + return $count; + } + + return 0; + } + + public function free(): void + { + // emulate it by fetching and discarding rows, similarly to what PDO does in this case + // @link http://php.net/manual/en/pdostatement.closecursor.php + // @link https://github.com/php/php-src/blob/php-7.0.11/ext/pdo/pdo_stmt.c#L2075 + // deliberately do not consider multiple result sets, since doctrine/dbal doesn't support them + while (sqlsrv_fetch($this->statement) === true) { + } + } + + /** @return mixed|false */ + private function fetch(int $fetchType) + { + return sqlsrv_fetch_array($this->statement, $fetchType) ?? false; + } +} diff --git a/3rdparty/doctrine/dbal/src/Driver/SQLSrv/Statement.php b/3rdparty/doctrine/dbal/src/Driver/SQLSrv/Statement.php new file mode 100644 index 00000000..227c3345 --- /dev/null +++ b/3rdparty/doctrine/dbal/src/Driver/SQLSrv/Statement.php @@ -0,0 +1,223 @@ + + */ + private array $variables = []; + + /** + * Bound parameter types. + * + * @var array + */ + private array $types = []; + + /** + * Append to any INSERT query to retrieve the last insert id. + */ + private const LAST_INSERT_ID_SQL = ';SELECT SCOPE_IDENTITY() AS LastInsertId;'; + + /** + * @internal The statement can be only instantiated by its driver connection. + * + * @param resource $conn + * @param string $sql + */ + public function __construct($conn, $sql) + { + $this->conn = $conn; + $this->sql = $sql; + + if (stripos($sql, 'INSERT INTO ') !== 0) { + return; + } + + $this->sql .= self::LAST_INSERT_ID_SQL; + } + + /** + * {@inheritDoc} + */ + public function bindValue($param, $value, $type = ParameterType::STRING): bool + { + assert(is_int($param)); + + if (func_num_args() < 3) { + Deprecation::trigger( + 'doctrine/dbal', + 'https://github.com/doctrine/dbal/pull/5558', + 'Not passing $type to Statement::bindValue() is deprecated.' + . ' Pass the type corresponding to the parameter being bound.', + ); + } + + $this->variables[$param] = $value; + $this->types[$param] = $type; + + return true; + } + + /** + * {@inheritDoc} + * + * @deprecated Use {@see bindValue()} instead. + */ + public function bindParam($param, &$variable, $type = ParameterType::STRING, $length = null): bool + { + Deprecation::trigger( + 'doctrine/dbal', + 'https://github.com/doctrine/dbal/pull/5563', + '%s is deprecated. Use bindValue() instead.', + __METHOD__, + ); + + assert(is_int($param)); + + if (func_num_args() < 3) { + Deprecation::trigger( + 'doctrine/dbal', + 'https://github.com/doctrine/dbal/pull/5558', + 'Not passing $type to Statement::bindParam() is deprecated.' + . ' Pass the type corresponding to the parameter being bound.', + ); + } + + $this->variables[$param] =& $variable; + $this->types[$param] = $type; + + // unset the statement resource if it exists as the new one will need to be bound to the new variable + $this->stmt = null; + + return true; + } + + /** + * {@inheritDoc} + */ + public function execute($params = null): ResultInterface + { + if ($params !== null) { + Deprecation::trigger( + 'doctrine/dbal', + 'https://github.com/doctrine/dbal/pull/5556', + 'Passing $params to Statement::execute() is deprecated. Bind parameters using' + . ' Statement::bindParam() or Statement::bindValue() instead.', + ); + + foreach ($params as $key => $val) { + if (is_int($key)) { + $this->bindValue($key + 1, $val, ParameterType::STRING); + } else { + $this->bindValue($key, $val, ParameterType::STRING); + } + } + } + + $this->stmt ??= $this->prepare(); + + if (! sqlsrv_execute($this->stmt)) { + throw Error::new(); + } + + return new Result($this->stmt); + } + + /** + * Prepares SQL Server statement resource + * + * @return resource + * + * @throws Exception + */ + private function prepare() + { + $params = []; + + foreach ($this->variables as $column => &$variable) { + switch ($this->types[$column]) { + case ParameterType::LARGE_OBJECT: + $params[$column - 1] = [ + &$variable, + SQLSRV_PARAM_IN, + SQLSRV_PHPTYPE_STREAM(SQLSRV_ENC_BINARY), + SQLSRV_SQLTYPE_VARBINARY('max'), + ]; + break; + + case ParameterType::BINARY: + $params[$column - 1] = [ + &$variable, + SQLSRV_PARAM_IN, + SQLSRV_PHPTYPE_STRING(SQLSRV_ENC_BINARY), + ]; + break; + + case ParameterType::ASCII: + $params[$column - 1] = [ + &$variable, + SQLSRV_PARAM_IN, + SQLSRV_PHPTYPE_STRING(SQLSRV_ENC_CHAR), + ]; + break; + + default: + $params[$column - 1] =& $variable; + break; + } + } + + $stmt = sqlsrv_prepare($this->conn, $this->sql, $params); + + if ($stmt === false) { + throw Error::new(); + } + + return $stmt; + } +} diff --git a/3rdparty/doctrine/dbal/src/Driver/SQLite3/Connection.php b/3rdparty/doctrine/dbal/src/Driver/SQLite3/Connection.php new file mode 100644 index 00000000..91b9b5ff --- /dev/null +++ b/3rdparty/doctrine/dbal/src/Driver/SQLite3/Connection.php @@ -0,0 +1,107 @@ +connection = $connection; + } + + public function prepare(string $sql): Statement + { + try { + $statement = $this->connection->prepare($sql); + } catch (\Exception $e) { + throw Exception::new($e); + } + + assert($statement !== false); + + return new Statement($this->connection, $statement); + } + + public function query(string $sql): Result + { + try { + $result = $this->connection->query($sql); + } catch (\Exception $e) { + throw Exception::new($e); + } + + assert($result !== false); + + return new Result($result, $this->connection->changes()); + } + + /** @inheritDoc */ + public function quote($value, $type = ParameterType::STRING): string + { + return sprintf('\'%s\'', SQLite3::escapeString($value)); + } + + public function exec(string $sql): int + { + try { + $this->connection->exec($sql); + } catch (\Exception $e) { + throw Exception::new($e); + } + + return $this->connection->changes(); + } + + /** @inheritDoc */ + public function lastInsertId($name = null): int + { + return $this->connection->lastInsertRowID(); + } + + public function beginTransaction(): bool + { + try { + return $this->connection->exec('BEGIN'); + } catch (\Exception $e) { + return false; + } + } + + public function commit(): bool + { + try { + return $this->connection->exec('COMMIT'); + } catch (\Exception $e) { + return false; + } + } + + public function rollBack(): bool + { + try { + return $this->connection->exec('ROLLBACK'); + } catch (\Exception $e) { + return false; + } + } + + public function getNativeConnection(): SQLite3 + { + return $this->connection; + } + + public function getServerVersion(): string + { + return SQLite3::version()['versionString']; + } +} diff --git a/3rdparty/doctrine/dbal/src/Driver/SQLite3/Driver.php b/3rdparty/doctrine/dbal/src/Driver/SQLite3/Driver.php new file mode 100644 index 00000000..fecc4819 --- /dev/null +++ b/3rdparty/doctrine/dbal/src/Driver/SQLite3/Driver.php @@ -0,0 +1,49 @@ +enableExceptions(true); + + UserDefinedFunctions::register([$connection, 'createFunction']); + + return new Connection($connection); + } +} diff --git a/3rdparty/doctrine/dbal/src/Driver/SQLite3/Exception.php b/3rdparty/doctrine/dbal/src/Driver/SQLite3/Exception.php new file mode 100644 index 00000000..3219fc32 --- /dev/null +++ b/3rdparty/doctrine/dbal/src/Driver/SQLite3/Exception.php @@ -0,0 +1,14 @@ +getMessage(), null, (int) $exception->getCode(), $exception); + } +} diff --git a/3rdparty/doctrine/dbal/src/Driver/SQLite3/Result.php b/3rdparty/doctrine/dbal/src/Driver/SQLite3/Result.php new file mode 100644 index 00000000..3881e189 --- /dev/null +++ b/3rdparty/doctrine/dbal/src/Driver/SQLite3/Result.php @@ -0,0 +1,91 @@ +result = $result; + $this->changes = $changes; + } + + /** @inheritDoc */ + public function fetchNumeric() + { + if ($this->result === null) { + return false; + } + + return $this->result->fetchArray(SQLITE3_NUM); + } + + /** @inheritDoc */ + public function fetchAssociative() + { + if ($this->result === null) { + return false; + } + + return $this->result->fetchArray(SQLITE3_ASSOC); + } + + /** @inheritDoc */ + public function fetchOne() + { + return FetchUtils::fetchOne($this); + } + + /** @inheritDoc */ + public function fetchAllNumeric(): array + { + return FetchUtils::fetchAllNumeric($this); + } + + /** @inheritDoc */ + public function fetchAllAssociative(): array + { + return FetchUtils::fetchAllAssociative($this); + } + + /** @inheritDoc */ + public function fetchFirstColumn(): array + { + return FetchUtils::fetchFirstColumn($this); + } + + public function rowCount(): int + { + return $this->changes; + } + + public function columnCount(): int + { + if ($this->result === null) { + return 0; + } + + return $this->result->numColumns(); + } + + public function free(): void + { + if ($this->result === null) { + return; + } + + $this->result->finalize(); + $this->result = null; + } +} diff --git a/3rdparty/doctrine/dbal/src/Driver/SQLite3/Statement.php b/3rdparty/doctrine/dbal/src/Driver/SQLite3/Statement.php new file mode 100644 index 00000000..01c3b8bb --- /dev/null +++ b/3rdparty/doctrine/dbal/src/Driver/SQLite3/Statement.php @@ -0,0 +1,136 @@ + SQLITE3_NULL, + ParameterType::INTEGER => SQLITE3_INTEGER, + ParameterType::STRING => SQLITE3_TEXT, + ParameterType::ASCII => SQLITE3_TEXT, + ParameterType::BINARY => SQLITE3_BLOB, + ParameterType::LARGE_OBJECT => SQLITE3_BLOB, + ParameterType::BOOLEAN => SQLITE3_INTEGER, + ]; + + private SQLite3 $connection; + private SQLite3Stmt $statement; + + /** @internal The statement can be only instantiated by its driver connection. */ + public function __construct(SQLite3 $connection, SQLite3Stmt $statement) + { + $this->connection = $connection; + $this->statement = $statement; + } + + /** + * @throws UnknownParameterType + * + * {@inheritDoc} + * + * @phpstan-assert ParameterType::* $type + */ + public function bindValue($param, $value, $type = ParameterType::STRING): bool + { + if (func_num_args() < 3) { + Deprecation::trigger( + 'doctrine/dbal', + 'https://github.com/doctrine/dbal/pull/5558', + 'Not passing $type to Statement::bindValue() is deprecated.' + . ' Pass the type corresponding to the parameter being bound.', + ); + } + + return $this->statement->bindValue($param, $value, $this->convertParamType($type)); + } + + /** + * @throws UnknownParameterType + * + * {@inheritDoc} + * + * @phpstan-assert ParameterType::* $type + */ + public function bindParam($param, &$variable, $type = ParameterType::STRING, $length = null): bool + { + Deprecation::trigger( + 'doctrine/dbal', + 'https://github.com/doctrine/dbal/pull/5563', + '%s is deprecated. Use bindValue() instead.', + __METHOD__, + ); + + if (func_num_args() < 3) { + Deprecation::trigger( + 'doctrine/dbal', + 'https://github.com/doctrine/dbal/pull/5558', + 'Not passing $type to Statement::bindParam() is deprecated.' + . ' Pass the type corresponding to the parameter being bound.', + ); + } + + return $this->statement->bindParam($param, $variable, $this->convertParamType($type)); + } + + /** @inheritDoc */ + public function execute($params = null): Result + { + if ($params !== null) { + Deprecation::trigger( + 'doctrine/dbal', + 'https://github.com/doctrine/dbal/pull/5556', + 'Passing $params to Statement::execute() is deprecated. Bind parameters using' + . ' Statement::bindParam() or Statement::bindValue() instead.', + ); + + foreach ($params as $param => $value) { + if (is_int($param)) { + $this->bindValue($param + 1, $value, ParameterType::STRING); + } else { + $this->bindValue($param, $value, ParameterType::STRING); + } + } + } + + try { + $result = $this->statement->execute(); + } catch (\Exception $e) { + throw Exception::new($e); + } + + assert($result !== false); + + return new Result($result, $this->connection->changes()); + } + + /** + * @phpstan-return value-of + * + * @phpstan-assert ParameterType::* $type + */ + private function convertParamType(int $type): int + { + if (! isset(self::PARAM_TYPE_MAP[$type])) { + throw UnknownParameterType::new($type); + } + + return self::PARAM_TYPE_MAP[$type]; + } +} diff --git a/3rdparty/doctrine/dbal/src/Driver/ServerInfoAwareConnection.php b/3rdparty/doctrine/dbal/src/Driver/ServerInfoAwareConnection.php new file mode 100644 index 00000000..5687ab0b --- /dev/null +++ b/3rdparty/doctrine/dbal/src/Driver/ServerInfoAwareConnection.php @@ -0,0 +1,21 @@ +execute() is called. + * + * As mentioned above, the named parameters are not natively supported by the mysqli driver, use executeQuery(), + * fetchAll(), fetchArray(), fetchColumn(), fetchAssoc() methods to have the named parameter emulated by doctrine. + * + * Most parameters are input parameters, that is, parameters that are + * used in a read-only fashion to build up the query. Some drivers support the invocation + * of stored procedures that return data as output parameters, and some also as input/output + * parameters that both send in data and are updated to receive it. + * + * @deprecated Use {@see bindValue()} instead. + * + * @param string|int $param Parameter identifier. For a prepared statement using named placeholders, + * this will be a parameter name of the form :name. For a prepared statement using + * question mark placeholders, this will be the 1-indexed position of the parameter. + * @param mixed $variable Name of the PHP variable to bind to the SQL statement parameter. + * @param int $type Explicit data type for the parameter using the {@see ParameterType} + * constants. + * @param int|null $length You must specify maxlength when using an OUT bind + * so that PHP allocates enough memory to hold the returned value. + * + * @return bool TRUE on success or FALSE on failure. + * + * @throws Exception + */ + public function bindParam($param, &$variable, $type = ParameterType::STRING, $length = null); + + /** + * Executes a prepared statement + * + * If the prepared statement included parameter markers, you must either: + * call {@see bindParam()} to bind PHP variables to the parameter markers: + * bound variables pass their value as input and receive the output value, + * if any, of their associated parameter markers or pass an array of input-only + * parameter values. + * + * @param mixed[]|null $params A numeric array of values with as many elements as there are + * bound parameters in the SQL statement being executed. + * + * @throws Exception + */ + public function execute($params = null): Result; +} diff --git a/3rdparty/doctrine/dbal/src/DriverManager.php b/3rdparty/doctrine/dbal/src/DriverManager.php new file mode 100644 index 00000000..7586bc47 --- /dev/null +++ b/3rdparty/doctrine/dbal/src/DriverManager.php @@ -0,0 +1,288 @@ +, + * driverClass?: class-string, + * driverOptions?: array, + * host?: string, + * password?: string, + * path?: string, + * persistent?: bool, + * platform?: Platforms\AbstractPlatform, + * port?: int, + * serverVersion?: string, + * url?: string, + * user?: string, + * unix_socket?: string, + * } + * @phpstan-type Params = array{ + * application_name?: string, + * charset?: string, + * dbname?: string, + * defaultTableOptions?: array, + * default_dbname?: string, + * driver?: key-of, + * driverClass?: class-string, + * driverOptions?: array, + * host?: string, + * keepSlave?: bool, + * keepReplica?: bool, + * master?: OverrideParams, + * memory?: bool, + * password?: string, + * path?: string, + * persistent?: bool, + * platform?: Platforms\AbstractPlatform, + * port?: int, + * primary?: OverrideParams, + * replica?: array, + * serverVersion?: string, + * sharding?: array, + * slaves?: array, + * url?: string, + * user?: string, + * wrapperClass?: class-string, + * unix_socket?: string, + * } + */ +final class DriverManager +{ + /** + * List of supported drivers and their mappings to the driver classes. + * + * To add your own driver use the 'driverClass' parameter to {@see DriverManager::getConnection()}. + */ + private const DRIVER_MAP = [ + 'pdo_mysql' => PDO\MySQL\Driver::class, + 'pdo_sqlite' => PDO\SQLite\Driver::class, + 'pdo_pgsql' => PDO\PgSQL\Driver::class, + 'pdo_oci' => PDO\OCI\Driver::class, + 'oci8' => OCI8\Driver::class, + 'ibm_db2' => IBMDB2\Driver::class, + 'pdo_sqlsrv' => PDO\SQLSrv\Driver::class, + 'mysqli' => Mysqli\Driver::class, + 'pgsql' => PgSQL\Driver::class, + 'sqlsrv' => SQLSrv\Driver::class, + 'sqlite3' => SQLite3\Driver::class, + ]; + + /** + * List of URL schemes from a database URL and their mappings to driver. + * + * @deprecated Use actual driver names instead. + * + * @var array + * @phpstan-var array> + */ + private static array $driverSchemeAliases = [ + 'db2' => 'ibm_db2', + 'mssql' => 'pdo_sqlsrv', + 'mysql' => 'pdo_mysql', + 'mysql2' => 'pdo_mysql', // Amazon RDS, for some weird reason + 'postgres' => 'pdo_pgsql', + 'postgresql' => 'pdo_pgsql', + 'pgsql' => 'pdo_pgsql', + 'sqlite' => 'pdo_sqlite', + 'sqlite3' => 'pdo_sqlite', + ]; + + /** + * Private constructor. This class cannot be instantiated. + * + * @codeCoverageIgnore + */ + private function __construct() + { + } + + /** + * Creates a connection object based on the specified parameters. + * This method returns a Doctrine\DBAL\Connection which wraps the underlying + * driver connection. + * + * $params must contain at least one of the following. + * + * Either 'driver' with one of the array keys of {@see DRIVER_MAP}, + * OR 'driverClass' that contains the full class name (with namespace) of the + * driver class to instantiate. + * + * Other (optional) parameters: + * + * user (string): + * The username to use when connecting. + * + * password (string): + * The password to use when connecting. + * + * driverOptions (array): + * Any additional driver-specific options for the driver. These are just passed + * through to the driver. + * + * wrapperClass: + * You may specify a custom wrapper class through the 'wrapperClass' + * parameter but this class MUST inherit from Doctrine\DBAL\Connection. + * + * driverClass: + * The driver class to use. + * + * @param Configuration|null $config The configuration to use. + * @param EventManager|null $eventManager The event manager to use. + * @phpstan-param Params $params + * + * @phpstan-return ($params is array{wrapperClass: class-string} ? T : Connection) + * + * @throws Exception + * + * @template T of Connection + */ + public static function getConnection( + #[SensitiveParameter] + array $params, + ?Configuration $config = null, + ?EventManager $eventManager = null + ): Connection { + // create default config and event manager, if not set + $config ??= new Configuration(); + $eventManager ??= new EventManager(); + $params = self::parseDatabaseUrl($params); + + // URL support for PrimaryReplicaConnection + if (isset($params['primary'])) { + $params['primary'] = self::parseDatabaseUrl($params['primary']); + } + + if (isset($params['replica'])) { + foreach ($params['replica'] as $key => $replicaParams) { + $params['replica'][$key] = self::parseDatabaseUrl($replicaParams); + } + } + + $driver = self::createDriver($params['driver'] ?? null, $params['driverClass'] ?? null); + + foreach ($config->getMiddlewares() as $middleware) { + $driver = $middleware->wrap($driver); + } + + $wrapperClass = $params['wrapperClass'] ?? Connection::class; + if (! is_a($wrapperClass, Connection::class, true)) { + throw Exception::invalidWrapperClass($wrapperClass); + } + + return new $wrapperClass($params, $driver, $config, $eventManager); + } + + /** + * Returns the list of supported drivers. + * + * @return string[] + * @phpstan-return list> + */ + public static function getAvailableDrivers(): array + { + return array_keys(self::DRIVER_MAP); + } + + /** + * @throws Exception + * + * @phpstan-assert key-of|null $driver + * @phpstan-assert class-string|null $driverClass + */ + private static function createDriver(?string $driver, ?string $driverClass): Driver + { + if ($driverClass === null) { + if ($driver === null) { + throw Exception::driverRequired(); + } + + if (! isset(self::DRIVER_MAP[$driver])) { + throw Exception::unknownDriver($driver, array_keys(self::DRIVER_MAP)); + } + + $driverClass = self::DRIVER_MAP[$driver]; + } elseif (! is_a($driverClass, Driver::class, true)) { + throw Exception::invalidDriverClass($driverClass); + } + + return new $driverClass(); + } + + /** + * Extracts parts from a database URL, if present, and returns an + * updated list of parameters. + * + * @param mixed[] $params The list of parameters. + * @phpstan-param Params $params + * + * @return mixed[] A modified list of parameters with info from a database + * URL extracted into indidivual parameter parts. + * @phpstan-return Params + * + * @throws Exception + */ + private static function parseDatabaseUrl( + #[SensitiveParameter] + array $params + ): array { + if (! isset($params['url'])) { + return $params; + } + + Deprecation::trigger( + 'doctrine/dbal', + 'https://github.com/doctrine/dbal/pull/5843', + 'The "url" connection parameter is deprecated. Please use %s to parse a database url before calling %s.', + DsnParser::class, + self::class, + ); + + $parser = new DsnParser(self::$driverSchemeAliases); + try { + $parsedParams = $parser->parse($params['url']); + } catch (MalformedDsnException $e) { + throw new Exception('Malformed parameter "url".', 0, $e); + } + + if (isset($parsedParams['driver'])) { + // The requested driver from the URL scheme takes precedence + // over the default custom driver from the connection parameters (if any). + unset($params['driverClass']); + } + + $params = array_merge($params, $parsedParams); + + // If a schemeless connection URL is given, we require a default driver or default custom driver + // as connection parameter. + if (! isset($params['driverClass']) && ! isset($params['driver'])) { + throw Exception::driverRequired($params['url']); + } + + return $params; + } +} diff --git a/3rdparty/doctrine/dbal/src/Event/ConnectionEventArgs.php b/3rdparty/doctrine/dbal/src/Event/ConnectionEventArgs.php new file mode 100644 index 00000000..9a69c254 --- /dev/null +++ b/3rdparty/doctrine/dbal/src/Event/ConnectionEventArgs.php @@ -0,0 +1,27 @@ +connection = $connection; + } + + /** @return Connection */ + public function getConnection() + { + return $this->connection; + } +} diff --git a/3rdparty/doctrine/dbal/src/Event/Listeners/OracleSessionInit.php b/3rdparty/doctrine/dbal/src/Event/Listeners/OracleSessionInit.php new file mode 100644 index 00000000..9598f43c --- /dev/null +++ b/3rdparty/doctrine/dbal/src/Event/Listeners/OracleSessionInit.php @@ -0,0 +1,77 @@ + 'HH24:MI:SS', + 'NLS_DATE_FORMAT' => 'YYYY-MM-DD HH24:MI:SS', + 'NLS_TIMESTAMP_FORMAT' => 'YYYY-MM-DD HH24:MI:SS', + 'NLS_TIMESTAMP_TZ_FORMAT' => 'YYYY-MM-DD HH24:MI:SS TZH:TZM', + 'NLS_NUMERIC_CHARACTERS' => '.,', + ]; + + /** @param string[] $oracleSessionVars */ + public function __construct(array $oracleSessionVars = []) + { + $this->_defaultSessionVars = array_merge($this->_defaultSessionVars, $oracleSessionVars); + } + + /** + * @return void + * + * @throws Exception + */ + public function postConnect(ConnectionEventArgs $args) + { + if (count($this->_defaultSessionVars) === 0) { + return; + } + + $vars = []; + foreach (array_change_key_case($this->_defaultSessionVars, CASE_UPPER) as $option => $value) { + if ($option === 'CURRENT_SCHEMA') { + $vars[] = $option . ' = ' . $value; + } else { + $vars[] = $option . " = '" . $value . "'"; + } + } + + $sql = 'ALTER SESSION SET ' . implode(' ', $vars); + $args->getConnection()->executeStatement($sql); + } + + /** + * {@inheritDoc} + */ + public function getSubscribedEvents() + { + return [Events::postConnect]; + } +} diff --git a/3rdparty/doctrine/dbal/src/Event/Listeners/SQLSessionInit.php b/3rdparty/doctrine/dbal/src/Event/Listeners/SQLSessionInit.php new file mode 100644 index 00000000..4ce32d62 --- /dev/null +++ b/3rdparty/doctrine/dbal/src/Event/Listeners/SQLSessionInit.php @@ -0,0 +1,43 @@ +sql = $sql; + } + + /** + * @return void + * + * @throws Exception + */ + public function postConnect(ConnectionEventArgs $args) + { + $args->getConnection()->executeStatement($this->sql); + } + + /** + * {@inheritDoc} + */ + public function getSubscribedEvents() + { + return [Events::postConnect]; + } +} diff --git a/3rdparty/doctrine/dbal/src/Event/Listeners/SQLiteSessionInit.php b/3rdparty/doctrine/dbal/src/Event/Listeners/SQLiteSessionInit.php new file mode 100644 index 00000000..950f05f4 --- /dev/null +++ b/3rdparty/doctrine/dbal/src/Event/Listeners/SQLiteSessionInit.php @@ -0,0 +1,30 @@ +getConnection()->executeStatement('PRAGMA foreign_keys=ON'); + } + + /** + * {@inheritDoc} + */ + public function getSubscribedEvents() + { + return [Events::postConnect]; + } +} diff --git a/3rdparty/doctrine/dbal/src/Event/SchemaAlterTableAddColumnEventArgs.php b/3rdparty/doctrine/dbal/src/Event/SchemaAlterTableAddColumnEventArgs.php new file mode 100644 index 00000000..9f3ff6ea --- /dev/null +++ b/3rdparty/doctrine/dbal/src/Event/SchemaAlterTableAddColumnEventArgs.php @@ -0,0 +1,81 @@ +column = $column; + $this->tableDiff = $tableDiff; + $this->platform = $platform; + } + + /** @return Column */ + public function getColumn() + { + return $this->column; + } + + /** @return TableDiff */ + public function getTableDiff() + { + return $this->tableDiff; + } + + /** @return AbstractPlatform */ + public function getPlatform() + { + return $this->platform; + } + + /** + * Passing multiple SQL statements as an array is deprecated. Pass each statement as an individual argument instead. + * + * @param string|string[] $sql + * + * @return SchemaAlterTableAddColumnEventArgs + */ + public function addSql($sql) + { + if (is_array($sql)) { + Deprecation::trigger( + 'doctrine/dbal', + 'https://github.com/doctrine/dbal/issues/3580', + 'Passing multiple SQL statements as an array to SchemaAlterTableAddColumnEventaArrgs::addSql() ' . + 'is deprecated. Pass each statement as an individual argument instead.', + ); + } + + $this->sql = array_merge($this->sql, is_array($sql) ? $sql : func_get_args()); + + return $this; + } + + /** @return string[] */ + public function getSql() + { + return $this->sql; + } +} diff --git a/3rdparty/doctrine/dbal/src/Event/SchemaAlterTableChangeColumnEventArgs.php b/3rdparty/doctrine/dbal/src/Event/SchemaAlterTableChangeColumnEventArgs.php new file mode 100644 index 00000000..9ba37aad --- /dev/null +++ b/3rdparty/doctrine/dbal/src/Event/SchemaAlterTableChangeColumnEventArgs.php @@ -0,0 +1,71 @@ +columnDiff = $columnDiff; + $this->tableDiff = $tableDiff; + $this->platform = $platform; + } + + /** @return ColumnDiff */ + public function getColumnDiff() + { + return $this->columnDiff; + } + + /** @return TableDiff */ + public function getTableDiff() + { + return $this->tableDiff; + } + + /** @return AbstractPlatform */ + public function getPlatform() + { + return $this->platform; + } + + /** + * Passing multiple SQL statements as an array is deprecated. Pass each statement as an individual argument instead. + * + * @param string|string[] $sql + * + * @return SchemaAlterTableChangeColumnEventArgs + */ + public function addSql($sql) + { + $this->sql = array_merge($this->sql, is_array($sql) ? $sql : func_get_args()); + + return $this; + } + + /** @return string[] */ + public function getSql() + { + return $this->sql; + } +} diff --git a/3rdparty/doctrine/dbal/src/Event/SchemaAlterTableEventArgs.php b/3rdparty/doctrine/dbal/src/Event/SchemaAlterTableEventArgs.php new file mode 100644 index 00000000..07c065a9 --- /dev/null +++ b/3rdparty/doctrine/dbal/src/Event/SchemaAlterTableEventArgs.php @@ -0,0 +1,62 @@ +tableDiff = $tableDiff; + $this->platform = $platform; + } + + /** @return TableDiff */ + public function getTableDiff() + { + return $this->tableDiff; + } + + /** @return AbstractPlatform */ + public function getPlatform() + { + return $this->platform; + } + + /** + * Passing multiple SQL statements as an array is deprecated. Pass each statement as an individual argument instead. + * + * @param string|string[] $sql + * + * @return SchemaAlterTableEventArgs + */ + public function addSql($sql) + { + $this->sql = array_merge($this->sql, is_array($sql) ? $sql : func_get_args()); + + return $this; + } + + /** @return string[] */ + public function getSql() + { + return $this->sql; + } +} diff --git a/3rdparty/doctrine/dbal/src/Event/SchemaAlterTableRemoveColumnEventArgs.php b/3rdparty/doctrine/dbal/src/Event/SchemaAlterTableRemoveColumnEventArgs.php new file mode 100644 index 00000000..4122b418 --- /dev/null +++ b/3rdparty/doctrine/dbal/src/Event/SchemaAlterTableRemoveColumnEventArgs.php @@ -0,0 +1,71 @@ +column = $column; + $this->tableDiff = $tableDiff; + $this->platform = $platform; + } + + /** @return Column */ + public function getColumn() + { + return $this->column; + } + + /** @return TableDiff */ + public function getTableDiff() + { + return $this->tableDiff; + } + + /** @return AbstractPlatform */ + public function getPlatform() + { + return $this->platform; + } + + /** + * Passing multiple SQL statements as an array is deprecated. Pass each statement as an individual argument instead. + * + * @param string|string[] $sql + * + * @return SchemaAlterTableRemoveColumnEventArgs + */ + public function addSql($sql) + { + $this->sql = array_merge($this->sql, is_array($sql) ? $sql : func_get_args()); + + return $this; + } + + /** @return string[] */ + public function getSql() + { + return $this->sql; + } +} diff --git a/3rdparty/doctrine/dbal/src/Event/SchemaAlterTableRenameColumnEventArgs.php b/3rdparty/doctrine/dbal/src/Event/SchemaAlterTableRenameColumnEventArgs.php new file mode 100644 index 00000000..21d3c164 --- /dev/null +++ b/3rdparty/doctrine/dbal/src/Event/SchemaAlterTableRenameColumnEventArgs.php @@ -0,0 +1,82 @@ +oldColumnName = $oldColumnName; + $this->column = $column; + $this->tableDiff = $tableDiff; + $this->platform = $platform; + } + + /** @return string */ + public function getOldColumnName() + { + return $this->oldColumnName; + } + + /** @return Column */ + public function getColumn() + { + return $this->column; + } + + /** @return TableDiff */ + public function getTableDiff() + { + return $this->tableDiff; + } + + /** @return AbstractPlatform */ + public function getPlatform() + { + return $this->platform; + } + + /** + * Passing multiple SQL statements as an array is deprecated. Pass each statement as an individual argument instead. + * + * @param string|string[] $sql + * + * @return SchemaAlterTableRenameColumnEventArgs + */ + public function addSql($sql) + { + $this->sql = array_merge($this->sql, is_array($sql) ? $sql : func_get_args()); + + return $this; + } + + /** @return string[] */ + public function getSql() + { + return $this->sql; + } +} diff --git a/3rdparty/doctrine/dbal/src/Event/SchemaColumnDefinitionEventArgs.php b/3rdparty/doctrine/dbal/src/Event/SchemaColumnDefinitionEventArgs.php new file mode 100644 index 00000000..04fcbde9 --- /dev/null +++ b/3rdparty/doctrine/dbal/src/Event/SchemaColumnDefinitionEventArgs.php @@ -0,0 +1,87 @@ +tableColumn = $tableColumn; + $this->table = $table; + $this->database = $database; + $this->connection = $connection; + } + + /** + * Allows to clear the column which means the column will be excluded from + * tables column list. + * + * @return SchemaColumnDefinitionEventArgs + */ + public function setColumn(?Column $column = null) + { + $this->column = $column; + + return $this; + } + + /** @return Column|null */ + public function getColumn() + { + return $this->column; + } + + /** @return mixed[] */ + public function getTableColumn() + { + return $this->tableColumn; + } + + /** @return string */ + public function getTable() + { + return $this->table; + } + + /** @return string */ + public function getDatabase() + { + return $this->database; + } + + /** @return Connection */ + public function getConnection() + { + return $this->connection; + } +} diff --git a/3rdparty/doctrine/dbal/src/Event/SchemaCreateTableColumnEventArgs.php b/3rdparty/doctrine/dbal/src/Event/SchemaCreateTableColumnEventArgs.php new file mode 100644 index 00000000..54f134d1 --- /dev/null +++ b/3rdparty/doctrine/dbal/src/Event/SchemaCreateTableColumnEventArgs.php @@ -0,0 +1,71 @@ +column = $column; + $this->table = $table; + $this->platform = $platform; + } + + /** @return Column */ + public function getColumn() + { + return $this->column; + } + + /** @return Table */ + public function getTable() + { + return $this->table; + } + + /** @return AbstractPlatform */ + public function getPlatform() + { + return $this->platform; + } + + /** + * Passing multiple SQL statements as an array is deprecated. Pass each statement as an individual argument instead. + * + * @param string|string[] $sql + * + * @return SchemaCreateTableColumnEventArgs + */ + public function addSql($sql) + { + $this->sql = array_merge($this->sql, is_array($sql) ? $sql : func_get_args()); + + return $this; + } + + /** @return string[] */ + public function getSql() + { + return $this->sql; + } +} diff --git a/3rdparty/doctrine/dbal/src/Event/SchemaCreateTableEventArgs.php b/3rdparty/doctrine/dbal/src/Event/SchemaCreateTableEventArgs.php new file mode 100644 index 00000000..a7d548de --- /dev/null +++ b/3rdparty/doctrine/dbal/src/Event/SchemaCreateTableEventArgs.php @@ -0,0 +1,87 @@ +table = $table; + $this->columns = $columns; + $this->options = $options; + $this->platform = $platform; + } + + /** @return Table */ + public function getTable() + { + return $this->table; + } + + /** @return mixed[][] */ + public function getColumns() + { + return $this->columns; + } + + /** @return mixed[] */ + public function getOptions() + { + return $this->options; + } + + /** @return AbstractPlatform */ + public function getPlatform() + { + return $this->platform; + } + + /** + * Passing multiple SQL statements as an array is deprecated. Pass each statement as an individual argument instead. + * + * @param string|string[] $sql + * + * @return SchemaCreateTableEventArgs + */ + public function addSql($sql) + { + $this->sql = array_merge($this->sql, is_array($sql) ? $sql : func_get_args()); + + return $this; + } + + /** @return string[] */ + public function getSql() + { + return $this->sql; + } +} diff --git a/3rdparty/doctrine/dbal/src/Event/SchemaDropTableEventArgs.php b/3rdparty/doctrine/dbal/src/Event/SchemaDropTableEventArgs.php new file mode 100644 index 00000000..6f279e96 --- /dev/null +++ b/3rdparty/doctrine/dbal/src/Event/SchemaDropTableEventArgs.php @@ -0,0 +1,59 @@ +table = $table; + $this->platform = $platform; + } + + /** @return string|Table */ + public function getTable() + { + return $this->table; + } + + /** @return AbstractPlatform */ + public function getPlatform() + { + return $this->platform; + } + + /** + * @param string $sql + * + * @return SchemaDropTableEventArgs + */ + public function setSql($sql) + { + $this->sql = $sql; + + return $this; + } + + /** @return string|null */ + public function getSql() + { + return $this->sql; + } +} diff --git a/3rdparty/doctrine/dbal/src/Event/SchemaEventArgs.php b/3rdparty/doctrine/dbal/src/Event/SchemaEventArgs.php new file mode 100644 index 00000000..77d1d390 --- /dev/null +++ b/3rdparty/doctrine/dbal/src/Event/SchemaEventArgs.php @@ -0,0 +1,29 @@ +preventDefault = true; + + return $this; + } + + /** @return bool */ + public function isDefaultPrevented() + { + return $this->preventDefault; + } +} diff --git a/3rdparty/doctrine/dbal/src/Event/SchemaIndexDefinitionEventArgs.php b/3rdparty/doctrine/dbal/src/Event/SchemaIndexDefinitionEventArgs.php new file mode 100644 index 00000000..dbee55a0 --- /dev/null +++ b/3rdparty/doctrine/dbal/src/Event/SchemaIndexDefinitionEventArgs.php @@ -0,0 +1,75 @@ +tableIndex = $tableIndex; + $this->table = $table; + $this->connection = $connection; + } + + /** + * Allows to clear the index which means the index will be excluded from tables index list. + * + * @return SchemaIndexDefinitionEventArgs + */ + public function setIndex(?Index $index = null) + { + $this->index = $index; + + return $this; + } + + /** @return Index|null */ + public function getIndex() + { + return $this->index; + } + + /** @return mixed[] */ + public function getTableIndex() + { + return $this->tableIndex; + } + + /** @return string */ + public function getTable() + { + return $this->table; + } + + /** @return Connection */ + public function getConnection() + { + return $this->connection; + } +} diff --git a/3rdparty/doctrine/dbal/src/Event/TransactionBeginEventArgs.php b/3rdparty/doctrine/dbal/src/Event/TransactionBeginEventArgs.php new file mode 100644 index 00000000..be4ccdf1 --- /dev/null +++ b/3rdparty/doctrine/dbal/src/Event/TransactionBeginEventArgs.php @@ -0,0 +1,10 @@ +connection = $connection; + } + + public function getConnection(): Connection + { + return $this->connection; + } +} diff --git a/3rdparty/doctrine/dbal/src/Event/TransactionRollBackEventArgs.php b/3rdparty/doctrine/dbal/src/Event/TransactionRollBackEventArgs.php new file mode 100644 index 00000000..9e6e650d --- /dev/null +++ b/3rdparty/doctrine/dbal/src/Event/TransactionRollBackEventArgs.php @@ -0,0 +1,10 @@ +getMessage(); + } else { + $message = 'An exception occurred in the driver: ' . $driverException->getMessage(); + } + + parent::__construct($message, $driverException->getCode(), $driverException); + + $this->query = $query; + } + + /** + * {@inheritDoc} + */ + public function getSQLState() + { + $previous = $this->getPrevious(); + assert($previous instanceof TheDriverException); + + return $previous->getSQLState(); + } + + public function getQuery(): ?Query + { + return $this->query; + } +} diff --git a/3rdparty/doctrine/dbal/src/Exception/ForeignKeyConstraintViolationException.php b/3rdparty/doctrine/dbal/src/Exception/ForeignKeyConstraintViolationException.php new file mode 100644 index 00000000..48d736f9 --- /dev/null +++ b/3rdparty/doctrine/dbal/src/Exception/ForeignKeyConstraintViolationException.php @@ -0,0 +1,10 @@ +|array */ + private array $originalParameters; + + /** @var array|array */ + private array $originalTypes; + + private int $originalParameterIndex = 0; + + /** @var list */ + private array $convertedSQL = []; + + /** @var list */ + private array $convertedParameters = []; + + /** @var array */ + private array $convertedTypes = []; + + /** + * @param array|array $parameters + * @param array|array $types + */ + public function __construct(array $parameters, array $types) + { + $this->originalParameters = $parameters; + $this->originalTypes = $types; + } + + public function acceptPositionalParameter(string $sql): void + { + $index = $this->originalParameterIndex; + + if (! array_key_exists($index, $this->originalParameters)) { + throw MissingPositionalParameter::new($index); + } + + $this->acceptParameter($index, $this->originalParameters[$index]); + + $this->originalParameterIndex++; + } + + public function acceptNamedParameter(string $sql): void + { + $name = substr($sql, 1); + + if (! array_key_exists($name, $this->originalParameters)) { + throw MissingNamedParameter::new($name); + } + + $this->acceptParameter($name, $this->originalParameters[$name]); + } + + public function acceptOther(string $sql): void + { + $this->convertedSQL[] = $sql; + } + + public function getSQL(): string + { + return implode('', $this->convertedSQL); + } + + /** @return list */ + public function getParameters(): array + { + return $this->convertedParameters; + } + + /** + * @param int|string $key + * @param mixed $value + */ + private function acceptParameter($key, $value): void + { + if (! isset($this->originalTypes[$key])) { + $this->convertedSQL[] = '?'; + $this->convertedParameters[] = $value; + + return; + } + + $type = $this->originalTypes[$key]; + + if ( + $type !== ArrayParameterType::INTEGER + && $type !== ArrayParameterType::STRING + && $type !== ArrayParameterType::ASCII + && $type !== ArrayParameterType::BINARY + ) { + $this->appendTypedParameter([$value], $type); + + return; + } + + if (count($value) === 0) { + $this->convertedSQL[] = 'NULL'; + + return; + } + + $this->appendTypedParameter($value, ArrayParameterType::toElementParameterType($type)); + } + + /** @return array */ + public function getTypes(): array + { + return $this->convertedTypes; + } + + /** + * @param list $values + * @param Type|int|string|null $type + */ + private function appendTypedParameter(array $values, $type): void + { + $this->convertedSQL[] = implode(', ', array_fill(0, count($values), '?')); + + $index = count($this->convertedParameters); + + foreach ($values as $value) { + $this->convertedParameters[] = $value; + $this->convertedTypes[$index] = $type; + + $index++; + } + } +} diff --git a/3rdparty/doctrine/dbal/src/FetchMode.php b/3rdparty/doctrine/dbal/src/FetchMode.php new file mode 100644 index 00000000..d80719f2 --- /dev/null +++ b/3rdparty/doctrine/dbal/src/FetchMode.php @@ -0,0 +1,20 @@ +getDriver() instanceof Driver\PDO\SQLite\Driver) { + throw new Exception('Cannot use TableGenerator with SQLite.'); + } + + $this->conn = DriverManager::getConnection( + $conn->getParams(), + $conn->getConfiguration(), + $conn->getEventManager(), + ); + + $this->generatorTableName = $generatorTableName; + } + + /** + * Generates the next unused value for the given sequence name. + * + * @param string $sequence + * + * @return int + * + * @throws Exception + */ + public function nextValue($sequence) + { + if (isset($this->sequences[$sequence])) { + $value = $this->sequences[$sequence]['value']; + $this->sequences[$sequence]['value']++; + if ($this->sequences[$sequence]['value'] >= $this->sequences[$sequence]['max']) { + unset($this->sequences[$sequence]); + } + + return $value; + } + + $this->conn->beginTransaction(); + + try { + $row = $this->conn->createQueryBuilder() + ->select('sequence_value', 'sequence_increment_by') + ->from($this->generatorTableName) + ->where('sequence_name = ?') + ->forUpdate() + ->setParameter(1, $sequence) + ->fetchAssociative(); + + if ($row !== false) { + $row = array_change_key_case($row, CASE_LOWER); + + $value = $row['sequence_value']; + $value++; + + assert(is_int($value)); + + if ($row['sequence_increment_by'] > 1) { + $this->sequences[$sequence] = [ + 'value' => $value, + 'max' => $row['sequence_value'] + $row['sequence_increment_by'], + ]; + } + + $sql = 'UPDATE ' . $this->generatorTableName . ' ' . + 'SET sequence_value = sequence_value + sequence_increment_by ' . + 'WHERE sequence_name = ? AND sequence_value = ?'; + $rows = $this->conn->executeStatement($sql, [$sequence, $row['sequence_value']]); + + if ($rows !== 1) { + throw new Exception('Race-condition detected while updating sequence. Aborting generation'); + } + } else { + $this->conn->insert( + $this->generatorTableName, + ['sequence_name' => $sequence, 'sequence_value' => 1, 'sequence_increment_by' => 1], + ); + $value = 1; + } + + $this->conn->commit(); + } catch (Throwable $e) { + $this->conn->rollBack(); + + throw new Exception( + 'Error occurred while generating ID with TableGenerator, aborted generation: ' . $e->getMessage(), + 0, + $e, + ); + } + + return $value; + } +} diff --git a/3rdparty/doctrine/dbal/src/Id/TableGeneratorSchemaVisitor.php b/3rdparty/doctrine/dbal/src/Id/TableGeneratorSchemaVisitor.php new file mode 100644 index 00000000..75c9fe9c --- /dev/null +++ b/3rdparty/doctrine/dbal/src/Id/TableGeneratorSchemaVisitor.php @@ -0,0 +1,77 @@ +generatorTableName = $generatorTableName; + } + + /** + * {@inheritDoc} + */ + public function acceptSchema(Schema $schema) + { + $table = $schema->createTable($this->generatorTableName); + $table->addColumn('sequence_name', 'string'); + $table->addColumn('sequence_value', 'integer', ['default' => 1]); + $table->addColumn('sequence_increment_by', 'integer', ['default' => 1]); + } + + /** + * {@inheritDoc} + */ + public function acceptTable(Table $table) + { + } + + /** + * {@inheritDoc} + */ + public function acceptColumn(Table $table, Column $column) + { + } + + /** + * {@inheritDoc} + */ + public function acceptForeignKey(Table $localTable, ForeignKeyConstraint $fkConstraint) + { + } + + /** + * {@inheritDoc} + */ + public function acceptIndex(Table $table, Index $index) + { + } + + /** + * {@inheritDoc} + */ + public function acceptSequence(Sequence $sequence) + { + } +} diff --git a/3rdparty/doctrine/dbal/src/LockMode.php b/3rdparty/doctrine/dbal/src/LockMode.php new file mode 100644 index 00000000..1a7ed239 --- /dev/null +++ b/3rdparty/doctrine/dbal/src/LockMode.php @@ -0,0 +1,23 @@ +logger = $logger; + } + + public function __destruct() + { + $this->logger->info('Disconnecting'); + } + + public function prepare(string $sql): DriverStatement + { + return new Statement( + parent::prepare($sql), + $this->logger, + $sql, + ); + } + + public function query(string $sql): Result + { + $this->logger->debug('Executing query: {sql}', ['sql' => $sql]); + + return parent::query($sql); + } + + public function exec(string $sql): int + { + $this->logger->debug('Executing statement: {sql}', ['sql' => $sql]); + + return parent::exec($sql); + } + + /** + * {@inheritDoc} + */ + public function beginTransaction() + { + $this->logger->debug('Beginning transaction'); + + return parent::beginTransaction(); + } + + /** + * {@inheritDoc} + */ + public function commit() + { + $this->logger->debug('Committing transaction'); + + return parent::commit(); + } + + /** + * {@inheritDoc} + */ + public function rollBack() + { + $this->logger->debug('Rolling back transaction'); + + return parent::rollBack(); + } +} diff --git a/3rdparty/doctrine/dbal/src/Logging/DebugStack.php b/3rdparty/doctrine/dbal/src/Logging/DebugStack.php new file mode 100644 index 00000000..1a970d06 --- /dev/null +++ b/3rdparty/doctrine/dbal/src/Logging/DebugStack.php @@ -0,0 +1,75 @@ +> + */ + public $queries = []; + + /** + * If Debug Stack is enabled (log queries) or not. + * + * @var bool + */ + public $enabled = true; + + /** @var float|null */ + public $start = null; + + /** @var int */ + public $currentQuery = 0; + + public function __construct() + { + Deprecation::trigger( + 'doctrine/dbal', + 'https://github.com/doctrine/dbal/pull/4967', + 'DebugStack is deprecated.', + ); + } + + /** + * {@inheritDoc} + */ + public function startQuery($sql, ?array $params = null, ?array $types = null) + { + if (! $this->enabled) { + return; + } + + $this->start = microtime(true); + + $this->queries[++$this->currentQuery] = [ + 'sql' => $sql, + 'params' => $params, + 'types' => $types, + 'executionMS' => 0, + ]; + } + + /** + * {@inheritDoc} + */ + public function stopQuery() + { + if (! $this->enabled) { + return; + } + + $this->queries[$this->currentQuery]['executionMS'] = microtime(true) - $this->start; + } +} diff --git a/3rdparty/doctrine/dbal/src/Logging/Driver.php b/3rdparty/doctrine/dbal/src/Logging/Driver.php new file mode 100644 index 00000000..32a5cd2c --- /dev/null +++ b/3rdparty/doctrine/dbal/src/Logging/Driver.php @@ -0,0 +1,58 @@ +logger = $logger; + } + + /** + * {@inheritDoc} + */ + public function connect( + #[SensitiveParameter] + array $params + ) { + $this->logger->info('Connecting with parameters {params}', ['params' => $this->maskPassword($params)]); + + return new Connection( + parent::connect($params), + $this->logger, + ); + } + + /** + * @param array $params Connection parameters + * + * @return array + */ + private function maskPassword( + #[SensitiveParameter] + array $params + ): array { + if (isset($params['password'])) { + $params['password'] = ''; + } + + if (isset($params['url'])) { + $params['url'] = ''; + } + + return $params; + } +} diff --git a/3rdparty/doctrine/dbal/src/Logging/LoggerChain.php b/3rdparty/doctrine/dbal/src/Logging/LoggerChain.php new file mode 100644 index 00000000..7a4eaa49 --- /dev/null +++ b/3rdparty/doctrine/dbal/src/Logging/LoggerChain.php @@ -0,0 +1,48 @@ + */ + private iterable $loggers; + + /** @param iterable $loggers */ + public function __construct(iterable $loggers = []) + { + Deprecation::trigger( + 'doctrine/dbal', + 'https://github.com/doctrine/dbal/pull/4967', + 'LoggerChain is deprecated', + ); + + $this->loggers = $loggers; + } + + /** + * {@inheritDoc} + */ + public function startQuery($sql, ?array $params = null, ?array $types = null) + { + foreach ($this->loggers as $logger) { + $logger->startQuery($sql, $params, $types); + } + } + + /** + * {@inheritDoc} + */ + public function stopQuery() + { + foreach ($this->loggers as $logger) { + $logger->stopQuery(); + } + } +} diff --git a/3rdparty/doctrine/dbal/src/Logging/Middleware.php b/3rdparty/doctrine/dbal/src/Logging/Middleware.php new file mode 100644 index 00000000..da0c1b90 --- /dev/null +++ b/3rdparty/doctrine/dbal/src/Logging/Middleware.php @@ -0,0 +1,24 @@ +logger = $logger; + } + + public function wrap(DriverInterface $driver): DriverInterface + { + return new Driver($driver, $this->logger); + } +} diff --git a/3rdparty/doctrine/dbal/src/Logging/SQLLogger.php b/3rdparty/doctrine/dbal/src/Logging/SQLLogger.php new file mode 100644 index 00000000..dab4a3a7 --- /dev/null +++ b/3rdparty/doctrine/dbal/src/Logging/SQLLogger.php @@ -0,0 +1,32 @@ +|array|null $params Statement parameters + * @param array|array|null $types Parameter types + * + * @return void + */ + public function startQuery($sql, ?array $params = null, ?array $types = null); + + /** + * Marks the last started query as stopped. This can be used for timing of queries. + * + * @return void + */ + public function stopQuery(); +} diff --git a/3rdparty/doctrine/dbal/src/Logging/Statement.php b/3rdparty/doctrine/dbal/src/Logging/Statement.php new file mode 100644 index 00000000..039b93b6 --- /dev/null +++ b/3rdparty/doctrine/dbal/src/Logging/Statement.php @@ -0,0 +1,100 @@ +|array */ + private array $params = []; + + /** @var array|array */ + private array $types = []; + + /** @internal This statement can be only instantiated by its connection. */ + public function __construct(StatementInterface $statement, LoggerInterface $logger, string $sql) + { + parent::__construct($statement); + + $this->logger = $logger; + $this->sql = $sql; + } + + /** + * {@inheritDoc} + * + * @deprecated Use {@see bindValue()} instead. + */ + public function bindParam($param, &$variable, $type = ParameterType::STRING, $length = null) + { + Deprecation::trigger( + 'doctrine/dbal', + 'https://github.com/doctrine/dbal/pull/5563', + '%s is deprecated. Use bindValue() instead.', + __METHOD__, + ); + + if (func_num_args() < 3) { + Deprecation::trigger( + 'doctrine/dbal', + 'https://github.com/doctrine/dbal/pull/5558', + 'Not passing $type to Statement::bindParam() is deprecated.' + . ' Pass the type corresponding to the parameter being bound.', + ); + } + + $this->params[$param] = &$variable; + $this->types[$param] = $type; + + return parent::bindParam($param, $variable, $type, ...array_slice(func_get_args(), 3)); + } + + /** + * {@inheritDoc} + */ + public function bindValue($param, $value, $type = ParameterType::STRING) + { + if (func_num_args() < 3) { + Deprecation::trigger( + 'doctrine/dbal', + 'https://github.com/doctrine/dbal/pull/5558', + 'Not passing $type to Statement::bindValue() is deprecated.' + . ' Pass the type corresponding to the parameter being bound.', + ); + } + + $this->params[$param] = $value; + $this->types[$param] = $type; + + return parent::bindValue($param, $value, $type); + } + + /** + * {@inheritDoc} + */ + public function execute($params = null): ResultInterface + { + $this->logger->debug('Executing statement: {sql} (parameters: {params}, types: {types})', [ + 'sql' => $this->sql, + 'params' => $params ?? $this->params, + 'types' => $this->types, + ]); + + return parent::execute($params); + } +} diff --git a/3rdparty/doctrine/dbal/src/ParameterType.php b/3rdparty/doctrine/dbal/src/ParameterType.php new file mode 100644 index 00000000..77917e87 --- /dev/null +++ b/3rdparty/doctrine/dbal/src/ParameterType.php @@ -0,0 +1,57 @@ + 0) { + $query .= sprintf(' OFFSET %d', $offset); + } + } elseif ($offset > 0) { + // 2^64-1 is the maximum of unsigned BIGINT, the biggest limit possible + $query .= sprintf(' LIMIT 18446744073709551615 OFFSET %d', $offset); + } + + return $query; + } + + /** + * {@inheritDoc} + * + * @deprecated Use {@see quoteIdentifier()} to quote identifiers instead. + */ + public function getIdentifierQuoteCharacter() + { + Deprecation::triggerIfCalledFromOutside( + 'doctrine/dbal', + 'https://github.com/doctrine/dbal/pull/5388', + 'AbstractMySQLPlatform::getIdentifierQuoteCharacter() is deprecated. Use quoteIdentifier() instead.', + ); + + return '`'; + } + + /** + * {@inheritDoc} + */ + public function getRegexpExpression() + { + return 'RLIKE'; + } + + /** + * {@inheritDoc} + */ + public function getLocateExpression($str, $substr, $startPos = false) + { + if ($startPos === false) { + return 'LOCATE(' . $substr . ', ' . $str . ')'; + } + + return 'LOCATE(' . $substr . ', ' . $str . ', ' . $startPos . ')'; + } + + /** + * {@inheritDoc} + */ + public function getConcatExpression() + { + return sprintf('CONCAT(%s)', implode(', ', func_get_args())); + } + + /** + * {@inheritDoc} + */ + protected function getDateArithmeticIntervalExpression($date, $operator, $interval, $unit) + { + $function = $operator === '+' ? 'DATE_ADD' : 'DATE_SUB'; + + return $function . '(' . $date . ', INTERVAL ' . $interval . ' ' . $unit . ')'; + } + + /** + * {@inheritDoc} + */ + public function getDateDiffExpression($date1, $date2) + { + return 'DATEDIFF(' . $date1 . ', ' . $date2 . ')'; + } + + public function getCurrentDatabaseExpression(): string + { + return 'DATABASE()'; + } + + /** + * {@inheritDoc} + */ + public function getLengthExpression($column) + { + return 'CHAR_LENGTH(' . $column . ')'; + } + + /** + * {@inheritDoc} + * + * @internal The method should be only used from within the {@see AbstractSchemaManager} class hierarchy. + */ + public function getListDatabasesSQL() + { + return 'SHOW DATABASES'; + } + + /** + * @deprecated + * + * {@inheritDoc} + */ + public function getListTableConstraintsSQL($table) + { + return 'SHOW INDEX FROM ' . $table; + } + + /** + * @deprecated The SQL used for schema introspection is an implementation detail and should not be relied upon. + * + * {@inheritDoc} + * + * Two approaches to listing the table indexes. The information_schema is + * preferred, because it doesn't cause problems with SQL keywords such as "order" or "table". + */ + public function getListTableIndexesSQL($table, $database = null) + { + if ($database !== null) { + return 'SELECT NON_UNIQUE AS Non_Unique, INDEX_NAME AS Key_name, COLUMN_NAME AS Column_Name,' . + ' SUB_PART AS Sub_Part, INDEX_TYPE AS Index_Type' . + ' FROM information_schema.STATISTICS WHERE TABLE_NAME = ' . $this->quoteStringLiteral($table) . + ' AND TABLE_SCHEMA = ' . $this->quoteStringLiteral($database) . + ' ORDER BY SEQ_IN_INDEX ASC'; + } + + return 'SHOW INDEX FROM ' . $table; + } + + /** + * {@inheritDoc} + * + * @internal The method should be only used from within the {@see AbstractSchemaManager} class hierarchy. + */ + public function getListViewsSQL($database) + { + return 'SELECT * FROM information_schema.VIEWS WHERE TABLE_SCHEMA = ' . $this->quoteStringLiteral($database); + } + + /** + * @deprecated The SQL used for schema introspection is an implementation detail and should not be relied upon. + * + * @param string $table + * @param string|null $database + * + * @return string + */ + public function getListTableForeignKeysSQL($table, $database = null) + { + // The schema name is passed multiple times as a literal in the WHERE clause instead of using a JOIN condition + // in order to avoid performance issues on MySQL older than 8.0 and the corresponding MariaDB versions + // caused by https://bugs.mysql.com/bug.php?id=81347 + return 'SELECT k.CONSTRAINT_NAME, k.COLUMN_NAME, k.REFERENCED_TABLE_NAME, ' . + 'k.REFERENCED_COLUMN_NAME /*!50116 , c.UPDATE_RULE, c.DELETE_RULE */ ' . + 'FROM INFORMATION_SCHEMA.KEY_COLUMN_USAGE k /*!50116 ' . + 'INNER JOIN INFORMATION_SCHEMA.REFERENTIAL_CONSTRAINTS c ON ' . + 'c.CONSTRAINT_NAME = k.CONSTRAINT_NAME AND ' . + 'c.TABLE_NAME = k.TABLE_NAME */ ' . + 'WHERE k.TABLE_NAME = ' . $this->quoteStringLiteral($table) . ' ' . + 'AND k.TABLE_SCHEMA = ' . $this->getDatabaseNameSQL($database) . ' /*!50116 ' . + 'AND c.CONSTRAINT_SCHEMA = ' . $this->getDatabaseNameSQL($database) . ' */' . + 'ORDER BY k.ORDINAL_POSITION'; + } + + /** + * {@inheritDoc} + */ + protected function getVarcharTypeDeclarationSQLSnippet($length, $fixed/*, $lengthOmitted = false*/) + { + if ($length <= 0 || (func_num_args() > 2 && func_get_arg(2))) { + Deprecation::trigger( + 'doctrine/dbal', + 'https://github.com/doctrine/dbal/issues/3263', + 'Relying on the default string column length on MySQL is deprecated' + . ', specify the length explicitly.', + ); + } + + return $fixed ? ($length > 0 ? 'CHAR(' . $length . ')' : 'CHAR(255)') + : ($length > 0 ? 'VARCHAR(' . $length . ')' : 'VARCHAR(255)'); + } + + /** + * {@inheritDoc} + */ + protected function getBinaryTypeDeclarationSQLSnippet($length, $fixed/*, $lengthOmitted = false*/) + { + if ($length <= 0 || (func_num_args() > 2 && func_get_arg(2))) { + Deprecation::trigger( + 'doctrine/dbal', + 'https://github.com/doctrine/dbal/issues/3263', + 'Relying on the default binary column length on MySQL is deprecated' + . ', specify the length explicitly.', + ); + } + + return $fixed + ? 'BINARY(' . ($length > 0 ? $length : 255) . ')' + : 'VARBINARY(' . ($length > 0 ? $length : 255) . ')'; + } + + /** + * Gets the SQL snippet used to declare a CLOB column type. + * TINYTEXT : 2 ^ 8 - 1 = 255 + * TEXT : 2 ^ 16 - 1 = 65535 + * MEDIUMTEXT : 2 ^ 24 - 1 = 16777215 + * LONGTEXT : 2 ^ 32 - 1 = 4294967295 + * + * {@inheritDoc} + */ + public function getClobTypeDeclarationSQL(array $column) + { + if (! empty($column['length']) && is_numeric($column['length'])) { + $length = $column['length']; + + if ($length <= static::LENGTH_LIMIT_TINYTEXT) { + return 'TINYTEXT'; + } + + if ($length <= static::LENGTH_LIMIT_TEXT) { + return 'TEXT'; + } + + if ($length <= static::LENGTH_LIMIT_MEDIUMTEXT) { + return 'MEDIUMTEXT'; + } + } + + return 'LONGTEXT'; + } + + /** + * {@inheritDoc} + */ + public function getDateTimeTypeDeclarationSQL(array $column) + { + if (isset($column['version']) && $column['version'] === true) { + return 'TIMESTAMP'; + } + + return 'DATETIME'; + } + + /** + * {@inheritDoc} + */ + public function getDateTypeDeclarationSQL(array $column) + { + return 'DATE'; + } + + /** + * {@inheritDoc} + */ + public function getTimeTypeDeclarationSQL(array $column) + { + return 'TIME'; + } + + /** + * {@inheritDoc} + */ + public function getBooleanTypeDeclarationSQL(array $column) + { + return 'TINYINT(1)'; + } + + /** + * {@inheritDoc} + * + * @deprecated + * + * MySQL prefers "autoincrement" identity columns since sequences can only + * be emulated with a table. + */ + public function prefersIdentityColumns() + { + Deprecation::trigger( + 'doctrine/dbal', + 'https://github.com/doctrine/dbal/pull/1519', + 'AbstractMySQLPlatform::prefersIdentityColumns() is deprecated.', + ); + + return true; + } + + /** + * {@inheritDoc} + * + * MySQL supports this through AUTO_INCREMENT columns. + */ + public function supportsIdentityColumns() + { + return true; + } + + /** + * {@inheritDoc} + * + * @internal The method should be only used from within the {@see AbstractPlatform} class hierarchy. + */ + public function supportsInlineColumnComments() + { + return true; + } + + /** + * {@inheritDoc} + * + * @internal The method should be only used from within the {@see AbstractPlatform} class hierarchy. + */ + public function supportsColumnCollation() + { + return true; + } + + /** + * @deprecated The SQL used for schema introspection is an implementation detail and should not be relied upon. + * + * {@inheritDoc} + */ + public function getListTablesSQL() + { + return "SHOW FULL TABLES WHERE Table_type = 'BASE TABLE'"; + } + + /** + * @deprecated The SQL used for schema introspection is an implementation detail and should not be relied upon. + * + * {@inheritDoc} + */ + public function getListTableColumnsSQL($table, $database = null) + { + return 'SELECT COLUMN_NAME AS Field, COLUMN_TYPE AS Type, IS_NULLABLE AS `Null`, ' . + 'COLUMN_KEY AS `Key`, COLUMN_DEFAULT AS `Default`, EXTRA AS Extra, COLUMN_COMMENT AS Comment, ' . + 'CHARACTER_SET_NAME AS CharacterSet, COLLATION_NAME AS Collation ' . + 'FROM information_schema.COLUMNS WHERE TABLE_SCHEMA = ' . $this->getDatabaseNameSQL($database) . + ' AND TABLE_NAME = ' . $this->quoteStringLiteral($table) . + ' ORDER BY ORDINAL_POSITION ASC'; + } + + /** + * @deprecated Use {@see getColumnTypeSQLSnippet()} instead. + * + * The SQL snippets required to elucidate a column type + * + * Returns an array of the form [column type SELECT snippet, additional JOIN statement snippet] + * + * @return array{string, string} + */ + public function getColumnTypeSQLSnippets(string $tableAlias = 'c'): array + { + Deprecation::triggerIfCalledFromOutside( + 'doctrine/dbal', + 'https://github.com/doctrine/dbal/pull/6202', + 'AbstractMySQLPlatform::getColumnTypeSQLSnippets() is deprecated. ' + . 'Use AbstractMySQLPlatform::getColumnTypeSQLSnippet() instead.', + ); + + return [$this->getColumnTypeSQLSnippet(...func_get_args()), '']; + } + + /** + * The SQL snippet required to elucidate a column type + * + * Returns a column type SELECT snippet string + */ + public function getColumnTypeSQLSnippet(string $tableAlias = 'c', ?string $databaseName = null): string + { + return $tableAlias . '.COLUMN_TYPE'; + } + + /** @deprecated The SQL used for schema introspection is an implementation detail and should not be relied upon. */ + public function getListTableMetadataSQL(string $table, ?string $database = null): string + { + return sprintf( + <<<'SQL' +SELECT t.ENGINE, + t.AUTO_INCREMENT, + t.TABLE_COMMENT, + t.CREATE_OPTIONS, + t.TABLE_COLLATION, + ccsa.CHARACTER_SET_NAME +FROM information_schema.TABLES t + INNER JOIN information_schema.`COLLATION_CHARACTER_SET_APPLICABILITY` ccsa + ON ccsa.COLLATION_NAME = t.TABLE_COLLATION +WHERE TABLE_TYPE = 'BASE TABLE' AND TABLE_SCHEMA = %s AND TABLE_NAME = %s +SQL + , + $this->getDatabaseNameSQL($database), + $this->quoteStringLiteral($table), + ); + } + + /** + * {@inheritDoc} + */ + public function getCreateTablesSQL(array $tables): array + { + $sql = []; + + foreach ($tables as $table) { + $sql = array_merge($sql, $this->getCreateTableWithoutForeignKeysSQL($table)); + } + + foreach ($tables as $table) { + if (! $table->hasOption('engine') || $this->engineSupportsForeignKeys($table->getOption('engine'))) { + foreach ($table->getForeignKeys() as $foreignKey) { + $sql[] = $this->getCreateForeignKeySQL( + $foreignKey, + $table->getQuotedName($this), + ); + } + } elseif (count($table->getForeignKeys()) > 0) { + Deprecation::trigger( + 'doctrine/dbal', + 'https://github.com/doctrine/dbal/pull/5414', + 'Relying on the DBAL not generating DDL for foreign keys on MySQL engines' + . ' other than InnoDB is deprecated.' + . ' Define foreign key constraints only if they are necessary.', + ); + } + } + + return $sql; + } + + /** + * {@inheritDoc} + */ + protected function _getCreateTableSQL($name, array $columns, array $options = []) + { + $queryFields = $this->getColumnDeclarationListSQL($columns); + + if (isset($options['uniqueConstraints']) && ! empty($options['uniqueConstraints'])) { + foreach ($options['uniqueConstraints'] as $constraintName => $definition) { + $queryFields .= ', ' . $this->getUniqueConstraintDeclarationSQL($constraintName, $definition); + } + } + + // add all indexes + if (isset($options['indexes']) && ! empty($options['indexes'])) { + foreach ($options['indexes'] as $indexName => $definition) { + $queryFields .= ', ' . $this->getIndexDeclarationSQL($indexName, $definition); + } + } + + // attach all primary keys + if (isset($options['primary']) && ! empty($options['primary'])) { + $keyColumns = array_unique(array_values($options['primary'])); + $queryFields .= ', PRIMARY KEY(' . implode(', ', $keyColumns) . ')'; + } + + $query = 'CREATE '; + + if (! empty($options['temporary'])) { + $query .= 'TEMPORARY '; + } + + $query .= 'TABLE ' . $name . ' (' . $queryFields . ') '; + $query .= $this->buildTableOptions($options); + $query .= $this->buildPartitionOptions($options); + + $sql = [$query]; + + // Propagate foreign key constraints only for InnoDB. + if (isset($options['foreignKeys'])) { + if (! isset($options['engine']) || $this->engineSupportsForeignKeys($options['engine'])) { + foreach ($options['foreignKeys'] as $definition) { + $sql[] = $this->getCreateForeignKeySQL($definition, $name); + } + } elseif (count($options['foreignKeys']) > 0) { + Deprecation::trigger( + 'doctrine/dbal', + 'https://github.com/doctrine/dbal/pull/5414', + 'Relying on the DBAL not generating DDL for foreign keys on MySQL engines' + . ' other than InnoDB is deprecated.' + . ' Define foreign key constraints only if they are necessary.', + ); + } + } + + return $sql; + } + + public function createSelectSQLBuilder(): SelectSQLBuilder + { + return new DefaultSelectSQLBuilder($this, 'FOR UPDATE', null); + } + + /** + * {@inheritDoc} + * + * @internal The method should be only used from within the {@see AbstractPlatform} class hierarchy. + */ + public function getDefaultValueDeclarationSQL($column) + { + // Unset the default value if the given column definition does not allow default values. + if ($column['type'] instanceof TextType || $column['type'] instanceof BlobType) { + $column['default'] = null; + } + + return parent::getDefaultValueDeclarationSQL($column); + } + + /** + * Build SQL for table options + * + * @param mixed[] $options + */ + private function buildTableOptions(array $options): string + { + if (isset($options['table_options'])) { + return $options['table_options']; + } + + $tableOptions = []; + + // Charset + if (! isset($options['charset'])) { + $options['charset'] = 'utf8'; + } + + $tableOptions[] = sprintf('DEFAULT CHARACTER SET %s', $options['charset']); + + if (isset($options['collate'])) { + Deprecation::trigger( + 'doctrine/dbal', + 'https://github.com/doctrine/dbal/issues/5214', + 'The "collate" option is deprecated in favor of "collation" and will be removed in 4.0.', + ); + $options['collation'] = $options['collate']; + } + + // Collation + if (! isset($options['collation'])) { + $options['collation'] = $options['charset'] . '_unicode_ci'; + } + + $tableOptions[] = $this->getColumnCollationDeclarationSQL($options['collation']); + + // Engine + if (! isset($options['engine'])) { + $options['engine'] = 'InnoDB'; + } + + $tableOptions[] = sprintf('ENGINE = %s', $options['engine']); + + // Auto increment + if (isset($options['auto_increment'])) { + $tableOptions[] = sprintf('AUTO_INCREMENT = %s', $options['auto_increment']); + } + + // Comment + if (isset($options['comment'])) { + $tableOptions[] = sprintf('COMMENT = %s ', $this->quoteStringLiteral($options['comment'])); + } + + // Row format + if (isset($options['row_format'])) { + $tableOptions[] = sprintf('ROW_FORMAT = %s', $options['row_format']); + } + + return implode(' ', $tableOptions); + } + + /** + * Build SQL for partition options. + * + * @param mixed[] $options + */ + private function buildPartitionOptions(array $options): string + { + return isset($options['partition_options']) + ? ' ' . $options['partition_options'] + : ''; + } + + private function engineSupportsForeignKeys(string $engine): bool + { + return strcasecmp(trim($engine), 'InnoDB') === 0; + } + + /** + * {@inheritDoc} + */ + public function getAlterTableSQL(TableDiff $diff) + { + $columnSql = []; + $queryParts = []; + $newName = $diff->getNewName(); + + if ($newName !== false) { + Deprecation::trigger( + 'doctrine/dbal', + 'https://github.com/doctrine/dbal/pull/5663', + 'Generation of SQL that renames a table using %s is deprecated. Use getRenameTableSQL() instead.', + __METHOD__, + ); + + $queryParts[] = 'RENAME TO ' . $newName->getQuotedName($this); + } + + foreach ($diff->getAddedColumns() as $column) { + if ($this->onSchemaAlterTableAddColumn($column, $diff, $columnSql)) { + continue; + } + + $columnProperties = array_merge($column->toArray(), [ + 'comment' => $this->getColumnComment($column), + ]); + + $queryParts[] = 'ADD ' . $this->getColumnDeclarationSQL( + $column->getQuotedName($this), + $columnProperties, + ); + } + + foreach ($diff->getDroppedColumns() as $column) { + if ($this->onSchemaAlterTableRemoveColumn($column, $diff, $columnSql)) { + continue; + } + + $queryParts[] = 'DROP ' . $column->getQuotedName($this); + } + + foreach ($diff->getModifiedColumns() as $columnDiff) { + if ($this->onSchemaAlterTableChangeColumn($columnDiff, $diff, $columnSql)) { + continue; + } + + $newColumn = $columnDiff->getNewColumn(); + + $newColumnProperties = array_merge($newColumn->toArray(), [ + 'comment' => $this->getColumnComment($newColumn), + ]); + + $oldColumn = $columnDiff->getOldColumn() ?? $columnDiff->getOldColumnName(); + + $queryParts[] = 'CHANGE ' . $oldColumn->getQuotedName($this) . ' ' + . $this->getColumnDeclarationSQL($newColumn->getQuotedName($this), $newColumnProperties); + } + + foreach ($diff->getRenamedColumns() as $oldColumnName => $column) { + if ($this->onSchemaAlterTableRenameColumn($oldColumnName, $column, $diff, $columnSql)) { + continue; + } + + $oldColumnName = new Identifier($oldColumnName); + + $columnProperties = array_merge($column->toArray(), [ + 'comment' => $this->getColumnComment($column), + ]); + + $queryParts[] = 'CHANGE ' . $oldColumnName->getQuotedName($this) . ' ' + . $this->getColumnDeclarationSQL($column->getQuotedName($this), $columnProperties); + } + + $addedIndexes = $this->indexAssetsByLowerCaseName($diff->getAddedIndexes()); + $modifiedIndexes = $this->indexAssetsByLowerCaseName($diff->getModifiedIndexes()); + $diffModified = false; + + if (isset($addedIndexes['primary'])) { + $keyColumns = array_unique(array_values($addedIndexes['primary']->getColumns())); + $queryParts[] = 'ADD PRIMARY KEY (' . implode(', ', $keyColumns) . ')'; + unset($addedIndexes['primary']); + $diffModified = true; + } elseif (isset($modifiedIndexes['primary'])) { + $addedColumns = $this->indexAssetsByLowerCaseName($diff->getAddedColumns()); + + // Necessary in case the new primary key includes a new auto_increment column + foreach ($modifiedIndexes['primary']->getColumns() as $columnName) { + if (isset($addedColumns[$columnName]) && $addedColumns[$columnName]->getAutoincrement()) { + $keyColumns = array_unique(array_values($modifiedIndexes['primary']->getColumns())); + $queryParts[] = 'DROP PRIMARY KEY'; + $queryParts[] = 'ADD PRIMARY KEY (' . implode(', ', $keyColumns) . ')'; + unset($modifiedIndexes['primary']); + $diffModified = true; + break; + } + } + } + + if ($diffModified) { + $diff = new TableDiff( + $diff->name, + $diff->getAddedColumns(), + $diff->getModifiedColumns(), + $diff->getDroppedColumns(), + array_values($addedIndexes), + array_values($modifiedIndexes), + $diff->getDroppedIndexes(), + $diff->getOldTable(), + $diff->getAddedForeignKeys(), + $diff->getModifiedForeignKeys(), + $diff->getDroppedForeignKeys(), + $diff->getRenamedColumns(), + $diff->getRenamedIndexes(), + ); + } + + $sql = []; + $tableSql = []; + + if (! $this->onSchemaAlterTable($diff, $tableSql)) { + if (count($queryParts) > 0) { + $sql[] = 'ALTER TABLE ' . ($diff->getOldTable() ?? $diff->getName($this))->getQuotedName($this) . ' ' + . implode(', ', $queryParts); + } + + $sql = array_merge( + $this->getPreAlterTableIndexForeignKeySQL($diff), + $sql, + $this->getPostAlterTableIndexForeignKeySQL($diff), + ); + } + + return array_merge($sql, $tableSql, $columnSql); + } + + /** + * {@inheritDoc} + */ + protected function getPreAlterTableIndexForeignKeySQL(TableDiff $diff) + { + $sql = []; + + $tableNameSQL = ($diff->getOldTable() ?? $diff->getName($this))->getQuotedName($this); + + foreach ($diff->getModifiedIndexes() as $changedIndex) { + $sql = array_merge($sql, $this->getPreAlterTableAlterPrimaryKeySQL($diff, $changedIndex)); + } + + foreach ($diff->getDroppedIndexes() as $droppedIndex) { + $sql = array_merge($sql, $this->getPreAlterTableAlterPrimaryKeySQL($diff, $droppedIndex)); + + foreach ($diff->getAddedIndexes() as $addedIndex) { + if ($droppedIndex->getColumns() !== $addedIndex->getColumns()) { + continue; + } + + $indexClause = 'INDEX ' . $addedIndex->getName(); + + if ($addedIndex->isPrimary()) { + $indexClause = 'PRIMARY KEY'; + } elseif ($addedIndex->isUnique()) { + $indexClause = 'UNIQUE INDEX ' . $addedIndex->getName(); + } + + $query = 'ALTER TABLE ' . $tableNameSQL . ' DROP INDEX ' . $droppedIndex->getName() . ', '; + $query .= 'ADD ' . $indexClause; + $query .= ' (' . $this->getIndexFieldDeclarationListSQL($addedIndex) . ')'; + + $sql[] = $query; + + $diff->unsetAddedIndex($addedIndex); + $diff->unsetDroppedIndex($droppedIndex); + + break; + } + } + + $engine = 'INNODB'; + + $table = $diff->getOldTable(); + + if ($table !== null && $table->hasOption('engine')) { + $engine = strtoupper(trim($table->getOption('engine'))); + } + + // Suppress foreign key constraint propagation on non-supporting engines. + if ($engine !== 'INNODB') { + $diff->addedForeignKeys = []; + $diff->changedForeignKeys = []; + $diff->removedForeignKeys = []; + } + + $sql = array_merge( + $sql, + $this->getPreAlterTableAlterIndexForeignKeySQL($diff), + parent::getPreAlterTableIndexForeignKeySQL($diff), + $this->getPreAlterTableRenameIndexForeignKeySQL($diff), + ); + + return $sql; + } + + /** + * @return string[] + * + * @throws Exception + */ + private function getPreAlterTableAlterPrimaryKeySQL(TableDiff $diff, Index $index): array + { + if (! $index->isPrimary()) { + return []; + } + + $table = $diff->getOldTable(); + + if ($table === null) { + return []; + } + + $sql = []; + + $tableNameSQL = ($diff->getOldTable() ?? $diff->getName($this))->getQuotedName($this); + + // Dropping primary keys requires to unset autoincrement attribute on the particular column first. + foreach ($index->getColumns() as $columnName) { + if (! $table->hasColumn($columnName)) { + continue; + } + + $column = $table->getColumn($columnName); + + if ($column->getAutoincrement() !== true) { + continue; + } + + $column->setAutoincrement(false); + + $sql[] = 'ALTER TABLE ' . $tableNameSQL . ' MODIFY ' . + $this->getColumnDeclarationSQL($column->getQuotedName($this), $column->toArray()); + + // original autoincrement information might be needed later on by other parts of the table alteration + $column->setAutoincrement(true); + } + + return $sql; + } + + /** + * @param TableDiff $diff The table diff to gather the SQL for. + * + * @return string[] + * + * @throws Exception + */ + private function getPreAlterTableAlterIndexForeignKeySQL(TableDiff $diff): array + { + $table = $diff->getOldTable(); + + if ($table === null) { + return []; + } + + $primaryKey = $table->getPrimaryKey(); + + if ($primaryKey === null) { + return []; + } + + $primaryKeyColumns = []; + + foreach ($primaryKey->getColumns() as $columnName) { + if (! $table->hasColumn($columnName)) { + continue; + } + + $primaryKeyColumns[] = $table->getColumn($columnName); + } + + if (count($primaryKeyColumns) === 0) { + return []; + } + + $sql = []; + + $tableNameSQL = $table->getQuotedName($this); + + foreach ($diff->getModifiedIndexes() as $changedIndex) { + // Changed primary key + if (! $changedIndex->isPrimary()) { + continue; + } + + foreach ($primaryKeyColumns as $column) { + // Check if an autoincrement column was dropped from the primary key. + if (! $column->getAutoincrement() || in_array($column->getName(), $changedIndex->getColumns(), true)) { + continue; + } + + // The autoincrement attribute needs to be removed from the dropped column + // before we can drop and recreate the primary key. + $column->setAutoincrement(false); + + $sql[] = 'ALTER TABLE ' . $tableNameSQL . ' MODIFY ' . + $this->getColumnDeclarationSQL($column->getQuotedName($this), $column->toArray()); + + // Restore the autoincrement attribute as it might be needed later on + // by other parts of the table alteration. + $column->setAutoincrement(true); + } + } + + return $sql; + } + + /** + * @param TableDiff $diff The table diff to gather the SQL for. + * + * @return string[] + */ + protected function getPreAlterTableRenameIndexForeignKeySQL(TableDiff $diff) + { + $sql = []; + + $tableNameSQL = ($diff->getOldTable() ?? $diff->getName($this))->getQuotedName($this); + + foreach ($this->getRemainingForeignKeyConstraintsRequiringRenamedIndexes($diff) as $foreignKey) { + if (in_array($foreignKey, $diff->getModifiedForeignKeys(), true)) { + continue; + } + + $sql[] = $this->getDropForeignKeySQL($foreignKey->getQuotedName($this), $tableNameSQL); + } + + return $sql; + } + + /** + * Returns the remaining foreign key constraints that require one of the renamed indexes. + * + * "Remaining" here refers to the diff between the foreign keys currently defined in the associated + * table and the foreign keys to be removed. + * + * @param TableDiff $diff The table diff to evaluate. + * + * @return ForeignKeyConstraint[] + */ + private function getRemainingForeignKeyConstraintsRequiringRenamedIndexes(TableDiff $diff): array + { + if (count($diff->getRenamedIndexes()) === 0) { + return []; + } + + $table = $diff->getOldTable(); + + if ($table === null) { + return []; + } + + $foreignKeys = []; + /** @var ForeignKeyConstraint[] $remainingForeignKeys */ + $remainingForeignKeys = array_diff_key( + $table->getForeignKeys(), + $diff->getDroppedForeignKeys(), + ); + + foreach ($remainingForeignKeys as $foreignKey) { + foreach ($diff->getRenamedIndexes() as $index) { + if ($foreignKey->intersectsIndexColumns($index)) { + $foreignKeys[] = $foreignKey; + + break; + } + } + } + + return $foreignKeys; + } + + /** + * {@inheritDoc} + */ + protected function getPostAlterTableIndexForeignKeySQL(TableDiff $diff) + { + return array_merge( + parent::getPostAlterTableIndexForeignKeySQL($diff), + $this->getPostAlterTableRenameIndexForeignKeySQL($diff), + ); + } + + /** + * @param TableDiff $diff The table diff to gather the SQL for. + * + * @return string[] + */ + protected function getPostAlterTableRenameIndexForeignKeySQL(TableDiff $diff) + { + $sql = []; + $newName = $diff->getNewName(); + + if ($newName !== false) { + $tableNameSQL = $newName->getQuotedName($this); + } else { + $tableNameSQL = ($diff->getOldTable() ?? $diff->getName($this))->getQuotedName($this); + } + + foreach ($this->getRemainingForeignKeyConstraintsRequiringRenamedIndexes($diff) as $foreignKey) { + if (in_array($foreignKey, $diff->getModifiedForeignKeys(), true)) { + continue; + } + + $sql[] = $this->getCreateForeignKeySQL($foreignKey, $tableNameSQL); + } + + return $sql; + } + + /** + * {@inheritDoc} + */ + protected function getCreateIndexSQLFlags(Index $index) + { + $type = ''; + if ($index->isUnique()) { + $type .= 'UNIQUE '; + } elseif ($index->hasFlag('fulltext')) { + $type .= 'FULLTEXT '; + } elseif ($index->hasFlag('spatial')) { + $type .= 'SPATIAL '; + } + + return $type; + } + + /** + * {@inheritDoc} + */ + public function getIntegerTypeDeclarationSQL(array $column) + { + return 'INT' . $this->_getCommonIntegerTypeDeclarationSQL($column); + } + + /** + * {@inheritDoc} + */ + public function getBigIntTypeDeclarationSQL(array $column) + { + return 'BIGINT' . $this->_getCommonIntegerTypeDeclarationSQL($column); + } + + /** + * {@inheritDoc} + */ + public function getSmallIntTypeDeclarationSQL(array $column) + { + return 'SMALLINT' . $this->_getCommonIntegerTypeDeclarationSQL($column); + } + + /** + * {@inheritDoc} + */ + public function getFloatDeclarationSQL(array $column) + { + return 'DOUBLE PRECISION' . $this->getUnsignedDeclaration($column); + } + + /** + * {@inheritDoc} + */ + public function getDecimalTypeDeclarationSQL(array $column) + { + return parent::getDecimalTypeDeclarationSQL($column) . $this->getUnsignedDeclaration($column); + } + + /** + * Get unsigned declaration for a column. + * + * @param mixed[] $columnDef + */ + private function getUnsignedDeclaration(array $columnDef): string + { + return ! empty($columnDef['unsigned']) ? ' UNSIGNED' : ''; + } + + /** + * {@inheritDoc} + */ + protected function _getCommonIntegerTypeDeclarationSQL(array $column) + { + $autoinc = ''; + if (! empty($column['autoincrement'])) { + $autoinc = ' AUTO_INCREMENT'; + } + + return $this->getUnsignedDeclaration($column) . $autoinc; + } + + /** + * {@inheritDoc} + * + * @internal The method should be only used from within the {@see AbstractPlatform} class hierarchy. + */ + public function getColumnCharsetDeclarationSQL($charset) + { + return 'CHARACTER SET ' . $charset; + } + + /** + * {@inheritDoc} + * + * @internal The method should be only used from within the {@see AbstractPlatform} class hierarchy. + */ + public function getAdvancedForeignKeyOptionsSQL(ForeignKeyConstraint $foreignKey) + { + $query = ''; + if ($foreignKey->hasOption('match')) { + $query .= ' MATCH ' . $foreignKey->getOption('match'); + } + + $query .= parent::getAdvancedForeignKeyOptionsSQL($foreignKey); + + return $query; + } + + /** + * {@inheritDoc} + */ + public function getDropIndexSQL($index, $table = null) + { + if ($index instanceof Index) { + Deprecation::trigger( + 'doctrine/dbal', + 'https://github.com/doctrine/dbal/issues/4798', + 'Passing $index as an Index object to %s is deprecated. Pass it as a quoted name instead.', + __METHOD__, + ); + + $indexName = $index->getQuotedName($this); + } elseif (is_string($index)) { + $indexName = $index; + } else { + throw new InvalidArgumentException( + __METHOD__ . '() expects $index parameter to be string or ' . Index::class . '.', + ); + } + + if ($table instanceof Table) { + Deprecation::trigger( + 'doctrine/dbal', + 'https://github.com/doctrine/dbal/issues/4798', + 'Passing $table as a Table object to %s is deprecated. Pass it as a quoted name instead.', + __METHOD__, + ); + + $table = $table->getQuotedName($this); + } elseif (! is_string($table)) { + throw new InvalidArgumentException( + __METHOD__ . '() expects $table parameter to be string or ' . Table::class . '.', + ); + } + + if ($index instanceof Index && $index->isPrimary()) { + // MySQL primary keys are always named "PRIMARY", + // so we cannot use them in statements because of them being keyword. + return $this->getDropPrimaryKeySQL($table); + } + + return 'DROP INDEX ' . $indexName . ' ON ' . $table; + } + + /** + * @param string $table + * + * @return string + */ + protected function getDropPrimaryKeySQL($table) + { + return 'ALTER TABLE ' . $table . ' DROP PRIMARY KEY'; + } + + /** + * The `ALTER TABLE ... DROP CONSTRAINT` syntax is only available as of MySQL 8.0.19. + * + * @link https://dev.mysql.com/doc/refman/8.0/en/alter-table.html + */ + public function getDropUniqueConstraintSQL(string $name, string $tableName): string + { + return $this->getDropIndexSQL($name, $tableName); + } + + /** + * {@inheritDoc} + */ + public function getSetTransactionIsolationSQL($level) + { + return 'SET SESSION TRANSACTION ISOLATION LEVEL ' . $this->_getTransactionIsolationLevelSQL($level); + } + + /** + * {@inheritDoc} + */ + public function getName() + { + Deprecation::triggerIfCalledFromOutside( + 'doctrine/dbal', + 'https://github.com/doctrine/dbal/issues/4749', + 'AbstractMySQLPlatform::getName() is deprecated. Identify platforms by their class.', + ); + + return 'mysql'; + } + + /** + * {@inheritDoc} + */ + public function getReadLockSQL() + { + return 'LOCK IN SHARE MODE'; + } + + /** + * {@inheritDoc} + */ + protected function initializeDoctrineTypeMappings() + { + $this->doctrineTypeMapping = [ + 'bigint' => Types::BIGINT, + 'binary' => Types::BINARY, + 'blob' => Types::BLOB, + 'char' => Types::STRING, + 'date' => Types::DATE_MUTABLE, + 'datetime' => Types::DATETIME_MUTABLE, + 'decimal' => Types::DECIMAL, + 'double' => Types::FLOAT, + 'float' => Types::FLOAT, + 'int' => Types::INTEGER, + 'integer' => Types::INTEGER, + 'longblob' => Types::BLOB, + 'longtext' => Types::TEXT, + 'mediumblob' => Types::BLOB, + 'mediumint' => Types::INTEGER, + 'mediumtext' => Types::TEXT, + 'numeric' => Types::DECIMAL, + 'real' => Types::FLOAT, + 'set' => Types::SIMPLE_ARRAY, + 'smallint' => Types::SMALLINT, + 'string' => Types::STRING, + 'text' => Types::TEXT, + 'time' => Types::TIME_MUTABLE, + 'timestamp' => Types::DATETIME_MUTABLE, + 'tinyblob' => Types::BLOB, + 'tinyint' => Types::BOOLEAN, + 'tinytext' => Types::TEXT, + 'varbinary' => Types::BINARY, + 'varchar' => Types::STRING, + 'year' => Types::DATE_MUTABLE, + ]; + } + + /** + * {@inheritDoc} + * + * @deprecated + */ + public function getVarcharMaxLength() + { + Deprecation::triggerIfCalledFromOutside( + 'doctrine/dbal', + 'https://github.com/doctrine/dbal/issues/3263', + 'AbstractMySQLPlatform::getVarcharMaxLength() is deprecated.', + ); + + return 65535; + } + + /** + * {@inheritDoc} + * + * @deprecated + */ + public function getBinaryMaxLength() + { + Deprecation::triggerIfCalledFromOutside( + 'doctrine/dbal', + 'https://github.com/doctrine/dbal/issues/3263', + 'AbstractMySQLPlatform::getBinaryMaxLength() is deprecated.', + ); + + return 65535; + } + + /** + * {@inheritDoc} + * + * @deprecated Implement {@see createReservedKeywordsList()} instead. + */ + protected function getReservedKeywordsClass() + { + Deprecation::triggerIfCalledFromOutside( + 'doctrine/dbal', + 'https://github.com/doctrine/dbal/issues/4510', + 'AbstractMySQLPlatform::getReservedKeywordsClass() is deprecated,' + . ' use AbstractMySQLPlatform::createReservedKeywordsList() instead.', + ); + + return Keywords\MySQLKeywords::class; + } + + /** + * {@inheritDoc} + * + * MySQL commits a transaction implicitly when DROP TABLE is executed, however not + * if DROP TEMPORARY TABLE is executed. + */ + public function getDropTemporaryTableSQL($table) + { + if ($table instanceof Table) { + Deprecation::trigger( + 'doctrine/dbal', + 'https://github.com/doctrine/dbal/issues/4798', + 'Passing $table as a Table object to %s is deprecated. Pass it as a quoted name instead.', + __METHOD__, + ); + + $table = $table->getQuotedName($this); + } elseif (! is_string($table)) { + throw new InvalidArgumentException( + __METHOD__ . '() expects $table parameter to be string or ' . Table::class . '.', + ); + } + + return 'DROP TEMPORARY TABLE ' . $table; + } + + /** + * Gets the SQL Snippet used to declare a BLOB column type. + * TINYBLOB : 2 ^ 8 - 1 = 255 + * BLOB : 2 ^ 16 - 1 = 65535 + * MEDIUMBLOB : 2 ^ 24 - 1 = 16777215 + * LONGBLOB : 2 ^ 32 - 1 = 4294967295 + * + * {@inheritDoc} + */ + public function getBlobTypeDeclarationSQL(array $column) + { + if (! empty($column['length']) && is_numeric($column['length'])) { + $length = $column['length']; + + if ($length <= static::LENGTH_LIMIT_TINYBLOB) { + return 'TINYBLOB'; + } + + if ($length <= static::LENGTH_LIMIT_BLOB) { + return 'BLOB'; + } + + if ($length <= static::LENGTH_LIMIT_MEDIUMBLOB) { + return 'MEDIUMBLOB'; + } + } + + return 'LONGBLOB'; + } + + /** + * {@inheritDoc} + */ + public function quoteStringLiteral($str) + { + $str = str_replace('\\', '\\\\', $str); // MySQL requires backslashes to be escaped + + return parent::quoteStringLiteral($str); + } + + /** + * {@inheritDoc} + */ + public function getDefaultTransactionIsolationLevel() + { + return TransactionIsolationLevel::REPEATABLE_READ; + } + + public function supportsColumnLengthIndexes(): bool + { + return true; + } + + /** @deprecated Will be removed without replacement. */ + protected function getDatabaseNameSQL(?string $databaseName): string + { + Deprecation::triggerIfCalledFromOutside( + 'doctrine/dbal', + 'https://github.com/doctrine/dbal/pull/6215', + '%s is deprecated without replacement.', + __METHOD__, + ); + + if ($databaseName !== null) { + return $this->quoteStringLiteral($databaseName); + } + + return $this->getCurrentDatabaseExpression(); + } + + public function createSchemaManager(Connection $connection): MySQLSchemaManager + { + return new MySQLSchemaManager($connection, $this); + } + + /** + * @param list $assets + * + * @return array + * + * @template T of AbstractAsset + */ + private function indexAssetsByLowerCaseName(array $assets): array + { + $result = []; + + foreach ($assets as $asset) { + $result[strtolower($asset->getName())] = $asset; + } + + return $result; + } + + public function fetchTableOptionsByTable(bool $includeTableName): string + { + $sql = <<<'SQL' + SELECT t.TABLE_NAME, + t.ENGINE, + t.AUTO_INCREMENT, + t.TABLE_COMMENT, + t.CREATE_OPTIONS, + t.TABLE_COLLATION, + ccsa.CHARACTER_SET_NAME + FROM information_schema.TABLES t + INNER JOIN information_schema.COLLATION_CHARACTER_SET_APPLICABILITY ccsa + ON ccsa.COLLATION_NAME = t.TABLE_COLLATION +SQL; + + $conditions = ['t.TABLE_SCHEMA = ?']; + + if ($includeTableName) { + $conditions[] = 't.TABLE_NAME = ?'; + } + + $conditions[] = "t.TABLE_TYPE = 'BASE TABLE'"; + + return $sql . ' WHERE ' . implode(' AND ', $conditions); + } +} diff --git a/3rdparty/doctrine/dbal/src/Platforms/AbstractPlatform.php b/3rdparty/doctrine/dbal/src/Platforms/AbstractPlatform.php new file mode 100644 index 00000000..ea84d21f --- /dev/null +++ b/3rdparty/doctrine/dbal/src/Platforms/AbstractPlatform.php @@ -0,0 +1,4727 @@ +disableTypeComments = $value; + } + + /** + * Sets the EventManager used by the Platform. + * + * @deprecated + * + * @return void + */ + public function setEventManager(EventManager $eventManager) + { + Deprecation::triggerIfCalledFromOutside( + 'doctrine/dbal', + 'https://github.com/doctrine/dbal/issues/5784', + '%s is deprecated.', + __METHOD__, + ); + + $this->_eventManager = $eventManager; + } + + /** + * Gets the EventManager used by the Platform. + * + * @deprecated + * + * @return EventManager|null + */ + public function getEventManager() + { + Deprecation::triggerIfCalledFromOutside( + 'doctrine/dbal', + 'https://github.com/doctrine/dbal/issues/5784', + '%s is deprecated.', + __METHOD__, + ); + + return $this->_eventManager; + } + + /** + * Returns the SQL snippet that declares a boolean column. + * + * @param mixed[] $column + * + * @return string + */ + abstract public function getBooleanTypeDeclarationSQL(array $column); + + /** + * Returns the SQL snippet that declares a 4 byte integer column. + * + * @param mixed[] $column + * + * @return string + */ + abstract public function getIntegerTypeDeclarationSQL(array $column); + + /** + * Returns the SQL snippet that declares an 8 byte integer column. + * + * @param mixed[] $column + * + * @return string + */ + abstract public function getBigIntTypeDeclarationSQL(array $column); + + /** + * Returns the SQL snippet that declares a 2 byte integer column. + * + * @param mixed[] $column + * + * @return string + */ + abstract public function getSmallIntTypeDeclarationSQL(array $column); + + /** + * Returns the SQL snippet that declares common properties of an integer column. + * + * @param mixed[] $column + * + * @return string + */ + abstract protected function _getCommonIntegerTypeDeclarationSQL(array $column); + + /** + * Lazy load Doctrine Type Mappings. + * + * @return void + */ + abstract protected function initializeDoctrineTypeMappings(); + + /** + * Initializes Doctrine Type Mappings with the platform defaults + * and with all additional type mappings. + */ + private function initializeAllDoctrineTypeMappings(): void + { + $this->initializeDoctrineTypeMappings(); + + foreach (Type::getTypesMap() as $typeName => $className) { + foreach (Type::getType($typeName)->getMappedDatabaseTypes($this) as $dbType) { + $dbType = strtolower($dbType); + $this->doctrineTypeMapping[$dbType] = $typeName; + } + } + } + + /** + * Returns the SQL snippet used to declare a column that can + * store characters in the ASCII character set + * + * @param mixed[] $column + */ + public function getAsciiStringTypeDeclarationSQL(array $column): string + { + return $this->getStringTypeDeclarationSQL($column); + } + + /** + * Returns the SQL snippet used to declare a VARCHAR column type. + * + * @deprecated Use {@link getStringTypeDeclarationSQL()} instead. + * + * @param mixed[] $column + * + * @return string + */ + public function getVarcharTypeDeclarationSQL(array $column) + { + if (isset($column['length'])) { + $lengthOmitted = false; + } else { + $column['length'] = $this->getVarcharDefaultLength(); + $lengthOmitted = true; + } + + $fixed = $column['fixed'] ?? false; + + $maxLength = $fixed + ? $this->getCharMaxLength() + : $this->getVarcharMaxLength(); + + if ($column['length'] > $maxLength) { + return $this->getClobTypeDeclarationSQL($column); + } + + return $this->getVarcharTypeDeclarationSQLSnippet($column['length'], $fixed, $lengthOmitted); + } + + /** + * Returns the SQL snippet used to declare a string column type. + * + * @param mixed[] $column + * + * @return string + */ + public function getStringTypeDeclarationSQL(array $column) + { + return $this->getVarcharTypeDeclarationSQL($column); + } + + /** + * Returns the SQL snippet used to declare a BINARY/VARBINARY column type. + * + * @param mixed[] $column The column definition. + * + * @return string + */ + public function getBinaryTypeDeclarationSQL(array $column) + { + if (isset($column['length'])) { + $lengthOmitted = false; + } else { + $column['length'] = $this->getBinaryDefaultLength(); + $lengthOmitted = true; + } + + $fixed = $column['fixed'] ?? false; + + $maxLength = $this->getBinaryMaxLength(); + + if ($column['length'] > $maxLength) { + if ($maxLength > 0) { + Deprecation::trigger( + 'doctrine/dbal', + 'https://github.com/doctrine/dbal/issues/3187', + 'Binary column length %d is greater than supported by the platform (%d).' + . ' Reduce the column length or use a BLOB column instead.', + $column['length'], + $maxLength, + ); + } + + return $this->getBlobTypeDeclarationSQL($column); + } + + return $this->getBinaryTypeDeclarationSQLSnippet($column['length'], $fixed, $lengthOmitted); + } + + /** + * Returns the SQL snippet to declare a GUID/UUID column. + * + * By default this maps directly to a CHAR(36) and only maps to more + * special datatypes when the underlying databases support this datatype. + * + * @param mixed[] $column + * + * @return string + */ + public function getGuidTypeDeclarationSQL(array $column) + { + $column['length'] = 36; + $column['fixed'] = true; + + return $this->getStringTypeDeclarationSQL($column); + } + + /** + * Returns the SQL snippet to declare a JSON column. + * + * By default this maps directly to a CLOB and only maps to more + * special datatypes when the underlying databases support this datatype. + * + * @param mixed[] $column + * + * @return string + */ + public function getJsonTypeDeclarationSQL(array $column) + { + return $this->getClobTypeDeclarationSQL($column); + } + + /** + * @param int|false $length + * @param bool $fixed + * + * @return string + * + * @throws Exception If not supported on this platform. + */ + protected function getVarcharTypeDeclarationSQLSnippet($length, $fixed/*, $lengthOmitted = false*/) + { + throw Exception::notSupported('VARCHARs not supported by Platform.'); + } + + /** + * Returns the SQL snippet used to declare a BINARY/VARBINARY column type. + * + * @param int|false $length The length of the column. + * @param bool $fixed Whether the column length is fixed. + * + * @return string + * + * @throws Exception If not supported on this platform. + */ + protected function getBinaryTypeDeclarationSQLSnippet($length, $fixed/*, $lengthOmitted = false*/) + { + throw Exception::notSupported('BINARY/VARBINARY column types are not supported by this platform.'); + } + + /** + * Returns the SQL snippet used to declare a CLOB column type. + * + * @param mixed[] $column + * + * @return string + */ + abstract public function getClobTypeDeclarationSQL(array $column); + + /** + * Returns the SQL Snippet used to declare a BLOB column type. + * + * @param mixed[] $column + * + * @return string + */ + abstract public function getBlobTypeDeclarationSQL(array $column); + + /** + * Gets the name of the platform. + * + * @deprecated Identify platforms by their class. + * + * @return string + */ + abstract public function getName(); + + /** + * Registers a doctrine type to be used in conjunction with a column type of this platform. + * + * @param string $dbType + * @param string $doctrineType + * + * @return void + * + * @throws Exception If the type is not found. + */ + public function registerDoctrineTypeMapping($dbType, $doctrineType) + { + if ($this->doctrineTypeMapping === null) { + $this->initializeAllDoctrineTypeMappings(); + } + + if (! Types\Type::hasType($doctrineType)) { + throw Exception::typeNotFound($doctrineType); + } + + $dbType = strtolower($dbType); + $this->doctrineTypeMapping[$dbType] = $doctrineType; + + $doctrineType = Type::getType($doctrineType); + + if (! $doctrineType->requiresSQLCommentHint($this)) { + return; + } + + $this->markDoctrineTypeCommented($doctrineType); + } + + /** + * Gets the Doctrine type that is mapped for the given database column type. + * + * @param string $dbType + * + * @return string + * + * @throws Exception + */ + public function getDoctrineTypeMapping($dbType) + { + if ($this->doctrineTypeMapping === null) { + $this->initializeAllDoctrineTypeMappings(); + } + + $dbType = strtolower($dbType); + + if (! isset($this->doctrineTypeMapping[$dbType])) { + throw new Exception( + 'Unknown database type ' . $dbType . ' requested, ' . static::class . ' may not support it.', + ); + } + + return $this->doctrineTypeMapping[$dbType]; + } + + /** + * Checks if a database type is currently supported by this platform. + * + * @param string $dbType + * + * @return bool + */ + public function hasDoctrineTypeMappingFor($dbType) + { + if ($this->doctrineTypeMapping === null) { + $this->initializeAllDoctrineTypeMappings(); + } + + $dbType = strtolower($dbType); + + return isset($this->doctrineTypeMapping[$dbType]); + } + + /** + * Initializes the Doctrine Type comments instance variable for in_array() checks. + * + * @deprecated This API will be removed in Doctrine DBAL 4.0. + * + * @return void + */ + protected function initializeCommentedDoctrineTypes() + { + Deprecation::triggerIfCalledFromOutside( + 'doctrine/dbal', + 'https://github.com/doctrine/dbal/pull/5058', + '%s is deprecated and will be removed in Doctrine DBAL 4.0.', + __METHOD__, + ); + + $this->doctrineTypeComments = []; + + foreach (Type::getTypesMap() as $typeName => $className) { + $type = Type::getType($typeName); + + if (! $type->requiresSQLCommentHint($this)) { + continue; + } + + $this->doctrineTypeComments[] = $typeName; + } + } + + /** + * Is it necessary for the platform to add a parsable type comment to allow reverse engineering the given type? + * + * @deprecated Use {@link Type::requiresSQLCommentHint()} instead. + * + * @return bool + */ + public function isCommentedDoctrineType(Type $doctrineType) + { + Deprecation::triggerIfCalledFromOutside( + 'doctrine/dbal', + 'https://github.com/doctrine/dbal/pull/5058', + '%s is deprecated and will be removed in Doctrine DBAL 4.0. Use Type::requiresSQLCommentHint() instead.', + __METHOD__, + ); + + if ($this->doctrineTypeComments === null) { + $this->initializeCommentedDoctrineTypes(); + } + + return $doctrineType->requiresSQLCommentHint($this); + } + + /** + * Marks this type as to be commented in ALTER TABLE and CREATE TABLE statements. + * + * @param string|Type $doctrineType + * + * @return void + */ + public function markDoctrineTypeCommented($doctrineType) + { + Deprecation::triggerIfCalledFromOutside( + 'doctrine/dbal', + 'https://github.com/doctrine/dbal/pull/5058', + '%s is deprecated and will be removed in Doctrine DBAL 4.0. Use Type::requiresSQLCommentHint() instead.', + __METHOD__, + ); + + if ($this->doctrineTypeComments === null) { + $this->initializeCommentedDoctrineTypes(); + } + + assert(is_array($this->doctrineTypeComments)); + + $this->doctrineTypeComments[] = $doctrineType instanceof Type ? $doctrineType->getName() : $doctrineType; + } + + /** + * Gets the comment to append to a column comment that helps parsing this type in reverse engineering. + * + * @deprecated This method will be removed without replacement. + * + * @return string + */ + public function getDoctrineTypeComment(Type $doctrineType) + { + Deprecation::triggerIfCalledFromOutside( + 'doctrine/dbal', + 'https://github.com/doctrine/dbal/pull/5107', + '%s is deprecated and will be removed in Doctrine DBAL 4.0.', + __METHOD__, + ); + + return '(DC2Type:' . $doctrineType->getName() . ')'; + } + + /** + * Gets the comment of a passed column modified by potential doctrine type comment hints. + * + * @deprecated This method will be removed without replacement. + * + * @return string|null + */ + protected function getColumnComment(Column $column) + { + Deprecation::triggerIfCalledFromOutside( + 'doctrine/dbal', + 'https://github.com/doctrine/dbal/pull/5107', + '%s is deprecated and will be removed in Doctrine DBAL 4.0.', + __METHOD__, + ); + + $comment = $column->getComment(); + + if (! $this->disableTypeComments && $column->getType()->requiresSQLCommentHint($this)) { + $comment .= $this->getDoctrineTypeComment($column->getType()); + } + + return $comment; + } + + /** + * Gets the character used for identifier quoting. + * + * @deprecated Use {@see quoteIdentifier()} to quote identifiers instead. + * + * @return string + */ + public function getIdentifierQuoteCharacter() + { + Deprecation::triggerIfCalledFromOutside( + 'doctrine/dbal', + 'https://github.com/doctrine/dbal/pull/5388', + 'AbstractPlatform::getIdentifierQuoteCharacter() is deprecated. Use quoteIdentifier() instead.', + ); + + return '"'; + } + + /** + * Gets the string portion that starts an SQL comment. + * + * @deprecated + * + * @return string + */ + public function getSqlCommentStartString() + { + Deprecation::trigger( + 'doctrine/dbal', + 'https://github.com/doctrine/dbal/pull/4724', + 'AbstractPlatform::getSqlCommentStartString() is deprecated.', + ); + + return '--'; + } + + /** + * Gets the string portion that ends an SQL comment. + * + * @deprecated + * + * @return string + */ + public function getSqlCommentEndString() + { + Deprecation::trigger( + 'doctrine/dbal', + 'https://github.com/doctrine/dbal/pull/4724', + 'AbstractPlatform::getSqlCommentEndString() is deprecated.', + ); + + return "\n"; + } + + /** + * Gets the maximum length of a char column. + * + * @deprecated + */ + public function getCharMaxLength(): int + { + Deprecation::triggerIfCalledFromOutside( + 'doctrine/dbal', + 'https://github.com/doctrine/dbal/issues/3263', + 'AbstractPlatform::getCharMaxLength() is deprecated.', + ); + + return $this->getVarcharMaxLength(); + } + + /** + * Gets the maximum length of a varchar column. + * + * @deprecated + * + * @return int + */ + public function getVarcharMaxLength() + { + Deprecation::triggerIfCalledFromOutside( + 'doctrine/dbal', + 'https://github.com/doctrine/dbal/issues/3263', + 'AbstractPlatform::getVarcharMaxLength() is deprecated.', + ); + + return 4000; + } + + /** + * Gets the default length of a varchar column. + * + * @deprecated + * + * @return int + */ + public function getVarcharDefaultLength() + { + Deprecation::triggerIfCalledFromOutside( + 'doctrine/dbal', + 'https://github.com/doctrine/dbal/issues/3263', + 'Relying on the default varchar column length is deprecated, specify the length explicitly.', + ); + + return 255; + } + + /** + * Gets the maximum length of a binary column. + * + * @deprecated + * + * @return int + */ + public function getBinaryMaxLength() + { + Deprecation::triggerIfCalledFromOutside( + 'doctrine/dbal', + 'https://github.com/doctrine/dbal/issues/3263', + 'AbstractPlatform::getBinaryMaxLength() is deprecated.', + ); + + return 4000; + } + + /** + * Gets the default length of a binary column. + * + * @deprecated + * + * @return int + */ + public function getBinaryDefaultLength() + { + Deprecation::trigger( + 'doctrine/dbal', + 'https://github.com/doctrine/dbal/issues/3263', + 'Relying on the default binary column length is deprecated, specify the length explicitly.', + ); + + return 255; + } + + /** + * Gets all SQL wildcard characters of the platform. + * + * @deprecated Use {@see AbstractPlatform::getLikeWildcardCharacters()} instead. + * + * @return string[] + */ + public function getWildcards() + { + Deprecation::trigger( + 'doctrine/dbal', + 'https://github.com/doctrine/dbal/pull/4724', + 'AbstractPlatform::getWildcards() is deprecated.' + . ' Use AbstractPlatform::getLikeWildcardCharacters() instead.', + ); + + return ['%', '_']; + } + + /** + * Returns the regular expression operator. + * + * @return string + * + * @throws Exception If not supported on this platform. + */ + public function getRegexpExpression() + { + throw Exception::notSupported(__METHOD__); + } + + /** + * Returns the SQL snippet to get the average value of a column. + * + * @deprecated Use AVG() in SQL instead. + * + * @param string $column The column to use. + * + * @return string Generated SQL including an AVG aggregate function. + */ + public function getAvgExpression($column) + { + Deprecation::trigger( + 'doctrine/dbal', + 'https://github.com/doctrine/dbal/pull/4724', + 'AbstractPlatform::getAvgExpression() is deprecated. Use AVG() in SQL instead.', + ); + + return 'AVG(' . $column . ')'; + } + + /** + * Returns the SQL snippet to get the number of rows (without a NULL value) of a column. + * + * If a '*' is used instead of a column the number of selected rows is returned. + * + * @deprecated Use COUNT() in SQL instead. + * + * @param string|int $column The column to use. + * + * @return string Generated SQL including a COUNT aggregate function. + */ + public function getCountExpression($column) + { + Deprecation::trigger( + 'doctrine/dbal', + 'https://github.com/doctrine/dbal/pull/4724', + 'AbstractPlatform::getCountExpression() is deprecated. Use COUNT() in SQL instead.', + ); + + return 'COUNT(' . $column . ')'; + } + + /** + * Returns the SQL snippet to get the highest value of a column. + * + * @deprecated Use MAX() in SQL instead. + * + * @param string $column The column to use. + * + * @return string Generated SQL including a MAX aggregate function. + */ + public function getMaxExpression($column) + { + Deprecation::trigger( + 'doctrine/dbal', + 'https://github.com/doctrine/dbal/pull/4724', + 'AbstractPlatform::getMaxExpression() is deprecated. Use MAX() in SQL instead.', + ); + + return 'MAX(' . $column . ')'; + } + + /** + * Returns the SQL snippet to get the lowest value of a column. + * + * @deprecated Use MIN() in SQL instead. + * + * @param string $column The column to use. + * + * @return string Generated SQL including a MIN aggregate function. + */ + public function getMinExpression($column) + { + Deprecation::trigger( + 'doctrine/dbal', + 'https://github.com/doctrine/dbal/pull/4724', + 'AbstractPlatform::getMinExpression() is deprecated. Use MIN() in SQL instead.', + ); + + return 'MIN(' . $column . ')'; + } + + /** + * Returns the SQL snippet to get the total sum of a column. + * + * @deprecated Use SUM() in SQL instead. + * + * @param string $column The column to use. + * + * @return string Generated SQL including a SUM aggregate function. + */ + public function getSumExpression($column) + { + Deprecation::trigger( + 'doctrine/dbal', + 'https://github.com/doctrine/dbal/pull/4724', + 'AbstractPlatform::getSumExpression() is deprecated. Use SUM() in SQL instead.', + ); + + return 'SUM(' . $column . ')'; + } + + // scalar functions + + /** + * Returns the SQL snippet to get the md5 sum of a column. + * + * Note: Not SQL92, but common functionality. + * + * @deprecated + * + * @param string $column + * + * @return string + */ + public function getMd5Expression($column) + { + Deprecation::trigger( + 'doctrine/dbal', + 'https://github.com/doctrine/dbal/pull/4724', + 'AbstractPlatform::getMd5Expression() is deprecated.', + ); + + return 'MD5(' . $column . ')'; + } + + /** + * Returns the SQL snippet to get the length of a text column in characters. + * + * @param string $column + * + * @return string + */ + public function getLengthExpression($column) + { + return 'LENGTH(' . $column . ')'; + } + + /** + * Returns the SQL snippet to get the squared value of a column. + * + * @deprecated Use SQRT() in SQL instead. + * + * @param string $column The column to use. + * + * @return string Generated SQL including an SQRT aggregate function. + */ + public function getSqrtExpression($column) + { + Deprecation::trigger( + 'doctrine/dbal', + 'https://github.com/doctrine/dbal/pull/4724', + 'AbstractPlatform::getSqrtExpression() is deprecated. Use SQRT() in SQL instead.', + ); + + return 'SQRT(' . $column . ')'; + } + + /** + * Returns the SQL snippet to round a numeric column to the number of decimals specified. + * + * @deprecated Use ROUND() in SQL instead. + * + * @param string $column + * @param string|int $decimals + * + * @return string + */ + public function getRoundExpression($column, $decimals = 0) + { + Deprecation::trigger( + 'doctrine/dbal', + 'https://github.com/doctrine/dbal/pull/4724', + 'AbstractPlatform::getRoundExpression() is deprecated. Use ROUND() in SQL instead.', + ); + + return 'ROUND(' . $column . ', ' . $decimals . ')'; + } + + /** + * Returns the SQL snippet to get the remainder of the division operation $expression1 / $expression2. + * + * @param string $expression1 + * @param string $expression2 + * + * @return string + */ + public function getModExpression($expression1, $expression2) + { + return 'MOD(' . $expression1 . ', ' . $expression2 . ')'; + } + + /** + * Returns the SQL snippet to trim a string. + * + * @param string $str The expression to apply the trim to. + * @param int $mode The position of the trim (leading/trailing/both). + * @param string|bool $char The char to trim, has to be quoted already. Defaults to space. + * + * @return string + */ + public function getTrimExpression($str, $mode = TrimMode::UNSPECIFIED, $char = false) + { + $expression = ''; + + switch ($mode) { + case TrimMode::LEADING: + $expression = 'LEADING '; + break; + + case TrimMode::TRAILING: + $expression = 'TRAILING '; + break; + + case TrimMode::BOTH: + $expression = 'BOTH '; + break; + } + + if ($char !== false) { + $expression .= $char . ' '; + } + + if ($mode !== TrimMode::UNSPECIFIED || $char !== false) { + $expression .= 'FROM '; + } + + return 'TRIM(' . $expression . $str . ')'; + } + + /** + * Returns the SQL snippet to trim trailing space characters from the expression. + * + * @deprecated Use RTRIM() in SQL instead. + * + * @param string $str Literal string or column name. + * + * @return string + */ + public function getRtrimExpression($str) + { + Deprecation::trigger( + 'doctrine/dbal', + 'https://github.com/doctrine/dbal/pull/4724', + 'AbstractPlatform::getRtrimExpression() is deprecated. Use RTRIM() in SQL instead.', + ); + + return 'RTRIM(' . $str . ')'; + } + + /** + * Returns the SQL snippet to trim leading space characters from the expression. + * + * @deprecated Use LTRIM() in SQL instead. + * + * @param string $str Literal string or column name. + * + * @return string + */ + public function getLtrimExpression($str) + { + Deprecation::trigger( + 'doctrine/dbal', + 'https://github.com/doctrine/dbal/pull/4724', + 'AbstractPlatform::getLtrimExpression() is deprecated. Use LTRIM() in SQL instead.', + ); + + return 'LTRIM(' . $str . ')'; + } + + /** + * Returns the SQL snippet to change all characters from the expression to uppercase, + * according to the current character set mapping. + * + * @deprecated Use UPPER() in SQL instead. + * + * @param string $str Literal string or column name. + * + * @return string + */ + public function getUpperExpression($str) + { + Deprecation::trigger( + 'doctrine/dbal', + 'https://github.com/doctrine/dbal/pull/4724', + 'AbstractPlatform::getUpperExpression() is deprecated. Use UPPER() in SQL instead.', + ); + + return 'UPPER(' . $str . ')'; + } + + /** + * Returns the SQL snippet to change all characters from the expression to lowercase, + * according to the current character set mapping. + * + * @deprecated Use LOWER() in SQL instead. + * + * @param string $str Literal string or column name. + * + * @return string + */ + public function getLowerExpression($str) + { + Deprecation::trigger( + 'doctrine/dbal', + 'https://github.com/doctrine/dbal/pull/4724', + 'AbstractPlatform::getLowerExpression() is deprecated. Use LOWER() in SQL instead.', + ); + + return 'LOWER(' . $str . ')'; + } + + /** + * Returns the SQL snippet to get the position of the first occurrence of substring $substr in string $str. + * + * @param string $str Literal string. + * @param string $substr Literal string to find. + * @param string|int|false $startPos Position to start at, beginning of string by default. + * + * @return string + * + * @throws Exception If not supported on this platform. + */ + public function getLocateExpression($str, $substr, $startPos = false) + { + throw Exception::notSupported(__METHOD__); + } + + /** + * Returns the SQL snippet to get the current system date. + * + * @deprecated Generate dates within the application. + * + * @return string + */ + public function getNowExpression() + { + Deprecation::trigger( + 'doctrine/dbal', + 'https://github.com/doctrine/dbal/pull/4753', + 'AbstractPlatform::getNowExpression() is deprecated. Generate dates within the application.', + ); + + return 'NOW()'; + } + + /** + * Returns a SQL snippet to get a substring inside an SQL statement. + * + * Note: Not SQL92, but common functionality. + * + * SQLite only supports the 2 parameter variant of this function. + * + * @param string $string An sql string literal or column name/alias. + * @param string|int $start Where to start the substring portion. + * @param string|int|null $length The substring portion length. + * + * @return string + */ + public function getSubstringExpression($string, $start, $length = null) + { + if ($length === null) { + return 'SUBSTRING(' . $string . ' FROM ' . $start . ')'; + } + + return 'SUBSTRING(' . $string . ' FROM ' . $start . ' FOR ' . $length . ')'; + } + + /** + * Returns a SQL snippet to concatenate the given expressions. + * + * Accepts an arbitrary number of string parameters. Each parameter must contain an expression. + * + * @return string + */ + public function getConcatExpression() + { + return implode(' || ', func_get_args()); + } + + /** + * Returns the SQL for a logical not. + * + * Example: + * + * $q = new Doctrine_Query(); + * $e = $q->expr; + * $q->select('*')->from('table') + * ->where($e->eq('id', $e->not('null')); + * + * + * @deprecated Use NOT() in SQL instead. + * + * @param string $expression + * + * @return string The logical expression. + */ + public function getNotExpression($expression) + { + Deprecation::trigger( + 'doctrine/dbal', + 'https://github.com/doctrine/dbal/pull/4724', + 'AbstractPlatform::getNotExpression() is deprecated. Use NOT() in SQL instead.', + ); + + return 'NOT(' . $expression . ')'; + } + + /** + * Returns the SQL that checks if an expression is null. + * + * @deprecated Use IS NULL in SQL instead. + * + * @param string $expression The expression that should be compared to null. + * + * @return string The logical expression. + */ + public function getIsNullExpression($expression) + { + Deprecation::trigger( + 'doctrine/dbal', + 'https://github.com/doctrine/dbal/pull/4724', + 'AbstractPlatform::getIsNullExpression() is deprecated. Use IS NULL in SQL instead.', + ); + + return $expression . ' IS NULL'; + } + + /** + * Returns the SQL that checks if an expression is not null. + * + * @deprecated Use IS NOT NULL in SQL instead. + * + * @param string $expression The expression that should be compared to null. + * + * @return string The logical expression. + */ + public function getIsNotNullExpression($expression) + { + Deprecation::trigger( + 'doctrine/dbal', + 'https://github.com/doctrine/dbal/pull/4724', + 'AbstractPlatform::getIsNotNullExpression() is deprecated. Use IS NOT NULL in SQL instead.', + ); + + return $expression . ' IS NOT NULL'; + } + + /** + * Returns the SQL that checks if an expression evaluates to a value between two values. + * + * The parameter $expression is checked if it is between $value1 and $value2. + * + * Note: There is a slight difference in the way BETWEEN works on some databases. + * http://www.w3schools.com/sql/sql_between.asp. If you want complete database + * independence you should avoid using between(). + * + * @deprecated Use BETWEEN in SQL instead. + * + * @param string $expression The value to compare to. + * @param string $value1 The lower value to compare with. + * @param string $value2 The higher value to compare with. + * + * @return string The logical expression. + */ + public function getBetweenExpression($expression, $value1, $value2) + { + Deprecation::trigger( + 'doctrine/dbal', + 'https://github.com/doctrine/dbal/pull/4724', + 'AbstractPlatform::getBetweenExpression() is deprecated. Use BETWEEN in SQL instead.', + ); + + return $expression . ' BETWEEN ' . $value1 . ' AND ' . $value2; + } + + /** + * Returns the SQL to get the arccosine of a value. + * + * @deprecated Use ACOS() in SQL instead. + * + * @param string $value + * + * @return string + */ + public function getAcosExpression($value) + { + Deprecation::trigger( + 'doctrine/dbal', + 'https://github.com/doctrine/dbal/pull/4724', + 'AbstractPlatform::getAcosExpression() is deprecated. Use ACOS() in SQL instead.', + ); + + return 'ACOS(' . $value . ')'; + } + + /** + * Returns the SQL to get the sine of a value. + * + * @deprecated Use SIN() in SQL instead. + * + * @param string $value + * + * @return string + */ + public function getSinExpression($value) + { + Deprecation::trigger( + 'doctrine/dbal', + 'https://github.com/doctrine/dbal/pull/4724', + 'AbstractPlatform::getSinExpression() is deprecated. Use SIN() in SQL instead.', + ); + + return 'SIN(' . $value . ')'; + } + + /** + * Returns the SQL to get the PI value. + * + * @deprecated Use PI() in SQL instead. + * + * @return string + */ + public function getPiExpression() + { + Deprecation::trigger( + 'doctrine/dbal', + 'https://github.com/doctrine/dbal/pull/4724', + 'AbstractPlatform::getPiExpression() is deprecated. Use PI() in SQL instead.', + ); + + return 'PI()'; + } + + /** + * Returns the SQL to get the cosine of a value. + * + * @deprecated Use COS() in SQL instead. + * + * @param string $value + * + * @return string + */ + public function getCosExpression($value) + { + Deprecation::trigger( + 'doctrine/dbal', + 'https://github.com/doctrine/dbal/pull/4724', + 'AbstractPlatform::getCosExpression() is deprecated. Use COS() in SQL instead.', + ); + + return 'COS(' . $value . ')'; + } + + /** + * Returns the SQL to calculate the difference in days between the two passed dates. + * + * Computes diff = date1 - date2. + * + * @param string $date1 + * @param string $date2 + * + * @return string + * + * @throws Exception If not supported on this platform. + */ + public function getDateDiffExpression($date1, $date2) + { + throw Exception::notSupported(__METHOD__); + } + + /** + * Returns the SQL to add the number of given seconds to a date. + * + * @param string $date + * @param int|string $seconds + * + * @return string + * + * @throws Exception If not supported on this platform. + */ + public function getDateAddSecondsExpression($date, $seconds) + { + if (is_int($seconds)) { + Deprecation::trigger( + 'doctrine/dbal', + 'https://github.com/doctrine/dbal/pull/3498', + 'Passing $seconds as an integer is deprecated. Pass it as a numeric string instead.', + ); + } + + return $this->getDateArithmeticIntervalExpression($date, '+', $seconds, DateIntervalUnit::SECOND); + } + + /** + * Returns the SQL to subtract the number of given seconds from a date. + * + * @param string $date + * @param int|string $seconds + * + * @return string + * + * @throws Exception If not supported on this platform. + */ + public function getDateSubSecondsExpression($date, $seconds) + { + if (is_int($seconds)) { + Deprecation::trigger( + 'doctrine/dbal', + 'https://github.com/doctrine/dbal/pull/3498', + 'Passing $seconds as an integer is deprecated. Pass it as a numeric string instead.', + ); + } + + return $this->getDateArithmeticIntervalExpression($date, '-', $seconds, DateIntervalUnit::SECOND); + } + + /** + * Returns the SQL to add the number of given minutes to a date. + * + * @param string $date + * @param int|string $minutes + * + * @return string + * + * @throws Exception If not supported on this platform. + */ + public function getDateAddMinutesExpression($date, $minutes) + { + if (is_int($minutes)) { + Deprecation::trigger( + 'doctrine/dbal', + 'https://github.com/doctrine/dbal/pull/3498', + 'Passing $minutes as an integer is deprecated. Pass it as a numeric string instead.', + ); + } + + return $this->getDateArithmeticIntervalExpression($date, '+', $minutes, DateIntervalUnit::MINUTE); + } + + /** + * Returns the SQL to subtract the number of given minutes from a date. + * + * @param string $date + * @param int|string $minutes + * + * @return string + * + * @throws Exception If not supported on this platform. + */ + public function getDateSubMinutesExpression($date, $minutes) + { + if (is_int($minutes)) { + Deprecation::trigger( + 'doctrine/dbal', + 'https://github.com/doctrine/dbal/pull/3498', + 'Passing $minutes as an integer is deprecated. Pass it as a numeric string instead.', + ); + } + + return $this->getDateArithmeticIntervalExpression($date, '-', $minutes, DateIntervalUnit::MINUTE); + } + + /** + * Returns the SQL to add the number of given hours to a date. + * + * @param string $date + * @param int|string $hours + * + * @return string + * + * @throws Exception If not supported on this platform. + */ + public function getDateAddHourExpression($date, $hours) + { + if (is_int($hours)) { + Deprecation::trigger( + 'doctrine/dbal', + 'https://github.com/doctrine/dbal/pull/3498', + 'Passing $hours as an integer is deprecated. Pass it as a numeric string instead.', + ); + } + + return $this->getDateArithmeticIntervalExpression($date, '+', $hours, DateIntervalUnit::HOUR); + } + + /** + * Returns the SQL to subtract the number of given hours to a date. + * + * @param string $date + * @param int|string $hours + * + * @return string + * + * @throws Exception If not supported on this platform. + */ + public function getDateSubHourExpression($date, $hours) + { + if (is_int($hours)) { + Deprecation::trigger( + 'doctrine/dbal', + 'https://github.com/doctrine/dbal/pull/3498', + 'Passing $hours as an integer is deprecated. Pass it as a numeric string instead.', + ); + } + + return $this->getDateArithmeticIntervalExpression($date, '-', $hours, DateIntervalUnit::HOUR); + } + + /** + * Returns the SQL to add the number of given days to a date. + * + * @param string $date + * @param int|string $days + * + * @return string + * + * @throws Exception If not supported on this platform. + */ + public function getDateAddDaysExpression($date, $days) + { + if (is_int($days)) { + Deprecation::trigger( + 'doctrine/dbal', + 'https://github.com/doctrine/dbal/pull/3498', + 'Passing $days as an integer is deprecated. Pass it as a numeric string instead.', + ); + } + + return $this->getDateArithmeticIntervalExpression($date, '+', $days, DateIntervalUnit::DAY); + } + + /** + * Returns the SQL to subtract the number of given days to a date. + * + * @param string $date + * @param int|string $days + * + * @return string + * + * @throws Exception If not supported on this platform. + */ + public function getDateSubDaysExpression($date, $days) + { + if (is_int($days)) { + Deprecation::trigger( + 'doctrine/dbal', + 'https://github.com/doctrine/dbal/pull/3498', + 'Passing $days as an integer is deprecated. Pass it as a numeric string instead.', + ); + } + + return $this->getDateArithmeticIntervalExpression($date, '-', $days, DateIntervalUnit::DAY); + } + + /** + * Returns the SQL to add the number of given weeks to a date. + * + * @param string $date + * @param int|string $weeks + * + * @return string + * + * @throws Exception If not supported on this platform. + */ + public function getDateAddWeeksExpression($date, $weeks) + { + if (is_int($weeks)) { + Deprecation::trigger( + 'doctrine/dbal', + 'https://github.com/doctrine/dbal/pull/3498', + 'Passing $weeks as an integer is deprecated. Pass it as a numeric string instead.', + ); + } + + return $this->getDateArithmeticIntervalExpression($date, '+', $weeks, DateIntervalUnit::WEEK); + } + + /** + * Returns the SQL to subtract the number of given weeks from a date. + * + * @param string $date + * @param int|string $weeks + * + * @return string + * + * @throws Exception If not supported on this platform. + */ + public function getDateSubWeeksExpression($date, $weeks) + { + if (is_int($weeks)) { + Deprecation::trigger( + 'doctrine/dbal', + 'https://github.com/doctrine/dbal/pull/3498', + 'Passing $weeks as an integer is deprecated. Pass it as a numeric string instead.', + ); + } + + return $this->getDateArithmeticIntervalExpression($date, '-', $weeks, DateIntervalUnit::WEEK); + } + + /** + * Returns the SQL to add the number of given months to a date. + * + * @param string $date + * @param int|string $months + * + * @return string + * + * @throws Exception If not supported on this platform. + */ + public function getDateAddMonthExpression($date, $months) + { + if (is_int($months)) { + Deprecation::trigger( + 'doctrine/dbal', + 'https://github.com/doctrine/dbal/pull/3498', + 'Passing $months as an integer is deprecated. Pass it as a numeric string instead.', + ); + } + + return $this->getDateArithmeticIntervalExpression($date, '+', $months, DateIntervalUnit::MONTH); + } + + /** + * Returns the SQL to subtract the number of given months to a date. + * + * @param string $date + * @param int|string $months + * + * @return string + * + * @throws Exception If not supported on this platform. + */ + public function getDateSubMonthExpression($date, $months) + { + if (is_int($months)) { + Deprecation::trigger( + 'doctrine/dbal', + 'https://github.com/doctrine/dbal/pull/3498', + 'Passing $months as an integer is deprecated. Pass it as a numeric string instead.', + ); + } + + return $this->getDateArithmeticIntervalExpression($date, '-', $months, DateIntervalUnit::MONTH); + } + + /** + * Returns the SQL to add the number of given quarters to a date. + * + * @param string $date + * @param int|string $quarters + * + * @return string + * + * @throws Exception If not supported on this platform. + */ + public function getDateAddQuartersExpression($date, $quarters) + { + if (is_int($quarters)) { + Deprecation::trigger( + 'doctrine/dbal', + 'https://github.com/doctrine/dbal/pull/3498', + 'Passing $quarters as an integer is deprecated. Pass it as a numeric string instead.', + ); + } + + return $this->getDateArithmeticIntervalExpression($date, '+', $quarters, DateIntervalUnit::QUARTER); + } + + /** + * Returns the SQL to subtract the number of given quarters from a date. + * + * @param string $date + * @param int|string $quarters + * + * @return string + * + * @throws Exception If not supported on this platform. + */ + public function getDateSubQuartersExpression($date, $quarters) + { + if (is_int($quarters)) { + Deprecation::trigger( + 'doctrine/dbal', + 'https://github.com/doctrine/dbal/pull/3498', + 'Passing $quarters as an integer is deprecated. Pass it as a numeric string instead.', + ); + } + + return $this->getDateArithmeticIntervalExpression($date, '-', $quarters, DateIntervalUnit::QUARTER); + } + + /** + * Returns the SQL to add the number of given years to a date. + * + * @param string $date + * @param int|string $years + * + * @return string + * + * @throws Exception If not supported on this platform. + */ + public function getDateAddYearsExpression($date, $years) + { + if (is_int($years)) { + Deprecation::trigger( + 'doctrine/dbal', + 'https://github.com/doctrine/dbal/pull/3498', + 'Passing $years as an integer is deprecated. Pass it as a numeric string instead.', + ); + } + + return $this->getDateArithmeticIntervalExpression($date, '+', $years, DateIntervalUnit::YEAR); + } + + /** + * Returns the SQL to subtract the number of given years from a date. + * + * @param string $date + * @param int|string $years + * + * @return string + * + * @throws Exception If not supported on this platform. + */ + public function getDateSubYearsExpression($date, $years) + { + if (is_int($years)) { + Deprecation::trigger( + 'doctrine/dbal', + 'https://github.com/doctrine/dbal/pull/3498', + 'Passing $years as an integer is deprecated. Pass it as a numeric string instead.', + ); + } + + return $this->getDateArithmeticIntervalExpression($date, '-', $years, DateIntervalUnit::YEAR); + } + + /** + * Returns the SQL for a date arithmetic expression. + * + * @param string $date The column or literal representing a date + * to perform the arithmetic operation on. + * @param string $operator The arithmetic operator (+ or -). + * @param int|string $interval The interval that shall be calculated into the date. + * @param string $unit The unit of the interval that shall be calculated into the date. + * One of the {@see DateIntervalUnit} constants. + * + * @return string + * + * @throws Exception If not supported on this platform. + */ + protected function getDateArithmeticIntervalExpression($date, $operator, $interval, $unit) + { + throw Exception::notSupported(__METHOD__); + } + + /** + * Generates the SQL expression which represents the given date interval multiplied by a number + * + * @param string $interval SQL expression describing the interval value + * @param int $multiplier Interval multiplier + */ + protected function multiplyInterval(string $interval, int $multiplier): string + { + return sprintf('(%s * %d)', $interval, $multiplier); + } + + /** + * Returns the SQL bit AND comparison expression. + * + * @param string $value1 + * @param string $value2 + * + * @return string + */ + public function getBitAndComparisonExpression($value1, $value2) + { + return '(' . $value1 . ' & ' . $value2 . ')'; + } + + /** + * Returns the SQL bit OR comparison expression. + * + * @param string $value1 + * @param string $value2 + * + * @return string + */ + public function getBitOrComparisonExpression($value1, $value2) + { + return '(' . $value1 . ' | ' . $value2 . ')'; + } + + /** + * Returns the SQL expression which represents the currently selected database. + */ + abstract public function getCurrentDatabaseExpression(): string; + + /** + * Returns the FOR UPDATE expression. + * + * @deprecated This API is not portable. Use {@link QueryBuilder::forUpdate()}` instead. + * + * @return string + */ + public function getForUpdateSQL() + { + Deprecation::triggerIfCalledFromOutside( + 'doctrine/dbal', + 'https://github.com/doctrine/dbal/pull/6191', + '%s is deprecated as non-portable.', + __METHOD__, + ); + + return 'FOR UPDATE'; + } + + /** + * Honors that some SQL vendors such as MsSql use table hints for locking instead of the + * ANSI SQL FOR UPDATE specification. + * + * @param string $fromClause The FROM clause to append the hint for the given lock mode to + * @param int $lockMode One of the Doctrine\DBAL\LockMode::* constants + * @phpstan-param LockMode::* $lockMode + */ + public function appendLockHint(string $fromClause, int $lockMode): string + { + switch ($lockMode) { + case LockMode::NONE: + case LockMode::OPTIMISTIC: + case LockMode::PESSIMISTIC_READ: + case LockMode::PESSIMISTIC_WRITE: + return $fromClause; + + default: + throw InvalidLockMode::fromLockMode($lockMode); + } + } + + /** + * Returns the SQL snippet to append to any SELECT statement which locks rows in shared read lock. + * + * This defaults to the ANSI SQL "FOR UPDATE", which is an exclusive lock (Write). Some database + * vendors allow to lighten this constraint up to be a real read lock. + * + * @deprecated This API is not portable. + * + * @return string + */ + public function getReadLockSQL() + { + Deprecation::trigger( + 'doctrine/dbal', + 'https://github.com/doctrine/dbal/pull/6191', + '%s is deprecated as non-portable.', + __METHOD__, + ); + + return $this->getForUpdateSQL(); + } + + /** + * Returns the SQL snippet to append to any SELECT statement which obtains an exclusive lock on the rows. + * + * The semantics of this lock mode should equal the SELECT .. FOR UPDATE of the ANSI SQL standard. + * + * @deprecated This API is not portable. + * + * @return string + */ + public function getWriteLockSQL() + { + Deprecation::trigger( + 'doctrine/dbal', + 'https://github.com/doctrine/dbal/pull/6191', + '%s is deprecated as non-portable.', + __METHOD__, + ); + + return $this->getForUpdateSQL(); + } + + /** + * Returns the SQL snippet to drop an existing table. + * + * @param Table|string $table + * + * @return string + * + * @throws InvalidArgumentException + */ + public function getDropTableSQL($table) + { + $tableArg = $table; + + if ($table instanceof Table) { + Deprecation::trigger( + 'doctrine/dbal', + 'https://github.com/doctrine/dbal/issues/4798', + 'Passing $table as a Table object to %s is deprecated. Pass it as a quoted name instead.', + __METHOD__, + ); + + $table = $table->getQuotedName($this); + } + + if (! is_string($table)) { + throw new InvalidArgumentException( + __METHOD__ . '() expects $table parameter to be string or ' . Table::class . '.', + ); + } + + if ($this->_eventManager !== null && $this->_eventManager->hasListeners(Events::onSchemaDropTable)) { + Deprecation::trigger( + 'doctrine/dbal', + 'https://github.com/doctrine/dbal/issues/5784', + 'Subscribing to %s events is deprecated.', + Events::onSchemaDropTable, + ); + + $eventArgs = new SchemaDropTableEventArgs($tableArg, $this); + $this->_eventManager->dispatchEvent(Events::onSchemaDropTable, $eventArgs); + + if ($eventArgs->isDefaultPrevented()) { + $sql = $eventArgs->getSql(); + + if ($sql === null) { + throw new UnexpectedValueException('Default implementation of DROP TABLE was overridden with NULL'); + } + + return $sql; + } + } + + return 'DROP TABLE ' . $table; + } + + /** + * Returns the SQL to safely drop a temporary table WITHOUT implicitly committing an open transaction. + * + * @param Table|string $table + * + * @return string + */ + public function getDropTemporaryTableSQL($table) + { + if ($table instanceof Table) { + Deprecation::trigger( + 'doctrine/dbal', + 'https://github.com/doctrine/dbal/issues/4798', + 'Passing $table as a Table object to %s is deprecated. Pass it as a quoted name instead.', + __METHOD__, + ); + + $table = $table->getQuotedName($this); + } + + return $this->getDropTableSQL($table); + } + + /** + * Returns the SQL to drop an index from a table. + * + * @param Index|string $index + * @param Table|string|null $table + * + * @return string + * + * @throws InvalidArgumentException + */ + public function getDropIndexSQL($index, $table = null) + { + if ($index instanceof Index) { + Deprecation::trigger( + 'doctrine/dbal', + 'https://github.com/doctrine/dbal/issues/4798', + 'Passing $index as an Index object to %s is deprecated. Pass it as a quoted name instead.', + __METHOD__, + ); + + $index = $index->getQuotedName($this); + } elseif (! is_string($index)) { + throw new InvalidArgumentException( + __METHOD__ . '() expects $index parameter to be string or ' . Index::class . '.', + ); + } + + return 'DROP INDEX ' . $index; + } + + /** + * Returns the SQL to drop a constraint. + * + * @internal The method should be only used from within the {@see AbstractPlatform} class hierarchy. + * + * @param Constraint|string $constraint + * @param Table|string $table + * + * @return string + */ + public function getDropConstraintSQL($constraint, $table) + { + if ($constraint instanceof Constraint) { + Deprecation::trigger( + 'doctrine/dbal', + 'https://github.com/doctrine/dbal/issues/4798', + 'Passing $constraint as a Constraint object to %s is deprecated. Pass it as a quoted name instead.', + __METHOD__, + ); + } else { + $constraint = new Identifier($constraint); + } + + if ($table instanceof Table) { + Deprecation::trigger( + 'doctrine/dbal', + 'https://github.com/doctrine/dbal/issues/4798', + 'Passing $table as a Table object to %s is deprecated. Pass it as a quoted name instead.', + __METHOD__, + ); + } else { + $table = new Identifier($table); + } + + $constraint = $constraint->getQuotedName($this); + $table = $table->getQuotedName($this); + + return 'ALTER TABLE ' . $table . ' DROP CONSTRAINT ' . $constraint; + } + + /** + * Returns the SQL to drop a foreign key. + * + * @param ForeignKeyConstraint|string $foreignKey + * @param Table|string $table + * + * @return string + */ + public function getDropForeignKeySQL($foreignKey, $table) + { + if ($foreignKey instanceof ForeignKeyConstraint) { + Deprecation::trigger( + 'doctrine/dbal', + 'https://github.com/doctrine/dbal/issues/4798', + 'Passing $foreignKey as a ForeignKeyConstraint object to %s is deprecated.' + . ' Pass it as a quoted name instead.', + __METHOD__, + ); + } else { + $foreignKey = new Identifier($foreignKey); + } + + if ($table instanceof Table) { + Deprecation::trigger( + 'doctrine/dbal', + 'https://github.com/doctrine/dbal/issues/4798', + 'Passing $table as a Table object to %s is deprecated. Pass it as a quoted name instead.', + __METHOD__, + ); + } else { + $table = new Identifier($table); + } + + $foreignKey = $foreignKey->getQuotedName($this); + $table = $table->getQuotedName($this); + + return 'ALTER TABLE ' . $table . ' DROP FOREIGN KEY ' . $foreignKey; + } + + /** + * Returns the SQL to drop a unique constraint. + */ + public function getDropUniqueConstraintSQL(string $name, string $tableName): string + { + return $this->getDropConstraintSQL($name, $tableName); + } + + /** + * Returns the SQL statement(s) to create a table with the specified name, columns and constraints + * on this platform. + * + * @param int $createFlags + * @phpstan-param int-mask-of $createFlags + * + * @return list The list of SQL statements. + * + * @throws Exception + * @throws InvalidArgumentException + */ + public function getCreateTableSQL(Table $table, $createFlags = self::CREATE_INDEXES) + { + if (! is_int($createFlags)) { + throw new InvalidArgumentException( + 'Second argument of AbstractPlatform::getCreateTableSQL() has to be integer.', + ); + } + + if (($createFlags & self::CREATE_INDEXES) === 0) { + Deprecation::trigger( + 'doctrine/dbal', + 'https://github.com/doctrine/dbal/pull/5416', + 'Unsetting the CREATE_INDEXES flag in AbstractPlatform::getCreateTableSQL() is deprecated.', + ); + } + + if (($createFlags & self::CREATE_FOREIGNKEYS) === 0) { + Deprecation::trigger( + 'doctrine/dbal', + 'https://github.com/doctrine/dbal/pull/5416', + 'Not setting the CREATE_FOREIGNKEYS flag in AbstractPlatform::getCreateTableSQL()' + . ' is deprecated. In order to build the statements that create multiple tables' + . ' referencing each other via foreign keys, use AbstractPlatform::getCreateTablesSQL().', + ); + } + + return $this->buildCreateTableSQL( + $table, + ($createFlags & self::CREATE_INDEXES) > 0, + ($createFlags & self::CREATE_FOREIGNKEYS) > 0, + ); + } + + public function createSelectSQLBuilder(): SelectSQLBuilder + { + return new DefaultSelectSQLBuilder($this, 'FOR UPDATE', 'SKIP LOCKED'); + } + + /** + * @internal + * + * @return list + * + * @throws Exception + */ + final protected function getCreateTableWithoutForeignKeysSQL(Table $table): array + { + return $this->buildCreateTableSQL($table, true, false); + } + + /** + * @return list + * + * @throws Exception + */ + private function buildCreateTableSQL(Table $table, bool $createIndexes, bool $createForeignKeys): array + { + if (count($table->getColumns()) === 0) { + throw Exception::noColumnsSpecifiedForTable($table->getName()); + } + + $tableName = $table->getQuotedName($this); + $options = $table->getOptions(); + $options['uniqueConstraints'] = []; + $options['indexes'] = []; + $options['primary'] = []; + + if ($createIndexes) { + foreach ($table->getIndexes() as $index) { + if (! $index->isPrimary()) { + $options['indexes'][$index->getQuotedName($this)] = $index; + + continue; + } + + $options['primary'] = $index->getQuotedColumns($this); + $options['primary_index'] = $index; + } + + foreach ($table->getUniqueConstraints() as $uniqueConstraint) { + $options['uniqueConstraints'][$uniqueConstraint->getQuotedName($this)] = $uniqueConstraint; + } + } + + if ($createForeignKeys) { + $options['foreignKeys'] = []; + + foreach ($table->getForeignKeys() as $fkConstraint) { + $options['foreignKeys'][] = $fkConstraint; + } + } + + $columnSql = []; + $columns = []; + + foreach ($table->getColumns() as $column) { + if ( + $this->_eventManager !== null + && $this->_eventManager->hasListeners(Events::onSchemaCreateTableColumn) + ) { + Deprecation::trigger( + 'doctrine/dbal', + 'https://github.com/doctrine/dbal/issues/5784', + 'Subscribing to %s events is deprecated.', + Events::onSchemaCreateTableColumn, + ); + + $eventArgs = new SchemaCreateTableColumnEventArgs($column, $table, $this); + + $this->_eventManager->dispatchEvent(Events::onSchemaCreateTableColumn, $eventArgs); + + $columnSql = array_merge($columnSql, $eventArgs->getSql()); + + if ($eventArgs->isDefaultPrevented()) { + continue; + } + } + + $columnData = $this->columnToArray($column); + + if (in_array($column->getName(), $options['primary'], true)) { + $columnData['primary'] = true; + } + + $columns[$columnData['name']] = $columnData; + } + + if ($this->_eventManager !== null && $this->_eventManager->hasListeners(Events::onSchemaCreateTable)) { + Deprecation::trigger( + 'doctrine/dbal', + 'https://github.com/doctrine/dbal/issues/5784', + 'Subscribing to %s events is deprecated.', + Events::onSchemaCreateTable, + ); + + $eventArgs = new SchemaCreateTableEventArgs($table, $columns, $options, $this); + + $this->_eventManager->dispatchEvent(Events::onSchemaCreateTable, $eventArgs); + + if ($eventArgs->isDefaultPrevented()) { + return array_merge($eventArgs->getSql(), $columnSql); + } + } + + $sql = $this->_getCreateTableSQL($tableName, $columns, $options); + + if ($this->supportsCommentOnStatement()) { + if ($table->hasOption('comment')) { + $sql[] = $this->getCommentOnTableSQL($tableName, $table->getOption('comment')); + } + + foreach ($table->getColumns() as $column) { + $comment = $this->getColumnComment($column); + + if ($comment === null || $comment === '') { + continue; + } + + $sql[] = $this->getCommentOnColumnSQL($tableName, $column->getQuotedName($this), $comment); + } + } + + return array_merge($sql, $columnSql); + } + + /** + * @param Table[] $tables + * + * @return list + * + * @throws Exception + */ + public function getCreateTablesSQL(array $tables): array + { + $sql = []; + + foreach ($tables as $table) { + $sql = array_merge($sql, $this->getCreateTableWithoutForeignKeysSQL($table)); + } + + foreach ($tables as $table) { + foreach ($table->getForeignKeys() as $foreignKey) { + $sql[] = $this->getCreateForeignKeySQL( + $foreignKey, + $table->getQuotedName($this), + ); + } + } + + return $sql; + } + + /** + * @param list $tables + * + * @return list + */ + public function getDropTablesSQL(array $tables): array + { + $sql = []; + + foreach ($tables as $table) { + foreach ($table->getForeignKeys() as $foreignKey) { + $sql[] = $this->getDropForeignKeySQL( + $foreignKey->getQuotedName($this), + $table->getQuotedName($this), + ); + } + } + + foreach ($tables as $table) { + $sql[] = $this->getDropTableSQL($table->getQuotedName($this)); + } + + return $sql; + } + + protected function getCommentOnTableSQL(string $tableName, ?string $comment): string + { + $tableName = new Identifier($tableName); + + return sprintf( + 'COMMENT ON TABLE %s IS %s', + $tableName->getQuotedName($this), + $this->quoteStringLiteral((string) $comment), + ); + } + + /** + * @param string $tableName + * @param string $columnName + * @param string|null $comment + * + * @return string + */ + public function getCommentOnColumnSQL($tableName, $columnName, $comment) + { + $tableName = new Identifier($tableName); + $columnName = new Identifier($columnName); + + return sprintf( + 'COMMENT ON COLUMN %s.%s IS %s', + $tableName->getQuotedName($this), + $columnName->getQuotedName($this), + $this->quoteStringLiteral((string) $comment), + ); + } + + /** + * Returns the SQL to create inline comment on a column. + * + * @param string $comment + * + * @return string + * + * @throws Exception If not supported on this platform. + */ + public function getInlineColumnCommentSQL($comment) + { + if (! $this->supportsInlineColumnComments()) { + throw Exception::notSupported(__METHOD__); + } + + return 'COMMENT ' . $this->quoteStringLiteral($comment); + } + + /** + * Returns the SQL used to create a table. + * + * @param string $name + * @param mixed[][] $columns + * @param mixed[] $options + * + * @return string[] + */ + protected function _getCreateTableSQL($name, array $columns, array $options = []) + { + $columnListSql = $this->getColumnDeclarationListSQL($columns); + + if (isset($options['uniqueConstraints']) && ! empty($options['uniqueConstraints'])) { + foreach ($options['uniqueConstraints'] as $index => $definition) { + $columnListSql .= ', ' . $this->getUniqueConstraintDeclarationSQL($index, $definition); + } + } + + if (isset($options['primary']) && ! empty($options['primary'])) { + $columnListSql .= ', PRIMARY KEY(' . implode(', ', array_unique(array_values($options['primary']))) . ')'; + } + + if (isset($options['indexes']) && ! empty($options['indexes'])) { + foreach ($options['indexes'] as $index => $definition) { + $columnListSql .= ', ' . $this->getIndexDeclarationSQL($index, $definition); + } + } + + $query = 'CREATE TABLE ' . $name . ' (' . $columnListSql; + $check = $this->getCheckDeclarationSQL($columns); + + if (! empty($check)) { + $query .= ', ' . $check; + } + + $query .= ')'; + + $sql = [$query]; + + if (isset($options['foreignKeys'])) { + foreach ($options['foreignKeys'] as $definition) { + $sql[] = $this->getCreateForeignKeySQL($definition, $name); + } + } + + return $sql; + } + + /** @return string */ + public function getCreateTemporaryTableSnippetSQL() + { + return 'CREATE TEMPORARY TABLE'; + } + + /** + * Generates SQL statements that can be used to apply the diff. + * + * @return list + */ + public function getAlterSchemaSQL(SchemaDiff $diff): array + { + return $diff->toSql($this); + } + + /** + * Returns the SQL to create a sequence on this platform. + * + * @return string + * + * @throws Exception If not supported on this platform. + */ + public function getCreateSequenceSQL(Sequence $sequence) + { + throw Exception::notSupported(__METHOD__); + } + + /** + * Returns the SQL to change a sequence on this platform. + * + * @return string + * + * @throws Exception If not supported on this platform. + */ + public function getAlterSequenceSQL(Sequence $sequence) + { + throw Exception::notSupported(__METHOD__); + } + + /** + * Returns the SQL snippet to drop an existing sequence. + * + * @param Sequence|string $sequence + * + * @return string + * + * @throws Exception If not supported on this platform. + */ + public function getDropSequenceSQL($sequence) + { + if (! $this->supportsSequences()) { + throw Exception::notSupported(__METHOD__); + } + + if ($sequence instanceof Sequence) { + Deprecation::trigger( + 'doctrine/dbal', + 'https://github.com/doctrine/dbal/issues/4798', + 'Passing $sequence as a Sequence object to %s is deprecated. Pass it as a quoted name instead.', + __METHOD__, + ); + + $sequence = $sequence->getQuotedName($this); + } + + return 'DROP SEQUENCE ' . $sequence; + } + + /** + * Returns the SQL to create a constraint on a table on this platform. + * + * @deprecated Use {@see getCreateIndexSQL()}, {@see getCreateForeignKeySQL()} + * or {@see getCreateUniqueConstraintSQL()} instead. + * + * @param Table|string $table + * + * @return string + * + * @throws InvalidArgumentException + */ + public function getCreateConstraintSQL(Constraint $constraint, $table) + { + if ($table instanceof Table) { + Deprecation::trigger( + 'doctrine/dbal', + 'https://github.com/doctrine/dbal/issues/4798', + 'Passing $table as a Table object to %s is deprecated. Pass it as a quoted name instead.', + __METHOD__, + ); + + $table = $table->getQuotedName($this); + } + + $query = 'ALTER TABLE ' . $table . ' ADD CONSTRAINT ' . $constraint->getQuotedName($this); + + $columnList = '(' . implode(', ', $constraint->getQuotedColumns($this)) . ')'; + + $referencesClause = ''; + if ($constraint instanceof Index) { + if ($constraint->isPrimary()) { + $query .= ' PRIMARY KEY'; + } elseif ($constraint->isUnique()) { + $query .= ' UNIQUE'; + } else { + throw new InvalidArgumentException( + 'Can only create primary or unique constraints, no common indexes with getCreateConstraintSQL().', + ); + } + } elseif ($constraint instanceof UniqueConstraint) { + $query .= ' UNIQUE'; + } elseif ($constraint instanceof ForeignKeyConstraint) { + $query .= ' FOREIGN KEY'; + + $referencesClause = ' REFERENCES ' . $constraint->getQuotedForeignTableName($this) . + ' (' . implode(', ', $constraint->getQuotedForeignColumns($this)) . ')'; + } + + $query .= ' ' . $columnList . $referencesClause; + + return $query; + } + + /** + * Returns the SQL to create an index on a table on this platform. + * + * @param Table|string $table The name of the table on which the index is to be created. + * + * @return string + * + * @throws InvalidArgumentException + */ + public function getCreateIndexSQL(Index $index, $table) + { + if ($table instanceof Table) { + Deprecation::trigger( + 'doctrine/dbal', + 'https://github.com/doctrine/dbal/issues/4798', + 'Passing $table as a Table object to %s is deprecated. Pass it as a quoted name instead.', + __METHOD__, + ); + + $table = $table->getQuotedName($this); + } + + $name = $index->getQuotedName($this); + $columns = $index->getColumns(); + + if (count($columns) === 0) { + throw new InvalidArgumentException(sprintf( + 'Incomplete or invalid index definition %s on table %s', + $name, + $table, + )); + } + + if ($index->isPrimary()) { + return $this->getCreatePrimaryKeySQL($index, $table); + } + + $query = 'CREATE ' . $this->getCreateIndexSQLFlags($index) . 'INDEX ' . $name . ' ON ' . $table; + $query .= ' (' . $this->getIndexFieldDeclarationListSQL($index) . ')' . $this->getPartialIndexSQL($index); + + return $query; + } + + /** + * Adds condition for partial index. + * + * @return string + */ + protected function getPartialIndexSQL(Index $index) + { + if ($this->supportsPartialIndexes() && $index->hasOption('where')) { + return ' WHERE ' . $index->getOption('where'); + } + + return ''; + } + + /** + * Adds additional flags for index generation. + * + * @return string + */ + protected function getCreateIndexSQLFlags(Index $index) + { + return $index->isUnique() ? 'UNIQUE ' : ''; + } + + /** + * Returns the SQL to create an unnamed primary key constraint. + * + * @param Table|string $table + * + * @return string + */ + public function getCreatePrimaryKeySQL(Index $index, $table) + { + if ($table instanceof Table) { + Deprecation::trigger( + 'doctrine/dbal', + 'https://github.com/doctrine/dbal/issues/4798', + 'Passing $table as a Table object to %s is deprecated. Pass it as a quoted name instead.', + __METHOD__, + ); + + $table = $table->getQuotedName($this); + } + + return 'ALTER TABLE ' . $table . ' ADD PRIMARY KEY (' . $this->getIndexFieldDeclarationListSQL($index) . ')'; + } + + /** + * Returns the SQL to create a named schema. + * + * @param string $schemaName + * + * @return string + * + * @throws Exception If not supported on this platform. + */ + public function getCreateSchemaSQL($schemaName) + { + if (! $this->supportsSchemas()) { + throw Exception::notSupported(__METHOD__); + } + + return 'CREATE SCHEMA ' . $schemaName; + } + + /** + * Returns the SQL to create a unique constraint on a table on this platform. + */ + public function getCreateUniqueConstraintSQL(UniqueConstraint $constraint, string $tableName): string + { + return $this->getCreateConstraintSQL($constraint, $tableName); + } + + /** + * Returns the SQL snippet to drop a schema. + * + * @throws Exception If not supported on this platform. + */ + public function getDropSchemaSQL(string $schemaName): string + { + if (! $this->supportsSchemas()) { + throw Exception::notSupported(__METHOD__); + } + + return 'DROP SCHEMA ' . $schemaName; + } + + /** + * Quotes a string so that it can be safely used as a table or column name, + * even if it is a reserved word of the platform. This also detects identifier + * chains separated by dot and quotes them independently. + * + * NOTE: Just because you CAN use quoted identifiers doesn't mean + * you SHOULD use them. In general, they end up causing way more + * problems than they solve. + * + * @param string $str The identifier name to be quoted. + * + * @return string The quoted identifier string. + */ + public function quoteIdentifier($str) + { + if (strpos($str, '.') !== false) { + $parts = array_map([$this, 'quoteSingleIdentifier'], explode('.', $str)); + + return implode('.', $parts); + } + + return $this->quoteSingleIdentifier($str); + } + + /** + * Quotes a single identifier (no dot chain separation). + * + * @param string $str The identifier name to be quoted. + * + * @return string The quoted identifier string. + */ + public function quoteSingleIdentifier($str) + { + $c = $this->getIdentifierQuoteCharacter(); + + return $c . str_replace($c, $c . $c, $str) . $c; + } + + /** + * Returns the SQL to create a new foreign key. + * + * @param ForeignKeyConstraint $foreignKey The foreign key constraint. + * @param Table|string $table The name of the table on which the foreign key is to be created. + * + * @return string + */ + public function getCreateForeignKeySQL(ForeignKeyConstraint $foreignKey, $table) + { + if ($table instanceof Table) { + Deprecation::trigger( + 'doctrine/dbal', + 'https://github.com/doctrine/dbal/issues/4798', + 'Passing $table as a Table object to %s is deprecated. Pass it as a quoted name instead.', + __METHOD__, + ); + + $table = $table->getQuotedName($this); + } + + return 'ALTER TABLE ' . $table . ' ADD ' . $this->getForeignKeyDeclarationSQL($foreignKey); + } + + /** + * Gets the SQL statements for altering an existing table. + * + * This method returns an array of SQL statements, since some platforms need several statements. + * + * @return list + * + * @throws Exception If not supported on this platform. + */ + public function getAlterTableSQL(TableDiff $diff) + { + throw Exception::notSupported(__METHOD__); + } + + /** @return list */ + public function getRenameTableSQL(string $oldName, string $newName): array + { + return [ + sprintf('ALTER TABLE %s RENAME TO %s', $oldName, $newName), + ]; + } + + /** + * @param mixed[] $columnSql + * + * @return bool + */ + protected function onSchemaAlterTableAddColumn(Column $column, TableDiff $diff, &$columnSql) + { + if ($this->_eventManager === null) { + return false; + } + + if (! $this->_eventManager->hasListeners(Events::onSchemaAlterTableAddColumn)) { + return false; + } + + Deprecation::trigger( + 'doctrine/dbal', + 'https://github.com/doctrine/dbal/issues/5784', + 'Subscribing to %s events is deprecated.', + Events::onSchemaAlterTableAddColumn, + ); + + $eventArgs = new SchemaAlterTableAddColumnEventArgs($column, $diff, $this); + $this->_eventManager->dispatchEvent(Events::onSchemaAlterTableAddColumn, $eventArgs); + + $columnSql = array_merge($columnSql, $eventArgs->getSql()); + + return $eventArgs->isDefaultPrevented(); + } + + /** + * @param string[] $columnSql + * + * @return bool + */ + protected function onSchemaAlterTableRemoveColumn(Column $column, TableDiff $diff, &$columnSql) + { + if ($this->_eventManager === null) { + return false; + } + + if (! $this->_eventManager->hasListeners(Events::onSchemaAlterTableRemoveColumn)) { + return false; + } + + Deprecation::trigger( + 'doctrine/dbal', + 'https://github.com/doctrine/dbal/issues/5784', + 'Subscribing to %s events is deprecated.', + Events::onSchemaAlterTableRemoveColumn, + ); + + $eventArgs = new SchemaAlterTableRemoveColumnEventArgs($column, $diff, $this); + $this->_eventManager->dispatchEvent(Events::onSchemaAlterTableRemoveColumn, $eventArgs); + + $columnSql = array_merge($columnSql, $eventArgs->getSql()); + + return $eventArgs->isDefaultPrevented(); + } + + /** + * @param string[] $columnSql + * + * @return bool + */ + protected function onSchemaAlterTableChangeColumn(ColumnDiff $columnDiff, TableDiff $diff, &$columnSql) + { + if ($this->_eventManager === null) { + return false; + } + + if (! $this->_eventManager->hasListeners(Events::onSchemaAlterTableChangeColumn)) { + return false; + } + + Deprecation::trigger( + 'doctrine/dbal', + 'https://github.com/doctrine/dbal/issues/5784', + 'Subscribing to %s events is deprecated.', + Events::onSchemaAlterTableChangeColumn, + ); + + $eventArgs = new SchemaAlterTableChangeColumnEventArgs($columnDiff, $diff, $this); + $this->_eventManager->dispatchEvent(Events::onSchemaAlterTableChangeColumn, $eventArgs); + + $columnSql = array_merge($columnSql, $eventArgs->getSql()); + + return $eventArgs->isDefaultPrevented(); + } + + /** + * @param string $oldColumnName + * @param string[] $columnSql + * + * @return bool + */ + protected function onSchemaAlterTableRenameColumn($oldColumnName, Column $column, TableDiff $diff, &$columnSql) + { + if ($this->_eventManager === null) { + return false; + } + + if (! $this->_eventManager->hasListeners(Events::onSchemaAlterTableRenameColumn)) { + return false; + } + + Deprecation::trigger( + 'doctrine/dbal', + 'https://github.com/doctrine/dbal/issues/5784', + 'Subscribing to %s events is deprecated.', + Events::onSchemaAlterTableRenameColumn, + ); + + $eventArgs = new SchemaAlterTableRenameColumnEventArgs($oldColumnName, $column, $diff, $this); + $this->_eventManager->dispatchEvent(Events::onSchemaAlterTableRenameColumn, $eventArgs); + + $columnSql = array_merge($columnSql, $eventArgs->getSql()); + + return $eventArgs->isDefaultPrevented(); + } + + /** + * @param string[] $sql + * + * @return bool + */ + protected function onSchemaAlterTable(TableDiff $diff, &$sql) + { + if ($this->_eventManager === null) { + return false; + } + + if (! $this->_eventManager->hasListeners(Events::onSchemaAlterTable)) { + return false; + } + + Deprecation::trigger( + 'doctrine/dbal', + 'https://github.com/doctrine/dbal/issues/5784', + 'Subscribing to %s events is deprecated.', + Events::onSchemaAlterTable, + ); + + $eventArgs = new SchemaAlterTableEventArgs($diff, $this); + $this->_eventManager->dispatchEvent(Events::onSchemaAlterTable, $eventArgs); + + $sql = array_merge($sql, $eventArgs->getSql()); + + return $eventArgs->isDefaultPrevented(); + } + + /** @return string[] */ + protected function getPreAlterTableIndexForeignKeySQL(TableDiff $diff) + { + $tableNameSQL = ($diff->getOldTable() ?? $diff->getName($this))->getQuotedName($this); + + $sql = []; + if ($this->supportsForeignKeyConstraints()) { + foreach ($diff->getDroppedForeignKeys() as $foreignKey) { + if ($foreignKey instanceof ForeignKeyConstraint) { + $foreignKey = $foreignKey->getQuotedName($this); + } + + $sql[] = $this->getDropForeignKeySQL($foreignKey, $tableNameSQL); + } + + foreach ($diff->getModifiedForeignKeys() as $foreignKey) { + $sql[] = $this->getDropForeignKeySQL($foreignKey->getQuotedName($this), $tableNameSQL); + } + } + + foreach ($diff->getDroppedIndexes() as $index) { + $sql[] = $this->getDropIndexSQL($index->getQuotedName($this), $tableNameSQL); + } + + foreach ($diff->getModifiedIndexes() as $index) { + $sql[] = $this->getDropIndexSQL($index->getQuotedName($this), $tableNameSQL); + } + + return $sql; + } + + /** @return string[] */ + protected function getPostAlterTableIndexForeignKeySQL(TableDiff $diff) + { + $sql = []; + $newName = $diff->getNewName(); + + if ($newName !== false) { + $tableNameSQL = $newName->getQuotedName($this); + } else { + $tableNameSQL = ($diff->getOldTable() ?? $diff->getName($this))->getQuotedName($this); + } + + if ($this->supportsForeignKeyConstraints()) { + foreach ($diff->getAddedForeignKeys() as $foreignKey) { + $sql[] = $this->getCreateForeignKeySQL($foreignKey, $tableNameSQL); + } + + foreach ($diff->getModifiedForeignKeys() as $foreignKey) { + $sql[] = $this->getCreateForeignKeySQL($foreignKey, $tableNameSQL); + } + } + + foreach ($diff->getAddedIndexes() as $index) { + $sql[] = $this->getCreateIndexSQL($index, $tableNameSQL); + } + + foreach ($diff->getModifiedIndexes() as $index) { + $sql[] = $this->getCreateIndexSQL($index, $tableNameSQL); + } + + foreach ($diff->getRenamedIndexes() as $oldIndexName => $index) { + $oldIndexName = new Identifier($oldIndexName); + $sql = array_merge( + $sql, + $this->getRenameIndexSQL($oldIndexName->getQuotedName($this), $index, $tableNameSQL), + ); + } + + return $sql; + } + + /** + * Returns the SQL for renaming an index on a table. + * + * @param string $oldIndexName The name of the index to rename from. + * @param Index $index The definition of the index to rename to. + * @param string $tableName The table to rename the given index on. + * + * @return string[] The sequence of SQL statements for renaming the given index. + */ + protected function getRenameIndexSQL($oldIndexName, Index $index, $tableName) + { + return [ + $this->getDropIndexSQL($oldIndexName, $tableName), + $this->getCreateIndexSQL($index, $tableName), + ]; + } + + /** + * Gets declaration of a number of columns in bulk. + * + * @param mixed[][] $columns A multidimensional associative array. + * The first dimension determines the column name, while the second + * dimension is keyed with the name of the properties + * of the column being declared as array indexes. Currently, the types + * of supported column properties are as follows: + * + * length + * Integer value that determines the maximum length of the text + * column. If this argument is missing the column should be + * declared to have the longest length allowed by the DBMS. + * + * default + * Text value to be used as default for this column. + * + * notnull + * Boolean flag that indicates whether this column is constrained + * to not be set to null. + * charset + * Text value with the default CHARACTER SET for this column. + * collation + * Text value with the default COLLATION for this column. + * unique + * unique constraint + * + * @return string + */ + public function getColumnDeclarationListSQL(array $columns) + { + $declarations = []; + + foreach ($columns as $name => $column) { + $declarations[] = $this->getColumnDeclarationSQL($name, $column); + } + + return implode(', ', $declarations); + } + + /** + * Obtains DBMS specific SQL code portion needed to declare a generic type + * column to be used in statements like CREATE TABLE. + * + * @param string $name The name the column to be declared. + * @param mixed[] $column An associative array with the name of the properties + * of the column being declared as array indexes. Currently, the types + * of supported column properties are as follows: + * + * length + * Integer value that determines the maximum length of the text + * column. If this argument is missing the column should be + * declared to have the longest length allowed by the DBMS. + * + * default + * Text value to be used as default for this column. + * + * notnull + * Boolean flag that indicates whether this column is constrained + * to not be set to null. + * charset + * Text value with the default CHARACTER SET for this column. + * collation + * Text value with the default COLLATION for this column. + * unique + * unique constraint + * check + * column check constraint + * columnDefinition + * a string that defines the complete column + * + * @return string DBMS specific SQL code portion that should be used to declare the column. + * + * @throws Exception + */ + public function getColumnDeclarationSQL($name, array $column) + { + if (isset($column['columnDefinition'])) { + $declaration = $this->getCustomTypeDeclarationSQL($column); + } else { + $default = $this->getDefaultValueDeclarationSQL($column); + + $charset = ! empty($column['charset']) ? + ' ' . $this->getColumnCharsetDeclarationSQL($column['charset']) : ''; + + $collation = ! empty($column['collation']) ? + ' ' . $this->getColumnCollationDeclarationSQL($column['collation']) : ''; + + $notnull = ! empty($column['notnull']) ? ' NOT NULL' : ''; + + if (! empty($column['unique'])) { + Deprecation::trigger( + 'doctrine/dbal', + 'https://github.com/doctrine/dbal/pull/5656', + 'The usage of the "unique" column property is deprecated. Use unique constraints instead.', + ); + + $unique = ' ' . $this->getUniqueFieldDeclarationSQL(); + } else { + $unique = ''; + } + + if (! empty($column['check'])) { + Deprecation::trigger( + 'doctrine/dbal', + 'https://github.com/doctrine/dbal/pull/5656', + 'The usage of the "check" column property is deprecated.', + ); + + $check = ' ' . $column['check']; + } else { + $check = ''; + } + + $typeDecl = $column['type']->getSQLDeclaration($column, $this); + $declaration = $typeDecl . $charset . $default . $notnull . $unique . $check . $collation; + + if ($this->supportsInlineColumnComments() && isset($column['comment']) && $column['comment'] !== '') { + $declaration .= ' ' . $this->getInlineColumnCommentSQL($column['comment']); + } + } + + return $name . ' ' . $declaration; + } + + /** + * Returns the SQL snippet that declares a floating point column of arbitrary precision. + * + * @param mixed[] $column + * + * @return string + */ + public function getDecimalTypeDeclarationSQL(array $column) + { + if (empty($column['precision'])) { + if (! isset($column['precision'])) { + Deprecation::trigger( + 'doctrine/dbal', + 'https://github.com/doctrine/dbal/pull/5637', + 'Relying on the default decimal column precision is deprecated' + . ', specify the precision explicitly.', + ); + } + + $precision = 10; + } else { + $precision = $column['precision']; + } + + if (empty($column['scale'])) { + if (! isset($column['scale'])) { + Deprecation::trigger( + 'doctrine/dbal', + 'https://github.com/doctrine/dbal/pull/5637', + 'Relying on the default decimal column scale is deprecated' + . ', specify the scale explicitly.', + ); + } + + $scale = 0; + } else { + $scale = $column['scale']; + } + + return 'NUMERIC(' . $precision . ', ' . $scale . ')'; + } + + /** + * Obtains DBMS specific SQL code portion needed to set a default value + * declaration to be used in statements like CREATE TABLE. + * + * @param mixed[] $column The column definition array. + * + * @return string DBMS specific SQL code portion needed to set a default value. + */ + public function getDefaultValueDeclarationSQL($column) + { + if (! isset($column['default'])) { + return empty($column['notnull']) ? ' DEFAULT NULL' : ''; + } + + $default = $column['default']; + + if (! isset($column['type'])) { + return " DEFAULT '" . $default . "'"; + } + + $type = $column['type']; + + if ($type instanceof Types\PhpIntegerMappingType) { + return ' DEFAULT ' . $default; + } + + if ($type instanceof Types\PhpDateTimeMappingType && $default === $this->getCurrentTimestampSQL()) { + return ' DEFAULT ' . $this->getCurrentTimestampSQL(); + } + + if ($type instanceof Types\TimeType && $default === $this->getCurrentTimeSQL()) { + return ' DEFAULT ' . $this->getCurrentTimeSQL(); + } + + if ($type instanceof Types\DateType && $default === $this->getCurrentDateSQL()) { + return ' DEFAULT ' . $this->getCurrentDateSQL(); + } + + if ($type instanceof Types\BooleanType) { + return ' DEFAULT ' . $this->convertBooleans($default); + } + + return ' DEFAULT ' . $this->quoteStringLiteral($default); + } + + /** + * Obtains DBMS specific SQL code portion needed to set a CHECK constraint + * declaration to be used in statements like CREATE TABLE. + * + * @param string[]|mixed[][] $definition The check definition. + * + * @return string DBMS specific SQL code portion needed to set a CHECK constraint. + */ + public function getCheckDeclarationSQL(array $definition) + { + $constraints = []; + foreach ($definition as $column => $def) { + if (is_string($def)) { + $constraints[] = 'CHECK (' . $def . ')'; + } else { + if (isset($def['min'])) { + $constraints[] = 'CHECK (' . $column . ' >= ' . $def['min'] . ')'; + } + + if (isset($def['max'])) { + $constraints[] = 'CHECK (' . $column . ' <= ' . $def['max'] . ')'; + } + } + } + + return implode(', ', $constraints); + } + + /** + * Obtains DBMS specific SQL code portion needed to set a unique + * constraint declaration to be used in statements like CREATE TABLE. + * + * @param string $name The name of the unique constraint. + * @param UniqueConstraint $constraint The unique constraint definition. + * + * @return string DBMS specific SQL code portion needed to set a constraint. + * + * @throws InvalidArgumentException + */ + public function getUniqueConstraintDeclarationSQL($name, UniqueConstraint $constraint) + { + $columns = $constraint->getQuotedColumns($this); + $name = new Identifier($name); + + if (count($columns) === 0) { + throw new InvalidArgumentException("Incomplete definition. 'columns' required."); + } + + $constraintFlags = array_merge(['UNIQUE'], array_map('strtoupper', $constraint->getFlags())); + $constraintName = $name->getQuotedName($this); + $columnListNames = $this->getColumnsFieldDeclarationListSQL($columns); + + return sprintf('CONSTRAINT %s %s (%s)', $constraintName, implode(' ', $constraintFlags), $columnListNames); + } + + /** + * Obtains DBMS specific SQL code portion needed to set an index + * declaration to be used in statements like CREATE TABLE. + * + * @param string $name The name of the index. + * @param Index $index The index definition. + * + * @return string DBMS specific SQL code portion needed to set an index. + * + * @throws InvalidArgumentException + */ + public function getIndexDeclarationSQL($name, Index $index) + { + $columns = $index->getColumns(); + $name = new Identifier($name); + + if (count($columns) === 0) { + throw new InvalidArgumentException("Incomplete definition. 'columns' required."); + } + + return $this->getCreateIndexSQLFlags($index) . 'INDEX ' . $name->getQuotedName($this) + . ' (' . $this->getIndexFieldDeclarationListSQL($index) . ')' . $this->getPartialIndexSQL($index); + } + + /** + * Obtains SQL code portion needed to create a custom column, + * e.g. when a column has the "columnDefinition" keyword. + * Only "AUTOINCREMENT" and "PRIMARY KEY" are added if appropriate. + * + * @deprecated + * + * @param mixed[] $column + * + * @return string + */ + public function getCustomTypeDeclarationSQL(array $column) + { + Deprecation::triggerIfCalledFromOutside( + 'doctrine/dbal', + 'https://github.com/doctrine/dbal/pull/5527', + '%s is deprecated.', + __METHOD__, + ); + + return $column['columnDefinition']; + } + + /** + * Obtains DBMS specific SQL code portion needed to set an index + * declaration to be used in statements like CREATE TABLE. + * + * @deprecated + */ + public function getIndexFieldDeclarationListSQL(Index $index): string + { + Deprecation::triggerIfCalledFromOutside( + 'doctrine/dbal', + 'https://github.com/doctrine/dbal/pull/5527', + '%s is deprecated.', + __METHOD__, + ); + + return implode(', ', $index->getQuotedColumns($this)); + } + + /** + * Obtains DBMS specific SQL code portion needed to set an index + * declaration to be used in statements like CREATE TABLE. + * + * @deprecated + * + * @param mixed[] $columns + */ + public function getColumnsFieldDeclarationListSQL(array $columns): string + { + Deprecation::triggerIfCalledFromOutside( + 'doctrine/dbal', + 'https://github.com/doctrine/dbal/pull/5527', + '%s is deprecated.', + __METHOD__, + ); + + $ret = []; + + foreach ($columns as $column => $definition) { + if (is_array($definition)) { + $ret[] = $column; + } else { + $ret[] = $definition; + } + } + + return implode(', ', $ret); + } + + /** + * Returns the required SQL string that fits between CREATE ... TABLE + * to create the table as a temporary table. + * + * Should be overridden in driver classes to return the correct string for the + * specific database type. + * + * The default is to return the string "TEMPORARY" - this will result in a + * SQL error for any database that does not support temporary tables, or that + * requires a different SQL command from "CREATE TEMPORARY TABLE". + * + * @deprecated + * + * @return string The string required to be placed between "CREATE" and "TABLE" + * to generate a temporary table, if possible. + */ + public function getTemporaryTableSQL() + { + Deprecation::trigger( + 'doctrine/dbal', + 'https://github.com/doctrine/dbal/pull/4724', + 'AbstractPlatform::getTemporaryTableSQL() is deprecated.', + ); + + return 'TEMPORARY'; + } + + /** + * Some vendors require temporary table names to be qualified specially. + * + * @param string $tableName + * + * @return string + */ + public function getTemporaryTableName($tableName) + { + return $tableName; + } + + /** + * Obtain DBMS specific SQL code portion needed to set the FOREIGN KEY constraint + * of a column declaration to be used in statements like CREATE TABLE. + * + * @return string DBMS specific SQL code portion needed to set the FOREIGN KEY constraint + * of a column declaration. + */ + public function getForeignKeyDeclarationSQL(ForeignKeyConstraint $foreignKey) + { + $sql = $this->getForeignKeyBaseDeclarationSQL($foreignKey); + $sql .= $this->getAdvancedForeignKeyOptionsSQL($foreignKey); + + return $sql; + } + + /** + * Returns the FOREIGN KEY query section dealing with non-standard options + * as MATCH, INITIALLY DEFERRED, ON UPDATE, ... + * + * @param ForeignKeyConstraint $foreignKey The foreign key definition. + * + * @return string + */ + public function getAdvancedForeignKeyOptionsSQL(ForeignKeyConstraint $foreignKey) + { + $query = ''; + if ($foreignKey->hasOption('onUpdate')) { + $query .= ' ON UPDATE ' . $this->getForeignKeyReferentialActionSQL($foreignKey->getOption('onUpdate')); + } + + if ($foreignKey->hasOption('onDelete')) { + $query .= ' ON DELETE ' . $this->getForeignKeyReferentialActionSQL($foreignKey->getOption('onDelete')); + } + + return $query; + } + + /** + * Returns the given referential action in uppercase if valid, otherwise throws an exception. + * + * @param string $action The foreign key referential action. + * + * @return string + * + * @throws InvalidArgumentException If unknown referential action given. + */ + public function getForeignKeyReferentialActionSQL($action) + { + $upper = strtoupper($action); + switch ($upper) { + case 'CASCADE': + case 'SET NULL': + case 'NO ACTION': + case 'RESTRICT': + case 'SET DEFAULT': + return $upper; + + default: + throw new InvalidArgumentException('Invalid foreign key action: ' . $upper); + } + } + + /** + * Obtains DBMS specific SQL code portion needed to set the FOREIGN KEY constraint + * of a column declaration to be used in statements like CREATE TABLE. + * + * @return string + * + * @throws InvalidArgumentException + */ + public function getForeignKeyBaseDeclarationSQL(ForeignKeyConstraint $foreignKey) + { + $sql = ''; + if (strlen($foreignKey->getName()) > 0) { + $sql .= 'CONSTRAINT ' . $foreignKey->getQuotedName($this) . ' '; + } + + $sql .= 'FOREIGN KEY ('; + + if (count($foreignKey->getLocalColumns()) === 0) { + throw new InvalidArgumentException("Incomplete definition. 'local' required."); + } + + if (count($foreignKey->getForeignColumns()) === 0) { + throw new InvalidArgumentException("Incomplete definition. 'foreign' required."); + } + + if (strlen($foreignKey->getForeignTableName()) === 0) { + throw new InvalidArgumentException("Incomplete definition. 'foreignTable' required."); + } + + return $sql . implode(', ', $foreignKey->getQuotedLocalColumns($this)) + . ') REFERENCES ' + . $foreignKey->getQuotedForeignTableName($this) . ' (' + . implode(', ', $foreignKey->getQuotedForeignColumns($this)) . ')'; + } + + /** + * Obtains DBMS specific SQL code portion needed to set the UNIQUE constraint + * of a column declaration to be used in statements like CREATE TABLE. + * + * @deprecated Use UNIQUE in SQL instead. + * + * @return string DBMS specific SQL code portion needed to set the UNIQUE constraint + * of a column declaration. + */ + public function getUniqueFieldDeclarationSQL() + { + Deprecation::trigger( + 'doctrine/dbal', + 'https://github.com/doctrine/dbal/pull/4724', + 'AbstractPlatform::getUniqueFieldDeclarationSQL() is deprecated. Use UNIQUE in SQL instead.', + ); + + return 'UNIQUE'; + } + + /** + * Obtains DBMS specific SQL code portion needed to set the CHARACTER SET + * of a column declaration to be used in statements like CREATE TABLE. + * + * @param string $charset The name of the charset. + * + * @return string DBMS specific SQL code portion needed to set the CHARACTER SET + * of a column declaration. + */ + public function getColumnCharsetDeclarationSQL($charset) + { + return ''; + } + + /** + * Obtains DBMS specific SQL code portion needed to set the COLLATION + * of a column declaration to be used in statements like CREATE TABLE. + * + * @param string $collation The name of the collation. + * + * @return string DBMS specific SQL code portion needed to set the COLLATION + * of a column declaration. + */ + public function getColumnCollationDeclarationSQL($collation) + { + return $this->supportsColumnCollation() ? 'COLLATE ' . $this->quoteSingleIdentifier($collation) : ''; + } + + /** + * Whether the platform prefers identity columns (eg. autoincrement) for ID generation. + * Subclasses should override this method to return TRUE if they prefer identity columns. + * + * @deprecated + * + * @return bool + */ + public function prefersIdentityColumns() + { + Deprecation::trigger( + 'doctrine/dbal', + 'https://github.com/doctrine/dbal/pull/1519', + 'AbstractPlatform::prefersIdentityColumns() is deprecated.', + ); + + return false; + } + + /** + * Some platforms need the boolean values to be converted. + * + * The default conversion in this implementation converts to integers (false => 0, true => 1). + * + * Note: if the input is not a boolean the original input might be returned. + * + * There are two contexts when converting booleans: Literals and Prepared Statements. + * This method should handle the literal case + * + * @param mixed $item A boolean or an array of them. + * + * @return mixed A boolean database value or an array of them. + */ + public function convertBooleans($item) + { + if (is_array($item)) { + foreach ($item as $k => $value) { + if (! is_bool($value)) { + continue; + } + + $item[$k] = (int) $value; + } + } elseif (is_bool($item)) { + $item = (int) $item; + } + + return $item; + } + + /** + * Some platforms have boolean literals that needs to be correctly converted + * + * The default conversion tries to convert value into bool "(bool)$item" + * + * @param T $item + * + * @return (T is null ? null : bool) + * + * @template T + */ + public function convertFromBoolean($item) + { + return $item === null ? null : (bool) $item; + } + + /** + * This method should handle the prepared statements case. When there is no + * distinction, it's OK to use the same method. + * + * Note: if the input is not a boolean the original input might be returned. + * + * @param mixed $item A boolean or an array of them. + * + * @return mixed A boolean database value or an array of them. + */ + public function convertBooleansToDatabaseValue($item) + { + return $this->convertBooleans($item); + } + + /** + * Returns the SQL specific for the platform to get the current date. + * + * @return string + */ + public function getCurrentDateSQL() + { + return 'CURRENT_DATE'; + } + + /** + * Returns the SQL specific for the platform to get the current time. + * + * @return string + */ + public function getCurrentTimeSQL() + { + return 'CURRENT_TIME'; + } + + /** + * Returns the SQL specific for the platform to get the current timestamp + * + * @return string + */ + public function getCurrentTimestampSQL() + { + return 'CURRENT_TIMESTAMP'; + } + + /** + * Returns the SQL for a given transaction isolation level Connection constant. + * + * @param int $level + * + * @return string + * + * @throws InvalidArgumentException + */ + protected function _getTransactionIsolationLevelSQL($level) + { + switch ($level) { + case TransactionIsolationLevel::READ_UNCOMMITTED: + return 'READ UNCOMMITTED'; + + case TransactionIsolationLevel::READ_COMMITTED: + return 'READ COMMITTED'; + + case TransactionIsolationLevel::REPEATABLE_READ: + return 'REPEATABLE READ'; + + case TransactionIsolationLevel::SERIALIZABLE: + return 'SERIALIZABLE'; + + default: + throw new InvalidArgumentException('Invalid isolation level:' . $level); + } + } + + /** + * @internal The method should be only used from within the {@see AbstractSchemaManager} class hierarchy. + * + * @return string + * + * @throws Exception If not supported on this platform. + */ + public function getListDatabasesSQL() + { + throw Exception::notSupported(__METHOD__); + } + + /** + * Returns the SQL statement for retrieving the namespaces defined in the database. + * + * @deprecated Use {@see AbstractSchemaManager::listSchemaNames()} instead. + * + * @return string + * + * @throws Exception If not supported on this platform. + */ + public function getListNamespacesSQL() + { + Deprecation::triggerIfCalledFromOutside( + 'doctrine/dbal', + 'https://github.com/doctrine/dbal/issues/4503', + 'AbstractPlatform::getListNamespacesSQL() is deprecated,' + . ' use AbstractSchemaManager::listSchemaNames() instead.', + ); + + throw Exception::notSupported(__METHOD__); + } + + /** + * @internal The method should be only used from within the {@see AbstractSchemaManager} class hierarchy. + * + * @param string $database + * + * @return string + * + * @throws Exception If not supported on this platform. + */ + public function getListSequencesSQL($database) + { + throw Exception::notSupported(__METHOD__); + } + + /** + * @deprecated + * + * @param string $table + * + * @return string + * + * @throws Exception If not supported on this platform. + */ + public function getListTableConstraintsSQL($table) + { + throw Exception::notSupported(__METHOD__); + } + + /** + * @deprecated The SQL used for schema introspection is an implementation detail and should not be relied upon. + * + * @param string $table + * @param string $database + * + * @return string + * + * @throws Exception If not supported on this platform. + */ + public function getListTableColumnsSQL($table, $database = null) + { + throw Exception::notSupported(__METHOD__); + } + + /** + * @deprecated The SQL used for schema introspection is an implementation detail and should not be relied upon. + * + * @return string + * + * @throws Exception If not supported on this platform. + */ + public function getListTablesSQL() + { + throw Exception::notSupported(__METHOD__); + } + + /** + * @deprecated + * + * @return string + * + * @throws Exception If not supported on this platform. + */ + public function getListUsersSQL() + { + Deprecation::trigger( + 'doctrine/dbal', + 'https://github.com/doctrine/dbal/pull/4724', + 'AbstractPlatform::getListUsersSQL() is deprecated.', + ); + + throw Exception::notSupported(__METHOD__); + } + + /** + * Returns the SQL to list all views of a database or user. + * + * @internal The method should be only used from within the {@see AbstractSchemaManager} class hierarchy. + * + * @param string $database + * + * @return string + * + * @throws Exception If not supported on this platform. + */ + public function getListViewsSQL($database) + { + throw Exception::notSupported(__METHOD__); + } + + /** + * @deprecated The SQL used for schema introspection is an implementation detail and should not be relied upon. + * + * Returns the list of indexes for the current database. + * + * The current database parameter is optional but will always be passed + * when using the SchemaManager API and is the database the given table is in. + * + * Attention: Some platforms only support currentDatabase when they + * are connected with that database. Cross-database information schema + * requests may be impossible. + * + * @param string $table + * @param string $database + * + * @return string + * + * @throws Exception If not supported on this platform. + */ + public function getListTableIndexesSQL($table, $database = null) + { + throw Exception::notSupported(__METHOD__); + } + + /** + * @deprecated The SQL used for schema introspection is an implementation detail and should not be relied upon. + * + * @param string $table + * + * @return string + * + * @throws Exception If not supported on this platform. + */ + public function getListTableForeignKeysSQL($table) + { + throw Exception::notSupported(__METHOD__); + } + + /** + * @param string $name + * @param string $sql + * + * @return string + */ + public function getCreateViewSQL($name, $sql) + { + return 'CREATE VIEW ' . $name . ' AS ' . $sql; + } + + /** + * @param string $name + * + * @return string + */ + public function getDropViewSQL($name) + { + return 'DROP VIEW ' . $name; + } + + /** + * @param string $sequence + * + * @return string + * + * @throws Exception If not supported on this platform. + */ + public function getSequenceNextValSQL($sequence) + { + throw Exception::notSupported(__METHOD__); + } + + /** + * Returns the SQL to create a new database. + * + * @param string $name The name of the database that should be created. + * + * @return string + * + * @throws Exception If not supported on this platform. + */ + public function getCreateDatabaseSQL($name) + { + if (! $this->supportsCreateDropDatabase()) { + throw Exception::notSupported(__METHOD__); + } + + return 'CREATE DATABASE ' . $name; + } + + /** + * Returns the SQL snippet to drop an existing database. + * + * @param string $name The name of the database that should be dropped. + * + * @return string + */ + public function getDropDatabaseSQL($name) + { + if (! $this->supportsCreateDropDatabase()) { + throw Exception::notSupported(__METHOD__); + } + + return 'DROP DATABASE ' . $name; + } + + /** + * Returns the SQL to set the transaction isolation level. + * + * @param int $level + * + * @return string + * + * @throws Exception If not supported on this platform. + */ + public function getSetTransactionIsolationSQL($level) + { + throw Exception::notSupported(__METHOD__); + } + + /** + * Obtains DBMS specific SQL to be used to create datetime columns in + * statements like CREATE TABLE. + * + * @param mixed[] $column + * + * @return string + * + * @throws Exception If not supported on this platform. + */ + public function getDateTimeTypeDeclarationSQL(array $column) + { + throw Exception::notSupported(__METHOD__); + } + + /** + * Obtains DBMS specific SQL to be used to create datetime with timezone offset columns. + * + * @param mixed[] $column + * + * @return string + */ + public function getDateTimeTzTypeDeclarationSQL(array $column) + { + return $this->getDateTimeTypeDeclarationSQL($column); + } + + /** + * Obtains DBMS specific SQL to be used to create date columns in statements + * like CREATE TABLE. + * + * @param mixed[] $column + * + * @return string + * + * @throws Exception If not supported on this platform. + */ + public function getDateTypeDeclarationSQL(array $column) + { + throw Exception::notSupported(__METHOD__); + } + + /** + * Obtains DBMS specific SQL to be used to create time columns in statements + * like CREATE TABLE. + * + * @param mixed[] $column + * + * @return string + * + * @throws Exception If not supported on this platform. + */ + public function getTimeTypeDeclarationSQL(array $column) + { + throw Exception::notSupported(__METHOD__); + } + + /** + * @param mixed[] $column + * + * @return string + */ + public function getFloatDeclarationSQL(array $column) + { + return 'DOUBLE PRECISION'; + } + + /** + * Gets the default transaction isolation level of the platform. + * + * @see TransactionIsolationLevel + * + * @return TransactionIsolationLevel::* The default isolation level. + */ + public function getDefaultTransactionIsolationLevel() + { + return TransactionIsolationLevel::READ_COMMITTED; + } + + /* supports*() methods */ + + /** + * Whether the platform supports sequences. + * + * @return bool + */ + public function supportsSequences() + { + return false; + } + + /** + * Whether the platform supports identity columns. + * + * Identity columns are columns that receive an auto-generated value from the + * database on insert of a row. + * + * @return bool + */ + public function supportsIdentityColumns() + { + return false; + } + + /** + * Whether the platform emulates identity columns through sequences. + * + * Some platforms that do not support identity columns natively + * but support sequences can emulate identity columns by using + * sequences. + * + * @deprecated + * + * @return bool + */ + public function usesSequenceEmulatedIdentityColumns() + { + Deprecation::trigger( + 'doctrine/dbal', + 'https://github.com/doctrine/dbal/pull/5513', + '%s is deprecated.', + __METHOD__, + ); + + return false; + } + + /** + * Returns the name of the sequence for a particular identity column in a particular table. + * + * @deprecated + * + * @see usesSequenceEmulatedIdentityColumns + * + * @param string $tableName The name of the table to return the sequence name for. + * @param string $columnName The name of the identity column in the table to return the sequence name for. + * + * @return string + * + * @throws Exception If not supported on this platform. + */ + public function getIdentitySequenceName($tableName, $columnName) + { + throw Exception::notSupported(__METHOD__); + } + + /** + * Whether the platform supports indexes. + * + * @deprecated + * + * @return bool + */ + public function supportsIndexes() + { + Deprecation::trigger( + 'doctrine/dbal', + 'https://github.com/doctrine/dbal/pull/4724', + 'AbstractPlatform::supportsIndexes() is deprecated.', + ); + + return true; + } + + /** + * Whether the platform supports partial indexes. + * + * @return bool + */ + public function supportsPartialIndexes() + { + return false; + } + + /** + * Whether the platform supports indexes with column length definitions. + */ + public function supportsColumnLengthIndexes(): bool + { + return false; + } + + /** + * Whether the platform supports altering tables. + * + * @deprecated All platforms must implement altering tables. + * + * @return bool + */ + public function supportsAlterTable() + { + Deprecation::trigger( + 'doctrine/dbal', + 'https://github.com/doctrine/dbal/pull/4724', + 'AbstractPlatform::supportsAlterTable() is deprecated. All platforms must implement altering tables.', + ); + + return true; + } + + /** + * Whether the platform supports transactions. + * + * @deprecated + * + * @return bool + */ + public function supportsTransactions() + { + Deprecation::trigger( + 'doctrine/dbal', + 'https://github.com/doctrine/dbal/pull/4724', + 'AbstractPlatform::supportsTransactions() is deprecated.', + ); + + return true; + } + + /** + * Whether the platform supports savepoints. + * + * @return bool + */ + public function supportsSavepoints() + { + return true; + } + + /** + * Whether the platform supports releasing savepoints. + * + * @return bool + */ + public function supportsReleaseSavepoints() + { + return $this->supportsSavepoints(); + } + + /** + * Whether the platform supports primary key constraints. + * + * @deprecated + * + * @return bool + */ + public function supportsPrimaryConstraints() + { + Deprecation::trigger( + 'doctrine/dbal', + 'https://github.com/doctrine/dbal/pull/4724', + 'AbstractPlatform::supportsPrimaryConstraints() is deprecated.', + ); + + return true; + } + + /** + * Whether the platform supports foreign key constraints. + * + * @deprecated All platforms should support foreign key constraints. + * + * @return bool + */ + public function supportsForeignKeyConstraints() + { + Deprecation::triggerIfCalledFromOutside( + 'doctrine/dbal', + 'https://github.com/doctrine/dbal/pull/5409', + 'AbstractPlatform::supportsForeignKeyConstraints() is deprecated.', + ); + + return true; + } + + /** + * Whether the platform supports database schemas. + * + * @return bool + */ + public function supportsSchemas() + { + return false; + } + + /** + * Whether this platform can emulate schemas. + * + * @deprecated + * + * Platforms that either support or emulate schemas don't automatically + * filter a schema for the namespaced elements in {@see AbstractManager::introspectSchema()}. + * + * @return bool + */ + public function canEmulateSchemas() + { + Deprecation::trigger( + 'doctrine/dbal', + 'https://github.com/doctrine/dbal/pull/4805', + 'AbstractPlatform::canEmulateSchemas() is deprecated.', + ); + + return false; + } + + /** + * Returns the default schema name. + * + * @deprecated + * + * @return string + * + * @throws Exception If not supported on this platform. + */ + public function getDefaultSchemaName() + { + throw Exception::notSupported(__METHOD__); + } + + /** + * Whether this platform supports create database. + * + * Some databases don't allow to create and drop databases at all or only with certain tools. + * + * @deprecated + * + * @return bool + */ + public function supportsCreateDropDatabase() + { + Deprecation::triggerIfCalledFromOutside( + 'doctrine/dbal', + 'https://github.com/doctrine/dbal/pull/5513', + '%s is deprecated.', + __METHOD__, + ); + + return true; + } + + /** + * Whether the platform supports getting the affected rows of a recent update/delete type query. + * + * @deprecated + * + * @return bool + */ + public function supportsGettingAffectedRows() + { + Deprecation::trigger( + 'doctrine/dbal', + 'https://github.com/doctrine/dbal/pull/4724', + 'AbstractPlatform::supportsGettingAffectedRows() is deprecated.', + ); + + return true; + } + + /** + * Whether this platform support to add inline column comments as postfix. + * + * @return bool + */ + public function supportsInlineColumnComments() + { + return false; + } + + /** + * Whether this platform support the proprietary syntax "COMMENT ON asset". + * + * @return bool + */ + public function supportsCommentOnStatement() + { + return false; + } + + /** + * Does this platform have native guid type. + * + * @deprecated + * + * @return bool + */ + public function hasNativeGuidType() + { + Deprecation::triggerIfCalledFromOutside( + 'doctrine/dbal', + 'https://github.com/doctrine/dbal/pull/5509', + '%s is deprecated.', + __METHOD__, + ); + + return false; + } + + /** + * Does this platform have native JSON type. + * + * @deprecated + * + * @return bool + */ + public function hasNativeJsonType() + { + Deprecation::triggerIfCalledFromOutside( + 'doctrine/dbal', + 'https://github.com/doctrine/dbal/pull/5509', + '%s is deprecated.', + __METHOD__, + ); + + return false; + } + + /** + * Whether this platform supports views. + * + * @deprecated All platforms must implement support for views. + * + * @return bool + */ + public function supportsViews() + { + Deprecation::trigger( + 'doctrine/dbal', + 'https://github.com/doctrine/dbal/pull/4724', + 'AbstractPlatform::supportsViews() is deprecated. All platforms must implement support for views.', + ); + + return true; + } + + /** + * Does this platform support column collation? + * + * @return bool + */ + public function supportsColumnCollation() + { + return false; + } + + /** + * Gets the format string, as accepted by the date() function, that describes + * the format of a stored datetime value of this platform. + * + * @return string The format string. + */ + public function getDateTimeFormatString() + { + return 'Y-m-d H:i:s'; + } + + /** + * Gets the format string, as accepted by the date() function, that describes + * the format of a stored datetime with timezone value of this platform. + * + * @return string The format string. + */ + public function getDateTimeTzFormatString() + { + return 'Y-m-d H:i:s'; + } + + /** + * Gets the format string, as accepted by the date() function, that describes + * the format of a stored date value of this platform. + * + * @return string The format string. + */ + public function getDateFormatString() + { + return 'Y-m-d'; + } + + /** + * Gets the format string, as accepted by the date() function, that describes + * the format of a stored time value of this platform. + * + * @return string The format string. + */ + public function getTimeFormatString() + { + return 'H:i:s'; + } + + /** + * Adds an driver-specific LIMIT clause to the query. + * + * @param string $query + * @param int|null $limit + * @param int $offset + * + * @throws Exception + */ + final public function modifyLimitQuery($query, $limit, $offset = 0): string + { + if ($offset < 0) { + throw new Exception(sprintf( + 'Offset must be a positive integer or zero, %d given', + $offset, + )); + } + + if ($offset > 0 && ! $this->supportsLimitOffset()) { + throw new Exception(sprintf( + 'Platform %s does not support offset values in limit queries.', + $this->getName(), + )); + } + + if ($limit !== null) { + $limit = (int) $limit; + } + + return $this->doModifyLimitQuery($query, $limit, (int) $offset); + } + + /** + * Adds an platform-specific LIMIT clause to the query. + * + * @param string $query + * @param int|null $limit + * @param int $offset + * + * @return string + */ + protected function doModifyLimitQuery($query, $limit, $offset) + { + if ($limit !== null) { + $query .= sprintf(' LIMIT %d', $limit); + } + + if ($offset > 0) { + $query .= sprintf(' OFFSET %d', $offset); + } + + return $query; + } + + /** + * Whether the database platform support offsets in modify limit clauses. + * + * @deprecated All platforms must implement support for offsets in modify limit clauses. + * + * @return bool + */ + public function supportsLimitOffset() + { + Deprecation::triggerIfCalledFromOutside( + 'doctrine/dbal', + 'https://github.com/doctrine/dbal/pull/4724', + 'AbstractPlatform::supportsViews() is deprecated.' + . ' All platforms must implement support for offsets in modify limit clauses.', + ); + + return true; + } + + /** + * Maximum length of any given database identifier, like tables or column names. + * + * @return int + */ + public function getMaxIdentifierLength() + { + return 63; + } + + /** + * Returns the insert SQL for an empty insert statement. + * + * @param string $quotedTableName + * @param string $quotedIdentifierColumnName + * + * @return string + */ + public function getEmptyIdentityInsertSQL($quotedTableName, $quotedIdentifierColumnName) + { + return 'INSERT INTO ' . $quotedTableName . ' (' . $quotedIdentifierColumnName . ') VALUES (null)'; + } + + /** + * Generates a Truncate Table SQL statement for a given table. + * + * Cascade is not supported on many platforms but would optionally cascade the truncate by + * following the foreign keys. + * + * @param string $tableName + * @param bool $cascade + * + * @return string + */ + public function getTruncateTableSQL($tableName, $cascade = false) + { + $tableIdentifier = new Identifier($tableName); + + return 'TRUNCATE ' . $tableIdentifier->getQuotedName($this); + } + + /** + * This is for test reasons, many vendors have special requirements for dummy statements. + * + * @return string + */ + public function getDummySelectSQL() + { + $expression = func_num_args() > 0 ? func_get_arg(0) : '1'; + + return sprintf('SELECT %s', $expression); + } + + /** + * Returns the SQL to create a new savepoint. + * + * @param string $savepoint + * + * @return string + */ + public function createSavePoint($savepoint) + { + return 'SAVEPOINT ' . $savepoint; + } + + /** + * Returns the SQL to release a savepoint. + * + * @param string $savepoint + * + * @return string + */ + public function releaseSavePoint($savepoint) + { + return 'RELEASE SAVEPOINT ' . $savepoint; + } + + /** + * Returns the SQL to rollback a savepoint. + * + * @param string $savepoint + * + * @return string + */ + public function rollbackSavePoint($savepoint) + { + return 'ROLLBACK TO SAVEPOINT ' . $savepoint; + } + + /** + * Returns the keyword list instance of this platform. + * + * @throws Exception If no keyword list is specified. + */ + final public function getReservedKeywordsList(): KeywordList + { + // Store the instance so it doesn't need to be generated on every request. + return $this->_keywords ??= $this->createReservedKeywordsList(); + } + + /** + * Creates an instance of the reserved keyword list of this platform. + * + * This method will become @abstract in DBAL 4.0.0. + * + * @throws Exception + */ + protected function createReservedKeywordsList(): KeywordList + { + $class = $this->getReservedKeywordsClass(); + $keywords = new $class(); + if (! $keywords instanceof KeywordList) { + throw Exception::notSupported(__METHOD__); + } + + return $keywords; + } + + /** + * Returns the class name of the reserved keywords list. + * + * @deprecated Implement {@see createReservedKeywordsList()} instead. + * + * @return string + * @phpstan-return class-string + * + * @throws Exception If not supported on this platform. + */ + protected function getReservedKeywordsClass() + { + Deprecation::triggerIfCalledFromOutside( + 'doctrine/dbal', + 'https://github.com/doctrine/dbal/issues/4510', + 'AbstractPlatform::getReservedKeywordsClass() is deprecated,' + . ' use AbstractPlatform::createReservedKeywordsList() instead.', + ); + + throw Exception::notSupported(__METHOD__); + } + + /** + * Quotes a literal string. + * This method is NOT meant to fix SQL injections! + * It is only meant to escape this platform's string literal + * quote character inside the given literal string. + * + * @param string $str The literal string to be quoted. + * + * @return string The quoted literal string. + */ + public function quoteStringLiteral($str) + { + $c = $this->getStringLiteralQuoteCharacter(); + + return $c . str_replace($c, $c . $c, $str) . $c; + } + + /** + * Gets the character used for string literal quoting. + * + * @deprecated Use {@see quoteStringLiteral()} to quote string literals instead. + * + * @return string + */ + public function getStringLiteralQuoteCharacter() + { + Deprecation::triggerIfCalledFromOutside( + 'doctrine/dbal', + 'https://github.com/doctrine/dbal/pull/5388', + 'AbstractPlatform::getStringLiteralQuoteCharacter() is deprecated.' + . ' Use quoteStringLiteral() instead.', + ); + + return "'"; + } + + /** + * Escapes metacharacters in a string intended to be used with a LIKE + * operator. + * + * @param string $inputString a literal, unquoted string + * @param string $escapeChar should be reused by the caller in the LIKE + * expression. + */ + final public function escapeStringForLike(string $inputString, string $escapeChar): string + { + return preg_replace( + '~([' . preg_quote($this->getLikeWildcardCharacters() . $escapeChar, '~') . '])~u', + addcslashes($escapeChar, '\\') . '$1', + $inputString, + ); + } + + /** + * @return array An associative array with the name of the properties + * of the column being declared as array indexes. + */ + private function columnToArray(Column $column): array + { + $name = $column->getQuotedName($this); + + return array_merge($column->toArray(), [ + 'name' => $name, + 'version' => $column->hasPlatformOption('version') ? $column->getPlatformOption('version') : false, + 'comment' => $this->getColumnComment($column), + ]); + } + + /** @internal */ + public function createSQLParser(): Parser + { + return new Parser(false); + } + + protected function getLikeWildcardCharacters(): string + { + return '%_'; + } + + /** + * Compares the definitions of the given columns in the context of this platform. + * + * @throws Exception + */ + public function columnsEqual(Column $column1, Column $column2): bool + { + $column1Array = $this->columnToArray($column1); + $column2Array = $this->columnToArray($column2); + + // ignore explicit columnDefinition since it's not set on the Column generated by the SchemaManager + unset($column1Array['columnDefinition']); + unset($column2Array['columnDefinition']); + + if ( + $this->getColumnDeclarationSQL('', $column1Array) + !== $this->getColumnDeclarationSQL('', $column2Array) + ) { + return false; + } + + if (! $this->columnDeclarationsMatch($column1, $column2)) { + return false; + } + + // If the platform supports inline comments, all comparison is already done above + if ($this->supportsInlineColumnComments()) { + return true; + } + + if ($column1->getComment() !== $column2->getComment()) { + return false; + } + + // If disableTypeComments is true, we do not need to check types, all comparison is already done above + if ($this->disableTypeComments) { + return true; + } + + return $column1->getType() === $column2->getType(); + } + + /** + * Whether the database data type matches that expected for the doctrine type for the given colunms. + */ + private function columnDeclarationsMatch(Column $column1, Column $column2): bool + { + return ! ( + $column1->hasPlatformOption('declarationMismatch') || + $column2->hasPlatformOption('declarationMismatch') + ); + } + + /** + * Creates the schema manager that can be used to inspect and change the underlying + * database schema according to the dialect of the platform. + * + * @throws Exception + * + * @abstract + */ + public function createSchemaManager(Connection $connection): AbstractSchemaManager + { + throw Exception::notSupported(__METHOD__); + } +} diff --git a/3rdparty/doctrine/dbal/src/Platforms/DB2111Platform.php b/3rdparty/doctrine/dbal/src/Platforms/DB2111Platform.php new file mode 100644 index 00000000..40ab42f6 --- /dev/null +++ b/3rdparty/doctrine/dbal/src/Platforms/DB2111Platform.php @@ -0,0 +1,40 @@ + 0) { + $query .= sprintf(' OFFSET %u ROWS', $offset); + } + + if ($limit !== null) { + if ($limit < 0) { + throw new Exception(sprintf('Limit must be a positive integer or zero, %d given', $limit)); + } + + $query .= sprintf(' FETCH %s %u ROWS ONLY', $offset === 0 ? 'FIRST' : 'NEXT', $limit); + } + + return $query; + } +} diff --git a/3rdparty/doctrine/dbal/src/Platforms/DB2Platform.php b/3rdparty/doctrine/dbal/src/Platforms/DB2Platform.php new file mode 100644 index 00000000..69234f41 --- /dev/null +++ b/3rdparty/doctrine/dbal/src/Platforms/DB2Platform.php @@ -0,0 +1,1053 @@ +getCharMaxLength(); + } + + return parent::getVarcharTypeDeclarationSQL($column); + } + + /** + * {@inheritDoc} + */ + public function getBlobTypeDeclarationSQL(array $column) + { + // todo blob(n) with $column['length']; + return 'BLOB(1M)'; + } + + /** + * {@inheritDoc} + */ + protected function initializeDoctrineTypeMappings() + { + $this->doctrineTypeMapping = [ + 'bigint' => Types::BIGINT, + 'binary' => Types::BINARY, + 'blob' => Types::BLOB, + 'character' => Types::STRING, + 'clob' => Types::TEXT, + 'date' => Types::DATE_MUTABLE, + 'decimal' => Types::DECIMAL, + 'double' => Types::FLOAT, + 'integer' => Types::INTEGER, + 'real' => Types::FLOAT, + 'smallint' => Types::SMALLINT, + 'time' => Types::TIME_MUTABLE, + 'timestamp' => Types::DATETIME_MUTABLE, + 'varbinary' => Types::BINARY, + 'varchar' => Types::STRING, + ]; + } + + /** + * {@inheritDoc} + */ + public function isCommentedDoctrineType(Type $doctrineType) + { + Deprecation::trigger( + 'doctrine/dbal', + 'https://github.com/doctrine/dbal/pull/5058', + '%s() is deprecated and will be removed in Doctrine DBAL 4.0. Use Type::requiresSQLCommentHint() instead.', + __METHOD__, + ); + + if ($doctrineType->getName() === Types::BOOLEAN) { + // We require a commented boolean type in order to distinguish between boolean and smallint + // as both (have to) map to the same native type. + return true; + } + + return parent::isCommentedDoctrineType($doctrineType); + } + + /** + * {@inheritDoc} + */ + protected function getVarcharTypeDeclarationSQLSnippet($length, $fixed/*, $lengthOmitted = false*/) + { + if ($length <= 0 || (func_num_args() > 2 && func_get_arg(2))) { + Deprecation::trigger( + 'doctrine/dbal', + 'https://github.com/doctrine/dbal/issues/3263', + 'Relying on the default string column length on IBM DB2 is deprecated' + . ', specify the length explicitly.', + ); + } + + return $fixed ? ($length > 0 ? 'CHAR(' . $length . ')' : 'CHAR(254)') + : ($length > 0 ? 'VARCHAR(' . $length . ')' : 'VARCHAR(255)'); + } + + /** + * {@inheritDoc} + */ + protected function getBinaryTypeDeclarationSQLSnippet($length, $fixed/*, $lengthOmitted = false*/) + { + if ($length <= 0 || (func_num_args() > 2 && func_get_arg(2))) { + Deprecation::trigger( + 'doctrine/dbal', + 'https://github.com/doctrine/dbal/issues/3263', + 'Relying on the default binary column length on IBM DB2 is deprecated' + . ', specify the length explicitly.', + ); + } + + return $this->getVarcharTypeDeclarationSQLSnippet($length, $fixed) . ' FOR BIT DATA'; + } + + /** + * {@inheritDoc} + */ + public function getClobTypeDeclarationSQL(array $column) + { + // todo clob(n) with $column['length']; + return 'CLOB(1M)'; + } + + /** + * {@inheritDoc} + */ + public function getName() + { + Deprecation::triggerIfCalledFromOutside( + 'doctrine/dbal', + 'https://github.com/doctrine/dbal/issues/4749', + '%s() is deprecated. Identify platforms by their class.', + __METHOD__, + ); + + return 'db2'; + } + + /** + * {@inheritDoc} + */ + public function getBooleanTypeDeclarationSQL(array $column) + { + return 'SMALLINT'; + } + + /** + * {@inheritDoc} + */ + public function getIntegerTypeDeclarationSQL(array $column) + { + return 'INTEGER' . $this->_getCommonIntegerTypeDeclarationSQL($column); + } + + /** + * {@inheritDoc} + */ + public function getBigIntTypeDeclarationSQL(array $column) + { + return 'BIGINT' . $this->_getCommonIntegerTypeDeclarationSQL($column); + } + + /** + * {@inheritDoc} + */ + public function getSmallIntTypeDeclarationSQL(array $column) + { + return 'SMALLINT' . $this->_getCommonIntegerTypeDeclarationSQL($column); + } + + /** + * {@inheritDoc} + */ + protected function _getCommonIntegerTypeDeclarationSQL(array $column) + { + $autoinc = ''; + if (! empty($column['autoincrement'])) { + $autoinc = ' GENERATED BY DEFAULT AS IDENTITY'; + } + + return $autoinc; + } + + /** + * {@inheritDoc} + */ + public function getBitAndComparisonExpression($value1, $value2) + { + return 'BITAND(' . $value1 . ', ' . $value2 . ')'; + } + + /** + * {@inheritDoc} + */ + public function getBitOrComparisonExpression($value1, $value2) + { + return 'BITOR(' . $value1 . ', ' . $value2 . ')'; + } + + /** + * {@inheritDoc} + */ + protected function getDateArithmeticIntervalExpression($date, $operator, $interval, $unit) + { + switch ($unit) { + case DateIntervalUnit::WEEK: + $interval = $this->multiplyInterval((string) $interval, 7); + $unit = DateIntervalUnit::DAY; + break; + + case DateIntervalUnit::QUARTER: + $interval = $this->multiplyInterval((string) $interval, 3); + $unit = DateIntervalUnit::MONTH; + break; + } + + return $date . ' ' . $operator . ' ' . $interval . ' ' . $unit; + } + + /** + * {@inheritDoc} + */ + public function getDateDiffExpression($date1, $date2) + { + return 'DAYS(' . $date1 . ') - DAYS(' . $date2 . ')'; + } + + /** + * {@inheritDoc} + */ + public function getDateTimeTypeDeclarationSQL(array $column) + { + if (isset($column['version']) && $column['version'] === true) { + return 'TIMESTAMP(0) WITH DEFAULT'; + } + + return 'TIMESTAMP(0)'; + } + + /** + * {@inheritDoc} + */ + public function getDateTypeDeclarationSQL(array $column) + { + return 'DATE'; + } + + /** + * {@inheritDoc} + */ + public function getTimeTypeDeclarationSQL(array $column) + { + return 'TIME'; + } + + /** + * {@inheritDoc} + */ + public function getTruncateTableSQL($tableName, $cascade = false) + { + $tableIdentifier = new Identifier($tableName); + + return 'TRUNCATE ' . $tableIdentifier->getQuotedName($this) . ' IMMEDIATE'; + } + + /** + * @deprecated The SQL used for schema introspection is an implementation detail and should not be relied upon. + * + * This code fragment is originally from the Zend_Db_Adapter_Db2 class, but has been edited. + * + * @param string $table + * @param string $database + * + * @return string + */ + public function getListTableColumnsSQL($table, $database = null) + { + $table = $this->quoteStringLiteral($table); + + // We do the funky subquery and join syscat.columns.default this crazy way because + // as of db2 v10, the column is CLOB(64k) and the distinct operator won't allow a CLOB, + // it wants shorter stuff like a varchar. + return " + SELECT + cols.default, + subq.* + FROM ( + SELECT DISTINCT + c.tabschema, + c.tabname, + c.colname, + c.colno, + c.typename, + c.codepage, + c.nulls, + c.length, + c.scale, + c.identity, + tc.type AS tabconsttype, + c.remarks AS comment, + k.colseq, + CASE + WHEN c.generated = '" . self::SYSCAT_COLUMNS_GENERATED_DEFAULT . "' THEN 1 + ELSE 0 + END AS autoincrement + FROM syscat.columns c + LEFT JOIN (syscat.keycoluse k JOIN syscat.tabconst tc + ON (k.tabschema = tc.tabschema + AND k.tabname = tc.tabname + AND tc.type = '" . self::SYSCAT_TABCONST_TYPE_PRIMARY_KEY . "')) + ON (c.tabschema = k.tabschema + AND c.tabname = k.tabname + AND c.colname = k.colname) + WHERE UPPER(c.tabname) = UPPER(" . $table . ') + ORDER BY c.colno + ) subq + JOIN syscat.columns cols + ON subq.tabschema = cols.tabschema + AND subq.tabname = cols.tabname + AND subq.colno = cols.colno + ORDER BY subq.colno + '; + } + + /** + * @deprecated The SQL used for schema introspection is an implementation detail and should not be relied upon. + * + * {@inheritDoc} + */ + public function getListTablesSQL() + { + return "SELECT NAME FROM SYSIBM.SYSTABLES WHERE TYPE = '" . self::SYSIBM_SYSTABLES_TYPE_TABLE . "'" + . ' AND CREATOR = CURRENT_USER'; + } + + /** + * {@inheritDoc} + * + * @internal The method should be only used from within the {@see AbstractSchemaManager} class hierarchy. + */ + public function getListViewsSQL($database) + { + return 'SELECT NAME, TEXT FROM SYSIBM.SYSVIEWS'; + } + + /** + * @deprecated The SQL used for schema introspection is an implementation detail and should not be relied upon. + * + * {@inheritDoc} + */ + public function getListTableIndexesSQL($table, $database = null) + { + $table = $this->quoteStringLiteral($table); + + return "SELECT idx.INDNAME AS key_name, + idxcol.COLNAME AS column_name, + CASE + WHEN idx.UNIQUERULE = '" . self::SYSCAT_INDEXES_UNIQUERULE_IMPLEMENTS_PRIMARY_KEY . "' + THEN 1 + ELSE 0 + END AS primary, + CASE + WHEN idx.UNIQUERULE = '" . self::SYSCAT_INDEXES_UNIQUERULE_PERMITS_DUPLICATES . "' + THEN 1 + ELSE 0 + END AS non_unique + FROM SYSCAT.INDEXES AS idx + JOIN SYSCAT.INDEXCOLUSE AS idxcol + ON idx.INDSCHEMA = idxcol.INDSCHEMA AND idx.INDNAME = idxcol.INDNAME + WHERE idx.TABNAME = UPPER(" . $table . ') + ORDER BY idxcol.COLSEQ ASC'; + } + + /** + * @deprecated The SQL used for schema introspection is an implementation detail and should not be relied upon. + * + * {@inheritDoc} + */ + public function getListTableForeignKeysSQL($table) + { + $table = $this->quoteStringLiteral($table); + + return "SELECT fkcol.COLNAME AS local_column, + fk.REFTABNAME AS foreign_table, + pkcol.COLNAME AS foreign_column, + fk.CONSTNAME AS index_name, + CASE + WHEN fk.UPDATERULE = '" . self::SYSCAT_REFERENCES_UPDATERULE_RESTRICT . "' THEN 'RESTRICT' + ELSE NULL + END AS on_update, + CASE + WHEN fk.DELETERULE = '" . self::SYSCAT_REFERENCES_DELETERULE_CASCADE . "' THEN 'CASCADE' + WHEN fk.DELETERULE = '" . self::SYSCAT_REFERENCES_DELETERULE_SET_NULL . "' THEN 'SET NULL' + WHEN fk.DELETERULE = '" . self::SYSCAT_REFERENCES_DELETERULE_RESTRICT . "' THEN 'RESTRICT' + ELSE NULL + END AS on_delete + FROM SYSCAT.REFERENCES AS fk + JOIN SYSCAT.KEYCOLUSE AS fkcol + ON fk.CONSTNAME = fkcol.CONSTNAME + AND fk.TABSCHEMA = fkcol.TABSCHEMA + AND fk.TABNAME = fkcol.TABNAME + JOIN SYSCAT.KEYCOLUSE AS pkcol + ON fk.REFKEYNAME = pkcol.CONSTNAME + AND fk.REFTABSCHEMA = pkcol.TABSCHEMA + AND fk.REFTABNAME = pkcol.TABNAME + WHERE fk.TABNAME = UPPER(" . $table . ') + ORDER BY fkcol.COLSEQ ASC'; + } + + /** + * {@inheritDoc} + * + * @deprecated + */ + public function supportsCreateDropDatabase() + { + Deprecation::trigger( + 'doctrine/dbal', + 'https://github.com/doctrine/dbal/pull/5513', + '%s() is deprecated.', + __METHOD__, + ); + + return false; + } + + /** + * {@inheritDoc} + * + * @internal The method should be only used from within the {@see AbstractPlatform} class hierarchy. + */ + public function supportsCommentOnStatement() + { + return true; + } + + /** + * {@inheritDoc} + */ + public function getCurrentDateSQL() + { + return 'CURRENT DATE'; + } + + /** + * {@inheritDoc} + */ + public function getCurrentTimeSQL() + { + return 'CURRENT TIME'; + } + + /** + * {@inheritDoc} + */ + public function getCurrentTimestampSQL() + { + return 'CURRENT TIMESTAMP'; + } + + /** + * {@inheritDoc} + * + * @internal The method should be only used from within the {@see AbstractPlatform} class hierarchy. + */ + public function getIndexDeclarationSQL($name, Index $index) + { + // Index declaration in statements like CREATE TABLE is not supported. + throw Exception::notSupported(__METHOD__); + } + + /** + * {@inheritDoc} + */ + protected function _getCreateTableSQL($name, array $columns, array $options = []) + { + $indexes = []; + if (isset($options['indexes'])) { + $indexes = $options['indexes']; + } + + $options['indexes'] = []; + + $sqls = parent::_getCreateTableSQL($name, $columns, $options); + + foreach ($indexes as $definition) { + $sqls[] = $this->getCreateIndexSQL($definition, $name); + } + + return $sqls; + } + + /** + * {@inheritDoc} + */ + public function getAlterTableSQL(TableDiff $diff) + { + $sql = []; + $columnSql = []; + $commentsSQL = []; + + $tableNameSQL = ($diff->getOldTable() ?? $diff->getName($this))->getQuotedName($this); + + $queryParts = []; + foreach ($diff->getAddedColumns() as $column) { + if ($this->onSchemaAlterTableAddColumn($column, $diff, $columnSql)) { + continue; + } + + $columnDef = $column->toArray(); + $queryPart = 'ADD COLUMN ' . $this->getColumnDeclarationSQL($column->getQuotedName($this), $columnDef); + + // Adding non-nullable columns to a table requires a default value to be specified. + if ( + ! empty($columnDef['notnull']) && + ! isset($columnDef['default']) && + empty($columnDef['autoincrement']) + ) { + $queryPart .= ' WITH DEFAULT'; + } + + $queryParts[] = $queryPart; + + $comment = $this->getColumnComment($column); + + if ($comment === null || $comment === '') { + continue; + } + + $commentsSQL[] = $this->getCommentOnColumnSQL( + $tableNameSQL, + $column->getQuotedName($this), + $comment, + ); + } + + foreach ($diff->getDroppedColumns() as $column) { + if ($this->onSchemaAlterTableRemoveColumn($column, $diff, $columnSql)) { + continue; + } + + $queryParts[] = 'DROP COLUMN ' . $column->getQuotedName($this); + } + + foreach ($diff->getModifiedColumns() as $columnDiff) { + if ($this->onSchemaAlterTableChangeColumn($columnDiff, $diff, $columnSql)) { + continue; + } + + if ($columnDiff->hasCommentChanged()) { + $commentsSQL[] = $this->getCommentOnColumnSQL( + $tableNameSQL, + $columnDiff->getNewColumn()->getQuotedName($this), + $this->getColumnComment($columnDiff->getNewColumn()), + ); + } + + $this->gatherAlterColumnSQL( + $tableNameSQL, + $columnDiff, + $sql, + $queryParts, + ); + } + + foreach ($diff->getRenamedColumns() as $oldColumnName => $column) { + if ($this->onSchemaAlterTableRenameColumn($oldColumnName, $column, $diff, $columnSql)) { + continue; + } + + $oldColumnName = new Identifier($oldColumnName); + + $queryParts[] = 'RENAME COLUMN ' . $oldColumnName->getQuotedName($this) . + ' TO ' . $column->getQuotedName($this); + } + + $tableSql = []; + + if (! $this->onSchemaAlterTable($diff, $tableSql)) { + if (count($queryParts) > 0) { + $sql[] = 'ALTER TABLE ' . $tableNameSQL . ' ' . implode(' ', $queryParts); + } + + // Some table alteration operations require a table reorganization. + if (count($diff->getDroppedColumns()) > 0 || count($diff->getModifiedColumns()) > 0) { + $sql[] = "CALL SYSPROC.ADMIN_CMD ('REORG TABLE " . $tableNameSQL . "')"; + } + + $sql = array_merge($sql, $commentsSQL); + + $newName = $diff->getNewName(); + + if ($newName !== false) { + Deprecation::trigger( + 'doctrine/dbal', + 'https://github.com/doctrine/dbal/pull/5663', + 'Generation of "rename table" SQL using %s() is deprecated. Use getRenameTableSQL() instead.', + __METHOD__, + ); + + $sql[] = sprintf( + 'RENAME TABLE %s TO %s', + $tableNameSQL, + $newName->getQuotedName($this), + ); + } + + $sql = array_merge( + $this->getPreAlterTableIndexForeignKeySQL($diff), + $sql, + $this->getPostAlterTableIndexForeignKeySQL($diff), + ); + } + + return array_merge($sql, $tableSql, $columnSql); + } + + /** + * {@inheritDoc} + */ + public function getRenameTableSQL(string $oldName, string $newName): array + { + return [ + sprintf('RENAME TABLE %s TO %s', $oldName, $newName), + ]; + } + + /** + * Gathers the table alteration SQL for a given column diff. + * + * @param string $table The table to gather the SQL for. + * @param ColumnDiff $columnDiff The column diff to evaluate. + * @param string[] $sql The sequence of table alteration statements to fill. + * @param mixed[] $queryParts The sequence of column alteration clauses to fill. + */ + private function gatherAlterColumnSQL( + string $table, + ColumnDiff $columnDiff, + array &$sql, + array &$queryParts + ): void { + $alterColumnClauses = $this->getAlterColumnClausesSQL($columnDiff); + + if (empty($alterColumnClauses)) { + return; + } + + // If we have a single column alteration, we can append the clause to the main query. + if (count($alterColumnClauses) === 1) { + $queryParts[] = current($alterColumnClauses); + + return; + } + + // We have multiple alterations for the same column, + // so we need to trigger a complete ALTER TABLE statement + // for each ALTER COLUMN clause. + foreach ($alterColumnClauses as $alterColumnClause) { + $sql[] = 'ALTER TABLE ' . $table . ' ' . $alterColumnClause; + } + } + + /** + * Returns the ALTER COLUMN SQL clauses for altering a column described by the given column diff. + * + * @return string[] + */ + private function getAlterColumnClausesSQL(ColumnDiff $columnDiff): array + { + $newColumn = $columnDiff->getNewColumn()->toArray(); + + $alterClause = 'ALTER COLUMN ' . $columnDiff->getNewColumn()->getQuotedName($this); + + if ($newColumn['columnDefinition'] !== null) { + return [$alterClause . ' ' . $newColumn['columnDefinition']]; + } + + $clauses = []; + + if ( + $columnDiff->hasTypeChanged() || + $columnDiff->hasLengthChanged() || + $columnDiff->hasPrecisionChanged() || + $columnDiff->hasScaleChanged() || + $columnDiff->hasFixedChanged() + ) { + $clauses[] = $alterClause . ' SET DATA TYPE ' . $newColumn['type']->getSQLDeclaration($newColumn, $this); + } + + if ($columnDiff->hasNotNullChanged()) { + $clauses[] = $newColumn['notnull'] ? $alterClause . ' SET NOT NULL' : $alterClause . ' DROP NOT NULL'; + } + + if ($columnDiff->hasDefaultChanged()) { + if (isset($newColumn['default'])) { + $defaultClause = $this->getDefaultValueDeclarationSQL($newColumn); + + if ($defaultClause !== '') { + $clauses[] = $alterClause . ' SET' . $defaultClause; + } + } else { + $clauses[] = $alterClause . ' DROP DEFAULT'; + } + } + + return $clauses; + } + + /** + * {@inheritDoc} + */ + protected function getPreAlterTableIndexForeignKeySQL(TableDiff $diff) + { + $sql = []; + + $tableNameSQL = ($diff->getOldTable() ?? $diff->getName($this))->getQuotedName($this); + + foreach ($diff->getDroppedIndexes() as $droppedIndex) { + foreach ($diff->getAddedIndexes() as $addedIndex) { + if ($droppedIndex->getColumns() !== $addedIndex->getColumns()) { + continue; + } + + if ($droppedIndex->isPrimary()) { + $sql[] = 'ALTER TABLE ' . $tableNameSQL . ' DROP PRIMARY KEY'; + } elseif ($droppedIndex->isUnique()) { + $sql[] = 'ALTER TABLE ' . $tableNameSQL . ' DROP UNIQUE ' . $droppedIndex->getQuotedName($this); + } else { + $sql[] = $this->getDropIndexSQL($droppedIndex, $tableNameSQL); + } + + $sql[] = $this->getCreateIndexSQL($addedIndex, $tableNameSQL); + + $diff->unsetAddedIndex($addedIndex); + $diff->unsetDroppedIndex($droppedIndex); + + break; + } + } + + return array_merge($sql, parent::getPreAlterTableIndexForeignKeySQL($diff)); + } + + /** + * {@inheritDoc} + */ + protected function getRenameIndexSQL($oldIndexName, Index $index, $tableName) + { + if (strpos($tableName, '.') !== false) { + [$schema] = explode('.', $tableName); + $oldIndexName = $schema . '.' . $oldIndexName; + } + + return ['RENAME INDEX ' . $oldIndexName . ' TO ' . $index->getQuotedName($this)]; + } + + /** + * {@inheritDoc} + * + * @internal The method should be only used from within the {@see AbstractPlatform} class hierarchy. + */ + public function getDefaultValueDeclarationSQL($column) + { + if (! empty($column['autoincrement'])) { + return ''; + } + + if (! empty($column['version'])) { + if ((string) $column['type'] !== 'DateTime') { + $column['default'] = '1'; + } + } + + return parent::getDefaultValueDeclarationSQL($column); + } + + /** + * {@inheritDoc} + */ + public function getEmptyIdentityInsertSQL($quotedTableName, $quotedIdentifierColumnName) + { + return 'INSERT INTO ' . $quotedTableName . ' (' . $quotedIdentifierColumnName . ') VALUES (DEFAULT)'; + } + + /** + * {@inheritDoc} + */ + public function getCreateTemporaryTableSnippetSQL() + { + return 'DECLARE GLOBAL TEMPORARY TABLE'; + } + + /** + * {@inheritDoc} + */ + public function getTemporaryTableName($tableName) + { + return 'SESSION.' . $tableName; + } + + /** + * {@inheritDoc} + */ + protected function doModifyLimitQuery($query, $limit, $offset) + { + $where = []; + + if ($offset > 0) { + $where[] = sprintf('db22.DC_ROWNUM >= %d', $offset + 1); + } + + if ($limit !== null) { + $where[] = sprintf('db22.DC_ROWNUM <= %d', $offset + $limit); + } + + if (empty($where)) { + return $query; + } + + // Todo OVER() needs ORDER BY data! + return sprintf( + 'SELECT db22.* FROM (SELECT db21.*, ROW_NUMBER() OVER() AS DC_ROWNUM FROM (%s) db21) db22 WHERE %s', + $query, + implode(' AND ', $where), + ); + } + + /** + * {@inheritDoc} + */ + public function getLocateExpression($str, $substr, $startPos = false) + { + if ($startPos === false) { + return 'LOCATE(' . $substr . ', ' . $str . ')'; + } + + return 'LOCATE(' . $substr . ', ' . $str . ', ' . $startPos . ')'; + } + + /** + * {@inheritDoc} + */ + public function getSubstringExpression($string, $start, $length = null) + { + if ($length === null) { + return 'SUBSTR(' . $string . ', ' . $start . ')'; + } + + return 'SUBSTR(' . $string . ', ' . $start . ', ' . $length . ')'; + } + + /** + * {@inheritDoc} + */ + public function getLengthExpression($column) + { + return 'LENGTH(' . $column . ', CODEUNITS32)'; + } + + public function getCurrentDatabaseExpression(): string + { + return 'CURRENT_USER'; + } + + /** + * {@inheritDoc} + */ + public function supportsIdentityColumns() + { + return true; + } + + /** + * {@inheritDoc} + * + * @deprecated + */ + public function prefersIdentityColumns() + { + Deprecation::trigger( + 'doctrine/dbal', + 'https://github.com/doctrine/dbal/pull/1519', + '%s() is deprecated.', + __METHOD__, + ); + + return true; + } + + public function createSelectSQLBuilder(): SelectSQLBuilder + { + return new DefaultSelectSQLBuilder($this, 'WITH RR USE AND KEEP UPDATE LOCKS', null); + } + + /** + * {@inheritDoc} + * + * @deprecated This API is not portable. + */ + public function getForUpdateSQL() + { + return ' WITH RR USE AND KEEP UPDATE LOCKS'; + } + + /** + * {@inheritDoc} + */ + public function getDummySelectSQL() + { + $expression = func_num_args() > 0 ? func_get_arg(0) : '1'; + + return sprintf('SELECT %s FROM sysibm.sysdummy1', $expression); + } + + /** + * {@inheritDoc} + * + * DB2 supports savepoints, but they work semantically different than on other vendor platforms. + * + * TODO: We have to investigate how to get DB2 up and running with savepoints. + */ + public function supportsSavepoints() + { + return false; + } + + /** + * {@inheritDoc} + * + * @deprecated Implement {@see createReservedKeywordsList()} instead. + */ + protected function getReservedKeywordsClass() + { + Deprecation::triggerIfCalledFromOutside( + 'doctrine/dbal', + 'https://github.com/doctrine/dbal/issues/4510', + '%s() is deprecated,' + . ' use %s::createReservedKeywordsList() instead.', + __METHOD__, + static::class, + ); + + return Keywords\DB2Keywords::class; + } + + /** @deprecated The SQL used for schema introspection is an implementation detail and should not be relied upon. */ + public function getListTableCommentsSQL(string $table): string + { + return sprintf( + <<<'SQL' +SELECT REMARKS + FROM SYSIBM.SYSTABLES + WHERE NAME = UPPER( %s ) +SQL + , + $this->quoteStringLiteral($table), + ); + } + + public function createSchemaManager(Connection $connection): DB2SchemaManager + { + return new DB2SchemaManager($connection, $this); + } +} diff --git a/3rdparty/doctrine/dbal/src/Platforms/DateIntervalUnit.php b/3rdparty/doctrine/dbal/src/Platforms/DateIntervalUnit.php new file mode 100644 index 00000000..a95c4e28 --- /dev/null +++ b/3rdparty/doctrine/dbal/src/Platforms/DateIntervalUnit.php @@ -0,0 +1,29 @@ +keywords === null) { + $this->initializeKeywords(); + } + + return isset($this->keywords[strtoupper($word)]); + } + + /** @return void */ + protected function initializeKeywords() + { + $this->keywords = array_flip(array_map('strtoupper', $this->getKeywords())); + } + + /** + * Returns the list of keywords. + * + * @return string[] + */ + abstract protected function getKeywords(); + + /** + * Returns the name of this keyword list. + * + * @deprecated + * + * @return string + */ + abstract public function getName(); +} diff --git a/3rdparty/doctrine/dbal/src/Platforms/Keywords/MariaDBKeywords.php b/3rdparty/doctrine/dbal/src/Platforms/Keywords/MariaDBKeywords.php new file mode 100644 index 00000000..5417c6ca --- /dev/null +++ b/3rdparty/doctrine/dbal/src/Platforms/Keywords/MariaDBKeywords.php @@ -0,0 +1,276 @@ +keywordLists = $keywordLists; + } + + /** @return string[] */ + public function getViolations() + { + return $this->violations; + } + + /** + * @param string $word + * + * @return string[] + */ + private function isReservedWord($word): array + { + if ($word[0] === '`') { + $word = str_replace('`', '', $word); + } + + $keywordLists = []; + foreach ($this->keywordLists as $keywordList) { + if (! $keywordList->isKeyword($word)) { + continue; + } + + $keywordLists[] = $keywordList->getName(); + } + + return $keywordLists; + } + + /** + * @param string $asset + * @param string[] $violatedPlatforms + */ + private function addViolation($asset, $violatedPlatforms): void + { + if (count($violatedPlatforms) === 0) { + return; + } + + $this->violations[] = $asset . ' keyword violations: ' . implode(', ', $violatedPlatforms); + } + + /** + * {@inheritDoc} + */ + public function acceptColumn(Table $table, Column $column) + { + $this->addViolation( + 'Table ' . $table->getName() . ' column ' . $column->getName(), + $this->isReservedWord($column->getName()), + ); + } + + /** + * {@inheritDoc} + */ + public function acceptForeignKey(Table $localTable, ForeignKeyConstraint $fkConstraint) + { + } + + /** + * {@inheritDoc} + */ + public function acceptIndex(Table $table, Index $index) + { + } + + /** + * {@inheritDoc} + */ + public function acceptSchema(Schema $schema) + { + } + + /** + * {@inheritDoc} + */ + public function acceptSequence(Sequence $sequence) + { + } + + /** + * {@inheritDoc} + */ + public function acceptTable(Table $table) + { + $this->addViolation( + 'Table ' . $table->getName(), + $this->isReservedWord($table->getName()), + ); + } +} diff --git a/3rdparty/doctrine/dbal/src/Platforms/Keywords/SQLServer2012Keywords.php b/3rdparty/doctrine/dbal/src/Platforms/Keywords/SQLServer2012Keywords.php new file mode 100644 index 00000000..ebc45c40 --- /dev/null +++ b/3rdparty/doctrine/dbal/src/Platforms/Keywords/SQLServer2012Keywords.php @@ -0,0 +1,12 @@ +doctrineTypeMapping['json'] = Types::JSON; + } +} diff --git a/3rdparty/doctrine/dbal/src/Platforms/MariaDb1010Platform.php b/3rdparty/doctrine/dbal/src/Platforms/MariaDb1010Platform.php new file mode 100644 index 00000000..6cddbdd4 --- /dev/null +++ b/3rdparty/doctrine/dbal/src/Platforms/MariaDb1010Platform.php @@ -0,0 +1,47 @@ +getColumnTypeSQLSnippets('c', $database); + + return sprintf( + <<getDatabaseNameSQL($database), + $this->quoteStringLiteral($table), + ); + } + + /** + * Generate SQL snippets to reverse the aliasing of JSON to LONGTEXT. + * + * MariaDb aliases columns specified as JSON to LONGTEXT and sets a CHECK constraint to ensure the column + * is valid json. This function generates the SQL snippets which reverse this aliasing i.e. report a column + * as JSON where it was originally specified as such instead of LONGTEXT. + * + * The CHECK constraints are stored in information_schema.CHECK_CONSTRAINTS so query that table. + */ + public function getColumnTypeSQLSnippet(string $tableAlias = 'c', ?string $databaseName = null): string + { + if ($this->getJsonTypeDeclarationSQL([]) !== 'JSON') { + return parent::getColumnTypeSQLSnippet($tableAlias, $databaseName); + } + + if ($databaseName === null) { + Deprecation::trigger( + 'doctrine/dbal', + 'https://github.com/doctrine/dbal/pull/6215', + 'Not passing a database name to methods "getColumnTypeSQLSnippet()", ' + . '"getColumnTypeSQLSnippets()", and "getListTableColumnsSQL()" of "%s" is deprecated.', + self::class, + ); + } + + $subQueryAlias = 'i_' . $tableAlias; + + $databaseName = $this->getDatabaseNameSQL($databaseName); + + // The check for `CONSTRAINT_SCHEMA = $databaseName` is mandatory here to prevent performance issues + return <<getJsonTypeDeclarationSQL([]) === 'JSON' && ($column['type'] ?? null) instanceof JsonType) { + unset($column['collation']); + unset($column['charset']); + } + + return parent::getColumnDeclarationSQL($name, $column); + } +} diff --git a/3rdparty/doctrine/dbal/src/Platforms/MariaDb1052Platform.php b/3rdparty/doctrine/dbal/src/Platforms/MariaDb1052Platform.php new file mode 100644 index 00000000..a2199cdd --- /dev/null +++ b/3rdparty/doctrine/dbal/src/Platforms/MariaDb1052Platform.php @@ -0,0 +1,36 @@ +getQuotedName($this)]; + } +} diff --git a/3rdparty/doctrine/dbal/src/Platforms/MariaDb1060Platform.php b/3rdparty/doctrine/dbal/src/Platforms/MariaDb1060Platform.php new file mode 100644 index 00000000..b3ce4eb9 --- /dev/null +++ b/3rdparty/doctrine/dbal/src/Platforms/MariaDb1060Platform.php @@ -0,0 +1,16 @@ + */ + private $cache = []; + + public function __construct(CollationMetadataProvider $collationMetadataProvider) + { + $this->collationMetadataProvider = $collationMetadataProvider; + } + + public function getCollationCharset(string $collation): ?string + { + if (array_key_exists($collation, $this->cache)) { + return $this->cache[$collation]; + } + + return $this->cache[$collation] = $this->collationMetadataProvider->getCollationCharset($collation); + } +} diff --git a/3rdparty/doctrine/dbal/src/Platforms/MySQL/CollationMetadataProvider/ConnectionCollationMetadataProvider.php b/3rdparty/doctrine/dbal/src/Platforms/MySQL/CollationMetadataProvider/ConnectionCollationMetadataProvider.php new file mode 100644 index 00000000..8dc2421a --- /dev/null +++ b/3rdparty/doctrine/dbal/src/Platforms/MySQL/CollationMetadataProvider/ConnectionCollationMetadataProvider.php @@ -0,0 +1,41 @@ +connection = $connection; + } + + /** @throws Exception */ + public function getCollationCharset(string $collation): ?string + { + $charset = $this->connection->fetchOne( + <<<'SQL' +SELECT CHARACTER_SET_NAME +FROM information_schema.COLLATIONS +WHERE COLLATION_NAME = ?; +SQL + , + [$collation], + ); + + if ($charset !== false) { + return $charset; + } + + return null; + } +} diff --git a/3rdparty/doctrine/dbal/src/Platforms/MySQL/Comparator.php b/3rdparty/doctrine/dbal/src/Platforms/MySQL/Comparator.php new file mode 100644 index 00000000..c27fef74 --- /dev/null +++ b/3rdparty/doctrine/dbal/src/Platforms/MySQL/Comparator.php @@ -0,0 +1,94 @@ +collationMetadataProvider = $collationMetadataProvider; + } + + public function compareTables(Table $fromTable, Table $toTable): TableDiff + { + return parent::compareTables( + $this->normalizeColumns($fromTable), + $this->normalizeColumns($toTable), + ); + } + + /** + * {@inheritDoc} + */ + public function diffTable(Table $fromTable, Table $toTable) + { + return parent::diffTable( + $this->normalizeColumns($fromTable), + $this->normalizeColumns($toTable), + ); + } + + private function normalizeColumns(Table $table): Table + { + $tableOptions = array_intersect_key($table->getOptions(), [ + 'charset' => null, + 'collation' => null, + ]); + + $table = clone $table; + + foreach ($table->getColumns() as $column) { + $originalOptions = $column->getPlatformOptions(); + $normalizedOptions = $this->normalizeOptions($originalOptions); + + $overrideOptions = array_diff_assoc($normalizedOptions, $tableOptions); + + if ($overrideOptions === $originalOptions) { + continue; + } + + $column->setPlatformOptions($overrideOptions); + } + + return $table; + } + + /** + * @param array $options + * + * @return array + */ + private function normalizeOptions(array $options): array + { + if (isset($options['collation']) && ! isset($options['charset'])) { + $charset = $this->collationMetadataProvider->getCollationCharset($options['collation']); + + if ($charset !== null) { + $options['charset'] = $charset; + } + } + + return $options; + } +} diff --git a/3rdparty/doctrine/dbal/src/Platforms/MySQL57Platform.php b/3rdparty/doctrine/dbal/src/Platforms/MySQL57Platform.php new file mode 100644 index 00000000..d5169bdd --- /dev/null +++ b/3rdparty/doctrine/dbal/src/Platforms/MySQL57Platform.php @@ -0,0 +1,99 @@ +getQuotedName($this)]; + } + + /** + * {@inheritDoc} + * + * @deprecated Implement {@see createReservedKeywordsList()} instead. + */ + protected function getReservedKeywordsClass() + { + Deprecation::triggerIfCalledFromOutside( + 'doctrine/dbal', + 'https://github.com/doctrine/dbal/issues/4510', + 'MySQL57Platform::getReservedKeywordsClass() is deprecated,' + . ' use MySQL57Platform::createReservedKeywordsList() instead.', + ); + + return Keywords\MySQL57Keywords::class; + } + + /** + * {@inheritDoc} + */ + protected function initializeDoctrineTypeMappings() + { + parent::initializeDoctrineTypeMappings(); + + $this->doctrineTypeMapping['json'] = Types::JSON; + } +} diff --git a/3rdparty/doctrine/dbal/src/Platforms/MySQL80Platform.php b/3rdparty/doctrine/dbal/src/Platforms/MySQL80Platform.php new file mode 100644 index 00000000..d9324282 --- /dev/null +++ b/3rdparty/doctrine/dbal/src/Platforms/MySQL80Platform.php @@ -0,0 +1,34 @@ +multiplyInterval((string) $interval, 3); + break; + + case DateIntervalUnit::YEAR: + $interval = $this->multiplyInterval((string) $interval, 12); + break; + } + + return 'ADD_MONTHS(' . $date . ', ' . $operator . $interval . ')'; + + default: + $calculationClause = ''; + + switch ($unit) { + case DateIntervalUnit::SECOND: + $calculationClause = '/24/60/60'; + break; + + case DateIntervalUnit::MINUTE: + $calculationClause = '/24/60'; + break; + + case DateIntervalUnit::HOUR: + $calculationClause = '/24'; + break; + + case DateIntervalUnit::WEEK: + $calculationClause = '*7'; + break; + } + + return '(' . $date . $operator . $interval . $calculationClause . ')'; + } + } + + /** + * {@inheritDoc} + */ + public function getDateDiffExpression($date1, $date2) + { + return sprintf('TRUNC(%s) - TRUNC(%s)', $date1, $date2); + } + + /** + * {@inheritDoc} + */ + public function getBitAndComparisonExpression($value1, $value2) + { + return 'BITAND(' . $value1 . ', ' . $value2 . ')'; + } + + public function getCurrentDatabaseExpression(): string + { + return "SYS_CONTEXT('USERENV', 'CURRENT_SCHEMA')"; + } + + /** + * {@inheritDoc} + */ + public function getBitOrComparisonExpression($value1, $value2) + { + return '(' . $value1 . '-' . + $this->getBitAndComparisonExpression($value1, $value2) + . '+' . $value2 . ')'; + } + + /** + * {@inheritDoc} + */ + public function getCreatePrimaryKeySQL(Index $index, $table): string + { + if ($table instanceof Table) { + Deprecation::trigger( + 'doctrine/dbal', + 'https://github.com/doctrine/dbal/issues/4798', + 'Passing $table as a Table object to %s is deprecated. Pass it as a quoted name instead.', + __METHOD__, + ); + + $table = $table->getQuotedName($this); + } + + return 'ALTER TABLE ' . $table . ' ADD CONSTRAINT ' . $index->getQuotedName($this) + . ' PRIMARY KEY (' . $this->getIndexFieldDeclarationListSQL($index) . ')'; + } + + /** + * {@inheritDoc} + * + * Need to specifiy minvalue, since start with is hidden in the system and MINVALUE <= START WITH. + * Therefore we can use MINVALUE to be able to get a hint what START WITH was for later introspection + * in {@see listSequences()} + */ + public function getCreateSequenceSQL(Sequence $sequence) + { + return 'CREATE SEQUENCE ' . $sequence->getQuotedName($this) . + ' START WITH ' . $sequence->getInitialValue() . + ' MINVALUE ' . $sequence->getInitialValue() . + ' INCREMENT BY ' . $sequence->getAllocationSize() . + $this->getSequenceCacheSQL($sequence); + } + + /** + * {@inheritDoc} + */ + public function getAlterSequenceSQL(Sequence $sequence) + { + return 'ALTER SEQUENCE ' . $sequence->getQuotedName($this) . + ' INCREMENT BY ' . $sequence->getAllocationSize() + . $this->getSequenceCacheSQL($sequence); + } + + /** + * Cache definition for sequences + */ + private function getSequenceCacheSQL(Sequence $sequence): string + { + if ($sequence->getCache() === 0) { + return ' NOCACHE'; + } + + if ($sequence->getCache() === 1) { + return ' NOCACHE'; + } + + if ($sequence->getCache() > 1) { + return ' CACHE ' . $sequence->getCache(); + } + + return ''; + } + + /** + * {@inheritDoc} + */ + public function getSequenceNextValSQL($sequence) + { + return 'SELECT ' . $sequence . '.nextval FROM DUAL'; + } + + /** + * {@inheritDoc} + */ + public function getSetTransactionIsolationSQL($level) + { + return 'SET TRANSACTION ISOLATION LEVEL ' . $this->_getTransactionIsolationLevelSQL($level); + } + + /** + * {@inheritDoc} + */ + protected function _getTransactionIsolationLevelSQL($level) + { + switch ($level) { + case TransactionIsolationLevel::READ_UNCOMMITTED: + return 'READ UNCOMMITTED'; + + case TransactionIsolationLevel::READ_COMMITTED: + return 'READ COMMITTED'; + + case TransactionIsolationLevel::REPEATABLE_READ: + case TransactionIsolationLevel::SERIALIZABLE: + return 'SERIALIZABLE'; + + default: + return parent::_getTransactionIsolationLevelSQL($level); + } + } + + /** + * {@inheritDoc} + */ + public function getBooleanTypeDeclarationSQL(array $column) + { + return 'NUMBER(1)'; + } + + /** + * {@inheritDoc} + */ + public function getIntegerTypeDeclarationSQL(array $column) + { + return 'NUMBER(10)'; + } + + /** + * {@inheritDoc} + */ + public function getBigIntTypeDeclarationSQL(array $column) + { + return 'NUMBER(20)'; + } + + /** + * {@inheritDoc} + */ + public function getSmallIntTypeDeclarationSQL(array $column) + { + return 'NUMBER(5)'; + } + + /** + * {@inheritDoc} + */ + public function getDateTimeTypeDeclarationSQL(array $column) + { + return 'TIMESTAMP(0)'; + } + + /** + * {@inheritDoc} + */ + public function getDateTimeTzTypeDeclarationSQL(array $column) + { + return 'TIMESTAMP(0) WITH TIME ZONE'; + } + + /** + * {@inheritDoc} + */ + public function getDateTypeDeclarationSQL(array $column) + { + return 'DATE'; + } + + /** + * {@inheritDoc} + */ + public function getTimeTypeDeclarationSQL(array $column) + { + return 'DATE'; + } + + /** + * {@inheritDoc} + */ + protected function _getCommonIntegerTypeDeclarationSQL(array $column) + { + return ''; + } + + /** + * {@inheritDoc} + */ + protected function getVarcharTypeDeclarationSQLSnippet($length, $fixed/*, $lengthOmitted = false*/) + { + if ($length <= 0 || (func_num_args() > 2 && func_get_arg(2))) { + Deprecation::trigger( + 'doctrine/dbal', + 'https://github.com/doctrine/dbal/issues/3263', + 'Relying on the default string column length on Oracle is deprecated' + . ', specify the length explicitly.', + ); + } + + return $fixed ? ($length > 0 ? 'CHAR(' . $length . ')' : 'CHAR(2000)') + : ($length > 0 ? 'VARCHAR2(' . $length . ')' : 'VARCHAR2(4000)'); + } + + /** + * {@inheritDoc} + */ + protected function getBinaryTypeDeclarationSQLSnippet($length, $fixed/*, $lengthOmitted = false*/) + { + if ($length <= 0 || (func_num_args() > 2 && func_get_arg(2))) { + Deprecation::trigger( + 'doctrine/dbal', + 'https://github.com/doctrine/dbal/issues/3263', + 'Relying on the default binary column length on Oracle is deprecated' + . ', specify the length explicitly.', + ); + } + + return 'RAW(' . ($length > 0 ? $length : $this->getBinaryMaxLength()) . ')'; + } + + /** + * {@inheritDoc} + * + * @deprecated + */ + public function getBinaryMaxLength() + { + Deprecation::triggerIfCalledFromOutside( + 'doctrine/dbal', + 'https://github.com/doctrine/dbal/issues/3263', + 'OraclePlatform::getBinaryMaxLength() is deprecated.', + ); + + return 2000; + } + + /** + * {@inheritDoc} + */ + public function getClobTypeDeclarationSQL(array $column) + { + return 'CLOB'; + } + + /** + * {@inheritDoc} + * + * @internal The method should be only used from within the {@see AbstractSchemaManager} class hierarchy. + */ + public function getListDatabasesSQL() + { + return 'SELECT username FROM all_users'; + } + + /** + * {@inheritDoc} + * + * @internal The method should be only used from within the {@see AbstractSchemaManager} class hierarchy. + */ + public function getListSequencesSQL($database) + { + $database = $this->normalizeIdentifier($database); + $database = $this->quoteStringLiteral($database->getName()); + + return 'SELECT sequence_name, min_value, increment_by FROM sys.all_sequences ' . + 'WHERE SEQUENCE_OWNER = ' . $database; + } + + /** + * {@inheritDoc} + */ + protected function _getCreateTableSQL($name, array $columns, array $options = []) + { + $indexes = $options['indexes'] ?? []; + $options['indexes'] = []; + $sql = parent::_getCreateTableSQL($name, $columns, $options); + + foreach ($columns as $columnName => $column) { + if (isset($column['sequence'])) { + $sql[] = $this->getCreateSequenceSQL($column['sequence']); + } + + if ( + ! isset($column['autoincrement']) || ! $column['autoincrement'] && + (! isset($column['autoinc']) || ! $column['autoinc']) + ) { + continue; + } + + $sql = array_merge($sql, $this->getCreateAutoincrementSql($columnName, $name)); + } + + foreach ($indexes as $index) { + $sql[] = $this->getCreateIndexSQL($index, $name); + } + + return $sql; + } + + /** + * @deprecated The SQL used for schema introspection is an implementation detail and should not be relied upon. + * + * {@inheritDoc} + */ + public function getListTableIndexesSQL($table, $database = null) + { + $table = $this->normalizeIdentifier($table); + $table = $this->quoteStringLiteral($table->getName()); + + return "SELECT uind_col.index_name AS name, + ( + SELECT uind.index_type + FROM user_indexes uind + WHERE uind.index_name = uind_col.index_name + ) AS type, + decode( + ( + SELECT uind.uniqueness + FROM user_indexes uind + WHERE uind.index_name = uind_col.index_name + ), + 'NONUNIQUE', + 0, + 'UNIQUE', + 1 + ) AS is_unique, + uind_col.column_name AS column_name, + uind_col.column_position AS column_pos, + ( + SELECT ucon.constraint_type + FROM user_constraints ucon + WHERE ucon.index_name = uind_col.index_name + AND ucon.table_name = uind_col.table_name + ) AS is_primary + FROM user_ind_columns uind_col + WHERE uind_col.table_name = " . $table . ' + ORDER BY uind_col.column_position ASC'; + } + + /** + * @deprecated The SQL used for schema introspection is an implementation detail and should not be relied upon. + * + * {@inheritDoc} + */ + public function getListTablesSQL() + { + return 'SELECT * FROM sys.user_tables'; + } + + /** + * {@inheritDoc} + * + * @internal The method should be only used from within the {@see AbstractSchemaManager} class hierarchy. + */ + public function getListViewsSQL($database) + { + return 'SELECT view_name, text FROM sys.user_views'; + } + + /** + * @internal The method should be only used from within the OraclePlatform class hierarchy. + * + * @param string $name + * @param string $table + * @param int $start + * + * @return string[] + */ + public function getCreateAutoincrementSql($name, $table, $start = 1) + { + $tableIdentifier = $this->normalizeIdentifier($table); + $quotedTableName = $tableIdentifier->getQuotedName($this); + $unquotedTableName = $tableIdentifier->getName(); + + $nameIdentifier = $this->normalizeIdentifier($name); + $quotedName = $nameIdentifier->getQuotedName($this); + $unquotedName = $nameIdentifier->getName(); + + $sql = []; + + $autoincrementIdentifierName = $this->getAutoincrementIdentifierName($tableIdentifier); + + $idx = new Index($autoincrementIdentifierName, [$quotedName], true, true); + + $sql[] = "DECLARE + constraints_Count NUMBER; +BEGIN + SELECT COUNT(CONSTRAINT_NAME) INTO constraints_Count + FROM USER_CONSTRAINTS + WHERE TABLE_NAME = '" . $unquotedTableName . "' + AND CONSTRAINT_TYPE = 'P'; + IF constraints_Count = 0 OR constraints_Count = '' THEN + EXECUTE IMMEDIATE '" . $this->getCreateConstraintSQL($idx, $quotedTableName) . "'; + END IF; +END;"; + + $sequenceName = $this->getIdentitySequenceName( + $tableIdentifier->isQuoted() ? $quotedTableName : $unquotedTableName, + $nameIdentifier->isQuoted() ? $quotedName : $unquotedName, + ); + $sequence = new Sequence($sequenceName, $start); + $sql[] = $this->getCreateSequenceSQL($sequence); + + $sql[] = 'CREATE TRIGGER ' . $autoincrementIdentifierName . ' + BEFORE INSERT + ON ' . $quotedTableName . ' + FOR EACH ROW +DECLARE + last_Sequence NUMBER; + last_InsertID NUMBER; +BEGIN + IF (:NEW.' . $quotedName . ' IS NULL OR :NEW.' . $quotedName . ' = 0) THEN + SELECT ' . $sequenceName . '.NEXTVAL INTO :NEW.' . $quotedName . ' FROM DUAL; + ELSE + SELECT NVL(Last_Number, 0) INTO last_Sequence + FROM User_Sequences + WHERE Sequence_Name = \'' . $sequence->getName() . '\'; + SELECT :NEW.' . $quotedName . ' INTO last_InsertID FROM DUAL; + WHILE (last_InsertID > last_Sequence) LOOP + SELECT ' . $sequenceName . '.NEXTVAL INTO last_Sequence FROM DUAL; + END LOOP; + SELECT ' . $sequenceName . '.NEXTVAL INTO last_Sequence FROM DUAL; + END IF; +END;'; + + return $sql; + } + + /** + * @internal The method should be only used from within the OracleSchemaManager class hierarchy. + * + * Returns the SQL statements to drop the autoincrement for the given table name. + * + * @param string $table The table name to drop the autoincrement for. + * + * @return string[] + */ + public function getDropAutoincrementSql($table) + { + $table = $this->normalizeIdentifier($table); + $autoincrementIdentifierName = $this->getAutoincrementIdentifierName($table); + $identitySequenceName = $this->getIdentitySequenceName( + $table->isQuoted() ? $table->getQuotedName($this) : $table->getName(), + '', + ); + + return [ + 'DROP TRIGGER ' . $autoincrementIdentifierName, + $this->getDropSequenceSQL($identitySequenceName), + $this->getDropConstraintSQL($autoincrementIdentifierName, $table->getQuotedName($this)), + ]; + } + + /** + * Normalizes the given identifier. + * + * Uppercases the given identifier if it is not quoted by intention + * to reflect Oracle's internal auto uppercasing strategy of unquoted identifiers. + * + * @param string $name The identifier to normalize. + */ + private function normalizeIdentifier($name): Identifier + { + $identifier = new Identifier($name); + + return $identifier->isQuoted() ? $identifier : new Identifier(strtoupper($name)); + } + + /** + * Adds suffix to identifier, + * + * if the new string exceeds max identifier length, + * keeps $suffix, cuts from $identifier as much as the part exceeding. + */ + private function addSuffix(string $identifier, string $suffix): string + { + $maxPossibleLengthWithoutSuffix = $this->getMaxIdentifierLength() - strlen($suffix); + if (strlen($identifier) > $maxPossibleLengthWithoutSuffix) { + $identifier = substr($identifier, 0, $maxPossibleLengthWithoutSuffix); + } + + return $identifier . $suffix; + } + + /** + * Returns the autoincrement primary key identifier name for the given table identifier. + * + * Quotes the autoincrement primary key identifier name + * if the given table name is quoted by intention. + */ + private function getAutoincrementIdentifierName(Identifier $table): string + { + $identifierName = $this->addSuffix($table->getName(), '_AI_PK'); + + return $table->isQuoted() + ? $this->quoteSingleIdentifier($identifierName) + : $identifierName; + } + + /** + * @deprecated The SQL used for schema introspection is an implementation detail and should not be relied upon. + * + * {@inheritDoc} + */ + public function getListTableForeignKeysSQL($table) + { + $table = $this->normalizeIdentifier($table); + $table = $this->quoteStringLiteral($table->getName()); + + return "SELECT alc.constraint_name, + alc.DELETE_RULE, + cols.column_name \"local_column\", + cols.position, + ( + SELECT r_cols.table_name + FROM user_cons_columns r_cols + WHERE alc.r_constraint_name = r_cols.constraint_name + AND r_cols.position = cols.position + ) AS \"references_table\", + ( + SELECT r_cols.column_name + FROM user_cons_columns r_cols + WHERE alc.r_constraint_name = r_cols.constraint_name + AND r_cols.position = cols.position + ) AS \"foreign_column\" + FROM user_cons_columns cols + JOIN user_constraints alc + ON alc.constraint_name = cols.constraint_name + AND alc.constraint_type = 'R' + AND alc.table_name = " . $table . ' + ORDER BY cols.constraint_name ASC, cols.position ASC'; + } + + /** + * @deprecated + * + * {@inheritDoc} + */ + public function getListTableConstraintsSQL($table) + { + $table = $this->normalizeIdentifier($table); + $table = $this->quoteStringLiteral($table->getName()); + + return 'SELECT * FROM user_constraints WHERE table_name = ' . $table; + } + + /** + * @deprecated The SQL used for schema introspection is an implementation detail and should not be relied upon. + * + * {@inheritDoc} + */ + public function getListTableColumnsSQL($table, $database = null) + { + $table = $this->normalizeIdentifier($table); + $table = $this->quoteStringLiteral($table->getName()); + + $tabColumnsTableName = 'user_tab_columns'; + $colCommentsTableName = 'user_col_comments'; + $tabColumnsOwnerCondition = ''; + $colCommentsOwnerCondition = ''; + + if ($database !== null && $database !== '/') { + $database = $this->normalizeIdentifier($database); + $database = $this->quoteStringLiteral($database->getName()); + $tabColumnsTableName = 'all_tab_columns'; + $colCommentsTableName = 'all_col_comments'; + $tabColumnsOwnerCondition = ' AND c.owner = ' . $database; + $colCommentsOwnerCondition = ' AND d.OWNER = c.OWNER'; + } + + return sprintf( + <<<'SQL' +SELECT c.*, + ( + SELECT d.comments + FROM %s d + WHERE d.TABLE_NAME = c.TABLE_NAME%s + AND d.COLUMN_NAME = c.COLUMN_NAME + ) AS comments +FROM %s c +WHERE c.table_name = %s%s +ORDER BY c.column_id +SQL + , + $colCommentsTableName, + $colCommentsOwnerCondition, + $tabColumnsTableName, + $table, + $tabColumnsOwnerCondition, + ); + } + + /** + * {@inheritDoc} + */ + public function getDropForeignKeySQL($foreignKey, $table) + { + if ($foreignKey instanceof ForeignKeyConstraint) { + Deprecation::trigger( + 'doctrine/dbal', + 'https://github.com/doctrine/dbal/issues/4798', + 'Passing $foreignKey as a ForeignKeyConstraint object to %s is deprecated.' + . ' Pass it as a quoted name instead.', + __METHOD__, + ); + } else { + $foreignKey = new Identifier($foreignKey); + } + + if ($table instanceof Table) { + Deprecation::trigger( + 'doctrine/dbal', + 'https://github.com/doctrine/dbal/issues/4798', + 'Passing $table as a Table object to %s is deprecated. Pass it as a quoted name instead.', + __METHOD__, + ); + } else { + $table = new Identifier($table); + } + + $foreignKey = $foreignKey->getQuotedName($this); + $table = $table->getQuotedName($this); + + return 'ALTER TABLE ' . $table . ' DROP CONSTRAINT ' . $foreignKey; + } + + /** + * {@inheritDoc} + * + * @internal The method should be only used from within the {@see AbstractPlatform} class hierarchy. + */ + public function getAdvancedForeignKeyOptionsSQL(ForeignKeyConstraint $foreignKey) + { + $referentialAction = ''; + + if ($foreignKey->hasOption('onDelete')) { + $referentialAction = $this->getForeignKeyReferentialActionSQL($foreignKey->getOption('onDelete')); + } + + if ($referentialAction !== '') { + return ' ON DELETE ' . $referentialAction; + } + + return ''; + } + + /** + * {@inheritDoc} + * + * @internal The method should be only used from within the {@see AbstractPlatform} class hierarchy. + */ + public function getForeignKeyReferentialActionSQL($action) + { + $action = strtoupper($action); + + switch ($action) { + case 'RESTRICT': // RESTRICT is not supported, therefore falling back to NO ACTION. + case 'NO ACTION': + // NO ACTION cannot be declared explicitly, + // therefore returning empty string to indicate to OMIT the referential clause. + return ''; + + case 'CASCADE': + case 'SET NULL': + return $action; + + default: + // SET DEFAULT is not supported, throw exception instead. + throw new InvalidArgumentException('Invalid foreign key action: ' . $action); + } + } + + /** + * {@inheritDoc} + */ + public function getCreateDatabaseSQL($name) + { + return 'CREATE USER ' . $name; + } + + /** + * {@inheritDoc} + */ + public function getDropDatabaseSQL($name) + { + return 'DROP USER ' . $name . ' CASCADE'; + } + + /** + * {@inheritDoc} + */ + public function getAlterTableSQL(TableDiff $diff) + { + $sql = []; + $commentsSQL = []; + $columnSql = []; + + $fields = []; + + $tableNameSQL = ($diff->getOldTable() ?? $diff->getName($this))->getQuotedName($this); + + foreach ($diff->getAddedColumns() as $column) { + if ($this->onSchemaAlterTableAddColumn($column, $diff, $columnSql)) { + continue; + } + + $fields[] = $this->getColumnDeclarationSQL($column->getQuotedName($this), $column->toArray()); + $comment = $this->getColumnComment($column); + + if ($comment === null || $comment === '') { + continue; + } + + $commentsSQL[] = $this->getCommentOnColumnSQL( + $tableNameSQL, + $column->getQuotedName($this), + $comment, + ); + } + + if (count($fields) > 0) { + $sql[] = 'ALTER TABLE ' . $tableNameSQL . ' ADD (' . implode(', ', $fields) . ')'; + } + + $fields = []; + foreach ($diff->getModifiedColumns() as $columnDiff) { + if ($this->onSchemaAlterTableChangeColumn($columnDiff, $diff, $columnSql)) { + continue; + } + + $newColumn = $columnDiff->getNewColumn(); + + // Do not generate column alteration clause if type is binary and only fixed property has changed. + // Oracle only supports binary type columns with variable length. + // Avoids unnecessary table alteration statements. + if ( + $newColumn->getType() instanceof BinaryType && + $columnDiff->hasFixedChanged() && + count($columnDiff->changedProperties) === 1 + ) { + continue; + } + + $columnHasChangedComment = $columnDiff->hasCommentChanged(); + + /** + * Do not add query part if only comment has changed + */ + if (! ($columnHasChangedComment && count($columnDiff->changedProperties) === 1)) { + $newColumnProperties = $newColumn->toArray(); + + if (! $columnDiff->hasNotNullChanged()) { + unset($newColumnProperties['notnull']); + } + + $fields[] = $newColumn->getQuotedName($this) . $this->getColumnDeclarationSQL('', $newColumnProperties); + } + + if (! $columnHasChangedComment) { + continue; + } + + $commentsSQL[] = $this->getCommentOnColumnSQL( + $tableNameSQL, + $newColumn->getQuotedName($this), + $this->getColumnComment($newColumn), + ); + } + + if (count($fields) > 0) { + $sql[] = 'ALTER TABLE ' . $tableNameSQL . ' MODIFY (' . implode(', ', $fields) . ')'; + } + + foreach ($diff->getRenamedColumns() as $oldColumnName => $column) { + if ($this->onSchemaAlterTableRenameColumn($oldColumnName, $column, $diff, $columnSql)) { + continue; + } + + $oldColumnName = new Identifier($oldColumnName); + + $sql[] = 'ALTER TABLE ' . $tableNameSQL . ' RENAME COLUMN ' . $oldColumnName->getQuotedName($this) + . ' TO ' . $column->getQuotedName($this); + } + + $fields = []; + foreach ($diff->getDroppedColumns() as $column) { + if ($this->onSchemaAlterTableRemoveColumn($column, $diff, $columnSql)) { + continue; + } + + $fields[] = $column->getQuotedName($this); + } + + if (count($fields) > 0) { + $sql[] = 'ALTER TABLE ' . $tableNameSQL . ' DROP (' . implode(', ', $fields) . ')'; + } + + $tableSql = []; + + if (! $this->onSchemaAlterTable($diff, $tableSql)) { + $sql = array_merge($sql, $commentsSQL); + + $newName = $diff->getNewName(); + + if ($newName !== false) { + Deprecation::trigger( + 'doctrine/dbal', + 'https://github.com/doctrine/dbal/pull/5663', + 'Generation of "rename table" SQL using %s is deprecated. Use getRenameTableSQL() instead.', + __METHOD__, + ); + + $sql[] = sprintf( + 'ALTER TABLE %s RENAME TO %s', + $tableNameSQL, + $newName->getQuotedName($this), + ); + } + + $sql = array_merge( + $this->getPreAlterTableIndexForeignKeySQL($diff), + $sql, + $this->getPostAlterTableIndexForeignKeySQL($diff), + ); + } + + return array_merge($sql, $tableSql, $columnSql); + } + + /** + * {@inheritDoc} + * + * @internal The method should be only used from within the {@see AbstractPlatform} class hierarchy. + */ + public function getColumnDeclarationSQL($name, array $column) + { + if (isset($column['columnDefinition'])) { + $columnDef = $this->getCustomTypeDeclarationSQL($column); + } else { + $default = $this->getDefaultValueDeclarationSQL($column); + + $notnull = ''; + + if (isset($column['notnull'])) { + $notnull = $column['notnull'] ? ' NOT NULL' : ' NULL'; + } + + if (! empty($column['unique'])) { + Deprecation::trigger( + 'doctrine/dbal', + 'https://github.com/doctrine/dbal/pull/5656', + 'The usage of the "unique" column property is deprecated. Use unique constraints instead.', + ); + + $unique = ' ' . $this->getUniqueFieldDeclarationSQL(); + } else { + $unique = ''; + } + + if (! empty($column['check'])) { + Deprecation::trigger( + 'doctrine/dbal', + 'https://github.com/doctrine/dbal/pull/5656', + 'The usage of the "check" column property is deprecated.', + ); + + $check = ' ' . $column['check']; + } else { + $check = ''; + } + + $typeDecl = $column['type']->getSQLDeclaration($column, $this); + $columnDef = $typeDecl . $default . $notnull . $unique . $check; + } + + return $name . ' ' . $columnDef; + } + + /** + * {@inheritDoc} + */ + protected function getRenameIndexSQL($oldIndexName, Index $index, $tableName) + { + if (strpos($tableName, '.') !== false) { + [$schema] = explode('.', $tableName); + $oldIndexName = $schema . '.' . $oldIndexName; + } + + return ['ALTER INDEX ' . $oldIndexName . ' RENAME TO ' . $index->getQuotedName($this)]; + } + + /** + * {@inheritDoc} + * + * @deprecated + */ + public function usesSequenceEmulatedIdentityColumns() + { + Deprecation::trigger( + 'doctrine/dbal', + 'https://github.com/doctrine/dbal/pull/5513', + '%s is deprecated.', + __METHOD__, + ); + + return true; + } + + /** + * {@inheritDoc} + * + * @internal The method should be only used from within the OraclePlatform class hierarchy. + */ + public function getIdentitySequenceName($tableName, $columnName) + { + $table = new Identifier($tableName); + + // No usage of column name to preserve BC compatibility with <2.5 + $identitySequenceName = $this->addSuffix($table->getName(), '_SEQ'); + + if ($table->isQuoted()) { + $identitySequenceName = '"' . $identitySequenceName . '"'; + } + + $identitySequenceIdentifier = $this->normalizeIdentifier($identitySequenceName); + + return $identitySequenceIdentifier->getQuotedName($this); + } + + /** + * {@inheritDoc} + * + * @internal The method should be only used from within the {@see AbstractPlatform} class hierarchy. + */ + public function supportsCommentOnStatement() + { + return true; + } + + /** + * {@inheritDoc} + */ + public function getName() + { + Deprecation::triggerIfCalledFromOutside( + 'doctrine/dbal', + 'https://github.com/doctrine/dbal/issues/4749', + 'OraclePlatform::getName() is deprecated. Identify platforms by their class.', + ); + + return 'oracle'; + } + + /** + * {@inheritDoc} + */ + protected function doModifyLimitQuery($query, $limit, $offset) + { + if ($limit === null && $offset <= 0) { + return $query; + } + + if (preg_match('/^\s*SELECT/i', $query) === 1) { + if (preg_match('/\sFROM\s/i', $query) === 0) { + $query .= ' FROM dual'; + } + + $columns = ['a.*']; + + if ($offset > 0) { + $columns[] = 'ROWNUM AS doctrine_rownum'; + } + + $query = sprintf('SELECT %s FROM (%s) a', implode(', ', $columns), $query); + + if ($limit !== null) { + $query .= sprintf(' WHERE ROWNUM <= %d', $offset + $limit); + } + + if ($offset > 0) { + $query = sprintf('SELECT * FROM (%s) WHERE doctrine_rownum >= %d', $query, $offset + 1); + } + } + + return $query; + } + + /** + * {@inheritDoc} + */ + public function getCreateTemporaryTableSnippetSQL() + { + return 'CREATE GLOBAL TEMPORARY TABLE'; + } + + /** + * {@inheritDoc} + */ + public function getDateTimeTzFormatString() + { + return 'Y-m-d H:i:sP'; + } + + /** + * {@inheritDoc} + */ + public function getDateFormatString() + { + return 'Y-m-d 00:00:00'; + } + + /** + * {@inheritDoc} + */ + public function getTimeFormatString() + { + return '1900-01-01 H:i:s'; + } + + /** + * {@inheritDoc} + */ + public function getMaxIdentifierLength() + { + return 30; + } + + /** + * {@inheritDoc} + */ + public function supportsSequences() + { + return true; + } + + /** + * {@inheritDoc} + */ + public function supportsReleaseSavepoints() + { + return false; + } + + /** + * {@inheritDoc} + */ + public function getTruncateTableSQL($tableName, $cascade = false) + { + $tableIdentifier = new Identifier($tableName); + + return 'TRUNCATE TABLE ' . $tableIdentifier->getQuotedName($this); + } + + /** + * {@inheritDoc} + */ + public function getDummySelectSQL() + { + $expression = func_num_args() > 0 ? func_get_arg(0) : '1'; + + return sprintf('SELECT %s FROM DUAL', $expression); + } + + /** + * {@inheritDoc} + */ + protected function initializeDoctrineTypeMappings() + { + $this->doctrineTypeMapping = [ + 'binary_double' => Types::FLOAT, + 'binary_float' => Types::FLOAT, + 'binary_integer' => Types::BOOLEAN, + 'blob' => Types::BLOB, + 'char' => Types::STRING, + 'clob' => Types::TEXT, + 'date' => Types::DATE_MUTABLE, + 'float' => Types::FLOAT, + 'integer' => Types::INTEGER, + 'long' => Types::STRING, + 'long raw' => Types::BLOB, + 'nchar' => Types::STRING, + 'nclob' => Types::TEXT, + 'number' => Types::INTEGER, + 'nvarchar2' => Types::STRING, + 'pls_integer' => Types::BOOLEAN, + 'raw' => Types::BINARY, + 'rowid' => Types::STRING, + 'timestamp' => Types::DATETIME_MUTABLE, + 'timestamptz' => Types::DATETIMETZ_MUTABLE, + 'urowid' => Types::STRING, + 'varchar' => Types::STRING, + 'varchar2' => Types::STRING, + ]; + } + + /** + * {@inheritDoc} + */ + public function releaseSavePoint($savepoint) + { + return ''; + } + + /** + * {@inheritDoc} + * + * @deprecated Implement {@see createReservedKeywordsList()} instead. + */ + protected function getReservedKeywordsClass() + { + Deprecation::triggerIfCalledFromOutside( + 'doctrine/dbal', + 'https://github.com/doctrine/dbal/issues/4510', + 'OraclePlatform::getReservedKeywordsClass() is deprecated,' + . ' use OraclePlatform::createReservedKeywordsList() instead.', + ); + + return Keywords\OracleKeywords::class; + } + + /** + * {@inheritDoc} + */ + public function getBlobTypeDeclarationSQL(array $column) + { + return 'BLOB'; + } + + /** @deprecated The SQL used for schema introspection is an implementation detail and should not be relied upon. */ + public function getListTableCommentsSQL(string $table, ?string $database = null): string + { + $tableCommentsName = 'user_tab_comments'; + $ownerCondition = ''; + + if ($database !== null && $database !== '/') { + $tableCommentsName = 'all_tab_comments'; + $ownerCondition = ' AND owner = ' . $this->quoteStringLiteral( + $this->normalizeIdentifier($database)->getName(), + ); + } + + return sprintf( + <<<'SQL' +SELECT comments FROM %s WHERE table_name = %s%s +SQL + , + $tableCommentsName, + $this->quoteStringLiteral($this->normalizeIdentifier($table)->getName()), + $ownerCondition, + ); + } + + public function createSchemaManager(Connection $connection): OracleSchemaManager + { + return new OracleSchemaManager($connection, $this); + } +} diff --git a/3rdparty/doctrine/dbal/src/Platforms/PostgreSQL100Platform.php b/3rdparty/doctrine/dbal/src/Platforms/PostgreSQL100Platform.php new file mode 100644 index 00000000..6ee1f90f --- /dev/null +++ b/3rdparty/doctrine/dbal/src/Platforms/PostgreSQL100Platform.php @@ -0,0 +1,36 @@ + [ + 't', + 'true', + 'y', + 'yes', + 'on', + '1', + ], + 'false' => [ + 'f', + 'false', + 'n', + 'no', + 'off', + '0', + ], + ]; + + /** + * PostgreSQL has different behavior with some drivers + * with regard to how booleans have to be handled. + * + * Enables use of 'true'/'false' or otherwise 1 and 0 instead. + * + * @param bool $flag + * + * @return void + */ + public function setUseBooleanTrueFalseStrings($flag) + { + $this->useBooleanTrueFalseStrings = (bool) $flag; + } + + /** + * {@inheritDoc} + */ + public function getSubstringExpression($string, $start, $length = null) + { + if ($length === null) { + return 'SUBSTRING(' . $string . ' FROM ' . $start . ')'; + } + + return 'SUBSTRING(' . $string . ' FROM ' . $start . ' FOR ' . $length . ')'; + } + + /** + * {@inheritDoc} + * + * @deprecated Generate dates within the application. + */ + public function getNowExpression() + { + Deprecation::trigger( + 'doctrine/dbal', + 'https://github.com/doctrine/dbal/pull/4753', + 'PostgreSQLPlatform::getNowExpression() is deprecated. Generate dates within the application.', + ); + + return 'LOCALTIMESTAMP(0)'; + } + + /** + * {@inheritDoc} + */ + public function getRegexpExpression() + { + return 'SIMILAR TO'; + } + + /** + * {@inheritDoc} + */ + public function getLocateExpression($str, $substr, $startPos = false) + { + if ($startPos !== false) { + $str = $this->getSubstringExpression($str, $startPos); + + return 'CASE WHEN (POSITION(' . $substr . ' IN ' . $str . ') = 0) THEN 0' + . ' ELSE (POSITION(' . $substr . ' IN ' . $str . ') + ' . $startPos . ' - 1) END'; + } + + return 'POSITION(' . $substr . ' IN ' . $str . ')'; + } + + /** + * {@inheritDoc} + */ + protected function getDateArithmeticIntervalExpression($date, $operator, $interval, $unit) + { + if ($unit === DateIntervalUnit::QUARTER) { + $interval = $this->multiplyInterval((string) $interval, 3); + $unit = DateIntervalUnit::MONTH; + } + + return '(' . $date . ' ' . $operator . ' (' . $interval . " || ' " . $unit . "')::interval)"; + } + + /** + * {@inheritDoc} + */ + public function getDateDiffExpression($date1, $date2) + { + return '(DATE(' . $date1 . ')-DATE(' . $date2 . '))'; + } + + public function getCurrentDatabaseExpression(): string + { + return 'CURRENT_DATABASE()'; + } + + /** + * {@inheritDoc} + */ + public function supportsSequences() + { + return true; + } + + /** + * {@inheritDoc} + */ + public function supportsSchemas() + { + return true; + } + + /** + * {@inheritDoc} + * + * @deprecated + */ + public function getDefaultSchemaName() + { + Deprecation::trigger( + 'doctrine/dbal', + 'https://github.com/doctrine/dbal/pull/5513', + '%s is deprecated.', + __METHOD__, + ); + + return 'public'; + } + + /** + * {@inheritDoc} + */ + public function supportsIdentityColumns() + { + return true; + } + + /** + * {@inheritDoc} + * + * @internal The method should be only used from within the {@see AbstractPlatform} class hierarchy. + */ + public function supportsPartialIndexes() + { + return true; + } + + /** + * {@inheritDoc} + * + * @deprecated + */ + public function usesSequenceEmulatedIdentityColumns() + { + Deprecation::trigger( + 'doctrine/dbal', + 'https://github.com/doctrine/dbal/pull/5513', + '%s is deprecated.', + __METHOD__, + ); + + return true; + } + + /** + * {@inheritDoc} + * + * @deprecated + */ + public function getIdentitySequenceName($tableName, $columnName) + { + Deprecation::triggerIfCalledFromOutside( + 'doctrine/dbal', + 'https://github.com/doctrine/dbal/pull/5513', + '%s is deprecated.', + __METHOD__, + ); + + return $tableName . '_' . $columnName . '_seq'; + } + + /** + * {@inheritDoc} + * + * @internal The method should be only used from within the {@see AbstractPlatform} class hierarchy. + */ + public function supportsCommentOnStatement() + { + return true; + } + + /** + * {@inheritDoc} + * + * @deprecated + */ + public function hasNativeGuidType() + { + Deprecation::triggerIfCalledFromOutside( + 'doctrine/dbal', + 'https://github.com/doctrine/dbal/pull/5509', + '%s is deprecated.', + __METHOD__, + ); + + return true; + } + + public function createSelectSQLBuilder(): SelectSQLBuilder + { + return new DefaultSelectSQLBuilder($this, 'FOR UPDATE', null); + } + + /** + * {@inheritDoc} + * + * @internal The method should be only used from within the {@see AbstractSchemaManager} class hierarchy. + */ + public function getListDatabasesSQL() + { + return 'SELECT datname FROM pg_database'; + } + + /** + * {@inheritDoc} + * + * @deprecated Use {@see PostgreSQLSchemaManager::listSchemaNames()} instead. + */ + public function getListNamespacesSQL() + { + Deprecation::triggerIfCalledFromOutside( + 'doctrine/dbal', + 'https://github.com/doctrine/dbal/issues/4503', + 'PostgreSQLPlatform::getListNamespacesSQL() is deprecated,' + . ' use PostgreSQLSchemaManager::listSchemaNames() instead.', + ); + + return "SELECT schema_name AS nspname + FROM information_schema.schemata + WHERE schema_name NOT LIKE 'pg\_%' + AND schema_name != 'information_schema'"; + } + + /** + * {@inheritDoc} + * + * @internal The method should be only used from within the {@see AbstractSchemaManager} class hierarchy. + */ + public function getListSequencesSQL($database) + { + return 'SELECT sequence_name AS relname, + sequence_schema AS schemaname, + minimum_value AS min_value, + increment AS increment_by + FROM information_schema.sequences + WHERE sequence_catalog = ' . $this->quoteStringLiteral($database) . " + AND sequence_schema NOT LIKE 'pg\_%' + AND sequence_schema != 'information_schema'"; + } + + /** + * @deprecated The SQL used for schema introspection is an implementation detail and should not be relied upon. + * + * {@inheritDoc} + */ + public function getListTablesSQL() + { + return "SELECT quote_ident(table_name) AS table_name, + table_schema AS schema_name + FROM information_schema.tables + WHERE table_schema NOT LIKE 'pg\_%' + AND table_schema != 'information_schema' + AND table_name != 'geometry_columns' + AND table_name != 'spatial_ref_sys' + AND table_type != 'VIEW'"; + } + + /** + * {@inheritDoc} + * + * @internal The method should be only used from within the {@see AbstractSchemaManager} class hierarchy. + */ + public function getListViewsSQL($database) + { + return 'SELECT quote_ident(table_name) AS viewname, + table_schema AS schemaname, + view_definition AS definition + FROM information_schema.views + WHERE view_definition IS NOT NULL'; + } + + /** + * @deprecated The SQL used for schema introspection is an implementation detail and should not be relied upon. + * + * @param string $table + * @param string|null $database + * + * @return string + */ + public function getListTableForeignKeysSQL($table, $database = null) + { + return 'SELECT quote_ident(r.conname) as conname, pg_catalog.pg_get_constraintdef(r.oid, true) as condef + FROM pg_catalog.pg_constraint r + WHERE r.conrelid = + ( + SELECT c.oid + FROM pg_catalog.pg_class c, pg_catalog.pg_namespace n + WHERE ' . $this->getTableWhereClause($table) . " AND n.oid = c.relnamespace + ) + AND r.contype = 'f'"; + } + + /** + * @deprecated + * + * {@inheritDoc} + */ + public function getListTableConstraintsSQL($table) + { + $table = new Identifier($table); + $table = $this->quoteStringLiteral($table->getName()); + + return sprintf( + <<<'SQL' +SELECT + quote_ident(relname) as relname +FROM + pg_class +WHERE oid IN ( + SELECT indexrelid + FROM pg_index, pg_class + WHERE pg_class.relname = %s + AND pg_class.oid = pg_index.indrelid + AND (indisunique = 't' OR indisprimary = 't') + ) +SQL + , + $table, + ); + } + + /** + * @deprecated The SQL used for schema introspection is an implementation detail and should not be relied upon. + * + * {@inheritDoc} + */ + public function getListTableIndexesSQL($table, $database = null) + { + return 'SELECT quote_ident(relname) as relname, pg_index.indisunique, pg_index.indisprimary, + pg_index.indkey, pg_index.indrelid, + pg_get_expr(indpred, indrelid) AS where + FROM pg_class, pg_index + WHERE oid IN ( + SELECT indexrelid + FROM pg_index si, pg_class sc, pg_namespace sn + WHERE ' . $this->getTableWhereClause($table, 'sc', 'sn') . ' + AND sc.oid=si.indrelid AND sc.relnamespace = sn.oid + ) AND pg_index.indexrelid = oid'; + } + + /** + * @param string $table + * @param string $classAlias + * @param string $namespaceAlias + */ + private function getTableWhereClause($table, $classAlias = 'c', $namespaceAlias = 'n'): string + { + $whereClause = $namespaceAlias . ".nspname NOT IN ('pg_catalog', 'information_schema', 'pg_toast') AND "; + if (strpos($table, '.') !== false) { + [$schema, $table] = explode('.', $table); + $schema = $this->quoteStringLiteral($schema); + } else { + $schema = 'ANY(current_schemas(false))'; + } + + $table = new Identifier($table); + $table = $this->quoteStringLiteral($table->getName()); + + return $whereClause . sprintf( + '%s.relname = %s AND %s.nspname = %s', + $classAlias, + $table, + $namespaceAlias, + $schema, + ); + } + + /** + * @deprecated The SQL used for schema introspection is an implementation detail and should not be relied upon. + * + * {@inheritDoc} + */ + public function getListTableColumnsSQL($table, $database = null) + { + return "SELECT + a.attnum, + quote_ident(a.attname) AS field, + t.typname AS type, + format_type(a.atttypid, a.atttypmod) AS complete_type, + (SELECT tc.collcollate FROM pg_catalog.pg_collation tc WHERE tc.oid = a.attcollation) AS collation, + (SELECT t1.typname FROM pg_catalog.pg_type t1 WHERE t1.oid = t.typbasetype) AS domain_type, + (SELECT format_type(t2.typbasetype, t2.typtypmod) FROM + pg_catalog.pg_type t2 WHERE t2.typtype = 'd' AND t2.oid = a.atttypid) AS domain_complete_type, + a.attnotnull AS isnotnull, + (SELECT 't' + FROM pg_index + WHERE c.oid = pg_index.indrelid + AND pg_index.indkey[0] = a.attnum + AND pg_index.indisprimary = 't' + ) AS pri, + (SELECT pg_get_expr(adbin, adrelid) + FROM pg_attrdef + WHERE c.oid = pg_attrdef.adrelid + AND pg_attrdef.adnum=a.attnum + ) AS default, + (SELECT pg_description.description + FROM pg_description WHERE pg_description.objoid = c.oid AND a.attnum = pg_description.objsubid + ) AS comment + FROM pg_attribute a, pg_class c, pg_type t, pg_namespace n + WHERE " . $this->getTableWhereClause($table, 'c', 'n') . ' + AND a.attnum > 0 + AND a.attrelid = c.oid + AND a.atttypid = t.oid + AND n.oid = c.relnamespace + ORDER BY a.attnum'; + } + + /** + * {@inheritDoc} + * + * @internal The method should be only used from within the {@see AbstractPlatform} class hierarchy. + */ + public function getAdvancedForeignKeyOptionsSQL(ForeignKeyConstraint $foreignKey) + { + $query = ''; + + if ($foreignKey->hasOption('match')) { + $query .= ' MATCH ' . $foreignKey->getOption('match'); + } + + $query .= parent::getAdvancedForeignKeyOptionsSQL($foreignKey); + + if ($foreignKey->hasOption('deferrable') && $foreignKey->getOption('deferrable') !== false) { + $query .= ' DEFERRABLE'; + } else { + $query .= ' NOT DEFERRABLE'; + } + + if ( + ($foreignKey->hasOption('feferred') && $foreignKey->getOption('feferred') !== false) + || ($foreignKey->hasOption('deferred') && $foreignKey->getOption('deferred') !== false) + ) { + $query .= ' INITIALLY DEFERRED'; + } else { + $query .= ' INITIALLY IMMEDIATE'; + } + + return $query; + } + + /** + * {@inheritDoc} + */ + public function getAlterTableSQL(TableDiff $diff) + { + $sql = []; + $commentsSQL = []; + $columnSql = []; + + $table = $diff->getOldTable() ?? $diff->getName($this); + + $tableNameSQL = $table->getQuotedName($this); + + foreach ($diff->getAddedColumns() as $addedColumn) { + if ($this->onSchemaAlterTableAddColumn($addedColumn, $diff, $columnSql)) { + continue; + } + + $query = 'ADD ' . $this->getColumnDeclarationSQL( + $addedColumn->getQuotedName($this), + $addedColumn->toArray(), + ); + + $sql[] = 'ALTER TABLE ' . $tableNameSQL . ' ' . $query; + + $comment = $this->getColumnComment($addedColumn); + + if ($comment === null || $comment === '') { + continue; + } + + $commentsSQL[] = $this->getCommentOnColumnSQL( + $tableNameSQL, + $addedColumn->getQuotedName($this), + $comment, + ); + } + + foreach ($diff->getDroppedColumns() as $droppedColumn) { + if ($this->onSchemaAlterTableRemoveColumn($droppedColumn, $diff, $columnSql)) { + continue; + } + + $query = 'DROP ' . $droppedColumn->getQuotedName($this); + $sql[] = 'ALTER TABLE ' . $tableNameSQL . ' ' . $query; + } + + foreach ($diff->getModifiedColumns() as $columnDiff) { + if ($this->onSchemaAlterTableChangeColumn($columnDiff, $diff, $columnSql)) { + continue; + } + + if ($this->isUnchangedBinaryColumn($columnDiff)) { + continue; + } + + $oldColumn = $columnDiff->getOldColumn() ?? $columnDiff->getOldColumnName(); + $newColumn = $columnDiff->getNewColumn(); + + $oldColumnName = $oldColumn->getQuotedName($this); + + if ( + $columnDiff->hasTypeChanged() + || $columnDiff->hasPrecisionChanged() + || $columnDiff->hasScaleChanged() + || $columnDiff->hasFixedChanged() + ) { + $type = $newColumn->getType(); + + // SERIAL/BIGSERIAL are not "real" types and we can't alter a column to that type + $columnDefinition = $newColumn->toArray(); + $columnDefinition['autoincrement'] = false; + + // here was a server version check before, but DBAL API does not support this anymore. + $query = 'ALTER ' . $oldColumnName . ' TYPE ' . $type->getSQLDeclaration($columnDefinition, $this); + $sql[] = 'ALTER TABLE ' . $tableNameSQL . ' ' . $query; + } + + if ($columnDiff->hasDefaultChanged()) { + $defaultClause = $newColumn->getDefault() === null + ? ' DROP DEFAULT' + : ' SET' . $this->getDefaultValueDeclarationSQL($newColumn->toArray()); + + $query = 'ALTER ' . $oldColumnName . $defaultClause; + $sql[] = 'ALTER TABLE ' . $tableNameSQL . ' ' . $query; + } + + if ($columnDiff->hasNotNullChanged()) { + $query = 'ALTER ' . $oldColumnName . ' ' . ($newColumn->getNotnull() ? 'SET' : 'DROP') . ' NOT NULL'; + $sql[] = 'ALTER TABLE ' . $tableNameSQL . ' ' . $query; + } + + if ($columnDiff->hasAutoIncrementChanged()) { + if ($newColumn->getAutoincrement()) { + // add autoincrement + $seqName = $this->getIdentitySequenceName( + $table->getName(), + $oldColumnName, + ); + + $sql[] = 'CREATE SEQUENCE ' . $seqName; + $sql[] = "SELECT setval('" . $seqName . "', (SELECT MAX(" . $oldColumnName . ') FROM ' + . $tableNameSQL . '))'; + $query = 'ALTER ' . $oldColumnName . " SET DEFAULT nextval('" . $seqName . "')"; + } else { + // Drop autoincrement, but do NOT drop the sequence. It might be re-used by other tables or have + $query = 'ALTER ' . $oldColumnName . ' DROP DEFAULT'; + } + + $sql[] = 'ALTER TABLE ' . $tableNameSQL . ' ' . $query; + } + + $oldComment = $this->getOldColumnComment($columnDiff); + $newComment = $this->getColumnComment($newColumn); + + if ( + $columnDiff->hasCommentChanged() + || ($columnDiff->getOldColumn() !== null && $oldComment !== $newComment) + ) { + $commentsSQL[] = $this->getCommentOnColumnSQL( + $tableNameSQL, + $newColumn->getQuotedName($this), + $newComment, + ); + } + + if (! $columnDiff->hasLengthChanged()) { + continue; + } + + $query = 'ALTER ' . $oldColumnName . ' TYPE ' + . $newColumn->getType()->getSQLDeclaration($newColumn->toArray(), $this); + $sql[] = 'ALTER TABLE ' . $tableNameSQL . ' ' . $query; + } + + foreach ($diff->getRenamedColumns() as $oldColumnName => $column) { + if ($this->onSchemaAlterTableRenameColumn($oldColumnName, $column, $diff, $columnSql)) { + continue; + } + + $oldColumnName = new Identifier($oldColumnName); + + $sql[] = 'ALTER TABLE ' . $tableNameSQL . ' RENAME COLUMN ' . $oldColumnName->getQuotedName($this) + . ' TO ' . $column->getQuotedName($this); + } + + $tableSql = []; + + if (! $this->onSchemaAlterTable($diff, $tableSql)) { + $sql = array_merge($sql, $commentsSQL); + + $newName = $diff->getNewName(); + + if ($newName !== false) { + Deprecation::trigger( + 'doctrine/dbal', + 'https://github.com/doctrine/dbal/pull/5663', + 'Generation of "rename table" SQL using %s is deprecated. Use getRenameTableSQL() instead.', + __METHOD__, + ); + + $sql[] = sprintf( + 'ALTER TABLE %s RENAME TO %s', + $tableNameSQL, + $newName->getQuotedName($this), + ); + } + + $sql = array_merge( + $this->getPreAlterTableIndexForeignKeySQL($diff), + $sql, + $this->getPostAlterTableIndexForeignKeySQL($diff), + ); + } + + return array_merge($sql, $tableSql, $columnSql); + } + + /** + * Checks whether a given column diff is a logically unchanged binary type column. + * + * Used to determine whether a column alteration for a binary type column can be skipped. + * Doctrine's {@see BinaryType} and {@see BlobType} are mapped to the same database column type on this platform + * as this platform does not have a native VARBINARY/BINARY column type. Therefore the comparator + * might detect differences for binary type columns which do not have to be propagated + * to database as there actually is no difference at database level. + */ + private function isUnchangedBinaryColumn(ColumnDiff $columnDiff): bool + { + $newColumnType = $columnDiff->getNewColumn()->getType(); + + if (! $newColumnType instanceof BinaryType && ! $newColumnType instanceof BlobType) { + return false; + } + + $oldColumn = $columnDiff->getOldColumn() instanceof Column ? $columnDiff->getOldColumn() : null; + + if ($oldColumn !== null) { + $oldColumnType = $oldColumn->getType(); + + if (! $oldColumnType instanceof BinaryType && ! $oldColumnType instanceof BlobType) { + return false; + } + + return count(array_diff($columnDiff->changedProperties, ['type', 'length', 'fixed'])) === 0; + } + + if ($columnDiff->hasTypeChanged()) { + return false; + } + + return count(array_diff($columnDiff->changedProperties, ['length', 'fixed'])) === 0; + } + + /** + * {@inheritDoc} + */ + protected function getRenameIndexSQL($oldIndexName, Index $index, $tableName) + { + if (strpos($tableName, '.') !== false) { + [$schema] = explode('.', $tableName); + $oldIndexName = $schema . '.' . $oldIndexName; + } + + return ['ALTER INDEX ' . $oldIndexName . ' RENAME TO ' . $index->getQuotedName($this)]; + } + + /** + * {@inheritDoc} + * + * @internal The method should be only used from within the {@see AbstractPlatform} class hierarchy. + */ + public function getCommentOnColumnSQL($tableName, $columnName, $comment) + { + $tableName = new Identifier($tableName); + $columnName = new Identifier($columnName); + $comment = $comment === null ? 'NULL' : $this->quoteStringLiteral($comment); + + return sprintf( + 'COMMENT ON COLUMN %s.%s IS %s', + $tableName->getQuotedName($this), + $columnName->getQuotedName($this), + $comment, + ); + } + + /** + * {@inheritDoc} + */ + public function getCreateSequenceSQL(Sequence $sequence) + { + return 'CREATE SEQUENCE ' . $sequence->getQuotedName($this) . + ' INCREMENT BY ' . $sequence->getAllocationSize() . + ' MINVALUE ' . $sequence->getInitialValue() . + ' START ' . $sequence->getInitialValue() . + $this->getSequenceCacheSQL($sequence); + } + + /** + * {@inheritDoc} + */ + public function getAlterSequenceSQL(Sequence $sequence) + { + return 'ALTER SEQUENCE ' . $sequence->getQuotedName($this) . + ' INCREMENT BY ' . $sequence->getAllocationSize() . + $this->getSequenceCacheSQL($sequence); + } + + /** + * Cache definition for sequences + */ + private function getSequenceCacheSQL(Sequence $sequence): string + { + if ($sequence->getCache() > 1) { + return ' CACHE ' . $sequence->getCache(); + } + + return ''; + } + + /** + * {@inheritDoc} + */ + public function getDropSequenceSQL($sequence) + { + return parent::getDropSequenceSQL($sequence) . ' CASCADE'; + } + + /** + * {@inheritDoc} + */ + public function getDropForeignKeySQL($foreignKey, $table) + { + return $this->getDropConstraintSQL($foreignKey, $table); + } + + /** + * {@inheritDoc} + */ + public function getDropIndexSQL($index, $table = null) + { + if ($index instanceof Index && $index->isPrimary() && $table !== null) { + $constraintName = $index->getName() === 'primary' ? $this->tableName($table) . '_pkey' : $index->getName(); + + return $this->getDropConstraintSQL($constraintName, $table); + } + + if ($index === '"primary"' && $table !== null) { + $constraintName = $this->tableName($table) . '_pkey'; + + return $this->getDropConstraintSQL($constraintName, $table); + } + + if ($table !== null) { + $indexName = $index instanceof Index ? $index->getQuotedName($this) : $index; + $tableName = $table instanceof Table ? $table->getQuotedName($this) : $table; + + if (strpos($tableName, '.') !== false) { + [$schema] = explode('.', $tableName); + $index = $schema . '.' . $indexName; + } + } + + return parent::getDropIndexSQL($index, $table); + } + + /** + * @param Table|string|null $table + * + * @return string + */ + private function tableName($table) + { + return $table instanceof Table ? $table->getName() : (string) $table; + } + + /** + * {@inheritDoc} + */ + protected function _getCreateTableSQL($name, array $columns, array $options = []) + { + $queryFields = $this->getColumnDeclarationListSQL($columns); + + if (isset($options['primary']) && ! empty($options['primary'])) { + $keyColumns = array_unique(array_values($options['primary'])); + $queryFields .= ', PRIMARY KEY(' . implode(', ', $keyColumns) . ')'; + } + + $unlogged = isset($options['unlogged']) && $options['unlogged'] === true ? ' UNLOGGED' : ''; + + $query = 'CREATE' . $unlogged . ' TABLE ' . $name . ' (' . $queryFields . ')'; + + $sql = [$query]; + + if (isset($options['indexes']) && ! empty($options['indexes'])) { + foreach ($options['indexes'] as $index) { + $sql[] = $this->getCreateIndexSQL($index, $name); + } + } + + if (isset($options['uniqueConstraints'])) { + foreach ($options['uniqueConstraints'] as $uniqueConstraint) { + $sql[] = $this->getCreateConstraintSQL($uniqueConstraint, $name); + } + } + + if (isset($options['foreignKeys'])) { + foreach ($options['foreignKeys'] as $definition) { + $sql[] = $this->getCreateForeignKeySQL($definition, $name); + } + } + + return $sql; + } + + /** + * Converts a single boolean value. + * + * First converts the value to its native PHP boolean type + * and passes it to the given callback function to be reconverted + * into any custom representation. + * + * @param mixed $value The value to convert. + * @param callable $callback The callback function to use for converting the real boolean value. + * + * @return mixed + * + * @throws UnexpectedValueException + */ + private function convertSingleBooleanValue($value, $callback) + { + if ($value === null) { + return $callback(null); + } + + if (is_bool($value) || is_numeric($value)) { + return $callback((bool) $value); + } + + if (! is_string($value)) { + return $callback(true); + } + + /** + * Better safe than sorry: http://php.net/in_array#106319 + */ + if (in_array(strtolower(trim($value)), $this->booleanLiterals['false'], true)) { + return $callback(false); + } + + if (in_array(strtolower(trim($value)), $this->booleanLiterals['true'], true)) { + return $callback(true); + } + + throw new UnexpectedValueException(sprintf("Unrecognized boolean literal '%s'", $value)); + } + + /** + * Converts one or multiple boolean values. + * + * First converts the value(s) to their native PHP boolean type + * and passes them to the given callback function to be reconverted + * into any custom representation. + * + * @param mixed $item The value(s) to convert. + * @param callable $callback The callback function to use for converting the real boolean value(s). + * + * @return mixed + */ + private function doConvertBooleans($item, $callback) + { + if (is_array($item)) { + foreach ($item as $key => $value) { + $item[$key] = $this->convertSingleBooleanValue($value, $callback); + } + + return $item; + } + + return $this->convertSingleBooleanValue($item, $callback); + } + + /** + * {@inheritDoc} + * + * Postgres wants boolean values converted to the strings 'true'/'false'. + */ + public function convertBooleans($item) + { + if (! $this->useBooleanTrueFalseStrings) { + return parent::convertBooleans($item); + } + + return $this->doConvertBooleans( + $item, + /** @param mixed $value */ + static function ($value) { + if ($value === null) { + return 'NULL'; + } + + return $value === true ? 'true' : 'false'; + }, + ); + } + + /** + * {@inheritDoc} + */ + public function convertBooleansToDatabaseValue($item) + { + if (! $this->useBooleanTrueFalseStrings) { + return parent::convertBooleansToDatabaseValue($item); + } + + return $this->doConvertBooleans( + $item, + /** @param mixed $value */ + static function ($value): ?int { + return $value === null ? null : (int) $value; + }, + ); + } + + /** + * {@inheritDoc} + * + * @param T $item + * + * @return (T is null ? null : bool) + * + * @template T + */ + public function convertFromBoolean($item) + { + if ($item !== null && in_array(strtolower($item), $this->booleanLiterals['false'], true)) { + return false; + } + + return parent::convertFromBoolean($item); + } + + /** + * {@inheritDoc} + */ + public function getSequenceNextValSQL($sequence) + { + return "SELECT NEXTVAL('" . $sequence . "')"; + } + + /** + * {@inheritDoc} + */ + public function getSetTransactionIsolationSQL($level) + { + return 'SET SESSION CHARACTERISTICS AS TRANSACTION ISOLATION LEVEL ' + . $this->_getTransactionIsolationLevelSQL($level); + } + + /** + * {@inheritDoc} + */ + public function getBooleanTypeDeclarationSQL(array $column) + { + return 'BOOLEAN'; + } + + /** + * {@inheritDoc} + */ + public function getIntegerTypeDeclarationSQL(array $column) + { + if (! empty($column['autoincrement'])) { + return 'SERIAL'; + } + + return 'INT'; + } + + /** + * {@inheritDoc} + */ + public function getBigIntTypeDeclarationSQL(array $column) + { + if (! empty($column['autoincrement'])) { + return 'BIGSERIAL'; + } + + return 'BIGINT'; + } + + /** + * {@inheritDoc} + */ + public function getSmallIntTypeDeclarationSQL(array $column) + { + if (! empty($column['autoincrement'])) { + return 'SMALLSERIAL'; + } + + return 'SMALLINT'; + } + + /** + * {@inheritDoc} + */ + public function getGuidTypeDeclarationSQL(array $column) + { + return 'UUID'; + } + + /** + * {@inheritDoc} + */ + public function getDateTimeTypeDeclarationSQL(array $column) + { + return 'TIMESTAMP(0) WITHOUT TIME ZONE'; + } + + /** + * {@inheritDoc} + */ + public function getDateTimeTzTypeDeclarationSQL(array $column) + { + return 'TIMESTAMP(0) WITH TIME ZONE'; + } + + /** + * {@inheritDoc} + */ + public function getDateTypeDeclarationSQL(array $column) + { + return 'DATE'; + } + + /** + * {@inheritDoc} + */ + public function getTimeTypeDeclarationSQL(array $column) + { + return 'TIME(0) WITHOUT TIME ZONE'; + } + + /** + * {@inheritDoc} + */ + protected function _getCommonIntegerTypeDeclarationSQL(array $column) + { + return ''; + } + + /** + * {@inheritDoc} + */ + protected function getVarcharTypeDeclarationSQLSnippet($length, $fixed) + { + return $fixed ? ($length > 0 ? 'CHAR(' . $length . ')' : 'CHAR(255)') + : ($length > 0 ? 'VARCHAR(' . $length . ')' : 'VARCHAR(255)'); + } + + /** + * {@inheritDoc} + */ + protected function getBinaryTypeDeclarationSQLSnippet($length, $fixed) + { + return 'BYTEA'; + } + + /** + * {@inheritDoc} + */ + public function getClobTypeDeclarationSQL(array $column) + { + return 'TEXT'; + } + + /** + * {@inheritDoc} + */ + public function getName() + { + Deprecation::triggerIfCalledFromOutside( + 'doctrine/dbal', + 'https://github.com/doctrine/dbal/issues/4749', + 'PostgreSQLPlatform::getName() is deprecated. Identify platforms by their class.', + ); + + return 'postgresql'; + } + + /** + * {@inheritDoc} + */ + public function getDateTimeTzFormatString() + { + return 'Y-m-d H:i:sO'; + } + + /** + * {@inheritDoc} + */ + public function getEmptyIdentityInsertSQL($quotedTableName, $quotedIdentifierColumnName) + { + return 'INSERT INTO ' . $quotedTableName . ' (' . $quotedIdentifierColumnName . ') VALUES (DEFAULT)'; + } + + /** + * {@inheritDoc} + */ + public function getTruncateTableSQL($tableName, $cascade = false) + { + $tableIdentifier = new Identifier($tableName); + $sql = 'TRUNCATE ' . $tableIdentifier->getQuotedName($this); + + if ($cascade) { + $sql .= ' CASCADE'; + } + + return $sql; + } + + /** + * Get the snippet used to retrieve the default value for a given column + */ + public function getDefaultColumnValueSQLSnippet(): string + { + return <<<'SQL' + SELECT pg_get_expr(adbin, adrelid) + FROM pg_attrdef + WHERE c.oid = pg_attrdef.adrelid + AND pg_attrdef.adnum=a.attnum + SQL; + } + + /** + * {@inheritDoc} + */ + public function getReadLockSQL() + { + return 'FOR SHARE'; + } + + /** + * {@inheritDoc} + */ + protected function initializeDoctrineTypeMappings() + { + $this->doctrineTypeMapping = [ + 'bigint' => Types::BIGINT, + 'bigserial' => Types::BIGINT, + 'bool' => Types::BOOLEAN, + 'boolean' => Types::BOOLEAN, + 'bpchar' => Types::STRING, + 'bytea' => Types::BLOB, + 'char' => Types::STRING, + 'date' => Types::DATE_MUTABLE, + 'datetime' => Types::DATETIME_MUTABLE, + 'decimal' => Types::DECIMAL, + 'double' => Types::FLOAT, + 'double precision' => Types::FLOAT, + 'float' => Types::FLOAT, + 'float4' => Types::FLOAT, + 'float8' => Types::FLOAT, + 'inet' => Types::STRING, + 'int' => Types::INTEGER, + 'int2' => Types::SMALLINT, + 'int4' => Types::INTEGER, + 'int8' => Types::BIGINT, + 'integer' => Types::INTEGER, + 'interval' => Types::STRING, + 'json' => Types::JSON, + 'jsonb' => Types::JSON, + 'money' => Types::DECIMAL, + 'numeric' => Types::DECIMAL, + 'serial' => Types::INTEGER, + 'serial4' => Types::INTEGER, + 'serial8' => Types::BIGINT, + 'real' => Types::FLOAT, + 'smallint' => Types::SMALLINT, + 'text' => Types::TEXT, + 'time' => Types::TIME_MUTABLE, + 'timestamp' => Types::DATETIME_MUTABLE, + 'timestamptz' => Types::DATETIMETZ_MUTABLE, + 'timetz' => Types::TIME_MUTABLE, + 'tsvector' => Types::TEXT, + 'uuid' => Types::GUID, + 'varchar' => Types::STRING, + 'year' => Types::DATE_MUTABLE, + '_varchar' => Types::STRING, + ]; + } + + /** + * {@inheritDoc} + * + * @deprecated + */ + public function getVarcharMaxLength() + { + Deprecation::triggerIfCalledFromOutside( + 'doctrine/dbal', + 'https://github.com/doctrine/dbal/issues/3263', + 'PostgreSQLPlatform::getVarcharMaxLength() is deprecated.', + ); + + return 65535; + } + + /** + * {@inheritDoc} + */ + public function getBinaryMaxLength() + { + Deprecation::triggerIfCalledFromOutside( + 'doctrine/dbal', + 'https://github.com/doctrine/dbal/issues/3263', + 'PostgreSQLPlatform::getBinaryMaxLength() is deprecated.', + ); + + return 0; + } + + /** + * {@inheritDoc} + * + * @deprecated + */ + public function getBinaryDefaultLength() + { + Deprecation::trigger( + 'doctrine/dbal', + 'https://github.com/doctrine/dbal/issues/3263', + 'Relying on the default binary column length is deprecated, specify the length explicitly.', + ); + + return 0; + } + + /** + * {@inheritDoc} + * + * @deprecated + */ + public function hasNativeJsonType() + { + Deprecation::triggerIfCalledFromOutside( + 'doctrine/dbal', + 'https://github.com/doctrine/dbal/pull/5509', + '%s is deprecated.', + __METHOD__, + ); + + return true; + } + + /** + * {@inheritDoc} + * + * @deprecated Implement {@see createReservedKeywordsList()} instead. + */ + protected function getReservedKeywordsClass() + { + Deprecation::triggerIfCalledFromOutside( + 'doctrine/dbal', + 'https://github.com/doctrine/dbal/issues/4510', + 'PostgreSQLPlatform::getReservedKeywordsClass() is deprecated,' + . ' use PostgreSQLPlatform::createReservedKeywordsList() instead.', + ); + + return Keywords\PostgreSQL94Keywords::class; + } + + /** + * {@inheritDoc} + */ + public function getBlobTypeDeclarationSQL(array $column) + { + return 'BYTEA'; + } + + /** + * {@inheritDoc} + * + * @internal The method should be only used from within the {@see AbstractPlatform} class hierarchy. + */ + public function getDefaultValueDeclarationSQL($column) + { + if (isset($column['autoincrement']) && $column['autoincrement'] === true) { + return ''; + } + + return parent::getDefaultValueDeclarationSQL($column); + } + + /** + * {@inheritDoc} + * + * @internal The method should be only used from within the {@see AbstractPlatform} class hierarchy. + */ + public function supportsColumnCollation() + { + return true; + } + + /** + * {@inheritDoc} + */ + public function getJsonTypeDeclarationSQL(array $column) + { + if (! empty($column['jsonb'])) { + return 'JSONB'; + } + + return 'JSON'; + } + + private function getOldColumnComment(ColumnDiff $columnDiff): ?string + { + $oldColumn = $columnDiff->getOldColumn(); + + if ($oldColumn !== null) { + return $this->getColumnComment($oldColumn); + } + + return null; + } + + /** @deprecated The SQL used for schema introspection is an implementation detail and should not be relied upon. */ + public function getListTableMetadataSQL(string $table, ?string $schema = null): string + { + if ($schema !== null) { + $table = $schema . '.' . $table; + } + + return sprintf( + <<<'SQL' +SELECT obj_description(%s::regclass) AS table_comment; +SQL + , + $this->quoteStringLiteral($table), + ); + } + + public function createSchemaManager(Connection $connection): PostgreSQLSchemaManager + { + return new PostgreSQLSchemaManager($connection, $this); + } +} diff --git a/3rdparty/doctrine/dbal/src/Platforms/SQLServer/Comparator.php b/3rdparty/doctrine/dbal/src/Platforms/SQLServer/Comparator.php new file mode 100644 index 00000000..41ef422b --- /dev/null +++ b/3rdparty/doctrine/dbal/src/Platforms/SQLServer/Comparator.php @@ -0,0 +1,63 @@ +databaseCollation = $databaseCollation; + } + + public function compareTables(Table $fromTable, Table $toTable): TableDiff + { + return parent::compareTables( + $this->normalizeColumns($fromTable), + $this->normalizeColumns($toTable), + ); + } + + /** + * {@inheritDoc} + */ + public function diffTable(Table $fromTable, Table $toTable) + { + return parent::diffTable( + $this->normalizeColumns($fromTable), + $this->normalizeColumns($toTable), + ); + } + + private function normalizeColumns(Table $table): Table + { + $table = clone $table; + + foreach ($table->getColumns() as $column) { + $options = $column->getPlatformOptions(); + + if (! isset($options['collation']) || $options['collation'] !== $this->databaseCollation) { + continue; + } + + unset($options['collation']); + $column->setPlatformOptions($options); + } + + return $table; + } +} diff --git a/3rdparty/doctrine/dbal/src/Platforms/SQLServer/SQL/Builder/SQLServerSelectSQLBuilder.php b/3rdparty/doctrine/dbal/src/Platforms/SQLServer/SQL/Builder/SQLServerSelectSQLBuilder.php new file mode 100644 index 00000000..dff12221 --- /dev/null +++ b/3rdparty/doctrine/dbal/src/Platforms/SQLServer/SQL/Builder/SQLServerSelectSQLBuilder.php @@ -0,0 +1,86 @@ +platform = $platform; + } + + public function buildSQL(SelectQuery $query): string + { + $parts = ['SELECT']; + + if ($query->isDistinct()) { + $parts[] = 'DISTINCT'; + } + + $parts[] = implode(', ', $query->getColumns()); + + $from = $query->getFrom(); + + if (count($from) > 0) { + $parts[] = 'FROM ' . implode(', ', $from); + } + + $forUpdate = $query->getForUpdate(); + + if ($forUpdate !== null) { + $with = ['UPDLOCK', 'ROWLOCK']; + + if ($forUpdate->getConflictResolutionMode() === ConflictResolutionMode::SKIP_LOCKED) { + $with[] = 'READPAST'; + } + + $parts[] = 'WITH (' . implode(', ', $with) . ')'; + } + + $where = $query->getWhere(); + + if ($where !== null) { + $parts[] = 'WHERE ' . $where; + } + + $groupBy = $query->getGroupBy(); + + if (count($groupBy) > 0) { + $parts[] = 'GROUP BY ' . implode(', ', $groupBy); + } + + $having = $query->getHaving(); + + if ($having !== null) { + $parts[] = 'HAVING ' . $having; + } + + $orderBy = $query->getOrderBy(); + + if (count($orderBy) > 0) { + $parts[] = 'ORDER BY ' . implode(', ', $orderBy); + } + + $sql = implode(' ', $parts); + $limit = $query->getLimit(); + + if ($limit->isDefined()) { + $sql = $this->platform->modifyLimitQuery($sql, $limit->getMaxResults(), $limit->getFirstResult()); + } + + return $sql; + } +} diff --git a/3rdparty/doctrine/dbal/src/Platforms/SQLServer2012Platform.php b/3rdparty/doctrine/dbal/src/Platforms/SQLServer2012Platform.php new file mode 100644 index 00000000..a8ba2fa0 --- /dev/null +++ b/3rdparty/doctrine/dbal/src/Platforms/SQLServer2012Platform.php @@ -0,0 +1,13 @@ +getConvertExpression('date', 'GETDATE()'); + } + + /** + * {@inheritDoc} + */ + public function getCurrentTimeSQL() + { + return $this->getConvertExpression('time', 'GETDATE()'); + } + + /** + * Returns an expression that converts an expression of one data type to another. + * + * @param string $dataType The target native data type. Alias data types cannot be used. + * @param string $expression The SQL expression to convert. + */ + private function getConvertExpression($dataType, $expression): string + { + return sprintf('CONVERT(%s, %s)', $dataType, $expression); + } + + /** + * {@inheritDoc} + */ + protected function getDateArithmeticIntervalExpression($date, $operator, $interval, $unit) + { + $factorClause = ''; + + if ($operator === '-') { + $factorClause = '-1 * '; + } + + return 'DATEADD(' . $unit . ', ' . $factorClause . $interval . ', ' . $date . ')'; + } + + /** + * {@inheritDoc} + */ + public function getDateDiffExpression($date1, $date2) + { + return 'DATEDIFF(day, ' . $date2 . ',' . $date1 . ')'; + } + + /** + * {@inheritDoc} + * + * Microsoft SQL Server prefers "autoincrement" identity columns + * since sequences can only be emulated with a table. + * + * @deprecated + */ + public function prefersIdentityColumns() + { + Deprecation::trigger( + 'doctrine/dbal', + 'https://github.com/doctrine/dbal/pull/1519', + 'SQLServerPlatform::prefersIdentityColumns() is deprecated.', + ); + + return true; + } + + /** + * {@inheritDoc} + * + * Microsoft SQL Server supports this through AUTO_INCREMENT columns. + */ + public function supportsIdentityColumns() + { + return true; + } + + /** + * {@inheritDoc} + */ + public function supportsReleaseSavepoints() + { + return false; + } + + /** + * {@inheritDoc} + */ + public function supportsSchemas() + { + return true; + } + + /** + * {@inheritDoc} + * + * @deprecated + */ + public function getDefaultSchemaName() + { + Deprecation::trigger( + 'doctrine/dbal', + 'https://github.com/doctrine/dbal/pull/5513', + '%s is deprecated.', + __METHOD__, + ); + + return 'dbo'; + } + + /** + * {@inheritDoc} + * + * @internal The method should be only used from within the {@see AbstractPlatform} class hierarchy. + */ + public function supportsColumnCollation() + { + return true; + } + + public function supportsSequences(): bool + { + return true; + } + + public function getAlterSequenceSQL(Sequence $sequence): string + { + return 'ALTER SEQUENCE ' . $sequence->getQuotedName($this) . + ' INCREMENT BY ' . $sequence->getAllocationSize(); + } + + public function getCreateSequenceSQL(Sequence $sequence): string + { + return 'CREATE SEQUENCE ' . $sequence->getQuotedName($this) . + ' START WITH ' . $sequence->getInitialValue() . + ' INCREMENT BY ' . $sequence->getAllocationSize() . + ' MINVALUE ' . $sequence->getInitialValue(); + } + + /** + * {@inheritDoc} + * + * @internal The method should be only used from within the {@see AbstractSchemaManager} class hierarchy. + */ + public function getListSequencesSQL($database) + { + return 'SELECT seq.name, + CAST( + seq.increment AS VARCHAR(MAX) + ) AS increment, -- CAST avoids driver error for sql_variant type + CAST( + seq.start_value AS VARCHAR(MAX) + ) AS start_value -- CAST avoids driver error for sql_variant type + FROM sys.sequences AS seq'; + } + + /** + * {@inheritDoc} + */ + public function getSequenceNextValSQL($sequence) + { + return 'SELECT NEXT VALUE FOR ' . $sequence; + } + + /** + * {@inheritDoc} + * + * @deprecated + */ + public function hasNativeGuidType() + { + Deprecation::triggerIfCalledFromOutside( + 'doctrine/dbal', + 'https://github.com/doctrine/dbal/pull/5509', + '%s is deprecated.', + __METHOD__, + ); + + return true; + } + + /** + * {@inheritDoc} + */ + public function getDropForeignKeySQL($foreignKey, $table) + { + if (! $foreignKey instanceof ForeignKeyConstraint) { + $foreignKey = new Identifier($foreignKey); + } + + if (! $table instanceof Table) { + $table = new Identifier($table); + } + + $foreignKey = $foreignKey->getQuotedName($this); + $table = $table->getQuotedName($this); + + return 'ALTER TABLE ' . $table . ' DROP CONSTRAINT ' . $foreignKey; + } + + /** + * {@inheritDoc} + */ + public function getDropIndexSQL($index, $table = null) + { + if ($index instanceof Index) { + Deprecation::trigger( + 'doctrine/dbal', + 'https://github.com/doctrine/dbal/issues/4798', + 'Passing $index as an Index object to %s is deprecated. Pass it as a quoted name instead.', + __METHOD__, + ); + + $index = $index->getQuotedName($this); + } elseif (! is_string($index)) { + throw new InvalidArgumentException( + __METHOD__ . '() expects $index parameter to be string or ' . Index::class . '.', + ); + } + + if ($table instanceof Table) { + Deprecation::trigger( + 'doctrine/dbal', + 'https://github.com/doctrine/dbal/issues/4798', + 'Passing $table as an Table object to %s is deprecated. Pass it as a quoted name instead.', + __METHOD__, + ); + + $table = $table->getQuotedName($this); + } elseif (! is_string($table)) { + throw new InvalidArgumentException( + __METHOD__ . '() expects $table parameter to be string or ' . Table::class . '.', + ); + } + + return 'DROP INDEX ' . $index . ' ON ' . $table; + } + + /** + * {@inheritDoc} + */ + protected function _getCreateTableSQL($name, array $columns, array $options = []) + { + $defaultConstraintsSql = []; + $commentsSql = []; + + $tableComment = $options['comment'] ?? null; + if ($tableComment !== null) { + $commentsSql[] = $this->getCommentOnTableSQL($name, $tableComment); + } + + // @todo does other code breaks because of this? + // force primary keys to be not null + foreach ($columns as &$column) { + if (! empty($column['primary'])) { + $column['notnull'] = true; + } + + // Build default constraints SQL statements. + if (isset($column['default'])) { + $defaultConstraintsSql[] = 'ALTER TABLE ' . $name . + ' ADD' . $this->getDefaultConstraintDeclarationSQL($name, $column); + } + + if (empty($column['comment']) && ! is_numeric($column['comment'])) { + continue; + } + + $commentsSql[] = $this->getCreateColumnCommentSQL($name, $column['name'], $column['comment']); + } + + $columnListSql = $this->getColumnDeclarationListSQL($columns); + + if (isset($options['uniqueConstraints']) && ! empty($options['uniqueConstraints'])) { + foreach ($options['uniqueConstraints'] as $constraintName => $definition) { + $columnListSql .= ', ' . $this->getUniqueConstraintDeclarationSQL($constraintName, $definition); + } + } + + if (isset($options['primary']) && ! empty($options['primary'])) { + $flags = ''; + if (isset($options['primary_index']) && $options['primary_index']->hasFlag('nonclustered')) { + $flags = ' NONCLUSTERED'; + } + + $columnListSql .= ', PRIMARY KEY' . $flags + . ' (' . implode(', ', array_unique(array_values($options['primary']))) . ')'; + } + + $query = 'CREATE TABLE ' . $name . ' (' . $columnListSql; + + $check = $this->getCheckDeclarationSQL($columns); + if (! empty($check)) { + $query .= ', ' . $check; + } + + $query .= ')'; + + $sql = [$query]; + + if (isset($options['indexes']) && ! empty($options['indexes'])) { + foreach ($options['indexes'] as $index) { + $sql[] = $this->getCreateIndexSQL($index, $name); + } + } + + if (isset($options['foreignKeys'])) { + foreach ($options['foreignKeys'] as $definition) { + $sql[] = $this->getCreateForeignKeySQL($definition, $name); + } + } + + return array_merge($sql, $commentsSql, $defaultConstraintsSql); + } + + /** + * {@inheritDoc} + */ + public function getCreatePrimaryKeySQL(Index $index, $table) + { + if ($table instanceof Table) { + Deprecation::trigger( + 'doctrine/dbal', + 'https://github.com/doctrine/dbal/issues/4798', + 'Passing $table as a Table object to %s is deprecated. Pass it as a quoted name instead.', + __METHOD__, + ); + + $identifier = $table->getQuotedName($this); + } else { + $identifier = $table; + } + + $sql = 'ALTER TABLE ' . $identifier . ' ADD PRIMARY KEY'; + + if ($index->hasFlag('nonclustered')) { + $sql .= ' NONCLUSTERED'; + } + + return $sql . ' (' . $this->getIndexFieldDeclarationListSQL($index) . ')'; + } + + private function unquoteSingleIdentifier(string $possiblyQuotedName): string + { + return str_starts_with($possiblyQuotedName, '[') && str_ends_with($possiblyQuotedName, ']') + ? substr($possiblyQuotedName, 1, -1) + : $possiblyQuotedName; + } + + /** + * Returns the SQL statement for creating a column comment. + * + * SQL Server does not support native column comments, + * therefore the extended properties functionality is used + * as a workaround to store them. + * The property name used to store column comments is "MS_Description" + * which provides compatibility with SQL Server Management Studio, + * as column comments are stored in the same property there when + * specifying a column's "Description" attribute. + * + * @param string $tableName The quoted table name to which the column belongs. + * @param string $columnName The quoted column name to create the comment for. + * @param string|null $comment The column's comment. + * + * @return string + */ + protected function getCreateColumnCommentSQL($tableName, $columnName, $comment) + { + if (strpos($tableName, '.') !== false) { + [$schemaName, $tableName] = explode('.', $tableName); + } else { + $schemaName = 'dbo'; + } + + return $this->getAddExtendedPropertySQL( + 'MS_Description', + $comment, + 'SCHEMA', + $this->quoteStringLiteral($this->unquoteSingleIdentifier($schemaName)), + 'TABLE', + $this->quoteStringLiteral($this->unquoteSingleIdentifier($tableName)), + 'COLUMN', + $this->quoteStringLiteral($this->unquoteSingleIdentifier($columnName)), + ); + } + + /** + * Returns the SQL snippet for declaring a default constraint. + * + * @internal The method should be only used from within the SQLServerPlatform class hierarchy. + * + * @param string $table Name of the table to return the default constraint declaration for. + * @param mixed[] $column Column definition. + * + * @return string + * + * @throws InvalidArgumentException + */ + public function getDefaultConstraintDeclarationSQL($table, array $column) + { + if (! isset($column['default'])) { + throw new InvalidArgumentException("Incomplete column definition. 'default' required."); + } + + $columnName = new Identifier($column['name']); + + return ' CONSTRAINT ' . + $this->generateDefaultConstraintName($table, $column['name']) . + $this->getDefaultValueDeclarationSQL($column) . + ' FOR ' . $columnName->getQuotedName($this); + } + + /** + * {@inheritDoc} + */ + public function getCreateIndexSQL(Index $index, $table) + { + $constraint = parent::getCreateIndexSQL($index, $table); + + if ($index->isUnique() && ! $index->isPrimary()) { + $constraint = $this->_appendUniqueConstraintDefinition($constraint, $index); + } + + return $constraint; + } + + /** + * {@inheritDoc} + */ + protected function getCreateIndexSQLFlags(Index $index) + { + $type = ''; + if ($index->isUnique()) { + $type .= 'UNIQUE '; + } + + if ($index->hasFlag('clustered')) { + $type .= 'CLUSTERED '; + } elseif ($index->hasFlag('nonclustered')) { + $type .= 'NONCLUSTERED '; + } + + return $type; + } + + /** + * Extend unique key constraint with required filters + * + * @param string $sql + */ + private function _appendUniqueConstraintDefinition($sql, Index $index): string + { + $fields = []; + + foreach ($index->getQuotedColumns($this) as $field) { + $fields[] = $field . ' IS NOT NULL'; + } + + return $sql . ' WHERE ' . implode(' AND ', $fields); + } + + /** + * {@inheritDoc} + */ + public function getAlterTableSQL(TableDiff $diff) + { + $queryParts = []; + $sql = []; + $columnSql = []; + $commentsSql = []; + + $table = $diff->getOldTable() ?? $diff->getName($this); + + $tableName = $table->getName(); + + foreach ($diff->getAddedColumns() as $column) { + if ($this->onSchemaAlterTableAddColumn($column, $diff, $columnSql)) { + continue; + } + + $columnProperties = $column->toArray(); + + $addColumnSql = 'ADD ' . $this->getColumnDeclarationSQL($column->getQuotedName($this), $columnProperties); + + if (isset($columnProperties['default'])) { + $addColumnSql .= ' CONSTRAINT ' . $this->generateDefaultConstraintName( + $tableName, + $column->getQuotedName($this), + ) . $this->getDefaultValueDeclarationSQL($columnProperties); + } + + $queryParts[] = $addColumnSql; + + $comment = $this->getColumnComment($column); + + if (empty($comment) && ! is_numeric($comment)) { + continue; + } + + $commentsSql[] = $this->getCreateColumnCommentSQL( + $tableName, + $column->getQuotedName($this), + $comment, + ); + } + + foreach ($diff->getDroppedColumns() as $column) { + if ($this->onSchemaAlterTableRemoveColumn($column, $diff, $columnSql)) { + continue; + } + + $queryParts[] = 'DROP COLUMN ' . $column->getQuotedName($this); + } + + foreach ($diff->getModifiedColumns() as $columnDiff) { + if ($this->onSchemaAlterTableChangeColumn($columnDiff, $diff, $columnSql)) { + continue; + } + + $newColumn = $columnDiff->getNewColumn(); + $newComment = $this->getColumnComment($newColumn); + $hasNewComment = ! empty($newComment) || is_numeric($newComment); + + $oldColumn = $columnDiff->getOldColumn(); + + if ($oldColumn instanceof Column) { + $oldComment = $this->getColumnComment($oldColumn); + $hasOldComment = ! empty($oldComment) || is_numeric($oldComment); + + if ($hasOldComment && $hasNewComment && $oldComment !== $newComment) { + $commentsSql[] = $this->getAlterColumnCommentSQL( + $tableName, + $newColumn->getQuotedName($this), + $newComment, + ); + } elseif ($hasOldComment && ! $hasNewComment) { + $commentsSql[] = $this->getDropColumnCommentSQL( + $tableName, + $newColumn->getQuotedName($this), + ); + } elseif (! $hasOldComment && $hasNewComment) { + $commentsSql[] = $this->getCreateColumnCommentSQL( + $tableName, + $newColumn->getQuotedName($this), + $newComment, + ); + } + } + + // Do not add query part if only comment has changed. + if ($columnDiff->hasCommentChanged() && count($columnDiff->changedProperties) === 1) { + continue; + } + + $requireDropDefaultConstraint = $this->alterColumnRequiresDropDefaultConstraint($columnDiff); + + if ($requireDropDefaultConstraint) { + $oldColumn = $columnDiff->getOldColumn(); + + if ($oldColumn !== null) { + $oldColumnName = $oldColumn->getName(); + } else { + $oldColumnName = $columnDiff->oldColumnName; + } + + $queryParts[] = $this->getAlterTableDropDefaultConstraintClause($tableName, $oldColumnName); + } + + $columnProperties = $newColumn->toArray(); + + $queryParts[] = 'ALTER COLUMN ' . + $this->getColumnDeclarationSQL($newColumn->getQuotedName($this), $columnProperties); + + if ( + ! isset($columnProperties['default']) + || (! $requireDropDefaultConstraint && ! $columnDiff->hasDefaultChanged()) + ) { + continue; + } + + $queryParts[] = $this->getAlterTableAddDefaultConstraintClause($tableName, $newColumn); + } + + $tableNameSQL = $table->getQuotedName($this); + + foreach ($diff->getRenamedColumns() as $oldColumnName => $newColumn) { + if ($this->onSchemaAlterTableRenameColumn($oldColumnName, $newColumn, $diff, $columnSql)) { + continue; + } + + $oldColumnName = new Identifier($oldColumnName); + + $sql[] = "sp_rename '" . $tableNameSQL . '.' . $oldColumnName->getQuotedName($this) . + "', '" . $newColumn->getQuotedName($this) . "', 'COLUMN'"; + + // Recreate default constraint with new column name if necessary (for future reference). + if ($newColumn->getDefault() === null) { + continue; + } + + $queryParts[] = $this->getAlterTableDropDefaultConstraintClause( + $tableName, + $oldColumnName->getQuotedName($this), + ); + $queryParts[] = $this->getAlterTableAddDefaultConstraintClause($tableName, $newColumn); + } + + $tableSql = []; + + if ($this->onSchemaAlterTable($diff, $tableSql)) { + return array_merge($tableSql, $columnSql); + } + + foreach ($queryParts as $query) { + $sql[] = 'ALTER TABLE ' . $tableNameSQL . ' ' . $query; + } + + $sql = array_merge($sql, $commentsSql); + + $newName = $diff->getNewName(); + + if ($newName !== false) { + Deprecation::trigger( + 'doctrine/dbal', + 'https://github.com/doctrine/dbal/pull/5663', + 'Generation of "rename table" SQL using %s is deprecated. Use getRenameTableSQL() instead.', + __METHOD__, + ); + + $sql = array_merge($sql, $this->getRenameTableSQL($tableName, $newName->getName())); + } + + $sql = array_merge( + $this->getPreAlterTableIndexForeignKeySQL($diff), + $sql, + $this->getPostAlterTableIndexForeignKeySQL($diff), + ); + + return array_merge($sql, $tableSql, $columnSql); + } + + /** + * {@inheritDoc} + */ + public function getRenameTableSQL(string $oldName, string $newName): array + { + return [ + sprintf('sp_rename %s, %s', $this->quoteStringLiteral($oldName), $this->quoteStringLiteral($newName)), + + /* Rename table's default constraints names + * to match the new table name. + * This is necessary to ensure that the default + * constraints can be referenced in future table + * alterations as the table name is encoded in + * default constraints' names. */ + sprintf( + <<<'SQL' + DECLARE @sql NVARCHAR(MAX) = N''; + SELECT @sql += N'EXEC sp_rename N''' + dc.name + ''', N''' + + REPLACE(dc.name, '%s', '%s') + ''', ''OBJECT'';' + FROM sys.default_constraints dc + JOIN sys.tables tbl + ON dc.parent_object_id = tbl.object_id + WHERE tbl.name = %s; + EXEC sp_executesql @sql + SQL, + $this->generateIdentifierName($oldName), + $this->generateIdentifierName($newName), + $this->quoteStringLiteral($newName), + ), + ]; + } + + /** + * Returns the SQL clause for adding a default constraint in an ALTER TABLE statement. + * + * @param string $tableName The name of the table to generate the clause for. + * @param Column $column The column to generate the clause for. + */ + private function getAlterTableAddDefaultConstraintClause($tableName, Column $column): string + { + $columnDef = $column->toArray(); + $columnDef['name'] = $column->getQuotedName($this); + + return 'ADD' . $this->getDefaultConstraintDeclarationSQL($tableName, $columnDef); + } + + /** + * Returns the SQL clause for dropping an existing default constraint in an ALTER TABLE statement. + * + * @param string $tableName The name of the table to generate the clause for. + * @param string $columnName The name of the column to generate the clause for. + */ + private function getAlterTableDropDefaultConstraintClause($tableName, $columnName): string + { + return 'DROP CONSTRAINT ' . $this->generateDefaultConstraintName($tableName, $columnName); + } + + /** + * Checks whether a column alteration requires dropping its default constraint first. + * + * Different to other database vendors SQL Server implements column default values + * as constraints and therefore changes in a column's default value as well as changes + * in a column's type require dropping the default constraint first before being to + * alter the particular column to the new definition. + */ + private function alterColumnRequiresDropDefaultConstraint(ColumnDiff $columnDiff): bool + { + $oldColumn = $columnDiff->getOldColumn(); + + // We can only decide whether to drop an existing default constraint + // if we know the original default value. + if (! $oldColumn instanceof Column) { + return false; + } + + // We only need to drop an existing default constraint if we know the + // column was defined with a default value before. + if ($oldColumn->getDefault() === null) { + return false; + } + + // We need to drop an existing default constraint if the column was + // defined with a default value before and it has changed. + if ($columnDiff->hasDefaultChanged()) { + return true; + } + + // We need to drop an existing default constraint if the column was + // defined with a default value before and the native column type has changed. + return $columnDiff->hasTypeChanged() || $columnDiff->hasFixedChanged(); + } + + /** + * Returns the SQL statement for altering a column comment. + * + * SQL Server does not support native column comments, + * therefore the extended properties functionality is used + * as a workaround to store them. + * The property name used to store column comments is "MS_Description" + * which provides compatibility with SQL Server Management Studio, + * as column comments are stored in the same property there when + * specifying a column's "Description" attribute. + * + * @param string $tableName The quoted table name to which the column belongs. + * @param string $columnName The quoted column name to alter the comment for. + * @param string|null $comment The column's comment. + * + * @return string + */ + protected function getAlterColumnCommentSQL($tableName, $columnName, $comment) + { + if (strpos($tableName, '.') !== false) { + [$schemaName, $tableName] = explode('.', $tableName); + } else { + $schemaName = 'dbo'; + } + + return $this->getUpdateExtendedPropertySQL( + 'MS_Description', + $comment, + 'SCHEMA', + $this->quoteStringLiteral($this->unquoteSingleIdentifier($schemaName)), + 'TABLE', + $this->quoteStringLiteral($this->unquoteSingleIdentifier($tableName)), + 'COLUMN', + $this->quoteStringLiteral($this->unquoteSingleIdentifier($columnName)), + ); + } + + /** + * Returns the SQL statement for dropping a column comment. + * + * SQL Server does not support native column comments, + * therefore the extended properties functionality is used + * as a workaround to store them. + * The property name used to store column comments is "MS_Description" + * which provides compatibility with SQL Server Management Studio, + * as column comments are stored in the same property there when + * specifying a column's "Description" attribute. + * + * @param string $tableName The quoted table name to which the column belongs. + * @param string $columnName The quoted column name to drop the comment for. + * + * @return string + */ + protected function getDropColumnCommentSQL($tableName, $columnName) + { + if (strpos($tableName, '.') !== false) { + [$schemaName, $tableName] = explode('.', $tableName); + } else { + $schemaName = 'dbo'; + } + + return $this->getDropExtendedPropertySQL( + 'MS_Description', + 'SCHEMA', + $this->quoteStringLiteral($this->unquoteSingleIdentifier($schemaName)), + 'TABLE', + $this->quoteStringLiteral($this->unquoteSingleIdentifier($tableName)), + 'COLUMN', + $this->quoteStringLiteral($this->unquoteSingleIdentifier($columnName)), + ); + } + + /** + * {@inheritDoc} + */ + protected function getRenameIndexSQL($oldIndexName, Index $index, $tableName) + { + return [sprintf( + "EXEC sp_rename N'%s.%s', N'%s', N'INDEX'", + $tableName, + $oldIndexName, + $index->getQuotedName($this), + ), + ]; + } + + /** + * Returns the SQL statement for adding an extended property to a database object. + * + * @internal The method should be only used from within the SQLServerPlatform class hierarchy. + * + * @link http://msdn.microsoft.com/en-us/library/ms180047%28v=sql.90%29.aspx + * + * @param string $name The name of the property to add. + * @param string|null $value The value of the property to add. + * @param string|null $level0Type The type of the object at level 0 the property belongs to. + * @param string|null $level0Name The name of the object at level 0 the property belongs to. + * @param string|null $level1Type The type of the object at level 1 the property belongs to. + * @param string|null $level1Name The name of the object at level 1 the property belongs to. + * @param string|null $level2Type The type of the object at level 2 the property belongs to. + * @param string|null $level2Name The name of the object at level 2 the property belongs to. + * + * @return string + */ + public function getAddExtendedPropertySQL( + $name, + $value = null, + $level0Type = null, + $level0Name = null, + $level1Type = null, + $level1Name = null, + $level2Type = null, + $level2Name = null + ) { + return 'EXEC sp_addextendedproperty ' . + 'N' . $this->quoteStringLiteral($name) . ', N' . $this->quoteStringLiteral($value ?? '') . ', ' . + 'N' . $this->quoteStringLiteral($level0Type ?? '') . ', ' . $level0Name . ', ' . + 'N' . $this->quoteStringLiteral($level1Type ?? '') . ', ' . $level1Name . + ($level2Type !== null || $level2Name !== null + ? ', N' . $this->quoteStringLiteral($level2Type ?? '') . ', ' . $level2Name + : '' + ); + } + + /** + * Returns the SQL statement for dropping an extended property from a database object. + * + * @internal The method should be only used from within the SQLServerPlatform class hierarchy. + * + * @link http://technet.microsoft.com/en-gb/library/ms178595%28v=sql.90%29.aspx + * + * @param string $name The name of the property to drop. + * @param string|null $level0Type The type of the object at level 0 the property belongs to. + * @param string|null $level0Name The name of the object at level 0 the property belongs to. + * @param string|null $level1Type The type of the object at level 1 the property belongs to. + * @param string|null $level1Name The name of the object at level 1 the property belongs to. + * @param string|null $level2Type The type of the object at level 2 the property belongs to. + * @param string|null $level2Name The name of the object at level 2 the property belongs to. + * + * @return string + */ + public function getDropExtendedPropertySQL( + $name, + $level0Type = null, + $level0Name = null, + $level1Type = null, + $level1Name = null, + $level2Type = null, + $level2Name = null + ) { + return 'EXEC sp_dropextendedproperty ' . + 'N' . $this->quoteStringLiteral($name) . ', ' . + 'N' . $this->quoteStringLiteral($level0Type ?? '') . ', ' . $level0Name . ', ' . + 'N' . $this->quoteStringLiteral($level1Type ?? '') . ', ' . $level1Name . + ($level2Type !== null || $level2Name !== null + ? ', N' . $this->quoteStringLiteral($level2Type ?? '') . ', ' . $level2Name + : '' + ); + } + + /** + * Returns the SQL statement for updating an extended property of a database object. + * + * @internal The method should be only used from within the SQLServerPlatform class hierarchy. + * + * @link http://msdn.microsoft.com/en-us/library/ms186885%28v=sql.90%29.aspx + * + * @param string $name The name of the property to update. + * @param string|null $value The value of the property to update. + * @param string|null $level0Type The type of the object at level 0 the property belongs to. + * @param string|null $level0Name The name of the object at level 0 the property belongs to. + * @param string|null $level1Type The type of the object at level 1 the property belongs to. + * @param string|null $level1Name The name of the object at level 1 the property belongs to. + * @param string|null $level2Type The type of the object at level 2 the property belongs to. + * @param string|null $level2Name The name of the object at level 2 the property belongs to. + * + * @return string + */ + public function getUpdateExtendedPropertySQL( + $name, + $value = null, + $level0Type = null, + $level0Name = null, + $level1Type = null, + $level1Name = null, + $level2Type = null, + $level2Name = null + ) { + return 'EXEC sp_updateextendedproperty ' . + 'N' . $this->quoteStringLiteral($name) . ', N' . $this->quoteStringLiteral($value ?? '') . ', ' . + 'N' . $this->quoteStringLiteral($level0Type ?? '') . ', ' . $level0Name . ', ' . + 'N' . $this->quoteStringLiteral($level1Type ?? '') . ', ' . $level1Name . + ($level2Type !== null || $level2Name !== null + ? ', N' . $this->quoteStringLiteral($level2Type ?? '') . ', ' . $level2Name + : '' + ); + } + + /** + * {@inheritDoc} + */ + public function getEmptyIdentityInsertSQL($quotedTableName, $quotedIdentifierColumnName) + { + return 'INSERT INTO ' . $quotedTableName . ' DEFAULT VALUES'; + } + + /** + * @deprecated The SQL used for schema introspection is an implementation detail and should not be relied upon. + * + * {@inheritDoc} + */ + public function getListTablesSQL() + { + // "sysdiagrams" table must be ignored as it's internal SQL Server table for Database Diagrams + // Category 2 must be ignored as it is "MS SQL Server 'pseudo-system' object[s]" for replication + return 'SELECT name, SCHEMA_NAME (uid) AS schema_name FROM sysobjects' + . " WHERE type = 'U' AND name != 'sysdiagrams' AND category != 2 ORDER BY name"; + } + + /** + * @deprecated The SQL used for schema introspection is an implementation detail and should not be relied upon. + * + * {@inheritDoc} + */ + public function getListTableColumnsSQL($table, $database = null) + { + return "SELECT col.name, + type.name AS type, + col.max_length AS length, + ~col.is_nullable AS notnull, + def.definition AS [default], + col.scale, + col.precision, + col.is_identity AS autoincrement, + col.collation_name AS collation, + CAST(prop.value AS NVARCHAR(MAX)) AS comment -- CAST avoids driver error for sql_variant type + FROM sys.columns AS col + JOIN sys.types AS type + ON col.user_type_id = type.user_type_id + JOIN sys.objects AS obj + ON col.object_id = obj.object_id + JOIN sys.schemas AS scm + ON obj.schema_id = scm.schema_id + LEFT JOIN sys.default_constraints def + ON col.default_object_id = def.object_id + AND col.object_id = def.parent_object_id + LEFT JOIN sys.extended_properties AS prop + ON obj.object_id = prop.major_id + AND col.column_id = prop.minor_id + AND prop.name = 'MS_Description' + WHERE obj.type = 'U' + AND " . $this->getTableWhereClause($table, 'scm.name', 'obj.name'); + } + + /** + * @deprecated The SQL used for schema introspection is an implementation detail and should not be relied upon. + * + * @param string $table + * @param string|null $database + * + * @return string + */ + public function getListTableForeignKeysSQL($table, $database = null) + { + return 'SELECT f.name AS ForeignKey, + SCHEMA_NAME (f.SCHEMA_ID) AS SchemaName, + OBJECT_NAME (f.parent_object_id) AS TableName, + COL_NAME (fc.parent_object_id,fc.parent_column_id) AS ColumnName, + SCHEMA_NAME (o.SCHEMA_ID) ReferenceSchemaName, + OBJECT_NAME (f.referenced_object_id) AS ReferenceTableName, + COL_NAME(fc.referenced_object_id,fc.referenced_column_id) AS ReferenceColumnName, + f.delete_referential_action_desc, + f.update_referential_action_desc + FROM sys.foreign_keys AS f + INNER JOIN sys.foreign_key_columns AS fc + INNER JOIN sys.objects AS o ON o.OBJECT_ID = fc.referenced_object_id + ON f.OBJECT_ID = fc.constraint_object_id + WHERE ' . + $this->getTableWhereClause($table, 'SCHEMA_NAME (f.schema_id)', 'OBJECT_NAME (f.parent_object_id)') . + ' ORDER BY fc.constraint_column_id'; + } + + /** + * @deprecated The SQL used for schema introspection is an implementation detail and should not be relied upon. + * + * {@inheritDoc} + */ + public function getListTableIndexesSQL($table, $database = null) + { + return "SELECT idx.name AS key_name, + col.name AS column_name, + ~idx.is_unique AS non_unique, + idx.is_primary_key AS [primary], + CASE idx.type + WHEN '1' THEN 'clustered' + WHEN '2' THEN 'nonclustered' + ELSE NULL + END AS flags + FROM sys.tables AS tbl + JOIN sys.schemas AS scm ON tbl.schema_id = scm.schema_id + JOIN sys.indexes AS idx ON tbl.object_id = idx.object_id + JOIN sys.index_columns AS idxcol ON idx.object_id = idxcol.object_id AND idx.index_id = idxcol.index_id + JOIN sys.columns AS col ON idxcol.object_id = col.object_id AND idxcol.column_id = col.column_id + WHERE " . $this->getTableWhereClause($table, 'scm.name', 'tbl.name') . ' + ORDER BY idx.index_id ASC, idxcol.key_ordinal ASC'; + } + + /** + * {@inheritDoc} + * + * @internal The method should be only used from within the {@see AbstractSchemaManager} class hierarchy. + */ + public function getListViewsSQL($database) + { + return "SELECT name, definition FROM sysobjects + INNER JOIN sys.sql_modules ON sysobjects.id = sys.sql_modules.object_id + WHERE type = 'V' ORDER BY name"; + } + + /** + * Returns the where clause to filter schema and table name in a query. + * + * @param string $table The full qualified name of the table. + * @param string $schemaColumn The name of the column to compare the schema to in the where clause. + * @param string $tableColumn The name of the column to compare the table to in the where clause. + */ + private function getTableWhereClause($table, $schemaColumn, $tableColumn): string + { + if (strpos($table, '.') !== false) { + [$schema, $table] = explode('.', $table); + $schema = $this->quoteStringLiteral($schema); + $table = $this->quoteStringLiteral($table); + } else { + $schema = 'SCHEMA_NAME()'; + $table = $this->quoteStringLiteral($table); + } + + return sprintf('(%s = %s AND %s = %s)', $tableColumn, $table, $schemaColumn, $schema); + } + + /** + * {@inheritDoc} + */ + public function getLocateExpression($str, $substr, $startPos = false) + { + if ($startPos === false) { + return 'CHARINDEX(' . $substr . ', ' . $str . ')'; + } + + return 'CHARINDEX(' . $substr . ', ' . $str . ', ' . $startPos . ')'; + } + + /** + * {@inheritDoc} + */ + public function getModExpression($expression1, $expression2) + { + return $expression1 . ' % ' . $expression2; + } + + /** + * {@inheritDoc} + */ + public function getTrimExpression($str, $mode = TrimMode::UNSPECIFIED, $char = false) + { + if ($char === false) { + switch ($mode) { + case TrimMode::LEADING: + $trimFn = 'LTRIM'; + break; + + case TrimMode::TRAILING: + $trimFn = 'RTRIM'; + break; + + default: + return 'LTRIM(RTRIM(' . $str . '))'; + } + + return $trimFn . '(' . $str . ')'; + } + + $pattern = "'%[^' + " . $char . " + ']%'"; + + if ($mode === TrimMode::LEADING) { + return 'stuff(' . $str . ', 1, patindex(' . $pattern . ', ' . $str . ') - 1, null)'; + } + + if ($mode === TrimMode::TRAILING) { + return 'reverse(stuff(reverse(' . $str . '), 1, ' + . 'patindex(' . $pattern . ', reverse(' . $str . ')) - 1, null))'; + } + + return 'reverse(stuff(reverse(stuff(' . $str . ', 1, patindex(' . $pattern . ', ' . $str . ') - 1, null)), 1, ' + . 'patindex(' . $pattern . ', reverse(stuff(' . $str . ', 1, patindex(' . $pattern . ', ' . $str + . ') - 1, null))) - 1, null))'; + } + + /** + * {@inheritDoc} + */ + public function getConcatExpression() + { + return sprintf('CONCAT(%s)', implode(', ', func_get_args())); + } + + /** + * {@inheritDoc} + * + * @internal The method should be only used from within the {@see AbstractSchemaManager} class hierarchy. + */ + public function getListDatabasesSQL() + { + return 'SELECT * FROM sys.databases'; + } + + /** + * {@inheritDoc} + * + * @deprecated Use {@see SQLServerSchemaManager::listSchemaNames()} instead. + */ + public function getListNamespacesSQL() + { + Deprecation::triggerIfCalledFromOutside( + 'doctrine/dbal', + 'https://github.com/doctrine/dbal/issues/4503', + 'SQLServerPlatform::getListNamespacesSQL() is deprecated,' + . ' use SQLServerSchemaManager::listSchemaNames() instead.', + ); + + return "SELECT name FROM sys.schemas WHERE name NOT IN('guest', 'INFORMATION_SCHEMA', 'sys')"; + } + + /** + * {@inheritDoc} + */ + public function getSubstringExpression($string, $start, $length = null) + { + if ($length !== null) { + return 'SUBSTRING(' . $string . ', ' . $start . ', ' . $length . ')'; + } + + return 'SUBSTRING(' . $string . ', ' . $start . ', LEN(' . $string . ') - ' . $start . ' + 1)'; + } + + /** + * {@inheritDoc} + */ + public function getLengthExpression($column) + { + return 'LEN(' . $column . ')'; + } + + public function getCurrentDatabaseExpression(): string + { + return 'DB_NAME()'; + } + + /** + * {@inheritDoc} + */ + public function getSetTransactionIsolationSQL($level) + { + return 'SET TRANSACTION ISOLATION LEVEL ' . $this->_getTransactionIsolationLevelSQL($level); + } + + /** + * {@inheritDoc} + */ + public function getIntegerTypeDeclarationSQL(array $column) + { + return 'INT' . $this->_getCommonIntegerTypeDeclarationSQL($column); + } + + /** + * {@inheritDoc} + */ + public function getBigIntTypeDeclarationSQL(array $column) + { + return 'BIGINT' . $this->_getCommonIntegerTypeDeclarationSQL($column); + } + + /** + * {@inheritDoc} + */ + public function getSmallIntTypeDeclarationSQL(array $column) + { + return 'SMALLINT' . $this->_getCommonIntegerTypeDeclarationSQL($column); + } + + /** + * {@inheritDoc} + */ + public function getGuidTypeDeclarationSQL(array $column) + { + return 'UNIQUEIDENTIFIER'; + } + + /** + * {@inheritDoc} + */ + public function getDateTimeTzTypeDeclarationSQL(array $column) + { + return 'DATETIMEOFFSET(6)'; + } + + /** + * {@inheritDoc} + */ + public function getAsciiStringTypeDeclarationSQL(array $column): string + { + $length = $column['length'] ?? null; + + if (empty($column['fixed'])) { + return sprintf('VARCHAR(%d)', $length ?? 255); + } + + return sprintf('CHAR(%d)', $length ?? 255); + } + + /** + * {@inheritDoc} + */ + protected function getVarcharTypeDeclarationSQLSnippet($length, $fixed/*, $lengthOmitted = false*/) + { + if ($length <= 0 || (func_num_args() > 2 && func_get_arg(2))) { + Deprecation::trigger( + 'doctrine/dbal', + 'https://github.com/doctrine/dbal/issues/3263', + 'Relying on the default string column length on SQL Server is deprecated' + . ', specify the length explicitly.', + ); + } + + return $fixed + ? 'NCHAR(' . ($length > 0 ? $length : 255) . ')' + : 'NVARCHAR(' . ($length > 0 ? $length : 255) . ')'; + } + + /** + * {@inheritDoc} + */ + protected function getBinaryTypeDeclarationSQLSnippet($length, $fixed/*, $lengthOmitted = false*/) + { + if ($length <= 0 || (func_num_args() > 2 && func_get_arg(2))) { + Deprecation::trigger( + 'doctrine/dbal', + 'https://github.com/doctrine/dbal/issues/3263', + 'Relying on the default binary column length on SQL Server is deprecated' + . ', specify the length explicitly.', + ); + } + + return $fixed + ? 'BINARY(' . ($length > 0 ? $length : 255) . ')' + : 'VARBINARY(' . ($length > 0 ? $length : 255) . ')'; + } + + /** + * {@inheritDoc} + * + * @deprecated + */ + public function getBinaryMaxLength() + { + Deprecation::triggerIfCalledFromOutside( + 'doctrine/dbal', + 'https://github.com/doctrine/dbal/issues/3263', + 'SQLServerPlatform::getBinaryMaxLength() is deprecated.', + ); + + return 8000; + } + + /** + * {@inheritDoc} + */ + public function getClobTypeDeclarationSQL(array $column) + { + return 'VARCHAR(MAX)'; + } + + /** + * {@inheritDoc} + */ + protected function _getCommonIntegerTypeDeclarationSQL(array $column) + { + return ! empty($column['autoincrement']) ? ' IDENTITY' : ''; + } + + /** + * {@inheritDoc} + */ + public function getDateTimeTypeDeclarationSQL(array $column) + { + // 3 - microseconds precision length + // http://msdn.microsoft.com/en-us/library/ms187819.aspx + return 'DATETIME2(6)'; + } + + /** + * {@inheritDoc} + */ + public function getDateTypeDeclarationSQL(array $column) + { + return 'DATE'; + } + + /** + * {@inheritDoc} + */ + public function getTimeTypeDeclarationSQL(array $column) + { + return 'TIME(0)'; + } + + /** + * {@inheritDoc} + */ + public function getBooleanTypeDeclarationSQL(array $column) + { + return 'BIT'; + } + + /** + * {@inheritDoc} + */ + protected function doModifyLimitQuery($query, $limit, $offset) + { + if ($limit === null && $offset <= 0) { + return $query; + } + + if ($this->shouldAddOrderBy($query)) { + if (preg_match('/^SELECT\s+DISTINCT/im', $query) > 0) { + // SQL Server won't let us order by a non-selected column in a DISTINCT query, + // so we have to do this madness. This says, order by the first column in the + // result. SQL Server's docs say that a nonordered query's result order is non- + // deterministic anyway, so this won't do anything that a bunch of update and + // deletes to the table wouldn't do anyway. + $query .= ' ORDER BY 1'; + } else { + // In another DBMS, we could do ORDER BY 0, but SQL Server gets angry if you + // use constant expressions in the order by list. + $query .= ' ORDER BY (SELECT 0)'; + } + } + + // This looks somewhat like MYSQL, but limit/offset are in inverse positions + // Supposedly SQL:2008 core standard. + // Per TSQL spec, FETCH NEXT n ROWS ONLY is not valid without OFFSET n ROWS. + $query .= sprintf(' OFFSET %d ROWS', $offset); + + if ($limit !== null) { + $query .= sprintf(' FETCH NEXT %d ROWS ONLY', $limit); + } + + return $query; + } + + /** + * {@inheritDoc} + */ + public function convertBooleans($item) + { + if (is_array($item)) { + foreach ($item as $key => $value) { + if (! is_bool($value) && ! is_numeric($value)) { + continue; + } + + $item[$key] = (int) (bool) $value; + } + } elseif (is_bool($item) || is_numeric($item)) { + $item = (int) (bool) $item; + } + + return $item; + } + + /** + * {@inheritDoc} + */ + public function getCreateTemporaryTableSnippetSQL() + { + return 'CREATE TABLE'; + } + + /** + * {@inheritDoc} + */ + public function getTemporaryTableName($tableName) + { + return '#' . $tableName; + } + + /** + * {@inheritDoc} + */ + public function getDateTimeFormatString() + { + return 'Y-m-d H:i:s.u'; + } + + /** + * {@inheritDoc} + */ + public function getDateFormatString() + { + return 'Y-m-d'; + } + + /** + * {@inheritDoc} + */ + public function getTimeFormatString() + { + return 'H:i:s'; + } + + /** + * {@inheritDoc} + */ + public function getDateTimeTzFormatString() + { + return 'Y-m-d H:i:s.u P'; + } + + /** + * {@inheritDoc} + */ + public function getName() + { + return 'mssql'; + } + + /** + * {@inheritDoc} + */ + protected function initializeDoctrineTypeMappings() + { + $this->doctrineTypeMapping = [ + 'bigint' => Types::BIGINT, + 'binary' => Types::BINARY, + 'bit' => Types::BOOLEAN, + 'blob' => Types::BLOB, + 'char' => Types::STRING, + 'date' => Types::DATE_MUTABLE, + 'datetime' => Types::DATETIME_MUTABLE, + 'datetime2' => Types::DATETIME_MUTABLE, + 'datetimeoffset' => Types::DATETIMETZ_MUTABLE, + 'decimal' => Types::DECIMAL, + 'double' => Types::FLOAT, + 'double precision' => Types::FLOAT, + 'float' => Types::FLOAT, + 'image' => Types::BLOB, + 'int' => Types::INTEGER, + 'money' => Types::INTEGER, + 'nchar' => Types::STRING, + 'ntext' => Types::TEXT, + 'numeric' => Types::DECIMAL, + 'nvarchar' => Types::STRING, + 'real' => Types::FLOAT, + 'smalldatetime' => Types::DATETIME_MUTABLE, + 'smallint' => Types::SMALLINT, + 'smallmoney' => Types::INTEGER, + 'sysname' => Types::STRING, + 'text' => Types::TEXT, + 'time' => Types::TIME_MUTABLE, + 'tinyint' => Types::SMALLINT, + 'uniqueidentifier' => Types::GUID, + 'varbinary' => Types::BINARY, + 'varchar' => Types::STRING, + 'xml' => Types::TEXT, + ]; + } + + /** + * {@inheritDoc} + */ + public function createSavePoint($savepoint) + { + return 'SAVE TRANSACTION ' . $savepoint; + } + + /** + * {@inheritDoc} + */ + public function releaseSavePoint($savepoint) + { + return ''; + } + + /** + * {@inheritDoc} + */ + public function rollbackSavePoint($savepoint) + { + return 'ROLLBACK TRANSACTION ' . $savepoint; + } + + /** + * {@inheritDoc} + * + * @internal The method should be only used from within the {@see AbstractPlatform} class hierarchy. + */ + public function getForeignKeyReferentialActionSQL($action) + { + // RESTRICT is not supported, therefore falling back to NO ACTION. + if (strtoupper($action) === 'RESTRICT') { + return 'NO ACTION'; + } + + return parent::getForeignKeyReferentialActionSQL($action); + } + + public function appendLockHint(string $fromClause, int $lockMode): string + { + switch ($lockMode) { + case LockMode::NONE: + case LockMode::OPTIMISTIC: + return $fromClause; + + case LockMode::PESSIMISTIC_READ: + return $fromClause . ' WITH (HOLDLOCK, ROWLOCK)'; + + case LockMode::PESSIMISTIC_WRITE: + return $fromClause . ' WITH (UPDLOCK, ROWLOCK)'; + + default: + throw InvalidLockMode::fromLockMode($lockMode); + } + } + + /** + * {@inheritDoc} + * + * @deprecated This API is not portable. + */ + public function getForUpdateSQL() + { + return ' '; + } + + /** + * {@inheritDoc} + * + * @deprecated Implement {@see createReservedKeywordsList()} instead. + */ + protected function getReservedKeywordsClass() + { + Deprecation::triggerIfCalledFromOutside( + 'doctrine/dbal', + 'https://github.com/doctrine/dbal/issues/4510', + 'SQLServerPlatform::getReservedKeywordsClass() is deprecated,' + . ' use SQLServerPlatform::createReservedKeywordsList() instead.', + ); + + return Keywords\SQLServer2012Keywords::class; + } + + /** + * {@inheritDoc} + */ + public function quoteSingleIdentifier($str) + { + return '[' . str_replace(']', ']]', $str) . ']'; + } + + /** + * {@inheritDoc} + */ + public function getTruncateTableSQL($tableName, $cascade = false) + { + $tableIdentifier = new Identifier($tableName); + + return 'TRUNCATE TABLE ' . $tableIdentifier->getQuotedName($this); + } + + /** + * {@inheritDoc} + */ + public function getBlobTypeDeclarationSQL(array $column) + { + return 'VARBINARY(MAX)'; + } + + /** + * {@inheritDoc} + * + * @internal The method should be only used from within the {@see AbstractPlatform} class hierarchy. + */ + public function getColumnDeclarationSQL($name, array $column) + { + if (isset($column['columnDefinition'])) { + $columnDef = $this->getCustomTypeDeclarationSQL($column); + } else { + $collation = ! empty($column['collation']) ? + ' ' . $this->getColumnCollationDeclarationSQL($column['collation']) : ''; + + $notnull = ! empty($column['notnull']) ? ' NOT NULL' : ''; + + if (! empty($column['unique'])) { + Deprecation::trigger( + 'doctrine/dbal', + 'https://github.com/doctrine/dbal/pull/5656', + 'The usage of the "unique" column property is deprecated. Use unique constraints instead.', + ); + + $unique = ' ' . $this->getUniqueFieldDeclarationSQL(); + } else { + $unique = ''; + } + + if (! empty($column['check'])) { + Deprecation::trigger( + 'doctrine/dbal', + 'https://github.com/doctrine/dbal/pull/5656', + 'The usage of the "check" column property is deprecated.', + ); + + $check = ' ' . $column['check']; + } else { + $check = ''; + } + + $typeDecl = $column['type']->getSQLDeclaration($column, $this); + $columnDef = $typeDecl . $collation . $notnull . $unique . $check; + } + + return $name . ' ' . $columnDef; + } + + /** + * {@inheritDoc} + * + * SQL Server does not support quoting collation identifiers. + */ + public function getColumnCollationDeclarationSQL($collation) + { + return 'COLLATE ' . $collation; + } + + public function columnsEqual(Column $column1, Column $column2): bool + { + if (! parent::columnsEqual($column1, $column2)) { + return false; + } + + return $this->getDefaultValueDeclarationSQL($column1->toArray()) + === $this->getDefaultValueDeclarationSQL($column2->toArray()); + } + + protected function getLikeWildcardCharacters(): string + { + return parent::getLikeWildcardCharacters() . '[]^'; + } + + /** + * Returns a unique default constraint name for a table and column. + * + * @param string $table Name of the table to generate the unique default constraint name for. + * @param string $column Name of the column in the table to generate the unique default constraint name for. + */ + private function generateDefaultConstraintName($table, $column): string + { + return 'DF_' . $this->generateIdentifierName($table) . '_' . $this->generateIdentifierName($column); + } + + /** + * Returns a hash value for a given identifier. + * + * @param string $identifier Identifier to generate a hash value for. + */ + private function generateIdentifierName($identifier): string + { + // Always generate name for unquoted identifiers to ensure consistency. + $identifier = new Identifier($identifier); + + return strtoupper(dechex(crc32($identifier->getName()))); + } + + protected function getCommentOnTableSQL(string $tableName, ?string $comment): string + { + if (str_contains($tableName, '.')) { + [$schemaName, $tableName] = explode('.', $tableName); + } else { + $schemaName = 'dbo'; + } + + return $this->getAddExtendedPropertySQL( + 'MS_Description', + $comment, + 'SCHEMA', + $this->quoteStringLiteral($schemaName), + 'TABLE', + $this->quoteStringLiteral($this->unquoteSingleIdentifier($tableName)), + ); + } + + /** @deprecated The SQL used for schema introspection is an implementation detail and should not be relied upon. */ + public function getListTableMetadataSQL(string $table): string + { + return sprintf( + <<<'SQL' + SELECT + p.value AS [table_comment] + FROM + sys.tables AS tbl + INNER JOIN sys.extended_properties AS p ON p.major_id=tbl.object_id AND p.minor_id=0 AND p.class=1 + WHERE + (tbl.name=N%s and SCHEMA_NAME(tbl.schema_id)=N'dbo' and p.name=N'MS_Description') + SQL + , + $this->quoteStringLiteral($table), + ); + } + + /** @param string $query */ + private function shouldAddOrderBy($query): bool + { + // Find the position of the last instance of ORDER BY and ensure it is not within a parenthetical statement + // but can be in a newline + $matches = []; + $matchesCount = preg_match_all('/[\\s]+order\\s+by\\s/im', $query, $matches, PREG_OFFSET_CAPTURE); + if ($matchesCount === 0) { + return true; + } + + // ORDER BY instance may be in a subquery after ORDER BY + // e.g. SELECT col1 FROM test ORDER BY (SELECT col2 from test ORDER BY col2) + // if in the searched query ORDER BY clause was found where + // number of open parentheses after the occurrence of the clause is equal to + // number of closed brackets after the occurrence of the clause, + // it means that ORDER BY is included in the query being checked + while ($matchesCount > 0) { + $orderByPos = $matches[0][--$matchesCount][1]; + $openBracketsCount = substr_count($query, '(', $orderByPos); + $closedBracketsCount = substr_count($query, ')', $orderByPos); + if ($openBracketsCount === $closedBracketsCount) { + return false; + } + } + + return true; + } + + public function createSchemaManager(Connection $connection): SQLServerSchemaManager + { + return new SQLServerSchemaManager($connection, $this); + } +} diff --git a/3rdparty/doctrine/dbal/src/Platforms/SQLite/Comparator.php b/3rdparty/doctrine/dbal/src/Platforms/SQLite/Comparator.php new file mode 100644 index 00000000..5218871c --- /dev/null +++ b/3rdparty/doctrine/dbal/src/Platforms/SQLite/Comparator.php @@ -0,0 +1,61 @@ +normalizeColumns($fromTable), + $this->normalizeColumns($toTable), + ); + } + + /** + * {@inheritDoc} + */ + public function diffTable(Table $fromTable, Table $toTable) + { + return parent::diffTable( + $this->normalizeColumns($fromTable), + $this->normalizeColumns($toTable), + ); + } + + private function normalizeColumns(Table $table): Table + { + $table = clone $table; + + foreach ($table->getColumns() as $column) { + $options = $column->getPlatformOptions(); + + if (! isset($options['collation']) || strcasecmp($options['collation'], 'binary') !== 0) { + continue; + } + + unset($options['collation']); + $column->setPlatformOptions($options); + } + + return $table; + } +} diff --git a/3rdparty/doctrine/dbal/src/Platforms/SqlitePlatform.php b/3rdparty/doctrine/dbal/src/Platforms/SqlitePlatform.php new file mode 100644 index 00000000..b2e1280d --- /dev/null +++ b/3rdparty/doctrine/dbal/src/Platforms/SqlitePlatform.php @@ -0,0 +1,1545 @@ + 0 THEN INSTR(SUBSTR(' . $str . ', ' . $startPos . '), ' . $substr . ') + ' . $startPos + . ' - 1 ELSE 0 END'; + } + + /** + * {@inheritDoc} + */ + protected function getDateArithmeticIntervalExpression($date, $operator, $interval, $unit) + { + switch ($unit) { + case DateIntervalUnit::SECOND: + case DateIntervalUnit::MINUTE: + case DateIntervalUnit::HOUR: + return 'DATETIME(' . $date . ",'" . $operator . $interval . ' ' . $unit . "')"; + } + + switch ($unit) { + case DateIntervalUnit::WEEK: + $interval = $this->multiplyInterval((string) $interval, 7); + $unit = DateIntervalUnit::DAY; + break; + + case DateIntervalUnit::QUARTER: + $interval = $this->multiplyInterval((string) $interval, 3); + $unit = DateIntervalUnit::MONTH; + break; + } + + if (! is_numeric($interval)) { + $interval = "' || " . $interval . " || '"; + } + + return 'DATE(' . $date . ",'" . $operator . $interval . ' ' . $unit . "')"; + } + + /** + * {@inheritDoc} + */ + public function getDateDiffExpression($date1, $date2) + { + return sprintf("JULIANDAY(%s, 'start of day') - JULIANDAY(%s, 'start of day')", $date1, $date2); + } + + /** + * {@inheritDoc} + * + * The DBAL doesn't support databases on the SQLite platform. The expression here always returns a fixed string + * as an indicator of an implicitly selected database. + * + * @link https://www.sqlite.org/lang_select.html + * @see Connection::getDatabase() + */ + public function getCurrentDatabaseExpression(): string + { + return "'main'"; + } + + /** @link https://www2.sqlite.org/cvstrac/wiki?p=UnsupportedSql */ + public function createSelectSQLBuilder(): SelectSQLBuilder + { + return new DefaultSelectSQLBuilder($this, null, null); + } + + /** + * {@inheritDoc} + */ + protected function _getTransactionIsolationLevelSQL($level) + { + switch ($level) { + case TransactionIsolationLevel::READ_UNCOMMITTED: + return '0'; + + case TransactionIsolationLevel::READ_COMMITTED: + case TransactionIsolationLevel::REPEATABLE_READ: + case TransactionIsolationLevel::SERIALIZABLE: + return '1'; + + default: + return parent::_getTransactionIsolationLevelSQL($level); + } + } + + /** + * {@inheritDoc} + */ + public function getSetTransactionIsolationSQL($level) + { + return 'PRAGMA read_uncommitted = ' . $this->_getTransactionIsolationLevelSQL($level); + } + + /** + * {@inheritDoc} + * + * @deprecated + */ + public function prefersIdentityColumns() + { + Deprecation::trigger( + 'doctrine/dbal', + 'https://github.com/doctrine/dbal/pull/1519', + 'SqlitePlatform::prefersIdentityColumns() is deprecated.', + ); + + return true; + } + + /** + * {@inheritDoc} + */ + public function getBooleanTypeDeclarationSQL(array $column) + { + return 'BOOLEAN'; + } + + /** + * {@inheritDoc} + */ + public function getIntegerTypeDeclarationSQL(array $column) + { + return 'INTEGER' . $this->_getCommonIntegerTypeDeclarationSQL($column); + } + + /** + * {@inheritDoc} + */ + public function getBigIntTypeDeclarationSQL(array $column) + { + // SQLite autoincrement is implicit for INTEGER PKs, but not for BIGINT columns + if (! empty($column['autoincrement'])) { + return $this->getIntegerTypeDeclarationSQL($column); + } + + return 'BIGINT' . $this->_getCommonIntegerTypeDeclarationSQL($column); + } + + /** + * @deprecated Use {@see getSmallIntTypeDeclarationSQL()} instead. + * + * @param array $column + * + * @return string + */ + public function getTinyIntTypeDeclarationSQL(array $column) + { + Deprecation::trigger( + 'doctrine/dbal', + 'https://github.com/doctrine/dbal/pull/5511', + '%s is deprecated. Use getSmallIntTypeDeclarationSQL() instead.', + __METHOD__, + ); + + // SQLite autoincrement is implicit for INTEGER PKs, but not for TINYINT columns + if (! empty($column['autoincrement'])) { + return $this->getIntegerTypeDeclarationSQL($column); + } + + return 'TINYINT' . $this->_getCommonIntegerTypeDeclarationSQL($column); + } + + /** + * {@inheritDoc} + */ + public function getSmallIntTypeDeclarationSQL(array $column) + { + // SQLite autoincrement is implicit for INTEGER PKs, but not for SMALLINT columns + if (! empty($column['autoincrement'])) { + return $this->getIntegerTypeDeclarationSQL($column); + } + + return 'SMALLINT' . $this->_getCommonIntegerTypeDeclarationSQL($column); + } + + /** + * @deprecated Use {@see getIntegerTypeDeclarationSQL()} instead. + * + * @param array $column + * + * @return string + */ + public function getMediumIntTypeDeclarationSQL(array $column) + { + Deprecation::trigger( + 'doctrine/dbal', + 'https://github.com/doctrine/dbal/pull/5511', + '%s is deprecated. Use getIntegerTypeDeclarationSQL() instead.', + __METHOD__, + ); + + // SQLite autoincrement is implicit for INTEGER PKs, but not for MEDIUMINT columns + if (! empty($column['autoincrement'])) { + return $this->getIntegerTypeDeclarationSQL($column); + } + + return 'MEDIUMINT' . $this->_getCommonIntegerTypeDeclarationSQL($column); + } + + /** + * {@inheritDoc} + */ + public function getDateTimeTypeDeclarationSQL(array $column) + { + return 'DATETIME'; + } + + /** + * {@inheritDoc} + */ + public function getDateTypeDeclarationSQL(array $column) + { + return 'DATE'; + } + + /** + * {@inheritDoc} + */ + public function getTimeTypeDeclarationSQL(array $column) + { + return 'TIME'; + } + + /** + * {@inheritDoc} + */ + protected function _getCommonIntegerTypeDeclarationSQL(array $column) + { + // sqlite autoincrement is only possible for the primary key + if (! empty($column['autoincrement'])) { + return ' PRIMARY KEY AUTOINCREMENT'; + } + + return ! empty($column['unsigned']) ? ' UNSIGNED' : ''; + } + + /** + * Disables schema emulation. + * + * Schema emulation is enabled by default to maintain backwards compatibility. + * Disable it to opt-in to the behavior of DBAL 4. + * + * @deprecated Will be removed in DBAL 4.0. + */ + public function disableSchemaEmulation(): void + { + $this->schemaEmulationEnabled = false; + } + + private function emulateSchemaNamespacing(string $tableName): string + { + return $this->schemaEmulationEnabled + ? str_replace('.', '__', $tableName) + : $tableName; + } + + /** + * {@inheritDoc} + * + * @internal The method should be only used from within the {@see AbstractPlatform} class hierarchy. + */ + public function getForeignKeyDeclarationSQL(ForeignKeyConstraint $foreignKey) + { + return parent::getForeignKeyDeclarationSQL(new ForeignKeyConstraint( + $foreignKey->getQuotedLocalColumns($this), + $this->emulateSchemaNamespacing($foreignKey->getQuotedForeignTableName($this)), + $foreignKey->getQuotedForeignColumns($this), + $foreignKey->getName(), + $foreignKey->getOptions(), + )); + } + + /** + * {@inheritDoc} + */ + protected function _getCreateTableSQL($name, array $columns, array $options = []) + { + $name = $this->emulateSchemaNamespacing($name); + $queryFields = $this->getColumnDeclarationListSQL($columns); + + if (isset($options['uniqueConstraints']) && ! empty($options['uniqueConstraints'])) { + foreach ($options['uniqueConstraints'] as $constraintName => $definition) { + $queryFields .= ', ' . $this->getUniqueConstraintDeclarationSQL($constraintName, $definition); + } + } + + $queryFields .= $this->getNonAutoincrementPrimaryKeyDefinition($columns, $options); + + if (isset($options['foreignKeys'])) { + foreach ($options['foreignKeys'] as $foreignKey) { + $queryFields .= ', ' . $this->getForeignKeyDeclarationSQL($foreignKey); + } + } + + $tableComment = ''; + if (isset($options['comment'])) { + $comment = trim($options['comment'], " '"); + + $tableComment = $this->getInlineTableCommentSQL($comment); + } + + $query = ['CREATE TABLE ' . $name . ' ' . $tableComment . '(' . $queryFields . ')']; + + if (isset($options['alter']) && $options['alter'] === true) { + return $query; + } + + if (isset($options['indexes']) && ! empty($options['indexes'])) { + foreach ($options['indexes'] as $indexDef) { + $query[] = $this->getCreateIndexSQL($indexDef, $name); + } + } + + if (isset($options['unique']) && ! empty($options['unique'])) { + foreach ($options['unique'] as $indexDef) { + $query[] = $this->getCreateIndexSQL($indexDef, $name); + } + } + + return $query; + } + + /** + * Generate a PRIMARY KEY definition if no autoincrement value is used + * + * @param mixed[][] $columns + * @param mixed[] $options + */ + private function getNonAutoincrementPrimaryKeyDefinition(array $columns, array $options): string + { + if (empty($options['primary'])) { + return ''; + } + + $keyColumns = array_unique(array_values($options['primary'])); + + foreach ($keyColumns as $keyColumn) { + if (! empty($columns[$keyColumn]['autoincrement'])) { + return ''; + } + } + + return ', PRIMARY KEY(' . implode(', ', $keyColumns) . ')'; + } + + /** + * {@inheritDoc} + */ + protected function getVarcharTypeDeclarationSQLSnippet($length, $fixed) + { + return $fixed ? ($length > 0 ? 'CHAR(' . $length . ')' : 'CHAR(255)') + : ($length > 0 ? 'VARCHAR(' . $length . ')' : 'TEXT'); + } + + /** + * {@inheritDoc} + */ + protected function getBinaryTypeDeclarationSQLSnippet($length, $fixed) + { + return 'BLOB'; + } + + /** + * {@inheritDoc} + * + * @deprecated + */ + public function getBinaryMaxLength() + { + Deprecation::triggerIfCalledFromOutside( + 'doctrine/dbal', + 'https://github.com/doctrine/dbal/issues/3263', + 'SqlitePlatform::getBinaryMaxLength() is deprecated.', + ); + + return 0; + } + + /** + * {@inheritDoc} + * + * @deprecated + */ + public function getBinaryDefaultLength() + { + Deprecation::trigger( + 'doctrine/dbal', + 'https://github.com/doctrine/dbal/issues/3263', + 'Relying on the default binary column length is deprecated, specify the length explicitly.', + ); + + return 0; + } + + /** + * {@inheritDoc} + */ + public function getClobTypeDeclarationSQL(array $column) + { + return 'CLOB'; + } + + /** + * @deprecated + * + * {@inheritDoc} + */ + public function getListTableConstraintsSQL($table) + { + $table = $this->emulateSchemaNamespacing($table); + + return sprintf( + "SELECT sql FROM sqlite_master WHERE type='index' AND tbl_name = %s AND sql NOT NULL ORDER BY name", + $this->quoteStringLiteral($table), + ); + } + + /** + * @deprecated The SQL used for schema introspection is an implementation detail and should not be relied upon. + * + * {@inheritDoc} + */ + public function getListTableColumnsSQL($table, $database = null) + { + $table = $this->emulateSchemaNamespacing($table); + + return sprintf('PRAGMA table_info(%s)', $this->quoteStringLiteral($table)); + } + + /** + * @deprecated The SQL used for schema introspection is an implementation detail and should not be relied upon. + * + * {@inheritDoc} + */ + public function getListTableIndexesSQL($table, $database = null) + { + $table = $this->emulateSchemaNamespacing($table); + + return sprintf('PRAGMA index_list(%s)', $this->quoteStringLiteral($table)); + } + + /** + * @deprecated The SQL used for schema introspection is an implementation detail and should not be relied upon. + * + * {@inheritDoc} + */ + public function getListTablesSQL() + { + return 'SELECT name FROM sqlite_master' + . " WHERE type = 'table'" + . " AND name != 'sqlite_sequence'" + . " AND name != 'geometry_columns'" + . " AND name != 'spatial_ref_sys'" + . ' UNION ALL SELECT name FROM sqlite_temp_master' + . " WHERE type = 'table' ORDER BY name"; + } + + /** + * {@inheritDoc} + * + * @internal The method should be only used from within the {@see AbstractSchemaManager} class hierarchy. + */ + public function getListViewsSQL($database) + { + return "SELECT name, sql FROM sqlite_master WHERE type='view' AND sql NOT NULL"; + } + + /** + * {@inheritDoc} + * + * @internal The method should be only used from within the {@see AbstractPlatform} class hierarchy. + */ + public function getAdvancedForeignKeyOptionsSQL(ForeignKeyConstraint $foreignKey) + { + $query = parent::getAdvancedForeignKeyOptionsSQL($foreignKey); + + if (! $foreignKey->hasOption('deferrable') || $foreignKey->getOption('deferrable') === false) { + $query .= ' NOT'; + } + + $query .= ' DEFERRABLE'; + $query .= ' INITIALLY'; + + if ($foreignKey->hasOption('deferred') && $foreignKey->getOption('deferred') !== false) { + $query .= ' DEFERRED'; + } else { + $query .= ' IMMEDIATE'; + } + + return $query; + } + + /** + * {@inheritDoc} + * + * @deprecated + */ + public function supportsCreateDropDatabase() + { + Deprecation::trigger( + 'doctrine/dbal', + 'https://github.com/doctrine/dbal/pull/5513', + '%s is deprecated.', + __METHOD__, + ); + + return false; + } + + /** + * {@inheritDoc} + */ + public function supportsIdentityColumns() + { + return true; + } + + /** + * {@inheritDoc} + * + * @internal The method should be only used from within the {@see AbstractPlatform} class hierarchy. + */ + public function supportsColumnCollation() + { + return true; + } + + /** + * {@inheritDoc} + * + * @internal The method should be only used from within the {@see AbstractPlatform} class hierarchy. + */ + public function supportsInlineColumnComments() + { + return true; + } + + /** + * {@inheritDoc} + */ + public function getName() + { + Deprecation::triggerIfCalledFromOutside( + 'doctrine/dbal', + 'https://github.com/doctrine/dbal/issues/4749', + 'SqlitePlatform::getName() is deprecated. Identify platforms by their class.', + ); + + return 'sqlite'; + } + + /** + * {@inheritDoc} + */ + public function getTruncateTableSQL($tableName, $cascade = false) + { + $tableIdentifier = new Identifier($tableName); + $tableName = $this->emulateSchemaNamespacing($tableIdentifier->getQuotedName($this)); + + return 'DELETE FROM ' . $tableName; + } + + /** + * User-defined function for Sqlite that is used with PDO::sqliteCreateFunction(). + * + * @deprecated The driver will use {@see sqrt()} in the next major release. + * + * @param int|float $value + * + * @return float + */ + public static function udfSqrt($value) + { + return sqrt($value); + } + + /** + * User-defined function for Sqlite that implements MOD(a, b). + * + * @deprecated The driver will use {@see UserDefinedFunctions::mod()} in the next major release. + * + * @param int $a + * @param int $b + * + * @return int + */ + public static function udfMod($a, $b) + { + return UserDefinedFunctions::mod($a, $b); + } + + /** + * @deprecated The driver will use {@see UserDefinedFunctions::locate()} in the next major release. + * + * @param string $str + * @param string $substr + * @param int $offset + * + * @return int + */ + public static function udfLocate($str, $substr, $offset = 0) + { + return UserDefinedFunctions::locate($str, $substr, $offset); + } + + /** + * {@inheritDoc} + * + * @deprecated This API is not portable. + */ + public function getForUpdateSQL() + { + return ''; + } + + /** + * {@inheritDoc} + * + * @internal The method should be only used from within the {@see AbstractPlatform} class hierarchy. + */ + public function getInlineColumnCommentSQL($comment) + { + return '--' . str_replace("\n", "\n--", $comment) . "\n"; + } + + private function getInlineTableCommentSQL(string $comment): string + { + return $this->getInlineColumnCommentSQL($comment); + } + + /** + * {@inheritDoc} + */ + protected function initializeDoctrineTypeMappings() + { + $this->doctrineTypeMapping = [ + 'bigint' => Types\Types::BIGINT, + 'bigserial' => Types\Types::BIGINT, + 'blob' => Types\Types::BLOB, + 'boolean' => Types\Types::BOOLEAN, + 'char' => Types\Types::STRING, + 'clob' => Types\Types::TEXT, + 'date' => Types\Types::DATE_MUTABLE, + 'datetime' => Types\Types::DATETIME_MUTABLE, + 'decimal' => Types\Types::DECIMAL, + 'double' => Types\Types::FLOAT, + 'double precision' => Types\Types::FLOAT, + 'float' => Types\Types::FLOAT, + 'image' => Types\Types::STRING, + 'int' => Types\Types::INTEGER, + 'integer' => Types\Types::INTEGER, + 'longtext' => Types\Types::TEXT, + 'longvarchar' => Types\Types::STRING, + 'mediumint' => Types\Types::INTEGER, + 'mediumtext' => Types\Types::TEXT, + 'ntext' => Types\Types::STRING, + 'numeric' => Types\Types::DECIMAL, + 'nvarchar' => Types\Types::STRING, + 'real' => Types\Types::FLOAT, + 'serial' => Types\Types::INTEGER, + 'smallint' => Types\Types::SMALLINT, + 'text' => Types\Types::TEXT, + 'time' => Types\Types::TIME_MUTABLE, + 'timestamp' => Types\Types::DATETIME_MUTABLE, + 'tinyint' => Types\Types::BOOLEAN, + 'tinytext' => Types\Types::TEXT, + 'varchar' => Types\Types::STRING, + 'varchar2' => Types\Types::STRING, + ]; + } + + /** + * {@inheritDoc} + * + * @deprecated Implement {@see createReservedKeywordsList()} instead. + */ + protected function getReservedKeywordsClass() + { + Deprecation::triggerIfCalledFromOutside( + 'doctrine/dbal', + 'https://github.com/doctrine/dbal/issues/4510', + 'SqlitePlatform::getReservedKeywordsClass() is deprecated,' + . ' use SqlitePlatform::createReservedKeywordsList() instead.', + ); + + return Keywords\SQLiteKeywords::class; + } + + /** + * {@inheritDoc} + */ + protected function getPreAlterTableIndexForeignKeySQL(TableDiff $diff) + { + return []; + } + + /** + * {@inheritDoc} + */ + protected function getPostAlterTableIndexForeignKeySQL(TableDiff $diff) + { + $table = $diff->getOldTable(); + + if (! $table instanceof Table) { + throw new Exception( + 'Sqlite platform requires for alter table the table diff with reference to original table schema', + ); + } + + $sql = []; + $tableName = $diff->getNewName(); + + if ($tableName === false) { + $tableName = $diff->getName($this); + } + + foreach ($this->getIndexesInAlteredTable($diff, $table) as $index) { + if ($index->isPrimary()) { + continue; + } + + $sql[] = $this->getCreateIndexSQL($index, $tableName->getQuotedName($this)); + } + + return $sql; + } + + /** + * {@inheritDoc} + */ + protected function doModifyLimitQuery($query, $limit, $offset) + { + if ($limit === null && $offset > 0) { + return sprintf('%s LIMIT -1 OFFSET %d', $query, $offset); + } + + return parent::doModifyLimitQuery($query, $limit, $offset); + } + + /** + * {@inheritDoc} + */ + public function getBlobTypeDeclarationSQL(array $column) + { + return 'BLOB'; + } + + /** + * {@inheritDoc} + */ + public function getTemporaryTableName($tableName) + { + $tableName = $this->emulateSchemaNamespacing($tableName); + + return $tableName; + } + + /** + * {@inheritDoc} + * + * @deprecated + * + * Sqlite Platform emulates schema by underscoring each dot and generating tables + * into the default database. + * + * This hack is implemented to be able to use SQLite as testdriver when + * using schema supporting databases. + */ + public function canEmulateSchemas() + { + Deprecation::triggerIfCalledFromOutside( + 'doctrine/dbal', + 'https://github.com/doctrine/dbal/pull/4805', + 'SqlitePlatform::canEmulateSchemas() is deprecated.', + ); + + return $this->schemaEmulationEnabled; + } + + /** + * {@inheritDoc} + */ + public function getCreateTablesSQL(array $tables): array + { + $sql = []; + + foreach ($tables as $table) { + $sql = array_merge($sql, $this->getCreateTableSQL($table)); + } + + return $sql; + } + + /** + * {@inheritDoc} + */ + public function getCreateIndexSQL(Index $index, $table) + { + if ($table instanceof Table) { + Deprecation::trigger( + 'doctrine/dbal', + 'https://github.com/doctrine/dbal/issues/4798', + 'Passing $table as a Table object to %s is deprecated. Pass it as a quoted name instead.', + __METHOD__, + ); + + $table = $table->getQuotedName($this); + } + + $name = $index->getQuotedName($this); + $columns = $index->getColumns(); + + if (count($columns) === 0) { + throw new InvalidArgumentException(sprintf( + 'Incomplete or invalid index definition %s on table %s', + $name, + $table, + )); + } + + if ($index->isPrimary()) { + return $this->getCreatePrimaryKeySQL($index, $table); + } + + if (strpos($table, '.') !== false) { + [$schema, $table] = explode('.', $table, 2); + $name = $schema . '.' . $name; + } + + $query = 'CREATE ' . $this->getCreateIndexSQLFlags($index) . 'INDEX ' . $name . ' ON ' . $table; + $query .= ' (' . $this->getIndexFieldDeclarationListSQL($index) . ')' . $this->getPartialIndexSQL($index); + + return $query; + } + + /** + * {@inheritDoc} + */ + public function getDropTablesSQL(array $tables): array + { + $sql = []; + + foreach ($tables as $table) { + $sql[] = $this->getDropTableSQL($table->getQuotedName($this)); + } + + return $sql; + } + + /** + * {@inheritDoc} + */ + public function getCreatePrimaryKeySQL(Index $index, $table) + { + throw new Exception('Sqlite platform does not support alter primary key.'); + } + + /** + * {@inheritDoc} + */ + public function getCreateForeignKeySQL(ForeignKeyConstraint $foreignKey, $table) + { + throw new Exception('Sqlite platform does not support alter foreign key.'); + } + + /** + * {@inheritDoc} + */ + public function getDropForeignKeySQL($foreignKey, $table) + { + throw new Exception('Sqlite platform does not support alter foreign key.'); + } + + /** + * {@inheritDoc} + * + * @deprecated + */ + public function getCreateConstraintSQL(Constraint $constraint, $table) + { + throw new Exception('Sqlite platform does not support alter constraint.'); + } + + /** + * {@inheritDoc} + * + * @param int|null $createFlags + * @phpstan-param int-mask-of|null $createFlags + */ + public function getCreateTableSQL(Table $table, $createFlags = null) + { + $createFlags = $createFlags ?? self::CREATE_INDEXES | self::CREATE_FOREIGNKEYS; + + return parent::getCreateTableSQL($table, $createFlags); + } + + /** + * @deprecated The SQL used for schema introspection is an implementation detail and should not be relied upon. + * + * @param string $table + * @param string|null $database + * + * @return string + */ + public function getListTableForeignKeysSQL($table, $database = null) + { + $table = $this->emulateSchemaNamespacing($table); + + return sprintf('PRAGMA foreign_key_list(%s)', $this->quoteStringLiteral($table)); + } + + /** + * {@inheritDoc} + */ + public function getAlterTableSQL(TableDiff $diff) + { + $sql = $this->getSimpleAlterTableSQL($diff); + if ($sql !== false) { + return $sql; + } + + $table = $diff->getOldTable(); + + if (! $table instanceof Table) { + throw new Exception( + 'Sqlite platform requires for alter table the table diff with reference to original table schema', + ); + } + + $columns = []; + $oldColumnNames = []; + $newColumnNames = []; + $columnSql = []; + + foreach ($table->getColumns() as $columnName => $column) { + $columnName = strtolower($columnName); + $columns[$columnName] = $column; + $oldColumnNames[$columnName] = $newColumnNames[$columnName] = $column->getQuotedName($this); + } + + foreach ($diff->getDroppedColumns() as $column) { + if ($this->onSchemaAlterTableRemoveColumn($column, $diff, $columnSql)) { + continue; + } + + $columnName = strtolower($column->getName()); + if (! isset($columns[$columnName])) { + continue; + } + + unset( + $columns[$columnName], + $oldColumnNames[$columnName], + $newColumnNames[$columnName], + ); + } + + foreach ($diff->getRenamedColumns() as $oldColumnName => $column) { + if ($this->onSchemaAlterTableRenameColumn($oldColumnName, $column, $diff, $columnSql)) { + continue; + } + + $oldColumnName = strtolower($oldColumnName); + + $columns = $this->replaceColumn( + $table->getName(), + $columns, + $oldColumnName, + $column, + ); + + if (! isset($newColumnNames[$oldColumnName])) { + continue; + } + + $newColumnNames[$oldColumnName] = $column->getQuotedName($this); + } + + foreach ($diff->getModifiedColumns() as $columnDiff) { + if ($this->onSchemaAlterTableChangeColumn($columnDiff, $diff, $columnSql)) { + continue; + } + + $oldColumn = $columnDiff->getOldColumn() ?? $columnDiff->getOldColumnName(); + + $oldColumnName = strtolower($oldColumn->getName()); + + $columns = $this->replaceColumn( + $table->getName(), + $columns, + $oldColumnName, + $columnDiff->getNewColumn(), + ); + + if (! isset($newColumnNames[$oldColumnName])) { + continue; + } + + $newColumnNames[$oldColumnName] = $columnDiff->getNewColumn()->getQuotedName($this); + } + + foreach ($diff->getAddedColumns() as $column) { + if ($this->onSchemaAlterTableAddColumn($column, $diff, $columnSql)) { + continue; + } + + $columns[strtolower($column->getName())] = $column; + } + + $sql = []; + $tableSql = []; + if (! $this->onSchemaAlterTable($diff, $tableSql)) { + $tableName = $table->getName(); + if (strpos($tableName, '.') !== false) { + [, $tableName] = explode('.', $tableName, 2); + } + + $dataTable = new Table('__temp__' . $tableName); + + $newTable = new Table( + $table->getQuotedName($this), + $columns, + $this->getPrimaryIndexInAlteredTable($diff, $table), + [], + $this->getForeignKeysInAlteredTable($diff, $table), + $table->getOptions(), + ); + $newTable->addOption('alter', true); + + $sql = $this->getPreAlterTableIndexForeignKeySQL($diff); + + $sql[] = sprintf( + 'CREATE TEMPORARY TABLE %s AS SELECT %s FROM %s', + $dataTable->getQuotedName($this), + implode(', ', $oldColumnNames), + $table->getQuotedName($this), + ); + $sql[] = $this->getDropTableSQL($table); + + $sql = array_merge($sql, $this->getCreateTableSQL($newTable)); + $sql[] = sprintf( + 'INSERT INTO %s (%s) SELECT %s FROM %s', + $newTable->getQuotedName($this), + implode(', ', $newColumnNames), + implode(', ', $oldColumnNames), + $dataTable->getQuotedName($this), + ); + $sql[] = $this->getDropTableSQL($dataTable->getQuotedName($this)); + + $newName = $diff->getNewName(); + + if ($newName !== false) { + Deprecation::trigger( + 'doctrine/dbal', + 'https://github.com/doctrine/dbal/pull/5663', + 'Generation of "rename table" SQL using %s is deprecated. Use getRenameTableSQL() instead.', + __METHOD__, + ); + + $sql[] = sprintf( + 'ALTER TABLE %s RENAME TO %s', + $newTable->getQuotedName($this), + $newName->getQuotedName($this), + ); + } + + $sql = array_merge($sql, $this->getPostAlterTableIndexForeignKeySQL($diff)); + } + + return array_merge($sql, $tableSql, $columnSql); + } + + /** + * Replace the column with the given name with the new column. + * + * @param string $tableName + * @param array $columns + * @param string $columnName + * + * @return array + * + * @throws Exception + */ + private function replaceColumn($tableName, array $columns, $columnName, Column $column): array + { + $keys = array_keys($columns); + $index = array_search($columnName, $keys, true); + + if ($index === false) { + throw SchemaException::columnDoesNotExist($columnName, $tableName); + } + + $values = array_values($columns); + + $keys[$index] = strtolower($column->getName()); + $values[$index] = $column; + + return array_combine($keys, $values); + } + + /** + * @return string[]|false + * + * @throws Exception + */ + private function getSimpleAlterTableSQL(TableDiff $diff) + { + // Suppress changes on integer type autoincrement columns. + foreach ($diff->getModifiedColumns() as $columnDiff) { + $oldColumn = $columnDiff->getOldColumn(); + + if ($oldColumn === null) { + continue; + } + + $newColumn = $columnDiff->getNewColumn(); + + if (! $newColumn->getAutoincrement() || ! $newColumn->getType() instanceof IntegerType) { + continue; + } + + $oldColumnName = $oldColumn->getName(); + + if (! $columnDiff->hasTypeChanged() && $columnDiff->hasUnsignedChanged()) { + unset($diff->changedColumns[$oldColumnName]); + + continue; + } + + $fromColumnType = $oldColumn->getType(); + + if (! ($fromColumnType instanceof Types\SmallIntType) && ! ($fromColumnType instanceof Types\BigIntType)) { + continue; + } + + unset($diff->changedColumns[$oldColumnName]); + } + + if ( + count($diff->getModifiedColumns()) > 0 + || count($diff->getDroppedColumns()) > 0 + || count($diff->getRenamedColumns()) > 0 + || count($diff->getAddedIndexes()) > 0 + || count($diff->getModifiedIndexes()) > 0 + || count($diff->getDroppedIndexes()) > 0 + || count($diff->getRenamedIndexes()) > 0 + || count($diff->getAddedForeignKeys()) > 0 + || count($diff->getModifiedForeignKeys()) > 0 + || count($diff->getDroppedForeignKeys()) > 0 + ) { + return false; + } + + $table = $diff->getOldTable() ?? $diff->getName($this); + + $sql = []; + $tableSql = []; + $columnSql = []; + + foreach ($diff->getAddedColumns() as $column) { + if ($this->onSchemaAlterTableAddColumn($column, $diff, $columnSql)) { + continue; + } + + $definition = array_merge([ + 'unique' => null, + 'autoincrement' => null, + 'default' => null, + ], $column->toArray()); + + $type = $definition['type']; + + switch (true) { + case isset($definition['columnDefinition']) || $definition['autoincrement'] || $definition['unique']: + case $type instanceof Types\DateTimeType && $definition['default'] === $this->getCurrentTimestampSQL(): + case $type instanceof Types\DateType && $definition['default'] === $this->getCurrentDateSQL(): + case $type instanceof Types\TimeType && $definition['default'] === $this->getCurrentTimeSQL(): + return false; + } + + $definition['name'] = $column->getQuotedName($this); + if ($type instanceof Types\StringType) { + $definition['length'] ??= 255; + } + + $sql[] = 'ALTER TABLE ' . $table->getQuotedName($this) . ' ADD COLUMN ' + . $this->getColumnDeclarationSQL($definition['name'], $definition); + } + + if (! $this->onSchemaAlterTable($diff, $tableSql)) { + if ($diff->newName !== false) { + Deprecation::trigger( + 'doctrine/dbal', + 'https://github.com/doctrine/dbal/pull/5663', + 'Generation of SQL that renames a table using %s is deprecated.' + . ' Use getRenameTableSQL() instead.', + __METHOD__, + ); + + $newTable = new Identifier($diff->newName); + + $sql[] = 'ALTER TABLE ' . $table->getQuotedName($this) . ' RENAME TO ' + . $newTable->getQuotedName($this); + } + } + + return array_merge($sql, $tableSql, $columnSql); + } + + /** @return string[] */ + private function getColumnNamesInAlteredTable(TableDiff $diff, Table $fromTable): array + { + $columns = []; + + foreach ($fromTable->getColumns() as $columnName => $column) { + $columns[strtolower($columnName)] = $column->getName(); + } + + foreach ($diff->getDroppedColumns() as $column) { + $columnName = strtolower($column->getName()); + if (! isset($columns[$columnName])) { + continue; + } + + unset($columns[$columnName]); + } + + foreach ($diff->getRenamedColumns() as $oldColumnName => $column) { + $columnName = $column->getName(); + $columns[strtolower($oldColumnName)] = $columnName; + $columns[strtolower($columnName)] = $columnName; + } + + foreach ($diff->getModifiedColumns() as $columnDiff) { + $oldColumn = $columnDiff->getOldColumn() ?? $columnDiff->getOldColumnName(); + + $oldColumnName = $oldColumn->getName(); + $newColumnName = $columnDiff->getNewColumn()->getName(); + $columns[strtolower($oldColumnName)] = $newColumnName; + $columns[strtolower($newColumnName)] = $newColumnName; + } + + foreach ($diff->getAddedColumns() as $column) { + $columnName = $column->getName(); + $columns[strtolower($columnName)] = $columnName; + } + + return $columns; + } + + /** @return Index[] */ + private function getIndexesInAlteredTable(TableDiff $diff, Table $fromTable): array + { + $indexes = $fromTable->getIndexes(); + $columnNames = $this->getColumnNamesInAlteredTable($diff, $fromTable); + + foreach ($indexes as $key => $index) { + foreach ($diff->getRenamedIndexes() as $oldIndexName => $renamedIndex) { + if (strtolower($key) !== strtolower($oldIndexName)) { + continue; + } + + unset($indexes[$key]); + } + + $changed = false; + $indexColumns = []; + foreach ($index->getColumns() as $columnName) { + $normalizedColumnName = strtolower($columnName); + if (! isset($columnNames[$normalizedColumnName])) { + unset($indexes[$key]); + continue 2; + } + + $indexColumns[] = $columnNames[$normalizedColumnName]; + if ($columnName === $columnNames[$normalizedColumnName]) { + continue; + } + + $changed = true; + } + + if (! $changed) { + continue; + } + + $indexes[$key] = new Index( + $index->getName(), + $indexColumns, + $index->isUnique(), + $index->isPrimary(), + $index->getFlags(), + ); + } + + foreach ($diff->getDroppedIndexes() as $index) { + $indexName = strtolower($index->getName()); + if (strlen($indexName) === 0 || ! isset($indexes[$indexName])) { + continue; + } + + unset($indexes[$indexName]); + } + + foreach ( + array_merge( + $diff->getModifiedIndexes(), + $diff->getAddedIndexes(), + $diff->getRenamedIndexes(), + ) as $index + ) { + $indexName = strtolower($index->getName()); + if (strlen($indexName) > 0) { + $indexes[$indexName] = $index; + } else { + $indexes[] = $index; + } + } + + return $indexes; + } + + /** @return ForeignKeyConstraint[] */ + private function getForeignKeysInAlteredTable(TableDiff $diff, Table $fromTable): array + { + $foreignKeys = $fromTable->getForeignKeys(); + $columnNames = $this->getColumnNamesInAlteredTable($diff, $fromTable); + + foreach ($foreignKeys as $key => $constraint) { + $changed = false; + $localColumns = []; + foreach ($constraint->getLocalColumns() as $columnName) { + $normalizedColumnName = strtolower($columnName); + if (! isset($columnNames[$normalizedColumnName])) { + unset($foreignKeys[$key]); + continue 2; + } + + $localColumns[] = $columnNames[$normalizedColumnName]; + if ($columnName === $columnNames[$normalizedColumnName]) { + continue; + } + + $changed = true; + } + + if (! $changed) { + continue; + } + + $foreignKeys[$key] = new ForeignKeyConstraint( + $localColumns, + $constraint->getForeignTableName(), + $constraint->getForeignColumns(), + $constraint->getName(), + $constraint->getOptions(), + ); + } + + foreach ($diff->getDroppedForeignKeys() as $constraint) { + if (! $constraint instanceof ForeignKeyConstraint) { + $constraint = new Identifier($constraint); + } + + $constraintName = strtolower($constraint->getName()); + if (strlen($constraintName) === 0 || ! isset($foreignKeys[$constraintName])) { + continue; + } + + unset($foreignKeys[$constraintName]); + } + + foreach (array_merge($diff->getModifiedForeignKeys(), $diff->getAddedForeignKeys()) as $constraint) { + $constraintName = strtolower($constraint->getName()); + if (strlen($constraintName) > 0) { + $foreignKeys[$constraintName] = $constraint; + } else { + $foreignKeys[] = $constraint; + } + } + + return $foreignKeys; + } + + /** @return Index[] */ + private function getPrimaryIndexInAlteredTable(TableDiff $diff, Table $fromTable): array + { + $primaryIndex = []; + + foreach ($this->getIndexesInAlteredTable($diff, $fromTable) as $index) { + if (! $index->isPrimary()) { + continue; + } + + $primaryIndex = [$index->getName() => $index]; + } + + return $primaryIndex; + } + + public function createSchemaManager(Connection $connection): SqliteSchemaManager + { + return new SqliteSchemaManager($connection, $this); + } +} diff --git a/3rdparty/doctrine/dbal/src/Platforms/TrimMode.php b/3rdparty/doctrine/dbal/src/Platforms/TrimMode.php new file mode 100644 index 00000000..01356c0d --- /dev/null +++ b/3rdparty/doctrine/dbal/src/Platforms/TrimMode.php @@ -0,0 +1,21 @@ +converter = $converter; + } + + public function prepare(string $sql): DriverStatement + { + return new Statement( + parent::prepare($sql), + $this->converter, + ); + } + + public function query(string $sql): DriverResult + { + return new Result( + parent::query($sql), + $this->converter, + ); + } +} diff --git a/3rdparty/doctrine/dbal/src/Portability/Converter.php b/3rdparty/doctrine/dbal/src/Portability/Converter.php new file mode 100644 index 00000000..effbf986 --- /dev/null +++ b/3rdparty/doctrine/dbal/src/Portability/Converter.php @@ -0,0 +1,300 @@ +createConvertValue($convertEmptyStringToNull, $rightTrimString); + $convertNumeric = $this->createConvertRow($convertValue, null); + $convertAssociative = $this->createConvertRow($convertValue, $case); + + $this->convertNumeric = $this->createConvert($convertNumeric, [self::class, 'id']); + $this->convertAssociative = $this->createConvert($convertAssociative, [self::class, 'id']); + $this->convertOne = $this->createConvert($convertValue, [self::class, 'id']); + + $this->convertAllNumeric = $this->createConvertAll($convertNumeric, [self::class, 'id']); + $this->convertAllAssociative = $this->createConvertAll($convertAssociative, [self::class, 'id']); + $this->convertFirstColumn = $this->createConvertAll($convertValue, [self::class, 'id']); + } + + /** + * @param array|false $row + * + * @return list|false + */ + public function convertNumeric($row) + { + return ($this->convertNumeric)($row); + } + + /** + * @param array|false $row + * + * @return array|false + */ + public function convertAssociative($row) + { + return ($this->convertAssociative)($row); + } + + /** + * @param mixed|false $value + * + * @return mixed|false + */ + public function convertOne($value) + { + return ($this->convertOne)($value); + } + + /** + * @param list> $data + * + * @return list> + */ + public function convertAllNumeric(array $data): array + { + return ($this->convertAllNumeric)($data); + } + + /** + * @param list> $data + * + * @return list> + */ + public function convertAllAssociative(array $data): array + { + return ($this->convertAllAssociative)($data); + } + + /** + * @param list $data + * + * @return list + */ + public function convertFirstColumn(array $data): array + { + return ($this->convertFirstColumn)($data); + } + + /** + * @param T $value + * + * @return T + * + * @template T + */ + private static function id($value) + { + return $value; + } + + /** + * @param T $value + * + * @return T|null + * + * @template T + */ + private static function convertEmptyStringToNull($value) + { + if ($value === '') { + return null; + } + + return $value; + } + + /** + * @param T $value + * + * @return T|string + * @phpstan-return (T is string ? string : T) + * + * @template T + */ + private static function rightTrimString($value) + { + if (! is_string($value)) { + return $value; + } + + return rtrim($value); + } + + /** + * Creates a function that will convert each individual value retrieved from the database + * + * @param bool $convertEmptyStringToNull Whether each empty string should be converted to NULL + * @param bool $rightTrimString Whether each string should right-trimmed + * + * @return callable|null The resulting function or NULL if no conversion is needed + */ + private function createConvertValue(bool $convertEmptyStringToNull, bool $rightTrimString): ?callable + { + $functions = []; + + if ($convertEmptyStringToNull) { + $functions[] = [self::class, 'convertEmptyStringToNull']; + } + + if ($rightTrimString) { + $functions[] = [self::class, 'rightTrimString']; + } + + return $this->compose(...$functions); + } + + /** + * Creates a function that will convert each array-row retrieved from the database + * + * @param callable|null $function The function that will convert each value + * @param self::CASE_LOWER|self::CASE_UPPER|null $case Column name case + * + * @return callable|null The resulting function or NULL if no conversion is needed + */ + private function createConvertRow(?callable $function, ?int $case): ?callable + { + $functions = []; + + if ($function !== null) { + $functions[] = $this->createMapper($function); + } + + if ($case !== null) { + $functions[] = static function (array $row) use ($case): array { + return array_change_key_case($row, $case); + }; + } + + return $this->compose(...$functions); + } + + /** + * Creates a function that will be applied to the return value of Statement::fetch*() + * or an identity function if no conversion is needed + * + * @param callable|null $function The function that will convert each tow + * @param callable $id Identity function + */ + private function createConvert(?callable $function, callable $id): callable + { + if ($function === null) { + return $id; + } + + return /** + * @param T $value + * + * @phpstan-return (T is false ? false : T) + * + * @template T + */ + static function ($value) use ($function) { + if ($value === false) { + return false; + } + + return $function($value); + }; + } + + /** + * Creates a function that will be applied to the return value of Statement::fetchAll*() + * or an identity function if no transformation is required + * + * @param callable|null $function The function that will transform each value + * @param callable $id Identity function + */ + private function createConvertAll(?callable $function, callable $id): callable + { + if ($function === null) { + return $id; + } + + return $this->createMapper($function); + } + + /** + * Creates a function that maps each value of the array using the given function + * + * @param callable $function The function that maps each value of the array + */ + private function createMapper(callable $function): callable + { + return static function (array $array) use ($function): array { + return array_map($function, $array); + }; + } + + /** + * Creates a composition of the given set of functions + * + * @param callable(T):T ...$functions The functions to compose + * + * @return callable(T):T|null + * + * @template T + */ + private function compose(callable ...$functions): ?callable + { + return array_reduce($functions, static function (?callable $carry, callable $item): callable { + if ($carry === null) { + return $item; + } + + return /** + * @param T $value + * + * @return T + * + * @template T + */ + static function ($value) use ($carry, $item) { + return $item($carry($value)); + }; + }); + } +} diff --git a/3rdparty/doctrine/dbal/src/Portability/Driver.php b/3rdparty/doctrine/dbal/src/Portability/Driver.php new file mode 100644 index 00000000..a5ac9679 --- /dev/null +++ b/3rdparty/doctrine/dbal/src/Portability/Driver.php @@ -0,0 +1,83 @@ +mode = $mode; + $this->case = $case; + } + + /** + * {@inheritDoc} + */ + public function connect( + #[SensitiveParameter] + array $params + ) { + $connection = parent::connect($params); + + $portability = (new OptimizeFlags())( + $this->getDatabasePlatform(), + $this->mode, + ); + + $case = null; + + if ($this->case !== 0 && ($portability & Connection::PORTABILITY_FIX_CASE) !== 0) { + $nativeConnection = null; + if (method_exists($connection, 'getNativeConnection')) { + try { + $nativeConnection = $connection->getNativeConnection(); + } catch (LogicException $e) { + } + } + + if ($nativeConnection instanceof PDO) { + $portability &= ~Connection::PORTABILITY_FIX_CASE; + $nativeConnection->setAttribute( + PDO::ATTR_CASE, + $this->case === ColumnCase::LOWER ? PDO::CASE_LOWER : PDO::CASE_UPPER, + ); + } else { + $case = $this->case === ColumnCase::LOWER ? Converter::CASE_LOWER : Converter::CASE_UPPER; + } + } + + $convertEmptyStringToNull = ($portability & Connection::PORTABILITY_EMPTY_TO_NULL) !== 0; + $rightTrimString = ($portability & Connection::PORTABILITY_RTRIM) !== 0; + + if (! $convertEmptyStringToNull && ! $rightTrimString && $case === null) { + return $connection; + } + + return new Connection( + $connection, + new Converter($convertEmptyStringToNull, $rightTrimString, $case), + ); + } +} diff --git a/3rdparty/doctrine/dbal/src/Portability/Middleware.php b/3rdparty/doctrine/dbal/src/Portability/Middleware.php new file mode 100644 index 00000000..fae2c556 --- /dev/null +++ b/3rdparty/doctrine/dbal/src/Portability/Middleware.php @@ -0,0 +1,38 @@ +mode = $mode; + $this->case = $case; + } + + public function wrap(DriverInterface $driver): DriverInterface + { + if ($this->mode !== 0) { + return new Driver($driver, $this->mode, $this->case); + } + + return $driver; + } +} diff --git a/3rdparty/doctrine/dbal/src/Portability/OptimizeFlags.php b/3rdparty/doctrine/dbal/src/Portability/OptimizeFlags.php new file mode 100644 index 00000000..884a936c --- /dev/null +++ b/3rdparty/doctrine/dbal/src/Portability/OptimizeFlags.php @@ -0,0 +1,42 @@ + + */ + private static array $platforms = [ + DB2Platform::class => 0, + OraclePlatform::class => Connection::PORTABILITY_EMPTY_TO_NULL, + PostgreSQLPlatform::class => 0, + SqlitePlatform::class => 0, + SQLServerPlatform::class => 0, + ]; + + public function __invoke(AbstractPlatform $platform, int $flags): int + { + foreach (self::$platforms as $class => $mask) { + if ($platform instanceof $class) { + $flags &= ~$mask; + + break; + } + } + + return $flags; + } +} diff --git a/3rdparty/doctrine/dbal/src/Portability/Result.php b/3rdparty/doctrine/dbal/src/Portability/Result.php new file mode 100644 index 00000000..da1eca98 --- /dev/null +++ b/3rdparty/doctrine/dbal/src/Portability/Result.php @@ -0,0 +1,81 @@ +converter = $converter; + } + + /** + * {@inheritDoc} + */ + public function fetchNumeric() + { + return $this->converter->convertNumeric( + parent::fetchNumeric(), + ); + } + + /** + * {@inheritDoc} + */ + public function fetchAssociative() + { + return $this->converter->convertAssociative( + parent::fetchAssociative(), + ); + } + + /** + * {@inheritDoc} + */ + public function fetchOne() + { + return $this->converter->convertOne( + parent::fetchOne(), + ); + } + + /** + * {@inheritDoc} + */ + public function fetchAllNumeric(): array + { + return $this->converter->convertAllNumeric( + parent::fetchAllNumeric(), + ); + } + + /** + * {@inheritDoc} + */ + public function fetchAllAssociative(): array + { + return $this->converter->convertAllAssociative( + parent::fetchAllAssociative(), + ); + } + + /** + * {@inheritDoc} + */ + public function fetchFirstColumn(): array + { + return $this->converter->convertFirstColumn( + parent::fetchFirstColumn(), + ); + } +} diff --git a/3rdparty/doctrine/dbal/src/Portability/Statement.php b/3rdparty/doctrine/dbal/src/Portability/Statement.php new file mode 100644 index 00000000..8fcd79d4 --- /dev/null +++ b/3rdparty/doctrine/dbal/src/Portability/Statement.php @@ -0,0 +1,36 @@ +Statement and applies portability measures. + */ + public function __construct(DriverStatement $stmt, Converter $converter) + { + parent::__construct($stmt); + + $this->converter = $converter; + } + + /** + * {@inheritDoc} + */ + public function execute($params = null): ResultInterface + { + return new Result( + parent::execute($params), + $this->converter, + ); + } +} diff --git a/3rdparty/doctrine/dbal/src/Query.php b/3rdparty/doctrine/dbal/src/Query.php new file mode 100644 index 00000000..e4ab80e7 --- /dev/null +++ b/3rdparty/doctrine/dbal/src/Query.php @@ -0,0 +1,60 @@ + + */ + private array $params; + + /** + * The types of the parameters bound to the query. + * + * @var array + */ + private array $types; + + /** + * @param array $params + * @param array $types + */ + public function __construct(string $sql, array $params, array $types) + { + $this->sql = $sql; + $this->params = $params; + $this->types = $types; + } + + public function getSQL(): string + { + return $this->sql; + } + + /** @return array */ + public function getParams(): array + { + return $this->params; + } + + /** @return array */ + public function getTypes(): array + { + return $this->types; + } +} diff --git a/3rdparty/doctrine/dbal/src/Query/Expression/CompositeExpression.php b/3rdparty/doctrine/dbal/src/Query/Expression/CompositeExpression.php new file mode 100644 index 00000000..c241ff7f --- /dev/null +++ b/3rdparty/doctrine/dbal/src/Query/Expression/CompositeExpression.php @@ -0,0 +1,183 @@ +type = $type; + + $this->addMultiple($parts); + + Deprecation::triggerIfCalledFromOutside( + 'doctrine/dbal', + 'https://github.com/doctrine/dbal/pull/3864', + 'Do not use CompositeExpression constructor directly, use static and() and or() factory methods.', + ); + } + + /** + * @param self|string $part + * @param self|string ...$parts + */ + public static function and($part, ...$parts): self + { + return new self(self::TYPE_AND, array_merge([$part], $parts)); + } + + /** + * @param self|string $part + * @param self|string ...$parts + */ + public static function or($part, ...$parts): self + { + return new self(self::TYPE_OR, array_merge([$part], $parts)); + } + + /** + * Adds multiple parts to composite expression. + * + * @deprecated This class will be made immutable. Use with() instead. + * + * @param self[]|string[] $parts + * + * @return CompositeExpression + */ + public function addMultiple(array $parts = []) + { + Deprecation::triggerIfCalledFromOutside( + 'doctrine/dbal', + 'https://github.com/doctrine/dbal/issues/3844', + 'CompositeExpression::addMultiple() is deprecated, use CompositeExpression::with() instead.', + ); + + foreach ($parts as $part) { + $this->add($part); + } + + return $this; + } + + /** + * Adds an expression to composite expression. + * + * @deprecated This class will be made immutable. Use with() instead. + * + * @param mixed $part + * + * @return CompositeExpression + */ + public function add($part) + { + Deprecation::triggerIfCalledFromOutside( + 'doctrine/dbal', + 'https://github.com/doctrine/dbal/issues/3844', + 'CompositeExpression::add() is deprecated, use CompositeExpression::with() instead.', + ); + + if ($part === null) { + return $this; + } + + if ($part instanceof self && count($part) === 0) { + return $this; + } + + $this->parts[] = $part; + + return $this; + } + + /** + * Returns a new CompositeExpression with the given parts added. + * + * @param self|string $part + * @param self|string ...$parts + */ + public function with($part, ...$parts): self + { + $that = clone $this; + + $that->parts = array_merge($that->parts, [$part], $parts); + + return $that; + } + + /** + * Retrieves the amount of expressions on composite expression. + * + * @return int + * @phpstan-return int<0, max> + */ + #[ReturnTypeWillChange] + public function count() + { + return count($this->parts); + } + + /** + * Retrieves the string representation of this composite expression. + * + * @return string + */ + public function __toString() + { + if ($this->count() === 1) { + return (string) $this->parts[0]; + } + + return '(' . implode(') ' . $this->type . ' (', $this->parts) . ')'; + } + + /** + * Returns the type of this composite expression (AND/OR). + * + * @return string + */ + public function getType() + { + return $this->type; + } +} diff --git a/3rdparty/doctrine/dbal/src/Query/Expression/ExpressionBuilder.php b/3rdparty/doctrine/dbal/src/Query/Expression/ExpressionBuilder.php new file mode 100644 index 00000000..d532b289 --- /dev/null +++ b/3rdparty/doctrine/dbal/src/Query/Expression/ExpressionBuilder.php @@ -0,0 +1,323 @@ +'; + public const LT = '<'; + public const LTE = '<='; + public const GT = '>'; + public const GTE = '>='; + + /** + * The DBAL Connection. + */ + private Connection $connection; + + /** + * Initializes a new ExpressionBuilder. + * + * @param Connection $connection The DBAL Connection. + */ + public function __construct(Connection $connection) + { + $this->connection = $connection; + } + + /** + * Creates a conjunction of the given expressions. + * + * @param string|CompositeExpression $expression + * @param string|CompositeExpression ...$expressions + */ + public function and($expression, ...$expressions): CompositeExpression + { + return CompositeExpression::and($expression, ...$expressions); + } + + /** + * Creates a disjunction of the given expressions. + * + * @param string|CompositeExpression $expression + * @param string|CompositeExpression ...$expressions + */ + public function or($expression, ...$expressions): CompositeExpression + { + return CompositeExpression::or($expression, ...$expressions); + } + + /** + * @deprecated Use `and()` instead. + * + * @param mixed $x Optional clause. Defaults = null, but requires + * at least one defined when converting to string. + * + * @return CompositeExpression + */ + public function andX($x = null) + { + Deprecation::trigger( + 'doctrine/dbal', + 'https://github.com/doctrine/dbal/pull/3851', + 'ExpressionBuilder::andX() is deprecated, use ExpressionBuilder::and() instead.', + ); + + return new CompositeExpression(CompositeExpression::TYPE_AND, func_get_args()); + } + + /** + * @deprecated Use `or()` instead. + * + * @param mixed $x Optional clause. Defaults = null, but requires + * at least one defined when converting to string. + * + * @return CompositeExpression + */ + public function orX($x = null) + { + Deprecation::trigger( + 'doctrine/dbal', + 'https://github.com/doctrine/dbal/pull/3851', + 'ExpressionBuilder::orX() is deprecated, use ExpressionBuilder::or() instead.', + ); + + return new CompositeExpression(CompositeExpression::TYPE_OR, func_get_args()); + } + + /** + * Creates a comparison expression. + * + * @param mixed $x The left expression. + * @param string $operator One of the ExpressionBuilder::* constants. + * @param mixed $y The right expression. + * + * @return string + */ + public function comparison($x, $operator, $y) + { + return $x . ' ' . $operator . ' ' . $y; + } + + /** + * Creates an equality comparison expression with the given arguments. + * + * First argument is considered the left expression and the second is the right expression. + * When converted to string, it will generated a = . Example: + * + * [php] + * // u.id = ? + * $expr->eq('u.id', '?'); + * + * @param mixed $x The left expression. + * @param mixed $y The right expression. + * + * @return string + */ + public function eq($x, $y) + { + return $this->comparison($x, self::EQ, $y); + } + + /** + * Creates a non equality comparison expression with the given arguments. + * First argument is considered the left expression and the second is the right expression. + * When converted to string, it will generated a <> . Example: + * + * [php] + * // u.id <> 1 + * $q->where($q->expr()->neq('u.id', '1')); + * + * @param mixed $x The left expression. + * @param mixed $y The right expression. + * + * @return string + */ + public function neq($x, $y) + { + return $this->comparison($x, self::NEQ, $y); + } + + /** + * Creates a lower-than comparison expression with the given arguments. + * First argument is considered the left expression and the second is the right expression. + * When converted to string, it will generated a < . Example: + * + * [php] + * // u.id < ? + * $q->where($q->expr()->lt('u.id', '?')); + * + * @param mixed $x The left expression. + * @param mixed $y The right expression. + * + * @return string + */ + public function lt($x, $y) + { + return $this->comparison($x, self::LT, $y); + } + + /** + * Creates a lower-than-equal comparison expression with the given arguments. + * First argument is considered the left expression and the second is the right expression. + * When converted to string, it will generated a <= . Example: + * + * [php] + * // u.id <= ? + * $q->where($q->expr()->lte('u.id', '?')); + * + * @param mixed $x The left expression. + * @param mixed $y The right expression. + * + * @return string + */ + public function lte($x, $y) + { + return $this->comparison($x, self::LTE, $y); + } + + /** + * Creates a greater-than comparison expression with the given arguments. + * First argument is considered the left expression and the second is the right expression. + * When converted to string, it will generated a > . Example: + * + * [php] + * // u.id > ? + * $q->where($q->expr()->gt('u.id', '?')); + * + * @param mixed $x The left expression. + * @param mixed $y The right expression. + * + * @return string + */ + public function gt($x, $y) + { + return $this->comparison($x, self::GT, $y); + } + + /** + * Creates a greater-than-equal comparison expression with the given arguments. + * First argument is considered the left expression and the second is the right expression. + * When converted to string, it will generated a >= . Example: + * + * [php] + * // u.id >= ? + * $q->where($q->expr()->gte('u.id', '?')); + * + * @param mixed $x The left expression. + * @param mixed $y The right expression. + * + * @return string + */ + public function gte($x, $y) + { + return $this->comparison($x, self::GTE, $y); + } + + /** + * Creates an IS NULL expression with the given arguments. + * + * @param string $x The expression to be restricted by IS NULL. + * + * @return string + */ + public function isNull($x) + { + return $x . ' IS NULL'; + } + + /** + * Creates an IS NOT NULL expression with the given arguments. + * + * @param string $x The expression to be restricted by IS NOT NULL. + * + * @return string + */ + public function isNotNull($x) + { + return $x . ' IS NOT NULL'; + } + + /** + * Creates a LIKE() comparison expression with the given arguments. + * + * @param string $x The expression to be inspected by the LIKE comparison + * @param mixed $y The pattern to compare against + * + * @return string + */ + public function like($x, $y/*, ?string $escapeChar = null */) + { + return $this->comparison($x, 'LIKE', $y) . + (func_num_args() >= 3 ? sprintf(' ESCAPE %s', func_get_arg(2)) : ''); + } + + /** + * Creates a NOT LIKE() comparison expression with the given arguments. + * + * @param string $x The expression to be inspected by the NOT LIKE comparison + * @param mixed $y The pattern to compare against + * + * @return string + */ + public function notLike($x, $y/*, ?string $escapeChar = null */) + { + return $this->comparison($x, 'NOT LIKE', $y) . + (func_num_args() >= 3 ? sprintf(' ESCAPE %s', func_get_arg(2)) : ''); + } + + /** + * Creates an IN () comparison expression with the given arguments. + * + * @param string $x The SQL expression to be matched against the set. + * @param string|string[] $y The SQL expression or an array of SQL expressions representing the set. + * + * @return string + */ + public function in($x, $y) + { + return $this->comparison($x, 'IN', '(' . implode(', ', (array) $y) . ')'); + } + + /** + * Creates a NOT IN () comparison expression with the given arguments. + * + * @param string $x The SQL expression to be matched against the set. + * @param string|string[] $y The SQL expression or an array of SQL expressions representing the set. + * + * @return string + */ + public function notIn($x, $y) + { + return $this->comparison($x, 'NOT IN', '(' . implode(', ', (array) $y) . ')'); + } + + /** + * Builds an SQL literal from a given input parameter. + * + * The usage of this method is discouraged. Use prepared statements + * or {@see AbstractPlatform::quoteStringLiteral()} instead. + * + * @param mixed $input The parameter to be quoted. + * @param int|null $type The type of the parameter. + * + * @return string + */ + public function literal($input, $type = null) + { + return $this->connection->quote($input, $type); + } +} diff --git a/3rdparty/doctrine/dbal/src/Query/ForUpdate.php b/3rdparty/doctrine/dbal/src/Query/ForUpdate.php new file mode 100644 index 00000000..fe54df90 --- /dev/null +++ b/3rdparty/doctrine/dbal/src/Query/ForUpdate.php @@ -0,0 +1,21 @@ +conflictResolutionMode = $conflictResolutionMode; + } + + public function getConflictResolutionMode(): int + { + return $this->conflictResolutionMode; + } +} diff --git a/3rdparty/doctrine/dbal/src/Query/ForUpdate/ConflictResolutionMode.php b/3rdparty/doctrine/dbal/src/Query/ForUpdate/ConflictResolutionMode.php new file mode 100644 index 00000000..f968f7b9 --- /dev/null +++ b/3rdparty/doctrine/dbal/src/Query/ForUpdate/ConflictResolutionMode.php @@ -0,0 +1,27 @@ +maxResults = $maxResults; + $this->firstResult = $firstResult; + } + + public function isDefined(): bool + { + return $this->maxResults !== null || $this->firstResult !== 0; + } + + public function getMaxResults(): ?int + { + return $this->maxResults; + } + + public function getFirstResult(): int + { + return $this->firstResult; + } +} diff --git a/3rdparty/doctrine/dbal/src/Query/QueryBuilder.php b/3rdparty/doctrine/dbal/src/Query/QueryBuilder.php new file mode 100644 index 00000000..208579c9 --- /dev/null +++ b/3rdparty/doctrine/dbal/src/Query/QueryBuilder.php @@ -0,0 +1,1759 @@ + [], + 'distinct' => false, + 'from' => [], + 'join' => [], + 'set' => [], + 'where' => null, + 'groupBy' => [], + 'having' => null, + 'orderBy' => [], + 'values' => [], + 'for_update' => null, + ]; + + /** + * The array of SQL parts collected. + * + * @var mixed[] + */ + private array $sqlParts = self::SQL_PARTS_DEFAULTS; + + /** + * The complete SQL string for this query. + */ + private ?string $sql = null; + + /** + * The query parameters. + * + * @var list|array + */ + private $params = []; + + /** + * The parameter type map of this query. + * + * @var array|array + */ + private array $paramTypes = []; + + /** + * The type of query this is. Can be select, update or delete. + * + * @phpstan-var self::SELECT|self::DELETE|self::UPDATE|self::INSERT + */ + private int $type = self::SELECT; + + /** + * The state of the query object. Can be dirty or clean. + * + * @phpstan-var self::STATE_* + */ + private int $state = self::STATE_CLEAN; + + /** + * The index of the first result to retrieve. + */ + private int $firstResult = 0; + + /** + * The maximum number of results to retrieve or NULL to retrieve all results. + */ + private ?int $maxResults = null; + + /** + * The counter of bound parameters used with {@see bindValue). + */ + private int $boundCounter = 0; + + /** + * The query cache profile used for caching results. + */ + private ?QueryCacheProfile $resultCacheProfile = null; + + /** + * Initializes a new QueryBuilder. + * + * @param Connection $connection The DBAL Connection. + */ + public function __construct(Connection $connection) + { + $this->connection = $connection; + } + + /** + * Gets an ExpressionBuilder used for object-oriented construction of query expressions. + * This producer method is intended for convenient inline usage. Example: + * + * + * $qb = $conn->createQueryBuilder() + * ->select('u') + * ->from('users', 'u') + * ->where($qb->expr()->eq('u.id', 1)); + * + * + * For more complex expression construction, consider storing the expression + * builder object in a local variable. + * + * @return ExpressionBuilder + */ + public function expr() + { + return $this->connection->getExpressionBuilder(); + } + + /** + * Gets the type of the currently built query. + * + * @deprecated If necessary, track the type of the query being built outside of the builder. + * + * @return int + */ + public function getType() + { + Deprecation::trigger( + 'doctrine/dbal', + 'https://github.com/doctrine/dbal/pull/5551', + 'Relying on the type of the query being built is deprecated.' + . ' If necessary, track the type of the query being built outside of the builder.', + ); + + return $this->type; + } + + /** + * Gets the associated DBAL Connection for this query builder. + * + * @deprecated Use the connection used to instantiate the builder instead. + * + * @return Connection + */ + public function getConnection() + { + Deprecation::trigger( + 'doctrine/dbal', + 'https://github.com/doctrine/dbal/pull/5780', + '%s is deprecated. Use the connection used to instantiate the builder instead.', + __METHOD__, + ); + + return $this->connection; + } + + /** + * Gets the state of this query builder instance. + * + * @deprecated The builder state is an internal concern. + * + * @return int Either QueryBuilder::STATE_DIRTY or QueryBuilder::STATE_CLEAN. + * @phpstan-return self::STATE_* + */ + public function getState() + { + Deprecation::trigger( + 'doctrine/dbal', + 'https://github.com/doctrine/dbal/pull/5551', + 'Relying on the query builder state is deprecated as it is an internal concern.', + ); + + return $this->state; + } + + /** + * Prepares and executes an SQL query and returns the first row of the result + * as an associative array. + * + * @return array|false False is returned if no rows are found. + * + * @throws Exception + */ + public function fetchAssociative() + { + return $this->executeQuery()->fetchAssociative(); + } + + /** + * Prepares and executes an SQL query and returns the first row of the result + * as a numerically indexed array. + * + * @return array|false False is returned if no rows are found. + * + * @throws Exception + */ + public function fetchNumeric() + { + return $this->executeQuery()->fetchNumeric(); + } + + /** + * Prepares and executes an SQL query and returns the value of a single column + * of the first row of the result. + * + * @return mixed|false False is returned if no rows are found. + * + * @throws Exception + */ + public function fetchOne() + { + return $this->executeQuery()->fetchOne(); + } + + /** + * Prepares and executes an SQL query and returns the result as an array of numeric arrays. + * + * @return array> + * + * @throws Exception + */ + public function fetchAllNumeric(): array + { + return $this->executeQuery()->fetchAllNumeric(); + } + + /** + * Prepares and executes an SQL query and returns the result as an array of associative arrays. + * + * @return array> + * + * @throws Exception + */ + public function fetchAllAssociative(): array + { + return $this->executeQuery()->fetchAllAssociative(); + } + + /** + * Prepares and executes an SQL query and returns the result as an associative array with the keys + * mapped to the first column and the values mapped to the second column. + * + * @return array + * + * @throws Exception + */ + public function fetchAllKeyValue(): array + { + return $this->executeQuery()->fetchAllKeyValue(); + } + + /** + * Prepares and executes an SQL query and returns the result as an associative array with the keys mapped + * to the first column and the values being an associative array representing the rest of the columns + * and their values. + * + * @return array> + * + * @throws Exception + */ + public function fetchAllAssociativeIndexed(): array + { + return $this->executeQuery()->fetchAllAssociativeIndexed(); + } + + /** + * Prepares and executes an SQL query and returns the result as an array of the first column values. + * + * @return array + * + * @throws Exception + */ + public function fetchFirstColumn(): array + { + return $this->executeQuery()->fetchFirstColumn(); + } + + /** + * Executes an SQL query (SELECT) and returns a Result. + * + * @throws Exception + */ + public function executeQuery(): Result + { + return $this->connection->executeQuery( + $this->getSQL(), + $this->params, + $this->paramTypes, + $this->resultCacheProfile, + ); + } + + /** + * Executes an SQL statement and returns the number of affected rows. + * + * Should be used for INSERT, UPDATE and DELETE + * + * @return int The number of affected rows. + * + * @throws Exception + */ + public function executeStatement(): int + { + return $this->connection->executeStatement($this->getSQL(), $this->params, $this->paramTypes); + } + + /** + * Executes this query using the bound parameters and their types. + * + * @deprecated Use {@see executeQuery()} or {@see executeStatement()} instead. + * + * @return Result|int|string + * + * @throws Exception + */ + public function execute() + { + if ($this->type === self::SELECT) { + Deprecation::trigger( + 'doctrine/dbal', + 'https://github.com/doctrine/dbal/pull/4578', + 'QueryBuilder::execute() is deprecated, use QueryBuilder::executeQuery() for SQL queries instead.', + ); + + return $this->executeQuery(); + } + + Deprecation::trigger( + 'doctrine/dbal', + 'https://github.com/doctrine/dbal/pull/4578', + 'QueryBuilder::execute() is deprecated, use QueryBuilder::executeStatement() for SQL statements instead.', + ); + + return $this->connection->executeStatement($this->getSQL(), $this->params, $this->paramTypes); + } + + /** + * Gets the complete SQL string formed by the current specifications of this QueryBuilder. + * + * + * $qb = $em->createQueryBuilder() + * ->select('u') + * ->from('User', 'u') + * echo $qb->getSQL(); // SELECT u FROM User u + * + * + * @return string The SQL query string. + */ + public function getSQL() + { + if ($this->sql !== null && $this->state === self::STATE_CLEAN) { + return $this->sql; + } + + switch ($this->type) { + case self::INSERT: + $sql = $this->getSQLForInsert(); + break; + + case self::DELETE: + $sql = $this->getSQLForDelete(); + break; + + case self::UPDATE: + $sql = $this->getSQLForUpdate(); + break; + + case self::SELECT: + $sql = $this->getSQLForSelect(); + break; + } + + $this->state = self::STATE_CLEAN; + $this->sql = $sql; + + return $sql; + } + + /** + * Sets a query parameter for the query being constructed. + * + * + * $qb = $conn->createQueryBuilder() + * ->select('u') + * ->from('users', 'u') + * ->where('u.id = :user_id') + * ->setParameter('user_id', 1); + * + * + * @param int|string $key Parameter position or name + * @param mixed $value Parameter value + * @param int|string|Type|null $type Parameter type + * + * @return $this This QueryBuilder instance. + */ + public function setParameter($key, $value, $type = ParameterType::STRING) + { + if ($type !== null) { + $this->paramTypes[$key] = $type; + } else { + Deprecation::trigger( + 'doctrine/dbal', + 'https://github.com/doctrine/dbal/pull/5550', + 'Using NULL as prepared statement parameter type is deprecated.' + . 'Omit or use ParameterType::STRING instead', + ); + } + + $this->params[$key] = $value; + + return $this; + } + + /** + * Sets a collection of query parameters for the query being constructed. + * + * + * $qb = $conn->createQueryBuilder() + * ->select('u') + * ->from('users', 'u') + * ->where('u.id = :user_id1 OR u.id = :user_id2') + * ->setParameters(array( + * 'user_id1' => 1, + * 'user_id2' => 2 + * )); + * + * + * @param list|array $params Parameters to set + * @param array|array $types Parameter types + * + * @return $this This QueryBuilder instance. + */ + public function setParameters(array $params, array $types = []) + { + $this->paramTypes = $types; + $this->params = $params; + + return $this; + } + + /** + * Gets all defined query parameters for the query being constructed indexed by parameter index or name. + * + * @return list|array The currently defined query parameters + */ + public function getParameters() + { + return $this->params; + } + + /** + * Gets a (previously set) query parameter of the query being constructed. + * + * @param mixed $key The key (index or name) of the bound parameter. + * + * @return mixed The value of the bound parameter. + */ + public function getParameter($key) + { + return $this->params[$key] ?? null; + } + + /** + * Gets all defined query parameter types for the query being constructed indexed by parameter index or name. + * + * @return array|array The currently defined + * query parameter types + */ + public function getParameterTypes() + { + return $this->paramTypes; + } + + /** + * Gets a (previously set) query parameter type of the query being constructed. + * + * @param int|string $key The key of the bound parameter type + * + * @return int|string|Type The value of the bound parameter type + */ + public function getParameterType($key) + { + return $this->paramTypes[$key] ?? ParameterType::STRING; + } + + /** + * Sets the position of the first result to retrieve (the "offset"). + * + * @param int $firstResult The first result to return. + * + * @return $this This QueryBuilder instance. + */ + public function setFirstResult($firstResult) + { + $this->state = self::STATE_DIRTY; + $this->firstResult = $firstResult; + + return $this; + } + + /** + * Gets the position of the first result the query object was set to retrieve (the "offset"). + * + * @return int The position of the first result. + */ + public function getFirstResult() + { + return $this->firstResult; + } + + /** + * Sets the maximum number of results to retrieve (the "limit"). + * + * @param int|null $maxResults The maximum number of results to retrieve or NULL to retrieve all results. + * + * @return $this This QueryBuilder instance. + */ + public function setMaxResults($maxResults) + { + $this->state = self::STATE_DIRTY; + $this->maxResults = $maxResults; + + return $this; + } + + /** + * Gets the maximum number of results the query object was set to retrieve (the "limit"). + * Returns NULL if all results will be returned. + * + * @return int|null The maximum number of results. + */ + public function getMaxResults() + { + return $this->maxResults; + } + + /** + * Locks the queried rows for a subsequent update. + * + * @return $this + */ + public function forUpdate(int $conflictResolutionMode = ConflictResolutionMode::ORDINARY): self + { + $this->state = self::STATE_DIRTY; + + $this->sqlParts['for_update'] = new ForUpdate($conflictResolutionMode); + + return $this; + } + + /** + * Either appends to or replaces a single, generic query part. + * + * The available parts are: 'select', 'from', 'set', 'where', + * 'groupBy', 'having' and 'orderBy'. + * + * @param string $sqlPartName + * @param mixed $sqlPart + * @param bool $append + * + * @return $this This QueryBuilder instance. + */ + public function add($sqlPartName, $sqlPart, $append = false) + { + $isArray = is_array($sqlPart); + $isMultiple = is_array($this->sqlParts[$sqlPartName]); + + if ($isMultiple && ! $isArray) { + $sqlPart = [$sqlPart]; + } + + $this->state = self::STATE_DIRTY; + + if ($append) { + if ( + $sqlPartName === 'orderBy' + || $sqlPartName === 'groupBy' + || $sqlPartName === 'select' + || $sqlPartName === 'set' + ) { + foreach ($sqlPart as $part) { + $this->sqlParts[$sqlPartName][] = $part; + } + } elseif ($isArray && is_array($sqlPart[key($sqlPart)])) { + $key = key($sqlPart); + $this->sqlParts[$sqlPartName][$key][] = $sqlPart[$key]; + } elseif ($isMultiple) { + $this->sqlParts[$sqlPartName][] = $sqlPart; + } else { + $this->sqlParts[$sqlPartName] = $sqlPart; + } + + return $this; + } + + $this->sqlParts[$sqlPartName] = $sqlPart; + + return $this; + } + + /** + * Specifies an item that is to be returned in the query result. + * Replaces any previously specified selections, if any. + * + * USING AN ARRAY ARGUMENT IS DEPRECATED. Pass each value as an individual argument. + * + * + * $qb = $conn->createQueryBuilder() + * ->select('u.id', 'p.id') + * ->from('users', 'u') + * ->leftJoin('u', 'phonenumbers', 'p', 'u.id = p.user_id'); + * + * + * @param string|string[]|null $select The selection expression. USING AN ARRAY OR NULL IS DEPRECATED. + * Pass each value as an individual argument. + * + * @return $this This QueryBuilder instance. + */ + public function select($select = null/*, string ...$selects*/) + { + $this->type = self::SELECT; + + if ($select === null) { + return $this; + } + + if (is_array($select)) { + Deprecation::trigger( + 'doctrine/dbal', + 'https://github.com/doctrine/dbal/issues/3837', + 'Passing an array for the first argument to QueryBuilder::select() is deprecated, ' . + 'pass each value as an individual variadic argument instead.', + ); + } + + $selects = is_array($select) ? $select : func_get_args(); + + return $this->add('select', $selects); + } + + /** + * Adds or removes DISTINCT to/from the query. + * + * + * $qb = $conn->createQueryBuilder() + * ->select('u.id') + * ->distinct() + * ->from('users', 'u') + * + * + * @return $this This QueryBuilder instance. + */ + public function distinct(/* bool $distinct = true */): self + { + $this->sqlParts['distinct'] = func_num_args() < 1 || func_get_arg(0); + $this->state = self::STATE_DIRTY; + + return $this; + } + + /** + * Adds an item that is to be returned in the query result. + * + * USING AN ARRAY ARGUMENT IS DEPRECATED. Pass each value as an individual argument. + * + * + * $qb = $conn->createQueryBuilder() + * ->select('u.id') + * ->addSelect('p.id') + * ->from('users', 'u') + * ->leftJoin('u', 'phonenumbers', 'u.id = p.user_id'); + * + * + * @param string|string[]|null $select The selection expression. USING AN ARRAY OR NULL IS DEPRECATED. + * Pass each value as an individual argument. + * + * @return $this This QueryBuilder instance. + */ + public function addSelect($select = null/*, string ...$selects*/) + { + $this->type = self::SELECT; + + if ($select === null) { + return $this; + } + + if (is_array($select)) { + Deprecation::trigger( + 'doctrine/dbal', + 'https://github.com/doctrine/dbal/issues/3837', + 'Passing an array for the first argument to QueryBuilder::addSelect() is deprecated, ' . + 'pass each value as an individual variadic argument instead.', + ); + } + + $selects = is_array($select) ? $select : func_get_args(); + + return $this->add('select', $selects, true); + } + + /** + * Turns the query being built into a bulk delete query that ranges over + * a certain table. + * + * + * $qb = $conn->createQueryBuilder() + * ->delete('users', 'u') + * ->where('u.id = :user_id') + * ->setParameter(':user_id', 1); + * + * + * @param string $delete The table whose rows are subject to the deletion. + * @param string $alias The table alias used in the constructed query. + * + * @return $this This QueryBuilder instance. + */ + public function delete($delete = null, $alias = null) + { + $this->type = self::DELETE; + + if ($delete === null) { + return $this; + } + + return $this->add('from', [ + 'table' => $delete, + 'alias' => $alias, + ]); + } + + /** + * Turns the query being built into a bulk update query that ranges over + * a certain table + * + * + * $qb = $conn->createQueryBuilder() + * ->update('counters', 'c') + * ->set('c.value', 'c.value + 1') + * ->where('c.id = ?'); + * + * + * @param string $update The table whose rows are subject to the update. + * @param string $alias The table alias used in the constructed query. + * + * @return $this This QueryBuilder instance. + */ + public function update($update = null, $alias = null) + { + $this->type = self::UPDATE; + + if ($update === null) { + return $this; + } + + return $this->add('from', [ + 'table' => $update, + 'alias' => $alias, + ]); + } + + /** + * Turns the query being built into an insert query that inserts into + * a certain table + * + * + * $qb = $conn->createQueryBuilder() + * ->insert('users') + * ->values( + * array( + * 'name' => '?', + * 'password' => '?' + * ) + * ); + * + * + * @param string $insert The table into which the rows should be inserted. + * + * @return $this This QueryBuilder instance. + */ + public function insert($insert = null) + { + $this->type = self::INSERT; + + if ($insert === null) { + return $this; + } + + return $this->add('from', ['table' => $insert]); + } + + /** + * Creates and adds a query root corresponding to the table identified by the + * given alias, forming a cartesian product with any existing query roots. + * + * + * $qb = $conn->createQueryBuilder() + * ->select('u.id') + * ->from('users', 'u') + * + * + * @param string $from The table. + * @param string|null $alias The alias of the table. + * + * @return $this This QueryBuilder instance. + */ + public function from($from, $alias = null) + { + return $this->add('from', [ + 'table' => $from, + 'alias' => $alias, + ], true); + } + + /** + * Creates and adds a join to the query. + * + * + * $qb = $conn->createQueryBuilder() + * ->select('u.name') + * ->from('users', 'u') + * ->join('u', 'phonenumbers', 'p', 'p.is_primary = 1'); + * + * + * @param string $fromAlias The alias that points to a from clause. + * @param string $join The table name to join. + * @param string $alias The alias of the join table. + * @param string $condition The condition for the join. + * + * @return $this This QueryBuilder instance. + */ + public function join($fromAlias, $join, $alias, $condition = null) + { + return $this->innerJoin($fromAlias, $join, $alias, $condition); + } + + /** + * Creates and adds a join to the query. + * + * + * $qb = $conn->createQueryBuilder() + * ->select('u.name') + * ->from('users', 'u') + * ->innerJoin('u', 'phonenumbers', 'p', 'p.is_primary = 1'); + * + * + * @param string $fromAlias The alias that points to a from clause. + * @param string $join The table name to join. + * @param string $alias The alias of the join table. + * @param string $condition The condition for the join. + * + * @return $this This QueryBuilder instance. + */ + public function innerJoin($fromAlias, $join, $alias, $condition = null) + { + return $this->add('join', [ + $fromAlias => [ + 'joinType' => 'inner', + 'joinTable' => $join, + 'joinAlias' => $alias, + 'joinCondition' => $condition, + ], + ], true); + } + + /** + * Creates and adds a left join to the query. + * + * + * $qb = $conn->createQueryBuilder() + * ->select('u.name') + * ->from('users', 'u') + * ->leftJoin('u', 'phonenumbers', 'p', 'p.is_primary = 1'); + * + * + * @param string $fromAlias The alias that points to a from clause. + * @param string $join The table name to join. + * @param string $alias The alias of the join table. + * @param string $condition The condition for the join. + * + * @return $this This QueryBuilder instance. + */ + public function leftJoin($fromAlias, $join, $alias, $condition = null) + { + return $this->add('join', [ + $fromAlias => [ + 'joinType' => 'left', + 'joinTable' => $join, + 'joinAlias' => $alias, + 'joinCondition' => $condition, + ], + ], true); + } + + /** + * Creates and adds a right join to the query. + * + * + * $qb = $conn->createQueryBuilder() + * ->select('u.name') + * ->from('users', 'u') + * ->rightJoin('u', 'phonenumbers', 'p', 'p.is_primary = 1'); + * + * + * @param string $fromAlias The alias that points to a from clause. + * @param string $join The table name to join. + * @param string $alias The alias of the join table. + * @param string $condition The condition for the join. + * + * @return $this This QueryBuilder instance. + */ + public function rightJoin($fromAlias, $join, $alias, $condition = null) + { + return $this->add('join', [ + $fromAlias => [ + 'joinType' => 'right', + 'joinTable' => $join, + 'joinAlias' => $alias, + 'joinCondition' => $condition, + ], + ], true); + } + + /** + * Sets a new value for a column in a bulk update query. + * + * + * $qb = $conn->createQueryBuilder() + * ->update('counters', 'c') + * ->set('c.value', 'c.value + 1') + * ->where('c.id = ?'); + * + * + * @param string $key The column to set. + * @param string $value The value, expression, placeholder, etc. + * + * @return $this This QueryBuilder instance. + */ + public function set($key, $value) + { + return $this->add('set', $key . ' = ' . $value, true); + } + + /** + * Specifies one or more restrictions to the query result. + * Replaces any previously specified restrictions, if any. + * + * + * $qb = $conn->createQueryBuilder() + * ->select('c.value') + * ->from('counters', 'c') + * ->where('c.id = ?'); + * + * // You can optionally programmatically build and/or expressions + * $qb = $conn->createQueryBuilder(); + * + * $or = $qb->expr()->orx(); + * $or->add($qb->expr()->eq('c.id', 1)); + * $or->add($qb->expr()->eq('c.id', 2)); + * + * $qb->update('counters', 'c') + * ->set('c.value', 'c.value + 1') + * ->where($or); + * + * + * @param mixed $predicates The restriction predicates. + * + * @return $this This QueryBuilder instance. + */ + public function where($predicates) + { + if (! (func_num_args() === 1 && $predicates instanceof CompositeExpression)) { + $predicates = CompositeExpression::and(...func_get_args()); + } + + return $this->add('where', $predicates); + } + + /** + * Adds one or more restrictions to the query results, forming a logical + * conjunction with any previously specified restrictions. + * + * + * $qb = $conn->createQueryBuilder() + * ->select('u') + * ->from('users', 'u') + * ->where('u.username LIKE ?') + * ->andWhere('u.is_active = 1'); + * + * + * @see where() + * + * @param mixed $where The query restrictions. + * + * @return $this This QueryBuilder instance. + */ + public function andWhere($where) + { + $args = func_get_args(); + $where = $this->getQueryPart('where'); + + if ($where instanceof CompositeExpression && $where->getType() === CompositeExpression::TYPE_AND) { + $where = $where->with(...$args); + } else { + array_unshift($args, $where); + $where = CompositeExpression::and(...$args); + } + + return $this->add('where', $where, true); + } + + /** + * Adds one or more restrictions to the query results, forming a logical + * disjunction with any previously specified restrictions. + * + * + * $qb = $em->createQueryBuilder() + * ->select('u.name') + * ->from('users', 'u') + * ->where('u.id = 1') + * ->orWhere('u.id = 2'); + * + * + * @see where() + * + * @param mixed $where The WHERE statement. + * + * @return $this This QueryBuilder instance. + */ + public function orWhere($where) + { + $args = func_get_args(); + $where = $this->getQueryPart('where'); + + if ($where instanceof CompositeExpression && $where->getType() === CompositeExpression::TYPE_OR) { + $where = $where->with(...$args); + } else { + array_unshift($args, $where); + $where = CompositeExpression::or(...$args); + } + + return $this->add('where', $where, true); + } + + /** + * Specifies a grouping over the results of the query. + * Replaces any previously specified groupings, if any. + * + * USING AN ARRAY ARGUMENT IS DEPRECATED. Pass each value as an individual argument. + * + * + * $qb = $conn->createQueryBuilder() + * ->select('u.name') + * ->from('users', 'u') + * ->groupBy('u.id'); + * + * + * @param string|string[] $groupBy The grouping expression. USING AN ARRAY IS DEPRECATED. + * Pass each value as an individual argument. + * + * @return $this This QueryBuilder instance. + */ + public function groupBy($groupBy/*, string ...$groupBys*/) + { + if (is_array($groupBy) && count($groupBy) === 0) { + return $this; + } + + if (is_array($groupBy)) { + Deprecation::trigger( + 'doctrine/dbal', + 'https://github.com/doctrine/dbal/issues/3837', + 'Passing an array for the first argument to QueryBuilder::groupBy() is deprecated, ' . + 'pass each value as an individual variadic argument instead.', + ); + } + + $groupBy = is_array($groupBy) ? $groupBy : func_get_args(); + + return $this->add('groupBy', $groupBy, false); + } + + /** + * Adds a grouping expression to the query. + * + * USING AN ARRAY ARGUMENT IS DEPRECATED. Pass each value as an individual argument. + * + * + * $qb = $conn->createQueryBuilder() + * ->select('u.name') + * ->from('users', 'u') + * ->groupBy('u.lastLogin') + * ->addGroupBy('u.createdAt'); + * + * + * @param string|string[] $groupBy The grouping expression. USING AN ARRAY IS DEPRECATED. + * Pass each value as an individual argument. + * + * @return $this This QueryBuilder instance. + */ + public function addGroupBy($groupBy/*, string ...$groupBys*/) + { + if (is_array($groupBy) && count($groupBy) === 0) { + return $this; + } + + if (is_array($groupBy)) { + Deprecation::trigger( + 'doctrine/dbal', + 'https://github.com/doctrine/dbal/issues/3837', + 'Passing an array for the first argument to QueryBuilder::addGroupBy() is deprecated, ' . + 'pass each value as an individual variadic argument instead.', + ); + } + + $groupBy = is_array($groupBy) ? $groupBy : func_get_args(); + + return $this->add('groupBy', $groupBy, true); + } + + /** + * Sets a value for a column in an insert query. + * + * + * $qb = $conn->createQueryBuilder() + * ->insert('users') + * ->values( + * array( + * 'name' => '?' + * ) + * ) + * ->setValue('password', '?'); + * + * + * @param string $column The column into which the value should be inserted. + * @param string $value The value that should be inserted into the column. + * + * @return $this This QueryBuilder instance. + */ + public function setValue($column, $value) + { + $this->sqlParts['values'][$column] = $value; + + return $this; + } + + /** + * Specifies values for an insert query indexed by column names. + * Replaces any previous values, if any. + * + * + * $qb = $conn->createQueryBuilder() + * ->insert('users') + * ->values( + * array( + * 'name' => '?', + * 'password' => '?' + * ) + * ); + * + * + * @param mixed[] $values The values to specify for the insert query indexed by column names. + * + * @return $this This QueryBuilder instance. + */ + public function values(array $values) + { + return $this->add('values', $values); + } + + /** + * Specifies a restriction over the groups of the query. + * Replaces any previous having restrictions, if any. + * + * @param mixed $having The restriction over the groups. + * + * @return $this This QueryBuilder instance. + */ + public function having($having) + { + if (! (func_num_args() === 1 && $having instanceof CompositeExpression)) { + $having = CompositeExpression::and(...func_get_args()); + } + + return $this->add('having', $having); + } + + /** + * Adds a restriction over the groups of the query, forming a logical + * conjunction with any existing having restrictions. + * + * @param mixed $having The restriction to append. + * + * @return $this This QueryBuilder instance. + */ + public function andHaving($having) + { + $args = func_get_args(); + $having = $this->getQueryPart('having'); + + if ($having instanceof CompositeExpression && $having->getType() === CompositeExpression::TYPE_AND) { + $having = $having->with(...$args); + } else { + array_unshift($args, $having); + $having = CompositeExpression::and(...$args); + } + + return $this->add('having', $having); + } + + /** + * Adds a restriction over the groups of the query, forming a logical + * disjunction with any existing having restrictions. + * + * @param mixed $having The restriction to add. + * + * @return $this This QueryBuilder instance. + */ + public function orHaving($having) + { + $args = func_get_args(); + $having = $this->getQueryPart('having'); + + if ($having instanceof CompositeExpression && $having->getType() === CompositeExpression::TYPE_OR) { + $having = $having->with(...$args); + } else { + array_unshift($args, $having); + $having = CompositeExpression::or(...$args); + } + + return $this->add('having', $having); + } + + /** + * Specifies an ordering for the query results. + * Replaces any previously specified orderings, if any. + * + * @param string $sort The ordering expression. + * @param string $order The ordering direction. + * + * @return $this This QueryBuilder instance. + */ + public function orderBy($sort, $order = null) + { + return $this->add('orderBy', $sort . ' ' . ($order ?? 'ASC'), false); + } + + /** + * Adds an ordering to the query results. + * + * @param string $sort The ordering expression. + * @param string $order The ordering direction. + * + * @return $this This QueryBuilder instance. + */ + public function addOrderBy($sort, $order = null) + { + return $this->add('orderBy', $sort . ' ' . ($order ?? 'ASC'), true); + } + + /** + * Gets a query part by its name. + * + * @deprecated The query parts are implementation details and should not be relied upon. + * + * @param string $queryPartName + * + * @return mixed + */ + public function getQueryPart($queryPartName) + { + Deprecation::triggerIfCalledFromOutside( + 'doctrine/dbal', + 'https://github.com/doctrine/dbal/pull/6179', + 'Getting query parts is deprecated as they are implementation details.', + ); + + return $this->sqlParts[$queryPartName]; + } + + /** + * Gets all query parts. + * + * @deprecated The query parts are implementation details and should not be relied upon. + * + * @return mixed[] + */ + public function getQueryParts() + { + Deprecation::trigger( + 'doctrine/dbal', + 'https://github.com/doctrine/dbal/pull/6179', + 'Getting query parts is deprecated as they are implementation details.', + ); + + return $this->sqlParts; + } + + /** + * Resets SQL parts. + * + * @deprecated Use the dedicated reset*() methods instead. + * + * @param string[]|null $queryPartNames + * + * @return $this This QueryBuilder instance. + */ + public function resetQueryParts($queryPartNames = null) + { + Deprecation::trigger( + 'doctrine/dbal', + 'https://github.com/doctrine/dbal/pull/6193', + '%s() is deprecated, instead use dedicated reset methods for the parts that shall be reset.', + __METHOD__, + ); + + $queryPartNames ??= array_keys($this->sqlParts); + + foreach ($queryPartNames as $queryPartName) { + $this->sqlParts[$queryPartName] = self::SQL_PARTS_DEFAULTS[$queryPartName]; + } + + $this->state = self::STATE_DIRTY; + + return $this; + } + + /** + * Resets a single SQL part. + * + * @deprecated Use the dedicated reset*() methods instead. + * + * @param string $queryPartName + * + * @return $this This QueryBuilder instance. + */ + public function resetQueryPart($queryPartName) + { + if ($queryPartName === 'distinct') { + Deprecation::trigger( + 'doctrine/dbal', + 'https://github.com/doctrine/dbal/pull/6193', + 'Calling %s() with "distinct" is deprecated, call distinct(false) instead.', + __METHOD__, + ); + + return $this->distinct(false); + } + + $newMethodName = 'reset' . ucfirst($queryPartName); + if (array_key_exists($queryPartName, self::SQL_PARTS_DEFAULTS) && method_exists($this, $newMethodName)) { + Deprecation::trigger( + 'doctrine/dbal', + 'https://github.com/doctrine/dbal/pull/6193', + 'Calling %s() with "%s" is deprecated, call %s() instead.', + __METHOD__, + $queryPartName, + $newMethodName, + ); + + return $this->$newMethodName(); + } + + Deprecation::trigger( + 'doctrine/dbal', + 'https://github.com/doctrine/dbal/pull/6193', + 'Calling %s() with "%s" is deprecated without replacement.', + __METHOD__, + $queryPartName, + $newMethodName, + ); + + $this->sqlParts[$queryPartName] = self::SQL_PARTS_DEFAULTS[$queryPartName]; + + $this->state = self::STATE_DIRTY; + + return $this; + } + + /** + * Resets the WHERE conditions for the query. + * + * @return $this This QueryBuilder instance. + */ + public function resetWhere(): self + { + $this->sqlParts['where'] = self::SQL_PARTS_DEFAULTS['where']; + + $this->state = self::STATE_DIRTY; + + return $this; + } + + /** + * Resets the grouping for the query. + * + * @return $this This QueryBuilder instance. + */ + public function resetGroupBy(): self + { + $this->sqlParts['groupBy'] = self::SQL_PARTS_DEFAULTS['groupBy']; + + $this->state = self::STATE_DIRTY; + + return $this; + } + + /** + * Resets the HAVING conditions for the query. + * + * @return $this This QueryBuilder instance. + */ + public function resetHaving(): self + { + $this->sqlParts['having'] = self::SQL_PARTS_DEFAULTS['having']; + + $this->state = self::STATE_DIRTY; + + return $this; + } + + /** + * Resets the ordering for the query. + * + * @return $this This QueryBuilder instance. + */ + public function resetOrderBy(): self + { + $this->sqlParts['orderBy'] = self::SQL_PARTS_DEFAULTS['orderBy']; + + $this->state = self::STATE_DIRTY; + + return $this; + } + + /** @throws Exception */ + private function getSQLForSelect(): string + { + return $this->connection->getDatabasePlatform() + ->createSelectSQLBuilder() + ->buildSQL( + new SelectQuery( + $this->sqlParts['distinct'], + $this->sqlParts['select'], + $this->getFromClauses(), + $this->sqlParts['where'], + $this->sqlParts['groupBy'], + $this->sqlParts['having'], + $this->sqlParts['orderBy'], + new Limit($this->maxResults, $this->firstResult), + $this->sqlParts['for_update'], + ), + ); + } + + /** + * @return string[] + * + * @throws QueryException + */ + private function getFromClauses(): array + { + $fromClauses = []; + $knownAliases = []; + + // Loop through all FROM clauses + foreach ($this->sqlParts['from'] as $from) { + if ($from['alias'] === null) { + $tableSql = $from['table']; + $tableReference = $from['table']; + } else { + $tableSql = $from['table'] . ' ' . $from['alias']; + $tableReference = $from['alias']; + } + + $knownAliases[$tableReference] = true; + + $fromClauses[$tableReference] = $tableSql . $this->getSQLForJoins($tableReference, $knownAliases); + } + + $this->verifyAllAliasesAreKnown($knownAliases); + + return $fromClauses; + } + + /** + * @param array $knownAliases + * + * @throws QueryException + */ + private function verifyAllAliasesAreKnown(array $knownAliases): void + { + foreach ($this->sqlParts['join'] as $fromAlias => $joins) { + if (! isset($knownAliases[$fromAlias])) { + throw QueryException::unknownAlias($fromAlias, array_keys($knownAliases)); + } + } + } + + /** + * Converts this instance into an INSERT string in SQL. + */ + private function getSQLForInsert(): string + { + return 'INSERT INTO ' . $this->sqlParts['from']['table'] . + ' (' . implode(', ', array_keys($this->sqlParts['values'])) . ')' . + ' VALUES(' . implode(', ', $this->sqlParts['values']) . ')'; + } + + /** + * Converts this instance into an UPDATE string in SQL. + */ + private function getSQLForUpdate(): string + { + $table = $this->sqlParts['from']['table'] + . ($this->sqlParts['from']['alias'] ? ' ' . $this->sqlParts['from']['alias'] : ''); + + return 'UPDATE ' . $table + . ' SET ' . implode(', ', $this->sqlParts['set']) + . ($this->sqlParts['where'] !== null ? ' WHERE ' . ((string) $this->sqlParts['where']) : ''); + } + + /** + * Converts this instance into a DELETE string in SQL. + */ + private function getSQLForDelete(): string + { + $table = $this->sqlParts['from']['table'] + . ($this->sqlParts['from']['alias'] ? ' ' . $this->sqlParts['from']['alias'] : ''); + + return 'DELETE FROM ' . $table + . ($this->sqlParts['where'] !== null ? ' WHERE ' . ((string) $this->sqlParts['where']) : ''); + } + + /** + * Gets a string representation of this QueryBuilder which corresponds to + * the final SQL query being constructed. + * + * @return string The string representation of this QueryBuilder. + */ + public function __toString() + { + return $this->getSQL(); + } + + /** + * Creates a new named parameter and bind the value $value to it. + * + * This method provides a shortcut for {@see Statement::bindValue()} + * when using prepared statements. + * + * The parameter $value specifies the value that you want to bind. If + * $placeholder is not provided createNamedParameter() will automatically + * create a placeholder for you. An automatic placeholder will be of the + * name ':dcValue1', ':dcValue2' etc. + * + * Example: + * + * $value = 2; + * $q->eq( 'id', $q->createNamedParameter( $value ) ); + * $stmt = $q->executeQuery(); // executed with 'id = 2' + * + * + * @link http://www.zetacomponents.org + * + * @param mixed $value + * @param int|string|Type|null $type + * @param string $placeHolder The name to bind with. The string must start with a colon ':'. + * + * @return string the placeholder name used. + */ + public function createNamedParameter($value, $type = ParameterType::STRING, $placeHolder = null) + { + if ($placeHolder === null) { + $this->boundCounter++; + $placeHolder = ':dcValue' . $this->boundCounter; + } + + $this->setParameter(substr($placeHolder, 1), $value, $type); + + return $placeHolder; + } + + /** + * Creates a new positional parameter and bind the given value to it. + * + * Attention: If you are using positional parameters with the query builder you have + * to be very careful to bind all parameters in the order they appear in the SQL + * statement , otherwise they get bound in the wrong order which can lead to serious + * bugs in your code. + * + * Example: + * + * $qb = $conn->createQueryBuilder(); + * $qb->select('u.*') + * ->from('users', 'u') + * ->where('u.username = ' . $qb->createPositionalParameter('Foo', ParameterType::STRING)) + * ->orWhere('u.username = ' . $qb->createPositionalParameter('Bar', ParameterType::STRING)) + * + * + * @param mixed $value + * @param int|string|Type|null $type + * + * @return string + */ + public function createPositionalParameter($value, $type = ParameterType::STRING) + { + $this->setParameter($this->boundCounter, $value, $type); + $this->boundCounter++; + + return '?'; + } + + /** + * @param string $fromAlias + * @param array $knownAliases + * + * @throws QueryException + */ + private function getSQLForJoins($fromAlias, array &$knownAliases): string + { + $sql = ''; + + if (isset($this->sqlParts['join'][$fromAlias])) { + foreach ($this->sqlParts['join'][$fromAlias] as $join) { + if (array_key_exists($join['joinAlias'], $knownAliases)) { + throw QueryException::nonUniqueAlias((string) $join['joinAlias'], array_keys($knownAliases)); + } + + $sql .= ' ' . strtoupper($join['joinType']) + . ' JOIN ' . $join['joinTable'] . ' ' . $join['joinAlias']; + if ($join['joinCondition'] !== null) { + $sql .= ' ON ' . $join['joinCondition']; + } + + $knownAliases[$join['joinAlias']] = true; + } + + foreach ($this->sqlParts['join'][$fromAlias] as $join) { + $sql .= $this->getSQLForJoins($join['joinAlias'], $knownAliases); + } + } + + return $sql; + } + + /** + * Deep clone of all expression objects in the SQL parts. + * + * @return void + */ + public function __clone() + { + foreach ($this->sqlParts as $part => $elements) { + if (is_array($this->sqlParts[$part])) { + foreach ($this->sqlParts[$part] as $idx => $element) { + if (! is_object($element)) { + continue; + } + + $this->sqlParts[$part][$idx] = clone $element; + } + } elseif (is_object($elements)) { + $this->sqlParts[$part] = clone $elements; + } + } + + foreach ($this->params as $name => $param) { + if (! is_object($param)) { + continue; + } + + $this->params[$name] = clone $param; + } + } + + /** + * Enables caching of the results of this query, for given amount of seconds + * and optionally specified which key to use for the cache entry. + * + * @return $this + */ + public function enableResultCache(QueryCacheProfile $cacheProfile): self + { + $this->resultCacheProfile = $cacheProfile; + + return $this; + } + + /** + * Disables caching of the results of this query. + * + * @return $this + */ + public function disableResultCache(): self + { + $this->resultCacheProfile = null; + + return $this; + } +} diff --git a/3rdparty/doctrine/dbal/src/Query/QueryException.php b/3rdparty/doctrine/dbal/src/Query/QueryException.php new file mode 100644 index 00000000..bfb5eafc --- /dev/null +++ b/3rdparty/doctrine/dbal/src/Query/QueryException.php @@ -0,0 +1,36 @@ +distinct = $distinct; + $this->columns = $columns; + $this->from = $from; + $this->where = $where; + $this->groupBy = $groupBy; + $this->having = $having; + $this->orderBy = $orderBy; + $this->limit = $limit; + $this->forUpdate = $forUpdate; + } + + public function isDistinct(): bool + { + return $this->distinct; + } + + /** @return string[] */ + public function getColumns(): array + { + return $this->columns; + } + + /** @return string[] */ + public function getFrom(): array + { + return $this->from; + } + + public function getWhere(): ?string + { + return $this->where; + } + + /** @return string[] */ + public function getGroupBy(): array + { + return $this->groupBy; + } + + public function getHaving(): ?string + { + return $this->having; + } + + /** @return string[] */ + public function getOrderBy(): array + { + return $this->orderBy; + } + + public function getLimit(): Limit + { + return $this->limit; + } + + public function getForUpdate(): ?ForUpdate + { + return $this->forUpdate; + } +} diff --git a/3rdparty/doctrine/dbal/src/Result.php b/3rdparty/doctrine/dbal/src/Result.php new file mode 100644 index 00000000..f63b07f0 --- /dev/null +++ b/3rdparty/doctrine/dbal/src/Result.php @@ -0,0 +1,339 @@ +result = $result; + $this->connection = $connection; + } + + /** + * Returns the next row of the result as a numeric array or FALSE if there are no more rows. + * + * @return list|false + * + * @throws Exception + */ + public function fetchNumeric() + { + try { + return $this->result->fetchNumeric(); + } catch (DriverException $e) { + throw $this->connection->convertException($e); + } + } + + /** + * Returns the next row of the result as an associative array or FALSE if there are no more rows. + * + * @return array|false + * + * @throws Exception + */ + public function fetchAssociative() + { + try { + return $this->result->fetchAssociative(); + } catch (DriverException $e) { + throw $this->connection->convertException($e); + } + } + + /** + * Returns the first value of the next row of the result or FALSE if there are no more rows. + * + * @return mixed|false + * + * @throws Exception + */ + public function fetchOne() + { + try { + return $this->result->fetchOne(); + } catch (DriverException $e) { + throw $this->connection->convertException($e); + } + } + + /** + * Returns an array containing all of the result rows represented as numeric arrays. + * + * @return list> + * + * @throws Exception + */ + public function fetchAllNumeric(): array + { + try { + return $this->result->fetchAllNumeric(); + } catch (DriverException $e) { + throw $this->connection->convertException($e); + } + } + + /** + * Returns an array containing all of the result rows represented as associative arrays. + * + * @return list> + * + * @throws Exception + */ + public function fetchAllAssociative(): array + { + try { + return $this->result->fetchAllAssociative(); + } catch (DriverException $e) { + throw $this->connection->convertException($e); + } + } + + /** + * Returns an array containing the values of the first column of the result. + * + * @return array + * + * @throws Exception + */ + public function fetchAllKeyValue(): array + { + $this->ensureHasKeyValue(); + + $data = []; + + foreach ($this->fetchAllNumeric() as [$key, $value]) { + $data[$key] = $value; + } + + return $data; + } + + /** + * Returns an associative array with the keys mapped to the first column and the values being + * an associative array representing the rest of the columns and their values. + * + * @return array> + * + * @throws Exception + */ + public function fetchAllAssociativeIndexed(): array + { + $data = []; + + foreach ($this->fetchAllAssociative() as $row) { + $data[array_shift($row)] = $row; + } + + return $data; + } + + /** + * @return list + * + * @throws Exception + */ + public function fetchFirstColumn(): array + { + try { + return $this->result->fetchFirstColumn(); + } catch (DriverException $e) { + throw $this->connection->convertException($e); + } + } + + /** + * @return Traversable> + * + * @throws Exception + */ + public function iterateNumeric(): Traversable + { + while (($row = $this->fetchNumeric()) !== false) { + yield $row; + } + } + + /** + * @return Traversable> + * + * @throws Exception + */ + public function iterateAssociative(): Traversable + { + while (($row = $this->fetchAssociative()) !== false) { + yield $row; + } + } + + /** + * @return Traversable + * + * @throws Exception + */ + public function iterateKeyValue(): Traversable + { + $this->ensureHasKeyValue(); + + foreach ($this->iterateNumeric() as [$key, $value]) { + yield $key => $value; + } + } + + /** + * Returns an iterator over the result set with the keys mapped to the first column and the values being + * an associative array representing the rest of the columns and their values. + * + * @return Traversable> + * + * @throws Exception + */ + public function iterateAssociativeIndexed(): Traversable + { + foreach ($this->iterateAssociative() as $row) { + yield array_shift($row) => $row; + } + } + + /** + * @return Traversable + * + * @throws Exception + */ + public function iterateColumn(): Traversable + { + while (($value = $this->fetchOne()) !== false) { + yield $value; + } + } + + /** @throws Exception */ + public function rowCount(): int + { + try { + return $this->result->rowCount(); + } catch (DriverException $e) { + throw $this->connection->convertException($e); + } + } + + /** @throws Exception */ + public function columnCount(): int + { + try { + return $this->result->columnCount(); + } catch (DriverException $e) { + throw $this->connection->convertException($e); + } + } + + public function free(): void + { + $this->result->free(); + } + + /** @throws Exception */ + private function ensureHasKeyValue(): void + { + $columnCount = $this->columnCount(); + + if ($columnCount < 2) { + throw NoKeyValue::fromColumnCount($columnCount); + } + } + + /** + * BC layer for a wide-spread use-case of old DBAL APIs + * + * @deprecated Use {@see fetchNumeric()}, {@see fetchAssociative()} or {@see fetchOne()} instead. + * + * @phpstan-param FetchMode::* $mode + * + * @return mixed + * + * @throws Exception + */ + public function fetch(int $mode = FetchMode::ASSOCIATIVE) + { + Deprecation::trigger( + 'doctrine/dbal', + 'https://github.com/doctrine/dbal/pull/4007', + '%s is deprecated, please use fetchNumeric(), fetchAssociative() or fetchOne() instead.', + __METHOD__, + ); + + if (func_num_args() > 1) { + throw new LogicException('Only invocations with one argument are still supported by this legacy API.'); + } + + if ($mode === FetchMode::ASSOCIATIVE) { + return $this->fetchAssociative(); + } + + if ($mode === FetchMode::NUMERIC) { + return $this->fetchNumeric(); + } + + if ($mode === FetchMode::COLUMN) { + return $this->fetchOne(); + } + + throw new LogicException('Only fetch modes declared on Doctrine\DBAL\FetchMode are supported by legacy API.'); + } + + /** + * BC layer for a wide-spread use-case of old DBAL APIs + * + * @deprecated Use {@see fetchAllNumeric()}, {@see fetchAllAssociative()} or {@see fetchFirstColumn()} instead. + * + * @phpstan-param FetchMode::* $mode + * + * @return list + * + * @throws Exception + */ + public function fetchAll(int $mode = FetchMode::ASSOCIATIVE): array + { + Deprecation::trigger( + 'doctrine/dbal', + 'https://github.com/doctrine/dbal/pull/4007', + '%s is deprecated, please use fetchAllNumeric(), fetchAllAssociative() or fetchFirstColumn() instead.', + __METHOD__, + ); + + if (func_num_args() > 1) { + throw new LogicException('Only invocations with one argument are still supported by this legacy API.'); + } + + if ($mode === FetchMode::ASSOCIATIVE) { + return $this->fetchAllAssociative(); + } + + if ($mode === FetchMode::NUMERIC) { + return $this->fetchAllNumeric(); + } + + if ($mode === FetchMode::COLUMN) { + return $this->fetchFirstColumn(); + } + + throw new LogicException('Only fetch modes declared on Doctrine\DBAL\FetchMode are supported by legacy API.'); + } +} diff --git a/3rdparty/doctrine/dbal/src/SQL/Builder/CreateSchemaObjectsSQLBuilder.php b/3rdparty/doctrine/dbal/src/SQL/Builder/CreateSchemaObjectsSQLBuilder.php new file mode 100644 index 00000000..74579b3e --- /dev/null +++ b/3rdparty/doctrine/dbal/src/SQL/Builder/CreateSchemaObjectsSQLBuilder.php @@ -0,0 +1,85 @@ +platform = $platform; + } + + /** + * @return list + * + * @throws Exception + */ + public function buildSQL(Schema $schema): array + { + return array_merge( + $this->buildNamespaceStatements($schema->getNamespaces()), + $this->buildSequenceStatements($schema->getSequences()), + $this->buildTableStatements($schema->getTables()), + ); + } + + /** + * @param string[] $namespaces + * + * @return list + * + * @throws Exception + */ + private function buildNamespaceStatements(array $namespaces): array + { + $statements = []; + + if ($this->platform->supportsSchemas()) { + foreach ($namespaces as $namespace) { + $statements[] = $this->platform->getCreateSchemaSQL($namespace); + } + } + + return $statements; + } + + /** + * @param Table[] $tables + * + * @return list + * + * @throws Exception + */ + private function buildTableStatements(array $tables): array + { + return $this->platform->getCreateTablesSQL($tables); + } + + /** + * @param Sequence[] $sequences + * + * @return list + * + * @throws Exception + */ + private function buildSequenceStatements(array $sequences): array + { + $statements = []; + + foreach ($sequences as $sequence) { + $statements[] = $this->platform->getCreateSequenceSQL($sequence); + } + + return $statements; + } +} diff --git a/3rdparty/doctrine/dbal/src/SQL/Builder/DefaultSelectSQLBuilder.php b/3rdparty/doctrine/dbal/src/SQL/Builder/DefaultSelectSQLBuilder.php new file mode 100644 index 00000000..9dcb7a19 --- /dev/null +++ b/3rdparty/doctrine/dbal/src/SQL/Builder/DefaultSelectSQLBuilder.php @@ -0,0 +1,95 @@ +platform = $platform; + $this->forUpdateSQL = $forUpdateSQL; + $this->skipLockedSQL = $skipLockedSQL; + } + + /** @throws Exception */ + public function buildSQL(SelectQuery $query): string + { + $parts = ['SELECT']; + + if ($query->isDistinct()) { + $parts[] = 'DISTINCT'; + } + + $parts[] = implode(', ', $query->getColumns()); + + $from = $query->getFrom(); + + if (count($from) > 0) { + $parts[] = 'FROM ' . implode(', ', $from); + } + + $where = $query->getWhere(); + + if ($where !== null) { + $parts[] = 'WHERE ' . $where; + } + + $groupBy = $query->getGroupBy(); + + if (count($groupBy) > 0) { + $parts[] = 'GROUP BY ' . implode(', ', $groupBy); + } + + $having = $query->getHaving(); + + if ($having !== null) { + $parts[] = 'HAVING ' . $having; + } + + $orderBy = $query->getOrderBy(); + + if (count($orderBy) > 0) { + $parts[] = 'ORDER BY ' . implode(', ', $orderBy); + } + + $sql = implode(' ', $parts); + $limit = $query->getLimit(); + + if ($limit->isDefined()) { + $sql = $this->platform->modifyLimitQuery($sql, $limit->getMaxResults(), $limit->getFirstResult()); + } + + $forUpdate = $query->getForUpdate(); + + if ($forUpdate !== null) { + if ($this->forUpdateSQL === null) { + throw Exception::notSupported('FOR UPDATE'); + } + + $sql .= ' ' . $this->forUpdateSQL; + + if ($forUpdate->getConflictResolutionMode() === ConflictResolutionMode::SKIP_LOCKED) { + if ($this->skipLockedSQL === null) { + throw Exception::notSupported('SKIP LOCKED'); + } + + $sql .= ' ' . $this->skipLockedSQL; + } + } + + return $sql; + } +} diff --git a/3rdparty/doctrine/dbal/src/SQL/Builder/DropSchemaObjectsSQLBuilder.php b/3rdparty/doctrine/dbal/src/SQL/Builder/DropSchemaObjectsSQLBuilder.php new file mode 100644 index 00000000..8de742a3 --- /dev/null +++ b/3rdparty/doctrine/dbal/src/SQL/Builder/DropSchemaObjectsSQLBuilder.php @@ -0,0 +1,62 @@ +platform = $platform; + } + + /** + * @return list + * + * @throws Exception + */ + public function buildSQL(Schema $schema): array + { + return array_merge( + $this->buildSequenceStatements($schema->getSequences()), + $this->buildTableStatements($schema->getTables()), + ); + } + + /** + * @param list
$tables + * + * @return list + */ + private function buildTableStatements(array $tables): array + { + return $this->platform->getDropTablesSQL($tables); + } + + /** + * @param list $sequences + * + * @return list + * + * @throws Exception + */ + private function buildSequenceStatements(array $sequences): array + { + $statements = []; + + foreach ($sequences as $sequence) { + $statements[] = $this->platform->getDropSequenceSQL($sequence); + } + + return $statements; + } +} diff --git a/3rdparty/doctrine/dbal/src/SQL/Builder/SelectSQLBuilder.php b/3rdparty/doctrine/dbal/src/SQL/Builder/SelectSQLBuilder.php new file mode 100644 index 00000000..ddbf73c0 --- /dev/null +++ b/3rdparty/doctrine/dbal/src/SQL/Builder/SelectSQLBuilder.php @@ -0,0 +1,12 @@ +getMySQLStringLiteralPattern("'"), + $this->getMySQLStringLiteralPattern('"'), + ]; + } else { + $patterns = [ + $this->getAnsiSQLStringLiteralPattern("'"), + $this->getAnsiSQLStringLiteralPattern('"'), + ]; + } + + $patterns = array_merge($patterns, [ + self::BACKTICK_IDENTIFIER, + self::BRACKET_IDENTIFIER, + self::MULTICHAR, + self::ONE_LINE_COMMENT, + self::MULTI_LINE_COMMENT, + self::OTHER, + ]); + + $this->sqlPattern = sprintf('(%s)', implode('|', $patterns)); + $this->tokenPattern = '~\\G' + . '(?P' . self::NAMED_PARAMETER . ')' + . '|(?P' . self::POSITIONAL_PARAMETER . ')' + . '|(?P' . $this->sqlPattern . '|' . self::SPECIAL . ')' + . '~s'; + } + + /** + * Parses the given SQL statement + * + * @throws Exception + */ + public function parse(string $sql, Visitor $visitor): void + { + $offset = 0; + $length = strlen($sql); + while ($offset < $length) { + if (preg_match($this->tokenPattern, $sql, $matches, 0, $offset) === 1) { + $match = $matches[0]; + if ($matches['named'] !== '') { + $visitor->acceptNamedParameter($match); + } elseif ($matches['positional'] !== '') { + $visitor->acceptPositionalParameter($match); + } else { + $visitor->acceptOther($match); + } + + $offset += strlen($match); + } elseif (preg_last_error() !== PREG_NO_ERROR) { + // @codeCoverageIgnoreStart + throw RegularExpressionError::new(); + // @codeCoverageIgnoreEnd + } + } + } + + private function getMySQLStringLiteralPattern(string $delimiter): string + { + return $delimiter . '((\\\\.)|(?![' . $delimiter . '\\\\]).)*' . $delimiter; + } + + private function getAnsiSQLStringLiteralPattern(string $delimiter): string + { + return $delimiter . '[^' . $delimiter . ']*' . $delimiter; + } +} diff --git a/3rdparty/doctrine/dbal/src/SQL/Parser/Exception.php b/3rdparty/doctrine/dbal/src/SQL/Parser/Exception.php new file mode 100644 index 00000000..0c14b358 --- /dev/null +++ b/3rdparty/doctrine/dbal/src/SQL/Parser/Exception.php @@ -0,0 +1,11 @@ + Table($tableName)); if you want to rename the table, you have to make sure this does not get + * recreated during schema migration. + */ +abstract class AbstractAsset +{ + /** @var string */ + protected $_name = ''; + + /** + * Namespace of the asset. If none isset the default namespace is assumed. + * + * @var string|null + */ + protected $_namespace; + + /** @var bool */ + protected $_quoted = false; + + /** + * Sets the name of this asset. + * + * @param string $name + * + * @return void + */ + protected function _setName($name) + { + if ($this->isIdentifierQuoted($name)) { + $this->_quoted = true; + $name = $this->trimQuotes($name); + } + + if (strpos($name, '.') !== false) { + $parts = explode('.', $name); + $this->_namespace = $parts[0]; + $name = $parts[1]; + } + + $this->_name = $name; + } + + /** + * Is this asset in the default namespace? + * + * @param string $defaultNamespaceName + * + * @return bool + */ + public function isInDefaultNamespace($defaultNamespaceName) + { + return $this->_namespace === $defaultNamespaceName || $this->_namespace === null; + } + + /** + * Gets the namespace name of this asset. + * + * If NULL is returned this means the default namespace is used. + * + * @return string|null + */ + public function getNamespaceName() + { + return $this->_namespace; + } + + /** + * The shortest name is stripped of the default namespace. All other + * namespaced elements are returned as full-qualified names. + * + * @param string|null $defaultNamespaceName + * + * @return string + */ + public function getShortestName($defaultNamespaceName) + { + $shortestName = $this->getName(); + if ($this->_namespace === $defaultNamespaceName) { + $shortestName = $this->_name; + } + + return strtolower($shortestName); + } + + /** + * The normalized name is full-qualified and lower-cased. Lower-casing is + * actually wrong, but we have to do it to keep our sanity. If you are + * using database objects that only differentiate in the casing (FOO vs + * Foo) then you will NOT be able to use Doctrine Schema abstraction. + * + * Every non-namespaced element is prefixed with the default namespace + * name which is passed as argument to this method. + * + * @deprecated Use {@see getNamespaceName()} and {@see getName()} instead. + * + * @param string $defaultNamespaceName + * + * @return string + */ + public function getFullQualifiedName($defaultNamespaceName) + { + Deprecation::triggerIfCalledFromOutside( + 'doctrine/dbal', + 'https://github.com/doctrine/dbal/pull/4814', + 'AbstractAsset::getFullQualifiedName() is deprecated.' + . ' Use AbstractAsset::getNamespaceName() and ::getName() instead.', + ); + + $name = $this->getName(); + if ($this->_namespace === null) { + $name = $defaultNamespaceName . '.' . $name; + } + + return strtolower($name); + } + + /** + * Checks if this asset's name is quoted. + * + * @return bool + */ + public function isQuoted() + { + return $this->_quoted; + } + + /** + * Checks if this identifier is quoted. + * + * @param string $identifier + * + * @return bool + */ + protected function isIdentifierQuoted($identifier) + { + return isset($identifier[0]) && ($identifier[0] === '`' || $identifier[0] === '"' || $identifier[0] === '['); + } + + /** + * Trim quotes from the identifier. + * + * @param string $identifier + * + * @return string + */ + protected function trimQuotes($identifier) + { + return str_replace(['`', '"', '[', ']'], '', $identifier); + } + + /** + * Returns the name of this schema asset. + * + * @return string + */ + public function getName() + { + if ($this->_namespace !== null) { + return $this->_namespace . '.' . $this->_name; + } + + return $this->_name; + } + + /** + * Gets the quoted representation of this asset but only if it was defined with one. Otherwise + * return the plain unquoted value as inserted. + * + * @return string + */ + public function getQuotedName(AbstractPlatform $platform) + { + $keywords = $platform->getReservedKeywordsList(); + $parts = explode('.', $this->getName()); + foreach ($parts as $k => $v) { + $parts[$k] = $this->_quoted || $keywords->isKeyword($v) ? $platform->quoteIdentifier($v) : $v; + } + + return implode('.', $parts); + } + + /** + * Generates an identifier from a list of column names obeying a certain string length. + * + * This is especially important for Oracle, since it does not allow identifiers larger than 30 chars, + * however building idents automatically for foreign keys, composite keys or such can easily create + * very long names. + * + * @param string[] $columnNames + * @param string $prefix + * @param int $maxSize + * + * @return string + */ + protected function _generateIdentifierName($columnNames, $prefix = '', $maxSize = 30) + { + $hash = implode('', array_map(static function ($column): string { + return dechex(crc32($column)); + }, $columnNames)); + + return strtoupper(substr($prefix . '_' . $hash, 0, $maxSize)); + } +} diff --git a/3rdparty/doctrine/dbal/src/Schema/AbstractSchemaManager.php b/3rdparty/doctrine/dbal/src/Schema/AbstractSchemaManager.php new file mode 100644 index 00000000..2e38bb88 --- /dev/null +++ b/3rdparty/doctrine/dbal/src/Schema/AbstractSchemaManager.php @@ -0,0 +1,1808 @@ +_conn = $connection; + $this->_platform = $platform; + } + + /** + * Returns the associated platform. + * + * @deprecated Use {@link Connection::getDatabasePlatform()} instead. + * + * @return T + */ + public function getDatabasePlatform() + { + Deprecation::trigger( + 'doctrine/dbal', + 'https://github.com/doctrine/dbal/pull/5387', + 'AbstractSchemaManager::getDatabasePlatform() is deprecated.' + . ' Use Connection::getDatabasePlatform() instead.', + ); + + return $this->_platform; + } + + /** + * Tries any method on the schema manager. Normally a method throws an + * exception when your DBMS doesn't support it or if an error occurs. + * This method allows you to try and method on your SchemaManager + * instance and will return false if it does not work or is not supported. + * + * + * $result = $sm->tryMethod('dropView', 'view_name'); + * + * + * @deprecated + * + * @return mixed + */ + public function tryMethod() + { + Deprecation::triggerIfCalledFromOutside( + 'doctrine/dbal', + 'https://github.com/doctrine/dbal/pull/4897', + 'AbstractSchemaManager::tryMethod() is deprecated.', + ); + + $args = func_get_args(); + $method = $args[0]; + unset($args[0]); + $args = array_values($args); + + $callback = [$this, $method]; + assert(is_callable($callback)); + + try { + return call_user_func_array($callback, $args); + } catch (Throwable $e) { + return false; + } + } + + /** + * Lists the available databases for this connection. + * + * @return string[] + * + * @throws Exception + */ + public function listDatabases() + { + $sql = $this->_platform->getListDatabasesSQL(); + + $databases = $this->_conn->fetchAllAssociative($sql); + + return $this->_getPortableDatabasesList($databases); + } + + /** + * Returns a list of all namespaces in the current database. + * + * @deprecated Use {@see listSchemaNames()} instead. + * + * @return string[] + * + * @throws Exception + */ + public function listNamespaceNames() + { + Deprecation::triggerIfCalledFromOutside( + 'doctrine/dbal', + 'https://github.com/doctrine/dbal/issues/4503', + 'AbstractSchemaManager::listNamespaceNames() is deprecated,' + . ' use AbstractSchemaManager::listSchemaNames() instead.', + ); + + $sql = $this->_platform->getListNamespacesSQL(); + + $namespaces = $this->_conn->fetchAllAssociative($sql); + + return $this->getPortableNamespacesList($namespaces); + } + + /** + * Returns a list of the names of all schemata in the current database. + * + * @return list + * + * @throws Exception + */ + public function listSchemaNames(): array + { + throw Exception::notSupported(__METHOD__); + } + + /** + * Lists the available sequences for this connection. + * + * @param string|null $database + * + * @return Sequence[] + * + * @throws Exception + */ + public function listSequences($database = null) + { + if ($database === null) { + $database = $this->getDatabase(__METHOD__); + } else { + Deprecation::trigger( + 'doctrine/dbal', + 'https://github.com/doctrine/dbal/issues/5284', + 'Passing $database to AbstractSchemaManager::listSequences() is deprecated.', + ); + } + + $sql = $this->_platform->getListSequencesSQL($database); + + $sequences = $this->_conn->fetchAllAssociative($sql); + + return $this->filterAssetNames($this->_getPortableSequencesList($sequences)); + } + + /** + * Lists the columns for a given table. + * + * In contrast to other libraries and to the old version of Doctrine, + * this column definition does try to contain the 'primary' column for + * the reason that it is not portable across different RDBMS. Use + * {@see listTableIndexes($tableName)} to retrieve the primary key + * of a table. Where a RDBMS specifies more details, these are held + * in the platformDetails array. + * + * @param string $table The name of the table. + * @param string|null $database + * + * @return Column[] + * + * @throws Exception + */ + public function listTableColumns($table, $database = null) + { + if ($database === null) { + $database = $this->getDatabase(__METHOD__); + } else { + Deprecation::triggerIfCalledFromOutside( + 'doctrine/dbal', + 'https://github.com/doctrine/dbal/issues/5284', + 'Passing $database to AbstractSchemaManager::listTableColumns() is deprecated.', + ); + } + + $sql = $this->_platform->getListTableColumnsSQL($table, $database); + + $tableColumns = $this->_conn->fetchAllAssociative($sql); + + return $this->_getPortableTableColumnList($table, $database, $tableColumns); + } + + /** + * @param string $table + * @param string|null $database + * + * @return Column[] + * + * @throws Exception + */ + protected function doListTableColumns($table, $database = null): array + { + if ($database === null) { + $database = $this->getDatabase(__METHOD__); + } else { + Deprecation::triggerIfCalledFromOutside( + 'doctrine/dbal', + 'https://github.com/doctrine/dbal/issues/5284', + 'Passing $database to AbstractSchemaManager::doListTableColumns() is deprecated.', + ); + } + + return $this->_getPortableTableColumnList( + $table, + $database, + $this->selectTableColumns($database, $this->normalizeName($table)) + ->fetchAllAssociative(), + ); + } + + /** + * Lists the indexes for a given table returning an array of Index instances. + * + * Keys of the portable indexes list are all lower-cased. + * + * @param string $table The name of the table. + * + * @return Index[] + * + * @throws Exception + */ + public function listTableIndexes($table) + { + $sql = $this->_platform->getListTableIndexesSQL($table, $this->_conn->getDatabase()); + + $tableIndexes = $this->_conn->fetchAllAssociative($sql); + + return $this->_getPortableTableIndexesList($tableIndexes, $table); + } + + /** + * @param string $table + * + * @return Index[] + * + * @throws Exception + */ + protected function doListTableIndexes($table): array + { + $database = $this->getDatabase(__METHOD__); + $table = $this->normalizeName($table); + + return $this->_getPortableTableIndexesList( + $this->selectIndexColumns( + $database, + $table, + )->fetchAllAssociative(), + $table, + ); + } + + /** + * Returns true if all the given tables exist. + * + * The usage of a string $tableNames is deprecated. Pass a one-element array instead. + * + * @param string|string[] $names + * + * @return bool + * + * @throws Exception + */ + public function tablesExist($names) + { + if (is_string($names)) { + Deprecation::trigger( + 'doctrine/dbal', + 'https://github.com/doctrine/dbal/issues/3580', + 'The usage of a string $tableNames in AbstractSchemaManager::tablesExist() is deprecated. ' . + 'Pass a one-element array instead.', + ); + } + + $names = array_map('strtolower', (array) $names); + + return count($names) === count(array_intersect($names, array_map('strtolower', $this->listTableNames()))); + } + + /** + * Returns a list of all tables in the current database. + * + * @return string[] + * + * @throws Exception + */ + public function listTableNames() + { + $sql = $this->_platform->getListTablesSQL(); + + $tables = $this->_conn->fetchAllAssociative($sql); + $tableNames = $this->_getPortableTablesList($tables); + + return $this->filterAssetNames($tableNames); + } + + /** + * @return list + * + * @throws Exception + */ + protected function doListTableNames(): array + { + $database = $this->getDatabase(__METHOD__); + + return $this->filterAssetNames( + $this->_getPortableTablesList( + $this->selectTableNames($database) + ->fetchAllAssociative(), + ), + ); + } + + /** + * Filters asset names if they are configured to return only a subset of all + * the found elements. + * + * @param mixed[] $assetNames + * + * @return mixed[] + */ + protected function filterAssetNames($assetNames) + { + $filter = $this->_conn->getConfiguration()->getSchemaAssetsFilter(); + if ($filter === null) { + return $assetNames; + } + + return array_values(array_filter($assetNames, $filter)); + } + + /** + * Lists the tables for this connection. + * + * @return list
+ * + * @throws Exception + */ + public function listTables() + { + $tableNames = $this->listTableNames(); + + $tables = []; + foreach ($tableNames as $tableName) { + $tables[] = $this->introspectTable($tableName); + } + + return $tables; + } + + /** + * @return list
+ * + * @throws Exception + */ + protected function doListTables(): array + { + $database = $this->getDatabase(__METHOD__); + + $tableColumnsByTable = $this->fetchTableColumnsByTable($database); + $indexColumnsByTable = $this->fetchIndexColumnsByTable($database); + $foreignKeyColumnsByTable = $this->fetchForeignKeyColumnsByTable($database); + $tableOptionsByTable = $this->fetchTableOptionsByTable($database); + + $filter = $this->_conn->getConfiguration()->getSchemaAssetsFilter(); + $tables = []; + + foreach ($tableColumnsByTable as $tableName => $tableColumns) { + if ($filter !== null && ! $filter($tableName)) { + continue; + } + + $tables[] = new Table( + $tableName, + $this->_getPortableTableColumnList($tableName, $database, $tableColumns), + $this->_getPortableTableIndexesList($indexColumnsByTable[$tableName] ?? [], $tableName), + [], + $this->_getPortableTableForeignKeysList($foreignKeyColumnsByTable[$tableName] ?? []), + $tableOptionsByTable[$tableName] ?? [], + ); + } + + return $tables; + } + + /** + * @deprecated Use {@see introspectTable()} instead. + * + * @param string $name + * + * @return Table + * + * @throws Exception + */ + public function listTableDetails($name) + { + Deprecation::triggerIfCalledFromOutside( + 'doctrine/dbal', + 'https://github.com/doctrine/dbal/pull/5595', + '%s is deprecated. Use introspectTable() instead.', + __METHOD__, + ); + + $columns = $this->listTableColumns($name); + $foreignKeys = []; + + if ($this->_platform->supportsForeignKeyConstraints()) { + $foreignKeys = $this->listTableForeignKeys($name); + } + + $indexes = $this->listTableIndexes($name); + + return new Table($name, $columns, $indexes, [], $foreignKeys); + } + + /** + * @param string $name + * + * @throws Exception + */ + protected function doListTableDetails($name): Table + { + $database = $this->getDatabase(__METHOD__); + + $normalizedName = $this->normalizeName($name); + + $tableOptionsByTable = $this->fetchTableOptionsByTable($database, $normalizedName); + + if ($this->_platform->supportsForeignKeyConstraints()) { + $foreignKeys = $this->listTableForeignKeys($name); + } else { + $foreignKeys = []; + } + + return new Table( + $name, + $this->listTableColumns($name, $database), + $this->listTableIndexes($name), + [], + $foreignKeys, + $tableOptionsByTable[$normalizedName] ?? [], + ); + } + + /** + * An extension point for those platforms where case sensitivity of the object name depends on whether it's quoted. + * + * Such platforms should convert a possibly quoted name into a value of the corresponding case. + */ + protected function normalizeName(string $name): string + { + $identifier = new Identifier($name); + + return $identifier->getName(); + } + + /** + * Selects names of tables in the specified database. + * + * @throws Exception + * + * @abstract + */ + protected function selectTableNames(string $databaseName): Result + { + throw Exception::notSupported(__METHOD__); + } + + /** + * Selects definitions of table columns in the specified database. If the table name is specified, narrows down + * the selection to this table. + * + * @throws Exception + * + * @abstract + */ + protected function selectTableColumns(string $databaseName, ?string $tableName = null): Result + { + throw Exception::notSupported(__METHOD__); + } + + /** + * Selects definitions of index columns in the specified database. If the table name is specified, narrows down + * the selection to this table. + * + * @throws Exception + */ + protected function selectIndexColumns(string $databaseName, ?string $tableName = null): Result + { + throw Exception::notSupported(__METHOD__); + } + + /** + * Selects definitions of foreign key columns in the specified database. If the table name is specified, + * narrows down the selection to this table. + * + * @throws Exception + */ + protected function selectForeignKeyColumns(string $databaseName, ?string $tableName = null): Result + { + throw Exception::notSupported(__METHOD__); + } + + /** + * Fetches definitions of table columns in the specified database and returns them grouped by table name. + * + * @return array>> + * + * @throws Exception + */ + protected function fetchTableColumnsByTable(string $databaseName): array + { + return $this->fetchAllAssociativeGrouped($this->selectTableColumns($databaseName)); + } + + /** + * Fetches definitions of index columns in the specified database and returns them grouped by table name. + * + * @return array>> + * + * @throws Exception + */ + protected function fetchIndexColumnsByTable(string $databaseName): array + { + return $this->fetchAllAssociativeGrouped($this->selectIndexColumns($databaseName)); + } + + /** + * Fetches definitions of foreign key columns in the specified database and returns them grouped by table name. + * + * @return array>> + * + * @throws Exception + */ + protected function fetchForeignKeyColumnsByTable(string $databaseName): array + { + if (! $this->_platform->supportsForeignKeyConstraints()) { + return []; + } + + return $this->fetchAllAssociativeGrouped( + $this->selectForeignKeyColumns($databaseName), + ); + } + + /** + * Fetches table options for the tables in the specified database and returns them grouped by table name. + * If the table name is specified, narrows down the selection to this table. + * + * @return array> + * + * @throws Exception + */ + protected function fetchTableOptionsByTable(string $databaseName, ?string $tableName = null): array + { + throw Exception::notSupported(__METHOD__); + } + + /** + * Introspects the table with the given name. + * + * @throws Exception + */ + public function introspectTable(string $name): Table + { + $table = $this->listTableDetails($name); + + if ($table->getColumns() === []) { + throw SchemaException::tableDoesNotExist($name); + } + + return $table; + } + + /** + * Lists the views this connection has. + * + * @return View[] + * + * @throws Exception + */ + public function listViews() + { + $database = $this->_conn->getDatabase(); + $sql = $this->_platform->getListViewsSQL($database); + $views = $this->_conn->fetchAllAssociative($sql); + + return $this->_getPortableViewsList($views); + } + + /** + * Lists the foreign keys for the given table. + * + * @param string $table The name of the table. + * @param string|null $database + * + * @return ForeignKeyConstraint[] + * + * @throws Exception + */ + public function listTableForeignKeys($table, $database = null) + { + if ($database === null) { + $database = $this->getDatabase(__METHOD__); + } else { + Deprecation::trigger( + 'doctrine/dbal', + 'https://github.com/doctrine/dbal/issues/5284', + 'Passing $database to AbstractSchemaManager::listTableForeignKeys() is deprecated.', + ); + } + + $sql = $this->_platform->getListTableForeignKeysSQL($table, $database); + $tableForeignKeys = $this->_conn->fetchAllAssociative($sql); + + return $this->_getPortableTableForeignKeysList($tableForeignKeys); + } + + /** + * @param string $table + * @param string|null $database + * + * @return ForeignKeyConstraint[] + * + * @throws Exception + */ + protected function doListTableForeignKeys($table, $database = null): array + { + if ($database === null) { + $database = $this->getDatabase(__METHOD__); + } else { + Deprecation::trigger( + 'doctrine/dbal', + 'https://github.com/doctrine/dbal/issues/5284', + 'Passing $database to AbstractSchemaManager::listTableForeignKeys() is deprecated.', + ); + } + + return $this->_getPortableTableForeignKeysList( + $this->selectForeignKeyColumns( + $database, + $this->normalizeName($table), + )->fetchAllAssociative(), + ); + } + + /* drop*() Methods */ + + /** + * Drops a database. + * + * NOTE: You can not drop the database this SchemaManager is currently connected to. + * + * @param string $database The name of the database to drop. + * + * @return void + * + * @throws Exception + */ + public function dropDatabase($database) + { + $this->_conn->executeStatement( + $this->_platform->getDropDatabaseSQL($database), + ); + } + + /** + * Drops a schema. + * + * @throws Exception + */ + public function dropSchema(string $schemaName): void + { + $this->_conn->executeStatement( + $this->_platform->getDropSchemaSQL($schemaName), + ); + } + + /** + * Drops the given table. + * + * @param string $name The name of the table to drop. + * + * @return void + * + * @throws Exception + */ + public function dropTable($name) + { + $this->_conn->executeStatement( + $this->_platform->getDropTableSQL($name), + ); + } + + /** + * Drops the index from the given table. + * + * @param Index|string $index The name of the index. + * @param Table|string $table The name of the table. + * + * @return void + * + * @throws Exception + */ + public function dropIndex($index, $table) + { + if ($index instanceof Index) { + Deprecation::trigger( + 'doctrine/dbal', + 'https://github.com/doctrine/dbal/issues/4798', + 'Passing $index as an Index object to %s is deprecated. Pass it as a quoted name instead.', + __METHOD__, + ); + + $index = $index->getQuotedName($this->_platform); + } + + if ($table instanceof Table) { + Deprecation::trigger( + 'doctrine/dbal', + 'https://github.com/doctrine/dbal/issues/4798', + 'Passing $table as an Table object to %s is deprecated. Pass it as a quoted name instead.', + __METHOD__, + ); + + $table = $table->getQuotedName($this->_platform); + } + + $this->_conn->executeStatement( + $this->_platform->getDropIndexSQL($index, $table), + ); + } + + /** + * Drops the constraint from the given table. + * + * @deprecated Use {@see dropIndex()}, {@see dropForeignKey()} or {@see dropUniqueConstraint()} instead. + * + * @param Table|string $table The name of the table. + * + * @return void + * + * @throws Exception + */ + public function dropConstraint(Constraint $constraint, $table) + { + if ($table instanceof Table) { + Deprecation::trigger( + 'doctrine/dbal', + 'https://github.com/doctrine/dbal/issues/4798', + 'Passing $table as a Table object to %s is deprecated. Pass it as a quoted name instead.', + __METHOD__, + ); + + $table = $table->getQuotedName($this->_platform); + } + + $this->_conn->executeStatement($this->_platform->getDropConstraintSQL( + $constraint->getQuotedName($this->_platform), + $table, + )); + } + + /** + * Drops a foreign key from a table. + * + * @param ForeignKeyConstraint|string $foreignKey The name of the foreign key. + * @param Table|string $table The name of the table with the foreign key. + * + * @return void + * + * @throws Exception + */ + public function dropForeignKey($foreignKey, $table) + { + if ($foreignKey instanceof ForeignKeyConstraint) { + Deprecation::trigger( + 'doctrine/dbal', + 'https://github.com/doctrine/dbal/issues/4798', + 'Passing $foreignKey as a ForeignKeyConstraint object to %s is deprecated.' + . ' Pass it as a quoted name instead.', + __METHOD__, + ); + + $foreignKey = $foreignKey->getQuotedName($this->_platform); + } + + if ($table instanceof Table) { + Deprecation::trigger( + 'doctrine/dbal', + 'https://github.com/doctrine/dbal/issues/4798', + 'Passing $table as a Table object to %s is deprecated. Pass it as a quoted name instead.', + __METHOD__, + ); + + $table = $table->getQuotedName($this->_platform); + } + + $this->_conn->executeStatement( + $this->_platform->getDropForeignKeySQL($foreignKey, $table), + ); + } + + /** + * Drops a sequence with a given name. + * + * @param string $name The name of the sequence to drop. + * + * @return void + * + * @throws Exception + */ + public function dropSequence($name) + { + $this->_conn->executeStatement( + $this->_platform->getDropSequenceSQL($name), + ); + } + + /** + * Drops the unique constraint from the given table. + * + * @throws Exception + */ + public function dropUniqueConstraint(string $name, string $tableName): void + { + $this->_conn->executeStatement( + $this->_platform->getDropUniqueConstraintSQL($name, $tableName), + ); + } + + /** + * Drops a view. + * + * @param string $name The name of the view. + * + * @return void + * + * @throws Exception + */ + public function dropView($name) + { + $this->_conn->executeStatement( + $this->_platform->getDropViewSQL($name), + ); + } + + /* create*() Methods */ + + /** @throws Exception */ + public function createSchemaObjects(Schema $schema): void + { + $this->_execSql($schema->toSql($this->_platform)); + } + + /** + * Creates a new database. + * + * @param string $database The name of the database to create. + * + * @return void + * + * @throws Exception + */ + public function createDatabase($database) + { + $this->_conn->executeStatement( + $this->_platform->getCreateDatabaseSQL($database), + ); + } + + /** + * Creates a new table. + * + * @return void + * + * @throws Exception + */ + public function createTable(Table $table) + { + $createFlags = AbstractPlatform::CREATE_INDEXES | AbstractPlatform::CREATE_FOREIGNKEYS; + $this->_execSql($this->_platform->getCreateTableSQL($table, $createFlags)); + } + + /** + * Creates a new sequence. + * + * @param Sequence $sequence + * + * @return void + * + * @throws Exception + */ + public function createSequence($sequence) + { + $this->_conn->executeStatement( + $this->_platform->getCreateSequenceSQL($sequence), + ); + } + + /** + * Creates a constraint on a table. + * + * @deprecated Use {@see createIndex()}, {@see createForeignKey()} or {@see createUniqueConstraint()} instead. + * + * @param Table|string $table + * + * @return void + * + * @throws Exception + */ + public function createConstraint(Constraint $constraint, $table) + { + $this->_conn->executeStatement( + $this->_platform->getCreateConstraintSQL($constraint, $table), + ); + } + + /** + * Creates a new index on a table. + * + * @param Table|string $table The name of the table on which the index is to be created. + * + * @return void + * + * @throws Exception + */ + public function createIndex(Index $index, $table) + { + $this->_conn->executeStatement( + $this->_platform->getCreateIndexSQL($index, $table), + ); + } + + /** + * Creates a new foreign key. + * + * @param ForeignKeyConstraint $foreignKey The ForeignKey instance. + * @param Table|string $table The name of the table on which the foreign key is to be created. + * + * @return void + * + * @throws Exception + */ + public function createForeignKey(ForeignKeyConstraint $foreignKey, $table) + { + $this->_conn->executeStatement( + $this->_platform->getCreateForeignKeySQL($foreignKey, $table), + ); + } + + /** + * Creates a unique constraint on a table. + * + * @throws Exception + */ + public function createUniqueConstraint(UniqueConstraint $uniqueConstraint, string $tableName): void + { + $this->_conn->executeStatement( + $this->_platform->getCreateUniqueConstraintSQL($uniqueConstraint, $tableName), + ); + } + + /** + * Creates a new view. + * + * @return void + * + * @throws Exception + */ + public function createView(View $view) + { + $this->_conn->executeStatement( + $this->_platform->getCreateViewSQL( + $view->getQuotedName($this->_platform), + $view->getSql(), + ), + ); + } + + /* dropAndCreate*() Methods */ + + /** @throws Exception */ + public function dropSchemaObjects(Schema $schema): void + { + $this->_execSql($schema->toDropSql($this->_platform)); + } + + /** + * Drops and creates a constraint. + * + * @deprecated Use {@see dropIndex()} and {@see createIndex()}, + * {@see dropForeignKey()} and {@see createForeignKey()} + * or {@see dropUniqueConstraint()} and {@see createUniqueConstraint()} instead. + * + * @see dropConstraint() + * @see createConstraint() + * + * @param Table|string $table + * + * @return void + * + * @throws Exception + */ + public function dropAndCreateConstraint(Constraint $constraint, $table) + { + Deprecation::trigger( + 'doctrine/dbal', + 'https://github.com/doctrine/dbal/pull/4897', + 'AbstractSchemaManager::dropAndCreateConstraint() is deprecated.' + . ' Use AbstractSchemaManager::dropIndex() and AbstractSchemaManager::createIndex(),' + . ' AbstractSchemaManager::dropForeignKey() and AbstractSchemaManager::createForeignKey()' + . ' or AbstractSchemaManager::dropUniqueConstraint()' + . ' and AbstractSchemaManager::createUniqueConstraint() instead.', + ); + + $this->tryMethod('dropConstraint', $constraint, $table); + $this->createConstraint($constraint, $table); + } + + /** + * Drops and creates a new index on a table. + * + * @deprecated Use {@see dropIndex()} and {@see createIndex()} instead. + * + * @param Table|string $table The name of the table on which the index is to be created. + * + * @return void + * + * @throws Exception + */ + public function dropAndCreateIndex(Index $index, $table) + { + Deprecation::trigger( + 'doctrine/dbal', + 'https://github.com/doctrine/dbal/pull/4897', + 'AbstractSchemaManager::dropAndCreateIndex() is deprecated.' + . ' Use AbstractSchemaManager::dropIndex() and AbstractSchemaManager::createIndex() instead.', + ); + + $this->tryMethod('dropIndex', $index->getQuotedName($this->_platform), $table); + $this->createIndex($index, $table); + } + + /** + * Drops and creates a new foreign key. + * + * @deprecated Use {@see dropForeignKey()} and {@see createForeignKey()} instead. + * + * @param ForeignKeyConstraint $foreignKey An associative array that defines properties + * of the foreign key to be created. + * @param Table|string $table The name of the table on which the foreign key is to be created. + * + * @return void + * + * @throws Exception + */ + public function dropAndCreateForeignKey(ForeignKeyConstraint $foreignKey, $table) + { + Deprecation::trigger( + 'doctrine/dbal', + 'https://github.com/doctrine/dbal/pull/4897', + 'AbstractSchemaManager::dropAndCreateForeignKey() is deprecated.' + . ' Use AbstractSchemaManager::dropForeignKey() and AbstractSchemaManager::createForeignKey() instead.', + ); + + $this->tryMethod('dropForeignKey', $foreignKey, $table); + $this->createForeignKey($foreignKey, $table); + } + + /** + * Drops and create a new sequence. + * + * @deprecated Use {@see dropSequence()} and {@see createSequence()} instead. + * + * @return void + * + * @throws Exception + */ + public function dropAndCreateSequence(Sequence $sequence) + { + Deprecation::trigger( + 'doctrine/dbal', + 'https://github.com/doctrine/dbal/pull/4897', + 'AbstractSchemaManager::dropAndCreateSequence() is deprecated.' + . ' Use AbstractSchemaManager::dropSequence() and AbstractSchemaManager::createSequence() instead.', + ); + + $this->tryMethod('dropSequence', $sequence->getQuotedName($this->_platform)); + $this->createSequence($sequence); + } + + /** + * Drops and creates a new table. + * + * @deprecated Use {@see dropTable()} and {@see createTable()} instead. + * + * @return void + * + * @throws Exception + */ + public function dropAndCreateTable(Table $table) + { + Deprecation::trigger( + 'doctrine/dbal', + 'https://github.com/doctrine/dbal/pull/4897', + 'AbstractSchemaManager::dropAndCreateTable() is deprecated.' + . ' Use AbstractSchemaManager::dropTable() and AbstractSchemaManager::createTable() instead.', + ); + + $this->tryMethod('dropTable', $table->getQuotedName($this->_platform)); + $this->createTable($table); + } + + /** + * Drops and creates a new database. + * + * @deprecated Use {@see dropDatabase()} and {@see createDatabase()} instead. + * + * @param string $database The name of the database to create. + * + * @return void + * + * @throws Exception + */ + public function dropAndCreateDatabase($database) + { + Deprecation::trigger( + 'doctrine/dbal', + 'https://github.com/doctrine/dbal/pull/4897', + 'AbstractSchemaManager::dropAndCreateDatabase() is deprecated.' + . ' Use AbstractSchemaManager::dropDatabase() and AbstractSchemaManager::createDatabase() instead.', + ); + + $this->tryMethod('dropDatabase', $database); + $this->createDatabase($database); + } + + /** + * Drops and creates a new view. + * + * @deprecated Use {@see dropView()} and {@see createView()} instead. + * + * @return void + * + * @throws Exception + */ + public function dropAndCreateView(View $view) + { + Deprecation::trigger( + 'doctrine/dbal', + 'https://github.com/doctrine/dbal/pull/4897', + 'AbstractSchemaManager::dropAndCreateView() is deprecated.' + . ' Use AbstractSchemaManager::dropView() and AbstractSchemaManager::createView() instead.', + ); + + $this->tryMethod('dropView', $view->getQuotedName($this->_platform)); + $this->createView($view); + } + + /** + * Alters an existing schema. + * + * @throws Exception + */ + public function alterSchema(SchemaDiff $schemaDiff): void + { + $this->_execSql($this->_platform->getAlterSchemaSQL($schemaDiff)); + } + + /** + * Migrates an existing schema to a new schema. + * + * @throws Exception + */ + public function migrateSchema(Schema $toSchema): void + { + $schemaDiff = $this->createComparator() + ->compareSchemas($this->introspectSchema(), $toSchema); + + $this->alterSchema($schemaDiff); + } + + /* alterTable() Methods */ + + /** + * Alters an existing tables schema. + * + * @return void + * + * @throws Exception + */ + public function alterTable(TableDiff $tableDiff) + { + $this->_execSql($this->_platform->getAlterTableSQL($tableDiff)); + } + + /** + * Renames a given table to another name. + * + * @param string $name The current name of the table. + * @param string $newName The new name of the table. + * + * @return void + * + * @throws Exception + */ + public function renameTable($name, $newName) + { + $this->_execSql($this->_platform->getRenameTableSQL($name, $newName)); + } + + /** + * Methods for filtering return values of list*() methods to convert + * the native DBMS data definition to a portable Doctrine definition + */ + + /** + * @param mixed[] $databases + * + * @return string[] + */ + protected function _getPortableDatabasesList($databases) + { + $list = []; + foreach ($databases as $value) { + $list[] = $this->_getPortableDatabaseDefinition($value); + } + + return $list; + } + + /** + * Converts a list of namespace names from the native DBMS data definition to a portable Doctrine definition. + * + * @deprecated Use {@see listSchemaNames()} instead. + * + * @param array> $namespaces The list of namespace names + * in the native DBMS data definition. + * + * @return string[] + */ + protected function getPortableNamespacesList(array $namespaces) + { + Deprecation::triggerIfCalledFromOutside( + 'doctrine/dbal', + 'https://github.com/doctrine/dbal/issues/4503', + 'AbstractSchemaManager::getPortableNamespacesList() is deprecated,' + . ' use AbstractSchemaManager::listSchemaNames() instead.', + ); + + $namespacesList = []; + + foreach ($namespaces as $namespace) { + $namespacesList[] = $this->getPortableNamespaceDefinition($namespace); + } + + return $namespacesList; + } + + /** + * @param mixed $database + * + * @return mixed + */ + protected function _getPortableDatabaseDefinition($database) + { + return $database; + } + + /** + * Converts a namespace definition from the native DBMS data definition to a portable Doctrine definition. + * + * @deprecated Use {@see listSchemaNames()} instead. + * + * @param array $namespace The native DBMS namespace definition. + * + * @return mixed + */ + protected function getPortableNamespaceDefinition(array $namespace) + { + Deprecation::triggerIfCalledFromOutside( + 'doctrine/dbal', + 'https://github.com/doctrine/dbal/issues/4503', + 'AbstractSchemaManager::getPortableNamespaceDefinition() is deprecated,' + . ' use AbstractSchemaManager::listSchemaNames() instead.', + ); + + return $namespace; + } + + /** + * @param mixed[][] $sequences + * + * @return Sequence[] + * + * @throws Exception + */ + protected function _getPortableSequencesList($sequences) + { + $list = []; + + foreach ($sequences as $value) { + $list[] = $this->_getPortableSequenceDefinition($value); + } + + return $list; + } + + /** + * @param mixed[] $sequence + * + * @return Sequence + * + * @throws Exception + */ + protected function _getPortableSequenceDefinition($sequence) + { + throw Exception::notSupported('Sequences'); + } + + /** + * Independent of the database the keys of the column list result are lowercased. + * + * The name of the created column instance however is kept in its case. + * + * @param string $table The name of the table. + * @param string $database + * @param mixed[][] $tableColumns + * + * @return Column[] + * + * @throws Exception + */ + protected function _getPortableTableColumnList($table, $database, $tableColumns) + { + $eventManager = $this->_platform->getEventManager(); + + $list = []; + foreach ($tableColumns as $tableColumn) { + $column = null; + $defaultPrevented = false; + + if ($eventManager !== null && $eventManager->hasListeners(Events::onSchemaColumnDefinition)) { + Deprecation::trigger( + 'doctrine/dbal', + 'https://github.com/doctrine/dbal/issues/5784', + 'Subscribing to %s events is deprecated. Use a custom schema manager instead.', + Events::onSchemaColumnDefinition, + ); + + $eventArgs = new SchemaColumnDefinitionEventArgs($tableColumn, $table, $database, $this->_conn); + $eventManager->dispatchEvent(Events::onSchemaColumnDefinition, $eventArgs); + + $defaultPrevented = $eventArgs->isDefaultPrevented(); + $column = $eventArgs->getColumn(); + } + + if (! $defaultPrevented) { + $column = $this->_getPortableTableColumnDefinition($tableColumn); + } + + if ($column === null) { + continue; + } + + $name = strtolower($column->getQuotedName($this->_platform)); + $list[$name] = $column; + } + + return $list; + } + + /** + * Gets Table Column Definition. + * + * @param mixed[] $tableColumn + * + * @return Column + * + * @throws Exception + */ + abstract protected function _getPortableTableColumnDefinition($tableColumn); + + /** + * Aggregates and groups the index results according to the required data result. + * + * @param mixed[][] $tableIndexes + * @param string|null $tableName + * + * @return Index[] + * + * @throws Exception + */ + protected function _getPortableTableIndexesList($tableIndexes, $tableName = null) + { + $result = []; + foreach ($tableIndexes as $tableIndex) { + $indexName = $keyName = $tableIndex['key_name']; + if ($tableIndex['primary']) { + $keyName = 'primary'; + } + + $keyName = strtolower($keyName); + + if (! isset($result[$keyName])) { + $options = [ + 'lengths' => [], + ]; + + if (isset($tableIndex['where'])) { + $options['where'] = $tableIndex['where']; + } + + $result[$keyName] = [ + 'name' => $indexName, + 'columns' => [], + 'unique' => ! $tableIndex['non_unique'], + 'primary' => $tableIndex['primary'], + 'flags' => $tableIndex['flags'] ?? [], + 'options' => $options, + ]; + } + + $result[$keyName]['columns'][] = $tableIndex['column_name']; + $result[$keyName]['options']['lengths'][] = $tableIndex['length'] ?? null; + } + + $eventManager = $this->_platform->getEventManager(); + + $indexes = []; + foreach ($result as $indexKey => $data) { + $index = null; + $defaultPrevented = false; + + if ($eventManager !== null && $eventManager->hasListeners(Events::onSchemaIndexDefinition)) { + Deprecation::trigger( + 'doctrine/dbal', + 'https://github.com/doctrine/dbal/issues/5784', + 'Subscribing to %s events is deprecated. Use a custom schema manager instead.', + Events::onSchemaColumnDefinition, + ); + + $eventArgs = new SchemaIndexDefinitionEventArgs($data, $tableName, $this->_conn); + $eventManager->dispatchEvent(Events::onSchemaIndexDefinition, $eventArgs); + + $defaultPrevented = $eventArgs->isDefaultPrevented(); + $index = $eventArgs->getIndex(); + } + + if (! $defaultPrevented) { + $index = new Index( + $data['name'], + $data['columns'], + $data['unique'], + $data['primary'], + $data['flags'], + $data['options'], + ); + } + + if ($index === null) { + continue; + } + + $indexes[$indexKey] = $index; + } + + return $indexes; + } + + /** + * @param mixed[][] $tables + * + * @return string[] + */ + protected function _getPortableTablesList($tables) + { + $list = []; + foreach ($tables as $value) { + $list[] = $this->_getPortableTableDefinition($value); + } + + return $list; + } + + /** + * @param mixed $table + * + * @return string + */ + protected function _getPortableTableDefinition($table) + { + return $table; + } + + /** + * @param mixed[][] $views + * + * @return View[] + */ + protected function _getPortableViewsList($views) + { + $list = []; + foreach ($views as $value) { + $view = $this->_getPortableViewDefinition($value); + + if ($view === false) { + continue; + } + + $viewName = strtolower($view->getQuotedName($this->_platform)); + $list[$viewName] = $view; + } + + return $list; + } + + /** + * @param mixed[] $view + * + * @return View|false + */ + protected function _getPortableViewDefinition($view) + { + return false; + } + + /** + * @param mixed[][] $tableForeignKeys + * + * @return ForeignKeyConstraint[] + */ + protected function _getPortableTableForeignKeysList($tableForeignKeys) + { + $list = []; + + foreach ($tableForeignKeys as $value) { + $list[] = $this->_getPortableTableForeignKeyDefinition($value); + } + + return $list; + } + + /** + * @param mixed $tableForeignKey + * + * @return ForeignKeyConstraint + * + * @abstract + */ + protected function _getPortableTableForeignKeyDefinition($tableForeignKey) + { + return $tableForeignKey; + } + + /** + * @internal + * + * @param string[]|string $sql + * + * @return void + * + * @throws Exception + */ + protected function _execSql($sql) + { + foreach ((array) $sql as $query) { + $this->_conn->executeStatement($query); + } + } + + /** + * Creates a schema instance for the current database. + * + * @deprecated Use {@link introspectSchema()} instead. + * + * @return Schema + * + * @throws Exception + */ + public function createSchema() + { + Deprecation::triggerIfCalledFromOutside( + 'doctrine/dbal', + 'https://github.com/doctrine/dbal/pull/5613', + '%s is deprecated. Use introspectSchema() instead.', + __METHOD__, + ); + + $schemaNames = []; + + if ($this->_platform->supportsSchemas()) { + $schemaNames = $this->listNamespaceNames(); + } + + $sequences = []; + + if ($this->_platform->supportsSequences()) { + $sequences = $this->listSequences(); + } + + $tables = $this->listTables(); + + return new Schema($tables, $sequences, $this->createSchemaConfig(), $schemaNames); + } + + /** + * Returns a {@see Schema} instance representing the current database schema. + * + * @throws Exception + */ + public function introspectSchema(): Schema + { + return $this->createSchema(); + } + + /** + * Creates the configuration for this schema. + * + * @return SchemaConfig + * + * @throws Exception + */ + public function createSchemaConfig() + { + $schemaConfig = new SchemaConfig(); + $schemaConfig->setMaxIdentifierLength($this->_platform->getMaxIdentifierLength()); + + $searchPaths = $this->getSchemaSearchPaths(); + if (isset($searchPaths[0])) { + $schemaConfig->setName($searchPaths[0]); + } + + $params = $this->_conn->getParams(); + if (! isset($params['defaultTableOptions'])) { + $params['defaultTableOptions'] = []; + } + + if (! isset($params['defaultTableOptions']['charset']) && isset($params['charset'])) { + $params['defaultTableOptions']['charset'] = $params['charset']; + } + + $schemaConfig->setDefaultTableOptions($params['defaultTableOptions']); + + return $schemaConfig; + } + + /** + * The search path for namespaces in the currently connected database. + * + * The first entry is usually the default namespace in the Schema. All + * further namespaces contain tables/sequences which can also be addressed + * with a short, not full-qualified name. + * + * For databases that don't support subschema/namespaces this method + * returns the name of the currently connected database. + * + * @deprecated + * + * @return string[] + * + * @throws Exception + */ + public function getSchemaSearchPaths() + { + Deprecation::triggerIfCalledFromOutside( + 'doctrine/dbal', + 'https://github.com/doctrine/dbal/pull/4821', + 'AbstractSchemaManager::getSchemaSearchPaths() is deprecated.', + ); + + $database = $this->_conn->getDatabase(); + + if ($database !== null) { + return [$database]; + } + + return []; + } + + /** + * Given a table comment this method tries to extract a typehint for Doctrine Type, or returns + * the type given as default. + * + * @internal This method should be only used from within the AbstractSchemaManager class hierarchy. + * + * @param string|null $comment + * @param string $currentType + * + * @return string + */ + public function extractDoctrineTypeFromComment($comment, $currentType) + { + if ($this->_conn->getConfiguration()->getDisableTypeComments()) { + return $currentType; + } + + if ($comment !== null && preg_match('(\(DC2Type:(((?!\)).)+)\))', $comment, $match) === 1) { + return $match[1]; + } + + return $currentType; + } + + /** + * @internal This method should be only used from within the AbstractSchemaManager class hierarchy. + * + * @param string|null $comment + * @param string|null $type + * + * @return string|null + */ + public function removeDoctrineTypeFromComment($comment, $type) + { + if ($this->_conn->getConfiguration()->getDisableTypeComments()) { + return $comment; + } + + if ($comment === null) { + return null; + } + + return str_replace('(DC2Type:' . $type . ')', '', $comment); + } + + /** @throws Exception */ + private function getDatabase(string $methodName): string + { + $database = $this->_conn->getDatabase(); + + if ($database === null) { + throw DatabaseRequired::new($methodName); + } + + return $database; + } + + public function createComparator(): Comparator + { + return new Comparator($this->_platform); + } + + /** + * @return array>> + * + * @throws Exception + */ + private function fetchAllAssociativeGrouped(Result $result): array + { + $data = []; + + foreach ($result->fetchAllAssociative() as $row) { + $tableName = $this->_getPortableTableDefinition($row); + $data[$tableName][] = $row; + } + + return $data; + } +} diff --git a/3rdparty/doctrine/dbal/src/Schema/Column.php b/3rdparty/doctrine/dbal/src/Schema/Column.php new file mode 100644 index 00000000..e1580178 --- /dev/null +++ b/3rdparty/doctrine/dbal/src/Schema/Column.php @@ -0,0 +1,466 @@ +_setName($name); + $this->setType($type); + $this->setOptions($options); + } + + /** + * @param mixed[] $options + * + * @return Column + * + * @throws SchemaException + */ + public function setOptions(array $options) + { + foreach ($options as $name => $value) { + $method = 'set' . $name; + + if (! method_exists($this, $method)) { + throw UnknownColumnOption::new($name); + } + + $this->$method($value); + } + + return $this; + } + + /** @return Column */ + public function setType(Type $type) + { + $this->_type = $type; + + return $this; + } + + /** + * @param int|null $length + * + * @return Column + */ + public function setLength($length) + { + if ($length !== null) { + $this->_length = (int) $length; + } else { + $this->_length = null; + } + + return $this; + } + + /** + * @param int $precision + * + * @return Column + */ + public function setPrecision($precision) + { + if (! is_numeric($precision)) { + $precision = 10; // defaults to 10 when no valid precision is given. + } + + $this->_precision = (int) $precision; + + return $this; + } + + /** + * @param int $scale + * + * @return Column + */ + public function setScale($scale) + { + if (! is_numeric($scale)) { + $scale = 0; + } + + $this->_scale = (int) $scale; + + return $this; + } + + /** + * @param bool $unsigned + * + * @return Column + */ + public function setUnsigned($unsigned) + { + $this->_unsigned = (bool) $unsigned; + + return $this; + } + + /** + * @param bool $fixed + * + * @return Column + */ + public function setFixed($fixed) + { + $this->_fixed = (bool) $fixed; + + return $this; + } + + /** + * @param bool $notnull + * + * @return Column + */ + public function setNotnull($notnull) + { + $this->_notnull = (bool) $notnull; + + return $this; + } + + /** + * @param mixed $default + * + * @return Column + */ + public function setDefault($default) + { + $this->_default = $default; + + return $this; + } + + /** + * @param mixed[] $platformOptions + * + * @return Column + */ + public function setPlatformOptions(array $platformOptions) + { + $this->_platformOptions = $platformOptions; + + return $this; + } + + /** + * @param string $name + * @param mixed $value + * + * @return Column + */ + public function setPlatformOption($name, $value) + { + $this->_platformOptions[$name] = $value; + + return $this; + } + + /** + * @param string|null $value + * + * @return Column + */ + public function setColumnDefinition($value) + { + $this->_columnDefinition = $value; + + return $this; + } + + /** @return Type */ + public function getType() + { + return $this->_type; + } + + /** @return int|null */ + public function getLength() + { + return $this->_length; + } + + /** @return int */ + public function getPrecision() + { + return $this->_precision; + } + + /** @return int */ + public function getScale() + { + return $this->_scale; + } + + /** @return bool */ + public function getUnsigned() + { + return $this->_unsigned; + } + + /** @return bool */ + public function getFixed() + { + return $this->_fixed; + } + + /** @return bool */ + public function getNotnull() + { + return $this->_notnull; + } + + /** @return mixed */ + public function getDefault() + { + return $this->_default; + } + + /** @return mixed[] */ + public function getPlatformOptions() + { + return $this->_platformOptions; + } + + /** + * @param string $name + * + * @return bool + */ + public function hasPlatformOption($name) + { + return isset($this->_platformOptions[$name]); + } + + /** + * @param string $name + * + * @return mixed + */ + public function getPlatformOption($name) + { + return $this->_platformOptions[$name]; + } + + /** @return string|null */ + public function getColumnDefinition() + { + return $this->_columnDefinition; + } + + /** @return bool */ + public function getAutoincrement() + { + return $this->_autoincrement; + } + + /** + * @param bool $flag + * + * @return Column + */ + public function setAutoincrement($flag) + { + $this->_autoincrement = $flag; + + return $this; + } + + /** + * @param string|null $comment + * + * @return Column + */ + public function setComment($comment) + { + $this->_comment = $comment; + + return $this; + } + + /** @return string|null */ + public function getComment() + { + return $this->_comment; + } + + /** + * @deprecated Use {@link setPlatformOption()} instead + * + * @param string $name + * @param mixed $value + * + * @return Column + */ + public function setCustomSchemaOption($name, $value) + { + Deprecation::trigger( + 'doctrine/dbal', + 'https://github.com/doctrine/dbal/pull/5476', + 'Column::setCustomSchemaOption() is deprecated. Use setPlatformOption() instead.', + ); + + $this->_customSchemaOptions[$name] = $value; + + return $this; + } + + /** + * @deprecated Use {@link hasPlatformOption()} instead + * + * @param string $name + * + * @return bool + */ + public function hasCustomSchemaOption($name) + { + Deprecation::trigger( + 'doctrine/dbal', + 'https://github.com/doctrine/dbal/pull/5476', + 'Column::hasCustomSchemaOption() is deprecated. Use hasPlatformOption() instead.', + ); + + return isset($this->_customSchemaOptions[$name]); + } + + /** + * @deprecated Use {@link getPlatformOption()} instead + * + * @param string $name + * + * @return mixed + */ + public function getCustomSchemaOption($name) + { + Deprecation::trigger( + 'doctrine/dbal', + 'https://github.com/doctrine/dbal/pull/5476', + 'Column::getCustomSchemaOption() is deprecated. Use getPlatformOption() instead.', + ); + + return $this->_customSchemaOptions[$name]; + } + + /** + * @deprecated Use {@link setPlatformOptions()} instead + * + * @param mixed[] $customSchemaOptions + * + * @return Column + */ + public function setCustomSchemaOptions(array $customSchemaOptions) + { + Deprecation::trigger( + 'doctrine/dbal', + 'https://github.com/doctrine/dbal/pull/5476', + 'Column::setCustomSchemaOptions() is deprecated. Use setPlatformOptions() instead.', + ); + + $this->_customSchemaOptions = $customSchemaOptions; + + return $this; + } + + /** + * @deprecated Use {@link getPlatformOptions()} instead + * + * @return mixed[] + */ + public function getCustomSchemaOptions() + { + Deprecation::triggerIfCalledFromOutside( + 'doctrine/dbal', + 'https://github.com/doctrine/dbal/pull/5476', + 'Column::getCustomSchemaOptions() is deprecated. Use getPlatformOptions() instead.', + ); + + return $this->_customSchemaOptions; + } + + /** @return mixed[] */ + public function toArray() + { + return array_merge([ + 'name' => $this->_name, + 'type' => $this->_type, + 'default' => $this->_default, + 'notnull' => $this->_notnull, + 'length' => $this->_length, + 'precision' => $this->_precision, + 'scale' => $this->_scale, + 'fixed' => $this->_fixed, + 'unsigned' => $this->_unsigned, + 'autoincrement' => $this->_autoincrement, + 'columnDefinition' => $this->_columnDefinition, + 'comment' => $this->_comment, + ], $this->_platformOptions, $this->_customSchemaOptions); + } +} diff --git a/3rdparty/doctrine/dbal/src/Schema/ColumnDiff.php b/3rdparty/doctrine/dbal/src/Schema/ColumnDiff.php new file mode 100644 index 00000000..bd1b0eee --- /dev/null +++ b/3rdparty/doctrine/dbal/src/Schema/ColumnDiff.php @@ -0,0 +1,169 @@ +oldColumnName = $oldColumnName; + $this->column = $column; + $this->changedProperties = $changedProperties; + $this->fromColumn = $fromColumn; + } + + public function getOldColumn(): ?Column + { + return $this->fromColumn; + } + + public function getNewColumn(): Column + { + return $this->column; + } + + public function hasTypeChanged(): bool + { + return $this->hasChanged('type'); + } + + public function hasLengthChanged(): bool + { + return $this->hasChanged('length'); + } + + public function hasPrecisionChanged(): bool + { + return $this->hasChanged('precision'); + } + + public function hasScaleChanged(): bool + { + return $this->hasChanged('scale'); + } + + public function hasUnsignedChanged(): bool + { + return $this->hasChanged('unsigned'); + } + + public function hasFixedChanged(): bool + { + return $this->hasChanged('fixed'); + } + + public function hasNotNullChanged(): bool + { + return $this->hasChanged('notnull'); + } + + public function hasDefaultChanged(): bool + { + return $this->hasChanged('default'); + } + + public function hasAutoIncrementChanged(): bool + { + return $this->hasChanged('autoincrement'); + } + + public function hasCommentChanged(): bool + { + return $this->hasChanged('comment'); + } + + /** + * @deprecated Use {@see hasTypeChanged()}, {@see hasLengthChanged()}, {@see hasPrecisionChanged()}, + * {@see hasScaleChanged()}, {@see hasUnsignedChanged()}, {@see hasFixedChanged()}, {@see hasNotNullChanged()}, + * {@see hasDefaultChanged()}, {@see hasAutoIncrementChanged()} or {@see hasCommentChanged()} instead. + * + * @param string $propertyName + * + * @return bool + */ + public function hasChanged($propertyName) + { + return in_array($propertyName, $this->changedProperties, true); + } + + /** + * @deprecated Use {@see $fromColumn} instead. + * + * @return Identifier + */ + public function getOldColumnName() + { + Deprecation::trigger( + 'doctrine/dbal', + 'https://github.com/doctrine/dbal/pull/5622', + '%s is deprecated. Use $fromColumn instead.', + __METHOD__, + ); + + if ($this->fromColumn !== null) { + $name = $this->fromColumn->getName(); + $quote = $this->fromColumn->isQuoted(); + } else { + $name = $this->oldColumnName; + $quote = false; + } + + return new Identifier($name, $quote); + } +} diff --git a/3rdparty/doctrine/dbal/src/Schema/Comparator.php b/3rdparty/doctrine/dbal/src/Schema/Comparator.php new file mode 100644 index 00000000..8114ec5e --- /dev/null +++ b/3rdparty/doctrine/dbal/src/Schema/Comparator.php @@ -0,0 +1,716 @@ +platform = $platform; + } + + /** @param list $args */ + public function __call(string $method, array $args): SchemaDiff + { + if ($method !== 'compareSchemas') { + throw new BadMethodCallException(sprintf('Unknown method "%s"', $method)); + } + + return $this->doCompareSchemas(...$args); + } + + /** @param list $args */ + public static function __callStatic(string $method, array $args): SchemaDiff + { + if ($method !== 'compareSchemas') { + throw new BadMethodCallException(sprintf('Unknown method "%s"', $method)); + } + + Deprecation::trigger( + 'doctrine/dbal', + 'https://github.com/doctrine/dbal/pull/4707', + 'Calling %s::%s() statically is deprecated.', + self::class, + $method, + ); + + $comparator = new self(); + + return $comparator->doCompareSchemas(...$args); + } + + /** + * Returns a SchemaDiff object containing the differences between the schemas $fromSchema and $toSchema. + * + * This method should be called non-statically since it will be declared as non-static in the next major release. + * + * @return SchemaDiff + * + * @throws SchemaException + */ + private function doCompareSchemas( + Schema $fromSchema, + Schema $toSchema + ) { + $createdSchemas = []; + $droppedSchemas = []; + $createdTables = []; + $alteredTables = []; + $droppedTables = []; + $createdSequences = []; + $alteredSequences = []; + $droppedSequences = []; + + $orphanedForeignKeys = []; + + $foreignKeysToTable = []; + + foreach ($toSchema->getNamespaces() as $namespace) { + if ($fromSchema->hasNamespace($namespace)) { + continue; + } + + $createdSchemas[$namespace] = $namespace; + } + + foreach ($fromSchema->getNamespaces() as $namespace) { + if ($toSchema->hasNamespace($namespace)) { + continue; + } + + $droppedSchemas[$namespace] = $namespace; + } + + foreach ($toSchema->getTables() as $table) { + $tableName = $table->getShortestName($toSchema->getName()); + if (! $fromSchema->hasTable($tableName)) { + $createdTables[$tableName] = $toSchema->getTable($tableName); + } else { + $tableDifferences = $this->diffTable( + $fromSchema->getTable($tableName), + $toSchema->getTable($tableName), + ); + + if ($tableDifferences !== false) { + $alteredTables[$tableName] = $tableDifferences; + } + } + } + + /* Check if there are tables removed */ + foreach ($fromSchema->getTables() as $table) { + $tableName = $table->getShortestName($fromSchema->getName()); + + $table = $fromSchema->getTable($tableName); + if (! $toSchema->hasTable($tableName)) { + $droppedTables[$tableName] = $table; + } + + // also remember all foreign keys that point to a specific table + foreach ($table->getForeignKeys() as $foreignKey) { + $foreignTable = strtolower($foreignKey->getForeignTableName()); + if (! isset($foreignKeysToTable[$foreignTable])) { + $foreignKeysToTable[$foreignTable] = []; + } + + $foreignKeysToTable[$foreignTable][] = $foreignKey; + } + } + + foreach ($droppedTables as $tableName => $table) { + if (! isset($foreignKeysToTable[$tableName])) { + continue; + } + + foreach ($foreignKeysToTable[$tableName] as $foreignKey) { + if (isset($droppedTables[strtolower($foreignKey->getLocalTableName())])) { + continue; + } + + $orphanedForeignKeys[] = $foreignKey; + } + + // deleting duplicated foreign keys present on both on the orphanedForeignKey + // and the removedForeignKeys from changedTables + foreach ($foreignKeysToTable[$tableName] as $foreignKey) { + // strtolower the table name to make if compatible with getShortestName + $localTableName = strtolower($foreignKey->getLocalTableName()); + if (! isset($alteredTables[$localTableName])) { + continue; + } + + foreach ($alteredTables[$localTableName]->getDroppedForeignKeys() as $droppedForeignKey) { + assert($droppedForeignKey instanceof ForeignKeyConstraint); + + // We check if the key is from the removed table if not we skip. + if ($tableName !== strtolower($droppedForeignKey->getForeignTableName())) { + continue; + } + + $alteredTables[$localTableName]->unsetDroppedForeignKey($droppedForeignKey); + } + } + } + + foreach ($toSchema->getSequences() as $sequence) { + $sequenceName = $sequence->getShortestName($toSchema->getName()); + if (! $fromSchema->hasSequence($sequenceName)) { + if (! $this->isAutoIncrementSequenceInSchema($fromSchema, $sequence)) { + $createdSequences[] = $sequence; + } + } else { + if ($this->diffSequence($sequence, $fromSchema->getSequence($sequenceName))) { + $alteredSequences[] = $toSchema->getSequence($sequenceName); + } + } + } + + foreach ($fromSchema->getSequences() as $sequence) { + if ($this->isAutoIncrementSequenceInSchema($toSchema, $sequence)) { + continue; + } + + $sequenceName = $sequence->getShortestName($fromSchema->getName()); + + if ($toSchema->hasSequence($sequenceName)) { + continue; + } + + $droppedSequences[] = $sequence; + } + + $diff = new SchemaDiff( + $createdTables, + $alteredTables, + $droppedTables, + $fromSchema, + $createdSchemas, + $droppedSchemas, + $createdSequences, + $alteredSequences, + $droppedSequences, + ); + + $diff->orphanedForeignKeys = $orphanedForeignKeys; + + return $diff; + } + + /** + * @deprecated Use non-static call to {@see compareSchemas()} instead. + * + * @return SchemaDiff + * + * @throws SchemaException + */ + public function compare(Schema $fromSchema, Schema $toSchema) + { + Deprecation::trigger( + 'doctrine/dbal', + 'https://github.com/doctrine/dbal/pull/4707', + 'Method compare() is deprecated. Use a non-static call to compareSchemas() instead.', + ); + + return $this->compareSchemas($fromSchema, $toSchema); + } + + /** + * @param Schema $schema + * @param Sequence $sequence + */ + private function isAutoIncrementSequenceInSchema($schema, $sequence): bool + { + foreach ($schema->getTables() as $table) { + if ($sequence->isAutoIncrementsFor($table)) { + return true; + } + } + + return false; + } + + /** @return bool */ + public function diffSequence(Sequence $sequence1, Sequence $sequence2) + { + if ($sequence1->getAllocationSize() !== $sequence2->getAllocationSize()) { + return true; + } + + return $sequence1->getInitialValue() !== $sequence2->getInitialValue(); + } + + /** + * Returns the difference between the tables $fromTable and $toTable. + * + * If there are no differences this method returns the boolean false. + * + * @deprecated Use {@see compareTables()} and, optionally, {@see TableDiff::isEmpty()} instead. + * + * @return TableDiff|false + * + * @throws Exception + */ + public function diffTable(Table $fromTable, Table $toTable) + { + Deprecation::triggerIfCalledFromOutside( + 'doctrine/dbal', + 'https://github.com/doctrine/dbal/pull/5770', + '%s is deprecated. Use compareTables() instead.', + __METHOD__, + ); + + $diff = $this->compareTables($fromTable, $toTable); + + if ($diff->isEmpty()) { + return false; + } + + return $diff; + } + + /** + * Compares the tables and returns the difference between them. + * + * @throws Exception + */ + public function compareTables(Table $fromTable, Table $toTable): TableDiff + { + $addedColumns = []; + $modifiedColumns = []; + $droppedColumns = []; + $addedIndexes = []; + $modifiedIndexes = []; + $droppedIndexes = []; + $addedForeignKeys = []; + $modifiedForeignKeys = []; + $droppedForeignKeys = []; + + $fromTableColumns = $fromTable->getColumns(); + $toTableColumns = $toTable->getColumns(); + + /* See if all the columns in "from" table exist in "to" table */ + foreach ($toTableColumns as $columnName => $column) { + if ($fromTable->hasColumn($columnName)) { + continue; + } + + $addedColumns[$columnName] = $column; + } + + /* See if there are any removed columns in "to" table */ + foreach ($fromTableColumns as $columnName => $column) { + // See if column is removed in "to" table. + if (! $toTable->hasColumn($columnName)) { + $droppedColumns[$columnName] = $column; + + continue; + } + + $toColumn = $toTable->getColumn($columnName); + + // See if column has changed properties in "to" table. + $changedProperties = $this->diffColumn($column, $toColumn); + + if ($this->platform !== null) { + if ($this->columnsEqual($column, $toColumn)) { + continue; + } + } elseif (count($changedProperties) === 0) { + continue; + } + + $modifiedColumns[$column->getName()] = new ColumnDiff( + $column->getName(), + $toColumn, + $changedProperties, + $column, + ); + } + + $renamedColumns = $this->detectRenamedColumns($addedColumns, $droppedColumns); + + $fromTableIndexes = $fromTable->getIndexes(); + $toTableIndexes = $toTable->getIndexes(); + + /* See if all the indexes in "from" table exist in "to" table */ + foreach ($toTableIndexes as $indexName => $index) { + if (($index->isPrimary() && $fromTable->getPrimaryKey() !== null) || $fromTable->hasIndex($indexName)) { + continue; + } + + $addedIndexes[$indexName] = $index; + } + + /* See if there are any removed indexes in "to" table */ + foreach ($fromTableIndexes as $indexName => $index) { + // See if index is removed in "to" table. + if ( + ($index->isPrimary() && $toTable->getPrimaryKey() === null) || + ! $index->isPrimary() && ! $toTable->hasIndex($indexName) + ) { + $droppedIndexes[$indexName] = $index; + + continue; + } + + // See if index has changed in "to" table. + $toTableIndex = $index->isPrimary() ? $toTable->getPrimaryKey() : $toTable->getIndex($indexName); + assert($toTableIndex instanceof Index); + + if (! $this->diffIndex($index, $toTableIndex)) { + continue; + } + + $modifiedIndexes[$indexName] = $toTableIndex; + } + + $renamedIndexes = $this->detectRenamedIndexes($addedIndexes, $droppedIndexes); + + $fromForeignKeys = $fromTable->getForeignKeys(); + $toForeignKeys = $toTable->getForeignKeys(); + + foreach ($fromForeignKeys as $fromKey => $fromConstraint) { + foreach ($toForeignKeys as $toKey => $toConstraint) { + if ($this->diffForeignKey($fromConstraint, $toConstraint) === false) { + unset($fromForeignKeys[$fromKey], $toForeignKeys[$toKey]); + } else { + if (strtolower($fromConstraint->getName()) === strtolower($toConstraint->getName())) { + $modifiedForeignKeys[] = $toConstraint; + + unset($fromForeignKeys[$fromKey], $toForeignKeys[$toKey]); + } + } + } + } + + foreach ($fromForeignKeys as $fromConstraint) { + $droppedForeignKeys[] = $fromConstraint; + } + + foreach ($toForeignKeys as $toConstraint) { + $addedForeignKeys[] = $toConstraint; + } + + return new TableDiff( + $toTable->getName(), + $addedColumns, + $modifiedColumns, + $droppedColumns, + $addedIndexes, + $modifiedIndexes, + $droppedIndexes, + $fromTable, + $addedForeignKeys, + $modifiedForeignKeys, + $droppedForeignKeys, + $renamedColumns, + $renamedIndexes, + ); + } + + /** + * Try to find columns that only changed their name, rename operations maybe cheaper than add/drop + * however ambiguities between different possibilities should not lead to renaming at all. + * + * @param array $addedColumns + * @param array $removedColumns + * + * @return array + * + * @throws Exception + */ + private function detectRenamedColumns(array &$addedColumns, array &$removedColumns): array + { + $candidatesByName = []; + + foreach ($addedColumns as $addedColumnName => $addedColumn) { + foreach ($removedColumns as $removedColumn) { + if (! $this->columnsEqual($addedColumn, $removedColumn)) { + continue; + } + + $candidatesByName[$addedColumn->getName()][] = [$removedColumn, $addedColumn, $addedColumnName]; + } + } + + $renamedColumns = []; + + foreach ($candidatesByName as $candidates) { + if (count($candidates) !== 1) { + continue; + } + + [$removedColumn, $addedColumn] = $candidates[0]; + $removedColumnName = $removedColumn->getName(); + $addedColumnName = strtolower($addedColumn->getName()); + + if (isset($renamedColumns[$removedColumnName])) { + continue; + } + + $renamedColumns[$removedColumnName] = $addedColumn; + unset( + $addedColumns[$addedColumnName], + $removedColumns[strtolower($removedColumnName)], + ); + } + + return $renamedColumns; + } + + /** + * Try to find indexes that only changed their name, rename operations maybe cheaper than add/drop + * however ambiguities between different possibilities should not lead to renaming at all. + * + * @param array $addedIndexes + * @param array $removedIndexes + * + * @return array + */ + private function detectRenamedIndexes(array &$addedIndexes, array &$removedIndexes): array + { + $candidatesByName = []; + + // Gather possible rename candidates by comparing each added and removed index based on semantics. + foreach ($addedIndexes as $addedIndexName => $addedIndex) { + foreach ($removedIndexes as $removedIndex) { + if ($this->diffIndex($addedIndex, $removedIndex)) { + continue; + } + + $candidatesByName[$addedIndex->getName()][] = [$removedIndex, $addedIndex, $addedIndexName]; + } + } + + $renamedIndexes = []; + + foreach ($candidatesByName as $candidates) { + // If the current rename candidate contains exactly one semantically equal index, + // we can safely rename it. + // Otherwise, it is unclear if a rename action is really intended, + // therefore we let those ambiguous indexes be added/dropped. + if (count($candidates) !== 1) { + continue; + } + + [$removedIndex, $addedIndex] = $candidates[0]; + + $removedIndexName = strtolower($removedIndex->getName()); + $addedIndexName = strtolower($addedIndex->getName()); + + if (isset($renamedIndexes[$removedIndexName])) { + continue; + } + + $renamedIndexes[$removedIndexName] = $addedIndex; + unset( + $addedIndexes[$addedIndexName], + $removedIndexes[$removedIndexName], + ); + } + + return $renamedIndexes; + } + + /** + * @internal The method should be only used from within the {@see Comparator} class hierarchy. + * + * @return bool + */ + public function diffForeignKey(ForeignKeyConstraint $key1, ForeignKeyConstraint $key2) + { + if ( + array_map('strtolower', $key1->getUnquotedLocalColumns()) + !== array_map('strtolower', $key2->getUnquotedLocalColumns()) + ) { + return true; + } + + if ( + array_map('strtolower', $key1->getUnquotedForeignColumns()) + !== array_map('strtolower', $key2->getUnquotedForeignColumns()) + ) { + return true; + } + + if ($key1->getUnqualifiedForeignTableName() !== $key2->getUnqualifiedForeignTableName()) { + return true; + } + + if ($key1->onUpdate() !== $key2->onUpdate()) { + return true; + } + + return $key1->onDelete() !== $key2->onDelete(); + } + + /** + * Compares the definitions of the given columns + * + * @internal The method should be only used from within the {@see Comparator} class hierarchy. + * + * @throws Exception + */ + public function columnsEqual(Column $column1, Column $column2): bool + { + if ($this->platform === null) { + return $this->diffColumn($column1, $column2) === []; + } + + return $this->platform->columnsEqual($column1, $column2); + } + + /** + * Returns the difference between the columns + * + * If there are differences this method returns the changed properties as a + * string array, otherwise an empty array gets returned. + * + * @deprecated Use {@see columnsEqual()} instead. + * + * @return string[] + */ + public function diffColumn(Column $column1, Column $column2) + { + Deprecation::triggerIfCalledFromOutside( + 'doctrine/dbal', + 'https://github.com/doctrine/dbal/pull/5650', + '%s is deprecated. Use diffTable() instead.', + __METHOD__, + ); + + $properties1 = $column1->toArray(); + $properties2 = $column2->toArray(); + + $changedProperties = []; + + if (get_class($properties1['type']) !== get_class($properties2['type'])) { + $changedProperties[] = 'type'; + } + + foreach (['notnull', 'unsigned', 'autoincrement'] as $property) { + if ($properties1[$property] === $properties2[$property]) { + continue; + } + + $changedProperties[] = $property; + } + + // Null values need to be checked additionally as they tell whether to create or drop a default value. + // null != 0, null != false, null != '' etc. This affects platform's table alteration SQL generation. + if ( + ($properties1['default'] === null) !== ($properties2['default'] === null) + || $properties1['default'] != $properties2['default'] // @phpstan-ignore notEqual.notAllowed + ) { + $changedProperties[] = 'default'; + } + + if ( + ($properties1['type'] instanceof Types\StringType && ! $properties1['type'] instanceof Types\GuidType) || + $properties1['type'] instanceof Types\BinaryType + ) { + // check if value of length is set at all, default value assumed otherwise. + $length1 = $properties1['length'] ?? 255; + $length2 = $properties2['length'] ?? 255; + if ($length1 !== $length2) { + $changedProperties[] = 'length'; + } + + if ($properties1['fixed'] !== $properties2['fixed']) { + $changedProperties[] = 'fixed'; + } + } elseif ($properties1['type'] instanceof Types\DecimalType) { + if (($properties1['precision'] ?? 10) !== ($properties2['precision'] ?? 10)) { + $changedProperties[] = 'precision'; + } + + if ($properties1['scale'] !== $properties2['scale']) { + $changedProperties[] = 'scale'; + } + } + + // A null value and an empty string are actually equal for a comment so they should not trigger a change. + if ( + $properties1['comment'] !== $properties2['comment'] && + ! ($properties1['comment'] === null && $properties2['comment'] === '') && + ! ($properties2['comment'] === null && $properties1['comment'] === '') + ) { + $changedProperties[] = 'comment'; + } + + $customOptions1 = $column1->getCustomSchemaOptions(); + $customOptions2 = $column2->getCustomSchemaOptions(); + + foreach (array_merge(array_keys($customOptions1), array_keys($customOptions2)) as $key) { + if (! array_key_exists($key, $properties1) || ! array_key_exists($key, $properties2)) { + $changedProperties[] = $key; + } elseif ($properties1[$key] !== $properties2[$key]) { + $changedProperties[] = $key; + } + } + + $platformOptions1 = $column1->getPlatformOptions(); + $platformOptions2 = $column2->getPlatformOptions(); + + foreach (array_keys(array_intersect_key($platformOptions1, $platformOptions2)) as $key) { + if ($properties1[$key] === $properties2[$key]) { + continue; + } + + $changedProperties[] = $key; + } + + return array_unique($changedProperties); + } + + /** + * Finds the difference between the indexes $index1 and $index2. + * + * Compares $index1 with $index2 and returns true if there are any + * differences or false in case there are no differences. + * + * @internal The method should be only used from within the {@see Comparator} class hierarchy. + * + * @return bool + */ + public function diffIndex(Index $index1, Index $index2) + { + return ! ($index1->isFulfilledBy($index2) && $index2->isFulfilledBy($index1)); + } +} diff --git a/3rdparty/doctrine/dbal/src/Schema/Constraint.php b/3rdparty/doctrine/dbal/src/Schema/Constraint.php new file mode 100644 index 00000000..f47ee1fd --- /dev/null +++ b/3rdparty/doctrine/dbal/src/Schema/Constraint.php @@ -0,0 +1,41 @@ + + */ +class DB2SchemaManager extends AbstractSchemaManager +{ + /** + * {@inheritDoc} + */ + public function listTableNames() + { + return $this->doListTableNames(); + } + + /** + * {@inheritDoc} + */ + public function listTables() + { + return $this->doListTables(); + } + + /** + * {@inheritDoc} + * + * @deprecated Use {@see introspectTable()} instead. + */ + public function listTableDetails($name) + { + Deprecation::triggerIfCalledFromOutside( + 'doctrine/dbal', + 'https://github.com/doctrine/dbal/pull/5595', + '%s is deprecated. Use introspectTable() instead.', + __METHOD__, + ); + + return $this->doListTableDetails($name); + } + + /** + * {@inheritDoc} + */ + public function listTableColumns($table, $database = null) + { + return $this->doListTableColumns($table, $database); + } + + /** + * {@inheritDoc} + */ + public function listTableIndexes($table) + { + return $this->doListTableIndexes($table); + } + + /** + * {@inheritDoc} + */ + public function listTableForeignKeys($table, $database = null) + { + return $this->doListTableForeignKeys($table, $database); + } + + /** + * {@inheritDoc} + * + * @throws Exception + */ + protected function _getPortableTableColumnDefinition($tableColumn) + { + $tableColumn = array_change_key_case($tableColumn, CASE_LOWER); + + $length = null; + $fixed = null; + $scale = false; + $precision = false; + + $default = null; + + if ($tableColumn['default'] !== null && $tableColumn['default'] !== 'NULL') { + $default = $tableColumn['default']; + + if (preg_match('/^\'(.*)\'$/s', $default, $matches) === 1) { + $default = str_replace("''", "'", $matches[1]); + } + } + + $type = $this->_platform->getDoctrineTypeMapping($tableColumn['typename']); + + if (isset($tableColumn['comment'])) { + $type = $this->extractDoctrineTypeFromComment($tableColumn['comment'], $type); + $tableColumn['comment'] = $this->removeDoctrineTypeFromComment($tableColumn['comment'], $type); + } + + switch (strtolower($tableColumn['typename'])) { + case 'varchar': + if ($tableColumn['codepage'] === 0) { + $type = Types::BINARY; + } + + $length = $tableColumn['length']; + $fixed = false; + break; + + case 'character': + if ($tableColumn['codepage'] === 0) { + $type = Types::BINARY; + } + + $length = $tableColumn['length']; + $fixed = true; + break; + + case 'clob': + $length = $tableColumn['length']; + break; + + case 'decimal': + case 'double': + case 'real': + $scale = $tableColumn['scale']; + $precision = $tableColumn['length']; + break; + } + + $options = [ + 'length' => $length, + 'fixed' => (bool) $fixed, + 'default' => $default, + 'autoincrement' => (bool) $tableColumn['autoincrement'], + 'notnull' => $tableColumn['nulls'] === 'N', + 'comment' => isset($tableColumn['comment']) && $tableColumn['comment'] !== '' + ? $tableColumn['comment'] + : null, + ]; + + if ($scale !== null && $precision !== null) { + $options['scale'] = $scale; + $options['precision'] = $precision; + } + + return new Column($tableColumn['colname'], Type::getType($type), $options); + } + + /** + * {@inheritDoc} + */ + protected function _getPortableTableDefinition($table) + { + $table = array_change_key_case($table, CASE_LOWER); + + return $table['name']; + } + + /** + * {@inheritDoc} + */ + protected function _getPortableTableIndexesList($tableIndexes, $tableName = null) + { + foreach ($tableIndexes as &$tableIndexRow) { + $tableIndexRow = array_change_key_case($tableIndexRow, CASE_LOWER); + $tableIndexRow['primary'] = (bool) $tableIndexRow['primary']; + } + + return parent::_getPortableTableIndexesList($tableIndexes, $tableName); + } + + /** + * {@inheritDoc} + */ + protected function _getPortableTableForeignKeyDefinition($tableForeignKey) + { + return new ForeignKeyConstraint( + $tableForeignKey['local_columns'], + $tableForeignKey['foreign_table'], + $tableForeignKey['foreign_columns'], + $tableForeignKey['name'], + $tableForeignKey['options'], + ); + } + + /** + * {@inheritDoc} + */ + protected function _getPortableTableForeignKeysList($tableForeignKeys) + { + $foreignKeys = []; + + foreach ($tableForeignKeys as $tableForeignKey) { + $tableForeignKey = array_change_key_case($tableForeignKey, CASE_LOWER); + + if (! isset($foreignKeys[$tableForeignKey['index_name']])) { + $foreignKeys[$tableForeignKey['index_name']] = [ + 'local_columns' => [$tableForeignKey['local_column']], + 'foreign_table' => $tableForeignKey['foreign_table'], + 'foreign_columns' => [$tableForeignKey['foreign_column']], + 'name' => $tableForeignKey['index_name'], + 'options' => [ + 'onUpdate' => $tableForeignKey['on_update'], + 'onDelete' => $tableForeignKey['on_delete'], + ], + ]; + } else { + $foreignKeys[$tableForeignKey['index_name']]['local_columns'][] = $tableForeignKey['local_column']; + $foreignKeys[$tableForeignKey['index_name']]['foreign_columns'][] = $tableForeignKey['foreign_column']; + } + } + + return parent::_getPortableTableForeignKeysList($foreignKeys); + } + + /** + * @param string $def + * + * @return string|null + */ + protected function _getPortableForeignKeyRuleDef($def) + { + if ($def === 'C') { + return 'CASCADE'; + } + + if ($def === 'N') { + return 'SET NULL'; + } + + return null; + } + + /** + * {@inheritDoc} + */ + protected function _getPortableViewDefinition($view) + { + $view = array_change_key_case($view, CASE_LOWER); + + $sql = ''; + $pos = strpos($view['text'], ' AS '); + + if ($pos !== false) { + $sql = substr($view['text'], $pos + 4); + } + + return new View($view['name'], $sql); + } + + protected function normalizeName(string $name): string + { + $identifier = new Identifier($name); + + return $identifier->isQuoted() ? $identifier->getName() : strtoupper($name); + } + + protected function selectTableNames(string $databaseName): Result + { + $sql = <<<'SQL' +SELECT NAME +FROM SYSIBM.SYSTABLES +WHERE TYPE = 'T' + AND CREATOR = ? +SQL; + + return $this->_conn->executeQuery($sql, [$databaseName]); + } + + protected function selectTableColumns(string $databaseName, ?string $tableName = null): Result + { + $sql = 'SELECT'; + + if ($tableName === null) { + $sql .= ' C.TABNAME AS NAME,'; + } + + $sql .= <<<'SQL' + C.COLNAME, + C.TYPENAME, + C.CODEPAGE, + C.NULLS, + C.LENGTH, + C.SCALE, + C.REMARKS AS COMMENT, + CASE + WHEN C.GENERATED = 'D' THEN 1 + ELSE 0 + END AS AUTOINCREMENT, + C.DEFAULT +FROM SYSCAT.COLUMNS C + JOIN SYSCAT.TABLES AS T + ON T.TABSCHEMA = C.TABSCHEMA + AND T.TABNAME = C.TABNAME +SQL; + + $conditions = ['C.TABSCHEMA = ?', "T.TYPE = 'T'"]; + $params = [$databaseName]; + + if ($tableName !== null) { + $conditions[] = 'C.TABNAME = ?'; + $params[] = $tableName; + } + + $sql .= ' WHERE ' . implode(' AND ', $conditions) . ' ORDER BY C.TABNAME, C.COLNO'; + + return $this->_conn->executeQuery($sql, $params); + } + + protected function selectIndexColumns(string $databaseName, ?string $tableName = null): Result + { + $sql = 'SELECT'; + + if ($tableName === null) { + $sql .= ' IDX.TABNAME AS NAME,'; + } + + $sql .= <<<'SQL' + IDX.INDNAME AS KEY_NAME, + IDXCOL.COLNAME AS COLUMN_NAME, + CASE + WHEN IDX.UNIQUERULE = 'P' THEN 1 + ELSE 0 + END AS PRIMARY, + CASE + WHEN IDX.UNIQUERULE = 'D' THEN 1 + ELSE 0 + END AS NON_UNIQUE + FROM SYSCAT.INDEXES AS IDX + JOIN SYSCAT.TABLES AS T + ON IDX.TABSCHEMA = T.TABSCHEMA AND IDX.TABNAME = T.TABNAME + JOIN SYSCAT.INDEXCOLUSE AS IDXCOL + ON IDX.INDSCHEMA = IDXCOL.INDSCHEMA AND IDX.INDNAME = IDXCOL.INDNAME +SQL; + + $conditions = ['IDX.TABSCHEMA = ?', "T.TYPE = 'T'"]; + $params = [$databaseName]; + + if ($tableName !== null) { + $conditions[] = 'IDX.TABNAME = ?'; + $params[] = $tableName; + } + + $sql .= ' WHERE ' . implode(' AND ', $conditions) . ' ORDER BY IDX.INDNAME, IDXCOL.COLSEQ'; + + return $this->_conn->executeQuery($sql, $params); + } + + protected function selectForeignKeyColumns(string $databaseName, ?string $tableName = null): Result + { + $sql = 'SELECT'; + + if ($tableName === null) { + $sql .= ' R.TABNAME AS NAME,'; + } + + $sql .= <<<'SQL' + FKCOL.COLNAME AS LOCAL_COLUMN, + R.REFTABNAME AS FOREIGN_TABLE, + PKCOL.COLNAME AS FOREIGN_COLUMN, + R.CONSTNAME AS INDEX_NAME, + CASE + WHEN R.UPDATERULE = 'R' THEN 'RESTRICT' + END AS ON_UPDATE, + CASE + WHEN R.DELETERULE = 'C' THEN 'CASCADE' + WHEN R.DELETERULE = 'N' THEN 'SET NULL' + WHEN R.DELETERULE = 'R' THEN 'RESTRICT' + END AS ON_DELETE + FROM SYSCAT.REFERENCES AS R + JOIN SYSCAT.TABLES AS T + ON T.TABSCHEMA = R.TABSCHEMA + AND T.TABNAME = R.TABNAME + JOIN SYSCAT.KEYCOLUSE AS FKCOL + ON FKCOL.CONSTNAME = R.CONSTNAME + AND FKCOL.TABSCHEMA = R.TABSCHEMA + AND FKCOL.TABNAME = R.TABNAME + JOIN SYSCAT.KEYCOLUSE AS PKCOL + ON PKCOL.CONSTNAME = R.REFKEYNAME + AND PKCOL.TABSCHEMA = R.REFTABSCHEMA + AND PKCOL.TABNAME = R.REFTABNAME + AND PKCOL.COLSEQ = FKCOL.COLSEQ +SQL; + + $conditions = ['R.TABSCHEMA = ?', "T.TYPE = 'T'"]; + $params = [$databaseName]; + + if ($tableName !== null) { + $conditions[] = 'R.TABNAME = ?'; + $params[] = $tableName; + } + + $sql .= ' WHERE ' . implode(' AND ', $conditions) . ' ORDER BY R.CONSTNAME, FKCOL.COLSEQ'; + + return $this->_conn->executeQuery($sql, $params); + } + + /** + * {@inheritDoc} + */ + protected function fetchTableOptionsByTable(string $databaseName, ?string $tableName = null): array + { + $sql = 'SELECT NAME, REMARKS'; + + $conditions = []; + $params = []; + + if ($tableName !== null) { + $conditions[] = 'NAME = ?'; + $params[] = $tableName; + } + + $sql .= ' FROM SYSIBM.SYSTABLES'; + + if ($conditions !== []) { + $sql .= ' WHERE ' . implode(' AND ', $conditions); + } + + /** @var array> $metadata */ + $metadata = $this->_conn->executeQuery($sql, $params) + ->fetchAllAssociativeIndexed(); + + $tableOptions = []; + foreach ($metadata as $table => $data) { + $data = array_change_key_case($data, CASE_LOWER); + + $tableOptions[$table] = ['comment' => $data['remarks']]; + } + + return $tableOptions; + } +} diff --git a/3rdparty/doctrine/dbal/src/Schema/DefaultSchemaManagerFactory.php b/3rdparty/doctrine/dbal/src/Schema/DefaultSchemaManagerFactory.php new file mode 100644 index 00000000..efba87f2 --- /dev/null +++ b/3rdparty/doctrine/dbal/src/Schema/DefaultSchemaManagerFactory.php @@ -0,0 +1,20 @@ +getDatabasePlatform()->createSchemaManager($connection); + } +} diff --git a/3rdparty/doctrine/dbal/src/Schema/Exception/ColumnAlreadyExists.php b/3rdparty/doctrine/dbal/src/Schema/Exception/ColumnAlreadyExists.php new file mode 100644 index 00000000..a0d62d20 --- /dev/null +++ b/3rdparty/doctrine/dbal/src/Schema/Exception/ColumnAlreadyExists.php @@ -0,0 +1,20 @@ +getName(), + implode(', ', $foreignKey->getColumns()), + $foreignKey->getForeignTableName(), + implode(', ', $foreignKey->getForeignColumns()), + ), + ); + } +} diff --git a/3rdparty/doctrine/dbal/src/Schema/Exception/NamespaceAlreadyExists.php b/3rdparty/doctrine/dbal/src/Schema/Exception/NamespaceAlreadyExists.php new file mode 100644 index 00000000..527e196e --- /dev/null +++ b/3rdparty/doctrine/dbal/src/Schema/Exception/NamespaceAlreadyExists.php @@ -0,0 +1,20 @@ + Identifier) + * + * @var Identifier[] + */ + protected $_localColumnNames; + + /** + * Table or asset identifier instance of the referenced table name the foreign key constraint is associated with. + * + * @var Table|Identifier + */ + protected $_foreignTableName; + + /** + * Asset identifier instances of the referenced table column names the foreign key constraint is associated with. + * array($columnName => Identifier) + * + * @var Identifier[] + */ + protected $_foreignColumnNames; + + /** + * Options associated with the foreign key constraint. + * + * @var mixed[] + */ + protected $_options; + + /** + * Initializes the foreign key constraint. + * + * @param string[] $localColumnNames Names of the referencing table columns. + * @param Table|string $foreignTableName Referenced table. + * @param string[] $foreignColumnNames Names of the referenced table columns. + * @param string|null $name Name of the foreign key constraint. + * @param mixed[] $options Options associated with the foreign key constraint. + */ + public function __construct( + array $localColumnNames, + $foreignTableName, + array $foreignColumnNames, + $name = null, + array $options = [] + ) { + if ($name !== null) { + $this->_setName($name); + } + + $this->_localColumnNames = $this->createIdentifierMap($localColumnNames); + + if ($foreignTableName instanceof Table) { + $this->_foreignTableName = $foreignTableName; + } else { + $this->_foreignTableName = new Identifier($foreignTableName); + } + + $this->_foreignColumnNames = $this->createIdentifierMap($foreignColumnNames); + $this->_options = $options; + } + + /** + * @param string[] $names + * + * @return Identifier[] + */ + private function createIdentifierMap(array $names): array + { + $identifiers = []; + + foreach ($names as $name) { + $identifiers[$name] = new Identifier($name); + } + + return $identifiers; + } + + /** + * Returns the name of the referencing table + * the foreign key constraint is associated with. + * + * @deprecated Use the table that contains the foreign key as part of its {@see Table::$_fkConstraints} instead. + * + * @return string + */ + public function getLocalTableName() + { + return $this->_localTable->getName(); + } + + /** + * Sets the Table instance of the referencing table + * the foreign key constraint is associated with. + * + * @deprecated Use the table that contains the foreign key as part of its {@see Table::$_fkConstraints} instead. + * + * @param Table $table Instance of the referencing table. + * + * @return void + */ + public function setLocalTable(Table $table) + { + $this->_localTable = $table; + } + + /** + * @deprecated Use the table that contains the foreign key as part of its {@see Table::$_fkConstraints} instead. + * + * @return Table + */ + public function getLocalTable() + { + return $this->_localTable; + } + + /** + * Returns the names of the referencing table columns + * the foreign key constraint is associated with. + * + * @return string[] + */ + public function getLocalColumns() + { + return array_keys($this->_localColumnNames); + } + + /** + * Returns the quoted representation of the referencing table column names + * the foreign key constraint is associated with. + * + * But only if they were defined with one or the referencing table column name + * is a keyword reserved by the platform. + * Otherwise the plain unquoted value as inserted is returned. + * + * @param AbstractPlatform $platform The platform to use for quotation. + * + * @return string[] + */ + public function getQuotedLocalColumns(AbstractPlatform $platform) + { + $columns = []; + + foreach ($this->_localColumnNames as $column) { + $columns[] = $column->getQuotedName($platform); + } + + return $columns; + } + + /** + * Returns unquoted representation of local table column names for comparison with other FK + * + * @return string[] + */ + public function getUnquotedLocalColumns() + { + return array_map([$this, 'trimQuotes'], $this->getLocalColumns()); + } + + /** + * Returns unquoted representation of foreign table column names for comparison with other FK + * + * @return string[] + */ + public function getUnquotedForeignColumns() + { + return array_map([$this, 'trimQuotes'], $this->getForeignColumns()); + } + + /** + * {@inheritDoc} + * + * @deprecated Use {@see getLocalColumns()} instead. + * + * @see getLocalColumns + */ + public function getColumns() + { + return $this->getLocalColumns(); + } + + /** + * Returns the quoted representation of the referencing table column names + * the foreign key constraint is associated with. + * + * But only if they were defined with one or the referencing table column name + * is a keyword reserved by the platform. + * Otherwise the plain unquoted value as inserted is returned. + * + * @deprecated Use {@see getQuotedLocalColumns()} instead. + * + * @see getQuotedLocalColumns + * + * @param AbstractPlatform $platform The platform to use for quotation. + * + * @return string[] + */ + public function getQuotedColumns(AbstractPlatform $platform) + { + return $this->getQuotedLocalColumns($platform); + } + + /** + * Returns the name of the referenced table + * the foreign key constraint is associated with. + * + * @return string + */ + public function getForeignTableName() + { + return $this->_foreignTableName->getName(); + } + + /** + * Returns the non-schema qualified foreign table name. + * + * @return string + */ + public function getUnqualifiedForeignTableName() + { + $name = $this->_foreignTableName->getName(); + $position = strrpos($name, '.'); + + if ($position !== false) { + $name = substr($name, $position + 1); + } + + if ($this->isIdentifierQuoted($name)) { + $name = $this->trimQuotes($name); + } + + return strtolower($name); + } + + /** + * Returns the quoted representation of the referenced table name + * the foreign key constraint is associated with. + * + * But only if it was defined with one or the referenced table name + * is a keyword reserved by the platform. + * Otherwise the plain unquoted value as inserted is returned. + * + * @param AbstractPlatform $platform The platform to use for quotation. + * + * @return string + */ + public function getQuotedForeignTableName(AbstractPlatform $platform) + { + return $this->_foreignTableName->getQuotedName($platform); + } + + /** + * Returns the names of the referenced table columns + * the foreign key constraint is associated with. + * + * @return string[] + */ + public function getForeignColumns() + { + return array_keys($this->_foreignColumnNames); + } + + /** + * Returns the quoted representation of the referenced table column names + * the foreign key constraint is associated with. + * + * But only if they were defined with one or the referenced table column name + * is a keyword reserved by the platform. + * Otherwise the plain unquoted value as inserted is returned. + * + * @param AbstractPlatform $platform The platform to use for quotation. + * + * @return string[] + */ + public function getQuotedForeignColumns(AbstractPlatform $platform) + { + $columns = []; + + foreach ($this->_foreignColumnNames as $column) { + $columns[] = $column->getQuotedName($platform); + } + + return $columns; + } + + /** + * Returns whether or not a given option + * is associated with the foreign key constraint. + * + * @param string $name Name of the option to check. + * + * @return bool + */ + public function hasOption($name) + { + return isset($this->_options[$name]); + } + + /** + * Returns an option associated with the foreign key constraint. + * + * @param string $name Name of the option the foreign key constraint is associated with. + * + * @return mixed + */ + public function getOption($name) + { + return $this->_options[$name]; + } + + /** + * Returns the options associated with the foreign key constraint. + * + * @return mixed[] + */ + public function getOptions() + { + return $this->_options; + } + + /** + * Returns the referential action for UPDATE operations + * on the referenced table the foreign key constraint is associated with. + * + * @return string|null + */ + public function onUpdate() + { + return $this->onEvent('onUpdate'); + } + + /** + * Returns the referential action for DELETE operations + * on the referenced table the foreign key constraint is associated with. + * + * @return string|null + */ + public function onDelete() + { + return $this->onEvent('onDelete'); + } + + /** + * Returns the referential action for a given database operation + * on the referenced table the foreign key constraint is associated with. + * + * @param string $event Name of the database operation/event to return the referential action for. + */ + private function onEvent($event): ?string + { + if (isset($this->_options[$event])) { + $onEvent = strtoupper($this->_options[$event]); + + if ($onEvent !== 'NO ACTION' && $onEvent !== 'RESTRICT') { + return $onEvent; + } + } + + return null; + } + + /** + * Checks whether this foreign key constraint intersects the given index columns. + * + * Returns `true` if at least one of this foreign key's local columns + * matches one of the given index's columns, `false` otherwise. + * + * @param Index $index The index to be checked against. + * + * @return bool + */ + public function intersectsIndexColumns(Index $index) + { + foreach ($index->getColumns() as $indexColumn) { + foreach ($this->_localColumnNames as $localColumn) { + if (strtolower($indexColumn) === strtolower($localColumn->getName())) { + return true; + } + } + } + + return false; + } +} diff --git a/3rdparty/doctrine/dbal/src/Schema/Identifier.php b/3rdparty/doctrine/dbal/src/Schema/Identifier.php new file mode 100644 index 00000000..f34465e9 --- /dev/null +++ b/3rdparty/doctrine/dbal/src/Schema/Identifier.php @@ -0,0 +1,27 @@ +_setName($identifier); + + if (! $quote || $this->_quoted) { + return; + } + + $this->_setName('"' . $this->getName() . '"'); + } +} diff --git a/3rdparty/doctrine/dbal/src/Schema/Index.php b/3rdparty/doctrine/dbal/src/Schema/Index.php new file mode 100644 index 00000000..84fac414 --- /dev/null +++ b/3rdparty/doctrine/dbal/src/Schema/Index.php @@ -0,0 +1,365 @@ + Identifier) + * + * @var Identifier[] + */ + protected $_columns = []; + + /** @var bool */ + protected $_isUnique = false; + + /** @var bool */ + protected $_isPrimary = false; + + /** + * Platform specific flags for indexes. + * array($flagName => true) + * + * @var true[] + */ + protected $_flags = []; + + /** + * Platform specific options + * + * @todo $_flags should eventually be refactored into options + * @var mixed[] + */ + private array $options = []; + + /** + * @param string $name + * @param string[] $columns + * @param bool $isUnique + * @param bool $isPrimary + * @param string[] $flags + * @param mixed[] $options + */ + public function __construct( + $name, + array $columns, + $isUnique = false, + $isPrimary = false, + array $flags = [], + array $options = [] + ) { + $isUnique = $isUnique || $isPrimary; + + $this->_setName($name); + $this->_isUnique = $isUnique; + $this->_isPrimary = $isPrimary; + $this->options = $options; + + foreach ($columns as $column) { + $this->_addColumn($column); + } + + foreach ($flags as $flag) { + $this->addFlag($flag); + } + } + + /** @throws InvalidArgumentException */ + protected function _addColumn(string $column): void + { + $this->_columns[$column] = new Identifier($column); + } + + /** + * {@inheritDoc} + */ + public function getColumns() + { + return array_keys($this->_columns); + } + + /** + * {@inheritDoc} + */ + public function getQuotedColumns(AbstractPlatform $platform) + { + $subParts = $platform->supportsColumnLengthIndexes() && $this->hasOption('lengths') + ? $this->getOption('lengths') : []; + + $columns = []; + + foreach ($this->_columns as $column) { + $length = array_shift($subParts); + + $quotedColumn = $column->getQuotedName($platform); + + if ($length !== null) { + $quotedColumn .= '(' . $length . ')'; + } + + $columns[] = $quotedColumn; + } + + return $columns; + } + + /** @return string[] */ + public function getUnquotedColumns() + { + return array_map([$this, 'trimQuotes'], $this->getColumns()); + } + + /** + * Is the index neither unique nor primary key? + * + * @return bool + */ + public function isSimpleIndex() + { + return ! $this->_isPrimary && ! $this->_isUnique; + } + + /** @return bool */ + public function isUnique() + { + return $this->_isUnique; + } + + /** @return bool */ + public function isPrimary() + { + return $this->_isPrimary; + } + + /** + * @param string $name + * @param int $pos + * + * @return bool + */ + public function hasColumnAtPosition($name, $pos = 0) + { + $name = $this->trimQuotes(strtolower($name)); + $indexColumns = array_map('strtolower', $this->getUnquotedColumns()); + + return array_search($name, $indexColumns, true) === $pos; + } + + /** + * Checks if this index exactly spans the given column names in the correct order. + * + * @param string[] $columnNames + * + * @return bool + */ + public function spansColumns(array $columnNames) + { + $columns = $this->getColumns(); + $numberOfColumns = count($columns); + $sameColumns = true; + + for ($i = 0; $i < $numberOfColumns; $i++) { + if ( + isset($columnNames[$i]) + && $this->trimQuotes(strtolower($columns[$i])) === $this->trimQuotes(strtolower($columnNames[$i])) + ) { + continue; + } + + $sameColumns = false; + } + + return $sameColumns; + } + + /** + * Keeping misspelled function name for backwards compatibility + * + * @deprecated Use {@see isFulfilledBy()} instead. + * + * @return bool + */ + public function isFullfilledBy(Index $other) + { + return $this->isFulfilledBy($other); + } + + /** + * Checks if the other index already fulfills all the indexing and constraint needs of the current one. + */ + public function isFulfilledBy(Index $other): bool + { + // allow the other index to be equally large only. It being larger is an option + // but it creates a problem with scenarios of the kind PRIMARY KEY(foo,bar) UNIQUE(foo) + if (count($other->getColumns()) !== count($this->getColumns())) { + return false; + } + + // Check if columns are the same, and even in the same order + $sameColumns = $this->spansColumns($other->getColumns()); + + if ($sameColumns) { + if (! $this->samePartialIndex($other)) { + return false; + } + + if (! $this->hasSameColumnLengths($other)) { + return false; + } + + if (! $this->isUnique() && ! $this->isPrimary()) { + // this is a special case: If the current key is neither primary or unique, any unique or + // primary key will always have the same effect for the index and there cannot be any constraint + // overlaps. This means a primary or unique index can always fulfill the requirements of just an + // index that has no constraints. + return true; + } + + if ($other->isPrimary() !== $this->isPrimary()) { + return false; + } + + return $other->isUnique() === $this->isUnique(); + } + + return false; + } + + /** + * Detects if the other index is a non-unique, non primary index that can be overwritten by this one. + * + * @return bool + */ + public function overrules(Index $other) + { + if ($other->isPrimary()) { + return false; + } + + if ($this->isSimpleIndex() && $other->isUnique()) { + return false; + } + + return $this->spansColumns($other->getColumns()) + && ($this->isPrimary() || $this->isUnique()) + && $this->samePartialIndex($other); + } + + /** + * Returns platform specific flags for indexes. + * + * @return string[] + */ + public function getFlags() + { + return array_keys($this->_flags); + } + + /** + * Adds Flag for an index that translates to platform specific handling. + * + * @param string $flag + * + * @return Index + * + * @example $index->addFlag('CLUSTERED') + */ + public function addFlag($flag) + { + $this->_flags[strtolower($flag)] = true; + + return $this; + } + + /** + * Does this index have a specific flag? + * + * @param string $flag + * + * @return bool + */ + public function hasFlag($flag) + { + return isset($this->_flags[strtolower($flag)]); + } + + /** + * Removes a flag. + * + * @param string $flag + * + * @return void + */ + public function removeFlag($flag) + { + unset($this->_flags[strtolower($flag)]); + } + + /** + * @param string $name + * + * @return bool + */ + public function hasOption($name) + { + return isset($this->options[strtolower($name)]); + } + + /** + * @param string $name + * + * @return mixed + */ + public function getOption($name) + { + return $this->options[strtolower($name)]; + } + + /** @return mixed[] */ + public function getOptions() + { + return $this->options; + } + + /** + * Return whether the two indexes have the same partial index + */ + private function samePartialIndex(Index $other): bool + { + if ( + $this->hasOption('where') + && $other->hasOption('where') + && $this->getOption('where') === $other->getOption('where') + ) { + return true; + } + + return ! $this->hasOption('where') && ! $other->hasOption('where'); + } + + /** + * Returns whether the index has the same column lengths as the other + */ + private function hasSameColumnLengths(self $other): bool + { + $filter = static function (?int $length): bool { + return $length !== null; + }; + + return array_filter($this->options['lengths'] ?? [], $filter) + === array_filter($other->options['lengths'] ?? [], $filter); + } +} diff --git a/3rdparty/doctrine/dbal/src/Schema/LegacySchemaManagerFactory.php b/3rdparty/doctrine/dbal/src/Schema/LegacySchemaManagerFactory.php new file mode 100644 index 00000000..01c856b6 --- /dev/null +++ b/3rdparty/doctrine/dbal/src/Schema/LegacySchemaManagerFactory.php @@ -0,0 +1,19 @@ +getDriver()->getSchemaManager( + $connection, + $connection->getDatabasePlatform(), + ); + } +} diff --git a/3rdparty/doctrine/dbal/src/Schema/MySQLSchemaManager.php b/3rdparty/doctrine/dbal/src/Schema/MySQLSchemaManager.php new file mode 100644 index 00000000..6e444d21 --- /dev/null +++ b/3rdparty/doctrine/dbal/src/Schema/MySQLSchemaManager.php @@ -0,0 +1,605 @@ + + */ +class MySQLSchemaManager extends AbstractSchemaManager +{ + /** @see https://mariadb.com/kb/en/library/string-literals/#escape-sequences */ + private const MARIADB_ESCAPE_SEQUENCES = [ + '\\0' => "\0", + "\\'" => "'", + '\\"' => '"', + '\\b' => "\b", + '\\n' => "\n", + '\\r' => "\r", + '\\t' => "\t", + '\\Z' => "\x1a", + '\\\\' => '\\', + '\\%' => '%', + '\\_' => '_', + + // Internally, MariaDB escapes single quotes using the standard syntax + "''" => "'", + ]; + + /** + * {@inheritDoc} + */ + public function listTableNames() + { + return $this->doListTableNames(); + } + + /** + * {@inheritDoc} + */ + public function listTables() + { + return $this->doListTables(); + } + + /** + * {@inheritDoc} + * + * @deprecated Use {@see introspectTable()} instead. + */ + public function listTableDetails($name) + { + Deprecation::triggerIfCalledFromOutside( + 'doctrine/dbal', + 'https://github.com/doctrine/dbal/pull/5595', + '%s is deprecated. Use introspectTable() instead.', + __METHOD__, + ); + + return $this->doListTableDetails($name); + } + + /** + * {@inheritDoc} + */ + public function listTableColumns($table, $database = null) + { + return $this->doListTableColumns($table, $database); + } + + /** + * {@inheritDoc} + */ + public function listTableIndexes($table) + { + return $this->doListTableIndexes($table); + } + + /** + * {@inheritDoc} + */ + public function listTableForeignKeys($table, $database = null) + { + return $this->doListTableForeignKeys($table, $database); + } + + /** + * {@inheritDoc} + */ + protected function _getPortableViewDefinition($view) + { + return new View($view['TABLE_NAME'], $view['VIEW_DEFINITION']); + } + + /** + * {@inheritDoc} + */ + protected function _getPortableTableDefinition($table) + { + return array_shift($table); + } + + /** + * {@inheritDoc} + */ + protected function _getPortableTableIndexesList($tableIndexes, $tableName = null) + { + foreach ($tableIndexes as $k => $v) { + $v = array_change_key_case($v, CASE_LOWER); + if ($v['key_name'] === 'PRIMARY') { + $v['primary'] = true; + } else { + $v['primary'] = false; + } + + if (strpos($v['index_type'], 'FULLTEXT') !== false) { + $v['flags'] = ['FULLTEXT']; + } elseif (strpos($v['index_type'], 'SPATIAL') !== false) { + $v['flags'] = ['SPATIAL']; + } + + // Ignore prohibited prefix `length` for spatial index + if (strpos($v['index_type'], 'SPATIAL') === false) { + $v['length'] = isset($v['sub_part']) ? (int) $v['sub_part'] : null; + } + + $tableIndexes[$k] = $v; + } + + return parent::_getPortableTableIndexesList($tableIndexes, $tableName); + } + + /** + * {@inheritDoc} + */ + protected function _getPortableDatabaseDefinition($database) + { + return $database['Database']; + } + + /** + * {@inheritDoc} + */ + protected function _getPortableTableColumnDefinition($tableColumn) + { + $tableColumn = array_change_key_case($tableColumn, CASE_LOWER); + + $dbType = strtolower($tableColumn['type']); + $dbType = strtok($dbType, '(), '); + assert(is_string($dbType)); + + $length = $tableColumn['length'] ?? strtok('(), '); + + $fixed = null; + + if (! isset($tableColumn['name'])) { + $tableColumn['name'] = ''; + } + + $scale = null; + $precision = null; + + $type = $origType = $this->_platform->getDoctrineTypeMapping($dbType); + + // In cases where not connected to a database DESCRIBE $table does not return 'Comment' + if (isset($tableColumn['comment'])) { + $type = $this->extractDoctrineTypeFromComment($tableColumn['comment'], $type); + $tableColumn['comment'] = $this->removeDoctrineTypeFromComment($tableColumn['comment'], $type); + } + + switch ($dbType) { + case 'char': + case 'binary': + $fixed = true; + break; + + case 'float': + case 'double': + case 'real': + case 'numeric': + case 'decimal': + if ( + preg_match( + '([A-Za-z]+\(([0-9]+),([0-9]+)\))', + $tableColumn['type'], + $match, + ) === 1 + ) { + $precision = $match[1]; + $scale = $match[2]; + $length = null; + } + + break; + + case 'tinytext': + $length = AbstractMySQLPlatform::LENGTH_LIMIT_TINYTEXT; + break; + + case 'text': + $length = AbstractMySQLPlatform::LENGTH_LIMIT_TEXT; + break; + + case 'mediumtext': + $length = AbstractMySQLPlatform::LENGTH_LIMIT_MEDIUMTEXT; + break; + + case 'tinyblob': + $length = AbstractMySQLPlatform::LENGTH_LIMIT_TINYBLOB; + break; + + case 'blob': + $length = AbstractMySQLPlatform::LENGTH_LIMIT_BLOB; + break; + + case 'mediumblob': + $length = AbstractMySQLPlatform::LENGTH_LIMIT_MEDIUMBLOB; + break; + + case 'tinyint': + case 'smallint': + case 'mediumint': + case 'int': + case 'integer': + case 'bigint': + case 'year': + $length = null; + break; + } + + if ($this->_platform instanceof MariaDb1027Platform) { + $columnDefault = $this->getMariaDb1027ColumnDefault($this->_platform, $tableColumn['default']); + } else { + $columnDefault = $tableColumn['default']; + } + + $options = [ + 'length' => $length !== null ? (int) $length : null, + 'unsigned' => strpos($tableColumn['type'], 'unsigned') !== false, + 'fixed' => (bool) $fixed, + 'default' => $columnDefault, + 'notnull' => $tableColumn['null'] !== 'YES', + 'scale' => null, + 'precision' => null, + 'autoincrement' => strpos($tableColumn['extra'], 'auto_increment') !== false, + 'comment' => isset($tableColumn['comment']) && $tableColumn['comment'] !== '' + ? $tableColumn['comment'] + : null, + ]; + + if ($scale !== null && $precision !== null) { + $options['scale'] = (int) $scale; + $options['precision'] = (int) $precision; + } + + $column = new Column($tableColumn['field'], Type::getType($type), $options); + + if (isset($tableColumn['characterset'])) { + $column->setPlatformOption('charset', $tableColumn['characterset']); + } + + if (isset($tableColumn['collation'])) { + $column->setPlatformOption('collation', $tableColumn['collation']); + } + + if (isset($tableColumn['declarationMismatch'])) { + $column->setPlatformOption('declarationMismatch', $tableColumn['declarationMismatch']); + } + + // Check underlying database type where doctrine type is inferred from DC2Type comment + // and set a flag if it is not as expected. + if ($type === 'json' && $origType !== $type && $this->expectedDbType($type, $options) !== $dbType) { + $column->setPlatformOption('declarationMismatch', true); + } + + return $column; + } + + /** + * Returns the database data type for a given doctrine type and column + * + * Note that for data types that depend on length where length is not part of the column definition + * and therefore the $tableColumn['length'] will not be set, for example TEXT (which could be LONGTEXT, + * MEDIUMTEXT) or BLOB (LONGBLOB or TINYBLOB), the expectedDbType cannot be inferred exactly, merely + * the default type. + * + * This method is intended to be used to determine underlying database type where doctrine type is + * inferred from a DC2Type comment. + * + * @param mixed[] $tableColumn + */ + private function expectedDbType(string $type, array $tableColumn): string + { + $_type = Type::getType($type); + $expectedDbType = strtolower($_type->getSQLDeclaration($tableColumn, $this->_platform)); + $expectedDbType = strtok($expectedDbType, '(), '); + + return $expectedDbType === false ? '' : $expectedDbType; + } + + /** + * Return Doctrine/Mysql-compatible column default values for MariaDB 10.2.7+ servers. + * + * - Since MariaDb 10.2.7 column defaults stored in information_schema are now quoted + * to distinguish them from expressions (see MDEV-10134). + * - CURRENT_TIMESTAMP, CURRENT_TIME, CURRENT_DATE are stored in information_schema + * as current_timestamp(), currdate(), currtime() + * - Quoted 'NULL' is not enforced by Maria, it is technically possible to have + * null in some circumstances (see https://jira.mariadb.org/browse/MDEV-14053) + * - \' is always stored as '' in information_schema (normalized) + * + * @link https://mariadb.com/kb/en/library/information-schema-columns-table/ + * @link https://jira.mariadb.org/browse/MDEV-13132 + * + * @param string|null $columnDefault default value as stored in information_schema for MariaDB >= 10.2.7 + */ + private function getMariaDb1027ColumnDefault(MariaDb1027Platform $platform, ?string $columnDefault): ?string + { + if ($columnDefault === 'NULL' || $columnDefault === null) { + return null; + } + + if (preg_match('/^\'(.*)\'$/', $columnDefault, $matches) === 1) { + return strtr($matches[1], self::MARIADB_ESCAPE_SEQUENCES); + } + + switch ($columnDefault) { + case 'current_timestamp()': + return $platform->getCurrentTimestampSQL(); + + case 'curdate()': + return $platform->getCurrentDateSQL(); + + case 'curtime()': + return $platform->getCurrentTimeSQL(); + } + + return $columnDefault; + } + + /** + * {@inheritDoc} + */ + protected function _getPortableTableForeignKeysList($tableForeignKeys) + { + $list = []; + foreach ($tableForeignKeys as $value) { + $value = array_change_key_case($value, CASE_LOWER); + if (! isset($list[$value['constraint_name']])) { + if (! isset($value['delete_rule']) || $value['delete_rule'] === 'RESTRICT') { + $value['delete_rule'] = null; + } + + if (! isset($value['update_rule']) || $value['update_rule'] === 'RESTRICT') { + $value['update_rule'] = null; + } + + $list[$value['constraint_name']] = [ + 'name' => $value['constraint_name'], + 'local' => [], + 'foreign' => [], + 'foreignTable' => $value['referenced_table_name'], + 'onDelete' => $value['delete_rule'], + 'onUpdate' => $value['update_rule'], + ]; + } + + $list[$value['constraint_name']]['local'][] = $value['column_name']; + $list[$value['constraint_name']]['foreign'][] = $value['referenced_column_name']; + } + + return parent::_getPortableTableForeignKeysList($list); + } + + /** + * {@inheritDoc} + */ + protected function _getPortableTableForeignKeyDefinition($tableForeignKey): ForeignKeyConstraint + { + return new ForeignKeyConstraint( + $tableForeignKey['local'], + $tableForeignKey['foreignTable'], + $tableForeignKey['foreign'], + $tableForeignKey['name'], + [ + 'onDelete' => $tableForeignKey['onDelete'], + 'onUpdate' => $tableForeignKey['onUpdate'], + ], + ); + } + + public function createComparator(): Comparator + { + return new MySQL\Comparator( + $this->_platform, + new CachingCollationMetadataProvider( + new ConnectionCollationMetadataProvider($this->_conn), + ), + ); + } + + protected function selectTableNames(string $databaseName): Result + { + $sql = <<<'SQL' +SELECT TABLE_NAME +FROM information_schema.TABLES +WHERE TABLE_SCHEMA = ? + AND TABLE_TYPE = 'BASE TABLE' +ORDER BY TABLE_NAME +SQL; + + return $this->_conn->executeQuery($sql, [$databaseName]); + } + + protected function selectTableColumns(string $databaseName, ?string $tableName = null): Result + { + // @todo 4.0 - call getColumnTypeSQLSnippet() instead + [$columnTypeSQL, $joinCheckConstraintSQL] = $this->_platform->getColumnTypeSQLSnippets('c', $databaseName); + + $sql = 'SELECT'; + + if ($tableName === null) { + $sql .= ' c.TABLE_NAME,'; + } + + $sql .= <<_conn->executeQuery($sql, $params); + } + + protected function selectIndexColumns(string $databaseName, ?string $tableName = null): Result + { + $sql = 'SELECT'; + + if ($tableName === null) { + $sql .= ' TABLE_NAME,'; + } + + $sql .= <<<'SQL' + NON_UNIQUE AS Non_Unique, + INDEX_NAME AS Key_name, + COLUMN_NAME AS Column_Name, + SUB_PART AS Sub_Part, + INDEX_TYPE AS Index_Type +FROM information_schema.STATISTICS +SQL; + + $conditions = ['TABLE_SCHEMA = ?']; + $params = [$databaseName]; + + if ($tableName !== null) { + $conditions[] = 'TABLE_NAME = ?'; + $params[] = $tableName; + } + + $sql .= ' WHERE ' . implode(' AND ', $conditions) . ' ORDER BY SEQ_IN_INDEX'; + + return $this->_conn->executeQuery($sql, $params); + } + + protected function selectForeignKeyColumns(string $databaseName, ?string $tableName = null): Result + { + $sql = 'SELECT DISTINCT'; + + if ($tableName === null) { + $sql .= ' k.TABLE_NAME,'; + } + + $sql .= <<<'SQL' + k.CONSTRAINT_NAME, + k.COLUMN_NAME, + k.REFERENCED_TABLE_NAME, + k.REFERENCED_COLUMN_NAME, + k.ORDINAL_POSITION /*!50116, + c.UPDATE_RULE, + c.DELETE_RULE */ +FROM information_schema.key_column_usage k /*!50116 +INNER JOIN information_schema.referential_constraints c +ON c.CONSTRAINT_NAME = k.CONSTRAINT_NAME +AND c.TABLE_NAME = k.TABLE_NAME */ +SQL; + + $conditions = ['k.TABLE_SCHEMA = ?']; + $params = [$databaseName]; + + if ($tableName !== null) { + $conditions[] = 'k.TABLE_NAME = ?'; + $params[] = $tableName; + } + + $conditions[] = 'k.REFERENCED_COLUMN_NAME IS NOT NULL'; + + $sql .= ' WHERE ' . implode(' AND ', $conditions) + // The schema name is passed multiple times in the WHERE clause instead of using a JOIN condition + // in order to avoid performance issues on MySQL older than 8.0 and the corresponding MariaDB versions + // caused by https://bugs.mysql.com/bug.php?id=81347. + // Use a string literal for the database name since the internal PDO SQL parser + // cannot recognize parameter placeholders inside conditional comments + . ' /*!50116 AND c.CONSTRAINT_SCHEMA = ' . $this->_conn->quote($databaseName) . ' */' + . ' ORDER BY k.ORDINAL_POSITION'; + + return $this->_conn->executeQuery($sql, $params); + } + + /** + * {@inheritDoc} + */ + protected function fetchTableOptionsByTable(string $databaseName, ?string $tableName = null): array + { + $sql = $this->_platform->fetchTableOptionsByTable($tableName !== null); + + $params = [$databaseName]; + if ($tableName !== null) { + $params[] = $tableName; + } + + /** @var array> $metadata */ + $metadata = $this->_conn->executeQuery($sql, $params) + ->fetchAllAssociativeIndexed(); + + $tableOptions = []; + foreach ($metadata as $table => $data) { + $data = array_change_key_case($data, CASE_LOWER); + + $tableOptions[$table] = [ + 'engine' => $data['engine'], + 'collation' => $data['table_collation'], + 'charset' => $data['character_set_name'], + 'autoincrement' => $data['auto_increment'], + 'comment' => $data['table_comment'], + 'create_options' => $this->parseCreateOptions($data['create_options']), + ]; + } + + return $tableOptions; + } + + /** @return string[]|true[] */ + private function parseCreateOptions(?string $string): array + { + $options = []; + + if ($string === null || $string === '') { + return $options; + } + + foreach (explode(' ', $string) as $pair) { + $parts = explode('=', $pair, 2); + + $options[$parts[0]] = $parts[1] ?? true; + } + + return $options; + } +} diff --git a/3rdparty/doctrine/dbal/src/Schema/OracleSchemaManager.php b/3rdparty/doctrine/dbal/src/Schema/OracleSchemaManager.php new file mode 100644 index 00000000..3608e056 --- /dev/null +++ b/3rdparty/doctrine/dbal/src/Schema/OracleSchemaManager.php @@ -0,0 +1,537 @@ + + */ +class OracleSchemaManager extends AbstractSchemaManager +{ + /** + * {@inheritDoc} + */ + public function listTableNames() + { + return $this->doListTableNames(); + } + + /** + * {@inheritDoc} + */ + public function listTables() + { + return $this->doListTables(); + } + + /** + * {@inheritDoc} + * + * @deprecated Use {@see introspectTable()} instead. + */ + public function listTableDetails($name) + { + Deprecation::triggerIfCalledFromOutside( + 'doctrine/dbal', + 'https://github.com/doctrine/dbal/pull/5595', + '%s is deprecated. Use introspectTable() instead.', + __METHOD__, + ); + + return $this->doListTableDetails($name); + } + + /** + * {@inheritDoc} + */ + public function listTableColumns($table, $database = null) + { + return $this->doListTableColumns($table, $database); + } + + /** + * {@inheritDoc} + */ + public function listTableIndexes($table) + { + return $this->doListTableIndexes($table); + } + + /** + * {@inheritDoc} + */ + public function listTableForeignKeys($table, $database = null) + { + return $this->doListTableForeignKeys($table, $database); + } + + /** + * {@inheritDoc} + */ + protected function _getPortableViewDefinition($view) + { + $view = array_change_key_case($view, CASE_LOWER); + + return new View($this->getQuotedIdentifierName($view['view_name']), $view['text']); + } + + /** + * {@inheritDoc} + */ + protected function _getPortableTableDefinition($table) + { + $table = array_change_key_case($table, CASE_LOWER); + + return $this->getQuotedIdentifierName($table['table_name']); + } + + /** + * {@inheritDoc} + */ + protected function _getPortableTableIndexesList($tableIndexes, $tableName = null) + { + $indexBuffer = []; + foreach ($tableIndexes as $tableIndex) { + $tableIndex = array_change_key_case($tableIndex, CASE_LOWER); + + $keyName = strtolower($tableIndex['name']); + $buffer = []; + + if ($tableIndex['is_primary'] === 'P') { + $keyName = 'primary'; + $buffer['primary'] = true; + $buffer['non_unique'] = false; + } else { + $buffer['primary'] = false; + $buffer['non_unique'] = ! $tableIndex['is_unique']; + } + + $buffer['key_name'] = $keyName; + $buffer['column_name'] = $this->getQuotedIdentifierName($tableIndex['column_name']); + $indexBuffer[] = $buffer; + } + + return parent::_getPortableTableIndexesList($indexBuffer, $tableName); + } + + /** + * {@inheritDoc} + */ + protected function _getPortableTableColumnDefinition($tableColumn) + { + $tableColumn = array_change_key_case($tableColumn, CASE_LOWER); + + $dbType = strtolower($tableColumn['data_type']); + if (strpos($dbType, 'timestamp(') === 0) { + if (strpos($dbType, 'with time zone') !== false) { + $dbType = 'timestamptz'; + } else { + $dbType = 'timestamp'; + } + } + + $unsigned = $fixed = $precision = $scale = $length = null; + + if (! isset($tableColumn['column_name'])) { + $tableColumn['column_name'] = ''; + } + + // Default values returned from database sometimes have trailing spaces. + if (is_string($tableColumn['data_default'])) { + $tableColumn['data_default'] = trim($tableColumn['data_default']); + } + + if ($tableColumn['data_default'] === '' || $tableColumn['data_default'] === 'NULL') { + $tableColumn['data_default'] = null; + } + + if ($tableColumn['data_default'] !== null) { + // Default values returned from database are represented as literal expressions + if (preg_match('/^\'(.*)\'$/s', $tableColumn['data_default'], $matches) === 1) { + $tableColumn['data_default'] = str_replace("''", "'", $matches[1]); + } + } + + if ($tableColumn['data_precision'] !== null) { + $precision = (int) $tableColumn['data_precision']; + } + + if ($tableColumn['data_scale'] !== null) { + $scale = (int) $tableColumn['data_scale']; + } + + $type = $this->_platform->getDoctrineTypeMapping($dbType); + $type = $this->extractDoctrineTypeFromComment($tableColumn['comments'], $type); + $tableColumn['comments'] = $this->removeDoctrineTypeFromComment($tableColumn['comments'], $type); + + switch ($dbType) { + case 'number': + if ($precision === 20 && $scale === 0) { + $type = 'bigint'; + } elseif ($precision === 5 && $scale === 0) { + $type = 'smallint'; + } elseif ($precision === 1 && $scale === 0) { + $type = 'boolean'; + } elseif ($scale > 0) { + $type = 'decimal'; + } + + break; + + case 'varchar': + case 'varchar2': + case 'nvarchar2': + $length = $tableColumn['char_length']; + $fixed = false; + break; + + case 'raw': + $length = $tableColumn['data_length']; + $fixed = true; + break; + + case 'char': + case 'nchar': + $length = $tableColumn['char_length']; + $fixed = true; + break; + } + + $options = [ + 'notnull' => $tableColumn['nullable'] === 'N', + 'fixed' => (bool) $fixed, + 'unsigned' => (bool) $unsigned, + 'default' => $tableColumn['data_default'], + 'length' => $length, + 'precision' => $precision, + 'scale' => $scale, + 'comment' => isset($tableColumn['comments']) && $tableColumn['comments'] !== '' + ? $tableColumn['comments'] + : null, + ]; + + return new Column($this->getQuotedIdentifierName($tableColumn['column_name']), Type::getType($type), $options); + } + + /** + * {@inheritDoc} + */ + protected function _getPortableTableForeignKeysList($tableForeignKeys) + { + $list = []; + foreach ($tableForeignKeys as $value) { + $value = array_change_key_case($value, CASE_LOWER); + if (! isset($list[$value['constraint_name']])) { + if ($value['delete_rule'] === 'NO ACTION') { + $value['delete_rule'] = null; + } + + $list[$value['constraint_name']] = [ + 'name' => $this->getQuotedIdentifierName($value['constraint_name']), + 'local' => [], + 'foreign' => [], + 'foreignTable' => $value['references_table'], + 'onDelete' => $value['delete_rule'], + ]; + } + + $localColumn = $this->getQuotedIdentifierName($value['local_column']); + $foreignColumn = $this->getQuotedIdentifierName($value['foreign_column']); + + $list[$value['constraint_name']]['local'][$value['position']] = $localColumn; + $list[$value['constraint_name']]['foreign'][$value['position']] = $foreignColumn; + } + + return parent::_getPortableTableForeignKeysList($list); + } + + /** + * {@inheritDoc} + */ + protected function _getPortableTableForeignKeyDefinition($tableForeignKey): ForeignKeyConstraint + { + return new ForeignKeyConstraint( + array_values($tableForeignKey['local']), + $this->getQuotedIdentifierName($tableForeignKey['foreignTable']), + array_values($tableForeignKey['foreign']), + $this->getQuotedIdentifierName($tableForeignKey['name']), + ['onDelete' => $tableForeignKey['onDelete']], + ); + } + + /** + * {@inheritDoc} + */ + protected function _getPortableSequenceDefinition($sequence) + { + $sequence = array_change_key_case($sequence, CASE_LOWER); + + return new Sequence( + $this->getQuotedIdentifierName($sequence['sequence_name']), + (int) $sequence['increment_by'], + (int) $sequence['min_value'], + ); + } + + /** + * {@inheritDoc} + */ + protected function _getPortableDatabaseDefinition($database) + { + $database = array_change_key_case($database, CASE_LOWER); + + return $database['username']; + } + + /** + * {@inheritDoc} + */ + public function createDatabase($database) + { + $statement = $this->_platform->getCreateDatabaseSQL($database); + + $params = $this->_conn->getParams(); + + if (isset($params['password'])) { + $statement .= ' IDENTIFIED BY ' . $params['password']; + } + + $this->_conn->executeStatement($statement); + + $statement = 'GRANT DBA TO ' . $database; + $this->_conn->executeStatement($statement); + } + + /** + * @internal The method should be only used from within the OracleSchemaManager class hierarchy. + * + * @param string $table + * + * @return bool + * + * @throws Exception + */ + public function dropAutoincrement($table) + { + $sql = $this->_platform->getDropAutoincrementSql($table); + foreach ($sql as $query) { + $this->_conn->executeStatement($query); + } + + return true; + } + + /** + * {@inheritDoc} + */ + public function dropTable($name) + { + $this->tryMethod('dropAutoincrement', $name); + + parent::dropTable($name); + } + + /** + * Returns the quoted representation of the given identifier name. + * + * Quotes non-uppercase identifiers explicitly to preserve case + * and thus make references to the particular identifier work. + * + * @param string $identifier The identifier to quote. + */ + private function getQuotedIdentifierName($identifier): string + { + if (preg_match('/[a-z]/', $identifier) === 1) { + return $this->_platform->quoteIdentifier($identifier); + } + + return $identifier; + } + + protected function selectTableNames(string $databaseName): Result + { + $sql = <<<'SQL' +SELECT TABLE_NAME +FROM ALL_TABLES +WHERE OWNER = :OWNER +ORDER BY TABLE_NAME +SQL; + + return $this->_conn->executeQuery($sql, ['OWNER' => $databaseName]); + } + + protected function selectTableColumns(string $databaseName, ?string $tableName = null): Result + { + $sql = 'SELECT'; + + if ($tableName === null) { + $sql .= ' C.TABLE_NAME,'; + } + + $sql .= <<<'SQL' + C.COLUMN_NAME, + C.DATA_TYPE, + C.DATA_DEFAULT, + C.DATA_PRECISION, + C.DATA_SCALE, + C.CHAR_LENGTH, + C.DATA_LENGTH, + C.NULLABLE, + D.COMMENTS + FROM ALL_TAB_COLUMNS C + INNER JOIN ALL_TABLES T + ON T.OWNER = C.OWNER + AND T.TABLE_NAME = C.TABLE_NAME + LEFT JOIN ALL_COL_COMMENTS D + ON D.OWNER = C.OWNER + AND D.TABLE_NAME = C.TABLE_NAME + AND D.COLUMN_NAME = C.COLUMN_NAME +SQL; + + $conditions = ['C.OWNER = :OWNER']; + $params = ['OWNER' => $databaseName]; + + if ($tableName !== null) { + $conditions[] = 'C.TABLE_NAME = :TABLE_NAME'; + $params['TABLE_NAME'] = $tableName; + } + + $sql .= ' WHERE ' . implode(' AND ', $conditions) . ' ORDER BY C.COLUMN_ID'; + + return $this->_conn->executeQuery($sql, $params); + } + + protected function selectIndexColumns(string $databaseName, ?string $tableName = null): Result + { + $sql = 'SELECT'; + + if ($tableName === null) { + $sql .= ' IND_COL.TABLE_NAME,'; + } + + $sql .= <<<'SQL' + IND_COL.INDEX_NAME AS NAME, + IND.INDEX_TYPE AS TYPE, + DECODE(IND.UNIQUENESS, 'NONUNIQUE', 0, 'UNIQUE', 1) AS IS_UNIQUE, + IND_COL.COLUMN_NAME, + IND_COL.COLUMN_POSITION AS COLUMN_POS, + CON.CONSTRAINT_TYPE AS IS_PRIMARY + FROM ALL_IND_COLUMNS IND_COL + LEFT JOIN ALL_INDEXES IND + ON IND.OWNER = IND_COL.INDEX_OWNER + AND IND.INDEX_NAME = IND_COL.INDEX_NAME + LEFT JOIN ALL_CONSTRAINTS CON + ON CON.OWNER = IND_COL.INDEX_OWNER + AND CON.INDEX_NAME = IND_COL.INDEX_NAME +SQL; + + $conditions = ['IND_COL.INDEX_OWNER = :OWNER']; + $params = ['OWNER' => $databaseName]; + + if ($tableName !== null) { + $conditions[] = 'IND_COL.TABLE_NAME = :TABLE_NAME'; + $params['TABLE_NAME'] = $tableName; + } + + $sql .= ' WHERE ' . implode(' AND ', $conditions) . ' ORDER BY IND_COL.TABLE_NAME, IND_COL.INDEX_NAME' + . ', IND_COL.COLUMN_POSITION'; + + return $this->_conn->executeQuery($sql, $params); + } + + protected function selectForeignKeyColumns(string $databaseName, ?string $tableName = null): Result + { + $sql = 'SELECT'; + + if ($tableName === null) { + $sql .= ' COLS.TABLE_NAME,'; + } + + $sql .= <<<'SQL' + ALC.CONSTRAINT_NAME, + ALC.DELETE_RULE, + COLS.COLUMN_NAME LOCAL_COLUMN, + COLS.POSITION, + R_COLS.TABLE_NAME REFERENCES_TABLE, + R_COLS.COLUMN_NAME FOREIGN_COLUMN + FROM ALL_CONS_COLUMNS COLS + LEFT JOIN ALL_CONSTRAINTS ALC ON ALC.OWNER = COLS.OWNER AND ALC.CONSTRAINT_NAME = COLS.CONSTRAINT_NAME + LEFT JOIN ALL_CONS_COLUMNS R_COLS ON R_COLS.OWNER = ALC.R_OWNER AND + R_COLS.CONSTRAINT_NAME = ALC.R_CONSTRAINT_NAME AND + R_COLS.POSITION = COLS.POSITION +SQL; + + $conditions = ["ALC.CONSTRAINT_TYPE = 'R'", 'COLS.OWNER = :OWNER']; + $params = ['OWNER' => $databaseName]; + + if ($tableName !== null) { + $conditions[] = 'COLS.TABLE_NAME = :TABLE_NAME'; + $params['TABLE_NAME'] = $tableName; + } + + $sql .= ' WHERE ' . implode(' AND ', $conditions) . ' ORDER BY COLS.TABLE_NAME, COLS.CONSTRAINT_NAME' + . ', COLS.POSITION'; + + return $this->_conn->executeQuery($sql, $params); + } + + /** + * {@inheritDoc} + */ + protected function fetchTableOptionsByTable(string $databaseName, ?string $tableName = null): array + { + $sql = 'SELECT TABLE_NAME, COMMENTS'; + + $conditions = ['OWNER = :OWNER']; + $params = ['OWNER' => $databaseName]; + + if ($tableName !== null) { + $conditions[] = 'TABLE_NAME = :TABLE_NAME'; + $params['TABLE_NAME'] = $tableName; + } + + $sql .= ' FROM ALL_TAB_COMMENTS WHERE ' . implode(' AND ', $conditions); + + /** @var array> $metadata */ + $metadata = $this->_conn->executeQuery($sql, $params) + ->fetchAllAssociativeIndexed(); + + $tableOptions = []; + foreach ($metadata as $table => $data) { + $data = array_change_key_case($data, CASE_LOWER); + + $tableOptions[$table] = [ + 'comment' => $data['comments'], + ]; + } + + return $tableOptions; + } + + protected function normalizeName(string $name): string + { + $identifier = new Identifier($name); + + return $identifier->isQuoted() ? $identifier->getName() : strtoupper($name); + } +} diff --git a/3rdparty/doctrine/dbal/src/Schema/PostgreSQLSchemaManager.php b/3rdparty/doctrine/dbal/src/Schema/PostgreSQLSchemaManager.php new file mode 100644 index 00000000..0510d1a8 --- /dev/null +++ b/3rdparty/doctrine/dbal/src/Schema/PostgreSQLSchemaManager.php @@ -0,0 +1,784 @@ + + */ +class PostgreSQLSchemaManager extends AbstractSchemaManager +{ + /** @var string[]|null */ + private ?array $existingSchemaPaths = null; + + /** + * {@inheritDoc} + */ + public function listTableNames() + { + return $this->doListTableNames(); + } + + /** + * {@inheritDoc} + */ + public function listTables() + { + return $this->doListTables(); + } + + /** + * {@inheritDoc} + * + * @deprecated Use {@see introspectTable()} instead. + */ + public function listTableDetails($name) + { + Deprecation::triggerIfCalledFromOutside( + 'doctrine/dbal', + 'https://github.com/doctrine/dbal/pull/5595', + '%s is deprecated. Use introspectTable() instead.', + __METHOD__, + ); + + return $this->doListTableDetails($name); + } + + /** + * {@inheritDoc} + */ + public function listTableColumns($table, $database = null) + { + return $this->doListTableColumns($table, $database); + } + + /** + * {@inheritDoc} + */ + public function listTableIndexes($table) + { + return $this->doListTableIndexes($table); + } + + /** + * {@inheritDoc} + */ + public function listTableForeignKeys($table, $database = null) + { + return $this->doListTableForeignKeys($table, $database); + } + + /** + * Gets all the existing schema names. + * + * @deprecated Use {@see listSchemaNames()} instead. + * + * @return string[] + * + * @throws Exception + */ + public function getSchemaNames() + { + Deprecation::trigger( + 'doctrine/dbal', + 'https://github.com/doctrine/dbal/issues/4503', + 'PostgreSQLSchemaManager::getSchemaNames() is deprecated,' + . ' use PostgreSQLSchemaManager::listSchemaNames() instead.', + ); + + return $this->listNamespaceNames(); + } + + /** + * {@inheritDoc} + */ + public function listSchemaNames(): array + { + return $this->_conn->fetchFirstColumn( + <<<'SQL' +SELECT schema_name +FROM information_schema.schemata +WHERE schema_name NOT LIKE 'pg\_%' +AND schema_name != 'information_schema' +SQL, + ); + } + + /** + * {@inheritDoc} + * + * @deprecated + */ + public function getSchemaSearchPaths() + { + Deprecation::triggerIfCalledFromOutside( + 'doctrine/dbal', + 'https://github.com/doctrine/dbal/pull/4821', + 'PostgreSQLSchemaManager::getSchemaSearchPaths() is deprecated.', + ); + + $params = $this->_conn->getParams(); + + $searchPaths = $this->_conn->fetchOne('SHOW search_path'); + assert($searchPaths !== false); + + $schema = explode(',', $searchPaths); + + if (isset($params['user'])) { + $schema = str_replace('"$user"', $params['user'], $schema); + } + + return array_map('trim', $schema); + } + + /** + * Gets names of all existing schemas in the current users search path. + * + * This is a PostgreSQL only function. + * + * @internal The method should be only used from within the PostgreSQLSchemaManager class hierarchy. + * + * @return string[] + * + * @throws Exception + */ + public function getExistingSchemaSearchPaths() + { + if ($this->existingSchemaPaths === null) { + $this->determineExistingSchemaSearchPaths(); + } + + assert($this->existingSchemaPaths !== null); + + return $this->existingSchemaPaths; + } + + /** + * Returns the name of the current schema. + * + * @return string|null + * + * @throws Exception + */ + protected function getCurrentSchema() + { + $schemas = $this->getExistingSchemaSearchPaths(); + + return array_shift($schemas); + } + + /** + * Sets or resets the order of the existing schemas in the current search path of the user. + * + * This is a PostgreSQL only function. + * + * @internal The method should be only used from within the PostgreSQLSchemaManager class hierarchy. + * + * @return void + * + * @throws Exception + */ + public function determineExistingSchemaSearchPaths() + { + $names = $this->listSchemaNames(); + $paths = $this->getSchemaSearchPaths(); + + $this->existingSchemaPaths = array_filter($paths, static function ($v) use ($names): bool { + return in_array($v, $names, true); + }); + } + + /** + * {@inheritDoc} + */ + protected function _getPortableTableForeignKeyDefinition($tableForeignKey) + { + $onUpdate = null; + $onDelete = null; + + if ( + preg_match( + '(ON UPDATE ([a-zA-Z0-9]+( (NULL|ACTION|DEFAULT))?))', + $tableForeignKey['condef'], + $match, + ) === 1 + ) { + $onUpdate = $match[1]; + } + + if ( + preg_match( + '(ON DELETE ([a-zA-Z0-9]+( (NULL|ACTION|DEFAULT))?))', + $tableForeignKey['condef'], + $match, + ) === 1 + ) { + $onDelete = $match[1]; + } + + $result = preg_match('/FOREIGN KEY \((.+)\) REFERENCES (.+)\((.+)\)/', $tableForeignKey['condef'], $values); + assert($result === 1); + + // PostgreSQL returns identifiers that are keywords with quotes, we need them later, don't get + // the idea to trim them here. + $localColumns = array_map('trim', explode(',', $values[1])); + $foreignColumns = array_map('trim', explode(',', $values[3])); + $foreignTable = $values[2]; + + return new ForeignKeyConstraint( + $localColumns, + $foreignTable, + $foreignColumns, + $tableForeignKey['conname'], + ['onUpdate' => $onUpdate, 'onDelete' => $onDelete], + ); + } + + /** + * {@inheritDoc} + */ + protected function _getPortableViewDefinition($view) + { + return new View($view['schemaname'] . '.' . $view['viewname'], $view['definition']); + } + + /** + * {@inheritDoc} + */ + protected function _getPortableTableDefinition($table) + { + $currentSchema = $this->getCurrentSchema(); + + if ($table['schema_name'] === $currentSchema) { + return $table['table_name']; + } + + return $table['schema_name'] . '.' . $table['table_name']; + } + + /** + * {@inheritDoc} + */ + protected function _getPortableTableIndexesList($tableIndexes, $tableName = null) + { + $buffer = []; + foreach ($tableIndexes as $row) { + $colNumbers = array_map('intval', explode(' ', $row['indkey'])); + $columnNameSql = sprintf( + <<<'SQL' + SELECT attnum, + quote_ident(attname) AS attname + FROM pg_attribute + WHERE attrelid = %d + AND attnum IN (%s) + ORDER BY attnum + SQL, + $row['indrelid'], + implode(', ', $colNumbers), + ); + + $indexColumns = $this->_conn->fetchAllAssociative($columnNameSql); + + // required for getting the order of the columns right. + foreach ($colNumbers as $colNum) { + foreach ($indexColumns as $colRow) { + if ($colNum !== $colRow['attnum']) { + continue; + } + + $buffer[] = [ + 'key_name' => $row['relname'], + 'column_name' => trim($colRow['attname']), + 'non_unique' => ! $row['indisunique'], + 'primary' => $row['indisprimary'], + 'where' => $row['where'], + ]; + } + } + } + + return parent::_getPortableTableIndexesList($buffer, $tableName); + } + + /** + * {@inheritDoc} + */ + protected function _getPortableDatabaseDefinition($database) + { + return $database['datname']; + } + + /** + * {@inheritDoc} + * + * @deprecated Use {@see listSchemaNames()} instead. + */ + protected function getPortableNamespaceDefinition(array $namespace) + { + Deprecation::triggerIfCalledFromOutside( + 'doctrine/dbal', + 'https://github.com/doctrine/dbal/issues/4503', + 'PostgreSQLSchemaManager::getPortableNamespaceDefinition() is deprecated,' + . ' use PostgreSQLSchemaManager::listSchemaNames() instead.', + ); + + return $namespace['nspname']; + } + + /** + * {@inheritDoc} + */ + protected function _getPortableSequenceDefinition($sequence) + { + if ($sequence['schemaname'] !== 'public') { + $sequenceName = $sequence['schemaname'] . '.' . $sequence['relname']; + } else { + $sequenceName = $sequence['relname']; + } + + return new Sequence($sequenceName, (int) $sequence['increment_by'], (int) $sequence['min_value']); + } + + /** + * {@inheritDoc} + */ + protected function _getPortableTableColumnDefinition($tableColumn) + { + $tableColumn = array_change_key_case($tableColumn, CASE_LOWER); + + if (strtolower($tableColumn['type']) === 'varchar' || strtolower($tableColumn['type']) === 'bpchar') { + // get length from varchar definition + $length = preg_replace('~.*\(([0-9]*)\).*~', '$1', $tableColumn['complete_type']); + $tableColumn['length'] = $length; + } + + $matches = []; + + $autoincrement = false; + + if ( + $tableColumn['default'] !== null + && preg_match("/^nextval\('(.*)'(::.*)?\)$/", $tableColumn['default'], $matches) === 1 + ) { + $tableColumn['sequence'] = $matches[1]; + $tableColumn['default'] = null; + $autoincrement = true; + } + + if ($tableColumn['default'] !== null) { + if (preg_match("/^['(](.*)[')]::/", $tableColumn['default'], $matches) === 1) { + $tableColumn['default'] = $matches[1]; + } elseif (preg_match('/^NULL::/', $tableColumn['default']) === 1) { + $tableColumn['default'] = null; + } + } + + $length = $tableColumn['length'] ?? null; + if ($length === '-1' && isset($tableColumn['atttypmod'])) { + $length = $tableColumn['atttypmod'] - 4; + } + + if ((int) $length <= 0) { + $length = null; + } + + $fixed = null; + + if (! isset($tableColumn['name'])) { + $tableColumn['name'] = ''; + } + + $precision = null; + $scale = null; + $jsonb = null; + + $dbType = strtolower($tableColumn['type']); + if ( + $tableColumn['domain_type'] !== null + && $tableColumn['domain_type'] !== '' + && ! $this->_platform->hasDoctrineTypeMappingFor($tableColumn['type']) + ) { + $dbType = strtolower($tableColumn['domain_type']); + $tableColumn['complete_type'] = $tableColumn['domain_complete_type']; + } + + $type = $this->_platform->getDoctrineTypeMapping($dbType); + $type = $this->extractDoctrineTypeFromComment($tableColumn['comment'], $type); + $tableColumn['comment'] = $this->removeDoctrineTypeFromComment($tableColumn['comment'], $type); + + switch ($dbType) { + case 'smallint': + case 'int2': + $tableColumn['default'] = $this->fixVersion94NegativeNumericDefaultValue($tableColumn['default']); + $length = null; + break; + + case 'int': + case 'int4': + case 'integer': + $tableColumn['default'] = $this->fixVersion94NegativeNumericDefaultValue($tableColumn['default']); + $length = null; + break; + + case 'bigint': + case 'int8': + $tableColumn['default'] = $this->fixVersion94NegativeNumericDefaultValue($tableColumn['default']); + $length = null; + break; + + case 'bool': + case 'boolean': + if ($tableColumn['default'] === 'true') { + $tableColumn['default'] = true; + } + + if ($tableColumn['default'] === 'false') { + $tableColumn['default'] = false; + } + + $length = null; + break; + + case 'json': + case 'text': + case '_varchar': + case 'varchar': + $tableColumn['default'] = $this->parseDefaultExpression($tableColumn['default']); + $fixed = false; + break; + case 'interval': + $fixed = false; + break; + + case 'char': + case 'bpchar': + $fixed = true; + break; + + case 'float': + case 'float4': + case 'float8': + case 'double': + case 'double precision': + case 'real': + case 'decimal': + case 'money': + case 'numeric': + $tableColumn['default'] = $this->fixVersion94NegativeNumericDefaultValue($tableColumn['default']); + + if ( + preg_match( + '([A-Za-z]+\(([0-9]+),([0-9]+)\))', + $tableColumn['complete_type'], + $match, + ) === 1 + ) { + $precision = $match[1]; + $scale = $match[2]; + $length = null; + } + + break; + + case 'year': + $length = null; + break; + + // PostgreSQL 9.4+ only + case 'jsonb': + $jsonb = true; + break; + } + + if ( + $tableColumn['default'] !== null && preg_match( + "('([^']+)'::)", + $tableColumn['default'], + $match, + ) === 1 + ) { + $tableColumn['default'] = $match[1]; + } + + $options = [ + 'length' => $length, + 'notnull' => (bool) $tableColumn['isnotnull'], + 'default' => $tableColumn['default'], + 'precision' => $precision, + 'scale' => $scale, + 'fixed' => $fixed, + 'autoincrement' => $autoincrement, + 'comment' => isset($tableColumn['comment']) && $tableColumn['comment'] !== '' + ? $tableColumn['comment'] + : null, + ]; + + $column = new Column($tableColumn['field'], Type::getType($type), $options); + + if (! empty($tableColumn['collation'])) { + $column->setPlatformOption('collation', $tableColumn['collation']); + } + + if ($column->getType()->getName() === Types::JSON) { + if (! $column->getType() instanceof JsonType) { + Deprecation::trigger( + 'doctrine/dbal', + 'https://github.com/doctrine/dbal/pull/5049', + <<<'DEPRECATION' + %s not extending %s while being named %s is deprecated, + and will lead to jsonb never to being used in 4.0., + DEPRECATION, + get_class($column->getType()), + JsonType::class, + Types::JSON, + ); + } + + $column->setPlatformOption('jsonb', $jsonb); + } + + return $column; + } + + /** + * PostgreSQL 9.4 puts parentheses around negative numeric default values that need to be stripped eventually. + * + * @param mixed $defaultValue + * + * @return mixed + */ + private function fixVersion94NegativeNumericDefaultValue($defaultValue) + { + if ($defaultValue !== null && strpos($defaultValue, '(') === 0) { + return trim($defaultValue, '()'); + } + + return $defaultValue; + } + + /** + * Parses a default value expression as given by PostgreSQL + */ + private function parseDefaultExpression(?string $default): ?string + { + if ($default === null) { + return $default; + } + + return str_replace("''", "'", $default); + } + + protected function selectTableNames(string $databaseName): Result + { + $sql = <<<'SQL' +SELECT quote_ident(table_name) AS table_name, + table_schema AS schema_name +FROM information_schema.tables +WHERE table_catalog = ? + AND table_schema NOT LIKE 'pg\_%' + AND table_schema != 'information_schema' + AND table_name != 'geometry_columns' + AND table_name != 'spatial_ref_sys' + AND table_type = 'BASE TABLE' +ORDER BY + quote_ident(table_name) +SQL; + + return $this->_conn->executeQuery($sql, [$databaseName]); + } + + protected function selectTableColumns(string $databaseName, ?string $tableName = null): Result + { + $sql = 'SELECT'; + + if ($tableName === null) { + $sql .= ' quote_ident(c.relname) AS table_name, quote_ident(n.nspname) AS schema_name,'; + } + + $sql .= sprintf(<<<'SQL' + a.attnum, + quote_ident(a.attname) AS field, + t.typname AS type, + format_type(a.atttypid, a.atttypmod) AS complete_type, + (SELECT tc.collcollate FROM pg_catalog.pg_collation tc WHERE tc.oid = a.attcollation) AS collation, + (SELECT t1.typname FROM pg_catalog.pg_type t1 WHERE t1.oid = t.typbasetype) AS domain_type, + (SELECT format_type(t2.typbasetype, t2.typtypmod) FROM + pg_catalog.pg_type t2 WHERE t2.typtype = 'd' AND t2.oid = a.atttypid) AS domain_complete_type, + a.attnotnull AS isnotnull, + (SELECT 't' + FROM pg_index + WHERE c.oid = pg_index.indrelid + AND pg_index.indkey[0] = a.attnum + AND pg_index.indisprimary = 't' + ) AS pri, + (%s) AS default, + (SELECT pg_description.description + FROM pg_description WHERE pg_description.objoid = c.oid AND a.attnum = pg_description.objsubid + ) AS comment + FROM pg_attribute a + INNER JOIN pg_class c + ON c.oid = a.attrelid + INNER JOIN pg_type t + ON t.oid = a.atttypid + INNER JOIN pg_namespace n + ON n.oid = c.relnamespace + LEFT JOIN pg_depend d + ON d.objid = c.oid + AND d.deptype = 'e' + AND d.classid = (SELECT oid FROM pg_class WHERE relname = 'pg_class') +SQL, $this->_platform->getDefaultColumnValueSQLSnippet()); + + $conditions = array_merge([ + 'a.attnum > 0', + "c.relkind = 'r'", + 'd.refobjid IS NULL', + ], $this->buildQueryConditions($tableName)); + + $sql .= ' WHERE ' . implode(' AND ', $conditions) . ' ORDER BY a.attnum'; + + return $this->_conn->executeQuery($sql); + } + + protected function selectIndexColumns(string $databaseName, ?string $tableName = null): Result + { + $sql = 'SELECT'; + + if ($tableName === null) { + $sql .= ' quote_ident(tc.relname) AS table_name, quote_ident(tn.nspname) AS schema_name,'; + } + + $sql .= <<<'SQL' + quote_ident(ic.relname) AS relname, + i.indisunique, + i.indisprimary, + i.indkey, + i.indrelid, + pg_get_expr(indpred, indrelid) AS "where" + FROM pg_index i + JOIN pg_class AS tc ON tc.oid = i.indrelid + JOIN pg_namespace tn ON tn.oid = tc.relnamespace + JOIN pg_class AS ic ON ic.oid = i.indexrelid + WHERE ic.oid IN ( + SELECT indexrelid + FROM pg_index i, pg_class c, pg_namespace n +SQL; + + $conditions = array_merge([ + 'c.oid = i.indrelid', + 'c.relnamespace = n.oid', + ], $this->buildQueryConditions($tableName)); + + $sql .= ' WHERE ' . implode(' AND ', $conditions) . ') ORDER BY quote_ident(ic.relname)'; + + return $this->_conn->executeQuery($sql); + } + + protected function selectForeignKeyColumns(string $databaseName, ?string $tableName = null): Result + { + $sql = 'SELECT'; + + if ($tableName === null) { + $sql .= ' quote_ident(tc.relname) AS table_name, quote_ident(tn.nspname) AS schema_name,'; + } + + $sql .= <<<'SQL' + quote_ident(r.conname) as conname, + pg_get_constraintdef(r.oid, true) as condef + FROM pg_constraint r + JOIN pg_class AS tc ON tc.oid = r.conrelid + JOIN pg_namespace tn ON tn.oid = tc.relnamespace + WHERE r.conrelid IN + ( + SELECT c.oid + FROM pg_class c, pg_namespace n +SQL; + + $conditions = array_merge(['n.oid = c.relnamespace'], $this->buildQueryConditions($tableName)); + + $sql .= ' WHERE ' . implode(' AND ', $conditions) . ") AND r.contype = 'f' ORDER BY quote_ident(r.conname)"; + + return $this->_conn->executeQuery($sql); + } + + /** + * {@inheritDoc} + */ + protected function fetchTableOptionsByTable(string $databaseName, ?string $tableName = null): array + { + $sql = <<<'SQL' +SELECT n.nspname AS schema_name, + c.relname AS table_name, + CASE c.relpersistence WHEN 'u' THEN true ELSE false END as unlogged, + obj_description(c.oid, 'pg_class') AS comment +FROM pg_class c + INNER JOIN pg_namespace n + ON n.oid = c.relnamespace +SQL; + + $conditions = array_merge(["c.relkind = 'r'"], $this->buildQueryConditions($tableName)); + + $sql .= ' WHERE ' . implode(' AND ', $conditions); + + $tableOptions = []; + foreach ($this->_conn->iterateAssociative($sql) as $row) { + $tableOptions[$this->_getPortableTableDefinition($row)] = $row; + } + + return $tableOptions; + } + + /** + * @param string|null $tableName + * + * @return list + */ + private function buildQueryConditions($tableName): array + { + $conditions = []; + + if ($tableName !== null) { + if (strpos($tableName, '.') !== false) { + [$schemaName, $tableName] = explode('.', $tableName); + $conditions[] = 'n.nspname = ' . $this->_platform->quoteStringLiteral($schemaName); + } else { + $conditions[] = 'n.nspname = ANY(current_schemas(false))'; + } + + $identifier = new Identifier($tableName); + $conditions[] = 'c.relname = ' . $this->_platform->quoteStringLiteral($identifier->getName()); + } + + $conditions[] = "n.nspname NOT IN ('pg_catalog', 'information_schema', 'pg_toast')"; + + return $conditions; + } +} diff --git a/3rdparty/doctrine/dbal/src/Schema/SQLServerSchemaManager.php b/3rdparty/doctrine/dbal/src/Schema/SQLServerSchemaManager.php new file mode 100644 index 00000000..e82ccded --- /dev/null +++ b/3rdparty/doctrine/dbal/src/Schema/SQLServerSchemaManager.php @@ -0,0 +1,615 @@ + + */ +class SQLServerSchemaManager extends AbstractSchemaManager +{ + private ?string $databaseCollation = null; + + /** + * {@inheritDoc} + */ + public function listTableNames() + { + return $this->doListTableNames(); + } + + /** + * {@inheritDoc} + */ + public function listTables() + { + return $this->doListTables(); + } + + /** + * {@inheritDoc} + * + * @deprecated Use {@see introspectTable()} instead. + */ + public function listTableDetails($name) + { + Deprecation::triggerIfCalledFromOutside( + 'doctrine/dbal', + 'https://github.com/doctrine/dbal/pull/5595', + '%s is deprecated. Use introspectTable() instead.', + __METHOD__, + ); + + return $this->doListTableDetails($name); + } + + /** + * {@inheritDoc} + */ + public function listTableColumns($table, $database = null) + { + return $this->doListTableColumns($table, $database); + } + + /** + * {@inheritDoc} + */ + public function listTableIndexes($table) + { + return $this->doListTableIndexes($table); + } + + /** + * {@inheritDoc} + */ + public function listTableForeignKeys($table, $database = null) + { + return $this->doListTableForeignKeys($table, $database); + } + + /** + * {@inheritDoc} + */ + public function listSchemaNames(): array + { + return $this->_conn->fetchFirstColumn( + <<<'SQL' +SELECT name +FROM sys.schemas +WHERE name NOT IN('guest', 'INFORMATION_SCHEMA', 'sys') +SQL, + ); + } + + /** + * {@inheritDoc} + */ + protected function _getPortableSequenceDefinition($sequence) + { + return new Sequence($sequence['name'], (int) $sequence['increment'], (int) $sequence['start_value']); + } + + /** + * {@inheritDoc} + */ + protected function _getPortableTableColumnDefinition($tableColumn) + { + $dbType = strtok($tableColumn['type'], '(), '); + assert(is_string($dbType)); + + $fixed = null; + $length = (int) $tableColumn['length']; + $default = $tableColumn['default']; + + if (! isset($tableColumn['name'])) { + $tableColumn['name'] = ''; + } + + if ($default !== null) { + $default = $this->parseDefaultExpression($default); + } + + switch ($dbType) { + case 'nchar': + case 'ntext': + // Unicode data requires 2 bytes per character + $length /= 2; + break; + + case 'nvarchar': + if ($length === -1) { + break; + } + + // Unicode data requires 2 bytes per character + $length /= 2; + break; + + case 'varchar': + // TEXT type is returned as VARCHAR(MAX) with a length of -1 + if ($length === -1) { + $dbType = 'text'; + } + + break; + + case 'varbinary': + if ($length === -1) { + $dbType = 'blob'; + } + + break; + } + + if ($dbType === 'char' || $dbType === 'nchar' || $dbType === 'binary') { + $fixed = true; + } + + $type = $this->_platform->getDoctrineTypeMapping($dbType); + $type = $this->extractDoctrineTypeFromComment($tableColumn['comment'], $type); + $tableColumn['comment'] = $this->removeDoctrineTypeFromComment($tableColumn['comment'], $type); + + $options = [ + 'unsigned' => false, + 'fixed' => (bool) $fixed, + 'default' => $default, + 'notnull' => (bool) $tableColumn['notnull'], + 'scale' => $tableColumn['scale'], + 'precision' => $tableColumn['precision'], + 'autoincrement' => (bool) $tableColumn['autoincrement'], + 'comment' => $tableColumn['comment'] !== '' ? $tableColumn['comment'] : null, + ]; + + if ($length !== 0 && ($type === 'text' || $type === 'string' || $type === 'binary')) { + $options['length'] = $length; + } + + $column = new Column($tableColumn['name'], Type::getType($type), $options); + + if (isset($tableColumn['collation']) && $tableColumn['collation'] !== 'NULL') { + $column->setPlatformOption('collation', $tableColumn['collation']); + } + + return $column; + } + + private function parseDefaultExpression(string $value): ?string + { + while (preg_match('/^\((.*)\)$/s', $value, $matches) === 1) { + $value = $matches[1]; + } + + if ($value === 'NULL') { + return null; + } + + if (preg_match('/^\'(.*)\'$/s', $value, $matches) === 1) { + $value = str_replace("''", "'", $matches[1]); + } + + if ($value === 'getdate()') { + return $this->_platform->getCurrentTimestampSQL(); + } + + return $value; + } + + /** + * {@inheritDoc} + */ + protected function _getPortableTableForeignKeysList($tableForeignKeys) + { + $foreignKeys = []; + + foreach ($tableForeignKeys as $tableForeignKey) { + $name = $tableForeignKey['ForeignKey']; + + if (! isset($foreignKeys[$name])) { + $referencedTableName = $tableForeignKey['ReferenceTableName']; + + if ($tableForeignKey['ReferenceSchemaName'] !== 'dbo') { + $referencedTableName = $tableForeignKey['ReferenceSchemaName'] . '.' . $referencedTableName; + } + + $foreignKeys[$name] = [ + 'local_columns' => [$tableForeignKey['ColumnName']], + 'foreign_table' => $referencedTableName, + 'foreign_columns' => [$tableForeignKey['ReferenceColumnName']], + 'name' => $name, + 'options' => [ + 'onUpdate' => str_replace('_', ' ', $tableForeignKey['update_referential_action_desc']), + 'onDelete' => str_replace('_', ' ', $tableForeignKey['delete_referential_action_desc']), + ], + ]; + } else { + $foreignKeys[$name]['local_columns'][] = $tableForeignKey['ColumnName']; + $foreignKeys[$name]['foreign_columns'][] = $tableForeignKey['ReferenceColumnName']; + } + } + + return parent::_getPortableTableForeignKeysList($foreignKeys); + } + + /** + * {@inheritDoc} + */ + protected function _getPortableTableIndexesList($tableIndexes, $tableName = null) + { + foreach ($tableIndexes as &$tableIndex) { + $tableIndex['non_unique'] = (bool) $tableIndex['non_unique']; + $tableIndex['primary'] = (bool) $tableIndex['primary']; + $tableIndex['flags'] = $tableIndex['flags'] ? [$tableIndex['flags']] : null; + } + + return parent::_getPortableTableIndexesList($tableIndexes, $tableName); + } + + /** + * {@inheritDoc} + */ + protected function _getPortableTableForeignKeyDefinition($tableForeignKey) + { + return new ForeignKeyConstraint( + $tableForeignKey['local_columns'], + $tableForeignKey['foreign_table'], + $tableForeignKey['foreign_columns'], + $tableForeignKey['name'], + $tableForeignKey['options'], + ); + } + + /** + * {@inheritDoc} + */ + protected function _getPortableTableDefinition($table) + { + if ($table['schema_name'] !== 'dbo') { + return $table['schema_name'] . '.' . $table['table_name']; + } + + return $table['table_name']; + } + + /** + * {@inheritDoc} + */ + protected function _getPortableDatabaseDefinition($database) + { + return $database['name']; + } + + /** + * {@inheritDoc} + * + * @deprecated Use {@see listSchemaNames()} instead. + */ + protected function getPortableNamespaceDefinition(array $namespace) + { + Deprecation::triggerIfCalledFromOutside( + 'doctrine/dbal', + 'https://github.com/doctrine/dbal/issues/4503', + 'SQLServerSchemaManager::getPortableNamespaceDefinition() is deprecated,' + . ' use SQLServerSchemaManager::listSchemaNames() instead.', + ); + + return $namespace['name']; + } + + /** + * {@inheritDoc} + */ + protected function _getPortableViewDefinition($view) + { + // @todo + return new View($view['name'], $view['definition']); + } + + /** + * {@inheritDoc} + */ + public function alterTable(TableDiff $tableDiff) + { + $droppedColumns = $tableDiff->getDroppedColumns(); + + if (count($droppedColumns) > 0) { + $tableName = ($tableDiff->getOldTable() ?? $tableDiff->getName($this->_platform))->getName(); + + foreach ($droppedColumns as $col) { + foreach ($this->getColumnConstraints($tableName, $col->getName()) as $constraint) { + $this->_conn->executeStatement( + sprintf( + 'ALTER TABLE %s DROP CONSTRAINT %s', + $tableName, + $constraint, + ), + ); + } + } + } + + parent::alterTable($tableDiff); + } + + /** + * Returns the names of the constraints for a given column. + * + * @return iterable + * + * @throws Exception + */ + private function getColumnConstraints(string $table, string $column): iterable + { + return $this->_conn->iterateColumn( + <<<'SQL' +SELECT o.name +FROM sys.objects o + INNER JOIN sys.objects t + ON t.object_id = o.parent_object_id + AND t.type = 'U' + INNER JOIN sys.default_constraints dc + ON dc.object_id = o.object_id + INNER JOIN sys.columns c + ON c.column_id = dc.parent_column_id + AND c.object_id = t.object_id +WHERE t.name = ? + AND c.name = ? +SQL + , + [$table, $column], + ); + } + + /** @throws Exception */ + public function createComparator(): Comparator + { + return new SQLServer\Comparator($this->_platform, $this->getDatabaseCollation()); + } + + /** @throws Exception */ + private function getDatabaseCollation(): string + { + if ($this->databaseCollation === null) { + $databaseCollation = $this->_conn->fetchOne( + 'SELECT collation_name FROM sys.databases WHERE name = ' + . $this->_platform->getCurrentDatabaseExpression(), + ); + + // a database is always selected, even if omitted in the connection parameters + assert(is_string($databaseCollation)); + + $this->databaseCollation = $databaseCollation; + } + + return $this->databaseCollation; + } + + protected function selectTableNames(string $databaseName): Result + { + // The "sysdiagrams" table must be ignored as it's internal SQL Server table for Database Diagrams + $sql = <<<'SQL' +SELECT name AS table_name, + SCHEMA_NAME(schema_id) AS schema_name +FROM sys.objects +WHERE type = 'U' + AND name != 'sysdiagrams' +ORDER BY name +SQL; + + return $this->_conn->executeQuery($sql); + } + + protected function selectTableColumns(string $databaseName, ?string $tableName = null): Result + { + $sql = 'SELECT'; + + if ($tableName === null) { + $sql .= ' obj.name AS table_name, scm.name AS schema_name,'; + } + + $sql .= <<<'SQL' + col.name, + type.name AS type, + col.max_length AS length, + ~col.is_nullable AS notnull, + def.definition AS [default], + col.scale, + col.precision, + col.is_identity AS autoincrement, + col.collation_name AS collation, + -- CAST avoids driver error for sql_variant type + CAST(prop.value AS NVARCHAR(MAX)) AS comment + FROM sys.columns AS col + JOIN sys.types AS type + ON col.user_type_id = type.user_type_id + JOIN sys.objects AS obj + ON col.object_id = obj.object_id + JOIN sys.schemas AS scm + ON obj.schema_id = scm.schema_id + LEFT JOIN sys.default_constraints def + ON col.default_object_id = def.object_id + AND col.object_id = def.parent_object_id + LEFT JOIN sys.extended_properties AS prop + ON obj.object_id = prop.major_id + AND col.column_id = prop.minor_id + AND prop.name = 'MS_Description' +SQL; + + // The "sysdiagrams" table must be ignored as it's internal SQL Server table for Database Diagrams + $conditions = ["obj.type = 'U'", "obj.name != 'sysdiagrams'"]; + $params = []; + + if ($tableName !== null) { + $conditions[] = $this->getTableWhereClause($tableName, 'scm.name', 'obj.name'); + } + + $sql .= ' WHERE ' . implode(' AND ', $conditions); + + return $this->_conn->executeQuery($sql, $params); + } + + protected function selectIndexColumns(string $databaseName, ?string $tableName = null): Result + { + $sql = 'SELECT'; + + if ($tableName === null) { + $sql .= ' tbl.name AS table_name, scm.name AS schema_name,'; + } + + $sql .= <<<'SQL' + idx.name AS key_name, + col.name AS column_name, + ~idx.is_unique AS non_unique, + idx.is_primary_key AS [primary], + CASE idx.type + WHEN '1' THEN 'clustered' + WHEN '2' THEN 'nonclustered' + ELSE NULL + END AS flags + FROM sys.tables AS tbl + JOIN sys.schemas AS scm + ON tbl.schema_id = scm.schema_id + JOIN sys.indexes AS idx + ON tbl.object_id = idx.object_id + JOIN sys.index_columns AS idxcol + ON idx.object_id = idxcol.object_id + AND idx.index_id = idxcol.index_id + JOIN sys.columns AS col + ON idxcol.object_id = col.object_id + AND idxcol.column_id = col.column_id +SQL; + + $conditions = []; + $params = []; + + if ($tableName !== null) { + $conditions[] = $this->getTableWhereClause($tableName, 'scm.name', 'tbl.name'); + $sql .= ' WHERE ' . implode(' AND ', $conditions); + } + + $sql .= ' ORDER BY idx.index_id, idxcol.key_ordinal'; + + return $this->_conn->executeQuery($sql, $params); + } + + protected function selectForeignKeyColumns(string $databaseName, ?string $tableName = null): Result + { + $sql = 'SELECT'; + + if ($tableName === null) { + $sql .= ' OBJECT_NAME (f.parent_object_id) AS table_name, SCHEMA_NAME(f.schema_id) AS schema_name,'; + } + + $sql .= <<<'SQL' + f.name AS ForeignKey, + SCHEMA_NAME (f.SCHEMA_ID) AS SchemaName, + OBJECT_NAME (f.parent_object_id) AS TableName, + COL_NAME (fc.parent_object_id,fc.parent_column_id) AS ColumnName, + SCHEMA_NAME (o.SCHEMA_ID) ReferenceSchemaName, + OBJECT_NAME (f.referenced_object_id) AS ReferenceTableName, + COL_NAME(fc.referenced_object_id,fc.referenced_column_id) AS ReferenceColumnName, + f.delete_referential_action_desc, + f.update_referential_action_desc + FROM sys.foreign_keys AS f + INNER JOIN sys.foreign_key_columns AS fc + INNER JOIN sys.objects AS o ON o.OBJECT_ID = fc.referenced_object_id + ON f.OBJECT_ID = fc.constraint_object_id +SQL; + + $conditions = []; + $params = []; + + if ($tableName !== null) { + $conditions[] = $this->getTableWhereClause( + $tableName, + 'SCHEMA_NAME(f.schema_id)', + 'OBJECT_NAME(f.parent_object_id)', + ); + + $sql .= ' WHERE ' . implode(' AND ', $conditions); + } + + $sql .= ' ORDER BY fc.constraint_column_id'; + + return $this->_conn->executeQuery($sql, $params); + } + + /** + * {@inheritDoc} + */ + protected function fetchTableOptionsByTable(string $databaseName, ?string $tableName = null): array + { + $sql = <<<'SQL' + SELECT + scm.name AS schema_name, + tbl.name AS table_name, + p.value AS [table_comment] + FROM + sys.tables AS tbl + JOIN sys.schemas AS scm + ON tbl.schema_id = scm.schema_id + INNER JOIN sys.extended_properties AS p ON p.major_id=tbl.object_id AND p.minor_id=0 AND p.class=1 +SQL; + + $conditions = ["p.name = N'MS_Description'"]; + + if ($tableName !== null) { + $conditions[] = $this->getTableWhereClause($tableName, 'scm.name', 'tbl.name'); + } + + $sql .= ' WHERE ' . implode(' AND ', $conditions); + + $tableOptions = []; + foreach ($this->_conn->iterateAssociative($sql) as $data) { + $data = array_change_key_case($data, CASE_LOWER); + + $tableOptions[$this->_getPortableTableDefinition($data)] = [ + 'comment' => $data['table_comment'], + ]; + } + + return $tableOptions; + } + + /** + * Returns the where clause to filter schema and table name in a query. + * + * @param string $table The full qualified name of the table. + * @param string $schemaColumn The name of the column to compare the schema to in the where clause. + * @param string $tableColumn The name of the column to compare the table to in the where clause. + */ + private function getTableWhereClause($table, $schemaColumn, $tableColumn): string + { + if (strpos($table, '.') !== false) { + [$schema, $table] = explode('.', $table); + $schema = $this->_platform->quoteStringLiteral($schema); + $table = $this->_platform->quoteStringLiteral($table); + } else { + $schema = 'SCHEMA_NAME()'; + $table = $this->_platform->quoteStringLiteral($table); + } + + return sprintf('(%s = %s AND %s = %s)', $tableColumn, $table, $schemaColumn, $schema); + } +} diff --git a/3rdparty/doctrine/dbal/src/Schema/Schema.php b/3rdparty/doctrine/dbal/src/Schema/Schema.php new file mode 100644 index 00000000..703c83c5 --- /dev/null +++ b/3rdparty/doctrine/dbal/src/Schema/Schema.php @@ -0,0 +1,523 @@ +_schemaConfig = $schemaConfig; + $this->_setName($schemaConfig->getName() ?? 'public'); + + foreach ($namespaces as $namespace) { + $this->createNamespace($namespace); + } + + foreach ($tables as $table) { + $this->_addTable($table); + } + + foreach ($sequences as $sequence) { + $this->_addSequence($sequence); + } + } + + /** + * @deprecated + * + * @return bool + */ + public function hasExplicitForeignKeyIndexes() + { + Deprecation::trigger( + 'doctrine/dbal', + 'https://github.com/doctrine/dbal/pull/4822', + 'Schema::hasExplicitForeignKeyIndexes() is deprecated.', + ); + + return $this->_schemaConfig->hasExplicitForeignKeyIndexes(); + } + + /** + * @return void + * + * @throws SchemaException + */ + protected function _addTable(Table $table) + { + $namespaceName = $table->getNamespaceName(); + $tableName = $this->normalizeName($table); + + if (isset($this->_tables[$tableName])) { + throw SchemaException::tableAlreadyExists($tableName); + } + + if ( + $namespaceName !== null + && ! $table->isInDefaultNamespace($this->getName()) + && ! $this->hasNamespace($namespaceName) + ) { + $this->createNamespace($namespaceName); + } + + $this->_tables[$tableName] = $table; + $table->setSchemaConfig($this->_schemaConfig); + } + + /** + * @return void + * + * @throws SchemaException + */ + protected function _addSequence(Sequence $sequence) + { + $namespaceName = $sequence->getNamespaceName(); + $seqName = $this->normalizeName($sequence); + + if (isset($this->_sequences[$seqName])) { + throw SchemaException::sequenceAlreadyExists($seqName); + } + + if ( + $namespaceName !== null + && ! $sequence->isInDefaultNamespace($this->getName()) + && ! $this->hasNamespace($namespaceName) + ) { + $this->createNamespace($namespaceName); + } + + $this->_sequences[$seqName] = $sequence; + } + + /** + * Returns the namespaces of this schema. + * + * @return string[] A list of namespace names. + */ + public function getNamespaces() + { + return $this->namespaces; + } + + /** + * Gets all tables of this schema. + * + * @return Table[] + */ + public function getTables() + { + return $this->_tables; + } + + /** + * @param string $name + * + * @return Table + * + * @throws SchemaException + */ + public function getTable($name) + { + $name = $this->getFullQualifiedAssetName($name); + if (! isset($this->_tables[$name])) { + throw SchemaException::tableDoesNotExist($name); + } + + return $this->_tables[$name]; + } + + /** @param string $name */ + private function getFullQualifiedAssetName($name): string + { + $name = $this->getUnquotedAssetName($name); + + if (strpos($name, '.') === false) { + $name = $this->getName() . '.' . $name; + } + + return strtolower($name); + } + + private function normalizeName(AbstractAsset $asset): string + { + return $asset->getFullQualifiedName($this->getName()); + } + + /** + * Returns the unquoted representation of a given asset name. + * + * @param string $assetName Quoted or unquoted representation of an asset name. + */ + private function getUnquotedAssetName($assetName): string + { + if ($this->isIdentifierQuoted($assetName)) { + return $this->trimQuotes($assetName); + } + + return $assetName; + } + + /** + * Does this schema have a namespace with the given name? + * + * @param string $name + * + * @return bool + */ + public function hasNamespace($name) + { + $name = strtolower($this->getUnquotedAssetName($name)); + + return isset($this->namespaces[$name]); + } + + /** + * Does this schema have a table with the given name? + * + * @param string $name + * + * @return bool + */ + public function hasTable($name) + { + $name = $this->getFullQualifiedAssetName($name); + + return isset($this->_tables[$name]); + } + + /** + * Gets all table names, prefixed with a schema name, even the default one if present. + * + * @deprecated Use {@see getTables()} and {@see Table::getName()} instead. + * + * @return string[] + */ + public function getTableNames() + { + Deprecation::trigger( + 'doctrine/dbal', + 'https://github.com/doctrine/dbal/pull/4800', + 'Schema::getTableNames() is deprecated.' + . ' Use Schema::getTables() and Table::getName() instead.', + __METHOD__, + ); + + return array_keys($this->_tables); + } + + /** + * @param string $name + * + * @return bool + */ + public function hasSequence($name) + { + $name = $this->getFullQualifiedAssetName($name); + + return isset($this->_sequences[$name]); + } + + /** + * @param string $name + * + * @return Sequence + * + * @throws SchemaException + */ + public function getSequence($name) + { + $name = $this->getFullQualifiedAssetName($name); + if (! $this->hasSequence($name)) { + throw SchemaException::sequenceDoesNotExist($name); + } + + return $this->_sequences[$name]; + } + + /** @return Sequence[] */ + public function getSequences() + { + return $this->_sequences; + } + + /** + * Creates a new namespace. + * + * @param string $name The name of the namespace to create. + * + * @return Schema This schema instance. + * + * @throws SchemaException + */ + public function createNamespace($name) + { + $unquotedName = strtolower($this->getUnquotedAssetName($name)); + + if (isset($this->namespaces[$unquotedName])) { + throw SchemaException::namespaceAlreadyExists($unquotedName); + } + + $this->namespaces[$unquotedName] = $name; + + return $this; + } + + /** + * Creates a new table. + * + * @param string $name + * + * @return Table + * + * @throws SchemaException + */ + public function createTable($name) + { + $table = new Table($name); + $this->_addTable($table); + + foreach ($this->_schemaConfig->getDefaultTableOptions() as $option => $value) { + $table->addOption($option, $value); + } + + return $table; + } + + /** + * Renames a table. + * + * @param string $oldName + * @param string $newName + * + * @return Schema + * + * @throws SchemaException + */ + public function renameTable($oldName, $newName) + { + $table = $this->getTable($oldName); + $table->_setName($newName); + + $this->dropTable($oldName); + $this->_addTable($table); + + return $this; + } + + /** + * Drops a table from the schema. + * + * @param string $name + * + * @return Schema + * + * @throws SchemaException + */ + public function dropTable($name) + { + $name = $this->getFullQualifiedAssetName($name); + $this->getTable($name); + unset($this->_tables[$name]); + + return $this; + } + + /** + * Creates a new sequence. + * + * @param string $name + * @param int $allocationSize + * @param int $initialValue + * + * @return Sequence + * + * @throws SchemaException + */ + public function createSequence($name, $allocationSize = 1, $initialValue = 1) + { + $seq = new Sequence($name, $allocationSize, $initialValue); + $this->_addSequence($seq); + + return $seq; + } + + /** + * @param string $name + * + * @return Schema + */ + public function dropSequence($name) + { + $name = $this->getFullQualifiedAssetName($name); + unset($this->_sequences[$name]); + + return $this; + } + + /** + * Returns an array of necessary SQL queries to create the schema on the given platform. + * + * @return list + * + * @throws Exception + */ + public function toSql(AbstractPlatform $platform) + { + $builder = new CreateSchemaObjectsSQLBuilder($platform); + + return $builder->buildSQL($this); + } + + /** + * Return an array of necessary SQL queries to drop the schema on the given platform. + * + * @return list + * + * @throws Exception + */ + public function toDropSql(AbstractPlatform $platform) + { + $builder = new DropSchemaObjectsSQLBuilder($platform); + + return $builder->buildSQL($this); + } + + /** + * @deprecated + * + * @return string[] + * + * @throws SchemaException + */ + public function getMigrateToSql(Schema $toSchema, AbstractPlatform $platform) + { + $schemaDiff = (new Comparator())->compareSchemas($this, $toSchema); + + return $schemaDiff->toSql($platform); + } + + /** + * @deprecated + * + * @return string[] + * + * @throws SchemaException + */ + public function getMigrateFromSql(Schema $fromSchema, AbstractPlatform $platform) + { + $schemaDiff = (new Comparator())->compareSchemas($fromSchema, $this); + + return $schemaDiff->toSql($platform); + } + + /** + * @deprecated + * + * @return void + */ + public function visit(Visitor $visitor) + { + Deprecation::triggerIfCalledFromOutside( + 'doctrine/dbal', + 'https://github.com/doctrine/dbal/pull/5435', + 'Schema::visit() is deprecated.', + ); + + $visitor->acceptSchema($this); + + if ($visitor instanceof NamespaceVisitor) { + foreach ($this->namespaces as $namespace) { + $visitor->acceptNamespace($namespace); + } + } + + foreach ($this->_tables as $table) { + $table->visit($visitor); + } + + foreach ($this->_sequences as $sequence) { + $sequence->visit($visitor); + } + } + + /** + * Cloning a Schema triggers a deep clone of all related assets. + * + * @return void + */ + public function __clone() + { + foreach ($this->_tables as $k => $table) { + $this->_tables[$k] = clone $table; + } + + foreach ($this->_sequences as $k => $sequence) { + $this->_sequences[$k] = clone $sequence; + } + } +} diff --git a/3rdparty/doctrine/dbal/src/Schema/SchemaConfig.php b/3rdparty/doctrine/dbal/src/Schema/SchemaConfig.php new file mode 100644 index 00000000..5e394514 --- /dev/null +++ b/3rdparty/doctrine/dbal/src/Schema/SchemaConfig.php @@ -0,0 +1,120 @@ +hasExplicitForeignKeyIndexes; + } + + /** + * @deprecated + * + * @param bool $flag + * + * @return void + */ + public function setExplicitForeignKeyIndexes($flag) + { + Deprecation::trigger( + 'doctrine/dbal', + 'https://github.com/doctrine/dbal/pull/4822', + 'SchemaConfig::setExplicitForeignKeyIndexes() is deprecated.', + ); + + $this->hasExplicitForeignKeyIndexes = (bool) $flag; + } + + /** + * @param int $length + * + * @return void + */ + public function setMaxIdentifierLength($length) + { + $this->maxIdentifierLength = (int) $length; + } + + /** @return int */ + public function getMaxIdentifierLength() + { + return $this->maxIdentifierLength; + } + + /** + * Gets the default namespace of schema objects. + * + * @return string|null + */ + public function getName() + { + return $this->name; + } + + /** + * Sets the default namespace name of schema objects. + * + * @param string $name The value to set. + * + * @return void + */ + public function setName($name) + { + $this->name = $name; + } + + /** + * Gets the default options that are passed to Table instances created with + * Schema#createTable(). + * + * @return mixed[] + */ + public function getDefaultTableOptions() + { + return $this->defaultTableOptions; + } + + /** + * @param mixed[] $defaultTableOptions + * + * @return void + */ + public function setDefaultTableOptions(array $defaultTableOptions) + { + $this->defaultTableOptions = $defaultTableOptions; + } +} diff --git a/3rdparty/doctrine/dbal/src/Schema/SchemaDiff.php b/3rdparty/doctrine/dbal/src/Schema/SchemaDiff.php new file mode 100644 index 00000000..a6a86657 --- /dev/null +++ b/3rdparty/doctrine/dbal/src/Schema/SchemaDiff.php @@ -0,0 +1,294 @@ + $createdSchemas + * @param array $droppedSchemas + * @param array $createdSequences + * @param array $alteredSequences + * @param array $droppedSequences + */ + public function __construct( + $newTables = [], + $changedTables = [], + $removedTables = [], + ?Schema $fromSchema = null, + $createdSchemas = [], + $droppedSchemas = [], + $createdSequences = [], + $alteredSequences = [], + $droppedSequences = [] + ) { + $this->newTables = $newTables; + + $this->changedTables = array_filter($changedTables, static function (TableDiff $diff): bool { + return ! $diff->isEmpty(); + }); + + $this->removedTables = $removedTables; + $this->fromSchema = $fromSchema; + $this->newNamespaces = $createdSchemas; + $this->removedNamespaces = $droppedSchemas; + $this->newSequences = $createdSequences; + $this->changedSequences = $alteredSequences; + $this->removedSequences = $droppedSequences; + } + + /** @return array */ + public function getCreatedSchemas(): array + { + return $this->newNamespaces; + } + + /** @return array */ + public function getDroppedSchemas(): array + { + return $this->removedNamespaces; + } + + /** @return array
*/ + public function getCreatedTables(): array + { + return $this->newTables; + } + + /** @return array */ + public function getAlteredTables(): array + { + return $this->changedTables; + } + + /** @return array
*/ + public function getDroppedTables(): array + { + return $this->removedTables; + } + + /** @return array */ + public function getCreatedSequences(): array + { + return $this->newSequences; + } + + /** @return array */ + public function getAlteredSequences(): array + { + return $this->changedSequences; + } + + /** @return array */ + public function getDroppedSequences(): array + { + return $this->removedSequences; + } + + /** + * Returns whether the diff is empty (contains no changes). + */ + public function isEmpty(): bool + { + return count($this->newNamespaces) === 0 + && count($this->removedNamespaces) === 0 + && count($this->newTables) === 0 + && count($this->changedTables) === 0 + && count($this->removedTables) === 0 + && count($this->newSequences) === 0 + && count($this->changedSequences) === 0 + && count($this->removedSequences) === 0; + } + + /** + * The to save sql mode ensures that the following things don't happen: + * + * 1. Tables are deleted + * 2. Sequences are deleted + * 3. Foreign Keys which reference tables that would otherwise be deleted. + * + * This way it is ensured that assets are deleted which might not be relevant to the metadata schema at all. + * + * @deprecated + * + * @return list + */ + public function toSaveSql(AbstractPlatform $platform) + { + Deprecation::trigger( + 'doctrine/dbal', + 'https://github.com/doctrine/dbal/pull/5766', + '%s is deprecated.', + __METHOD__, + ); + + return $this->_toSql($platform, true); + } + + /** + * @deprecated Use {@link AbstractPlatform::getAlterSchemaSQL()} instead. + * + * @return list + */ + public function toSql(AbstractPlatform $platform) + { + Deprecation::triggerIfCalledFromOutside( + 'doctrine/dbal', + 'https://github.com/doctrine/dbal/pull/5766', + '%s is deprecated. Use AbstractPlatform::getAlterSchemaSQL() instead.', + __METHOD__, + ); + + return $this->_toSql($platform, false); + } + + /** + * @param bool $saveMode + * + * @return list + */ + protected function _toSql(AbstractPlatform $platform, $saveMode = false) + { + $sql = []; + + if ($platform->supportsSchemas()) { + foreach ($this->getCreatedSchemas() as $schema) { + $sql[] = $platform->getCreateSchemaSQL($schema); + } + } + + if ($platform->supportsForeignKeyConstraints() && $saveMode === false) { + foreach ($this->orphanedForeignKeys as $orphanedForeignKey) { + $sql[] = $platform->getDropForeignKeySQL($orphanedForeignKey, $orphanedForeignKey->getLocalTable()); + } + } + + if ($platform->supportsSequences() === true) { + foreach ($this->getAlteredSequences() as $sequence) { + $sql[] = $platform->getAlterSequenceSQL($sequence); + } + + if ($saveMode === false) { + foreach ($this->getDroppedSequences() as $sequence) { + $sql[] = $platform->getDropSequenceSQL($sequence); + } + } + + foreach ($this->getCreatedSequences() as $sequence) { + $sql[] = $platform->getCreateSequenceSQL($sequence); + } + } + + $sql = array_merge($sql, $platform->getCreateTablesSQL($this->getCreatedTables())); + + if ($saveMode === false) { + $sql = array_merge($sql, $platform->getDropTablesSQL($this->getDroppedTables())); + } + + foreach ($this->getAlteredTables() as $tableDiff) { + $sql = array_merge($sql, $platform->getAlterTableSQL($tableDiff)); + } + + return $sql; + } +} diff --git a/3rdparty/doctrine/dbal/src/Schema/SchemaException.php b/3rdparty/doctrine/dbal/src/Schema/SchemaException.php new file mode 100644 index 00000000..24a9dd34 --- /dev/null +++ b/3rdparty/doctrine/dbal/src/Schema/SchemaException.php @@ -0,0 +1,203 @@ +_setName($name); + $this->setAllocationSize($allocationSize); + $this->setInitialValue($initialValue); + $this->cache = $cache; + } + + /** @return int */ + public function getAllocationSize() + { + return $this->allocationSize; + } + + /** @return int */ + public function getInitialValue() + { + return $this->initialValue; + } + + /** @return int|null */ + public function getCache() + { + return $this->cache; + } + + /** + * @param int $allocationSize + * + * @return Sequence + */ + public function setAllocationSize($allocationSize) + { + if ($allocationSize > 0) { + $this->allocationSize = $allocationSize; + } else { + $this->allocationSize = 1; + } + + return $this; + } + + /** + * @param int $initialValue + * + * @return Sequence + */ + public function setInitialValue($initialValue) + { + if ($initialValue > 0) { + $this->initialValue = $initialValue; + } else { + $this->initialValue = 1; + } + + return $this; + } + + /** + * @param int $cache + * + * @return Sequence + */ + public function setCache($cache) + { + $this->cache = $cache; + + return $this; + } + + /** + * Checks if this sequence is an autoincrement sequence for a given table. + * + * This is used inside the comparator to not report sequences as missing, + * when the "from" schema implicitly creates the sequences. + * + * @return bool + */ + public function isAutoIncrementsFor(Table $table) + { + $primaryKey = $table->getPrimaryKey(); + + if ($primaryKey === null) { + return false; + } + + $pkColumns = $primaryKey->getColumns(); + + if (count($pkColumns) !== 1) { + return false; + } + + $column = $table->getColumn($pkColumns[0]); + + if (! $column->getAutoincrement()) { + return false; + } + + $sequenceName = $this->getShortestName($table->getNamespaceName()); + $tableName = $table->getShortestName($table->getNamespaceName()); + $tableSequenceName = sprintf('%s_%s_seq', $tableName, $column->getShortestName($table->getNamespaceName())); + + return $tableSequenceName === $sequenceName; + } + + /** + * @deprecated + * + * @return void + */ + public function visit(Visitor $visitor) + { + Deprecation::triggerIfCalledFromOutside( + 'doctrine/dbal', + 'https://github.com/doctrine/dbal/pull/5435', + 'Sequence::visit() is deprecated.', + ); + + $visitor->acceptSequence($this); + } +} diff --git a/3rdparty/doctrine/dbal/src/Schema/SqliteSchemaManager.php b/3rdparty/doctrine/dbal/src/Schema/SqliteSchemaManager.php new file mode 100644 index 00000000..4b96739e --- /dev/null +++ b/3rdparty/doctrine/dbal/src/Schema/SqliteSchemaManager.php @@ -0,0 +1,807 @@ + + */ +class SqliteSchemaManager extends AbstractSchemaManager +{ + /** + * {@inheritDoc} + */ + public function listTableNames() + { + return $this->doListTableNames(); + } + + /** + * {@inheritDoc} + */ + public function listTables() + { + return $this->doListTables(); + } + + /** + * {@inheritDoc} + * + * @deprecated Use {@see introspectTable()} instead. + */ + public function listTableDetails($name) + { + Deprecation::triggerIfCalledFromOutside( + 'doctrine/dbal', + 'https://github.com/doctrine/dbal/pull/5595', + '%s is deprecated. Use introspectTable() instead.', + __METHOD__, + ); + + return $this->doListTableDetails($name); + } + + /** + * {@inheritDoc} + */ + public function listTableColumns($table, $database = null) + { + return $this->doListTableColumns($table, $database); + } + + /** + * {@inheritDoc} + */ + public function listTableIndexes($table) + { + return $this->doListTableIndexes($table); + } + + /** + * {@inheritDoc} + */ + protected function fetchForeignKeyColumnsByTable(string $databaseName): array + { + $columnsByTable = parent::fetchForeignKeyColumnsByTable($databaseName); + + if (count($columnsByTable) > 0) { + foreach ($columnsByTable as $table => $columns) { + $columnsByTable[$table] = $this->addDetailsToTableForeignKeyColumns($table, $columns); + } + } + + return $columnsByTable; + } + + /** + * {@inheritDoc} + * + * @deprecated Delete the database file using the filesystem. + */ + public function dropDatabase($database) + { + Deprecation::trigger( + 'doctrine/dbal', + 'https://github.com/doctrine/dbal/issues/4963', + 'SqliteSchemaManager::dropDatabase() is deprecated. Delete the database file using the filesystem.', + ); + + if (! file_exists($database)) { + return; + } + + unlink($database); + } + + /** + * {@inheritDoc} + * + * @deprecated The engine will create the database file automatically. + */ + public function createDatabase($database) + { + Deprecation::trigger( + 'doctrine/dbal', + 'https://github.com/doctrine/dbal/issues/4963', + 'SqliteSchemaManager::createDatabase() is deprecated.' + . ' The engine will create the database file automatically.', + ); + + $params = $this->_conn->getParams(); + + $params['path'] = $database; + unset($params['memory']); + + $conn = DriverManager::getConnection($params); + $conn->connect(); + $conn->close(); + } + + /** + * {@inheritDoc} + */ + public function createForeignKey(ForeignKeyConstraint $foreignKey, $table) + { + if (! $table instanceof Table) { + $table = $this->listTableDetails($table); + } + + $this->alterTable(new TableDiff($table->getName(), [], [], [], [], [], [], $table, [$foreignKey])); + } + + /** + * {@inheritDoc} + * + * @deprecated Use {@see dropForeignKey()} and {@see createForeignKey()} instead. + */ + public function dropAndCreateForeignKey(ForeignKeyConstraint $foreignKey, $table) + { + Deprecation::trigger( + 'doctrine/dbal', + 'https://github.com/doctrine/dbal/pull/4897', + 'SqliteSchemaManager::dropAndCreateForeignKey() is deprecated.' + . ' Use SqliteSchemaManager::dropForeignKey() and SqliteSchemaManager::createForeignKey() instead.', + ); + + if (! $table instanceof Table) { + $table = $this->listTableDetails($table); + } + + $this->alterTable(new TableDiff($table->getName(), [], [], [], [], [], [], $table, [], [$foreignKey])); + } + + /** + * {@inheritDoc} + */ + public function dropForeignKey($foreignKey, $table) + { + if (! $table instanceof Table) { + $table = $this->listTableDetails($table); + } + + $this->alterTable(new TableDiff($table->getName(), [], [], [], [], [], [], $table, [], [], [$foreignKey])); + } + + /** + * {@inheritDoc} + */ + public function listTableForeignKeys($table, $database = null) + { + $table = $this->normalizeName($table); + + $columns = $this->selectForeignKeyColumns($database ?? 'main', $table) + ->fetchAllAssociative(); + + if (count($columns) > 0) { + $columns = $this->addDetailsToTableForeignKeyColumns($table, $columns); + } + + return $this->_getPortableTableForeignKeysList($columns); + } + + /** + * {@inheritDoc} + */ + protected function _getPortableTableDefinition($table) + { + return $table['table_name']; + } + + /** + * {@inheritDoc} + */ + protected function _getPortableTableIndexesList($tableIndexes, $tableName = null) + { + $indexBuffer = []; + + // fetch primary + $indexArray = $this->_conn->fetchAllAssociative('SELECT * FROM PRAGMA_TABLE_INFO (?)', [$tableName]); + + usort( + $indexArray, + /** + * @param array $a + * @param array $b + */ + static function (array $a, array $b): int { + if ($a['pk'] === $b['pk']) { + return $a['cid'] - $b['cid']; + } + + return $a['pk'] - $b['pk']; + }, + ); + + foreach ($indexArray as $indexColumnRow) { + if ($indexColumnRow['pk'] === 0 || $indexColumnRow['pk'] === '0') { + continue; + } + + $indexBuffer[] = [ + 'key_name' => 'primary', + 'primary' => true, + 'non_unique' => false, + 'column_name' => $indexColumnRow['name'], + ]; + } + + // fetch regular indexes + foreach ($tableIndexes as $tableIndex) { + // Ignore indexes with reserved names, e.g. autoindexes + if (strpos($tableIndex['name'], 'sqlite_') === 0) { + continue; + } + + $keyName = $tableIndex['name']; + $idx = []; + $idx['key_name'] = $keyName; + $idx['primary'] = false; + $idx['non_unique'] = ! $tableIndex['unique']; + + $indexArray = $this->_conn->fetchAllAssociative('SELECT * FROM PRAGMA_INDEX_INFO (?)', [$keyName]); + + foreach ($indexArray as $indexColumnRow) { + $idx['column_name'] = $indexColumnRow['name']; + $indexBuffer[] = $idx; + } + } + + return parent::_getPortableTableIndexesList($indexBuffer, $tableName); + } + + /** + * {@inheritDoc} + */ + protected function _getPortableTableColumnList($table, $database, $tableColumns) + { + $list = parent::_getPortableTableColumnList($table, $database, $tableColumns); + + // find column with autoincrement + $autoincrementColumn = null; + $autoincrementCount = 0; + + foreach ($tableColumns as $tableColumn) { + if ($tableColumn['pk'] === 0 || $tableColumn['pk'] === '0') { + continue; + } + + $autoincrementCount++; + if ($autoincrementColumn !== null || strtolower($tableColumn['type']) !== 'integer') { + continue; + } + + $autoincrementColumn = $tableColumn['name']; + } + + if ($autoincrementCount === 1 && $autoincrementColumn !== null) { + foreach ($list as $column) { + if ($autoincrementColumn !== $column->getName()) { + continue; + } + + $column->setAutoincrement(true); + } + } + + // inspect column collation and comments + $createSql = $this->getCreateTableSQL($table); + + foreach ($list as $columnName => $column) { + $type = $column->getType(); + + if ($type instanceof StringType || $type instanceof TextType) { + $column->setPlatformOption( + 'collation', + $this->parseColumnCollationFromSQL($columnName, $createSql) ?? 'BINARY', + ); + } + + $comment = $this->parseColumnCommentFromSQL($columnName, $createSql); + + if ($comment === null) { + continue; + } + + $type = $this->extractDoctrineTypeFromComment($comment, ''); + + if ($type !== '') { + $column->setType(Type::getType($type)); + + $comment = $this->removeDoctrineTypeFromComment($comment, $type); + } + + $column->setComment($comment); + } + + return $list; + } + + /** + * {@inheritDoc} + */ + protected function _getPortableTableColumnDefinition($tableColumn) + { + $parts = explode('(', $tableColumn['type']); + $tableColumn['type'] = trim($parts[0]); + if (isset($parts[1])) { + $length = trim($parts[1], ')'); + $tableColumn['length'] = $length; + } + + $dbType = strtolower($tableColumn['type']); + $length = $tableColumn['length'] ?? null; + $unsigned = false; + + if (strpos($dbType, ' unsigned') !== false) { + $dbType = str_replace(' unsigned', '', $dbType); + $unsigned = true; + } + + $fixed = false; + $type = $this->_platform->getDoctrineTypeMapping($dbType); + $default = $tableColumn['dflt_value']; + if ($default === 'NULL') { + $default = null; + } + + if ($default !== null) { + // SQLite returns the default value as a literal expression, so we need to parse it + if (preg_match('/^\'(.*)\'$/s', $default, $matches) === 1) { + $default = str_replace("''", "'", $matches[1]); + } + } + + $notnull = (bool) $tableColumn['notnull']; + + if (! isset($tableColumn['name'])) { + $tableColumn['name'] = ''; + } + + $precision = null; + $scale = null; + + switch ($dbType) { + case 'char': + $fixed = true; + break; + case 'float': + case 'double': + case 'real': + case 'decimal': + case 'numeric': + if (isset($tableColumn['length'])) { + if (strpos($tableColumn['length'], ',') === false) { + $tableColumn['length'] .= ',0'; + } + + [$precision, $scale] = array_map('trim', explode(',', $tableColumn['length'])); + } + + $length = null; + break; + } + + $options = [ + 'length' => $length, + 'unsigned' => $unsigned, + 'fixed' => $fixed, + 'notnull' => $notnull, + 'default' => $default, + 'precision' => $precision, + 'scale' => $scale, + ]; + + return new Column($tableColumn['name'], Type::getType($type), $options); + } + + /** + * {@inheritDoc} + */ + protected function _getPortableViewDefinition($view) + { + return new View($view['name'], $view['sql']); + } + + /** + * {@inheritDoc} + */ + protected function _getPortableTableForeignKeysList($tableForeignKeys) + { + $list = []; + foreach ($tableForeignKeys as $value) { + $value = array_change_key_case($value, CASE_LOWER); + $id = $value['id']; + if (! isset($list[$id])) { + if (! isset($value['on_delete']) || $value['on_delete'] === 'RESTRICT') { + $value['on_delete'] = null; + } + + if (! isset($value['on_update']) || $value['on_update'] === 'RESTRICT') { + $value['on_update'] = null; + } + + $list[$id] = [ + 'name' => $value['constraint_name'], + 'local' => [], + 'foreign' => [], + 'foreignTable' => $value['table'], + 'onDelete' => $value['on_delete'], + 'onUpdate' => $value['on_update'], + 'deferrable' => $value['deferrable'], + 'deferred' => $value['deferred'], + ]; + } + + $list[$id]['local'][] = $value['from']; + + if ($value['to'] === null) { + // Inferring a shorthand form for the foreign key constraint, where the "to" field is empty. + // @see https://www.sqlite.org/foreignkeys.html#fk_indexes. + $foreignTableIndexes = $this->_getPortableTableIndexesList([], $value['table']); + + if (! isset($foreignTableIndexes['primary'])) { + continue; + } + + $list[$id]['foreign'] = [...$list[$id]['foreign'], ...$foreignTableIndexes['primary']->getColumns()]; + + continue; + } + + $list[$id]['foreign'][] = $value['to']; + } + + return parent::_getPortableTableForeignKeysList($list); + } + + /** + * {@inheritDoc} + */ + protected function _getPortableTableForeignKeyDefinition($tableForeignKey): ForeignKeyConstraint + { + return new ForeignKeyConstraint( + $tableForeignKey['local'], + $tableForeignKey['foreignTable'], + $tableForeignKey['foreign'], + $tableForeignKey['name'], + [ + 'onDelete' => $tableForeignKey['onDelete'], + 'onUpdate' => $tableForeignKey['onUpdate'], + 'deferrable' => $tableForeignKey['deferrable'], + 'deferred' => $tableForeignKey['deferred'], + ], + ); + } + + private function parseColumnCollationFromSQL(string $column, string $sql): ?string + { + $pattern = '{' . $this->buildIdentifierPattern($column) + . '[^,(]+(?:\([^()]+\)[^,]*)?(?:(?:DEFAULT|CHECK)\s*(?:\(.*?\))?[^,]*)*COLLATE\s+["\']?([^\s,"\')]+)}is'; + + if (preg_match($pattern, $sql, $match) !== 1) { + return null; + } + + return $match[1]; + } + + private function parseTableCommentFromSQL(string $table, string $sql): ?string + { + $pattern = '/\s* # Allow whitespace characters at start of line +CREATE\sTABLE' . $this->buildIdentifierPattern($table) . ' +( # Start capture + (?:\s*--[^\n]*\n?)+ # Capture anything that starts with whitespaces followed by -- until the end of the line(s) +)/ix'; + + if (preg_match($pattern, $sql, $match) !== 1) { + return null; + } + + $comment = preg_replace('{^\s*--}m', '', rtrim($match[1], "\n")); + + return $comment === '' ? null : $comment; + } + + private function parseColumnCommentFromSQL(string $column, string $sql): ?string + { + $pattern = '{[\s(,]' . $this->buildIdentifierPattern($column) + . '(?:\([^)]*?\)|[^,(])*?,?((?:(?!\n))(?:\s*--[^\n]*\n?)+)}i'; + + if (preg_match($pattern, $sql, $match) !== 1) { + return null; + } + + $comment = preg_replace('{^\s*--}m', '', rtrim($match[1], "\n")); + + return $comment === '' ? null : $comment; + } + + /** + * Returns a regular expression pattern that matches the given unquoted or quoted identifier. + */ + private function buildIdentifierPattern(string $identifier): string + { + return '(?:' . implode('|', array_map( + static function (string $sql): string { + return '\W' . preg_quote($sql, '/') . '\W'; + }, + [ + $identifier, + $this->_platform->quoteSingleIdentifier($identifier), + ], + )) . ')'; + } + + /** @throws Exception */ + private function getCreateTableSQL(string $table): string + { + $sql = $this->_conn->fetchOne( + <<<'SQL' +SELECT sql + FROM ( + SELECT * + FROM sqlite_master + UNION ALL + SELECT * + FROM sqlite_temp_master + ) +WHERE type = 'table' +AND name = ? +SQL + , + [$table], + ); + + if ($sql !== false) { + return $sql; + } + + return ''; + } + + /** + * @param list> $columns + * + * @return list> + * + * @throws Exception + */ + private function addDetailsToTableForeignKeyColumns(string $table, array $columns): array + { + $foreignKeyDetails = $this->getForeignKeyDetails($table); + $foreignKeyCount = count($foreignKeyDetails); + + foreach ($columns as $i => $column) { + // SQLite identifies foreign keys in reverse order of appearance in SQL + $columns[$i] = array_merge($column, $foreignKeyDetails[$foreignKeyCount - $column['id'] - 1]); + } + + return $columns; + } + + /** + * @param string $table + * + * @return list> + * + * @throws Exception + */ + private function getForeignKeyDetails($table) + { + $createSql = $this->getCreateTableSQL($table); + + if ( + preg_match_all( + '# + (?:CONSTRAINT\s+(\S+)\s+)? + (?:FOREIGN\s+KEY[^)]+\)\s*)? + REFERENCES\s+\S+\s*(?:\([^)]+\))? + (?: + [^,]*? + (NOT\s+DEFERRABLE|DEFERRABLE) + (?:\s+INITIALLY\s+(DEFERRED|IMMEDIATE))? + )?#isx', + $createSql, + $match, + ) === 0 + ) { + return []; + } + + $names = $match[1]; + $deferrable = $match[2]; + $deferred = $match[3]; + $details = []; + + for ($i = 0, $count = count($match[0]); $i < $count; $i++) { + $details[] = [ + 'constraint_name' => isset($names[$i]) && $names[$i] !== '' ? $names[$i] : null, + 'deferrable' => isset($deferrable[$i]) && strcasecmp($deferrable[$i], 'deferrable') === 0, + 'deferred' => isset($deferred[$i]) && strcasecmp($deferred[$i], 'deferred') === 0, + ]; + } + + return $details; + } + + public function createComparator(): Comparator + { + return new SQLite\Comparator($this->_platform); + } + + /** + * {@inheritDoc} + * + * @deprecated + */ + public function getSchemaSearchPaths() + { + Deprecation::triggerIfCalledFromOutside( + 'doctrine/dbal', + 'https://github.com/doctrine/dbal/pull/4821', + 'SqliteSchemaManager::getSchemaSearchPaths() is deprecated.', + ); + + // SQLite does not support schemas or databases + return []; + } + + protected function selectTableNames(string $databaseName): Result + { + $sql = <<<'SQL' +SELECT name AS table_name +FROM sqlite_master +WHERE type = 'table' + AND name != 'sqlite_sequence' + AND name != 'geometry_columns' + AND name != 'spatial_ref_sys' +UNION ALL +SELECT name +FROM sqlite_temp_master +WHERE type = 'table' +ORDER BY name +SQL; + + return $this->_conn->executeQuery($sql); + } + + protected function selectTableColumns(string $databaseName, ?string $tableName = null): Result + { + $sql = <<<'SQL' + SELECT t.name AS table_name, + c.* + FROM sqlite_master t + JOIN pragma_table_info(t.name) c +SQL; + + $conditions = [ + "t.type = 'table'", + "t.name NOT IN ('geometry_columns', 'spatial_ref_sys', 'sqlite_sequence')", + ]; + $params = []; + + if ($tableName !== null) { + $conditions[] = 't.name = ?'; + $params[] = $this->_platform->canEmulateSchemas() + ? str_replace('.', '__', $tableName) + : $tableName; + } + + $sql .= ' WHERE ' . implode(' AND ', $conditions) . ' ORDER BY t.name, c.cid'; + + return $this->_conn->executeQuery($sql, $params); + } + + protected function selectIndexColumns(string $databaseName, ?string $tableName = null): Result + { + $sql = <<<'SQL' + SELECT t.name AS table_name, + i.* + FROM sqlite_master t + JOIN pragma_index_list(t.name) i +SQL; + + $conditions = [ + "t.type = 'table'", + "t.name NOT IN ('geometry_columns', 'spatial_ref_sys', 'sqlite_sequence')", + ]; + $params = []; + + if ($tableName !== null) { + $conditions[] = 't.name = ?'; + $params[] = $this->_platform->canEmulateSchemas() + ? str_replace('.', '__', $tableName) + : $tableName; + } + + $sql .= ' WHERE ' . implode(' AND ', $conditions) . ' ORDER BY t.name, i.seq'; + + return $this->_conn->executeQuery($sql, $params); + } + + protected function selectForeignKeyColumns(string $databaseName, ?string $tableName = null): Result + { + $sql = <<<'SQL' + SELECT t.name AS table_name, + p.* + FROM sqlite_master t + JOIN pragma_foreign_key_list(t.name) p + ON p."seq" != '-1' +SQL; + + $conditions = [ + "t.type = 'table'", + "t.name NOT IN ('geometry_columns', 'spatial_ref_sys', 'sqlite_sequence')", + ]; + $params = []; + + if ($tableName !== null) { + $conditions[] = 't.name = ?'; + $params[] = $this->_platform->canEmulateSchemas() + ? str_replace('.', '__', $tableName) + : $tableName; + } + + $sql .= ' WHERE ' . implode(' AND ', $conditions) . ' ORDER BY t.name, p.id DESC, p.seq'; + + return $this->_conn->executeQuery($sql, $params); + } + + /** + * {@inheritDoc} + */ + protected function fetchTableOptionsByTable(string $databaseName, ?string $tableName = null): array + { + if ($tableName === null) { + $tables = $this->listTableNames(); + } else { + $tables = [$tableName]; + } + + $tableOptions = []; + foreach ($tables as $table) { + $comment = $this->parseTableCommentFromSQL($table, $this->getCreateTableSQL($table)); + + if ($comment === null) { + continue; + } + + $tableOptions[$table]['comment'] = $comment; + } + + return $tableOptions; + } +} diff --git a/3rdparty/doctrine/dbal/src/Schema/Table.php b/3rdparty/doctrine/dbal/src/Schema/Table.php new file mode 100644 index 00000000..ce4cc326 --- /dev/null +++ b/3rdparty/doctrine/dbal/src/Schema/Table.php @@ -0,0 +1,1041 @@ + [], + ]; + + /** @var SchemaConfig|null */ + protected $_schemaConfig; + + /** @var Index[] */ + private array $implicitIndexes = []; + + /** + * @param Column[] $columns + * @param Index[] $indexes + * @param UniqueConstraint[] $uniqueConstraints + * @param ForeignKeyConstraint[] $fkConstraints + * @param mixed[] $options + * + * @throws SchemaException + * @throws Exception + */ + public function __construct( + string $name, + array $columns = [], + array $indexes = [], + array $uniqueConstraints = [], + array $fkConstraints = [], + array $options = [] + ) { + if ($name === '') { + throw InvalidTableName::new($name); + } + + $this->_setName($name); + + foreach ($columns as $column) { + $this->_addColumn($column); + } + + foreach ($indexes as $idx) { + $this->_addIndex($idx); + } + + foreach ($uniqueConstraints as $uniqueConstraint) { + $this->_addUniqueConstraint($uniqueConstraint); + } + + foreach ($fkConstraints as $constraint) { + $this->_addForeignKeyConstraint($constraint); + } + + $this->_options = array_merge($this->_options, $options); + } + + /** @return void */ + public function setSchemaConfig(SchemaConfig $schemaConfig) + { + $this->_schemaConfig = $schemaConfig; + } + + /** @return int */ + protected function _getMaxIdentifierLength() + { + if ($this->_schemaConfig instanceof SchemaConfig) { + return $this->_schemaConfig->getMaxIdentifierLength(); + } + + return 63; + } + + /** + * Sets the Primary Key. + * + * @param string[] $columnNames + * @param string|false $indexName + * + * @return self + * + * @throws SchemaException + */ + public function setPrimaryKey(array $columnNames, $indexName = false) + { + if ($indexName === false) { + $indexName = 'primary'; + } + + $this->_addIndex($this->_createIndex($columnNames, $indexName, true, true)); + + foreach ($columnNames as $columnName) { + $column = $this->getColumn($columnName); + $column->setNotnull(true); + } + + return $this; + } + + /** + * @param string[] $columnNames + * @param string[] $flags + * @param mixed[] $options + * + * @return self + * + * @throws SchemaException + */ + public function addIndex(array $columnNames, ?string $indexName = null, array $flags = [], array $options = []) + { + $indexName ??= $this->_generateIdentifierName( + array_merge([$this->getName()], $columnNames), + 'idx', + $this->_getMaxIdentifierLength(), + ); + + return $this->_addIndex($this->_createIndex($columnNames, $indexName, false, false, $flags, $options)); + } + + /** + * @param string[] $columnNames + * @param string[] $flags + * @param mixed[] $options + * + * @return self + */ + public function addUniqueConstraint( + array $columnNames, + ?string $indexName = null, + array $flags = [], + array $options = [] + ): Table { + $indexName ??= $this->_generateIdentifierName( + array_merge([$this->getName()], $columnNames), + 'uniq', + $this->_getMaxIdentifierLength(), + ); + + return $this->_addUniqueConstraint($this->_createUniqueConstraint($columnNames, $indexName, $flags, $options)); + } + + /** + * Drops the primary key from this table. + * + * @return void + * + * @throws SchemaException + */ + public function dropPrimaryKey() + { + if ($this->_primaryKeyName === null) { + return; + } + + $this->dropIndex($this->_primaryKeyName); + $this->_primaryKeyName = null; + } + + /** + * Drops an index from this table. + * + * @param string $name The index name. + * + * @return void + * + * @throws SchemaException If the index does not exist. + */ + public function dropIndex($name) + { + $name = $this->normalizeIdentifier($name); + + if (! $this->hasIndex($name)) { + throw SchemaException::indexDoesNotExist($name, $this->_name); + } + + unset($this->_indexes[$name]); + } + + /** + * @param string[] $columnNames + * @param string|null $indexName + * @param mixed[] $options + * + * @return self + * + * @throws SchemaException + */ + public function addUniqueIndex(array $columnNames, $indexName = null, array $options = []) + { + $indexName ??= $this->_generateIdentifierName( + array_merge([$this->getName()], $columnNames), + 'uniq', + $this->_getMaxIdentifierLength(), + ); + + return $this->_addIndex($this->_createIndex($columnNames, $indexName, true, false, [], $options)); + } + + /** + * Renames an index. + * + * @param string $oldName The name of the index to rename from. + * @param string|null $newName The name of the index to rename to. + * If null is given, the index name will be auto-generated. + * + * @return self This table instance. + * + * @throws SchemaException If no index exists for the given current name + * or if an index with the given new name already exists on this table. + */ + public function renameIndex($oldName, $newName = null) + { + $oldName = $this->normalizeIdentifier($oldName); + $normalizedNewName = $this->normalizeIdentifier($newName); + + if ($oldName === $normalizedNewName) { + return $this; + } + + if (! $this->hasIndex($oldName)) { + throw SchemaException::indexDoesNotExist($oldName, $this->_name); + } + + if ($this->hasIndex($normalizedNewName)) { + throw SchemaException::indexAlreadyExists($normalizedNewName, $this->_name); + } + + $oldIndex = $this->_indexes[$oldName]; + + if ($oldIndex->isPrimary()) { + $this->dropPrimaryKey(); + + return $this->setPrimaryKey($oldIndex->getColumns(), $newName ?? false); + } + + unset($this->_indexes[$oldName]); + + if ($oldIndex->isUnique()) { + return $this->addUniqueIndex($oldIndex->getColumns(), $newName, $oldIndex->getOptions()); + } + + return $this->addIndex($oldIndex->getColumns(), $newName, $oldIndex->getFlags(), $oldIndex->getOptions()); + } + + /** + * Checks if an index begins in the order of the given columns. + * + * @param string[] $columnNames + * + * @return bool + */ + public function columnsAreIndexed(array $columnNames) + { + foreach ($this->getIndexes() as $index) { + if ($index->spansColumns($columnNames)) { + return true; + } + } + + return false; + } + + /** + * @param string[] $columnNames + * @param string $indexName + * @param bool $isUnique + * @param bool $isPrimary + * @param string[] $flags + * @param mixed[] $options + * + * @throws SchemaException + */ + private function _createIndex( + array $columnNames, + $indexName, + $isUnique, + $isPrimary, + array $flags = [], + array $options = [] + ): Index { + if (preg_match('(([^a-zA-Z0-9_]+))', $this->normalizeIdentifier($indexName)) === 1) { + throw SchemaException::indexNameInvalid($indexName); + } + + foreach ($columnNames as $columnName) { + if (! $this->hasColumn($columnName)) { + throw SchemaException::columnDoesNotExist($columnName, $this->_name); + } + } + + return new Index($indexName, $columnNames, $isUnique, $isPrimary, $flags, $options); + } + + /** + * @param string $name + * @param string $typeName + * @param mixed[] $options + * + * @return Column + * + * @throws SchemaException + */ + public function addColumn($name, $typeName, array $options = []) + { + $column = new Column($name, Type::getType($typeName), $options); + + $this->_addColumn($column); + + return $column; + } + + /** + * Change Column Details. + * + * @deprecated Use {@link modifyColumn()} instead. + * + * @param string $name + * @param mixed[] $options + * + * @return self + * + * @throws SchemaException + */ + public function changeColumn($name, array $options) + { + Deprecation::trigger( + 'doctrine/dbal', + 'https://github.com/doctrine/dbal/pull/5747', + '%s is deprecated. Use modifyColumn() instead.', + __METHOD__, + ); + + return $this->modifyColumn($name, $options); + } + + /** + * @param string $name + * @param mixed[] $options + * + * @return self + * + * @throws SchemaException + */ + public function modifyColumn($name, array $options) + { + $column = $this->getColumn($name); + $column->setOptions($options); + + return $this; + } + + /** + * Drops a Column from the Table. + * + * @param string $name + * + * @return self + */ + public function dropColumn($name) + { + $name = $this->normalizeIdentifier($name); + + unset($this->_columns[$name]); + + return $this; + } + + /** + * Adds a foreign key constraint. + * + * Name is inferred from the local columns. + * + * @param Table|string $foreignTable Table schema instance or table name + * @param string[] $localColumnNames + * @param string[] $foreignColumnNames + * @param mixed[] $options + * @param string|null $name + * + * @return self + * + * @throws SchemaException + */ + public function addForeignKeyConstraint( + $foreignTable, + array $localColumnNames, + array $foreignColumnNames, + array $options = [], + $name = null + ) { + $name ??= $this->_generateIdentifierName( + array_merge([$this->getName()], $localColumnNames), + 'fk', + $this->_getMaxIdentifierLength(), + ); + + if ($foreignTable instanceof Table) { + foreach ($foreignColumnNames as $columnName) { + if (! $foreignTable->hasColumn($columnName)) { + throw SchemaException::columnDoesNotExist($columnName, $foreignTable->getName()); + } + } + } + + foreach ($localColumnNames as $columnName) { + if (! $this->hasColumn($columnName)) { + throw SchemaException::columnDoesNotExist($columnName, $this->_name); + } + } + + $constraint = new ForeignKeyConstraint( + $localColumnNames, + $foreignTable, + $foreignColumnNames, + $name, + $options, + ); + + return $this->_addForeignKeyConstraint($constraint); + } + + /** + * @param string $name + * @param mixed $value + * + * @return self + */ + public function addOption($name, $value) + { + $this->_options[$name] = $value; + + return $this; + } + + /** + * @return void + * + * @throws SchemaException + */ + protected function _addColumn(Column $column) + { + $columnName = $column->getName(); + $columnName = $this->normalizeIdentifier($columnName); + + if (isset($this->_columns[$columnName])) { + throw SchemaException::columnAlreadyExists($this->getName(), $columnName); + } + + $this->_columns[$columnName] = $column; + } + + /** + * Adds an index to the table. + * + * @return self + * + * @throws SchemaException + */ + protected function _addIndex(Index $indexCandidate) + { + $indexName = $indexCandidate->getName(); + $indexName = $this->normalizeIdentifier($indexName); + $replacedImplicitIndexes = []; + + foreach ($this->implicitIndexes as $name => $implicitIndex) { + if (! $implicitIndex->isFulfilledBy($indexCandidate) || ! isset($this->_indexes[$name])) { + continue; + } + + $replacedImplicitIndexes[] = $name; + } + + if ( + (isset($this->_indexes[$indexName]) && ! in_array($indexName, $replacedImplicitIndexes, true)) || + ($this->_primaryKeyName !== null && $indexCandidate->isPrimary()) + ) { + throw SchemaException::indexAlreadyExists($indexName, $this->_name); + } + + foreach ($replacedImplicitIndexes as $name) { + unset($this->_indexes[$name], $this->implicitIndexes[$name]); + } + + if ($indexCandidate->isPrimary()) { + $this->_primaryKeyName = $indexName; + } + + $this->_indexes[$indexName] = $indexCandidate; + + return $this; + } + + /** @return self */ + protected function _addUniqueConstraint(UniqueConstraint $constraint): Table + { + $mergedNames = array_merge([$this->getName()], $constraint->getColumns()); + $name = strlen($constraint->getName()) > 0 + ? $constraint->getName() + : $this->_generateIdentifierName($mergedNames, 'fk', $this->_getMaxIdentifierLength()); + + $name = $this->normalizeIdentifier($name); + + $this->uniqueConstraints[$name] = $constraint; + + // If there is already an index that fulfills this requirements drop the request. In the case of __construct + // calling this method during hydration from schema-details all the explicitly added indexes lead to duplicates. + // This creates computation overhead in this case, however no duplicate indexes are ever added (column based). + $indexName = $this->_generateIdentifierName($mergedNames, 'idx', $this->_getMaxIdentifierLength()); + + $indexCandidate = $this->_createIndex($constraint->getColumns(), $indexName, true, false); + + foreach ($this->_indexes as $existingIndex) { + if ($indexCandidate->isFulfilledBy($existingIndex)) { + return $this; + } + } + + $this->implicitIndexes[$this->normalizeIdentifier($indexName)] = $indexCandidate; + + return $this; + } + + /** @return self */ + protected function _addForeignKeyConstraint(ForeignKeyConstraint $constraint) + { + $constraint->setLocalTable($this); + + if (strlen($constraint->getName()) > 0) { + $name = $constraint->getName(); + } else { + $name = $this->_generateIdentifierName( + array_merge([$this->getName()], $constraint->getLocalColumns()), + 'fk', + $this->_getMaxIdentifierLength(), + ); + } + + $name = $this->normalizeIdentifier($name); + + $this->_fkConstraints[$name] = $constraint; + + /* Add an implicit index (defined by the DBAL) on the foreign key + columns. If there is already a user-defined index that fulfills these + requirements drop the request. In the case of __construct() calling + this method during hydration from schema-details, all the explicitly + added indexes lead to duplicates. This creates computation overhead in + this case, however no duplicate indexes are ever added (based on + columns). */ + $indexName = $this->_generateIdentifierName( + array_merge([$this->getName()], $constraint->getColumns()), + 'idx', + $this->_getMaxIdentifierLength(), + ); + + $indexCandidate = $this->_createIndex($constraint->getColumns(), $indexName, false, false); + + foreach ($this->_indexes as $existingIndex) { + if ($indexCandidate->isFulfilledBy($existingIndex)) { + return $this; + } + } + + $this->_addIndex($indexCandidate); + $this->implicitIndexes[$this->normalizeIdentifier($indexName)] = $indexCandidate; + + return $this; + } + + /** + * Returns whether this table has a foreign key constraint with the given name. + * + * @param string $name + * + * @return bool + */ + public function hasForeignKey($name) + { + $name = $this->normalizeIdentifier($name); + + return isset($this->_fkConstraints[$name]); + } + + /** + * Returns the foreign key constraint with the given name. + * + * @param string $name The constraint name. + * + * @return ForeignKeyConstraint + * + * @throws SchemaException If the foreign key does not exist. + */ + public function getForeignKey($name) + { + $name = $this->normalizeIdentifier($name); + + if (! $this->hasForeignKey($name)) { + throw SchemaException::foreignKeyDoesNotExist($name, $this->_name); + } + + return $this->_fkConstraints[$name]; + } + + /** + * Removes the foreign key constraint with the given name. + * + * @param string $name The constraint name. + * + * @return void + * + * @throws SchemaException + */ + public function removeForeignKey($name) + { + $name = $this->normalizeIdentifier($name); + + if (! $this->hasForeignKey($name)) { + throw SchemaException::foreignKeyDoesNotExist($name, $this->_name); + } + + unset($this->_fkConstraints[$name]); + } + + /** + * Returns whether this table has a unique constraint with the given name. + */ + public function hasUniqueConstraint(string $name): bool + { + $name = $this->normalizeIdentifier($name); + + return isset($this->uniqueConstraints[$name]); + } + + /** + * Returns the unique constraint with the given name. + * + * @throws SchemaException If the unique constraint does not exist. + */ + public function getUniqueConstraint(string $name): UniqueConstraint + { + $name = $this->normalizeIdentifier($name); + + if (! $this->hasUniqueConstraint($name)) { + throw SchemaException::uniqueConstraintDoesNotExist($name, $this->_name); + } + + return $this->uniqueConstraints[$name]; + } + + /** + * Removes the unique constraint with the given name. + * + * @throws SchemaException If the unique constraint does not exist. + */ + public function removeUniqueConstraint(string $name): void + { + $name = $this->normalizeIdentifier($name); + + if (! $this->hasUniqueConstraint($name)) { + throw SchemaException::uniqueConstraintDoesNotExist($name, $this->_name); + } + + unset($this->uniqueConstraints[$name]); + } + + /** + * Returns ordered list of columns (primary keys are first, then foreign keys, then the rest) + * + * @return Column[] + */ + public function getColumns() + { + $primaryKeyColumns = $this->getPrimaryKey() !== null ? $this->getPrimaryKeyColumns() : []; + $foreignKeyColumns = $this->getForeignKeyColumns(); + $remainderColumns = $this->filterColumns( + array_merge(array_keys($primaryKeyColumns), array_keys($foreignKeyColumns)), + true, + ); + + return array_merge($primaryKeyColumns, $foreignKeyColumns, $remainderColumns); + } + + /** + * Returns the foreign key columns + * + * @deprecated Use {@see getForeignKey()} and {@see ForeignKeyConstraint::getLocalColumns()} instead. + * + * @return Column[] + */ + public function getForeignKeyColumns() + { + Deprecation::triggerIfCalledFromOutside( + 'doctrine/dbal', + 'https://github.com/doctrine/dbal/pull/5731', + '%s is deprecated. Use getForeignKey() and ForeignKeyConstraint::getLocalColumns() instead.', + __METHOD__, + ); + + $foreignKeyColumns = []; + + foreach ($this->getForeignKeys() as $foreignKey) { + $foreignKeyColumns = array_merge($foreignKeyColumns, $foreignKey->getLocalColumns()); + } + + return $this->filterColumns($foreignKeyColumns); + } + + /** + * Returns only columns that have specified names + * + * @param string[] $columnNames + * + * @return Column[] + */ + private function filterColumns(array $columnNames, bool $reverse = false): array + { + return array_filter($this->_columns, static function (string $columnName) use ($columnNames, $reverse): bool { + return in_array($columnName, $columnNames, true) !== $reverse; + }, ARRAY_FILTER_USE_KEY); + } + + /** + * Returns whether this table has a Column with the given name. + * + * @param string $name The column name. + * + * @return bool + */ + public function hasColumn($name) + { + $name = $this->normalizeIdentifier($name); + + return isset($this->_columns[$name]); + } + + /** + * Returns the Column with the given name. + * + * @param string $name The column name. + * + * @return Column + * + * @throws SchemaException If the column does not exist. + */ + public function getColumn($name) + { + $name = $this->normalizeIdentifier($name); + + if (! $this->hasColumn($name)) { + throw SchemaException::columnDoesNotExist($name, $this->_name); + } + + return $this->_columns[$name]; + } + + /** + * Returns the primary key. + * + * @return Index|null The primary key, or null if this Table has no primary key. + */ + public function getPrimaryKey() + { + if ($this->_primaryKeyName !== null) { + return $this->getIndex($this->_primaryKeyName); + } + + return null; + } + + /** + * Returns the primary key columns. + * + * @deprecated Use {@see getPrimaryKey()} and {@see Index::getColumns()} instead. + * + * @return Column[] + * + * @throws Exception + */ + public function getPrimaryKeyColumns() + { + Deprecation::triggerIfCalledFromOutside( + 'doctrine/dbal', + 'https://github.com/doctrine/dbal/pull/5731', + '%s is deprecated. Use getPrimaryKey() and Index::getColumns() instead.', + __METHOD__, + ); + + $primaryKey = $this->getPrimaryKey(); + + if ($primaryKey === null) { + throw new Exception('Table ' . $this->getName() . ' has no primary key.'); + } + + return $this->filterColumns($primaryKey->getColumns()); + } + + /** + * Returns whether this table has a primary key. + * + * @deprecated Use {@see getPrimaryKey()} instead. + * + * @return bool + */ + public function hasPrimaryKey() + { + Deprecation::trigger( + 'doctrine/dbal', + 'https://github.com/doctrine/dbal/pull/5731', + '%s is deprecated. Use getPrimaryKey() instead.', + __METHOD__, + ); + + return $this->_primaryKeyName !== null && $this->hasIndex($this->_primaryKeyName); + } + + /** + * Returns whether this table has an Index with the given name. + * + * @param string $name The index name. + * + * @return bool + */ + public function hasIndex($name) + { + $name = $this->normalizeIdentifier($name); + + return isset($this->_indexes[$name]); + } + + /** + * Returns the Index with the given name. + * + * @param string $name The index name. + * + * @return Index + * + * @throws SchemaException If the index does not exist. + */ + public function getIndex($name) + { + $name = $this->normalizeIdentifier($name); + if (! $this->hasIndex($name)) { + throw SchemaException::indexDoesNotExist($name, $this->_name); + } + + return $this->_indexes[$name]; + } + + /** @return Index[] */ + public function getIndexes() + { + return $this->_indexes; + } + + /** + * Returns the unique constraints. + * + * @return UniqueConstraint[] + */ + public function getUniqueConstraints(): array + { + return $this->uniqueConstraints; + } + + /** + * Returns the foreign key constraints. + * + * @return ForeignKeyConstraint[] + */ + public function getForeignKeys() + { + return $this->_fkConstraints; + } + + /** + * @param string $name + * + * @return bool + */ + public function hasOption($name) + { + return isset($this->_options[$name]); + } + + /** + * @param string $name + * + * @return mixed + */ + public function getOption($name) + { + return $this->_options[$name]; + } + + /** @return mixed[] */ + public function getOptions() + { + return $this->_options; + } + + /** + * @deprecated + * + * @return void + * + * @throws SchemaException + */ + public function visit(Visitor $visitor) + { + Deprecation::triggerIfCalledFromOutside( + 'doctrine/dbal', + 'https://github.com/doctrine/dbal/pull/5435', + 'Table::visit() is deprecated.', + ); + + $visitor->acceptTable($this); + + foreach ($this->getColumns() as $column) { + $visitor->acceptColumn($this, $column); + } + + foreach ($this->getIndexes() as $index) { + $visitor->acceptIndex($this, $index); + } + + foreach ($this->getForeignKeys() as $constraint) { + $visitor->acceptForeignKey($this, $constraint); + } + } + + /** + * Clone of a Table triggers a deep clone of all affected assets. + * + * @return void + */ + public function __clone() + { + foreach ($this->_columns as $k => $column) { + $this->_columns[$k] = clone $column; + } + + foreach ($this->_indexes as $k => $index) { + $this->_indexes[$k] = clone $index; + } + + foreach ($this->_fkConstraints as $k => $fk) { + $this->_fkConstraints[$k] = clone $fk; + $this->_fkConstraints[$k]->setLocalTable($this); + } + } + + /** + * @param string[] $columnNames + * @param string[] $flags + * @param mixed[] $options + * + * @throws SchemaException + */ + private function _createUniqueConstraint( + array $columnNames, + string $indexName, + array $flags = [], + array $options = [] + ): UniqueConstraint { + if (preg_match('(([^a-zA-Z0-9_]+))', $this->normalizeIdentifier($indexName)) === 1) { + throw SchemaException::indexNameInvalid($indexName); + } + + foreach ($columnNames as $columnName) { + if (! $this->hasColumn($columnName)) { + throw SchemaException::columnDoesNotExist($columnName, $this->_name); + } + } + + return new UniqueConstraint($indexName, $columnNames, $flags, $options); + } + + /** + * Normalizes a given identifier. + * + * Trims quotes and lowercases the given identifier. + * + * @return string The normalized identifier. + */ + private function normalizeIdentifier(?string $identifier): string + { + if ($identifier === null) { + return ''; + } + + return $this->trimQuotes(strtolower($identifier)); + } + + public function setComment(?string $comment): self + { + // For keeping backward compatibility with MySQL in previous releases, table comments are stored as options. + $this->addOption('comment', $comment); + + return $this; + } + + public function getComment(): ?string + { + return $this->_options['comment'] ?? null; + } +} diff --git a/3rdparty/doctrine/dbal/src/Schema/TableDiff.php b/3rdparty/doctrine/dbal/src/Schema/TableDiff.php new file mode 100644 index 00000000..9aaf9e77 --- /dev/null +++ b/3rdparty/doctrine/dbal/src/Schema/TableDiff.php @@ -0,0 +1,361 @@ + $addedColumns + * @param array $modifiedColumns + * @param array $droppedColumns + * @param array $addedIndexes + * @param array $changedIndexes + * @param array $removedIndexes + * @param list $addedForeignKeys + * @param list $changedForeignKeys + * @param list $removedForeignKeys + * @param array $renamedColumns + * @param array $renamedIndexes + */ + public function __construct( + $tableName, + $addedColumns = [], + $modifiedColumns = [], + $droppedColumns = [], + $addedIndexes = [], + $changedIndexes = [], + $removedIndexes = [], + ?Table $fromTable = null, + $addedForeignKeys = [], + $changedForeignKeys = [], + $removedForeignKeys = [], + $renamedColumns = [], + $renamedIndexes = [] + ) { + $this->name = $tableName; + $this->addedColumns = $addedColumns; + $this->changedColumns = $modifiedColumns; + $this->renamedColumns = $renamedColumns; + $this->removedColumns = $droppedColumns; + $this->addedIndexes = $addedIndexes; + $this->changedIndexes = $changedIndexes; + $this->renamedIndexes = $renamedIndexes; + $this->removedIndexes = $removedIndexes; + $this->addedForeignKeys = $addedForeignKeys; + $this->changedForeignKeys = $changedForeignKeys; + $this->removedForeignKeys = $removedForeignKeys; + + if ($fromTable === null) { + Deprecation::trigger( + 'doctrine/dbal', + 'https://github.com/doctrine/dbal/pull/5678', + 'Not passing the $fromTable to %s is deprecated.', + __METHOD__, + ); + } + + $this->fromTable = $fromTable; + } + + /** + * @deprecated Use {@see getOldTable()} instead. + * + * @param AbstractPlatform $platform The platform to use for retrieving this table diff's name. + * + * @return Identifier + */ + public function getName(AbstractPlatform $platform) + { + return new Identifier( + $this->fromTable instanceof Table ? $this->fromTable->getQuotedName($platform) : $this->name, + ); + } + + /** + * @deprecated Rename tables via {@link AbstractSchemaManager::renameTable()} instead. + * + * @return Identifier|false + */ + public function getNewName() + { + Deprecation::triggerIfCalledFromOutside( + 'doctrine/dbal', + 'https://github.com/doctrine/dbal/pull/5663', + '%s is deprecated. Rename tables via AbstractSchemaManager::renameTable() instead.', + __METHOD__, + ); + + if ($this->newName === false) { + return false; + } + + return new Identifier($this->newName); + } + + public function getOldTable(): ?Table + { + return $this->fromTable; + } + + /** @return list */ + public function getAddedColumns(): array + { + return array_values($this->addedColumns); + } + + /** @return list */ + public function getModifiedColumns(): array + { + return array_values($this->changedColumns); + } + + /** @return list */ + public function getDroppedColumns(): array + { + return array_values($this->removedColumns); + } + + /** @return array */ + public function getRenamedColumns(): array + { + return $this->renamedColumns; + } + + /** @return list */ + public function getAddedIndexes(): array + { + return array_values($this->addedIndexes); + } + + /** + * @internal This method exists only for compatibility with the current implementation of schema managers + * that modify the diff while processing it. + */ + public function unsetAddedIndex(Index $index): void + { + $this->addedIndexes = array_filter( + $this->addedIndexes, + static function (Index $addedIndex) use ($index): bool { + return $addedIndex !== $index; + }, + ); + } + + /** @return array */ + public function getModifiedIndexes(): array + { + return array_values($this->changedIndexes); + } + + /** @return list */ + public function getDroppedIndexes(): array + { + return array_values($this->removedIndexes); + } + + /** + * @internal This method exists only for compatibility with the current implementation of schema managers + * that modify the diff while processing it. + */ + public function unsetDroppedIndex(Index $index): void + { + $this->removedIndexes = array_filter( + $this->removedIndexes, + static function (Index $removedIndex) use ($index): bool { + return $removedIndex !== $index; + }, + ); + } + + /** @return array */ + public function getRenamedIndexes(): array + { + return $this->renamedIndexes; + } + + /** @return list */ + public function getAddedForeignKeys(): array + { + return $this->addedForeignKeys; + } + + /** @return list */ + public function getModifiedForeignKeys(): array + { + return $this->changedForeignKeys; + } + + /** @return list */ + public function getDroppedForeignKeys(): array + { + return $this->removedForeignKeys; + } + + /** + * @internal This method exists only for compatibility with the current implementation of the schema comparator. + * + * @param ForeignKeyConstraint|string $foreignKey + */ + public function unsetDroppedForeignKey($foreignKey): void + { + $this->removedForeignKeys = array_filter( + $this->removedForeignKeys, + static function ($removedForeignKey) use ($foreignKey): bool { + return $removedForeignKey !== $foreignKey; + }, + ); + } + + /** + * Returns whether the diff is empty (contains no changes). + */ + public function isEmpty(): bool + { + return count($this->addedColumns) === 0 + && count($this->changedColumns) === 0 + && count($this->removedColumns) === 0 + && count($this->renamedColumns) === 0 + && count($this->addedIndexes) === 0 + && count($this->changedIndexes) === 0 + && count($this->removedIndexes) === 0 + && count($this->renamedIndexes) === 0 + && count($this->addedForeignKeys) === 0 + && count($this->changedForeignKeys) === 0 + && count($this->removedForeignKeys) === 0; + } +} diff --git a/3rdparty/doctrine/dbal/src/Schema/UniqueConstraint.php b/3rdparty/doctrine/dbal/src/Schema/UniqueConstraint.php new file mode 100644 index 00000000..f353f303 --- /dev/null +++ b/3rdparty/doctrine/dbal/src/Schema/UniqueConstraint.php @@ -0,0 +1,154 @@ + Identifier) + * + * @var Identifier[] + */ + protected $columns = []; + + /** + * Platform specific flags. + * array($flagName => true) + * + * @var true[] + */ + protected $flags = []; + + /** + * Platform specific options. + * + * @var mixed[] + */ + private array $options; + + /** + * @param string[] $columns + * @param string[] $flags + * @param mixed[] $options + */ + public function __construct(string $name, array $columns, array $flags = [], array $options = []) + { + $this->_setName($name); + + $this->options = $options; + + foreach ($columns as $column) { + $this->addColumn($column); + } + + foreach ($flags as $flag) { + $this->addFlag($flag); + } + } + + /** + * {@inheritDoc} + */ + public function getColumns() + { + return array_keys($this->columns); + } + + /** + * {@inheritDoc} + */ + public function getQuotedColumns(AbstractPlatform $platform) + { + $columns = []; + + foreach ($this->columns as $column) { + $columns[] = $column->getQuotedName($platform); + } + + return $columns; + } + + /** @return string[] */ + public function getUnquotedColumns(): array + { + return array_map([$this, 'trimQuotes'], $this->getColumns()); + } + + /** + * Returns platform specific flags for unique constraint. + * + * @return string[] + */ + public function getFlags(): array + { + return array_keys($this->flags); + } + + /** + * Adds flag for a unique constraint that translates to platform specific handling. + * + * @return $this + * + * @example $uniqueConstraint->addFlag('CLUSTERED') + */ + public function addFlag(string $flag): UniqueConstraint + { + $this->flags[strtolower($flag)] = true; + + return $this; + } + + /** + * Does this unique constraint have a specific flag? + */ + public function hasFlag(string $flag): bool + { + return isset($this->flags[strtolower($flag)]); + } + + /** + * Removes a flag. + */ + public function removeFlag(string $flag): void + { + unset($this->flags[strtolower($flag)]); + } + + /** + * Does this unique constraint have a specific option? + */ + public function hasOption(string $name): bool + { + return isset($this->options[strtolower($name)]); + } + + /** @return mixed */ + public function getOption(string $name) + { + return $this->options[strtolower($name)]; + } + + /** @return mixed[] */ + public function getOptions(): array + { + return $this->options; + } + + /** + * Adds a new column to the unique constraint. + */ + protected function addColumn(string $column): void + { + $this->columns[$column] = new Identifier($column); + } +} diff --git a/3rdparty/doctrine/dbal/src/Schema/View.php b/3rdparty/doctrine/dbal/src/Schema/View.php new file mode 100644 index 00000000..b19f2ad1 --- /dev/null +++ b/3rdparty/doctrine/dbal/src/Schema/View.php @@ -0,0 +1,28 @@ +_setName($name); + $this->sql = $sql; + } + + /** @return string */ + public function getSql() + { + return $this->sql; + } +} diff --git a/3rdparty/doctrine/dbal/src/Schema/Visitor/AbstractVisitor.php b/3rdparty/doctrine/dbal/src/Schema/Visitor/AbstractVisitor.php new file mode 100644 index 00000000..f8f3b582 --- /dev/null +++ b/3rdparty/doctrine/dbal/src/Schema/Visitor/AbstractVisitor.php @@ -0,0 +1,49 @@ +platform = $platform; + } + + /** + * {@inheritDoc} + */ + public function acceptNamespace($namespaceName) + { + if (! $this->platform->supportsSchemas()) { + return; + } + + $this->createNamespaceQueries[] = $this->platform->getCreateSchemaSQL($namespaceName); + } + + /** + * {@inheritDoc} + */ + public function acceptTable(Table $table) + { + $this->createTableQueries = array_merge($this->createTableQueries, $this->platform->getCreateTableSQL($table)); + } + + /** + * {@inheritDoc} + */ + public function acceptForeignKey(Table $localTable, ForeignKeyConstraint $fkConstraint) + { + if (! $this->platform->supportsForeignKeyConstraints()) { + return; + } + + $this->createFkConstraintQueries[] = $this->platform->getCreateForeignKeySQL($fkConstraint, $localTable); + } + + /** + * {@inheritDoc} + */ + public function acceptSequence(Sequence $sequence) + { + $this->createSequenceQueries[] = $this->platform->getCreateSequenceSQL($sequence); + } + + /** @return void */ + public function resetQueries() + { + $this->createNamespaceQueries = []; + $this->createTableQueries = []; + $this->createSequenceQueries = []; + $this->createFkConstraintQueries = []; + } + + /** + * Gets all queries collected so far. + * + * @return string[] + */ + public function getQueries() + { + return array_merge( + $this->createNamespaceQueries, + $this->createSequenceQueries, + $this->createTableQueries, + $this->createFkConstraintQueries, + ); + } +} diff --git a/3rdparty/doctrine/dbal/src/Schema/Visitor/DropSchemaSqlCollector.php b/3rdparty/doctrine/dbal/src/Schema/Visitor/DropSchemaSqlCollector.php new file mode 100644 index 00000000..ddec6b4a --- /dev/null +++ b/3rdparty/doctrine/dbal/src/Schema/Visitor/DropSchemaSqlCollector.php @@ -0,0 +1,107 @@ +platform = $platform; + $this->initializeQueries(); + } + + /** + * {@inheritDoc} + */ + public function acceptTable(Table $table) + { + $this->tables->attach($table); + } + + /** + * {@inheritDoc} + */ + public function acceptForeignKey(Table $localTable, ForeignKeyConstraint $fkConstraint) + { + if (strlen($fkConstraint->getName()) === 0) { + throw SchemaException::namedForeignKeyRequired($localTable, $fkConstraint); + } + + $this->constraints->attach($fkConstraint, $localTable); + } + + /** + * {@inheritDoc} + */ + public function acceptSequence(Sequence $sequence) + { + $this->sequences->attach($sequence); + } + + /** @return void */ + public function clearQueries() + { + $this->initializeQueries(); + } + + /** @return string[] */ + public function getQueries() + { + $sql = []; + + foreach ($this->constraints as $fkConstraint) { + assert($fkConstraint instanceof ForeignKeyConstraint); + $localTable = $this->constraints[$fkConstraint]; + $sql[] = $this->platform->getDropForeignKeySQL( + $fkConstraint->getQuotedName($this->platform), + $localTable->getQuotedName($this->platform), + ); + } + + foreach ($this->sequences as $sequence) { + assert($sequence instanceof Sequence); + $sql[] = $this->platform->getDropSequenceSQL($sequence->getQuotedName($this->platform)); + } + + foreach ($this->tables as $table) { + assert($table instanceof Table); + $sql[] = $this->platform->getDropTableSQL($table->getQuotedName($this->platform)); + } + + return $sql; + } + + private function initializeQueries(): void + { + $this->constraints = new SplObjectStorage(); + $this->sequences = new SplObjectStorage(); + $this->tables = new SplObjectStorage(); + } +} diff --git a/3rdparty/doctrine/dbal/src/Schema/Visitor/Graphviz.php b/3rdparty/doctrine/dbal/src/Schema/Visitor/Graphviz.php new file mode 100644 index 00000000..5eff0d94 --- /dev/null +++ b/3rdparty/doctrine/dbal/src/Schema/Visitor/Graphviz.php @@ -0,0 +1,164 @@ +output .= $this->createNodeRelation( + $fkConstraint->getLocalTableName() . ':col' . current($fkConstraint->getLocalColumns()) . ':se', + $fkConstraint->getForeignTableName() . ':col' . current($fkConstraint->getForeignColumns()) . ':se', + [ + 'dir' => 'back', + 'arrowtail' => 'dot', + 'arrowhead' => 'normal', + ], + ); + } + + /** + * {@inheritDoc} + */ + public function acceptSchema(Schema $schema) + { + $this->output = 'digraph "' . $schema->getName() . '" {' . "\n"; + $this->output .= 'splines = true;' . "\n"; + $this->output .= 'overlap = false;' . "\n"; + $this->output .= 'outputorder=edgesfirst;' . "\n"; + $this->output .= 'mindist = 0.6;' . "\n"; + $this->output .= 'sep = .2;' . "\n"; + } + + /** + * {@inheritDoc} + */ + public function acceptTable(Table $table) + { + $this->output .= $this->createNode( + $table->getName(), + [ + 'label' => $this->createTableLabel($table), + 'shape' => 'plaintext', + ], + ); + } + + private function createTableLabel(Table $table): string + { + // Start the table + $label = '<
'; + + // The title + $label .= ''; + + // The attributes block + foreach ($table->getColumns() as $column) { + $columnLabel = $column->getName(); + + $label .= '' + . '' + . '' + . ''; + } + + // End the table + $label .= '
' + . '' . $table->getName() . '
' + . '' . $columnLabel . '' + . '' + . '' + . strtolower($column->getType()->getName()) + . '' + . ''; + + $primaryKey = $table->getPrimaryKey(); + + if ($primaryKey !== null && in_array($column->getName(), $primaryKey->getColumns(), true)) { + $label .= "\xe2\x9c\xb7"; + } + + $label .= '
>'; + + return $label; + } + + /** + * @param string $name + * @param string[] $options + */ + private function createNode($name, $options): string + { + $node = $name . ' ['; + foreach ($options as $key => $value) { + $node .= $key . '=' . $value . ' '; + } + + $node .= "]\n"; + + return $node; + } + + /** + * @param string $node1 + * @param string $node2 + * @param string[] $options + */ + private function createNodeRelation($node1, $node2, $options): string + { + $relation = $node1 . ' -> ' . $node2 . ' ['; + foreach ($options as $key => $value) { + $relation .= $key . '=' . $value . ' '; + } + + $relation .= "]\n"; + + return $relation; + } + + /** + * Get Graphviz Output + * + * @return string + */ + public function getOutput() + { + return $this->output . '}'; + } + + /** + * Writes dot language output to a file. This should usually be a *.dot file. + * + * You have to convert the output into a viewable format. For example use "neato" on linux systems + * and execute: + * + * neato -Tpng -o er.png er.dot + * + * @param string $filename + * + * @return void + */ + public function write($filename) + { + file_put_contents($filename, $this->getOutput()); + } +} diff --git a/3rdparty/doctrine/dbal/src/Schema/Visitor/NamespaceVisitor.php b/3rdparty/doctrine/dbal/src/Schema/Visitor/NamespaceVisitor.php new file mode 100644 index 00000000..44382434 --- /dev/null +++ b/3rdparty/doctrine/dbal/src/Schema/Visitor/NamespaceVisitor.php @@ -0,0 +1,20 @@ +schema = $schema; + } + + /** + * {@inheritDoc} + */ + public function acceptTable(Table $table) + { + if ($this->schema === null) { + return; + } + + if ($table->isInDefaultNamespace($this->schema->getName())) { + return; + } + + $this->schema->dropTable($table->getName()); + } + + /** + * {@inheritDoc} + */ + public function acceptSequence(Sequence $sequence) + { + if ($this->schema === null) { + return; + } + + if ($sequence->isInDefaultNamespace($this->schema->getName())) { + return; + } + + $this->schema->dropSequence($sequence->getName()); + } + + /** + * {@inheritDoc} + */ + public function acceptForeignKey(Table $localTable, ForeignKeyConstraint $fkConstraint) + { + if ($this->schema === null) { + return; + } + + // The table may already be deleted in a previous + // RemoveNamespacedAssets#acceptTable call. Removing Foreign keys that + // point to nowhere. + if (! $this->schema->hasTable($fkConstraint->getForeignTableName())) { + $localTable->removeForeignKey($fkConstraint->getName()); + + return; + } + + $foreignTable = $this->schema->getTable($fkConstraint->getForeignTableName()); + if ($foreignTable->isInDefaultNamespace($this->schema->getName())) { + return; + } + + $localTable->removeForeignKey($fkConstraint->getName()); + } +} diff --git a/3rdparty/doctrine/dbal/src/Schema/Visitor/Visitor.php b/3rdparty/doctrine/dbal/src/Schema/Visitor/Visitor.php new file mode 100644 index 00000000..8b34864c --- /dev/null +++ b/3rdparty/doctrine/dbal/src/Schema/Visitor/Visitor.php @@ -0,0 +1,45 @@ +Statement for the given SQL and Connection. + * + * @internal The statement can be only instantiated by {@see Connection}. + * + * @param Connection $conn The connection for handling statement errors. + * @param Driver\Statement $statement The underlying driver-level statement. + * @param string $sql The SQL of the statement. + * + * @throws Exception + */ + public function __construct(Connection $conn, Driver\Statement $statement, string $sql) + { + $this->conn = $conn; + $this->stmt = $statement; + $this->sql = $sql; + $this->platform = $conn->getDatabasePlatform(); + } + + /** + * Binds a parameter value to the statement. + * + * The value can optionally be bound with a DBAL mapping type. + * If bound with a DBAL mapping type, the binding type is derived from the mapping + * type and the value undergoes the conversion routines of the mapping type before + * being bound. + * + * @param string|int $param The name or position of the parameter. + * @param mixed $value The value of the parameter. + * @param mixed $type Either a PDO binding type or a DBAL mapping type name or instance. + * + * @return bool TRUE on success, FALSE on failure. + * + * @throws Exception + */ + public function bindValue($param, $value, $type = ParameterType::STRING) + { + $this->params[$param] = $value; + $this->types[$param] = $type; + + $bindingType = ParameterType::STRING; + + if ($type !== null) { + if (is_string($type)) { + $type = Type::getType($type); + } + + $bindingType = $type; + + if ($type instanceof Type) { + $value = $type->convertToDatabaseValue($value, $this->platform); + $bindingType = $type->getBindingType(); + } + } + + try { + return $this->stmt->bindValue($param, $value, $bindingType); + } catch (Driver\Exception $e) { + throw $this->conn->convertException($e); + } + } + + /** + * Binds a parameter to a value by reference. + * + * Binding a parameter by reference does not support DBAL mapping types. + * + * @deprecated Use {@see bindValue()} instead. + * + * @param string|int $param The name or position of the parameter. + * @param mixed $variable The reference to the variable to bind. + * @param int $type The binding type. + * @param int|null $length Must be specified when using an OUT bind + * so that PHP allocates enough memory to hold the returned value. + * + * @return bool TRUE on success, FALSE on failure. + * + * @throws Exception + */ + public function bindParam($param, &$variable, $type = ParameterType::STRING, $length = null) + { + Deprecation::trigger( + 'doctrine/dbal', + 'https://github.com/doctrine/dbal/pull/5563', + '%s is deprecated. Use bindValue() instead.', + __METHOD__, + ); + + $this->params[$param] = $variable; + $this->types[$param] = $type; + + try { + if (func_num_args() > 3) { + return $this->stmt->bindParam($param, $variable, $type, $length); + } + + return $this->stmt->bindParam($param, $variable, $type); + } catch (Driver\Exception $e) { + throw $this->conn->convertException($e); + } + } + + /** + * Executes the statement with the currently bound parameters. + * + * @deprecated Statement::execute() is deprecated, use Statement::executeQuery() or executeStatement() instead + * + * @param mixed[]|null $params + * + * @throws Exception + */ + public function execute($params = null): Result + { + Deprecation::triggerIfCalledFromOutside( + 'doctrine/dbal', + 'https://github.com/doctrine/dbal/pull/4580', + '%s() is deprecated, use Statement::executeQuery() or Statement::executeStatement() instead', + __METHOD__, + ); + + if ($params !== null) { + $this->params = $params; + } + + $logger = $this->conn->getConfiguration()->getSQLLogger(); + if ($logger !== null) { + $logger->startQuery($this->sql, $this->params, $this->types); + } + + try { + return new Result( + $this->stmt->execute($params), + $this->conn, + ); + } catch (Driver\Exception $ex) { + throw $this->conn->convertExceptionDuringQuery($ex, $this->sql, $this->params, $this->types); + } finally { + if ($logger !== null) { + $logger->stopQuery(); + } + } + } + + /** + * Executes the statement with the currently bound parameters and return result. + * + * @param mixed[] $params + * + * @throws Exception + */ + public function executeQuery(array $params = []): Result + { + if (func_num_args() > 0) { + Deprecation::trigger( + 'doctrine/dbal', + 'https://github.com/doctrine/dbal/pull/5556', + 'Passing $params to Statement::executeQuery() is deprecated. Bind parameters using' + . ' Statement::bindParam() or Statement::bindValue() instead.', + ); + } + + if ($params === []) { + $params = null; // Workaround as long execute() exists and used internally. + } + + return $this->execute($params); + } + + /** + * Executes the statement with the currently bound parameters and return affected rows. + * + * @param mixed[] $params + * + * @throws Exception + */ + public function executeStatement(array $params = []): int + { + if (func_num_args() > 0) { + Deprecation::trigger( + 'doctrine/dbal', + 'https://github.com/doctrine/dbal/pull/5556', + 'Passing $params to Statement::executeStatement() is deprecated. Bind parameters using' + . ' Statement::bindParam() or Statement::bindValue() instead.', + ); + } + + if ($params === []) { + $params = null; // Workaround as long execute() exists and used internally. + } + + return $this->execute($params)->rowCount(); + } + + /** + * Gets the wrapped driver statement. + * + * @return Driver\Statement + */ + public function getWrappedStatement() + { + return $this->stmt; + } +} diff --git a/3rdparty/doctrine/dbal/src/Tools/Console/Command/CommandCompatibility.php b/3rdparty/doctrine/dbal/src/Tools/Console/Command/CommandCompatibility.php new file mode 100644 index 00000000..0ef081c7 --- /dev/null +++ b/3rdparty/doctrine/dbal/src/Tools/Console/Command/CommandCompatibility.php @@ -0,0 +1,63 @@ +hasReturnType()) { + /** @internal */ + trait CommandCompatibility + { + protected function configure(): void + { + $this->doConfigure(); + } + + protected function execute(InputInterface $input, OutputInterface $output): int + { + return $this->doExecute($input, $output); + } + } +// Symfony 7 +} elseif ((new ReflectionMethod(Command::class, 'execute'))->hasReturnType()) { + /** @internal */ + trait CommandCompatibility + { + /** @return void */ + protected function configure() + { + $this->doConfigure(); + } + + protected function execute(InputInterface $input, OutputInterface $output): int + { + return $this->doExecute($input, $output); + } + } +} else { + /** @internal */ + trait CommandCompatibility + { + /** @return void */ + protected function configure() + { + $this->doConfigure(); + } + + /** + * {@inheritDoc} + * + * @return int + */ + protected function execute(InputInterface $input, OutputInterface $output) + { + return $this->doExecute($input, $output); + } + } +} diff --git a/3rdparty/doctrine/dbal/src/Tools/Console/Command/ReservedWordsCommand.php b/3rdparty/doctrine/dbal/src/Tools/Console/Command/ReservedWordsCommand.php new file mode 100644 index 00000000..238af5ed --- /dev/null +++ b/3rdparty/doctrine/dbal/src/Tools/Console/Command/ReservedWordsCommand.php @@ -0,0 +1,221 @@ + */ + private array $keywordLists; + + private ConnectionProvider $connectionProvider; + + public function __construct(ConnectionProvider $connectionProvider) + { + Deprecation::triggerIfCalledFromOutside( + 'doctrine/dbal', + 'https://github.com/doctrine/dbal/pull/5431', + 'ReservedWordsCommand is deprecated. Use database documentation instead.', + ); + + parent::__construct(); + + $this->connectionProvider = $connectionProvider; + + $this->keywordLists = [ + 'db2' => new DB2Keywords(), + 'mariadb102' => new MariaDb102Keywords(), + 'mariadb117' => new MariaDb117Keywords(), + 'mysql' => new MySQLKeywords(), + 'mysql57' => new MySQL57Keywords(), + 'mysql80' => new MySQL80Keywords(), + 'mysql84' => new MySQL84Keywords(), + 'oracle' => new OracleKeywords(), + 'pgsql' => new PostgreSQL94Keywords(), + 'pgsql100' => new PostgreSQL100Keywords(), + 'sqlite' => new SQLiteKeywords(), + 'sqlserver' => new SQLServer2012Keywords(), + ]; + } + + /** + * Add or replace a keyword list. + */ + public function setKeywordList(string $name, KeywordList $keywordList): void + { + $this->keywordLists[$name] = $keywordList; + } + + /** + * If you want to add or replace a keywords list use this command. + * + * @param string $name + * @param class-string $class + * + * @return void + */ + public function setKeywordListClass($name, $class) + { + Deprecation::trigger( + 'doctrine/dbal', + 'https://github.com/doctrine/dbal/issues/4510', + 'ReservedWordsCommand::setKeywordListClass() is deprecated,' + . ' use ReservedWordsCommand::setKeywordList() instead.', + ); + + $this->keywordLists[$name] = new $class(); + } + + private function doConfigure(): void + { + $this + ->setName('dbal:reserved-words') + ->setDescription('Checks if the current database contains identifiers that are reserved.') + ->setDefinition([ + new InputOption('connection', null, InputOption::VALUE_REQUIRED, 'The named database connection'), + new InputOption( + 'list', + 'l', + InputOption::VALUE_OPTIONAL | InputOption::VALUE_IS_ARRAY, + 'Keyword-List name.', + ), + ]) + ->setHelp(<<<'EOT' +Checks if the current database contains tables and columns +with names that are identifiers in this dialect or in other SQL dialects. + +By default all supported platform keywords are checked: + + %command.full_name% + +If you want to check against specific dialects you can +pass them to the command: + + %command.full_name% -l mysql -l pgsql + +The following keyword lists are currently shipped with Doctrine: + + * db2 + * mariadb102 + * mariadb117 + * mysql + * mysql57 + * mysql80 + * mysql84 + * oracle + * pgsql + * pgsql100 + * sqlite + * sqlserver +EOT); + } + + /** @throws Exception */ + private function doExecute(InputInterface $input, OutputInterface $output): int + { + $output->writeln( + 'The dbal:reserved-words command is deprecated.' + . ' Use the documentation on the used database platform(s) instead.', + ); + $output->writeln(''); + + $conn = $this->getConnection($input); + + $keywordLists = $input->getOption('list'); + + if (is_string($keywordLists)) { + $keywordLists = [$keywordLists]; + } elseif (! is_array($keywordLists)) { + $keywordLists = []; + } + + if (count($keywordLists) === 0) { + $keywordLists = array_keys($this->keywordLists); + } + + $keywords = []; + foreach ($keywordLists as $keywordList) { + if (! isset($this->keywordLists[$keywordList])) { + throw new InvalidArgumentException( + "There exists no keyword list with name '" . $keywordList . "'. " . + 'Known lists: ' . implode(', ', array_keys($this->keywordLists)), + ); + } + + $keywords[] = $this->keywordLists[$keywordList]; + } + + $output->write( + 'Checking keyword violations for ' . implode(', ', $keywordLists) . '...', + true, + ); + + $schema = $conn->getSchemaManager()->introspectSchema(); + $visitor = new ReservedKeywordsValidator($keywords); + $schema->visit($visitor); + + $violations = $visitor->getViolations(); + if (count($violations) !== 0) { + $output->write( + 'There are ' . count($violations) . ' reserved keyword violations' + . ' in your database schema:', + true, + ); + + foreach ($violations as $violation) { + $output->write(' - ' . $violation, true); + } + + return 1; + } + + $output->write('No reserved keywords violations have been found!', true); + + return 0; + } + + private function getConnection(InputInterface $input): Connection + { + $connectionName = $input->getOption('connection'); + assert(is_string($connectionName) || $connectionName === null); + + if ($connectionName !== null) { + return $this->connectionProvider->getConnection($connectionName); + } + + return $this->connectionProvider->getDefaultConnection(); + } +} diff --git a/3rdparty/doctrine/dbal/src/Tools/Console/Command/RunSqlCommand.php b/3rdparty/doctrine/dbal/src/Tools/Console/Command/RunSqlCommand.php new file mode 100644 index 00000000..64913be6 --- /dev/null +++ b/3rdparty/doctrine/dbal/src/Tools/Console/Command/RunSqlCommand.php @@ -0,0 +1,119 @@ +connectionProvider = $connectionProvider; + } + + private function doConfigure(): void + { + $this + ->setName('dbal:run-sql') + ->setDescription('Executes arbitrary SQL directly from the command line.') + ->setDefinition([ + new InputOption('connection', null, InputOption::VALUE_REQUIRED, 'The named database connection'), + new InputArgument('sql', InputArgument::REQUIRED, 'The SQL statement to execute.'), + new InputOption('depth', null, InputOption::VALUE_REQUIRED, 'Dumping depth of result set (deprecated).'), + new InputOption('force-fetch', null, InputOption::VALUE_NONE, 'Forces fetching the result.'), + ]) + ->setHelp(<<<'EOT' +The %command.name% command executes the given SQL query and +outputs the results: + +php %command.full_name% "SELECT * FROM users" +EOT); + } + + /** @throws Exception */ + private function doExecute(InputInterface $input, OutputInterface $output): int + { + $conn = $this->getConnection($input); + $io = new SymfonyStyle($input, $output); + + $sql = $input->getArgument('sql'); + + if ($sql === null) { + throw new RuntimeException("Argument 'SQL' is required in order to execute this command correctly."); + } + + assert(is_string($sql)); + + if ($input->getOption('depth') !== null) { + $io->warning('Parameter "depth" is deprecated and has no effect anymore.'); + } + + $forceFetch = $input->getOption('force-fetch'); + assert(is_bool($forceFetch)); + + if (stripos($sql, 'select') === 0 || $forceFetch) { + $this->runQuery($io, $conn, $sql); + } else { + $this->runStatement($io, $conn, $sql); + } + + return 0; + } + + private function getConnection(InputInterface $input): Connection + { + $connectionName = $input->getOption('connection'); + assert(is_string($connectionName) || $connectionName === null); + + if ($connectionName !== null) { + return $this->connectionProvider->getConnection($connectionName); + } + + return $this->connectionProvider->getDefaultConnection(); + } + + /** @throws Exception */ + private function runQuery(SymfonyStyle $io, Connection $conn, string $sql): void + { + $resultSet = $conn->fetchAllAssociative($sql); + if ($resultSet === []) { + $io->success('The query yielded an empty result set.'); + + return; + } + + $io->table(array_keys($resultSet[0]), $resultSet); + } + + /** @throws Exception */ + private function runStatement(SymfonyStyle $io, Connection $conn, string $sql): void + { + $io->success(sprintf('%d rows affected.', $conn->executeStatement($sql))); + } +} diff --git a/3rdparty/doctrine/dbal/src/Tools/Console/ConnectionNotFound.php b/3rdparty/doctrine/dbal/src/Tools/Console/ConnectionNotFound.php new file mode 100644 index 00000000..81ca4182 --- /dev/null +++ b/3rdparty/doctrine/dbal/src/Tools/Console/ConnectionNotFound.php @@ -0,0 +1,9 @@ +connection = $connection; + $this->defaultConnectionName = $defaultConnectionName; + } + + public function getDefaultConnection(): Connection + { + return $this->connection; + } + + public function getConnection(string $name): Connection + { + if ($name !== $this->defaultConnectionName) { + throw new ConnectionNotFound(sprintf('Connection with name "%s" does not exist.', $name)); + } + + return $this->connection; + } +} diff --git a/3rdparty/doctrine/dbal/src/Tools/Console/ConsoleRunner.php b/3rdparty/doctrine/dbal/src/Tools/Console/ConsoleRunner.php new file mode 100644 index 00000000..e8fa3c60 --- /dev/null +++ b/3rdparty/doctrine/dbal/src/Tools/Console/ConsoleRunner.php @@ -0,0 +1,81 @@ +setCatchExceptions(true); + self::addCommands($cli, $connectionProvider); + $cli->addCommands($commands); + $cli->run(); + } + + /** @return void */ + public static function addCommands(Application $cli, ConnectionProvider $connectionProvider) + { + $cli->addCommands([ + new RunSqlCommand($connectionProvider), + new ReservedWordsCommand($connectionProvider), + ]); + } + + /** + * Prints the instructions to create a configuration file + * + * @deprecated This method will be removed without replacement. + * + * @return void + */ + public static function printCliConfigTemplate() + { + echo <<<'HELP' +You are missing a "cli-config.php" or "config/cli-config.php" file in your +project, which is required to get the Doctrine-DBAL Console working. You can use the +following sample as a template: + +> */ + private array $schemeMapping; + + /** @param array> $schemeMapping An array used to map DSN schemes to DBAL drivers */ + public function __construct(array $schemeMapping = []) + { + $this->schemeMapping = $schemeMapping; + } + + /** + * @phpstan-return Params + * + * @throws MalformedDsnException + */ + public function parse( + #[SensitiveParameter] + string $dsn + ): array { + // (pdo-)?sqlite3?:///... => (pdo-)?sqlite3?://localhost/... or else the URL will be invalid + $url = preg_replace('#^((?:pdo-)?sqlite3?):///#', '$1://localhost/', $dsn); + assert($url !== null); + + $url = parse_url($url); + + if ($url === false) { + throw MalformedDsnException::new(); + } + + foreach ($url as $param => $value) { + if (! is_string($value)) { + continue; + } + + $url[$param] = rawurldecode($value); + } + + $params = []; + + if (isset($url['scheme'])) { + $params['driver'] = $this->parseDatabaseUrlScheme($url['scheme']); + } + + if (isset($url['host'])) { + $params['host'] = $url['host']; + } + + if (isset($url['port'])) { + $params['port'] = $url['port']; + } + + if (isset($url['user'])) { + $params['user'] = $url['user']; + } + + if (isset($url['pass'])) { + $params['password'] = $url['pass']; + } + + if (isset($params['driver']) && is_a($params['driver'], Driver::class, true)) { + $params['driverClass'] = $params['driver']; + unset($params['driver']); + } + + $params = $this->parseDatabaseUrlPath($url, $params); + $params = $this->parseDatabaseUrlQuery($url, $params); + + return $params; + } + + /** + * Parses the given connection URL and resolves the given connection parameters. + * + * Assumes that the connection URL scheme is already parsed and resolved into the given connection parameters + * via {@see parseDatabaseUrlScheme}. + * + * @see parseDatabaseUrlScheme + * + * @param mixed[] $url The URL parts to evaluate. + * @param mixed[] $params The connection parameters to resolve. + * + * @return mixed[] The resolved connection parameters. + */ + private function parseDatabaseUrlPath(array $url, array $params): array + { + if (! isset($url['path'])) { + return $params; + } + + $url['path'] = $this->normalizeDatabaseUrlPath($url['path']); + + // If we do not have a known DBAL driver, we do not know any connection URL path semantics to evaluate + // and therefore treat the path as a regular DBAL connection URL path. + if (! isset($params['driver'])) { + return $this->parseRegularDatabaseUrlPath($url, $params); + } + + if (strpos($params['driver'], 'sqlite') !== false) { + return $this->parseSqliteDatabaseUrlPath($url, $params); + } + + return $this->parseRegularDatabaseUrlPath($url, $params); + } + + /** + * Normalizes the given connection URL path. + * + * @return string The normalized connection URL path + */ + private function normalizeDatabaseUrlPath(string $urlPath): string + { + // Trim leading slash from URL path. + return substr($urlPath, 1); + } + + /** + * Parses the query part of the given connection URL and resolves the given connection parameters. + * + * @param mixed[] $url The connection URL parts to evaluate. + * @param mixed[] $params The connection parameters to resolve. + * + * @return mixed[] The resolved connection parameters. + */ + private function parseDatabaseUrlQuery(array $url, array $params): array + { + if (! isset($url['query'])) { + return $params; + } + + $query = []; + + parse_str($url['query'], $query); // simply ingest query as extra params, e.g. charset or sslmode + + return array_merge($params, $query); // parse_str wipes existing array elements + } + + /** + * Parses the given regular connection URL and resolves the given connection parameters. + * + * Assumes that the "path" URL part is already normalized via {@see normalizeDatabaseUrlPath}. + * + * @see normalizeDatabaseUrlPath + * + * @param mixed[] $url The regular connection URL parts to evaluate. + * @param mixed[] $params The connection parameters to resolve. + * + * @return mixed[] The resolved connection parameters. + */ + private function parseRegularDatabaseUrlPath(array $url, array $params): array + { + $params['dbname'] = $url['path']; + + return $params; + } + + /** + * Parses the given SQLite connection URL and resolves the given connection parameters. + * + * Assumes that the "path" URL part is already normalized via {@see normalizeDatabaseUrlPath}. + * + * @see normalizeDatabaseUrlPath + * + * @param mixed[] $url The SQLite connection URL parts to evaluate. + * @param mixed[] $params The connection parameters to resolve. + * + * @return mixed[] The resolved connection parameters. + */ + private function parseSqliteDatabaseUrlPath(array $url, array $params): array + { + if ($url['path'] === ':memory:') { + $params['memory'] = true; + + return $params; + } + + $params['path'] = $url['path']; // pdo_sqlite driver uses 'path' instead of 'dbname' key + + return $params; + } + + /** + * Parses the scheme part from given connection URL and resolves the given connection parameters. + * + * @return string The resolved driver. + */ + private function parseDatabaseUrlScheme(string $scheme): string + { + // URL schemes must not contain underscores, but dashes are ok + $driver = str_replace('-', '_', $scheme); + + // If the driver is an alias (e.g. "postgres"), map it to the actual name ("pdo-pgsql"). + // Otherwise, let checkParams decide later if the driver exists. + return $this->schemeMapping[$driver] ?? $driver; + } +} diff --git a/3rdparty/doctrine/dbal/src/TransactionIsolationLevel.php b/3rdparty/doctrine/dbal/src/TransactionIsolationLevel.php new file mode 100644 index 00000000..9020343a --- /dev/null +++ b/3rdparty/doctrine/dbal/src/TransactionIsolationLevel.php @@ -0,0 +1,33 @@ +getClobTypeDeclarationSQL($column); + } + + /** + * {@inheritDoc} + */ + public function convertToDatabaseValue($value, AbstractPlatform $platform) + { + return serialize($value); + } + + /** + * {@inheritDoc} + */ + public function convertToPHPValue($value, AbstractPlatform $platform) + { + if ($value === null) { + return null; + } + + $value = is_resource($value) ? stream_get_contents($value) : $value; + + set_error_handler(function (int $code, string $message): bool { + if ($code === E_DEPRECATED || $code === E_USER_DEPRECATED) { + return false; + } + + throw ConversionException::conversionFailedUnserialization($this->getName(), $message); + }); + + try { + return unserialize($value); + } finally { + restore_error_handler(); + } + } + + /** + * {@inheritDoc} + */ + public function getName() + { + return Types::ARRAY; + } + + /** + * {@inheritDoc} + * + * @deprecated + */ + public function requiresSQLCommentHint(AbstractPlatform $platform) + { + Deprecation::triggerIfCalledFromOutside( + 'doctrine/dbal', + 'https://github.com/doctrine/dbal/pull/5509', + '%s is deprecated.', + __METHOD__, + ); + + return true; + } +} diff --git a/3rdparty/doctrine/dbal/src/Types/AsciiStringType.php b/3rdparty/doctrine/dbal/src/Types/AsciiStringType.php new file mode 100644 index 00000000..4ea92d97 --- /dev/null +++ b/3rdparty/doctrine/dbal/src/Types/AsciiStringType.php @@ -0,0 +1,29 @@ +getAsciiStringTypeDeclarationSQL($column); + } + + public function getBindingType(): int + { + return ParameterType::ASCII; + } + + public function getName(): string + { + return Types::ASCII_STRING; + } +} diff --git a/3rdparty/doctrine/dbal/src/Types/BigIntType.php b/3rdparty/doctrine/dbal/src/Types/BigIntType.php new file mode 100644 index 00000000..8d57a112 --- /dev/null +++ b/3rdparty/doctrine/dbal/src/Types/BigIntType.php @@ -0,0 +1,50 @@ +getBigIntTypeDeclarationSQL($column); + } + + /** + * {@inheritDoc} + */ + public function getBindingType() + { + return ParameterType::STRING; + } + + /** + * {@inheritDoc} + * + * @param T $value + * + * @return (T is null ? null : string) + * + * @template T + */ + public function convertToPHPValue($value, AbstractPlatform $platform) + { + return $value === null ? null : (string) $value; + } +} diff --git a/3rdparty/doctrine/dbal/src/Types/BinaryType.php b/3rdparty/doctrine/dbal/src/Types/BinaryType.php new file mode 100644 index 00000000..acbbd87a --- /dev/null +++ b/3rdparty/doctrine/dbal/src/Types/BinaryType.php @@ -0,0 +1,67 @@ +getBinaryTypeDeclarationSQL($column); + } + + /** + * {@inheritDoc} + */ + public function convertToPHPValue($value, AbstractPlatform $platform) + { + if ($value === null) { + return null; + } + + if (is_string($value)) { + $fp = fopen('php://temp', 'rb+'); + assert(is_resource($fp)); + fwrite($fp, $value); + fseek($fp, 0); + $value = $fp; + } + + if (! is_resource($value)) { + throw ConversionException::conversionFailed($value, Types::BINARY); + } + + return $value; + } + + /** + * {@inheritDoc} + */ + public function getName() + { + return Types::BINARY; + } + + /** + * {@inheritDoc} + */ + public function getBindingType() + { + return ParameterType::BINARY; + } +} diff --git a/3rdparty/doctrine/dbal/src/Types/BlobType.php b/3rdparty/doctrine/dbal/src/Types/BlobType.php new file mode 100644 index 00000000..cfaabec9 --- /dev/null +++ b/3rdparty/doctrine/dbal/src/Types/BlobType.php @@ -0,0 +1,67 @@ +getBlobTypeDeclarationSQL($column); + } + + /** + * {@inheritDoc} + */ + public function convertToPHPValue($value, AbstractPlatform $platform) + { + if ($value === null) { + return null; + } + + if (is_string($value)) { + $fp = fopen('php://temp', 'rb+'); + assert(is_resource($fp)); + fwrite($fp, $value); + fseek($fp, 0); + $value = $fp; + } + + if (! is_resource($value)) { + throw ConversionException::conversionFailed($value, Types::BLOB); + } + + return $value; + } + + /** + * {@inheritDoc} + */ + public function getName() + { + return Types::BLOB; + } + + /** + * {@inheritDoc} + */ + public function getBindingType() + { + return ParameterType::LARGE_OBJECT; + } +} diff --git a/3rdparty/doctrine/dbal/src/Types/BooleanType.php b/3rdparty/doctrine/dbal/src/Types/BooleanType.php new file mode 100644 index 00000000..7dc7f3a9 --- /dev/null +++ b/3rdparty/doctrine/dbal/src/Types/BooleanType.php @@ -0,0 +1,79 @@ +getBooleanTypeDeclarationSQL($column); + } + + /** + * {@inheritDoc} + */ + public function convertToDatabaseValue($value, AbstractPlatform $platform) + { + return $platform->convertBooleansToDatabaseValue($value); + } + + /** + * {@inheritDoc} + * + * @param T $value + * + * @return (T is null ? null : bool) + * + * @template T + */ + public function convertToPHPValue($value, AbstractPlatform $platform) + { + return $platform->convertFromBoolean($value); + } + + /** + * {@inheritDoc} + */ + public function getName() + { + return Types::BOOLEAN; + } + + /** + * {@inheritDoc} + */ + public function getBindingType() + { + return ParameterType::BOOLEAN; + } + + /** + * @deprecated + * + * @return bool + */ + public function requiresSQLCommentHint(AbstractPlatform $platform) + { + Deprecation::triggerIfCalledFromOutside( + 'doctrine/dbal', + 'https://github.com/doctrine/dbal/pull/5509', + '%s is deprecated.', + __METHOD__, + ); + + // We require a commented boolean type in order to distinguish between + // boolean and smallint as both (have to) map to the same native type. + return $platform instanceof DB2Platform; + } +} diff --git a/3rdparty/doctrine/dbal/src/Types/ConversionException.php b/3rdparty/doctrine/dbal/src/Types/ConversionException.php new file mode 100644 index 00000000..2401aaeb --- /dev/null +++ b/3rdparty/doctrine/dbal/src/Types/ConversionException.php @@ -0,0 +1,121 @@ + 32 ? substr($value, 0, 20) . '...' : $value; + + return new self('Could not convert database value "' . $value . '" to Doctrine Type ' . $toType, 0, $previous); + } + + /** + * Thrown when a Database to Doctrine Type Conversion fails and we can make a statement + * about the expected format. + * + * @param mixed $value + * @param string $toType + * @param string $expectedFormat + * + * @return ConversionException + */ + public static function conversionFailedFormat($value, $toType, $expectedFormat, ?Throwable $previous = null) + { + $value = strlen($value) > 32 ? substr($value, 0, 20) . '...' : $value; + + return new self( + 'Could not convert database value "' . $value . '" to Doctrine Type ' . + $toType . '. Expected format: ' . $expectedFormat, + 0, + $previous, + ); + } + + /** + * Thrown when the PHP value passed to the converter was not of the expected type. + * + * @param mixed $value + * @param string $toType + * @param string[] $possibleTypes + * + * @return ConversionException + */ + public static function conversionFailedInvalidType( + $value, + $toType, + array $possibleTypes, + ?Throwable $previous = null + ) { + if (is_scalar($value) || $value === null) { + return new self(sprintf( + 'Could not convert PHP value %s to type %s. Expected one of the following types: %s', + var_export($value, true), + $toType, + implode(', ', $possibleTypes), + ), 0, $previous); + } + + return new self(sprintf( + 'Could not convert PHP value of type %s to type %s. Expected one of the following types: %s', + is_object($value) ? get_class($value) : gettype($value), + $toType, + implode(', ', $possibleTypes), + ), 0, $previous); + } + + /** + * @param mixed $value + * @param string $format + * @param string $error + * + * @return ConversionException + */ + public static function conversionFailedSerialization($value, $format, $error /*, ?Throwable $previous = null */) + { + $actualType = is_object($value) ? get_class($value) : gettype($value); + + return new self(sprintf( + "Could not convert PHP type '%s' to '%s', as an '%s' error was triggered by the serialization", + $actualType, + $format, + $error, + ), 0, func_num_args() >= 4 ? func_get_arg(3) : null); + } + + public static function conversionFailedUnserialization(string $format, string $error): self + { + return new self(sprintf( + "Could not convert database value to '%s' as an error was triggered by the unserialization: '%s'", + $format, + $error, + )); + } +} diff --git a/3rdparty/doctrine/dbal/src/Types/DateImmutableType.php b/3rdparty/doctrine/dbal/src/Types/DateImmutableType.php new file mode 100644 index 00000000..da96b69d --- /dev/null +++ b/3rdparty/doctrine/dbal/src/Types/DateImmutableType.php @@ -0,0 +1,92 @@ +format($platform->getDateFormatString()); + } + + throw ConversionException::conversionFailedInvalidType( + $value, + $this->getName(), + ['null', DateTimeImmutable::class], + ); + } + + /** + * {@inheritDoc} + * + * @param T $value + * + * @return (T is null ? null : DateTimeImmutable) + * + * @template T + */ + public function convertToPHPValue($value, AbstractPlatform $platform) + { + if ($value === null || $value instanceof DateTimeImmutable) { + return $value; + } + + $dateTime = DateTimeImmutable::createFromFormat('!' . $platform->getDateFormatString(), $value); + + if ($dateTime === false) { + throw ConversionException::conversionFailedFormat( + $value, + $this->getName(), + $platform->getDateFormatString(), + ); + } + + return $dateTime; + } + + /** + * {@inheritDoc} + * + * @deprecated + */ + public function requiresSQLCommentHint(AbstractPlatform $platform) + { + Deprecation::triggerIfCalledFromOutside( + 'doctrine/dbal', + 'https://github.com/doctrine/dbal/pull/5509', + '%s is deprecated.', + __METHOD__, + ); + + return true; + } +} diff --git a/3rdparty/doctrine/dbal/src/Types/DateIntervalType.php b/3rdparty/doctrine/dbal/src/Types/DateIntervalType.php new file mode 100644 index 00000000..1630dc55 --- /dev/null +++ b/3rdparty/doctrine/dbal/src/Types/DateIntervalType.php @@ -0,0 +1,110 @@ +getStringTypeDeclarationSQL($column); + } + + /** + * {@inheritDoc} + * + * @param T $value + * + * @return (T is null ? null : string) + * + * @template T + */ + public function convertToDatabaseValue($value, AbstractPlatform $platform) + { + if ($value === null) { + return null; + } + + if ($value instanceof DateInterval) { + return $value->format(self::FORMAT); + } + + throw ConversionException::conversionFailedInvalidType($value, $this->getName(), ['null', DateInterval::class]); + } + + /** + * {@inheritDoc} + * + * @param T $value + * + * @return (T is null ? null : DateInterval) + * + * @template T + */ + public function convertToPHPValue($value, AbstractPlatform $platform) + { + if ($value === null || $value instanceof DateInterval) { + return $value; + } + + $negative = false; + + if (isset($value[0]) && ($value[0] === '+' || $value[0] === '-')) { + $negative = $value[0] === '-'; + $value = substr($value, 1); + } + + try { + $interval = new DateInterval($value); + + if ($negative) { + $interval->invert = 1; + } + + return $interval; + } catch (Throwable $exception) { + throw ConversionException::conversionFailedFormat($value, $this->getName(), self::FORMAT, $exception); + } + } + + /** + * {@inheritDoc} + * + * @deprecated + */ + public function requiresSQLCommentHint(AbstractPlatform $platform) + { + Deprecation::triggerIfCalledFromOutside( + 'doctrine/dbal', + 'https://github.com/doctrine/dbal/pull/5509', + '%s is deprecated.', + __METHOD__, + ); + + return true; + } +} diff --git a/3rdparty/doctrine/dbal/src/Types/DateTimeImmutableType.php b/3rdparty/doctrine/dbal/src/Types/DateTimeImmutableType.php new file mode 100644 index 00000000..a8c7fec9 --- /dev/null +++ b/3rdparty/doctrine/dbal/src/Types/DateTimeImmutableType.php @@ -0,0 +1,98 @@ +format($platform->getDateTimeFormatString()); + } + + throw ConversionException::conversionFailedInvalidType( + $value, + $this->getName(), + ['null', DateTimeImmutable::class], + ); + } + + /** + * {@inheritDoc} + * + * @param T $value + * + * @return (T is null ? null : DateTimeImmutable) + * + * @template T + */ + public function convertToPHPValue($value, AbstractPlatform $platform) + { + if ($value === null || $value instanceof DateTimeImmutable) { + return $value; + } + + $dateTime = DateTimeImmutable::createFromFormat($platform->getDateTimeFormatString(), $value); + + if ($dateTime !== false) { + return $dateTime; + } + + try { + return new DateTimeImmutable($value); + } catch (Exception $e) { + throw ConversionException::conversionFailedFormat( + $value, + $this->getName(), + $platform->getDateTimeFormatString(), + $e, + ); + } + } + + /** + * {@inheritDoc} + * + * @deprecated + */ + public function requiresSQLCommentHint(AbstractPlatform $platform) + { + Deprecation::triggerIfCalledFromOutside( + 'doctrine/dbal', + 'https://github.com/doctrine/dbal/pull/5509', + '%s is deprecated.', + __METHOD__, + ); + + return true; + } +} diff --git a/3rdparty/doctrine/dbal/src/Types/DateTimeType.php b/3rdparty/doctrine/dbal/src/Types/DateTimeType.php new file mode 100644 index 00000000..3ff592ae --- /dev/null +++ b/3rdparty/doctrine/dbal/src/Types/DateTimeType.php @@ -0,0 +1,115 @@ +getDateTimeTypeDeclarationSQL($column); + } + + /** + * {@inheritDoc} + * + * @param T $value + * + * @return (T is null ? null : string) + * + * @template T + */ + public function convertToDatabaseValue($value, AbstractPlatform $platform) + { + if ($value === null) { + return $value; + } + + if ($value instanceof DateTimeImmutable) { + Deprecation::triggerIfCalledFromOutside( + 'doctrine/dbal', + 'https://github.com/doctrine/dbal/pull/6017', + 'Passing an instance of %s is deprecated, use %s::%s() instead.', + get_class($value), + DateTimeImmutableType::class, + __FUNCTION__, + ); + } + + if ($value instanceof DateTimeInterface) { + return $value->format($platform->getDateTimeFormatString()); + } + + throw ConversionException::conversionFailedInvalidType( + $value, + $this->getName(), + ['null', DateTime::class, DateTimeImmutable::class], + ); + } + + /** + * {@inheritDoc} + * + * @param T $value + * + * @return (T is null ? null : DateTimeInterface) + * + * @template T + */ + public function convertToPHPValue($value, AbstractPlatform $platform) + { + if ($value instanceof DateTimeImmutable) { + Deprecation::triggerIfCalledFromOutside( + 'doctrine/dbal', + 'https://github.com/doctrine/dbal/pull/6017', + 'Passing an instance of %s is deprecated, use %s::%s() instead.', + get_class($value), + DateTimeImmutableType::class, + __FUNCTION__, + ); + } + + if ($value === null || $value instanceof DateTimeInterface) { + return $value; + } + + $dateTime = DateTime::createFromFormat($platform->getDateTimeFormatString(), $value); + + if ($dateTime !== false) { + return $dateTime; + } + + try { + return new DateTime($value); + } catch (Exception $e) { + throw ConversionException::conversionFailedFormat( + $value, + $this->getName(), + $platform->getDateTimeFormatString(), + $e, + ); + } + } +} diff --git a/3rdparty/doctrine/dbal/src/Types/DateTimeTzImmutableType.php b/3rdparty/doctrine/dbal/src/Types/DateTimeTzImmutableType.php new file mode 100644 index 00000000..70f2c78b --- /dev/null +++ b/3rdparty/doctrine/dbal/src/Types/DateTimeTzImmutableType.php @@ -0,0 +1,92 @@ +format($platform->getDateTimeTzFormatString()); + } + + throw ConversionException::conversionFailedInvalidType( + $value, + $this->getName(), + ['null', DateTimeImmutable::class], + ); + } + + /** + * {@inheritDoc} + * + * @param T $value + * + * @return (T is null ? null : DateTimeImmutable) + * + * @template T + */ + public function convertToPHPValue($value, AbstractPlatform $platform) + { + if ($value === null || $value instanceof DateTimeImmutable) { + return $value; + } + + $dateTime = DateTimeImmutable::createFromFormat($platform->getDateTimeTzFormatString(), $value); + + if ($dateTime !== false) { + return $dateTime; + } + + throw ConversionException::conversionFailedFormat( + $value, + $this->getName(), + $platform->getDateTimeTzFormatString(), + ); + } + + /** + * {@inheritDoc} + * + * @deprecated + */ + public function requiresSQLCommentHint(AbstractPlatform $platform) + { + Deprecation::triggerIfCalledFromOutside( + 'doctrine/dbal', + 'https://github.com/doctrine/dbal/pull/5509', + '%s is deprecated.', + __METHOD__, + ); + + return true; + } +} diff --git a/3rdparty/doctrine/dbal/src/Types/DateTimeTzType.php b/3rdparty/doctrine/dbal/src/Types/DateTimeTzType.php new file mode 100644 index 00000000..b3b5db81 --- /dev/null +++ b/3rdparty/doctrine/dbal/src/Types/DateTimeTzType.php @@ -0,0 +1,122 @@ +getDateTimeTzTypeDeclarationSQL($column); + } + + /** + * {@inheritDoc} + * + * @param T $value + * + * @return (T is null ? null : string) + * + * @template T + */ + public function convertToDatabaseValue($value, AbstractPlatform $platform) + { + if ($value === null) { + return $value; + } + + if ($value instanceof DateTimeImmutable) { + Deprecation::triggerIfCalledFromOutside( + 'doctrine/dbal', + 'https://github.com/doctrine/dbal/pull/6017', + 'Passing an instance of %s is deprecated, use %s::%s() instead.', + get_class($value), + DateTimeTzImmutableType::class, + __FUNCTION__, + ); + } + + if ($value instanceof DateTimeInterface) { + return $value->format($platform->getDateTimeTzFormatString()); + } + + throw ConversionException::conversionFailedInvalidType( + $value, + $this->getName(), + ['null', DateTime::class], + ); + } + + /** + * {@inheritDoc} + * + * @param T $value + * + * @return (T is null ? null : DateTimeInterface) + * + * @template T + */ + public function convertToPHPValue($value, AbstractPlatform $platform) + { + if ($value instanceof DateTimeImmutable) { + Deprecation::triggerIfCalledFromOutside( + 'doctrine/dbal', + 'https://github.com/doctrine/dbal/pull/6017', + 'Passing an instance of %s is deprecated, use %s::%s() instead.', + get_class($value), + DateTimeTzImmutableType::class, + __FUNCTION__, + ); + } + + if ($value === null || $value instanceof DateTimeInterface) { + return $value; + } + + $dateTime = DateTime::createFromFormat($platform->getDateTimeTzFormatString(), $value); + if ($dateTime !== false) { + return $dateTime; + } + + throw ConversionException::conversionFailedFormat( + $value, + $this->getName(), + $platform->getDateTimeTzFormatString(), + ); + } +} diff --git a/3rdparty/doctrine/dbal/src/Types/DateType.php b/3rdparty/doctrine/dbal/src/Types/DateType.php new file mode 100644 index 00000000..86a5ab1e --- /dev/null +++ b/3rdparty/doctrine/dbal/src/Types/DateType.php @@ -0,0 +1,104 @@ +getDateTypeDeclarationSQL($column); + } + + /** + * {@inheritDoc} + * + * @phpstan-param T $value + * + * @return (T is null ? null : string) + * + * @template T + */ + public function convertToDatabaseValue($value, AbstractPlatform $platform) + { + if ($value === null) { + return $value; + } + + if ($value instanceof DateTimeInterface) { + if ($value instanceof DateTimeImmutable) { + Deprecation::triggerIfCalledFromOutside( + 'doctrine/dbal', + 'https://github.com/doctrine/dbal/pull/6017', + 'Passing an instance of %s is deprecated, use %s::%s() instead.', + get_class($value), + DateImmutableType::class, + __FUNCTION__, + ); + } + + return $value->format($platform->getDateFormatString()); + } + + throw ConversionException::conversionFailedInvalidType($value, $this->getName(), ['null', DateTime::class]); + } + + /** + * {@inheritDoc} + * + * @param T $value + * + * @return (T is null ? null : DateTimeInterface) + * + * @template T + */ + public function convertToPHPValue($value, AbstractPlatform $platform) + { + if ($value instanceof DateTimeImmutable) { + Deprecation::triggerIfCalledFromOutside( + 'doctrine/dbal', + 'https://github.com/doctrine/dbal/pull/6017', + 'Passing an instance of %s is deprecated, use %s::%s() instead.', + get_class($value), + DateImmutableType::class, + __FUNCTION__, + ); + } + + if ($value === null || $value instanceof DateTimeInterface) { + return $value; + } + + $dateTime = DateTime::createFromFormat('!' . $platform->getDateFormatString(), $value); + if ($dateTime !== false) { + return $dateTime; + } + + throw ConversionException::conversionFailedFormat( + $value, + $this->getName(), + $platform->getDateFormatString(), + ); + } +} diff --git a/3rdparty/doctrine/dbal/src/Types/DecimalType.php b/3rdparty/doctrine/dbal/src/Types/DecimalType.php new file mode 100644 index 00000000..308134b0 --- /dev/null +++ b/3rdparty/doctrine/dbal/src/Types/DecimalType.php @@ -0,0 +1,47 @@ +getDecimalTypeDeclarationSQL($column); + } + + /** + * {@inheritDoc} + */ + public function convertToPHPValue($value, AbstractPlatform $platform) + { + // Some drivers starting from PHP 8.1 can represent decimals as float/int + // See also: https://github.com/doctrine/dbal/pull/4818 + if ((PHP_VERSION_ID >= 80100 || $platform instanceof SqlitePlatform) && (is_float($value) || is_int($value))) { + return (string) $value; + } + + return $value; + } +} diff --git a/3rdparty/doctrine/dbal/src/Types/FloatType.php b/3rdparty/doctrine/dbal/src/Types/FloatType.php new file mode 100644 index 00000000..e01b7741 --- /dev/null +++ b/3rdparty/doctrine/dbal/src/Types/FloatType.php @@ -0,0 +1,38 @@ +getFloatDeclarationSQL($column); + } + + /** + * {@inheritDoc} + * + * @param T $value + * + * @return (T is null ? null : float) + * + * @template T + */ + public function convertToPHPValue($value, AbstractPlatform $platform) + { + return $value === null ? null : (float) $value; + } +} diff --git a/3rdparty/doctrine/dbal/src/Types/GuidType.php b/3rdparty/doctrine/dbal/src/Types/GuidType.php new file mode 100644 index 00000000..3c8b7f4f --- /dev/null +++ b/3rdparty/doctrine/dbal/src/Types/GuidType.php @@ -0,0 +1,45 @@ +getGuidTypeDeclarationSQL($column); + } + + /** + * {@inheritDoc} + */ + public function getName() + { + return Types::GUID; + } + + /** + * {@inheritDoc} + * + * @deprecated + */ + public function requiresSQLCommentHint(AbstractPlatform $platform) + { + Deprecation::triggerIfCalledFromOutside( + 'doctrine/dbal', + 'https://github.com/doctrine/dbal/pull/5509', + '%s is deprecated.', + __METHOD__, + ); + + return ! $platform->hasNativeGuidType(); + } +} diff --git a/3rdparty/doctrine/dbal/src/Types/IntegerType.php b/3rdparty/doctrine/dbal/src/Types/IntegerType.php new file mode 100644 index 00000000..7c2d7110 --- /dev/null +++ b/3rdparty/doctrine/dbal/src/Types/IntegerType.php @@ -0,0 +1,50 @@ +getIntegerTypeDeclarationSQL($column); + } + + /** + * {@inheritDoc} + * + * @param T $value + * + * @return (T is null ? null : int) + * + * @template T + */ + public function convertToPHPValue($value, AbstractPlatform $platform) + { + return $value === null ? null : (int) $value; + } + + /** + * {@inheritDoc} + */ + public function getBindingType() + { + return ParameterType::INTEGER; + } +} diff --git a/3rdparty/doctrine/dbal/src/Types/JsonType.php b/3rdparty/doctrine/dbal/src/Types/JsonType.php new file mode 100644 index 00000000..27f872c8 --- /dev/null +++ b/3rdparty/doctrine/dbal/src/Types/JsonType.php @@ -0,0 +1,96 @@ +getJsonTypeDeclarationSQL($column); + } + + /** + * {@inheritDoc} + * + * @param T $value + * + * @return (T is null ? null : string) + * + * @template T + */ + public function convertToDatabaseValue($value, AbstractPlatform $platform) + { + if ($value === null) { + return null; + } + + try { + return json_encode($value, JSON_THROW_ON_ERROR | JSON_PRESERVE_ZERO_FRACTION); + } catch (JsonException $e) { + throw ConversionException::conversionFailedSerialization($value, 'json', $e->getMessage(), $e); + } + } + + /** + * {@inheritDoc} + */ + public function convertToPHPValue($value, AbstractPlatform $platform) + { + if ($value === null || $value === '') { + return null; + } + + if (is_resource($value)) { + $value = stream_get_contents($value); + } + + try { + return json_decode($value, true, 512, JSON_THROW_ON_ERROR); + } catch (JsonException $e) { + throw ConversionException::conversionFailed($value, $this->getName(), $e); + } + } + + /** + * {@inheritDoc} + */ + public function getName() + { + return Types::JSON; + } + + /** + * {@inheritDoc} + * + * @deprecated + */ + public function requiresSQLCommentHint(AbstractPlatform $platform) + { + Deprecation::triggerIfCalledFromOutside( + 'doctrine/dbal', + 'https://github.com/doctrine/dbal/pull/5509', + '%s is deprecated.', + __METHOD__, + ); + + return ! $platform->hasNativeJsonType(); + } +} diff --git a/3rdparty/doctrine/dbal/src/Types/ObjectType.php b/3rdparty/doctrine/dbal/src/Types/ObjectType.php new file mode 100644 index 00000000..497e9c40 --- /dev/null +++ b/3rdparty/doctrine/dbal/src/Types/ObjectType.php @@ -0,0 +1,88 @@ +getClobTypeDeclarationSQL($column); + } + + /** + * {@inheritDoc} + * + * @param mixed $value + * + * @return string + */ + public function convertToDatabaseValue($value, AbstractPlatform $platform) + { + return serialize($value); + } + + /** + * {@inheritDoc} + */ + public function convertToPHPValue($value, AbstractPlatform $platform) + { + if ($value === null) { + return null; + } + + $value = is_resource($value) ? stream_get_contents($value) : $value; + + set_error_handler(function (int $code, string $message): bool { + throw ConversionException::conversionFailedUnserialization($this->getName(), $message); + }); + + try { + return unserialize($value); + } finally { + restore_error_handler(); + } + } + + /** + * {@inheritDoc} + */ + public function getName() + { + return Types::OBJECT; + } + + /** + * {@inheritDoc} + * + * @deprecated + */ + public function requiresSQLCommentHint(AbstractPlatform $platform) + { + Deprecation::triggerIfCalledFromOutside( + 'doctrine/dbal', + 'https://github.com/doctrine/dbal/pull/5509', + '%s is deprecated.', + __METHOD__, + ); + + return true; + } +} diff --git a/3rdparty/doctrine/dbal/src/Types/PhpDateTimeMappingType.php b/3rdparty/doctrine/dbal/src/Types/PhpDateTimeMappingType.php new file mode 100644 index 00000000..45658505 --- /dev/null +++ b/3rdparty/doctrine/dbal/src/Types/PhpDateTimeMappingType.php @@ -0,0 +1,12 @@ +getClobTypeDeclarationSQL($column); + } + + /** + * {@inheritDoc} + * + * @param mixed $value + * + * @return string|null + */ + public function convertToDatabaseValue($value, AbstractPlatform $platform) + { + if (! is_array($value) || count($value) === 0) { + return null; + } + + return implode(',', $value); + } + + /** + * {@inheritDoc} + * + * @param mixed $value + * + * @return list + */ + public function convertToPHPValue($value, AbstractPlatform $platform) + { + if ($value === null) { + return []; + } + + $value = is_resource($value) ? stream_get_contents($value) : $value; + + return explode(',', $value); + } + + /** + * {@inheritDoc} + */ + public function getName() + { + return Types::SIMPLE_ARRAY; + } + + /** + * {@inheritDoc} + * + * @deprecated + */ + public function requiresSQLCommentHint(AbstractPlatform $platform) + { + Deprecation::triggerIfCalledFromOutside( + 'doctrine/dbal', + 'https://github.com/doctrine/dbal/pull/5509', + '%s is deprecated.', + __METHOD__, + ); + + return true; + } +} diff --git a/3rdparty/doctrine/dbal/src/Types/SmallIntType.php b/3rdparty/doctrine/dbal/src/Types/SmallIntType.php new file mode 100644 index 00000000..2c8567a1 --- /dev/null +++ b/3rdparty/doctrine/dbal/src/Types/SmallIntType.php @@ -0,0 +1,50 @@ +getSmallIntTypeDeclarationSQL($column); + } + + /** + * {@inheritDoc} + * + * @param T $value + * + * @return (T is null ? null : int) + * + * @template T + */ + public function convertToPHPValue($value, AbstractPlatform $platform) + { + return $value === null ? null : (int) $value; + } + + /** + * {@inheritDoc} + */ + public function getBindingType() + { + return ParameterType::INTEGER; + } +} diff --git a/3rdparty/doctrine/dbal/src/Types/StringType.php b/3rdparty/doctrine/dbal/src/Types/StringType.php new file mode 100644 index 00000000..1992e8fa --- /dev/null +++ b/3rdparty/doctrine/dbal/src/Types/StringType.php @@ -0,0 +1,27 @@ +getStringTypeDeclarationSQL($column); + } + + /** + * {@inheritDoc} + */ + public function getName() + { + return Types::STRING; + } +} diff --git a/3rdparty/doctrine/dbal/src/Types/TextType.php b/3rdparty/doctrine/dbal/src/Types/TextType.php new file mode 100644 index 00000000..d060bb2d --- /dev/null +++ b/3rdparty/doctrine/dbal/src/Types/TextType.php @@ -0,0 +1,38 @@ +getClobTypeDeclarationSQL($column); + } + + /** + * {@inheritDoc} + */ + public function convertToPHPValue($value, AbstractPlatform $platform) + { + return is_resource($value) ? stream_get_contents($value) : $value; + } + + /** + * {@inheritDoc} + */ + public function getName() + { + return Types::TEXT; + } +} diff --git a/3rdparty/doctrine/dbal/src/Types/TimeImmutableType.php b/3rdparty/doctrine/dbal/src/Types/TimeImmutableType.php new file mode 100644 index 00000000..9373f591 --- /dev/null +++ b/3rdparty/doctrine/dbal/src/Types/TimeImmutableType.php @@ -0,0 +1,92 @@ +format($platform->getTimeFormatString()); + } + + throw ConversionException::conversionFailedInvalidType( + $value, + $this->getName(), + ['null', DateTimeImmutable::class], + ); + } + + /** + * {@inheritDoc} + * + * @param T $value + * + * @return (T is null ? null : DateTimeImmutable) + * + * @template T + */ + public function convertToPHPValue($value, AbstractPlatform $platform) + { + if ($value === null || $value instanceof DateTimeImmutable) { + return $value; + } + + $dateTime = DateTimeImmutable::createFromFormat('!' . $platform->getTimeFormatString(), $value); + + if ($dateTime !== false) { + return $dateTime; + } + + throw ConversionException::conversionFailedFormat( + $value, + $this->getName(), + $platform->getTimeFormatString(), + ); + } + + /** + * {@inheritDoc} + * + * @deprecated + */ + public function requiresSQLCommentHint(AbstractPlatform $platform) + { + Deprecation::triggerIfCalledFromOutside( + 'doctrine/dbal', + 'https://github.com/doctrine/dbal/pull/5509', + '%s is deprecated.', + __METHOD__, + ); + + return true; + } +} diff --git a/3rdparty/doctrine/dbal/src/Types/TimeType.php b/3rdparty/doctrine/dbal/src/Types/TimeType.php new file mode 100644 index 00000000..7356fc20 --- /dev/null +++ b/3rdparty/doctrine/dbal/src/Types/TimeType.php @@ -0,0 +1,104 @@ +getTimeTypeDeclarationSQL($column); + } + + /** + * {@inheritDoc} + * + * @param T $value + * + * @return (T is null ? null : string) + * + * @template T + */ + public function convertToDatabaseValue($value, AbstractPlatform $platform) + { + if ($value === null) { + return $value; + } + + if ($value instanceof DateTimeImmutable) { + Deprecation::triggerIfCalledFromOutside( + 'doctrine/dbal', + 'https://github.com/doctrine/dbal/pull/6017', + 'Passing an instance of %s is deprecated, use %s::%s() instead.', + get_class($value), + TimeImmutableType::class, + __FUNCTION__, + ); + } + + if ($value instanceof DateTimeInterface) { + return $value->format($platform->getTimeFormatString()); + } + + throw ConversionException::conversionFailedInvalidType($value, $this->getName(), ['null', DateTime::class]); + } + + /** + * {@inheritDoc} + * + * @param T $value + * + * @return (T is null ? null : DateTimeInterface) + * + * @template T + */ + public function convertToPHPValue($value, AbstractPlatform $platform) + { + if ($value instanceof DateTimeImmutable) { + Deprecation::triggerIfCalledFromOutside( + 'doctrine/dbal', + 'https://github.com/doctrine/dbal/pull/6017', + 'Passing an instance of %s is deprecated, use %s::%s() instead.', + get_class($value), + TimeImmutableType::class, + __FUNCTION__, + ); + } + + if ($value === null || $value instanceof DateTimeInterface) { + return $value; + } + + $dateTime = DateTime::createFromFormat('!' . $platform->getTimeFormatString(), $value); + if ($dateTime !== false) { + return $dateTime; + } + + throw ConversionException::conversionFailedFormat( + $value, + $this->getName(), + $platform->getTimeFormatString(), + ); + } +} diff --git a/3rdparty/doctrine/dbal/src/Types/Type.php b/3rdparty/doctrine/dbal/src/Types/Type.php new file mode 100644 index 00000000..7613811e --- /dev/null +++ b/3rdparty/doctrine/dbal/src/Types/Type.php @@ -0,0 +1,296 @@ + ArrayType::class, + Types::ASCII_STRING => AsciiStringType::class, + Types::BIGINT => BigIntType::class, + Types::BINARY => BinaryType::class, + Types::BLOB => BlobType::class, + Types::BOOLEAN => BooleanType::class, + Types::DATE_MUTABLE => DateType::class, + Types::DATE_IMMUTABLE => DateImmutableType::class, + Types::DATEINTERVAL => DateIntervalType::class, + Types::DATETIME_MUTABLE => DateTimeType::class, + Types::DATETIME_IMMUTABLE => DateTimeImmutableType::class, + Types::DATETIMETZ_MUTABLE => DateTimeTzType::class, + Types::DATETIMETZ_IMMUTABLE => DateTimeTzImmutableType::class, + Types::DECIMAL => DecimalType::class, + Types::FLOAT => FloatType::class, + Types::GUID => GuidType::class, + Types::INTEGER => IntegerType::class, + Types::JSON => JsonType::class, + Types::OBJECT => ObjectType::class, + Types::SIMPLE_ARRAY => SimpleArrayType::class, + Types::SMALLINT => SmallIntType::class, + Types::STRING => StringType::class, + Types::TEXT => TextType::class, + Types::TIME_MUTABLE => TimeType::class, + Types::TIME_IMMUTABLE => TimeImmutableType::class, + ]; + + private static ?TypeRegistry $typeRegistry = null; + + /** @internal Do not instantiate directly - use {@see Type::addType()} method instead. */ + final public function __construct() + { + } + + /** + * Converts a value from its PHP representation to its database representation + * of this type. + * + * @param mixed $value The value to convert. + * @param AbstractPlatform $platform The currently used database platform. + * + * @return mixed The database representation of the value. + * + * @throws ConversionException + */ + public function convertToDatabaseValue($value, AbstractPlatform $platform) + { + return $value; + } + + /** + * Converts a value from its database representation to its PHP representation + * of this type. + * + * @param mixed $value The value to convert. + * @param AbstractPlatform $platform The currently used database platform. + * + * @return mixed The PHP representation of the value. + * + * @throws ConversionException + */ + public function convertToPHPValue($value, AbstractPlatform $platform) + { + return $value; + } + + /** + * Gets the SQL declaration snippet for a column of this type. + * + * @param mixed[] $column The column definition + * @param AbstractPlatform $platform The currently used database platform. + * + * @return string + */ + abstract public function getSQLDeclaration(array $column, AbstractPlatform $platform); + + /** + * Gets the name of this type. + * + * @deprecated this method will be removed in Doctrine DBAL 4.0, + * use {@see TypeRegistry::lookupName()} instead. + * + * @return string + */ + abstract public function getName(); + + final public static function getTypeRegistry(): TypeRegistry + { + return self::$typeRegistry ??= self::createTypeRegistry(); + } + + private static function createTypeRegistry(): TypeRegistry + { + $instances = []; + + foreach (self::BUILTIN_TYPES_MAP as $name => $class) { + $instances[$name] = new $class(); + } + + return new TypeRegistry($instances); + } + + /** + * Factory method to create type instances. + * Type instances are implemented as flyweights. + * + * @param string $name The name of the type (as returned by getName()). + * + * @return Type + * + * @throws Exception + */ + public static function getType($name) + { + return self::getTypeRegistry()->get($name); + } + + /** + * Finds a name for the given type. + * + * @throws Exception + */ + public static function lookupName(self $type): string + { + return self::getTypeRegistry()->lookupName($type); + } + + /** + * Adds a custom type to the type map. + * + * @param string $name The name of the type. This should correspond to what getName() returns. + * @param class-string $className The class name of the custom type. + * + * @return void + * + * @throws Exception + */ + public static function addType($name, $className) + { + self::getTypeRegistry()->register($name, new $className()); + } + + /** + * Checks if exists support for a type. + * + * @param string $name The name of the type. + * + * @return bool TRUE if type is supported; FALSE otherwise. + */ + public static function hasType($name) + { + return self::getTypeRegistry()->has($name); + } + + /** + * Overrides an already defined type to use a different implementation. + * + * @param string $name + * @param class-string $className + * + * @return void + * + * @throws Exception + */ + public static function overrideType($name, $className) + { + self::getTypeRegistry()->override($name, new $className()); + } + + /** + * Gets the (preferred) binding type for values of this type that + * can be used when binding parameters to prepared statements. + * + * This method should return one of the {@see ParameterType} constants. + * + * @return int + */ + public function getBindingType() + { + return ParameterType::STRING; + } + + /** + * Gets the types array map which holds all registered types and the corresponding + * type class + * + * @return array + */ + public static function getTypesMap() + { + return array_map( + static function (Type $type): string { + return get_class($type); + }, + self::getTypeRegistry()->getMap(), + ); + } + + /** + * Does working with this column require SQL conversion functions? + * + * This is a metadata function that is required for example in the ORM. + * Usage of {@see convertToDatabaseValueSQL} and + * {@see convertToPHPValueSQL} works for any type and mostly + * does nothing. This method can additionally be used for optimization purposes. + * + * @deprecated Consumers should call {@see convertToDatabaseValueSQL} and {@see convertToPHPValueSQL} + * regardless of the type. + * + * @return bool + */ + public function canRequireSQLConversion() + { + return false; + } + + /** + * Modifies the SQL expression (identifier, parameter) to convert to a database value. + * + * @param string $sqlExpr + * + * @return string + */ + public function convertToDatabaseValueSQL($sqlExpr, AbstractPlatform $platform) + { + return $sqlExpr; + } + + /** + * Modifies the SQL expression (identifier, parameter) to convert to a PHP value. + * + * @param string $sqlExpr + * @param AbstractPlatform $platform + * + * @return string + */ + public function convertToPHPValueSQL($sqlExpr, $platform) + { + return $sqlExpr; + } + + /** + * Gets an array of database types that map to this Doctrine type. + * + * @return string[] + */ + public function getMappedDatabaseTypes(AbstractPlatform $platform) + { + return []; + } + + /** + * If this Doctrine Type maps to an already mapped database type, + * reverse schema engineering can't tell them apart. You need to mark + * one of those types as commented, which will have Doctrine use an SQL + * comment to typehint the actual Doctrine Type. + * + * @deprecated + * + * @return bool + */ + public function requiresSQLCommentHint(AbstractPlatform $platform) + { + Deprecation::triggerIfCalledFromOutside( + 'doctrine/dbal', + 'https://github.com/doctrine/dbal/pull/5509', + '%s is deprecated.', + __METHOD__, + ); + + return false; + } +} diff --git a/3rdparty/doctrine/dbal/src/Types/TypeRegistry.php b/3rdparty/doctrine/dbal/src/Types/TypeRegistry.php new file mode 100644 index 00000000..9b64c6fa --- /dev/null +++ b/3rdparty/doctrine/dbal/src/Types/TypeRegistry.php @@ -0,0 +1,127 @@ + Map of type names and their corresponding flyweight objects. */ + private array $instances; + /** @var array */ + private array $instancesReverseIndex; + + /** @param array $instances */ + public function __construct(array $instances = []) + { + $this->instances = []; + $this->instancesReverseIndex = []; + foreach ($instances as $name => $type) { + $this->register($name, $type); + } + } + + /** + * Finds a type by the given name. + * + * @throws Exception + */ + public function get(string $name): Type + { + $type = $this->instances[$name] ?? null; + if ($type === null) { + throw Exception::unknownColumnType($name); + } + + return $type; + } + + /** + * Finds a name for the given type. + * + * @throws Exception + */ + public function lookupName(Type $type): string + { + $name = $this->findTypeName($type); + + if ($name === null) { + throw Exception::typeNotRegistered($type); + } + + return $name; + } + + /** + * Checks if there is a type of the given name. + */ + public function has(string $name): bool + { + return isset($this->instances[$name]); + } + + /** + * Registers a custom type to the type map. + * + * @throws Exception + */ + public function register(string $name, Type $type): void + { + if (isset($this->instances[$name])) { + throw Exception::typeExists($name); + } + + if ($this->findTypeName($type) !== null) { + throw Exception::typeAlreadyRegistered($type); + } + + $this->instances[$name] = $type; + $this->instancesReverseIndex[spl_object_id($type)] = $name; + } + + /** + * Overrides an already defined type to use a different implementation. + * + * @throws Exception + */ + public function override(string $name, Type $type): void + { + $origType = $this->instances[$name] ?? null; + if ($origType === null) { + throw Exception::typeNotFound($name); + } + + if (($this->findTypeName($type) ?? $name) !== $name) { + throw Exception::typeAlreadyRegistered($type); + } + + unset($this->instancesReverseIndex[spl_object_id($origType)]); + $this->instances[$name] = $type; + $this->instancesReverseIndex[spl_object_id($type)] = $name; + } + + /** + * Gets the map of all registered types and their corresponding type instances. + * + * @internal + * + * @return array + */ + public function getMap(): array + { + return $this->instances; + } + + private function findTypeName(Type $type): ?string + { + return $this->instancesReverseIndex[spl_object_id($type)] ?? null; + } +} diff --git a/3rdparty/doctrine/dbal/src/Types/Types.php b/3rdparty/doctrine/dbal/src/Types/Types.php new file mode 100644 index 00000000..54b0dfec --- /dev/null +++ b/3rdparty/doctrine/dbal/src/Types/Types.php @@ -0,0 +1,47 @@ +format($platform->getDateTimeFormatString()); + } + + throw ConversionException::conversionFailedInvalidType( + $value, + $this->getName(), + ['null', DateTimeImmutable::class], + ); + } + + /** + * {@inheritDoc} + * + * @param T $value + * + * @return (T is null ? null : DateTimeImmutable) + * + * @template T + */ + public function convertToPHPValue($value, AbstractPlatform $platform) + { + if ($value === null || $value instanceof DateTimeImmutable) { + return $value; + } + + try { + $dateTime = new DateTimeImmutable($value); + } catch (Exception $e) { + throw ConversionException::conversionFailed($value, $this->getName(), $e); + } + + return $dateTime; + } + + /** + * {@inheritDoc} + * + * @deprecated + */ + public function requiresSQLCommentHint(AbstractPlatform $platform) + { + Deprecation::triggerIfCalledFromOutside( + 'doctrine/dbal', + 'https://github.com/doctrine/dbal/pull/5509', + '%s is deprecated.', + __METHOD__, + ); + + return true; + } +} diff --git a/3rdparty/doctrine/dbal/src/Types/VarDateTimeType.php b/3rdparty/doctrine/dbal/src/Types/VarDateTimeType.php new file mode 100644 index 00000000..35ad4032 --- /dev/null +++ b/3rdparty/doctrine/dbal/src/Types/VarDateTimeType.php @@ -0,0 +1,42 @@ + 0 it is necessary to use this type. + */ +class VarDateTimeType extends DateTimeType +{ + /** + * {@inheritDoc} + * + * @param T $value + * + * @return (T is null ? null : DateTimeInterface) + * + * @template T + */ + public function convertToPHPValue($value, AbstractPlatform $platform) + { + if ($value === null || $value instanceof DateTime) { + return $value; + } + + try { + $dateTime = new DateTime($value); + } catch (Exception $e) { + throw ConversionException::conversionFailed($value, $this->getName(), $e); + } + + return $dateTime; + } +} diff --git a/3rdparty/doctrine/dbal/src/VersionAwarePlatformDriver.php b/3rdparty/doctrine/dbal/src/VersionAwarePlatformDriver.php new file mode 100644 index 00000000..ffcfcd63 --- /dev/null +++ b/3rdparty/doctrine/dbal/src/VersionAwarePlatformDriver.php @@ -0,0 +1,30 @@ +|null */ + private static $type; + + /** @var LoggerInterface|null */ + private static $logger; + + /** @var array */ + private static $ignoredPackages = []; + + /** @var array */ + private static $triggeredDeprecations = []; + + /** @var array */ + private static $ignoredLinks = []; + + /** @var bool */ + private static $deduplication = true; + + /** + * Trigger a deprecation for the given package and identfier. + * + * The link should point to a Github issue or Wiki entry detailing the + * deprecation. It is additionally used to de-duplicate the trigger of the + * same deprecation during a request. + * + * @param float|int|string $args + */ + public static function trigger(string $package, string $link, string $message, ...$args): void + { + $type = self::$type ?? self::getTypeFromEnv(); + + if ($type === self::TYPE_NONE) { + return; + } + + if (isset(self::$ignoredLinks[$link])) { + return; + } + + if (array_key_exists($link, self::$triggeredDeprecations)) { + self::$triggeredDeprecations[$link]++; + } else { + self::$triggeredDeprecations[$link] = 1; + } + + if (self::$deduplication === true && self::$triggeredDeprecations[$link] > 1) { + return; + } + + if (isset(self::$ignoredPackages[$package])) { + return; + } + + $backtrace = debug_backtrace(DEBUG_BACKTRACE_IGNORE_ARGS, 2); + + $message = sprintf($message, ...$args); + + self::delegateTriggerToBackend($message, $backtrace, $link, $package); + } + + /** + * Trigger a deprecation for the given package and identifier when called from outside. + * + * "Outside" means we assume that $package is currently installed as a + * dependency and the caller is not a file in that package. When $package + * is installed as a root package then deprecations triggered from the + * tests folder are also considered "outside". + * + * This deprecation method assumes that you are using Composer to install + * the dependency and are using the default /vendor/ folder and not a + * Composer plugin to change the install location. The assumption is also + * that $package is the exact composer packge name. + * + * Compared to {@link trigger()} this method causes some overhead when + * deprecation tracking is enabled even during deduplication, because it + * needs to call {@link debug_backtrace()} + * + * @param float|int|string $args + */ + public static function triggerIfCalledFromOutside(string $package, string $link, string $message, ...$args): void + { + $type = self::$type ?? self::getTypeFromEnv(); + + if ($type === self::TYPE_NONE) { + return; + } + + $backtrace = debug_backtrace(DEBUG_BACKTRACE_IGNORE_ARGS, 2); + + // first check that the caller is not from a tests folder, in which case we always let deprecations pass + if (isset($backtrace[1]['file'], $backtrace[0]['file']) && strpos($backtrace[1]['file'], DIRECTORY_SEPARATOR . 'tests' . DIRECTORY_SEPARATOR) === false) { + $path = DIRECTORY_SEPARATOR . 'vendor' . DIRECTORY_SEPARATOR . str_replace('/', DIRECTORY_SEPARATOR, $package) . DIRECTORY_SEPARATOR; + + if (strpos($backtrace[0]['file'], $path) === false) { + return; + } + + if (strpos($backtrace[1]['file'], $path) !== false) { + return; + } + } + + if (isset(self::$ignoredLinks[$link])) { + return; + } + + if (array_key_exists($link, self::$triggeredDeprecations)) { + self::$triggeredDeprecations[$link]++; + } else { + self::$triggeredDeprecations[$link] = 1; + } + + if (self::$deduplication === true && self::$triggeredDeprecations[$link] > 1) { + return; + } + + if (isset(self::$ignoredPackages[$package])) { + return; + } + + $message = sprintf($message, ...$args); + + self::delegateTriggerToBackend($message, $backtrace, $link, $package); + } + + /** @param list $backtrace */ + private static function delegateTriggerToBackend(string $message, array $backtrace, string $link, string $package): void + { + $type = self::$type ?? self::getTypeFromEnv(); + + if (($type & self::TYPE_PSR_LOGGER) > 0) { + $context = [ + 'file' => $backtrace[0]['file'] ?? null, + 'line' => $backtrace[0]['line'] ?? null, + 'package' => $package, + 'link' => $link, + ]; + + assert(self::$logger !== null); + + self::$logger->notice($message, $context); + } + + if (! (($type & self::TYPE_TRIGGER_ERROR) > 0)) { + return; + } + + $message .= sprintf( + ' (%s:%d called by %s:%d, %s, package %s)', + self::basename($backtrace[0]['file'] ?? 'native code'), + $backtrace[0]['line'] ?? 0, + self::basename($backtrace[1]['file'] ?? 'native code'), + $backtrace[1]['line'] ?? 0, + $link, + $package + ); + + @trigger_error($message, E_USER_DEPRECATED); + } + + /** + * A non-local-aware version of PHPs basename function. + */ + private static function basename(string $filename): string + { + $pos = strrpos($filename, DIRECTORY_SEPARATOR); + + if ($pos === false) { + return $filename; + } + + return substr($filename, $pos + 1); + } + + public static function enableTrackingDeprecations(): void + { + self::$type = self::$type ?? self::getTypeFromEnv(); + self::$type |= self::TYPE_TRACK_DEPRECATIONS; + } + + public static function enableWithTriggerError(): void + { + self::$type = self::$type ?? self::getTypeFromEnv(); + self::$type |= self::TYPE_TRIGGER_ERROR; + } + + public static function enableWithPsrLogger(LoggerInterface $logger): void + { + self::$type = self::$type ?? self::getTypeFromEnv(); + self::$type |= self::TYPE_PSR_LOGGER; + self::$logger = $logger; + } + + public static function withoutDeduplication(): void + { + self::$deduplication = false; + } + + public static function disable(): void + { + self::$type = self::TYPE_NONE; + self::$logger = null; + self::$deduplication = true; + self::$ignoredLinks = []; + + foreach (self::$triggeredDeprecations as $link => $count) { + self::$triggeredDeprecations[$link] = 0; + } + } + + public static function ignorePackage(string $packageName): void + { + self::$ignoredPackages[$packageName] = true; + } + + public static function ignoreDeprecations(string ...$links): void + { + foreach ($links as $link) { + self::$ignoredLinks[$link] = true; + } + } + + public static function getUniqueTriggeredDeprecationsCount(): int + { + return array_reduce(self::$triggeredDeprecations, static function (int $carry, int $count) { + return $carry + $count; + }, 0); + } + + /** + * Returns each triggered deprecation link identifier and the amount of occurrences. + * + * @return array + */ + public static function getTriggeredDeprecations(): array + { + return self::$triggeredDeprecations; + } + + /** @return int-mask-of */ + private static function getTypeFromEnv(): int + { + switch ($_SERVER['DOCTRINE_DEPRECATIONS'] ?? $_ENV['DOCTRINE_DEPRECATIONS'] ?? null) { + case 'trigger': + self::$type = self::TYPE_TRIGGER_ERROR; + break; + + case 'track': + self::$type = self::TYPE_TRACK_DEPRECATIONS; + break; + + default: + self::$type = self::TYPE_NONE; + break; + } + + return self::$type; + } +} diff --git a/3rdparty/doctrine/deprecations/src/PHPUnit/VerifyDeprecations.php b/3rdparty/doctrine/deprecations/src/PHPUnit/VerifyDeprecations.php new file mode 100644 index 00000000..a6c7ad6f --- /dev/null +++ b/3rdparty/doctrine/deprecations/src/PHPUnit/VerifyDeprecations.php @@ -0,0 +1,66 @@ + */ + private $doctrineDeprecationsExpectations = []; + + /** @var array */ + private $doctrineNoDeprecationsExpectations = []; + + public function expectDeprecationWithIdentifier(string $identifier): void + { + $this->doctrineDeprecationsExpectations[$identifier] = Deprecation::getTriggeredDeprecations()[$identifier] ?? 0; + } + + public function expectNoDeprecationWithIdentifier(string $identifier): void + { + $this->doctrineNoDeprecationsExpectations[$identifier] = Deprecation::getTriggeredDeprecations()[$identifier] ?? 0; + } + + /** @before */ + #[Before] + public function enableDeprecationTracking(): void + { + Deprecation::enableTrackingDeprecations(); + } + + /** @after */ + #[After] + public function verifyDeprecationsAreTriggered(): void + { + foreach ($this->doctrineDeprecationsExpectations as $identifier => $expectation) { + $actualCount = Deprecation::getTriggeredDeprecations()[$identifier] ?? 0; + + $this->assertTrue( + $actualCount > $expectation, + sprintf( + "Expected deprecation with identifier '%s' was not triggered by code executed in test.", + $identifier + ) + ); + } + + foreach ($this->doctrineNoDeprecationsExpectations as $identifier => $expectation) { + $actualCount = Deprecation::getTriggeredDeprecations()[$identifier] ?? 0; + + $this->assertTrue( + $actualCount === $expectation, + sprintf( + "Expected deprecation with identifier '%s' was triggered by code executed in test, but expected not to.", + $identifier + ) + ); + } + } +} diff --git a/3rdparty/doctrine/event-manager/LICENSE b/3rdparty/doctrine/event-manager/LICENSE new file mode 100644 index 00000000..8c38cc1b --- /dev/null +++ b/3rdparty/doctrine/event-manager/LICENSE @@ -0,0 +1,19 @@ +Copyright (c) 2006-2015 Doctrine Project + +Permission is hereby granted, free of charge, to any person obtaining a copy of +this software and associated documentation files (the "Software"), to deal in +the Software without restriction, including without limitation the rights to +use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies +of the Software, and to permit persons to whom the Software is furnished to do +so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. diff --git a/3rdparty/doctrine/event-manager/src/EventArgs.php b/3rdparty/doctrine/event-manager/src/EventArgs.php new file mode 100644 index 00000000..eea3d8a0 --- /dev/null +++ b/3rdparty/doctrine/event-manager/src/EventArgs.php @@ -0,0 +1,37 @@ + => + * + * @var array + */ + private array $listeners = []; + + /** + * Dispatches an event to all registered listeners. + * + * @param string $eventName The name of the event to dispatch. The name of the event is + * the name of the method that is invoked on listeners. + * @param EventArgs|null $eventArgs The event arguments to pass to the event handlers/listeners. + * If not supplied, the single empty EventArgs instance is used. + */ + public function dispatchEvent(string $eventName, EventArgs|null $eventArgs = null): void + { + if (! isset($this->listeners[$eventName])) { + return; + } + + $eventArgs ??= EventArgs::getEmptyInstance(); + + foreach ($this->listeners[$eventName] as $listener) { + $listener->$eventName($eventArgs); + } + } + + /** + * Gets the listeners of a specific event. + * + * @param string $event The name of the event. + * + * @return object[] + */ + public function getListeners(string $event): array + { + return $this->listeners[$event] ?? []; + } + + /** + * Gets all listeners keyed by event name. + * + * @return array The event listeners for the specified event, or all event listeners. + */ + public function getAllListeners(): array + { + return $this->listeners; + } + + /** + * Checks whether an event has any registered listeners. + */ + public function hasListeners(string $event): bool + { + return ! empty($this->listeners[$event]); + } + + /** + * Adds an event listener that listens on the specified events. + * + * @param string|string[] $events The event(s) to listen on. + * @param object $listener The listener object. + */ + public function addEventListener(string|array $events, object $listener): void + { + // Picks the hash code related to that listener + $hash = spl_object_hash($listener); + + foreach ((array) $events as $event) { + // Overrides listener if a previous one was associated already + // Prevents duplicate listeners on same event (same instance only) + $this->listeners[$event][$hash] = $listener; + } + } + + /** + * Removes an event listener from the specified events. + * + * @param string|string[] $events + */ + public function removeEventListener(string|array $events, object $listener): void + { + // Picks the hash code related to that listener + $hash = spl_object_hash($listener); + + foreach ((array) $events as $event) { + unset($this->listeners[$event][$hash]); + } + } + + /** + * Adds an EventSubscriber. + * + * The subscriber is asked for all the events it is interested in and added + * as a listener for these events. + */ + public function addEventSubscriber(EventSubscriber $subscriber): void + { + $this->addEventListener($subscriber->getSubscribedEvents(), $subscriber); + } + + /** + * Removes an EventSubscriber. + * + * The subscriber is asked for all the events it is interested in and removed + * as a listener for these events. + */ + public function removeEventSubscriber(EventSubscriber $subscriber): void + { + $this->removeEventListener($subscriber->getSubscribedEvents(), $subscriber); + } +} diff --git a/3rdparty/doctrine/event-manager/src/EventSubscriber.php b/3rdparty/doctrine/event-manager/src/EventSubscriber.php new file mode 100644 index 00000000..89cef558 --- /dev/null +++ b/3rdparty/doctrine/event-manager/src/EventSubscriber.php @@ -0,0 +1,21 @@ +> + */ + private array $tokens = []; + + /** + * Current lexer position in input string. + */ + private int $position = 0; + + /** + * Current peek of current lexer position. + */ + private int $peek = 0; + + /** + * The next token in the input. + * + * @var Token|null + */ + public Token|null $lookahead; + + /** + * The last matched/seen token. + * + * @var Token|null + */ + public Token|null $token; + + /** + * Composed regex for input parsing. + * + * @var non-empty-string|null + */ + private string|null $regex = null; + + /** + * Sets the input data to be tokenized. + * + * The Lexer is immediately reset and the new input tokenized. + * Any unprocessed tokens from any previous input are lost. + * + * @param string $input The input to be tokenized. + * + * @return void + */ + public function setInput(string $input) + { + $this->input = $input; + $this->tokens = []; + + $this->reset(); + $this->scan($input); + } + + /** + * Resets the lexer. + * + * @return void + */ + public function reset() + { + $this->lookahead = null; + $this->token = null; + $this->peek = 0; + $this->position = 0; + } + + /** + * Resets the peek pointer to 0. + * + * @return void + */ + public function resetPeek() + { + $this->peek = 0; + } + + /** + * Resets the lexer position on the input to the given position. + * + * @param int $position Position to place the lexical scanner. + * + * @return void + */ + public function resetPosition(int $position = 0) + { + $this->position = $position; + } + + /** + * Retrieve the original lexer's input until a given position. + * + * @return string + */ + public function getInputUntilPosition(int $position) + { + return substr($this->input, 0, $position); + } + + /** + * Checks whether a given token matches the current lookahead. + * + * @param T $type + * + * @return bool + * + * @psalm-assert-if-true !=null $this->lookahead + */ + public function isNextToken(int|string|UnitEnum $type) + { + return $this->lookahead !== null && $this->lookahead->isA($type); + } + + /** + * Checks whether any of the given tokens matches the current lookahead. + * + * @param list $types + * + * @return bool + * + * @psalm-assert-if-true !=null $this->lookahead + */ + public function isNextTokenAny(array $types) + { + return $this->lookahead !== null && $this->lookahead->isA(...$types); + } + + /** + * Moves to the next token in the input string. + * + * @return bool + * + * @psalm-assert-if-true !null $this->lookahead + */ + public function moveNext() + { + $this->peek = 0; + $this->token = $this->lookahead; + $this->lookahead = isset($this->tokens[$this->position]) + ? $this->tokens[$this->position++] : null; + + return $this->lookahead !== null; + } + + /** + * Tells the lexer to skip input tokens until it sees a token with the given value. + * + * @param T $type The token type to skip until. + * + * @return void + */ + public function skipUntil(int|string|UnitEnum $type) + { + while ($this->lookahead !== null && ! $this->lookahead->isA($type)) { + $this->moveNext(); + } + } + + /** + * Checks if given value is identical to the given token. + * + * @return bool + */ + public function isA(string $value, int|string|UnitEnum $token) + { + return $this->getType($value) === $token; + } + + /** + * Moves the lookahead token forward. + * + * @return Token|null The next token or NULL if there are no more tokens ahead. + */ + public function peek() + { + if (isset($this->tokens[$this->position + $this->peek])) { + return $this->tokens[$this->position + $this->peek++]; + } + + return null; + } + + /** + * Peeks at the next token, returns it and immediately resets the peek. + * + * @return Token|null The next token or NULL if there are no more tokens ahead. + */ + public function glimpse() + { + $peek = $this->peek(); + $this->peek = 0; + + return $peek; + } + + /** + * Scans the input string for tokens. + * + * @param string $input A query string. + * + * @return void + */ + protected function scan(string $input) + { + if (! isset($this->regex)) { + $this->regex = sprintf( + '/(%s)|%s/%s', + implode(')|(', $this->getCatchablePatterns()), + implode('|', $this->getNonCatchablePatterns()), + $this->getModifiers(), + ); + } + + $flags = PREG_SPLIT_NO_EMPTY | PREG_SPLIT_DELIM_CAPTURE | PREG_SPLIT_OFFSET_CAPTURE; + $matches = preg_split($this->regex, $input, -1, $flags); + + if ($matches === false) { + // Work around https://bugs.php.net/78122 + $matches = [[$input, 0]]; + } + + foreach ($matches as $match) { + // Must remain before 'value' assignment since it can change content + $firstMatch = $match[0]; + $type = $this->getType($firstMatch); + + $this->tokens[] = new Token( + $firstMatch, + $type, + $match[1], + ); + } + } + + /** + * Gets the literal for a given token. + * + * @param T $token + * + * @return int|string + */ + public function getLiteral(int|string|UnitEnum $token) + { + if ($token instanceof UnitEnum) { + return $token::class . '::' . $token->name; + } + + $className = static::class; + + $reflClass = new ReflectionClass($className); + $constants = $reflClass->getConstants(); + + foreach ($constants as $name => $value) { + if ($value === $token) { + return $className . '::' . $name; + } + } + + return $token; + } + + /** + * Regex modifiers + * + * @return string + */ + protected function getModifiers() + { + return 'iu'; + } + + /** + * Lexical catchable patterns. + * + * @return string[] + */ + abstract protected function getCatchablePatterns(); + + /** + * Lexical non-catchable patterns. + * + * @return string[] + */ + abstract protected function getNonCatchablePatterns(); + + /** + * Retrieve token type. Also processes the token value if necessary. + * + * @return T|null + * + * @param-out V $value + */ + abstract protected function getType(string &$value); +} diff --git a/3rdparty/doctrine/lexer/src/Token.php b/3rdparty/doctrine/lexer/src/Token.php new file mode 100644 index 00000000..b6df6946 --- /dev/null +++ b/3rdparty/doctrine/lexer/src/Token.php @@ -0,0 +1,56 @@ +value = $value; + $this->type = $type; + $this->position = $position; + } + + /** @param T ...$types */ + public function isA(...$types): bool + { + return in_array($this->type, $types, true); + } +} diff --git a/3rdparty/egulias/email-validator/CONTRIBUTING.md b/3rdparty/egulias/email-validator/CONTRIBUTING.md new file mode 100644 index 00000000..907bc2c9 --- /dev/null +++ b/3rdparty/egulias/email-validator/CONTRIBUTING.md @@ -0,0 +1,153 @@ +# Contributing + +When contributing to this repository make sure to follow the Pull request process below. +Reduce to the minimum 3rd party dependencies. + +Please note we have a [code of conduct](#Code of Conduct), please follow it in all your interactions with the project. + +## Pull Request Process + +When doing a PR to v2 remember that you also have to do the PR port to v3, or tests confirming the bug is not reproducible. + +1. Supported version is v3. If you are fixing a bug in v2, please port to v3 +2. Use the title as a brief description of the changes +3. Describe the changes you are proposing + 1. If adding an extra validation state the benefits of adding it and the problem is solving + 2. Document in the readme, by adding it to the list +4. Provide appropriate tests for the code you are submitting: aim to keep the existing coverage percentage. +5. Add your Twitter handle (if you have) so we can thank you there. + +## License +By contributing, you agree that your contributions will be licensed under its MIT License. + +## Code of Conduct + +### Our Pledge + +We as members, contributors, and leaders pledge to make participation in our +community a harassment-free experience for everyone, regardless of age, body +size, visible or invisible disability, ethnicity, sex characteristics, gender +identity and expression, level of experience, education, socio-economic status, +nationality, personal appearance, race, religion, or sexual identity +and orientation. + +We pledge to act and interact in ways that contribute to an open, welcoming, +diverse, inclusive, and healthy community. + +### Our Standards + +Examples of behavior that contributes to a positive environment for our +community include: + +* Demonstrating empathy and kindness toward other people +* Being respectful of differing opinions, viewpoints, and experiences +* Giving and gracefully accepting constructive feedback +* Accepting responsibility and apologizing to those affected by our mistakes, + and learning from the experience +* Focusing on what is best not just for us as individuals, but for the + overall community + +Examples of unacceptable behavior include: + +* The use of sexualized language or imagery, and sexual attention or + advances of any kind +* Trolling, insulting or derogatory comments, and personal or political attacks +* Public or private harassment +* Publishing others' private information, such as a physical or email + address, without their explicit permission +* Other conduct which could reasonably be considered inappropriate in a + professional setting + +### Enforcement Responsibilities + +Community leaders are responsible for clarifying and enforcing our standards of +acceptable behavior and will take appropriate and fair corrective action in +response to any behavior that they deem inappropriate, threatening, offensive, +or harmful. + +Community leaders have the right and responsibility to remove, edit, or reject +comments, commits, code, wiki edits, issues, and other contributions that are +not aligned to this Code of Conduct, and will communicate reasons for moderation +decisions when appropriate. + +### Scope + +This Code of Conduct applies within all community spaces, and also applies when +an individual is officially representing the community in public spaces. +Examples of representing our community include using an official e-mail address, +posting via an official social media account, or acting as an appointed +representative at an online or offline event. + +### Enforcement + +Instances of abusive, harassing, or otherwise unacceptable behavior may be +reported to the community leaders responsible for enforcement at . +All complaints will be reviewed and investigated promptly and fairly. + +All community leaders are obligated to respect the privacy and security of the +reporter of any incident. + +#### Enforcement Guidelines + +Community leaders will follow these Community Impact Guidelines in determining +the consequences for any action they deem in violation of this Code of Conduct: + +#### 1. Correction + +**Community Impact**: Use of inappropriate language or other behavior deemed +unprofessional or unwelcome in the community. + +**Consequence**: A private, written warning from community leaders, providing +clarity around the nature of the violation and an explanation of why the +behavior was inappropriate. A public apology may be requested. + +#### 2. Warning + +**Community Impact**: A violation through a single incident or series +of actions. + +**Consequence**: A warning with consequences for continued behavior. No +interaction with the people involved, including unsolicited interaction with +those enforcing the Code of Conduct, for a specified period of time. This +includes avoiding interactions in community spaces as well as external channels +like social media. Violating these terms may lead to a temporary or +permanent ban. + +#### 3. Temporary Ban + +**Community Impact**: A serious violation of community standards, including +sustained inappropriate behavior. + +**Consequence**: A temporary ban from any sort of interaction or public +communication with the community for a specified period of time. No public or +private interaction with the people involved, including unsolicited interaction +with those enforcing the Code of Conduct, is allowed during this period. +Violating these terms may lead to a permanent ban. + +#### 4. Permanent Ban + +**Community Impact**: Demonstrating a pattern of violation of community +standards, including sustained inappropriate behavior, harassment of an +individual, or aggression toward or disparagement of classes of individuals. + +**Consequence**: A permanent ban from any sort of public interaction within +the community. + +### Attribution + +This Code of Conduct is adapted from the [Contributor Covenant][homepage], +version 2.0, available at +[https://www.contributor-covenant.org/version/2/0/code_of_conduct.html][v2.0]. + +Community Impact Guidelines were inspired by +[Mozilla's code of conduct enforcement ladder][Mozilla CoC]. + +For answers to common questions about this code of conduct, see the FAQ at +[https://www.contributor-covenant.org/faq][FAQ]. Translations are available +at [https://www.contributor-covenant.org/translations][translations]. + +[homepage]: https://www.contributor-covenant.org +[v2.0]: https://www.contributor-covenant.org/version/2/0/code_of_conduct.html +[Mozilla CoC]: https://github.com/mozilla/diversity +[FAQ]: https://www.contributor-covenant.org/faq +[translations]: https://www.contributor-covenant.org/translations diff --git a/3rdparty/egulias/email-validator/LICENSE b/3rdparty/egulias/email-validator/LICENSE new file mode 100644 index 00000000..b1902a4e --- /dev/null +++ b/3rdparty/egulias/email-validator/LICENSE @@ -0,0 +1,19 @@ +Copyright (c) 2013-2023 Eduardo Gulias Davis + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is furnished +to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +THE SOFTWARE. diff --git a/3rdparty/egulias/email-validator/src/EmailLexer.php b/3rdparty/egulias/email-validator/src/EmailLexer.php new file mode 100644 index 00000000..a7fdc2d2 --- /dev/null +++ b/3rdparty/egulias/email-validator/src/EmailLexer.php @@ -0,0 +1,329 @@ + */ +class EmailLexer extends AbstractLexer +{ + //ASCII values + public const S_EMPTY = -1; + public const C_NUL = 0; + public const S_HTAB = 9; + public const S_LF = 10; + public const S_CR = 13; + public const S_SP = 32; + public const EXCLAMATION = 33; + public const S_DQUOTE = 34; + public const NUMBER_SIGN = 35; + public const DOLLAR = 36; + public const PERCENTAGE = 37; + public const AMPERSAND = 38; + public const S_SQUOTE = 39; + public const S_OPENPARENTHESIS = 40; + public const S_CLOSEPARENTHESIS = 41; + public const ASTERISK = 42; + public const S_PLUS = 43; + public const S_COMMA = 44; + public const S_HYPHEN = 45; + public const S_DOT = 46; + public const S_SLASH = 47; + public const S_COLON = 58; + public const S_SEMICOLON = 59; + public const S_LOWERTHAN = 60; + public const S_EQUAL = 61; + public const S_GREATERTHAN = 62; + public const QUESTIONMARK = 63; + public const S_AT = 64; + public const S_OPENBRACKET = 91; + public const S_BACKSLASH = 92; + public const S_CLOSEBRACKET = 93; + public const CARET = 94; + public const S_UNDERSCORE = 95; + public const S_BACKTICK = 96; + public const S_OPENCURLYBRACES = 123; + public const S_PIPE = 124; + public const S_CLOSECURLYBRACES = 125; + public const S_TILDE = 126; + public const C_DEL = 127; + public const INVERT_QUESTIONMARK = 168; + public const INVERT_EXCLAMATION = 173; + public const GENERIC = 300; + public const S_IPV6TAG = 301; + public const INVALID = 302; + public const CRLF = 1310; + public const S_DOUBLECOLON = 5858; + public const ASCII_INVALID_FROM = 127; + public const ASCII_INVALID_TO = 199; + + /** + * US-ASCII visible characters not valid for atext (@link http://tools.ietf.org/html/rfc5322#section-3.2.3) + * + * @var array + */ + protected $charValue = [ + '{' => self::S_OPENCURLYBRACES, + '}' => self::S_CLOSECURLYBRACES, + '(' => self::S_OPENPARENTHESIS, + ')' => self::S_CLOSEPARENTHESIS, + '<' => self::S_LOWERTHAN, + '>' => self::S_GREATERTHAN, + '[' => self::S_OPENBRACKET, + ']' => self::S_CLOSEBRACKET, + ':' => self::S_COLON, + ';' => self::S_SEMICOLON, + '@' => self::S_AT, + '\\' => self::S_BACKSLASH, + '/' => self::S_SLASH, + ',' => self::S_COMMA, + '.' => self::S_DOT, + "'" => self::S_SQUOTE, + "`" => self::S_BACKTICK, + '"' => self::S_DQUOTE, + '-' => self::S_HYPHEN, + '::' => self::S_DOUBLECOLON, + ' ' => self::S_SP, + "\t" => self::S_HTAB, + "\r" => self::S_CR, + "\n" => self::S_LF, + "\r\n" => self::CRLF, + 'IPv6' => self::S_IPV6TAG, + '' => self::S_EMPTY, + '\0' => self::C_NUL, + '*' => self::ASTERISK, + '!' => self::EXCLAMATION, + '&' => self::AMPERSAND, + '^' => self::CARET, + '$' => self::DOLLAR, + '%' => self::PERCENTAGE, + '~' => self::S_TILDE, + '|' => self::S_PIPE, + '_' => self::S_UNDERSCORE, + '=' => self::S_EQUAL, + '+' => self::S_PLUS, + '¿' => self::INVERT_QUESTIONMARK, + '?' => self::QUESTIONMARK, + '#' => self::NUMBER_SIGN, + '¡' => self::INVERT_EXCLAMATION, + ]; + + public const INVALID_CHARS_REGEX = "/[^\p{S}\p{C}\p{Cc}]+/iu"; + + public const VALID_UTF8_REGEX = '/\p{Cc}+/u'; + + public const CATCHABLE_PATTERNS = [ + '[a-zA-Z]+[46]?', //ASCII and domain literal + '[^\x00-\x7F]', //UTF-8 + '[0-9]+', + '\r\n', + '::', + '\s+?', + '.', + ]; + + public const NON_CATCHABLE_PATTERNS = [ + '[\xA0-\xff]+', + ]; + + public const MODIFIERS = 'iu'; + + /** @var bool */ + protected $hasInvalidTokens = false; + + /** + * @var Token + */ + protected Token $previous; + + /** + * The last matched/seen token. + * + * @var Token + */ + public Token $current; + + /** + * @var Token + */ + private Token $nullToken; + + /** @var string */ + private $accumulator = ''; + + /** @var bool */ + private $hasToRecord = false; + + public function __construct() + { + /** @var Token $nullToken */ + $nullToken = new Token('', self::S_EMPTY, 0); + $this->nullToken = $nullToken; + + $this->current = $this->previous = $this->nullToken; + $this->lookahead = null; + } + + public function reset(): void + { + $this->hasInvalidTokens = false; + parent::reset(); + $this->current = $this->previous = $this->nullToken; + } + + /** + * @param int $type + * @throws \UnexpectedValueException + * @return boolean + * + */ + public function find($type): bool + { + $search = clone $this; + $search->skipUntil($type); + + if (!$search->lookahead) { + throw new \UnexpectedValueException($type . ' not found'); + } + return true; + } + + /** + * moveNext + * + * @return boolean + */ + public function moveNext(): bool + { + if ($this->hasToRecord && $this->previous === $this->nullToken) { + $this->accumulator .= $this->current->value; + } + + $this->previous = $this->current; + + if ($this->lookahead === null) { + $this->lookahead = $this->nullToken; + } + + $hasNext = parent::moveNext(); + $this->current = $this->token ?? $this->nullToken; + + if ($this->hasToRecord) { + $this->accumulator .= $this->current->value; + } + + return $hasNext; + } + + /** + * Retrieve token type. Also processes the token value if necessary. + * + * @param string $value + * @throws \InvalidArgumentException + * @return integer + */ + protected function getType(&$value): int + { + $encoded = $value; + + if (mb_detect_encoding($value, 'auto', true) !== 'UTF-8') { + $encoded = mb_convert_encoding($value, 'UTF-8', 'Windows-1252'); + } + + if ($this->isValid($encoded)) { + return $this->charValue[$encoded]; + } + + if ($this->isNullType($encoded)) { + return self::C_NUL; + } + + if ($this->isInvalidChar($encoded)) { + $this->hasInvalidTokens = true; + return self::INVALID; + } + + return self::GENERIC; + } + + protected function isValid(string $value): bool + { + return isset($this->charValue[$value]); + } + + protected function isNullType(string $value): bool + { + return $value === "\0"; + } + + protected function isInvalidChar(string $value): bool + { + return !preg_match(self::INVALID_CHARS_REGEX, $value); + } + + protected function isUTF8Invalid(string $value): bool + { + return preg_match(self::VALID_UTF8_REGEX, $value) !== false; + } + + public function hasInvalidTokens(): bool + { + return $this->hasInvalidTokens; + } + + /** + * getPrevious + * + * @return Token + */ + public function getPrevious(): Token + { + return $this->previous; + } + + /** + * Lexical catchable patterns. + * + * @return string[] + */ + protected function getCatchablePatterns(): array + { + return self::CATCHABLE_PATTERNS; + } + + /** + * Lexical non-catchable patterns. + * + * @return string[] + */ + protected function getNonCatchablePatterns(): array + { + return self::NON_CATCHABLE_PATTERNS; + } + + protected function getModifiers(): string + { + return self::MODIFIERS; + } + + public function getAccumulatedValues(): string + { + return $this->accumulator; + } + + public function startRecording(): void + { + $this->hasToRecord = true; + } + + public function stopRecording(): void + { + $this->hasToRecord = false; + } + + public function clearRecorded(): void + { + $this->accumulator = ''; + } +} diff --git a/3rdparty/egulias/email-validator/src/EmailParser.php b/3rdparty/egulias/email-validator/src/EmailParser.php new file mode 100644 index 00000000..fc449c76 --- /dev/null +++ b/3rdparty/egulias/email-validator/src/EmailParser.php @@ -0,0 +1,90 @@ +addLongEmailWarning($this->localPart, $this->domainPart); + + return $result; + } + + protected function preLeftParsing(): Result + { + if (!$this->hasAtToken()) { + return new InvalidEmail(new NoLocalPart(), $this->lexer->current->value); + } + return new ValidEmail(); + } + + protected function parseLeftFromAt(): Result + { + return $this->processLocalPart(); + } + + protected function parseRightFromAt(): Result + { + return $this->processDomainPart(); + } + + private function processLocalPart(): Result + { + $localPartParser = new LocalPart($this->lexer); + $localPartResult = $localPartParser->parse(); + $this->localPart = $localPartParser->localPart(); + $this->warnings = [...$localPartParser->getWarnings(), ...$this->warnings]; + + return $localPartResult; + } + + private function processDomainPart(): Result + { + $domainPartParser = new DomainPart($this->lexer); + $domainPartResult = $domainPartParser->parse(); + $this->domainPart = $domainPartParser->domainPart(); + $this->warnings = [...$domainPartParser->getWarnings(), ...$this->warnings]; + + return $domainPartResult; + } + + public function getDomainPart(): string + { + return $this->domainPart; + } + + public function getLocalPart(): string + { + return $this->localPart; + } + + private function addLongEmailWarning(string $localPart, string $parsedDomainPart): void + { + if (strlen($localPart . '@' . $parsedDomainPart) > self::EMAIL_MAX_LENGTH) { + $this->warnings[EmailTooLong::CODE] = new EmailTooLong(); + } + } +} diff --git a/3rdparty/egulias/email-validator/src/EmailValidator.php b/3rdparty/egulias/email-validator/src/EmailValidator.php new file mode 100644 index 00000000..5a2e5c82 --- /dev/null +++ b/3rdparty/egulias/email-validator/src/EmailValidator.php @@ -0,0 +1,67 @@ +lexer = new EmailLexer(); + } + + /** + * @param string $email + * @param EmailValidation $emailValidation + * @return bool + */ + public function isValid(string $email, EmailValidation $emailValidation) + { + $isValid = $emailValidation->isValid($email, $this->lexer); + $this->warnings = $emailValidation->getWarnings(); + $this->error = $emailValidation->getError(); + + return $isValid; + } + + /** + * @return boolean + */ + public function hasWarnings() + { + return !empty($this->warnings); + } + + /** + * @return array + */ + public function getWarnings() + { + return $this->warnings; + } + + /** + * @return InvalidEmail|null + */ + public function getError() + { + return $this->error; + } +} diff --git a/3rdparty/egulias/email-validator/src/MessageIDParser.php b/3rdparty/egulias/email-validator/src/MessageIDParser.php new file mode 100644 index 00000000..35bd0a7f --- /dev/null +++ b/3rdparty/egulias/email-validator/src/MessageIDParser.php @@ -0,0 +1,91 @@ +addLongEmailWarning($this->idLeft, $this->idRight); + + return $result; + } + + protected function preLeftParsing(): Result + { + if (!$this->hasAtToken()) { + return new InvalidEmail(new NoLocalPart(), $this->lexer->current->value); + } + return new ValidEmail(); + } + + protected function parseLeftFromAt(): Result + { + return $this->processIDLeft(); + } + + protected function parseRightFromAt(): Result + { + return $this->processIDRight(); + } + + private function processIDLeft(): Result + { + $localPartParser = new IDLeftPart($this->lexer); + $localPartResult = $localPartParser->parse(); + $this->idLeft = $localPartParser->localPart(); + $this->warnings = [...$localPartParser->getWarnings(), ...$this->warnings]; + + return $localPartResult; + } + + private function processIDRight(): Result + { + $domainPartParser = new IDRightPart($this->lexer); + $domainPartResult = $domainPartParser->parse(); + $this->idRight = $domainPartParser->domainPart(); + $this->warnings = [...$domainPartParser->getWarnings(), ...$this->warnings]; + + return $domainPartResult; + } + + public function getLeftPart(): string + { + return $this->idLeft; + } + + public function getRightPart(): string + { + return $this->idRight; + } + + private function addLongEmailWarning(string $localPart, string $parsedDomainPart): void + { + if (strlen($localPart . '@' . $parsedDomainPart) > self::EMAILID_MAX_LENGTH) { + $this->warnings[EmailTooLong::CODE] = new EmailTooLong(); + } + } +} diff --git a/3rdparty/egulias/email-validator/src/Parser.php b/3rdparty/egulias/email-validator/src/Parser.php new file mode 100644 index 00000000..d577e3ea --- /dev/null +++ b/3rdparty/egulias/email-validator/src/Parser.php @@ -0,0 +1,78 @@ +lexer = $lexer; + } + + public function parse(string $str): Result + { + $this->lexer->setInput($str); + + if ($this->lexer->hasInvalidTokens()) { + return new InvalidEmail(new ExpectingATEXT("Invalid tokens found"), $this->lexer->current->value); + } + + $preParsingResult = $this->preLeftParsing(); + if ($preParsingResult->isInvalid()) { + return $preParsingResult; + } + + $localPartResult = $this->parseLeftFromAt(); + + if ($localPartResult->isInvalid()) { + return $localPartResult; + } + + $domainPartResult = $this->parseRightFromAt(); + + if ($domainPartResult->isInvalid()) { + return $domainPartResult; + } + + return new ValidEmail(); + } + + /** + * @return Warning\Warning[] + */ + public function getWarnings(): array + { + return $this->warnings; + } + + protected function hasAtToken(): bool + { + $this->lexer->moveNext(); + $this->lexer->moveNext(); + + return !$this->lexer->current->isA(EmailLexer::S_AT); + } +} diff --git a/3rdparty/egulias/email-validator/src/Parser/Comment.php b/3rdparty/egulias/email-validator/src/Parser/Comment.php new file mode 100644 index 00000000..7b5b47e2 --- /dev/null +++ b/3rdparty/egulias/email-validator/src/Parser/Comment.php @@ -0,0 +1,102 @@ +lexer = $lexer; + $this->commentStrategy = $commentStrategy; + } + + public function parse(): Result + { + if ($this->lexer->current->isA(EmailLexer::S_OPENPARENTHESIS)) { + $this->openedParenthesis++; + if ($this->noClosingParenthesis()) { + return new InvalidEmail(new UnclosedComment(), $this->lexer->current->value); + } + } + + if ($this->lexer->current->isA(EmailLexer::S_CLOSEPARENTHESIS)) { + return new InvalidEmail(new UnOpenedComment(), $this->lexer->current->value); + } + + $this->warnings[WarningComment::CODE] = new WarningComment(); + + $moreTokens = true; + while ($this->commentStrategy->exitCondition($this->lexer, $this->openedParenthesis) && $moreTokens) { + + if ($this->lexer->isNextToken(EmailLexer::S_OPENPARENTHESIS)) { + $this->openedParenthesis++; + } + $this->warnEscaping(); + if ($this->lexer->isNextToken(EmailLexer::S_CLOSEPARENTHESIS)) { + $this->openedParenthesis--; + } + $moreTokens = $this->lexer->moveNext(); + } + + if ($this->openedParenthesis >= 1) { + return new InvalidEmail(new UnclosedComment(), $this->lexer->current->value); + } + if ($this->openedParenthesis < 0) { + return new InvalidEmail(new UnOpenedComment(), $this->lexer->current->value); + } + + $finalValidations = $this->commentStrategy->endOfLoopValidations($this->lexer); + + $this->warnings = [...$this->warnings, ...$this->commentStrategy->getWarnings()]; + + return $finalValidations; + } + + + /** + * @return void + */ + private function warnEscaping(): void + { + //Backslash found + if (!$this->lexer->current->isA(EmailLexer::S_BACKSLASH)) { + return; + } + + if (!$this->lexer->isNextTokenAny(array(EmailLexer::S_SP, EmailLexer::S_HTAB, EmailLexer::C_DEL))) { + return; + } + + $this->warnings[QuotedPart::CODE] = + new QuotedPart($this->lexer->getPrevious()->type, $this->lexer->current->type); + } + + private function noClosingParenthesis(): bool + { + try { + $this->lexer->find(EmailLexer::S_CLOSEPARENTHESIS); + return false; + } catch (\RuntimeException $e) { + return true; + } + } +} diff --git a/3rdparty/egulias/email-validator/src/Parser/CommentStrategy/CommentStrategy.php b/3rdparty/egulias/email-validator/src/Parser/CommentStrategy/CommentStrategy.php new file mode 100644 index 00000000..8834db04 --- /dev/null +++ b/3rdparty/egulias/email-validator/src/Parser/CommentStrategy/CommentStrategy.php @@ -0,0 +1,22 @@ +isNextToken(EmailLexer::S_DOT)); + } + + public function endOfLoopValidations(EmailLexer $lexer): Result + { + //test for end of string + if (!$lexer->isNextToken(EmailLexer::S_DOT)) { + return new InvalidEmail(new ExpectingATEXT('DOT not found near CLOSEPARENTHESIS'), $lexer->current->value); + } + //add warning + //Address is valid within the message but cannot be used unmodified for the envelope + return new ValidEmail(); + } + + public function getWarnings(): array + { + return []; + } +} diff --git a/3rdparty/egulias/email-validator/src/Parser/CommentStrategy/LocalComment.php b/3rdparty/egulias/email-validator/src/Parser/CommentStrategy/LocalComment.php new file mode 100644 index 00000000..5f30a90b --- /dev/null +++ b/3rdparty/egulias/email-validator/src/Parser/CommentStrategy/LocalComment.php @@ -0,0 +1,38 @@ + + */ + private $warnings = []; + + public function exitCondition(EmailLexer $lexer, int $openedParenthesis): bool + { + return !$lexer->isNextToken(EmailLexer::S_AT); + } + + public function endOfLoopValidations(EmailLexer $lexer): Result + { + if (!$lexer->isNextToken(EmailLexer::S_AT)) { + return new InvalidEmail(new ExpectingATEXT('ATEX is not expected after closing comments'), $lexer->current->value); + } + $this->warnings[CFWSNearAt::CODE] = new CFWSNearAt(); + return new ValidEmail(); + } + + public function getWarnings(): array + { + return $this->warnings; + } +} diff --git a/3rdparty/egulias/email-validator/src/Parser/DomainLiteral.php b/3rdparty/egulias/email-validator/src/Parser/DomainLiteral.php new file mode 100644 index 00000000..5093e508 --- /dev/null +++ b/3rdparty/egulias/email-validator/src/Parser/DomainLiteral.php @@ -0,0 +1,210 @@ +addTagWarnings(); + + $IPv6TAG = false; + $addressLiteral = ''; + + do { + if ($this->lexer->current->isA(EmailLexer::C_NUL)) { + return new InvalidEmail(new ExpectingDTEXT(), $this->lexer->current->value); + } + + $this->addObsoleteWarnings(); + + if ($this->lexer->isNextTokenAny(array(EmailLexer::S_OPENBRACKET, EmailLexer::S_OPENBRACKET))) { + return new InvalidEmail(new ExpectingDTEXT(), $this->lexer->current->value); + } + + if ($this->lexer->isNextTokenAny( + array(EmailLexer::S_HTAB, EmailLexer::S_SP, EmailLexer::CRLF) + )) { + $this->warnings[CFWSWithFWS::CODE] = new CFWSWithFWS(); + $this->parseFWS(); + } + + if ($this->lexer->isNextToken(EmailLexer::S_CR)) { + return new InvalidEmail(new CRNoLF(), $this->lexer->current->value); + } + + if ($this->lexer->current->isA(EmailLexer::S_BACKSLASH)) { + return new InvalidEmail(new UnusualElements($this->lexer->current->value), $this->lexer->current->value); + } + if ($this->lexer->current->isA(EmailLexer::S_IPV6TAG)) { + $IPv6TAG = true; + } + + if ($this->lexer->current->isA(EmailLexer::S_CLOSEBRACKET)) { + break; + } + + $addressLiteral .= $this->lexer->current->value; + } while ($this->lexer->moveNext()); + + + //Encapsulate + $addressLiteral = str_replace('[', '', $addressLiteral); + $isAddressLiteralIPv4 = $this->checkIPV4Tag($addressLiteral); + + if (!$isAddressLiteralIPv4) { + return new ValidEmail(); + } + + $addressLiteral = $this->convertIPv4ToIPv6($addressLiteral); + + if (!$IPv6TAG) { + $this->warnings[WarningDomainLiteral::CODE] = new WarningDomainLiteral(); + return new ValidEmail(); + } + + $this->warnings[AddressLiteral::CODE] = new AddressLiteral(); + + $this->checkIPV6Tag($addressLiteral); + + return new ValidEmail(); + } + + /** + * @param string $addressLiteral + * @param int $maxGroups + */ + public function checkIPV6Tag($addressLiteral, $maxGroups = 8): void + { + $prev = $this->lexer->getPrevious(); + if ($prev->isA(EmailLexer::S_COLON)) { + $this->warnings[IPV6ColonEnd::CODE] = new IPV6ColonEnd(); + } + + $IPv6 = substr($addressLiteral, 5); + //Daniel Marschall's new IPv6 testing strategy + $matchesIP = explode(':', $IPv6); + $groupCount = count($matchesIP); + $colons = strpos($IPv6, '::'); + + if (count(preg_grep('/^[0-9A-Fa-f]{0,4}$/', $matchesIP, PREG_GREP_INVERT)) !== 0) { + $this->warnings[IPV6BadChar::CODE] = new IPV6BadChar(); + } + + if ($colons === false) { + // We need exactly the right number of groups + if ($groupCount !== $maxGroups) { + $this->warnings[IPV6GroupCount::CODE] = new IPV6GroupCount(); + } + return; + } + + if ($colons !== strrpos($IPv6, '::')) { + $this->warnings[IPV6DoubleColon::CODE] = new IPV6DoubleColon(); + return; + } + + if ($colons === 0 || $colons === (strlen($IPv6) - 2)) { + // RFC 4291 allows :: at the start or end of an address + //with 7 other groups in addition + ++$maxGroups; + } + + if ($groupCount > $maxGroups) { + $this->warnings[IPV6MaxGroups::CODE] = new IPV6MaxGroups(); + } elseif ($groupCount === $maxGroups) { + $this->warnings[IPV6Deprecated::CODE] = new IPV6Deprecated(); + } + } + + public function convertIPv4ToIPv6(string $addressLiteralIPv4): string + { + $matchesIP = []; + $IPv4Match = preg_match(self::IPV4_REGEX, $addressLiteralIPv4, $matchesIP); + + // Extract IPv4 part from the end of the address-literal (if there is one) + if ($IPv4Match > 0) { + $index = (int) strrpos($addressLiteralIPv4, $matchesIP[0]); + //There's a match but it is at the start + if ($index > 0) { + // Convert IPv4 part to IPv6 format for further testing + return substr($addressLiteralIPv4, 0, $index) . '0:0'; + } + } + + return $addressLiteralIPv4; + } + + /** + * @param string $addressLiteral + * + * @return bool + */ + protected function checkIPV4Tag($addressLiteral): bool + { + $matchesIP = []; + $IPv4Match = preg_match(self::IPV4_REGEX, $addressLiteral, $matchesIP); + + // Extract IPv4 part from the end of the address-literal (if there is one) + + if ($IPv4Match > 0) { + $index = strrpos($addressLiteral, $matchesIP[0]); + //There's a match but it is at the start + if ($index === 0) { + $this->warnings[AddressLiteral::CODE] = new AddressLiteral(); + return false; + } + } + + return true; + } + + private function addObsoleteWarnings(): void + { + if (in_array($this->lexer->current->type, self::OBSOLETE_WARNINGS)) { + $this->warnings[ObsoleteDTEXT::CODE] = new ObsoleteDTEXT(); + } + } + + private function addTagWarnings(): void + { + if ($this->lexer->isNextToken(EmailLexer::S_COLON)) { + $this->warnings[IPV6ColonStart::CODE] = new IPV6ColonStart(); + } + if ($this->lexer->isNextToken(EmailLexer::S_IPV6TAG)) { + $lexer = clone $this->lexer; + $lexer->moveNext(); + if ($lexer->isNextToken(EmailLexer::S_DOUBLECOLON)) { + $this->warnings[IPV6ColonStart::CODE] = new IPV6ColonStart(); + } + } + } +} diff --git a/3rdparty/egulias/email-validator/src/Parser/DomainPart.php b/3rdparty/egulias/email-validator/src/Parser/DomainPart.php new file mode 100644 index 00000000..3b6284b7 --- /dev/null +++ b/3rdparty/egulias/email-validator/src/Parser/DomainPart.php @@ -0,0 +1,327 @@ +lexer->clearRecorded(); + $this->lexer->startRecording(); + + $this->lexer->moveNext(); + + $domainChecks = $this->performDomainStartChecks(); + if ($domainChecks->isInvalid()) { + return $domainChecks; + } + + if ($this->lexer->current->isA(EmailLexer::S_AT)) { + return new InvalidEmail(new ConsecutiveAt(), $this->lexer->current->value); + } + + $result = $this->doParseDomainPart(); + if ($result->isInvalid()) { + return $result; + } + + $end = $this->checkEndOfDomain(); + if ($end->isInvalid()) { + return $end; + } + + $this->lexer->stopRecording(); + $this->domainPart = $this->lexer->getAccumulatedValues(); + + $length = strlen($this->domainPart); + if ($length > self::DOMAIN_MAX_LENGTH) { + return new InvalidEmail(new DomainTooLong(), $this->lexer->current->value); + } + + return new ValidEmail(); + } + + private function checkEndOfDomain(): Result + { + $prev = $this->lexer->getPrevious(); + if ($prev->isA(EmailLexer::S_DOT)) { + return new InvalidEmail(new DotAtEnd(), $this->lexer->current->value); + } + if ($prev->isA(EmailLexer::S_HYPHEN)) { + return new InvalidEmail(new DomainHyphened('Hypen found at the end of the domain'), $prev->value); + } + + if ($this->lexer->current->isA(EmailLexer::S_SP)) { + return new InvalidEmail(new CRLFAtTheEnd(), $prev->value); + } + return new ValidEmail(); + } + + private function performDomainStartChecks(): Result + { + $invalidTokens = $this->checkInvalidTokensAfterAT(); + if ($invalidTokens->isInvalid()) { + return $invalidTokens; + } + + $missingDomain = $this->checkEmptyDomain(); + if ($missingDomain->isInvalid()) { + return $missingDomain; + } + + if ($this->lexer->current->isA(EmailLexer::S_OPENPARENTHESIS)) { + $this->warnings[DeprecatedComment::CODE] = new DeprecatedComment(); + } + return new ValidEmail(); + } + + private function checkEmptyDomain(): Result + { + $thereIsNoDomain = $this->lexer->current->isA(EmailLexer::S_EMPTY) || + ($this->lexer->current->isA(EmailLexer::S_SP) && + !$this->lexer->isNextToken(EmailLexer::GENERIC)); + + if ($thereIsNoDomain) { + return new InvalidEmail(new NoDomainPart(), $this->lexer->current->value); + } + + return new ValidEmail(); + } + + private function checkInvalidTokensAfterAT(): Result + { + if ($this->lexer->current->isA(EmailLexer::S_DOT)) { + return new InvalidEmail(new DotAtStart(), $this->lexer->current->value); + } + if ($this->lexer->current->isA(EmailLexer::S_HYPHEN)) { + return new InvalidEmail(new DomainHyphened('After AT'), $this->lexer->current->value); + } + return new ValidEmail(); + } + + protected function parseComments(): Result + { + $commentParser = new Comment($this->lexer, new DomainComment()); + $result = $commentParser->parse(); + $this->warnings = [...$this->warnings, ...$commentParser->getWarnings()]; + + return $result; + } + + protected function doParseDomainPart(): Result + { + $tldMissing = true; + $hasComments = false; + $domain = ''; + do { + $prev = $this->lexer->getPrevious(); + + $notAllowedChars = $this->checkNotAllowedChars($this->lexer->current); + if ($notAllowedChars->isInvalid()) { + return $notAllowedChars; + } + + if ( + $this->lexer->current->isA(EmailLexer::S_OPENPARENTHESIS) || + $this->lexer->current->isA(EmailLexer::S_CLOSEPARENTHESIS) + ) { + $hasComments = true; + $commentsResult = $this->parseComments(); + + //Invalid comment parsing + if ($commentsResult->isInvalid()) { + return $commentsResult; + } + } + + $dotsResult = $this->checkConsecutiveDots(); + if ($dotsResult->isInvalid()) { + return $dotsResult; + } + + if ($this->lexer->current->isA(EmailLexer::S_OPENBRACKET)) { + $literalResult = $this->parseDomainLiteral(); + + $this->addTLDWarnings($tldMissing); + return $literalResult; + } + + $labelCheck = $this->checkLabelLength(); + if ($labelCheck->isInvalid()) { + return $labelCheck; + } + + $FwsResult = $this->parseFWS(); + if ($FwsResult->isInvalid()) { + return $FwsResult; + } + + $domain .= $this->lexer->current->value; + + if ($this->lexer->current->isA(EmailLexer::S_DOT) && $this->lexer->isNextToken(EmailLexer::GENERIC)) { + $tldMissing = false; + } + + $exceptionsResult = $this->checkDomainPartExceptions($prev, $hasComments); + if ($exceptionsResult->isInvalid()) { + return $exceptionsResult; + } + $this->lexer->moveNext(); + } while (!$this->lexer->current->isA(EmailLexer::S_EMPTY)); + + $labelCheck = $this->checkLabelLength(true); + if ($labelCheck->isInvalid()) { + return $labelCheck; + } + $this->addTLDWarnings($tldMissing); + + $this->domainPart = $domain; + return new ValidEmail(); + } + + /** + * @param Token $token + * + * @return Result + */ + private function checkNotAllowedChars(Token $token): Result + { + $notAllowed = [EmailLexer::S_BACKSLASH => true, EmailLexer::S_SLASH => true]; + if (isset($notAllowed[$token->type])) { + return new InvalidEmail(new CharNotAllowed(), $token->value); + } + return new ValidEmail(); + } + + /** + * @return Result + */ + protected function parseDomainLiteral(): Result + { + try { + $this->lexer->find(EmailLexer::S_CLOSEBRACKET); + } catch (\RuntimeException $e) { + return new InvalidEmail(new ExpectingDomainLiteralClose(), $this->lexer->current->value); + } + + $domainLiteralParser = new DomainLiteralParser($this->lexer); + $result = $domainLiteralParser->parse(); + $this->warnings = [...$this->warnings, ...$domainLiteralParser->getWarnings()]; + return $result; + } + + /** + * @param Token $prev + * @param bool $hasComments + * + * @return Result + */ + protected function checkDomainPartExceptions(Token $prev, bool $hasComments): Result + { + if ($this->lexer->current->isA(EmailLexer::S_OPENBRACKET) && $prev->type !== EmailLexer::S_AT) { + return new InvalidEmail(new ExpectingATEXT('OPENBRACKET not after AT'), $this->lexer->current->value); + } + + if ($this->lexer->current->isA(EmailLexer::S_HYPHEN) && $this->lexer->isNextToken(EmailLexer::S_DOT)) { + return new InvalidEmail(new DomainHyphened('Hypen found near DOT'), $this->lexer->current->value); + } + + if ( + $this->lexer->current->isA(EmailLexer::S_BACKSLASH) + && $this->lexer->isNextToken(EmailLexer::GENERIC) + ) { + return new InvalidEmail(new ExpectingATEXT('Escaping following "ATOM"'), $this->lexer->current->value); + } + + return $this->validateTokens($hasComments); + } + + protected function validateTokens(bool $hasComments): Result + { + $validDomainTokens = array( + EmailLexer::GENERIC => true, + EmailLexer::S_HYPHEN => true, + EmailLexer::S_DOT => true, + ); + + if ($hasComments) { + $validDomainTokens[EmailLexer::S_OPENPARENTHESIS] = true; + $validDomainTokens[EmailLexer::S_CLOSEPARENTHESIS] = true; + } + + if (!isset($validDomainTokens[$this->lexer->current->type])) { + return new InvalidEmail(new ExpectingATEXT('Invalid token in domain: ' . $this->lexer->current->value), $this->lexer->current->value); + } + + return new ValidEmail(); + } + + private function checkLabelLength(bool $isEndOfDomain = false): Result + { + if ($this->lexer->current->isA(EmailLexer::S_DOT) || $isEndOfDomain) { + if ($this->isLabelTooLong($this->label)) { + return new InvalidEmail(new LabelTooLong(), $this->lexer->current->value); + } + $this->label = ''; + } + $this->label .= $this->lexer->current->value; + return new ValidEmail(); + } + + + private function isLabelTooLong(string $label): bool + { + if (preg_match('/[^\x00-\x7F]/', $label)) { + idn_to_ascii($label, IDNA_DEFAULT, INTL_IDNA_VARIANT_UTS46, $idnaInfo); + /** @psalm-var array{errors: int, ...} $idnaInfo */ + return (bool) ($idnaInfo['errors'] & IDNA_ERROR_LABEL_TOO_LONG); + } + return strlen($label) > self::LABEL_MAX_LENGTH; + } + + private function addTLDWarnings(bool $isTLDMissing): void + { + if ($isTLDMissing) { + $this->warnings[TLD::CODE] = new TLD(); + } + } + + public function domainPart(): string + { + return $this->domainPart; + } +} diff --git a/3rdparty/egulias/email-validator/src/Parser/DoubleQuote.php b/3rdparty/egulias/email-validator/src/Parser/DoubleQuote.php new file mode 100644 index 00000000..b5335d30 --- /dev/null +++ b/3rdparty/egulias/email-validator/src/Parser/DoubleQuote.php @@ -0,0 +1,91 @@ +checkDQUOTE(); + if ($validQuotedString->isInvalid()) { + return $validQuotedString; + } + + $special = [ + EmailLexer::S_CR => true, + EmailLexer::S_HTAB => true, + EmailLexer::S_LF => true + ]; + + $invalid = [ + EmailLexer::C_NUL => true, + EmailLexer::S_HTAB => true, + EmailLexer::S_CR => true, + EmailLexer::S_LF => true + ]; + + $setSpecialsWarning = true; + + $this->lexer->moveNext(); + + while (!$this->lexer->current->isA(EmailLexer::S_DQUOTE) && !$this->lexer->current->isA(EmailLexer::S_EMPTY)) { + if (isset($special[$this->lexer->current->type]) && $setSpecialsWarning) { + $this->warnings[CFWSWithFWS::CODE] = new CFWSWithFWS(); + $setSpecialsWarning = false; + } + if ($this->lexer->current->isA(EmailLexer::S_BACKSLASH) && $this->lexer->isNextToken(EmailLexer::S_DQUOTE)) { + $this->lexer->moveNext(); + } + + $this->lexer->moveNext(); + + if (!$this->escaped() && isset($invalid[$this->lexer->current->type])) { + return new InvalidEmail(new ExpectingATEXT("Expecting ATEXT between DQUOTE"), $this->lexer->current->value); + } + } + + $prev = $this->lexer->getPrevious(); + + if ($prev->isA(EmailLexer::S_BACKSLASH)) { + $validQuotedString = $this->checkDQUOTE(); + if ($validQuotedString->isInvalid()) { + return $validQuotedString; + } + } + + if (!$this->lexer->isNextToken(EmailLexer::S_AT) && !$prev->isA(EmailLexer::S_BACKSLASH)) { + return new InvalidEmail(new ExpectingATEXT("Expecting ATEXT between DQUOTE"), $this->lexer->current->value); + } + + return new ValidEmail(); + } + + protected function checkDQUOTE(): Result + { + $previous = $this->lexer->getPrevious(); + + if ($this->lexer->isNextToken(EmailLexer::GENERIC) && $previous->isA(EmailLexer::GENERIC)) { + $description = 'https://tools.ietf.org/html/rfc5322#section-3.2.4 - quoted string should be a unit'; + return new InvalidEmail(new ExpectingATEXT($description), $this->lexer->current->value); + } + + try { + $this->lexer->find(EmailLexer::S_DQUOTE); + } catch (\Exception $e) { + return new InvalidEmail(new UnclosedQuotedString(), $this->lexer->current->value); + } + $this->warnings[QuotedString::CODE] = new QuotedString($previous->value, $this->lexer->current->value); + + return new ValidEmail(); + } +} diff --git a/3rdparty/egulias/email-validator/src/Parser/FoldingWhiteSpace.php b/3rdparty/egulias/email-validator/src/Parser/FoldingWhiteSpace.php new file mode 100644 index 00000000..348a7af4 --- /dev/null +++ b/3rdparty/egulias/email-validator/src/Parser/FoldingWhiteSpace.php @@ -0,0 +1,87 @@ +isFWS()) { + return new ValidEmail(); + } + + $previous = $this->lexer->getPrevious(); + + $resultCRLF = $this->checkCRLFInFWS(); + if ($resultCRLF->isInvalid()) { + return $resultCRLF; + } + + if ($this->lexer->current->isA(EmailLexer::S_CR)) { + return new InvalidEmail(new CRNoLF(), $this->lexer->current->value); + } + + if ($this->lexer->isNextToken(EmailLexer::GENERIC) && !$previous->isA(EmailLexer::S_AT)) { + return new InvalidEmail(new AtextAfterCFWS(), $this->lexer->current->value); + } + + if ($this->lexer->current->isA(EmailLexer::S_LF) || $this->lexer->current->isA(EmailLexer::C_NUL)) { + return new InvalidEmail(new ExpectingCTEXT(), $this->lexer->current->value); + } + + if ($this->lexer->isNextToken(EmailLexer::S_AT) || $previous->isA(EmailLexer::S_AT)) { + $this->warnings[CFWSNearAt::CODE] = new CFWSNearAt(); + } else { + $this->warnings[CFWSWithFWS::CODE] = new CFWSWithFWS(); + } + + return new ValidEmail(); + } + + protected function checkCRLFInFWS(): Result + { + if (!$this->lexer->current->isA(EmailLexer::CRLF)) { + return new ValidEmail(); + } + + if (!$this->lexer->isNextTokenAny(array(EmailLexer::S_SP, EmailLexer::S_HTAB))) { + return new InvalidEmail(new CRLFX2(), $this->lexer->current->value); + } + + //this has no coverage. Condition is repeated from above one + if (!$this->lexer->isNextTokenAny(array(EmailLexer::S_SP, EmailLexer::S_HTAB))) { + return new InvalidEmail(new CRLFAtTheEnd(), $this->lexer->current->value); + } + + return new ValidEmail(); + } + + protected function isFWS(): bool + { + if ($this->escaped()) { + return false; + } + + return in_array($this->lexer->current->type, self::FWS_TYPES); + } +} diff --git a/3rdparty/egulias/email-validator/src/Parser/IDLeftPart.php b/3rdparty/egulias/email-validator/src/Parser/IDLeftPart.php new file mode 100644 index 00000000..bedcf7b2 --- /dev/null +++ b/3rdparty/egulias/email-validator/src/Parser/IDLeftPart.php @@ -0,0 +1,15 @@ +lexer->current->value); + } +} diff --git a/3rdparty/egulias/email-validator/src/Parser/IDRightPart.php b/3rdparty/egulias/email-validator/src/Parser/IDRightPart.php new file mode 100644 index 00000000..d2fc1d74 --- /dev/null +++ b/3rdparty/egulias/email-validator/src/Parser/IDRightPart.php @@ -0,0 +1,29 @@ + true, + EmailLexer::S_SQUOTE => true, + EmailLexer::S_BACKTICK => true, + EmailLexer::S_SEMICOLON => true, + EmailLexer::S_GREATERTHAN => true, + EmailLexer::S_LOWERTHAN => true, + ]; + + if (isset($invalidDomainTokens[$this->lexer->current->type])) { + return new InvalidEmail(new ExpectingATEXT('Invalid token in domain: ' . $this->lexer->current->value), $this->lexer->current->value); + } + return new ValidEmail(); + } +} diff --git a/3rdparty/egulias/email-validator/src/Parser/LocalPart.php b/3rdparty/egulias/email-validator/src/Parser/LocalPart.php new file mode 100644 index 00000000..10f95654 --- /dev/null +++ b/3rdparty/egulias/email-validator/src/Parser/LocalPart.php @@ -0,0 +1,163 @@ + EmailLexer::S_COMMA, + EmailLexer::S_CLOSEBRACKET => EmailLexer::S_CLOSEBRACKET, + EmailLexer::S_OPENBRACKET => EmailLexer::S_OPENBRACKET, + EmailLexer::S_GREATERTHAN => EmailLexer::S_GREATERTHAN, + EmailLexer::S_LOWERTHAN => EmailLexer::S_LOWERTHAN, + EmailLexer::S_COLON => EmailLexer::S_COLON, + EmailLexer::S_SEMICOLON => EmailLexer::S_SEMICOLON, + EmailLexer::INVALID => EmailLexer::INVALID + ]; + + /** + * @var string + */ + private $localPart = ''; + + + public function parse(): Result + { + $this->lexer->clearRecorded(); + $this->lexer->startRecording(); + + while (!$this->lexer->current->isA(EmailLexer::S_AT) && !$this->lexer->current->isA(EmailLexer::S_EMPTY)) { + if ($this->hasDotAtStart()) { + return new InvalidEmail(new DotAtStart(), $this->lexer->current->value); + } + + if ($this->lexer->current->isA(EmailLexer::S_DQUOTE)) { + $dquoteParsingResult = $this->parseDoubleQuote(); + + //Invalid double quote parsing + if ($dquoteParsingResult->isInvalid()) { + return $dquoteParsingResult; + } + } + + if ( + $this->lexer->current->isA(EmailLexer::S_OPENPARENTHESIS) || + $this->lexer->current->isA(EmailLexer::S_CLOSEPARENTHESIS) + ) { + $commentsResult = $this->parseComments(); + + //Invalid comment parsing + if ($commentsResult->isInvalid()) { + return $commentsResult; + } + } + + if ($this->lexer->current->isA(EmailLexer::S_DOT) && $this->lexer->isNextToken(EmailLexer::S_DOT)) { + return new InvalidEmail(new ConsecutiveDot(), $this->lexer->current->value); + } + + if ( + $this->lexer->current->isA(EmailLexer::S_DOT) && + $this->lexer->isNextToken(EmailLexer::S_AT) + ) { + return new InvalidEmail(new DotAtEnd(), $this->lexer->current->value); + } + + $resultEscaping = $this->validateEscaping(); + if ($resultEscaping->isInvalid()) { + return $resultEscaping; + } + + $resultToken = $this->validateTokens(false); + if ($resultToken->isInvalid()) { + return $resultToken; + } + + $resultFWS = $this->parseLocalFWS(); + if ($resultFWS->isInvalid()) { + return $resultFWS; + } + + $this->lexer->moveNext(); + } + + $this->lexer->stopRecording(); + $this->localPart = rtrim($this->lexer->getAccumulatedValues(), '@'); + if (strlen($this->localPart) > LocalTooLong::LOCAL_PART_LENGTH) { + $this->warnings[LocalTooLong::CODE] = new LocalTooLong(); + } + + return new ValidEmail(); + } + + protected function validateTokens(bool $hasComments): Result + { + if (isset(self::INVALID_TOKENS[$this->lexer->current->type])) { + return new InvalidEmail(new ExpectingATEXT('Invalid token found'), $this->lexer->current->value); + } + return new ValidEmail(); + } + + public function localPart(): string + { + return $this->localPart; + } + + private function parseLocalFWS(): Result + { + $foldingWS = new FoldingWhiteSpace($this->lexer); + $resultFWS = $foldingWS->parse(); + if ($resultFWS->isValid()) { + $this->warnings = [...$this->warnings, ...$foldingWS->getWarnings()]; + } + return $resultFWS; + } + + private function hasDotAtStart(): bool + { + return $this->lexer->current->isA(EmailLexer::S_DOT) && $this->lexer->getPrevious()->isA(EmailLexer::S_EMPTY); + } + + private function parseDoubleQuote(): Result + { + $dquoteParser = new DoubleQuote($this->lexer); + $parseAgain = $dquoteParser->parse(); + $this->warnings = [...$this->warnings, ...$dquoteParser->getWarnings()]; + + return $parseAgain; + } + + protected function parseComments(): Result + { + $commentParser = new Comment($this->lexer, new LocalComment()); + $result = $commentParser->parse(); + $this->warnings = [...$this->warnings, ...$commentParser->getWarnings()]; + + return $result; + } + + private function validateEscaping(): Result + { + //Backslash found + if (!$this->lexer->current->isA(EmailLexer::S_BACKSLASH)) { + return new ValidEmail(); + } + + if ($this->lexer->isNextToken(EmailLexer::GENERIC)) { + return new InvalidEmail(new ExpectingATEXT('Found ATOM after escaping'), $this->lexer->current->value); + } + + return new ValidEmail(); + } +} diff --git a/3rdparty/egulias/email-validator/src/Parser/PartParser.php b/3rdparty/egulias/email-validator/src/Parser/PartParser.php new file mode 100644 index 00000000..53afb257 --- /dev/null +++ b/3rdparty/egulias/email-validator/src/Parser/PartParser.php @@ -0,0 +1,63 @@ +lexer = $lexer; + } + + abstract public function parse(): Result; + + /** + * @return Warning[] + */ + public function getWarnings() + { + return $this->warnings; + } + + protected function parseFWS(): Result + { + $foldingWS = new FoldingWhiteSpace($this->lexer); + $resultFWS = $foldingWS->parse(); + $this->warnings = [...$this->warnings, ...$foldingWS->getWarnings()]; + return $resultFWS; + } + + protected function checkConsecutiveDots(): Result + { + if ($this->lexer->current->isA(EmailLexer::S_DOT) && $this->lexer->isNextToken(EmailLexer::S_DOT)) { + return new InvalidEmail(new ConsecutiveDot(), $this->lexer->current->value); + } + + return new ValidEmail(); + } + + protected function escaped(): bool + { + $previous = $this->lexer->getPrevious(); + + return $previous->isA(EmailLexer::S_BACKSLASH) + && !$this->lexer->current->isA(EmailLexer::GENERIC); + } +} diff --git a/3rdparty/egulias/email-validator/src/Result/InvalidEmail.php b/3rdparty/egulias/email-validator/src/Result/InvalidEmail.php new file mode 100644 index 00000000..82699acc --- /dev/null +++ b/3rdparty/egulias/email-validator/src/Result/InvalidEmail.php @@ -0,0 +1,49 @@ +token = $token; + $this->reason = $reason; + } + + public function isValid(): bool + { + return false; + } + + public function isInvalid(): bool + { + return true; + } + + public function description(): string + { + return $this->reason->description() . " in char " . $this->token; + } + + public function code(): int + { + return $this->reason->code(); + } + + public function reason(): Reason + { + return $this->reason; + } +} diff --git a/3rdparty/egulias/email-validator/src/Result/MultipleErrors.php b/3rdparty/egulias/email-validator/src/Result/MultipleErrors.php new file mode 100644 index 00000000..5fa85afc --- /dev/null +++ b/3rdparty/egulias/email-validator/src/Result/MultipleErrors.php @@ -0,0 +1,56 @@ +reasons[$reason->code()] = $reason; + } + + /** + * @return Reason[] + */ + public function getReasons() : array + { + return $this->reasons; + } + + public function reason() : Reason + { + return 0 !== count($this->reasons) + ? current($this->reasons) + : new EmptyReason(); + } + + public function description() : string + { + $description = ''; + foreach($this->reasons as $reason) { + $description .= $reason->description() . PHP_EOL; + } + + return $description; + } + + public function code() : int + { + return 0; + } +} diff --git a/3rdparty/egulias/email-validator/src/Result/Reason/AtextAfterCFWS.php b/3rdparty/egulias/email-validator/src/Result/Reason/AtextAfterCFWS.php new file mode 100644 index 00000000..96e22842 --- /dev/null +++ b/3rdparty/egulias/email-validator/src/Result/Reason/AtextAfterCFWS.php @@ -0,0 +1,16 @@ +detailedDescription = $details; + } +} diff --git a/3rdparty/egulias/email-validator/src/Result/Reason/DomainAcceptsNoMail.php b/3rdparty/egulias/email-validator/src/Result/Reason/DomainAcceptsNoMail.php new file mode 100644 index 00000000..bcaefb68 --- /dev/null +++ b/3rdparty/egulias/email-validator/src/Result/Reason/DomainAcceptsNoMail.php @@ -0,0 +1,16 @@ +exception = $exception; + + } + public function code() : int + { + return 999; + } + + public function description() : string + { + return $this->exception->getMessage(); + } +} diff --git a/3rdparty/egulias/email-validator/src/Result/Reason/ExpectingATEXT.php b/3rdparty/egulias/email-validator/src/Result/Reason/ExpectingATEXT.php new file mode 100644 index 00000000..07ea8d23 --- /dev/null +++ b/3rdparty/egulias/email-validator/src/Result/Reason/ExpectingATEXT.php @@ -0,0 +1,16 @@ +detailedDescription; + } +} diff --git a/3rdparty/egulias/email-validator/src/Result/Reason/ExpectingCTEXT.php b/3rdparty/egulias/email-validator/src/Result/Reason/ExpectingCTEXT.php new file mode 100644 index 00000000..64f5f7c3 --- /dev/null +++ b/3rdparty/egulias/email-validator/src/Result/Reason/ExpectingCTEXT.php @@ -0,0 +1,16 @@ +element = $element; + } + + public function code() : int + { + return 201; + } + + public function description() : string + { + return 'Unusual element found, wourld render invalid in majority of cases. Element found: ' . $this->element; + } +} diff --git a/3rdparty/egulias/email-validator/src/Result/Result.php b/3rdparty/egulias/email-validator/src/Result/Result.php new file mode 100644 index 00000000..0e50fc51 --- /dev/null +++ b/3rdparty/egulias/email-validator/src/Result/Result.php @@ -0,0 +1,31 @@ +reason = new ReasonSpoofEmail(); + parent::__construct($this->reason, ''); + } +} diff --git a/3rdparty/egulias/email-validator/src/Result/ValidEmail.php b/3rdparty/egulias/email-validator/src/Result/ValidEmail.php new file mode 100644 index 00000000..fdc882fa --- /dev/null +++ b/3rdparty/egulias/email-validator/src/Result/ValidEmail.php @@ -0,0 +1,27 @@ +dnsGetRecord = $dnsGetRecord; + } + + public function isValid(string $email, EmailLexer $emailLexer): bool + { + // use the input to check DNS if we cannot extract something similar to a domain + $host = $email; + + // Arguable pattern to extract the domain. Not aiming to validate the domain nor the email + if (false !== $lastAtPos = strrpos($email, '@')) { + $host = substr($email, $lastAtPos + 1); + } + + // Get the domain parts + $hostParts = explode('.', $host); + + $isLocalDomain = count($hostParts) <= 1; + $isReservedTopLevel = in_array($hostParts[(count($hostParts) - 1)], self::RESERVED_DNS_TOP_LEVEL_NAMES, true); + + // Exclude reserved top level DNS names + if ($isLocalDomain || $isReservedTopLevel) { + $this->error = new InvalidEmail(new LocalOrReservedDomain(), $host); + return false; + } + + return $this->checkDns($host); + } + + public function getError(): ?InvalidEmail + { + return $this->error; + } + + /** + * @return Warning[] + */ + public function getWarnings(): array + { + return $this->warnings; + } + + /** + * @param string $host + * + * @return bool + */ + protected function checkDns($host) + { + $variant = INTL_IDNA_VARIANT_UTS46; + + $host = rtrim(idn_to_ascii($host, IDNA_DEFAULT, $variant), '.'); + + $hostParts = explode('.', $host); + $host = array_pop($hostParts); + + while (count($hostParts) > 0) { + $host = array_pop($hostParts) . '.' . $host; + + if ($this->validateDnsRecords($host)) { + return true; + } + } + + return false; + } + + + /** + * Validate the DNS records for given host. + * + * @param string $host A set of DNS records in the format returned by dns_get_record. + * + * @return bool True on success. + */ + private function validateDnsRecords($host): bool + { + $dnsRecordsResult = $this->dnsGetRecord->getRecords($host, DNS_A + DNS_MX); + + if ($dnsRecordsResult->withError()) { + $this->error = new InvalidEmail(new UnableToGetDNSRecord(), ''); + return false; + } + + $dnsRecords = $dnsRecordsResult->getRecords(); + + // Combined check for A+MX+AAAA can fail with SERVFAIL, even in the presence of valid A/MX records + $aaaaRecordsResult = $this->dnsGetRecord->getRecords($host, DNS_AAAA); + + if (! $aaaaRecordsResult->withError()) { + $dnsRecords = array_merge($dnsRecords, $aaaaRecordsResult->getRecords()); + } + + // No MX, A or AAAA DNS records + if ($dnsRecords === []) { + $this->error = new InvalidEmail(new ReasonNoDNSRecord(), ''); + return false; + } + + // For each DNS record + foreach ($dnsRecords as $dnsRecord) { + if (!$this->validateMXRecord($dnsRecord)) { + // No MX records (fallback to A or AAAA records) + if (empty($this->mxRecords)) { + $this->warnings[NoDNSMXRecord::CODE] = new NoDNSMXRecord(); + } + return false; + } + } + return true; + } + + /** + * Validate an MX record + * + * @param array $dnsRecord Given DNS record. + * + * @return bool True if valid. + */ + private function validateMxRecord($dnsRecord): bool + { + if (!isset($dnsRecord['type'])) { + $this->error = new InvalidEmail(new ReasonNoDNSRecord(), ''); + return false; + } + + if ($dnsRecord['type'] !== 'MX') { + return true; + } + + // "Null MX" record indicates the domain accepts no mail (https://tools.ietf.org/html/rfc7505) + if (empty($dnsRecord['target']) || $dnsRecord['target'] === '.') { + $this->error = new InvalidEmail(new DomainAcceptsNoMail(), ""); + return false; + } + + $this->mxRecords[] = $dnsRecord; + + return true; + } +} diff --git a/3rdparty/egulias/email-validator/src/Validation/DNSGetRecordWrapper.php b/3rdparty/egulias/email-validator/src/Validation/DNSGetRecordWrapper.php new file mode 100644 index 00000000..5d04c010 --- /dev/null +++ b/3rdparty/egulias/email-validator/src/Validation/DNSGetRecordWrapper.php @@ -0,0 +1,30 @@ +> $records + * @param bool $error + */ + public function __construct(private readonly array $records, private readonly bool $error = false) + { + } + + /** + * @return list> + */ + public function getRecords(): array + { + return $this->records; + } + + public function withError(): bool + { + return $this->error; + } +} diff --git a/3rdparty/egulias/email-validator/src/Validation/EmailValidation.php b/3rdparty/egulias/email-validator/src/Validation/EmailValidation.php new file mode 100644 index 00000000..1bcc0a70 --- /dev/null +++ b/3rdparty/egulias/email-validator/src/Validation/EmailValidation.php @@ -0,0 +1,34 @@ +setChecks(Spoofchecker::SINGLE_SCRIPT); + + if ($checker->isSuspicious($email)) { + $this->error = new SpoofEmail(); + } + + return $this->error === null; + } + + public function getError() : ?InvalidEmail + { + return $this->error; + } + + public function getWarnings() : array + { + return []; + } +} diff --git a/3rdparty/egulias/email-validator/src/Validation/MessageIDValidation.php b/3rdparty/egulias/email-validator/src/Validation/MessageIDValidation.php new file mode 100644 index 00000000..97d1ea7a --- /dev/null +++ b/3rdparty/egulias/email-validator/src/Validation/MessageIDValidation.php @@ -0,0 +1,55 @@ +parse($email); + $this->warnings = $parser->getWarnings(); + if ($result->isInvalid()) { + /** @psalm-suppress PropertyTypeCoercion */ + $this->error = $result; + return false; + } + } catch (\Exception $invalid) { + $this->error = new InvalidEmail(new ExceptionFound($invalid), ''); + return false; + } + + return true; + } + + /** + * @return Warning[] + */ + public function getWarnings(): array + { + return $this->warnings; + } + + public function getError(): ?InvalidEmail + { + return $this->error; + } +} diff --git a/3rdparty/egulias/email-validator/src/Validation/MultipleValidationWithAnd.php b/3rdparty/egulias/email-validator/src/Validation/MultipleValidationWithAnd.php new file mode 100644 index 00000000..c908053f --- /dev/null +++ b/3rdparty/egulias/email-validator/src/Validation/MultipleValidationWithAnd.php @@ -0,0 +1,105 @@ +validations as $validation) { + $emailLexer->reset(); + $validationResult = $validation->isValid($email, $emailLexer); + $result = $result && $validationResult; + $this->warnings = [...$this->warnings, ...$validation->getWarnings()]; + if (!$validationResult) { + $this->processError($validation); + } + + if ($this->shouldStop($result)) { + break; + } + } + + return $result; + } + + private function initErrorStorage(): void + { + if (null === $this->error) { + $this->error = new MultipleErrors(); + } + } + + private function processError(EmailValidation $validation): void + { + if (null !== $validation->getError()) { + $this->initErrorStorage(); + /** @psalm-suppress PossiblyNullReference */ + $this->error->addReason($validation->getError()->reason()); + } + } + + private function shouldStop(bool $result): bool + { + return !$result && $this->mode === self::STOP_ON_ERROR; + } + + /** + * Returns the validation errors. + */ + public function getError(): ?InvalidEmail + { + return $this->error; + } + + /** + * @return Warning[] + */ + public function getWarnings(): array + { + return $this->warnings; + } +} diff --git a/3rdparty/egulias/email-validator/src/Validation/NoRFCWarningsValidation.php b/3rdparty/egulias/email-validator/src/Validation/NoRFCWarningsValidation.php new file mode 100644 index 00000000..06885ed7 --- /dev/null +++ b/3rdparty/egulias/email-validator/src/Validation/NoRFCWarningsValidation.php @@ -0,0 +1,41 @@ +getWarnings())) { + return true; + } + + $this->error = new InvalidEmail(new RFCWarnings(), ''); + + return false; + } + + /** + * {@inheritdoc} + */ + public function getError() : ?InvalidEmail + { + return $this->error ?: parent::getError(); + } +} diff --git a/3rdparty/egulias/email-validator/src/Validation/RFCValidation.php b/3rdparty/egulias/email-validator/src/Validation/RFCValidation.php new file mode 100644 index 00000000..f59cbfc8 --- /dev/null +++ b/3rdparty/egulias/email-validator/src/Validation/RFCValidation.php @@ -0,0 +1,54 @@ +parse($email); + $this->warnings = $parser->getWarnings(); + if ($result->isInvalid()) { + /** @psalm-suppress PropertyTypeCoercion */ + $this->error = $result; + return false; + } + } catch (\Exception $invalid) { + $this->error = new InvalidEmail(new ExceptionFound($invalid), ''); + return false; + } + + return true; + } + + public function getError(): ?InvalidEmail + { + return $this->error; + } + + /** + * @return Warning[] + */ + public function getWarnings(): array + { + return $this->warnings; + } +} diff --git a/3rdparty/egulias/email-validator/src/Warning/AddressLiteral.php b/3rdparty/egulias/email-validator/src/Warning/AddressLiteral.php new file mode 100644 index 00000000..474ff0e7 --- /dev/null +++ b/3rdparty/egulias/email-validator/src/Warning/AddressLiteral.php @@ -0,0 +1,14 @@ +message = 'Address literal in domain part'; + $this->rfcNumber = 5321; + } +} diff --git a/3rdparty/egulias/email-validator/src/Warning/CFWSNearAt.php b/3rdparty/egulias/email-validator/src/Warning/CFWSNearAt.php new file mode 100644 index 00000000..8bac12b1 --- /dev/null +++ b/3rdparty/egulias/email-validator/src/Warning/CFWSNearAt.php @@ -0,0 +1,13 @@ +message = "Deprecated folding white space near @"; + } +} diff --git a/3rdparty/egulias/email-validator/src/Warning/CFWSWithFWS.php b/3rdparty/egulias/email-validator/src/Warning/CFWSWithFWS.php new file mode 100644 index 00000000..ba57601c --- /dev/null +++ b/3rdparty/egulias/email-validator/src/Warning/CFWSWithFWS.php @@ -0,0 +1,13 @@ +message = 'Folding whites space followed by folding white space'; + } +} diff --git a/3rdparty/egulias/email-validator/src/Warning/Comment.php b/3rdparty/egulias/email-validator/src/Warning/Comment.php new file mode 100644 index 00000000..6508295e --- /dev/null +++ b/3rdparty/egulias/email-validator/src/Warning/Comment.php @@ -0,0 +1,13 @@ +message = "Comments found in this email"; + } +} diff --git a/3rdparty/egulias/email-validator/src/Warning/DeprecatedComment.php b/3rdparty/egulias/email-validator/src/Warning/DeprecatedComment.php new file mode 100644 index 00000000..a2578076 --- /dev/null +++ b/3rdparty/egulias/email-validator/src/Warning/DeprecatedComment.php @@ -0,0 +1,13 @@ +message = 'Deprecated comments'; + } +} diff --git a/3rdparty/egulias/email-validator/src/Warning/DomainLiteral.php b/3rdparty/egulias/email-validator/src/Warning/DomainLiteral.php new file mode 100644 index 00000000..034388c4 --- /dev/null +++ b/3rdparty/egulias/email-validator/src/Warning/DomainLiteral.php @@ -0,0 +1,14 @@ +message = 'Domain Literal'; + $this->rfcNumber = 5322; + } +} diff --git a/3rdparty/egulias/email-validator/src/Warning/EmailTooLong.php b/3rdparty/egulias/email-validator/src/Warning/EmailTooLong.php new file mode 100644 index 00000000..d25ad123 --- /dev/null +++ b/3rdparty/egulias/email-validator/src/Warning/EmailTooLong.php @@ -0,0 +1,15 @@ +message = 'Email is too long, exceeds ' . EmailParser::EMAIL_MAX_LENGTH; + } +} diff --git a/3rdparty/egulias/email-validator/src/Warning/IPV6BadChar.php b/3rdparty/egulias/email-validator/src/Warning/IPV6BadChar.php new file mode 100644 index 00000000..3ecd5bcc --- /dev/null +++ b/3rdparty/egulias/email-validator/src/Warning/IPV6BadChar.php @@ -0,0 +1,14 @@ +message = 'Bad char in IPV6 domain literal'; + $this->rfcNumber = 5322; + } +} diff --git a/3rdparty/egulias/email-validator/src/Warning/IPV6ColonEnd.php b/3rdparty/egulias/email-validator/src/Warning/IPV6ColonEnd.php new file mode 100644 index 00000000..3f0c2f2d --- /dev/null +++ b/3rdparty/egulias/email-validator/src/Warning/IPV6ColonEnd.php @@ -0,0 +1,14 @@ +message = ':: found at the end of the domain literal'; + $this->rfcNumber = 5322; + } +} diff --git a/3rdparty/egulias/email-validator/src/Warning/IPV6ColonStart.php b/3rdparty/egulias/email-validator/src/Warning/IPV6ColonStart.php new file mode 100644 index 00000000..742fb3bd --- /dev/null +++ b/3rdparty/egulias/email-validator/src/Warning/IPV6ColonStart.php @@ -0,0 +1,14 @@ +message = ':: found at the start of the domain literal'; + $this->rfcNumber = 5322; + } +} diff --git a/3rdparty/egulias/email-validator/src/Warning/IPV6Deprecated.php b/3rdparty/egulias/email-validator/src/Warning/IPV6Deprecated.php new file mode 100644 index 00000000..59c3037a --- /dev/null +++ b/3rdparty/egulias/email-validator/src/Warning/IPV6Deprecated.php @@ -0,0 +1,14 @@ +message = 'Deprecated form of IPV6'; + $this->rfcNumber = 5321; + } +} diff --git a/3rdparty/egulias/email-validator/src/Warning/IPV6DoubleColon.php b/3rdparty/egulias/email-validator/src/Warning/IPV6DoubleColon.php new file mode 100644 index 00000000..d4066026 --- /dev/null +++ b/3rdparty/egulias/email-validator/src/Warning/IPV6DoubleColon.php @@ -0,0 +1,14 @@ +message = 'Double colon found after IPV6 tag'; + $this->rfcNumber = 5322; + } +} diff --git a/3rdparty/egulias/email-validator/src/Warning/IPV6GroupCount.php b/3rdparty/egulias/email-validator/src/Warning/IPV6GroupCount.php new file mode 100644 index 00000000..551bc3af --- /dev/null +++ b/3rdparty/egulias/email-validator/src/Warning/IPV6GroupCount.php @@ -0,0 +1,14 @@ +message = 'Group count is not IPV6 valid'; + $this->rfcNumber = 5322; + } +} diff --git a/3rdparty/egulias/email-validator/src/Warning/IPV6MaxGroups.php b/3rdparty/egulias/email-validator/src/Warning/IPV6MaxGroups.php new file mode 100644 index 00000000..7f8a410a --- /dev/null +++ b/3rdparty/egulias/email-validator/src/Warning/IPV6MaxGroups.php @@ -0,0 +1,14 @@ +message = 'Reached the maximum number of IPV6 groups allowed'; + $this->rfcNumber = 5321; + } +} diff --git a/3rdparty/egulias/email-validator/src/Warning/LocalTooLong.php b/3rdparty/egulias/email-validator/src/Warning/LocalTooLong.php new file mode 100644 index 00000000..b46b874c --- /dev/null +++ b/3rdparty/egulias/email-validator/src/Warning/LocalTooLong.php @@ -0,0 +1,15 @@ +message = 'Local part is too long, exceeds 64 chars (octets)'; + $this->rfcNumber = 5322; + } +} diff --git a/3rdparty/egulias/email-validator/src/Warning/NoDNSMXRecord.php b/3rdparty/egulias/email-validator/src/Warning/NoDNSMXRecord.php new file mode 100644 index 00000000..bddb96be --- /dev/null +++ b/3rdparty/egulias/email-validator/src/Warning/NoDNSMXRecord.php @@ -0,0 +1,14 @@ +message = 'No MX DSN record was found for this email'; + $this->rfcNumber = 5321; + } +} diff --git a/3rdparty/egulias/email-validator/src/Warning/ObsoleteDTEXT.php b/3rdparty/egulias/email-validator/src/Warning/ObsoleteDTEXT.php new file mode 100644 index 00000000..412fd499 --- /dev/null +++ b/3rdparty/egulias/email-validator/src/Warning/ObsoleteDTEXT.php @@ -0,0 +1,14 @@ +rfcNumber = 5322; + $this->message = 'Obsolete DTEXT in domain literal'; + } +} diff --git a/3rdparty/egulias/email-validator/src/Warning/QuotedPart.php b/3rdparty/egulias/email-validator/src/Warning/QuotedPart.php new file mode 100644 index 00000000..db0850c9 --- /dev/null +++ b/3rdparty/egulias/email-validator/src/Warning/QuotedPart.php @@ -0,0 +1,27 @@ +name; + } + + if ($postToken instanceof UnitEnum) { + $postToken = $postToken->name; + } + + $this->message = "Deprecated Quoted String found between $prevToken and $postToken"; + } +} diff --git a/3rdparty/egulias/email-validator/src/Warning/QuotedString.php b/3rdparty/egulias/email-validator/src/Warning/QuotedString.php new file mode 100644 index 00000000..388da0bc --- /dev/null +++ b/3rdparty/egulias/email-validator/src/Warning/QuotedString.php @@ -0,0 +1,17 @@ +message = "Quoted String found between $prevToken and $postToken"; + } +} diff --git a/3rdparty/egulias/email-validator/src/Warning/TLD.php b/3rdparty/egulias/email-validator/src/Warning/TLD.php new file mode 100644 index 00000000..10cec281 --- /dev/null +++ b/3rdparty/egulias/email-validator/src/Warning/TLD.php @@ -0,0 +1,13 @@ +message = "RFC5321, TLD"; + } +} diff --git a/3rdparty/egulias/email-validator/src/Warning/Warning.php b/3rdparty/egulias/email-validator/src/Warning/Warning.php new file mode 100644 index 00000000..7adb3b85 --- /dev/null +++ b/3rdparty/egulias/email-validator/src/Warning/Warning.php @@ -0,0 +1,53 @@ +message; + } + + /** + * @return int + */ + public function code() + { + return self::CODE; + } + + /** + * @return int + */ + public function RFCNumber() + { + return $this->rfcNumber; + } + + /** + * @return string + */ + public function __toString(): string + { + return $this->message() . " rfc: " . $this->rfcNumber . "internal code: " . static::CODE; + } +} diff --git a/3rdparty/f7cloud/lognormalizer/COPYING b/3rdparty/f7cloud/lognormalizer/COPYING new file mode 100644 index 00000000..2def0e88 --- /dev/null +++ b/3rdparty/f7cloud/lognormalizer/COPYING @@ -0,0 +1,661 @@ + GNU AFFERO GENERAL PUBLIC LICENSE + Version 3, 19 November 2007 + + Copyright (C) 2007 Free Software Foundation, Inc. + Everyone is permitted to copy and distribute verbatim copies + of this license document, but changing it is not allowed. + + Preamble + + The GNU Affero General Public License is a free, copyleft license for +software and other kinds of works, specifically designed to ensure +cooperation with the community in the case of network server software. + + The licenses for most software and other practical works are designed +to take away your freedom to share and change the works. By contrast, +our General Public Licenses are intended to guarantee your freedom to +share and change all versions of a program--to make sure it remains free +software for all its users. + + When we speak of free software, we are referring to freedom, not +price. Our General Public Licenses are designed to make sure that you +have the freedom to distribute copies of free software (and charge for +them if you wish), that you receive source code or can get it if you +want it, that you can change the software or use pieces of it in new +free programs, and that you know you can do these things. + + Developers that use our General Public Licenses protect your rights +with two steps: (1) assert copyright on the software, and (2) offer +you this License which gives you legal permission to copy, distribute +and/or modify the software. + + A secondary benefit of defending all users' freedom is that +improvements made in alternate versions of the program, if they +receive widespread use, become available for other developers to +incorporate. Many developers of free software are heartened and +encouraged by the resulting cooperation. However, in the case of +software used on network servers, this result may fail to come about. +The GNU General Public License permits making a modified version and +letting the public access it on a server without ever releasing its +source code to the public. + + The GNU Affero General Public License is designed specifically to +ensure that, in such cases, the modified source code becomes available +to the community. It requires the operator of a network server to +provide the source code of the modified version running there to the +users of that server. Therefore, public use of a modified version, on +a publicly accessible server, gives the public access to the source +code of the modified version. + + An older license, called the Affero General Public License and +published by Affero, was designed to accomplish similar goals. This is +a different license, not a version of the Affero GPL, but Affero has +released a new version of the Affero GPL which permits relicensing under +this license. + + The precise terms and conditions for copying, distribution and +modification follow. + + TERMS AND CONDITIONS + + 0. Definitions. + + "This License" refers to version 3 of the GNU Affero General Public License. + + "Copyright" also means copyright-like laws that apply to other kinds of +works, such as semiconductor masks. + + "The Program" refers to any copyrightable work licensed under this +License. Each licensee is addressed as "you". "Licensees" and +"recipients" may be individuals or organizations. + + To "modify" a work means to copy from or adapt all or part of the work +in a fashion requiring copyright permission, other than the making of an +exact copy. The resulting work is called a "modified version" of the +earlier work or a work "based on" the earlier work. + + A "covered work" means either the unmodified Program or a work based +on the Program. + + To "propagate" a work means to do anything with it that, without +permission, would make you directly or secondarily liable for +infringement under applicable copyright law, except executing it on a +computer or modifying a private copy. Propagation includes copying, +distribution (with or without modification), making available to the +public, and in some countries other activities as well. + + To "convey" a work means any kind of propagation that enables other +parties to make or receive copies. Mere interaction with a user through +a computer network, with no transfer of a copy, is not conveying. + + An interactive user interface displays "Appropriate Legal Notices" +to the extent that it includes a convenient and prominently visible +feature that (1) displays an appropriate copyright notice, and (2) +tells the user that there is no warranty for the work (except to the +extent that warranties are provided), that licensees may convey the +work under this License, and how to view a copy of this License. If +the interface presents a list of user commands or options, such as a +menu, a prominent item in the list meets this criterion. + + 1. Source Code. + + The "source code" for a work means the preferred form of the work +for making modifications to it. "Object code" means any non-source +form of a work. + + A "Standard Interface" means an interface that either is an official +standard defined by a recognized standards body, or, in the case of +interfaces specified for a particular programming language, one that +is widely used among developers working in that language. + + The "System Libraries" of an executable work include anything, other +than the work as a whole, that (a) is included in the normal form of +packaging a Major Component, but which is not part of that Major +Component, and (b) serves only to enable use of the work with that +Major Component, or to implement a Standard Interface for which an +implementation is available to the public in source code form. A +"Major Component", in this context, means a major essential component +(kernel, window system, and so on) of the specific operating system +(if any) on which the executable work runs, or a compiler used to +produce the work, or an object code interpreter used to run it. + + The "Corresponding Source" for a work in object code form means all +the source code needed to generate, install, and (for an executable +work) run the object code and to modify the work, including scripts to +control those activities. However, it does not include the work's +System Libraries, or general-purpose tools or generally available free +programs which are used unmodified in performing those activities but +which are not part of the work. For example, Corresponding Source +includes interface definition files associated with source files for +the work, and the source code for shared libraries and dynamically +linked subprograms that the work is specifically designed to require, +such as by intimate data communication or control flow between those +subprograms and other parts of the work. + + The Corresponding Source need not include anything that users +can regenerate automatically from other parts of the Corresponding +Source. + + The Corresponding Source for a work in source code form is that +same work. + + 2. Basic Permissions. + + All rights granted under this License are granted for the term of +copyright on the Program, and are irrevocable provided the stated +conditions are met. This License explicitly affirms your unlimited +permission to run the unmodified Program. The output from running a +covered work is covered by this License only if the output, given its +content, constitutes a covered work. This License acknowledges your +rights of fair use or other equivalent, as provided by copyright law. + + You may make, run and propagate covered works that you do not +convey, without conditions so long as your license otherwise remains +in force. You may convey covered works to others for the sole purpose +of having them make modifications exclusively for you, or provide you +with facilities for running those works, provided that you comply with +the terms of this License in conveying all material for which you do +not control copyright. Those thus making or running the covered works +for you must do so exclusively on your behalf, under your direction +and control, on terms that prohibit them from making any copies of +your copyrighted material outside their relationship with you. + + Conveying under any other circumstances is permitted solely under +the conditions stated below. Sublicensing is not allowed; section 10 +makes it unnecessary. + + 3. Protecting Users' Legal Rights From Anti-Circumvention Law. + + No covered work shall be deemed part of an effective technological +measure under any applicable law fulfilling obligations under article +11 of the WIPO copyright treaty adopted on 20 December 1996, or +similar laws prohibiting or restricting circumvention of such +measures. + + When you convey a covered work, you waive any legal power to forbid +circumvention of technological measures to the extent such circumvention +is effected by exercising rights under this License with respect to +the covered work, and you disclaim any intention to limit operation or +modification of the work as a means of enforcing, against the work's +users, your or third parties' legal rights to forbid circumvention of +technological measures. + + 4. Conveying Verbatim Copies. + + You may convey verbatim copies of the Program's source code as you +receive it, in any medium, provided that you conspicuously and +appropriately publish on each copy an appropriate copyright notice; +keep intact all notices stating that this License and any +non-permissive terms added in accord with section 7 apply to the code; +keep intact all notices of the absence of any warranty; and give all +recipients a copy of this License along with the Program. + + You may charge any price or no price for each copy that you convey, +and you may offer support or warranty protection for a fee. + + 5. Conveying Modified Source Versions. + + You may convey a work based on the Program, or the modifications to +produce it from the Program, in the form of source code under the +terms of section 4, provided that you also meet all of these conditions: + + a) The work must carry prominent notices stating that you modified + it, and giving a relevant date. + + b) The work must carry prominent notices stating that it is + released under this License and any conditions added under section + 7. This requirement modifies the requirement in section 4 to + "keep intact all notices". + + c) You must license the entire work, as a whole, under this + License to anyone who comes into possession of a copy. This + License will therefore apply, along with any applicable section 7 + additional terms, to the whole of the work, and all its parts, + regardless of how they are packaged. This License gives no + permission to license the work in any other way, but it does not + invalidate such permission if you have separately received it. + + d) If the work has interactive user interfaces, each must display + Appropriate Legal Notices; however, if the Program has interactive + interfaces that do not display Appropriate Legal Notices, your + work need not make them do so. + + A compilation of a covered work with other separate and independent +works, which are not by their nature extensions of the covered work, +and which are not combined with it such as to form a larger program, +in or on a volume of a storage or distribution medium, is called an +"aggregate" if the compilation and its resulting copyright are not +used to limit the access or legal rights of the compilation's users +beyond what the individual works permit. Inclusion of a covered work +in an aggregate does not cause this License to apply to the other +parts of the aggregate. + + 6. Conveying Non-Source Forms. + + You may convey a covered work in object code form under the terms +of sections 4 and 5, provided that you also convey the +machine-readable Corresponding Source under the terms of this License, +in one of these ways: + + a) Convey the object code in, or embodied in, a physical product + (including a physical distribution medium), accompanied by the + Corresponding Source fixed on a durable physical medium + customarily used for software interchange. + + b) Convey the object code in, or embodied in, a physical product + (including a physical distribution medium), accompanied by a + written offer, valid for at least three years and valid for as + long as you offer spare parts or customer support for that product + model, to give anyone who possesses the object code either (1) a + copy of the Corresponding Source for all the software in the + product that is covered by this License, on a durable physical + medium customarily used for software interchange, for a price no + more than your reasonable cost of physically performing this + conveying of source, or (2) access to copy the + Corresponding Source from a network server at no charge. + + c) Convey individual copies of the object code with a copy of the + written offer to provide the Corresponding Source. This + alternative is allowed only occasionally and noncommercially, and + only if you received the object code with such an offer, in accord + with subsection 6b. + + d) Convey the object code by offering access from a designated + place (gratis or for a charge), and offer equivalent access to the + Corresponding Source in the same way through the same place at no + further charge. You need not require recipients to copy the + Corresponding Source along with the object code. If the place to + copy the object code is a network server, the Corresponding Source + may be on a different server (operated by you or a third party) + that supports equivalent copying facilities, provided you maintain + clear directions next to the object code saying where to find the + Corresponding Source. Regardless of what server hosts the + Corresponding Source, you remain obligated to ensure that it is + available for as long as needed to satisfy these requirements. + + e) Convey the object code using peer-to-peer transmission, provided + you inform other peers where the object code and Corresponding + Source of the work are being offered to the general public at no + charge under subsection 6d. + + A separable portion of the object code, whose source code is excluded +from the Corresponding Source as a System Library, need not be +included in conveying the object code work. + + A "User Product" is either (1) a "consumer product", which means any +tangible personal property which is normally used for personal, family, +or household purposes, or (2) anything designed or sold for incorporation +into a dwelling. In determining whether a product is a consumer product, +doubtful cases shall be resolved in favor of coverage. For a particular +product received by a particular user, "normally used" refers to a +typical or common use of that class of product, regardless of the status +of the particular user or of the way in which the particular user +actually uses, or expects or is expected to use, the product. A product +is a consumer product regardless of whether the product has substantial +commercial, industrial or non-consumer uses, unless such uses represent +the only significant mode of use of the product. + + "Installation Information" for a User Product means any methods, +procedures, authorization keys, or other information required to install +and execute modified versions of a covered work in that User Product from +a modified version of its Corresponding Source. The information must +suffice to ensure that the continued functioning of the modified object +code is in no case prevented or interfered with solely because +modification has been made. + + If you convey an object code work under this section in, or with, or +specifically for use in, a User Product, and the conveying occurs as +part of a transaction in which the right of possession and use of the +User Product is transferred to the recipient in perpetuity or for a +fixed term (regardless of how the transaction is characterized), the +Corresponding Source conveyed under this section must be accompanied +by the Installation Information. But this requirement does not apply +if neither you nor any third party retains the ability to install +modified object code on the User Product (for example, the work has +been installed in ROM). + + The requirement to provide Installation Information does not include a +requirement to continue to provide support service, warranty, or updates +for a work that has been modified or installed by the recipient, or for +the User Product in which it has been modified or installed. Access to a +network may be denied when the modification itself materially and +adversely affects the operation of the network or violates the rules and +protocols for communication across the network. + + Corresponding Source conveyed, and Installation Information provided, +in accord with this section must be in a format that is publicly +documented (and with an implementation available to the public in +source code form), and must require no special password or key for +unpacking, reading or copying. + + 7. Additional Terms. + + "Additional permissions" are terms that supplement the terms of this +License by making exceptions from one or more of its conditions. +Additional permissions that are applicable to the entire Program shall +be treated as though they were included in this License, to the extent +that they are valid under applicable law. If additional permissions +apply only to part of the Program, that part may be used separately +under those permissions, but the entire Program remains governed by +this License without regard to the additional permissions. + + When you convey a copy of a covered work, you may at your option +remove any additional permissions from that copy, or from any part of +it. (Additional permissions may be written to require their own +removal in certain cases when you modify the work.) You may place +additional permissions on material, added by you to a covered work, +for which you have or can give appropriate copyright permission. + + Notwithstanding any other provision of this License, for material you +add to a covered work, you may (if authorized by the copyright holders of +that material) supplement the terms of this License with terms: + + a) Disclaiming warranty or limiting liability differently from the + terms of sections 15 and 16 of this License; or + + b) Requiring preservation of specified reasonable legal notices or + author attributions in that material or in the Appropriate Legal + Notices displayed by works containing it; or + + c) Prohibiting misrepresentation of the origin of that material, or + requiring that modified versions of such material be marked in + reasonable ways as different from the original version; or + + d) Limiting the use for publicity purposes of names of licensors or + authors of the material; or + + e) Declining to grant rights under trademark law for use of some + trade names, trademarks, or service marks; or + + f) Requiring indemnification of licensors and authors of that + material by anyone who conveys the material (or modified versions of + it) with contractual assumptions of liability to the recipient, for + any liability that these contractual assumptions directly impose on + those licensors and authors. + + All other non-permissive additional terms are considered "further +restrictions" within the meaning of section 10. If the Program as you +received it, or any part of it, contains a notice stating that it is +governed by this License along with a term that is a further +restriction, you may remove that term. If a license document contains +a further restriction but permits relicensing or conveying under this +License, you may add to a covered work material governed by the terms +of that license document, provided that the further restriction does +not survive such relicensing or conveying. + + If you add terms to a covered work in accord with this section, you +must place, in the relevant source files, a statement of the +additional terms that apply to those files, or a notice indicating +where to find the applicable terms. + + Additional terms, permissive or non-permissive, may be stated in the +form of a separately written license, or stated as exceptions; +the above requirements apply either way. + + 8. Termination. + + You may not propagate or modify a covered work except as expressly +provided under this License. Any attempt otherwise to propagate or +modify it is void, and will automatically terminate your rights under +this License (including any patent licenses granted under the third +paragraph of section 11). + + However, if you cease all violation of this License, then your +license from a particular copyright holder is reinstated (a) +provisionally, unless and until the copyright holder explicitly and +finally terminates your license, and (b) permanently, if the copyright +holder fails to notify you of the violation by some reasonable means +prior to 60 days after the cessation. + + Moreover, your license from a particular copyright holder is +reinstated permanently if the copyright holder notifies you of the +violation by some reasonable means, this is the first time you have +received notice of violation of this License (for any work) from that +copyright holder, and you cure the violation prior to 30 days after +your receipt of the notice. + + Termination of your rights under this section does not terminate the +licenses of parties who have received copies or rights from you under +this License. If your rights have been terminated and not permanently +reinstated, you do not qualify to receive new licenses for the same +material under section 10. + + 9. Acceptance Not Required for Having Copies. + + You are not required to accept this License in order to receive or +run a copy of the Program. Ancillary propagation of a covered work +occurring solely as a consequence of using peer-to-peer transmission +to receive a copy likewise does not require acceptance. However, +nothing other than this License grants you permission to propagate or +modify any covered work. These actions infringe copyright if you do +not accept this License. Therefore, by modifying or propagating a +covered work, you indicate your acceptance of this License to do so. + + 10. Automatic Licensing of Downstream Recipients. + + Each time you convey a covered work, the recipient automatically +receives a license from the original licensors, to run, modify and +propagate that work, subject to this License. You are not responsible +for enforcing compliance by third parties with this License. + + An "entity transaction" is a transaction transferring control of an +organization, or substantially all assets of one, or subdividing an +organization, or merging organizations. If propagation of a covered +work results from an entity transaction, each party to that +transaction who receives a copy of the work also receives whatever +licenses to the work the party's predecessor in interest had or could +give under the previous paragraph, plus a right to possession of the +Corresponding Source of the work from the predecessor in interest, if +the predecessor has it or can get it with reasonable efforts. + + You may not impose any further restrictions on the exercise of the +rights granted or affirmed under this License. For example, you may +not impose a license fee, royalty, or other charge for exercise of +rights granted under this License, and you may not initiate litigation +(including a cross-claim or counterclaim in a lawsuit) alleging that +any patent claim is infringed by making, using, selling, offering for +sale, or importing the Program or any portion of it. + + 11. Patents. + + A "contributor" is a copyright holder who authorizes use under this +License of the Program or a work on which the Program is based. The +work thus licensed is called the contributor's "contributor version". + + A contributor's "essential patent claims" are all patent claims +owned or controlled by the contributor, whether already acquired or +hereafter acquired, that would be infringed by some manner, permitted +by this License, of making, using, or selling its contributor version, +but do not include claims that would be infringed only as a +consequence of further modification of the contributor version. For +purposes of this definition, "control" includes the right to grant +patent sublicenses in a manner consistent with the requirements of +this License. + + Each contributor grants you a non-exclusive, worldwide, royalty-free +patent license under the contributor's essential patent claims, to +make, use, sell, offer for sale, import and otherwise run, modify and +propagate the contents of its contributor version. + + In the following three paragraphs, a "patent license" is any express +agreement or commitment, however denominated, not to enforce a patent +(such as an express permission to practice a patent or covenant not to +sue for patent infringement). To "grant" such a patent license to a +party means to make such an agreement or commitment not to enforce a +patent against the party. + + If you convey a covered work, knowingly relying on a patent license, +and the Corresponding Source of the work is not available for anyone +to copy, free of charge and under the terms of this License, through a +publicly available network server or other readily accessible means, +then you must either (1) cause the Corresponding Source to be so +available, or (2) arrange to deprive yourself of the benefit of the +patent license for this particular work, or (3) arrange, in a manner +consistent with the requirements of this License, to extend the patent +license to downstream recipients. "Knowingly relying" means you have +actual knowledge that, but for the patent license, your conveying the +covered work in a country, or your recipient's use of the covered work +in a country, would infringe one or more identifiable patents in that +country that you have reason to believe are valid. + + If, pursuant to or in connection with a single transaction or +arrangement, you convey, or propagate by procuring conveyance of, a +covered work, and grant a patent license to some of the parties +receiving the covered work authorizing them to use, propagate, modify +or convey a specific copy of the covered work, then the patent license +you grant is automatically extended to all recipients of the covered +work and works based on it. + + A patent license is "discriminatory" if it does not include within +the scope of its coverage, prohibits the exercise of, or is +conditioned on the non-exercise of one or more of the rights that are +specifically granted under this License. You may not convey a covered +work if you are a party to an arrangement with a third party that is +in the business of distributing software, under which you make payment +to the third party based on the extent of your activity of conveying +the work, and under which the third party grants, to any of the +parties who would receive the covered work from you, a discriminatory +patent license (a) in connection with copies of the covered work +conveyed by you (or copies made from those copies), or (b) primarily +for and in connection with specific products or compilations that +contain the covered work, unless you entered into that arrangement, +or that patent license was granted, prior to 28 March 2007. + + Nothing in this License shall be construed as excluding or limiting +any implied license or other defenses to infringement that may +otherwise be available to you under applicable patent law. + + 12. No Surrender of Others' Freedom. + + If conditions are imposed on you (whether by court order, agreement or +otherwise) that contradict the conditions of this License, they do not +excuse you from the conditions of this License. If you cannot convey a +covered work so as to satisfy simultaneously your obligations under this +License and any other pertinent obligations, then as a consequence you may +not convey it at all. For example, if you agree to terms that obligate you +to collect a royalty for further conveying from those to whom you convey +the Program, the only way you could satisfy both those terms and this +License would be to refrain entirely from conveying the Program. + + 13. Remote Network Interaction; Use with the GNU General Public License. + + Notwithstanding any other provision of this License, if you modify the +Program, your modified version must prominently offer all users +interacting with it remotely through a computer network (if your version +supports such interaction) an opportunity to receive the Corresponding +Source of your version by providing access to the Corresponding Source +from a network server at no charge, through some standard or customary +means of facilitating copying of software. This Corresponding Source +shall include the Corresponding Source for any work covered by version 3 +of the GNU General Public License that is incorporated pursuant to the +following paragraph. + + Notwithstanding any other provision of this License, you have +permission to link or combine any covered work with a work licensed +under version 3 of the GNU General Public License into a single +combined work, and to convey the resulting work. The terms of this +License will continue to apply to the part which is the covered work, +but the work with which it is combined will remain governed by version +3 of the GNU General Public License. + + 14. Revised Versions of this License. + + The Free Software Foundation may publish revised and/or new versions of +the GNU Affero General Public License from time to time. Such new versions +will be similar in spirit to the present version, but may differ in detail to +address new problems or concerns. + + Each version is given a distinguishing version number. If the +Program specifies that a certain numbered version of the GNU Affero General +Public License "or any later version" applies to it, you have the +option of following the terms and conditions either of that numbered +version or of any later version published by the Free Software +Foundation. If the Program does not specify a version number of the +GNU Affero General Public License, you may choose any version ever published +by the Free Software Foundation. + + If the Program specifies that a proxy can decide which future +versions of the GNU Affero General Public License can be used, that proxy's +public statement of acceptance of a version permanently authorizes you +to choose that version for the Program. + + Later license versions may give you additional or different +permissions. However, no additional obligations are imposed on any +author or copyright holder as a result of your choosing to follow a +later version. + + 15. Disclaimer of Warranty. + + THERE IS NO WARRANTY FOR THE PROGRAM, TO THE EXTENT PERMITTED BY +APPLICABLE LAW. EXCEPT WHEN OTHERWISE STATED IN WRITING THE COPYRIGHT +HOLDERS AND/OR OTHER PARTIES PROVIDE THE PROGRAM "AS IS" WITHOUT WARRANTY +OF ANY KIND, EITHER EXPRESSED OR IMPLIED, INCLUDING, BUT NOT LIMITED TO, +THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR +PURPOSE. THE ENTIRE RISK AS TO THE QUALITY AND PERFORMANCE OF THE PROGRAM +IS WITH YOU. SHOULD THE PROGRAM PROVE DEFECTIVE, YOU ASSUME THE COST OF +ALL NECESSARY SERVICING, REPAIR OR CORRECTION. + + 16. Limitation of Liability. + + IN NO EVENT UNLESS REQUIRED BY APPLICABLE LAW OR AGREED TO IN WRITING +WILL ANY COPYRIGHT HOLDER, OR ANY OTHER PARTY WHO MODIFIES AND/OR CONVEYS +THE PROGRAM AS PERMITTED ABOVE, BE LIABLE TO YOU FOR DAMAGES, INCLUDING ANY +GENERAL, SPECIAL, INCIDENTAL OR CONSEQUENTIAL DAMAGES ARISING OUT OF THE +USE OR INABILITY TO USE THE PROGRAM (INCLUDING BUT NOT LIMITED TO LOSS OF +DATA OR DATA BEING RENDERED INACCURATE OR LOSSES SUSTAINED BY YOU OR THIRD +PARTIES OR A FAILURE OF THE PROGRAM TO OPERATE WITH ANY OTHER PROGRAMS), +EVEN IF SUCH HOLDER OR OTHER PARTY HAS BEEN ADVISED OF THE POSSIBILITY OF +SUCH DAMAGES. + + 17. Interpretation of Sections 15 and 16. + + If the disclaimer of warranty and limitation of liability provided +above cannot be given local legal effect according to their terms, +reviewing courts shall apply local law that most closely approximates +an absolute waiver of all civil liability in connection with the +Program, unless a warranty or assumption of liability accompanies a +copy of the Program in return for a fee. + + END OF TERMS AND CONDITIONS + + How to Apply These Terms to Your New Programs + + If you develop a new program, and you want it to be of the greatest +possible use to the public, the best way to achieve this is to make it +free software which everyone can redistribute and change under these terms. + + To do so, attach the following notices to the program. It is safest +to attach them to the start of each source file to most effectively +state the exclusion of warranty; and each file should have at least +the "copyright" line and a pointer to where the full notice is found. + + + Copyright (C) + + This program is free software: you can redistribute it and/or modify + it under the terms of the GNU Affero General Public License as published by + the Free Software Foundation, either version 3 of the License, or + (at your option) any later version. + + This program is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU Affero General Public License for more details. + + You should have received a copy of the GNU Affero General Public License + along with this program. If not, see . + +Also add information on how to contact you by electronic and paper mail. + + If your software can interact with users remotely through a computer +network, you should also make sure that it provides a way for users to +get its source. For example, if your program is a web application, its +interface could display a "Source" link that leads users to an archive +of the code. There are many ways you could offer source, and different +solutions will be better for different programs; see section 13 for the +specific requirements. + + You should also get your employer (if you work as a programmer) or school, +if any, to sign a "copyright disclaimer" for the program, if necessary. +For more information on this, and how to apply and follow the GNU AGPL, see +. \ No newline at end of file diff --git a/3rdparty/f7cloud/lognormalizer/src/Normalizer.php b/3rdparty/f7cloud/lognormalizer/src/Normalizer.php new file mode 100644 index 00000000..3795f614 --- /dev/null +++ b/3rdparty/f7cloud/lognormalizer/src/Normalizer.php @@ -0,0 +1,330 @@ + + * @author Jordi Boggiano + * + * @copyright Olivier Paroz 2015 + * @copyright Jordi Boggiano 2014-2015 + */ + +namespace F7cloud\LogNormalizer; + +use Throwable; + +/** + * Converts any variable to a String + * + * @package F7cloud\LogNormalizer + */ +class Normalizer { + + /** + * @type string + */ + const SIMPLE_DATE = "Y-m-d H:i:s"; + + /** + * @type int + */ + private $maxRecursionDepth; + + /** + * @type int + */ + private $maxArrayItems; + + /** + * @type string + */ + private $dateFormat; + + /** + * @param int $maxRecursionDepth The maximum depth at which to go when inspecting objects + * @param int $maxArrayItems The maximum number of Array elements you want to show, when + * parsing an array + * @param null|string $dateFormat The format to apply to dates + */ + public function __construct($maxRecursionDepth = 4, $maxArrayItems = 20, $dateFormat = null) { + $this->maxRecursionDepth = $maxRecursionDepth; + $this->maxArrayItems = $maxArrayItems; + if ($dateFormat !== null) { + $this->dateFormat = $dateFormat; + } else { + $this->dateFormat = static::SIMPLE_DATE; + } + } + + /** + * Normalises the variable, JSON encodes it if needed and cleans up the result + * + * @param mixed $data + * + * @return string|null + */ + public function format(&$data) { + $data = $this->normalize($data); + $data = $this->convertToString($data); + + return $data; + } + + /** + * Converts Objects, Arrays, Dates and Exceptions to a String or an Array + * + * @uses F7cloud\LogNormalizer\Normalizer::normalizeTraversable + * @uses F7cloud\LogNormalizer\Normalizer::normalizeDate + * @uses F7cloud\LogNormalizer\Normalizer::normalizeObject + * @uses F7cloud\LogNormalizer\Normalizer::normalizeResource + * + * @param $data + * @param int $depth + * + * @return string|array + */ + public function normalize($data, $depth = 0) { + $scalar = $this->normalizeScalar($data); + if (!is_array($scalar)) { + return $scalar; + } + $decisionArray = [ + 'normalizeTraversable' => [$data, $depth], + 'normalizeDate' => [$data], + 'normalizeObject' => [$data, $depth], + 'normalizeResource' => [$data], + ]; + + foreach ($decisionArray as $functionName => $arguments) { + $dataType = call_user_func_array([$this, $functionName], $arguments); + if ($dataType !== null) { + return $dataType; + } + } + + return '[unknown(' . gettype($data) . ')]'; + } + + /** + * JSON encodes data which isn't already a string and cleans up the result + * + * @todo: could maybe do a better job removing slashes + * + * @param mixed $data + * + * @return string|null + */ + public function convertToString($data) { + if (!is_string($data)) { + $data = @json_encode($data, JSON_UNESCAPED_SLASHES | JSON_UNESCAPED_UNICODE); + // Removing null byte and double slashes from object properties + $data = str_replace(['\\u0000', '\\\\'], ["", "\\"], $data); + } + + return $data; + } + + /** + * Returns various, filtered, scalar elements + * + * We're returning an array here to detect failure because null is a scalar and so is false + * + * @param $data + * + * @return mixed + */ + private function normalizeScalar($data) { + if (null === $data || is_scalar($data)) { + /*// utf8_encode only works for Latin1 so we rely on mbstring + if (is_string($data)) { + $data = mb_convert_encoding($data, "UTF-8"); + }*/ + + if (is_float($data)) { + $data = $this->normalizeFloat($data); + } + + return $data; + } + + return []; + } + + /** + * Normalises infinite and trigonometric floats + * + * @param float $data + * + * @return string|double + */ + private function normalizeFloat($data) { + if (is_infinite($data)) { + $postfix = 'INF'; + if ($data < 0) { + $postfix = '-' . $postfix; + } + $data = $postfix; + } else { + if (is_nan($data)) { + $data = 'NaN'; + } + } + + return $data; + } + + /** + * Returns an array containing normalized elements + * + * @used-by F7cloud\LogNormalizer\Normalizer::normalize + * + * @param $data + * @param int $depth + * + * @return array|null + */ + private function normalizeTraversable($data, $depth = 0) { + if (is_array($data) || $data instanceof \Traversable) { + return $this->normalizeTraversableElement($data, $depth); + } + + return null; + } + + /** + * Converts each element of a traversable variable to String + * + * @param $data + * @param int $depth + * + * @return array + */ + private function normalizeTraversableElement($data, $depth) { + $maxObjectRecursion = $this->maxRecursionDepth; + $maxArrayItems = $this->maxArrayItems; + $count = 1; + $normalized = []; + $nextDepth = $depth + 1; + foreach ($data as $key => $value) { + if ($count >= $maxArrayItems) { + $normalized['...'] = + 'Over ' . $maxArrayItems . ' items, aborting normalization'; + break; + } + $count++; + if ($depth < $maxObjectRecursion) { + $normalized[$key] = $this->normalize($value, $nextDepth); + } + } + + return $normalized; + } + + /** + * Converts a date to String + * + * @used-by F7cloud\LogNormalizer\Normalizer::normalize + * + * @param mixed $data + * + * @return null|string + */ + private function normalizeDate($data) { + if ($data instanceof \DateTime) { + return $data->format($this->dateFormat); + } + + return null; + } + + /** + * Converts an Object to an Array + * + * We don't convert to json here as we would double encode them + * + * @used-by F7cloud\LogNormalizer\Normalizer::normalize + * + * @param mixed $data + * @param int $depth + * + * @return array|null + */ + private function normalizeObject($data, $depth) { + if (is_object($data)) { + if ($data instanceof Throwable) { + return $this->normalizeException($data); + } + // We don't need to go too deep in the recursion + $maxObjectRecursion = $this->maxRecursionDepth; + $arrayObject = new \ArrayObject($data); + $serializedObject = $arrayObject->getArrayCopy(); + if ($depth < $maxObjectRecursion) { + $depth++; + $response = $this->normalize($serializedObject, $depth); + + return [$this->getObjetName($data) => $response]; + } + + return $this->getObjetName($data); + } + + return null; + } + + /** + * Converts an Exception to String + * + * @param Throwable $exception + * + * @return string[] + */ + private function normalizeException(Throwable $exception) { + $data = [ + 'class' => get_class($exception), + 'message' => $exception->getMessage(), + 'code' => $exception->getCode(), + 'file' => $exception->getFile() . ':' . $exception->getLine(), + ]; + $trace = $exception->getTraceAsString(); + $data['trace'] = $trace; + + $previous = $exception->getPrevious(); + if ($previous) { + $data['previous'] = $this->normalizeException($previous); + } + + return $data; + } + + /** + * Formats the output of the object parsing + * + * @param $object + * + * @return string + */ + private function getObjetName($object) { + return sprintf('[object] (%s)', get_class($object)); + } + + /** + * Converts a resource to a String + * + * @used-by F7cloud\LogNormalizer\Normalizer::normalize + * + * @param $data + * + * @return string|null + */ + private function normalizeResource($data) { + if (is_resource($data)) { + return '[resource] ' . substr((string)$data, 0, 40); + } + + return null; + } + +} diff --git a/3rdparty/fusonic/opengraph/LICENSE b/3rdparty/fusonic/opengraph/LICENSE new file mode 100644 index 00000000..bb8f68c2 --- /dev/null +++ b/3rdparty/fusonic/opengraph/LICENSE @@ -0,0 +1,20 @@ +Copyright (c) 2014-2024 Fusonic GmbH (https://www.fusonic.net) + +Permission is hereby granted, free of charge, to any person obtaining +a copy of this software and associated documentation files (the +"Software"), to deal in the Software without restriction, including +without limitation the rights to use, copy, modify, merge, publish, +distribute, sublicense, and/or sell copies of the Software, and to +permit persons to whom the Software is furnished to do so, subject to +the following conditions: + +The above copyright notice and this permission notice shall be +included in all copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, +EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF +MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND +NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE +LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION +OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION +WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. diff --git a/3rdparty/fusonic/opengraph/src/Consumer.php b/3rdparty/fusonic/opengraph/src/Consumer.php new file mode 100644 index 00000000..7dc884ab --- /dev/null +++ b/3rdparty/fusonic/opengraph/src/Consumer.php @@ -0,0 +1,142 @@ +client || null === $this->requestFactory) { + throw new \LogicException( + 'To use loadUrl() you must provide $client and $requestFactory when instantiating the consumer.' + ); + } + + $request = $this->requestFactory->createRequest('GET', $url); + $response = $this->client->sendRequest($request); + + return $this->loadHtml($response->getBody()->getContents(), $url); + } + + /** + * Crawls the given HTML string for OpenGraph data. + * + * @param string $html HTML string, usually whole content of crawled web resource + * @param string|null $fallbackUrl URL to use when fallback mode is enabled + */ + public function loadHtml(string $html, ?string $fallbackUrl = null): ObjectBase + { + // Extract all data that can be found + $page = $this->extractOpenGraphData($html); + + // Use the user's URL as fallback + if ($this->useFallbackMode && null === $page->url) { + $page->url = $fallbackUrl; + } + + // Return result + return $page; + } + + private function extractOpenGraphData(string $content): ObjectBase + { + $crawler = new Crawler(); + $crawler->addHtmlContent(content: $content); + + $properties = []; + foreach (['name', 'property'] as $t) { + // Get all meta-tags starting with "og:" + $ogMetaTags = $crawler->filter("meta[{$t}^='og:']"); + + // Create clean property array + $props = []; + + /** @var \DOMElement $tag */ + foreach ($ogMetaTags as $tag) { + $name = strtolower(trim($tag->getAttribute($t))); + $value = trim($tag->getAttribute('content')); + $props[] = new Property($name, $value); + } + + $properties = array_merge($properties, $props); + } + + // Create new object + $object = new Website(); + + // Assign all properties to the object + $object->assignProperties($properties, $this->debug); + + // Fallback for url + if ($this->useFallbackMode && null === $object->url) { + $urlElement = $crawler->filter("link[rel='canonical']")->first(); + if ($urlElement->count() > 0) { + $object->url = trim($urlElement->attr('href') ?? ''); + } + } + + // Fallback for title + if ($this->useFallbackMode && null === $object->title) { + $titleElement = $crawler->filter('title')->first(); + if ($titleElement->count() > 0) { + $object->title = trim($titleElement->text()); + } + } + + // Fallback for description + if ($this->useFallbackMode && null === $object->description) { + $descriptionElement = $crawler->filter("meta[property='description']")->first(); + if ($descriptionElement->count() > 0) { + $object->description = trim($descriptionElement->attr('content') ?? ''); + } + } + + return $object; + } +} diff --git a/3rdparty/fusonic/opengraph/src/Elements/Audio.php b/3rdparty/fusonic/opengraph/src/Elements/Audio.php new file mode 100644 index 00000000..394348cc --- /dev/null +++ b/3rdparty/fusonic/opengraph/src/Elements/Audio.php @@ -0,0 +1,66 @@ +url = $url; + } + + /** + * Gets all properties set on this element. + * + * @return Property[] + */ + public function getProperties(): array + { + $properties = []; + + // URL must precede all other properties + if (null !== $this->url) { + $properties[] = new Property(Property::AUDIO_URL, $this->url); + } + + if (null !== $this->secureUrl) { + $properties[] = new Property(Property::AUDIO_SECURE_URL, $this->secureUrl); + } + + if (null !== $this->type) { + $properties[] = new Property(Property::AUDIO_TYPE, $this->type); + } + + return $properties; + } +} diff --git a/3rdparty/fusonic/opengraph/src/Elements/ElementBase.php b/3rdparty/fusonic/opengraph/src/Elements/ElementBase.php new file mode 100644 index 00000000..56600649 --- /dev/null +++ b/3rdparty/fusonic/opengraph/src/Elements/ElementBase.php @@ -0,0 +1,25 @@ +url = $url; + } + + /** + * Gets all properties set on this element. + * + * @return Property[] + */ + public function getProperties(): array + { + $properties = []; + + // URL must precede all other properties + if (null !== $this->url) { + $properties[] = new Property(Property::IMAGE_URL, $this->url); + } + + if (null !== $this->height) { + $properties[] = new Property(Property::IMAGE_HEIGHT, $this->height); + } + + if (null !== $this->secureUrl) { + $properties[] = new Property(Property::IMAGE_SECURE_URL, $this->secureUrl); + } + + if (null !== $this->type) { + $properties[] = new Property(Property::IMAGE_TYPE, $this->type); + } + + if (null !== $this->width) { + $properties[] = new Property(Property::IMAGE_WIDTH, $this->width); + } + + if (null !== $this->userGenerated) { + $properties[] = new Property(Property::IMAGE_USER_GENERATED, $this->userGenerated); + } + + return $properties; + } +} diff --git a/3rdparty/fusonic/opengraph/src/Elements/Video.php b/3rdparty/fusonic/opengraph/src/Elements/Video.php new file mode 100644 index 00000000..c5062fe2 --- /dev/null +++ b/3rdparty/fusonic/opengraph/src/Elements/Video.php @@ -0,0 +1,84 @@ +url = $url; + } + + /** + * Gets all properties set on this element. + * + * @return Property[] + */ + public function getProperties(): array + { + $properties = []; + + // URL must precede all other properties + if (null !== $this->url) { + $properties[] = new Property(Property::VIDEO_URL, $this->url); + } + + if (null !== $this->height) { + $properties[] = new Property(Property::VIDEO_HEIGHT, $this->height); + } + + if (null !== $this->secureUrl) { + $properties[] = new Property(Property::VIDEO_SECURE_URL, $this->secureUrl); + } + + if (null !== $this->type) { + $properties[] = new Property(Property::VIDEO_TYPE, $this->type); + } + + if (null !== $this->width) { + $properties[] = new Property(Property::VIDEO_WIDTH, $this->width); + } + + return $properties; + } +} diff --git a/3rdparty/fusonic/opengraph/src/Objects/ObjectBase.php b/3rdparty/fusonic/opengraph/src/Objects/ObjectBase.php new file mode 100644 index 00000000..40e3bf97 --- /dev/null +++ b/3rdparty/fusonic/opengraph/src/Objects/ObjectBase.php @@ -0,0 +1,355 @@ +key; + $value = $property->value; + + switch ($name) { + case Property::AUDIO: + case Property::AUDIO_URL: + $this->audios[] = new Audio($value); + break; + case Property::AUDIO_SECURE_URL: + case Property::AUDIO_TYPE: + if (\count($this->audios) > 0) { + $this->handleAudioAttribute($this->audios[\count($this->audios) - 1], $name, $value); + } elseif ($debug) { + throw new \UnexpectedValueException( + \sprintf( + "Found '%s' property but no audio was found before.", + $name + ) + ); + } + break; + case Property::DESCRIPTION: + if (null === $this->description) { + $this->description = $value; + } + break; + case Property::DETERMINER: + if (null === $this->determiner) { + $this->determiner = $value; + } + break; + case Property::IMAGE: + case Property::IMAGE_URL: + $this->images[] = new Image($value); + break; + case Property::IMAGE_HEIGHT: + case Property::IMAGE_SECURE_URL: + case Property::IMAGE_TYPE: + case Property::IMAGE_WIDTH: + case Property::IMAGE_USER_GENERATED: + if (\count($this->images) > 0) { + $this->handleImageAttribute($this->images[\count($this->images) - 1], $name, $value); + } elseif ($debug) { + throw new \UnexpectedValueException( + \sprintf( + "Found '%s' property but no image was found before.", + $name + ) + ); + } + break; + case Property::LOCALE: + if (null === $this->locale) { + $this->locale = $value; + } + break; + case Property::LOCALE_ALTERNATE: + $this->localeAlternate[] = $value; + break; + case Property::RICH_ATTACHMENT: + $this->richAttachment = $this->convertToBoolean($value); + break; + case Property::SEE_ALSO: + $this->seeAlso[] = $value; + break; + case Property::SITE_NAME: + if (null === $this->siteName) { + $this->siteName = $value; + } + break; + case Property::TITLE: + if (null === $this->title) { + $this->title = $value; + } + break; + case Property::UPDATED_TIME: + if (null === $this->updatedTime) { + $this->updatedTime = $this->convertToDateTime($value); + } + break; + case Property::URL: + if (null === $this->url) { + $this->url = $value; + } + break; + case Property::VIDEO: + case Property::VIDEO_URL: + $this->videos[] = new Video($value); + break; + case Property::VIDEO_HEIGHT: + case Property::VIDEO_SECURE_URL: + case Property::VIDEO_TYPE: + case Property::VIDEO_WIDTH: + if (\count($this->videos) > 0) { + $this->handleVideoAttribute($this->videos[\count($this->videos) - 1], $name, $value); + } elseif ($debug) { + throw new \UnexpectedValueException(\sprintf( + "Found '%s' property but no video was found before.", + $name + )); + } + } + } + } + + private function handleImageAttribute(Image $element, string $name, string $value): void + { + switch ($name) { + case Property::IMAGE_HEIGHT: + $element->height = (int) $value; + break; + case Property::IMAGE_WIDTH: + $element->width = (int) $value; + break; + case Property::IMAGE_TYPE: + $element->type = $value; + break; + case Property::IMAGE_SECURE_URL: + $element->secureUrl = $value; + break; + case Property::IMAGE_USER_GENERATED: + $element->userGenerated = $this->convertToBoolean($value); + break; + } + } + + private function handleVideoAttribute(Video $element, string $name, string $value): void + { + switch ($name) { + case Property::VIDEO_HEIGHT: + $element->height = (int) $value; + break; + case Property::VIDEO_WIDTH: + $element->width = (int) $value; + break; + case Property::VIDEO_TYPE: + $element->type = $value; + break; + case Property::VIDEO_SECURE_URL: + $element->secureUrl = $value; + break; + } + } + + private function handleAudioAttribute(Audio $element, string $name, string $value): void + { + switch ($name) { + case Property::AUDIO_TYPE: + $element->type = $value; + break; + case Property::AUDIO_SECURE_URL: + $element->secureUrl = $value; + break; + } + } + + protected function convertToDateTime(string $value): ?\DateTimeImmutable + { + try { + return new \DateTimeImmutable($value); + } catch (\Exception $e) { + return null; + } + } + + protected function convertToBoolean(string $value): bool + { + switch (strtolower($value)) { + case '1': + case 'true': + return true; + default: + return false; + } + } + + /** + * Gets all properties set on this object. + * + * @return Property[] + */ + public function getProperties(): array + { + $properties = []; + + foreach ($this->audios as $audio) { + $properties = array_merge($properties, $audio->getProperties()); + } + + if (null !== $this->title) { + $properties[] = new Property(Property::TITLE, $this->title); + } + + if (null !== $this->description) { + $properties[] = new Property(Property::DESCRIPTION, $this->description); + } + + if (null !== $this->determiner) { + $properties[] = new Property(Property::DETERMINER, $this->determiner); + } + + foreach ($this->images as $image) { + $properties = array_merge($properties, $image->getProperties()); + } + + if (null !== $this->locale) { + $properties[] = new Property(Property::LOCALE, $this->locale); + } + + foreach ($this->localeAlternate as $locale) { + $properties[] = new Property(Property::LOCALE_ALTERNATE, $locale); + } + + if (null !== $this->richAttachment) { + $properties[] = new Property(Property::RICH_ATTACHMENT, (int) $this->richAttachment); + } + + foreach ($this->seeAlso as $seeAlso) { + $properties[] = new Property(Property::SEE_ALSO, $seeAlso); + } + + if (null !== $this->siteName) { + $properties[] = new Property(Property::SITE_NAME, $this->siteName); + } + + if (null !== $this->type) { + $properties[] = new Property(Property::TYPE, $this->type); + } + + if (null !== $this->updatedTime) { + $properties[] = new Property(Property::UPDATED_TIME, $this->updatedTime->format('c')); + } + + if (null !== $this->url) { + $properties[] = new Property(Property::URL, $this->url); + } + + foreach ($this->videos as $video) { + $properties = array_merge($properties, $video->getProperties()); + } + + return $properties; + } +} diff --git a/3rdparty/fusonic/opengraph/src/Objects/Website.php b/3rdparty/fusonic/opengraph/src/Objects/Website.php new file mode 100644 index 00000000..a973c77f --- /dev/null +++ b/3rdparty/fusonic/opengraph/src/Objects/Website.php @@ -0,0 +1,26 @@ +type = self::TYPE; + } +} diff --git a/3rdparty/fusonic/opengraph/src/Property.php b/3rdparty/fusonic/opengraph/src/Property.php new file mode 100644 index 00000000..00fe7bea --- /dev/null +++ b/3rdparty/fusonic/opengraph/src/Property.php @@ -0,0 +1,58 @@ +doctype ? ' />' : '>'); + + foreach ($object->getProperties() as $property) { + if ('' !== $html) { + $html .= "\n"; + } + + if (null === $property->value) { + continue; + } elseif ($property->value instanceof \DateTimeInterface) { + $value = $property->value->format('c'); + } elseif (\is_object($property->value)) { + throw new \UnexpectedValueException( + \sprintf( + "Cannot handle value of type '%s' for property '%s'.", + \get_class($property->value), + $property->key + ) + ); + } elseif (true === $property->value) { + $value = '1'; + } elseif (false === $property->value) { + $value = '0'; + } else { + $value = (string) $property->value; + } + + $html .= \sprintf($format, $property->key, htmlspecialchars($value)); + } + + return $html; + } +} diff --git a/3rdparty/giggsey/libphonenumber-for-php-lite/LICENSE.txt b/3rdparty/giggsey/libphonenumber-for-php-lite/LICENSE.txt new file mode 100644 index 00000000..66a27ec5 --- /dev/null +++ b/3rdparty/giggsey/libphonenumber-for-php-lite/LICENSE.txt @@ -0,0 +1,177 @@ + Apache License + Version 2.0, January 2004 + http://www.apache.org/licenses/ + + TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION + + 1. Definitions. + + "License" shall mean the terms and conditions for use, reproduction, + and distribution as defined by Sections 1 through 9 of this document. + + "Licensor" shall mean the copyright owner or entity authorized by + the copyright owner that is granting the License. + + "Legal Entity" shall mean the union of the acting entity and all + other entities that control, are controlled by, or are under common + control with that entity. For the purposes of this definition, + "control" means (i) the power, direct or indirect, to cause the + direction or management of such entity, whether by contract or + otherwise, or (ii) ownership of fifty percent (50%) or more of the + outstanding shares, or (iii) beneficial ownership of such entity. + + "You" (or "Your") shall mean an individual or Legal Entity + exercising permissions granted by this License. + + "Source" form shall mean the preferred form for making modifications, + including but not limited to software source code, documentation + source, and configuration files. + + "Object" form shall mean any form resulting from mechanical + transformation or translation of a Source form, including but + not limited to compiled object code, generated documentation, + and conversions to other media types. + + "Work" shall mean the work of authorship, whether in Source or + Object form, made available under the License, as indicated by a + copyright notice that is included in or attached to the work + (an example is provided in the Appendix below). + + "Derivative Works" shall mean any work, whether in Source or Object + form, that is based on (or derived from) the Work and for which the + editorial revisions, annotations, elaborations, or other modifications + represent, as a whole, an original work of authorship. For the purposes + of this License, Derivative Works shall not include works that remain + separable from, or merely link (or bind by name) to the interfaces of, + the Work and Derivative Works thereof. + + "Contribution" shall mean any work of authorship, including + the original version of the Work and any modifications or additions + to that Work or Derivative Works thereof, that is intentionally + submitted to Licensor for inclusion in the Work by the copyright owner + or by an individual or Legal Entity authorized to submit on behalf of + the copyright owner. For the purposes of this definition, "submitted" + means any form of electronic, verbal, or written communication sent + to the Licensor or its representatives, including but not limited to + communication on electronic mailing lists, source code control systems, + and issue tracking systems that are managed by, or on behalf of, the + Licensor for the purpose of discussing and improving the Work, but + excluding communication that is conspicuously marked or otherwise + designated in writing by the copyright owner as "Not a Contribution." + + "Contributor" shall mean Licensor and any individual or Legal Entity + on behalf of whom a Contribution has been received by Licensor and + subsequently incorporated within the Work. + + 2. Grant of Copyright License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + copyright license to reproduce, prepare Derivative Works of, + publicly display, publicly perform, sublicense, and distribute the + Work and such Derivative Works in Source or Object form. + + 3. Grant of Patent License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + (except as stated in this section) patent license to make, have made, + use, offer to sell, sell, import, and otherwise transfer the Work, + where such license applies only to those patent claims licensable + by such Contributor that are necessarily infringed by their + Contribution(s) alone or by combination of their Contribution(s) + with the Work to which such Contribution(s) was submitted. If You + institute patent litigation against any entity (including a + cross-claim or counterclaim in a lawsuit) alleging that the Work + or a Contribution incorporated within the Work constitutes direct + or contributory patent infringement, then any patent licenses + granted to You under this License for that Work shall terminate + as of the date such litigation is filed. + + 4. Redistribution. You may reproduce and distribute copies of the + Work or Derivative Works thereof in any medium, with or without + modifications, and in Source or Object form, provided that You + meet the following conditions: + + (a) You must give any other recipients of the Work or + Derivative Works a copy of this License; and + + (b) You must cause any modified files to carry prominent notices + stating that You changed the files; and + + (c) You must retain, in the Source form of any Derivative Works + that You distribute, all copyright, patent, trademark, and + attribution notices from the Source form of the Work, + excluding those notices that do not pertain to any part of + the Derivative Works; and + + (d) If the Work includes a "NOTICE" text file as part of its + distribution, then any Derivative Works that You distribute must + include a readable copy of the attribution notices contained + within such NOTICE file, excluding those notices that do not + pertain to any part of the Derivative Works, in at least one + of the following places: within a NOTICE text file distributed + as part of the Derivative Works; within the Source form or + documentation, if provided along with the Derivative Works; or, + within a display generated by the Derivative Works, if and + wherever such third-party notices normally appear. The contents + of the NOTICE file are for informational purposes only and + do not modify the License. You may add Your own attribution + notices within Derivative Works that You distribute, alongside + or as an addendum to the NOTICE text from the Work, provided + that such additional attribution notices cannot be construed + as modifying the License. + + You may add Your own copyright statement to Your modifications and + may provide additional or different license terms and conditions + for use, reproduction, or distribution of Your modifications, or + for any such Derivative Works as a whole, provided Your use, + reproduction, and distribution of the Work otherwise complies with + the conditions stated in this License. + + 5. Submission of Contributions. Unless You explicitly state otherwise, + any Contribution intentionally submitted for inclusion in the Work + by You to the Licensor shall be under the terms and conditions of + this License, without any additional terms or conditions. + Notwithstanding the above, nothing herein shall supersede or modify + the terms of any separate license agreement you may have executed + with Licensor regarding such Contributions. + + 6. Trademarks. This License does not grant permission to use the trade + names, trademarks, service marks, or product names of the Licensor, + except as required for reasonable and customary use in describing the + origin of the Work and reproducing the content of the NOTICE file. + + 7. Disclaimer of Warranty. Unless required by applicable law or + agreed to in writing, Licensor provides the Work (and each + Contributor provides its Contributions) on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or + implied, including, without limitation, any warranties or conditions + of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A + PARTICULAR PURPOSE. You are solely responsible for determining the + appropriateness of using or redistributing the Work and assume any + risks associated with Your exercise of permissions under this License. + + 8. Limitation of Liability. In no event and under no legal theory, + whether in tort (including negligence), contract, or otherwise, + unless required by applicable law (such as deliberate and grossly + negligent acts) or agreed to in writing, shall any Contributor be + liable to You for damages, including any direct, indirect, special, + incidental, or consequential damages of any character arising as a + result of this License or out of the use or inability to use the + Work (including but not limited to damages for loss of goodwill, + work stoppage, computer failure or malfunction, or any and all + other commercial damages or losses), even if such Contributor + has been advised of the possibility of such damages. + + 9. Accepting Warranty or Additional Liability. While redistributing + the Work or Derivative Works thereof, You may choose to offer, + and charge a fee for, acceptance of support, warranty, indemnity, + or other liability obligations and/or rights consistent with this + License. However, in accepting such obligations, You may act only + on Your own behalf and on Your sole responsibility, not on behalf + of any other Contributor, and only if You agree to indemnify, + defend, and hold each Contributor harmless for any liability + incurred by, or claims asserted against, such Contributor by reason + of your accepting any such warranty or additional liability. + + END OF TERMS AND CONDITIONS + diff --git a/3rdparty/giggsey/libphonenumber-for-php-lite/METADATA-VERSION.php b/3rdparty/giggsey/libphonenumber-for-php-lite/METADATA-VERSION.php new file mode 100644 index 00000000..c8a75d09 --- /dev/null +++ b/3rdparty/giggsey/libphonenumber-for-php-lite/METADATA-VERSION.php @@ -0,0 +1,11 @@ + + */ + public const COUNTRY_CODE_TO_REGION_CODE_MAP = [ + 1 => [ + 'US', + 'AG', + 'AI', + 'AS', + 'BB', + 'BM', + 'BS', + 'CA', + 'DM', + 'DO', + 'GD', + 'GU', + 'JM', + 'KN', + 'KY', + 'LC', + 'MP', + 'MS', + 'PR', + 'SX', + 'TC', + 'TT', + 'VC', + 'VG', + 'VI', + ], + 7 => ['RU', 'KZ'], + 20 => ['EG'], + 27 => ['ZA'], + 30 => ['GR'], + 31 => ['NL'], + 32 => ['BE'], + 33 => ['FR'], + 34 => ['ES'], + 36 => ['HU'], + 39 => ['IT', 'VA'], + 40 => ['RO'], + 41 => ['CH'], + 43 => ['AT'], + 44 => ['GB', 'GG', 'IM', 'JE'], + 45 => ['DK'], + 46 => ['SE'], + 47 => ['NO', 'SJ'], + 48 => ['PL'], + 49 => ['DE'], + 51 => ['PE'], + 52 => ['MX'], + 53 => ['CU'], + 54 => ['AR'], + 55 => ['BR'], + 56 => ['CL'], + 57 => ['CO'], + 58 => ['VE'], + 60 => ['MY'], + 61 => ['AU', 'CC', 'CX'], + 62 => ['ID'], + 63 => ['PH'], + 64 => ['NZ'], + 65 => ['SG'], + 66 => ['TH'], + 81 => ['JP'], + 82 => ['KR'], + 84 => ['VN'], + 86 => ['CN'], + 90 => ['TR'], + 91 => ['IN'], + 92 => ['PK'], + 93 => ['AF'], + 94 => ['LK'], + 95 => ['MM'], + 98 => ['IR'], + 211 => ['SS'], + 212 => ['MA', 'EH'], + 213 => ['DZ'], + 216 => ['TN'], + 218 => ['LY'], + 220 => ['GM'], + 221 => ['SN'], + 222 => ['MR'], + 223 => ['ML'], + 224 => ['GN'], + 225 => ['CI'], + 226 => ['BF'], + 227 => ['NE'], + 228 => ['TG'], + 229 => ['BJ'], + 230 => ['MU'], + 231 => ['LR'], + 232 => ['SL'], + 233 => ['GH'], + 234 => ['NG'], + 235 => ['TD'], + 236 => ['CF'], + 237 => ['CM'], + 238 => ['CV'], + 239 => ['ST'], + 240 => ['GQ'], + 241 => ['GA'], + 242 => ['CG'], + 243 => ['CD'], + 244 => ['AO'], + 245 => ['GW'], + 246 => ['IO'], + 247 => ['AC'], + 248 => ['SC'], + 249 => ['SD'], + 250 => ['RW'], + 251 => ['ET'], + 252 => ['SO'], + 253 => ['DJ'], + 254 => ['KE'], + 255 => ['TZ'], + 256 => ['UG'], + 257 => ['BI'], + 258 => ['MZ'], + 260 => ['ZM'], + 261 => ['MG'], + 262 => ['RE', 'YT'], + 263 => ['ZW'], + 264 => ['NA'], + 265 => ['MW'], + 266 => ['LS'], + 267 => ['BW'], + 268 => ['SZ'], + 269 => ['KM'], + 290 => ['SH', 'TA'], + 291 => ['ER'], + 297 => ['AW'], + 298 => ['FO'], + 299 => ['GL'], + 350 => ['GI'], + 351 => ['PT'], + 352 => ['LU'], + 353 => ['IE'], + 354 => ['IS'], + 355 => ['AL'], + 356 => ['MT'], + 357 => ['CY'], + 358 => ['FI', 'AX'], + 359 => ['BG'], + 370 => ['LT'], + 371 => ['LV'], + 372 => ['EE'], + 373 => ['MD'], + 374 => ['AM'], + 375 => ['BY'], + 376 => ['AD'], + 377 => ['MC'], + 378 => ['SM'], + 380 => ['UA'], + 381 => ['RS'], + 382 => ['ME'], + 383 => ['XK'], + 385 => ['HR'], + 386 => ['SI'], + 387 => ['BA'], + 389 => ['MK'], + 420 => ['CZ'], + 421 => ['SK'], + 423 => ['LI'], + 500 => ['FK'], + 501 => ['BZ'], + 502 => ['GT'], + 503 => ['SV'], + 504 => ['HN'], + 505 => ['NI'], + 506 => ['CR'], + 507 => ['PA'], + 508 => ['PM'], + 509 => ['HT'], + 590 => ['GP', 'BL', 'MF'], + 591 => ['BO'], + 592 => ['GY'], + 593 => ['EC'], + 594 => ['GF'], + 595 => ['PY'], + 596 => ['MQ'], + 597 => ['SR'], + 598 => ['UY'], + 599 => ['CW', 'BQ'], + 670 => ['TL'], + 672 => ['NF'], + 673 => ['BN'], + 674 => ['NR'], + 675 => ['PG'], + 676 => ['TO'], + 677 => ['SB'], + 678 => ['VU'], + 679 => ['FJ'], + 680 => ['PW'], + 681 => ['WF'], + 682 => ['CK'], + 683 => ['NU'], + 685 => ['WS'], + 686 => ['KI'], + 687 => ['NC'], + 688 => ['TV'], + 689 => ['PF'], + 690 => ['TK'], + 691 => ['FM'], + 692 => ['MH'], + 800 => ['001'], + 808 => ['001'], + 850 => ['KP'], + 852 => ['HK'], + 853 => ['MO'], + 855 => ['KH'], + 856 => ['LA'], + 870 => ['001'], + 878 => ['001'], + 880 => ['BD'], + 881 => ['001'], + 882 => ['001'], + 883 => ['001'], + 886 => ['TW'], + 888 => ['001'], + 960 => ['MV'], + 961 => ['LB'], + 962 => ['JO'], + 963 => ['SY'], + 964 => ['IQ'], + 965 => ['KW'], + 966 => ['SA'], + 967 => ['YE'], + 968 => ['OM'], + 970 => ['PS'], + 971 => ['AE'], + 972 => ['IL'], + 973 => ['BH'], + 974 => ['QA'], + 975 => ['BT'], + 976 => ['MN'], + 977 => ['NP'], + 979 => ['001'], + 992 => ['TJ'], + 993 => ['TM'], + 994 => ['AZ'], + 995 => ['GE'], + 996 => ['KG'], + 998 => ['UZ'], + ]; +} diff --git a/3rdparty/giggsey/libphonenumber-for-php-lite/src/MatchType.php b/3rdparty/giggsey/libphonenumber-for-php-lite/src/MatchType.php new file mode 100644 index 00000000..cb7b7989 --- /dev/null +++ b/3rdparty/giggsey/libphonenumber-for-php-lite/src/MatchType.php @@ -0,0 +1,18 @@ + + */ + protected array $groups = []; + + private int $searchIndex = 0; + + public function __construct(string $pattern, string $subject) + { + $this->pattern = str_replace('/', '\/', $pattern); + $this->subject = $subject; + } + + protected function doMatch(string $type = 'find', int $offset = 0): bool + { + $final_pattern = '(?:' . $this->pattern . ')'; + switch ($type) { + case 'matches': + $final_pattern = '^' . $final_pattern . '$'; + break; + case 'lookingAt': + $final_pattern = '^' . $final_pattern; + break; + case 'find': + default: + // no changes + break; + } + $final_pattern = '/' . $final_pattern . '/ui'; + + $search = mb_substr($this->subject, $offset); + + $result = preg_match($final_pattern, $search, $groups, PREG_OFFSET_CAPTURE); + + if ($result === 1) { + // Expand $groups into $this->groups, but being multi-byte aware + + $positions = []; + + foreach ($groups as $group) { + $positions[] = [ + $group[0], + $offset + mb_strlen(substr($search, 0, $group[1])), + ]; + } + + $this->groups = $positions; + } + + return ($result === 1); + } + + public function matches(): bool + { + return $this->doMatch('matches'); + } + + public function lookingAt(): bool + { + return $this->doMatch('lookingAt'); + } + + public function find(?int $offset = null): bool + { + if ($offset === null) { + $offset = $this->searchIndex; + } + + // Increment search index for the next time we call this + $this->searchIndex++; + return $this->doMatch('find', $offset); + } + + public function groupCount(): ?int + { + if ($this->groups === []) { + return null; + } + + return count($this->groups) - 1; + } + + public function group(?int $group = null): ?string + { + if ($group === null) { + $group = 0; + } + return $this->groups[$group][0] ?? null; + } + + public function end(?int $group = null): ?int + { + if ($group === null) { + $group = 0; + } + if (!isset($this->groups[$group])) { + return null; + } + return $this->groups[$group][1] + mb_strlen($this->groups[$group][0]); + } + + public function start(?int $group = null): mixed + { + if ($group === null) { + $group = 0; + } + if (!isset($this->groups[$group])) { + return null; + } + + return $this->groups[$group][1]; + } + + public function replaceFirst(string $replacement): string + { + return preg_replace('/' . $this->pattern . '/x', $replacement, $this->subject, 1); + } + + public function replaceAll(string $replacement): string + { + return preg_replace('/' . $this->pattern . '/x', $replacement, $this->subject); + } + + public function reset(string $input = ''): static + { + $this->subject = $input; + + return $this; + } +} diff --git a/3rdparty/giggsey/libphonenumber-for-php-lite/src/MatcherAPIInterface.php b/3rdparty/giggsey/libphonenumber-for-php-lite/src/MatcherAPIInterface.php new file mode 100644 index 00000000..1a449bee --- /dev/null +++ b/3rdparty/giggsey/libphonenumber-for-php-lite/src/MatcherAPIInterface.php @@ -0,0 +1,23 @@ +regionToMetadataMap[$regionCode])) { + // The regionCode here will be valid and won't be '001', so we don't need to worry about + // what to pass in for the country calling code. + $this->loadMetadataFromFile($this->currentFilePrefix, $regionCode, 0); + } + + return $this->regionToMetadataMap[$regionCode]; + } + + public function getMetadataForNonGeographicalRegion(int $countryCallingCode): PhoneMetadata + { + if (!isset($this->countryCodeToNonGeographicalMetadataMap[$countryCallingCode])) { + $this->loadMetadataFromFile($this->currentFilePrefix, PhoneNumberUtil::REGION_CODE_FOR_NON_GEO_ENTITY, $countryCallingCode); + } + + return $this->countryCodeToNonGeographicalMetadataMap[$countryCallingCode]; + } + + /** + * @throws RuntimeException + */ + public function loadMetadataFromFile(string $filePrefix, string $regionCode, int $countryCallingCode): void + { + $regionCode = strtoupper($regionCode); + + $isNonGeoRegion = PhoneNumberUtil::REGION_CODE_FOR_NON_GEO_ENTITY === $regionCode; + + $class = $filePrefix . ($isNonGeoRegion ? $countryCallingCode : ucfirst($regionCode)); + + if (!class_exists($class)) { + throw new RuntimeException('missing metadata: ' . $class); + } + + $metadata = new $class(); + + if (!$metadata instanceof PhoneMetadata) { + throw new RuntimeException('invalid metadata: ' . $class); + } + + if ($isNonGeoRegion) { + $this->countryCodeToNonGeographicalMetadataMap[$countryCallingCode] = $metadata; + } else { + $this->regionToMetadataMap[$regionCode] = $metadata; + } + } +} diff --git a/3rdparty/giggsey/libphonenumber-for-php-lite/src/NumberFormat.php b/3rdparty/giggsey/libphonenumber-for-php-lite/src/NumberFormat.php new file mode 100644 index 00000000..a36153ef --- /dev/null +++ b/3rdparty/giggsey/libphonenumber-for-php-lite/src/NumberFormat.php @@ -0,0 +1,183 @@ + + */ + protected array $leadingDigitsPattern = []; + protected string $nationalPrefixFormattingRule = ''; + protected bool $hasNationalPrefixFormattingRule = false; + protected bool $nationalPrefixOptionalWhenFormatting = false; + protected bool $hasNationalPrefixOptionalWhenFormatting = false; + protected string $domesticCarrierCodeFormattingRule = ''; + protected bool $hasDomesticCarrierCodeFormattingRule = false; + + public function hasPattern(): bool + { + return $this->hasPattern; + } + + public function getPattern(): string + { + return $this->pattern; + } + + public function setPattern(string $value): static + { + $this->hasPattern = true; + $this->pattern = $value; + + return $this; + } + + public function hasNationalPrefixOptionalWhenFormatting(): bool + { + return $this->hasNationalPrefixOptionalWhenFormatting; + } + + public function getNationalPrefixOptionalWhenFormatting(): bool + { + return $this->nationalPrefixOptionalWhenFormatting; + } + + public function setNationalPrefixOptionalWhenFormatting(bool $nationalPrefixOptionalWhenFormatting): static + { + $this->hasNationalPrefixOptionalWhenFormatting = true; + $this->nationalPrefixOptionalWhenFormatting = $nationalPrefixOptionalWhenFormatting; + + return $this; + } + + public function hasFormat(): bool + { + return $this->hasFormat; + } + + public function getFormat(): string + { + return $this->format; + } + + public function setFormat(string $value): static + { + $this->hasFormat = true; + $this->format = $value; + + return $this; + } + + /** + * @return array + */ + public function leadingDigitPatterns(): array + { + return $this->leadingDigitsPattern; + } + + public function leadingDigitsPatternSize(): int + { + return count($this->leadingDigitsPattern); + } + + public function getLeadingDigitsPattern(int $index): string + { + return $this->leadingDigitsPattern[$index]; + } + + /** + * @param array $patterns + */ + public function setLeadingDigitsPattern(array $patterns): static + { + $this->leadingDigitsPattern = $patterns; + return $this; + } + + public function addLeadingDigitsPattern(string $value): static + { + $this->leadingDigitsPattern[] = $value; + + return $this; + } + + public function hasNationalPrefixFormattingRule(): bool + { + return $this->hasNationalPrefixFormattingRule; + } + + public function getNationalPrefixFormattingRule(): string + { + return $this->nationalPrefixFormattingRule; + } + + public function setNationalPrefixFormattingRule(string $value): static + { + $this->hasNationalPrefixFormattingRule = true; + $this->nationalPrefixFormattingRule = $value; + + return $this; + } + + public function clearNationalPrefixFormattingRule(): static + { + $this->nationalPrefixFormattingRule = ''; + + return $this; + } + + public function hasDomesticCarrierCodeFormattingRule(): bool + { + return $this->hasDomesticCarrierCodeFormattingRule; + } + + public function getDomesticCarrierCodeFormattingRule(): string + { + return $this->domesticCarrierCodeFormattingRule; + } + + public function setDomesticCarrierCodeFormattingRule(string $value): static + { + $this->hasDomesticCarrierCodeFormattingRule = true; + $this->domesticCarrierCodeFormattingRule = $value; + + return $this; + } + + public function mergeFrom(NumberFormat $other): static + { + if ($other->hasPattern()) { + $this->setPattern($other->getPattern()); + } + if ($other->hasFormat()) { + $this->setFormat($other->getFormat()); + } + $leadingDigitsPatternSize = $other->leadingDigitsPatternSize(); + for ($i = 0; $i < $leadingDigitsPatternSize; $i++) { + $this->addLeadingDigitsPattern($other->getLeadingDigitsPattern($i)); + } + if ($other->hasNationalPrefixFormattingRule()) { + $this->setNationalPrefixFormattingRule($other->getNationalPrefixFormattingRule()); + } + if ($other->hasDomesticCarrierCodeFormattingRule()) { + $this->setDomesticCarrierCodeFormattingRule($other->getDomesticCarrierCodeFormattingRule()); + } + if ($other->hasNationalPrefixOptionalWhenFormatting()) { + $this->setNationalPrefixOptionalWhenFormatting($other->getNationalPrefixOptionalWhenFormatting()); + } + + return $this; + } +} diff --git a/3rdparty/giggsey/libphonenumber-for-php-lite/src/NumberParseException.php b/3rdparty/giggsey/libphonenumber-for-php-lite/src/NumberParseException.php new file mode 100644 index 00000000..b2f8f735 --- /dev/null +++ b/3rdparty/giggsey/libphonenumber-for-php-lite/src/NumberParseException.php @@ -0,0 +1,64 @@ +message = $message; + $this->errorType = $errorType; + } + + /** + * Returns the error type of the exception that has been thrown. + */ + public function getErrorType(): int + { + return $this->errorType; + } + + public function __toString(): string + { + return 'Error type: ' . $this->errorType . '. ' . $this->message; + } +} diff --git a/3rdparty/giggsey/libphonenumber-for-php-lite/src/PhoneMetadata.php b/3rdparty/giggsey/libphonenumber-for-php-lite/src/PhoneMetadata.php new file mode 100644 index 00000000..aa625148 --- /dev/null +++ b/3rdparty/giggsey/libphonenumber-for-php-lite/src/PhoneMetadata.php @@ -0,0 +1,305 @@ +mainCountryForCode; + } + + public function getMainCountryForCode(): bool + { + return $this->mainCountryForCode; + } + + public function numberFormatSize(): int + { + return count($this->numberFormat); + } + + public function getNumberFormat(int $index): NumberFormat + { + return $this->numberFormat[$index]; + } + + public function intlNumberFormatSize(): int + { + return count($this->intlNumberFormat); + } + + public function getIntlNumberFormat(int $index): NumberFormat + { + return $this->intlNumberFormat[$index]; + } + + public function hasGeneralDesc(): bool + { + return $this->generalDesc !== null; + } + + public function getGeneralDesc(): ?PhoneNumberDesc + { + return $this->generalDesc; + } + + public function hasFixedLine(): bool + { + return $this->fixedLine !== null; + } + + public function getFixedLine(): ?PhoneNumberDesc + { + return $this->fixedLine; + } + + public function hasMobile(): bool + { + return $this->mobile !== null; + } + + public function getMobile(): ?PhoneNumberDesc + { + return $this->mobile; + } + + public function getTollFree(): ?PhoneNumberDesc + { + return $this->tollFree; + } + + public function getPremiumRate(): ?PhoneNumberDesc + { + return $this->premiumRate; + } + + public function getSharedCost(): ?PhoneNumberDesc + { + return $this->sharedCost; + } + + public function getPersonalNumber(): ?PhoneNumberDesc + { + return $this->personalNumber; + } + + public function getVoip(): ?PhoneNumberDesc + { + return $this->voip; + } + + public function getPager(): ?PhoneNumberDesc + { + return $this->pager; + } + + public function getUan(): ?PhoneNumberDesc + { + return $this->uan; + } + + public function hasEmergency(): bool + { + return $this->emergency !== null; + } + + public function getEmergency(): ?PhoneNumberDesc + { + return $this->emergency; + } + + public function getVoicemail(): ?PhoneNumberDesc + { + return $this->voicemail; + } + + public function getShortCode(): ?PhoneNumberDesc + { + return $this->short_code; + } + + + public function getStandardRate(): ?PhoneNumberDesc + { + return $this->standard_rate; + } + + public function getCarrierSpecific(): ?PhoneNumberDesc + { + return $this->carrierSpecific; + } + + public function getSmsServices(): ?PhoneNumberDesc + { + return $this->smsServices; + } + + public function getNoInternationalDialling(): ?PhoneNumberDesc + { + return $this->noInternationalDialling; + } + + + public function getId(): ?string + { + return static::ID; + } + + public function getCountryCode(): ?int + { + return static::COUNTRY_CODE; + } + + public function getInternationalPrefix(): ?string + { + return $this->internationalPrefix; + } + + + public function hasPreferredInternationalPrefix(): bool + { + return ($this->preferredInternationalPrefix !== null); + } + + public function getPreferredInternationalPrefix(): ?string + { + return $this->preferredInternationalPrefix; + } + + public function hasNationalPrefix(): bool + { + return static::NATIONAL_PREFIX !== null; + } + + public function getNationalPrefix(): ?string + { + return static::NATIONAL_PREFIX; + } + + public function hasPreferredExtnPrefix(): bool + { + return $this->preferredExtnPrefix !== null; + } + + public function getPreferredExtnPrefix(): ?string + { + return $this->preferredExtnPrefix; + } + + public function hasNationalPrefixForParsing(): bool + { + return $this->nationalPrefixForParsing !== null; + } + + public function getNationalPrefixForParsing(): ?string + { + return $this->nationalPrefixForParsing; + } + + public function getNationalPrefixTransformRule(): ?string + { + return $this->nationalPrefixTransformRule; + } + + public function getSameMobileAndFixedLinePattern(): bool + { + return $this->sameMobileAndFixedLinePattern; + } + + /** + * @return NumberFormat[] + */ + public function numberFormats(): array + { + return $this->numberFormat; + } + + /** + * @return NumberFormat[] + */ + public function intlNumberFormats(): array + { + return $this->intlNumberFormat; + } + + public function hasLeadingDigits(): bool + { + return static::LEADING_DIGITS !== null; + } + + public function getLeadingDigits(): ?string + { + return static::LEADING_DIGITS; + } + + public function isMobileNumberPortableRegion(): bool + { + return $this->mobileNumberPortableRegion; + } + + public function setInternationalPrefix(string $value): static + { + $this->internationalPrefix = $value; + return $this; + } +} diff --git a/3rdparty/giggsey/libphonenumber-for-php-lite/src/PhoneNumber.php b/3rdparty/giggsey/libphonenumber-for-php-lite/src/PhoneNumber.php new file mode 100644 index 00000000..0d5de5d3 --- /dev/null +++ b/3rdparty/giggsey/libphonenumber-for-php-lite/src/PhoneNumber.php @@ -0,0 +1,409 @@ +clearCountryCode(); + $this->clearNationalNumber(); + $this->clearExtension(); + $this->clearItalianLeadingZero(); + $this->clearNumberOfLeadingZeros(); + $this->clearRawInput(); + $this->clearCountryCodeSource(); + $this->clearPreferredDomesticCarrierCode(); + return $this; + } + + public function clearCountryCode(): static + { + $this->countryCode = null; + return $this; + } + + public function clearNationalNumber(): static + { + $this->nationalNumber = null; + return $this; + } + + public function clearExtension(): static + { + $this->extension = null; + return $this; + } + + public function clearItalianLeadingZero(): static + { + $this->italianLeadingZero = null; + return $this; + } + + public function clearNumberOfLeadingZeros(): static + { + $this->hasNumberOfLeadingZeros = false; + $this->numberOfLeadingZeros = 1; + return $this; + } + + public function clearRawInput(): static + { + $this->rawInput = null; + return $this; + } + + public function clearCountryCodeSource(): static + { + $this->countryCodeSource = CountryCodeSource::UNSPECIFIED; + return $this; + } + + public function clearPreferredDomesticCarrierCode(): static + { + $this->preferredDomesticCarrierCode = null; + return $this; + } + + /** + * Merges the information from another phone number into this phone number. + */ + public function mergeFrom(PhoneNumber $other): static + { + if ($other->hasCountryCode()) { + $this->setCountryCode($other->getCountryCode()); + } + if ($other->hasNationalNumber()) { + $this->setNationalNumber($other->getNationalNumber()); + } + if ($other->hasExtension()) { + $this->setExtension($other->getExtension()); + } + if ($other->hasItalianLeadingZero()) { + $this->setItalianLeadingZero($other->isItalianLeadingZero()); + } + if ($other->hasNumberOfLeadingZeros()) { + $this->setNumberOfLeadingZeros($other->getNumberOfLeadingZeros()); + } + if ($other->hasRawInput()) { + $this->setRawInput($other->getRawInput()); + } + if ($other->hasCountryCodeSource()) { + $this->setCountryCodeSource($other->getCountryCodeSource()); + } + if ($other->hasPreferredDomesticCarrierCode()) { + $this->setPreferredDomesticCarrierCode($other->getPreferredDomesticCarrierCode()); + } + return $this; + } + + public function hasCountryCode(): bool + { + return $this->countryCode !== null; + } + + public function getCountryCode(): ?int + { + return $this->countryCode; + } + + public function setCountryCode(int $value): static + { + $this->countryCode = $value; + return $this; + } + + public function hasNationalNumber(): bool + { + return $this->nationalNumber !== null; + } + + public function getNationalNumber(): ?string + { + return $this->nationalNumber; + } + + public function setNationalNumber(string $value): static + { + $this->nationalNumber = $value; + return $this; + } + + public function hasExtension(): bool + { + return isset($this->extension) && $this->extension !== ''; + } + + public function getExtension(): ?string + { + return $this->extension; + } + + public function setExtension(string $value): static + { + $this->extension = $value; + return $this; + } + + public function hasItalianLeadingZero(): bool + { + return isset($this->italianLeadingZero); + } + + public function setItalianLeadingZero(bool $value): static + { + $this->italianLeadingZero = $value; + return $this; + } + + /** + * Returns whether this phone number uses an italian leading zero. + * + * @return bool|null True if it uses an italian leading zero, false it it does not, null if not set. + */ + public function isItalianLeadingZero(): ?bool + { + return $this->italianLeadingZero ?? null; + } + + public function hasNumberOfLeadingZeros(): bool + { + return $this->hasNumberOfLeadingZeros; + } + + public function getNumberOfLeadingZeros(): int + { + return $this->numberOfLeadingZeros; + } + + public function setNumberOfLeadingZeros(int $value): static + { + $this->hasNumberOfLeadingZeros = true; + $this->numberOfLeadingZeros = $value; + return $this; + } + + public function hasRawInput(): bool + { + return isset($this->rawInput); + } + + public function getRawInput(): ?string + { + return $this->rawInput; + } + + public function setRawInput(string $value): static + { + $this->rawInput = $value; + return $this; + } + + public function hasCountryCodeSource(): bool + { + return $this->countryCodeSource !== CountryCodeSource::UNSPECIFIED; + } + + public function getCountryCodeSource(): ?CountryCodeSource + { + return $this->countryCodeSource; + } + + public function setCountryCodeSource(CountryCodeSource $value): static + { + $this->countryCodeSource = $value; + return $this; + } + + public function hasPreferredDomesticCarrierCode(): bool + { + return isset($this->preferredDomesticCarrierCode); + } + + public function getPreferredDomesticCarrierCode(): ?string + { + return $this->preferredDomesticCarrierCode; + } + + public function setPreferredDomesticCarrierCode(string $value): static + { + $this->preferredDomesticCarrierCode = $value; + return $this; + } + + /** + * Returns whether this phone number is equal to another. + * + * @param PhoneNumber $other The phone number to compare. + * + * @return bool True if the phone numbers are equal, false otherwise. + */ + public function equals(PhoneNumber $other): bool + { + if ($this === $other) { + return true; + } + + return $this->countryCode === $other->countryCode + && $this->nationalNumber === $other->nationalNumber + && $this->extension === $other->extension + && $this->italianLeadingZero === $other->italianLeadingZero + && $this->numberOfLeadingZeros === $other->numberOfLeadingZeros + && $this->rawInput === $other->rawInput + && $this->countryCodeSource === $other->countryCodeSource + && $this->preferredDomesticCarrierCode === $other->preferredDomesticCarrierCode; + } + + /** + * Returns a string representation of this phone number. + */ + public function __toString(): string + { + $outputString = 'Country Code: ' . $this->countryCode; + $outputString .= ' National Number: ' . $this->nationalNumber; + if ($this->hasItalianLeadingZero()) { + $outputString .= ' Leading Zero(s): true'; + } + if ($this->hasNumberOfLeadingZeros()) { + $outputString .= ' Number of leading zeros: ' . $this->numberOfLeadingZeros; + } + if ($this->hasExtension()) { + $outputString .= ' Extension: ' . $this->extension; + } + if ($this->hasCountryCodeSource()) { + $outputString .= ' Country Code Source: ' . $this->countryCodeSource->name; + } + if ($this->hasPreferredDomesticCarrierCode()) { + $outputString .= ' Preferred Domestic Carrier Code: ' . $this->preferredDomesticCarrierCode; + } + return $outputString; + } + + public function serialize(): ?string + { + return serialize($this->__serialize()); + } + + public function __serialize(): array + { + return [ + $this->countryCode, + $this->nationalNumber, + $this->extension, + $this->italianLeadingZero, + $this->numberOfLeadingZeros, + $this->rawInput, + $this->countryCodeSource, + $this->preferredDomesticCarrierCode, + ]; + } + + public function unserialize($data): void + { + $this->__unserialize(unserialize($data, ['allowed_classes' => [__CLASS__]])); + } + + /** + * @param array{int,string,string,bool|null,int,string|null,CountryCodeSource|null,string|null} $data + */ + public function __unserialize(array $data): void + { + [ + $this->countryCode, + $this->nationalNumber, + $this->extension, + $this->italianLeadingZero, + $this->numberOfLeadingZeros, + $this->rawInput, + $countryCodeSource, + $this->preferredDomesticCarrierCode + ] = $data; + + // BC layer to allow this method to unserialize "old" phonenumbers + if (is_int($countryCodeSource)) { + $countryCodeSource = CountryCodeSource::from($countryCodeSource); + } + $this->countryCodeSource = $countryCodeSource; + + if ($this->numberOfLeadingZeros > 1) { + $this->hasNumberOfLeadingZeros = true; + } + } +} diff --git a/3rdparty/giggsey/libphonenumber-for-php-lite/src/PhoneNumberDesc.php b/3rdparty/giggsey/libphonenumber-for-php-lite/src/PhoneNumberDesc.php new file mode 100644 index 00000000..5ba1ec91 --- /dev/null +++ b/3rdparty/giggsey/libphonenumber-for-php-lite/src/PhoneNumberDesc.php @@ -0,0 +1,140 @@ +possibleLength; + } + + /** + * @param int[] $possibleLength + */ + public function setPossibleLength(array $possibleLength): static + { + $this->possibleLength = $possibleLength; + return $this; + } + + public function addPossibleLength(int $possibleLength): void + { + if (!in_array($possibleLength, $this->possibleLength, true)) { + $this->possibleLength[] = $possibleLength; + } + } + + public function clearPossibleLength(): void + { + $this->possibleLength = []; + } + + /** + * @return int[] + */ + public function getPossibleLengthLocalOnly(): array + { + return $this->possibleLengthLocalOnly; + } + + /** + * @param int[] $possibleLengthLocalOnly + */ + public function setPossibleLengthLocalOnly(array $possibleLengthLocalOnly): static + { + $this->possibleLengthLocalOnly = $possibleLengthLocalOnly; + + return $this; + } + + public function addPossibleLengthLocalOnly(int $possibleLengthLocalOnly): void + { + if (!in_array($possibleLengthLocalOnly, $this->possibleLengthLocalOnly, true)) { + $this->possibleLengthLocalOnly[] = $possibleLengthLocalOnly; + } + } + + public function clearPossibleLengthLocalOnly(): void + { + $this->possibleLengthLocalOnly = []; + } + + /** + * @return boolean + */ + public function hasNationalNumberPattern(): bool + { + return $this->hasNationalNumberPattern; + } + + public function getNationalNumberPattern(): string + { + return $this->nationalNumberPattern; + } + + public function setNationalNumberPattern(string $value): static + { + $this->hasNationalNumberPattern = true; + $this->nationalNumberPattern = $value; + + return $this; + } + + public function hasExampleNumber(): bool + { + return $this->hasExampleNumber; + } + + public function getExampleNumber(): string + { + return $this->exampleNumber; + } + + public function setExampleNumber(string $value): static + { + $this->hasExampleNumber = true; + $this->exampleNumber = $value; + + return $this; + } + + private static self $emptyObject; + + /** + * Used for metadata as a shortcut to an empty object + * Use the same object to reduce load further + * @internal + */ + public static function empty(): self + { + if (!isset(self::$emptyObject)) { + self::$emptyObject = (new self()) + ->setPossibleLength([-1]); + } + + return self::$emptyObject; + } +} diff --git a/3rdparty/giggsey/libphonenumber-for-php-lite/src/PhoneNumberFormat.php b/3rdparty/giggsey/libphonenumber-for-php-lite/src/PhoneNumberFormat.php new file mode 100644 index 00000000..21920478 --- /dev/null +++ b/3rdparty/giggsey/libphonenumber-for-php-lite/src/PhoneNumberFormat.php @@ -0,0 +1,25 @@ += 0.'); + } + + $this->start = $start; + $this->rawString = $rawString; + $this->number = $number; + } + + /** + * Returns the phone number matched by the receiver. + */ + public function number(): PhoneNumber + { + return $this->number; + } + + /** + * Returns the start index of the matched phone number within the searched text. + */ + public function start(): int + { + return $this->start; + } + + /** + * Returns the exclusive end index of the matched phone number within the searched text. + */ + public function end(): int + { + return $this->start + mb_strlen($this->rawString); + } + + /** + * Returns the raw string matched as a phone number in the searched text. + */ + public function rawString(): string + { + return $this->rawString; + } + + public function __toString() + { + return "PhoneNumberMatch [{$this->start()},{$this->end()}) {$this->rawString}"; + } +} diff --git a/3rdparty/giggsey/libphonenumber-for-php-lite/src/PhoneNumberType.php b/3rdparty/giggsey/libphonenumber-for-php-lite/src/PhoneNumberType.php new file mode 100644 index 00000000..e0b303e5 --- /dev/null +++ b/3rdparty/giggsey/libphonenumber-for-php-lite/src/PhoneNumberType.php @@ -0,0 +1,56 @@ +If you use this library, and want to be notified about important changes, please sign up to + * our mailing list. + * + * NOTE: A lot of methods in this class require Region Code strings. These must be provided using + * CLDR two-letter region-code format. These should be in upper-case. The list of the codes + * can be found here: + * http://www.unicode.org/cldr/charts/30/supplemental/territory_information.html + * @phpstan-consistent-constructor + * @no-named-arguments + */ +class PhoneNumberUtil +{ + /** Flags to use when compiling regular expressions for phone numbers */ + protected const REGEX_FLAGS = 'ui'; //Unicode and case insensitive + // The minimum and maximum length of the national significant number. + protected const MIN_LENGTH_FOR_NSN = 2; + // The ITU says the maximum length should be 15, but we have found longer numbers in Germany. + protected const MAX_LENGTH_FOR_NSN = 17; + + // We don't allow input strings for parsing to be longer than 250 chars. This prevents malicious + // input from overflowing the regular-expression engine. + protected const MAX_INPUT_STRING_LENGTH = 250; + + // The maximum length of the country calling code. + protected const MAX_LENGTH_COUNTRY_CODE = 3; + + /** + * @internal + */ + public const REGION_CODE_FOR_NON_GEO_ENTITY = '001'; + + // Region-code for the unknown region. + public const UNKNOWN_REGION = 'ZZ'; + + protected const NANPA_COUNTRY_CODE = 1; + // The PLUS_SIGN signifies the international prefix. + protected const PLUS_SIGN = '+'; + protected const PLUS_CHARS = '++'; + protected const STAR_SIGN = '*'; + + protected const RFC3966_EXTN_PREFIX = ';ext='; + protected const RFC3966_PREFIX = 'tel:'; + protected const RFC3966_PHONE_CONTEXT = ';phone-context='; + protected const RFC3966_ISDN_SUBADDRESS = ';isub='; + + // We use this pattern to check if the phone number has at least three letters in it - if so, then + // we treat it as a number where some phone-number digits are represented by letters. + protected const VALID_ALPHA_PHONE_PATTERN = '(?:.*?[A-Za-z]){3}.*'; + // We accept alpha characters in phone numbers, ASCII only, upper and lower case. + protected const VALID_ALPHA = 'A-Za-z'; + + // Default extension prefix to use when formatting. This will be put in front of any extension + // component of the number, after the main national number is formatted. For example, if you wish + // the default extension formatting to be " extn: 3456", then you should specify " extn: " here + // as the default extension prefix. This can be overridden by region-specific preferences. + protected const DEFAULT_EXTN_PREFIX = ' ext. '; + + // Regular expression of acceptable punctuation found in phone numbers, used to find numbers in + // text and to decide what is a viable phone number. This excludes diallable characters. + // This consists of dash characters, white space characters, full stops, slashes, + // square brackets, parentheses and tildes. It also includes the letter 'x' as that is found as a + // placeholder for carrier information in some phone numbers. Full-width variants are also + // present. + protected const VALID_PUNCTUATION = "-x\xE2\x80\x90-\xE2\x80\x95\xE2\x88\x92\xE3\x83\xBC\xEF\xBC\x8D-\xEF\xBC\x8F \xC2\xA0\xC2\xAD\xE2\x80\x8B\xE2\x81\xA0\xE3\x80\x80()\xEF\xBC\x88\xEF\xBC\x89\xEF\xBC\xBB\xEF\xBC\xBD.\\[\\]/~\xE2\x81\x93\xE2\x88\xBC"; + protected const DIGITS = '\\p{Nd}'; + + // Pattern that makes it easy to distinguish whether a region has a single international dialing + // prefix or not. If a region has a single international prefix (e.g. 011 in USA), it will be + // represented as a string that contains a sequence of ASCII digits, and possible a tilde, which + // signals waiting for the tone. If there are multiple available international prefixes in a + // region, they will be represented as a regex string that always contains one or more characters + // that are not ASCII digits or a tilde. + protected const SINGLE_INTERNATIONAL_PREFIX = "[\\d]+(?:[~\xE2\x81\x93\xE2\x88\xBC\xEF\xBD\x9E][\\d]+)?"; + protected const NON_DIGITS_PATTERN = '(\\D+)'; + + // The FIRST_GROUP_PATTERN was originally set to $1 but there are some countries for which the + // first group is not used in the national pattern (e.g. Argentina) so the $1 group does not match + // correctly. Therefore, we use \d, so that the first group actually used in the pattern will be + // matched. + protected const FIRST_GROUP_PATTERN = '(\$\\d)'; + // Constants used in the formatting rules to represent the national prefix, first group and + // carrier code respectively. + protected const NP_STRING = '$NP'; + protected const FG_STRING = '$FG'; + protected const CC_STRING = '$CC'; + + // A pattern that is used to determine if the national prefix formatting rule has the first group + // only, i.e, does not start with the national prefix. Note that the pattern explicitly allows + // for unbalanced parentheses. + protected const FIRST_GROUP_ONLY_PREFIX_PATTERN = '\\(?\\$1\\)?'; + /** + * @internal + */ + public const PLUS_CHARS_PATTERN = '[' . self::PLUS_CHARS . ']+'; + protected const SEPARATOR_PATTERN = '[' . self::VALID_PUNCTUATION . ']+'; + protected const CAPTURING_DIGIT_PATTERN = '(' . self::DIGITS . ')'; + protected const VALID_START_CHAR_PATTERN = '[' . self::PLUS_CHARS . self::DIGITS . ']'; + protected const SECOND_NUMBER_START_PATTERN = '[\\\\/] *x'; + //protected const UNWANTED_END_CHAR_PATTERN = '[[\\P{N}&&\\P{L}]&&[^#]]+$'; + protected const UNWANTED_END_CHAR_PATTERN = '[^' . self::DIGITS . self::VALID_ALPHA . '#]+$'; + protected const DIALLABLE_CHAR_MAPPINGS = self::ASCII_DIGIT_MAPPINGS + + [self::PLUS_SIGN => self::PLUS_SIGN] + + ['*' => '*', '#' => '#']; + + protected static ?PhoneNumberUtil $instance; + + /** + * Only upper-case variants of alpha characters are stored. + */ + protected const ALPHA_MAPPINGS = [ + 'A' => '2', + 'B' => '2', + 'C' => '2', + 'D' => '3', + 'E' => '3', + 'F' => '3', + 'G' => '4', + 'H' => '4', + 'I' => '4', + 'J' => '5', + 'K' => '5', + 'L' => '5', + 'M' => '6', + 'N' => '6', + 'O' => '6', + 'P' => '7', + 'Q' => '7', + 'R' => '7', + 'S' => '7', + 'T' => '8', + 'U' => '8', + 'V' => '8', + 'W' => '9', + 'X' => '9', + 'Y' => '9', + 'Z' => '9', + ]; + + /** + * Map of country calling codes that use a mobile token before the area code. One example of when + * this is relevant is when determining the length of the national destination code, which should + * be the length of the area code plus the length of the mobile token. + */ + protected const MOBILE_TOKEN_MAPPINGS = [ + '54' => '9', + ]; + + /** + * Set of country codes that have geographically assigned mobile numbers (see GEO_MOBILE_COUNTRIES + * below) which are not based on *area codes*. For example, in China mobile numbers start with a + * carrier indicator, and beyond that are geographically assigned: this carrier indicator is not + * considered to be an area code. + */ + protected const GEO_MOBILE_COUNTRIES_WITHOUT_MOBILE_AREA_CODES = [ + 86, // China + ]; + + /** + * Set of country codes that doesn't have national prefix, but it has area codes. + */ + protected const COUNTRIES_WITHOUT_NATIONAL_PREFIX_WITH_AREA_CODES = [ + 52, // Mexico + ]; + + /** + * Set of country calling codes that have geographically assigned mobile numbers. This may not be + * complete; we add calling codes case by case, as we find geographical mobile numbers or hear + * from user reports. Note that countries like the US, where we can't distinguish between + * fixed-line or mobile numbers, are not listed here, since we consider FIXED_LINE_OR_MOBILE to be + * a possibly geographically-related type anyway (like FIXED_LINE). + */ + protected const GEO_MOBILE_COUNTRIES = [ + 52, // Mexico + 54, // Argentina + 55, // Brazil + 62, // Indonesia: some prefixes only (fixed CMDA wireless) + 86, // China + ]; + + /** + * For performance reasons, amalgamate both into one map. + */ + protected const ALPHA_PHONE_MAPPINGS = self::ALPHA_MAPPINGS + self::ASCII_DIGIT_MAPPINGS; + + /** + * Separate map of all symbols that we wish to retain when formatting alpha numbers. This + * includes digits, ASCII letters and number grouping symbols such as "-" and " ". + * + * @var array + */ + protected static array $ALL_PLUS_NUMBER_GROUPING_SYMBOLS; + + /** + * Simple ASCII digits map used to populate ALPHA_PHONE_MAPPINGS and + * ALL_PLUS_NUMBER_GROUPING_SYMBOLS. + */ + protected const ASCII_DIGIT_MAPPINGS = [ + '0' => '0', + '1' => '1', + '2' => '2', + '3' => '3', + '4' => '4', + '5' => '5', + '6' => '6', + '7' => '7', + '8' => '8', + '9' => '9', + ]; + + /** + * Regexp of all possible ways to write extensions, for use when parsing. This will be run as a + * case-insensitive regexp match. Wide character versions are also provided after each ASCII + * version. + */ + protected static string $EXTN_PATTERNS_FOR_PARSING; + protected static string $EXTN_PATTERNS_FOR_MATCHING; + // Regular expression of valid global-number-digits for the phone-context parameter, following the + // syntax defined in RFC3966. + protected static string $RFC3966_VISUAL_SEPARATOR = '[\\-\\.\\(\\)]?'; + protected static string $RFC3966_PHONE_DIGIT; + protected static string $RFC3966_GLOBAL_NUMBER_DIGITS; + + // Regular expression of valid domainname for the phone-context parameter, following the syntax + // defined in RFC3966. + protected static string $ALPHANUM; + protected static string $RFC3966_DOMAINLABEL; + protected static string $RFC3966_TOPLABEL; + protected static string $RFC3966_DOMAINNAME; + protected static string $EXTN_PATTERN; + protected static string $VALID_PHONE_NUMBER_PATTERN; + protected static string $MIN_LENGTH_PHONE_NUMBER_PATTERN; + /** + * Regular expression of viable phone numbers. This is location independent. Checks we have at + * least three leading digits, and only valid punctuation, alpha characters and + * digits in the phone number. Does not include extension data. + * The symbol 'x' is allowed here as valid punctuation since it is often used as a placeholder for + * carrier codes, for example in Brazilian phone numbers. We also allow multiple "+" characters at + * the start. + * Corresponds to the following: + * [digits]{minLengthNsn}| + * plus_sign*(([punctuation]|[star])*[digits]){3,}([punctuation]|[star]|[digits]|[alpha])* + * + * The first reg-ex is to allow short numbers (two digits long) to be parsed if they are entered + * as "15" etc, but only if there is no punctuation in them. The second expression restricts the + * number of digits to three or more, but then allows them to be in international form, and to + * have alpha-characters and punctuation. + * + * Note VALID_PUNCTUATION starts with a -, so must be the first in the range. + */ + protected static string $VALID_PHONE_NUMBER; + protected const NUMERIC_CHARACTERS = [ + "\xef\xbc\x90" => 0, + "\xef\xbc\x91" => 1, + "\xef\xbc\x92" => 2, + "\xef\xbc\x93" => 3, + "\xef\xbc\x94" => 4, + "\xef\xbc\x95" => 5, + "\xef\xbc\x96" => 6, + "\xef\xbc\x97" => 7, + "\xef\xbc\x98" => 8, + "\xef\xbc\x99" => 9, + + "\xd9\xa0" => 0, + "\xd9\xa1" => 1, + "\xd9\xa2" => 2, + "\xd9\xa3" => 3, + "\xd9\xa4" => 4, + "\xd9\xa5" => 5, + "\xd9\xa6" => 6, + "\xd9\xa7" => 7, + "\xd9\xa8" => 8, + "\xd9\xa9" => 9, + + "\xdb\xb0" => 0, + "\xdb\xb1" => 1, + "\xdb\xb2" => 2, + "\xdb\xb3" => 3, + "\xdb\xb4" => 4, + "\xdb\xb5" => 5, + "\xdb\xb6" => 6, + "\xdb\xb7" => 7, + "\xdb\xb8" => 8, + "\xdb\xb9" => 9, + + "\xe1\xa0\x90" => 0, + "\xe1\xa0\x91" => 1, + "\xe1\xa0\x92" => 2, + "\xe1\xa0\x93" => 3, + "\xe1\xa0\x94" => 4, + "\xe1\xa0\x95" => 5, + "\xe1\xa0\x96" => 6, + "\xe1\xa0\x97" => 7, + "\xe1\xa0\x98" => 8, + "\xe1\xa0\x99" => 9, + ]; + + /** + * The set of county calling codes that map to the non-geo entity region ("001"). + * + * @var int[] + */ + protected array $countryCodesForNonGeographicalRegion = []; + /** + * The set of regions the library supports. + * + * @var string[] + */ + protected array $supportedRegions = []; + + /** + * A mapping from a country calling code to the region codes which denote the region represented + * by that country calling code. In the case of multiple regions sharing a calling code, such as + * the NANPA regions, the one indicated with "isMainCountryForCode" in the metadata should be + * first. + * + * @var array + */ + protected array $countryCallingCodeToRegionCodeMap = []; + /** + * The set of regions that share country calling code 1. + * + * @var string[] + */ + protected array $nanpaRegions = []; + protected MetadataSourceInterface $metadataSource; + protected MatcherAPIInterface $matcherAPI; + + /** + * This class implements a singleton, so the only constructor is protected. + * @param array $countryCallingCodeToRegionCodeMap + */ + protected function __construct(MetadataSourceInterface $metadataSource, array $countryCallingCodeToRegionCodeMap) + { + $this->metadataSource = $metadataSource; + $this->countryCallingCodeToRegionCodeMap = $countryCallingCodeToRegionCodeMap; + $this->init(); + $this->matcherAPI = new RegexBasedMatcher(); + static::initExtnPatterns(); + static::initExtnPattern(); + static::initRFC3966Patterns(); + + static::$ALL_PLUS_NUMBER_GROUPING_SYMBOLS = []; + // Put (lower letter -> upper letter) and (upper letter -> upper letter) mappings. + foreach (static::ALPHA_MAPPINGS as $c => $value) { + static::$ALL_PLUS_NUMBER_GROUPING_SYMBOLS[strtolower($c)] = $c; + static::$ALL_PLUS_NUMBER_GROUPING_SYMBOLS[(string) $c] = (string) $c; + } + static::$ALL_PLUS_NUMBER_GROUPING_SYMBOLS += static::ASCII_DIGIT_MAPPINGS; + static::$ALL_PLUS_NUMBER_GROUPING_SYMBOLS['-'] = '-'; + static::$ALL_PLUS_NUMBER_GROUPING_SYMBOLS["\xEF\xBC\x8D"] = '-'; + static::$ALL_PLUS_NUMBER_GROUPING_SYMBOLS["\xE2\x80\x90"] = '-'; + static::$ALL_PLUS_NUMBER_GROUPING_SYMBOLS["\xE2\x80\x91"] = '-'; + static::$ALL_PLUS_NUMBER_GROUPING_SYMBOLS["\xE2\x80\x92"] = '-'; + static::$ALL_PLUS_NUMBER_GROUPING_SYMBOLS["\xE2\x80\x93"] = '-'; + static::$ALL_PLUS_NUMBER_GROUPING_SYMBOLS["\xE2\x80\x94"] = '-'; + static::$ALL_PLUS_NUMBER_GROUPING_SYMBOLS["\xE2\x80\x95"] = '-'; + static::$ALL_PLUS_NUMBER_GROUPING_SYMBOLS["\xE2\x88\x92"] = '-'; + static::$ALL_PLUS_NUMBER_GROUPING_SYMBOLS['/'] = '/'; + static::$ALL_PLUS_NUMBER_GROUPING_SYMBOLS["\xEF\xBC\x8F"] = '/'; + static::$ALL_PLUS_NUMBER_GROUPING_SYMBOLS[' '] = ' '; + static::$ALL_PLUS_NUMBER_GROUPING_SYMBOLS["\xE3\x80\x80"] = ' '; + static::$ALL_PLUS_NUMBER_GROUPING_SYMBOLS["\xE2\x81\xA0"] = ' '; + static::$ALL_PLUS_NUMBER_GROUPING_SYMBOLS['.'] = '.'; + static::$ALL_PLUS_NUMBER_GROUPING_SYMBOLS["\xEF\xBC\x8E"] = '.'; + + static::initValidPhoneNumberPatterns(); + } + + /** + * Gets a {@link PhoneNumberUtil} instance to carry out international phone number formatting, + * parsing or validation. The instance is loaded with phone number metadata for a number of most + * commonly used regions. + * + *

The {@link PhoneNumberUtil} is implemented as a singleton. Therefore, calling getInstance + * multiple times will only result in one instance being created. + * + * @param array $countryCallingCodeToRegionCodeMap + * @return PhoneNumberUtil instance + */ + public static function getInstance( + string $metadataLocation = __NAMESPACE__ . '\data\PhoneNumberMetadata_', + array $countryCallingCodeToRegionCodeMap = CountryCodeToRegionCodeMap::COUNTRY_CODE_TO_REGION_CODE_MAP, + ): PhoneNumberUtil { + if (!isset(static::$instance)) { + $metadataSource = new MultiFileMetadataSourceImpl($metadataLocation); + + static::$instance = new static($metadataSource, $countryCallingCodeToRegionCodeMap); + } + return static::$instance; + } + + protected function init(): void + { + $supportedRegions = [[]]; + + foreach ($this->countryCallingCodeToRegionCodeMap as $countryCode => $regionCodes) { + // We can assume that if the country calling code maps to the non-geo entity region code then + // that's the only region code it maps to. + if (count($regionCodes) === 1 && static::REGION_CODE_FOR_NON_GEO_ENTITY === $regionCodes[0]) { + // This is the subset of all country codes that map to the non-geo entity region code. + $this->countryCodesForNonGeographicalRegion[] = $countryCode; + } else { + // The supported regions set does not include the "001" non-geo entity region code. + $supportedRegions[] = $regionCodes; + } + } + + $this->supportedRegions = array_merge(...$supportedRegions); + + // If the non-geo entity still got added to the set of supported regions it must be because + // there are entries that list the non-geo entity alongside normal regions (which is wrong). + // If we discover this, remove the non-geo entity from the set of supported regions and log. + $idx_region_code_non_geo_entity = array_search(static::REGION_CODE_FOR_NON_GEO_ENTITY, $this->supportedRegions, true); + if ($idx_region_code_non_geo_entity !== false) { + unset($this->supportedRegions[$idx_region_code_non_geo_entity]); + } + $this->nanpaRegions = $this->countryCallingCodeToRegionCodeMap[static::NANPA_COUNTRY_CODE]; + } + + protected static function initExtnPatterns(): void + { + static::$EXTN_PATTERNS_FOR_PARSING = static::createExtnPattern(true); + static::$EXTN_PATTERNS_FOR_MATCHING = static::createExtnPattern(false); + } + + protected static function initRFC3966Patterns(): void + { + static::$RFC3966_PHONE_DIGIT = '(' . static::DIGITS . '|' . static::$RFC3966_VISUAL_SEPARATOR . ')'; + static::$RFC3966_GLOBAL_NUMBER_DIGITS = '^\\' . static::PLUS_SIGN . static::$RFC3966_PHONE_DIGIT . '*' . static::DIGITS . static::$RFC3966_PHONE_DIGIT . '*$'; + + static::$ALPHANUM = static::VALID_ALPHA . static::DIGITS; + static::$RFC3966_DOMAINLABEL = '[' . static::$ALPHANUM . ']+((\\-)*[' . static::$ALPHANUM . '])*'; + static::$RFC3966_TOPLABEL = '[' . static::VALID_ALPHA . ']+((\\-)*[' . static::$ALPHANUM . '])*'; + static::$RFC3966_DOMAINNAME = '^(' . static::$RFC3966_DOMAINLABEL . '\\.)*' . static::$RFC3966_TOPLABEL . '\\.?$'; + } + + /** + * Helper method for constructing regular expressions for parsing. Creates an expression that + * captures up to maxLength digits. + */ + protected static function extnDigits(int $maxLength): string + { + return '(' . self::DIGITS . '{1,' . $maxLength . '})'; + } + + /** + * Helper initialiser method to create the regular-expression pattern to match extensions. + * Note that there are currently six capturing groups for the extension itself. If this number is + * changed, MaybeStripExtension needs to be updated. + */ + protected static function createExtnPattern(bool $forParsing): string + { + // We cap the maximum length of an extension based on the ambiguity of the way the extension is + // prefixed. As per ITU, the officially allowed length for extensions is actually 40, but we + // don't support this since we haven't seen real examples and this introduces many false + // interpretations as the extension labels are not standardized. + $extLimitAfterExplicitLabel = 20; + $extLimitAfterLikelyLabel = 15; + $extLimitAfterAmbiguousChar = 9; + $extLimitWhenNotSure = 6; + + + + $possibleSeparatorsBetweenNumberAndExtLabel = "[ \xC2\xA0\\t,]*"; + // Optional full stop (.) or colon, followed by zero or more spaces/tabs/commas. + $possibleCharsAfterExtLabel = "[:\\.\xEf\xBC\x8E]?[ \xC2\xA0\\t,-]*"; + $optionalExtnSuffix = '#?'; + + // Here the extension is called out in more explicit way, i.e mentioning it obvious patterns + // like "ext.". Canonical-equivalence doesn't seem to be an option with Android java, so we + // allow two options for representing the accented o - the character itself, and one in the + // unicode decomposed form with the combining acute accent. + $explicitExtLabels = "(?:e?xt(?:ensi(?:o\xCC\x81?|\xC3\xB3))?n?|\xEF\xBD\x85?\xEF\xBD\x98\xEF\xBD\x94\xEF\xBD\x8E?|\xD0\xB4\xD0\xBE\xD0\xB1|anexo)"; + // One-character symbols that can be used to indicate an extension, and less commonly used + // or more ambiguous extension labels. + $ambiguousExtLabels = "(?:[x\xEF\xBD\x98#\xEF\xBC\x83~\xEF\xBD\x9E]|int|\xEF\xBD\x89\xEF\xBD\x8E\xEF\xBD\x94)"; + // When extension is not separated clearly. + $ambiguousSeparator = '[- ]+'; + + $rfcExtn = static::RFC3966_EXTN_PREFIX . static::extnDigits($extLimitAfterExplicitLabel); + $explicitExtn = $possibleSeparatorsBetweenNumberAndExtLabel . $explicitExtLabels + . $possibleCharsAfterExtLabel . static::extnDigits($extLimitAfterExplicitLabel) + . $optionalExtnSuffix; + $ambiguousExtn = $possibleSeparatorsBetweenNumberAndExtLabel . $ambiguousExtLabels + . $possibleCharsAfterExtLabel . static::extnDigits($extLimitAfterAmbiguousChar) . $optionalExtnSuffix; + $americanStyleExtnWithSuffix = $ambiguousSeparator . static::extnDigits($extLimitWhenNotSure) . '#'; + + // The first regular expression covers RFC 3966 format, where the extension is added using + // ";ext=". The second more generic where extension is mentioned with explicit labels like + // "ext:". In both the above cases we allow more numbers in extension than any other extension + // labels. The third one captures when single character extension labels or less commonly used + // labels are used. In such cases we capture fewer extension digits in order to reduce the + // chance of falsely interpreting two numbers beside each other as a number + extension. The + // fourth one covers the special case of American numbers where the extension is written with a + // hash at the end, such as "- 503#". + $extensionPattern = + $rfcExtn . '|' + . $explicitExtn . '|' + . $ambiguousExtn . '|' + . $americanStyleExtnWithSuffix; + // Additional pattern that is supported when parsing extensions, not when matching. + if ($forParsing) { + // This is same as possibleSeparatorsBetweenNumberAndExtLabel, but not matching comma as + // extension label may have it. + $possibleSeparatorsNumberExtLabelNoComma = "[ \xC2\xA0\\t]*"; + // ",," is commonly used for auto dialling the extension when connected. First comma is matched + // through possibleSeparatorsBetweenNumberAndExtLabel, so we do not repeat it here. Semi-colon + // works in Iphone and Android also to pop up a button with the extension number following. + $autoDiallingAndExtLabelsFound = '(?:,{2}|;)'; + + $autoDiallingExtn = $possibleSeparatorsNumberExtLabelNoComma + . $autoDiallingAndExtLabelsFound . $possibleCharsAfterExtLabel + . static::extnDigits($extLimitAfterLikelyLabel) . $optionalExtnSuffix; + $onlyCommasExtn = $possibleSeparatorsNumberExtLabelNoComma + . '(?:,)+' . $possibleCharsAfterExtLabel . static::extnDigits($extLimitAfterAmbiguousChar) + . $optionalExtnSuffix; + // Here the first pattern is exclusively for extension autodialling formats which are used + // when dialling and in this case we accept longer extensions. However, the second pattern + // is more liberal on the number of commas that acts as extension labels, so we have a strict + // cap on the number of digits in such extensions. + return $extensionPattern . '|' + . $autoDiallingExtn . '|' + . $onlyCommasExtn; + } + return $extensionPattern; + } + + protected static function initExtnPattern(): void + { + static::$EXTN_PATTERN = '/(?:' . static::$EXTN_PATTERNS_FOR_PARSING . ')$/' . static::REGEX_FLAGS; + } + + protected static function initValidPhoneNumberPatterns(): void + { + static::initExtnPatterns(); + static::$MIN_LENGTH_PHONE_NUMBER_PATTERN = '[' . static::DIGITS . ']{' . static::MIN_LENGTH_FOR_NSN . '}'; + static::$VALID_PHONE_NUMBER = '[' . static::PLUS_CHARS . ']*(?:[' . static::VALID_PUNCTUATION . static::STAR_SIGN . ']*[' . static::DIGITS . ']){3,}[' . static::VALID_PUNCTUATION . static::STAR_SIGN . static::VALID_ALPHA . static::DIGITS . ']*'; + static::$VALID_PHONE_NUMBER_PATTERN = '%^' . static::$MIN_LENGTH_PHONE_NUMBER_PATTERN . '$|^' . static::$VALID_PHONE_NUMBER . '(?:' . static::$EXTN_PATTERNS_FOR_PARSING . ')?$%' . static::REGEX_FLAGS; + } + + /** + * Used for testing purposes only to reset the PhoneNumberUtil singleton to null. + */ + public static function resetInstance(): void + { + static::$instance = null; + } + + /** + * Converts all alpha characters in a number to their respective digits on a keypad, but retains + * existing formatting. + */ + public static function convertAlphaCharactersInNumber(string $number): string + { + return static::normalizeHelper($number, static::ALPHA_PHONE_MAPPINGS, false); + } + + /** + * Normalizes a string of characters representing a phone number by replacing all characters found + * in the accompanying map with the values therein, and stripping all other characters if + * removeNonMatches is true. + * + * @param string $number a string of characters representing a phone number + * @param array $normalizationReplacements a mapping of characters to what they should be replaced by in + * the normalized version of the phone number. + * @param bool $removeNonMatches indicates whether characters that are not able to be replaced. + * should be stripped from the number. If this is false, they will be left unchanged in the number. + * @return string the normalized string version of the phone number. + */ + protected static function normalizeHelper(string $number, array $normalizationReplacements, bool $removeNonMatches): string + { + $normalizedNumber = ''; + $strLength = mb_strlen($number, 'UTF-8'); + for ($i = 0; $i < $strLength; $i++) { + $character = mb_substr($number, $i, 1, 'UTF-8'); + if (isset($normalizationReplacements[mb_strtoupper($character, 'UTF-8')])) { + $normalizedNumber .= $normalizationReplacements[mb_strtoupper($character, 'UTF-8')]; + } elseif (!$removeNonMatches) { + $normalizedNumber .= $character; + } + // If neither of the above are true, we remove this character. + } + return $normalizedNumber; + } + + /** + * Helper function to check if the national prefix formatting rule has the first group only, i.e., + * does not start with the national prefix. + */ + public static function formattingRuleHasFirstGroupOnly(string $nationalPrefixFormattingRule): bool + { + $firstGroupOnlyPrefixPatternMatcher = new Matcher( + static::FIRST_GROUP_ONLY_PREFIX_PATTERN, + $nationalPrefixFormattingRule + ); + + return $nationalPrefixFormattingRule === '' + || $firstGroupOnlyPrefixPatternMatcher->matches(); + } + + /** + * Returns all regions the library has metadata for. + * + * @return string[] An unordered array of the two-letter region codes for every geographical region the + * library supports + */ + public function getSupportedRegions(): array + { + return $this->supportedRegions; + } + + /** + * Returns all global network calling codes the library has metadata for. + * + * @return int[] An unordered array of the country calling codes for every non-geographical entity + * the library supports + */ + public function getSupportedGlobalNetworkCallingCodes(): array + { + return $this->countryCodesForNonGeographicalRegion; + } + + /** + * Returns all country calling codes the library has metadata for, covering both non-geographical + * entities (global network calling codes) and those used for geographical entities. The could be + * used to populate a drop-down box of country calling codes for a phone-number widget, for + * instance. + * + * @return int[] An unordered array of the country calling codes for every geographical and + * non-geographical entity the library supports + */ + public function getSupportedCallingCodes(): array + { + return array_keys($this->countryCallingCodeToRegionCodeMap); + } + + /** + * Returns true if there is any possible number data set for a particular PhoneNumberDesc. + */ + protected static function descHasPossibleNumberData(PhoneNumberDesc $desc): bool + { + // If this is empty, it means numbers of this type inherit from the "general desc" -> the value + // '-1' means that no numbers exist for this type. + $possibleLength = $desc->getPossibleLength(); + return count($possibleLength) !== 1 || $possibleLength[0] !== -1; + } + + /** + * Returns true if there is any data set for a particular PhoneNumberDesc. + */ + protected static function descHasData(PhoneNumberDesc $desc): bool + { + // Checking most properties since we don't know what's present, since a custom build may have + // stripped just one of them (e.g. liteBuild strips exampleNumber). We don't bother checking the + // possibleLengthsLocalOnly, since if this is the only thing that's present we don't really + // support the type at all: no type-specific methods will work with only this data. + return $desc->hasExampleNumber() + || static::descHasPossibleNumberData($desc) + || $desc->hasNationalNumberPattern(); + } + + /** + * Returns the types we have metadata for based on the PhoneMetadata object passed in. + * @return array + */ + private function getSupportedTypesForMetadata(PhoneMetadata $metadata): array + { + $types = []; + foreach (PhoneNumberType::cases() as $type) { + if ($type === PhoneNumberType::FIXED_LINE_OR_MOBILE || $type === PhoneNumberType::UNKNOWN) { + // Never return FIXED_LINE_OR_MOBILE (it is a convenience type, and represents that a + // particular number type can't be determined) or UNKNOWN (the non-type). + continue; + } + + if (self::descHasData($this->getNumberDescByType($metadata, $type))) { + $types[] = $type; + } + } + + return $types; + } + + /** + * Returns the types for a given region which the library has metadata for. Will not include + * FIXED_LINE_OR_MOBILE (if the numbers in this region could be classified as FIXED_LINE_OR_MOBILE, + * both FIXED_LINE and MOBILE would be present) and UNKNOWN. + * + * No types will be returned for invalid or unknown region codes. + * + * @return array + */ + public function getSupportedTypesForRegion(string $regionCode): array + { + if (!$this->isValidRegionCode($regionCode)) { + return []; + } + $metadata = $this->getMetadataForRegion($regionCode); + return $this->getSupportedTypesForMetadata($metadata); + } + + /** + * Returns the types for a country-code belonging to a non-geographical entity which the library + * has metadata for. Will not include FIXED_LINE_OR_MOBILE (if numbers for this non-geographical + * entity could be classified as FIXED_LINE_OR_MOBILE, both FIXED_LINE and MOBILE would be + * present) and UNKNOWN. + * + * @return array + */ + public function getSupportedTypesForNonGeoEntity(int $countryCallingCode): array + { + $metadata = $this->getMetadataForNonGeographicalRegion($countryCallingCode); + if ($metadata === null) { + return []; + } + + return $this->getSupportedTypesForMetadata($metadata); + } + + /** + * Gets the length of the geographical area code from the {@code nationalNumber} field of the + * PhoneNumber object passed in, so that clients could use it to split a national significant + * number into geographical area code and subscriber number. It works in such a way that the + * resultant subscriber number should be diallable, at least on some devices. An example of how + * this could be used: + * + * + * $phoneUtil = PhoneNumberUtil::getInstance(); + * $number = $phoneUtil->parse("16502530000", "US"); + * $nationalSignificantNumber = $phoneUtil->getNationalSignificantNumber($number); + * + * $areaCodeLength = $phoneUtil->getLengthOfGeographicalAreaCode($number); + * if ($areaCodeLength > 0) + * { + * $areaCode = substr($nationalSignificantNumber, 0,$areaCodeLength); + * $subscriberNumber = substr($nationalSignificantNumber, $areaCodeLength); + * } else { + * $areaCode = ""; + * $subscriberNumber = $nationalSignificantNumber; + * } + * + * + * N.B.: area code is a very ambiguous concept, so the I18N team generally recommends against + * using it for most purposes, but recommends using the more general {@code nationalNumber} + * instead. Read the following carefully before deciding to use this method: + *

    + *
  • geographical area codes change over time, and this method honors those changes; + * therefore, it doesn't guarantee the stability of the result it produces. + *
  • subscriber numbers may not be diallable from all devices (notably mobile devices, which + * typically requires the full national_number to be dialled in most regions). + *
  • most non-geographical numbers have no area codes, including numbers from non-geographical + * entities + *
  • some geographical numbers have no area codes. + *
+ * + * @param PhoneNumber $number PhoneNumber object for which clients want to know the length of the area code. + * @return int the length of area code of the PhoneNumber object passed in. + */ + public function getLengthOfGeographicalAreaCode(PhoneNumber $number): int + { + $metadata = $this->getMetadataForRegion($this->getRegionCodeForNumber($number)); + if ($metadata === null) { + return 0; + } + + $countryCallingCode = $number->getCountryCode(); + + // If a country doesn't use a national prefix, and this number doesn't have an Italian leading + // zero, we assume it is a closed dialling plan with no area codes. + // Note:this is our general assumption, but there are exceptions which are tracked in + // COUNTRIES_WITHOUT_NATIONAL_PREFIX_WITH_AREA_CODES. + if (!$metadata->hasNationalPrefix() + && !$number->isItalianLeadingZero() + && !in_array($countryCallingCode, static::COUNTRIES_WITHOUT_NATIONAL_PREFIX_WITH_AREA_CODES, true) + ) { + return 0; + } + + $type = $this->getNumberType($number); + + if ($type === PhoneNumberType::MOBILE + // Note this is a rough heuristic; it doesn't cover Indonesia well, for example, where area + // codes are present for some mobile phones but not for others. We have no better way of + // representing this in the metadata at this point. + && in_array($countryCallingCode, static::GEO_MOBILE_COUNTRIES_WITHOUT_MOBILE_AREA_CODES, true) + ) { + return 0; + } + + if (!$this->isNumberGeographical($type, $countryCallingCode)) { + return 0; + } + + return $this->getLengthOfNationalDestinationCode($number); + } + + /** + * Returns the metadata for the given region code or {@code null} if the region code is invalid + * or unknown. + */ + public function getMetadataForRegion(?string $regionCode): ?PhoneMetadata + { + if (!$this->isValidRegionCode($regionCode)) { + return null; + } + + return $this->metadataSource->getMetadataForRegion($regionCode); + } + + /** + * Helper function to check region code is not unknown or null. + */ + protected function isValidRegionCode(?string $regionCode): bool + { + return $regionCode !== null && in_array(strtoupper($regionCode), $this->supportedRegions, true); + } + + /** + * Returns the region where a phone number is from. This could be used for geocoding at the region + * level. Only guarantees correct results for valid, full numbers (not short-codes, or invalid + * numbers). + * + * @param PhoneNumber $number the phone number whose origin we want to know + * @return null|string the region where the phone number is from, or null if no region matches this calling + * code + */ + public function getRegionCodeForNumber(PhoneNumber $number): ?string + { + $countryCode = $number->getCountryCode(); + if (!isset($this->countryCallingCodeToRegionCodeMap[$countryCode])) { + return null; + } + $regions = $this->countryCallingCodeToRegionCodeMap[$countryCode]; + if (count($regions) === 1) { + return $regions[0]; + } + + return $this->getRegionCodeForNumberFromRegionList($number, $regions); + } + + /** + * Returns the region code for a number from the list of region codes passing in. + * @param string[] $regionCodes + */ + protected function getRegionCodeForNumberFromRegionList(PhoneNumber $number, array $regionCodes): ?string + { + $nationalNumber = $this->getNationalSignificantNumber($number); + foreach ($regionCodes as $regionCode) { + // If leadingDigits is present, use this. Otherwise, do full validation. + // Metadata cannot be null because the region codes come from the country calling code map. + $metadata = $this->getMetadataForRegion($regionCode); + if ($metadata->hasLeadingDigits()) { + $nbMatches = preg_match( + '/' . $metadata->getLeadingDigits() . '/', + $nationalNumber, + $matches, + PREG_OFFSET_CAPTURE + ); + if ($nbMatches > 0 && $matches[0][1] === 0) { + return $regionCode; + } + } elseif ($this->getNumberTypeHelper($nationalNumber, $metadata) !== PhoneNumberType::UNKNOWN) { + return $regionCode; + } + } + return null; + } + + /** + * Gets the national significant number of the a phone number. Note a national significant number + * doesn't contain a national prefix or any formatting. + * + * @param PhoneNumber $number the phone number for which the national significant number is needed + * @return string the national significant number of the PhoneNumber object passed in + */ + public function getNationalSignificantNumber(PhoneNumber $number): string + { + // If leading zero(s) have been set, we prefix this now. Note this is not a national prefix. + $nationalNumber = ''; + if ($number->isItalianLeadingZero() && $number->getNumberOfLeadingZeros() > 0) { + $zeros = str_repeat('0', $number->getNumberOfLeadingZeros()); + $nationalNumber .= $zeros; + } + $nationalNumber .= $number->getNationalNumber(); + return $nationalNumber; + } + + protected function getNumberTypeHelper(string $nationalNumber, PhoneMetadata $metadata): PhoneNumberType + { + if (!$this->isNumberMatchingDesc($nationalNumber, $metadata->getGeneralDesc())) { + return PhoneNumberType::UNKNOWN; + } + if ($this->isNumberMatchingDesc($nationalNumber, $metadata->getPremiumRate())) { + return PhoneNumberType::PREMIUM_RATE; + } + if ($this->isNumberMatchingDesc($nationalNumber, $metadata->getTollFree())) { + return PhoneNumberType::TOLL_FREE; + } + if ($this->isNumberMatchingDesc($nationalNumber, $metadata->getSharedCost())) { + return PhoneNumberType::SHARED_COST; + } + if ($this->isNumberMatchingDesc($nationalNumber, $metadata->getVoip())) { + return PhoneNumberType::VOIP; + } + if ($this->isNumberMatchingDesc($nationalNumber, $metadata->getPersonalNumber())) { + return PhoneNumberType::PERSONAL_NUMBER; + } + if ($this->isNumberMatchingDesc($nationalNumber, $metadata->getPager())) { + return PhoneNumberType::PAGER; + } + if ($this->isNumberMatchingDesc($nationalNumber, $metadata->getUan())) { + return PhoneNumberType::UAN; + } + if ($this->isNumberMatchingDesc($nationalNumber, $metadata->getVoicemail())) { + return PhoneNumberType::VOICEMAIL; + } + $isFixedLine = $this->isNumberMatchingDesc($nationalNumber, $metadata->getFixedLine()); + if ($isFixedLine) { + if ($metadata->getSameMobileAndFixedLinePattern()) { + return PhoneNumberType::FIXED_LINE_OR_MOBILE; + } + + if ($this->isNumberMatchingDesc($nationalNumber, $metadata->getMobile())) { + return PhoneNumberType::FIXED_LINE_OR_MOBILE; + } + return PhoneNumberType::FIXED_LINE; + } + // Otherwise, test to see if the number is mobile. Only do this if certain that the patterns for + // mobile and fixed line aren't the same. + if (!$metadata->getSameMobileAndFixedLinePattern() && + $this->isNumberMatchingDesc($nationalNumber, $metadata->getMobile()) + ) { + return PhoneNumberType::MOBILE; + } + return PhoneNumberType::UNKNOWN; + } + + protected function isNumberMatchingDesc(string $nationalNumber, ?PhoneNumberDesc $numberDesc): bool + { + if ($numberDesc === null) { + return false; + } + // Check if any possible number lengths are present; if so, we use them to avoid checking the + // validation pattern if they don't match. If they are absent, this means they match the general + // description, which we have already checked before checking a specific number type. + $actualLength = mb_strlen($nationalNumber); + $possibleLengths = $numberDesc->getPossibleLength(); + if (count($possibleLengths) > 0 && !in_array($actualLength, $possibleLengths, true)) { + return false; + } + + return $this->matcherAPI->matchNationalNumber($nationalNumber, $numberDesc, false); + } + + /** + * isNumberGeographical(PhoneNumber) + * + * Tests whether a phone number has a geographical association. It checks if the number is + * associated with a certain region in the country to which it belongs. Note that this doesn't + * verify if the number is actually in use. + * + * isNumberGeographical(PhoneNumberType, $countryCallingCode) + * + * Tests whether a phone number has a geographical association, as represented by its type and the + * country it belongs to. + * + * This version exists since calculating the phone number type is expensive; if we have already + * done this, we don't want to do it again. + * + * @param PhoneNumber|PhoneNumberType $phoneNumberObjOrType A PhoneNumber object, or a PhoneNumberType integer + * @param int|null $countryCallingCode Used when passing a PhoneNumberType + */ + public function isNumberGeographical(PhoneNumber|PhoneNumberType $phoneNumberObjOrType, ?int $countryCallingCode = null): bool + { + if ($phoneNumberObjOrType instanceof PhoneNumber) { + return $this->isNumberGeographical($this->getNumberType($phoneNumberObjOrType), $phoneNumberObjOrType->getCountryCode()); + } + + return $phoneNumberObjOrType === PhoneNumberType::FIXED_LINE + || $phoneNumberObjOrType === PhoneNumberType::FIXED_LINE_OR_MOBILE + || (in_array($countryCallingCode, static::GEO_MOBILE_COUNTRIES, true) + && $phoneNumberObjOrType === PhoneNumberType::MOBILE); + } + + /** + * Gets the type of a valid phone number. + * + * @param PhoneNumber $number the number the phone number that we want to know the type + * @return PhoneNumberType the type of the phone number, or PhoneNumberType::UNKNOWN if it is invalid + */ + public function getNumberType(PhoneNumber $number): PhoneNumberType + { + $regionCode = $this->getRegionCodeForNumber($number); + $metadata = $this->getMetadataForRegionOrCallingCode($number->getCountryCode(), $regionCode); + if ($metadata === null) { + return PhoneNumberType::UNKNOWN; + } + $nationalSignificantNumber = $this->getNationalSignificantNumber($number); + return $this->getNumberTypeHelper($nationalSignificantNumber, $metadata); + } + + protected function getMetadataForRegionOrCallingCode(int $countryCallingCode, ?string $regionCode): ?PhoneMetadata + { + return static::REGION_CODE_FOR_NON_GEO_ENTITY === $regionCode ? + $this->getMetadataForNonGeographicalRegion($countryCallingCode) : $this->getMetadataForRegion($regionCode); + } + + public function getMetadataForNonGeographicalRegion(int $countryCallingCode): ?PhoneMetadata + { + if (!isset($this->countryCallingCodeToRegionCodeMap[$countryCallingCode])) { + return null; + } + return $this->metadataSource->getMetadataForNonGeographicalRegion($countryCallingCode); + } + + /** + * Gets the length of the national destination code (NDC) from the PhoneNumber object passed in, + * so that clients could use it to split a national significant number into NDC and subscriber + * number. The NDC of a phone number is normally the first group of digit(s) right after the + * country calling code when the number is formatted in the international format, if there is a + * subscriber number part that follows. + * + * follows. + * + * N.B.: similar to an area code, not all numbers have an NDC! + * + * An example of how this could be used: + * + * + * $phoneUtil = PhoneNumberUtil::getInstance(); + * $number = $phoneUtil->parse("18002530000", "US"); + * $nationalSignificantNumber = $phoneUtil->getNationalSignificantNumber($number); + * + * $nationalDestinationCodeLength = $phoneUtil->getLengthOfNationalDestinationCode($number); + * if ($nationalDestinationCodeLength > 0) { + * $nationalDestinationCode = substr($nationalSignificantNumber, 0, $nationalDestinationCodeLength); + * $subscriberNumber = substr($nationalSignificantNumber, $nationalDestinationCodeLength); + * } else { + * $nationalDestinationCode = ""; + * $subscriberNumber = $nationalSignificantNumber; + * } + * + * + * Refer to the unit tests to see the difference between this function and + * {@see getLengthOfGeographicalAreaCode}. + * + * @param PhoneNumber $number the PhoneNumber object for which clients want to know the length of the NDC. + * @return int the length of NDC of the PhoneNumber object passed in, which could be zero + */ + public function getLengthOfNationalDestinationCode(PhoneNumber $number): int + { + if ($number->hasExtension()) { + // We don't want to alter the proto given to us, but we don't want to include the extension + // when we format it, so we copy it and clear the extension here. + $copiedProto = new PhoneNumber(); + $copiedProto->mergeFrom($number); + $copiedProto->clearExtension(); + } else { + $copiedProto = clone $number; + } + + $nationalSignificantNumber = $this->format($copiedProto, PhoneNumberFormat::INTERNATIONAL); + + $numberGroups = preg_split('/' . static::NON_DIGITS_PATTERN . '/', $nationalSignificantNumber); + + // The pattern will start with "+COUNTRY_CODE " so the first group will always be the empty + // string (before the + symbol) and the second group will be the country calling code. The third + // group will be area code if it is not the last group. + if ($numberGroups === false || count($numberGroups) <= 3) { + return 0; + } + + if ($this->getNumberType($number) === PhoneNumberType::MOBILE) { + // For example Argentinian mobile numbers, when formatted in the international format, are in + // the form of +54 9 NDC XXXX.... As a result, we take the length of the third group (NDC) and + // add the length of the second group (which is the mobile token), which also forms part of + // the national significant number. This assumes that the mobile token is always formatted + // separately from the rest of the phone number. + + $mobileToken = static::getCountryMobileToken($number->getCountryCode()); + if ($mobileToken !== '') { + return mb_strlen($numberGroups[2]) + mb_strlen($numberGroups[3]); + } + } + return mb_strlen($numberGroups[2]); + } + + /** + * Formats a phone number in the specified format using default rules. Note that this does not + * promise to produce a phone number that the user can dial from where they are - although we do + * format in either 'national' or 'international' format depending on what the client asks for, we + * do not currently support a more abbreviated format, such as for users in the same "area" who + * could potentially dial the number without area code. Note that if the phone number has a + * country calling code of 0 or an otherwise invalid country calling code, we cannot work out + * which formatting rules to apply so we return the national significant number with no formatting + * applied. + */ + public function format(PhoneNumber $number, PhoneNumberFormat $numberFormat): string + { + if ($number->getNationalNumber() === '0' && $number->hasRawInput()) { + // Unparseable numbers that kept their raw input just use that. + // This is the only case where a number can be formatted as E164 without a + // leading '+' symbol (but the original number wasn't parseable anyway). + // TODO: Consider removing the 'if' above so that unparseable + // strings without raw input format to the empty string instead of "+00" + $rawInput = $number->getRawInput(); + if ($rawInput !== '') { + return $rawInput; + } + } + + $formattedNumber = ''; + $countryCallingCode = $number->getCountryCode(); + $nationalSignificantNumber = $this->getNationalSignificantNumber($number); + + if ($countryCallingCode === null) { + return $nationalSignificantNumber; + } + + if ($numberFormat === PhoneNumberFormat::E164) { + // Early exit for E164 case (even if the country calling code is invalid) since no formatting + // of the national number needs to be applied. Extensions are not formatted. + $formattedNumber .= $nationalSignificantNumber; + $this->prefixNumberWithCountryCallingCode($countryCallingCode, PhoneNumberFormat::E164, $formattedNumber); + return $formattedNumber; + } + + if (!$this->hasValidCountryCallingCode($countryCallingCode)) { + $formattedNumber .= $nationalSignificantNumber; + return $formattedNumber; + } + + // Note getRegionCodeForCountryCode() is used because formatting information for regions which + // share a country calling code is contained by only one region for performance reasons. For + // example, for NANPA regions it will be contained in the metadata for US. + $regionCode = $this->getRegionCodeForCountryCode($countryCallingCode); + // Metadata cannot be null because the country calling code is valid (which means that the + // region code cannot be ZZ and must be one of our supported region codes). + $metadata = $this->getMetadataForRegionOrCallingCode($countryCallingCode, $regionCode); + $formattedNumber .= $this->formatNsn($nationalSignificantNumber, $metadata, $numberFormat); + $this->maybeAppendFormattedExtension($number, $metadata, $numberFormat, $formattedNumber); + $this->prefixNumberWithCountryCallingCode($countryCallingCode, $numberFormat, $formattedNumber); + return $formattedNumber; + } + + /** + * A helper function that is used by format and formatByPattern. + */ + protected function prefixNumberWithCountryCallingCode(int $countryCallingCode, PhoneNumberFormat $numberFormat, string &$formattedNumber): void + { + switch ($numberFormat) { + case PhoneNumberFormat::E164: + $formattedNumber = static::PLUS_SIGN . $countryCallingCode . $formattedNumber; + return; + case PhoneNumberFormat::INTERNATIONAL: + $formattedNumber = static::PLUS_SIGN . $countryCallingCode . ' ' . $formattedNumber; + return; + case PhoneNumberFormat::RFC3966: + $formattedNumber = static::RFC3966_PREFIX . static::PLUS_SIGN . $countryCallingCode . '-' . $formattedNumber; + return; + case PhoneNumberFormat::NATIONAL: + return; + } + } + + /** + * Helper function to check the country calling code is valid. + */ + protected function hasValidCountryCallingCode(int $countryCallingCode): bool + { + return isset($this->countryCallingCodeToRegionCodeMap[$countryCallingCode]); + } + + /** + * Returns the region code that matches the specific country calling code. In the case of no + * region code being found, ZZ will be returned. In the case of multiple regions, the one + * designated in the metadata as the "main" region for this calling code will be returned. If the + * countryCallingCode entered is valid but doesn't match a specific region (such as in the case of + * non-geographical calling codes like 800) the value "001" will be returned (corresponding to + * the value for World in the UN M.49 schema). + */ + public function getRegionCodeForCountryCode(int $countryCallingCode): string + { + $regionCodes = $this->countryCallingCodeToRegionCodeMap[$countryCallingCode] ?? null; + return $regionCodes === null ? static::UNKNOWN_REGION : $regionCodes[0]; + } + + /** + * Note in some regions, the national number can be written in two completely different ways + * depending on whether it forms part of the NATIONAL format or INTERNATIONAL format. The + * numberFormat parameter here is used to specify which format to use for those cases. If a + * carrierCode is specified, this will be inserted into the formatted string to replace $CC. + */ + protected function formatNsn(string $number, PhoneMetadata $metadata, PhoneNumberFormat $numberFormat, ?string $carrierCode = null): string + { + $intlNumberFormats = $metadata->intlNumberFormats(); + // When the intlNumberFormats exists, we use that to format national number for the + // INTERNATIONAL format instead of using the numberDesc.numberFormats. + $availableFormats = (count($intlNumberFormats) === 0 || $numberFormat === PhoneNumberFormat::NATIONAL) + ? $metadata->numberFormats() + : $metadata->intlNumberFormats(); + $formattingPattern = $this->chooseFormattingPatternForNumber($availableFormats, $number); + return ($formattingPattern === null) + ? $number + : $this->formatNsnUsingPattern($number, $formattingPattern, $numberFormat, $carrierCode); + } + + /** + * @param NumberFormat[] $availableFormats + */ + public function chooseFormattingPatternForNumber(array $availableFormats, string $nationalNumber): ?NumberFormat + { + foreach ($availableFormats as $numFormat) { + $leadingDigitsPatternMatcher = null; + $size = $numFormat->leadingDigitsPatternSize(); + // We always use the last leading_digits_pattern, as it is the most detailed. + if ($size > 0) { + $leadingDigitsPatternMatcher = new Matcher( + $numFormat->getLeadingDigitsPattern($size - 1), + $nationalNumber + ); + } + if ($size === 0 || $leadingDigitsPatternMatcher->lookingAt()) { + $m = new Matcher($numFormat->getPattern(), $nationalNumber); + if ($m->matches() > 0) { + return $numFormat; + } + } + } + return null; + } + + /** + * Note that carrierCode is optional - if null or an empty string, no carrier code replacement + * will take place. + */ + public function formatNsnUsingPattern( + string $nationalNumber, + NumberFormat $formattingPattern, + PhoneNumberFormat $numberFormat, + ?string $carrierCode = null, + ): string { + $numberFormatRule = $formattingPattern->getFormat(); + $m = new Matcher($formattingPattern->getPattern(), $nationalNumber); + if ($numberFormat === PhoneNumberFormat::NATIONAL && + $carrierCode !== null && $carrierCode !== '' && + $formattingPattern->getDomesticCarrierCodeFormattingRule() !== '' + ) { + // Replace the $CC in the formatting rule with the desired carrier code. + $carrierCodeFormattingRule = $formattingPattern->getDomesticCarrierCodeFormattingRule(); + $carrierCodeFormattingRule = str_replace(static::CC_STRING, $carrierCode, $carrierCodeFormattingRule); + // Now replace the $FG in the formatting rule with the first group and the carrier code + // combined in the appropriate way. + $firstGroupMatcher = new Matcher(static::FIRST_GROUP_PATTERN, $numberFormatRule); + $numberFormatRule = $firstGroupMatcher->replaceFirst($carrierCodeFormattingRule); + $formattedNationalNumber = $m->replaceAll($numberFormatRule); + } else { + // Use the national prefix formatting rule instead. + $nationalPrefixFormattingRule = $formattingPattern->getNationalPrefixFormattingRule(); + if ($numberFormat === PhoneNumberFormat::NATIONAL && $nationalPrefixFormattingRule !== '') { + $firstGroupMatcher = new Matcher(static::FIRST_GROUP_PATTERN, $numberFormatRule); + $formattedNationalNumber = $m->replaceAll( + $firstGroupMatcher->replaceFirst($nationalPrefixFormattingRule) + ); + } else { + $formattedNationalNumber = $m->replaceAll($numberFormatRule); + } + } + if ($numberFormat === PhoneNumberFormat::RFC3966) { + // Strip any leading punctuation. + $matcher = new Matcher(static::SEPARATOR_PATTERN, $formattedNationalNumber); + if ($matcher->lookingAt()) { + $formattedNationalNumber = $matcher->replaceFirst(''); + } + // Replace the rest with a dash between each number group. + $formattedNationalNumber = $matcher->reset($formattedNationalNumber)->replaceAll('-'); + } + return $formattedNationalNumber; + } + + /** + * Appends the formatted extension of a phone number to formattedNumber, if the phone number had + * an extension specified. + */ + protected function maybeAppendFormattedExtension(PhoneNumber $number, ?PhoneMetadata $metadata, PhoneNumberFormat $numberFormat, string &$formattedNumber): void + { + if ($number->hasExtension() && $number->getExtension() !== '') { + if ($numberFormat === PhoneNumberFormat::RFC3966) { + $formattedNumber .= static::RFC3966_EXTN_PREFIX . $number->getExtension(); + } elseif ($metadata !== null && $metadata->hasPreferredExtnPrefix()) { + $formattedNumber .= $metadata->getPreferredExtnPrefix() . $number->getExtension(); + } else { + $formattedNumber .= static::DEFAULT_EXTN_PREFIX . $number->getExtension(); + } + } + } + + /** + * Returns the mobile token for the provided country calling code if it has one, otherwise + * returns an empty string. A mobile token is a number inserted before the area code when dialing + * a mobile number from that country from abroad. + * + * @param int $countryCallingCode the country calling code for which we want the mobile token + * @return string the mobile token, as a string, for the given country calling code + */ + public static function getCountryMobileToken(int $countryCallingCode): string + { + return static::MOBILE_TOKEN_MAPPINGS[$countryCallingCode] ?? ''; + } + + /** + * Checks if the number is a valid vanity (alpha) number such as 800 MICROSOFT. A valid vanity + * number will start with at least 3 digits and will have three or more alpha characters. This + * does not do region-specific checks - to work out if this number is actually valid for a region, + * it should be parsed and methods such as {@see isPossibleNumberWithReason} and + * {@see isValidNumber} should be used. + * + * @param string $number the number that needs to be checked + * @return bool true if the number is a valid vanity number + */ + public function isAlphaNumber(string $number): bool + { + if (!static::isViablePhoneNumber($number)) { + // Number is too short, or doesn't match the basic phone number pattern. + return false; + } + $this->maybeStripExtension($number); + return preg_match('/' . static::VALID_ALPHA_PHONE_PATTERN . '/' . static::REGEX_FLAGS, $number) === 1; + } + + /** + * Checks to see if the string of characters could possibly be a phone number at all. At the + * moment, checks to see that the string begins with at least 2 digits, ignoring any punctuation + * commonly found in phone numbers. + * This method does not require the number to be normalized in advance - but does assume that + * leading non-number symbols have been removed, such as by the method extractPossibleNumber. + * + * @param string $number to be checked for viability as a phone number + * @return bool true if the number could be a phone number of some sort, otherwise false + */ + public static function isViablePhoneNumber(string $number): bool + { + if (!isset(static::$VALID_PHONE_NUMBER_PATTERN)) { + static::initValidPhoneNumberPatterns(); + } + + if (mb_strlen($number) < static::MIN_LENGTH_FOR_NSN) { + return false; + } + + $validPhoneNumberPattern = static::getValidPhoneNumberPattern(); + + $m = preg_match($validPhoneNumberPattern, $number); + return $m > 0; + } + + /** + * We append optionally the extension pattern to the end here, as a valid phone number may + * have an extension prefix appended, followed by 1 or more digits. + */ + protected static function getValidPhoneNumberPattern(): string + { + return static::$VALID_PHONE_NUMBER_PATTERN; + } + + /** + * Strips any extension (as in, the part of the number dialled after the call is connected, + * usually indicated with extn, ext, x or similar) from the end of the number, and returns it. + * + * @param string $number the non-normalized telephone number that we wish to strip the extension from + * @return string the phone extension + */ + protected function maybeStripExtension(string &$number): string + { + $matches = []; + $find = preg_match(static::$EXTN_PATTERN, $number, $matches, PREG_OFFSET_CAPTURE); + // If we find a potential extension, and the number preceding this is a viable number, we assume + // it is an extension. + if ($find > 0 && static::isViablePhoneNumber(substr($number, 0, $matches[0][1]))) { + // The numbers are captured into groups in the regular expression. + + for ($i = 1, $length = count($matches); $i <= $length; $i++) { + if ($matches[$i][0] !== '') { + // We go through the capturing groups until we find one that captured some digits. If none + // did, then we will return the empty string. + $extension = $matches[$i][0]; + $number = substr($number, 0, $matches[0][1]); + return $extension; + } + } + } + return ''; + } + + /** + * Parses a string and returns it in proto buffer format. This method differs from {@see parse} + * in that it always populates the raw_input field of the protocol buffer with numberToParse as + * well as the country_code_source field. + * + * @param string $numberToParse number that we are attempting to parse. This can contain formatting + * such as +, ( and -, as well as a phone number extension. It can also + * be provided in RFC3966 format. + * @param string|null $defaultRegion region that we are expecting the number to be from. This is only used + * if the number being parsed is not written in international format. + * The country calling code for the number in this case would be stored + * as that of the default region supplied. + * @return PhoneNumber a phone number proto buffer filled with the parsed number + */ + public function parseAndKeepRawInput(string $numberToParse, ?string $defaultRegion, ?PhoneNumber $phoneNumber = null): PhoneNumber + { + if ($phoneNumber === null) { + $phoneNumber = new PhoneNumber(); + } + $this->parseHelper($numberToParse, $defaultRegion, true, true, $phoneNumber); + return $phoneNumber; + } + + /** + * A helper function to set the values related to leading zeros in a PhoneNumber. + */ + public static function setItalianLeadingZerosForPhoneNumber(string $nationalNumber, PhoneNumber $phoneNumber): void + { + if (strlen($nationalNumber) > 1 && str_starts_with($nationalNumber, '0')) { + $phoneNumber->setItalianLeadingZero(true); + $numberOfLeadingZeros = 1; + // Note that if the national number is all "0"s, the last "0" is not counted as a leading + // zero. + while ($numberOfLeadingZeros < (strlen($nationalNumber) - 1) && + $nationalNumber[$numberOfLeadingZeros] === '0') { + $numberOfLeadingZeros++; + } + + if ($numberOfLeadingZeros !== 1) { + $phoneNumber->setNumberOfLeadingZeros($numberOfLeadingZeros); + } + } + } + + /** + * Parses a string and fills up the phoneNumber. This method is the same as the public + * parse() method, with the exception that it allows the default region to be null, for use by + * isNumberMatch(). checkRegion should be set to false if it is permitted for the default region + * to be null or unknown ("ZZ"). + * @throws NumberParseException + */ + protected function parseHelper(string $numberToParse, ?string $defaultRegion, bool $keepRawInput, bool $checkRegion, PhoneNumber $phoneNumber): void + { + $numberToParse = trim($numberToParse); + + if (mb_strlen($numberToParse) > static::MAX_INPUT_STRING_LENGTH) { + throw new NumberParseException( + NumberParseException::TOO_LONG, + 'The string supplied was too long to parse.' + ); + } + + $nationalNumber = ''; + $this->buildNationalNumberForParsing($numberToParse, $nationalNumber); + + if (!static::isViablePhoneNumber($nationalNumber)) { + throw new NumberParseException( + NumberParseException::NOT_A_NUMBER, + 'The string supplied did not seem to be a phone number.' + ); + } + + // Check the region supplied is valid, or that the extracted number starts with some sort of + + // sign so the number's region can be determined. + if ($checkRegion && !$this->checkRegionForParsing($nationalNumber, $defaultRegion)) { + throw new NumberParseException( + NumberParseException::INVALID_COUNTRY_CODE, + 'Missing or invalid default region.' + ); + } + + if ($keepRawInput) { + $phoneNumber->setRawInput($numberToParse); + } + // Attempt to parse extension first, since it doesn't require region-specific data and we want + // to have the non-normalised number here. + $extension = $this->maybeStripExtension($nationalNumber); + if ($extension !== '') { + $phoneNumber->setExtension($extension); + } + + $regionMetadata = $this->getMetadataForRegion($defaultRegion); + // Check to see if the number is given in international format so we know whether this number is + // from the default region or not. + $normalizedNationalNumber = ''; + try { + // TODO: This method should really just take in the string buffer that has already + // been created, and just remove the prefix, rather than taking in a string and then + // outputting a string buffer. + $countryCode = $this->maybeExtractCountryCode( + $nationalNumber, + $regionMetadata, + $normalizedNationalNumber, + $keepRawInput, + $phoneNumber + ); + } catch (NumberParseException $e) { + $matcher = new Matcher(static::PLUS_CHARS_PATTERN, $nationalNumber); + if ($e->getErrorType() === NumberParseException::INVALID_COUNTRY_CODE && $matcher->lookingAt()) { + // Strip the plus-char, and try again. + $countryCode = $this->maybeExtractCountryCode( + substr($nationalNumber, $matcher->end()), + $regionMetadata, + $normalizedNationalNumber, + $keepRawInput, + $phoneNumber + ); + if ($countryCode === 0) { + throw new NumberParseException( + NumberParseException::INVALID_COUNTRY_CODE, + 'Could not interpret numbers after plus-sign.' + ); + } + } else { + throw new NumberParseException($e->getErrorType(), $e->getMessage(), $e); + } + } + if ($countryCode !== 0) { + $phoneNumberRegion = $this->getRegionCodeForCountryCode($countryCode); + if ($phoneNumberRegion !== $defaultRegion) { + // Metadata cannot be null because the country calling code is valid. + $regionMetadata = $this->getMetadataForRegionOrCallingCode($countryCode, $phoneNumberRegion); + } + } else { + // If no extracted country calling code, use the region supplied instead. The national number + // is just the normalized version of the number we were given to parse. + + $normalizedNationalNumber .= static::normalize($nationalNumber); + if ($defaultRegion !== null) { + $countryCode = $regionMetadata->getCountryCode(); + $phoneNumber->setCountryCode($countryCode); + } elseif ($keepRawInput) { + $phoneNumber->clearCountryCodeSource(); + } + } + if (mb_strlen($normalizedNationalNumber) < static::MIN_LENGTH_FOR_NSN) { + throw new NumberParseException( + NumberParseException::TOO_SHORT_NSN, + 'The string supplied is too short to be a phone number.' + ); + } + if ($regionMetadata !== null) { + $carrierCode = ''; + $potentialNationalNumber = $normalizedNationalNumber; + $this->maybeStripNationalPrefixAndCarrierCode($potentialNationalNumber, $regionMetadata, $carrierCode); + // We require that the NSN remaining after stripping the national prefix and carrier code be + // long enough to be a possible length for the region. Otherwise, we don't do the stripping, + // since the original number could be a valid short number. + $validationResult = $this->testNumberLength($potentialNationalNumber, $regionMetadata); + if ($validationResult !== ValidationResult::TOO_SHORT + && $validationResult !== ValidationResult::IS_POSSIBLE_LOCAL_ONLY + && $validationResult !== ValidationResult::INVALID_LENGTH) { + $normalizedNationalNumber = $potentialNationalNumber; + if ($keepRawInput && $carrierCode !== '') { + $phoneNumber->setPreferredDomesticCarrierCode($carrierCode); + } + } + } + $lengthOfNationalNumber = mb_strlen($normalizedNationalNumber); + if ($lengthOfNationalNumber < static::MIN_LENGTH_FOR_NSN) { + throw new NumberParseException( + NumberParseException::TOO_SHORT_NSN, + 'The string supplied is too short to be a phone number.' + ); + } + if ($lengthOfNationalNumber > static::MAX_LENGTH_FOR_NSN) { + throw new NumberParseException( + NumberParseException::TOO_LONG, + 'The string supplied is too long to be a phone number.' + ); + } + static::setItalianLeadingZerosForPhoneNumber($normalizedNationalNumber, $phoneNumber); + + /* + * We have to store the National Number as a string instead of a "long" as Google do + * + * Since PHP doesn't always support 64 bit INTs, this was a float, but that had issues + * with long numbers. + * + * We have to remove the leading zeroes ourself though + */ + if ((int) $normalizedNationalNumber === 0) { + $normalizedNationalNumber = '0'; + } else { + $normalizedNationalNumber = ltrim($normalizedNationalNumber, '0'); + } + + $phoneNumber->setNationalNumber($normalizedNationalNumber); + } + + /** + * Extracts the value of the phone-context parameter of numberToExtractFrom where the index of + * ";phone-context=" is the parameter indexOfPhoneContext, following the syntax defined in + * RFC3966. + * + * @return string|null the extracted string (possibly empty), or null if no phone-context parameter is found. + */ + protected function extractPhoneContext(string $numberToExtractFrom, false|int $indexOfPhoneContext): ?string + { + // If no phone-context parameter is present + if ($indexOfPhoneContext === false) { + return null; + } + + $phoneContextStart = $indexOfPhoneContext + strlen(static::RFC3966_PHONE_CONTEXT); + // If phone-context parameter is empty + if ($phoneContextStart >= mb_strlen($numberToExtractFrom)) { + return ''; + } + + $phoneContextEnd = strpos($numberToExtractFrom, ';', $phoneContextStart); + // If phone-context is not the last parameter + if ($phoneContextEnd !== false) { + return substr($numberToExtractFrom, $phoneContextStart, $phoneContextEnd - $phoneContextStart); + } + + return substr($numberToExtractFrom, $phoneContextStart); + } + + /** + * Returns whether the value of phoneContext follows the syntax defined in RFC3966. + */ + protected function isPhoneContextValid(?string $phoneContext): bool + { + if ($phoneContext === null) { + return true; + } + + if ($phoneContext === '') { + return false; + } + + $numberDigitsPattern = '/' . static::$RFC3966_GLOBAL_NUMBER_DIGITS . '/' . static::REGEX_FLAGS; + $domainNamePattern = '/' . static::$RFC3966_DOMAINNAME . '/' . static::REGEX_FLAGS; + + // Does phone-context value match pattern of global-number-digits or domainname + return preg_match($numberDigitsPattern, $phoneContext) === 1 || preg_match($domainNamePattern, $phoneContext) === 1; + } + + /** + * Returns a new phone number containing only the fields needed to uniquely identify a phone + * number, rather than any fields that capture the context in which the phone number was created. + * These fields correspond to those set in parse() rather than parseAndKeepRawInput() + */ + protected static function copyCoreFieldsOnly(PhoneNumber $phoneNumberIn): PhoneNumber + { + $phoneNumber = new PhoneNumber(); + $phoneNumber->setCountryCode($phoneNumberIn->getCountryCode()); + $phoneNumber->setNationalNumber($phoneNumberIn->getNationalNumber()); + if ($phoneNumberIn->hasExtension()) { + $phoneNumber->setExtension($phoneNumberIn->getExtension()); + } + if ($phoneNumberIn->isItalianLeadingZero()) { + $phoneNumber->setItalianLeadingZero(true); + // This field is only relevant if there are leading zeros at all. + $phoneNumber->setNumberOfLeadingZeros($phoneNumberIn->getNumberOfLeadingZeros()); + } + return $phoneNumber; + } + + /** + * Converts numberToParse to a form that we can parse and write it to nationalNumber if it is + * written in RFC3966; otherwise extract a possible number out of it and write to nationalNumber. + * @throws NumberParseException + */ + protected function buildNationalNumberForParsing(string $numberToParse, string &$nationalNumber): void + { + $indexOfPhoneContext = strpos($numberToParse, static::RFC3966_PHONE_CONTEXT); + $phoneContext = $this->extractPhoneContext($numberToParse, $indexOfPhoneContext); + + if (!$this->isPhoneContextValid($phoneContext)) { + throw new NumberParseException(NumberParseException::NOT_A_NUMBER, 'The phone-context value is invalid.'); + } + + if ($phoneContext !== null) { + // If the phone context contains a phone number prefix, we need to capture it, whereas domains + // will be ignored. + if (str_starts_with($phoneContext, self::PLUS_SIGN)) { + // Additional parameters might follow the phone context. If so, we will remove them here + // because the parameters after phone context are not important for parsing the phone + // number. + $nationalNumber .= $phoneContext; + } + + // Now append everything between the "tel:" prefix and the phone-context. This should include + // the national number, an optional extension or isdn-subaddress component. Note we also + // handle the case when "tel:" is missing, as we have seen in some of the phone number inputs. + // In that case, we append everything from the beginning. + $indexOfRfc3966Prefix = strpos($numberToParse, static::RFC3966_PREFIX); + $indexOfNationalNumber = ($indexOfRfc3966Prefix !== false) ? $indexOfRfc3966Prefix + strlen(static::RFC3966_PREFIX) : 0; + $nationalNumber .= substr( + $numberToParse, + $indexOfNationalNumber, + (int) $indexOfPhoneContext - $indexOfNationalNumber + ); + } else { + // Extract a possible number from the string passed in (this strips leading characters that + // could not be the start of a phone number.) + $nationalNumber .= static::extractPossibleNumber($numberToParse); + } + + // Delete the isdn-subaddress and everything after it if it is present. Note extension won't + // appear at the same time with isdn-subaddress according to paragraph 5.3 of the RFC3966 spec, + $indexOfIsdn = strpos($nationalNumber, static::RFC3966_ISDN_SUBADDRESS); + if ($indexOfIsdn > 0) { + $nationalNumber = substr($nationalNumber, 0, $indexOfIsdn); + } + // If both phone context and isdn-subaddress are absent but other parameters are present, the + // parameters are left in nationalNumber. This is because we are concerned about deleting + // content from a potential number string when there is no strong evidence that the number is + // actually written in RFC3966. + } + + /** + * Attempts to extract a possible number from the string passed in. This currently strips all + * leading characters that cannot be used to start a phone number. Characters that can be used to + * start a phone number are defined in the VALID_START_CHAR_PATTERN. If none of these characters + * are found in the number passed in, an empty string is returned. This function also attempts to + * strip off any alternative extensions or endings if two or more are present, such as in the case + * of: (530) 583-6985 x302/x2303. The second extension here makes this actually two phone numbers, + * (530) 583-6985 x302 and (530) 583-6985 x2303. We remove the second extension so that the first + * number is parsed correctly. + * + * @param string $number the string that might contain a phone number + * @return string the number, stripped of any non-phone-number prefix (such as "Tel:") or an empty + * string if no character used to start phone numbers (such as + or any digit) is + * found in the number + */ + public static function extractPossibleNumber(string $number): string + { + $matches = []; + $match = preg_match('/' . static::VALID_START_CHAR_PATTERN . '/ui', $number, $matches, PREG_OFFSET_CAPTURE); + if ($match > 0) { + $number = substr($number, $matches[0][1]); + // Remove trailing non-alpha non-numerical characters. + $trailingCharsMatcher = new Matcher(static::UNWANTED_END_CHAR_PATTERN, $number); + if ($trailingCharsMatcher->find() && $trailingCharsMatcher->start() > 0) { + $number = substr($number, 0, $trailingCharsMatcher->start()); + } + + // Check for extra numbers at the end. + $match = preg_match('%' . static::SECOND_NUMBER_START_PATTERN . '%', $number, $matches, PREG_OFFSET_CAPTURE); + if ($match > 0) { + $number = substr($number, 0, $matches[0][1]); + } + + return $number; + } + + return ''; + } + + /** + * Checks to see that the region code used is valid, or if it is not valid, that the number to + * parse starts with a + symbol so that we can attempt to infer the region from the number. + * Returns false if it cannot use the region provided and the region cannot be inferred. + */ + protected function checkRegionForParsing(string $numberToParse, ?string $defaultRegion): bool + { + if (!$this->isValidRegionCode($defaultRegion)) { + // If the number is null or empty, we can't infer the region. + $plusCharsPatternMatcher = new Matcher(static::PLUS_CHARS_PATTERN, $numberToParse); + if ($numberToParse === '' || !$plusCharsPatternMatcher->lookingAt()) { + return false; + } + } + return true; + } + + /** + * Tries to extract a country calling code from a number. This method will return zero if no + * country calling code is considered to be present. Country calling codes are extracted in the + * following ways: + *
    + *
  • by stripping the international dialing prefix of the region the person is dialing from, + * if this is present in the number, and looking at the next digits + *
  • by stripping the '+' sign if present and then looking at the next digits + *
  • by comparing the start of the number and the country calling code of the default region. + * If the number is not considered possible for the numbering plan of the default region + * initially, but starts with the country calling code of this region, validation will be + * reattempted after stripping this country calling code. If this number is considered a + * possible number, then the first digits will be considered the country calling code and + * removed as such. + *
+ * It will throw a NumberParseException if the number starts with a '+' but the country calling + * code supplied after this does not match that of any known region. + * + * @param string $number non-normalized telephone number that we wish to extract a country calling + * code from - may begin with '+' + * @param PhoneMetadata|null $defaultRegionMetadata metadata about the region this number may be from + * @param string $nationalNumber a string buffer to store the national significant number in, in the case + * that a country calling code was extracted. The number is appended to any existing contents. + * If no country calling code was extracted, this will be left unchanged. + * @param bool $keepRawInput true if the country_code_source and preferred_carrier_code fields of + * phoneNumber should be populated. + * @param PhoneNumber $phoneNumber the PhoneNumber object where the country_code and country_code_source need + * to be populated. Note the country_code is always populated, whereas country_code_source is + * only populated when keepCountryCodeSource is true. + * @return int the country calling code extracted or 0 if none could be extracted + * @throws NumberParseException + */ + public function maybeExtractCountryCode( + string $number, + ?PhoneMetadata $defaultRegionMetadata, + string &$nationalNumber, + bool $keepRawInput, + PhoneNumber $phoneNumber + ): int { + if ($number === '') { + return 0; + } + $fullNumber = $number; + // Set the default prefix to be something that will never match. + $possibleCountryIddPrefix = 'NonMatch'; + if ($defaultRegionMetadata !== null) { + $possibleCountryIddPrefix = $defaultRegionMetadata->getInternationalPrefix(); + } + $countryCodeSource = $this->maybeStripInternationalPrefixAndNormalize($fullNumber, $possibleCountryIddPrefix); + + if ($keepRawInput) { + $phoneNumber->setCountryCodeSource($countryCodeSource); + } + if ($countryCodeSource !== CountryCodeSource::FROM_DEFAULT_COUNTRY) { + if (mb_strlen($fullNumber) <= static::MIN_LENGTH_FOR_NSN) { + throw new NumberParseException( + NumberParseException::TOO_SHORT_AFTER_IDD, + 'Phone number had an IDD, but after this was not long enough to be a viable phone number.' + ); + } + $potentialCountryCode = $this->extractCountryCode($fullNumber, $nationalNumber); + + if ($potentialCountryCode !== 0) { + $phoneNumber->setCountryCode($potentialCountryCode); + return $potentialCountryCode; + } + + // If this fails, they must be using a strange country calling code that we don't recognize, + // or that doesn't exist. + throw new NumberParseException( + NumberParseException::INVALID_COUNTRY_CODE, + 'Country calling code supplied was not recognised.' + ); + } + + if ($defaultRegionMetadata !== null) { + // Check to see if the number starts with the country calling code for the default region. If + // so, we remove the country calling code, and do some checks on the validity of the number + // before and after. + $defaultCountryCode = $defaultRegionMetadata->getCountryCode(); + $defaultCountryCodeString = (string) $defaultCountryCode; + $normalizedNumber = $fullNumber; + if (str_starts_with($normalizedNumber, $defaultCountryCodeString)) { + $potentialNationalNumber = substr($normalizedNumber, mb_strlen($defaultCountryCodeString)); + $generalDesc = $defaultRegionMetadata->getGeneralDesc(); + // Don't need the carrier code. + $carrierCode = ''; + $this->maybeStripNationalPrefixAndCarrierCode( + $potentialNationalNumber, + $defaultRegionMetadata, + $carrierCode + ); + // If the number was not valid before but is valid now, or if it was too long before, we + // consider the number with the country calling code stripped to be a better result and + // keep that instead. + if ((!$this->matcherAPI->matchNationalNumber($fullNumber, $generalDesc, false) + && $this->matcherAPI->matchNationalNumber($potentialNationalNumber, $generalDesc, false)) + || $this->testNumberLength($fullNumber, $defaultRegionMetadata) === ValidationResult::TOO_LONG + ) { + $nationalNumber .= $potentialNationalNumber; + if ($keepRawInput) { + $phoneNumber->setCountryCodeSource(CountryCodeSource::FROM_NUMBER_WITHOUT_PLUS_SIGN); + } + $phoneNumber->setCountryCode($defaultCountryCode); + return $defaultCountryCode; + } + } + } + // No country calling code present. + $phoneNumber->setCountryCode(0); + return 0; + } + + /** + * Strips any international prefix (such as +, 00, 011) present in the number provided, normalizes + * the resulting number, and indicates if an international prefix was present. + * + * @param string $number the non-normalized telephone number that we wish to strip any international + * dialing prefix from. + * @param string $possibleIddPrefix string the international direct dialing prefix from the region we + * think this number may be dialed in + * @return CountryCodeSource the corresponding CountryCodeSource if an international dialing prefix could be + * removed from the number, otherwise CountryCodeSource::FROM_DEFAULT_COUNTRY if the number did + * not seem to be in international format. + */ + public function maybeStripInternationalPrefixAndNormalize(string &$number, string $possibleIddPrefix): CountryCodeSource + { + if ($number === '') { + return CountryCodeSource::FROM_DEFAULT_COUNTRY; + } + $matches = []; + // Check to see if the number begins with one or more plus signs. + $match = preg_match('/^' . static::PLUS_CHARS_PATTERN . '/' . static::REGEX_FLAGS, $number, $matches, PREG_OFFSET_CAPTURE); + if ($match > 0) { + $number = mb_substr($number, $matches[0][1] + mb_strlen($matches[0][0])); + // Can now normalize the rest of the number since we've consumed the "+" sign at the start. + $number = static::normalize($number); + return CountryCodeSource::FROM_NUMBER_WITH_PLUS_SIGN; + } + // Attempt to parse the first digits as an international prefix. + $iddPattern = $possibleIddPrefix; + $number = static::normalize($number); + return $this->parsePrefixAsIdd($iddPattern, $number) + ? CountryCodeSource::FROM_NUMBER_WITH_IDD + : CountryCodeSource::FROM_DEFAULT_COUNTRY; + } + + /** + * Normalizes a string of characters representing a phone number. This performs + * the following conversions: + * Punctuation is stripped. + * For ALPHA/VANITY numbers: + * Letters are converted to their numeric representation on a telephone + * keypad. The keypad used here is the one defined in ITU Recommendation + * E.161. This is only done if there are 3 or more letters in the number, + * to lessen the risk that such letters are typos. + * For other numbers: + * - Wide-ascii digits are converted to normal ASCII (European) digits. + * - Arabic-Indic numerals are converted to European numerals. + * - Spurious alpha characters are stripped. + * + * @param string $number a string of characters representing a phone number. + * @return string the normalized string version of the phone number. + */ + public static function normalize(string $number): string + { + $m = new Matcher(static::VALID_ALPHA_PHONE_PATTERN, $number); + if ($m->matches()) { + return static::normalizeHelper($number, static::ALPHA_PHONE_MAPPINGS, true); + } + + return static::normalizeDigitsOnly($number); + } + + /** + * Normalizes a string of characters representing a phone number. This converts wide-ascii and + * arabic-indic numerals to European numerals, and strips punctuation and alpha characters. + * + * @param $number string a string of characters representing a phone number + * @return string the normalized string version of the phone number + */ + public static function normalizeDigitsOnly(string $number): string + { + return static::normalizeDigits($number, false /* strip non-digits */); + } + + public static function normalizeDigits(string $number, bool $keepNonDigits): string + { + $normalizedDigits = ''; + $numberAsArray = preg_split('/(?lookingAt()) { + $matchEnd = $m->end(); + // Only strip this if the first digit after the match is not a 0, since country calling codes + // cannot begin with 0. + $digitMatcher = new Matcher(static::CAPTURING_DIGIT_PATTERN, substr($number, $matchEnd)); + if ($digitMatcher->find()) { + $normalizedGroup = static::normalizeDigitsOnly($digitMatcher->group(1)); + if ($normalizedGroup === '0') { + return false; + } + } + $number = substr($number, $matchEnd); + return true; + } + return false; + } + + /** + * Extracts country calling code from fullNumber, returns it and places the remaining number in nationalNumber. + * It assumes that the leading plus sign or IDD has already been removed. + * Returns 0 if fullNumber doesn't start with a valid country calling code, and leaves nationalNumber unmodified. + * @internal + */ + public function extractCountryCode(string $fullNumber, string &$nationalNumber): int + { + if (($fullNumber === '') || ($fullNumber[0] === '0')) { + // Country codes do not begin with a '0'. + return 0; + } + $numberLength = mb_strlen($fullNumber); + for ($i = 1; $i <= static::MAX_LENGTH_COUNTRY_CODE && $i <= $numberLength; $i++) { + $potentialCountryCode = (int) substr($fullNumber, 0, $i); + if (isset($this->countryCallingCodeToRegionCodeMap[$potentialCountryCode])) { + $nationalNumber .= substr($fullNumber, $i); + return $potentialCountryCode; + } + } + return 0; + } + + /** + * Strips any national prefix (such as 0, 1) present in the number provided. + * + * @param string $number the normalized telephone number that we wish to strip any national + * dialing prefix from + * @param PhoneMetadata $metadata the metadata for the region that we think this number is from + * @param string $carrierCode a place to insert the carrier code if one is extracted + * @return bool true if a national prefix or carrier code (or both) could be extracted. + */ + public function maybeStripNationalPrefixAndCarrierCode(string &$number, PhoneMetadata $metadata, string &$carrierCode): bool + { + $possibleNationalPrefix = $metadata->getNationalPrefixForParsing(); + if ($number === '' || $possibleNationalPrefix === null || $possibleNationalPrefix === '') { + // Early return for numbers of zero length. + return false; + } + + // Attempt to parse the first digits as a national prefix. + $prefixMatcher = new Matcher($possibleNationalPrefix, $number); + if ($prefixMatcher->lookingAt()) { + $generalDesc = $metadata->getGeneralDesc(); + // Check if the original number is viable. + $isViableOriginalNumber = $this->matcherAPI->matchNationalNumber($number, $generalDesc, false); + // $prefixMatcher->group($numOfGroups) === null implies nothing was captured by the capturing + // groups in $possibleNationalPrefix; therefore, no transformation is necessary, and we just + // remove the national prefix + $numOfGroups = $prefixMatcher->groupCount(); + $transformRule = $metadata->getNationalPrefixTransformRule(); + if ($transformRule === null + || $transformRule === '' + || $prefixMatcher->group($numOfGroups - 1) === null + ) { + // If the original number was viable, and the resultant number is not, we return. + if ($isViableOriginalNumber && + !$this->matcherAPI->matchNationalNumber( + substr($number, $prefixMatcher->end()), + $generalDesc, + false + )) { + return false; + } + if ($numOfGroups > 0 && $prefixMatcher->group($numOfGroups) !== null) { + $carrierCode .= $prefixMatcher->group(1); + } + + $number = substr($number, $prefixMatcher->end()); + return true; + } + + // Check that the resultant number is still viable. If not, return. Check this by copying + // the string and making the transformation on the copy first. + $transformedNumber = $number; + $numberLength = mb_strlen($number); + $transformedNumber = substr_replace( + $transformedNumber, + $prefixMatcher->replaceFirst($transformRule), + 0, + $numberLength + ); + if ($isViableOriginalNumber + && !$this->matcherAPI->matchNationalNumber($transformedNumber, $generalDesc, false)) { + return false; + } + if ($numOfGroups > 1) { + $carrierCode .= $prefixMatcher->group(1); + } + $number = substr_replace($number, $transformedNumber, 0, mb_strlen($number)); + return true; + } + return false; + } + + /** + * Convenience wrapper around isPossibleNumberForTypeWithReason. Instead of returning the reason + * for failure, this method returns true if the number is either a possible fully-qualified + * number (containing the area code and country code), or if the number could be a possible local + * number (with a country code, but missing an area code). Local numbers are considered possible + * if they could be possibly dialled in this format: if the area code is needed for a call to + * connect, the number is not considered possible without it. + * + * @param PhoneNumber $number The number that needs to be checked + * @param PhoneNumberType $type PhoneNumberType The type we are interested in + * @return bool true if the number is possible for this particular type + */ + public function isPossibleNumberForType(PhoneNumber $number, PhoneNumberType $type): bool + { + $result = $this->isPossibleNumberForTypeWithReason($number, $type); + return $result === ValidationResult::IS_POSSIBLE + || $result === ValidationResult::IS_POSSIBLE_LOCAL_ONLY; + } + + /** + * Helper method to check a number against possible lengths for this number type, and determine + * whether it matches, or is too short or too long. + */ + protected function testNumberLength(string $number, PhoneMetadata $metadata, PhoneNumberType $type = PhoneNumberType::UNKNOWN): ValidationResult + { + $descForType = $this->getNumberDescByType($metadata, $type); + // There should always be "possibleLengths" set for every element. This is declared in the XML + // schema which is verified by PhoneNumberMetadataSchemaTest. + // For size efficiency, where a sub-description (e.g. fixed-line) has the same possibleLengths + // as the parent, this is missing, so we fall back to the general desc (where no numbers of the + // type exist at all, there is one possible length (-1) which is guaranteed not to match the + // length of any real phone number). + $possibleLengths = (count($descForType->getPossibleLength()) === 0) + ? $metadata->getGeneralDesc()->getPossibleLength() : $descForType->getPossibleLength(); + + $localLengths = $descForType->getPossibleLengthLocalOnly(); + + if ($type === PhoneNumberType::FIXED_LINE_OR_MOBILE) { + if (!static::descHasPossibleNumberData($this->getNumberDescByType($metadata, PhoneNumberType::FIXED_LINE))) { + // The rate case has been encountered where no fixedLine data is available (true for some + // non-geographical entities), so we just check mobile. + return $this->testNumberLength($number, $metadata, PhoneNumberType::MOBILE); + } + + $mobileDesc = $this->getNumberDescByType($metadata, PhoneNumberType::MOBILE); + if (static::descHasPossibleNumberData($mobileDesc)) { + // Note that when adding the possible lengths from mobile, we have to again check they + // aren't empty since if they are this indicates they are the same as the general desc and + // should be obtained from there. + $possibleLengths = array_merge( + $possibleLengths, + (count($mobileDesc->getPossibleLength()) === 0) + ? $metadata->getGeneralDesc()->getPossibleLength() : $mobileDesc->getPossibleLength() + ); + + // The current list is sorted; we need to merge in the new list and re-sort (duplicates + // are okay). Sorting isn't so expensive because the lists are very small. + sort($possibleLengths); + + if (count($localLengths) === 0) { + $localLengths = $mobileDesc->getPossibleLengthLocalOnly(); + } else { + $localLengths = array_merge($localLengths, $mobileDesc->getPossibleLengthLocalOnly()); + sort($localLengths); + } + } + } + + + // If the type is not supported at all (indicated by the possible lengths containing -1 at this + // point) we return invalid length. + + if ($possibleLengths[0] === -1) { + return ValidationResult::INVALID_LENGTH; + } + + $actualLength = mb_strlen($number); + + // This is safe because there is never an overlap between the possible lengths and the local-only + // lengths; this is checked at build time. + + if (in_array($actualLength, $localLengths, true)) { + return ValidationResult::IS_POSSIBLE_LOCAL_ONLY; + } + + $minimumLength = reset($possibleLengths); + if ($minimumLength === $actualLength) { + return ValidationResult::IS_POSSIBLE; + } + + if ($minimumLength > $actualLength) { + return ValidationResult::TOO_SHORT; + } + + if (isset($possibleLengths[count($possibleLengths) - 1]) && $possibleLengths[count($possibleLengths) - 1] < $actualLength) { + return ValidationResult::TOO_LONG; + } + + // We skip the first element; we've already checked it. + array_shift($possibleLengths); + return in_array($actualLength, $possibleLengths, true) ? ValidationResult::IS_POSSIBLE : ValidationResult::INVALID_LENGTH; + } + + /** + * Returns a list with the region codes that match the specific country calling code. For + * non-geographical country calling codes, the region code 001 is returned. Also, in the case + * of no region code being found, an empty list is returned. + * @return string[] + */ + public function getRegionCodesForCountryCode(int $countryCallingCode): array + { + $regionCodes = $this->countryCallingCodeToRegionCodeMap[$countryCallingCode] ?? null; + return $regionCodes ?? []; + } + + /** + * Returns the country calling code for a specific region. For example, this would be 1 for the + * United States, and 64 for New Zealand. Assumes the region is already valid. + * + * @param string $regionCode the region that we want to get the country calling code for + * @return int the country calling code for the region denoted by regionCode + */ + public function getCountryCodeForRegion(string $regionCode): int + { + if (!$this->isValidRegionCode($regionCode)) { + return 0; + } + return $this->getCountryCodeForValidRegion($regionCode); + } + + /** + * Returns the country calling code for a specific region. For example, this would be 1 for the + * United States, and 64 for New Zealand. Assumes the region is already valid. + * + * @param string $regionCode the region that we want to get the country calling code for + * @return int the country calling code for the region denoted by regionCode + * @throws InvalidArgumentException if the region is invalid + */ + protected function getCountryCodeForValidRegion(string $regionCode): int + { + $metadata = $this->getMetadataForRegion($regionCode); + if ($metadata === null) { + throw new InvalidArgumentException('Invalid region code: ' . $regionCode); + } + return $metadata->getCountryCode(); + } + + /** + * Returns a number formatted in such a way that it can be dialed from a mobile phone in a + * specific region. If the number cannot be reached from the region (e.g. some countries block + * toll-free numbers from being called outside of the country), the method returns an empty + * string. + * + * @param PhoneNumber $number the phone number to be formatted + * @param string $regionCallingFrom the region where the call is being placed + * @param bool $withFormatting whether the number should be returned with formatting symbols, such as + * spaces and dashes. + * @return string the formatted phone number + */ + public function formatNumberForMobileDialing(PhoneNumber $number, string $regionCallingFrom, bool $withFormatting): string + { + $countryCallingCode = $number->getCountryCode(); + if (!$this->hasValidCountryCallingCode($countryCallingCode)) { + return $number->hasRawInput() ? $number->getRawInput() : ''; + } + + $formattedNumber = ''; + // Clear the extension, as that part cannot normally be dialed together with the main number. + $numberNoExt = new PhoneNumber(); + $numberNoExt->mergeFrom($number)->clearExtension(); + $regionCode = $this->getRegionCodeForCountryCode($countryCallingCode); + $numberType = $this->getNumberType($numberNoExt); + $isValidNumber = ($numberType !== PhoneNumberType::UNKNOWN); + if (strtoupper($regionCallingFrom) === $regionCode) { + $isFixedLineOrMobile = ($numberType === PhoneNumberType::FIXED_LINE || $numberType === PhoneNumberType::MOBILE || $numberType === PhoneNumberType::FIXED_LINE_OR_MOBILE); + // Carrier codes may be needed in some countries. We handle this here. + if ($regionCode === 'BR' && $isFixedLineOrMobile) { + // Historically, we set this to an empty string when parsing with raw input if none was + // found in the input string. However, this doesn't result in a number we can dial. For this + // reason, we treat the empty string the same as if it isn't set at all. + $formattedNumber = $numberNoExt->getPreferredDomesticCarrierCode() !== '' + ? $this->formatNationalNumberWithPreferredCarrierCode($numberNoExt, '') + // Brazilian fixed line and mobile numbers need to be dialed with a carrier code when + // called within Brazil. Without that, most of the carriers won't connect the call. + // Because of that, we return an empty string here. + : ''; + } elseif ($countryCallingCode === static::NANPA_COUNTRY_CODE) { + // For NANPA countries, we output international format for numbers that can be dialed + // internationally, since that always works, except for numbers which might potentially be + // short numbers, which are always dialled in national format. + $regionMetadata = $this->getMetadataForRegion($regionCallingFrom); + if ($this->canBeInternationallyDialled($numberNoExt) + && $this->testNumberLength($this->getNationalSignificantNumber($numberNoExt), $regionMetadata) + !== ValidationResult::TOO_SHORT + ) { + $formattedNumber = $this->format($numberNoExt, PhoneNumberFormat::INTERNATIONAL); + } else { + $formattedNumber = $this->format($numberNoExt, PhoneNumberFormat::NATIONAL); + } + } elseif (( + $regionCode === static::REGION_CODE_FOR_NON_GEO_ENTITY || + // MX fixed line and mobile numbers should always be formatted in international format, + // even when dialed within MX. For national format to work, a carrier code needs to be + // used, and the correct carrier code depends on if the caller and callee are from the + // same local area. It is trickier to get that to work correctly than using + // international format, which is tested to work fine on all carriers. + // CL fixed line numbers need the national prefix when dialing in the national format, + // but don't have it when used for display. The reverse is true for mobile numbers. + // As a result, we output them in the international format to make it work. + ( + ($regionCode === 'MX' || $regionCode === 'CL' || $regionCode === 'UZ') + && $isFixedLineOrMobile + ) + ) && $this->canBeInternationallyDialled($numberNoExt) + ) { + $formattedNumber = $this->format($numberNoExt, PhoneNumberFormat::INTERNATIONAL); + } else { + $formattedNumber = $this->format($numberNoExt, PhoneNumberFormat::NATIONAL); + } + } elseif ($isValidNumber && $this->canBeInternationallyDialled($numberNoExt)) { + // We assume that short numbers are not diallable from outside their region, so if a number + // is not a valid regular length phone number, we treat it as if it cannot be internationally + // dialled. + return $withFormatting ? + $this->format($numberNoExt, PhoneNumberFormat::INTERNATIONAL) : + $this->format($numberNoExt, PhoneNumberFormat::E164); + } + return $withFormatting ? $formattedNumber : static::normalizeDiallableCharsOnly($formattedNumber); + } + + /** + * Formats a phone number in national format for dialing using the carrier as specified in the + * {@code carrierCode}. The {@code carrierCode} will always be used regardless of whether the + * phone number already has a preferred domestic carrier code stored. If {@code carrierCode} + * contains an empty string, returns the number in national format without any carrier code. + * + * @param PhoneNumber $number the phone number to be formatted + * @param string|null $carrierCode the carrier selection code to be used + * @return string the formatted phone number in national format for dialing using the carrier as + * specified in the {@code carrierCode} + */ + public function formatNationalNumberWithCarrierCode(PhoneNumber $number, ?string $carrierCode): string + { + $countryCallingCode = $number->getCountryCode(); + $nationalSignificantNumber = $this->getNationalSignificantNumber($number); + if (!$this->hasValidCountryCallingCode($countryCallingCode)) { + return $nationalSignificantNumber; + } + + // Note getRegionCodeForCountryCode() is used because formatting information for regions which + // share a country calling code is contained by only one region for performance reasons. For + // example, for NANPA regions it will be contained in the metadata for US. + $regionCode = $this->getRegionCodeForCountryCode($countryCallingCode); + // Metadata cannot be null because the country calling code is valid. + $metadata = $this->getMetadataForRegionOrCallingCode($countryCallingCode, $regionCode); + + $formattedNumber = $this->formatNsn( + $nationalSignificantNumber, + $metadata, + PhoneNumberFormat::NATIONAL, + $carrierCode + ); + $this->maybeAppendFormattedExtension($number, $metadata, PhoneNumberFormat::NATIONAL, $formattedNumber); + $this->prefixNumberWithCountryCallingCode( + $countryCallingCode, + PhoneNumberFormat::NATIONAL, + $formattedNumber + ); + return $formattedNumber; + } + + /** + * Formats a phone number in national format for dialing using the carrier as specified in the + * preferredDomesticCarrierCode field of the PhoneNumber object passed in. If that is missing, + * use the {@code fallbackCarrierCode} passed in instead. If there is no + * {@code preferredDomesticCarrierCode}, and the {@code fallbackCarrierCode} contains an empty + * string, return the number in national format without any carrier code. + * + *

Use {@see formatNationalNumberWithCarrierCode} instead if the carrier code passed in + * should take precedence over the number's {@code preferredDomesticCarrierCode} when formatting. + * + * @param PhoneNumber $number the phone number to be formatted + * @param string $fallbackCarrierCode the carrier selection code to be used, if none is found in the + * phone number itself + * @return string the formatted phone number in national format for dialing using the number's + * {@code preferredDomesticCarrierCode}, or the {@code fallbackCarrierCode} passed in if + * none is found + */ + public function formatNationalNumberWithPreferredCarrierCode(PhoneNumber $number, string $fallbackCarrierCode): string + { + return $this->formatNationalNumberWithCarrierCode( + $number, + // Historically, we set this to an empty string when parsing with raw input if none was + // found in the input string. However, this doesn't result in a number we can dial. For this + // reason, we treat the empty string the same as if it isn't set at all. + $number->hasPreferredDomesticCarrierCode() && $number->getPreferredDomesticCarrierCode() !== '' + ? $number->getPreferredDomesticCarrierCode() + : $fallbackCarrierCode + ); + } + + /** + * Returns true if the number can be dialled from outside the region, or unknown. If the number + * can only be dialled from within the region, returns false. Does not check the number is a valid + * number. Note that, at the moment, this method does not handle short numbers (which are + * currently all presumed to not be diallable from outside their country). + * + * @param PhoneNumber $number the phone-number for which we want to know whether it is diallable from outside the region + */ + public function canBeInternationallyDialled(PhoneNumber $number): bool + { + $metadata = $this->getMetadataForRegion($this->getRegionCodeForNumber($number)); + if ($metadata === null) { + // Note numbers belonging to non-geographical entities (e.g. +800 numbers) are always + // internationally diallable, and will be caught here. + return true; + } + $nationalSignificantNumber = $this->getNationalSignificantNumber($number); + return !$this->isNumberMatchingDesc($nationalSignificantNumber, $metadata->getNoInternationalDialling()); + } + + /** + * Normalizes a string of characters representing a phone number. This strips all characters which + * are not diallable on a mobile phone keypad (including all non-ASCII digits). + * + * @param string $number a string of characters representing a phone number + * @return string the normalized string version of the phone number + */ + public static function normalizeDiallableCharsOnly(string $number): string + { + return static::normalizeHelper($number, static::DIALLABLE_CHAR_MAPPINGS, true /* remove non matches */); + } + + /** + * Formats a phone number for out-of-country dialing purposes. + * + * Note that in this version, if the number was entered originally using alpha characters and + * this version of the number is stored in raw_input, this representation of the number will be + * used rather than the digit representation. Grouping information, as specified by characters + * such as "-" and " ", will be retained. + * + *

Caveats:

+ *
    + *
  • This will not produce good results if the country calling code is both present in the raw + * input _and_ is the start of the national number. This is not a problem in the regions + * which typically use alpha numbers. + *
  • This will also not produce good results if the raw input has any grouping information + * within the first three digits of the national number, and if the function needs to strip + * preceding digits/words in the raw input before these digits. Normally people group the + * first three digits together so this is not a huge problem - and will be fixed if it + * proves to be so. + *
+ * + * @param PhoneNumber $number the phone number that needs to be formatted + * @param $regionCallingFrom string the region where the call is being placed + * @return string the formatted phone number + */ + public function formatOutOfCountryKeepingAlphaChars(PhoneNumber $number, string $regionCallingFrom): string + { + $rawInput = $number->getRawInput(); + // If there is no raw input, then we can't keep alpha characters because there aren't any. + // In this case, we return formatOutOfCountryCallingNumber. + if ($rawInput === null || $rawInput === '') { + return $this->formatOutOfCountryCallingNumber($number, $regionCallingFrom); + } + $countryCode = $number->getCountryCode(); + if (!$this->hasValidCountryCallingCode($countryCode)) { + return $rawInput; + } + // Strip any prefix such as country calling code, IDD, that was present. We do this by comparing + // the number in raw_input with the parsed number. + // To do this, first we normalize punctuation. We retain number grouping symbols such as " " + // only. + $rawInput = self::normalizeHelper($rawInput, static::$ALL_PLUS_NUMBER_GROUPING_SYMBOLS, true); + // Now we trim everything before the first three digits in the parsed number. We choose three + // because all valid alpha numbers have 3 digits at the start - if it does not, then we don't + // trim anything at all. Similarly, if the national number was less than three digits, we don't + // trim anything at all. + $nationalNumber = $this->getNationalSignificantNumber($number); + if (mb_strlen($nationalNumber) > 3) { + $firstNationalNumberDigit = strpos($rawInput, substr($nationalNumber, 0, 3)); + if ($firstNationalNumberDigit !== false) { + $rawInput = substr($rawInput, $firstNationalNumberDigit); + } + } + $metadataForRegionCallingFrom = $this->getMetadataForRegion($regionCallingFrom); + if ($countryCode === static::NANPA_COUNTRY_CODE) { + if ($this->isNANPACountry($regionCallingFrom)) { + return $countryCode . ' ' . $rawInput; + } + } elseif ($metadataForRegionCallingFrom !== null && + $countryCode === $this->getCountryCodeForValidRegion($regionCallingFrom) + ) { + $formattingPattern = + $this->chooseFormattingPatternForNumber( + $metadataForRegionCallingFrom->numberFormats(), + $nationalNumber + ); + if ($formattingPattern === null) { + // If no pattern above is matched, we format the original input. + return $rawInput; + } + $newFormat = new NumberFormat(); + $newFormat->mergeFrom($formattingPattern); + // The first group is the first group of digits that the user wrote together. + $newFormat->setPattern('(\\d+)(.*)'); + // Here we just concatenate them back together after the national prefix has been fixed. + $newFormat->setFormat('$1$2'); + // Now we format using this pattern instead of the default pattern, but with the national + // prefix prefixed if necessary. + // This will not work in the cases where the pattern (and not the leading digits) decide + // whether a national prefix needs to be used, since we have overridden the pattern to match + // anything, but that is not the case in the metadata to date. + return $this->formatNsnUsingPattern($rawInput, $newFormat, PhoneNumberFormat::NATIONAL); + } + $internationalPrefixForFormatting = ''; + // If an unsupported region-calling-from is entered, or a country with multiple international + // prefixes, the international format of the number is returned, unless there is a preferred + // international prefix. + if ($metadataForRegionCallingFrom !== null) { + $internationalPrefix = $metadataForRegionCallingFrom->getInternationalPrefix(); + $uniqueInternationalPrefixMatcher = new Matcher(static::SINGLE_INTERNATIONAL_PREFIX, $internationalPrefix); + $internationalPrefixForFormatting = + $uniqueInternationalPrefixMatcher->matches() + ? $internationalPrefix + : $metadataForRegionCallingFrom->getPreferredInternationalPrefix(); + } + $formattedNumber = $rawInput; + $regionCode = $this->getRegionCodeForCountryCode($countryCode); + // Metadata cannot be null because the country calling code is valid. + $metadataForRegion = $this->getMetadataForRegionOrCallingCode($countryCode, $regionCode); + // Strip any extension + $this->maybeStripExtension($formattedNumber); + // Append the formatted extension + $this->maybeAppendFormattedExtension( + $number, + $metadataForRegion, + PhoneNumberFormat::INTERNATIONAL, + $formattedNumber + ); + if ($internationalPrefixForFormatting !== null && $internationalPrefixForFormatting !== '') { + $formattedNumber = $internationalPrefixForFormatting . ' ' . $countryCode . ' ' . $formattedNumber; + } else { + // Invalid region entered as country-calling-from (so no metadata was found for it) or the + // region chosen has multiple international dialling prefixes. + $this->prefixNumberWithCountryCallingCode( + $countryCode, + PhoneNumberFormat::INTERNATIONAL, + $formattedNumber + ); + } + return $formattedNumber; + } + + /** + * Formats a phone number for out-of-country dialing purposes. If no regionCallingFrom is + * supplied, we format the number in its INTERNATIONAL format. If the country calling code is the + * same as that of the region where the number is from, then NATIONAL formatting will be applied. + * + *

If the number itself has a country calling code of zero or an otherwise invalid country + * calling code, then we return the number with no formatting applied. + * + *

Note this function takes care of the case for calling inside of NANPA and between Russia and + * Kazakhstan (who share the same country calling code). In those cases, no international prefix + * is used. For regions which have multiple international prefixes, the number in its + * INTERNATIONAL format will be returned instead. + * + * @param PhoneNumber $number the phone number to be formatted + * @param string $regionCallingFrom the region where the call is being placed + * @return string the formatted phone number + */ + public function formatOutOfCountryCallingNumber(PhoneNumber $number, string $regionCallingFrom): string + { + if (!$this->isValidRegionCode($regionCallingFrom)) { + return $this->format($number, PhoneNumberFormat::INTERNATIONAL); + } + $countryCallingCode = $number->getCountryCode(); + $nationalSignificantNumber = $this->getNationalSignificantNumber($number); + if (!$this->hasValidCountryCallingCode($countryCallingCode)) { + return $nationalSignificantNumber; + } + if ($countryCallingCode === static::NANPA_COUNTRY_CODE) { + if ($this->isNANPACountry($regionCallingFrom)) { + // For NANPA regions, return the national format for these regions but prefix it with the + // country calling code. + return $countryCallingCode . ' ' . $this->format($number, PhoneNumberFormat::NATIONAL); + } + } elseif ($countryCallingCode === $this->getCountryCodeForValidRegion($regionCallingFrom)) { + // If regions share a country calling code, the country calling code need not be dialled. + // This also applies when dialling within a region, so this if clause covers both these cases. + // Technically this is the case for dialling from La Reunion to other overseas departments of + // France (French Guiana, Martinique, Guadeloupe), but not vice versa - so we don't cover this + // edge case for now and for those cases return the version including country calling code. + // Details here: http://www.petitfute.com/voyage/225-info-pratiques-reunion + return $this->format($number, PhoneNumberFormat::NATIONAL); + } + // Metadata cannot be null because we checked 'isValidRegionCode()' above. + /** @var PhoneMetadata $metadataForRegionCallingFrom */ + $metadataForRegionCallingFrom = $this->getMetadataForRegion($regionCallingFrom); + + $internationalPrefix = $metadataForRegionCallingFrom->getInternationalPrefix(); + + // In general, if there is a preferred international prefix, use that. Otherwise, for regions + // that have multiple international prefixes, the international format of the number is + // returned since we would not know which one to use. + $internationalPrefixForFormatting = ''; + if ($metadataForRegionCallingFrom->hasPreferredInternationalPrefix()) { + $internationalPrefixForFormatting = $metadataForRegionCallingFrom->getPreferredInternationalPrefix(); + } else { + $uniqueInternationalPrefixMatcher = new Matcher(static::SINGLE_INTERNATIONAL_PREFIX, $internationalPrefix); + + if ($uniqueInternationalPrefixMatcher->matches()) { + $internationalPrefixForFormatting = $internationalPrefix; + } + } + + $regionCode = $this->getRegionCodeForCountryCode($countryCallingCode); + // Metadata cannot be null because the country calling code is valid. + /** @var PhoneMetadata $metadataForRegion */ + $metadataForRegion = $this->getMetadataForRegionOrCallingCode($countryCallingCode, $regionCode); + $formattedNationalNumber = $this->formatNsn( + $nationalSignificantNumber, + $metadataForRegion, + PhoneNumberFormat::INTERNATIONAL + ); + $formattedNumber = $formattedNationalNumber; + $this->maybeAppendFormattedExtension( + $number, + $metadataForRegion, + PhoneNumberFormat::INTERNATIONAL, + $formattedNumber + ); + if ($internationalPrefixForFormatting !== '') { + $formattedNumber = $internationalPrefixForFormatting . ' ' . $countryCallingCode . ' ' . $formattedNumber; + } else { + $this->prefixNumberWithCountryCallingCode( + $countryCallingCode, + PhoneNumberFormat::INTERNATIONAL, + $formattedNumber + ); + } + return $formattedNumber; + } + + /** + * Checks if this is a region under the North American Numbering Plan Administration (NANPA). + * @return bool true if regionCode is one of the regions under NANPA + */ + public function isNANPACountry(string $regionCode): bool + { + return in_array(strtoupper($regionCode), $this->nanpaRegions, true); + } + + /** + * Formats a phone number using the original phone number format (e.g. INTERNATIONAL or NATIONAL) + * that the number is parsed from, provided that the number has been parsed with + * parseAndKeepRawInput. Otherwise the number will be formatted in NATIONAL format. + * + * The original format is embedded in the country_code_source field of the PhoneNumber object + * passed in, which is only set when parsing keeps the raw input. When we don't have a formatting + * pattern for the number, the method falls back to returning the raw input. + * + * Note this method guarantees no digit will be inserted, removed or modified as a result of + * formatting. + * + * @param PhoneNumber $number the phone number that needs to be formatted in its original number format + * @param string $regionCallingFrom the region whose IDD needs to be prefixed if the original number + * has one + * @return string the formatted phone number in its original number format + */ + public function formatInOriginalFormat(PhoneNumber $number, string $regionCallingFrom): string + { + if ($number->hasRawInput() && !$this->hasFormattingPatternForNumber($number)) { + // We check if we have the formatting pattern because without that, we might format the number + // as a group without national prefix. + return $number->getRawInput(); + } + if (!$number->hasCountryCodeSource()) { + return $this->format($number, PhoneNumberFormat::NATIONAL); + } + switch ($number->getCountryCodeSource()) { + case CountryCodeSource::FROM_NUMBER_WITH_PLUS_SIGN: + $formattedNumber = $this->format($number, PhoneNumberFormat::INTERNATIONAL); + break; + case CountryCodeSource::FROM_NUMBER_WITH_IDD: + $formattedNumber = $this->formatOutOfCountryCallingNumber($number, $regionCallingFrom); + break; + case CountryCodeSource::FROM_NUMBER_WITHOUT_PLUS_SIGN: + $formattedNumber = substr($this->format($number, PhoneNumberFormat::INTERNATIONAL), 1); + break; + case CountryCodeSource::FROM_DEFAULT_COUNTRY: + // Fall-through to default case. + default: + + $regionCode = $this->getRegionCodeForCountryCode($number->getCountryCode()); + // We strip non-digits from the NDD here, and from the raw input later, so that we can + // compare them easily. + $nationalPrefix = $this->getNddPrefixForRegion($regionCode, true /* strip non-digits */); + $nationalFormat = $this->format($number, PhoneNumberFormat::NATIONAL); + if ($nationalPrefix === null || $nationalPrefix === '') { + // If the region doesn't have a national prefix at all, we can safely return the national + // format without worrying about a national prefix being added. + $formattedNumber = $nationalFormat; + break; + } + // Otherwise, we check if the original number was entered with a national prefix. + if ($this->rawInputContainsNationalPrefix( + $number->getRawInput(), + $nationalPrefix, + $regionCode + ) + ) { + // If so, we can safely return the national format. + $formattedNumber = $nationalFormat; + break; + } + // Metadata cannot be null here because getNddPrefixForRegion() (above) returns null if + // there is no metadata for the region. + $metadata = $this->getMetadataForRegion($regionCode); + $nationalNumber = $this->getNationalSignificantNumber($number); + $formatRule = $this->chooseFormattingPatternForNumber($metadata->numberFormats(), $nationalNumber); + // The format rule could still be null here if the national number was 0 and there was no + // raw input (this should not be possible for numbers generated by the phonenumber library + // as they would also not have a country calling code and we would have exited earlier). + if ($formatRule === null) { + $formattedNumber = $nationalFormat; + break; + } + // When the format we apply to this number doesn't contain national prefix, we can just + // return the national format. + // TODO: Refactor the code below with the code in isNationalPrefixPresentIfRequired. + $candidateNationalPrefixRule = $formatRule->getNationalPrefixFormattingRule(); + // We assume that the first-group symbol will never be _before_ the national prefix. + $indexOfFirstGroup = strpos($candidateNationalPrefixRule, '$1'); + if ($indexOfFirstGroup <= 0) { + $formattedNumber = $nationalFormat; + break; + } + $candidateNationalPrefixRule = substr($candidateNationalPrefixRule, 0, $indexOfFirstGroup); + $candidateNationalPrefixRule = static::normalizeDigitsOnly($candidateNationalPrefixRule); + if ($candidateNationalPrefixRule === '') { + // National prefix not used when formatting this number. + $formattedNumber = $nationalFormat; + break; + } + // Otherwise, we need to remove the national prefix from our output. + $numFormatCopy = new NumberFormat(); + $numFormatCopy->mergeFrom($formatRule); + $numFormatCopy->clearNationalPrefixFormattingRule(); + $numberFormats = []; + $numberFormats[] = $numFormatCopy; + $formattedNumber = $this->formatByPattern($number, PhoneNumberFormat::NATIONAL, $numberFormats); + break; + } + $rawInput = $number->getRawInput(); + // If no digit is inserted/removed/modified as a result of our formatting, we return the + // formatted phone number; otherwise we return the raw input the user entered. + if ($rawInput !== '') { + $normalizedFormattedNumber = static::normalizeDiallableCharsOnly($formattedNumber); + $normalizedRawInput = static::normalizeDiallableCharsOnly($rawInput); + if ($normalizedFormattedNumber !== $normalizedRawInput) { + $formattedNumber = $rawInput; + } + } + return $formattedNumber; + } + + protected function hasFormattingPatternForNumber(PhoneNumber $number): bool + { + $countryCallingCode = $number->getCountryCode(); + $phoneNumberRegion = $this->getRegionCodeForCountryCode($countryCallingCode); + $metadata = $this->getMetadataForRegionOrCallingCode($countryCallingCode, $phoneNumberRegion); + if ($metadata === null) { + return false; + } + $nationalNumber = $this->getNationalSignificantNumber($number); + $formatRule = $this->chooseFormattingPatternForNumber($metadata->numberFormats(), $nationalNumber); + return $formatRule !== null; + } + + /** + * Returns the national dialling prefix for a specific region. For example, this would be 1 for + * the United States, and 0 for New Zealand. Set stripNonDigits to true to strip symbols like "~" + * (which indicates a wait for a dialling tone) from the prefix returned. If no national prefix is + * present, we return null. + * + *

Warning: Do not use this method for do-your-own formatting - for some regions, the + * national dialling prefix is used only for certain types of numbers. Use the library's + * formatting functions to prefix the national prefix when required. + * + * @param string $regionCode the region that we want to get the dialling prefix for + * @param bool $stripNonDigits true to strip non-digits from the national dialling prefix + * @return string|null the dialling prefix for the region denoted by regionCode + */ + public function getNddPrefixForRegion(string $regionCode, bool $stripNonDigits): ?string + { + $metadata = $this->getMetadataForRegion($regionCode); + if ($metadata === null) { + return null; + } + $nationalPrefix = $metadata->getNationalPrefix(); + // If no national prefix was found, we return null. + if ($nationalPrefix === null || $nationalPrefix === '') { + return null; + } + if ($stripNonDigits) { + // Note: if any other non-numeric symbols are ever used in national prefixes, these would have + // to be removed here as well. + $nationalPrefix = str_replace('~', '', $nationalPrefix); + } + return $nationalPrefix; + } + + /** + * Check if rawInput, which is assumed to be in the national format, has a national prefix. The + * national prefix is assumed to be in digits-only form. + */ + protected function rawInputContainsNationalPrefix(string $rawInput, string $nationalPrefix, string $regionCode): bool + { + $normalizedNationalNumber = static::normalizeDigitsOnly($rawInput); + if (str_starts_with($normalizedNationalNumber, $nationalPrefix)) { + try { + // Some Japanese numbers (e.g. 00777123) might be mistaken to contain the national prefix + // when written without it (e.g. 0777123) if we just do prefix matching. To tackle that, we + // check the validity of the number if the assumed national prefix is removed (777123 won't + // be valid in Japan). + return $this->isValidNumber( + $this->parse(substr($normalizedNationalNumber, mb_strlen($nationalPrefix)), $regionCode) + ); + } catch (NumberParseException) { + return false; + } + } + return false; + } + + /** + * Tests whether a phone number matches a valid pattern. Note this doesn't verify the number + * is actually in use, which is impossible to tell by just looking at a number itself. It only + * verifies whether the parsed, canonicalised number is valid: not whether a particular series of + * digits entered by the user is diallable from the region provided when parsing. For example, the + * number +41 (0) 78 927 2696 can be parsed into a number with country code "41" and national + * significant number "789272696". This is valid, while the original string is not diallable. + * + * @param PhoneNumber $number the phone number that we want to validate + * @return bool that indicates whether the number is of a valid pattern + */ + public function isValidNumber(PhoneNumber $number): bool + { + $regionCode = $this->getRegionCodeForNumber($number); + return $this->isValidNumberForRegion($number, $regionCode); + } + + /** + * Tests whether a phone number is valid for a certain region. Note this doesn't verify the number + * is actually in use, which is impossible to tell by just looking at a number itself. If the + * country calling code is not the same as the country calling code for the region, this + * immediately exits with false. After this, the specific number pattern rules for the region are + * examined. This is useful for determining for example whether a particular number is valid for + * Canada, rather than just a valid NANPA number. + * Warning: In most cases, you want to use {@see isValidNumber} instead. For example, this + * method will mark numbers from British Crown dependencies such as the Isle of Man as invalid for + * the region "GB" (United Kingdom), since it has its own region code, "IM", which may be + * undesirable. + * + * @param PhoneNumber $number the phone number that we want to validate + * @param string|null $regionCode the region that we want to validate the phone number for + * @return bool that indicates whether the number is of a valid pattern + */ + public function isValidNumberForRegion(PhoneNumber $number, ?string $regionCode): bool + { + $countryCode = $number->getCountryCode(); + if ($countryCode === null) { + return false; + } + $metadata = $this->getMetadataForRegionOrCallingCode($countryCode, $regionCode); + if (($metadata === null) || + (static::REGION_CODE_FOR_NON_GEO_ENTITY !== $regionCode && + $countryCode !== $this->getCountryCodeForValidRegion($regionCode)) + ) { + // Either the region code was invalid, or the country calling code for this number does not + // match that of the region code. + return false; + } + $nationalSignificantNumber = $this->getNationalSignificantNumber($number); + + return $this->getNumberTypeHelper($nationalSignificantNumber, $metadata) !== PhoneNumberType::UNKNOWN; + } + + /** + * Parses a string and returns it as a phone number in proto buffer format. The method is quite + * lenient and looks for a number in the input text (raw input) and does not check whether the + * string is definitely only a phone number. To do this, it ignores punctuation and white-space, + * as well as any text before the number (e.g. a leading “Tel: ”) and trims the non-number bits. + * It will accept a number in any format (E164, national, international etc), assuming it can + * interpreted with the defaultRegion supplied. It also attempts to convert any alpha characters + * into digits if it thinks this is a vanity number of the type "1800 MICROSOFT". + * + *

This method will throw a {@link NumberParseException} if the number is not considered to + * be a possible number. Note that validation of whether the number is actually a valid number + * for a particular region is not performed. This can be done separately with {@see isValidNumber}. + * + *

Note this method canonicalizes the phone number such that different representations can be + * easily compared, no matter what form it was originally entered in (e.g. national, + * international). If you want to record context about the number being parsed, such as the raw + * input that was entered, how the country code was derived etc. then call {@see parseAndKeepRawInput} + * instead. + * + * @param string $numberToParse number that we are attempting to parse. This can contain formatting + * such as +, ( and -, as well as a phone number extension. + * @param string|null $defaultRegion region that we are expecting the number to be from. This is only used + * if the number being parsed is not written in international format. + * The country_code for the number in this case would be stored as that + * of the default region supplied. If the number is guaranteed to + * start with a '+' followed by the country calling code, then + * "ZZ" or null can be supplied. + * @return PhoneNumber a phone number proto buffer filled with the parsed number + * @throws NumberParseException if the string is not considered to be a viable phone number (e.g. + * too few or too many digits) or if no default region was supplied + * and the number is not in international format (does not start + * with +) + */ + public function parse(string $numberToParse, ?string $defaultRegion = null, ?PhoneNumber $phoneNumber = null, bool $keepRawInput = false): PhoneNumber + { + if ($phoneNumber === null) { + $phoneNumber = new PhoneNumber(); + } + $this->parseHelper($numberToParse, $defaultRegion, $keepRawInput, true, $phoneNumber); + return $phoneNumber; + } + + /** + * Formats a phone number in the specified format using client-defined formatting rules. Note that + * if the phone number has a country calling code of zero or an otherwise invalid country calling + * code, we cannot work out things like whether there should be a national prefix applied, or how + * to format extensions, so we return the national significant number with no formatting applied. + * + * @param NumberFormat[] $userDefinedFormats formatting rules specified by clients + * @return string the formatted phone number + */ + public function formatByPattern(PhoneNumber $number, PhoneNumberFormat $numberFormat, array $userDefinedFormats): string + { + $countryCallingCode = $number->getCountryCode(); + $nationalSignificantNumber = $this->getNationalSignificantNumber($number); + if (!$this->hasValidCountryCallingCode($countryCallingCode)) { + return $nationalSignificantNumber; + } + // Note getRegionCodeForCountryCode() is used because formatting information for regions which + // share a country calling code is contained by only one region for performance reasons. For + // example, for NANPA regions it will be contained in the metadata for US. + $regionCode = $this->getRegionCodeForCountryCode($countryCallingCode); + // Metadata cannot be null because the country calling code is valid. + $metadata = $this->getMetadataForRegionOrCallingCode($countryCallingCode, $regionCode); + + $formattedNumber = ''; + + $formattingPattern = $this->chooseFormattingPatternForNumber($userDefinedFormats, $nationalSignificantNumber); + if ($formattingPattern === null) { + // If no pattern above is matched, we format the number as a whole. + $formattedNumber .= $nationalSignificantNumber; + } else { + $numFormatCopy = new NumberFormat(); + // Before we do a replacement of the national prefix pattern $NP with the national prefix, we + // need to copy the rule so that subsequent replacements for different numbers have the + // appropriate national prefix. + $numFormatCopy->mergeFrom($formattingPattern); + $nationalPrefixFormattingRule = $formattingPattern->getNationalPrefixFormattingRule(); + if ($nationalPrefixFormattingRule !== '') { + $nationalPrefix = $metadata->getNationalPrefix(); + if ($nationalPrefix !== null && $nationalPrefix !== '') { + // Replace $NP with national prefix and $FG with the first group ($1). + $nationalPrefixFormattingRule = str_replace( + [static::NP_STRING, static::FG_STRING], + [$nationalPrefix, '$1'], + $nationalPrefixFormattingRule + ); + $numFormatCopy->setNationalPrefixFormattingRule($nationalPrefixFormattingRule); + } else { + // We don't want to have a rule for how to format the national prefix if there isn't one. + $numFormatCopy->clearNationalPrefixFormattingRule(); + } + } + $formattedNumber .= $this->formatNsnUsingPattern($nationalSignificantNumber, $numFormatCopy, $numberFormat); + } + $this->maybeAppendFormattedExtension($number, $metadata, $numberFormat, $formattedNumber); + $this->prefixNumberWithCountryCallingCode($countryCallingCode, $numberFormat, $formattedNumber); + return $formattedNumber; + } + + /** + * Gets a valid number for the specified region. + * + * @param $regionCode string the region for which an example number is needed + * @return PhoneNumber|null a valid fixed-line number for the specified region. Returns null when the metadata + * does not contain such information, or the region 001 is passed in. For 001 (representing + * non-geographical numbers), call {@see getExampleNumberForNonGeoEntity} instead. + */ + public function getExampleNumber(string $regionCode): ?PhoneNumber + { + return $this->getExampleNumberForType($regionCode, PhoneNumberType::FIXED_LINE); + } + + /** + * Gets an invalid number for the specified region. This is useful for unit-testing purposes, + * where you want to test what will happen with an invalid number. Note that the number that is + * returned will always be able to be parsed and will have the correct country code. It may also + * be a valid *short* number/code for this region. Validity checking such numbers is handled with + * {@link ShortNumberInfo}. + * + * @param string $regionCode The region for which an example number is needed + * @return PhoneNumber|null An invalid number for the specified region. Returns null when an unsupported region + * or the region 001 (Earth) is passed in. + */ + public function getInvalidExampleNumber(string $regionCode): ?PhoneNumber + { + if (!$this->isValidRegionCode($regionCode)) { + return null; + } + + // We start off with a valid fixed-line number since every country supports this. Alternatively + // we could start with a different number type, since fixed-line numbers typically have a wide + // breadth of valid number lengths and we may have to make it very short before we get an + // invalid number. + + $desc = $this->getNumberDescByType($this->getMetadataForRegion($regionCode), PhoneNumberType::FIXED_LINE); + + if ($desc->getExampleNumber() === '') { + // This shouldn't happen; we have a test for this. + return null; + } + + $exampleNumber = $desc->getExampleNumber(); + + // Try and make the number invalid. We do this by changing the length. We try reducing the + // length of the number, since currently no region has a number that is the same length as + // MIN_LENGTH_FOR_NSN. This is probably quicker than making the number longer, which is another + // alternative. We could also use the possible number pattern to extract the possible lengths of + // the number to make this faster, but this method is only for unit-testing so simplicity is + // preferred to performance. We don't want to return a number that can't be parsed, so we check + // the number is long enough. We try all possible lengths because phone number plans often have + // overlapping prefixes so the number 123456 might be valid as a fixed-line number, and 12345 as + // a mobile number. It would be faster to loop in a different order, but we prefer numbers that + // look closer to real numbers (and it gives us a variety of different lengths for the resulting + // phone numbers - otherwise they would all be MIN_LENGTH_FOR_NSN digits long.) + for ($phoneNumberLength = mb_strlen($exampleNumber) - 1; $phoneNumberLength >= static::MIN_LENGTH_FOR_NSN; $phoneNumberLength--) { + $numberToTry = mb_substr($exampleNumber, 0, $phoneNumberLength); + try { + $possiblyValidNumber = $this->parse($numberToTry, $regionCode); + if (!$this->isValidNumber($possiblyValidNumber)) { + return $possiblyValidNumber; + } + } catch (NumberParseException) { + // Shouldn't happen: we have already checked the length, we know example numbers have + // only valid digits, and we know the region code is fine. + } + } + // We have a test to check that this doesn't happen for any of our supported regions. + return null; + } + + /** + * Gets a valid number for the specified region and number type. + * + * @param string|PhoneNumberType $regionCodeOrType the region for which an example number is needed or the + * PhoneNumberType when not passing in $type + * @return PhoneNumber|null a valid number for the specified region and type. Returns null when the metadata + * does not contain such information or if an invalid region or region 001 was entered. + * For 001 (representing non-geographical numbers), call + * {@see getExampleNumberForNonGeoEntity} instead. + * + * If $regionCodeOrType is the only parameter supplied, then a valid number for the specified number type + * will be returned that may belong to any country. + */ + public function getExampleNumberForType(string|PhoneNumberType $regionCodeOrType, ?PhoneNumberType $type = null): ?PhoneNumber + { + if ($type === null) { + if (!$regionCodeOrType instanceof PhoneNumberType) { + throw new TypeError('$regionCodeOrType must be a PhoneNumberType'); + } + + /* + * Gets a valid number for the specified number type (it may belong to any country). + */ + foreach ($this->getSupportedRegions() as $regionCode) { + $exampleNumber = $this->getExampleNumberForType($regionCode, $regionCodeOrType); + if ($exampleNumber !== null) { + return $exampleNumber; + } + } + + // If there wasn't an example number for a region, try the non-geographical entities. + foreach ($this->getSupportedGlobalNetworkCallingCodes() as $countryCallingCode) { + $desc = $this->getNumberDescByType($this->getMetadataForNonGeographicalRegion($countryCallingCode), $regionCodeOrType); + try { + if ($desc->getExampleNumber() !== '') { + return $this->parse('+' . $countryCallingCode . $desc->getExampleNumber(), static::UNKNOWN_REGION); + } + } catch (NumberParseException) { + // noop + } + } + // There are no example numbers of this type for any country in the library. + return null; + } + + if (!is_string($regionCodeOrType)) { + throw new TypeError('$regionCodeOrType must be a string if $type is null'); + } + + // Check the region code is valid. + if (!$this->isValidRegionCode($regionCodeOrType)) { + return null; + } + $desc = $this->getNumberDescByType($this->getMetadataForRegion($regionCodeOrType), $type); + try { + if ($desc->hasExampleNumber()) { + return $this->parse($desc->getExampleNumber(), $regionCodeOrType); + } + } catch (NumberParseException) { + // noop + } + return null; + } + + protected function getNumberDescByType(PhoneMetadata $metadata, PhoneNumberType $type): PhoneNumberDesc + { + return match ($type) { + PhoneNumberType::PREMIUM_RATE => $metadata->getPremiumRate(), + PhoneNumberType::TOLL_FREE => $metadata->getTollFree(), + PhoneNumberType::MOBILE => $metadata->getMobile(), + PhoneNumberType::FIXED_LINE, PhoneNumberType::FIXED_LINE_OR_MOBILE => $metadata->getFixedLine(), + PhoneNumberType::SHARED_COST => $metadata->getSharedCost(), + PhoneNumberType::VOIP => $metadata->getVoip(), + PhoneNumberType::PERSONAL_NUMBER => $metadata->getPersonalNumber(), + PhoneNumberType::PAGER => $metadata->getPager(), + PhoneNumberType::UAN => $metadata->getUan(), + PhoneNumberType::VOICEMAIL => $metadata->getVoicemail(), + default => $metadata->getGeneralDesc(), + }; + } + + /** + * Gets a valid number for the specified country calling code for a non-geographical entity. + * + * @param int $countryCallingCode the country calling code for a non-geographical entity + * @return PhoneNumber|null a valid number for the non-geographical entity. Returns null when the metadata + * does not contain such information, or the country calling code passed in does not belong + * to a non-geographical entity. + */ + public function getExampleNumberForNonGeoEntity(int $countryCallingCode): ?PhoneNumber + { + $metadata = $this->getMetadataForNonGeographicalRegion($countryCallingCode); + if ($metadata !== null) { + // For geographical entities, fixed-line data is always present. However, for non-geographical + // entities, this is not the case, so we have to go through different types to find the + // example number. We don't check fixed-line or personal number since they aren't used by + // non-geographical entities (if this changes, a unit-test will catch this.) + /** @var PhoneNumberDesc[] $list */ + $list = [ + $metadata->getMobile(), + $metadata->getTollFree(), + $metadata->getSharedCost(), + $metadata->getVoip(), + $metadata->getVoicemail(), + $metadata->getUan(), + $metadata->getPremiumRate(), + ]; + foreach ($list as $desc) { + try { + if ($desc !== null && $desc->hasExampleNumber()) { + return $this->parse('+' . $countryCallingCode . $desc->getExampleNumber(), self::UNKNOWN_REGION); + } + } catch (NumberParseException) { + // noop + } + } + } + return null; + } + + + /** + * Takes two phone numbers and compares them for equality. + * + *

Returns EXACT_MATCH if the country_code, NSN, presence of a leading zero + * for Italian numbers and any extension present are the same. Returns NSN_MATCH + * if either or both has no region specified, and the NSNs and extensions are + * the same. Returns SHORT_NSN_MATCH if either or both has no region specified, + * or the region specified is the same, and one NSN could be a shorter version + * of the other number. This includes the case where one has an extension + * specified, and the other does not. Returns NO_MATCH otherwise. For example, + * the numbers +1 345 657 1234 and 657 1234 are a SHORT_NSN_MATCH. The numbers + * +1 345 657 1234 and 345 657 are a NO_MATCH. + * + * @param $firstNumberIn string|PhoneNumber First number to compare. If it is a + * string it can contain formatting, and can have country calling code specified + * with + at the start. + * @param $secondNumberIn string|PhoneNumber Second number to compare. If it is a + * string it can contain formatting, and can have country calling code specified + * with + at the start. + * @throws InvalidArgumentException + */ + public function isNumberMatch(PhoneNumber|string $firstNumberIn, PhoneNumber|string $secondNumberIn): MatchType + { + if (is_string($firstNumberIn) && is_string($secondNumberIn)) { + try { + $firstNumberAsProto = $this->parse($firstNumberIn, static::UNKNOWN_REGION); + return $this->isNumberMatch($firstNumberAsProto, $secondNumberIn); + } catch (NumberParseException $e) { + if ($e->getErrorType() === NumberParseException::INVALID_COUNTRY_CODE) { + try { + $secondNumberAsProto = $this->parse($secondNumberIn, static::UNKNOWN_REGION); + return $this->isNumberMatch($secondNumberAsProto, $firstNumberIn); + } catch (NumberParseException $e2) { + if ($e2->getErrorType() === NumberParseException::INVALID_COUNTRY_CODE) { + try { + $firstNumberProto = new PhoneNumber(); + $secondNumberProto = new PhoneNumber(); + $this->parseHelper($firstNumberIn, null, false, false, $firstNumberProto); + $this->parseHelper($secondNumberIn, null, false, false, $secondNumberProto); + return $this->isNumberMatch($firstNumberProto, $secondNumberProto); + } catch (NumberParseException) { + // Fall through and return MatchType::NOT_A_NUMBER + } + } + } + } + } + return MatchType::NOT_A_NUMBER; + } + if ($firstNumberIn instanceof PhoneNumber && is_string($secondNumberIn)) { + try { + $secondNumberAsProto = $this->parse($secondNumberIn, static::UNKNOWN_REGION); + return $this->isNumberMatch($firstNumberIn, $secondNumberAsProto); + } catch (NumberParseException $e) { + if ($e->getErrorType() === NumberParseException::INVALID_COUNTRY_CODE) { + // The second number has no country calling code. EXACT_MATCH is no longer possible. + // We parse it as if the region was the same as that for the first number, and if + // EXACT_MATCH is returned, we replace this with NSN_MATCH. + $firstNumberRegion = $this->getRegionCodeForCountryCode($firstNumberIn->getCountryCode()); + try { + if ($firstNumberRegion !== static::UNKNOWN_REGION) { + $secondNumberWithFirstNumberRegion = $this->parse($secondNumberIn, $firstNumberRegion); + $match = $this->isNumberMatch($firstNumberIn, $secondNumberWithFirstNumberRegion); + if ($match === MatchType::EXACT_MATCH) { + return MatchType::NSN_MATCH; + } + return $match; + } + + // If the first number didn't have a valid country calling code, then we parse the + // second number without one as well. + $secondNumberProto = new PhoneNumber(); + $this->parseHelper($secondNumberIn, null, false, false, $secondNumberProto); + return $this->isNumberMatch($firstNumberIn, $secondNumberProto); + } catch (NumberParseException) { + // Fall-through to return NOT_A_NUMBER. + } + } + } + } + if ($firstNumberIn instanceof PhoneNumber && $secondNumberIn instanceof PhoneNumber) { + // We only care about the fields that uniquely define a number, so we copy these across + // explicitly. + $firstNumber = self::copyCoreFieldsOnly($firstNumberIn); + $secondNumber = self::copyCoreFieldsOnly($secondNumberIn); + + // Early exit if both had extensions and these are different. + if ($firstNumber->hasExtension() && $secondNumber->hasExtension() && + $firstNumber->getExtension() !== $secondNumber->getExtension() + ) { + return MatchType::NO_MATCH; + } + + $firstNumberCountryCode = $firstNumber->getCountryCode(); + $secondNumberCountryCode = $secondNumber->getCountryCode(); + // Both had country_code specified. + if ($firstNumberCountryCode !== 0 && $secondNumberCountryCode !== 0) { + if ($firstNumber->equals($secondNumber)) { + return MatchType::EXACT_MATCH; + } + + if ($firstNumberCountryCode === $secondNumberCountryCode && + $this->isNationalNumberSuffixOfTheOther($firstNumber, $secondNumber)) { + // A SHORT_NSN_MATCH occurs if there is a difference because of the presence or absence of + // an 'Italian leading zero', the presence or absence of an extension, or one NSN being a + // shorter variant of the other. + return MatchType::SHORT_NSN_MATCH; + } + // This is not a match. + return MatchType::NO_MATCH; + } + // Checks cases where one or both country_code fields were not specified. To make equality + // checks easier, we first set the country_code fields to be equal. + $firstNumber->setCountryCode($secondNumberCountryCode); + // If all else was the same, then this is an NSN_MATCH. + if ($firstNumber->equals($secondNumber)) { + return MatchType::NSN_MATCH; + } + if ($this->isNationalNumberSuffixOfTheOther($firstNumber, $secondNumber)) { + return MatchType::SHORT_NSN_MATCH; + } + return MatchType::NO_MATCH; + } + return MatchType::NOT_A_NUMBER; + } + + /** + * Returns true when one national number is the suffix of the other or both are the same. + */ + protected function isNationalNumberSuffixOfTheOther(PhoneNumber $firstNumber, PhoneNumber $secondNumber): bool + { + $firstNumberNationalNumber = trim((string) $firstNumber->getNationalNumber()); + $secondNumberNationalNumber = trim((string) $secondNumber->getNationalNumber()); + + return str_ends_with($firstNumberNationalNumber, $secondNumberNationalNumber) || str_ends_with($secondNumberNationalNumber, $firstNumberNationalNumber); + } + + /** + * Returns true if the supplied region supports mobile number portability. Returns false for + * invalid, unknown or regions that don't support mobile number portability. + * + * @param string $regionCode the region for which we want to know whether it supports mobile number + * portability or not. + */ + public function isMobileNumberPortableRegion(string $regionCode): bool + { + $metadata = $this->getMetadataForRegion($regionCode); + if ($metadata === null) { + return false; + } + + return $metadata->isMobileNumberPortableRegion(); + } + + /** + * Check whether a phone number is a possible number given a number in the form of a string, and + * the region where the number could be dialed from. It provides a more lenient check than + * {@see isValidNumber}. See {@see isPossibleNumber(PhoneNumber)} for details. + * + * Convenience wrapper around {@see isPossibleNumberWithReason}. Instead of returning the reason + * for failure, this method returns a bool value. + * For failure, this method returns true if the number is either a possible fully-qualified number + * (containing the area code and country code), or if the number could be a possible local number + * (with a country code, but missing an area code). Local numbers are considered possible if they + * could be possibly dialled in this format: if the area code is needed for a call to connect, the + * number is not considered possible without it. + * + * Note: There are two ways to call this method. + * + * isPossibleNumber(PhoneNumber $numberObject) + * isPossibleNumber(string '+441174960126', string 'GB') + * + * @param string|PhoneNumber $number the number that needs to be checked, in the form of a string + * @param string|null $regionDialingFrom the region that we are expecting the number to be dialed from. + * Note this is different from the region where the number belongs. For example, the number + * +1 650 253 0000 is a number that belongs to US. When written in this form, it can be + * dialed from any region. When it is written as 00 1 650 253 0000, it can be dialed from any + * region which uses an international dialling prefix of 00. When it is written as + * 650 253 0000, it can only be dialed from within the US, and when written as 253 0000, it + * can only be dialed from within a smaller area in the US (Mountain View, CA, to be more + * specific). + * @return bool true if the number is possible + */ + public function isPossibleNumber(PhoneNumber|string $number, ?string $regionDialingFrom = null): bool + { + if (is_string($number)) { + try { + return $this->isPossibleNumber($this->parse($number, $regionDialingFrom)); + } catch (NumberParseException) { + return false; + } + } else { + $result = $this->isPossibleNumberWithReason($number); + return $result === ValidationResult::IS_POSSIBLE + || $result === ValidationResult::IS_POSSIBLE_LOCAL_ONLY; + } + } + + + /** + * Check whether a phone number is a possible number. It provides a more lenient check than + * {@see isValidNumber} in the following sense: + *

    + *
  1. It only checks the length of phone numbers. In particular, it doesn't check starting + * digits of the number. + *
  2. It doesn't attempt to figure out the type of the number, but uses general rules which + * applies to all types of phone numbers in a region. Therefore, it is much faster than + * isValidNumber. + *
  3. For some numbers (particularly fixed-line), many regions have the concept of area code, + * which together with subscriber number constitute the national significant number. It is + * sometimes okay to dial only the subscriber number when dialing in the same area. This + * function will return IS_POSSIBLE_LOCAL_ONLY if the subscriber-number-only version is + * passed in. On the other hand, because isValidNumber validates using information on both + * starting digits (for fixed line numbers, that would most likely be area codes) and + * length (obviously includes the length of area codes for fixed line numbers), it will + * return false for the subscriber-number-only version. + *
+ * + * There is a known issue with this + * method: if a number is possible only in a certain region among several regions that share the + * same country calling code, this method will consider only the "main" region. For example, + * +1310xxxx are valid numbers in Canada. However, they are not possible in the US. As a result, + * this method will return IS_POSSIBLE_LOCAL_ONLY for +1310xxxx. + * + * @param PhoneNumber $number the number that needs to be checked + * @return ValidationResult object which indicates whether the number is possible + */ + public function isPossibleNumberWithReason(PhoneNumber $number): ValidationResult + { + return $this->isPossibleNumberForTypeWithReason($number, PhoneNumberType::UNKNOWN); + } + + /** + * Check whether a phone number is a possible number of a particular type. For types that don't + * exist in a particular region, this will return a result that isn't so useful; it is recommended + * that you use {@see getSupportedTypesForRegion} or {@see getSupportedTypesForNonGeoEntity} + * respectively before calling this method to determine whether you should call it for this number + * at all. + * + * This provides a more lenient check than {@see isValidNumber} in the following sense: + * + *
    + *
  1. It only checks the length of phone numbers. In particular, it doesn't check starting + * digits of the number. + *
  2. For some numbers (particularly fixed-line), many regions have the concept of area code, + * which together with subscriber number constitute the national significant number. It is + * sometimes okay to dial only the subscriber number when dialing in the same area. This + * function will return IS_POSSIBLE_LOCAL_ONLY if the subscriber-number-only version is + * passed in. On the other hand, because isValidNumber validates using information on both + * starting digits (for fixed line numbers, that would most likely be area codes) and + * length (obviously includes the length of area codes for fixed line numbers), it will + * return false for the subscriber-number-only version. + *
+ * + * There is a known issue with this + * method: if a number is possible only in a certain region among several regions that share the + * same country calling code, this method will consider only the "main" region. For example, + * +1310xxxx are valid numbers in Canada. However, they are not possible in the US. As a result, + * this method will return IS_POSSIBLE_LOCAL_ONLY for +1310xxxx. + * + * @param PhoneNumber $number the number that needs to be checked + * @param PhoneNumberType $type the PhoneNumberType we are interested in + * @return ValidationResult object which indicates whether the number is possible + */ + public function isPossibleNumberForTypeWithReason(PhoneNumber $number, PhoneNumberType $type): ValidationResult + { + $nationalNumber = $this->getNationalSignificantNumber($number); + $countryCode = $number->getCountryCode(); + + // Note: For regions that share a country calling code, like NANPA numbers, we just use the + // rules from the default region (US in this case) since the getRegionCodeForNumber will not + // work if the number is possible but not valid. There is in fact one country calling code (290) + // where the possible number pattern differs between various regions (Saint Helena and Tristan + // da Cuñha), but this is handled by putting all possible lengths for any country with this + // country calling code in the metadata for the default region in this case. + if (!$this->hasValidCountryCallingCode($countryCode)) { + return ValidationResult::INVALID_COUNTRY_CODE; + } + + $regionCode = $this->getRegionCodeForCountryCode($countryCode); + // Metadata cannot be null because the country calling code is valid. + $metadata = $this->getMetadataForRegionOrCallingCode($countryCode, $regionCode); + return $this->testNumberLength($nationalNumber, $metadata, $type); + } + + /** + * Attempts to extract a valid number from a phone number that is too long to be valid, and resets + * the PhoneNumber object passed in to that valid version. If no valid number could be extracted, + * the PhoneNumber object passed in will not be modified. + * + * @param PhoneNumber $number a PhoneNumber object which contains a number that is too long to be valid. + * @return bool true if a valid phone number can be successfully extracted. + */ + public function truncateTooLongNumber(PhoneNumber $number): bool + { + if ($this->isValidNumber($number)) { + return true; + } + $numberCopy = new PhoneNumber(); + $numberCopy->mergeFrom($number); + $nationalNumber = $number->getNationalNumber(); + do { + $nationalNumber = (string) floor((int) $nationalNumber / 10); + $numberCopy->setNationalNumber($nationalNumber); + if ($nationalNumber === '0' || $this->isPossibleNumberWithReason($numberCopy) === ValidationResult::TOO_SHORT) { + return false; + } + } while (!$this->isValidNumber($numberCopy)); + $number->setNationalNumber($nationalNumber); + return true; + } +} diff --git a/3rdparty/giggsey/libphonenumber-for-php-lite/src/RegexBasedMatcher.php b/3rdparty/giggsey/libphonenumber-for-php-lite/src/RegexBasedMatcher.php new file mode 100644 index 00000000..a61ea2f5 --- /dev/null +++ b/3rdparty/giggsey/libphonenumber-for-php-lite/src/RegexBasedMatcher.php @@ -0,0 +1,42 @@ +getNationalNumberPattern(); + + // We don't want to consider it a prefix match when matching non-empty input against an empty + // pattern + + if ($nationalNumberPattern === '') { + return false; + } + + return $this->match($number, $nationalNumberPattern, $allowPrefixMatch); + } + + private function match(string $number, string $pattern, bool $allowPrefixMatch): bool + { + $matcher = new Matcher($pattern, $number); + + if (!$matcher->lookingAt()) { + return false; + } + + return $matcher->matches() ? true : $allowPrefixMatch; + } +} diff --git a/3rdparty/giggsey/libphonenumber-for-php-lite/src/ShortNumberCost.php b/3rdparty/giggsey/libphonenumber-for-php-lite/src/ShortNumberCost.php new file mode 100644 index 00000000..d540423e --- /dev/null +++ b/3rdparty/giggsey/libphonenumber-for-php-lite/src/ShortNumberCost.php @@ -0,0 +1,17 @@ + + */ + protected array $countryCallingCodeToRegionCodeMap = []; + protected const REGIONS_WHERE_EMERGENCY_NUMBERS_MUST_BE_EXACT = [ + 'BR', + 'CL', + 'NI', + ]; + + protected function __construct( + protected MatcherAPIInterface $matcherAPI, + protected MetadataSourceInterface $metadataSource = new MultiFileMetadataSourceImpl(__NAMESPACE__ . '\data\ShortNumberMetadata_'), + ) { + // TODO: Create ShortNumberInfo for a given map + $this->countryCallingCodeToRegionCodeMap = CountryCodeToRegionCodeMap::COUNTRY_CODE_TO_REGION_CODE_MAP; + + // Initialise PhoneNumberUtil to make sure regex's are setup correctly + PhoneNumberUtil::getInstance(); + } + + /** + * Returns the singleton instance of ShortNumberInfo + */ + public static function getInstance(): ShortNumberInfo + { + if (!isset(static::$instance)) { + static::$instance = new self(new RegexBasedMatcher()); + } + + return static::$instance; + } + + public static function resetInstance(): void + { + static::$instance = null; + } + + /** + * Returns a list with the region codes that match the specific country calling code. For + * non-geographical country calling codes, the region code 001 is returned. Also, in the case + * of no region code being found, an empty list is returned. + * + * @return string[] + */ + protected function getRegionCodesForCountryCode(int $countryCallingCode): array + { + return $this->countryCallingCodeToRegionCodeMap[$countryCallingCode] ?? []; + } + + /** + * Helper method to check that the country calling code of the number matches the region it's + * being dialed from. + */ + protected function regionDialingFromMatchesNumber(PhoneNumber $number, ?string $regionDialingFrom): bool + { + if ($regionDialingFrom === null || $regionDialingFrom === '') { + return false; + } + + $regionCodes = $this->getRegionCodesForCountryCode($number->getCountryCode()); + + return in_array(strtoupper($regionDialingFrom), $regionCodes, true); + } + + /** + * @return string[] + */ + public function getSupportedRegions(): array + { + return ShortNumbersRegionCodeSet::SHORT_NUMBERS_REGION_CODE_SET; + } + + /** + * Gets a valid short number for the specified region. + * + * @param $regionCode String the region for which an example short number is needed + * @return string a valid short number for the specified region. Returns an empty string when the + * metadata does not contain such information. + */ + public function getExampleShortNumber(string $regionCode): string + { + $phoneMetadata = $this->getMetadataForRegion($regionCode); + if ($phoneMetadata === null) { + return ''; + } + + /** @var PhoneNumberDesc $desc */ + $desc = $phoneMetadata->getShortCode(); + if ($desc !== null && $desc->hasExampleNumber()) { + return $desc->getExampleNumber(); + } + return ''; + } + + public function getMetadataForRegion(string $regionCode): ?PhoneMetadata + { + $regionCode = strtoupper($regionCode); + + if (!in_array($regionCode, ShortNumbersRegionCodeSet::SHORT_NUMBERS_REGION_CODE_SET, true)) { + return null; + } + + try { + return $this->metadataSource->getMetadataForRegion($regionCode); + } catch (RuntimeException) { + return null; + } + } + + /** + * Gets a valid short number for the specified cost category. + * + * @param string $regionCode the region for which an example short number is needed + * @param ShortNumberCost $cost the cost category of number that is needed + * @return string a valid short number for the specified region and cost category. Returns an empty string + * when the metadata does not contain such information, or the cost is UNKNOWN_COST. + */ + public function getExampleShortNumberForCost(string $regionCode, ShortNumberCost $cost): string + { + $phoneMetadata = $this->getMetadataForRegion($regionCode); + if ($phoneMetadata === null) { + return ''; + } + + $desc = null; + switch ($cost) { + case ShortNumberCost::TOLL_FREE: + $desc = $phoneMetadata->getTollFree(); + break; + case ShortNumberCost::STANDARD_RATE: + $desc = $phoneMetadata->getStandardRate(); + break; + case ShortNumberCost::PREMIUM_RATE: + $desc = $phoneMetadata->getPremiumRate(); + break; + default: + // UNKNOWN_COST numbers are computed by the process of elimination from the other cost categories + break; + } + + if ($desc !== null && $desc->hasExampleNumber()) { + return $desc->getExampleNumber(); + } + + return ''; + } + + /** + * Returns true if the given number, exactly as dialed, might be used to connect to an emergency + * service in the given region. + *

+ * This method accepts a string, rather than a PhoneNumber, because it needs to distinguish + * cases such as "+1 911" and "911", where the former may not connect to an emergency service in + * all cases but the latter would. This method takes into account cases where the number might + * contain formatting, or might have additional digits appended (when it is okay to do that in + * the specified region). + * + * @param string $number the phone number to test + * @param string $regionCode the region where the phone number if being dialled + * @return bool whether the number might be used to connect to an emergency service in the given region + */ + public function connectsToEmergencyNumber(string $number, string $regionCode): bool + { + return $this->matchesEmergencyNumberHelper($number, $regionCode, true /* allows prefix match */); + } + + protected function matchesEmergencyNumberHelper(string $number, string $regionCode, bool $allowPrefixMatch): bool + { + $number = PhoneNumberUtil::extractPossibleNumber($number); + $matcher = new Matcher(PhoneNumberUtil::PLUS_CHARS_PATTERN, $number); + if ($matcher->lookingAt()) { + // Returns false if the number starts with a plus sign. We don't believe dialing the country + // code before emergency numbers (e.g. +1911) works, but later, if that proves to work, we can + // add additional logic here to handle it. + return false; + } + + $metadata = $this->getMetadataForRegion($regionCode); + if ($metadata === null || !$metadata->hasEmergency()) { + return false; + } + + $normalizedNumber = PhoneNumberUtil::normalizeDigitsOnly($number); + $emergencyDesc = $metadata->getEmergency(); + + $allowPrefixMatchForRegion = ( + $allowPrefixMatch + && !in_array(strtoupper($regionCode), static::REGIONS_WHERE_EMERGENCY_NUMBERS_MUST_BE_EXACT, true) + ); + + return $this->matcherAPI->matchNationalNumber($normalizedNumber, $emergencyDesc, $allowPrefixMatchForRegion); + } + + /** + * Given a valid short number, determines whether it is carrier-specific (however, nothing is + * implied about its validity). Carrier-specific numbers may connect to a different end-point, or + * not connect at all, depending on the user's carrier. If it is important that the number is + * valid, then its validity must first be checked using {@see isValidShortNumber} or + * {@see isValidShortNumberForRegion}. + * + * @param PhoneNumber $number the valid short number to check + * @return bool whether the short number is carrier-specific, assuming the input was a valid short + * number + */ + public function isCarrierSpecific(PhoneNumber $number): bool + { + $regionCodes = $this->getRegionCodesForCountryCode($number->getCountryCode()); + $regionCode = $this->getRegionCodeForShortNumberFromRegionList($number, $regionCodes); + $nationalNumber = $this->getNationalSignificantNumber($number); + $phoneMetadata = $this->getMetadataForRegion($regionCode); + + return ($phoneMetadata !== null) && $this->matchesPossibleNumberAndNationalNumber( + $nationalNumber, + $phoneMetadata->getCarrierSpecific() + ); + } + + /** + * Given a valid short number, determines whether it is carrier-specific when dialed from the + * given region (however, nothing is implied about its validity). Carrier-specific numbers may + * connect to a different end-point, or not connect at all, depending on the user's carrier. If + * it is important that the number is valid, then its validity must first be checked using + * {@see isValidShortNumber} or {@see isValidShortNumberForRegion}. Returns false if the + * number doesn't match the region provided. + * @param PhoneNumber $number The valid short number to check + * @param string $regionDialingFrom The region from which the number is dialed + * @return bool Whether the short number is carrier-specific in the provided region, assuming the + * input was a valid short number + */ + public function isCarrierSpecificForRegion(PhoneNumber $number, string $regionDialingFrom): bool + { + if (!$this->regionDialingFromMatchesNumber($number, $regionDialingFrom)) { + return false; + } + + $nationalNumber = $this->getNationalSignificantNumber($number); + $phoneMetadata = $this->getMetadataForRegion($regionDialingFrom); + + return ($phoneMetadata !== null) + && $this->matchesPossibleNumberAndNationalNumber($nationalNumber, $phoneMetadata->getCarrierSpecific()); + } + + /** + * Given a valid short number, determines whether it is an SMS service (however, nothing is + * implied about its validity). An SMS service is where the primary or only intended usage is to + * receive and/or send text messages (SMSs). This includes MMS as MMS numbers downgrade to SMS if + * the other party isn't MMS-capable. If it is important that the number is valid, then its + * validity must first be checked using {@see isValidShortNumber} or {@see isValidShortNumberForRegion}. + * Returns false if the number doesn't match the region provided. + * + * @param PhoneNumber $number The valid short number to check + * @param string $regionDialingFrom The region from which the number is dialed + * @return bool Whether the short number is an SMS service in the provided region, assuming the input + * was a valid short number. + */ + public function isSmsServiceForRegion(PhoneNumber $number, string $regionDialingFrom): bool + { + if (!$this->regionDialingFromMatchesNumber($number, $regionDialingFrom)) { + return false; + } + + $phoneMetadata = $this->getMetadataForRegion($regionDialingFrom); + + return ($phoneMetadata !== null) + && $this->matchesPossibleNumberAndNationalNumber( + $this->getNationalSignificantNumber($number), + $phoneMetadata->getSmsServices() + ); + } + + /** + * Helper method to get the region code for a given phone number, from a list of possible region + * codes. If the list contains more than one region, the first region for which the number is + * valid is returned. + * + * @param string[] $regionCodes + * @return string|null Region Code (or null if none are found) + */ + protected function getRegionCodeForShortNumberFromRegionList(PhoneNumber $number, array $regionCodes): ?string + { + if (count($regionCodes) === 0) { + return null; + } + + if (count($regionCodes) === 1) { + return $regionCodes[0]; + } + + $nationalNumber = $this->getNationalSignificantNumber($number); + + foreach ($regionCodes as $regionCode) { + $phoneMetadata = $this->getMetadataForRegion($regionCode); + if ($phoneMetadata !== null + && $this->matchesPossibleNumberAndNationalNumber($nationalNumber, $phoneMetadata->getShortCode()) + ) { + // The number is valid for this region. + return $regionCode; + } + } + return null; + } + + /** + * Check whether a short number is a possible number. If a country calling code is shared by + * multiple regions, this returns true if it's possible in any of them. This provides a more + * lenient check than {@see isValidShortNumber}. See {@see isPossibleShortNumberForRegion} + * for details. + * + * @param $number PhoneNumber the short number to check + * @return bool whether the number is a possible short number + */ + public function isPossibleShortNumber(PhoneNumber $number): bool + { + $regionCodes = $this->getRegionCodesForCountryCode($number->getCountryCode()); + $shortNumberLength = strlen($this->getNationalSignificantNumber($number)); + + foreach ($regionCodes as $region) { + $phoneMetadata = $this->getMetadataForRegion($region); + + if ($phoneMetadata === null) { + continue; + } + + if (in_array($shortNumberLength, $phoneMetadata->getGeneralDesc()->getPossibleLength(), true)) { + return true; + } + } + + return false; + } + + /** + * Check whether a short number is a possible number when dialled from a region, given the number + * in the form of a string, and the region where the number is dialled from. This provides a more + * lenient check than {@see isValidShortNumber}. + * + * @param PhoneNumber $shortNumber The short number to check + * @param string $regionDialingFrom Region dialing From + * @return bool whether the number is a possible short number + */ + public function isPossibleShortNumberForRegion(PhoneNumber $shortNumber, string $regionDialingFrom): bool + { + if (!$this->regionDialingFromMatchesNumber($shortNumber, $regionDialingFrom)) { + return false; + } + + $phoneMetadata = $this->getMetadataForRegion($regionDialingFrom); + + if ($phoneMetadata === null) { + return false; + } + + $numberLength = strlen($this->getNationalSignificantNumber($shortNumber)); + return in_array($numberLength, $phoneMetadata->getGeneralDesc()->getPossibleLength(), true); + } + + /** + * Tests whether a short number matches a valid pattern. If a country calling code is shared by + * multiple regions, this returns true if it's valid in any of them. Note that this doesn't verify + * the number is actually in use, which is impossible to tell by just looking at the number + * itself. See {@see isValidShortNumberForRegion(PhoneNumber, String)} for details. + * + * @param $number PhoneNumber the short number for which we want to test the validity + * @return bool whether the short number matches a valid pattern + */ + public function isValidShortNumber(PhoneNumber $number): bool + { + $regionCodes = $this->getRegionCodesForCountryCode($number->getCountryCode()); + $regionCode = $this->getRegionCodeForShortNumberFromRegionList($number, $regionCodes); + if ($regionCode !== null && count($regionCodes) > 1) { + // If a matching region had been found for the phone number from among two or more regions, + // then we have already implicitly verified its validity for that region. + return true; + } + + return $this->isValidShortNumberForRegion($number, $regionCode); + } + + /** + * Tests whether a short number matches a valid pattern in a region. Note that this doesn't verify + * the number is actually in use, which is impossible to tell by just looking at the number + * itself. + * + * @param PhoneNumber $number The Short number for which we want to test the validity + * @param string|null $regionDialingFrom the region from which the number is dialed + * @return bool whether the short number matches a valid pattern + */ + public function isValidShortNumberForRegion(PhoneNumber $number, ?string $regionDialingFrom): bool + { + if (!$this->regionDialingFromMatchesNumber($number, $regionDialingFrom)) { + return false; + } + $phoneMetadata = $this->getMetadataForRegion($regionDialingFrom); + + if ($phoneMetadata === null) { + return false; + } + + $shortNumber = $this->getNationalSignificantNumber($number); + + $generalDesc = $phoneMetadata->getGeneralDesc(); + + if (!$this->matchesPossibleNumberAndNationalNumber($shortNumber, $generalDesc)) { + return false; + } + + $shortNumberDesc = $phoneMetadata->getShortCode(); + + return $this->matchesPossibleNumberAndNationalNumber($shortNumber, $shortNumberDesc); + } + + /** + * Gets the expected cost category of a short number when dialled from a region (however, nothing is + * implied about its validity). If it is important that the number is valid, then its validity + * must first be checked using {@link isValidShortNumberForRegion}. Note that emergency numbers + * are always considered toll-free. + * Example usage: + *

{@code
+     * $shortInfo = ShortNumberInfo::getInstance();
+     * $shortNumber = PhoneNumberUtil::parse("110", "US);
+     * $regionCode = "FR";
+     * if ($shortInfo->isValidShortNumberForRegion($shortNumber, $regionCode)) {
+     *     $cost = $shortInfo->getExpectedCostForRegion($shortNumber, $regionCode);
+     *    // Do something with the cost information here.
+     * }}
+ * + * @param PhoneNumber $number the short number for which we want to know the expected cost category, + * as a string + * @param string $regionDialingFrom the region from which the number is dialed + * @return ShortNumberCost the expected cost category for that region of the short number. Returns ShortNumberCost::UNKNOWN_COST if + * the number does not match a cost category. Note that an invalid number may match any cost + * category. + */ + public function getExpectedCostForRegion(PhoneNumber $number, string $regionDialingFrom): ShortNumberCost + { + if (!$this->regionDialingFromMatchesNumber($number, $regionDialingFrom)) { + return ShortNumberCost::UNKNOWN_COST; + } + // Note that regionDialingFrom may be null, in which case phoneMetadata will also be null. + $phoneMetadata = $this->getMetadataForRegion($regionDialingFrom); + if ($phoneMetadata === null) { + return ShortNumberCost::UNKNOWN_COST; + } + + $shortNumber = $this->getNationalSignificantNumber($number); + + // The possible lengths are not present for a particular sub-type if they match the general + // description; for this reason, we check the possible lengths against the general description + // first to allow an early exit if possible. + if (!in_array(strlen($shortNumber), $phoneMetadata->getGeneralDesc()->getPossibleLength(), true)) { + return ShortNumberCost::UNKNOWN_COST; + } + + // The cost categories are tested in order of decreasing expense, since if for some reason the + // patterns overlap the most expensive matching cost category should be returned. + if ($this->matchesPossibleNumberAndNationalNumber($shortNumber, $phoneMetadata->getPremiumRate())) { + return ShortNumberCost::PREMIUM_RATE; + } + + if ($this->matchesPossibleNumberAndNationalNumber($shortNumber, $phoneMetadata->getStandardRate())) { + return ShortNumberCost::STANDARD_RATE; + } + + if ($this->matchesPossibleNumberAndNationalNumber($shortNumber, $phoneMetadata->getTollFree())) { + return ShortNumberCost::TOLL_FREE; + } + + if ($this->isEmergencyNumber($shortNumber, $regionDialingFrom)) { + // Emergency numbers are implicitly toll-free. + return ShortNumberCost::TOLL_FREE; + } + + return ShortNumberCost::UNKNOWN_COST; + } + + /** + * Gets the expected cost category of a short number (however, nothing is implied about its + * validity). If the country calling code is unique to a region, this method behaves exactly the + * same as {@see getExpectedCostForRegion(PhoneNumber, String)}. However, if the country calling + * code is shared by multiple regions, then it returns the highest cost in the sequence + * PREMIUM_RATE, UNKNOWN_COST, STANDARD_RATE, TOLL_FREE. The reason for the position of + * UNKNOWN_COST in this order is that if a number is UNKNOWN_COST in one region but STANDARD_RATE + * or TOLL_FREE in another, its expected cost cannot be estimated as one of the latter since it + * might be a PREMIUM_RATE number. + * + *

+ * For example, if a number is STANDARD_RATE in the US, but TOLL_FREE in Canada, the expected + * cost returned by this method will be STANDARD_RATE, since the NANPA countries share the same + * country calling code. + *

+ * + * Note: If the region from which the number is dialed is known, it is highly preferable to call + * {@see getExpectedCostForRegion(PhoneNumber, String)} instead. + * + * @param PhoneNumber $number the short number for which we want to know the expected cost category + * @return ShortNumberCost the highest expected cost category of the short number in the region(s) with the given + * country calling code + */ + public function getExpectedCost(PhoneNumber $number): ShortNumberCost + { + $regionCodes = $this->getRegionCodesForCountryCode($number->getCountryCode()); + + if ($regionCodes === []) { + return ShortNumberCost::UNKNOWN_COST; + } + if (count($regionCodes) === 1) { + return $this->getExpectedCostForRegion($number, $regionCodes[0]); + } + $cost = ShortNumberCost::TOLL_FREE; + foreach ($regionCodes as $regionCode) { + $costForRegion = $this->getExpectedCostForRegion($number, $regionCode); + switch ($costForRegion) { + case ShortNumberCost::PREMIUM_RATE: + return ShortNumberCost::PREMIUM_RATE; + + case ShortNumberCost::UNKNOWN_COST: + $cost = ShortNumberCost::UNKNOWN_COST; + break; + + case ShortNumberCost::STANDARD_RATE: + if ($cost !== ShortNumberCost::UNKNOWN_COST) { + $cost = ShortNumberCost::STANDARD_RATE; + } + break; + case ShortNumberCost::TOLL_FREE: + // Do nothing + break; + } + } + return $cost; + } + + /** + * Returns true if the given number exactly matches an emergency service number in the given + * region. + *

+ * This method takes into account cases where the number might contain formatting, but doesn't + * allow additional digits to be appended. Note that {@code isEmergencyNumber(number, region)} + * implies {@code connectsToEmergencyNumber(number, region)}. + * + * @param string $number the phone number to test + * @param string $regionCode the region where the phone number is being dialled + * @return bool whether the number exactly matches an emergency services number in the given region + */ + public function isEmergencyNumber(string $number, string $regionCode): bool + { + return $this->matchesEmergencyNumberHelper($number, $regionCode, false /* doesn't allow prefix match */); + } + + /** + * Gets the national significant number of the a phone number. Note a national significant number + * doesn't contain a national prefix or any formatting. + *

+ * This is a temporary duplicate of the {@code getNationalSignificantNumber} method from + * {@code PhoneNumberUtil}. Ultimately a canonical static version should exist in a separate + * utility class (to prevent {@code ShortNumberInfo} needing to depend on PhoneNumberUtil). + * + * @param PhoneNumber $number the phone number for which the national significant number is needed + * @return string the national significant number of the PhoneNumber object passed in + */ + protected function getNationalSignificantNumber(PhoneNumber $number): string + { + // If leading zero(s) have been set, we prefix this now. Note this is not a national prefix. + $nationalNumber = ''; + if ($number->isItalianLeadingZero()) { + $zeros = str_repeat('0', $number->getNumberOfLeadingZeros()); + $nationalNumber .= $zeros; + } + + $nationalNumber .= $number->getNationalNumber(); + + return $nationalNumber; + } + + /** + * TODO: Once we have benchmarked ShortnumberInfo, consider if it is worth keeping + * this performance optimization. + */ + protected function matchesPossibleNumberAndNationalNumber(string $number, PhoneNumberDesc $numberDesc): bool + { + if (count($numberDesc->getPossibleLength()) > 0 && !in_array(strlen($number), $numberDesc->getPossibleLength(), true)) { + return false; + } + + return $this->matcherAPI->matchNationalNumber($number, $numberDesc, false); + } +} diff --git a/3rdparty/giggsey/libphonenumber-for-php-lite/src/ShortNumbersRegionCodeSet.php b/3rdparty/giggsey/libphonenumber-for-php-lite/src/ShortNumbersRegionCodeSet.php new file mode 100644 index 00000000..4915b098 --- /dev/null +++ b/3rdparty/giggsey/libphonenumber-for-php-lite/src/ShortNumbersRegionCodeSet.php @@ -0,0 +1,266 @@ + +Copyright (c) 2012 Jeremy Lindblom +Copyright (c) 2014 Graham Campbell +Copyright (c) 2015 Márk Sági-Kazár +Copyright (c) 2015 Tobias Schultze +Copyright (c) 2016 Tobias Nyholm +Copyright (c) 2016 George Mponos + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in +all copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +THE SOFTWARE. diff --git a/3rdparty/guzzlehttp/guzzle/src/BodySummarizer.php b/3rdparty/guzzlehttp/guzzle/src/BodySummarizer.php new file mode 100644 index 00000000..761506dd --- /dev/null +++ b/3rdparty/guzzlehttp/guzzle/src/BodySummarizer.php @@ -0,0 +1,28 @@ +truncateAt = $truncateAt; + } + + /** + * Returns a summarized message body. + */ + public function summarize(MessageInterface $message): ?string + { + return $this->truncateAt === null + ? Psr7\Message::bodySummary($message) + : Psr7\Message::bodySummary($message, $this->truncateAt); + } +} diff --git a/3rdparty/guzzlehttp/guzzle/src/BodySummarizerInterface.php b/3rdparty/guzzlehttp/guzzle/src/BodySummarizerInterface.php new file mode 100644 index 00000000..3e02e036 --- /dev/null +++ b/3rdparty/guzzlehttp/guzzle/src/BodySummarizerInterface.php @@ -0,0 +1,13 @@ + 'http://www.foo.com/1.0/', + * 'timeout' => 0, + * 'allow_redirects' => false, + * 'proxy' => '192.168.16.1:10' + * ]); + * + * Client configuration settings include the following options: + * + * - handler: (callable) Function that transfers HTTP requests over the + * wire. The function is called with a Psr7\Http\Message\RequestInterface + * and array of transfer options, and must return a + * GuzzleHttp\Promise\PromiseInterface that is fulfilled with a + * Psr7\Http\Message\ResponseInterface on success. + * If no handler is provided, a default handler will be created + * that enables all of the request options below by attaching all of the + * default middleware to the handler. + * - base_uri: (string|UriInterface) Base URI of the client that is merged + * into relative URIs. Can be a string or instance of UriInterface. + * - **: any request option + * + * @param array $config Client configuration settings. + * + * @see RequestOptions for a list of available request options. + */ + public function __construct(array $config = []) + { + if (!isset($config['handler'])) { + $config['handler'] = HandlerStack::create(); + } elseif (!\is_callable($config['handler'])) { + throw new InvalidArgumentException('handler must be a callable'); + } + + // Convert the base_uri to a UriInterface + if (isset($config['base_uri'])) { + $config['base_uri'] = Psr7\Utils::uriFor($config['base_uri']); + } + + $this->configureDefaults($config); + } + + /** + * @param string $method + * @param array $args + * + * @return PromiseInterface|ResponseInterface + * + * @deprecated Client::__call will be removed in guzzlehttp/guzzle:8.0. + */ + public function __call($method, $args) + { + if (\count($args) < 1) { + throw new InvalidArgumentException('Magic request methods require a URI and optional options array'); + } + + $uri = $args[0]; + $opts = $args[1] ?? []; + + return \substr($method, -5) === 'Async' + ? $this->requestAsync(\substr($method, 0, -5), $uri, $opts) + : $this->request($method, $uri, $opts); + } + + /** + * Asynchronously send an HTTP request. + * + * @param array $options Request options to apply to the given + * request and to the transfer. See \GuzzleHttp\RequestOptions. + */ + public function sendAsync(RequestInterface $request, array $options = []): PromiseInterface + { + // Merge the base URI into the request URI if needed. + $options = $this->prepareDefaults($options); + + return $this->transfer( + $request->withUri($this->buildUri($request->getUri(), $options), $request->hasHeader('Host')), + $options + ); + } + + /** + * Send an HTTP request. + * + * @param array $options Request options to apply to the given + * request and to the transfer. See \GuzzleHttp\RequestOptions. + * + * @throws GuzzleException + */ + public function send(RequestInterface $request, array $options = []): ResponseInterface + { + $options[RequestOptions::SYNCHRONOUS] = true; + + return $this->sendAsync($request, $options)->wait(); + } + + /** + * The HttpClient PSR (PSR-18) specify this method. + * + * {@inheritDoc} + */ + public function sendRequest(RequestInterface $request): ResponseInterface + { + $options[RequestOptions::SYNCHRONOUS] = true; + $options[RequestOptions::ALLOW_REDIRECTS] = false; + $options[RequestOptions::HTTP_ERRORS] = false; + + return $this->sendAsync($request, $options)->wait(); + } + + /** + * Create and send an asynchronous HTTP request. + * + * Use an absolute path to override the base path of the client, or a + * relative path to append to the base path of the client. The URL can + * contain the query string as well. Use an array to provide a URL + * template and additional variables to use in the URL template expansion. + * + * @param string $method HTTP method + * @param string|UriInterface $uri URI object or string. + * @param array $options Request options to apply. See \GuzzleHttp\RequestOptions. + */ + public function requestAsync(string $method, $uri = '', array $options = []): PromiseInterface + { + $options = $this->prepareDefaults($options); + // Remove request modifying parameter because it can be done up-front. + $headers = $options['headers'] ?? []; + $body = $options['body'] ?? null; + $version = $options['version'] ?? '1.1'; + // Merge the URI into the base URI. + $uri = $this->buildUri(Psr7\Utils::uriFor($uri), $options); + if (\is_array($body)) { + throw $this->invalidBody(); + } + $request = new Psr7\Request($method, $uri, $headers, $body, $version); + // Remove the option so that they are not doubly-applied. + unset($options['headers'], $options['body'], $options['version']); + + return $this->transfer($request, $options); + } + + /** + * Create and send an HTTP request. + * + * Use an absolute path to override the base path of the client, or a + * relative path to append to the base path of the client. The URL can + * contain the query string as well. + * + * @param string $method HTTP method. + * @param string|UriInterface $uri URI object or string. + * @param array $options Request options to apply. See \GuzzleHttp\RequestOptions. + * + * @throws GuzzleException + */ + public function request(string $method, $uri = '', array $options = []): ResponseInterface + { + $options[RequestOptions::SYNCHRONOUS] = true; + + return $this->requestAsync($method, $uri, $options)->wait(); + } + + /** + * Get a client configuration option. + * + * These options include default request options of the client, a "handler" + * (if utilized by the concrete client), and a "base_uri" if utilized by + * the concrete client. + * + * @param string|null $option The config option to retrieve. + * + * @return mixed + * + * @deprecated Client::getConfig will be removed in guzzlehttp/guzzle:8.0. + */ + public function getConfig(?string $option = null) + { + return $option === null + ? $this->config + : ($this->config[$option] ?? null); + } + + private function buildUri(UriInterface $uri, array $config): UriInterface + { + if (isset($config['base_uri'])) { + $uri = Psr7\UriResolver::resolve(Psr7\Utils::uriFor($config['base_uri']), $uri); + } + + if (isset($config['idn_conversion']) && ($config['idn_conversion'] !== false)) { + $idnOptions = ($config['idn_conversion'] === true) ? \IDNA_DEFAULT : $config['idn_conversion']; + $uri = Utils::idnUriConvert($uri, $idnOptions); + } + + return $uri->getScheme() === '' && $uri->getHost() !== '' ? $uri->withScheme('http') : $uri; + } + + /** + * Configures the default options for a client. + */ + private function configureDefaults(array $config): void + { + $defaults = [ + 'allow_redirects' => RedirectMiddleware::$defaultSettings, + 'http_errors' => true, + 'decode_content' => true, + 'verify' => true, + 'cookies' => false, + 'idn_conversion' => false, + ]; + + // Use the standard Linux HTTP_PROXY and HTTPS_PROXY if set. + + // We can only trust the HTTP_PROXY environment variable in a CLI + // process due to the fact that PHP has no reliable mechanism to + // get environment variables that start with "HTTP_". + if (\PHP_SAPI === 'cli' && ($proxy = Utils::getenv('HTTP_PROXY'))) { + $defaults['proxy']['http'] = $proxy; + } + + if ($proxy = Utils::getenv('HTTPS_PROXY')) { + $defaults['proxy']['https'] = $proxy; + } + + if ($noProxy = Utils::getenv('NO_PROXY')) { + $cleanedNoProxy = \str_replace(' ', '', $noProxy); + $defaults['proxy']['no'] = \explode(',', $cleanedNoProxy); + } + + $this->config = $config + $defaults; + + if (!empty($config['cookies']) && $config['cookies'] === true) { + $this->config['cookies'] = new CookieJar(); + } + + // Add the default user-agent header. + if (!isset($this->config['headers'])) { + $this->config['headers'] = ['User-Agent' => Utils::defaultUserAgent()]; + } else { + // Add the User-Agent header if one was not already set. + foreach (\array_keys($this->config['headers']) as $name) { + if (\strtolower($name) === 'user-agent') { + return; + } + } + $this->config['headers']['User-Agent'] = Utils::defaultUserAgent(); + } + } + + /** + * Merges default options into the array. + * + * @param array $options Options to modify by reference + */ + private function prepareDefaults(array $options): array + { + $defaults = $this->config; + + if (!empty($defaults['headers'])) { + // Default headers are only added if they are not present. + $defaults['_conditional'] = $defaults['headers']; + unset($defaults['headers']); + } + + // Special handling for headers is required as they are added as + // conditional headers and as headers passed to a request ctor. + if (\array_key_exists('headers', $options)) { + // Allows default headers to be unset. + if ($options['headers'] === null) { + $defaults['_conditional'] = []; + unset($options['headers']); + } elseif (!\is_array($options['headers'])) { + throw new InvalidArgumentException('headers must be an array'); + } + } + + // Shallow merge defaults underneath options. + $result = $options + $defaults; + + // Remove null values. + foreach ($result as $k => $v) { + if ($v === null) { + unset($result[$k]); + } + } + + return $result; + } + + /** + * Transfers the given request and applies request options. + * + * The URI of the request is not modified and the request options are used + * as-is without merging in default options. + * + * @param array $options See \GuzzleHttp\RequestOptions. + */ + private function transfer(RequestInterface $request, array $options): PromiseInterface + { + $request = $this->applyOptions($request, $options); + /** @var HandlerStack $handler */ + $handler = $options['handler']; + + try { + return P\Create::promiseFor($handler($request, $options)); + } catch (\Exception $e) { + return P\Create::rejectionFor($e); + } + } + + /** + * Applies the array of request options to a request. + */ + private function applyOptions(RequestInterface $request, array &$options): RequestInterface + { + $modify = [ + 'set_headers' => [], + ]; + + if (isset($options['headers'])) { + if (array_keys($options['headers']) === range(0, count($options['headers']) - 1)) { + throw new InvalidArgumentException('The headers array must have header name as keys.'); + } + $modify['set_headers'] = $options['headers']; + unset($options['headers']); + } + + if (isset($options['form_params'])) { + if (isset($options['multipart'])) { + throw new InvalidArgumentException('You cannot use ' + .'form_params and multipart at the same time. Use the ' + .'form_params option if you want to send application/' + .'x-www-form-urlencoded requests, and the multipart ' + .'option to send multipart/form-data requests.'); + } + $options['body'] = \http_build_query($options['form_params'], '', '&'); + unset($options['form_params']); + // Ensure that we don't have the header in different case and set the new value. + $options['_conditional'] = Psr7\Utils::caselessRemove(['Content-Type'], $options['_conditional']); + $options['_conditional']['Content-Type'] = 'application/x-www-form-urlencoded'; + } + + if (isset($options['multipart'])) { + $options['body'] = new Psr7\MultipartStream($options['multipart']); + unset($options['multipart']); + } + + if (isset($options['json'])) { + $options['body'] = Utils::jsonEncode($options['json']); + unset($options['json']); + // Ensure that we don't have the header in different case and set the new value. + $options['_conditional'] = Psr7\Utils::caselessRemove(['Content-Type'], $options['_conditional']); + $options['_conditional']['Content-Type'] = 'application/json'; + } + + if (!empty($options['decode_content']) + && $options['decode_content'] !== true + ) { + // Ensure that we don't have the header in different case and set the new value. + $options['_conditional'] = Psr7\Utils::caselessRemove(['Accept-Encoding'], $options['_conditional']); + $modify['set_headers']['Accept-Encoding'] = $options['decode_content']; + } + + if (isset($options['body'])) { + if (\is_array($options['body'])) { + throw $this->invalidBody(); + } + $modify['body'] = Psr7\Utils::streamFor($options['body']); + unset($options['body']); + } + + if (!empty($options['auth']) && \is_array($options['auth'])) { + $value = $options['auth']; + $type = isset($value[2]) ? \strtolower($value[2]) : 'basic'; + switch ($type) { + case 'basic': + // Ensure that we don't have the header in different case and set the new value. + $modify['set_headers'] = Psr7\Utils::caselessRemove(['Authorization'], $modify['set_headers']); + $modify['set_headers']['Authorization'] = 'Basic ' + .\base64_encode("$value[0]:$value[1]"); + break; + case 'digest': + // @todo: Do not rely on curl + $options['curl'][\CURLOPT_HTTPAUTH] = \CURLAUTH_DIGEST; + $options['curl'][\CURLOPT_USERPWD] = "$value[0]:$value[1]"; + break; + case 'ntlm': + $options['curl'][\CURLOPT_HTTPAUTH] = \CURLAUTH_NTLM; + $options['curl'][\CURLOPT_USERPWD] = "$value[0]:$value[1]"; + break; + } + } + + if (isset($options['query'])) { + $value = $options['query']; + if (\is_array($value)) { + $value = \http_build_query($value, '', '&', \PHP_QUERY_RFC3986); + } + if (!\is_string($value)) { + throw new InvalidArgumentException('query must be a string or array'); + } + $modify['query'] = $value; + unset($options['query']); + } + + // Ensure that sink is not an invalid value. + if (isset($options['sink'])) { + // TODO: Add more sink validation? + if (\is_bool($options['sink'])) { + throw new InvalidArgumentException('sink must not be a boolean'); + } + } + + if (isset($options['version'])) { + $modify['version'] = $options['version']; + } + + $request = Psr7\Utils::modifyRequest($request, $modify); + if ($request->getBody() instanceof Psr7\MultipartStream) { + // Use a multipart/form-data POST if a Content-Type is not set. + // Ensure that we don't have the header in different case and set the new value. + $options['_conditional'] = Psr7\Utils::caselessRemove(['Content-Type'], $options['_conditional']); + $options['_conditional']['Content-Type'] = 'multipart/form-data; boundary=' + .$request->getBody()->getBoundary(); + } + + // Merge in conditional headers if they are not present. + if (isset($options['_conditional'])) { + // Build up the changes so it's in a single clone of the message. + $modify = []; + foreach ($options['_conditional'] as $k => $v) { + if (!$request->hasHeader($k)) { + $modify['set_headers'][$k] = $v; + } + } + $request = Psr7\Utils::modifyRequest($request, $modify); + // Don't pass this internal value along to middleware/handlers. + unset($options['_conditional']); + } + + return $request; + } + + /** + * Return an InvalidArgumentException with pre-set message. + */ + private function invalidBody(): InvalidArgumentException + { + return new InvalidArgumentException('Passing in the "body" request ' + .'option as an array to send a request is not supported. ' + .'Please use the "form_params" request option to send a ' + .'application/x-www-form-urlencoded request, or the "multipart" ' + .'request option to send a multipart/form-data request.'); + } +} diff --git a/3rdparty/guzzlehttp/guzzle/src/ClientInterface.php b/3rdparty/guzzlehttp/guzzle/src/ClientInterface.php new file mode 100644 index 00000000..6aaee61a --- /dev/null +++ b/3rdparty/guzzlehttp/guzzle/src/ClientInterface.php @@ -0,0 +1,84 @@ +request('GET', $uri, $options); + } + + /** + * Create and send an HTTP HEAD request. + * + * Use an absolute path to override the base path of the client, or a + * relative path to append to the base path of the client. The URL can + * contain the query string as well. + * + * @param string|UriInterface $uri URI object or string. + * @param array $options Request options to apply. + * + * @throws GuzzleException + */ + public function head($uri, array $options = []): ResponseInterface + { + return $this->request('HEAD', $uri, $options); + } + + /** + * Create and send an HTTP PUT request. + * + * Use an absolute path to override the base path of the client, or a + * relative path to append to the base path of the client. The URL can + * contain the query string as well. + * + * @param string|UriInterface $uri URI object or string. + * @param array $options Request options to apply. + * + * @throws GuzzleException + */ + public function put($uri, array $options = []): ResponseInterface + { + return $this->request('PUT', $uri, $options); + } + + /** + * Create and send an HTTP POST request. + * + * Use an absolute path to override the base path of the client, or a + * relative path to append to the base path of the client. The URL can + * contain the query string as well. + * + * @param string|UriInterface $uri URI object or string. + * @param array $options Request options to apply. + * + * @throws GuzzleException + */ + public function post($uri, array $options = []): ResponseInterface + { + return $this->request('POST', $uri, $options); + } + + /** + * Create and send an HTTP PATCH request. + * + * Use an absolute path to override the base path of the client, or a + * relative path to append to the base path of the client. The URL can + * contain the query string as well. + * + * @param string|UriInterface $uri URI object or string. + * @param array $options Request options to apply. + * + * @throws GuzzleException + */ + public function patch($uri, array $options = []): ResponseInterface + { + return $this->request('PATCH', $uri, $options); + } + + /** + * Create and send an HTTP DELETE request. + * + * Use an absolute path to override the base path of the client, or a + * relative path to append to the base path of the client. The URL can + * contain the query string as well. + * + * @param string|UriInterface $uri URI object or string. + * @param array $options Request options to apply. + * + * @throws GuzzleException + */ + public function delete($uri, array $options = []): ResponseInterface + { + return $this->request('DELETE', $uri, $options); + } + + /** + * Create and send an asynchronous HTTP request. + * + * Use an absolute path to override the base path of the client, or a + * relative path to append to the base path of the client. The URL can + * contain the query string as well. Use an array to provide a URL + * template and additional variables to use in the URL template expansion. + * + * @param string $method HTTP method + * @param string|UriInterface $uri URI object or string. + * @param array $options Request options to apply. + */ + abstract public function requestAsync(string $method, $uri, array $options = []): PromiseInterface; + + /** + * Create and send an asynchronous HTTP GET request. + * + * Use an absolute path to override the base path of the client, or a + * relative path to append to the base path of the client. The URL can + * contain the query string as well. Use an array to provide a URL + * template and additional variables to use in the URL template expansion. + * + * @param string|UriInterface $uri URI object or string. + * @param array $options Request options to apply. + */ + public function getAsync($uri, array $options = []): PromiseInterface + { + return $this->requestAsync('GET', $uri, $options); + } + + /** + * Create and send an asynchronous HTTP HEAD request. + * + * Use an absolute path to override the base path of the client, or a + * relative path to append to the base path of the client. The URL can + * contain the query string as well. Use an array to provide a URL + * template and additional variables to use in the URL template expansion. + * + * @param string|UriInterface $uri URI object or string. + * @param array $options Request options to apply. + */ + public function headAsync($uri, array $options = []): PromiseInterface + { + return $this->requestAsync('HEAD', $uri, $options); + } + + /** + * Create and send an asynchronous HTTP PUT request. + * + * Use an absolute path to override the base path of the client, or a + * relative path to append to the base path of the client. The URL can + * contain the query string as well. Use an array to provide a URL + * template and additional variables to use in the URL template expansion. + * + * @param string|UriInterface $uri URI object or string. + * @param array $options Request options to apply. + */ + public function putAsync($uri, array $options = []): PromiseInterface + { + return $this->requestAsync('PUT', $uri, $options); + } + + /** + * Create and send an asynchronous HTTP POST request. + * + * Use an absolute path to override the base path of the client, or a + * relative path to append to the base path of the client. The URL can + * contain the query string as well. Use an array to provide a URL + * template and additional variables to use in the URL template expansion. + * + * @param string|UriInterface $uri URI object or string. + * @param array $options Request options to apply. + */ + public function postAsync($uri, array $options = []): PromiseInterface + { + return $this->requestAsync('POST', $uri, $options); + } + + /** + * Create and send an asynchronous HTTP PATCH request. + * + * Use an absolute path to override the base path of the client, or a + * relative path to append to the base path of the client. The URL can + * contain the query string as well. Use an array to provide a URL + * template and additional variables to use in the URL template expansion. + * + * @param string|UriInterface $uri URI object or string. + * @param array $options Request options to apply. + */ + public function patchAsync($uri, array $options = []): PromiseInterface + { + return $this->requestAsync('PATCH', $uri, $options); + } + + /** + * Create and send an asynchronous HTTP DELETE request. + * + * Use an absolute path to override the base path of the client, or a + * relative path to append to the base path of the client. The URL can + * contain the query string as well. Use an array to provide a URL + * template and additional variables to use in the URL template expansion. + * + * @param string|UriInterface $uri URI object or string. + * @param array $options Request options to apply. + */ + public function deleteAsync($uri, array $options = []): PromiseInterface + { + return $this->requestAsync('DELETE', $uri, $options); + } +} diff --git a/3rdparty/guzzlehttp/guzzle/src/Cookie/CookieJar.php b/3rdparty/guzzlehttp/guzzle/src/Cookie/CookieJar.php new file mode 100644 index 00000000..b616cf2e --- /dev/null +++ b/3rdparty/guzzlehttp/guzzle/src/Cookie/CookieJar.php @@ -0,0 +1,307 @@ +strictMode = $strictMode; + + foreach ($cookieArray as $cookie) { + if (!($cookie instanceof SetCookie)) { + $cookie = new SetCookie($cookie); + } + $this->setCookie($cookie); + } + } + + /** + * Create a new Cookie jar from an associative array and domain. + * + * @param array $cookies Cookies to create the jar from + * @param string $domain Domain to set the cookies to + */ + public static function fromArray(array $cookies, string $domain): self + { + $cookieJar = new self(); + foreach ($cookies as $name => $value) { + $cookieJar->setCookie(new SetCookie([ + 'Domain' => $domain, + 'Name' => $name, + 'Value' => $value, + 'Discard' => true, + ])); + } + + return $cookieJar; + } + + /** + * Evaluate if this cookie should be persisted to storage + * that survives between requests. + * + * @param SetCookie $cookie Being evaluated. + * @param bool $allowSessionCookies If we should persist session cookies + */ + public static function shouldPersist(SetCookie $cookie, bool $allowSessionCookies = false): bool + { + if ($cookie->getExpires() || $allowSessionCookies) { + if (!$cookie->getDiscard()) { + return true; + } + } + + return false; + } + + /** + * Finds and returns the cookie based on the name + * + * @param string $name cookie name to search for + * + * @return SetCookie|null cookie that was found or null if not found + */ + public function getCookieByName(string $name): ?SetCookie + { + foreach ($this->cookies as $cookie) { + if ($cookie->getName() !== null && \strcasecmp($cookie->getName(), $name) === 0) { + return $cookie; + } + } + + return null; + } + + public function toArray(): array + { + return \array_map(static function (SetCookie $cookie): array { + return $cookie->toArray(); + }, $this->getIterator()->getArrayCopy()); + } + + public function clear(?string $domain = null, ?string $path = null, ?string $name = null): void + { + if (!$domain) { + $this->cookies = []; + + return; + } elseif (!$path) { + $this->cookies = \array_filter( + $this->cookies, + static function (SetCookie $cookie) use ($domain): bool { + return !$cookie->matchesDomain($domain); + } + ); + } elseif (!$name) { + $this->cookies = \array_filter( + $this->cookies, + static function (SetCookie $cookie) use ($path, $domain): bool { + return !($cookie->matchesPath($path) + && $cookie->matchesDomain($domain)); + } + ); + } else { + $this->cookies = \array_filter( + $this->cookies, + static function (SetCookie $cookie) use ($path, $domain, $name) { + return !($cookie->getName() == $name + && $cookie->matchesPath($path) + && $cookie->matchesDomain($domain)); + } + ); + } + } + + public function clearSessionCookies(): void + { + $this->cookies = \array_filter( + $this->cookies, + static function (SetCookie $cookie): bool { + return !$cookie->getDiscard() && $cookie->getExpires(); + } + ); + } + + public function setCookie(SetCookie $cookie): bool + { + // If the name string is empty (but not 0), ignore the set-cookie + // string entirely. + $name = $cookie->getName(); + if (!$name && $name !== '0') { + return false; + } + + // Only allow cookies with set and valid domain, name, value + $result = $cookie->validate(); + if ($result !== true) { + if ($this->strictMode) { + throw new \RuntimeException('Invalid cookie: '.$result); + } + $this->removeCookieIfEmpty($cookie); + + return false; + } + + // Resolve conflicts with previously set cookies + foreach ($this->cookies as $i => $c) { + // Two cookies are identical, when their path, and domain are + // identical. + if ($c->getPath() != $cookie->getPath() + || $c->getDomain() != $cookie->getDomain() + || $c->getName() != $cookie->getName() + ) { + continue; + } + + // The previously set cookie is a discard cookie and this one is + // not so allow the new cookie to be set + if (!$cookie->getDiscard() && $c->getDiscard()) { + unset($this->cookies[$i]); + continue; + } + + // If the new cookie's expiration is further into the future, then + // replace the old cookie + if ($cookie->getExpires() > $c->getExpires()) { + unset($this->cookies[$i]); + continue; + } + + // If the value has changed, we better change it + if ($cookie->getValue() !== $c->getValue()) { + unset($this->cookies[$i]); + continue; + } + + // The cookie exists, so no need to continue + return false; + } + + $this->cookies[] = $cookie; + + return true; + } + + public function count(): int + { + return \count($this->cookies); + } + + /** + * @return \ArrayIterator + */ + public function getIterator(): \ArrayIterator + { + return new \ArrayIterator(\array_values($this->cookies)); + } + + public function extractCookies(RequestInterface $request, ResponseInterface $response): void + { + if ($cookieHeader = $response->getHeader('Set-Cookie')) { + foreach ($cookieHeader as $cookie) { + $sc = SetCookie::fromString($cookie); + if (!$sc->getDomain()) { + $sc->setDomain($request->getUri()->getHost()); + } + if (0 !== \strpos($sc->getPath(), '/')) { + $sc->setPath($this->getCookiePathFromRequest($request)); + } + if (!$sc->matchesDomain($request->getUri()->getHost())) { + continue; + } + // Note: At this point `$sc->getDomain()` being a public suffix should + // be rejected, but we don't want to pull in the full PSL dependency. + $this->setCookie($sc); + } + } + } + + /** + * Computes cookie path following RFC 6265 section 5.1.4 + * + * @see https://datatracker.ietf.org/doc/html/rfc6265#section-5.1.4 + */ + private function getCookiePathFromRequest(RequestInterface $request): string + { + $uriPath = $request->getUri()->getPath(); + if ('' === $uriPath) { + return '/'; + } + if (0 !== \strpos($uriPath, '/')) { + return '/'; + } + if ('/' === $uriPath) { + return '/'; + } + $lastSlashPos = \strrpos($uriPath, '/'); + if (0 === $lastSlashPos || false === $lastSlashPos) { + return '/'; + } + + return \substr($uriPath, 0, $lastSlashPos); + } + + public function withCookieHeader(RequestInterface $request): RequestInterface + { + $values = []; + $uri = $request->getUri(); + $scheme = $uri->getScheme(); + $host = $uri->getHost(); + $path = $uri->getPath() ?: '/'; + + foreach ($this->cookies as $cookie) { + if ($cookie->matchesPath($path) + && $cookie->matchesDomain($host) + && !$cookie->isExpired() + && (!$cookie->getSecure() || $scheme === 'https') + ) { + $values[] = $cookie->getName().'=' + .$cookie->getValue(); + } + } + + return $values + ? $request->withHeader('Cookie', \implode('; ', $values)) + : $request; + } + + /** + * If a cookie already exists and the server asks to set it again with a + * null value, the cookie must be deleted. + */ + private function removeCookieIfEmpty(SetCookie $cookie): void + { + $cookieValue = $cookie->getValue(); + if ($cookieValue === null || $cookieValue === '') { + $this->clear( + $cookie->getDomain(), + $cookie->getPath(), + $cookie->getName() + ); + } + } +} diff --git a/3rdparty/guzzlehttp/guzzle/src/Cookie/CookieJarInterface.php b/3rdparty/guzzlehttp/guzzle/src/Cookie/CookieJarInterface.php new file mode 100644 index 00000000..93ada58d --- /dev/null +++ b/3rdparty/guzzlehttp/guzzle/src/Cookie/CookieJarInterface.php @@ -0,0 +1,80 @@ + + */ +interface CookieJarInterface extends \Countable, \IteratorAggregate +{ + /** + * Create a request with added cookie headers. + * + * If no matching cookies are found in the cookie jar, then no Cookie + * header is added to the request and the same request is returned. + * + * @param RequestInterface $request Request object to modify. + * + * @return RequestInterface returns the modified request. + */ + public function withCookieHeader(RequestInterface $request): RequestInterface; + + /** + * Extract cookies from an HTTP response and store them in the CookieJar. + * + * @param RequestInterface $request Request that was sent + * @param ResponseInterface $response Response that was received + */ + public function extractCookies(RequestInterface $request, ResponseInterface $response): void; + + /** + * Sets a cookie in the cookie jar. + * + * @param SetCookie $cookie Cookie to set. + * + * @return bool Returns true on success or false on failure + */ + public function setCookie(SetCookie $cookie): bool; + + /** + * Remove cookies currently held in the cookie jar. + * + * Invoking this method without arguments will empty the whole cookie jar. + * If given a $domain argument only cookies belonging to that domain will + * be removed. If given a $domain and $path argument, cookies belonging to + * the specified path within that domain are removed. If given all three + * arguments, then the cookie with the specified name, path and domain is + * removed. + * + * @param string|null $domain Clears cookies matching a domain + * @param string|null $path Clears cookies matching a domain and path + * @param string|null $name Clears cookies matching a domain, path, and name + */ + public function clear(?string $domain = null, ?string $path = null, ?string $name = null): void; + + /** + * Discard all sessions cookies. + * + * Removes cookies that don't have an expire field or a have a discard + * field set to true. To be called when the user agent shuts down according + * to RFC 2965. + */ + public function clearSessionCookies(): void; + + /** + * Converts the cookie jar to an array. + */ + public function toArray(): array; +} diff --git a/3rdparty/guzzlehttp/guzzle/src/Cookie/FileCookieJar.php b/3rdparty/guzzlehttp/guzzle/src/Cookie/FileCookieJar.php new file mode 100644 index 00000000..290236d5 --- /dev/null +++ b/3rdparty/guzzlehttp/guzzle/src/Cookie/FileCookieJar.php @@ -0,0 +1,101 @@ +filename = $cookieFile; + $this->storeSessionCookies = $storeSessionCookies; + + if (\file_exists($cookieFile)) { + $this->load($cookieFile); + } + } + + /** + * Saves the file when shutting down + */ + public function __destruct() + { + $this->save($this->filename); + } + + /** + * Saves the cookies to a file. + * + * @param string $filename File to save + * + * @throws \RuntimeException if the file cannot be found or created + */ + public function save(string $filename): void + { + $json = []; + /** @var SetCookie $cookie */ + foreach ($this as $cookie) { + if (CookieJar::shouldPersist($cookie, $this->storeSessionCookies)) { + $json[] = $cookie->toArray(); + } + } + + $jsonStr = Utils::jsonEncode($json); + if (false === \file_put_contents($filename, $jsonStr, \LOCK_EX)) { + throw new \RuntimeException("Unable to save file {$filename}"); + } + } + + /** + * Load cookies from a JSON formatted file. + * + * Old cookies are kept unless overwritten by newly loaded ones. + * + * @param string $filename Cookie file to load. + * + * @throws \RuntimeException if the file cannot be loaded. + */ + public function load(string $filename): void + { + $json = \file_get_contents($filename); + if (false === $json) { + throw new \RuntimeException("Unable to load file {$filename}"); + } + if ($json === '') { + return; + } + + $data = Utils::jsonDecode($json, true); + if (\is_array($data)) { + foreach ($data as $cookie) { + $this->setCookie(new SetCookie($cookie)); + } + } elseif (\is_scalar($data) && !empty($data)) { + throw new \RuntimeException("Invalid cookie file: {$filename}"); + } + } +} diff --git a/3rdparty/guzzlehttp/guzzle/src/Cookie/SessionCookieJar.php b/3rdparty/guzzlehttp/guzzle/src/Cookie/SessionCookieJar.php new file mode 100644 index 00000000..cb3e67c6 --- /dev/null +++ b/3rdparty/guzzlehttp/guzzle/src/Cookie/SessionCookieJar.php @@ -0,0 +1,77 @@ +sessionKey = $sessionKey; + $this->storeSessionCookies = $storeSessionCookies; + $this->load(); + } + + /** + * Saves cookies to session when shutting down + */ + public function __destruct() + { + $this->save(); + } + + /** + * Save cookies to the client session + */ + public function save(): void + { + $json = []; + /** @var SetCookie $cookie */ + foreach ($this as $cookie) { + if (CookieJar::shouldPersist($cookie, $this->storeSessionCookies)) { + $json[] = $cookie->toArray(); + } + } + + $_SESSION[$this->sessionKey] = \json_encode($json); + } + + /** + * Load the contents of the client session into the data array + */ + protected function load(): void + { + if (!isset($_SESSION[$this->sessionKey])) { + return; + } + $data = \json_decode($_SESSION[$this->sessionKey], true); + if (\is_array($data)) { + foreach ($data as $cookie) { + $this->setCookie(new SetCookie($cookie)); + } + } elseif (\strlen($data)) { + throw new \RuntimeException('Invalid cookie data'); + } + } +} diff --git a/3rdparty/guzzlehttp/guzzle/src/Cookie/SetCookie.php b/3rdparty/guzzlehttp/guzzle/src/Cookie/SetCookie.php new file mode 100644 index 00000000..47c4d10a --- /dev/null +++ b/3rdparty/guzzlehttp/guzzle/src/Cookie/SetCookie.php @@ -0,0 +1,492 @@ + null, + 'Value' => null, + 'Domain' => null, + 'Path' => '/', + 'Max-Age' => null, + 'Expires' => null, + 'Secure' => false, + 'Discard' => false, + 'HttpOnly' => false, + ]; + + /** + * @var array Cookie data + */ + private $data; + + /** + * Create a new SetCookie object from a string. + * + * @param string $cookie Set-Cookie header string + */ + public static function fromString(string $cookie): self + { + // Create the default return array + $data = self::$defaults; + // Explode the cookie string using a series of semicolons + $pieces = \array_filter(\array_map('trim', \explode(';', $cookie))); + // The name of the cookie (first kvp) must exist and include an equal sign. + if (!isset($pieces[0]) || \strpos($pieces[0], '=') === false) { + return new self($data); + } + + // Add the cookie pieces into the parsed data array + foreach ($pieces as $part) { + $cookieParts = \explode('=', $part, 2); + $key = \trim($cookieParts[0]); + $value = isset($cookieParts[1]) + ? \trim($cookieParts[1], " \n\r\t\0\x0B") + : true; + + // Only check for non-cookies when cookies have been found + if (!isset($data['Name'])) { + $data['Name'] = $key; + $data['Value'] = $value; + } else { + foreach (\array_keys(self::$defaults) as $search) { + if (!\strcasecmp($search, $key)) { + if ($search === 'Max-Age') { + if (is_numeric($value)) { + $data[$search] = (int) $value; + } + } elseif ($search === 'Secure' || $search === 'Discard' || $search === 'HttpOnly') { + if ($value) { + $data[$search] = true; + } + } else { + $data[$search] = $value; + } + continue 2; + } + } + $data[$key] = $value; + } + } + + return new self($data); + } + + /** + * @param array $data Array of cookie data provided by a Cookie parser + */ + public function __construct(array $data = []) + { + $this->data = self::$defaults; + + if (isset($data['Name'])) { + $this->setName($data['Name']); + } + + if (isset($data['Value'])) { + $this->setValue($data['Value']); + } + + if (isset($data['Domain'])) { + $this->setDomain($data['Domain']); + } + + if (isset($data['Path'])) { + $this->setPath($data['Path']); + } + + if (isset($data['Max-Age'])) { + $this->setMaxAge($data['Max-Age']); + } + + if (isset($data['Expires'])) { + $this->setExpires($data['Expires']); + } + + if (isset($data['Secure'])) { + $this->setSecure($data['Secure']); + } + + if (isset($data['Discard'])) { + $this->setDiscard($data['Discard']); + } + + if (isset($data['HttpOnly'])) { + $this->setHttpOnly($data['HttpOnly']); + } + + // Set the remaining values that don't have extra validation logic + foreach (array_diff(array_keys($data), array_keys(self::$defaults)) as $key) { + $this->data[$key] = $data[$key]; + } + + // Extract the Expires value and turn it into a UNIX timestamp if needed + if (!$this->getExpires() && $this->getMaxAge()) { + // Calculate the Expires date + $this->setExpires(\time() + $this->getMaxAge()); + } elseif (null !== ($expires = $this->getExpires()) && !\is_numeric($expires)) { + $this->setExpires($expires); + } + } + + public function __toString() + { + $str = $this->data['Name'].'='.($this->data['Value'] ?? '').'; '; + foreach ($this->data as $k => $v) { + if ($k !== 'Name' && $k !== 'Value' && $v !== null && $v !== false) { + if ($k === 'Expires') { + $str .= 'Expires='.\gmdate('D, d M Y H:i:s \G\M\T', $v).'; '; + } else { + $str .= ($v === true ? $k : "{$k}={$v}").'; '; + } + } + } + + return \rtrim($str, '; '); + } + + public function toArray(): array + { + return $this->data; + } + + /** + * Get the cookie name. + * + * @return string + */ + public function getName() + { + return $this->data['Name']; + } + + /** + * Set the cookie name. + * + * @param string $name Cookie name + */ + public function setName($name): void + { + if (!is_string($name)) { + trigger_deprecation('guzzlehttp/guzzle', '7.4', 'Not passing a string to %s::%s() is deprecated and will cause an error in 8.0.', __CLASS__, __FUNCTION__); + } + + $this->data['Name'] = (string) $name; + } + + /** + * Get the cookie value. + * + * @return string|null + */ + public function getValue() + { + return $this->data['Value']; + } + + /** + * Set the cookie value. + * + * @param string $value Cookie value + */ + public function setValue($value): void + { + if (!is_string($value)) { + trigger_deprecation('guzzlehttp/guzzle', '7.4', 'Not passing a string to %s::%s() is deprecated and will cause an error in 8.0.', __CLASS__, __FUNCTION__); + } + + $this->data['Value'] = (string) $value; + } + + /** + * Get the domain. + * + * @return string|null + */ + public function getDomain() + { + return $this->data['Domain']; + } + + /** + * Set the domain of the cookie. + * + * @param string|null $domain + */ + public function setDomain($domain): void + { + if (!is_string($domain) && null !== $domain) { + trigger_deprecation('guzzlehttp/guzzle', '7.4', 'Not passing a string or null to %s::%s() is deprecated and will cause an error in 8.0.', __CLASS__, __FUNCTION__); + } + + $this->data['Domain'] = null === $domain ? null : (string) $domain; + } + + /** + * Get the path. + * + * @return string + */ + public function getPath() + { + return $this->data['Path']; + } + + /** + * Set the path of the cookie. + * + * @param string $path Path of the cookie + */ + public function setPath($path): void + { + if (!is_string($path)) { + trigger_deprecation('guzzlehttp/guzzle', '7.4', 'Not passing a string to %s::%s() is deprecated and will cause an error in 8.0.', __CLASS__, __FUNCTION__); + } + + $this->data['Path'] = (string) $path; + } + + /** + * Maximum lifetime of the cookie in seconds. + * + * @return int|null + */ + public function getMaxAge() + { + return null === $this->data['Max-Age'] ? null : (int) $this->data['Max-Age']; + } + + /** + * Set the max-age of the cookie. + * + * @param int|null $maxAge Max age of the cookie in seconds + */ + public function setMaxAge($maxAge): void + { + if (!is_int($maxAge) && null !== $maxAge) { + trigger_deprecation('guzzlehttp/guzzle', '7.4', 'Not passing an int or null to %s::%s() is deprecated and will cause an error in 8.0.', __CLASS__, __FUNCTION__); + } + + $this->data['Max-Age'] = $maxAge === null ? null : (int) $maxAge; + } + + /** + * The UNIX timestamp when the cookie Expires. + * + * @return string|int|null + */ + public function getExpires() + { + return $this->data['Expires']; + } + + /** + * Set the unix timestamp for which the cookie will expire. + * + * @param int|string|null $timestamp Unix timestamp or any English textual datetime description. + */ + public function setExpires($timestamp): void + { + if (!is_int($timestamp) && !is_string($timestamp) && null !== $timestamp) { + trigger_deprecation('guzzlehttp/guzzle', '7.4', 'Not passing an int, string or null to %s::%s() is deprecated and will cause an error in 8.0.', __CLASS__, __FUNCTION__); + } + + $this->data['Expires'] = null === $timestamp ? null : (\is_numeric($timestamp) ? (int) $timestamp : \strtotime((string) $timestamp)); + } + + /** + * Get whether or not this is a secure cookie. + * + * @return bool + */ + public function getSecure() + { + return $this->data['Secure']; + } + + /** + * Set whether or not the cookie is secure. + * + * @param bool $secure Set to true or false if secure + */ + public function setSecure($secure): void + { + if (!is_bool($secure)) { + trigger_deprecation('guzzlehttp/guzzle', '7.4', 'Not passing a bool to %s::%s() is deprecated and will cause an error in 8.0.', __CLASS__, __FUNCTION__); + } + + $this->data['Secure'] = (bool) $secure; + } + + /** + * Get whether or not this is a session cookie. + * + * @return bool|null + */ + public function getDiscard() + { + return $this->data['Discard']; + } + + /** + * Set whether or not this is a session cookie. + * + * @param bool $discard Set to true or false if this is a session cookie + */ + public function setDiscard($discard): void + { + if (!is_bool($discard)) { + trigger_deprecation('guzzlehttp/guzzle', '7.4', 'Not passing a bool to %s::%s() is deprecated and will cause an error in 8.0.', __CLASS__, __FUNCTION__); + } + + $this->data['Discard'] = (bool) $discard; + } + + /** + * Get whether or not this is an HTTP only cookie. + * + * @return bool + */ + public function getHttpOnly() + { + return $this->data['HttpOnly']; + } + + /** + * Set whether or not this is an HTTP only cookie. + * + * @param bool $httpOnly Set to true or false if this is HTTP only + */ + public function setHttpOnly($httpOnly): void + { + if (!is_bool($httpOnly)) { + trigger_deprecation('guzzlehttp/guzzle', '7.4', 'Not passing a bool to %s::%s() is deprecated and will cause an error in 8.0.', __CLASS__, __FUNCTION__); + } + + $this->data['HttpOnly'] = (bool) $httpOnly; + } + + /** + * Check if the cookie matches a path value. + * + * A request-path path-matches a given cookie-path if at least one of + * the following conditions holds: + * + * - The cookie-path and the request-path are identical. + * - The cookie-path is a prefix of the request-path, and the last + * character of the cookie-path is %x2F ("/"). + * - The cookie-path is a prefix of the request-path, and the first + * character of the request-path that is not included in the cookie- + * path is a %x2F ("/") character. + * + * @param string $requestPath Path to check against + */ + public function matchesPath(string $requestPath): bool + { + $cookiePath = $this->getPath(); + + // Match on exact matches or when path is the default empty "/" + if ($cookiePath === '/' || $cookiePath == $requestPath) { + return true; + } + + // Ensure that the cookie-path is a prefix of the request path. + if (0 !== \strpos($requestPath, $cookiePath)) { + return false; + } + + // Match if the last character of the cookie-path is "/" + if (\substr($cookiePath, -1, 1) === '/') { + return true; + } + + // Match if the first character not included in cookie path is "/" + return \substr($requestPath, \strlen($cookiePath), 1) === '/'; + } + + /** + * Check if the cookie matches a domain value. + * + * @param string $domain Domain to check against + */ + public function matchesDomain(string $domain): bool + { + $cookieDomain = $this->getDomain(); + if (null === $cookieDomain) { + return true; + } + + // Remove the leading '.' as per spec in RFC 6265. + // https://datatracker.ietf.org/doc/html/rfc6265#section-5.2.3 + $cookieDomain = \ltrim(\strtolower($cookieDomain), '.'); + + $domain = \strtolower($domain); + + // Domain not set or exact match. + if ('' === $cookieDomain || $domain === $cookieDomain) { + return true; + } + + // Matching the subdomain according to RFC 6265. + // https://datatracker.ietf.org/doc/html/rfc6265#section-5.1.3 + if (\filter_var($domain, \FILTER_VALIDATE_IP)) { + return false; + } + + return (bool) \preg_match('/\.'.\preg_quote($cookieDomain, '/').'$/', $domain); + } + + /** + * Check if the cookie is expired. + */ + public function isExpired(): bool + { + return $this->getExpires() !== null && \time() > $this->getExpires(); + } + + /** + * Check if the cookie is valid according to RFC 6265. + * + * @return bool|string Returns true if valid or an error message if invalid + */ + public function validate() + { + $name = $this->getName(); + if ($name === '') { + return 'The cookie name must not be empty'; + } + + // Check if any of the invalid characters are present in the cookie name + if (\preg_match( + '/[\x00-\x20\x22\x28-\x29\x2c\x2f\x3a-\x40\x5c\x7b\x7d\x7f]/', + $name + )) { + return 'Cookie name must not contain invalid characters: ASCII ' + .'Control characters (0-31;127), space, tab and the ' + .'following characters: ()<>@,;:\"/?={}'; + } + + // Value must not be null. 0 and empty string are valid. Empty strings + // are technically against RFC 6265, but known to happen in the wild. + $value = $this->getValue(); + if ($value === null) { + return 'The cookie value must not be empty'; + } + + // Domains must not be empty, but can be 0. "0" is not a valid internet + // domain, but may be used as server name in a private network. + $domain = $this->getDomain(); + if ($domain === null || $domain === '') { + return 'The cookie domain must not be empty'; + } + + return true; + } +} diff --git a/3rdparty/guzzlehttp/guzzle/src/Exception/BadResponseException.php b/3rdparty/guzzlehttp/guzzle/src/Exception/BadResponseException.php new file mode 100644 index 00000000..ba67ad49 --- /dev/null +++ b/3rdparty/guzzlehttp/guzzle/src/Exception/BadResponseException.php @@ -0,0 +1,39 @@ +request = $request; + $this->handlerContext = $handlerContext; + } + + /** + * Get the request that caused the exception + */ + public function getRequest(): RequestInterface + { + return $this->request; + } + + /** + * Get contextual information about the error from the underlying handler. + * + * The contents of this array will vary depending on which handler you are + * using. It may also be just an empty array. Relying on this data will + * couple you to a specific handler, but can give more debug information + * when needed. + */ + public function getHandlerContext(): array + { + return $this->handlerContext; + } +} diff --git a/3rdparty/guzzlehttp/guzzle/src/Exception/GuzzleException.php b/3rdparty/guzzlehttp/guzzle/src/Exception/GuzzleException.php new file mode 100644 index 00000000..fa3ed699 --- /dev/null +++ b/3rdparty/guzzlehttp/guzzle/src/Exception/GuzzleException.php @@ -0,0 +1,9 @@ +getStatusCode() : 0; + parent::__construct($message, $code, $previous); + $this->request = $request; + $this->response = $response; + $this->handlerContext = $handlerContext; + } + + /** + * Wrap non-RequestExceptions with a RequestException + */ + public static function wrapException(RequestInterface $request, \Throwable $e): RequestException + { + return $e instanceof RequestException ? $e : new RequestException($e->getMessage(), $request, null, $e); + } + + /** + * Factory method to create a new exception with a normalized error message + * + * @param RequestInterface $request Request sent + * @param ResponseInterface $response Response received + * @param \Throwable|null $previous Previous exception + * @param array $handlerContext Optional handler context + * @param BodySummarizerInterface|null $bodySummarizer Optional body summarizer + */ + public static function create( + RequestInterface $request, + ?ResponseInterface $response = null, + ?\Throwable $previous = null, + array $handlerContext = [], + ?BodySummarizerInterface $bodySummarizer = null + ): self { + if (!$response) { + return new self( + 'Error completing request', + $request, + null, + $previous, + $handlerContext + ); + } + + $level = (int) \floor($response->getStatusCode() / 100); + if ($level === 4) { + $label = 'Client error'; + $className = ClientException::class; + } elseif ($level === 5) { + $label = 'Server error'; + $className = ServerException::class; + } else { + $label = 'Unsuccessful request'; + $className = __CLASS__; + } + + $uri = \GuzzleHttp\Psr7\Utils::redactUserInfo($request->getUri()); + + // Client Error: `GET /` resulted in a `404 Not Found` response: + // ... (truncated) + $message = \sprintf( + '%s: `%s %s` resulted in a `%s %s` response', + $label, + $request->getMethod(), + $uri->__toString(), + $response->getStatusCode(), + $response->getReasonPhrase() + ); + + $summary = ($bodySummarizer ?? new BodySummarizer())->summarize($response); + + if ($summary !== null) { + $message .= ":\n{$summary}\n"; + } + + return new $className($message, $request, $response, $previous, $handlerContext); + } + + /** + * Get the request that caused the exception + */ + public function getRequest(): RequestInterface + { + return $this->request; + } + + /** + * Get the associated response + */ + public function getResponse(): ?ResponseInterface + { + return $this->response; + } + + /** + * Check if a response was received + */ + public function hasResponse(): bool + { + return $this->response !== null; + } + + /** + * Get contextual information about the error from the underlying handler. + * + * The contents of this array will vary depending on which handler you are + * using. It may also be just an empty array. Relying on this data will + * couple you to a specific handler, but can give more debug information + * when needed. + */ + public function getHandlerContext(): array + { + return $this->handlerContext; + } +} diff --git a/3rdparty/guzzlehttp/guzzle/src/Exception/ServerException.php b/3rdparty/guzzlehttp/guzzle/src/Exception/ServerException.php new file mode 100644 index 00000000..8055e067 --- /dev/null +++ b/3rdparty/guzzlehttp/guzzle/src/Exception/ServerException.php @@ -0,0 +1,10 @@ +maxHandles = $maxHandles; + } + + public function create(RequestInterface $request, array $options): EasyHandle + { + $protocolVersion = $request->getProtocolVersion(); + + if ('2' === $protocolVersion || '2.0' === $protocolVersion) { + if (!self::supportsHttp2()) { + throw new ConnectException('HTTP/2 is supported by the cURL handler, however libcurl is built without HTTP/2 support.', $request); + } + } elseif ('1.0' !== $protocolVersion && '1.1' !== $protocolVersion) { + throw new ConnectException(sprintf('HTTP/%s is not supported by the cURL handler.', $protocolVersion), $request); + } + + if (isset($options['curl']['body_as_string'])) { + $options['_body_as_string'] = $options['curl']['body_as_string']; + unset($options['curl']['body_as_string']); + } + + $easy = new EasyHandle(); + $easy->request = $request; + $easy->options = $options; + $conf = $this->getDefaultConf($easy); + $this->applyMethod($easy, $conf); + $this->applyHandlerOptions($easy, $conf); + $this->applyHeaders($easy, $conf); + unset($conf['_headers']); + + // Add handler options from the request configuration options + if (isset($options['curl'])) { + $conf = \array_replace($conf, $options['curl']); + } + + $conf[\CURLOPT_HEADERFUNCTION] = $this->createHeaderFn($easy); + $easy->handle = $this->handles ? \array_pop($this->handles) : \curl_init(); + curl_setopt_array($easy->handle, $conf); + + return $easy; + } + + private static function supportsHttp2(): bool + { + static $supportsHttp2 = null; + + if (null === $supportsHttp2) { + $supportsHttp2 = self::supportsTls12() + && defined('CURL_VERSION_HTTP2') + && (\CURL_VERSION_HTTP2 & \curl_version()['features']); + } + + return $supportsHttp2; + } + + private static function supportsTls12(): bool + { + static $supportsTls12 = null; + + if (null === $supportsTls12) { + $supportsTls12 = \CURL_SSLVERSION_TLSv1_2 & \curl_version()['features']; + } + + return $supportsTls12; + } + + private static function supportsTls13(): bool + { + static $supportsTls13 = null; + + if (null === $supportsTls13) { + $supportsTls13 = defined('CURL_SSLVERSION_TLSv1_3') + && (\CURL_SSLVERSION_TLSv1_3 & \curl_version()['features']); + } + + return $supportsTls13; + } + + public function release(EasyHandle $easy): void + { + $resource = $easy->handle; + unset($easy->handle); + + if (\count($this->handles) >= $this->maxHandles) { + \curl_close($resource); + } else { + // Remove all callback functions as they can hold onto references + // and are not cleaned up by curl_reset. Using curl_setopt_array + // does not work for some reason, so removing each one + // individually. + \curl_setopt($resource, \CURLOPT_HEADERFUNCTION, null); + \curl_setopt($resource, \CURLOPT_READFUNCTION, null); + \curl_setopt($resource, \CURLOPT_WRITEFUNCTION, null); + \curl_setopt($resource, \CURLOPT_PROGRESSFUNCTION, null); + \curl_reset($resource); + $this->handles[] = $resource; + } + } + + /** + * Completes a cURL transaction, either returning a response promise or a + * rejected promise. + * + * @param callable(RequestInterface, array): PromiseInterface $handler + * @param CurlFactoryInterface $factory Dictates how the handle is released + */ + public static function finish(callable $handler, EasyHandle $easy, CurlFactoryInterface $factory): PromiseInterface + { + if (isset($easy->options['on_stats'])) { + self::invokeStats($easy); + } + + if (!$easy->response || $easy->errno) { + return self::finishError($handler, $easy, $factory); + } + + // Return the response if it is present and there is no error. + $factory->release($easy); + + // Rewind the body of the response if possible. + $body = $easy->response->getBody(); + if ($body->isSeekable()) { + $body->rewind(); + } + + return new FulfilledPromise($easy->response); + } + + private static function invokeStats(EasyHandle $easy): void + { + $curlStats = \curl_getinfo($easy->handle); + $curlStats['appconnect_time'] = \curl_getinfo($easy->handle, \CURLINFO_APPCONNECT_TIME); + $stats = new TransferStats( + $easy->request, + $easy->response, + $curlStats['total_time'], + $easy->errno, + $curlStats + ); + ($easy->options['on_stats'])($stats); + } + + /** + * @param callable(RequestInterface, array): PromiseInterface $handler + */ + private static function finishError(callable $handler, EasyHandle $easy, CurlFactoryInterface $factory): PromiseInterface + { + // Get error information and release the handle to the factory. + $ctx = [ + 'errno' => $easy->errno, + 'error' => \curl_error($easy->handle), + 'appconnect_time' => \curl_getinfo($easy->handle, \CURLINFO_APPCONNECT_TIME), + ] + \curl_getinfo($easy->handle); + $ctx[self::CURL_VERSION_STR] = self::getCurlVersion(); + $factory->release($easy); + + // Retry when nothing is present or when curl failed to rewind. + if (empty($easy->options['_err_message']) && (!$easy->errno || $easy->errno == 65)) { + return self::retryFailedRewind($handler, $easy, $ctx); + } + + return self::createRejection($easy, $ctx); + } + + private static function getCurlVersion(): string + { + static $curlVersion = null; + + if (null === $curlVersion) { + $curlVersion = \curl_version()['version']; + } + + return $curlVersion; + } + + private static function createRejection(EasyHandle $easy, array $ctx): PromiseInterface + { + static $connectionErrors = [ + \CURLE_OPERATION_TIMEOUTED => true, + \CURLE_COULDNT_RESOLVE_HOST => true, + \CURLE_COULDNT_CONNECT => true, + \CURLE_SSL_CONNECT_ERROR => true, + \CURLE_GOT_NOTHING => true, + ]; + + if ($easy->createResponseException) { + return P\Create::rejectionFor( + new RequestException( + 'An error was encountered while creating the response', + $easy->request, + $easy->response, + $easy->createResponseException, + $ctx + ) + ); + } + + // If an exception was encountered during the onHeaders event, then + // return a rejected promise that wraps that exception. + if ($easy->onHeadersException) { + return P\Create::rejectionFor( + new RequestException( + 'An error was encountered during the on_headers event', + $easy->request, + $easy->response, + $easy->onHeadersException, + $ctx + ) + ); + } + + $uri = $easy->request->getUri(); + + $sanitizedError = self::sanitizeCurlError($ctx['error'] ?? '', $uri); + + $message = \sprintf( + 'cURL error %s: %s (%s)', + $ctx['errno'], + $sanitizedError, + 'see https://curl.haxx.se/libcurl/c/libcurl-errors.html' + ); + + if ('' !== $sanitizedError) { + $redactedUriString = \GuzzleHttp\Psr7\Utils::redactUserInfo($uri)->__toString(); + if ($redactedUriString !== '' && false === \strpos($sanitizedError, $redactedUriString)) { + $message .= \sprintf(' for %s', $redactedUriString); + } + } + + // Create a connection exception if it was a specific error code. + $error = isset($connectionErrors[$easy->errno]) + ? new ConnectException($message, $easy->request, null, $ctx) + : new RequestException($message, $easy->request, $easy->response, null, $ctx); + + return P\Create::rejectionFor($error); + } + + private static function sanitizeCurlError(string $error, UriInterface $uri): string + { + if ('' === $error) { + return $error; + } + + $baseUri = $uri->withQuery('')->withFragment(''); + $baseUriString = $baseUri->__toString(); + + if ('' === $baseUriString) { + return $error; + } + + $redactedUriString = \GuzzleHttp\Psr7\Utils::redactUserInfo($baseUri)->__toString(); + + return str_replace($baseUriString, $redactedUriString, $error); + } + + /** + * @return array + */ + private function getDefaultConf(EasyHandle $easy): array + { + $conf = [ + '_headers' => $easy->request->getHeaders(), + \CURLOPT_CUSTOMREQUEST => $easy->request->getMethod(), + \CURLOPT_URL => (string) $easy->request->getUri()->withFragment(''), + \CURLOPT_RETURNTRANSFER => false, + \CURLOPT_HEADER => false, + \CURLOPT_CONNECTTIMEOUT => 300, + ]; + + if (\defined('CURLOPT_PROTOCOLS')) { + $conf[\CURLOPT_PROTOCOLS] = \CURLPROTO_HTTP | \CURLPROTO_HTTPS; + } + + $version = $easy->request->getProtocolVersion(); + + if ('2' === $version || '2.0' === $version) { + $conf[\CURLOPT_HTTP_VERSION] = \CURL_HTTP_VERSION_2_0; + } elseif ('1.1' === $version) { + $conf[\CURLOPT_HTTP_VERSION] = \CURL_HTTP_VERSION_1_1; + } else { + $conf[\CURLOPT_HTTP_VERSION] = \CURL_HTTP_VERSION_1_0; + } + + return $conf; + } + + private function applyMethod(EasyHandle $easy, array &$conf): void + { + $body = $easy->request->getBody(); + $size = $body->getSize(); + + if ($size === null || $size > 0) { + $this->applyBody($easy->request, $easy->options, $conf); + + return; + } + + $method = $easy->request->getMethod(); + if ($method === 'PUT' || $method === 'POST') { + // See https://datatracker.ietf.org/doc/html/rfc7230#section-3.3.2 + if (!$easy->request->hasHeader('Content-Length')) { + $conf[\CURLOPT_HTTPHEADER][] = 'Content-Length: 0'; + } + } elseif ($method === 'HEAD') { + $conf[\CURLOPT_NOBODY] = true; + unset( + $conf[\CURLOPT_WRITEFUNCTION], + $conf[\CURLOPT_READFUNCTION], + $conf[\CURLOPT_FILE], + $conf[\CURLOPT_INFILE] + ); + } + } + + private function applyBody(RequestInterface $request, array $options, array &$conf): void + { + $size = $request->hasHeader('Content-Length') + ? (int) $request->getHeaderLine('Content-Length') + : null; + + // Send the body as a string if the size is less than 1MB OR if the + // [curl][body_as_string] request value is set. + if (($size !== null && $size < 1000000) || !empty($options['_body_as_string'])) { + $conf[\CURLOPT_POSTFIELDS] = (string) $request->getBody(); + // Don't duplicate the Content-Length header + $this->removeHeader('Content-Length', $conf); + $this->removeHeader('Transfer-Encoding', $conf); + } else { + $conf[\CURLOPT_UPLOAD] = true; + if ($size !== null) { + $conf[\CURLOPT_INFILESIZE] = $size; + $this->removeHeader('Content-Length', $conf); + } + $body = $request->getBody(); + if ($body->isSeekable()) { + $body->rewind(); + } + $conf[\CURLOPT_READFUNCTION] = static function ($ch, $fd, $length) use ($body) { + return $body->read($length); + }; + } + + // If the Expect header is not present, prevent curl from adding it + if (!$request->hasHeader('Expect')) { + $conf[\CURLOPT_HTTPHEADER][] = 'Expect:'; + } + + // cURL sometimes adds a content-type by default. Prevent this. + if (!$request->hasHeader('Content-Type')) { + $conf[\CURLOPT_HTTPHEADER][] = 'Content-Type:'; + } + } + + private function applyHeaders(EasyHandle $easy, array &$conf): void + { + foreach ($conf['_headers'] as $name => $values) { + foreach ($values as $value) { + $value = (string) $value; + if ($value === '') { + // cURL requires a special format for empty headers. + // See https://github.com/guzzle/guzzle/issues/1882 for more details. + $conf[\CURLOPT_HTTPHEADER][] = "$name;"; + } else { + $conf[\CURLOPT_HTTPHEADER][] = "$name: $value"; + } + } + } + + // Remove the Accept header if one was not set + if (!$easy->request->hasHeader('Accept')) { + $conf[\CURLOPT_HTTPHEADER][] = 'Accept:'; + } + } + + /** + * Remove a header from the options array. + * + * @param string $name Case-insensitive header to remove + * @param array $options Array of options to modify + */ + private function removeHeader(string $name, array &$options): void + { + foreach (\array_keys($options['_headers']) as $key) { + if (!\strcasecmp($key, $name)) { + unset($options['_headers'][$key]); + + return; + } + } + } + + private function applyHandlerOptions(EasyHandle $easy, array &$conf): void + { + $options = $easy->options; + if (isset($options['verify'])) { + if ($options['verify'] === false) { + unset($conf[\CURLOPT_CAINFO]); + $conf[\CURLOPT_SSL_VERIFYHOST] = 0; + $conf[\CURLOPT_SSL_VERIFYPEER] = false; + } else { + $conf[\CURLOPT_SSL_VERIFYHOST] = 2; + $conf[\CURLOPT_SSL_VERIFYPEER] = true; + if (\is_string($options['verify'])) { + // Throw an error if the file/folder/link path is not valid or doesn't exist. + if (!\file_exists($options['verify'])) { + throw new \InvalidArgumentException("SSL CA bundle not found: {$options['verify']}"); + } + // If it's a directory or a link to a directory use CURLOPT_CAPATH. + // If not, it's probably a file, or a link to a file, so use CURLOPT_CAINFO. + if ( + \is_dir($options['verify']) + || ( + \is_link($options['verify']) === true + && ($verifyLink = \readlink($options['verify'])) !== false + && \is_dir($verifyLink) + ) + ) { + $conf[\CURLOPT_CAPATH] = $options['verify']; + } else { + $conf[\CURLOPT_CAINFO] = $options['verify']; + } + } + } + } + + if (!isset($options['curl'][\CURLOPT_ENCODING]) && !empty($options['decode_content'])) { + $accept = $easy->request->getHeaderLine('Accept-Encoding'); + if ($accept) { + $conf[\CURLOPT_ENCODING] = $accept; + } else { + // The empty string enables all available decoders and implicitly + // sets a matching 'Accept-Encoding' header. + $conf[\CURLOPT_ENCODING] = ''; + // But as the user did not specify any encoding preference, + // let's leave it up to server by preventing curl from sending + // the header, which will be interpreted as 'Accept-Encoding: *'. + // https://www.rfc-editor.org/rfc/rfc9110#field.accept-encoding + $conf[\CURLOPT_HTTPHEADER][] = 'Accept-Encoding:'; + } + } + + if (!isset($options['sink'])) { + // Use a default temp stream if no sink was set. + $options['sink'] = \GuzzleHttp\Psr7\Utils::tryFopen('php://temp', 'w+'); + } + $sink = $options['sink']; + if (!\is_string($sink)) { + $sink = \GuzzleHttp\Psr7\Utils::streamFor($sink); + } elseif (!\is_dir(\dirname($sink))) { + // Ensure that the directory exists before failing in curl. + throw new \RuntimeException(\sprintf('Directory %s does not exist for sink value of %s', \dirname($sink), $sink)); + } else { + $sink = new LazyOpenStream($sink, 'w+'); + } + $easy->sink = $sink; + $conf[\CURLOPT_WRITEFUNCTION] = static function ($ch, $write) use ($sink): int { + return $sink->write($write); + }; + + $timeoutRequiresNoSignal = false; + if (isset($options['timeout'])) { + $timeoutRequiresNoSignal |= $options['timeout'] < 1; + $conf[\CURLOPT_TIMEOUT_MS] = $options['timeout'] * 1000; + } + + // CURL default value is CURL_IPRESOLVE_WHATEVER + if (isset($options['force_ip_resolve'])) { + if ('v4' === $options['force_ip_resolve']) { + $conf[\CURLOPT_IPRESOLVE] = \CURL_IPRESOLVE_V4; + } elseif ('v6' === $options['force_ip_resolve']) { + $conf[\CURLOPT_IPRESOLVE] = \CURL_IPRESOLVE_V6; + } + } + + if (isset($options['connect_timeout'])) { + $timeoutRequiresNoSignal |= $options['connect_timeout'] < 1; + $conf[\CURLOPT_CONNECTTIMEOUT_MS] = $options['connect_timeout'] * 1000; + } + + if ($timeoutRequiresNoSignal && \strtoupper(\substr(\PHP_OS, 0, 3)) !== 'WIN') { + $conf[\CURLOPT_NOSIGNAL] = true; + } + + if (isset($options['proxy'])) { + if (!\is_array($options['proxy'])) { + $conf[\CURLOPT_PROXY] = $options['proxy']; + } else { + $scheme = $easy->request->getUri()->getScheme(); + if (isset($options['proxy'][$scheme])) { + $host = $easy->request->getUri()->getHost(); + if (isset($options['proxy']['no']) && Utils::isHostInNoProxy($host, $options['proxy']['no'])) { + unset($conf[\CURLOPT_PROXY]); + } else { + $conf[\CURLOPT_PROXY] = $options['proxy'][$scheme]; + } + } + } + } + + if (isset($options['crypto_method'])) { + $protocolVersion = $easy->request->getProtocolVersion(); + + // If HTTP/2, upgrade TLS 1.0 and 1.1 to 1.2 + if ('2' === $protocolVersion || '2.0' === $protocolVersion) { + if ( + \STREAM_CRYPTO_METHOD_TLSv1_0_CLIENT === $options['crypto_method'] + || \STREAM_CRYPTO_METHOD_TLSv1_1_CLIENT === $options['crypto_method'] + || \STREAM_CRYPTO_METHOD_TLSv1_2_CLIENT === $options['crypto_method'] + ) { + $conf[\CURLOPT_SSLVERSION] = \CURL_SSLVERSION_TLSv1_2; + } elseif (defined('STREAM_CRYPTO_METHOD_TLSv1_3_CLIENT') && \STREAM_CRYPTO_METHOD_TLSv1_3_CLIENT === $options['crypto_method']) { + if (!self::supportsTls13()) { + throw new \InvalidArgumentException('Invalid crypto_method request option: TLS 1.3 not supported by your version of cURL'); + } + $conf[\CURLOPT_SSLVERSION] = \CURL_SSLVERSION_TLSv1_3; + } else { + throw new \InvalidArgumentException('Invalid crypto_method request option: unknown version provided'); + } + } elseif (\STREAM_CRYPTO_METHOD_TLSv1_0_CLIENT === $options['crypto_method']) { + $conf[\CURLOPT_SSLVERSION] = \CURL_SSLVERSION_TLSv1_0; + } elseif (\STREAM_CRYPTO_METHOD_TLSv1_1_CLIENT === $options['crypto_method']) { + $conf[\CURLOPT_SSLVERSION] = \CURL_SSLVERSION_TLSv1_1; + } elseif (\STREAM_CRYPTO_METHOD_TLSv1_2_CLIENT === $options['crypto_method']) { + if (!self::supportsTls12()) { + throw new \InvalidArgumentException('Invalid crypto_method request option: TLS 1.2 not supported by your version of cURL'); + } + $conf[\CURLOPT_SSLVERSION] = \CURL_SSLVERSION_TLSv1_2; + } elseif (defined('STREAM_CRYPTO_METHOD_TLSv1_3_CLIENT') && \STREAM_CRYPTO_METHOD_TLSv1_3_CLIENT === $options['crypto_method']) { + if (!self::supportsTls13()) { + throw new \InvalidArgumentException('Invalid crypto_method request option: TLS 1.3 not supported by your version of cURL'); + } + $conf[\CURLOPT_SSLVERSION] = \CURL_SSLVERSION_TLSv1_3; + } else { + throw new \InvalidArgumentException('Invalid crypto_method request option: unknown version provided'); + } + } + + if (isset($options['cert'])) { + $cert = $options['cert']; + if (\is_array($cert)) { + $conf[\CURLOPT_SSLCERTPASSWD] = $cert[1]; + $cert = $cert[0]; + } + if (!\file_exists($cert)) { + throw new \InvalidArgumentException("SSL certificate not found: {$cert}"); + } + // OpenSSL (versions 0.9.3 and later) also support "P12" for PKCS#12-encoded files. + // see https://curl.se/libcurl/c/CURLOPT_SSLCERTTYPE.html + $ext = pathinfo($cert, \PATHINFO_EXTENSION); + if (preg_match('#^(der|p12)$#i', $ext)) { + $conf[\CURLOPT_SSLCERTTYPE] = strtoupper($ext); + } + $conf[\CURLOPT_SSLCERT] = $cert; + } + + if (isset($options['ssl_key'])) { + if (\is_array($options['ssl_key'])) { + if (\count($options['ssl_key']) === 2) { + [$sslKey, $conf[\CURLOPT_SSLKEYPASSWD]] = $options['ssl_key']; + } else { + [$sslKey] = $options['ssl_key']; + } + } + + $sslKey = $sslKey ?? $options['ssl_key']; + + if (!\file_exists($sslKey)) { + throw new \InvalidArgumentException("SSL private key not found: {$sslKey}"); + } + $conf[\CURLOPT_SSLKEY] = $sslKey; + } + + if (isset($options['progress'])) { + $progress = $options['progress']; + if (!\is_callable($progress)) { + throw new \InvalidArgumentException('progress client option must be callable'); + } + $conf[\CURLOPT_NOPROGRESS] = false; + $conf[\CURLOPT_PROGRESSFUNCTION] = static function ($resource, int $downloadSize, int $downloaded, int $uploadSize, int $uploaded) use ($progress) { + $progress($downloadSize, $downloaded, $uploadSize, $uploaded); + }; + } + + if (!empty($options['debug'])) { + $conf[\CURLOPT_STDERR] = Utils::debugResource($options['debug']); + $conf[\CURLOPT_VERBOSE] = true; + } + } + + /** + * This function ensures that a response was set on a transaction. If one + * was not set, then the request is retried if possible. This error + * typically means you are sending a payload, curl encountered a + * "Connection died, retrying a fresh connect" error, tried to rewind the + * stream, and then encountered a "necessary data rewind wasn't possible" + * error, causing the request to be sent through curl_multi_info_read() + * without an error status. + * + * @param callable(RequestInterface, array): PromiseInterface $handler + */ + private static function retryFailedRewind(callable $handler, EasyHandle $easy, array $ctx): PromiseInterface + { + try { + // Only rewind if the body has been read from. + $body = $easy->request->getBody(); + if ($body->tell() > 0) { + $body->rewind(); + } + } catch (\RuntimeException $e) { + $ctx['error'] = 'The connection unexpectedly failed without ' + .'providing an error. The request would have been retried, ' + .'but attempting to rewind the request body failed. ' + .'Exception: '.$e; + + return self::createRejection($easy, $ctx); + } + + // Retry no more than 3 times before giving up. + if (!isset($easy->options['_curl_retries'])) { + $easy->options['_curl_retries'] = 1; + } elseif ($easy->options['_curl_retries'] == 2) { + $ctx['error'] = 'The cURL request was retried 3 times ' + .'and did not succeed. The most likely reason for the failure ' + .'is that cURL was unable to rewind the body of the request ' + .'and subsequent retries resulted in the same error. Turn on ' + .'the debug option to see what went wrong. See ' + .'https://bugs.php.net/bug.php?id=47204 for more information.'; + + return self::createRejection($easy, $ctx); + } else { + ++$easy->options['_curl_retries']; + } + + return $handler($easy->request, $easy->options); + } + + private function createHeaderFn(EasyHandle $easy): callable + { + if (isset($easy->options['on_headers'])) { + $onHeaders = $easy->options['on_headers']; + + if (!\is_callable($onHeaders)) { + throw new \InvalidArgumentException('on_headers must be callable'); + } + } else { + $onHeaders = null; + } + + return static function ($ch, $h) use ( + $onHeaders, + $easy, + &$startingResponse + ) { + $value = \trim($h); + if ($value === '') { + $startingResponse = true; + try { + $easy->createResponse(); + } catch (\Exception $e) { + $easy->createResponseException = $e; + + return -1; + } + if ($onHeaders !== null) { + try { + $onHeaders($easy->response); + } catch (\Exception $e) { + // Associate the exception with the handle and trigger + // a curl header write error by returning 0. + $easy->onHeadersException = $e; + + return -1; + } + } + } elseif ($startingResponse) { + $startingResponse = false; + $easy->headers = [$value]; + } else { + $easy->headers[] = $value; + } + + return \strlen($h); + }; + } + + public function __destruct() + { + foreach ($this->handles as $id => $handle) { + \curl_close($handle); + unset($this->handles[$id]); + } + } +} diff --git a/3rdparty/guzzlehttp/guzzle/src/Handler/CurlFactoryInterface.php b/3rdparty/guzzlehttp/guzzle/src/Handler/CurlFactoryInterface.php new file mode 100644 index 00000000..fe57ed5d --- /dev/null +++ b/3rdparty/guzzlehttp/guzzle/src/Handler/CurlFactoryInterface.php @@ -0,0 +1,25 @@ +factory = $options['handle_factory'] + ?? new CurlFactory(3); + } + + public function __invoke(RequestInterface $request, array $options): PromiseInterface + { + if (isset($options['delay'])) { + \usleep($options['delay'] * 1000); + } + + $easy = $this->factory->create($request, $options); + \curl_exec($easy->handle); + $easy->errno = \curl_errno($easy->handle); + + return CurlFactory::finish($this, $easy, $this->factory); + } +} diff --git a/3rdparty/guzzlehttp/guzzle/src/Handler/CurlMultiHandler.php b/3rdparty/guzzlehttp/guzzle/src/Handler/CurlMultiHandler.php new file mode 100644 index 00000000..73a6abe3 --- /dev/null +++ b/3rdparty/guzzlehttp/guzzle/src/Handler/CurlMultiHandler.php @@ -0,0 +1,284 @@ + An array of delay times, indexed by handle id in `addRequest`. + * + * @see CurlMultiHandler::addRequest + */ + private $delays = []; + + /** + * @var array An associative array of CURLMOPT_* options and corresponding values for curl_multi_setopt() + */ + private $options = []; + + /** @var resource|\CurlMultiHandle */ + private $_mh; + + /** + * This handler accepts the following options: + * + * - handle_factory: An optional factory used to create curl handles + * - select_timeout: Optional timeout (in seconds) to block before timing + * out while selecting curl handles. Defaults to 1 second. + * - options: An associative array of CURLMOPT_* options and + * corresponding values for curl_multi_setopt() + */ + public function __construct(array $options = []) + { + $this->factory = $options['handle_factory'] ?? new CurlFactory(50); + + if (isset($options['select_timeout'])) { + $this->selectTimeout = $options['select_timeout']; + } elseif ($selectTimeout = Utils::getenv('GUZZLE_CURL_SELECT_TIMEOUT')) { + @trigger_error('Since guzzlehttp/guzzle 7.2.0: Using environment variable GUZZLE_CURL_SELECT_TIMEOUT is deprecated. Use option "select_timeout" instead.', \E_USER_DEPRECATED); + $this->selectTimeout = (int) $selectTimeout; + } else { + $this->selectTimeout = 1; + } + + $this->options = $options['options'] ?? []; + + // unsetting the property forces the first access to go through + // __get(). + unset($this->_mh); + } + + /** + * @param string $name + * + * @return resource|\CurlMultiHandle + * + * @throws \BadMethodCallException when another field as `_mh` will be gotten + * @throws \RuntimeException when curl can not initialize a multi handle + */ + public function __get($name) + { + if ($name !== '_mh') { + throw new \BadMethodCallException("Can not get other property as '_mh'."); + } + + $multiHandle = \curl_multi_init(); + + if (false === $multiHandle) { + throw new \RuntimeException('Can not initialize curl multi handle.'); + } + + $this->_mh = $multiHandle; + + foreach ($this->options as $option => $value) { + // A warning is raised in case of a wrong option. + curl_multi_setopt($this->_mh, $option, $value); + } + + return $this->_mh; + } + + public function __destruct() + { + if (isset($this->_mh)) { + \curl_multi_close($this->_mh); + unset($this->_mh); + } + } + + public function __invoke(RequestInterface $request, array $options): PromiseInterface + { + $easy = $this->factory->create($request, $options); + $id = (int) $easy->handle; + + $promise = new Promise( + [$this, 'execute'], + function () use ($id) { + return $this->cancel($id); + } + ); + + $this->addRequest(['easy' => $easy, 'deferred' => $promise]); + + return $promise; + } + + /** + * Ticks the curl event loop. + */ + public function tick(): void + { + // Add any delayed handles if needed. + if ($this->delays) { + $currentTime = Utils::currentTime(); + foreach ($this->delays as $id => $delay) { + if ($currentTime >= $delay) { + unset($this->delays[$id]); + \curl_multi_add_handle( + $this->_mh, + $this->handles[$id]['easy']->handle + ); + } + } + } + + // Run curl_multi_exec in the queue to enable other async tasks to run + P\Utils::queue()->add(Closure::fromCallable([$this, 'tickInQueue'])); + + // Step through the task queue which may add additional requests. + P\Utils::queue()->run(); + + if ($this->active && \curl_multi_select($this->_mh, $this->selectTimeout) === -1) { + // Perform a usleep if a select returns -1. + // See: https://bugs.php.net/bug.php?id=61141 + \usleep(250); + } + + while (\curl_multi_exec($this->_mh, $this->active) === \CURLM_CALL_MULTI_PERFORM) { + // Prevent busy looping for slow HTTP requests. + \curl_multi_select($this->_mh, $this->selectTimeout); + } + + $this->processMessages(); + } + + /** + * Runs \curl_multi_exec() inside the event loop, to prevent busy looping + */ + private function tickInQueue(): void + { + if (\curl_multi_exec($this->_mh, $this->active) === \CURLM_CALL_MULTI_PERFORM) { + \curl_multi_select($this->_mh, 0); + P\Utils::queue()->add(Closure::fromCallable([$this, 'tickInQueue'])); + } + } + + /** + * Runs until all outstanding connections have completed. + */ + public function execute(): void + { + $queue = P\Utils::queue(); + + while ($this->handles || !$queue->isEmpty()) { + // If there are no transfers, then sleep for the next delay + if (!$this->active && $this->delays) { + \usleep($this->timeToNext()); + } + $this->tick(); + } + } + + private function addRequest(array $entry): void + { + $easy = $entry['easy']; + $id = (int) $easy->handle; + $this->handles[$id] = $entry; + if (empty($easy->options['delay'])) { + \curl_multi_add_handle($this->_mh, $easy->handle); + } else { + $this->delays[$id] = Utils::currentTime() + ($easy->options['delay'] / 1000); + } + } + + /** + * Cancels a handle from sending and removes references to it. + * + * @param int $id Handle ID to cancel and remove. + * + * @return bool True on success, false on failure. + */ + private function cancel($id): bool + { + if (!is_int($id)) { + trigger_deprecation('guzzlehttp/guzzle', '7.4', 'Not passing an integer to %s::%s() is deprecated and will cause an error in 8.0.', __CLASS__, __FUNCTION__); + } + + // Cannot cancel if it has been processed. + if (!isset($this->handles[$id])) { + return false; + } + + $handle = $this->handles[$id]['easy']->handle; + unset($this->delays[$id], $this->handles[$id]); + \curl_multi_remove_handle($this->_mh, $handle); + \curl_close($handle); + + return true; + } + + private function processMessages(): void + { + while ($done = \curl_multi_info_read($this->_mh)) { + if ($done['msg'] !== \CURLMSG_DONE) { + // if it's not done, then it would be premature to remove the handle. ref https://github.com/guzzle/guzzle/pull/2892#issuecomment-945150216 + continue; + } + $id = (int) $done['handle']; + \curl_multi_remove_handle($this->_mh, $done['handle']); + + if (!isset($this->handles[$id])) { + // Probably was cancelled. + continue; + } + + $entry = $this->handles[$id]; + unset($this->handles[$id], $this->delays[$id]); + $entry['easy']->errno = $done['result']; + $entry['deferred']->resolve( + CurlFactory::finish($this, $entry['easy'], $this->factory) + ); + } + } + + private function timeToNext(): int + { + $currentTime = Utils::currentTime(); + $nextTime = \PHP_INT_MAX; + foreach ($this->delays as $time) { + if ($time < $nextTime) { + $nextTime = $time; + } + } + + return ((int) \max(0, $nextTime - $currentTime)) * 1000000; + } +} diff --git a/3rdparty/guzzlehttp/guzzle/src/Handler/EasyHandle.php b/3rdparty/guzzlehttp/guzzle/src/Handler/EasyHandle.php new file mode 100644 index 00000000..1bc39f4b --- /dev/null +++ b/3rdparty/guzzlehttp/guzzle/src/Handler/EasyHandle.php @@ -0,0 +1,112 @@ +headers); + + $normalizedKeys = Utils::normalizeHeaderKeys($headers); + + if (!empty($this->options['decode_content']) && isset($normalizedKeys['content-encoding'])) { + $headers['x-encoded-content-encoding'] = $headers[$normalizedKeys['content-encoding']]; + unset($headers[$normalizedKeys['content-encoding']]); + if (isset($normalizedKeys['content-length'])) { + $headers['x-encoded-content-length'] = $headers[$normalizedKeys['content-length']]; + + $bodyLength = (int) $this->sink->getSize(); + if ($bodyLength) { + $headers[$normalizedKeys['content-length']] = $bodyLength; + } else { + unset($headers[$normalizedKeys['content-length']]); + } + } + } + + // Attach a response to the easy handle with the parsed headers. + $this->response = new Response( + $status, + $headers, + $this->sink, + $ver, + $reason + ); + } + + /** + * @param string $name + * + * @return void + * + * @throws \BadMethodCallException + */ + public function __get($name) + { + $msg = $name === 'handle' ? 'The EasyHandle has been released' : 'Invalid property: '.$name; + throw new \BadMethodCallException($msg); + } +} diff --git a/3rdparty/guzzlehttp/guzzle/src/Handler/HeaderProcessor.php b/3rdparty/guzzlehttp/guzzle/src/Handler/HeaderProcessor.php new file mode 100644 index 00000000..5554b8fa --- /dev/null +++ b/3rdparty/guzzlehttp/guzzle/src/Handler/HeaderProcessor.php @@ -0,0 +1,42 @@ +|null $queue The parameters to be passed to the append function, as an indexed array. + * @param callable|null $onFulfilled Callback to invoke when the return value is fulfilled. + * @param callable|null $onRejected Callback to invoke when the return value is rejected. + */ + public function __construct(?array $queue = null, ?callable $onFulfilled = null, ?callable $onRejected = null) + { + $this->onFulfilled = $onFulfilled; + $this->onRejected = $onRejected; + + if ($queue) { + // array_values included for BC + $this->append(...array_values($queue)); + } + } + + public function __invoke(RequestInterface $request, array $options): PromiseInterface + { + if (!$this->queue) { + throw new \OutOfBoundsException('Mock queue is empty'); + } + + if (isset($options['delay']) && \is_numeric($options['delay'])) { + \usleep((int) $options['delay'] * 1000); + } + + $this->lastRequest = $request; + $this->lastOptions = $options; + $response = \array_shift($this->queue); + + if (isset($options['on_headers'])) { + if (!\is_callable($options['on_headers'])) { + throw new \InvalidArgumentException('on_headers must be callable'); + } + try { + $options['on_headers']($response); + } catch (\Exception $e) { + $msg = 'An error was encountered during the on_headers event'; + $response = new RequestException($msg, $request, $response, $e); + } + } + + if (\is_callable($response)) { + $response = $response($request, $options); + } + + $response = $response instanceof \Throwable + ? P\Create::rejectionFor($response) + : P\Create::promiseFor($response); + + return $response->then( + function (?ResponseInterface $value) use ($request, $options) { + $this->invokeStats($request, $options, $value); + if ($this->onFulfilled) { + ($this->onFulfilled)($value); + } + + if ($value !== null && isset($options['sink'])) { + $contents = (string) $value->getBody(); + $sink = $options['sink']; + + if (\is_resource($sink)) { + \fwrite($sink, $contents); + } elseif (\is_string($sink)) { + \file_put_contents($sink, $contents); + } elseif ($sink instanceof StreamInterface) { + $sink->write($contents); + } + } + + return $value; + }, + function ($reason) use ($request, $options) { + $this->invokeStats($request, $options, null, $reason); + if ($this->onRejected) { + ($this->onRejected)($reason); + } + + return P\Create::rejectionFor($reason); + } + ); + } + + /** + * Adds one or more variadic requests, exceptions, callables, or promises + * to the queue. + * + * @param mixed ...$values + */ + public function append(...$values): void + { + foreach ($values as $value) { + if ($value instanceof ResponseInterface + || $value instanceof \Throwable + || $value instanceof PromiseInterface + || \is_callable($value) + ) { + $this->queue[] = $value; + } else { + throw new \TypeError('Expected a Response, Promise, Throwable or callable. Found '.Utils::describeType($value)); + } + } + } + + /** + * Get the last received request. + */ + public function getLastRequest(): ?RequestInterface + { + return $this->lastRequest; + } + + /** + * Get the last received request options. + */ + public function getLastOptions(): array + { + return $this->lastOptions; + } + + /** + * Returns the number of remaining items in the queue. + */ + public function count(): int + { + return \count($this->queue); + } + + public function reset(): void + { + $this->queue = []; + } + + /** + * @param mixed $reason Promise or reason. + */ + private function invokeStats( + RequestInterface $request, + array $options, + ?ResponseInterface $response = null, + $reason = null + ): void { + if (isset($options['on_stats'])) { + $transferTime = $options['transfer_time'] ?? 0; + $stats = new TransferStats($request, $response, $transferTime, $reason); + ($options['on_stats'])($stats); + } + } +} diff --git a/3rdparty/guzzlehttp/guzzle/src/Handler/Proxy.php b/3rdparty/guzzlehttp/guzzle/src/Handler/Proxy.php new file mode 100644 index 00000000..9df70cf2 --- /dev/null +++ b/3rdparty/guzzlehttp/guzzle/src/Handler/Proxy.php @@ -0,0 +1,51 @@ +getProtocolVersion(); + + if ('1.0' !== $protocolVersion && '1.1' !== $protocolVersion) { + throw new ConnectException(sprintf('HTTP/%s is not supported by the stream handler.', $protocolVersion), $request); + } + + $startTime = isset($options['on_stats']) ? Utils::currentTime() : null; + + try { + // Does not support the expect header. + $request = $request->withoutHeader('Expect'); + + // Append a content-length header if body size is zero to match + // the behavior of `CurlHandler` + if ( + ( + 0 === \strcasecmp('PUT', $request->getMethod()) + || 0 === \strcasecmp('POST', $request->getMethod()) + ) + && 0 === $request->getBody()->getSize() + ) { + $request = $request->withHeader('Content-Length', '0'); + } + + return $this->createResponse( + $request, + $options, + $this->createStream($request, $options), + $startTime + ); + } catch (\InvalidArgumentException $e) { + throw $e; + } catch (\Exception $e) { + // Determine if the error was a networking error. + $message = $e->getMessage(); + // This list can probably get more comprehensive. + if (false !== \strpos($message, 'getaddrinfo') // DNS lookup failed + || false !== \strpos($message, 'Connection refused') + || false !== \strpos($message, "couldn't connect to host") // error on HHVM + || false !== \strpos($message, 'connection attempt failed') + ) { + $e = new ConnectException($e->getMessage(), $request, $e); + } else { + $e = RequestException::wrapException($request, $e); + } + $this->invokeStats($options, $request, $startTime, null, $e); + + return P\Create::rejectionFor($e); + } + } + + private function invokeStats( + array $options, + RequestInterface $request, + ?float $startTime, + ?ResponseInterface $response = null, + ?\Throwable $error = null + ): void { + if (isset($options['on_stats'])) { + $stats = new TransferStats($request, $response, Utils::currentTime() - $startTime, $error, []); + ($options['on_stats'])($stats); + } + } + + /** + * @param resource $stream + */ + private function createResponse(RequestInterface $request, array $options, $stream, ?float $startTime): PromiseInterface + { + $hdrs = $this->lastHeaders; + $this->lastHeaders = []; + + try { + [$ver, $status, $reason, $headers] = HeaderProcessor::parseHeaders($hdrs); + } catch (\Exception $e) { + return P\Create::rejectionFor( + new RequestException('An error was encountered while creating the response', $request, null, $e) + ); + } + + [$stream, $headers] = $this->checkDecode($options, $headers, $stream); + $stream = Psr7\Utils::streamFor($stream); + $sink = $stream; + + if (\strcasecmp('HEAD', $request->getMethod())) { + $sink = $this->createSink($stream, $options); + } + + try { + $response = new Psr7\Response($status, $headers, $sink, $ver, $reason); + } catch (\Exception $e) { + return P\Create::rejectionFor( + new RequestException('An error was encountered while creating the response', $request, null, $e) + ); + } + + if (isset($options['on_headers'])) { + try { + $options['on_headers']($response); + } catch (\Exception $e) { + return P\Create::rejectionFor( + new RequestException('An error was encountered during the on_headers event', $request, $response, $e) + ); + } + } + + // Do not drain when the request is a HEAD request because they have + // no body. + if ($sink !== $stream) { + $this->drain($stream, $sink, $response->getHeaderLine('Content-Length')); + } + + $this->invokeStats($options, $request, $startTime, $response, null); + + return new FulfilledPromise($response); + } + + private function createSink(StreamInterface $stream, array $options): StreamInterface + { + if (!empty($options['stream'])) { + return $stream; + } + + $sink = $options['sink'] ?? Psr7\Utils::tryFopen('php://temp', 'r+'); + + return \is_string($sink) ? new Psr7\LazyOpenStream($sink, 'w+') : Psr7\Utils::streamFor($sink); + } + + /** + * @param resource $stream + */ + private function checkDecode(array $options, array $headers, $stream): array + { + // Automatically decode responses when instructed. + if (!empty($options['decode_content'])) { + $normalizedKeys = Utils::normalizeHeaderKeys($headers); + if (isset($normalizedKeys['content-encoding'])) { + $encoding = $headers[$normalizedKeys['content-encoding']]; + if ($encoding[0] === 'gzip' || $encoding[0] === 'deflate') { + $stream = new Psr7\InflateStream(Psr7\Utils::streamFor($stream)); + $headers['x-encoded-content-encoding'] = $headers[$normalizedKeys['content-encoding']]; + + // Remove content-encoding header + unset($headers[$normalizedKeys['content-encoding']]); + + // Fix content-length header + if (isset($normalizedKeys['content-length'])) { + $headers['x-encoded-content-length'] = $headers[$normalizedKeys['content-length']]; + $length = (int) $stream->getSize(); + if ($length === 0) { + unset($headers[$normalizedKeys['content-length']]); + } else { + $headers[$normalizedKeys['content-length']] = [$length]; + } + } + } + } + } + + return [$stream, $headers]; + } + + /** + * Drains the source stream into the "sink" client option. + * + * @param string $contentLength Header specifying the amount of + * data to read. + * + * @throws \RuntimeException when the sink option is invalid. + */ + private function drain(StreamInterface $source, StreamInterface $sink, string $contentLength): StreamInterface + { + // If a content-length header is provided, then stop reading once + // that number of bytes has been read. This can prevent infinitely + // reading from a stream when dealing with servers that do not honor + // Connection: Close headers. + Psr7\Utils::copyToStream( + $source, + $sink, + (\strlen($contentLength) > 0 && (int) $contentLength > 0) ? (int) $contentLength : -1 + ); + + $sink->seek(0); + $source->close(); + + return $sink; + } + + /** + * Create a resource and check to ensure it was created successfully + * + * @param callable $callback Callable that returns stream resource + * + * @return resource + * + * @throws \RuntimeException on error + */ + private function createResource(callable $callback) + { + $errors = []; + \set_error_handler(static function ($_, $msg, $file, $line) use (&$errors): bool { + $errors[] = [ + 'message' => $msg, + 'file' => $file, + 'line' => $line, + ]; + + return true; + }); + + try { + $resource = $callback(); + } finally { + \restore_error_handler(); + } + + if (!$resource) { + $message = 'Error creating resource: '; + foreach ($errors as $err) { + foreach ($err as $key => $value) { + $message .= "[$key] $value".\PHP_EOL; + } + } + throw new \RuntimeException(\trim($message)); + } + + return $resource; + } + + /** + * @return resource + */ + private function createStream(RequestInterface $request, array $options) + { + static $methods; + if (!$methods) { + $methods = \array_flip(\get_class_methods(__CLASS__)); + } + + if (!\in_array($request->getUri()->getScheme(), ['http', 'https'])) { + throw new RequestException(\sprintf("The scheme '%s' is not supported.", $request->getUri()->getScheme()), $request); + } + + // HTTP/1.1 streams using the PHP stream wrapper require a + // Connection: close header + if ($request->getProtocolVersion() === '1.1' + && !$request->hasHeader('Connection') + ) { + $request = $request->withHeader('Connection', 'close'); + } + + // Ensure SSL is verified by default + if (!isset($options['verify'])) { + $options['verify'] = true; + } + + $params = []; + $context = $this->getDefaultContext($request); + + if (isset($options['on_headers']) && !\is_callable($options['on_headers'])) { + throw new \InvalidArgumentException('on_headers must be callable'); + } + + if (!empty($options)) { + foreach ($options as $key => $value) { + $method = "add_{$key}"; + if (isset($methods[$method])) { + $this->{$method}($request, $context, $value, $params); + } + } + } + + if (isset($options['stream_context'])) { + if (!\is_array($options['stream_context'])) { + throw new \InvalidArgumentException('stream_context must be an array'); + } + $context = \array_replace_recursive($context, $options['stream_context']); + } + + // Microsoft NTLM authentication only supported with curl handler + if (isset($options['auth'][2]) && 'ntlm' === $options['auth'][2]) { + throw new \InvalidArgumentException('Microsoft NTLM authentication only supported with curl handler'); + } + + $uri = $this->resolveHost($request, $options); + + $contextResource = $this->createResource( + static function () use ($context, $params) { + return \stream_context_create($context, $params); + } + ); + + return $this->createResource( + function () use ($uri, &$http_response_header, $contextResource, $context, $options, $request) { + $resource = @\fopen((string) $uri, 'r', false, $contextResource); + $this->lastHeaders = $http_response_header ?? []; + + if (false === $resource) { + throw new ConnectException(sprintf('Connection refused for URI %s', $uri), $request, null, $context); + } + + if (isset($options['read_timeout'])) { + $readTimeout = $options['read_timeout']; + $sec = (int) $readTimeout; + $usec = ($readTimeout - $sec) * 100000; + \stream_set_timeout($resource, $sec, $usec); + } + + return $resource; + } + ); + } + + private function resolveHost(RequestInterface $request, array $options): UriInterface + { + $uri = $request->getUri(); + + if (isset($options['force_ip_resolve']) && !\filter_var($uri->getHost(), \FILTER_VALIDATE_IP)) { + if ('v4' === $options['force_ip_resolve']) { + $records = \dns_get_record($uri->getHost(), \DNS_A); + if (false === $records || !isset($records[0]['ip'])) { + throw new ConnectException(\sprintf("Could not resolve IPv4 address for host '%s'", $uri->getHost()), $request); + } + + return $uri->withHost($records[0]['ip']); + } + if ('v6' === $options['force_ip_resolve']) { + $records = \dns_get_record($uri->getHost(), \DNS_AAAA); + if (false === $records || !isset($records[0]['ipv6'])) { + throw new ConnectException(\sprintf("Could not resolve IPv6 address for host '%s'", $uri->getHost()), $request); + } + + return $uri->withHost('['.$records[0]['ipv6'].']'); + } + } + + return $uri; + } + + private function getDefaultContext(RequestInterface $request): array + { + $headers = ''; + foreach ($request->getHeaders() as $name => $value) { + foreach ($value as $val) { + $headers .= "$name: $val\r\n"; + } + } + + $context = [ + 'http' => [ + 'method' => $request->getMethod(), + 'header' => $headers, + 'protocol_version' => $request->getProtocolVersion(), + 'ignore_errors' => true, + 'follow_location' => 0, + ], + 'ssl' => [ + 'peer_name' => $request->getUri()->getHost(), + ], + ]; + + $body = (string) $request->getBody(); + + if ('' !== $body) { + $context['http']['content'] = $body; + // Prevent the HTTP handler from adding a Content-Type header. + if (!$request->hasHeader('Content-Type')) { + $context['http']['header'] .= "Content-Type:\r\n"; + } + } + + $context['http']['header'] = \rtrim($context['http']['header']); + + return $context; + } + + /** + * @param mixed $value as passed via Request transfer options. + */ + private function add_proxy(RequestInterface $request, array &$options, $value, array &$params): void + { + $uri = null; + + if (!\is_array($value)) { + $uri = $value; + } else { + $scheme = $request->getUri()->getScheme(); + if (isset($value[$scheme])) { + if (!isset($value['no']) || !Utils::isHostInNoProxy($request->getUri()->getHost(), $value['no'])) { + $uri = $value[$scheme]; + } + } + } + + if (!$uri) { + return; + } + + $parsed = $this->parse_proxy($uri); + $options['http']['proxy'] = $parsed['proxy']; + + if ($parsed['auth']) { + if (!isset($options['http']['header'])) { + $options['http']['header'] = []; + } + $options['http']['header'] .= "\r\nProxy-Authorization: {$parsed['auth']}"; + } + } + + /** + * Parses the given proxy URL to make it compatible with the format PHP's stream context expects. + */ + private function parse_proxy(string $url): array + { + $parsed = \parse_url($url); + + if ($parsed !== false && isset($parsed['scheme']) && $parsed['scheme'] === 'http') { + if (isset($parsed['host']) && isset($parsed['port'])) { + $auth = null; + if (isset($parsed['user']) && isset($parsed['pass'])) { + $auth = \base64_encode("{$parsed['user']}:{$parsed['pass']}"); + } + + return [ + 'proxy' => "tcp://{$parsed['host']}:{$parsed['port']}", + 'auth' => $auth ? "Basic {$auth}" : null, + ]; + } + } + + // Return proxy as-is. + return [ + 'proxy' => $url, + 'auth' => null, + ]; + } + + /** + * @param mixed $value as passed via Request transfer options. + */ + private function add_timeout(RequestInterface $request, array &$options, $value, array &$params): void + { + if ($value > 0) { + $options['http']['timeout'] = $value; + } + } + + /** + * @param mixed $value as passed via Request transfer options. + */ + private function add_crypto_method(RequestInterface $request, array &$options, $value, array &$params): void + { + if ( + $value === \STREAM_CRYPTO_METHOD_TLSv1_0_CLIENT + || $value === \STREAM_CRYPTO_METHOD_TLSv1_1_CLIENT + || $value === \STREAM_CRYPTO_METHOD_TLSv1_2_CLIENT + || (defined('STREAM_CRYPTO_METHOD_TLSv1_3_CLIENT') && $value === \STREAM_CRYPTO_METHOD_TLSv1_3_CLIENT) + ) { + $options['http']['crypto_method'] = $value; + + return; + } + + throw new \InvalidArgumentException('Invalid crypto_method request option: unknown version provided'); + } + + /** + * @param mixed $value as passed via Request transfer options. + */ + private function add_verify(RequestInterface $request, array &$options, $value, array &$params): void + { + if ($value === false) { + $options['ssl']['verify_peer'] = false; + $options['ssl']['verify_peer_name'] = false; + + return; + } + + if (\is_string($value)) { + $options['ssl']['cafile'] = $value; + if (!\file_exists($value)) { + throw new \RuntimeException("SSL CA bundle not found: $value"); + } + } elseif ($value !== true) { + throw new \InvalidArgumentException('Invalid verify request option'); + } + + $options['ssl']['verify_peer'] = true; + $options['ssl']['verify_peer_name'] = true; + $options['ssl']['allow_self_signed'] = false; + } + + /** + * @param mixed $value as passed via Request transfer options. + */ + private function add_cert(RequestInterface $request, array &$options, $value, array &$params): void + { + if (\is_array($value)) { + $options['ssl']['passphrase'] = $value[1]; + $value = $value[0]; + } + + if (!\file_exists($value)) { + throw new \RuntimeException("SSL certificate not found: {$value}"); + } + + $options['ssl']['local_cert'] = $value; + } + + /** + * @param mixed $value as passed via Request transfer options. + */ + private function add_progress(RequestInterface $request, array &$options, $value, array &$params): void + { + self::addNotification( + $params, + static function ($code, $a, $b, $c, $transferred, $total) use ($value) { + if ($code == \STREAM_NOTIFY_PROGRESS) { + // The upload progress cannot be determined. Use 0 for cURL compatibility: + // https://curl.se/libcurl/c/CURLOPT_PROGRESSFUNCTION.html + $value($total, $transferred, 0, 0); + } + } + ); + } + + /** + * @param mixed $value as passed via Request transfer options. + */ + private function add_debug(RequestInterface $request, array &$options, $value, array &$params): void + { + if ($value === false) { + return; + } + + static $map = [ + \STREAM_NOTIFY_CONNECT => 'CONNECT', + \STREAM_NOTIFY_AUTH_REQUIRED => 'AUTH_REQUIRED', + \STREAM_NOTIFY_AUTH_RESULT => 'AUTH_RESULT', + \STREAM_NOTIFY_MIME_TYPE_IS => 'MIME_TYPE_IS', + \STREAM_NOTIFY_FILE_SIZE_IS => 'FILE_SIZE_IS', + \STREAM_NOTIFY_REDIRECTED => 'REDIRECTED', + \STREAM_NOTIFY_PROGRESS => 'PROGRESS', + \STREAM_NOTIFY_FAILURE => 'FAILURE', + \STREAM_NOTIFY_COMPLETED => 'COMPLETED', + \STREAM_NOTIFY_RESOLVE => 'RESOLVE', + ]; + static $args = ['severity', 'message', 'message_code', 'bytes_transferred', 'bytes_max']; + + $value = Utils::debugResource($value); + $ident = $request->getMethod().' '.$request->getUri()->withFragment(''); + self::addNotification( + $params, + static function (int $code, ...$passed) use ($ident, $value, $map, $args): void { + \fprintf($value, '<%s> [%s] ', $ident, $map[$code]); + foreach (\array_filter($passed) as $i => $v) { + \fwrite($value, $args[$i].': "'.$v.'" '); + } + \fwrite($value, "\n"); + } + ); + } + + private static function addNotification(array &$params, callable $notify): void + { + // Wrap the existing function if needed. + if (!isset($params['notification'])) { + $params['notification'] = $notify; + } else { + $params['notification'] = self::callArray([ + $params['notification'], + $notify, + ]); + } + } + + private static function callArray(array $functions): callable + { + return static function (...$args) use ($functions) { + foreach ($functions as $fn) { + $fn(...$args); + } + }; + } +} diff --git a/3rdparty/guzzlehttp/guzzle/src/HandlerStack.php b/3rdparty/guzzlehttp/guzzle/src/HandlerStack.php new file mode 100644 index 00000000..03f9a18f --- /dev/null +++ b/3rdparty/guzzlehttp/guzzle/src/HandlerStack.php @@ -0,0 +1,275 @@ +push(Middleware::httpErrors(), 'http_errors'); + $stack->push(Middleware::redirect(), 'allow_redirects'); + $stack->push(Middleware::cookies(), 'cookies'); + $stack->push(Middleware::prepareBody(), 'prepare_body'); + + return $stack; + } + + /** + * @param (callable(RequestInterface, array): PromiseInterface)|null $handler Underlying HTTP handler. + */ + public function __construct(?callable $handler = null) + { + $this->handler = $handler; + } + + /** + * Invokes the handler stack as a composed handler + * + * @return ResponseInterface|PromiseInterface + */ + public function __invoke(RequestInterface $request, array $options) + { + $handler = $this->resolve(); + + return $handler($request, $options); + } + + /** + * Dumps a string representation of the stack. + * + * @return string + */ + public function __toString() + { + $depth = 0; + $stack = []; + + if ($this->handler !== null) { + $stack[] = '0) Handler: '.$this->debugCallable($this->handler); + } + + $result = ''; + foreach (\array_reverse($this->stack) as $tuple) { + ++$depth; + $str = "{$depth}) Name: '{$tuple[1]}', "; + $str .= 'Function: '.$this->debugCallable($tuple[0]); + $result = "> {$str}\n{$result}"; + $stack[] = $str; + } + + foreach (\array_keys($stack) as $k) { + $result .= "< {$stack[$k]}\n"; + } + + return $result; + } + + /** + * Set the HTTP handler that actually returns a promise. + * + * @param callable(RequestInterface, array): PromiseInterface $handler Accepts a request and array of options and + * returns a Promise. + */ + public function setHandler(callable $handler): void + { + $this->handler = $handler; + $this->cached = null; + } + + /** + * Returns true if the builder has a handler. + */ + public function hasHandler(): bool + { + return $this->handler !== null; + } + + /** + * Unshift a middleware to the bottom of the stack. + * + * @param callable(callable): callable $middleware Middleware function + * @param string $name Name to register for this middleware. + */ + public function unshift(callable $middleware, ?string $name = null): void + { + \array_unshift($this->stack, [$middleware, $name]); + $this->cached = null; + } + + /** + * Push a middleware to the top of the stack. + * + * @param callable(callable): callable $middleware Middleware function + * @param string $name Name to register for this middleware. + */ + public function push(callable $middleware, string $name = ''): void + { + $this->stack[] = [$middleware, $name]; + $this->cached = null; + } + + /** + * Add a middleware before another middleware by name. + * + * @param string $findName Middleware to find + * @param callable(callable): callable $middleware Middleware function + * @param string $withName Name to register for this middleware. + */ + public function before(string $findName, callable $middleware, string $withName = ''): void + { + $this->splice($findName, $withName, $middleware, true); + } + + /** + * Add a middleware after another middleware by name. + * + * @param string $findName Middleware to find + * @param callable(callable): callable $middleware Middleware function + * @param string $withName Name to register for this middleware. + */ + public function after(string $findName, callable $middleware, string $withName = ''): void + { + $this->splice($findName, $withName, $middleware, false); + } + + /** + * Remove a middleware by instance or name from the stack. + * + * @param callable|string $remove Middleware to remove by instance or name. + */ + public function remove($remove): void + { + if (!is_string($remove) && !is_callable($remove)) { + trigger_deprecation('guzzlehttp/guzzle', '7.4', 'Not passing a callable or string to %s::%s() is deprecated and will cause an error in 8.0.', __CLASS__, __FUNCTION__); + } + + $this->cached = null; + $idx = \is_callable($remove) ? 0 : 1; + $this->stack = \array_values(\array_filter( + $this->stack, + static function ($tuple) use ($idx, $remove) { + return $tuple[$idx] !== $remove; + } + )); + } + + /** + * Compose the middleware and handler into a single callable function. + * + * @return callable(RequestInterface, array): PromiseInterface + */ + public function resolve(): callable + { + if ($this->cached === null) { + if (($prev = $this->handler) === null) { + throw new \LogicException('No handler has been specified'); + } + + foreach (\array_reverse($this->stack) as $fn) { + /** @var callable(RequestInterface, array): PromiseInterface $prev */ + $prev = $fn[0]($prev); + } + + $this->cached = $prev; + } + + return $this->cached; + } + + private function findByName(string $name): int + { + foreach ($this->stack as $k => $v) { + if ($v[1] === $name) { + return $k; + } + } + + throw new \InvalidArgumentException("Middleware not found: $name"); + } + + /** + * Splices a function into the middleware list at a specific position. + */ + private function splice(string $findName, string $withName, callable $middleware, bool $before): void + { + $this->cached = null; + $idx = $this->findByName($findName); + $tuple = [$middleware, $withName]; + + if ($before) { + if ($idx === 0) { + \array_unshift($this->stack, $tuple); + } else { + $replacement = [$tuple, $this->stack[$idx]]; + \array_splice($this->stack, $idx, 1, $replacement); + } + } elseif ($idx === \count($this->stack) - 1) { + $this->stack[] = $tuple; + } else { + $replacement = [$this->stack[$idx], $tuple]; + \array_splice($this->stack, $idx, 1, $replacement); + } + } + + /** + * Provides a debug string for a given callable. + * + * @param callable|string $fn Function to write as a string. + */ + private function debugCallable($fn): string + { + if (\is_string($fn)) { + return "callable({$fn})"; + } + + if (\is_array($fn)) { + return \is_string($fn[0]) + ? "callable({$fn[0]}::{$fn[1]})" + : "callable(['".\get_class($fn[0])."', '{$fn[1]}'])"; + } + + /** @var object $fn */ + return 'callable('.\spl_object_hash($fn).')'; + } +} diff --git a/3rdparty/guzzlehttp/guzzle/src/MessageFormatter.php b/3rdparty/guzzlehttp/guzzle/src/MessageFormatter.php new file mode 100644 index 00000000..9b77eee8 --- /dev/null +++ b/3rdparty/guzzlehttp/guzzle/src/MessageFormatter.php @@ -0,0 +1,199 @@ +>>>>>>>\n{request}\n<<<<<<<<\n{response}\n--------\n{error}"; + public const SHORT = '[{ts}] "{method} {target} HTTP/{version}" {code}'; + + /** + * @var string Template used to format log messages + */ + private $template; + + /** + * @param string $template Log message template + */ + public function __construct(?string $template = self::CLF) + { + $this->template = $template ?: self::CLF; + } + + /** + * Returns a formatted message string. + * + * @param RequestInterface $request Request that was sent + * @param ResponseInterface|null $response Response that was received + * @param \Throwable|null $error Exception that was received + */ + public function format(RequestInterface $request, ?ResponseInterface $response = null, ?\Throwable $error = null): string + { + $cache = []; + + /** @var string */ + return \preg_replace_callback( + '/{\s*([A-Za-z_\-\.0-9]+)\s*}/', + function (array $matches) use ($request, $response, $error, &$cache) { + if (isset($cache[$matches[1]])) { + return $cache[$matches[1]]; + } + + $result = ''; + switch ($matches[1]) { + case 'request': + $result = Psr7\Message::toString($request); + break; + case 'response': + $result = $response ? Psr7\Message::toString($response) : ''; + break; + case 'req_headers': + $result = \trim($request->getMethod() + .' '.$request->getRequestTarget()) + .' HTTP/'.$request->getProtocolVersion()."\r\n" + .$this->headers($request); + break; + case 'res_headers': + $result = $response ? + \sprintf( + 'HTTP/%s %d %s', + $response->getProtocolVersion(), + $response->getStatusCode(), + $response->getReasonPhrase() + )."\r\n".$this->headers($response) + : 'NULL'; + break; + case 'req_body': + $result = $request->getBody()->__toString(); + break; + case 'res_body': + if (!$response instanceof ResponseInterface) { + $result = 'NULL'; + break; + } + + $body = $response->getBody(); + + if (!$body->isSeekable()) { + $result = 'RESPONSE_NOT_LOGGEABLE'; + break; + } + + $result = $response->getBody()->__toString(); + break; + case 'ts': + case 'date_iso_8601': + $result = \gmdate('c'); + break; + case 'date_common_log': + $result = \date('d/M/Y:H:i:s O'); + break; + case 'method': + $result = $request->getMethod(); + break; + case 'version': + $result = $request->getProtocolVersion(); + break; + case 'uri': + case 'url': + $result = $request->getUri()->__toString(); + break; + case 'target': + $result = $request->getRequestTarget(); + break; + case 'req_version': + $result = $request->getProtocolVersion(); + break; + case 'res_version': + $result = $response + ? $response->getProtocolVersion() + : 'NULL'; + break; + case 'host': + $result = $request->getHeaderLine('Host'); + break; + case 'hostname': + $result = \gethostname(); + break; + case 'code': + $result = $response ? $response->getStatusCode() : 'NULL'; + break; + case 'phrase': + $result = $response ? $response->getReasonPhrase() : 'NULL'; + break; + case 'error': + $result = $error ? $error->getMessage() : 'NULL'; + break; + default: + // handle prefixed dynamic headers + if (\strpos($matches[1], 'req_header_') === 0) { + $result = $request->getHeaderLine(\substr($matches[1], 11)); + } elseif (\strpos($matches[1], 'res_header_') === 0) { + $result = $response + ? $response->getHeaderLine(\substr($matches[1], 11)) + : 'NULL'; + } + } + + $cache[$matches[1]] = $result; + + return $result; + }, + $this->template + ); + } + + /** + * Get headers from message as string + */ + private function headers(MessageInterface $message): string + { + $result = ''; + foreach ($message->getHeaders() as $name => $values) { + $result .= $name.': '.\implode(', ', $values)."\r\n"; + } + + return \trim($result); + } +} diff --git a/3rdparty/guzzlehttp/guzzle/src/MessageFormatterInterface.php b/3rdparty/guzzlehttp/guzzle/src/MessageFormatterInterface.php new file mode 100644 index 00000000..a39ac248 --- /dev/null +++ b/3rdparty/guzzlehttp/guzzle/src/MessageFormatterInterface.php @@ -0,0 +1,18 @@ +withCookieHeader($request); + + return $handler($request, $options) + ->then( + static function (ResponseInterface $response) use ($cookieJar, $request): ResponseInterface { + $cookieJar->extractCookies($request, $response); + + return $response; + } + ); + }; + }; + } + + /** + * Middleware that throws exceptions for 4xx or 5xx responses when the + * "http_errors" request option is set to true. + * + * @param BodySummarizerInterface|null $bodySummarizer The body summarizer to use in exception messages. + * + * @return callable(callable): callable Returns a function that accepts the next handler. + */ + public static function httpErrors(?BodySummarizerInterface $bodySummarizer = null): callable + { + return static function (callable $handler) use ($bodySummarizer): callable { + return static function ($request, array $options) use ($handler, $bodySummarizer) { + if (empty($options['http_errors'])) { + return $handler($request, $options); + } + + return $handler($request, $options)->then( + static function (ResponseInterface $response) use ($request, $bodySummarizer) { + $code = $response->getStatusCode(); + if ($code < 400) { + return $response; + } + throw RequestException::create($request, $response, null, [], $bodySummarizer); + } + ); + }; + }; + } + + /** + * Middleware that pushes history data to an ArrayAccess container. + * + * @param array|\ArrayAccess $container Container to hold the history (by reference). + * + * @return callable(callable): callable Returns a function that accepts the next handler. + * + * @throws \InvalidArgumentException if container is not an array or ArrayAccess. + */ + public static function history(&$container): callable + { + if (!\is_array($container) && !$container instanceof \ArrayAccess) { + throw new \InvalidArgumentException('history container must be an array or object implementing ArrayAccess'); + } + + return static function (callable $handler) use (&$container): callable { + return static function (RequestInterface $request, array $options) use ($handler, &$container) { + return $handler($request, $options)->then( + static function ($value) use ($request, &$container, $options) { + $container[] = [ + 'request' => $request, + 'response' => $value, + 'error' => null, + 'options' => $options, + ]; + + return $value; + }, + static function ($reason) use ($request, &$container, $options) { + $container[] = [ + 'request' => $request, + 'response' => null, + 'error' => $reason, + 'options' => $options, + ]; + + return P\Create::rejectionFor($reason); + } + ); + }; + }; + } + + /** + * Middleware that invokes a callback before and after sending a request. + * + * The provided listener cannot modify or alter the response. It simply + * "taps" into the chain to be notified before returning the promise. The + * before listener accepts a request and options array, and the after + * listener accepts a request, options array, and response promise. + * + * @param callable $before Function to invoke before forwarding the request. + * @param callable $after Function invoked after forwarding. + * + * @return callable Returns a function that accepts the next handler. + */ + public static function tap(?callable $before = null, ?callable $after = null): callable + { + return static function (callable $handler) use ($before, $after): callable { + return static function (RequestInterface $request, array $options) use ($handler, $before, $after) { + if ($before) { + $before($request, $options); + } + $response = $handler($request, $options); + if ($after) { + $after($request, $options, $response); + } + + return $response; + }; + }; + } + + /** + * Middleware that handles request redirects. + * + * @return callable Returns a function that accepts the next handler. + */ + public static function redirect(): callable + { + return static function (callable $handler): RedirectMiddleware { + return new RedirectMiddleware($handler); + }; + } + + /** + * Middleware that retries requests based on the boolean result of + * invoking the provided "decider" function. + * + * If no delay function is provided, a simple implementation of exponential + * backoff will be utilized. + * + * @param callable $decider Function that accepts the number of retries, + * a request, [response], and [exception] and + * returns true if the request is to be retried. + * @param callable $delay Function that accepts the number of retries and + * returns the number of milliseconds to delay. + * + * @return callable Returns a function that accepts the next handler. + */ + public static function retry(callable $decider, ?callable $delay = null): callable + { + return static function (callable $handler) use ($decider, $delay): RetryMiddleware { + return new RetryMiddleware($decider, $handler, $delay); + }; + } + + /** + * Middleware that logs requests, responses, and errors using a message + * formatter. + * + * @phpstan-param \Psr\Log\LogLevel::* $logLevel Level at which to log requests. + * + * @param LoggerInterface $logger Logs messages. + * @param MessageFormatterInterface|MessageFormatter $formatter Formatter used to create message strings. + * @param string $logLevel Level at which to log requests. + * + * @return callable Returns a function that accepts the next handler. + */ + public static function log(LoggerInterface $logger, $formatter, string $logLevel = 'info'): callable + { + // To be compatible with Guzzle 7.1.x we need to allow users to pass a MessageFormatter + if (!$formatter instanceof MessageFormatter && !$formatter instanceof MessageFormatterInterface) { + throw new \LogicException(sprintf('Argument 2 to %s::log() must be of type %s', self::class, MessageFormatterInterface::class)); + } + + return static function (callable $handler) use ($logger, $formatter, $logLevel): callable { + return static function (RequestInterface $request, array $options = []) use ($handler, $logger, $formatter, $logLevel) { + return $handler($request, $options)->then( + static function ($response) use ($logger, $request, $formatter, $logLevel): ResponseInterface { + $message = $formatter->format($request, $response); + $logger->log($logLevel, $message); + + return $response; + }, + static function ($reason) use ($logger, $request, $formatter): PromiseInterface { + $response = $reason instanceof RequestException ? $reason->getResponse() : null; + $message = $formatter->format($request, $response, P\Create::exceptionFor($reason)); + $logger->error($message); + + return P\Create::rejectionFor($reason); + } + ); + }; + }; + } + + /** + * This middleware adds a default content-type if possible, a default + * content-length or transfer-encoding header, and the expect header. + */ + public static function prepareBody(): callable + { + return static function (callable $handler): PrepareBodyMiddleware { + return new PrepareBodyMiddleware($handler); + }; + } + + /** + * Middleware that applies a map function to the request before passing to + * the next handler. + * + * @param callable $fn Function that accepts a RequestInterface and returns + * a RequestInterface. + */ + public static function mapRequest(callable $fn): callable + { + return static function (callable $handler) use ($fn): callable { + return static function (RequestInterface $request, array $options) use ($handler, $fn) { + return $handler($fn($request), $options); + }; + }; + } + + /** + * Middleware that applies a map function to the resolved promise's + * response. + * + * @param callable $fn Function that accepts a ResponseInterface and + * returns a ResponseInterface. + */ + public static function mapResponse(callable $fn): callable + { + return static function (callable $handler) use ($fn): callable { + return static function (RequestInterface $request, array $options) use ($handler, $fn) { + return $handler($request, $options)->then($fn); + }; + }; + } +} diff --git a/3rdparty/guzzlehttp/guzzle/src/Pool.php b/3rdparty/guzzlehttp/guzzle/src/Pool.php new file mode 100644 index 00000000..ddc304bb --- /dev/null +++ b/3rdparty/guzzlehttp/guzzle/src/Pool.php @@ -0,0 +1,125 @@ + $rfn) { + if ($rfn instanceof RequestInterface) { + yield $key => $client->sendAsync($rfn, $opts); + } elseif (\is_callable($rfn)) { + yield $key => $rfn($opts); + } else { + throw new \InvalidArgumentException('Each value yielded by the iterator must be a Psr7\Http\Message\RequestInterface or a callable that returns a promise that fulfills with a Psr7\Message\Http\ResponseInterface object.'); + } + } + }; + + $this->each = new EachPromise($requests(), $config); + } + + /** + * Get promise + */ + public function promise(): PromiseInterface + { + return $this->each->promise(); + } + + /** + * Sends multiple requests concurrently and returns an array of responses + * and exceptions that uses the same ordering as the provided requests. + * + * IMPORTANT: This method keeps every request and response in memory, and + * as such, is NOT recommended when sending a large number or an + * indeterminate number of requests concurrently. + * + * @param ClientInterface $client Client used to send the requests + * @param array|\Iterator $requests Requests to send concurrently. + * @param array $options Passes through the options available in + * {@see Pool::__construct} + * + * @return array Returns an array containing the response or an exception + * in the same order that the requests were sent. + * + * @throws \InvalidArgumentException if the event format is incorrect. + */ + public static function batch(ClientInterface $client, $requests, array $options = []): array + { + $res = []; + self::cmpCallback($options, 'fulfilled', $res); + self::cmpCallback($options, 'rejected', $res); + $pool = new static($client, $requests, $options); + $pool->promise()->wait(); + \ksort($res); + + return $res; + } + + /** + * Execute callback(s) + */ + private static function cmpCallback(array &$options, string $name, array &$results): void + { + if (!isset($options[$name])) { + $options[$name] = static function ($v, $k) use (&$results) { + $results[$k] = $v; + }; + } else { + $currentFn = $options[$name]; + $options[$name] = static function ($v, $k) use (&$results, $currentFn) { + $currentFn($v, $k); + $results[$k] = $v; + }; + } + } +} diff --git a/3rdparty/guzzlehttp/guzzle/src/PrepareBodyMiddleware.php b/3rdparty/guzzlehttp/guzzle/src/PrepareBodyMiddleware.php new file mode 100644 index 00000000..7dde6c5f --- /dev/null +++ b/3rdparty/guzzlehttp/guzzle/src/PrepareBodyMiddleware.php @@ -0,0 +1,105 @@ +nextHandler = $nextHandler; + } + + public function __invoke(RequestInterface $request, array $options): PromiseInterface + { + $fn = $this->nextHandler; + + // Don't do anything if the request has no body. + if ($request->getBody()->getSize() === 0) { + return $fn($request, $options); + } + + $modify = []; + + // Add a default content-type if possible. + if (!$request->hasHeader('Content-Type')) { + if ($uri = $request->getBody()->getMetadata('uri')) { + if (is_string($uri) && $type = Psr7\MimeType::fromFilename($uri)) { + $modify['set_headers']['Content-Type'] = $type; + } + } + } + + // Add a default content-length or transfer-encoding header. + if (!$request->hasHeader('Content-Length') + && !$request->hasHeader('Transfer-Encoding') + ) { + $size = $request->getBody()->getSize(); + if ($size !== null) { + $modify['set_headers']['Content-Length'] = $size; + } else { + $modify['set_headers']['Transfer-Encoding'] = 'chunked'; + } + } + + // Add the expect header if needed. + $this->addExpectHeader($request, $options, $modify); + + return $fn(Psr7\Utils::modifyRequest($request, $modify), $options); + } + + /** + * Add expect header + */ + private function addExpectHeader(RequestInterface $request, array $options, array &$modify): void + { + // Determine if the Expect header should be used + if ($request->hasHeader('Expect')) { + return; + } + + $expect = $options['expect'] ?? null; + + // Return if disabled or using HTTP/1.0 + if ($expect === false || $request->getProtocolVersion() === '1.0') { + return; + } + + // The expect header is unconditionally enabled + if ($expect === true) { + $modify['set_headers']['Expect'] = '100-Continue'; + + return; + } + + // By default, send the expect header when the payload is > 1mb + if ($expect === null) { + $expect = 1048576; + } + + // Always add if the body cannot be rewound, the size cannot be + // determined, or the size is greater than the cutoff threshold + $body = $request->getBody(); + $size = $body->getSize(); + + if ($size === null || $size >= (int) $expect || !$body->isSeekable()) { + $modify['set_headers']['Expect'] = '100-Continue'; + } + } +} diff --git a/3rdparty/guzzlehttp/guzzle/src/RedirectMiddleware.php b/3rdparty/guzzlehttp/guzzle/src/RedirectMiddleware.php new file mode 100644 index 00000000..7aa21a62 --- /dev/null +++ b/3rdparty/guzzlehttp/guzzle/src/RedirectMiddleware.php @@ -0,0 +1,228 @@ + 5, + 'protocols' => ['http', 'https'], + 'strict' => false, + 'referer' => false, + 'track_redirects' => false, + ]; + + /** + * @var callable(RequestInterface, array): PromiseInterface + */ + private $nextHandler; + + /** + * @param callable(RequestInterface, array): PromiseInterface $nextHandler Next handler to invoke. + */ + public function __construct(callable $nextHandler) + { + $this->nextHandler = $nextHandler; + } + + public function __invoke(RequestInterface $request, array $options): PromiseInterface + { + $fn = $this->nextHandler; + + if (empty($options['allow_redirects'])) { + return $fn($request, $options); + } + + if ($options['allow_redirects'] === true) { + $options['allow_redirects'] = self::$defaultSettings; + } elseif (!\is_array($options['allow_redirects'])) { + throw new \InvalidArgumentException('allow_redirects must be true, false, or array'); + } else { + // Merge the default settings with the provided settings + $options['allow_redirects'] += self::$defaultSettings; + } + + if (empty($options['allow_redirects']['max'])) { + return $fn($request, $options); + } + + return $fn($request, $options) + ->then(function (ResponseInterface $response) use ($request, $options) { + return $this->checkRedirect($request, $options, $response); + }); + } + + /** + * @return ResponseInterface|PromiseInterface + */ + public function checkRedirect(RequestInterface $request, array $options, ResponseInterface $response) + { + if (\strpos((string) $response->getStatusCode(), '3') !== 0 + || !$response->hasHeader('Location') + ) { + return $response; + } + + $this->guardMax($request, $response, $options); + $nextRequest = $this->modifyRequest($request, $options, $response); + + // If authorization is handled by curl, unset it if URI is cross-origin. + if (Psr7\UriComparator::isCrossOrigin($request->getUri(), $nextRequest->getUri()) && defined('\CURLOPT_HTTPAUTH')) { + unset( + $options['curl'][\CURLOPT_HTTPAUTH], + $options['curl'][\CURLOPT_USERPWD] + ); + } + + if (isset($options['allow_redirects']['on_redirect'])) { + ($options['allow_redirects']['on_redirect'])( + $request, + $response, + $nextRequest->getUri() + ); + } + + $promise = $this($nextRequest, $options); + + // Add headers to be able to track history of redirects. + if (!empty($options['allow_redirects']['track_redirects'])) { + return $this->withTracking( + $promise, + (string) $nextRequest->getUri(), + $response->getStatusCode() + ); + } + + return $promise; + } + + /** + * Enable tracking on promise. + */ + private function withTracking(PromiseInterface $promise, string $uri, int $statusCode): PromiseInterface + { + return $promise->then( + static function (ResponseInterface $response) use ($uri, $statusCode) { + // Note that we are pushing to the front of the list as this + // would be an earlier response than what is currently present + // in the history header. + $historyHeader = $response->getHeader(self::HISTORY_HEADER); + $statusHeader = $response->getHeader(self::STATUS_HISTORY_HEADER); + \array_unshift($historyHeader, $uri); + \array_unshift($statusHeader, (string) $statusCode); + + return $response->withHeader(self::HISTORY_HEADER, $historyHeader) + ->withHeader(self::STATUS_HISTORY_HEADER, $statusHeader); + } + ); + } + + /** + * Check for too many redirects. + * + * @throws TooManyRedirectsException Too many redirects. + */ + private function guardMax(RequestInterface $request, ResponseInterface $response, array &$options): void + { + $current = $options['__redirect_count'] + ?? 0; + $options['__redirect_count'] = $current + 1; + $max = $options['allow_redirects']['max']; + + if ($options['__redirect_count'] > $max) { + throw new TooManyRedirectsException("Will not follow more than {$max} redirects", $request, $response); + } + } + + public function modifyRequest(RequestInterface $request, array $options, ResponseInterface $response): RequestInterface + { + // Request modifications to apply. + $modify = []; + $protocols = $options['allow_redirects']['protocols']; + + // Use a GET request if this is an entity enclosing request and we are + // not forcing RFC compliance, but rather emulating what all browsers + // would do. + $statusCode = $response->getStatusCode(); + if ($statusCode == 303 + || ($statusCode <= 302 && !$options['allow_redirects']['strict']) + ) { + $safeMethods = ['GET', 'HEAD', 'OPTIONS']; + $requestMethod = $request->getMethod(); + + $modify['method'] = in_array($requestMethod, $safeMethods) ? $requestMethod : 'GET'; + $modify['body'] = ''; + } + + $uri = self::redirectUri($request, $response, $protocols); + if (isset($options['idn_conversion']) && ($options['idn_conversion'] !== false)) { + $idnOptions = ($options['idn_conversion'] === true) ? \IDNA_DEFAULT : $options['idn_conversion']; + $uri = Utils::idnUriConvert($uri, $idnOptions); + } + + $modify['uri'] = $uri; + Psr7\Message::rewindBody($request); + + // Add the Referer header if it is told to do so and only + // add the header if we are not redirecting from https to http. + if ($options['allow_redirects']['referer'] + && $modify['uri']->getScheme() === $request->getUri()->getScheme() + ) { + $uri = $request->getUri()->withUserInfo(''); + $modify['set_headers']['Referer'] = (string) $uri; + } else { + $modify['remove_headers'][] = 'Referer'; + } + + // Remove Authorization and Cookie headers if URI is cross-origin. + if (Psr7\UriComparator::isCrossOrigin($request->getUri(), $modify['uri'])) { + $modify['remove_headers'][] = 'Authorization'; + $modify['remove_headers'][] = 'Cookie'; + } + + return Psr7\Utils::modifyRequest($request, $modify); + } + + /** + * Set the appropriate URL on the request based on the location header. + */ + private static function redirectUri( + RequestInterface $request, + ResponseInterface $response, + array $protocols + ): UriInterface { + $location = Psr7\UriResolver::resolve( + $request->getUri(), + new Psr7\Uri($response->getHeaderLine('Location')) + ); + + // Ensure that the redirect URI is allowed based on the protocols. + if (!\in_array($location->getScheme(), $protocols)) { + throw new BadResponseException(\sprintf('Redirect URI, %s, does not use one of the allowed redirect protocols: %s', $location, \implode(', ', $protocols)), $request, $response); + } + + return $location; + } +} diff --git a/3rdparty/guzzlehttp/guzzle/src/RequestOptions.php b/3rdparty/guzzlehttp/guzzle/src/RequestOptions.php new file mode 100644 index 00000000..84a3500e --- /dev/null +++ b/3rdparty/guzzlehttp/guzzle/src/RequestOptions.php @@ -0,0 +1,274 @@ +decider = $decider; + $this->nextHandler = $nextHandler; + $this->delay = $delay ?: __CLASS__.'::exponentialDelay'; + } + + /** + * Default exponential backoff delay function. + * + * @return int milliseconds. + */ + public static function exponentialDelay(int $retries): int + { + return (int) 2 ** ($retries - 1) * 1000; + } + + public function __invoke(RequestInterface $request, array $options): PromiseInterface + { + if (!isset($options['retries'])) { + $options['retries'] = 0; + } + + $fn = $this->nextHandler; + + return $fn($request, $options) + ->then( + $this->onFulfilled($request, $options), + $this->onRejected($request, $options) + ); + } + + /** + * Execute fulfilled closure + */ + private function onFulfilled(RequestInterface $request, array $options): callable + { + return function ($value) use ($request, $options) { + if (!($this->decider)( + $options['retries'], + $request, + $value, + null + )) { + return $value; + } + + return $this->doRetry($request, $options, $value); + }; + } + + /** + * Execute rejected closure + */ + private function onRejected(RequestInterface $req, array $options): callable + { + return function ($reason) use ($req, $options) { + if (!($this->decider)( + $options['retries'], + $req, + null, + $reason + )) { + return P\Create::rejectionFor($reason); + } + + return $this->doRetry($req, $options); + }; + } + + private function doRetry(RequestInterface $request, array $options, ?ResponseInterface $response = null): PromiseInterface + { + $options['delay'] = ($this->delay)(++$options['retries'], $response, $request); + + return $this($request, $options); + } +} diff --git a/3rdparty/guzzlehttp/guzzle/src/TransferStats.php b/3rdparty/guzzlehttp/guzzle/src/TransferStats.php new file mode 100644 index 00000000..93fa334c --- /dev/null +++ b/3rdparty/guzzlehttp/guzzle/src/TransferStats.php @@ -0,0 +1,133 @@ +request = $request; + $this->response = $response; + $this->transferTime = $transferTime; + $this->handlerErrorData = $handlerErrorData; + $this->handlerStats = $handlerStats; + } + + public function getRequest(): RequestInterface + { + return $this->request; + } + + /** + * Returns the response that was received (if any). + */ + public function getResponse(): ?ResponseInterface + { + return $this->response; + } + + /** + * Returns true if a response was received. + */ + public function hasResponse(): bool + { + return $this->response !== null; + } + + /** + * Gets handler specific error data. + * + * This might be an exception, a integer representing an error code, or + * anything else. Relying on this value assumes that you know what handler + * you are using. + * + * @return mixed + */ + public function getHandlerErrorData() + { + return $this->handlerErrorData; + } + + /** + * Get the effective URI the request was sent to. + */ + public function getEffectiveUri(): UriInterface + { + return $this->request->getUri(); + } + + /** + * Get the estimated time the request was being transferred by the handler. + * + * @return float|null Time in seconds. + */ + public function getTransferTime(): ?float + { + return $this->transferTime; + } + + /** + * Gets an array of all of the handler specific transfer data. + */ + public function getHandlerStats(): array + { + return $this->handlerStats; + } + + /** + * Get a specific handler statistic from the handler by name. + * + * @param string $stat Handler specific transfer stat to retrieve. + * + * @return mixed|null + */ + public function getHandlerStat(string $stat) + { + return $this->handlerStats[$stat] ?? null; + } +} diff --git a/3rdparty/guzzlehttp/guzzle/src/Utils.php b/3rdparty/guzzlehttp/guzzle/src/Utils.php new file mode 100644 index 00000000..c6a5893d --- /dev/null +++ b/3rdparty/guzzlehttp/guzzle/src/Utils.php @@ -0,0 +1,384 @@ += 0) { + if (\function_exists('curl_multi_exec') && \function_exists('curl_exec')) { + $handler = Proxy::wrapSync(new CurlMultiHandler(), new CurlHandler()); + } elseif (\function_exists('curl_exec')) { + $handler = new CurlHandler(); + } elseif (\function_exists('curl_multi_exec')) { + $handler = new CurlMultiHandler(); + } + } + + if (\ini_get('allow_url_fopen')) { + $handler = $handler + ? Proxy::wrapStreaming($handler, new StreamHandler()) + : new StreamHandler(); + } elseif (!$handler) { + throw new \RuntimeException('GuzzleHttp requires cURL, the allow_url_fopen ini setting, or a custom HTTP handler.'); + } + + return $handler; + } + + /** + * Get the default User-Agent string to use with Guzzle. + */ + public static function defaultUserAgent(): string + { + return sprintf('GuzzleHttp/%d', ClientInterface::MAJOR_VERSION); + } + + /** + * Returns the default cacert bundle for the current system. + * + * First, the openssl.cafile and curl.cainfo php.ini settings are checked. + * If those settings are not configured, then the common locations for + * bundles found on Red Hat, CentOS, Fedora, Ubuntu, Debian, FreeBSD, OS X + * and Windows are checked. If any of these file locations are found on + * disk, they will be utilized. + * + * Note: the result of this function is cached for subsequent calls. + * + * @throws \RuntimeException if no bundle can be found. + * + * @deprecated Utils::defaultCaBundle will be removed in guzzlehttp/guzzle:8.0. This method is not needed in PHP 5.6+. + */ + public static function defaultCaBundle(): string + { + static $cached = null; + static $cafiles = [ + // Red Hat, CentOS, Fedora (provided by the ca-certificates package) + '/etc/pki/tls/certs/ca-bundle.crt', + // Ubuntu, Debian (provided by the ca-certificates package) + '/etc/ssl/certs/ca-certificates.crt', + // FreeBSD (provided by the ca_root_nss package) + '/usr/local/share/certs/ca-root-nss.crt', + // SLES 12 (provided by the ca-certificates package) + '/var/lib/ca-certificates/ca-bundle.pem', + // OS X provided by homebrew (using the default path) + '/usr/local/etc/openssl/cert.pem', + // Google app engine + '/etc/ca-certificates.crt', + // Windows? + 'C:\\windows\\system32\\curl-ca-bundle.crt', + 'C:\\windows\\curl-ca-bundle.crt', + ]; + + if ($cached) { + return $cached; + } + + if ($ca = \ini_get('openssl.cafile')) { + return $cached = $ca; + } + + if ($ca = \ini_get('curl.cainfo')) { + return $cached = $ca; + } + + foreach ($cafiles as $filename) { + if (\file_exists($filename)) { + return $cached = $filename; + } + } + + throw new \RuntimeException( + <<< EOT +No system CA bundle could be found in any of the the common system locations. +PHP versions earlier than 5.6 are not properly configured to use the system's +CA bundle by default. In order to verify peer certificates, you will need to +supply the path on disk to a certificate bundle to the 'verify' request +option: https://docs.guzzlephp.org/en/latest/request-options.html#verify. If +you do not need a specific certificate bundle, then Mozilla provides a commonly +used CA bundle which can be downloaded here (provided by the maintainer of +cURL): https://curl.haxx.se/ca/cacert.pem. Once you have a CA bundle available +on disk, you can set the 'openssl.cafile' PHP ini setting to point to the path +to the file, allowing you to omit the 'verify' request option. See +https://curl.haxx.se/docs/sslcerts.html for more information. +EOT + ); + } + + /** + * Creates an associative array of lowercase header names to the actual + * header casing. + */ + public static function normalizeHeaderKeys(array $headers): array + { + $result = []; + foreach (\array_keys($headers) as $key) { + $result[\strtolower($key)] = $key; + } + + return $result; + } + + /** + * Returns true if the provided host matches any of the no proxy areas. + * + * This method will strip a port from the host if it is present. Each pattern + * can be matched with an exact match (e.g., "foo.com" == "foo.com") or a + * partial match: (e.g., "foo.com" == "baz.foo.com" and ".foo.com" == + * "baz.foo.com", but ".foo.com" != "foo.com"). + * + * Areas are matched in the following cases: + * 1. "*" (without quotes) always matches any hosts. + * 2. An exact match. + * 3. The area starts with "." and the area is the last part of the host. e.g. + * '.mit.edu' will match any host that ends with '.mit.edu'. + * + * @param string $host Host to check against the patterns. + * @param string[] $noProxyArray An array of host patterns. + * + * @throws InvalidArgumentException + */ + public static function isHostInNoProxy(string $host, array $noProxyArray): bool + { + if (\strlen($host) === 0) { + throw new InvalidArgumentException('Empty host provided'); + } + + // Strip port if present. + [$host] = \explode(':', $host, 2); + + foreach ($noProxyArray as $area) { + // Always match on wildcards. + if ($area === '*') { + return true; + } + + if (empty($area)) { + // Don't match on empty values. + continue; + } + + if ($area === $host) { + // Exact matches. + return true; + } + // Special match if the area when prefixed with ".". Remove any + // existing leading "." and add a new leading ".". + $area = '.'.\ltrim($area, '.'); + if (\substr($host, -\strlen($area)) === $area) { + return true; + } + } + + return false; + } + + /** + * Wrapper for json_decode that throws when an error occurs. + * + * @param string $json JSON data to parse + * @param bool $assoc When true, returned objects will be converted + * into associative arrays. + * @param int $depth User specified recursion depth. + * @param int $options Bitmask of JSON decode options. + * + * @return object|array|string|int|float|bool|null + * + * @throws InvalidArgumentException if the JSON cannot be decoded. + * + * @see https://www.php.net/manual/en/function.json-decode.php + */ + public static function jsonDecode(string $json, bool $assoc = false, int $depth = 512, int $options = 0) + { + $data = \json_decode($json, $assoc, $depth, $options); + if (\JSON_ERROR_NONE !== \json_last_error()) { + throw new InvalidArgumentException('json_decode error: '.\json_last_error_msg()); + } + + return $data; + } + + /** + * Wrapper for JSON encoding that throws when an error occurs. + * + * @param mixed $value The value being encoded + * @param int $options JSON encode option bitmask + * @param int $depth Set the maximum depth. Must be greater than zero. + * + * @throws InvalidArgumentException if the JSON cannot be encoded. + * + * @see https://www.php.net/manual/en/function.json-encode.php + */ + public static function jsonEncode($value, int $options = 0, int $depth = 512): string + { + $json = \json_encode($value, $options, $depth); + if (\JSON_ERROR_NONE !== \json_last_error()) { + throw new InvalidArgumentException('json_encode error: '.\json_last_error_msg()); + } + + /** @var string */ + return $json; + } + + /** + * Wrapper for the hrtime() or microtime() functions + * (depending on the PHP version, one of the two is used) + * + * @return float UNIX timestamp + * + * @internal + */ + public static function currentTime(): float + { + return (float) \function_exists('hrtime') ? \hrtime(true) / 1e9 : \microtime(true); + } + + /** + * @throws InvalidArgumentException + * + * @internal + */ + public static function idnUriConvert(UriInterface $uri, int $options = 0): UriInterface + { + if ($uri->getHost()) { + $asciiHost = self::idnToAsci($uri->getHost(), $options, $info); + if ($asciiHost === false) { + $errorBitSet = $info['errors'] ?? 0; + + $errorConstants = array_filter(array_keys(get_defined_constants()), static function (string $name): bool { + return substr($name, 0, 11) === 'IDNA_ERROR_'; + }); + + $errors = []; + foreach ($errorConstants as $errorConstant) { + if ($errorBitSet & constant($errorConstant)) { + $errors[] = $errorConstant; + } + } + + $errorMessage = 'IDN conversion failed'; + if ($errors) { + $errorMessage .= ' (errors: '.implode(', ', $errors).')'; + } + + throw new InvalidArgumentException($errorMessage); + } + if ($uri->getHost() !== $asciiHost) { + // Replace URI only if the ASCII version is different + $uri = $uri->withHost($asciiHost); + } + } + + return $uri; + } + + /** + * @internal + */ + public static function getenv(string $name): ?string + { + if (isset($_SERVER[$name])) { + return (string) $_SERVER[$name]; + } + + if (\PHP_SAPI === 'cli' && ($value = \getenv($name)) !== false && $value !== null) { + return (string) $value; + } + + return null; + } + + /** + * @return string|false + */ + private static function idnToAsci(string $domain, int $options, ?array &$info = []) + { + if (\function_exists('idn_to_ascii') && \defined('INTL_IDNA_VARIANT_UTS46')) { + return \idn_to_ascii($domain, $options, \INTL_IDNA_VARIANT_UTS46, $info); + } + + throw new \Error('ext-idn or symfony/polyfill-intl-idn not loaded or too old'); + } +} diff --git a/3rdparty/guzzlehttp/guzzle/src/functions.php b/3rdparty/guzzlehttp/guzzle/src/functions.php new file mode 100644 index 00000000..9ab4b964 --- /dev/null +++ b/3rdparty/guzzlehttp/guzzle/src/functions.php @@ -0,0 +1,167 @@ + +Copyright (c) 2015 Graham Campbell +Copyright (c) 2017 Tobias Schultze +Copyright (c) 2020 Tobias Nyholm + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in +all copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +THE SOFTWARE. diff --git a/3rdparty/guzzlehttp/promises/src/AggregateException.php b/3rdparty/guzzlehttp/promises/src/AggregateException.php new file mode 100644 index 00000000..40ffdbcf --- /dev/null +++ b/3rdparty/guzzlehttp/promises/src/AggregateException.php @@ -0,0 +1,19 @@ +then(function ($v) { echo $v; }); + * + * @param callable $generatorFn Generator function to wrap into a promise. + * + * @return Promise + * + * @see https://github.com/petkaantonov/bluebird/blob/master/API.md#generators inspiration + */ +final class Coroutine implements PromiseInterface +{ + /** + * @var PromiseInterface|null + */ + private $currentPromise; + + /** + * @var Generator + */ + private $generator; + + /** + * @var Promise + */ + private $result; + + public function __construct(callable $generatorFn) + { + $this->generator = $generatorFn(); + $this->result = new Promise(function (): void { + while (isset($this->currentPromise)) { + $this->currentPromise->wait(); + } + }); + try { + $this->nextCoroutine($this->generator->current()); + } catch (Throwable $throwable) { + $this->result->reject($throwable); + } + } + + /** + * Create a new coroutine. + */ + public static function of(callable $generatorFn): self + { + return new self($generatorFn); + } + + public function then( + ?callable $onFulfilled = null, + ?callable $onRejected = null + ): PromiseInterface { + return $this->result->then($onFulfilled, $onRejected); + } + + public function otherwise(callable $onRejected): PromiseInterface + { + return $this->result->otherwise($onRejected); + } + + public function wait(bool $unwrap = true) + { + return $this->result->wait($unwrap); + } + + public function getState(): string + { + return $this->result->getState(); + } + + public function resolve($value): void + { + $this->result->resolve($value); + } + + public function reject($reason): void + { + $this->result->reject($reason); + } + + public function cancel(): void + { + $this->currentPromise->cancel(); + $this->result->cancel(); + } + + private function nextCoroutine($yielded): void + { + $this->currentPromise = Create::promiseFor($yielded) + ->then([$this, '_handleSuccess'], [$this, '_handleFailure']); + } + + /** + * @internal + */ + public function _handleSuccess($value): void + { + unset($this->currentPromise); + try { + $next = $this->generator->send($value); + if ($this->generator->valid()) { + $this->nextCoroutine($next); + } else { + $this->result->resolve($value); + } + } catch (Throwable $throwable) { + $this->result->reject($throwable); + } + } + + /** + * @internal + */ + public function _handleFailure($reason): void + { + unset($this->currentPromise); + try { + $nextYield = $this->generator->throw(Create::exceptionFor($reason)); + // The throw was caught, so keep iterating on the coroutine + $this->nextCoroutine($nextYield); + } catch (Throwable $throwable) { + $this->result->reject($throwable); + } + } +} diff --git a/3rdparty/guzzlehttp/promises/src/Create.php b/3rdparty/guzzlehttp/promises/src/Create.php new file mode 100644 index 00000000..9d3fc4a1 --- /dev/null +++ b/3rdparty/guzzlehttp/promises/src/Create.php @@ -0,0 +1,79 @@ +then([$promise, 'resolve'], [$promise, 'reject']); + + return $promise; + } + + return new FulfilledPromise($value); + } + + /** + * Creates a rejected promise for a reason if the reason is not a promise. + * If the provided reason is a promise, then it is returned as-is. + * + * @param mixed $reason Promise or reason. + */ + public static function rejectionFor($reason): PromiseInterface + { + if ($reason instanceof PromiseInterface) { + return $reason; + } + + return new RejectedPromise($reason); + } + + /** + * Create an exception for a rejected promise value. + * + * @param mixed $reason + */ + public static function exceptionFor($reason): \Throwable + { + if ($reason instanceof \Throwable) { + return $reason; + } + + return new RejectionException($reason); + } + + /** + * Returns an iterator for the given value. + * + * @param mixed $value + */ + public static function iterFor($value): \Iterator + { + if ($value instanceof \Iterator) { + return $value; + } + + if (is_array($value)) { + return new \ArrayIterator($value); + } + + return new \ArrayIterator([$value]); + } +} diff --git a/3rdparty/guzzlehttp/promises/src/Each.php b/3rdparty/guzzlehttp/promises/src/Each.php new file mode 100644 index 00000000..dd72c831 --- /dev/null +++ b/3rdparty/guzzlehttp/promises/src/Each.php @@ -0,0 +1,81 @@ + $onFulfilled, + 'rejected' => $onRejected, + ]))->promise(); + } + + /** + * Like of, but only allows a certain number of outstanding promises at any + * given time. + * + * $concurrency may be an integer or a function that accepts the number of + * pending promises and returns a numeric concurrency limit value to allow + * for dynamic a concurrency size. + * + * @param mixed $iterable + * @param int|callable $concurrency + */ + public static function ofLimit( + $iterable, + $concurrency, + ?callable $onFulfilled = null, + ?callable $onRejected = null + ): PromiseInterface { + return (new EachPromise($iterable, [ + 'fulfilled' => $onFulfilled, + 'rejected' => $onRejected, + 'concurrency' => $concurrency, + ]))->promise(); + } + + /** + * Like limit, but ensures that no promise in the given $iterable argument + * is rejected. If any promise is rejected, then the aggregate promise is + * rejected with the encountered rejection. + * + * @param mixed $iterable + * @param int|callable $concurrency + */ + public static function ofLimitAll( + $iterable, + $concurrency, + ?callable $onFulfilled = null + ): PromiseInterface { + return self::ofLimit( + $iterable, + $concurrency, + $onFulfilled, + function ($reason, $idx, PromiseInterface $aggregate): void { + $aggregate->reject($reason); + } + ); + } +} diff --git a/3rdparty/guzzlehttp/promises/src/EachPromise.php b/3rdparty/guzzlehttp/promises/src/EachPromise.php new file mode 100644 index 00000000..e1238981 --- /dev/null +++ b/3rdparty/guzzlehttp/promises/src/EachPromise.php @@ -0,0 +1,248 @@ +iterable = Create::iterFor($iterable); + + if (isset($config['concurrency'])) { + $this->concurrency = $config['concurrency']; + } + + if (isset($config['fulfilled'])) { + $this->onFulfilled = $config['fulfilled']; + } + + if (isset($config['rejected'])) { + $this->onRejected = $config['rejected']; + } + } + + /** @psalm-suppress InvalidNullableReturnType */ + public function promise(): PromiseInterface + { + if ($this->aggregate) { + return $this->aggregate; + } + + try { + $this->createPromise(); + /** @psalm-assert Promise $this->aggregate */ + $this->iterable->rewind(); + $this->refillPending(); + } catch (\Throwable $e) { + $this->aggregate->reject($e); + } + + /** + * @psalm-suppress NullableReturnStatement + */ + return $this->aggregate; + } + + private function createPromise(): void + { + $this->mutex = false; + $this->aggregate = new Promise(function (): void { + if ($this->checkIfFinished()) { + return; + } + reset($this->pending); + // Consume a potentially fluctuating list of promises while + // ensuring that indexes are maintained (precluding array_shift). + while ($promise = current($this->pending)) { + next($this->pending); + $promise->wait(); + if (Is::settled($this->aggregate)) { + return; + } + } + }); + + // Clear the references when the promise is resolved. + $clearFn = function (): void { + $this->iterable = $this->concurrency = $this->pending = null; + $this->onFulfilled = $this->onRejected = null; + $this->nextPendingIndex = 0; + }; + + $this->aggregate->then($clearFn, $clearFn); + } + + private function refillPending(): void + { + if (!$this->concurrency) { + // Add all pending promises. + while ($this->addPending() && $this->advanceIterator()) { + } + + return; + } + + // Add only up to N pending promises. + $concurrency = is_callable($this->concurrency) + ? ($this->concurrency)(count($this->pending)) + : $this->concurrency; + $concurrency = max($concurrency - count($this->pending), 0); + // Concurrency may be set to 0 to disallow new promises. + if (!$concurrency) { + return; + } + // Add the first pending promise. + $this->addPending(); + // Note this is special handling for concurrency=1 so that we do + // not advance the iterator after adding the first promise. This + // helps work around issues with generators that might not have the + // next value to yield until promise callbacks are called. + while (--$concurrency + && $this->advanceIterator() + && $this->addPending()) { + } + } + + private function addPending(): bool + { + if (!$this->iterable || !$this->iterable->valid()) { + return false; + } + + $promise = Create::promiseFor($this->iterable->current()); + $key = $this->iterable->key(); + + // Iterable keys may not be unique, so we use a counter to + // guarantee uniqueness + $idx = $this->nextPendingIndex++; + + $this->pending[$idx] = $promise->then( + function ($value) use ($idx, $key): void { + if ($this->onFulfilled) { + ($this->onFulfilled)( + $value, + $key, + $this->aggregate + ); + } + $this->step($idx); + }, + function ($reason) use ($idx, $key): void { + if ($this->onRejected) { + ($this->onRejected)( + $reason, + $key, + $this->aggregate + ); + } + $this->step($idx); + } + ); + + return true; + } + + private function advanceIterator(): bool + { + // Place a lock on the iterator so that we ensure to not recurse, + // preventing fatal generator errors. + if ($this->mutex) { + return false; + } + + $this->mutex = true; + + try { + $this->iterable->next(); + $this->mutex = false; + + return true; + } catch (\Throwable $e) { + $this->aggregate->reject($e); + $this->mutex = false; + + return false; + } + } + + private function step(int $idx): void + { + // If the promise was already resolved, then ignore this step. + if (Is::settled($this->aggregate)) { + return; + } + + unset($this->pending[$idx]); + + // Only refill pending promises if we are not locked, preventing the + // EachPromise to recursively invoke the provided iterator, which + // cause a fatal error: "Cannot resume an already running generator" + if ($this->advanceIterator() && !$this->checkIfFinished()) { + // Add more pending promises if possible. + $this->refillPending(); + } + } + + private function checkIfFinished(): bool + { + if (!$this->pending && !$this->iterable->valid()) { + // Resolve the promise if there's nothing left to do. + $this->aggregate->resolve(null); + + return true; + } + + return false; + } +} diff --git a/3rdparty/guzzlehttp/promises/src/FulfilledPromise.php b/3rdparty/guzzlehttp/promises/src/FulfilledPromise.php new file mode 100644 index 00000000..727ec315 --- /dev/null +++ b/3rdparty/guzzlehttp/promises/src/FulfilledPromise.php @@ -0,0 +1,89 @@ +value = $value; + } + + public function then( + ?callable $onFulfilled = null, + ?callable $onRejected = null + ): PromiseInterface { + // Return itself if there is no onFulfilled function. + if (!$onFulfilled) { + return $this; + } + + $queue = Utils::queue(); + $p = new Promise([$queue, 'run']); + $value = $this->value; + $queue->add(static function () use ($p, $value, $onFulfilled): void { + if (Is::pending($p)) { + try { + $p->resolve($onFulfilled($value)); + } catch (\Throwable $e) { + $p->reject($e); + } + } + }); + + return $p; + } + + public function otherwise(callable $onRejected): PromiseInterface + { + return $this->then(null, $onRejected); + } + + public function wait(bool $unwrap = true) + { + return $unwrap ? $this->value : null; + } + + public function getState(): string + { + return self::FULFILLED; + } + + public function resolve($value): void + { + if ($value !== $this->value) { + throw new \LogicException('Cannot resolve a fulfilled promise'); + } + } + + public function reject($reason): void + { + throw new \LogicException('Cannot reject a fulfilled promise'); + } + + public function cancel(): void + { + // pass + } +} diff --git a/3rdparty/guzzlehttp/promises/src/Is.php b/3rdparty/guzzlehttp/promises/src/Is.php new file mode 100644 index 00000000..f3f05038 --- /dev/null +++ b/3rdparty/guzzlehttp/promises/src/Is.php @@ -0,0 +1,40 @@ +getState() === PromiseInterface::PENDING; + } + + /** + * Returns true if a promise is fulfilled or rejected. + */ + public static function settled(PromiseInterface $promise): bool + { + return $promise->getState() !== PromiseInterface::PENDING; + } + + /** + * Returns true if a promise is fulfilled. + */ + public static function fulfilled(PromiseInterface $promise): bool + { + return $promise->getState() === PromiseInterface::FULFILLED; + } + + /** + * Returns true if a promise is rejected. + */ + public static function rejected(PromiseInterface $promise): bool + { + return $promise->getState() === PromiseInterface::REJECTED; + } +} diff --git a/3rdparty/guzzlehttp/promises/src/Promise.php b/3rdparty/guzzlehttp/promises/src/Promise.php new file mode 100644 index 00000000..c0c5be2c --- /dev/null +++ b/3rdparty/guzzlehttp/promises/src/Promise.php @@ -0,0 +1,281 @@ +waitFn = $waitFn; + $this->cancelFn = $cancelFn; + } + + public function then( + ?callable $onFulfilled = null, + ?callable $onRejected = null + ): PromiseInterface { + if ($this->state === self::PENDING) { + $p = new Promise(null, [$this, 'cancel']); + $this->handlers[] = [$p, $onFulfilled, $onRejected]; + $p->waitList = $this->waitList; + $p->waitList[] = $this; + + return $p; + } + + // Return a fulfilled promise and immediately invoke any callbacks. + if ($this->state === self::FULFILLED) { + $promise = Create::promiseFor($this->result); + + return $onFulfilled ? $promise->then($onFulfilled) : $promise; + } + + // It's either cancelled or rejected, so return a rejected promise + // and immediately invoke any callbacks. + $rejection = Create::rejectionFor($this->result); + + return $onRejected ? $rejection->then(null, $onRejected) : $rejection; + } + + public function otherwise(callable $onRejected): PromiseInterface + { + return $this->then(null, $onRejected); + } + + public function wait(bool $unwrap = true) + { + $this->waitIfPending(); + + if ($this->result instanceof PromiseInterface) { + return $this->result->wait($unwrap); + } + if ($unwrap) { + if ($this->state === self::FULFILLED) { + return $this->result; + } + // It's rejected so "unwrap" and throw an exception. + throw Create::exceptionFor($this->result); + } + } + + public function getState(): string + { + return $this->state; + } + + public function cancel(): void + { + if ($this->state !== self::PENDING) { + return; + } + + $this->waitFn = $this->waitList = null; + + if ($this->cancelFn) { + $fn = $this->cancelFn; + $this->cancelFn = null; + try { + $fn(); + } catch (\Throwable $e) { + $this->reject($e); + } + } + + // Reject the promise only if it wasn't rejected in a then callback. + /** @psalm-suppress RedundantCondition */ + if ($this->state === self::PENDING) { + $this->reject(new CancellationException('Promise has been cancelled')); + } + } + + public function resolve($value): void + { + $this->settle(self::FULFILLED, $value); + } + + public function reject($reason): void + { + $this->settle(self::REJECTED, $reason); + } + + private function settle(string $state, $value): void + { + if ($this->state !== self::PENDING) { + // Ignore calls with the same resolution. + if ($state === $this->state && $value === $this->result) { + return; + } + throw $this->state === $state + ? new \LogicException("The promise is already {$state}.") + : new \LogicException("Cannot change a {$this->state} promise to {$state}"); + } + + if ($value === $this) { + throw new \LogicException('Cannot fulfill or reject a promise with itself'); + } + + // Clear out the state of the promise but stash the handlers. + $this->state = $state; + $this->result = $value; + $handlers = $this->handlers; + $this->handlers = null; + $this->waitList = $this->waitFn = null; + $this->cancelFn = null; + + if (!$handlers) { + return; + } + + // If the value was not a settled promise or a thenable, then resolve + // it in the task queue using the correct ID. + if (!is_object($value) || !method_exists($value, 'then')) { + $id = $state === self::FULFILLED ? 1 : 2; + // It's a success, so resolve the handlers in the queue. + Utils::queue()->add(static function () use ($id, $value, $handlers): void { + foreach ($handlers as $handler) { + self::callHandler($id, $value, $handler); + } + }); + } elseif ($value instanceof Promise && Is::pending($value)) { + // We can just merge our handlers onto the next promise. + $value->handlers = array_merge($value->handlers, $handlers); + } else { + // Resolve the handlers when the forwarded promise is resolved. + $value->then( + static function ($value) use ($handlers): void { + foreach ($handlers as $handler) { + self::callHandler(1, $value, $handler); + } + }, + static function ($reason) use ($handlers): void { + foreach ($handlers as $handler) { + self::callHandler(2, $reason, $handler); + } + } + ); + } + } + + /** + * Call a stack of handlers using a specific callback index and value. + * + * @param int $index 1 (resolve) or 2 (reject). + * @param mixed $value Value to pass to the callback. + * @param array $handler Array of handler data (promise and callbacks). + */ + private static function callHandler(int $index, $value, array $handler): void + { + /** @var PromiseInterface $promise */ + $promise = $handler[0]; + + // The promise may have been cancelled or resolved before placing + // this thunk in the queue. + if (Is::settled($promise)) { + return; + } + + try { + if (isset($handler[$index])) { + /* + * If $f throws an exception, then $handler will be in the exception + * stack trace. Since $handler contains a reference to the callable + * itself we get a circular reference. We clear the $handler + * here to avoid that memory leak. + */ + $f = $handler[$index]; + unset($handler); + $promise->resolve($f($value)); + } elseif ($index === 1) { + // Forward resolution values as-is. + $promise->resolve($value); + } else { + // Forward rejections down the chain. + $promise->reject($value); + } + } catch (\Throwable $reason) { + $promise->reject($reason); + } + } + + private function waitIfPending(): void + { + if ($this->state !== self::PENDING) { + return; + } elseif ($this->waitFn) { + $this->invokeWaitFn(); + } elseif ($this->waitList) { + $this->invokeWaitList(); + } else { + // If there's no wait function, then reject the promise. + $this->reject('Cannot wait on a promise that has ' + .'no internal wait function. You must provide a wait ' + .'function when constructing the promise to be able to ' + .'wait on a promise.'); + } + + Utils::queue()->run(); + + /** @psalm-suppress RedundantCondition */ + if ($this->state === self::PENDING) { + $this->reject('Invoking the wait callback did not resolve the promise'); + } + } + + private function invokeWaitFn(): void + { + try { + $wfn = $this->waitFn; + $this->waitFn = null; + $wfn(true); + } catch (\Throwable $reason) { + if ($this->state === self::PENDING) { + // The promise has not been resolved yet, so reject the promise + // with the exception. + $this->reject($reason); + } else { + // The promise was already resolved, so there's a problem in + // the application. + throw $reason; + } + } + } + + private function invokeWaitList(): void + { + $waitList = $this->waitList; + $this->waitList = null; + + foreach ($waitList as $result) { + do { + $result->waitIfPending(); + $result = $result->result; + } while ($result instanceof Promise); + + if ($result instanceof PromiseInterface) { + $result->wait(false); + } + } + } +} diff --git a/3rdparty/guzzlehttp/promises/src/PromiseInterface.php b/3rdparty/guzzlehttp/promises/src/PromiseInterface.php new file mode 100644 index 00000000..c11721e4 --- /dev/null +++ b/3rdparty/guzzlehttp/promises/src/PromiseInterface.php @@ -0,0 +1,91 @@ +reason = $reason; + } + + public function then( + ?callable $onFulfilled = null, + ?callable $onRejected = null + ): PromiseInterface { + // If there's no onRejected callback then just return self. + if (!$onRejected) { + return $this; + } + + $queue = Utils::queue(); + $reason = $this->reason; + $p = new Promise([$queue, 'run']); + $queue->add(static function () use ($p, $reason, $onRejected): void { + if (Is::pending($p)) { + try { + // Return a resolved promise if onRejected does not throw. + $p->resolve($onRejected($reason)); + } catch (\Throwable $e) { + // onRejected threw, so return a rejected promise. + $p->reject($e); + } + } + }); + + return $p; + } + + public function otherwise(callable $onRejected): PromiseInterface + { + return $this->then(null, $onRejected); + } + + public function wait(bool $unwrap = true) + { + if ($unwrap) { + throw Create::exceptionFor($this->reason); + } + + return null; + } + + public function getState(): string + { + return self::REJECTED; + } + + public function resolve($value): void + { + throw new \LogicException('Cannot resolve a rejected promise'); + } + + public function reject($reason): void + { + if ($reason !== $this->reason) { + throw new \LogicException('Cannot reject a rejected promise'); + } + } + + public function cancel(): void + { + // pass + } +} diff --git a/3rdparty/guzzlehttp/promises/src/RejectionException.php b/3rdparty/guzzlehttp/promises/src/RejectionException.php new file mode 100644 index 00000000..47dca862 --- /dev/null +++ b/3rdparty/guzzlehttp/promises/src/RejectionException.php @@ -0,0 +1,49 @@ +reason = $reason; + + $message = 'The promise was rejected'; + + if ($description) { + $message .= ' with reason: '.$description; + } elseif (is_string($reason) + || (is_object($reason) && method_exists($reason, '__toString')) + ) { + $message .= ' with reason: '.$this->reason; + } elseif ($reason instanceof \JsonSerializable) { + $message .= ' with reason: '.json_encode($this->reason, JSON_PRETTY_PRINT); + } + + parent::__construct($message); + } + + /** + * Returns the rejection reason. + * + * @return mixed + */ + public function getReason() + { + return $this->reason; + } +} diff --git a/3rdparty/guzzlehttp/promises/src/TaskQueue.php b/3rdparty/guzzlehttp/promises/src/TaskQueue.php new file mode 100644 index 00000000..503e0b2d --- /dev/null +++ b/3rdparty/guzzlehttp/promises/src/TaskQueue.php @@ -0,0 +1,71 @@ +run(); + * + * @final + */ +class TaskQueue implements TaskQueueInterface +{ + private $enableShutdown = true; + private $queue = []; + + public function __construct(bool $withShutdown = true) + { + if ($withShutdown) { + register_shutdown_function(function (): void { + if ($this->enableShutdown) { + // Only run the tasks if an E_ERROR didn't occur. + $err = error_get_last(); + if (!$err || ($err['type'] ^ E_ERROR)) { + $this->run(); + } + } + }); + } + } + + public function isEmpty(): bool + { + return !$this->queue; + } + + public function add(callable $task): void + { + $this->queue[] = $task; + } + + public function run(): void + { + while ($task = array_shift($this->queue)) { + /** @var callable $task */ + $task(); + } + } + + /** + * The task queue will be run and exhausted by default when the process + * exits IFF the exit is not the result of a PHP E_ERROR error. + * + * You can disable running the automatic shutdown of the queue by calling + * this function. If you disable the task queue shutdown process, then you + * MUST either run the task queue (as a result of running your event loop + * or manually using the run() method) or wait on each outstanding promise. + * + * Note: This shutdown will occur before any destructors are triggered. + */ + public function disableShutdown(): void + { + $this->enableShutdown = false; + } +} diff --git a/3rdparty/guzzlehttp/promises/src/TaskQueueInterface.php b/3rdparty/guzzlehttp/promises/src/TaskQueueInterface.php new file mode 100644 index 00000000..34c561a4 --- /dev/null +++ b/3rdparty/guzzlehttp/promises/src/TaskQueueInterface.php @@ -0,0 +1,24 @@ + + * while ($eventLoop->isRunning()) { + * GuzzleHttp\Promise\Utils::queue()->run(); + * } + * + * + * @param TaskQueueInterface|null $assign Optionally specify a new queue instance. + */ + public static function queue(?TaskQueueInterface $assign = null): TaskQueueInterface + { + static $queue; + + if ($assign) { + $queue = $assign; + } elseif (!$queue) { + $queue = new TaskQueue(); + } + + return $queue; + } + + /** + * Adds a function to run in the task queue when it is next `run()` and + * returns a promise that is fulfilled or rejected with the result. + * + * @param callable $task Task function to run. + */ + public static function task(callable $task): PromiseInterface + { + $queue = self::queue(); + $promise = new Promise([$queue, 'run']); + $queue->add(function () use ($task, $promise): void { + try { + if (Is::pending($promise)) { + $promise->resolve($task()); + } + } catch (\Throwable $e) { + $promise->reject($e); + } + }); + + return $promise; + } + + /** + * Synchronously waits on a promise to resolve and returns an inspection + * state array. + * + * Returns a state associative array containing a "state" key mapping to a + * valid promise state. If the state of the promise is "fulfilled", the + * array will contain a "value" key mapping to the fulfilled value of the + * promise. If the promise is rejected, the array will contain a "reason" + * key mapping to the rejection reason of the promise. + * + * @param PromiseInterface $promise Promise or value. + */ + public static function inspect(PromiseInterface $promise): array + { + try { + return [ + 'state' => PromiseInterface::FULFILLED, + 'value' => $promise->wait(), + ]; + } catch (RejectionException $e) { + return ['state' => PromiseInterface::REJECTED, 'reason' => $e->getReason()]; + } catch (\Throwable $e) { + return ['state' => PromiseInterface::REJECTED, 'reason' => $e]; + } + } + + /** + * Waits on all of the provided promises, but does not unwrap rejected + * promises as thrown exception. + * + * Returns an array of inspection state arrays. + * + * @see inspect for the inspection state array format. + * + * @param PromiseInterface[] $promises Traversable of promises to wait upon. + */ + public static function inspectAll($promises): array + { + $results = []; + foreach ($promises as $key => $promise) { + $results[$key] = self::inspect($promise); + } + + return $results; + } + + /** + * Waits on all of the provided promises and returns the fulfilled values. + * + * Returns an array that contains the value of each promise (in the same + * order the promises were provided). An exception is thrown if any of the + * promises are rejected. + * + * @param iterable $promises Iterable of PromiseInterface objects to wait on. + * + * @throws \Throwable on error + */ + public static function unwrap($promises): array + { + $results = []; + foreach ($promises as $key => $promise) { + $results[$key] = $promise->wait(); + } + + return $results; + } + + /** + * Given an array of promises, return a promise that is fulfilled when all + * the items in the array are fulfilled. + * + * The promise's fulfillment value is an array with fulfillment values at + * respective positions to the original array. If any promise in the array + * rejects, the returned promise is rejected with the rejection reason. + * + * @param mixed $promises Promises or values. + * @param bool $recursive If true, resolves new promises that might have been added to the stack during its own resolution. + */ + public static function all($promises, bool $recursive = false): PromiseInterface + { + $results = []; + $promise = Each::of( + $promises, + function ($value, $idx) use (&$results): void { + $results[$idx] = $value; + }, + function ($reason, $idx, Promise $aggregate): void { + if (Is::pending($aggregate)) { + $aggregate->reject($reason); + } + } + )->then(function () use (&$results) { + ksort($results); + + return $results; + }); + + if (true === $recursive) { + $promise = $promise->then(function ($results) use ($recursive, &$promises) { + foreach ($promises as $promise) { + if (Is::pending($promise)) { + return self::all($promises, $recursive); + } + } + + return $results; + }); + } + + return $promise; + } + + /** + * Initiate a competitive race between multiple promises or values (values + * will become immediately fulfilled promises). + * + * When count amount of promises have been fulfilled, the returned promise + * is fulfilled with an array that contains the fulfillment values of the + * winners in order of resolution. + * + * This promise is rejected with a {@see AggregateException} if the number + * of fulfilled promises is less than the desired $count. + * + * @param int $count Total number of promises. + * @param mixed $promises Promises or values. + */ + public static function some(int $count, $promises): PromiseInterface + { + $results = []; + $rejections = []; + + return Each::of( + $promises, + function ($value, $idx, PromiseInterface $p) use (&$results, $count): void { + if (Is::settled($p)) { + return; + } + $results[$idx] = $value; + if (count($results) >= $count) { + $p->resolve(null); + } + }, + function ($reason) use (&$rejections): void { + $rejections[] = $reason; + } + )->then( + function () use (&$results, &$rejections, $count) { + if (count($results) !== $count) { + throw new AggregateException( + 'Not enough promises to fulfill count', + $rejections + ); + } + ksort($results); + + return array_values($results); + } + ); + } + + /** + * Like some(), with 1 as count. However, if the promise fulfills, the + * fulfillment value is not an array of 1 but the value directly. + * + * @param mixed $promises Promises or values. + */ + public static function any($promises): PromiseInterface + { + return self::some(1, $promises)->then(function ($values) { + return $values[0]; + }); + } + + /** + * Returns a promise that is fulfilled when all of the provided promises have + * been fulfilled or rejected. + * + * The returned promise is fulfilled with an array of inspection state arrays. + * + * @see inspect for the inspection state array format. + * + * @param mixed $promises Promises or values. + */ + public static function settle($promises): PromiseInterface + { + $results = []; + + return Each::of( + $promises, + function ($value, $idx) use (&$results): void { + $results[$idx] = ['state' => PromiseInterface::FULFILLED, 'value' => $value]; + }, + function ($reason, $idx) use (&$results): void { + $results[$idx] = ['state' => PromiseInterface::REJECTED, 'reason' => $reason]; + } + )->then(function () use (&$results) { + ksort($results); + + return $results; + }); + } +} diff --git a/3rdparty/guzzlehttp/psr7/LICENSE b/3rdparty/guzzlehttp/psr7/LICENSE new file mode 100644 index 00000000..51c7ec81 --- /dev/null +++ b/3rdparty/guzzlehttp/psr7/LICENSE @@ -0,0 +1,26 @@ +The MIT License (MIT) + +Copyright (c) 2015 Michael Dowling +Copyright (c) 2015 Márk Sági-Kazár +Copyright (c) 2015 Graham Campbell +Copyright (c) 2016 Tobias Schultze +Copyright (c) 2016 George Mponos +Copyright (c) 2018 Tobias Nyholm + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in +all copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +THE SOFTWARE. diff --git a/3rdparty/guzzlehttp/psr7/src/AppendStream.php b/3rdparty/guzzlehttp/psr7/src/AppendStream.php new file mode 100644 index 00000000..ee8f3788 --- /dev/null +++ b/3rdparty/guzzlehttp/psr7/src/AppendStream.php @@ -0,0 +1,248 @@ +addStream($stream); + } + } + + public function __toString(): string + { + try { + $this->rewind(); + + return $this->getContents(); + } catch (\Throwable $e) { + if (\PHP_VERSION_ID >= 70400) { + throw $e; + } + trigger_error(sprintf('%s::__toString exception: %s', self::class, (string) $e), E_USER_ERROR); + + return ''; + } + } + + /** + * Add a stream to the AppendStream + * + * @param StreamInterface $stream Stream to append. Must be readable. + * + * @throws \InvalidArgumentException if the stream is not readable + */ + public function addStream(StreamInterface $stream): void + { + if (!$stream->isReadable()) { + throw new \InvalidArgumentException('Each stream must be readable'); + } + + // The stream is only seekable if all streams are seekable + if (!$stream->isSeekable()) { + $this->seekable = false; + } + + $this->streams[] = $stream; + } + + public function getContents(): string + { + return Utils::copyToString($this); + } + + /** + * Closes each attached stream. + */ + public function close(): void + { + $this->pos = $this->current = 0; + $this->seekable = true; + + foreach ($this->streams as $stream) { + $stream->close(); + } + + $this->streams = []; + } + + /** + * Detaches each attached stream. + * + * Returns null as it's not clear which underlying stream resource to return. + */ + public function detach() + { + $this->pos = $this->current = 0; + $this->seekable = true; + + foreach ($this->streams as $stream) { + $stream->detach(); + } + + $this->streams = []; + + return null; + } + + public function tell(): int + { + return $this->pos; + } + + /** + * Tries to calculate the size by adding the size of each stream. + * + * If any of the streams do not return a valid number, then the size of the + * append stream cannot be determined and null is returned. + */ + public function getSize(): ?int + { + $size = 0; + + foreach ($this->streams as $stream) { + $s = $stream->getSize(); + if ($s === null) { + return null; + } + $size += $s; + } + + return $size; + } + + public function eof(): bool + { + return !$this->streams + || ($this->current >= count($this->streams) - 1 + && $this->streams[$this->current]->eof()); + } + + public function rewind(): void + { + $this->seek(0); + } + + /** + * Attempts to seek to the given position. Only supports SEEK_SET. + */ + public function seek($offset, $whence = SEEK_SET): void + { + if (!$this->seekable) { + throw new \RuntimeException('This AppendStream is not seekable'); + } elseif ($whence !== SEEK_SET) { + throw new \RuntimeException('The AppendStream can only seek with SEEK_SET'); + } + + $this->pos = $this->current = 0; + + // Rewind each stream + foreach ($this->streams as $i => $stream) { + try { + $stream->rewind(); + } catch (\Exception $e) { + throw new \RuntimeException('Unable to seek stream ' + .$i.' of the AppendStream', 0, $e); + } + } + + // Seek to the actual position by reading from each stream + while ($this->pos < $offset && !$this->eof()) { + $result = $this->read(min(8096, $offset - $this->pos)); + if ($result === '') { + break; + } + } + } + + /** + * Reads from all of the appended streams until the length is met or EOF. + */ + public function read($length): string + { + $buffer = ''; + $total = count($this->streams) - 1; + $remaining = $length; + $progressToNext = false; + + while ($remaining > 0) { + // Progress to the next stream if needed. + if ($progressToNext || $this->streams[$this->current]->eof()) { + $progressToNext = false; + if ($this->current === $total) { + break; + } + ++$this->current; + } + + $result = $this->streams[$this->current]->read($remaining); + + if ($result === '') { + $progressToNext = true; + continue; + } + + $buffer .= $result; + $remaining = $length - strlen($buffer); + } + + $this->pos += strlen($buffer); + + return $buffer; + } + + public function isReadable(): bool + { + return true; + } + + public function isWritable(): bool + { + return false; + } + + public function isSeekable(): bool + { + return $this->seekable; + } + + public function write($string): int + { + throw new \RuntimeException('Cannot write to an AppendStream'); + } + + /** + * @return mixed + */ + public function getMetadata($key = null) + { + return $key ? null : []; + } +} diff --git a/3rdparty/guzzlehttp/psr7/src/BufferStream.php b/3rdparty/guzzlehttp/psr7/src/BufferStream.php new file mode 100644 index 00000000..2b0eb77b --- /dev/null +++ b/3rdparty/guzzlehttp/psr7/src/BufferStream.php @@ -0,0 +1,147 @@ +hwm = $hwm; + } + + public function __toString(): string + { + return $this->getContents(); + } + + public function getContents(): string + { + $buffer = $this->buffer; + $this->buffer = ''; + + return $buffer; + } + + public function close(): void + { + $this->buffer = ''; + } + + public function detach() + { + $this->close(); + + return null; + } + + public function getSize(): ?int + { + return strlen($this->buffer); + } + + public function isReadable(): bool + { + return true; + } + + public function isWritable(): bool + { + return true; + } + + public function isSeekable(): bool + { + return false; + } + + public function rewind(): void + { + $this->seek(0); + } + + public function seek($offset, $whence = SEEK_SET): void + { + throw new \RuntimeException('Cannot seek a BufferStream'); + } + + public function eof(): bool + { + return strlen($this->buffer) === 0; + } + + public function tell(): int + { + throw new \RuntimeException('Cannot determine the position of a BufferStream'); + } + + /** + * Reads data from the buffer. + */ + public function read($length): string + { + $currentLength = strlen($this->buffer); + + if ($length >= $currentLength) { + // No need to slice the buffer because we don't have enough data. + $result = $this->buffer; + $this->buffer = ''; + } else { + // Slice up the result to provide a subset of the buffer. + $result = substr($this->buffer, 0, $length); + $this->buffer = substr($this->buffer, $length); + } + + return $result; + } + + /** + * Writes data to the buffer. + */ + public function write($string): int + { + $this->buffer .= $string; + + if (strlen($this->buffer) >= $this->hwm) { + return 0; + } + + return strlen($string); + } + + /** + * @return mixed + */ + public function getMetadata($key = null) + { + if ($key === 'hwm') { + return $this->hwm; + } + + return $key ? null : []; + } +} diff --git a/3rdparty/guzzlehttp/psr7/src/CachingStream.php b/3rdparty/guzzlehttp/psr7/src/CachingStream.php new file mode 100644 index 00000000..7e4554d5 --- /dev/null +++ b/3rdparty/guzzlehttp/psr7/src/CachingStream.php @@ -0,0 +1,153 @@ +remoteStream = $stream; + $this->stream = $target ?: new Stream(Utils::tryFopen('php://temp', 'r+')); + } + + public function getSize(): ?int + { + $remoteSize = $this->remoteStream->getSize(); + + if (null === $remoteSize) { + return null; + } + + return max($this->stream->getSize(), $remoteSize); + } + + public function rewind(): void + { + $this->seek(0); + } + + public function seek($offset, $whence = SEEK_SET): void + { + if ($whence === SEEK_SET) { + $byte = $offset; + } elseif ($whence === SEEK_CUR) { + $byte = $offset + $this->tell(); + } elseif ($whence === SEEK_END) { + $size = $this->remoteStream->getSize(); + if ($size === null) { + $size = $this->cacheEntireStream(); + } + $byte = $size + $offset; + } else { + throw new \InvalidArgumentException('Invalid whence'); + } + + $diff = $byte - $this->stream->getSize(); + + if ($diff > 0) { + // Read the remoteStream until we have read in at least the amount + // of bytes requested, or we reach the end of the file. + while ($diff > 0 && !$this->remoteStream->eof()) { + $this->read($diff); + $diff = $byte - $this->stream->getSize(); + } + } else { + // We can just do a normal seek since we've already seen this byte. + $this->stream->seek($byte); + } + } + + public function read($length): string + { + // Perform a regular read on any previously read data from the buffer + $data = $this->stream->read($length); + $remaining = $length - strlen($data); + + // More data was requested so read from the remote stream + if ($remaining) { + // If data was written to the buffer in a position that would have + // been filled from the remote stream, then we must skip bytes on + // the remote stream to emulate overwriting bytes from that + // position. This mimics the behavior of other PHP stream wrappers. + $remoteData = $this->remoteStream->read( + $remaining + $this->skipReadBytes + ); + + if ($this->skipReadBytes) { + $len = strlen($remoteData); + $remoteData = substr($remoteData, $this->skipReadBytes); + $this->skipReadBytes = max(0, $this->skipReadBytes - $len); + } + + $data .= $remoteData; + $this->stream->write($remoteData); + } + + return $data; + } + + public function write($string): int + { + // When appending to the end of the currently read stream, you'll want + // to skip bytes from being read from the remote stream to emulate + // other stream wrappers. Basically replacing bytes of data of a fixed + // length. + $overflow = (strlen($string) + $this->tell()) - $this->remoteStream->tell(); + if ($overflow > 0) { + $this->skipReadBytes += $overflow; + } + + return $this->stream->write($string); + } + + public function eof(): bool + { + return $this->stream->eof() && $this->remoteStream->eof(); + } + + /** + * Close both the remote stream and buffer stream + */ + public function close(): void + { + $this->remoteStream->close(); + $this->stream->close(); + } + + private function cacheEntireStream(): int + { + $target = new FnStream(['write' => 'strlen']); + Utils::copyToStream($this, $target); + + return $this->tell(); + } +} diff --git a/3rdparty/guzzlehttp/psr7/src/DroppingStream.php b/3rdparty/guzzlehttp/psr7/src/DroppingStream.php new file mode 100644 index 00000000..6e3d209d --- /dev/null +++ b/3rdparty/guzzlehttp/psr7/src/DroppingStream.php @@ -0,0 +1,49 @@ +stream = $stream; + $this->maxLength = $maxLength; + } + + public function write($string): int + { + $diff = $this->maxLength - $this->stream->getSize(); + + // Begin returning 0 when the underlying stream is too large. + if ($diff <= 0) { + return 0; + } + + // Write the stream or a subset of the stream if needed. + if (strlen($string) < $diff) { + return $this->stream->write($string); + } + + return $this->stream->write(substr($string, 0, $diff)); + } +} diff --git a/3rdparty/guzzlehttp/psr7/src/Exception/MalformedUriException.php b/3rdparty/guzzlehttp/psr7/src/Exception/MalformedUriException.php new file mode 100644 index 00000000..3a084779 --- /dev/null +++ b/3rdparty/guzzlehttp/psr7/src/Exception/MalformedUriException.php @@ -0,0 +1,14 @@ + */ + private $methods; + + /** + * @param array $methods Hash of method name to a callable. + */ + public function __construct(array $methods) + { + $this->methods = $methods; + + // Create the functions on the class + foreach ($methods as $name => $fn) { + $this->{'_fn_'.$name} = $fn; + } + } + + /** + * Lazily determine which methods are not implemented. + * + * @throws \BadMethodCallException + */ + public function __get(string $name): void + { + throw new \BadMethodCallException(str_replace('_fn_', '', $name) + .'() is not implemented in the FnStream'); + } + + /** + * The close method is called on the underlying stream only if possible. + */ + public function __destruct() + { + if (isset($this->_fn_close)) { + ($this->_fn_close)(); + } + } + + /** + * An unserialize would allow the __destruct to run when the unserialized value goes out of scope. + * + * @throws \LogicException + */ + public function __wakeup(): void + { + throw new \LogicException('FnStream should never be unserialized'); + } + + /** + * Adds custom functionality to an underlying stream by intercepting + * specific method calls. + * + * @param StreamInterface $stream Stream to decorate + * @param array $methods Hash of method name to a closure + * + * @return FnStream + */ + public static function decorate(StreamInterface $stream, array $methods) + { + // If any of the required methods were not provided, then simply + // proxy to the decorated stream. + foreach (array_diff(self::SLOTS, array_keys($methods)) as $diff) { + /** @var callable $callable */ + $callable = [$stream, $diff]; + $methods[$diff] = $callable; + } + + return new self($methods); + } + + public function __toString(): string + { + try { + /** @var string */ + return ($this->_fn___toString)(); + } catch (\Throwable $e) { + if (\PHP_VERSION_ID >= 70400) { + throw $e; + } + trigger_error(sprintf('%s::__toString exception: %s', self::class, (string) $e), E_USER_ERROR); + + return ''; + } + } + + public function close(): void + { + ($this->_fn_close)(); + } + + public function detach() + { + return ($this->_fn_detach)(); + } + + public function getSize(): ?int + { + return ($this->_fn_getSize)(); + } + + public function tell(): int + { + return ($this->_fn_tell)(); + } + + public function eof(): bool + { + return ($this->_fn_eof)(); + } + + public function isSeekable(): bool + { + return ($this->_fn_isSeekable)(); + } + + public function rewind(): void + { + ($this->_fn_rewind)(); + } + + public function seek($offset, $whence = SEEK_SET): void + { + ($this->_fn_seek)($offset, $whence); + } + + public function isWritable(): bool + { + return ($this->_fn_isWritable)(); + } + + public function write($string): int + { + return ($this->_fn_write)($string); + } + + public function isReadable(): bool + { + return ($this->_fn_isReadable)(); + } + + public function read($length): string + { + return ($this->_fn_read)($length); + } + + public function getContents(): string + { + return ($this->_fn_getContents)(); + } + + /** + * @return mixed + */ + public function getMetadata($key = null) + { + return ($this->_fn_getMetadata)($key); + } +} diff --git a/3rdparty/guzzlehttp/psr7/src/Header.php b/3rdparty/guzzlehttp/psr7/src/Header.php new file mode 100644 index 00000000..bbce8b03 --- /dev/null +++ b/3rdparty/guzzlehttp/psr7/src/Header.php @@ -0,0 +1,134 @@ +]+>|[^=]+/', $kvp, $matches)) { + $m = $matches[0]; + if (isset($m[1])) { + $part[trim($m[0], $trimmed)] = trim($m[1], $trimmed); + } else { + $part[] = trim($m[0], $trimmed); + } + } + } + if ($part) { + $params[] = $part; + } + } + } + + return $params; + } + + /** + * Converts an array of header values that may contain comma separated + * headers into an array of headers with no comma separated values. + * + * @param string|array $header Header to normalize. + * + * @deprecated Use self::splitList() instead. + */ + public static function normalize($header): array + { + $result = []; + foreach ((array) $header as $value) { + foreach (self::splitList($value) as $parsed) { + $result[] = $parsed; + } + } + + return $result; + } + + /** + * Splits a HTTP header defined to contain a comma-separated list into + * each individual value. Empty values will be removed. + * + * Example headers include 'accept', 'cache-control' and 'if-none-match'. + * + * This method must not be used to parse headers that are not defined as + * a list, such as 'user-agent' or 'set-cookie'. + * + * @param string|string[] $values Header value as returned by MessageInterface::getHeader() + * + * @return string[] + */ + public static function splitList($values): array + { + if (!\is_array($values)) { + $values = [$values]; + } + + $result = []; + foreach ($values as $value) { + if (!\is_string($value)) { + throw new \TypeError('$header must either be a string or an array containing strings.'); + } + + $v = ''; + $isQuoted = false; + $isEscaped = false; + for ($i = 0, $max = \strlen($value); $i < $max; ++$i) { + if ($isEscaped) { + $v .= $value[$i]; + $isEscaped = false; + + continue; + } + + if (!$isQuoted && $value[$i] === ',') { + $v = \trim($v); + if ($v !== '') { + $result[] = $v; + } + + $v = ''; + continue; + } + + if ($isQuoted && $value[$i] === '\\') { + $isEscaped = true; + $v .= $value[$i]; + + continue; + } + if ($value[$i] === '"') { + $isQuoted = !$isQuoted; + $v .= $value[$i]; + + continue; + } + + $v .= $value[$i]; + } + + $v = \trim($v); + if ($v !== '') { + $result[] = $v; + } + } + + return $result; + } +} diff --git a/3rdparty/guzzlehttp/psr7/src/HttpFactory.php b/3rdparty/guzzlehttp/psr7/src/HttpFactory.php new file mode 100644 index 00000000..3ef15103 --- /dev/null +++ b/3rdparty/guzzlehttp/psr7/src/HttpFactory.php @@ -0,0 +1,94 @@ +getSize(); + } + + return new UploadedFile($stream, $size, $error, $clientFilename, $clientMediaType); + } + + public function createStream(string $content = ''): StreamInterface + { + return Utils::streamFor($content); + } + + public function createStreamFromFile(string $file, string $mode = 'r'): StreamInterface + { + try { + $resource = Utils::tryFopen($file, $mode); + } catch (\RuntimeException $e) { + if ('' === $mode || false === \in_array($mode[0], ['r', 'w', 'a', 'x', 'c'], true)) { + throw new \InvalidArgumentException(sprintf('Invalid file opening mode "%s"', $mode), 0, $e); + } + + throw $e; + } + + return Utils::streamFor($resource); + } + + public function createStreamFromResource($resource): StreamInterface + { + return Utils::streamFor($resource); + } + + public function createServerRequest(string $method, $uri, array $serverParams = []): ServerRequestInterface + { + if (empty($method)) { + if (!empty($serverParams['REQUEST_METHOD'])) { + $method = $serverParams['REQUEST_METHOD']; + } else { + throw new \InvalidArgumentException('Cannot determine HTTP method'); + } + } + + return new ServerRequest($method, $uri, [], null, '1.1', $serverParams); + } + + public function createResponse(int $code = 200, string $reasonPhrase = ''): ResponseInterface + { + return new Response($code, [], null, '1.1', $reasonPhrase); + } + + public function createRequest(string $method, $uri): RequestInterface + { + return new Request($method, $uri); + } + + public function createUri(string $uri = ''): UriInterface + { + return new Uri($uri); + } +} diff --git a/3rdparty/guzzlehttp/psr7/src/InflateStream.php b/3rdparty/guzzlehttp/psr7/src/InflateStream.php new file mode 100644 index 00000000..e674c9ab --- /dev/null +++ b/3rdparty/guzzlehttp/psr7/src/InflateStream.php @@ -0,0 +1,37 @@ + 15 + 32]); + $this->stream = $stream->isSeekable() ? new Stream($resource) : new NoSeekStream(new Stream($resource)); + } +} diff --git a/3rdparty/guzzlehttp/psr7/src/LazyOpenStream.php b/3rdparty/guzzlehttp/psr7/src/LazyOpenStream.php new file mode 100644 index 00000000..f6c84904 --- /dev/null +++ b/3rdparty/guzzlehttp/psr7/src/LazyOpenStream.php @@ -0,0 +1,49 @@ +filename = $filename; + $this->mode = $mode; + + // unsetting the property forces the first access to go through + // __get(). + unset($this->stream); + } + + /** + * Creates the underlying stream lazily when required. + */ + protected function createStream(): StreamInterface + { + return Utils::streamFor(Utils::tryFopen($this->filename, $this->mode)); + } +} diff --git a/3rdparty/guzzlehttp/psr7/src/LimitStream.php b/3rdparty/guzzlehttp/psr7/src/LimitStream.php new file mode 100644 index 00000000..fb223255 --- /dev/null +++ b/3rdparty/guzzlehttp/psr7/src/LimitStream.php @@ -0,0 +1,157 @@ +stream = $stream; + $this->setLimit($limit); + $this->setOffset($offset); + } + + public function eof(): bool + { + // Always return true if the underlying stream is EOF + if ($this->stream->eof()) { + return true; + } + + // No limit and the underlying stream is not at EOF + if ($this->limit === -1) { + return false; + } + + return $this->stream->tell() >= $this->offset + $this->limit; + } + + /** + * Returns the size of the limited subset of data + */ + public function getSize(): ?int + { + if (null === ($length = $this->stream->getSize())) { + return null; + } elseif ($this->limit === -1) { + return $length - $this->offset; + } else { + return min($this->limit, $length - $this->offset); + } + } + + /** + * Allow for a bounded seek on the read limited stream + */ + public function seek($offset, $whence = SEEK_SET): void + { + if ($whence !== SEEK_SET || $offset < 0) { + throw new \RuntimeException(sprintf( + 'Cannot seek to offset %s with whence %s', + $offset, + $whence + )); + } + + $offset += $this->offset; + + if ($this->limit !== -1) { + if ($offset > $this->offset + $this->limit) { + $offset = $this->offset + $this->limit; + } + } + + $this->stream->seek($offset); + } + + /** + * Give a relative tell() + */ + public function tell(): int + { + return $this->stream->tell() - $this->offset; + } + + /** + * Set the offset to start limiting from + * + * @param int $offset Offset to seek to and begin byte limiting from + * + * @throws \RuntimeException if the stream cannot be seeked. + */ + public function setOffset(int $offset): void + { + $current = $this->stream->tell(); + + if ($current !== $offset) { + // If the stream cannot seek to the offset position, then read to it + if ($this->stream->isSeekable()) { + $this->stream->seek($offset); + } elseif ($current > $offset) { + throw new \RuntimeException("Could not seek to stream offset $offset"); + } else { + $this->stream->read($offset - $current); + } + } + + $this->offset = $offset; + } + + /** + * Set the limit of bytes that the decorator allows to be read from the + * stream. + * + * @param int $limit Number of bytes to allow to be read from the stream. + * Use -1 for no limit. + */ + public function setLimit(int $limit): void + { + $this->limit = $limit; + } + + public function read($length): string + { + if ($this->limit === -1) { + return $this->stream->read($length); + } + + // Check if the current position is less than the total allowed + // bytes + original offset + $remaining = ($this->offset + $this->limit) - $this->stream->tell(); + if ($remaining > 0) { + // Only return the amount of requested data, ensuring that the byte + // limit is not exceeded + return $this->stream->read(min($remaining, $length)); + } + + return ''; + } +} diff --git a/3rdparty/guzzlehttp/psr7/src/Message.php b/3rdparty/guzzlehttp/psr7/src/Message.php new file mode 100644 index 00000000..5561a513 --- /dev/null +++ b/3rdparty/guzzlehttp/psr7/src/Message.php @@ -0,0 +1,246 @@ +getMethod().' ' + .$message->getRequestTarget()) + .' HTTP/'.$message->getProtocolVersion(); + if (!$message->hasHeader('host')) { + $msg .= "\r\nHost: ".$message->getUri()->getHost(); + } + } elseif ($message instanceof ResponseInterface) { + $msg = 'HTTP/'.$message->getProtocolVersion().' ' + .$message->getStatusCode().' ' + .$message->getReasonPhrase(); + } else { + throw new \InvalidArgumentException('Unknown message type'); + } + + foreach ($message->getHeaders() as $name => $values) { + if (is_string($name) && strtolower($name) === 'set-cookie') { + foreach ($values as $value) { + $msg .= "\r\n{$name}: ".$value; + } + } else { + $msg .= "\r\n{$name}: ".implode(', ', $values); + } + } + + return "{$msg}\r\n\r\n".$message->getBody(); + } + + /** + * Get a short summary of the message body. + * + * Will return `null` if the response is not printable. + * + * @param MessageInterface $message The message to get the body summary + * @param int $truncateAt The maximum allowed size of the summary + */ + public static function bodySummary(MessageInterface $message, int $truncateAt = 120): ?string + { + $body = $message->getBody(); + + if (!$body->isSeekable() || !$body->isReadable()) { + return null; + } + + $size = $body->getSize(); + + if ($size === 0) { + return null; + } + + $body->rewind(); + $summary = $body->read($truncateAt); + $body->rewind(); + + if ($size > $truncateAt) { + $summary .= ' (truncated...)'; + } + + // Matches any printable character, including unicode characters: + // letters, marks, numbers, punctuation, spacing, and separators. + if (preg_match('/[^\pL\pM\pN\pP\pS\pZ\n\r\t]/u', $summary) !== 0) { + return null; + } + + return $summary; + } + + /** + * Attempts to rewind a message body and throws an exception on failure. + * + * The body of the message will only be rewound if a call to `tell()` + * returns a value other than `0`. + * + * @param MessageInterface $message Message to rewind + * + * @throws \RuntimeException + */ + public static function rewindBody(MessageInterface $message): void + { + $body = $message->getBody(); + + if ($body->tell()) { + $body->rewind(); + } + } + + /** + * Parses an HTTP message into an associative array. + * + * The array contains the "start-line" key containing the start line of + * the message, "headers" key containing an associative array of header + * array values, and a "body" key containing the body of the message. + * + * @param string $message HTTP request or response to parse. + */ + public static function parseMessage(string $message): array + { + if (!$message) { + throw new \InvalidArgumentException('Invalid message'); + } + + $message = ltrim($message, "\r\n"); + + $messageParts = preg_split("/\r?\n\r?\n/", $message, 2); + + if ($messageParts === false || count($messageParts) !== 2) { + throw new \InvalidArgumentException('Invalid message: Missing header delimiter'); + } + + [$rawHeaders, $body] = $messageParts; + $rawHeaders .= "\r\n"; // Put back the delimiter we split previously + $headerParts = preg_split("/\r?\n/", $rawHeaders, 2); + + if ($headerParts === false || count($headerParts) !== 2) { + throw new \InvalidArgumentException('Invalid message: Missing status line'); + } + + [$startLine, $rawHeaders] = $headerParts; + + if (preg_match("/(?:^HTTP\/|^[A-Z]+ \S+ HTTP\/)(\d+(?:\.\d+)?)/i", $startLine, $matches) && $matches[1] === '1.0') { + // Header folding is deprecated for HTTP/1.1, but allowed in HTTP/1.0 + $rawHeaders = preg_replace(Rfc7230::HEADER_FOLD_REGEX, ' ', $rawHeaders); + } + + /** @var array[] $headerLines */ + $count = preg_match_all(Rfc7230::HEADER_REGEX, $rawHeaders, $headerLines, PREG_SET_ORDER); + + // If these aren't the same, then one line didn't match and there's an invalid header. + if ($count !== substr_count($rawHeaders, "\n")) { + // Folding is deprecated, see https://datatracker.ietf.org/doc/html/rfc7230#section-3.2.4 + if (preg_match(Rfc7230::HEADER_FOLD_REGEX, $rawHeaders)) { + throw new \InvalidArgumentException('Invalid header syntax: Obsolete line folding'); + } + + throw new \InvalidArgumentException('Invalid header syntax'); + } + + $headers = []; + + foreach ($headerLines as $headerLine) { + $headers[$headerLine[1]][] = $headerLine[2]; + } + + return [ + 'start-line' => $startLine, + 'headers' => $headers, + 'body' => $body, + ]; + } + + /** + * Constructs a URI for an HTTP request message. + * + * @param string $path Path from the start-line + * @param array $headers Array of headers (each value an array). + */ + public static function parseRequestUri(string $path, array $headers): string + { + $hostKey = array_filter(array_keys($headers), function ($k) { + // Numeric array keys are converted to int by PHP. + $k = (string) $k; + + return strtolower($k) === 'host'; + }); + + // If no host is found, then a full URI cannot be constructed. + if (!$hostKey) { + return $path; + } + + $host = $headers[reset($hostKey)][0]; + $scheme = substr($host, -4) === ':443' ? 'https' : 'http'; + + return $scheme.'://'.$host.'/'.ltrim($path, '/'); + } + + /** + * Parses a request message string into a request object. + * + * @param string $message Request message string. + */ + public static function parseRequest(string $message): RequestInterface + { + $data = self::parseMessage($message); + $matches = []; + if (!preg_match('/^[\S]+\s+([a-zA-Z]+:\/\/|\/).*/', $data['start-line'], $matches)) { + throw new \InvalidArgumentException('Invalid request string'); + } + $parts = explode(' ', $data['start-line'], 3); + $version = isset($parts[2]) ? explode('/', $parts[2])[1] : '1.1'; + + $request = new Request( + $parts[0], + $matches[1] === '/' ? self::parseRequestUri($parts[1], $data['headers']) : $parts[1], + $data['headers'], + $data['body'], + $version + ); + + return $matches[1] === '/' ? $request : $request->withRequestTarget($parts[1]); + } + + /** + * Parses a response message string into a response object. + * + * @param string $message Response message string. + */ + public static function parseResponse(string $message): ResponseInterface + { + $data = self::parseMessage($message); + // According to https://datatracker.ietf.org/doc/html/rfc7230#section-3.1.2 + // the space between status-code and reason-phrase is required. But + // browsers accept responses without space and reason as well. + if (!preg_match('/^HTTP\/.* [0-9]{3}( .*|$)/', $data['start-line'])) { + throw new \InvalidArgumentException('Invalid response string: '.$data['start-line']); + } + $parts = explode(' ', $data['start-line'], 3); + + return new Response( + (int) $parts[1], + $data['headers'], + $data['body'], + explode('/', $parts[0])[1], + $parts[2] ?? null + ); + } +} diff --git a/3rdparty/guzzlehttp/psr7/src/MessageTrait.php b/3rdparty/guzzlehttp/psr7/src/MessageTrait.php new file mode 100644 index 00000000..65dbc4ba --- /dev/null +++ b/3rdparty/guzzlehttp/psr7/src/MessageTrait.php @@ -0,0 +1,265 @@ + array of values */ + private $headers = []; + + /** @var string[] Map of lowercase header name => original name at registration */ + private $headerNames = []; + + /** @var string */ + private $protocol = '1.1'; + + /** @var StreamInterface|null */ + private $stream; + + public function getProtocolVersion(): string + { + return $this->protocol; + } + + public function withProtocolVersion($version): MessageInterface + { + if ($this->protocol === $version) { + return $this; + } + + $new = clone $this; + $new->protocol = $version; + + return $new; + } + + public function getHeaders(): array + { + return $this->headers; + } + + public function hasHeader($header): bool + { + return isset($this->headerNames[strtolower($header)]); + } + + public function getHeader($header): array + { + $header = strtolower($header); + + if (!isset($this->headerNames[$header])) { + return []; + } + + $header = $this->headerNames[$header]; + + return $this->headers[$header]; + } + + public function getHeaderLine($header): string + { + return implode(', ', $this->getHeader($header)); + } + + public function withHeader($header, $value): MessageInterface + { + $this->assertHeader($header); + $value = $this->normalizeHeaderValue($value); + $normalized = strtolower($header); + + $new = clone $this; + if (isset($new->headerNames[$normalized])) { + unset($new->headers[$new->headerNames[$normalized]]); + } + $new->headerNames[$normalized] = $header; + $new->headers[$header] = $value; + + return $new; + } + + public function withAddedHeader($header, $value): MessageInterface + { + $this->assertHeader($header); + $value = $this->normalizeHeaderValue($value); + $normalized = strtolower($header); + + $new = clone $this; + if (isset($new->headerNames[$normalized])) { + $header = $this->headerNames[$normalized]; + $new->headers[$header] = array_merge($this->headers[$header], $value); + } else { + $new->headerNames[$normalized] = $header; + $new->headers[$header] = $value; + } + + return $new; + } + + public function withoutHeader($header): MessageInterface + { + $normalized = strtolower($header); + + if (!isset($this->headerNames[$normalized])) { + return $this; + } + + $header = $this->headerNames[$normalized]; + + $new = clone $this; + unset($new->headers[$header], $new->headerNames[$normalized]); + + return $new; + } + + public function getBody(): StreamInterface + { + if (!$this->stream) { + $this->stream = Utils::streamFor(''); + } + + return $this->stream; + } + + public function withBody(StreamInterface $body): MessageInterface + { + if ($body === $this->stream) { + return $this; + } + + $new = clone $this; + $new->stream = $body; + + return $new; + } + + /** + * @param (string|string[])[] $headers + */ + private function setHeaders(array $headers): void + { + $this->headerNames = $this->headers = []; + foreach ($headers as $header => $value) { + // Numeric array keys are converted to int by PHP. + $header = (string) $header; + + $this->assertHeader($header); + $value = $this->normalizeHeaderValue($value); + $normalized = strtolower($header); + if (isset($this->headerNames[$normalized])) { + $header = $this->headerNames[$normalized]; + $this->headers[$header] = array_merge($this->headers[$header], $value); + } else { + $this->headerNames[$normalized] = $header; + $this->headers[$header] = $value; + } + } + } + + /** + * @param mixed $value + * + * @return string[] + */ + private function normalizeHeaderValue($value): array + { + if (!is_array($value)) { + return $this->trimAndValidateHeaderValues([$value]); + } + + if (count($value) === 0) { + throw new \InvalidArgumentException('Header value can not be an empty array.'); + } + + return $this->trimAndValidateHeaderValues($value); + } + + /** + * Trims whitespace from the header values. + * + * Spaces and tabs ought to be excluded by parsers when extracting the field value from a header field. + * + * header-field = field-name ":" OWS field-value OWS + * OWS = *( SP / HTAB ) + * + * @param mixed[] $values Header values + * + * @return string[] Trimmed header values + * + * @see https://datatracker.ietf.org/doc/html/rfc7230#section-3.2.4 + */ + private function trimAndValidateHeaderValues(array $values): array + { + return array_map(function ($value) { + if (!is_scalar($value) && null !== $value) { + throw new \InvalidArgumentException(sprintf( + 'Header value must be scalar or null but %s provided.', + is_object($value) ? get_class($value) : gettype($value) + )); + } + + $trimmed = trim((string) $value, " \t"); + $this->assertValue($trimmed); + + return $trimmed; + }, array_values($values)); + } + + /** + * @see https://datatracker.ietf.org/doc/html/rfc7230#section-3.2 + * + * @param mixed $header + */ + private function assertHeader($header): void + { + if (!is_string($header)) { + throw new \InvalidArgumentException(sprintf( + 'Header name must be a string but %s provided.', + is_object($header) ? get_class($header) : gettype($header) + )); + } + + if (!preg_match('/^[a-zA-Z0-9\'`#$%&*+.^_|~!-]+$/D', $header)) { + throw new \InvalidArgumentException( + sprintf('"%s" is not valid header name.', $header) + ); + } + } + + /** + * @see https://datatracker.ietf.org/doc/html/rfc7230#section-3.2 + * + * field-value = *( field-content / obs-fold ) + * field-content = field-vchar [ 1*( SP / HTAB ) field-vchar ] + * field-vchar = VCHAR / obs-text + * VCHAR = %x21-7E + * obs-text = %x80-FF + * obs-fold = CRLF 1*( SP / HTAB ) + */ + private function assertValue(string $value): void + { + // The regular expression intentionally does not support the obs-fold production, because as + // per RFC 7230#3.2.4: + // + // A sender MUST NOT generate a message that includes + // line folding (i.e., that has any field-value that contains a match to + // the obs-fold rule) unless the message is intended for packaging + // within the message/http media type. + // + // Clients must not send a request with line folding and a server sending folded headers is + // likely very rare. Line folding is a fairly obscure feature of HTTP/1.1 and thus not accepting + // folding is not likely to break any legitimate use case. + if (!preg_match('/^[\x20\x09\x21-\x7E\x80-\xFF]*$/D', $value)) { + throw new \InvalidArgumentException( + sprintf('"%s" is not valid header value.', $value) + ); + } + } +} diff --git a/3rdparty/guzzlehttp/psr7/src/MimeType.php b/3rdparty/guzzlehttp/psr7/src/MimeType.php new file mode 100644 index 00000000..b131bdbe --- /dev/null +++ b/3rdparty/guzzlehttp/psr7/src/MimeType.php @@ -0,0 +1,1259 @@ + 'application/vnd.1000minds.decision-model+xml', + '3dml' => 'text/vnd.in3d.3dml', + '3ds' => 'image/x-3ds', + '3g2' => 'video/3gpp2', + '3gp' => 'video/3gp', + '3gpp' => 'video/3gpp', + '3mf' => 'model/3mf', + '7z' => 'application/x-7z-compressed', + '7zip' => 'application/x-7z-compressed', + '123' => 'application/vnd.lotus-1-2-3', + 'aab' => 'application/x-authorware-bin', + 'aac' => 'audio/aac', + 'aam' => 'application/x-authorware-map', + 'aas' => 'application/x-authorware-seg', + 'abw' => 'application/x-abiword', + 'ac' => 'application/vnd.nokia.n-gage.ac+xml', + 'ac3' => 'audio/ac3', + 'acc' => 'application/vnd.americandynamics.acc', + 'ace' => 'application/x-ace-compressed', + 'acu' => 'application/vnd.acucobol', + 'acutc' => 'application/vnd.acucorp', + 'adp' => 'audio/adpcm', + 'adts' => 'audio/aac', + 'aep' => 'application/vnd.audiograph', + 'afm' => 'application/x-font-type1', + 'afp' => 'application/vnd.ibm.modcap', + 'age' => 'application/vnd.age', + 'ahead' => 'application/vnd.ahead.space', + 'ai' => 'application/pdf', + 'aif' => 'audio/x-aiff', + 'aifc' => 'audio/x-aiff', + 'aiff' => 'audio/x-aiff', + 'air' => 'application/vnd.adobe.air-application-installer-package+zip', + 'ait' => 'application/vnd.dvb.ait', + 'ami' => 'application/vnd.amiga.ami', + 'aml' => 'application/automationml-aml+xml', + 'amlx' => 'application/automationml-amlx+zip', + 'amr' => 'audio/amr', + 'apk' => 'application/vnd.android.package-archive', + 'apng' => 'image/apng', + 'appcache' => 'text/cache-manifest', + 'appinstaller' => 'application/appinstaller', + 'application' => 'application/x-ms-application', + 'appx' => 'application/appx', + 'appxbundle' => 'application/appxbundle', + 'apr' => 'application/vnd.lotus-approach', + 'arc' => 'application/x-freearc', + 'arj' => 'application/x-arj', + 'asc' => 'application/pgp-signature', + 'asf' => 'video/x-ms-asf', + 'asm' => 'text/x-asm', + 'aso' => 'application/vnd.accpac.simply.aso', + 'asx' => 'video/x-ms-asf', + 'atc' => 'application/vnd.acucorp', + 'atom' => 'application/atom+xml', + 'atomcat' => 'application/atomcat+xml', + 'atomdeleted' => 'application/atomdeleted+xml', + 'atomsvc' => 'application/atomsvc+xml', + 'atx' => 'application/vnd.antix.game-component', + 'au' => 'audio/x-au', + 'avci' => 'image/avci', + 'avcs' => 'image/avcs', + 'avi' => 'video/x-msvideo', + 'avif' => 'image/avif', + 'aw' => 'application/applixware', + 'azf' => 'application/vnd.airzip.filesecure.azf', + 'azs' => 'application/vnd.airzip.filesecure.azs', + 'azv' => 'image/vnd.airzip.accelerator.azv', + 'azw' => 'application/vnd.amazon.ebook', + 'b16' => 'image/vnd.pco.b16', + 'bat' => 'application/x-msdownload', + 'bcpio' => 'application/x-bcpio', + 'bdf' => 'application/x-font-bdf', + 'bdm' => 'application/vnd.syncml.dm+wbxml', + 'bdoc' => 'application/x-bdoc', + 'bed' => 'application/vnd.realvnc.bed', + 'bh2' => 'application/vnd.fujitsu.oasysprs', + 'bin' => 'application/octet-stream', + 'blb' => 'application/x-blorb', + 'blorb' => 'application/x-blorb', + 'bmi' => 'application/vnd.bmi', + 'bmml' => 'application/vnd.balsamiq.bmml+xml', + 'bmp' => 'image/bmp', + 'book' => 'application/vnd.framemaker', + 'box' => 'application/vnd.previewsystems.box', + 'boz' => 'application/x-bzip2', + 'bpk' => 'application/octet-stream', + 'bpmn' => 'application/octet-stream', + 'bsp' => 'model/vnd.valve.source.compiled-map', + 'btf' => 'image/prs.btif', + 'btif' => 'image/prs.btif', + 'buffer' => 'application/octet-stream', + 'bz' => 'application/x-bzip', + 'bz2' => 'application/x-bzip2', + 'c' => 'text/x-c', + 'c4d' => 'application/vnd.clonk.c4group', + 'c4f' => 'application/vnd.clonk.c4group', + 'c4g' => 'application/vnd.clonk.c4group', + 'c4p' => 'application/vnd.clonk.c4group', + 'c4u' => 'application/vnd.clonk.c4group', + 'c11amc' => 'application/vnd.cluetrust.cartomobile-config', + 'c11amz' => 'application/vnd.cluetrust.cartomobile-config-pkg', + 'cab' => 'application/vnd.ms-cab-compressed', + 'caf' => 'audio/x-caf', + 'cap' => 'application/vnd.tcpdump.pcap', + 'car' => 'application/vnd.curl.car', + 'cat' => 'application/vnd.ms-pki.seccat', + 'cb7' => 'application/x-cbr', + 'cba' => 'application/x-cbr', + 'cbr' => 'application/x-cbr', + 'cbt' => 'application/x-cbr', + 'cbz' => 'application/x-cbr', + 'cc' => 'text/x-c', + 'cco' => 'application/x-cocoa', + 'cct' => 'application/x-director', + 'ccxml' => 'application/ccxml+xml', + 'cdbcmsg' => 'application/vnd.contact.cmsg', + 'cdf' => 'application/x-netcdf', + 'cdfx' => 'application/cdfx+xml', + 'cdkey' => 'application/vnd.mediastation.cdkey', + 'cdmia' => 'application/cdmi-capability', + 'cdmic' => 'application/cdmi-container', + 'cdmid' => 'application/cdmi-domain', + 'cdmio' => 'application/cdmi-object', + 'cdmiq' => 'application/cdmi-queue', + 'cdr' => 'application/cdr', + 'cdx' => 'chemical/x-cdx', + 'cdxml' => 'application/vnd.chemdraw+xml', + 'cdy' => 'application/vnd.cinderella', + 'cer' => 'application/pkix-cert', + 'cfs' => 'application/x-cfs-compressed', + 'cgm' => 'image/cgm', + 'chat' => 'application/x-chat', + 'chm' => 'application/vnd.ms-htmlhelp', + 'chrt' => 'application/vnd.kde.kchart', + 'cif' => 'chemical/x-cif', + 'cii' => 'application/vnd.anser-web-certificate-issue-initiation', + 'cil' => 'application/vnd.ms-artgalry', + 'cjs' => 'application/node', + 'cla' => 'application/vnd.claymore', + 'class' => 'application/octet-stream', + 'cld' => 'model/vnd.cld', + 'clkk' => 'application/vnd.crick.clicker.keyboard', + 'clkp' => 'application/vnd.crick.clicker.palette', + 'clkt' => 'application/vnd.crick.clicker.template', + 'clkw' => 'application/vnd.crick.clicker.wordbank', + 'clkx' => 'application/vnd.crick.clicker', + 'clp' => 'application/x-msclip', + 'cmc' => 'application/vnd.cosmocaller', + 'cmdf' => 'chemical/x-cmdf', + 'cml' => 'chemical/x-cml', + 'cmp' => 'application/vnd.yellowriver-custom-menu', + 'cmx' => 'image/x-cmx', + 'cod' => 'application/vnd.rim.cod', + 'coffee' => 'text/coffeescript', + 'com' => 'application/x-msdownload', + 'conf' => 'text/plain', + 'cpio' => 'application/x-cpio', + 'cpl' => 'application/cpl+xml', + 'cpp' => 'text/x-c', + 'cpt' => 'application/mac-compactpro', + 'crd' => 'application/x-mscardfile', + 'crl' => 'application/pkix-crl', + 'crt' => 'application/x-x509-ca-cert', + 'crx' => 'application/x-chrome-extension', + 'cryptonote' => 'application/vnd.rig.cryptonote', + 'csh' => 'application/x-csh', + 'csl' => 'application/vnd.citationstyles.style+xml', + 'csml' => 'chemical/x-csml', + 'csp' => 'application/vnd.commonspace', + 'csr' => 'application/octet-stream', + 'css' => 'text/css', + 'cst' => 'application/x-director', + 'csv' => 'text/csv', + 'cu' => 'application/cu-seeme', + 'curl' => 'text/vnd.curl', + 'cwl' => 'application/cwl', + 'cww' => 'application/prs.cww', + 'cxt' => 'application/x-director', + 'cxx' => 'text/x-c', + 'dae' => 'model/vnd.collada+xml', + 'daf' => 'application/vnd.mobius.daf', + 'dart' => 'application/vnd.dart', + 'dataless' => 'application/vnd.fdsn.seed', + 'davmount' => 'application/davmount+xml', + 'dbf' => 'application/vnd.dbf', + 'dbk' => 'application/docbook+xml', + 'dcr' => 'application/x-director', + 'dcurl' => 'text/vnd.curl.dcurl', + 'dd2' => 'application/vnd.oma.dd2+xml', + 'ddd' => 'application/vnd.fujixerox.ddd', + 'ddf' => 'application/vnd.syncml.dmddf+xml', + 'dds' => 'image/vnd.ms-dds', + 'deb' => 'application/x-debian-package', + 'def' => 'text/plain', + 'deploy' => 'application/octet-stream', + 'der' => 'application/x-x509-ca-cert', + 'dfac' => 'application/vnd.dreamfactory', + 'dgc' => 'application/x-dgc-compressed', + 'dib' => 'image/bmp', + 'dic' => 'text/x-c', + 'dir' => 'application/x-director', + 'dis' => 'application/vnd.mobius.dis', + 'disposition-notification' => 'message/disposition-notification', + 'dist' => 'application/octet-stream', + 'distz' => 'application/octet-stream', + 'djv' => 'image/vnd.djvu', + 'djvu' => 'image/vnd.djvu', + 'dll' => 'application/octet-stream', + 'dmg' => 'application/x-apple-diskimage', + 'dmn' => 'application/octet-stream', + 'dmp' => 'application/vnd.tcpdump.pcap', + 'dms' => 'application/octet-stream', + 'dna' => 'application/vnd.dna', + 'doc' => 'application/msword', + 'docm' => 'application/vnd.ms-word.template.macroEnabled.12', + 'docx' => 'application/vnd.openxmlformats-officedocument.wordprocessingml.document', + 'dot' => 'application/msword', + 'dotm' => 'application/vnd.ms-word.template.macroEnabled.12', + 'dotx' => 'application/vnd.openxmlformats-officedocument.wordprocessingml.template', + 'dp' => 'application/vnd.osgi.dp', + 'dpg' => 'application/vnd.dpgraph', + 'dpx' => 'image/dpx', + 'dra' => 'audio/vnd.dra', + 'drle' => 'image/dicom-rle', + 'dsc' => 'text/prs.lines.tag', + 'dssc' => 'application/dssc+der', + 'dtb' => 'application/x-dtbook+xml', + 'dtd' => 'application/xml-dtd', + 'dts' => 'audio/vnd.dts', + 'dtshd' => 'audio/vnd.dts.hd', + 'dump' => 'application/octet-stream', + 'dvb' => 'video/vnd.dvb.file', + 'dvi' => 'application/x-dvi', + 'dwd' => 'application/atsc-dwd+xml', + 'dwf' => 'model/vnd.dwf', + 'dwg' => 'image/vnd.dwg', + 'dxf' => 'image/vnd.dxf', + 'dxp' => 'application/vnd.spotfire.dxp', + 'dxr' => 'application/x-director', + 'ear' => 'application/java-archive', + 'ecelp4800' => 'audio/vnd.nuera.ecelp4800', + 'ecelp7470' => 'audio/vnd.nuera.ecelp7470', + 'ecelp9600' => 'audio/vnd.nuera.ecelp9600', + 'ecma' => 'application/ecmascript', + 'edm' => 'application/vnd.novadigm.edm', + 'edx' => 'application/vnd.novadigm.edx', + 'efif' => 'application/vnd.picsel', + 'ei6' => 'application/vnd.pg.osasli', + 'elc' => 'application/octet-stream', + 'emf' => 'image/emf', + 'eml' => 'message/rfc822', + 'emma' => 'application/emma+xml', + 'emotionml' => 'application/emotionml+xml', + 'emz' => 'application/x-msmetafile', + 'eol' => 'audio/vnd.digital-winds', + 'eot' => 'application/vnd.ms-fontobject', + 'eps' => 'application/postscript', + 'epub' => 'application/epub+zip', + 'es3' => 'application/vnd.eszigno3+xml', + 'esa' => 'application/vnd.osgi.subsystem', + 'esf' => 'application/vnd.epson.esf', + 'et3' => 'application/vnd.eszigno3+xml', + 'etx' => 'text/x-setext', + 'eva' => 'application/x-eva', + 'evy' => 'application/x-envoy', + 'exe' => 'application/octet-stream', + 'exi' => 'application/exi', + 'exp' => 'application/express', + 'exr' => 'image/aces', + 'ext' => 'application/vnd.novadigm.ext', + 'ez' => 'application/andrew-inset', + 'ez2' => 'application/vnd.ezpix-album', + 'ez3' => 'application/vnd.ezpix-package', + 'f' => 'text/x-fortran', + 'f4v' => 'video/mp4', + 'f77' => 'text/x-fortran', + 'f90' => 'text/x-fortran', + 'fbs' => 'image/vnd.fastbidsheet', + 'fcdt' => 'application/vnd.adobe.formscentral.fcdt', + 'fcs' => 'application/vnd.isac.fcs', + 'fdf' => 'application/vnd.fdf', + 'fdt' => 'application/fdt+xml', + 'fe_launch' => 'application/vnd.denovo.fcselayout-link', + 'fg5' => 'application/vnd.fujitsu.oasysgp', + 'fgd' => 'application/x-director', + 'fh' => 'image/x-freehand', + 'fh4' => 'image/x-freehand', + 'fh5' => 'image/x-freehand', + 'fh7' => 'image/x-freehand', + 'fhc' => 'image/x-freehand', + 'fig' => 'application/x-xfig', + 'fits' => 'image/fits', + 'flac' => 'audio/x-flac', + 'fli' => 'video/x-fli', + 'flo' => 'application/vnd.micrografx.flo', + 'flv' => 'video/x-flv', + 'flw' => 'application/vnd.kde.kivio', + 'flx' => 'text/vnd.fmi.flexstor', + 'fly' => 'text/vnd.fly', + 'fm' => 'application/vnd.framemaker', + 'fnc' => 'application/vnd.frogans.fnc', + 'fo' => 'application/vnd.software602.filler.form+xml', + 'for' => 'text/x-fortran', + 'fpx' => 'image/vnd.fpx', + 'frame' => 'application/vnd.framemaker', + 'fsc' => 'application/vnd.fsc.weblaunch', + 'fst' => 'image/vnd.fst', + 'ftc' => 'application/vnd.fluxtime.clip', + 'fti' => 'application/vnd.anser-web-funds-transfer-initiation', + 'fvt' => 'video/vnd.fvt', + 'fxp' => 'application/vnd.adobe.fxp', + 'fxpl' => 'application/vnd.adobe.fxp', + 'fzs' => 'application/vnd.fuzzysheet', + 'g2w' => 'application/vnd.geoplan', + 'g3' => 'image/g3fax', + 'g3w' => 'application/vnd.geospace', + 'gac' => 'application/vnd.groove-account', + 'gam' => 'application/x-tads', + 'gbr' => 'application/rpki-ghostbusters', + 'gca' => 'application/x-gca-compressed', + 'gdl' => 'model/vnd.gdl', + 'gdoc' => 'application/vnd.google-apps.document', + 'ged' => 'text/vnd.familysearch.gedcom', + 'geo' => 'application/vnd.dynageo', + 'geojson' => 'application/geo+json', + 'gex' => 'application/vnd.geometry-explorer', + 'ggb' => 'application/vnd.geogebra.file', + 'ggt' => 'application/vnd.geogebra.tool', + 'ghf' => 'application/vnd.groove-help', + 'gif' => 'image/gif', + 'gim' => 'application/vnd.groove-identity-message', + 'glb' => 'model/gltf-binary', + 'gltf' => 'model/gltf+json', + 'gml' => 'application/gml+xml', + 'gmx' => 'application/vnd.gmx', + 'gnumeric' => 'application/x-gnumeric', + 'gpg' => 'application/gpg-keys', + 'gph' => 'application/vnd.flographit', + 'gpx' => 'application/gpx+xml', + 'gqf' => 'application/vnd.grafeq', + 'gqs' => 'application/vnd.grafeq', + 'gram' => 'application/srgs', + 'gramps' => 'application/x-gramps-xml', + 'gre' => 'application/vnd.geometry-explorer', + 'grv' => 'application/vnd.groove-injector', + 'grxml' => 'application/srgs+xml', + 'gsf' => 'application/x-font-ghostscript', + 'gsheet' => 'application/vnd.google-apps.spreadsheet', + 'gslides' => 'application/vnd.google-apps.presentation', + 'gtar' => 'application/x-gtar', + 'gtm' => 'application/vnd.groove-tool-message', + 'gtw' => 'model/vnd.gtw', + 'gv' => 'text/vnd.graphviz', + 'gxf' => 'application/gxf', + 'gxt' => 'application/vnd.geonext', + 'gz' => 'application/gzip', + 'gzip' => 'application/gzip', + 'h' => 'text/x-c', + 'h261' => 'video/h261', + 'h263' => 'video/h263', + 'h264' => 'video/h264', + 'hal' => 'application/vnd.hal+xml', + 'hbci' => 'application/vnd.hbci', + 'hbs' => 'text/x-handlebars-template', + 'hdd' => 'application/x-virtualbox-hdd', + 'hdf' => 'application/x-hdf', + 'heic' => 'image/heic', + 'heics' => 'image/heic-sequence', + 'heif' => 'image/heif', + 'heifs' => 'image/heif-sequence', + 'hej2' => 'image/hej2k', + 'held' => 'application/atsc-held+xml', + 'hh' => 'text/x-c', + 'hjson' => 'application/hjson', + 'hlp' => 'application/winhlp', + 'hpgl' => 'application/vnd.hp-hpgl', + 'hpid' => 'application/vnd.hp-hpid', + 'hps' => 'application/vnd.hp-hps', + 'hqx' => 'application/mac-binhex40', + 'hsj2' => 'image/hsj2', + 'htc' => 'text/x-component', + 'htke' => 'application/vnd.kenameaapp', + 'htm' => 'text/html', + 'html' => 'text/html', + 'hvd' => 'application/vnd.yamaha.hv-dic', + 'hvp' => 'application/vnd.yamaha.hv-voice', + 'hvs' => 'application/vnd.yamaha.hv-script', + 'i2g' => 'application/vnd.intergeo', + 'icc' => 'application/vnd.iccprofile', + 'ice' => 'x-conference/x-cooltalk', + 'icm' => 'application/vnd.iccprofile', + 'ico' => 'image/x-icon', + 'ics' => 'text/calendar', + 'ief' => 'image/ief', + 'ifb' => 'text/calendar', + 'ifm' => 'application/vnd.shana.informed.formdata', + 'iges' => 'model/iges', + 'igl' => 'application/vnd.igloader', + 'igm' => 'application/vnd.insors.igm', + 'igs' => 'model/iges', + 'igx' => 'application/vnd.micrografx.igx', + 'iif' => 'application/vnd.shana.informed.interchange', + 'img' => 'application/octet-stream', + 'imp' => 'application/vnd.accpac.simply.imp', + 'ims' => 'application/vnd.ms-ims', + 'in' => 'text/plain', + 'ini' => 'text/plain', + 'ink' => 'application/inkml+xml', + 'inkml' => 'application/inkml+xml', + 'install' => 'application/x-install-instructions', + 'iota' => 'application/vnd.astraea-software.iota', + 'ipfix' => 'application/ipfix', + 'ipk' => 'application/vnd.shana.informed.package', + 'irm' => 'application/vnd.ibm.rights-management', + 'irp' => 'application/vnd.irepository.package+xml', + 'iso' => 'application/x-iso9660-image', + 'itp' => 'application/vnd.shana.informed.formtemplate', + 'its' => 'application/its+xml', + 'ivp' => 'application/vnd.immervision-ivp', + 'ivu' => 'application/vnd.immervision-ivu', + 'jad' => 'text/vnd.sun.j2me.app-descriptor', + 'jade' => 'text/jade', + 'jam' => 'application/vnd.jam', + 'jar' => 'application/java-archive', + 'jardiff' => 'application/x-java-archive-diff', + 'java' => 'text/x-java-source', + 'jhc' => 'image/jphc', + 'jisp' => 'application/vnd.jisp', + 'jls' => 'image/jls', + 'jlt' => 'application/vnd.hp-jlyt', + 'jng' => 'image/x-jng', + 'jnlp' => 'application/x-java-jnlp-file', + 'joda' => 'application/vnd.joost.joda-archive', + 'jp2' => 'image/jp2', + 'jpe' => 'image/jpeg', + 'jpeg' => 'image/jpeg', + 'jpf' => 'image/jpx', + 'jpg' => 'image/jpeg', + 'jpg2' => 'image/jp2', + 'jpgm' => 'video/jpm', + 'jpgv' => 'video/jpeg', + 'jph' => 'image/jph', + 'jpm' => 'video/jpm', + 'jpx' => 'image/jpx', + 'js' => 'application/javascript', + 'json' => 'application/json', + 'json5' => 'application/json5', + 'jsonld' => 'application/ld+json', + 'jsonml' => 'application/jsonml+json', + 'jsx' => 'text/jsx', + 'jt' => 'model/jt', + 'jxr' => 'image/jxr', + 'jxra' => 'image/jxra', + 'jxrs' => 'image/jxrs', + 'jxs' => 'image/jxs', + 'jxsc' => 'image/jxsc', + 'jxsi' => 'image/jxsi', + 'jxss' => 'image/jxss', + 'kar' => 'audio/midi', + 'karbon' => 'application/vnd.kde.karbon', + 'kdb' => 'application/octet-stream', + 'kdbx' => 'application/x-keepass2', + 'key' => 'application/x-iwork-keynote-sffkey', + 'kfo' => 'application/vnd.kde.kformula', + 'kia' => 'application/vnd.kidspiration', + 'kml' => 'application/vnd.google-earth.kml+xml', + 'kmz' => 'application/vnd.google-earth.kmz', + 'kne' => 'application/vnd.kinar', + 'knp' => 'application/vnd.kinar', + 'kon' => 'application/vnd.kde.kontour', + 'kpr' => 'application/vnd.kde.kpresenter', + 'kpt' => 'application/vnd.kde.kpresenter', + 'kpxx' => 'application/vnd.ds-keypoint', + 'ksp' => 'application/vnd.kde.kspread', + 'ktr' => 'application/vnd.kahootz', + 'ktx' => 'image/ktx', + 'ktx2' => 'image/ktx2', + 'ktz' => 'application/vnd.kahootz', + 'kwd' => 'application/vnd.kde.kword', + 'kwt' => 'application/vnd.kde.kword', + 'lasxml' => 'application/vnd.las.las+xml', + 'latex' => 'application/x-latex', + 'lbd' => 'application/vnd.llamagraphics.life-balance.desktop', + 'lbe' => 'application/vnd.llamagraphics.life-balance.exchange+xml', + 'les' => 'application/vnd.hhe.lesson-player', + 'less' => 'text/less', + 'lgr' => 'application/lgr+xml', + 'lha' => 'application/octet-stream', + 'link66' => 'application/vnd.route66.link66+xml', + 'list' => 'text/plain', + 'list3820' => 'application/vnd.ibm.modcap', + 'listafp' => 'application/vnd.ibm.modcap', + 'litcoffee' => 'text/coffeescript', + 'lnk' => 'application/x-ms-shortcut', + 'log' => 'text/plain', + 'lostxml' => 'application/lost+xml', + 'lrf' => 'application/octet-stream', + 'lrm' => 'application/vnd.ms-lrm', + 'ltf' => 'application/vnd.frogans.ltf', + 'lua' => 'text/x-lua', + 'luac' => 'application/x-lua-bytecode', + 'lvp' => 'audio/vnd.lucent.voice', + 'lwp' => 'application/vnd.lotus-wordpro', + 'lzh' => 'application/octet-stream', + 'm1v' => 'video/mpeg', + 'm2a' => 'audio/mpeg', + 'm2v' => 'video/mpeg', + 'm3a' => 'audio/mpeg', + 'm3u' => 'text/plain', + 'm3u8' => 'application/vnd.apple.mpegurl', + 'm4a' => 'audio/x-m4a', + 'm4p' => 'application/mp4', + 'm4s' => 'video/iso.segment', + 'm4u' => 'application/vnd.mpegurl', + 'm4v' => 'video/x-m4v', + 'm13' => 'application/x-msmediaview', + 'm14' => 'application/x-msmediaview', + 'm21' => 'application/mp21', + 'ma' => 'application/mathematica', + 'mads' => 'application/mads+xml', + 'maei' => 'application/mmt-aei+xml', + 'mag' => 'application/vnd.ecowin.chart', + 'maker' => 'application/vnd.framemaker', + 'man' => 'text/troff', + 'manifest' => 'text/cache-manifest', + 'map' => 'application/json', + 'mar' => 'application/octet-stream', + 'markdown' => 'text/markdown', + 'mathml' => 'application/mathml+xml', + 'mb' => 'application/mathematica', + 'mbk' => 'application/vnd.mobius.mbk', + 'mbox' => 'application/mbox', + 'mc1' => 'application/vnd.medcalcdata', + 'mcd' => 'application/vnd.mcd', + 'mcurl' => 'text/vnd.curl.mcurl', + 'md' => 'text/markdown', + 'mdb' => 'application/x-msaccess', + 'mdi' => 'image/vnd.ms-modi', + 'mdx' => 'text/mdx', + 'me' => 'text/troff', + 'mesh' => 'model/mesh', + 'meta4' => 'application/metalink4+xml', + 'metalink' => 'application/metalink+xml', + 'mets' => 'application/mets+xml', + 'mfm' => 'application/vnd.mfmp', + 'mft' => 'application/rpki-manifest', + 'mgp' => 'application/vnd.osgeo.mapguide.package', + 'mgz' => 'application/vnd.proteus.magazine', + 'mid' => 'audio/midi', + 'midi' => 'audio/midi', + 'mie' => 'application/x-mie', + 'mif' => 'application/vnd.mif', + 'mime' => 'message/rfc822', + 'mj2' => 'video/mj2', + 'mjp2' => 'video/mj2', + 'mjs' => 'text/javascript', + 'mk3d' => 'video/x-matroska', + 'mka' => 'audio/x-matroska', + 'mkd' => 'text/x-markdown', + 'mks' => 'video/x-matroska', + 'mkv' => 'video/x-matroska', + 'mlp' => 'application/vnd.dolby.mlp', + 'mmd' => 'application/vnd.chipnuts.karaoke-mmd', + 'mmf' => 'application/vnd.smaf', + 'mml' => 'text/mathml', + 'mmr' => 'image/vnd.fujixerox.edmics-mmr', + 'mng' => 'video/x-mng', + 'mny' => 'application/x-msmoney', + 'mobi' => 'application/x-mobipocket-ebook', + 'mods' => 'application/mods+xml', + 'mov' => 'video/quicktime', + 'movie' => 'video/x-sgi-movie', + 'mp2' => 'audio/mpeg', + 'mp2a' => 'audio/mpeg', + 'mp3' => 'audio/mpeg', + 'mp4' => 'video/mp4', + 'mp4a' => 'audio/mp4', + 'mp4s' => 'application/mp4', + 'mp4v' => 'video/mp4', + 'mp21' => 'application/mp21', + 'mpc' => 'application/vnd.mophun.certificate', + 'mpd' => 'application/dash+xml', + 'mpe' => 'video/mpeg', + 'mpeg' => 'video/mpeg', + 'mpf' => 'application/media-policy-dataset+xml', + 'mpg' => 'video/mpeg', + 'mpg4' => 'video/mp4', + 'mpga' => 'audio/mpeg', + 'mpkg' => 'application/vnd.apple.installer+xml', + 'mpm' => 'application/vnd.blueice.multipass', + 'mpn' => 'application/vnd.mophun.application', + 'mpp' => 'application/vnd.ms-project', + 'mpt' => 'application/vnd.ms-project', + 'mpy' => 'application/vnd.ibm.minipay', + 'mqy' => 'application/vnd.mobius.mqy', + 'mrc' => 'application/marc', + 'mrcx' => 'application/marcxml+xml', + 'ms' => 'text/troff', + 'mscml' => 'application/mediaservercontrol+xml', + 'mseed' => 'application/vnd.fdsn.mseed', + 'mseq' => 'application/vnd.mseq', + 'msf' => 'application/vnd.epson.msf', + 'msg' => 'application/vnd.ms-outlook', + 'msh' => 'model/mesh', + 'msi' => 'application/x-msdownload', + 'msix' => 'application/msix', + 'msixbundle' => 'application/msixbundle', + 'msl' => 'application/vnd.mobius.msl', + 'msm' => 'application/octet-stream', + 'msp' => 'application/octet-stream', + 'msty' => 'application/vnd.muvee.style', + 'mtl' => 'model/mtl', + 'mts' => 'model/vnd.mts', + 'mus' => 'application/vnd.musician', + 'musd' => 'application/mmt-usd+xml', + 'musicxml' => 'application/vnd.recordare.musicxml+xml', + 'mvb' => 'application/x-msmediaview', + 'mvt' => 'application/vnd.mapbox-vector-tile', + 'mwf' => 'application/vnd.mfer', + 'mxf' => 'application/mxf', + 'mxl' => 'application/vnd.recordare.musicxml', + 'mxmf' => 'audio/mobile-xmf', + 'mxml' => 'application/xv+xml', + 'mxs' => 'application/vnd.triscape.mxs', + 'mxu' => 'video/vnd.mpegurl', + 'n-gage' => 'application/vnd.nokia.n-gage.symbian.install', + 'n3' => 'text/n3', + 'nb' => 'application/mathematica', + 'nbp' => 'application/vnd.wolfram.player', + 'nc' => 'application/x-netcdf', + 'ncx' => 'application/x-dtbncx+xml', + 'nfo' => 'text/x-nfo', + 'ngdat' => 'application/vnd.nokia.n-gage.data', + 'nitf' => 'application/vnd.nitf', + 'nlu' => 'application/vnd.neurolanguage.nlu', + 'nml' => 'application/vnd.enliven', + 'nnd' => 'application/vnd.noblenet-directory', + 'nns' => 'application/vnd.noblenet-sealer', + 'nnw' => 'application/vnd.noblenet-web', + 'npx' => 'image/vnd.net-fpx', + 'nq' => 'application/n-quads', + 'nsc' => 'application/x-conference', + 'nsf' => 'application/vnd.lotus-notes', + 'nt' => 'application/n-triples', + 'ntf' => 'application/vnd.nitf', + 'numbers' => 'application/x-iwork-numbers-sffnumbers', + 'nzb' => 'application/x-nzb', + 'oa2' => 'application/vnd.fujitsu.oasys2', + 'oa3' => 'application/vnd.fujitsu.oasys3', + 'oas' => 'application/vnd.fujitsu.oasys', + 'obd' => 'application/x-msbinder', + 'obgx' => 'application/vnd.openblox.game+xml', + 'obj' => 'model/obj', + 'oda' => 'application/oda', + 'odb' => 'application/vnd.oasis.opendocument.database', + 'odc' => 'application/vnd.oasis.opendocument.chart', + 'odf' => 'application/vnd.oasis.opendocument.formula', + 'odft' => 'application/vnd.oasis.opendocument.formula-template', + 'odg' => 'application/vnd.oasis.opendocument.graphics', + 'odi' => 'application/vnd.oasis.opendocument.image', + 'odm' => 'application/vnd.oasis.opendocument.text-master', + 'odp' => 'application/vnd.oasis.opendocument.presentation', + 'ods' => 'application/vnd.oasis.opendocument.spreadsheet', + 'odt' => 'application/vnd.oasis.opendocument.text', + 'oga' => 'audio/ogg', + 'ogex' => 'model/vnd.opengex', + 'ogg' => 'audio/ogg', + 'ogv' => 'video/ogg', + 'ogx' => 'application/ogg', + 'omdoc' => 'application/omdoc+xml', + 'onepkg' => 'application/onenote', + 'onetmp' => 'application/onenote', + 'onetoc' => 'application/onenote', + 'onetoc2' => 'application/onenote', + 'opf' => 'application/oebps-package+xml', + 'opml' => 'text/x-opml', + 'oprc' => 'application/vnd.palm', + 'opus' => 'audio/ogg', + 'org' => 'text/x-org', + 'osf' => 'application/vnd.yamaha.openscoreformat', + 'osfpvg' => 'application/vnd.yamaha.openscoreformat.osfpvg+xml', + 'osm' => 'application/vnd.openstreetmap.data+xml', + 'otc' => 'application/vnd.oasis.opendocument.chart-template', + 'otf' => 'font/otf', + 'otg' => 'application/vnd.oasis.opendocument.graphics-template', + 'oth' => 'application/vnd.oasis.opendocument.text-web', + 'oti' => 'application/vnd.oasis.opendocument.image-template', + 'otp' => 'application/vnd.oasis.opendocument.presentation-template', + 'ots' => 'application/vnd.oasis.opendocument.spreadsheet-template', + 'ott' => 'application/vnd.oasis.opendocument.text-template', + 'ova' => 'application/x-virtualbox-ova', + 'ovf' => 'application/x-virtualbox-ovf', + 'owl' => 'application/rdf+xml', + 'oxps' => 'application/oxps', + 'oxt' => 'application/vnd.openofficeorg.extension', + 'p' => 'text/x-pascal', + 'p7a' => 'application/x-pkcs7-signature', + 'p7b' => 'application/x-pkcs7-certificates', + 'p7c' => 'application/pkcs7-mime', + 'p7m' => 'application/pkcs7-mime', + 'p7r' => 'application/x-pkcs7-certreqresp', + 'p7s' => 'application/pkcs7-signature', + 'p8' => 'application/pkcs8', + 'p10' => 'application/x-pkcs10', + 'p12' => 'application/x-pkcs12', + 'pac' => 'application/x-ns-proxy-autoconfig', + 'pages' => 'application/x-iwork-pages-sffpages', + 'pas' => 'text/x-pascal', + 'paw' => 'application/vnd.pawaafile', + 'pbd' => 'application/vnd.powerbuilder6', + 'pbm' => 'image/x-portable-bitmap', + 'pcap' => 'application/vnd.tcpdump.pcap', + 'pcf' => 'application/x-font-pcf', + 'pcl' => 'application/vnd.hp-pcl', + 'pclxl' => 'application/vnd.hp-pclxl', + 'pct' => 'image/x-pict', + 'pcurl' => 'application/vnd.curl.pcurl', + 'pcx' => 'image/x-pcx', + 'pdb' => 'application/x-pilot', + 'pde' => 'text/x-processing', + 'pdf' => 'application/pdf', + 'pem' => 'application/x-x509-user-cert', + 'pfa' => 'application/x-font-type1', + 'pfb' => 'application/x-font-type1', + 'pfm' => 'application/x-font-type1', + 'pfr' => 'application/font-tdpfr', + 'pfx' => 'application/x-pkcs12', + 'pgm' => 'image/x-portable-graymap', + 'pgn' => 'application/x-chess-pgn', + 'pgp' => 'application/pgp', + 'phar' => 'application/octet-stream', + 'php' => 'application/x-httpd-php', + 'php3' => 'application/x-httpd-php', + 'php4' => 'application/x-httpd-php', + 'phps' => 'application/x-httpd-php-source', + 'phtml' => 'application/x-httpd-php', + 'pic' => 'image/x-pict', + 'pkg' => 'application/octet-stream', + 'pki' => 'application/pkixcmp', + 'pkipath' => 'application/pkix-pkipath', + 'pkpass' => 'application/vnd.apple.pkpass', + 'pl' => 'application/x-perl', + 'plb' => 'application/vnd.3gpp.pic-bw-large', + 'plc' => 'application/vnd.mobius.plc', + 'plf' => 'application/vnd.pocketlearn', + 'pls' => 'application/pls+xml', + 'pm' => 'application/x-perl', + 'pml' => 'application/vnd.ctc-posml', + 'png' => 'image/png', + 'pnm' => 'image/x-portable-anymap', + 'portpkg' => 'application/vnd.macports.portpkg', + 'pot' => 'application/vnd.ms-powerpoint', + 'potm' => 'application/vnd.ms-powerpoint.presentation.macroEnabled.12', + 'potx' => 'application/vnd.openxmlformats-officedocument.presentationml.template', + 'ppa' => 'application/vnd.ms-powerpoint', + 'ppam' => 'application/vnd.ms-powerpoint.addin.macroEnabled.12', + 'ppd' => 'application/vnd.cups-ppd', + 'ppm' => 'image/x-portable-pixmap', + 'pps' => 'application/vnd.ms-powerpoint', + 'ppsm' => 'application/vnd.ms-powerpoint.slideshow.macroEnabled.12', + 'ppsx' => 'application/vnd.openxmlformats-officedocument.presentationml.slideshow', + 'ppt' => 'application/powerpoint', + 'pptm' => 'application/vnd.ms-powerpoint.presentation.macroEnabled.12', + 'pptx' => 'application/vnd.openxmlformats-officedocument.presentationml.presentation', + 'pqa' => 'application/vnd.palm', + 'prc' => 'model/prc', + 'pre' => 'application/vnd.lotus-freelance', + 'prf' => 'application/pics-rules', + 'provx' => 'application/provenance+xml', + 'ps' => 'application/postscript', + 'psb' => 'application/vnd.3gpp.pic-bw-small', + 'psd' => 'application/x-photoshop', + 'psf' => 'application/x-font-linux-psf', + 'pskcxml' => 'application/pskc+xml', + 'pti' => 'image/prs.pti', + 'ptid' => 'application/vnd.pvi.ptid1', + 'pub' => 'application/x-mspublisher', + 'pvb' => 'application/vnd.3gpp.pic-bw-var', + 'pwn' => 'application/vnd.3m.post-it-notes', + 'pya' => 'audio/vnd.ms-playready.media.pya', + 'pyo' => 'model/vnd.pytha.pyox', + 'pyox' => 'model/vnd.pytha.pyox', + 'pyv' => 'video/vnd.ms-playready.media.pyv', + 'qam' => 'application/vnd.epson.quickanime', + 'qbo' => 'application/vnd.intu.qbo', + 'qfx' => 'application/vnd.intu.qfx', + 'qps' => 'application/vnd.publishare-delta-tree', + 'qt' => 'video/quicktime', + 'qwd' => 'application/vnd.quark.quarkxpress', + 'qwt' => 'application/vnd.quark.quarkxpress', + 'qxb' => 'application/vnd.quark.quarkxpress', + 'qxd' => 'application/vnd.quark.quarkxpress', + 'qxl' => 'application/vnd.quark.quarkxpress', + 'qxt' => 'application/vnd.quark.quarkxpress', + 'ra' => 'audio/x-realaudio', + 'ram' => 'audio/x-pn-realaudio', + 'raml' => 'application/raml+yaml', + 'rapd' => 'application/route-apd+xml', + 'rar' => 'application/x-rar', + 'ras' => 'image/x-cmu-raster', + 'rcprofile' => 'application/vnd.ipunplugged.rcprofile', + 'rdf' => 'application/rdf+xml', + 'rdz' => 'application/vnd.data-vision.rdz', + 'relo' => 'application/p2p-overlay+xml', + 'rep' => 'application/vnd.businessobjects', + 'res' => 'application/x-dtbresource+xml', + 'rgb' => 'image/x-rgb', + 'rif' => 'application/reginfo+xml', + 'rip' => 'audio/vnd.rip', + 'ris' => 'application/x-research-info-systems', + 'rl' => 'application/resource-lists+xml', + 'rlc' => 'image/vnd.fujixerox.edmics-rlc', + 'rld' => 'application/resource-lists-diff+xml', + 'rm' => 'audio/x-pn-realaudio', + 'rmi' => 'audio/midi', + 'rmp' => 'audio/x-pn-realaudio-plugin', + 'rms' => 'application/vnd.jcp.javame.midlet-rms', + 'rmvb' => 'application/vnd.rn-realmedia-vbr', + 'rnc' => 'application/relax-ng-compact-syntax', + 'rng' => 'application/xml', + 'roa' => 'application/rpki-roa', + 'roff' => 'text/troff', + 'rp9' => 'application/vnd.cloanto.rp9', + 'rpm' => 'audio/x-pn-realaudio-plugin', + 'rpss' => 'application/vnd.nokia.radio-presets', + 'rpst' => 'application/vnd.nokia.radio-preset', + 'rq' => 'application/sparql-query', + 'rs' => 'application/rls-services+xml', + 'rsa' => 'application/x-pkcs7', + 'rsat' => 'application/atsc-rsat+xml', + 'rsd' => 'application/rsd+xml', + 'rsheet' => 'application/urc-ressheet+xml', + 'rss' => 'application/rss+xml', + 'rtf' => 'text/rtf', + 'rtx' => 'text/richtext', + 'run' => 'application/x-makeself', + 'rusd' => 'application/route-usd+xml', + 'rv' => 'video/vnd.rn-realvideo', + 's' => 'text/x-asm', + 's3m' => 'audio/s3m', + 'saf' => 'application/vnd.yamaha.smaf-audio', + 'sass' => 'text/x-sass', + 'sbml' => 'application/sbml+xml', + 'sc' => 'application/vnd.ibm.secure-container', + 'scd' => 'application/x-msschedule', + 'scm' => 'application/vnd.lotus-screencam', + 'scq' => 'application/scvp-cv-request', + 'scs' => 'application/scvp-cv-response', + 'scss' => 'text/x-scss', + 'scurl' => 'text/vnd.curl.scurl', + 'sda' => 'application/vnd.stardivision.draw', + 'sdc' => 'application/vnd.stardivision.calc', + 'sdd' => 'application/vnd.stardivision.impress', + 'sdkd' => 'application/vnd.solent.sdkm+xml', + 'sdkm' => 'application/vnd.solent.sdkm+xml', + 'sdp' => 'application/sdp', + 'sdw' => 'application/vnd.stardivision.writer', + 'sea' => 'application/octet-stream', + 'see' => 'application/vnd.seemail', + 'seed' => 'application/vnd.fdsn.seed', + 'sema' => 'application/vnd.sema', + 'semd' => 'application/vnd.semd', + 'semf' => 'application/vnd.semf', + 'senmlx' => 'application/senml+xml', + 'sensmlx' => 'application/sensml+xml', + 'ser' => 'application/java-serialized-object', + 'setpay' => 'application/set-payment-initiation', + 'setreg' => 'application/set-registration-initiation', + 'sfd-hdstx' => 'application/vnd.hydrostatix.sof-data', + 'sfs' => 'application/vnd.spotfire.sfs', + 'sfv' => 'text/x-sfv', + 'sgi' => 'image/sgi', + 'sgl' => 'application/vnd.stardivision.writer-global', + 'sgm' => 'text/sgml', + 'sgml' => 'text/sgml', + 'sh' => 'application/x-sh', + 'shar' => 'application/x-shar', + 'shex' => 'text/shex', + 'shf' => 'application/shf+xml', + 'shtml' => 'text/html', + 'sid' => 'image/x-mrsid-image', + 'sieve' => 'application/sieve', + 'sig' => 'application/pgp-signature', + 'sil' => 'audio/silk', + 'silo' => 'model/mesh', + 'sis' => 'application/vnd.symbian.install', + 'sisx' => 'application/vnd.symbian.install', + 'sit' => 'application/x-stuffit', + 'sitx' => 'application/x-stuffitx', + 'siv' => 'application/sieve', + 'skd' => 'application/vnd.koan', + 'skm' => 'application/vnd.koan', + 'skp' => 'application/vnd.koan', + 'skt' => 'application/vnd.koan', + 'sldm' => 'application/vnd.ms-powerpoint.slide.macroenabled.12', + 'sldx' => 'application/vnd.openxmlformats-officedocument.presentationml.slide', + 'slim' => 'text/slim', + 'slm' => 'text/slim', + 'sls' => 'application/route-s-tsid+xml', + 'slt' => 'application/vnd.epson.salt', + 'sm' => 'application/vnd.stepmania.stepchart', + 'smf' => 'application/vnd.stardivision.math', + 'smi' => 'application/smil', + 'smil' => 'application/smil', + 'smv' => 'video/x-smv', + 'smzip' => 'application/vnd.stepmania.package', + 'snd' => 'audio/basic', + 'snf' => 'application/x-font-snf', + 'so' => 'application/octet-stream', + 'spc' => 'application/x-pkcs7-certificates', + 'spdx' => 'text/spdx', + 'spf' => 'application/vnd.yamaha.smaf-phrase', + 'spl' => 'application/x-futuresplash', + 'spot' => 'text/vnd.in3d.spot', + 'spp' => 'application/scvp-vp-response', + 'spq' => 'application/scvp-vp-request', + 'spx' => 'audio/ogg', + 'sql' => 'application/x-sql', + 'src' => 'application/x-wais-source', + 'srt' => 'application/x-subrip', + 'sru' => 'application/sru+xml', + 'srx' => 'application/sparql-results+xml', + 'ssdl' => 'application/ssdl+xml', + 'sse' => 'application/vnd.kodak-descriptor', + 'ssf' => 'application/vnd.epson.ssf', + 'ssml' => 'application/ssml+xml', + 'sst' => 'application/octet-stream', + 'st' => 'application/vnd.sailingtracker.track', + 'stc' => 'application/vnd.sun.xml.calc.template', + 'std' => 'application/vnd.sun.xml.draw.template', + 'step' => 'application/STEP', + 'stf' => 'application/vnd.wt.stf', + 'sti' => 'application/vnd.sun.xml.impress.template', + 'stk' => 'application/hyperstudio', + 'stl' => 'model/stl', + 'stp' => 'application/STEP', + 'stpx' => 'model/step+xml', + 'stpxz' => 'model/step-xml+zip', + 'stpz' => 'model/step+zip', + 'str' => 'application/vnd.pg.format', + 'stw' => 'application/vnd.sun.xml.writer.template', + 'styl' => 'text/stylus', + 'stylus' => 'text/stylus', + 'sub' => 'text/vnd.dvb.subtitle', + 'sus' => 'application/vnd.sus-calendar', + 'susp' => 'application/vnd.sus-calendar', + 'sv4cpio' => 'application/x-sv4cpio', + 'sv4crc' => 'application/x-sv4crc', + 'svc' => 'application/vnd.dvb.service', + 'svd' => 'application/vnd.svd', + 'svg' => 'image/svg+xml', + 'svgz' => 'image/svg+xml', + 'swa' => 'application/x-director', + 'swf' => 'application/x-shockwave-flash', + 'swi' => 'application/vnd.aristanetworks.swi', + 'swidtag' => 'application/swid+xml', + 'sxc' => 'application/vnd.sun.xml.calc', + 'sxd' => 'application/vnd.sun.xml.draw', + 'sxg' => 'application/vnd.sun.xml.writer.global', + 'sxi' => 'application/vnd.sun.xml.impress', + 'sxm' => 'application/vnd.sun.xml.math', + 'sxw' => 'application/vnd.sun.xml.writer', + 't' => 'text/troff', + 't3' => 'application/x-t3vm-image', + 't38' => 'image/t38', + 'taglet' => 'application/vnd.mynfc', + 'tao' => 'application/vnd.tao.intent-module-archive', + 'tap' => 'image/vnd.tencent.tap', + 'tar' => 'application/x-tar', + 'tcap' => 'application/vnd.3gpp2.tcap', + 'tcl' => 'application/x-tcl', + 'td' => 'application/urc-targetdesc+xml', + 'teacher' => 'application/vnd.smart.teacher', + 'tei' => 'application/tei+xml', + 'teicorpus' => 'application/tei+xml', + 'tex' => 'application/x-tex', + 'texi' => 'application/x-texinfo', + 'texinfo' => 'application/x-texinfo', + 'text' => 'text/plain', + 'tfi' => 'application/thraud+xml', + 'tfm' => 'application/x-tex-tfm', + 'tfx' => 'image/tiff-fx', + 'tga' => 'image/x-tga', + 'tgz' => 'application/x-tar', + 'thmx' => 'application/vnd.ms-officetheme', + 'tif' => 'image/tiff', + 'tiff' => 'image/tiff', + 'tk' => 'application/x-tcl', + 'tmo' => 'application/vnd.tmobile-livetv', + 'toml' => 'application/toml', + 'torrent' => 'application/x-bittorrent', + 'tpl' => 'application/vnd.groove-tool-template', + 'tpt' => 'application/vnd.trid.tpt', + 'tr' => 'text/troff', + 'tra' => 'application/vnd.trueapp', + 'trig' => 'application/trig', + 'trm' => 'application/x-msterminal', + 'ts' => 'video/mp2t', + 'tsd' => 'application/timestamped-data', + 'tsv' => 'text/tab-separated-values', + 'ttc' => 'font/collection', + 'ttf' => 'font/ttf', + 'ttl' => 'text/turtle', + 'ttml' => 'application/ttml+xml', + 'twd' => 'application/vnd.simtech-mindmapper', + 'twds' => 'application/vnd.simtech-mindmapper', + 'txd' => 'application/vnd.genomatix.tuxedo', + 'txf' => 'application/vnd.mobius.txf', + 'txt' => 'text/plain', + 'u3d' => 'model/u3d', + 'u8dsn' => 'message/global-delivery-status', + 'u8hdr' => 'message/global-headers', + 'u8mdn' => 'message/global-disposition-notification', + 'u8msg' => 'message/global', + 'u32' => 'application/x-authorware-bin', + 'ubj' => 'application/ubjson', + 'udeb' => 'application/x-debian-package', + 'ufd' => 'application/vnd.ufdl', + 'ufdl' => 'application/vnd.ufdl', + 'ulx' => 'application/x-glulx', + 'umj' => 'application/vnd.umajin', + 'unityweb' => 'application/vnd.unity', + 'uo' => 'application/vnd.uoml+xml', + 'uoml' => 'application/vnd.uoml+xml', + 'uri' => 'text/uri-list', + 'uris' => 'text/uri-list', + 'urls' => 'text/uri-list', + 'usda' => 'model/vnd.usda', + 'usdz' => 'model/vnd.usdz+zip', + 'ustar' => 'application/x-ustar', + 'utz' => 'application/vnd.uiq.theme', + 'uu' => 'text/x-uuencode', + 'uva' => 'audio/vnd.dece.audio', + 'uvd' => 'application/vnd.dece.data', + 'uvf' => 'application/vnd.dece.data', + 'uvg' => 'image/vnd.dece.graphic', + 'uvh' => 'video/vnd.dece.hd', + 'uvi' => 'image/vnd.dece.graphic', + 'uvm' => 'video/vnd.dece.mobile', + 'uvp' => 'video/vnd.dece.pd', + 'uvs' => 'video/vnd.dece.sd', + 'uvt' => 'application/vnd.dece.ttml+xml', + 'uvu' => 'video/vnd.uvvu.mp4', + 'uvv' => 'video/vnd.dece.video', + 'uvva' => 'audio/vnd.dece.audio', + 'uvvd' => 'application/vnd.dece.data', + 'uvvf' => 'application/vnd.dece.data', + 'uvvg' => 'image/vnd.dece.graphic', + 'uvvh' => 'video/vnd.dece.hd', + 'uvvi' => 'image/vnd.dece.graphic', + 'uvvm' => 'video/vnd.dece.mobile', + 'uvvp' => 'video/vnd.dece.pd', + 'uvvs' => 'video/vnd.dece.sd', + 'uvvt' => 'application/vnd.dece.ttml+xml', + 'uvvu' => 'video/vnd.uvvu.mp4', + 'uvvv' => 'video/vnd.dece.video', + 'uvvx' => 'application/vnd.dece.unspecified', + 'uvvz' => 'application/vnd.dece.zip', + 'uvx' => 'application/vnd.dece.unspecified', + 'uvz' => 'application/vnd.dece.zip', + 'vbox' => 'application/x-virtualbox-vbox', + 'vbox-extpack' => 'application/x-virtualbox-vbox-extpack', + 'vcard' => 'text/vcard', + 'vcd' => 'application/x-cdlink', + 'vcf' => 'text/x-vcard', + 'vcg' => 'application/vnd.groove-vcard', + 'vcs' => 'text/x-vcalendar', + 'vcx' => 'application/vnd.vcx', + 'vdi' => 'application/x-virtualbox-vdi', + 'vds' => 'model/vnd.sap.vds', + 'vhd' => 'application/x-virtualbox-vhd', + 'vis' => 'application/vnd.visionary', + 'viv' => 'video/vnd.vivo', + 'vlc' => 'application/videolan', + 'vmdk' => 'application/x-virtualbox-vmdk', + 'vob' => 'video/x-ms-vob', + 'vor' => 'application/vnd.stardivision.writer', + 'vox' => 'application/x-authorware-bin', + 'vrml' => 'model/vrml', + 'vsd' => 'application/vnd.visio', + 'vsf' => 'application/vnd.vsf', + 'vss' => 'application/vnd.visio', + 'vst' => 'application/vnd.visio', + 'vsw' => 'application/vnd.visio', + 'vtf' => 'image/vnd.valve.source.texture', + 'vtt' => 'text/vtt', + 'vtu' => 'model/vnd.vtu', + 'vxml' => 'application/voicexml+xml', + 'w3d' => 'application/x-director', + 'wad' => 'application/x-doom', + 'wadl' => 'application/vnd.sun.wadl+xml', + 'war' => 'application/java-archive', + 'wasm' => 'application/wasm', + 'wav' => 'audio/x-wav', + 'wax' => 'audio/x-ms-wax', + 'wbmp' => 'image/vnd.wap.wbmp', + 'wbs' => 'application/vnd.criticaltools.wbs+xml', + 'wbxml' => 'application/wbxml', + 'wcm' => 'application/vnd.ms-works', + 'wdb' => 'application/vnd.ms-works', + 'wdp' => 'image/vnd.ms-photo', + 'weba' => 'audio/webm', + 'webapp' => 'application/x-web-app-manifest+json', + 'webm' => 'video/webm', + 'webmanifest' => 'application/manifest+json', + 'webp' => 'image/webp', + 'wg' => 'application/vnd.pmi.widget', + 'wgsl' => 'text/wgsl', + 'wgt' => 'application/widget', + 'wif' => 'application/watcherinfo+xml', + 'wks' => 'application/vnd.ms-works', + 'wm' => 'video/x-ms-wm', + 'wma' => 'audio/x-ms-wma', + 'wmd' => 'application/x-ms-wmd', + 'wmf' => 'image/wmf', + 'wml' => 'text/vnd.wap.wml', + 'wmlc' => 'application/wmlc', + 'wmls' => 'text/vnd.wap.wmlscript', + 'wmlsc' => 'application/vnd.wap.wmlscriptc', + 'wmv' => 'video/x-ms-wmv', + 'wmx' => 'video/x-ms-wmx', + 'wmz' => 'application/x-msmetafile', + 'woff' => 'font/woff', + 'woff2' => 'font/woff2', + 'word' => 'application/msword', + 'wpd' => 'application/vnd.wordperfect', + 'wpl' => 'application/vnd.ms-wpl', + 'wps' => 'application/vnd.ms-works', + 'wqd' => 'application/vnd.wqd', + 'wri' => 'application/x-mswrite', + 'wrl' => 'model/vrml', + 'wsc' => 'message/vnd.wfa.wsc', + 'wsdl' => 'application/wsdl+xml', + 'wspolicy' => 'application/wspolicy+xml', + 'wtb' => 'application/vnd.webturbo', + 'wvx' => 'video/x-ms-wvx', + 'x3d' => 'model/x3d+xml', + 'x3db' => 'model/x3d+fastinfoset', + 'x3dbz' => 'model/x3d+binary', + 'x3dv' => 'model/x3d-vrml', + 'x3dvz' => 'model/x3d+vrml', + 'x3dz' => 'model/x3d+xml', + 'x32' => 'application/x-authorware-bin', + 'x_b' => 'model/vnd.parasolid.transmit.binary', + 'x_t' => 'model/vnd.parasolid.transmit.text', + 'xaml' => 'application/xaml+xml', + 'xap' => 'application/x-silverlight-app', + 'xar' => 'application/vnd.xara', + 'xav' => 'application/xcap-att+xml', + 'xbap' => 'application/x-ms-xbap', + 'xbd' => 'application/vnd.fujixerox.docuworks.binder', + 'xbm' => 'image/x-xbitmap', + 'xca' => 'application/xcap-caps+xml', + 'xcs' => 'application/calendar+xml', + 'xdf' => 'application/xcap-diff+xml', + 'xdm' => 'application/vnd.syncml.dm+xml', + 'xdp' => 'application/vnd.adobe.xdp+xml', + 'xdssc' => 'application/dssc+xml', + 'xdw' => 'application/vnd.fujixerox.docuworks', + 'xel' => 'application/xcap-el+xml', + 'xenc' => 'application/xenc+xml', + 'xer' => 'application/patch-ops-error+xml', + 'xfdf' => 'application/xfdf', + 'xfdl' => 'application/vnd.xfdl', + 'xht' => 'application/xhtml+xml', + 'xhtm' => 'application/vnd.pwg-xhtml-print+xml', + 'xhtml' => 'application/xhtml+xml', + 'xhvml' => 'application/xv+xml', + 'xif' => 'image/vnd.xiff', + 'xl' => 'application/excel', + 'xla' => 'application/vnd.ms-excel', + 'xlam' => 'application/vnd.ms-excel.addin.macroEnabled.12', + 'xlc' => 'application/vnd.ms-excel', + 'xlf' => 'application/xliff+xml', + 'xlm' => 'application/vnd.ms-excel', + 'xls' => 'application/vnd.ms-excel', + 'xlsb' => 'application/vnd.ms-excel.sheet.binary.macroEnabled.12', + 'xlsm' => 'application/vnd.ms-excel.sheet.macroEnabled.12', + 'xlsx' => 'application/vnd.openxmlformats-officedocument.spreadsheetml.sheet', + 'xlt' => 'application/vnd.ms-excel', + 'xltm' => 'application/vnd.ms-excel.template.macroEnabled.12', + 'xltx' => 'application/vnd.openxmlformats-officedocument.spreadsheetml.template', + 'xlw' => 'application/vnd.ms-excel', + 'xm' => 'audio/xm', + 'xml' => 'application/xml', + 'xns' => 'application/xcap-ns+xml', + 'xo' => 'application/vnd.olpc-sugar', + 'xop' => 'application/xop+xml', + 'xpi' => 'application/x-xpinstall', + 'xpl' => 'application/xproc+xml', + 'xpm' => 'image/x-xpixmap', + 'xpr' => 'application/vnd.is-xpr', + 'xps' => 'application/vnd.ms-xpsdocument', + 'xpw' => 'application/vnd.intercon.formnet', + 'xpx' => 'application/vnd.intercon.formnet', + 'xsd' => 'application/xml', + 'xsf' => 'application/prs.xsf+xml', + 'xsl' => 'application/xml', + 'xslt' => 'application/xslt+xml', + 'xsm' => 'application/vnd.syncml+xml', + 'xspf' => 'application/xspf+xml', + 'xul' => 'application/vnd.mozilla.xul+xml', + 'xvm' => 'application/xv+xml', + 'xvml' => 'application/xv+xml', + 'xwd' => 'image/x-xwindowdump', + 'xyz' => 'chemical/x-xyz', + 'xz' => 'application/x-xz', + 'yaml' => 'text/yaml', + 'yang' => 'application/yang', + 'yin' => 'application/yin+xml', + 'yml' => 'text/yaml', + 'ymp' => 'text/x-suse-ymp', + 'z' => 'application/x-compress', + 'z1' => 'application/x-zmachine', + 'z2' => 'application/x-zmachine', + 'z3' => 'application/x-zmachine', + 'z4' => 'application/x-zmachine', + 'z5' => 'application/x-zmachine', + 'z6' => 'application/x-zmachine', + 'z7' => 'application/x-zmachine', + 'z8' => 'application/x-zmachine', + 'zaz' => 'application/vnd.zzazz.deck+xml', + 'zip' => 'application/zip', + 'zir' => 'application/vnd.zul', + 'zirz' => 'application/vnd.zul', + 'zmm' => 'application/vnd.handheld-entertainment+xml', + 'zsh' => 'text/x-scriptzsh', + ]; + + /** + * Determines the mimetype of a file by looking at its extension. + * + * @see https://raw.githubusercontent.com/jshttp/mime-db/master/db.json + */ + public static function fromFilename(string $filename): ?string + { + return self::fromExtension(pathinfo($filename, PATHINFO_EXTENSION)); + } + + /** + * Maps a file extensions to a mimetype. + * + * @see https://raw.githubusercontent.com/jshttp/mime-db/master/db.json + */ + public static function fromExtension(string $extension): ?string + { + return self::MIME_TYPES[strtolower($extension)] ?? null; + } +} diff --git a/3rdparty/guzzlehttp/psr7/src/MultipartStream.php b/3rdparty/guzzlehttp/psr7/src/MultipartStream.php new file mode 100644 index 00000000..43d718f6 --- /dev/null +++ b/3rdparty/guzzlehttp/psr7/src/MultipartStream.php @@ -0,0 +1,165 @@ +boundary = $boundary ?: bin2hex(random_bytes(20)); + $this->stream = $this->createStream($elements); + } + + public function getBoundary(): string + { + return $this->boundary; + } + + public function isWritable(): bool + { + return false; + } + + /** + * Get the headers needed before transferring the content of a POST file + * + * @param string[] $headers + */ + private function getHeaders(array $headers): string + { + $str = ''; + foreach ($headers as $key => $value) { + $str .= "{$key}: {$value}\r\n"; + } + + return "--{$this->boundary}\r\n".trim($str)."\r\n\r\n"; + } + + /** + * Create the aggregate stream that will be used to upload the POST data + */ + protected function createStream(array $elements = []): StreamInterface + { + $stream = new AppendStream(); + + foreach ($elements as $element) { + if (!is_array($element)) { + throw new \UnexpectedValueException('An array is expected'); + } + $this->addElement($stream, $element); + } + + // Add the trailing boundary with CRLF + $stream->addStream(Utils::streamFor("--{$this->boundary}--\r\n")); + + return $stream; + } + + private function addElement(AppendStream $stream, array $element): void + { + foreach (['contents', 'name'] as $key) { + if (!array_key_exists($key, $element)) { + throw new \InvalidArgumentException("A '{$key}' key is required"); + } + } + + $element['contents'] = Utils::streamFor($element['contents']); + + if (empty($element['filename'])) { + $uri = $element['contents']->getMetadata('uri'); + if ($uri && \is_string($uri) && \substr($uri, 0, 6) !== 'php://' && \substr($uri, 0, 7) !== 'data://') { + $element['filename'] = $uri; + } + } + + [$body, $headers] = $this->createElement( + $element['name'], + $element['contents'], + $element['filename'] ?? null, + $element['headers'] ?? [] + ); + + $stream->addStream(Utils::streamFor($this->getHeaders($headers))); + $stream->addStream($body); + $stream->addStream(Utils::streamFor("\r\n")); + } + + /** + * @param string[] $headers + * + * @return array{0: StreamInterface, 1: string[]} + */ + private function createElement(string $name, StreamInterface $stream, ?string $filename, array $headers): array + { + // Set a default content-disposition header if one was no provided + $disposition = self::getHeader($headers, 'content-disposition'); + if (!$disposition) { + $headers['Content-Disposition'] = ($filename === '0' || $filename) + ? sprintf( + 'form-data; name="%s"; filename="%s"', + $name, + basename($filename) + ) + : "form-data; name=\"{$name}\""; + } + + // Set a default content-length header if one was no provided + $length = self::getHeader($headers, 'content-length'); + if (!$length) { + if ($length = $stream->getSize()) { + $headers['Content-Length'] = (string) $length; + } + } + + // Set a default Content-Type if one was not supplied + $type = self::getHeader($headers, 'content-type'); + if (!$type && ($filename === '0' || $filename)) { + $headers['Content-Type'] = MimeType::fromFilename($filename) ?? 'application/octet-stream'; + } + + return [$stream, $headers]; + } + + /** + * @param string[] $headers + */ + private static function getHeader(array $headers, string $key): ?string + { + $lowercaseHeader = strtolower($key); + foreach ($headers as $k => $v) { + if (strtolower((string) $k) === $lowercaseHeader) { + return $v; + } + } + + return null; + } +} diff --git a/3rdparty/guzzlehttp/psr7/src/NoSeekStream.php b/3rdparty/guzzlehttp/psr7/src/NoSeekStream.php new file mode 100644 index 00000000..161a224f --- /dev/null +++ b/3rdparty/guzzlehttp/psr7/src/NoSeekStream.php @@ -0,0 +1,28 @@ +source = $source; + $this->size = $options['size'] ?? null; + $this->metadata = $options['metadata'] ?? []; + $this->buffer = new BufferStream(); + } + + public function __toString(): string + { + try { + return Utils::copyToString($this); + } catch (\Throwable $e) { + if (\PHP_VERSION_ID >= 70400) { + throw $e; + } + trigger_error(sprintf('%s::__toString exception: %s', self::class, (string) $e), E_USER_ERROR); + + return ''; + } + } + + public function close(): void + { + $this->detach(); + } + + public function detach() + { + $this->tellPos = 0; + $this->source = null; + + return null; + } + + public function getSize(): ?int + { + return $this->size; + } + + public function tell(): int + { + return $this->tellPos; + } + + public function eof(): bool + { + return $this->source === null; + } + + public function isSeekable(): bool + { + return false; + } + + public function rewind(): void + { + $this->seek(0); + } + + public function seek($offset, $whence = SEEK_SET): void + { + throw new \RuntimeException('Cannot seek a PumpStream'); + } + + public function isWritable(): bool + { + return false; + } + + public function write($string): int + { + throw new \RuntimeException('Cannot write to a PumpStream'); + } + + public function isReadable(): bool + { + return true; + } + + public function read($length): string + { + $data = $this->buffer->read($length); + $readLen = strlen($data); + $this->tellPos += $readLen; + $remaining = $length - $readLen; + + if ($remaining) { + $this->pump($remaining); + $data .= $this->buffer->read($remaining); + $this->tellPos += strlen($data) - $readLen; + } + + return $data; + } + + public function getContents(): string + { + $result = ''; + while (!$this->eof()) { + $result .= $this->read(1000000); + } + + return $result; + } + + /** + * @return mixed + */ + public function getMetadata($key = null) + { + if (!$key) { + return $this->metadata; + } + + return $this->metadata[$key] ?? null; + } + + private function pump(int $length): void + { + if ($this->source !== null) { + do { + $data = ($this->source)($length); + if ($data === false || $data === null) { + $this->source = null; + + return; + } + $this->buffer->write($data); + $length -= strlen($data); + } while ($length > 0); + } + } +} diff --git a/3rdparty/guzzlehttp/psr7/src/Query.php b/3rdparty/guzzlehttp/psr7/src/Query.php new file mode 100644 index 00000000..ccf867a0 --- /dev/null +++ b/3rdparty/guzzlehttp/psr7/src/Query.php @@ -0,0 +1,118 @@ + '1', 'foo[b]' => '2'])`. + * + * @param string $str Query string to parse + * @param int|bool $urlEncoding How the query string is encoded + */ + public static function parse(string $str, $urlEncoding = true): array + { + $result = []; + + if ($str === '') { + return $result; + } + + if ($urlEncoding === true) { + $decoder = function ($value) { + return rawurldecode(str_replace('+', ' ', (string) $value)); + }; + } elseif ($urlEncoding === PHP_QUERY_RFC3986) { + $decoder = 'rawurldecode'; + } elseif ($urlEncoding === PHP_QUERY_RFC1738) { + $decoder = 'urldecode'; + } else { + $decoder = function ($str) { + return $str; + }; + } + + foreach (explode('&', $str) as $kvp) { + $parts = explode('=', $kvp, 2); + $key = $decoder($parts[0]); + $value = isset($parts[1]) ? $decoder($parts[1]) : null; + if (!array_key_exists($key, $result)) { + $result[$key] = $value; + } else { + if (!is_array($result[$key])) { + $result[$key] = [$result[$key]]; + } + $result[$key][] = $value; + } + } + + return $result; + } + + /** + * Build a query string from an array of key value pairs. + * + * This function can use the return value of `parse()` to build a query + * string. This function does not modify the provided keys when an array is + * encountered (like `http_build_query()` would). + * + * @param array $params Query string parameters. + * @param int|false $encoding Set to false to not encode, + * PHP_QUERY_RFC3986 to encode using + * RFC3986, or PHP_QUERY_RFC1738 to + * encode using RFC1738. + * @param bool $treatBoolsAsInts Set to true to encode as 0/1, and + * false as false/true. + */ + public static function build(array $params, $encoding = PHP_QUERY_RFC3986, bool $treatBoolsAsInts = true): string + { + if (!$params) { + return ''; + } + + if ($encoding === false) { + $encoder = function (string $str): string { + return $str; + }; + } elseif ($encoding === PHP_QUERY_RFC3986) { + $encoder = 'rawurlencode'; + } elseif ($encoding === PHP_QUERY_RFC1738) { + $encoder = 'urlencode'; + } else { + throw new \InvalidArgumentException('Invalid type'); + } + + $castBool = $treatBoolsAsInts ? static function ($v) { return (int) $v; } : static function ($v) { return $v ? 'true' : 'false'; }; + + $qs = ''; + foreach ($params as $k => $v) { + $k = $encoder((string) $k); + if (!is_array($v)) { + $qs .= $k; + $v = is_bool($v) ? $castBool($v) : $v; + if ($v !== null) { + $qs .= '='.$encoder((string) $v); + } + $qs .= '&'; + } else { + foreach ($v as $vv) { + $qs .= $k; + $vv = is_bool($vv) ? $castBool($vv) : $vv; + if ($vv !== null) { + $qs .= '='.$encoder((string) $vv); + } + $qs .= '&'; + } + } + } + + return $qs ? (string) substr($qs, 0, -1) : ''; + } +} diff --git a/3rdparty/guzzlehttp/psr7/src/Request.php b/3rdparty/guzzlehttp/psr7/src/Request.php new file mode 100644 index 00000000..faafe1ad --- /dev/null +++ b/3rdparty/guzzlehttp/psr7/src/Request.php @@ -0,0 +1,159 @@ +assertMethod($method); + if (!($uri instanceof UriInterface)) { + $uri = new Uri($uri); + } + + $this->method = strtoupper($method); + $this->uri = $uri; + $this->setHeaders($headers); + $this->protocol = $version; + + if (!isset($this->headerNames['host'])) { + $this->updateHostFromUri(); + } + + if ($body !== '' && $body !== null) { + $this->stream = Utils::streamFor($body); + } + } + + public function getRequestTarget(): string + { + if ($this->requestTarget !== null) { + return $this->requestTarget; + } + + $target = $this->uri->getPath(); + if ($target === '') { + $target = '/'; + } + if ($this->uri->getQuery() != '') { + $target .= '?'.$this->uri->getQuery(); + } + + return $target; + } + + public function withRequestTarget($requestTarget): RequestInterface + { + if (preg_match('#\s#', $requestTarget)) { + throw new InvalidArgumentException( + 'Invalid request target provided; cannot contain whitespace' + ); + } + + $new = clone $this; + $new->requestTarget = $requestTarget; + + return $new; + } + + public function getMethod(): string + { + return $this->method; + } + + public function withMethod($method): RequestInterface + { + $this->assertMethod($method); + $new = clone $this; + $new->method = strtoupper($method); + + return $new; + } + + public function getUri(): UriInterface + { + return $this->uri; + } + + public function withUri(UriInterface $uri, $preserveHost = false): RequestInterface + { + if ($uri === $this->uri) { + return $this; + } + + $new = clone $this; + $new->uri = $uri; + + if (!$preserveHost || !isset($this->headerNames['host'])) { + $new->updateHostFromUri(); + } + + return $new; + } + + private function updateHostFromUri(): void + { + $host = $this->uri->getHost(); + + if ($host == '') { + return; + } + + if (($port = $this->uri->getPort()) !== null) { + $host .= ':'.$port; + } + + if (isset($this->headerNames['host'])) { + $header = $this->headerNames['host']; + } else { + $header = 'Host'; + $this->headerNames['host'] = 'Host'; + } + // Ensure Host is the first header. + // See: https://datatracker.ietf.org/doc/html/rfc7230#section-5.4 + $this->headers = [$header => [$host]] + $this->headers; + } + + /** + * @param mixed $method + */ + private function assertMethod($method): void + { + if (!is_string($method) || $method === '') { + throw new InvalidArgumentException('Method must be a non-empty string.'); + } + } +} diff --git a/3rdparty/guzzlehttp/psr7/src/Response.php b/3rdparty/guzzlehttp/psr7/src/Response.php new file mode 100644 index 00000000..34e612fd --- /dev/null +++ b/3rdparty/guzzlehttp/psr7/src/Response.php @@ -0,0 +1,161 @@ + 'Continue', + 101 => 'Switching Protocols', + 102 => 'Processing', + 200 => 'OK', + 201 => 'Created', + 202 => 'Accepted', + 203 => 'Non-Authoritative Information', + 204 => 'No Content', + 205 => 'Reset Content', + 206 => 'Partial Content', + 207 => 'Multi-status', + 208 => 'Already Reported', + 300 => 'Multiple Choices', + 301 => 'Moved Permanently', + 302 => 'Found', + 303 => 'See Other', + 304 => 'Not Modified', + 305 => 'Use Proxy', + 306 => 'Switch Proxy', + 307 => 'Temporary Redirect', + 308 => 'Permanent Redirect', + 400 => 'Bad Request', + 401 => 'Unauthorized', + 402 => 'Payment Required', + 403 => 'Forbidden', + 404 => 'Not Found', + 405 => 'Method Not Allowed', + 406 => 'Not Acceptable', + 407 => 'Proxy Authentication Required', + 408 => 'Request Time-out', + 409 => 'Conflict', + 410 => 'Gone', + 411 => 'Length Required', + 412 => 'Precondition Failed', + 413 => 'Request Entity Too Large', + 414 => 'Request-URI Too Large', + 415 => 'Unsupported Media Type', + 416 => 'Requested range not satisfiable', + 417 => 'Expectation Failed', + 418 => 'I\'m a teapot', + 422 => 'Unprocessable Entity', + 423 => 'Locked', + 424 => 'Failed Dependency', + 425 => 'Unordered Collection', + 426 => 'Upgrade Required', + 428 => 'Precondition Required', + 429 => 'Too Many Requests', + 431 => 'Request Header Fields Too Large', + 451 => 'Unavailable For Legal Reasons', + 500 => 'Internal Server Error', + 501 => 'Not Implemented', + 502 => 'Bad Gateway', + 503 => 'Service Unavailable', + 504 => 'Gateway Time-out', + 505 => 'HTTP Version not supported', + 506 => 'Variant Also Negotiates', + 507 => 'Insufficient Storage', + 508 => 'Loop Detected', + 510 => 'Not Extended', + 511 => 'Network Authentication Required', + ]; + + /** @var string */ + private $reasonPhrase; + + /** @var int */ + private $statusCode; + + /** + * @param int $status Status code + * @param (string|string[])[] $headers Response headers + * @param string|resource|StreamInterface|null $body Response body + * @param string $version Protocol version + * @param string|null $reason Reason phrase (when empty a default will be used based on the status code) + */ + public function __construct( + int $status = 200, + array $headers = [], + $body = null, + string $version = '1.1', + ?string $reason = null + ) { + $this->assertStatusCodeRange($status); + + $this->statusCode = $status; + + if ($body !== '' && $body !== null) { + $this->stream = Utils::streamFor($body); + } + + $this->setHeaders($headers); + if ($reason == '' && isset(self::PHRASES[$this->statusCode])) { + $this->reasonPhrase = self::PHRASES[$this->statusCode]; + } else { + $this->reasonPhrase = (string) $reason; + } + + $this->protocol = $version; + } + + public function getStatusCode(): int + { + return $this->statusCode; + } + + public function getReasonPhrase(): string + { + return $this->reasonPhrase; + } + + public function withStatus($code, $reasonPhrase = ''): ResponseInterface + { + $this->assertStatusCodeIsInteger($code); + $code = (int) $code; + $this->assertStatusCodeRange($code); + + $new = clone $this; + $new->statusCode = $code; + if ($reasonPhrase == '' && isset(self::PHRASES[$new->statusCode])) { + $reasonPhrase = self::PHRASES[$new->statusCode]; + } + $new->reasonPhrase = (string) $reasonPhrase; + + return $new; + } + + /** + * @param mixed $statusCode + */ + private function assertStatusCodeIsInteger($statusCode): void + { + if (filter_var($statusCode, FILTER_VALIDATE_INT) === false) { + throw new \InvalidArgumentException('Status code must be an integer value.'); + } + } + + private function assertStatusCodeRange(int $statusCode): void + { + if ($statusCode < 100 || $statusCode >= 600) { + throw new \InvalidArgumentException('Status code must be an integer value between 1xx and 5xx.'); + } + } +} diff --git a/3rdparty/guzzlehttp/psr7/src/Rfc7230.php b/3rdparty/guzzlehttp/psr7/src/Rfc7230.php new file mode 100644 index 00000000..8219dba4 --- /dev/null +++ b/3rdparty/guzzlehttp/psr7/src/Rfc7230.php @@ -0,0 +1,23 @@ +@,;:\\\"/[\]?={}\x01-\x20\x7F]++):[ \t]*+((?:[ \t]*+[\x21-\x7E\x80-\xFF]++)*+)[ \t]*+\r?\n)m"; + public const HEADER_FOLD_REGEX = "(\r?\n[ \t]++)"; +} diff --git a/3rdparty/guzzlehttp/psr7/src/ServerRequest.php b/3rdparty/guzzlehttp/psr7/src/ServerRequest.php new file mode 100644 index 00000000..3cc95345 --- /dev/null +++ b/3rdparty/guzzlehttp/psr7/src/ServerRequest.php @@ -0,0 +1,340 @@ +serverParams = $serverParams; + + parent::__construct($method, $uri, $headers, $body, $version); + } + + /** + * Return an UploadedFile instance array. + * + * @param array $files An array which respect $_FILES structure + * + * @throws InvalidArgumentException for unrecognized values + */ + public static function normalizeFiles(array $files): array + { + $normalized = []; + + foreach ($files as $key => $value) { + if ($value instanceof UploadedFileInterface) { + $normalized[$key] = $value; + } elseif (is_array($value) && isset($value['tmp_name'])) { + $normalized[$key] = self::createUploadedFileFromSpec($value); + } elseif (is_array($value)) { + $normalized[$key] = self::normalizeFiles($value); + continue; + } else { + throw new InvalidArgumentException('Invalid value in files specification'); + } + } + + return $normalized; + } + + /** + * Create and return an UploadedFile instance from a $_FILES specification. + * + * If the specification represents an array of values, this method will + * delegate to normalizeNestedFileSpec() and return that return value. + * + * @param array $value $_FILES struct + * + * @return UploadedFileInterface|UploadedFileInterface[] + */ + private static function createUploadedFileFromSpec(array $value) + { + if (is_array($value['tmp_name'])) { + return self::normalizeNestedFileSpec($value); + } + + return new UploadedFile( + $value['tmp_name'], + (int) $value['size'], + (int) $value['error'], + $value['name'], + $value['type'] + ); + } + + /** + * Normalize an array of file specifications. + * + * Loops through all nested files and returns a normalized array of + * UploadedFileInterface instances. + * + * @return UploadedFileInterface[] + */ + private static function normalizeNestedFileSpec(array $files = []): array + { + $normalizedFiles = []; + + foreach (array_keys($files['tmp_name']) as $key) { + $spec = [ + 'tmp_name' => $files['tmp_name'][$key], + 'size' => $files['size'][$key] ?? null, + 'error' => $files['error'][$key] ?? null, + 'name' => $files['name'][$key] ?? null, + 'type' => $files['type'][$key] ?? null, + ]; + $normalizedFiles[$key] = self::createUploadedFileFromSpec($spec); + } + + return $normalizedFiles; + } + + /** + * Return a ServerRequest populated with superglobals: + * $_GET + * $_POST + * $_COOKIE + * $_FILES + * $_SERVER + */ + public static function fromGlobals(): ServerRequestInterface + { + $method = $_SERVER['REQUEST_METHOD'] ?? 'GET'; + $headers = getallheaders(); + $uri = self::getUriFromGlobals(); + $body = new CachingStream(new LazyOpenStream('php://input', 'r+')); + $protocol = isset($_SERVER['SERVER_PROTOCOL']) ? str_replace('HTTP/', '', $_SERVER['SERVER_PROTOCOL']) : '1.1'; + + $serverRequest = new ServerRequest($method, $uri, $headers, $body, $protocol, $_SERVER); + + return $serverRequest + ->withCookieParams($_COOKIE) + ->withQueryParams($_GET) + ->withParsedBody($_POST) + ->withUploadedFiles(self::normalizeFiles($_FILES)); + } + + private static function extractHostAndPortFromAuthority(string $authority): array + { + $uri = 'http://'.$authority; + $parts = parse_url($uri); + if (false === $parts) { + return [null, null]; + } + + $host = $parts['host'] ?? null; + $port = $parts['port'] ?? null; + + return [$host, $port]; + } + + /** + * Get a Uri populated with values from $_SERVER. + */ + public static function getUriFromGlobals(): UriInterface + { + $uri = new Uri(''); + + $uri = $uri->withScheme(!empty($_SERVER['HTTPS']) && $_SERVER['HTTPS'] !== 'off' ? 'https' : 'http'); + + $hasPort = false; + if (isset($_SERVER['HTTP_HOST'])) { + [$host, $port] = self::extractHostAndPortFromAuthority($_SERVER['HTTP_HOST']); + if ($host !== null) { + $uri = $uri->withHost($host); + } + + if ($port !== null) { + $hasPort = true; + $uri = $uri->withPort($port); + } + } elseif (isset($_SERVER['SERVER_NAME'])) { + $uri = $uri->withHost($_SERVER['SERVER_NAME']); + } elseif (isset($_SERVER['SERVER_ADDR'])) { + $uri = $uri->withHost($_SERVER['SERVER_ADDR']); + } + + if (!$hasPort && isset($_SERVER['SERVER_PORT'])) { + $uri = $uri->withPort($_SERVER['SERVER_PORT']); + } + + $hasQuery = false; + if (isset($_SERVER['REQUEST_URI'])) { + $requestUriParts = explode('?', $_SERVER['REQUEST_URI'], 2); + $uri = $uri->withPath($requestUriParts[0]); + if (isset($requestUriParts[1])) { + $hasQuery = true; + $uri = $uri->withQuery($requestUriParts[1]); + } + } + + if (!$hasQuery && isset($_SERVER['QUERY_STRING'])) { + $uri = $uri->withQuery($_SERVER['QUERY_STRING']); + } + + return $uri; + } + + public function getServerParams(): array + { + return $this->serverParams; + } + + public function getUploadedFiles(): array + { + return $this->uploadedFiles; + } + + public function withUploadedFiles(array $uploadedFiles): ServerRequestInterface + { + $new = clone $this; + $new->uploadedFiles = $uploadedFiles; + + return $new; + } + + public function getCookieParams(): array + { + return $this->cookieParams; + } + + public function withCookieParams(array $cookies): ServerRequestInterface + { + $new = clone $this; + $new->cookieParams = $cookies; + + return $new; + } + + public function getQueryParams(): array + { + return $this->queryParams; + } + + public function withQueryParams(array $query): ServerRequestInterface + { + $new = clone $this; + $new->queryParams = $query; + + return $new; + } + + /** + * @return array|object|null + */ + public function getParsedBody() + { + return $this->parsedBody; + } + + public function withParsedBody($data): ServerRequestInterface + { + $new = clone $this; + $new->parsedBody = $data; + + return $new; + } + + public function getAttributes(): array + { + return $this->attributes; + } + + /** + * @return mixed + */ + public function getAttribute($attribute, $default = null) + { + if (false === array_key_exists($attribute, $this->attributes)) { + return $default; + } + + return $this->attributes[$attribute]; + } + + public function withAttribute($attribute, $value): ServerRequestInterface + { + $new = clone $this; + $new->attributes[$attribute] = $value; + + return $new; + } + + public function withoutAttribute($attribute): ServerRequestInterface + { + if (false === array_key_exists($attribute, $this->attributes)) { + return $this; + } + + $new = clone $this; + unset($new->attributes[$attribute]); + + return $new; + } +} diff --git a/3rdparty/guzzlehttp/psr7/src/Stream.php b/3rdparty/guzzlehttp/psr7/src/Stream.php new file mode 100644 index 00000000..0aff9b2b --- /dev/null +++ b/3rdparty/guzzlehttp/psr7/src/Stream.php @@ -0,0 +1,283 @@ +size = $options['size']; + } + + $this->customMetadata = $options['metadata'] ?? []; + $this->stream = $stream; + $meta = stream_get_meta_data($this->stream); + $this->seekable = $meta['seekable']; + $this->readable = (bool) preg_match(self::READABLE_MODES, $meta['mode']); + $this->writable = (bool) preg_match(self::WRITABLE_MODES, $meta['mode']); + $this->uri = $this->getMetadata('uri'); + } + + /** + * Closes the stream when the destructed + */ + public function __destruct() + { + $this->close(); + } + + public function __toString(): string + { + try { + if ($this->isSeekable()) { + $this->seek(0); + } + + return $this->getContents(); + } catch (\Throwable $e) { + if (\PHP_VERSION_ID >= 70400) { + throw $e; + } + trigger_error(sprintf('%s::__toString exception: %s', self::class, (string) $e), E_USER_ERROR); + + return ''; + } + } + + public function getContents(): string + { + if (!isset($this->stream)) { + throw new \RuntimeException('Stream is detached'); + } + + if (!$this->readable) { + throw new \RuntimeException('Cannot read from non-readable stream'); + } + + return Utils::tryGetContents($this->stream); + } + + public function close(): void + { + if (isset($this->stream)) { + if (is_resource($this->stream)) { + fclose($this->stream); + } + $this->detach(); + } + } + + public function detach() + { + if (!isset($this->stream)) { + return null; + } + + $result = $this->stream; + unset($this->stream); + $this->size = $this->uri = null; + $this->readable = $this->writable = $this->seekable = false; + + return $result; + } + + public function getSize(): ?int + { + if ($this->size !== null) { + return $this->size; + } + + if (!isset($this->stream)) { + return null; + } + + // Clear the stat cache if the stream has a URI + if ($this->uri) { + clearstatcache(true, $this->uri); + } + + $stats = fstat($this->stream); + if (is_array($stats) && isset($stats['size'])) { + $this->size = $stats['size']; + + return $this->size; + } + + return null; + } + + public function isReadable(): bool + { + return $this->readable; + } + + public function isWritable(): bool + { + return $this->writable; + } + + public function isSeekable(): bool + { + return $this->seekable; + } + + public function eof(): bool + { + if (!isset($this->stream)) { + throw new \RuntimeException('Stream is detached'); + } + + return feof($this->stream); + } + + public function tell(): int + { + if (!isset($this->stream)) { + throw new \RuntimeException('Stream is detached'); + } + + $result = ftell($this->stream); + + if ($result === false) { + throw new \RuntimeException('Unable to determine stream position'); + } + + return $result; + } + + public function rewind(): void + { + $this->seek(0); + } + + public function seek($offset, $whence = SEEK_SET): void + { + $whence = (int) $whence; + + if (!isset($this->stream)) { + throw new \RuntimeException('Stream is detached'); + } + if (!$this->seekable) { + throw new \RuntimeException('Stream is not seekable'); + } + if (fseek($this->stream, $offset, $whence) === -1) { + throw new \RuntimeException('Unable to seek to stream position ' + .$offset.' with whence '.var_export($whence, true)); + } + } + + public function read($length): string + { + if (!isset($this->stream)) { + throw new \RuntimeException('Stream is detached'); + } + if (!$this->readable) { + throw new \RuntimeException('Cannot read from non-readable stream'); + } + if ($length < 0) { + throw new \RuntimeException('Length parameter cannot be negative'); + } + + if (0 === $length) { + return ''; + } + + try { + $string = fread($this->stream, $length); + } catch (\Exception $e) { + throw new \RuntimeException('Unable to read from stream', 0, $e); + } + + if (false === $string) { + throw new \RuntimeException('Unable to read from stream'); + } + + return $string; + } + + public function write($string): int + { + if (!isset($this->stream)) { + throw new \RuntimeException('Stream is detached'); + } + if (!$this->writable) { + throw new \RuntimeException('Cannot write to a non-writable stream'); + } + + // We can't know the size after writing anything + $this->size = null; + $result = fwrite($this->stream, $string); + + if ($result === false) { + throw new \RuntimeException('Unable to write to stream'); + } + + return $result; + } + + /** + * @return mixed + */ + public function getMetadata($key = null) + { + if (!isset($this->stream)) { + return $key ? null : []; + } elseif (!$key) { + return $this->customMetadata + stream_get_meta_data($this->stream); + } elseif (isset($this->customMetadata[$key])) { + return $this->customMetadata[$key]; + } + + $meta = stream_get_meta_data($this->stream); + + return $meta[$key] ?? null; + } +} diff --git a/3rdparty/guzzlehttp/psr7/src/StreamDecoratorTrait.php b/3rdparty/guzzlehttp/psr7/src/StreamDecoratorTrait.php new file mode 100644 index 00000000..601c13af --- /dev/null +++ b/3rdparty/guzzlehttp/psr7/src/StreamDecoratorTrait.php @@ -0,0 +1,156 @@ +stream = $stream; + } + + /** + * Magic method used to create a new stream if streams are not added in + * the constructor of a decorator (e.g., LazyOpenStream). + * + * @return StreamInterface + */ + public function __get(string $name) + { + if ($name === 'stream') { + $this->stream = $this->createStream(); + + return $this->stream; + } + + throw new \UnexpectedValueException("$name not found on class"); + } + + public function __toString(): string + { + try { + if ($this->isSeekable()) { + $this->seek(0); + } + + return $this->getContents(); + } catch (\Throwable $e) { + if (\PHP_VERSION_ID >= 70400) { + throw $e; + } + trigger_error(sprintf('%s::__toString exception: %s', self::class, (string) $e), E_USER_ERROR); + + return ''; + } + } + + public function getContents(): string + { + return Utils::copyToString($this); + } + + /** + * Allow decorators to implement custom methods + * + * @return mixed + */ + public function __call(string $method, array $args) + { + /** @var callable $callable */ + $callable = [$this->stream, $method]; + $result = ($callable)(...$args); + + // Always return the wrapped object if the result is a return $this + return $result === $this->stream ? $this : $result; + } + + public function close(): void + { + $this->stream->close(); + } + + /** + * @return mixed + */ + public function getMetadata($key = null) + { + return $this->stream->getMetadata($key); + } + + public function detach() + { + return $this->stream->detach(); + } + + public function getSize(): ?int + { + return $this->stream->getSize(); + } + + public function eof(): bool + { + return $this->stream->eof(); + } + + public function tell(): int + { + return $this->stream->tell(); + } + + public function isReadable(): bool + { + return $this->stream->isReadable(); + } + + public function isWritable(): bool + { + return $this->stream->isWritable(); + } + + public function isSeekable(): bool + { + return $this->stream->isSeekable(); + } + + public function rewind(): void + { + $this->seek(0); + } + + public function seek($offset, $whence = SEEK_SET): void + { + $this->stream->seek($offset, $whence); + } + + public function read($length): string + { + return $this->stream->read($length); + } + + public function write($string): int + { + return $this->stream->write($string); + } + + /** + * Implement in subclasses to dynamically create streams when requested. + * + * @throws \BadMethodCallException + */ + protected function createStream(): StreamInterface + { + throw new \BadMethodCallException('Not implemented'); + } +} diff --git a/3rdparty/guzzlehttp/psr7/src/StreamWrapper.php b/3rdparty/guzzlehttp/psr7/src/StreamWrapper.php new file mode 100644 index 00000000..77b04d74 --- /dev/null +++ b/3rdparty/guzzlehttp/psr7/src/StreamWrapper.php @@ -0,0 +1,207 @@ +isReadable()) { + $mode = $stream->isWritable() ? 'r+' : 'r'; + } elseif ($stream->isWritable()) { + $mode = 'w'; + } else { + throw new \InvalidArgumentException('The stream must be readable, ' + .'writable, or both.'); + } + + return fopen('guzzle://stream', $mode, false, self::createStreamContext($stream)); + } + + /** + * Creates a stream context that can be used to open a stream as a php stream resource. + * + * @return resource + */ + public static function createStreamContext(StreamInterface $stream) + { + return stream_context_create([ + 'guzzle' => ['stream' => $stream], + ]); + } + + /** + * Registers the stream wrapper if needed + */ + public static function register(): void + { + if (!in_array('guzzle', stream_get_wrappers())) { + stream_wrapper_register('guzzle', __CLASS__); + } + } + + public function stream_open(string $path, string $mode, int $options, ?string &$opened_path = null): bool + { + $options = stream_context_get_options($this->context); + + if (!isset($options['guzzle']['stream'])) { + return false; + } + + $this->mode = $mode; + $this->stream = $options['guzzle']['stream']; + + return true; + } + + public function stream_read(int $count): string + { + return $this->stream->read($count); + } + + public function stream_write(string $data): int + { + return $this->stream->write($data); + } + + public function stream_tell(): int + { + return $this->stream->tell(); + } + + public function stream_eof(): bool + { + return $this->stream->eof(); + } + + public function stream_seek(int $offset, int $whence): bool + { + $this->stream->seek($offset, $whence); + + return true; + } + + /** + * @return resource|false + */ + public function stream_cast(int $cast_as) + { + $stream = clone $this->stream; + $resource = $stream->detach(); + + return $resource ?? false; + } + + /** + * @return array{ + * dev: int, + * ino: int, + * mode: int, + * nlink: int, + * uid: int, + * gid: int, + * rdev: int, + * size: int, + * atime: int, + * mtime: int, + * ctime: int, + * blksize: int, + * blocks: int + * }|false + */ + public function stream_stat() + { + if ($this->stream->getSize() === null) { + return false; + } + + static $modeMap = [ + 'r' => 33060, + 'rb' => 33060, + 'r+' => 33206, + 'w' => 33188, + 'wb' => 33188, + ]; + + return [ + 'dev' => 0, + 'ino' => 0, + 'mode' => $modeMap[$this->mode], + 'nlink' => 0, + 'uid' => 0, + 'gid' => 0, + 'rdev' => 0, + 'size' => $this->stream->getSize() ?: 0, + 'atime' => 0, + 'mtime' => 0, + 'ctime' => 0, + 'blksize' => 0, + 'blocks' => 0, + ]; + } + + /** + * @return array{ + * dev: int, + * ino: int, + * mode: int, + * nlink: int, + * uid: int, + * gid: int, + * rdev: int, + * size: int, + * atime: int, + * mtime: int, + * ctime: int, + * blksize: int, + * blocks: int + * } + */ + public function url_stat(string $path, int $flags): array + { + return [ + 'dev' => 0, + 'ino' => 0, + 'mode' => 0, + 'nlink' => 0, + 'uid' => 0, + 'gid' => 0, + 'rdev' => 0, + 'size' => 0, + 'atime' => 0, + 'mtime' => 0, + 'ctime' => 0, + 'blksize' => 0, + 'blocks' => 0, + ]; + } +} diff --git a/3rdparty/guzzlehttp/psr7/src/UploadedFile.php b/3rdparty/guzzlehttp/psr7/src/UploadedFile.php new file mode 100644 index 00000000..d9b779f1 --- /dev/null +++ b/3rdparty/guzzlehttp/psr7/src/UploadedFile.php @@ -0,0 +1,211 @@ + 'UPLOAD_ERR_OK', + UPLOAD_ERR_INI_SIZE => 'UPLOAD_ERR_INI_SIZE', + UPLOAD_ERR_FORM_SIZE => 'UPLOAD_ERR_FORM_SIZE', + UPLOAD_ERR_PARTIAL => 'UPLOAD_ERR_PARTIAL', + UPLOAD_ERR_NO_FILE => 'UPLOAD_ERR_NO_FILE', + UPLOAD_ERR_NO_TMP_DIR => 'UPLOAD_ERR_NO_TMP_DIR', + UPLOAD_ERR_CANT_WRITE => 'UPLOAD_ERR_CANT_WRITE', + UPLOAD_ERR_EXTENSION => 'UPLOAD_ERR_EXTENSION', + ]; + + /** + * @var string|null + */ + private $clientFilename; + + /** + * @var string|null + */ + private $clientMediaType; + + /** + * @var int + */ + private $error; + + /** + * @var string|null + */ + private $file; + + /** + * @var bool + */ + private $moved = false; + + /** + * @var int|null + */ + private $size; + + /** + * @var StreamInterface|null + */ + private $stream; + + /** + * @param StreamInterface|string|resource $streamOrFile + */ + public function __construct( + $streamOrFile, + ?int $size, + int $errorStatus, + ?string $clientFilename = null, + ?string $clientMediaType = null + ) { + $this->setError($errorStatus); + $this->size = $size; + $this->clientFilename = $clientFilename; + $this->clientMediaType = $clientMediaType; + + if ($this->isOk()) { + $this->setStreamOrFile($streamOrFile); + } + } + + /** + * Depending on the value set file or stream variable + * + * @param StreamInterface|string|resource $streamOrFile + * + * @throws InvalidArgumentException + */ + private function setStreamOrFile($streamOrFile): void + { + if (is_string($streamOrFile)) { + $this->file = $streamOrFile; + } elseif (is_resource($streamOrFile)) { + $this->stream = new Stream($streamOrFile); + } elseif ($streamOrFile instanceof StreamInterface) { + $this->stream = $streamOrFile; + } else { + throw new InvalidArgumentException( + 'Invalid stream or file provided for UploadedFile' + ); + } + } + + /** + * @throws InvalidArgumentException + */ + private function setError(int $error): void + { + if (!isset(UploadedFile::ERROR_MAP[$error])) { + throw new InvalidArgumentException( + 'Invalid error status for UploadedFile' + ); + } + + $this->error = $error; + } + + private static function isStringNotEmpty($param): bool + { + return is_string($param) && false === empty($param); + } + + /** + * Return true if there is no upload error + */ + private function isOk(): bool + { + return $this->error === UPLOAD_ERR_OK; + } + + public function isMoved(): bool + { + return $this->moved; + } + + /** + * @throws RuntimeException if is moved or not ok + */ + private function validateActive(): void + { + if (false === $this->isOk()) { + throw new RuntimeException(\sprintf('Cannot retrieve stream due to upload error (%s)', self::ERROR_MAP[$this->error])); + } + + if ($this->isMoved()) { + throw new RuntimeException('Cannot retrieve stream after it has already been moved'); + } + } + + public function getStream(): StreamInterface + { + $this->validateActive(); + + if ($this->stream instanceof StreamInterface) { + return $this->stream; + } + + /** @var string $file */ + $file = $this->file; + + return new LazyOpenStream($file, 'r+'); + } + + public function moveTo($targetPath): void + { + $this->validateActive(); + + if (false === self::isStringNotEmpty($targetPath)) { + throw new InvalidArgumentException( + 'Invalid path provided for move operation; must be a non-empty string' + ); + } + + if ($this->file) { + $this->moved = PHP_SAPI === 'cli' + ? rename($this->file, $targetPath) + : move_uploaded_file($this->file, $targetPath); + } else { + Utils::copyToStream( + $this->getStream(), + new LazyOpenStream($targetPath, 'w') + ); + + $this->moved = true; + } + + if (false === $this->moved) { + throw new RuntimeException( + sprintf('Uploaded file could not be moved to %s', $targetPath) + ); + } + } + + public function getSize(): ?int + { + return $this->size; + } + + public function getError(): int + { + return $this->error; + } + + public function getClientFilename(): ?string + { + return $this->clientFilename; + } + + public function getClientMediaType(): ?string + { + return $this->clientMediaType; + } +} diff --git a/3rdparty/guzzlehttp/psr7/src/Uri.php b/3rdparty/guzzlehttp/psr7/src/Uri.php new file mode 100644 index 00000000..a7cdfb00 --- /dev/null +++ b/3rdparty/guzzlehttp/psr7/src/Uri.php @@ -0,0 +1,743 @@ + 80, + 'https' => 443, + 'ftp' => 21, + 'gopher' => 70, + 'nntp' => 119, + 'news' => 119, + 'telnet' => 23, + 'tn3270' => 23, + 'imap' => 143, + 'pop' => 110, + 'ldap' => 389, + ]; + + /** + * Unreserved characters for use in a regex. + * + * @see https://datatracker.ietf.org/doc/html/rfc3986#section-2.3 + */ + private const CHAR_UNRESERVED = 'a-zA-Z0-9_\-\.~'; + + /** + * Sub-delims for use in a regex. + * + * @see https://datatracker.ietf.org/doc/html/rfc3986#section-2.2 + */ + private const CHAR_SUB_DELIMS = '!\$&\'\(\)\*\+,;='; + private const QUERY_SEPARATORS_REPLACEMENT = ['=' => '%3D', '&' => '%26']; + + /** @var string Uri scheme. */ + private $scheme = ''; + + /** @var string Uri user info. */ + private $userInfo = ''; + + /** @var string Uri host. */ + private $host = ''; + + /** @var int|null Uri port. */ + private $port; + + /** @var string Uri path. */ + private $path = ''; + + /** @var string Uri query string. */ + private $query = ''; + + /** @var string Uri fragment. */ + private $fragment = ''; + + /** @var string|null String representation */ + private $composedComponents; + + public function __construct(string $uri = '') + { + if ($uri !== '') { + $parts = self::parse($uri); + if ($parts === false) { + throw new MalformedUriException("Unable to parse URI: $uri"); + } + $this->applyParts($parts); + } + } + + /** + * UTF-8 aware \parse_url() replacement. + * + * The internal function produces broken output for non ASCII domain names + * (IDN) when used with locales other than "C". + * + * On the other hand, cURL understands IDN correctly only when UTF-8 locale + * is configured ("C.UTF-8", "en_US.UTF-8", etc.). + * + * @see https://bugs.php.net/bug.php?id=52923 + * @see https://www.php.net/manual/en/function.parse-url.php#114817 + * @see https://curl.haxx.se/libcurl/c/CURLOPT_URL.html#ENCODING + * + * @return array|false + */ + private static function parse(string $url) + { + // If IPv6 + $prefix = ''; + if (preg_match('%^(.*://\[[0-9:a-fA-F]+\])(.*?)$%', $url, $matches)) { + /** @var array{0:string, 1:string, 2:string} $matches */ + $prefix = $matches[1]; + $url = $matches[2]; + } + + /** @var string */ + $encodedUrl = preg_replace_callback( + '%[^:/@?&=#]+%usD', + static function ($matches) { + return urlencode($matches[0]); + }, + $url + ); + + $result = parse_url($prefix.$encodedUrl); + + if ($result === false) { + return false; + } + + return array_map('urldecode', $result); + } + + public function __toString(): string + { + if ($this->composedComponents === null) { + $this->composedComponents = self::composeComponents( + $this->scheme, + $this->getAuthority(), + $this->path, + $this->query, + $this->fragment + ); + } + + return $this->composedComponents; + } + + /** + * Composes a URI reference string from its various components. + * + * Usually this method does not need to be called manually but instead is used indirectly via + * `Psr\Http\Message\UriInterface::__toString`. + * + * PSR-7 UriInterface treats an empty component the same as a missing component as + * getQuery(), getFragment() etc. always return a string. This explains the slight + * difference to RFC 3986 Section 5.3. + * + * Another adjustment is that the authority separator is added even when the authority is missing/empty + * for the "file" scheme. This is because PHP stream functions like `file_get_contents` only work with + * `file:///myfile` but not with `file:/myfile` although they are equivalent according to RFC 3986. But + * `file:///` is the more common syntax for the file scheme anyway (Chrome for example redirects to + * that format). + * + * @see https://datatracker.ietf.org/doc/html/rfc3986#section-5.3 + */ + public static function composeComponents(?string $scheme, ?string $authority, string $path, ?string $query, ?string $fragment): string + { + $uri = ''; + + // weak type checks to also accept null until we can add scalar type hints + if ($scheme != '') { + $uri .= $scheme.':'; + } + + if ($authority != '' || $scheme === 'file') { + $uri .= '//'.$authority; + } + + if ($authority != '' && $path != '' && $path[0] != '/') { + $path = '/'.$path; + } + + $uri .= $path; + + if ($query != '') { + $uri .= '?'.$query; + } + + if ($fragment != '') { + $uri .= '#'.$fragment; + } + + return $uri; + } + + /** + * Whether the URI has the default port of the current scheme. + * + * `Psr\Http\Message\UriInterface::getPort` may return null or the standard port. This method can be used + * independently of the implementation. + */ + public static function isDefaultPort(UriInterface $uri): bool + { + return $uri->getPort() === null + || (isset(self::DEFAULT_PORTS[$uri->getScheme()]) && $uri->getPort() === self::DEFAULT_PORTS[$uri->getScheme()]); + } + + /** + * Whether the URI is absolute, i.e. it has a scheme. + * + * An instance of UriInterface can either be an absolute URI or a relative reference. This method returns true + * if it is the former. An absolute URI has a scheme. A relative reference is used to express a URI relative + * to another URI, the base URI. Relative references can be divided into several forms: + * - network-path references, e.g. '//example.com/path' + * - absolute-path references, e.g. '/path' + * - relative-path references, e.g. 'subpath' + * + * @see Uri::isNetworkPathReference + * @see Uri::isAbsolutePathReference + * @see Uri::isRelativePathReference + * @see https://datatracker.ietf.org/doc/html/rfc3986#section-4 + */ + public static function isAbsolute(UriInterface $uri): bool + { + return $uri->getScheme() !== ''; + } + + /** + * Whether the URI is a network-path reference. + * + * A relative reference that begins with two slash characters is termed an network-path reference. + * + * @see https://datatracker.ietf.org/doc/html/rfc3986#section-4.2 + */ + public static function isNetworkPathReference(UriInterface $uri): bool + { + return $uri->getScheme() === '' && $uri->getAuthority() !== ''; + } + + /** + * Whether the URI is a absolute-path reference. + * + * A relative reference that begins with a single slash character is termed an absolute-path reference. + * + * @see https://datatracker.ietf.org/doc/html/rfc3986#section-4.2 + */ + public static function isAbsolutePathReference(UriInterface $uri): bool + { + return $uri->getScheme() === '' + && $uri->getAuthority() === '' + && isset($uri->getPath()[0]) + && $uri->getPath()[0] === '/'; + } + + /** + * Whether the URI is a relative-path reference. + * + * A relative reference that does not begin with a slash character is termed a relative-path reference. + * + * @see https://datatracker.ietf.org/doc/html/rfc3986#section-4.2 + */ + public static function isRelativePathReference(UriInterface $uri): bool + { + return $uri->getScheme() === '' + && $uri->getAuthority() === '' + && (!isset($uri->getPath()[0]) || $uri->getPath()[0] !== '/'); + } + + /** + * Whether the URI is a same-document reference. + * + * A same-document reference refers to a URI that is, aside from its fragment + * component, identical to the base URI. When no base URI is given, only an empty + * URI reference (apart from its fragment) is considered a same-document reference. + * + * @param UriInterface $uri The URI to check + * @param UriInterface|null $base An optional base URI to compare against + * + * @see https://datatracker.ietf.org/doc/html/rfc3986#section-4.4 + */ + public static function isSameDocumentReference(UriInterface $uri, ?UriInterface $base = null): bool + { + if ($base !== null) { + $uri = UriResolver::resolve($base, $uri); + + return ($uri->getScheme() === $base->getScheme()) + && ($uri->getAuthority() === $base->getAuthority()) + && ($uri->getPath() === $base->getPath()) + && ($uri->getQuery() === $base->getQuery()); + } + + return $uri->getScheme() === '' && $uri->getAuthority() === '' && $uri->getPath() === '' && $uri->getQuery() === ''; + } + + /** + * Creates a new URI with a specific query string value removed. + * + * Any existing query string values that exactly match the provided key are + * removed. + * + * @param UriInterface $uri URI to use as a base. + * @param string $key Query string key to remove. + */ + public static function withoutQueryValue(UriInterface $uri, string $key): UriInterface + { + $result = self::getFilteredQueryString($uri, [$key]); + + return $uri->withQuery(implode('&', $result)); + } + + /** + * Creates a new URI with a specific query string value. + * + * Any existing query string values that exactly match the provided key are + * removed and replaced with the given key value pair. + * + * A value of null will set the query string key without a value, e.g. "key" + * instead of "key=value". + * + * @param UriInterface $uri URI to use as a base. + * @param string $key Key to set. + * @param string|null $value Value to set + */ + public static function withQueryValue(UriInterface $uri, string $key, ?string $value): UriInterface + { + $result = self::getFilteredQueryString($uri, [$key]); + + $result[] = self::generateQueryString($key, $value); + + return $uri->withQuery(implode('&', $result)); + } + + /** + * Creates a new URI with multiple specific query string values. + * + * It has the same behavior as withQueryValue() but for an associative array of key => value. + * + * @param UriInterface $uri URI to use as a base. + * @param (string|null)[] $keyValueArray Associative array of key and values + */ + public static function withQueryValues(UriInterface $uri, array $keyValueArray): UriInterface + { + $result = self::getFilteredQueryString($uri, array_keys($keyValueArray)); + + foreach ($keyValueArray as $key => $value) { + $result[] = self::generateQueryString((string) $key, $value !== null ? (string) $value : null); + } + + return $uri->withQuery(implode('&', $result)); + } + + /** + * Creates a URI from a hash of `parse_url` components. + * + * @see https://www.php.net/manual/en/function.parse-url.php + * + * @throws MalformedUriException If the components do not form a valid URI. + */ + public static function fromParts(array $parts): UriInterface + { + $uri = new self(); + $uri->applyParts($parts); + $uri->validateState(); + + return $uri; + } + + public function getScheme(): string + { + return $this->scheme; + } + + public function getAuthority(): string + { + $authority = $this->host; + if ($this->userInfo !== '') { + $authority = $this->userInfo.'@'.$authority; + } + + if ($this->port !== null) { + $authority .= ':'.$this->port; + } + + return $authority; + } + + public function getUserInfo(): string + { + return $this->userInfo; + } + + public function getHost(): string + { + return $this->host; + } + + public function getPort(): ?int + { + return $this->port; + } + + public function getPath(): string + { + return $this->path; + } + + public function getQuery(): string + { + return $this->query; + } + + public function getFragment(): string + { + return $this->fragment; + } + + public function withScheme($scheme): UriInterface + { + $scheme = $this->filterScheme($scheme); + + if ($this->scheme === $scheme) { + return $this; + } + + $new = clone $this; + $new->scheme = $scheme; + $new->composedComponents = null; + $new->removeDefaultPort(); + $new->validateState(); + + return $new; + } + + public function withUserInfo($user, $password = null): UriInterface + { + $info = $this->filterUserInfoComponent($user); + if ($password !== null) { + $info .= ':'.$this->filterUserInfoComponent($password); + } + + if ($this->userInfo === $info) { + return $this; + } + + $new = clone $this; + $new->userInfo = $info; + $new->composedComponents = null; + $new->validateState(); + + return $new; + } + + public function withHost($host): UriInterface + { + $host = $this->filterHost($host); + + if ($this->host === $host) { + return $this; + } + + $new = clone $this; + $new->host = $host; + $new->composedComponents = null; + $new->validateState(); + + return $new; + } + + public function withPort($port): UriInterface + { + $port = $this->filterPort($port); + + if ($this->port === $port) { + return $this; + } + + $new = clone $this; + $new->port = $port; + $new->composedComponents = null; + $new->removeDefaultPort(); + $new->validateState(); + + return $new; + } + + public function withPath($path): UriInterface + { + $path = $this->filterPath($path); + + if ($this->path === $path) { + return $this; + } + + $new = clone $this; + $new->path = $path; + $new->composedComponents = null; + $new->validateState(); + + return $new; + } + + public function withQuery($query): UriInterface + { + $query = $this->filterQueryAndFragment($query); + + if ($this->query === $query) { + return $this; + } + + $new = clone $this; + $new->query = $query; + $new->composedComponents = null; + + return $new; + } + + public function withFragment($fragment): UriInterface + { + $fragment = $this->filterQueryAndFragment($fragment); + + if ($this->fragment === $fragment) { + return $this; + } + + $new = clone $this; + $new->fragment = $fragment; + $new->composedComponents = null; + + return $new; + } + + public function jsonSerialize(): string + { + return $this->__toString(); + } + + /** + * Apply parse_url parts to a URI. + * + * @param array $parts Array of parse_url parts to apply. + */ + private function applyParts(array $parts): void + { + $this->scheme = isset($parts['scheme']) + ? $this->filterScheme($parts['scheme']) + : ''; + $this->userInfo = isset($parts['user']) + ? $this->filterUserInfoComponent($parts['user']) + : ''; + $this->host = isset($parts['host']) + ? $this->filterHost($parts['host']) + : ''; + $this->port = isset($parts['port']) + ? $this->filterPort($parts['port']) + : null; + $this->path = isset($parts['path']) + ? $this->filterPath($parts['path']) + : ''; + $this->query = isset($parts['query']) + ? $this->filterQueryAndFragment($parts['query']) + : ''; + $this->fragment = isset($parts['fragment']) + ? $this->filterQueryAndFragment($parts['fragment']) + : ''; + if (isset($parts['pass'])) { + $this->userInfo .= ':'.$this->filterUserInfoComponent($parts['pass']); + } + + $this->removeDefaultPort(); + } + + /** + * @param mixed $scheme + * + * @throws \InvalidArgumentException If the scheme is invalid. + */ + private function filterScheme($scheme): string + { + if (!is_string($scheme)) { + throw new \InvalidArgumentException('Scheme must be a string'); + } + + return \strtr($scheme, 'ABCDEFGHIJKLMNOPQRSTUVWXYZ', 'abcdefghijklmnopqrstuvwxyz'); + } + + /** + * @param mixed $component + * + * @throws \InvalidArgumentException If the user info is invalid. + */ + private function filterUserInfoComponent($component): string + { + if (!is_string($component)) { + throw new \InvalidArgumentException('User info must be a string'); + } + + return preg_replace_callback( + '/(?:[^%'.self::CHAR_UNRESERVED.self::CHAR_SUB_DELIMS.']+|%(?![A-Fa-f0-9]{2}))/', + [$this, 'rawurlencodeMatchZero'], + $component + ); + } + + /** + * @param mixed $host + * + * @throws \InvalidArgumentException If the host is invalid. + */ + private function filterHost($host): string + { + if (!is_string($host)) { + throw new \InvalidArgumentException('Host must be a string'); + } + + return \strtr($host, 'ABCDEFGHIJKLMNOPQRSTUVWXYZ', 'abcdefghijklmnopqrstuvwxyz'); + } + + /** + * @param mixed $port + * + * @throws \InvalidArgumentException If the port is invalid. + */ + private function filterPort($port): ?int + { + if ($port === null) { + return null; + } + + $port = (int) $port; + if (0 > $port || 0xFFFF < $port) { + throw new \InvalidArgumentException( + sprintf('Invalid port: %d. Must be between 0 and 65535', $port) + ); + } + + return $port; + } + + /** + * @param (string|int)[] $keys + * + * @return string[] + */ + private static function getFilteredQueryString(UriInterface $uri, array $keys): array + { + $current = $uri->getQuery(); + + if ($current === '') { + return []; + } + + $decodedKeys = array_map(function ($k): string { + return rawurldecode((string) $k); + }, $keys); + + return array_filter(explode('&', $current), function ($part) use ($decodedKeys) { + return !in_array(rawurldecode(explode('=', $part)[0]), $decodedKeys, true); + }); + } + + private static function generateQueryString(string $key, ?string $value): string + { + // Query string separators ("=", "&") within the key or value need to be encoded + // (while preventing double-encoding) before setting the query string. All other + // chars that need percent-encoding will be encoded by withQuery(). + $queryString = strtr($key, self::QUERY_SEPARATORS_REPLACEMENT); + + if ($value !== null) { + $queryString .= '='.strtr($value, self::QUERY_SEPARATORS_REPLACEMENT); + } + + return $queryString; + } + + private function removeDefaultPort(): void + { + if ($this->port !== null && self::isDefaultPort($this)) { + $this->port = null; + } + } + + /** + * Filters the path of a URI + * + * @param mixed $path + * + * @throws \InvalidArgumentException If the path is invalid. + */ + private function filterPath($path): string + { + if (!is_string($path)) { + throw new \InvalidArgumentException('Path must be a string'); + } + + return preg_replace_callback( + '/(?:[^'.self::CHAR_UNRESERVED.self::CHAR_SUB_DELIMS.'%:@\/]++|%(?![A-Fa-f0-9]{2}))/', + [$this, 'rawurlencodeMatchZero'], + $path + ); + } + + /** + * Filters the query string or fragment of a URI. + * + * @param mixed $str + * + * @throws \InvalidArgumentException If the query or fragment is invalid. + */ + private function filterQueryAndFragment($str): string + { + if (!is_string($str)) { + throw new \InvalidArgumentException('Query and fragment must be a string'); + } + + return preg_replace_callback( + '/(?:[^'.self::CHAR_UNRESERVED.self::CHAR_SUB_DELIMS.'%:@\/\?]++|%(?![A-Fa-f0-9]{2}))/', + [$this, 'rawurlencodeMatchZero'], + $str + ); + } + + private function rawurlencodeMatchZero(array $match): string + { + return rawurlencode($match[0]); + } + + private function validateState(): void + { + if ($this->host === '' && ($this->scheme === 'http' || $this->scheme === 'https')) { + $this->host = self::HTTP_DEFAULT_HOST; + } + + if ($this->getAuthority() === '') { + if (0 === strpos($this->path, '//')) { + throw new MalformedUriException('The path of a URI without an authority must not start with two slashes "//"'); + } + if ($this->scheme === '' && false !== strpos(explode('/', $this->path, 2)[0], ':')) { + throw new MalformedUriException('A relative URI must not have a path beginning with a segment containing a colon'); + } + } + } +} diff --git a/3rdparty/guzzlehttp/psr7/src/UriComparator.php b/3rdparty/guzzlehttp/psr7/src/UriComparator.php new file mode 100644 index 00000000..70c582aa --- /dev/null +++ b/3rdparty/guzzlehttp/psr7/src/UriComparator.php @@ -0,0 +1,52 @@ +getHost(), $modified->getHost()) !== 0) { + return true; + } + + if ($original->getScheme() !== $modified->getScheme()) { + return true; + } + + if (self::computePort($original) !== self::computePort($modified)) { + return true; + } + + return false; + } + + private static function computePort(UriInterface $uri): int + { + $port = $uri->getPort(); + + if (null !== $port) { + return $port; + } + + return 'https' === $uri->getScheme() ? 443 : 80; + } + + private function __construct() + { + // cannot be instantiated + } +} diff --git a/3rdparty/guzzlehttp/psr7/src/UriNormalizer.php b/3rdparty/guzzlehttp/psr7/src/UriNormalizer.php new file mode 100644 index 00000000..e1745573 --- /dev/null +++ b/3rdparty/guzzlehttp/psr7/src/UriNormalizer.php @@ -0,0 +1,220 @@ +getPath() === '' + && ($uri->getScheme() === 'http' || $uri->getScheme() === 'https') + ) { + $uri = $uri->withPath('/'); + } + + if ($flags & self::REMOVE_DEFAULT_HOST && $uri->getScheme() === 'file' && $uri->getHost() === 'localhost') { + $uri = $uri->withHost(''); + } + + if ($flags & self::REMOVE_DEFAULT_PORT && $uri->getPort() !== null && Uri::isDefaultPort($uri)) { + $uri = $uri->withPort(null); + } + + if ($flags & self::REMOVE_DOT_SEGMENTS && !Uri::isRelativePathReference($uri)) { + $uri = $uri->withPath(UriResolver::removeDotSegments($uri->getPath())); + } + + if ($flags & self::REMOVE_DUPLICATE_SLASHES) { + $uri = $uri->withPath(preg_replace('#//++#', '/', $uri->getPath())); + } + + if ($flags & self::SORT_QUERY_PARAMETERS && $uri->getQuery() !== '') { + $queryKeyValues = explode('&', $uri->getQuery()); + sort($queryKeyValues); + $uri = $uri->withQuery(implode('&', $queryKeyValues)); + } + + return $uri; + } + + /** + * Whether two URIs can be considered equivalent. + * + * Both URIs are normalized automatically before comparison with the given $normalizations bitmask. The method also + * accepts relative URI references and returns true when they are equivalent. This of course assumes they will be + * resolved against the same base URI. If this is not the case, determination of equivalence or difference of + * relative references does not mean anything. + * + * @param UriInterface $uri1 An URI to compare + * @param UriInterface $uri2 An URI to compare + * @param int $normalizations A bitmask of normalizations to apply, see constants + * + * @see https://datatracker.ietf.org/doc/html/rfc3986#section-6.1 + */ + public static function isEquivalent(UriInterface $uri1, UriInterface $uri2, int $normalizations = self::PRESERVING_NORMALIZATIONS): bool + { + return (string) self::normalize($uri1, $normalizations) === (string) self::normalize($uri2, $normalizations); + } + + private static function capitalizePercentEncoding(UriInterface $uri): UriInterface + { + $regex = '/(?:%[A-Fa-f0-9]{2})++/'; + + $callback = function (array $match): string { + return strtoupper($match[0]); + }; + + return + $uri->withPath( + preg_replace_callback($regex, $callback, $uri->getPath()) + )->withQuery( + preg_replace_callback($regex, $callback, $uri->getQuery()) + ); + } + + private static function decodeUnreservedCharacters(UriInterface $uri): UriInterface + { + $regex = '/%(?:2D|2E|5F|7E|3[0-9]|[46][1-9A-F]|[57][0-9A])/i'; + + $callback = function (array $match): string { + return rawurldecode($match[0]); + }; + + return + $uri->withPath( + preg_replace_callback($regex, $callback, $uri->getPath()) + )->withQuery( + preg_replace_callback($regex, $callback, $uri->getQuery()) + ); + } + + private function __construct() + { + // cannot be instantiated + } +} diff --git a/3rdparty/guzzlehttp/psr7/src/UriResolver.php b/3rdparty/guzzlehttp/psr7/src/UriResolver.php new file mode 100644 index 00000000..3737be1e --- /dev/null +++ b/3rdparty/guzzlehttp/psr7/src/UriResolver.php @@ -0,0 +1,211 @@ +getScheme() != '') { + return $rel->withPath(self::removeDotSegments($rel->getPath())); + } + + if ($rel->getAuthority() != '') { + $targetAuthority = $rel->getAuthority(); + $targetPath = self::removeDotSegments($rel->getPath()); + $targetQuery = $rel->getQuery(); + } else { + $targetAuthority = $base->getAuthority(); + if ($rel->getPath() === '') { + $targetPath = $base->getPath(); + $targetQuery = $rel->getQuery() != '' ? $rel->getQuery() : $base->getQuery(); + } else { + if ($rel->getPath()[0] === '/') { + $targetPath = $rel->getPath(); + } else { + if ($targetAuthority != '' && $base->getPath() === '') { + $targetPath = '/'.$rel->getPath(); + } else { + $lastSlashPos = strrpos($base->getPath(), '/'); + if ($lastSlashPos === false) { + $targetPath = $rel->getPath(); + } else { + $targetPath = substr($base->getPath(), 0, $lastSlashPos + 1).$rel->getPath(); + } + } + } + $targetPath = self::removeDotSegments($targetPath); + $targetQuery = $rel->getQuery(); + } + } + + return new Uri(Uri::composeComponents( + $base->getScheme(), + $targetAuthority, + $targetPath, + $targetQuery, + $rel->getFragment() + )); + } + + /** + * Returns the target URI as a relative reference from the base URI. + * + * This method is the counterpart to resolve(): + * + * (string) $target === (string) UriResolver::resolve($base, UriResolver::relativize($base, $target)) + * + * One use-case is to use the current request URI as base URI and then generate relative links in your documents + * to reduce the document size or offer self-contained downloadable document archives. + * + * $base = new Uri('http://example.com/a/b/'); + * echo UriResolver::relativize($base, new Uri('http://example.com/a/b/c')); // prints 'c'. + * echo UriResolver::relativize($base, new Uri('http://example.com/a/x/y')); // prints '../x/y'. + * echo UriResolver::relativize($base, new Uri('http://example.com/a/b/?q')); // prints '?q'. + * echo UriResolver::relativize($base, new Uri('http://example.org/a/b/')); // prints '//example.org/a/b/'. + * + * This method also accepts a target that is already relative and will try to relativize it further. Only a + * relative-path reference will be returned as-is. + * + * echo UriResolver::relativize($base, new Uri('/a/b/c')); // prints 'c' as well + */ + public static function relativize(UriInterface $base, UriInterface $target): UriInterface + { + if ($target->getScheme() !== '' + && ($base->getScheme() !== $target->getScheme() || $target->getAuthority() === '' && $base->getAuthority() !== '') + ) { + return $target; + } + + if (Uri::isRelativePathReference($target)) { + // As the target is already highly relative we return it as-is. It would be possible to resolve + // the target with `$target = self::resolve($base, $target);` and then try make it more relative + // by removing a duplicate query. But let's not do that automatically. + return $target; + } + + if ($target->getAuthority() !== '' && $base->getAuthority() !== $target->getAuthority()) { + return $target->withScheme(''); + } + + // We must remove the path before removing the authority because if the path starts with two slashes, the URI + // would turn invalid. And we also cannot set a relative path before removing the authority, as that is also + // invalid. + $emptyPathUri = $target->withScheme('')->withPath('')->withUserInfo('')->withPort(null)->withHost(''); + + if ($base->getPath() !== $target->getPath()) { + return $emptyPathUri->withPath(self::getRelativePath($base, $target)); + } + + if ($base->getQuery() === $target->getQuery()) { + // Only the target fragment is left. And it must be returned even if base and target fragment are the same. + return $emptyPathUri->withQuery(''); + } + + // If the base URI has a query but the target has none, we cannot return an empty path reference as it would + // inherit the base query component when resolving. + if ($target->getQuery() === '') { + $segments = explode('/', $target->getPath()); + /** @var string $lastSegment */ + $lastSegment = end($segments); + + return $emptyPathUri->withPath($lastSegment === '' ? './' : $lastSegment); + } + + return $emptyPathUri; + } + + private static function getRelativePath(UriInterface $base, UriInterface $target): string + { + $sourceSegments = explode('/', $base->getPath()); + $targetSegments = explode('/', $target->getPath()); + array_pop($sourceSegments); + $targetLastSegment = array_pop($targetSegments); + foreach ($sourceSegments as $i => $segment) { + if (isset($targetSegments[$i]) && $segment === $targetSegments[$i]) { + unset($sourceSegments[$i], $targetSegments[$i]); + } else { + break; + } + } + $targetSegments[] = $targetLastSegment; + $relativePath = str_repeat('../', count($sourceSegments)).implode('/', $targetSegments); + + // A reference to am empty last segment or an empty first sub-segment must be prefixed with "./". + // This also applies to a segment with a colon character (e.g., "file:colon") that cannot be used + // as the first segment of a relative-path reference, as it would be mistaken for a scheme name. + if ('' === $relativePath || false !== strpos(explode('/', $relativePath, 2)[0], ':')) { + $relativePath = "./$relativePath"; + } elseif ('/' === $relativePath[0]) { + if ($base->getAuthority() != '' && $base->getPath() === '') { + // In this case an extra slash is added by resolve() automatically. So we must not add one here. + $relativePath = ".$relativePath"; + } else { + $relativePath = "./$relativePath"; + } + } + + return $relativePath; + } + + private function __construct() + { + // cannot be instantiated + } +} diff --git a/3rdparty/guzzlehttp/psr7/src/Utils.php b/3rdparty/guzzlehttp/psr7/src/Utils.php new file mode 100644 index 00000000..7682d2cd --- /dev/null +++ b/3rdparty/guzzlehttp/psr7/src/Utils.php @@ -0,0 +1,477 @@ + $v) { + if (!in_array(strtolower((string) $k), $keys)) { + $result[$k] = $v; + } + } + + return $result; + } + + /** + * Copy the contents of a stream into another stream until the given number + * of bytes have been read. + * + * @param StreamInterface $source Stream to read from + * @param StreamInterface $dest Stream to write to + * @param int $maxLen Maximum number of bytes to read. Pass -1 + * to read the entire stream. + * + * @throws \RuntimeException on error. + */ + public static function copyToStream(StreamInterface $source, StreamInterface $dest, int $maxLen = -1): void + { + $bufferSize = 8192; + + if ($maxLen === -1) { + while (!$source->eof()) { + if (!$dest->write($source->read($bufferSize))) { + break; + } + } + } else { + $remaining = $maxLen; + while ($remaining > 0 && !$source->eof()) { + $buf = $source->read(min($bufferSize, $remaining)); + $len = strlen($buf); + if (!$len) { + break; + } + $remaining -= $len; + $dest->write($buf); + } + } + } + + /** + * Copy the contents of a stream into a string until the given number of + * bytes have been read. + * + * @param StreamInterface $stream Stream to read + * @param int $maxLen Maximum number of bytes to read. Pass -1 + * to read the entire stream. + * + * @throws \RuntimeException on error. + */ + public static function copyToString(StreamInterface $stream, int $maxLen = -1): string + { + $buffer = ''; + + if ($maxLen === -1) { + while (!$stream->eof()) { + $buf = $stream->read(1048576); + if ($buf === '') { + break; + } + $buffer .= $buf; + } + + return $buffer; + } + + $len = 0; + while (!$stream->eof() && $len < $maxLen) { + $buf = $stream->read($maxLen - $len); + if ($buf === '') { + break; + } + $buffer .= $buf; + $len = strlen($buffer); + } + + return $buffer; + } + + /** + * Calculate a hash of a stream. + * + * This method reads the entire stream to calculate a rolling hash, based + * on PHP's `hash_init` functions. + * + * @param StreamInterface $stream Stream to calculate the hash for + * @param string $algo Hash algorithm (e.g. md5, crc32, etc) + * @param bool $rawOutput Whether or not to use raw output + * + * @throws \RuntimeException on error. + */ + public static function hash(StreamInterface $stream, string $algo, bool $rawOutput = false): string + { + $pos = $stream->tell(); + + if ($pos > 0) { + $stream->rewind(); + } + + $ctx = hash_init($algo); + while (!$stream->eof()) { + hash_update($ctx, $stream->read(1048576)); + } + + $out = hash_final($ctx, $rawOutput); + $stream->seek($pos); + + return $out; + } + + /** + * Clone and modify a request with the given changes. + * + * This method is useful for reducing the number of clones needed to mutate + * a message. + * + * The changes can be one of: + * - method: (string) Changes the HTTP method. + * - set_headers: (array) Sets the given headers. + * - remove_headers: (array) Remove the given headers. + * - body: (mixed) Sets the given body. + * - uri: (UriInterface) Set the URI. + * - query: (string) Set the query string value of the URI. + * - version: (string) Set the protocol version. + * + * @param RequestInterface $request Request to clone and modify. + * @param array $changes Changes to apply. + */ + public static function modifyRequest(RequestInterface $request, array $changes): RequestInterface + { + if (!$changes) { + return $request; + } + + $headers = $request->getHeaders(); + + if (!isset($changes['uri'])) { + $uri = $request->getUri(); + } else { + // Remove the host header if one is on the URI + if ($host = $changes['uri']->getHost()) { + $changes['set_headers']['Host'] = $host; + + if ($port = $changes['uri']->getPort()) { + $standardPorts = ['http' => 80, 'https' => 443]; + $scheme = $changes['uri']->getScheme(); + if (isset($standardPorts[$scheme]) && $port != $standardPorts[$scheme]) { + $changes['set_headers']['Host'] .= ':'.$port; + } + } + } + $uri = $changes['uri']; + } + + if (!empty($changes['remove_headers'])) { + $headers = self::caselessRemove($changes['remove_headers'], $headers); + } + + if (!empty($changes['set_headers'])) { + $headers = self::caselessRemove(array_keys($changes['set_headers']), $headers); + $headers = $changes['set_headers'] + $headers; + } + + if (isset($changes['query'])) { + $uri = $uri->withQuery($changes['query']); + } + + if ($request instanceof ServerRequestInterface) { + $new = (new ServerRequest( + $changes['method'] ?? $request->getMethod(), + $uri, + $headers, + $changes['body'] ?? $request->getBody(), + $changes['version'] ?? $request->getProtocolVersion(), + $request->getServerParams() + )) + ->withParsedBody($request->getParsedBody()) + ->withQueryParams($request->getQueryParams()) + ->withCookieParams($request->getCookieParams()) + ->withUploadedFiles($request->getUploadedFiles()); + + foreach ($request->getAttributes() as $key => $value) { + $new = $new->withAttribute($key, $value); + } + + return $new; + } + + return new Request( + $changes['method'] ?? $request->getMethod(), + $uri, + $headers, + $changes['body'] ?? $request->getBody(), + $changes['version'] ?? $request->getProtocolVersion() + ); + } + + /** + * Read a line from the stream up to the maximum allowed buffer length. + * + * @param StreamInterface $stream Stream to read from + * @param int|null $maxLength Maximum buffer length + */ + public static function readLine(StreamInterface $stream, ?int $maxLength = null): string + { + $buffer = ''; + $size = 0; + + while (!$stream->eof()) { + if ('' === ($byte = $stream->read(1))) { + return $buffer; + } + $buffer .= $byte; + // Break when a new line is found or the max length - 1 is reached + if ($byte === "\n" || ++$size === $maxLength - 1) { + break; + } + } + + return $buffer; + } + + /** + * Redact the password in the user info part of a URI. + */ + public static function redactUserInfo(UriInterface $uri): UriInterface + { + $userInfo = $uri->getUserInfo(); + + if (false !== ($pos = \strpos($userInfo, ':'))) { + return $uri->withUserInfo(\substr($userInfo, 0, $pos), '***'); + } + + return $uri; + } + + /** + * Create a new stream based on the input type. + * + * Options is an associative array that can contain the following keys: + * - metadata: Array of custom metadata. + * - size: Size of the stream. + * + * This method accepts the following `$resource` types: + * - `Psr\Http\Message\StreamInterface`: Returns the value as-is. + * - `string`: Creates a stream object that uses the given string as the contents. + * - `resource`: Creates a stream object that wraps the given PHP stream resource. + * - `Iterator`: If the provided value implements `Iterator`, then a read-only + * stream object will be created that wraps the given iterable. Each time the + * stream is read from, data from the iterator will fill a buffer and will be + * continuously called until the buffer is equal to the requested read size. + * Subsequent read calls will first read from the buffer and then call `next` + * on the underlying iterator until it is exhausted. + * - `object` with `__toString()`: If the object has the `__toString()` method, + * the object will be cast to a string and then a stream will be returned that + * uses the string value. + * - `NULL`: When `null` is passed, an empty stream object is returned. + * - `callable` When a callable is passed, a read-only stream object will be + * created that invokes the given callable. The callable is invoked with the + * number of suggested bytes to read. The callable can return any number of + * bytes, but MUST return `false` when there is no more data to return. The + * stream object that wraps the callable will invoke the callable until the + * number of requested bytes are available. Any additional bytes will be + * buffered and used in subsequent reads. + * + * @param resource|string|int|float|bool|StreamInterface|callable|\Iterator|null $resource Entity body data + * @param array{size?: int, metadata?: array} $options Additional options + * + * @throws \InvalidArgumentException if the $resource arg is not valid. + */ + public static function streamFor($resource = '', array $options = []): StreamInterface + { + if (is_scalar($resource)) { + $stream = self::tryFopen('php://temp', 'r+'); + if ($resource !== '') { + fwrite($stream, (string) $resource); + fseek($stream, 0); + } + + return new Stream($stream, $options); + } + + switch (gettype($resource)) { + case 'resource': + /* + * The 'php://input' is a special stream with quirks and inconsistencies. + * We avoid using that stream by reading it into php://temp + */ + + /** @var resource $resource */ + if ((\stream_get_meta_data($resource)['uri'] ?? '') === 'php://input') { + $stream = self::tryFopen('php://temp', 'w+'); + stream_copy_to_stream($resource, $stream); + fseek($stream, 0); + $resource = $stream; + } + + return new Stream($resource, $options); + case 'object': + /** @var object $resource */ + if ($resource instanceof StreamInterface) { + return $resource; + } elseif ($resource instanceof \Iterator) { + return new PumpStream(function () use ($resource) { + if (!$resource->valid()) { + return false; + } + $result = $resource->current(); + $resource->next(); + + return $result; + }, $options); + } elseif (method_exists($resource, '__toString')) { + return self::streamFor((string) $resource, $options); + } + break; + case 'NULL': + return new Stream(self::tryFopen('php://temp', 'r+'), $options); + } + + if (is_callable($resource)) { + return new PumpStream($resource, $options); + } + + throw new \InvalidArgumentException('Invalid resource type: '.gettype($resource)); + } + + /** + * Safely opens a PHP stream resource using a filename. + * + * When fopen fails, PHP normally raises a warning. This function adds an + * error handler that checks for errors and throws an exception instead. + * + * @param string $filename File to open + * @param string $mode Mode used to open the file + * + * @return resource + * + * @throws \RuntimeException if the file cannot be opened + */ + public static function tryFopen(string $filename, string $mode) + { + $ex = null; + set_error_handler(static function (int $errno, string $errstr) use ($filename, $mode, &$ex): bool { + $ex = new \RuntimeException(sprintf( + 'Unable to open "%s" using mode "%s": %s', + $filename, + $mode, + $errstr + )); + + return true; + }); + + try { + /** @var resource $handle */ + $handle = fopen($filename, $mode); + } catch (\Throwable $e) { + $ex = new \RuntimeException(sprintf( + 'Unable to open "%s" using mode "%s": %s', + $filename, + $mode, + $e->getMessage() + ), 0, $e); + } + + restore_error_handler(); + + if ($ex) { + /** @var $ex \RuntimeException */ + throw $ex; + } + + return $handle; + } + + /** + * Safely gets the contents of a given stream. + * + * When stream_get_contents fails, PHP normally raises a warning. This + * function adds an error handler that checks for errors and throws an + * exception instead. + * + * @param resource $stream + * + * @throws \RuntimeException if the stream cannot be read + */ + public static function tryGetContents($stream): string + { + $ex = null; + set_error_handler(static function (int $errno, string $errstr) use (&$ex): bool { + $ex = new \RuntimeException(sprintf( + 'Unable to read stream contents: %s', + $errstr + )); + + return true; + }); + + try { + /** @var string|false $contents */ + $contents = stream_get_contents($stream); + + if ($contents === false) { + $ex = new \RuntimeException('Unable to read stream contents'); + } + } catch (\Throwable $e) { + $ex = new \RuntimeException(sprintf( + 'Unable to read stream contents: %s', + $e->getMessage() + ), 0, $e); + } + + restore_error_handler(); + + if ($ex) { + /** @var $ex \RuntimeException */ + throw $ex; + } + + return $contents; + } + + /** + * Returns a UriInterface for the given value. + * + * This function accepts a string or UriInterface and returns a + * UriInterface for the given value. If the value is already a + * UriInterface, it is returned as-is. + * + * @param string|UriInterface $uri + * + * @throws \InvalidArgumentException + */ + public static function uriFor($uri): UriInterface + { + if ($uri instanceof UriInterface) { + return $uri; + } + + if (is_string($uri)) { + return new Uri($uri); + } + + throw new \InvalidArgumentException('URI must be a string or UriInterface'); + } +} diff --git a/3rdparty/guzzlehttp/uri-template/LICENSE b/3rdparty/guzzlehttp/uri-template/LICENSE new file mode 100644 index 00000000..e70120fc --- /dev/null +++ b/3rdparty/guzzlehttp/uri-template/LICENSE @@ -0,0 +1,23 @@ +The MIT License (MIT) + +Copyright (c) 2014 Michael Dowling +Copyright (c) 2020 George Mponos +Copyright (c) 2020 Graham Campbell + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in +all copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +THE SOFTWARE. diff --git a/3rdparty/guzzlehttp/uri-template/src/UriTemplate.php b/3rdparty/guzzlehttp/uri-template/src/UriTemplate.php new file mode 100644 index 00000000..e848cd3b --- /dev/null +++ b/3rdparty/guzzlehttp/uri-template/src/UriTemplate.php @@ -0,0 +1,295 @@ + Hash for quick operator lookups + */ + private static $operatorHash = [ + '' => ['prefix' => '', 'joiner' => ',', 'query' => false], + '+' => ['prefix' => '', 'joiner' => ',', 'query' => false], + '#' => ['prefix' => '#', 'joiner' => ',', 'query' => false], + '.' => ['prefix' => '.', 'joiner' => '.', 'query' => false], + '/' => ['prefix' => '/', 'joiner' => '/', 'query' => false], + ';' => ['prefix' => ';', 'joiner' => ';', 'query' => true], + '?' => ['prefix' => '?', 'joiner' => '&', 'query' => true], + '&' => ['prefix' => '&', 'joiner' => '&', 'query' => true], + ]; + + /** + * @var string[] Delimiters + */ + private static $delims = [ + ':', + '/', + '?', + '#', + '[', + ']', + '@', + '!', + '$', + '&', + '\'', + '(', + ')', + '*', + '+', + ',', + ';', + '=', + ]; + + /** + * @var string[] Percent encoded delimiters + */ + private static $delimsPct = [ + '%3A', + '%2F', + '%3F', + '%23', + '%5B', + '%5D', + '%40', + '%21', + '%24', + '%26', + '%27', + '%28', + '%29', + '%2A', + '%2B', + '%2C', + '%3B', + '%3D', + ]; + + /** + * @param array $variables Variables to use in the template expansion + * + * @throws \RuntimeException + */ + public static function expand(string $template, array $variables): string + { + if (false === \strpos($template, '{')) { + return $template; + } + + /** @var string|null */ + $result = \preg_replace_callback( + '/\{([^\}]+)\}/', + self::expandMatchCallback($variables), + $template + ); + + if (null === $result) { + throw new \RuntimeException(\sprintf('Unable to process template: %s', \preg_last_error_msg())); + } + + return $result; + } + + /** + * @param array $variables Variables to use in the template expansion + * + * @return callable(string[]): string + */ + private static function expandMatchCallback(array $variables): callable + { + return static function (array $matches) use ($variables): string { + return self::expandMatch($matches, $variables); + }; + } + + /** + * Process an expansion + * + * @param array $variables Variables to use in the template expansion + * @param string[] $matches Matches met in the preg_replace_callback + * + * @return string Returns the replacement string + */ + private static function expandMatch(array $matches, array $variables): string + { + $replacements = []; + $parsed = self::parseExpression($matches[1]); + $prefix = self::$operatorHash[$parsed['operator']]['prefix']; + $joiner = self::$operatorHash[$parsed['operator']]['joiner']; + $useQuery = self::$operatorHash[$parsed['operator']]['query']; + $allUndefined = true; + + foreach ($parsed['values'] as $value) { + if (!isset($variables[$value['value']])) { + continue; + } + + $variable = $variables[$value['value']]; + $actuallyUseQuery = $useQuery; + $expanded = ''; + + if (\is_array($variable)) { + $isAssoc = self::isAssoc($variable); + $kvp = []; + /** @var mixed $var */ + foreach ($variable as $key => $var) { + if ($isAssoc) { + $key = \rawurlencode((string) $key); + $isNestedArray = \is_array($var); + } else { + $isNestedArray = false; + } + + if (!$isNestedArray) { + $var = \rawurlencode((string) $var); + if ($parsed['operator'] === '+' || $parsed['operator'] === '#') { + $var = self::decodeReserved($var); + } + } + + if ($value['modifier'] === '*') { + if ($isAssoc) { + if ($isNestedArray) { + // Nested arrays must allow for deeply nested structures. + $var = \http_build_query([$key => $var], '', '&', \PHP_QUERY_RFC3986); + } else { + $var = \sprintf('%s=%s', (string) $key, (string) $var); + } + } elseif ($key > 0 && $actuallyUseQuery) { + $var = \sprintf('%s=%s', $value['value'], (string) $var); + } + } + + /** @var string $var */ + $kvp[$key] = $var; + } + + if (0 === \count($variable)) { + $actuallyUseQuery = false; + } elseif ($value['modifier'] === '*') { + $expanded = \implode($joiner, $kvp); + if ($isAssoc) { + // Don't prepend the value name when using the explode + // modifier with an associative array. + $actuallyUseQuery = false; + } + } else { + if ($isAssoc) { + // When an associative array is encountered and the + // explode modifier is not set, then the result must be + // a comma separated list of keys followed by their + // respective values. + foreach ($kvp as $k => &$v) { + $v = \sprintf('%s,%s', $k, $v); + } + } + $expanded = \implode(',', $kvp); + } + } else { + $allUndefined = false; + if ($value['modifier'] === ':' && isset($value['position'])) { + $variable = \substr((string) $variable, 0, $value['position']); + } + $expanded = \rawurlencode((string) $variable); + if ($parsed['operator'] === '+' || $parsed['operator'] === '#') { + $expanded = self::decodeReserved($expanded); + } + } + + if ($actuallyUseQuery) { + if ($expanded === '' && $joiner !== '&') { + $expanded = $value['value']; + } else { + $expanded = \sprintf('%s=%s', $value['value'], $expanded); + } + } + + $replacements[] = $expanded; + } + + $ret = \implode($joiner, $replacements); + + if ('' === $ret) { + // Spec section 3.2.4 and 3.2.5 + if (false === $allUndefined && ('#' === $prefix || '.' === $prefix)) { + return $prefix; + } + } else { + if ('' !== $prefix) { + return \sprintf('%s%s', $prefix, $ret); + } + } + + return $ret; + } + + /** + * Parse an expression into parts + * + * @param string $expression Expression to parse + * + * @return array{operator:string, values:array} + */ + private static function parseExpression(string $expression): array + { + $result = []; + + if (isset(self::$operatorHash[$expression[0]])) { + $result['operator'] = $expression[0]; + /** @var string */ + $expression = \substr($expression, 1); + } else { + $result['operator'] = ''; + } + + $result['values'] = []; + foreach (\explode(',', $expression) as $value) { + $value = \trim($value); + $varspec = []; + if ($colonPos = \strpos($value, ':')) { + $varspec['value'] = (string) \substr($value, 0, $colonPos); + $varspec['modifier'] = ':'; + $varspec['position'] = (int) \substr($value, $colonPos + 1); + } elseif (\substr($value, -1) === '*') { + $varspec['modifier'] = '*'; + $varspec['value'] = (string) \substr($value, 0, -1); + } else { + $varspec['value'] = $value; + $varspec['modifier'] = ''; + } + $result['values'][] = $varspec; + } + + return $result; + } + + /** + * Determines if an array is associative. + * + * This makes the assumption that input arrays are sequences or hashes. + * This assumption is a tradeoff for accuracy in favor of speed, but it + * should work in almost every case where input is supplied for a URI + * template. + */ + private static function isAssoc(array $array): bool + { + return $array && \array_keys($array)[0] !== 0; + } + + /** + * Removes percent encoding on reserved characters (used with + and # + * modifiers). + */ + private static function decodeReserved(string $string): string + { + return \str_replace(self::$delimsPct, self::$delims, $string); + } +} diff --git a/3rdparty/icewind/searchdav/LICENSE b/3rdparty/icewind/searchdav/LICENSE new file mode 100644 index 00000000..dbbe3558 --- /dev/null +++ b/3rdparty/icewind/searchdav/LICENSE @@ -0,0 +1,661 @@ + GNU AFFERO GENERAL PUBLIC LICENSE + Version 3, 19 November 2007 + + Copyright (C) 2007 Free Software Foundation, Inc. + Everyone is permitted to copy and distribute verbatim copies + of this license document, but changing it is not allowed. + + Preamble + + The GNU Affero General Public License is a free, copyleft license for +software and other kinds of works, specifically designed to ensure +cooperation with the community in the case of network server software. + + The licenses for most software and other practical works are designed +to take away your freedom to share and change the works. By contrast, +our General Public Licenses are intended to guarantee your freedom to +share and change all versions of a program--to make sure it remains free +software for all its users. + + When we speak of free software, we are referring to freedom, not +price. Our General Public Licenses are designed to make sure that you +have the freedom to distribute copies of free software (and charge for +them if you wish), that you receive source code or can get it if you +want it, that you can change the software or use pieces of it in new +free programs, and that you know you can do these things. + + Developers that use our General Public Licenses protect your rights +with two steps: (1) assert copyright on the software, and (2) offer +you this License which gives you legal permission to copy, distribute +and/or modify the software. + + A secondary benefit of defending all users' freedom is that +improvements made in alternate versions of the program, if they +receive widespread use, become available for other developers to +incorporate. Many developers of free software are heartened and +encouraged by the resulting cooperation. However, in the case of +software used on network servers, this result may fail to come about. +The GNU General Public License permits making a modified version and +letting the public access it on a server without ever releasing its +source code to the public. + + The GNU Affero General Public License is designed specifically to +ensure that, in such cases, the modified source code becomes available +to the community. It requires the operator of a network server to +provide the source code of the modified version running there to the +users of that server. Therefore, public use of a modified version, on +a publicly accessible server, gives the public access to the source +code of the modified version. + + An older license, called the Affero General Public License and +published by Affero, was designed to accomplish similar goals. This is +a different license, not a version of the Affero GPL, but Affero has +released a new version of the Affero GPL which permits relicensing under +this license. + + The precise terms and conditions for copying, distribution and +modification follow. + + TERMS AND CONDITIONS + + 0. Definitions. + + "This License" refers to version 3 of the GNU Affero General Public License. + + "Copyright" also means copyright-like laws that apply to other kinds of +works, such as semiconductor masks. + + "The Program" refers to any copyrightable work licensed under this +License. Each licensee is addressed as "you". "Licensees" and +"recipients" may be individuals or organizations. + + To "modify" a work means to copy from or adapt all or part of the work +in a fashion requiring copyright permission, other than the making of an +exact copy. The resulting work is called a "modified version" of the +earlier work or a work "based on" the earlier work. + + A "covered work" means either the unmodified Program or a work based +on the Program. + + To "propagate" a work means to do anything with it that, without +permission, would make you directly or secondarily liable for +infringement under applicable copyright law, except executing it on a +computer or modifying a private copy. Propagation includes copying, +distribution (with or without modification), making available to the +public, and in some countries other activities as well. + + To "convey" a work means any kind of propagation that enables other +parties to make or receive copies. Mere interaction with a user through +a computer network, with no transfer of a copy, is not conveying. + + An interactive user interface displays "Appropriate Legal Notices" +to the extent that it includes a convenient and prominently visible +feature that (1) displays an appropriate copyright notice, and (2) +tells the user that there is no warranty for the work (except to the +extent that warranties are provided), that licensees may convey the +work under this License, and how to view a copy of this License. If +the interface presents a list of user commands or options, such as a +menu, a prominent item in the list meets this criterion. + + 1. Source Code. + + The "source code" for a work means the preferred form of the work +for making modifications to it. "Object code" means any non-source +form of a work. + + A "Standard Interface" means an interface that either is an official +standard defined by a recognized standards body, or, in the case of +interfaces specified for a particular programming language, one that +is widely used among developers working in that language. + + The "System Libraries" of an executable work include anything, other +than the work as a whole, that (a) is included in the normal form of +packaging a Major Component, but which is not part of that Major +Component, and (b) serves only to enable use of the work with that +Major Component, or to implement a Standard Interface for which an +implementation is available to the public in source code form. A +"Major Component", in this context, means a major essential component +(kernel, window system, and so on) of the specific operating system +(if any) on which the executable work runs, or a compiler used to +produce the work, or an object code interpreter used to run it. + + The "Corresponding Source" for a work in object code form means all +the source code needed to generate, install, and (for an executable +work) run the object code and to modify the work, including scripts to +control those activities. However, it does not include the work's +System Libraries, or general-purpose tools or generally available free +programs which are used unmodified in performing those activities but +which are not part of the work. For example, Corresponding Source +includes interface definition files associated with source files for +the work, and the source code for shared libraries and dynamically +linked subprograms that the work is specifically designed to require, +such as by intimate data communication or control flow between those +subprograms and other parts of the work. + + The Corresponding Source need not include anything that users +can regenerate automatically from other parts of the Corresponding +Source. + + The Corresponding Source for a work in source code form is that +same work. + + 2. Basic Permissions. + + All rights granted under this License are granted for the term of +copyright on the Program, and are irrevocable provided the stated +conditions are met. This License explicitly affirms your unlimited +permission to run the unmodified Program. The output from running a +covered work is covered by this License only if the output, given its +content, constitutes a covered work. This License acknowledges your +rights of fair use or other equivalent, as provided by copyright law. + + You may make, run and propagate covered works that you do not +convey, without conditions so long as your license otherwise remains +in force. You may convey covered works to others for the sole purpose +of having them make modifications exclusively for you, or provide you +with facilities for running those works, provided that you comply with +the terms of this License in conveying all material for which you do +not control copyright. Those thus making or running the covered works +for you must do so exclusively on your behalf, under your direction +and control, on terms that prohibit them from making any copies of +your copyrighted material outside their relationship with you. + + Conveying under any other circumstances is permitted solely under +the conditions stated below. Sublicensing is not allowed; section 10 +makes it unnecessary. + + 3. Protecting Users' Legal Rights From Anti-Circumvention Law. + + No covered work shall be deemed part of an effective technological +measure under any applicable law fulfilling obligations under article +11 of the WIPO copyright treaty adopted on 20 December 1996, or +similar laws prohibiting or restricting circumvention of such +measures. + + When you convey a covered work, you waive any legal power to forbid +circumvention of technological measures to the extent such circumvention +is effected by exercising rights under this License with respect to +the covered work, and you disclaim any intention to limit operation or +modification of the work as a means of enforcing, against the work's +users, your or third parties' legal rights to forbid circumvention of +technological measures. + + 4. Conveying Verbatim Copies. + + You may convey verbatim copies of the Program's source code as you +receive it, in any medium, provided that you conspicuously and +appropriately publish on each copy an appropriate copyright notice; +keep intact all notices stating that this License and any +non-permissive terms added in accord with section 7 apply to the code; +keep intact all notices of the absence of any warranty; and give all +recipients a copy of this License along with the Program. + + You may charge any price or no price for each copy that you convey, +and you may offer support or warranty protection for a fee. + + 5. Conveying Modified Source Versions. + + You may convey a work based on the Program, or the modifications to +produce it from the Program, in the form of source code under the +terms of section 4, provided that you also meet all of these conditions: + + a) The work must carry prominent notices stating that you modified + it, and giving a relevant date. + + b) The work must carry prominent notices stating that it is + released under this License and any conditions added under section + 7. This requirement modifies the requirement in section 4 to + "keep intact all notices". + + c) You must license the entire work, as a whole, under this + License to anyone who comes into possession of a copy. This + License will therefore apply, along with any applicable section 7 + additional terms, to the whole of the work, and all its parts, + regardless of how they are packaged. This License gives no + permission to license the work in any other way, but it does not + invalidate such permission if you have separately received it. + + d) If the work has interactive user interfaces, each must display + Appropriate Legal Notices; however, if the Program has interactive + interfaces that do not display Appropriate Legal Notices, your + work need not make them do so. + + A compilation of a covered work with other separate and independent +works, which are not by their nature extensions of the covered work, +and which are not combined with it such as to form a larger program, +in or on a volume of a storage or distribution medium, is called an +"aggregate" if the compilation and its resulting copyright are not +used to limit the access or legal rights of the compilation's users +beyond what the individual works permit. Inclusion of a covered work +in an aggregate does not cause this License to apply to the other +parts of the aggregate. + + 6. Conveying Non-Source Forms. + + You may convey a covered work in object code form under the terms +of sections 4 and 5, provided that you also convey the +machine-readable Corresponding Source under the terms of this License, +in one of these ways: + + a) Convey the object code in, or embodied in, a physical product + (including a physical distribution medium), accompanied by the + Corresponding Source fixed on a durable physical medium + customarily used for software interchange. + + b) Convey the object code in, or embodied in, a physical product + (including a physical distribution medium), accompanied by a + written offer, valid for at least three years and valid for as + long as you offer spare parts or customer support for that product + model, to give anyone who possesses the object code either (1) a + copy of the Corresponding Source for all the software in the + product that is covered by this License, on a durable physical + medium customarily used for software interchange, for a price no + more than your reasonable cost of physically performing this + conveying of source, or (2) access to copy the + Corresponding Source from a network server at no charge. + + c) Convey individual copies of the object code with a copy of the + written offer to provide the Corresponding Source. This + alternative is allowed only occasionally and noncommercially, and + only if you received the object code with such an offer, in accord + with subsection 6b. + + d) Convey the object code by offering access from a designated + place (gratis or for a charge), and offer equivalent access to the + Corresponding Source in the same way through the same place at no + further charge. You need not require recipients to copy the + Corresponding Source along with the object code. If the place to + copy the object code is a network server, the Corresponding Source + may be on a different server (operated by you or a third party) + that supports equivalent copying facilities, provided you maintain + clear directions next to the object code saying where to find the + Corresponding Source. Regardless of what server hosts the + Corresponding Source, you remain obligated to ensure that it is + available for as long as needed to satisfy these requirements. + + e) Convey the object code using peer-to-peer transmission, provided + you inform other peers where the object code and Corresponding + Source of the work are being offered to the general public at no + charge under subsection 6d. + + A separable portion of the object code, whose source code is excluded +from the Corresponding Source as a System Library, need not be +included in conveying the object code work. + + A "User Product" is either (1) a "consumer product", which means any +tangible personal property which is normally used for personal, family, +or household purposes, or (2) anything designed or sold for incorporation +into a dwelling. In determining whether a product is a consumer product, +doubtful cases shall be resolved in favor of coverage. For a particular +product received by a particular user, "normally used" refers to a +typical or common use of that class of product, regardless of the status +of the particular user or of the way in which the particular user +actually uses, or expects or is expected to use, the product. A product +is a consumer product regardless of whether the product has substantial +commercial, industrial or non-consumer uses, unless such uses represent +the only significant mode of use of the product. + + "Installation Information" for a User Product means any methods, +procedures, authorization keys, or other information required to install +and execute modified versions of a covered work in that User Product from +a modified version of its Corresponding Source. The information must +suffice to ensure that the continued functioning of the modified object +code is in no case prevented or interfered with solely because +modification has been made. + + If you convey an object code work under this section in, or with, or +specifically for use in, a User Product, and the conveying occurs as +part of a transaction in which the right of possession and use of the +User Product is transferred to the recipient in perpetuity or for a +fixed term (regardless of how the transaction is characterized), the +Corresponding Source conveyed under this section must be accompanied +by the Installation Information. But this requirement does not apply +if neither you nor any third party retains the ability to install +modified object code on the User Product (for example, the work has +been installed in ROM). + + The requirement to provide Installation Information does not include a +requirement to continue to provide support service, warranty, or updates +for a work that has been modified or installed by the recipient, or for +the User Product in which it has been modified or installed. Access to a +network may be denied when the modification itself materially and +adversely affects the operation of the network or violates the rules and +protocols for communication across the network. + + Corresponding Source conveyed, and Installation Information provided, +in accord with this section must be in a format that is publicly +documented (and with an implementation available to the public in +source code form), and must require no special password or key for +unpacking, reading or copying. + + 7. Additional Terms. + + "Additional permissions" are terms that supplement the terms of this +License by making exceptions from one or more of its conditions. +Additional permissions that are applicable to the entire Program shall +be treated as though they were included in this License, to the extent +that they are valid under applicable law. If additional permissions +apply only to part of the Program, that part may be used separately +under those permissions, but the entire Program remains governed by +this License without regard to the additional permissions. + + When you convey a copy of a covered work, you may at your option +remove any additional permissions from that copy, or from any part of +it. (Additional permissions may be written to require their own +removal in certain cases when you modify the work.) You may place +additional permissions on material, added by you to a covered work, +for which you have or can give appropriate copyright permission. + + Notwithstanding any other provision of this License, for material you +add to a covered work, you may (if authorized by the copyright holders of +that material) supplement the terms of this License with terms: + + a) Disclaiming warranty or limiting liability differently from the + terms of sections 15 and 16 of this License; or + + b) Requiring preservation of specified reasonable legal notices or + author attributions in that material or in the Appropriate Legal + Notices displayed by works containing it; or + + c) Prohibiting misrepresentation of the origin of that material, or + requiring that modified versions of such material be marked in + reasonable ways as different from the original version; or + + d) Limiting the use for publicity purposes of names of licensors or + authors of the material; or + + e) Declining to grant rights under trademark law for use of some + trade names, trademarks, or service marks; or + + f) Requiring indemnification of licensors and authors of that + material by anyone who conveys the material (or modified versions of + it) with contractual assumptions of liability to the recipient, for + any liability that these contractual assumptions directly impose on + those licensors and authors. + + All other non-permissive additional terms are considered "further +restrictions" within the meaning of section 10. If the Program as you +received it, or any part of it, contains a notice stating that it is +governed by this License along with a term that is a further +restriction, you may remove that term. If a license document contains +a further restriction but permits relicensing or conveying under this +License, you may add to a covered work material governed by the terms +of that license document, provided that the further restriction does +not survive such relicensing or conveying. + + If you add terms to a covered work in accord with this section, you +must place, in the relevant source files, a statement of the +additional terms that apply to those files, or a notice indicating +where to find the applicable terms. + + Additional terms, permissive or non-permissive, may be stated in the +form of a separately written license, or stated as exceptions; +the above requirements apply either way. + + 8. Termination. + + You may not propagate or modify a covered work except as expressly +provided under this License. Any attempt otherwise to propagate or +modify it is void, and will automatically terminate your rights under +this License (including any patent licenses granted under the third +paragraph of section 11). + + However, if you cease all violation of this License, then your +license from a particular copyright holder is reinstated (a) +provisionally, unless and until the copyright holder explicitly and +finally terminates your license, and (b) permanently, if the copyright +holder fails to notify you of the violation by some reasonable means +prior to 60 days after the cessation. + + Moreover, your license from a particular copyright holder is +reinstated permanently if the copyright holder notifies you of the +violation by some reasonable means, this is the first time you have +received notice of violation of this License (for any work) from that +copyright holder, and you cure the violation prior to 30 days after +your receipt of the notice. + + Termination of your rights under this section does not terminate the +licenses of parties who have received copies or rights from you under +this License. If your rights have been terminated and not permanently +reinstated, you do not qualify to receive new licenses for the same +material under section 10. + + 9. Acceptance Not Required for Having Copies. + + You are not required to accept this License in order to receive or +run a copy of the Program. Ancillary propagation of a covered work +occurring solely as a consequence of using peer-to-peer transmission +to receive a copy likewise does not require acceptance. However, +nothing other than this License grants you permission to propagate or +modify any covered work. These actions infringe copyright if you do +not accept this License. Therefore, by modifying or propagating a +covered work, you indicate your acceptance of this License to do so. + + 10. Automatic Licensing of Downstream Recipients. + + Each time you convey a covered work, the recipient automatically +receives a license from the original licensors, to run, modify and +propagate that work, subject to this License. You are not responsible +for enforcing compliance by third parties with this License. + + An "entity transaction" is a transaction transferring control of an +organization, or substantially all assets of one, or subdividing an +organization, or merging organizations. If propagation of a covered +work results from an entity transaction, each party to that +transaction who receives a copy of the work also receives whatever +licenses to the work the party's predecessor in interest had or could +give under the previous paragraph, plus a right to possession of the +Corresponding Source of the work from the predecessor in interest, if +the predecessor has it or can get it with reasonable efforts. + + You may not impose any further restrictions on the exercise of the +rights granted or affirmed under this License. For example, you may +not impose a license fee, royalty, or other charge for exercise of +rights granted under this License, and you may not initiate litigation +(including a cross-claim or counterclaim in a lawsuit) alleging that +any patent claim is infringed by making, using, selling, offering for +sale, or importing the Program or any portion of it. + + 11. Patents. + + A "contributor" is a copyright holder who authorizes use under this +License of the Program or a work on which the Program is based. The +work thus licensed is called the contributor's "contributor version". + + A contributor's "essential patent claims" are all patent claims +owned or controlled by the contributor, whether already acquired or +hereafter acquired, that would be infringed by some manner, permitted +by this License, of making, using, or selling its contributor version, +but do not include claims that would be infringed only as a +consequence of further modification of the contributor version. For +purposes of this definition, "control" includes the right to grant +patent sublicenses in a manner consistent with the requirements of +this License. + + Each contributor grants you a non-exclusive, worldwide, royalty-free +patent license under the contributor's essential patent claims, to +make, use, sell, offer for sale, import and otherwise run, modify and +propagate the contents of its contributor version. + + In the following three paragraphs, a "patent license" is any express +agreement or commitment, however denominated, not to enforce a patent +(such as an express permission to practice a patent or covenant not to +sue for patent infringement). To "grant" such a patent license to a +party means to make such an agreement or commitment not to enforce a +patent against the party. + + If you convey a covered work, knowingly relying on a patent license, +and the Corresponding Source of the work is not available for anyone +to copy, free of charge and under the terms of this License, through a +publicly available network server or other readily accessible means, +then you must either (1) cause the Corresponding Source to be so +available, or (2) arrange to deprive yourself of the benefit of the +patent license for this particular work, or (3) arrange, in a manner +consistent with the requirements of this License, to extend the patent +license to downstream recipients. "Knowingly relying" means you have +actual knowledge that, but for the patent license, your conveying the +covered work in a country, or your recipient's use of the covered work +in a country, would infringe one or more identifiable patents in that +country that you have reason to believe are valid. + + If, pursuant to or in connection with a single transaction or +arrangement, you convey, or propagate by procuring conveyance of, a +covered work, and grant a patent license to some of the parties +receiving the covered work authorizing them to use, propagate, modify +or convey a specific copy of the covered work, then the patent license +you grant is automatically extended to all recipients of the covered +work and works based on it. + + A patent license is "discriminatory" if it does not include within +the scope of its coverage, prohibits the exercise of, or is +conditioned on the non-exercise of one or more of the rights that are +specifically granted under this License. You may not convey a covered +work if you are a party to an arrangement with a third party that is +in the business of distributing software, under which you make payment +to the third party based on the extent of your activity of conveying +the work, and under which the third party grants, to any of the +parties who would receive the covered work from you, a discriminatory +patent license (a) in connection with copies of the covered work +conveyed by you (or copies made from those copies), or (b) primarily +for and in connection with specific products or compilations that +contain the covered work, unless you entered into that arrangement, +or that patent license was granted, prior to 28 March 2007. + + Nothing in this License shall be construed as excluding or limiting +any implied license or other defenses to infringement that may +otherwise be available to you under applicable patent law. + + 12. No Surrender of Others' Freedom. + + If conditions are imposed on you (whether by court order, agreement or +otherwise) that contradict the conditions of this License, they do not +excuse you from the conditions of this License. If you cannot convey a +covered work so as to satisfy simultaneously your obligations under this +License and any other pertinent obligations, then as a consequence you may +not convey it at all. For example, if you agree to terms that obligate you +to collect a royalty for further conveying from those to whom you convey +the Program, the only way you could satisfy both those terms and this +License would be to refrain entirely from conveying the Program. + + 13. Remote Network Interaction; Use with the GNU General Public License. + + Notwithstanding any other provision of this License, if you modify the +Program, your modified version must prominently offer all users +interacting with it remotely through a computer network (if your version +supports such interaction) an opportunity to receive the Corresponding +Source of your version by providing access to the Corresponding Source +from a network server at no charge, through some standard or customary +means of facilitating copying of software. This Corresponding Source +shall include the Corresponding Source for any work covered by version 3 +of the GNU General Public License that is incorporated pursuant to the +following paragraph. + + Notwithstanding any other provision of this License, you have +permission to link or combine any covered work with a work licensed +under version 3 of the GNU General Public License into a single +combined work, and to convey the resulting work. The terms of this +License will continue to apply to the part which is the covered work, +but the work with which it is combined will remain governed by version +3 of the GNU General Public License. + + 14. Revised Versions of this License. + + The Free Software Foundation may publish revised and/or new versions of +the GNU Affero General Public License from time to time. Such new versions +will be similar in spirit to the present version, but may differ in detail to +address new problems or concerns. + + Each version is given a distinguishing version number. If the +Program specifies that a certain numbered version of the GNU Affero General +Public License "or any later version" applies to it, you have the +option of following the terms and conditions either of that numbered +version or of any later version published by the Free Software +Foundation. If the Program does not specify a version number of the +GNU Affero General Public License, you may choose any version ever published +by the Free Software Foundation. + + If the Program specifies that a proxy can decide which future +versions of the GNU Affero General Public License can be used, that proxy's +public statement of acceptance of a version permanently authorizes you +to choose that version for the Program. + + Later license versions may give you additional or different +permissions. However, no additional obligations are imposed on any +author or copyright holder as a result of your choosing to follow a +later version. + + 15. Disclaimer of Warranty. + + THERE IS NO WARRANTY FOR THE PROGRAM, TO THE EXTENT PERMITTED BY +APPLICABLE LAW. EXCEPT WHEN OTHERWISE STATED IN WRITING THE COPYRIGHT +HOLDERS AND/OR OTHER PARTIES PROVIDE THE PROGRAM "AS IS" WITHOUT WARRANTY +OF ANY KIND, EITHER EXPRESSED OR IMPLIED, INCLUDING, BUT NOT LIMITED TO, +THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR +PURPOSE. THE ENTIRE RISK AS TO THE QUALITY AND PERFORMANCE OF THE PROGRAM +IS WITH YOU. SHOULD THE PROGRAM PROVE DEFECTIVE, YOU ASSUME THE COST OF +ALL NECESSARY SERVICING, REPAIR OR CORRECTION. + + 16. Limitation of Liability. + + IN NO EVENT UNLESS REQUIRED BY APPLICABLE LAW OR AGREED TO IN WRITING +WILL ANY COPYRIGHT HOLDER, OR ANY OTHER PARTY WHO MODIFIES AND/OR CONVEYS +THE PROGRAM AS PERMITTED ABOVE, BE LIABLE TO YOU FOR DAMAGES, INCLUDING ANY +GENERAL, SPECIAL, INCIDENTAL OR CONSEQUENTIAL DAMAGES ARISING OUT OF THE +USE OR INABILITY TO USE THE PROGRAM (INCLUDING BUT NOT LIMITED TO LOSS OF +DATA OR DATA BEING RENDERED INACCURATE OR LOSSES SUSTAINED BY YOU OR THIRD +PARTIES OR A FAILURE OF THE PROGRAM TO OPERATE WITH ANY OTHER PROGRAMS), +EVEN IF SUCH HOLDER OR OTHER PARTY HAS BEEN ADVISED OF THE POSSIBILITY OF +SUCH DAMAGES. + + 17. Interpretation of Sections 15 and 16. + + If the disclaimer of warranty and limitation of liability provided +above cannot be given local legal effect according to their terms, +reviewing courts shall apply local law that most closely approximates +an absolute waiver of all civil liability in connection with the +Program, unless a warranty or assumption of liability accompanies a +copy of the Program in return for a fee. + + END OF TERMS AND CONDITIONS + + How to Apply These Terms to Your New Programs + + If you develop a new program, and you want it to be of the greatest +possible use to the public, the best way to achieve this is to make it +free software which everyone can redistribute and change under these terms. + + To do so, attach the following notices to the program. It is safest +to attach them to the start of each source file to most effectively +state the exclusion of warranty; and each file should have at least +the "copyright" line and a pointer to where the full notice is found. + + + Copyright (C) + + This program is free software: you can redistribute it and/or modify + it under the terms of the GNU Affero General Public License as published + by the Free Software Foundation, either version 3 of the License, or + (at your option) any later version. + + This program is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU Affero General Public License for more details. + + You should have received a copy of the GNU Affero General Public License + along with this program. If not, see . + +Also add information on how to contact you by electronic and paper mail. + + If your software can interact with users remotely through a computer +network, you should also make sure that it provides a way for users to +get its source. For example, if your program is a web application, its +interface could display a "Source" link that leads users to an archive +of the code. There are many ways you could offer source, and different +solutions will be better for different programs; see section 13 for the +specific requirements. + + You should also get your employer (if you work as a programmer) or school, +if any, to sign a "copyright disclaimer" for the program, if necessary. +For more information on this, and how to apply and follow the GNU AGPL, see +. diff --git a/3rdparty/icewind/searchdav/src/Backend/ISearchBackend.php b/3rdparty/icewind/searchdav/src/Backend/ISearchBackend.php new file mode 100644 index 00000000..277f2840 --- /dev/null +++ b/3rdparty/icewind/searchdav/src/Backend/ISearchBackend.php @@ -0,0 +1,94 @@ + + * + * @license GNU AGPL version 3 or any later version + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU Affero General Public License as + * published by the Free Software Foundation, either version 3 of the + * License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Affero General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License + * along with this program. If not, see . + * + */ + +namespace SearchDAV\Backend; + +use Sabre\DAV\INode; +use SearchDAV\Query\Query; + +interface ISearchBackend { + /** + * Get the path of the search arbiter of this backend + * + * The search arbiter is the URI that the client will send its SEARCH requests to + * Note that this is not required to be the same as the search scopes which determine what to search in + * + * The returned value should be a path relative the root of the dav server. + * + * For example, if you want to support SEARCH requests on `https://example.com/dav.php/search` + * with the sabre/dav server listening on `/dav.php` you should return `search` as arbiter path. + * + * @return string + */ + public function getArbiterPath(): string; + + /** + * Whether the search backend supports search requests on this scope + * + * The scope defines the resource that it being searched, such as a folder or address book. + * + * Note that a search arbiter has no inherit limitations on which scopes it can support and scopes + * that reside on a different dav server entirely might be considered valid by an implementation. + * + * One example use case for this would be a service that provides additional indexing on a 3rd party service. + * + * @param string $href an absolute uri of the search scope + * @param string|integer $depth 0, 1 or 'infinite' + * @param string|null $path the path of the search scope relative to the dav server, or null if the scope is outside the dav server + * @return bool + */ + public function isValidScope(string $href, $depth, ?string $path): bool; + + /** + * List the available properties that can be used in search + * + * This is used to tell the search client what properties can be queried, used to filter and used to sort. + * + * Since sabre's PropFind handling mechanism is used to return the properties to the client, it's required that all + * properties which are listed as selectable have a PropFind handler set. + * + * @param string $href an absolute uri of the search scope + * @param string|null $path the path of the search scope relative to the dav server, or null if the scope is outside the dav server + * @return SearchPropertyDefinition[] + */ + public function getPropertyDefinitionsForScope(string $href, ?string $path): array; + + /** + * Preform the search request + * + * The search results consist of the uri for the found resource and an INode describing the resource + * To return the properties requested by the query sabre's existing PropFind method is used, thus the search implementation + * is not required to collect these properties and is free to ignore the `select` part of the query + * + * @param Query $query + * @return SearchResult[] + */ + public function search(Query $query): array; + + /** + * Called by the search plugin once the nodes to be returned have been found. + * This can be used to more efficiently load the requested properties for the results. + * + * @param INode[] $nodes + * @param string[] $requestProperties + */ + public function preloadPropertyFor(array $nodes, array $requestProperties): void; +} diff --git a/3rdparty/icewind/searchdav/src/Backend/SearchPropertyDefinition.php b/3rdparty/icewind/searchdav/src/Backend/SearchPropertyDefinition.php new file mode 100644 index 00000000..4dca05b9 --- /dev/null +++ b/3rdparty/icewind/searchdav/src/Backend/SearchPropertyDefinition.php @@ -0,0 +1,66 @@ + + * + * @license GNU AGPL version 3 or any later version + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU Affero General Public License as + * published by the Free Software Foundation, either version 3 of the + * License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Affero General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License + * along with this program. If not, see . + * + */ + +namespace SearchDAV\Backend; + +class SearchPropertyDefinition { + const XS = '{http://www.w3.org/2001/XMLSchema}'; + const DATATYPE_STRING = self::XS . 'string'; + const DATATYPE_INTEGER = self::XS . 'integer'; + const DATATYPE_NONNEGATIVE_INTEGER = self::XS . 'nonNegativeInteger'; + const DATATYPE_NON_NEGATIVE_INTEGER = self::XS . 'nonNegativeInteger'; + const DATATYPE_DECIMAL = self::XS . 'decimal'; + const DATATYPE_DATETIME = self::XS . 'dateTime'; + const DATATYPE_BOOLEAN = self::XS . 'boolean'; + + + /** @var boolean */ + public $searchable; + /** @var boolean */ + public $selectable; + /** @var boolean */ + public $sortable; + /** @var boolean */ + public $caseSensitive; + /** @var string */ + public $dataType; + /** @var string */ + public $name; + + /** + * SearchProperty constructor. + * + * @param string $name the name and namespace of the property in clark notation + * @param bool $searchable whether this property can be used as part of a search query + * @param bool $selectable whether this property can be returned as part of a search result + * @param bool $sortable whether this property can be used to sort the search result + * @param string $dataType the datatype of the property, one of the SearchProperty::DATATYPE_ constants or any XSD datatype in clark notation + * @param bool $caseSensitive whether comparisons on the property are case-sensitive, only applies to string properties + */ + public function __construct(string $name, bool $selectable, bool $searchable, bool $sortable, string $dataType = self::DATATYPE_STRING, bool $caseSensitive = true) { + $this->searchable = $searchable; + $this->selectable = $selectable; + $this->sortable = $sortable; + $this->dataType = $dataType; + $this->name = $name; + $this->caseSensitive = $caseSensitive; + } +} diff --git a/3rdparty/icewind/searchdav/src/Backend/SearchResult.php b/3rdparty/icewind/searchdav/src/Backend/SearchResult.php new file mode 100644 index 00000000..af485063 --- /dev/null +++ b/3rdparty/icewind/searchdav/src/Backend/SearchResult.php @@ -0,0 +1,42 @@ + + * + * @license GNU AGPL version 3 or any later version + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU Affero General Public License as + * published by the Free Software Foundation, either version 3 of the + * License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Affero General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License + * along with this program. If not, see . + * + */ + +namespace SearchDAV\Backend; + +use Sabre\DAV\INode; + +class SearchResult { + /** @var INode */ + public $node; + /** @var string */ + public $href; + + /** + * SearchResult constructor. + * + * @param INode $node + * @param string $href + */ + public function __construct(INode $node, string $href) { + $this->node = $node; + $this->href = $href; + } +} diff --git a/3rdparty/icewind/searchdav/src/DAV/DiscoverHandler.php b/3rdparty/icewind/searchdav/src/DAV/DiscoverHandler.php new file mode 100644 index 00000000..811cfc78 --- /dev/null +++ b/3rdparty/icewind/searchdav/src/DAV/DiscoverHandler.php @@ -0,0 +1,107 @@ + + * + * @license GNU AGPL version 3 or any later version + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU Affero General Public License as + * published by the Free Software Foundation, either version 3 of the + * License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Affero General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License + * along with this program. If not, see . + * + */ + +namespace SearchDAV\DAV; + +use Sabre\DAV\Exception\BadRequest; +use Sabre\DAV\Xml\Response\MultiStatus; +use Sabre\HTTP\RequestInterface; +use Sabre\HTTP\ResponseInterface; +use SearchDAV\Backend\ISearchBackend; +use SearchDAV\Backend\SearchPropertyDefinition; +use SearchDAV\XML\BasicSearch; +use SearchDAV\XML\BasicSearchSchema; +use SearchDAV\XML\PropDesc; +use SearchDAV\XML\QueryDiscoverResponse; +use SearchDAV\XML\Scope; + +class DiscoverHandler { + /** @var ISearchBackend */ + private $searchBackend; + + /** @var PathHelper */ + private $pathHelper; + + /** @var QueryParser */ + private $queryParser; + + /** + * @param ISearchBackend $searchBackend + * @param PathHelper $pathHelper + * @param QueryParser $queryParser + */ + public function __construct(ISearchBackend $searchBackend, PathHelper $pathHelper, QueryParser $queryParser) { + $this->searchBackend = $searchBackend; + $this->pathHelper = $pathHelper; + $this->queryParser = $queryParser; + } + + public function handelDiscoverRequest($xml, RequestInterface $request, ResponseInterface $response): bool { + if (!isset($xml['{DAV:}basicsearch'])) { + $response->setStatus(400); + $response->setBody('Unexpected xml content for query-schema-discovery, expected basicsearch'); + return false; + } + /** @var BasicSearch $query */ + $query = $xml['{DAV:}basicsearch']; + $scopes = $query->from; + $results = array_map(function (Scope $scope) { + $scope->path = $this->pathHelper->getPathFromUri($scope->href); + if ($this->searchBackend->isValidScope($scope->href, $scope->depth, $scope->path)) { + $searchProperties = $this->searchBackend->getPropertyDefinitionsForScope($scope->href, $scope->path); + $searchSchema = $this->getBasicSearchForProperties($searchProperties); + return new QueryDiscoverResponse($scope->href, $searchSchema, 200); + } else { + return new QueryDiscoverResponse($scope->href, null, 404); // TODO something other than 404? 403 maybe + } + }, $scopes); + $multiStatus = new MultiStatus($results); + $response->setStatus(207); + $response->setHeader('Content-Type', 'application/xml; charset="utf-8"'); + $response->setBody($this->queryParser->write('{DAV:}multistatus', $multiStatus, $request->getUrl())); + return false; + } + + private function hashDefinition(SearchPropertyDefinition $definition): string { + return $definition->dataType + . (($definition->searchable) ? '1' : '0') + . (($definition->sortable) ? '1' : '0') + . (($definition->selectable) ? '1' : '0'); + } + + /** + * @param SearchPropertyDefinition[] $propertyDefinitions + * @return BasicSearchSchema + */ + private function getBasicSearchForProperties(array $propertyDefinitions): BasicSearchSchema { + /** @var PropDesc[] $groups */ + $groups = []; + foreach ($propertyDefinitions as $propertyDefinition) { + $key = $this->hashDefinition($propertyDefinition); + if (!isset($groups[$key])) { + $groups[$key] = new PropDesc($propertyDefinition); + } + $groups[$key]->properties[] = $propertyDefinition->name; + } + + return new BasicSearchSchema(array_values($groups)); + } +} diff --git a/3rdparty/icewind/searchdav/src/DAV/PathHelper.php b/3rdparty/icewind/searchdav/src/DAV/PathHelper.php new file mode 100644 index 00000000..04098c78 --- /dev/null +++ b/3rdparty/icewind/searchdav/src/DAV/PathHelper.php @@ -0,0 +1,50 @@ + + * + * @license GNU AGPL version 3 or any later version + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU Affero General Public License as + * published by the Free Software Foundation, either version 3 of the + * License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Affero General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License + * along with this program. If not, see . + * + */ + +namespace SearchDAV\DAV; + +use Sabre\DAV\Exception\Forbidden; +use Sabre\DAV\Server; + +class PathHelper { + /** @var Server */ + private $server; + + /** + * PathHelper constructor. + * + * @param Server $server + */ + public function __construct(Server $server) { + $this->server = $server; + } + + public function getPathFromUri(string $uri): ?string { + if (strpos($uri, '://') === false) { + return $uri; + } + try { + return ($uri === '' && $this->server->getBaseUri() === '/') ? '' : $this->server->calculateUri($uri); + } catch (Forbidden $e) { + return null; + } + } +} diff --git a/3rdparty/icewind/searchdav/src/DAV/QueryParser.php b/3rdparty/icewind/searchdav/src/DAV/QueryParser.php new file mode 100644 index 00000000..915184e3 --- /dev/null +++ b/3rdparty/icewind/searchdav/src/DAV/QueryParser.php @@ -0,0 +1,83 @@ + + * + * @license GNU AGPL version 3 or any later version + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU Affero General Public License as + * published by the Free Software Foundation, either version 3 of the + * License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Affero General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License + * along with this program. If not, see . + * + */ + +namespace SearchDAV\DAV; + +use Sabre\Xml\Element; +use Sabre\Xml\Reader; +use Sabre\Xml\Service; +use SearchDAV\XML\BasicSearch; +use SearchDAV\XML\Limit; +use SearchDAV\XML\Literal; +use SearchDAV\XML\Operator; +use SearchDAV\XML\Order; +use SearchDAV\XML\Scope; +use function Sabre\Xml\Deserializer\keyValue; +use function Sabre\Xml\Deserializer\repeatingElements; + +class QueryParser extends Service { + public $namespaceMap = [ + 'DAV:' => 'd', + 'http://sabredav.org/ns' => 's', + 'http://www.w3.org/2001/XMLSchema' => 'xs', + SearchPlugin::SEARCHDAV_NS => 'sd' + ]; + + public function __construct() { + $this->elementMap = [ + '{DAV:}literal' => Literal::class, + '{DAV:}searchrequest' => Element\KeyValue::class, + '{DAV:}query-schema-discovery' => Element\KeyValue::class, + '{DAV:}basicsearch' => BasicSearch::class, + '{DAV:}select' => function (Reader $reader) { + return keyValue($reader, '{DAV:}scope')['{DAV:}prop']; + }, + '{DAV:}from' => function (Reader $reader) { + return repeatingElements($reader, '{DAV:}scope'); + }, + '{DAV:}orderby' => function (Reader $reader) { + return repeatingElements($reader, '{DAV:}order'); + }, + '{DAV:}scope' => Scope::class, + '{DAV:}where' => function (Reader $reader) { + $operators = array_map(function ($element) { + return $element['value']; + }, $reader->parseGetElements()); + return (isset($operators[0])) ? $operators[0] : null; + }, + '{DAV:}prop' => Element\Elements::class, + '{DAV:}order' => Order::class, + '{DAV:}eq' => Operator::class, + '{DAV:}gt' => Operator::class, + '{DAV:}gte' => Operator::class, + '{DAV:}lt' => Operator::class, + '{DAV:}lte' => Operator::class, + '{DAV:}and' => Operator::class, + '{DAV:}or' => Operator::class, + '{DAV:}like' => Operator::class, + '{DAV:}contains' => Operator::class, + '{DAV:}not' => Operator::class, + '{DAV:}is-collection' => Operator::class, + '{DAV:}is-defined' => Operator::class, + '{DAV:}limit' => Limit::class, + ]; + } +} diff --git a/3rdparty/icewind/searchdav/src/DAV/SearchHandler.php b/3rdparty/icewind/searchdav/src/DAV/SearchHandler.php new file mode 100644 index 00000000..a1fa5a75 --- /dev/null +++ b/3rdparty/icewind/searchdav/src/DAV/SearchHandler.php @@ -0,0 +1,200 @@ + + * + * @license GNU AGPL version 3 or any later version + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU Affero General Public License as + * published by the Free Software Foundation, either version 3 of the + * License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Affero General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License + * along with this program. If not, see . + * + */ + +namespace SearchDAV\DAV; + +use Sabre\DAV\Exception\BadRequest; +use Sabre\DAV\INode; +use Sabre\DAV\PropFind; +use Sabre\DAV\Server; +use Sabre\HTTP\ResponseInterface; +use SearchDAV\Backend\ISearchBackend; +use SearchDAV\Backend\SearchPropertyDefinition; +use SearchDAV\Backend\SearchResult; +use SearchDAV\Query\Operator; +use SearchDAV\Query\Order; +use SearchDAV\Query\Query; +use SearchDAV\XML\BasicSearch; + +class SearchHandler { + /** @var ISearchBackend */ + private $searchBackend; + + /** @var PathHelper */ + private $pathHelper; + + /** @var Server */ + private $server; + + /** + * @param ISearchBackend $searchBackend + * @param PathHelper $pathHelper + * @param Server $server + */ + public function __construct(ISearchBackend $searchBackend, PathHelper $pathHelper, Server $server) { + $this->searchBackend = $searchBackend; + $this->pathHelper = $pathHelper; + $this->server = $server; + } + + public function handleSearchRequest($xml, ResponseInterface $response): bool { + if (!isset($xml['{DAV:}basicsearch'])) { + $response->setStatus(400); + $response->setBody('Unexpected xml content for search request, expected basicsearch'); + return false; + } + /** @var BasicSearch $query */ + $query = $xml['{DAV:}basicsearch']; + if (!$query->select) { + $response->setStatus(400); + $response->setBody('Parse error: Missing {DAV:}select from {DAV:}basicsearch'); + return false; + } + $response->setStatus(207); + $response->setHeader('Content-Type', 'application/xml; charset="utf-8"'); + $allProps = []; + foreach ($query->from as $scope) { + $scope->path = $this->pathHelper->getPathFromUri($scope->href); + $props = $this->searchBackend->getPropertyDefinitionsForScope($scope->href, $scope->path); + foreach ($props as $prop) { + $allProps[$prop->name] = $prop; + } + } + try { + $results = $this->searchBackend->search($this->getQueryForXML($query, $allProps)); + } catch (BadRequest $e) { + $response->setStatus(400); + $response->setBody($e->getMessage()); + return false; + } + $data = $this->server->generateMultiStatus(iterator_to_array($this->getPropertiesIteratorResults( + $results, + $query->select + )), false); + $response->setBody($data); + return false; + } + + /** + * @param BasicSearch $xml + * @param SearchPropertyDefinition[] $allProps + * @return Query + * @throws BadRequest + */ + private function getQueryForXML(BasicSearch $xml, array $allProps): Query { + $orderBy = array_map(function (\SearchDAV\XML\Order $order) use ($allProps) { + if (!isset($allProps[$order->property])) { + throw new BadRequest('requested order by property is not a valid property for this scope'); + } + $prop = $allProps[$order->property]; + if (!$prop->sortable) { + throw new BadRequest('requested order by property is not sortable'); + } + return new Order($prop, $order->order); + }, $xml->orderBy); + $select = array_map(function ($propName) use ($allProps) { + if (!isset($allProps[$propName])) { + return null; + } + $prop = $allProps[$propName]; + if (!$prop->selectable) { + throw new BadRequest('requested property is not selectable'); + } + return $prop; + }, $xml->select); + $select = array_filter($select); + + $where = $xml->where ? $this->transformOperator($xml->where, $allProps) : null; + + return new Query($select, $xml->from, $where, $orderBy, $xml->limit); + } + + /** + * @param \SearchDAV\XML\Operator $operator + * @param SearchPropertyDefinition[] $allProps + * @return Operator + * @throws BadRequest + */ + private function transformOperator(\SearchDAV\XML\Operator $operator, array $allProps): Operator { + $arguments = array_map(function ($argument) use ($allProps) { + if (is_string($argument)) { + if (!isset($allProps[$argument])) { + throw new BadRequest('requested search property is not a valid property for this scope'); + } + $prop = $allProps[$argument]; + if (!$prop->searchable) { + throw new BadRequest('requested search property is not searchable'); + } + return $prop; + } else { + if ($argument instanceof \SearchDAV\XML\Operator) { + return $this->transformOperator($argument, $allProps); + } else { + return $argument; + } + } + }, $operator->arguments); + + return new Operator($operator->type, $arguments); + } + + /** + * Returns a list of properties for a given path + * + * The path that should be supplied should have the baseUrl stripped out + * The list of properties should be supplied in Clark notation. If the list is empty + * 'allprops' is assumed. + * + * If a depth of 1 is requested child elements will also be returned. + * + * @param SearchResult[] $results + * @param string[] $propertyNames + * @param int $depth + * @return \Iterator + */ + private function getPropertiesIteratorResults(array $results, array $propertyNames = [], int $depth = 0): \Iterator { + $propFindType = $propertyNames ? PropFind::NORMAL : PropFind::ALLPROPS; + + $this->searchBackend->preloadPropertyFor(array_map(function (SearchResult $result): INode { + return $result->node; + }, $results), $propertyNames); + + foreach ($results as $result) { + $node = $result->node; + $propFind = new PropFind($result->href, $propertyNames, $depth, $propFindType); + $r = $this->server->getPropertiesByNode($propFind, $node); + if ($r) { + $result = $propFind->getResultForMultiStatus(); + $result['href'] = $propFind->getPath(); + + // WebDAV recommends adding a slash to the path, if the path is + // a collection. + // Furthermore, iCal also demands this to be the case for + // principals. This is non-standard, but we support it. + $resourceType = $this->server->getResourceTypeForNode($node); + if (in_array('{DAV:}collection', $resourceType) || in_array('{DAV:}principal', $resourceType)) { + $result['href'] .= '/'; + } + yield $result; + } + } + } +} diff --git a/3rdparty/icewind/searchdav/src/DAV/SearchPlugin.php b/3rdparty/icewind/searchdav/src/DAV/SearchPlugin.php new file mode 100644 index 00000000..7c03b5a5 --- /dev/null +++ b/3rdparty/icewind/searchdav/src/DAV/SearchPlugin.php @@ -0,0 +1,128 @@ + + * + * @license GNU AGPL version 3 or any later version + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU Affero General Public License as + * published by the Free Software Foundation, either version 3 of the + * License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Affero General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License + * along with this program. If not, see . + * + */ + +namespace SearchDAV\DAV; + +use Sabre\DAV\INode; +use Sabre\DAV\PropFind; +use Sabre\DAV\Server; +use Sabre\DAV\ServerPlugin; +use Sabre\HTTP\RequestInterface; +use Sabre\HTTP\ResponseInterface; +use Sabre\Xml\ParseException; +use SearchDAV\Backend\ISearchBackend; +use SearchDAV\XML\SupportedQueryGrammar; + +class SearchPlugin extends ServerPlugin { + const SEARCHDAV_NS = 'https://github.com/icewind1991/SearchDAV/ns'; + + /** @var ISearchBackend */ + private $searchBackend; + + /** @var QueryParser */ + private $queryParser; + + /** @var PathHelper */ + private $pathHelper; + + /** @var SearchHandler */ + private $search; + + /** @var DiscoverHandler */ + private $discover; + + public function __construct(ISearchBackend $searchBackend) { + $this->searchBackend = $searchBackend; + $this->queryParser = new QueryParser(); + } + + public function initialize(Server $server): void { + $this->pathHelper = new PathHelper($server); + $this->search = new SearchHandler($this->searchBackend, $this->pathHelper, $server); + $this->discover = new DiscoverHandler($this->searchBackend, $this->pathHelper, $this->queryParser); + $server->on('method:SEARCH', [$this, 'searchHandler']); + $server->on('afterMethod:OPTIONS', [$this, 'optionHandler']); + $server->on('propFind', [$this, 'propFindHandler']); + } + + public function propFindHandler(PropFind $propFind, INode $node): void { + if ($propFind->getPath() === $this->searchBackend->getArbiterPath()) { + $propFind->handle('{DAV:}supported-query-grammar-set', new SupportedQueryGrammar()); + } + } + + /** + * SEARCH is allowed for users files + * + * @param string $path + * @return string[] + */ + public function getHTTPMethods($path): array { + $path = $this->pathHelper->getPathFromUri($path); + if ($this->searchBackend->getArbiterPath() === $path) { + return ['SEARCH']; + } else { + return []; + } + } + + public function optionHandler(RequestInterface $request, ResponseInterface $response): void { + if ($request->getPath() === $this->searchBackend->getArbiterPath()) { + $response->addHeader('DASL', ''); + } + } + + public function searchHandler(RequestInterface $request, ResponseInterface $response): bool { + $contentType = $request->getHeader('Content-Type') ?? ''; + + // Currently, we only support xml search queries + if ((strpos($contentType, 'text/xml') === false) && (strpos($contentType, 'application/xml') === false)) { + return true; + } + + if ($request->getPath() !== $this->searchBackend->getArbiterPath()) { + return true; + } + + try { + $xml = $this->queryParser->parse( + $request->getBodyAsString(), + $request->getUrl(), + $documentType + ); + } catch (ParseException $e) { + $response->setStatus(400); + $response->setBody('Parse error: ' . $e->getMessage()); + return false; + } + + switch ($documentType) { + case '{DAV:}searchrequest': + return $this->search->handleSearchRequest($xml, $response); + case '{DAV:}query-schema-discovery': + return $this->discover->handelDiscoverRequest($xml, $request, $response); + default: + $response->setStatus(400); + $response->setBody('Unexpected document type: ' . $documentType . ' for this Content-Type, expected {DAV:}searchrequest or {DAV:}query-schema-discovery'); + return false; + } + } +} diff --git a/3rdparty/icewind/searchdav/src/Query/Limit.php b/3rdparty/icewind/searchdav/src/Query/Limit.php new file mode 100644 index 00000000..f59550f3 --- /dev/null +++ b/3rdparty/icewind/searchdav/src/Query/Limit.php @@ -0,0 +1,39 @@ + + * + * @license GNU AGPL version 3 or any later version + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU Affero General Public License as + * published by the Free Software Foundation, either version 3 of the + * License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Affero General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License + * along with this program. If not, see . + * + */ + +namespace SearchDAV\Query; + +class Limit { + /** + * @var integer + * + * The maximum number of results to be returned + * + * If set to 0 then no limit should be imposed + */ + public $maxResults = 0; + /** + * @var integer + * + * The index of the first result to be returned (offset) + */ + public $firstResult = 0; +} diff --git a/3rdparty/icewind/searchdav/src/Query/Literal.php b/3rdparty/icewind/searchdav/src/Query/Literal.php new file mode 100644 index 00000000..99ce07a7 --- /dev/null +++ b/3rdparty/icewind/searchdav/src/Query/Literal.php @@ -0,0 +1,40 @@ + + * + * @license GNU AGPL version 3 or any later version + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU Affero General Public License as + * published by the Free Software Foundation, either version 3 of the + * License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Affero General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License + * along with this program. If not, see . + * + */ + +namespace SearchDAV\Query; + +class Literal { + /** + * @var string|boolean|\DateTime|integer + * + * The value of the literal + */ + public $value; + + /** + * Literal constructor. + * + * @param bool|\DateTime|int|string $value + */ + public function __construct($value = '') { + $this->value = $value; + } +} diff --git a/3rdparty/icewind/searchdav/src/Query/Operator.php b/3rdparty/icewind/searchdav/src/Query/Operator.php new file mode 100644 index 00000000..182f264c --- /dev/null +++ b/3rdparty/icewind/searchdav/src/Query/Operator.php @@ -0,0 +1,68 @@ + + * + * @license GNU AGPL version 3 or any later version + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU Affero General Public License as + * published by the Free Software Foundation, either version 3 of the + * License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Affero General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License + * along with this program. If not, see . + * + */ + +namespace SearchDAV\Query; + +class Operator { + const OPERATION_AND = '{DAV:}and'; + const OPERATION_OR = '{DAV:}or'; + const OPERATION_NOT = '{DAV:}not'; + const OPERATION_EQUAL = '{DAV:}eq'; + const OPERATION_LESS_THAN = '{DAV:}lt'; + const OPERATION_LESS_OR_EQUAL_THAN = '{DAV:}lte'; + const OPERATION_GREATER_THAN = '{DAV:}gt'; + const OPERATION_GREATER_OR_EQUAL_THAN = '{DAV:}gte'; + const OPERATION_IS_COLLECTION = '{DAV:}is-collection'; + const OPERATION_IS_DEFINED = '{DAV:}is-defined'; + const OPERATION_IS_LIKE = '{DAV:}like'; + const OPERATION_CONTAINS = '{DAV:}contains'; + + /** + * @var string + * + * The type of operation, one of the Operator::OPERATION_* constants + */ + public $type; + + /** + * @var (Literal|\SearchDAV\Backend\SearchPropertyDefinition|Operator)[] + * + * The list of arguments for the operation + * + * - SearchPropDefinition: property for comparison + * - Literal: literal value for comparison + * - Operator: nested operation for and/or/not operations + * + * Which type and what number of argument an Operator takes depends on the operator type. + */ + public $arguments; + + /** + * Operator constructor. + * + * @param string $type + * @param (Literal|\SearchDAV\Backend\SearchPropertyDefinition|Operator)[] $arguments + */ + public function __construct(string $type = '', array $arguments = []) { + $this->type = $type; + $this->arguments = $arguments; + } +} diff --git a/3rdparty/icewind/searchdav/src/Query/Order.php b/3rdparty/icewind/searchdav/src/Query/Order.php new file mode 100644 index 00000000..20f69f6a --- /dev/null +++ b/3rdparty/icewind/searchdav/src/Query/Order.php @@ -0,0 +1,52 @@ + + * + * @license GNU AGPL version 3 or any later version + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU Affero General Public License as + * published by the Free Software Foundation, either version 3 of the + * License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Affero General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License + * along with this program. If not, see . + * + */ + +namespace SearchDAV\Query; + +use SearchDAV\Backend\SearchPropertyDefinition; + +class Order { + const ASC = 'ascending'; + const DESC = 'descending'; + + /** + * @var SearchPropertyDefinition + * + * The property that should be sorted on. + */ + public $property; + /** + * @var string 'ascending' or 'descending' + * + * The sort direction + */ + public $order; + + /** + * Order constructor. + * @param SearchPropertyDefinition $property + * @param string $order + */ + public function __construct(SearchPropertyDefinition $property, string $order) { + $this->property = $property; + $this->order = $order; + } +} diff --git a/3rdparty/icewind/searchdav/src/Query/Query.php b/3rdparty/icewind/searchdav/src/Query/Query.php new file mode 100644 index 00000000..82f237ce --- /dev/null +++ b/3rdparty/icewind/searchdav/src/Query/Query.php @@ -0,0 +1,78 @@ + + * + * @license GNU AGPL version 3 or any later version + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU Affero General Public License as + * published by the Free Software Foundation, either version 3 of the + * License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Affero General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License + * along with this program. If not, see . + * + */ + +namespace SearchDAV\Query; + +use SearchDAV\Backend\SearchPropertyDefinition; + +class Query { + /** + * @var SearchPropertyDefinition[] + * + * The list of properties to be selected + */ + public $select; + /** + * @var Scope[] + * + * The collections to perform the search in + */ + public $from; + /** + * @var ?Operator + * + * The search operator, either a comparison ('gt', 'eq', ...) or a boolean operator ('and', 'or', 'not') + */ + public $where; + /** + * @var Order[] + * + * The list of order operations that should be used to order the results. + * + * Each order operations consists of a property to sort on and a sort direction. + * If more than one order operations are specified, the comparisons for ordering should + * be applied in the order that the order operations are defined in with the earlier comparisons being + * more significant. + */ + public $orderBy; + /** + * @var Limit + * + * The limit and offset for the search query + */ + public $limit; + + /** + * Query constructor. + * @param SearchPropertyDefinition[] $select + * @param Scope[] $from + * @param Operator|null $where + * @param Order[] $orderBy + * @param Limit $limit + */ + public function __construct(array $select, array $from, ?Operator $where, array $orderBy, Limit $limit) { + $this->select = $select; + $this->from = $from; + $this->where = $where; + $this->orderBy = $orderBy; + $this->limit = $limit; + } +} diff --git a/3rdparty/icewind/searchdav/src/Query/Scope.php b/3rdparty/icewind/searchdav/src/Query/Scope.php new file mode 100644 index 00000000..76b41aea --- /dev/null +++ b/3rdparty/icewind/searchdav/src/Query/Scope.php @@ -0,0 +1,59 @@ + + * + * @license GNU AGPL version 3 or any later version + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU Affero General Public License as + * published by the Free Software Foundation, either version 3 of the + * License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Affero General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License + * along with this program. If not, see . + * + */ + +namespace SearchDAV\Query; + +class Scope { + /** + * @var string + * + * The scope of the search, either as absolute uri or as a path relative to the + * search arbiter. + */ + public $href; + + /** + * @var string|int 0, 1 or 'infinite' + * + * How deep the search query should be with 0 being only the scope itself, + * 1 being all direct child entries of the scope and infinite being all entries + * in the scope collection at any depth. + */ + public $depth; + + /** + * @var string|null + * + * the path of the search scope relative to the dav server, or null if the scope is outside the dav server + */ + public $path; + + /** + * @param string $href + * @param int|string $depth + * @param string|null $path + */ + public function __construct(string $href = '', $depth = 1, ?string $path = null) { + $this->href = $href; + $this->depth = $depth; + $this->path = $path; + } +} diff --git a/3rdparty/icewind/searchdav/src/XML/BasicSearch.php b/3rdparty/icewind/searchdav/src/XML/BasicSearch.php new file mode 100644 index 00000000..c5451caf --- /dev/null +++ b/3rdparty/icewind/searchdav/src/XML/BasicSearch.php @@ -0,0 +1,98 @@ + + * + * @license GNU AGPL version 3 or any later version + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU Affero General Public License as + * published by the Free Software Foundation, either version 3 of the + * License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Affero General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License + * along with this program. If not, see . + * + */ + +namespace SearchDAV\XML; + +use Sabre\Xml\ParseException; +use Sabre\Xml\Reader; +use Sabre\Xml\XmlDeserializable; +use function Sabre\Xml\Deserializer\keyValue; + +/** + * The object representation of a search query made by the client + */ +class BasicSearch implements XmlDeserializable { + /** + * @var string[] + * + * The list of properties to be selected, specified in clark notation + */ + public $select; + /** + * @var Scope[] + * + * The collections to perform the search in + */ + public $from; + /** + * @var ?Operator + * + * The search operator, either a comparison ('gt', 'eq', ...) or a boolean operator ('and', 'or', 'not') + */ + public $where; + /** + * @var Order[] + * + * The list of order operations that should be used to order the results. + * + * Each order operations consists of a property to sort on and a sort direction. + * If more then one order operations are specified, the comparisons for ordering should + * be applied in the order that the order operations are defined in with the earlier comparisons being + * more significant. + */ + public $orderBy; + /** + * @var Limit + * + * The limit and offset for the search query + */ + public $limit; + + public function __construct(array $select, array $from, ?Operator $where, array $orderBy, Limit $limit) { + $this->select = $select; + $this->from = $from; + $this->where = $where; + $this->orderBy = $orderBy; + $this->limit = $limit; + } + + + /** + * @param Reader $reader + * @return BasicSearch + * @throws ParseException + */ + public static function xmlDeserialize(Reader $reader): BasicSearch { + $elements = keyValue($reader); + + if (!isset($elements['{DAV:}from'])) { + throw new ParseException('Missing {DAV:}from when parsing {DAV:}basicsearch'); + } + + return new BasicSearch( + $elements['{DAV:}select'] ?? [], + $elements['{DAV:}from'], + $elements['{DAV:}where'] ?? null, + $elements['{DAV:}orderby'] ?? [], + $elements['{DAV:}limit'] ?? new Limit() + ); + } +} diff --git a/3rdparty/icewind/searchdav/src/XML/BasicSearchSchema.php b/3rdparty/icewind/searchdav/src/XML/BasicSearchSchema.php new file mode 100644 index 00000000..c68aabe7 --- /dev/null +++ b/3rdparty/icewind/searchdav/src/XML/BasicSearchSchema.php @@ -0,0 +1,49 @@ + + * + * @license GNU AGPL version 3 or any later version + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU Affero General Public License as + * published by the Free Software Foundation, either version 3 of the + * License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Affero General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License + * along with this program. If not, see . + * + */ + +namespace SearchDAV\XML; + +use Sabre\Xml\Writer; +use Sabre\Xml\XmlSerializable; + +class BasicSearchSchema implements XmlSerializable { + /** @var PropDesc[] */ + public $properties; + + /** + * BasicSearchSchema constructor. + * + * @param PropDesc[] $properties + */ + public function __construct(array $properties) { + $this->properties = $properties; + } + + public function xmlSerialize(Writer $writer): void { + $childs = array_map(function (PropDesc $propDesc) { + return [ + 'name' => '{DAV:}propdesc', + 'value' => $propDesc + ]; + }, $this->properties); + $writer->writeElement('{DAV:}properties', $childs); + } +} diff --git a/3rdparty/icewind/searchdav/src/XML/Limit.php b/3rdparty/icewind/searchdav/src/XML/Limit.php new file mode 100644 index 00000000..b00809fc --- /dev/null +++ b/3rdparty/icewind/searchdav/src/XML/Limit.php @@ -0,0 +1,45 @@ + + * + * @license GNU AGPL version 3 or any later version + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU Affero General Public License as + * published by the Free Software Foundation, either version 3 of the + * License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Affero General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License + * along with this program. If not, see . + * + */ + +namespace SearchDAV\XML; + +use Sabre\Xml\Reader; +use Sabre\Xml\XmlDeserializable; +use SearchDAV\DAV\SearchPlugin; +use function Sabre\Xml\Deserializer\keyValue; + +/** + * The limit and offset of a search query + */ +class Limit extends \SearchDAV\Query\Limit implements XmlDeserializable { + public static function xmlDeserialize(Reader $reader): Limit { + $limit = new self(); + + $elements = keyValue($reader); + $namespace = SearchPlugin::SEARCHDAV_NS; + + $limit->maxResults = isset($elements['{DAV:}nresults']) ? $elements['{DAV:}nresults'] : 0; + $firstResult = '{' . $namespace . '}firstresult'; + $limit->firstResult = isset($elements[$firstResult]) ? $elements[$firstResult] : 0; + + return $limit; + } +} diff --git a/3rdparty/icewind/searchdav/src/XML/Literal.php b/3rdparty/icewind/searchdav/src/XML/Literal.php new file mode 100644 index 00000000..dc1f69d1 --- /dev/null +++ b/3rdparty/icewind/searchdav/src/XML/Literal.php @@ -0,0 +1,41 @@ + + * + * @license GNU AGPL version 3 or any later version + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU Affero General Public License as + * published by the Free Software Foundation, either version 3 of the + * License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Affero General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License + * along with this program. If not, see . + * + */ + +namespace SearchDAV\XML; + +use Sabre\Xml\Reader; +use Sabre\Xml\XmlDeserializable; + +class Literal extends \SearchDAV\Query\Literal implements XmlDeserializable { + public static function xmlDeserialize(Reader $reader): Literal { + $literal = new self(); + + if ($reader->isEmptyElement) { + $literal->value = ''; + } else { + $literal->value = $reader->readText(); + } + + $reader->read(); + + return $literal; + } +} diff --git a/3rdparty/icewind/searchdav/src/XML/Operator.php b/3rdparty/icewind/searchdav/src/XML/Operator.php new file mode 100644 index 00000000..fb44ba86 --- /dev/null +++ b/3rdparty/icewind/searchdav/src/XML/Operator.php @@ -0,0 +1,94 @@ + + * + * @license GNU AGPL version 3 or any later version + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU Affero General Public License as + * published by the Free Software Foundation, either version 3 of the + * License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Affero General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License + * along with this program. If not, see . + * + */ + +namespace SearchDAV\XML; + +use Sabre\Xml\Reader; +use Sabre\Xml\XmlDeserializable; +use SearchDAV\Query\Operator as QueryOperator; + +class Operator implements XmlDeserializable { + /** + * @var string + * + * The type of operation, one of the Operator::OPERATION_* constants + */ + public $type; + /** + * @var (Literal|string|Operator)[] + * + * The list of arguments for the operation + * + * - string: property name for comparison + * - Literal: literal value for comparison + * - Operation: nested operation for and/or/not operations + * + * Which type and what number of argument an Operator takes depends on the operator type. + */ + public $arguments; + + /** + * Operator constructor. + * + * @param string $type + * @param (Literal|string|Operator)[] $arguments + */ + public function __construct(string $type = '', array $arguments = []) { + $this->type = $type; + $this->arguments = $arguments; + } + + public static function xmlDeserialize(Reader $reader): Operator { + $operator = new self(); + + $operator->type = $reader->getClark() ?? ''; + if ($reader->isEmptyElement) { + $reader->next(); + return $operator; + } + + if ($operator->type === QueryOperator::OPERATION_CONTAINS) { + $operator->arguments[] = $reader->readString(); + $reader->next(); + return $operator; + } + + $reader->read(); + do { + if ($reader->nodeType === Reader::ELEMENT) { + $argument = $reader->parseCurrentElement(); + if ($argument['name'] === '{DAV:}prop') { + $operator->arguments[] = $argument['value'][0] ?? ''; + } else { + $operator->arguments[] = $argument['value']; + } + } else { + if (!$reader->read()) { + break; + } + } + } while ($reader->nodeType !== Reader::END_ELEMENT); + + $reader->read(); + + return $operator; + } +} diff --git a/3rdparty/icewind/searchdav/src/XML/Order.php b/3rdparty/icewind/searchdav/src/XML/Order.php new file mode 100644 index 00000000..74eab7eb --- /dev/null +++ b/3rdparty/icewind/searchdav/src/XML/Order.php @@ -0,0 +1,63 @@ + + * + * @license GNU AGPL version 3 or any later version + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU Affero General Public License as + * published by the Free Software Foundation, either version 3 of the + * License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Affero General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License + * along with this program. If not, see . + * + */ + +namespace SearchDAV\XML; + +use Sabre\Xml\Reader; +use Sabre\Xml\XmlDeserializable; +use function Sabre\Xml\Deserializer\keyValue; + +class Order implements XmlDeserializable { + /** + * @var string + * + * The property that should be sorted on. + */ + public $property; + /** + * @var string 'ascending' or 'descending' + * + * The sort direction + */ + public $order; + + /** + * Order constructor. + * + * @param string $property + * @param string $order + */ + public function __construct(string $property = '', string $order = \SearchDAV\Query\Order::ASC) { + $this->property = $property; + $this->order = $order; + } + + public static function xmlDeserialize(Reader $reader): Order { + $order = new self(); + + $childs = keyValue($reader); + + $order->order = array_key_exists('{DAV:}descending', $childs) ? \SearchDAV\Query\Order::DESC : \SearchDAV\Query\Order::ASC; + $order->property = $childs['{DAV:}prop'][0]; + + return $order; + } +} diff --git a/3rdparty/icewind/searchdav/src/XML/PropDesc.php b/3rdparty/icewind/searchdav/src/XML/PropDesc.php new file mode 100644 index 00000000..43ada6a4 --- /dev/null +++ b/3rdparty/icewind/searchdav/src/XML/PropDesc.php @@ -0,0 +1,78 @@ + + * + * @license GNU AGPL version 3 or any later version + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU Affero General Public License as + * published by the Free Software Foundation, either version 3 of the + * License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Affero General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License + * along with this program. If not, see . + * + */ + +namespace SearchDAV\XML; + +use Sabre\Xml\Writer; +use Sabre\Xml\XmlSerializable; +use SearchDAV\Backend\SearchPropertyDefinition; + +class PropDesc implements XmlSerializable { + /** + * @var string[] + */ + public $properties = []; + /** + * @var string + */ + public $dataType; + /** + * @var boolean + */ + public $searchable; + /** + * @var boolean + */ + public $selectable; + /** + * @var boolean + */ + public $sortable; + + public function __construct(SearchPropertyDefinition $propertyDefinition) { + $this->dataType = $propertyDefinition->dataType; + $this->sortable = $propertyDefinition->sortable; + $this->selectable = $propertyDefinition->selectable; + $this->searchable = $propertyDefinition->searchable; + } + + public function xmlSerialize(Writer $writer): void { + $data = [ + '{DAV:}dataType' => [$this->dataType => null] + ]; + if ($this->searchable) { + $data['{DAV:}searchable'] = null; + } + if ($this->sortable) { + $data['{DAV:}sortable'] = null; + } + if ($this->selectable) { + $data['{DAV:}selectable'] = null; + } + $writer->write(array_map(function ($propName) { + return [ + 'name' => '{DAV:}prop', + 'value' => $propName + ]; + }, $this->properties)); + $writer->write($data); + } +} diff --git a/3rdparty/icewind/searchdav/src/XML/QueryDiscoverResponse.php b/3rdparty/icewind/searchdav/src/XML/QueryDiscoverResponse.php new file mode 100644 index 00000000..0d8b6a27 --- /dev/null +++ b/3rdparty/icewind/searchdav/src/XML/QueryDiscoverResponse.php @@ -0,0 +1,61 @@ + + * + * @license GNU AGPL version 3 or any later version + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU Affero General Public License as + * published by the Free Software Foundation, either version 3 of the + * License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Affero General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License + * along with this program. If not, see . + * + */ + +namespace SearchDAV\XML; + +use Sabre\DAV\Xml\Element\Response; +use Sabre\Xml\Writer; +use function Sabre\HTTP\encodePath; + +class QueryDiscoverResponse extends Response { + /** + * @var BasicSearchSchema|null + */ + protected $schema; + + /** + * QueryDiscoverResponse constructor. + * + * @param string $href + * @param BasicSearchSchema|null $schema + * @param null|int|string $httpStatus + */ + public function __construct($href, ?BasicSearchSchema $schema = null, $httpStatus = null) { + if ($httpStatus !== null) { + $httpStatus = (string)$httpStatus; + } + parent::__construct($href, [], $httpStatus); + $this->schema = $schema; + } + + public function xmlSerialize(Writer $writer): void { + if ($status = $this->getHTTPStatus()) { + $writer->writeElement('{DAV:}status', 'HTTP/1.1 ' . $status . ' ' . \Sabre\HTTP\Response::$statusCodes[$status]); + } + $writer->writeElement('{DAV:}href', encodePath($this->getHref())); + + if ($this->schema) { + $writer->writeElement('{DAV:}query-schema', [ + '{DAV:}basicsearchschema' => $this->schema + ]); + } + } +} diff --git a/3rdparty/icewind/searchdav/src/XML/Scope.php b/3rdparty/icewind/searchdav/src/XML/Scope.php new file mode 100644 index 00000000..90a2384d --- /dev/null +++ b/3rdparty/icewind/searchdav/src/XML/Scope.php @@ -0,0 +1,38 @@ + + * + * @license GNU AGPL version 3 or any later version + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU Affero General Public License as + * published by the Free Software Foundation, either version 3 of the + * License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Affero General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License + * along with this program. If not, see . + * + */ + +namespace SearchDAV\XML; + +use Sabre\Xml\Reader; +use Sabre\Xml\XmlDeserializable; +use function Sabre\Xml\Deserializer\keyValue; + +class Scope extends \SearchDAV\Query\Scope implements XmlDeserializable { + public static function xmlDeserialize(Reader $reader): Scope { + $scope = new self(); + + $values = keyValue($reader); + $scope->href = $values['{DAV:}href']; + $scope->depth = $values['{DAV:}depth']; + + return $scope; + } +} diff --git a/3rdparty/icewind/searchdav/src/XML/SupportedQueryGrammar.php b/3rdparty/icewind/searchdav/src/XML/SupportedQueryGrammar.php new file mode 100644 index 00000000..a24e7b1f --- /dev/null +++ b/3rdparty/icewind/searchdav/src/XML/SupportedQueryGrammar.php @@ -0,0 +1,38 @@ + + * + * @license GNU AGPL version 3 or any later version + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU Affero General Public License as + * published by the Free Software Foundation, either version 3 of the + * License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Affero General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License + * along with this program. If not, see . + * + */ + +namespace SearchDAV\XML; + +use Sabre\Xml\Writer; +use Sabre\Xml\XmlSerializable; + +class SupportedQueryGrammar implements XmlSerializable { + const GRAMMAR_BASIC_SEARCH = '{DAV:}basicsearch'; + + public function xmlSerialize(Writer $writer): void { + $writer->startElement('{DAV:}supported-query-grammar'); + $writer->startElement('{DAV:}grammar'); + $writer->startElement(self::GRAMMAR_BASIC_SEARCH); + $writer->endElement(); + $writer->endElement(); + $writer->endElement(); + } +} diff --git a/3rdparty/icewind/smb/LICENSE.txt b/3rdparty/icewind/smb/LICENSE.txt new file mode 100644 index 00000000..fa0495d9 --- /dev/null +++ b/3rdparty/icewind/smb/LICENSE.txt @@ -0,0 +1,19 @@ +Copyright (c) 2014 Robin Appelman + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in +all copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +THE SOFTWARE. diff --git a/3rdparty/icewind/smb/LICENSES/AGPL-3.0-or-later.txt b/3rdparty/icewind/smb/LICENSES/AGPL-3.0-or-later.txt new file mode 100644 index 00000000..0c97efd2 --- /dev/null +++ b/3rdparty/icewind/smb/LICENSES/AGPL-3.0-or-later.txt @@ -0,0 +1,235 @@ +GNU AFFERO GENERAL PUBLIC LICENSE +Version 3, 19 November 2007 + +Copyright (C) 2007 Free Software Foundation, Inc. + +Everyone is permitted to copy and distribute verbatim copies of this license document, but changing it is not allowed. + + Preamble + +The GNU Affero General Public License is a free, copyleft license for software and other kinds of works, specifically designed to ensure cooperation with the community in the case of network server software. + +The licenses for most software and other practical works are designed to take away your freedom to share and change the works. By contrast, our General Public Licenses are intended to guarantee your freedom to share and change all versions of a program--to make sure it remains free software for all its users. + +When we speak of free software, we are referring to freedom, not price. Our General Public Licenses are designed to make sure that you have the freedom to distribute copies of free software (and charge for them if you wish), that you receive source code or can get it if you want it, that you can change the software or use pieces of it in new free programs, and that you know you can do these things. + +Developers that use our General Public Licenses protect your rights with two steps: (1) assert copyright on the software, and (2) offer you this License which gives you legal permission to copy, distribute and/or modify the software. + +A secondary benefit of defending all users' freedom is that improvements made in alternate versions of the program, if they receive widespread use, become available for other developers to incorporate. Many developers of free software are heartened and encouraged by the resulting cooperation. However, in the case of software used on network servers, this result may fail to come about. The GNU General Public License permits making a modified version and letting the public access it on a server without ever releasing its source code to the public. + +The GNU Affero General Public License is designed specifically to ensure that, in such cases, the modified source code becomes available to the community. It requires the operator of a network server to provide the source code of the modified version running there to the users of that server. Therefore, public use of a modified version, on a publicly accessible server, gives the public access to the source code of the modified version. + +An older license, called the Affero General Public License and published by Affero, was designed to accomplish similar goals. This is a different license, not a version of the Affero GPL, but Affero has released a new version of the Affero GPL which permits relicensing under this license. + +The precise terms and conditions for copying, distribution and modification follow. + + TERMS AND CONDITIONS + +0. Definitions. + +"This License" refers to version 3 of the GNU Affero General Public License. + +"Copyright" also means copyright-like laws that apply to other kinds of works, such as semiconductor masks. + +"The Program" refers to any copyrightable work licensed under this License. Each licensee is addressed as "you". "Licensees" and "recipients" may be individuals or organizations. + +To "modify" a work means to copy from or adapt all or part of the work in a fashion requiring copyright permission, other than the making of an exact copy. The resulting work is called a "modified version" of the earlier work or a work "based on" the earlier work. + +A "covered work" means either the unmodified Program or a work based on the Program. + +To "propagate" a work means to do anything with it that, without permission, would make you directly or secondarily liable for infringement under applicable copyright law, except executing it on a computer or modifying a private copy. Propagation includes copying, distribution (with or without modification), making available to the public, and in some countries other activities as well. + +To "convey" a work means any kind of propagation that enables other parties to make or receive copies. Mere interaction with a user through a computer network, with no transfer of a copy, is not conveying. + +An interactive user interface displays "Appropriate Legal Notices" to the extent that it includes a convenient and prominently visible feature that (1) displays an appropriate copyright notice, and (2) tells the user that there is no warranty for the work (except to the extent that warranties are provided), that licensees may convey the work under this License, and how to view a copy of this License. If the interface presents a list of user commands or options, such as a menu, a prominent item in the list meets this criterion. + +1. Source Code. +The "source code" for a work means the preferred form of the work for making modifications to it. "Object code" means any non-source form of a work. + +A "Standard Interface" means an interface that either is an official standard defined by a recognized standards body, or, in the case of interfaces specified for a particular programming language, one that is widely used among developers working in that language. + +The "System Libraries" of an executable work include anything, other than the work as a whole, that (a) is included in the normal form of packaging a Major Component, but which is not part of that Major Component, and (b) serves only to enable use of the work with that Major Component, or to implement a Standard Interface for which an implementation is available to the public in source code form. A "Major Component", in this context, means a major essential component (kernel, window system, and so on) of the specific operating system (if any) on which the executable work runs, or a compiler used to produce the work, or an object code interpreter used to run it. + +The "Corresponding Source" for a work in object code form means all the source code needed to generate, install, and (for an executable work) run the object code and to modify the work, including scripts to control those activities. However, it does not include the work's System Libraries, or general-purpose tools or generally available free programs which are used unmodified in performing those activities but which are not part of the work. For example, Corresponding Source includes interface definition files associated with source files for the work, and the source code for shared libraries and dynamically linked subprograms that the work is specifically designed to require, such as by intimate data communication or control flow between those +subprograms and other parts of the work. + +The Corresponding Source need not include anything that users can regenerate automatically from other parts of the Corresponding Source. + +The Corresponding Source for a work in source code form is that same work. + +2. Basic Permissions. +All rights granted under this License are granted for the term of copyright on the Program, and are irrevocable provided the stated conditions are met. This License explicitly affirms your unlimited permission to run the unmodified Program. The output from running a covered work is covered by this License only if the output, given its content, constitutes a covered work. This License acknowledges your rights of fair use or other equivalent, as provided by copyright law. + +You may make, run and propagate covered works that you do not convey, without conditions so long as your license otherwise remains in force. You may convey covered works to others for the sole purpose of having them make modifications exclusively for you, or provide you with facilities for running those works, provided that you comply with the terms of this License in conveying all material for which you do not control copyright. Those thus making or running the covered works for you must do so exclusively on your behalf, under your direction and control, on terms that prohibit them from making any copies of your copyrighted material outside their relationship with you. + +Conveying under any other circumstances is permitted solely under the conditions stated below. Sublicensing is not allowed; section 10 makes it unnecessary. + +3. Protecting Users' Legal Rights From Anti-Circumvention Law. +No covered work shall be deemed part of an effective technological measure under any applicable law fulfilling obligations under article 11 of the WIPO copyright treaty adopted on 20 December 1996, or similar laws prohibiting or restricting circumvention of such measures. + +When you convey a covered work, you waive any legal power to forbid circumvention of technological measures to the extent such circumvention is effected by exercising rights under this License with respect to the covered work, and you disclaim any intention to limit operation or modification of the work as a means of enforcing, against the work's users, your or third parties' legal rights to forbid circumvention of technological measures. + +4. Conveying Verbatim Copies. +You may convey verbatim copies of the Program's source code as you receive it, in any medium, provided that you conspicuously and appropriately publish on each copy an appropriate copyright notice; keep intact all notices stating that this License and any non-permissive terms added in accord with section 7 apply to the code; keep intact all notices of the absence of any warranty; and give all recipients a copy of this License along with the Program. + +You may charge any price or no price for each copy that you convey, and you may offer support or warranty protection for a fee. + +5. Conveying Modified Source Versions. +You may convey a work based on the Program, or the modifications to produce it from the Program, in the form of source code under the terms of section 4, provided that you also meet all of these conditions: + + a) The work must carry prominent notices stating that you modified it, and giving a relevant date. + + b) The work must carry prominent notices stating that it is released under this License and any conditions added under section 7. This requirement modifies the requirement in section 4 to "keep intact all notices". + + c) You must license the entire work, as a whole, under this License to anyone who comes into possession of a copy. This License will therefore apply, along with any applicable section 7 additional terms, to the whole of the work, and all its parts, regardless of how they are packaged. This License gives no permission to license the work in any other way, but it does not invalidate such permission if you have separately received it. + + d) If the work has interactive user interfaces, each must display Appropriate Legal Notices; however, if the Program has interactive interfaces that do not display Appropriate Legal Notices, your work need not make them do so. + +A compilation of a covered work with other separate and independent works, which are not by their nature extensions of the covered work, and which are not combined with it such as to form a larger program, in or on a volume of a storage or distribution medium, is called an "aggregate" if the compilation and its resulting copyright are not used to limit the access or legal rights of the compilation's users beyond what the individual works permit. Inclusion of a covered work in an aggregate does not cause this License to apply to the other parts of the aggregate. + +6. Conveying Non-Source Forms. +You may convey a covered work in object code form under the terms of sections 4 and 5, provided that you also convey the machine-readable Corresponding Source under the terms of this License, in one of these ways: + + a) Convey the object code in, or embodied in, a physical product (including a physical distribution medium), accompanied by the Corresponding Source fixed on a durable physical medium customarily used for software interchange. + + b) Convey the object code in, or embodied in, a physical product (including a physical distribution medium), accompanied by a written offer, valid for at least three years and valid for as long as you offer spare parts or customer support for that product model, to give anyone who possesses the object code either (1) a copy of the Corresponding Source for all the software in the product that is covered by this License, on a durable physical medium customarily used for software interchange, for a price no more than your reasonable cost of physically performing this conveying of source, or (2) access to copy the Corresponding Source from a network server at no charge. + + c) Convey individual copies of the object code with a copy of the written offer to provide the Corresponding Source. This alternative is allowed only occasionally and noncommercially, and only if you received the object code with such an offer, in accord with subsection 6b. + + d) Convey the object code by offering access from a designated place (gratis or for a charge), and offer equivalent access to the Corresponding Source in the same way through the same place at no further charge. You need not require recipients to copy the Corresponding Source along with the object code. If the place to copy the object code is a network server, the Corresponding Source may be on a different server (operated by you or a third party) that supports equivalent copying facilities, provided you maintain clear directions next to the object code saying where to find the Corresponding Source. Regardless of what server hosts the Corresponding Source, you remain obligated to ensure that it is available for as long as needed to satisfy these requirements. + + e) Convey the object code using peer-to-peer transmission, provided you inform other peers where the object code and Corresponding Source of the work are being offered to the general public at no charge under subsection 6d. + +A separable portion of the object code, whose source code is excluded from the Corresponding Source as a System Library, need not be included in conveying the object code work. + +A "User Product" is either (1) a "consumer product", which means any tangible personal property which is normally used for personal, family, or household purposes, or (2) anything designed or sold for incorporation into a dwelling. In determining whether a product is a consumer product, doubtful cases shall be resolved in favor of coverage. For a particular product received by a particular user, "normally used" refers to a typical or common use of that class of product, regardless of the status of the particular user or of the way in which the particular user actually uses, or expects or is expected to use, the product. A product is a consumer product regardless of whether the product has substantial commercial, industrial or non-consumer uses, unless such uses represent the only significant mode of use of the product. + +"Installation Information" for a User Product means any methods, procedures, authorization keys, or other information required to install and execute modified versions of a covered work in that User Product from a modified version of its Corresponding Source. The information must suffice to ensure that the continued functioning of the modified object code is in no case prevented or interfered with solely because modification has been made. + +If you convey an object code work under this section in, or with, or specifically for use in, a User Product, and the conveying occurs as part of a transaction in which the right of possession and use of the User Product is transferred to the recipient in perpetuity or for a fixed term (regardless of how the transaction is characterized), the Corresponding Source conveyed under this section must be accompanied by the Installation Information. But this requirement does not apply if neither you nor any third party retains the ability to install modified object code on the User Product (for example, the work has been installed in ROM). + +The requirement to provide Installation Information does not include a requirement to continue to provide support service, warranty, or updates for a work that has been modified or installed by the recipient, or for the User Product in which it has been modified or installed. Access to a network may be denied when the modification itself materially and adversely affects the operation of the network or violates the rules and protocols for communication across the network. + +Corresponding Source conveyed, and Installation Information provided, in accord with this section must be in a format that is publicly documented (and with an implementation available to the public in source code form), and must require no special password or key for unpacking, reading or copying. + +7. Additional Terms. +"Additional permissions" are terms that supplement the terms of this License by making exceptions from one or more of its conditions. Additional permissions that are applicable to the entire Program shall be treated as though they were included in this License, to the extent that they are valid under applicable law. If additional permissions apply only to part of the Program, that part may be used separately under those permissions, but the entire Program remains governed by this License without regard to the additional permissions. + +When you convey a copy of a covered work, you may at your option remove any additional permissions from that copy, or from any part of it. (Additional permissions may be written to require their own removal in certain cases when you modify the work.) You may place additional permissions on material, added by you to a covered work, for which you have or can give appropriate copyright permission. + +Notwithstanding any other provision of this License, for material you add to a covered work, you may (if authorized by the copyright holders of that material) supplement the terms of this License with terms: + + a) Disclaiming warranty or limiting liability differently from the terms of sections 15 and 16 of this License; or + + b) Requiring preservation of specified reasonable legal notices or author attributions in that material or in the Appropriate Legal Notices displayed by works containing it; or + + c) Prohibiting misrepresentation of the origin of that material, or requiring that modified versions of such material be marked in reasonable ways as different from the original version; or + + d) Limiting the use for publicity purposes of names of licensors or authors of the material; or + + e) Declining to grant rights under trademark law for use of some trade names, trademarks, or service marks; or + + f) Requiring indemnification of licensors and authors of that material by anyone who conveys the material (or modified versions of it) with contractual assumptions of liability to the recipient, for any liability that these contractual assumptions directly impose on those licensors and authors. + +All other non-permissive additional terms are considered "further restrictions" within the meaning of section 10. If the Program as you received it, or any part of it, contains a notice stating that it is governed by this License along with a term that is a further restriction, you may remove that term. If a license document contains a further restriction but permits relicensing or conveying under this License, you may add to a covered work material governed by the terms of that license document, provided that the further restriction does not survive such relicensing or conveying. + +If you add terms to a covered work in accord with this section, you must place, in the relevant source files, a statement of the additional terms that apply to those files, or a notice indicating where to find the applicable terms. + +Additional terms, permissive or non-permissive, may be stated in the form of a separately written license, or stated as exceptions; the above requirements apply either way. + +8. Termination. + +You may not propagate or modify a covered work except as expressly provided under this License. Any attempt otherwise to propagate or modify it is void, and will automatically terminate your rights under this License (including any patent licenses granted under the third paragraph of section 11). + +However, if you cease all violation of this License, then your license from a particular copyright holder is reinstated (a) provisionally, unless and until the copyright holder explicitly and finally terminates your license, and (b) permanently, if the copyright holder fails to notify you of the violation by some reasonable means prior to 60 days after the cessation. + +Moreover, your license from a particular copyright holder is reinstated permanently if the copyright holder notifies you of the violation by some reasonable means, this is the first time you have received notice of violation of this License (for any work) from that copyright holder, and you cure the violation prior to 30 days after your receipt of the notice. + +Termination of your rights under this section does not terminate the licenses of parties who have received copies or rights from you under this License. If your rights have been terminated and not permanently reinstated, you do not qualify to receive new licenses for the same material under section 10. + +9. Acceptance Not Required for Having Copies. + +You are not required to accept this License in order to receive or run a copy of the Program. Ancillary propagation of a covered work occurring solely as a consequence of using peer-to-peer transmission to receive a copy likewise does not require acceptance. However, nothing other than this License grants you permission to propagate or modify any covered work. These actions infringe copyright if you do not accept this License. Therefore, by modifying or propagating a covered work, you indicate your acceptance of this License to do so. + +10. Automatic Licensing of Downstream Recipients. + +Each time you convey a covered work, the recipient automatically receives a license from the original licensors, to run, modify and propagate that work, subject to this License. You are not responsible for enforcing compliance by third parties with this License. + +An "entity transaction" is a transaction transferring control of an organization, or substantially all assets of one, or subdividing an organization, or merging organizations. If propagation of a covered work results from an entity transaction, each party to that transaction who receives a copy of the work also receives whatever licenses to the work the party's predecessor in interest had or could give under the previous paragraph, plus a right to possession of the Corresponding Source of the work from the predecessor in interest, if the predecessor has it or can get it with reasonable efforts. + +You may not impose any further restrictions on the exercise of the rights granted or affirmed under this License. For example, you may not impose a license fee, royalty, or other charge for exercise of rights granted under this License, and you may not initiate litigation (including a cross-claim or counterclaim in a lawsuit) alleging that any patent claim is infringed by making, using, selling, offering for sale, or importing the Program or any portion of it. + +11. Patents. + +A "contributor" is a copyright holder who authorizes use under this License of the Program or a work on which the Program is based. The work thus licensed is called the contributor's "contributor version". + +A contributor's "essential patent claims" are all patent claims owned or controlled by the contributor, whether already acquired or hereafter acquired, that would be infringed by some manner, permitted by this License, of making, using, or selling its contributor version, but do not include claims that would be infringed only as a consequence of further modification of the contributor version. For purposes of this definition, "control" includes the right to grant patent sublicenses in a manner consistent with the requirements of this License. + +Each contributor grants you a non-exclusive, worldwide, royalty-free patent license under the contributor's essential patent claims, to make, use, sell, offer for sale, import and otherwise run, modify and propagate the contents of its contributor version. + +In the following three paragraphs, a "patent license" is any express agreement or commitment, however denominated, not to enforce a patent (such as an express permission to practice a patent or covenant not to sue for patent infringement). To "grant" such a patent license to a party means to make such an agreement or commitment not to enforce a patent against the party. + +If you convey a covered work, knowingly relying on a patent license, and the Corresponding Source of the work is not available for anyone to copy, free of charge and under the terms of this License, through a publicly available network server or other readily accessible means, then you must either (1) cause the Corresponding Source to be so available, or (2) arrange to deprive yourself of the benefit of the patent license for this particular work, or (3) arrange, in a manner consistent with the requirements of this License, to extend the patent +license to downstream recipients. "Knowingly relying" means you have actual knowledge that, but for the patent license, your conveying the covered work in a country, or your recipient's use of the covered work in a country, would infringe one or more identifiable patents in that country that you have reason to believe are valid. + +If, pursuant to or in connection with a single transaction or arrangement, you convey, or propagate by procuring conveyance of, a covered work, and grant a patent license to some of the parties receiving the covered work authorizing them to use, propagate, modify or convey a specific copy of the covered work, then the patent license you grant is automatically extended to all recipients of the covered work and works based on it. + +A patent license is "discriminatory" if it does not include within the scope of its coverage, prohibits the exercise of, or is conditioned on the non-exercise of one or more of the rights that are specifically granted under this License. You may not convey a covered work if you are a party to an arrangement with a third party that is in the business of distributing software, under which you make payment to the third party based on the extent of your activity of conveying the work, and under which the third party grants, to any of the parties who would receive the covered work from you, a discriminatory patent license (a) in connection with copies of the covered work conveyed by you (or copies made from those copies), or (b) primarily for and in connection with specific products or compilations that contain the covered work, unless you entered into that arrangement, or that patent license was granted, prior to 28 March 2007. + +Nothing in this License shall be construed as excluding or limiting any implied license or other defenses to infringement that may otherwise be available to you under applicable patent law. + +12. No Surrender of Others' Freedom. + +If conditions are imposed on you (whether by court order, agreement or otherwise) that contradict the conditions of this License, they do not excuse you from the conditions of this License. If you cannot convey a covered work so as to satisfy simultaneously your obligations under this License and any other pertinent obligations, then as a consequence you may +not convey it at all. For example, if you agree to terms that obligate you to collect a royalty for further conveying from those to whom you convey the Program, the only way you could satisfy both those terms and this License would be to refrain entirely from conveying the Program. + +13. Remote Network Interaction; Use with the GNU General Public License. + +Notwithstanding any other provision of this License, if you modify the Program, your modified version must prominently offer all users interacting with it remotely through a computer network (if your version supports such interaction) an opportunity to receive the Corresponding Source of your version by providing access to the Corresponding Source from a network server at no charge, through some standard or customary means of facilitating copying of software. This Corresponding Source shall include the Corresponding Source for any work covered by version 3 of the GNU General Public License that is incorporated pursuant to the following paragraph. + +Notwithstanding any other provision of this License, you have permission to link or combine any covered work with a work licensed under version 3 of the GNU General Public License into a single combined work, and to convey the resulting work. The terms of this License will continue to apply to the part which is the covered work, but the work with which it is combined will remain governed by version 3 of the GNU General Public License. + +14. Revised Versions of this License. + +The Free Software Foundation may publish revised and/or new versions of the GNU Affero General Public License from time to time. Such new versions will be similar in spirit to the present version, but may differ in detail to address new problems or concerns. + +Each version is given a distinguishing version number. If the Program specifies that a certain numbered version of the GNU Affero General Public License "or any later version" applies to it, you have the option of following the terms and conditions either of that numbered version or of any later version published by the Free Software Foundation. If the Program does not specify a version number of the GNU Affero General Public License, you may choose any version ever published by the Free Software Foundation. + +If the Program specifies that a proxy can decide which future versions of the GNU Affero General Public License can be used, that proxy's public statement of acceptance of a version permanently authorizes you to choose that version for the Program. + +Later license versions may give you additional or different permissions. However, no additional obligations are imposed on any author or copyright holder as a result of your choosing to follow a later version. + +15. Disclaimer of Warranty. + +THERE IS NO WARRANTY FOR THE PROGRAM, TO THE EXTENT PERMITTED BY APPLICABLE LAW. EXCEPT WHEN OTHERWISE STATED IN WRITING THE COPYRIGHT HOLDERS AND/OR OTHER PARTIES PROVIDE THE PROGRAM "AS IS" WITHOUT WARRANTY OF ANY KIND, EITHER EXPRESSED OR IMPLIED, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE. THE ENTIRE RISK AS TO THE QUALITY AND PERFORMANCE OF THE PROGRAM IS WITH YOU. SHOULD THE PROGRAM PROVE DEFECTIVE, YOU ASSUME THE COST OF ALL NECESSARY SERVICING, REPAIR OR CORRECTION. + +16. Limitation of Liability. + +IN NO EVENT UNLESS REQUIRED BY APPLICABLE LAW OR AGREED TO IN WRITING WILL ANY COPYRIGHT HOLDER, OR ANY OTHER PARTY WHO MODIFIES AND/OR CONVEYS THE PROGRAM AS PERMITTED ABOVE, BE LIABLE TO YOU FOR DAMAGES, INCLUDING ANY GENERAL, SPECIAL, INCIDENTAL OR CONSEQUENTIAL DAMAGES ARISING OUT OF THE USE OR INABILITY TO USE THE PROGRAM (INCLUDING BUT NOT LIMITED TO LOSS OF DATA OR DATA BEING RENDERED INACCURATE OR LOSSES SUSTAINED BY YOU OR THIRD PARTIES OR A FAILURE OF THE PROGRAM TO OPERATE WITH ANY OTHER PROGRAMS), EVEN IF SUCH HOLDER OR OTHER PARTY HAS BEEN ADVISED OF THE POSSIBILITY OF SUCH DAMAGES. + +17. Interpretation of Sections 15 and 16. + +If the disclaimer of warranty and limitation of liability provided above cannot be given local legal effect according to their terms, reviewing courts shall apply local law that most closely approximates an absolute waiver of all civil liability in connection with the Program, unless a warranty or assumption of liability accompanies a copy of the Program in return for a fee. + +END OF TERMS AND CONDITIONS + + How to Apply These Terms to Your New Programs + +If you develop a new program, and you want it to be of the greatest possible use to the public, the best way to achieve this is to make it free software which everyone can redistribute and change under these terms. + +To do so, attach the following notices to the program. It is safest to attach them to the start of each source file to most effectively state the exclusion of warranty; and each file should have at least the "copyright" line and a pointer to where the full notice is found. + + + Copyright (C) + + This program is free software: you can redistribute it and/or modify it under the terms of the GNU Affero General Public License as published by the Free Software Foundation, either version 3 of the License, or (at your option) any later version. + + This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU Affero General Public License for more details. + + You should have received a copy of the GNU Affero General Public License along with this program. If not, see . + +Also add information on how to contact you by electronic and paper mail. + +If your software can interact with users remotely through a computer network, you should also make sure that it provides a way for users to get its source. For example, if your program is a web application, its interface could display a "Source" link that leads users to an archive of the code. There are many ways you could offer source, and different solutions will be better for different programs; see section 13 for the specific requirements. + +You should also get your employer (if you work as a programmer) or school, if any, to sign a "copyright disclaimer" for the program, if necessary. For more information on this, and how to apply and follow the GNU AGPL, see . diff --git a/3rdparty/icewind/smb/LICENSES/CC0-1.0.txt b/3rdparty/icewind/smb/LICENSES/CC0-1.0.txt new file mode 100644 index 00000000..0e259d42 --- /dev/null +++ b/3rdparty/icewind/smb/LICENSES/CC0-1.0.txt @@ -0,0 +1,121 @@ +Creative Commons Legal Code + +CC0 1.0 Universal + + CREATIVE COMMONS CORPORATION IS NOT A LAW FIRM AND DOES NOT PROVIDE + LEGAL SERVICES. DISTRIBUTION OF THIS DOCUMENT DOES NOT CREATE AN + ATTORNEY-CLIENT RELATIONSHIP. CREATIVE COMMONS PROVIDES THIS + INFORMATION ON AN "AS-IS" BASIS. CREATIVE COMMONS MAKES NO WARRANTIES + REGARDING THE USE OF THIS DOCUMENT OR THE INFORMATION OR WORKS + PROVIDED HEREUNDER, AND DISCLAIMS LIABILITY FOR DAMAGES RESULTING FROM + THE USE OF THIS DOCUMENT OR THE INFORMATION OR WORKS PROVIDED + HEREUNDER. + +Statement of Purpose + +The laws of most jurisdictions throughout the world automatically confer +exclusive Copyright and Related Rights (defined below) upon the creator +and subsequent owner(s) (each and all, an "owner") of an original work of +authorship and/or a database (each, a "Work"). + +Certain owners wish to permanently relinquish those rights to a Work for +the purpose of contributing to a commons of creative, cultural and +scientific works ("Commons") that the public can reliably and without fear +of later claims of infringement build upon, modify, incorporate in other +works, reuse and redistribute as freely as possible in any form whatsoever +and for any purposes, including without limitation commercial purposes. +These owners may contribute to the Commons to promote the ideal of a free +culture and the further production of creative, cultural and scientific +works, or to gain reputation or greater distribution for their Work in +part through the use and efforts of others. + +For these and/or other purposes and motivations, and without any +expectation of additional consideration or compensation, the person +associating CC0 with a Work (the "Affirmer"), to the extent that he or she +is an owner of Copyright and Related Rights in the Work, voluntarily +elects to apply CC0 to the Work and publicly distribute the Work under its +terms, with knowledge of his or her Copyright and Related Rights in the +Work and the meaning and intended legal effect of CC0 on those rights. + +1. Copyright and Related Rights. A Work made available under CC0 may be +protected by copyright and related or neighboring rights ("Copyright and +Related Rights"). Copyright and Related Rights include, but are not +limited to, the following: + + i. the right to reproduce, adapt, distribute, perform, display, + communicate, and translate a Work; + ii. moral rights retained by the original author(s) and/or performer(s); +iii. publicity and privacy rights pertaining to a person's image or + likeness depicted in a Work; + iv. rights protecting against unfair competition in regards to a Work, + subject to the limitations in paragraph 4(a), below; + v. rights protecting the extraction, dissemination, use and reuse of data + in a Work; + vi. database rights (such as those arising under Directive 96/9/EC of the + European Parliament and of the Council of 11 March 1996 on the legal + protection of databases, and under any national implementation + thereof, including any amended or successor version of such + directive); and +vii. other similar, equivalent or corresponding rights throughout the + world based on applicable law or treaty, and any national + implementations thereof. + +2. Waiver. To the greatest extent permitted by, but not in contravention +of, applicable law, Affirmer hereby overtly, fully, permanently, +irrevocably and unconditionally waives, abandons, and surrenders all of +Affirmer's Copyright and Related Rights and associated claims and causes +of action, whether now known or unknown (including existing as well as +future claims and causes of action), in the Work (i) in all territories +worldwide, (ii) for the maximum duration provided by applicable law or +treaty (including future time extensions), (iii) in any current or future +medium and for any number of copies, and (iv) for any purpose whatsoever, +including without limitation commercial, advertising or promotional +purposes (the "Waiver"). Affirmer makes the Waiver for the benefit of each +member of the public at large and to the detriment of Affirmer's heirs and +successors, fully intending that such Waiver shall not be subject to +revocation, rescission, cancellation, termination, or any other legal or +equitable action to disrupt the quiet enjoyment of the Work by the public +as contemplated by Affirmer's express Statement of Purpose. + +3. Public License Fallback. Should any part of the Waiver for any reason +be judged legally invalid or ineffective under applicable law, then the +Waiver shall be preserved to the maximum extent permitted taking into +account Affirmer's express Statement of Purpose. In addition, to the +extent the Waiver is so judged Affirmer hereby grants to each affected +person a royalty-free, non transferable, non sublicensable, non exclusive, +irrevocable and unconditional license to exercise Affirmer's Copyright and +Related Rights in the Work (i) in all territories worldwide, (ii) for the +maximum duration provided by applicable law or treaty (including future +time extensions), (iii) in any current or future medium and for any number +of copies, and (iv) for any purpose whatsoever, including without +limitation commercial, advertising or promotional purposes (the +"License"). The License shall be deemed effective as of the date CC0 was +applied by Affirmer to the Work. Should any part of the License for any +reason be judged legally invalid or ineffective under applicable law, such +partial invalidity or ineffectiveness shall not invalidate the remainder +of the License, and in such case Affirmer hereby affirms that he or she +will not (i) exercise any of his or her remaining Copyright and Related +Rights in the Work or (ii) assert any associated claims and causes of +action with respect to the Work, in either case contrary to Affirmer's +express Statement of Purpose. + +4. Limitations and Disclaimers. + + a. No trademark or patent rights held by Affirmer are waived, abandoned, + surrendered, licensed or otherwise affected by this document. + b. Affirmer offers the Work as-is and makes no representations or + warranties of any kind concerning the Work, express, implied, + statutory or otherwise, including without limitation warranties of + title, merchantability, fitness for a particular purpose, non + infringement, or the absence of latent or other defects, accuracy, or + the present or absence of errors, whether or not discoverable, all to + the greatest extent permissible under applicable law. + c. Affirmer disclaims responsibility for clearing rights of other persons + that may apply to the Work or any use thereof, including without + limitation any person's Copyright and Related Rights in the Work. + Further, Affirmer disclaims responsibility for obtaining any necessary + consents, permissions or other rights required for any use of the + Work. + d. Affirmer understands and acknowledges that Creative Commons is not a + party to this document and has no duty or obligation with respect to + this CC0 or use of the Work. diff --git a/3rdparty/icewind/smb/LICENSES/MIT.txt b/3rdparty/icewind/smb/LICENSES/MIT.txt new file mode 100644 index 00000000..2071b23b --- /dev/null +++ b/3rdparty/icewind/smb/LICENSES/MIT.txt @@ -0,0 +1,9 @@ +MIT License + +Copyright (c) + +Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. diff --git a/3rdparty/icewind/smb/src/ACL.php b/3rdparty/icewind/smb/src/ACL.php new file mode 100644 index 00000000..9d83cd15 --- /dev/null +++ b/3rdparty/icewind/smb/src/ACL.php @@ -0,0 +1,69 @@ + + * SPDX-License-Identifier: AGPL-3.0-or-later + */ + +namespace Icewind\SMB; + +class ACL { + const TYPE_ALLOW = 0; + const TYPE_DENY = 1; + + const MASK_READ = 0x0001; + const MASK_WRITE = 0x0002; + const MASK_EXECUTE = 0x00020; + const MASK_DELETE = 0x10000; + + const FLAG_OBJECT_INHERIT = 0x1; + const FLAG_CONTAINER_INHERIT = 0x2; + + /** @var int */ + private $type; + /** @var int */ + private $flags; + /** @var int */ + private $mask; + + public function __construct(int $type, int $flags, int $mask) { + $this->type = $type; + $this->flags = $flags; + $this->mask = $mask; + } + + /** + * Check if the acl allows a specific permissions + * + * Note that this does not take inherited acls into account + * + * @param int $mask one of the ACL::MASK_* constants + * @return bool + */ + public function allows(int $mask): bool { + return $this->type === self::TYPE_ALLOW && ($this->mask & $mask) === $mask; + } + + /** + * Check if the acl allows a specific permissions + * + * Note that this does not take inherited acls into account + * + * @param int $mask one of the ACL::MASK_* constants + * @return bool + */ + public function denies(int $mask): bool { + return $this->type === self::TYPE_DENY && ($this->mask & $mask) === $mask; + } + + public function getType(): int { + return $this->type; + } + + public function getFlags(): int { + return $this->flags; + } + + public function getMask(): int { + return $this->mask; + } +} diff --git a/3rdparty/icewind/smb/src/AbstractServer.php b/3rdparty/icewind/smb/src/AbstractServer.php new file mode 100644 index 00000000..fe22fb10 --- /dev/null +++ b/3rdparty/icewind/smb/src/AbstractServer.php @@ -0,0 +1,61 @@ + + * SPDX-License-Identifier: AGPL-3.0-or-later + */ + +namespace Icewind\SMB; + +abstract class AbstractServer implements IServer { + const LOCALE = 'en_US.UTF-8'; + + /** @var string */ + protected $host; + + /** @var IAuth */ + protected $auth; + + /** @var ISystem */ + protected $system; + + /** @var ITimeZoneProvider */ + protected $timezoneProvider; + + /** @var IOptions */ + protected $options; + + /** + * @param string $host + * @param IAuth $auth + * @param ISystem $system + * @param ITimeZoneProvider $timeZoneProvider + * @param IOptions $options + */ + public function __construct(string $host, IAuth $auth, ISystem $system, ITimeZoneProvider $timeZoneProvider, IOptions $options) { + $this->host = $host; + $this->auth = $auth; + $this->system = $system; + $this->timezoneProvider = $timeZoneProvider; + $this->options = $options; + } + + public function getAuth(): IAuth { + return $this->auth; + } + + public function getHost(): string { + return $this->host; + } + + public function getTimeZone(): string { + return $this->timezoneProvider->get($this->host); + } + + public function getSystem(): ISystem { + return $this->system; + } + + public function getOptions(): IOptions { + return $this->options; + } +} diff --git a/3rdparty/icewind/smb/src/AbstractShare.php b/3rdparty/icewind/smb/src/AbstractShare.php new file mode 100644 index 00000000..77f50e4c --- /dev/null +++ b/3rdparty/icewind/smb/src/AbstractShare.php @@ -0,0 +1,37 @@ + + * SPDX-License-Identifier: MIT + */ + +namespace Icewind\SMB; + +use Icewind\SMB\Exception\InvalidPathException; + +abstract class AbstractShare implements IShare { + /** @var string[] */ + private $forbiddenCharacters; + + public function __construct() { + $this->forbiddenCharacters = ['?', '<', '>', ':', '*', '|', '"', chr(0), "\n", "\r"]; + } + + /** + * @param string $path + * @throws InvalidPathException + */ + protected function verifyPath(string $path): void { + foreach ($this->forbiddenCharacters as $char) { + if (strpos($path, $char) !== false) { + throw new InvalidPathException('Invalid path, "' . $char . '" is not allowed'); + } + } + } + + /** + * @param string[] $charList + */ + public function setForbiddenChars(array $charList): void { + $this->forbiddenCharacters = $charList; + } +} diff --git a/3rdparty/icewind/smb/src/AnonymousAuth.php b/3rdparty/icewind/smb/src/AnonymousAuth.php new file mode 100644 index 00000000..87bce166 --- /dev/null +++ b/3rdparty/icewind/smb/src/AnonymousAuth.php @@ -0,0 +1,33 @@ + + * SPDX-License-Identifier: AGPL-3.0-or-later + */ + +namespace Icewind\SMB; + +use Icewind\SMB\Exception\Exception; + +class AnonymousAuth implements IAuth { + public function getUsername(): ?string { + return null; + } + + public function getWorkgroup(): ?string { + return 'dummy'; + } + + public function getPassword(): ?string { + return null; + } + + public function getExtraCommandLineArguments(): string { + return '-N'; + } + + public function setExtraSmbClientOptions($smbClientState): void { + if (smbclient_option_set($smbClientState, SMBCLIENT_OPT_AUTO_ANONYMOUS_LOGIN, true) === false) { + throw new Exception("Failed to set smbclient options for anonymous auth"); + } + } +} diff --git a/3rdparty/icewind/smb/src/BasicAuth.php b/3rdparty/icewind/smb/src/BasicAuth.php new file mode 100644 index 00000000..a462109b --- /dev/null +++ b/3rdparty/icewind/smb/src/BasicAuth.php @@ -0,0 +1,42 @@ + + * SPDX-License-Identifier: AGPL-3.0-or-later + */ + +namespace Icewind\SMB; + +class BasicAuth implements IAuth { + /** @var string */ + private $username; + /** @var string|null */ + private $workgroup; + /** @var string */ + private $password; + + public function __construct(string $username, ?string $workgroup, string $password) { + $this->username = $username; + $this->workgroup = $workgroup; + $this->password = $password; + } + + public function getUsername(): ?string { + return $this->username; + } + + public function getWorkgroup(): ?string { + return $this->workgroup; + } + + public function getPassword(): ?string { + return $this->password; + } + + public function getExtraCommandLineArguments(): string { + return ($this->workgroup) ? '-W ' . escapeshellarg($this->workgroup) : ''; + } + + public function setExtraSmbClientOptions($smbClientState): void { + // noop + } +} diff --git a/3rdparty/icewind/smb/src/Change.php b/3rdparty/icewind/smb/src/Change.php new file mode 100644 index 00000000..c21297a2 --- /dev/null +++ b/3rdparty/icewind/smb/src/Change.php @@ -0,0 +1,27 @@ + + * SPDX-License-Identifier: MIT + */ + +namespace Icewind\SMB; + +class Change { + /** @var int */ + private $code; + /** @var string */ + private $path; + + public function __construct(int $code, string $path) { + $this->code = $code; + $this->path = $path; + } + + public function getCode(): int { + return $this->code; + } + + public function getPath(): string { + return $this->path; + } +} diff --git a/3rdparty/icewind/smb/src/Exception/AccessDeniedException.php b/3rdparty/icewind/smb/src/Exception/AccessDeniedException.php new file mode 100644 index 00000000..7e8a81d8 --- /dev/null +++ b/3rdparty/icewind/smb/src/Exception/AccessDeniedException.php @@ -0,0 +1,10 @@ + + * SPDX-License-Identifier: MIT + */ + +namespace Icewind\SMB\Exception; + +class AccessDeniedException extends ConnectException { +} diff --git a/3rdparty/icewind/smb/src/Exception/AlreadyExistsException.php b/3rdparty/icewind/smb/src/Exception/AlreadyExistsException.php new file mode 100644 index 00000000..7828efe8 --- /dev/null +++ b/3rdparty/icewind/smb/src/Exception/AlreadyExistsException.php @@ -0,0 +1,10 @@ + + * SPDX-License-Identifier: MIT + */ + +namespace Icewind\SMB\Exception; + +class AlreadyExistsException extends InvalidRequestException { +} diff --git a/3rdparty/icewind/smb/src/Exception/AuthenticationException.php b/3rdparty/icewind/smb/src/Exception/AuthenticationException.php new file mode 100644 index 00000000..bf51f5c2 --- /dev/null +++ b/3rdparty/icewind/smb/src/Exception/AuthenticationException.php @@ -0,0 +1,10 @@ + + * SPDX-License-Identifier: MIT + */ + +namespace Icewind\SMB\Exception; + +class AuthenticationException extends ConnectException { +} diff --git a/3rdparty/icewind/smb/src/Exception/ConnectException.php b/3rdparty/icewind/smb/src/Exception/ConnectException.php new file mode 100644 index 00000000..527e5835 --- /dev/null +++ b/3rdparty/icewind/smb/src/Exception/ConnectException.php @@ -0,0 +1,10 @@ + + * SPDX-License-Identifier: MIT + */ + +namespace Icewind\SMB\Exception; + +class ConnectException extends Exception { +} diff --git a/3rdparty/icewind/smb/src/Exception/ConnectionAbortedException.php b/3rdparty/icewind/smb/src/Exception/ConnectionAbortedException.php new file mode 100644 index 00000000..cc959c27 --- /dev/null +++ b/3rdparty/icewind/smb/src/Exception/ConnectionAbortedException.php @@ -0,0 +1,10 @@ + + * SPDX-License-Identifier: MIT + */ + +namespace Icewind\SMB\Exception; + +class ConnectionAbortedException extends ConnectException { +} diff --git a/3rdparty/icewind/smb/src/Exception/ConnectionException.php b/3rdparty/icewind/smb/src/Exception/ConnectionException.php new file mode 100644 index 00000000..deabfcd7 --- /dev/null +++ b/3rdparty/icewind/smb/src/Exception/ConnectionException.php @@ -0,0 +1,10 @@ + + * SPDX-License-Identifier: MIT + */ + +namespace Icewind\SMB\Exception; + +class ConnectionException extends ConnectException { +} diff --git a/3rdparty/icewind/smb/src/Exception/ConnectionRefusedException.php b/3rdparty/icewind/smb/src/Exception/ConnectionRefusedException.php new file mode 100644 index 00000000..826a1961 --- /dev/null +++ b/3rdparty/icewind/smb/src/Exception/ConnectionRefusedException.php @@ -0,0 +1,10 @@ + + * SPDX-License-Identifier: MIT + */ + +namespace Icewind\SMB\Exception; + +class ConnectionRefusedException extends ConnectException { +} diff --git a/3rdparty/icewind/smb/src/Exception/ConnectionResetException.php b/3rdparty/icewind/smb/src/Exception/ConnectionResetException.php new file mode 100644 index 00000000..464e752c --- /dev/null +++ b/3rdparty/icewind/smb/src/Exception/ConnectionResetException.php @@ -0,0 +1,10 @@ + + * SPDX-License-Identifier: MIT + */ + +namespace Icewind\SMB\Exception; + +class ConnectionResetException extends ConnectException { +} diff --git a/3rdparty/icewind/smb/src/Exception/DependencyException.php b/3rdparty/icewind/smb/src/Exception/DependencyException.php new file mode 100644 index 00000000..ed3d4279 --- /dev/null +++ b/3rdparty/icewind/smb/src/Exception/DependencyException.php @@ -0,0 +1,10 @@ + + * SPDX-License-Identifier: MIT + */ + +namespace Icewind\SMB\Exception; + +class DependencyException extends Exception { +} diff --git a/3rdparty/icewind/smb/src/Exception/Exception.php b/3rdparty/icewind/smb/src/Exception/Exception.php new file mode 100644 index 00000000..199cdf30 --- /dev/null +++ b/3rdparty/icewind/smb/src/Exception/Exception.php @@ -0,0 +1,51 @@ + + * SPDX-License-Identifier: MIT + */ + +namespace Icewind\SMB\Exception; + +use Throwable; + +/** + * @psalm-consistent-constructor + */ +class Exception extends \Exception { + public function __construct(string $message = "", int $code = 0, ?Throwable $previous = null) { + parent::__construct($message, $code, $previous); + } + + /** + * @param string|null $path + * @param string|int|null $error + * @return Exception + */ + public static function unknown(?string $path, $error): Exception { + $message = 'Unknown error (' . (string)$error . ')'; + if ($path) { + $message .= ' for ' . $path; + } + + return new Exception($message, is_int($error) ? $error : 0); + } + + /** + * @param array> $exceptionMap + * @param string|int|null $error + * @param string|null $path + * @return Exception + */ + public static function fromMap(array $exceptionMap, $error, ?string $path): Exception { + if (isset($exceptionMap[$error])) { + $exceptionClass = $exceptionMap[$error]; + if (is_numeric($error)) { + return new $exceptionClass($path, $error); + } else { + return new $exceptionClass($path); + } + } else { + return Exception::unknown($path, $error); + } + } +} diff --git a/3rdparty/icewind/smb/src/Exception/FileInUseException.php b/3rdparty/icewind/smb/src/Exception/FileInUseException.php new file mode 100644 index 00000000..44affabc --- /dev/null +++ b/3rdparty/icewind/smb/src/Exception/FileInUseException.php @@ -0,0 +1,10 @@ + + * SPDX-License-Identifier: MIT + */ + +namespace Icewind\SMB\Exception; + +class FileInUseException extends InvalidRequestException { +} diff --git a/3rdparty/icewind/smb/src/Exception/ForbiddenException.php b/3rdparty/icewind/smb/src/Exception/ForbiddenException.php new file mode 100644 index 00000000..2d070543 --- /dev/null +++ b/3rdparty/icewind/smb/src/Exception/ForbiddenException.php @@ -0,0 +1,10 @@ + + * SPDX-License-Identifier: MIT + */ + +namespace Icewind\SMB\Exception; + +class ForbiddenException extends InvalidRequestException { +} diff --git a/3rdparty/icewind/smb/src/Exception/HostDownException.php b/3rdparty/icewind/smb/src/Exception/HostDownException.php new file mode 100644 index 00000000..321f8d2f --- /dev/null +++ b/3rdparty/icewind/smb/src/Exception/HostDownException.php @@ -0,0 +1,10 @@ + + * SPDX-License-Identifier: MIT + */ + +namespace Icewind\SMB\Exception; + +class HostDownException extends ConnectException { +} diff --git a/3rdparty/icewind/smb/src/Exception/InvalidArgumentException.php b/3rdparty/icewind/smb/src/Exception/InvalidArgumentException.php new file mode 100644 index 00000000..69422392 --- /dev/null +++ b/3rdparty/icewind/smb/src/Exception/InvalidArgumentException.php @@ -0,0 +1,10 @@ + + * SPDX-License-Identifier: MIT + */ + +namespace Icewind\SMB\Exception; + +class InvalidArgumentException extends InvalidRequestException { +} diff --git a/3rdparty/icewind/smb/src/Exception/InvalidHostException.php b/3rdparty/icewind/smb/src/Exception/InvalidHostException.php new file mode 100644 index 00000000..630734ee --- /dev/null +++ b/3rdparty/icewind/smb/src/Exception/InvalidHostException.php @@ -0,0 +1,10 @@ + + * SPDX-License-Identifier: MIT + */ + +namespace Icewind\SMB\Exception; + +class InvalidHostException extends ConnectException { +} diff --git a/3rdparty/icewind/smb/src/Exception/InvalidParameterException.php b/3rdparty/icewind/smb/src/Exception/InvalidParameterException.php new file mode 100644 index 00000000..57a50a16 --- /dev/null +++ b/3rdparty/icewind/smb/src/Exception/InvalidParameterException.php @@ -0,0 +1,10 @@ + + * SPDX-License-Identifier: MIT + */ + +namespace Icewind\SMB\Exception; + +class InvalidParameterException extends InvalidRequestException { +} diff --git a/3rdparty/icewind/smb/src/Exception/InvalidPathException.php b/3rdparty/icewind/smb/src/Exception/InvalidPathException.php new file mode 100644 index 00000000..8b2ea3ae --- /dev/null +++ b/3rdparty/icewind/smb/src/Exception/InvalidPathException.php @@ -0,0 +1,10 @@ + + * SPDX-License-Identifier: MIT + */ + +namespace Icewind\SMB\Exception; + +class InvalidPathException extends InvalidRequestException { +} diff --git a/3rdparty/icewind/smb/src/Exception/InvalidRequestException.php b/3rdparty/icewind/smb/src/Exception/InvalidRequestException.php new file mode 100644 index 00000000..d6ee8db0 --- /dev/null +++ b/3rdparty/icewind/smb/src/Exception/InvalidRequestException.php @@ -0,0 +1,29 @@ + + * SPDX-License-Identifier: MIT + */ + +namespace Icewind\SMB\Exception; + +class InvalidRequestException extends Exception { + /** + * @var string + */ + protected $path; + + public function __construct(string $path = "", int $code = 0, ?\Throwable $previous = null) { + $class = get_class($this); + $parts = explode('\\', $class); + $baseName = array_pop($parts); + parent::__construct('Invalid request for ' . $path . ' (' . $baseName . ')', $code, $previous); + $this->path = $path; + } + + /** + * @return string + */ + public function getPath() { + return $this->path; + } +} diff --git a/3rdparty/icewind/smb/src/Exception/InvalidResourceException.php b/3rdparty/icewind/smb/src/Exception/InvalidResourceException.php new file mode 100644 index 00000000..95507e4f --- /dev/null +++ b/3rdparty/icewind/smb/src/Exception/InvalidResourceException.php @@ -0,0 +1,10 @@ + + * SPDX-License-Identifier: MIT + */ + +namespace Icewind\SMB\Exception; + +class InvalidResourceException extends Exception { +} diff --git a/3rdparty/icewind/smb/src/Exception/InvalidTicket.php b/3rdparty/icewind/smb/src/Exception/InvalidTicket.php new file mode 100644 index 00000000..eb718af3 --- /dev/null +++ b/3rdparty/icewind/smb/src/Exception/InvalidTicket.php @@ -0,0 +1,13 @@ + + * SPDX-License-Identifier: AGPL-3.0-or-later + */ + +namespace Icewind\SMB\Exception; + +class InvalidTicket extends Exception { + +} \ No newline at end of file diff --git a/3rdparty/icewind/smb/src/Exception/InvalidTypeException.php b/3rdparty/icewind/smb/src/Exception/InvalidTypeException.php new file mode 100644 index 00000000..4a5b12cb --- /dev/null +++ b/3rdparty/icewind/smb/src/Exception/InvalidTypeException.php @@ -0,0 +1,10 @@ + + * SPDX-License-Identifier: MIT + */ + +namespace Icewind\SMB\Exception; + +class InvalidTypeException extends InvalidRequestException { +} diff --git a/3rdparty/icewind/smb/src/Exception/NoLoginServerException.php b/3rdparty/icewind/smb/src/Exception/NoLoginServerException.php new file mode 100644 index 00000000..efe5b4e9 --- /dev/null +++ b/3rdparty/icewind/smb/src/Exception/NoLoginServerException.php @@ -0,0 +1,10 @@ + + * SPDX-License-Identifier: MIT + */ + +namespace Icewind\SMB\Exception; + +class NoLoginServerException extends ConnectException { +} diff --git a/3rdparty/icewind/smb/src/Exception/NoRouteToHostException.php b/3rdparty/icewind/smb/src/Exception/NoRouteToHostException.php new file mode 100644 index 00000000..475aaef6 --- /dev/null +++ b/3rdparty/icewind/smb/src/Exception/NoRouteToHostException.php @@ -0,0 +1,10 @@ + + * SPDX-License-Identifier: MIT + */ + +namespace Icewind\SMB\Exception; + +class NoRouteToHostException extends ConnectException { +} diff --git a/3rdparty/icewind/smb/src/Exception/NotEmptyException.php b/3rdparty/icewind/smb/src/Exception/NotEmptyException.php new file mode 100644 index 00000000..e76980b1 --- /dev/null +++ b/3rdparty/icewind/smb/src/Exception/NotEmptyException.php @@ -0,0 +1,10 @@ + + * SPDX-License-Identifier: MIT + */ + +namespace Icewind\SMB\Exception; + +class NotEmptyException extends InvalidRequestException { +} diff --git a/3rdparty/icewind/smb/src/Exception/NotFoundException.php b/3rdparty/icewind/smb/src/Exception/NotFoundException.php new file mode 100644 index 00000000..9cea30d8 --- /dev/null +++ b/3rdparty/icewind/smb/src/Exception/NotFoundException.php @@ -0,0 +1,10 @@ + + * SPDX-License-Identifier: MIT + */ + +namespace Icewind\SMB\Exception; + +class NotFoundException extends InvalidRequestException { +} diff --git a/3rdparty/icewind/smb/src/Exception/OutOfSpaceException.php b/3rdparty/icewind/smb/src/Exception/OutOfSpaceException.php new file mode 100644 index 00000000..1db6a720 --- /dev/null +++ b/3rdparty/icewind/smb/src/Exception/OutOfSpaceException.php @@ -0,0 +1,10 @@ + + * SPDX-License-Identifier: MIT + */ + +namespace Icewind\SMB\Exception; + +class OutOfSpaceException extends InvalidRequestException { +} diff --git a/3rdparty/icewind/smb/src/Exception/RevisionMismatchException.php b/3rdparty/icewind/smb/src/Exception/RevisionMismatchException.php new file mode 100644 index 00000000..a3954018 --- /dev/null +++ b/3rdparty/icewind/smb/src/Exception/RevisionMismatchException.php @@ -0,0 +1,15 @@ + + * SPDX-License-Identifier: MIT + */ + +namespace Icewind\SMB\Exception; + +use Throwable; + +class RevisionMismatchException extends Exception { + public function __construct(string $message = 'Protocol version mismatch', int $code = 0, Throwable $previous = null) { + parent::__construct($message, $code, $previous); + } +} diff --git a/3rdparty/icewind/smb/src/Exception/TimedOutException.php b/3rdparty/icewind/smb/src/Exception/TimedOutException.php new file mode 100644 index 00000000..57eeb0f0 --- /dev/null +++ b/3rdparty/icewind/smb/src/Exception/TimedOutException.php @@ -0,0 +1,10 @@ + + * SPDX-License-Identifier: MIT + */ + +namespace Icewind\SMB\Exception; + +class TimedOutException extends ConnectException { +} diff --git a/3rdparty/icewind/smb/src/IAuth.php b/3rdparty/icewind/smb/src/IAuth.php new file mode 100644 index 00000000..46ba4b81 --- /dev/null +++ b/3rdparty/icewind/smb/src/IAuth.php @@ -0,0 +1,29 @@ + + * SPDX-License-Identifier: AGPL-3.0-or-later + */ + +namespace Icewind\SMB; + +interface IAuth { + public function getUsername(): ?string; + + public function getWorkgroup(): ?string; + + public function getPassword(): ?string; + + /** + * Any extra command line option for smbclient that are required + * + * @return string + */ + public function getExtraCommandLineArguments(): string; + + /** + * Set any extra options for libsmbclient that are required + * + * @param resource $smbClientState + */ + public function setExtraSmbClientOptions($smbClientState): void; +} diff --git a/3rdparty/icewind/smb/src/IFileInfo.php b/3rdparty/icewind/smb/src/IFileInfo.php new file mode 100644 index 00000000..e71e0e40 --- /dev/null +++ b/3rdparty/icewind/smb/src/IFileInfo.php @@ -0,0 +1,45 @@ + + * SPDX-License-Identifier: MIT + */ + +namespace Icewind\SMB; + +interface IFileInfo { + /* + * Mappings of the DOS mode bits, as returned by smbc_getxattr() when the + * attribute name "system.dos_attr.mode" (or "system.dos_attr.*" or + * "system.*") is specified. + */ + const MODE_READONLY = 0x01; + const MODE_HIDDEN = 0x02; + const MODE_SYSTEM = 0x04; + const MODE_VOLUME_ID = 0x08; + const MODE_DIRECTORY = 0x10; + const MODE_ARCHIVE = 0x20; + const MODE_NORMAL = 0x80; + + public function getPath(): string; + + public function getName(): string; + + public function getSize(): int; + + public function getMTime(): int; + + public function isDirectory(): bool; + + public function isReadOnly(): bool; + + public function isHidden(): bool; + + public function isSystem(): bool; + + public function isArchived(): bool; + + /** + * @return ACL[] + */ + public function getAcls(): array; +} diff --git a/3rdparty/icewind/smb/src/INotifyHandler.php b/3rdparty/icewind/smb/src/INotifyHandler.php new file mode 100644 index 00000000..829ac7d9 --- /dev/null +++ b/3rdparty/icewind/smb/src/INotifyHandler.php @@ -0,0 +1,43 @@ + + * SPDX-License-Identifier: MIT + */ + +namespace Icewind\SMB; + +interface INotifyHandler { + // https://msdn.microsoft.com/en-us/library/dn392331.aspx + const NOTIFY_ADDED = 1; + const NOTIFY_REMOVED = 2; + const NOTIFY_MODIFIED = 3; + const NOTIFY_RENAMED_OLD = 4; + const NOTIFY_RENAMED_NEW = 5; + const NOTIFY_ADDED_STREAM = 6; + const NOTIFY_REMOVED_STREAM = 7; + const NOTIFY_MODIFIED_STREAM = 8; + const NOTIFY_REMOVED_BY_DELETE = 9; + + /** + * Get all changes detected since the start of the notify process or the last call to getChanges + * + * @return Change[] + */ + public function getChanges(): array; + + /** + * Listen actively to all incoming changes + * + * Note that this is a blocking process and will cause the process to block forever if not explicitly terminated + * + * @param callable(Change):?bool $callback + */ + public function listen(callable $callback): void; + + /** + * Stop listening for changes + * + * Note that any pending changes will be discarded + */ + public function stop(): void; +} diff --git a/3rdparty/icewind/smb/src/IOptions.php b/3rdparty/icewind/smb/src/IOptions.php new file mode 100644 index 00000000..b72700c9 --- /dev/null +++ b/3rdparty/icewind/smb/src/IOptions.php @@ -0,0 +1,26 @@ + + * SPDX-License-Identifier: AGPL-3.0-or-later + */ + +namespace Icewind\SMB; + +interface IOptions { + const PROTOCOL_NT1 = 'NT1'; + const PROTOCOL_SMB2 = 'SMB2'; + const PROTOCOL_SMB2_02 = 'SMB2_02'; + const PROTOCOL_SMB2_22 = 'SMB2_22'; + const PROTOCOL_SMB2_24 = 'SMB2_24'; + const PROTOCOL_SMB3 = 'SMB3'; + const PROTOCOL_SMB3_00 = 'SMB3_00'; + const PROTOCOL_SMB3_02 = 'SMB3_02'; + const PROTOCOL_SMB3_10 = 'SMB3_10'; + const PROTOCOL_SMB3_11 = 'SMB3_11'; + + public function getTimeout(): int; + + public function getMinProtocol(): ?string; + + public function getMaxProtocol(): ?string; +} diff --git a/3rdparty/icewind/smb/src/IServer.php b/3rdparty/icewind/smb/src/IServer.php new file mode 100644 index 00000000..c11fb450 --- /dev/null +++ b/3rdparty/icewind/smb/src/IServer.php @@ -0,0 +1,31 @@ + + * SPDX-License-Identifier: AGPL-3.0-or-later + */ + +namespace Icewind\SMB; + +interface IServer { + public function getAuth(): IAuth; + + public function getHost(): string; + + /** + * @return \Icewind\SMB\IShare[] + * + * @throws \Icewind\SMB\Exception\AuthenticationException + * @throws \Icewind\SMB\Exception\InvalidHostException + */ + public function listShares(): array; + + public function getShare(string $name): IShare; + + public function getTimeZone(): string; + + public function getSystem(): ISystem; + + public function getOptions(): IOptions; + + public static function available(ISystem $system): bool; +} diff --git a/3rdparty/icewind/smb/src/IShare.php b/3rdparty/icewind/smb/src/IShare.php new file mode 100644 index 00000000..617b8174 --- /dev/null +++ b/3rdparty/icewind/smb/src/IShare.php @@ -0,0 +1,164 @@ + + * SPDX-License-Identifier: MIT + */ + +namespace Icewind\SMB; + +use Icewind\SMB\Exception\AlreadyExistsException; +use Icewind\SMB\Exception\InvalidRequestException; +use Icewind\SMB\Exception\InvalidTypeException; +use Icewind\SMB\Exception\NotFoundException; + +interface IShare { + /** + * Get the name of the share + * + * @return string + */ + public function getName(): string; + + /** + * Download a remote file + * + * @param string $source remote file + * @param string $target local file + * @return bool + * + * @throws NotFoundException + * @throws InvalidTypeException + */ + public function get(string $source, string $target): bool; + + /** + * Upload a local file + * + * @param string $source local file + * @param string $target remote file + * @return bool + * + * @throws NotFoundException + * @throws InvalidTypeException + */ + public function put(string $source, string $target): bool; + + /** + * Open a readable stream to a remote file + * + * @param string $source + * @return resource a read only stream with the contents of the remote file + * + * @throws NotFoundException + * @throws InvalidTypeException + */ + public function read(string $source); + + /** + * Open a writable stream to a remote file + * Note: This method will truncate the file to 0bytes + * + * @param string $target + * @return resource a write only stream to upload a remote file + * + * @throws NotFoundException + * @throws InvalidTypeException + */ + public function write(string $target); + + /** + * Open a writable stream to a remote file and set the cursor to the end of the file + * + * @param string $target + * @return resource a write only stream to upload a remote file + * + * @throws NotFoundException + * @throws InvalidTypeException + * @throws InvalidRequestException + */ + public function append(string $target); + + /** + * Rename a remote file + * + * @param string $from + * @param string $to + * @return bool + * + * @throws NotFoundException + * @throws AlreadyExistsException + */ + public function rename(string $from, string $to): bool; + + /** + * Delete a file on the share + * + * @param string $path + * @return bool + * + * @throws NotFoundException + * @throws InvalidTypeException + */ + public function del(string $path): bool; + + /** + * List the content of a remote folder + * + * @param string $path + * @return IFileInfo[] + * + * @throws NotFoundException + * @throws InvalidTypeException + */ + public function dir(string $path): array; + + /** + * @param string $path + * @return IFileInfo + * + * @throws NotFoundException + */ + public function stat(string $path): IFileInfo; + + /** + * Create a folder on the share + * + * @param string $path + * @return bool + * + * @throws NotFoundException + * @throws AlreadyExistsException + */ + public function mkdir(string $path): bool; + + /** + * Remove a folder on the share + * + * @param string $path + * @return bool + * + * @throws NotFoundException + * @throws InvalidTypeException + */ + public function rmdir(string $path): bool; + + /** + * @param string $path + * @param int $mode a combination of FileInfo::MODE_READONLY, FileInfo::MODE_ARCHIVE, FileInfo::MODE_SYSTEM and FileInfo::MODE_HIDDEN, FileInfo::NORMAL + * @return mixed + */ + public function setMode(string $path, int $mode); + + /** + * @param string $path + * @return INotifyHandler + */ + public function notify(string $path); + + /** + * Get the IServer instance for this share + * + * @return IServer + */ + public function getServer(): IServer; +} diff --git a/3rdparty/icewind/smb/src/ISystem.php b/3rdparty/icewind/smb/src/ISystem.php new file mode 100644 index 00000000..90209f9a --- /dev/null +++ b/3rdparty/icewind/smb/src/ISystem.php @@ -0,0 +1,63 @@ + + * SPDX-License-Identifier: AGPL-3.0-or-later + */ + +namespace Icewind\SMB; + +/** + * The `ISystem` interface provides a way to access system dependent information + * such as the availability and location of certain binaries. + */ +interface ISystem { + /** + * Get the path to a file descriptor of the current process + * + * @param int $num the file descriptor id + * @return string + */ + public function getFD(int $num): string; + + /** + * Get the full path to the `smbclient` binary of null if the binary is not available + * + * @return string|null + */ + public function getSmbclientPath(): ?string; + + /** + * Get the full path to the `net` binary of null if the binary is not available + * + * @return string|null + */ + public function getNetPath(): ?string; + + /** + * Get the full path to the `smbcacls` binary of null if the binary is not available + * + * @return string|null + */ + public function getSmbcAclsPath(): ?string; + + /** + * Get the full path to the `stdbuf` binary of null if the binary is not available + * + * @return string|null + */ + public function getStdBufPath(): ?string; + + /** + * Get the full path to the `date` binary of null if the binary is not available + * + * @return string|null + */ + public function getDatePath(): ?string; + + /** + * Whether or not the smbclient php extension is enabled + * + * @return bool + */ + public function libSmbclientAvailable(): bool; +} diff --git a/3rdparty/icewind/smb/src/ITimeZoneProvider.php b/3rdparty/icewind/smb/src/ITimeZoneProvider.php new file mode 100644 index 00000000..dba3b581 --- /dev/null +++ b/3rdparty/icewind/smb/src/ITimeZoneProvider.php @@ -0,0 +1,17 @@ + + * SPDX-License-Identifier: AGPL-3.0-or-later + */ + +namespace Icewind\SMB; + +interface ITimeZoneProvider { + /** + * Get the timezone of the smb server + * + * @param string $host + * @return string + */ + public function get(string $host): string; +} diff --git a/3rdparty/icewind/smb/src/KerberosApacheAuth.php b/3rdparty/icewind/smb/src/KerberosApacheAuth.php new file mode 100644 index 00000000..eb22982f --- /dev/null +++ b/3rdparty/icewind/smb/src/KerberosApacheAuth.php @@ -0,0 +1,47 @@ + + * SPDX-License-Identifier: AGPL-3.0-or-later + */ + +namespace Icewind\SMB; + +use Icewind\SMB\Exception\DependencyException; +use Icewind\SMB\Exception\Exception; +use Icewind\SMB\Exception\InvalidTicket; + +/** + * Use existing kerberos ticket to authenticate and reuse the apache ticket cache (mod_auth_kerb) + * + * @deprecated Use `KerberosAuth` with `$auth->setTicket(KerberosTicket::fromEnv())` instead + */ +class KerberosApacheAuth extends KerberosAuth implements IAuth { + public function getTicket(): KerberosTicket { + if ($this->ticket === null) { + $ticket = KerberosTicket::fromEnv(); + if ($ticket === null) { + throw new InvalidTicket("No ticket found in environment"); + } + $this->ticket = $ticket; + } + return $this->ticket; + } + + /** + * Copy the ticket to a temporary location and use that ticket for authentication + * + * @return void + */ + public function copyTicket(): void { + $this->ticket = KerberosTicket::load($this->getTicket()->save()); + } + + /** + * Check if a valid kerberos ticket is present + * + * @return bool + */ + public function checkTicket(): bool { + return $this->getTicket()->isValid(); + } +} diff --git a/3rdparty/icewind/smb/src/KerberosAuth.php b/3rdparty/icewind/smb/src/KerberosAuth.php new file mode 100644 index 00000000..6e35f9bd --- /dev/null +++ b/3rdparty/icewind/smb/src/KerberosAuth.php @@ -0,0 +1,64 @@ + + * SPDX-License-Identifier: AGPL-3.0-or-later + */ + +namespace Icewind\SMB; + +use Icewind\SMB\Exception\Exception; + +/** + * Use existing kerberos ticket to authenticate + */ +class KerberosAuth implements IAuth { + /** @var ?KerberosTicket */ + protected $ticket = null; + + public function getTicket(): ?KerberosTicket { + return $this->ticket; + } + + public function setTicket(?KerberosTicket $ticket): void { + $this->ticket = $ticket; + } + + public function getUsername(): ?string { + return 'dummy'; + } + + public function getWorkgroup(): ?string { + return 'dummy'; + } + + public function getPassword(): ?string { + return null; + } + + private function setEnv():void { + $ticket = $this->getTicket(); + if ($ticket) { + $ticket->validate(); + + // note that even if the ticket name is the value we got from `getenv("KRB5CCNAME")` we still need to set the env variable ourselves + // this is because `getenv` also reads the variables passed from the SAPI (apache-php) and we need to set the variable in the OS's env + putenv("KRB5CCNAME=" . $ticket->getCacheName()); + } + } + + public function getExtraCommandLineArguments(): string { + $this->setEnv(); + return '-k'; + } + + public function setExtraSmbClientOptions($smbClientState): void { + $this->setEnv(); + + $success = (bool)smbclient_option_set($smbClientState, SMBCLIENT_OPT_USE_KERBEROS, true); + $success = $success && smbclient_option_set($smbClientState, SMBCLIENT_OPT_FALLBACK_AFTER_KERBEROS, false); + + if (!$success) { + throw new Exception("Failed to set smbclient options for kerberos auth"); + } + } +} diff --git a/3rdparty/icewind/smb/src/KerberosTicket.php b/3rdparty/icewind/smb/src/KerberosTicket.php new file mode 100644 index 00000000..c019b181 --- /dev/null +++ b/3rdparty/icewind/smb/src/KerberosTicket.php @@ -0,0 +1,85 @@ + + * SPDX-License-Identifier: AGPL-3.0-or-later + */ + +namespace Icewind\SMB; + +use Icewind\SMB\Exception\InvalidTicket; +use KRB5CCache; + +class KerberosTicket { + /** @var KRB5CCache */ + private $krb5; + /** @var string */ + private $cacheName; + + public function __construct(KRB5CCache $krb5, string $cacheName) { + $this->krb5 = $krb5; + $this->cacheName = $cacheName; + } + + public function getCacheName(): string { + return $this->cacheName; + } + + public function getName(): string{ + return $this->krb5->getName(); + } + + public function isValid(): bool { + return count($this->krb5->getEntries()) > 0; + } + + public function validate(): void { + if (!$this->isValid()) { + throw new InvalidTicket("No kerberos ticket found."); + } + } + + /** + * Load the ticket from the cache specified by the KRB5CCNAME variable. + * + * @return KerberosTicket|null + */ + public static function fromEnv(): ?KerberosTicket { + $ticketName = getenv("KRB5CCNAME"); + if (!$ticketName) { + return null; + } + $krb5 = new KRB5CCache(); + $krb5->open($ticketName); + return new KerberosTicket($krb5, $ticketName); + } + + public static function load(string $ticket): KerberosTicket { + $tmpFilename = tempnam(sys_get_temp_dir(), "krb5cc_php_"); + file_put_contents($tmpFilename, $ticket); + register_shutdown_function(function () use ($tmpFilename) { + if (file_exists($tmpFilename)) { + unlink($tmpFilename); + } + }); + + $ticketName = "FILE:" . $tmpFilename; + $krb5 = new KRB5CCache(); + $krb5->open($ticketName); + return new KerberosTicket($krb5, $ticketName); + } + + public function save(): string { + if (substr($this->cacheName, 0, 5) === 'FILE:') { + $ticket = file_get_contents(substr($this->cacheName, 5)); + } else { + $tmpFilename = tempnam(sys_get_temp_dir(), "krb5cc_php_"); + $tmpCacheFile = "FILE:" . $tmpFilename; + $this->krb5->save($tmpCacheFile); + $ticket = file_get_contents($tmpFilename); + unlink($tmpFilename); + } + return $ticket; + } +} \ No newline at end of file diff --git a/3rdparty/icewind/smb/src/Native/NativeFileInfo.php b/3rdparty/icewind/smb/src/Native/NativeFileInfo.php new file mode 100644 index 00000000..48804afb --- /dev/null +++ b/3rdparty/icewind/smb/src/Native/NativeFileInfo.php @@ -0,0 +1,142 @@ + + * SPDX-License-Identifier: MIT + */ + +namespace Icewind\SMB\Native; + +use Icewind\SMB\ACL; +use Icewind\SMB\Exception\Exception; +use Icewind\SMB\Exception\NotFoundException; +use Icewind\SMB\IFileInfo; + +class NativeFileInfo implements IFileInfo { + /** @var string */ + protected $path; + /** @var string */ + protected $name; + /** @var NativeShare */ + protected $share; + /** @var array{"mode": int, "size": int, "mtime": int}|null */ + protected $statCache = null; + + public function __construct(NativeShare $share, string $path, string $name) { + $this->share = $share; + $this->path = $path; + $this->name = $name; + } + + public function getPath(): string { + return $this->path; + } + + public function getName(): string { + return $this->name; + } + + /** + * @return array{"mode": int, "size": int, "mtime": int} + */ + protected function stat(): array { + if (is_null($this->statCache)) { + $this->statCache = $this->share->rawStat($this->path); + } + return $this->statCache; + } + + public function getSize(): int { + $stat = $this->stat(); + return $stat['size']; + } + + public function getMTime(): int { + $stat = $this->stat(); + return $stat['mtime']; + } + + /** + * On "mode": + * + * different smbclient versions seem to return different mode values for 'system.dos_attr.mode' + * + * older versions return the dos permissions mask as defined in `IFileInfo::MODE_*` while + * newer versions return the equivalent unix permission mask. + * + * Since the unix mask doesn't contain the proper hidden/archive/system flags we have to assume them + * as false (except for `hidden` where we use the unix dotfile convention) + */ + + protected function getMode(): int { + $mode = $this->stat()['mode']; + + // Let us ignore the ATTR_NOT_CONTENT_INDEXED for now + $mode &= ~0x00002000; + + return $mode; + } + + public function isDirectory(): bool { + $mode = $this->getMode(); + if ($mode > 0x1000) { + return ($mode & 0x4000 && !($mode & 0x8000)); // 0x4000: unix directory flag shares bits with 0xC000: socket + } else { + return (bool)($mode & IFileInfo::MODE_DIRECTORY); + } + } + + public function isReadOnly(): bool { + $mode = $this->getMode(); + if ($mode > 0x1000) { + return !(bool)($mode & 0x80); // 0x80: owner write permissions + } else { + return (bool)($mode & IFileInfo::MODE_READONLY); + } + } + + public function isHidden(): bool { + $mode = $this->getMode(); + if ($mode > 0x1000) { + return strlen($this->name) > 0 && $this->name[0] === '.'; + } else { + return (bool)($mode & IFileInfo::MODE_HIDDEN); + } + } + + public function isSystem(): bool { + $mode = $this->getMode(); + if ($mode > 0x1000) { + return false; + } else { + return (bool)($mode & IFileInfo::MODE_SYSTEM); + } + } + + public function isArchived(): bool { + $mode = $this->getMode(); + if ($mode > 0x1000) { + return false; + } else { + return (bool)($mode & IFileInfo::MODE_ARCHIVE); + } + } + + /** + * @return ACL[] + */ + public function getAcls(): array { + $acls = []; + $attribute = $this->share->getAttribute($this->path, 'system.nt_sec_desc.acl.*+'); + + foreach (explode(',', $attribute) as $acl) { + list($user, $permissions) = explode(':', $acl, 2); + $user = trim($user, '\\'); + list($type, $flags, $mask) = explode('/', $permissions); + $mask = hexdec($mask); + + $acls[$user] = new ACL((int)$type, (int)$flags, (int)$mask); + } + + return $acls; + } +} diff --git a/3rdparty/icewind/smb/src/Native/NativeReadStream.php b/3rdparty/icewind/smb/src/Native/NativeReadStream.php new file mode 100644 index 00000000..af1aa496 --- /dev/null +++ b/3rdparty/icewind/smb/src/Native/NativeReadStream.php @@ -0,0 +1,92 @@ + + * SPDX-License-Identifier: MIT + */ + +namespace Icewind\SMB\Native; + +use Icewind\SMB\StringBuffer; + +/** + * Stream optimized for read only usage + */ +class NativeReadStream extends NativeStream { + const CHUNK_SIZE = 1048576; // 1MB chunks + + /** @var StringBuffer */ + private $readBuffer; + + public function __construct() { + $this->readBuffer = new StringBuffer(); + } + + /** @var int */ + private $pos = 0; + + public function stream_open($path, $mode, $options, &$opened_path) { + return parent::stream_open($path, $mode, $options, $opened_path); + } + + /** + * Wrap a stream from libsmbclient-php into a regular php stream + * + * @param NativeState $state + * @param resource $smbStream + * @param string $mode + * @param string $url + * @return resource + */ + public static function wrap(NativeState $state, $smbStream, string $mode, string $url) { + return parent::wrapClass($state, $smbStream, $mode, $url, NativeReadStream::class); + } + + public function stream_read($count) { + // php reads 8192 bytes at once + // however due to network latency etc, it's faster to read in larger chunks + // and buffer the result + if (!parent::stream_eof() && $this->readBuffer->remaining() < $count) { + $chunk = parent::stream_read(self::CHUNK_SIZE); + if ($chunk === false) { + return false; + } + $this->readBuffer->push($chunk); + } + + $result = $this->readBuffer->read($count); + + $read = strlen($result); + $this->pos += $read; + + return $result; + } + + public function stream_seek($offset, $whence = SEEK_SET) { + $result = parent::stream_seek($offset, $whence); + if ($result) { + $this->readBuffer->clear(); + $pos = parent::stream_tell(); + if ($pos === false) { + return false; + } + $this->pos = $pos; + } + return $result; + } + + public function stream_eof() { + return $this->readBuffer->remaining() <= 0 && parent::stream_eof(); + } + + public function stream_tell() { + return $this->pos; + } + + public function stream_write($data) { + return false; + } + + public function stream_truncate($size) { + return false; + } +} diff --git a/3rdparty/icewind/smb/src/Native/NativeServer.php b/3rdparty/icewind/smb/src/Native/NativeServer.php new file mode 100644 index 00000000..2a9153ad --- /dev/null +++ b/3rdparty/icewind/smb/src/Native/NativeServer.php @@ -0,0 +1,64 @@ + + * SPDX-License-Identifier: MIT + */ + +namespace Icewind\SMB\Native; + +use Icewind\SMB\AbstractServer; +use Icewind\SMB\Exception\AuthenticationException; +use Icewind\SMB\Exception\InvalidHostException; +use Icewind\SMB\IAuth; +use Icewind\SMB\IOptions; +use Icewind\SMB\IShare; +use Icewind\SMB\ISystem; +use Icewind\SMB\ITimeZoneProvider; + +class NativeServer extends AbstractServer { + /** + * @var NativeState + */ + protected $state; + + public function __construct(string $host, IAuth $auth, ISystem $system, ITimeZoneProvider $timeZoneProvider, IOptions $options) { + parent::__construct($host, $auth, $system, $timeZoneProvider, $options); + $this->state = new NativeState(); + } + + protected function connect(): void { + $this->state->init($this->getAuth(), $this->getOptions()); + } + + /** + * @return IShare[] + * @throws AuthenticationException + * @throws InvalidHostException + */ + public function listShares(): array { + $this->connect(); + $shares = []; + $dh = $this->state->opendir('smb://' . $this->getHost()); + while ($share = $this->state->readdir($dh, '')) { + if ($share['type'] === 'file share') { + $shares[] = $this->getShare($share['name']); + } + } + $this->state->closedir($dh, ''); + return $shares; + } + + public function getShare(string $name): IShare { + return new NativeShare($this, $name); + } + + /** + * Check if the smbclient php extension is available + * + * @param ISystem $system + * @return bool + */ + public static function available(ISystem $system): bool { + return $system->libSmbclientAvailable(); + } +} diff --git a/3rdparty/icewind/smb/src/Native/NativeShare.php b/3rdparty/icewind/smb/src/Native/NativeShare.php new file mode 100644 index 00000000..0c7e3471 --- /dev/null +++ b/3rdparty/icewind/smb/src/Native/NativeShare.php @@ -0,0 +1,369 @@ + + * SPDX-License-Identifier: MIT + */ + +namespace Icewind\SMB\Native; + +use Icewind\SMB\AbstractShare; +use Icewind\SMB\Exception\AlreadyExistsException; +use Icewind\SMB\Exception\AuthenticationException; +use Icewind\SMB\Exception\ConnectionException; +use Icewind\SMB\Exception\DependencyException; +use Icewind\SMB\Exception\InvalidHostException; +use Icewind\SMB\Exception\InvalidPathException; +use Icewind\SMB\Exception\InvalidResourceException; +use Icewind\SMB\Exception\InvalidTypeException; +use Icewind\SMB\Exception\NotFoundException; +use Icewind\SMB\IFileInfo; +use Icewind\SMB\INotifyHandler; +use Icewind\SMB\IServer; +use Icewind\SMB\Wrapped\Server; +use Icewind\SMB\Wrapped\Share; + +class NativeShare extends AbstractShare { + /** + * @var IServer $server + */ + private $server; + + /** + * @var string $name + */ + private $name; + + /** @var NativeState|null $state */ + private $state = null; + + public function __construct(IServer $server, string $name) { + parent::__construct(); + $this->server = $server; + $this->name = $name; + } + + /** + * @throws ConnectionException + * @throws AuthenticationException + * @throws InvalidHostException + */ + protected function getState(): NativeState { + if ($this->state) { + return $this->state; + } + + $this->state = new NativeState(); + $this->state->init($this->server->getAuth(), $this->server->getOptions()); + return $this->state; + } + + /** + * Get the name of the share + * + * @return string + */ + public function getName(): string { + return $this->name; + } + + private function buildUrl(string $path): string { + $this->verifyPath($path); + $url = sprintf('smb://%s/%s', $this->server->getHost(), $this->name); + if ($path) { + $path = trim($path, '/'); + $url .= '/'; + $url .= implode('/', array_map('rawurlencode', explode('/', $path))); + } + return $url; + } + + /** + * List the content of a remote folder + * + * @param string $path + * @return IFileInfo[] + * + * @throws NotFoundException + * @throws InvalidTypeException + */ + public function dir(string $path): array { + $files = []; + + $dh = $this->getState()->opendir($this->buildUrl($path)); + while ($file = $this->getState()->readdir($dh, $path)) { + $name = $file['name']; + if ($name !== '.' and $name !== '..') { + $fullPath = $path . '/' . $name; + $files [] = new NativeFileInfo($this, $fullPath, $name); + } + } + + $this->getState()->closedir($dh, $path); + return $files; + } + + /** + * @param string $path + * @return IFileInfo + */ + public function stat(string $path): IFileInfo { + $info = new NativeFileInfo($this, $path, self::mb_basename($path)); + + // trigger attribute loading + $info->getSize(); + + return $info; + } + + /** + * @return array{"mode": int, "size": int, "mtime": int} + */ + public function rawStat(string $path): array { + return $this->getState()->stat($this->buildUrl($path)); + } + + /** + * Multibyte unicode safe version of basename() + * + * @param string $path + * @link http://php.net/manual/en/function.basename.php#121405 + * @return string + */ + protected static function mb_basename(string $path): string { + if (preg_match('@^.*[\\\\/]([^\\\\/]+)$@s', $path, $matches)) { + return $matches[1]; + } elseif (preg_match('@^([^\\\\/]+)$@s', $path, $matches)) { + return $matches[1]; + } + + return ''; + } + + /** + * Create a folder on the share + * + * @param string $path + * @return bool + * + * @throws NotFoundException + * @throws AlreadyExistsException + */ + public function mkdir(string $path): bool { + return $this->getState()->mkdir($this->buildUrl($path)); + } + + /** + * Remove a folder on the share + * + * @param string $path + * @return bool + * + * @throws NotFoundException + * @throws InvalidTypeException + */ + public function rmdir(string $path): bool { + return $this->getState()->rmdir($this->buildUrl($path)); + } + + /** + * Delete a file on the share + * + * @param string $path + * @return bool + * + * @throws NotFoundException + * @throws InvalidTypeException + */ + public function del(string $path): bool { + return $this->getState()->unlink($this->buildUrl($path)); + } + + /** + * Rename a remote file + * + * @param string $from + * @param string $to + * @return bool + * + * @throws NotFoundException + * @throws AlreadyExistsException + */ + public function rename(string $from, string $to): bool { + return $this->getState()->rename($this->buildUrl($from), $this->buildUrl($to)); + } + + /** + * Upload a local file + * + * @param string $source local file + * @param string $target remove file + * @return bool + * + * @throws NotFoundException + * @throws InvalidTypeException + */ + public function put(string $source, string $target): bool { + $sourceHandle = fopen($source, 'rb'); + $targetUrl = $this->buildUrl($target); + + $targetHandle = $this->getState()->create($targetUrl); + + while ($data = fread($sourceHandle, NativeReadStream::CHUNK_SIZE)) { + $this->getState()->write($targetHandle, $data, $targetUrl); + } + $this->getState()->close($targetHandle, $targetUrl); + return true; + } + + /** + * Download a remote file + * + * @param string $source remove file + * @param string $target local file + * @return bool + * + * @throws AuthenticationException + * @throws ConnectionException + * @throws InvalidHostException + * @throws InvalidPathException + * @throws InvalidResourceException + */ + public function get(string $source, string $target): bool { + if (!$target) { + throw new InvalidPathException('Invalid target path: Filename cannot be empty'); + } + + $sourceHandle = $this->getState()->open($this->buildUrl($source), 'r'); + + $targetHandle = @fopen($target, 'wb'); + if (!$targetHandle) { + $error = error_get_last(); + if (is_array($error)) { + $reason = $error['message']; + } else { + $reason = 'Unknown error'; + } + $this->getState()->close($sourceHandle, $this->buildUrl($source)); + throw new InvalidResourceException('Failed opening local file "' . $target . '" for writing: ' . $reason); + } + + while ($data = $this->getState()->read($sourceHandle, NativeReadStream::CHUNK_SIZE, $source)) { + fwrite($targetHandle, $data); + } + $this->getState()->close($sourceHandle, $this->buildUrl($source)); + return true; + } + + /** + * Open a readable stream to a remote file + * + * @param string $source + * @return resource a read only stream with the contents of the remote file + * + * @throws NotFoundException + * @throws InvalidTypeException + */ + public function read(string $source) { + $url = $this->buildUrl($source); + $handle = $this->getState()->open($url, 'r'); + return NativeReadStream::wrap($this->getState(), $handle, 'r', $url); + } + + /** + * Open a writeable stream to a remote file + * Note: This method will truncate the file to 0bytes first + * + * @param string $target + * @return resource a writeable stream + * + * @throws NotFoundException + * @throws InvalidTypeException + */ + public function write(string $target) { + $url = $this->buildUrl($target); + $handle = $this->getState()->create($url); + return NativeWriteStream::wrap($this->getState(), $handle, 'w', $url); + } + + /** + * Open a writeable stream and set the cursor to the end of the stream + * + * @param string $target + * @return resource a writeable stream + * + * @throws NotFoundException + * @throws InvalidTypeException + */ + public function append(string $target) { + $url = $this->buildUrl($target); + $handle = $this->getState()->open($url, "a+"); + return NativeWriteStream::wrap($this->getState(), $handle, "a", $url); + } + + /** + * Get extended attributes for the path + * + * @param string $path + * @param string $attribute attribute to get the info + * @return string the attribute value + */ + public function getAttribute(string $path, string $attribute): string { + return $this->getState()->getxattr($this->buildUrl($path), $attribute); + } + + /** + * Set extended attributes for the given path + * + * @param string $path + * @param string $attribute attribute to get the info + * @param string|int $value + * @return mixed the attribute value + */ + public function setAttribute(string $path, string $attribute, $value) { + if (is_int($value)) { + if ($attribute === 'system.dos_attr.mode') { + $value = '0x' . dechex($value); + } else { + throw new \InvalidArgumentException("Invalid value for attribute"); + } + } + + return $this->getState()->setxattr($this->buildUrl($path), $attribute, $value); + } + + /** + * Set DOS comaptible node mode + * + * @param string $path + * @param int $mode a combination of FileInfo::MODE_READONLY, FileInfo::MODE_ARCHIVE, FileInfo::MODE_SYSTEM and FileInfo::MODE_HIDDEN, FileInfo::NORMAL + * @return mixed + */ + public function setMode(string $path, int $mode) { + return $this->setAttribute($path, 'system.dos_attr.mode', $mode); + } + + /** + * Start smb notify listener + * Note: This is a blocking call + * + * @param string $path + * @return INotifyHandler + */ + public function notify(string $path): INotifyHandler { + // php-smbclient does not support notify (https://github.com/eduardok/libsmbclient-php/issues/29) + // so we use the smbclient based backend for this + if (!Server::available($this->server->getSystem())) { + throw new DependencyException('smbclient not found in path for notify command'); + } + $share = new Share($this->server, $this->getName(), $this->server->getSystem()); + return $share->notify($path); + } + + public function getServer(): IServer { + return $this->server; + } + + public function __destruct() { + unset($this->state); + } +} diff --git a/3rdparty/icewind/smb/src/Native/NativeState.php b/3rdparty/icewind/smb/src/Native/NativeState.php new file mode 100644 index 00000000..99cef052 --- /dev/null +++ b/3rdparty/icewind/smb/src/Native/NativeState.php @@ -0,0 +1,433 @@ + + * SPDX-License-Identifier: MIT + */ + +namespace Icewind\SMB\Native; + +use Icewind\SMB\Exception\AlreadyExistsException; +use Icewind\SMB\Exception\ConnectionException; +use Icewind\SMB\Exception\ConnectionRefusedException; +use Icewind\SMB\Exception\ConnectionResetException; +use Icewind\SMB\Exception\Exception; +use Icewind\SMB\Exception\FileInUseException; +use Icewind\SMB\Exception\ForbiddenException; +use Icewind\SMB\Exception\HostDownException; +use Icewind\SMB\Exception\InvalidArgumentException; +use Icewind\SMB\Exception\InvalidTypeException; +use Icewind\SMB\Exception\ConnectionAbortedException; +use Icewind\SMB\Exception\NoRouteToHostException; +use Icewind\SMB\Exception\NotEmptyException; +use Icewind\SMB\Exception\NotFoundException; +use Icewind\SMB\Exception\OutOfSpaceException; +use Icewind\SMB\Exception\TimedOutException; +use Icewind\SMB\IAuth; +use Icewind\SMB\IOptions; + +/** + * Low level wrapper for libsmbclient-php with error handling + */ +class NativeState { + /** @var resource|null */ + protected $state = null; + + /** @var bool */ + protected $connected = false; + + /** + * sync the garbage collection cycle + * __deconstruct() of KerberosAuth should not called too soon + * + * @var IAuth|null $auth + */ + protected $auth = null; + + // see error.h + const EXCEPTION_MAP = [ + 1 => ForbiddenException::class, + 2 => NotFoundException::class, + 13 => ForbiddenException::class, + 16 => FileInUseException::class, + 17 => AlreadyExistsException::class, + 20 => InvalidTypeException::class, + 21 => InvalidTypeException::class, + 22 => InvalidArgumentException::class, + 28 => OutOfSpaceException::class, + 39 => NotEmptyException::class, + 103 => ConnectionAbortedException::class, + 104 => ConnectionResetException::class, + 110 => TimedOutException::class, + 111 => ConnectionRefusedException::class, + 112 => HostDownException::class, + 113 => NoRouteToHostException::class + ]; + + protected function handleError(?string $path): void { + if (!$this->state) { + return; + } + $error = smbclient_state_errno($this->state); + if ($error === 0) { + return; + } + throw Exception::fromMap(self::EXCEPTION_MAP, $error, $path); + } + + /** + * @param mixed $result + * @param string|null $uri + * @throws Exception + */ + protected function testResult($result, ?string $uri): void { + if ($result === false or $result === null) { + // smb://host/share/path + if (is_string($uri) && count(explode('/', $uri, 5)) > 4) { + list(, , , , $path) = explode('/', $uri, 5); + $path = '/' . $path; + } else { + $path = $uri; + } + $this->handleError($path); + } + } + + /** + * @param IAuth $auth + * @param IOptions $options + * @return bool + */ + public function init(IAuth $auth, IOptions $options) { + if ($this->connected) { + return true; + } + /** @var resource $state */ + $state = smbclient_state_new(); + $this->state = $state; + /** @psalm-suppress UnusedFunctionCall */ + smbclient_option_set($this->state, SMBCLIENT_OPT_AUTO_ANONYMOUS_LOGIN, false); + /** @psalm-suppress UnusedFunctionCall */ + smbclient_option_set($this->state, SMBCLIENT_OPT_TIMEOUT, $options->getTimeout() * 1000); + + if (function_exists('smbclient_client_protocols')) { + smbclient_client_protocols($this->state, $options->getMinProtocol(), $options->getMaxProtocol()); + } + + $auth->setExtraSmbClientOptions($this->state); + + // sync the garbage collection cycle + // __deconstruct() of KerberosAuth should not caled too soon + $this->auth = $auth; + + $result = @smbclient_state_init($this->state, $auth->getWorkgroup(), $auth->getUsername(), $auth->getPassword()); + + $this->testResult($result, ''); + $this->connected = true; + return $result; + } + + /** + * @param string $uri + * @return resource + */ + public function opendir(string $uri) { + if (!$this->state) { + throw new ConnectionException("Not connected"); + } + /** @var resource $result */ + $result = @smbclient_opendir($this->state, $uri); + + $this->testResult($result, $uri); + return $result; + } + + /** + * @param resource $dir + * @param string $path + * @return array{"type": string, "comment": string, "name": string}|false + */ + public function readdir($dir, string $path) { + if (!$this->state) { + throw new ConnectionException("Not connected"); + } + /** @var array{"type": string, "comment": string, "name": string}|false $result */ + $result = @smbclient_readdir($this->state, $dir); + + $this->testResult($result, $path); + return $result; + } + + /** + * @param resource $dir + * @param string $path + * @return bool + */ + public function closedir($dir, string $path): bool { + if (!$this->state) { + throw new ConnectionException("Not connected"); + } + $result = @smbclient_closedir($this->state, $dir); + + $this->testResult($result, $path); + return $result; + } + + /** + * @param string $old + * @param string $new + * @return bool + */ + public function rename(string $old, string $new): bool { + if (!$this->state) { + throw new ConnectionException("Not connected"); + } + $result = @smbclient_rename($this->state, $old, $this->state, $new); + + $this->testResult($result, $new); + return $result; + } + + /** + * @param string $uri + * @return bool + */ + public function unlink(string $uri): bool { + if (!$this->state) { + throw new ConnectionException("Not connected"); + } + $result = @smbclient_unlink($this->state, $uri); + + $this->testResult($result, $uri); + return $result; + } + + /** + * @param string $uri + * @param int $mask + * @return bool + */ + public function mkdir(string $uri, int $mask = 0777): bool { + if (!$this->state) { + throw new ConnectionException("Not connected"); + } + $result = @smbclient_mkdir($this->state, $uri, $mask); + + $this->testResult($result, $uri); + return $result; + } + + /** + * @param string $uri + * @return bool + */ + public function rmdir(string $uri): bool { + if (!$this->state) { + throw new ConnectionException("Not connected"); + } + $result = @smbclient_rmdir($this->state, $uri); + + $this->testResult($result, $uri); + return $result; + } + + /** + * @param string $uri + * @return array{"mtime": int, "size": int, "mode": int} + */ + public function stat(string $uri): array { + if (!$this->state) { + throw new ConnectionException("Not connected"); + } + /** @var array{"mtime": int, "size": int, "mode": int} $result */ + $result = @smbclient_stat($this->state, $uri); + + $this->testResult($result, $uri); + return $result; + } + + /** + * @param resource $file + * @param string $path + * @return array{"mtime": int, "size": int, "mode": int} + */ + public function fstat($file, string $path): array { + if (!$this->state) { + throw new ConnectionException("Not connected"); + } + /** @var array{"mtime": int, "size": int, "mode": int} $result */ + $result = @smbclient_fstat($this->state, $file); + + $this->testResult($result, $path); + return $result; + } + + /** + * @param string $uri + * @param string $mode + * @param int $mask + * @return resource + */ + public function open(string $uri, string $mode, int $mask = 0666) { + if (!$this->state) { + throw new ConnectionException("Not connected"); + } + /** @var resource $result */ + $result = @smbclient_open($this->state, $uri, $mode, $mask); + + $this->testResult($result, $uri); + return $result; + } + + /** + * @param string $uri + * @param int $mask + * @return resource + */ + public function create(string $uri, int $mask = 0666) { + if (!$this->state) { + throw new ConnectionException("Not connected"); + } + /** @var resource $result */ + $result = @smbclient_creat($this->state, $uri, $mask); + + $this->testResult($result, $uri); + return $result; + } + + /** + * @param resource $file + * @param int $bytes + * @param string $path + * @return string + */ + public function read($file, int $bytes, string $path): string { + if (!$this->state) { + throw new ConnectionException("Not connected"); + } + /** @var string $result */ + $result = @smbclient_read($this->state, $file, $bytes); + + $this->testResult($result, $path); + return $result; + } + + /** + * @param resource $file + * @param string $data + * @param string $path + * @param int|null $length + * @return int + */ + public function write($file, string $data, string $path, ?int $length = null): int { + if (!$this->state) { + throw new ConnectionException("Not connected"); + } + if ($length) { + $result = @smbclient_write($this->state, $file, $data, $length); + } else { + $result = @smbclient_write($this->state, $file, $data); + } + + $this->testResult($result, $path); + if ($result === false) { + return 0; + } + return $result; + } + + /** + * @param resource $file + * @param int $offset + * @param int $whence SEEK_SET | SEEK_CUR | SEEK_END + * @param string|null $path + * + * @return false|int new file offset as measured from the start of the file on success. + */ + public function lseek($file, int $offset, int $whence = SEEK_SET, string $path = null) { + if (!$this->state) { + throw new ConnectionException("Not connected"); + } + // psalm doesn't think int|false == int|false for some reason, so we do a needless annotation to help it out + /** + * @psalm-suppress UnnecessaryVarAnnotation + * @var int|false $result + */ + $result = @smbclient_lseek($this->state, $file, $offset, $whence); + + $this->testResult($result, $path); + return $result; + } + + /** + * @param resource $file + * @param int $size + * @param string $path + * @return bool + */ + public function ftruncate($file, int $size, string $path): bool { + if (!$this->state) { + throw new ConnectionException("Not connected"); + } + $result = @smbclient_ftruncate($this->state, $file, $size); + + $this->testResult($result, $path); + return $result; + } + + /** + * @param resource $file + * @param string $path + * @return bool + */ + public function close($file, string $path): bool { + if (!$this->state) { + return false; + } + $result = @smbclient_close($this->state, $file); + + $this->testResult($result, $path); + return $result; + } + + /** + * @param string $uri + * @param string $key + * @return string + */ + public function getxattr(string $uri, string $key) { + if (!$this->state) { + throw new ConnectionException("Not connected"); + } + /** @var string $result */ + $result = @smbclient_getxattr($this->state, $uri, $key); + + $this->testResult($result, $uri); + return $result; + } + + /** + * @param string $uri + * @param string $key + * @param string $value + * @param int $flags + * @return bool + */ + public function setxattr(string $uri, string $key, string $value, int $flags = 0) { + if (!$this->state) { + throw new ConnectionException("Not connected"); + } + /** @var bool $result */ + $result = @smbclient_setxattr($this->state, $uri, $key, $value, $flags); + + $this->testResult($result, $uri); + return $result; + } + + public function __destruct() { + if ($this->connected && $this->state) { + if (smbclient_state_free($this->state) === false) { + throw new Exception("Failed to free smb state"); + } + } + } +} diff --git a/3rdparty/icewind/smb/src/Native/NativeStream.php b/3rdparty/icewind/smb/src/Native/NativeStream.php new file mode 100644 index 00000000..69166609 --- /dev/null +++ b/3rdparty/icewind/smb/src/Native/NativeStream.php @@ -0,0 +1,158 @@ + + * SPDX-License-Identifier: MIT + */ + +namespace Icewind\SMB\Native; + +use Icewind\SMB\Exception\Exception; +use Icewind\SMB\Exception\InvalidRequestException; +use Icewind\Streams\File; +use InvalidArgumentException; + +abstract class NativeStream implements File { + /** + * @var resource + * @psalm-suppress PropertyNotSetInConstructor + */ + public $context; + + /** + * @var NativeState + * @psalm-suppress PropertyNotSetInConstructor + */ + protected $state; + + /** + * @var resource + * @psalm-suppress PropertyNotSetInConstructor + */ + protected $handle; + + /** + * @var bool + */ + protected $eof = false; + + /** + * @var string + */ + protected $url = ''; + + /** + * Wrap a stream from libsmbclient-php into a regular php stream + * + * @param NativeState $state + * @param resource $smbStream + * @param string $mode + * @param string $url + * @param class-string $class + * @return resource + */ + protected static function wrapClass(NativeState $state, $smbStream, string $mode, string $url, string $class) { + if (stream_wrapper_register('nativesmb', $class) === false) { + throw new Exception("Failed to register stream wrapper"); + } + $context = stream_context_create([ + 'nativesmb' => [ + 'state' => $state, + 'handle' => $smbStream, + 'url' => $url + ] + ]); + $fh = fopen('nativesmb://', $mode, false, $context); + if (stream_wrapper_unregister('nativesmb') === false) { + throw new Exception("Failed to unregister stream wrapper"); + } + return $fh; + } + + public function stream_close() { + try { + return $this->state->close($this->handle, $this->url); + } catch (\Exception $e) { + return false; + } + } + + public function stream_eof() { + return $this->eof; + } + + public function stream_flush() { + return false; + } + + + public function stream_open($path, $mode, $options, &$opened_path) { + $context = stream_context_get_options($this->context); + if (!isset($context['nativesmb']) || !is_array($context['nativesmb'])) { + throw new InvalidArgumentException("context not set"); + } + $state = $context['nativesmb']['state']; + if (!$state instanceof NativeState) { + throw new InvalidArgumentException("invalid context set"); + } + $this->state = $state; + $handle = $context['nativesmb']['handle']; + if (!is_resource($handle)) { + throw new InvalidArgumentException("invalid context set"); + } + $this->handle = $handle; + $url = $context['nativesmb']['url']; + if (!is_string($url)) { + throw new InvalidArgumentException("invalid context set"); + } + $this->url = $url; + return true; + } + + public function stream_read($count) { + $result = $this->state->read($this->handle, $count, $this->url); + if (strlen($result) < $count) { + $this->eof = true; + } + return $result; + } + + public function stream_seek($offset, $whence = SEEK_SET) { + $this->eof = false; + try { + return $this->state->lseek($this->handle, $offset, $whence, $this->url) !== false; + } catch (InvalidRequestException $e) { + return false; + } + } + + /** + * @return array{"mtime": int, "size": int, "mode": int}|false + */ + public function stream_stat() { + try { + return $this->state->fstat($this->handle, $this->url); + } catch (Exception $e) { + return false; + } + } + + public function stream_tell() { + return $this->state->lseek($this->handle, 0, SEEK_CUR, $this->url); + } + + public function stream_write($data) { + return $this->state->write($this->handle, $data, $this->url); + } + + public function stream_truncate($size) { + return $this->state->ftruncate($this->handle, $size, $this->url); + } + + public function stream_set_option($option, $arg1, $arg2) { + return false; + } + + public function stream_lock($operation) { + return false; + } +} diff --git a/3rdparty/icewind/smb/src/Native/NativeWriteStream.php b/3rdparty/icewind/smb/src/Native/NativeWriteStream.php new file mode 100644 index 00000000..f09c80ee --- /dev/null +++ b/3rdparty/icewind/smb/src/Native/NativeWriteStream.php @@ -0,0 +1,95 @@ + + * SPDX-License-Identifier: MIT + */ + +namespace Icewind\SMB\Native; + +use Icewind\SMB\StringBuffer; + +/** + * Stream optimized for write only usage + */ +class NativeWriteStream extends NativeStream { + const CHUNK_SIZE = 1048576; // 1MB chunks + + /** @var StringBuffer */ + private $writeBuffer; + + /** @var int */ + private $pos = 0; + + public function __construct() { + $this->writeBuffer = new StringBuffer(); + } + + public function stream_open($path, $mode, $options, &$opened_path): bool { + return parent::stream_open($path, $mode, $options, $opened_path); + } + + /** + * Wrap a stream from libsmbclient-php into a regular php stream + * + * @param NativeState $state + * @param resource $smbStream + * @param string $mode + * @param string $url + * @return resource + */ + public static function wrap(NativeState $state, $smbStream, string $mode, string $url) { + return parent::wrapClass($state, $smbStream, $mode, $url, NativeWriteStream::class); + } + + public function stream_seek($offset, $whence = SEEK_SET) { + $this->flushWrite(); + $result = parent::stream_seek($offset, $whence); + if ($result) { + $pos = parent::stream_tell(); + if ($pos === false) { + return false; + } + $this->pos = $pos; + } + return $result; + } + + private function flushWrite(): void { + parent::stream_write($this->writeBuffer->flush()); + } + + public function stream_write($data) { + $written = $this->writeBuffer->push($data); + $this->pos += $written; + + if ($this->writeBuffer->remaining() >= self::CHUNK_SIZE) { + $this->flushWrite(); + } + + return $written; + } + + public function stream_close() { + try { + $this->flushWrite(); + $flushResult = true; + } catch (\Exception $e) { + $flushResult = false; + } + return parent::stream_close() && $flushResult; + } + + public function stream_tell() { + return $this->pos; + } + + public function stream_read($count) { + return false; + } + + public function stream_truncate($size) { + $this->flushWrite(); + $this->pos = $size; + return parent::stream_truncate($size); + } +} diff --git a/3rdparty/icewind/smb/src/Options.php b/3rdparty/icewind/smb/src/Options.php new file mode 100644 index 00000000..f250d4de --- /dev/null +++ b/3rdparty/icewind/smb/src/Options.php @@ -0,0 +1,41 @@ + + * SPDX-License-Identifier: AGPL-3.0-or-later + */ + +namespace Icewind\SMB; + +class Options implements IOptions { + /** @var int */ + private $timeout = 20; + + /** @var string|null */ + private $minProtocol; + /** @var string|null */ + private $maxProtocol; + + public function getTimeout(): int { + return $this->timeout; + } + + public function setTimeout(int $timeout): void { + $this->timeout = $timeout; + } + + public function getMinProtocol(): ?string { + return $this->minProtocol; + } + + public function setMinProtocol(?string $minProtocol): void { + $this->minProtocol = $minProtocol; + } + + public function getMaxProtocol(): ?string { + return $this->maxProtocol; + } + + public function setMaxProtocol(?string $maxProtocol): void { + $this->maxProtocol = $maxProtocol; + } +} diff --git a/3rdparty/icewind/smb/src/ServerFactory.php b/3rdparty/icewind/smb/src/ServerFactory.php new file mode 100644 index 00000000..ee7e5af8 --- /dev/null +++ b/3rdparty/icewind/smb/src/ServerFactory.php @@ -0,0 +1,70 @@ + + * SPDX-License-Identifier: AGPL-3.0-or-later + */ + +namespace Icewind\SMB; + +use Icewind\SMB\Exception\DependencyException; +use Icewind\SMB\Native\NativeServer; +use Icewind\SMB\Wrapped\Server; + +class ServerFactory { + const BACKENDS = [ + NativeServer::class, + Server::class + ]; + + /** @var ISystem */ + private $system; + + /** @var IOptions */ + private $options; + + /** @var ITimeZoneProvider */ + private $timeZoneProvider; + + /** + * ServerFactory constructor. + * + * @param IOptions|null $options + * @param ISystem|null $system + * @param ITimeZoneProvider|null $timeZoneProvider + */ + public function __construct( + ?IOptions $options = null, + ?ISystem $system = null, + ?ITimeZoneProvider $timeZoneProvider = null + ) { + if (is_null($options)) { + $options = new Options(); + } + if (is_null($system)) { + $system = new System(); + } + if (is_null($timeZoneProvider)) { + $timeZoneProvider = new TimeZoneProvider($system); + } + $this->options = $options; + $this->system = $system; + $this->timeZoneProvider = $timeZoneProvider; + } + + + /** + * @param string $host + * @param IAuth $credentials + * @return IServer + * @throws DependencyException + */ + public function createServer(string $host, IAuth $credentials): IServer { + foreach (self::BACKENDS as $backend) { + if (call_user_func("$backend::available", $this->system)) { + return new $backend($host, $credentials, $this->system, $this->timeZoneProvider, $this->options); + } + } + + throw new DependencyException('No valid backend available, ensure smbclient is in the path or php-smbclient is installed'); + } +} diff --git a/3rdparty/icewind/smb/src/StringBuffer.php b/3rdparty/icewind/smb/src/StringBuffer.php new file mode 100644 index 00000000..56d14edb --- /dev/null +++ b/3rdparty/icewind/smb/src/StringBuffer.php @@ -0,0 +1,48 @@ + + * SPDX-License-Identifier: AGPL-3.0-or-later + */ + +namespace Icewind\SMB; + +class StringBuffer { + /** @var string */ + private $buffer = ""; + /** @var int */ + private $pos = 0; + + public function clear(): void { + $this->buffer = ""; + $this->pos = 0; + } + + public function push(string $data): int { + $this->buffer = $this->flush() . $data; + return strlen($data); + } + + public function remaining(): int { + return strlen($this->buffer) - $this->pos; + } + + public function read(int $count): string { + $chunk = substr($this->buffer, $this->pos, $count); + $this->pos += strlen($chunk); + return $chunk; + } + + public function flush(): string { + if ($this->pos === 0) { + $remaining = $this->buffer; + } else { + $remaining = substr($this->buffer, $this->pos); + } + + $this->clear(); + + return $remaining; + } +} diff --git a/3rdparty/icewind/smb/src/System.php b/3rdparty/icewind/smb/src/System.php new file mode 100644 index 00000000..2867b8ee --- /dev/null +++ b/3rdparty/icewind/smb/src/System.php @@ -0,0 +1,75 @@ + + * SPDX-License-Identifier: MIT + */ + +namespace Icewind\SMB; + +use Icewind\SMB\Exception\Exception; + +class System implements ISystem { + /** @var (string|null)[] */ + private $paths = []; + + /** + * Get the path to a file descriptor of the current process + * + * @param int $num the file descriptor id + * @return string + * @throws Exception + */ + public function getFD(int $num): string { + $folders = [ + '/proc/self/fd', + '/dev/fd' + ]; + foreach ($folders as $folder) { + if (file_exists($folder)) { + return $folder . '/' . $num; + } + } + throw new Exception('Cant find file descriptor path'); + } + + public function getSmbclientPath(): ?string { + return $this->getBinaryPath('smbclient'); + } + + public function getNetPath(): ?string { + return $this->getBinaryPath('net'); + } + + public function getSmbcAclsPath(): ?string { + return $this->getBinaryPath('smbcacls'); + } + + public function getStdBufPath(): ?string { + return $this->getBinaryPath('stdbuf'); + } + + public function getDatePath(): ?string { + return $this->getBinaryPath('date'); + } + + public function libSmbclientAvailable(): bool { + return function_exists('smbclient_state_new'); + } + + protected function getBinaryPath(string $binary): ?string { + if (!isset($this->paths[$binary])) { + $result = null; + $output = []; + exec("which $binary 2>&1", $output, $result); + + if ($result === 0 && isset($output[0])) { + $this->paths[$binary] = (string)$output[0]; + } elseif (is_executable("/usr/bin/$binary")) { + $this->paths[$binary] = "/usr/bin/$binary"; + } else { + $this->paths[$binary] = null; + } + } + return $this->paths[$binary]; + } +} diff --git a/3rdparty/icewind/smb/src/TimeZoneProvider.php b/3rdparty/icewind/smb/src/TimeZoneProvider.php new file mode 100644 index 00000000..f1d2c38f --- /dev/null +++ b/3rdparty/icewind/smb/src/TimeZoneProvider.php @@ -0,0 +1,52 @@ + + * SPDX-License-Identifier: MIT + */ +namespace Icewind\SMB; + +class TimeZoneProvider implements ITimeZoneProvider { + /** + * @var string[] + */ + private $timeZones = []; + + /** + * @var ISystem + */ + private $system; + + /** + * @param ISystem $system + */ + public function __construct(ISystem $system) { + $this->system = $system; + } + + public function get(string $host): string { + if (!isset($this->timeZones[$host])) { + $timeZone = null; + $net = $this->system->getNetPath(); + // for local domain names we can assume same timezone + if ($net && $host && strpos($host, '.') !== false) { + $command = sprintf( + '%s time zone -S %s', + $net, + escapeshellarg($host) + ); + $timeZone = exec($command); + } + + if (!$timeZone) { + $date = $this->system->getDatePath(); + if ($date) { + $timeZone = exec($date . " +%z"); + } else { + $timeZone = date_default_timezone_get(); + } + } + $this->timeZones[$host] = $timeZone; + } + return $this->timeZones[$host]; + } +} diff --git a/3rdparty/icewind/smb/src/Wrapped/Connection.php b/3rdparty/icewind/smb/src/Wrapped/Connection.php new file mode 100644 index 00000000..95e63fb1 --- /dev/null +++ b/3rdparty/icewind/smb/src/Wrapped/Connection.php @@ -0,0 +1,115 @@ + + * SPDX-License-Identifier: MIT + */ + +namespace Icewind\SMB\Wrapped; + +use Icewind\SMB\Exception\AccessDeniedException; +use Icewind\SMB\Exception\AuthenticationException; +use Icewind\SMB\Exception\ConnectException; +use Icewind\SMB\Exception\ConnectionException; +use Icewind\SMB\Exception\ConnectionRefusedException; +use Icewind\SMB\Exception\InvalidHostException; +use Icewind\SMB\Exception\NoLoginServerException; + +class Connection extends RawConnection { + const DELIMITER = 'smb:'; + const DELIMITER_LENGTH = 4; + + /** @var Parser */ + private $parser; + + /** + * @param string $command + * @param Parser $parser + * @param array $env + */ + public function __construct(string $command, Parser $parser, array $env = []) { + parent::__construct($command, $env); + $this->parser = $parser; + } + + /** + * send input to smbclient + * + * @param string $input + */ + public function write(string $input) { + return parent::write($input . PHP_EOL); + } + + /** + * @throws ConnectException + */ + public function clearTillPrompt(): void { + $this->write(''); + do { + $promptLine = $this->readTillPrompt(); + if ($promptLine === false) { + break; + } + $this->parser->checkConnectionError($promptLine); + } while (!$this->isPrompt($promptLine)); + if ($this->write('') === false) { + throw new ConnectionRefusedException(); + } + $this->readTillPrompt(); + } + + /** + * get all unprocessed output from smbclient until the next prompt + * + * @return string[] + * @throws AuthenticationException + * @throws ConnectException + * @throws ConnectionException + * @throws InvalidHostException + * @throws NoLoginServerException + * @throws AccessDeniedException + */ + public function read(): array { + if (!$this->isValid()) { + throw new ConnectionException('Connection not valid'); + } + $output = $this->readTillPrompt(); + if ($output === false) { + $this->unknownError(false); + } + $output = explode("\n", $output); + // last line contains the prompt + array_pop($output); + return $output; + } + + private function isPrompt(string $line): bool { + return substr($line, 0, self::DELIMITER_LENGTH) === self::DELIMITER; + } + + /** + * @param string|bool $promptLine (optional) prompt line that might contain some info about the error + * @throws ConnectException + * @return no-return + */ + private function unknownError($promptLine = '') { + if ($promptLine) { //maybe we have some error we missed on the previous line + throw new ConnectException('Unknown error (' . $promptLine . ')'); + } else { + $error = $this->readError(); // maybe something on stderr + if ($error) { + throw new ConnectException('Unknown error (stderr: ' . $error . ')'); + } else { + throw new ConnectException('Unknown error'); + } + } + } + + public function close(bool $terminate = true): void { + if (get_resource_type($this->getInputStream()) === 'stream') { + // ignore any errors while trying to send the close command, the process might already be dead + @$this->write('close' . PHP_EOL); + } + $this->close_process($terminate); + } +} diff --git a/3rdparty/icewind/smb/src/Wrapped/ErrorCodes.php b/3rdparty/icewind/smb/src/Wrapped/ErrorCodes.php new file mode 100644 index 00000000..c053f7b4 --- /dev/null +++ b/3rdparty/icewind/smb/src/Wrapped/ErrorCodes.php @@ -0,0 +1,30 @@ + + * SPDX-License-Identifier: MIT + */ + +namespace Icewind\SMB\Wrapped; + +class ErrorCodes { + /** + * connection errors + */ + const LogonFailure = 'NT_STATUS_LOGON_FAILURE'; + const BadHostName = 'NT_STATUS_BAD_NETWORK_NAME'; + const Unsuccessful = 'NT_STATUS_UNSUCCESSFUL'; + const ConnectionRefused = 'NT_STATUS_CONNECTION_REFUSED'; + const NoLogonServers = 'NT_STATUS_NO_LOGON_SERVERS'; + + const PathNotFound = 'NT_STATUS_OBJECT_PATH_NOT_FOUND'; + const NoSuchFile = 'NT_STATUS_NO_SUCH_FILE'; + const ObjectNotFound = 'NT_STATUS_OBJECT_NAME_NOT_FOUND'; + const NameCollision = 'NT_STATUS_OBJECT_NAME_COLLISION'; + const AccessDenied = 'NT_STATUS_ACCESS_DENIED'; + const DirectoryNotEmpty = 'NT_STATUS_DIRECTORY_NOT_EMPTY'; + const FileIsADirectory = 'NT_STATUS_FILE_IS_A_DIRECTORY'; + const NotADirectory = 'NT_STATUS_NOT_A_DIRECTORY'; + const SharingViolation = 'NT_STATUS_SHARING_VIOLATION'; + const InvalidParameter = 'NT_STATUS_INVALID_PARAMETER'; + const RevisionMismatch = 'NT_STATUS_REVISION_MISMATCH'; +} diff --git a/3rdparty/icewind/smb/src/Wrapped/FileInfo.php b/3rdparty/icewind/smb/src/Wrapped/FileInfo.php new file mode 100644 index 00000000..5e957bd6 --- /dev/null +++ b/3rdparty/icewind/smb/src/Wrapped/FileInfo.php @@ -0,0 +1,88 @@ + + * SPDX-License-Identifier: MIT + */ + +namespace Icewind\SMB\Wrapped; + +use Icewind\SMB\ACL; +use Icewind\SMB\IFileInfo; + +class FileInfo implements IFileInfo { + /** @var string */ + protected $path; + /** @var string */ + protected $name; + /** @var int */ + protected $size; + /** @var int */ + protected $time; + /** @var int */ + protected $mode; + /** @var callable(): ACL[] */ + protected $aclCallback; + + /** + * @param string $path + * @param string $name + * @param int $size + * @param int $time + * @param int $mode + * @param callable(): ACL[] $aclCallback + */ + public function __construct(string $path, string $name, int $size, int $time, int $mode, callable $aclCallback) { + $this->path = $path; + $this->name = $name; + $this->size = $size; + $this->time = $time; + $this->mode = $mode; + $this->aclCallback = $aclCallback; + } + + /** + * @return string + */ + public function getPath(): string { + return $this->path; + } + + public function getName(): string { + return $this->name; + } + + public function getSize(): int { + return $this->size; + } + + public function getMTime(): int { + return $this->time; + } + + public function isDirectory(): bool { + return (bool)($this->mode & IFileInfo::MODE_DIRECTORY); + } + + public function isReadOnly(): bool { + return (bool)($this->mode & IFileInfo::MODE_READONLY); + } + + public function isHidden(): bool { + return (bool)($this->mode & IFileInfo::MODE_HIDDEN); + } + + public function isSystem(): bool { + return (bool)($this->mode & IFileInfo::MODE_SYSTEM); + } + + public function isArchived(): bool { + return (bool)($this->mode & IFileInfo::MODE_ARCHIVE); + } + + /** + * @return ACL[] + */ + public function getAcls(): array { + return ($this->aclCallback)(); + } +} diff --git a/3rdparty/icewind/smb/src/Wrapped/NotifyHandler.php b/3rdparty/icewind/smb/src/Wrapped/NotifyHandler.php new file mode 100644 index 00000000..70638734 --- /dev/null +++ b/3rdparty/icewind/smb/src/Wrapped/NotifyHandler.php @@ -0,0 +1,111 @@ + + * SPDX-License-Identifier: MIT + */ + +namespace Icewind\SMB\Wrapped; + +use Icewind\SMB\Change; +use Icewind\SMB\Exception\Exception; +use Icewind\SMB\Exception\RevisionMismatchException; +use Icewind\SMB\INotifyHandler; + +class NotifyHandler implements INotifyHandler { + /** @var Connection */ + private $connection; + + /** @var string */ + private $path; + + /** @var bool */ + private $listening = true; + + // see error.h + const EXCEPTION_MAP = [ + ErrorCodes::RevisionMismatch => RevisionMismatchException::class, + ]; + + /** + * @param Connection $connection + * @param string $path + */ + public function __construct(Connection $connection, string $path) { + $this->connection = $connection; + $this->path = $path; + } + + /** + * Get all changes detected since the start of the notify process or the last call to getChanges + * + * @return Change[] + */ + public function getChanges(): array { + if (!$this->listening) { + return []; + } + stream_set_blocking($this->connection->getOutputStream(), false); + $lines = []; + while (($line = $this->connection->readLine())) { + $this->checkForError($line); + $lines[] = $line; + } + stream_set_blocking($this->connection->getOutputStream(), true); + return array_values(array_filter(array_map([$this, 'parseChangeLine'], $lines))); + } + + /** + * Listen actively to all incoming changes + * + * Note that this is a blocking process and will cause the process to block forever if not explicitly terminated + * + * @param callable(Change):?bool $callback + */ + public function listen(callable $callback): void { + if ($this->listening) { + while (true) { + $line = $this->connection->readLine(); + if ($line === false) { + break; + } + $this->checkForError($line); + $change = $this->parseChangeLine($line); + if ($change) { + $result = $callback($change); + if ($result === false) { + break; + } + } + }; + } + } + + private function parseChangeLine(string $line): ?Change { + $code = (int)substr($line, 0, 4); + if ($code === 0) { + return null; + } + $subPath = str_replace('\\', '/', substr($line, 5)); + if ($this->path === '') { + return new Change($code, $subPath); + } else { + return new Change($code, $this->path . '/' . $subPath); + } + } + + private function checkForError(string $line): void { + if (substr($line, 0, 16) === 'notify returned ') { + $error = substr($line, 16); + throw Exception::fromMap(array_merge(self::EXCEPTION_MAP, Parser::EXCEPTION_MAP), $error, 'Notify is not supported with the used smb version'); + } + } + + public function stop(): void { + $this->listening = false; + $this->connection->close(); + } + + public function __destruct() { + $this->stop(); + } +} diff --git a/3rdparty/icewind/smb/src/Wrapped/Parser.php b/3rdparty/icewind/smb/src/Wrapped/Parser.php new file mode 100644 index 00000000..06812ee6 --- /dev/null +++ b/3rdparty/icewind/smb/src/Wrapped/Parser.php @@ -0,0 +1,276 @@ + + * SPDX-License-Identifier: MIT + */ + +namespace Icewind\SMB\Wrapped; + +use Icewind\SMB\ACL; +use Icewind\SMB\Exception\AccessDeniedException; +use Icewind\SMB\Exception\AlreadyExistsException; +use Icewind\SMB\Exception\AuthenticationException; +use Icewind\SMB\Exception\Exception; +use Icewind\SMB\Exception\FileInUseException; +use Icewind\SMB\Exception\InvalidHostException; +use Icewind\SMB\Exception\InvalidParameterException; +use Icewind\SMB\Exception\InvalidResourceException; +use Icewind\SMB\Exception\InvalidTypeException; +use Icewind\SMB\Exception\NoLoginServerException; +use Icewind\SMB\Exception\NotEmptyException; +use Icewind\SMB\Exception\NotFoundException; + +class Parser { + const MSG_NOT_FOUND = 'Error opening local file '; + + /** + * @var string + */ + protected $timeZone; + + // see error.h + const EXCEPTION_MAP = [ + ErrorCodes::LogonFailure => AuthenticationException::class, + ErrorCodes::PathNotFound => NotFoundException::class, + ErrorCodes::ObjectNotFound => NotFoundException::class, + ErrorCodes::NoSuchFile => NotFoundException::class, + ErrorCodes::NameCollision => AlreadyExistsException::class, + ErrorCodes::AccessDenied => AccessDeniedException::class, + ErrorCodes::DirectoryNotEmpty => NotEmptyException::class, + ErrorCodes::FileIsADirectory => InvalidTypeException::class, + ErrorCodes::NotADirectory => InvalidTypeException::class, + ErrorCodes::SharingViolation => FileInUseException::class, + ErrorCodes::InvalidParameter => InvalidParameterException::class + ]; + + const MODE_STRINGS = [ + 'R' => FileInfo::MODE_READONLY, + 'H' => FileInfo::MODE_HIDDEN, + 'S' => FileInfo::MODE_SYSTEM, + 'D' => FileInfo::MODE_DIRECTORY, + 'A' => FileInfo::MODE_ARCHIVE, + 'N' => FileInfo::MODE_NORMAL + ]; + + /** + * @param string $timeZone + */ + public function __construct(string $timeZone) { + $this->timeZone = $timeZone; + } + + private function getErrorCode(string $line): ?string { + $parts = explode(' ', $line); + foreach ($parts as $part) { + if (substr($part, 0, 9) === 'NT_STATUS') { + return $part; + } + } + return null; + } + + /** + * @param string[] $output + * @param string $path + * @return no-return + * @throws Exception + * @throws InvalidResourceException + * @throws NotFoundException + */ + public function checkForError(array $output, string $path): void { + if (strpos($output[0], 'does not exist')) { + throw new NotFoundException($path); + } + $error = $this->getErrorCode($output[0]); + + if (substr($output[0], 0, strlen(self::MSG_NOT_FOUND)) === self::MSG_NOT_FOUND) { + $localPath = substr($output[0], strlen(self::MSG_NOT_FOUND)); + throw new InvalidResourceException('Failed opening local file "' . $localPath . '" for writing'); + } + + throw Exception::fromMap(self::EXCEPTION_MAP, $error, $path); + } + + /** + * check if the first line holds a connection failure + * + * @param string $line + * @throws AuthenticationException + * @throws InvalidHostException + * @throws NoLoginServerException + * @throws AccessDeniedException + */ + public function checkConnectionError(string $line): void { + $line = rtrim($line, ')'); + if (substr($line, -23) === ErrorCodes::LogonFailure) { + throw new AuthenticationException('Invalid login'); + } + if (substr($line, -26) === ErrorCodes::BadHostName) { + throw new InvalidHostException('Invalid hostname'); + } + if (substr($line, -22) === ErrorCodes::Unsuccessful) { + throw new InvalidHostException('Connection unsuccessful'); + } + if (substr($line, -28) === ErrorCodes::ConnectionRefused) { + throw new InvalidHostException('Connection refused'); + } + if (substr($line, -26) === ErrorCodes::NoLogonServers) { + throw new NoLoginServerException('No login server'); + } + if (substr($line, -23) === ErrorCodes::AccessDenied) { + throw new AccessDeniedException('Access denied'); + } + } + + public function parseMode(string $mode): int { + $result = 0; + foreach (self::MODE_STRINGS as $char => $val) { + if (strpos($mode, $char) !== false) { + $result |= $val; + } + } + return $result; + } + + /** + * @param string[] $output + * @return array{"mtime": int, "mode": int, "size": int} + * @throws Exception + */ + public function parseStat(array $output): array { + $data = []; + foreach ($output as $line) { + // A line = explode statement may not fill all array elements + // properly. May happen when accessing non Windows Fileservers + $words = explode(':', $line, 2); + $name = isset($words[0]) ? $words[0] : ''; + $value = isset($words[1]) ? $words[1] : ''; + $value = trim($value); + + if (!isset($data[$name])) { + $data[$name] = $value; + } + } + $attributeStart = strpos($data['attributes'], '('); + if ($attributeStart === false) { + throw new Exception("Malformed state response from server"); + } + return [ + 'mtime' => strtotime($data['write_time']), + 'mode' => hexdec(substr($data['attributes'], $attributeStart + 1, -1)), + 'size' => isset($data['stream']) ? (int)(explode(' ', $data['stream'])[1]) : 0 + ]; + } + + /** + * @param string[] $output + * @param string $basePath + * @param callable(string):ACL[] $aclCallback + * @return FileInfo[] + */ + public function parseDir(array $output, string $basePath, callable $aclCallback): array { + //last line is used space + array_pop($output); + $regex = '/^\s*(.*?)\s\s\s\s+(?:([NDHARSCndharsc]*)\s+)?([0-9]+)\s+(.*)$/'; + //2 spaces, filename, optional type, size, date + $content = []; + foreach ($output as $line) { + if (preg_match($regex, $line, $matches)) { + list(, $name, $mode, $size, $time) = $matches; + if ($name !== '.' and $name !== '..') { + $mode = $this->parseMode(strtoupper($mode)); + $time = strtotime($time . ' ' . $this->timeZone); + $path = $basePath . '/' . $name; + $content[] = new FileInfo($path, $name, (int)$size, $time, $mode, function () use ($aclCallback, $path): array { + return $aclCallback($path); + }); + } + } + } + return $content; + } + + /** + * @param string[] $output + * @return array + */ + public function parseListShares(array $output): array { + $shareNames = []; + foreach ($output as $line) { + if (strpos($line, '|')) { + list($type, $name, $description) = explode('|', $line); + if (strtolower($type) === 'disk') { + $shareNames[$name] = $description; + } + } elseif (strpos($line, 'Disk')) { + // new output format + list($name, $description) = explode('Disk', $line); + $shareNames[trim($name)] = trim($description); + } + } + return $shareNames; + } + + /** + * @param string[] $rawAcls + * @return ACL[] + */ + public function parseACLs(array $rawAcls): array { + $acls = []; + foreach ($rawAcls as $acl) { + if (strpos($acl, ':') === false) { + continue; + } + [$type, $acl] = explode(':', $acl, 2); + if ($type !== 'ACL') { + continue; + } + [$user, $permissions] = explode(':', $acl, 2); + [$type, $flags, $mask] = explode('/', $permissions); + + $type = $type === 'ALLOWED' ? ACL::TYPE_ALLOW : ACL::TYPE_DENY; + + $flagsInt = 0; + foreach (explode('|', $flags) as $flagString) { + if ($flagString === 'OI') { + $flagsInt += ACL::FLAG_OBJECT_INHERIT; + } elseif ($flagString === 'CI') { + $flagsInt += ACL::FLAG_CONTAINER_INHERIT; + } + } + + if (substr($mask, 0, 2) === '0x') { + $maskInt = hexdec($mask); + } else { + $maskInt = 0; + foreach (explode('|', $mask) as $maskString) { + if ($maskString === 'R') { + $maskInt += ACL::MASK_READ; + } elseif ($maskString === 'W') { + $maskInt += ACL::MASK_WRITE; + } elseif ($maskString === 'X') { + $maskInt += ACL::MASK_EXECUTE; + } elseif ($maskString === 'D') { + $maskInt += ACL::MASK_DELETE; + } elseif ($maskString === 'READ') { + $maskInt += ACL::MASK_READ + ACL::MASK_EXECUTE; + } elseif ($maskString === 'CHANGE') { + $maskInt += ACL::MASK_READ + ACL::MASK_EXECUTE + ACL::MASK_WRITE + ACL::MASK_DELETE; + } elseif ($maskString === 'FULL') { + $maskInt += ACL::MASK_READ + ACL::MASK_EXECUTE + ACL::MASK_WRITE + ACL::MASK_DELETE; + } + } + } + + if (isset($acls[$user])) { + $existing = $acls[$user]; + $maskInt += $existing->getMask(); + } + $acls[$user] = new ACL($type, $flagsInt, $maskInt); + } + + ksort($acls); + + return $acls; + } +} diff --git a/3rdparty/icewind/smb/src/Wrapped/RawConnection.php b/3rdparty/icewind/smb/src/Wrapped/RawConnection.php new file mode 100644 index 00000000..13828d28 --- /dev/null +++ b/3rdparty/icewind/smb/src/Wrapped/RawConnection.php @@ -0,0 +1,250 @@ + + * SPDX-License-Identifier: MIT + */ + +namespace Icewind\SMB\Wrapped; + +use Icewind\SMB\Exception\ConnectException; +use Icewind\SMB\Exception\ConnectionException; + +class RawConnection { + /** + * @var string + */ + private $command; + + /** + * @var string[] + */ + private $env; + + /** + * @var resource[] $pipes + * + * $pipes[0] holds STDIN for smbclient + * $pipes[1] holds STDOUT for smbclient + * $pipes[3] holds the authfile for smbclient + * $pipes[4] holds the stream for writing files + * $pipes[5] holds the stream for reading files + */ + private $pipes = []; + + /** + * @var resource|null $process + */ + private $process; + + /** + * @var resource|null $authStream + */ + private $authStream = null; + + /** + * @param string $command + * @param array $env + */ + public function __construct(string $command, array $env = []) { + $this->command = $command; + $this->env = $env; + } + + /** + * @throws ConnectException + * @psalm-assert resource $this->process + */ + public function connect(): void { + if (is_null($this->getAuthStream())) { + throw new ConnectException('Authentication not set before connecting'); + } + + $descriptorSpec = [ + 0 => ['pipe', 'r'], // child reads from stdin + 1 => ['pipe', 'w'], // child writes to stdout + 2 => ['pipe', 'w'], // child writes to stderr + 3 => $this->getAuthStream(), // child reads from fd#3 + 4 => ['pipe', 'r'], // child reads from fd#4 + 5 => ['pipe', 'w'] // child writes to fd#5 + ]; + + setlocale(LC_ALL, Server::LOCALE); + $env = array_merge($this->env, [ + 'CLI_FORCE_INTERACTIVE' => 'y', // Make sure the prompt is displayed + 'CLI_NO_READLINE' => 1, // Not all distros build smbclient with readline, disable it to get consistent behaviour + 'LC_ALL' => Server::LOCALE, + 'LANG' => Server::LOCALE, + 'COLUMNS' => 8192, // prevent smbclient from line-wrapping it's output + 'TZ' => 'UTC', + ]); + $this->process = proc_open($this->command, $descriptorSpec, $this->pipes, '/', $env); + if (!$this->isValid()) { + throw new ConnectionException(); + } + } + + /** + * check if the connection is still active + * + * @return bool + * @psalm-assert-if-true resource $this->process + */ + public function isValid(): bool { + if (is_resource($this->process)) { + $status = proc_get_status($this->process); + return $status['running']; + } else { + return false; + } + } + + /** + * send input to the process + * + * @param string $input + * @return int|bool + */ + public function write(string $input) { + $result = @fwrite($this->getInputStream(), $input); + fflush($this->getInputStream()); + return $result; + } + + /** + * read output till the next prompt + * + * @return string|false + */ + public function readTillPrompt() { + $output = ""; + do { + $chunk = $this->readLine('\> '); + if ($chunk === false) { + return false; + } + $output .= $chunk; + } while (strlen($chunk) == 4096 && strpos($chunk, "smb:") === false); + return $output; + } + + /** + * read a line of output + * + * @return string|false + */ + public function readLine(string $end = "\n") { + return stream_get_line($this->getOutputStream(), 4096, $end); + } + + /** + * read a line of output + * + * @return string|false + */ + public function readError() { + $line = stream_get_line($this->getErrorStream(), 4086); + return $line !== false ? trim($line) : false; + } + + /** + * get all output until the process closes + * + * @return string[] + */ + public function readAll(): array { + $output = []; + while ($line = $this->readLine()) { + $output[] = $line; + } + return $output; + } + + /** + * @return resource + */ + public function getInputStream() { + return $this->pipes[0]; + } + + /** + * @return resource + */ + public function getOutputStream() { + return $this->pipes[1]; + } + + /** + * @return resource + */ + public function getErrorStream() { + return $this->pipes[2]; + } + + /** + * @return resource|null + */ + public function getAuthStream() { + return $this->authStream; + } + + /** + * @return resource + */ + public function getFileInputStream() { + return $this->pipes[4]; + } + + /** + * @return resource + */ + public function getFileOutputStream() { + return $this->pipes[5]; + } + + /** + * @param string|null $user + * @param string|null $password + * @psalm-assert resource $this->authStream + */ + public function writeAuthentication(?string $user, ?string $password): void { + $auth = ($password === null) + ? "username=$user" + : "username=$user\npassword=$password\n"; + + $this->authStream = fopen('php://temp', 'w+'); + fwrite($this->authStream, $auth); + rewind($this->authStream); + } + + /** + * @param bool $terminate + * @psalm-assert null $this->process + */ + public function close(bool $terminate = true): void { + $this->close_process($terminate); + } + + /** + * @param bool $terminate + * @psalm-assert null $this->process + */ + protected function close_process(bool $terminate = true): void { + if (!is_resource($this->process)) { + return; + } + if ($terminate) { + proc_terminate($this->process); + } + proc_close($this->process); + $this->process = null; + } + + public function reconnect(): void { + $this->close(); + $this->connect(); + } + + public function __destruct() { + $this->close(); + } +} diff --git a/3rdparty/icewind/smb/src/Wrapped/Server.php b/3rdparty/icewind/smb/src/Wrapped/Server.php new file mode 100644 index 00000000..6d605296 --- /dev/null +++ b/3rdparty/icewind/smb/src/Wrapped/Server.php @@ -0,0 +1,103 @@ + + * SPDX-License-Identifier: MIT + */ + +namespace Icewind\SMB\Wrapped; + +use Icewind\SMB\AbstractServer; +use Icewind\SMB\Exception\AuthenticationException; +use Icewind\SMB\Exception\ConnectException; +use Icewind\SMB\Exception\ConnectionException; +use Icewind\SMB\Exception\ConnectionRefusedException; +use Icewind\SMB\Exception\Exception; +use Icewind\SMB\Exception\InvalidHostException; +use Icewind\SMB\IShare; +use Icewind\SMB\ISystem; + +class Server extends AbstractServer { + /** + * Check if the smbclient php extension is available + * + * @param ISystem $system + * @return bool + */ + public static function available(ISystem $system): bool { + return $system->getSmbclientPath() !== null; + } + + private function getAuthFileArgument(): string { + if ($this->getAuth()->getUsername()) { + return '--authentication-file=' . $this->system->getFD(3); + } else { + return ''; + } + } + + /** + * @return IShare[] + * + * @throws AuthenticationException + * @throws InvalidHostException + * @throws ConnectException + */ + public function listShares(): array { + $maxProtocol = $this->options->getMaxProtocol(); + $minProtocol = $this->options->getMinProtocol(); + $smbClient = $this->system->getSmbclientPath(); + if ($smbClient === null) { + throw new Exception("Backend not available"); + } + $command = sprintf( + '%s %s %s %s %s -L %s', + $smbClient, + $this->getAuthFileArgument(), + $this->getAuth()->getExtraCommandLineArguments(), + $maxProtocol ? "--option='client max protocol=" . $maxProtocol . "'" : "", + $minProtocol ? "--option='client min protocol=" . $minProtocol . "'" : "", + escapeshellarg('//' . $this->getHost()) + ); + $connection = new RawConnection($command); + $connection->writeAuthentication($this->getAuth()->getUsername(), $this->getAuth()->getPassword()); + $connection->connect(); + if (!$connection->isValid()) { + throw new ConnectionException((string)$connection->readLine()); + } + + $parser = new Parser('UTC'); + + $output = $connection->readAll(); + if (isset($output[0])) { + $parser->checkConnectionError($output[0]); + } + + // sometimes we get an empty line first + if (count($output) < 2) { + $output = $connection->readAll(); + } + + if (isset($output[0])) { + $parser->checkConnectionError($output[0]); + } + if (count($output) === 0) { + throw new ConnectionRefusedException(); + } + + $shareNames = $parser->parseListShares($output); + + $shares = []; + foreach ($shareNames as $name => $_description) { + $shares[] = $this->getShare($name); + } + return $shares; + } + + /** + * @param string $name + * @return IShare + */ + public function getShare(string $name): IShare { + return new Share($this, $name, $this->system); + } +} diff --git a/3rdparty/icewind/smb/src/Wrapped/Share.php b/3rdparty/icewind/smb/src/Wrapped/Share.php new file mode 100644 index 00000000..63e1490d --- /dev/null +++ b/3rdparty/icewind/smb/src/Wrapped/Share.php @@ -0,0 +1,553 @@ + + * SPDX-License-Identifier: MIT + */ + +namespace Icewind\SMB\Wrapped; + +use Icewind\SMB\AbstractShare; +use Icewind\SMB\ACL; +use Icewind\SMB\Exception\AlreadyExistsException; +use Icewind\SMB\Exception\AuthenticationException; +use Icewind\SMB\Exception\ConnectException; +use Icewind\SMB\Exception\ConnectionException; +use Icewind\SMB\Exception\DependencyException; +use Icewind\SMB\Exception\Exception; +use Icewind\SMB\Exception\FileInUseException; +use Icewind\SMB\Exception\InvalidHostException; +use Icewind\SMB\Exception\InvalidTypeException; +use Icewind\SMB\Exception\NotFoundException; +use Icewind\SMB\Exception\InvalidRequestException; +use Icewind\SMB\IFileInfo; +use Icewind\SMB\INotifyHandler; +use Icewind\SMB\IServer; +use Icewind\SMB\ISystem; +use Icewind\Streams\CallbackWrapper; +use Icewind\SMB\Native\NativeShare; +use Icewind\SMB\Native\NativeServer; + +class Share extends AbstractShare { + /** + * @var IServer $server + */ + private $server; + + /** + * @var string $name + */ + private $name; + + /** + * @var Connection|null $connection + */ + public $connection = null; + + /** + * @var Parser + */ + protected $parser; + + /** + * @var ISystem + */ + private $system; + + const MODE_MAP = [ + FileInfo::MODE_READONLY => 'r', + FileInfo::MODE_HIDDEN => 'h', + FileInfo::MODE_ARCHIVE => 'a', + FileInfo::MODE_SYSTEM => 's' + ]; + + const EXEC_CMD = 'exec'; + + /** + * @param IServer $server + * @param string $name + * @param ISystem $system + */ + public function __construct(IServer $server, string $name, ISystem $system) { + parent::__construct(); + $this->server = $server; + $this->name = $name; + $this->system = $system; + $this->parser = new Parser('UTC'); + } + + private function getAuthFileArgument(): string { + if ($this->server->getAuth()->getUsername()) { + return '--authentication-file=' . $this->system->getFD(3); + } else { + return ''; + } + } + + protected function getConnection(): Connection { + $maxProtocol = $this->server->getOptions()->getMaxProtocol(); + $minProtocol = $this->server->getOptions()->getMinProtocol(); + $smbClient = $this->system->getSmbclientPath(); + $stdBuf = $this->system->getStdBufPath(); + if ($smbClient === null) { + throw new Exception("Backend not available"); + } + $command = sprintf( + '%s %s%s -t %s %s %s %s %s %s', + self::EXEC_CMD, + $stdBuf ? $stdBuf . ' -o0 ' : '', + $smbClient, + $this->server->getOptions()->getTimeout(), + $this->getAuthFileArgument(), + $this->server->getAuth()->getExtraCommandLineArguments(), + $maxProtocol ? "--option='client max protocol=" . $maxProtocol . "'" : "", + $minProtocol ? "--option='client min protocol=" . $minProtocol . "'" : "", + escapeshellarg('//' . $this->server->getHost() . '/' . $this->name) + ); + $connection = new Connection($command, $this->parser); + $connection->writeAuthentication($this->server->getAuth()->getUsername(), $this->server->getAuth()->getPassword()); + $connection->connect(); + if (!$connection->isValid()) { + throw new ConnectionException((string)$connection->readLine()); + } + // some versions of smbclient add a help message in first of the first prompt + $connection->clearTillPrompt(); + return $connection; + } + + /** + * @throws ConnectionException + * @throws AuthenticationException + * @throws InvalidHostException + * @psalm-assert Connection $this->connection + */ + protected function connect(): Connection { + if ($this->connection and $this->connection->isValid()) { + return $this->connection; + } + $this->connection = $this->getConnection(); + return $this->connection; + } + + /** + * @throws ConnectionException + * @throws AuthenticationException + * @throws InvalidHostException + * @psalm-assert Connection $this->connection + */ + protected function reconnect(): void { + if ($this->connection === null) { + $this->connect(); + } else { + $this->connection->reconnect(); + if (!$this->connection->isValid()) { + throw new ConnectionException(); + } + } + } + + /** + * Get the name of the share + * + * @return string + */ + public function getName(): string { + return $this->name; + } + + protected function simpleCommand(string $command, string $path): bool { + $escapedPath = $this->escapePath($path); + $cmd = $command . ' ' . $escapedPath; + $output = $this->execute($cmd); + return $this->parseOutput($output, $path); + } + + /** + * List the content of a remote folder + * + * @param string $path + * @return IFileInfo[] + * + * @throws NotFoundException + * @throws InvalidTypeException + */ + public function dir(string $path): array { + $escapedPath = $this->escapePath($path); + $output = $this->execute('cd ' . $escapedPath); + //check output for errors + $this->parseOutput($output, $path); + $output = $this->execute('dir'); + + $this->execute('cd /'); + + return $this->parser->parseDir($output, $path, function (string $path) { + return $this->getAcls($path); + }); + } + + /** + * @param string $path + * @return IFileInfo + */ + public function stat(string $path): IFileInfo { + // some windows server setups don't seem to like the allinfo command + // use the dir command instead to get the file info where possible + if ($path !== "" && $path !== "/") { + $parent = dirname($path); + $dir = $this->dir($parent); + $file = array_values(array_filter($dir, function (IFileInfo $info) use ($path) { + return $info->getPath() === $path; + })); + if ($file) { + return $file[0]; + } + } + + $escapedPath = $this->escapePath($path); + $output = $this->execute('allinfo ' . $escapedPath); + // Windows and non Windows Fileserver may respond different + // to the allinfo command for directories. If the result is a single + // line = error line, redo it with a different allinfo parameter + if ($escapedPath == '""' && count($output) < 2) { + $output = $this->execute('allinfo ' . '"."'); + } + if (count($output) < 3) { + $this->parseOutput($output, $path); + } + $stat = $this->parser->parseStat($output); + return new FileInfo($path, basename($path), $stat['size'], $stat['mtime'], $stat['mode'], function () use ($path) { + return $this->getAcls($path); + }); + } + + /** + * Create a folder on the share + * + * @param string $path + * @return bool + * + * @throws NotFoundException + * @throws AlreadyExistsException + */ + public function mkdir(string $path): bool { + return $this->simpleCommand('mkdir', $path); + } + + /** + * Remove a folder on the share + * + * @param string $path + * @return bool + * + * @throws NotFoundException + * @throws InvalidTypeException + */ + public function rmdir(string $path): bool { + return $this->simpleCommand('rmdir', $path); + } + + /** + * Delete a file on the share + * + * @param string $path + * @param bool $secondTry + * @return bool + * @throws InvalidTypeException + * @throws NotFoundException + * @throws \Exception + */ + public function del(string $path, bool $secondTry = false): bool { + //del return a file not found error when trying to delete a folder + //we catch it so we can check if $path doesn't exist or is of invalid type + try { + return $this->simpleCommand('del', $path); + } catch (NotFoundException $e) { + //no need to do anything with the result, we just check if this throws the not found error + try { + $this->simpleCommand('ls', $path); + } catch (NotFoundException $e2) { + throw $e; + } catch (\Exception $e2) { + throw new InvalidTypeException($path); + } + throw $e; + } catch (FileInUseException $e) { + if ($secondTry) { + throw $e; + } + $this->reconnect(); + return $this->del($path, true); + } + } + + /** + * Rename a remote file + * + * @param string $from + * @param string $to + * @return bool + * + * @throws NotFoundException + * @throws AlreadyExistsException + */ + public function rename(string $from, string $to): bool { + $path1 = $this->escapePath($from); + $path2 = $this->escapePath($to); + $output = $this->execute('rename ' . $path1 . ' ' . $path2); + return $this->parseOutput($output, $to); + } + + /** + * Upload a local file + * + * @param string $source local file + * @param string $target remove file + * @return bool + * + * @throws NotFoundException + * @throws InvalidTypeException + */ + public function put(string $source, string $target): bool { + $path1 = $this->escapeLocalPath($source); //first path is local, needs different escaping + $path2 = $this->escapePath($target); + $output = $this->execute('put ' . $path1 . ' ' . $path2); + return $this->parseOutput($output, $target); + } + + /** + * Download a remote file + * + * @param string $source remove file + * @param string $target local file + * @return bool + * + * @throws NotFoundException + * @throws InvalidTypeException + */ + public function get(string $source, string $target): bool { + $path1 = $this->escapePath($source); + $path2 = $this->escapeLocalPath($target); //second path is local, needs different escaping + $output = $this->execute('get ' . $path1 . ' ' . $path2); + return $this->parseOutput($output, $source); + } + + /** + * Open a readable stream to a remote file + * + * @param string $source + * @return resource a read only stream with the contents of the remote file + * + * @throws NotFoundException + * @throws InvalidTypeException + */ + public function read(string $source) { + $source = $this->escapePath($source); + // since returned stream is closed by the caller we need to create a new instance + // since we can't re-use the same file descriptor over multiple calls + $connection = $this->getConnection(); + stream_set_blocking($connection->getOutputStream(), false); + + $connection->write('get ' . $source . ' ' . $this->system->getFD(5)); + $connection->write('exit'); + $fh = $connection->getFileOutputStream(); + $fh = CallbackWrapper::wrap($fh, function() use ($connection) { + $connection->write(''); + }); + if (!is_resource($fh)) { + throw new Exception("Failed to wrap file output"); + } + return $fh; + } + + /** + * Open a writable stream to a remote file + * + * @param string $target + * @return resource a write only stream to upload a remote file + * + * @throws NotFoundException + * @throws InvalidTypeException + */ + public function write(string $target) { + $target = $this->escapePath($target); + // since returned stream is closed by the caller we need to create a new instance + // since we can't re-use the same file descriptor over multiple calls + $connection = $this->getConnection(); + + $fh = $connection->getFileInputStream(); + $connection->write('put ' . $this->system->getFD(4) . ' ' . $target); + $connection->write('exit'); + + // use a close callback to ensure the upload is finished before continuing + // this also serves as a way to keep the connection in scope + $stream = CallbackWrapper::wrap($fh, function() use ($connection) { + $connection->write(''); + }, null, function () use ($connection) { + $connection->close(false); // dont terminate, give the upload some time + }); + if (is_resource($stream)) { + return $stream; + } else { + throw new InvalidRequestException($target); + } + } + + /** + * Append to stream + * Note: smbclient does not support this (Use php-libsmbclient) + * + * @param string $target + * + * @throws DependencyException + */ + public function append(string $target) { + throw new DependencyException('php-libsmbclient is required for append'); + } + + /** + * @param string $path + * @param int $mode a combination of FileInfo::MODE_READONLY, FileInfo::MODE_ARCHIVE, FileInfo::MODE_SYSTEM and FileInfo::MODE_HIDDEN, FileInfo::NORMAL + * @return mixed + */ + public function setMode(string $path, int $mode) { + $modeString = ''; + foreach (self::MODE_MAP as $modeByte => $string) { + if ($mode & $modeByte) { + $modeString .= $string; + } + } + $path = $this->escapePath($path); + + // first reset the mode to normal + $cmd = 'setmode ' . $path . ' -rsha'; + $output = $this->execute($cmd); + $this->parseOutput($output, $path); + + if ($mode !== FileInfo::MODE_NORMAL) { + // then set the modes we want + $cmd = 'setmode ' . $path . ' ' . $modeString; + $output = $this->execute($cmd); + return $this->parseOutput($output, $path); + } else { + return true; + } + } + + /** + * @param string $path + * @return INotifyHandler + * @throws ConnectionException + * @throws DependencyException + */ + public function notify(string $path): INotifyHandler { + if (!$this->system->getStdBufPath()) { //stdbuf is required to disable smbclient's output buffering + throw new DependencyException('stdbuf is required for usage of the notify command'); + } + $connection = $this->getConnection(); // use a fresh connection since the notify command blocks the process + $command = 'notify ' . $this->escapePath($path); + $connection->write($command . PHP_EOL); + return new NotifyHandler($connection, $path); + } + + /** + * @param string $command + * @return string[] + */ + protected function execute(string $command): array { + $this->connect()->write($command); + return $this->connect()->read(); + } + + /** + * check output for errors + * + * @param string[] $lines + * @param string $path + * + * @return bool + * @throws AlreadyExistsException + * @throws \Icewind\SMB\Exception\AccessDeniedException + * @throws \Icewind\SMB\Exception\NotEmptyException + * @throws InvalidTypeException + * @throws \Icewind\SMB\Exception\Exception + * @throws NotFoundException + */ + protected function parseOutput(array $lines, string $path = ''): bool { + if (count($lines) === 0) { + return true; + } else { + $this->parser->checkForError($lines, $path); + } + } + + /** + * @param string $string + * @return string + */ + protected function escape(string $string): string { + return escapeshellarg($string); + } + + /** + * @param string $path + * @return string + */ + protected function escapePath(string $path): string { + $this->verifyPath($path); + if ($path === '/') { + $path = ''; + } + $path = str_replace('/', '\\', $path); + $path = str_replace('"', '^"', $path); + $path = ltrim($path, '\\'); + return '"' . $path . '"'; + } + + /** + * @param string $path + * @return string + */ + protected function escapeLocalPath(string $path): string { + $path = str_replace('"', '\"', $path); + return '"' . $path . '"'; + } + + /** + * @param string $path + * @return ACL[] + * @throws ConnectionException + * @throws ConnectException + */ + protected function getAcls(string $path): array { + $commandPath = $this->system->getSmbcAclsPath(); + if (!$commandPath) { + return []; + } + + $command = sprintf( + '%s %s %s %s/%s %s', + $commandPath, + $this->getAuthFileArgument(), + $this->server->getAuth()->getExtraCommandLineArguments(), + escapeshellarg('//' . $this->server->getHost()), + escapeshellarg($this->name), + escapeshellarg($path) + ); + $connection = new RawConnection($command); + $connection->writeAuthentication($this->server->getAuth()->getUsername(), $this->server->getAuth()->getPassword()); + $connection->connect(); + if (!$connection->isValid()) { + throw new ConnectionException((string)$connection->readLine()); + } + + $rawAcls = $connection->readAll(); + return $this->parser->parseACLs($rawAcls); + } + + public function getServer(): IServer { + return $this->server; + } + + public function __destruct() { + unset($this->connection); + } +} diff --git a/3rdparty/icewind/streams/LICENSE.txt b/3rdparty/icewind/streams/LICENSE.txt new file mode 100644 index 00000000..2cc1fa91 --- /dev/null +++ b/3rdparty/icewind/streams/LICENSE.txt @@ -0,0 +1,19 @@ +Copyright (c) 2015 Robin Appelman + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. diff --git a/3rdparty/icewind/streams/LICENSES/AGPL-3.0-or-later.txt b/3rdparty/icewind/streams/LICENSES/AGPL-3.0-or-later.txt new file mode 100644 index 00000000..0c97efd2 --- /dev/null +++ b/3rdparty/icewind/streams/LICENSES/AGPL-3.0-or-later.txt @@ -0,0 +1,235 @@ +GNU AFFERO GENERAL PUBLIC LICENSE +Version 3, 19 November 2007 + +Copyright (C) 2007 Free Software Foundation, Inc. + +Everyone is permitted to copy and distribute verbatim copies of this license document, but changing it is not allowed. + + Preamble + +The GNU Affero General Public License is a free, copyleft license for software and other kinds of works, specifically designed to ensure cooperation with the community in the case of network server software. + +The licenses for most software and other practical works are designed to take away your freedom to share and change the works. By contrast, our General Public Licenses are intended to guarantee your freedom to share and change all versions of a program--to make sure it remains free software for all its users. + +When we speak of free software, we are referring to freedom, not price. Our General Public Licenses are designed to make sure that you have the freedom to distribute copies of free software (and charge for them if you wish), that you receive source code or can get it if you want it, that you can change the software or use pieces of it in new free programs, and that you know you can do these things. + +Developers that use our General Public Licenses protect your rights with two steps: (1) assert copyright on the software, and (2) offer you this License which gives you legal permission to copy, distribute and/or modify the software. + +A secondary benefit of defending all users' freedom is that improvements made in alternate versions of the program, if they receive widespread use, become available for other developers to incorporate. Many developers of free software are heartened and encouraged by the resulting cooperation. However, in the case of software used on network servers, this result may fail to come about. The GNU General Public License permits making a modified version and letting the public access it on a server without ever releasing its source code to the public. + +The GNU Affero General Public License is designed specifically to ensure that, in such cases, the modified source code becomes available to the community. It requires the operator of a network server to provide the source code of the modified version running there to the users of that server. Therefore, public use of a modified version, on a publicly accessible server, gives the public access to the source code of the modified version. + +An older license, called the Affero General Public License and published by Affero, was designed to accomplish similar goals. This is a different license, not a version of the Affero GPL, but Affero has released a new version of the Affero GPL which permits relicensing under this license. + +The precise terms and conditions for copying, distribution and modification follow. + + TERMS AND CONDITIONS + +0. Definitions. + +"This License" refers to version 3 of the GNU Affero General Public License. + +"Copyright" also means copyright-like laws that apply to other kinds of works, such as semiconductor masks. + +"The Program" refers to any copyrightable work licensed under this License. Each licensee is addressed as "you". "Licensees" and "recipients" may be individuals or organizations. + +To "modify" a work means to copy from or adapt all or part of the work in a fashion requiring copyright permission, other than the making of an exact copy. The resulting work is called a "modified version" of the earlier work or a work "based on" the earlier work. + +A "covered work" means either the unmodified Program or a work based on the Program. + +To "propagate" a work means to do anything with it that, without permission, would make you directly or secondarily liable for infringement under applicable copyright law, except executing it on a computer or modifying a private copy. Propagation includes copying, distribution (with or without modification), making available to the public, and in some countries other activities as well. + +To "convey" a work means any kind of propagation that enables other parties to make or receive copies. Mere interaction with a user through a computer network, with no transfer of a copy, is not conveying. + +An interactive user interface displays "Appropriate Legal Notices" to the extent that it includes a convenient and prominently visible feature that (1) displays an appropriate copyright notice, and (2) tells the user that there is no warranty for the work (except to the extent that warranties are provided), that licensees may convey the work under this License, and how to view a copy of this License. If the interface presents a list of user commands or options, such as a menu, a prominent item in the list meets this criterion. + +1. Source Code. +The "source code" for a work means the preferred form of the work for making modifications to it. "Object code" means any non-source form of a work. + +A "Standard Interface" means an interface that either is an official standard defined by a recognized standards body, or, in the case of interfaces specified for a particular programming language, one that is widely used among developers working in that language. + +The "System Libraries" of an executable work include anything, other than the work as a whole, that (a) is included in the normal form of packaging a Major Component, but which is not part of that Major Component, and (b) serves only to enable use of the work with that Major Component, or to implement a Standard Interface for which an implementation is available to the public in source code form. A "Major Component", in this context, means a major essential component (kernel, window system, and so on) of the specific operating system (if any) on which the executable work runs, or a compiler used to produce the work, or an object code interpreter used to run it. + +The "Corresponding Source" for a work in object code form means all the source code needed to generate, install, and (for an executable work) run the object code and to modify the work, including scripts to control those activities. However, it does not include the work's System Libraries, or general-purpose tools or generally available free programs which are used unmodified in performing those activities but which are not part of the work. For example, Corresponding Source includes interface definition files associated with source files for the work, and the source code for shared libraries and dynamically linked subprograms that the work is specifically designed to require, such as by intimate data communication or control flow between those +subprograms and other parts of the work. + +The Corresponding Source need not include anything that users can regenerate automatically from other parts of the Corresponding Source. + +The Corresponding Source for a work in source code form is that same work. + +2. Basic Permissions. +All rights granted under this License are granted for the term of copyright on the Program, and are irrevocable provided the stated conditions are met. This License explicitly affirms your unlimited permission to run the unmodified Program. The output from running a covered work is covered by this License only if the output, given its content, constitutes a covered work. This License acknowledges your rights of fair use or other equivalent, as provided by copyright law. + +You may make, run and propagate covered works that you do not convey, without conditions so long as your license otherwise remains in force. You may convey covered works to others for the sole purpose of having them make modifications exclusively for you, or provide you with facilities for running those works, provided that you comply with the terms of this License in conveying all material for which you do not control copyright. Those thus making or running the covered works for you must do so exclusively on your behalf, under your direction and control, on terms that prohibit them from making any copies of your copyrighted material outside their relationship with you. + +Conveying under any other circumstances is permitted solely under the conditions stated below. Sublicensing is not allowed; section 10 makes it unnecessary. + +3. Protecting Users' Legal Rights From Anti-Circumvention Law. +No covered work shall be deemed part of an effective technological measure under any applicable law fulfilling obligations under article 11 of the WIPO copyright treaty adopted on 20 December 1996, or similar laws prohibiting or restricting circumvention of such measures. + +When you convey a covered work, you waive any legal power to forbid circumvention of technological measures to the extent such circumvention is effected by exercising rights under this License with respect to the covered work, and you disclaim any intention to limit operation or modification of the work as a means of enforcing, against the work's users, your or third parties' legal rights to forbid circumvention of technological measures. + +4. Conveying Verbatim Copies. +You may convey verbatim copies of the Program's source code as you receive it, in any medium, provided that you conspicuously and appropriately publish on each copy an appropriate copyright notice; keep intact all notices stating that this License and any non-permissive terms added in accord with section 7 apply to the code; keep intact all notices of the absence of any warranty; and give all recipients a copy of this License along with the Program. + +You may charge any price or no price for each copy that you convey, and you may offer support or warranty protection for a fee. + +5. Conveying Modified Source Versions. +You may convey a work based on the Program, or the modifications to produce it from the Program, in the form of source code under the terms of section 4, provided that you also meet all of these conditions: + + a) The work must carry prominent notices stating that you modified it, and giving a relevant date. + + b) The work must carry prominent notices stating that it is released under this License and any conditions added under section 7. This requirement modifies the requirement in section 4 to "keep intact all notices". + + c) You must license the entire work, as a whole, under this License to anyone who comes into possession of a copy. This License will therefore apply, along with any applicable section 7 additional terms, to the whole of the work, and all its parts, regardless of how they are packaged. This License gives no permission to license the work in any other way, but it does not invalidate such permission if you have separately received it. + + d) If the work has interactive user interfaces, each must display Appropriate Legal Notices; however, if the Program has interactive interfaces that do not display Appropriate Legal Notices, your work need not make them do so. + +A compilation of a covered work with other separate and independent works, which are not by their nature extensions of the covered work, and which are not combined with it such as to form a larger program, in or on a volume of a storage or distribution medium, is called an "aggregate" if the compilation and its resulting copyright are not used to limit the access or legal rights of the compilation's users beyond what the individual works permit. Inclusion of a covered work in an aggregate does not cause this License to apply to the other parts of the aggregate. + +6. Conveying Non-Source Forms. +You may convey a covered work in object code form under the terms of sections 4 and 5, provided that you also convey the machine-readable Corresponding Source under the terms of this License, in one of these ways: + + a) Convey the object code in, or embodied in, a physical product (including a physical distribution medium), accompanied by the Corresponding Source fixed on a durable physical medium customarily used for software interchange. + + b) Convey the object code in, or embodied in, a physical product (including a physical distribution medium), accompanied by a written offer, valid for at least three years and valid for as long as you offer spare parts or customer support for that product model, to give anyone who possesses the object code either (1) a copy of the Corresponding Source for all the software in the product that is covered by this License, on a durable physical medium customarily used for software interchange, for a price no more than your reasonable cost of physically performing this conveying of source, or (2) access to copy the Corresponding Source from a network server at no charge. + + c) Convey individual copies of the object code with a copy of the written offer to provide the Corresponding Source. This alternative is allowed only occasionally and noncommercially, and only if you received the object code with such an offer, in accord with subsection 6b. + + d) Convey the object code by offering access from a designated place (gratis or for a charge), and offer equivalent access to the Corresponding Source in the same way through the same place at no further charge. You need not require recipients to copy the Corresponding Source along with the object code. If the place to copy the object code is a network server, the Corresponding Source may be on a different server (operated by you or a third party) that supports equivalent copying facilities, provided you maintain clear directions next to the object code saying where to find the Corresponding Source. Regardless of what server hosts the Corresponding Source, you remain obligated to ensure that it is available for as long as needed to satisfy these requirements. + + e) Convey the object code using peer-to-peer transmission, provided you inform other peers where the object code and Corresponding Source of the work are being offered to the general public at no charge under subsection 6d. + +A separable portion of the object code, whose source code is excluded from the Corresponding Source as a System Library, need not be included in conveying the object code work. + +A "User Product" is either (1) a "consumer product", which means any tangible personal property which is normally used for personal, family, or household purposes, or (2) anything designed or sold for incorporation into a dwelling. In determining whether a product is a consumer product, doubtful cases shall be resolved in favor of coverage. For a particular product received by a particular user, "normally used" refers to a typical or common use of that class of product, regardless of the status of the particular user or of the way in which the particular user actually uses, or expects or is expected to use, the product. A product is a consumer product regardless of whether the product has substantial commercial, industrial or non-consumer uses, unless such uses represent the only significant mode of use of the product. + +"Installation Information" for a User Product means any methods, procedures, authorization keys, or other information required to install and execute modified versions of a covered work in that User Product from a modified version of its Corresponding Source. The information must suffice to ensure that the continued functioning of the modified object code is in no case prevented or interfered with solely because modification has been made. + +If you convey an object code work under this section in, or with, or specifically for use in, a User Product, and the conveying occurs as part of a transaction in which the right of possession and use of the User Product is transferred to the recipient in perpetuity or for a fixed term (regardless of how the transaction is characterized), the Corresponding Source conveyed under this section must be accompanied by the Installation Information. But this requirement does not apply if neither you nor any third party retains the ability to install modified object code on the User Product (for example, the work has been installed in ROM). + +The requirement to provide Installation Information does not include a requirement to continue to provide support service, warranty, or updates for a work that has been modified or installed by the recipient, or for the User Product in which it has been modified or installed. Access to a network may be denied when the modification itself materially and adversely affects the operation of the network or violates the rules and protocols for communication across the network. + +Corresponding Source conveyed, and Installation Information provided, in accord with this section must be in a format that is publicly documented (and with an implementation available to the public in source code form), and must require no special password or key for unpacking, reading or copying. + +7. Additional Terms. +"Additional permissions" are terms that supplement the terms of this License by making exceptions from one or more of its conditions. Additional permissions that are applicable to the entire Program shall be treated as though they were included in this License, to the extent that they are valid under applicable law. If additional permissions apply only to part of the Program, that part may be used separately under those permissions, but the entire Program remains governed by this License without regard to the additional permissions. + +When you convey a copy of a covered work, you may at your option remove any additional permissions from that copy, or from any part of it. (Additional permissions may be written to require their own removal in certain cases when you modify the work.) You may place additional permissions on material, added by you to a covered work, for which you have or can give appropriate copyright permission. + +Notwithstanding any other provision of this License, for material you add to a covered work, you may (if authorized by the copyright holders of that material) supplement the terms of this License with terms: + + a) Disclaiming warranty or limiting liability differently from the terms of sections 15 and 16 of this License; or + + b) Requiring preservation of specified reasonable legal notices or author attributions in that material or in the Appropriate Legal Notices displayed by works containing it; or + + c) Prohibiting misrepresentation of the origin of that material, or requiring that modified versions of such material be marked in reasonable ways as different from the original version; or + + d) Limiting the use for publicity purposes of names of licensors or authors of the material; or + + e) Declining to grant rights under trademark law for use of some trade names, trademarks, or service marks; or + + f) Requiring indemnification of licensors and authors of that material by anyone who conveys the material (or modified versions of it) with contractual assumptions of liability to the recipient, for any liability that these contractual assumptions directly impose on those licensors and authors. + +All other non-permissive additional terms are considered "further restrictions" within the meaning of section 10. If the Program as you received it, or any part of it, contains a notice stating that it is governed by this License along with a term that is a further restriction, you may remove that term. If a license document contains a further restriction but permits relicensing or conveying under this License, you may add to a covered work material governed by the terms of that license document, provided that the further restriction does not survive such relicensing or conveying. + +If you add terms to a covered work in accord with this section, you must place, in the relevant source files, a statement of the additional terms that apply to those files, or a notice indicating where to find the applicable terms. + +Additional terms, permissive or non-permissive, may be stated in the form of a separately written license, or stated as exceptions; the above requirements apply either way. + +8. Termination. + +You may not propagate or modify a covered work except as expressly provided under this License. Any attempt otherwise to propagate or modify it is void, and will automatically terminate your rights under this License (including any patent licenses granted under the third paragraph of section 11). + +However, if you cease all violation of this License, then your license from a particular copyright holder is reinstated (a) provisionally, unless and until the copyright holder explicitly and finally terminates your license, and (b) permanently, if the copyright holder fails to notify you of the violation by some reasonable means prior to 60 days after the cessation. + +Moreover, your license from a particular copyright holder is reinstated permanently if the copyright holder notifies you of the violation by some reasonable means, this is the first time you have received notice of violation of this License (for any work) from that copyright holder, and you cure the violation prior to 30 days after your receipt of the notice. + +Termination of your rights under this section does not terminate the licenses of parties who have received copies or rights from you under this License. If your rights have been terminated and not permanently reinstated, you do not qualify to receive new licenses for the same material under section 10. + +9. Acceptance Not Required for Having Copies. + +You are not required to accept this License in order to receive or run a copy of the Program. Ancillary propagation of a covered work occurring solely as a consequence of using peer-to-peer transmission to receive a copy likewise does not require acceptance. However, nothing other than this License grants you permission to propagate or modify any covered work. These actions infringe copyright if you do not accept this License. Therefore, by modifying or propagating a covered work, you indicate your acceptance of this License to do so. + +10. Automatic Licensing of Downstream Recipients. + +Each time you convey a covered work, the recipient automatically receives a license from the original licensors, to run, modify and propagate that work, subject to this License. You are not responsible for enforcing compliance by third parties with this License. + +An "entity transaction" is a transaction transferring control of an organization, or substantially all assets of one, or subdividing an organization, or merging organizations. If propagation of a covered work results from an entity transaction, each party to that transaction who receives a copy of the work also receives whatever licenses to the work the party's predecessor in interest had or could give under the previous paragraph, plus a right to possession of the Corresponding Source of the work from the predecessor in interest, if the predecessor has it or can get it with reasonable efforts. + +You may not impose any further restrictions on the exercise of the rights granted or affirmed under this License. For example, you may not impose a license fee, royalty, or other charge for exercise of rights granted under this License, and you may not initiate litigation (including a cross-claim or counterclaim in a lawsuit) alleging that any patent claim is infringed by making, using, selling, offering for sale, or importing the Program or any portion of it. + +11. Patents. + +A "contributor" is a copyright holder who authorizes use under this License of the Program or a work on which the Program is based. The work thus licensed is called the contributor's "contributor version". + +A contributor's "essential patent claims" are all patent claims owned or controlled by the contributor, whether already acquired or hereafter acquired, that would be infringed by some manner, permitted by this License, of making, using, or selling its contributor version, but do not include claims that would be infringed only as a consequence of further modification of the contributor version. For purposes of this definition, "control" includes the right to grant patent sublicenses in a manner consistent with the requirements of this License. + +Each contributor grants you a non-exclusive, worldwide, royalty-free patent license under the contributor's essential patent claims, to make, use, sell, offer for sale, import and otherwise run, modify and propagate the contents of its contributor version. + +In the following three paragraphs, a "patent license" is any express agreement or commitment, however denominated, not to enforce a patent (such as an express permission to practice a patent or covenant not to sue for patent infringement). To "grant" such a patent license to a party means to make such an agreement or commitment not to enforce a patent against the party. + +If you convey a covered work, knowingly relying on a patent license, and the Corresponding Source of the work is not available for anyone to copy, free of charge and under the terms of this License, through a publicly available network server or other readily accessible means, then you must either (1) cause the Corresponding Source to be so available, or (2) arrange to deprive yourself of the benefit of the patent license for this particular work, or (3) arrange, in a manner consistent with the requirements of this License, to extend the patent +license to downstream recipients. "Knowingly relying" means you have actual knowledge that, but for the patent license, your conveying the covered work in a country, or your recipient's use of the covered work in a country, would infringe one or more identifiable patents in that country that you have reason to believe are valid. + +If, pursuant to or in connection with a single transaction or arrangement, you convey, or propagate by procuring conveyance of, a covered work, and grant a patent license to some of the parties receiving the covered work authorizing them to use, propagate, modify or convey a specific copy of the covered work, then the patent license you grant is automatically extended to all recipients of the covered work and works based on it. + +A patent license is "discriminatory" if it does not include within the scope of its coverage, prohibits the exercise of, or is conditioned on the non-exercise of one or more of the rights that are specifically granted under this License. You may not convey a covered work if you are a party to an arrangement with a third party that is in the business of distributing software, under which you make payment to the third party based on the extent of your activity of conveying the work, and under which the third party grants, to any of the parties who would receive the covered work from you, a discriminatory patent license (a) in connection with copies of the covered work conveyed by you (or copies made from those copies), or (b) primarily for and in connection with specific products or compilations that contain the covered work, unless you entered into that arrangement, or that patent license was granted, prior to 28 March 2007. + +Nothing in this License shall be construed as excluding or limiting any implied license or other defenses to infringement that may otherwise be available to you under applicable patent law. + +12. No Surrender of Others' Freedom. + +If conditions are imposed on you (whether by court order, agreement or otherwise) that contradict the conditions of this License, they do not excuse you from the conditions of this License. If you cannot convey a covered work so as to satisfy simultaneously your obligations under this License and any other pertinent obligations, then as a consequence you may +not convey it at all. For example, if you agree to terms that obligate you to collect a royalty for further conveying from those to whom you convey the Program, the only way you could satisfy both those terms and this License would be to refrain entirely from conveying the Program. + +13. Remote Network Interaction; Use with the GNU General Public License. + +Notwithstanding any other provision of this License, if you modify the Program, your modified version must prominently offer all users interacting with it remotely through a computer network (if your version supports such interaction) an opportunity to receive the Corresponding Source of your version by providing access to the Corresponding Source from a network server at no charge, through some standard or customary means of facilitating copying of software. This Corresponding Source shall include the Corresponding Source for any work covered by version 3 of the GNU General Public License that is incorporated pursuant to the following paragraph. + +Notwithstanding any other provision of this License, you have permission to link or combine any covered work with a work licensed under version 3 of the GNU General Public License into a single combined work, and to convey the resulting work. The terms of this License will continue to apply to the part which is the covered work, but the work with which it is combined will remain governed by version 3 of the GNU General Public License. + +14. Revised Versions of this License. + +The Free Software Foundation may publish revised and/or new versions of the GNU Affero General Public License from time to time. Such new versions will be similar in spirit to the present version, but may differ in detail to address new problems or concerns. + +Each version is given a distinguishing version number. If the Program specifies that a certain numbered version of the GNU Affero General Public License "or any later version" applies to it, you have the option of following the terms and conditions either of that numbered version or of any later version published by the Free Software Foundation. If the Program does not specify a version number of the GNU Affero General Public License, you may choose any version ever published by the Free Software Foundation. + +If the Program specifies that a proxy can decide which future versions of the GNU Affero General Public License can be used, that proxy's public statement of acceptance of a version permanently authorizes you to choose that version for the Program. + +Later license versions may give you additional or different permissions. However, no additional obligations are imposed on any author or copyright holder as a result of your choosing to follow a later version. + +15. Disclaimer of Warranty. + +THERE IS NO WARRANTY FOR THE PROGRAM, TO THE EXTENT PERMITTED BY APPLICABLE LAW. EXCEPT WHEN OTHERWISE STATED IN WRITING THE COPYRIGHT HOLDERS AND/OR OTHER PARTIES PROVIDE THE PROGRAM "AS IS" WITHOUT WARRANTY OF ANY KIND, EITHER EXPRESSED OR IMPLIED, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE. THE ENTIRE RISK AS TO THE QUALITY AND PERFORMANCE OF THE PROGRAM IS WITH YOU. SHOULD THE PROGRAM PROVE DEFECTIVE, YOU ASSUME THE COST OF ALL NECESSARY SERVICING, REPAIR OR CORRECTION. + +16. Limitation of Liability. + +IN NO EVENT UNLESS REQUIRED BY APPLICABLE LAW OR AGREED TO IN WRITING WILL ANY COPYRIGHT HOLDER, OR ANY OTHER PARTY WHO MODIFIES AND/OR CONVEYS THE PROGRAM AS PERMITTED ABOVE, BE LIABLE TO YOU FOR DAMAGES, INCLUDING ANY GENERAL, SPECIAL, INCIDENTAL OR CONSEQUENTIAL DAMAGES ARISING OUT OF THE USE OR INABILITY TO USE THE PROGRAM (INCLUDING BUT NOT LIMITED TO LOSS OF DATA OR DATA BEING RENDERED INACCURATE OR LOSSES SUSTAINED BY YOU OR THIRD PARTIES OR A FAILURE OF THE PROGRAM TO OPERATE WITH ANY OTHER PROGRAMS), EVEN IF SUCH HOLDER OR OTHER PARTY HAS BEEN ADVISED OF THE POSSIBILITY OF SUCH DAMAGES. + +17. Interpretation of Sections 15 and 16. + +If the disclaimer of warranty and limitation of liability provided above cannot be given local legal effect according to their terms, reviewing courts shall apply local law that most closely approximates an absolute waiver of all civil liability in connection with the Program, unless a warranty or assumption of liability accompanies a copy of the Program in return for a fee. + +END OF TERMS AND CONDITIONS + + How to Apply These Terms to Your New Programs + +If you develop a new program, and you want it to be of the greatest possible use to the public, the best way to achieve this is to make it free software which everyone can redistribute and change under these terms. + +To do so, attach the following notices to the program. It is safest to attach them to the start of each source file to most effectively state the exclusion of warranty; and each file should have at least the "copyright" line and a pointer to where the full notice is found. + + + Copyright (C) + + This program is free software: you can redistribute it and/or modify it under the terms of the GNU Affero General Public License as published by the Free Software Foundation, either version 3 of the License, or (at your option) any later version. + + This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU Affero General Public License for more details. + + You should have received a copy of the GNU Affero General Public License along with this program. If not, see . + +Also add information on how to contact you by electronic and paper mail. + +If your software can interact with users remotely through a computer network, you should also make sure that it provides a way for users to get its source. For example, if your program is a web application, its interface could display a "Source" link that leads users to an archive of the code. There are many ways you could offer source, and different solutions will be better for different programs; see section 13 for the specific requirements. + +You should also get your employer (if you work as a programmer) or school, if any, to sign a "copyright disclaimer" for the program, if necessary. For more information on this, and how to apply and follow the GNU AGPL, see . diff --git a/3rdparty/icewind/streams/LICENSES/CC0-1.0.txt b/3rdparty/icewind/streams/LICENSES/CC0-1.0.txt new file mode 100644 index 00000000..0e259d42 --- /dev/null +++ b/3rdparty/icewind/streams/LICENSES/CC0-1.0.txt @@ -0,0 +1,121 @@ +Creative Commons Legal Code + +CC0 1.0 Universal + + CREATIVE COMMONS CORPORATION IS NOT A LAW FIRM AND DOES NOT PROVIDE + LEGAL SERVICES. DISTRIBUTION OF THIS DOCUMENT DOES NOT CREATE AN + ATTORNEY-CLIENT RELATIONSHIP. CREATIVE COMMONS PROVIDES THIS + INFORMATION ON AN "AS-IS" BASIS. CREATIVE COMMONS MAKES NO WARRANTIES + REGARDING THE USE OF THIS DOCUMENT OR THE INFORMATION OR WORKS + PROVIDED HEREUNDER, AND DISCLAIMS LIABILITY FOR DAMAGES RESULTING FROM + THE USE OF THIS DOCUMENT OR THE INFORMATION OR WORKS PROVIDED + HEREUNDER. + +Statement of Purpose + +The laws of most jurisdictions throughout the world automatically confer +exclusive Copyright and Related Rights (defined below) upon the creator +and subsequent owner(s) (each and all, an "owner") of an original work of +authorship and/or a database (each, a "Work"). + +Certain owners wish to permanently relinquish those rights to a Work for +the purpose of contributing to a commons of creative, cultural and +scientific works ("Commons") that the public can reliably and without fear +of later claims of infringement build upon, modify, incorporate in other +works, reuse and redistribute as freely as possible in any form whatsoever +and for any purposes, including without limitation commercial purposes. +These owners may contribute to the Commons to promote the ideal of a free +culture and the further production of creative, cultural and scientific +works, or to gain reputation or greater distribution for their Work in +part through the use and efforts of others. + +For these and/or other purposes and motivations, and without any +expectation of additional consideration or compensation, the person +associating CC0 with a Work (the "Affirmer"), to the extent that he or she +is an owner of Copyright and Related Rights in the Work, voluntarily +elects to apply CC0 to the Work and publicly distribute the Work under its +terms, with knowledge of his or her Copyright and Related Rights in the +Work and the meaning and intended legal effect of CC0 on those rights. + +1. Copyright and Related Rights. A Work made available under CC0 may be +protected by copyright and related or neighboring rights ("Copyright and +Related Rights"). Copyright and Related Rights include, but are not +limited to, the following: + + i. the right to reproduce, adapt, distribute, perform, display, + communicate, and translate a Work; + ii. moral rights retained by the original author(s) and/or performer(s); +iii. publicity and privacy rights pertaining to a person's image or + likeness depicted in a Work; + iv. rights protecting against unfair competition in regards to a Work, + subject to the limitations in paragraph 4(a), below; + v. rights protecting the extraction, dissemination, use and reuse of data + in a Work; + vi. database rights (such as those arising under Directive 96/9/EC of the + European Parliament and of the Council of 11 March 1996 on the legal + protection of databases, and under any national implementation + thereof, including any amended or successor version of such + directive); and +vii. other similar, equivalent or corresponding rights throughout the + world based on applicable law or treaty, and any national + implementations thereof. + +2. Waiver. To the greatest extent permitted by, but not in contravention +of, applicable law, Affirmer hereby overtly, fully, permanently, +irrevocably and unconditionally waives, abandons, and surrenders all of +Affirmer's Copyright and Related Rights and associated claims and causes +of action, whether now known or unknown (including existing as well as +future claims and causes of action), in the Work (i) in all territories +worldwide, (ii) for the maximum duration provided by applicable law or +treaty (including future time extensions), (iii) in any current or future +medium and for any number of copies, and (iv) for any purpose whatsoever, +including without limitation commercial, advertising or promotional +purposes (the "Waiver"). Affirmer makes the Waiver for the benefit of each +member of the public at large and to the detriment of Affirmer's heirs and +successors, fully intending that such Waiver shall not be subject to +revocation, rescission, cancellation, termination, or any other legal or +equitable action to disrupt the quiet enjoyment of the Work by the public +as contemplated by Affirmer's express Statement of Purpose. + +3. Public License Fallback. Should any part of the Waiver for any reason +be judged legally invalid or ineffective under applicable law, then the +Waiver shall be preserved to the maximum extent permitted taking into +account Affirmer's express Statement of Purpose. In addition, to the +extent the Waiver is so judged Affirmer hereby grants to each affected +person a royalty-free, non transferable, non sublicensable, non exclusive, +irrevocable and unconditional license to exercise Affirmer's Copyright and +Related Rights in the Work (i) in all territories worldwide, (ii) for the +maximum duration provided by applicable law or treaty (including future +time extensions), (iii) in any current or future medium and for any number +of copies, and (iv) for any purpose whatsoever, including without +limitation commercial, advertising or promotional purposes (the +"License"). The License shall be deemed effective as of the date CC0 was +applied by Affirmer to the Work. Should any part of the License for any +reason be judged legally invalid or ineffective under applicable law, such +partial invalidity or ineffectiveness shall not invalidate the remainder +of the License, and in such case Affirmer hereby affirms that he or she +will not (i) exercise any of his or her remaining Copyright and Related +Rights in the Work or (ii) assert any associated claims and causes of +action with respect to the Work, in either case contrary to Affirmer's +express Statement of Purpose. + +4. Limitations and Disclaimers. + + a. No trademark or patent rights held by Affirmer are waived, abandoned, + surrendered, licensed or otherwise affected by this document. + b. Affirmer offers the Work as-is and makes no representations or + warranties of any kind concerning the Work, express, implied, + statutory or otherwise, including without limitation warranties of + title, merchantability, fitness for a particular purpose, non + infringement, or the absence of latent or other defects, accuracy, or + the present or absence of errors, whether or not discoverable, all to + the greatest extent permissible under applicable law. + c. Affirmer disclaims responsibility for clearing rights of other persons + that may apply to the Work or any use thereof, including without + limitation any person's Copyright and Related Rights in the Work. + Further, Affirmer disclaims responsibility for obtaining any necessary + consents, permissions or other rights required for any use of the + Work. + d. Affirmer understands and acknowledges that Creative Commons is not a + party to this document and has no duty or obligation with respect to + this CC0 or use of the Work. diff --git a/3rdparty/icewind/streams/LICENSES/MIT.txt b/3rdparty/icewind/streams/LICENSES/MIT.txt new file mode 100644 index 00000000..2071b23b --- /dev/null +++ b/3rdparty/icewind/streams/LICENSES/MIT.txt @@ -0,0 +1,9 @@ +MIT License + +Copyright (c) + +Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. diff --git a/3rdparty/icewind/streams/composer.json.license b/3rdparty/icewind/streams/composer.json.license new file mode 100644 index 00000000..99ba7368 --- /dev/null +++ b/3rdparty/icewind/streams/composer.json.license @@ -0,0 +1,2 @@ +SPDX-FileCopyrightText: 2014 Robin Appelman +SPDX-License-Identifier: MIT diff --git a/3rdparty/icewind/streams/src/CallbackWrapper.php b/3rdparty/icewind/streams/src/CallbackWrapper.php new file mode 100644 index 00000000..a50aa05c --- /dev/null +++ b/3rdparty/icewind/streams/src/CallbackWrapper.php @@ -0,0 +1,131 @@ + + * SPDX-License-Identifier: MIT + */ +namespace Icewind\Streams; + +/** + * Wrapper that provides callbacks for write, read and close + * + * The following options should be passed in the context when opening the stream + * [ + * 'callback' => [ + * 'source' => resource + * 'read' => function($count){} (optional) + * 'write' => function($data){} (optional) + * 'close' => function(){} (optional) + * 'readdir' => function(){} (optional) + * ] + * ] + * + * All callbacks are called after the operation is executed on the source stream + */ +class CallbackWrapper extends Wrapper { + /** + * @var callable|null + */ + protected $readCallback; + + /** + * @var callable|null + */ + protected $writeCallback; + + /** + * @var callable|null + */ + protected $closeCallback; + + /** + * @var callable|null + */ + protected $readDirCallBack; + + /** + * @var callable|null + */ + protected $preCloseCallback; + + /** + * Wraps a stream with the provided callbacks + * + * @param resource $source + * @param callable|null $read (optional) + * @param callable|null $write (optional) + * @param callable|null $close (optional) + * @param callable|null $readDir (optional) + * @param callable|null $preClose (optional) + * @return resource|false + * + */ + public static function wrap($source, $read = null, $write = null, $close = null, $readDir = null, $preClose = null) { + $context = [ + 'source' => $source, + 'read' => $read, + 'write' => $write, + 'close' => $close, + 'readDir' => $readDir, + 'preClose' => $preClose, + ]; + return self::wrapSource($source, $context); + } + + protected function open() { + $context = $this->loadContext(); + + $this->readCallback = $context['read']; + $this->writeCallback = $context['write']; + $this->closeCallback = $context['close']; + $this->readDirCallBack = $context['readDir']; + $this->preCloseCallback = $context['preClose']; + return true; + } + + public function dir_opendir($path, $options) { + return $this->open(); + } + + public function stream_open($path, $mode, $options, &$opened_path) { + return $this->open(); + } + + public function stream_read($count) { + $result = parent::stream_read($count); + if (is_callable($this->readCallback)) { + call_user_func($this->readCallback, strlen($result)); + } + return $result; + } + + public function stream_write($data) { + $result = parent::stream_write($data); + if (is_callable($this->writeCallback)) { + call_user_func($this->writeCallback, $data); + } + return $result; + } + + public function stream_close() { + if (is_callable($this->preCloseCallback)) { + call_user_func($this->preCloseCallback, $this->source); + // prevent further calls by potential PHP 7 GC ghosts + $this->preCloseCallback = null; + } + $result = parent::stream_close(); + if (is_callable($this->closeCallback)) { + call_user_func($this->closeCallback); + // prevent further calls by potential PHP 7 GC ghosts + $this->closeCallback = null; + } + return $result; + } + + public function dir_readdir() { + $result = parent::dir_readdir(); + if (is_callable($this->readDirCallBack)) { + call_user_func($this->readDirCallBack); + } + return $result; + } +} diff --git a/3rdparty/icewind/streams/src/CountWrapper.php b/3rdparty/icewind/streams/src/CountWrapper.php new file mode 100644 index 00000000..eb2ba9a9 --- /dev/null +++ b/3rdparty/icewind/streams/src/CountWrapper.php @@ -0,0 +1,99 @@ + + * SPDX-License-Identifier: AGPL-3.0-or-later + */ + +namespace Icewind\Streams; + +/** + * Wrapper that counts the amount of data read and written + * + * The following options should be passed in the context when opening the stream + * [ + * 'callback' => [ + * 'source' => resource + * 'callback' => function($readCount, $writeCount){} + * ] + * ] + * + * The callback will be called when the stream is closed + */ +class CountWrapper extends Wrapper { + /** + * @var int + */ + protected $readCount = 0; + + /** + * @var int + */ + protected $writeCount = 0; + + /** + * @var callable + */ + protected $callback; + + /** + * Wraps a stream with the provided callbacks + * + * @param resource $source + * @param callable $callback + * @return resource|false + * + * @throws \BadMethodCallException + */ + public static function wrap($source, $callback) { + if (!is_callable($callback)) { + throw new \InvalidArgumentException('Invalid or missing callback'); + } + return self::wrapSource($source, [ + 'source' => $source, + 'callback' => $callback + ]); + } + + protected function open() { + $context = $this->loadContext(); + $this->callback = $context['callback']; + return true; + } + + public function stream_seek($offset, $whence = SEEK_SET) { + if ($whence === SEEK_SET) { + $this->readCount = $offset; + $this->writeCount = $offset; + } else if ($whence === SEEK_CUR) { + $this->readCount += $offset; + $this->writeCount += $offset; + } + return parent::stream_seek($offset, $whence); + } + + public function dir_opendir($path, $options) { + return $this->open(); + } + + public function stream_open($path, $mode, $options, &$opened_path) { + return $this->open(); + } + + public function stream_read($count) { + $result = parent::stream_read($count); + $this->readCount += strlen($result); + return $result; + } + + public function stream_write($data) { + $result = parent::stream_write($data); + $this->writeCount += strlen($data); + return $result; + } + + public function stream_close() { + $result = parent::stream_close(); + call_user_func($this->callback, $this->readCount, $this->writeCount); + return $result; + } +} diff --git a/3rdparty/icewind/streams/src/Directory.php b/3rdparty/icewind/streams/src/Directory.php new file mode 100644 index 00000000..3a4d01e6 --- /dev/null +++ b/3rdparty/icewind/streams/src/Directory.php @@ -0,0 +1,34 @@ + + * SPDX-License-Identifier: MIT + */ + +namespace Icewind\Streams; + +/** + * Interface for stream wrappers that implements a directory + */ +interface Directory { + /** + * @param string $path + * @param array $options + * @return bool + */ + public function dir_opendir($path, $options); + + /** + * @return string|bool + */ + public function dir_readdir(); + + /** + * @return bool + */ + public function dir_closedir(); + + /** + * @return bool + */ + public function dir_rewinddir(); +} diff --git a/3rdparty/icewind/streams/src/DirectoryFilter.php b/3rdparty/icewind/streams/src/DirectoryFilter.php new file mode 100644 index 00000000..3df6a37f --- /dev/null +++ b/3rdparty/icewind/streams/src/DirectoryFilter.php @@ -0,0 +1,56 @@ + + * SPDX-License-Identifier: MIT + */ + +namespace Icewind\Streams; + +/** + * Wrapper allows filtering of directories + * + * The filter callback will be called for each entry in the folder + * when the callback return false the entry will be filtered out + */ +class DirectoryFilter extends DirectoryWrapper { + /** + * @var callable + */ + private $filter; + + /** + * @param string $path + * @param array $options + * @return bool + */ + public function dir_opendir($path, $options) { + $context = $this->loadContext(); + $this->filter = $context['filter']; + return true; + } + + /** + * @return string + */ + public function dir_readdir() { + $file = readdir($this->source); + $filter = $this->filter; + // keep reading until we have an accepted entry or we're at the end of the folder + while ($file !== false && $filter($file) === false) { + $file = readdir($this->source); + } + return $file; + } + + /** + * @param resource $source + * @param callable $filter + * @return resource|false + */ + public static function wrap($source, callable $filter) { + return self::wrapSource($source, [ + 'source' => $source, + 'filter' => $filter + ]); + } +} diff --git a/3rdparty/icewind/streams/src/DirectoryWrapper.php b/3rdparty/icewind/streams/src/DirectoryWrapper.php new file mode 100644 index 00000000..fbae56a8 --- /dev/null +++ b/3rdparty/icewind/streams/src/DirectoryWrapper.php @@ -0,0 +1,46 @@ + + * SPDX-License-Identifier: MIT + */ + +namespace Icewind\Streams; + +class DirectoryWrapper extends Wrapper implements Directory { + public function stream_open($path, $mode, $options, &$opened_path) { + return false; + } + + /** + * @param string $path + * @param array $options + * @return bool + */ + public function dir_opendir($path, $options) { + $this->loadContext(); + return true; + } + + /** + * @return string|false + */ + public function dir_readdir() { + return readdir($this->source); + } + + /** + * @return bool + */ + public function dir_closedir() { + closedir($this->source); + return true; + } + + /** + * @return bool + */ + public function dir_rewinddir() { + rewinddir($this->source); + return true; + } +} diff --git a/3rdparty/icewind/streams/src/File.php b/3rdparty/icewind/streams/src/File.php new file mode 100644 index 00000000..249c92d1 --- /dev/null +++ b/3rdparty/icewind/streams/src/File.php @@ -0,0 +1,85 @@ + + * SPDX-License-Identifier: MIT + */ + +namespace Icewind\Streams; + +/** + * Interface for stream wrappers that implements a file + */ +interface File { + /** + * @param string $path + * @param string $mode + * @param int $options + * @param string $opened_path + * @return bool + */ + public function stream_open($path, $mode, $options, &$opened_path); + + /** + * @param int $offset + * @param int $whence + * @return bool + */ + public function stream_seek($offset, $whence = SEEK_SET); + + /** + * @return int|false + */ + public function stream_tell(); + + /** + * @param int $count + * @return string|false + */ + public function stream_read($count); + + /** + * @param string $data + * @return int|false + */ + public function stream_write($data); + + /** + * @param int $option + * @param int $arg1 + * @param int $arg2 + * @return bool + */ + public function stream_set_option($option, $arg1, $arg2); + + /** + * @param int $size + * @return bool + */ + public function stream_truncate($size); + + /** + * @return array|false + */ + public function stream_stat(); + + /** + * @param int $operation + * @return bool + */ + public function stream_lock($operation); + + /** + * @return bool + */ + public function stream_flush(); + + /** + * @return bool + */ + public function stream_eof(); + + /** + * @return bool + */ + public function stream_close(); +} diff --git a/3rdparty/icewind/streams/src/HashWrapper.php b/3rdparty/icewind/streams/src/HashWrapper.php new file mode 100644 index 00000000..7fb739e0 --- /dev/null +++ b/3rdparty/icewind/streams/src/HashWrapper.php @@ -0,0 +1,61 @@ + + * SPDX-License-Identifier: AGPL-3.0-or-later + */ + +namespace Icewind\Streams; + +abstract class HashWrapper extends Wrapper { + + /** + * @var callable|null + */ + private $callback; + + /** + * @var resource|\HashContext + */ + private $hashContext; + + /** + * Wraps a stream to make it seekable + * + * @param resource $source + * @param string $hash + * @param callable $callback + * @return resource|false + * + * @throws \BadMethodCallException + */ + public static function wrap($source, $hash, $callback) { + $context = [ + 'hash' => $hash, + 'callback' => $callback, + ]; + return self::wrapSource($source, $context); + } + + public function dir_opendir($path, $options) { + return false; + } + + public function stream_open($path, $mode, $options, &$opened_path) { + $context = $this->loadContext(); + $this->callback = $context['callback']; + $this->hashContext = hash_init($context['hash']); + return true; + } + + protected function updateHash($data) { + hash_update($this->hashContext, $data); + } + + public function stream_close() { + $hash = hash_final($this->hashContext); + if ($this->hashContext !== false && is_callable($this->callback)) { + call_user_func($this->callback, $hash); + } + return parent::stream_close(); + } +} diff --git a/3rdparty/icewind/streams/src/IteratorDirectory.php b/3rdparty/icewind/streams/src/IteratorDirectory.php new file mode 100644 index 00000000..24a4723d --- /dev/null +++ b/3rdparty/icewind/streams/src/IteratorDirectory.php @@ -0,0 +1,112 @@ + + * SPDX-License-Identifier: MIT + */ + +namespace Icewind\Streams; + +/** + * Create a directory handle from an iterator or array + * + * The following options should be passed in the context when opening the stream + * [ + * 'dir' => [ + * 'array' => string[] + * 'iterator' => \Iterator + * ] + * ] + * + * Either 'array' or 'iterator' need to be set, if both are set, 'iterator' takes preference + */ +class IteratorDirectory extends WrapperHandler implements Directory { + /** + * @var resource + */ + public $context; + + /** + * @var \Iterator + */ + protected $iterator; + + /** + * Load the source from the stream context and return the context options + * + * @param string $name + * @return array + * @throws \BadMethodCallException + */ + protected function loadContext($name = null) { + $context = parent::loadContext($name); + if (isset($context['iterator'])) { + $this->iterator = $context['iterator']; + } elseif (isset($context['array'])) { + $this->iterator = new \ArrayIterator($context['array']); + } else { + throw new \BadMethodCallException('Invalid context, iterator or array not set'); + } + return $context; + } + + /** + * @param string $path + * @param array $options + * @return bool + */ + public function dir_opendir($path, $options) { + $this->loadContext(); + return true; + } + + /** + * @return string|bool + */ + public function dir_readdir() { + if ($this->iterator->valid()) { + $result = $this->iterator->current(); + $this->iterator->next(); + return $result; + } else { + return false; + } + } + + /** + * @return bool + */ + public function dir_closedir() { + return true; + } + + /** + * @return bool + */ + public function dir_rewinddir() { + $this->iterator->rewind(); + return true; + } + + /** + * Creates a directory handle from the provided array or iterator + * + * @param \Iterator | array $source + * @return resource|false + * + * @throws \BadMethodCallException + */ + public static function wrap($source) { + if ($source instanceof \Iterator) { + $options = [ + 'iterator' => $source + ]; + } elseif (is_array($source)) { + $options = [ + 'array' => $source + ]; + } else { + throw new \BadMethodCallException('$source should be an Iterator or array'); + } + return self::wrapSource(self::NO_SOURCE_DIR, $options); + } +} diff --git a/3rdparty/icewind/streams/src/NullWrapper.php b/3rdparty/icewind/streams/src/NullWrapper.php new file mode 100644 index 00000000..f9227938 --- /dev/null +++ b/3rdparty/icewind/streams/src/NullWrapper.php @@ -0,0 +1,26 @@ + + * SPDX-License-Identifier: MIT + */ + +namespace Icewind\Streams; + +/** + * Stream wrapper that does nothing, used for tests + */ +class NullWrapper extends Wrapper { + public static function wrap($source) { + return self::wrapSource($source); + } + + public function stream_open($path, $mode, $options, &$opened_path) { + $this->loadContext(); + return true; + } + + public function dir_opendir($path, $options) { + $this->loadContext(); + return true; + } +} diff --git a/3rdparty/icewind/streams/src/Path.php b/3rdparty/icewind/streams/src/Path.php new file mode 100644 index 00000000..63fbd650 --- /dev/null +++ b/3rdparty/icewind/streams/src/Path.php @@ -0,0 +1,107 @@ + + * SPDX-License-Identifier: MIT + */ + +namespace Icewind\Streams; + +/** + * A string-like object that automatically registers a stream wrapper when used and removes the stream wrapper when no longer used + * + * Can optionally pass context options to the stream wrapper + */ +class Path { + + /** + * @var bool + */ + protected $registered = false; + + /** + * @var string + */ + protected $protocol; + + /** + * @var string + */ + protected $class; + + /** + * @var array + */ + protected $contextOptions; + + /** + * @param string $class + * @param array $contextOptions + */ + public function __construct($class, $contextOptions = []) { + $this->class = $class; + $this->contextOptions = $contextOptions; + } + + public function getProtocol() { + if (!$this->protocol) { + $this->protocol = 'auto' . uniqid(); + } + return $this->protocol; + } + + public function wrapPath($path) { + return $this->getProtocol() . '://' . $path; + } + + protected function register() { + if (!$this->registered) { + $this->appendDefaultContent($this->contextOptions); + stream_wrapper_register($this->getProtocol(), $this->class); + $this->registered = true; + } + } + + protected function unregister() { + stream_wrapper_unregister($this->getProtocol()); + $this->unsetDefaultContent($this->getProtocol()); + $this->registered = false; + } + + /** + * Add values to the default stream context + * + * @param array $values + */ + protected function appendDefaultContent($values) { + if (!is_array(current($values))) { + $values = [$this->getProtocol() => $values]; + } + $context = stream_context_get_default(); + $defaults = stream_context_get_options($context); + foreach ($values as $key => $value) { + $defaults[$key] = $value; + } + stream_context_set_default($defaults); + } + + /** + * Remove values from the default stream context + * + * @param string $key + */ + protected function unsetDefaultContent($key) { + $context = stream_context_get_default(); + $defaults = stream_context_get_options($context); + unset($defaults[$key]); + stream_context_set_default($defaults); + } + + public function __toString() { + $this->register(); + return $this->protocol . '://'; + } + + public function __destruct() { + $this->unregister(); + } +} diff --git a/3rdparty/icewind/streams/src/PathWrapper.php b/3rdparty/icewind/streams/src/PathWrapper.php new file mode 100644 index 00000000..3a6e2369 --- /dev/null +++ b/3rdparty/icewind/streams/src/PathWrapper.php @@ -0,0 +1,22 @@ + + * SPDX-License-Identifier: MIT + */ + +namespace Icewind\Streams; + +/** + * A string-like object that maps to an existing stream when opened + */ +class PathWrapper extends NullWrapper { + /** + * @param resource $source + * @return Path|string + */ + public static function getPath($source) { + return new Path(NullWrapper::class, [ + NullWrapper::getProtocol() => ['source' => $source] + ]); + } +} diff --git a/3rdparty/icewind/streams/src/ReadHashWrapper.php b/3rdparty/icewind/streams/src/ReadHashWrapper.php new file mode 100644 index 00000000..a9e9fc83 --- /dev/null +++ b/3rdparty/icewind/streams/src/ReadHashWrapper.php @@ -0,0 +1,23 @@ + + * SPDX-License-Identifier: AGPL-3.0-or-later + */ + +namespace Icewind\Streams; + +/** + * Wrapper that calculates the hash on the stream on read + * + * The stream and hash should be passed in when wrapping the stream. + * On close the callback will be called with the calculated checksum. + * + * For supported hashes see: http://php.net/manual/en/function.hash-algos.php + */ +class ReadHashWrapper extends HashWrapper { + public function stream_read($count) { + $data = parent::stream_read($count); + $this->updateHash($data); + return $data; + } +} diff --git a/3rdparty/icewind/streams/src/RetryWrapper.php b/3rdparty/icewind/streams/src/RetryWrapper.php new file mode 100644 index 00000000..2899a97a --- /dev/null +++ b/3rdparty/icewind/streams/src/RetryWrapper.php @@ -0,0 +1,51 @@ + + * SPDX-License-Identifier: MIT + */ + +namespace Icewind\Streams; + +/** + * Wrapper that retries reads/writes to remote streams that dont deliver/recieve all requested data at once + */ +class RetryWrapper extends Wrapper { + public static function wrap($source) { + return self::wrapSource($source); + } + + public function dir_opendir($path, $options) { + return false; + } + + public function stream_open($path, $mode, $options, &$opened_path) { + $this->loadContext(); + return true; + } + + public function stream_read($count) { + $result = parent::stream_read($count); + + $bytesReceived = strlen($result); + while (strlen($result) > 0 && $bytesReceived < $count && !$this->stream_eof()) { + $result .= parent::stream_read($count - $bytesReceived); + $bytesReceived = strlen($result); + } + + return $result; + } + + public function stream_write($data) { + $bytesToSend = strlen($data); + $bytesWritten = parent::stream_write($data); + $result = $bytesWritten; + + while ($bytesWritten > 0 && $result < $bytesToSend && !$this->stream_eof()) { + $dataLeft = substr($data, $result); + $bytesWritten = parent::stream_write($dataLeft); + $result += $bytesWritten; + } + + return $result; + } +} diff --git a/3rdparty/icewind/streams/src/SeekableWrapper.php b/3rdparty/icewind/streams/src/SeekableWrapper.php new file mode 100644 index 00000000..1eb06c62 --- /dev/null +++ b/3rdparty/icewind/streams/src/SeekableWrapper.php @@ -0,0 +1,82 @@ + + * SPDX-License-Identifier: MIT + */ + +namespace Icewind\Streams; + +/** + * Wrapper that provides callbacks for write, read and close + * + * The following options should be passed in the context when opening the stream + * [ + * 'callback' => [ + * 'source' => resource + * ] + * ] + * + * All callbacks are called after the operation is executed on the source stream + */ +class SeekableWrapper extends Wrapper { + /** + * @var resource + */ + protected $cache; + + public static function wrap($source) { + return self::wrapSource($source); + } + + public function dir_opendir($path, $options) { + return false; + } + + public function stream_open($path, $mode, $options, &$opened_path) { + $this->loadContext(); + $cache = fopen('php://temp', 'w+'); + if ($cache === false) { + return false; + } + $this->cache = $cache; + return true; + } + + protected function readTill($position) { + $current = ftell($this->source); + if ($position > $current) { + $data = parent::stream_read($position - $current); + $cachePosition = ftell($this->cache); + fseek($this->cache, $current); + fwrite($this->cache, $data); + fseek($this->cache, $cachePosition); + } + } + + public function stream_read($count) { + $current = ftell($this->cache); + $this->readTill($current + $count); + return fread($this->cache, $count); + } + + public function stream_seek($offset, $whence = SEEK_SET) { + if ($whence === SEEK_SET) { + $target = $offset; + } elseif ($whence === SEEK_CUR) { + $current = ftell($this->cache); + $target = $current + $offset; + } else { + return false; + } + $this->readTill($target); + return fseek($this->cache, $target) === 0; + } + + public function stream_tell() { + return ftell($this->cache); + } + + public function stream_eof() { + return parent::stream_eof() and (ftell($this->source) === ftell($this->cache)); + } +} diff --git a/3rdparty/icewind/streams/src/Url.php b/3rdparty/icewind/streams/src/Url.php new file mode 100644 index 00000000..dfe36a00 --- /dev/null +++ b/3rdparty/icewind/streams/src/Url.php @@ -0,0 +1,63 @@ + + * SPDX-License-Identifier: MIT + */ + +namespace Icewind\Streams; + +/** + * Interface for stream wrappers that implement url functions such as unlink, stat + */ +interface Url { + /** + * @param string $path + * @param array $options + * @return bool + */ + public function dir_opendir($path, $options); + + /** + * @param string $path + * @param string $mode + * @param int $options + * @param string $opened_path + * @return bool + */ + public function stream_open($path, $mode, $options, &$opened_path); + + /** + * @param string $path + * @param int $mode + * @param int $options + * @return bool + */ + public function mkdir($path, $mode, $options); + + /** + * @param string $source + * @param string $target + * @return bool + */ + public function rename($source, $target); + + /** + * @param string $path + * @param int $options + * @return bool + */ + public function rmdir($path, $options); + + /** + * @param string $path + * @return bool + */ + public function unlink($path); + + /** + * @param string $path + * @param int $flags + * @return array|false + */ + public function url_stat($path, $flags); +} diff --git a/3rdparty/icewind/streams/src/UrlCallback.php b/3rdparty/icewind/streams/src/UrlCallback.php new file mode 100644 index 00000000..4471c2d4 --- /dev/null +++ b/3rdparty/icewind/streams/src/UrlCallback.php @@ -0,0 +1,134 @@ + + * SPDX-License-Identifier: MIT + */ + +namespace Icewind\Streams; + +/** + * Wrapper that provides callbacks for url actions such as fopen, unlink, rename + * + * Usage: + * + * $path = UrlCallBack('/path/so/source', function(){ + * echo 'fopen'; + * }, function(){ + * echo 'opendir'; + * }, function(){ + * echo 'mkdir'; + * }, function(){ + * echo 'rename'; + * }, function(){ + * echo 'rmdir'; + * }, function(){ + * echo 'unlink'; + * }, function(){ + * echo 'stat'; + * }); + * + * mkdir($path); + * ... + * + * All callbacks are called after the operation is executed on the source stream + */ +class UrlCallback extends Wrapper implements Url { + + /** + * @param string $source + * @param callable $fopen + * @param callable $opendir + * @param callable $mkdir + * @param callable $rename + * @param callable $rmdir + * @param callable $unlink + * @param callable $stat + * @return \Icewind\Streams\Path + * + * @throws \BadMethodCallException + */ + public static function wrap( + $source, + $fopen = null, + $opendir = null, + $mkdir = null, + $rename = null, + $rmdir = null, + $unlink = null, + $stat = null + ) { + return new Path(static::class, [ + 'source' => $source, + 'fopen' => $fopen, + 'opendir' => $opendir, + 'mkdir' => $mkdir, + 'rename' => $rename, + 'rmdir' => $rmdir, + 'unlink' => $unlink, + 'stat' => $stat + ]); + } + + protected function loadUrlContext($url) { + list($protocol) = explode('://', $url); + $options = stream_context_get_options($this->context); + return $options[$protocol]; + } + + protected function callCallBack($context, $callback) { + if (is_callable($context[$callback])) { + call_user_func($context[$callback]); + } + } + + public function stream_open($path, $mode, $options, &$opened_path) { + $context = $this->loadUrlContext($path); + $this->callCallBack($context, 'fopen'); + $source = fopen($context['source'], $mode); + if ($source === false) { + return false; + } + $this->setSourceStream($source); + return true; + } + + public function dir_opendir($path, $options) { + $context = $this->loadUrlContext($path); + $this->callCallBack($context, 'opendir'); + $source = opendir($context['source']); + if ($source === false) { + return false; + } + $this->setSourceStream($source); + return true; + } + + public function mkdir($path, $mode, $options) { + $context = $this->loadUrlContext($path); + $this->callCallBack($context, 'mkdir'); + return mkdir($context['source'], $mode, ($options & STREAM_MKDIR_RECURSIVE) > 0); + } + + public function rmdir($path, $options) { + $context = $this->loadUrlContext($path); + $this->callCallBack($context, 'rmdir'); + return rmdir($context['source']); + } + + public function rename($source, $target) { + $context = $this->loadUrlContext($source); + $this->callCallBack($context, 'rename'); + list(, $target) = explode('://', $target); + return rename($context['source'], $target); + } + + public function unlink($path) { + $context = $this->loadUrlContext($path); + $this->callCallBack($context, 'unlink'); + return unlink($context['source']); + } + + public function url_stat($path, $flags) { + throw new \Exception('stat is not supported due to php bug 50526'); + } +} diff --git a/3rdparty/icewind/streams/src/Wrapper.php b/3rdparty/icewind/streams/src/Wrapper.php new file mode 100644 index 00000000..0a9c07be --- /dev/null +++ b/3rdparty/icewind/streams/src/Wrapper.php @@ -0,0 +1,130 @@ + + * SPDX-License-Identifier: MIT + */ + +namespace Icewind\Streams; + +/** + * Base class for stream wrappers, wraps an existing stream + * + * This wrapper itself doesn't implement any functionality but is just a base class for other wrappers to extend + */ +abstract class Wrapper extends WrapperHandler implements File, Directory { + /** + * @var resource + */ + public $context; + + /** + * The wrapped stream + * + * @var resource + */ + protected $source; + + /** + * @param resource $source + */ + protected function setSourceStream($source) { + $this->source = $source; + } + + protected function loadContext($name = null) { + $context = parent::loadContext($name); + if (isset($context['source']) and is_resource($context['source'])) { + $this->setSourceStream($context['source']); + } else { + throw new \BadMethodCallException('Invalid context, source not set'); + } + return $context; + } + + public function stream_seek($offset, $whence = SEEK_SET) { + $result = fseek($this->source, $offset, $whence); + return $result == 0; + } + + public function stream_tell() { + return ftell($this->source); + } + + public function stream_read($count) { + return fread($this->source, $count); + } + + public function stream_write($data) { + return fwrite($this->source, $data); + } + + public function stream_set_option($option, $arg1, $arg2) { + switch ($option) { + case STREAM_OPTION_BLOCKING: + return stream_set_blocking($this->source, (bool)$arg1); + case STREAM_OPTION_READ_TIMEOUT: + return stream_set_timeout($this->source, $arg1, $arg2); + case STREAM_OPTION_WRITE_BUFFER: + return stream_set_write_buffer($this->source, $arg1) === 0; + } + return false; + } + + public function stream_truncate($size) { + return ftruncate($this->source, $size); + } + + public function stream_stat() { + return fstat($this->source); + } + + public function stream_lock($mode) { + return flock($this->source, $mode); + } + + public function stream_flush() { + return fflush($this->source); + } + + public function stream_eof() { + return feof($this->source); + } + + public function stream_close() { + if (is_resource($this->source)) { + return fclose($this->source); + } + } + + public function dir_readdir() { + return readdir($this->source); + } + + public function dir_closedir() { + closedir($this->source); + return true; + } + + public function dir_rewinddir() { + return rewind($this->source); + } + + public function getSource() { + return $this->source; + } + + /** + * Retrieves header/metadata from the source stream. + * + * This is equivalent to calling `stream_get_meta_data` on the source stream except nested stream wrappers are handled transparently + * + * @return array + */ + public function getMetaData(): array { + $meta = stream_get_meta_data($this->source); + while (isset($meta['wrapper_data']) && $meta['wrapper_data'] instanceof Wrapper) { + $meta = $meta['wrapper_data']->getMetaData(); + } + return $meta; + } +} diff --git a/3rdparty/icewind/streams/src/WrapperHandler.php b/3rdparty/icewind/streams/src/WrapperHandler.php new file mode 100644 index 00000000..64b3be21 --- /dev/null +++ b/3rdparty/icewind/streams/src/WrapperHandler.php @@ -0,0 +1,99 @@ + + * SPDX-License-Identifier: AGPL-3.0-or-later + */ + +namespace Icewind\Streams; + +class WrapperHandler { + /** @var resource $context */ + protected $context; + + const NO_SOURCE_DIR = 1; + + /** + * get the protocol name that is generated for the class + * @param string|null $class + * @return string + */ + public static function getProtocol($class = null) { + if ($class === null) { + $class = static::class; + } + + $parts = explode('\\', $class); + return strtolower(array_pop($parts)); + } + + private static function buildContext($protocol, $context, $source) { + if (is_array($context)) { + $context['source'] = $source; + return stream_context_create([$protocol => $context]); + } else { + return $context; + } + } + + /** + * @param resource|int $source + * @param resource|array $context + * @param string|null $protocol deprecated, protocol is now automatically generated + * @param string|null $class deprecated, class is now automatically generated + * @return resource|false + */ + protected static function wrapSource($source, $context = [], $protocol = null, $class = null, $mode = 'r+') { + if ($class === null) { + $class = static::class; + } + + if ($protocol === null) { + $protocol = self::getProtocol($class); + } + + $context = self::buildContext($protocol, $context, $source); + try { + stream_wrapper_register($protocol, $class); + if (self::isDirectoryHandle($source)) { + return opendir($protocol . '://', $context); + } else { + return fopen($protocol . '://', $mode, false, $context); + } + } finally { + stream_wrapper_unregister($protocol); + } + } + + protected static function isDirectoryHandle($resource) { + if ($resource === self::NO_SOURCE_DIR) { + return true; + } + if (!is_resource($resource)) { + throw new \BadMethodCallException('Invalid stream source'); + } + $meta = stream_get_meta_data($resource); + return $meta['stream_type'] === 'dir' || $meta['stream_type'] === 'user-space-dir'; + } + + /** + * Load the source from the stream context and return the context options + * + * @param string|null $name if not set, the generated protocol name is used + * @return array + * @throws \BadMethodCallException + */ + protected function loadContext($name = null) { + if ($name === null) { + $parts = explode('\\', static::class); + $name = strtolower(array_pop($parts)); + } + + $context = stream_context_get_options($this->context); + if (isset($context[$name])) { + $context = $context[$name]; + } else { + throw new \BadMethodCallException('Invalid context, "' . $name . '" options not set'); + } + return $context; + } +} diff --git a/3rdparty/icewind/streams/src/WriteHashWrapper.php b/3rdparty/icewind/streams/src/WriteHashWrapper.php new file mode 100644 index 00000000..685d73c1 --- /dev/null +++ b/3rdparty/icewind/streams/src/WriteHashWrapper.php @@ -0,0 +1,22 @@ + + * SPDX-License-Identifier: AGPL-3.0-or-later + */ + +namespace Icewind\Streams; + +/** + * Wrapper that calculates the hash on the stream on write + * + * The stream and hash should be passed in when wrapping the stream. + * On close the callback will be called with the calculated checksum. + * + * For supported hashes see: http://php.net/manual/en/function.hash-algos.php + */ +class WriteHashWrapper extends HashWrapper { + public function stream_write($data) { + $this->updateHash($data); + return parent::stream_write($data); + } +} diff --git a/3rdparty/justinrainbow/json-schema/LICENSE b/3rdparty/justinrainbow/json-schema/LICENSE new file mode 100644 index 00000000..fa020fce --- /dev/null +++ b/3rdparty/justinrainbow/json-schema/LICENSE @@ -0,0 +1,21 @@ +MIT License + +Copyright (c) 2016 + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. diff --git a/3rdparty/justinrainbow/json-schema/src/JsonSchema/ConstraintError.php b/3rdparty/justinrainbow/json-schema/src/JsonSchema/ConstraintError.php new file mode 100644 index 00000000..c17cfeff --- /dev/null +++ b/3rdparty/justinrainbow/json-schema/src/JsonSchema/ConstraintError.php @@ -0,0 +1,117 @@ +getValue(); + static $messages = [ + self::ADDITIONAL_ITEMS => 'The item %s[%s] is not defined and the definition does not allow additional items', + self::ADDITIONAL_PROPERTIES => 'The property %s is not defined and the definition does not allow additional properties', + self::ALL_OF => 'Failed to match all schemas', + self::ANY_OF => 'Failed to match at least one schema', + self::DEPENDENCIES => '%s depends on %s, which is missing', + self::DISALLOW => 'Disallowed value was matched', + self::DIVISIBLE_BY => 'Is not divisible by %d', + self::ENUM => 'Does not have a value in the enumeration %s', + self::CONSTANT => 'Does not have a value equal to %s', + self::EXCLUSIVE_MINIMUM => 'Must have a minimum value greater than %d', + self::EXCLUSIVE_MAXIMUM => 'Must have a maximum value less than %d', + self::FORMAT_COLOR => 'Invalid color', + self::FORMAT_DATE => 'Invalid date %s, expected format YYYY-MM-DD', + self::FORMAT_DATE_TIME => 'Invalid date-time %s, expected format YYYY-MM-DDThh:mm:ssZ or YYYY-MM-DDThh:mm:ss+hh:mm', + self::FORMAT_DATE_UTC => 'Invalid time %s, expected integer of milliseconds since Epoch', + self::FORMAT_EMAIL => 'Invalid email', + self::FORMAT_HOSTNAME => 'Invalid hostname', + self::FORMAT_IP => 'Invalid IP address', + self::FORMAT_PHONE => 'Invalid phone number', + self::FORMAT_REGEX=> 'Invalid regex format %s', + self::FORMAT_STYLE => 'Invalid style', + self::FORMAT_TIME => 'Invalid time %s, expected format hh:mm:ss', + self::FORMAT_URL => 'Invalid URL format', + self::FORMAT_URL_REF => 'Invalid URL reference format', + self::LENGTH_MAX => 'Must be at most %d characters long', + self::INVALID_SCHEMA => 'Schema is not valid', + self::LENGTH_MIN => 'Must be at least %d characters long', + self::MAX_ITEMS => 'There must be a maximum of %d items in the array, %d found', + self::MAXIMUM => 'Must have a maximum value less than or equal to %d', + self::MIN_ITEMS => 'There must be a minimum of %d items in the array, %d found', + self::MINIMUM => 'Must have a minimum value greater than or equal to %d', + self::MISSING_MAXIMUM => 'Use of exclusiveMaximum requires presence of maximum', + self::MISSING_MINIMUM => 'Use of exclusiveMinimum requires presence of minimum', + /*self::MISSING_ERROR => 'Used for tests; this error is deliberately commented out',*/ + self::MULTIPLE_OF => 'Must be a multiple of %s', + self::NOT => 'Matched a schema which it should not', + self::ONE_OF => 'Failed to match exactly one schema', + self::REQUIRED => 'The property %s is required', + self::REQUIRES => 'The presence of the property %s requires that %s also be present', + self::PATTERN => 'Does not match the regex pattern %s', + self::PREGEX_INVALID => 'The pattern %s is invalid', + self::PROPERTIES_MIN => 'Must contain a minimum of %d properties', + self::PROPERTIES_MAX => 'Must contain no more than %d properties', + self::TYPE => '%s value found, but %s is required', + self::UNIQUE_ITEMS => 'There are no duplicates allowed in the array' + ]; + + if (!isset($messages[$name])) { + throw new InvalidArgumentException('Missing error message for ' . $name); + } + + return $messages[$name]; + } +} diff --git a/3rdparty/justinrainbow/json-schema/src/JsonSchema/Constraints/BaseConstraint.php b/3rdparty/justinrainbow/json-schema/src/JsonSchema/Constraints/BaseConstraint.php new file mode 100644 index 00000000..b1b34c32 --- /dev/null +++ b/3rdparty/justinrainbow/json-schema/src/JsonSchema/Constraints/BaseConstraint.php @@ -0,0 +1,170 @@ + + */ + protected $errorMask = Validator::ERROR_NONE; + + /** + * @var Factory + */ + protected $factory; + + public function __construct(?Factory $factory = null) + { + $this->factory = $factory ?: new Factory(); + } + + public function addError(ConstraintError $constraint, ?JsonPointer $path = null, array $more = []): void + { + $message = $constraint->getMessage(); + $name = $constraint->getValue(); + $error = [ + 'property' => $this->convertJsonPointerIntoPropertyPath($path ?: new JsonPointer('')), + 'pointer' => ltrim((string) ($path ?: new JsonPointer('')), '#'), + 'message' => ucfirst(vsprintf($message, array_map(static function ($val) { + if (is_scalar($val)) { + return is_bool($val) ? var_export($val, true) : $val; + } + + return json_encode($val); + }, array_values($more)))), + 'constraint' => [ + 'name' => $name, + 'params' => $more + ], + 'context' => $this->factory->getErrorContext(), + ]; + + if ($this->factory->getConfig(Constraint::CHECK_MODE_EXCEPTIONS)) { + throw new ValidationException(sprintf('Error validating %s: %s', $error['pointer'], $error['message'])); + } + + $this->errors[] = $error; + $this->errorMask |= $error['context']; + } + + public function addErrors(array $errors): void + { + if ($errors) { + $this->errors = array_merge($this->errors, $errors); + $errorMask = &$this->errorMask; + array_walk($errors, static function ($error) use (&$errorMask) { + if (isset($error['context'])) { + $errorMask |= $error['context']; + } + }); + } + } + + /** + * @phpstan-param int-mask-of $errorContext + */ + public function getErrors(int $errorContext = Validator::ERROR_ALL): array + { + if ($errorContext === Validator::ERROR_ALL) { + return $this->errors; + } + + return array_filter($this->errors, static function ($error) use ($errorContext) { + return (bool) ($errorContext & $error['context']); + }); + } + + /** + * @phpstan-param int-mask-of $errorContext + */ + public function numErrors(int $errorContext = Validator::ERROR_ALL): int + { + if ($errorContext === Validator::ERROR_ALL) { + return count($this->errors); + } + + return count($this->getErrors($errorContext)); + } + + public function isValid(): bool + { + return !$this->getErrors(); + } + + /** + * Clears any reported errors. Should be used between + * multiple validation checks. + */ + public function reset(): void + { + $this->errors = []; + $this->errorMask = Validator::ERROR_NONE; + } + + /** + * Get the error mask + * + * @phpstan-return int-mask-of + */ + public function getErrorMask(): int + { + return $this->errorMask; + } + + /** + * Recursively cast an associative array to an object + */ + public static function arrayToObjectRecursive(array $array): object + { + $json = json_encode($array); + if (json_last_error() !== JSON_ERROR_NONE) { + $message = 'Unable to encode schema array as JSON'; + if (function_exists('json_last_error_msg')) { + $message .= ': ' . json_last_error_msg(); + } + throw new InvalidArgumentException($message); + } + + return (object) json_decode($json, false); + } + + /** + * Transform a JSON pattern into a PCRE regex + */ + public static function jsonPatternToPhpRegex(string $pattern): string + { + return '~' . str_replace('~', '\\~', $pattern) . '~u'; + } + + protected function convertJsonPointerIntoPropertyPath(JsonPointer $pointer): string + { + $result = array_map( + static function ($path) { + return sprintf(is_numeric($path) ? '[%d]' : '.%s', $path); + }, + $pointer->getPropertyPaths() + ); + + return trim(implode('', $result), '.'); + } +} diff --git a/3rdparty/justinrainbow/json-schema/src/JsonSchema/Constraints/CollectionConstraint.php b/3rdparty/justinrainbow/json-schema/src/JsonSchema/Constraints/CollectionConstraint.php new file mode 100644 index 00000000..e42a6fb8 --- /dev/null +++ b/3rdparty/justinrainbow/json-schema/src/JsonSchema/Constraints/CollectionConstraint.php @@ -0,0 +1,136 @@ + + * @author Bruno Prieto Reis + */ +class CollectionConstraint extends Constraint +{ + /** + * {@inheritdoc} + */ + public function check(&$value, $schema = null, ?JsonPointer $path = null, $i = null): void + { + // Verify minItems + if (isset($schema->minItems) && count($value) < $schema->minItems) { + $this->addError(ConstraintError::MIN_ITEMS(), $path, ['minItems' => $schema->minItems, 'found' => count($value)]); + } + + // Verify maxItems + if (isset($schema->maxItems) && count($value) > $schema->maxItems) { + $this->addError(ConstraintError::MAX_ITEMS(), $path, ['maxItems' => $schema->maxItems, 'found' => count($value)]); + } + + // Verify uniqueItems + if (isset($schema->uniqueItems) && $schema->uniqueItems) { + $count = count($value); + for ($x = 0; $x < $count - 1; $x++) { + for ($y = $x + 1; $y < $count; $y++) { + if (DeepComparer::isEqual($value[$x], $value[$y])) { + $this->addError(ConstraintError::UNIQUE_ITEMS(), $path); + break 2; + } + } + } + } + + $this->validateItems($value, $schema, $path, $i); + } + + /** + * Validates the items + * + * @param array $value + * @param \stdClass $schema + * @param string $i + */ + protected function validateItems(&$value, $schema = null, ?JsonPointer $path = null, $i = null): void + { + if (\is_null($schema) || !isset($schema->items)) { + return; + } + + if ($schema->items === true) { + return; + } + + if (is_object($schema->items)) { + // just one type definition for the whole array + foreach ($value as $k => &$v) { + $initErrors = $this->getErrors(); + + // First check if its defined in "items" + $this->checkUndefined($v, $schema->items, $path, $k); + + // Recheck with "additionalItems" if the first test fails + if (count($initErrors) < count($this->getErrors()) && (isset($schema->additionalItems) && $schema->additionalItems !== false)) { + $secondErrors = $this->getErrors(); + $this->checkUndefined($v, $schema->additionalItems, $path, $k); + } + + // Reset errors if needed + if (isset($secondErrors) && count($secondErrors) < count($this->getErrors())) { + $this->errors = $secondErrors; + } elseif (isset($secondErrors) && count($secondErrors) === count($this->getErrors())) { + $this->errors = $initErrors; + } + } + unset($v); /* remove dangling reference to prevent any future bugs + * caused by accidentally using $v elsewhere */ + } else { + // Defined item type definitions + foreach ($value as $k => &$v) { + if (array_key_exists($k, $schema->items)) { + $this->checkUndefined($v, $schema->items[$k], $path, $k); + } else { + // Additional items + if (property_exists($schema, 'additionalItems')) { + if ($schema->additionalItems !== false) { + $this->checkUndefined($v, $schema->additionalItems, $path, $k); + } else { + $this->addError( + ConstraintError::ADDITIONAL_ITEMS(), + $path, + [ + 'item' => $i, + 'property' => $k, + 'additionalItems' => $schema->additionalItems + ] + ); + } + } else { + // Should be valid against an empty schema + $this->checkUndefined($v, new \stdClass(), $path, $k); + } + } + } + unset($v); /* remove dangling reference to prevent any future bugs + * caused by accidentally using $v elsewhere */ + + // Treat when we have more schema definitions than values, not for empty arrays + if (count($value) > 0) { + for ($k = count($value); $k < count($schema->items); $k++) { + $undefinedInstance = $this->factory->createInstanceFor('undefined'); + $this->checkUndefined($undefinedInstance, $schema->items[$k], $path, $k); + } + } + } + } +} diff --git a/3rdparty/justinrainbow/json-schema/src/JsonSchema/Constraints/ConstConstraint.php b/3rdparty/justinrainbow/json-schema/src/JsonSchema/Constraints/ConstConstraint.php new file mode 100644 index 00000000..b9bb1f48 --- /dev/null +++ b/3rdparty/justinrainbow/json-schema/src/JsonSchema/Constraints/ConstConstraint.php @@ -0,0 +1,51 @@ + + */ +class ConstConstraint extends Constraint +{ + /** + * {@inheritdoc} + */ + public function check(&$element, $schema = null, ?JsonPointer $path = null, $i = null): void + { + // Only validate const if the attribute exists + if ($element instanceof UndefinedConstraint && (!isset($schema->required) || !$schema->required)) { + return; + } + $const = $schema->const; + + $type = gettype($element); + $constType = gettype($const); + + if ($this->factory->getConfig(self::CHECK_MODE_TYPE_CAST) && $type === 'array' && $constType === 'object') { + if (DeepComparer::isEqual((object) $element, $const)) { + return; + } + } + + if (DeepComparer::isEqual($element, $const)) { + return; + } + + $this->addError(ConstraintError::CONSTANT(), $path, ['const' => $schema->const]); + } +} diff --git a/3rdparty/justinrainbow/json-schema/src/JsonSchema/Constraints/Constraint.php b/3rdparty/justinrainbow/json-schema/src/JsonSchema/Constraints/Constraint.php new file mode 100644 index 00000000..8e818f0a --- /dev/null +++ b/3rdparty/justinrainbow/json-schema/src/JsonSchema/Constraints/Constraint.php @@ -0,0 +1,202 @@ +withPropertyPaths( + array_merge( + $path->getPropertyPaths(), + [$i] + ) + ); + } + + /** + * Validates an array + * + * @param mixed $value + * @param mixed $schema + * @param mixed $i + */ + protected function checkArray(&$value, $schema = null, ?JsonPointer $path = null, $i = null): void + { + $validator = $this->factory->createInstanceFor('collection'); + $validator->check($value, $schema, $path, $i); + + $this->addErrors($validator->getErrors()); + } + + /** + * Validates an object + * + * @param mixed $value + * @param mixed $schema + * @param mixed $properties + * @param mixed $additionalProperties + * @param mixed $patternProperties + * @param array $appliedDefaults + */ + protected function checkObject( + &$value, + $schema = null, + ?JsonPointer $path = null, + $properties = null, + $additionalProperties = null, + $patternProperties = null, + array $appliedDefaults = [] + ): void { + /** @var ObjectConstraint $validator */ + $validator = $this->factory->createInstanceFor('object'); + $validator->check($value, $schema, $path, $properties, $additionalProperties, $patternProperties, $appliedDefaults); + + $this->addErrors($validator->getErrors()); + } + + /** + * Validates the type of the value + * + * @param mixed $value + * @param mixed $schema + * @param mixed $i + */ + protected function checkType(&$value, $schema = null, ?JsonPointer $path = null, $i = null): void + { + $validator = $this->factory->createInstanceFor('type'); + $validator->check($value, $schema, $path, $i); + + $this->addErrors($validator->getErrors()); + } + + /** + * Checks a undefined element + * + * @param mixed $value + * @param mixed $schema + * @param mixed $i + */ + protected function checkUndefined(&$value, $schema = null, ?JsonPointer $path = null, $i = null, bool $fromDefault = false): void + { + /** @var UndefinedConstraint $validator */ + $validator = $this->factory->createInstanceFor('undefined'); + + $validator->check($value, $this->factory->getSchemaStorage()->resolveRefSchema($schema), $path, $i, $fromDefault); + + $this->addErrors($validator->getErrors()); + } + + /** + * Checks a string element + * + * @param mixed $value + * @param mixed $schema + * @param mixed $i + */ + protected function checkString($value, $schema = null, ?JsonPointer $path = null, $i = null): void + { + $validator = $this->factory->createInstanceFor('string'); + $validator->check($value, $schema, $path, $i); + + $this->addErrors($validator->getErrors()); + } + + /** + * Checks a number element + * + * @param mixed $value + * @param mixed $schema + * @param mixed $i + */ + protected function checkNumber($value, $schema = null, ?JsonPointer $path = null, $i = null): void + { + $validator = $this->factory->createInstanceFor('number'); + $validator->check($value, $schema, $path, $i); + + $this->addErrors($validator->getErrors()); + } + + /** + * Checks a enum element + * + * @param mixed $value + * @param mixed $schema + * @param mixed $i + */ + protected function checkEnum($value, $schema = null, ?JsonPointer $path = null, $i = null): void + { + $validator = $this->factory->createInstanceFor('enum'); + $validator->check($value, $schema, $path, $i); + + $this->addErrors($validator->getErrors()); + } + + /** + * Checks a const element + * + * @param mixed $value + * @param mixed $schema + * @param mixed $i + */ + protected function checkConst($value, $schema = null, ?JsonPointer $path = null, $i = null): void + { + $validator = $this->factory->createInstanceFor('const'); + $validator->check($value, $schema, $path, $i); + + $this->addErrors($validator->getErrors()); + } + + /** + * Checks format of an element + * + * @param mixed $value + * @param mixed $schema + * @param mixed $i + */ + protected function checkFormat($value, $schema = null, ?JsonPointer $path = null, $i = null): void + { + $validator = $this->factory->createInstanceFor('format'); + $validator->check($value, $schema, $path, $i); + + $this->addErrors($validator->getErrors()); + } + + /** + * Get the type check based on the set check mode. + */ + protected function getTypeCheck(): TypeCheck\TypeCheckInterface + { + return $this->factory->getTypeCheck(); + } +} diff --git a/3rdparty/justinrainbow/json-schema/src/JsonSchema/Constraints/ConstraintInterface.php b/3rdparty/justinrainbow/json-schema/src/JsonSchema/Constraints/ConstraintInterface.php new file mode 100644 index 00000000..9e43ecb5 --- /dev/null +++ b/3rdparty/justinrainbow/json-schema/src/JsonSchema/Constraints/ConstraintInterface.php @@ -0,0 +1,59 @@ + + */ +interface ConstraintInterface +{ + /** + * returns all collected errors + */ + public function getErrors(): array; + + /** + * adds errors to this validator + */ + public function addErrors(array $errors): void; + + /** + * adds an error + * + * @param ConstraintError $constraint the constraint/rule that is broken, e.g.: ConstraintErrors::LENGTH_MIN() + * @param array $more more array elements to add to the error + */ + public function addError(ConstraintError $constraint, ?JsonPointer $path = null, array $more = []): void; + + /** + * checks if the validator has not raised errors + */ + public function isValid(): bool; + + /** + * invokes the validation of an element + * + * @abstract + * + * @param mixed $value + * @param mixed $schema + * @param mixed $i + * + * @throws \JsonSchema\Exception\ExceptionInterface + */ + public function check(&$value, $schema = null, ?JsonPointer $path = null, $i = null): void; +} diff --git a/3rdparty/justinrainbow/json-schema/src/JsonSchema/Constraints/EnumConstraint.php b/3rdparty/justinrainbow/json-schema/src/JsonSchema/Constraints/EnumConstraint.php new file mode 100644 index 00000000..21096385 --- /dev/null +++ b/3rdparty/justinrainbow/json-schema/src/JsonSchema/Constraints/EnumConstraint.php @@ -0,0 +1,59 @@ + + * @author Bruno Prieto Reis + */ +class EnumConstraint extends Constraint +{ + /** + * {@inheritdoc} + */ + public function check(&$element, $schema = null, ?JsonPointer $path = null, $i = null): void + { + // Only validate enum if the attribute exists + if ($element instanceof UndefinedConstraint && (!isset($schema->required) || !$schema->required)) { + return; + } + $type = gettype($element); + + foreach ($schema->enum as $enum) { + $enumType = gettype($enum); + + if ($enumType === 'object' + && $type === 'array' + && $this->factory->getConfig(self::CHECK_MODE_TYPE_CAST) + && DeepComparer::isEqual((object) $element, $enum) + ) { + return; + } + + if (($type === $enumType) && DeepComparer::isEqual($element, $enum)) { + return; + } + + if (is_numeric($element) && is_numeric($enum) && DeepComparer::isEqual((float) $element, (float) $enum)) { + return; + } + } + + $this->addError(ConstraintError::ENUM(), $path, ['enum' => $schema->enum]); + } +} diff --git a/3rdparty/justinrainbow/json-schema/src/JsonSchema/Constraints/Factory.php b/3rdparty/justinrainbow/json-schema/src/JsonSchema/Constraints/Factory.php new file mode 100644 index 00000000..b9220b9d --- /dev/null +++ b/3rdparty/justinrainbow/json-schema/src/JsonSchema/Constraints/Factory.php @@ -0,0 +1,217 @@ + + */ + private $checkMode = Constraint::CHECK_MODE_NORMAL; + + /** + * @var array + * @phpstan-var array, TypeCheck\TypeCheckInterface> + */ + private $typeCheck = []; + + /** + * @var int Validation context + */ + protected $errorContext = Validator::ERROR_DOCUMENT_VALIDATION; + + /** + * @var array + */ + protected $constraintMap = [ + 'array' => 'JsonSchema\Constraints\CollectionConstraint', + 'collection' => 'JsonSchema\Constraints\CollectionConstraint', + 'object' => 'JsonSchema\Constraints\ObjectConstraint', + 'type' => 'JsonSchema\Constraints\TypeConstraint', + 'undefined' => 'JsonSchema\Constraints\UndefinedConstraint', + 'string' => 'JsonSchema\Constraints\StringConstraint', + 'number' => 'JsonSchema\Constraints\NumberConstraint', + 'enum' => 'JsonSchema\Constraints\EnumConstraint', + 'const' => 'JsonSchema\Constraints\ConstConstraint', + 'format' => 'JsonSchema\Constraints\FormatConstraint', + 'schema' => 'JsonSchema\Constraints\SchemaConstraint', + 'validator' => 'JsonSchema\Validator' + ]; + + /** + * @var array + */ + private $instanceCache = []; + + /** + * @phpstan-param int-mask-of $checkMode + */ + public function __construct( + ?SchemaStorageInterface $schemaStorage = null, + ?UriRetrieverInterface $uriRetriever = null, + int $checkMode = Constraint::CHECK_MODE_NORMAL + ) { + // set provided config options + $this->setConfig($checkMode); + + $this->uriRetriever = $uriRetriever ?: new UriRetriever(); + $this->schemaStorage = $schemaStorage ?: new SchemaStorage($this->uriRetriever); + } + + /** + * Set config values + * + * @param int $checkMode Set checkMode options - does not preserve existing flags + * @phpstan-param int-mask-of $checkMode + */ + public function setConfig(int $checkMode = Constraint::CHECK_MODE_NORMAL): void + { + $this->checkMode = $checkMode; + } + + /** + * Enable checkMode flags + * + * @phpstan-param int-mask-of $options + */ + public function addConfig(int $options): void + { + $this->checkMode |= $options; + } + + /** + * Disable checkMode flags + * + * @phpstan-param int-mask-of $options + */ + public function removeConfig(int $options): void + { + $this->checkMode &= ~$options; + } + + /** + * Get checkMode option + * + * @param int|null $options Options to get, if null then return entire bitmask + * @phpstan-param int-mask-of|null $options Options to get, if null then return entire bitmask + * + * @phpstan-return int-mask-of + */ + public function getConfig(?int $options = null): int + { + if ($options === null) { + return $this->checkMode; + } + + return $this->checkMode & $options; + } + + public function getUriRetriever(): UriRetrieverInterface + { + return $this->uriRetriever; + } + + public function getSchemaStorage(): SchemaStorageInterface + { + return $this->schemaStorage; + } + + public function getTypeCheck(): TypeCheck\TypeCheckInterface + { + if (!isset($this->typeCheck[$this->checkMode])) { + $this->typeCheck[$this->checkMode] = ($this->checkMode & Constraint::CHECK_MODE_TYPE_CAST) + ? new TypeCheck\LooseTypeCheck() + : new TypeCheck\StrictTypeCheck(); + } + + return $this->typeCheck[$this->checkMode]; + } + + public function setConstraintClass(string $name, string $class): Factory + { + // Ensure class exists + if (!class_exists($class)) { + throw new InvalidArgumentException('Unknown constraint ' . $name); + } + // Ensure class is appropriate + if (!in_array('JsonSchema\Constraints\ConstraintInterface', class_implements($class))) { + throw new InvalidArgumentException('Invalid class ' . $name); + } + $this->constraintMap[$name] = $class; + + return $this; + } + + /** + * Create a constraint instance for the given constraint name. + * + * @param string $constraintName + * + * @throws InvalidArgumentException if is not possible create the constraint instance + * + * @return ConstraintInterface&BaseConstraint + * @phpstan-return ConstraintInterface&BaseConstraint + */ + public function createInstanceFor($constraintName) + { + if (!isset($this->constraintMap[$constraintName])) { + throw new InvalidArgumentException('Unknown constraint ' . $constraintName); + } + + if (!isset($this->instanceCache[$constraintName])) { + $this->instanceCache[$constraintName] = new $this->constraintMap[$constraintName]($this); + } + + return clone $this->instanceCache[$constraintName]; + } + + /** + * Get the error context + * + * @phpstan-return Validator::ERROR_DOCUMENT_VALIDATION|Validator::ERROR_SCHEMA_VALIDATION + */ + public function getErrorContext(): int + { + return $this->errorContext; + } + + /** + * Set the error context + * + * @phpstan-param Validator::ERROR_DOCUMENT_VALIDATION|Validator::ERROR_SCHEMA_VALIDATION $errorContext + */ + public function setErrorContext(int $errorContext): void + { + $this->errorContext = $errorContext; + } +} diff --git a/3rdparty/justinrainbow/json-schema/src/JsonSchema/Constraints/FormatConstraint.php b/3rdparty/justinrainbow/json-schema/src/JsonSchema/Constraints/FormatConstraint.php new file mode 100644 index 00000000..9ed4df9d --- /dev/null +++ b/3rdparty/justinrainbow/json-schema/src/JsonSchema/Constraints/FormatConstraint.php @@ -0,0 +1,217 @@ + + * + * @see http://tools.ietf.org/html/draft-zyp-json-schema-03#section-5.23 + */ +class FormatConstraint extends Constraint +{ + /** + * {@inheritdoc} + */ + public function check(&$element, $schema = null, ?JsonPointer $path = null, $i = null): void + { + if (!isset($schema->format) || $this->factory->getConfig(self::CHECK_MODE_DISABLE_FORMAT)) { + return; + } + + switch ($schema->format) { + case 'date': + if (is_string($element) && !$date = $this->validateDateTime($element, 'Y-m-d')) { + $this->addError(ConstraintError::FORMAT_DATE(), $path, [ + 'date' => $element, + 'format' => $schema->format + ] + ); + } + break; + + case 'time': + if (is_string($element) && !$this->validateDateTime($element, 'H:i:s')) { + $this->addError(ConstraintError::FORMAT_TIME(), $path, [ + 'time' => json_encode($element), + 'format' => $schema->format, + ] + ); + } + break; + + case 'date-time': + if (is_string($element) && null === Rfc3339::createFromString($element)) { + $this->addError(ConstraintError::FORMAT_DATE_TIME(), $path, [ + 'dateTime' => json_encode($element), + 'format' => $schema->format + ] + ); + } + break; + + case 'utc-millisec': + if (!$this->validateDateTime($element, 'U')) { + $this->addError(ConstraintError::FORMAT_DATE_UTC(), $path, [ + 'value' => $element, + 'format' => $schema->format]); + } + break; + + case 'regex': + if (!$this->validateRegex($element)) { + $this->addError(ConstraintError::FORMAT_REGEX(), $path, [ + 'value' => $element, + 'format' => $schema->format + ] + ); + } + break; + + case 'color': + if (!$this->validateColor($element)) { + $this->addError(ConstraintError::FORMAT_COLOR(), $path, ['format' => $schema->format]); + } + break; + + case 'style': + if (!$this->validateStyle($element)) { + $this->addError(ConstraintError::FORMAT_STYLE(), $path, ['format' => $schema->format]); + } + break; + + case 'phone': + if (!$this->validatePhone($element)) { + $this->addError(ConstraintError::FORMAT_PHONE(), $path, ['format' => $schema->format]); + } + break; + + case 'uri': + if (is_string($element) && !UriValidator::isValid($element)) { + $this->addError(ConstraintError::FORMAT_URL(), $path, ['format' => $schema->format]); + } + break; + + case 'uriref': + case 'uri-reference': + if (is_string($element) && !(UriValidator::isValid($element) || RelativeReferenceValidator::isValid($element))) { + $this->addError(ConstraintError::FORMAT_URL(), $path, ['format' => $schema->format]); + } + break; + + case 'email': + if (is_string($element) && null === filter_var($element, FILTER_VALIDATE_EMAIL, FILTER_NULL_ON_FAILURE | FILTER_FLAG_EMAIL_UNICODE)) { + $this->addError(ConstraintError::FORMAT_EMAIL(), $path, ['format' => $schema->format]); + } + break; + + case 'ip-address': + case 'ipv4': + if (is_string($element) && null === filter_var($element, FILTER_VALIDATE_IP, FILTER_NULL_ON_FAILURE | FILTER_FLAG_IPV4)) { + $this->addError(ConstraintError::FORMAT_IP(), $path, ['format' => $schema->format]); + } + break; + + case 'ipv6': + if (is_string($element) && null === filter_var($element, FILTER_VALIDATE_IP, FILTER_NULL_ON_FAILURE | FILTER_FLAG_IPV6)) { + $this->addError(ConstraintError::FORMAT_IP(), $path, ['format' => $schema->format]); + } + break; + + case 'host-name': + case 'hostname': + if (!$this->validateHostname($element)) { + $this->addError(ConstraintError::FORMAT_HOSTNAME(), $path, ['format' => $schema->format]); + } + break; + + default: + // Empty as it should be: + // The value of this keyword is called a format attribute. It MUST be a string. + // A format attribute can generally only validate a given set of instance types. + // If the type of the instance to validate is not in this set, validation for + // this format attribute and instance SHOULD succeed. + // http://json-schema.org/latest/json-schema-validation.html#anchor105 + break; + } + } + + protected function validateDateTime($datetime, $format) + { + $dt = \DateTime::createFromFormat($format, (string) $datetime); + + if (!$dt) { + return false; + } + + if ($datetime === $dt->format($format)) { + return true; + } + + return false; + } + + protected function validateRegex($regex) + { + if (!is_string($regex)) { + return true; + } + + return false !== @preg_match(self::jsonPatternToPhpRegex($regex), ''); + } + + protected function validateColor($color) + { + if (!is_string($color)) { + return true; + } + + if (in_array(strtolower($color), ['aqua', 'black', 'blue', 'fuchsia', + 'gray', 'green', 'lime', 'maroon', 'navy', 'olive', 'orange', 'purple', + 'red', 'silver', 'teal', 'white', 'yellow'])) { + return true; + } + + return preg_match('/^#([a-f0-9]{3}|[a-f0-9]{6})$/i', $color); + } + + protected function validateStyle($style) + { + $properties = explode(';', rtrim($style, ';')); + $invalidEntries = preg_grep('/^\s*[-a-z]+\s*:\s*.+$/i', $properties, PREG_GREP_INVERT); + + return empty($invalidEntries); + } + + protected function validatePhone($phone) + { + return preg_match('/^\+?(\(\d{3}\)|\d{3}) \d{3} \d{4}$/', $phone); + } + + protected function validateHostname($host) + { + if (!is_string($host)) { + return true; + } + + $hostnameRegex = '/^(([a-zA-Z0-9]|[a-zA-Z0-9][a-zA-Z0-9\-]*[a-zA-Z0-9])\.)*([A-Za-z0-9]|[A-Za-z0-9][A-Za-z0-9\-]*[A-Za-z0-9])$/i'; + + return preg_match($hostnameRegex, $host); + } +} diff --git a/3rdparty/justinrainbow/json-schema/src/JsonSchema/Constraints/NumberConstraint.php b/3rdparty/justinrainbow/json-schema/src/JsonSchema/Constraints/NumberConstraint.php new file mode 100644 index 00000000..e1b2ffc3 --- /dev/null +++ b/3rdparty/justinrainbow/json-schema/src/JsonSchema/Constraints/NumberConstraint.php @@ -0,0 +1,84 @@ + + * @author Bruno Prieto Reis + */ +class NumberConstraint extends Constraint +{ + /** + * {@inheritdoc} + */ + public function check(&$element, $schema = null, ?JsonPointer $path = null, $i = null): void + { + // Verify minimum + if (isset($schema->exclusiveMinimum)) { + if (isset($schema->minimum)) { + if ($schema->exclusiveMinimum && $element <= $schema->minimum) { + $this->addError(ConstraintError::EXCLUSIVE_MINIMUM(), $path, ['minimum' => $schema->minimum]); + } elseif ($element < $schema->minimum) { + $this->addError(ConstraintError::MINIMUM(), $path, ['minimum' => $schema->minimum]); + } + } else { + $this->addError(ConstraintError::MISSING_MINIMUM(), $path); + } + } elseif (isset($schema->minimum) && $element < $schema->minimum) { + $this->addError(ConstraintError::MINIMUM(), $path, ['minimum' => $schema->minimum]); + } + + // Verify maximum + if (isset($schema->exclusiveMaximum)) { + if (isset($schema->maximum)) { + if ($schema->exclusiveMaximum && $element >= $schema->maximum) { + $this->addError(ConstraintError::EXCLUSIVE_MAXIMUM(), $path, ['maximum' => $schema->maximum]); + } elseif ($element > $schema->maximum) { + $this->addError(ConstraintError::MAXIMUM(), $path, ['maximum' => $schema->maximum]); + } + } else { + $this->addError(ConstraintError::MISSING_MAXIMUM(), $path); + } + } elseif (isset($schema->maximum) && $element > $schema->maximum) { + $this->addError(ConstraintError::MAXIMUM(), $path, ['maximum' => $schema->maximum]); + } + + // Verify divisibleBy - Draft v3 + if (isset($schema->divisibleBy) && $this->fmod($element, $schema->divisibleBy) != 0) { + $this->addError(ConstraintError::DIVISIBLE_BY(), $path, ['divisibleBy' => $schema->divisibleBy]); + } + + // Verify multipleOf - Draft v4 + if (isset($schema->multipleOf) && $this->fmod($element, $schema->multipleOf) != 0) { + $this->addError(ConstraintError::MULTIPLE_OF(), $path, ['multipleOf' => $schema->multipleOf]); + } + + $this->checkFormat($element, $schema, $path, $i); + } + + private function fmod($number1, $number2) + { + $modulus = ($number1 - round($number1 / $number2) * $number2); + $precision = 0.0000000001; + + if (-$precision < $modulus && $modulus < $precision) { + return 0.0; + } + + return $modulus; + } +} diff --git a/3rdparty/justinrainbow/json-schema/src/JsonSchema/Constraints/ObjectConstraint.php b/3rdparty/justinrainbow/json-schema/src/JsonSchema/Constraints/ObjectConstraint.php new file mode 100644 index 00000000..c2c614a9 --- /dev/null +++ b/3rdparty/justinrainbow/json-schema/src/JsonSchema/Constraints/ObjectConstraint.php @@ -0,0 +1,190 @@ + List of properties to which a default value has been applied + */ + protected $appliedDefaults = []; + + /** + * {@inheritdoc} + * + * @param list $appliedDefaults + */ + public function check( + &$element, + $schema = null, + ?JsonPointer $path = null, + $properties = null, + $additionalProp = null, + $patternProperties = null, + $appliedDefaults = [] + ): void { + if ($element instanceof UndefinedConstraint) { + return; + } + + $this->appliedDefaults = $appliedDefaults; + + $matches = []; + if ($patternProperties) { + // validate the element pattern properties + $matches = $this->validatePatternProperties($element, $path, $patternProperties); + } + + if ($properties) { + // validate the element properties + $this->validateProperties($element, $properties, $path); + } + + // validate additional element properties & constraints + $this->validateElement($element, $matches, $schema, $path, $properties, $additionalProp); + } + + public function validatePatternProperties($element, ?JsonPointer $path, $patternProperties) + { + $matches = []; + foreach ($patternProperties as $pregex => $schema) { + $fullRegex = self::jsonPatternToPhpRegex($pregex); + + // Validate the pattern before using it to test for matches + if (@preg_match($fullRegex, '') === false) { + $this->addError(ConstraintError::PREGEX_INVALID(), $path, ['pregex' => $pregex]); + continue; + } + foreach ($element as $i => $value) { + if (preg_match($fullRegex, $i)) { + $matches[] = $i; + $this->checkUndefined($value, $schema ?: new \stdClass(), $path, $i, in_array($i, $this->appliedDefaults)); + } + } + } + + return $matches; + } + + /** + * Validates the element properties + * + * @param \StdClass $element Element to validate + * @param array $matches Matches from patternProperties (if any) + * @param \StdClass $schema ObjectConstraint definition + * @param JsonPointer|null $path Current test path + * @param \StdClass $properties Properties + * @param mixed $additionalProp Additional properties + */ + public function validateElement($element, $matches, $schema = null, ?JsonPointer $path = null, + $properties = null, $additionalProp = null) + { + $this->validateMinMaxConstraint($element, $schema, $path); + + foreach ($element as $i => $value) { + $definition = $this->getProperty($properties, $i); + + // no additional properties allowed + if (!in_array($i, $matches) && $additionalProp === false && $this->inlineSchemaProperty !== $i && !$definition) { + $this->addError(ConstraintError::ADDITIONAL_PROPERTIES(), $path, ['property' => $i]); + } + + // additional properties defined + if (!in_array($i, $matches) && $additionalProp && !$definition) { + if ($additionalProp === true) { + $this->checkUndefined($value, null, $path, $i, in_array($i, $this->appliedDefaults)); + } else { + $this->checkUndefined($value, $additionalProp, $path, $i, in_array($i, $this->appliedDefaults)); + } + } + + // property requires presence of another + $require = $this->getProperty($definition, 'requires'); + if ($require && !$this->getProperty($element, $require)) { + $this->addError(ConstraintError::REQUIRES(), $path, [ + 'property' => $i, + 'requiredProperty' => $require + ]); + } + + $property = $this->getProperty($element, $i, $this->factory->createInstanceFor('undefined')); + if (is_object($property)) { + $this->validateMinMaxConstraint(!($property instanceof UndefinedConstraint) ? $property : $element, $definition, $path); + } + } + } + + /** + * Validates the definition properties + * + * @param \stdClass $element Element to validate + * @param \stdClass $properties Property definitions + * @param JsonPointer|null $path Path? + */ + public function validateProperties(&$element, $properties = null, ?JsonPointer $path = null) + { + $undefinedConstraint = $this->factory->createInstanceFor('undefined'); + + foreach ($properties as $i => $value) { + $property = &$this->getProperty($element, $i, $undefinedConstraint); + $definition = $this->getProperty($properties, $i); + + if (is_object($definition)) { + // Undefined constraint will check for is_object() and quit if is not - so why pass it? + $this->checkUndefined($property, $definition, $path, $i, in_array($i, $this->appliedDefaults)); + } + } + } + + /** + * retrieves a property from an object or array + * + * @param mixed $element Element to validate + * @param string $property Property to retrieve + * @param mixed $fallback Default value if property is not found + * + * @return mixed + */ + protected function &getProperty(&$element, $property, $fallback = null) + { + if (is_array($element) && (isset($element[$property]) || array_key_exists($property, $element)) /*$this->checkMode == self::CHECK_MODE_TYPE_CAST*/) { + return $element[$property]; + } elseif (is_object($element) && property_exists($element, (string) $property)) { + return $element->$property; + } + + return $fallback; + } + + /** + * validating minimum and maximum property constraints (if present) against an element + * + * @param \stdClass $element Element to validate + * @param \stdClass $objectDefinition ObjectConstraint definition + * @param JsonPointer|null $path Path to test? + */ + protected function validateMinMaxConstraint($element, $objectDefinition, ?JsonPointer $path = null) + { + if (!$this->getTypeCheck()::isObject($element)) { + return; + } + + // Verify minimum number of properties + if (isset($objectDefinition->minProperties) && is_int($objectDefinition->minProperties)) { + if ($this->getTypeCheck()->propertyCount($element) < max(0, $objectDefinition->minProperties)) { + $this->addError(ConstraintError::PROPERTIES_MIN(), $path, ['minProperties' => $objectDefinition->minProperties]); + } + } + // Verify maximum number of properties + if (isset($objectDefinition->maxProperties) && is_int($objectDefinition->maxProperties)) { + if ($this->getTypeCheck()->propertyCount($element) > max(0, $objectDefinition->maxProperties)) { + $this->addError(ConstraintError::PROPERTIES_MAX(), $path, ['maxProperties' => $objectDefinition->maxProperties]); + } + } + } +} diff --git a/3rdparty/justinrainbow/json-schema/src/JsonSchema/Constraints/SchemaConstraint.php b/3rdparty/justinrainbow/json-schema/src/JsonSchema/Constraints/SchemaConstraint.php new file mode 100644 index 00000000..7852e851 --- /dev/null +++ b/3rdparty/justinrainbow/json-schema/src/JsonSchema/Constraints/SchemaConstraint.php @@ -0,0 +1,97 @@ + + * @author Bruno Prieto Reis + */ +class SchemaConstraint extends Constraint +{ + private const DEFAULT_SCHEMA_SPEC = 'http://json-schema.org/draft-04/schema#'; + + /** + * {@inheritdoc} + */ + public function check(&$element, $schema = null, ?JsonPointer $path = null, $i = null): void + { + if ($schema !== null) { + // passed schema + $validationSchema = $schema; + } elseif ($this->getTypeCheck()->propertyExists($element, $this->inlineSchemaProperty)) { + // inline schema + $validationSchema = $this->getTypeCheck()->propertyGet($element, $this->inlineSchemaProperty); + } else { + throw new InvalidArgumentException('no schema found to verify against'); + } + + // cast array schemas to object + if (is_array($validationSchema)) { + $validationSchema = BaseConstraint::arrayToObjectRecursive($validationSchema); + } + + // validate schema against whatever is defined in $validationSchema->$schema. If no + // schema is defined, assume self::DEFAULT_SCHEMA_SPEC (currently draft-04). + if ($this->factory->getConfig(self::CHECK_MODE_VALIDATE_SCHEMA)) { + if (!$this->getTypeCheck()->isObject($validationSchema)) { + throw new RuntimeException('Cannot validate the schema of a non-object'); + } + if ($this->getTypeCheck()->propertyExists($validationSchema, '$schema')) { + $schemaSpec = $this->getTypeCheck()->propertyGet($validationSchema, '$schema'); + } else { + $schemaSpec = self::DEFAULT_SCHEMA_SPEC; + } + + // get the spec schema + $schemaStorage = $this->factory->getSchemaStorage(); + if (!$this->getTypeCheck()->isObject($schemaSpec)) { + $schemaSpec = $schemaStorage->getSchema($schemaSpec); + } + + // save error count, config & subtract CHECK_MODE_VALIDATE_SCHEMA + $initialErrorCount = $this->numErrors(); + $initialConfig = $this->factory->getConfig(); + $initialContext = $this->factory->getErrorContext(); + $this->factory->removeConfig(self::CHECK_MODE_VALIDATE_SCHEMA | self::CHECK_MODE_APPLY_DEFAULTS); + $this->factory->addConfig(self::CHECK_MODE_TYPE_CAST); + $this->factory->setErrorContext(Validator::ERROR_SCHEMA_VALIDATION); + + // validate schema + try { + $this->check($validationSchema, $schemaSpec); + } catch (\Exception $e) { + if ($this->factory->getConfig(self::CHECK_MODE_EXCEPTIONS)) { + throw new InvalidSchemaException('Schema did not pass validation', 0, $e); + } + } + if ($this->numErrors() > $initialErrorCount) { + $this->addError(ConstraintError::INVALID_SCHEMA(), $path); + } + + // restore the initial config + $this->factory->setConfig($initialConfig); + $this->factory->setErrorContext($initialContext); + } + + // validate element against $validationSchema + $this->checkUndefined($element, $validationSchema, $path, $i); + } +} diff --git a/3rdparty/justinrainbow/json-schema/src/JsonSchema/Constraints/StringConstraint.php b/3rdparty/justinrainbow/json-schema/src/JsonSchema/Constraints/StringConstraint.php new file mode 100644 index 00000000..7b811614 --- /dev/null +++ b/3rdparty/justinrainbow/json-schema/src/JsonSchema/Constraints/StringConstraint.php @@ -0,0 +1,63 @@ + + * @author Bruno Prieto Reis + */ +class StringConstraint extends Constraint +{ + /** + * {@inheritdoc} + */ + public function check(&$element, $schema = null, ?JsonPointer $path = null, $i = null): void + { + // Verify maxLength + if (isset($schema->maxLength) && $this->strlen($element) > $schema->maxLength) { + $this->addError(ConstraintError::LENGTH_MAX(), $path, [ + 'maxLength' => $schema->maxLength, + ]); + } + + //verify minLength + if (isset($schema->minLength) && $this->strlen($element) < $schema->minLength) { + $this->addError(ConstraintError::LENGTH_MIN(), $path, [ + 'minLength' => $schema->minLength, + ]); + } + + // Verify a regex pattern + if (isset($schema->pattern) && !preg_match(self::jsonPatternToPhpRegex($schema->pattern), $element)) { + $this->addError(ConstraintError::PATTERN(), $path, [ + 'pattern' => $schema->pattern, + ]); + } + + $this->checkFormat($element, $schema, $path, $i); + } + + private function strlen($string) + { + if (extension_loaded('mbstring')) { + return mb_strlen($string, mb_detect_encoding($string)); + } + + // mbstring is present on all test platforms, so strlen() can be ignored for coverage + return strlen($string); // @codeCoverageIgnore + } +} diff --git a/3rdparty/justinrainbow/json-schema/src/JsonSchema/Constraints/TypeCheck/LooseTypeCheck.php b/3rdparty/justinrainbow/json-schema/src/JsonSchema/Constraints/TypeCheck/LooseTypeCheck.php new file mode 100644 index 00000000..8f9f349b --- /dev/null +++ b/3rdparty/justinrainbow/json-schema/src/JsonSchema/Constraints/TypeCheck/LooseTypeCheck.php @@ -0,0 +1,70 @@ +{$property}; + } + + return $value[$property]; + } + + public static function propertySet(&$value, $property, $data) + { + if (is_object($value)) { + $value->{$property} = $data; + } else { + $value[$property] = $data; + } + } + + public static function propertyExists($value, $property) + { + if (is_object($value)) { + return property_exists($value, $property); + } + + return is_array($value) && array_key_exists($property, $value); + } + + public static function propertyCount($value) + { + if (is_object($value)) { + return count(get_object_vars($value)); + } + + return count($value); + } + + /** + * Check if the provided array is associative or not + * + * @param array $arr + * + * @return bool + */ + private static function isAssociativeArray($arr) + { + return array_keys($arr) !== range(0, count($arr) - 1); + } +} diff --git a/3rdparty/justinrainbow/json-schema/src/JsonSchema/Constraints/TypeCheck/StrictTypeCheck.php b/3rdparty/justinrainbow/json-schema/src/JsonSchema/Constraints/TypeCheck/StrictTypeCheck.php new file mode 100644 index 00000000..b0aa813a --- /dev/null +++ b/3rdparty/justinrainbow/json-schema/src/JsonSchema/Constraints/TypeCheck/StrictTypeCheck.php @@ -0,0 +1,42 @@ +{$property}; + } + + public static function propertySet(&$value, $property, $data) + { + $value->{$property} = $data; + } + + public static function propertyExists($value, $property) + { + return property_exists($value, $property); + } + + public static function propertyCount($value) + { + if (!is_object($value)) { + return 0; + } + + return count(get_object_vars($value)); + } +} diff --git a/3rdparty/justinrainbow/json-schema/src/JsonSchema/Constraints/TypeCheck/TypeCheckInterface.php b/3rdparty/justinrainbow/json-schema/src/JsonSchema/Constraints/TypeCheck/TypeCheckInterface.php new file mode 100644 index 00000000..0333b0aa --- /dev/null +++ b/3rdparty/justinrainbow/json-schema/src/JsonSchema/Constraints/TypeCheck/TypeCheckInterface.php @@ -0,0 +1,20 @@ + + * @author Bruno Prieto Reis + */ +class TypeConstraint extends Constraint +{ + /** + * @var array|string[] type wordings for validation error messages + */ + public static $wording = [ + 'integer' => 'an integer', + 'number' => 'a number', + 'boolean' => 'a boolean', + 'object' => 'an object', + 'array' => 'an array', + 'string' => 'a string', + 'null' => 'a null', + 'any' => null, // validation of 'any' is always true so is not needed in message wording + 0 => null, // validation of a false-y value is always true, so not needed as well + ]; + + /** + * {@inheritdoc} + */ + public function check(&$value = null, $schema = null, ?JsonPointer $path = null, $i = null): void + { + $type = isset($schema->type) ? $schema->type : null; + $isValid = false; + $coerce = $this->factory->getConfig(self::CHECK_MODE_COERCE_TYPES); + $earlyCoerce = $this->factory->getConfig(self::CHECK_MODE_EARLY_COERCE); + $wording = []; + + if (is_array($type)) { + $this->validateTypesArray($value, $type, $wording, $isValid, $path, $coerce && $earlyCoerce); + if (!$isValid && $coerce && !$earlyCoerce) { + $this->validateTypesArray($value, $type, $wording, $isValid, $path, true); + } + } elseif (is_object($type)) { + $this->checkUndefined($value, $type, $path); + + return; + } else { + $isValid = $this->validateType($value, $type, $coerce && $earlyCoerce); + if (!$isValid && $coerce && !$earlyCoerce) { + $isValid = $this->validateType($value, $type, true); + } + } + + if ($isValid === false) { + if (!is_array($type)) { + $this->validateTypeNameWording($type); + $wording[] = self::$wording[$type]; + } + $this->addError(ConstraintError::TYPE(), $path, [ + 'found' => gettype($value), + 'expected' => $this->implodeWith($wording, ', ', 'or') + ]); + } + } + + /** + * Validates the given $value against the array of types in $type. Sets the value + * of $isValid to true, if at least one $type mateches the type of $value or the value + * passed as $isValid is already true. + * + * @param mixed $value Value to validate + * @param array $type TypeConstraints to check against + * @param array $validTypesWording An array of wordings of the valid types of the array $type + * @param bool $isValid The current validation value + * @param ?JsonPointer $path + * @param bool $coerce + */ + protected function validateTypesArray(&$value, array $type, &$validTypesWording, &$isValid, $path, $coerce = false) + { + foreach ($type as $tp) { + // already valid, so no need to waste cycles looping over everything + if ($isValid) { + return; + } + + // $tp can be an object, if it's a schema instead of a simple type, validate it + // with a new type constraint + if (is_object($tp)) { + if (!$isValid) { + $validator = $this->factory->createInstanceFor('type'); + $subSchema = new \stdClass(); + $subSchema->type = $tp; + $validator->check($value, $subSchema, $path, null); + $error = $validator->getErrors(); + $isValid = !(bool) $error; + $validTypesWording[] = self::$wording['object']; + } + } else { + $this->validateTypeNameWording($tp); + $validTypesWording[] = self::$wording[$tp]; + if (!$isValid) { + $isValid = $this->validateType($value, $tp, $coerce); + } + } + } + } + + /** + * Implodes the given array like implode() with turned around parameters and with the + * difference, that, if $listEnd isn't false, the last element delimiter is $listEnd instead of + * $delimiter. + * + * @param array $elements The elements to implode + * @param string $delimiter The delimiter to use + * @param bool $listEnd The last delimiter to use (defaults to $delimiter) + * + * @return string + */ + protected function implodeWith(array $elements, $delimiter = ', ', $listEnd = false) + { + if ($listEnd === false || !isset($elements[1])) { + return implode($delimiter, $elements); + } + $lastElement = array_slice($elements, -1); + $firsElements = join($delimiter, array_slice($elements, 0, -1)); + $implodedElements = array_merge([$firsElements], $lastElement); + + return join(" $listEnd ", $implodedElements); + } + + /** + * Validates the given $type, if there's an associated self::$wording. If not, throws an + * exception. + * + * @param string $type The type to validate + * + * @throws StandardUnexpectedValueException + */ + protected function validateTypeNameWording($type) + { + if (!array_key_exists($type, self::$wording)) { + throw new StandardUnexpectedValueException( + sprintf( + 'No wording for %s available, expected wordings are: [%s]', + var_export($type, true), + implode(', ', array_filter(self::$wording))) + ); + } + } + + /** + * Verifies that a given value is of a certain type + * + * @param mixed $value Value to validate + * @param string $type TypeConstraint to check against + * + * @throws InvalidArgumentException + * + * @return bool + */ + protected function validateType(&$value, $type, $coerce = false) + { + //mostly the case for inline schema + if (!$type) { + return true; + } + + if ('any' === $type) { + return true; + } + + if ('object' === $type) { + return $this->getTypeCheck()->isObject($value); + } + + if ('array' === $type) { + if ($coerce) { + $value = $this->toArray($value); + } + + return $this->getTypeCheck()->isArray($value); + } + + if ('integer' === $type) { + if ($coerce) { + $value = $this->toInteger($value); + } + + return is_int($value); + } + + if ('number' === $type) { + if ($coerce) { + $value = $this->toNumber($value); + } + + return is_numeric($value) && !is_string($value); + } + + if ('boolean' === $type) { + if ($coerce) { + $value = $this->toBoolean($value); + } + + return is_bool($value); + } + + if ('string' === $type) { + if ($coerce) { + $value = $this->toString($value); + } + + return is_string($value); + } + + if ('null' === $type) { + if ($coerce) { + $value = $this->toNull($value); + } + + return is_null($value); + } + + throw new InvalidArgumentException((is_object($value) ? 'object' : $value) . ' is an invalid type for ' . $type); + } + + /** + * Converts a value to boolean. For example, "true" becomes true. + * + * @param mixed $value The value to convert to boolean + * + * @return bool|mixed + */ + protected function toBoolean($value) + { + if ($value === 1 || $value === 'true') { + return true; + } + if (is_null($value) || $value === 0 || $value === 'false') { + return false; + } + if ($this->getTypeCheck()->isArray($value) && count($value) === 1) { + return $this->toBoolean(reset($value)); + } + + return $value; + } + + /** + * Converts a value to a number. For example, "4.5" becomes 4.5. + * + * @param mixed $value the value to convert to a number + * + * @return int|float|mixed + */ + protected function toNumber($value) + { + if (is_numeric($value)) { + return $value + 0; // cast to number + } + if (is_bool($value) || is_null($value)) { + return (int) $value; + } + if ($this->getTypeCheck()->isArray($value) && count($value) === 1) { + return $this->toNumber(reset($value)); + } + + return $value; + } + + /** + * Converts a value to an integer. For example, "4" becomes 4. + * + * @param mixed $value + * + * @return int|mixed + */ + protected function toInteger($value) + { + $numberValue = $this->toNumber($value); + if (is_numeric($numberValue) && (int) $numberValue == $numberValue) { + return (int) $numberValue; // cast to number + } + + return $value; + } + + /** + * Converts a value to an array containing that value. For example, [4] becomes 4. + * + * @param mixed $value + * + * @return array|mixed + */ + protected function toArray($value) + { + if (is_scalar($value) || is_null($value)) { + return [$value]; + } + + return $value; + } + + /** + * Convert a value to a string representation of that value. For example, null becomes "". + * + * @param mixed $value + * + * @return string|mixed + */ + protected function toString($value) + { + if (is_numeric($value)) { + return "$value"; + } + if ($value === true) { + return 'true'; + } + if ($value === false) { + return 'false'; + } + if (is_null($value)) { + return ''; + } + if ($this->getTypeCheck()->isArray($value) && count($value) === 1) { + return $this->toString(reset($value)); + } + + return $value; + } + + /** + * Convert a value to a null. For example, 0 becomes null. + * + * @param mixed $value + * + * @return null|mixed + */ + protected function toNull($value) + { + if ($value === 0 || $value === false || $value === '') { + return null; + } + if ($this->getTypeCheck()->isArray($value) && count($value) === 1) { + return $this->toNull(reset($value)); + } + + return $value; + } +} diff --git a/3rdparty/justinrainbow/json-schema/src/JsonSchema/Constraints/UndefinedConstraint.php b/3rdparty/justinrainbow/json-schema/src/JsonSchema/Constraints/UndefinedConstraint.php new file mode 100644 index 00000000..1ea0a7bc --- /dev/null +++ b/3rdparty/justinrainbow/json-schema/src/JsonSchema/Constraints/UndefinedConstraint.php @@ -0,0 +1,428 @@ + List of properties to which a default value has been applied + */ + protected $appliedDefaults = []; + + /** + * {@inheritdoc} + * */ + public function check(&$value, $schema = null, ?JsonPointer $path = null, $i = null, bool $fromDefault = false): void + { + if (is_null($schema) || !is_object($schema)) { + return; + } + + $path = $this->incrementPath($path, $i); + if ($fromDefault) { + $path->setFromDefault(); + } + + // check special properties + $this->validateCommonProperties($value, $schema, $path, $i); + + // check allOf, anyOf, and oneOf properties + $this->validateOfProperties($value, $schema, $path, ''); + + // check known types + $this->validateTypes($value, $schema, $path, $i); + } + + /** + * Validates the value against the types + * + * @param mixed $value + * @param mixed $schema + * @param JsonPointer $path + * @param string $i + */ + public function validateTypes(&$value, $schema, JsonPointer $path, $i = null) + { + // check array + if ($this->getTypeCheck()->isArray($value)) { + $this->checkArray($value, $schema, $path, $i); + } + + // check object + if (LooseTypeCheck::isObject($value)) { + // object processing should always be run on assoc arrays, + // so use LooseTypeCheck here even if CHECK_MODE_TYPE_CAST + // is not set (i.e. don't use $this->getTypeCheck() here). + $this->checkObject( + $value, + $schema, + $path, + isset($schema->properties) ? $schema->properties : null, + isset($schema->additionalProperties) ? $schema->additionalProperties : null, + isset($schema->patternProperties) ? $schema->patternProperties : null, + $this->appliedDefaults + ); + } + + // check string + if (is_string($value)) { + $this->checkString($value, $schema, $path, $i); + } + + // check numeric + if (is_numeric($value)) { + $this->checkNumber($value, $schema, $path, $i); + } + + // check enum + if (isset($schema->enum)) { + $this->checkEnum($value, $schema, $path, $i); + } + + // check const + if (isset($schema->const)) { + $this->checkConst($value, $schema, $path, $i); + } + } + + /** + * Validates common properties + * + * @param mixed $value + * @param mixed $schema + * @param JsonPointer $path + * @param string $i + */ + protected function validateCommonProperties(&$value, $schema, JsonPointer $path, $i = '') + { + // if it extends another schema, it must pass that schema as well + if (isset($schema->extends)) { + if (is_string($schema->extends)) { + $schema->extends = $this->validateUri($schema, $schema->extends); + } + if (is_array($schema->extends)) { + foreach ($schema->extends as $extends) { + $this->checkUndefined($value, $extends, $path, $i); + } + } else { + $this->checkUndefined($value, $schema->extends, $path, $i); + } + } + + // Apply default values from schema + if (!$path->fromDefault()) { + $this->applyDefaultValues($value, $schema, $path); + } + + // Verify required values + if ($this->getTypeCheck()->isObject($value)) { + if (!($value instanceof self) && isset($schema->required) && is_array($schema->required)) { + // Draft 4 - Required is an array of strings - e.g. "required": ["foo", ...] + foreach ($schema->required as $required) { + if (!$this->getTypeCheck()->propertyExists($value, $required)) { + $this->addError( + ConstraintError::REQUIRED(), + $this->incrementPath($path, $required), [ + 'property' => $required + ] + ); + } + } + } elseif (isset($schema->required) && !is_array($schema->required)) { + // Draft 3 - Required attribute - e.g. "foo": {"type": "string", "required": true} + if ($schema->required && $value instanceof self) { + $propertyPaths = $path->getPropertyPaths(); + $propertyName = end($propertyPaths); + $this->addError(ConstraintError::REQUIRED(), $path, ['property' => $propertyName]); + } + } else { + // if the value is both undefined and not required, skip remaining checks + // in this method which assume an actual, defined instance when validating. + if ($value instanceof self) { + return; + } + } + } + + // Verify type + if (!($value instanceof self)) { + $this->checkType($value, $schema, $path, $i); + } + + // Verify disallowed items + if (isset($schema->disallow)) { + $initErrors = $this->getErrors(); + + $typeSchema = new \stdClass(); + $typeSchema->type = $schema->disallow; + $this->checkType($value, $typeSchema, $path); + + // if no new errors were raised it must be a disallowed value + if (count($this->getErrors()) == count($initErrors)) { + $this->addError(ConstraintError::DISALLOW(), $path); + } else { + $this->errors = $initErrors; + } + } + + if (isset($schema->not)) { + $initErrors = $this->getErrors(); + $this->checkUndefined($value, $schema->not, $path, $i); + + // if no new errors were raised then the instance validated against the "not" schema + if (count($this->getErrors()) == count($initErrors)) { + $this->addError(ConstraintError::NOT(), $path); + } else { + $this->errors = $initErrors; + } + } + + // Verify that dependencies are met + if (isset($schema->dependencies) && $this->getTypeCheck()->isObject($value)) { + $this->validateDependencies($value, $schema->dependencies, $path); + } + } + + /** + * Check whether a default should be applied for this value + * + * @param mixed $schema + * @param mixed $parentSchema + * @param bool $requiredOnly + * + * @return bool + */ + private function shouldApplyDefaultValue($requiredOnly, $schema, $name = null, $parentSchema = null) + { + // required-only mode is off + if (!$requiredOnly) { + return true; + } + // draft-04 required is set + if ( + $name !== null + && isset($parentSchema->required) + && is_array($parentSchema->required) + && in_array($name, $parentSchema->required) + ) { + return true; + } + // draft-03 required is set + if (isset($schema->required) && !is_array($schema->required) && $schema->required) { + return true; + } + // default case + return false; + } + + /** + * Apply default values + * + * @param mixed $value + * @param mixed $schema + * @param JsonPointer $path + */ + protected function applyDefaultValues(&$value, $schema, $path): void + { + // only apply defaults if feature is enabled + if (!$this->factory->getConfig(self::CHECK_MODE_APPLY_DEFAULTS)) { + return; + } + + // apply defaults if appropriate + $requiredOnly = (bool) $this->factory->getConfig(self::CHECK_MODE_ONLY_REQUIRED_DEFAULTS); + if (isset($schema->properties) && LooseTypeCheck::isObject($value)) { + // $value is an object or assoc array, and properties are defined - treat as an object + foreach ($schema->properties as $currentProperty => $propertyDefinition) { + $propertyDefinition = $this->factory->getSchemaStorage()->resolveRefSchema($propertyDefinition); + if ( + !LooseTypeCheck::propertyExists($value, $currentProperty) + && property_exists($propertyDefinition, 'default') + && $this->shouldApplyDefaultValue($requiredOnly, $propertyDefinition, $currentProperty, $schema) + ) { + // assign default value + if (is_object($propertyDefinition->default)) { + LooseTypeCheck::propertySet($value, $currentProperty, clone $propertyDefinition->default); + } else { + LooseTypeCheck::propertySet($value, $currentProperty, $propertyDefinition->default); + } + $this->appliedDefaults[] = $currentProperty; + } + } + } elseif (isset($schema->items) && LooseTypeCheck::isArray($value)) { + $items = []; + if (LooseTypeCheck::isArray($schema->items)) { + $items = $schema->items; + } elseif (isset($schema->minItems) && count($value) < $schema->minItems) { + $items = array_fill(count($value), $schema->minItems - count($value), $schema->items); + } + // $value is an array, and items are defined - treat as plain array + foreach ($items as $currentItem => $itemDefinition) { + $itemDefinition = $this->factory->getSchemaStorage()->resolveRefSchema($itemDefinition); + if ( + !array_key_exists($currentItem, $value) + && property_exists($itemDefinition, 'default') + && $this->shouldApplyDefaultValue($requiredOnly, $itemDefinition)) { + if (is_object($itemDefinition->default)) { + $value[$currentItem] = clone $itemDefinition->default; + } else { + $value[$currentItem] = $itemDefinition->default; + } + } + $path->setFromDefault(); + } + } elseif ( + $value instanceof self + && property_exists($schema, 'default') + && $this->shouldApplyDefaultValue($requiredOnly, $schema)) { + // $value is a leaf, not a container - apply the default directly + $value = is_object($schema->default) ? clone $schema->default : $schema->default; + $path->setFromDefault(); + } + } + + /** + * Validate allOf, anyOf, and oneOf properties + * + * @param mixed $value + * @param mixed $schema + * @param JsonPointer $path + * @param string $i + */ + protected function validateOfProperties(&$value, $schema, JsonPointer $path, $i = '') + { + // Verify type + if ($value instanceof self) { + return; + } + + if (isset($schema->allOf)) { + $isValid = true; + foreach ($schema->allOf as $allOf) { + $initErrors = $this->getErrors(); + $this->checkUndefined($value, $allOf, $path, $i); + $isValid = $isValid && (count($this->getErrors()) == count($initErrors)); + } + if (!$isValid) { + $this->addError(ConstraintError::ALL_OF(), $path); + } + } + + if (isset($schema->anyOf)) { + $isValid = false; + $startErrors = $this->getErrors(); + $coerceOrDefaults = $this->factory->getConfig(self::CHECK_MODE_COERCE_TYPES | self::CHECK_MODE_APPLY_DEFAULTS); + + foreach ($schema->anyOf as $anyOf) { + $initErrors = $this->getErrors(); + try { + $anyOfValue = $coerceOrDefaults ? DeepCopy::copyOf($value) : $value; + $this->checkUndefined($anyOfValue, $anyOf, $path, $i); + if ($isValid = (count($this->getErrors()) === count($initErrors))) { + $value = $anyOfValue; + break; + } + } catch (ValidationException $e) { + $isValid = false; + } + } + if (!$isValid) { + $this->addError(ConstraintError::ANY_OF(), $path); + } else { + $this->errors = $startErrors; + } + } + + if (isset($schema->oneOf)) { + $allErrors = []; + $matchedSchemas = []; + $startErrors = $this->getErrors(); + $coerceOrDefaults = $this->factory->getConfig(self::CHECK_MODE_COERCE_TYPES | self::CHECK_MODE_APPLY_DEFAULTS); + + foreach ($schema->oneOf as $oneOf) { + try { + $this->errors = []; + + $oneOfValue = $coerceOrDefaults ? DeepCopy::copyOf($value) : $value; + $this->checkUndefined($oneOfValue, $oneOf, $path, $i); + if (count($this->getErrors()) === 0) { + $matchedSchemas[] = ['schema' => $oneOf, 'value' => $oneOfValue]; + } + $allErrors = array_merge($allErrors, array_values($this->getErrors())); + } catch (ValidationException $e) { + // deliberately do nothing here - validation failed, but we want to check + // other schema options in the OneOf field. + } + } + if (count($matchedSchemas) !== 1) { + $this->addErrors(array_merge($allErrors, $startErrors)); + $this->addError(ConstraintError::ONE_OF(), $path); + } else { + $this->errors = $startErrors; + $value = $matchedSchemas[0]['value']; + } + } + } + + /** + * Validate dependencies + * + * @param mixed $value + * @param mixed $dependencies + * @param JsonPointer $path + * @param string $i + */ + protected function validateDependencies($value, $dependencies, JsonPointer $path, $i = '') + { + foreach ($dependencies as $key => $dependency) { + if ($this->getTypeCheck()->propertyExists($value, $key)) { + if (is_string($dependency)) { + // Draft 3 string is allowed - e.g. "dependencies": {"bar": "foo"} + if (!$this->getTypeCheck()->propertyExists($value, $dependency)) { + $this->addError(ConstraintError::DEPENDENCIES(), $path, [ + 'key' => $key, + 'dependency' => $dependency + ]); + } + } elseif (is_array($dependency)) { + // Draft 4 must be an array - e.g. "dependencies": {"bar": ["foo"]} + foreach ($dependency as $d) { + if (!$this->getTypeCheck()->propertyExists($value, $d)) { + $this->addError(ConstraintError::DEPENDENCIES(), $path, [ + 'key' => $key, + 'dependency' => $dependency + ]); + } + } + } elseif (is_object($dependency)) { + // Schema - e.g. "dependencies": {"bar": {"properties": {"foo": {...}}}} + $this->checkUndefined($value, $dependency, $path, $i); + } + } + } + } + + protected function validateUri($schema, $schemaUri = null) + { + $resolver = new UriResolver(); + $retriever = $this->factory->getUriRetriever(); + + $jsonSchema = null; + if ($resolver->isValid($schemaUri)) { + $schemaId = property_exists($schema, 'id') ? $schema->id : null; + $jsonSchema = $retriever->retrieve($schemaId, $schemaUri); + } + + return $jsonSchema; + } +} diff --git a/3rdparty/justinrainbow/json-schema/src/JsonSchema/Entity/JsonPointer.php b/3rdparty/justinrainbow/json-schema/src/JsonSchema/Entity/JsonPointer.php new file mode 100644 index 00000000..e674fa6f --- /dev/null +++ b/3rdparty/justinrainbow/json-schema/src/JsonSchema/Entity/JsonPointer.php @@ -0,0 +1,163 @@ + + */ +class JsonPointer +{ + /** @var string */ + private $filename; + + /** @var string[] */ + private $propertyPaths = []; + + /** + * @var bool Whether the value at this path was set from a schema default + */ + private $fromDefault = false; + + /** + * @param string $value + * + * @throws InvalidArgumentException when $value is not a string + */ + public function __construct($value) + { + if (!is_string($value)) { + throw new InvalidArgumentException('Ref value must be a string'); + } + + $splitRef = explode('#', $value, 2); + $this->filename = $splitRef[0]; + if (array_key_exists(1, $splitRef)) { + $this->propertyPaths = $this->decodePropertyPaths($splitRef[1]); + } + } + + /** + * @param string $propertyPathString + * + * @return string[] + */ + private function decodePropertyPaths($propertyPathString) + { + $paths = []; + foreach (explode('/', trim($propertyPathString, '/')) as $path) { + $path = $this->decodePath($path); + if (is_string($path) && '' !== $path) { + $paths[] = $path; + } + } + + return $paths; + } + + /** + * @return array + */ + private function encodePropertyPaths() + { + return array_map( + [$this, 'encodePath'], + $this->getPropertyPaths() + ); + } + + /** + * @param string $path + * + * @return string + */ + private function decodePath($path) + { + return strtr($path, ['~1' => '/', '~0' => '~', '%25' => '%']); + } + + /** + * @param string $path + * + * @return string + */ + private function encodePath($path) + { + return strtr($path, ['/' => '~1', '~' => '~0', '%' => '%25']); + } + + /** + * @return string + */ + public function getFilename() + { + return $this->filename; + } + + /** + * @return string[] + */ + public function getPropertyPaths() + { + return $this->propertyPaths; + } + + /** + * @param array $propertyPaths + * + * @return JsonPointer + */ + public function withPropertyPaths(array $propertyPaths) + { + $new = clone $this; + $new->propertyPaths = array_map(function ($p): string { return (string) $p; }, $propertyPaths); + + return $new; + } + + /** + * @return string + */ + public function getPropertyPathAsString() + { + return rtrim('#/' . implode('/', $this->encodePropertyPaths()), '/'); + } + + /** + * @return string + */ + public function __toString() + { + return $this->getFilename() . $this->getPropertyPathAsString(); + } + + /** + * Mark the value at this path as being set from a schema default + */ + public function setFromDefault(): void + { + $this->fromDefault = true; + } + + /** + * Check whether the value at this path was set from a schema default + * + * @return bool + */ + public function fromDefault() + { + return $this->fromDefault; + } +} diff --git a/3rdparty/justinrainbow/json-schema/src/JsonSchema/Enum.php b/3rdparty/justinrainbow/json-schema/src/JsonSchema/Enum.php new file mode 100644 index 00000000..ef8cb285 --- /dev/null +++ b/3rdparty/justinrainbow/json-schema/src/JsonSchema/Enum.php @@ -0,0 +1,9 @@ + + */ +class UnresolvableJsonPointerException extends InvalidArgumentException +{ +} diff --git a/3rdparty/justinrainbow/json-schema/src/JsonSchema/Exception/UriResolverException.php b/3rdparty/justinrainbow/json-schema/src/JsonSchema/Exception/UriResolverException.php new file mode 100644 index 00000000..9170a3fc --- /dev/null +++ b/3rdparty/justinrainbow/json-schema/src/JsonSchema/Exception/UriResolverException.php @@ -0,0 +1,19 @@ + + */ +class ObjectIterator implements \Iterator, \Countable +{ + /** @var object */ + private $object; + + /** @var int */ + private $position = 0; + + /** @var array */ + private $data = []; + + /** @var bool */ + private $initialized = false; + + /** + * @param object $object + */ + public function __construct($object) + { + $this->object = $object; + } + + /** + * {@inheritdoc} + */ + #[\ReturnTypeWillChange] + public function current() + { + $this->initialize(); + + return $this->data[$this->position]; + } + + /** + * {@inheritdoc} + */ + public function next(): void + { + $this->initialize(); + $this->position++; + } + + /** + * {@inheritdoc} + */ + public function key(): int + { + $this->initialize(); + + return $this->position; + } + + /** + * {@inheritdoc} + */ + public function valid(): bool + { + $this->initialize(); + + return isset($this->data[$this->position]); + } + + /** + * {@inheritdoc} + */ + public function rewind(): void + { + $this->initialize(); + $this->position = 0; + } + + /** + * {@inheritdoc} + */ + public function count(): int + { + $this->initialize(); + + return count($this->data); + } + + /** + * Initializer + */ + private function initialize() + { + if (!$this->initialized) { + $this->data = $this->buildDataFromObject($this->object); + $this->initialized = true; + } + } + + /** + * @param object $object + * + * @return array + */ + private function buildDataFromObject($object) + { + $result = []; + + $stack = new \SplStack(); + $stack->push($object); + + while (!$stack->isEmpty()) { + $current = $stack->pop(); + if (is_object($current)) { + array_push($result, $current); + } + + foreach ($this->getDataFromItem($current) as $propertyName => $propertyValue) { + if (is_object($propertyValue) || is_array($propertyValue)) { + $stack->push($propertyValue); + } + } + } + + return $result; + } + + /** + * @param object|array $item + * + * @return array + */ + private function getDataFromItem($item) + { + if (!is_object($item) && !is_array($item)) { + return []; + } + + return is_object($item) ? get_object_vars($item) : $item; + } +} diff --git a/3rdparty/justinrainbow/json-schema/src/JsonSchema/Rfc3339.php b/3rdparty/justinrainbow/json-schema/src/JsonSchema/Rfc3339.php new file mode 100644 index 00000000..3524f681 --- /dev/null +++ b/3rdparty/justinrainbow/json-schema/src/JsonSchema/Rfc3339.php @@ -0,0 +1,32 @@ +uriRetriever = $uriRetriever ?: new UriRetriever(); + $this->uriResolver = $uriResolver ?: new UriResolver(); + } + + /** + * @return UriRetrieverInterface + */ + public function getUriRetriever() + { + return $this->uriRetriever; + } + + /** + * @return UriResolverInterface + */ + public function getUriResolver() + { + return $this->uriResolver; + } + + /** + * {@inheritdoc} + */ + public function addSchema(string $id, $schema = null): void + { + if (is_null($schema) && $id !== self::INTERNAL_PROVIDED_SCHEMA_URI) { + // if the schema was user-provided to Validator and is still null, then assume this is + // what the user intended, as there's no way for us to retrieve anything else. User-supplied + // schemas do not have an associated URI when passed via Validator::validate(). + $schema = $this->uriRetriever->retrieve($id); + } + + // cast array schemas to object + if (is_array($schema)) { + $schema = BaseConstraint::arrayToObjectRecursive($schema); + } + + // workaround for bug in draft-03 & draft-04 meta-schemas (id & $ref defined with incorrect format) + // see https://github.com/json-schema-org/JSON-Schema-Test-Suite/issues/177#issuecomment-293051367 + if (is_object($schema) && property_exists($schema, 'id')) { + if ($schema->id === 'http://json-schema.org/draft-04/schema#') { + $schema->properties->id->format = 'uri-reference'; + } elseif ($schema->id === 'http://json-schema.org/draft-03/schema#') { + $schema->properties->id->format = 'uri-reference'; + $schema->properties->{'$ref'}->format = 'uri-reference'; + } + } + + $this->scanForSubschemas($schema, $id); + + // resolve references + $this->expandRefs($schema, $id); + + $this->schemas[$id] = $schema; + } + + /** + * Recursively resolve all references against the provided base + * + * @param mixed $schema + */ + private function expandRefs(&$schema, ?string $parentId = null): void + { + if (!is_object($schema)) { + if (is_array($schema)) { + foreach ($schema as &$member) { + $this->expandRefs($member, $parentId); + } + } + + return; + } + + if (property_exists($schema, '$ref') && is_string($schema->{'$ref'})) { + $refPointer = new JsonPointer($this->uriResolver->resolve($schema->{'$ref'}, $parentId)); + $schema->{'$ref'} = (string) $refPointer; + } + + foreach ($schema as $propertyName => &$member) { + if (in_array($propertyName, ['enum', 'const'])) { + // Enum and const don't allow $ref as a keyword, see https://github.com/json-schema-org/JSON-Schema-Test-Suite/pull/445 + continue; + } + + $childId = $parentId; + if (property_exists($schema, 'id') && is_string($schema->id) && $childId !== $schema->id) { + $childId = $this->uriResolver->resolve($schema->id, $childId); + } + + $this->expandRefs($member, $childId); + } + } + + /** + * {@inheritdoc} + */ + public function getSchema(string $id) + { + if (!array_key_exists($id, $this->schemas)) { + $this->addSchema($id); + } + + return $this->schemas[$id]; + } + + /** + * {@inheritdoc} + */ + public function resolveRef(string $ref, $resolveStack = []) + { + $jsonPointer = new JsonPointer($ref); + + // resolve filename for pointer + $fileName = $jsonPointer->getFilename(); + if (!strlen($fileName)) { + throw new UnresolvableJsonPointerException(sprintf( + "Could not resolve fragment '%s': no file is defined", + $jsonPointer->getPropertyPathAsString() + )); + } + + // get & process the schema + $refSchema = $this->getSchema($fileName); + foreach ($jsonPointer->getPropertyPaths() as $path) { + if (is_object($refSchema) && property_exists($refSchema, $path)) { + $refSchema = $this->resolveRefSchema($refSchema->{$path}, $resolveStack); + } elseif (is_array($refSchema) && array_key_exists($path, $refSchema)) { + $refSchema = $this->resolveRefSchema($refSchema[$path], $resolveStack); + } else { + throw new UnresolvableJsonPointerException(sprintf( + 'File: %s is found, but could not resolve fragment: %s', + $jsonPointer->getFilename(), + $jsonPointer->getPropertyPathAsString() + )); + } + } + + return $refSchema; + } + + /** + * {@inheritdoc} + */ + public function resolveRefSchema($refSchema, $resolveStack = []) + { + if (is_object($refSchema) && property_exists($refSchema, '$ref') && is_string($refSchema->{'$ref'})) { + if (in_array($refSchema, $resolveStack, true)) { + throw new UnresolvableJsonPointerException(sprintf( + 'Dereferencing a pointer to %s results in an infinite loop', + $refSchema->{'$ref'} + )); + } + $resolveStack[] = $refSchema; + + return $this->resolveRef($refSchema->{'$ref'}, $resolveStack); + } + + return $refSchema; + } + + /** + * @param mixed $schema + */ + private function scanForSubschemas($schema, string $parentId): void + { + if (!$schema instanceof \stdClass && !is_array($schema)) { + return; + } + + foreach ($schema as $propertyName => $potentialSubSchema) { + if (!is_object($potentialSubSchema)) { + continue; + } + + if (property_exists($potentialSubSchema, 'id') && is_string($potentialSubSchema->id) && property_exists($potentialSubSchema, 'type')) { + // Enum and const don't allow id as a keyword, see https://github.com/json-schema-org/JSON-Schema-Test-Suite/pull/471 + if (in_array($propertyName, ['enum', 'const'])) { + continue; + } + + // Found sub schema + $this->addSchema($this->uriResolver->resolve($potentialSubSchema->id, $parentId), $potentialSubSchema); + } + + $this->scanForSubschemas($potentialSubSchema, $parentId); + } + } +} diff --git a/3rdparty/justinrainbow/json-schema/src/JsonSchema/SchemaStorageInterface.php b/3rdparty/justinrainbow/json-schema/src/JsonSchema/SchemaStorageInterface.php new file mode 100644 index 00000000..f625cdd2 --- /dev/null +++ b/3rdparty/justinrainbow/json-schema/src/JsonSchema/SchemaStorageInterface.php @@ -0,0 +1,38 @@ + $left + * @param array $right + */ + private static function isArrayEqual(array $left, array $right): bool + { + if (count($left) !== count($right)) { + return false; + } + foreach ($left as $key => $value) { + if (!array_key_exists($key, $right)) { + return false; + } + + if (!self::isEqual($value, $right[$key])) { + return false; + } + } + + return true; + } +} diff --git a/3rdparty/justinrainbow/json-schema/src/JsonSchema/Tool/DeepCopy.php b/3rdparty/justinrainbow/json-schema/src/JsonSchema/Tool/DeepCopy.php new file mode 100644 index 00000000..8b09156f --- /dev/null +++ b/3rdparty/justinrainbow/json-schema/src/JsonSchema/Tool/DeepCopy.php @@ -0,0 +1,38 @@ + 65535)) { + return false; + } + + // Validate path (reject illegal characters: < > { } | \ ^ `) + if (!empty($matches[6]) && preg_match('/[<>{}|\\\^`]/', $matches[6])) { + return false; + } + + return true; + } + + // If not hierarchical, check non-hierarchical URIs + if (preg_match($nonHierarchicalPattern, $uri, $matches) === 1) { + $scheme = strtolower($matches[1]); // Extract the scheme + + // Special case: `mailto:` must contain a **valid email address** + if ($scheme === 'mailto') { + return preg_match($emailPattern, $matches[2]) === 1; + } + + return true; // Valid non-hierarchical URI + } + + return false; + } +} diff --git a/3rdparty/justinrainbow/json-schema/src/JsonSchema/Uri/Retrievers/AbstractRetriever.php b/3rdparty/justinrainbow/json-schema/src/JsonSchema/Uri/Retrievers/AbstractRetriever.php new file mode 100644 index 00000000..f4ae718a --- /dev/null +++ b/3rdparty/justinrainbow/json-schema/src/JsonSchema/Uri/Retrievers/AbstractRetriever.php @@ -0,0 +1,37 @@ + + */ +abstract class AbstractRetriever implements UriRetrieverInterface +{ + /** + * Media content type + * + * @var string + */ + protected $contentType; + + /** + * {@inheritdoc} + * + * @see \JsonSchema\Uri\Retrievers\UriRetrieverInterface::getContentType() + */ + public function getContentType() + { + return $this->contentType; + } +} diff --git a/3rdparty/justinrainbow/json-schema/src/JsonSchema/Uri/Retrievers/Curl.php b/3rdparty/justinrainbow/json-schema/src/JsonSchema/Uri/Retrievers/Curl.php new file mode 100644 index 00000000..116607ae --- /dev/null +++ b/3rdparty/justinrainbow/json-schema/src/JsonSchema/Uri/Retrievers/Curl.php @@ -0,0 +1,85 @@ + + */ +class Curl extends AbstractRetriever +{ + protected $messageBody; + + public function __construct() + { + if (!function_exists('curl_init')) { + // Cannot test this, because curl_init is present on all test platforms plus mock + throw new RuntimeException('cURL not installed'); // @codeCoverageIgnore + } + } + + /** + * {@inheritdoc} + * + * @see \JsonSchema\Uri\Retrievers\UriRetrieverInterface::retrieve() + */ + public function retrieve($uri) + { + $ch = curl_init(); + + curl_setopt($ch, CURLOPT_URL, $uri); + curl_setopt($ch, CURLOPT_HEADER, true); + curl_setopt($ch, CURLOPT_RETURNTRANSFER, true); + curl_setopt($ch, CURLOPT_HTTPHEADER, ['Accept: ' . Validator::SCHEMA_MEDIA_TYPE]); + + $response = curl_exec($ch); + if (false === $response) { + throw new \JsonSchema\Exception\ResourceNotFoundException('JSON schema not found'); + } + + $this->fetchMessageBody($response); + $this->fetchContentType($response); + + curl_close($ch); + + return $this->messageBody; + } + + /** + * @param string $response cURL HTTP response + */ + private function fetchMessageBody($response) + { + preg_match("/(?:\r\n){2}(.*)$/ms", $response, $match); + $this->messageBody = $match[1]; + } + + /** + * @param string $response cURL HTTP response + * + * @return bool Whether the Content-Type header was found or not + */ + protected function fetchContentType($response) + { + if (0 < preg_match("/Content-Type:(\V*)/ims", $response, $match)) { + $this->contentType = trim($match[1]); + + return true; + } + + return false; + } +} diff --git a/3rdparty/justinrainbow/json-schema/src/JsonSchema/Uri/Retrievers/FileGetContents.php b/3rdparty/justinrainbow/json-schema/src/JsonSchema/Uri/Retrievers/FileGetContents.php new file mode 100644 index 00000000..18fb7fcf --- /dev/null +++ b/3rdparty/justinrainbow/json-schema/src/JsonSchema/Uri/Retrievers/FileGetContents.php @@ -0,0 +1,95 @@ + + */ +class FileGetContents extends AbstractRetriever +{ + protected $messageBody; + + /** + * {@inheritdoc} + * + * @see \JsonSchema\Uri\Retrievers\UriRetrieverInterface::retrieve() + */ + public function retrieve($uri) + { + $errorMessage = null; + set_error_handler(function ($errno, $errstr) use (&$errorMessage) { + $errorMessage = $errstr; + }); + $response = file_get_contents($uri); + restore_error_handler(); + + if ($errorMessage) { + throw new ResourceNotFoundException($errorMessage); + } + + if (false === $response) { + throw new ResourceNotFoundException('JSON schema not found at ' . $uri); + } + + if ($response == '' + && substr($uri, 0, 7) == 'file://' && substr($uri, -1) == '/' + ) { + throw new ResourceNotFoundException('JSON schema not found at ' . $uri); + } + + $this->messageBody = $response; + if (!empty($http_response_header)) { + // $http_response_header cannot be tested, because it's defined in the method's local scope + // See http://php.net/manual/en/reserved.variables.httpresponseheader.php for more info. + $this->fetchContentType($http_response_header); // @codeCoverageIgnore + } else { // @codeCoverageIgnore + // Could be a "file://" url or something else - fake up the response + $this->contentType = null; + } + + return $this->messageBody; + } + + /** + * @param array $headers HTTP Response Headers + * + * @return bool Whether the Content-Type header was found or not + */ + private function fetchContentType(array $headers) + { + foreach (array_reverse($headers) as $header) { + if ($this->contentType = self::getContentTypeMatchInHeader($header)) { + return true; + } + } + + return false; + } + + /** + * @param string $header + * + * @return string|null + */ + protected static function getContentTypeMatchInHeader($header) + { + if (0 < preg_match("/Content-Type:(\V*)/ims", $header, $match)) { + return trim($match[1]); + } + + return null; + } +} diff --git a/3rdparty/justinrainbow/json-schema/src/JsonSchema/Uri/Retrievers/PredefinedArray.php b/3rdparty/justinrainbow/json-schema/src/JsonSchema/Uri/Retrievers/PredefinedArray.php new file mode 100644 index 00000000..11693002 --- /dev/null +++ b/3rdparty/justinrainbow/json-schema/src/JsonSchema/Uri/Retrievers/PredefinedArray.php @@ -0,0 +1,58 @@ + '{ ... }', + * 'http://acme.com/schemas/address#' => '{ ... }', + * )) + * + * $schema = $retriever->retrieve('http://acme.com/schemas/person#'); + */ +class PredefinedArray extends AbstractRetriever +{ + /** + * Contains schemas as URI => JSON + * + * @var array + */ + private $schemas; + + /** + * Constructor + * + * @param array $schemas + * @param string $contentType + */ + public function __construct(array $schemas, $contentType = Validator::SCHEMA_MEDIA_TYPE) + { + $this->schemas = $schemas; + $this->contentType = $contentType; + } + + /** + * {@inheritdoc} + * + * @see \JsonSchema\Uri\Retrievers\UriRetrieverInterface::retrieve() + */ + public function retrieve($uri) + { + if (!array_key_exists($uri, $this->schemas)) { + throw new \JsonSchema\Exception\ResourceNotFoundException(sprintf( + 'The JSON schema "%s" was not found.', + $uri + )); + } + + return $this->schemas[$uri]; + } +} diff --git a/3rdparty/justinrainbow/json-schema/src/JsonSchema/Uri/Retrievers/UriRetrieverInterface.php b/3rdparty/justinrainbow/json-schema/src/JsonSchema/Uri/Retrievers/UriRetrieverInterface.php new file mode 100644 index 00000000..99a2d9e7 --- /dev/null +++ b/3rdparty/justinrainbow/json-schema/src/JsonSchema/Uri/Retrievers/UriRetrieverInterface.php @@ -0,0 +1,38 @@ + + */ +interface UriRetrieverInterface +{ + /** + * Retrieve a schema from the specified URI + * + * @param string $uri URI that resolves to a JSON schema + * + * @throws \JsonSchema\Exception\ResourceNotFoundException + * + * @return mixed string|null + */ + public function retrieve($uri); + + /** + * Get media content type + * + * @return string + */ + public function getContentType(); +} diff --git a/3rdparty/justinrainbow/json-schema/src/JsonSchema/Uri/UriResolver.php b/3rdparty/justinrainbow/json-schema/src/JsonSchema/Uri/UriResolver.php new file mode 100644 index 00000000..00a47e81 --- /dev/null +++ b/3rdparty/justinrainbow/json-schema/src/JsonSchema/Uri/UriResolver.php @@ -0,0 +1,189 @@ + + */ +class UriResolver implements UriResolverInterface +{ + /** + * Parses a URI into five main components + * + * @param string $uri + * + * @return array + */ + public function parse($uri) + { + preg_match('|^(([^:/?#]+):)?(//([^/?#]*))?([^?#]*)(\?([^#]*))?(#(.*))?|', (string) $uri, $match); + + $components = []; + if (5 < count($match)) { + $components = [ + 'scheme' => $match[2], + 'authority' => $match[4], + 'path' => $match[5] + ]; + } + if (7 < count($match)) { + $components['query'] = $match[7]; + } + if (9 < count($match)) { + $components['fragment'] = $match[9]; + } + + return $components; + } + + /** + * Builds a URI based on n array with the main components + * + * @param array $components + * + * @return string + */ + public function generate(array $components) + { + $uri = $components['scheme'] . '://' + . $components['authority'] + . $components['path']; + + if (array_key_exists('query', $components) && strlen($components['query'])) { + $uri .= '?' . $components['query']; + } + if (array_key_exists('fragment', $components)) { + $uri .= '#' . $components['fragment']; + } + + return $uri; + } + + /** + * {@inheritdoc} + */ + public function resolve($uri, $baseUri = null) + { + // treat non-uri base as local file path + if ( + !is_null($baseUri) && + !filter_var($baseUri, \FILTER_VALIDATE_URL) && + !preg_match('|^[^/]+://|u', $baseUri) + ) { + if (is_file($baseUri)) { + $baseUri = 'file://' . realpath($baseUri); + } elseif (is_dir($baseUri)) { + $baseUri = 'file://' . realpath($baseUri) . '/'; + } else { + $baseUri = 'file://' . getcwd() . '/' . $baseUri; + } + } + + if ($uri == '') { + return $baseUri; + } + + $components = $this->parse($uri); + $path = $components['path']; + + if (!empty($components['scheme'])) { + return $uri; + } + $baseComponents = $this->parse($baseUri); + $basePath = $baseComponents['path']; + + $baseComponents['path'] = self::combineRelativePathWithBasePath($path, $basePath); + if (isset($components['fragment'])) { + $baseComponents['fragment'] = $components['fragment']; + } + + return $this->generate($baseComponents); + } + + /** + * Tries to glue a relative path onto an absolute one + * + * @param string $relativePath + * @param string $basePath + * + * @throws UriResolverException + * + * @return string Merged path + */ + public static function combineRelativePathWithBasePath($relativePath, $basePath) + { + $relativePath = self::normalizePath($relativePath); + if (!$relativePath) { + return $basePath; + } + if ($relativePath[0] === '/') { + return $relativePath; + } + if (!$basePath) { + throw new UriResolverException(sprintf("Unable to resolve URI '%s' from base '%s'", $relativePath, $basePath)); + } + + $dirname = $basePath[strlen($basePath) - 1] === '/' ? $basePath : dirname($basePath); + $combined = rtrim($dirname, '/') . '/' . ltrim($relativePath, '/'); + $combinedSegments = explode('/', $combined); + $collapsedSegments = []; + while ($combinedSegments) { + $segment = array_shift($combinedSegments); + if ($segment === '..') { + if (count($collapsedSegments) <= 1) { + // Do not remove the top level (domain) + // This is not ideal - the domain should not be part of the path here. parse() and generate() + // should handle the "domain" separately, like the schema. + // Then the if-condition here would be `if (!$collapsedSegments) {`. + throw new UriResolverException(sprintf("Unable to resolve URI '%s' from base '%s'", $relativePath, $basePath)); + } + array_pop($collapsedSegments); + } else { + $collapsedSegments[] = $segment; + } + } + + return implode('/', $collapsedSegments); + } + + /** + * Normalizes a URI path component by removing dot-slash and double slashes + * + * @param string $path + * + * @return string + */ + private static function normalizePath($path) + { + $path = preg_replace('|((?parse($uri); + + return !empty($components); + } +} diff --git a/3rdparty/justinrainbow/json-schema/src/JsonSchema/Uri/UriRetriever.php b/3rdparty/justinrainbow/json-schema/src/JsonSchema/Uri/UriRetriever.php new file mode 100644 index 00000000..4094cccc --- /dev/null +++ b/3rdparty/justinrainbow/json-schema/src/JsonSchema/Uri/UriRetriever.php @@ -0,0 +1,351 @@ + + */ +class UriRetriever implements BaseUriRetrieverInterface +{ + /** + * @var array Map of URL translations + */ + protected $translationMap = [ + // use local copies of the spec schemas + '|^https?://json-schema.org/draft-(0[34])/schema#?|' => 'package://dist/schema/json-schema-draft-$1.json' + ]; + + /** + * @var array A list of endpoints for media type check exclusion + */ + protected $allowedInvalidContentTypeEndpoints = [ + 'http://json-schema.org/', + 'https://json-schema.org/' + ]; + + /** + * @var null|UriRetrieverInterface + */ + protected $uriRetriever = null; + + /** + * @var array|object[] + * + * @see loadSchema + */ + private $schemaCache = []; + + /** + * Adds an endpoint to the media type validation exclusion list + * + * @param string $endpoint + */ + public function addInvalidContentTypeEndpoint($endpoint) + { + $this->allowedInvalidContentTypeEndpoints[] = $endpoint; + } + + /** + * Guarantee the correct media type was encountered + * + * @param UriRetrieverInterface $uriRetriever + * @param string $uri + * + * @return bool|void + */ + public function confirmMediaType($uriRetriever, $uri) + { + $contentType = $uriRetriever->getContentType(); + + if (is_null($contentType)) { + // Well, we didn't get an invalid one + return; + } + + if (in_array($contentType, [Validator::SCHEMA_MEDIA_TYPE, 'application/json'])) { + return; + } + + foreach ($this->allowedInvalidContentTypeEndpoints as $endpoint) { + if (!\is_null($uri) && strpos($uri, $endpoint) === 0) { + return true; + } + } + + throw new InvalidSchemaMediaTypeException(sprintf('Media type %s expected', Validator::SCHEMA_MEDIA_TYPE)); + } + + /** + * Get a URI Retriever + * + * If none is specified, sets a default FileGetContents retriever and + * returns that object. + * + * @return UriRetrieverInterface + */ + public function getUriRetriever() + { + if (is_null($this->uriRetriever)) { + $this->setUriRetriever(new FileGetContents()); + } + + return $this->uriRetriever; + } + + /** + * Resolve a schema based on pointer + * + * URIs can have a fragment at the end in the format of + * #/path/to/object and we are to look up the 'path' property of + * the first object then the 'to' and 'object' properties. + * + * @param object $jsonSchema JSON Schema contents + * @param string $uri JSON Schema URI + * + * @throws ResourceNotFoundException + * + * @return object JSON Schema after walking down the fragment pieces + */ + public function resolvePointer($jsonSchema, $uri) + { + $resolver = new UriResolver(); + $parsed = $resolver->parse($uri); + if (empty($parsed['fragment'])) { + return $jsonSchema; + } + + $path = explode('/', $parsed['fragment']); + while ($path) { + $pathElement = array_shift($path); + if (!empty($pathElement)) { + $pathElement = str_replace('~1', '/', $pathElement); + $pathElement = str_replace('~0', '~', $pathElement); + if (!empty($jsonSchema->$pathElement)) { + $jsonSchema = $jsonSchema->$pathElement; + } else { + throw new ResourceNotFoundException( + 'Fragment "' . $parsed['fragment'] . '" not found' + . ' in ' . $uri + ); + } + + if (!is_object($jsonSchema)) { + throw new ResourceNotFoundException( + 'Fragment part "' . $pathElement . '" is no object ' + . ' in ' . $uri + ); + } + } + } + + return $jsonSchema; + } + + /** + * {@inheritdoc} + */ + public function retrieve($uri, $baseUri = null, $translate = true) + { + $resolver = new UriResolver(); + $resolvedUri = $fetchUri = $resolver->resolve($uri, $baseUri); + + //fetch URL without #fragment + $arParts = $resolver->parse($resolvedUri); + if (isset($arParts['fragment'])) { + unset($arParts['fragment']); + $fetchUri = $resolver->generate($arParts); + } + + // apply URI translations + if ($translate) { + $fetchUri = $this->translate($fetchUri); + } + + $jsonSchema = $this->loadSchema($fetchUri); + + // Use the JSON pointer if specified + $jsonSchema = $this->resolvePointer($jsonSchema, $resolvedUri); + + if ($jsonSchema instanceof \stdClass) { + $jsonSchema->id = $resolvedUri; + } + + return $jsonSchema; + } + + /** + * Fetch a schema from the given URI, json-decode it and return it. + * Caches schema objects. + * + * @param string $fetchUri Absolute URI + * + * @return object JSON schema object + */ + protected function loadSchema($fetchUri) + { + if (isset($this->schemaCache[$fetchUri])) { + return $this->schemaCache[$fetchUri]; + } + + $uriRetriever = $this->getUriRetriever(); + $contents = $this->uriRetriever->retrieve($fetchUri); + $this->confirmMediaType($uriRetriever, $fetchUri); + $jsonSchema = json_decode($contents); + + if (JSON_ERROR_NONE < $error = json_last_error()) { + throw new JsonDecodingException($error); + } + + $this->schemaCache[$fetchUri] = $jsonSchema; + + return $jsonSchema; + } + + /** + * Set the URI Retriever + * + * @param UriRetrieverInterface $uriRetriever + * + * @return $this for chaining + */ + public function setUriRetriever(UriRetrieverInterface $uriRetriever) + { + $this->uriRetriever = $uriRetriever; + + return $this; + } + + /** + * Parses a URI into five main components + * + * @param string $uri + * + * @return array + */ + public function parse($uri) + { + preg_match('|^(([^:/?#]+):)?(//([^/?#]*))?([^?#]*)(\?([^#]*))?(#(.*))?|', $uri, $match); + + $components = []; + if (5 < count($match)) { + $components = [ + 'scheme' => $match[2], + 'authority' => $match[4], + 'path' => $match[5] + ]; + } + + if (7 < count($match)) { + $components['query'] = $match[7]; + } + + if (9 < count($match)) { + $components['fragment'] = $match[9]; + } + + return $components; + } + + /** + * Builds a URI based on n array with the main components + * + * @param array $components + * + * @return string + */ + public function generate(array $components) + { + $uri = $components['scheme'] . '://' + . $components['authority'] + . $components['path']; + + if (array_key_exists('query', $components)) { + $uri .= $components['query']; + } + + if (array_key_exists('fragment', $components)) { + $uri .= $components['fragment']; + } + + return $uri; + } + + /** + * Resolves a URI + * + * @param string $uri Absolute or relative + * @param string $baseUri Optional base URI + * + * @return string + */ + public function resolve($uri, $baseUri = null) + { + $components = $this->parse($uri); + $path = $components['path']; + + if ((array_key_exists('scheme', $components)) && ('http' === $components['scheme'])) { + return $uri; + } + + $baseComponents = $this->parse($baseUri); + $basePath = $baseComponents['path']; + + $baseComponents['path'] = UriResolver::combineRelativePathWithBasePath($path, $basePath); + + return $this->generate($baseComponents); + } + + /** + * @param string $uri + * + * @return bool + */ + public function isValid($uri) + { + $components = $this->parse($uri); + + return !empty($components); + } + + /** + * Set a URL translation rule + */ + public function setTranslation($from, $to) + { + $this->translationMap[$from] = $to; + } + + /** + * Apply URI translation rules + */ + public function translate($uri) + { + foreach ($this->translationMap as $from => $to) { + $uri = preg_replace($from, $to, $uri); + } + + // translate references to local files within the json-schema package + $uri = preg_replace('|^package://|', sprintf('file://%s/', realpath(__DIR__ . '/../../..')), $uri); + + return $uri; + } +} diff --git a/3rdparty/justinrainbow/json-schema/src/JsonSchema/UriResolverInterface.php b/3rdparty/justinrainbow/json-schema/src/JsonSchema/UriResolverInterface.php new file mode 100644 index 00000000..e80e2be7 --- /dev/null +++ b/3rdparty/justinrainbow/json-schema/src/JsonSchema/UriResolverInterface.php @@ -0,0 +1,28 @@ + + * @author Bruno Prieto Reis + * + * @see README.md + */ +class Validator extends BaseConstraint +{ + public const SCHEMA_MEDIA_TYPE = 'application/schema+json'; + + public const ERROR_NONE = 0; + public const ERROR_ALL = -1; + public const ERROR_DOCUMENT_VALIDATION = 1; + public const ERROR_SCHEMA_VALIDATION = 2; + + /** + * Validates the given data against the schema and returns an object containing the results + * Both the php object and the schema are supposed to be a result of a json_decode call. + * The validation works as defined by the schema proposal in http://json-schema.org. + * + * Note that the first argument is passed by reference, so you must pass in a variable. + * + * @param mixed $value + * @param mixed $schema + * + * @phpstan-param int-mask-of $checkMode + * @phpstan-return int-mask-of + */ + public function validate(&$value, $schema = null, ?int $checkMode = null): int + { + // reset errors prior to validation + $this->reset(); + + // set checkMode + $initialCheckMode = $this->factory->getConfig(); + if ($checkMode !== null) { + $this->factory->setConfig($checkMode); + } + + // add provided schema to SchemaStorage with internal URI to allow internal $ref resolution + $schemaURI = SchemaStorage::INTERNAL_PROVIDED_SCHEMA_URI; + if (LooseTypeCheck::propertyExists($schema, 'id')) { + $schemaURI = LooseTypeCheck::propertyGet($schema, 'id'); + } + $this->factory->getSchemaStorage()->addSchema($schemaURI, $schema); + + $validator = $this->factory->createInstanceFor('schema'); + $validator->check( + $value, + $this->factory->getSchemaStorage()->getSchema($schemaURI) + ); + + $this->factory->setConfig($initialCheckMode); + + $this->addErrors(array_unique($validator->getErrors(), SORT_REGULAR)); + + return $validator->getErrorMask(); + } + + /** + * Alias to validate(), to maintain backwards-compatibility with the previous API + * + * @deprecated since 6.0.0, use Validator::validate() instead, to be removed in 7.0 + * + * @param mixed $value + * @param mixed $schema + * + * @phpstan-return int-mask-of + */ + public function check($value, $schema): int + { + return $this->validate($value, $schema); + } + + /** + * Alias to validate(), to maintain backwards-compatibility with the previous API + * + * @deprecated since 6.0.0, use Validator::validate() instead, to be removed in 7.0 + * + * @param mixed $value + * @param mixed $schema + * + * @phpstan-return int-mask-of + */ + public function coerce(&$value, $schema): int + { + return $this->validate($value, $schema, Constraint::CHECK_MODE_COERCE_TYPES); + } +} diff --git a/3rdparty/kornrunner/blurhash/LICENSE b/3rdparty/kornrunner/blurhash/LICENSE new file mode 100644 index 00000000..11cc9da4 --- /dev/null +++ b/3rdparty/kornrunner/blurhash/LICENSE @@ -0,0 +1,21 @@ +MIT License + +Copyright (c) 2019 Boris Momčilović + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. diff --git a/3rdparty/kornrunner/blurhash/src/AC.php b/3rdparty/kornrunner/blurhash/src/AC.php new file mode 100644 index 00000000..f2bdcbd0 --- /dev/null +++ b/3rdparty/kornrunner/blurhash/src/AC.php @@ -0,0 +1,34 @@ + 0; + return $sign * pow(abs($base), $exp); + } +} \ No newline at end of file diff --git a/3rdparty/kornrunner/blurhash/src/Base83.php b/3rdparty/kornrunner/blurhash/src/Base83.php new file mode 100644 index 00000000..3d891f9a --- /dev/null +++ b/3rdparty/kornrunner/blurhash/src/Base83.php @@ -0,0 +1,39 @@ + 9) || ($components_y < 1 || $components_y > 9)) { + throw new InvalidArgumentException("x and y component counts must be between 1 and 9 inclusive."); + } + $height = count($image); + $width = count($image[0]); + + $image_linear = $image; + if (!$linear) { + $image_linear = []; + for ($y = 0; $y < $height; $y++) { + $line = []; + for ($x = 0; $x < $width; $x++) { + $pixel = $image[$y][$x]; + $line[] = [ + Color::toLinear($pixel[0]), + Color::toLinear($pixel[1]), + Color::toLinear($pixel[2]) + ]; + } + $image_linear[] = $line; + } + } + + $components = []; + $scale = 1 / ($width * $height); + for ($y = 0; $y < $components_y; $y++) { + for ($x = 0; $x < $components_x; $x++) { + $normalisation = $x == 0 && $y == 0 ? 1 : 2; + $r = $g = $b = 0; + for ($i = 0; $i < $width; $i++) { + for ($j = 0; $j < $height; $j++) { + $color = $image_linear[$j][$i]; + $basis = $normalisation + * cos(M_PI * $i * $x / $width) + * cos(M_PI * $j * $y / $height); + + $r += $basis * $color[0]; + $g += $basis * $color[1]; + $b += $basis * $color[2]; + } + } + + $components[] = [ + $r * $scale, + $g * $scale, + $b * $scale + ]; + } + } + + $dc_value = DC::encode(array_shift($components) ?: []); + + $max_ac_component = 0; + foreach ($components as $component) { + $component[] = $max_ac_component; + $max_ac_component = max ($component); + } + + $quant_max_ac_component = (int) max(0, min(82, floor($max_ac_component * 166 - 0.5))); + $ac_component_norm_factor = ($quant_max_ac_component + 1) / 166; + + $ac_values = []; + foreach ($components as $component) { + $ac_values[] = AC::encode($component, $ac_component_norm_factor); + } + + $blurhash = Base83::encode($components_x - 1 + ($components_y - 1) * 9, 1); + $blurhash .= Base83::encode($quant_max_ac_component, 1); + $blurhash .= Base83::encode($dc_value, 4); + foreach ($ac_values as $ac_value) { + $blurhash .= Base83::encode((int) $ac_value, 2); + } + + return $blurhash; + } + + public static function decode (string $blurhash, int $width, int $height, float $punch = 1.0, bool $linear = false): array { + if (empty($blurhash) || strlen($blurhash) < 6) { + throw new InvalidArgumentException("Blurhash string must be at least 6 characters"); + } + + $size_info = Base83::decode($blurhash[0]); + $size_y = intdiv($size_info, 9) + 1; + $size_x = ($size_info % 9) + 1; + + $length = strlen($blurhash); + $expected_length = (int) (4 + (2 * $size_y * $size_x)); + if ($length !== $expected_length) { + throw new InvalidArgumentException("Blurhash length mismatch: length is {$length} but it should be {$expected_length}"); + } + + $colors = [DC::decode(Base83::decode(substr($blurhash, 2, 4)))]; + + $quant_max_ac_component = Base83::decode($blurhash[1]); + $max_value = ($quant_max_ac_component + 1) / 166; + for ($i = 1; $i < $size_x * $size_y; $i++) { + $value = Base83::decode(substr($blurhash, 4 + $i * 2, 2)); + $colors[$i] = AC::decode($value, $max_value * $punch); + } + + $pixels = []; + for ($y = 0; $y < $height; $y++) { + $row = []; + for ($x = 0; $x < $width; $x++) { + $r = $g = $b = 0; + for ($j = 0; $j < $size_y; $j++) { + for ($i = 0; $i < $size_x; $i++) { + $color = $colors[$i + $j * $size_x]; + $basis = + cos((M_PI * $x * $i) / $width) * + cos((M_PI * $y * $j) / $height); + + $r += $color[0] * $basis; + $g += $color[1] * $basis; + $b += $color[2] * $basis; + } + } + + $row[] = $linear ? [$r, $g, $b] : [ + Color::toSRGB($r), + Color::toSRGB($g), + Color::toSRGB($b) + ]; + } + $pixels[] = $row; + } + + return $pixels; + } +} diff --git a/3rdparty/kornrunner/blurhash/src/Color.php b/3rdparty/kornrunner/blurhash/src/Color.php new file mode 100644 index 00000000..13e5b3e5 --- /dev/null +++ b/3rdparty/kornrunner/blurhash/src/Color.php @@ -0,0 +1,20 @@ +> 16; + $g = ($value >> 8) & 255; + $b = $value & 255; + return [ + Color::toLinear($r), + Color::toLinear($g), + Color::toLinear($b) + ]; + } +} \ No newline at end of file diff --git a/3rdparty/laravel/serializable-closure/LICENSE.md b/3rdparty/laravel/serializable-closure/LICENSE.md new file mode 100644 index 00000000..79810c84 --- /dev/null +++ b/3rdparty/laravel/serializable-closure/LICENSE.md @@ -0,0 +1,21 @@ +The MIT License (MIT) + +Copyright (c) Taylor Otwell + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in +all copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +THE SOFTWARE. diff --git a/3rdparty/laravel/serializable-closure/src/Contracts/Serializable.php b/3rdparty/laravel/serializable-closure/src/Contracts/Serializable.php new file mode 100644 index 00000000..1a62922e --- /dev/null +++ b/3rdparty/laravel/serializable-closure/src/Contracts/Serializable.php @@ -0,0 +1,20 @@ +serializable = Serializers\Signed::$signer + ? new Serializers\Signed($closure) + : new Serializers\Native($closure); + } + + /** + * Resolve the closure with the given arguments. + * + * @return mixed + */ + public function __invoke() + { + return call_user_func_array($this->serializable, func_get_args()); + } + + /** + * Gets the closure. + * + * @return \Closure + */ + public function getClosure() + { + return $this->serializable->getClosure(); + } + + /** + * Create a new unsigned serializable closure instance. + * + * @param Closure $closure + * @return \Laravel\SerializableClosure\UnsignedSerializableClosure + */ + public static function unsigned(Closure $closure) + { + return new UnsignedSerializableClosure($closure); + } + + /** + * Sets the serializable closure secret key. + * + * @param string|null $secret + * @return void + */ + public static function setSecretKey($secret) + { + Serializers\Signed::$signer = $secret + ? new Hmac($secret) + : null; + } + + /** + * Sets the serializable closure secret key. + * + * @param \Closure|null $transformer + * @return void + */ + public static function transformUseVariablesUsing($transformer) + { + Serializers\Native::$transformUseVariables = $transformer; + } + + /** + * Sets the serializable closure secret key. + * + * @param \Closure|null $resolver + * @return void + */ + public static function resolveUseVariablesUsing($resolver) + { + Serializers\Native::$resolveUseVariables = $resolver; + } + + /** + * Get the serializable representation of the closure. + * + * @return array{serializable: \Laravel\SerializableClosure\Serializers\Signed|\Laravel\SerializableClosure\Contracts\Serializable} + */ + public function __serialize() + { + return [ + 'serializable' => $this->serializable, + ]; + } + + /** + * Restore the closure after serialization. + * + * @param array{serializable: \Laravel\SerializableClosure\Serializers\Signed|\Laravel\SerializableClosure\Contracts\Serializable} $data + * @return void + * + * @throws \Laravel\SerializableClosure\Exceptions\InvalidSignatureException + */ + public function __unserialize($data) + { + if (Signed::$signer && ! $data['serializable'] instanceof Signed) { + throw new InvalidSignatureException(); + } + + $this->serializable = $data['serializable']; + } +} diff --git a/3rdparty/laravel/serializable-closure/src/Serializers/Native.php b/3rdparty/laravel/serializable-closure/src/Serializers/Native.php new file mode 100644 index 00000000..6fe884bb --- /dev/null +++ b/3rdparty/laravel/serializable-closure/src/Serializers/Native.php @@ -0,0 +1,526 @@ +closure = $closure; + } + + /** + * Resolve the closure with the given arguments. + * + * @return mixed + */ + public function __invoke() + { + return call_user_func_array($this->closure, func_get_args()); + } + + /** + * Gets the closure. + * + * @return \Closure + */ + public function getClosure() + { + return $this->closure; + } + + /** + * Get the serializable representation of the closure. + * + * @return array + */ + public function __serialize() + { + if ($this->scope === null) { + $this->scope = new ClosureScope(); + $this->scope->toSerialize++; + } + + $this->scope->serializations++; + + $scope = $object = null; + $reflector = $this->getReflector(); + + if ($reflector->isBindingRequired()) { + $object = $reflector->getClosureThis(); + + static::wrapClosures($object, $this->scope); + } + + if ($scope = $reflector->getClosureScopeClass()) { + $scope = $scope->name; + } + + $this->reference = spl_object_hash($this->closure); + + $this->scope[$this->closure] = $this; + + $use = $reflector->getUseVariables(); + + if (static::$transformUseVariables) { + $use = call_user_func(static::$transformUseVariables, $reflector->getUseVariables()); + } + + $code = $reflector->getCode(); + + $this->mapByReference($use); + + $data = [ + 'use' => $use, + 'function' => $code, + 'scope' => $scope, + 'this' => $object, + 'self' => $this->reference, + ]; + + if (! --$this->scope->serializations && ! --$this->scope->toSerialize) { + $this->scope = null; + } + + return $data; + } + + /** + * Restore the closure after serialization. + * + * @param array $data + * @return void + */ + public function __unserialize($data) + { + ClosureStream::register(); + + $this->code = $data; + unset($data); + + $this->code['objects'] = []; + + if ($this->code['use']) { + $this->scope = new ClosureScope(); + + if (static::$resolveUseVariables) { + $this->code['use'] = call_user_func(static::$resolveUseVariables, $this->code['use']); + } + + $this->mapPointers($this->code['use']); + + extract($this->code['use'], EXTR_OVERWRITE | EXTR_REFS); + + $this->scope = null; + } + + $this->closure = include ClosureStream::STREAM_PROTO.'://'.$this->code['function']; + + if ($this->code['this'] === $this) { + $this->code['this'] = null; + } + + $this->closure = $this->closure->bindTo($this->code['this'], $this->code['scope']); + + if (! empty($this->code['objects'])) { + foreach ($this->code['objects'] as $item) { + $item['property']->setValue($item['instance'], $item['object']->getClosure()); + } + } + + $this->code = $this->code['function']; + } + + /** + * Ensures the given closures are serializable. + * + * @param mixed $data + * @param \Laravel\SerializableClosure\Support\ClosureScope $storage + * @return void + */ + public static function wrapClosures(&$data, $storage) + { + if ($data instanceof Closure) { + $data = new static($data); + } elseif (is_array($data)) { + if (isset($data[self::ARRAY_RECURSIVE_KEY])) { + return; + } + + $data[self::ARRAY_RECURSIVE_KEY] = true; + + foreach ($data as $key => &$value) { + if ($key === self::ARRAY_RECURSIVE_KEY) { + continue; + } + static::wrapClosures($value, $storage); + } + + unset($value); + unset($data[self::ARRAY_RECURSIVE_KEY]); + } elseif ($data instanceof \stdClass) { + if (isset($storage[$data])) { + $data = $storage[$data]; + + return; + } + + $data = $storage[$data] = clone $data; + + foreach ($data as &$value) { + static::wrapClosures($value, $storage); + } + + unset($value); + } elseif (is_object($data) && ! $data instanceof static && ! $data instanceof UnitEnum) { + if (isset($storage[$data])) { + $data = $storage[$data]; + + return; + } + + $instance = $data; + $reflection = new ReflectionObject($instance); + + if (! $reflection->isUserDefined()) { + $storage[$instance] = $data; + + return; + } + + $storage[$instance] = $data = $reflection->newInstanceWithoutConstructor(); + + do { + if (! $reflection->isUserDefined()) { + break; + } + + foreach ($reflection->getProperties() as $property) { + if ($property->isStatic() || ! $property->getDeclaringClass()->isUserDefined()) { + continue; + } + + $property->setAccessible(true); + + if (! $property->isInitialized($instance)) { + continue; + } + + $value = $property->getValue($instance); + + if (is_array($value) || is_object($value)) { + static::wrapClosures($value, $storage); + } + + $property->setValue($data, $value); + } + } while ($reflection = $reflection->getParentClass()); + } + } + + /** + * Gets the closure's reflector. + * + * @return \Laravel\SerializableClosure\Support\ReflectionClosure + */ + public function getReflector() + { + if ($this->reflector === null) { + $this->code = null; + $this->reflector = new ReflectionClosure($this->closure); + } + + return $this->reflector; + } + + /** + * Internal method used to map closure pointers. + * + * @param mixed $data + * @return void + */ + protected function mapPointers(&$data) + { + $scope = $this->scope; + + if ($data instanceof static) { + $data = &$data->closure; + } elseif (is_array($data)) { + if (isset($data[self::ARRAY_RECURSIVE_KEY])) { + return; + } + + $data[self::ARRAY_RECURSIVE_KEY] = true; + + foreach ($data as $key => &$value) { + if ($key === self::ARRAY_RECURSIVE_KEY) { + continue; + } elseif ($value instanceof static) { + $data[$key] = &$value->closure; + } elseif ($value instanceof SelfReference && $value->hash === $this->code['self']) { + $data[$key] = &$this->closure; + } else { + $this->mapPointers($value); + } + } + + unset($value); + unset($data[self::ARRAY_RECURSIVE_KEY]); + } elseif ($data instanceof \stdClass) { + if (isset($scope[$data])) { + return; + } + + $scope[$data] = true; + + foreach ($data as $key => &$value) { + if ($value instanceof SelfReference && $value->hash === $this->code['self']) { + $data->{$key} = &$this->closure; + } elseif (is_array($value) || is_object($value)) { + $this->mapPointers($value); + } + } + + unset($value); + } elseif (is_object($data) && ! ($data instanceof Closure)) { + if (isset($scope[$data])) { + return; + } + + $scope[$data] = true; + $reflection = new ReflectionObject($data); + + do { + if (! $reflection->isUserDefined()) { + break; + } + + foreach ($reflection->getProperties() as $property) { + if ($property->isStatic() || ! $property->getDeclaringClass()->isUserDefined()) { + continue; + } + + $property->setAccessible(true); + + if (! $property->isInitialized($data) || $property->isReadOnly()) { + continue; + } + + $item = $property->getValue($data); + + if ($item instanceof SerializableClosure || $item instanceof UnsignedSerializableClosure || ($item instanceof SelfReference && $item->hash === $this->code['self'])) { + $this->code['objects'][] = [ + 'instance' => $data, + 'property' => $property, + 'object' => $item instanceof SelfReference ? $this : $item, + ]; + } elseif (is_array($item) || is_object($item)) { + $this->mapPointers($item); + $property->setValue($data, $item); + } + } + } while ($reflection = $reflection->getParentClass()); + } + } + + /** + * Internal method used to map closures by reference. + * + * @param mixed $data + * @return void + */ + protected function mapByReference(&$data) + { + if ($data instanceof Closure) { + if ($data === $this->closure) { + $data = new SelfReference($this->reference); + + return; + } + + if (isset($this->scope[$data])) { + $data = $this->scope[$data]; + + return; + } + + $instance = new static($data); + + $instance->scope = $this->scope; + + $data = $this->scope[$data] = $instance; + } elseif (is_array($data)) { + if (isset($data[self::ARRAY_RECURSIVE_KEY])) { + return; + } + + $data[self::ARRAY_RECURSIVE_KEY] = true; + + foreach ($data as $key => &$value) { + if ($key === self::ARRAY_RECURSIVE_KEY) { + continue; + } + + $this->mapByReference($value); + } + + unset($value); + unset($data[self::ARRAY_RECURSIVE_KEY]); + } elseif ($data instanceof \stdClass) { + if (isset($this->scope[$data])) { + $data = $this->scope[$data]; + + return; + } + + $instance = $data; + $this->scope[$instance] = $data = clone $data; + + foreach ($data as &$value) { + $this->mapByReference($value); + } + + unset($value); + } elseif (is_object($data) && ! $data instanceof SerializableClosure && ! $data instanceof UnsignedSerializableClosure) { + if (isset($this->scope[$data])) { + $data = $this->scope[$data]; + + return; + } + + $instance = $data; + + if ($data instanceof DateTimeInterface) { + $this->scope[$instance] = $data; + + return; + } + + if ($data instanceof UnitEnum) { + $this->scope[$instance] = $data; + + return; + } + + $reflection = new ReflectionObject($data); + + if (! $reflection->isUserDefined()) { + $this->scope[$instance] = $data; + + return; + } + + $this->scope[$instance] = $data = $reflection->newInstanceWithoutConstructor(); + + do { + if (! $reflection->isUserDefined()) { + break; + } + + foreach ($reflection->getProperties() as $property) { + if ($property->isStatic() || ! $property->getDeclaringClass()->isUserDefined() || $this->isVirtualProperty($property)) { + continue; + } + + $property->setAccessible(true); + + if (! $property->isInitialized($instance) || ($property->isReadOnly() && $property->class !== $reflection->name)) { + continue; + } + + $value = $property->getValue($instance); + + if (is_array($value) || is_object($value)) { + $this->mapByReference($value); + } + + $property->setValue($data, $value); + } + } while ($reflection = $reflection->getParentClass()); + } + } + + /** + * Determine is virtual property. + * + * @param \ReflectionProperty $property + * @return bool + */ + protected function isVirtualProperty(ReflectionProperty $property): bool + { + return method_exists($property, 'isVirtual') && $property->isVirtual(); + } +} diff --git a/3rdparty/laravel/serializable-closure/src/Serializers/Signed.php b/3rdparty/laravel/serializable-closure/src/Serializers/Signed.php new file mode 100644 index 00000000..16f9593f --- /dev/null +++ b/3rdparty/laravel/serializable-closure/src/Serializers/Signed.php @@ -0,0 +1,91 @@ +closure = $closure; + } + + /** + * Resolve the closure with the given arguments. + * + * @return mixed + */ + public function __invoke() + { + return call_user_func_array($this->closure, func_get_args()); + } + + /** + * Gets the closure. + * + * @return \Closure + */ + public function getClosure() + { + return $this->closure; + } + + /** + * Get the serializable representation of the closure. + * + * @return array + */ + public function __serialize() + { + if (! static::$signer) { + throw new MissingSecretKeyException(); + } + + return static::$signer->sign( + serialize(new Native($this->closure)) + ); + } + + /** + * Restore the closure after serialization. + * + * @param array{serializable: string, hash: string} $signature + * @return void + * + * @throws \Laravel\SerializableClosure\Exceptions\InvalidSignatureException + */ + public function __unserialize($signature) + { + if (static::$signer && ! static::$signer->verify($signature)) { + throw new InvalidSignatureException(); + } + + /** @var \Laravel\SerializableClosure\Contracts\Serializable $serializable */ + $serializable = unserialize($signature['serializable']); + + $this->closure = $serializable->getClosure(); + } +} diff --git a/3rdparty/laravel/serializable-closure/src/Signers/Hmac.php b/3rdparty/laravel/serializable-closure/src/Signers/Hmac.php new file mode 100644 index 00000000..41ed01ae --- /dev/null +++ b/3rdparty/laravel/serializable-closure/src/Signers/Hmac.php @@ -0,0 +1,53 @@ +secret = $secret; + } + + /** + * Sign the given serializable. + * + * @param string $serialized + * @return array + */ + public function sign($serialized) + { + return [ + 'serializable' => $serialized, + 'hash' => base64_encode(hash_hmac('sha256', $serialized, $this->secret, true)), + ]; + } + + /** + * Verify the given signature. + * + * @param array{serializable: string, hash: string} $signature + * @return bool + */ + public function verify($signature) + { + return hash_equals(base64_encode( + hash_hmac('sha256', $signature['serializable'], $this->secret, true) + ), $signature['hash']); + } +} diff --git a/3rdparty/laravel/serializable-closure/src/Support/ClosureScope.php b/3rdparty/laravel/serializable-closure/src/Support/ClosureScope.php new file mode 100644 index 00000000..64ca19a3 --- /dev/null +++ b/3rdparty/laravel/serializable-closure/src/Support/ClosureScope.php @@ -0,0 +1,22 @@ +content = "length = strlen($this->content); + + return true; + } + + /** + * Read from stream. + * + * @param int $count + * @return string + */ + public function stream_read($count) + { + $value = substr($this->content, $this->pointer, $count); + + $this->pointer += $count; + + return $value; + } + + /** + * Tests for end-of-file on a file pointer. + * + * @return bool + */ + public function stream_eof() + { + return $this->pointer >= $this->length; + } + + /** + * Change stream options. + * + * @param int $option + * @param int $arg1 + * @param int $arg2 + * @return bool + */ + public function stream_set_option($option, $arg1, $arg2) + { + return false; + } + + /** + * Retrieve information about a file resource. + * + * @return array|bool + */ + public function stream_stat() + { + $stat = stat(__FILE__); + // @phpstan-ignore-next-line + $stat[7] = $stat['size'] = $this->length; + + return $stat; + } + + /** + * Retrieve information about a file. + * + * @param string $path + * @param int $flags + * @return array|bool + */ + public function url_stat($path, $flags) + { + $stat = stat(__FILE__); + // @phpstan-ignore-next-line + $stat[7] = $stat['size'] = $this->length; + + return $stat; + } + + /** + * Seeks to specific location in a stream. + * + * @param int $offset + * @param int $whence + * @return bool + */ + public function stream_seek($offset, $whence = SEEK_SET) + { + $crt = $this->pointer; + + switch ($whence) { + case SEEK_SET: + $this->pointer = $offset; + break; + case SEEK_CUR: + $this->pointer += $offset; + break; + case SEEK_END: + $this->pointer = $this->length + $offset; + break; + } + + if ($this->pointer < 0 || $this->pointer >= $this->length) { + $this->pointer = $crt; + + return false; + } + + return true; + } + + /** + * Retrieve the current position of a stream. + * + * @return int + */ + public function stream_tell() + { + return $this->pointer; + } + + /** + * Registers the stream. + * + * @return void + */ + public static function register() + { + if (! static::$isRegistered) { + static::$isRegistered = stream_wrapper_register(static::STREAM_PROTO, __CLASS__); + } + } +} diff --git a/3rdparty/laravel/serializable-closure/src/Support/ReflectionClosure.php b/3rdparty/laravel/serializable-closure/src/Support/ReflectionClosure.php new file mode 100644 index 00000000..fb90bef0 --- /dev/null +++ b/3rdparty/laravel/serializable-closure/src/Support/ReflectionClosure.php @@ -0,0 +1,1243 @@ +isStaticClosure === null) { + $this->isStaticClosure = strtolower(substr($this->getCode(), 0, 6)) === 'static'; + } + + return $this->isStaticClosure; + } + + /** + * Checks if the closure is a "short closure". + * + * @return bool + */ + public function isShortClosure() + { + if ($this->isShortClosure === null) { + $code = $this->getCode(); + + if ($this->isStatic()) { + $code = substr($code, 6); + } + + $this->isShortClosure = strtolower(substr(trim($code), 0, 2)) === 'fn'; + } + + return $this->isShortClosure; + } + + /** + * Get the closure's code. + * + * @return string + */ + public function getCode() + { + if ($this->code !== null) { + return $this->code; + } + + $fileName = $this->getFileName(); + $line = $this->getStartLine() - 1; + + $className = null; + + if (null !== $className = $this->getClosureScopeClass()) { + $className = '\\'.trim($className->getName(), '\\'); + } + + $builtin_types = self::getBuiltinTypes(); + $class_keywords = ['self', 'static', 'parent']; + + $ns = $this->getClosureNamespaceName(); + $nsf = $ns == '' ? '' : ($ns[0] == '\\' ? $ns : '\\'.$ns); + + $_file = var_export($fileName, true); + $_dir = var_export(dirname($fileName), true); + $_namespace = var_export($ns, true); + $_class = var_export(trim($className ?: '', '\\'), true); + $_function = $ns.($ns == '' ? '' : '\\').'{closure}'; + $_method = ($className == '' ? '' : trim($className, '\\').'::').$_function; + $_function = var_export($_function, true); + $_method = var_export($_method, true); + $_trait = null; + + $tokens = $this->getTokens(); + $state = $lastState = 'start'; + $inside_structure = false; + $isFirstClassCallable = false; + $isShortClosure = false; + + $inside_structure_mark = 0; + $open = 0; + $code = ''; + $id_start = $id_start_ci = $id_name = $context = ''; + $classes = $functions = $constants = null; + $use = []; + $lineAdd = 0; + $isUsingScope = false; + $isUsingThisObject = false; + + for ($i = 0, $l = count($tokens); $i < $l; $i++) { + $token = $tokens[$i]; + + switch ($state) { + case 'start': + if ($token[0] === T_FUNCTION || $token[0] === T_STATIC) { + $code .= $token[1]; + + $state = $token[0] === T_FUNCTION ? 'function' : 'static'; + } elseif ($token[0] === T_FN) { + $isShortClosure = true; + $code .= $token[1]; + $state = 'closure_args'; + } elseif ($token[0] === T_PUBLIC || $token[0] === T_PROTECTED || $token[0] === T_PRIVATE) { + $code = ''; + $isFirstClassCallable = true; + } + break; + case 'static': + if ($token[0] === T_WHITESPACE || $token[0] === T_COMMENT || $token[0] === T_FUNCTION) { + $code .= $token[1]; + if ($token[0] === T_FUNCTION) { + $state = 'function'; + } + } elseif ($token[0] === T_FN) { + $isShortClosure = true; + $code .= $token[1]; + $state = 'closure_args'; + } else { + $code = ''; + $state = 'start'; + } + break; + case 'function': + switch ($token[0]) { + case T_STRING: + if ($isFirstClassCallable) { + $state = 'closure_args'; + break; + } + + $code = ''; + $state = 'named_function'; + break; + case '(': + $code .= '('; + $state = 'closure_args'; + break; + default: + $code .= is_array($token) ? $token[1] : $token; + } + break; + case 'named_function': + if ($token[0] === T_FUNCTION || $token[0] === T_STATIC) { + $code = $token[1]; + $state = $token[0] === T_FUNCTION ? 'function' : 'static'; + } elseif ($token[0] === T_FN) { + $isShortClosure = true; + $code .= $token[1]; + $state = 'closure_args'; + } + break; + case 'closure_args': + switch ($token[0]) { + case T_NAME_QUALIFIED: + [$id_start, $id_start_ci, $id_name] = $this->parseNameQualified($token[1]); + $context = 'args'; + $state = 'id_name'; + $lastState = 'closure_args'; + break; + case T_NS_SEPARATOR: + case T_STRING: + $id_start = $token[1]; + $id_start_ci = strtolower($id_start); + $id_name = ''; + $context = 'args'; + $state = 'id_name'; + $lastState = 'closure_args'; + break; + case T_USE: + $code .= $token[1]; + $state = 'use'; + break; + case T_DOUBLE_ARROW: + $code .= $token[1]; + if ($isShortClosure) { + $state = 'closure'; + } + break; + case ':': + $code .= ':'; + $state = 'return'; + break; + case '{': + $code .= '{'; + $state = 'closure'; + $open++; + break; + default: + $code .= is_array($token) ? $token[1] : $token; + } + break; + case 'use': + switch ($token[0]) { + case T_VARIABLE: + $use[] = substr($token[1], 1); + $code .= $token[1]; + break; + case '{': + $code .= '{'; + $state = 'closure'; + $open++; + break; + case ':': + $code .= ':'; + $state = 'return'; + break; + default: + $code .= is_array($token) ? $token[1] : $token; + break; + } + break; + case 'return': + switch ($token[0]) { + case T_WHITESPACE: + case T_COMMENT: + case T_DOC_COMMENT: + $code .= $token[1]; + break; + case T_NS_SEPARATOR: + case T_STRING: + $id_start = $token[1]; + $id_start_ci = strtolower($id_start); + $id_name = ''; + $context = 'return_type'; + $state = 'id_name'; + $lastState = 'return'; + break 2; + case T_NAME_QUALIFIED: + [$id_start, $id_start_ci, $id_name] = $this->parseNameQualified($token[1]); + $context = 'return_type'; + $state = 'id_name'; + $lastState = 'return'; + break 2; + case T_DOUBLE_ARROW: + $code .= $token[1]; + if ($isShortClosure) { + $state = 'closure'; + } + break; + case '{': + $code .= '{'; + $state = 'closure'; + $open++; + break; + default: + $code .= is_array($token) ? $token[1] : $token; + break; + } + break; + case 'closure': + switch ($token[0]) { + case T_CURLY_OPEN: + case T_DOLLAR_OPEN_CURLY_BRACES: + case '{': + $code .= is_array($token) ? $token[1] : $token; + $open++; + break; + case '}': + $code .= '}'; + if (--$open === 0 && ! $isShortClosure) { + break 3; + } elseif ($inside_structure) { + $inside_structure = ! ($open === $inside_structure_mark); + } + break; + case '(': + case '[': + $code .= $token[0]; + if ($isShortClosure) { + $open++; + } + break; + case ')': + case ']': + if ($isShortClosure) { + if ($open === 0) { + break 3; + } + $open--; + } + $code .= $token[0]; + break; + case ',': + case ';': + if ($isShortClosure && $open === 0) { + break 3; + } + $code .= $token[0]; + break; + case T_LINE: + $code .= $token[2] - $line + $lineAdd; + break; + case T_FILE: + $code .= $_file; + break; + case T_DIR: + $code .= $_dir; + break; + case T_NS_C: + $code .= $_namespace; + break; + case T_CLASS_C: + $code .= $inside_structure ? $token[1] : $_class; + break; + case T_FUNC_C: + $code .= $inside_structure ? $token[1] : $_function; + break; + case T_METHOD_C: + $code .= $inside_structure ? $token[1] : $_method; + break; + case T_COMMENT: + if (substr($token[1], 0, 8) === '#trackme') { + $timestamp = time(); + $code .= '/**'.PHP_EOL; + $code .= '* Date : '.date(DATE_W3C, $timestamp).PHP_EOL; + $code .= '* Timestamp : '.$timestamp.PHP_EOL; + $code .= '* Line : '.($line + 1).PHP_EOL; + $code .= '* File : '.$_file.PHP_EOL.'*/'.PHP_EOL; + $lineAdd += 5; + } else { + $code .= $token[1]; + } + break; + case T_VARIABLE: + if ($token[1] == '$this' && ! $inside_structure) { + $isUsingThisObject = true; + } + $code .= $token[1]; + break; + case T_STATIC: + case T_NS_SEPARATOR: + case T_STRING: + $id_start = $token[1]; + $id_start_ci = strtolower($id_start); + $id_name = ''; + $context = 'root'; + $state = 'id_name'; + $lastState = 'closure'; + break 2; + case T_NAME_QUALIFIED: + [$id_start, $id_start_ci, $id_name] = $this->parseNameQualified($token[1]); + $context = 'root'; + $state = 'id_name'; + $lastState = 'closure'; + break 2; + case T_NEW: + $code .= $token[1]; + $context = 'new'; + $state = 'id_start'; + $lastState = 'closure'; + break 2; + case T_USE: + $code .= $token[1]; + $context = 'use'; + $state = 'id_start'; + $lastState = 'closure'; + break; + case T_INSTANCEOF: + case T_INSTEADOF: + $code .= $token[1]; + $context = 'instanceof'; + $state = 'id_start'; + $lastState = 'closure'; + break; + case T_OBJECT_OPERATOR: + case T_NULLSAFE_OBJECT_OPERATOR: + case T_DOUBLE_COLON: + $code .= $token[1]; + $lastState = 'closure'; + $state = 'ignore_next'; + break; + case T_FUNCTION: + $code .= $token[1]; + $state = 'closure_args'; + if (! $inside_structure) { + $inside_structure = true; + $inside_structure_mark = $open; + } + break; + case T_TRAIT_C: + if ($_trait === null) { + $startLine = $this->getStartLine(); + $endLine = $this->getEndLine(); + $structures = $this->getStructures(); + + $_trait = ''; + + foreach ($structures as &$struct) { + if ($struct['type'] === 'trait' && + $struct['start'] <= $startLine && + $struct['end'] >= $endLine + ) { + $_trait = ($ns == '' ? '' : $ns.'\\').$struct['name']; + break; + } + } + + $_trait = var_export($_trait, true); + } + + $code .= $_trait; + break; + default: + $code .= is_array($token) ? $token[1] : $token; + } + break; + case 'ignore_next': + switch ($token[0]) { + case T_WHITESPACE: + case T_COMMENT: + case T_DOC_COMMENT: + $code .= $token[1]; + break; + case T_CLASS: + case T_NEW: + case T_STATIC: + case T_VARIABLE: + case T_STRING: + case T_CLASS_C: + case T_FILE: + case T_DIR: + case T_METHOD_C: + case T_FUNC_C: + case T_FUNCTION: + case T_INSTANCEOF: + case T_LINE: + case T_NS_C: + case T_TRAIT_C: + case T_USE: + $code .= $token[1]; + $state = $lastState; + break; + default: + $state = $lastState; + $i--; + } + break; + case 'id_start': + switch ($token[0]) { + case T_WHITESPACE: + case T_COMMENT: + case T_DOC_COMMENT: + $code .= $token[1]; + break; + case T_NS_SEPARATOR: + case T_NAME_FULLY_QUALIFIED: + case T_STRING: + case T_STATIC: + $id_start = $token[1]; + $id_start_ci = strtolower($id_start); + $id_name = ''; + $state = 'id_name'; + break 2; + case T_NAME_QUALIFIED: + [$id_start, $id_start_ci, $id_name] = $this->parseNameQualified($token[1]); + $state = 'id_name'; + break 2; + case T_VARIABLE: + $code .= $token[1]; + $state = $lastState; + break; + case T_CLASS: + $code .= $token[1]; + $state = 'anonymous'; + break; + default: + $i--; //reprocess last + $state = 'id_name'; + } + break; + case 'id_name': + switch ($token[0]) { + case $token[0] === ':' && ! in_array($context, ['instanceof', 'new'], true): + if ($lastState === 'closure' && $context === 'root') { + $state = 'closure'; + $code .= $id_start.$token; + } + + break; + case T_NAME_QUALIFIED: + case T_NS_SEPARATOR: + case T_STRING: + case T_WHITESPACE: + case T_COMMENT: + case T_DOC_COMMENT: + $id_name .= $token[1]; + break; + case '(': + if ($isShortClosure) { + $open++; + } + if ($context === 'new' || false !== strpos($id_name, '\\')) { + if ($id_start_ci === 'self' || $id_start_ci === 'static') { + if (! $inside_structure) { + $isUsingScope = true; + } + } elseif ($id_start !== '\\' && ! in_array($id_start_ci, $class_keywords)) { + if ($classes === null) { + $classes = $this->getClasses(); + } + if (isset($classes[$id_start_ci])) { + $id_start = $classes[$id_start_ci]; + } + if ($id_start[0] !== '\\') { + $id_start = $nsf.'\\'.$id_start; + } + } + } else { + if ($id_start !== '\\') { + if ($functions === null) { + $functions = $this->getFunctions(); + } + if (isset($functions[$id_start_ci])) { + $id_start = $functions[$id_start_ci]; + } elseif ($nsf !== '\\' && function_exists($nsf.'\\'.$id_start)) { + $id_start = $nsf.'\\'.$id_start; + // Cache it to functions array + $functions[$id_start_ci] = $id_start; + } + } + } + $code .= $id_start.$id_name.'('; + $state = $lastState; + break; + case T_VARIABLE: + case T_DOUBLE_COLON: + if ($id_start !== '\\') { + if ($id_start_ci === 'self' || $id_start_ci === 'parent') { + if (! $inside_structure) { + $isUsingScope = true; + } + } elseif ($id_start_ci === 'static') { + if (! $inside_structure) { + $isUsingScope = $token[0] === T_DOUBLE_COLON; + } + } elseif (! (\PHP_MAJOR_VERSION >= 7 && in_array($id_start_ci, $builtin_types))) { + if ($classes === null) { + $classes = $this->getClasses(); + } + if (isset($classes[$id_start_ci])) { + $id_start = $classes[$id_start_ci]; + } + if ($id_start[0] !== '\\') { + $id_start = $nsf.'\\'.$id_start; + } + } + } + + $code .= $id_start.$id_name.$token[1]; + $state = $token[0] === T_DOUBLE_COLON ? 'ignore_next' : $lastState; + break; + default: + if ($id_start !== '\\' && ! defined($id_start)) { + if ($constants === null) { + $constants = $this->getConstants(); + } + if (isset($constants[$id_start])) { + $id_start = $constants[$id_start]; + } elseif ($context === 'new') { + if (in_array($id_start_ci, $class_keywords)) { + if (! $inside_structure) { + $isUsingScope = true; + } + } else { + if ($classes === null) { + $classes = $this->getClasses(); + } + if (isset($classes[$id_start_ci])) { + $id_start = $classes[$id_start_ci]; + } + if ($id_start[0] !== '\\') { + $id_start = $nsf.'\\'.$id_start; + } + } + } elseif ($context === 'use' || + $context === 'instanceof' || + $context === 'args' || + $context === 'return_type' || + $context === 'extends' || + $context === 'root' + ) { + if (in_array($id_start_ci, $class_keywords)) { + if (! $inside_structure && ! $id_start_ci === 'static') { + $isUsingScope = true; + } + } elseif (! (\PHP_MAJOR_VERSION >= 7 && in_array($id_start_ci, $builtin_types))) { + if ($classes === null) { + $classes = $this->getClasses(); + } + if (isset($classes[$id_start_ci])) { + $id_start = $classes[$id_start_ci]; + } + if ($id_start[0] !== '\\') { + $id_start = $nsf.'\\'.$id_start; + } + } + } + } + $code .= $id_start.$id_name; + $state = $lastState; + $i--; //reprocess last token + } + break; + case 'anonymous': + switch ($token[0]) { + case T_NAME_QUALIFIED: + [$id_start, $id_start_ci, $id_name] = $this->parseNameQualified($token[1]); + $state = 'id_name'; + $lastState = 'anonymous'; + break 2; + case T_NS_SEPARATOR: + case T_STRING: + $id_start = $token[1]; + $id_start_ci = strtolower($id_start); + $id_name = ''; + $state = 'id_name'; + $context = 'extends'; + $lastState = 'anonymous'; + break; + case '{': + $state = 'closure'; + if (! $inside_structure) { + $inside_structure = true; + $inside_structure_mark = $open; + } + $i--; + break; + default: + $code .= is_array($token) ? $token[1] : $token; + } + break; + } + } + + if ($isShortClosure) { + $this->useVariables = $this->getStaticVariables(); + } else { + $this->useVariables = empty($use) ? $use : array_intersect_key($this->getStaticVariables(), array_flip($use)); + } + + $this->isShortClosure = $isShortClosure; + $this->isBindingRequired = $isUsingThisObject; + $this->isScopeRequired = $isUsingScope; + + $attributesCode = array_map(function ($attribute) { + $arguments = $attribute->getArguments(); + + $name = $attribute->getName(); + $arguments = implode(', ', array_map(function ($argument, $key) { + $argument = sprintf("'%s'", str_replace("'", "\\'", $argument)); + + if (is_string($key)) { + $argument = sprintf('%s: %s', $key, $argument); + } + + return $argument; + }, $arguments, array_keys($arguments))); + + return "#[$name($arguments)]"; + }, $this->getAttributes()); + + if (! empty($attributesCode)) { + $code = implode("\n", array_merge($attributesCode, [$code])); + } + + $this->code = $code; + + return $this->code; + } + + /** + * Get PHP native built in types. + * + * @return array + */ + protected static function getBuiltinTypes() + { + return ['array', 'callable', 'string', 'int', 'bool', 'float', 'iterable', 'void', 'object', 'mixed', 'false', 'null', 'never']; + } + + /** + * Gets the use variables by the closure. + * + * @return array + */ + public function getUseVariables() + { + if ($this->useVariables !== null) { + return $this->useVariables; + } + + $tokens = $this->getTokens(); + $use = []; + $state = 'start'; + + foreach ($tokens as &$token) { + $is_array = is_array($token); + + switch ($state) { + case 'start': + if ($is_array && $token[0] === T_USE) { + $state = 'use'; + } + break; + case 'use': + if ($is_array) { + if ($token[0] === T_VARIABLE) { + $use[] = substr($token[1], 1); + } + } elseif ($token == ')') { + break 2; + } + break; + } + } + + $this->useVariables = empty($use) ? $use : array_intersect_key($this->getStaticVariables(), array_flip($use)); + + return $this->useVariables; + } + + /** + * Checks if binding is required. + * + * @return bool + */ + public function isBindingRequired() + { + if ($this->isBindingRequired === null) { + $this->getCode(); + } + + return $this->isBindingRequired; + } + + /** + * Checks if access to the scope is required. + * + * @return bool + */ + public function isScopeRequired() + { + if ($this->isScopeRequired === null) { + $this->getCode(); + } + + return $this->isScopeRequired; + } + + /** + * The hash of the current file name. + * + * @return string + */ + protected function getHashedFileName() + { + if ($this->hashedName === null) { + $this->hashedName = sha1($this->getFileName()); + } + + return $this->hashedName; + } + + /** + * Get the file tokens. + * + * @return array + */ + protected function getFileTokens() + { + $key = $this->getHashedFileName(); + + if (! isset(static::$files[$key])) { + static::$files[$key] = token_get_all(file_get_contents($this->getFileName())); + } + + return static::$files[$key]; + } + + /** + * Get the tokens. + * + * @return array + */ + protected function getTokens() + { + if ($this->tokens === null) { + $tokens = $this->getFileTokens(); + $startLine = $this->getStartLine(); + $endLine = $this->getEndLine(); + $results = []; + $start = false; + + foreach ($tokens as &$token) { + if (! is_array($token)) { + if ($start) { + $results[] = $token; + } + + continue; + } + + $line = $token[2]; + + if ($line <= $endLine) { + if ($line >= $startLine) { + $start = true; + $results[] = $token; + } + + continue; + } + + break; + } + + $this->tokens = $results; + } + + return $this->tokens; + } + + /** + * Get the classes. + * + * @return array + */ + protected function getClasses() + { + $line = $this->getStartLine(); + + foreach ($this->getStructures() as $struct) { + if ($struct['type'] === 'namespace' && + $struct['start'] <= $line && + $struct['end'] >= $line + ) { + return $struct['classes']; + } + } + + return []; + } + + /** + * Get the functions. + * + * @return array + */ + protected function getFunctions() + { + $key = $this->getHashedFileName(); + + if (! isset(static::$functions[$key])) { + $this->fetchItems(); + } + + return static::$functions[$key]; + } + + /** + * Gets the constants. + * + * @return array + */ + protected function getConstants() + { + $key = $this->getHashedFileName(); + + if (! isset(static::$constants[$key])) { + $this->fetchItems(); + } + + return static::$constants[$key]; + } + + /** + * Get the structures. + * + * @return array + */ + protected function getStructures() + { + $key = $this->getHashedFileName(); + + if (! isset(static::$structures[$key])) { + $this->fetchItems(); + } + + return static::$structures[$key]; + } + + /** + * Fetch the items. + * + * @return void. + */ + protected function fetchItems() + { + $key = $this->getHashedFileName(); + + $classes = []; + $functions = []; + $constants = []; + $structures = []; + $tokens = $this->getFileTokens(); + + $open = 0; + $state = 'start'; + $lastState = ''; + $prefix = ''; + $name = ''; + $alias = ''; + $isFunc = $isConst = false; + + $startLine = $lastKnownLine = 0; + $structType = $structName = ''; + $structIgnore = false; + + $namespace = ''; + $namespaceStartLine = 0; + $namespaceBraced = false; + $namespaceClasses = []; + + foreach ($tokens as $token) { + if (is_array($token)) { + $lastKnownLine = $token[2]; + } + + switch ($state) { + case 'start': + switch ($token[0]) { + case T_NAMESPACE: + $structures[] = [ + 'type' => 'namespace', + 'name' => $namespace, + 'start' => $namespaceStartLine, + 'end' => $token[2] - 1, + 'classes' => $namespaceClasses, + ]; + $namespace = ''; + $namespaceClasses = []; + $state = 'namespace'; + $namespaceStartLine = $token[2]; + break; + case T_CLASS: + case T_INTERFACE: + case T_TRAIT: + $state = 'before_structure'; + $startLine = $token[2]; + $structType = $token[0] == T_CLASS + ? 'class' + : ($token[0] == T_INTERFACE ? 'interface' : 'trait'); + break; + case T_USE: + $state = 'use'; + $prefix = $name = $alias = ''; + $isFunc = $isConst = false; + break; + case T_FUNCTION: + $state = 'structure'; + $structIgnore = true; + break; + case T_NEW: + $state = 'new'; + break; + case T_OBJECT_OPERATOR: + case T_DOUBLE_COLON: + $state = 'invoke'; + break; + case '}': + if ($namespaceBraced) { + $structures[] = [ + 'type' => 'namespace', + 'name' => $namespace, + 'start' => $namespaceStartLine, + 'end' => $lastKnownLine, + 'classes' => $namespaceClasses, + ]; + $namespaceBraced = false; + $namespace = ''; + $namespaceClasses = []; + } + break; + } + break; + case 'namespace': + switch ($token[0]) { + case T_STRING: + case T_NAME_QUALIFIED: + $namespace = $token[1]; + break; + case ';': + case '{': + $state = 'start'; + $namespaceBraced = $token[0] === '{'; + break; + } + break; + case 'use': + switch ($token[0]) { + case T_FUNCTION: + $isFunc = true; + break; + case T_CONST: + $isConst = true; + break; + case T_NS_SEPARATOR: + $name .= $token[1]; + break; + case T_STRING: + $name .= $token[1]; + $alias = $token[1]; + break; + case T_NAME_QUALIFIED: + $name .= $token[1]; + $pieces = explode('\\', $token[1]); + $alias = end($pieces); + break; + case T_AS: + $lastState = 'use'; + $state = 'alias'; + break; + case '{': + $prefix = $name; + $name = $alias = ''; + $state = 'use-group'; + break; + case ',': + case ';': + if ($name === '' || $name[0] !== '\\') { + $name = '\\'.$name; + } + + if ($alias !== '') { + if ($isFunc) { + $functions[strtolower($alias)] = $name; + } elseif ($isConst) { + $constants[$alias] = $name; + } else { + $classes[strtolower($alias)] = $name; + $namespaceClasses[strtolower($alias)] = $name; + } + } + $name = $alias = ''; + $state = $token === ';' ? 'start' : 'use'; + break; + } + break; + case 'use-group': + switch ($token[0]) { + case T_NS_SEPARATOR: + $name .= $token[1]; + break; + case T_NAME_QUALIFIED: + $name .= $token[1]; + $pieces = explode('\\', $token[1]); + $alias = end($pieces); + break; + case T_STRING: + $name .= $token[1]; + $alias = $token[1]; + break; + case T_AS: + $lastState = 'use-group'; + $state = 'alias'; + break; + case ',': + case '}': + + if ($prefix === '' || $prefix[0] !== '\\') { + $prefix = '\\'.$prefix; + } + + if ($alias !== '') { + if ($isFunc) { + $functions[strtolower($alias)] = $prefix.$name; + } elseif ($isConst) { + $constants[$alias] = $prefix.$name; + } else { + $classes[strtolower($alias)] = $prefix.$name; + $namespaceClasses[strtolower($alias)] = $prefix.$name; + } + } + $name = $alias = ''; + $state = $token === '}' ? 'use' : 'use-group'; + break; + } + break; + case 'alias': + if ($token[0] === T_STRING) { + $alias = $token[1]; + $state = $lastState; + } + break; + case 'new': + switch ($token[0]) { + case T_WHITESPACE: + case T_COMMENT: + case T_DOC_COMMENT: + break 2; + case T_CLASS: + $state = 'structure'; + $structIgnore = true; + break; + default: + $state = 'start'; + } + break; + case 'invoke': + switch ($token[0]) { + case T_WHITESPACE: + case T_COMMENT: + case T_DOC_COMMENT: + break 2; + default: + $state = 'start'; + } + break; + case 'before_structure': + if ($token[0] == T_STRING) { + $structName = $token[1]; + $state = 'structure'; + } + break; + case 'structure': + switch ($token[0]) { + case '{': + case T_CURLY_OPEN: + case T_DOLLAR_OPEN_CURLY_BRACES: + $open++; + break; + case '}': + if (--$open == 0) { + if (! $structIgnore) { + $structures[] = [ + 'type' => $structType, + 'name' => $structName, + 'start' => $startLine, + 'end' => $lastKnownLine, + ]; + } + $structIgnore = false; + $state = 'start'; + } + break; + } + break; + } + } + + $structures[] = [ + 'type' => 'namespace', + 'name' => $namespace, + 'start' => $namespaceStartLine, + 'end' => PHP_INT_MAX, + 'classes' => $namespaceClasses, + ]; + + static::$classes[$key] = $classes; + static::$functions[$key] = $functions; + static::$constants[$key] = $constants; + static::$structures[$key] = $structures; + } + + /** + * Returns the namespace associated to the closure. + * + * @return string + */ + protected function getClosureNamespaceName() + { + $startLine = $this->getStartLine(); + $endLine = $this->getEndLine(); + + foreach ($this->getStructures() as $struct) { + if ($struct['type'] === 'namespace' && + $struct['start'] <= $startLine && + $struct['end'] >= $endLine + ) { + return $struct['name']; + } + } + + return ''; + } + + /** + * Parse the given token. + * + * @param string $token + * @return array + */ + protected function parseNameQualified($token) + { + $pieces = explode('\\', $token); + + $id_start = array_shift($pieces); + + $id_start_ci = strtolower($id_start); + + $id_name = '\\'.implode('\\', $pieces); + + return [$id_start, $id_start_ci, $id_name]; + } +} diff --git a/3rdparty/laravel/serializable-closure/src/Support/SelfReference.php b/3rdparty/laravel/serializable-closure/src/Support/SelfReference.php new file mode 100644 index 00000000..58319504 --- /dev/null +++ b/3rdparty/laravel/serializable-closure/src/Support/SelfReference.php @@ -0,0 +1,24 @@ +hash = $hash; + } +} diff --git a/3rdparty/laravel/serializable-closure/src/UnsignedSerializableClosure.php b/3rdparty/laravel/serializable-closure/src/UnsignedSerializableClosure.php new file mode 100644 index 00000000..afc2127e --- /dev/null +++ b/3rdparty/laravel/serializable-closure/src/UnsignedSerializableClosure.php @@ -0,0 +1,69 @@ +serializable = new Serializers\Native($closure); + } + + /** + * Resolve the closure with the given arguments. + * + * @return mixed + */ + public function __invoke() + { + return call_user_func_array($this->serializable, func_get_args()); + } + + /** + * Gets the closure. + * + * @return \Closure + */ + public function getClosure() + { + return $this->serializable->getClosure(); + } + + /** + * Get the serializable representation of the closure. + * + * @return array{serializable: \Laravel\SerializableClosure\Contracts\Serializable} + */ + public function __serialize() + { + return [ + 'serializable' => $this->serializable, + ]; + } + + /** + * Restore the closure after serialization. + * + * @param array{serializable: \Laravel\SerializableClosure\Contracts\Serializable} $data + * @return void + */ + public function __unserialize($data) + { + $this->serializable = $data['serializable']; + } +} diff --git a/3rdparty/lcobucci/clock/LICENSE b/3rdparty/lcobucci/clock/LICENSE new file mode 100644 index 00000000..58ea9440 --- /dev/null +++ b/3rdparty/lcobucci/clock/LICENSE @@ -0,0 +1,21 @@ +MIT License + +Copyright (c) 2017 Luís Cobucci + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. diff --git a/3rdparty/lcobucci/clock/src/Clock.php b/3rdparty/lcobucci/clock/src/Clock.php new file mode 100644 index 00000000..45a033b8 --- /dev/null +++ b/3rdparty/lcobucci/clock/src/Clock.php @@ -0,0 +1,12 @@ +now = $now; + } + + public function now(): DateTimeImmutable + { + return $this->now; + } +} diff --git a/3rdparty/lcobucci/clock/src/SystemClock.php b/3rdparty/lcobucci/clock/src/SystemClock.php new file mode 100644 index 00000000..69de81f8 --- /dev/null +++ b/3rdparty/lcobucci/clock/src/SystemClock.php @@ -0,0 +1,31 @@ +timezone); + } +} diff --git a/3rdparty/marc-mabe/php-enum/LICENSE.txt b/3rdparty/marc-mabe/php-enum/LICENSE.txt new file mode 100644 index 00000000..02bcecec --- /dev/null +++ b/3rdparty/marc-mabe/php-enum/LICENSE.txt @@ -0,0 +1,27 @@ +Copyright (c) 2020, Marc Bennewitz +All rights reserved. + +Redistribution and use in source and binary forms, with or without modification, +are permitted provided that the following conditions are met: + + * Redistributions of source code must retain the above copyright notice, + this list of conditions and the following disclaimer. + + * Redistributions in binary form must reproduce the above copyright notice, + this list of conditions and the following disclaimer in the documentation + and/or other materials provided with the distribution. + + * Neither the name of the organisation nor the names of its contributors + may be used to endorse or promote products derived from this software + without specific prior written permission. + +THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND +ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED +WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE +DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE +FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL +DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR +SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER +CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, +OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE +OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. diff --git a/3rdparty/marc-mabe/php-enum/src/Enum.php b/3rdparty/marc-mabe/php-enum/src/Enum.php new file mode 100644 index 00000000..d602d8bc --- /dev/null +++ b/3rdparty/marc-mabe/php-enum/src/Enum.php @@ -0,0 +1,484 @@ + + */ + private $value; + + /** + * The ordinal number of the enumerator + * + * @var null|int + */ + private $ordinal; + + /** + * A map of enumerator names and values by enumeration class + * + * @var array, array>> + */ + private static $constants = []; + + /** + * A List of available enumerator names by enumeration class + * + * @var array, string[]> + */ + private static $names = []; + + /** + * A map of enumerator names and instances by enumeration class + * + * @var array, array> + */ + private static $instances = []; + + /** + * Constructor + * + * @param null|bool|int|float|string|array $value The value of the enumerator + * @param int|null $ordinal The ordinal number of the enumerator + */ + final private function __construct($value, $ordinal = null) + { + $this->value = $value; + $this->ordinal = $ordinal; + } + + /** + * Get the name of the enumerator + * + * @return string + * @see getName() + */ + public function __toString(): string + { + return $this->getName(); + } + + /** + * @throws LogicException Enums are not cloneable + * because instances are implemented as singletons + */ + final public function __clone() + { + throw new LogicException('Enums are not cloneable'); + } + + /** + * @throws LogicException Serialization is not supported by default in this pseudo-enum implementation + * + * @psalm-return never-return + */ + final public function __sleep() + { + throw new LogicException('Serialization is not supported by default in this pseudo-enum implementation'); + } + + /** + * @throws LogicException Serialization is not supported by default in this pseudo-enum implementation + * + * @psalm-return never-return + */ + final public function __wakeup() + { + throw new LogicException('Serialization is not supported by default in this pseudo-enum implementation'); + } + + /** + * Get the value of the enumerator + * + * @return null|bool|int|float|string|array + */ + final public function getValue() + { + return $this->value; + } + + /** + * Get the name of the enumerator + * + * @return string + * + * @phpstan-return string + * @psalm-return non-empty-string + */ + final public function getName() + { + return self::$names[static::class][$this->ordinal ?? $this->getOrdinal()]; + } + + /** + * Get the ordinal number of the enumerator + * + * @return int + */ + final public function getOrdinal() + { + if ($this->ordinal === null) { + $ordinal = 0; + $value = $this->value; + $constants = self::$constants[static::class] ?? static::getConstants(); + foreach ($constants as $constValue) { + if ($value === $constValue) { + break; + } + ++$ordinal; + } + + $this->ordinal = $ordinal; + } + + return $this->ordinal; + } + + /** + * Compare this enumerator against another and check if it's the same. + * + * @param static|null|bool|int|float|string|array $enumerator An enumerator object or value + * @return bool + */ + final public function is($enumerator) + { + return $this === $enumerator || $this->value === $enumerator + + // The following additional conditions are required only because of the issue of serializable singletons + || ($enumerator instanceof static + && \get_class($enumerator) === static::class + && $enumerator->value === $this->value + ); + } + + /** + * Get an enumerator instance of the given enumerator value or instance + * + * @param static|null|bool|int|float|string|array $enumerator An enumerator object or value + * @return static + * @throws InvalidArgumentException On an unknown or invalid value + * @throws LogicException On ambiguous constant values + * + * @psalm-pure + */ + final public static function get($enumerator) + { + if ($enumerator instanceof static) { + if (\get_class($enumerator) !== static::class) { + throw new InvalidArgumentException(sprintf( + 'Invalid value of type %s for enumeration %s', + \get_class($enumerator), + static::class + )); + } + + return $enumerator; + } + + return static::byValue($enumerator); + } + + /** + * Get an enumerator instance by the given value + * + * @param null|bool|int|float|string|array $value Enumerator value + * @return static + * @throws InvalidArgumentException On an unknown or invalid value + * @throws LogicException On ambiguous constant values + * + * @psalm-pure + */ + final public static function byValue($value) + { + /** @var mixed $value */ + + $constants = self::$constants[static::class] ?? static::getConstants(); + + $name = \array_search($value, $constants, true); + if ($name === false) { + throw new InvalidArgumentException(sprintf( + 'Unknown value %s for enumeration %s', + \is_scalar($value) + ? \var_export($value, true) + : 'of type ' . (\is_object($value) ? \get_class($value) : \gettype($value)), + static::class + )); + } + + /** @var static $instance */ + $instance = self::$instances[static::class][$name] + ?? self::$instances[static::class][$name] = new static($constants[$name]); + + return $instance; + } + + /** + * Get an enumerator instance by the given name + * + * @param string $name The name of the enumerator + * @return static + * @throws InvalidArgumentException On an invalid or unknown name + * @throws LogicException On ambiguous values + * + * @psalm-pure + */ + final public static function byName(string $name) + { + if (isset(self::$instances[static::class][$name])) { + /** @var static $instance */ + $instance = self::$instances[static::class][$name]; + return $instance; + } + + $const = static::class . "::{$name}"; + if (!\defined($const)) { + throw new InvalidArgumentException("{$const} not defined"); + } + + assert( + self::noAmbiguousValues(static::getConstants()), + 'Ambiguous enumerator values detected for ' . static::class + ); + + /** @var array|bool|float|int|string|null $value */ + $value = \constant($const); + return self::$instances[static::class][$name] = new static($value); + } + + /** + * Get an enumeration instance by the given ordinal number + * + * @param int $ordinal The ordinal number of the enumerator + * @return static + * @throws InvalidArgumentException On an invalid ordinal number + * @throws LogicException On ambiguous values + * + * @psalm-pure + */ + final public static function byOrdinal(int $ordinal) + { + $constants = self::$constants[static::class] ?? static::getConstants(); + + if (!isset(self::$names[static::class][$ordinal])) { + throw new InvalidArgumentException(\sprintf( + 'Invalid ordinal number %s, must between 0 and %s', + $ordinal, + \count(self::$names[static::class]) - 1 + )); + } + + $name = self::$names[static::class][$ordinal]; + + /** @var static $instance */ + $instance = self::$instances[static::class][$name] + ?? self::$instances[static::class][$name] = new static($constants[$name], $ordinal); + + return $instance; + } + + /** + * Get a list of enumerator instances ordered by ordinal number + * + * @return static[] + * + * @phpstan-return array + * @psalm-return list + * @psalm-pure + */ + final public static function getEnumerators() + { + if (!isset(self::$names[static::class])) { + static::getConstants(); + } + + /** @var callable $byNameFn */ + $byNameFn = [static::class, 'byName']; + return \array_map($byNameFn, self::$names[static::class]); + } + + /** + * Get a list of enumerator values ordered by ordinal number + * + * @return (null|bool|int|float|string|array)[] + * + * @phpstan-return array> + * @psalm-return list + * @psalm-pure + */ + final public static function getValues() + { + return \array_values(self::$constants[static::class] ?? static::getConstants()); + } + + /** + * Get a list of enumerator names ordered by ordinal number + * + * @return string[] + * + * @phpstan-return array + * @psalm-return list + * @psalm-pure + */ + final public static function getNames() + { + if (!isset(self::$names[static::class])) { + static::getConstants(); + } + return self::$names[static::class]; + } + + /** + * Get a list of enumerator ordinal numbers + * + * @return int[] + * + * @phpstan-return array + * @psalm-return list + * @psalm-pure + */ + final public static function getOrdinals() + { + $count = \count(self::$constants[static::class] ?? static::getConstants()); + return $count ? \range(0, $count - 1) : []; + } + + /** + * Get all available constants of the called class + * + * @return (null|bool|int|float|string|array)[] + * @throws LogicException On ambiguous constant values + * + * @phpstan-return array> + * @psalm-return array + * @psalm-pure + */ + final public static function getConstants() + { + if (isset(self::$constants[static::class])) { + return self::$constants[static::class]; + } + + $reflection = new ReflectionClass(static::class); + $constants = []; + + do { + $scopeConstants = []; + // Enumerators must be defined as public class constants + foreach ($reflection->getReflectionConstants() as $reflConstant) { + if ($reflConstant->isPublic()) { + $scopeConstants[ $reflConstant->getName() ] = $reflConstant->getValue(); + } + } + + $constants = $scopeConstants + $constants; + } while (($reflection = $reflection->getParentClass()) && $reflection->name !== __CLASS__); + + /** @var array> $constants */ + + assert( + self::noAmbiguousValues($constants), + 'Ambiguous enumerator values detected for ' . static::class + ); + + self::$names[static::class] = \array_keys($constants); + return self::$constants[static::class] = $constants; + } + + /** + * Test that the given constants does not contain ambiguous values + * @param array> $constants + * @return bool + */ + private static function noAmbiguousValues($constants) + { + foreach ($constants as $value) { + $names = \array_keys($constants, $value, true); + if (\count($names) > 1) { + return false; + } + } + + return true; + } + + /** + * Test if the given enumerator is part of this enumeration + * + * @param static|null|bool|int|float|string|array $enumerator + * @return bool + * + * @psalm-pure + */ + final public static function has($enumerator) + { + if ($enumerator instanceof static) { + return \get_class($enumerator) === static::class; + } + + return static::hasValue($enumerator); + } + + /** + * Test if the given enumerator value is part of this enumeration + * + * @param null|bool|int|float|string|array $value + * @return bool + * + * @psalm-pure + */ + final public static function hasValue($value) + { + return \in_array($value, self::$constants[static::class] ?? static::getConstants(), true); + } + + /** + * Test if the given enumerator name is part of this enumeration + * + * @param string $name + * @return bool + * + * @psalm-pure + */ + final public static function hasName(string $name) + { + return \defined("static::{$name}"); + } + + /** + * Get an enumerator instance by the given name. + * + * This will be called automatically on calling a method + * with the same name of a defined enumerator. + * + * @param string $method The name of the enumerator (called as method) + * @param array $args There should be no arguments + * @return static + * @throws InvalidArgumentException On an invalid or unknown name + * @throws LogicException On ambiguous constant values + * + * @psalm-pure + */ + final public static function __callStatic(string $method, array $args) + { + return static::byName($method); + } +} diff --git a/3rdparty/marc-mabe/php-enum/src/EnumMap.php b/3rdparty/marc-mabe/php-enum/src/EnumMap.php new file mode 100644 index 00000000..eae61bac --- /dev/null +++ b/3rdparty/marc-mabe/php-enum/src/EnumMap.php @@ -0,0 +1,393 @@ +). + * + * @template T of Enum + * @implements ArrayAccess + * @implements IteratorAggregate + * + * @copyright 2020, Marc Bennewitz + * @license http://github.com/marc-mabe/php-enum/blob/master/LICENSE.txt New BSD License + * @link http://github.com/marc-mabe/php-enum for the canonical source repository + */ +class EnumMap implements ArrayAccess, Countable, IteratorAggregate +{ + /** + * The classname of the enumeration type + * @var class-string + */ + private $enumeration; + + /** + * Internal map of ordinal number and data value + * @var array + */ + private $map = []; + + /** + * Constructor + * @param class-string $enumeration The classname of the enumeration type + * @param null|iterable, mixed> $map Initialize map + * @throws InvalidArgumentException + */ + public function __construct(string $enumeration, ?iterable $map = null) + { + if (!\is_subclass_of($enumeration, Enum::class)) { + throw new InvalidArgumentException(\sprintf( + '%s can handle subclasses of %s only', + __CLASS__, + Enum::class + )); + } + $this->enumeration = $enumeration; + + if ($map) { + $this->addIterable($map); + } + } + + /** + * Add virtual private property "__pairs" with a list of key-value-pairs + * to the result of var_dump. + * + * This helps debugging as internally the map is using the ordinal number. + * + * @return array + */ + public function __debugInfo(): array { + $dbg = (array)$this; + $dbg["\0" . self::class . "\0__pairs"] = array_map(function ($k, $v) { + return [$k, $v]; + }, $this->getKeys(), $this->getValues()); + return $dbg; + } + + /* write access (mutable) */ + + /** + * Adds the given enumerator (object or value) mapping to the specified data value. + * @param T|null|bool|int|float|string|array $enumerator + * @param mixed $value + * @throws InvalidArgumentException On an invalid given enumerator + * @see offsetSet() + */ + public function add($enumerator, $value): void + { + $ord = ($this->enumeration)::get($enumerator)->getOrdinal(); + $this->map[$ord] = $value; + } + + /** + * Adds the given iterable, mapping enumerators (objects or values) to data values. + * @param iterable, mixed> $map + * @throws InvalidArgumentException On an invalid given enumerator + */ + public function addIterable(iterable $map): void + { + $innerMap = $this->map; + foreach ($map as $enumerator => $value) { + $ord = ($this->enumeration)::get($enumerator)->getOrdinal(); + $innerMap[$ord] = $value; + } + $this->map = $innerMap; + } + + /** + * Removes the given enumerator (object or value) mapping. + * @param T|null|bool|int|float|string|array $enumerator + * @throws InvalidArgumentException On an invalid given enumerator + * @see offsetUnset() + */ + public function remove($enumerator): void + { + $ord = ($this->enumeration)::get($enumerator)->getOrdinal(); + unset($this->map[$ord]); + } + + /** + * Removes the given iterable enumerator (object or value) mappings. + * @param iterable> $enumerators + * @throws InvalidArgumentException On an invalid given enumerator + */ + public function removeIterable(iterable $enumerators): void + { + $map = $this->map; + foreach ($enumerators as $enumerator) { + $ord = ($this->enumeration)::get($enumerator)->getOrdinal(); + unset($map[$ord]); + } + + $this->map = $map; + } + + /* write access (immutable) */ + + /** + * Creates a new map with the given enumerator (object or value) mapping to the specified data value added. + * @param T|null|bool|int|float|string|array $enumerator + * @param mixed $value + * @return static + * @throws InvalidArgumentException On an invalid given enumerator + */ + public function with($enumerator, $value): self + { + $clone = clone $this; + $clone->add($enumerator, $value); + return $clone; + } + + /** + * Creates a new map with the given iterable mapping enumerators (objects or values) to data values added. + * @param iterable, mixed> $map + * @return static + * @throws InvalidArgumentException On an invalid given enumerator + */ + public function withIterable(iterable $map): self + { + $clone = clone $this; + $clone->addIterable($map); + return $clone; + } + + /** + * Create a new map with the given enumerator mapping removed. + * @param T|null|bool|int|float|string|array $enumerator + * @return static + * @throws InvalidArgumentException On an invalid given enumerator + */ + public function without($enumerator): self + { + $clone = clone $this; + $clone->remove($enumerator); + return $clone; + } + + /** + * Creates a new map with the given iterable enumerator (object or value) mappings removed. + * @param iterable> $enumerators + * @return static + * @throws InvalidArgumentException On an invalid given enumerator + */ + public function withoutIterable(iterable $enumerators): self + { + $clone = clone $this; + $clone->removeIterable($enumerators); + return $clone; + } + + /* read access */ + + /** + * Get the classname of the enumeration type. + * @return class-string + */ + public function getEnumeration(): string + { + return $this->enumeration; + } + + /** + * Get the mapped data value of the given enumerator (object or value). + * @param T|null|bool|int|float|string|array $enumerator + * @return mixed + * @throws InvalidArgumentException On an invalid given enumerator + * @throws UnexpectedValueException If the given enumerator does not exist in this map + * @see offsetGet() + */ + public function get($enumerator) + { + $enumerator = ($this->enumeration)::get($enumerator); + $ord = $enumerator->getOrdinal(); + if (!\array_key_exists($ord, $this->map)) { + throw new UnexpectedValueException(sprintf( + 'Enumerator %s could not be found', + \var_export($enumerator->getValue(), true) + )); + } + + return $this->map[$ord]; + } + + /** + * Get a list of enumerator keys. + * @return T[] + * + * @phpstan-return array + * @psalm-return list + */ + public function getKeys(): array + { + /** @var callable $byOrdinalFn */ + $byOrdinalFn = [$this->enumeration, 'byOrdinal']; + + return \array_map($byOrdinalFn, \array_keys($this->map)); + } + + /** + * Get a list of mapped data values. + * @return mixed[] + * + * @phpstan-return array + * @psalm-return list + */ + public function getValues(): array + { + return \array_values($this->map); + } + + /** + * Search for the given data value. + * @param mixed $value + * @param bool $strict Use strict type comparison + * @return T|null The enumerator object of the first matching data value or NULL + */ + public function search($value, bool $strict = false) + { + /** @var false|int $ord */ + $ord = \array_search($value, $this->map, $strict); + if ($ord !== false) { + return ($this->enumeration)::byOrdinal($ord); + } + + return null; + } + + /** + * Test if the given enumerator key (object or value) exists. + * @param T|null|bool|int|float|string|array $enumerator + * @return bool + * @see offsetExists() + */ + public function has($enumerator): bool + { + try { + $ord = ($this->enumeration)::get($enumerator)->getOrdinal(); + return \array_key_exists($ord, $this->map); + } catch (InvalidArgumentException $e) { + // An invalid enumerator can't be contained in this map + return false; + } + } + + /** + * Test if the given enumerator key (object or value) exists. + * @param T|null|bool|int|float|string|array $enumerator + * @return bool + * @see offsetExists() + * @see has() + * @deprecated Will trigger deprecation warning in last 4.x and removed in 5.x + */ + public function contains($enumerator): bool + { + return $this->has($enumerator); + } + + /* ArrayAccess */ + + /** + * Test if the given enumerator key (object or value) exists and is not NULL + * @param T|null|bool|int|float|string|array $enumerator + * @return bool + * @see contains() + */ + public function offsetExists($enumerator): bool + { + try { + return isset($this->map[($this->enumeration)::get($enumerator)->getOrdinal()]); + } catch (InvalidArgumentException $e) { + // An invalid enumerator can't be an offset of this map + return false; + } + } + + /** + * Get the mapped data value of the given enumerator (object or value). + * @param T|null|bool|int|float|string|array $enumerator + * @return mixed The mapped date value of the given enumerator or NULL + * @throws InvalidArgumentException On an invalid given enumerator + * @see get() + */ + #[\ReturnTypeWillChange] + public function offsetGet($enumerator) + { + try { + return $this->get($enumerator); + } catch (UnexpectedValueException $e) { + return null; + } + } + + /** + * Adds the given enumerator (object or value) mapping to the specified data value. + * @param T|null|bool|int|float|string|array $enumerator + * @param mixed $value + * @return void + * @throws InvalidArgumentException On an invalid given enumerator + * @see add() + */ + public function offsetSet($enumerator, $value = null): void + { + $this->add($enumerator, $value); + } + + /** + * Removes the given enumerator (object or value) mapping. + * @param T|null|bool|int|float|string|array $enumerator + * @return void + * @throws InvalidArgumentException On an invalid given enumerator + * @see remove() + */ + public function offsetUnset($enumerator): void + { + $this->remove($enumerator); + } + + /* IteratorAggregate */ + + /** + * Get a new Iterator. + * + * @return Iterator Iterator + */ + public function getIterator(): Iterator + { + $map = $this->map; + foreach ($map as $ordinal => $value) { + yield ($this->enumeration)::byOrdinal($ordinal) => $value; + } + } + + /* Countable */ + + /** + * Count the number of elements + * + * @return int + */ + public function count(): int + { + return \count($this->map); + } + + /** + * Tests if the map is empty + * + * @return bool + */ + public function isEmpty(): bool + { + return empty($this->map); + } +} diff --git a/3rdparty/marc-mabe/php-enum/src/EnumSerializableTrait.php b/3rdparty/marc-mabe/php-enum/src/EnumSerializableTrait.php new file mode 100644 index 00000000..1f54b9d4 --- /dev/null +++ b/3rdparty/marc-mabe/php-enum/src/EnumSerializableTrait.php @@ -0,0 +1,110 @@ + + */ + abstract public function getValue(); + + /** + * Returns an array of data to be serialized. + * This magic method will be called by serialize() in PHP >= 7.4 + * + * @return array> + */ + public function __serialize(): array + { + return ['value' => $this->getValue()]; + } + + /** + * Receives an array of data to be unserialized on a new instance without constructor. + * This magic method will be called in PHP >= 7.4 is the data where serialized with PHP >= 7.4. + * + * @throws RuntimeException On missing, unknown or invalid value + * @throws LogicException On calling this method on an already initialized enumerator + * + * @param array $data + * @return void + */ + public function __unserialize(array $data): void + { + if (!\array_key_exists('value', $data)) { + throw new RuntimeException('Missing array key "value"'); + } + + /** @var mixed $value */ + $value = $data['value']; + $constants = self::getConstants(); + $name = \array_search($value, $constants, true); + if ($name === false) { + $message = \is_scalar($value) + ? 'Unknown value ' . \var_export($value, true) + : 'Invalid value of type ' . (\is_object($value) ? \get_class($value) : \gettype($value)); + throw new RuntimeException($message); + } + + $class = static::class; + $enumerator = $this; + $closure = function () use ($class, $name, $value, $enumerator) { + if ($value !== null && $this->value !== null) { + throw new LogicException('Do not call this directly - please use unserialize($enum) instead'); + } + + $this->value = $value; + + if (!isset(self::$instances[$class][$name])) { + self::$instances[$class][$name] = $enumerator; + } + }; + $closure->bindTo($this, Enum::class)(); + } + + /** + * Serialize the value of the enumeration + * This will be called automatically on `serialize()` if the enumeration implements the `Serializable` interface + * + * @return string + * @deprecated Since PHP 7.4 + */ + public function serialize(): string + { + return \serialize($this->getValue()); + } + + /** + * Unserializes a given serialized value and push it into the current instance + * This will be called automatically on `unserialize()` if the enumeration implements the `Serializable` interface + * @param string $serialized + * @return void + * @throws RuntimeException On an unknown or invalid value + * @throws LogicException On calling this method on an already initialized enumerator + * @deprecated Since PHP 7.4 + */ + public function unserialize($serialized): void + { + $this->__unserialize(['value' => \unserialize($serialized)]); + } +} diff --git a/3rdparty/marc-mabe/php-enum/src/EnumSet.php b/3rdparty/marc-mabe/php-enum/src/EnumSet.php new file mode 100644 index 00000000..42e34b39 --- /dev/null +++ b/3rdparty/marc-mabe/php-enum/src/EnumSet.php @@ -0,0 +1,1193 @@ +) + * based on an integer or binary bitset depending of given enumeration size. + * + * @template T of Enum + * @implements IteratorAggregate + * + * @copyright 2020, Marc Bennewitz + * @license http://github.com/marc-mabe/php-enum/blob/master/LICENSE.txt New BSD License + * @link http://github.com/marc-mabe/php-enum for the canonical source repository + */ +class EnumSet implements IteratorAggregate, Countable +{ + /** + * The classname of the Enumeration + * @var class-string + */ + private $enumeration; + + /** + * Number of enumerators defined in the enumeration + * @var int + */ + private $enumerationCount; + + /** + * Integer or binary (little endian) bitset + * @var int|string + */ + private $bitset = 0; + + /** + * Integer or binary (little endian) empty bitset + * + * @var int|string + */ + private $emptyBitset = 0; + + /**#@+ + * Defines private method names to be called depended of how the bitset type was set too. + * ... Integer or binary bitset. + * ... *Int or *Bin method + * + * @var string + */ + /** @var string */ + private $fnDoGetIterator = 'doGetIteratorInt'; + + /** @var string */ + private $fnDoCount = 'doCountInt'; + + /** @var string */ + private $fnDoGetOrdinals = 'doGetOrdinalsInt'; + + /** @var string */ + private $fnDoGetBit = 'doGetBitInt'; + + /** @var string */ + private $fnDoSetBit = 'doSetBitInt'; + + /** @var string */ + private $fnDoUnsetBit = 'doUnsetBitInt'; + + /** @var string */ + private $fnDoGetBinaryBitsetLe = 'doGetBinaryBitsetLeInt'; + + /** @var string */ + private $fnDoSetBinaryBitsetLe = 'doSetBinaryBitsetLeInt'; + /**#@-*/ + + /** + * Constructor + * + * @param class-string $enumeration The classname of the enumeration + * @param iterable>|null $enumerators iterable list of enumerators initializing the set + * @throws InvalidArgumentException + */ + public function __construct(string $enumeration, ?iterable $enumerators = null) + { + if (!\is_subclass_of($enumeration, Enum::class)) { + throw new InvalidArgumentException(\sprintf( + '%s can handle subclasses of %s only', + __METHOD__, + Enum::class + )); + } + + $this->enumeration = $enumeration; + $this->enumerationCount = \count($enumeration::getConstants()); + + // By default the bitset is initialized as integer bitset + // in case the enumeration has more enumerators then integer bits + // we will switch this into a binary bitset + if ($this->enumerationCount > \PHP_INT_SIZE * 8) { + // init binary bitset with zeros + $this->bitset = $this->emptyBitset = \str_repeat("\0", (int)\ceil($this->enumerationCount / 8)); + + // switch internal binary bitset functions + $this->fnDoGetIterator = 'doGetIteratorBin'; + $this->fnDoCount = 'doCountBin'; + $this->fnDoGetOrdinals = 'doGetOrdinalsBin'; + $this->fnDoGetBit = 'doGetBitBin'; + $this->fnDoSetBit = 'doSetBitBin'; + $this->fnDoUnsetBit = 'doUnsetBitBin'; + $this->fnDoGetBinaryBitsetLe = 'doGetBinaryBitsetLeBin'; + $this->fnDoSetBinaryBitsetLe = 'doSetBinaryBitsetLeBin'; + } + + if ($enumerators !== null) { + foreach ($enumerators as $enumerator) { + $this->{$this->fnDoSetBit}($enumeration::get($enumerator)->getOrdinal()); + } + } + } + + /** + * Add virtual private property "__enumerators" with a list of enumerator values set + * to the result of var_dump. + * + * This helps debugging as internally the enumerators of this EnumSet gets stored + * as either integer or binary bit-array. + * + * @return array + */ + public function __debugInfo() { + $dbg = (array)$this; + $dbg["\0" . self::class . "\0__enumerators"] = $this->getValues(); + return $dbg; + } + + /** + * Get the classname of the enumeration + * @return class-string + */ + public function getEnumeration(): string + { + return $this->enumeration; + } + + /* write access (mutable) */ + + /** + * Adds an enumerator object or value + * @param T|null|bool|int|float|string|array $enumerator Enumerator object or value + * @return void + * @throws InvalidArgumentException On an invalid given enumerator + */ + public function add($enumerator): void + { + $this->{$this->fnDoSetBit}(($this->enumeration)::get($enumerator)->getOrdinal()); + } + + /** + * Adds all enumerator objects or values of the given iterable + * @param iterable> $enumerators Iterable list of enumerator objects or values + * @return void + * @throws InvalidArgumentException On an invalid given enumerator + */ + public function addIterable(iterable $enumerators): void + { + $bitset = $this->bitset; + + try { + foreach ($enumerators as $enumerator) { + $this->{$this->fnDoSetBit}(($this->enumeration)::get($enumerator)->getOrdinal()); + } + } catch (\Throwable $e) { + // reset all changes until error happened + $this->bitset = $bitset; + throw $e; + } + } + + /** + * Removes the given enumerator object or value + * @param T|null|bool|int|float|string|array $enumerator Enumerator object or value + * @return void + * @throws InvalidArgumentException On an invalid given enumerator + */ + public function remove($enumerator): void + { + $this->{$this->fnDoUnsetBit}(($this->enumeration)::get($enumerator)->getOrdinal()); + } + + /** + * Adds an enumerator object or value + * @param T|null|bool|int|float|string|array $enumerator Enumerator object or value + * @return void + * @throws InvalidArgumentException On an invalid given enumerator + * @see add() + * @see with() + * @deprecated Will trigger deprecation warning in last 4.x and removed in 5.x + */ + public function attach($enumerator): void + { + $this->add($enumerator); + } + + /** + * Removes the given enumerator object or value + * @param T|null|bool|int|float|string|array $enumerator Enumerator object or value + * @return void + * @throws InvalidArgumentException On an invalid given enumerator + * @see remove() + * @see without() + * @deprecated Will trigger deprecation warning in last 4.x and removed in 5.x + */ + public function detach($enumerator): void + { + $this->remove($enumerator); + } + + /** + * Removes all enumerator objects or values of the given iterable + * @param iterable> $enumerators Iterable list of enumerator objects or values + * @return void + * @throws InvalidArgumentException On an invalid given enumerator + */ + public function removeIterable(iterable $enumerators): void + { + $bitset = $this->bitset; + + try { + foreach ($enumerators as $enumerator) { + $this->{$this->fnDoUnsetBit}(($this->enumeration)::get($enumerator)->getOrdinal()); + } + } catch (\Throwable $e) { + // reset all changes until error happened + $this->bitset = $bitset; + throw $e; + } + } + + /** + * Modify this set from both this and other (this | other) + * + * @param EnumSet $other EnumSet of the same enumeration to produce the union + * @return void + * @throws InvalidArgumentException If $other doesn't match the enumeration + */ + public function setUnion(EnumSet $other): void + { + if ($this->enumeration !== $other->enumeration) { + throw new InvalidArgumentException(\sprintf( + 'Other should be of the same enumeration as this %s', + $this->enumeration + )); + } + + $this->bitset = $this->bitset | $other->bitset; + } + + /** + * Modify this set with enumerators common to both this and other (this & other) + * + * @param EnumSet $other EnumSet of the same enumeration to produce the intersect + * @return void + * @throws InvalidArgumentException If $other doesn't match the enumeration + */ + public function setIntersect(EnumSet $other): void + { + if ($this->enumeration !== $other->enumeration) { + throw new InvalidArgumentException(\sprintf( + 'Other should be of the same enumeration as this %s', + $this->enumeration + )); + } + + $this->bitset = $this->bitset & $other->bitset; + } + + /** + * Modify this set with enumerators in this but not in other (this - other) + * + * @param EnumSet $other EnumSet of the same enumeration to produce the diff + * @return void + * @throws InvalidArgumentException If $other doesn't match the enumeration + */ + public function setDiff(EnumSet $other): void + { + if ($this->enumeration !== $other->enumeration) { + throw new InvalidArgumentException(\sprintf( + 'Other should be of the same enumeration as this %s', + $this->enumeration + )); + } + + $this->bitset = $this->bitset & ~$other->bitset; + } + + /** + * Modify this set with enumerators in either this and other but not in both (this ^ other) + * + * @param EnumSet $other EnumSet of the same enumeration to produce the symmetric difference + * @return void + * @throws InvalidArgumentException If $other doesn't match the enumeration + */ + public function setSymDiff(EnumSet $other): void + { + if ($this->enumeration !== $other->enumeration) { + throw new InvalidArgumentException(\sprintf( + 'Other should be of the same enumeration as this %s', + $this->enumeration + )); + } + + $this->bitset = $this->bitset ^ $other->bitset; + } + + /** + * Set the given binary bitset in little-endian order + * + * @param string $bitset + * @return void + * @throws InvalidArgumentException On out-of-range bits given as input bitset + * @uses doSetBinaryBitsetLeBin() + * @uses doSetBinaryBitsetLeInt() + */ + public function setBinaryBitsetLe(string $bitset): void + { + $this->{$this->fnDoSetBinaryBitsetLe}($bitset); + } + + /** + * Set binary bitset in little-endian order + * + * @param string $bitset + * @return void + * @throws InvalidArgumentException On out-of-range bits given as input bitset + * @see setBinaryBitsetLeBin() + * @see doSetBinaryBitsetLeInt() + */ + private function doSetBinaryBitsetLeBin($bitset): void + { + /** @var string $thisBitset */ + $thisBitset = $this->bitset; + + $size = \strlen($thisBitset); + $sizeIn = \strlen($bitset); + + if ($sizeIn < $size) { + // add "\0" if the given bitset is not long enough + $bitset .= \str_repeat("\0", $size - $sizeIn); + } elseif ($sizeIn > $size) { + if (\ltrim(\substr($bitset, $size), "\0") !== '') { + throw new InvalidArgumentException('out-of-range bits detected'); + } + $bitset = \substr($bitset, 0, $size); + } + + // truncate out-of-range bits of last byte + $lastByteMaxOrd = $this->enumerationCount % 8; + if ($lastByteMaxOrd !== 0) { + $lastByte = $bitset[-1]; + $lastByteExpected = \chr((1 << $lastByteMaxOrd) - 1) & $lastByte; + if ($lastByte !== $lastByteExpected) { + throw new InvalidArgumentException('out-of-range bits detected'); + } + + $this->bitset = \substr($bitset, 0, -1) . $lastByteExpected; + } + + $this->bitset = $bitset; + } + + /** + * Set binary bitset in little-endian order + * + * @param string $bitset + * @return void + * @throws InvalidArgumentException On out-of-range bits given as input bitset + * @see setBinaryBitsetLeBin() + * @see doSetBinaryBitsetLeBin() + */ + private function doSetBinaryBitsetLeInt($bitset): void + { + $len = \strlen($bitset); + $int = 0; + for ($i = 0; $i < $len; ++$i) { + $ord = \ord($bitset[$i]); + + if ($ord && $i > \PHP_INT_SIZE - 1) { + throw new InvalidArgumentException('out-of-range bits detected'); + } + + $int |= $ord << (8 * $i); + } + + if ($int & (~0 << $this->enumerationCount)) { + throw new InvalidArgumentException('out-of-range bits detected'); + } + + $this->bitset = $int; + } + + /** + * Set the given binary bitset in big-endian order + * + * @param string $bitset + * @return void + * @throws InvalidArgumentException On out-of-range bits given as input bitset + */ + public function setBinaryBitsetBe(string $bitset): void + { + $this->{$this->fnDoSetBinaryBitsetLe}(\strrev($bitset)); + } + + /** + * Set a bit at the given ordinal number + * + * @param int $ordinal Ordinal number of bit to set + * @param bool $bit The bit to set + * @return void + * @throws InvalidArgumentException If the given ordinal number is out-of-range + * @uses doSetBitBin() + * @uses doSetBitInt() + * @uses doUnsetBitBin() + * @uses doUnsetBitInt() + */ + public function setBit(int $ordinal, bool $bit): void + { + if ($ordinal < 0 || $ordinal > $this->enumerationCount) { + throw new InvalidArgumentException("Ordinal number must be between 0 and {$this->enumerationCount}"); + } + + if ($bit) { + $this->{$this->fnDoSetBit}($ordinal); + } else { + $this->{$this->fnDoUnsetBit}($ordinal); + } + } + + /** + * Set a bit at the given ordinal number. + * + * This is the binary bitset implementation. + * + * @param int $ordinal Ordinal number of bit to set + * @return void + * @see setBit() + * @see doSetBitInt() + */ + private function doSetBitBin($ordinal): void + { + /** @var string $thisBitset */ + $thisBitset = $this->bitset; + + $byte = (int) ($ordinal / 8); + $thisBitset[$byte] = $thisBitset[$byte] | \chr(1 << ($ordinal % 8)); + + $this->bitset = $thisBitset; + } + + /** + * Set a bit at the given ordinal number. + * + * This is the binary bitset implementation. + * + * @param int $ordinal Ordinal number of bit to set + * @return void + * @see setBit() + * @see doSetBitBin() + */ + private function doSetBitInt($ordinal): void + { + /** @var int $thisBitset */ + $thisBitset = $this->bitset; + + $this->bitset = $thisBitset | (1 << $ordinal); + } + + /** + * Unset a bit at the given ordinal number. + * + * This is the binary bitset implementation. + * + * @param int $ordinal Ordinal number of bit to unset + * @return void + * @see setBit() + * @see doUnsetBitInt() + */ + private function doUnsetBitBin($ordinal): void + { + /** @var string $thisBitset */ + $thisBitset = $this->bitset; + + $byte = (int) ($ordinal / 8); + $thisBitset[$byte] = $thisBitset[$byte] & \chr(~(1 << ($ordinal % 8))); + + $this->bitset = $thisBitset; + } + + /** + * Unset a bit at the given ordinal number. + * + * This is the integer bitset implementation. + * + * @param int $ordinal Ordinal number of bit to unset + * @return void + * @see setBit() + * @see doUnsetBitBin() + */ + private function doUnsetBitInt($ordinal): void + { + /** @var int $thisBitset */ + $thisBitset = $this->bitset; + + $this->bitset = $thisBitset & ~(1 << $ordinal); + } + + /* write access (immutable) */ + + /** + * Creates a new set with the given enumerator object or value added + * @param T|null|bool|int|float|string|array $enumerator Enumerator object or value + * @return static + * @throws InvalidArgumentException On an invalid given enumerator + */ + public function with($enumerator): self + { + $clone = clone $this; + $clone->{$this->fnDoSetBit}(($this->enumeration)::get($enumerator)->getOrdinal()); + return $clone; + } + + /** + * Creates a new set with the given enumeration objects or values added + * @param iterable> $enumerators Iterable list of enumerator objects or values + * @return static + * @throws InvalidArgumentException On an invalid given enumerator + */ + public function withIterable(iterable $enumerators): self + { + $clone = clone $this; + foreach ($enumerators as $enumerator) { + $clone->{$this->fnDoSetBit}(($this->enumeration)::get($enumerator)->getOrdinal()); + } + return $clone; + } + + /** + * Create a new set with the given enumerator object or value removed + * @param T|null|bool|int|float|string|array $enumerator Enumerator object or value + * @return static + * @throws InvalidArgumentException On an invalid given enumerator + */ + public function without($enumerator): self + { + $clone = clone $this; + $clone->{$this->fnDoUnsetBit}(($this->enumeration)::get($enumerator)->getOrdinal()); + return $clone; + } + + /** + * Creates a new set with the given enumeration objects or values removed + * @param iterable> $enumerators Iterable list of enumerator objects or values + * @return static + * @throws InvalidArgumentException On an invalid given enumerator + */ + public function withoutIterable(iterable $enumerators): self + { + $clone = clone $this; + foreach ($enumerators as $enumerator) { + $clone->{$this->fnDoUnsetBit}(($this->enumeration)::get($enumerator)->getOrdinal()); + } + return $clone; + } + + /** + * Create a new set with enumerators from both this and other (this | other) + * + * @param EnumSet $other EnumSet of the same enumeration to produce the union + * @return static + * @throws InvalidArgumentException If $other doesn't match the enumeration + */ + public function withUnion(EnumSet $other): self + { + $clone = clone $this; + $clone->setUnion($other); + return $clone; + } + + /** + * Create a new set with enumerators from both this and other (this | other) + * + * @param EnumSet $other EnumSet of the same enumeration to produce the union + * @return static + * @throws InvalidArgumentException If $other doesn't match the enumeration + * @see withUnion() + * @see setUnion() + * @deprecated Will trigger deprecation warning in last 4.x and removed in 5.x + */ + public function union(EnumSet $other): self + { + return $this->withUnion($other); + } + + /** + * Create a new set with enumerators common to both this and other (this & other) + * + * @param EnumSet $other EnumSet of the same enumeration to produce the intersect + * @return static + * @throws InvalidArgumentException If $other doesn't match the enumeration + */ + public function withIntersect(EnumSet $other): self + { + $clone = clone $this; + $clone->setIntersect($other); + return $clone; + } + + /** + * Create a new set with enumerators common to both this and other (this & other) + * + * @param EnumSet $other EnumSet of the same enumeration to produce the intersect + * @return static + * @throws InvalidArgumentException If $other doesn't match the enumeration + * @see withIntersect() + * @see setIntersect() + * @deprecated Will trigger deprecation warning in last 4.x and removed in 5.x + */ + public function intersect(EnumSet $other): self + { + return $this->withIntersect($other); + } + + /** + * Create a new set with enumerators in this but not in other (this - other) + * + * @param EnumSet $other EnumSet of the same enumeration to produce the diff + * @return static + * @throws InvalidArgumentException If $other doesn't match the enumeration + */ + public function withDiff(EnumSet $other): self + { + $clone = clone $this; + $clone->setDiff($other); + return $clone; + } + + /** + * Create a new set with enumerators in this but not in other (this - other) + * + * @param EnumSet $other EnumSet of the same enumeration to produce the diff + * @return static + * @throws InvalidArgumentException If $other doesn't match the enumeration + * @see withDiff() + * @see setDiff() + * @deprecated Will trigger deprecation warning in last 4.x and removed in 5.x + */ + public function diff(EnumSet $other): self + { + return $this->withDiff($other); + } + + /** + * Create a new set with enumerators in either this and other but not in both (this ^ other) + * + * @param EnumSet $other EnumSet of the same enumeration to produce the symmetric difference + * @return static + * @throws InvalidArgumentException If $other doesn't match the enumeration + */ + public function withSymDiff(EnumSet $other): self + { + $clone = clone $this; + $clone->setSymDiff($other); + return $clone; + } + + /** + * Create a new set with enumerators in either this and other but not in both (this ^ other) + * + * @param EnumSet $other EnumSet of the same enumeration to produce the symmetric difference + * @return static + * @throws InvalidArgumentException If $other doesn't match the enumeration + * @see withSymDiff() + * @see setSymDiff() + * @deprecated Will trigger deprecation warning in last 4.x and removed in 5.x + */ + public function symDiff(EnumSet $other): self + { + return $this->withSymDiff($other); + } + + /** + * Create a new set with the given binary bitset in little-endian order + * + * @param string $bitset + * @return static + * @throws InvalidArgumentException On out-of-range bits given as input bitset + * @uses doSetBinaryBitsetLeBin() + * @uses doSetBinaryBitsetLeInt() + */ + public function withBinaryBitsetLe(string $bitset): self + { + $clone = clone $this; + $clone->{$this->fnDoSetBinaryBitsetLe}($bitset); + return $clone; + } + + /** + * Create a new set with the given binary bitset in big-endian order + * + * @param string $bitset + * @return static + * @throws InvalidArgumentException On out-of-range bits given as input bitset + */ + public function withBinaryBitsetBe(string $bitset): self + { + $clone = $this; + $clone->{$this->fnDoSetBinaryBitsetLe}(\strrev($bitset)); + return $clone; + } + + /** + * Create a new set with the bit at the given ordinal number set + * + * @param int $ordinal Ordinal number of bit to set + * @param bool $bit The bit to set + * @return static + * @throws InvalidArgumentException If the given ordinal number is out-of-range + * @uses doSetBitBin() + * @uses doSetBitInt() + * @uses doUnsetBitBin() + * @uses doUnsetBitInt() + */ + public function withBit(int $ordinal, bool $bit): self + { + $clone = clone $this; + $clone->setBit($ordinal, $bit); + return $clone; + } + + /* read access */ + + /** + * Test if the given enumerator exists + * @param T|null|bool|int|float|string|array $enumerator + * @return bool + */ + public function has($enumerator): bool + { + return $this->{$this->fnDoGetBit}(($this->enumeration)::get($enumerator)->getOrdinal()); + } + + /** + * Test if the given enumerator exists + * @param T|null|bool|int|float|string|array $enumerator + * @return bool + * @see has() + * @deprecated Will trigger deprecation warning in last 4.x and removed in 5.x + */ + public function contains($enumerator): bool + { + return $this->has($enumerator); + } + + /* IteratorAggregate */ + + /** + * Get a new iterator + * @return Iterator + * @uses doGetIteratorInt() + * @uses doGetIteratorBin() + */ + public function getIterator(): Iterator + { + return $this->{$this->fnDoGetIterator}(); + } + + /** + * Get a new Iterator. + * + * This is the binary bitset implementation. + * + * @return Iterator + * @see getIterator() + * @see goGetIteratorInt() + */ + private function doGetIteratorBin() + { + /** @var string $bitset */ + $bitset = $this->bitset; + $byteLen = \strlen($bitset); + for ($bytePos = 0; $bytePos < $byteLen; ++$bytePos) { + if ($bitset[$bytePos] === "\0") { + // fast skip null byte + continue; + } + + $ord = \ord($bitset[$bytePos]); + for ($bitPos = 0; $bitPos < 8; ++$bitPos) { + if ($ord & (1 << $bitPos)) { + $ordinal = $bytePos * 8 + $bitPos; + yield $ordinal => ($this->enumeration)::byOrdinal($ordinal); + } + } + } + } + + /** + * Get a new Iterator. + * + * This is the integer bitset implementation. + * + * @return Iterator + * @see getIterator() + * @see doGetIteratorBin() + */ + private function doGetIteratorInt() + { + /** @var int $bitset */ + $bitset = $this->bitset; + $count = $this->enumerationCount; + for ($ordinal = 0; $ordinal < $count; ++$ordinal) { + if ($bitset & (1 << $ordinal)) { + yield $ordinal => ($this->enumeration)::byOrdinal($ordinal); + } + } + } + + /* Countable */ + + /** + * Count the number of elements + * + * @return int + * @uses doCountBin() + * @uses doCountInt() + */ + public function count(): int + { + return $this->{$this->fnDoCount}(); + } + + /** + * Count the number of elements. + * + * This is the binary bitset implementation. + * + * @return int + * @see count() + * @see doCountInt() + */ + private function doCountBin() + { + /** @var string $bitset */ + $bitset = $this->bitset; + $count = 0; + $byteLen = \strlen($bitset); + for ($bytePos = 0; $bytePos < $byteLen; ++$bytePos) { + if ($bitset[$bytePos] === "\0") { + // fast skip null byte + continue; + } + + $ord = \ord($bitset[$bytePos]); + if ($ord & 0b00000001) ++$count; + if ($ord & 0b00000010) ++$count; + if ($ord & 0b00000100) ++$count; + if ($ord & 0b00001000) ++$count; + if ($ord & 0b00010000) ++$count; + if ($ord & 0b00100000) ++$count; + if ($ord & 0b01000000) ++$count; + if ($ord & 0b10000000) ++$count; + } + return $count; + } + + /** + * Count the number of elements. + * + * This is the integer bitset implementation. + * + * @return int + * @see count() + * @see doCountBin() + */ + private function doCountInt() + { + /** @var int $bitset */ + $bitset = $this->bitset; + $count = 0; + + // PHP does not support right shift unsigned + if ($bitset < 0) { + $count = 1; + $bitset = $bitset & \PHP_INT_MAX; + } + + // iterate byte by byte and count set bits + $phpIntBitSize = \PHP_INT_SIZE * 8; + for ($bitPos = 0; $bitPos < $phpIntBitSize; $bitPos += 8) { + $bitChk = 0xff << $bitPos; + $byte = $bitset & $bitChk; + if ($byte) { + $byte = $byte >> $bitPos; + if ($byte & 0b00000001) ++$count; + if ($byte & 0b00000010) ++$count; + if ($byte & 0b00000100) ++$count; + if ($byte & 0b00001000) ++$count; + if ($byte & 0b00010000) ++$count; + if ($byte & 0b00100000) ++$count; + if ($byte & 0b01000000) ++$count; + if ($byte & 0b10000000) ++$count; + } + + if ($bitset <= $bitChk) { + break; + } + } + + return $count; + } + + /** + * Check if this EnumSet is the same as other + * @param EnumSet $other + * @return bool + */ + public function isEqual(EnumSet $other): bool + { + return $this->enumeration === $other->enumeration + && $this->bitset === $other->bitset; + } + + /** + * Check if this EnumSet is a subset of other + * @param EnumSet $other + * @return bool + */ + public function isSubset(EnumSet $other): bool + { + return $this->enumeration === $other->enumeration + && ($this->bitset & $other->bitset) === $this->bitset; + } + + /** + * Check if this EnumSet is a superset of other + * @param EnumSet $other + * @return bool + */ + public function isSuperset(EnumSet $other): bool + { + return $this->enumeration === $other->enumeration + && ($this->bitset | $other->bitset) === $this->bitset; + } + + /** + * Tests if the set is empty + * + * @return bool + */ + public function isEmpty(): bool + { + return $this->bitset === $this->emptyBitset; + } + + /** + * Get ordinal numbers of the defined enumerators as array + * @return array + * @uses doGetOrdinalsBin() + * @uses doGetOrdinalsInt() + */ + public function getOrdinals(): array + { + return $this->{$this->fnDoGetOrdinals}(); + } + + /** + * Get ordinal numbers of the defined enumerators as array. + * + * This is the binary bitset implementation. + * + * @return array + * @see getOrdinals() + * @see goGetOrdinalsInt() + */ + private function doGetOrdinalsBin() + { + /** @var string $bitset */ + $bitset = $this->bitset; + $ordinals = []; + $byteLen = \strlen($bitset); + for ($bytePos = 0; $bytePos < $byteLen; ++$bytePos) { + if ($bitset[$bytePos] === "\0") { + // fast skip null byte + continue; + } + + $ord = \ord($bitset[$bytePos]); + for ($bitPos = 0; $bitPos < 8; ++$bitPos) { + if ($ord & (1 << $bitPos)) { + $ordinals[] = $bytePos * 8 + $bitPos; + } + } + } + return $ordinals; + } + + /** + * Get ordinal numbers of the defined enumerators as array. + * + * This is the integer bitset implementation. + * + * @return array + * @see getOrdinals() + * @see doGetOrdinalsBin() + */ + private function doGetOrdinalsInt() + { + /** @var int $bitset */ + $bitset = $this->bitset; + $ordinals = []; + $count = $this->enumerationCount; + for ($ordinal = 0; $ordinal < $count; ++$ordinal) { + if ($bitset & (1 << $ordinal)) { + $ordinals[] = $ordinal; + } + } + return $ordinals; + } + + /** + * Get values of the defined enumerators as array + * @return (null|bool|int|float|string|array)[] + * + * @phpstan-return array> + * @psalm-return list + */ + public function getValues(): array + { + $enumeration = $this->enumeration; + $values = []; + foreach ($this->getOrdinals() as $ord) { + $values[] = $enumeration::byOrdinal($ord)->getValue(); + } + return $values; + } + + /** + * Get names of the defined enumerators as array + * @return string[] + * + * @phpstan-return array + * @psalm-return list + */ + public function getNames(): array + { + $enumeration = $this->enumeration; + $names = []; + foreach ($this->getOrdinals() as $ord) { + $names[] = $enumeration::byOrdinal($ord)->getName(); + } + return $names; + } + + /** + * Get the defined enumerators as array + * @return Enum[] + * + * @phpstan-return array + * @psalm-return list + */ + public function getEnumerators(): array + { + $enumeration = $this->enumeration; + $enumerators = []; + foreach ($this->getOrdinals() as $ord) { + $enumerators[] = $enumeration::byOrdinal($ord); + } + return $enumerators; + } + + /** + * Get binary bitset in little-endian order + * + * @return string + * @uses doGetBinaryBitsetLeBin() + * @uses doGetBinaryBitsetLeInt() + */ + public function getBinaryBitsetLe(): string + { + return $this->{$this->fnDoGetBinaryBitsetLe}(); + } + + /** + * Get binary bitset in little-endian order. + * + * This is the binary bitset implementation. + * + * @return string + * @see getBinaryBitsetLe() + * @see doGetBinaryBitsetLeInt() + */ + private function doGetBinaryBitsetLeBin() + { + /** @var string $bitset */ + $bitset = $this->bitset; + + return $bitset; + } + + /** + * Get binary bitset in little-endian order. + * + * This is the integer bitset implementation. + * + * @return string + * @see getBinaryBitsetLe() + * @see doGetBinaryBitsetLeBin() + */ + private function doGetBinaryBitsetLeInt() + { + $bin = \pack(\PHP_INT_SIZE === 8 ? 'P' : 'V', $this->bitset); + return \substr($bin, 0, (int)\ceil($this->enumerationCount / 8)); + } + + /** + * Get binary bitset in big-endian order + * + * @return string + */ + public function getBinaryBitsetBe(): string + { + return \strrev($this->getBinaryBitsetLe()); + } + + /** + * Get a bit at the given ordinal number + * + * @param int $ordinal Ordinal number of bit to get + * @return bool + * @throws InvalidArgumentException If the given ordinal number is out-of-range + * @uses doGetBitBin() + * @uses doGetBitInt() + */ + public function getBit(int $ordinal): bool + { + if ($ordinal < 0 || $ordinal > $this->enumerationCount) { + throw new InvalidArgumentException("Ordinal number must be between 0 and {$this->enumerationCount}"); + } + + return $this->{$this->fnDoGetBit}($ordinal); + } + + /** + * Get a bit at the given ordinal number. + * + * This is the binary bitset implementation. + * + * @param int $ordinal Ordinal number of bit to get + * @return bool + * @see getBit() + * @see doGetBitInt() + */ + private function doGetBitBin($ordinal) + { + /** @var string $bitset */ + $bitset = $this->bitset; + + return (\ord($bitset[(int) ($ordinal / 8)]) & 1 << ($ordinal % 8)) !== 0; + } + + /** + * Get a bit at the given ordinal number. + * + * This is the integer bitset implementation. + * + * @param int $ordinal Ordinal number of bit to get + * @return bool + * @see getBit() + * @see doGetBitBin() + */ + private function doGetBitInt($ordinal) + { + /** @var int $bitset */ + $bitset = $this->bitset; + + return (bool)($bitset & (1 << $ordinal)); + } +} diff --git a/3rdparty/marc-mabe/php-enum/stubs/Stringable.php b/3rdparty/marc-mabe/php-enum/stubs/Stringable.php new file mode 100644 index 00000000..77e037cb --- /dev/null +++ b/3rdparty/marc-mabe/php-enum/stubs/Stringable.php @@ -0,0 +1,11 @@ + false, + + // Prevents the parser from automatically assigning the HTML5 namespace to the DOM document. + 'disable_html_ns' => false, + ); + + protected $errors = array(); + + public function __construct(array $defaultOptions = array()) + { + $this->defaultOptions = array_merge($this->defaultOptions, $defaultOptions); + } + + /** + * Get the current default options. + * + * @return array + */ + public function getOptions() + { + return $this->defaultOptions; + } + + /** + * Load and parse an HTML file. + * + * This will apply the HTML5 parser, which is tolerant of many + * varieties of HTML, including XHTML 1, HTML 4, and well-formed HTML + * 3. Note that in these cases, not all of the old data will be + * preserved. For example, XHTML's XML declaration will be removed. + * + * The rules governing parsing are set out in the HTML 5 spec. + * + * @param string|resource $file The path to the file to parse. If this is a resource, it is + * assumed to be an open stream whose pointer is set to the first + * byte of input. + * @param array $options Configuration options when parsing the HTML. + * + * @return \DOMDocument A DOM document. These object type is defined by the libxml + * library, and should have been included with your version of PHP. + */ + public function load($file, array $options = array()) + { + // Handle the case where file is a resource. + if (is_resource($file)) { + return $this->parse(stream_get_contents($file), $options); + } + + return $this->parse(file_get_contents($file), $options); + } + + /** + * Parse a HTML Document from a string. + * + * Take a string of HTML 5 (or earlier) and parse it into a + * DOMDocument. + * + * @param string $string A html5 document as a string. + * @param array $options Configuration options when parsing the HTML. + * + * @return \DOMDocument A DOM document. DOM is part of libxml, which is included with + * almost all distribtions of PHP. + */ + public function loadHTML($string, array $options = array()) + { + return $this->parse($string, $options); + } + + /** + * Convenience function to load an HTML file. + * + * This is here to provide backwards compatibility with the + * PHP DOM implementation. It simply calls load(). + * + * @param string $file The path to the file to parse. If this is a resource, it is + * assumed to be an open stream whose pointer is set to the first + * byte of input. + * @param array $options Configuration options when parsing the HTML. + * + * @return \DOMDocument A DOM document. These object type is defined by the libxml + * library, and should have been included with your version of PHP. + */ + public function loadHTMLFile($file, array $options = array()) + { + return $this->load($file, $options); + } + + /** + * Parse a HTML fragment from a string. + * + * @param string $string the HTML5 fragment as a string + * @param array $options Configuration options when parsing the HTML + * + * @return \DOMDocumentFragment A DOM fragment. The DOM is part of libxml, which is included with + * almost all distributions of PHP. + */ + public function loadHTMLFragment($string, array $options = array()) + { + return $this->parseFragment($string, $options); + } + + /** + * Return all errors encountered into parsing phase. + * + * @return array + */ + public function getErrors() + { + return $this->errors; + } + + /** + * Return true it some errors were encountered into parsing phase. + * + * @return bool + */ + public function hasErrors() + { + return count($this->errors) > 0; + } + + /** + * Parse an input string. + * + * @param string $input + * @param array $options + * + * @return \DOMDocument + */ + public function parse($input, array $options = array()) + { + $this->errors = array(); + $options = array_merge($this->defaultOptions, $options); + $events = new DOMTreeBuilder(false, $options); + $scanner = new Scanner($input, !empty($options['encoding']) ? $options['encoding'] : 'UTF-8'); + $parser = new Tokenizer($scanner, $events, !empty($options['xmlNamespaces']) ? Tokenizer::CONFORMANT_XML : Tokenizer::CONFORMANT_HTML); + + $parser->parse(); + $this->errors = $events->getErrors(); + + return $events->document(); + } + + /** + * Parse an input stream where the stream is a fragment. + * + * Lower-level loading function. This requires an input stream instead + * of a string, file, or resource. + * + * @param string $input The input data to parse in the form of a string. + * @param array $options An array of options. + * + * @return \DOMDocumentFragment + */ + public function parseFragment($input, array $options = array()) + { + $options = array_merge($this->defaultOptions, $options); + $events = new DOMTreeBuilder(true, $options); + $scanner = new Scanner($input, !empty($options['encoding']) ? $options['encoding'] : 'UTF-8'); + $parser = new Tokenizer($scanner, $events, !empty($options['xmlNamespaces']) ? Tokenizer::CONFORMANT_XML : Tokenizer::CONFORMANT_HTML); + + $parser->parse(); + $this->errors = $events->getErrors(); + + return $events->fragment(); + } + + /** + * Save a DOM into a given file as HTML5. + * + * @param mixed $dom The DOM to be serialized. + * @param string|resource $file The filename to be written or resource to write to. + * @param array $options Configuration options when serializing the DOM. These include: + * - encode_entities: Text written to the output is escaped by default and not all + * entities are encoded. If this is set to true all entities will be encoded. + * Defaults to false. + */ + public function save($dom, $file, $options = array()) + { + $close = true; + if (is_resource($file)) { + $stream = $file; + $close = false; + } else { + $stream = fopen($file, 'wb'); + } + $options = array_merge($this->defaultOptions, $options); + $rules = new OutputRules($stream, $options); + $trav = new Traverser($dom, $stream, $rules, $options); + + $trav->walk(); + /* + * release the traverser to avoid cyclic references and allow PHP to free memory without waiting for gc_collect_cycles + */ + $rules->unsetTraverser(); + if ($close) { + fclose($stream); + } + } + + /** + * Convert a DOM into an HTML5 string. + * + * @param mixed $dom The DOM to be serialized. + * @param array $options Configuration options when serializing the DOM. These include: + * - encode_entities: Text written to the output is escaped by default and not all + * entities are encoded. If this is set to true all entities will be encoded. + * Defaults to false. + * + * @return string A HTML5 documented generated from the DOM. + */ + public function saveHTML($dom, $options = array()) + { + $stream = fopen('php://temp', 'wb'); + $this->save($dom, $stream, array_merge($this->defaultOptions, $options)); + + $html = stream_get_contents($stream, -1, 0); + + fclose($stream); + + return $html; + } +} diff --git a/3rdparty/masterminds/html5/src/HTML5/Elements.php b/3rdparty/masterminds/html5/src/HTML5/Elements.php new file mode 100644 index 00000000..5d8cfd44 --- /dev/null +++ b/3rdparty/masterminds/html5/src/HTML5/Elements.php @@ -0,0 +1,637 @@ + [PARENT-TAG-NAME-TO-CLOSE1, PARENT-TAG-NAME-TO-CLOSE2, ...]. + * + * Order is important, after auto-closing one parent with might have to close also their parent. + * + * @var array + */ + public static $optionalEndElementsParentsToClose = array( + 'tr' => array('td', 'tr'), + 'td' => array('td', 'th'), + 'th' => array('td', 'th'), + 'tfoot' => array('td', 'th', 'tr', 'tbody', 'thead'), + 'tbody' => array('td', 'th', 'tr', 'thead'), + ); + + /** + * The HTML5 elements as defined in http://dev.w3.org/html5/markup/elements.html. + * + * @var array + */ + public static $html5 = array( + 'a' => 1, + 'abbr' => 1, + 'address' => 65, // NORMAL | BLOCK_TAG + 'area' => 9, // NORMAL | VOID_TAG + 'article' => 81, // NORMAL | AUTOCLOSE_P | BLOCK_TAG + 'aside' => 81, // NORMAL | AUTOCLOSE_P | BLOCK_TAG + 'audio' => 1, // NORMAL + 'b' => 1, + 'base' => 9, // NORMAL | VOID_TAG + 'bdi' => 1, + 'bdo' => 1, + 'blockquote' => 81, // NORMAL | AUTOCLOSE_P | BLOCK_TAG + 'body' => 1, + 'br' => 9, // NORMAL | VOID_TAG + 'button' => 1, + 'canvas' => 65, // NORMAL | BLOCK_TAG + 'caption' => 1, + 'cite' => 1, + 'code' => 1, + 'col' => 9, // NORMAL | VOID_TAG + 'colgroup' => 1, + 'command' => 9, // NORMAL | VOID_TAG + // "data" => 1, // This is highly experimental and only part of the whatwg spec (not w3c). See https://developer.mozilla.org/en-US/docs/HTML/Element/data + 'datalist' => 1, + 'dd' => 65, // NORMAL | BLOCK_TAG + 'del' => 1, + 'details' => 17, // NORMAL | AUTOCLOSE_P, + 'dfn' => 1, + 'dialog' => 17, // NORMAL | AUTOCLOSE_P, + 'div' => 81, // NORMAL | AUTOCLOSE_P | BLOCK_TAG + 'dl' => 81, // NORMAL | AUTOCLOSE_P | BLOCK_TAG + 'dt' => 1, + 'em' => 1, + 'embed' => 9, // NORMAL | VOID_TAG + 'fieldset' => 81, // NORMAL | AUTOCLOSE_P | BLOCK_TAG + 'figcaption' => 81, // NORMAL | AUTOCLOSE_P | BLOCK_TAG + 'figure' => 81, // NORMAL | AUTOCLOSE_P | BLOCK_TAG + 'footer' => 81, // NORMAL | AUTOCLOSE_P | BLOCK_TAG + 'form' => 81, // NORMAL | AUTOCLOSE_P | BLOCK_TAG + 'h1' => 81, // NORMAL | AUTOCLOSE_P | BLOCK_TAG + 'h2' => 81, // NORMAL | AUTOCLOSE_P | BLOCK_TAG + 'h3' => 81, // NORMAL | AUTOCLOSE_P | BLOCK_TAG + 'h4' => 81, // NORMAL | AUTOCLOSE_P | BLOCK_TAG + 'h5' => 81, // NORMAL | AUTOCLOSE_P | BLOCK_TAG + 'h6' => 81, // NORMAL | AUTOCLOSE_P | BLOCK_TAG + 'head' => 1, + 'header' => 81, // NORMAL | AUTOCLOSE_P | BLOCK_TAG + 'hgroup' => 81, // NORMAL | AUTOCLOSE_P | BLOCK_TAG + 'hr' => 73, // NORMAL | VOID_TAG + 'html' => 1, + 'i' => 1, + 'iframe' => 3, // NORMAL | TEXT_RAW + 'img' => 9, // NORMAL | VOID_TAG + 'input' => 9, // NORMAL | VOID_TAG + 'kbd' => 1, + 'ins' => 1, + 'keygen' => 9, // NORMAL | VOID_TAG + 'label' => 1, + 'legend' => 1, + 'li' => 1, + 'link' => 9, // NORMAL | VOID_TAG + 'map' => 1, + 'mark' => 1, + 'menu' => 17, // NORMAL | AUTOCLOSE_P, + 'meta' => 9, // NORMAL | VOID_TAG + 'meter' => 1, + 'nav' => 17, // NORMAL | AUTOCLOSE_P, + 'noscript' => 65, // NORMAL | BLOCK_TAG + 'object' => 1, + 'ol' => 81, // NORMAL | AUTOCLOSE_P | BLOCK_TAG + 'optgroup' => 1, + 'option' => 1, + 'output' => 65, // NORMAL | BLOCK_TAG + 'p' => 209, // NORMAL | AUTOCLOSE_P | BLOCK_TAG | BLOCK_ONLY_INLINE + 'param' => 9, // NORMAL | VOID_TAG + 'pre' => 81, // NORMAL | AUTOCLOSE_P | BLOCK_TAG + 'progress' => 1, + 'q' => 1, + 'rp' => 1, + 'rt' => 1, + 'ruby' => 1, + 's' => 1, + 'samp' => 1, + 'script' => 3, // NORMAL | TEXT_RAW + 'section' => 81, // NORMAL | AUTOCLOSE_P | BLOCK_TAG + 'select' => 1, + 'small' => 1, + 'source' => 9, // NORMAL | VOID_TAG + 'span' => 1, + 'strong' => 1, + 'style' => 3, // NORMAL | TEXT_RAW + 'sub' => 1, + 'summary' => 17, // NORMAL | AUTOCLOSE_P, + 'sup' => 1, + 'table' => 65, // NORMAL | BLOCK_TAG + 'tbody' => 1, + 'td' => 1, + 'textarea' => 5, // NORMAL | TEXT_RCDATA + 'tfoot' => 65, // NORMAL | BLOCK_TAG + 'th' => 1, + 'thead' => 1, + 'time' => 1, + 'title' => 5, // NORMAL | TEXT_RCDATA + 'tr' => 1, + 'track' => 9, // NORMAL | VOID_TAG + 'u' => 1, + 'ul' => 81, // NORMAL | AUTOCLOSE_P | BLOCK_TAG + 'var' => 1, + 'video' => 1, + 'wbr' => 9, // NORMAL | VOID_TAG + + // Legacy? + 'basefont' => 8, // VOID_TAG + 'bgsound' => 8, // VOID_TAG + 'noframes' => 2, // RAW_TEXT + 'frame' => 9, // NORMAL | VOID_TAG + 'frameset' => 1, + 'center' => 16, + 'dir' => 16, + 'listing' => 16, // AUTOCLOSE_P + 'plaintext' => 48, // AUTOCLOSE_P | TEXT_PLAINTEXT + 'applet' => 0, + 'marquee' => 0, + 'isindex' => 8, // VOID_TAG + 'xmp' => 20, // AUTOCLOSE_P | VOID_TAG | RAW_TEXT + 'noembed' => 2, // RAW_TEXT + ); + + /** + * The MathML elements. + * See http://www.w3.org/wiki/MathML/Elements. + * + * In our case we are only concerned with presentation MathML and not content + * MathML. There is a nice list of this subset at https://developer.mozilla.org/en-US/docs/MathML/Element. + * + * @var array + */ + public static $mathml = array( + 'maction' => 1, + 'maligngroup' => 1, + 'malignmark' => 1, + 'math' => 1, + 'menclose' => 1, + 'merror' => 1, + 'mfenced' => 1, + 'mfrac' => 1, + 'mglyph' => 1, + 'mi' => 1, + 'mlabeledtr' => 1, + 'mlongdiv' => 1, + 'mmultiscripts' => 1, + 'mn' => 1, + 'mo' => 1, + 'mover' => 1, + 'mpadded' => 1, + 'mphantom' => 1, + 'mroot' => 1, + 'mrow' => 1, + 'ms' => 1, + 'mscarries' => 1, + 'mscarry' => 1, + 'msgroup' => 1, + 'msline' => 1, + 'mspace' => 1, + 'msqrt' => 1, + 'msrow' => 1, + 'mstack' => 1, + 'mstyle' => 1, + 'msub' => 1, + 'msup' => 1, + 'msubsup' => 1, + 'mtable' => 1, + 'mtd' => 1, + 'mtext' => 1, + 'mtr' => 1, + 'munder' => 1, + 'munderover' => 1, + ); + + /** + * The svg elements. + * + * The Mozilla documentation has a good list at https://developer.mozilla.org/en-US/docs/SVG/Element. + * The w3c list appears to be lacking in some areas like filter effect elements. + * That list can be found at http://www.w3.org/wiki/SVG/Elements. + * + * Note, FireFox appears to do a better job rendering filter effects than chrome. + * While they are in the spec I'm not sure how widely implemented they are. + * + * @var array + */ + public static $svg = array( + 'a' => 1, + 'altGlyph' => 1, + 'altGlyphDef' => 1, + 'altGlyphItem' => 1, + 'animate' => 1, + 'animateColor' => 1, + 'animateMotion' => 1, + 'animateTransform' => 1, + 'circle' => 1, + 'clipPath' => 1, + 'color-profile' => 1, + 'cursor' => 1, + 'defs' => 1, + 'desc' => 1, + 'ellipse' => 1, + 'feBlend' => 1, + 'feColorMatrix' => 1, + 'feComponentTransfer' => 1, + 'feComposite' => 1, + 'feConvolveMatrix' => 1, + 'feDiffuseLighting' => 1, + 'feDisplacementMap' => 1, + 'feDistantLight' => 1, + 'feFlood' => 1, + 'feFuncA' => 1, + 'feFuncB' => 1, + 'feFuncG' => 1, + 'feFuncR' => 1, + 'feGaussianBlur' => 1, + 'feImage' => 1, + 'feMerge' => 1, + 'feMergeNode' => 1, + 'feMorphology' => 1, + 'feOffset' => 1, + 'fePointLight' => 1, + 'feSpecularLighting' => 1, + 'feSpotLight' => 1, + 'feTile' => 1, + 'feTurbulence' => 1, + 'filter' => 1, + 'font' => 1, + 'font-face' => 1, + 'font-face-format' => 1, + 'font-face-name' => 1, + 'font-face-src' => 1, + 'font-face-uri' => 1, + 'foreignObject' => 1, + 'g' => 1, + 'glyph' => 1, + 'glyphRef' => 1, + 'hkern' => 1, + 'image' => 1, + 'line' => 1, + 'linearGradient' => 1, + 'marker' => 1, + 'mask' => 1, + 'metadata' => 1, + 'missing-glyph' => 1, + 'mpath' => 1, + 'path' => 1, + 'pattern' => 1, + 'polygon' => 1, + 'polyline' => 1, + 'radialGradient' => 1, + 'rect' => 1, + 'script' => 3, // NORMAL | RAW_TEXT + 'set' => 1, + 'stop' => 1, + 'style' => 3, // NORMAL | RAW_TEXT + 'svg' => 1, + 'switch' => 1, + 'symbol' => 1, + 'text' => 1, + 'textPath' => 1, + 'title' => 1, + 'tref' => 1, + 'tspan' => 1, + 'use' => 1, + 'view' => 1, + 'vkern' => 1, + ); + + /** + * Some attributes in SVG are case sensitive. + * + * This map contains key/value pairs with the key as the lowercase attribute + * name and the value with the correct casing. + */ + public static $svgCaseSensitiveAttributeMap = array( + 'attributename' => 'attributeName', + 'attributetype' => 'attributeType', + 'basefrequency' => 'baseFrequency', + 'baseprofile' => 'baseProfile', + 'calcmode' => 'calcMode', + 'clippathunits' => 'clipPathUnits', + 'contentscripttype' => 'contentScriptType', + 'contentstyletype' => 'contentStyleType', + 'diffuseconstant' => 'diffuseConstant', + 'edgemode' => 'edgeMode', + 'externalresourcesrequired' => 'externalResourcesRequired', + 'filterres' => 'filterRes', + 'filterunits' => 'filterUnits', + 'glyphref' => 'glyphRef', + 'gradienttransform' => 'gradientTransform', + 'gradientunits' => 'gradientUnits', + 'kernelmatrix' => 'kernelMatrix', + 'kernelunitlength' => 'kernelUnitLength', + 'keypoints' => 'keyPoints', + 'keysplines' => 'keySplines', + 'keytimes' => 'keyTimes', + 'lengthadjust' => 'lengthAdjust', + 'limitingconeangle' => 'limitingConeAngle', + 'markerheight' => 'markerHeight', + 'markerunits' => 'markerUnits', + 'markerwidth' => 'markerWidth', + 'maskcontentunits' => 'maskContentUnits', + 'maskunits' => 'maskUnits', + 'numoctaves' => 'numOctaves', + 'pathlength' => 'pathLength', + 'patterncontentunits' => 'patternContentUnits', + 'patterntransform' => 'patternTransform', + 'patternunits' => 'patternUnits', + 'pointsatx' => 'pointsAtX', + 'pointsaty' => 'pointsAtY', + 'pointsatz' => 'pointsAtZ', + 'preservealpha' => 'preserveAlpha', + 'preserveaspectratio' => 'preserveAspectRatio', + 'primitiveunits' => 'primitiveUnits', + 'refx' => 'refX', + 'refy' => 'refY', + 'repeatcount' => 'repeatCount', + 'repeatdur' => 'repeatDur', + 'requiredextensions' => 'requiredExtensions', + 'requiredfeatures' => 'requiredFeatures', + 'specularconstant' => 'specularConstant', + 'specularexponent' => 'specularExponent', + 'spreadmethod' => 'spreadMethod', + 'startoffset' => 'startOffset', + 'stddeviation' => 'stdDeviation', + 'stitchtiles' => 'stitchTiles', + 'surfacescale' => 'surfaceScale', + 'systemlanguage' => 'systemLanguage', + 'tablevalues' => 'tableValues', + 'targetx' => 'targetX', + 'targety' => 'targetY', + 'textlength' => 'textLength', + 'viewbox' => 'viewBox', + 'viewtarget' => 'viewTarget', + 'xchannelselector' => 'xChannelSelector', + 'ychannelselector' => 'yChannelSelector', + 'zoomandpan' => 'zoomAndPan', + ); + + /** + * Some SVG elements are case sensitive. + * This map contains these. + * + * The map contains key/value store of the name is lowercase as the keys and + * the correct casing as the value. + */ + public static $svgCaseSensitiveElementMap = array( + 'altglyph' => 'altGlyph', + 'altglyphdef' => 'altGlyphDef', + 'altglyphitem' => 'altGlyphItem', + 'animatecolor' => 'animateColor', + 'animatemotion' => 'animateMotion', + 'animatetransform' => 'animateTransform', + 'clippath' => 'clipPath', + 'feblend' => 'feBlend', + 'fecolormatrix' => 'feColorMatrix', + 'fecomponenttransfer' => 'feComponentTransfer', + 'fecomposite' => 'feComposite', + 'feconvolvematrix' => 'feConvolveMatrix', + 'fediffuselighting' => 'feDiffuseLighting', + 'fedisplacementmap' => 'feDisplacementMap', + 'fedistantlight' => 'feDistantLight', + 'feflood' => 'feFlood', + 'fefunca' => 'feFuncA', + 'fefuncb' => 'feFuncB', + 'fefuncg' => 'feFuncG', + 'fefuncr' => 'feFuncR', + 'fegaussianblur' => 'feGaussianBlur', + 'feimage' => 'feImage', + 'femerge' => 'feMerge', + 'femergenode' => 'feMergeNode', + 'femorphology' => 'feMorphology', + 'feoffset' => 'feOffset', + 'fepointlight' => 'fePointLight', + 'fespecularlighting' => 'feSpecularLighting', + 'fespotlight' => 'feSpotLight', + 'fetile' => 'feTile', + 'feturbulence' => 'feTurbulence', + 'foreignobject' => 'foreignObject', + 'glyphref' => 'glyphRef', + 'lineargradient' => 'linearGradient', + 'radialgradient' => 'radialGradient', + 'textpath' => 'textPath', + ); + + /** + * Check whether the given element meets the given criterion. + * + * Example: + * + * Elements::isA('script', Elements::TEXT_RAW); // Returns true. + * + * Elements::isA('script', Elements::TEXT_RCDATA); // Returns false. + * + * @param string $name The element name. + * @param int $mask One of the constants on this class. + * + * @return bool true if the element matches the mask, false otherwise. + */ + public static function isA($name, $mask) + { + return (static::element($name) & $mask) === $mask; + } + + /** + * Test if an element is a valid html5 element. + * + * @param string $name The name of the element. + * + * @return bool true if a html5 element and false otherwise. + */ + public static function isHtml5Element($name) + { + // html5 element names are case insensitive. Forcing lowercase for the check. + // Do we need this check or will all data passed here already be lowercase? + return isset(static::$html5[strtolower($name)]); + } + + /** + * Test if an element name is a valid MathML presentation element. + * + * @param string $name The name of the element. + * + * @return bool true if a MathML name and false otherwise. + */ + public static function isMathMLElement($name) + { + // MathML is case-sensitive unlike html5 elements. + return isset(static::$mathml[$name]); + } + + /** + * Test if an element is a valid SVG element. + * + * @param string $name The name of the element. + * + * @return bool true if a SVG element and false otherise. + */ + public static function isSvgElement($name) + { + // SVG is case-sensitive unlike html5 elements. + return isset(static::$svg[$name]); + } + + /** + * Is an element name valid in an html5 document. + * This includes html5 elements along with other allowed embedded content + * such as svg and mathml. + * + * @param string $name The name of the element. + * + * @return bool true if valid and false otherwise. + */ + public static function isElement($name) + { + return static::isHtml5Element($name) || static::isMathMLElement($name) || static::isSvgElement($name); + } + + /** + * Get the element mask for the given element name. + * + * @param string $name The name of the element. + * + * @return int the element mask. + */ + public static function element($name) + { + if (isset(static::$html5[$name])) { + return static::$html5[$name]; + } + if (isset(static::$svg[$name])) { + return static::$svg[$name]; + } + if (isset(static::$mathml[$name])) { + return static::$mathml[$name]; + } + + return 0; + } + + /** + * Normalize a SVG element name to its proper case and form. + * + * @param string $name The name of the element. + * + * @return string the normalized form of the element name. + */ + public static function normalizeSvgElement($name) + { + $name = strtolower($name); + if (isset(static::$svgCaseSensitiveElementMap[$name])) { + $name = static::$svgCaseSensitiveElementMap[$name]; + } + + return $name; + } + + /** + * Normalize a SVG attribute name to its proper case and form. + * + * @param string $name The name of the attribute. + * + * @return string The normalized form of the attribute name. + */ + public static function normalizeSvgAttribute($name) + { + $name = strtolower($name); + if (isset(static::$svgCaseSensitiveAttributeMap[$name])) { + $name = static::$svgCaseSensitiveAttributeMap[$name]; + } + + return $name; + } + + /** + * Normalize a MathML attribute name to its proper case and form. + * Note, all MathML element names are lowercase. + * + * @param string $name The name of the attribute. + * + * @return string The normalized form of the attribute name. + */ + public static function normalizeMathMlAttribute($name) + { + $name = strtolower($name); + + // Only one attribute has a mixed case form for MathML. + if ('definitionurl' === $name) { + $name = 'definitionURL'; + } + + return $name; + } +} diff --git a/3rdparty/masterminds/html5/src/HTML5/Entities.php b/3rdparty/masterminds/html5/src/HTML5/Entities.php new file mode 100644 index 00000000..0e7227dc --- /dev/null +++ b/3rdparty/masterminds/html5/src/HTML5/Entities.php @@ -0,0 +1,2236 @@ + 'Á', + 'Aacut' => 'Á', + 'aacute' => 'á', + 'aacut' => 'á', + 'Abreve' => 'Ă', + 'abreve' => 'ă', + 'ac' => '∾', + 'acd' => '∿', + 'acE' => '∾̳', + 'Acirc' => 'Â', + 'Acir' => 'Â', + 'acirc' => 'â', + 'acir' => 'â', + 'acute' => '´', + 'acut' => '´', + 'Acy' => 'А', + 'acy' => 'а', + 'AElig' => 'Æ', + 'AEli' => 'Æ', + 'aelig' => 'æ', + 'aeli' => 'æ', + 'af' => '⁡', + 'Afr' => '𝔄', + 'afr' => '𝔞', + 'Agrave' => 'À', + 'Agrav' => 'À', + 'agrave' => 'à', + 'agrav' => 'à', + 'alefsym' => 'ℵ', + 'aleph' => 'ℵ', + 'Alpha' => 'Α', + 'alpha' => 'α', + 'Amacr' => 'Ā', + 'amacr' => 'ā', + 'amalg' => '⨿', + 'AMP' => '&', + 'AM' => '&', + 'amp' => '&', + 'am' => '&', + 'And' => '⩓', + 'and' => '∧', + 'andand' => '⩕', + 'andd' => '⩜', + 'andslope' => '⩘', + 'andv' => '⩚', + 'ang' => '∠', + 'ange' => '⦤', + 'angle' => '∠', + 'angmsd' => '∡', + 'angmsdaa' => '⦨', + 'angmsdab' => '⦩', + 'angmsdac' => '⦪', + 'angmsdad' => '⦫', + 'angmsdae' => '⦬', + 'angmsdaf' => '⦭', + 'angmsdag' => '⦮', + 'angmsdah' => '⦯', + 'angrt' => '∟', + 'angrtvb' => '⊾', + 'angrtvbd' => '⦝', + 'angsph' => '∢', + 'angst' => 'Å', + 'angzarr' => '⍼', + 'Aogon' => 'Ą', + 'aogon' => 'ą', + 'Aopf' => '𝔸', + 'aopf' => '𝕒', + 'ap' => '≈', + 'apacir' => '⩯', + 'apE' => '⩰', + 'ape' => '≊', + 'apid' => '≋', + 'apos' => '\'', + 'ApplyFunction' => '⁡', + 'approx' => '≈', + 'approxeq' => '≊', + 'Aring' => 'Å', + 'Arin' => 'Å', + 'aring' => 'å', + 'arin' => 'å', + 'Ascr' => '𝒜', + 'ascr' => '𝒶', + 'Assign' => '≔', + 'ast' => '*', + 'asymp' => '≈', + 'asympeq' => '≍', + 'Atilde' => 'Ã', + 'Atild' => 'Ã', + 'atilde' => 'ã', + 'atild' => 'ã', + 'Auml' => 'Ä', + 'Aum' => 'Ä', + 'auml' => 'ä', + 'aum' => 'ä', + 'awconint' => '∳', + 'awint' => '⨑', + 'backcong' => '≌', + 'backepsilon' => '϶', + 'backprime' => '‵', + 'backsim' => '∽', + 'backsimeq' => '⋍', + 'Backslash' => '∖', + 'Barv' => '⫧', + 'barvee' => '⊽', + 'Barwed' => '⌆', + 'barwed' => '⌅', + 'barwedge' => '⌅', + 'bbrk' => '⎵', + 'bbrktbrk' => '⎶', + 'bcong' => '≌', + 'Bcy' => 'Б', + 'bcy' => 'б', + 'bdquo' => '„', + 'becaus' => '∵', + 'Because' => '∵', + 'because' => '∵', + 'bemptyv' => '⦰', + 'bepsi' => '϶', + 'bernou' => 'ℬ', + 'Bernoullis' => 'ℬ', + 'Beta' => 'Β', + 'beta' => 'β', + 'beth' => 'ℶ', + 'between' => '≬', + 'Bfr' => '𝔅', + 'bfr' => '𝔟', + 'bigcap' => '⋂', + 'bigcirc' => '◯', + 'bigcup' => '⋃', + 'bigodot' => '⨀', + 'bigoplus' => '⨁', + 'bigotimes' => '⨂', + 'bigsqcup' => '⨆', + 'bigstar' => '★', + 'bigtriangledown' => '▽', + 'bigtriangleup' => '△', + 'biguplus' => '⨄', + 'bigvee' => '⋁', + 'bigwedge' => '⋀', + 'bkarow' => '⤍', + 'blacklozenge' => '⧫', + 'blacksquare' => '▪', + 'blacktriangle' => '▴', + 'blacktriangledown' => '▾', + 'blacktriangleleft' => '◂', + 'blacktriangleright' => '▸', + 'blank' => '␣', + 'blk12' => '▒', + 'blk14' => '░', + 'blk34' => '▓', + 'block' => '█', + 'bne' => '=⃥', + 'bnequiv' => '≡⃥', + 'bNot' => '⫭', + 'bnot' => '⌐', + 'Bopf' => '𝔹', + 'bopf' => '𝕓', + 'bot' => '⊥', + 'bottom' => '⊥', + 'bowtie' => '⋈', + 'boxbox' => '⧉', + 'boxDL' => '╗', + 'boxDl' => '╖', + 'boxdL' => '╕', + 'boxdl' => '┐', + 'boxDR' => '╔', + 'boxDr' => '╓', + 'boxdR' => '╒', + 'boxdr' => '┌', + 'boxH' => '═', + 'boxh' => '─', + 'boxHD' => '╦', + 'boxHd' => '╤', + 'boxhD' => '╥', + 'boxhd' => '┬', + 'boxHU' => '╩', + 'boxHu' => '╧', + 'boxhU' => '╨', + 'boxhu' => '┴', + 'boxminus' => '⊟', + 'boxplus' => '⊞', + 'boxtimes' => '⊠', + 'boxUL' => '╝', + 'boxUl' => '╜', + 'boxuL' => '╛', + 'boxul' => '┘', + 'boxUR' => '╚', + 'boxUr' => '╙', + 'boxuR' => '╘', + 'boxur' => '└', + 'boxV' => '║', + 'boxv' => '│', + 'boxVH' => '╬', + 'boxVh' => '╫', + 'boxvH' => '╪', + 'boxvh' => '┼', + 'boxVL' => '╣', + 'boxVl' => '╢', + 'boxvL' => '╡', + 'boxvl' => '┤', + 'boxVR' => '╠', + 'boxVr' => '╟', + 'boxvR' => '╞', + 'boxvr' => '├', + 'bprime' => '‵', + 'Breve' => '˘', + 'breve' => '˘', + 'brvbar' => '¦', + 'brvba' => '¦', + 'Bscr' => 'ℬ', + 'bscr' => '𝒷', + 'bsemi' => '⁏', + 'bsim' => '∽', + 'bsime' => '⋍', + 'bsol' => '\\', + 'bsolb' => '⧅', + 'bsolhsub' => '⟈', + 'bull' => '•', + 'bullet' => '•', + 'bump' => '≎', + 'bumpE' => '⪮', + 'bumpe' => '≏', + 'Bumpeq' => '≎', + 'bumpeq' => '≏', + 'Cacute' => 'Ć', + 'cacute' => 'ć', + 'Cap' => '⋒', + 'cap' => '∩', + 'capand' => '⩄', + 'capbrcup' => '⩉', + 'capcap' => '⩋', + 'capcup' => '⩇', + 'capdot' => '⩀', + 'CapitalDifferentialD' => 'ⅅ', + 'caps' => '∩︀', + 'caret' => '⁁', + 'caron' => 'ˇ', + 'Cayleys' => 'ℭ', + 'ccaps' => '⩍', + 'Ccaron' => 'Č', + 'ccaron' => 'č', + 'Ccedil' => 'Ç', + 'Ccedi' => 'Ç', + 'ccedil' => 'ç', + 'ccedi' => 'ç', + 'Ccirc' => 'Ĉ', + 'ccirc' => 'ĉ', + 'Cconint' => '∰', + 'ccups' => '⩌', + 'ccupssm' => '⩐', + 'Cdot' => 'Ċ', + 'cdot' => 'ċ', + 'cedil' => '¸', + 'cedi' => '¸', + 'Cedilla' => '¸', + 'cemptyv' => '⦲', + 'cent' => '¢', + 'cen' => '¢', + 'CenterDot' => '·', + 'centerdot' => '·', + 'Cfr' => 'ℭ', + 'cfr' => '𝔠', + 'CHcy' => 'Ч', + 'chcy' => 'ч', + 'check' => '✓', + 'checkmark' => '✓', + 'Chi' => 'Χ', + 'chi' => 'χ', + 'cir' => '○', + 'circ' => 'ˆ', + 'circeq' => '≗', + 'circlearrowleft' => '↺', + 'circlearrowright' => '↻', + 'circledast' => '⊛', + 'circledcirc' => '⊚', + 'circleddash' => '⊝', + 'CircleDot' => '⊙', + 'circledR' => '®', + 'circledS' => 'Ⓢ', + 'CircleMinus' => '⊖', + 'CirclePlus' => '⊕', + 'CircleTimes' => '⊗', + 'cirE' => '⧃', + 'cire' => '≗', + 'cirfnint' => '⨐', + 'cirmid' => '⫯', + 'cirscir' => '⧂', + 'ClockwiseContourIntegral' => '∲', + 'CloseCurlyDoubleQuote' => '”', + 'CloseCurlyQuote' => '’', + 'clubs' => '♣', + 'clubsuit' => '♣', + 'Colon' => '∷', + 'colon' => ':', + 'Colone' => '⩴', + 'colone' => '≔', + 'coloneq' => '≔', + 'comma' => ',', + 'commat' => '@', + 'comp' => '∁', + 'compfn' => '∘', + 'complement' => '∁', + 'complexes' => 'ℂ', + 'cong' => '≅', + 'congdot' => '⩭', + 'Congruent' => '≡', + 'Conint' => '∯', + 'conint' => '∮', + 'ContourIntegral' => '∮', + 'Copf' => 'ℂ', + 'copf' => '𝕔', + 'coprod' => '∐', + 'Coproduct' => '∐', + 'COPY' => '©', + 'COP' => '©', + 'copy' => '©', + 'cop' => '©', + 'copysr' => '℗', + 'CounterClockwiseContourIntegral' => '∳', + 'crarr' => '↵', + 'Cross' => '⨯', + 'cross' => '✗', + 'Cscr' => '𝒞', + 'cscr' => '𝒸', + 'csub' => '⫏', + 'csube' => '⫑', + 'csup' => '⫐', + 'csupe' => '⫒', + 'ctdot' => '⋯', + 'cudarrl' => '⤸', + 'cudarrr' => '⤵', + 'cuepr' => '⋞', + 'cuesc' => '⋟', + 'cularr' => '↶', + 'cularrp' => '⤽', + 'Cup' => '⋓', + 'cup' => '∪', + 'cupbrcap' => '⩈', + 'CupCap' => '≍', + 'cupcap' => '⩆', + 'cupcup' => '⩊', + 'cupdot' => '⊍', + 'cupor' => '⩅', + 'cups' => '∪︀', + 'curarr' => '↷', + 'curarrm' => '⤼', + 'curlyeqprec' => '⋞', + 'curlyeqsucc' => '⋟', + 'curlyvee' => '⋎', + 'curlywedge' => '⋏', + 'curren' => '¤', + 'curre' => '¤', + 'curvearrowleft' => '↶', + 'curvearrowright' => '↷', + 'cuvee' => '⋎', + 'cuwed' => '⋏', + 'cwconint' => '∲', + 'cwint' => '∱', + 'cylcty' => '⌭', + 'Dagger' => '‡', + 'dagger' => '†', + 'daleth' => 'ℸ', + 'Darr' => '↡', + 'dArr' => '⇓', + 'darr' => '↓', + 'dash' => '‐', + 'Dashv' => '⫤', + 'dashv' => '⊣', + 'dbkarow' => '⤏', + 'dblac' => '˝', + 'Dcaron' => 'Ď', + 'dcaron' => 'ď', + 'Dcy' => 'Д', + 'dcy' => 'д', + 'DD' => 'ⅅ', + 'dd' => 'ⅆ', + 'ddagger' => '‡', + 'ddarr' => '⇊', + 'DDotrahd' => '⤑', + 'ddotseq' => '⩷', + 'deg' => '°', + 'de' => '°', + 'Del' => '∇', + 'Delta' => 'Δ', + 'delta' => 'δ', + 'demptyv' => '⦱', + 'dfisht' => '⥿', + 'Dfr' => '𝔇', + 'dfr' => '𝔡', + 'dHar' => '⥥', + 'dharl' => '⇃', + 'dharr' => '⇂', + 'DiacriticalAcute' => '´', + 'DiacriticalDot' => '˙', + 'DiacriticalDoubleAcute' => '˝', + 'DiacriticalGrave' => '`', + 'DiacriticalTilde' => '˜', + 'diam' => '⋄', + 'Diamond' => '⋄', + 'diamond' => '⋄', + 'diamondsuit' => '♦', + 'diams' => '♦', + 'die' => '¨', + 'DifferentialD' => 'ⅆ', + 'digamma' => 'ϝ', + 'disin' => '⋲', + 'div' => '÷', + 'divide' => '÷', + 'divid' => '÷', + 'divideontimes' => '⋇', + 'divonx' => '⋇', + 'DJcy' => 'Ђ', + 'djcy' => 'ђ', + 'dlcorn' => '⌞', + 'dlcrop' => '⌍', + 'dollar' => '$', + 'Dopf' => '𝔻', + 'dopf' => '𝕕', + 'Dot' => '¨', + 'dot' => '˙', + 'DotDot' => '⃜', + 'doteq' => '≐', + 'doteqdot' => '≑', + 'DotEqual' => '≐', + 'dotminus' => '∸', + 'dotplus' => '∔', + 'dotsquare' => '⊡', + 'doublebarwedge' => '⌆', + 'DoubleContourIntegral' => '∯', + 'DoubleDot' => '¨', + 'DoubleDownArrow' => '⇓', + 'DoubleLeftArrow' => '⇐', + 'DoubleLeftRightArrow' => '⇔', + 'DoubleLeftTee' => '⫤', + 'DoubleLongLeftArrow' => '⟸', + 'DoubleLongLeftRightArrow' => '⟺', + 'DoubleLongRightArrow' => '⟹', + 'DoubleRightArrow' => '⇒', + 'DoubleRightTee' => '⊨', + 'DoubleUpArrow' => '⇑', + 'DoubleUpDownArrow' => '⇕', + 'DoubleVerticalBar' => '∥', + 'DownArrow' => '↓', + 'Downarrow' => '⇓', + 'downarrow' => '↓', + 'DownArrowBar' => '⤓', + 'DownArrowUpArrow' => '⇵', + 'DownBreve' => '̑', + 'downdownarrows' => '⇊', + 'downharpoonleft' => '⇃', + 'downharpoonright' => '⇂', + 'DownLeftRightVector' => '⥐', + 'DownLeftTeeVector' => '⥞', + 'DownLeftVector' => '↽', + 'DownLeftVectorBar' => '⥖', + 'DownRightTeeVector' => '⥟', + 'DownRightVector' => '⇁', + 'DownRightVectorBar' => '⥗', + 'DownTee' => '⊤', + 'DownTeeArrow' => '↧', + 'drbkarow' => '⤐', + 'drcorn' => '⌟', + 'drcrop' => '⌌', + 'Dscr' => '𝒟', + 'dscr' => '𝒹', + 'DScy' => 'Ѕ', + 'dscy' => 'ѕ', + 'dsol' => '⧶', + 'Dstrok' => 'Đ', + 'dstrok' => 'đ', + 'dtdot' => '⋱', + 'dtri' => '▿', + 'dtrif' => '▾', + 'duarr' => '⇵', + 'duhar' => '⥯', + 'dwangle' => '⦦', + 'DZcy' => 'Џ', + 'dzcy' => 'џ', + 'dzigrarr' => '⟿', + 'Eacute' => 'É', + 'Eacut' => 'É', + 'eacute' => 'é', + 'eacut' => 'é', + 'easter' => '⩮', + 'Ecaron' => 'Ě', + 'ecaron' => 'ě', + 'ecir' => 'ê', + 'Ecirc' => 'Ê', + 'Ecir' => 'Ê', + 'ecirc' => 'ê', + 'ecolon' => '≕', + 'Ecy' => 'Э', + 'ecy' => 'э', + 'eDDot' => '⩷', + 'Edot' => 'Ė', + 'eDot' => '≑', + 'edot' => 'ė', + 'ee' => 'ⅇ', + 'efDot' => '≒', + 'Efr' => '𝔈', + 'efr' => '𝔢', + 'eg' => '⪚', + 'Egrave' => 'È', + 'Egrav' => 'È', + 'egrave' => 'è', + 'egrav' => 'è', + 'egs' => '⪖', + 'egsdot' => '⪘', + 'el' => '⪙', + 'Element' => '∈', + 'elinters' => '⏧', + 'ell' => 'ℓ', + 'els' => '⪕', + 'elsdot' => '⪗', + 'Emacr' => 'Ē', + 'emacr' => 'ē', + 'empty' => '∅', + 'emptyset' => '∅', + 'EmptySmallSquare' => '◻', + 'emptyv' => '∅', + 'EmptyVerySmallSquare' => '▫', + 'emsp' => ' ', + 'emsp13' => ' ', + 'emsp14' => ' ', + 'ENG' => 'Ŋ', + 'eng' => 'ŋ', + 'ensp' => ' ', + 'Eogon' => 'Ę', + 'eogon' => 'ę', + 'Eopf' => '𝔼', + 'eopf' => '𝕖', + 'epar' => '⋕', + 'eparsl' => '⧣', + 'eplus' => '⩱', + 'epsi' => 'ε', + 'Epsilon' => 'Ε', + 'epsilon' => 'ε', + 'epsiv' => 'ϵ', + 'eqcirc' => '≖', + 'eqcolon' => '≕', + 'eqsim' => '≂', + 'eqslantgtr' => '⪖', + 'eqslantless' => '⪕', + 'Equal' => '⩵', + 'equals' => '=', + 'EqualTilde' => '≂', + 'equest' => '≟', + 'Equilibrium' => '⇌', + 'equiv' => '≡', + 'equivDD' => '⩸', + 'eqvparsl' => '⧥', + 'erarr' => '⥱', + 'erDot' => '≓', + 'Escr' => 'ℰ', + 'escr' => 'ℯ', + 'esdot' => '≐', + 'Esim' => '⩳', + 'esim' => '≂', + 'Eta' => 'Η', + 'eta' => 'η', + 'ETH' => 'Ð', + 'ET' => 'Ð', + 'eth' => 'ð', + 'et' => 'ð', + 'Euml' => 'Ë', + 'Eum' => 'Ë', + 'euml' => 'ë', + 'eum' => 'ë', + 'euro' => '€', + 'excl' => '!', + 'exist' => '∃', + 'Exists' => '∃', + 'expectation' => 'ℰ', + 'ExponentialE' => 'ⅇ', + 'exponentiale' => 'ⅇ', + 'fallingdotseq' => '≒', + 'Fcy' => 'Ф', + 'fcy' => 'ф', + 'female' => '♀', + 'ffilig' => 'ffi', + 'fflig' => 'ff', + 'ffllig' => 'ffl', + 'Ffr' => '𝔉', + 'ffr' => '𝔣', + 'filig' => 'fi', + 'FilledSmallSquare' => '◼', + 'FilledVerySmallSquare' => '▪', + 'fjlig' => 'fj', + 'flat' => '♭', + 'fllig' => 'fl', + 'fltns' => '▱', + 'fnof' => 'ƒ', + 'Fopf' => '𝔽', + 'fopf' => '𝕗', + 'ForAll' => '∀', + 'forall' => '∀', + 'fork' => '⋔', + 'forkv' => '⫙', + 'Fouriertrf' => 'ℱ', + 'fpartint' => '⨍', + 'frac12' => '½', + 'frac1' => '¼', + 'frac13' => '⅓', + 'frac14' => '¼', + 'frac15' => '⅕', + 'frac16' => '⅙', + 'frac18' => '⅛', + 'frac23' => '⅔', + 'frac25' => '⅖', + 'frac34' => '¾', + 'frac3' => '¾', + 'frac35' => '⅗', + 'frac38' => '⅜', + 'frac45' => '⅘', + 'frac56' => '⅚', + 'frac58' => '⅝', + 'frac78' => '⅞', + 'frasl' => '⁄', + 'frown' => '⌢', + 'Fscr' => 'ℱ', + 'fscr' => '𝒻', + 'gacute' => 'ǵ', + 'Gamma' => 'Γ', + 'gamma' => 'γ', + 'Gammad' => 'Ϝ', + 'gammad' => 'ϝ', + 'gap' => '⪆', + 'Gbreve' => 'Ğ', + 'gbreve' => 'ğ', + 'Gcedil' => 'Ģ', + 'Gcirc' => 'Ĝ', + 'gcirc' => 'ĝ', + 'Gcy' => 'Г', + 'gcy' => 'г', + 'Gdot' => 'Ġ', + 'gdot' => 'ġ', + 'gE' => '≧', + 'ge' => '≥', + 'gEl' => '⪌', + 'gel' => '⋛', + 'geq' => '≥', + 'geqq' => '≧', + 'geqslant' => '⩾', + 'ges' => '⩾', + 'gescc' => '⪩', + 'gesdot' => '⪀', + 'gesdoto' => '⪂', + 'gesdotol' => '⪄', + 'gesl' => '⋛︀', + 'gesles' => '⪔', + 'Gfr' => '𝔊', + 'gfr' => '𝔤', + 'Gg' => '⋙', + 'gg' => '≫', + 'ggg' => '⋙', + 'gimel' => 'ℷ', + 'GJcy' => 'Ѓ', + 'gjcy' => 'ѓ', + 'gl' => '≷', + 'gla' => '⪥', + 'glE' => '⪒', + 'glj' => '⪤', + 'gnap' => '⪊', + 'gnapprox' => '⪊', + 'gnE' => '≩', + 'gne' => '⪈', + 'gneq' => '⪈', + 'gneqq' => '≩', + 'gnsim' => '⋧', + 'Gopf' => '𝔾', + 'gopf' => '𝕘', + 'grave' => '`', + 'GreaterEqual' => '≥', + 'GreaterEqualLess' => '⋛', + 'GreaterFullEqual' => '≧', + 'GreaterGreater' => '⪢', + 'GreaterLess' => '≷', + 'GreaterSlantEqual' => '⩾', + 'GreaterTilde' => '≳', + 'Gscr' => '𝒢', + 'gscr' => 'ℊ', + 'gsim' => '≳', + 'gsime' => '⪎', + 'gsiml' => '⪐', + 'GT' => '>', + 'G' => '>', + 'Gt' => '≫', + 'gt' => '>', + 'g' => '>', + 'gtcc' => '⪧', + 'gtcir' => '⩺', + 'gtdot' => '⋗', + 'gtlPar' => '⦕', + 'gtquest' => '⩼', + 'gtrapprox' => '⪆', + 'gtrarr' => '⥸', + 'gtrdot' => '⋗', + 'gtreqless' => '⋛', + 'gtreqqless' => '⪌', + 'gtrless' => '≷', + 'gtrsim' => '≳', + 'gvertneqq' => '≩︀', + 'gvnE' => '≩︀', + 'Hacek' => 'ˇ', + 'hairsp' => ' ', + 'half' => '½', + 'hamilt' => 'ℋ', + 'HARDcy' => 'Ъ', + 'hardcy' => 'ъ', + 'hArr' => '⇔', + 'harr' => '↔', + 'harrcir' => '⥈', + 'harrw' => '↭', + 'Hat' => '^', + 'hbar' => 'ℏ', + 'Hcirc' => 'Ĥ', + 'hcirc' => 'ĥ', + 'hearts' => '♥', + 'heartsuit' => '♥', + 'hellip' => '…', + 'hercon' => '⊹', + 'Hfr' => 'ℌ', + 'hfr' => '𝔥', + 'HilbertSpace' => 'ℋ', + 'hksearow' => '⤥', + 'hkswarow' => '⤦', + 'hoarr' => '⇿', + 'homtht' => '∻', + 'hookleftarrow' => '↩', + 'hookrightarrow' => '↪', + 'Hopf' => 'ℍ', + 'hopf' => '𝕙', + 'horbar' => '―', + 'HorizontalLine' => '─', + 'Hscr' => 'ℋ', + 'hscr' => '𝒽', + 'hslash' => 'ℏ', + 'Hstrok' => 'Ħ', + 'hstrok' => 'ħ', + 'HumpDownHump' => '≎', + 'HumpEqual' => '≏', + 'hybull' => '⁃', + 'hyphen' => '‐', + 'Iacute' => 'Í', + 'Iacut' => 'Í', + 'iacute' => 'í', + 'iacut' => 'í', + 'ic' => '⁣', + 'Icirc' => 'Î', + 'Icir' => 'Î', + 'icirc' => 'î', + 'icir' => 'î', + 'Icy' => 'И', + 'icy' => 'и', + 'Idot' => 'İ', + 'IEcy' => 'Е', + 'iecy' => 'е', + 'iexcl' => '¡', + 'iexc' => '¡', + 'iff' => '⇔', + 'Ifr' => 'ℑ', + 'ifr' => '𝔦', + 'Igrave' => 'Ì', + 'Igrav' => 'Ì', + 'igrave' => 'ì', + 'igrav' => 'ì', + 'ii' => 'ⅈ', + 'iiiint' => '⨌', + 'iiint' => '∭', + 'iinfin' => '⧜', + 'iiota' => '℩', + 'IJlig' => 'IJ', + 'ijlig' => 'ij', + 'Im' => 'ℑ', + 'Imacr' => 'Ī', + 'imacr' => 'ī', + 'image' => 'ℑ', + 'ImaginaryI' => 'ⅈ', + 'imagline' => 'ℐ', + 'imagpart' => 'ℑ', + 'imath' => 'ı', + 'imof' => '⊷', + 'imped' => 'Ƶ', + 'Implies' => '⇒', + 'in' => '∈', + 'incare' => '℅', + 'infin' => '∞', + 'infintie' => '⧝', + 'inodot' => 'ı', + 'Int' => '∬', + 'int' => '∫', + 'intcal' => '⊺', + 'integers' => 'ℤ', + 'Integral' => '∫', + 'intercal' => '⊺', + 'Intersection' => '⋂', + 'intlarhk' => '⨗', + 'intprod' => '⨼', + 'InvisibleComma' => '⁣', + 'InvisibleTimes' => '⁢', + 'IOcy' => 'Ё', + 'iocy' => 'ё', + 'Iogon' => 'Į', + 'iogon' => 'į', + 'Iopf' => '𝕀', + 'iopf' => '𝕚', + 'Iota' => 'Ι', + 'iota' => 'ι', + 'iprod' => '⨼', + 'iquest' => '¿', + 'iques' => '¿', + 'Iscr' => 'ℐ', + 'iscr' => '𝒾', + 'isin' => '∈', + 'isindot' => '⋵', + 'isinE' => '⋹', + 'isins' => '⋴', + 'isinsv' => '⋳', + 'isinv' => '∈', + 'it' => '⁢', + 'Itilde' => 'Ĩ', + 'itilde' => 'ĩ', + 'Iukcy' => 'І', + 'iukcy' => 'і', + 'Iuml' => 'Ï', + 'Ium' => 'Ï', + 'iuml' => 'ï', + 'ium' => 'ï', + 'Jcirc' => 'Ĵ', + 'jcirc' => 'ĵ', + 'Jcy' => 'Й', + 'jcy' => 'й', + 'Jfr' => '𝔍', + 'jfr' => '𝔧', + 'jmath' => 'ȷ', + 'Jopf' => '𝕁', + 'jopf' => '𝕛', + 'Jscr' => '𝒥', + 'jscr' => '𝒿', + 'Jsercy' => 'Ј', + 'jsercy' => 'ј', + 'Jukcy' => 'Є', + 'jukcy' => 'є', + 'Kappa' => 'Κ', + 'kappa' => 'κ', + 'kappav' => 'ϰ', + 'Kcedil' => 'Ķ', + 'kcedil' => 'ķ', + 'Kcy' => 'К', + 'kcy' => 'к', + 'Kfr' => '𝔎', + 'kfr' => '𝔨', + 'kgreen' => 'ĸ', + 'KHcy' => 'Х', + 'khcy' => 'х', + 'KJcy' => 'Ќ', + 'kjcy' => 'ќ', + 'Kopf' => '𝕂', + 'kopf' => '𝕜', + 'Kscr' => '𝒦', + 'kscr' => '𝓀', + 'lAarr' => '⇚', + 'Lacute' => 'Ĺ', + 'lacute' => 'ĺ', + 'laemptyv' => '⦴', + 'lagran' => 'ℒ', + 'Lambda' => 'Λ', + 'lambda' => 'λ', + 'Lang' => '⟪', + 'lang' => '⟨', + 'langd' => '⦑', + 'langle' => '⟨', + 'lap' => '⪅', + 'Laplacetrf' => 'ℒ', + 'laquo' => '«', + 'laqu' => '«', + 'Larr' => '↞', + 'lArr' => '⇐', + 'larr' => '←', + 'larrb' => '⇤', + 'larrbfs' => '⤟', + 'larrfs' => '⤝', + 'larrhk' => '↩', + 'larrlp' => '↫', + 'larrpl' => '⤹', + 'larrsim' => '⥳', + 'larrtl' => '↢', + 'lat' => '⪫', + 'lAtail' => '⤛', + 'latail' => '⤙', + 'late' => '⪭', + 'lates' => '⪭︀', + 'lBarr' => '⤎', + 'lbarr' => '⤌', + 'lbbrk' => '❲', + 'lbrace' => '{', + 'lbrack' => '[', + 'lbrke' => '⦋', + 'lbrksld' => '⦏', + 'lbrkslu' => '⦍', + 'Lcaron' => 'Ľ', + 'lcaron' => 'ľ', + 'Lcedil' => 'Ļ', + 'lcedil' => 'ļ', + 'lceil' => '⌈', + 'lcub' => '{', + 'Lcy' => 'Л', + 'lcy' => 'л', + 'ldca' => '⤶', + 'ldquo' => '“', + 'ldquor' => '„', + 'ldrdhar' => '⥧', + 'ldrushar' => '⥋', + 'ldsh' => '↲', + 'lE' => '≦', + 'le' => '≤', + 'LeftAngleBracket' => '⟨', + 'LeftArrow' => '←', + 'Leftarrow' => '⇐', + 'leftarrow' => '←', + 'LeftArrowBar' => '⇤', + 'LeftArrowRightArrow' => '⇆', + 'leftarrowtail' => '↢', + 'LeftCeiling' => '⌈', + 'LeftDoubleBracket' => '⟦', + 'LeftDownTeeVector' => '⥡', + 'LeftDownVector' => '⇃', + 'LeftDownVectorBar' => '⥙', + 'LeftFloor' => '⌊', + 'leftharpoondown' => '↽', + 'leftharpoonup' => '↼', + 'leftleftarrows' => '⇇', + 'LeftRightArrow' => '↔', + 'Leftrightarrow' => '⇔', + 'leftrightarrow' => '↔', + 'leftrightarrows' => '⇆', + 'leftrightharpoons' => '⇋', + 'leftrightsquigarrow' => '↭', + 'LeftRightVector' => '⥎', + 'LeftTee' => '⊣', + 'LeftTeeArrow' => '↤', + 'LeftTeeVector' => '⥚', + 'leftthreetimes' => '⋋', + 'LeftTriangle' => '⊲', + 'LeftTriangleBar' => '⧏', + 'LeftTriangleEqual' => '⊴', + 'LeftUpDownVector' => '⥑', + 'LeftUpTeeVector' => '⥠', + 'LeftUpVector' => '↿', + 'LeftUpVectorBar' => '⥘', + 'LeftVector' => '↼', + 'LeftVectorBar' => '⥒', + 'lEg' => '⪋', + 'leg' => '⋚', + 'leq' => '≤', + 'leqq' => '≦', + 'leqslant' => '⩽', + 'les' => '⩽', + 'lescc' => '⪨', + 'lesdot' => '⩿', + 'lesdoto' => '⪁', + 'lesdotor' => '⪃', + 'lesg' => '⋚︀', + 'lesges' => '⪓', + 'lessapprox' => '⪅', + 'lessdot' => '⋖', + 'lesseqgtr' => '⋚', + 'lesseqqgtr' => '⪋', + 'LessEqualGreater' => '⋚', + 'LessFullEqual' => '≦', + 'LessGreater' => '≶', + 'lessgtr' => '≶', + 'LessLess' => '⪡', + 'lesssim' => '≲', + 'LessSlantEqual' => '⩽', + 'LessTilde' => '≲', + 'lfisht' => '⥼', + 'lfloor' => '⌊', + 'Lfr' => '𝔏', + 'lfr' => '𝔩', + 'lg' => '≶', + 'lgE' => '⪑', + 'lHar' => '⥢', + 'lhard' => '↽', + 'lharu' => '↼', + 'lharul' => '⥪', + 'lhblk' => '▄', + 'LJcy' => 'Љ', + 'ljcy' => 'љ', + 'Ll' => '⋘', + 'll' => '≪', + 'llarr' => '⇇', + 'llcorner' => '⌞', + 'Lleftarrow' => '⇚', + 'llhard' => '⥫', + 'lltri' => '◺', + 'Lmidot' => 'Ŀ', + 'lmidot' => 'ŀ', + 'lmoust' => '⎰', + 'lmoustache' => '⎰', + 'lnap' => '⪉', + 'lnapprox' => '⪉', + 'lnE' => '≨', + 'lne' => '⪇', + 'lneq' => '⪇', + 'lneqq' => '≨', + 'lnsim' => '⋦', + 'loang' => '⟬', + 'loarr' => '⇽', + 'lobrk' => '⟦', + 'LongLeftArrow' => '⟵', + 'Longleftarrow' => '⟸', + 'longleftarrow' => '⟵', + 'LongLeftRightArrow' => '⟷', + 'Longleftrightarrow' => '⟺', + 'longleftrightarrow' => '⟷', + 'longmapsto' => '⟼', + 'LongRightArrow' => '⟶', + 'Longrightarrow' => '⟹', + 'longrightarrow' => '⟶', + 'looparrowleft' => '↫', + 'looparrowright' => '↬', + 'lopar' => '⦅', + 'Lopf' => '𝕃', + 'lopf' => '𝕝', + 'loplus' => '⨭', + 'lotimes' => '⨴', + 'lowast' => '∗', + 'lowbar' => '_', + 'LowerLeftArrow' => '↙', + 'LowerRightArrow' => '↘', + 'loz' => '◊', + 'lozenge' => '◊', + 'lozf' => '⧫', + 'lpar' => '(', + 'lparlt' => '⦓', + 'lrarr' => '⇆', + 'lrcorner' => '⌟', + 'lrhar' => '⇋', + 'lrhard' => '⥭', + 'lrm' => '‎', + 'lrtri' => '⊿', + 'lsaquo' => '‹', + 'Lscr' => 'ℒ', + 'lscr' => '𝓁', + 'Lsh' => '↰', + 'lsh' => '↰', + 'lsim' => '≲', + 'lsime' => '⪍', + 'lsimg' => '⪏', + 'lsqb' => '[', + 'lsquo' => '‘', + 'lsquor' => '‚', + 'Lstrok' => 'Ł', + 'lstrok' => 'ł', + 'LT' => '<', + 'L' => '<', + 'Lt' => '≪', + 'lt' => '<', + 'l' => '<', + 'ltcc' => '⪦', + 'ltcir' => '⩹', + 'ltdot' => '⋖', + 'lthree' => '⋋', + 'ltimes' => '⋉', + 'ltlarr' => '⥶', + 'ltquest' => '⩻', + 'ltri' => '◃', + 'ltrie' => '⊴', + 'ltrif' => '◂', + 'ltrPar' => '⦖', + 'lurdshar' => '⥊', + 'luruhar' => '⥦', + 'lvertneqq' => '≨︀', + 'lvnE' => '≨︀', + 'macr' => '¯', + 'mac' => '¯', + 'male' => '♂', + 'malt' => '✠', + 'maltese' => '✠', + 'Map' => '⤅', + 'map' => '↦', + 'mapsto' => '↦', + 'mapstodown' => '↧', + 'mapstoleft' => '↤', + 'mapstoup' => '↥', + 'marker' => '▮', + 'mcomma' => '⨩', + 'Mcy' => 'М', + 'mcy' => 'м', + 'mdash' => '—', + 'mDDot' => '∺', + 'measuredangle' => '∡', + 'MediumSpace' => ' ', + 'Mellintrf' => 'ℳ', + 'Mfr' => '𝔐', + 'mfr' => '𝔪', + 'mho' => '℧', + 'micro' => 'µ', + 'micr' => 'µ', + 'mid' => '∣', + 'midast' => '*', + 'midcir' => '⫰', + 'middot' => '·', + 'middo' => '·', + 'minus' => '−', + 'minusb' => '⊟', + 'minusd' => '∸', + 'minusdu' => '⨪', + 'MinusPlus' => '∓', + 'mlcp' => '⫛', + 'mldr' => '…', + 'mnplus' => '∓', + 'models' => '⊧', + 'Mopf' => '𝕄', + 'mopf' => '𝕞', + 'mp' => '∓', + 'Mscr' => 'ℳ', + 'mscr' => '𝓂', + 'mstpos' => '∾', + 'Mu' => 'Μ', + 'mu' => 'μ', + 'multimap' => '⊸', + 'mumap' => '⊸', + 'nabla' => '∇', + 'Nacute' => 'Ń', + 'nacute' => 'ń', + 'nang' => '∠⃒', + 'nap' => '≉', + 'napE' => '⩰̸', + 'napid' => '≋̸', + 'napos' => 'ʼn', + 'napprox' => '≉', + 'natur' => '♮', + 'natural' => '♮', + 'naturals' => 'ℕ', + 'nbsp' => ' ', + 'nbs' => ' ', + 'nbump' => '≎̸', + 'nbumpe' => '≏̸', + 'ncap' => '⩃', + 'Ncaron' => 'Ň', + 'ncaron' => 'ň', + 'Ncedil' => 'Ņ', + 'ncedil' => 'ņ', + 'ncong' => '≇', + 'ncongdot' => '⩭̸', + 'ncup' => '⩂', + 'Ncy' => 'Н', + 'ncy' => 'н', + 'ndash' => '–', + 'ne' => '≠', + 'nearhk' => '⤤', + 'neArr' => '⇗', + 'nearr' => '↗', + 'nearrow' => '↗', + 'nedot' => '≐̸', + 'NegativeMediumSpace' => '​', + 'NegativeThickSpace' => '​', + 'NegativeThinSpace' => '​', + 'NegativeVeryThinSpace' => '​', + 'nequiv' => '≢', + 'nesear' => '⤨', + 'nesim' => '≂̸', + 'NestedGreaterGreater' => '≫', + 'NestedLessLess' => '≪', + 'NewLine' => ' +', + 'nexist' => '∄', + 'nexists' => '∄', + 'Nfr' => '𝔑', + 'nfr' => '𝔫', + 'ngE' => '≧̸', + 'nge' => '≱', + 'ngeq' => '≱', + 'ngeqq' => '≧̸', + 'ngeqslant' => '⩾̸', + 'nges' => '⩾̸', + 'nGg' => '⋙̸', + 'ngsim' => '≵', + 'nGt' => '≫⃒', + 'ngt' => '≯', + 'ngtr' => '≯', + 'nGtv' => '≫̸', + 'nhArr' => '⇎', + 'nharr' => '↮', + 'nhpar' => '⫲', + 'ni' => '∋', + 'nis' => '⋼', + 'nisd' => '⋺', + 'niv' => '∋', + 'NJcy' => 'Њ', + 'njcy' => 'њ', + 'nlArr' => '⇍', + 'nlarr' => '↚', + 'nldr' => '‥', + 'nlE' => '≦̸', + 'nle' => '≰', + 'nLeftarrow' => '⇍', + 'nleftarrow' => '↚', + 'nLeftrightarrow' => '⇎', + 'nleftrightarrow' => '↮', + 'nleq' => '≰', + 'nleqq' => '≦̸', + 'nleqslant' => '⩽̸', + 'nles' => '⩽̸', + 'nless' => '≮', + 'nLl' => '⋘̸', + 'nlsim' => '≴', + 'nLt' => '≪⃒', + 'nlt' => '≮', + 'nltri' => '⋪', + 'nltrie' => '⋬', + 'nLtv' => '≪̸', + 'nmid' => '∤', + 'NoBreak' => '⁠', + 'NonBreakingSpace' => ' ', + 'Nopf' => 'ℕ', + 'nopf' => '𝕟', + 'Not' => '⫬', + 'not' => '¬', + 'no' => '¬', + 'NotCongruent' => '≢', + 'NotCupCap' => '≭', + 'NotDoubleVerticalBar' => '∦', + 'NotElement' => '∉', + 'NotEqual' => '≠', + 'NotEqualTilde' => '≂̸', + 'NotExists' => '∄', + 'NotGreater' => '≯', + 'NotGreaterEqual' => '≱', + 'NotGreaterFullEqual' => '≧̸', + 'NotGreaterGreater' => '≫̸', + 'NotGreaterLess' => '≹', + 'NotGreaterSlantEqual' => '⩾̸', + 'NotGreaterTilde' => '≵', + 'NotHumpDownHump' => '≎̸', + 'NotHumpEqual' => '≏̸', + 'notin' => '∉', + 'notindot' => '⋵̸', + 'notinE' => '⋹̸', + 'notinva' => '∉', + 'notinvb' => '⋷', + 'notinvc' => '⋶', + 'NotLeftTriangle' => '⋪', + 'NotLeftTriangleBar' => '⧏̸', + 'NotLeftTriangleEqual' => '⋬', + 'NotLess' => '≮', + 'NotLessEqual' => '≰', + 'NotLessGreater' => '≸', + 'NotLessLess' => '≪̸', + 'NotLessSlantEqual' => '⩽̸', + 'NotLessTilde' => '≴', + 'NotNestedGreaterGreater' => '⪢̸', + 'NotNestedLessLess' => '⪡̸', + 'notni' => '∌', + 'notniva' => '∌', + 'notnivb' => '⋾', + 'notnivc' => '⋽', + 'NotPrecedes' => '⊀', + 'NotPrecedesEqual' => '⪯̸', + 'NotPrecedesSlantEqual' => '⋠', + 'NotReverseElement' => '∌', + 'NotRightTriangle' => '⋫', + 'NotRightTriangleBar' => '⧐̸', + 'NotRightTriangleEqual' => '⋭', + 'NotSquareSubset' => '⊏̸', + 'NotSquareSubsetEqual' => '⋢', + 'NotSquareSuperset' => '⊐̸', + 'NotSquareSupersetEqual' => '⋣', + 'NotSubset' => '⊂⃒', + 'NotSubsetEqual' => '⊈', + 'NotSucceeds' => '⊁', + 'NotSucceedsEqual' => '⪰̸', + 'NotSucceedsSlantEqual' => '⋡', + 'NotSucceedsTilde' => '≿̸', + 'NotSuperset' => '⊃⃒', + 'NotSupersetEqual' => '⊉', + 'NotTilde' => '≁', + 'NotTildeEqual' => '≄', + 'NotTildeFullEqual' => '≇', + 'NotTildeTilde' => '≉', + 'NotVerticalBar' => '∤', + 'npar' => '∦', + 'nparallel' => '∦', + 'nparsl' => '⫽⃥', + 'npart' => '∂̸', + 'npolint' => '⨔', + 'npr' => '⊀', + 'nprcue' => '⋠', + 'npre' => '⪯̸', + 'nprec' => '⊀', + 'npreceq' => '⪯̸', + 'nrArr' => '⇏', + 'nrarr' => '↛', + 'nrarrc' => '⤳̸', + 'nrarrw' => '↝̸', + 'nRightarrow' => '⇏', + 'nrightarrow' => '↛', + 'nrtri' => '⋫', + 'nrtrie' => '⋭', + 'nsc' => '⊁', + 'nsccue' => '⋡', + 'nsce' => '⪰̸', + 'Nscr' => '𝒩', + 'nscr' => '𝓃', + 'nshortmid' => '∤', + 'nshortparallel' => '∦', + 'nsim' => '≁', + 'nsime' => '≄', + 'nsimeq' => '≄', + 'nsmid' => '∤', + 'nspar' => '∦', + 'nsqsube' => '⋢', + 'nsqsupe' => '⋣', + 'nsub' => '⊄', + 'nsubE' => '⫅̸', + 'nsube' => '⊈', + 'nsubset' => '⊂⃒', + 'nsubseteq' => '⊈', + 'nsubseteqq' => '⫅̸', + 'nsucc' => '⊁', + 'nsucceq' => '⪰̸', + 'nsup' => '⊅', + 'nsupE' => '⫆̸', + 'nsupe' => '⊉', + 'nsupset' => '⊃⃒', + 'nsupseteq' => '⊉', + 'nsupseteqq' => '⫆̸', + 'ntgl' => '≹', + 'Ntilde' => 'Ñ', + 'Ntild' => 'Ñ', + 'ntilde' => 'ñ', + 'ntild' => 'ñ', + 'ntlg' => '≸', + 'ntriangleleft' => '⋪', + 'ntrianglelefteq' => '⋬', + 'ntriangleright' => '⋫', + 'ntrianglerighteq' => '⋭', + 'Nu' => 'Ν', + 'nu' => 'ν', + 'num' => '#', + 'numero' => '№', + 'numsp' => ' ', + 'nvap' => '≍⃒', + 'nVDash' => '⊯', + 'nVdash' => '⊮', + 'nvDash' => '⊭', + 'nvdash' => '⊬', + 'nvge' => '≥⃒', + 'nvgt' => '>⃒', + 'nvHarr' => '⤄', + 'nvinfin' => '⧞', + 'nvlArr' => '⤂', + 'nvle' => '≤⃒', + 'nvlt' => '<⃒', + 'nvltrie' => '⊴⃒', + 'nvrArr' => '⤃', + 'nvrtrie' => '⊵⃒', + 'nvsim' => '∼⃒', + 'nwarhk' => '⤣', + 'nwArr' => '⇖', + 'nwarr' => '↖', + 'nwarrow' => '↖', + 'nwnear' => '⤧', + 'Oacute' => 'Ó', + 'Oacut' => 'Ó', + 'oacute' => 'ó', + 'oacut' => 'ó', + 'oast' => '⊛', + 'ocir' => 'ô', + 'Ocirc' => 'Ô', + 'Ocir' => 'Ô', + 'ocirc' => 'ô', + 'Ocy' => 'О', + 'ocy' => 'о', + 'odash' => '⊝', + 'Odblac' => 'Ő', + 'odblac' => 'ő', + 'odiv' => '⨸', + 'odot' => '⊙', + 'odsold' => '⦼', + 'OElig' => 'Œ', + 'oelig' => 'œ', + 'ofcir' => '⦿', + 'Ofr' => '𝔒', + 'ofr' => '𝔬', + 'ogon' => '˛', + 'Ograve' => 'Ò', + 'Ograv' => 'Ò', + 'ograve' => 'ò', + 'ograv' => 'ò', + 'ogt' => '⧁', + 'ohbar' => '⦵', + 'ohm' => 'Ω', + 'oint' => '∮', + 'olarr' => '↺', + 'olcir' => '⦾', + 'olcross' => '⦻', + 'oline' => '‾', + 'olt' => '⧀', + 'Omacr' => 'Ō', + 'omacr' => 'ō', + 'Omega' => 'Ω', + 'omega' => 'ω', + 'Omicron' => 'Ο', + 'omicron' => 'ο', + 'omid' => '⦶', + 'ominus' => '⊖', + 'Oopf' => '𝕆', + 'oopf' => '𝕠', + 'opar' => '⦷', + 'OpenCurlyDoubleQuote' => '“', + 'OpenCurlyQuote' => '‘', + 'operp' => '⦹', + 'oplus' => '⊕', + 'Or' => '⩔', + 'or' => '∨', + 'orarr' => '↻', + 'ord' => 'º', + 'order' => 'ℴ', + 'orderof' => 'ℴ', + 'ordf' => 'ª', + 'ordm' => 'º', + 'origof' => '⊶', + 'oror' => '⩖', + 'orslope' => '⩗', + 'orv' => '⩛', + 'oS' => 'Ⓢ', + 'Oscr' => '𝒪', + 'oscr' => 'ℴ', + 'Oslash' => 'Ø', + 'Oslas' => 'Ø', + 'oslash' => 'ø', + 'oslas' => 'ø', + 'osol' => '⊘', + 'Otilde' => 'Õ', + 'Otild' => 'Õ', + 'otilde' => 'õ', + 'otild' => 'õ', + 'Otimes' => '⨷', + 'otimes' => '⊗', + 'otimesas' => '⨶', + 'Ouml' => 'Ö', + 'Oum' => 'Ö', + 'ouml' => 'ö', + 'oum' => 'ö', + 'ovbar' => '⌽', + 'OverBar' => '‾', + 'OverBrace' => '⏞', + 'OverBracket' => '⎴', + 'OverParenthesis' => '⏜', + 'par' => '¶', + 'para' => '¶', + 'parallel' => '∥', + 'parsim' => '⫳', + 'parsl' => '⫽', + 'part' => '∂', + 'PartialD' => '∂', + 'Pcy' => 'П', + 'pcy' => 'п', + 'percnt' => '%', + 'period' => '.', + 'permil' => '‰', + 'perp' => '⊥', + 'pertenk' => '‱', + 'Pfr' => '𝔓', + 'pfr' => '𝔭', + 'Phi' => 'Φ', + 'phi' => 'φ', + 'phiv' => 'ϕ', + 'phmmat' => 'ℳ', + 'phone' => '☎', + 'Pi' => 'Π', + 'pi' => 'π', + 'pitchfork' => '⋔', + 'piv' => 'ϖ', + 'planck' => 'ℏ', + 'planckh' => 'ℎ', + 'plankv' => 'ℏ', + 'plus' => '+', + 'plusacir' => '⨣', + 'plusb' => '⊞', + 'pluscir' => '⨢', + 'plusdo' => '∔', + 'plusdu' => '⨥', + 'pluse' => '⩲', + 'PlusMinus' => '±', + 'plusmn' => '±', + 'plusm' => '±', + 'plussim' => '⨦', + 'plustwo' => '⨧', + 'pm' => '±', + 'Poincareplane' => 'ℌ', + 'pointint' => '⨕', + 'Popf' => 'ℙ', + 'popf' => '𝕡', + 'pound' => '£', + 'poun' => '£', + 'Pr' => '⪻', + 'pr' => '≺', + 'prap' => '⪷', + 'prcue' => '≼', + 'prE' => '⪳', + 'pre' => '⪯', + 'prec' => '≺', + 'precapprox' => '⪷', + 'preccurlyeq' => '≼', + 'Precedes' => '≺', + 'PrecedesEqual' => '⪯', + 'PrecedesSlantEqual' => '≼', + 'PrecedesTilde' => '≾', + 'preceq' => '⪯', + 'precnapprox' => '⪹', + 'precneqq' => '⪵', + 'precnsim' => '⋨', + 'precsim' => '≾', + 'Prime' => '″', + 'prime' => '′', + 'primes' => 'ℙ', + 'prnap' => '⪹', + 'prnE' => '⪵', + 'prnsim' => '⋨', + 'prod' => '∏', + 'Product' => '∏', + 'profalar' => '⌮', + 'profline' => '⌒', + 'profsurf' => '⌓', + 'prop' => '∝', + 'Proportion' => '∷', + 'Proportional' => '∝', + 'propto' => '∝', + 'prsim' => '≾', + 'prurel' => '⊰', + 'Pscr' => '𝒫', + 'pscr' => '𝓅', + 'Psi' => 'Ψ', + 'psi' => 'ψ', + 'puncsp' => ' ', + 'Qfr' => '𝔔', + 'qfr' => '𝔮', + 'qint' => '⨌', + 'Qopf' => 'ℚ', + 'qopf' => '𝕢', + 'qprime' => '⁗', + 'Qscr' => '𝒬', + 'qscr' => '𝓆', + 'quaternions' => 'ℍ', + 'quatint' => '⨖', + 'quest' => '?', + 'questeq' => '≟', + 'QUOT' => '"', + 'QUO' => '"', + 'quot' => '"', + 'quo' => '"', + 'rAarr' => '⇛', + 'race' => '∽̱', + 'Racute' => 'Ŕ', + 'racute' => 'ŕ', + 'radic' => '√', + 'raemptyv' => '⦳', + 'Rang' => '⟫', + 'rang' => '⟩', + 'rangd' => '⦒', + 'range' => '⦥', + 'rangle' => '⟩', + 'raquo' => '»', + 'raqu' => '»', + 'Rarr' => '↠', + 'rArr' => '⇒', + 'rarr' => '→', + 'rarrap' => '⥵', + 'rarrb' => '⇥', + 'rarrbfs' => '⤠', + 'rarrc' => '⤳', + 'rarrfs' => '⤞', + 'rarrhk' => '↪', + 'rarrlp' => '↬', + 'rarrpl' => '⥅', + 'rarrsim' => '⥴', + 'Rarrtl' => '⤖', + 'rarrtl' => '↣', + 'rarrw' => '↝', + 'rAtail' => '⤜', + 'ratail' => '⤚', + 'ratio' => '∶', + 'rationals' => 'ℚ', + 'RBarr' => '⤐', + 'rBarr' => '⤏', + 'rbarr' => '⤍', + 'rbbrk' => '❳', + 'rbrace' => '}', + 'rbrack' => ']', + 'rbrke' => '⦌', + 'rbrksld' => '⦎', + 'rbrkslu' => '⦐', + 'Rcaron' => 'Ř', + 'rcaron' => 'ř', + 'Rcedil' => 'Ŗ', + 'rcedil' => 'ŗ', + 'rceil' => '⌉', + 'rcub' => '}', + 'Rcy' => 'Р', + 'rcy' => 'р', + 'rdca' => '⤷', + 'rdldhar' => '⥩', + 'rdquo' => '”', + 'rdquor' => '”', + 'rdsh' => '↳', + 'Re' => 'ℜ', + 'real' => 'ℜ', + 'realine' => 'ℛ', + 'realpart' => 'ℜ', + 'reals' => 'ℝ', + 'rect' => '▭', + 'REG' => '®', + 'RE' => '®', + 'reg' => '®', + 're' => '®', + 'ReverseElement' => '∋', + 'ReverseEquilibrium' => '⇋', + 'ReverseUpEquilibrium' => '⥯', + 'rfisht' => '⥽', + 'rfloor' => '⌋', + 'Rfr' => 'ℜ', + 'rfr' => '𝔯', + 'rHar' => '⥤', + 'rhard' => '⇁', + 'rharu' => '⇀', + 'rharul' => '⥬', + 'Rho' => 'Ρ', + 'rho' => 'ρ', + 'rhov' => 'ϱ', + 'RightAngleBracket' => '⟩', + 'RightArrow' => '→', + 'Rightarrow' => '⇒', + 'rightarrow' => '→', + 'RightArrowBar' => '⇥', + 'RightArrowLeftArrow' => '⇄', + 'rightarrowtail' => '↣', + 'RightCeiling' => '⌉', + 'RightDoubleBracket' => '⟧', + 'RightDownTeeVector' => '⥝', + 'RightDownVector' => '⇂', + 'RightDownVectorBar' => '⥕', + 'RightFloor' => '⌋', + 'rightharpoondown' => '⇁', + 'rightharpoonup' => '⇀', + 'rightleftarrows' => '⇄', + 'rightleftharpoons' => '⇌', + 'rightrightarrows' => '⇉', + 'rightsquigarrow' => '↝', + 'RightTee' => '⊢', + 'RightTeeArrow' => '↦', + 'RightTeeVector' => '⥛', + 'rightthreetimes' => '⋌', + 'RightTriangle' => '⊳', + 'RightTriangleBar' => '⧐', + 'RightTriangleEqual' => '⊵', + 'RightUpDownVector' => '⥏', + 'RightUpTeeVector' => '⥜', + 'RightUpVector' => '↾', + 'RightUpVectorBar' => '⥔', + 'RightVector' => '⇀', + 'RightVectorBar' => '⥓', + 'ring' => '˚', + 'risingdotseq' => '≓', + 'rlarr' => '⇄', + 'rlhar' => '⇌', + 'rlm' => '‏', + 'rmoust' => '⎱', + 'rmoustache' => '⎱', + 'rnmid' => '⫮', + 'roang' => '⟭', + 'roarr' => '⇾', + 'robrk' => '⟧', + 'ropar' => '⦆', + 'Ropf' => 'ℝ', + 'ropf' => '𝕣', + 'roplus' => '⨮', + 'rotimes' => '⨵', + 'RoundImplies' => '⥰', + 'rpar' => ')', + 'rpargt' => '⦔', + 'rppolint' => '⨒', + 'rrarr' => '⇉', + 'Rrightarrow' => '⇛', + 'rsaquo' => '›', + 'Rscr' => 'ℛ', + 'rscr' => '𝓇', + 'Rsh' => '↱', + 'rsh' => '↱', + 'rsqb' => ']', + 'rsquo' => '’', + 'rsquor' => '’', + 'rthree' => '⋌', + 'rtimes' => '⋊', + 'rtri' => '▹', + 'rtrie' => '⊵', + 'rtrif' => '▸', + 'rtriltri' => '⧎', + 'RuleDelayed' => '⧴', + 'ruluhar' => '⥨', + 'rx' => '℞', + 'Sacute' => 'Ś', + 'sacute' => 'ś', + 'sbquo' => '‚', + 'Sc' => '⪼', + 'sc' => '≻', + 'scap' => '⪸', + 'Scaron' => 'Š', + 'scaron' => 'š', + 'sccue' => '≽', + 'scE' => '⪴', + 'sce' => '⪰', + 'Scedil' => 'Ş', + 'scedil' => 'ş', + 'Scirc' => 'Ŝ', + 'scirc' => 'ŝ', + 'scnap' => '⪺', + 'scnE' => '⪶', + 'scnsim' => '⋩', + 'scpolint' => '⨓', + 'scsim' => '≿', + 'Scy' => 'С', + 'scy' => 'с', + 'sdot' => '⋅', + 'sdotb' => '⊡', + 'sdote' => '⩦', + 'searhk' => '⤥', + 'seArr' => '⇘', + 'searr' => '↘', + 'searrow' => '↘', + 'sect' => '§', + 'sec' => '§', + 'semi' => ';', + 'seswar' => '⤩', + 'setminus' => '∖', + 'setmn' => '∖', + 'sext' => '✶', + 'Sfr' => '𝔖', + 'sfr' => '𝔰', + 'sfrown' => '⌢', + 'sharp' => '♯', + 'SHCHcy' => 'Щ', + 'shchcy' => 'щ', + 'SHcy' => 'Ш', + 'shcy' => 'ш', + 'ShortDownArrow' => '↓', + 'ShortLeftArrow' => '←', + 'shortmid' => '∣', + 'shortparallel' => '∥', + 'ShortRightArrow' => '→', + 'ShortUpArrow' => '↑', + 'shy' => '­', + 'sh' => '­', + 'Sigma' => 'Σ', + 'sigma' => 'σ', + 'sigmaf' => 'ς', + 'sigmav' => 'ς', + 'sim' => '∼', + 'simdot' => '⩪', + 'sime' => '≃', + 'simeq' => '≃', + 'simg' => '⪞', + 'simgE' => '⪠', + 'siml' => '⪝', + 'simlE' => '⪟', + 'simne' => '≆', + 'simplus' => '⨤', + 'simrarr' => '⥲', + 'slarr' => '←', + 'SmallCircle' => '∘', + 'smallsetminus' => '∖', + 'smashp' => '⨳', + 'smeparsl' => '⧤', + 'smid' => '∣', + 'smile' => '⌣', + 'smt' => '⪪', + 'smte' => '⪬', + 'smtes' => '⪬︀', + 'SOFTcy' => 'Ь', + 'softcy' => 'ь', + 'sol' => '/', + 'solb' => '⧄', + 'solbar' => '⌿', + 'Sopf' => '𝕊', + 'sopf' => '𝕤', + 'spades' => '♠', + 'spadesuit' => '♠', + 'spar' => '∥', + 'sqcap' => '⊓', + 'sqcaps' => '⊓︀', + 'sqcup' => '⊔', + 'sqcups' => '⊔︀', + 'Sqrt' => '√', + 'sqsub' => '⊏', + 'sqsube' => '⊑', + 'sqsubset' => '⊏', + 'sqsubseteq' => '⊑', + 'sqsup' => '⊐', + 'sqsupe' => '⊒', + 'sqsupset' => '⊐', + 'sqsupseteq' => '⊒', + 'squ' => '□', + 'Square' => '□', + 'square' => '□', + 'SquareIntersection' => '⊓', + 'SquareSubset' => '⊏', + 'SquareSubsetEqual' => '⊑', + 'SquareSuperset' => '⊐', + 'SquareSupersetEqual' => '⊒', + 'SquareUnion' => '⊔', + 'squarf' => '▪', + 'squf' => '▪', + 'srarr' => '→', + 'Sscr' => '𝒮', + 'sscr' => '𝓈', + 'ssetmn' => '∖', + 'ssmile' => '⌣', + 'sstarf' => '⋆', + 'Star' => '⋆', + 'star' => '☆', + 'starf' => '★', + 'straightepsilon' => 'ϵ', + 'straightphi' => 'ϕ', + 'strns' => '¯', + 'Sub' => '⋐', + 'sub' => '⊂', + 'subdot' => '⪽', + 'subE' => '⫅', + 'sube' => '⊆', + 'subedot' => '⫃', + 'submult' => '⫁', + 'subnE' => '⫋', + 'subne' => '⊊', + 'subplus' => '⪿', + 'subrarr' => '⥹', + 'Subset' => '⋐', + 'subset' => '⊂', + 'subseteq' => '⊆', + 'subseteqq' => '⫅', + 'SubsetEqual' => '⊆', + 'subsetneq' => '⊊', + 'subsetneqq' => '⫋', + 'subsim' => '⫇', + 'subsub' => '⫕', + 'subsup' => '⫓', + 'succ' => '≻', + 'succapprox' => '⪸', + 'succcurlyeq' => '≽', + 'Succeeds' => '≻', + 'SucceedsEqual' => '⪰', + 'SucceedsSlantEqual' => '≽', + 'SucceedsTilde' => '≿', + 'succeq' => '⪰', + 'succnapprox' => '⪺', + 'succneqq' => '⪶', + 'succnsim' => '⋩', + 'succsim' => '≿', + 'SuchThat' => '∋', + 'Sum' => '∑', + 'sum' => '∑', + 'sung' => '♪', + 'Sup' => '⋑', + 'sup' => '³', + 'sup1' => '¹', + 'sup2' => '²', + 'sup3' => '³', + 'supdot' => '⪾', + 'supdsub' => '⫘', + 'supE' => '⫆', + 'supe' => '⊇', + 'supedot' => '⫄', + 'Superset' => '⊃', + 'SupersetEqual' => '⊇', + 'suphsol' => '⟉', + 'suphsub' => '⫗', + 'suplarr' => '⥻', + 'supmult' => '⫂', + 'supnE' => '⫌', + 'supne' => '⊋', + 'supplus' => '⫀', + 'Supset' => '⋑', + 'supset' => '⊃', + 'supseteq' => '⊇', + 'supseteqq' => '⫆', + 'supsetneq' => '⊋', + 'supsetneqq' => '⫌', + 'supsim' => '⫈', + 'supsub' => '⫔', + 'supsup' => '⫖', + 'swarhk' => '⤦', + 'swArr' => '⇙', + 'swarr' => '↙', + 'swarrow' => '↙', + 'swnwar' => '⤪', + 'szlig' => 'ß', + 'szli' => 'ß', + 'Tab' => ' ', + 'target' => '⌖', + 'Tau' => 'Τ', + 'tau' => 'τ', + 'tbrk' => '⎴', + 'Tcaron' => 'Ť', + 'tcaron' => 'ť', + 'Tcedil' => 'Ţ', + 'tcedil' => 'ţ', + 'Tcy' => 'Т', + 'tcy' => 'т', + 'tdot' => '⃛', + 'telrec' => '⌕', + 'Tfr' => '𝔗', + 'tfr' => '𝔱', + 'there4' => '∴', + 'Therefore' => '∴', + 'therefore' => '∴', + 'Theta' => 'Θ', + 'theta' => 'θ', + 'thetasym' => 'ϑ', + 'thetav' => 'ϑ', + 'thickapprox' => '≈', + 'thicksim' => '∼', + 'ThickSpace' => '  ', + 'thinsp' => ' ', + 'ThinSpace' => ' ', + 'thkap' => '≈', + 'thksim' => '∼', + 'THORN' => 'Þ', + 'THOR' => 'Þ', + 'thorn' => 'þ', + 'thor' => 'þ', + 'Tilde' => '∼', + 'tilde' => '˜', + 'TildeEqual' => '≃', + 'TildeFullEqual' => '≅', + 'TildeTilde' => '≈', + 'times' => '×', + 'time' => '×', + 'timesb' => '⊠', + 'timesbar' => '⨱', + 'timesd' => '⨰', + 'tint' => '∭', + 'toea' => '⤨', + 'top' => '⊤', + 'topbot' => '⌶', + 'topcir' => '⫱', + 'Topf' => '𝕋', + 'topf' => '𝕥', + 'topfork' => '⫚', + 'tosa' => '⤩', + 'tprime' => '‴', + 'TRADE' => '™', + 'trade' => '™', + 'triangle' => '▵', + 'triangledown' => '▿', + 'triangleleft' => '◃', + 'trianglelefteq' => '⊴', + 'triangleq' => '≜', + 'triangleright' => '▹', + 'trianglerighteq' => '⊵', + 'tridot' => '◬', + 'trie' => '≜', + 'triminus' => '⨺', + 'TripleDot' => '⃛', + 'triplus' => '⨹', + 'trisb' => '⧍', + 'tritime' => '⨻', + 'trpezium' => '⏢', + 'Tscr' => '𝒯', + 'tscr' => '𝓉', + 'TScy' => 'Ц', + 'tscy' => 'ц', + 'TSHcy' => 'Ћ', + 'tshcy' => 'ћ', + 'Tstrok' => 'Ŧ', + 'tstrok' => 'ŧ', + 'twixt' => '≬', + 'twoheadleftarrow' => '↞', + 'twoheadrightarrow' => '↠', + 'Uacute' => 'Ú', + 'Uacut' => 'Ú', + 'uacute' => 'ú', + 'uacut' => 'ú', + 'Uarr' => '↟', + 'uArr' => '⇑', + 'uarr' => '↑', + 'Uarrocir' => '⥉', + 'Ubrcy' => 'Ў', + 'ubrcy' => 'ў', + 'Ubreve' => 'Ŭ', + 'ubreve' => 'ŭ', + 'Ucirc' => 'Û', + 'Ucir' => 'Û', + 'ucirc' => 'û', + 'ucir' => 'û', + 'Ucy' => 'У', + 'ucy' => 'у', + 'udarr' => '⇅', + 'Udblac' => 'Ű', + 'udblac' => 'ű', + 'udhar' => '⥮', + 'ufisht' => '⥾', + 'Ufr' => '𝔘', + 'ufr' => '𝔲', + 'Ugrave' => 'Ù', + 'Ugrav' => 'Ù', + 'ugrave' => 'ù', + 'ugrav' => 'ù', + 'uHar' => '⥣', + 'uharl' => '↿', + 'uharr' => '↾', + 'uhblk' => '▀', + 'ulcorn' => '⌜', + 'ulcorner' => '⌜', + 'ulcrop' => '⌏', + 'ultri' => '◸', + 'Umacr' => 'Ū', + 'umacr' => 'ū', + 'uml' => '¨', + 'um' => '¨', + 'UnderBar' => '_', + 'UnderBrace' => '⏟', + 'UnderBracket' => '⎵', + 'UnderParenthesis' => '⏝', + 'Union' => '⋃', + 'UnionPlus' => '⊎', + 'Uogon' => 'Ų', + 'uogon' => 'ų', + 'Uopf' => '𝕌', + 'uopf' => '𝕦', + 'UpArrow' => '↑', + 'Uparrow' => '⇑', + 'uparrow' => '↑', + 'UpArrowBar' => '⤒', + 'UpArrowDownArrow' => '⇅', + 'UpDownArrow' => '↕', + 'Updownarrow' => '⇕', + 'updownarrow' => '↕', + 'UpEquilibrium' => '⥮', + 'upharpoonleft' => '↿', + 'upharpoonright' => '↾', + 'uplus' => '⊎', + 'UpperLeftArrow' => '↖', + 'UpperRightArrow' => '↗', + 'Upsi' => 'ϒ', + 'upsi' => 'υ', + 'upsih' => 'ϒ', + 'Upsilon' => 'Υ', + 'upsilon' => 'υ', + 'UpTee' => '⊥', + 'UpTeeArrow' => '↥', + 'upuparrows' => '⇈', + 'urcorn' => '⌝', + 'urcorner' => '⌝', + 'urcrop' => '⌎', + 'Uring' => 'Ů', + 'uring' => 'ů', + 'urtri' => '◹', + 'Uscr' => '𝒰', + 'uscr' => '𝓊', + 'utdot' => '⋰', + 'Utilde' => 'Ũ', + 'utilde' => 'ũ', + 'utri' => '▵', + 'utrif' => '▴', + 'uuarr' => '⇈', + 'Uuml' => 'Ü', + 'Uum' => 'Ü', + 'uuml' => 'ü', + 'uum' => 'ü', + 'uwangle' => '⦧', + 'vangrt' => '⦜', + 'varepsilon' => 'ϵ', + 'varkappa' => 'ϰ', + 'varnothing' => '∅', + 'varphi' => 'ϕ', + 'varpi' => 'ϖ', + 'varpropto' => '∝', + 'vArr' => '⇕', + 'varr' => '↕', + 'varrho' => 'ϱ', + 'varsigma' => 'ς', + 'varsubsetneq' => '⊊︀', + 'varsubsetneqq' => '⫋︀', + 'varsupsetneq' => '⊋︀', + 'varsupsetneqq' => '⫌︀', + 'vartheta' => 'ϑ', + 'vartriangleleft' => '⊲', + 'vartriangleright' => '⊳', + 'Vbar' => '⫫', + 'vBar' => '⫨', + 'vBarv' => '⫩', + 'Vcy' => 'В', + 'vcy' => 'в', + 'VDash' => '⊫', + 'Vdash' => '⊩', + 'vDash' => '⊨', + 'vdash' => '⊢', + 'Vdashl' => '⫦', + 'Vee' => '⋁', + 'vee' => '∨', + 'veebar' => '⊻', + 'veeeq' => '≚', + 'vellip' => '⋮', + 'Verbar' => '‖', + 'verbar' => '|', + 'Vert' => '‖', + 'vert' => '|', + 'VerticalBar' => '∣', + 'VerticalLine' => '|', + 'VerticalSeparator' => '❘', + 'VerticalTilde' => '≀', + 'VeryThinSpace' => ' ', + 'Vfr' => '𝔙', + 'vfr' => '𝔳', + 'vltri' => '⊲', + 'vnsub' => '⊂⃒', + 'vnsup' => '⊃⃒', + 'Vopf' => '𝕍', + 'vopf' => '𝕧', + 'vprop' => '∝', + 'vrtri' => '⊳', + 'Vscr' => '𝒱', + 'vscr' => '𝓋', + 'vsubnE' => '⫋︀', + 'vsubne' => '⊊︀', + 'vsupnE' => '⫌︀', + 'vsupne' => '⊋︀', + 'Vvdash' => '⊪', + 'vzigzag' => '⦚', + 'Wcirc' => 'Ŵ', + 'wcirc' => 'ŵ', + 'wedbar' => '⩟', + 'Wedge' => '⋀', + 'wedge' => '∧', + 'wedgeq' => '≙', + 'weierp' => '℘', + 'Wfr' => '𝔚', + 'wfr' => '𝔴', + 'Wopf' => '𝕎', + 'wopf' => '𝕨', + 'wp' => '℘', + 'wr' => '≀', + 'wreath' => '≀', + 'Wscr' => '𝒲', + 'wscr' => '𝓌', + 'xcap' => '⋂', + 'xcirc' => '◯', + 'xcup' => '⋃', + 'xdtri' => '▽', + 'Xfr' => '𝔛', + 'xfr' => '𝔵', + 'xhArr' => '⟺', + 'xharr' => '⟷', + 'Xi' => 'Ξ', + 'xi' => 'ξ', + 'xlArr' => '⟸', + 'xlarr' => '⟵', + 'xmap' => '⟼', + 'xnis' => '⋻', + 'xodot' => '⨀', + 'Xopf' => '𝕏', + 'xopf' => '𝕩', + 'xoplus' => '⨁', + 'xotime' => '⨂', + 'xrArr' => '⟹', + 'xrarr' => '⟶', + 'Xscr' => '𝒳', + 'xscr' => '𝓍', + 'xsqcup' => '⨆', + 'xuplus' => '⨄', + 'xutri' => '△', + 'xvee' => '⋁', + 'xwedge' => '⋀', + 'Yacute' => 'Ý', + 'Yacut' => 'Ý', + 'yacute' => 'ý', + 'yacut' => 'ý', + 'YAcy' => 'Я', + 'yacy' => 'я', + 'Ycirc' => 'Ŷ', + 'ycirc' => 'ŷ', + 'Ycy' => 'Ы', + 'ycy' => 'ы', + 'yen' => '¥', + 'ye' => '¥', + 'Yfr' => '𝔜', + 'yfr' => '𝔶', + 'YIcy' => 'Ї', + 'yicy' => 'ї', + 'Yopf' => '𝕐', + 'yopf' => '𝕪', + 'Yscr' => '𝒴', + 'yscr' => '𝓎', + 'YUcy' => 'Ю', + 'yucy' => 'ю', + 'Yuml' => 'Ÿ', + 'yuml' => 'ÿ', + 'yum' => 'ÿ', + 'Zacute' => 'Ź', + 'zacute' => 'ź', + 'Zcaron' => 'Ž', + 'zcaron' => 'ž', + 'Zcy' => 'З', + 'zcy' => 'з', + 'Zdot' => 'Ż', + 'zdot' => 'ż', + 'zeetrf' => 'ℨ', + 'ZeroWidthSpace' => '​', + 'Zeta' => 'Ζ', + 'zeta' => 'ζ', + 'Zfr' => 'ℨ', + 'zfr' => '𝔷', + 'ZHcy' => 'Ж', + 'zhcy' => 'ж', + 'zigrarr' => '⇝', + 'Zopf' => 'ℤ', + 'zopf' => '𝕫', + 'Zscr' => '𝒵', + 'zscr' => '𝓏', + 'zwj' => '‍', + 'zwnj' => '‌', + ); +} diff --git a/3rdparty/masterminds/html5/src/HTML5/Exception.php b/3rdparty/masterminds/html5/src/HTML5/Exception.php new file mode 100644 index 00000000..64e97e6f --- /dev/null +++ b/3rdparty/masterminds/html5/src/HTML5/Exception.php @@ -0,0 +1,10 @@ + self::NAMESPACE_HTML, + 'svg' => self::NAMESPACE_SVG, + 'math' => self::NAMESPACE_MATHML, + ); + + /** + * Holds the always available namespaces (which does not require the XMLNS declaration). + * + * @var array + */ + protected $implicitNamespaces = array( + 'xml' => self::NAMESPACE_XML, + 'xmlns' => self::NAMESPACE_XMLNS, + 'xlink' => self::NAMESPACE_XLINK, + ); + + /** + * Holds a stack of currently active namespaces. + * + * @var array + */ + protected $nsStack = array(); + + /** + * Holds the number of namespaces declared by a node. + * + * @var array + */ + protected $pushes = array(); + + /** + * Defined in 8.2.5. + */ + const IM_INITIAL = 0; + + const IM_BEFORE_HTML = 1; + + const IM_BEFORE_HEAD = 2; + + const IM_IN_HEAD = 3; + + const IM_IN_HEAD_NOSCRIPT = 4; + + const IM_AFTER_HEAD = 5; + + const IM_IN_BODY = 6; + + const IM_TEXT = 7; + + const IM_IN_TABLE = 8; + + const IM_IN_TABLE_TEXT = 9; + + const IM_IN_CAPTION = 10; + + const IM_IN_COLUMN_GROUP = 11; + + const IM_IN_TABLE_BODY = 12; + + const IM_IN_ROW = 13; + + const IM_IN_CELL = 14; + + const IM_IN_SELECT = 15; + + const IM_IN_SELECT_IN_TABLE = 16; + + const IM_AFTER_BODY = 17; + + const IM_IN_FRAMESET = 18; + + const IM_AFTER_FRAMESET = 19; + + const IM_AFTER_AFTER_BODY = 20; + + const IM_AFTER_AFTER_FRAMESET = 21; + + const IM_IN_SVG = 22; + + const IM_IN_MATHML = 23; + + protected $options = array(); + + protected $stack = array(); + + protected $current; // Pointer in the tag hierarchy. + protected $rules; + protected $doc; + + protected $frag; + + protected $processor; + + protected $insertMode = 0; + + /** + * Track if we are in an element that allows only inline child nodes. + * + * @var string|null + */ + protected $onlyInline; + + /** + * Quirks mode is enabled by default. + * Any document that is missing the DT will be considered to be in quirks mode. + */ + protected $quirks = true; + + protected $errors = array(); + + public function __construct($isFragment = false, array $options = array()) + { + $this->options = $options; + + if (isset($options[self::OPT_TARGET_DOC])) { + $this->doc = $options[self::OPT_TARGET_DOC]; + } else { + $impl = new \DOMImplementation(); + // XXX: + // Create the doctype. For now, we are always creating HTML5 + // documents, and attempting to up-convert any older DTDs to HTML5. + $dt = $impl->createDocumentType('html'); + // $this->doc = \DOMImplementation::createDocument(NULL, 'html', $dt); + $this->doc = $impl->createDocument(null, '', $dt); + $this->doc->encoding = !empty($options['encoding']) ? $options['encoding'] : 'UTF-8'; + } + + $this->errors = array(); + + $this->current = $this->doc; // ->documentElement; + + // Create a rules engine for tags. + $this->rules = new TreeBuildingRules(); + + $implicitNS = array(); + if (isset($this->options[self::OPT_IMPLICIT_NS])) { + $implicitNS = $this->options[self::OPT_IMPLICIT_NS]; + } elseif (isset($this->options['implicitNamespaces'])) { + $implicitNS = $this->options['implicitNamespaces']; + } + + // Fill $nsStack with the defalut HTML5 namespaces, plus the "implicitNamespaces" array taken form $options + array_unshift($this->nsStack, $implicitNS + array('' => self::NAMESPACE_HTML) + $this->implicitNamespaces); + + if ($isFragment) { + $this->insertMode = static::IM_IN_BODY; + $this->frag = $this->doc->createDocumentFragment(); + $this->current = $this->frag; + } + } + + /** + * Get the document. + */ + public function document() + { + return $this->doc; + } + + /** + * Get the DOM fragment for the body. + * + * This returns a DOMNodeList because a fragment may have zero or more + * DOMNodes at its root. + * + * @see http://www.w3.org/TR/2012/CR-html5-20121217/syntax.html#concept-frag-parse-context + * + * @return \DOMDocumentFragment + */ + public function fragment() + { + return $this->frag; + } + + /** + * Provide an instruction processor. + * + * This is used for handling Processor Instructions as they are + * inserted. If omitted, PI's are inserted directly into the DOM tree. + * + * @param InstructionProcessor $proc + */ + public function setInstructionProcessor(InstructionProcessor $proc) + { + $this->processor = $proc; + } + + public function doctype($name, $idType = 0, $id = null, $quirks = false) + { + // This is used solely for setting quirks mode. Currently we don't + // try to preserve the inbound DT. We convert it to HTML5. + $this->quirks = $quirks; + + if ($this->insertMode > static::IM_INITIAL) { + $this->parseError('Illegal placement of DOCTYPE tag. Ignoring: ' . $name); + + return; + } + + $this->insertMode = static::IM_BEFORE_HTML; + } + + /** + * Process the start tag. + * + * @todo - XMLNS namespace handling (we need to parse, even if it's not valid) + * - XLink, MathML and SVG namespace handling + * - Omission rules: 8.1.2.4 Optional tags + * + * @param string $name + * @param array $attributes + * @param bool $selfClosing + * + * @return int + */ + public function startTag($name, $attributes = array(), $selfClosing = false) + { + $lname = $this->normalizeTagName($name); + + // Make sure we have an html element. + if (!$this->doc->documentElement && 'html' !== $name && !$this->frag) { + $this->startTag('html'); + } + + // Set quirks mode if we're at IM_INITIAL with no doctype. + if ($this->insertMode === static::IM_INITIAL) { + $this->quirks = true; + $this->parseError('No DOCTYPE specified.'); + } + + // SPECIAL TAG HANDLING: + // Spec says do this, and "don't ask." + // find the spec where this is defined... looks problematic + if ('image' === $name && !($this->insertMode === static::IM_IN_SVG || $this->insertMode === static::IM_IN_MATHML)) { + $name = 'img'; + } + + // Autoclose p tags where appropriate. + if ($this->insertMode >= static::IM_IN_BODY && Elements::isA($name, Elements::AUTOCLOSE_P)) { + $this->autoclose('p'); + } + + // Set insert mode: + switch ($name) { + case 'html': + $this->insertMode = static::IM_BEFORE_HEAD; + break; + case 'head': + if ($this->insertMode > static::IM_BEFORE_HEAD) { + $this->parseError('Unexpected head tag outside of head context.'); + } else { + $this->insertMode = static::IM_IN_HEAD; + } + break; + case 'body': + $this->insertMode = static::IM_IN_BODY; + break; + case 'svg': + $this->insertMode = static::IM_IN_SVG; + break; + case 'math': + $this->insertMode = static::IM_IN_MATHML; + break; + case 'noscript': + if ($this->insertMode === static::IM_IN_HEAD) { + $this->insertMode = static::IM_IN_HEAD_NOSCRIPT; + } + break; + } + + // Special case handling for SVG. + if ($this->insertMode === static::IM_IN_SVG) { + $lname = Elements::normalizeSvgElement($lname); + } + + $pushes = 0; + // when we found a tag thats appears inside $nsRoots, we have to switch the defalut namespace + if (isset($this->nsRoots[$lname]) && $this->nsStack[0][''] !== $this->nsRoots[$lname]) { + array_unshift($this->nsStack, array( + '' => $this->nsRoots[$lname], + ) + $this->nsStack[0]); + ++$pushes; + } + $needsWorkaround = false; + if (isset($this->options['xmlNamespaces']) && $this->options['xmlNamespaces']) { + // when xmlNamespaces is true a and we found a 'xmlns' or 'xmlns:*' attribute, we should add a new item to the $nsStack + foreach ($attributes as $aName => $aVal) { + if ('xmlns' === $aName) { + $needsWorkaround = $aVal; + array_unshift($this->nsStack, array( + '' => $aVal, + ) + $this->nsStack[0]); + ++$pushes; + } elseif ('xmlns' === (($pos = strpos($aName, ':')) ? substr($aName, 0, $pos) : '')) { + array_unshift($this->nsStack, array( + substr($aName, $pos + 1) => $aVal, + ) + $this->nsStack[0]); + ++$pushes; + } + } + } + + if ($this->onlyInline && Elements::isA($lname, Elements::BLOCK_TAG)) { + $this->autoclose($this->onlyInline); + $this->onlyInline = null; + } + + // some elements as table related tags might have optional end tags that force us to auto close multiple tags + // https://www.w3.org/TR/html401/struct/tables.html + if ($this->current instanceof \DOMElement && isset(Elements::$optionalEndElementsParentsToClose[$lname])) { + foreach (Elements::$optionalEndElementsParentsToClose[$lname] as $parentElName) { + if ($this->current instanceof \DOMElement && $this->current->tagName === $parentElName) { + $this->autoclose($parentElName); + } + } + } + + try { + $prefix = ($pos = strpos($lname, ':')) ? substr($lname, 0, $pos) : ''; + + if (false !== $needsWorkaround) { + $xml = "<$lname xmlns=\"$needsWorkaround\" " . (strlen($prefix) && isset($this->nsStack[0][$prefix]) ? ("xmlns:$prefix=\"" . $this->nsStack[0][$prefix] . '"') : '') . '/>'; + + $frag = new \DOMDocument('1.0', 'UTF-8'); + $frag->loadXML($xml); + + $ele = $this->doc->importNode($frag->documentElement, true); + } else { + if (!isset($this->nsStack[0][$prefix]) || ('' === $prefix && isset($this->options[self::OPT_DISABLE_HTML_NS]) && $this->options[self::OPT_DISABLE_HTML_NS])) { + $ele = $this->doc->createElement($lname); + } else { + $ele = $this->doc->createElementNS($this->nsStack[0][$prefix], $lname); + } + } + } catch (\DOMException $e) { + $this->parseError("Illegal tag name: <$lname>. Replaced with ."); + $ele = $this->doc->createElement('invalid'); + } + + if (Elements::isA($lname, Elements::BLOCK_ONLY_INLINE)) { + $this->onlyInline = $lname; + } + + // When we add some namespacess, we have to track them. Later, when "endElement" is invoked, we have to remove them. + // When we are on a void tag, we do not need to care about namesapce nesting. + if ($pushes > 0 && !Elements::isA($name, Elements::VOID_TAG)) { + // PHP tends to free the memory used by DOM, + // to avoid spl_object_hash collisions whe have to avoid garbage collection of $ele storing it into $pushes + // see https://bugs.php.net/bug.php?id=67459 + $this->pushes[spl_object_hash($ele)] = array($pushes, $ele); + } + + foreach ($attributes as $aName => $aVal) { + // xmlns attributes can't be set + if ('xmlns' === $aName) { + continue; + } + + if ($this->insertMode === static::IM_IN_SVG) { + $aName = Elements::normalizeSvgAttribute($aName); + } elseif ($this->insertMode === static::IM_IN_MATHML) { + $aName = Elements::normalizeMathMlAttribute($aName); + } + + $aVal = (string) $aVal; + + try { + $prefix = ($pos = strpos($aName, ':')) ? substr($aName, 0, $pos) : false; + + if ('xmlns' === $prefix) { + $ele->setAttributeNS(self::NAMESPACE_XMLNS, $aName, $aVal); + } elseif (false !== $prefix && isset($this->nsStack[0][$prefix])) { + $ele->setAttributeNS($this->nsStack[0][$prefix], $aName, $aVal); + } else { + $ele->setAttribute($aName, $aVal); + } + } catch (\DOMException $e) { + $this->parseError("Illegal attribute name for tag $name. Ignoring: $aName"); + continue; + } + + // This is necessary on a non-DTD schema, like HTML5. + if ('id' === $aName) { + $ele->setIdAttribute('id', true); + } + } + + if ($this->frag !== $this->current && $this->rules->hasRules($name)) { + // Some elements have special processing rules. Handle those separately. + $this->current = $this->rules->evaluate($ele, $this->current); + } else { + // Otherwise, it's a standard element. + $this->current->appendChild($ele); + + if (!Elements::isA($name, Elements::VOID_TAG)) { + $this->current = $ele; + } + + // Self-closing tags should only be respected on foreign elements + // (and are implied on void elements) + // See: https://www.w3.org/TR/html5/syntax.html#start-tags + if (Elements::isHtml5Element($name)) { + $selfClosing = false; + } + } + + // This is sort of a last-ditch attempt to correct for cases where no head/body + // elements are provided. + if ($this->insertMode <= static::IM_BEFORE_HEAD && 'head' !== $name && 'html' !== $name) { + $this->insertMode = static::IM_IN_BODY; + } + + // When we are on a void tag, we do not need to care about namesapce nesting, + // but we have to remove the namespaces pushed to $nsStack. + if ($pushes > 0 && Elements::isA($name, Elements::VOID_TAG)) { + // remove the namespaced definded by current node + for ($i = 0; $i < $pushes; ++$i) { + array_shift($this->nsStack); + } + } + + if ($selfClosing) { + $this->endTag($name); + } + + // Return the element mask, which the tokenizer can then use to set + // various processing rules. + return Elements::element($name); + } + + public function endTag($name) + { + $lname = $this->normalizeTagName($name); + + // Special case within 12.2.6.4.7: An end tag whose tag name is "br" should be treated as an opening tag + if ('br' === $name) { + $this->parseError('Closing tag encountered for void element br.'); + + $this->startTag('br'); + } + // Ignore closing tags for other unary elements. + elseif (Elements::isA($name, Elements::VOID_TAG)) { + return; + } + + if ($this->insertMode <= static::IM_BEFORE_HTML) { + // 8.2.5.4.2 + if (in_array($name, array( + 'html', + 'br', + 'head', + 'title', + ))) { + $this->startTag('html'); + $this->endTag($name); + $this->insertMode = static::IM_BEFORE_HEAD; + + return; + } + + // Ignore the tag. + $this->parseError('Illegal closing tag at global scope.'); + + return; + } + + // Special case handling for SVG. + if ($this->insertMode === static::IM_IN_SVG) { + $lname = Elements::normalizeSvgElement($lname); + } + + $cid = spl_object_hash($this->current); + + // XXX: HTML has no parent. What do we do, though, + // if this element appears in the wrong place? + if ('html' === $lname) { + return; + } + + // remove the namespaced definded by current node + if (isset($this->pushes[$cid])) { + for ($i = 0; $i < $this->pushes[$cid][0]; ++$i) { + array_shift($this->nsStack); + } + unset($this->pushes[$cid]); + } + + if (!$this->autoclose($lname)) { + $this->parseError('Could not find closing tag for ' . $lname); + } + + switch ($lname) { + case 'head': + $this->insertMode = static::IM_AFTER_HEAD; + break; + case 'body': + $this->insertMode = static::IM_AFTER_BODY; + break; + case 'svg': + case 'mathml': + $this->insertMode = static::IM_IN_BODY; + break; + } + } + + public function comment($cdata) + { + // TODO: Need to handle case where comment appears outside of the HTML tag. + $node = $this->doc->createComment($cdata); + $this->current->appendChild($node); + } + + public function text($data) + { + // XXX: Hmmm.... should we really be this strict? + if ($this->insertMode < static::IM_IN_HEAD) { + // Per '8.2.5.4.3 The "before head" insertion mode' the characters + // " \t\n\r\f" should be ignored but no mention of a parse error. This is + // practical as most documents contain these characters. Other text is not + // expected here so recording a parse error is necessary. + $dataTmp = trim($data, " \t\n\r\f"); + if (!empty($dataTmp)) { + // fprintf(STDOUT, "Unexpected insert mode: %d", $this->insertMode); + $this->parseError('Unexpected text. Ignoring: ' . $dataTmp); + } + + return; + } + // fprintf(STDOUT, "Appending text %s.", $data); + $node = $this->doc->createTextNode($data); + $this->current->appendChild($node); + } + + public function eof() + { + // If the $current isn't the $root, do we need to do anything? + } + + public function parseError($msg, $line = 0, $col = 0) + { + $this->errors[] = sprintf('Line %d, Col %d: %s', $line, $col, $msg); + } + + public function getErrors() + { + return $this->errors; + } + + public function cdata($data) + { + $node = $this->doc->createCDATASection($data); + $this->current->appendChild($node); + } + + public function processingInstruction($name, $data = null) + { + // XXX: Ignore initial XML declaration, per the spec. + if ($this->insertMode === static::IM_INITIAL && 'xml' === strtolower($name)) { + return; + } + + // Important: The processor may modify the current DOM tree however it sees fit. + if ($this->processor instanceof InstructionProcessor) { + $res = $this->processor->process($this->current, $name, $data); + if (!empty($res)) { + $this->current = $res; + } + + return; + } + + // Otherwise, this is just a dumb PI element. + $node = $this->doc->createProcessingInstruction($name, $data); + + $this->current->appendChild($node); + } + + // ========================================================================== + // UTILITIES + // ========================================================================== + + /** + * Apply normalization rules to a tag name. + * See sections 2.9 and 8.1.2. + * + * @param string $tagName + * + * @return string The normalized tag name. + */ + protected function normalizeTagName($tagName) + { + /* + * Section 2.9 suggests that we should not do this. if (strpos($name, ':') !== false) { // We know from the grammar that there must be at least one other // char besides :, since : is not a legal tag start. $parts = explode(':', $name); return array_pop($parts); } + */ + return $tagName; + } + + protected function quirksTreeResolver($name) + { + throw new \Exception('Not implemented.'); + } + + /** + * Automatically climb the tree and close the closest node with the matching $tag. + * + * @param string $tagName + * + * @return bool + */ + protected function autoclose($tagName) + { + $working = $this->current; + do { + if (XML_ELEMENT_NODE !== $working->nodeType) { + return false; + } + if ($working->tagName === $tagName) { + $this->current = $working->parentNode; + + return true; + } + } while ($working = $working->parentNode); + + return false; + } + + /** + * Checks if the given tagname is an ancestor of the present candidate. + * + * If $this->current or anything above $this->current matches the given tag + * name, this returns true. + * + * @param string $tagName + * + * @return bool + */ + protected function isAncestor($tagName) + { + $candidate = $this->current; + while (XML_ELEMENT_NODE === $candidate->nodeType) { + if ($candidate->tagName === $tagName) { + return true; + } + $candidate = $candidate->parentNode; + } + + return false; + } + + /** + * Returns true if the immediate parent element is of the given tagname. + * + * @param string $tagName + * + * @return bool + */ + protected function isParent($tagName) + { + return $this->current->tagName === $tagName; + } +} diff --git a/3rdparty/masterminds/html5/src/HTML5/Parser/EventHandler.php b/3rdparty/masterminds/html5/src/HTML5/Parser/EventHandler.php new file mode 100644 index 00000000..9893a718 --- /dev/null +++ b/3rdparty/masterminds/html5/src/HTML5/Parser/EventHandler.php @@ -0,0 +1,114 @@ +). + * + * @return int one of the Tokenizer::TEXTMODE_* constants + */ + public function startTag($name, $attributes = array(), $selfClosing = false); + + /** + * An end-tag. + */ + public function endTag($name); + + /** + * A comment section (unparsed character data). + */ + public function comment($cdata); + + /** + * A unit of parsed character data. + * + * Entities in this text are *already decoded*. + */ + public function text($cdata); + + /** + * Indicates that the document has been entirely processed. + */ + public function eof(); + + /** + * Emitted when the parser encounters an error condition. + */ + public function parseError($msg, $line, $col); + + /** + * A CDATA section. + * + * @param string $data + * The unparsed character data + */ + public function cdata($data); + + /** + * This is a holdover from the XML spec. + * + * While user agents don't get PIs, server-side does. + * + * @param string $name The name of the processor (e.g. 'php'). + * @param string $data The unparsed data. + */ + public function processingInstruction($name, $data = null); +} diff --git a/3rdparty/masterminds/html5/src/HTML5/Parser/FileInputStream.php b/3rdparty/masterminds/html5/src/HTML5/Parser/FileInputStream.php new file mode 100644 index 00000000..b081ed96 --- /dev/null +++ b/3rdparty/masterminds/html5/src/HTML5/Parser/FileInputStream.php @@ -0,0 +1,33 @@ +errors = UTF8Utils::checkForIllegalCodepoints($data); + + $data = $this->replaceLinefeeds($data); + + $this->data = $data; + $this->char = 0; + $this->EOF = strlen($data); + } + + /** + * Check if upcomming chars match the given sequence. + * + * This will read the stream for the $sequence. If it's + * found, this will return true. If not, return false. + * Since this unconsumes any chars it reads, the caller + * will still need to read the next sequence, even if + * this returns true. + * + * Example: $this->scanner->sequenceMatches('') will + * see if the input stream is at the start of a + * '' string. + * + * @param string $sequence + * @param bool $caseSensitive + * + * @return bool + */ + public function sequenceMatches($sequence, $caseSensitive = true) + { + $portion = substr($this->data, $this->char, strlen($sequence)); + + return $caseSensitive ? $portion === $sequence : 0 === strcasecmp($portion, $sequence); + } + + /** + * Get the current position. + * + * @return int The current intiger byte position. + */ + public function position() + { + return $this->char; + } + + /** + * Take a peek at the next character in the data. + * + * @return string The next character. + */ + public function peek() + { + if (($this->char + 1) < $this->EOF) { + return $this->data[$this->char + 1]; + } + + return false; + } + + /** + * Get the next character. + * Note: This advances the pointer. + * + * @return string The next character. + */ + public function next() + { + ++$this->char; + + if ($this->char < $this->EOF) { + return $this->data[$this->char]; + } + + return false; + } + + /** + * Get the current character. + * Note, this does not advance the pointer. + * + * @return string The current character. + */ + public function current() + { + if ($this->char < $this->EOF) { + return $this->data[$this->char]; + } + + return false; + } + + /** + * Silently consume N chars. + * + * @param int $count + */ + public function consume($count = 1) + { + $this->char += $count; + } + + /** + * Unconsume some of the data. + * This moves the data pointer backwards. + * + * @param int $howMany The number of characters to move the pointer back. + */ + public function unconsume($howMany = 1) + { + if (($this->char - $howMany) >= 0) { + $this->char -= $howMany; + } + } + + /** + * Get the next group of that contains hex characters. + * Note, along with getting the characters the pointer in the data will be + * moved as well. + * + * @return string The next group that is hex characters. + */ + public function getHex() + { + return $this->doCharsWhile(static::CHARS_HEX); + } + + /** + * Get the next group of characters that are ASCII Alpha characters. + * Note, along with getting the characters the pointer in the data will be + * moved as well. + * + * @return string The next group of ASCII alpha characters. + */ + public function getAsciiAlpha() + { + return $this->doCharsWhile(static::CHARS_ALPHA); + } + + /** + * Get the next group of characters that are ASCII Alpha characters and numbers. + * Note, along with getting the characters the pointer in the data will be + * moved as well. + * + * @return string The next group of ASCII alpha characters and numbers. + */ + public function getAsciiAlphaNum() + { + return $this->doCharsWhile(static::CHARS_ALNUM); + } + + /** + * Get the next group of numbers. + * Note, along with getting the characters the pointer in the data will be + * moved as well. + * + * @return string The next group of numbers. + */ + public function getNumeric() + { + return $this->doCharsWhile('0123456789'); + } + + /** + * Consume whitespace. + * Whitespace in HTML5 is: formfeed, tab, newline, space. + * + * @return int The length of the matched whitespaces. + */ + public function whitespace() + { + if ($this->char >= $this->EOF) { + return false; + } + + $len = strspn($this->data, "\n\t\f ", $this->char); + + $this->char += $len; + + return $len; + } + + /** + * Returns the current line that is being consumed. + * + * @return int The current line number. + */ + public function currentLine() + { + if (empty($this->EOF) || 0 === $this->char) { + return 1; + } + + // Add one to $this->char because we want the number for the next + // byte to be processed. + return substr_count($this->data, "\n", 0, min($this->char, $this->EOF)) + 1; + } + + /** + * Read chars until something in the mask is encountered. + * + * @param string $mask + * + * @return mixed + */ + public function charsUntil($mask) + { + return $this->doCharsUntil($mask); + } + + /** + * Read chars as long as the mask matches. + * + * @param string $mask + * + * @return int + */ + public function charsWhile($mask) + { + return $this->doCharsWhile($mask); + } + + /** + * Returns the current column of the current line that the tokenizer is at. + * + * Newlines are column 0. The first char after a newline is column 1. + * + * @return int The column number. + */ + public function columnOffset() + { + // Short circuit for the first char. + if (0 === $this->char) { + return 0; + } + + // strrpos is weird, and the offset needs to be negative for what we + // want (i.e., the last \n before $this->char). This needs to not have + // one (to make it point to the next character, the one we want the + // position of) added to it because strrpos's behaviour includes the + // final offset byte. + $backwardFrom = $this->char - 1 - strlen($this->data); + $lastLine = strrpos($this->data, "\n", $backwardFrom); + + // However, for here we want the length up until the next byte to be + // processed, so add one to the current byte ($this->char). + if (false !== $lastLine) { + $findLengthOf = substr($this->data, $lastLine + 1, $this->char - 1 - $lastLine); + } else { + // After a newline. + $findLengthOf = substr($this->data, 0, $this->char); + } + + return UTF8Utils::countChars($findLengthOf); + } + + /** + * Get all characters until EOF. + * + * This consumes characters until the EOF. + * + * @return int The number of characters remaining. + */ + public function remainingChars() + { + if ($this->char < $this->EOF) { + $data = substr($this->data, $this->char); + $this->char = $this->EOF; + + return $data; + } + + return ''; // false; + } + + /** + * Replace linefeed characters according to the spec. + * + * @param $data + * + * @return string + */ + private function replaceLinefeeds($data) + { + /* + * U+000D CARRIAGE RETURN (CR) characters and U+000A LINE FEED (LF) characters are treated specially. + * Any CR characters that are followed by LF characters must be removed, and any CR characters not + * followed by LF characters must be converted to LF characters. Thus, newlines in HTML DOMs are + * represented by LF characters, and there are never any CR characters in the input to the tokenization + * stage. + */ + $crlfTable = array( + "\0" => "\xEF\xBF\xBD", + "\r\n" => "\n", + "\r" => "\n", + ); + + return strtr($data, $crlfTable); + } + + /** + * Read to a particular match (or until $max bytes are consumed). + * + * This operates on byte sequences, not characters. + * + * Matches as far as possible until we reach a certain set of bytes + * and returns the matched substring. + * + * @param string $bytes Bytes to match. + * @param int $max Maximum number of bytes to scan. + * + * @return mixed Index or false if no match is found. You should use strong + * equality when checking the result, since index could be 0. + */ + private function doCharsUntil($bytes, $max = null) + { + if ($this->char >= $this->EOF) { + return false; + } + + if (0 === $max || $max) { + $len = strcspn($this->data, $bytes, $this->char, $max); + } else { + $len = strcspn($this->data, $bytes, $this->char); + } + + $string = (string) substr($this->data, $this->char, $len); + $this->char += $len; + + return $string; + } + + /** + * Returns the string so long as $bytes matches. + * + * Matches as far as possible with a certain set of bytes + * and returns the matched substring. + * + * @param string $bytes A mask of bytes to match. If ANY byte in this mask matches the + * current char, the pointer advances and the char is part of the + * substring. + * @param int $max The max number of chars to read. + * + * @return string + */ + private function doCharsWhile($bytes, $max = null) + { + if ($this->char >= $this->EOF) { + return false; + } + + if (0 === $max || $max) { + $len = strspn($this->data, $bytes, $this->char, $max); + } else { + $len = strspn($this->data, $bytes, $this->char); + } + + $string = (string) substr($this->data, $this->char, $len); + $this->char += $len; + + return $string; + } +} diff --git a/3rdparty/masterminds/html5/src/HTML5/Parser/StringInputStream.php b/3rdparty/masterminds/html5/src/HTML5/Parser/StringInputStream.php new file mode 100644 index 00000000..75b08861 --- /dev/null +++ b/3rdparty/masterminds/html5/src/HTML5/Parser/StringInputStream.php @@ -0,0 +1,336 @@ + + +Permission is hereby granted, free of charge, to any person obtaining a +copy of this software and associated documentation files (the + "Software"), to deal in the Software without restriction, including +without limitation the rights to use, copy, modify, merge, publish, +distribute, sublicense, and/or sell copies of the Software, and to +permit persons to whom the Software is furnished to do so, subject to +the following conditions: + +The above copyright notice and this permission notice shall be included +in all copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS +OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF +MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. +IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY +CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, +TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE +SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. + +*/ + +// Some conventions: +// - /* */ indicates verbatim text from the HTML 5 specification +// MPB: Not sure which version of the spec. Moving from HTML5lib to +// HTML5-PHP, I have been using this version: +// http://www.w3.org/TR/2012/CR-html5-20121217/Overview.html#contents +// +// - // indicates regular comments + +/** + * @deprecated since 2.4, to remove in 3.0. Use a string in the scanner instead. + */ +class StringInputStream implements InputStream +{ + /** + * The string data we're parsing. + */ + private $data; + + /** + * The current integer byte position we are in $data. + */ + private $char; + + /** + * Length of $data; when $char === $data, we are at the end-of-file. + */ + private $EOF; + + /** + * Parse errors. + */ + public $errors = array(); + + /** + * Create a new InputStream wrapper. + * + * @param string $data Data to parse. + * @param string $encoding The encoding to use for the data. + * @param string $debug A fprintf format to use to echo the data on stdout. + */ + public function __construct($data, $encoding = 'UTF-8', $debug = '') + { + $data = UTF8Utils::convertToUTF8($data, $encoding); + if ($debug) { + fprintf(STDOUT, $debug, $data, strlen($data)); + } + + // There is good reason to question whether it makes sense to + // do this here, since most of these checks are done during + // parsing, and since this check doesn't actually *do* anything. + $this->errors = UTF8Utils::checkForIllegalCodepoints($data); + + $data = $this->replaceLinefeeds($data); + + $this->data = $data; + $this->char = 0; + $this->EOF = strlen($data); + } + + public function __toString() + { + return $this->data; + } + + /** + * Replace linefeed characters according to the spec. + */ + protected function replaceLinefeeds($data) + { + /* + * U+000D CARRIAGE RETURN (CR) characters and U+000A LINE FEED (LF) characters are treated specially. + * Any CR characters that are followed by LF characters must be removed, and any CR characters not + * followed by LF characters must be converted to LF characters. Thus, newlines in HTML DOMs are + * represented by LF characters, and there are never any CR characters in the input to the tokenization + * stage. + */ + $crlfTable = array( + "\0" => "\xEF\xBF\xBD", + "\r\n" => "\n", + "\r" => "\n", + ); + + return strtr($data, $crlfTable); + } + + /** + * Returns the current line that the tokenizer is at. + */ + public function currentLine() + { + if (empty($this->EOF) || 0 === $this->char) { + return 1; + } + // Add one to $this->char because we want the number for the next + // byte to be processed. + return substr_count($this->data, "\n", 0, min($this->char, $this->EOF)) + 1; + } + + /** + * @deprecated + */ + public function getCurrentLine() + { + return $this->currentLine(); + } + + /** + * Returns the current column of the current line that the tokenizer is at. + * Newlines are column 0. The first char after a newline is column 1. + * + * @return int The column number. + */ + public function columnOffset() + { + // Short circuit for the first char. + if (0 === $this->char) { + return 0; + } + // strrpos is weird, and the offset needs to be negative for what we + // want (i.e., the last \n before $this->char). This needs to not have + // one (to make it point to the next character, the one we want the + // position of) added to it because strrpos's behaviour includes the + // final offset byte. + $backwardFrom = $this->char - 1 - strlen($this->data); + $lastLine = strrpos($this->data, "\n", $backwardFrom); + + // However, for here we want the length up until the next byte to be + // processed, so add one to the current byte ($this->char). + if (false !== $lastLine) { + $findLengthOf = substr($this->data, $lastLine + 1, $this->char - 1 - $lastLine); + } else { + // After a newline. + $findLengthOf = substr($this->data, 0, $this->char); + } + + return UTF8Utils::countChars($findLengthOf); + } + + /** + * @deprecated + */ + public function getColumnOffset() + { + return $this->columnOffset(); + } + + /** + * Get the current character. + * + * @return string The current character. + */ + #[\ReturnTypeWillChange] + public function current() + { + return $this->data[$this->char]; + } + + /** + * Advance the pointer. + * This is part of the Iterator interface. + */ + #[\ReturnTypeWillChange] + public function next() + { + ++$this->char; + } + + /** + * Rewind to the start of the string. + */ + #[\ReturnTypeWillChange] + public function rewind() + { + $this->char = 0; + } + + /** + * Is the current pointer location valid. + * + * @return bool Whether the current pointer location is valid. + */ + #[\ReturnTypeWillChange] + public function valid() + { + return $this->char < $this->EOF; + } + + /** + * Get all characters until EOF. + * + * This reads to the end of the file, and sets the read marker at the + * end of the file. + * + * Note this performs bounds checking. + * + * @return string Returns the remaining text. If called when the InputStream is + * already exhausted, it returns an empty string. + */ + public function remainingChars() + { + if ($this->char < $this->EOF) { + $data = substr($this->data, $this->char); + $this->char = $this->EOF; + + return $data; + } + + return ''; // false; + } + + /** + * Read to a particular match (or until $max bytes are consumed). + * + * This operates on byte sequences, not characters. + * + * Matches as far as possible until we reach a certain set of bytes + * and returns the matched substring. + * + * @param string $bytes Bytes to match. + * @param int $max Maximum number of bytes to scan. + * + * @return mixed Index or false if no match is found. You should use strong + * equality when checking the result, since index could be 0. + */ + public function charsUntil($bytes, $max = null) + { + if ($this->char >= $this->EOF) { + return false; + } + + if (0 === $max || $max) { + $len = strcspn($this->data, $bytes, $this->char, $max); + } else { + $len = strcspn($this->data, $bytes, $this->char); + } + + $string = (string) substr($this->data, $this->char, $len); + $this->char += $len; + + return $string; + } + + /** + * Returns the string so long as $bytes matches. + * + * Matches as far as possible with a certain set of bytes + * and returns the matched substring. + * + * @param string $bytes A mask of bytes to match. If ANY byte in this mask matches the + * current char, the pointer advances and the char is part of the + * substring. + * @param int $max The max number of chars to read. + * + * @return string + */ + public function charsWhile($bytes, $max = null) + { + if ($this->char >= $this->EOF) { + return false; + } + + if (0 === $max || $max) { + $len = strspn($this->data, $bytes, $this->char, $max); + } else { + $len = strspn($this->data, $bytes, $this->char); + } + $string = (string) substr($this->data, $this->char, $len); + $this->char += $len; + + return $string; + } + + /** + * Unconsume characters. + * + * @param int $howMany The number of characters to unconsume. + */ + public function unconsume($howMany = 1) + { + if (($this->char - $howMany) >= 0) { + $this->char -= $howMany; + } + } + + /** + * Look ahead without moving cursor. + */ + public function peek() + { + if (($this->char + 1) <= $this->EOF) { + return $this->data[$this->char + 1]; + } + + return false; + } + + #[\ReturnTypeWillChange] + public function key() + { + return $this->char; + } +} diff --git a/3rdparty/masterminds/html5/src/HTML5/Parser/Tokenizer.php b/3rdparty/masterminds/html5/src/HTML5/Parser/Tokenizer.php new file mode 100644 index 00000000..e8b4aa09 --- /dev/null +++ b/3rdparty/masterminds/html5/src/HTML5/Parser/Tokenizer.php @@ -0,0 +1,1214 @@ +scanner = $scanner; + $this->events = $eventHandler; + $this->mode = $mode; + } + + /** + * Begin parsing. + * + * This will begin scanning the document, tokenizing as it goes. + * Tokens are emitted into the event handler. + * + * Tokenizing will continue until the document is completely + * read. Errors are emitted into the event handler, but + * the parser will attempt to continue parsing until the + * entire input stream is read. + */ + public function parse() + { + do { + $this->consumeData(); + // FIXME: Add infinite loop protection. + } while ($this->carryOn); + } + + /** + * Set the text mode for the character data reader. + * + * HTML5 defines three different modes for reading text: + * - Normal: Read until a tag is encountered. + * - RCDATA: Read until a tag is encountered, but skip a few otherwise- + * special characters. + * - Raw: Read until a special closing tag is encountered (viz. pre, script) + * + * This allows those modes to be set. + * + * Normally, setting is done by the event handler via a special return code on + * startTag(), but it can also be set manually using this function. + * + * @param int $textmode One of Elements::TEXT_*. + * @param string $untilTag The tag that should stop RAW or RCDATA mode. Normal mode does not + * use this indicator. + */ + public function setTextMode($textmode, $untilTag = null) + { + $this->textMode = $textmode & (Elements::TEXT_RAW | Elements::TEXT_RCDATA); + $this->untilTag = $untilTag; + } + + /** + * Consume a character and make a move. + * HTML5 8.2.4.1. + */ + protected function consumeData() + { + $tok = $this->scanner->current(); + + if ('&' === $tok) { + // Character reference + $ref = $this->decodeCharacterReference(); + $this->buffer($ref); + + $tok = $this->scanner->current(); + } + + // Parse tag + if ('<' === $tok) { + // Any buffered text data can go out now. + $this->flushBuffer(); + + $tok = $this->scanner->next(); + + if (false === $tok) { + // end of string + $this->parseError('Illegal tag opening'); + } elseif ('!' === $tok) { + $this->markupDeclaration(); + } elseif ('/' === $tok) { + $this->endTag(); + } elseif ('?' === $tok) { + $this->processingInstruction(); + } elseif ($this->is_alpha($tok)) { + $this->tagName(); + } else { + $this->parseError('Illegal tag opening'); + // TODO is this necessary ? + $this->characterData(); + } + + $tok = $this->scanner->current(); + } + + if (false === $tok) { + // Handle end of document + $this->eof(); + } else { + // Parse character + switch ($this->textMode) { + case Elements::TEXT_RAW: + $this->rawText($tok); + break; + + case Elements::TEXT_RCDATA: + $this->rcdata($tok); + break; + + default: + if ('<' === $tok || '&' === $tok) { + break; + } + + // NULL character + if ("\00" === $tok) { + $this->parseError('Received null character.'); + + $this->text .= $tok; + $this->scanner->consume(); + + break; + } + + $this->text .= $this->scanner->charsUntil("<&\0"); + } + } + + return $this->carryOn; + } + + /** + * Parse anything that looks like character data. + * + * Different rules apply based on the current text mode. + * + * @see Elements::TEXT_RAW Elements::TEXT_RCDATA. + */ + protected function characterData() + { + $tok = $this->scanner->current(); + if (false === $tok) { + return false; + } + switch ($this->textMode) { + case Elements::TEXT_RAW: + return $this->rawText($tok); + case Elements::TEXT_RCDATA: + return $this->rcdata($tok); + default: + if ('<' === $tok || '&' === $tok) { + return false; + } + + return $this->text($tok); + } + } + + /** + * This buffers the current token as character data. + * + * @param string $tok The current token. + * + * @return bool + */ + protected function text($tok) + { + // This should never happen... + if (false === $tok) { + return false; + } + + // NULL character + if ("\00" === $tok) { + $this->parseError('Received null character.'); + } + + $this->buffer($tok); + $this->scanner->consume(); + + return true; + } + + /** + * Read text in RAW mode. + * + * @param string $tok The current token. + * + * @return bool + */ + protected function rawText($tok) + { + if (is_null($this->untilTag)) { + return $this->text($tok); + } + + $sequence = 'untilTag . '>'; + $txt = $this->readUntilSequence($sequence); + $this->events->text($txt); + $this->setTextMode(0); + + return $this->endTag(); + } + + /** + * Read text in RCDATA mode. + * + * @param string $tok The current token. + * + * @return bool + */ + protected function rcdata($tok) + { + if (is_null($this->untilTag)) { + return $this->text($tok); + } + + $sequence = 'untilTag; + $txt = ''; + + $caseSensitive = !Elements::isHtml5Element($this->untilTag); + while (false !== $tok && !('<' == $tok && ($this->scanner->sequenceMatches($sequence, $caseSensitive)))) { + if ('&' == $tok) { + $txt .= $this->decodeCharacterReference(); + $tok = $this->scanner->current(); + } else { + $txt .= $tok; + $tok = $this->scanner->next(); + } + } + $len = strlen($sequence); + $this->scanner->consume($len); + $len += $this->scanner->whitespace(); + if ('>' !== $this->scanner->current()) { + $this->parseError('Unclosed RCDATA end tag'); + } + + $this->scanner->unconsume($len); + $this->events->text($txt); + $this->setTextMode(0); + + return $this->endTag(); + } + + /** + * If the document is read, emit an EOF event. + */ + protected function eof() + { + // fprintf(STDOUT, "EOF"); + $this->flushBuffer(); + $this->events->eof(); + $this->carryOn = false; + } + + /** + * Look for markup. + */ + protected function markupDeclaration() + { + $tok = $this->scanner->next(); + + // Comment: + if ('-' == $tok && '-' == $this->scanner->peek()) { + $this->scanner->consume(2); + + return $this->comment(); + } elseif ('D' == $tok || 'd' == $tok) { // Doctype + return $this->doctype(); + } elseif ('[' == $tok) { // CDATA section + return $this->cdataSection(); + } + + // FINISH + $this->parseError('Expected . Emit an empty comment because 8.2.4.46 says to. + if ('>' == $tok) { + // Parse error. Emit the comment token. + $this->parseError("Expected comment data, got '>'"); + $this->events->comment(''); + $this->scanner->consume(); + + return true; + } + + // Replace NULL with the replacement char. + if ("\0" == $tok) { + $tok = UTF8Utils::FFFD; + } + while (!$this->isCommentEnd()) { + $comment .= $tok; + $tok = $this->scanner->next(); + } + + $this->events->comment($comment); + $this->scanner->consume(); + + return true; + } + + /** + * Check if the scanner has reached the end of a comment. + * + * @return bool + */ + protected function isCommentEnd() + { + $tok = $this->scanner->current(); + + // EOF + if (false === $tok) { + // Hit the end. + $this->parseError('Unexpected EOF in a comment.'); + + return true; + } + + // If next two tokens are not '--', not the end. + if ('-' != $tok || '-' != $this->scanner->peek()) { + return false; + } + + $this->scanner->consume(2); // Consume '-' and one of '!' or '>' + + // Test for '>' + if ('>' == $this->scanner->current()) { + return true; + } + // Test for '!>' + if ('!' == $this->scanner->current() && '>' == $this->scanner->peek()) { + $this->scanner->consume(); // Consume the last '>' + return true; + } + // Unread '-' and one of '!' or '>'; + $this->scanner->unconsume(2); + + return false; + } + + /** + * Parse a DOCTYPE. + * + * Parse a DOCTYPE declaration. This method has strong bearing on whether or + * not Quirksmode is enabled on the event handler. + * + * @todo This method is a little long. Should probably refactor. + * + * @return bool + */ + protected function doctype() + { + // Check that string is DOCTYPE. + if ($this->scanner->sequenceMatches('DOCTYPE', false)) { + $this->scanner->consume(7); + } else { + $chars = $this->scanner->charsWhile('DOCTYPEdoctype'); + $this->parseError('Expected DOCTYPE, got %s', $chars); + + return $this->bogusComment('scanner->whitespace(); + $tok = $this->scanner->current(); + + // EOF: die. + if (false === $tok) { + $this->events->doctype('html5', EventHandler::DOCTYPE_NONE, '', true); + $this->eof(); + + return true; + } + + // NULL char: convert. + if ("\0" === $tok) { + $this->parseError('Unexpected null character in DOCTYPE.'); + } + + $stop = " \n\f>"; + $doctypeName = $this->scanner->charsUntil($stop); + // Lowercase ASCII, replace \0 with FFFD + $doctypeName = strtolower(strtr($doctypeName, "\0", UTF8Utils::FFFD)); + + $tok = $this->scanner->current(); + + // If false, emit a parse error, DOCTYPE, and return. + if (false === $tok) { + $this->parseError('Unexpected EOF in DOCTYPE declaration.'); + $this->events->doctype($doctypeName, EventHandler::DOCTYPE_NONE, null, true); + + return true; + } + + // Short DOCTYPE, like + if ('>' == $tok) { + // DOCTYPE without a name. + if (0 == strlen($doctypeName)) { + $this->parseError('Expected a DOCTYPE name. Got nothing.'); + $this->events->doctype($doctypeName, 0, null, true); + $this->scanner->consume(); + + return true; + } + $this->events->doctype($doctypeName); + $this->scanner->consume(); + + return true; + } + $this->scanner->whitespace(); + + $pub = strtoupper($this->scanner->getAsciiAlpha()); + $white = $this->scanner->whitespace(); + + // Get ID, and flag it as pub or system. + if (('PUBLIC' == $pub || 'SYSTEM' == $pub) && $white > 0) { + // Get the sys ID. + $type = 'PUBLIC' == $pub ? EventHandler::DOCTYPE_PUBLIC : EventHandler::DOCTYPE_SYSTEM; + $id = $this->quotedString("\0>"); + if (false === $id) { + $this->events->doctype($doctypeName, $type, $pub, false); + + return true; + } + + // Premature EOF. + if (false === $this->scanner->current()) { + $this->parseError('Unexpected EOF in DOCTYPE'); + $this->events->doctype($doctypeName, $type, $id, true); + + return true; + } + + // Well-formed complete DOCTYPE. + $this->scanner->whitespace(); + if ('>' == $this->scanner->current()) { + $this->events->doctype($doctypeName, $type, $id, false); + $this->scanner->consume(); + + return true; + } + + // If we get here, we have scanner->charsUntil('>'); + $this->parseError('Malformed DOCTYPE.'); + $this->events->doctype($doctypeName, $type, $id, true); + $this->scanner->consume(); + + return true; + } + + // Else it's a bogus DOCTYPE. + // Consume to > and trash. + $this->scanner->charsUntil('>'); + + $this->parseError('Expected PUBLIC or SYSTEM. Got %s.', $pub); + $this->events->doctype($doctypeName, 0, null, true); + $this->scanner->consume(); + + return true; + } + + /** + * Utility for reading a quoted string. + * + * @param string $stopchars Characters (in addition to a close-quote) that should stop the string. + * E.g. sometimes '>' is higher precedence than '"' or "'". + * + * @return mixed String if one is found (quotations omitted). + */ + protected function quotedString($stopchars) + { + $tok = $this->scanner->current(); + if ('"' == $tok || "'" == $tok) { + $this->scanner->consume(); + $ret = $this->scanner->charsUntil($tok . $stopchars); + if ($this->scanner->current() == $tok) { + $this->scanner->consume(); + } else { + // Parse error because no close quote. + $this->parseError('Expected %s, got %s', $tok, $this->scanner->current()); + } + + return $ret; + } + + return false; + } + + /** + * Handle a CDATA section. + * + * @return bool + */ + protected function cdataSection() + { + $cdata = ''; + $this->scanner->consume(); + + $chars = $this->scanner->charsWhile('CDAT'); + if ('CDATA' != $chars || '[' != $this->scanner->current()) { + $this->parseError('Expected [CDATA[, got %s', $chars); + + return $this->bogusComment('scanner->next(); + do { + if (false === $tok) { + $this->parseError('Unexpected EOF inside CDATA.'); + $this->bogusComment('scanner->next(); + } while (!$this->scanner->sequenceMatches(']]>')); + + // Consume ]]> + $this->scanner->consume(3); + + $this->events->cdata($cdata); + + return true; + } + + // ================================================================ + // Non-HTML5 + // ================================================================ + + /** + * Handle a processing instruction. + * + * XML processing instructions are supposed to be ignored in HTML5, + * treated as "bogus comments". However, since we're not a user + * agent, we allow them. We consume until ?> and then issue a + * EventListener::processingInstruction() event. + * + * @return bool + */ + protected function processingInstruction() + { + if ('?' != $this->scanner->current()) { + return false; + } + + $tok = $this->scanner->next(); + $procName = $this->scanner->getAsciiAlpha(); + $white = $this->scanner->whitespace(); + + // If not a PI, send to bogusComment. + if (0 == strlen($procName) || 0 == $white || false == $this->scanner->current()) { + $this->parseError("Expected processing instruction name, got $tok"); + $this->bogusComment('. + while (!('?' == $this->scanner->current() && '>' == $this->scanner->peek())) { + $data .= $this->scanner->current(); + + $tok = $this->scanner->next(); + if (false === $tok) { + $this->parseError('Unexpected EOF in processing instruction.'); + $this->events->processingInstruction($procName, $data); + + return true; + } + } + + $this->scanner->consume(2); // Consume the closing tag + $this->events->processingInstruction($procName, $data); + + return true; + } + + // ================================================================ + // UTILITY FUNCTIONS + // ================================================================ + + /** + * Read from the input stream until we get to the desired sequene + * or hit the end of the input stream. + * + * @param string $sequence + * + * @return string + */ + protected function readUntilSequence($sequence) + { + $buffer = ''; + + // Optimization for reading larger blocks faster. + $first = substr($sequence, 0, 1); + while (false !== $this->scanner->current()) { + $buffer .= $this->scanner->charsUntil($first); + + // Stop as soon as we hit the stopping condition. + if ($this->scanner->sequenceMatches($sequence, false)) { + return $buffer; + } + $buffer .= $this->scanner->current(); + $this->scanner->consume(); + } + + // If we get here, we hit the EOF. + $this->parseError('Unexpected EOF during text read.'); + + return $buffer; + } + + /** + * Check if upcomming chars match the given sequence. + * + * This will read the stream for the $sequence. If it's + * found, this will return true. If not, return false. + * Since this unconsumes any chars it reads, the caller + * will still need to read the next sequence, even if + * this returns true. + * + * Example: $this->scanner->sequenceMatches('') will + * see if the input stream is at the start of a + * '' string. + * + * @param string $sequence + * @param bool $caseSensitive + * + * @return bool + */ + protected function sequenceMatches($sequence, $caseSensitive = true) + { + @trigger_error(__METHOD__ . ' method is deprecated since version 2.4 and will be removed in 3.0. Use Scanner::sequenceMatches() instead.', E_USER_DEPRECATED); + + return $this->scanner->sequenceMatches($sequence, $caseSensitive); + } + + /** + * Send a TEXT event with the contents of the text buffer. + * + * This emits an EventHandler::text() event with the current contents of the + * temporary text buffer. (The buffer is used to group as much PCDATA + * as we can instead of emitting lots and lots of TEXT events.) + */ + protected function flushBuffer() + { + if ('' === $this->text) { + return; + } + $this->events->text($this->text); + $this->text = ''; + } + + /** + * Add text to the temporary buffer. + * + * @see flushBuffer() + * + * @param string $str + */ + protected function buffer($str) + { + $this->text .= $str; + } + + /** + * Emit a parse error. + * + * A parse error always returns false because it never consumes any + * characters. + * + * @param string $msg + * + * @return string + */ + protected function parseError($msg) + { + $args = func_get_args(); + + if (count($args) > 1) { + array_shift($args); + $msg = vsprintf($msg, $args); + } + + $line = $this->scanner->currentLine(); + $col = $this->scanner->columnOffset(); + $this->events->parseError($msg, $line, $col); + + return false; + } + + /** + * Decode a character reference and return the string. + * + * If $inAttribute is set to true, a bare & will be returned as-is. + * + * @param bool $inAttribute Set to true if the text is inside of an attribute value. + * false otherwise. + * + * @return string + */ + protected function decodeCharacterReference($inAttribute = false) + { + // Next char after &. + $tok = $this->scanner->next(); + $start = $this->scanner->position(); + + if (false === $tok) { + return '&'; + } + + // These indicate not an entity. We return just + // the &. + if ("\t" === $tok || "\n" === $tok || "\f" === $tok || ' ' === $tok || '&' === $tok || '<' === $tok) { + // $this->scanner->next(); + return '&'; + } + + // Numeric entity + if ('#' === $tok) { + $tok = $this->scanner->next(); + + if (false === $tok) { + $this->parseError('Expected &#DEC; &#HEX;, got EOF'); + $this->scanner->unconsume(1); + + return '&'; + } + + // Hexidecimal encoding. + // X[0-9a-fA-F]+; + // x[0-9a-fA-F]+; + if ('x' === $tok || 'X' === $tok) { + $tok = $this->scanner->next(); // Consume x + + // Convert from hex code to char. + $hex = $this->scanner->getHex(); + if (empty($hex)) { + $this->parseError('Expected &#xHEX;, got &#x%s', $tok); + // We unconsume because we don't know what parser rules might + // be in effect for the remaining chars. For example. '&#>' + // might result in a specific parsing rule inside of tag + // contexts, while not inside of pcdata context. + $this->scanner->unconsume(2); + + return '&'; + } + $entity = CharacterReference::lookupHex($hex); + } // Decimal encoding. + // [0-9]+; + else { + // Convert from decimal to char. + $numeric = $this->scanner->getNumeric(); + if (false === $numeric) { + $this->parseError('Expected &#DIGITS;, got &#%s', $tok); + $this->scanner->unconsume(2); + + return '&'; + } + $entity = CharacterReference::lookupDecimal($numeric); + } + } elseif ('=' === $tok && $inAttribute) { + return '&'; + } else { // String entity. + // Attempt to consume a string up to a ';'. + // [a-zA-Z0-9]+; + $cname = $this->scanner->getAsciiAlphaNum(); + $entity = CharacterReference::lookupName($cname); + + // When no entity is found provide the name of the unmatched string + // and continue on as the & is not part of an entity. The & will + // be converted to & elsewhere. + if (null === $entity) { + if (!$inAttribute || '' === $cname) { + $this->parseError("No match in entity table for '%s'", $cname); + } + $this->scanner->unconsume($this->scanner->position() - $start); + + return '&'; + } + } + + // The scanner has advanced the cursor for us. + $tok = $this->scanner->current(); + + // We have an entity. We're done here. + if (';' === $tok) { + $this->scanner->consume(); + + return $entity; + } + + // Failing to match ; means unconsume the entire string. + $this->scanner->unconsume($this->scanner->position() - $start); + + $this->parseError('Expected &ENTITY;, got &ENTITY%s (no trailing ;) ', $tok); + + return '&'; + } + + /** + * Checks whether a (single-byte) character is an ASCII letter or not. + * + * @param string $input A single-byte string + * + * @return bool True if it is a letter, False otherwise + */ + protected function is_alpha($input) + { + $code = ord($input); + + return ($code >= 97 && $code <= 122) || ($code >= 65 && $code <= 90); + } +} diff --git a/3rdparty/masterminds/html5/src/HTML5/Parser/TreeBuildingRules.php b/3rdparty/masterminds/html5/src/HTML5/Parser/TreeBuildingRules.php new file mode 100644 index 00000000..00d3951f --- /dev/null +++ b/3rdparty/masterminds/html5/src/HTML5/Parser/TreeBuildingRules.php @@ -0,0 +1,127 @@ + 1, + 'dd' => 1, + 'dt' => 1, + 'rt' => 1, + 'rp' => 1, + 'tr' => 1, + 'th' => 1, + 'td' => 1, + 'thead' => 1, + 'tfoot' => 1, + 'tbody' => 1, + 'table' => 1, + 'optgroup' => 1, + 'option' => 1, + ); + + /** + * Returns true if the given tagname has special processing rules. + */ + public function hasRules($tagname) + { + return isset(static::$tags[$tagname]); + } + + /** + * Evaluate the rule for the current tag name. + * + * This may modify the existing DOM. + * + * @return \DOMElement The new Current DOM element. + */ + public function evaluate($new, $current) + { + switch ($new->tagName) { + case 'li': + return $this->handleLI($new, $current); + case 'dt': + case 'dd': + return $this->handleDT($new, $current); + case 'rt': + case 'rp': + return $this->handleRT($new, $current); + case 'optgroup': + return $this->closeIfCurrentMatches($new, $current, array( + 'optgroup', + )); + case 'option': + return $this->closeIfCurrentMatches($new, $current, array( + 'option', + )); + case 'tr': + return $this->closeIfCurrentMatches($new, $current, array( + 'tr', + )); + case 'td': + case 'th': + return $this->closeIfCurrentMatches($new, $current, array( + 'th', + 'td', + )); + case 'tbody': + case 'thead': + case 'tfoot': + case 'table': // Spec isn't explicit about this, but it's necessary. + + return $this->closeIfCurrentMatches($new, $current, array( + 'thead', + 'tfoot', + 'tbody', + )); + } + + return $current; + } + + protected function handleLI($ele, $current) + { + return $this->closeIfCurrentMatches($ele, $current, array( + 'li', + )); + } + + protected function handleDT($ele, $current) + { + return $this->closeIfCurrentMatches($ele, $current, array( + 'dt', + 'dd', + )); + } + + protected function handleRT($ele, $current) + { + return $this->closeIfCurrentMatches($ele, $current, array( + 'rt', + 'rp', + )); + } + + protected function closeIfCurrentMatches($ele, $current, $match) + { + if (in_array($current->tagName, $match, true)) { + $current->parentNode->appendChild($ele); + } else { + $current->appendChild($ele); + } + + return $ele; + } +} diff --git a/3rdparty/masterminds/html5/src/HTML5/Parser/UTF8Utils.php b/3rdparty/masterminds/html5/src/HTML5/Parser/UTF8Utils.php new file mode 100644 index 00000000..4405e4cc --- /dev/null +++ b/3rdparty/masterminds/html5/src/HTML5/Parser/UTF8Utils.php @@ -0,0 +1,177 @@ + + +Permission is hereby granted, free of charge, to any person obtaining a +copy of this software and associated documentation files (the + "Software"), to deal in the Software without restriction, including +without limitation the rights to use, copy, modify, merge, publish, +distribute, sublicense, and/or sell copies of the Software, and to +permit persons to whom the Software is furnished to do so, subject to +the following conditions: + +The above copyright notice and this permission notice shall be included +in all copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS +OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF +MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. +IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY +CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, +TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE +SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. +*/ + +use Masterminds\HTML5\Exception; + +class UTF8Utils +{ + /** + * The Unicode replacement character. + */ + const FFFD = "\xEF\xBF\xBD"; + + /** + * Count the number of characters in a string. + * UTF-8 aware. This will try (in order) iconv, MB, and finally a custom counter. + * + * @param string $string + * + * @return int + */ + public static function countChars($string) + { + // Get the length for the string we need. + if (function_exists('mb_strlen')) { + return mb_strlen($string, 'utf-8'); + } + + if (function_exists('iconv_strlen')) { + return iconv_strlen($string, 'utf-8'); + } + + $count = count_chars($string); + + // 0x80 = 0x7F - 0 + 1 (one added to get inclusive range) + // 0x33 = 0xF4 - 0x2C + 1 (one added to get inclusive range) + return array_sum(array_slice($count, 0, 0x80)) + array_sum(array_slice($count, 0xC2, 0x33)); + } + + /** + * Convert data from the given encoding to UTF-8. + * + * This has not yet been tested with charactersets other than UTF-8. + * It should work with ISO-8859-1/-13 and standard Latin Win charsets. + * + * @param string $data The data to convert + * @param string $encoding A valid encoding. Examples: http://www.php.net/manual/en/mbstring.supported-encodings.php + * + * @return string + */ + public static function convertToUTF8($data, $encoding = 'UTF-8') + { + /* + * From the HTML5 spec: Given an encoding, the bytes in the input stream must be converted + * to Unicode characters for the tokeniser, as described by the rules for that encoding, + * except that the leading U+FEFF BYTE ORDER MARK character, if any, must not be stripped + * by the encoding layer (it is stripped by the rule below). Bytes or sequences of bytes + * in the original byte stream that could not be converted to Unicode characters must be + * converted to U+FFFD REPLACEMENT CHARACTER code points. + */ + + // mb_convert_encoding is chosen over iconv because of a bug. The best + // details for the bug are on http://us1.php.net/manual/en/function.iconv.php#108643 + // which contains links to the actual but reports as well as work around + // details. + if (function_exists('mb_convert_encoding')) { + // mb library has the following behaviors: + // - UTF-16 surrogates result in false. + // - Overlongs and outside Plane 16 result in empty strings. + + // Before we run mb_convert_encoding we need to tell it what to do with + // characters it does not know. This could be different than the parent + // application executing this library so we store the value, change it + // to our needs, and then change it back when we are done. This feels + // a little excessive and it would be great if there was a better way. + $save = mb_substitute_character(); + mb_substitute_character('none'); + $data = mb_convert_encoding($data, 'UTF-8', $encoding); + mb_substitute_character($save); + } + // @todo Get iconv running in at least some environments if that is possible. + elseif (function_exists('iconv') && 'auto' !== $encoding) { + // fprintf(STDOUT, "iconv found\n"); + // iconv has the following behaviors: + // - Overlong representations are ignored. + // - Beyond Plane 16 is replaced with a lower char. + // - Incomplete sequences generate a warning. + $data = @iconv($encoding, 'UTF-8//IGNORE', $data); + } else { + throw new Exception('Not implemented, please install mbstring or iconv'); + } + + /* + * One leading U+FEFF BYTE ORDER MARK character must be ignored if any are present. + */ + if ("\xEF\xBB\xBF" === substr($data, 0, 3)) { + $data = substr($data, 3); + } + + return $data; + } + + /** + * Checks for Unicode code points that are not valid in a document. + * + * @param string $data A string to analyze + * + * @return array An array of (string) error messages produced by the scanning + */ + public static function checkForIllegalCodepoints($data) + { + // Vestigal error handling. + $errors = array(); + + /* + * All U+0000 null characters in the input must be replaced by U+FFFD REPLACEMENT CHARACTERs. + * Any occurrences of such characters is a parse error. + */ + for ($i = 0, $count = substr_count($data, "\0"); $i < $count; ++$i) { + $errors[] = 'null-character'; + } + + /* + * Any occurrences of any characters in the ranges U+0001 to U+0008, U+000B, U+000E to U+001F, U+007F + * to U+009F, U+D800 to U+DFFF , U+FDD0 to U+FDEF, and characters U+FFFE, U+FFFF, U+1FFFE, U+1FFFF, + * U+2FFFE, U+2FFFF, U+3FFFE, U+3FFFF, U+4FFFE, U+4FFFF, U+5FFFE, U+5FFFF, U+6FFFE, U+6FFFF, U+7FFFE, + * U+7FFFF, U+8FFFE, U+8FFFF, U+9FFFE, U+9FFFF, U+AFFFE, U+AFFFF, U+BFFFE, U+BFFFF, U+CFFFE, U+CFFFF, + * U+DFFFE, U+DFFFF, U+EFFFE, U+EFFFF, U+FFFFE, U+FFFFF, U+10FFFE, and U+10FFFF are parse errors. + * (These are all control characters or permanently undefined Unicode characters.) + */ + // Check PCRE is loaded. + $count = preg_match_all( + '/(?: + [\x01-\x08\x0B\x0E-\x1F\x7F] # U+0001 to U+0008, U+000B, U+000E to U+001F and U+007F + | + \xC2[\x80-\x9F] # U+0080 to U+009F + | + \xED(?:\xA0[\x80-\xFF]|[\xA1-\xBE][\x00-\xFF]|\xBF[\x00-\xBF]) # U+D800 to U+DFFFF + | + \xEF\xB7[\x90-\xAF] # U+FDD0 to U+FDEF + | + \xEF\xBF[\xBE\xBF] # U+FFFE and U+FFFF + | + [\xF0-\xF4][\x8F-\xBF]\xBF[\xBE\xBF] # U+nFFFE and U+nFFFF (1 <= n <= 10_{16}) + )/x', $data, $matches); + for ($i = 0; $i < $count; ++$i) { + $errors[] = 'invalid-codepoint'; + } + + return $errors; + } +} diff --git a/3rdparty/masterminds/html5/src/HTML5/Serializer/HTML5Entities.php b/3rdparty/masterminds/html5/src/HTML5/Serializer/HTML5Entities.php new file mode 100644 index 00000000..e9421a12 --- /dev/null +++ b/3rdparty/masterminds/html5/src/HTML5/Serializer/HTML5Entities.php @@ -0,0 +1,1533 @@ + ' ', + "\n" => ' ', + '!' => '!', + '"' => '"', + '#' => '#', + '$' => '$', + '%' => '%', + '&' => '&', + '\'' => ''', + '(' => '(', + ')' => ')', + '*' => '*', + '+' => '+', + ',' => ',', + '.' => '.', + '/' => '/', + ':' => ':', + ';' => ';', + '<' => '<', + '<⃒' => '&nvlt', + '=' => '=', + '=⃥' => '&bne', + '>' => '>', + '>⃒' => '&nvgt', + '?' => '?', + '@' => '@', + '[' => '[', + '\\' => '\', + ']' => ']', + '^' => '^', + '_' => '_', + '`' => '`', + 'fj' => '&fjlig', + '{' => '{', + '|' => '|', + '}' => '}', + ' ' => ' ', + '¡' => '¡', + '¢' => '¢', + '£' => '£', + '¤' => '¤', + '¥' => '¥', + '¦' => '¦', + '§' => '§', + '¨' => '¨', + '©' => '©', + 'ª' => 'ª', + '«' => '«', + '¬' => '¬', + '­' => '­', + '®' => '®', + '¯' => '¯', + '°' => '°', + '±' => '±', + '²' => '²', + '³' => '³', + '´' => '´', + 'µ' => 'µ', + '¶' => '¶', + '·' => '·', + '¸' => '¸', + '¹' => '¹', + 'º' => 'º', + '»' => '»', + '¼' => '¼', + '½' => '½', + '¾' => '¾', + '¿' => '¿', + 'À' => 'À', + 'Á' => 'Á', + 'Â' => 'Â', + 'Ã' => 'Ã', + 'Ä' => 'Ä', + 'Å' => 'Å', + 'Æ' => 'Æ', + 'Ç' => 'Ç', + 'È' => 'È', + 'É' => 'É', + 'Ê' => 'Ê', + 'Ë' => 'Ë', + 'Ì' => 'Ì', + 'Í' => 'Í', + 'Î' => 'Î', + 'Ï' => 'Ï', + 'Ð' => 'Ð', + 'Ñ' => 'Ñ', + 'Ò' => 'Ò', + 'Ó' => 'Ó', + 'Ô' => 'Ô', + 'Õ' => 'Õ', + 'Ö' => 'Ö', + '×' => '×', + 'Ø' => 'Ø', + 'Ù' => 'Ù', + 'Ú' => 'Ú', + 'Û' => 'Û', + 'Ü' => 'Ü', + 'Ý' => 'Ý', + 'Þ' => 'Þ', + 'ß' => 'ß', + 'à' => 'à', + 'á' => 'á', + 'â' => 'â', + 'ã' => 'ã', + 'ä' => 'ä', + 'å' => 'å', + 'æ' => 'æ', + 'ç' => 'ç', + 'è' => 'è', + 'é' => 'é', + 'ê' => 'ê', + 'ë' => 'ë', + 'ì' => 'ì', + 'í' => 'í', + 'î' => 'î', + 'ï' => 'ï', + 'ð' => 'ð', + 'ñ' => 'ñ', + 'ò' => 'ò', + 'ó' => 'ó', + 'ô' => 'ô', + 'õ' => 'õ', + 'ö' => 'ö', + '÷' => '÷', + 'ø' => 'ø', + 'ù' => 'ù', + 'ú' => 'ú', + 'û' => 'û', + 'ü' => 'ü', + 'ý' => 'ý', + 'þ' => 'þ', + 'ÿ' => 'ÿ', + 'Ā' => 'Ā', + 'ā' => 'ā', + 'Ă' => 'Ă', + 'ă' => 'ă', + 'Ą' => 'Ą', + 'ą' => 'ą', + 'Ć' => 'Ć', + 'ć' => 'ć', + 'Ĉ' => 'Ĉ', + 'ĉ' => 'ĉ', + 'Ċ' => 'Ċ', + 'ċ' => 'ċ', + 'Č' => 'Č', + 'č' => 'č', + 'Ď' => 'Ď', + 'ď' => 'ď', + 'Đ' => 'Đ', + 'đ' => 'đ', + 'Ē' => 'Ē', + 'ē' => 'ē', + 'Ė' => 'Ė', + 'ė' => 'ė', + 'Ę' => 'Ę', + 'ę' => 'ę', + 'Ě' => 'Ě', + 'ě' => 'ě', + 'Ĝ' => 'Ĝ', + 'ĝ' => 'ĝ', + 'Ğ' => 'Ğ', + 'ğ' => 'ğ', + 'Ġ' => 'Ġ', + 'ġ' => 'ġ', + 'Ģ' => 'Ģ', + 'Ĥ' => 'Ĥ', + 'ĥ' => 'ĥ', + 'Ħ' => 'Ħ', + 'ħ' => 'ħ', + 'Ĩ' => 'Ĩ', + 'ĩ' => 'ĩ', + 'Ī' => 'Ī', + 'ī' => 'ī', + 'Į' => 'Į', + 'į' => 'į', + 'İ' => 'İ', + 'ı' => 'ı', + 'IJ' => 'IJ', + 'ij' => 'ij', + 'Ĵ' => 'Ĵ', + 'ĵ' => 'ĵ', + 'Ķ' => 'Ķ', + 'ķ' => 'ķ', + 'ĸ' => 'ĸ', + 'Ĺ' => 'Ĺ', + 'ĺ' => 'ĺ', + 'Ļ' => 'Ļ', + 'ļ' => 'ļ', + 'Ľ' => 'Ľ', + 'ľ' => 'ľ', + 'Ŀ' => 'Ŀ', + 'ŀ' => 'ŀ', + 'Ł' => 'Ł', + 'ł' => 'ł', + 'Ń' => 'Ń', + 'ń' => 'ń', + 'Ņ' => 'Ņ', + 'ņ' => 'ņ', + 'Ň' => 'Ň', + 'ň' => 'ň', + 'ʼn' => 'ʼn', + 'Ŋ' => 'Ŋ', + 'ŋ' => 'ŋ', + 'Ō' => 'Ō', + 'ō' => 'ō', + 'Ő' => 'Ő', + 'ő' => 'ő', + 'Œ' => 'Œ', + 'œ' => 'œ', + 'Ŕ' => 'Ŕ', + 'ŕ' => 'ŕ', + 'Ŗ' => 'Ŗ', + 'ŗ' => 'ŗ', + 'Ř' => 'Ř', + 'ř' => 'ř', + 'Ś' => 'Ś', + 'ś' => 'ś', + 'Ŝ' => 'Ŝ', + 'ŝ' => 'ŝ', + 'Ş' => 'Ş', + 'ş' => 'ş', + 'Š' => 'Š', + 'š' => 'š', + 'Ţ' => 'Ţ', + 'ţ' => 'ţ', + 'Ť' => 'Ť', + 'ť' => 'ť', + 'Ŧ' => 'Ŧ', + 'ŧ' => 'ŧ', + 'Ũ' => 'Ũ', + 'ũ' => 'ũ', + 'Ū' => 'Ū', + 'ū' => 'ū', + 'Ŭ' => 'Ŭ', + 'ŭ' => 'ŭ', + 'Ů' => 'Ů', + 'ů' => 'ů', + 'Ű' => 'Ű', + 'ű' => 'ű', + 'Ų' => 'Ų', + 'ų' => 'ų', + 'Ŵ' => 'Ŵ', + 'ŵ' => 'ŵ', + 'Ŷ' => 'Ŷ', + 'ŷ' => 'ŷ', + 'Ÿ' => 'Ÿ', + 'Ź' => 'Ź', + 'ź' => 'ź', + 'Ż' => 'Ż', + 'ż' => 'ż', + 'Ž' => 'Ž', + 'ž' => 'ž', + 'ƒ' => 'ƒ', + 'Ƶ' => 'Ƶ', + 'ǵ' => 'ǵ', + 'ȷ' => 'ȷ', + 'ˆ' => 'ˆ', + 'ˇ' => 'ˇ', + '˘' => '˘', + '˙' => '˙', + '˚' => '˚', + '˛' => '˛', + '˜' => '˜', + '˝' => '˝', + '̑' => '̑', + 'Α' => 'Α', + 'Β' => 'Β', + 'Γ' => 'Γ', + 'Δ' => 'Δ', + 'Ε' => 'Ε', + 'Ζ' => 'Ζ', + 'Η' => 'Η', + 'Θ' => 'Θ', + 'Ι' => 'Ι', + 'Κ' => 'Κ', + 'Λ' => 'Λ', + 'Μ' => 'Μ', + 'Ν' => 'Ν', + 'Ξ' => 'Ξ', + 'Ο' => 'Ο', + 'Π' => 'Π', + 'Ρ' => 'Ρ', + 'Σ' => 'Σ', + 'Τ' => 'Τ', + 'Υ' => 'Υ', + 'Φ' => 'Φ', + 'Χ' => 'Χ', + 'Ψ' => 'Ψ', + 'Ω' => 'Ω', + 'α' => 'α', + 'β' => 'β', + 'γ' => 'γ', + 'δ' => 'δ', + 'ε' => 'ε', + 'ζ' => 'ζ', + 'η' => 'η', + 'θ' => 'θ', + 'ι' => 'ι', + 'κ' => 'κ', + 'λ' => 'λ', + 'μ' => 'μ', + 'ν' => 'ν', + 'ξ' => 'ξ', + 'ο' => 'ο', + 'π' => 'π', + 'ρ' => 'ρ', + 'ς' => 'ς', + 'σ' => 'σ', + 'τ' => 'τ', + 'υ' => 'υ', + 'φ' => 'φ', + 'χ' => 'χ', + 'ψ' => 'ψ', + 'ω' => 'ω', + 'ϑ' => 'ϑ', + 'ϒ' => 'ϒ', + 'ϕ' => 'ϕ', + 'ϖ' => 'ϖ', + 'Ϝ' => 'Ϝ', + 'ϝ' => 'ϝ', + 'ϰ' => 'ϰ', + 'ϱ' => 'ϱ', + 'ϵ' => 'ϵ', + '϶' => '϶', + 'Ё' => 'Ё', + 'Ђ' => 'Ђ', + 'Ѓ' => 'Ѓ', + 'Є' => 'Є', + 'Ѕ' => 'Ѕ', + 'І' => 'І', + 'Ї' => 'Ї', + 'Ј' => 'Ј', + 'Љ' => 'Љ', + 'Њ' => 'Њ', + 'Ћ' => 'Ћ', + 'Ќ' => 'Ќ', + 'Ў' => 'Ў', + 'Џ' => 'Џ', + 'А' => 'А', + 'Б' => 'Б', + 'В' => 'В', + 'Г' => 'Г', + 'Д' => 'Д', + 'Е' => 'Е', + 'Ж' => 'Ж', + 'З' => 'З', + 'И' => 'И', + 'Й' => 'Й', + 'К' => 'К', + 'Л' => 'Л', + 'М' => 'М', + 'Н' => 'Н', + 'О' => 'О', + 'П' => 'П', + 'Р' => 'Р', + 'С' => 'С', + 'Т' => 'Т', + 'У' => 'У', + 'Ф' => 'Ф', + 'Х' => 'Х', + 'Ц' => 'Ц', + 'Ч' => 'Ч', + 'Ш' => 'Ш', + 'Щ' => 'Щ', + 'Ъ' => 'Ъ', + 'Ы' => 'Ы', + 'Ь' => 'Ь', + 'Э' => 'Э', + 'Ю' => 'Ю', + 'Я' => 'Я', + 'а' => 'а', + 'б' => 'б', + 'в' => 'в', + 'г' => 'г', + 'д' => 'д', + 'е' => 'е', + 'ж' => 'ж', + 'з' => 'з', + 'и' => 'и', + 'й' => 'й', + 'к' => 'к', + 'л' => 'л', + 'м' => 'м', + 'н' => 'н', + 'о' => 'о', + 'п' => 'п', + 'р' => 'р', + 'с' => 'с', + 'т' => 'т', + 'у' => 'у', + 'ф' => 'ф', + 'х' => 'х', + 'ц' => 'ц', + 'ч' => 'ч', + 'ш' => 'ш', + 'щ' => 'щ', + 'ъ' => 'ъ', + 'ы' => 'ы', + 'ь' => 'ь', + 'э' => 'э', + 'ю' => 'ю', + 'я' => 'я', + 'ё' => 'ё', + 'ђ' => 'ђ', + 'ѓ' => 'ѓ', + 'є' => 'є', + 'ѕ' => 'ѕ', + 'і' => 'і', + 'ї' => 'ї', + 'ј' => 'ј', + 'љ' => 'љ', + 'њ' => 'њ', + 'ћ' => 'ћ', + 'ќ' => 'ќ', + 'ў' => 'ў', + 'џ' => 'џ', + ' ' => ' ', + ' ' => ' ', + ' ' => ' ', + ' ' => ' ', + ' ' => ' ', + ' ' => ' ', + ' ' => ' ', + ' ' => ' ', + '​' => '​', + '‌' => '‌', + '‍' => '‍', + '‎' => '‎', + '‏' => '‏', + '‐' => '‐', + '–' => '–', + '—' => '—', + '―' => '―', + '‖' => '‖', + '‘' => '‘', + '’' => '’', + '‚' => '‚', + '“' => '“', + '”' => '”', + '„' => '„', + '†' => '†', + '‡' => '‡', + '•' => '•', + '‥' => '‥', + '…' => '…', + '‰' => '‰', + '‱' => '‱', + '′' => '′', + '″' => '″', + '‴' => '‴', + '‵' => '‵', + '‹' => '‹', + '›' => '›', + '‾' => '‾', + '⁁' => '⁁', + '⁃' => '⁃', + '⁄' => '⁄', + '⁏' => '⁏', + '⁗' => '⁗', + ' ' => ' ', + '  ' => '&ThickSpace', + '⁠' => '⁠', + '⁡' => '⁡', + '⁢' => '⁢', + '⁣' => '⁣', + '€' => '€', + '⃛' => '⃛', + '⃜' => '⃜', + 'ℂ' => 'ℂ', + '℅' => '℅', + 'ℊ' => 'ℊ', + 'ℋ' => 'ℋ', + 'ℌ' => 'ℌ', + 'ℍ' => 'ℍ', + 'ℎ' => 'ℎ', + 'ℏ' => 'ℏ', + 'ℐ' => 'ℐ', + 'ℑ' => 'ℑ', + 'ℒ' => 'ℒ', + 'ℓ' => 'ℓ', + 'ℕ' => 'ℕ', + '№' => '№', + '℗' => '℗', + '℘' => '℘', + 'ℙ' => 'ℙ', + 'ℚ' => 'ℚ', + 'ℛ' => 'ℛ', + 'ℜ' => 'ℜ', + 'ℝ' => 'ℝ', + '℞' => '℞', + '™' => '™', + 'ℤ' => 'ℤ', + '℧' => '℧', + 'ℨ' => 'ℨ', + '℩' => '℩', + 'ℬ' => 'ℬ', + 'ℭ' => 'ℭ', + 'ℯ' => 'ℯ', + 'ℰ' => 'ℰ', + 'ℱ' => 'ℱ', + 'ℳ' => 'ℳ', + 'ℴ' => 'ℴ', + 'ℵ' => 'ℵ', + 'ℶ' => 'ℶ', + 'ℷ' => 'ℷ', + 'ℸ' => 'ℸ', + 'ⅅ' => 'ⅅ', + 'ⅆ' => 'ⅆ', + 'ⅇ' => 'ⅇ', + 'ⅈ' => 'ⅈ', + '⅓' => '⅓', + '⅔' => '⅔', + '⅕' => '⅕', + '⅖' => '⅖', + '⅗' => '⅗', + '⅘' => '⅘', + '⅙' => '⅙', + '⅚' => '⅚', + '⅛' => '⅛', + '⅜' => '⅜', + '⅝' => '⅝', + '⅞' => '⅞', + '←' => '←', + '↑' => '↑', + '→' => '→', + '↓' => '↓', + '↔' => '↔', + '↕' => '↕', + '↖' => '↖', + '↗' => '↗', + '↘' => '↘', + '↙' => '↙', + '↚' => '↚', + '↛' => '↛', + '↝' => '↝', + '↝̸' => '&nrarrw', + '↞' => '↞', + '↟' => '↟', + '↠' => '↠', + '↡' => '↡', + '↢' => '↢', + '↣' => '↣', + '↤' => '↤', + '↥' => '↥', + '↦' => '↦', + '↧' => '↧', + '↩' => '↩', + '↪' => '↪', + '↫' => '↫', + '↬' => '↬', + '↭' => '↭', + '↮' => '↮', + '↰' => '↰', + '↱' => '↱', + '↲' => '↲', + '↳' => '↳', + '↵' => '↵', + '↶' => '↶', + '↷' => '↷', + '↺' => '↺', + '↻' => '↻', + '↼' => '↼', + '↽' => '↽', + '↾' => '↾', + '↿' => '↿', + '⇀' => '⇀', + '⇁' => '⇁', + '⇂' => '⇂', + '⇃' => '⇃', + '⇄' => '⇄', + '⇅' => '⇅', + '⇆' => '⇆', + '⇇' => '⇇', + '⇈' => '⇈', + '⇉' => '⇉', + '⇊' => '⇊', + '⇋' => '⇋', + '⇌' => '⇌', + '⇍' => '⇍', + '⇎' => '⇎', + '⇏' => '⇏', + '⇐' => '⇐', + '⇑' => '⇑', + '⇒' => '⇒', + '⇓' => '⇓', + '⇔' => '⇔', + '⇕' => '⇕', + '⇖' => '⇖', + '⇗' => '⇗', + '⇘' => '⇘', + '⇙' => '⇙', + '⇚' => '⇚', + '⇛' => '⇛', + '⇝' => '⇝', + '⇤' => '⇤', + '⇥' => '⇥', + '⇵' => '⇵', + '⇽' => '⇽', + '⇾' => '⇾', + '⇿' => '⇿', + '∀' => '∀', + '∁' => '∁', + '∂' => '∂', + '∂̸' => '&npart', + '∃' => '∃', + '∄' => '∄', + '∅' => '∅', + '∇' => '∇', + '∈' => '∈', + '∉' => '∉', + '∋' => '∋', + '∌' => '∌', + '∏' => '∏', + '∐' => '∐', + '∑' => '∑', + '−' => '−', + '∓' => '∓', + '∔' => '∔', + '∖' => '∖', + '∗' => '∗', + '∘' => '∘', + '√' => '√', + '∝' => '∝', + '∞' => '∞', + '∟' => '∟', + '∠' => '∠', + '∠⃒' => '&nang', + '∡' => '∡', + '∢' => '∢', + '∣' => '∣', + '∤' => '∤', + '∥' => '∥', + '∦' => '∦', + '∧' => '∧', + '∨' => '∨', + '∩' => '∩', + '∩︀' => '&caps', + '∪' => '∪', + '∪︀' => '&cups', + '∫' => '∫', + '∬' => '∬', + '∭' => '∭', + '∮' => '∮', + '∯' => '∯', + '∰' => '∰', + '∱' => '∱', + '∲' => '∲', + '∳' => '∳', + '∴' => '∴', + '∵' => '∵', + '∶' => '∶', + '∷' => '∷', + '∸' => '∸', + '∺' => '∺', + '∻' => '∻', + '∼' => '∼', + '∼⃒' => '&nvsim', + '∽' => '∽', + '∽̱' => '&race', + '∾' => '∾', + '∾̳' => '&acE', + '∿' => '∿', + '≀' => '≀', + '≁' => '≁', + '≂' => '≂', + '≂̸' => '&nesim', + '≃' => '≃', + '≄' => '≄', + '≅' => '≅', + '≆' => '≆', + '≇' => '≇', + '≈' => '≈', + '≉' => '≉', + '≊' => '≊', + '≋' => '≋', + '≋̸' => '&napid', + '≌' => '≌', + '≍' => '≍', + '≍⃒' => '&nvap', + '≎' => '≎', + '≎̸' => '&nbump', + '≏' => '≏', + '≏̸' => '&nbumpe', + '≐' => '≐', + '≐̸' => '&nedot', + '≑' => '≑', + '≒' => '≒', + '≓' => '≓', + '≔' => '≔', + '≕' => '≕', + '≖' => '≖', + '≗' => '≗', + '≙' => '≙', + '≚' => '≚', + '≜' => '≜', + '≟' => '≟', + '≠' => '≠', + '≡' => '≡', + '≡⃥' => '&bnequiv', + '≢' => '≢', + '≤' => '≤', + '≤⃒' => '&nvle', + '≥' => '≥', + '≥⃒' => '&nvge', + '≦' => '≦', + '≦̸' => '&nlE', + '≧' => '≧', + '≧̸' => '&NotGreaterFullEqual', + '≨' => '≨', + '≨︀' => '&lvertneqq', + '≩' => '≩', + '≩︀' => '&gvertneqq', + '≪' => '≪', + '≪̸' => '&nLtv', + '≪⃒' => '&nLt', + '≫' => '≫', + '≫̸' => '&NotGreaterGreater', + '≫⃒' => '&nGt', + '≬' => '≬', + '≭' => '≭', + '≮' => '≮', + '≯' => '≯', + '≰' => '≰', + '≱' => '≱', + '≲' => '≲', + '≳' => '≳', + '≴' => '≴', + '≵' => '≵', + '≶' => '≶', + '≷' => '≷', + '≸' => '≸', + '≹' => '≹', + '≺' => '≺', + '≻' => '≻', + '≼' => '≼', + '≽' => '≽', + '≾' => '≾', + '≿' => '≿', + '≿̸' => '&NotSucceedsTilde', + '⊀' => '⊀', + '⊁' => '⊁', + '⊂' => '⊂', + '⊂⃒' => '&vnsub', + '⊃' => '⊃', + '⊃⃒' => '&nsupset', + '⊄' => '⊄', + '⊅' => '⊅', + '⊆' => '⊆', + '⊇' => '⊇', + '⊈' => '⊈', + '⊉' => '⊉', + '⊊' => '⊊', + '⊊︀' => '&vsubne', + '⊋' => '⊋', + '⊋︀' => '&vsupne', + '⊍' => '⊍', + '⊎' => '⊎', + '⊏' => '⊏', + '⊏̸' => '&NotSquareSubset', + '⊐' => '⊐', + '⊐̸' => '&NotSquareSuperset', + '⊑' => '⊑', + '⊒' => '⊒', + '⊓' => '⊓', + '⊓︀' => '&sqcaps', + '⊔' => '⊔', + '⊔︀' => '&sqcups', + '⊕' => '⊕', + '⊖' => '⊖', + '⊗' => '⊗', + '⊘' => '⊘', + '⊙' => '⊙', + '⊚' => '⊚', + '⊛' => '⊛', + '⊝' => '⊝', + '⊞' => '⊞', + '⊟' => '⊟', + '⊠' => '⊠', + '⊡' => '⊡', + '⊢' => '⊢', + '⊣' => '⊣', + '⊤' => '⊤', + '⊥' => '⊥', + '⊧' => '⊧', + '⊨' => '⊨', + '⊩' => '⊩', + '⊪' => '⊪', + '⊫' => '⊫', + '⊬' => '⊬', + '⊭' => '⊭', + '⊮' => '⊮', + '⊯' => '⊯', + '⊰' => '⊰', + '⊲' => '⊲', + '⊳' => '⊳', + '⊴' => '⊴', + '⊴⃒' => '&nvltrie', + '⊵' => '⊵', + '⊵⃒' => '&nvrtrie', + '⊶' => '⊶', + '⊷' => '⊷', + '⊸' => '⊸', + '⊹' => '⊹', + '⊺' => '⊺', + '⊻' => '⊻', + '⊽' => '⊽', + '⊾' => '⊾', + '⊿' => '⊿', + '⋀' => '⋀', + '⋁' => '⋁', + '⋂' => '⋂', + '⋃' => '⋃', + '⋄' => '⋄', + '⋅' => '⋅', + '⋆' => '⋆', + '⋇' => '⋇', + '⋈' => '⋈', + '⋉' => '⋉', + '⋊' => '⋊', + '⋋' => '⋋', + '⋌' => '⋌', + '⋍' => '⋍', + '⋎' => '⋎', + '⋏' => '⋏', + '⋐' => '⋐', + '⋑' => '⋑', + '⋒' => '⋒', + '⋓' => '⋓', + '⋔' => '⋔', + '⋕' => '⋕', + '⋖' => '⋖', + '⋗' => '⋗', + '⋘' => '⋘', + '⋘̸' => '&nLl', + '⋙' => '⋙', + '⋙̸' => '&nGg', + '⋚' => '⋚', + '⋚︀' => '&lesg', + '⋛' => '⋛', + '⋛︀' => '&gesl', + '⋞' => '⋞', + '⋟' => '⋟', + '⋠' => '⋠', + '⋡' => '⋡', + '⋢' => '⋢', + '⋣' => '⋣', + '⋦' => '⋦', + '⋧' => '⋧', + '⋨' => '⋨', + '⋩' => '⋩', + '⋪' => '⋪', + '⋫' => '⋫', + '⋬' => '⋬', + '⋭' => '⋭', + '⋮' => '⋮', + '⋯' => '⋯', + '⋰' => '⋰', + '⋱' => '⋱', + '⋲' => '⋲', + '⋳' => '⋳', + '⋴' => '⋴', + '⋵' => '⋵', + '⋵̸' => '¬indot', + '⋶' => '⋶', + '⋷' => '⋷', + '⋹' => '⋹', + '⋹̸' => '¬inE', + '⋺' => '⋺', + '⋻' => '⋻', + '⋼' => '⋼', + '⋽' => '⋽', + '⋾' => '⋾', + '⌅' => '⌅', + '⌆' => '⌆', + '⌈' => '⌈', + '⌉' => '⌉', + '⌊' => '⌊', + '⌋' => '⌋', + '⌌' => '⌌', + '⌍' => '⌍', + '⌎' => '⌎', + '⌏' => '⌏', + '⌐' => '⌐', + '⌒' => '⌒', + '⌓' => '⌓', + '⌕' => '⌕', + '⌖' => '⌖', + '⌜' => '⌜', + '⌝' => '⌝', + '⌞' => '⌞', + '⌟' => '⌟', + '⌢' => '⌢', + '⌣' => '⌣', + '⌭' => '⌭', + '⌮' => '⌮', + '⌶' => '⌶', + '⌽' => '⌽', + '⌿' => '⌿', + '⍼' => '⍼', + '⎰' => '⎰', + '⎱' => '⎱', + '⎴' => '⎴', + '⎵' => '⎵', + '⎶' => '⎶', + '⏜' => '⏜', + '⏝' => '⏝', + '⏞' => '⏞', + '⏟' => '⏟', + '⏢' => '⏢', + '⏧' => '⏧', + '␣' => '␣', + 'Ⓢ' => 'Ⓢ', + '─' => '─', + '│' => '│', + '┌' => '┌', + '┐' => '┐', + '└' => '└', + '┘' => '┘', + '├' => '├', + '┤' => '┤', + '┬' => '┬', + '┴' => '┴', + '┼' => '┼', + '═' => '═', + '║' => '║', + '╒' => '╒', + '╓' => '╓', + '╔' => '╔', + '╕' => '╕', + '╖' => '╖', + '╗' => '╗', + '╘' => '╘', + '╙' => '╙', + '╚' => '╚', + '╛' => '╛', + '╜' => '╜', + '╝' => '╝', + '╞' => '╞', + '╟' => '╟', + '╠' => '╠', + '╡' => '╡', + '╢' => '╢', + '╣' => '╣', + '╤' => '╤', + '╥' => '╥', + '╦' => '╦', + '╧' => '╧', + '╨' => '╨', + '╩' => '╩', + '╪' => '╪', + '╫' => '╫', + '╬' => '╬', + '▀' => '▀', + '▄' => '▄', + '█' => '█', + '░' => '░', + '▒' => '▒', + '▓' => '▓', + '□' => '□', + '▪' => '▪', + '▫' => '▫', + '▭' => '▭', + '▮' => '▮', + '▱' => '▱', + '△' => '△', + '▴' => '▴', + '▵' => '▵', + '▸' => '▸', + '▹' => '▹', + '▽' => '▽', + '▾' => '▾', + '▿' => '▿', + '◂' => '◂', + '◃' => '◃', + '◊' => '◊', + '○' => '○', + '◬' => '◬', + '◯' => '◯', + '◸' => '◸', + '◹' => '◹', + '◺' => '◺', + '◻' => '◻', + '◼' => '◼', + '★' => '★', + '☆' => '☆', + '☎' => '☎', + '♀' => '♀', + '♂' => '♂', + '♠' => '♠', + '♣' => '♣', + '♥' => '♥', + '♦' => '♦', + '♪' => '♪', + '♭' => '♭', + '♮' => '♮', + '♯' => '♯', + '✓' => '✓', + '✗' => '✗', + '✠' => '✠', + '✶' => '✶', + '❘' => '❘', + '❲' => '❲', + '❳' => '❳', + '⟈' => '⟈', + '⟉' => '⟉', + '⟦' => '⟦', + '⟧' => '⟧', + '⟨' => '⟨', + '⟩' => '⟩', + '⟪' => '⟪', + '⟫' => '⟫', + '⟬' => '⟬', + '⟭' => '⟭', + '⟵' => '⟵', + '⟶' => '⟶', + '⟷' => '⟷', + '⟸' => '⟸', + '⟹' => '⟹', + '⟺' => '⟺', + '⟼' => '⟼', + '⟿' => '⟿', + '⤂' => '⤂', + '⤃' => '⤃', + '⤄' => '⤄', + '⤅' => '⤅', + '⤌' => '⤌', + '⤍' => '⤍', + '⤎' => '⤎', + '⤏' => '⤏', + '⤐' => '⤐', + '⤑' => '⤑', + '⤒' => '⤒', + '⤓' => '⤓', + '⤖' => '⤖', + '⤙' => '⤙', + '⤚' => '⤚', + '⤛' => '⤛', + '⤜' => '⤜', + '⤝' => '⤝', + '⤞' => '⤞', + '⤟' => '⤟', + '⤠' => '⤠', + '⤣' => '⤣', + '⤤' => '⤤', + '⤥' => '⤥', + '⤦' => '⤦', + '⤧' => '⤧', + '⤨' => '⤨', + '⤩' => '⤩', + '⤪' => '⤪', + '⤳' => '⤳', + '⤳̸' => '&nrarrc', + '⤵' => '⤵', + '⤶' => '⤶', + '⤷' => '⤷', + '⤸' => '⤸', + '⤹' => '⤹', + '⤼' => '⤼', + '⤽' => '⤽', + '⥅' => '⥅', + '⥈' => '⥈', + '⥉' => '⥉', + '⥊' => '⥊', + '⥋' => '⥋', + '⥎' => '⥎', + '⥏' => '⥏', + '⥐' => '⥐', + '⥑' => '⥑', + '⥒' => '⥒', + '⥓' => '⥓', + '⥔' => '⥔', + '⥕' => '⥕', + '⥖' => '⥖', + '⥗' => '⥗', + '⥘' => '⥘', + '⥙' => '⥙', + '⥚' => '⥚', + '⥛' => '⥛', + '⥜' => '⥜', + '⥝' => '⥝', + '⥞' => '⥞', + '⥟' => '⥟', + '⥠' => '⥠', + '⥡' => '⥡', + '⥢' => '⥢', + '⥣' => '⥣', + '⥤' => '⥤', + '⥥' => '⥥', + '⥦' => '⥦', + '⥧' => '⥧', + '⥨' => '⥨', + '⥩' => '⥩', + '⥪' => '⥪', + '⥫' => '⥫', + '⥬' => '⥬', + '⥭' => '⥭', + '⥮' => '⥮', + '⥯' => '⥯', + '⥰' => '⥰', + '⥱' => '⥱', + '⥲' => '⥲', + '⥳' => '⥳', + '⥴' => '⥴', + '⥵' => '⥵', + '⥶' => '⥶', + '⥸' => '⥸', + '⥹' => '⥹', + '⥻' => '⥻', + '⥼' => '⥼', + '⥽' => '⥽', + '⥾' => '⥾', + '⥿' => '⥿', + '⦅' => '⦅', + '⦆' => '⦆', + '⦋' => '⦋', + '⦌' => '⦌', + '⦍' => '⦍', + '⦎' => '⦎', + '⦏' => '⦏', + '⦐' => '⦐', + '⦑' => '⦑', + '⦒' => '⦒', + '⦓' => '⦓', + '⦔' => '⦔', + '⦕' => '⦕', + '⦖' => '⦖', + '⦚' => '⦚', + '⦜' => '⦜', + '⦝' => '⦝', + '⦤' => '⦤', + '⦥' => '⦥', + '⦦' => '⦦', + '⦧' => '⦧', + '⦨' => '⦨', + '⦩' => '⦩', + '⦪' => '⦪', + '⦫' => '⦫', + '⦬' => '⦬', + '⦭' => '⦭', + '⦮' => '⦮', + '⦯' => '⦯', + '⦰' => '⦰', + '⦱' => '⦱', + '⦲' => '⦲', + '⦳' => '⦳', + '⦴' => '⦴', + '⦵' => '⦵', + '⦶' => '⦶', + '⦷' => '⦷', + '⦹' => '⦹', + '⦻' => '⦻', + '⦼' => '⦼', + '⦾' => '⦾', + '⦿' => '⦿', + '⧀' => '⧀', + '⧁' => '⧁', + '⧂' => '⧂', + '⧃' => '⧃', + '⧄' => '⧄', + '⧅' => '⧅', + '⧉' => '⧉', + '⧍' => '⧍', + '⧎' => '⧎', + '⧏' => '⧏', + '⧏̸' => '&NotLeftTriangleBar', + '⧐' => '⧐', + '⧐̸' => '&NotRightTriangleBar', + '⧜' => '⧜', + '⧝' => '⧝', + '⧞' => '⧞', + '⧣' => '⧣', + '⧤' => '⧤', + '⧥' => '⧥', + '⧫' => '⧫', + '⧴' => '⧴', + '⧶' => '⧶', + '⨀' => '⨀', + '⨁' => '⨁', + '⨂' => '⨂', + '⨄' => '⨄', + '⨆' => '⨆', + '⨌' => '⨌', + '⨍' => '⨍', + '⨐' => '⨐', + '⨑' => '⨑', + '⨒' => '⨒', + '⨓' => '⨓', + '⨔' => '⨔', + '⨕' => '⨕', + '⨖' => '⨖', + '⨗' => '⨗', + '⨢' => '⨢', + '⨣' => '⨣', + '⨤' => '⨤', + '⨥' => '⨥', + '⨦' => '⨦', + '⨧' => '⨧', + '⨩' => '⨩', + '⨪' => '⨪', + '⨭' => '⨭', + '⨮' => '⨮', + '⨯' => '⨯', + '⨰' => '⨰', + '⨱' => '⨱', + '⨳' => '⨳', + '⨴' => '⨴', + '⨵' => '⨵', + '⨶' => '⨶', + '⨷' => '⨷', + '⨸' => '⨸', + '⨹' => '⨹', + '⨺' => '⨺', + '⨻' => '⨻', + '⨼' => '⨼', + '⨿' => '⨿', + '⩀' => '⩀', + '⩂' => '⩂', + '⩃' => '⩃', + '⩄' => '⩄', + '⩅' => '⩅', + '⩆' => '⩆', + '⩇' => '⩇', + '⩈' => '⩈', + '⩉' => '⩉', + '⩊' => '⩊', + '⩋' => '⩋', + '⩌' => '⩌', + '⩍' => '⩍', + '⩐' => '⩐', + '⩓' => '⩓', + '⩔' => '⩔', + '⩕' => '⩕', + '⩖' => '⩖', + '⩗' => '⩗', + '⩘' => '⩘', + '⩚' => '⩚', + '⩛' => '⩛', + '⩜' => '⩜', + '⩝' => '⩝', + '⩟' => '⩟', + '⩦' => '⩦', + '⩪' => '⩪', + '⩭' => '⩭', + '⩭̸' => '&ncongdot', + '⩮' => '⩮', + '⩯' => '⩯', + '⩰' => '⩰', + '⩰̸' => '&napE', + '⩱' => '⩱', + '⩲' => '⩲', + '⩳' => '⩳', + '⩴' => '⩴', + '⩵' => '⩵', + '⩷' => '⩷', + '⩸' => '⩸', + '⩹' => '⩹', + '⩺' => '⩺', + '⩻' => '⩻', + '⩼' => '⩼', + '⩽' => '⩽', + '⩽̸' => '&nles', + '⩾' => '⩾', + '⩾̸' => '&nges', + '⩿' => '⩿', + '⪀' => '⪀', + '⪁' => '⪁', + '⪂' => '⪂', + '⪃' => '⪃', + '⪄' => '⪄', + '⪅' => '⪅', + '⪆' => '⪆', + '⪇' => '⪇', + '⪈' => '⪈', + '⪉' => '⪉', + '⪊' => '⪊', + '⪋' => '⪋', + '⪌' => '⪌', + '⪍' => '⪍', + '⪎' => '⪎', + '⪏' => '⪏', + '⪐' => '⪐', + '⪑' => '⪑', + '⪒' => '⪒', + '⪓' => '⪓', + '⪔' => '⪔', + '⪕' => '⪕', + '⪖' => '⪖', + '⪗' => '⪗', + '⪘' => '⪘', + '⪙' => '⪙', + '⪚' => '⪚', + '⪝' => '⪝', + '⪞' => '⪞', + '⪟' => '⪟', + '⪠' => '⪠', + '⪡' => '⪡', + '⪡̸' => '&NotNestedLessLess', + '⪢' => '⪢', + '⪢̸' => '&NotNestedGreaterGreater', + '⪤' => '⪤', + '⪥' => '⪥', + '⪦' => '⪦', + '⪧' => '⪧', + '⪨' => '⪨', + '⪩' => '⪩', + '⪪' => '⪪', + '⪫' => '⪫', + '⪬' => '⪬', + '⪬︀' => '&smtes', + '⪭' => '⪭', + '⪭︀' => '&lates', + '⪮' => '⪮', + '⪯' => '⪯', + '⪯̸' => '&NotPrecedesEqual', + '⪰' => '⪰', + '⪰̸' => '&NotSucceedsEqual', + '⪳' => '⪳', + '⪴' => '⪴', + '⪵' => '⪵', + '⪶' => '⪶', + '⪷' => '⪷', + '⪸' => '⪸', + '⪹' => '⪹', + '⪺' => '⪺', + '⪻' => '⪻', + '⪼' => '⪼', + '⪽' => '⪽', + '⪾' => '⪾', + '⪿' => '⪿', + '⫀' => '⫀', + '⫁' => '⫁', + '⫂' => '⫂', + '⫃' => '⫃', + '⫄' => '⫄', + '⫅' => '⫅', + '⫅̸' => '&nsubE', + '⫆' => '⫆', + '⫆̸' => '&nsupseteqq', + '⫇' => '⫇', + '⫈' => '⫈', + '⫋' => '⫋', + '⫋︀' => '&vsubnE', + '⫌' => '⫌', + '⫌︀' => '&varsupsetneqq', + '⫏' => '⫏', + '⫐' => '⫐', + '⫑' => '⫑', + '⫒' => '⫒', + '⫓' => '⫓', + '⫔' => '⫔', + '⫕' => '⫕', + '⫖' => '⫖', + '⫗' => '⫗', + '⫘' => '⫘', + '⫙' => '⫙', + '⫚' => '⫚', + '⫛' => '⫛', + '⫤' => '⫤', + '⫦' => '⫦', + '⫧' => '⫧', + '⫨' => '⫨', + '⫩' => '⫩', + '⫫' => '⫫', + '⫬' => '⫬', + '⫭' => '⫭', + '⫮' => '⫮', + '⫯' => '⫯', + '⫰' => '⫰', + '⫱' => '⫱', + '⫲' => '⫲', + '⫳' => '⫳', + '⫽︀' => '&varsupsetneqq', + 'ff' => 'ff', + 'fi' => 'fi', + 'fl' => 'fl', + 'ffi' => 'ffi', + 'ffl' => 'ffl', + '𝒜' => '𝒜', + '𝒞' => '𝒞', + '𝒟' => '𝒟', + '𝒢' => '𝒢', + '𝒥' => '𝒥', + '𝒦' => '𝒦', + '𝒩' => '𝒩', + '𝒪' => '𝒪', + '𝒫' => '𝒫', + '𝒬' => '𝒬', + '𝒮' => '𝒮', + '𝒯' => '𝒯', + '𝒰' => '𝒰', + '𝒱' => '𝒱', + '𝒲' => '𝒲', + '𝒳' => '𝒳', + '𝒴' => '𝒴', + '𝒵' => '𝒵', + '𝒶' => '𝒶', + '𝒷' => '𝒷', + '𝒸' => '𝒸', + '𝒹' => '𝒹', + '𝒻' => '𝒻', + '𝒽' => '𝒽', + '𝒾' => '𝒾', + '𝒿' => '𝒿', + '𝓀' => '𝓀', + '𝓁' => '𝓁', + '𝓂' => '𝓂', + '𝓃' => '𝓃', + '𝓅' => '𝓅', + '𝓆' => '𝓆', + '𝓇' => '𝓇', + '𝓈' => '𝓈', + '𝓉' => '𝓉', + '𝓊' => '𝓊', + '𝓋' => '𝓋', + '𝓌' => '𝓌', + '𝓍' => '𝓍', + '𝓎' => '𝓎', + '𝓏' => '𝓏', + '𝔄' => '𝔄', + '𝔅' => '𝔅', + '𝔇' => '𝔇', + '𝔈' => '𝔈', + '𝔉' => '𝔉', + '𝔊' => '𝔊', + '𝔍' => '𝔍', + '𝔎' => '𝔎', + '𝔏' => '𝔏', + '𝔐' => '𝔐', + '𝔑' => '𝔑', + '𝔒' => '𝔒', + '𝔓' => '𝔓', + '𝔔' => '𝔔', + '𝔖' => '𝔖', + '𝔗' => '𝔗', + '𝔘' => '𝔘', + '𝔙' => '𝔙', + '𝔚' => '𝔚', + '𝔛' => '𝔛', + '𝔜' => '𝔜', + '𝔞' => '𝔞', + '𝔟' => '𝔟', + '𝔠' => '𝔠', + '𝔡' => '𝔡', + '𝔢' => '𝔢', + '𝔣' => '𝔣', + '𝔤' => '𝔤', + '𝔥' => '𝔥', + '𝔦' => '𝔦', + '𝔧' => '𝔧', + '𝔨' => '𝔨', + '𝔩' => '𝔩', + '𝔪' => '𝔪', + '𝔫' => '𝔫', + '𝔬' => '𝔬', + '𝔭' => '𝔭', + '𝔮' => '𝔮', + '𝔯' => '𝔯', + '𝔰' => '𝔰', + '𝔱' => '𝔱', + '𝔲' => '𝔲', + '𝔳' => '𝔳', + '𝔴' => '𝔴', + '𝔵' => '𝔵', + '𝔶' => '𝔶', + '𝔷' => '𝔷', + '𝔸' => '𝔸', + '𝔹' => '𝔹', + '𝔻' => '𝔻', + '𝔼' => '𝔼', + '𝔽' => '𝔽', + '𝔾' => '𝔾', + '𝕀' => '𝕀', + '𝕁' => '𝕁', + '𝕂' => '𝕂', + '𝕃' => '𝕃', + '𝕄' => '𝕄', + '𝕆' => '𝕆', + '𝕊' => '𝕊', + '𝕋' => '𝕋', + '𝕌' => '𝕌', + '𝕍' => '𝕍', + '𝕎' => '𝕎', + '𝕏' => '𝕏', + '𝕐' => '𝕐', + '𝕒' => '𝕒', + '𝕓' => '𝕓', + '𝕔' => '𝕔', + '𝕕' => '𝕕', + '𝕖' => '𝕖', + '𝕗' => '𝕗', + '𝕘' => '𝕘', + '𝕙' => '𝕙', + '𝕚' => '𝕚', + '𝕛' => '𝕛', + '𝕜' => '𝕜', + '𝕝' => '𝕝', + '𝕞' => '𝕞', + '𝕟' => '𝕟', + '𝕠' => '𝕠', + '𝕡' => '𝕡', + '𝕢' => '𝕢', + '𝕣' => '𝕣', + '𝕤' => '𝕤', + '𝕥' => '𝕥', + '𝕦' => '𝕦', + '𝕧' => '𝕧', + '𝕨' => '𝕨', + '𝕩' => '𝕩', + '𝕪' => '𝕪', + '𝕫' => '𝕫', + ); +} diff --git a/3rdparty/masterminds/html5/src/HTML5/Serializer/OutputRules.php b/3rdparty/masterminds/html5/src/HTML5/Serializer/OutputRules.php new file mode 100644 index 00000000..ec467f22 --- /dev/null +++ b/3rdparty/masterminds/html5/src/HTML5/Serializer/OutputRules.php @@ -0,0 +1,553 @@ +'http://www.w3.org/1999/xhtml', + 'attrNamespace'=>'http://www.w3.org/1999/xhtml', + + 'nodeName'=>'img', 'nodeName'=>array('img', 'a'), + 'attrName'=>'alt', 'attrName'=>array('title', 'alt'), + ), + */ + array( + 'nodeNamespace' => 'http://www.w3.org/1999/xhtml', + 'attrName' => array('href', + 'hreflang', + 'http-equiv', + 'icon', + 'id', + 'keytype', + 'kind', + 'label', + 'lang', + 'language', + 'list', + 'maxlength', + 'media', + 'method', + 'name', + 'placeholder', + 'rel', + 'rows', + 'rowspan', + 'sandbox', + 'spellcheck', + 'scope', + 'seamless', + 'shape', + 'size', + 'sizes', + 'span', + 'src', + 'srcdoc', + 'srclang', + 'srcset', + 'start', + 'step', + 'style', + 'summary', + 'tabindex', + 'target', + 'title', + 'type', + 'value', + 'width', + 'border', + 'charset', + 'cite', + 'class', + 'code', + 'codebase', + 'color', + 'cols', + 'colspan', + 'content', + 'coords', + 'data', + 'datetime', + 'default', + 'dir', + 'dirname', + 'enctype', + 'for', + 'form', + 'formaction', + 'headers', + 'height', + 'accept', + 'accept-charset', + 'accesskey', + 'action', + 'align', + 'alt', + 'bgcolor', + ), + ), + array( + 'nodeNamespace' => 'http://www.w3.org/1999/xhtml', + 'xpath' => 'starts-with(local-name(), \'data-\')', + ), + ); + + const DOCTYPE = ''; + + public function __construct($output, $options = array()) + { + if (isset($options['encode_entities'])) { + $this->encode = $options['encode_entities']; + } + + $this->outputMode = static::IM_IN_HTML; + $this->out = $output; + $this->hasHTML5 = defined('ENT_HTML5'); + } + + public function addRule(array $rule) + { + $this->nonBooleanAttributes[] = $rule; + } + + public function setTraverser(Traverser $traverser) + { + $this->traverser = $traverser; + + return $this; + } + + public function unsetTraverser() + { + $this->traverser = null; + + return $this; + } + + public function document($dom) + { + $this->doctype(); + if ($dom->documentElement) { + foreach ($dom->childNodes as $node) { + $this->traverser->node($node); + } + $this->nl(); + } + } + + protected function doctype() + { + $this->wr(static::DOCTYPE); + $this->nl(); + } + + public function element($ele) + { + $name = $ele->tagName; + + // Per spec: + // If the element has a declared namespace in the HTML, MathML or + // SVG namespaces, we use the lname instead of the tagName. + if ($this->traverser->isLocalElement($ele)) { + $name = $ele->localName; + } + + // If we are in SVG or MathML there is special handling. + // Using if/elseif instead of switch because it's faster in PHP. + if ('svg' == $name) { + $this->outputMode = static::IM_IN_SVG; + $name = Elements::normalizeSvgElement($name); + } elseif ('math' == $name) { + $this->outputMode = static::IM_IN_MATHML; + } + + $this->openTag($ele); + if (Elements::isA($name, Elements::TEXT_RAW)) { + foreach ($ele->childNodes as $child) { + if ($child instanceof \DOMCharacterData) { + $this->wr($child->data); + } elseif ($child instanceof \DOMElement) { + $this->element($child); + } + } + } else { + // Handle children. + if ($ele->hasChildNodes()) { + $this->traverser->children($ele->childNodes); + } + + // Close out the SVG or MathML special handling. + if ('svg' == $name || 'math' == $name) { + $this->outputMode = static::IM_IN_HTML; + } + } + + // If not unary, add a closing tag. + if (!Elements::isA($name, Elements::VOID_TAG)) { + $this->closeTag($ele); + } + } + + /** + * Write a text node. + * + * @param \DOMText $ele The text node to write. + */ + public function text($ele) + { + if (isset($ele->parentNode) && isset($ele->parentNode->tagName) && Elements::isA($ele->parentNode->localName, Elements::TEXT_RAW)) { + $this->wr($ele->data); + + return; + } + + // FIXME: This probably needs some flags set. + $this->wr($this->enc($ele->data)); + } + + public function cdata($ele) + { + // This encodes CDATA. + $this->wr($ele->ownerDocument->saveXML($ele)); + } + + public function comment($ele) + { + // These produce identical output. + // $this->wr(''); + $this->wr($ele->ownerDocument->saveXML($ele)); + } + + public function processorInstruction($ele) + { + $this->wr('wr($ele->target) + ->wr(' ') + ->wr($ele->data) + ->wr('?>'); + } + + /** + * Write the namespace attributes. + * + * @param \DOMNode $ele The element being written. + */ + protected function namespaceAttrs($ele) + { + if (!$this->xpath || $this->xpath->document !== $ele->ownerDocument) { + $this->xpath = new \DOMXPath($ele->ownerDocument); + } + + foreach ($this->xpath->query('namespace::*[not(.=../../namespace::*)]', $ele) as $nsNode) { + if (!in_array($nsNode->nodeValue, $this->implicitNamespaces)) { + $this->wr(' ')->wr($nsNode->nodeName)->wr('="')->wr($nsNode->nodeValue)->wr('"'); + } + } + } + + /** + * Write the opening tag. + * + * Tags for HTML, MathML, and SVG are in the local name. Otherwise, use the + * qualified name (8.3). + * + * @param \DOMNode $ele The element being written. + */ + protected function openTag($ele) + { + $this->wr('<')->wr($this->traverser->isLocalElement($ele) ? $ele->localName : $ele->tagName); + + $this->attrs($ele); + $this->namespaceAttrs($ele); + + if ($this->outputMode == static::IM_IN_HTML) { + $this->wr('>'); + } // If we are not in html mode we are in SVG, MathML, or XML embedded content. + else { + if ($ele->hasChildNodes()) { + $this->wr('>'); + } // If there are no children this is self closing. + else { + $this->wr(' />'); + } + } + } + + protected function attrs($ele) + { + // FIXME: Needs support for xml, xmlns, xlink, and namespaced elements. + if (!$ele->hasAttributes()) { + return $this; + } + + // TODO: Currently, this always writes name="value", and does not do + // value-less attributes. + $map = $ele->attributes; + $len = $map->length; + for ($i = 0; $i < $len; ++$i) { + $node = $map->item($i); + $val = $this->enc($node->value, true); + + // XXX: The spec says that we need to ensure that anything in + // the XML, XMLNS, or XLink NS's should use the canonical + // prefix. It seems that DOM does this for us already, but there + // may be exceptions. + $name = $node->nodeName; + + // Special handling for attributes in SVG and MathML. + // Using if/elseif instead of switch because it's faster in PHP. + if ($this->outputMode == static::IM_IN_SVG) { + $name = Elements::normalizeSvgAttribute($name); + } elseif ($this->outputMode == static::IM_IN_MATHML) { + $name = Elements::normalizeMathMlAttribute($name); + } + + $this->wr(' ')->wr($name); + + if ((isset($val) && '' !== $val) || $this->nonBooleanAttribute($node)) { + $this->wr('="')->wr($val)->wr('"'); + } + } + } + + protected function nonBooleanAttribute(\DOMAttr $attr) + { + $ele = $attr->ownerElement; + foreach ($this->nonBooleanAttributes as $rule) { + if (isset($rule['nodeNamespace']) && $rule['nodeNamespace'] !== $ele->namespaceURI) { + continue; + } + if (isset($rule['attNamespace']) && $rule['attNamespace'] !== $attr->namespaceURI) { + continue; + } + if (isset($rule['nodeName']) && !is_array($rule['nodeName']) && $rule['nodeName'] !== $ele->localName) { + continue; + } + if (isset($rule['nodeName']) && is_array($rule['nodeName']) && !in_array($ele->localName, $rule['nodeName'], true)) { + continue; + } + if (isset($rule['attrName']) && !is_array($rule['attrName']) && $rule['attrName'] !== $attr->localName) { + continue; + } + if (isset($rule['attrName']) && is_array($rule['attrName']) && !in_array($attr->localName, $rule['attrName'], true)) { + continue; + } + if (isset($rule['xpath'])) { + $xp = $this->getXPath($attr); + if (isset($rule['prefixes'])) { + foreach ($rule['prefixes'] as $nsPrefix => $ns) { + $xp->registerNamespace($nsPrefix, $ns); + } + } + if (!$xp->evaluate($rule['xpath'], $attr)) { + continue; + } + } + + return true; + } + + return false; + } + + private function getXPath(\DOMNode $node) + { + if (!$this->xpath) { + $this->xpath = new \DOMXPath($node->ownerDocument); + } + + return $this->xpath; + } + + /** + * Write the closing tag. + * + * Tags for HTML, MathML, and SVG are in the local name. Otherwise, use the + * qualified name (8.3). + * + * @param \DOMNode $ele The element being written. + */ + protected function closeTag($ele) + { + if ($this->outputMode == static::IM_IN_HTML || $ele->hasChildNodes()) { + $this->wr('wr($this->traverser->isLocalElement($ele) ? $ele->localName : $ele->tagName)->wr('>'); + } + } + + /** + * Write to the output. + * + * @param string $text The string to put into the output + * + * @return $this + */ + protected function wr($text) + { + fwrite($this->out, $text); + + return $this; + } + + /** + * Write a new line character. + * + * @return $this + */ + protected function nl() + { + fwrite($this->out, PHP_EOL); + + return $this; + } + + /** + * Encode text. + * + * When encode is set to false, the default value, the text passed in is + * escaped per section 8.3 of the html5 spec. For details on how text is + * escaped see the escape() method. + * + * When encoding is set to true the text is converted to named character + * references where appropriate. Section 8.1.4 Character references of the + * html5 spec refers to using named character references. This is useful for + * characters that can't otherwise legally be used in the text. + * + * The named character references are listed in section 8.5. + * + * @see http://www.w3.org/TR/2013/CR-html5-20130806/syntax.html#named-character-references True encoding will turn all named character references into their entities. + * This includes such characters as +.# and many other common ones. By default + * encoding here will just escape &'<>". + * + * Note, PHP 5.4+ has better html5 encoding. + * + * @todo Use the Entities class in php 5.3 to have html5 entities. + * + * @param string $text Text to encode. + * @param bool $attribute True if we are encoding an attrubute, false otherwise. + * + * @return string The encoded text. + */ + protected function enc($text, $attribute = false) + { + // Escape the text rather than convert to named character references. + if (!$this->encode) { + return $this->escape($text, $attribute); + } + + // If we are in PHP 5.4+ we can use the native html5 entity functionality to + // convert the named character references. + + if ($this->hasHTML5) { + return htmlentities($text, ENT_HTML5 | ENT_SUBSTITUTE | ENT_QUOTES, 'UTF-8', false); + } // If a version earlier than 5.4 html5 entities are not entirely handled. + // This manually handles them. + else { + return strtr($text, HTML5Entities::$map); + } + } + + /** + * Escape test. + * + * According to the html5 spec section 8.3 Serializing HTML fragments, text + * within tags that are not style, script, xmp, iframe, noembed, and noframes + * need to be properly escaped. + * + * The & should be converted to &, no breaking space unicode characters + * converted to  , when in attribute mode the " should be converted to + * ", and when not in attribute mode the < and > should be converted to + * < and >. + * + * @see http://www.w3.org/TR/2013/CR-html5-20130806/syntax.html#escapingString + * + * @param string $text Text to escape. + * @param bool $attribute True if we are escaping an attrubute, false otherwise. + */ + protected function escape($text, $attribute = false) + { + // Not using htmlspecialchars because, while it does escaping, it doesn't + // match the requirements of section 8.5. For example, it doesn't handle + // non-breaking spaces. + if ($attribute) { + $replace = array( + '"' => '"', + '&' => '&', + "\xc2\xa0" => ' ', + ); + } else { + $replace = array( + '<' => '<', + '>' => '>', + '&' => '&', + "\xc2\xa0" => ' ', + ); + } + + return strtr($text, $replace); + } +} diff --git a/3rdparty/masterminds/html5/src/HTML5/Serializer/RulesInterface.php b/3rdparty/masterminds/html5/src/HTML5/Serializer/RulesInterface.php new file mode 100644 index 00000000..69a6ecda --- /dev/null +++ b/3rdparty/masterminds/html5/src/HTML5/Serializer/RulesInterface.php @@ -0,0 +1,99 @@ + 'html', + 'http://www.w3.org/1998/Math/MathML' => 'math', + 'http://www.w3.org/2000/svg' => 'svg', + ); + + protected $dom; + + protected $options; + + protected $encode = false; + + protected $rules; + + protected $out; + + /** + * Create a traverser. + * + * @param \DOMNode|\DOMNodeList $dom The document or node to traverse. + * @param resource $out A stream that allows writing. The traverser will output into this + * stream. + * @param array $options An array of options for the traverser as key/value pairs. These include: + * - encode_entities: A bool to specify if full encding should happen for all named + * charachter references. Defaults to false which escapes &'<>". + * - output_rules: The path to the class handling the output rules. + */ + public function __construct($dom, $out, RulesInterface $rules, $options = array()) + { + $this->dom = $dom; + $this->out = $out; + $this->rules = $rules; + $this->options = $options; + + $this->rules->setTraverser($this); + } + + /** + * Tell the traverser to walk the DOM. + * + * @return resource $out Returns the output stream. + */ + public function walk() + { + if ($this->dom instanceof \DOMDocument) { + $this->rules->document($this->dom); + } elseif ($this->dom instanceof \DOMDocumentFragment) { + // Document fragments are a special case. Only the children need to + // be serialized. + if ($this->dom->hasChildNodes()) { + $this->children($this->dom->childNodes); + } + } // If NodeList, loop + elseif ($this->dom instanceof \DOMNodeList) { + // If this is a NodeList of DOMDocuments this will not work. + $this->children($this->dom); + } // Else assume this is a DOMNode-like datastructure. + else { + $this->node($this->dom); + } + + return $this->out; + } + + /** + * Process a node in the DOM. + * + * @param mixed $node A node implementing \DOMNode. + */ + public function node($node) + { + // A listing of types is at http://php.net/manual/en/dom.constants.php + switch ($node->nodeType) { + case XML_ELEMENT_NODE: + $this->rules->element($node); + break; + case XML_TEXT_NODE: + $this->rules->text($node); + break; + case XML_CDATA_SECTION_NODE: + $this->rules->cdata($node); + break; + case XML_PI_NODE: + $this->rules->processorInstruction($node); + break; + case XML_COMMENT_NODE: + $this->rules->comment($node); + break; + // Currently we don't support embedding DTDs. + default: + //print ''; + break; + } + } + + /** + * Walk through all the nodes on a node list. + * + * @param \DOMNodeList $nl A list of child elements to walk through. + */ + public function children($nl) + { + foreach ($nl as $node) { + $this->node($node); + } + } + + /** + * Is an element local? + * + * @param mixed $ele An element that implement \DOMNode. + * + * @return bool true if local and false otherwise. + */ + public function isLocalElement($ele) + { + $uri = $ele->namespaceURI; + if (empty($uri)) { + return false; + } + + return isset(static::$local_ns[$uri]); + } +} diff --git a/3rdparty/mexitek/phpcolors/LICENSE b/3rdparty/mexitek/phpcolors/LICENSE new file mode 100644 index 00000000..f764bbc2 --- /dev/null +++ b/3rdparty/mexitek/phpcolors/LICENSE @@ -0,0 +1,21 @@ +MIT License + +Copyright (c) 2020 Arlo Carreon, http://arlocarreon.com + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. diff --git a/3rdparty/mexitek/phpcolors/src/Mexitek/PHPColors/Color.php b/3rdparty/mexitek/phpcolors/src/Mexitek/PHPColors/Color.php new file mode 100644 index 00000000..282e9926 --- /dev/null +++ b/3rdparty/mexitek/phpcolors/src/Mexitek/PHPColors/Color.php @@ -0,0 +1,801 @@ + + * Info: http://mexitek.github.io/phpColors/ + * License: http://arlo.mit-license.org/ + */ + +namespace Mexitek\PHPColors; + +use Exception; + +/** + * A color utility that helps manipulate HEX colors + */ +class Color +{ + /** + * @var string + */ + private $_hex; + + /** + * @var array + */ + private $_hsl; + + /** + * @var array + */ + private $_rgb; + + /** + * Auto darkens/lightens by 10% for sexily-subtle gradients. + * Set this to FALSE to adjust automatic shade to be between given color + * and black (for darken) or white (for lighten) + */ + public const DEFAULT_ADJUST = 10; + + /** + * Instantiates the class with a HEX value + * @param string $hex + * @throws Exception + */ + public function __construct(string $hex) + { + $color = self::sanitizeHex($hex); + $this->_hex = $color; + $this->_hsl = self::hexToHsl($color); + $this->_rgb = self::hexToRgb($color); + } + + /** + * Given a HEX string returns a HSL array equivalent. + * @param string $color + * @return array HSL associative array + * @throws Exception + */ + public static function hexToHsl(string $color): array + { + // Sanity check + $color = self::sanitizeHex($color); + + // Convert HEX to DEC + $R = hexdec($color[0] . $color[1]); + $G = hexdec($color[2] . $color[3]); + $B = hexdec($color[4] . $color[5]); + + $HSL = array(); + + $var_R = ($R / 255); + $var_G = ($G / 255); + $var_B = ($B / 255); + + $var_Min = min($var_R, $var_G, $var_B); + $var_Max = max($var_R, $var_G, $var_B); + $del_Max = $var_Max - $var_Min; + + $L = ($var_Max + $var_Min) / 2; + + if ($del_Max == 0) { + $H = 0; + $S = 0; + } else { + if ($L < 0.5) { + $S = $del_Max / ($var_Max + $var_Min); + } else { + $S = $del_Max / (2 - $var_Max - $var_Min); + } + + $del_R = ((($var_Max - $var_R) / 6) + ($del_Max / 2)) / $del_Max; + $del_G = ((($var_Max - $var_G) / 6) + ($del_Max / 2)) / $del_Max; + $del_B = ((($var_Max - $var_B) / 6) + ($del_Max / 2)) / $del_Max; + + if ($var_R == $var_Max) { + $H = $del_B - $del_G; + } elseif ($var_G == $var_Max) { + $H = (1 / 3) + $del_R - $del_B; + } elseif ($var_B == $var_Max) { + $H = (2 / 3) + $del_G - $del_R; + } + + if ($H < 0) { + $H++; + } + if ($H > 1) { + $H--; + } + } + + $HSL['H'] = ($H * 360); + $HSL['S'] = $S; + $HSL['L'] = $L; + + return $HSL; + } + + /** + * Given a HSL associative array returns the equivalent HEX string + * @param array $hsl + * @return string HEX string + * @throws Exception "Bad HSL Array" + */ + public static function hslToHex(array $hsl = array()): string + { + // Make sure it's HSL + if (empty($hsl) || !isset($hsl["H"], $hsl["S"], $hsl["L"])) { + throw new Exception("Param was not an HSL array"); + } + + list($H, $S, $L) = array($hsl['H'] / 360, $hsl['S'], $hsl['L']); + + if ($S == 0) { + $r = $L * 255; + $g = $L * 255; + $b = $L * 255; + } else { + if ($L < 0.5) { + $var_2 = $L * (1 + $S); + } else { + $var_2 = ($L + $S) - ($S * $L); + } + + $var_1 = 2 * $L - $var_2; + + $r = 255 * self::hueToRgb($var_1, $var_2, $H + (1 / 3)); + $g = 255 * self::hueToRgb($var_1, $var_2, $H); + $b = 255 * self::hueToRgb($var_1, $var_2, $H - (1 / 3)); + } + + // Convert to hex + $r = dechex(round($r)); + $g = dechex(round($g)); + $b = dechex(round($b)); + + // Make sure we get 2 digits for decimals + $r = (strlen("" . $r) === 1) ? "0" . $r : $r; + $g = (strlen("" . $g) === 1) ? "0" . $g : $g; + $b = (strlen("" . $b) === 1) ? "0" . $b : $b; + + return $r . $g . $b; + } + + + /** + * Given a HEX string returns a RGB array equivalent. + * @param string $color + * @return array RGB associative array + * @throws Exception + */ + public static function hexToRgb(string $color): array + { + // Sanity check + $color = self::sanitizeHex($color); + + // Convert HEX to DEC + $R = hexdec($color[0] . $color[1]); + $G = hexdec($color[2] . $color[3]); + $B = hexdec($color[4] . $color[5]); + + $RGB['R'] = $R; + $RGB['G'] = $G; + $RGB['B'] = $B; + + return $RGB; + } + + + /** + * Given an RGB associative array returns the equivalent HEX string + * @param array $rgb + * @return string Hex string + * @throws Exception "Bad RGB Array" + */ + public static function rgbToHex(array $rgb = array()): string + { + // Make sure it's RGB + if (empty($rgb) || !isset($rgb["R"], $rgb["G"], $rgb["B"])) { + throw new Exception("Param was not an RGB array"); + } + + // https://github.com/mexitek/phpColors/issues/25#issuecomment-88354815 + // Convert RGB to HEX + $hex[0] = str_pad(dechex((int)$rgb['R']), 2, '0', STR_PAD_LEFT); + $hex[1] = str_pad(dechex((int)$rgb['G']), 2, '0', STR_PAD_LEFT); + $hex[2] = str_pad(dechex((int)$rgb['B']), 2, '0', STR_PAD_LEFT); + + // Make sure that 2 digits are allocated to each color. + $hex[0] = (strlen($hex[0]) === 1) ? '0' . $hex[0] : $hex[0]; + $hex[1] = (strlen($hex[1]) === 1) ? '0' . $hex[1] : $hex[1]; + $hex[2] = (strlen($hex[2]) === 1) ? '0' . $hex[2] : $hex[2]; + + return implode('', $hex); + } + + /** + * Given an RGB associative array, returns CSS string output. + * @param array $rgb + * @return string rgb(r,g,b) string + * @throws Exception "Bad RGB Array" + */ + public static function rgbToString(array $rgb = array()): string + { + // Make sure it's RGB + if (empty($rgb) || !isset($rgb["R"], $rgb["G"], $rgb["B"])) { + throw new Exception("Param was not an RGB array"); + } + + return 'rgb(' . + $rgb['R'] . ', ' . + $rgb['G'] . ', ' . + $rgb['B'] . ')'; + } + + /** + * Given a standard color name, return hex code + * + * @param string $color_name + * @return string + */ + public static function nameToHex(string $color_name): string + { + $colors = array( + 'aliceblue' => 'F0F8FF', + 'antiquewhite' => 'FAEBD7', + 'aqua' => '00FFFF', + 'aquamarine' => '7FFFD4', + 'azure' => 'F0FFFF', + 'beige' => 'F5F5DC', + 'bisque' => 'FFE4C4', + 'black' => '000000', + 'blanchedalmond' => 'FFEBCD', + 'blue' => '0000FF', + 'blueviolet' => '8A2BE2', + 'brown' => 'A52A2A', + 'burlywood' => 'DEB887', + 'cadetblue' => '5F9EA0', + 'chartreuse' => '7FFF00', + 'chocolate' => 'D2691E', + 'coral' => 'FF7F50', + 'cornflowerblue' => '6495ED', + 'cornsilk' => 'FFF8DC', + 'crimson' => 'DC143C', + 'cyan' => '00FFFF', + 'darkblue' => '00008B', + 'darkcyan' => '008B8B', + 'darkgoldenrod' => 'B8860B', + 'darkgray' => 'A9A9A9', + 'darkgreen' => '006400', + 'darkgrey' => 'A9A9A9', + 'darkkhaki' => 'BDB76B', + 'darkmagenta' => '8B008B', + 'darkolivegreen' => '556B2F', + 'darkorange' => 'FF8C00', + 'darkorchid' => '9932CC', + 'darkred' => '8B0000', + 'darksalmon' => 'E9967A', + 'darkseagreen' => '8FBC8F', + 'darkslateblue' => '483D8B', + 'darkslategray' => '2F4F4F', + 'darkslategrey' => '2F4F4F', + 'darkturquoise' => '00CED1', + 'darkviolet' => '9400D3', + 'deeppink' => 'FF1493', + 'deepskyblue' => '00BFFF', + 'dimgray' => '696969', + 'dimgrey' => '696969', + 'dodgerblue' => '1E90FF', + 'firebrick' => 'B22222', + 'floralwhite' => 'FFFAF0', + 'forestgreen' => '228B22', + 'fuchsia' => 'FF00FF', + 'gainsboro' => 'DCDCDC', + 'ghostwhite' => 'F8F8FF', + 'gold' => 'FFD700', + 'goldenrod' => 'DAA520', + 'gray' => '808080', + 'green' => '008000', + 'greenyellow' => 'ADFF2F', + 'grey' => '808080', + 'honeydew' => 'F0FFF0', + 'hotpink' => 'FF69B4', + 'indianred' => 'CD5C5C', + 'indigo' => '4B0082', + 'ivory' => 'FFFFF0', + 'khaki' => 'F0E68C', + 'lavender' => 'E6E6FA', + 'lavenderblush' => 'FFF0F5', + 'lawngreen' => '7CFC00', + 'lemonchiffon' => 'FFFACD', + 'lightblue' => 'ADD8E6', + 'lightcoral' => 'F08080', + 'lightcyan' => 'E0FFFF', + 'lightgoldenrodyellow' => 'FAFAD2', + 'lightgray' => 'D3D3D3', + 'lightgreen' => '90EE90', + 'lightgrey' => 'D3D3D3', + 'lightpink' => 'FFB6C1', + 'lightsalmon' => 'FFA07A', + 'lightseagreen' => '20B2AA', + 'lightskyblue' => '87CEFA', + 'lightslategray' => '778899', + 'lightslategrey' => '778899', + 'lightsteelblue' => 'B0C4DE', + 'lightyellow' => 'FFFFE0', + 'lime' => '00FF00', + 'limegreen' => '32CD32', + 'linen' => 'FAF0E6', + 'magenta' => 'FF00FF', + 'maroon' => '800000', + 'mediumaquamarine' => '66CDAA', + 'mediumblue' => '0000CD', + 'mediumorchid' => 'BA55D3', + 'mediumpurple' => '9370D0', + 'mediumseagreen' => '3CB371', + 'mediumslateblue' => '7B68EE', + 'mediumspringgreen' => '00FA9A', + 'mediumturquoise' => '48D1CC', + 'mediumvioletred' => 'C71585', + 'midnightblue' => '191970', + 'mintcream' => 'F5FFFA', + 'mistyrose' => 'FFE4E1', + 'moccasin' => 'FFE4B5', + 'navajowhite' => 'FFDEAD', + 'navy' => '000080', + 'oldlace' => 'FDF5E6', + 'olive' => '808000', + 'olivedrab' => '6B8E23', + 'orange' => 'FFA500', + 'orangered' => 'FF4500', + 'orchid' => 'DA70D6', + 'palegoldenrod' => 'EEE8AA', + 'palegreen' => '98FB98', + 'paleturquoise' => 'AFEEEE', + 'palevioletred' => 'DB7093', + 'papayawhip' => 'FFEFD5', + 'peachpuff' => 'FFDAB9', + 'peru' => 'CD853F', + 'pink' => 'FFC0CB', + 'plum' => 'DDA0DD', + 'powderblue' => 'B0E0E6', + 'purple' => '800080', + 'red' => 'FF0000', + 'rosybrown' => 'BC8F8F', + 'royalblue' => '4169E1', + 'saddlebrown' => '8B4513', + 'salmon' => 'FA8072', + 'sandybrown' => 'F4A460', + 'seagreen' => '2E8B57', + 'seashell' => 'FFF5EE', + 'sienna' => 'A0522D', + 'silver' => 'C0C0C0', + 'skyblue' => '87CEEB', + 'slateblue' => '6A5ACD', + 'slategray' => '708090', + 'slategrey' => '708090', + 'snow' => 'FFFAFA', + 'springgreen' => '00FF7F', + 'steelblue' => '4682B4', + 'tan' => 'D2B48C', + 'teal' => '008080', + 'thistle' => 'D8BFD8', + 'tomato' => 'FF6347', + 'turquoise' => '40E0D0', + 'violet' => 'EE82EE', + 'wheat' => 'F5DEB3', + 'white' => 'FFFFFF', + 'whitesmoke' => 'F5F5F5', + 'yellow' => 'FFFF00', + 'yellowgreen' => '9ACD32' + ); + + $color_name = strtolower($color_name); + if (isset($colors[$color_name])) { + return '#' . $colors[$color_name]; + } + + return $color_name; + } + + /** + * Given a HEX value, returns a darker color. If no desired amount provided, then the color halfway between + * given HEX and black will be returned. + * @param int $amount + * @return string Darker HEX value + * @throws Exception + */ + public function darken(int $amount = self::DEFAULT_ADJUST): string + { + // Darken + $darkerHSL = $this->darkenHsl($this->_hsl, $amount); + // Return as HEX + return self::hslToHex($darkerHSL); + } + + /** + * Given a HEX value, returns a lighter color. If no desired amount provided, then the color halfway between + * given HEX and white will be returned. + * @param int $amount + * @return string Lighter HEX value + * @throws Exception + */ + public function lighten(int $amount = self::DEFAULT_ADJUST): string + { + // Lighten + $lighterHSL = $this->lightenHsl($this->_hsl, $amount); + // Return as HEX + return self::hslToHex($lighterHSL); + } + + /** + * Given a HEX value, returns a mixed color. If no desired amount provided, then the color mixed by this ratio + * @param string $hex2 Secondary HEX value to mix with + * @param int $amount = -100..0..+100 + * @return string mixed HEX value + * @throws Exception + */ + public function mix(string $hex2, int $amount = 0): string + { + $rgb2 = self::hexToRgb($hex2); + $mixed = $this->mixRgb($this->_rgb, $rgb2, $amount); + // Return as HEX + return self::rgbToHex($mixed); + } + + /** + * Creates an array with two shades that can be used to make a gradient + * @param int $amount Optional percentage amount you want your contrast color + * @return array An array with a 'light' and 'dark' index + * @throws Exception + */ + public function makeGradient(int $amount = self::DEFAULT_ADJUST): array + { + // Decide which color needs to be made + if ($this->isLight()) { + $lightColor = $this->_hex; + $darkColor = $this->darken($amount); + } else { + $lightColor = $this->lighten($amount); + $darkColor = $this->_hex; + } + + // Return our gradient array + return array("light" => $lightColor, "dark" => $darkColor); + } + + + /** + * Returns whether or not given color is considered "light" + * @param string|bool $color + * @param int $lighterThan + * @return boolean + */ + public function isLight($color = false, int $lighterThan = 130): bool + { + // Get our color + $color = ($color) ? $color : $this->_hex; + + // Calculate straight from rbg + $r = hexdec($color[0] . $color[1]); + $g = hexdec($color[2] . $color[3]); + $b = hexdec($color[4] . $color[5]); + + return (($r * 299 + $g * 587 + $b * 114) / 1000 > $lighterThan); + } + + /** + * Returns whether or not a given color is considered "dark" + * @param string|bool $color + * @param int $darkerThan + * @return boolean + */ + public function isDark($color = false, int $darkerThan = 130): bool + { + // Get our color + $color = ($color) ? $color : $this->_hex; + + // Calculate straight from rbg + $r = hexdec($color[0] . $color[1]); + $g = hexdec($color[2] . $color[3]); + $b = hexdec($color[4] . $color[5]); + + return (($r * 299 + $g * 587 + $b * 114) / 1000 <= $darkerThan); + } + + /** + * Returns the complimentary color + * @return string Complementary hex color + * @throws Exception + */ + public function complementary(): string + { + // Get our HSL + $hsl = $this->_hsl; + + // Adjust Hue 180 degrees + $hsl['H'] += ($hsl['H'] > 180) ? -180 : 180; + + // Return the new value in HEX + return self::hslToHex($hsl); + } + + /** + * Returns the HSL array of your color + */ + public function getHsl(): array + { + return $this->_hsl; + } + + /** + * Returns your original color + */ + public function getHex(): string + { + return $this->_hex; + } + + /** + * Returns the RGB array of your color + */ + public function getRgb(): array + { + return $this->_rgb; + } + + /** + * Returns the cross browser CSS3 gradient + * @param int $amount Optional: percentage amount to light/darken the gradient + * @param boolean $vintageBrowsers Optional: include vendor prefixes for browsers that almost died out already + * @param string $prefix Optional: prefix for every lines + * @param string $suffix Optional: suffix for every lines + * @return string CSS3 gradient for chrome, safari, firefox, opera and IE10 + * @throws Exception + * @link http://caniuse.com/css-gradients Resource for the browser support + */ + public function getCssGradient($amount = self::DEFAULT_ADJUST, $vintageBrowsers = false, $suffix = "", $prefix = ""): string + { + // Get the recommended gradient + $g = $this->makeGradient($amount); + + $css = ""; + /* fallback/image non-cover color */ + $css .= "{$prefix}background-color: #" . $this->_hex . ";{$suffix}"; + + /* IE Browsers */ + $css .= "{$prefix}filter: progid:DXImageTransform.Microsoft.gradient(startColorstr='#" . $g['light'] . "', endColorstr='#" . $g['dark'] . "');{$suffix}"; + + /* Safari 4+, Chrome 1-9 */ + if ($vintageBrowsers) { + $css .= "{$prefix}background-image: -webkit-gradient(linear, 0% 0%, 0% 100%, from(#" . $g['light'] . "), to(#" . $g['dark'] . "));{$suffix}"; + } + + /* Safari 5.1+, Mobile Safari, Chrome 10+ */ + $css .= "{$prefix}background-image: -webkit-linear-gradient(top, #" . $g['light'] . ", #" . $g['dark'] . ");{$suffix}"; + + if ($vintageBrowsers) { + /* Firefox 3.6+ */ + $css .= "{$prefix}background-image: -moz-linear-gradient(top, #" . $g['light'] . ", #" . $g['dark'] . ");{$suffix}"; + + /* Opera 11.10+ */ + $css .= "{$prefix}background-image: -o-linear-gradient(top, #" . $g['light'] . ", #" . $g['dark'] . ");{$suffix}"; + } + + /* Unprefixed version (standards): FF 16+, IE10+, Chrome 26+, Safari 7+, Opera 12.1+ */ + $css .= "{$prefix}background-image: linear-gradient(to bottom, #" . $g['light'] . ", #" . $g['dark'] . ");{$suffix}"; + + // Return our CSS + return $css; + } + + /** + * Darkens a given HSL array + * @param array $hsl + * @param int $amount + * @return array $hsl + */ + private function darkenHsl(array $hsl, int $amount = self::DEFAULT_ADJUST): array + { + // Check if we were provided a number + if ($amount) { + $hsl['L'] = ($hsl['L'] * 100) - $amount; + $hsl['L'] = ($hsl['L'] < 0) ? 0 : $hsl['L'] / 100; + } else { + // We need to find out how much to darken + $hsl['L'] /= 2; + } + + return $hsl; + } + + /** + * Lightens a given HSL array + * @param array $hsl + * @param int $amount + * @return array + */ + private function lightenHsl(array $hsl, int $amount = self::DEFAULT_ADJUST): array + { + // Check if we were provided a number + if ($amount) { + $hsl['L'] = ($hsl['L'] * 100) + $amount; + $hsl['L'] = ($hsl['L'] > 100) ? 1 : $hsl['L'] / 100; + } else { + // We need to find out how much to lighten + $hsl['L'] += (1 - $hsl['L']) / 2; + } + + return $hsl; + } + + /** + * Mix two RGB colors and return the resulting RGB color + * ported from http://phpxref.pagelines.com/nav.html?includes/class.colors.php.source.html + * @param array $rgb1 + * @param array $rgb2 + * @param int $amount ranged -100..0..+100 + * @return array + */ + private function mixRgb(array $rgb1, array $rgb2, int $amount = 0): array + { + $r1 = ($amount + 100) / 100; + $r2 = 2 - $r1; + + $rmix = (($rgb1['R'] * $r1) + ($rgb2['R'] * $r2)) / 2; + $gmix = (($rgb1['G'] * $r1) + ($rgb2['G'] * $r2)) / 2; + $bmix = (($rgb1['B'] * $r1) + ($rgb2['B'] * $r2)) / 2; + + return array('R' => $rmix, 'G' => $gmix, 'B' => $bmix); + } + + /** + * Given a Hue, returns corresponding RGB value + * @param float $v1 + * @param float $v2 + * @param float $vH + * @return float + */ + private static function hueToRgb(float $v1, float $v2, float $vH): float + { + if ($vH < 0) { + ++$vH; + } + + if ($vH > 1) { + --$vH; + } + + if ((6 * $vH) < 1) { + return ($v1 + ($v2 - $v1) * 6 * $vH); + } + + if ((2 * $vH) < 1) { + return $v2; + } + + if ((3 * $vH) < 2) { + return ($v1 + ($v2 - $v1) * ((2 / 3) - $vH) * 6); + } + + return $v1; + } + + /** + * Checks the HEX string for correct formatting and converts short format to long + * @param string $hex + * @return string + * @throws Exception + */ + private static function sanitizeHex(string $hex): string + { + // Strip # sign if it is present + $color = str_replace("#", "", $hex); + + // Validate hex string + if (!preg_match('/^[a-fA-F0-9]+$/', $color)) { + throw new Exception("HEX color does not match format"); + } + + // Make sure it's 6 digits + if (strlen($color) === 3) { + $color = $color[0] . $color[0] . $color[1] . $color[1] . $color[2] . $color[2]; + } elseif (strlen($color) !== 6) { + throw new Exception("HEX color needs to be 6 or 3 digits long"); + } + + return $color; + } + + /** + * Converts object into its string representation + * @return string + */ + public function __toString() + { + return "#" . $this->getHex(); + } + + /** + * @param string $name + * @return mixed|null + */ + public function __get(string $name) + { + switch (strtolower($name)) { + case 'red': + case 'r': + return $this->_rgb["R"]; + case 'green': + case 'g': + return $this->_rgb["G"]; + case 'blue': + case 'b': + return $this->_rgb["B"]; + case 'hue': + case 'h': + return $this->_hsl["H"]; + case 'saturation': + case 's': + return $this->_hsl["S"]; + case 'lightness': + case 'l': + return $this->_hsl["L"]; + } + + $trace = debug_backtrace(); + trigger_error( + 'Undefined property via __get(): ' . $name . ' in ' . $trace[0]['file'] . ' on line ' . $trace[0]['line'], + E_USER_NOTICE + ); + return null; + } + + /** + * @param string $name + * @param mixed $value + * @throws Exception + */ + public function __set(string $name, $value) + { + switch (strtolower($name)) { + case 'red': + case 'r': + $this->_rgb["R"] = $value; + $this->_hex = self::rgbToHex($this->_rgb); + $this->_hsl = self::hexToHsl($this->_hex); + break; + case 'green': + case 'g': + $this->_rgb["G"] = $value; + $this->_hex = self::rgbToHex($this->_rgb); + $this->_hsl = self::hexToHsl($this->_hex); + break; + case 'blue': + case 'b': + $this->_rgb["B"] = $value; + $this->_hex = self::rgbToHex($this->_rgb); + $this->_hsl = self::hexToHsl($this->_hex); + break; + case 'hue': + case 'h': + $this->_hsl["H"] = $value; + $this->_hex = self::hslToHex($this->_hsl); + $this->_rgb = self::hexToRgb($this->_hex); + break; + case 'saturation': + case 's': + $this->_hsl["S"] = $value; + $this->_hex = self::hslToHex($this->_hsl); + $this->_rgb = self::hexToRgb($this->_hex); + break; + case 'lightness': + case 'light': + case 'l': + $this->_hsl["L"] = $value; + $this->_hex = self::hslToHex($this->_hsl); + $this->_rgb = self::hexToRgb($this->_hex); + break; + } + } +} diff --git a/3rdparty/microsoft/azure-storage-blob/BreakingChanges.md b/3rdparty/microsoft/azure-storage-blob/BreakingChanges.md new file mode 100644 index 00000000..0bb7f8c5 --- /dev/null +++ b/3rdparty/microsoft/azure-storage-blob/BreakingChanges.md @@ -0,0 +1,5 @@ +Tracking Breaking changes in 1.0.0 + +* Removed `dataSerializer` parameter from `BlobRextProxy` constructor. +* Option parameter type of `BlobRestProxy::CreateBlockBlob` and `BlobRestProxy::CreatePageBlobFromContent` changed and added `setUseTransactionalMD5` method. +* Deprecated PHP 5.5 support. \ No newline at end of file diff --git a/3rdparty/microsoft/azure-storage-blob/CONTRIBUTING.md b/3rdparty/microsoft/azure-storage-blob/CONTRIBUTING.md new file mode 100644 index 00000000..3dac3d63 --- /dev/null +++ b/3rdparty/microsoft/azure-storage-blob/CONTRIBUTING.md @@ -0,0 +1 @@ +This [repository](https://github.com/azure/azure-storage-blob-php) is currently used for releasing only, please go to [azure-storage-php](https://github.com/azure/azure-storage-php) for submitting issues or contribution. \ No newline at end of file diff --git a/3rdparty/microsoft/azure-storage-blob/ChangeLog.md b/3rdparty/microsoft/azure-storage-blob/ChangeLog.md new file mode 100644 index 00000000..9ac67da4 --- /dev/null +++ b/3rdparty/microsoft/azure-storage-blob/ChangeLog.md @@ -0,0 +1,58 @@ +2022.08 - version 1.5.4 +* Check `$copyProgress` is not null before using it in `strpos`. + +2021.09 - version 1.5.3 +* Upgraded dependency for `azure-storage-common` to version 1.5.2. +* Resolved some interface inconsistency between `IBlob`/`BlobRestProxy`. +* Imported `Psr\Http\Message\StreamInterface` in `IBlob`. + +2020.12 - version 1.5.2 +* Resolved an issue where access condition does not work for large block blob uploads. +* Guzzle version is now updated to support both 6.x and 7.x. + +2020.08 - version 1.5.1 +* Lower case query parameter names. + +2020.01 - version 1.5.0 + +* Added support to include deleted in blob list. +* Added support to undelete a blob. +* Fixed the issue in SAS token where special characters were not correctly encoded. +* Samples no longer uses ‘BlobRestProxy’ directly, instead, ‘ServicesBuilder’ is used. + +2019.04 - version 1.4.0 + +* Added support for OAuth authentication. +* Resolved some issues on Linux platform. + +2019.03 - version 1.3.0 + +* Fixed a bug where blob name '0' cannot be created. +* Documentation refinement. +* `ListContainer` now can have ETag more robustly fetched from response header. + +2018.08 - version 1.2.0 + +* Updated Azure Storage API version from 2016-05-31 to 2017-04-17. +* Added method `setBlobTier` method in `BlobRestProxy` to set blob tiers. +* Added support setting or getting blob tiers related properties when creating blobs, listing blobs, getting blob properties and copying blobs. +* Set the `getBlobUrl()` method in `BlobRestProxy` visibility to public. + +2018.04 - version 1.1.0 + +* Private method BlobRestProxy::getBlobUrl now preserves primary URI path when exists. +* MD files are modified for better readability and formatting. +* CACERT can now be set when creating RestProxies using `$options` parameter. +* Added a sample in `BlobSamples.php` to list all blobs with certain prefix. This is a recommended implementation of using continuation token to list all the blobs. +* Removed unnecessary trailing spaces. +* Assertions are re-factored in test cases. +* Now the test framework uses `PHPUnit\Framework\TestCase` instead of `PHPUnit_Framework_TestCase`. + +2018.01 - version 1.0.0 + +* Created `BlobSharedAccessSignatureHelper` and moved method `SharedAccessSignatureHelper::generateBlobServiceSharedAccessSignatureToken()` into `BlobSharedAccessSignatureHelper`. +* Added static builder methods `createBlobService` and `createContainerAnonymousAccess` into `BlobRestProxy`. +* Removed `dataSerializer` parameter from `BlobRestProxy` constructor. +* Added `setUseTransactionalMD5` method for options of `BlobRestProxy::CreateBlockBlob` and `BlobRestProxy::CreatePageBlobFromContent`. Default false, enabling transactional MD5 validation will take more cpu and memory resources. +* Fixed a bug that CopyBlobFromURLOptions not found. +* Deprecated PHP 5.5 support. diff --git a/3rdparty/microsoft/azure-storage-blob/LICENSE b/3rdparty/microsoft/azure-storage-blob/LICENSE new file mode 100644 index 00000000..79288605 --- /dev/null +++ b/3rdparty/microsoft/azure-storage-blob/LICENSE @@ -0,0 +1,21 @@ +The MIT License (MIT) + +Copyright (c) 2016 Microsoft Corporation + +Permission is hereby granted, free of charge, to any person obtaining a copy + of this software and associated documentation files (the "Software"), to deal + in the Software without restriction, including without limitation the rights + to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + copies of the Software, and to permit persons to whom the Software is + furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in + all copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN + THE SOFTWARE. \ No newline at end of file diff --git a/3rdparty/microsoft/azure-storage-blob/src/Blob/BlobRestProxy.php b/3rdparty/microsoft/azure-storage-blob/src/Blob/BlobRestProxy.php new file mode 100644 index 00000000..ca7a6c59 --- /dev/null +++ b/3rdparty/microsoft/azure-storage-blob/src/Blob/BlobRestProxy.php @@ -0,0 +1,4788 @@ + + * @copyright 2016 Microsoft Corporation + * @license https://github.com/azure/azure-storage-php/LICENSE + * @link https://github.com/azure/azure-storage-php + */ + +namespace MicrosoftAzure\Storage\Blob; + +use GuzzleHttp\Psr7; +use MicrosoftAzure\Storage\Blob\Internal\IBlob; +use MicrosoftAzure\Storage\Blob\Internal\BlobResources as Resources; +use MicrosoftAzure\Storage\Blob\Models\AppendBlockOptions; +use MicrosoftAzure\Storage\Blob\Models\AppendBlockResult; +use MicrosoftAzure\Storage\Blob\Models\BlobServiceOptions; +use MicrosoftAzure\Storage\Blob\Models\BlobType; +use MicrosoftAzure\Storage\Blob\Models\Block; +use MicrosoftAzure\Storage\Blob\Models\BlockList; +use MicrosoftAzure\Storage\Blob\Models\BreakLeaseResult; +use MicrosoftAzure\Storage\Blob\Models\CommitBlobBlocksOptions; +use MicrosoftAzure\Storage\Blob\Models\CopyBlobFromURLOptions; +use MicrosoftAzure\Storage\Blob\Models\CopyBlobOptions; +use MicrosoftAzure\Storage\Blob\Models\CopyBlobResult; +use MicrosoftAzure\Storage\Blob\Models\CreateBlobBlockOptions; +use MicrosoftAzure\Storage\Blob\Models\CreateBlobOptions; +use MicrosoftAzure\Storage\Blob\Models\CreateBlobPagesOptions; +use MicrosoftAzure\Storage\Blob\Models\CreateBlobPagesResult; +use MicrosoftAzure\Storage\Blob\Models\CreateBlobSnapshotOptions; +use MicrosoftAzure\Storage\Blob\Models\CreateBlobSnapshotResult; +use MicrosoftAzure\Storage\Blob\Models\CreateContainerOptions; +use MicrosoftAzure\Storage\Blob\Models\CreatePageBlobOptions; +use MicrosoftAzure\Storage\Blob\Models\UndeleteBlobOptions; +use MicrosoftAzure\Storage\Blob\Models\DeleteBlobOptions; +use MicrosoftAzure\Storage\Blob\Models\GetBlobMetadataOptions; +use MicrosoftAzure\Storage\Blob\Models\GetBlobMetadataResult; +use MicrosoftAzure\Storage\Blob\Models\GetBlobOptions; +use MicrosoftAzure\Storage\Blob\Models\GetBlobPropertiesOptions; +use MicrosoftAzure\Storage\Blob\Models\GetBlobPropertiesResult; +use MicrosoftAzure\Storage\Blob\Models\GetBlobResult; +use MicrosoftAzure\Storage\Blob\Models\GetContainerACLResult; +use MicrosoftAzure\Storage\Blob\Models\GetContainerPropertiesResult; +use MicrosoftAzure\Storage\Blob\Models\LeaseMode; +use MicrosoftAzure\Storage\Blob\Models\LeaseResult; +use MicrosoftAzure\Storage\Blob\Models\ListBlobBlocksOptions; +use MicrosoftAzure\Storage\Blob\Models\ListBlobBlocksResult; +use MicrosoftAzure\Storage\Blob\Models\ListBlobsOptions; +use MicrosoftAzure\Storage\Blob\Models\ListBlobsResult; +use MicrosoftAzure\Storage\Blob\Models\ListContainersOptions; +use MicrosoftAzure\Storage\Blob\Models\ListContainersResult; +use MicrosoftAzure\Storage\Blob\Models\ListPageBlobRangesDiffResult; +use MicrosoftAzure\Storage\Blob\Models\ListPageBlobRangesOptions; +use MicrosoftAzure\Storage\Blob\Models\ListPageBlobRangesResult; +use MicrosoftAzure\Storage\Blob\Models\PageWriteOption; +use MicrosoftAzure\Storage\Blob\Models\PutBlobResult; +use MicrosoftAzure\Storage\Blob\Models\PutBlockResult; +use MicrosoftAzure\Storage\Blob\Models\SetBlobMetadataResult; +use MicrosoftAzure\Storage\Blob\Models\SetBlobPropertiesOptions; +use MicrosoftAzure\Storage\Blob\Models\SetBlobPropertiesResult; +use MicrosoftAzure\Storage\Blob\Models\SetBlobTierOptions; +use MicrosoftAzure\Storage\Common\Internal\Authentication\SharedAccessSignatureAuthScheme; +use MicrosoftAzure\Storage\Common\Internal\Authentication\SharedKeyAuthScheme; +use MicrosoftAzure\Storage\Common\Internal\Authentication\TokenAuthScheme; +use MicrosoftAzure\Storage\Common\Internal\Http\HttpFormatter; +use MicrosoftAzure\Storage\Common\Internal\Middlewares\CommonRequestMiddleware; +use MicrosoftAzure\Storage\Common\Internal\Serialization\XmlSerializer; +use MicrosoftAzure\Storage\Common\Internal\ServiceRestProxy; +use MicrosoftAzure\Storage\Common\Internal\ServiceRestTrait; +use MicrosoftAzure\Storage\Common\Internal\StorageServiceSettings; +use MicrosoftAzure\Storage\Common\Internal\Utilities; +use MicrosoftAzure\Storage\Common\Internal\Validate; +use MicrosoftAzure\Storage\Common\LocationMode; +use MicrosoftAzure\Storage\Common\Models\Range; +use MicrosoftAzure\Storage\Common\SharedAccessSignatureHelper; +use Psr\Http\Message\StreamInterface; +use GuzzleHttp\Psr7\Utils; + +/** + * This class constructs HTTP requests and receive HTTP responses for blob + * service layer. + * + * @category Microsoft + * @package MicrosoftAzure\Storage\Blob + * @author Azure Storage PHP SDK + * @copyright 2016 Microsoft Corporation + * @license https://github.com/azure/azure-storage-php/LICENSE + * @link https://github.com/azure/azure-storage-php + */ +class BlobRestProxy extends ServiceRestProxy implements IBlob +{ + use ServiceRestTrait; + + private $singleBlobUploadThresholdInBytes = Resources::MB_IN_BYTES_32; + private $blockSize = Resources::MB_IN_BYTES_4; + + /** + * Builds a blob service object, it accepts the following + * options: + * + * - http: (array) the underlying guzzle options. refer to + * http://docs.guzzlephp.org/en/latest/request-options.html for detailed available options + * - middlewares: (mixed) the middleware should be either an instance of a sub-class that + * implements {@see MicrosoftAzure\Storage\Common\Middlewares\IMiddleware}, or a + * `callable` that follows the Guzzle middleware implementation convention + * + * Please refer to + * https://azure.microsoft.com/en-us/documentation/articles/storage-configure-connection-string + * for how to construct a connection string with storage account name/key, or with a shared + * access signature (SAS Token). + * + * @param string $connectionString The configuration connection string. + * @param array $options Array of options to pass to the service + * @return BlobRestProxy + */ + public static function createBlobService( + $connectionString, + array $options = [] + ) { + $settings = StorageServiceSettings::createFromConnectionString( + $connectionString + ); + + $primaryUri = Utilities::tryAddUrlScheme( + $settings->getBlobEndpointUri() + ); + + $secondaryUri = Utilities::tryAddUrlScheme( + $settings->getBlobSecondaryEndpointUri() + ); + + $blobWrapper = new BlobRestProxy( + $primaryUri, + $secondaryUri, + $settings->getName(), + $options + ); + + // Getting authentication scheme + if ($settings->hasSasToken()) { + $authScheme = new SharedAccessSignatureAuthScheme( + $settings->getSasToken() + ); + } else { + $authScheme = new SharedKeyAuthScheme( + $settings->getName(), + $settings->getKey() + ); + } + + // Adding common request middleware + $commonRequestMiddleware = new CommonRequestMiddleware( + $authScheme, + Resources::STORAGE_API_LATEST_VERSION, + Resources::BLOB_SDK_VERSION + ); + $blobWrapper->pushMiddleware($commonRequestMiddleware); + + return $blobWrapper; + } + + /** + * Builds a blob service object, it accepts the following + * options: + * + * - http: (array) the underlying guzzle options. refer to + * http://docs.guzzlephp.org/en/latest/request-options.html for detailed available options + * - middlewares: (mixed) the middleware should be either an instance of a sub-class that + * implements {@see MicrosoftAzure\Storage\Common\Middlewares\IMiddleware}, or a + * `callable` that follows the Guzzle middleware implementation convention + * + * Please refer to + * https://docs.microsoft.com/en-us/azure/storage/common/storage-auth-aad + * for authenticate access to Azure blobs and queues using Azure Active Directory. + * + * @param string $token The bearer token passed as reference. + * @param string $connectionString The configuration connection string. + * @param array $options Array of options to pass to the service + * + * @return BlobRestProxy + */ + public static function createBlobServiceWithTokenCredential( + &$token, + $connectionString, + array $options = [] + ) { + $settings = StorageServiceSettings::createFromConnectionStringForTokenCredential( + $connectionString + ); + + $primaryUri = Utilities::tryAddUrlScheme( + $settings->getBlobEndpointUri() + ); + + $secondaryUri = Utilities::tryAddUrlScheme( + $settings->getBlobSecondaryEndpointUri() + ); + + $blobWrapper = new BlobRestProxy( + $primaryUri, + $secondaryUri, + $settings->getName(), + $options + ); + + // Getting authentication scheme + $authScheme = new TokenAuthScheme( + $token + ); + + // Adding common request middleware + $commonRequestMiddleware = new CommonRequestMiddleware( + $authScheme, + Resources::STORAGE_API_LATEST_VERSION, + Resources::BLOB_SDK_VERSION + ); + $blobWrapper->pushMiddleware($commonRequestMiddleware); + + return $blobWrapper; + } + + /** + * Builds an anonymous access object with given primary service + * endpoint. The service endpoint should contain a scheme and a + * host, e.g.: + * http://mystorageaccount.blob.core.windows.net + * + * @param string $primaryServiceEndpoint Primary service endpoint. + * @param array $options Optional request options. + * + * @return BlobRestProxy + */ + public static function createContainerAnonymousAccess( + $primaryServiceEndpoint, + array $options = [] + ) { + Validate::canCastAsString($primaryServiceEndpoint, '$primaryServiceEndpoint'); + + $secondaryServiceEndpoint = Utilities::tryGetSecondaryEndpointFromPrimaryEndpoint( + $primaryServiceEndpoint + ); + + $blobWrapper = new BlobRestProxy( + $primaryServiceEndpoint, + $secondaryServiceEndpoint, + Utilities::tryParseAccountNameFromUrl($primaryServiceEndpoint), + $options + ); + + $blobWrapper->pushMiddleware(new CommonRequestMiddleware( + null, + Resources::STORAGE_API_LATEST_VERSION, + Resources::BLOB_SDK_VERSION + )); + + return $blobWrapper; + } + + /** + * Get the value for SingleBlobUploadThresholdInBytes + * + * @return int + */ + public function getSingleBlobUploadThresholdInBytes() + { + return $this->singleBlobUploadThresholdInBytes; + } + + /** + * Get the value for blockSize + * + * @return int + */ + public function getBlockSize() + { + return $this->blockSize; + } + + /** + * Set the value for SingleBlobUploadThresholdInBytes, Max 256MB + * + * @param int $val The max size to send as a single blob block + * + * @return void + */ + public function setSingleBlobUploadThresholdInBytes($val) + { + if ($val > Resources::MB_IN_BYTES_256) { + // What should the proper action here be? + $val = Resources::MB_IN_BYTES_256; + } elseif ($val < 1) { + // another spot that could use looking at + $val = Resources::MB_IN_BYTES_32; + } + $this->singleBlobUploadThresholdInBytes = $val; + //If block size is larger than singleBlobUploadThresholdInBytes, honor + //threshold. + $this->blockSize = $val > $this->blockSize ? $this->blockSize : $val; + } + + /** + * Set the value for block size, Max 100MB + * + * @param int $val The max size for each block to be sent. + * + * @return void + */ + public function setBlockSize($val) + { + if ($val > Resources::MB_IN_BYTES_100) { + // What should the proper action here be? + $val = Resources::MB_IN_BYTES_100; + } elseif ($val < 1) { + // another spot that could use looking at + $val = Resources::MB_IN_BYTES_4; + } + //If block size is larger than singleBlobUploadThresholdInBytes, honor + //threshold. + $val = $val > $this->singleBlobUploadThresholdInBytes ? + $this->singleBlobUploadThresholdInBytes : $val; + $this->blockSize = $val; + } + + /** + * Get the block size of multiple upload block size using the provided + * content + * + * @param StreamInterface $content The content of the blocks. + * + * @return int + */ + private function getMultipleUploadBlockSizeUsingContent($content) + { + //Default value is 100 MB. + $result = Resources::MB_IN_BYTES_100; + //PHP must be ran in 64bit environment so content->getSize() could + //return a guaranteed accurate size. + if (Utilities::is64BitPHP()) { + //Content must be seekable to determine the size. + if ($content->isSeekable()) { + $size = $content->getSize(); + //When threshold is lower than 100MB, assume maximum number of + //block is used for the block blob, if the blockSize is still + //smaller than the assumed size, it means assumed size should + //be hornored, otherwise the blocks count will exceed maximum + //value allowed. + if ($this->blockSize < $result) { + $assumedSize = ceil((float)$size / + (float)(Resources::MAX_BLOB_BLOCKS)); + if ($this->blockSize <= $assumedSize) { + $result = $assumedSize; + } else { + $result = $this->blockSize; + } + } + } + } else { + // If not, we could only honor user's setting to determine + // chunk size. + $result = $this->blockSize; + } + return $result; + } + + /** + * Gets the copy blob source name with specified parameters. + * + * @param string $containerName The name of the container. + * @param string $blobName The name of the blob. + * @param Models\CopyBlobOptions $options The optional parameters. + * + * @return string + */ + private function getCopyBlobSourceName( + $containerName, + $blobName, + Models\CopyBlobOptions $options + ) { + $sourceName = $this->getBlobUrl($containerName, $blobName); + + if (!is_null($options->getSourceSnapshot())) { + $sourceName .= '?snapshot=' . $options->getSourceSnapshot(); + } + + return $sourceName; + } + + /** + * Creates URI path for blob or container. + * + * @param string $container The container name. + * @param string $blob The blob name. + * + * @return string + */ + private function createPath($container, $blob = '') + { + if (empty($blob) && ($blob != '0')) { + return empty($container) ? '/' : $container; + } + $encodedBlob = urlencode($blob); + // Unencode the forward slashes to match what the server expects. + $encodedBlob = str_replace('%2F', '/', $encodedBlob); + // Unencode the backward slashes to match what the server expects. + $encodedBlob = str_replace('%5C', '/', $encodedBlob); + // Re-encode the spaces (encoded as space) to the % encoding. + $encodedBlob = str_replace('+', '%20', $encodedBlob); + // Empty container means accessing default container + if (empty($container)) { + return $encodedBlob; + } + return '/' . $container . '/' . $encodedBlob; + } + + /** + * Creates full URI to the given blob. + * + * @param string $container The container name. + * @param string $blob The blob name. + * + * @return string + */ + public function getBlobUrl($container, $blob) + { + $encodedBlob = $this->createPath($container, $blob); + $uri = $this->getPsrPrimaryUri(); + $exPath = $uri->getPath(); + + if ($exPath != '') { + //Remove the duplicated slash in the path. + $encodedBlob = str_replace('//', '/', $exPath . $encodedBlob); + } + + return (string) $uri->withPath($encodedBlob); + } + + /** + * Helper method to create promise for getContainerProperties API call. + * + * @param string $container The container name. + * @param Models\BlobServiceOptions $options The optional parameters. + * @param string $operation The operation string. Should be + * 'metadata' to get metadata. + * + * @return \GuzzleHttp\Promise\PromiseInterface + */ + private function getContainerPropertiesAsyncImpl( + $container, + Models\BlobServiceOptions $options = null, + $operation = null + ) { + Validate::canCastAsString($container, 'container'); + + $method = Resources::HTTP_GET; + $headers = array(); + $queryParams = array(); + $postParams = array(); + $path = $this->createPath($container); + + if (is_null($options)) { + $options = new BlobServiceOptions(); + } + + $this->addOptionalQueryParam( + $queryParams, + Resources::QP_REST_TYPE, + 'container' + ); + $this->addOptionalQueryParam( + $queryParams, + Resources::QP_COMP, + $operation + ); + + $this->addOptionalHeader( + $headers, + Resources::X_MS_LEASE_ID, + $options->getLeaseId() + ); + $this->addOptionalAccessConditionHeader( + $headers, + $options->getAccessConditions() + ); + + return $this->sendAsync( + $method, + $headers, + $queryParams, + $postParams, + $path, + Resources::STATUS_OK, + Resources::EMPTY_STRING, + $options + )->then(function ($response) { + $responseHeaders = HttpFormatter::formatHeaders($response->getHeaders()); + return GetContainerPropertiesResult::create($responseHeaders); + }, null); + } + + /** + * Adds optional create blob headers. + * + * @param CreateBlobOptions $options The optional parameters. + * @param array $headers The HTTP request headers. + * + * @return array + */ + private function addCreateBlobOptionalHeaders( + CreateBlobOptions $options, + array $headers + ) { + $headers = $this->addOptionalAccessConditionHeader( + $headers, + $options->getAccessConditions() + ); + + $this->addOptionalHeader( + $headers, + Resources::X_MS_LEASE_ID, + $options->getLeaseId() + ); + + $headers = $this->addMetadataHeaders( + $headers, + $options->getMetadata() + ); + + $contentType = $options->getContentType(); + if (is_null($contentType)) { + $contentType = Resources::BINARY_FILE_TYPE; + } + + $this->addOptionalHeader( + $headers, + Resources::X_MS_BLOB_CONTENT_TYPE, + $contentType + ); + $this->addOptionalHeader( + $headers, + Resources::X_MS_BLOB_CONTENT_ENCODING, + $options->getContentEncoding() + ); + $this->addOptionalHeader( + $headers, + Resources::X_MS_BLOB_CONTENT_LANGUAGE, + $options->getContentLanguage() + ); + $this->addOptionalHeader( + $headers, + Resources::X_MS_BLOB_CONTENT_MD5, + $options->getContentMD5() + ); + $this->addOptionalHeader( + $headers, + Resources::X_MS_BLOB_CACHE_CONTROL, + $options->getCacheControl() + ); + $this->addOptionalHeader( + $headers, + Resources::X_MS_BLOB_CONTENT_DISPOSITION, + $options->getContentDisposition() + ); + $this->addOptionalHeader( + $headers, + Resources::CONTENT_TYPE, + Resources::URL_ENCODED_CONTENT_TYPE + ); + + return $headers; + } + + /** + * Adds Range header to the headers array. + * + * @param array $headers The HTTP request headers. + * @param integer $start The start byte. + * @param integer $end The end byte. + * + * @return array + */ + private function addOptionalRangeHeader(array $headers, $start, $end) + { + if (!is_null($start) || !is_null($end)) { + $range = $start . '-' . $end; + $rangeValue = 'bytes=' . $range; + $this->addOptionalHeader($headers, Resources::RANGE, $rangeValue); + } + + return $headers; + } + + /** + * Get the expected status code of a given lease action. + * + * @param string $leaseAction The given lease action + * @return string + * @throws \Exception + */ + private static function getStatusCodeOfLeaseAction($leaseAction) + { + switch ($leaseAction) { + case LeaseMode::ACQUIRE_ACTION: + $statusCode = Resources::STATUS_CREATED; + break; + case LeaseMode::RENEW_ACTION: + $statusCode = Resources::STATUS_OK; + break; + case LeaseMode::RELEASE_ACTION: + $statusCode = Resources::STATUS_OK; + break; + case LeaseMode::BREAK_ACTION: + $statusCode = Resources::STATUS_ACCEPTED; + break; + default: + throw new \Exception(Resources::NOT_IMPLEMENTED_MSG); + } + + return $statusCode; + } + + /** + * Creates promise that does the actual work for leasing a blob. + * + * @param string $leaseAction Lease action string. + * @param string $container Container name. + * @param string $blob Blob to lease name. + * @param string $proposedLeaseId Proposed lease id. + * @param int $leaseDuration Lease duration, in seconds. + * @param string $leaseId Existing lease id. + * @param int $breakPeriod Break period, in seconds. + * @param string $expectedStatusCode Expected status code. + * @param Models\BlobServiceOptions $options Optional parameters. + * @param Models\AccessCondition $accessCondition Access conditions. + * + * @return \GuzzleHttp\Promise\PromiseInterface + */ + private function putLeaseAsyncImpl( + $leaseAction, + $container, + $blob, + $proposedLeaseId, + $leaseDuration, + $leaseId, + $breakPeriod, + $expectedStatusCode, + Models\BlobServiceOptions $options, + Models\AccessCondition $accessCondition = null + ) { + Validate::canCastAsString($blob, 'blob'); + Validate::canCastAsString($container, 'container'); + Validate::notNullOrEmpty($container, 'container'); + + $method = Resources::HTTP_PUT; + $headers = array(); + $queryParams = array(); + $postParams = array(); + + if (empty($blob)) { + $path = $this->createPath($container); + $this->addOptionalQueryParam( + $queryParams, + Resources::QP_REST_TYPE, + 'container' + ); + } else { + $path = $this->createPath($container, $blob); + } + $this->addOptionalQueryParam($queryParams, Resources::QP_COMP, 'lease'); + $this->addOptionalQueryParam( + $queryParams, + Resources::QP_TIMEOUT, + $options->getTimeout() + ); + + $this->addOptionalHeader($headers, Resources::X_MS_LEASE_ID, $leaseId); + $this->addOptionalHeader( + $headers, + Resources::X_MS_LEASE_ACTION, + $leaseAction + ); + $this->addOptionalHeader( + $headers, + Resources::X_MS_LEASE_BREAK_PERIOD, + $breakPeriod + ); + $this->addOptionalHeader( + $headers, + Resources::X_MS_LEASE_DURATION, + $leaseDuration + ); + $this->addOptionalHeader( + $headers, + Resources::X_MS_PROPOSED_LEASE_ID, + $proposedLeaseId + ); + $this->addOptionalAccessConditionHeader($headers, $accessCondition); + + if (!is_null($options)) { + $options = new BlobServiceOptions(); + } + + $options->setLocationMode(LocationMode::PRIMARY_ONLY); + + return $this->sendAsync( + $method, + $headers, + $queryParams, + $postParams, + $path, + $expectedStatusCode, + Resources::EMPTY_STRING, + $options + ); + } + + /** + * Creates promise that does actual work for create and clear blob pages. + * + * @param string $action Either clear or create. + * @param string $container The container name. + * @param string $blob The blob name. + * @param Range $range The page ranges. + * @param string $content The content string. + * @param CreateBlobPagesOptions $options The optional parameters. + * + * @return \GuzzleHttp\Promise\PromiseInterface + */ + private function updatePageBlobPagesAsyncImpl( + $action, + $container, + $blob, + Range $range, + $content, + CreateBlobPagesOptions $options = null + ) { + Validate::canCastAsString($blob, 'blob'); + Validate::notNullOrEmpty($blob, 'blob'); + Validate::canCastAsString($container, 'container'); + Validate::canCastAsString($content, 'content'); + Validate::isTrue( + $range instanceof Range, + sprintf( + Resources::INVALID_PARAM_MSG, + 'range', + get_class(new Range(0)) + ) + ); + $body = Utils::streamFor($content); + + $method = Resources::HTTP_PUT; + $headers = array(); + $queryParams = array(); + $postParams = array(); + $path = $this->createPath($container, $blob); + + if (is_null($options)) { + $options = new CreateBlobPagesOptions(); + } + + $headers = $this->addOptionalRangeHeader( + $headers, + $range->getStart(), + $range->getEnd() + ); + + $headers = $this->addOptionalAccessConditionHeader( + $headers, + $options->getAccessConditions() + ); + + $this->addOptionalHeader( + $headers, + Resources::X_MS_LEASE_ID, + $options->getLeaseId() + ); + $this->addOptionalHeader( + $headers, + Resources::CONTENT_MD5, + $options->getContentMD5() + ); + $this->addOptionalHeader( + $headers, + Resources::X_MS_PAGE_WRITE, + $action + ); + $this->addOptionalHeader( + $headers, + Resources::CONTENT_TYPE, + Resources::URL_ENCODED_CONTENT_TYPE + ); + $this->addOptionalQueryParam($queryParams, Resources::QP_COMP, 'page'); + + $options->setLocationMode(LocationMode::PRIMARY_ONLY); + + return $this->sendAsync( + $method, + $headers, + $queryParams, + $postParams, + $path, + Resources::STATUS_CREATED, + $body, + $options + )->then(function ($response) { + return CreateBlobPagesResult::create( + HttpFormatter::formatHeaders($response->getHeaders()) + ); + }, null); + } + + /** + * Lists all of the containers in the given storage account. + * + * @param ListContainersOptions $options The optional parameters. + * + * @return ListContainersResult + * + * @see http://msdn.microsoft.com/en-us/library/windowsazure/dd179352.aspx + */ + public function listContainers(ListContainersOptions $options = null) + { + return $this->listContainersAsync($options)->wait(); + } + + /** + * Create a promise for lists all of the containers in the given + * storage account. + * + * @param ListContainersOptions $options The optional parameters. + * + * @return \GuzzleHttp\Promise\PromiseInterface + */ + public function listContainersAsync( + ListContainersOptions $options = null + ) { + $method = Resources::HTTP_GET; + $headers = array(); + $queryParams = array(); + $postParams = array(); + $path = Resources::EMPTY_STRING; + + if (is_null($options)) { + $options = new ListContainersOptions(); + } + + $this->addOptionalQueryParam( + $queryParams, + Resources::QP_COMP, + 'list' + ); + $this->addOptionalQueryParam( + $queryParams, + Resources::QP_PREFIX_LOWERCASE, + $options->getPrefix() + ); + $this->addOptionalQueryParam( + $queryParams, + Resources::QP_MARKER_LOWERCASE, + $options->getNextMarker() + ); + $this->addOptionalQueryParam( + $queryParams, + Resources::QP_MAX_RESULTS_LOWERCASE, + $options->getMaxResults() + ); + $isInclude = $options->getIncludeMetadata(); + $isInclude = $isInclude ? 'metadata' : null; + $this->addOptionalQueryParam( + $queryParams, + Resources::QP_INCLUDE, + $isInclude + ); + + $dataSerializer = $this->dataSerializer; + + return $this->sendAsync( + $method, + $headers, + $queryParams, + $postParams, + $path, + Resources::STATUS_OK, + Resources::EMPTY_STRING, + $options + )->then(function ($response) use ($dataSerializer) { + $parsed = $this->dataSerializer->unserialize($response->getBody()); + return ListContainersResult::create( + $parsed, + Utilities::getLocationFromHeaders($response->getHeaders()) + ); + }); + } + + /** + * Creates a new container in the given storage account. + * + * @param string $container The container name. + * @param Models\CreateContainerOptions $options The optional parameters. + * + * @return void + * + * @see http://msdn.microsoft.com/en-us/library/windowsazure/dd179468.aspx + */ + public function createContainer( + $container, + Models\CreateContainerOptions $options = null + ) { + $this->createContainerAsync($container, $options)->wait(); + } + + /** + * Creates a new container in the given storage account. + * + * @param string $container The container name. + * @param Models\CreateContainerOptions $options The optional parameters. + * + * @return \GuzzleHttp\Promise\PromiseInterface + * + * @see http://msdn.microsoft.com/en-us/library/windowsazure/dd179468.aspx + */ + public function createContainerAsync( + $container, + Models\CreateContainerOptions $options = null + ) { + Validate::canCastAsString($container, 'container'); + Validate::notNullOrEmpty($container, 'container'); + + $method = Resources::HTTP_PUT; + $postParams = array(); + $queryParams = array(Resources::QP_REST_TYPE => 'container'); + $path = $this->createPath($container); + + if (is_null($options)) { + $options = new CreateContainerOptions(); + } + + $metadata = $options->getMetadata(); + $headers = $this->generateMetadataHeaders($metadata); + $this->addOptionalHeader( + $headers, + Resources::X_MS_BLOB_PUBLIC_ACCESS, + $options->getPublicAccess() + ); + + $options->setLocationMode(LocationMode::PRIMARY_ONLY); + + return $this->sendAsync( + $method, + $headers, + $queryParams, + $postParams, + $path, + Resources::STATUS_CREATED, + Resources::EMPTY_STRING, + $options + ); + } + + /** + * Deletes a container in the given storage account. + * + * @param string $container The container name. + * @param Models\BlobServiceOptions $options The optional parameters. + * + * @return void + * + * @see http://msdn.microsoft.com/en-us/library/windowsazure/dd179408.aspx + */ + public function deleteContainer( + $container, + Models\BlobServiceOptions $options = null + ) { + $this->deleteContainerAsync($container, $options)->wait(); + } + + /** + * Create a promise for deleting a container. + * + * @param string $container name of the container + * @param Models\BlobServiceOptions|null $options optional parameters + * + * @return \GuzzleHttp\Promise\PromiseInterface + */ + public function deleteContainerAsync( + $container, + Models\BlobServiceOptions $options = null + ) { + Validate::canCastAsString($container, 'container'); + Validate::notNullOrEmpty($container, 'container'); + + $method = Resources::HTTP_DELETE; + $headers = array(); + $postParams = array(); + $queryParams = array(); + $path = $this->createPath($container); + + if (is_null($options)) { + $options = new BlobServiceOptions(); + } + + $this->addOptionalHeader( + $headers, + Resources::X_MS_LEASE_ID, + $options->getLeaseId() + ); + $headers = $this->addOptionalAccessConditionHeader( + $headers, + $options->getAccessConditions() + ); + + $this->addOptionalQueryParam( + $queryParams, + Resources::QP_REST_TYPE, + 'container' + ); + + $options->setLocationMode(LocationMode::PRIMARY_ONLY); + + return $this->sendAsync( + $method, + $headers, + $queryParams, + $postParams, + $path, + Resources::STATUS_ACCEPTED, + Resources::EMPTY_STRING, + $options + ); + } + + /** + * Returns all properties and metadata on the container. + * + * @param string $container name + * @param Models\BlobServiceOptions $options optional parameters + * + * @return Models\GetContainerPropertiesResult + * + * @see http://msdn.microsoft.com/en-us/library/windowsazure/dd179370.aspx + */ + public function getContainerProperties( + $container, + Models\BlobServiceOptions $options = null + ) { + return $this->getContainerPropertiesAsync($container, $options)->wait(); + } + + /** + * Create promise to return all properties and metadata on the container. + * + * @param string $container name + * @param Models\BlobServiceOptions $options optional parameters + * + * @return \GuzzleHttp\Promise\PromiseInterface + * + * @see http://msdn.microsoft.com/en-us/library/windowsazure/dd179370.aspx + */ + public function getContainerPropertiesAsync( + $container, + Models\BlobServiceOptions $options = null + ) { + return $this->getContainerPropertiesAsyncImpl($container, $options); + } + + /** + * Returns only user-defined metadata for the specified container. + * + * @param string $container name + * @param Models\BlobServiceOptions $options optional parameters + * + * @return Models\GetContainerPropertiesResult + * + * @see http://msdn.microsoft.com/en-us/library/windowsazure/ee691976.aspx + */ + public function getContainerMetadata( + $container, + Models\BlobServiceOptions $options = null + ) { + return $this->getContainerMetadataAsync($container, $options)->wait(); + } + + /** + * Create promise to return only user-defined metadata for the specified + * container. + * + * @param string $container name + * @param Models\BlobServiceOptions $options optional parameters + * + * @return \GuzzleHttp\Promise\PromiseInterface + * + * @see http://msdn.microsoft.com/en-us/library/windowsazure/ee691976.aspx + */ + public function getContainerMetadataAsync( + $container, + Models\BlobServiceOptions $options = null + ) { + return $this->getContainerPropertiesAsyncImpl($container, $options, 'metadata'); + } + + /** + * Gets the access control list (ACL) and any container-level access policies + * for the container. + * + * @param string $container The container name. + * @param Models\BlobServiceOptions $options The optional parameters. + * + * @return Models\GetContainerACLResult + * + * @see http://msdn.microsoft.com/en-us/library/windowsazure/dd179469.aspx + */ + public function getContainerAcl( + $container, + Models\BlobServiceOptions $options = null + ) { + return $this->getContainerAclAsync($container, $options)->wait(); + } + + /** + * Creates the promise to get the access control list (ACL) and any + * container-level access policies for the container. + * + * @param string $container The container name. + * @param Models\BlobServiceOptions $options The optional parameters. + * + * @return \GuzzleHttp\Promise\PromiseInterface + * + * @see http://msdn.microsoft.com/en-us/library/windowsazure/dd179469.aspx + */ + public function getContainerAclAsync( + $container, + Models\BlobServiceOptions $options = null + ) { + Validate::canCastAsString($container, 'container'); + + $method = Resources::HTTP_GET; + $headers = array(); + $postParams = array(); + $queryParams = array(); + $path = $this->createPath($container); + + if (is_null($options)) { + $options = new BlobServiceOptions(); + } + + $this->addOptionalQueryParam( + $queryParams, + Resources::QP_REST_TYPE, + 'container' + ); + $this->addOptionalQueryParam( + $queryParams, + Resources::QP_COMP, + 'acl' + ); + + $this->addOptionalHeader( + $headers, + Resources::X_MS_LEASE_ID, + $options->getLeaseId() + ); + $this->addOptionalAccessConditionHeader( + $headers, + $options->getAccessConditions() + ); + + $dataSerializer = $this->dataSerializer; + + $promise = $this->sendAsync( + $method, + $headers, + $queryParams, + $postParams, + $path, + Resources::STATUS_OK, + Resources::EMPTY_STRING, + $options + ); + + return $promise->then(function ($response) use ($dataSerializer) { + $responseHeaders = HttpFormatter::formatHeaders($response->getHeaders()); + + $access = Utilities::tryGetValue( + $responseHeaders, + Resources::X_MS_BLOB_PUBLIC_ACCESS + ); + $etag = Utilities::tryGetValue($responseHeaders, Resources::ETAG); + $modified = Utilities::tryGetValue( + $responseHeaders, + Resources::LAST_MODIFIED + ); + $modifiedDate = Utilities::convertToDateTime($modified); + $parsed = $dataSerializer->unserialize($response->getBody()); + + return GetContainerAclResult::create( + $access, + $etag, + $modifiedDate, + $parsed + ); + }, null); + } + + /** + * Sets the ACL and any container-level access policies for the container. + * + * @param string $container name + * @param Models\ContainerACL $acl access control list for container + * @param Models\BlobServiceOptions $options optional parameters + * + * @return void + * + * @see http://msdn.microsoft.com/en-us/library/windowsazure/dd179391.aspx + */ + public function setContainerAcl( + $container, + Models\ContainerACL $acl, + Models\BlobServiceOptions $options = null + ) { + $this->setContainerAclAsync($container, $acl, $options)->wait(); + } + + /** + * Creates promise to set the ACL and any container-level access policies + * for the container. + * + * @param string $container name + * @param Models\ContainerACL $acl access control list for container + * @param Models\BlobServiceOptions $options optional parameters + * + * @return \GuzzleHttp\Promise\PromiseInterface + * + * @see http://msdn.microsoft.com/en-us/library/windowsazure/dd179391.aspx + */ + public function setContainerAclAsync( + $container, + Models\ContainerACL $acl, + Models\BlobServiceOptions $options = null + ) { + Validate::canCastAsString($container, 'container'); + Validate::notNullOrEmpty($acl, 'acl'); + + $method = Resources::HTTP_PUT; + $headers = array(); + $postParams = array(); + $queryParams = array(); + $path = $this->createPath($container); + $body = $acl->toXml($this->dataSerializer); + + if (is_null($options)) { + $options = new BlobServiceOptions(); + } + + $this->addOptionalQueryParam( + $queryParams, + Resources::QP_REST_TYPE, + 'container' + ); + $this->addOptionalQueryParam( + $queryParams, + Resources::QP_COMP, + 'acl' + ); + $this->addOptionalHeader( + $headers, + Resources::X_MS_BLOB_PUBLIC_ACCESS, + $acl->getPublicAccess() + ); + $this->addOptionalHeader( + $headers, + Resources::CONTENT_TYPE, + Resources::URL_ENCODED_CONTENT_TYPE + ); + $this->addOptionalHeader( + $headers, + Resources::X_MS_LEASE_ID, + $options->getLeaseId() + ); + $this->addOptionalAccessConditionHeader( + $headers, + $options->getAccessConditions() + ); + + $options->setLocationMode(LocationMode::PRIMARY_ONLY); + + return $this->sendAsync( + $method, + $headers, + $queryParams, + $postParams, + $path, + Resources::STATUS_OK, + $body, + $options + ); + } + + /** + * Sets metadata headers on the container. + * + * @param string $container name + * @param array $metadata metadata key/value pair. + * @param Models\BlobServiceOptions $options optional parameters + * + * @return void + * + * @see http://msdn.microsoft.com/en-us/library/windowsazure/dd179362.aspx + */ + public function setContainerMetadata( + $container, + array $metadata, + Models\BlobServiceOptions $options = null + ) { + $this->setContainerMetadataAsync($container, $metadata, $options)->wait(); + } + + /** + * Sets metadata headers on the container. + * + * @param string $container name + * @param array $metadata metadata key/value pair. + * @param Models\BlobServiceOptions $options optional parameters + * + * @return \GuzzleHttp\Promise\PromiseInterface + * + * @see http://msdn.microsoft.com/en-us/library/windowsazure/dd179362.aspx + */ + public function setContainerMetadataAsync( + $container, + array $metadata, + Models\BlobServiceOptions $options = null + ) { + Validate::canCastAsString($container, 'container'); + Utilities::validateMetadata($metadata); + + $method = Resources::HTTP_PUT; + $headers = $this->generateMetadataHeaders($metadata); + $postParams = array(); + $queryParams = array(); + $path = $this->createPath($container); + + if (is_null($options)) { + $options = new BlobServiceOptions(); + } + + $this->addOptionalQueryParam( + $queryParams, + Resources::QP_REST_TYPE, + 'container' + ); + $this->addOptionalQueryParam( + $queryParams, + Resources::QP_COMP, + 'metadata' + ); + + $this->addOptionalHeader( + $headers, + Resources::X_MS_LEASE_ID, + $options->getLeaseId() + ); + + $headers = $this->addOptionalAccessConditionHeader( + $headers, + $options->getAccessConditions() + ); + + $options->setLocationMode(LocationMode::PRIMARY_ONLY); + + return $this->sendAsync( + $method, + $headers, + $queryParams, + $postParams, + $path, + Resources::STATUS_OK, + Resources::EMPTY_STRING, + $options + ); + } + + /** + * Sets blob tier on the blob. + * + * @param string $container name + * @param string $blob name of the blob + * @param Models\SetBlobTierOptions $options optional parameters + * + * @return void + * + * @see https://docs.microsoft.com/en-us/rest/api/storageservices/set-blob-tier + */ + public function setBlobTier( + $container, + $blob, + Models\SetBlobTierOptions $options = null + ) { + $this->setBlobTierAsync($container, $blob, $options)->wait(); + } + + /** + * Sets blob tier on the blob. + * + * @param string $container name + * @param string $blob name of the blob + * @param Models\SetBlobTierOptions $options optional parameters + * + * @return \GuzzleHttp\Promise\PromiseInterface + * + * @see https://docs.microsoft.com/en-us/rest/api/storageservices/set-blob-tier + */ + public function setBlobTierAsync( + $container, + $blob, + Models\SetBlobTierOptions $options = null + ) + { + Validate::canCastAsString($container, 'container'); + Validate::canCastAsString($blob, 'blob'); + Validate::notNullOrEmpty($blob, 'blob'); + + $method = Resources::HTTP_PUT; + $headers = array(); + $postParams = array(); + $queryParams = array(); + $path = $this->createPath($container, $blob); + + if (is_null($options)) { + $options = new SetBlobTierOptions(); + } + + $this->addOptionalQueryParam( + $queryParams, + Resources::QP_COMP, + 'tier' + ); + + $this->addOptionalHeader( + $headers, + Resources::X_MS_ACCESS_TIER, + $options->getAccessTier() + ); + + $options->setLocationMode(LocationMode::PRIMARY_ONLY); + + return $this->sendAsync( + $method, + $headers, + $queryParams, + $postParams, + $path, + array(Resources::STATUS_OK, Resources::STATUS_ACCEPTED), + Resources::EMPTY_STRING, + $options + ); + } + + /** + * Lists all of the blobs in the given container. + * + * @param string $container The container name. + * @param Models\ListBlobsOptions $options The optional parameters. + * + * @return Models\ListBlobsResult + * + * @see http://msdn.microsoft.com/en-us/library/windowsazure/dd135734.aspx + */ + public function listBlobs($container, Models\ListBlobsOptions $options = null) + { + return $this->listBlobsAsync($container, $options)->wait(); + } + + /** + * Creates promise to list all of the blobs in the given container. + * + * @param string $container The container name. + * @param Models\ListBlobsOptions $options The optional parameters. + * + * @return \GuzzleHttp\Promise\PromiseInterface + * + * @see http://msdn.microsoft.com/en-us/library/windowsazure/dd135734.aspx + */ + public function listBlobsAsync( + $container, + Models\ListBlobsOptions $options = null + ) { + Validate::notNull($container, 'container'); + Validate::canCastAsString($container, 'container'); + + $method = Resources::HTTP_GET; + $headers = array(); + $postParams = array(); + $queryParams = array(); + $path = $this->createPath($container); + + if (is_null($options)) { + $options = new ListBlobsOptions(); + } + + $this->addOptionalQueryParam( + $queryParams, + Resources::QP_REST_TYPE, + 'container' + ); + $this->addOptionalQueryParam( + $queryParams, + Resources::QP_COMP, + 'list' + ); + $this->addOptionalQueryParam( + $queryParams, + Resources::QP_PREFIX_LOWERCASE, + str_replace('\\', '/', $options->getPrefix()) + ); + $this->addOptionalQueryParam( + $queryParams, + Resources::QP_MARKER_LOWERCASE, + $options->getNextMarker() + ); + $this->addOptionalQueryParam( + $queryParams, + Resources::QP_DELIMITER, + $options->getDelimiter() + ); + $this->addOptionalQueryParam( + $queryParams, + Resources::QP_MAX_RESULTS_LOWERCASE, + $options->getMaxResults() + ); + + $includeMetadata = $options->getIncludeMetadata(); + $includeSnapshots = $options->getIncludeSnapshots(); + $includeUncommittedBlobs = $options->getIncludeUncommittedBlobs(); + $includecopy = $options->getIncludeCopy(); + $includeDeleted = $options->getIncludeDeleted(); + + $includeValue = static::groupQueryValues( + array( + $includeMetadata ? 'metadata' : null, + $includeSnapshots ? 'snapshots' : null, + $includeUncommittedBlobs ? 'uncommittedblobs' : null, + $includecopy ? 'copy' : null, + $includeDeleted ? 'deleted' : null, + ) + ); + + $this->addOptionalQueryParam( + $queryParams, + Resources::QP_INCLUDE, + $includeValue + ); + + $dataSerializer = $this->dataSerializer; + + return $this->sendAsync( + $method, + $headers, + $queryParams, + $postParams, + $path, + Resources::STATUS_OK, + Resources::EMPTY_STRING, + $options + )->then(function ($response) use ($dataSerializer) { + $parsed = $dataSerializer->unserialize($response->getBody()); + return ListBlobsResult::create( + $parsed, + Utilities::getLocationFromHeaders($response->getHeaders()) + ); + }, null); + } + + /** + * Creates a new page blob. Note that calling createPageBlob to create a page + * blob only initializes the blob. + * To add content to a page blob, call createBlobPages method. + * + * @param string $container The container name. + * @param string $blob The blob name. + * @param integer $length Specifies the maximum size + * for the page blob, up to 1 TB. + * The page blob size must be + * aligned to a 512-byte + * boundary. + * @param Models\CreatePageBlobOptions $options The optional parameters. + * + * @return Models\PutBlobResult + * + * @see http://msdn.microsoft.com/en-us/library/windowsazure/dd179451.aspx + */ + public function createPageBlob( + $container, + $blob, + $length, + Models\CreatePageBlobOptions $options = null + ) { + return $this->createPageBlobAsync( + $container, + $blob, + $length, + $options + )->wait(); + } + + /** + * Creates promise to create a new page blob. Note that calling + * createPageBlob to create a page blob only initializes the blob. + * To add content to a page blob, call createBlobPages method. + * + * @param string $container The container name. + * @param string $blob The blob name. + * @param integer $length Specifies the maximum size + * for the page blob, up to 1 TB. + * The page blob size must be + * aligned to a 512-byte + * boundary. + * @param Models\CreatePageBlobOptions $options The optional parameters. + * + * @return \GuzzleHttp\Promise\PromiseInterface + * + * @see http://msdn.microsoft.com/en-us/library/windowsazure/dd179451.aspx + */ + public function createPageBlobAsync( + $container, + $blob, + $length, + Models\CreatePageBlobOptions $options = null + ) { + Validate::canCastAsString($container, 'container'); + Validate::canCastAsString($blob, 'blob'); + Validate::notNullOrEmpty($blob, 'blob'); + Validate::isInteger($length, 'length'); + Validate::notNull($length, 'length'); + + $method = Resources::HTTP_PUT; + $headers = array(); + $postParams = array(); + $queryParams = array(); + $path = $this->createPath($container, $blob); + + if (is_null($options)) { + $options = new CreatePageBlobOptions(); + } + + $this->addOptionalHeader( + $headers, + Resources::X_MS_BLOB_TYPE, + BlobType::PAGE_BLOB + ); + $this->addOptionalHeader( + $headers, + Resources::X_MS_BLOB_CONTENT_LENGTH, + $length + ); + $this->addOptionalHeader( + $headers, + Resources::X_MS_BLOB_SEQUENCE_NUMBER, + $options->getSequenceNumber() + ); + $this->addOptionalHeader( + $headers, + Resources::X_MS_ACCESS_TIER, + $options->getAccessTier() + ); + $headers = $this->addCreateBlobOptionalHeaders($options, $headers); + + $options->setLocationMode(LocationMode::PRIMARY_ONLY); + + return $this->sendAsync( + $method, + $headers, + $queryParams, + $postParams, + $path, + Resources::STATUS_CREATED, + Resources::EMPTY_STRING, + $options + )->then(function ($response) { + return PutBlobResult::create( + HttpFormatter::formatHeaders($response->getHeaders()) + ); + }, null); + } + + /** + * Create a new append blob. + * If the blob already exists on the service, it will be overwritten. + * + * @param string $container The container name. + * @param string $blob The blob name. + * @param Models\CreateBlobOptions $options The optional parameters. + * + * @return Models\PutBlobResult + * + * @see http://msdn.microsoft.com/en-us/library/windowsazure/dd179451.aspx + */ + public function createAppendBlob( + $container, + $blob, + Models\CreateBlobOptions $options = null + ) { + return $this->createAppendBlobAsync( + $container, + $blob, + $options + )->wait(); + } + + /** + * Creates promise to create a new append blob. + * If the blob already exists on the service, it will be overwritten. + * + * @param string $container The container name. + * @param string $blob The blob name. + * @param Models\CreateBlobOptions $options The optional parameters. + * + * @return \GuzzleHttp\Promise\PromiseInterface + * + * @see http://msdn.microsoft.com/en-us/library/windowsazure/dd179451.aspx + */ + public function createAppendBlobAsync( + $container, + $blob, + Models\CreateBlobOptions $options = null + ) { + Validate::canCastAsString($container, 'container'); + Validate::notNullOrEmpty($container, 'container'); + Validate::canCastAsString($blob, 'blob'); + Validate::notNullOrEmpty($blob, 'blob'); + + $method = Resources::HTTP_PUT; + $headers = array(); + $postParams = array(); + $queryParams = array(); + $path = $this->createPath($container, $blob); + + if (is_null($options)) { + $options = new CreateBlobOptions(); + } + + $this->addOptionalHeader( + $headers, + Resources::X_MS_BLOB_TYPE, + BlobType::APPEND_BLOB + ); + $headers = $this->addCreateBlobOptionalHeaders($options, $headers); + + $options->setLocationMode(LocationMode::PRIMARY_ONLY); + + return $this->sendAsync( + $method, + $headers, + $queryParams, + $postParams, + $path, + Resources::STATUS_CREATED, + Resources::EMPTY_STRING, + $options + )->then(function ($response) { + return PutBlobResult::create( + HttpFormatter::formatHeaders($response->getHeaders()) + ); + }, null); + } + + /** + * Creates a new block blob or updates the content of an existing block blob. + * + * Updating an existing block blob overwrites any existing metadata on the blob. + * Partial updates are not supported with createBlockBlob the content of the + * existing blob is overwritten with the content of the new blob. To perform a + * partial update of the content of a block blob, use the createBlockList + * method. + * Note that the default content type is application/octet-stream. + * + * @param string $container The name of the container. + * @param string $blob The name of the blob. + * @param string|resource|StreamInterface $content The content of the blob. + * @param Models\CreateBlockBlobOptions $options The optional parameters. + * + * @return Models\PutBlobResult + * + * @see http://msdn.microsoft.com/en-us/library/windowsazure/dd179451.aspx + */ + public function createBlockBlob( + $container, + $blob, + $content, + Models\CreateBlockBlobOptions $options = null + ) { + return $this->createBlockBlobAsync( + $container, + $blob, + $content, + $options + )->wait(); + } + + /** + * Creates a promise to create a new block blob or updates the content of + * an existing block blob. + * + * Updating an existing block blob overwrites any existing metadata on the blob. + * Partial updates are not supported with createBlockBlob the content of the + * existing blob is overwritten with the content of the new blob. To perform a + * partial update of the content of a block blob, use the createBlockList + * method. + * + * @param string $container The name of the container. + * @param string $blob The name of the blob. + * @param string|resource|StreamInterface $content The content of the blob. + * @param Models\CreateBlockBlobOptions $options The optional parameters. + * + * @return \GuzzleHttp\Promise\PromiseInterface + * + * @see http://msdn.microsoft.com/en-us/library/windowsazure/dd179451.aspx + */ + public function createBlockBlobAsync( + $container, + $blob, + $content, + Models\CreateBlockBlobOptions $options = null + ) { + $body = Utils::streamFor($content); + + //If the size of the stream is not seekable or larger than the single + //upload threshold then call concurrent upload. Otherwise call putBlob. + $promise = null; + if (!Utilities::isStreamLargerThanSizeOrNotSeekable( + $body, + $this->singleBlobUploadThresholdInBytes + )) { + $promise = $this->createBlockBlobBySingleUploadAsync( + $container, + $blob, + $body, + $options + ); + } else { + // This is for large or failsafe upload + $promise = $this->createBlockBlobByMultipleUploadAsync( + $container, + $blob, + $body, + $options + ); + } + + //return the parsed result, instead of the raw response. + return $promise; + } + + /** + * Create a new page blob and upload the content to the page blob. + * + * @param string $container The name of the container. + * @param string $blob The name of the blob. + * @param int $length The length of the blob. + * @param string|resource|StreamInterface $content The content of the blob. + * @param Models\CreatePageBlobFromContentOptions + * $options The optional parameters. + * + * @return Models\GetBlobPropertiesResult + * + * @see https://docs.microsoft.com/en-us/rest/api/storageservices/fileservices/get-blob-properties + */ + public function createPageBlobFromContent( + $container, + $blob, + $length, + $content, + Models\CreatePageBlobFromContentOptions $options = null + ) { + return $this->createPageBlobFromContentAsync( + $container, + $blob, + $length, + $content, + $options + )->wait(); + } + + /** + * Creates a promise to create a new page blob and upload the content + * to the page blob. + * + * @param string $container The name of the container. + * @param string $blob The name of the blob. + * @param int $length The length of the blob. + * @param string|resource|StreamInterface $content The content of the blob. + * @param Models\CreatePageBlobFromContentOptions + * $options The optional parameters. + * + * @return \GuzzleHttp\Promise\PromiseInterface + * + * @see https://docs.microsoft.com/en-us/rest/api/storageservices/fileservices/get-blob-properties + */ + public function createPageBlobFromContentAsync( + $container, + $blob, + $length, + $content, + Models\CreatePageBlobFromContentOptions $options = null + ) { + $body = Utils::streamFor($content); + $self = $this; + + if (is_null($options)) { + $options = new Models\CreatePageBlobFromContentOptions(); + } + + $createBlobPromise = $this->createPageBlobAsync( + $container, + $blob, + $length, + $options + ); + + $uploadBlobPromise = $createBlobPromise->then( + function ($value) use ( + $self, + $container, + $blob, + $body, + $options + ) { + $result = $value; + return $self->uploadPageBlobAsync( + $container, + $blob, + $body, + $options + ); + }, + null + ); + + return $uploadBlobPromise->then( + function ($value) use ( + $self, + $container, + $blob, + $options + ) { + $getBlobPropertiesOptions = new GetBlobPropertiesOptions(); + $getBlobPropertiesOptions->setLeaseId($options->getLeaseId()); + + return $self->getBlobPropertiesAsync( + $container, + $blob, + $getBlobPropertiesOptions + ); + }, + null + ); + } + + /** + * Creates promise to create a new block blob or updates the content of an + * existing block blob. This only supports contents smaller than single + * upload threashold. + * + * Updating an existing block blob overwrites any existing metadata on + * the blob. + * + * @param string $container The name of the container. + * @param string $blob The name of the blob. + * @param StreamInterface $content The content of the blob. + * @param Models\CreateBlobOptions $options The optional parameters. + * + * @return \GuzzleHttp\Promise\PromiseInterface + * + * @see http://msdn.microsoft.com/en-us/library/windowsazure/dd179451.aspx + */ + protected function createBlockBlobBySingleUploadAsync( + $container, + $blob, + $content, + Models\CreateBlobOptions $options = null + ) { + Validate::canCastAsString($container, 'container'); + Validate::canCastAsString($blob, 'blob'); + Validate::notNullOrEmpty($blob, 'blob'); + Validate::isTrue( + $options == null || + $options instanceof CreateBlobOptions, + sprintf( + Resources::INVALID_PARAM_MSG, + 'options', + get_class(new CreateBlobOptions()) + ) + ); + + $method = Resources::HTTP_PUT; + $headers = array(); + $postParams = array(); + $queryParams = array(); + $path = $this->createPath($container, $blob); + + if (is_null($options)) { + $options = new CreateBlobOptions(); + } + + $headers = $this->addCreateBlobOptionalHeaders($options, $headers); + + $this->addOptionalHeader( + $headers, + Resources::X_MS_BLOB_TYPE, + BlobType::BLOCK_BLOB + ); + + $options->setLocationMode(LocationMode::PRIMARY_ONLY); + + return $this->sendAsync( + $method, + $headers, + $queryParams, + $postParams, + $path, + Resources::STATUS_CREATED, + $content, + $options + )->then( + function ($response) { + return PutBlobResult::create( + HttpFormatter::formatHeaders($response->getHeaders()) + ); + }, + null + ); + } + + /** + * This method creates the blob blocks. This method will send the request + * concurrently for better performance. + * + * @param string $container Name of the container + * @param string $blob Name of the blob + * @param StreamInterface $content Content's stream + * @param Models\CreateBlockBlobOptions $options Array that contains + * all the option + * + * @return \GuzzleHttp\Promise\PromiseInterface + */ + protected function createBlockBlobByMultipleUploadAsync( + $container, + $blob, + $content, + Models\CreateBlockBlobOptions $options = null + ) { + Validate::canCastAsString($container, 'container'); + Validate::canCastAsString($blob, 'blob'); + + if ($content->isSeekable() && Utilities::is64BitPHP()) { + Validate::isTrue( + $content->getSize() <= Resources::MAX_BLOCK_BLOB_SIZE, + Resources::CONTENT_SIZE_TOO_LARGE + ); + } + + if (is_null($options)) { + $options = new Models\CreateBlockBlobOptions(); + } + + $createBlobBlockOptions = CreateBlobBlockOptions::create($options); + $selfInstance = $this; + + $method = Resources::HTTP_PUT; + $headers = $this->createBlobBlockHeader($createBlobBlockOptions); + $postParams = array(); + $path = $this->createPath($container, $blob); + $useTransactionalMD5 = $options->getUseTransactionalMD5(); + + $blockIds = array(); + //Determine the block size according to the content and threshold. + $blockSize = $this->getMultipleUploadBlockSizeUsingContent($content); + $counter = 0; + //create the generator for requests. + //this generator also constructs the blockId array on the fly. + $generator = function () use ( + $content, + &$blockIds, + $blockSize, + $createBlobBlockOptions, + $method, + $headers, + $postParams, + $path, + $useTransactionalMD5, + &$counter, + $selfInstance + ) { + //read the content. + $blockContent = $content->read($blockSize); + //construct the blockId + $blockId = base64_encode( + str_pad($counter++, 6, '0', STR_PAD_LEFT) + ); + $size = strlen($blockContent); + if ($size == 0) { + return null; + } + + if ($useTransactionalMD5) { + $contentMD5 = base64_encode(md5($blockContent, true)); + $selfInstance->addOptionalHeader( + $headers, + Resources::CONTENT_MD5, + $contentMD5 + ); + } + + //add the id to array. + array_push($blockIds, new Block($blockId, 'Uncommitted')); + $queryParams = $selfInstance->createBlobBlockQueryParams( + $createBlobBlockOptions, + $blockId, + true + ); + //return the array of requests. + return $selfInstance->createRequest( + $method, + $headers, + $queryParams, + $postParams, + $path, + LocationMode::PRIMARY_ONLY, + $blockContent + ); + }; + + //Send the request concurrently. + //Does not need to evaluate the results. If operation not successful, + //exception will be thrown. + $putBlobPromise = $this->sendConcurrentAsync( + $generator, + Resources::STATUS_CREATED, + $options + ); + + $commitBlobPromise = $putBlobPromise->then( + function ($value) use ( + $selfInstance, + $container, + $blob, + &$blockIds, + $putBlobPromise, + $options + ) { + return $selfInstance->commitBlobBlocksAsync( + $container, + $blob, + $blockIds, + CommitBlobBlocksOptions::create($options) + ); + }, + null + ); + + return $commitBlobPromise; + } + + + /** + * This method upload the page blob pages. This method will send the request + * concurrently for better performance. + * + * @param string $container Name of the container + * @param string $blob Name of the blob + * @param StreamInterface $content Content's stream + * @param Models\CreatePageBlobFromContentOptions + * $options Array that contains + * all the option + * + * @return \GuzzleHttp\Promise\PromiseInterface + */ + private function uploadPageBlobAsync( + $container, + $blob, + $content, + Models\CreatePageBlobFromContentOptions $options = null + ) { + Validate::canCastAsString($container, 'container'); + Validate::notNullOrEmpty($container, 'container'); + + Validate::canCastAsString($blob, 'blob'); + Validate::notNullOrEmpty($blob, 'blob'); + + if (is_null($options)) { + $options = new Models\CreatePageBlobFromContentOptions(); + } + + $method = Resources::HTTP_PUT; + $postParams = array(); + $queryParams = array(); + $path = $this->createPath($container, $blob); + $useTransactionalMD5 = $options->getUseTransactionalMD5(); + + $this->addOptionalQueryParam($queryParams, Resources::QP_COMP, 'page'); + $this->addOptionalQueryParam( + $queryParams, + Resources::QP_TIMEOUT, + $options->getTimeout() + ); + + $pageSize = Resources::MB_IN_BYTES_4; + $start = 0; + $end = -1; + + //create the generator for requests. + $generator = function () use ( + $content, + $pageSize, + $method, + $postParams, + $queryParams, + $path, + $useTransactionalMD5, + &$start, + &$end, + $options + ) { + //read the content. + do { + $pageContent = $content->read($pageSize); + $size = strlen($pageContent); + + if ($size == 0) { + return null; + } + + $end += $size; + $start = ($end - $size + 1); + + // If all Zero, skip this range + } while (Utilities::allZero($pageContent)); + + $headers = array(); + $headers = $this->addOptionalRangeHeader( + $headers, + $start, + $end + ); + $headers = $this->addOptionalAccessConditionHeader( + $headers, + $options->getAccessConditions() + ); + $this->addOptionalHeader( + $headers, + Resources::X_MS_LEASE_ID, + $options->getLeaseId() + ); + $this->addOptionalHeader( + $headers, + Resources::X_MS_PAGE_WRITE, + PageWriteOption::UPDATE_OPTION + ); + + if ($useTransactionalMD5) { + $contentMD5 = base64_encode(md5($pageContent, true)); + $this->addOptionalHeader( + $headers, + Resources::CONTENT_MD5, + $contentMD5 + ); + } + + //return the array of requests. + return $this->createRequest( + $method, + $headers, + $queryParams, + $postParams, + $path, + LocationMode::PRIMARY_ONLY, + $pageContent + ); + }; + + //Send the request concurrently. + //Does not need to evaluate the results. If operation is not successful, + //exception will be thrown. + return $this->sendConcurrentAsync( + $generator, + Resources::STATUS_CREATED, + $options + ); + } + + /** + * Clears a range of pages from the blob. + * + * @param string $container name of the container + * @param string $blob name of the blob + * @param Range $range Can be up to the value of + * the blob's full size. + * Note that ranges must be + * aligned to 512 (0-511, + * 512-1023) + * @param Models\CreateBlobPagesOptions $options optional parameters + * + * @return Models\CreateBlobPagesResult + * + * @see http://msdn.microsoft.com/en-us/library/windowsazure/ee691975.aspx + */ + public function clearBlobPages( + $container, + $blob, + Range $range, + Models\CreateBlobPagesOptions $options = null + ) { + return $this->clearBlobPagesAsync( + $container, + $blob, + $range, + $options + )->wait(); + } + + /** + * Creates promise to clear a range of pages from the blob. + * + * @param string $container name of the container + * @param string $blob name of the blob + * @param Range $range Can be up to the value of + * the blob's full size. + * Note that ranges must be + * aligned to 512 (0-511, + * 512-1023) + * @param Models\CreateBlobPagesOptions $options optional parameters + * + * @return \GuzzleHttp\Promise\PromiseInterface + * + * @see http://msdn.microsoft.com/en-us/library/windowsazure/ee691975.aspx + */ + public function clearBlobPagesAsync( + $container, + $blob, + Range $range, + Models\CreateBlobPagesOptions $options = null + ) { + return $this->updatePageBlobPagesAsyncImpl( + PageWriteOption::CLEAR_OPTION, + $container, + $blob, + $range, + Resources::EMPTY_STRING, + $options + ); + } + + /** + * Creates a range of pages to a page blob. + * + * @param string $container name of the container + * @param string $blob name of the blob + * @param Range $range Can be up to 4 MB in + * size. Note that ranges + * must be aligned to 512 + * (0-511, 512-1023) + * @param string|resource|StreamInterface $content the blob contents. + * @param Models\CreateBlobPagesOptions $options optional parameters + * + * @return Models\CreateBlobPagesResult + * + * @see http://msdn.microsoft.com/en-us/library/windowsazure/ee691975.aspx + */ + public function createBlobPages( + $container, + $blob, + Range $range, + $content, + Models\CreateBlobPagesOptions $options = null + ) { + return $this->createBlobPagesAsync( + $container, + $blob, + $range, + $content, + $options + )->wait(); + } + + /** + * Creates promise to create a range of pages to a page blob. + * + * @param string $container name of the container + * @param string $blob name of the blob + * @param Range $range Can be up to 4 MB in + * size. Note that ranges + * must be aligned to 512 + * (0-511, 512-1023) + * @param string|resource|StreamInterface $content the blob contents. + * @param Models\CreateBlobPagesOptions $options optional parameters + * + * @return \GuzzleHttp\Promise\PromiseInterface + * + * @see http://msdn.microsoft.com/en-us/library/windowsazure/ee691975.aspx + */ + public function createBlobPagesAsync( + $container, + $blob, + Range $range, + $content, + Models\CreateBlobPagesOptions $options = null + ) { + $contentStream = Utils::streamFor($content); + //because the content is at most 4MB long, can retrieve all the data + //here at once. + $body = $contentStream->getContents(); + + //if the range is not align to 512, throw exception. + $chunks = (int)($range->getLength() / 512); + if ($chunks * 512 != $range->getLength()) { + throw new \RuntimeException(Resources::ERROR_RANGE_NOT_ALIGN_TO_512); + } + + return $this->updatePageBlobPagesAsyncImpl( + PageWriteOption::UPDATE_OPTION, + $container, + $blob, + $range, + $body, + $options + ); + } + + /** + * Creates a new block to be committed as part of a block blob. + * + * @param string $container name of the container + * @param string $blob name of the blob + * @param string $blockId must be less than or + * equal to 64 bytes in + * size. For a given blob, + * the length of the value + * specified for the + * blockid parameter must + * be the same size for + * each block. + * @param resource|string|StreamInterface $content the blob block contents + * @param Models\CreateBlobBlockOptions $options optional parameters + * + * @return Models\PutBlockResult + * + * @see http://msdn.microsoft.com/en-us/library/windowsazure/dd135726.aspx + */ + public function createBlobBlock( + $container, + $blob, + $blockId, + $content, + Models\CreateBlobBlockOptions $options = null + ) { + return $this->createBlobBlockAsync( + $container, + $blob, + $blockId, + $content, + $options + )->wait(); + } + + /** + * Creates a new block to be committed as part of a block blob. + * + * @param string $container name of the container + * @param string $blob name of the blob + * @param string $blockId must be less than or + * equal to 64 bytes in + * size. For a given blob, + * the length of the value + * specified for the + * blockid parameter must + * be the same size for + * each block. + * @param resource|string|StreamInterface $content the blob block contents + * @param Models\CreateBlobBlockOptions $options optional parameters + * + * @return \GuzzleHttp\Promise\PromiseInterface + * + * @see http://msdn.microsoft.com/en-us/library/windowsazure/dd135726.aspx + */ + public function createBlobBlockAsync( + $container, + $blob, + $blockId, + $content, + Models\CreateBlobBlockOptions $options = null + ) { + Validate::canCastAsString($container, 'container'); + Validate::canCastAsString($blob, 'blob'); + Validate::notNullOrEmpty($blob, 'blob'); + Validate::canCastAsString($blockId, 'blockId'); + Validate::notNullOrEmpty($blockId, 'blockId'); + + if (is_null($options)) { + $options = new CreateBlobBlockOptions(); + } + + $method = Resources::HTTP_PUT; + $headers = $this->createBlobBlockHeader($options); + $postParams = array(); + $queryParams = $this->createBlobBlockQueryParams($options, $blockId); + $path = $this->createPath($container, $blob); + $contentStream = Utils::streamFor($content); + $body = $contentStream->getContents(); + + $options->setLocationMode(LocationMode::PRIMARY_ONLY); + + return $this->sendAsync( + $method, + $headers, + $queryParams, + $postParams, + $path, + Resources::STATUS_CREATED, + $body, + $options + )->then(function ($response) { + return PutBlockResult::create( + HttpFormatter::formatHeaders($response->getHeaders()) + ); + }); + } + + /** + * Commits a new block of data to the end of an existing append blob. + * + * @param string $container name of the container + * @param string $blob name of the blob + * @param resource|string|StreamInterface $content the blob block contents + * @param Models\AppendBlockOptions $options optional parameters + * + * @return Models\AppendBlockResult + * + * @see https://docs.microsoft.com/en-us/rest/api/storageservices/fileservices/append-block + */ + public function appendBlock( + $container, + $blob, + $content, + Models\AppendBlockOptions $options = null + ) { + return $this->appendBlockAsync( + $container, + $blob, + $content, + $options + )->wait(); + } + + + /** + * Creates promise to commit a new block of data to the end of an existing append blob. + * + * @param string $container name of the container + * @param string $blob name of the blob + * @param resource|string|StreamInterface $content the blob block contents + * @param Models\AppendBlockOptions $options optional parameters + * + * @return \GuzzleHttp\Promise\PromiseInterface + * + * @see https://docs.microsoft.com/en-us/rest/api/storageservices/fileservices/append-block + */ + public function appendBlockAsync( + $container, + $blob, + $content, + Models\AppendBlockOptions $options = null + ) { + Validate::canCastAsString($container, 'container'); + Validate::notNullOrEmpty($container, 'container'); + Validate::canCastAsString($blob, 'blob'); + Validate::notNullOrEmpty($blob, 'blob'); + + if (is_null($options)) { + $options = new AppendBlockOptions(); + } + + $method = Resources::HTTP_PUT; + $headers = array(); + $postParams = array(); + $queryParams = array(); + $path = $this->createPath($container, $blob); + + $contentStream = Utils::streamFor($content); + $length = $contentStream->getSize(); + $body = $contentStream->getContents(); + + $this->addOptionalQueryParam( + $queryParams, + Resources::QP_COMP, + 'appendblock' + ); + + $headers = $this->addOptionalAccessConditionHeader( + $headers, + $options->getAccessConditions() + ); + + $this->addOptionalHeader( + $headers, + Resources::CONTENT_LENGTH, + $length + ); + $this->addOptionalHeader( + $headers, + Resources::CONTENT_MD5, + $options->getContentMD5() + ); + $this->addOptionalHeader( + $headers, + Resources::X_MS_BLOB_CONDITION_MAXSIZE, + $options->getMaxBlobSize() + ); + $this->addOptionalHeader( + $headers, + Resources::X_MS_BLOB_CONDITION_APPENDPOS, + $options->getAppendPosition() + ); + $this->addOptionalHeader( + $headers, + Resources::X_MS_LEASE_ID, + $options->getLeaseId() + ); + + $options->setLocationMode(LocationMode::PRIMARY_ONLY); + + return $this->sendAsync( + $method, + $headers, + $queryParams, + $postParams, + $path, + Resources::STATUS_CREATED, + $body, + $options + )->then(function ($response) { + return AppendBlockResult::create( + HttpFormatter::formatHeaders($response->getHeaders()) + ); + }); + } + + /** + * create the header for createBlobBlock(s) + * @param Models\CreateBlobBlockOptions $options the option of the request + * + * @return array + */ + protected function createBlobBlockHeader(Models\CreateBlobBlockOptions $options = null) + { + $headers = array(); + $this->addOptionalHeader( + $headers, + Resources::X_MS_LEASE_ID, + $options->getLeaseId() + ); + $this->addOptionalHeader( + $headers, + Resources::CONTENT_MD5, + $options->getContentMD5() + ); + $this->addOptionalHeader( + $headers, + Resources::CONTENT_TYPE, + Resources::URL_ENCODED_CONTENT_TYPE + ); + + return $headers; + } + + /** + * create the query params for createBlobBlock(s) + * @param Models\CreateBlobBlockOptions $options the option of the + * request + * @param string $blockId the block id of the + * block. + * @param bool $isConcurrent if the query + * parameter is for + * concurrent upload. + * + * @return array the constructed query parameters. + */ + protected function createBlobBlockQueryParams( + Models\CreateBlobBlockOptions $options, + $blockId, + $isConcurrent = false + ) { + $queryParams = array(); + $this->addOptionalQueryParam( + $queryParams, + Resources::QP_COMP, + 'block' + ); + $this->addOptionalQueryParam( + $queryParams, + Resources::QP_BLOCKID, + $blockId + ); + if ($isConcurrent) { + $this->addOptionalQueryParam( + $queryParams, + Resources::QP_TIMEOUT, + $options->getTimeout() + ); + } + + return $queryParams; + } + + /** + * This method writes a blob by specifying the list of block IDs that make up the + * blob. In order to be written as part of a blob, a block must have been + * successfully written to the server in a prior createBlobBlock method. + * + * You can call Put Block List to update a blob by uploading only those blocks + * that have changed, then committing the new and existing blocks together. + * You can do this by specifying whether to commit a block from the committed + * block list or from the uncommitted block list, or to commit the most recently + * uploaded version of the block, whichever list it may belong to. + * + * @param string $container The container name. + * @param string $blob The blob name. + * @param Models\BlockList|Block[] $blockList The block entries. + * @param Models\CommitBlobBlocksOptions $options The optional parameters. + * + * @return Models\PutBlobResult + * + * @see http://msdn.microsoft.com/en-us/library/windowsazure/dd179467.aspx + */ + public function commitBlobBlocks( + $container, + $blob, + $blockList, + Models\CommitBlobBlocksOptions $options = null + ) { + return $this->commitBlobBlocksAsync( + $container, + $blob, + $blockList, + $options + )->wait(); + } + + /** + * This method writes a blob by specifying the list of block IDs that make up the + * blob. In order to be written as part of a blob, a block must have been + * successfully written to the server in a prior createBlobBlock method. + * + * You can call Put Block List to update a blob by uploading only those blocks + * that have changed, then committing the new and existing blocks together. + * You can do this by specifying whether to commit a block from the committed + * block list or from the uncommitted block list, or to commit the most recently + * uploaded version of the block, whichever list it may belong to. + * + * @param string $container The container name. + * @param string $blob The blob name. + * @param Models\BlockList|Block[] $blockList The block entries. + * @param Models\CommitBlobBlocksOptions $options The optional parameters. + * + * @return \GuzzleHttp\Promise\PromiseInterface + * + * @see http://msdn.microsoft.com/en-us/library/windowsazure/dd179467.aspx + */ + public function commitBlobBlocksAsync( + $container, + $blob, + $blockList, + Models\CommitBlobBlocksOptions $options = null + ) { + Validate::canCastAsString($container, 'container'); + Validate::canCastAsString($blob, 'blob'); + Validate::notNullOrEmpty($blob, 'blob'); + Validate::isTrue( + $blockList instanceof BlockList || is_array($blockList), + sprintf( + Resources::INVALID_PARAM_MSG, + 'blockList', + get_class(new BlockList()) + ) + ); + + $method = Resources::HTTP_PUT; + $postParams = array(); + $queryParams = array(); + $path = $this->createPath($container, $blob); + $isArray = is_array($blockList); + $blockList = $isArray ? BlockList::create($blockList) : $blockList; + $body = $blockList->toXml($this->dataSerializer); + + if (is_null($options)) { + $options = new CommitBlobBlocksOptions(); + } + + $blobContentType = $options->getContentType(); + $blobContentEncoding = $options->getContentEncoding(); + $blobContentLanguage = $options->getContentLanguage(); + $blobContentMD5 = $options->getContentMD5(); + $blobCacheControl = $options->getCacheControl(); + $blobCcontentDisposition = $options->getContentDisposition(); + $leaseId = $options->getLeaseId(); + $contentType = Resources::URL_ENCODED_CONTENT_TYPE; + + $metadata = $options->getMetadata(); + $headers = $this->generateMetadataHeaders($metadata); + $headers = $this->addOptionalAccessConditionHeader( + $headers, + $options->getAccessConditions() + ); + + $this->addOptionalHeader( + $headers, + Resources::X_MS_LEASE_ID, + $leaseId + ); + $this->addOptionalHeader( + $headers, + Resources::X_MS_BLOB_CACHE_CONTROL, + $blobCacheControl + ); + $this->addOptionalHeader( + $headers, + Resources::X_MS_BLOB_CONTENT_DISPOSITION, + $blobCcontentDisposition + ); + $this->addOptionalHeader( + $headers, + Resources::X_MS_BLOB_CONTENT_TYPE, + $blobContentType + ); + $this->addOptionalHeader( + $headers, + Resources::X_MS_BLOB_CONTENT_ENCODING, + $blobContentEncoding + ); + $this->addOptionalHeader( + $headers, + Resources::X_MS_BLOB_CONTENT_LANGUAGE, + $blobContentLanguage + ); + $this->addOptionalHeader( + $headers, + Resources::X_MS_BLOB_CONTENT_MD5, + $blobContentMD5 + ); + $this->addOptionalHeader( + $headers, + Resources::CONTENT_TYPE, + $contentType + ); + + $this->addOptionalQueryParam( + $queryParams, + Resources::QP_COMP, + 'blocklist' + ); + + $options->setLocationMode(LocationMode::PRIMARY_ONLY); + + return $this->sendAsync( + $method, + $headers, + $queryParams, + $postParams, + $path, + Resources::STATUS_CREATED, + $body, + $options + )->then(function ($response) { + return PutBlobResult::create( + HttpFormatter::formatHeaders($response->getHeaders()) + ); + }, null); + } + + /** + * Retrieves the list of blocks that have been uploaded as part of a block blob. + * + * There are two block lists maintained for a blob: + * 1) Committed Block List: The list of blocks that have been successfully + * committed to a given blob with commitBlobBlocks. + * 2) Uncommitted Block List: The list of blocks that have been uploaded for a + * blob using Put Block (REST API), but that have not yet been committed. + * These blocks are stored in Windows Azure in association with a blob, but do + * not yet form part of the blob. + * + * @param string $container name of the container + * @param string $blob name of the blob + * @param Models\ListBlobBlocksOptions $options optional parameters + * + * @return Models\ListBlobBlocksResult + * + * @see http://msdn.microsoft.com/en-us/library/windowsazure/dd179400.aspx + */ + public function listBlobBlocks( + $container, + $blob, + Models\ListBlobBlocksOptions $options = null + ) { + return $this->listBlobBlocksAsync($container, $blob, $options)->wait(); + } + + /** + * Creates promise to retrieve the list of blocks that have been uploaded as + * part of a block blob. + * + * There are two block lists maintained for a blob: + * 1) Committed Block List: The list of blocks that have been successfully + * committed to a given blob with commitBlobBlocks. + * 2) Uncommitted Block List: The list of blocks that have been uploaded for a + * blob using Put Block (REST API), but that have not yet been committed. + * These blocks are stored in Windows Azure in association with a blob, but do + * not yet form part of the blob. + * + * @param string $container name of the container + * @param string $blob name of the blob + * @param Models\ListBlobBlocksOptions $options optional parameters + * + * @return \GuzzleHttp\Promise\PromiseInterface + * + * @see http://msdn.microsoft.com/en-us/library/windowsazure/dd179400.aspx + */ + public function listBlobBlocksAsync( + $container, + $blob, + Models\ListBlobBlocksOptions $options = null + ) { + Validate::canCastAsString($container, 'container'); + Validate::canCastAsString($blob, 'blob'); + Validate::notNullOrEmpty($blob, 'blob'); + + $method = Resources::HTTP_GET; + $headers = array(); + $postParams = array(); + $queryParams = array(); + $path = $this->createPath($container, $blob); + + if (is_null($options)) { + $options = new ListBlobBlocksOptions(); + } + + $this->addOptionalHeader( + $headers, + Resources::X_MS_LEASE_ID, + $options->getLeaseId() + ); + + $this->addOptionalQueryParam( + $queryParams, + Resources::QP_BLOCK_LIST_TYPE, + $options->getBlockListType() + ); + $this->addOptionalQueryParam( + $queryParams, + Resources::QP_SNAPSHOT, + $options->getSnapshot() + ); + $this->addOptionalQueryParam( + $queryParams, + Resources::QP_COMP, + 'blocklist' + ); + + return $this->sendAsync( + $method, + $headers, + $queryParams, + $postParams, + $path, + Resources::STATUS_OK, + Resources::EMPTY_STRING, + $options + )->then(function ($response) { + $parsed = $this->dataSerializer->unserialize($response->getBody()); + + return ListBlobBlocksResult::create( + HttpFormatter::formatHeaders($response->getHeaders()), + $parsed + ); + }, null); + } + + /** + * Returns all properties and metadata on the blob. + * + * @param string $container name of the container + * @param string $blob name of the blob + * @param Models\GetBlobPropertiesOptions $options optional parameters + * + * @return Models\GetBlobPropertiesResult + * + * @see http://msdn.microsoft.com/en-us/library/windowsazure/dd179394.aspx + */ + public function getBlobProperties( + $container, + $blob, + Models\GetBlobPropertiesOptions $options = null + ) { + return $this->getBlobPropertiesAsync( + $container, + $blob, + $options + )->wait(); + } + + /** + * Creates promise to return all properties and metadata on the blob. + * + * @param string $container name of the container + * @param string $blob name of the blob + * @param Models\GetBlobPropertiesOptions $options optional parameters + * + * @return \GuzzleHttp\Promise\PromiseInterface + * + * @see http://msdn.microsoft.com/en-us/library/windowsazure/dd179394.aspx + */ + public function getBlobPropertiesAsync( + $container, + $blob, + Models\GetBlobPropertiesOptions $options = null + ) { + Validate::canCastAsString($container, 'container'); + Validate::canCastAsString($blob, 'blob'); + Validate::notNullOrEmpty($blob, 'blob'); + + $method = Resources::HTTP_HEAD; + $headers = array(); + $postParams = array(); + $queryParams = array(); + $path = $this->createPath($container, $blob); + + if (is_null($options)) { + $options = new GetBlobPropertiesOptions(); + } + + $headers = $this->addOptionalAccessConditionHeader( + $headers, + $options->getAccessConditions() + ); + + $this->addOptionalHeader( + $headers, + Resources::X_MS_LEASE_ID, + $options->getLeaseId() + ); + $this->addOptionalQueryParam( + $queryParams, + Resources::QP_SNAPSHOT, + $options->getSnapshot() + ); + + return $this->sendAsync( + $method, + $headers, + $queryParams, + $postParams, + $path, + Resources::STATUS_OK, + Resources::EMPTY_STRING, + $options + )->then(function ($response) { + $formattedHeaders = HttpFormatter::formatHeaders($response->getHeaders()); + return GetBlobPropertiesResult::create($formattedHeaders); + }, null); + } + + /** + * Returns all properties and metadata on the blob. + * + * @param string $container name of the container + * @param string $blob name of the blob + * @param Models\GetBlobMetadataOptions $options optional parameters + * + * @return Models\GetBlobMetadataResult + * + * @see http://msdn.microsoft.com/en-us/library/windowsazure/dd179350.aspx + */ + public function getBlobMetadata( + $container, + $blob, + Models\GetBlobMetadataOptions $options = null + ) { + return $this->getBlobMetadataAsync($container, $blob, $options)->wait(); + } + + /** + * Creates promise to return all properties and metadata on the blob. + * + * @param string $container name of the container + * @param string $blob name of the blob + * @param Models\GetBlobMetadataOptions $options optional parameters + * + * @return \GuzzleHttp\Promise\PromiseInterface + * + * @see http://msdn.microsoft.com/en-us/library/windowsazure/dd179350.aspx + */ + public function getBlobMetadataAsync( + $container, + $blob, + Models\GetBlobMetadataOptions $options = null + ) { + Validate::canCastAsString($container, 'container'); + Validate::canCastAsString($blob, 'blob'); + Validate::notNullOrEmpty($blob, 'blob'); + + $method = Resources::HTTP_HEAD; + $headers = array(); + $postParams = array(); + $queryParams = array(); + $path = $this->createPath($container, $blob); + + if (is_null($options)) { + $options = new GetBlobMetadataOptions(); + } + + $headers = $this->addOptionalAccessConditionHeader( + $headers, + $options->getAccessConditions() + ); + + $this->addOptionalHeader( + $headers, + Resources::X_MS_LEASE_ID, + $options->getLeaseId() + ); + $this->addOptionalQueryParam( + $queryParams, + Resources::QP_SNAPSHOT, + $options->getSnapshot() + ); + + $this->addOptionalQueryParam( + $queryParams, + Resources::QP_COMP, + 'metadata' + ); + + return $this->sendAsync( + $method, + $headers, + $queryParams, + $postParams, + $path, + Resources::STATUS_OK, + Resources::EMPTY_STRING, + $options + )->then(function ($response) { + $responseHeaders = HttpFormatter::formatHeaders($response->getHeaders()); + return GetBlobMetadataResult::create($responseHeaders); + }); + } + + /** + * Returns a list of active page ranges for a page blob. Active page ranges are + * those that have been populated with data. + * + * @param string $container name of the container + * @param string $blob name of the blob + * @param Models\ListPageBlobRangesOptions $options optional parameters + * + * @return Models\ListPageBlobRangesResult + * + * @see http://msdn.microsoft.com/en-us/library/windowsazure/ee691973.aspx + */ + public function listPageBlobRanges( + $container, + $blob, + Models\ListPageBlobRangesOptions $options = null + ) { + return $this->listPageBlobRangesAsync( + $container, + $blob, + $options + )->wait(); + } + + /** + * Creates promise to return a list of active page ranges for a page blob. + * Active page ranges are those that have been populated with data. + * + * @param string $container name of the container + * @param string $blob name of the blob + * @param Models\ListPageBlobRangesOptions $options optional parameters + * + * @return \GuzzleHttp\Promise\PromiseInterface + * + * @see http://msdn.microsoft.com/en-us/library/windowsazure/ee691973.aspx + */ + public function listPageBlobRangesAsync( + $container, + $blob, + Models\ListPageBlobRangesOptions $options = null + ) { + return $this->listPageBlobRangesAsyncImpl($container, $blob, null, $options); + } + + /** + * Returns a list of page ranges that have been updated or cleared. + * + * Returns a list of page ranges that have been updated or cleared since + * the snapshot specified by `previousSnapshotTime`. Gets all of the page + * ranges by default, or only the page ranges over a specific range of + * bytes if `rangeStart` and `rangeEnd` in the `options` are specified. + * + * @param string $container name of the container + * @param string $blob name of the blob + * @param string $previousSnapshotTime previous snapshot time + * for comparison which + * should be prior to the + * snapshot time defined + * in `options` + * @param Models\ListPageBlobRangesOptions $options optional parameters + * + * @return Models\ListPageBlobRangesDiffResult + * + * @see https://docs.microsoft.com/en-us/rest/api/storageservices/version-2015-07-08 + */ + public function listPageBlobRangesDiff( + $container, + $blob, + $previousSnapshotTime, + Models\ListPageBlobRangesOptions $options = null + ) { + return $this->listPageBlobRangesDiffAsync( + $container, + $blob, + $previousSnapshotTime, + $options + )->wait(); + } + + /** + * Creates promise to return a list of page ranges that have been updated + * or cleared. + * + * Creates promise to return a list of page ranges that have been updated + * or cleared since the snapshot specified by `previousSnapshotTime`. Gets + * all of the page ranges by default, or only the page ranges over a specific + * range of bytes if `rangeStart` and `rangeEnd` in the `options` are specified. + * + * @param string $container name of the container + * @param string $blob name of the blob + * @param string $previousSnapshotTime previous snapshot time + * for comparison which + * should be prior to the + * snapshot time defined + * in `options` + * @param Models\ListPageBlobRangesOptions $options optional parameters + * + * @return \GuzzleHttp\Promise\PromiseInterface + * + * @see http://msdn.microsoft.com/en-us/library/windowsazure/ee691973.aspx + */ + public function listPageBlobRangesDiffAsync( + $container, + $blob, + $previousSnapshotTime, + Models\ListPageBlobRangesOptions $options = null + ) { + return $this->listPageBlobRangesAsyncImpl( + $container, + $blob, + $previousSnapshotTime, + $options + ); + } + + /** + * Creates promise to return a list of page ranges. + + * If `previousSnapshotTime` is specified, the response will include + * only the pages that differ between the target snapshot or blob and + * the previous snapshot. + * + * @param string $container name of the container + * @param string $blob name of the blob + * @param string $previousSnapshotTime previous snapshot time + * for comparison which + * should be prior to the + * snapshot time defined + * in `options` + * @param Models\ListPageBlobRangesOptions $options optional parameters + * + * @return \GuzzleHttp\Promise\PromiseInterface + * + * @see http://msdn.microsoft.com/en-us/library/windowsazure/ee691973.aspx + */ + private function listPageBlobRangesAsyncImpl( + $container, + $blob, + $previousSnapshotTime = null, + Models\ListPageBlobRangesOptions $options = null + ) { + Validate::canCastAsString($container, 'container'); + Validate::canCastAsString($blob, 'blob'); + Validate::notNullOrEmpty($blob, 'blob'); + + $method = Resources::HTTP_GET; + $headers = array(); + $queryParams = array(); + $postParams = array(); + $path = $this->createPath($container, $blob); + + if (is_null($options)) { + $options = new ListPageBlobRangesOptions(); + } + + $headers = $this->addOptionalAccessConditionHeader( + $headers, + $options->getAccessConditions() + ); + + $range = $options->getRange(); + if ($range) { + $headers = $this->addOptionalRangeHeader( + $headers, + $range->getStart(), + $range->getEnd() + ); + } + + $this->addOptionalHeader( + $headers, + Resources::X_MS_LEASE_ID, + $options->getLeaseId() + ); + $this->addOptionalQueryParam( + $queryParams, + Resources::QP_SNAPSHOT, + $options->getSnapshot() + ); + $this->addOptionalQueryParam( + $queryParams, + Resources::QP_COMP, + 'pagelist' + ); + $this->addOptionalQueryParam( + $queryParams, + Resources::QP_PRE_SNAPSHOT, + $previousSnapshotTime + ); + + $dataSerializer = $this->dataSerializer; + + return $this->sendAsync( + $method, + $headers, + $queryParams, + $postParams, + $path, + Resources::STATUS_OK, + Resources::EMPTY_STRING, + $options + )->then(function ($response) use ($dataSerializer, $previousSnapshotTime) { + $parsed = $dataSerializer->unserialize($response->getBody()); + if (is_null($previousSnapshotTime)) { + return ListPageBlobRangesResult::create( + HttpFormatter::formatHeaders($response->getHeaders()), + $parsed + ); + } else { + return ListPageBlobRangesDiffResult::create( + HttpFormatter::formatHeaders($response->getHeaders()), + $parsed + ); + } + }, null); + } + + /** + * Sets system properties defined for a blob. + * + * @param string $container name of the container + * @param string $blob name of the blob + * @param Models\SetBlobPropertiesOptions $options optional parameters + * + * @return Models\SetBlobPropertiesResult + * + * @see http://msdn.microsoft.com/en-us/library/windowsazure/ee691966.aspx + */ + public function setBlobProperties( + $container, + $blob, + Models\SetBlobPropertiesOptions $options = null + ) { + return $this->setBlobPropertiesAsync( + $container, + $blob, + $options + )->wait(); + } + + /** + * Creates promise to set system properties defined for a blob. + * + * @param string $container name of the container + * @param string $blob name of the blob + * @param Models\SetBlobPropertiesOptions $options optional parameters + * + * @return \GuzzleHttp\Promise\PromiseInterface + * + * @see http://msdn.microsoft.com/en-us/library/windowsazure/ee691966.aspx + */ + public function setBlobPropertiesAsync( + $container, + $blob, + Models\SetBlobPropertiesOptions $options = null + ) { + Validate::canCastAsString($container, 'container'); + Validate::canCastAsString($blob, 'blob'); + Validate::notNullOrEmpty($blob, 'blob'); + + $method = Resources::HTTP_PUT; + $headers = array(); + $postParams = array(); + $queryParams = array(); + $path = $this->createPath($container, $blob); + + if (is_null($options)) { + $options = new SetBlobPropertiesOptions(); + } + + $blobContentType = $options->getContentType(); + $blobContentEncoding = $options->getContentEncoding(); + $blobContentLanguage = $options->getContentLanguage(); + $blobContentLength = $options->getContentLength(); + $blobContentMD5 = $options->getContentMD5(); + $blobCacheControl = $options->getCacheControl(); + $blobContentDisposition = $options->getContentDisposition(); + $leaseId = $options->getLeaseId(); + $sNumberAction = $options->getSequenceNumberAction(); + $sNumber = $options->getSequenceNumber(); + + $headers = $this->addOptionalAccessConditionHeader( + $headers, + $options->getAccessConditions() + ); + + $this->addOptionalHeader( + $headers, + Resources::X_MS_LEASE_ID, + $leaseId + ); + $this->addOptionalHeader( + $headers, + Resources::X_MS_BLOB_CACHE_CONTROL, + $blobCacheControl + ); + $this->addOptionalHeader( + $headers, + Resources::X_MS_BLOB_CONTENT_DISPOSITION, + $blobContentDisposition + ); + $this->addOptionalHeader( + $headers, + Resources::X_MS_BLOB_CONTENT_TYPE, + $blobContentType + ); + $this->addOptionalHeader( + $headers, + Resources::X_MS_BLOB_CONTENT_ENCODING, + $blobContentEncoding + ); + $this->addOptionalHeader( + $headers, + Resources::X_MS_BLOB_CONTENT_LANGUAGE, + $blobContentLanguage + ); + $this->addOptionalHeader( + $headers, + Resources::X_MS_BLOB_CONTENT_LENGTH, + $blobContentLength + ); + $this->addOptionalHeader( + $headers, + Resources::X_MS_BLOB_CONTENT_MD5, + $blobContentMD5 + ); + $this->addOptionalHeader( + $headers, + Resources::X_MS_BLOB_SEQUENCE_NUMBER_ACTION, + $sNumberAction + ); + $this->addOptionalHeader( + $headers, + Resources::X_MS_BLOB_SEQUENCE_NUMBER, + $sNumber + ); + + $this->addOptionalQueryParam($queryParams, Resources::QP_COMP, 'properties'); + + $options->setLocationMode(LocationMode::PRIMARY_ONLY); + + return $this->sendAsync( + $method, + $headers, + $queryParams, + $postParams, + $path, + Resources::STATUS_OK, + Resources::EMPTY_STRING, + $options + )->then(function ($response) { + return SetBlobPropertiesResult::create( + HttpFormatter::formatHeaders($response->getHeaders()) + ); + }, null); + } + + /** + * Sets metadata headers on the blob. + * + * @param string $container name of the container + * @param string $blob name of the blob + * @param array $metadata key/value pair representation + * @param Models\BlobServiceOptions $options optional parameters + * + * @return Models\SetBlobMetadataResult + * + * @see http://msdn.microsoft.com/en-us/library/windowsazure/dd179414.aspx + */ + public function setBlobMetadata( + $container, + $blob, + array $metadata, + Models\BlobServiceOptions $options = null + ) { + return $this->setBlobMetadataAsync( + $container, + $blob, + $metadata, + $options + )->wait(); + } + + /** + * Creates promise to set metadata headers on the blob. + * + * @param string $container name of the container + * @param string $blob name of the blob + * @param array $metadata key/value pair representation + * @param Models\BlobServiceOptions $options optional parameters + * + * @return \GuzzleHttp\Promise\PromiseInterface + * + * @see http://msdn.microsoft.com/en-us/library/windowsazure/dd179414.aspx + */ + public function setBlobMetadataAsync( + $container, + $blob, + array $metadata, + Models\BlobServiceOptions $options = null + ) { + Validate::canCastAsString($container, 'container'); + Validate::canCastAsString($blob, 'blob'); + Validate::notNullOrEmpty($blob, 'blob'); + Utilities::validateMetadata($metadata); + + $method = Resources::HTTP_PUT; + $headers = array(); + $postParams = array(); + $queryParams = array(); + $path = $this->createPath($container, $blob); + + if (is_null($options)) { + $options = new BlobServiceOptions(); + } + + $headers = $this->addOptionalAccessConditionHeader( + $headers, + $options->getAccessConditions() + ); + $headers = $this->addMetadataHeaders($headers, $metadata); + + $this->addOptionalHeader( + $headers, + Resources::X_MS_LEASE_ID, + $options->getLeaseId() + ); + $this->addOptionalQueryParam( + $queryParams, + Resources::QP_COMP, + 'metadata' + ); + + $options->setLocationMode(LocationMode::PRIMARY_ONLY); + + return $this->sendAsync( + $method, + $headers, + $queryParams, + $postParams, + $path, + Resources::STATUS_OK, + Resources::EMPTY_STRING, + $options + )->then(function ($response) { + return SetBlobMetadataResult::create( + HttpFormatter::formatHeaders($response->getHeaders()) + ); + }, null); + } + + /** + * Downloads a blob to a file, the result contains its metadata and + * properties. The result will not contain a stream pointing to the + * content of the file. + * + * @param string $path The path and name of the file + * @param string $container name of the container + * @param string $blob name of the blob + * @param Models\GetBlobOptions $options optional parameters + * + * @return Models\GetBlobResult + * + * @see http://msdn.microsoft.com/en-us/library/windowsazure/dd179440.aspx + */ + public function saveBlobToFile( + $path, + $container, + $blob, + Models\GetBlobOptions $options = null + ) { + return $this->saveBlobToFileAsync( + $path, + $container, + $blob, + $options + )->wait(); + } + + /** + * Creates promise to download a blob to a file, the result contains its + * metadata and properties. The result will not contain a stream pointing + * to the content of the file. + * + * @param string $path The path and name of the file + * @param string $container name of the container + * @param string $blob name of the blob + * @param Models\GetBlobOptions $options optional parameters + * + * @return \GuzzleHttp\Promise\PromiseInterface + * @throws \Exception + * @see http://msdn.microsoft.com/en-us/library/windowsazure/dd179440.aspx + */ + public function saveBlobToFileAsync( + $path, + $container, + $blob, + Models\GetBlobOptions $options = null + ) { + $resource = fopen($path, 'w+'); + if ($resource == null) { + throw new \Exception(Resources::ERROR_FILE_COULD_NOT_BE_OPENED); + } + return $this->getBlobAsync($container, $blob, $options)->then( + function ($result) use ($path, $resource) { + $content = $result->getContentStream(); + while (!feof($content)) { + fwrite( + $resource, + stream_get_contents($content, Resources::MB_IN_BYTES_4) + ); + } + + $content = null; + fclose($resource); + + return $result; + }, + null + ); + } + + /** + * Reads or downloads a blob from the system, including its metadata and + * properties. + * + * @param string $container name of the container + * @param string $blob name of the blob + * @param Models\GetBlobOptions $options optional parameters + * + * @return Models\GetBlobResult + * + * @see http://msdn.microsoft.com/en-us/library/windowsazure/dd179440.aspx + */ + public function getBlob( + $container, + $blob, + Models\GetBlobOptions $options = null + ) { + return $this->getBlobAsync($container, $blob, $options)->wait(); + } + + /** + * Creates promise to read or download a blob from the system, including its + * metadata and properties. + * + * @param string $container name of the container + * @param string $blob name of the blob + * @param Models\GetBlobOptions $options optional parameters + * + * @return \GuzzleHttp\Promise\PromiseInterface + * + * @see http://msdn.microsoft.com/en-us/library/windowsazure/dd179440.aspx + */ + public function getBlobAsync( + $container, + $blob, + Models\GetBlobOptions $options = null + ) { + Validate::canCastAsString($container, 'container'); + Validate::canCastAsString($blob, 'blob'); + + $method = Resources::HTTP_GET; + $headers = array(); + $postParams = array(); + $queryParams = array(); + $path = $this->createPath($container, $blob); + + if (is_null($options)) { + $options = new GetBlobOptions(); + } + + $getMD5 = $options->getRangeGetContentMD5(); + $headers = $this->addOptionalAccessConditionHeader( + $headers, + $options->getAccessConditions() + ); + + $range = $options->getRange(); + if ($range) { + $headers = $this->addOptionalRangeHeader( + $headers, + $range->getStart(), + $range->getEnd() + ); + } + + $this->addOptionalHeader( + $headers, + Resources::X_MS_LEASE_ID, + $options->getLeaseId() + ); + $this->addOptionalHeader( + $headers, + Resources::X_MS_RANGE_GET_CONTENT_MD5, + $getMD5 ? 'true' : null + ); + $this->addOptionalQueryParam( + $queryParams, + Resources::QP_SNAPSHOT, + $options->getSnapshot() + ); + + $options->setIsStreaming(true); + + return $this->sendAsync( + $method, + $headers, + $queryParams, + $postParams, + $path, + array(Resources::STATUS_OK, Resources::STATUS_PARTIAL_CONTENT), + Resources::EMPTY_STRING, + $options + )->then(function ($response) { + $metadata = Utilities::getMetadataArray( + HttpFormatter::formatHeaders($response->getHeaders()) + ); + + return GetBlobResult::create( + HttpFormatter::formatHeaders($response->getHeaders()), + $response->getBody(), + $metadata + ); + }); + } + + /** + * Undeletes a blob. + * + * @param string $container name of the container + * @param string $blob name of the blob + * @param Models\UndeleteBlobOptions $options optional parameters + * + * @return void + * + * @see https://docs.microsoft.com/en-us/rest/api/storageservices/undelete-blob + */ + public function undeleteBlob( + $container, + $blob, + Models\UndeleteBlobOptions $options = null + ) { + $this->undeleteBlobAsync($container, $blob, $options)->wait(); + } + + /** + * Undeletes a blob. + * + * @param string $container name of the container + * @param string $blob name of the blob + * @param Models\UndeleteBlobOptions $options optional parameters + * + * @return \GuzzleHttp\Promise\PromiseInterface + * + * @see https://docs.microsoft.com/en-us/rest/api/storageservices/undelete-blob + */ + public function undeleteBlobAsync( + $container, + $blob, + Models\UndeleteBlobOptions $options = null + ) { + Validate::canCastAsString($container, 'container'); + Validate::canCastAsString($blob, 'blob'); + Validate::notNullOrEmpty($blob, 'blob'); + + $method = Resources::HTTP_PUT; + $headers = array(); + $postParams = array(); + $queryParams = array(); + $path = $this->createPath($container, $blob); + + if (is_null($options)) { + $options = new UndeleteBlobOptions(); + } + + $leaseId = $options->getLeaseId(); + + $headers = $this->addOptionalAccessConditionHeader( + $headers, + $options->getAccessConditions() + ); + + $this->addOptionalHeader( + $headers, + Resources::X_MS_LEASE_ID, + $leaseId + ); + + $this->addOptionalQueryParam($queryParams, Resources::QP_COMP, 'undelete'); + + $options->setLocationMode(LocationMode::PRIMARY_ONLY); + + return $this->sendAsync( + $method, + $headers, + $queryParams, + $postParams, + $path, + Resources::STATUS_OK, + Resources::EMPTY_STRING, + $options + ); + } + + /** + * Deletes a blob or blob snapshot. + * + * Note that if the snapshot entry is specified in the $options then only this + * blob snapshot is deleted. To delete all blob snapshots, do not set Snapshot + * and just set getDeleteSnaphotsOnly to true. + * + * @param string $container name of the container + * @param string $blob name of the blob + * @param Models\DeleteBlobOptions $options optional parameters + * + * @return void + * + * @see http://msdn.microsoft.com/en-us/library/windowsazure/dd179413.aspx + */ + public function deleteBlob( + $container, + $blob, + Models\DeleteBlobOptions $options = null + ) { + $this->deleteBlobAsync($container, $blob, $options)->wait(); + } + + /** + * Creates promise to delete a blob or blob snapshot. + * + * Note that if the snapshot entry is specified in the $options then only this + * blob snapshot is deleted. To delete all blob snapshots, do not set Snapshot + * and just set getDeleteSnaphotsOnly to true. + * + * @param string $container name of the container + * @param string $blob name of the blob + * @param Models\DeleteBlobOptions $options optional parameters + * + * @return \GuzzleHttp\Promise\PromiseInterface + * + * @see http://msdn.microsoft.com/en-us/library/windowsazure/dd179413.aspx + */ + public function deleteBlobAsync( + $container, + $blob, + Models\DeleteBlobOptions $options = null + ) { + Validate::canCastAsString($container, 'container'); + Validate::canCastAsString($blob, 'blob'); + Validate::notNullOrEmpty($blob, 'blob'); + + $method = Resources::HTTP_DELETE; + $headers = array(); + $postParams = array(); + $queryParams = array(); + $path = $this->createPath($container, $blob); + + if (is_null($options)) { + $options = new DeleteBlobOptions(); + } + + if (is_null($options->getSnapshot())) { + $delSnapshots = $options->getDeleteSnaphotsOnly() ? 'only' : 'include'; + $this->addOptionalHeader( + $headers, + Resources::X_MS_DELETE_SNAPSHOTS, + $delSnapshots + ); + } else { + $this->addOptionalQueryParam( + $queryParams, + Resources::QP_SNAPSHOT, + $options->getSnapshot() + ); + } + + $headers = $this->addOptionalAccessConditionHeader( + $headers, + $options->getAccessConditions() + ); + + $this->addOptionalHeader( + $headers, + Resources::X_MS_LEASE_ID, + $options->getLeaseId() + ); + + $options->setLocationMode(LocationMode::PRIMARY_ONLY); + + return $this->sendAsync( + $method, + $headers, + $queryParams, + $postParams, + $path, + Resources::STATUS_ACCEPTED, + Resources::EMPTY_STRING, + $options + ); + } + + /** + * Creates a snapshot of a blob. + * + * @param string $container The name of the container. + * @param string $blob The name of the blob. + * @param Models\CreateBlobSnapshotOptions $options The optional parameters. + * + * @return Models\CreateBlobSnapshotResult + * + * @see http://msdn.microsoft.com/en-us/library/windowsazure/ee691971.aspx + */ + public function createBlobSnapshot( + $container, + $blob, + Models\CreateBlobSnapshotOptions $options = null + ) { + return $this->createBlobSnapshotAsync( + $container, + $blob, + $options + )->wait(); + } + + /** + * Creates promise to create a snapshot of a blob. + * + * @param string $container The name of the container. + * @param string $blob The name of the blob. + * @param Models\CreateBlobSnapshotOptions $options The optional parameters. + * + * @return \GuzzleHttp\Promise\PromiseInterface + * + * @see http://msdn.microsoft.com/en-us/library/windowsazure/ee691971.aspx + */ + public function createBlobSnapshotAsync( + $container, + $blob, + Models\CreateBlobSnapshotOptions $options = null + ) { + Validate::canCastAsString($container, 'container'); + Validate::canCastAsString($blob, 'blob'); + Validate::notNullOrEmpty($blob, 'blob'); + + $method = Resources::HTTP_PUT; + $headers = array(); + $postParams = array(); + $queryParams = array(); + $path = $this->createPath($container, $blob); + + if (is_null($options)) { + $options = new CreateBlobSnapshotOptions(); + } + + $queryParams[Resources::QP_COMP] = 'snapshot'; + + $headers = $this->addOptionalAccessConditionHeader( + $headers, + $options->getAccessConditions() + ); + $headers = $this->addMetadataHeaders($headers, $options->getMetadata()); + $this->addOptionalHeader( + $headers, + Resources::X_MS_LEASE_ID, + $options->getLeaseId() + ); + + $options->setLocationMode(LocationMode::PRIMARY_ONLY); + + return $this->sendAsync( + $method, + $headers, + $queryParams, + $postParams, + $path, + Resources::STATUS_CREATED, + Resources::EMPTY_STRING, + $options + )->then(function ($response) { + return CreateBlobSnapshotResult::create( + HttpFormatter::formatHeaders($response->getHeaders()) + ); + }, null); + } + + /** + * Copies a source blob to a destination blob within the same storage account. + * + * @param string $destinationContainer name of the destination + * container + * @param string $destinationBlob name of the destination + * blob + * @param string $sourceContainer name of the source + * container + * @param string $sourceBlob name of the source + * blob + * @param Models\CopyBlobOptions $options optional parameters + * + * @return Models\CopyBlobResult + * + * @see http://msdn.microsoft.com/en-us/library/windowsazure/dd894037.aspx + */ + public function copyBlob( + $destinationContainer, + $destinationBlob, + $sourceContainer, + $sourceBlob, + Models\CopyBlobOptions $options = null + ) { + return $this->copyBlobAsync( + $destinationContainer, + $destinationBlob, + $sourceContainer, + $sourceBlob, + $options + )->wait(); + } + + /** + * Creates promise to copy a source blob to a destination blob within the + * same storage account. + * + * @param string $destinationContainer name of the destination + * container + * @param string $destinationBlob name of the destination + * blob + * @param string $sourceContainer name of the source + * container + * @param string $sourceBlob name of the source + * blob + * @param Models\CopyBlobOptions $options optional parameters + * + * @return \GuzzleHttp\Promise\PromiseInterface + * + * @see http://msdn.microsoft.com/en-us/library/windowsazure/dd894037.aspx + */ + public function copyBlobAsync( + $destinationContainer, + $destinationBlob, + $sourceContainer, + $sourceBlob, + Models\CopyBlobOptions $options = null + ) { + if (is_null($options)) { + $options = new CopyBlobOptions(); + } + + $sourceBlobPath = $this->getCopyBlobSourceName( + $sourceContainer, + $sourceBlob, + $options + ); + + return $this->copyBlobFromURLAsync( + $destinationContainer, + $destinationBlob, + $sourceBlobPath, + $options + ); + } + + /** + * Copies from a source URL to a destination blob. + * + * @param string $destinationContainer name of the + * destination + * container + * @param string $destinationBlob name of the + * destination + * blob + * @param string $sourceURL URL of the + * source + * resource + * @param Models\CopyBlobFromURLOptions $options optional + * parameters + * + * @return Models\CopyBlobResult + * + * @see http://msdn.microsoft.com/en-us/library/windowsazure/dd894037.aspx + */ + public function copyBlobFromURL( + $destinationContainer, + $destinationBlob, + $sourceURL, + Models\CopyBlobFromURLOptions $options = null + ) { + return $this->copyBlobFromURLAsync( + $destinationContainer, + $destinationBlob, + $sourceURL, + $options + )->wait(); + } + + /** + * Creates promise to copy from source URL to a destination blob. + * + * @param string $destinationContainer name of the + * destination + * container + * @param string $destinationBlob name of the + * destination + * blob + * @param string $sourceURL URL of the + * source + * resource + * @param Models\CopyBlobFromURLOptions $options optional + * parameters + * + * @return \GuzzleHttp\Promise\PromiseInterface + * + * @see http://msdn.microsoft.com/en-us/library/windowsazure/dd894037.aspx + */ + public function copyBlobFromURLAsync( + $destinationContainer, + $destinationBlob, + $sourceURL, + Models\CopyBlobFromURLOptions $options = null + ) { + $method = Resources::HTTP_PUT; + $headers = array(); + $postParams = array(); + $queryParams = array(); + $destinationBlobPath = $this->createPath( + $destinationContainer, + $destinationBlob + ); + + if (is_null($options)) { + $options = new CopyBlobFromURLOptions(); + } + + if ($options->getIsIncrementalCopy()) { + $this->addOptionalQueryParam( + $queryParams, + Resources::QP_COMP, + 'incrementalcopy' + ); + } + + $headers = $this->addOptionalAccessConditionHeader( + $headers, + $options->getAccessConditions() + ); + + $headers = $this->addOptionalSourceAccessConditionHeader( + $headers, + $options->getSourceAccessConditions() + ); + + $this->addOptionalHeader( + $headers, + Resources::X_MS_COPY_SOURCE, + $sourceURL + ); + + $headers = $this->addMetadataHeaders($headers, $options->getMetadata()); + + $this->addOptionalHeader( + $headers, + Resources::X_MS_LEASE_ID, + $options->getLeaseId() + ); + + $this->addOptionalHeader( + $headers, + Resources::X_MS_SOURCE_LEASE_ID, + $options->getSourceLeaseId() + ); + + $this->addOptionalHeader( + $headers, + Resources::X_MS_ACCESS_TIER, + $options->getAccessTier() + ); + + $options->setLocationMode(LocationMode::PRIMARY_ONLY); + + return $this->sendAsync( + $method, + $headers, + $queryParams, + $postParams, + $destinationBlobPath, + Resources::STATUS_ACCEPTED, + Resources::EMPTY_STRING, + $options + )->then(function ($response) { + return CopyBlobResult::create( + HttpFormatter::formatHeaders($response->getHeaders()) + ); + }, null); + } + + /** + * Abort a blob copy operation + * + * @param string $container name of the container + * @param string $blob name of the blob + * @param string $copyId copy operation identifier. + * @param Models\BlobServiceOptions $options optional parameters + * + * @return void + * + * @see https://docs.microsoft.com/en-us/rest/api/storageservices/fileservices/abort-copy-blob + */ + public function abortCopy( + $container, + $blob, + $copyId, + Models\BlobServiceOptions $options = null + ) { + return $this->abortCopyAsync( + $container, + $blob, + $copyId, + $options + )->wait(); + } + + /** + * Creates promise to abort a blob copy operation + * + * @param string $container name of the container + * @param string $blob name of the blob + * @param string $copyId copy operation identifier. + * @param Models\BlobServiceOptions $options optional parameters + * + * @return \GuzzleHttp\Promise\PromiseInterface + * + * @see https://docs.microsoft.com/en-us/rest/api/storageservices/fileservices/abort-copy-blob + */ + public function abortCopyAsync( + $container, + $blob, + $copyId, + Models\BlobServiceOptions $options = null + ) { + Validate::canCastAsString($container, 'container'); + Validate::canCastAsString($blob, 'blob'); + Validate::canCastAsString($copyId, 'copyId'); + Validate::notNullOrEmpty($container, 'container'); + Validate::notNullOrEmpty($blob, 'blob'); + Validate::notNullOrEmpty($copyId, 'copyId'); + + $method = Resources::HTTP_PUT; + $headers = array(); + $postParams = array(); + $queryParams = array(); + $destinationBlobPath = $this->createPath( + $container, + $blob + ); + + if (is_null($options)) { + $options = new BlobServiceOptions(); + } + + $this->addOptionalQueryParam( + $queryParams, + Resources::QP_TIMEOUT, + $options->getTimeout() + ); + + $this->addOptionalQueryParam( + $queryParams, + Resources::QP_COMP, + 'copy' + ); + + $this->addOptionalQueryParam( + $queryParams, + Resources::QP_COPY_ID, + $copyId + ); + + $this->addOptionalHeader( + $headers, + Resources::X_MS_LEASE_ID, + $options->getLeaseId() + ); + + $this->addOptionalHeader( + $headers, + Resources::X_MS_COPY_ACTION, + 'abort' + ); + + return $this->sendAsync( + $method, + $headers, + $queryParams, + $postParams, + $destinationBlobPath, + Resources::STATUS_NO_CONTENT, + Resources::EMPTY_STRING, + $options + ); + } + + /** + * Establishes an exclusive write lock on a blob. To write to a locked + * blob, a client must provide a lease ID. + * + * @param string $container name of the container + * @param string $blob name of the blob + * @param string $proposedLeaseId lease id when acquiring + * @param int $leaseDuration the lease duration. + * A non-infinite + * lease can be between + * 15 and 60 seconds. + * Default is never + * to expire. + * @param Models\BlobServiceOptions $options optional parameters + * + * @return Models\LeaseResult + * + * @see http://msdn.microsoft.com/en-us/library/windowsazure/ee691972.aspx + */ + public function acquireLease( + $container, + $blob, + $proposedLeaseId = null, + $leaseDuration = null, + Models\BlobServiceOptions $options = null + ) { + return $this->acquireLeaseAsync( + $container, + $blob, + $proposedLeaseId, + $leaseDuration, + $options + )->wait(); + } + + /** + * Creates promise to establish an exclusive one-minute write lock on a blob. + * To write to a locked blob, a client must provide a lease ID. + * + * @param string $container name of the container + * @param string $blob name of the blob + * @param string $proposedLeaseId lease id when acquiring + * @param int $leaseDuration the lease duration. + * A non-infinite + * lease can be between + * 15 and 60 seconds. + * Default is never to + * expire. + * @param Models\BlobServiceOptions $options optional parameters + * + * @return \GuzzleHttp\Promise\PromiseInterface + * + * @see http://msdn.microsoft.com/en-us/library/windowsazure/ee691972.aspx + */ + public function acquireLeaseAsync( + $container, + $blob, + $proposedLeaseId = null, + $leaseDuration = null, + Models\BlobServiceOptions $options = null + ) { + if ($options === null) { + $options = new BlobServiceOptions(); + } + + if ($leaseDuration === null) { + $leaseDuration = -1; + } + + return $this->putLeaseAsyncImpl( + LeaseMode::ACQUIRE_ACTION, + $container, + $blob, + $proposedLeaseId, + $leaseDuration, + null /* leaseId */, + null /* breakPeriod */, + self::getStatusCodeOfLeaseAction(LeaseMode::ACQUIRE_ACTION), + $options, + $options->getAccessConditions() + )->then(function ($response) { + return LeaseResult::create( + HttpFormatter::formatHeaders($response->getHeaders()) + ); + }, null); + } + + /** + * change an existing lease + * + * @param string $container name of the container + * @param string $blob name of the blob + * @param string $leaseId lease id when acquiring + * @param string $proposedLeaseId lease id when acquiring + * @param Models\BlobServiceOptions $options optional parameters + * + * @return Models\LeaseResult + * + * @see https://docs.microsoft.com/en-us/rest/api/storageservices/fileservices/lease-blob + */ + public function changeLease( + $container, + $blob, + $leaseId, + $proposedLeaseId, + Models\BlobServiceOptions $options = null + ) { + return $this->changeLeaseAsync( + $container, + $blob, + $leaseId, + $proposedLeaseId, + $options + )->wait(); + } + + /** + * Creates promise to change an existing lease + * + * @param string $container name of the container + * @param string $blob name of the blob + * @param string $leaseId lease id when acquiring + * @param string $proposedLeaseId the proposed lease id + * @param Models\BlobServiceOptions $options optional parameters + * + * @return \GuzzleHttp\Promise\PromiseInterface + * + * @see https://docs.microsoft.com/en-us/rest/api/storageservices/fileservices/lease-blob + */ + public function changeLeaseAsync( + $container, + $blob, + $leaseId, + $proposedLeaseId, + Models\BlobServiceOptions $options = null + ) { + return $this->putLeaseAsyncImpl( + LeaseMode::CHANGE_ACTION, + $container, + $blob, + $proposedLeaseId, + null /* leaseDuration */, + $leaseId, + null /* breakPeriod */, + self::getStatusCodeOfLeaseAction(LeaseMode::RENEW_ACTION), + is_null($options) ? new BlobServiceOptions() : $options + )->then(function ($response) { + return LeaseResult::create( + HttpFormatter::formatHeaders($response->getHeaders()) + ); + }, null); + } + + /** + * Renews an existing lease + * + * @param string $container name of the container + * @param string $blob name of the blob + * @param string $leaseId lease id when acquiring + * @param Models\BlobServiceOptions $options optional parameters + * + * @return Models\LeaseResult + * + * @see https://docs.microsoft.com/en-us/rest/api/storageservices/fileservices/lease-blob + */ + public function renewLease( + $container, + $blob, + $leaseId, + Models\BlobServiceOptions $options = null + ) { + return $this->renewLeaseAsync( + $container, + $blob, + $leaseId, + $options + )->wait(); + } + + /** + * Creates promise to renew an existing lease + * + * @param string $container name of the container + * @param string $blob name of the blob + * @param string $leaseId lease id when acquiring + * @param Models\BlobServiceOptions $options optional parameters + * + * @return \GuzzleHttp\Promise\PromiseInterface + * + * @see https://docs.microsoft.com/en-us/rest/api/storageservices/fileservices/lease-blob + */ + public function renewLeaseAsync( + $container, + $blob, + $leaseId, + Models\BlobServiceOptions $options = null + ) { + return $this->putLeaseAsyncImpl( + LeaseMode::RENEW_ACTION, + $container, + $blob, + null /* proposedLeaseId */, + null /* leaseDuration */, + $leaseId, + null /* breakPeriod */, + self::getStatusCodeOfLeaseAction(LeaseMode::RENEW_ACTION), + is_null($options) ? new BlobServiceOptions() : $options + )->then(function ($response) { + return LeaseResult::create( + HttpFormatter::formatHeaders($response->getHeaders()) + ); + }, null); + } + + /** + * Frees the lease if it is no longer needed so that another client may + * immediately acquire a lease against the blob. + * + * @param string $container name of the container + * @param string $blob name of the blob + * @param string $leaseId lease id when acquiring + * @param Models\BlobServiceOptions $options optional parameters + * + * @return void + * + * @see https://docs.microsoft.com/en-us/rest/api/storageservices/fileservices/lease-blob + */ + public function releaseLease( + $container, + $blob, + $leaseId, + Models\BlobServiceOptions $options = null + ) { + $this->releaseLeaseAsync($container, $blob, $leaseId, $options)->wait(); + } + + /** + * Creates promise to free the lease if it is no longer needed so that + * another client may immediately acquire a lease against the blob. + * + * @param string $container name of the container + * @param string $blob name of the blob + * @param string $leaseId lease id when acquiring + * @param Models\BlobServiceOptions $options optional parameters + * + * @return \GuzzleHttp\Promise\PromiseInterface + * + * @see https://docs.microsoft.com/en-us/rest/api/storageservices/fileservices/lease-blob + */ + public function releaseLeaseAsync( + $container, + $blob, + $leaseId, + Models\BlobServiceOptions $options = null + ) { + return $this->putLeaseAsyncImpl( + LeaseMode::RELEASE_ACTION, + $container, + $blob, + null /* proposedLeaseId */, + null /* leaseDuration */, + $leaseId, + null /* breakPeriod */, + self::getStatusCodeOfLeaseAction(LeaseMode::RELEASE_ACTION), + is_null($options) ? new BlobServiceOptions() : $options + ); + } + + /** + * Ends the lease but ensure that another client cannot acquire a new lease until + * the current lease period has expired. + * + * @param string $container name of the container + * @param string $blob name of the blob + * @param int $breakPeriod the proposed duration of seconds that + * lease should continue before it it broken, + * between 0 and 60 seconds. + * @param Models\BlobServiceOptions $options optional parameters + * + * @return BreakLeaseResult + * + * @see https://docs.microsoft.com/en-us/rest/api/storageservices/fileservices/lease-blob + */ + public function breakLease( + $container, + $blob, + $breakPeriod = null, + Models\BlobServiceOptions $options = null + ) { + return $this->breakLeaseAsync( + $container, + $blob, + $breakPeriod, + $options + )->wait(); + } + + /** + * Creates promise to end the lease but ensure that another client cannot + * acquire a new lease until the current lease period has expired. + * + * @param string $container name of the container + * @param string $blob name of the blob + * @param int $breakPeriod break period, in seconds + * @param Models\BlobServiceOptions $options optional parameters + * + * @return \GuzzleHttp\Promise\PromiseInterface + * + * @see https://docs.microsoft.com/en-us/rest/api/storageservices/fileservices/lease-blob + */ + public function breakLeaseAsync( + $container, + $blob, + $breakPeriod = null, + Models\BlobServiceOptions $options = null + ) { + return $this->putLeaseAsyncImpl( + LeaseMode::BREAK_ACTION, + $container, + $blob, + null /* proposedLeaseId */, + null /* leaseDuration */, + null /* leaseId */, + $breakPeriod, + self::getStatusCodeOfLeaseAction(LeaseMode::BREAK_ACTION), + is_null($options) ? new BlobServiceOptions() : $options + )->then(function ($response) { + return BreakLeaseResult::create( + HttpFormatter::formatHeaders($response->getHeaders()) + ); + }, null); + } + + /** + * Adds optional header to headers if set + * + * @param array $headers The array of request headers. + * @param Models\AccessCondition $accessCondition The access condition object. + * + * @return array + */ + public function addOptionalAccessConditionHeader( + array $headers, + array $accessConditions = null + ) { + if (!empty($accessConditions)) { + foreach ($accessConditions as $accessCondition) { + if (!is_null($accessCondition)) { + $header = $accessCondition->getHeader(); + + if ($header != Resources::EMPTY_STRING) { + $value = $accessCondition->getValue(); + if ($value instanceof \DateTime) { + $value = gmdate( + Resources::AZURE_DATE_FORMAT, + $value->getTimestamp() + ); + } + $headers[$header] = $value; + } + } + } + } + + return $headers; + } + + /** + * Adds optional header to headers if set + * + * @param array $headers The array of request headers. + * @param array $accessCondition The access condition object. + * + * @return array + */ + public function addOptionalSourceAccessConditionHeader( + array $headers, + array $accessConditions = null + ) { + if (!empty($accessConditions)) { + foreach ($accessConditions as $accessCondition) { + if (!is_null($accessCondition)) { + $header = $accessCondition->getHeader(); + $headerName = null; + if (!empty($header)) { + switch ($header) { + case Resources::IF_MATCH: + $headerName = Resources::X_MS_SOURCE_IF_MATCH; + break; + case Resources::IF_UNMODIFIED_SINCE: + $headerName = Resources::X_MS_SOURCE_IF_UNMODIFIED_SINCE; + break; + case Resources::IF_MODIFIED_SINCE: + $headerName = Resources::X_MS_SOURCE_IF_MODIFIED_SINCE; + break; + case Resources::IF_NONE_MATCH: + $headerName = Resources::X_MS_SOURCE_IF_NONE_MATCH; + break; + default: + throw new \Exception(Resources::INVALID_ACH_MSG); + break; + } + } + $value = $accessCondition->getValue(); + if ($value instanceof \DateTime) { + $value = gmdate( + Resources::AZURE_DATE_FORMAT, + $value->getTimestamp() + ); + } + + $this->addOptionalHeader($headers, $headerName, $value); + } + } + } + + return $headers; + } +} diff --git a/3rdparty/microsoft/azure-storage-blob/src/Blob/BlobSharedAccessSignatureHelper.php b/3rdparty/microsoft/azure-storage-blob/src/Blob/BlobSharedAccessSignatureHelper.php new file mode 100644 index 00000000..c85dc87e --- /dev/null +++ b/3rdparty/microsoft/azure-storage-blob/src/Blob/BlobSharedAccessSignatureHelper.php @@ -0,0 +1,213 @@ + + * @copyright Microsoft Corporation + * @license https://github.com/azure/azure-storage-php/LICENSE + * @link https://github.com/azure/azure-storage-php + */ + +namespace MicrosoftAzure\Storage\Blob; + +use MicrosoftAzure\Storage\Blob\Internal\BlobResources as Resources; +use MicrosoftAzure\Storage\Common\Internal\Utilities; +use MicrosoftAzure\Storage\Common\Internal\Validate; +use MicrosoftAzure\Storage\Common\SharedAccessSignatureHelper; + +/** + * Provides methods to generate Azure Storage Shared Access Signature + * + * @category Microsoft + * @package MicrosoftAzure\Storage\Blob + * @author Azure Storage PHP SDK + * @copyright 2017 Microsoft Corporation + * @license https://github.com/azure/azure-storage-php/LICENSE + * @link https://github.com/azure/azure-storage-php + */ +class BlobSharedAccessSignatureHelper extends SharedAccessSignatureHelper +{ + /** + * Constructor. + * + * @param string $accountName the name of the storage account. + * @param string $accountKey the shared key of the storage account + * + */ + public function __construct($accountName, $accountKey) + { + parent::__construct($accountName, $accountKey); + } + + /** + * Generates Blob service shared access signature. + * + * This only supports version 2015-04-05 and later. + * + * @param string $signedResource Resource name to generate the + * canonicalized resource. + * It can be Resources::RESOURCE_TYPE_BLOB + * or Resources::RESOURCE_TYPE_CONTAINER. + * @param string $resourceName The name of the resource, including + * the path of the resource. It should be + * - {container}/{blob}: for blobs, + * - {container}: for containers, e.g.: + * mymusic/music.mp3 or + * music.mp3 + * @param string $signedPermissions Signed permissions. + * @param \Datetime|string $signedExpiry Signed expiry date. + * @param \Datetime|string $signedStart Signed start date. + * @param string $signedIP Signed IP address. + * @param string $signedProtocol Signed protocol. + * @param string $signedIdentifier Signed identifier. + * @param string $cacheControl Cache-Control header (rscc). + * @param string $contentDisposition Content-Disposition header (rscd). + * @param string $contentEncoding Content-Encoding header (rsce). + * @param string $contentLanguage Content-Language header (rscl). + * @param string $contentType Content-Type header (rsct). + * + * @see Constructing an service SAS at + * https://docs.microsoft.com/en-us/rest/api/storageservices/constructing-a-service-sas + * @return string + */ + public function generateBlobServiceSharedAccessSignatureToken( + $signedResource, + $resourceName, + $signedPermissions, + $signedExpiry, + $signedStart = "", + $signedIP = "", + $signedProtocol = "", + $signedIdentifier = "", + $cacheControl = "", + $contentDisposition = "", + $contentEncoding = "", + $contentLanguage = "", + $contentType = "" + ) + { + // check that the resource name is valid. + Validate::canCastAsString($signedResource, 'signedResource'); + Validate::notNullOrEmpty($signedResource, 'signedResource'); + Validate::isTrue( + $signedResource == Resources::RESOURCE_TYPE_BLOB || + $signedResource == Resources::RESOURCE_TYPE_CONTAINER, + \sprintf( + Resources::INVALID_VALUE_MSG, + '$signedResource', + 'Can only be \'b\' or \'c\'.' + ) + ); + + // check that the resource name is valid. + Validate::notNullOrEmpty($resourceName, 'resourceName'); + Validate::canCastAsString($resourceName, 'resourceName'); + + // validate and sanitize signed permissions + $signedPermissions = $this->validateAndSanitizeStringWithArray( + strtolower($signedPermissions), + Resources::ACCESS_PERMISSIONS[$signedResource] + ); + + // check that expiry is valid + if ($signedExpiry instanceof \Datetime) { + $signedExpiry = Utilities::isoDate($signedExpiry); + } + Validate::notNullOrEmpty($signedExpiry, 'signedExpiry'); + Validate::canCastAsString($signedExpiry, 'signedExpiry'); + Validate::isDateString($signedExpiry, 'signedExpiry'); + + // check that signed start is valid + if ($signedStart instanceof \Datetime) { + $signedStart = Utilities::isoDate($signedStart); + } + Validate::canCastAsString($signedStart, 'signedStart'); + if (strlen($signedStart) > 0) { + Validate::isDateString($signedStart, 'signedStart'); + } + + // check that signed IP is valid + Validate::canCastAsString($signedIP, 'signedIP'); + + // validate and sanitize signed protocol + $signedProtocol = $this->validateAndSanitizeSignedProtocol($signedProtocol); + + // check that signed identifier is valid + Validate::canCastAsString($signedIdentifier, 'signedIdentifier'); + Validate::isTrue( + strlen($signedIdentifier) <= 64, + sprintf(Resources::INVALID_STRING_LENGTH, 'signedIdentifier', 'maximum 64') + ); + + Validate::canCastAsString($cacheControl, 'cacheControl'); + Validate::canCastAsString($contentDisposition, 'contentDisposition'); + Validate::canCastAsString($contentEncoding, 'contentEncoding'); + Validate::canCastAsString($contentLanguage, 'contentLanguage'); + Validate::canCastAsString($contentType, 'contentType'); + + // construct an array with the parameters to generate the shared access signature at the account level + $parameters = array(); + $parameters[] = $signedPermissions; + $parameters[] = $signedStart; + $parameters[] = $signedExpiry; + $parameters[] = static::generateCanonicalResource( + $this->accountName, + Resources::RESOURCE_TYPE_BLOB, + $resourceName + ); + $parameters[] = $signedIdentifier; + $parameters[] = $signedIP; + $parameters[] = $signedProtocol; + $parameters[] = Resources::STORAGE_API_LATEST_VERSION; + $parameters[] = $cacheControl; + $parameters[] = $contentDisposition; + $parameters[] = $contentEncoding; + $parameters[] = $contentLanguage; + $parameters[] = $contentType; + + // implode the parameters into a string + $stringToSign = implode("\n", $parameters); + // decode the account key from base64 + $decodedAccountKey = base64_decode($this->accountKey); + // create the signature with hmac sha256 + $signature = hash_hmac("sha256", $stringToSign, $decodedAccountKey, true); + // encode the signature as base64 + $sig = urlencode(base64_encode($signature)); + + $buildOptQueryStr = function ($string, $abrv) { + return $string === '' ? '' : $abrv . $string; + }; + //adding all the components for account SAS together. + $sas = 'sv=' . Resources::STORAGE_API_LATEST_VERSION; + $sas .= '&sr=' . $signedResource; + $sas .= $buildOptQueryStr($cacheControl, '&rscc='); + $sas .= $buildOptQueryStr($contentDisposition, '&rscd='); + $sas .= $buildOptQueryStr($contentEncoding, '&rsce='); + $sas .= $buildOptQueryStr($contentLanguage, '&rscl='); + $sas .= $buildOptQueryStr($contentType, '&rsct='); + + $sas .= $buildOptQueryStr($signedStart, '&st='); + $sas .= '&se=' . $signedExpiry; + $sas .= '&sp=' . $signedPermissions; + $sas .= $buildOptQueryStr($signedIP, '&sip='); + $sas .= $buildOptQueryStr($signedProtocol, '&spr='); + $sas .= $buildOptQueryStr($signedIdentifier, '&si='); + $sas .= '&sig=' . $sig; + + return $sas; + } +} diff --git a/3rdparty/microsoft/azure-storage-blob/src/Blob/Internal/BlobResources.php b/3rdparty/microsoft/azure-storage-blob/src/Blob/Internal/BlobResources.php new file mode 100644 index 00000000..a94adb8f --- /dev/null +++ b/3rdparty/microsoft/azure-storage-blob/src/Blob/Internal/BlobResources.php @@ -0,0 +1,110 @@ + + * @copyright 2016 Microsoft Corporation + * @license https://github.com/azure/azure-storage-php/LICENSE + * @link https://github.com/azure/azure-storage-php + */ + +namespace MicrosoftAzure\Storage\Blob\Internal; + +use MicrosoftAzure\Storage\Common\Internal\Resources; + +/** + * Project resources. + * + * @ignore + * @category Microsoft + * @package MicrosoftAzure\Storage\Common\Internal + * @author Azure Storage PHP SDK + * @copyright 2017 Microsoft Corporation + * @license https://github.com/azure/azure-storage-php/LICENSE + * @link https://github.com/azure/azure-storage-php + */ +class BlobResources extends Resources +{ + // @codingStandardsIgnoreStart + + const BLOB_SDK_VERSION = '1.5.4'; + const STORAGE_API_LATEST_VERSION = '2017-11-09'; + + // Error messages + const INVALID_BTE_MSG = "The blob block type must exist in %s"; + const INVALID_BLOB_PAT_MSG = 'The provided access type is invalid.'; + const INVALID_ACH_MSG = 'The provided access condition header is invalid'; + const ERROR_TOO_LARGE_FOR_BLOCK_BLOB = 'Error: Exceeds the upper limit of the blob.'; + const ERROR_RANGE_NOT_ALIGN_TO_512 = 'Error: Range of the page blob must be align to 512'; + const ERROR_CONTAINER_NOT_EXIST = 'The specified container does not exist'; + const ERROR_BLOB_NOT_EXIST = 'The specified blob does not exist'; + const CONTENT_SIZE_TOO_LARGE = 'The content is too large for the selected blob type.'; + + // Headers + const X_MS_BLOB_PUBLIC_ACCESS = 'x-ms-blob-public-access'; + const X_MS_BLOB_SEQUENCE_NUMBER = 'x-ms-blob-sequence-number'; + const X_MS_BLOB_SEQUENCE_NUMBER_ACTION = 'x-ms-sequence-number-action'; + const X_MS_BLOB_TYPE = 'x-ms-blob-type'; + const X_MS_BLOB_CONTENT_TYPE = 'x-ms-blob-content-type'; + const X_MS_BLOB_CONTENT_ENCODING = 'x-ms-blob-content-encoding'; + const X_MS_BLOB_CONTENT_LANGUAGE = 'x-ms-blob-content-language'; + const X_MS_BLOB_CONTENT_MD5 = 'x-ms-blob-content-md5'; + const X_MS_BLOB_CACHE_CONTROL = 'x-ms-blob-cache-control'; + const X_MS_BLOB_CONTENT_DISPOSITION = 'x-ms-blob-content-disposition'; + const X_MS_BLOB_CONTENT_LENGTH = 'x-ms-blob-content-length'; + const X_MS_BLOB_CONDITION_MAXSIZE = 'x-ms-blob-condition-maxsize'; + const X_MS_BLOB_CONDITION_APPENDPOS = 'x-ms-blob-condition-appendpos'; + const X_MS_BLOB_APPEND_OFFSET = 'x-ms-blob-append-offset'; + const X_MS_BLOB_COMMITTED_BLOCK_COUNT = 'x-ms-blob-committed-block-count'; + const X_MS_LEASE_DURATION = 'x-ms-lease-duration'; + const X_MS_LEASE_ID = 'x-ms-lease-id'; + const X_MS_LEASE_TIME = 'x-ms-lease-time'; + const X_MS_LEASE_STATUS = 'x-ms-lease-status'; + const X_MS_LEASE_STATE = 'x-ms-lease-state'; + const X_MS_LEASE_ACTION = 'x-ms-lease-action'; + const X_MS_PROPOSED_LEASE_ID = 'x-ms-proposed-lease-id'; + const X_MS_LEASE_BREAK_PERIOD = 'x-ms-lease-break-period'; + const X_MS_PAGE_WRITE = 'x-ms-page-write'; + const X_MS_REQUEST_SERVER_ENCRYPTED = 'x-ms-request-server-encrypted'; + const X_MS_SERVER_ENCRYPTED = 'x-ms-server-encrypted'; + const X_MS_INCREMENTAL_COPY = 'x-ms-incremental-copy'; + const X_MS_COPY_DESTINATION_SNAPSHOT = 'x-ms-copy-destination-snapshot'; + const X_MS_ACCESS_TIER = 'x-ms-access-tier'; + const X_MS_ACCESS_TIER_INFERRED = 'x-ms-access-tier-inferred'; + const X_MS_ACCESS_TIER_CHANGE_TIME = 'x-ms-access-tier-change-time'; + const X_MS_ARCHIVE_STATUS = 'x-ms-archive-status'; + const MAX_BLOB_SIZE = 'x-ms-blob-condition-maxsize'; + const MAX_APPEND_POSITION = 'x-ms-blob-condition-appendpos'; + const SEQUENCE_NUMBER_LESS_THAN_OR_EQUAL = 'x-ms-if-sequence-number-le'; + const SEQUENCE_NUMBER_LESS_THAN = 'x-ms-if-sequence-number-lt'; + const SEQUENCE_NUMBER_EQUAL = 'x-ms-if-sequence-number-eq'; + const BLOB_CONTENT_MD5 = 'x-ms-blob-content-md5'; + + // Query parameters + const QP_DELIMITER = 'Delimiter'; + const QP_BLOCKID = 'blockid'; + const QP_BLOCK_LIST_TYPE = 'blocklisttype'; + const QP_PRE_SNAPSHOT = 'prevsnapshot'; + + // Resource permissions + const ACCESS_PERMISSIONS = [ + Resources::RESOURCE_TYPE_BLOB => ['r', 'a', 'c', 'w', 'd'], + Resources::RESOURCE_TYPE_CONTAINER => ['r', 'a', 'c', 'w', 'd', 'l'] + ]; + + // @codingStandardsIgnoreEnd +} diff --git a/3rdparty/microsoft/azure-storage-blob/src/Blob/Internal/IBlob.php b/3rdparty/microsoft/azure-storage-blob/src/Blob/Internal/IBlob.php new file mode 100644 index 00000000..fa9ce6b3 --- /dev/null +++ b/3rdparty/microsoft/azure-storage-blob/src/Blob/Internal/IBlob.php @@ -0,0 +1,1665 @@ + + * @copyright 2016 Microsoft Corporation + * @license https://github.com/azure/azure-storage-php/LICENSE + * @link https://github.com/azure/azure-storage-php + */ + +namespace MicrosoftAzure\Storage\Blob\Internal; + +use MicrosoftAzure\Storage\Blob\Models as BlobModels; +use MicrosoftAzure\Storage\Common\Models\ServiceOptions; +use MicrosoftAzure\Storage\Common\Models\ServiceProperties; +use MicrosoftAzure\Storage\Common\Models\Range; +use Psr\Http\Message\StreamInterface; + +/** + * This interface has all REST APIs provided by Windows Azure for Blob service. + * + * @ignore + * @category Microsoft + * @package MicrosoftAzure\Storage\Blob\Internal + * @author Azure Storage PHP SDK + * @copyright 2016 Microsoft Corporation + * @license https://github.com/azure/azure-storage-php/LICENSE + * @link https://github.com/azure/azure-storage-php + * @see http://msdn.microsoft.com/en-us/library/windowsazure/dd135733.aspx + */ +interface IBlob +{ + /** + * Gets the properties of the service. + * + * @param ServiceOptions $options optional service options. + * + * @return GetServicePropertiesResult + * + * @see http://msdn.microsoft.com/en-us/library/windowsazure/hh452239.aspx + */ + public function getServiceProperties(ServiceOptions $options = null); + + /** + * Creates promise to get the properties of the service. + * + * @param ServiceOptions $options The optional parameters. + * + * @return \GuzzleHttp\Promise\PromiseInterface + * + * @see http://msdn.microsoft.com/en-us/library/windowsazure/hh452239.aspx + */ + public function getServicePropertiesAsync(ServiceOptions $options = null); + + /** + * Sets the properties of the service. + * + * @param ServiceProperties $serviceProperties new service properties + * @param ServiceOptions $options optional parameters + * + * @return void + * + * @see http://msdn.microsoft.com/en-us/library/windowsazure/hh452235.aspx + */ + public function setServiceProperties( + ServiceProperties $serviceProperties, + ServiceOptions $options = null + ); + + /** + * Retieves statistics related to replication for the service. The operation + * will only be sent to secondary location endpoint. + * + * @param ServiceOptions|null $options The options this operation sends with. + * + * @return GetServiceStatsResult + * + * @see https://docs.microsoft.com/en-us/rest/api/storageservices/get-blob-service-stats + */ + public function getServiceStats(ServiceOptions $options = null); + + /** + * Creates promise that retrieves statistics related to replication for the + * service. The operation will only be sent to secondary location endpoint. + * + * @param ServiceOptions|null $options The options this operation sends with. + * + * @return \GuzzleHttp\Promise\PromiseInterface + * + * @see https://docs.microsoft.com/en-us/rest/api/storageservices/get-blob-service-stats + */ + public function getServiceStatsAsync(ServiceOptions $options = null); + + /** + * Creates the promise to set the properties of the service. + * + * It's recommended to use getServiceProperties, alter the returned object and + * then use setServiceProperties with this altered object. + * + * @param ServiceProperties $serviceProperties new service properties. + * @param ServiceOptions $options optional parameters + * + * @return \GuzzleHttp\Promise\PromiseInterface + * + * @see http://msdn.microsoft.com/en-us/library/windowsazure/hh452235.aspx + */ + public function setServicePropertiesAsync( + ServiceProperties $serviceProperties, + ServiceOptions $options = null + ); + + /** + * Lists all of the containers in the given storage account. + * + * @param BlobModels\ListContainersOptions $options optional parameters + * + * @return BlobModels\ListContainersResult + * + * @see http://msdn.microsoft.com/en-us/library/windowsazure/dd179352.aspx + */ + public function listContainers(BlobModels\ListContainersOptions $options = null); + + /** + * Create a promise for lists all of the containers in the given + * storage account. + * + * @param BlobModels\ListContainersOptions $options The optional parameters. + * + * @return \GuzzleHttp\Promise\PromiseInterface + */ + public function listContainersAsync( + BlobModels\ListContainersOptions $options = null + ); + + /** + * Creates a new container in the given storage account. + * + * @param string $container name + * @param BlobModels\CreateContainerOptions $options optional parameters + * + * @return void + * + * @see http://msdn.microsoft.com/en-us/library/windowsazure/dd179468.aspx + */ + public function createContainer( + $container, + BlobModels\CreateContainerOptions $options = null + ); + + /** + * Creates a new container in the given storage account. + * + * @param string $container The container name. + * @param BlobModels\CreateContainerOptions $options The optional parameters. + * + * @return \GuzzleHttp\Promise\PromiseInterface + * + * @see http://msdn.microsoft.com/en-us/library/windowsazure/dd179468.aspx + */ + public function createContainerAsync( + $container, + BlobModels\CreateContainerOptions $options = null + ); + + /** + * Creates a new container in the given storage account. + * + * @param string $container name + * @param BlobModels\BlobServiceOptions $options optional parameters + * + * @return void + * + * @see http://msdn.microsoft.com/en-us/library/windowsazure/dd179408.aspx + */ + public function deleteContainer( + $container, + BlobModels\BlobServiceOptions $options = null + ); + + /** + * Create a promise for deleting a container. + * + * @param string $container name of the container + * @param BlobModels\BlobServiceOptions $options optional parameters + * + * @return \GuzzleHttp\Promise\PromiseInterface + */ + public function deleteContainerAsync( + $container, + BlobModels\BlobServiceOptions $options = null + ); + + /** + * Returns all properties and metadata on the container. + * + * @param string $container name + * @param BlobModels\BlobServiceOptions $options optional parameters + * + * @return BlobModels\GetContainerPropertiesResult + * + * @see http://msdn.microsoft.com/en-us/library/windowsazure/dd179370.aspx + */ + public function getContainerProperties( + $container, + BlobModels\BlobServiceOptions $options = null + ); + + /** + * Create promise to return all properties and metadata on the container. + * + * @param string $container name + * @param BlobModels\BlobServiceOptions $options optional parameters + * + * @return \GuzzleHttp\Promise\PromiseInterface + * + * @see http://msdn.microsoft.com/en-us/library/windowsazure/dd179370.aspx + */ + public function getContainerPropertiesAsync( + $container, + BlobModels\BlobServiceOptions $options = null + ); + + /** + * Returns only user-defined metadata for the specified container. + * + * @param string $container name + * @param BlobModels\BlobServiceOptions $options optional parameters + * + * @return BlobModels\GetContainerPropertiesResult + * + * @see http://msdn.microsoft.com/en-us/library/windowsazure/ee691976.aspx + */ + public function getContainerMetadata( + $container, + BlobModels\BlobServiceOptions $options = null + ); + + /** + * Create promise to return only user-defined metadata for the specified + * container. + * + * @param string $container name + * @param BlobModels\BlobServiceOptions $options optional parameters + * + * @return \GuzzleHttp\Promise\PromiseInterface + * + * @see http://msdn.microsoft.com/en-us/library/windowsazure/ee691976.aspx + */ + public function getContainerMetadataAsync( + $container, + BlobModels\BlobServiceOptions $options = null + ); + + /** + * Gets the access control list (ACL) and any container-level access policies + * for the container. + * + * @param string $container name + * @param BlobModels\BlobServiceOptions $options optional parameters + * + * @return BlobModels\GetContainerACLResult + * + * @see http://msdn.microsoft.com/en-us/library/windowsazure/dd179469.aspx + */ + public function getContainerAcl( + $container, + BlobModels\BlobServiceOptions $options = null + ); + + /** + * Creates the promise to get the access control list (ACL) and any + * container-level access policies for the container. + * + * @param string $container The container name. + * @param BlobModels\BlobServiceOptions $options The optional parameters. + * + * @return \GuzzleHttp\Promise\PromiseInterface + * + * @see http://msdn.microsoft.com/en-us/library/windowsazure/dd179469.aspx + */ + public function getContainerAclAsync( + $container, + BlobModels\BlobServiceOptions $options = null + ); + + /** + * Sets the ACL and any container-level access policies for the container. + * + * @param string $container name + * @param BlobModels\ContainerACL $acl access control list for container + * @param BlobModels\BlobServiceOptions $options optional parameters + * + * @return void + * + * @see http://msdn.microsoft.com/en-us/library/windowsazure/dd179391.aspx + */ + public function setContainerAcl( + $container, + BlobModels\ContainerACL $acl, + BlobModels\BlobServiceOptions $options = null + ); + + /** + * Creates promise to set the ACL and any container-level access policies + * for the container. + * + * @param string $container name + * @param BlobModels\ContainerACL $acl access control list for container + * @param BlobModels\BlobServiceOptions $options optional parameters + * + * @return \GuzzleHttp\Promise\PromiseInterface + * + * @see http://msdn.microsoft.com/en-us/library/windowsazure/dd179391.aspx + */ + public function setContainerAclAsync( + $container, + BlobModels\ContainerACL $acl, + BlobModels\BlobServiceOptions $options = null + ); + + /** + * Sets metadata headers on the container. + * + * @param string $container name + * @param array $metadata metadata key/value pair. + * @param BlobModels\BlobServiceOptions $options optional parameters + * + * @return void + * + * @see http://msdn.microsoft.com/en-us/library/windowsazure/dd179362.aspx + */ + public function setContainerMetadata( + $container, + array $metadata, + BlobModels\BlobServiceOptions $options = null + ); + + /** + * Sets metadata headers on the container. + * + * @param string $container name + * @param array $metadata metadata key/value pair. + * @param BlobModels\BlobServiceOptions $options optional parameters + * + * @return \GuzzleHttp\Promise\PromiseInterface + * + * @see http://msdn.microsoft.com/en-us/library/windowsazure/dd179362.aspx + */ + public function setContainerMetadataAsync( + $container, + array $metadata, + BlobModels\BlobServiceOptions $options = null + ); + + /** + * Lists all of the blobs in the given container. + * + * @param string $container name + * @param BlobModels\ListBlobsOptions $options optional parameters + * + * @return BlobModels\ListBlobsResult + * + * @see http://msdn.microsoft.com/en-us/library/windowsazure/dd135734.aspx + */ + public function listBlobs( + $container, + BlobModels\ListBlobsOptions $options = null + ); + + /** + * Creates promise to list all of the blobs in the given container. + * + * @param string $container The container name. + * @param BlobModels\ListBlobsOptions $options The optional parameters. + * + * @return \GuzzleHttp\Promise\PromiseInterface + * + * @see http://msdn.microsoft.com/en-us/library/windowsazure/dd135734.aspx + */ + public function listBlobsAsync( + $container, + BlobModels\ListBlobsOptions $options = null + ); + + /** + * Creates a new page blob. Note that calling createPageBlob to create a page + * blob only initializes the blob. + * To add content to a page blob, call createBlobPages method. + * + * @param string $container name of the container + * @param string $blob name of the blob + * @param int $length specifies the maximum size + * for the page blob, up to 1 TB. The page blob size must be aligned to + * a 512-byte boundary. + * @param BlobModels\CreatePageBlobOptions $options optional parameters + * + * @return BlobModels\PutBlobResult + * + * @see http://msdn.microsoft.com/en-us/library/windowsazure/dd179451.aspx + */ + public function createPageBlob( + $container, + $blob, + $length, + BlobModels\CreatePageBlobOptions $options = null + ); + + /** + * Creates promise to create a new page blob. Note that calling + * createPageBlob to create a page blob only initializes the blob. + * To add content to a page blob, call createBlobPages method. + * + * @param string $container The container name. + * @param string $blob The blob name. + * @param integer $length Specifies the maximum size + * for the page blob, up to + * 1 TB. The page blob size + * must be aligned to a + * 512-byte boundary. + * @param BlobModels\CreatePageBlobOptions $options The optional parameters. + * + * @return \GuzzleHttp\Promise\PromiseInterface + * + * @see http://msdn.microsoft.com/en-us/library/windowsazure/dd179451.aspx + */ + public function createPageBlobAsync( + $container, + $blob, + $length, + BlobModels\CreatePageBlobOptions $options = null + ); + + /** + * Create a new append blob. + * If the blob already exists on the service, it will be overwritten. + * + * @param string $container The container name. + * @param string $blob The blob name. + * @param BlobModels\CreateBlobOptions $options The optional parameters. + * + * @return BlobModels\PutBlobResult + * + * @see http://msdn.microsoft.com/en-us/library/windowsazure/dd179451.aspx + */ + public function createAppendBlob( + $container, + $blob, + BlobModels\CreateBlobOptions $options = null + ); + + + /** + * Creates promise to create a new append blob. + * If the blob already exists on the service, it will be overwritten. + * + * @param string $container The container name. + * @param string $blob The blob name. + * @param BlobModels\CreateBlobOptions $options The optional parameters. + * + * @return \GuzzleHttp\Promise\PromiseInterface + * + * @see http://msdn.microsoft.com/en-us/library/windowsazure/dd179451.aspx + */ + public function createAppendBlobAsync( + $container, + $blob, + BlobModels\CreateBlobOptions $options = null + ); + + /** + * Creates a new block blob or updates the content of an existing block blob. + * Updating an existing block blob overwrites any existing metadata on the blob. + * Partial updates are not supported with createBlockBlob; the content of the + * existing blob is overwritten with the content of the new blob. To perform a + * partial update of the content of a block blob, use the createBlockList method. + * + * @param string $container name of the container + * @param string $blob name of the blob + * @param string|resource|StreamInterface $content content of the blob + * @param BlobModels\CreateBlockBlobOptions $options optional parameters + * + * @return BlobModels\PutBlobResult + * + * @see http://msdn.microsoft.com/en-us/library/windowsazure/dd179451.aspx + */ + public function createBlockBlob( + $container, + $blob, + $content, + BlobModels\CreateBlockBlobOptions $options = null + ); + + /** + * Creates a promise to create a new block blob or updates the content of + * an existing block blob. + * + * Updating an existing block blob overwrites any existing metadata on the blob. + * Partial updates are not supported with createBlockBlob the content of the + * existing blob is overwritten with the content of the new blob. To perform a + * partial update of the content of a block blob, use the createBlockList + * method. + * + * @param string $container The name of the container. + * @param string $blob The name of the blob. + * @param string|resource|StreamInterface $content The content of the blob. + * @param BlobModels\CreateBlockBlobOptions $options The optional parameters. + * + * @return \GuzzleHttp\Promise\PromiseInterface + * + * @see http://msdn.microsoft.com/en-us/library/windowsazure/dd179451.aspx + */ + public function createBlockBlobAsync( + $container, + $blob, + $content, + BlobModels\CreateBlockBlobOptions $options = null + ); + + /** + * Create a new page blob and upload the content to the page blob. + * + * @param string $container The name of the container. + * @param string $blob The name of the blob. + * @param int $length The length of the blob. + * @param string|resource|StreamInterface $content The content of the blob. + * @param BlobModels\CreatePageBlobFromContentOptions + * $options The optional parameters. + * + * @return BlobModels\GetBlobPropertiesResult + * + * @see https://docs.microsoft.com/en-us/rest/api/storageservices/fileservices/get-blob-properties + */ + public function createPageBlobFromContent( + $container, + $blob, + $length, + $content, + BlobModels\CreatePageBlobFromContentOptions $options = null + ); + + /** + * Creates a promise to create a new page blob and upload the content + * to the page blob. + * + * @param string $container The name of the container. + * @param string $blob The name of the blob. + * @param int $length The length of the blob. + * @param string|resource|StreamInterface $content The content of the blob. + * @param BlobModels\CreatePageBlobFromContentOptions + * $options The optional parameters. + * + * @return \GuzzleHttp\Promise\PromiseInterface + * + * @see https://docs.microsoft.com/en-us/rest/api/storageservices/fileservices/get-blob-properties + */ + public function createPageBlobFromContentAsync( + $container, + $blob, + $length, + $content, + BlobModels\CreatePageBlobFromContentOptions $options = null + ); + + /** + * Clears a range of pages from the blob. + * + * @param string $container name of the container + * @param string $blob name of the blob + * @param Range $range Can be up to the value + * of the blob's full size. + * @param BlobModels\CreateBlobPagesOptions $options optional parameters + * + * @return BlobModels\CreateBlobPagesResult. + * + * @see http://msdn.microsoft.com/en-us/library/windowsazure/ee691975.aspx + */ + public function clearBlobPages( + $container, + $blob, + Range $range, + BlobModels\CreateBlobPagesOptions $options = null + ); + + /** + * Creates promise to clear a range of pages from the blob. + * + * @param string $container name of the container + * @param string $blob name of the blob + * @param Range $range Can be up to the value + * of the blob's full size. + * Note that ranges must be + * aligned to 512 (0-511, + * 512-1023) + * @param BlobModels\CreateBlobPagesOptions $options optional parameters + * + * @return \GuzzleHttp\Promise\PromiseInterface + * + * @see http://msdn.microsoft.com/en-us/library/windowsazure/ee691975.aspx + */ + public function clearBlobPagesAsync( + $container, + $blob, + Range $range, + BlobModels\CreateBlobPagesOptions $options = null + ); + + /** + * Creates a range of pages to a page blob. + * + * @param string $container name of the container + * @param string $blob name of the blob + * @param Range $range Can be up to 4 MB in size + * @param string $content the blob contents + * @param BlobModels\CreateBlobPagesOptions $options optional parameters + * + * @return BlobModels\CreateBlobPagesResult + * + * @see http://msdn.microsoft.com/en-us/library/windowsazure/ee691975.aspx + */ + public function createBlobPages( + $container, + $blob, + Range $range, + $content, + BlobModels\CreateBlobPagesOptions $options = null + ); + + /** + * Creates promise to create a range of pages to a page blob. + * + * @param string $container name of the container + * @param string $blob name of the blob + * @param Range $range Can be up to 4 MB in + * size. Note that ranges + * must be aligned to 512 + * (0-511, 512-1023) + * @param string|resource|StreamInterface $content the blob contents. + * @param BlobModels\CreateBlobPagesOptions $options optional parameters + * + * @return \GuzzleHttp\Promise\PromiseInterface + * + * @see http://msdn.microsoft.com/en-us/library/windowsazure/ee691975.aspx + */ + public function createBlobPagesAsync( + $container, + $blob, + Range $range, + $content, + BlobModels\CreateBlobPagesOptions $options = null + ); + + /** + * Creates a new block to be committed as part of a block blob. + * + * @param string $container name of the container + * @param string $blob name of the blob + * @param string $blockId must be less than or equal to + * 64 bytes in size. For a given blob, the length of the value specified for the + * blockid parameter must be the same size for each block. + * @param string $content the blob block contents + * @param BlobModels\CreateBlobBlockOptions $options optional parameters + * + * @return void + * + * @see http://msdn.microsoft.com/en-us/library/windowsazure/dd135726.aspx + */ + public function createBlobBlock( + $container, + $blob, + $blockId, + $content, + BlobModels\CreateBlobBlockOptions $options = null + ); + + /** + * Creates a new block to be committed as part of a block blob. + * + * @param string $container name of the container + * @param string $blob name of the blob + * @param string $blockId must be less than or + * equal to 64 bytes in + * size. For a given + * blob, the length of + * the value specified + * for the blockid + * parameter must + * be the same size for + * each block. + * @param resource|string|StreamInterface $content the blob block contents + * @param BlobModels\CreateBlobBlockOptions $options optional parameters + * + * @return \GuzzleHttp\Promise\PromiseInterface + * + * @see http://msdn.microsoft.com/en-us/library/windowsazure/dd135726.aspx + */ + public function createBlobBlockAsync( + $container, + $blob, + $blockId, + $content, + BlobModels\CreateBlobBlockOptions $options = null + ); + + /** + * Commits a new block of data to the end of an existing append blob. + * + * @param string $container name of the container + * @param string $blob name of the blob + * @param resource|string|StreamInterface $content the blob block contents + * @param BlobModels\AppendBlockOptions $options optional parameters + * + * @return BlobModels\AppendBlockResult + * + * @see https://docs.microsoft.com/en-us/rest/api/storageservices/fileservices/append-block + */ + public function appendBlock( + $container, + $blob, + $content, + BlobModels\AppendBlockOptions $options = null + ); + + /** + * Creates promise to commit a new block of data to the end of an existing append blob. + * + * @param string $container name of the container + * @param string $blob name of the blob + * @param resource|string|StreamInterface $content the blob block contents + * @param BlobModels\AppendBlockOptions $options optional parameters + * + * @return \GuzzleHttp\Promise\PromiseInterface + * + * @see https://docs.microsoft.com/en-us/rest/api/storageservices/fileservices/append-block + */ + public function appendBlockAsync( + $container, + $blob, + $content, + BlobModels\AppendBlockOptions $options = null + ); + + /** + * This method writes a blob by specifying the list of block IDs that make up the + * blob. In order to be written as part of a blob, a block must have been + * successfully written to the server in a prior createBlobBlock method. + * + * You can call Put Block List to update a blob by uploading only those blocks + * that have changed, then committing the new and existing blocks together. + * You can do this by specifying whether to commit a block from the committed + * block list or from the uncommitted block list, or to commit the most recently + * uploaded version of the block, whichever list it may belong to. + * + * @param string $container name of the container + * @param string $blob name of the blob + * @param BlobModels\BlockList|BlobModels\Block[] $blockList the block list entries + * @param BlobModels\CommitBlobBlocksOptions $options optional parameters + * + * @return BlobModels\PutBlobResult + * + * @see http://msdn.microsoft.com/en-us/library/windowsazure/dd179467.aspx + */ + public function commitBlobBlocks( + $container, + $blob, + $blockList, + BlobModels\CommitBlobBlocksOptions $options = null + ); + + /** + * This method writes a blob by specifying the list of block IDs that make up the + * blob. In order to be written as part of a blob, a block must have been + * successfully written to the server in a prior createBlobBlock method. + * + * You can call Put Block List to update a blob by uploading only those blocks + * that have changed, then committing the new and existing blocks together. + * You can do this by specifying whether to commit a block from the committed + * block list or from the uncommitted block list, or to commit the most recently + * uploaded version of the block, whichever list it may belong to. + * + * @param string $container name of the container + * @param string $blob name of the blob + * @param BlobModels\BlockList|BlobModels\Block[] $blockList the block list + * entries + * @param BlobModels\CommitBlobBlocksOptions $options optional parameters + * + * @return \GuzzleHttp\Promise\PromiseInterface + * + * @see http://msdn.microsoft.com/en-us/library/windowsazure/dd179467.aspx + */ + public function commitBlobBlocksAsync( + $container, + $blob, + $blockList, + BlobModels\CommitBlobBlocksOptions $options = null + ); + + /** + * Retrieves the list of blocks that have been uploaded as part of a block blob. + * + * There are two block lists maintained for a blob: + * 1) Committed Block List: The list of blocks that have been successfully + * committed to a given blob with commitBlobBlocks. + * 2) Uncommitted Block List: The list of blocks that have been uploaded for a + * blob using Put Block (REST API), but that have not yet been committed. + * These blocks are stored in Windows Azure in association with a blob, but do + * not yet form part of the blob. + * + * @param string $container name of the container + * @param string $blob name of the blob + * @param BlobModels\ListBlobBlocksOptions $options optional parameters + * + * @return BlobModels\ListBlobBlocksResult + * + * @see http://msdn.microsoft.com/en-us/library/windowsazure/dd179400.aspx + */ + public function listBlobBlocks( + $container, + $blob, + BlobModels\ListBlobBlocksOptions $options = null + ); + + /** + * Creates promise to retrieve the list of blocks that have been uploaded as + * part of a block blob. + * + * There are two block lists maintained for a blob: + * 1) Committed Block List: The list of blocks that have been successfully + * committed to a given blob with commitBlobBlocks. + * 2) Uncommitted Block List: The list of blocks that have been uploaded for a + * blob using Put Block (REST API), but that have not yet been committed. + * These blocks are stored in Windows Azure in association with a blob, but do + * not yet form part of the blob. + * + * @param string $container name of the container + * @param string $blob name of the blob + * @param BlobModels\ListBlobBlocksOptions $options optional parameters + * + * @return \GuzzleHttp\Promise\PromiseInterface + * + * @see http://msdn.microsoft.com/en-us/library/windowsazure/dd179400.aspx + */ + public function listBlobBlocksAsync( + $container, + $blob, + BlobModels\ListBlobBlocksOptions $options = null + ); + + /** + * Returns all properties and metadata on the blob. + * + * @param string $container name of the container + * @param string $blob name of the blob + * @param BlobModels\GetBlobPropertiesOptions $options optional parameters + * + * @return BlobModels\GetBlobPropertiesResult + * + * @see http://msdn.microsoft.com/en-us/library/windowsazure/dd179394.aspx + */ + public function getBlobProperties( + $container, + $blob, + BlobModels\GetBlobPropertiesOptions $options = null + ); + + /** + * Creates promise to return all properties and metadata on the blob. + * + * @param string $container name of the container + * @param string $blob name of the blob + * @param BlobModels\GetBlobPropertiesOptions $options optional parameters + * + * @return \GuzzleHttp\Promise\PromiseInterface + * + * @see http://msdn.microsoft.com/en-us/library/windowsazure/dd179394.aspx + */ + public function getBlobPropertiesAsync( + $container, + $blob, + BlobModels\GetBlobPropertiesOptions $options = null + ); + + /** + * Returns all properties and metadata on the blob. + * + * @param string $container name of the container + * @param string $blob name of the blob + * @param BlobModels\GetBlobMetadataOptions $options optional parameters + * + * @return BlobModels\GetBlobMetadataResult + * + * @see http://msdn.microsoft.com/en-us/library/windowsazure/dd179350.aspx + */ + public function getBlobMetadata( + $container, + $blob, + BlobModels\GetBlobMetadataOptions $options = null + ); + + /** + * Creates promise to return all properties and metadata on the blob. + * + * @param string $container name of the container + * @param string $blob name of the blob + * @param BlobModels\GetBlobMetadataOptions $options optional parameters + * + * @return \GuzzleHttp\Promise\PromiseInterface + * + * @see http://msdn.microsoft.com/en-us/library/windowsazure/dd179350.aspx + */ + public function getBlobMetadataAsync( + $container, + $blob, + BlobModels\GetBlobMetadataOptions $options = null + ); + + /** + * Returns a list of active page ranges for a page blob. Active page ranges are + * those that have been populated with data. + * + * @param string $container name of the container + * @param string $blob name of the blob + * @param BlobModels\ListPageBlobRangesOptions $options optional parameters + * + * @return BlobModels\ListPageBlobRangesResult + * + * @see http://msdn.microsoft.com/en-us/library/windowsazure/ee691973.aspx + */ + public function listPageBlobRanges( + $container, + $blob, + BlobModels\ListPageBlobRangesOptions $options = null + ); + + /** + * Creates promise to return a list of active page ranges for a page blob. + * Active page ranges are those that have been populated with data. + * + * @param string $container name of the + * container + * @param string $blob name of the blob + * @param BlobModels\ListPageBlobRangesOptions $options optional parameters + * + * @return \GuzzleHttp\Promise\PromiseInterface + * + * @see http://msdn.microsoft.com/en-us/library/windowsazure/ee691973.aspx + */ + public function listPageBlobRangesAsync( + $container, + $blob, + BlobModels\ListPageBlobRangesOptions $options = null + ); + + /** + * Returns a list of page ranges that have been updated or cleared. + * + * Returns a list of page ranges that have been updated or cleared since + * the snapshot specified by `previousSnapshotTime`. Gets all of the page + * ranges by default, or only the page ranges over a specific range of + * bytes if `rangeStart` and `rangeEnd` in the `options` are specified. + * + * @param string $container name of the container + * @param string $blob name of the blob + * @param string $previousSnapshotTime previous snapshot time + * for comparison which + * should be prior to the + * snapshot time defined + * in `options` + * @param BlobModels\ListPageBlobRangesOptions $options optional parameters + * + * @return BlobModels\ListPageBlobRangesDiffResult + * + * @see https://docs.microsoft.com/en-us/rest/api/storageservices/version-2015-07-08 + */ + public function listPageBlobRangesDiff( + $container, + $blob, + $previousSnapshotTime, + BlobModels\ListPageBlobRangesOptions $options = null + ); + + /** + * Creates promise to return a list of page ranges that have been updated + * or cleared. + * + * Creates promise to return a list of page ranges that have been updated + * or cleared since the snapshot specified by `previousSnapshotTime`. Gets + * all of the page ranges by default, or only the page ranges over a specific + * range of bytes if `rangeStart` and `rangeEnd` in the `options` are specified. + * + * @param string $container name of the container + * @param string $blob name of the blob + * @param string $previousSnapshotTime previous snapshot time + * for comparison which + * should be prior to the + * snapshot time defined + * in `options` + * @param BlobModels\ListPageBlobRangesOptions $options optional parameters + * + * @return \GuzzleHttp\Promise\PromiseInterface + * + * @see http://msdn.microsoft.com/en-us/library/windowsazure/ee691973.aspx + */ + public function listPageBlobRangesDiffAsync( + $container, + $blob, + $previousSnapshotTime, + BlobModels\ListPageBlobRangesOptions $options = null + ); + + /** + * Sets blob tier on the blob. + * + * @param string $container name + * @param string $blob name of the blob + * @param BlobModels\SetBlobTierOptions $options optional parameters + * + * @return void + * + * @see https://docs.microsoft.com/en-us/rest/api/storageservices/set-blob-tier + */ + public function setBlobTier( + $container, + $blob, + BlobModels\SetBlobTierOptions $options = null + ); + + /** + * Sets blob tier on the blob. + * + * @param string $container name + * @param string $blob name of the blob + * @param BlobModels\SetBlobTierOptions $options optional parameters + * + * @return \GuzzleHttp\Promise\PromiseInterface + * + * @see https://docs.microsoft.com/en-us/rest/api/storageservices/set-blob-tier + */ + public function setBlobTierAsync( + $container, + $blob, + BlobModels\SetBlobTierOptions $options = null + ); + + /** + * Sets system properties defined for a blob. + * + * @param string $container name of the container + * @param string $blob name of the blob + * @param BlobModels\SetBlobPropertiesOptions $options optional parameters + * + * @return BlobModels\SetBlobPropertiesResult + * + * @see http://msdn.microsoft.com/en-us/library/windowsazure/ee691966.aspx + */ + public function setBlobProperties( + $container, + $blob, + BlobModels\SetBlobPropertiesOptions $options = null + ); + + /** + * Creates promise to set system properties defined for a blob. + * + * @param string $container name of the container + * @param string $blob name of the blob + * @param BlobModels\SetBlobPropertiesOptions $options optional parameters + * + * @return \GuzzleHttp\Promise\PromiseInterface + * + * @see http://msdn.microsoft.com/en-us/library/windowsazure/ee691966.aspx + */ + public function setBlobPropertiesAsync( + $container, + $blob, + BlobModels\SetBlobPropertiesOptions $options = null + ); + + /** + * Sets metadata headers on the blob. + * + * @param string $container name of the container + * @param string $blob name of the blob + * @param array $metadata key/value pair representation + * @param BlobModels\BlobServiceOptions $options optional parameters + * + * @return BlobModels\SetBlobMetadataResult + * + * @see http://msdn.microsoft.com/en-us/library/windowsazure/dd179414.aspx + */ + public function setBlobMetadata( + $container, + $blob, + array $metadata, + BlobModels\BlobServiceOptions $options = null + ); + + /** + * Creates promise to set metadata headers on the blob. + * + * @param string $container name of the container + * @param string $blob name of the blob + * @param array $metadata key/value pair representation + * @param BlobModels\BlobServiceOptions $options optional parameters + * + * @return \GuzzleHttp\Promise\PromiseInterface + * + * @see http://msdn.microsoft.com/en-us/library/windowsazure/dd179414.aspx + */ + public function setBlobMetadataAsync( + $container, + $blob, + array $metadata, + BlobModels\BlobServiceOptions $options = null + ); + + /** + * Downloads a blob to a file, the result contains its metadata and + * properties. The result will not contain a stream pointing to the + * content of the file. + * + * @param string $path The path and name of the file + * @param string $container name of the container + * @param string $blob name of the blob + * @param BlobModels\GetBlobOptions $options optional parameters + * + * @return BlobModels\GetBlobResult + * + * @see http://msdn.microsoft.com/en-us/library/windowsazure/dd179440.aspx + */ + public function saveBlobToFile( + $path, + $container, + $blob, + BlobModels\GetBlobOptions $options = null + ); + + /** + * Creates promise to download a blob to a file, the result contains its + * metadata and properties. The result will not contain a stream pointing + * to the content of the file. + * + * @param string $path The path and name of the file + * @param string $container name of the container + * @param string $blob name of the blob + * @param BlobModels\GetBlobOptions $options optional parameters + * + * @return \GuzzleHttp\Promise\PromiseInterface + * @throws \Exception + * @see http://msdn.microsoft.com/en-us/library/windowsazure/dd179440.aspx + */ + public function saveBlobToFileAsync( + $path, + $container, + $blob, + BlobModels\GetBlobOptions $options = null + ); + + /** + * Undeletes a blob. + * + * @param string $container name of the container + * @param string $blob name of the blob + * @param BlobModels\UndeleteBlobOptions $options optional parameters + * + * @return void + * + * @see https://docs.microsoft.com/en-us/rest/api/storageservices/undelete-blob + */ + public function undeleteBlob( + $container, + $blob, + BlobModels\UndeleteBlobOptions $options = null + ); + + /** + * Undeletes a blob. + * + * @param string $container name of the container + * @param string $blob name of the blob + * @param BlobModels\UndeleteBlobOptions $options optional parameters + * + * @return \GuzzleHttp\Promise\PromiseInterface + * + * @see https://docs.microsoft.com/en-us/rest/api/storageservices/undelete-blob + */ + public function undeleteBlobAsync( + $container, + $blob, + BlobModels\UndeleteBlobOptions $options = null + ); + + /** + * Reads or downloads a blob from the system, including its metadata and + * properties. + * + * @param string $container name of the container + * @param string $blob name of the blob + * @param BlobModels\GetBlobOptions $options optional parameters + * + * @return BlobModels\GetBlobResult + * + * @see http://msdn.microsoft.com/en-us/library/windowsazure/dd179440.aspx + */ + public function getBlob( + $container, + $blob, + BlobModels\GetBlobOptions $options = null + ); + + /** + * Creates promise to read or download a blob from the system, including its + * metadata and properties. + * + * @param string $container name of the container + * @param string $blob name of the blob + * @param BlobModels\GetBlobOptions $options optional parameters + * + * @return \GuzzleHttp\Promise\PromiseInterface + * + * @see http://msdn.microsoft.com/en-us/library/windowsazure/dd179440.aspx + */ + public function getBlobAsync( + $container, + $blob, + BlobModels\GetBlobOptions $options = null + ); + + /** + * Deletes a blob or blob snapshot. + * + * Note that if the snapshot entry is specified in the $options then only this + * blob snapshot is deleted. To delete all blob snapshots, do not set Snapshot + * and just set getDeleteSnaphotsOnly to true. + * + * @param string $container name of the container + * @param string $blob name of the blob + * @param BlobModels\DeleteBlobOptions $options optional parameters + * + * @return void + * + * @see http://msdn.microsoft.com/en-us/library/windowsazure/dd179413.aspx + */ + public function deleteBlob( + $container, + $blob, + BlobModels\DeleteBlobOptions $options = null + ); + + /** + * Creates promise to delete a blob or blob snapshot. + * + * Note that if the snapshot entry is specified in the $options then only this + * blob snapshot is deleted. To delete all blob snapshots, do not set Snapshot + * and just set getDeleteSnaphotsOnly to true. + * + * @param string $container name of the container + * @param string $blob name of the blob + * @param BlobModels\DeleteBlobOptions $options optional parameters + * + * @return \GuzzleHttp\Promise\PromiseInterface + * + * @see http://msdn.microsoft.com/en-us/library/windowsazure/dd179413.aspx + */ + public function deleteBlobAsync( + $container, + $blob, + BlobModels\DeleteBlobOptions $options = null + ); + + /** + * Creates a snapshot of a blob. + * + * @param string $container name of the container + * @param string $blob name of the blob + * @param BlobModels\CreateBlobSnapshotOptions $options optional parameters + * + * @return BlobModels\CreateBlobSnapshotResult + * + * @see http://msdn.microsoft.com/en-us/library/windowsazure/ee691971.aspx + */ + public function createBlobSnapshot( + $container, + $blob, + BlobModels\CreateBlobSnapshotOptions $options = null + ); + + /** + * Creates promise to create a snapshot of a blob. + * + * @param string $container The name of the + * container. + * @param string $blob The name of the + * blob. + * @param BlobModels\CreateBlobSnapshotOptions $options The optional + * parameters. + * + * @return \GuzzleHttp\Promise\PromiseInterface + * + * @see http://msdn.microsoft.com/en-us/library/windowsazure/ee691971.aspx + */ + public function createBlobSnapshotAsync( + $container, + $blob, + BlobModels\CreateBlobSnapshotOptions $options = null + ); + + /** + * Copies a source blob to a destination blob within the same storage account. + * + * @param string $destinationContainer name of container + * @param string $destinationBlob name of blob + * @param string $sourceContainer name of container + * @param string $sourceBlob name of blob + * @param BlobModels\CopyBlobOptions $options optional parameters + * + * @return BlobModels\CopyBlobResult + * + * @see http://msdn.microsoft.com/en-us/library/windowsazure/dd894037.aspx + */ + public function copyBlob( + $destinationContainer, + $destinationBlob, + $sourceContainer, + $sourceBlob, + BlobModels\CopyBlobOptions $options = null + ); + + /** + * Creates promise to copy a source blob to a destination blob within the + * same storage account. + * + * @param string $destinationContainer name of the + * destination + * container + * @param string $destinationBlob name of the + * destination blob + * @param string $sourceContainer name of the source + * container + * @param string $sourceBlob name of the source + * blob + * @param BlobModels\CopyBlobOptions $options optional parameters + * + * @return \GuzzleHttp\Promise\PromiseInterface + * + * @see http://msdn.microsoft.com/en-us/library/windowsazure/dd894037.aspx + */ + public function copyBlobAsync( + $destinationContainer, + $destinationBlob, + $sourceContainer, + $sourceBlob, + BlobModels\CopyBlobOptions $options = null + ); + + /** + * Copies from a source URL to a destination blob. + * + * @param string $destinationContainer name of the + * destination + * container + * @param string $destinationBlob name of the + * destination + * blob + * @param string $sourceURL URL of the + * source + * resource + * @param BlobModels\CopyBlobFromURLOptions $options optional + * parameters + * + * @return BlobModels\CopyBlobResult + * + * @see http://msdn.microsoft.com/en-us/library/windowsazure/dd894037.aspx + */ + public function copyBlobFromURL( + $destinationContainer, + $destinationBlob, + $sourceURL, + BlobModels\CopyBlobFromURLOptions $options = null + ); + + /** + * Creates promise to copy from source URL to a destination blob. + * + * @param string $destinationContainer name of the + * destination + * container + * @param string $destinationBlob name of the + * destination + * blob + * @param string $sourceURL URL of the + * source + * resource + * @param BlobModels\CopyBlobFromURLOptions $options optional + * parameters + * + * @return \GuzzleHttp\Promise\PromiseInterface + * + * @see http://msdn.microsoft.com/en-us/library/windowsazure/dd894037.aspx + */ + public function copyBlobFromURLAsync( + $destinationContainer, + $destinationBlob, + $sourceURL, + BlobModels\CopyBlobFromURLOptions $options = null + ); + + /** + * Abort a blob copy operation + * + * @param string $container name of the container + * @param string $blob name of the blob + * @param string $copyId copy operation identifier. + * @param BlobModels\BlobServiceOptions $options optional parameters + * + * @return void + * + * @see https://docs.microsoft.com/en-us/rest/api/storageservices/fileservices/abort-copy-blob + */ + public function abortCopy( + $container, + $blob, + $copyId, + BlobModels\BlobServiceOptions $options = null + ); + + /** + * Creates promise to abort a blob copy operation + * + * @param string $container name of the container + * @param string $blob name of the blob + * @param string $copyId copy operation identifier. + * @param BlobModels\BlobServiceOptions $options optional parameters + * + * @return \GuzzleHttp\Promise\PromiseInterface + * + * @see https://docs.microsoft.com/en-us/rest/api/storageservices/fileservices/abort-copy-blob + */ + public function abortCopyAsync( + $container, + $blob, + $copyId, + BlobModels\BlobServiceOptions $options = null + ); + + /** + * Establishes an exclusive write lock on a blob. To write to a locked + * blob, a client must provide a lease ID. + * + * @param string $container name of the container + * @param string $blob name of the blob + * @param string $proposedLeaseId lease id when acquiring + * @param int $leaseDuration the lease duration. A non-infinite + * lease can be between 15 and 60 seconds. + * Default is never to expire. + * @param BlobModels\BlobServiceOptions $options optional parameters + * + * @return BlobModels\LeaseResult + * + * @see http://msdn.microsoft.com/en-us/library/windowsazure/ee691972.aspx + */ + public function acquireLease( + $container, + $blob, + $proposedLeaseId = null, + $leaseDuration = null, + BlobModels\BlobServiceOptions $options = null + ); + + /** + * Creates promise to establish an exclusive one-minute write lock on a blob. + * To write to a locked blob, a client must provide a lease ID. + * + * @param string $container name of the container + * @param string $blob name of the blob + * @param string $proposedLeaseId lease id when acquiring + * @param int $leaseDuration the lease duration. A non-infinite + * lease can be between 15 and 60 seconds. + * Default is never to expire. + * @param BlobModels\BlobServiceOptions $options optional parameters + * + * @return \GuzzleHttp\Promise\PromiseInterface + * + * @see http://msdn.microsoft.com/en-us/library/windowsazure/ee691972.aspx + */ + public function acquireLeaseAsync( + $container, + $blob, + $proposedLeaseId = null, + $leaseDuration = null, + BlobModels\BlobServiceOptions $options = null + ); + + /** + * change an existing lease + * + * @param string $container name of the container + * @param string $blob name of the blob + * @param string $leaseId lease id when acquiring + * @param string $proposedLeaseId lease id when acquiring + * @param BlobModels\BlobServiceOptions $options optional parameters + * + * @return BlobModels\LeaseResult + * + * @see https://docs.microsoft.com/en-us/rest/api/storageservices/fileservices/lease-blob + */ + public function changeLease( + $container, + $blob, + $leaseId, + $proposedLeaseId, + BlobModels\BlobServiceOptions $options = null + ); + + /** + * Creates promise to change an existing lease + * + * @param string $container name of the container + * @param string $blob name of the blob + * @param string $leaseId lease id when acquiring + * @param string $proposedLeaseId the proposed lease id + * @param BlobModels\BlobServiceOptions $options optional parameters + * + * @return \GuzzleHttp\Promise\PromiseInterface + * + * @see https://docs.microsoft.com/en-us/rest/api/storageservices/fileservices/lease-blob + */ + public function changeLeaseAsync( + $container, + $blob, + $leaseId, + $proposedLeaseId, + BlobModels\BlobServiceOptions $options = null + ); + + /** + * Renews an existing lease + * + * @param string $container name of the container + * @param string $blob name of the blob + * @param string $leaseId lease id when acquiring + * @param BlobModels\BlobServiceOptions $options optional parameters + * + * @return BlobModels\AcquireLeaseResult + * + * @see http://msdn.microsoft.com/en-us/library/windowsazure/ee691972.aspx + */ + public function renewLease( + $container, + $blob, + $leaseId, + BlobModels\BlobServiceOptions $options = null + ); + + /** + * Creates promise to renew an existing lease + * + * @param string $container name of the container + * @param string $blob name of the blob + * @param string $leaseId lease id when acquiring + * @param BlobModels\BlobServiceOptions $options optional parameters + * + * @return \GuzzleHttp\Promise\PromiseInterface + * + * @see http://msdn.microsoft.com/en-us/library/windowsazure/ee691972.aspx + */ + public function renewLeaseAsync( + $container, + $blob, + $leaseId, + BlobModels\BlobServiceOptions $options = null + ); + + + /** + * Frees the lease if it is no longer needed so that another client may + * immediately acquire a lease against the blob. + * + * @param string $container name of the container + * @param string $blob name of the blob + * @param string $leaseId lease id when acquiring + * @param BlobModels\BlobServiceOptions $options optional parameters + * + * @return void + * + * @see http://msdn.microsoft.com/en-us/library/windowsazure/ee691972.aspx + */ + public function releaseLease( + $container, + $blob, + $leaseId, + BlobModels\BlobServiceOptions $options = null + ); + + /** + * Creates promise to free the lease if it is no longer needed so that + * another client may immediately acquire a lease against the blob. + * + * @param string $container name of the container + * @param string $blob name of the blob + * @param string $leaseId lease id when acquiring + * @param BlobModels\BlobServiceOptions $options optional parameters + * + * @return \GuzzleHttp\Promise\PromiseInterface + * + * @see http://msdn.microsoft.com/en-us/library/windowsazure/ee691972.aspx + */ + public function releaseLeaseAsync( + $container, + $blob, + $leaseId, + BlobModels\BlobServiceOptions $options = null + ); + + /** + * Ends the lease but ensure that another client cannot acquire a new lease until + * the current lease period has expired. + * + * @param string $container name of the container + * @param string $blob name of the blob + * @param BlobModels\BlobServiceOptions $options optional parameters + * + * @return void + * + * @see http://msdn.microsoft.com/en-us/library/windowsazure/ee691972.aspx + */ + public function breakLease( + $container, + $blob, + $breakPeriod = null, + BlobModels\BlobServiceOptions $options = null + ); + + /** + * Creates promise to end the lease but ensure that another client cannot + * acquire a new lease until the current lease period has expired. + * + * @param string $container name of the container + * @param string $blob name of the blob + * @param BlobModels\BlobServiceOptions $options optional parameters + * + * @return \GuzzleHttp\Promise\PromiseInterface + * + * @see http://msdn.microsoft.com/en-us/library/windowsazure/ee691972.aspx + */ + public function breakLeaseAsync( + $container, + $blob, + $breakPeriod = null, + BlobModels\BlobServiceOptions $options = null + ); +} diff --git a/3rdparty/microsoft/azure-storage-blob/src/Blob/Models/AccessCondition.php b/3rdparty/microsoft/azure-storage-blob/src/Blob/Models/AccessCondition.php new file mode 100644 index 00000000..6daae088 --- /dev/null +++ b/3rdparty/microsoft/azure-storage-blob/src/Blob/Models/AccessCondition.php @@ -0,0 +1,355 @@ + + * @copyright 2016 Microsoft Corporation + * @license https://github.com/azure/azure-storage-php/LICENSE + * @link https://github.com/azure/azure-storage-php + */ + +namespace MicrosoftAzure\Storage\Blob\Models; + +use MicrosoftAzure\Storage\Blob\Internal\BlobResources as Resources; +use MicrosoftAzure\Storage\Common\Internal\Validate; +use MicrosoftAzure\Storage\Common\Internal\WindowsAzureUtilities; + +/** + * Represents a set of access conditions to be used for operations against the + * storage services. + * + * @category Microsoft + * @package MicrosoftAzure\Storage\Blob\Models + * @author Azure Storage PHP SDK + * @copyright 2016 Microsoft Corporation + * @license https://github.com/azure/azure-storage-php/LICENSE + * @link https://github.com/azure/azure-storage-php + */ +class AccessCondition +{ + private $_header = Resources::EMPTY_STRING; + private $_value; + + /** + * Constructor + * + * @param string $headerType header name + * @param string $value header value + * + * @internal + */ + protected function __construct($headerType, $value) + { + $this->setHeader($headerType); + $this->setValue($value); + } + + /** + * Specifies that no access condition is set. + * + * @return \MicrosoftAzure\Storage\Blob\Models\AccessCondition + */ + public static function none() + { + return new AccessCondition(Resources::EMPTY_STRING, null); + } + + /** + * Returns an access condition such that an operation will be performed only if + * the resource's ETag value matches the specified ETag value. + *

+ * Setting this access condition modifies the request to include the HTTP + * If-Match conditional header. If this access condition is set, the + * operation is performed only if the ETag of the resource matches the specified + * ETag. + *

+ * For more information, see + * + * Specifying Conditional Headers for Blob Service Operations. + * + * @param string $etag a string that represents the ETag value to check. + * + * @return \MicrosoftAzure\Storage\Blob\Models\AccessCondition + */ + public static function ifMatch($etag) + { + return new AccessCondition(Resources::IF_MATCH, $etag); + } + + /** + * Returns an access condition such that an operation will be performed only if + * the resource has been modified since the specified time. + *

+ * Setting this access condition modifies the request to include the HTTP + * If-Modified-Since conditional header. If this access condition is set, + * the operation is performed only if the resource has been modified since the + * specified time. + *

+ * For more information, see + * + * Specifying Conditional Headers for Blob Service Operations. + * + * @param \DateTime $lastModified date that represents the last-modified + * time to check for the resource. + * + * @return \MicrosoftAzure\Storage\Blob\Models\AccessCondition + */ + public static function ifModifiedSince(\DateTime $lastModified) + { + Validate::isDate($lastModified); + return new AccessCondition( + Resources::IF_MODIFIED_SINCE, + $lastModified + ); + } + + /** + * Returns an access condition such that an operation will be performed only if + * the resource's ETag value does not match the specified ETag value. + *

+ * Setting this access condition modifies the request to include the HTTP + * If-None-Match conditional header. If this access condition is set, the + * operation is performed only if the ETag of the resource does not match the + * specified ETag. + *

+ * For more information, + * see + * Specifying Conditional Headers for Blob Service Operations. + * + * @param string $etag string that represents the ETag value to check. + * + * @return \MicrosoftAzure\Storage\Blob\Models\AccessCondition + */ + public static function ifNoneMatch($etag) + { + return new AccessCondition(Resources::IF_NONE_MATCH, $etag); + } + + /** + * Returns an access condition such that an operation will be performed only if + * the resource has not been modified since the specified time. + *

+ * Setting this access condition modifies the request to include the HTTP + * If-Unmodified-Since conditional header. If this access condition is + * set, the operation is performed only if the resource has not been modified + * since the specified time. + *

+ * For more information, see + * + * Specifying Conditional Headers for Blob Service Operations. + * + * @param \DateTime $lastModified date that represents the last-modified + * time to check for the resource. + * + * @return \MicrosoftAzure\Storage\Blob\Models\AccessCondition + */ + public static function ifNotModifiedSince(\DateTime $lastModified) + { + Validate::isDate($lastModified); + return new AccessCondition( + Resources::IF_UNMODIFIED_SINCE, + $lastModified + ); + } + + /** + * Returns an access condition such that an operation will be performed only if + * the operation would cause the blob to exceed that limit or if the append + * position is equal to this number. + *

+ * Setting this access condition modifies the request to include the HTTP + * x-ms-blob-condition-appendpos conditional header. If this access condition + * is set, the operation is performed only if the append position is equal to this number + *

+ * For more information, + * see + * Specifying Conditional Headers for Blob Service Operations. + * + * @param int $appendPosition int that represents the append position + * + * @return \MicrosoftAzure\Storage\Blob\Models\AccessCondition + */ + public static function appendPosition($appendPosition) + { + return new AccessCondition(Resources::MAX_APPEND_POSITION, $appendPosition); + } + + /** + * Returns an access condition such that an operation will be performed only if + * the operation would cause the blob to exceed that limit or if the blob size + * is already greater than the value specified in this header. + *

+ * Setting this access condition modifies the request to include the HTTP + * x-ms-blob-condition-maxsize conditional header. If this access condition + * is set, the operation is performed only if the operation would cause the blob + * to exceed that limit or if the blob size is already greater than the value + * specified in this header. + *

+ * For more information, + * see + * Specifying Conditional Headers for Blob Service Operations. + * + * @param int $maxBlobSize int that represents the max blob size + * + * @return \MicrosoftAzure\Storage\Blob\Models\AccessCondition + */ + public static function maxBlobSize($maxBlobSize) + { + return new AccessCondition(Resources::MAX_BLOB_SIZE, $maxBlobSize); + } + + /** + * Returns an access condition such that an operation will be performed only if + * the blob’s sequence number is less than the specified value. + *

+ * Setting this access condition modifies the request to include the HTTP + * x-ms-if-sequence-number-lt conditional header. If this access condition + * is set, the operation is performed only if the blob’s sequence number is less + * than the specified value. + *

+ * For more information, + * see + * Specifying Conditional Headers for Blob Service Operations. + * + * @param int $sequenceNumber int that represents the sequence number value to check. + * + * @return \MicrosoftAzure\Storage\Blob\Models\AccessCondition + */ + public static function ifSequenceNumberLessThan($sequenceNumber) + { + return new AccessCondition(Resources::SEQUENCE_NUMBER_LESS_THAN, $sequenceNumber); + } + + /** + * Returns an access condition such that an operation will be performed only if + * the blob’s sequence number is equal to the specified value. + *

+ * Setting this access condition modifies the request to include the HTTP + * x-ms-if-sequence-number-eq conditional header. If this access condition + * is set, the operation is performed only if the blob’s sequence number is equal to + * the specified value. + *

+ * For more information, + * see + * Specifying Conditional Headers for Blob Service Operations. + * + * @param int $sequenceNumber int that represents the sequence number value to check. + * + * @return \MicrosoftAzure\Storage\Blob\Models\AccessCondition + */ + public static function ifSequenceNumberEqual($sequenceNumber) + { + return new AccessCondition(Resources::SEQUENCE_NUMBER_EQUAL, $sequenceNumber); + } + + /** + * Returns an access condition such that an operation will be performed only if + * the blob’s sequence number is less than or equal to the specified value. + *

+ * Setting this access condition modifies the request to include the HTTP + * x-ms-if-sequence-number-le conditional header. If this access condition + * is set, the operation is performed only if the blob’s sequence number is less + * than or equal to the specified value. + *

+ * For more information, + * see + * Specifying Conditional Headers for Blob Service Operations. + * + * @param int $sequenceNumber int that represents the sequence number value to check. + * + * @return \MicrosoftAzure\Storage\Blob\Models\AccessCondition + */ + public static function ifSequenceNumberLessThanOrEqual($sequenceNumber) + { + return new AccessCondition(Resources::SEQUENCE_NUMBER_LESS_THAN_OR_EQUAL, $sequenceNumber); + } + + /** + * Sets header type + * + * @param string $headerType can be one of Resources + * + * @return void + */ + public function setHeader($headerType) + { + $valid = AccessCondition::isValid($headerType); + Validate::isTrue($valid, Resources::INVALID_HT_MSG); + + $this->_header = $headerType; + } + + /** + * Gets header type + * + * @return string + */ + public function getHeader() + { + return $this->_header; + } + + /** + * Sets the header value + * + * @param string $value the value to use + * + * @return void + */ + public function setValue($value) + { + $this->_value = $value; + } + + /** + * Gets the header value + * + * @return string + */ + public function getValue() + { + return $this->_value; + } + + /** + * Check if the $headerType belongs to valid header types + * + * @param string $headerType candidate header type + * + * @internal + * + * @return boolean + */ + public static function isValid($headerType) + { + if ($headerType == Resources::EMPTY_STRING + || $headerType == Resources::IF_UNMODIFIED_SINCE + || $headerType == Resources::IF_MATCH + || $headerType == Resources::IF_MODIFIED_SINCE + || $headerType == Resources::IF_NONE_MATCH + || $headerType == Resources::MAX_BLOB_SIZE + || $headerType == Resources::MAX_APPEND_POSITION + || $headerType == Resources::SEQUENCE_NUMBER_LESS_THAN_OR_EQUAL + || $headerType == Resources::SEQUENCE_NUMBER_LESS_THAN + || $headerType == Resources::SEQUENCE_NUMBER_EQUAL + ) { + return true; + } else { + return false; + } + } +} diff --git a/3rdparty/microsoft/azure-storage-blob/src/Blob/Models/AccessTierTrait.php b/3rdparty/microsoft/azure-storage-blob/src/Blob/Models/AccessTierTrait.php new file mode 100644 index 00000000..10f0c142 --- /dev/null +++ b/3rdparty/microsoft/azure-storage-blob/src/Blob/Models/AccessTierTrait.php @@ -0,0 +1,70 @@ + + * @copyright 2018 Microsoft Corporation + * @license https://github.com/azure/azure-storage-php/LICENSE + * @link https://github.com/azure/azure-storage-php + */ + +namespace MicrosoftAzure\Storage\Blob\Models; + +/** + * Trait implementing setting and getting accessTier. + * + * @category Microsoft + * @package MicrosoftAzure\Storage\Blob\Models + * @author Azure Storage PHP SDK + * @copyright 2018 Microsoft Corporation + * @license https://github.com/azure/azure-storage-php/LICENSE + * @link https://github.com/azure/azure-storage-php + */ +trait AccessTierTrait +{ + /** + * @var string $accessTier Version 2017-04-17 and newer. For page blobs on a premium storage account, otherwise a block blob + * on blob storage account or storageV2 general account. + * Specifies the tier to be set on the blob. Currently, for block blob, tiers like "Hot", "Cool" + * and "Archive" can be used; for premium page blobs, "P4", "P6", "P10" and etc. can be set. + * Check following link for a full list of supported tiers. + * https://docs.microsoft.com/en-us/rest/api/storageservices/set-blob-tier + */ + private $accessTier; + + /** + * Gets blob access tier. + * + * @return string + */ + public function getAccessTier() + { + return $this->accessTier; + } + + /** + * Sets blob access tier. + * + * @param string $accessTier value. + * + * @return void + */ + public function setAccessTier($accessTier) + { + $this->accessTier = $accessTier; + } +} diff --git a/3rdparty/microsoft/azure-storage-blob/src/Blob/Models/AppendBlockOptions.php b/3rdparty/microsoft/azure-storage-blob/src/Blob/Models/AppendBlockOptions.php new file mode 100644 index 00000000..6820cbbe --- /dev/null +++ b/3rdparty/microsoft/azure-storage-blob/src/Blob/Models/AppendBlockOptions.php @@ -0,0 +1,108 @@ + + * @copyright 2017 Microsoft Corporation + * @license https://github.com/azure/azure-storage-php/LICENSE + * @link https://github.com/azure/azure-storage-php + */ + +namespace MicrosoftAzure\Storage\Blob\Models; + +/** + * Optional parameters for appendBlock wrapper + * + * @category Microsoft + * @package MicrosoftAzure\Storage\Blob\Models + * @author Azure Storage PHP SDK + * @copyright 2016 Microsoft Corporation + * @license https://github.com/azure/azure-storage-php/LICENSE + * @link https://github.com/azure/azure-storage-php + */ +class AppendBlockOptions extends BlobServiceOptions +{ + private $contentMD5; + private $maxBlobSize; + private $appendPosition; + + /** + * Gets block contentMD5. + * + * @return string + */ + public function getContentMD5() + { + return $this->contentMD5; + } + + /** + * Sets block contentMD5. + * + * @param string $contentMD5 value. + * + * @return void + */ + public function setContentMD5($contentMD5) + { + $this->contentMD5 = $contentMD5; + } + + /** + * Gets the max length in bytes allowed for the append blob to grow to. + * + * @return int + */ + public function getMaxBlobSize() + { + return $this->maxBlobSize; + } + + /** + * Sets the max length in bytes allowed for the append blob to grow to. + * + * @param int $maxBlobSize value. + * + * @return void + */ + public function setMaxBlobSize($maxBlobSize) + { + $this->maxBlobSize = $maxBlobSize; + } + + /** + * Gets append blob appendPosition. + * + * @return int + */ + public function getAppendPosition() + { + return $this->appendPosition; + } + + /** + * Sets append blob appendPosition. + * + * @param int $appendPosition value. + * + * @return void + */ + public function setAppendPosition($appendPosition) + { + $this->appendPosition = $appendPosition; + } +} diff --git a/3rdparty/microsoft/azure-storage-blob/src/Blob/Models/AppendBlockResult.php b/3rdparty/microsoft/azure-storage-blob/src/Blob/Models/AppendBlockResult.php new file mode 100644 index 00000000..39ca5ad1 --- /dev/null +++ b/3rdparty/microsoft/azure-storage-blob/src/Blob/Models/AppendBlockResult.php @@ -0,0 +1,244 @@ + + * @copyright 2017 Microsoft Corporation + * @license https://github.com/azure/azure-storage-php/LICENSE + * @link https://github.com/azure/azure-storage-php + */ + +namespace MicrosoftAzure\Storage\Blob\Models; + +use MicrosoftAzure\Storage\Blob\Internal\BlobResources as Resources; +use MicrosoftAzure\Storage\Common\Internal\Utilities; + +/** + * The result of calling appendBlock API. + * + * @category Microsoft + * @package MicrosoftAzure\Storage\Blob\Models + * @author Azure Storage PHP SDK + * @copyright 2016 Microsoft Corporation + * @license https://github.com/azure/azure-storage-php/LICENSE + * @link https://github.com/azure/azure-storage-php + */ +class AppendBlockResult +{ + private $appendOffset; + private $committedBlockCount; + private $contentMD5; + private $etag; + private $lastModified; + private $requestServerEncrypted; + + /** + * Creates AppendBlockResult object from the response of the put block request. + * + * @param array $headers The HTTP response headers in array representation. + * + * @internal + * + * @return AppendBlockResult + */ + public static function create(array $headers) + { + $result = new AppendBlockResult(); + + $result->setAppendOffset( + intval( + Utilities::tryGetValueInsensitive( + Resources::X_MS_BLOB_APPEND_OFFSET, $headers + ) + ) + ); + + $result->setCommittedBlockCount( + intval( + Utilities::tryGetValueInsensitive( + Resources::X_MS_BLOB_COMMITTED_BLOCK_COUNT, $headers + ) + ) + ); + + $result->setContentMD5( + Utilities::tryGetValueInsensitive(Resources::CONTENT_MD5, $headers) + ); + + $result->setEtag( + Utilities::tryGetValueInsensitive(Resources::ETAG, $headers) + ); + + if (Utilities::arrayKeyExistsInsensitive( + Resources::LAST_MODIFIED, + $headers + )) { + $lastModified = Utilities::tryGetValueInsensitive( + Resources::LAST_MODIFIED, + $headers + ); + $lastModified = Utilities::rfc1123ToDateTime($lastModified); + + $result->setLastModified($lastModified); + } + + $result->setRequestServerEncrypted( + Utilities::toBoolean( + Utilities::tryGetValueInsensitive( + Resources::X_MS_REQUEST_SERVER_ENCRYPTED, + $headers + ), + true + ) + ); + + return $result; + } + + /** + * Gets Etag of the blob that the client can use to perform conditional + * PUT operations by using the If-Match request header. + * + * @return string + */ + public function getEtag() + { + return $this->etag; + } + + /** + * Sets the etag value. + * + * @param string $etag etag as a string. + * + * @return void + */ + protected function setEtag($etag) + { + $this->etag = $etag; + } + + /** + * Gets $lastModified value. + * + * @return \DateTime + */ + public function getLastModified() + { + return $this->lastModified; + } + + /** + * Sets the $lastModified value. + * + * @param \DateTime $lastModified $lastModified value. + * + * @return void + */ + protected function setLastModified($lastModified) + { + $this->lastModified = $lastModified; + } + + /** + * Gets block content MD5. + * + * @return string + */ + public function getContentMD5() + { + return $this->contentMD5; + } + + /** + * Sets the content MD5 value. + * + * @param string $contentMD5 conent MD5 as a string. + * + * @return void + */ + protected function setContentMD5($contentMD5) + { + $this->contentMD5 = $contentMD5; + } + + /** + * Gets the offset at which the block was committed, in bytes. + * + * @return int + */ + public function getAppendOffset() + { + return $this->appendOffset; + } + + /** + * Sets the offset at which the block was committed, in bytes. + * + * @param int $appendOffset append offset, in bytes. + * + * @return void + */ + protected function setAppendOffset($appendOffset) + { + $this->appendOffset = $appendOffset; + } + + /** + * Gets the number of committed blocks present in the blob. + * + * @return int + */ + public function getCommittedBlockCount() + { + return $this->committedBlockCount; + } + + /** + * Sets the number of committed blocks present in the blob. + * + * @param int $committedBlockCount the number of committed blocks present in the blob. + * + * @return void + */ + protected function setCommittedBlockCount($committedBlockCount) + { + $this->committedBlockCount = $committedBlockCount; + } + + /** + * Gets the whether the contents of the request are successfully encrypted. + * + * @return boolean + */ + public function getRequestServerEncrypted() + { + return $this->requestServerEncrypted; + } + + /** + * Sets the request server encryption value. + * + * @param boolean $requestServerEncrypted + * + * @return void + */ + public function setRequestServerEncrypted($requestServerEncrypted) + { + $this->requestServerEncrypted = $requestServerEncrypted; + } +} diff --git a/3rdparty/microsoft/azure-storage-blob/src/Blob/Models/Blob.php b/3rdparty/microsoft/azure-storage-blob/src/Blob/Models/Blob.php new file mode 100644 index 00000000..6bfe4c24 --- /dev/null +++ b/3rdparty/microsoft/azure-storage-blob/src/Blob/Models/Blob.php @@ -0,0 +1,153 @@ + + * @copyright 2016 Microsoft Corporation + * @license https://github.com/azure/azure-storage-php/LICENSE + * @link https://github.com/azure/azure-storage-php + */ + +namespace MicrosoftAzure\Storage\Blob\Models; + +/** + * Represents windows azure blob object + * + * @category Microsoft + * @package MicrosoftAzure\Storage\Blob\Models + * @author Azure Storage PHP SDK + * @copyright 2016 Microsoft Corporation + * @license https://github.com/azure/azure-storage-php/LICENSE + * @link https://github.com/azure/azure-storage-php + */ +class Blob +{ + private $_name; + private $_url; + private $_snapshot; + private $_metadata; + private $_properties; + /** + * Gets blob name. + * + * @return string + */ + public function getName() + { + return $this->_name; + } + + /** + * Sets blob name. + * + * @param string $name value. + * + * @return void + */ + public function setName($name) + { + $this->_name = $name; + } + + /** + * Gets blob snapshot. + * + * @return string + */ + public function getSnapshot() + { + return $this->_snapshot; + } + + /** + * Sets blob snapshot. + * + * @param string $snapshot value. + * + * @return void + */ + public function setSnapshot($snapshot) + { + $this->_snapshot = $snapshot; + } + + /** + * Gets blob url. + * + * @return string + */ + public function getUrl() + { + return $this->_url; + } + + /** + * Sets blob url. + * + * @param string $url value. + * + * @return void + */ + public function setUrl($url) + { + $this->_url = $url; + } + + /** + * Gets blob metadata. + * + * @return array + */ + public function getMetadata() + { + return $this->_metadata; + } + + /** + * Sets blob metadata. + * + * @param array $metadata value. + * + * @return void + */ + public function setMetadata(array $metadata = null) + { + $this->_metadata = $metadata; + } + + /** + * Gets blob properties. + * + * @return BlobProperties + */ + public function getProperties() + { + return $this->_properties; + } + + /** + * Sets blob properties. + * + * @param BlobProperties $properties value. + * + * @return void + */ + public function setProperties($properties) + { + $this->_properties = $properties; + } +} diff --git a/3rdparty/microsoft/azure-storage-blob/src/Blob/Models/BlobAccessPolicy.php b/3rdparty/microsoft/azure-storage-blob/src/Blob/Models/BlobAccessPolicy.php new file mode 100644 index 00000000..73192001 --- /dev/null +++ b/3rdparty/microsoft/azure-storage-blob/src/Blob/Models/BlobAccessPolicy.php @@ -0,0 +1,61 @@ + + * @copyright 2017 Microsoft Corporation + * @license https://github.com/azure/azure-storage-php/LICENSE + * @link https://github.com/azure/azure-storage-php + */ + +namespace MicrosoftAzure\Storage\Blob\Models; + +use MicrosoftAzure\Storage\Blob\Internal\BlobResources; +use MicrosoftAzure\Storage\Common\Models\AccessPolicy; + +/** + * Holds access policy elements + * + * @category Microsoft + * @package MicrosoftAzure\Storage\Blob\Models + * @author Azure Storage PHP SDK + * @copyright 2017 Microsoft Corporation + * @license https://github.com/azure/azure-storage-php/LICENSE + * @link https://github.com/azure/azure-storage-php + */ +class BlobAccessPolicy extends AccessPolicy +{ + /** + * Get the valid permissions for the given resource. + * + * @return array + */ + public static function getResourceValidPermissions() + { + return BlobResources::ACCESS_PERMISSIONS[ + BlobResources::RESOURCE_TYPE_BLOB + ]; + } + + /** + * Constructor + */ + public function __construct() + { + parent::__construct(BlobResources::RESOURCE_TYPE_BLOB); + } +} diff --git a/3rdparty/microsoft/azure-storage-blob/src/Blob/Models/BlobBlockType.php b/3rdparty/microsoft/azure-storage-blob/src/Blob/Models/BlobBlockType.php new file mode 100644 index 00000000..5495a3dd --- /dev/null +++ b/3rdparty/microsoft/azure-storage-blob/src/Blob/Models/BlobBlockType.php @@ -0,0 +1,64 @@ + + * @copyright 2016 Microsoft Corporation + * @license https://github.com/azure/azure-storage-php/LICENSE + * @link https://github.com/azure/azure-storage-php + */ + +namespace MicrosoftAzure\Storage\Blob\Models; + +/** + * Holds available blob block types + * + * @category Microsoft + * @package MicrosoftAzure\Storage\Blob\Models + * @author Azure Storage PHP SDK + * @copyright 2016 Microsoft Corporation + * @license https://github.com/azure/azure-storage-php/LICENSE + * @link https://github.com/azure/azure-storage-php + */ +class BlobBlockType +{ + const COMMITTED_TYPE = 'Committed'; + const UNCOMMITTED_TYPE = 'Uncommitted'; + const LATEST_TYPE = 'Latest'; + + /** + * Validates the provided type. + * + * @param string $type The entry type. + * + * @internal + * + * @return boolean + */ + public static function isValid($type) + { + switch ($type) { + case self::COMMITTED_TYPE: + case self::LATEST_TYPE: + case self::UNCOMMITTED_TYPE: + return true; + + default: + return false; + } + } +} diff --git a/3rdparty/microsoft/azure-storage-blob/src/Blob/Models/BlobPrefix.php b/3rdparty/microsoft/azure-storage-blob/src/Blob/Models/BlobPrefix.php new file mode 100644 index 00000000..95deea8f --- /dev/null +++ b/3rdparty/microsoft/azure-storage-blob/src/Blob/Models/BlobPrefix.php @@ -0,0 +1,62 @@ + + * @copyright 2016 Microsoft Corporation + * @license https://github.com/azure/azure-storage-php/LICENSE + * @link https://github.com/azure/azure-storage-php + */ + +namespace MicrosoftAzure\Storage\Blob\Models; + +/** + * Represents BlobPrefix object + * + * @category Microsoft + * @package MicrosoftAzure\Storage\Blob\Models + * @author Azure Storage PHP SDK + * @copyright 2016 Microsoft Corporation + * @license https://github.com/azure/azure-storage-php/LICENSE + * @link https://github.com/azure/azure-storage-php + */ +class BlobPrefix +{ + private $_name; + + /** + * Gets blob name. + * + * @return string + */ + public function getName() + { + return $this->_name; + } + + /** + * Sets blob name. + * + * @param string $name value. + * + * @return void + */ + public function setName($name) + { + $this->_name = $name; + } +} diff --git a/3rdparty/microsoft/azure-storage-blob/src/Blob/Models/BlobProperties.php b/3rdparty/microsoft/azure-storage-blob/src/Blob/Models/BlobProperties.php new file mode 100644 index 00000000..a81b0a87 --- /dev/null +++ b/3rdparty/microsoft/azure-storage-blob/src/Blob/Models/BlobProperties.php @@ -0,0 +1,887 @@ + + * @copyright 2016 Microsoft Corporation + * @license https://github.com/azure/azure-storage-php/LICENSE + * @link https://github.com/azure/azure-storage-php + */ + +namespace MicrosoftAzure\Storage\Blob\Models; + +use MicrosoftAzure\Storage\Blob\Internal\BlobResources as Resources; +use MicrosoftAzure\Storage\Common\Internal\Validate; +use MicrosoftAzure\Storage\Common\Internal\Utilities; + +/** + * Represents blob properties + * + * @category Microsoft + * @package MicrosoftAzure\Storage\Blob\Models + * @author Azure Storage PHP SDK + * @copyright 2016 Microsoft Corporation + * @license https://github.com/azure/azure-storage-php/LICENSE + * @link https://github.com/azure/azure-storage-php + */ +class BlobProperties +{ + private $lastModified; + private $creationTime; + private $etag; + private $contentType; + private $contentLength; + private $contentEncoding; + private $contentLanguage; + private $contentMD5; + private $contentRange; + private $cacheControl; + private $contentDisposition; + private $blobType; + private $leaseStatus; + private $leaseState; + private $leaseDuration; + private $sequenceNumber; + private $serverEncrypted; + private $committedBlockCount; + private $copyState; + private $copyDestinationSnapshot; + private $incrementalCopy; + private $rangeContentMD5; + private $accessTier; + private $accessTierInferred; + private $accessTierChangeTime; + private $archiveStatus; + private $deletedTime; + private $remainingRetentionDays; + + /** + * Creates BlobProperties object from $parsed response in array representation of XML elements + * + * @param array $parsed parsed response in array format. + * + * @internal + * + * @return BlobProperties + */ + public static function createFromXml(array $parsed) + { + $result = new BlobProperties(); + $clean = array_change_key_case($parsed); + + $result->setCommonBlobProperties($clean); + $result->setLeaseStatus(Utilities::tryGetValue($clean, 'leasestatus')); + $result->setLeaseState(Utilities::tryGetValue($clean, 'leasestate')); + $result->setLeaseDuration(Utilities::tryGetValue($clean, 'leaseduration')); + $result->setCopyState(CopyState::createFromXml($clean)); + + $result->setIncrementalCopy( + Utilities::toBoolean( + Utilities::tryGetValue($clean, 'incrementalcopy'), + true + ) + ); + + $result->setAccessTier(( + Utilities::tryGetValue($clean, 'accesstier') + )); + + $result->setAccessTierInferred( + Utilities::toBoolean( + Utilities::tryGetValue($clean, 'accesstierinferred'), + true + ) + ); + + $accesstierchangetime = Utilities::tryGetValue($clean, 'accesstierchangetime'); + if (!is_null($accesstierchangetime)) { + $accesstierchangetime = Utilities::rfc1123ToDateTime($accesstierchangetime); + $result->setAccessTierChangeTime($accesstierchangetime); + } + + $result->setArchiveStatus( + Utilities::tryGetValue($clean, 'archivestatus') + ); + + $deletedtime = Utilities::tryGetValue($clean, 'deletedtime'); + if (!is_null($deletedtime)) { + $deletedtime = Utilities::rfc1123ToDateTime($deletedtime); + $result->setDeletedTime($deletedtime); + } + + $remainingretentiondays = Utilities::tryGetValue($clean, 'remainingretentiondays'); + if (!is_null($remainingretentiondays)) { + $result->setRemainingRetentionDays((int) $remainingretentiondays); + } + + $creationtime = Utilities::tryGetValue($clean, 'creation-time'); + if (!is_null($creationtime)) { + $creationtime = Utilities::rfc1123ToDateTime($creationtime); + $result->setCreationTime($creationtime); + } + + return $result; + } + + /** + * Creates BlobProperties object from $parsed response in array representation of http headers + * + * @param array $parsed parsed response in array format. + * + * @internal + * + * @return BlobProperties + */ + public static function createFromHttpHeaders(array $parsed) + { + $result = new BlobProperties(); + $clean = array_change_key_case($parsed); + + $result->setCommonBlobProperties($clean); + + $result->setBlobType(Utilities::tryGetValue($clean, Resources::X_MS_BLOB_TYPE)); + $result->setLeaseStatus(Utilities::tryGetValue($clean, Resources::X_MS_LEASE_STATUS)); + $result->setLeaseState(Utilities::tryGetValue($clean, Resources::X_MS_LEASE_STATE)); + $result->setLeaseDuration(Utilities::tryGetValue($clean, Resources::X_MS_LEASE_DURATION)); + $result->setCopyState(CopyState::createFromHttpHeaders($clean)); + + $result->setServerEncrypted( + Utilities::toBoolean( + Utilities::tryGetValue( + $clean, + Resources::X_MS_SERVER_ENCRYPTED + ), + true + ) + ); + $result->setIncrementalCopy( + Utilities::toBoolean( + Utilities::tryGetValue( + $clean, + Resources::X_MS_INCREMENTAL_COPY + ), + true + ) + ); + $result->setCommittedBlockCount( + intval(Utilities::tryGetValue( + $clean, + Resources::X_MS_BLOB_COMMITTED_BLOCK_COUNT + )) + ); + $result->setCopyDestinationSnapshot( + Utilities::tryGetValue( + $clean, + Resources::X_MS_COPY_DESTINATION_SNAPSHOT + ) + ); + + $result->setAccessTier(( + Utilities::tryGetValue($clean, Resources::X_MS_ACCESS_TIER) + )); + + $result->setAccessTierInferred( + Utilities::toBoolean( + Utilities::tryGetValue($clean, Resources::X_MS_ACCESS_TIER_INFERRED), + true + ) + ); + + $date = Utilities::tryGetValue($clean, Resources::X_MS_ACCESS_TIER_CHANGE_TIME); + if (!is_null($date)) { + $date = Utilities::rfc1123ToDateTime($date); + $result->setAccessTierChangeTime($date); + } + + $result->setArchiveStatus( + Utilities::tryGetValue($clean, Resources::X_MS_ARCHIVE_STATUS) + ); + + return $result; + } + + /** + * Gets blob lastModified. + * + * @return \DateTime + */ + public function getLastModified() + { + return $this->lastModified; + } + + /** + * Sets blob lastModified. + * + * @param \DateTime $lastModified value. + * + * @return void + */ + public function setLastModified(\DateTime $lastModified) + { + Validate::isDate($lastModified); + $this->lastModified = $lastModified; + } + + /** + * Gets blob creationTime. + * + * @return \DateTime + */ + public function getCreationTime() + { + return $this->creationTime; + } + + /** + * Sets blob creationTime. + * + * @param \DateTime $creationTime value. + * + * @return void + */ + public function setCreationTime(\DateTime $creationTime) + { + Validate::isDate($creationTime); + $this->creationTime = $creationTime; + } + + /** + * Gets blob etag. + * + * @return string + */ + public function getETag() + { + return $this->etag; + } + + /** + * Sets blob etag. + * + * @param string $etag value. + * + * @return void + */ + public function setETag($etag) + { + $this->etag = $etag; + } + + /** + * Gets blob contentType. + * + * @return string + */ + public function getContentType() + { + return $this->contentType; + } + + /** + * Sets blob contentType. + * + * @param string $contentType value. + * + * @return void + */ + public function setContentType($contentType) + { + $this->contentType = $contentType; + } + + /** + * Gets blob contentRange. + * + * @return string + */ + public function getContentRange() + { + return $this->contentRange; + } + + /** + * Sets blob contentRange. + * + * @param string $contentRange value. + * + * @return void + */ + public function setContentRange($contentRange) + { + $this->contentRange = $contentRange; + } + + /** + * Gets blob contentLength. + * + * @return integer + */ + public function getContentLength() + { + return $this->contentLength; + } + + /** + * Sets blob contentLength. + * + * @param integer $contentLength value. + * + * @return void + */ + public function setContentLength($contentLength) + { + Validate::isInteger($contentLength, 'contentLength'); + $this->contentLength = $contentLength; + } + + /** + * Gets blob contentEncoding. + * + * @return string + */ + public function getContentEncoding() + { + return $this->contentEncoding; + } + + /** + * Sets blob contentEncoding. + * + * @param string $contentEncoding value. + * + * @return void + */ + public function setContentEncoding($contentEncoding) + { + $this->contentEncoding = $contentEncoding; + } + + /** + * Gets blob access tier. + * + * @return string + */ + public function getAccessTier() + { + return $this->accessTier; + } + + /** + * Sets blob access tier. + * + * @param string $accessTier value. + * + * @return void + */ + public function setAccessTier($accessTier) + { + $this->accessTier = $accessTier; + } + + /** + * Gets blob archive status. + * + * @return string + */ + public function getArchiveStatus() + { + return $this->archiveStatus; + } + + /** + * Sets blob archive status. + * + * @param string $archiveStatus value. + * + * @return void + */ + public function setArchiveStatus($archiveStatus) + { + $this->archiveStatus = $archiveStatus; + } + + /** + * Gets blob deleted time. + * + * @return string + */ + public function getDeletedTime() + { + return $this->deletedTime; + } + + /** + * Sets blob deleted time. + * + * @param \DateTime $deletedTime value. + * + * @return void + */ + public function setDeletedTime(\DateTime $deletedTime) + { + $this->deletedTime = $deletedTime; + } + + /** + * Gets blob remaining retention days. + * + * @return integer + */ + public function getRemainingRetentionDays() + { + return $this->remainingRetentionDays; + } + + /** + * Sets blob remaining retention days. + * + * @param integer $remainingRetentionDays value. + * + * @return void + */ + public function setRemainingRetentionDays($remainingRetentionDays) + { + $this->remainingRetentionDays = $remainingRetentionDays; + } + + + /** + * Gets blob access inferred. + * + * @return boolean + */ + public function getAccessTierInferred() + { + return $this->accessTierInferred; + } + + /** + * Sets blob access tier inferred. + * + * @param boolean $accessTierInferred value. + * + * @return void + */ + public function setAccessTierInferred($accessTierInferred) + { + Validate::isBoolean($accessTierInferred); + $this->accessTierInferred = $accessTierInferred; + } + + /** + * Gets blob access tier change time. + * + * @return \DateTime + */ + public function getAccessTierChangeTime() + { + return $this->accessTierChangeTime; + } + + /** + * Sets blob access tier change time. + * + * @param \DateTime $accessTierChangeTime value. + * + * @return void + */ + public function setAccessTierChangeTime(\DateTime $accessTierChangeTime) + { + Validate::isDate($accessTierChangeTime); + $this->accessTierChangeTime = $accessTierChangeTime; + } + + /** + * Gets blob contentLanguage. + * + * @return string + */ + public function getContentLanguage() + { + return $this->contentLanguage; + } + + /** + * Sets blob contentLanguage. + * + * @param string $contentLanguage value. + * + * @return void + */ + public function setContentLanguage($contentLanguage) + { + $this->contentLanguage = $contentLanguage; + } + + /** + * Gets blob contentMD5. + * + * @return string + */ + public function getContentMD5() + { + return $this->contentMD5; + } + + /** + * Sets blob contentMD5. + * + * @param string $contentMD5 value. + * + * @return void + */ + public function setContentMD5($contentMD5) + { + $this->contentMD5 = $contentMD5; + } + + /** + * Gets blob range contentMD5. + * + * @return string + */ + public function getRangeContentMD5() + { + return $this->rangeContentMD5; + } + + /** + * Sets blob range contentMD5. + * + * @param string rangeContentMD5 value. + * + * @return void + */ + public function setRangeContentMD5($rangeContentMD5) + { + $this->rangeContentMD5 = $rangeContentMD5; + } + + /** + * Gets blob cacheControl. + * + * @return string + */ + public function getCacheControl() + { + return $this->cacheControl; + } + + /** + * Sets blob cacheControl. + * + * @param string $cacheControl value. + * + * @return void + */ + public function setCacheControl($cacheControl) + { + $this->cacheControl = $cacheControl; + } + + /** + * Gets blob contentDisposition. + * + * @return string + */ + public function getContentDisposition() + { + return $this->contentDisposition; + } + + /** + * Sets blob contentDisposition. + * + * @param string $contentDisposition value. + * + * @return void + */ + public function setContentDisposition($contentDisposition) + { + $this->contentDisposition = $contentDisposition; + } + + /** + * Gets blob blobType. + * + * @return string + */ + public function getBlobType() + { + return $this->blobType; + } + + /** + * Sets blob blobType. + * + * @param string $blobType value. + * + * @return void + */ + public function setBlobType($blobType) + { + $this->blobType = $blobType; + } + + /** + * Gets blob leaseStatus. + * + * @return string + */ + public function getLeaseStatus() + { + return $this->leaseStatus; + } + + /** + * Sets blob leaseStatus. + * + * @param string $leaseStatus value. + * + * @return void + */ + public function setLeaseStatus($leaseStatus) + { + $this->leaseStatus = $leaseStatus; + } + + /** + * Gets blob lease state. + * + * @return string + */ + public function getLeaseState() + { + return $this->leaseState; + } + + /** + * Sets blob lease state. + * + * @param string $leaseState value. + * + * @return void + */ + public function setLeaseState($leaseState) + { + $this->leaseState = $leaseState; + } + + /** + * Gets blob lease duration. + * + * @return string + */ + public function getLeaseDuration() + { + return $this->leaseDuration; + } + + /** + * Sets blob leaseStatus. + * + * @param string $leaseDuration value. + * + * @return void + */ + public function setLeaseDuration($leaseDuration) + { + $this->leaseDuration = $leaseDuration; + } + + /** + * Gets blob sequenceNumber. + * + * @return int + */ + public function getSequenceNumber() + { + return $this->sequenceNumber; + } + + /** + * Sets blob sequenceNumber. + * + * @param int $sequenceNumber value. + * + * @return void + */ + public function setSequenceNumber($sequenceNumber) + { + Validate::isInteger($sequenceNumber, 'sequenceNumber'); + $this->sequenceNumber = $sequenceNumber; + } + + /** + * Gets the server encryption status of the blob. + * + * @return boolean + */ + public function getServerEncrypted() + { + return $this->serverEncrypted; + } + + /** + * Sets the server encryption status of the blob. + * + * @param boolean $serverEncrypted + * + * @return void + */ + public function setServerEncrypted($serverEncrypted) + { + $this->serverEncrypted = $serverEncrypted; + } + + /** + * Gets the number of committed blocks present in the blob. + * + * @return int + */ + public function getCommittedBlockCount() + { + return $this->committedBlockCount; + } + + /** + * Sets the number of committed blocks present in the blob. + * + * @param int $committedBlockCount the number of committed blocks present in the blob. + * + * @return void + */ + public function setCommittedBlockCount($committedBlockCount) + { + $this->committedBlockCount = $committedBlockCount; + } + + /** + * Gets copy state of the blob. + * + * @return CopyState + */ + public function getCopyState() + { + return $this->copyState; + } + + /** + * Sets the copy state of the blob. + * + * @param CopyState $copyState the copy state of the blob. + * + * @return void + */ + public function setCopyState($copyState) + { + $this->copyState = $copyState; + } + + /** + * Gets snapshot time of the last successful incremental copy snapshot for this blob. + * + * @return string + */ + public function getCopyDestinationSnapshot() + { + return $this->copyDestinationSnapshot; + } + + /** + * Sets snapshot time of the last successful incremental copy snapshot for this blob. + * + * @param string $copyDestinationSnapshot last successful incremental copy snapshot. + */ + public function setCopyDestinationSnapshot($copyDestinationSnapshot) + { + $this->copyDestinationSnapshot = $copyDestinationSnapshot; + } + + /** + * Gets whether the blob is an incremental copy blob. + * + * @return boolean + */ + public function getIncrementalCopy() + { + return $this->incrementalCopy; + } + + /** + * Sets whether the blob is an incremental copy blob. + * + * @param boolean $incrementalCopy whether blob is an incremental copy blob. + */ + public function setIncrementalCopy($incrementalCopy) + { + $this->incrementalCopy = $incrementalCopy; + } + + private function setCommonBlobProperties(array $clean) + { + $date = Utilities::tryGetValue($clean, Resources::LAST_MODIFIED); + if (!is_null($date)) { + $date = Utilities::rfc1123ToDateTime($date); + $this->setLastModified($date); + } + + $this->setBlobType(Utilities::tryGetValue($clean, 'blobtype')); + + $this->setContentLength(intval($clean[Resources::CONTENT_LENGTH])); + $this->setETag(Utilities::tryGetValue($clean, Resources::ETAG)); + $this->setSequenceNumber( + intval( + Utilities::tryGetValue($clean, Resources::X_MS_BLOB_SEQUENCE_NUMBER) + ) + ); + $this->setContentRange( + Utilities::tryGetValue($clean, Resources::CONTENT_RANGE) + ); + $this->setCacheControl( + Utilities::tryGetValue($clean, Resources::CACHE_CONTROL) + ); + $this->setContentDisposition( + Utilities::tryGetValue($clean, Resources::CONTENT_DISPOSITION) + ); + $this->setContentEncoding( + Utilities::tryGetValue($clean, Resources::CONTENT_ENCODING) + ); + $this->setContentLanguage( + Utilities::tryGetValue($clean, Resources::CONTENT_LANGUAGE) + ); + $this->setContentType( + Utilities::tryGetValue($clean, Resources::CONTENT_TYPE_LOWER_CASE) + ); + + if (Utilities::tryGetValue($clean, Resources::CONTENT_MD5) && + !Utilities::tryGetValue($clean, Resources::CONTENT_RANGE) + ) { + $this->setContentMD5( + Utilities::tryGetValue($clean, Resources::CONTENT_MD5) + ); + } else { + $this->setContentMD5( + Utilities::tryGetValue($clean, Resources::BLOB_CONTENT_MD5) + ); + $this->setRangeContentMD5( + Utilities::tryGetValue($clean, Resources::CONTENT_MD5) + ); + } + } +} diff --git a/3rdparty/microsoft/azure-storage-blob/src/Blob/Models/BlobServiceOptions.php b/3rdparty/microsoft/azure-storage-blob/src/Blob/Models/BlobServiceOptions.php new file mode 100644 index 00000000..d442ec62 --- /dev/null +++ b/3rdparty/microsoft/azure-storage-blob/src/Blob/Models/BlobServiceOptions.php @@ -0,0 +1,92 @@ + + * @copyright 2016 Microsoft Corporation + * @license https://github.com/azure/azure-storage-php/LICENSE + * @link https://github.com/azure/azure-storage-php + */ + +namespace MicrosoftAzure\Storage\Blob\Models; + +use MicrosoftAzure\Storage\Common\Models\ServiceOptions; + +/** + * Blob service options. + * + * @category Microsoft + * @package MicrosoftAzure\Storage\Blob\Models + * @author Azure Storage PHP SDK + * @copyright 2016 Microsoft Corporation + * @license https://github.com/azure/azure-storage-php/LICENSE + * @link https://github.com/azure/azure-storage-php + */ +class BlobServiceOptions extends ServiceOptions +{ + private $_leaseId; + private $_accessConditions; + + /** + * Gets lease Id for the blob + * + * @return string + */ + public function getLeaseId() + { + return $this->_leaseId; + } + + /** + * Sets lease Id for the blob + * + * @param string $leaseId the blob lease id. + * + * @return void + */ + public function setLeaseId($leaseId) + { + $this->_leaseId = $leaseId; + } + + /** + * Gets access condition + * + * @return \MicrosoftAzure\Storage\Blob\Models\AccessCondition[] + */ + public function getAccessConditions() + { + return $this->_accessConditions; + } + + /** + * Sets access condition + * + * @param mixed $accessConditions value to use. + * + * @return void + */ + public function setAccessConditions($accessConditions) + { + if (!is_null($accessConditions) && + is_array($accessConditions)) { + $this->_accessConditions = $accessConditions; + } else { + $this->_accessConditions = [$accessConditions]; + } + } +} diff --git a/3rdparty/microsoft/azure-storage-blob/src/Blob/Models/BlobType.php b/3rdparty/microsoft/azure-storage-blob/src/Blob/Models/BlobType.php new file mode 100644 index 00000000..6a539d0c --- /dev/null +++ b/3rdparty/microsoft/azure-storage-blob/src/Blob/Models/BlobType.php @@ -0,0 +1,42 @@ + + * @copyright 2016 Microsoft Corporation + * @license https://github.com/azure/azure-storage-php/LICENSE + * @link https://github.com/azure/azure-storage-php + */ + +namespace MicrosoftAzure\Storage\Blob\Models; + +/** + * Encapsulates blob types + * + * @category Microsoft + * @package MicrosoftAzure\Storage\Blob\Models + * @author Azure Storage PHP SDK + * @copyright 2016 Microsoft Corporation + * @license https://github.com/azure/azure-storage-php/LICENSE + * @link https://github.com/azure/azure-storage-php + */ +class BlobType +{ + const BLOCK_BLOB = 'BlockBlob'; + const PAGE_BLOB = 'PageBlob'; + const APPEND_BLOB = 'AppendBlob'; +} diff --git a/3rdparty/microsoft/azure-storage-blob/src/Blob/Models/Block.php b/3rdparty/microsoft/azure-storage-blob/src/Blob/Models/Block.php new file mode 100644 index 00000000..a1eab59e --- /dev/null +++ b/3rdparty/microsoft/azure-storage-blob/src/Blob/Models/Block.php @@ -0,0 +1,97 @@ + + * @copyright 2016 Microsoft Corporation + * @license https://github.com/azure/azure-storage-php/LICENSE + * @link https://github.com/azure/azure-storage-php + */ + +namespace MicrosoftAzure\Storage\Blob\Models; + +/** + * Holds information about blob block. + * + * @category Microsoft + * @package MicrosoftAzure\Storage\Blob\Models + * @author Azure Storage PHP SDK + * @copyright 2016 Microsoft Corporation + * @license https://github.com/azure/azure-storage-php/LICENSE + * @link https://github.com/azure/azure-storage-php + */ +class Block +{ + private $_blockId; + private $_type; + + /** + * Constructor. + * + * @param string $blockId The ID of this block. + * @param string $type The type of the block. + */ + public function __construct($blockId = '', $type = '') + { + $this->_blockId = $blockId; + $this->_type = $type; + } + + /** + * Sets the blockId. + * + * @param string $blockId The id of the block. + * + * @return void + */ + public function setBlockId($blockId) + { + $this->_blockId = $blockId; + } + + /** + * Gets the blockId. + * + * @return string + */ + public function getBlockId() + { + return $this->_blockId; + } + + /** + * Sets the type. + * + * @param string $type The type of the block. + * + * @return void + */ + public function setType($type) + { + $this->_type = $type; + } + + /** + * Gets the type. + * + * @return string + */ + public function getType() + { + return $this->_type; + } +} diff --git a/3rdparty/microsoft/azure-storage-blob/src/Blob/Models/BlockList.php b/3rdparty/microsoft/azure-storage-blob/src/Blob/Models/BlockList.php new file mode 100644 index 00000000..fb23b309 --- /dev/null +++ b/3rdparty/microsoft/azure-storage-blob/src/Blob/Models/BlockList.php @@ -0,0 +1,172 @@ + + * @copyright 2016 Microsoft Corporation + * @license https://github.com/azure/azure-storage-php/LICENSE + * @link https://github.com/azure/azure-storage-php + */ + +namespace MicrosoftAzure\Storage\Blob\Models; + +use MicrosoftAzure\Storage\Blob\Internal\BlobResources as Resources; +use MicrosoftAzure\Storage\Common\Internal\Validate; +use MicrosoftAzure\Storage\Common\Internal\Serialization\XmlSerializer; + +/** + * Holds block list used for commitBlobBlocks + * + * @category Microsoft + * @package MicrosoftAzure\Storage\Blob\Models + * @author Azure Storage PHP SDK + * @copyright 2016 Microsoft Corporation + * @license https://github.com/azure/azure-storage-php/LICENSE + * @link https://github.com/azure/azure-storage-php + */ +class BlockList +{ + private $entries; + private static $xmlRootName = 'BlockList'; + + /** + * Creates block list from array of blocks. + * + * @param Block[] The blocks array. + * + * @return BlockList + */ + public static function create(array $array) + { + $blockList = new BlockList(); + + foreach ($array as $value) { + $blockList->addEntry($value->getBlockId(), $value->getType()); + } + + return $blockList; + } + + /** + * Adds new entry to the block list entries. + * + * @param string $blockId The block id. + * @param string $type The entry type, you can use BlobBlockType. + * + * @return void + */ + public function addEntry($blockId, $type) + { + Validate::canCastAsString($blockId, 'blockId'); + Validate::isTrue( + BlobBlockType::isValid($type), + sprintf(Resources::INVALID_BTE_MSG, get_class(new BlobBlockType())) + ); + $block = new Block(); + $block->setBlockId($blockId); + $block->setType($type); + + $this->entries[] = $block; + } + + /** + * Addds committed block entry. + * + * @param string $blockId The block id. + * + * @return void + */ + public function addCommittedEntry($blockId) + { + $this->addEntry($blockId, BlobBlockType::COMMITTED_TYPE); + } + + /** + * Addds uncommitted block entry. + * + * @param string $blockId The block id. + * + * @return void + */ + public function addUncommittedEntry($blockId) + { + $this->addEntry($blockId, BlobBlockType::UNCOMMITTED_TYPE); + } + + /** + * Addds latest block entry. + * + * @param string $blockId The block id. + * + * @return void + */ + public function addLatestEntry($blockId) + { + $this->addEntry($blockId, BlobBlockType::LATEST_TYPE); + } + + /** + * Gets blob block entry. + * + * @param string $blockId The id of the block. + * + * @return Block + */ + public function getEntry($blockId) + { + foreach ($this->entries as $value) { + if ($blockId == $value->getBlockId()) { + return $value; + } + } + + return null; + } + + /** + * Gets all blob block entries. + * + * @return Block[] + */ + public function getEntries() + { + return $this->entries; + } + + /** + * Converts the BlockList object to XML representation + * + * @param XmlSerializer $xmlSerializer The XML serializer. + * + * @internal + * + * @return string + */ + public function toXml(XmlSerializer $xmlSerializer) + { + $properties = array(XmlSerializer::ROOT_NAME => self::$xmlRootName); + $array = array(); + + foreach ($this->entries as $value) { + $array[] = array( + $value->getType() => $value->getBlockId() + ); + } + + return $xmlSerializer->serialize($array, $properties); + } +} diff --git a/3rdparty/microsoft/azure-storage-blob/src/Blob/Models/BreakLeaseResult.php b/3rdparty/microsoft/azure-storage-blob/src/Blob/Models/BreakLeaseResult.php new file mode 100644 index 00000000..4cb6c737 --- /dev/null +++ b/3rdparty/microsoft/azure-storage-blob/src/Blob/Models/BreakLeaseResult.php @@ -0,0 +1,83 @@ + + * @copyright 2016 Microsoft Corporation + * @license https://github.com/azure/azure-storage-php/LICENSE + * @link https://github.com/azure/azure-storage-php + */ + +namespace MicrosoftAzure\Storage\Blob\Models; + +use MicrosoftAzure\Storage\Blob\Internal\BlobResources as Resources; +use MicrosoftAzure\Storage\Common\Internal\Utilities; + +/** + * The result of calling breakLease API. + * + * @category Microsoft + * @package MicrosoftAzure\Storage\Blob\Models + * @author Azure Storage PHP SDK + * @copyright 2016 Microsoft Corporation + * @license https://github.com/azure/azure-storage-php/LICENSE + * @link https://github.com/azure/azure-storage-php + */ +class BreakLeaseResult +{ + private $_leaseTime; + + /** + * Creates BreakLeaseResult from response headers + * + * @param array $headers response headers + * + * @return BreakLeaseResult + */ + public static function create($headers) + { + $result = new BreakLeaseResult(); + + $result->setLeaseTime( + Utilities::tryGetValue($headers, Resources::X_MS_LEASE_TIME) + ); + + return $result; + } + + /** + * Gets lease time. + * + * @return string + */ + public function getLeaseTime() + { + return $this->_leaseTime; + } + + /** + * Sets lease time. + * + * @param string $leaseTime the blob lease time. + * + * @return void + */ + protected function setLeaseTime($leaseTime) + { + $this->_leaseTime = $leaseTime; + } +} diff --git a/3rdparty/microsoft/azure-storage-blob/src/Blob/Models/CommitBlobBlocksOptions.php b/3rdparty/microsoft/azure-storage-blob/src/Blob/Models/CommitBlobBlocksOptions.php new file mode 100644 index 00000000..632270c6 --- /dev/null +++ b/3rdparty/microsoft/azure-storage-blob/src/Blob/Models/CommitBlobBlocksOptions.php @@ -0,0 +1,224 @@ + + * @copyright 2016 Microsoft Corporation + * @license https://github.com/azure/azure-storage-php/LICENSE + * @link https://github.com/azure/azure-storage-php + */ + +namespace MicrosoftAzure\Storage\Blob\Models; + +/** + * Optional parameters for commitBlobBlocks + * + * @category Microsoft + * @package MicrosoftAzure\Storage\Blob\Models + * @author Azure Storage PHP SDK + * @copyright 2016 Microsoft Corporation + * @license https://github.com/azure/azure-storage-php/LICENSE + * @link https://github.com/azure/azure-storage-php + */ +class CommitBlobBlocksOptions extends BlobServiceOptions +{ + private $_contentType; + private $_contentEncoding; + private $_contentLanguage; + private $_contentMD5; + private $_cacheControl; + private $_contentDisposition; + private $_metadata; + + /** + * Gets ContentType. + * + * @return string + */ + public function getContentType() + { + return $this->_contentType; + } + + /** + * Sets ContentType. + * + * @param string $contentType value. + * + * @return void + */ + public function setContentType($contentType) + { + $this->_contentType = $contentType; + } + + /** + * Gets ContentEncoding. + * + * @return string + */ + public function getContentEncoding() + { + return $this->_contentEncoding; + } + + /** + * Sets ContentEncoding. + * + * @param string $contentEncoding value. + * + * @return void + */ + public function setContentEncoding($contentEncoding) + { + $this->_contentEncoding = $contentEncoding; + } + + /** + * Gets ContentLanguage. + * + * @return string + */ + public function getContentLanguage() + { + return $this->_contentLanguage; + } + + /** + * Sets ContentLanguage. + * + * @param string $contentLanguage value. + * + * @return void + */ + public function setContentLanguage($contentLanguage) + { + $this->_contentLanguage = $contentLanguage; + } + + /** + * Gets ContentMD5. + * + * @return string + */ + public function getContentMD5() + { + return $this->_contentMD5; + } + + /** + * Sets ContentMD5. + * + * @param string $contentMD5 value. + * + * @return void + */ + public function setContentMD5($contentMD5) + { + $this->_contentMD5 = $contentMD5; + } + + /** + * Gets cache control. + * + * @return string + */ + public function getCacheControl() + { + return $this->_cacheControl; + } + + /** + * Sets cacheControl. + * + * @param string $cacheControl value to use. + * + * @return void + */ + public function setCacheControl($cacheControl) + { + $this->_cacheControl = $cacheControl; + } + + /** + * Gets content disposition. + * + * @return string + */ + public function getContentDisposition() + { + return $this->_contentDisposition; + } + + /** + * Sets contentDisposition. + * + * @param string $contentDisposition value to use. + * + * @return void + */ + public function setContentDisposition($contentDisposition) + { + $this->_contentDisposition = $contentDisposition; + } + + /** + * Gets blob metadata. + * + * @return array + */ + public function getMetadata() + { + return $this->_metadata; + } + + /** + * Sets blob metadata. + * + * @param array $metadata value. + * + * @return void + */ + public function setMetadata(array $metadata = null) + { + $this->_metadata = $metadata; + } + + /** + * Create a instance using the given options + * @param mixed $options Input options + * + * @internal + * + * @return self + */ + public static function create($options) + { + $result = new CommitBlobBlocksOptions(); + $result->setContentType($options->getContentType()); + $result->setContentEncoding($options->getContentEncoding()); + $result->setContentLanguage($options->getContentLanguage()); + $result->setContentMD5($options->getContentMD5()); + $result->setCacheControl($options->getCacheControl()); + $result->setContentDisposition($options->getContentDisposition()); + $result->setMetadata($options->getMetadata()); + $result->setLeaseId($options->getLeaseId()); + $result->setAccessConditions($options->getAccessConditions()); + + return $result; + } +} diff --git a/3rdparty/microsoft/azure-storage-blob/src/Blob/Models/Container.php b/3rdparty/microsoft/azure-storage-blob/src/Blob/Models/Container.php new file mode 100644 index 00000000..4816f509 --- /dev/null +++ b/3rdparty/microsoft/azure-storage-blob/src/Blob/Models/Container.php @@ -0,0 +1,131 @@ + + * @copyright 2016 Microsoft Corporation + * @license https://github.com/azure/azure-storage-php/LICENSE + * @link https://github.com/azure/azure-storage-php + */ + +namespace MicrosoftAzure\Storage\Blob\Models; + +/** + * WindowsAzure container object. + * + * @category Microsoft + * @package MicrosoftAzure\Storage\Blob\Models + * @author Azure Storage PHP SDK + * @copyright 2016 Microsoft Corporation + * @license https://github.com/azure/azure-storage-php/LICENSE + * @link https://github.com/azure/azure-storage-php + */ +class Container +{ + private $_name; + private $_url; + private $_metadata; + private $_properties; + + /** + * Gets container name. + * + * @return string + */ + public function getName() + { + return $this->_name; + } + + /** + * Sets container name. + * + * @param string $name value. + * + * @return void + */ + public function setName($name) + { + $this->_name = $name; + } + + /** + * Gets container url. + * + * @return string + */ + public function getUrl() + { + return $this->_url; + } + + /** + * Sets container url. + * + * @param string $url value. + * + * @return void + */ + public function setUrl($url) + { + $this->_url = $url; + } + + /** + * Gets container metadata. + * + * @return array + */ + public function getMetadata() + { + return $this->_metadata; + } + + /** + * Sets container metadata. + * + * @param array $metadata value. + * + * @return void + */ + public function setMetadata(array $metadata = null) + { + $this->_metadata = $metadata; + } + + /** + * Gets container properties + * + * @return ContainerProperties + */ + public function getProperties() + { + return $this->_properties; + } + + /** + * Sets container properties + * + * @param ContainerProperties $properties container properties + * + * @return void + */ + public function setProperties(ContainerProperties $properties) + { + $this->_properties = $properties; + } +} diff --git a/3rdparty/microsoft/azure-storage-blob/src/Blob/Models/ContainerACL.php b/3rdparty/microsoft/azure-storage-blob/src/Blob/Models/ContainerACL.php new file mode 100644 index 00000000..4383abc7 --- /dev/null +++ b/3rdparty/microsoft/azure-storage-blob/src/Blob/Models/ContainerACL.php @@ -0,0 +1,164 @@ + + * @copyright 2016 Microsoft Corporation + * @license https://github.com/azure/azure-storage-php/LICENSE + * @link https://github.com/azure/azure-storage-php + */ + +namespace MicrosoftAzure\Storage\Blob\Models; + +use MicrosoftAzure\Storage\Common\Internal\ACLBase; +use MicrosoftAzure\Storage\Blob\Internal\BlobResources as Resources; +use MicrosoftAzure\Storage\Common\Internal\Validate; + +/** + * Holds container ACL members. + * + * @category Microsoft + * @package MicrosoftAzure\Storage\Blob\Models + * @author Azure Storage PHP SDK + * @copyright 2016 Microsoft Corporation + * @license https://github.com/azure/azure-storage-php/LICENSE + * @link https://github.com/azure/azure-storage-php + */ +class ContainerACL extends ACLBase +{ + private $publicAccess; + + /** + * Constructor. + */ + public function __construct() + { + //setting the resource type to a default value. + $this->setResourceType(Resources::RESOURCE_TYPE_CONTAINER); + } + + /** + * Parses the given array into signed identifiers and create an instance of + * ContainerACL + * + * @param string $publicAccess The container public access. + * @param array $parsed The parsed response into array representation. + * + * @internal + * + * @return ContainerACL + */ + public static function create($publicAccess, array $parsed = null) + { + Validate::isTrue( + PublicAccessType::isValid($publicAccess), + Resources::INVALID_BLOB_PAT_MSG + ); + $result = new ContainerACL(); + $result->fromXmlArray($parsed); + $result->setPublicAccess($publicAccess); + + return $result; + } + + /** + * Gets container publicAccess. + * + * @return string + */ + public function getPublicAccess() + { + return $this->publicAccess; + } + + /** + * Sets container publicAccess. + * + * @param string $publicAccess value. + * + * @return void + */ + public function setPublicAccess($publicAccess) + { + Validate::isTrue( + PublicAccessType::isValid($publicAccess), + Resources::INVALID_BLOB_PAT_MSG + ); + $this->publicAccess = $publicAccess; + $this->setResourceType( + self::getResourceTypeByPublicAccess($publicAccess) + ); + } + + /** + * Gets the resource type according to the given public access. Default + * value is Resources::RESOURCE_TYPE_CONTAINER. + * + * @param string $publicAccess The public access that determines the + * resource type. + * + * @return string + */ + private static function getResourceTypeByPublicAccess($publicAccess) + { + $result = ''; + + switch ($publicAccess) { + case PublicAccessType::BLOBS_ONLY: + $result = Resources::RESOURCE_TYPE_BLOB; + break; + case PublicAccessType::CONTAINER_AND_BLOBS: + $result = Resources::RESOURCE_TYPE_CONTAINER; + break; + default: + $result = Resources::RESOURCE_TYPE_CONTAINER; + break; + } + + return $result; + } + + /** + * Validate if the resource type is for the class. + * + * @param string $resourceType the resource type to be validated. + * + * @throws \InvalidArgumentException + * + * @internal + * + * @return void + */ + protected static function validateResourceType($resourceType) + { + Validate::isTrue( + $resourceType == Resources::RESOURCE_TYPE_BLOB || + $resourceType == Resources::RESOURCE_TYPE_CONTAINER, + Resources::INVALID_RESOURCE_TYPE + ); + } + + /** + * Create a ContainerAccessPolicy object. + * + * @return ContainerAccessPolicy + */ + protected static function createAccessPolicy() + { + return new ContainerAccessPolicy(); + } +} diff --git a/3rdparty/microsoft/azure-storage-blob/src/Blob/Models/ContainerAccessPolicy.php b/3rdparty/microsoft/azure-storage-blob/src/Blob/Models/ContainerAccessPolicy.php new file mode 100644 index 00000000..c1815123 --- /dev/null +++ b/3rdparty/microsoft/azure-storage-blob/src/Blob/Models/ContainerAccessPolicy.php @@ -0,0 +1,61 @@ + + * @copyright 2017 Microsoft Corporation + * @license https://github.com/azure/azure-storage-php/LICENSE + * @link https://github.com/azure/azure-storage-php + */ + +namespace MicrosoftAzure\Storage\Blob\Models; + +use MicrosoftAzure\Storage\Blob\Internal\BlobResources; +use MicrosoftAzure\Storage\Common\Models\AccessPolicy; + +/** + * Holds access policy elements + * + * @category Microsoft + * @package MicrosoftAzure\Storage\Blob\Models + * @author Azure Storage PHP SDK + * @copyright 2017 Microsoft Corporation + * @license https://github.com/azure/azure-storage-php/LICENSE + * @link https://github.com/azure/azure-storage-php + */ +class ContainerAccessPolicy extends AccessPolicy +{ + /** + * Get the valid permissions for the given resource. + * + * @return array + */ + public static function getResourceValidPermissions() + { + return BlobResources::ACCESS_PERMISSIONS[ + BlobResources::RESOURCE_TYPE_CONTAINER + ]; + } + + /** + * Constructor + */ + public function __construct() + { + parent::__construct(BlobResources::RESOURCE_TYPE_CONTAINER); + } +} diff --git a/3rdparty/microsoft/azure-storage-blob/src/Blob/Models/ContainerProperties.php b/3rdparty/microsoft/azure-storage-blob/src/Blob/Models/ContainerProperties.php new file mode 100644 index 00000000..b333b4b3 --- /dev/null +++ b/3rdparty/microsoft/azure-storage-blob/src/Blob/Models/ContainerProperties.php @@ -0,0 +1,184 @@ + + * @copyright 2016 Microsoft Corporation + * @license https://github.com/azure/azure-storage-php/LICENSE + * @link https://github.com/azure/azure-storage-php + */ + +namespace MicrosoftAzure\Storage\Blob\Models; + +use MicrosoftAzure\Storage\Blob\Internal\BlobResources as Resources; +use MicrosoftAzure\Storage\Common\Internal\Validate; + +/** + * Holds container properties fields + * + * @category Microsoft + * @package MicrosoftAzure\Storage\Blob\Models + * @author Azure Storage PHP SDK + * @copyright 2016 Microsoft Corporation + * @license https://github.com/azure/azure-storage-php/LICENSE + * @link https://github.com/azure/azure-storage-php + */ +class ContainerProperties +{ + private $etag; + private $lastModified; + private $leaseDuration; + private $leaseStatus; + private $leaseState; + private $publicAccess; + + /** + * Gets container lastModified. + * + * @return \DateTime + */ + public function getLastModified() + { + return $this->lastModified; + } + + /** + * Sets container lastModified. + * + * @param \DateTime $lastModified value. + * + * @return void + */ + public function setLastModified(\DateTime $lastModified) + { + $this->lastModified = $lastModified; + } + + /** + * Gets container etag. + * + * @return string + */ + public function getETag() + { + return $this->etag; + } + + /** + * Sets container etag. + * + * @param string $etag value. + * + * @return void + */ + public function setETag($etag) + { + $this->etag = $etag; + } + + /** + * Gets blob leaseStatus. + * + * @return string + */ + public function getLeaseStatus() + { + return $this->leaseStatus; + } + + /** + * Sets blob leaseStatus. + * + * @param string $leaseStatus value. + * + * @return void + */ + public function setLeaseStatus($leaseStatus) + { + $this->leaseStatus = $leaseStatus; + } + + /** + * Gets blob lease state. + * + * @return string + */ + public function getLeaseState() + { + return $this->leaseState; + } + + /** + * Sets blob lease state. + * + * @param string $leaseState value. + * + * @return void + */ + public function setLeaseState($leaseState) + { + $this->leaseState = $leaseState; + } + + /** + * Gets blob lease duration. + * + * @return string + */ + public function getLeaseDuration() + { + return $this->leaseDuration; + } + + /** + * Sets blob leaseStatus. + * + * @param string $leaseDuration value. + * + * @return void + */ + public function setLeaseDuration($leaseDuration) + { + $this->leaseDuration = $leaseDuration; + } + + /** + * Gets container publicAccess. + * + * @return string + */ + public function getPublicAccess() + { + return $this->publicAccess; + } + + /** + * Sets container publicAccess. + * + * @param string $publicAccess value. + * + * @return void + */ + public function setPublicAccess($publicAccess) + { + Validate::isTrue( + PublicAccessType::isValid($publicAccess), + Resources::INVALID_BLOB_PAT_MSG + ); + $this->publicAccess = $publicAccess; + } +} diff --git a/3rdparty/microsoft/azure-storage-blob/src/Blob/Models/CopyBlobFromURLOptions.php b/3rdparty/microsoft/azure-storage-blob/src/Blob/Models/CopyBlobFromURLOptions.php new file mode 100644 index 00000000..1e3bbbe1 --- /dev/null +++ b/3rdparty/microsoft/azure-storage-blob/src/Blob/Models/CopyBlobFromURLOptions.php @@ -0,0 +1,138 @@ + + * @copyright 2017 Microsoft Corporation + * @license https://github.com/azure/azure-storage-php/LICENSE + * @link https://github.com/azure/azure-storage-php + */ + +namespace MicrosoftAzure\Storage\Blob\Models; + +/** + * optional parameters for CopyBlobOptions wrapper + * + * @category Microsoft + * @package MicrosoftAzure\Storage\Blob\Models + * @author Azure Storage PHP SDK + * @copyright 2017 Microsoft Corporation + * @license https://github.com/azure/azure-storage-php/LICENSE + * @link https://github.com/azure/azure-storage-php + */ +class CopyBlobFromURLOptions extends BlobServiceOptions +{ + use AccessTierTrait; + + private $sourceLeaseId; + private $sourceAccessConditions; + private $metadata; + private $isIncrementalCopy; + + /** + * Gets source access condition + * + * @return AccessCondition[] + */ + public function getSourceAccessConditions() + { + return $this->sourceAccessConditions; + } + + /** + * Sets source access condition + * + * @param array $sourceAccessConditions value to use. + * + * @return void + */ + public function setSourceAccessConditions($sourceAccessConditions) + { + if (!is_null($sourceAccessConditions) && + is_array($sourceAccessConditions)) { + $this->sourceAccessConditions = $sourceAccessConditions; + } else { + $this->sourceAccessConditions = [$sourceAccessConditions]; + } + } + + /** + * Gets metadata. + * + * @return array + */ + public function getMetadata() + { + return $this->metadata; + } + + /** + * Sets metadata. + * + * @param array $metadata value. + * + * @return void + */ + public function setMetadata(array $metadata) + { + $this->metadata = $metadata; + } + + /** + * Gets source lease ID. + * + * @return string + */ + public function getSourceLeaseId() + { + return $this->sourceLeaseId; + } + + /** + * Sets source lease ID. + * + * @param string $sourceLeaseId value. + * + * @return void + */ + public function setSourceLeaseId($sourceLeaseId) + { + $this->sourceLeaseId = $sourceLeaseId; + } + + /** + * Gets isIncrementalCopy. + * + * @return boolean + */ + public function getIsIncrementalCopy() + { + return $this->isIncrementalCopy; + } + + /** + * Sets isIncrementalCopy. + * + * @param boolean $isIncrementalCopy + * + * @return void + */ + public function setIsIncrementalCopy($isIncrementalCopy) + { + $this->isIncrementalCopy = $isIncrementalCopy; + } +} diff --git a/3rdparty/microsoft/azure-storage-blob/src/Blob/Models/CopyBlobOptions.php b/3rdparty/microsoft/azure-storage-blob/src/Blob/Models/CopyBlobOptions.php new file mode 100644 index 00000000..85fafcc8 --- /dev/null +++ b/3rdparty/microsoft/azure-storage-blob/src/Blob/Models/CopyBlobOptions.php @@ -0,0 +1,62 @@ + + * @copyright 2016 Microsoft Corporation + * @license https://github.com/azure/azure-storage-php/LICENSE + * @link https://github.com/azure/azure-storage-php + */ + +namespace MicrosoftAzure\Storage\Blob\Models; + +/** + * optional parameters for CopyBlobOptions wrapper + * + * @category Microsoft + * @package MicrosoftAzure\Storage\Blob\Models + * @author Azure Storage PHP SDK + * @copyright 2016 Microsoft Corporation + * @license https://github.com/azure/azure-storage-php/LICENSE + * @link https://github.com/azure/azure-storage-php + */ +class CopyBlobOptions extends CopyBlobFromURLOptions +{ + private $sourceSnapshot; + + /** + * Gets source snapshot. + * + * @return string + */ + public function getSourceSnapshot() + { + return $this->sourceSnapshot; + } + + /** + * Sets source snapshot. + * + * @param string $sourceSnapshot value. + * + * @return void + */ + public function setSourceSnapshot($sourceSnapshot) + { + $this->sourceSnapshot = $sourceSnapshot; + } +} diff --git a/3rdparty/microsoft/azure-storage-blob/src/Blob/Models/CopyBlobResult.php b/3rdparty/microsoft/azure-storage-blob/src/Blob/Models/CopyBlobResult.php new file mode 100644 index 00000000..2e15a835 --- /dev/null +++ b/3rdparty/microsoft/azure-storage-blob/src/Blob/Models/CopyBlobResult.php @@ -0,0 +1,179 @@ + + * @copyright 2016 Microsoft Corporation + * @license https://github.com/azure/azure-storage-php/LICENSE + * @link https://github.com/azure/azure-storage-php + */ + +namespace MicrosoftAzure\Storage\Blob\Models; + +use MicrosoftAzure\Storage\Blob\Internal\BlobResources as Resources; +use MicrosoftAzure\Storage\Common\Internal\Utilities; + +/** + * The result of calling copyBlob API. + * + * @category Microsoft + * @package MicrosoftAzure\Storage\Blob\Models + * @author Azure Storage PHP SDK + * @copyright 2016 Microsoft Corporation + * @license https://github.com/azure/azure-storage-php/LICENSE + * @link https://github.com/azure/azure-storage-php + */ +class CopyBlobResult +{ + private $_etag; + private $_lastModified; + private $_copyId; + private $_copyStatus; + + /** + * Creates CopyBlobResult object from the response of the copy blob request. + * + * @param array $headers The HTTP response headers in array representation. + * + * @internal + * + * @return CopyBlobResult + */ + public static function create(array $headers) + { + $result = new CopyBlobResult(); + $result->setETag( + Utilities::tryGetValueInsensitive( + Resources::ETAG, + $headers + ) + ); + $result->setCopyId( + Utilities::tryGetValueInsensitive( + Resources::X_MS_COPY_ID, + $headers + ) + ); + $result->setCopyStatus( + Utilities::tryGetValueInsensitive( + Resources::X_MS_COPY_STATUS, + $headers + ) + ); + if (Utilities::arrayKeyExistsInsensitive(Resources::LAST_MODIFIED, $headers)) { + $lastModified = Utilities::tryGetValueInsensitive( + Resources::LAST_MODIFIED, + $headers + ); + $result->setLastModified(Utilities::rfc1123ToDateTime($lastModified)); + } + + return $result; + } + + /** + * Gets copy Id + * + * @return string + */ + public function getCopyId() + { + return $this->_copyId; + } + + /** + * Sets copy Id + * + * @param string $copyId the blob copy id. + * + * @internal + * + * @return void + */ + protected function setCopyId($copyId) + { + $this->_copyId = $copyId; + } + + /** + * Gets copy status + * + * @return string + */ + public function getCopyStatus() + { + return $this->_copyStatus; + } + + /** + * Sets copy status + * + * @param string $status the copy status. + * + * @internal + * + * @return void + */ + protected function setCopyStatus($copystatus) + { + $this->_copyStatus = $copystatus; + } + + /** + * Gets ETag. + * + * @return string + */ + public function getETag() + { + return $this->_etag; + } + + /** + * Sets ETag. + * + * @param string $etag value. + * + * @return void + */ + protected function setETag($etag) + { + $this->_etag = $etag; + } + + /** + * Gets blob lastModified. + * + * @return \DateTime + */ + public function getLastModified() + { + return $this->_lastModified; + } + + /** + * Sets blob lastModified. + * + * @param \DateTime $lastModified value. + * + * @return void + */ + protected function setLastModified(\DateTime $lastModified) + { + $this->_lastModified = $lastModified; + } +} diff --git a/3rdparty/microsoft/azure-storage-blob/src/Blob/Models/CopyState.php b/3rdparty/microsoft/azure-storage-blob/src/Blob/Models/CopyState.php new file mode 100644 index 00000000..732e28ed --- /dev/null +++ b/3rdparty/microsoft/azure-storage-blob/src/Blob/Models/CopyState.php @@ -0,0 +1,294 @@ + + * @copyright 2017 Microsoft Corporation + * @license https://github.com/azure/azure-storage-php/LICENSE + * @link https://github.com/azure/azure-storage-php + */ + +namespace MicrosoftAzure\Storage\Blob\Models; + +use MicrosoftAzure\Storage\Blob\Internal\BlobResources as Resources; +use MicrosoftAzure\Storage\Common\Internal\Utilities; + +/** + * Represents blob copy state + * + * @category Microsoft + * @package MicrosoftAzure\Storage\Blob\Models + * @author Azure Storage PHP SDK + * @copyright 2016 Microsoft Corporation + * @license https://github.com/azure/azure-storage-php/LICENSE + * @link https://github.com/azure/azure-storage-php + */ +class CopyState +{ + private $_copyId; + private $_completionTime; + private $_status; + private $_statusDescription; + private $_source; + private $_bytesCopied; + private $_totalBytes; + + /** + * Creates CopyState object from $parsed response in array representation of XML elements + * + * @param array $parsed parsed response in array format. + * + * @internal + * + * @return \MicrosoftAzure\Storage\Blob\Models\CopyState + */ + public static function createFromXml(array $parsed) + { + $result = new CopyState(); + $clean = array_change_key_case($parsed); + + $copyCompletionTime = Utilities::tryGetValue($clean, 'copycompletiontime'); + if (!is_null($copyCompletionTime)) { + $copyCompletionTime = Utilities::rfc1123ToDateTime($copyCompletionTime); + $result->setCompletionTime($copyCompletionTime); + } + + $result->setCopyId(Utilities::tryGetValue($clean, 'copyid')); + $result->setStatus(Utilities::tryGetValue($clean, 'copystatus')); + $result->setStatusDescription(Utilities::tryGetValue($clean, 'copystatusdescription')); + $result->setSource(Utilities::tryGetValue($clean, 'copysource')); + + $copyProgress = Utilities::tryGetValue($clean, 'copyprogress'); + + if (!is_null($copyProgress) && strpos($copyProgress, '/') !== false) { + $parts = explode('/', $copyProgress); + $bytesCopied = intval($parts[0]); + $totalBytes = intval($parts[1]); + + $result->setBytesCopied($bytesCopied); + $result->setTotalBytes($totalBytes); + } + + return $result; + } + + /** + * Creates CopyState object from $parsed response in array representation of http headers + * + * @param array $parsed parsed response in array format. + * + * @internal + * + * @return \MicrosoftAzure\Storage\Blob\Models\CopyState + */ + public static function createFromHttpHeaders(array $parsed) + { + $result = new CopyState(); + $clean = array_change_key_case($parsed); + + $copyCompletionTime = Utilities::tryGetValue($clean, Resources::X_MS_COPY_COMPLETION_TIME); + if (!is_null($copyCompletionTime)) { + $copyCompletionTime = Utilities::rfc1123ToDateTime($copyCompletionTime); + $result->setCompletionTime($copyCompletionTime); + } + + $result->setCopyId(Utilities::tryGetValue($clean, Resources::X_MS_COPY_ID)); + $result->setStatus(Utilities::tryGetValue($clean, Resources::X_MS_COPY_STATUS)); + $result->setStatusDescription(Utilities::tryGetValue($clean, Resources::X_MS_COPY_STATUS_DESCRIPTION)); + $result->setSource(Utilities::tryGetValue($clean, Resources::X_MS_COPY_SOURCE)); + + $copyProgress = Utilities::tryGetValue($clean, Resources::X_MS_COPY_PROGRESS); + if (!is_null($copyProgress) && strpos($copyProgress, '/') !== false) { + $parts = explode('/', $copyProgress); + $bytesCopied = intval($parts[0]); + $totalBytes = intval($parts[1]); + + $result->setBytesCopied($bytesCopied); + $result->setTotalBytes($totalBytes); + } + + return $result; + } + + /** + * Gets copy Id + * + * @return string + */ + public function getCopyId() + { + return $this->_copyId; + } + + /** + * Sets copy Id + * + * @param string $copyId the blob copy id. + * + * @internal + * + * @return void + */ + protected function setCopyId($copyId) + { + $this->_copyId = $copyId; + } + + /** + * Gets copy completion time + * + * @return \DateTime + */ + public function getCompletionTime() + { + return $this->_completionTime; + } + + /** + * Sets copy completion time + * + * @param \DateTime $completionTime the copy completion time. + * + * @internal + * + * @return void + */ + protected function setCompletionTime($completionTime) + { + $this->_completionTime = $completionTime; + } + + /** + * Gets copy status + * + * @return string + */ + public function getStatus() + { + return $this->_status; + } + + /** + * Sets copy status + * + * @param string $status the copy status. + * + * @internal + * + * @return void + */ + protected function setStatus($status) + { + $this->_status = $status; + } + + /** + * Gets copy status description + * + * @return string + */ + public function getStatusDescription() + { + return $this->_statusDescription; + } + + /** + * Sets copy status description + * + * @param string $statusDescription the copy status description. + * + * @internal + * + * @return void + */ + protected function setStatusDescription($statusDescription) + { + $this->_statusDescription = $statusDescription; + } + + /** + * Gets copy source + * + * @return string + */ + public function getSource() + { + return $this->_source; + } + + /** + * Sets copy source + * + * @param string $source the copy source. + * + * @internal + * + * @return void + */ + protected function setSource($source) + { + $this->_source = $source; + } + + /** + * Gets bytes copied + * + * @return int + */ + public function getBytesCopied() + { + return $this->_bytesCopied; + } + + /** + * Sets bytes copied + * + * @param int $bytesCopied the bytes copied. + * + * @internal + * + * @return void + */ + protected function setBytesCopied($bytesCopied) + { + $this->_bytesCopied = $bytesCopied; + } + + /** + * Gets total bytes to be copied + * + * @return int + */ + public function getTotalBytes() + { + return $this->_bytesCopied; + } + + /** + * Sets total bytes to be copied + * + * @param int $totalBytes the bytes copied. + * + * @internal + * + * @return void + */ + protected function setTotalBytes($totalBytes) + { + $this->_totalBytes = $totalBytes; + } +} diff --git a/3rdparty/microsoft/azure-storage-blob/src/Blob/Models/CreateBlobBlockOptions.php b/3rdparty/microsoft/azure-storage-blob/src/Blob/Models/CreateBlobBlockOptions.php new file mode 100644 index 00000000..7ee56d4a --- /dev/null +++ b/3rdparty/microsoft/azure-storage-blob/src/Blob/Models/CreateBlobBlockOptions.php @@ -0,0 +1,101 @@ + + * @copyright 2016 Microsoft Corporation + * @license https://github.com/azure/azure-storage-php/LICENSE + * @link https://github.com/azure/azure-storage-php + */ + +namespace MicrosoftAzure\Storage\Blob\Models; + +/** + * Optional parameters for createBlobBlock wrapper + * + * @category Microsoft + * @package MicrosoftAzure\Storage\Blob\Models + * @author Azure Storage PHP SDK + * @copyright 2016 Microsoft Corporation + * @license https://github.com/azure/azure-storage-php/LICENSE + * @link https://github.com/azure/azure-storage-php + */ +class CreateBlobBlockOptions extends BlobServiceOptions +{ + private $_contentMD5; + private $_numberOfConcurrency; + + /** + * Gets blob contentMD5. + * + * @return string + */ + public function getContentMD5() + { + return $this->_contentMD5; + } + + /** + * Sets blob contentMD5. + * + * @param string $contentMD5 value. + * + * @return void + */ + public function setContentMD5($contentMD5) + { + $this->_contentMD5 = $contentMD5; + } + + /** + * Gets number of concurrency for sending a blob. + * + * @return int + */ + public function getNumberOfConcurrency() + { + return $this->_numberOfConcurrency; + } + + /** + * Sets number of concurrency for sending a blob. + * + * @param int $numberOfConcurrency the number of concurrent requests. + */ + public function setNumberOfConcurrency($numberOfConcurrency) + { + $this->_numberOfConcurrency = $numberOfConcurrency; + } + + /** + * Construct a CreateBlobBlockOptions object from a createBlobOptions. + * + * @param CreateBlobOptions $createBlobOptions + * + * @return CreateBlobBlockOptions + */ + public static function create(CreateBlobOptions $createBlobOptions) + { + $result = new CreateBlobBlockOptions(); + $result->setTimeout($createBlobOptions->getTimeout()); + $result->setLeaseId($createBlobOptions->getLeaseId()); + $result->setNumberOfConcurrency( + $createBlobOptions->getNumberOfConcurrency() + ); + return $result; + } +} diff --git a/3rdparty/microsoft/azure-storage-blob/src/Blob/Models/CreateBlobOptions.php b/3rdparty/microsoft/azure-storage-blob/src/Blob/Models/CreateBlobOptions.php new file mode 100644 index 00000000..f359f462 --- /dev/null +++ b/3rdparty/microsoft/azure-storage-blob/src/Blob/Models/CreateBlobOptions.php @@ -0,0 +1,247 @@ + + * @copyright 2016 Microsoft Corporation + * @license https://github.com/azure/azure-storage-php/LICENSE + * @link https://github.com/azure/azure-storage-php + */ + +namespace MicrosoftAzure\Storage\Blob\Models; + +use MicrosoftAzure\Storage\Common\Internal\Validate; + +/** + * optional parameters for createXXXBlob wrapper + * + * @category Microsoft + * @package MicrosoftAzure\Storage\Blob\Models + * @author Azure Storage PHP SDK + * @copyright 2016 Microsoft Corporation + * @license https://github.com/azure/azure-storage-php/LICENSE + * @link https://github.com/azure/azure-storage-php + */ +class CreateBlobOptions extends BlobServiceOptions +{ + private $_contentType; + private $_contentEncoding; + private $_contentLanguage; + private $_contentMD5; + private $_cacheControl; + private $_contentDisposition; + private $_metadata; + private $_sequenceNumber; + private $_numberOfConcurrency; + + /** + * Gets blob contentType. + * + * @return string + */ + public function getContentType() + { + return $this->_contentType; + } + + /** + * Sets blob contentType. + * + * @param string $contentType value. + * + * @return void + */ + public function setContentType($contentType) + { + $this->_contentType = $contentType; + } + + /** + * Gets contentEncoding. + * + * @return string + */ + public function getContentEncoding() + { + return $this->_contentEncoding; + } + + /** + * Sets contentEncoding. + * + * @param string $contentEncoding value. + * + * @return void + */ + public function setContentEncoding($contentEncoding) + { + $this->_contentEncoding = $contentEncoding; + } + + /** + * Gets contentLanguage. + * + * @return string + */ + public function getContentLanguage() + { + return $this->_contentLanguage; + } + + /** + * Sets contentLanguage. + * + * @param string $contentLanguage value. + * + * @return void + */ + public function setContentLanguage($contentLanguage) + { + $this->_contentLanguage = $contentLanguage; + } + + /** + * Gets contentMD5. + * + * @return string + */ + public function getContentMD5() + { + return $this->_contentMD5; + } + + /** + * Sets contentMD5. + * + * @param string $contentMD5 value. + * + * @return void + */ + public function setContentMD5($contentMD5) + { + $this->_contentMD5 = $contentMD5; + } + + /** + * Gets cacheControl. + * + * @return string + */ + public function getCacheControl() + { + return $this->_cacheControl; + } + + /** + * Sets cacheControl. + * + * @param string $cacheControl value to use. + * + * @return void + */ + public function setCacheControl($cacheControl) + { + $this->_cacheControl = $cacheControl; + } + + /** + * Gets content disposition. + * + * @return string + */ + public function getContentDisposition() + { + return $this->_contentDisposition; + } + + /** + * Sets content disposition. + * + * @param string $contentDisposition value to use. + * + * @return void + */ + public function setContentDisposition($contentDisposition) + { + $this->_contentDisposition = $contentDisposition; + } + + /** + * Gets blob metadata. + * + * @return array + */ + public function getMetadata() + { + return $this->_metadata; + } + + /** + * Sets blob metadata. + * + * @param array $metadata value. + * + * @return void + */ + public function setMetadata(array $metadata) + { + $this->_metadata = $metadata; + } + + /** + * Gets blob sequenceNumber. + * + * @return int + */ + public function getSequenceNumber() + { + return $this->_sequenceNumber; + } + + /** + * Sets blob sequenceNumber. + * + * @param int $sequenceNumber value. + * + * @return void + */ + public function setSequenceNumber($sequenceNumber) + { + Validate::isInteger($sequenceNumber, 'sequenceNumber'); + $this->_sequenceNumber = $sequenceNumber; + } + + /** + * Gets number of concurrency for sending a blob. + * + * @return int + */ + public function getNumberOfConcurrency() + { + return $this->_numberOfConcurrency; + } + + /** + * Sets number of concurrency for sending a blob. + * + * @param int $numberOfConcurrency the number of concurrent requests. + */ + public function setNumberOfConcurrency($numberOfConcurrency) + { + $this->_numberOfConcurrency = $numberOfConcurrency; + } +} diff --git a/3rdparty/microsoft/azure-storage-blob/src/Blob/Models/CreateBlobPagesOptions.php b/3rdparty/microsoft/azure-storage-blob/src/Blob/Models/CreateBlobPagesOptions.php new file mode 100644 index 00000000..97e1bc1b --- /dev/null +++ b/3rdparty/microsoft/azure-storage-blob/src/Blob/Models/CreateBlobPagesOptions.php @@ -0,0 +1,62 @@ + + * @copyright 2016 Microsoft Corporation + * @license https://github.com/azure/azure-storage-php/LICENSE + * @link https://github.com/azure/azure-storage-php + */ + +namespace MicrosoftAzure\Storage\Blob\Models; + +/** + * Optional parameters for create and clear blob pages + * + * @category Microsoft + * @package MicrosoftAzure\Storage\Blob\Models + * @author Azure Storage PHP SDK + * @copyright 2016 Microsoft Corporation + * @license https://github.com/azure/azure-storage-php/LICENSE + * @link https://github.com/azure/azure-storage-php + */ +class CreateBlobPagesOptions extends BlobServiceOptions +{ + private $_contentMD5; + + /** + * Gets blob contentMD5. + * + * @return string + */ + public function getContentMD5() + { + return $this->_contentMD5; + } + + /** + * Sets blob contentMD5. + * + * @param string $contentMD5 value. + * + * @return void + */ + public function setContentMD5($contentMD5) + { + $this->_contentMD5 = $contentMD5; + } +} diff --git a/3rdparty/microsoft/azure-storage-blob/src/Blob/Models/CreateBlobPagesResult.php b/3rdparty/microsoft/azure-storage-blob/src/Blob/Models/CreateBlobPagesResult.php new file mode 100644 index 00000000..5a6caf51 --- /dev/null +++ b/3rdparty/microsoft/azure-storage-blob/src/Blob/Models/CreateBlobPagesResult.php @@ -0,0 +1,207 @@ + + * @copyright 2016 Microsoft Corporation + * @license https://github.com/azure/azure-storage-php/LICENSE + * @link https://github.com/azure/azure-storage-php + */ + +namespace MicrosoftAzure\Storage\Blob\Models; + +use MicrosoftAzure\Storage\Blob\Internal\BlobResources as Resources; +use MicrosoftAzure\Storage\Common\Internal\Validate; +use MicrosoftAzure\Storage\Common\Internal\Utilities; + +/** + * Holds result of calling create or clear blob pages + * + * @category Microsoft + * @package MicrosoftAzure\Storage\Blob\Models + * @author Azure Storage PHP SDK + * @copyright 2016 Microsoft Corporation + * @license https://github.com/azure/azure-storage-php/LICENSE + * @link https://github.com/azure/azure-storage-php + */ +class CreateBlobPagesResult +{ + private $contentMD5; + private $etag; + private $lastModified; + private $requestServerEncrypted; + private $sequenceNumber; + + /** + * Creates CreateBlobPagesResult object from $parsed response in array + * representation + * + * @param array $headers HTTP response headers + * + * @internal + * + * @return CreateBlobPagesResult + */ + public static function create(array $headers) + { + $result = new CreateBlobPagesResult(); + $clean = array_change_key_case($headers); + + $date = $clean[Resources::LAST_MODIFIED]; + $date = Utilities::rfc1123ToDateTime($date); + $result->setETag($clean[Resources::ETAG]); + $result->setLastModified($date); + + $result->setContentMD5( + Utilities::tryGetValue($clean, Resources::CONTENT_MD5) + ); + + $result->setRequestServerEncrypted( + Utilities::toBoolean( + Utilities::tryGetValueInsensitive( + Resources::X_MS_REQUEST_SERVER_ENCRYPTED, + $headers + ), + true + ) + ); + + $result->setSequenceNumber( + intval( + Utilities::tryGetValue( + $clean, + Resources::X_MS_BLOB_SEQUENCE_NUMBER + ) + ) + ); + + return $result; + } + + /** + * Gets blob lastModified. + * + * @return \DateTime. + */ + public function getLastModified() + { + return $this->lastModified; + } + + /** + * Sets blob lastModified. + * + * @param \DateTime $lastModified value. + * + * @return void + */ + protected function setLastModified($lastModified) + { + Validate::isDate($lastModified); + $this->lastModified = $lastModified; + } + + /** + * Gets blob etag. + * + * @return string + */ + public function getETag() + { + return $this->etag; + } + + /** + * Sets blob etag. + * + * @param string $etag value. + * + * @return void + */ + protected function setETag($etag) + { + Validate::canCastAsString($etag, 'etag'); + $this->etag = $etag; + } + + /** + * Gets blob contentMD5. + * + * @return string + */ + public function getContentMD5() + { + return $this->contentMD5; + } + + /** + * Sets blob contentMD5. + * + * @param string $contentMD5 value. + * + * @return void + */ + protected function setContentMD5($contentMD5) + { + $this->contentMD5 = $contentMD5; + } + + /** + * Gets the whether the contents of the request are successfully encrypted. + * + * @return boolean + */ + public function getRequestServerEncrypted() + { + return $this->requestServerEncrypted; + } + + /** + * Sets the request server encryption value. + * + * @param boolean $requestServerEncrypted + * + * @return void + */ + public function setRequestServerEncrypted($requestServerEncrypted) + { + $this->requestServerEncrypted = $requestServerEncrypted; + } + + /** + * Gets blob sequenceNumber. + * + * @return int + */ + public function getSequenceNumber() + { + return $this->sequenceNumber; + } + + /** + * Sets blob sequenceNumber. + * + * @param int $sequenceNumber value. + * + * @return void + */ + protected function setSequenceNumber($sequenceNumber) + { + Validate::isInteger($sequenceNumber, 'sequenceNumber'); + $this->sequenceNumber = $sequenceNumber; + } +} diff --git a/3rdparty/microsoft/azure-storage-blob/src/Blob/Models/CreateBlobSnapshotOptions.php b/3rdparty/microsoft/azure-storage-blob/src/Blob/Models/CreateBlobSnapshotOptions.php new file mode 100644 index 00000000..6b89184d --- /dev/null +++ b/3rdparty/microsoft/azure-storage-blob/src/Blob/Models/CreateBlobSnapshotOptions.php @@ -0,0 +1,62 @@ + + * @copyright 2016 Microsoft Corporation + * @license https://github.com/azure/azure-storage-php/LICENSE + * @link https://github.com/azure/azure-storage-php + */ + +namespace MicrosoftAzure\Storage\Blob\Models; + +/** + * The optional parameters for createBlobSnapshot wrapper. + * + * @category Microsoft + * @package MicrosoftAzure\Storage\Blob\Models + * @author Azure Storage PHP SDK + * @copyright 2016 Microsoft Corporation + * @license https://github.com/azure/azure-storage-php/LICENSE + * @link https://github.com/azure/azure-storage-php + */ +class CreateBlobSnapshotOptions extends BlobServiceOptions +{ + private $_metadata; + + /** + * Gets metadata. + * + * @return array + */ + public function getMetadata() + { + return $this->_metadata; + } + + /** + * Sets metadata. + * + * @param array $metadata The metadata array. + * + * @return void + */ + public function setMetadata(array $metadata) + { + $this->_metadata = $metadata; + } +} diff --git a/3rdparty/microsoft/azure-storage-blob/src/Blob/Models/CreateBlobSnapshotResult.php b/3rdparty/microsoft/azure-storage-blob/src/Blob/Models/CreateBlobSnapshotResult.php new file mode 100644 index 00000000..b94c0edd --- /dev/null +++ b/3rdparty/microsoft/azure-storage-blob/src/Blob/Models/CreateBlobSnapshotResult.php @@ -0,0 +1,139 @@ + + * @copyright 2016 Microsoft Corporation + * @license https://github.com/azure/azure-storage-php/LICENSE + * @link https://github.com/azure/azure-storage-php + */ + +namespace MicrosoftAzure\Storage\Blob\Models; + +use MicrosoftAzure\Storage\Blob\Internal\BlobResources as Resources; +use MicrosoftAzure\Storage\Common\Internal\Utilities; + +/** + * The result of creating Blob snapshot. + * + * @category Microsoft + * @package MicrosoftAzure\Storage\Blob\Models + * @author Azure Storage PHP SDK + * @copyright 2016 Microsoft Corporation + * @license https://github.com/azure/azure-storage-php/LICENSE + * @link https://github.com/azure/azure-storage-php + */ +class CreateBlobSnapshotResult +{ + private $_snapshot; + private $_etag; + private $_lastModified; + + /** + * Creates CreateBlobSnapshotResult object from the response of the + * create Blob snapshot request. + * + * @param array $headers The HTTP response headers in array representation. + * + * @internal + * + * @return CreateBlobSnapshotResult + */ + public static function create(array $headers) + { + $result = new CreateBlobSnapshotResult(); + $headerWithLowerCaseKey = array_change_key_case($headers); + + $result->setETag($headerWithLowerCaseKey[Resources::ETAG]); + + $result->setLastModified( + Utilities::rfc1123ToDateTime( + $headerWithLowerCaseKey[Resources::LAST_MODIFIED] + ) + ); + + $result->setSnapshot($headerWithLowerCaseKey[Resources::X_MS_SNAPSHOT]); + + return $result; + } + + /** + * Gets snapshot. + * + * @return string + */ + public function getSnapshot() + { + return $this->_snapshot; + } + + /** + * Sets snapshot. + * + * @param string $snapshot value. + * + * @return void + */ + protected function setSnapshot($snapshot) + { + $this->_snapshot = $snapshot; + } + + /** + * Gets ETag. + * + * @return string + */ + public function getETag() + { + return $this->_etag; + } + + /** + * Sets ETag. + * + * @param string $etag value. + * + * @return void + */ + protected function setETag($etag) + { + $this->_etag = $etag; + } + + /** + * Gets blob lastModified. + * + * @return \DateTime + */ + public function getLastModified() + { + return $this->_lastModified; + } + + /** + * Sets blob lastModified. + * + * @param \DateTime $lastModified value. + * + * @return void + */ + protected function setLastModified($lastModified) + { + $this->_lastModified = $lastModified; + } +} diff --git a/3rdparty/microsoft/azure-storage-blob/src/Blob/Models/CreateBlockBlobOptions.php b/3rdparty/microsoft/azure-storage-blob/src/Blob/Models/CreateBlockBlobOptions.php new file mode 100644 index 00000000..105cd8c6 --- /dev/null +++ b/3rdparty/microsoft/azure-storage-blob/src/Blob/Models/CreateBlockBlobOptions.php @@ -0,0 +1,42 @@ + + * @copyright 2018 Microsoft Corporation + * @license https://github.com/azure/azure-storage-php/LICENSE + * @link https://github.com/azure/azure-storage-php + */ + +namespace MicrosoftAzure\Storage\Blob\Models; + +use MicrosoftAzure\Storage\Common\Models\TransactionalMD5Trait; + +/** + * Optional parameters for CreateBlockBlob. + * + * @category Microsoft + * @package MicrosoftAzure\Storage\Blob\Models + * @author Azure Storage PHP SDK + * @copyright 2018 Microsoft Corporation + * @license https://github.com/azure/azure-storage-php/LICENSE + * @link https://github.com/azure/azure-storage-php + */ +class CreateBlockBlobOptions extends CreateBlobOptions +{ + use TransactionalMD5Trait; +} diff --git a/3rdparty/microsoft/azure-storage-blob/src/Blob/Models/CreateContainerOptions.php b/3rdparty/microsoft/azure-storage-blob/src/Blob/Models/CreateContainerOptions.php new file mode 100644 index 00000000..5658155a --- /dev/null +++ b/3rdparty/microsoft/azure-storage-blob/src/Blob/Models/CreateContainerOptions.php @@ -0,0 +1,113 @@ + + * @copyright 2016 Microsoft Corporation + * @license https://github.com/azure/azure-storage-php/LICENSE + * @link https://github.com/azure/azure-storage-php + */ + +namespace MicrosoftAzure\Storage\Blob\Models; + +use MicrosoftAzure\Storage\Common\Internal\Validate; + +/** + * Optional parameters for createContainer API + * + * @category Microsoft + * @package MicrosoftAzure\Storage\Blob\Models + * @author Azure Storage PHP SDK + * @copyright 2016 Microsoft Corporation + * @license https://github.com/azure/azure-storage-php/LICENSE + * @link https://github.com/azure/azure-storage-php + */ +class CreateContainerOptions extends BlobServiceOptions +{ + private $_publicAccess; + private $_metadata; + + /** + * Gets container public access. + * + * @return string + */ + public function getPublicAccess() + { + return $this->_publicAccess; + } + + /** + * Specifies whether data in the container may be accessed publicly and the level + * of access. Possible values include: + * 1) container: Specifies full public read access for container and blob data. + * Clients can enumerate blobs within the container via anonymous request, but + * cannot enumerate containers within the storage account. + * 2) blob: Specifies public read access for blobs. Blob data within this + * container can be read via anonymous request, but container data is not + * available. Clients cannot enumerate blobs within the container via + * anonymous request. + * If this value is not specified in the request, container data is private to + * the account owner. + * + * @param string $publicAccess access modifier for the container + * + * @return void + */ + public function setPublicAccess($publicAccess) + { + Validate::canCastAsString($publicAccess, 'publicAccess'); + $this->_publicAccess = $publicAccess; + } + + /** + * Gets user defined metadata. + * + * @return array + */ + public function getMetadata() + { + return $this->_metadata; + } + + /** + * Sets user defined metadata. This metadata should be added without the header + * prefix (x-ms-meta-*). + * + * @param array $metadata user defined metadata object in array form. + * + * @return void + */ + public function setMetadata(array $metadata) + { + $this->_metadata = $metadata; + } + + /** + * Adds new metadata element. This element should be added without the header + * prefix (x-ms-meta-*). + * + * @param string $key metadata key element. + * @param string $value metadata value element. + * + * @return void + */ + public function addMetadata($key, $value) + { + $this->_metadata[$key] = $value; + } +} diff --git a/3rdparty/microsoft/azure-storage-blob/src/Blob/Models/CreatePageBlobFromContentOptions.php b/3rdparty/microsoft/azure-storage-blob/src/Blob/Models/CreatePageBlobFromContentOptions.php new file mode 100644 index 00000000..941d8dcc --- /dev/null +++ b/3rdparty/microsoft/azure-storage-blob/src/Blob/Models/CreatePageBlobFromContentOptions.php @@ -0,0 +1,42 @@ + + * @copyright 2018 Microsoft Corporation + * @license https://github.com/azure/azure-storage-php/LICENSE + * @link https://github.com/azure/azure-storage-php + */ + +namespace MicrosoftAzure\Storage\Blob\Models; + +use MicrosoftAzure\Storage\Common\Models\TransactionalMD5Trait; + +/** + * Optional parameters for createPageBlobFromContent. + * + * @category Microsoft + * @package MicrosoftAzure\Storage\Blob\Models + * @author Azure Storage PHP SDK + * @copyright 2018 Microsoft Corporation + * @license https://github.com/azure/azure-storage-php/LICENSE + * @link https://github.com/azure/azure-storage-php + */ +class CreatePageBlobFromContentOptions extends CreatePageBlobOptions +{ + use TransactionalMD5Trait; +} diff --git a/3rdparty/microsoft/azure-storage-blob/src/Blob/Models/CreatePageBlobOptions.php b/3rdparty/microsoft/azure-storage-blob/src/Blob/Models/CreatePageBlobOptions.php new file mode 100644 index 00000000..b663571d --- /dev/null +++ b/3rdparty/microsoft/azure-storage-blob/src/Blob/Models/CreatePageBlobOptions.php @@ -0,0 +1,40 @@ + + * @copyright 2018 Microsoft Corporation + * @license https://github.com/azure/azure-storage-php/LICENSE + * @link https://github.com/azure/azure-storage-php + */ + +namespace MicrosoftAzure\Storage\Blob\Models; + +/** + * Optional parameters for CreatePageBlob. + * + * @category Microsoft + * @package MicrosoftAzure\Storage\Blob\Models + * @author Azure Storage PHP SDK + * @copyright 2018 Microsoft Corporation + * @license https://github.com/azure/azure-storage-php/LICENSE + * @link https://github.com/azure/azure-storage-php + */ +class CreatePageBlobOptions extends CreateBlobOptions +{ + use AccessTierTrait; +} diff --git a/3rdparty/microsoft/azure-storage-blob/src/Blob/Models/DeleteBlobOptions.php b/3rdparty/microsoft/azure-storage-blob/src/Blob/Models/DeleteBlobOptions.php new file mode 100644 index 00000000..52c20912 --- /dev/null +++ b/3rdparty/microsoft/azure-storage-blob/src/Blob/Models/DeleteBlobOptions.php @@ -0,0 +1,88 @@ + + * @copyright 2016 Microsoft Corporation + * @license https://github.com/azure/azure-storage-php/LICENSE + * @link https://github.com/azure/azure-storage-php + */ + +namespace MicrosoftAzure\Storage\Blob\Models; + +use MicrosoftAzure\Storage\Common\Internal\Validate; + +/** + * Optional parameters for deleteBlob wrapper + * + * @category Microsoft + * @package MicrosoftAzure\Storage\Blob\Models + * @author Azure Storage PHP SDK + * @copyright 2016 Microsoft Corporation + * @license https://github.com/azure/azure-storage-php/LICENSE + * @link https://github.com/azure/azure-storage-php + */ +class DeleteBlobOptions extends BlobServiceOptions +{ + private $_snapshot; + private $_deleteSnaphotsOnly; + + /** + * Gets blob snapshot. + * + * @return string + */ + public function getSnapshot() + { + return $this->_snapshot; + } + + /** + * Sets blob snapshot. + * + * @param string $snapshot value. + * + * @return void + */ + public function setSnapshot($snapshot) + { + $this->_snapshot = $snapshot; + } + + /** + * Gets blob deleteSnaphotsOnly. + * + * @return boolean + */ + public function getDeleteSnaphotsOnly() + { + return $this->_deleteSnaphotsOnly; + } + + /** + * Sets blob deleteSnaphotsOnly. + * + * @param string $deleteSnaphotsOnly value. + * + * @return boolean + */ + public function setDeleteSnaphotsOnly($deleteSnaphotsOnly) + { + Validate::isBoolean($deleteSnaphotsOnly); + $this->_deleteSnaphotsOnly = $deleteSnaphotsOnly; + } +} diff --git a/3rdparty/microsoft/azure-storage-blob/src/Blob/Models/GetBlobMetadataOptions.php b/3rdparty/microsoft/azure-storage-blob/src/Blob/Models/GetBlobMetadataOptions.php new file mode 100644 index 00000000..7cba28fa --- /dev/null +++ b/3rdparty/microsoft/azure-storage-blob/src/Blob/Models/GetBlobMetadataOptions.php @@ -0,0 +1,62 @@ + + * @copyright 2016 Microsoft Corporation + * @license https://github.com/azure/azure-storage-php/LICENSE + * @link https://github.com/azure/azure-storage-php + */ + +namespace MicrosoftAzure\Storage\Blob\Models; + +/** + * Optional parameters for getBlobMetadata wrapper + * + * @category Microsoft + * @package MicrosoftAzure\Storage\Blob\Models + * @author Azure Storage PHP SDK + * @copyright 2016 Microsoft Corporation + * @license https://github.com/azure/azure-storage-php/LICENSE + * @link https://github.com/azure/azure-storage-php + */ +class GetBlobMetadataOptions extends BlobServiceOptions +{ + private $_snapshot; + + /** + * Gets blob snapshot. + * + * @return string + */ + public function getSnapshot() + { + return $this->_snapshot; + } + + /** + * Sets blob snapshot. + * + * @param string $snapshot value. + * + * @return void + */ + public function setSnapshot($snapshot) + { + $this->_snapshot = $snapshot; + } +} diff --git a/3rdparty/microsoft/azure-storage-blob/src/Blob/Models/GetBlobMetadataResult.php b/3rdparty/microsoft/azure-storage-blob/src/Blob/Models/GetBlobMetadataResult.php new file mode 100644 index 00000000..637f1dbe --- /dev/null +++ b/3rdparty/microsoft/azure-storage-blob/src/Blob/Models/GetBlobMetadataResult.php @@ -0,0 +1,54 @@ + + * @copyright 2016 Microsoft Corporation + * @license https://github.com/azure/azure-storage-php/LICENSE + * @link https://github.com/azure/azure-storage-php + */ + +namespace MicrosoftAzure\Storage\Blob\Models; + +use MicrosoftAzure\Storage\Common\Internal\MetadataTrait; + +/** + * Holds results of calling getBlobMetadata wrapper + * + * @category Microsoft + * @package MicrosoftAzure\Storage\Blob\Models + * @author Azure Storage PHP SDK + * @copyright 2016 Microsoft Corporation + * @license https://github.com/azure/azure-storage-php/LICENSE + * @link https://github.com/azure/azure-storage-php + */ +class GetBlobMetadataResult +{ + use MetadataTrait; + + /** + * Creates the instance from the parsed headers. + * + * @param array $parsed Parsed headers + * + * @return GetBlobMetadataResult + */ + public static function create(array $parsed) + { + return static::createMetadataResult($parsed); + } +} diff --git a/3rdparty/microsoft/azure-storage-blob/src/Blob/Models/GetBlobOptions.php b/3rdparty/microsoft/azure-storage-blob/src/Blob/Models/GetBlobOptions.php new file mode 100644 index 00000000..99f06a2d --- /dev/null +++ b/3rdparty/microsoft/azure-storage-blob/src/Blob/Models/GetBlobOptions.php @@ -0,0 +1,112 @@ + + * @copyright 2016 Microsoft Corporation + * @license https://github.com/azure/azure-storage-php/LICENSE + * @link https://github.com/azure/azure-storage-php + */ + +namespace MicrosoftAzure\Storage\Blob\Models; + +use MicrosoftAzure\Storage\Common\Internal\Validate; +use MicrosoftAzure\Storage\Common\Models\Range; + +/** + * Optional parameters for getBlob wrapper + * + * @category Microsoft + * @package MicrosoftAzure\Storage\Blob\Models + * @author Azure Storage PHP SDK + * @copyright 2016 Microsoft Corporation + * @license https://github.com/azure/azure-storage-php/LICENSE + * @link https://github.com/azure/azure-storage-php + */ +class GetBlobOptions extends BlobServiceOptions +{ + private $snapshot; + private $range; + private $rangeGetContentMD5; + + /** + * Gets blob snapshot. + * + * @return string + */ + public function getSnapshot() + { + return $this->snapshot; + } + + /** + * Sets blob snapshot. + * + * @param string $snapshot value. + * + * @return void + */ + public function setSnapshot($snapshot) + { + $this->snapshot = $snapshot; + } + + /** + * Gets Blob range. + * + * @return Range + */ + public function getRange() + { + return $this->range; + } + + /** + * Sets Blob range. + * + * @param Range $range value. + * + * @return void + */ + public function setRange(Range $range) + { + $this->range = $range; + } + + /** + * Gets rangeGetContentMD5 + * + * @return boolean + */ + public function getRangeGetContentMD5() + { + return $this->rangeGetContentMD5; + } + + /** + * Sets rangeGetContentMD5 + * + * @param boolean $rangeGetContentMD5 value + * + * @return void + */ + public function setRangeGetContentMD5($rangeGetContentMD5) + { + Validate::isBoolean($rangeGetContentMD5); + $this->rangeGetContentMD5 = $rangeGetContentMD5; + } +} diff --git a/3rdparty/microsoft/azure-storage-blob/src/Blob/Models/GetBlobPropertiesOptions.php b/3rdparty/microsoft/azure-storage-blob/src/Blob/Models/GetBlobPropertiesOptions.php new file mode 100644 index 00000000..2f4c24bc --- /dev/null +++ b/3rdparty/microsoft/azure-storage-blob/src/Blob/Models/GetBlobPropertiesOptions.php @@ -0,0 +1,62 @@ + + * @copyright 2016 Microsoft Corporation + * @license https://github.com/azure/azure-storage-php/LICENSE + * @link https://github.com/azure/azure-storage-php + */ + +namespace MicrosoftAzure\Storage\Blob\Models; + +/** + * Optional parameters for getBlobProperties wrapper + * + * @category Microsoft + * @package MicrosoftAzure\Storage\Blob\Models + * @author Azure Storage PHP SDK + * @copyright 2016 Microsoft Corporation + * @license https://github.com/azure/azure-storage-php/LICENSE + * @link https://github.com/azure/azure-storage-php + */ +class GetBlobPropertiesOptions extends BlobServiceOptions +{ + private $_snapshot; + + /** + * Gets blob snapshot. + * + * @return string + */ + public function getSnapshot() + { + return $this->_snapshot; + } + + /** + * Sets blob snapshot. + * + * @param string $snapshot value. + * + * @return void + */ + public function setSnapshot($snapshot) + { + $this->_snapshot = $snapshot; + } +} diff --git a/3rdparty/microsoft/azure-storage-blob/src/Blob/Models/GetBlobPropertiesResult.php b/3rdparty/microsoft/azure-storage-blob/src/Blob/Models/GetBlobPropertiesResult.php new file mode 100644 index 00000000..18b99925 --- /dev/null +++ b/3rdparty/microsoft/azure-storage-blob/src/Blob/Models/GetBlobPropertiesResult.php @@ -0,0 +1,84 @@ + + * @copyright 2016 Microsoft Corporation + * @license https://github.com/azure/azure-storage-php/LICENSE + * @link https://github.com/azure/azure-storage-php + */ + +namespace MicrosoftAzure\Storage\Blob\Models; + +use MicrosoftAzure\Storage\Common\Internal\MetadataTrait; + +/** + * Holds result of calling getBlobProperties + * + * @category Microsoft + * @package MicrosoftAzure\Storage\Blob\Models + * @author Azure Storage PHP SDK + * @copyright 2016 Microsoft Corporation + * @license https://github.com/azure/azure-storage-php/LICENSE + * @link https://github.com/azure/azure-storage-php + */ +class GetBlobPropertiesResult +{ + use MetadataTrait; + + private $_properties; + + /** + * Gets blob properties. + * + * @return BlobProperties + */ + public function getProperties() + { + return $this->_properties; + } + + /** + * Sets blob properties. + * + * @param BlobProperties $properties value. + * + * @return void + */ + protected function setProperties($properties) + { + $this->_properties = $properties; + } + + /** + * Create a instance using the given headers. + * + * @param array $headers response headers parsed in an array + * + * @internal + * + * @return GetBlobPropertiesResult + */ + public static function create(array $headers) + { + $result = static::createMetadataResult($headers); + + $result->setProperties(BlobProperties::createFromHttpHeaders($headers)); + + return $result; + } +} diff --git a/3rdparty/microsoft/azure-storage-blob/src/Blob/Models/GetBlobResult.php b/3rdparty/microsoft/azure-storage-blob/src/Blob/Models/GetBlobResult.php new file mode 100644 index 00000000..e4d54fa8 --- /dev/null +++ b/3rdparty/microsoft/azure-storage-blob/src/Blob/Models/GetBlobResult.php @@ -0,0 +1,134 @@ + + * @copyright 2016 Microsoft Corporation + * @license https://github.com/azure/azure-storage-php/LICENSE + * @link https://github.com/azure/azure-storage-php + */ + +namespace MicrosoftAzure\Storage\Blob\Models; + +use Psr\Http\Message\StreamInterface; + +/** + * Holds result of GetBlob API. + * + * @category Microsoft + * @package MicrosoftAzure\Storage\Blob\Models + * @author Azure Storage PHP SDK + * @copyright 2016 Microsoft Corporation + * @license https://github.com/azure/azure-storage-php/LICENSE + * @link https://github.com/azure/azure-storage-php + */ +class GetBlobResult +{ + private $properties; + private $metadata; + private $contentStream; + + /** + * Creates GetBlobResult from getBlob call. + * + * @param array $headers The HTTP response headers. + * @param StreamInterface $body The response body. + * @param array $metadata The blob metadata. + * + * @internal + * + * @return GetBlobResult + */ + public static function create( + array $headers, + StreamInterface $body, + array $metadata + ) { + $result = new GetBlobResult(); + $result->setContentStream($body->detach()); + $result->setProperties(BlobProperties::createFromHttpHeaders($headers)); + $result->setMetadata(is_null($metadata) ? array() : $metadata); + + return $result; + } + + /** + * Gets blob metadata. + * + * @return array + */ + public function getMetadata() + { + return $this->metadata; + } + + /** + * Sets blob metadata. + * + * @param array $metadata value. + * + * @return void + */ + protected function setMetadata(array $metadata) + { + $this->metadata = $metadata; + } + + /** + * Gets blob properties. + * + * @return BlobProperties + */ + public function getProperties() + { + return $this->properties; + } + + /** + * Sets blob properties. + * + * @param BlobProperties $properties value. + * + * @return void + */ + protected function setProperties(BlobProperties $properties) + { + $this->properties = $properties; + } + + /** + * Gets blob contentStream. + * + * @return resource + */ + public function getContentStream() + { + return $this->contentStream; + } + + /** + * Sets blob contentStream. + * + * @param resource $contentStream The stream handle. + * + * @return void + */ + protected function setContentStream($contentStream) + { + $this->contentStream = $contentStream; + } +} diff --git a/3rdparty/microsoft/azure-storage-blob/src/Blob/Models/GetContainerACLResult.php b/3rdparty/microsoft/azure-storage-blob/src/Blob/Models/GetContainerACLResult.php new file mode 100644 index 00000000..412c3697 --- /dev/null +++ b/3rdparty/microsoft/azure-storage-blob/src/Blob/Models/GetContainerACLResult.php @@ -0,0 +1,137 @@ + + * @copyright 2016 Microsoft Corporation + * @license https://github.com/azure/azure-storage-php/LICENSE + * @link https://github.com/azure/azure-storage-php + */ + +namespace MicrosoftAzure\Storage\Blob\Models; + +/** + * Holds container ACL + * + * @category Microsoft + * @package MicrosoftAzure\Storage\Blob\Models + * @author Azure Storage PHP SDK + * @copyright 2016 Microsoft Corporation + * @license https://github.com/azure/azure-storage-php/LICENSE + * @link https://github.com/azure/azure-storage-php + */ +class GetContainerACLResult +{ + private $containerACL; + private $lastModified; + + private $etag; + + /** + * Parses the given array into signed identifiers + * + * @param string $publicAccess container public access + * @param string $etag container etag + * @param \DateTime $lastModified last modification date + * @param array $parsed parsed response into array + * representation + * + * @internal + * + * @return self + */ + public static function create( + $publicAccess, + $etag, + \DateTime $lastModified, + array $parsed = null + ) { + $result = new GetContainerAclResult(); + $result->setETag($etag); + $result->setLastModified($lastModified); + $acl = ContainerACL::create($publicAccess, $parsed); + $result->setContainerAcl($acl); + + return $result; + } + + /** + * Gets container ACL + * + * @return ContainerACL + */ + public function getContainerAcl() + { + return $this->containerACL; + } + + /** + * Sets container ACL + * + * @param ContainerACL $containerACL value. + * + * @return void + */ + protected function setContainerAcl(ContainerACL $containerACL) + { + $this->containerACL = $containerACL; + } + + /** + * Gets container lastModified. + * + * @return \DateTime. + */ + public function getLastModified() + { + return $this->lastModified; + } + + /** + * Sets container lastModified. + * + * @param \DateTime $lastModified value. + * + * @return void + */ + protected function setLastModified(\DateTime $lastModified) + { + $this->lastModified = $lastModified; + } + + /** + * Gets container etag. + * + * @return string + */ + public function getETag() + { + return $this->etag; + } + + /** + * Sets container etag. + * + * @param string $etag value. + * + * @return void + */ + protected function setETag($etag) + { + $this->etag = $etag; + } +} diff --git a/3rdparty/microsoft/azure-storage-blob/src/Blob/Models/GetContainerPropertiesResult.php b/3rdparty/microsoft/azure-storage-blob/src/Blob/Models/GetContainerPropertiesResult.php new file mode 100644 index 00000000..4d8c4106 --- /dev/null +++ b/3rdparty/microsoft/azure-storage-blob/src/Blob/Models/GetContainerPropertiesResult.php @@ -0,0 +1,175 @@ + + * @copyright 2016 Microsoft Corporation + * @license https://github.com/azure/azure-storage-php/LICENSE + * @link https://github.com/azure/azure-storage-php + */ + +namespace MicrosoftAzure\Storage\Blob\Models; + +use MicrosoftAzure\Storage\Common\Internal\MetadataTrait; +use MicrosoftAzure\Storage\Blob\Internal\BlobResources as Resources; +use MicrosoftAzure\Storage\Common\Internal\Utilities; +use MicrosoftAzure\Storage\Common\Internal\Validate; + +/** + * Holds result of getContainerProperties and getContainerMetadata + * + * @category Microsoft + * @package MicrosoftAzure\Storage\Blob\Models + * @author Azure Storage PHP SDK + * @copyright 2016 Microsoft Corporation + * @license https://github.com/azure/azure-storage-php/LICENSE + * @link https://github.com/azure/azure-storage-php + */ +class GetContainerPropertiesResult +{ + use MetadataTrait; + + private $leaseStatus; + private $leaseState; + private $leaseDuration; + private $publicAccess; + + /** + * Gets blob leaseStatus. + * + * @return string + */ + public function getLeaseStatus() + { + return $this->leaseStatus; + } + + /** + * Sets blob leaseStatus. + * + * @param string $leaseStatus value. + * + * @return void + */ + public function setLeaseStatus($leaseStatus) + { + $this->leaseStatus = $leaseStatus; + } + + /** + * Gets blob lease state. + * + * @return string + */ + public function getLeaseState() + { + return $this->leaseState; + } + + /** + * Sets blob lease state. + * + * @param string $leaseState value. + * + * @return void + */ + public function setLeaseState($leaseState) + { + $this->leaseState = $leaseState; + } + + /** + * Gets blob lease duration. + * + * @return string + */ + public function getLeaseDuration() + { + return $this->leaseDuration; + } + + /** + * Sets blob leaseStatus. + * + * @param string $leaseDuration value. + * + * @return void + */ + public function setLeaseDuration($leaseDuration) + { + $this->leaseDuration = $leaseDuration; + } + + /** + * Gets container publicAccess. + * + * @return string + */ + public function getPublicAccess() + { + return $this->publicAccess; + } + + /** + * Sets container publicAccess. + * + * @param string $publicAccess value. + * + * @return void + */ + public function setPublicAccess($publicAccess) + { + Validate::isTrue( + PublicAccessType::isValid($publicAccess), + Resources::INVALID_BLOB_PAT_MSG + ); + $this->publicAccess = $publicAccess; + } + + /** + * Create an instance using the response headers from the API call. + * + * @param array $responseHeaders The array contains all the response headers + * + * @internal + * + * @return GetContainerPropertiesResult + */ + public static function create(array $responseHeaders) + { + $result = static::createMetadataResult($responseHeaders); + + $result->setLeaseStatus(Utilities::tryGetValueInsensitive( + Resources::X_MS_LEASE_STATUS, + $responseHeaders + )); + $result->setLeaseState(Utilities::tryGetValueInsensitive( + Resources::X_MS_LEASE_STATE, + $responseHeaders + )); + $result->setLeaseDuration(Utilities::tryGetValueInsensitive( + Resources::X_MS_LEASE_DURATION, + $responseHeaders + )); + $result->setPublicAccess(Utilities::tryGetValueInsensitive( + Resources::X_MS_BLOB_PUBLIC_ACCESS, + $responseHeaders + )); + + return $result; + } +} diff --git a/3rdparty/microsoft/azure-storage-blob/src/Blob/Models/LeaseMode.php b/3rdparty/microsoft/azure-storage-blob/src/Blob/Models/LeaseMode.php new file mode 100644 index 00000000..03bc72f3 --- /dev/null +++ b/3rdparty/microsoft/azure-storage-blob/src/Blob/Models/LeaseMode.php @@ -0,0 +1,44 @@ + + * @copyright 2016 Microsoft Corporation + * @license https://github.com/azure/azure-storage-php/LICENSE + * @link https://github.com/azure/azure-storage-php + */ + +namespace MicrosoftAzure\Storage\Blob\Models; + +/** + * Modes for leasing a blob + * + * @category Microsoft + * @package MicrosoftAzure\Storage\Blob\Models + * @author Azure Storage PHP SDK + * @copyright 2016 Microsoft Corporation + * @license https://github.com/azure/azure-storage-php/LICENSE + * @link https://github.com/azure/azure-storage-php + */ +class LeaseMode +{ + const ACQUIRE_ACTION = 'acquire'; + const RENEW_ACTION = 'renew'; + const RELEASE_ACTION = 'release'; + const BREAK_ACTION = 'break'; + const CHANGE_ACTION = 'change'; +} diff --git a/3rdparty/microsoft/azure-storage-blob/src/Blob/Models/LeaseResult.php b/3rdparty/microsoft/azure-storage-blob/src/Blob/Models/LeaseResult.php new file mode 100644 index 00000000..f29121da --- /dev/null +++ b/3rdparty/microsoft/azure-storage-blob/src/Blob/Models/LeaseResult.php @@ -0,0 +1,85 @@ + + * @copyright 2016 Microsoft Corporation + * @license https://github.com/azure/azure-storage-php/LICENSE + * @link https://github.com/azure/azure-storage-php + */ + +namespace MicrosoftAzure\Storage\Blob\Models; + +use MicrosoftAzure\Storage\Blob\Internal\BlobResources as Resources; +use MicrosoftAzure\Storage\Common\Internal\Utilities; + +/** + * The result of calling acquireLease API. + * + * @category Microsoft + * @package MicrosoftAzure\Storage\Blob\Models + * @author Azure Storage PHP SDK + * @copyright 2016 Microsoft Corporation + * @license https://github.com/azure/azure-storage-php/LICENSE + * @link https://github.com/azure/azure-storage-php + */ +class LeaseResult +{ + private $leaseId; + + /** + * Creates LeaseResult from response headers + * + * @param array $headers response headers + * + * @internal + * + * @return \MicrosoftAzure\Storage\Blob\Models\LeaseResult + */ + public static function create(array $headers) + { + $result = new LeaseResult(); + + $result->setLeaseId( + Utilities::tryGetValue($headers, Resources::X_MS_LEASE_ID) + ); + + return $result; + } + + /** + * Gets lease Id for the blob + * + * @return string + */ + public function getLeaseId() + { + return $this->leaseId; + } + + /** + * Sets lease Id for the blob + * + * @param string $leaseId the blob lease id. + * + * @return void + */ + protected function setLeaseId($leaseId) + { + $this->leaseId = $leaseId; + } +} diff --git a/3rdparty/microsoft/azure-storage-blob/src/Blob/Models/ListBlobBlocksOptions.php b/3rdparty/microsoft/azure-storage-blob/src/Blob/Models/ListBlobBlocksOptions.php new file mode 100644 index 00000000..043b8aa7 --- /dev/null +++ b/3rdparty/microsoft/azure-storage-blob/src/Blob/Models/ListBlobBlocksOptions.php @@ -0,0 +1,141 @@ + + * @copyright 2016 Microsoft Corporation + * @license https://github.com/azure/azure-storage-php/LICENSE + * @link https://github.com/azure/azure-storage-php + */ + +namespace MicrosoftAzure\Storage\Blob\Models; + +use MicrosoftAzure\Storage\Common\Internal\Validate; + +/** + * Optional parameters for listBlobBlock wrapper + * + * @category Microsoft + * @package MicrosoftAzure\Storage\Blob\Models + * @author Azure Storage PHP SDK + * @copyright 2016 Microsoft Corporation + * @license https://github.com/azure/azure-storage-php/LICENSE + * @link https://github.com/azure/azure-storage-php + */ +class ListBlobBlocksOptions extends BlobServiceOptions +{ + private $_snapshot; + private $_includeUncommittedBlobs; + private $_includeCommittedBlobs; + private static $_listType; + + /** + * Constructs the static variable $listType. + */ + public function __construct() + { + parent::__construct(); + self::$_listType[true][true] = 'all'; + self::$_listType[true][false] = 'uncommitted'; + self::$_listType[false][true] = 'committed'; + self::$_listType[false][false] = 'all'; + + $this->_includeUncommittedBlobs = false; + $this->_includeCommittedBlobs = false; + } + + /** + * Gets blob snapshot. + * + * @return string + */ + public function getSnapshot() + { + return $this->_snapshot; + } + + /** + * Sets blob snapshot. + * + * @param string $snapshot value. + * + * @return void + */ + public function setSnapshot($snapshot) + { + $this->_snapshot = $snapshot; + } + + /** + * Sets the include uncommittedBlobs flag. + * + * @param bool $includeUncommittedBlobs value. + * + * @return void + */ + public function setIncludeUncommittedBlobs($includeUncommittedBlobs) + { + Validate::isBoolean($includeUncommittedBlobs); + $this->_includeUncommittedBlobs = $includeUncommittedBlobs; + } + + /** + * Indicates if uncommittedBlobs is included or not. + * + * @return boolean + */ + public function getIncludeUncommittedBlobs() + { + return $this->_includeUncommittedBlobs; + } + + /** + * Sets the include committedBlobs flag. + * + * @param bool $includeCommittedBlobs value. + * + * @return void + */ + public function setIncludeCommittedBlobs($includeCommittedBlobs) + { + Validate::isBoolean($includeCommittedBlobs); + $this->_includeCommittedBlobs = $includeCommittedBlobs; + } + + /** + * Indicates if committedBlobs is included or not. + * + * @return boolean + */ + public function getIncludeCommittedBlobs() + { + return $this->_includeCommittedBlobs; + } + + /** + * Gets block list type. + * + * @return string + */ + public function getBlockListType() + { + $includeUncommitted = $this->_includeUncommittedBlobs; + $includeCommitted = $this->_includeCommittedBlobs; + + return self::$_listType[$includeUncommitted][$includeCommitted]; + } +} diff --git a/3rdparty/microsoft/azure-storage-blob/src/Blob/Models/ListBlobBlocksResult.php b/3rdparty/microsoft/azure-storage-blob/src/Blob/Models/ListBlobBlocksResult.php new file mode 100644 index 00000000..c9421ca9 --- /dev/null +++ b/3rdparty/microsoft/azure-storage-blob/src/Blob/Models/ListBlobBlocksResult.php @@ -0,0 +1,252 @@ + + * @copyright 2016 Microsoft Corporation + * @license https://github.com/azure/azure-storage-php/LICENSE + * @link https://github.com/azure/azure-storage-php + */ + +namespace MicrosoftAzure\Storage\Blob\Models; + +use MicrosoftAzure\Storage\Common\Internal\Validate; +use MicrosoftAzure\Storage\Blob\Internal\BlobResources as Resources; +use MicrosoftAzure\Storage\Common\Internal\Utilities; + +/** + * Holds result of listBlobBlocks + * + * @category Microsoft + * @package MicrosoftAzure\Storage\Blob\Models + * @author Azure Storage PHP SDK + * @copyright 2016 Microsoft Corporation + * @license https://github.com/azure/azure-storage-php/LICENSE + * @link https://github.com/azure/azure-storage-php + */ +class ListBlobBlocksResult +{ + private $lastModified; + private $etag; + private $contentType; + private $contentLength; + private $committedBlocks; + private $uncommittedBlocks; + + /** + * Gets block entries from parsed response + * + * @param array $parsed HTTP response + * @param string $type Block type + * + * @return array + */ + private static function getEntries(array $parsed, $type) + { + $entries = array(); + + if (is_array($parsed)) { + $rawEntries = array(); + + if (array_key_exists($type, $parsed) + && is_array($parsed[$type]) + && !empty($parsed[$type]) + ) { + $rawEntries = Utilities::getArray($parsed[$type]['Block']); + } + + foreach ($rawEntries as $value) { + $entries[$value['Name']] = $value['Size']; + } + } + + return $entries; + } + + /** + * Creates ListBlobBlocksResult from given response headers and parsed body + * + * @param array $headers HTTP response headers + * @param array $parsed HTTP response body in array representation + * + * @internal + * + * @return ListBlobBlocksResult + */ + public static function create(array $headers, array $parsed) + { + $result = new ListBlobBlocksResult(); + $clean = array_change_key_case($headers); + + $result->setETag(Utilities::tryGetValue($clean, Resources::ETAG)); + $date = Utilities::tryGetValue($clean, Resources::LAST_MODIFIED); + if (!is_null($date)) { + $date = Utilities::rfc1123ToDateTime($date); + $result->setLastModified($date); + } + $result->setContentLength( + intval( + Utilities::tryGetValue($clean, Resources::X_MS_BLOB_CONTENT_LENGTH) + ) + ); + $result->setContentType( + Utilities::tryGetValue($clean, Resources::CONTENT_TYPE_LOWER_CASE) + ); + + $result->uncommittedBlocks = self::getEntries( + $parsed, + 'UncommittedBlocks' + ); + $result->committedBlocks = self::getEntries($parsed, 'CommittedBlocks'); + + return $result; + } + + /** + * Gets blob lastModified. + * + * @return \DateTime + */ + public function getLastModified() + { + return $this->lastModified; + } + + /** + * Sets blob lastModified. + * + * @param \DateTime $lastModified value. + * + * @return void + */ + protected function setLastModified(\DateTime $lastModified) + { + Validate::isDate($lastModified); + $this->lastModified = $lastModified; + } + + /** + * Gets blob etag. + * + * @return string + */ + public function getETag() + { + return $this->etag; + } + + /** + * Sets blob etag. + * + * @param string $etag value. + * + * @return void + */ + protected function setETag($etag) + { + $this->etag = $etag; + } + + /** + * Gets blob contentType. + * + * @return string + */ + public function getContentType() + { + return $this->contentType; + } + + /** + * Sets blob contentType. + * + * @param string $contentType value. + * + * @return void + */ + protected function setContentType($contentType) + { + $this->contentType = $contentType; + } + + /** + * Gets blob contentLength. + * + * @return integer + */ + public function getContentLength() + { + return $this->contentLength; + } + + /** + * Sets blob contentLength. + * + * @param integer $contentLength value. + * + * @return void + */ + protected function setContentLength($contentLength) + { + Validate::isInteger($contentLength, 'contentLength'); + $this->contentLength = $contentLength; + } + + /** + * Gets uncommitted blocks + * + * @return array + */ + public function getUncommittedBlocks() + { + return $this->uncommittedBlocks; + } + + /** + * Sets uncommitted blocks + * + * @param array $uncommittedBlocks The uncommitted blocks entries + * + * @return void + */ + protected function setUncommittedBlocks(array $uncommittedBlocks) + { + $this->uncommittedBlocks = $uncommittedBlocks; + } + + /** + * Gets committed blocks + * + * @return array + */ + public function getCommittedBlocks() + { + return $this->committedBlocks; + } + + /** + * Sets committed blocks + * + * @param array $committedBlocks The committed blocks entries + * + * @return void + */ + protected function setCommittedBlocks(array $committedBlocks) + { + $this->committedBlocks = $committedBlocks; + } +} diff --git a/3rdparty/microsoft/azure-storage-blob/src/Blob/Models/ListBlobsOptions.php b/3rdparty/microsoft/azure-storage-blob/src/Blob/Models/ListBlobsOptions.php new file mode 100644 index 00000000..ad8f31a4 --- /dev/null +++ b/3rdparty/microsoft/azure-storage-blob/src/Blob/Models/ListBlobsOptions.php @@ -0,0 +1,236 @@ + + * @copyright 2016 Microsoft Corporation + * @license https://github.com/azure/azure-storage-php/LICENSE + * @link https://github.com/azure/azure-storage-php + */ + +namespace MicrosoftAzure\Storage\Blob\Models; + +use MicrosoftAzure\Storage\Common\Internal\Validate; +use MicrosoftAzure\Storage\Common\MarkerContinuationTokenTrait; + +/** + * Optional parameters for listBlobs API. + * + * @category Microsoft + * @package MicrosoftAzure\Storage\Blob\Models + * @author Azure Storage PHP SDK + * @copyright 2016 Microsoft Corporation + * @license https://github.com/azure/azure-storage-php/LICENSE + * @link https://github.com/azure/azure-storage-php + */ +class ListBlobsOptions extends BlobServiceOptions +{ + use MarkerContinuationTokenTrait; + + private $_prefix; + private $_delimiter; + private $_maxResults; + private $_includeMetadata; + private $_includeSnapshots; + private $_includeUncommittedBlobs; + private $_includeCopy; + private $_includeDeleted; + + /** + * Gets prefix. + * + * @return string + */ + public function getPrefix() + { + return $this->_prefix; + } + + /** + * Sets prefix. + * + * @param string $prefix value. + * + * @return void + */ + public function setPrefix($prefix) + { + Validate::canCastAsString($prefix, 'prefix'); + $this->_prefix = $prefix; + } + + /** + * Gets delimiter. + * + * @return string + */ + public function getDelimiter() + { + return $this->_delimiter; + } + + /** + * Sets prefix. + * + * @param string $delimiter value. + * + * @return void + */ + public function setDelimiter($delimiter) + { + Validate::canCastAsString($delimiter, 'delimiter'); + $this->_delimiter = $delimiter; + } + + /** + * Gets max results. + * + * @return integer + */ + public function getMaxResults() + { + return $this->_maxResults; + } + + /** + * Sets max results. + * + * @param integer $maxResults value. + * + * @return void + */ + public function setMaxResults($maxResults) + { + Validate::isInteger($maxResults, 'maxResults'); + $this->_maxResults = $maxResults; + } + + /** + * Indicates if metadata is included or not. + * + * @return boolean + */ + public function getIncludeMetadata() + { + return $this->_includeMetadata; + } + + /** + * Sets the include metadata flag. + * + * @param bool $includeMetadata value. + * + * @return void + */ + public function setIncludeMetadata($includeMetadata) + { + Validate::isBoolean($includeMetadata); + $this->_includeMetadata = $includeMetadata; + } + + /** + * Indicates if snapshots is included or not. + * + * @return boolean + */ + public function getIncludeSnapshots() + { + return $this->_includeSnapshots; + } + + /** + * Sets the include snapshots flag. + * + * @param bool $includeSnapshots value. + * + * @return void + */ + public function setIncludeSnapshots($includeSnapshots) + { + Validate::isBoolean($includeSnapshots); + $this->_includeSnapshots = $includeSnapshots; + } + + /** + * Indicates if uncommittedBlobs is included or not. + * + * @return boolean + */ + public function getIncludeUncommittedBlobs() + { + return $this->_includeUncommittedBlobs; + } + + /** + * Sets the include uncommittedBlobs flag. + * + * @param bool $includeUncommittedBlobs value. + * + * @return void + */ + public function setIncludeUncommittedBlobs($includeUncommittedBlobs) + { + Validate::isBoolean($includeUncommittedBlobs); + $this->_includeUncommittedBlobs = $includeUncommittedBlobs; + } + + /** + * Indicates if copy is included or not. + * + * @return boolean + */ + public function getIncludeCopy() + { + return $this->_includeCopy; + } + + /** + * Sets the include copy flag. + * + * @param bool $includeCopy value. + * + * @return void + */ + public function setIncludeCopy($includeCopy) + { + Validate::isBoolean($includeCopy); + $this->_includeCopy = $includeCopy; + } + + /** + * Indicates if deleted is included or not. + * + * @return boolean + */ + public function getIncludeDeleted() + { + return $this->_includeDeleted; + } + + /** + * Sets the include deleted flag. + * + * @param bool $includeDeleted value. + * + * @return void + */ + public function setIncludeDeleted($includeDeleted) + { + Validate::isBoolean($includeDeleted); + $this->_includeDeleted = $includeDeleted; + } +} diff --git a/3rdparty/microsoft/azure-storage-blob/src/Blob/Models/ListBlobsResult.php b/3rdparty/microsoft/azure-storage-blob/src/Blob/Models/ListBlobsResult.php new file mode 100644 index 00000000..8de0c41e --- /dev/null +++ b/3rdparty/microsoft/azure-storage-blob/src/Blob/Models/ListBlobsResult.php @@ -0,0 +1,313 @@ + + * @copyright 2016 Microsoft Corporation + * @license https://github.com/azure/azure-storage-php/LICENSE + * @link https://github.com/azure/azure-storage-php + */ + +namespace MicrosoftAzure\Storage\Blob\Models; + +use MicrosoftAzure\Storage\Blob\Internal\BlobResources as Resources; +use MicrosoftAzure\Storage\Common\Internal\Utilities; +use MicrosoftAzure\Storage\Common\MarkerContinuationTokenTrait; +use MicrosoftAzure\Storage\Common\Models\MarkerContinuationToken; + +/** + * Hold result of calliing listBlobs wrapper. + * + * @category Microsoft + * @package MicrosoftAzure\Storage\Blob\Models + * @author Azure Storage PHP SDK + * @copyright 2016 Microsoft Corporation + * @license https://github.com/azure/azure-storage-php/LICENSE + * @link https://github.com/azure/azure-storage-php + */ +class ListBlobsResult +{ + use MarkerContinuationTokenTrait; + + private $blobPrefixes; + private $blobs; + private $delimiter; + private $prefix; + private $marker; + private $maxResults; + private $containerName; + + /** + * Creates ListBlobsResult object from parsed XML response. + * + * @param array $parsed XML response parsed into array. + * @param string $location Contains the location for the previous + * request. + * + * @internal + * + * @return ListBlobsResult + */ + public static function create(array $parsed, $location = '') + { + $result = new ListBlobsResult(); + $serviceEndpoint = Utilities::tryGetKeysChainValue( + $parsed, + Resources::XTAG_ATTRIBUTES, + Resources::XTAG_SERVICE_ENDPOINT + ); + $containerName = Utilities::tryGetKeysChainValue( + $parsed, + Resources::XTAG_ATTRIBUTES, + Resources::XTAG_CONTAINER_NAME + ); + $result->setContainerName($containerName); + $result->setPrefix(Utilities::tryGetValue( + $parsed, + Resources::QP_PREFIX + )); + $result->setMarker(Utilities::tryGetValue( + $parsed, + Resources::QP_MARKER + )); + + $nextMarker = + Utilities::tryGetValue($parsed, Resources::QP_NEXT_MARKER); + + if ($nextMarker != null) { + $result->setContinuationToken( + new MarkerContinuationToken( + $nextMarker, + $location + ) + ); + } + + $result->setMaxResults(intval( + Utilities::tryGetValue($parsed, Resources::QP_MAX_RESULTS, 0) + )); + $result->setDelimiter(Utilities::tryGetValue( + $parsed, + Resources::QP_DELIMITER + )); + $blobs = array(); + $blobPrefixes = array(); + $rawBlobs = array(); + $rawBlobPrefixes = array(); + + if (is_array($parsed['Blobs']) + && array_key_exists('Blob', $parsed['Blobs']) + ) { + $rawBlobs = Utilities::getArray($parsed['Blobs']['Blob']); + } + + foreach ($rawBlobs as $value) { + $blob = new Blob(); + $blob->setName($value['Name']); + $blob->setUrl($serviceEndpoint . $containerName . '/' . $value['Name']); + $blob->setSnapshot(Utilities::tryGetValue($value, 'Snapshot')); + $blob->setProperties( + BlobProperties::createFromXml( + Utilities::tryGetValue($value, 'Properties') + ) + ); + $blob->setMetadata( + Utilities::tryGetValue($value, Resources::QP_METADATA, array()) + ); + + $blobs[] = $blob; + } + + if (is_array($parsed['Blobs']) + && array_key_exists('BlobPrefix', $parsed['Blobs']) + ) { + $rawBlobPrefixes = Utilities::getArray($parsed['Blobs']['BlobPrefix']); + } + + foreach ($rawBlobPrefixes as $value) { + $blobPrefix = new BlobPrefix(); + $blobPrefix->setName($value['Name']); + + $blobPrefixes[] = $blobPrefix; + } + + $result->setBlobs($blobs); + $result->setBlobPrefixes($blobPrefixes); + + return $result; + } + + /** + * Gets blobs. + * + * @return Blob[] + */ + public function getBlobs() + { + return $this->blobs; + } + + /** + * Sets blobs. + * + * @param Blob[] $blobs list of blobs + * + * @return void + */ + protected function setBlobs(array $blobs) + { + $this->blobs = array(); + foreach ($blobs as $blob) { + $this->blobs[] = clone $blob; + } + } + + /** + * Gets blobPrefixes. + * + * @return array + */ + public function getBlobPrefixes() + { + return $this->blobPrefixes; + } + + /** + * Sets blobPrefixes. + * + * @param array $blobPrefixes list of blobPrefixes + * + * @return void + */ + protected function setBlobPrefixes(array $blobPrefixes) + { + $this->blobPrefixes = array(); + foreach ($blobPrefixes as $blob) { + $this->blobPrefixes[] = clone $blob; + } + } + + /** + * Gets prefix. + * + * @return string + */ + public function getPrefix() + { + return $this->prefix; + } + + /** + * Sets prefix. + * + * @param string $prefix value. + * + * @return void + */ + protected function setPrefix($prefix) + { + $this->prefix = $prefix; + } + + /** + * Gets prefix. + * + * @return string + */ + public function getDelimiter() + { + return $this->delimiter; + } + + /** + * Sets prefix. + * + * @param string $delimiter value. + * + * @return void + */ + protected function setDelimiter($delimiter) + { + $this->delimiter = $delimiter; + } + + /** + * Gets marker. + * + * @return string + */ + public function getMarker() + { + return $this->marker; + } + + /** + * Sets marker. + * + * @param string $marker value. + * + * @return void + */ + protected function setMarker($marker) + { + $this->marker = $marker; + } + + /** + * Gets max results. + * + * @return integer + */ + public function getMaxResults() + { + return $this->maxResults; + } + + /** + * Sets max results. + * + * @param integer $maxResults value. + * + * @return void + */ + protected function setMaxResults($maxResults) + { + $this->maxResults = $maxResults; + } + + /** + * Gets container name. + * + * @return string + */ + public function getContainerName() + { + return $this->containerName; + } + + /** + * Sets container name. + * + * @param string $containerName value. + * + * @return void + */ + protected function setContainerName($containerName) + { + $this->containerName = $containerName; + } +} diff --git a/3rdparty/microsoft/azure-storage-blob/src/Blob/Models/ListContainersOptions.php b/3rdparty/microsoft/azure-storage-blob/src/Blob/Models/ListContainersOptions.php new file mode 100644 index 00000000..aeb7195f --- /dev/null +++ b/3rdparty/microsoft/azure-storage-blob/src/Blob/Models/ListContainersOptions.php @@ -0,0 +1,126 @@ + + * @copyright 2016 Microsoft Corporation + * @license https://github.com/azure/azure-storage-php/LICENSE + * @link https://github.com/azure/azure-storage-php + */ + +namespace MicrosoftAzure\Storage\Blob\Models; + +use MicrosoftAzure\Storage\Common\MarkerContinuationTokenTrait; +use MicrosoftAzure\Storage\Common\Internal\Validate; + +/** + * Options for listBlobs API. + * + * @category Microsoft + * @package MicrosoftAzure\Storage\Blob\Models + * @author Azure Storage PHP SDK + * @copyright 2016 Microsoft Corporation + * @license https://github.com/azure/azure-storage-php/LICENSE + * @link https://github.com/azure/azure-storage-php + */ +class ListContainersOptions extends BlobServiceOptions +{ + use MarkerContinuationTokenTrait; + + private $_prefix; + private $_maxResults; + private $_includeMetadata; + + /** + * Gets prefix - filters the results to return only containers whose name + * begins with the specified prefix. + * + * @return string + */ + public function getPrefix() + { + return $this->_prefix; + } + + /** + * Sets prefix - filters the results to return only containers whose name + * begins with the specified prefix. + * + * @param string $prefix value. + * + * @return void + */ + public function setPrefix($prefix) + { + Validate::canCastAsString($prefix, 'prefix'); + $this->_prefix = $prefix; + } + + /** + * Gets max results which specifies the maximum number of containers to return. + * If the request does not specify maxresults, or specifies a value + * greater than 5,000, the server will return up to 5,000 items. + * If the parameter is set to a value less than or equal to zero, + * the server will return status code 400 (Bad Request). + * + * @return string + */ + public function getMaxResults() + { + return $this->_maxResults; + } + + /** + * Sets max results which specifies the maximum number of containers to return. + * If the request does not specify maxresults, or specifies a value + * greater than 5,000, the server will return up to 5,000 items. + * If the parameter is set to a value less than or equal to zero, + * the server will return status code 400 (Bad Request). + * + * @param string $maxResults value. + * + * @return void + */ + public function setMaxResults($maxResults) + { + Validate::canCastAsString($maxResults, 'maxResults'); + $this->_maxResults = $maxResults; + } + + /** + * Indicates if metadata is included or not. + * + * @return string + */ + public function getIncludeMetadata() + { + return $this->_includeMetadata; + } + + /** + * Sets the include metadata flag. + * + * @param bool $includeMetadata value. + * + * @return void + */ + public function setIncludeMetadata($includeMetadata) + { + Validate::isBoolean($includeMetadata); + $this->_includeMetadata = $includeMetadata; + } +} diff --git a/3rdparty/microsoft/azure-storage-blob/src/Blob/Models/ListContainersResult.php b/3rdparty/microsoft/azure-storage-blob/src/Blob/Models/ListContainersResult.php new file mode 100644 index 00000000..f0943bab --- /dev/null +++ b/3rdparty/microsoft/azure-storage-blob/src/Blob/Models/ListContainersResult.php @@ -0,0 +1,251 @@ + + * @copyright 2016 Microsoft Corporation + * @license https://github.com/azure/azure-storage-php/LICENSE + * @link https://github.com/azure/azure-storage-php + */ + +namespace MicrosoftAzure\Storage\Blob\Models; + +use MicrosoftAzure\Storage\Blob\Internal\BlobResources as Resources; +use MicrosoftAzure\Storage\Common\Internal\Utilities; +use MicrosoftAzure\Storage\Common\Models\MarkerContinuationToken; +use MicrosoftAzure\Storage\Common\MarkerContinuationTokenTrait; + +/** + * Container to hold list container response object. + * + * @category Microsoft + * @package MicrosoftAzure\Storage\Blob\Models + * @author Azure Storage PHP SDK + * @copyright 2016 Microsoft Corporation + * @license https://github.com/azure/azure-storage-php/LICENSE + * @link https://github.com/azure/azure-storage-php + */ +class ListContainersResult +{ + use MarkerContinuationTokenTrait; + + private $containers; + private $prefix; + private $marker; + private $maxResults; + private $accountName; + + /** + * Creates ListBlobResult object from parsed XML response. + * + * @param array $parsedResponse XML response parsed into array. + * @param string $location Contains the location for the previous + * request. + * + * @internal + * + * @return ListContainersResult + */ + public static function create(array $parsedResponse, $location = '') + { + $result = new ListContainersResult(); + $serviceEndpoint = Utilities::tryGetKeysChainValue( + $parsedResponse, + Resources::XTAG_ATTRIBUTES, + Resources::XTAG_SERVICE_ENDPOINT + ); + $result->setAccountName(Utilities::tryParseAccountNameFromUrl( + $serviceEndpoint + )); + $result->setPrefix(Utilities::tryGetValue( + $parsedResponse, + Resources::QP_PREFIX + )); + $result->setMarker(Utilities::tryGetValue( + $parsedResponse, + Resources::QP_MARKER + )); + + $nextMarker = + Utilities::tryGetValue($parsedResponse, Resources::QP_NEXT_MARKER); + + if ($nextMarker != null) { + $result->setContinuationToken( + new MarkerContinuationToken( + $nextMarker, + $location + ) + ); + } + + $result->setMaxResults(Utilities::tryGetValue( + $parsedResponse, + Resources::QP_MAX_RESULTS + )); + $containers = array(); + $rawContainer = array(); + + if (!empty($parsedResponse['Containers'])) { + $containersArray = $parsedResponse['Containers']['Container']; + $rawContainer = Utilities::getArray($containersArray); + } + + foreach ($rawContainer as $value) { + $container = new Container(); + $container->setName($value['Name']); + $container->setUrl($serviceEndpoint . $value['Name']); + $container->setMetadata( + Utilities::tryGetValue($value, Resources::QP_METADATA, array()) + ); + $properties = new ContainerProperties(); + $date = $value['Properties']['Last-Modified']; + $date = Utilities::rfc1123ToDateTime($date); + $properties->setLastModified($date); + $properties->setETag(Utilities::tryGetValueInsensitive(Resources::ETAG, $value['Properties'])); + + if (array_key_exists('LeaseStatus', $value['Properties'])) { + $properties->setLeaseStatus($value['Properties']['LeaseStatus']); + } + if (array_key_exists('LeaseState', $value['Properties'])) { + $properties->setLeaseStatus($value['Properties']['LeaseState']); + } + if (array_key_exists('LeaseDuration', $value['Properties'])) { + $properties->setLeaseStatus($value['Properties']['LeaseDuration']); + } + if (array_key_exists('PublicAccess', $value['Properties'])) { + $properties->setPublicAccess($value['Properties']['PublicAccess']); + } + $container->setProperties($properties); + $containers[] = $container; + } + $result->setContainers($containers); + return $result; + } + + /** + * Sets containers. + * + * @param array $containers list of containers. + * + * @return void + */ + protected function setContainers(array $containers) + { + $this->containers = array(); + foreach ($containers as $container) { + $this->containers[] = clone $container; + } + } + + /** + * Gets containers. + * + * @return Container[] + */ + public function getContainers() + { + return $this->containers; + } + + /** + * Gets prefix. + * + * @return string + */ + public function getPrefix() + { + return $this->prefix; + } + + /** + * Sets prefix. + * + * @param string $prefix value. + * + * @return void + */ + protected function setPrefix($prefix) + { + $this->prefix = $prefix; + } + + /** + * Gets marker. + * + * @return string + */ + public function getMarker() + { + return $this->marker; + } + + /** + * Sets marker. + * + * @param string $marker value. + * + * @return void + */ + protected function setMarker($marker) + { + $this->marker = $marker; + } + + /** + * Gets max results. + * + * @return string + */ + public function getMaxResults() + { + return $this->maxResults; + } + + /** + * Sets max results. + * + * @param string $maxResults value. + * + * @return void + */ + protected function setMaxResults($maxResults) + { + $this->maxResults = $maxResults; + } + + /** + * Gets account name. + * + * @return string + */ + public function getAccountName() + { + return $this->accountName; + } + + /** + * Sets account name. + * + * @param string $accountName value. + * + * @return void + */ + protected function setAccountName($accountName) + { + $this->accountName = $accountName; + } +} diff --git a/3rdparty/microsoft/azure-storage-blob/src/Blob/Models/ListPageBlobRangesDiffResult.php b/3rdparty/microsoft/azure-storage-blob/src/Blob/Models/ListPageBlobRangesDiffResult.php new file mode 100644 index 00000000..9dddd583 --- /dev/null +++ b/3rdparty/microsoft/azure-storage-blob/src/Blob/Models/ListPageBlobRangesDiffResult.php @@ -0,0 +1,101 @@ + + * @copyright 2017 Microsoft Corporation + * @license https://github.com/azure/azure-storage-php/LICENSE + * @link https://github.com/azure/azure-storage-php + */ + +namespace MicrosoftAzure\Storage\Blob\Models; + +use MicrosoftAzure\Storage\Common\Internal\Utilities; +use MicrosoftAzure\Storage\Blob\Internal\BlobResources as Resources; +use MicrosoftAzure\Storage\Common\Models\RangeDiff; + +/** + * Holds result of calling listPageBlobRangesDiff wrapper + * + * @category Microsoft + * @package MicrosoftAzure\Storage\Blob\Models + * @author Azure Storage PHP SDK + * @copyright 2017 Microsoft Corporation + * @license https://github.com/azure/azure-storage-php/LICENSE + * @link https://github.com/azure/azure-storage-php + */ +class ListPageBlobRangesDiffResult extends ListPageBlobRangesResult +{ + /** + * Creates ListPageBlobRangesDiffResult object from $parsed response in array representation + * + * @param array $headers HTTP response headers + * @param array $parsed parsed response in array format. + * + * @internal + * + * @return ListPageBlobRangesDiffResult + */ + public static function create(array $headers, array $parsed = null) + { + $result = new ListPageBlobRangesDiffResult(); + $headers = array_change_key_case($headers); + + $date = $headers[Resources::LAST_MODIFIED]; + $date = Utilities::rfc1123ToDateTime($date); + $blobLength = intval($headers[Resources::X_MS_BLOB_CONTENT_LENGTH]); + + $result->setContentLength($blobLength); + $result->setLastModified($date); + $result->setETag($headers[Resources::ETAG]); + + if (is_null($parsed)) { + return $result; + } + + $parsed = array_change_key_case($parsed); + + $rawRanges = array(); + if (!empty($parsed[strtolower(Resources::XTAG_PAGE_RANGE)])) { + $rawRanges = Utilities::getArray($parsed[strtolower(Resources::XTAG_PAGE_RANGE)]); + } + + $pageRanges = array(); + foreach ($rawRanges as $value) { + $pageRanges[] = new RangeDiff( + intval($value[Resources::XTAG_RANGE_START]), + intval($value[Resources::XTAG_RANGE_END]) + ); + } + + $rawRanges = array(); + if (!empty($parsed[strtolower(Resources::XTAG_CLEAR_RANGE)])) { + $rawRanges = Utilities::getArray($parsed[strtolower(Resources::XTAG_CLEAR_RANGE)]); + } + + foreach ($rawRanges as $value) { + $pageRanges[] = new RangeDiff( + intval($value[Resources::XTAG_RANGE_START]), + intval($value[Resources::XTAG_RANGE_END]), + true + ); + } + + $result->setRanges($pageRanges); + return $result; + } +} diff --git a/3rdparty/microsoft/azure-storage-blob/src/Blob/Models/ListPageBlobRangesOptions.php b/3rdparty/microsoft/azure-storage-blob/src/Blob/Models/ListPageBlobRangesOptions.php new file mode 100644 index 00000000..17279b1c --- /dev/null +++ b/3rdparty/microsoft/azure-storage-blob/src/Blob/Models/ListPageBlobRangesOptions.php @@ -0,0 +1,90 @@ + + * @copyright 2016 Microsoft Corporation + * @license https://github.com/azure/azure-storage-php/LICENSE + * @link https://github.com/azure/azure-storage-php + */ + +namespace MicrosoftAzure\Storage\Blob\Models; + +use MicrosoftAzure\Storage\Common\Internal\Validate; +use MicrosoftAzure\Storage\Common\Models\Range; + +/** + * Optional parameters for listPageBlobRanges wrapper + * + * @category Microsoft + * @package MicrosoftAzure\Storage\Blob\Models + * @author Azure Storage PHP SDK + * @copyright 2016 Microsoft Corporation + * @license https://github.com/azure/azure-storage-php/LICENSE + * @link https://github.com/azure/azure-storage-php + */ +class ListPageBlobRangesOptions extends BlobServiceOptions +{ + private $snapshot; + private $range; + private $_rangeStart; + private $_rangeEnd; + + /** + * Gets blob snapshot. + * + * @return string + */ + public function getSnapshot() + { + return $this->snapshot; + } + + /** + * Sets blob snapshot. + * + * @param string $snapshot value. + * + * @return void + */ + public function setSnapshot($snapshot) + { + $this->snapshot = $snapshot; + } + + /** + * Gets Blob range. + * + * @return Range + */ + public function getRange() + { + return $this->range; + } + + /** + * Sets Blob range. + * + * @param Range $range value. + * + * @return void + */ + public function setRange(Range $range) + { + $this->range = $range; + } +} diff --git a/3rdparty/microsoft/azure-storage-blob/src/Blob/Models/ListPageBlobRangesResult.php b/3rdparty/microsoft/azure-storage-blob/src/Blob/Models/ListPageBlobRangesResult.php new file mode 100644 index 00000000..7c40d498 --- /dev/null +++ b/3rdparty/microsoft/azure-storage-blob/src/Blob/Models/ListPageBlobRangesResult.php @@ -0,0 +1,182 @@ + + * @copyright 2016 Microsoft Corporation + * @license https://github.com/azure/azure-storage-php/LICENSE + * @link https://github.com/azure/azure-storage-php + */ + +namespace MicrosoftAzure\Storage\Blob\Models; + +use MicrosoftAzure\Storage\Common\Internal\Validate; +use MicrosoftAzure\Storage\Common\Internal\Utilities; +use MicrosoftAzure\Storage\Blob\Internal\BlobResources as Resources; +use MicrosoftAzure\Storage\Common\Models\Range; + +/** + * Holds result of calling listPageBlobRanges wrapper + * + * @category Microsoft + * @package MicrosoftAzure\Storage\Blob\Models + * @author Azure Storage PHP SDK + * @copyright 2016 Microsoft Corporation + * @license https://github.com/azure/azure-storage-php/LICENSE + * @link https://github.com/azure/azure-storage-php + */ +class ListPageBlobRangesResult +{ + private $_lastModified; + private $_etag; + private $_contentLength; + private $_pageRanges; + + /** + * Creates BlobProperties object from $parsed response in array representation + * + * @param array $headers HTTP response headers + * @param array $parsed parsed response in array format. + * + * @internal + * + * @return ListPageBlobRangesResult + */ + public static function create(array $headers, array $parsed = null) + { + $result = new ListPageBlobRangesResult(); + $headers = array_change_key_case($headers); + + $date = $headers[Resources::LAST_MODIFIED]; + $date = Utilities::rfc1123ToDateTime($date); + $blobLength = intval($headers[Resources::X_MS_BLOB_CONTENT_LENGTH]); + $rawRanges = array(); + + if (!empty($parsed[Resources::XTAG_PAGE_RANGE])) { + $parsed = array_change_key_case($parsed); + $rawRanges = Utilities::getArray($parsed[strtolower(RESOURCES::XTAG_PAGE_RANGE)]); + } + + $pageRanges = array(); + foreach ($rawRanges as $value) { + $pageRanges[] = new Range( + intval($value[Resources::XTAG_RANGE_START]), + intval($value[Resources::XTAG_RANGE_END]) + ); + } + $result->setRanges($pageRanges); + $result->setContentLength($blobLength); + $result->setETag($headers[Resources::ETAG]); + $result->setLastModified($date); + + return $result; + } + + /** + * Gets blob lastModified. + * + * @return \DateTime + */ + public function getLastModified() + { + return $this->_lastModified; + } + + /** + * Sets blob lastModified. + * + * @param \DateTime $lastModified value. + * + * @return void + */ + protected function setLastModified(\DateTime $lastModified) + { + Validate::isDate($lastModified); + $this->_lastModified = $lastModified; + } + + /** + * Gets blob etag. + * + * @return string + */ + public function getETag() + { + return $this->_etag; + } + + /** + * Sets blob etag. + * + * @param string $etag value. + * + * @return void + */ + protected function setETag($etag) + { + Validate::canCastAsString($etag, 'etag'); + $this->_etag = $etag; + } + + /** + * Gets blob contentLength. + * + * @return integer + */ + public function getContentLength() + { + return $this->_contentLength; + } + + /** + * Sets blob contentLength. + * + * @param integer $contentLength value. + * + * @return void + */ + protected function setContentLength($contentLength) + { + Validate::isInteger($contentLength, 'contentLength'); + $this->_contentLength = $contentLength; + } + + /** + * Gets page ranges + * + * @return array + */ + public function getRanges() + { + return $this->_pageRanges; + } + + /** + * Sets page ranges + * + * @param array $pageRanges page ranges to set + * + * @return void + */ + protected function setRanges(array $pageRanges) + { + $this->_pageRanges = array(); + foreach ($pageRanges as $pageRange) { + $this->_pageRanges[] = clone $pageRange; + } + } +} diff --git a/3rdparty/microsoft/azure-storage-blob/src/Blob/Models/PageWriteOption.php b/3rdparty/microsoft/azure-storage-blob/src/Blob/Models/PageWriteOption.php new file mode 100644 index 00000000..40524430 --- /dev/null +++ b/3rdparty/microsoft/azure-storage-blob/src/Blob/Models/PageWriteOption.php @@ -0,0 +1,41 @@ + + * @copyright 2016 Microsoft Corporation + * @license https://github.com/azure/azure-storage-php/LICENSE + * @link https://github.com/azure/azure-storage-php + */ + +namespace MicrosoftAzure\Storage\Blob\Models; + +/** + * Holds available blob page write options + * + * @category Microsoft + * @package MicrosoftAzure\Storage\Blob\Models + * @author Azure Storage PHP SDK + * @copyright 2016 Microsoft Corporation + * @license https://github.com/azure/azure-storage-php/LICENSE + * @link https://github.com/azure/azure-storage-php + */ +class PageWriteOption +{ + const CLEAR_OPTION = 'clear'; + const UPDATE_OPTION = 'update'; +} diff --git a/3rdparty/microsoft/azure-storage-blob/src/Blob/Models/PublicAccessType.php b/3rdparty/microsoft/azure-storage-blob/src/Blob/Models/PublicAccessType.php new file mode 100644 index 00000000..40b59918 --- /dev/null +++ b/3rdparty/microsoft/azure-storage-blob/src/Blob/Models/PublicAccessType.php @@ -0,0 +1,67 @@ + + * @copyright 2016 Microsoft Corporation + * @license https://github.com/azure/azure-storage-php/LICENSE + * @link https://github.com/azure/azure-storage-php + */ + +namespace MicrosoftAzure\Storage\Blob\Models; + +use MicrosoftAzure\Storage\Blob\Internal\BlobResources as Resources; + +/** + * Holds public access types for a container. + * + * @category Microsoft + * @package MicrosoftAzure\Storage\Blob\Models + * @author Azure Storage PHP SDK + * @copyright 2016 Microsoft Corporation + * @license https://github.com/azure/azure-storage-php/LICENSE + * @link https://github.com/azure/azure-storage-php + */ +class PublicAccessType +{ + const NONE = null; + const BLOBS_ONLY = 'blob'; + const CONTAINER_AND_BLOBS = 'container'; + + /** + * Validates the public access. + * + * @param string $type The public access type. + * + * @internal + * + * @return boolean + */ + public static function isValid($type) + { + // When $type is null, switch statement will take it + // equal to self::NONE (EMPTY_STRING) + switch ($type) { + case self::NONE: + case self::BLOBS_ONLY: + case self::CONTAINER_AND_BLOBS: + return true; + default: + return false; + } + } +} diff --git a/3rdparty/microsoft/azure-storage-blob/src/Blob/Models/PutBlobResult.php b/3rdparty/microsoft/azure-storage-blob/src/Blob/Models/PutBlobResult.php new file mode 100644 index 00000000..d0ef2e9e --- /dev/null +++ b/3rdparty/microsoft/azure-storage-blob/src/Blob/Models/PutBlobResult.php @@ -0,0 +1,182 @@ + + * @copyright 2017 Microsoft Corporation + * @license https://github.com/azure/azure-storage-php/LICENSE + * @link https://github.com/azure/azure-storage-php + */ + +namespace MicrosoftAzure\Storage\Blob\Models; + +use MicrosoftAzure\Storage\Blob\Internal\BlobResources as Resources; +use MicrosoftAzure\Storage\Common\Internal\Utilities; + +/** + * The result of calling PutBlob API. + * + * @category Microsoft + * @package MicrosoftAzure\Storage\Blob\Models + * @author Azure Storage PHP SDK + * @copyright 2017 Microsoft Corporation + * @license https://github.com/azure/azure-storage-php/LICENSE + * @link https://github.com/azure/azure-storage-php + */ +class PutBlobResult +{ + private $contentMD5; + private $etag; + private $lastModified; + private $requestServerEncrypted; + + /** + * Creates PutBlobResult object from the response of the put blob request. + * + * @param array $headers The HTTP response headers in array representation. + * + * @internal + * + * @return PutBlobResult + */ + public static function create(array $headers) + { + $result = new PutBlobResult(); + + $result->setETag( + Utilities::tryGetValueInsensitive( + Resources::ETAG, + $headers + ) + ); + + if (Utilities::arrayKeyExistsInsensitive( + Resources::LAST_MODIFIED, + $headers + )) { + $lastModified = Utilities::tryGetValueInsensitive( + Resources::LAST_MODIFIED, + $headers + ); + $result->setLastModified(Utilities::rfc1123ToDateTime($lastModified)); + } + + $result->setContentMD5( + Utilities::tryGetValueInsensitive(Resources::CONTENT_MD5, $headers) + ); + + $result->setRequestServerEncrypted( + Utilities::toBoolean( + Utilities::tryGetValueInsensitive( + Resources::X_MS_REQUEST_SERVER_ENCRYPTED, + $headers + ), + true + ) + ); + + return $result; + } + + /** + * Gets ETag. + * + * @return string + */ + public function getETag() + { + return $this->etag; + } + + /** + * Sets ETag. + * + * @param string $etag value. + * + * @return void + */ + protected function setETag($etag) + { + $this->etag = $etag; + } + + /** + * Gets blob lastModified. + * + * @return \DateTime + */ + public function getLastModified() + { + return $this->lastModified; + } + + /** + * Sets blob lastModified. + * + * @param \DateTime $lastModified value. + * + * @return void + */ + protected function setLastModified(\DateTime $lastModified) + { + $this->lastModified = $lastModified; + } + + /** + * Gets block content MD5. + * + * @return string + */ + public function getContentMD5() + { + return $this->contentMD5; + } + + /** + * Sets the content MD5 value. + * + * @param string $contentMD5 conent MD5 as a string. + * + * @return void + */ + protected function setContentMD5($contentMD5) + { + $this->contentMD5 = $contentMD5; + } + + /** + * Gets the whether the contents of the request are successfully encrypted. + * + * @return boolean + */ + public function getRequestServerEncrypted() + { + return $this->requestServerEncrypted; + } + + /** + * Sets the request server encryption value. + * + * @param boolean $requestServerEncrypted + * + * @return void + */ + public function setRequestServerEncrypted($requestServerEncrypted) + { + $this->requestServerEncrypted = $requestServerEncrypted; + } +} diff --git a/3rdparty/microsoft/azure-storage-blob/src/Blob/Models/PutBlockResult.php b/3rdparty/microsoft/azure-storage-blob/src/Blob/Models/PutBlockResult.php new file mode 100644 index 00000000..80a2921b --- /dev/null +++ b/3rdparty/microsoft/azure-storage-blob/src/Blob/Models/PutBlockResult.php @@ -0,0 +1,118 @@ + + * @copyright 2016 Microsoft Corporation + * @license https://github.com/azure/azure-storage-php/LICENSE + * @link https://github.com/azure/azure-storage-php + */ + +namespace MicrosoftAzure\Storage\Blob\Models; + +use MicrosoftAzure\Storage\Blob\Internal\BlobResources as Resources; +use MicrosoftAzure\Storage\Common\Internal\Utilities; + +/** + * The result of calling PutBlock API. + * + * @category Microsoft + * @package MicrosoftAzure\Storage\Blob\Models + * @author Azure Storage PHP SDK + * @copyright 2016 Microsoft Corporation + * @license https://github.com/azure/azure-storage-php/LICENSE + * @link https://github.com/azure/azure-storage-php + */ +class PutBlockResult +{ + private $contentMD5; + private $requestServerEncrypted; + + /** + * Creates PutBlockResult object from the response of the put block request. + * + * @param array $headers The HTTP response headers in array representation. + * + * @internal + * + * @return PutBlockResult + */ + public static function create(array $headers) + { + $result = new PutBlockResult(); + + $result->setContentMD5( + Utilities::tryGetValueInsensitive(Resources::CONTENT_MD5, $headers) + ); + + $result->setRequestServerEncrypted( + Utilities::toBoolean( + Utilities::tryGetValueInsensitive( + Resources::X_MS_REQUEST_SERVER_ENCRYPTED, + $headers + ), + true + ) + ); + + return $result; + } + + /** + * Gets block content MD5. + * + * @return string + */ + public function getContentMD5() + { + return $this->contentMD5; + } + + /** + * Sets the content MD5 value. + * + * @param string $contentMD5 conent MD5 as a string. + * + * @return void + */ + protected function setContentMD5($contentMD5) + { + $this->contentMD5 = $contentMD5; + } + + /** + * Gets the whether the contents of the request are successfully encrypted. + * + * @return boolean + */ + public function getRequestServerEncrypted() + { + return $this->requestServerEncrypted; + } + + /** + * Sets the request server encryption value. + * + * @param boolean $requestServerEncrypted + * + * @return void + */ + public function setRequestServerEncrypted($requestServerEncrypted) + { + $this->requestServerEncrypted = $requestServerEncrypted; + } +} diff --git a/3rdparty/microsoft/azure-storage-blob/src/Blob/Models/SetBlobMetadataResult.php b/3rdparty/microsoft/azure-storage-blob/src/Blob/Models/SetBlobMetadataResult.php new file mode 100644 index 00000000..478899f9 --- /dev/null +++ b/3rdparty/microsoft/azure-storage-blob/src/Blob/Models/SetBlobMetadataResult.php @@ -0,0 +1,151 @@ + + * @copyright 2016 Microsoft Corporation + * @license https://github.com/azure/azure-storage-php/LICENSE + * @link https://github.com/azure/azure-storage-php + */ + +namespace MicrosoftAzure\Storage\Blob\Models; + +use MicrosoftAzure\Storage\Blob\Internal\BlobResources as Resources; +use MicrosoftAzure\Storage\Common\Internal\Validate; +use MicrosoftAzure\Storage\Common\Internal\Utilities; + +/** + * Holds results of calling getBlobMetadata wrapper + * + * @category Microsoft + * @package MicrosoftAzure\Storage\Blob\Models + * @author Azure Storage PHP SDK + * @copyright 2016 Microsoft Corporation + * @license https://github.com/azure/azure-storage-php/LICENSE + * @link https://github.com/azure/azure-storage-php + */ +class SetBlobMetadataResult +{ + private $etag; + private $lastModified; + private $requestServerEncrypted; + + /** + * Creates SetBlobMetadataResult from response headers. + * + * @param array $headers response headers + * + * @internal + * + * @return SetBlobMetadataResult + */ + public static function create(array $headers) + { + $result = new SetBlobMetadataResult(); + + $result->setETag(Utilities::tryGetValueInsensitive( + Resources::ETAG, + $headers + )); + + $date = Utilities::tryGetValueInsensitive( + Resources::LAST_MODIFIED, + $headers + ); + $result->setLastModified(Utilities::rfc1123ToDateTime($date)); + + $result->setRequestServerEncrypted( + Utilities::toBoolean( + Utilities::tryGetValueInsensitive( + Resources::X_MS_REQUEST_SERVER_ENCRYPTED, + $headers + ), + true + ) + ); + + return $result; + } + + /** + * Gets blob lastModified. + * + * @return \DateTime + */ + public function getLastModified() + { + return $this->lastModified; + } + + /** + * Sets blob lastModified. + * + * @param \DateTime $lastModified value. + * + * @return void + */ + protected function setLastModified(\DateTime $lastModified) + { + Validate::isDate($lastModified); + $this->lastModified = $lastModified; + } + + /** + * Gets blob etag. + * + * @return string + */ + public function getETag() + { + return $this->etag; + } + + /** + * Sets blob etag. + * + * @param string $etag value. + * + * @return void + */ + protected function setETag($etag) + { + Validate::canCastAsString($etag, 'etag'); + $this->etag = $etag; + } + + /** + * Gets the whether the contents of the request are successfully encrypted. + * + * @return boolean + */ + public function getRequestServerEncrypted() + { + return $this->requestServerEncrypted; + } + + /** + * Sets the request server encryption value. + * + * @param boolean $requestServerEncrypted + * + * @return void + */ + public function setRequestServerEncrypted($requestServerEncrypted) + { + $this->requestServerEncrypted = $requestServerEncrypted; + } +} diff --git a/3rdparty/microsoft/azure-storage-blob/src/Blob/Models/SetBlobPropertiesOptions.php b/3rdparty/microsoft/azure-storage-blob/src/Blob/Models/SetBlobPropertiesOptions.php new file mode 100644 index 00000000..0b45c005 --- /dev/null +++ b/3rdparty/microsoft/azure-storage-blob/src/Blob/Models/SetBlobPropertiesOptions.php @@ -0,0 +1,252 @@ + + * @copyright 2016 Microsoft Corporation + * @license https://github.com/azure/azure-storage-php/LICENSE + * @link https://github.com/azure/azure-storage-php + */ + +namespace MicrosoftAzure\Storage\Blob\Models; + +/** + * Optional parameters for setBlobProperties wrapper + * + * @category Microsoft + * @package MicrosoftAzure\Storage\Blob\Models + * @author Azure Storage PHP SDK + * @copyright 2016 Microsoft Corporation + * @license https://github.com/azure/azure-storage-php/LICENSE + * @link https://github.com/azure/azure-storage-php + */ +class SetBlobPropertiesOptions extends BlobServiceOptions +{ + private $_blobProperties; + private $_sequenceNumberAction; + + /** + * Creates a new SetBlobPropertiesOptions with a specified BlobProperties + * instance. + * + * @param BlobProperties $blobProperties The blob properties instance. + */ + public function __construct(BlobProperties $blobProperties = null) + { + parent::__construct(); + $this->_blobProperties = is_null($blobProperties) + ? new BlobProperties() : clone $blobProperties; + } + + /** + * Gets blob sequenceNumber. + * + * @return integer + */ + public function getSequenceNumber() + { + return $this->_blobProperties->getSequenceNumber(); + } + + /** + * Sets blob sequenceNumber. + * + * @param integer $sequenceNumber value. + * + * @return void + */ + public function setSequenceNumber($sequenceNumber) + { + $this->_blobProperties->setSequenceNumber($sequenceNumber); + } + + /** + * Gets lease Id for the blob + * + * @return string + */ + public function getSequenceNumberAction() + { + return $this->_sequenceNumberAction; + } + + /** + * Sets lease Id for the blob + * + * @param string $sequenceNumberAction action. + * + * @return void + */ + public function setSequenceNumberAction($sequenceNumberAction) + { + $this->_sequenceNumberAction = $sequenceNumberAction; + } + + /** + * Gets blob contentLength. + * + * @return integer + */ + public function getContentLength() + { + return $this->_blobProperties->getContentLength(); + } + + /** + * Sets blob contentLength. + * + * @param integer $contentLength value. + * + * @return void + */ + public function setContentLength($contentLength) + { + $this->_blobProperties->setContentLength($contentLength); + } + + /** + * Gets ContentType. + * + * @return string + */ + public function getContentType() + { + return $this->_blobProperties->getContentType(); + } + + /** + * Sets ContentType. + * + * @param string $contentType value. + * + * @return void + */ + public function setContentType($contentType) + { + $this->_blobProperties->setContentType($contentType); + } + + /** + * Gets ContentEncoding. + * + * @return string + */ + public function getContentEncoding() + { + return $this->_blobProperties->getContentEncoding(); + } + + /** + * Sets ContentEncoding. + * + * @param string $contentEncoding value. + * + * @return void + */ + public function setContentEncoding($contentEncoding) + { + $this->_blobProperties->setContentEncoding($contentEncoding); + } + + /** + * Gets ContentLanguage. + * + * @return string + */ + public function getContentLanguage() + { + return $this->_blobProperties->getContentLanguage(); + } + + /** + * Sets ContentLanguage. + * + * @param string $contentLanguage value. + * + * @return void + */ + public function setContentLanguage($contentLanguage) + { + $this->_blobProperties->setContentLanguage($contentLanguage); + } + + /** + * Gets ContentMD5. + * + * @return void + */ + public function getContentMD5() + { + return $this->_blobProperties->getContentMD5(); + } + + /** + * Sets blob ContentMD5. + * + * @param string $contentMD5 value. + * + * @return void + */ + public function setContentMD5($contentMD5) + { + $this->_blobProperties->setContentMD5($contentMD5); + } + + /** + * Gets cache control. + * + * @return string + */ + public function getCacheControl() + { + return $this->_blobProperties->getCacheControl(); + } + + /** + * Sets cacheControl. + * + * @param string $cacheControl value to use. + * + * @return void + */ + public function setCacheControl($cacheControl) + { + $this->_blobProperties->setCacheControl($cacheControl); + } + + /** + * Gets content disposition. + * + * @return string + */ + public function getContentDisposition() + { + return $this->_blobProperties->getContentDisposition(); + } + + /** + * Sets contentDisposition. + * + * @param string $contentDisposition value to use. + * + * @return void + */ + public function setContentDisposition($contentDisposition) + { + $this->_blobProperties->setContentDisposition($contentDisposition); + } +} diff --git a/3rdparty/microsoft/azure-storage-blob/src/Blob/Models/SetBlobPropertiesResult.php b/3rdparty/microsoft/azure-storage-blob/src/Blob/Models/SetBlobPropertiesResult.php new file mode 100644 index 00000000..ba49de95 --- /dev/null +++ b/3rdparty/microsoft/azure-storage-blob/src/Blob/Models/SetBlobPropertiesResult.php @@ -0,0 +1,144 @@ + + * @copyright 2016 Microsoft Corporation + * @license https://github.com/azure/azure-storage-php/LICENSE + * @link https://github.com/azure/azure-storage-php + */ + +namespace MicrosoftAzure\Storage\Blob\Models; + +use MicrosoftAzure\Storage\Blob\Internal\BlobResources as Resources; +use MicrosoftAzure\Storage\Common\Internal\Validate; +use MicrosoftAzure\Storage\Common\Internal\Utilities; + +/** + * Holds result of calling setBlobProperties wrapper + * + * @category Microsoft + * @package MicrosoftAzure\Storage\Blob\Models + * @author Azure Storage PHP SDK + * @copyright 2016 Microsoft Corporation + * @license https://github.com/azure/azure-storage-php/LICENSE + * @link https://github.com/azure/azure-storage-php + */ +class SetBlobPropertiesResult +{ + private $_lastModified; + private $_etag; + private $_sequenceNumber; + + /** + * Creates SetBlobPropertiesResult from response headers. + * + * @param array $headers response headers + * + * @internal + * + * @return SetBlobPropertiesResult + */ + public static function create(array $headers) + { + $result = new SetBlobPropertiesResult(); + $date = Utilities::tryGetValueInsensitive( + Resources::LAST_MODIFIED, + $headers + ); + $result->setLastModified(Utilities::rfc1123ToDateTime($date)); + $result->setETag(Utilities::tryGetValueInsensitive( + Resources::ETAG, + $headers + )); + $result->setSequenceNumber(Utilities::tryGetValueInsensitive( + Resources::X_MS_BLOB_SEQUENCE_NUMBER, + $headers + )); + + return $result; + } + + /** + * Gets blob lastModified. + * + * @return \DateTime + */ + public function getLastModified() + { + return $this->_lastModified; + } + + /** + * Sets blob lastModified. + * + * @param \DateTime $lastModified value. + * + * @return void + */ + protected function setLastModified(\DateTime $lastModified) + { + Validate::isDate($lastModified); + $this->_lastModified = $lastModified; + } + + /** + * Gets blob etag. + * + * @return string + */ + public function getETag() + { + return $this->_etag; + } + + /** + * Sets blob etag. + * + * @param string $etag value. + * + * @return void + */ + protected function setETag($etag) + { + Validate::canCastAsString($etag, 'etag'); + $this->_etag = $etag; + } + + /** + * Gets blob sequenceNumber. + * + * @return int + */ + public function getSequenceNumber() + { + return $this->_sequenceNumber; + } + + /** + * Sets blob sequenceNumber. + * + * @param int $sequenceNumber value. + * + * @return void + */ + protected function setSequenceNumber($sequenceNumber) + { + Validate::isInteger($sequenceNumber, 'sequenceNumber'); + $this->_sequenceNumber = $sequenceNumber; + } +} diff --git a/3rdparty/microsoft/azure-storage-blob/src/Blob/Models/SetBlobTierOptions.php b/3rdparty/microsoft/azure-storage-blob/src/Blob/Models/SetBlobTierOptions.php new file mode 100644 index 00000000..43c8169e --- /dev/null +++ b/3rdparty/microsoft/azure-storage-blob/src/Blob/Models/SetBlobTierOptions.php @@ -0,0 +1,42 @@ + + * @copyright 2018 Microsoft Corporation + * @license https://github.com/azure/azure-storage-php/LICENSE + * @link https://github.com/azure/azure-storage-php + */ + +namespace MicrosoftAzure\Storage\Blob\Models; + +use MicrosoftAzure\Storage\Common\Models\ServiceOptions; + +/** + * Optional parameters for SetBlobTier. + * + * @category Microsoft + * @package MicrosoftAzure\Storage\Blob\Models + * @author Azure Storage PHP SDK + * @copyright 2018 Microsoft Corporation + * @license https://github.com/azure/azure-storage-php/LICENSE + * @link https://github.com/azure/azure-storage-php + */ +class SetBlobTierOptions extends ServiceOptions +{ + use AccessTierTrait; +} diff --git a/3rdparty/microsoft/azure-storage-blob/src/Blob/Models/UndeleteBlobOptions.php b/3rdparty/microsoft/azure-storage-blob/src/Blob/Models/UndeleteBlobOptions.php new file mode 100644 index 00000000..b6c268a3 --- /dev/null +++ b/3rdparty/microsoft/azure-storage-blob/src/Blob/Models/UndeleteBlobOptions.php @@ -0,0 +1,39 @@ + + * @copyright 2016 Microsoft Corporation + * @license https://github.com/azure/azure-storage-php/LICENSE + * @link https://github.com/azure/azure-storage-php + */ + +namespace MicrosoftAzure\Storage\Blob\Models; + +/** + * Optional parameters for deleteBlob wrapper + * + * @category Microsoft + * @package MicrosoftAzure\Storage\Blob\Models + * @author Azure Storage PHP SDK + * @copyright 2016 Microsoft Corporation + * @license https://github.com/azure/azure-storage-php/LICENSE + * @link https://github.com/azure/azure-storage-php + */ +class UndeleteBlobOptions extends BlobServiceOptions +{ +} diff --git a/3rdparty/microsoft/azure-storage-common/BreakingChanges.md b/3rdparty/microsoft/azure-storage-common/BreakingChanges.md new file mode 100644 index 00000000..db55af77 --- /dev/null +++ b/3rdparty/microsoft/azure-storage-common/BreakingChanges.md @@ -0,0 +1,10 @@ +Tracking Breaking changes in 1.0.0 + +* Removed `ServiceBuilder.php`, moved static builder methods into `BlobRestProxy`, `TableRestProxy`, `QueueRestProxy` and `FileRestProxy`. +* Moved method `SharedAccessSignatureHelper::generateBlobServiceSharedAccessSignatureToken()` into `BlobSharedAccessSignatureHelper`. +* Moved method `SharedAccessSignatureHelper::generateTableServiceSharedAccessSignatureToken()` into `TableSharedAccessSignatureHelper`. +* Moved method `SharedAccessSignatureHelper::generateQueueServiceSharedAccessSignatureToken()` into `QueueSharedAccessSignatureHelper`. +* Moved method `SharedAccessSignatureHelper::generateFileServiceSharedAccessSignatureToken()` into `FileSharedAccessSignatureHelper`. +* `CommonMiddleWare` constructor requires storage service version as parameter now. +* `AccessPolicy` class is now an abstract class, added children classes `BlobAccessPolicy`, `ContainerAccessPolicy`, `TableAccessPolicy`, `QueueAccessPolicy`, `FileAccessPolicy` and `ShareAccessPolicy`. +* Deprecated PHP 5.5 support. \ No newline at end of file diff --git a/3rdparty/microsoft/azure-storage-common/CONTRIBUTING.md b/3rdparty/microsoft/azure-storage-common/CONTRIBUTING.md new file mode 100644 index 00000000..5a546777 --- /dev/null +++ b/3rdparty/microsoft/azure-storage-common/CONTRIBUTING.md @@ -0,0 +1 @@ +This [repository](https://github.com/azure/azure-storage-common-php) is currently used for releasing only, please go to [azure-storage-php](https://github.com/azure/azure-storage-php) for submitting issues or contribution. \ No newline at end of file diff --git a/3rdparty/microsoft/azure-storage-common/ChangeLog.md b/3rdparty/microsoft/azure-storage-common/ChangeLog.md new file mode 100644 index 00000000..00885e97 --- /dev/null +++ b/3rdparty/microsoft/azure-storage-common/ChangeLog.md @@ -0,0 +1,57 @@ +2021.09 - version 1.5.2 +* Added support for guzzle 7.3. +* Resolve some warnings when calling `Psr7\stream_for`, uses `Utils::streamFor` instead. +* Added colon to non-UTC timestamps. +* Fixed type hint for `ServiceException::getResponse()`. +* Fixed random number range that might cause an overflow in the guid generation. +* Added logic to convert to exception when promise is rejected with string. +* Compares `strlen` result with an integer instead of string. + +2020.12 - version 1.5.1 +* Guzzle version is now updated to support both 6.x and 7.x. + +2020.08 - version 1.5.0 +* Resolved TLS 1.2 issue and some test issues. +* Check $uri null type before array/string access. +* Accept DateTimeImmutable as EdmType input. +* Added client-request-id to requests. +* Updated getContinuationToken return type. +* Call static methods using `static::` not `self::`. +* Added $isSecondary parameter for appendBlobRetryDecider. +* Retry on no response from server after a successful connection + +2020.01 - version 1.4.1 +* Changed to perform override existence instead of value check for ‘$options[‘verify’]’ in ‘ServiceRestProxy’. + +2019.04 - version 1.4.0 +* Added support for OAuth authentication. +* Resolved some issues on Linux platform. + +2019.03 - version 1.3.0 +* Documentation refinement. + +2018.08 - version 1.2.0 + +* Fixed a bug `generateCanonicalResource` returns an empty string if `$resource` starts with "/". +* Supported optional middleware retry on connection failures. +* Fixed a typo of `DEAFULT_RETRY_INTERVAL`. + +2018.04 - version 1.1.0 + +* MD files are modified for better readability and formatting. +* CACERT can now be set when creating RestProxies using `$options` parameter. +* Removed unnecessary trailing spaces. +* Assertions are re-factored in test cases. +* Now the test framework uses `PHPUnit\Framework\TestCase` instead of `PHPUnit_Framework_TestCase`. + +2018.01 - version 1.0.0 + +* Removed `ServiceBuilder.php`, moved static builder methods into `BlobRestProxy`, `TableRestProxy`, `QueueRestProxy` and `FileRestProxy`. +* Moved method `SharedAccessSignatureHelper::generateBlobServiceSharedAccessSignatureToken()` into `BlobSharedAccessSignatureHelper`. +* Moved method `SharedAccessSignatureHelper::generateTableServiceSharedAccessSignatureToken()` into `TableSharedAccessSignatureHelper`. +* Moved method `SharedAccessSignatureHelper::generateQueueServiceSharedAccessSignatureToken()` into `QueueSharedAccessSignatureHelper`. +* Moved method `SharedAccessSignatureHelper::generateFileServiceSharedAccessSignatureToken()` into `FileSharedAccessSignatureHelper`. +* `CommonMiddleWare` constructor requires storage service version as parameter now. +* `AccessPolicy` class is now an abstract class, added children classes `BlobAccessPolicy`, `ContainerAccessPolicy`, `TableAccessPolicy`, `QueueAccessPolicy`, `FileAccessPolicy` and `ShareAccessPolicy`. +* Fixed a bug that `Utilities::allZero()` will return true for non-zero data chunks. +* Deprecated PHP 5.5 support. \ No newline at end of file diff --git a/3rdparty/microsoft/azure-storage-common/LICENSE b/3rdparty/microsoft/azure-storage-common/LICENSE new file mode 100644 index 00000000..79288605 --- /dev/null +++ b/3rdparty/microsoft/azure-storage-common/LICENSE @@ -0,0 +1,21 @@ +The MIT License (MIT) + +Copyright (c) 2016 Microsoft Corporation + +Permission is hereby granted, free of charge, to any person obtaining a copy + of this software and associated documentation files (the "Software"), to deal + in the Software without restriction, including without limitation the rights + to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + copies of the Software, and to permit persons to whom the Software is + furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in + all copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN + THE SOFTWARE. \ No newline at end of file diff --git a/3rdparty/microsoft/azure-storage-common/src/Common/CloudConfigurationManager.php b/3rdparty/microsoft/azure-storage-common/src/Common/CloudConfigurationManager.php new file mode 100644 index 00000000..2e67af6d --- /dev/null +++ b/3rdparty/microsoft/azure-storage-common/src/Common/CloudConfigurationManager.php @@ -0,0 +1,155 @@ + + * @copyright 2016 Microsoft Corporation + * @license https://github.com/azure/azure-storage-php/LICENSE + * @link https://github.com/azure/azure-storage-php + */ + +namespace MicrosoftAzure\Storage\Common; + +use MicrosoftAzure\Storage\Common\Internal\Utilities; +use MicrosoftAzure\Storage\Common\Internal\Validate; +use MicrosoftAzure\Storage\Common\Internal\ConnectionStringSource; + +/** + * Configuration manager for accessing Windows Azure settings. + * + * @category Microsoft + * @package MicrosoftAzure\Storage\Common + * @author Azure Storage PHP SDK + * @copyright 2016 Microsoft Corporation + * @license https://github.com/azure/azure-storage-php/LICENSE + * @link https://github.com/azure/azure-storage-php + */ +class CloudConfigurationManager +{ + private static $_isInitialized = false; + private static $_sources; + + /** + * Restrict users from creating instances from this class + */ + private function __construct() + { + } + + /** + * Initializes the connection string source providers. + * + * @return void + */ + private static function _init() + { + if (!self::$_isInitialized) { + self::$_sources = array(); + + // Get list of default connection string sources. + $default = ConnectionStringSource::getDefaultSources(); + foreach ($default as $name => $provider) { + self::$_sources[$name] = $provider; + } + + self::$_isInitialized = true; + } + } + + /** + * Gets a connection string from all available sources. + * + * @param string $key The connection string key name. + * + * @return string If the key does not exist return null. + */ + public static function getConnectionString($key) + { + Validate::canCastAsString($key, 'key'); + + self::_init(); + $value = null; + + foreach (self::$_sources as $source) { + $value = call_user_func_array($source, array($key)); + + if (!empty($value)) { + break; + } + } + + return $value; + } + + /** + * Registers a new connection string source provider. If the source to get + * registered is a default source, only the name of the source is required. + * + * @param string $name The source name. + * @param callable $provider The source callback. + * @param boolean $prepend When true, the $provider is processed first when + * calling getConnectionString. When false (the default) the $provider is + * processed after the existing callbacks. + * + * @return void + */ + public static function registerSource($name, $provider = null, $prepend = false) + { + Validate::canCastAsString($name, 'name'); + Validate::notNullOrEmpty($name, 'name'); + + self::_init(); + $default = ConnectionStringSource::getDefaultSources(); + + // Try to get callback if the user is trying to register a default source. + $provider = Utilities::tryGetValue($default, $name, $provider); + + Validate::notNullOrEmpty($provider, 'callback'); + + if ($prepend) { + self::$_sources = array_merge( + array($name => $provider), + self::$_sources + ); + } else { + self::$_sources[$name] = $provider; + } + } + + /** + * Unregisters a connection string source. + * + * @param string $name The source name. + * + * @return callable + */ + public static function unregisterSource($name) + { + Validate::canCastAsString($name, 'name'); + Validate::notNullOrEmpty($name, 'name'); + + self::_init(); + + $sourceCallback = Utilities::tryGetValue(self::$_sources, $name); + + if (!is_null($sourceCallback)) { + unset(self::$_sources[$name]); + } + + return $sourceCallback; + } +} diff --git a/3rdparty/microsoft/azure-storage-common/src/Common/Exceptions/InvalidArgumentTypeException.php b/3rdparty/microsoft/azure-storage-common/src/Common/Exceptions/InvalidArgumentTypeException.php new file mode 100644 index 00000000..b0f6aa3e --- /dev/null +++ b/3rdparty/microsoft/azure-storage-common/src/Common/Exceptions/InvalidArgumentTypeException.php @@ -0,0 +1,55 @@ + + * @copyright 2016 Microsoft Corporation + * @license https://github.com/azure/azure-storage-php/LICENSE + * @link https://github.com/azure/azure-storage-php + */ + +namespace MicrosoftAzure\Storage\Common\Exceptions; + +use MicrosoftAzure\Storage\Common\Internal\Resources; + +/** + * Exception thrown if an argument type does not match with the expected type. + * + * @category Microsoft + * @package MicrosoftAzure\Storage\Common\Exceptions + * @author Azure Storage PHP SDK + * @copyright 2016 Microsoft Corporation + * @license https://github.com/azure/azure-storage-php/LICENSE + * @link https://github.com/azure/azure-storage-php + */ +class InvalidArgumentTypeException extends \InvalidArgumentException +{ + /** + * Constructor. + * + * @param string $validType The valid type that should be provided by the user. + * @param string $name The parameter name. + * + * @return InvalidArgumentTypeException + */ + public function __construct($validType, $name = null) + { + parent::__construct( + sprintf(Resources::INVALID_PARAM_MSG, $name, $validType) + ); + } +} diff --git a/3rdparty/microsoft/azure-storage-common/src/Common/Exceptions/ServiceException.php b/3rdparty/microsoft/azure-storage-common/src/Common/Exceptions/ServiceException.php new file mode 100644 index 00000000..f262a3ed --- /dev/null +++ b/3rdparty/microsoft/azure-storage-common/src/Common/Exceptions/ServiceException.php @@ -0,0 +1,177 @@ + + * @copyright 2016 Microsoft Corporation + * @license https://github.com/azure/azure-storage-php/LICENSE + * @link https://github.com/azure/azure-storage-php + */ + +namespace MicrosoftAzure\Storage\Common\Exceptions; + +use MicrosoftAzure\Storage\Common\Internal\Serialization\XmlSerializer; +use MicrosoftAzure\Storage\Common\Internal\Resources; +use Psr\Http\Message\ResponseInterface; + +/** + * Fires when the response code is incorrect. + * + * @category Microsoft + * @package MicrosoftAzure\Storage\Common\Exceptions + * @author Azure Storage PHP SDK + * @copyright 2016 Microsoft Corporation + * @license https://github.com/azure/azure-storage-php/LICENSE + * @link https://github.com/azure/azure-storage-php + */ +class ServiceException extends \LogicException +{ + private $response; + private $errorText; + private $errorMessage; + + /** + * Constructor + * + * @param ResponseInterface $response The response received that causes the + * exception. + * + * @internal + * + * @return ServiceException + */ + public function __construct(ResponseInterface $response) + { + parent::__construct( + sprintf( + Resources::AZURE_ERROR_MSG, + $response->getStatusCode(), + $response->getReasonPhrase(), + $response->getBody() + ) + ); + $this->code = $response->getStatusCode(); + $this->response = $response; + $this->errorText = $response->getReasonPhrase(); + $this->errorMessage = self::parseErrorMessage($response); + } + + /** + * Error message to be parsed. + * + * @param ResponseInterface $response The response with a response body. + * + * @internal + * + * @return string + */ + protected static function parseErrorMessage(ResponseInterface $response) + { + //try to parse using xml serializer, if failed, return the whole body + //as the error message. + $serializer = new XmlSerializer(); + $errorMessage = ''; + try { + $internalErrors = libxml_use_internal_errors(true); + $parsedArray = $serializer->unserialize($response->getBody()); + $messages = array(); + foreach (libxml_get_errors() as $error) { + $messages[] = $error->message; + } + if (!empty($messages)) { + throw new \Exception( + sprintf(Resources::ERROR_CANNOT_PARSE_XML, implode('; ', $messages)) + ); + } + libxml_use_internal_errors($internalErrors); + if (array_key_exists(Resources::XTAG_MESSAGE, $parsedArray)) { + $errorMessage = $parsedArray[Resources::XTAG_MESSAGE]; + } else { + $errorMessage = $response->getBody(); + } + } catch (\Exception $e) { + $errorMessage = $response->getBody(); + } + return $errorMessage; + } + + /** + * Gets error text. + * + * @return string + */ + public function getErrorText() + { + return $this->errorText; + } + + /** + * Gets detailed error message. + * + * @return string + */ + public function getErrorMessage() + { + return $this->errorMessage; + } + + /** + * Gets the request ID of the failure. + * + * @return string + */ + public function getRequestID() + { + $requestID = ''; + if (array_key_exists( + Resources::X_MS_REQUEST_ID, + $this->getResponse()->getHeaders() + )) { + $requestID = $this->getResponse() + ->getHeaders()[Resources::X_MS_REQUEST_ID][0]; + } + return $requestID; + } + + /** + * Gets the Date of the failure. + * + * @return string + */ + public function getDate() + { + $date = ''; + if (array_key_exists( + Resources::DATE, + $this->getResponse()->getHeaders() + )) { + $date = $this->getResponse() + ->getHeaders()[Resources::DATE][0]; + } + return $date; + } + + /** + * Gets the response of the failue. + * + * @return ResponseInterface + */ + public function getResponse() + { + return $this->response; + } +} diff --git a/3rdparty/microsoft/azure-storage-common/src/Common/Internal/ACLBase.php b/3rdparty/microsoft/azure-storage-common/src/Common/Internal/ACLBase.php new file mode 100644 index 00000000..fcad54cd --- /dev/null +++ b/3rdparty/microsoft/azure-storage-common/src/Common/Internal/ACLBase.php @@ -0,0 +1,260 @@ + + * @copyright 2017 Microsoft Corporation + * @license https://github.com/azure/azure-storage-php/LICENSE + * @link https://github.com/azure/azure-storage-php + */ + +namespace MicrosoftAzure\Storage\Common\Internal; + +use MicrosoftAzure\Storage\Common\Models\AccessPolicy; +use MicrosoftAzure\Storage\Common\Models\SignedIdentifier; +use MicrosoftAzure\Storage\Common\Internal\Serialization\XmlSerializer; + +/** + * Provide base class for service ACLs. + * + * @category Microsoft + * @package MicrosoftAzure\Storage\Common\Models + * @author Azure Storage PHP SDK + * @copyright 2017 Microsoft Corporation + * @license https://github.com/azure/azure-storage-php/LICENSE + * @link https://github.com/azure/azure-storage-php + */ +abstract class ACLBase +{ + private $signedIdentifiers = array(); + private $resourceType = ''; + + /** + * Create an AccessPolicy object by resource type. + * + * @return AccessPolicy + */ + abstract protected static function createAccessPolicy(); + + /** + * Validate if the resource type for the class. + * + * @param string $resourceType the resource type to be validated. + * + * @throws \InvalidArgumentException + * + * @internal + * + * @return void + */ + abstract protected static function validateResourceType($resourceType); + + /** + * Converts signed identifiers to array representation for XML serialization + * + * @internal + * + * @return array + */ + public function toArray() + { + $array = array(); + + foreach ($this->getSignedIdentifiers() as $value) { + $array[] = $value->toArray(); + } + + return $array; + } + + /** + * Converts this signed identifiers to XML representation. + * + * @param XmlSerializer $xmlSerializer The XML serializer. + * + * @internal + * + * @return string + */ + public function toXml(XmlSerializer $serializer) + { + $properties = array( + XmlSerializer::DEFAULT_TAG => Resources::XTAG_SIGNED_IDENTIFIER, + XmlSerializer::ROOT_NAME => Resources::XTAG_SIGNED_IDENTIFIERS + ); + + return $serializer->serialize($this->toArray(), $properties); + } + + /** + * Construct the signed identifiers from a given parsed XML in array + * representation. + * + * @param array|null $parsed The parsed XML into array representation. + * + * @internal + * + * @return void + */ + public function fromXmlArray(array $parsed = null) + { + $this->setSignedIdentifiers(array()); + + // Initialize signed identifiers. + if (!empty($parsed) && + is_array($parsed[Resources::XTAG_SIGNED_IDENTIFIER]) + ) { + $entries = $parsed[Resources::XTAG_SIGNED_IDENTIFIER]; + $temp = Utilities::getArray($entries); + + foreach ($temp as $value) { + $accessPolicy = $value[Resources::XTAG_ACCESS_POLICY]; + $startString = urldecode( + $accessPolicy[Resources::XTAG_SIGNED_START] + ); + $expiryString = urldecode( + $accessPolicy[Resources::XTAG_SIGNED_EXPIRY] + ); + $start = Utilities::convertToDateTime($startString); + $expiry = Utilities::convertToDateTime($expiryString); + $permission = $accessPolicy[Resources::XTAG_SIGNED_PERMISSION]; + $id = $value[Resources::XTAG_SIGNED_ID]; + $this->addSignedIdentifier($id, $start, $expiry, $permission); + } + } + } + + /** + * Gets the type of resource this ACL relate to. + * + * @internal + * + * @return string + */ + protected function getResourceType() + { + return $this->resourceType; + } + + /** + * Set the type of resource this ACL relate to. + * + * @internal + * + * @return void + */ + protected function setResourceType($resourceType) + { + static::validateResourceType($resourceType); + $this->resourceType = $resourceType; + } + + /** + * Add a signed identifier to the ACL. + * + * @param string $id A unique id for this signed identifier. + * @param \DateTime $start The time at which the Shared Access + * Signature becomes valid. If omitted, start + * time for this call is assumed to be the + * time when the service receives the + * request. + * @param \DateTime $expiry The time at which the Shared Access + * Signature becomes invalid. + * @param string $permissions The permissions associated with the Shared + * Access Signature. The user is restricted to + * operations allowed by the permissions. + * + * @return void + * + * @see https://docs.microsoft.com/en-us/rest/api/storageservices/fileservices/establishing-a-stored-access-policy + */ + public function addSignedIdentifier( + $id, + \DateTime $start, + \DateTime $expiry, + $permissions + ) { + Validate::canCastAsString($id, 'id'); + if ($start != null) { + Validate::isDate($start); + } + Validate::isDate($expiry); + Validate::canCastAsString($permissions, 'permissions'); + + $accessPolicy = static::createAccessPolicy(); + $accessPolicy->setStart($start); + $accessPolicy->setExpiry($expiry); + $accessPolicy->setPermission($permissions); + + $signedIdentifier = new SignedIdentifier(); + $signedIdentifier->setId($id); + $signedIdentifier->setAccessPolicy($accessPolicy); + + // Remove the signed identifier with the same ID. + $this->removeSignedIdentifier($id); + + // There can be no more than 5 signed identifiers at the same time. + Validate::isTrue( + count($this->getSignedIdentifiers()) < 5, + Resources::ERROR_TOO_MANY_SIGNED_IDENTIFIERS + ); + + $this->signedIdentifiers[] = $signedIdentifier; + } + + /** + * Remove the signed identifier with given ID. + * + * @param string $id The ID of the signed identifier to be removed. + * + * @return boolean + */ + public function removeSignedIdentifier($id) + { + Validate::canCastAsString($id, 'id'); + //var_dump($this->signedIdentifiers); + for ($i = 0; $i < count($this->signedIdentifiers); ++$i) { + if ($this->signedIdentifiers[$i]->getId() == $id) { + array_splice($this->signedIdentifiers, $i, 1); + return true; + } + } + + return false; + } + + /** + * Gets signed identifiers. + * + * @return array + */ + public function getSignedIdentifiers() + { + return $this->signedIdentifiers; + } + + public function setSignedIdentifiers(array $signedIdentifiers) + { + // There can be no more than 5 signed identifiers at the same time. + Validate::isTrue( + count($signedIdentifiers) <= 5, + Resources::ERROR_TOO_MANY_SIGNED_IDENTIFIERS + ); + $this->signedIdentifiers = $signedIdentifiers; + } +} diff --git a/3rdparty/microsoft/azure-storage-common/src/Common/Internal/Authentication/IAuthScheme.php b/3rdparty/microsoft/azure-storage-common/src/Common/Internal/Authentication/IAuthScheme.php new file mode 100644 index 00000000..be50dc8d --- /dev/null +++ b/3rdparty/microsoft/azure-storage-common/src/Common/Internal/Authentication/IAuthScheme.php @@ -0,0 +1,52 @@ + + * @copyright Microsoft Corporation + * @license https://github.com/azure/azure-storage-php/LICENSE + * @link https://github.com/azure/azure-storage-php + */ + +namespace MicrosoftAzure\Storage\Common\Internal\Authentication; + +use GuzzleHttp\Psr7\Request; + +/** + * Interface for azure authentication schemes. + * + * @ignore + * @category Microsoft + * @package MicrosoftAzure\Storage\Common\Internal\Authentication + * @author Azure Storage PHP SDK + * @copyright Microsoft Corporation + * @license https://github.com/azure/azure-storage-php/LICENSE + * @link https://github.com/azure/azure-storage-php + */ +interface IAuthScheme +{ + /** + * Signs a request. + * + * @param \GuzzleHttp\Psr7\Request $request HTTP request object. + * + * @abstract + * + * @return \GuzzleHttp\Psr7\Request + */ + public function signRequest(Request $request); +} diff --git a/3rdparty/microsoft/azure-storage-common/src/Common/Internal/Authentication/SharedAccessSignatureAuthScheme.php b/3rdparty/microsoft/azure-storage-common/src/Common/Internal/Authentication/SharedAccessSignatureAuthScheme.php new file mode 100644 index 00000000..a0216394 --- /dev/null +++ b/3rdparty/microsoft/azure-storage-common/src/Common/Internal/Authentication/SharedAccessSignatureAuthScheme.php @@ -0,0 +1,96 @@ + + * @copyright 2016 Microsoft Corporation + * @license https://github.com/azure/azure-storage-php/LICENSE + * @link https://github.com/azure/azure-storage-php + */ + +namespace MicrosoftAzure\Storage\Common\Internal\Authentication; + +use GuzzleHttp\Psr7\Request; +use MicrosoftAzure\Storage\Common\Internal\Resources; + +/** + * Base class for azure authentication schemes. + * + * @ignore + * @category Microsoft + * @package MicrosoftAzure\Storage\Common\Internal\Authentication + * @author Azure Storage PHP SDK + * @copyright 2016 Microsoft Corporation + * @license https://github.com/azure/azure-storage-php/LICENSE + * @link https://github.com/azure/azure-storage-php + */ +class SharedAccessSignatureAuthScheme implements IAuthScheme +{ + /** + * The sas token + */ + protected $sasToken; + + /** + * Constructor. + * + * @param string $sasToken shared access signature token. + * + */ + public function __construct($sasToken) + { + // Remove '?' in front of the SAS token if existing + $this->sasToken = str_replace('?', '', $sasToken, $i); + + if ($i > 1) { + throw new \InvalidArgumentException( + sprintf( + Resources::INVALID_SAS_TOKEN, + $sasToken + ) + ); + } + } + + /** + * Adds authentication header to the request headers. + * + * @param \GuzzleHttp\Psr7\Request $request HTTP request object. + * + * @abstract + * + * @return \GuzzleHttp\Psr7\Request + */ + public function signRequest(Request $request) + { + // initial URI + $uri = $request->getUri(); + + // new query values from SAS token + $queryValues = explode('&', $this->sasToken); + + // append SAS token query values to existing URI + foreach ($queryValues as $queryField) { + list($key, $value) = explode('=', $queryField); + + $uri = \GuzzleHttp\Psr7\Uri::withQueryValue($uri, $key, $value); + } + + // replace URI + return $request->withUri($uri, true); + } +} diff --git a/3rdparty/microsoft/azure-storage-common/src/Common/Internal/Authentication/SharedKeyAuthScheme.php b/3rdparty/microsoft/azure-storage-common/src/Common/Internal/Authentication/SharedKeyAuthScheme.php new file mode 100644 index 00000000..7a7be4ab --- /dev/null +++ b/3rdparty/microsoft/azure-storage-common/src/Common/Internal/Authentication/SharedKeyAuthScheme.php @@ -0,0 +1,317 @@ + + * @copyright 2016 Microsoft Corporation + * @license https://github.com/azure/azure-storage-php/LICENSE + * @link https://github.com/azure/azure-storage-php + */ + +namespace MicrosoftAzure\Storage\Common\Internal\Authentication; + +use GuzzleHttp\Psr7\Query; +use GuzzleHttp\Psr7\Request; +use MicrosoftAzure\Storage\Common\Internal\Http\HttpFormatter; +use MicrosoftAzure\Storage\Common\Internal\Resources; +use MicrosoftAzure\Storage\Common\Internal\Utilities; + +/** + * Provides shared key authentication scheme for blob and queue. For more info + * check: http://msdn.microsoft.com/en-us/library/windowsazure/dd179428.aspx + * + * @ignore + * @category Microsoft + * @package MicrosoftAzure\Storage\Common\Internal\Authentication + * @author Azure Storage PHP SDK + * @copyright 2016 Microsoft Corporation + * @license https://github.com/azure/azure-storage-php/LICENSE + * @link https://github.com/azure/azure-storage-php + */ +class SharedKeyAuthScheme implements IAuthScheme +{ + /** + * The account name + */ + protected $accountName; + + /** + * The account key + */ + protected $accountKey; + + /** + * The included headers + */ + protected $includedHeaders; + + /** + * Constructor. + * + * @param string $accountName storage account name. + * @param string $accountKey storage account primary or secondary key. + * + * @return SharedKeyAuthScheme + */ + public function __construct($accountName, $accountKey) + { + $this->accountKey = $accountKey; + $this->accountName = $accountName; + + $this->includedHeaders = array(); + $this->includedHeaders[] = Resources::CONTENT_ENCODING; + $this->includedHeaders[] = Resources::CONTENT_LANGUAGE; + $this->includedHeaders[] = Resources::CONTENT_LENGTH; + $this->includedHeaders[] = Resources::CONTENT_MD5; + $this->includedHeaders[] = Resources::CONTENT_TYPE; + $this->includedHeaders[] = Resources::DATE; + $this->includedHeaders[] = Resources::IF_MODIFIED_SINCE; + $this->includedHeaders[] = Resources::IF_MATCH; + $this->includedHeaders[] = Resources::IF_NONE_MATCH; + $this->includedHeaders[] = Resources::IF_UNMODIFIED_SINCE; + $this->includedHeaders[] = Resources::RANGE; + } + + /** + * Computes the authorization signature for blob and queue shared key. + * + * @param array $headers request headers. + * @param string $url reuqest url. + * @param array $queryParams query variables. + * @param string $httpMethod request http method. + * + * @see Blob and Queue Services (Shared Key Authentication) at + * http://msdn.microsoft.com/en-us/library/windowsazure/dd179428.aspx + * + * @return string + */ + protected function computeSignature( + array $headers, + $url, + array $queryParams, + $httpMethod + ) { + $canonicalizedHeaders = $this->computeCanonicalizedHeaders($headers); + + $canonicalizedResource = $this->computeCanonicalizedResource( + $url, + $queryParams + ); + + $stringToSign = array(); + $stringToSign[] = strtoupper($httpMethod); + + foreach ($this->includedHeaders as $header) { + $stringToSign[] = Utilities::tryGetValueInsensitive($header, $headers); + } + + if (count($canonicalizedHeaders) > 0) { + $stringToSign[] = implode("\n", $canonicalizedHeaders); + } + + $stringToSign[] = $canonicalizedResource; + $stringToSign = implode("\n", $stringToSign); + + return $stringToSign; + } + + /** + * Returns authorization header to be included in the request. + * + * @param array $headers request headers. + * @param string $url reuqest url. + * @param array $queryParams query variables. + * @param string $httpMethod request http method. + * + * @see Specifying the Authorization Header section at + * http://msdn.microsoft.com/en-us/library/windowsazure/dd179428.aspx + * + * @return string + */ + public function getAuthorizationHeader( + array $headers, + $url, + array $queryParams, + $httpMethod + ) { + $signature = $this->computeSignature( + $headers, + $url, + $queryParams, + $httpMethod + ); + + return 'SharedKey ' . $this->accountName . ':' . base64_encode( + hash_hmac('sha256', $signature, base64_decode($this->accountKey), true) + ); + } + + /** + * Computes canonicalized headers for headers array. + * + * @param array $headers request headers. + * + * @see Constructing the Canonicalized Headers String section at + * http://msdn.microsoft.com/en-us/library/windowsazure/dd179428.aspx + * + * @return array + */ + protected function computeCanonicalizedHeaders($headers) + { + $canonicalizedHeaders = array(); + $normalizedHeaders = array(); + $validPrefix = Resources::X_MS_HEADER_PREFIX; + + if (is_null($normalizedHeaders)) { + return $canonicalizedHeaders; + } + + foreach ($headers as $header => $value) { + // Convert header to lower case. + $header = strtolower($header); + + // Retrieve all headers for the resource that begin with x-ms-, + // including the x-ms-date header. + if (Utilities::startsWith($header, $validPrefix)) { + // Unfold the string by replacing any breaking white space + // (meaning what splits the headers, which is \r\n) with a single + // space. + $value = str_replace("\r\n", ' ', $value); + + // Trim any white space around the colon in the header. + $value = ltrim($value); + $header = rtrim($header); + + $normalizedHeaders[$header] = $value; + } + } + + // Sort the headers lexicographically by header name, in ascending order. + // Note that each header may appear only once in the string. + ksort($normalizedHeaders); + + foreach ($normalizedHeaders as $key => $value) { + $canonicalizedHeaders[] = $key . ':' . $value; + } + + return $canonicalizedHeaders; + } + + /** + * Computes canonicalized resources from URL using Table formar + * + * @param string $url request url. + * @param array $queryParams request query variables. + * + * @see Constructing the Canonicalized Resource String section at + * http://msdn.microsoft.com/en-us/library/windowsazure/dd179428.aspx + * + * @return string + */ + protected function computeCanonicalizedResourceForTable($url, $queryParams) + { + $queryParams = array_change_key_case($queryParams); + + // 1. Beginning with an empty string (""), append a forward slash (/), + // followed by the name of the account that owns the accessed resource. + $canonicalizedResource = '/' . $this->accountName; + + // 2. Append the resource's encoded URI path, without any query parameters. + $canonicalizedResource .= parse_url($url, PHP_URL_PATH); + + // 3. The query string should include the question mark and the comp + // parameter (for example, ?comp=metadata). No other parameters should + // be included on the query string. + if (array_key_exists(Resources::QP_COMP, $queryParams)) { + $canonicalizedResource .= '?' . Resources::QP_COMP . '='; + $canonicalizedResource .= $queryParams[Resources::QP_COMP]; + } + + return $canonicalizedResource; + } + + /** + * Computes canonicalized resources from URL. + * + * @param string $url request url. + * @param array $queryParams request query variables. + * + * @see Constructing the Canonicalized Resource String section at + * http://msdn.microsoft.com/en-us/library/windowsazure/dd179428.aspx + * + * @return string + */ + protected function computeCanonicalizedResource($url, $queryParams) + { + $queryParams = array_change_key_case($queryParams); + + // 1. Beginning with an empty string (""), append a forward slash (/), + // followed by the name of the account that owns the accessed resource. + $canonicalizedResource = '/' . $this->accountName; + + // 2. Append the resource's encoded URI path, without any query parameters. + $canonicalizedResource .= parse_url($url, PHP_URL_PATH); + + // 3. Retrieve all query parameters on the resource URI, including the comp + // parameter if it exists. + // 4. Sort the query parameters lexicographically by parameter name, in + // ascending order. + if (count($queryParams) > 0) { + ksort($queryParams); + } + + // 5. Convert all parameter names to lowercase. + // 6. URL-decode each query parameter name and value. + // 7. Append each query parameter name and value to the string in the + // following format: + // parameter-name:parameter-value + // 9. Group query parameters + // 10. Append a new line character (\n) after each name-value pair. + foreach ($queryParams as $key => $value) { + // $value must already be ordered lexicographically + // See: ServiceRestProxy::groupQueryValues + $canonicalizedResource .= "\n" . $key . ':' . $value; + } + + return $canonicalizedResource; + } + + /** + * Adds authentication header to the request headers. + * + * @param \GuzzleHttp\Psr7\Request $request HTTP request object. + * + * @abstract + * + * @return \GuzzleHttp\Psr7\Request + */ + public function signRequest(Request $request) + { + $requestHeaders = HttpFormatter::formatHeaders($request->getHeaders()); + + $signedKey = $this->getAuthorizationHeader( + $requestHeaders, + $request->getUri(), + Query::parse( + $request->getUri()->getQuery() + ), + $request->getMethod() + ); + + return $request->withHeader(Resources::AUTHENTICATION, $signedKey); + } +} diff --git a/3rdparty/microsoft/azure-storage-common/src/Common/Internal/Authentication/TokenAuthScheme.php b/3rdparty/microsoft/azure-storage-common/src/Common/Internal/Authentication/TokenAuthScheme.php new file mode 100644 index 00000000..d654cc1b --- /dev/null +++ b/3rdparty/microsoft/azure-storage-common/src/Common/Internal/Authentication/TokenAuthScheme.php @@ -0,0 +1,73 @@ + + * @copyright 2019 Microsoft Corporation + * @license https://github.com/azure/azure-storage-php/LICENSE + * @link https://github.com/azure/azure-storage-php + */ + +namespace MicrosoftAzure\Storage\Common\Internal\Authentication; + +use GuzzleHttp\Psr7\Request; +use MicrosoftAzure\Storage\Common\Internal\Resources; +use MicrosoftAzure\Storage\Common\Internal\Validate; + +/** + * Azure authentication scheme for token credential. + * + * @ignore + * @category Microsoft + * @package MicrosoftAzure\Storage\Common\Internal\Authentication + * @author Azure Storage PHP SDK + * @copyright 2019 Microsoft Corporation + * @license https://github.com/azure/azure-storage-php/LICENSE + * @link https://github.com/azure/azure-storage-php + */ +class TokenAuthScheme implements IAuthScheme +{ + /** + * The authentication token + */ + protected $tokenRef; + + /** + * Constructor. + * + * @param string $token the token used for AAD authentication. + */ + public function __construct(&$token) + { + $this->tokenRef =& $token; + } + + /** + * Adds authentication header to the request headers. + * + * @param \GuzzleHttp\Psr7\Request $request HTTP request object. + * + * @abstract + * + * @return \GuzzleHttp\Psr7\Request + */ + public function signRequest(Request $request) + { + $bearerToken = "Bearer ". $this->tokenRef; + return $request->withHeader(Resources::AUTHENTICATION, $bearerToken); + } +} diff --git a/3rdparty/microsoft/azure-storage-common/src/Common/Internal/ConnectionStringParser.php b/3rdparty/microsoft/azure-storage-common/src/Common/Internal/ConnectionStringParser.php new file mode 100644 index 00000000..789513a0 --- /dev/null +++ b/3rdparty/microsoft/azure-storage-common/src/Common/Internal/ConnectionStringParser.php @@ -0,0 +1,335 @@ + + * @copyright 2016 Microsoft Corporation + * @license https://github.com/azure/azure-storage-php/LICENSE + * @link https://github.com/azure/azure-storage-php + */ + +namespace MicrosoftAzure\Storage\Common\Internal; + +/** + * Helper methods for parsing connection strings. The rules for formatting connection + * strings are defined here: + * www.connectionstrings.com/articles/show/important-rules-for-connection-strings + * + * @ignore + * @category Microsoft + * @package MicrosoftAzure\Storage\Common\Internal + * @author Azure Storage PHP SDK + * @copyright 2016 Microsoft Corporation + * @license https://github.com/azure/azure-storage-php/LICENSE + * @link https://github.com/azure/azure-storage-php + */ +class ConnectionStringParser +{ + const EXPECT_KEY = 'ExpectKey'; + const EXPECT_ASSIGNMENT = 'ExpectAssignment'; + const EXPECT_VALUE = 'ExpectValue'; + const EXPECT_SEPARATOR = 'ExpectSeparator'; + + private $_argumentName; + private $_value; + private $_pos; + private $_state; + + /** + * Parses the connection string into a collection of key/value pairs. + * + * @param string $argumentName Name of the argument to be used in error + * messages. + * @param string $connectionString Connection string. + * + * @return array + */ + public static function parseConnectionString($argumentName, $connectionString) + { + Validate::canCastAsString($argumentName, 'argumentName'); + Validate::notNullOrEmpty($argumentName, 'argumentName'); + Validate::canCastAsString($connectionString, 'connectionString'); + Validate::notNullOrEmpty($connectionString, 'connectionString'); + + $parser = new ConnectionStringParser($argumentName, $connectionString); + return $parser->_parse(); + } + + /** + * Initializes the object. + * + * @param string $argumentName Name of the argument to be used in error + * messages. + * @param string $value Connection string. + */ + private function __construct($argumentName, $value) + { + $this->_argumentName = $argumentName; + $this->_value = $value; + $this->_pos = 0; + $this->_state = ConnectionStringParser::EXPECT_KEY; + } + + /** + * Parses the connection string. + * + * @return array + * + * @throws \RuntimeException + */ + private function _parse() + { + $key = null; + $value = null; + $connectionStringValues = array(); + + while (true) { + $this->_skipWhiteSpaces(); + + if ($this->_pos == strlen($this->_value) + && $this->_state != ConnectionStringParser::EXPECT_VALUE + ) { + // Not stopping after the end has been reached and a value is + // expected results in creating an empty value, which we expect. + break; + } + + switch ($this->_state) { + case ConnectionStringParser::EXPECT_KEY: + $key = $this->_extractKey(); + $this->_state = ConnectionStringParser::EXPECT_ASSIGNMENT; + break; + + case ConnectionStringParser::EXPECT_ASSIGNMENT: + $this->_skipOperator('='); + $this->_state = ConnectionStringParser::EXPECT_VALUE; + break; + + case ConnectionStringParser::EXPECT_VALUE: + $value = $this->_extractValue(); + $this->_state = + ConnectionStringParser::EXPECT_SEPARATOR; + $connectionStringValues[$key] = $value; + $key = null; + $value = null; + break; + + default: + $this->_skipOperator(';'); + $this->_state = ConnectionStringParser::EXPECT_KEY; + break; + } + } + + // Must end parsing in the valid state (expected key or separator) + if ($this->_state == ConnectionStringParser::EXPECT_ASSIGNMENT) { + throw $this->_createException( + $this->_pos, + Resources::MISSING_CONNECTION_STRING_CHAR, + '=' + ); + } + + return $connectionStringValues; + } + + /** + *Generates an invalid connection string exception with the detailed error + * message. + * + * @param integer $position The position of the error. + * @param string $errorString The short error formatting string. + * + * @return \RuntimeException + */ + private function _createException($position, $errorString) + { + $arguments = func_get_args(); + + // Remove first and second arguments (position and error string) + unset($arguments[0], $arguments[1]); + + // Create a short error message. + $errorString = vsprintf($errorString, $arguments); + + // Add position. + $errorString = sprintf( + Resources::ERROR_PARSING_STRING, + $errorString, + $position + ); + + // Create final error message. + $errorString = sprintf( + Resources::INVALID_CONNECTION_STRING, + $this->_argumentName, + $errorString + ); + + return new \RuntimeException($errorString); + } + + /** + * Skips whitespaces at the current position. + * + * @return void + */ + private function _skipWhiteSpaces() + { + while ($this->_pos < strlen($this->_value) + && ctype_space($this->_value[$this->_pos]) + ) { + $this->_pos++; + } + } + + /** + * Extracts the key's value. + * + * @return string + */ + private function _extractValue() + { + $value = Resources::EMPTY_STRING; + + if ($this->_pos < strlen($this->_value)) { + $ch = $this->_value[$this->_pos]; + + if ($ch == '"' || $ch == '\'') { + // Value is contained between double quotes or skipped single quotes. + $this->_pos++; + $value = $this->_extractString($ch); + } else { + $firstPos = $this->_pos; + $isFound = false; + + while ($this->_pos < strlen($this->_value) && !$isFound) { + $ch = $this->_value[$this->_pos]; + + if ($ch == ';') { + $isFound = true; + } else { + $this->_pos++; + } + } + + $value = rtrim( + substr($this->_value, $firstPos, $this->_pos - $firstPos) + ); + } + } + + return $value; + } + + /** + * Extracts key at the current position. + * + * @return string + */ + private function _extractKey() + { + $key = null; + $firstPos = $this->_pos; + $ch = $this->_value[$this->_pos]; + + if ($ch == '"' || $ch == '\'') { + $this->_pos++; + $key = $this->_extractString($ch); + } elseif ($ch == ';' || $ch == '=') { + // Key name was expected. + throw $this->_createException( + $firstPos, + Resources::ERROR_CONNECTION_STRING_MISSING_KEY + ); + } else { + while ($this->_pos < strlen($this->_value)) { + $ch = $this->_value[$this->_pos]; + + // At this point we've read the key, break. + if ($ch == '=') { + break; + } + + $this->_pos++; + } + $key = rtrim(substr($this->_value, $firstPos, $this->_pos - $firstPos)); + } + + if (strlen($key) == 0) { + // Empty key name. + throw $this->_createException( + $firstPos, + Resources::ERROR_CONNECTION_STRING_EMPTY_KEY + ); + } + + return $key; + } + + /** + * Extracts the string until the given quotation mark. + * + * @param string $quote The quotation mark terminating the string. + * + * @return string + */ + private function _extractString($quote) + { + $firstPos = $this->_pos; + + while ($this->_pos < strlen($this->_value) + && $this->_value[$this->_pos] != $quote + ) { + $this->_pos++; + } + + if ($this->_pos == strlen($this->_value)) { + // Runaway string. + throw $this->_createException( + $this->_pos, + Resources::ERROR_CONNECTION_STRING_MISSING_CHARACTER, + $quote + ); + } + + return substr($this->_value, $firstPos, $this->_pos++ - $firstPos); + } + + /** + * Skips specified operator. + * + * @param string $operatorChar The operator character. + * + * @return void + * + * @throws \RuntimeException + */ + private function _skipOperator($operatorChar) + { + if ($this->_value[$this->_pos] != $operatorChar) { + // Character was expected. + throw $this->_createException( + $this->_pos, + Resources::MISSING_CONNECTION_STRING_CHAR, + $operatorChar + ); + } + + $this->_pos++; + } +} diff --git a/3rdparty/microsoft/azure-storage-common/src/Common/Internal/ConnectionStringSource.php b/3rdparty/microsoft/azure-storage-common/src/Common/Internal/ConnectionStringSource.php new file mode 100644 index 00000000..36c2aec6 --- /dev/null +++ b/3rdparty/microsoft/azure-storage-common/src/Common/Internal/ConnectionStringSource.php @@ -0,0 +1,83 @@ + + * @copyright 2016 Microsoft Corporation + * @license https://github.com/azure/azure-storage-php/LICENSE + * @link https://github.com/azure/azure-storage-php + */ + +namespace MicrosoftAzure\Storage\Common\Internal; + +/** + * Holder for default connection string sources used in CloudConfigurationManager. + * + * @ignore + * @category Microsoft + * @package MicrosoftAzure\Storage\Common\Internal + * @author Azure Storage PHP SDK + * @copyright 2016 Microsoft Corporation + * @license https://github.com/azure/azure-storage-php/LICENSE + * @link https://github.com/azure/azure-storage-php + */ +class ConnectionStringSource +{ + private static $_defaultSources; + private static $_isInitialized; + const ENVIRONMENT_SOURCE = 'environment_source'; + + /** + * Initializes the default sources. + * + * @return void + */ + private static function _init() + { + if (!self::$_isInitialized) { + self::$_defaultSources = array( + self::ENVIRONMENT_SOURCE => array(__CLASS__, 'environmentSource') + ); + self::$_isInitialized = true; + } + } + + /** + * Gets a connection string value from the system environment. + * + * @param string $key The connection string name. + * + * @return string + */ + public static function environmentSource($key) + { + Validate::canCastAsString($key, 'key'); + + return getenv($key); + } + + /** + * Gets list of default sources. + * + * @return array + */ + public static function getDefaultSources() + { + self::_init(); + return self::$_defaultSources; + } +} diff --git a/3rdparty/microsoft/azure-storage-common/src/Common/Internal/Http/HttpCallContext.php b/3rdparty/microsoft/azure-storage-common/src/Common/Internal/Http/HttpCallContext.php new file mode 100644 index 00000000..31eba33e --- /dev/null +++ b/3rdparty/microsoft/azure-storage-common/src/Common/Internal/Http/HttpCallContext.php @@ -0,0 +1,432 @@ + + * @copyright 2016 Microsoft Corporation + * @license https://github.com/azure/azure-storage-php/LICENSE + * @link https://github.com/azure/azure-storage-php + */ + +namespace MicrosoftAzure\Storage\Common\Internal\Http; + +use MicrosoftAzure\Storage\Common\Internal\Utilities; +use MicrosoftAzure\Storage\Common\Internal\Resources; +use MicrosoftAzure\Storage\Common\Internal\Validate; +use MicrosoftAzure\Storage\Common\Models\ServiceOptions; + +/** + * Holds basic elements for making HTTP call. + * + * @ignore + * @category Microsoft + * @package MicrosoftAzure\Storage\Common\Internal\Http + * @author Azure Storage PHP SDK + * @copyright 2016 Microsoft Corporation + * @license https://github.com/azure/azure-storage-php/LICENSE + * @link https://github.com/azure/azure-storage-php + */ +class HttpCallContext +{ + private $_method; + private $_headers; + private $_queryParams; + private $_postParameters; + private $_uri; + private $_path; + private $_statusCodes; + private $_body; + private $_serviceOptions; + + /** + * Default constructor. + */ + public function __construct() + { + $this->_method = null; + $this->_body = null; + $this->_path = null; + $this->_uri = null; + $this->_queryParams = array(); + $this->_postParameters = array(); + $this->_statusCodes = array(); + $this->_headers = array(); + $this->_serviceOptions = new ServiceOptions(); + } + + /** + * Gets method. + * + * @return string + */ + public function getMethod() + { + return $this->_method; + } + + /** + * Sets method. + * + * @param string $method The method value. + * + * @return void + */ + public function setMethod($method) + { + Validate::canCastAsString($method, 'method'); + + $this->_method = $method; + } + + /** + * Gets headers. + * + * @return array + */ + public function getHeaders() + { + return $this->_headers; + } + + /** + * Sets headers. + * + * Ignores the header if its value is empty. + * + * @param array $headers The headers value. + * + * @return void + */ + public function setHeaders(array $headers) + { + $this->_headers = array(); + foreach ($headers as $key => $value) { + $this->addHeader($key, $value); + } + } + + /** + * Gets queryParams. + * + * @return array + */ + public function getQueryParameters() + { + return $this->_queryParams; + } + + /** + * Sets queryParams. + * + * Ignores the query variable if its value is empty. + * + * @param array $queryParams The queryParams value. + * + * @return void + */ + public function setQueryParameters(array $queryParams) + { + $this->_queryParams = array(); + foreach ($queryParams as $key => $value) { + $this->addQueryParameter($key, $value); + } + } + + /** + * Gets uri. + * + * @return string + */ + public function getUri() + { + return $this->_uri; + } + + /** + * Sets uri. + * + * @param string $uri The uri value. + * + * @return void + */ + public function setUri($uri) + { + Validate::canCastAsString($uri, 'uri'); + + $this->_uri = $uri; + } + + /** + * Gets path. + * + * @return string + */ + public function getPath() + { + return $this->_path; + } + + /** + * Sets path. + * + * @param string $path The path value. + * + * @return void + */ + public function setPath($path) + { + Validate::canCastAsString($path, 'path'); + + $this->_path = $path; + } + + /** + * Gets statusCodes. + * + * @return array + */ + public function getStatusCodes() + { + return $this->_statusCodes; + } + + /** + * Sets statusCodes. + * + * @param array $statusCodes The statusCodes value. + * + * @return void + */ + public function setStatusCodes(array $statusCodes) + { + $this->_statusCodes = array(); + foreach ($statusCodes as $value) { + $this->addStatusCode($value); + } + } + + /** + * Gets body. + * + * @return string + */ + public function getBody() + { + return $this->_body; + } + + /** + * Sets body. + * + * @param string $body The body value. + * + * @return void + */ + public function setBody($body) + { + Validate::canCastAsString($body, 'body'); + + $this->_body = $body; + } + + /** + * Adds or sets header pair. + * + * @param string $name The HTTP header name. + * @param string $value The HTTP header value. + * + * @return void + */ + public function addHeader($name, $value) + { + Validate::canCastAsString($name, 'name'); + Validate::canCastAsString($value, 'value'); + + $this->_headers[$name] = $value; + } + + /** + * Adds or sets header pair. + * + * Ignores header if it's value satisfies empty(). + * + * @param string $name The HTTP header name. + * @param string $value The HTTP header value. + * + * @return void + */ + public function addOptionalHeader($name, $value) + { + Validate::canCastAsString($name, 'name'); + Validate::canCastAsString($value, 'value'); + + if (!empty($value)) { + $this->_headers[$name] = $value; + } + } + + /** + * Removes header from the HTTP request headers. + * + * @param string $name The HTTP header name. + * + * @return void + */ + public function removeHeader($name) + { + Validate::canCastAsString($name, 'name'); + Validate::notNullOrEmpty($name, 'name'); + + unset($this->_headers[$name]); + } + + /** + * Adds or sets query parameter pair. + * + * @param string $name The URI query parameter name. + * @param string $value The URI query parameter value. + * + * @return void + */ + public function addQueryParameter($name, $value) + { + Validate::canCastAsString($name, 'name'); + Validate::canCastAsString($value, 'value'); + + $this->_queryParams[$name] = $value; + } + + /** + * Gets HTTP POST parameters. + * + * @return array + */ + public function getPostParameters() + { + return $this->_postParameters; + } + + /** + * Sets HTTP POST parameters. + * + * @param array $postParameters The HTTP POST parameters. + * + * @return void + */ + public function setPostParameters(array $postParameters) + { + Validate::isArray($postParameters, 'postParameters'); + $this->_postParameters = $postParameters; + } + + /** + * Adds or sets query parameter pair. + * + * Ignores query parameter if it's value satisfies empty(). + * + * @param string $name The URI query parameter name. + * @param string $value The URI query parameter value. + * + * @return void + */ + public function addOptionalQueryParameter($name, $value) + { + Validate::canCastAsString($name, 'name'); + Validate::canCastAsString($value, 'value'); + + if (!empty($value)) { + $this->_queryParams[$name] = $value; + } + } + + /** + * Adds status code to the expected status codes. + * + * @param integer $statusCode The expected status code. + * + * @return void + */ + public function addStatusCode($statusCode) + { + Validate::isInteger($statusCode, 'statusCode'); + + $this->_statusCodes[] = $statusCode; + } + + /** + * Gets header value. + * + * @param string $name The header name. + * + * @return mixed + */ + public function getHeader($name) + { + return Utilities::tryGetValue($this->_headers, $name); + } + + /** + * Gets the saved service options + * + * @return ServiceOptions + */ + public function getServiceOptions() + { + if ($this->_serviceOptions == null) { + $this->_serviceOptions = new ServiceOptions(); + } + return $this->_serviceOptions; + } + + /** + * Sets the service options + * + * @param ServiceOptions $serviceOptions the service options to be set. + * + * @return void + */ + public function setServiceOptions(ServiceOptions $serviceOptions) + { + $this->_serviceOptions = $serviceOptions; + } + + /** + * Converts the context object to string. + * + * @return string + */ + public function __toString() + { + $headers = Resources::EMPTY_STRING; + $uri = $this->_uri; + + if ($uri === null) { + $uri = '/'; + } elseif ($uri[strlen($uri)-1] != '/') { + $uri = $uri.'/'; + } + + foreach ($this->_headers as $key => $value) { + $headers .= "$key: $value\n"; + } + + $str = "$this->_method $uri$this->_path HTTP/1.1\n$headers\n"; + $str .= "$this->_body"; + + return $str; + } +} diff --git a/3rdparty/microsoft/azure-storage-common/src/Common/Internal/Http/HttpFormatter.php b/3rdparty/microsoft/azure-storage-common/src/Common/Internal/Http/HttpFormatter.php new file mode 100644 index 00000000..b29f3d14 --- /dev/null +++ b/3rdparty/microsoft/azure-storage-common/src/Common/Internal/Http/HttpFormatter.php @@ -0,0 +1,59 @@ + + * @copyright 2016 Microsoft Corporation + * @license https://github.com/azure/azure-storage-php/LICENSE + * @link https://github.com/azure/azure-storage-php + */ + +namespace MicrosoftAzure\Storage\Common\Internal\Http; + +/** + * Helper class to format the http headers + * + * @ignore + * @category Microsoft + * @package MicrosoftAzure\Storage\Common\Internal\Http + * @author Azure Storage PHP SDK + * @copyright 2016 Microsoft Corporation + * @license https://github.com/azure/azure-storage-php/LICENSE + * @link https://github.com/azure/azure-storage-php + */ +class HttpFormatter +{ + /** + * Convert a http headers array into an uniformed format for further process + * + * @param array $headers headers for format + * + * @return array + */ + public static function formatHeaders(array $headers) + { + $result = array(); + foreach ($headers as $key => $value) { + if (is_array($value) && count($value) == 1) { + $result[strtolower($key)] = $value[0]; + } else { + $result[strtolower($key)] = $value; + } + } + + return $result; + } +} diff --git a/3rdparty/microsoft/azure-storage-common/src/Common/Internal/MetadataTrait.php b/3rdparty/microsoft/azure-storage-common/src/Common/Internal/MetadataTrait.php new file mode 100644 index 00000000..1f9e79bc --- /dev/null +++ b/3rdparty/microsoft/azure-storage-common/src/Common/Internal/MetadataTrait.php @@ -0,0 +1,142 @@ + + * @copyright 2017 Microsoft Corporation + * @license https://github.com/azure/azure-storage-php/LICENSE + * @link https://github.com/azure/azure-storage-php + */ + +namespace MicrosoftAzure\Storage\Common\Internal; + +/** + * Trait implementing common logic for metadata, last-modified and etag. The + * code is shared for multiple REST APIs. + * + * @category Microsoft + * @package MicrosoftAzure\Storage\Common\Internal + * @author Azure Storage PHP SDK + * @copyright 2017 Microsoft Corporation + * @license https://github.com/azure/azure-storage-php/LICENSE + * @link https://github.com/azure/azure-storage-php + */ +trait MetadataTrait +{ + private $lastModified; + private $etag; + private $metadata; + + /** + * Any operation that modifies the share or its properties or metadata + * updates the last modified time. Operations on files do not affect the + * last modified time of the share. + * + * @return \DateTime. + */ + public function getLastModified() + { + return $this->lastModified; + } + + /** + * Sets share lastModified. + * + * @param \DateTime $lastModified value. + * + * @return void + */ + protected function setLastModified(\DateTime $lastModified) + { + $this->lastModified = $lastModified; + } + + /** + * The entity tag for the share. If the request version is 2011-08-18 or + * newer, the ETag value will be in quotes. + * + * @return string + */ + public function getETag() + { + return $this->etag; + } + + /** + * Sets share etag. + * + * @param string $etag value. + * + * @return void + */ + protected function setETag($etag) + { + $this->etag = $etag; + } + + /** + * Gets user defined metadata. + * + * @return array + */ + public function getMetadata() + { + return $this->metadata; + } + + /** + * Sets user defined metadata. This metadata should be added without the + * header prefix (x-ms-meta-*). + * + * @param array $metadata user defined metadata object in array form. + * + * @return void + */ + protected function setMetadata(array $metadata) + { + $this->metadata = $metadata; + } + + /** + * Create an instance using the response headers from the API call. + * + * @param array $responseHeaders The array contains all the response headers + * + * @internal + * + * @return GetShareMetadataResult + */ + public static function createMetadataResult(array $responseHeaders) + { + $result = new static(); + $metadata = Utilities::getMetadataArray($responseHeaders); + $date = Utilities::tryGetValueInsensitive( + Resources::LAST_MODIFIED, + $responseHeaders + ); + $date = Utilities::rfc1123ToDateTime($date); + $result->setETag(Utilities::tryGetValueInsensitive( + Resources::ETAG, + $responseHeaders + )); + $result->setMetadata($metadata); + $result->setLastModified($date); + + return $result; + } +} diff --git a/3rdparty/microsoft/azure-storage-common/src/Common/Internal/Middlewares/CommonRequestMiddleware.php b/3rdparty/microsoft/azure-storage-common/src/Common/Internal/Middlewares/CommonRequestMiddleware.php new file mode 100644 index 00000000..4da9ba0b --- /dev/null +++ b/3rdparty/microsoft/azure-storage-common/src/Common/Internal/Middlewares/CommonRequestMiddleware.php @@ -0,0 +1,132 @@ + + * @copyright 2017 Microsoft Corporation + * @license https://github.com/azure/azure-storage-php/LICENSE + * @link https://github.com/azure/azure-storage-php + */ + +namespace MicrosoftAzure\Storage\Common\Internal\Middlewares; + +use MicrosoftAzure\Storage\Common\Middlewares\MiddlewareBase; +use MicrosoftAzure\Storage\Common\Internal\Authentication\IAuthScheme; +use MicrosoftAzure\Storage\Common\Internal\Resources; +use Psr\Http\Message\RequestInterface; + +/** + * CommonRequestMiddleware is the middleware used to add the necessary headers + * and to sign the request with provided authentication scheme. This middleware + * is by default applied to each of the request. + * + * @ignore + * @category Microsoft + * @package MicrosoftAzure\Storage\Common\Internal + * @author Azure Storage PHP SDK + * @copyright 2017 Microsoft Corporation + * @license https://github.com/azure/azure-storage-php/LICENSE + * @link https://github.com/azure/azure-storage-php + */ +class CommonRequestMiddleware extends MiddlewareBase +{ + private $authenticationScheme; + private $headers; + private $msVersion; + private $userAgent; + + /** + * Creates CommonRequestMiddleware with the passed scheme and headers to + * be added. + * + * @param IAuthScheme $authenticationScheme The authentication scheme. + * @param string $storageAPIVersion Azure Storage Service API version, + * like '2016-05-31'. + * @param string $serviceSDKVersion Like '1.0.1' or '1.2.0'. + * @param array $headers The headers to be added. + */ + public function __construct( + IAuthScheme $authenticationScheme = null, + $storageAPIVersion, + $serviceSDKVersion, + array $headers = array() + ) { + $this->authenticationScheme = $authenticationScheme; + $this->msVersion = $storageAPIVersion; + $this->userAgent = self::getUserAgent($serviceSDKVersion); + $this->headers = $headers; + } + + /** + * Add the provided headers, the date, then sign the request using the + * authentication scheme, and return it. + * + * @param RequestInterface $request un-signed request. + * + * @return RequestInterface + */ + protected function onRequest(RequestInterface $request) + { + $result = $request; + + //Adding headers. + foreach ($this->headers as $key => $value) { + $headers = $result->getHeaders(); + if (!array_key_exists($key, $headers)) { + $result = $result->withHeader($key, $value); + } + } + + //rewriting version and user-agent. + $result = $result->withHeader( + Resources::X_MS_VERSION, + $this->msVersion + ); + $result = $result->withHeader( + Resources::USER_AGENT, + $this->userAgent + ); + + //Adding date. + $date = gmdate(Resources::AZURE_DATE_FORMAT, time()); + $result = $result->withHeader(Resources::DATE, $date); + + //Adding client request-ID if not specified by the user. + if (!$result->hasHeader(Resources::X_MS_CLIENT_REQUEST_ID)) { + $result = $result->withHeader(Resources::X_MS_CLIENT_REQUEST_ID, \uniqid()); + } + //Sign the request if authentication scheme is not null. + $request = $this->authenticationScheme == null ? + $request : $this->authenticationScheme->signRequest($result); + return $request; + } + + /** + * Gets the user agent string used in request header. + * + * @param $serviceSDKVersion + * + * @return string + */ + private static function getUserAgent($serviceSDKVersion) + { + // e.g. User-Agent: Azure-Storage/1.0.1-1.1.1 (PHP 5.5.32)/WINNT + return 'Azure-Storage/' . $serviceSDKVersion . '-' . + Resources::COMMON_SDK_VERSION . + ' (PHP ' . PHP_VERSION . ')' . '/' . php_uname("s"); + } +} diff --git a/3rdparty/microsoft/azure-storage-common/src/Common/Internal/Resources.php b/3rdparty/microsoft/azure-storage-common/src/Common/Internal/Resources.php new file mode 100644 index 00000000..d3eff1cc --- /dev/null +++ b/3rdparty/microsoft/azure-storage-common/src/Common/Internal/Resources.php @@ -0,0 +1,422 @@ + + * @copyright 2016 Microsoft Corporation + * @license https://github.com/azure/azure-storage-php/LICENSE + * @link https://github.com/azure/azure-storage-php + */ + +namespace MicrosoftAzure\Storage\Common\Internal; + +/** + * Project resources. + * + * @ignore + * @category Microsoft + * @package MicrosoftAzure\Storage\Common\Internal + * @author Azure Storage PHP SDK + * @copyright 2016 Microsoft Corporation + * @license https://github.com/azure/azure-storage-php/LICENSE + * @link https://github.com/azure/azure-storage-php + */ +class Resources +{ + // @codingStandardsIgnoreStart + + // Connection strings + const USE_DEVELOPMENT_STORAGE_NAME = 'UseDevelopmentStorage'; + const DEVELOPMENT_STORAGE_PROXY_URI_NAME = 'DevelopmentStorageProxyUri'; + const DEFAULT_ENDPOINTS_PROTOCOL_NAME = 'DefaultEndpointsProtocol'; + const ACCOUNT_NAME_NAME = 'AccountName'; + const ACCOUNT_KEY_NAME = 'AccountKey'; + const SAS_TOKEN_NAME = 'SharedAccessSignature'; + const BLOB_ENDPOINT_NAME = 'BlobEndpoint'; + const QUEUE_ENDPOINT_NAME = 'QueueEndpoint'; + const TABLE_ENDPOINT_NAME = 'TableEndpoint'; + const FILE_ENDPOINT_NAME = 'FileEndpoint'; + const SHARED_ACCESS_SIGNATURE_NAME = 'SharedAccessSignature'; + const ENDPOINT_SUFFIX_NAME = 'EndpointSuffix'; + const DEFAULT_ENDPOINT_SUFFIX = 'core.windows.net'; + const DEV_STORE_NAME = 'devstoreaccount1'; + const DEV_STORE_KEY = 'Eby8vdM02xNOcqFlqUwJPLlmEtlCDXJ1OUzFT50uSRZ6IFsuFq2UVErCz4I6tq/K1SZFPTOtr/KBHBeksoGMGw=='; + const BLOB_BASE_DNS_NAME = 'blob.core.windows.net'; + const BLOB_DNS_PREFIX = 'blob.'; + const QUEUE_BASE_DNS_NAME = 'queue.core.windows.net'; + const QUEUE_DNS_PREFIX = 'queue.'; + const TABLE_BASE_DNS_NAME = 'table.core.windows.net'; + const TABLE_DNS_PREFIX = 'table.'; + const FILE_BASE_DNS_NAME = 'file.core.windows.net'; + const FILE_DNS_PREFIX = 'file.'; + const DEV_STORE_CONNECTION_STRING = 'BlobEndpoint=127.0.0.1:10000;QueueEndpoint=127.0.0.1:10001;TableEndpoint=127.0.0.1:10002;AccountName=devstoreaccount1;AccountKey=Eby8vdM02xNOcqFlqUwJPLlmEtlCDXJ1OUzFT50uSRZ6IFsuFq2UVErCz4I6tq/K1SZFPTOtr/KBHBeksoGMGw=='; + const SUBSCRIPTION_ID_NAME = 'SubscriptionID'; + const CERTIFICATE_PATH_NAME = 'CertificatePath'; + const SECONDARY_STRING = '-secondary'; + const PRIMARY_STRING = '-primary'; + + // Messages + const INVALID_FUNCTION_NAME = 'The class %s does not have a function named %s.'; + const INVALID_TYPE_MSG = 'The provided variable should be of type: '; + const INVALID_META_MSG = 'Metadata cannot contain newline characters.'; + const AZURE_ERROR_MSG = "Fail:\nCode: %s\nValue: %s\ndetails (if any): %s."; + const NOT_IMPLEMENTED_MSG = 'This method is not implemented.'; + const NULL_OR_EMPTY_MSG = "'%s' can't be NULL or empty."; + const NULL_MSG = "'%s' can't be NULL."; + const INVALID_URL_MSG = 'Provided URL is invalid.'; + const INVALID_HT_MSG = 'The header type provided is invalid.'; + const INVALID_VERSION_MSG = 'Server does not support any known protocol versions.'; + const INVALID_EXC_OBJ_MSG = 'Exception object type should be ServiceException.'; + const INVALID_PARAM_MSG = "The provided variable '%s' should be of type '%s'"; + const INVALID_VALUE_MSG = "The provided variable '%s' has unexpected value. Reason: '%s'"; + const INVALID_STRING_LENGTH = "The provided variable '%s' should be of %s characters long"; + const INVALID_SVC_PROP_MSG = 'The provided service properties is invalid.'; + const UNKNOWN_SRILZER_MSG = 'The provided serializer type is unknown'; + const INVALID_CREATE_SERVICE_OPTIONS_MSG = 'Must provide valid location or affinity group.'; + const INVALID_UPDATE_SERVICE_OPTIONS_MSG = 'Must provide either description or label.'; + const INVALID_CONFIG_MSG = 'Config object must be of type Configuration'; + const INVALID_CONFIG_HOSTNAME = "The provided hostname '%s' is invalid."; + const INVALID_CONFIG_URI = "The provided URI '%s' is invalid. It has to pass the check 'filter_var(, FILTER_VALIDATE_URL)'."; + const INVALID_CONFIG_VALUE = "The provided config value '%s' does not belong to the valid values subset:\n%s"; + const INVALID_ACCOUNT_KEY_FORMAT = "The provided account key '%s' is not a valid base64 string. It has to pass the check 'base64_decode(, true)'."; + const MISSING_CONNECTION_STRING_SETTINGS = "The provided connection string '%s' does not have complete configuration settings."; + const INVALID_CONNECTION_STRING_SETTING_KEY = "The setting key '%s' is not found in the expected configuration setting keys:\n%s"; + const INVALID_CERTIFICATE_PATH = "The provided certificate path '%s' is invalid."; + const INSTANCE_TYPE_VALIDATION_MSG = 'The type of %s is %s but is expected to be %s.'; + const INVALID_MESSAGE_OBJECT_TO_SERIALIZE = 'The given object does not have required methods, so it could not be serialized.'; + const MISSING_CONNECTION_STRING_CHAR = "Missing %s character"; + const ERROR_PARSING_STRING = "'%s' at position %d."; + const INVALID_CONNECTION_STRING = "Argument '%s' is not a valid connection string: '%s'"; + const ERROR_CONNECTION_STRING_MISSING_KEY = 'Missing key name'; + const ERROR_CONNECTION_STRING_EMPTY_KEY = 'Empty key name'; + const ERROR_CONNECTION_STRING_MISSING_CHARACTER = "Missing %s character"; + const ERROR_EMPTY_SETTINGS = 'No keys were found in the connection string'; + const MISSING_LOCK_LOCATION_MSG = 'The lock location of the brokered message is missing.'; + const INVALID_SAS_TOKEN = 'The shared access signatures (SAS) provided is not valid \'%s\''; + const INVALID_SLOT = "The provided deployment slot '%s' is not valid. Only 'staging' and 'production' are accepted."; + const INVALID_DEPLOYMENT_LOCATOR_MSG = 'A slot or deployment name must be provided.'; + const INVALID_CHANGE_MODE_MSG = "The change mode must be 'Auto' or 'Manual'. Use Mode class constants for that purpose."; + const INVALID_DEPLOYMENT_STATUS_MSG = "The change mode must be 'Running' or 'Suspended'. Use DeploymentStatus class constants for that purpose."; + const ERROR_OAUTH_GET_ACCESS_TOKEN = 'Unable to get oauth access token for endpoint \'%s\', account name \'%s\''; + const ERROR_OAUTH_SERVICE_MISSING = 'OAuth service missing for account name \'%s\''; + const ERROR_METHOD_NOT_FOUND = 'Method \'%s\' not found in object class \'%s\''; + const ERROR_INVALID_DATE_STRING = 'Parameter \'%s\' is not a date formatted string \'%s\''; + const ERROR_FILE_COULD_NOT_BE_OPENED = 'Error: file with given path could not be opened or created.'; + const INVALID_PARAM_GENERAL = 'The provided parameter \'%s\' is invalid'; + const INVALID_NEGATIVE_PARAM = 'The provided parameter \'%s\' should be positive number.'; + const SIGNED_SERVICE_INVALID_VALIDATION_MSG = 'The signed service should only be a combination of the letters b(lob) q(ueue) t(able) or f(ile).'; + const SIGNED_RESOURCE_TYPE_INVALID_VALIDATION_MSG = 'The signed resource type should only be a combination of the letters s(ervice) c(container) or o(bject).'; + const STRING_NOT_WITH_GIVEN_COMBINATION = 'The string should only be a combination of the letters %s.'; + const SIGNED_PROTOCOL_INVALID_VALIDATION_MSG = 'The signed protocol is invalid: possible values are https or https,http.'; + const ERROR_RESOURCE_TYPE_NOT_SUPPORTED = 'The given resource type cannot be recognized or is not supported.'; + const ERROR_TOO_MANY_SIGNED_IDENTIFIERS = 'There can be at most 5 signed identifiers at the same time.'; + const INVALID_PERMISSION_PROVIDED = 'Invalid permission provided, the permission of resource type \'%s\' can only be of \'%s\''; + const INVALID_RESOURCE_TYPE = 'Provided resource type is invalid.'; + const ERROR_KEY_NOT_EXIST = "The key '%s' does not exist in the given array."; + const RESOURCE_RANGE_LENGTH_MUST_SET = "The start and end/length of the range must be set."; + const INVALID_ACCEPT_CONTENT_TYPE = "The given accept content type is not valid."; + const ERROR_CANNOT_PARSE_XML = "Cannot parse XML, reasons: %s"; + const INVALID_SCHEME = 'HTTP scheme can only be string \'http\' or \'https\'.'; + const AAD_TOKEN_MUST_START_WITH_BEARER = 'AAD token is invalid, please make sure that it has format \'Bearer ################\''; + + // HTTP Headers + const X_MS_HEADER_PREFIX = 'x-ms-'; + const X_MS_META_HEADER_PREFIX = 'x-ms-meta-'; + const X_MS_VERSION = 'x-ms-version'; + const X_MS_DATE = 'x-ms-date'; + const X_MS_COPY_ACTION = 'x-ms-copy-action'; + const X_MS_COPY_ID = 'x-ms-copy-id'; + const X_MS_COPY_COMPLETION_TIME = 'x-ms-copy-completion-time'; + const X_MS_COPY_STATUS = 'x-ms-copy-status'; + const X_MS_COPY_STATUS_DESCRIPTION = 'x-ms-copy-status-description'; + const X_MS_COPY_SOURCE = 'x-ms-copy-source'; + const X_MS_COPY_PROGRESS = 'x-ms-copy-progress'; + const X_MS_RANGE = 'x-ms-range'; + const X_MS_RANGE_GET_CONTENT_MD5 = 'x-ms-range-get-content-md5'; + const X_MS_DELETE_SNAPSHOTS = 'x-ms-delete-snapshots'; + const X_MS_SNAPSHOT = 'x-ms-snapshot'; + const X_MS_SOURCE_IF_MODIFIED_SINCE = 'x-ms-source-if-modified-since'; + const X_MS_SOURCE_IF_UNMODIFIED_SINCE = 'x-ms-source-if-unmodified-since'; + const X_MS_SOURCE_IF_MATCH = 'x-ms-source-if-match'; + const X_MS_SOURCE_IF_NONE_MATCH = 'x-ms-source-if-none-match'; + const X_MS_SOURCE_LEASE_ID = 'x-ms-source-lease-id'; + const X_MS_CONTINUATION_NEXTTABLENAME = 'x-ms-continuation-nexttablename'; + const X_MS_CONTINUATION_NEXTPARTITIONKEY = 'x-ms-continuation-nextpartitionkey'; + const X_MS_CONTINUATION_NEXTROWKEY = 'x-ms-continuation-nextrowkey'; + const X_MS_REQUEST_ID = 'x-ms-request-id'; + const X_MS_CLIENT_REQUEST_ID = 'x-ms-client-request-id'; + const X_MS_CONTINUATION_LOCATION_MODE = 'x-ms-continuation-location-mode'; + const X_MS_TYPE = 'x-ms-type'; + const X_MS_CONTENT_LENGTH = 'x-ms-content-length'; + const X_MS_CACHE_CONTROL = 'x-ms-cache-control'; + const X_MS_CONTENT_TYPE = 'x-ms-content-type'; + const X_MS_CONTENT_MD5 = 'x-ms-content-md5'; + const X_MS_CONTENT_ENCODING = 'x-ms-content-encoding'; + const X_MS_CONTENT_LANGUAGE = 'x-ms-content-language'; + const X_MS_CONTENT_DISPOSITION = 'x-ms-content-disposition'; + const X_MS_WRITE = 'x-ms-write'; + const ETAG = 'etag'; + const LAST_MODIFIED = 'last-modified'; + const DATE = 'date'; + const AUTHENTICATION = 'authorization'; + const WRAP_AUTHORIZATION = 'WRAP access_token="%s"'; + const CONTENT_ENCODING = 'content-encoding'; + const CONTENT_LANGUAGE = 'content-language'; + const CONTENT_LENGTH = 'content-length'; + const CONTENT_LENGTH_NO_SPACE = 'contentlength'; + const CONTENT_MD5 = 'content-md5'; + const CONTENT_TYPE_LOWER_CASE = 'content-type'; + const CONTENT_TYPE = 'Content-Type'; + const CONTENT_ID = 'content-id'; + const CONTENT_RANGE = 'content-range'; + const CACHE_CONTROL = 'cache-control'; + const CONTENT_DISPOSITION = 'content-disposition'; + const IF_MODIFIED_SINCE = 'if-modified-since'; + const IF_MATCH = 'if-match'; + const IF_NONE_MATCH = 'if-none-match'; + const IF_UNMODIFIED_SINCE = 'if-unmodified-since'; + const RANGE = 'range'; + const DATA_SERVICE_VERSION = 'dataserviceversion'; + const MAX_DATA_SERVICE_VERSION = 'maxdataserviceversion'; + const ACCEPT_HEADER = 'accept'; + const ACCEPT_CHARSET = 'accept-charset'; + const USER_AGENT = 'User-Agent'; + const PREFER = 'Prefer'; + + // HTTP Methods + const HTTP_GET = 'GET'; + const HTTP_PUT = 'PUT'; + const HTTP_POST = 'POST'; + const HTTP_HEAD = 'HEAD'; + const HTTP_DELETE = 'DELETE'; + const HTTP_MERGE = 'MERGE'; + + // Misc + const EMPTY_STRING = ''; + const SEPARATOR = ','; + const AZURE_DATE_FORMAT = 'D, d M Y H:i:s T'; + const TIMESTAMP_FORMAT = 'Y-m-d H:i:s'; + const EMULATED = 'EMULATED'; + const EMULATOR_BLOB_URI = '127.0.0.1:10000'; + const EMULATOR_QUEUE_URI = '127.0.0.1:10001'; + const EMULATOR_TABLE_URI = '127.0.0.1:10002'; + const ASTERISK = '*'; + const SERVICE_MANAGEMENT_URL = 'https://management.core.windows.net'; + const HTTP_SCHEME = 'http'; + const HTTPS_SCHEME = 'https'; + const SETTING_NAME = 'SettingName'; + const SETTING_CONSTRAINT = 'SettingConstraint'; + const DEV_STORE_URI = 'http://127.0.0.1'; + const SERVICE_URI_FORMAT = "%s://%s.%s"; + const WRAP_ENDPOINT_URI_FORMAT = "https://%s-sb.accesscontrol.windows.net/WRAPv0.9"; + const MB_IN_BYTES_1 = 1048576; + const MB_IN_BYTES_4 = 4194304; + const MB_IN_BYTES_32 = 33554432; + const MB_IN_BYTES_64 = 67108864; + const MB_IN_BYTES_128 = 134217728; + const MB_IN_BYTES_256 = 268435456; + const MB_IN_BYTES_100 = 104857600; + const GB_IN_BYTES = 1073741824; + const GB_IN_BYTES_200 = 214748364800; + const MAX_BLOB_BLOCKS = 50000; + const MAX_BLOCK_BLOB_SIZE = 5242880000000; + const RETURN_CONTENT = 'return-content'; + const NUMBER_OF_CONCURRENCY = 25;//Guzzle's default value + const DEFAULT_NUMBER_OF_RETRIES = 3; + const DEFAULT_RETRY_INTERVAL = 1000;//Milliseconds + const BEARER = 'Bearer '; + + // Header values + const COMMON_SDK_VERSION = '1.5.2'; + const INT32_MAX = 2147483647; + const INT32_MIN = -2147483648; + + // Query parameter names + const QP_ENTRIES = 'Entries'; + const QP_PREFIX = 'Prefix'; + const QP_PREFIX_LOWERCASE = 'prefix'; + const QP_MAX_RESULTS = 'MaxResults'; + const QP_MAX_RESULTS_LOWERCASE = 'maxresults'; + const QP_MARKER = 'Marker'; + const QP_MARKER_LOWERCASE = 'marker'; + const QP_METADATA = 'Metadata'; + const QP_NEXT_MARKER = 'NextMarker'; + const QP_COMP = 'comp'; + const QP_INCLUDE = 'include'; + const QP_TIMEOUT = 'timeout'; + const QP_REST_TYPE = 'restype'; + const QP_SNAPSHOT = 'snapshot'; + const QP_COPY_ID = 'copyid'; + const QP_NAME = 'Name'; + const QP_PROPERTIES = 'Properties'; + const QP_LAST_MODIFIED = 'Last-Modified'; + const QP_ETAG = 'Etag'; + const QP_QUOTA = 'Quota'; + const QP_CONTENT_LENGTH = 'Content-Length'; + + // Request body content types + const URL_ENCODED_CONTENT_TYPE = 'application/x-www-form-urlencoded'; + const BINARY_FILE_TYPE = 'application/octet-stream'; + const HTTP_TYPE = 'application/http'; + const MULTIPART_MIXED_TYPE = 'multipart/mixed'; + + // Common used XML tags + const XTAG_ATTRIBUTES = '@attributes'; + const XTAG_NAMESPACE = '@namespace'; + const XTAG_LABEL = 'Label'; + const XTAG_NAME = 'Name'; + const XTAG_DESCRIPTION = 'Description'; + const XTAG_LOCATION = 'Location'; + const XTAG_AFFINITY_GROUP = 'AffinityGroup'; + const XTAG_HOSTED_SERVICES = 'HostedServices'; + const XTAG_STORAGE_SERVICES = 'StorageServices'; + const XTAG_STORAGE_SERVICE = 'StorageService'; + const XTAG_DISPLAY_NAME = 'DisplayName'; + const XTAG_SERVICE_NAME = 'ServiceName'; + const XTAG_URL = 'Url'; + const XTAG_ID = 'ID'; + const XTAG_STATUS = 'Status'; + const XTAG_HTTP_STATUS_CODE = 'HttpStatusCode'; + const XTAG_CODE = 'Code'; + const XTAG_MESSAGE = 'Message'; + const XTAG_STORAGE_SERVICE_PROPERTIES = 'StorageServiceProperties'; + const XTAG_SERVICE_ENDPOINT = 'ServiceEndpoint'; + const XTAG_ENDPOINT = 'Endpoint'; + const XTAG_ENDPOINTS = 'Endpoints'; + const XTAG_PRIMARY = 'Primary'; + const XTAG_SECONDARY = 'Secondary'; + const XTAG_KEY_TYPE = 'KeyType'; + const XTAG_STORAGE_SERVICE_KEYS = 'StorageServiceKeys'; + const XTAG_ERROR = 'Error'; + const XTAG_HOSTED_SERVICE = 'HostedService'; + const XTAG_HOSTED_SERVICE_PROPERTIES = 'HostedServiceProperties'; + const XTAG_CREATE_HOSTED_SERVICE = 'CreateHostedService'; + const XTAG_CREATE_STORAGE_SERVICE_INPUT = 'CreateStorageServiceInput'; + const XTAG_UPDATE_STORAGE_SERVICE_INPUT = 'UpdateStorageServiceInput'; + const XTAG_CREATE_AFFINITY_GROUP = 'CreateAffinityGroup'; + const XTAG_UPDATE_AFFINITY_GROUP = 'UpdateAffinityGroup'; + const XTAG_UPDATE_HOSTED_SERVICE = 'UpdateHostedService'; + const XTAG_PACKAGE_URL = 'PackageUrl'; + const XTAG_CONFIGURATION = 'Configuration'; + const XTAG_START_DEPLOYMENT = 'StartDeployment'; + const XTAG_TREAT_WARNINGS_AS_ERROR = 'TreatWarningsAsError'; + const XTAG_CREATE_DEPLOYMENT = 'CreateDeployment'; + const XTAG_DEPLOYMENT_SLOT = 'DeploymentSlot'; + const XTAG_PRIVATE_ID = 'PrivateID'; + const XTAG_ROLE_INSTANCE_LIST = 'RoleInstanceList'; + const XTAG_UPGRADE_DOMAIN_COUNT = 'UpgradeDomainCount'; + const XTAG_ROLE_LIST = 'RoleList'; + const XTAG_SDK_VERSION = 'SdkVersion'; + const XTAG_INPUT_ENDPOINT_LIST = 'InputEndpointList'; + const XTAG_LOCKED = 'Locked'; + const XTAG_ROLLBACK_ALLOWED = 'RollbackAllowed'; + const XTAG_UPGRADE_STATUS = 'UpgradeStatus'; + const XTAG_UPGRADE_TYPE = 'UpgradeType'; + const XTAG_CURRENT_UPGRADE_DOMAIN_STATE = 'CurrentUpgradeDomainState'; + const XTAG_CURRENT_UPGRADE_DOMAIN = 'CurrentUpgradeDomain'; + const XTAG_ROLE_NAME = 'RoleName'; + const XTAG_INSTANCE_NAME = 'InstanceName'; + const XTAG_INSTANCE_STATUS = 'InstanceStatus'; + const XTAG_INSTANCE_UPGRADE_DOMAIN = 'InstanceUpgradeDomain'; + const XTAG_INSTANCE_FAULT_DOMAIN = 'InstanceFaultDomain'; + const XTAG_INSTANCE_SIZE = 'InstanceSize'; + const XTAG_INSTANCE_STATE_DETAILS = 'InstanceStateDetails'; + const XTAG_INSTANCE_ERROR_CODE = 'InstanceErrorCode'; + const XTAG_OS_VERSION = 'OsVersion'; + const XTAG_ROLE_INSTANCE = 'RoleInstance'; + const XTAG_ROLE = 'Role'; + const XTAG_INPUT_ENDPOINT = 'InputEndpoint'; + const XTAG_VIP = 'Vip'; + const XTAG_PORT = 'Port'; + const XTAG_DEPLOYMENT = 'Deployment'; + const XTAG_DEPLOYMENTS = 'Deployments'; + const XTAG_REGENERATE_KEYS = 'RegenerateKeys'; + const XTAG_SWAP = 'Swap'; + const XTAG_PRODUCTION = 'Production'; + const XTAG_SOURCE_DEPLOYMENT = 'SourceDeployment'; + const XTAG_CHANGE_CONFIGURATION = 'ChangeConfiguration'; + const XTAG_MODE = 'Mode'; + const XTAG_UPDATE_DEPLOYMENT_STATUS = 'UpdateDeploymentStatus'; + const XTAG_ROLE_TO_UPGRADE = 'RoleToUpgrade'; + const XTAG_FORCE = 'Force'; + const XTAG_UPGRADE_DEPLOYMENT = 'UpgradeDeployment'; + const XTAG_UPGRADE_DOMAIN = 'UpgradeDomain'; + const XTAG_WALK_UPGRADE_DOMAIN = 'WalkUpgradeDomain'; + const XTAG_ROLLBACK_UPDATE_OR_UPGRADE = 'RollbackUpdateOrUpgrade'; + const XTAG_CONTAINER_NAME = 'ContainerName'; + const XTAG_ACCOUNT_NAME = 'AccountName'; + const XTAG_LOGGING = 'Logging'; + const XTAG_HOUR_METRICS = 'HourMetrics'; + const XTAG_MINUTE_METRICS = 'MinuteMetrics'; + const XTAG_CORS = 'Cors'; + const XTAG_CORS_RULE = 'CorsRule'; + const XTAG_ALLOWED_ORIGINS = 'AllowedOrigins'; + const XTAG_ALLOWED_METHODS = 'AllowedMethods'; + const XTAG_ALLOWED_HEADERS = 'AllowedHeaders'; + const XTAG_EXPOSED_HEADERS = 'ExposedHeaders'; + const XTAG_MAX_AGE_IN_SECONDS = 'MaxAgeInSeconds'; + const XTAG_SIGNED_IDENTIFIERS = 'SignedIdentifiers'; + const XTAG_SIGNED_IDENTIFIER = 'SignedIdentifier'; + const XTAG_ACCESS_POLICY = 'AccessPolicy'; + const XTAG_SIGNED_START = 'Start'; + const XTAG_SIGNED_EXPIRY = 'Expiry'; + const XTAG_SIGNED_PERMISSION = 'Permission'; + const XTAG_SIGNED_ID = 'Id'; + const XTAG_DEFAULT_SERVICE_VERSION = 'DefaultServiceVersion'; + const XTAG_GEO_REPLICATION = 'GeoReplication'; + const XTAG_LAST_SYNC_TIME = 'LastSyncTime'; + const XTAG_PAGE_RANGE = 'PageRange'; + const XTAG_CLEAR_RANGE = 'ClearRange'; + const XTAG_RANGE_START = 'Start'; + const XTAG_RANGE_END = 'End'; + + // PHP URL Keys + const PHP_URL_SCHEME = 'scheme'; + const PHP_URL_HOST = 'host'; + const PHP_URL_PORT = 'port'; + const PHP_URL_USER = 'user'; + const PHP_URL_PASS = 'pass'; + const PHP_URL_PATH = 'path'; + const PHP_URL_QUERY = 'query'; + const PHP_URL_FRAGMENT = 'fragment'; + + // Status Codes + const STATUS_OK = 200; + const STATUS_CREATED = 201; + const STATUS_ACCEPTED = 202; + const STATUS_NO_CONTENT = 204; + const STATUS_PARTIAL_CONTENT = 206; + const STATUS_MOVED_PERMANENTLY = 301; + + // Resource Types + const RESOURCE_TYPE_BLOB = 'b'; + const RESOURCE_TYPE_CONTAINER = 'c'; + const RESOURCE_TYPE_QUEUE = 'q'; + const RESOURCE_TYPE_TABLE = 't'; + const RESOURCE_TYPE_SHARE = 's'; + const RESOURCE_TYPE_FILE = 'f'; + + // Request Options String + const ROS_LOCATION_MODE = 'location_mode'; + const ROS_SECONDARY_URI = 'secondary_uri'; + const ROS_PRIMARY_URI = 'primary_uri'; + const ROS_DECODE_CONTENT = 'decode_content'; + const ROS_STREAM = 'stream'; + const ROS_HANDLER = 'requestHandler'; + + // @codingStandardsIgnoreEnd +} diff --git a/3rdparty/microsoft/azure-storage-common/src/Common/Internal/RestProxy.php b/3rdparty/microsoft/azure-storage-common/src/Common/Internal/RestProxy.php new file mode 100644 index 00000000..c00bafc9 --- /dev/null +++ b/3rdparty/microsoft/azure-storage-common/src/Common/Internal/RestProxy.php @@ -0,0 +1,131 @@ + + * @copyright 2016 Microsoft Corporation + * @license https://github.com/azure/azure-storage-php/LICENSE + * @link https://github.com/azure/azure-storage-php + */ + +namespace MicrosoftAzure\Storage\Common\Internal; + +use MicrosoftAzure\Storage\Common\Internal\IMiddleware; + +/** + * Base class for all REST proxies. + * + * @category Microsoft + * @package MicrosoftAzure\Storage\Common\Internal + * @author Azure Storage PHP SDK + * @copyright 2016 Microsoft Corporation + * @license https://github.com/azure/azure-storage-php/LICENSE + * @link https://github.com/azure/azure-storage-php + */ +class RestProxy +{ + /** + * @var array + */ + private $middlewares; + + /** + * @var Serialization\ISerializer + */ + protected $dataSerializer; + + /** + * Initializes new RestProxy object. + * + * @param Serialization\ISerializer $dataSerializer The data serializer. + */ + public function __construct(Serialization\ISerializer $dataSerializer = null) + { + $this->middlewares = array(); + $this->dataSerializer = $dataSerializer; + //For logging the request and responses. + // $this->middlewares[] = new HistoryMiddleware('.\\messages.log'); + } + + /** + * Gets middlewares that will be handling the request and response. + * + * @return array + */ + public function getMiddlewares() + { + return $this->middlewares; + } + + /** + * Push a new middleware into the middlewares array. The newly added + * middleware will be the most inner middleware when executed. + * + * @param callable|IMiddleware $middleware the middleware to be added. + * + * @return void + */ + public function pushMiddleware($middleware) + { + $this->middlewares[] = $middleware; + } + + /** + * Adds optional query parameter. + * + * Doesn't add the value if it satisfies empty(). + * + * @param array &$queryParameters The query parameters. + * @param string $key The query variable name. + * @param string $value The query variable value. + * + * @return void + */ + protected function addOptionalQueryParam(array &$queryParameters, $key, $value) + { + Validate::isArray($queryParameters, 'queryParameters'); + Validate::canCastAsString($key, 'key'); + Validate::canCastAsString($value, 'value'); + + if (!is_null($value) && Resources::EMPTY_STRING !== $value) { + $queryParameters[$key] = $value; + } + } + + /** + * Adds optional header. + * + * Doesn't add the value if it satisfies empty(). + * + * @param array &$headers The HTTP header parameters. + * @param string $key The HTTP header name. + * @param string $value The HTTP header value. + * + * @return void + */ + protected function addOptionalHeader(array &$headers, $key, $value) + { + Validate::isArray($headers, 'headers'); + Validate::canCastAsString($key, 'key'); + Validate::canCastAsString($value, 'value'); + + if (!is_null($value) && Resources::EMPTY_STRING !== $value) { + $headers[$key] = $value; + } + } +} diff --git a/3rdparty/microsoft/azure-storage-common/src/Common/Internal/Serialization/ISerializer.php b/3rdparty/microsoft/azure-storage-common/src/Common/Internal/Serialization/ISerializer.php new file mode 100644 index 00000000..0e3a2ebe --- /dev/null +++ b/3rdparty/microsoft/azure-storage-common/src/Common/Internal/Serialization/ISerializer.php @@ -0,0 +1,70 @@ + + * @copyright 2016 Microsoft Corporation + * @license https://github.com/azure/azure-storage-php/LICENSE + * @link https://github.com/azure/azure-storage-php + */ + +namespace MicrosoftAzure\Storage\Common\Internal\Serialization; + +/** + * The serialization interface. + * + * @ignore + * @category Microsoft + * @package MicrosoftAzure\Storage\Common\Internal\Serialization + * @author Azure Storage PHP SDK + * @copyright 2016 Microsoft Corporation + * @license https://github.com/azure/azure-storage-php/LICENSE + * @link https://github.com/azure/azure-storage-php + */ +interface ISerializer +{ + /** + * Serialize an object into a XML. + * + * @param Object $targetObject The target object to be serialized. + * @param string $rootName The name of the root. + * + * @return string + */ + public static function objectSerialize($targetObject, $rootName); + + /** + * Serializes given array. The array indices must be string to use them as + * as element name. + * + * @param array $array The object to serialize represented in array. + * @param array $properties The used properties in the serialization process. + * + * @return string + */ + public function serialize(array $array, array $properties = null); + + + /** + * Unserializes given serialized string. + * + * @param string $serialized The serialized object in string representation. + * + * @return array + */ + public function unserialize($serialized); +} diff --git a/3rdparty/microsoft/azure-storage-common/src/Common/Internal/Serialization/JsonSerializer.php b/3rdparty/microsoft/azure-storage-common/src/Common/Internal/Serialization/JsonSerializer.php new file mode 100644 index 00000000..6083f7c7 --- /dev/null +++ b/3rdparty/microsoft/azure-storage-common/src/Common/Internal/Serialization/JsonSerializer.php @@ -0,0 +1,96 @@ + + * @copyright Microsoft Corporation + * @license https://github.com/azure/azure-storage-php/LICENSE + * @link https://github.com/azure/azure-storage-php + */ + +namespace MicrosoftAzure\Storage\Common\Internal\Serialization; + +use MicrosoftAzure\Storage\Common\Internal\Validate; + +/** + * Perform JSON serialization / deserialization + * + * @ignore + * @category Microsoft + * @package MicrosoftAzure\Storage\Common\Internal\Serialization + * @author Azure Storage PHP SDK + * @copyright Microsoft Corporation + * @license https://github.com/azure/azure-storage-php/LICENSE + * @link https://github.com/azure/azure-storage-php + */ +class JsonSerializer implements ISerializer +{ + /** + * Serialize an object with specified root element name. + * + * @param object $targetObject The target object. + * @param string $rootName The name of the root element. + * + * @return string + */ + public static function objectSerialize($targetObject, $rootName) + { + Validate::notNull($targetObject, 'targetObject'); + Validate::canCastAsString($rootName, 'rootName'); + + $contianer = new \stdClass(); + + $contianer->$rootName = $targetObject; + + return json_encode($contianer); + } + + /** + * Serializes given array. The array indices must be string to use them as + * as element name. + * + * @param array $array The object to serialize represented in array. + * @param array $properties The used properties in the serialization process. + * + * @return string + */ + public function serialize(array $array = null, array $properties = null) + { + Validate::isArray($array, 'array'); + + return json_encode($array); + } + + /** + * Unserializes given serialized string to array. + * + * @param string $serialized The serialized object in string representation. + * + * @return array + */ + public function unserialize($serialized) + { + Validate::canCastAsString($serialized, 'serialized'); + + $json = json_decode($serialized); + if ($json && !is_array($json)) { + return get_object_vars($json); + } else { + return $json; + } + } +} diff --git a/3rdparty/microsoft/azure-storage-common/src/Common/Internal/Serialization/MessageSerializer.php b/3rdparty/microsoft/azure-storage-common/src/Common/Internal/Serialization/MessageSerializer.php new file mode 100644 index 00000000..2df7d7e9 --- /dev/null +++ b/3rdparty/microsoft/azure-storage-common/src/Common/Internal/Serialization/MessageSerializer.php @@ -0,0 +1,178 @@ + + * @copyright 2017 Microsoft Corporation + * @license https://github.com/azure/azure-storage-php/LICENSE + * @link https://github.com/azure/azure-storage-php + */ + +namespace MicrosoftAzure\Storage\Common\Internal\Serialization; + +use MicrosoftAzure\Storage\Common\Internal\Validate; +use MicrosoftAzure\Storage\Common\Internal\Resources; +use GuzzleHttp\Exception\RequestException; + +/** + * Provides functionality to serialize a message to a string. + * + * @ignore + * @category Microsoft + * @package MicrosoftAzure\Storage\Common\Internal\Serialization + * @author Azure Storage PHP SDK + * @copyright 2017 Microsoft Corporation + * @license https://github.com/azure/azure-storage-php/LICENSE + * @link https://github.com/azure/azure-storage-php + */ +class MessageSerializer +{ + /** + * Serialize a message to a string. The message object must be either a type + * of \Exception, or have following methods implemented. + * getHeaders() + * getProtocolVersion() + * (getUri() && getMethod()) || (getStatusCode() && getReasonPhrase()) + * + * @param object $message The message to be serialized. + * + * @return string + */ + public static function objectSerialize($targetObject) + { + //if the object is of exception type, serialize it using the methods + //without checking the methods. + if ($targetObject instanceof RequestException) { + return self::serializeRequestException($targetObject); + } elseif ($targetObject instanceof \Exception) { + return self::serializeException($targetObject); + } + + Validate::methodExists($targetObject, 'getHeaders', 'targetObject'); + Validate::methodExists($targetObject, 'getProtocolVersion', 'targetObject'); + + // Serialize according to the implemented method. + if (method_exists($targetObject, 'getUri') && + method_exists($targetObject, 'getMethod')) { + return self::serializeRequest($targetObject); + } elseif (method_exists($targetObject, 'getStatusCode') && + method_exists($targetObject, 'getReasonPhrase')) { + return self::serializeResponse($targetObject); + } else { + throw new \InvalidArgumentException( + Resources::INVALID_MESSAGE_OBJECT_TO_SERIALIZE + ); + } + } + + /** + * Serialize the request type that implemented the following methods: + * getHeaders() + * getProtocolVersion() + * getUri() + * getMethod() + * + * @param object $request The request to be serialized. + * + * @return string + */ + private static function serializeRequest($request) + { + $headers = $request->getHeaders(); + $version = $request->getProtocolVersion(); + $uri = $request->getUri(); + $method = $request->getMethod(); + + $resultString = "Request:\n"; + $resultString .= "URI: {$uri}\nHTTP Version: {$version}\nMethod: {$method}\n"; + $resultString .= self::serializeHeaders($headers); + + return $resultString; + } + + /** + * Serialize the response type that implemented the following methods: + * getHeaders() + * getProtocolVersion() + * getStatusCode() + * getReasonPhrase() + * + * @param object $response The response to be serialized + * + * @return string + */ + private static function serializeResponse($response) + { + $headers = $response->getHeaders(); + $version = $response->getProtocolVersion(); + $status = $response->getStatusCode(); + $reason = $response->getReasonPhrase(); + + $resultString = "Response:\n"; + $resultString .= "Status Code: {$status}\nReason: {$reason}\n"; + $resultString .= "HTTP Version: {$version}\n"; + $resultString .= self::serializeHeaders($headers); + + return $resultString; + } + + /** + * Serialize the message headers. + * + * @param array $headers The headers to be serialized. + * + * @return string + */ + private static function serializeHeaders(array $headers) + { + $resultString = "Headers:\n"; + foreach ($headers as $key => $value) { + $resultString .= sprintf("%s: %s\n", $key, $value[0]); + } + + return $resultString; + } + + /** + * Serialize the request exception. + * + * @param RequestException $e the request exception to be serialized. + * + * @return string + */ + private static function serializeRequestException(RequestException $e) + { + $resultString = sprintf("Reason:\n%s\n", $e); + if ($e->hasResponse()) { + $resultString .= self::serializeResponse($e->getResponse()); + } + + return $resultString; + } + + /** + * Serialize the general exception + * + * @param \Exception $e general exception to be serialized. + * + * @return string + */ + private static function serializeException(\Exception $e) + { + return sprintf("Reason:\n%s\n", $e); + } +} diff --git a/3rdparty/microsoft/azure-storage-common/src/Common/Internal/Serialization/XmlSerializer.php b/3rdparty/microsoft/azure-storage-common/src/Common/Internal/Serialization/XmlSerializer.php new file mode 100644 index 00000000..ebd12432 --- /dev/null +++ b/3rdparty/microsoft/azure-storage-common/src/Common/Internal/Serialization/XmlSerializer.php @@ -0,0 +1,245 @@ + + * @copyright 2016 Microsoft Corporation + * @license https://github.com/azure/azure-storage-php/LICENSE + * @link https://github.com/azure/azure-storage-php + */ + +namespace MicrosoftAzure\Storage\Common\Internal\Serialization; + +use MicrosoftAzure\Storage\Common\Internal\Utilities; +use MicrosoftAzure\Storage\Common\Internal\Resources; +use MicrosoftAzure\Storage\Common\Internal\Validate; + +/** + * Short description + * + * @ignore + * @category Microsoft + * @package MicrosoftAzure\Storage\Common\Internal\Serialization + * @author Azure Storage PHP SDK + * @copyright 2016 Microsoft Corporation + * @license https://github.com/azure/azure-storage-php/LICENSE + * @link https://github.com/azure/azure-storage-php + */ +class XmlSerializer implements ISerializer +{ + const STANDALONE = 'standalone'; + const ROOT_NAME = 'rootName'; + const DEFAULT_TAG = 'defaultTag'; + + /** + * Converts a SimpleXML object to an Array recursively + * ensuring all sub-elements are arrays as well. + * + * @param string $sxml The SimpleXML object. + * @param array $arr The array into which to store results. + * + * @return array + */ + private function sxml2arr($sxml, array $arr = null) + { + foreach ((array) $sxml as $key => $value) { + if (is_object($value) || (is_array($value))) { + $arr[$key] = $this->sxml2arr($value); + } else { + $arr[$key] = $value; + } + } + + return $arr; + } + + /** + * Takes an array and produces XML based on it. + * + * @param XMLWriter $xmlw XMLWriter object that was previously instanted + * and is used for creating the XML. + * @param array $data Array to be converted to XML. + * @param string $defaultTag Default XML tag to be used if none specified. + * + * @return void + */ + private function arr2xml(\XMLWriter $xmlw, array $data, $defaultTag = null) + { + foreach ($data as $key => $value) { + if ($key === Resources::XTAG_ATTRIBUTES) { + foreach ($value as $attributeName => $attributeValue) { + $xmlw->writeAttribute($attributeName, $attributeValue); + } + } elseif (is_array($value)) { + if (!is_int($key)) { + if ($key != Resources::EMPTY_STRING) { + $xmlw->startElement($key); + } else { + $xmlw->startElement($defaultTag); + } + } + + $this->arr2xml($xmlw, $value); + + if (!is_int($key)) { + $xmlw->endElement(); + } + } else { + $xmlw->writeElement($key, $value); + } + } + } + + /** + * Gets the attributes of a specified object if get attributes + * method is exposed. + * + * @param object $targetObject The target object. + * @param array $methodArray The array of method of the target object. + * + * @return mixed + */ + private static function getInstanceAttributes($targetObject, array $methodArray) + { + foreach ($methodArray as $method) { + if ($method->name == 'getAttributes') { + $classProperty = $method->invoke($targetObject); + return $classProperty; + } + } + return null; + } + + /** + * Serialize an object with specified root element name. + * + * @param object $targetObject The target object. + * @param string $rootName The name of the root element. + * + * @return string + */ + public static function objectSerialize($targetObject, $rootName) + { + Validate::notNull($targetObject, 'targetObject'); + Validate::canCastAsString($rootName, 'rootName'); + $xmlWriter = new \XmlWriter(); + $xmlWriter->openMemory(); + $xmlWriter->setIndent(true); + $reflectionClass = new \ReflectionClass($targetObject); + $methodArray = $reflectionClass->getMethods(); + $attributes = self::getInstanceAttributes( + $targetObject, + $methodArray + ); + + $xmlWriter->startElement($rootName); + if (!is_null($attributes)) { + foreach (array_keys($attributes) as $attributeKey) { + $xmlWriter->writeAttribute( + $attributeKey, + $attributes[$attributeKey] + ); + } + } + + foreach ($methodArray as $method) { + if ((strpos($method->name, 'get') === 0) + && $method->isPublic() + && ($method->name != 'getAttributes') + ) { + $variableName = substr($method->name, 3); + $variableValue = $method->invoke($targetObject); + if (!empty($variableValue)) { + if (gettype($variableValue) === 'object') { + $xmlWriter->writeRaw( + XmlSerializer::objectSerialize( + $variableValue, + $variableName + ) + ); + } else { + $xmlWriter->writeElement($variableName, $variableValue); + } + } + } + } + $xmlWriter->endElement(); + return $xmlWriter->outputMemory(true); + } + + /** + * Serializes given array. The array indices must be string to use them as + * as element name. + * + * @param array $array The object to serialize represented in array. + * @param array $properties The used properties in the serialization process. + * + * @return string + */ + public function serialize(array $array, array $properties = null) + { + $xmlVersion = '1.0'; + $xmlEncoding = 'UTF-8'; + $standalone = Utilities::tryGetValue($properties, self::STANDALONE); + $defaultTag = Utilities::tryGetValue($properties, self::DEFAULT_TAG); + $rootName = Utilities::tryGetValue($properties, self::ROOT_NAME); + $docNamespace = Utilities::tryGetValue( + $array, + Resources::XTAG_NAMESPACE, + null + ); + + if (!is_array($array)) { + return false; + } + + $xmlw = new \XmlWriter(); + $xmlw->openMemory(); + $xmlw->setIndent(true); + $xmlw->startDocument($xmlVersion, $xmlEncoding, $standalone); + + if (is_null($docNamespace)) { + $xmlw->startElement($rootName); + } else { + foreach ($docNamespace as $uri => $prefix) { + $xmlw->startElementNS($prefix, $rootName, $uri); + break; + } + } + + unset($array[Resources::XTAG_NAMESPACE]); + self::arr2xml($xmlw, $array, $defaultTag); + + $xmlw->endElement(); + + return $xmlw->outputMemory(true); + } + + /** + * Unserializes given serialized string. + * + * @param string $serialized The serialized object in string representation. + * + * @return array + */ + public function unserialize($serialized) + { + $sxml = new \SimpleXMLElement($serialized); + + return $this->sxml2arr($sxml); + } +} diff --git a/3rdparty/microsoft/azure-storage-common/src/Common/Internal/ServiceRestProxy.php b/3rdparty/microsoft/azure-storage-common/src/Common/Internal/ServiceRestProxy.php new file mode 100644 index 00000000..15ef327e --- /dev/null +++ b/3rdparty/microsoft/azure-storage-common/src/Common/Internal/ServiceRestProxy.php @@ -0,0 +1,658 @@ + + * @copyright 2016 Microsoft Corporation + * @license https://github.com/azure/azure-storage-php/LICENSE + * @link https://github.com/azure/azure-storage-php + */ + +namespace MicrosoftAzure\Storage\Common\Internal; + +use MicrosoftAzure\Storage\Common\Exceptions\ServiceException; +use MicrosoftAzure\Storage\Common\Internal\RetryMiddlewareFactory; +use MicrosoftAzure\Storage\Common\Internal\Serialization\XmlSerializer; +use MicrosoftAzure\Storage\Common\Models\ServiceOptions; +use MicrosoftAzure\Storage\Common\Internal\Http\HttpCallContext; +use MicrosoftAzure\Storage\Common\Internal\Middlewares\MiddlewareBase; +use MicrosoftAzure\Storage\Common\Middlewares\MiddlewareStack; +use MicrosoftAzure\Storage\Common\LocationMode; +use GuzzleHttp\Promise\EachPromise; +use GuzzleHttp\Exception\RequestException; +use GuzzleHttp\Psr7\Request; +use GuzzleHttp\Psr7\Uri; +use GuzzleHttp\Client; +use GuzzleHttp\Psr7; +use Psr\Http\Message\ResponseInterface; + +/** + * Base class for all services rest proxies. + * + * @ignore + * @category Microsoft + * @package MicrosoftAzure\Storage\Common\Internal + * @author Azure Storage PHP SDK + * @copyright 2016 Microsoft Corporation + * @license https://github.com/azure/azure-storage-php/LICENSE + * @link https://github.com/azure/azure-storage-php + */ +class ServiceRestProxy extends RestProxy +{ + private $accountName; + private $psrPrimaryUri; + private $psrSecondaryUri; + private $options; + private $client; + + /** + * Initializes new ServiceRestProxy object. + * + * @param string $primaryUri The storage account + * primary uri. + * @param string $secondaryUri The storage account + * secondary uri. + * @param string $accountName The name of the account. + * @param array $options Array of options for + * the service + */ + public function __construct( + $primaryUri, + $secondaryUri, + $accountName, + array $options = [] + ) { + $primaryUri = Utilities::appendDelimiter($primaryUri, '/'); + $secondaryUri = Utilities::appendDelimiter($secondaryUri, '/'); + + $dataSerializer = new XmlSerializer(); + parent::__construct($dataSerializer); + + $this->accountName = $accountName; + $this->psrPrimaryUri = new Uri($primaryUri); + $this->psrSecondaryUri = new Uri($secondaryUri); + $this->options = array_merge(array('http' => array()), $options); + $this->client = self::createClient($this->options['http']); + } + + /** + * Create a Guzzle client for future usage. + * + * @param array $options Optional parameters for the client. + * + * @return Client + */ + private static function createClient(array $options) + { + $verify = true; + //Disable SSL if proxy has been set, and set the proxy in the client. + $proxy = getenv('HTTP_PROXY'); + // For testing with Fiddler + // $proxy = 'localhost:8888'; + // $verify = false; + if (!empty($proxy)) { + $options['proxy'] = $proxy; + } + + if (isset($options['verify'])) { + $verify = $options['verify']; + } + + return (new \GuzzleHttp\Client( + array_merge( + $options, + array( + "defaults" => array( + "allow_redirects" => true, + "exceptions" => true, + "decode_content" => true, + "config" => [ + "curl" => [ + CURLOPT_SSLVERSION => CURL_SSLVERSION_TLSv1_2 + ] + ] + ), + 'cookies' => true, + 'verify' => $verify, + ) + ) + )); + } + + /** + * Gets the account name. + * + * @return string + */ + public function getAccountName() + { + return $this->accountName; + } + + /** + * Create a middleware stack with given middleware. + * + * @param ServiceOptions $serviceOptions The options user passed in. + * + * @return MiddlewareStack + */ + protected function createMiddlewareStack(ServiceOptions $serviceOptions) + { + //If handler stack is not defined by the user, create a default + //middleware stack. + $stack = null; + if (array_key_exists('stack', $this->options['http'])) { + $stack = $this->options['http']['stack']; + } elseif ($serviceOptions->getMiddlewareStack() != null) { + $stack = $serviceOptions->getMiddlewareStack(); + } else { + $stack = new MiddlewareStack(); + } + + //Push all the middlewares specified in the $serviceOptions to the + //handlerstack. + if ($serviceOptions->getMiddlewares() != array()) { + foreach ($serviceOptions->getMiddlewares() as $middleware) { + $stack->push($middleware); + } + } + + //Push all the middlewares specified in the $options to the + //handlerstack. + if (array_key_exists('middlewares', $this->options)) { + foreach ($this->options['middlewares'] as $middleware) { + $stack->push($middleware); + } + } + + //Push all the middlewares specified in $this->middlewares to the + //handlerstack. + foreach ($this->getMiddlewares() as $middleware) { + $stack->push($middleware); + } + + return $stack; + } + + /** + * Send the requests concurrently. Number of concurrency can be modified + * by inserting a new key/value pair with the key 'number_of_concurrency' + * into the $requestOptions of $serviceOptions. Return only the promise. + * + * @param callable $generator the generator function to generate + * request upon fulfillment + * @param int $statusCode The expected status code for each of the + * request generated by generator. + * @param ServiceOptions $options The service options for the concurrent + * requests. + * + * @return \GuzzleHttp\Promise\Promise|\GuzzleHttp\Promise\PromiseInterface + */ + protected function sendConcurrentAsync( + callable $generator, + $statusCode, + ServiceOptions $options + ) { + $client = $this->client; + $middlewareStack = $this->createMiddlewareStack($options); + + $sendAsync = function ($request, $options) use ($client) { + if ($request->getMethod() == 'HEAD') { + $options['decode_content'] = false; + } + return $client->sendAsync($request, $options); + }; + + $handler = $middlewareStack->apply($sendAsync); + + $requestOptions = $this->generateRequestOptions($options, $handler); + + $promises = \call_user_func( + function () use ( + $generator, + $handler, + $requestOptions + ) { + while (is_callable($generator) && ($request = $generator())) { + yield \call_user_func($handler, $request, $requestOptions); + } + } + ); + + $eachPromise = new EachPromise($promises, [ + 'concurrency' => $options->getNumberOfConcurrency(), + 'fulfilled' => function ($response, $index) use ($statusCode) { + //the promise is fulfilled, evaluate the response + self::throwIfError( + $response, + $statusCode + ); + }, + 'rejected' => function ($reason, $index) { + //Still rejected even if the retry logic has been applied. + //Throwing exception. + throw $reason; + } + ]); + + return $eachPromise->promise(); + } + + + /** + * Create the request to be sent. + * + * @param string $method The method of the HTTP request + * @param array $headers The header field of the request + * @param array $queryParams The query parameter of the request + * @param array $postParameters The HTTP POST parameters + * @param string $path URL path + * @param string $body Request body + * + * @return \GuzzleHttp\Psr7\Request + */ + protected function createRequest( + $method, + array $headers, + array $queryParams, + array $postParameters, + $path, + $locationMode, + $body = Resources::EMPTY_STRING + ) { + if ($locationMode == LocationMode::SECONDARY_ONLY || + $locationMode == LocationMode::SECONDARY_THEN_PRIMARY) { + $uri = $this->psrSecondaryUri; + } else { + $uri = $this->psrPrimaryUri; + } + + //Append the path, not replacing it. + if ($path != null) { + $exPath = $uri->getPath(); + if ($exPath != '') { + //Remove the duplicated slash in the path. + if ($path != '' && $path[0] == '/') { + $path = $exPath . substr($path, 1); + } else { + $path = $exPath . $path; + } + } + $uri = $uri->withPath($path); + } + + // add query parameters into headers + if ($queryParams != null) { + $queryString = Psr7\Query::build($queryParams); + $uri = $uri->withQuery($queryString); + } + + // add post parameters into bodies + $actualBody = null; + if (empty($body)) { + if (empty($headers[Resources::CONTENT_TYPE])) { + $headers[Resources::CONTENT_TYPE] = Resources::URL_ENCODED_CONTENT_TYPE; + $actualBody = Psr7\Query::build($postParameters); + } + } else { + $actualBody = $body; + } + + $request = new Request( + $method, + $uri, + $headers, + $actualBody + ); + + //add content-length to header + $bodySize = $request->getBody()->getSize(); + if ($bodySize > 0) { + $request = $request->withHeader('content-length', $bodySize); + } + return $request; + } + + /** + * Create promise of sending HTTP request with the specified parameters. + * + * @param string $method HTTP method used in the request + * @param array $headers HTTP headers. + * @param array $queryParams URL query parameters. + * @param array $postParameters The HTTP POST parameters. + * @param string $path URL path + * @param array|int $expected Expected Status Codes. + * @param string $body Request body + * @param ServiceOptions $serviceOptions Service options + * + * @return \GuzzleHttp\Promise\PromiseInterface + */ + protected function sendAsync( + $method, + array $headers, + array $queryParams, + array $postParameters, + $path, + $expected = Resources::STATUS_OK, + $body = Resources::EMPTY_STRING, + ServiceOptions $serviceOptions = null + ) { + if ($serviceOptions == null) { + $serviceOptions = new ServiceOptions(); + } + $this->addOptionalQueryParam( + $queryParams, + Resources::QP_TIMEOUT, + $serviceOptions->getTimeout() + ); + + $request = $this->createRequest( + $method, + $headers, + $queryParams, + $postParameters, + $path, + $serviceOptions->getLocationMode(), + $body + ); + + $client = $this->client; + + $middlewareStack = $this->createMiddlewareStack($serviceOptions); + + $sendAsync = function ($request, $options) use ($client) { + return $client->sendAsync($request, $options); + }; + + $handler = $middlewareStack->apply($sendAsync); + + $requestOptions = + $this->generateRequestOptions($serviceOptions, $handler); + + if ($request->getMethod() == 'HEAD') { + $requestOptions[Resources::ROS_DECODE_CONTENT] = false; + } + + $promise = \call_user_func($handler, $request, $requestOptions); + + return $promise->then( + function ($response) use ($expected, $requestOptions) { + self::throwIfError( + $response, + $expected + ); + + return self::addLocationHeaderToResponse( + $response, + $requestOptions[Resources::ROS_LOCATION_MODE] + ); + }, + function ($reason) use ($expected) { + return $this->onRejected($reason, $expected); + } + ); + } + + /** + * @param string|\Exception $reason Rejection reason. + * @param array|int $expected Expected Status Codes. + * + * @return ResponseInterface + */ + protected function onRejected($reason, $expected) + { + if (!($reason instanceof \Exception)) { + throw new \RuntimeException($reason); + } + if (!($reason instanceof RequestException)) { + throw $reason; + } + $response = $reason->getResponse(); + if ($response != null) { + self::throwIfError( + $response, + $expected + ); + } else { + //if could not get response but promise rejected, throw reason. + throw $reason; + } + return $response; + } + + /** + * Generate the request options using the given service options and stored + * information. + * + * @param ServiceOptions $serviceOptions The service options used to + * generate request options. + * @param callable $handler The handler used to send the + * request. + * @return array + */ + protected function generateRequestOptions( + ServiceOptions $serviceOptions, + callable $handler + ) { + $result = array(); + $result[Resources::ROS_LOCATION_MODE] = $serviceOptions->getLocationMode(); + $result[Resources::ROS_STREAM] = $serviceOptions->getIsStreaming(); + $result[Resources::ROS_DECODE_CONTENT] = $serviceOptions->getDecodeContent(); + $result[Resources::ROS_HANDLER] = $handler; + $result[Resources::ROS_SECONDARY_URI] = $this->getPsrSecondaryUri(); + $result[Resources::ROS_PRIMARY_URI] = $this->getPsrPrimaryUri(); + + return $result; + } + + /** + * Sends the context. + * + * @param HttpCallContext $context The context of the request. + * @return \GuzzleHttp\Psr7\Response + */ + protected function sendContext(HttpCallContext $context) + { + return $this->sendContextAsync($context)->wait(); + } + + /** + * Creates the promise to send the context. + * + * @param HttpCallContext $context The context of the request. + * + * @return \GuzzleHttp\Promise\PromiseInterface + */ + protected function sendContextAsync(HttpCallContext $context) + { + return $this->sendAsync( + $context->getMethod(), + $context->getHeaders(), + $context->getQueryParameters(), + $context->getPostParameters(), + $context->getPath(), + $context->getStatusCodes(), + $context->getBody(), + $context->getServiceOptions() + ); + } + + /** + * Throws ServiceException if the received status code is not expected. + * + * @param ResponseInterface $response The response received + * @param array|int $expected The expected status codes. + * + * @return void + * + * @throws ServiceException + */ + public static function throwIfError(ResponseInterface $response, $expected) + { + $expectedStatusCodes = is_array($expected) ? $expected : array($expected); + + if (!in_array($response->getStatusCode(), $expectedStatusCodes)) { + throw new ServiceException($response); + } + } + + /** + * Adds HTTP POST parameter to the specified + * + * @param array $postParameters An array of HTTP POST parameters. + * @param string $key The key of a HTTP POST parameter. + * @param string $value the value of a HTTP POST parameter. + * + * @return array + */ + public function addPostParameter( + array $postParameters, + $key, + $value + ) { + Validate::isArray($postParameters, 'postParameters'); + $postParameters[$key] = $value; + return $postParameters; + } + + /** + * Groups set of values into one value separated with Resources::SEPARATOR + * + * @param array $values array of values to be grouped. + * + * @return string + */ + public static function groupQueryValues(array $values) + { + Validate::isArray($values, 'values'); + $joined = Resources::EMPTY_STRING; + + sort($values); + + foreach ($values as $value) { + if (!is_null($value) && !empty($value)) { + $joined .= $value . Resources::SEPARATOR; + } + } + + return trim($joined, Resources::SEPARATOR); + } + + /** + * Adds metadata elements to headers array + * + * @param array $headers HTTP request headers + * @param array $metadata user specified metadata + * + * @return array + */ + protected function addMetadataHeaders(array $headers, array $metadata = null) + { + Utilities::validateMetadata($metadata); + + $metadata = $this->generateMetadataHeaders($metadata); + $headers = array_merge($headers, $metadata); + + return $headers; + } + + /** + * Generates metadata headers by prefixing each element with 'x-ms-meta'. + * + * @param array $metadata user defined metadata. + * + * @return array + */ + public function generateMetadataHeaders(array $metadata = null) + { + $metadataHeaders = array(); + + if (is_array($metadata) && !is_null($metadata)) { + foreach ($metadata as $key => $value) { + $headerName = Resources::X_MS_META_HEADER_PREFIX; + if (strpos($value, "\r") !== false + || strpos($value, "\n") !== false + ) { + throw new \InvalidArgumentException(Resources::INVALID_META_MSG); + } + + // Metadata name is case-presrved and case insensitive + $headerName .= $key; + $metadataHeaders[$headerName] = $value; + } + } + + return $metadataHeaders; + } + + /** + * Get the primary URI in PSR form. + * + * @return Uri + */ + public function getPsrPrimaryUri() + { + return $this->psrPrimaryUri; + } + + /** + * Get the secondary URI in PSR form. + * + * @return Uri + */ + public function getPsrSecondaryUri() + { + return $this->psrSecondaryUri; + } + + /** + * Adds the header that indicates the location mode to the response header. + * + * @return ResponseInterface + */ + private static function addLocationHeaderToResponse( + ResponseInterface $response, + $locationMode + ) { + //If the response already has this header, return itself. + if ($response->hasHeader(Resources::X_MS_CONTINUATION_LOCATION_MODE)) { + return $response; + } + //Otherwise, add the header that indicates the endpoint to be used if + //continuation token is used for subsequent request. Notice that if the + //response does not have location header set at the moment, it means + //that the user have not set a retry middleware. + if ($locationMode == LocationMode::PRIMARY_THEN_SECONDARY) { + $response = $response->withHeader( + Resources::X_MS_CONTINUATION_LOCATION_MODE, + LocationMode::PRIMARY_ONLY + ); + } elseif ($locationMode == LocationMode::SECONDARY_THEN_PRIMARY) { + $response = $response->withHeader( + Resources::X_MS_CONTINUATION_LOCATION_MODE, + LocationMode::SECONDARY_ONLY + ); + } elseif ($locationMode == LocationMode::SECONDARY_ONLY || + $locationMode == LocationMode::PRIMARY_ONLY) { + $response = $response->withHeader( + Resources::X_MS_CONTINUATION_LOCATION_MODE, + $locationMode + ); + } + return $response; + } +} diff --git a/3rdparty/microsoft/azure-storage-common/src/Common/Internal/ServiceRestTrait.php b/3rdparty/microsoft/azure-storage-common/src/Common/Internal/ServiceRestTrait.php new file mode 100644 index 00000000..cc4f5071 --- /dev/null +++ b/3rdparty/microsoft/azure-storage-common/src/Common/Internal/ServiceRestTrait.php @@ -0,0 +1,259 @@ + + * @copyright 2017 Microsoft Corporation + * @license https://github.com/azure/azure-storage-php/LICENSE + * @link https://github.com/azure/azure-storage-php + */ + +namespace MicrosoftAzure\Storage\Common\Internal; + +use MicrosoftAzure\Storage\Common\LocationMode; +use MicrosoftAzure\Storage\Common\Models\ServiceOptions; +use MicrosoftAzure\Storage\Common\Models\ServiceProperties; +use MicrosoftAzure\Storage\Common\Models\GetServicePropertiesResult; +use MicrosoftAzure\Storage\Common\Models\GetServiceStatsResult; + +/** + * Trait implementing common REST API for all the services, including the + * following: + * Get/Set Service Properties + * Get service stats + * + * @category Microsoft + * @package MicrosoftAzure\Storage\Common\Internal + * @author Azure Storage PHP SDK + * @copyright 2017 Microsoft Corporation + * @license https://github.com/azure/azure-storage-php/LICENSE + * @link https://github.com/azure/azure-storage-php + */ +trait ServiceRestTrait +{ + /** + * Gets the properties of the service. + * + * @param ServiceOptions $options The optional parameters. + * + * @return \MicrosoftAzure\Storage\Common\Models\GetServicePropertiesResult + * + * @see http://msdn.microsoft.com/en-us/library/windowsazure/hh452239.aspx + */ + public function getServiceProperties( + ServiceOptions $options = null + ) { + return $this->getServicePropertiesAsync($options)->wait(); + } + + /** + * Creates promise to get the properties of the service. + * + * @param ServiceOptions $options The optional parameters. + * + * @return \GuzzleHttp\Promise\PromiseInterface + * + * @see http://msdn.microsoft.com/en-us/library/windowsazure/hh452239.aspx + */ + public function getServicePropertiesAsync( + ServiceOptions $options = null + ) { + $method = Resources::HTTP_GET; + $headers = array(); + $queryParams = array(); + $postParams = array(); + $path = Resources::EMPTY_STRING; + + if (is_null($options)) { + $options = new ServiceOptions(); + } + + $this->addOptionalQueryParam( + $queryParams, + Resources::QP_REST_TYPE, + 'service' + ); + $this->addOptionalQueryParam( + $queryParams, + Resources::QP_COMP, + 'properties' + ); + + $dataSerializer = $this->dataSerializer; + + return $this->sendAsync( + $method, + $headers, + $queryParams, + $postParams, + $path, + Resources::STATUS_OK, + Resources::EMPTY_STRING, + $options + )->then(function ($response) use ($dataSerializer) { + $parsed = $dataSerializer->unserialize($response->getBody()); + return GetServicePropertiesResult::create($parsed); + }, null); + } + + /** + * Sets the properties of the service. + * + * It's recommended to use getServiceProperties, alter the returned object and + * then use setServiceProperties with this altered object. + * + * @param ServiceProperties $serviceProperties The service properties. + * @param ServiceOptions $options The optional parameters. + * + * @return void + * + * @see http://msdn.microsoft.com/en-us/library/windowsazure/hh452235.aspx + */ + public function setServiceProperties( + ServiceProperties $serviceProperties, + ServiceOptions $options = null + ) { + $this->setServicePropertiesAsync($serviceProperties, $options)->wait(); + } + + /** + * Creates the promise to set the properties of the service. + * + * It's recommended to use getServiceProperties, alter the returned object and + * then use setServiceProperties with this altered object. + * + * @param ServiceProperties $serviceProperties The service properties. + * @param ServiceOptions $options The optional parameters. + * + * @return \GuzzleHttp\Promise\PromiseInterface + * + * @see http://msdn.microsoft.com/en-us/library/windowsazure/hh452235.aspx + */ + public function setServicePropertiesAsync( + ServiceProperties $serviceProperties, + ServiceOptions $options = null + ) { + Validate::isTrue( + $serviceProperties instanceof ServiceProperties, + Resources::INVALID_SVC_PROP_MSG + ); + + $method = Resources::HTTP_PUT; + $headers = array(); + $queryParams = array(); + $postParams = array(); + $path = Resources::EMPTY_STRING; + $body = $serviceProperties->toXml($this->dataSerializer); + + if (is_null($options)) { + $options = new ServiceOptions(); + } + + $this->addOptionalQueryParam( + $queryParams, + Resources::QP_REST_TYPE, + 'service' + ); + $this->addOptionalQueryParam( + $queryParams, + Resources::QP_COMP, + 'properties' + ); + $this->addOptionalHeader( + $headers, + Resources::CONTENT_TYPE, + Resources::URL_ENCODED_CONTENT_TYPE + ); + + $options->setLocationMode(LocationMode::PRIMARY_ONLY); + + return $this->sendAsync( + $method, + $headers, + $queryParams, + $postParams, + $path, + Resources::STATUS_ACCEPTED, + $body, + $options + ); + } + + /** + * Retrieves statistics related to replication for the service. The operation + * will only be sent to secondary location endpoint. + * + * @param ServiceOptions|null $options The options this operation sends with. + * + * @return GetServiceStatsResult + */ + public function getServiceStats(ServiceOptions $options = null) + { + return $this->getServiceStatsAsync($options)->wait(); + } + + /** + * Creates promise that retrieves statistics related to replication for the + * service. The operation will only be sent to secondary location endpoint. + * + * @param ServiceOptions|null $options The options this operation sends with. + * + * @return \GuzzleHttp\Promise\PromiseInterface + */ + public function getServiceStatsAsync(ServiceOptions $options = null) + { + $method = Resources::HTTP_GET; + $headers = array(); + $queryParams = array(); + $postParams = array(); + $path = Resources::EMPTY_STRING; + + if (is_null($options)) { + $options = new ServiceOptions(); + } + + $this->addOptionalQueryParam( + $queryParams, + Resources::QP_REST_TYPE, + 'service' + ); + $this->addOptionalQueryParam( + $queryParams, + Resources::QP_COMP, + 'stats' + ); + + $dataSerializer = $this->dataSerializer; + + $options->setLocationMode(LocationMode::SECONDARY_ONLY); + + return $this->sendAsync( + $method, + $headers, + $queryParams, + $postParams, + $path, + Resources::STATUS_OK, + Resources::EMPTY_STRING, + $options + )->then(function ($response) use ($dataSerializer) { + $parsed = $dataSerializer->unserialize($response->getBody()); + return GetServiceStatsResult::create($parsed); + }, null); + } +} diff --git a/3rdparty/microsoft/azure-storage-common/src/Common/Internal/ServiceSettings.php b/3rdparty/microsoft/azure-storage-common/src/Common/Internal/ServiceSettings.php new file mode 100644 index 00000000..d4551a4e --- /dev/null +++ b/3rdparty/microsoft/azure-storage-common/src/Common/Internal/ServiceSettings.php @@ -0,0 +1,288 @@ + + * @copyright 2016 Microsoft Corporation + * @license https://github.com/azure/azure-storage-php/LICENSE + * @link https://github.com/azure/azure-storage-php + */ + +namespace MicrosoftAzure\Storage\Common\Internal; + +/** + * Base class for all REST services settings. + * + * Derived classes must implement the following members: + * 1- $isInitialized: A static property that indicates whether the class's static + * members have been initialized. + * 2- init(): A protected static method that initializes static members. + * 3- $validSettingKeys: A static property that contains valid setting keys for this + * service. + * 4- createFromConnectionString($connectionString): A public static function that + * takes a connection string and returns the created settings object. + * + * @category Microsoft + * @package MicrosoftAzure\Storage\Common\Internal + * @author Azure Storage PHP SDK + * @copyright 2016 Microsoft Corporation + * @license https://github.com/azure/azure-storage-php/LICENSE + * @link https://github.com/azure/azure-storage-php + */ +abstract class ServiceSettings +{ + /** + * Throws an exception if the connection string format does not match any of the + * available formats. + * + * @param string $connectionString The invalid formatted connection string. + * + * @return void + * + * @throws \RuntimeException + */ + protected static function noMatch($connectionString) + { + throw new \RuntimeException( + sprintf(Resources::MISSING_CONNECTION_STRING_SETTINGS, $connectionString) + ); + } + + /** + * Parses the connection string and then validate that the parsed keys belong to + * the $validSettingKeys + * + * @param string $connectionString The user provided connection string. + * + * @return array The tokenized connection string keys. + * + * @throws \RuntimeException + */ + protected static function parseAndValidateKeys($connectionString) + { + // Initialize the static values if they are not initialized yet. + if (!static::$isInitialized) { + static::init(); + static::$isInitialized = true; + } + + $tokenizedSettings = ConnectionStringParser::parseConnectionString( + 'connectionString', + $connectionString + ); + + // Assure that all given keys are valid. + foreach ($tokenizedSettings as $key => $value) { + if (!Utilities::inArrayInsensitive($key, static::$validSettingKeys)) { + throw new \RuntimeException( + sprintf( + Resources::INVALID_CONNECTION_STRING_SETTING_KEY, + $key, + implode("\n", static::$validSettingKeys) + ) + ); + } + } + + return $tokenizedSettings; + } + + /** + * Creates an anonymous function that acts as predicate. + * + * @param array $requirements The array of conditions to satisfy. + * @param boolean $isRequired Either these conditions are all required or all + * optional. + * @param boolean $atLeastOne Indicates that at least one requirement must + * succeed. + * + * @return callable + */ + protected static function getValidator( + array $requirements, + $isRequired, + $atLeastOne + ) { + // @codingStandardsIgnoreStart + + return function ($userSettings) use ($requirements, $isRequired, $atLeastOne) { + $oneFound = false; + $result = array_change_key_case($userSettings); + foreach ($requirements as $requirement) { + $settingName = strtolower($requirement[Resources::SETTING_NAME]); + + // Check if the setting name exists in the provided user settings. + if (array_key_exists($settingName, $result)) { + // Check if the provided user setting value is valid. + $validationFunc = $requirement[Resources::SETTING_CONSTRAINT]; + $isValid = $validationFunc($result[$settingName]); + + if ($isValid) { + // Remove the setting as indicator for successful validation. + unset($result[$settingName]); + $oneFound = true; + } + } else { + // If required then fail because the setting does not exist + if ($isRequired) { + return null; + } + } + } + + if ($atLeastOne) { + // At least one requirement must succeed, otherwise fail. + return $oneFound ? $result : null; + } else { + return $result; + } + }; + + // @codingStandardsIgnoreEnd + } + + /** + * Creates at lease one succeed predicate for the provided list of requirements. + * + * @return callable + */ + protected static function atLeastOne() + { + $allSettings = func_get_args(); + return self::getValidator($allSettings, false, true); + } + + /** + * Creates an optional predicate for the provided list of requirements. + * + * @return callable + */ + protected static function optional() + { + $optionalSettings = func_get_args(); + return self::getValidator($optionalSettings, false, false); + } + + /** + * Creates an required predicate for the provided list of requirements. + * + * @return callable + */ + protected static function allRequired() + { + $requiredSettings = func_get_args(); + return self::getValidator($requiredSettings, true, false); + } + + /** + * Creates a setting value condition using the passed predicate. + * + * @param string $name The setting key name. + * @param callable $predicate The setting value predicate. + * + * @return array + */ + protected static function settingWithFunc($name, $predicate) + { + $requirement = array(); + $requirement[Resources::SETTING_NAME] = $name; + $requirement[Resources::SETTING_CONSTRAINT] = $predicate; + + return $requirement; + } + + /** + * Creates a setting value condition that validates it is one of the + * passed valid values. + * + * @param string $name The setting key name. + * + * @return array + */ + protected static function setting($name) + { + $validValues = func_get_args(); + + // Remove $name argument. + unset($validValues[0]); + + $validValuesCount = func_num_args(); + + $predicate = function ($settingValue) use ($validValuesCount, $validValues) { + if (empty($validValues)) { + // No restrictions, succeed, + return true; + } + + // Check to find if the $settingValue is valid or not. The index must + // start from 1 as unset deletes the value but does not update the array + // indecies. + for ($index = 1; $index < $validValuesCount; $index++) { + if ($settingValue == $validValues[$index]) { + // $settingValue is found in valid values set, succeed. + return true; + } + } + + throw new \RuntimeException( + sprintf( + Resources::INVALID_CONFIG_VALUE, + $settingValue, + implode("\n", $validValues) + ) + ); + + // $settingValue is missing in valid values set, fail. + return false; + }; + + return self::settingWithFunc($name, $predicate); + } + + /** + * Tests to see if a given list of settings matches a set of filters exactly. + * + * @param array $settings The settings to check. + * + * @return boolean If any filter returns null, false. If there are any settings + * left over after all filters are processed, false. Otherwise true. + */ + protected static function matchedSpecification(array $settings) + { + $constraints = func_get_args(); + + // Remove first element which corresponds to $settings + unset($constraints[0]); + + foreach ($constraints as $constraint) { + $remainingSettings = $constraint($settings); + + if (is_null($remainingSettings)) { + return false; + } else { + $settings = $remainingSettings; + } + } + + if (empty($settings)) { + return true; + } + + return false; + } +} diff --git a/3rdparty/microsoft/azure-storage-common/src/Common/Internal/StorageServiceSettings.php b/3rdparty/microsoft/azure-storage-common/src/Common/Internal/StorageServiceSettings.php new file mode 100644 index 00000000..57c95bc2 --- /dev/null +++ b/3rdparty/microsoft/azure-storage-common/src/Common/Internal/StorageServiceSettings.php @@ -0,0 +1,713 @@ + + * @copyright 2016 Microsoft Corporation + * @license https://github.com/azure/azure-storage-php/LICENSE + * @link https://github.com/azure/azure-storage-php + */ + +namespace MicrosoftAzure\Storage\Common\Internal; + +/** + * Represents the settings used to sign and access a request against the storage + * service. For more information about storage service connection strings check this + * page: http://msdn.microsoft.com/en-us/library/ee758697 + * + * @ignore + * @category Microsoft + * @package MicrosoftAzure\Storage\Common\Internal + * @author Azure Storage PHP SDK + * @copyright 2016 Microsoft Corporation + * @license https://github.com/azure/azure-storage-php/LICENSE + * @link https://github.com/azure/azure-storage-php + */ +class StorageServiceSettings extends ServiceSettings +{ + private $name; + private $key; + private $sas; + private $blobEndpointUri; + private $queueEndpointUri; + private $tableEndpointUri; + private $fileEndpointUri; + private $blobSecondaryEndpointUri; + private $queueSecondaryEndpointUri; + private $tableSecondaryEndpointUri; + private $fileSecondaryEndpointUri; + + private static $devStoreAccount; + private static $useDevelopmentStorageSetting; + private static $developmentStorageProxyUriSetting; + private static $defaultEndpointsProtocolSetting; + private static $accountNameSetting; + private static $accountKeySetting; + private static $sasTokenSetting; + private static $blobEndpointSetting; + private static $queueEndpointSetting; + private static $tableEndpointSetting; + private static $fileEndpointSetting; + private static $endpointSuffixSetting; + + /** + * If initialized or not + * @internal + */ + protected static $isInitialized = false; + + /** + * Valid setting keys + * @internal + */ + protected static $validSettingKeys = array(); + + /** + * Initializes static members of the class. + * + * @return void + */ + protected static function init() + { + self::$useDevelopmentStorageSetting = self::setting( + Resources::USE_DEVELOPMENT_STORAGE_NAME, + 'true' + ); + + self::$developmentStorageProxyUriSetting = self::settingWithFunc( + Resources::DEVELOPMENT_STORAGE_PROXY_URI_NAME, + Validate::getIsValidUri() + ); + + self::$defaultEndpointsProtocolSetting = self::setting( + Resources::DEFAULT_ENDPOINTS_PROTOCOL_NAME, + 'http', + 'https' + ); + + self::$accountNameSetting = self::setting(Resources::ACCOUNT_NAME_NAME); + + self::$accountKeySetting = self::settingWithFunc( + Resources::ACCOUNT_KEY_NAME, + // base64_decode will return false if the $key is not in base64 format. + function ($key) { + $isValidBase64String = base64_decode($key, true); + if ($isValidBase64String) { + return true; + } else { + throw new \RuntimeException( + sprintf(Resources::INVALID_ACCOUNT_KEY_FORMAT, $key) + ); + } + } + ); + + self::$sasTokenSetting = self::setting(Resources::SAS_TOKEN_NAME); + + self::$blobEndpointSetting = self::settingWithFunc( + Resources::BLOB_ENDPOINT_NAME, + Validate::getIsValidUri() + ); + + self::$queueEndpointSetting = self::settingWithFunc( + Resources::QUEUE_ENDPOINT_NAME, + Validate::getIsValidUri() + ); + + self::$tableEndpointSetting = self::settingWithFunc( + Resources::TABLE_ENDPOINT_NAME, + Validate::getIsValidUri() + ); + + self::$fileEndpointSetting = self::settingWithFunc( + Resources::FILE_ENDPOINT_NAME, + Validate::getIsValidUri() + ); + + self::$endpointSuffixSetting = self::settingWithFunc( + Resources::ENDPOINT_SUFFIX_NAME, + Validate::getIsValidHostname() + ); + + self::$validSettingKeys[] = Resources::USE_DEVELOPMENT_STORAGE_NAME; + self::$validSettingKeys[] = Resources::DEVELOPMENT_STORAGE_PROXY_URI_NAME; + self::$validSettingKeys[] = Resources::DEFAULT_ENDPOINTS_PROTOCOL_NAME; + self::$validSettingKeys[] = Resources::ACCOUNT_NAME_NAME; + self::$validSettingKeys[] = Resources::ACCOUNT_KEY_NAME; + self::$validSettingKeys[] = Resources::SAS_TOKEN_NAME; + self::$validSettingKeys[] = Resources::BLOB_ENDPOINT_NAME; + self::$validSettingKeys[] = Resources::QUEUE_ENDPOINT_NAME; + self::$validSettingKeys[] = Resources::TABLE_ENDPOINT_NAME; + self::$validSettingKeys[] = Resources::FILE_ENDPOINT_NAME; + self::$validSettingKeys[] = Resources::ENDPOINT_SUFFIX_NAME; + } + + /** + * Creates new storage service settings instance. + * + * @param string $name The storage service name. + * @param string $key The storage service key. + * @param string $blobEndpointUri The storage service blob + * endpoint. + * @param string $queueEndpointUri The storage service queue + * endpoint. + * @param string $tableEndpointUri The storage service table + * endpoint. + * @param string $fileEndpointUri The storage service file + * endpoint. + * @param string $blobSecondaryEndpointUri The storage service secondary + * blob endpoint. + * @param string $queueSecondaryEndpointUri The storage service secondary + * queue endpoint. + * @param string $tableSecondaryEndpointUri The storage service secondary + * table endpoint. + * @param string $fileSecondaryEndpointUri The storage service secondary + * file endpoint. + * @param string $sas The storage service SAS token. + */ + public function __construct( + $name, + $key, + $blobEndpointUri, + $queueEndpointUri, + $tableEndpointUri, + $fileEndpointUri, + $blobSecondaryEndpointUri = null, + $queueSecondaryEndpointUri = null, + $tableSecondaryEndpointUri = null, + $fileSecondaryEndpointUri = null, + $sas = null + ) { + $this->name = $name; + $this->key = $key; + $this->sas = $sas; + $this->blobEndpointUri = $blobEndpointUri; + $this->queueEndpointUri = $queueEndpointUri; + $this->tableEndpointUri = $tableEndpointUri; + $this->fileEndpointUri = $fileEndpointUri; + $this->blobSecondaryEndpointUri = $blobSecondaryEndpointUri; + $this->queueSecondaryEndpointUri = $queueSecondaryEndpointUri; + $this->tableSecondaryEndpointUri = $tableSecondaryEndpointUri; + $this->fileSecondaryEndpointUri = $fileSecondaryEndpointUri; + } + + /** + * Returns a StorageServiceSettings with development storage credentials using + * the specified proxy Uri. + * + * @param string $proxyUri The proxy endpoint to use. + * + * @return StorageServiceSettings + */ + private static function getDevelopmentStorageAccount($proxyUri) + { + if (is_null($proxyUri)) { + return self::developmentStorageAccount(); + } + + $scheme = parse_url($proxyUri, PHP_URL_SCHEME); + $host = parse_url($proxyUri, PHP_URL_HOST); + $prefix = $scheme . "://" . $host; + + return new StorageServiceSettings( + Resources::DEV_STORE_NAME, + Resources::DEV_STORE_KEY, + $prefix . ':10000/devstoreaccount1/', + $prefix . ':10001/devstoreaccount1/', + $prefix . ':10002/devstoreaccount1/', + null + ); + } + + /** + * Gets a StorageServiceSettings object that references the development storage + * account. + * + * @return StorageServiceSettings + */ + public static function developmentStorageAccount() + { + if (is_null(self::$devStoreAccount)) { + self::$devStoreAccount = self::getDevelopmentStorageAccount( + Resources::DEV_STORE_URI + ); + } + + return self::$devStoreAccount; + } + + /** + * Gets the default service endpoint using the specified protocol and account + * name. + * + * @param string $scheme The scheme of the service end point. + * @param string $accountName The account name of the service. + * @param string $dnsPrefix The service DNS prefix. + * @param string $dnsSuffix The service DNS suffix. + * @param bool $isSecondary If generating secondary endpoint. + * + * @return string + */ + private static function getServiceEndpoint( + $scheme, + $accountName, + $dnsPrefix, + $dnsSuffix = null, + $isSecondary = false + ) { + if ($isSecondary) { + $accountName .= Resources::SECONDARY_STRING; + } + if ($dnsSuffix === null) { + $dnsSuffix = Resources::DEFAULT_ENDPOINT_SUFFIX; + } + return sprintf( + Resources::SERVICE_URI_FORMAT, + $scheme, + $accountName, + $dnsPrefix.$dnsSuffix + ); + } + + /** + * Creates StorageServiceSettings object given endpoints uri. + * + * @param array $settings The service settings. + * @param string $blobEndpointUri The blob endpoint uri. + * @param string $queueEndpointUri The queue endpoint uri. + * @param string $tableEndpointUri The table endpoint uri. + * @param string $fileEndpointUri The file endpoint uri. + * @param string $blobSecondaryEndpointUri The blob secondary endpoint uri. + * @param string $queueSecondaryEndpointUri The queue secondary endpoint uri. + * @param string $tableSecondaryEndpointUri The table secondary endpoint uri. + * @param string $fileSecondaryEndpointUri The file secondary endpoint uri. + * + * @return StorageServiceSettings + */ + private static function createStorageServiceSettings( + array $settings, + $blobEndpointUri = null, + $queueEndpointUri = null, + $tableEndpointUri = null, + $fileEndpointUri = null, + $blobSecondaryEndpointUri = null, + $queueSecondaryEndpointUri = null, + $tableSecondaryEndpointUri = null, + $fileSecondaryEndpointUri = null + ) { + $blobEndpointUri = Utilities::tryGetValueInsensitive( + Resources::BLOB_ENDPOINT_NAME, + $settings, + $blobEndpointUri + ); + $queueEndpointUri = Utilities::tryGetValueInsensitive( + Resources::QUEUE_ENDPOINT_NAME, + $settings, + $queueEndpointUri + ); + $tableEndpointUri = Utilities::tryGetValueInsensitive( + Resources::TABLE_ENDPOINT_NAME, + $settings, + $tableEndpointUri + ); + $fileEndpointUri = Utilities::tryGetValueInsensitive( + Resources::FILE_ENDPOINT_NAME, + $settings, + $fileEndpointUri + ); + $accountName = Utilities::tryGetValueInsensitive( + Resources::ACCOUNT_NAME_NAME, + $settings + ); + $accountKey = Utilities::tryGetValueInsensitive( + Resources::ACCOUNT_KEY_NAME, + $settings + ); + $sasToken = Utilities::tryGetValueInsensitive( + Resources::SAS_TOKEN_NAME, + $settings + ); + + return new StorageServiceSettings( + $accountName, + $accountKey, + $blobEndpointUri, + $queueEndpointUri, + $tableEndpointUri, + $fileEndpointUri, + $blobSecondaryEndpointUri, + $queueSecondaryEndpointUri, + $tableSecondaryEndpointUri, + $fileSecondaryEndpointUri, + $sasToken + ); + } + + /** + * Creates a StorageServiceSettings object from the given connection string. + * + * @param string $connectionString The storage settings connection string. + * + * @return StorageServiceSettings + */ + public static function createFromConnectionString($connectionString) + { + $tokenizedSettings = self::parseAndValidateKeys($connectionString); + + // Devstore case + $matchedSpecs = self::matchedSpecification( + $tokenizedSettings, + self::allRequired(self::$useDevelopmentStorageSetting), + self::optional(self::$developmentStorageProxyUriSetting) + ); + if ($matchedSpecs) { + $proxyUri = Utilities::tryGetValueInsensitive( + Resources::DEVELOPMENT_STORAGE_PROXY_URI_NAME, + $tokenizedSettings + ); + + return self::getDevelopmentStorageAccount($proxyUri); + } + + // Automatic case + $matchedSpecs = self::matchedSpecification( + $tokenizedSettings, + self::allRequired( + self::$defaultEndpointsProtocolSetting, + self::$accountNameSetting, + self::$accountKeySetting + ), + self::optional( + self::$blobEndpointSetting, + self::$queueEndpointSetting, + self::$tableEndpointSetting, + self::$fileEndpointSetting, + self::$endpointSuffixSetting + ) + ); + if ($matchedSpecs) { + $scheme = Utilities::tryGetValueInsensitive( + Resources::DEFAULT_ENDPOINTS_PROTOCOL_NAME, + $tokenizedSettings + ); + $accountName = Utilities::tryGetValueInsensitive( + Resources::ACCOUNT_NAME_NAME, + $tokenizedSettings + ); + $endpointSuffix = Utilities::tryGetValueInsensitive( + Resources::ENDPOINT_SUFFIX_NAME, + $tokenizedSettings + ); + return self::createStorageServiceSettings( + $tokenizedSettings, + self::getServiceEndpoint( + $scheme, + $accountName, + Resources::BLOB_DNS_PREFIX, + $endpointSuffix + ), + self::getServiceEndpoint( + $scheme, + $accountName, + Resources::QUEUE_DNS_PREFIX, + $endpointSuffix + ), + self::getServiceEndpoint( + $scheme, + $accountName, + Resources::TABLE_DNS_PREFIX, + $endpointSuffix + ), + self::getServiceEndpoint( + $scheme, + $accountName, + Resources::FILE_DNS_PREFIX, + $endpointSuffix + ), + self::getServiceEndpoint( + $scheme, + $accountName, + Resources::BLOB_DNS_PREFIX, + $endpointSuffix, + true + ), + self::getServiceEndpoint( + $scheme, + $accountName, + Resources::QUEUE_DNS_PREFIX, + $endpointSuffix, + true + ), + self::getServiceEndpoint( + $scheme, + $accountName, + Resources::TABLE_DNS_PREFIX, + $endpointSuffix, + true + ), + self::getServiceEndpoint( + $scheme, + $accountName, + Resources::FILE_DNS_PREFIX, + $endpointSuffix, + true + ) + ); + } + + // Explicit case for AccountName/AccountKey combination + $matchedSpecs = self::matchedSpecification( + $tokenizedSettings, + self::atLeastOne( + self::$blobEndpointSetting, + self::$queueEndpointSetting, + self::$tableEndpointSetting, + self::$fileEndpointSetting + ), + self::allRequired( + self::$accountNameSetting, + self::$accountKeySetting + ) + ); + if ($matchedSpecs) { + return self::createStorageServiceSettings($tokenizedSettings); + } + + // Explicit case for SAS token + $matchedSpecs = self::matchedSpecification( + $tokenizedSettings, + self::atLeastOne( + self::$blobEndpointSetting, + self::$queueEndpointSetting, + self::$tableEndpointSetting, + self::$fileEndpointSetting + ), + self::allRequired( + self::$sasTokenSetting + ) + ); + if ($matchedSpecs) { + return self::createStorageServiceSettings($tokenizedSettings); + } + + self::noMatch($connectionString); + } + + /** + * Creates a StorageServiceSettings object from the given connection string. + * Note this is only for AAD connection string, it should at least contain + * the account name. + * + * @param string $connectionString The storage settings connection string. + * + * @return StorageServiceSettings + */ + public static function createFromConnectionStringForTokenCredential($connectionString) + { + // Explicit case for AAD token, Connection string could only have account + // name. + $tokenizedSettings = self::parseAndValidateKeys($connectionString); + + $scheme = Utilities::tryGetValueInsensitive( + Resources::DEFAULT_ENDPOINTS_PROTOCOL_NAME, + $tokenizedSettings + ); + $accountName = Utilities::tryGetValueInsensitive( + Resources::ACCOUNT_NAME_NAME, + $tokenizedSettings + ); + $endpointSuffix = Utilities::tryGetValueInsensitive( + Resources::ENDPOINT_SUFFIX_NAME, + $tokenizedSettings + ); + return self::createStorageServiceSettings( + $tokenizedSettings, + self::getServiceEndpoint( + $scheme, + $accountName, + Resources::BLOB_DNS_PREFIX, + $endpointSuffix + ), + self::getServiceEndpoint( + $scheme, + $accountName, + Resources::QUEUE_DNS_PREFIX, + $endpointSuffix + ), + self::getServiceEndpoint( + $scheme, + $accountName, + Resources::TABLE_DNS_PREFIX, + $endpointSuffix + ), + self::getServiceEndpoint( + $scheme, + $accountName, + Resources::FILE_DNS_PREFIX, + $endpointSuffix + ), + self::getServiceEndpoint( + $scheme, + $accountName, + Resources::BLOB_DNS_PREFIX, + $endpointSuffix, + true + ), + self::getServiceEndpoint( + $scheme, + $accountName, + Resources::QUEUE_DNS_PREFIX, + $endpointSuffix, + true + ), + self::getServiceEndpoint( + $scheme, + $accountName, + Resources::TABLE_DNS_PREFIX, + $endpointSuffix, + true + ), + self::getServiceEndpoint( + $scheme, + $accountName, + Resources::FILE_DNS_PREFIX, + $endpointSuffix, + true + ) + ); + } + + /** + * Gets storage service name. + * + * @return string + */ + public function getName() + { + return $this->name; + } + + /** + * Gets storage service key. + * + * @return string + */ + public function getKey() + { + return $this->key; + } + + /** + * Checks if there is a SAS token. + * + * @return boolean + */ + public function hasSasToken() + { + return !empty($this->sas); + } + + /** + * Gets storage service SAS token. + * + * @return string + */ + public function getSasToken() + { + return $this->sas; + } + + /** + * Gets storage service blob endpoint uri. + * + * @return string + */ + public function getBlobEndpointUri() + { + return $this->blobEndpointUri; + } + + /** + * Gets storage service queue endpoint uri. + * + * @return string + */ + public function getQueueEndpointUri() + { + return $this->queueEndpointUri; + } + + /** + * Gets storage service table endpoint uri. + * + * @return string + */ + public function getTableEndpointUri() + { + return $this->tableEndpointUri; + } + + /** + * Gets storage service file endpoint uri. + * + * @return string + */ + public function getFileEndpointUri() + { + return $this->fileEndpointUri; + } + + /** + * Gets storage service secondary blob endpoint uri. + * + * @return string + */ + public function getBlobSecondaryEndpointUri() + { + return $this->blobSecondaryEndpointUri; + } + + /** + * Gets storage service secondary queue endpoint uri. + * + * @return string + */ + public function getQueueSecondaryEndpointUri() + { + return $this->queueSecondaryEndpointUri; + } + + /** + * Gets storage service secondary table endpoint uri. + * + * @return string + */ + public function getTableSecondaryEndpointUri() + { + return $this->tableSecondaryEndpointUri; + } + + /** + * Gets storage service secondary file endpoint uri. + * + * @return string + */ + public function getFileSecondaryEndpointUri() + { + return $this->fileSecondaryEndpointUri; + } +} diff --git a/3rdparty/microsoft/azure-storage-common/src/Common/Internal/Utilities.php b/3rdparty/microsoft/azure-storage-common/src/Common/Internal/Utilities.php new file mode 100644 index 00000000..5765e4f5 --- /dev/null +++ b/3rdparty/microsoft/azure-storage-common/src/Common/Internal/Utilities.php @@ -0,0 +1,907 @@ + + * @copyright 2016 Microsoft Corporation + * @license https://github.com/azure/azure-storage-php/LICENSE + * @link https://github.com/azure/azure-storage-php + */ + +namespace MicrosoftAzure\Storage\Common\Internal; + +use Psr\Http\Message\StreamInterface; + +/** + * Utilities for the project + * + * @category Microsoft + * @package MicrosoftAzure\Storage\Common\Internal + * @author Azure Storage PHP SDK + * @copyright 2016 Microsoft Corporation + * @license https://github.com/azure/azure-storage-php/LICENSE + * @link https://github.com/azure/azure-storage-php + */ +class Utilities +{ + /** + * Returns the specified value of the $key passed from $array and in case that + * this $key doesn't exist, the default value is returned. + * + * @param array $array The array to be used. + * @param mixed $key The array key. + * @param mixed $default The value to return if $key is not found in $array. + * + * @return mixed + */ + public static function tryGetValue($array, $key, $default = null) + { + return (!is_null($array)) && is_array($array) && array_key_exists($key, $array) + ? $array[$key] + : $default; + } + + /** + * Adds a url scheme if there is no scheme. Return null if input URL is null. + * + * @param string $url The URL. + * @param string $scheme The scheme. By default HTTP + * + * @return string + */ + public static function tryAddUrlScheme($url, $scheme = 'http') + { + if ($url == null) { + return $url; + } + + $urlScheme = parse_url($url, PHP_URL_SCHEME); + + if (empty($urlScheme)) { + $url = "$scheme://" . $url; + } + + return $url; + } + + /** + * Parse storage account name from an endpoint url. + * + * @param string $url The endpoint $url + * + * @return string + */ + public static function tryParseAccountNameFromUrl($url) + { + $host = parse_url($url, PHP_URL_HOST); + + // first token of the url host is account name + return explode('.', $host)[0]; + } + + /** + * Parse secondary endpoint url string from a primary endpoint url. + * + * Return null if the primary endpoint url is invalid. + * + * @param string $uri The primary endpoint url string. + * + * @return null|string + */ + public static function tryGetSecondaryEndpointFromPrimaryEndpoint($uri) + { + $splitTokens = explode('.', $uri); + if (count($splitTokens) > 0 && $splitTokens[0] != '') { + $schemaAccountToken = $splitTokens[0]; + $schemaAccountSplitTokens = explode('/', $schemaAccountToken); + if (count($schemaAccountSplitTokens) > 0 && + $schemaAccountSplitTokens[0] != '') { + $accountName = $schemaAccountSplitTokens[ + count($schemaAccountSplitTokens) - 1 + ]; + $schemaAccountSplitTokens[count($schemaAccountSplitTokens) - 1] = + $accountName . Resources::SECONDARY_STRING; + + $splitTokens[0] = implode('/', $schemaAccountSplitTokens); + $secondaryUri = implode('.', $splitTokens); + return $secondaryUri; + } + } + return null; + } + + /** + * tries to get nested array with index name $key from $array. + * + * Returns empty array object if the value is NULL. + * + * @param string $key The index name. + * @param array $array The array object. + * + * @return array + */ + public static function tryGetArray($key, array $array) + { + return Utilities::getArray(Utilities::tryGetValue($array, $key)); + } + + /** + * Adds the given key/value pair into array if the value doesn't satisfy empty(). + * + * This function just validates that the given $array is actually array. If it's + * NULL the function treats it as array. + * + * @param string $key The key. + * @param string $value The value. + * @param array &$array The array. If NULL will be used as array. + * + * @return void + */ + public static function addIfNotEmpty($key, $value, array &$array) + { + if (!is_null($array)) { + Validate::isArray($array, 'array'); + } + + if (!empty($value)) { + $array[$key] = $value; + } + } + + /** + * Returns the specified value of the key chain passed from $array and in case + * that key chain doesn't exist, null is returned. + * + * @param array $array Array to be used. + * + * @return mixed + */ + public static function tryGetKeysChainValue(array $array) + { + $arguments = func_get_args(); + $numArguments = func_num_args(); + + $currentArray = $array; + for ($i = 1; $i < $numArguments; $i++) { + if (is_array($currentArray)) { + if (array_key_exists($arguments[$i], $currentArray)) { + $currentArray = $currentArray[$arguments[$i]]; + } else { + return null; + } + } else { + return null; + } + } + + return $currentArray; + } + + /** + * Checks if the passed $string starts with $prefix + * + * @param string $string word to seaech in + * @param string $prefix prefix to be matched + * @param boolean $ignoreCase true to ignore case during the comparison; + * otherwise, false + * + * @return boolean + */ + public static function startsWith($string, $prefix, $ignoreCase = false) + { + if ($ignoreCase) { + $string = strtolower($string); + $prefix = strtolower($prefix); + } + return ($prefix == substr($string, 0, strlen($prefix))); + } + + /** + * Returns grouped items from passed $var + * + * @param array $var item to group + * + * @return array + */ + public static function getArray(array $var) + { + if (is_null($var) || empty($var)) { + return array(); + } + + foreach ($var as $value) { + if ((gettype($value) == 'object') + && (get_class($value) == 'SimpleXMLElement') + ) { + return (array) $var; + } elseif (!is_array($value)) { + return array($var); + } + } + + return $var; + } + + /** + * Unserializes the passed $xml into array. + * + * @param string $xml XML to be parsed. + * + * @return array + */ + public static function unserialize($xml) + { + $sxml = new \SimpleXMLElement($xml); + + return self::_sxml2arr($sxml); + } + + /** + * Converts a SimpleXML object to an Array recursively + * ensuring all sub-elements are arrays as well. + * + * @param string $sxml SimpleXML object + * @param array $arr Array into which to store results + * + * @return array + */ + private static function _sxml2arr($sxml, array $arr = null) + { + foreach ((array) $sxml as $key => $value) { + if (is_object($value) || (is_array($value))) { + $arr[$key] = self::_sxml2arr($value); + } else { + $arr[$key] = $value; + } + } + + return $arr; + } + + /** + * Serializes given array into xml. The array indices must be string to use + * them as XML tags. + * + * @param array $array object to serialize represented in array. + * @param string $rootName name of the XML root element. + * @param string $defaultTag default tag for non-tagged elements. + * @param string $standalone adds 'standalone' header tag, values 'yes'/'no' + * + * @return string + */ + public static function serialize( + array $array, + $rootName, + $defaultTag = null, + $standalone = null + ) { + $xmlVersion = '1.0'; + $xmlEncoding = 'UTF-8'; + + if (!is_array($array)) { + return false; + } + + $xmlw = new \XmlWriter(); + $xmlw->openMemory(); + $xmlw->startDocument($xmlVersion, $xmlEncoding, $standalone); + + $xmlw->startElement($rootName); + + self::_arr2xml($xmlw, $array, $defaultTag); + + $xmlw->endElement(); + + return $xmlw->outputMemory(true); + } + + /** + * Takes an array and produces XML based on it. + * + * @param XMLWriter $xmlw XMLWriter object that was previously instanted + * and is used for creating the XML. + * @param array $data Array to be converted to XML + * @param string $defaultTag Default XML tag to be used if none specified. + * + * @return void + */ + private static function _arr2xml( + \XMLWriter $xmlw, + array $data, + $defaultTag = null + ) { + foreach ($data as $key => $value) { + if (strcmp($key, '@attributes') == 0) { + foreach ($value as $attributeName => $attributeValue) { + $xmlw->writeAttribute($attributeName, $attributeValue); + } + } elseif (is_array($value)) { + if (!is_int($key)) { + if ($key != Resources::EMPTY_STRING) { + $xmlw->startElement($key); + } else { + $xmlw->startElement($defaultTag); + } + } + + self::_arr2xml($xmlw, $value); + + if (!is_int($key)) { + $xmlw->endElement(); + } + continue; + } else { + $xmlw->writeElement($key, $value); + } + } + } + + /** + * Converts string into boolean value. + * + * @param string $obj boolean value in string format. + * @param bool $skipNull If $skipNull is set, will return NULL directly + * when $obj is NULL. + * + * @return bool + */ + public static function toBoolean($obj, $skipNull = false) + { + if ($skipNull && is_null($obj)) { + return null; + } + + return filter_var($obj, FILTER_VALIDATE_BOOLEAN); + } + + /** + * Converts string into boolean value. + * + * @param bool $obj boolean value to convert. + * + * @return string + */ + public static function booleanToString($obj) + { + return $obj ? 'true' : 'false'; + } + + /** + * Converts a given date string into \DateTime object + * + * @param string $date windows azure date ins string representation. + * + * @return \DateTime + */ + public static function rfc1123ToDateTime($date) + { + $timeZone = new \DateTimeZone('GMT'); + $format = Resources::AZURE_DATE_FORMAT; + + return \DateTime::createFromFormat($format, $date, $timeZone); + } + + /** + * Generate ISO 8601 compliant date string in UTC time zone + * + * @param \DateTimeInterface $date The date value to convert + * + * @return string + */ + public static function isoDate(\DateTimeInterface $date) + { + $date = clone $date; + $date = $date->setTimezone(new \DateTimeZone('UTC')); + + return str_replace('+00:00', 'Z', $date->format('c')); + } + + /** + * Converts a DateTime object into an Edm.DaeTime value in UTC timezone, + * represented as a string. + * + * @param mixed $value The datetime value. + * + * @return string + */ + public static function convertToEdmDateTime($value) + { + if (empty($value)) { + return $value; + } + + if (is_string($value)) { + $value = self::convertToDateTime($value); + } + + Validate::isDate($value); + + $cloned = clone $value; + $cloned->setTimezone(new \DateTimeZone('UTC')); + return str_replace('+00:00', 'Z', $cloned->format("Y-m-d\TH:i:s.u0P")); + } + + /** + * Converts a string to a \DateTime object. Returns false on failure. + * + * @param string $value The string value to parse. + * + * @return \DateTime + */ + public static function convertToDateTime($value) + { + if ($value instanceof \DateTime) { + return $value; + } + + if (substr($value, -1) == 'Z') { + $value = substr($value, 0, strlen($value) - 1); + } + + return new \DateTime($value, new \DateTimeZone('UTC')); + } + + /** + * Converts string to stream handle. + * + * @param string $string The string contents. + * + * @return resource + */ + public static function stringToStream($string) + { + return fopen('data://text/plain,' . urlencode($string), 'rb'); + } + + /** + * Sorts an array based on given keys order. + * + * @param array $array The array to sort. + * @param array $order The keys order array. + * + * @return array + */ + public static function orderArray(array $array, array $order) + { + $ordered = array(); + + foreach ($order as $key) { + if (array_key_exists($key, $array)) { + $ordered[$key] = $array[$key]; + } + } + + return $ordered; + } + + /** + * Checks if a value exists in an array. The comparison is done in a case + * insensitive manner. + * + * @param string $needle The searched value. + * @param array $haystack The array. + * + * @return boolean + */ + public static function inArrayInsensitive($needle, array $haystack) + { + return in_array(strtolower($needle), array_map('strtolower', $haystack)); + } + + /** + * Checks if the given key exists in the array. The comparison is done in a case + * insensitive manner. + * + * @param string $key The value to check. + * @param array $search The array with keys to check. + * + * @return boolean + */ + public static function arrayKeyExistsInsensitive($key, array $search) + { + return array_key_exists(strtolower($key), array_change_key_case($search)); + } + + /** + * Returns the specified value of the $key passed from $array and in case that + * this $key doesn't exist, the default value is returned. The key matching is + * done in a case insensitive manner. + * + * @param string $key The array key. + * @param array $haystack The array to be used. + * @param mixed $default The value to return if $key is not found in $array. + * + * @return mixed + */ + public static function tryGetValueInsensitive($key, $haystack, $default = null) + { + $array = array_change_key_case($haystack); + return Utilities::tryGetValue($array, strtolower($key), $default); + } + + /** + * Returns a string representation of a version 4 GUID, which uses random + * numbers.There are 6 reserved bits, and the GUIDs have this format: + * xxxxxxxx-xxxx-4xxx-[8|9|a|b]xxx-xxxxxxxxxxxx + * where 'x' is a hexadecimal digit, 0-9a-f. + * + * See http://tools.ietf.org/html/rfc4122 for more information. + * + * Note: This function is available on all platforms, while the + * com_create_guid() is only available for Windows. + * + * @return string A new GUID. + */ + public static function getGuid() + { + // @codingStandardsIgnoreStart + + return sprintf( + '%04x%04x-%04x-%04x-%02x%02x-%04x%04x%04x', + mt_rand(0, 65535), + mt_rand(0, 65535), // 32 bits for "time_low" + mt_rand(0, 65535), // 16 bits for "time_mid" + mt_rand(0, 4096) + 16384, // 16 bits for "time_hi_and_version", with + // the most significant 4 bits being 0100 + // to indicate randomly generated version + mt_rand(0, 64) + 128, // 8 bits for "clock_seq_hi", with + // the most significant 2 bits being 10, + // required by version 4 GUIDs. + mt_rand(0, 255), // 8 bits for "clock_seq_low" + mt_rand(0, 65535), // 16 bits for "node 0" and "node 1" + mt_rand(0, 65535), // 16 bits for "node 2" and "node 3" + mt_rand(0, 65535) // 16 bits for "node 4" and "node 5" + ); + + // @codingStandardsIgnoreEnd + } + + /** + * Creates a list of objects of type $class from the provided array using static + * create method. + * + * @param array $parsed The object in array representation + * @param string $class The class name. Must have static method create. + * + * @return array + */ + public static function createInstanceList(array $parsed, $class) + { + $list = array(); + + foreach ($parsed as $value) { + $list[] = $class::create($value); + } + + return $list; + } + + /** + * Takes a string and return if it ends with the specified character/string. + * + * @param string $haystack The string to search in. + * @param string $needle postfix to match. + * @param boolean $ignoreCase Set true to ignore case during the comparison; + * otherwise, false + * + * @return boolean + */ + public static function endsWith($haystack, $needle, $ignoreCase = false) + { + if ($ignoreCase) { + $haystack = strtolower($haystack); + $needle = strtolower($needle); + } + $length = strlen($needle); + if ($length == 0) { + return true; + } + + return (substr($haystack, -$length) === $needle); + } + + /** + * Get id from entity object or string. + * If entity is object than validate type and return $entity->$method() + * If entity is string than return this string + * + * @param object|string $entity Entity with id property + * @param string $type Entity type to validate + * @param string $method Methods that gets id (getId by default) + * + * @return string + */ + public static function getEntityId($entity, $type, $method = 'getId') + { + if (is_string($entity)) { + return $entity; + } else { + Validate::isA($entity, $type, 'entity'); + Validate::methodExists($entity, $method, $type); + + return $entity->$method(); + } + } + + /** + * Generate a pseudo-random string of bytes using a cryptographically strong + * algorithm. + * + * @param int $length Length of the string in bytes + * + * @return string|boolean Generated string of bytes on success, or FALSE on + * failure. + */ + public static function generateCryptoKey($length) + { + return openssl_random_pseudo_bytes($length); + } + + /** + * Convert base 256 number to decimal number. + * + * @param string $number Base 256 number + * + * @return string Decimal number + */ + public static function base256ToDec($number) + { + Validate::canCastAsString($number, 'number'); + + $result = 0; + $base = 1; + for ($i = strlen($number) - 1; $i >= 0; $i--) { + $result = bcadd($result, bcmul(ord($number[$i]), $base)); + $base = bcmul($base, 256); + } + + return $result; + } + + /** + * To evaluate if the stream is larger than a certain size. To restore + * the stream, it has to be seekable, so will return true if the stream + * is not seekable. + * @param StreamInterface $stream The stream to be evaluated. + * @param int $size The size if the string is larger than. + * + * @return boolean true if the stream is larger than the given size. + */ + public static function isStreamLargerThanSizeOrNotSeekable(StreamInterface $stream, $size) + { + Validate::isInteger($size, 'size'); + Validate::isTrue( + $stream instanceof StreamInterface, + sprintf(Resources::INVALID_PARAM_MSG, 'stream', 'Psr\Http\Message\StreamInterface') + ); + $result = true; + if ($stream->isSeekable()) { + $position = $stream->tell(); + try { + $stream->seek($size); + } catch (\RuntimeException $e) { + $pos = strpos( + $e->getMessage(), + 'to seek to stream position ' + ); + if ($pos == null) { + throw $e; + } + $result = false; + } + if ($stream->eof()) { + $result = false; + } elseif ($stream->read(1) == '') { + $result = false; + } + $stream->seek($position); + } + return $result; + } + + /** + * Gets metadata array by parsing them from given headers. + * + * @param array $headers HTTP headers containing metadata elements. + * + * @return array + */ + public static function getMetadataArray(array $headers) + { + $metadata = array(); + foreach ($headers as $key => $value) { + $isMetadataHeader = Utilities::startsWith( + strtolower($key), + Resources::X_MS_META_HEADER_PREFIX + ); + + if ($isMetadataHeader) { + // Metadata name is case-presrved and case insensitive + $MetadataName = str_ireplace( + Resources::X_MS_META_HEADER_PREFIX, + Resources::EMPTY_STRING, + $key + ); + $metadata[$MetadataName] = $value; + } + } + + return $metadata; + } + + /** + * Validates the provided metadata array. + * + * @param array $metadata The metadata array. + * + * @return void + */ + public static function validateMetadata(array $metadata = null) + { + if (!is_null($metadata)) { + Validate::isArray($metadata, 'metadata'); + } else { + $metadata = array(); + } + + foreach ($metadata as $key => $value) { + Validate::canCastAsString($key, 'metadata key'); + Validate::canCastAsString($value, 'metadata value'); + } + } + + /** + * Append the content to file. + * @param string $path The file to append to. + * @param string $content The content to append. + * + * @return void + */ + public static function appendToFile($path, $content) + { + $resource = @fopen($path, 'a+'); + if ($resource != null) { + fwrite($resource, $content); + fclose($resource); + } + } + + /** + * Check if all the bytes are zero. + * + * @param string $content The content. + * @return bool + */ + public static function allZero($content) + { + $size = strlen($content); + + // If all Zero, skip this range + for ($i = 0; $i < $size; $i++) { + if (ord($content[$i]) != 0) { + return false; + } + } + + return true; + } + + /** + * Append the delimiter to the string. The delimiter will not be added if + * the string already ends with this delimiter. + * + * @param string $string The string to add delimiter to. + * @param string $delimiter The delimiter to be added. + * + * @return string + */ + public static function appendDelimiter($string, $delimiter) + { + if (!self::endsWith($string, $delimiter)) { + $string .= $delimiter; + } + + return $string; + } + + /** + * Static function used to determine if the request is performed against + * secondary endpoint. + * + * @param Psr\Http\Message\RequestInterface $request The request performed. + * @param array $options The options of the + * request. Must contain + * Resources::ROS_SECONDARY_URI + * + * @return boolean + */ + public static function requestSentToSecondary( + \Psr\Http\Message\RequestInterface $request, + array $options + ) { + $uri = $request->getUri(); + $secondaryUri = $options[Resources::ROS_SECONDARY_URI]; + $isSecondary = false; + if (strpos((string)$uri, (string)$secondaryUri) !== false) { + $isSecondary = true; + } + return $isSecondary; + } + + /** + * Gets the location value from the headers. + * + * @param array $headers request/response headers. + * + * @return string + */ + public static function getLocationFromHeaders(array $headers) + { + $value = Utilities::tryGetValue( + $headers, + Resources::X_MS_CONTINUATION_LOCATION_MODE + ); + + $result = ''; + if (\is_string($value)) { + $result = $value; + } elseif (!empty($value)) { + $result = $value[0]; + } + return $result; + } + + /** + * Gets if the value is a double value or string representation of a double + * value + * + * @param mixed $value The value to be verified. + * + * @return boolean + */ + public static function isDouble($value) + { + return is_numeric($value) && is_double($value + 0); + } + + /** + * Calculates the content MD5 which is base64 encoded. This should be align + * with the server calculated MD5. + * + * @param string $content the content to be calculated. + * + * @return string + */ + public static function calculateContentMD5($content) + { + Validate::notNull($content, 'content'); + Validate::canCastAsString($content, 'content'); + + return base64_encode(md5($content, true)); + } + + /** + * Return if the environment is in 64 bit PHP. + * + * @return bool + */ + public static function is64BitPHP() + { + return PHP_INT_SIZE == 8; + } +} diff --git a/3rdparty/microsoft/azure-storage-common/src/Common/Internal/Validate.php b/3rdparty/microsoft/azure-storage-common/src/Common/Internal/Validate.php new file mode 100644 index 00000000..1d292684 --- /dev/null +++ b/3rdparty/microsoft/azure-storage-common/src/Common/Internal/Validate.php @@ -0,0 +1,461 @@ + + * @copyright 2016 Microsoft Corporation + * @license https://github.com/azure/azure-storage-php/LICENSE + * @link https://github.com/azure/azure-storage-php + */ + +namespace MicrosoftAzure\Storage\Common\Internal; + +use MicrosoftAzure\Storage\Common\Exceptions\InvalidArgumentTypeException; + +/** + * Validates against a condition and throws an exception in case of failure. + * + * @category Microsoft + * @package MicrosoftAzure\Storage\Common\Internal + * @author Azure Storage PHP SDK + * @copyright 2016 Microsoft Corporation + * @license https://github.com/azure/azure-storage-php/LICENSE + * @link https://github.com/azure/azure-storage-php + */ +class Validate +{ + /** + * Throws exception if the provided variable type is not array. + * + * @param mixed $var The variable to check. + * @param string $name The parameter name. + * + * @throws InvalidArgumentTypeException. + * + * @return void + */ + public static function isArray($var, $name) + { + if (!is_array($var)) { + throw new InvalidArgumentTypeException(gettype(array()), $name); + } + } + + /** + * Throws exception if the provided variable can not convert to a string. + * + * @param mixed $var The variable to check. + * @param string $name The parameter name. + * + * @throws InvalidArgumentTypeException + * + * @return void + */ + public static function canCastAsString($var, $name) + { + try { + (string)$var; + } catch (\Exception $e) { + throw new InvalidArgumentTypeException(gettype(''), $name); + } + } + + /** + * Throws exception if the provided variable type is not boolean. + * + * @param mixed $var variable to check against. + * + * @throws InvalidArgumentTypeException + * + * @return void + */ + public static function isBoolean($var) + { + (bool)$var; + } + + /** + * Throws exception if the provided variable is set to null. + * + * @param mixed $var The variable to check. + * @param string $name The parameter name. + * + * @throws \InvalidArgumentException + * + * @return void + */ + public static function notNullOrEmpty($var, $name) + { + if (is_null($var) || (empty($var) && $var != '0')) { + throw new \InvalidArgumentException( + sprintf(Resources::NULL_OR_EMPTY_MSG, $name) + ); + } + } + + /** + * Throws exception if the provided variable is not double. + * + * @param mixed $var The variable to check. + * @param string $name The parameter name. + * + * @throws \InvalidArgumentException + * + * @return void + */ + public static function isDouble($var, $name) + { + if (!is_numeric($var)) { + throw new InvalidArgumentTypeException('double', $name); + } + } + + /** + * Throws exception if the provided variable type is not integer. + * + * @param mixed $var The variable to check. + * @param string $name The parameter name. + * + * @throws InvalidArgumentTypeException + * + * @return void + */ + public static function isInteger($var, $name) + { + try { + (int)$var; + } catch (\Exception $e) { + throw new InvalidArgumentTypeException(gettype(123), $name); + } + } + + /** + * Returns whether the variable is an empty or null string. + * + * @param string $var value. + * + * @return boolean + */ + public static function isNullOrEmptyString($var) + { + try { + (string)$var; + } catch (\Exception $e) { + return false; + } + + return (!isset($var) || trim($var)===''); + } + + /** + * Throws exception if the provided condition is not satisfied. + * + * @param bool $isSatisfied condition result. + * @param string $failureMessage the exception message + * + * @throws \Exception + * + * @return void + */ + public static function isTrue($isSatisfied, $failureMessage) + { + if (!$isSatisfied) { + throw new \InvalidArgumentException($failureMessage); + } + } + + /** + * Throws exception if the provided $date doesn't implement \DateTimeInterface + * + * @param mixed $date variable to check against. + * + * @throws InvalidArgumentTypeException + * + * @return void + */ + public static function isDate($date) + { + if (gettype($date) != 'object' || !($date instanceof \DateTimeInterface)) { + throw new InvalidArgumentTypeException('DateTimeInterface'); + } + } + + /** + * Throws exception if the provided variable is set to null. + * + * @param mixed $var The variable to check. + * @param string $name The parameter name. + * + * @throws \InvalidArgumentException + * + * @return void + */ + public static function notNull($var, $name) + { + if (is_null($var)) { + throw new \InvalidArgumentException(sprintf(Resources::NULL_MSG, $name)); + } + } + + /** + * Throws exception if the object is not of the specified class type. + * + * @param mixed $objectInstance An object that requires class type validation. + * @param mixed $classInstance The instance of the class the the + * object instance should be. + * @param string $name The name of the object. + * + * @throws \InvalidArgumentException + * + * @return void + */ + public static function isInstanceOf($objectInstance, $classInstance, $name) + { + Validate::notNull($classInstance, 'classInstance'); + if (is_null($objectInstance)) { + return true; + } + + $objectType = gettype($objectInstance); + $classType = gettype($classInstance); + + if ($objectType === $classType) { + return true; + } else { + throw new \InvalidArgumentException( + sprintf( + Resources::INSTANCE_TYPE_VALIDATION_MSG, + $name, + $objectType, + $classType + ) + ); + } + } + + /** + * Creates an anonymous function that checks if the given hostname is valid or not. + * + * @return callable + */ + public static function getIsValidHostname() + { + return function ($hostname) { + return Validate::isValidHostname($hostname); + }; + } + + /** + * Throws an exception if the string is not of a valid hostname. + * + * @param string $hostname String to check. + * + * @throws \InvalidArgumentException + * + * @return boolean + */ + public static function isValidHostname($hostname) + { + if (defined('FILTER_VALIDATE_DOMAIN')) { + $isValid = filter_var($hostname, FILTER_VALIDATE_DOMAIN, FILTER_FLAG_HOSTNAME); + } else { + // (less accurate) fallback for PHP < 7.0 + $isValid = preg_match('/^[a-z0-9_-]+(\.[a-z0-9_-]+)*$/i', $hostname); + } + + if ($isValid) { + return true; + } else { + throw new \RuntimeException( + sprintf(Resources::INVALID_CONFIG_HOSTNAME, $hostname) + ); + } + } + + /** + * Creates a anonymous function that check if the given uri is valid or not. + * + * @return callable + */ + public static function getIsValidUri() + { + return function ($uri) { + return Validate::isValidUri($uri); + }; + } + + /** + * Throws exception if the string is not of a valid uri. + * + * @param string $uri String to check. + * + * @throws \InvalidArgumentException + * + * @return boolean + */ + public static function isValidUri($uri) + { + $isValid = filter_var($uri, FILTER_VALIDATE_URL); + + if ($isValid) { + return true; + } else { + throw new \RuntimeException( + sprintf(Resources::INVALID_CONFIG_URI, $uri) + ); + } + } + + /** + * Throws exception if the provided variable type is not object. + * + * @param mixed $var The variable to check. + * @param string $name The parameter name. + * + * @throws InvalidArgumentTypeException. + * + * @return boolean + */ + public static function isObject($var, $name) + { + if (!is_object($var)) { + throw new InvalidArgumentTypeException('object', $name); + } + + return true; + } + + /** + * Throws exception if the object is not of the specified class type. + * + * @param mixed $objectInstance An object that requires class type validation. + * @param string $class The class the object instance should be. + * @param string $name The parameter name. + * + * @throws \InvalidArgumentException + * + * @return boolean + */ + public static function isA($objectInstance, $class, $name) + { + Validate::canCastAsString($class, 'class'); + Validate::notNull($objectInstance, 'objectInstance'); + Validate::isObject($objectInstance, 'objectInstance'); + + $objectType = get_class($objectInstance); + + if (is_a($objectInstance, $class)) { + return true; + } else { + throw new \InvalidArgumentException( + sprintf( + Resources::INSTANCE_TYPE_VALIDATION_MSG, + $name, + $objectType, + $class + ) + ); + } + } + + /** + * Validate if method exists in object + * + * @param object $objectInstance An object that requires method existing + * validation + * @param string $method Method name + * @param string $name The parameter name + * + * @return boolean + */ + public static function methodExists($objectInstance, $method, $name) + { + Validate::canCastAsString($method, 'method'); + Validate::notNull($objectInstance, 'objectInstance'); + Validate::isObject($objectInstance, 'objectInstance'); + + if (method_exists($objectInstance, $method)) { + return true; + } else { + throw new \InvalidArgumentException( + sprintf( + Resources::ERROR_METHOD_NOT_FOUND, + $method, + $name + ) + ); + } + } + + /** + * Validate if string is date formatted + * + * @param string $value Value to validate + * @param string $name Name of parameter to insert in erro message + * + * @throws \InvalidArgumentException + * + * @return boolean + */ + public static function isDateString($value, $name) + { + Validate::canCastAsString($value, 'value'); + + try { + new \DateTime($value); + return true; + } catch (\Exception $e) { + throw new \InvalidArgumentException( + sprintf( + Resources::ERROR_INVALID_DATE_STRING, + $name, + $value + ) + ); + } + } + + /** + * Validate if the provided array has key, throw exception otherwise. + * + * @param string $key The key to be searched. + * @param string $name The name of the array. + * @param array $array The array to be validated. + * + * @throws \UnexpectedValueException + * @throws \InvalidArgumentException + * + * @return boolean + */ + public static function hasKey($key, $name, array $array) + { + Validate::isArray($array, $name); + + if (!array_key_exists($key, $array)) { + throw new \UnexpectedValueException( + sprintf( + Resources::INVALID_VALUE_MSG, + $name, + sprintf(Resources::ERROR_KEY_NOT_EXIST, $key) + ) + ); + } + + return true; + } +} diff --git a/3rdparty/microsoft/azure-storage-common/src/Common/LocationMode.php b/3rdparty/microsoft/azure-storage-common/src/Common/LocationMode.php new file mode 100644 index 00000000..250dc973 --- /dev/null +++ b/3rdparty/microsoft/azure-storage-common/src/Common/LocationMode.php @@ -0,0 +1,53 @@ + + * @copyright 2017 Microsoft Corporation + * @license https://github.com/azure/azure-storage-php/LICENSE + * @link https://github.com/azure/azure-storage-php + */ + +namespace MicrosoftAzure\Storage\Common; + +/** + * Location mode for the service. + * + * @category Microsoft + * @package MicrosoftAzure\Storage\Common + * @author Azure Storage PHP SDK + * @copyright 2017 Microsoft Corporation + * @license https://github.com/azure/azure-storage-php/LICENSE + * @link https://github.com/azure/azure-storage-php + */ +class LocationMode +{ + //Request will only be sent to primary endpoint, except for + //getServiceStats APIs. + const PRIMARY_ONLY = 'PrimaryOnly'; + + //Request will only be sent to secondary endpoint. + const SECONDARY_ONLY = 'SecondaryOnly'; + + //Request will be sent to primary endpoint first, and retry for secondary + //endpoint. + const PRIMARY_THEN_SECONDARY = 'PrimaryThenSecondary'; + + //Request will be sent to secondary endpoint first, and retry for primary + //endpoint. + const SECONDARY_THEN_PRIMARY = 'SecondaryThenPrimary'; +} diff --git a/3rdparty/microsoft/azure-storage-common/src/Common/Logger.php b/3rdparty/microsoft/azure-storage-common/src/Common/Logger.php new file mode 100644 index 00000000..b329be52 --- /dev/null +++ b/3rdparty/microsoft/azure-storage-common/src/Common/Logger.php @@ -0,0 +1,77 @@ + + * @copyright 2016 Microsoft Corporation + * @license https://github.com/azure/azure-storage-php/LICENSE + * @link https://github.com/azure/azure-storage-php + */ + +namespace MicrosoftAzure\Storage\Common; + +use MicrosoftAzure\Storage\Common\Internal\Resources; + +/** + * Logger class for debugging purpose. + * + * @category Microsoft + * @package MicrosoftAzure\Storage\Common + * @author Azure Storage PHP SDK + * @copyright 2016 Microsoft Corporation + * @license https://github.com/azure/azure-storage-php/LICENSE + * @link https://github.com/azure/azure-storage-php + */ +class Logger +{ + /** + * @var string + */ + private static $_filePath; + + /** + * Logs $var to file. + * + * @param mixed $var The data to log. + * @param string $tip The help message. + * + * @return void + */ + public static function log($var, $tip = Resources::EMPTY_STRING) + { + if (!empty($tip)) { + error_log($tip . "\n", 3, self::$_filePath); + } + + if (is_array($var) || is_object($var)) { + error_log(print_r($var, true), 3, self::$_filePath); + } else { + error_log($var . "\n", 3, self::$_filePath); + } + } + + /** + * Sets file path to use. + * + * @param string $filePath The log file path. + * @return void + */ + public static function setLogFile($filePath) + { + self::$_filePath = $filePath; + } +} diff --git a/3rdparty/microsoft/azure-storage-common/src/Common/MarkerContinuationTokenTrait.php b/3rdparty/microsoft/azure-storage-common/src/Common/MarkerContinuationTokenTrait.php new file mode 100644 index 00000000..4532c46c --- /dev/null +++ b/3rdparty/microsoft/azure-storage-common/src/Common/MarkerContinuationTokenTrait.php @@ -0,0 +1,109 @@ + + * @copyright 2017 Microsoft Corporation + * @license https://github.com/azure/azure-storage-php/LICENSE + * @link https://github.com/azure/azure-storage-php + */ + +namespace MicrosoftAzure\Storage\Common; + +use MicrosoftAzure\Storage\Common\Models\MarkerContinuationToken; + +/** + * Trait implementing logic for continuation tokens that has nextMarker. + * + * @category Microsoft + * @package MicrosoftAzure\Storage\Common + * @author Azure Storage PHP SDK + * @copyright 2017 Microsoft Corporation + * @license https://github.com/azure/azure-storage-php/LICENSE + * @link https://github.com/azure/azure-storage-php + */ +trait MarkerContinuationTokenTrait +{ + private $continuationToken; + + /** + * Setter for continuationToken + * + * @param MarkerContinuationToken|null $continuationToken the continuation + * token to be set. + */ + public function setContinuationToken(MarkerContinuationToken $continuationToken = null) + { + $this->continuationToken = $continuationToken; + } + + public function setMarker($marker) + { + if ($this->continuationToken == null) { + $this->continuationToken = new MarkerContinuationToken(); + }; + $this->continuationToken->setNextMarker($marker); + } + + /** + * Getter for continuationToken + * + * @return MarkerContinuationToken + */ + public function getContinuationToken() + { + return $this->continuationToken; + } + + /** + * Gets the next marker to list/query items. + * + * @return string + */ + public function getNextMarker() + { + if ($this->continuationToken == null) { + return null; + } + return $this->continuationToken->getNextMarker(); + } + + /** + * Gets for location for previous request. + * + * @return string + */ + public function getLocation() + { + if ($this->continuationToken == null) { + return null; + } + return $this->continuationToken->getLocation(); + } + + public function getLocationMode() + { + if ($this->continuationToken == null) { + return parent::getLocationMode(); + } elseif ($this->continuationToken->getLocation() == '') { + return parent::getLocationMode(); + } else { + return $this->getLocation(); + } + } +} diff --git a/3rdparty/microsoft/azure-storage-common/src/Common/Middlewares/HistoryMiddleware.php b/3rdparty/microsoft/azure-storage-common/src/Common/Middlewares/HistoryMiddleware.php new file mode 100644 index 00000000..b3e4ae29 --- /dev/null +++ b/3rdparty/microsoft/azure-storage-common/src/Common/Middlewares/HistoryMiddleware.php @@ -0,0 +1,200 @@ + + * @copyright 2017 Microsoft Corporation + * @license https://github.com/azure/azure-storage-php/LICENSE + * @link https://github.com/azure/azure-storage-php + */ + +namespace MicrosoftAzure\Storage\Common\Middlewares; + +use MicrosoftAzure\Storage\Common\Internal\Validate; +use MicrosoftAzure\Storage\Common\Internal\Utilities; +use MicrosoftAzure\Storage\Common\Internal\Serialization\MessageSerializer; +use Psr\Http\Message\RequestInterface; +use Psr\Http\Message\ResponseInterface; +use GuzzleHttp\Promise\RejectedPromise; + +/** + * This class provides the functionality to log the requests/options/responses. + * Logging large number of entries without providing a file path may exhaust + * the memory. + * + * The middleware should be pushed into client options if the logging is + * intended to persist between different API calls. + * + * @category Microsoft + * @package MicrosoftAzure\Storage\Common\Middlewares + * @author Azure Storage PHP SDK + * @copyright 2017 Microsoft Corporation + * @license https://github.com/azure/azure-storage-php/LICENSE + * @link https://github.com/azure/azure-storage-php + */ +class HistoryMiddleware extends MiddlewareBase +{ + private $history; + private $path; + private $count; + + const TITLE_LENGTH = 120; + + /** + * Gets the saved paried history. + * + * @return array + */ + public function getHistory() + { + return $this->history; + } + + /** + * Constructor + * + * @param string $path the path to save the history. If path is provided, + * no data is going to be saved to memory and the + * entries are going to be serialized and saved to given + * path. + * + */ + public function __construct($path = '') + { + $this->history = array(); + $this->path = $path; + $this->count = 0; + } + + /** + * Add an entry to history + * + * @param array $entry the entry to be added. + */ + public function addHistory(array $entry) + { + if ($this->path !== '') { + $this->appendNewEntryToPath($entry); + } else { + Validate::isTrue( + array_key_exists('request', $entry) && + array_key_exists('options', $entry) && + (array_key_exists('response', $entry) || + array_key_exists('reason', $entry)), + 'Given history entry not in correct format' + ); + $this->history[] = $entry; + } + ++$this->count; + } + + /** + * Clear the history + * + * @return void + */ + public function clearHistory() + { + $this->history = array(); + $this->count = 0; + } + + /** + * This function will be invoked after the request is sent, if + * the promise is fulfilled. + * + * @param RequestInterface $request the request sent. + * @param array $options the options that the request sent with. + * + * @return callable + */ + protected function onFulfilled(RequestInterface $request, array $options) + { + $reflection = $this; + return function (ResponseInterface $response) use ( + $reflection, + $request, + $options + ) { + $reflection->addHistory([ + 'request' => $request, + 'response' => $response, + 'options' => $options + ]); + return $response; + }; + } + + /** + * This function will be executed after the request is sent, if + * the promise is rejected. + * + * @param RequestInterface $request the request sent. + * @param array $options the options that the request sent with. + * + * @return callable + */ + protected function onRejected(RequestInterface $request, array $options) + { + $reflection = $this; + return function ($reason) use ( + $reflection, + $request, + $options + ) { + $reflection->addHistory([ + 'request' => $request, + 'reason' => $reason, + 'options' => $options + ]); + return new RejectedPromise($reason); + }; + } + + /** + * Append the new entry to saved file path. + * + * @param array $entry the entry to be added. + * + * @return void + */ + private function appendNewEntryToPath(array $entry) + { + $entryNoString = "Entry " . $this->count; + $delimiter = str_pad( + $entryNoString, + self::TITLE_LENGTH, + '-', + STR_PAD_BOTH + ) . PHP_EOL; + $entryString = $delimiter; + $entryString .= sprintf( + "Time: %s\n", + (new \DateTime("now", new \DateTimeZone('UTC')))->format('Y-m-d H:i:s') + ); + $entryString .= MessageSerializer::objectSerialize($entry['request']); + if (array_key_exists('reason', $entry)) { + $entryString .= MessageSerializer::objectSerialize($entry['reason']); + } elseif (array_key_exists('response', $entry)) { + $entryString .= MessageSerializer::objectSerialize($entry['response']); + } + + $entryString .= $delimiter; + + Utilities::appendToFile($this->path, $entryString); + } +} diff --git a/3rdparty/microsoft/azure-storage-common/src/Common/Middlewares/IMiddleware.php b/3rdparty/microsoft/azure-storage-common/src/Common/Middlewares/IMiddleware.php new file mode 100644 index 00000000..b4b3909c --- /dev/null +++ b/3rdparty/microsoft/azure-storage-common/src/Common/Middlewares/IMiddleware.php @@ -0,0 +1,71 @@ + + * @copyright 2017 Microsoft Corporation + * @license https://github.com/azure/azure-storage-php/LICENSE + * @link https://github.com/azure/azure-storage-php + */ + +namespace MicrosoftAzure\Storage\Common\Middlewares; + +/** + * IMiddleware is called before sending the request and after receiving the + * response. + * + * @category Microsoft + * @package MicrosoftAzure\Storage\Common\Middlewares + * @author Azure Storage PHP SDK + * @copyright 2017 Microsoft Corporation + * @license https://github.com/azure/azure-storage-php/LICENSE + * @link https://github.com/azure/azure-storage-php + */ + +interface IMiddleware +{ + /** + * This function will return a callable with $request and $options as + * its parameters and returns a promise. The callable can modify the + * request, fulfilled response or rejected reason when invoked with certain + * conditions. Sample middleware implementation: + * + * ``` + * return function ( + * RequestInterface $request, + * array $options + * ) use ($handler) { + * //do something prior to sending the request. + * $promise = $handler($request, $options); + * return $promise->then( + * function (ResponseInterface $response) use ($request, $options) { + * //do something + * return $response; + * }, + * function ($reason) use ($request, $options) { + * //do something + * return new GuzzleHttp\Promise\RejectedPromise($reason); + * } + * ); + * }; + * ``` + * + * @param callable $handler The next handler. + * @return callable + */ + public function __invoke(callable $handler); +} diff --git a/3rdparty/microsoft/azure-storage-common/src/Common/Middlewares/MiddlewareBase.php b/3rdparty/microsoft/azure-storage-common/src/Common/Middlewares/MiddlewareBase.php new file mode 100644 index 00000000..5b420804 --- /dev/null +++ b/3rdparty/microsoft/azure-storage-common/src/Common/Middlewares/MiddlewareBase.php @@ -0,0 +1,115 @@ + + * @copyright 2017 Microsoft Corporation + * @license https://github.com/azure/azure-storage-php/LICENSE + * @link https://github.com/azure/azure-storage-php + */ + +namespace MicrosoftAzure\Storage\Common\Middlewares; + +use Psr\Http\Message\RequestInterface; +use Psr\Http\Message\ResponseInterface; +use GuzzleHttp\Promise\RejectedPromise; + +/** + * This class provides the base structure of middleware that can be used for + * doing customized behavior including modifying the request, response or + * other behaviors like logging, retrying and debugging. + * + * @category Microsoft + * @package MicrosoftAzure\Storage\Common\Middlewares + * @author Azure Storage PHP SDK + * @copyright 2017 Microsoft Corporation + * @license https://github.com/azure/azure-storage-php/LICENSE + * @link https://github.com/azure/azure-storage-php + */ +class MiddlewareBase implements IMiddleware +{ + + /** + * Middleware augments the functionality of handlers by invoking them + * in the process of generating responses. And it returns a function + * that accepts the next handler to invoke. Refer to + * http://docs.guzzlephp.org/en/latest/handlers-and-middleware.html#middleware + * for more detailed information. + * + * @param callable The handler function. + * + * @return callable The function that accepts the next handler to invoke. + */ + public function __invoke(callable $handler) + { + $reflection = $this; + return function ($request, $options) use ($handler, $reflection) { + $request = $reflection->onRequest($request); + return $handler($request, $options)->then( + $reflection->onFulfilled($request, $options), + $reflection->onRejected($request, $options) + ); + }; + } + + /** + * This function will be executed before the request is sent. + * + * @param RequestInterface $request the request before altered. + * + * @return RequestInterface the request after altered. + */ + protected function onRequest(RequestInterface $request) + { + //do nothing + return $request; + } + + /** + * This function will be invoked after the request is sent, if + * the promise is fulfilled. + * + * @param RequestInterface $request the request sent. + * @param array $options the options that the request sent with. + * + * @return callable + */ + protected function onFulfilled(RequestInterface $request, array $options) + { + return function (ResponseInterface $response) { + //do nothing + return $response; + }; + } + + /** + * This function will be executed after the request is sent, if + * the promise is rejected. + * + * @param RequestInterface $request the request sent. + * @param array $options the options that the request sent with. + * + * @return callable + */ + protected function onRejected(RequestInterface $request, array $options) + { + return function ($reason) { + //do nothing + return new RejectedPromise($reason); + }; + } +} diff --git a/3rdparty/microsoft/azure-storage-common/src/Common/Middlewares/MiddlewareStack.php b/3rdparty/microsoft/azure-storage-common/src/Common/Middlewares/MiddlewareStack.php new file mode 100644 index 00000000..cc7fc7e4 --- /dev/null +++ b/3rdparty/microsoft/azure-storage-common/src/Common/Middlewares/MiddlewareStack.php @@ -0,0 +1,70 @@ + + * @copyright 2017 Microsoft Corporation + * @license https://github.com/azure/azure-storage-php/LICENSE + * @link https://github.com/azure/azure-storage-php + */ + +namespace MicrosoftAzure\Storage\Common\Middlewares; + +/** + * This class provides the stack that handles the logic of applying each + * middlewares to the request or the response. + * + * @category Microsoft + * @package MicrosoftAzure\Storage\Common\Middlewares + * @author Azure Storage PHP SDK + * @copyright 2017 Microsoft Corporation + * @license https://github.com/azure/azure-storage-php/LICENSE + * @link https://github.com/azure/azure-storage-php + */ +class MiddlewareStack +{ + private $middlewares = array(); + + /** + * Push the given middleware into the middleware stack. + * + * @param IMiddleware|callable $middleware The middleware to be pushed. + * + * @return void + */ + public function push($middleware) + { + array_unshift($this->middlewares, $middleware); + } + + /** + * Apply the middlewares to the handler. + * + * @param callable $handler the handler to which the middleware applies. + * + * @return callable + */ + public function apply(callable $handler) + { + $result = $handler; + foreach ($this->middlewares as $middleware) { + $result = $middleware($result); + } + + return $result; + } +} diff --git a/3rdparty/microsoft/azure-storage-common/src/Common/Middlewares/RetryMiddleware.php b/3rdparty/microsoft/azure-storage-common/src/Common/Middlewares/RetryMiddleware.php new file mode 100644 index 00000000..79c41220 --- /dev/null +++ b/3rdparty/microsoft/azure-storage-common/src/Common/Middlewares/RetryMiddleware.php @@ -0,0 +1,181 @@ + + * @copyright 2017 Microsoft Corporation + * @license https://github.com/azure/azure-storage-php/LICENSE + * @link https://github.com/azure/azure-storage-php + */ + +namespace MicrosoftAzure\Storage\Common\Middlewares; + +use MicrosoftAzure\Storage\Common\LocationMode; +use MicrosoftAzure\Storage\Common\Internal\Resources; +use MicrosoftAzure\Storage\Common\Internal\Utilities; +use Psr\Http\Message\RequestInterface; +use Psr\Http\Message\ResponseInterface; +use GuzzleHttp\Psr7\Uri; +use GuzzleHttp\Promise\RejectedPromise; + +/** + * This class provides the functionality of a middleware that handles all the + * retry logic for the request. + * + * @category Microsoft + * @package MicrosoftAzure\Storage\Common\Middlewares + * @author Azure Storage PHP SDK + * @copyright 2017 Microsoft Corporation + * @license https://github.com/azure/azure-storage-php/LICENSE + * @link https://github.com/azure/azure-storage-php + */ +class RetryMiddleware extends MiddlewareBase +{ + private $intervalCalculator; + private $decider; + + public function __construct( + callable $intervalCalculator, + callable $decider + ) { + $this->intervalCalculator = $intervalCalculator; + $this->decider = $decider; + } + + /** + * This function will be invoked after the request is sent, if + * the promise is fulfilled. + * + * @param RequestInterface $request the request sent. + * @param array $options the options that the request sent with. + * + * @return callable + */ + protected function onFulfilled(RequestInterface $request, array $options) + { + return function (ResponseInterface $response) use ($request, $options) { + $isSecondary = Utilities::requestSentToSecondary($request, $options); + if (!isset($options['retries'])) { + $options['retries'] = 0; + } + if (call_user_func( + $this->decider, + $options['retries'], + $request, + $response, + null, + $isSecondary + )) { + return $this->retry($request, $options, $response); + } + //Add the header that indicates the endpoint to be used if + //continuation token is used for subsequent request. + if ($isSecondary) { + $response = $response->withHeader( + Resources::X_MS_CONTINUATION_LOCATION_MODE, + LocationMode::SECONDARY_ONLY + ); + } else { + $response = $response->withHeader( + Resources::X_MS_CONTINUATION_LOCATION_MODE, + LocationMode::PRIMARY_ONLY + ); + } + return $response; + }; + } + + /** + * This function will be executed after the request is sent, if + * the promise is rejected. + * + * @param RequestInterface $request the request sent. + * @param array $options the options that the request sent with. + * + * @return callable + */ + protected function onRejected(RequestInterface $request, array $options) + { + return function ($reason) use ($request, $options) { + $isSecondary = Utilities::requestSentToSecondary($request, $options); + if (!isset($options['retries'])) { + $options['retries'] = 0; + } + + if (call_user_func( + $this->decider, + $options['retries'], + $request, + null, + $reason, + $isSecondary + )) { + return $this->retry($request, $options); + } + return new RejectedPromise($reason); + }; + } + + /** + * This function does the real retry job. + * + * @param RequestInterface $request the request sent. + * @param array $options the options that the request sent with. + * @param ResponseInterface $response the response of the request + * + * @return callable + */ + private function retry( + RequestInterface $request, + array $options, + ResponseInterface $response = null + ) { + $options['delay'] = call_user_func( + $this->intervalCalculator, + ++$options['retries'] + ); + + //Change the request URI according to the location mode. + if (array_key_exists(Resources::ROS_LOCATION_MODE, $options)) { + $locationMode = $options[Resources::ROS_LOCATION_MODE]; + //If have RA-GRS enabled for the request, switch between + //primary and secondary. + if ($locationMode == LocationMode::PRIMARY_THEN_SECONDARY || + $locationMode == LocationMode::SECONDARY_THEN_PRIMARY) { + $primaryUri = $options[Resources::ROS_PRIMARY_URI]; + $secondaryUri = $options[Resources::ROS_SECONDARY_URI]; + + $target = $request->getRequestTarget(); + if (Utilities::startsWith($target, '/')) { + $target = substr($target, 1); + $primaryUri = new Uri($primaryUri . $target); + $secondaryUri = new Uri($secondaryUri . $target); + } + + //substitute the uri. + if ((string)$request->getUri() == (string)$primaryUri) { + $request = $request->withUri($secondaryUri); + } elseif ((string)$request->getUri() == (string)$secondaryUri) { + $request = $request->withUri($primaryUri); + } + } + } + $handler = $options[Resources::ROS_HANDLER]; + + return \call_user_func($handler, $request, $options); + } +} diff --git a/3rdparty/microsoft/azure-storage-common/src/Common/Middlewares/RetryMiddlewareFactory.php b/3rdparty/microsoft/azure-storage-common/src/Common/Middlewares/RetryMiddlewareFactory.php new file mode 100644 index 00000000..f024ef7a --- /dev/null +++ b/3rdparty/microsoft/azure-storage-common/src/Common/Middlewares/RetryMiddlewareFactory.php @@ -0,0 +1,271 @@ + + * @copyright 2016 Microsoft Corporation + * @license https://github.com/azure/azure-storage-php/LICENSE + * @link https://github.com/azure/azure-storage-php + */ + +namespace MicrosoftAzure\Storage\Common\Middlewares; + +use MicrosoftAzure\Storage\Common\Internal\Resources; +use MicrosoftAzure\Storage\Common\Internal\Validate; +use GuzzleHttp\Exception\ConnectException; +use GuzzleHttp\Exception\RequestException; + +/** + * This class provides static functions that creates retry handlers for Guzzle + * HTTP clients to handle retry policy. + * + * @category Microsoft + * @package MicrosoftAzure\Storage\Common\Middlewares + * @author Azure Storage PHP SDK + * @copyright 2016 Microsoft Corporation + * @license https://github.com/azure/azure-storage-php/LICENSE + * @link https://github.com/azure/azure-storage-php + */ +class RetryMiddlewareFactory +{ + //The interval will be increased linearly, the nth retry will have a + //wait time equal to n * interval. + const LINEAR_INTERVAL_ACCUMULATION = 'Linear'; + //The interval will be increased exponentially, the nth retry will have a + //wait time equal to pow(2, n) * interval. + const EXPONENTIAL_INTERVAL_ACCUMULATION = 'Exponential'; + //This is for the general type of logic that handles retry. + const GENERAL_RETRY_TYPE = 'General'; + //This is for the append blob retry only. + const APPEND_BLOB_RETRY_TYPE = 'Append Blob Retry'; + + /** + * Create the retry handler for the Guzzle client, according to the given + * attributes. + * + * @param string $type The type that controls the logic of + * the decider of the retry handler. + * Possible value can be + * self::GENERAL_RETRY_TYPE or + * self::APPEND_BLOB_RETRY_TYPE + * @param int $numberOfRetries The maximum number of retries. + * @param int $interval The minimum interval between each retry + * @param string $accumulationMethod If the interval increases linearly or + * exponentially. + * Possible value can be + * self::LINEAR_INTERVAL_ACCUMULATION or + * self::EXPONENTIAL_INTERVAL_ACCUMULATION + * @param bool $retryConnect Whether to retry on connection failures. + * @return RetryMiddleware A RetryMiddleware object that contains + * the logic of how the request should be + * handled after a response. + */ + public static function create( + $type = self::GENERAL_RETRY_TYPE, + $numberOfRetries = Resources::DEFAULT_NUMBER_OF_RETRIES, + $interval = Resources::DEFAULT_RETRY_INTERVAL, + $accumulationMethod = self::LINEAR_INTERVAL_ACCUMULATION, + $retryConnect = false + ) { + //Validate the input parameters + //type + Validate::isTrue( + $type == self::GENERAL_RETRY_TYPE || + $type == self::APPEND_BLOB_RETRY_TYPE, + sprintf( + Resources::INVALID_PARAM_GENERAL, + 'type' + ) + ); + //numberOfRetries + Validate::isTrue( + $numberOfRetries > 0, + sprintf( + Resources::INVALID_NEGATIVE_PARAM, + 'numberOfRetries' + ) + ); + //interval + Validate::isTrue( + $interval > 0, + sprintf( + Resources::INVALID_NEGATIVE_PARAM, + 'interval' + ) + ); + //accumulationMethod + Validate::isTrue( + $accumulationMethod == self::LINEAR_INTERVAL_ACCUMULATION || + $accumulationMethod == self::EXPONENTIAL_INTERVAL_ACCUMULATION, + sprintf( + Resources::INVALID_PARAM_GENERAL, + 'accumulationMethod' + ) + ); + //retryConnect + Validate::isBoolean($retryConnect); + + //Get the interval calculator according to the type of the + //accumulation method. + $intervalCalculator = + $accumulationMethod == self::LINEAR_INTERVAL_ACCUMULATION ? + static::createLinearDelayCalculator($interval) : + static::createExponentialDelayCalculator($interval); + + //Get the retry decider according to the type of the retry and + //the number of retries. + $retryDecider = static::createRetryDecider($type, $numberOfRetries, $retryConnect); + + //construct the retry middle ware. + return new RetryMiddleware($intervalCalculator, $retryDecider); + } + + /** + * Create the retry decider for the retry handler. It will return a callable + * that accepts the number of retries, the request, the response and the + * exception, and return the decision for a retry. + * + * @param string $type The type of the retry handler. + * @param int $maxRetries The maximum number of retries to be done. + * @param bool $retryConnect Whether to retry on connection failures. + * + * @return callable The callable that will return if the request should + * be retried. + */ + protected static function createRetryDecider($type, $maxRetries, $retryConnect) + { + return function ( + $retries, + $request, + $response = null, + $exception = null, + $isSecondary = false + ) use ( + $type, + $maxRetries, + $retryConnect + ) { + //Exceeds the retry limit. No retry. + if ($retries >= $maxRetries) { + return false; + } + + if (!$response) { + if (!$exception || !($exception instanceof RequestException)) { + return false; + } elseif ($exception instanceof ConnectException) { + return $retryConnect; + } else { + $response = $exception->getResponse(); + if (!$response) { + return true; + } + } + } + + if ($type == self::GENERAL_RETRY_TYPE) { + return static::generalRetryDecider( + $response->getStatusCode(), + $isSecondary + ); + } else { + return static::appendBlobRetryDecider( + $response->getStatusCode(), + $isSecondary + ); + } + + return true; + }; + } + + /** + * Decide if the given status code indicate the request should be retried. + * + * @param int $statusCode Status code of the previous request. + * @param bool $isSecondary Whether the request is sent to secondary endpoint. + * + * @return bool true if the request should be retried. + */ + protected static function generalRetryDecider($statusCode, $isSecondary) + { + $retry = false; + if ($statusCode == 408) { + $retry = true; + } elseif ($statusCode >= 500) { + if ($statusCode != 501 && $statusCode != 505) { + $retry = true; + } + } elseif ($isSecondary && $statusCode == 404) { + $retry = true; + } + return $retry; + } + + /** + * Decide if the given status code indicate the request should be retried. + * This is for append blob. + * + * @param int $statusCode Status code of the previous request. + * @param bool $isSecondary Whether the request is sent to secondary endpoint. + * + * @return bool true if the request should be retried. + */ + protected static function appendBlobRetryDecider($statusCode, $isSecondary) + { + //The retry logic is different for append blob. + //First it will need to record the former status code if it is + //server error. Then if the following request is 412 then it + //needs to be retried. Currently this is not implemented so will + //only adapt to the general retry decider. + //TODO: add logic for append blob's retry when implemented. + $retry = static::generalRetryDecider($statusCode, $isSecondary); + return $retry; + } + + /** + * Create the delay calculator that increases the interval linearly + * according to the number of retries. + * + * @param int $interval the minimum interval of the retry. + * + * @return callable a calculator that will return the interval + * according to the number of retries. + */ + protected static function createLinearDelayCalculator($interval) + { + return function ($retries) use ($interval) { + return $retries * $interval; + }; + } + + /** + * Create the delay calculator that increases the interval exponentially + * according to the number of retries. + * + * @param int $interval the minimum interval of the retry. + * + * @return callable a calculator that will return the interval + * according to the number of retries. + */ + protected static function createExponentialDelayCalculator($interval) + { + return function ($retries) use ($interval) { + return $interval * ((int)\pow(2, $retries)); + }; + } +} diff --git a/3rdparty/microsoft/azure-storage-common/src/Common/Models/AccessPolicy.php b/3rdparty/microsoft/azure-storage-common/src/Common/Models/AccessPolicy.php new file mode 100644 index 00000000..ff626c95 --- /dev/null +++ b/3rdparty/microsoft/azure-storage-common/src/Common/Models/AccessPolicy.php @@ -0,0 +1,218 @@ + + * @copyright 2016 Microsoft Corporation + * @license https://github.com/azure/azure-storage-php/LICENSE + * @link https://github.com/azure/azure-storage-php + */ + +namespace MicrosoftAzure\Storage\Common\Models; + +use MicrosoftAzure\Storage\Common\Internal\Utilities; +use MicrosoftAzure\Storage\Common\Internal\Validate; +use MicrosoftAzure\Storage\Common\Internal\Resources; + +/** + * Holds access policy elements + * + * @category Microsoft + * @package MicrosoftAzure\Storage\Common\Models + * @author Azure Storage PHP SDK + * @copyright 2016 Microsoft Corporation + * @license https://github.com/azure/azure-storage-php/LICENSE + * @link https://github.com/azure/azure-storage-php + */ +abstract class AccessPolicy +{ + private $start; + private $expiry; + private $permission; + private $resourceType; + + /** + * Get the valid permissions for the given resource. + * + * @return array + */ + abstract protected static function getResourceValidPermissions(); + + /** + * Constructor + * + * @param string $resourceType the resource type of this access policy. + */ + public function __construct($resourceType) + { + Validate::canCastAsString($resourceType, 'resourceType'); + Validate::isTrue( + $resourceType == Resources::RESOURCE_TYPE_BLOB || + $resourceType == Resources::RESOURCE_TYPE_CONTAINER || + $resourceType == Resources::RESOURCE_TYPE_QUEUE || + $resourceType == Resources::RESOURCE_TYPE_TABLE || + $resourceType == Resources::RESOURCE_TYPE_FILE || + $resourceType == Resources::RESOURCE_TYPE_SHARE, + Resources::ERROR_RESOURCE_TYPE_NOT_SUPPORTED + ); + + $this->resourceType = $resourceType; + } + + /** + * Gets start. + * + * @return \DateTime. + */ + public function getStart() + { + return $this->start; + } + + /** + * Sets start. + * + * @param \DateTime $start value. + * + * @return void + */ + public function setStart(\DateTime $start = null) + { + if ($start != null) { + Validate::isDate($start); + } + $this->start = $start; + } + + /** + * Gets expiry. + * + * @return \DateTime. + */ + public function getExpiry() + { + return $this->expiry; + } + + /** + * Sets expiry. + * + * @param \DateTime $expiry value. + * + * @return void + */ + public function setExpiry($expiry) + { + Validate::isDate($expiry); + $this->expiry = $expiry; + } + + /** + * Gets permission. + * + * @return string. + */ + public function getPermission() + { + return $this->permission; + } + + /** + * Sets permission. + * + * @param string $permission value. + * + * @throws \InvalidArgumentException + * + * @return void + */ + public function setPermission($permission) + { + $this->permission = $this->validatePermission($permission); + } + + /** + * Gets resource type. + * + * @return string. + */ + public function getResourceType() + { + return $this->resourceType; + } + + /** + * Validate the permission against its corresponding allowed permissions + * + * @param string $permission The permission to be validated. + * + * @throws \InvalidArgumentException + * + * @return string + */ + private function validatePermission($permission) + { + $validPermissions = static::getResourceValidPermissions(); + $result = ''; + foreach ($validPermissions as $validPermission) { + if (strpos($permission, $validPermission) !== false) { + //append the valid permission to result. + $result .= $validPermission; + //remove all the character that represents the permission. + $permission = str_replace( + $validPermission, + '', + $permission + ); + } + } + //After filtering all the permissions, if there is still characters + //left in the given permission, throw exception. + Validate::isTrue( + $permission == '', + sprintf( + Resources::INVALID_PERMISSION_PROVIDED, + $this->getResourceType(), + implode(', ', $validPermissions) + ) + ); + + return $result; + } + + /** + * Converts this current object to XML representation. + * + * @internal + * + * @return array + */ + public function toArray() + { + $array = array(); + + if ($this->getStart() != null) { + $array[Resources::XTAG_SIGNED_START] = + Utilities::convertToEdmDateTime($this->getStart()); + } + $array[Resources::XTAG_SIGNED_EXPIRY] = + Utilities::convertToEdmDateTime($this->getExpiry()); + $array[Resources::XTAG_SIGNED_PERMISSION] = $this->getPermission(); + + return $array; + } +} diff --git a/3rdparty/microsoft/azure-storage-common/src/Common/Models/CORS.php b/3rdparty/microsoft/azure-storage-common/src/Common/Models/CORS.php new file mode 100644 index 00000000..80c4c01f --- /dev/null +++ b/3rdparty/microsoft/azure-storage-common/src/Common/Models/CORS.php @@ -0,0 +1,269 @@ + + * @copyright 2016 Microsoft Corporation + * @license https://github.com/azure/azure-storage-php/LICENSE + * @link https://github.com/azure/azure-storage-php + */ + +namespace MicrosoftAzure\Storage\Common\Models; + +use MicrosoftAzure\Storage\Common\Internal\Resources; +use MicrosoftAzure\Storage\Common\Internal\Validate; + +/** + * Provides functionality and data structure for Cross-Origin Resource Sharing + * rules. + * + * @category Microsoft + * @package MicrosoftAzure\Storage\Common\Models + * @author Azure Storage PHP SDK + * @copyright 2017 Microsoft Corporation + * @license https://github.com/azure/azure-storage-php/LICENSE + * @link https://github.com/azure/azure-storage-php + */ +class CORS +{ + private $allowedOrigins; + private $allowedMethods; + private $allowedHeaders; + private $exposedHeaders; + private $maxAgeInSeconds; + + /** + * Constructor of the class. + * + * @param string[] $allowedOrigins The origin domains that are permitted + * to make request against the storage + * service via CORS. + * @param string[] $allowedMethods The methods (HTTP request verbs) that + * the origin domain may use for a CORS + * request. + * @param string[] $allowedHeaders The request headers that the origin + * domain may specify on the CORS request. + * @param string[] $exposedHeaders The response headers that may be sent in + * the response to the CORS request and + * exposed by the browser to the request + * issuer. + * @param int $maxAgeInSeconds The maximum amount of time that a + * browser should cache the preflight + * OPTIONS request. + */ + public function __construct( + array $allowedOrigins, + array $allowedMethods, + array $allowedHeaders, + array $exposedHeaders, + $maxAgeInSeconds + ) { + $this->setAllowedOrigins($allowedOrigins); + $this->setAllowedMethods($allowedMethods); + $this->setAllowedHeaders($allowedHeaders); + $this->setExposedHeaders($exposedHeaders); + $this->setMaxedAgeInSeconds($maxAgeInSeconds); + } + + /** + * Create an instance with parsed XML response with 'CORS' root. + * + * @param array $parsedResponse The response used to create an instance. + * + * @internal + * + * @return CORS + */ + public static function create(array $parsedResponse) + { + Validate::hasKey( + Resources::XTAG_ALLOWED_ORIGINS, + 'parsedResponse', + $parsedResponse + ); + Validate::hasKey( + Resources::XTAG_ALLOWED_METHODS, + 'parsedResponse', + $parsedResponse + ); + Validate::hasKey( + Resources::XTAG_ALLOWED_HEADERS, + 'parsedResponse', + $parsedResponse + ); + Validate::hasKey( + Resources::XTAG_EXPOSED_HEADERS, + 'parsedResponse', + $parsedResponse + ); + Validate::hasKey( + Resources::XTAG_MAX_AGE_IN_SECONDS, + 'parsedResponse', + $parsedResponse + ); + + // Get the values from the parsed response. + $allowedOrigins = array_filter(explode( + ',', + $parsedResponse[Resources::XTAG_ALLOWED_ORIGINS] + )); + $allowedMethods = array_filter(explode( + ',', + $parsedResponse[Resources::XTAG_ALLOWED_METHODS] + )); + $allowedHeaders = array_filter(explode( + ',', + $parsedResponse[Resources::XTAG_ALLOWED_HEADERS] + )); + $exposedHeaders = array_filter(explode( + ',', + $parsedResponse[Resources::XTAG_EXPOSED_HEADERS] + )); + $maxAgeInSeconds = intval( + $parsedResponse[Resources::XTAG_MAX_AGE_IN_SECONDS] + ); + + return new CORS( + $allowedOrigins, + $allowedMethods, + $allowedHeaders, + $exposedHeaders, + $maxAgeInSeconds + ); + } + + /** + * Converts this object to array with XML tags + * + * @return array + */ + public function toArray() + { + return array( + Resources::XTAG_ALLOWED_ORIGINS => + implode(',', $this->getAllowedOrigins()), + Resources::XTAG_ALLOWED_METHODS => + implode(',', $this->getAllowedMethods()), + Resources::XTAG_ALLOWED_HEADERS => + implode(',', $this->getAllowedHeaders()), + Resources::XTAG_EXPOSED_HEADERS => + implode(',', $this->getExposedHeaders()), + Resources::XTAG_MAX_AGE_IN_SECONDS => + $this->getMaxedAgeInSeconds() + ); + } + + /** + * Setter for allowedOrigins + * + * @param string[] $allowedOrigins the allowed origins to be set. + */ + public function setAllowedOrigins(array $allowedOrigins) + { + $this->allowedOrigins = $allowedOrigins; + } + + /** + * Getter for allowedOrigins + * + * @return string[] + */ + public function getAllowedOrigins() + { + return $this->allowedOrigins; + } + + /** + * Setter for allowedMethods + * + * @param string[] $allowedMethods the allowed methods to be set. + */ + public function setAllowedMethods(array $allowedMethods) + { + $this->allowedMethods = $allowedMethods; + } + + /** + * Getter for allowedMethods + * + * @return string[] + */ + public function getAllowedMethods() + { + return $this->allowedMethods; + } + + /** + * Setter for allowedHeaders + * + * @param string[] $allowedHeaders the allowed headers to be set. + */ + public function setAllowedHeaders(array $allowedHeaders) + { + $this->allowedHeaders = $allowedHeaders; + } + + /** + * Getter for allowedHeaders + * + * @return string[] + */ + public function getAllowedHeaders() + { + return $this->allowedHeaders; + } + + /** + * Setter for exposedHeaders + * + * @param string[] $exposedHeaders the exposed headers to be set. + */ + public function setExposedHeaders(array $exposedHeaders) + { + $this->exposedHeaders = $exposedHeaders; + } + + /** + * Getter for exposedHeaders + * + * @return string[] + */ + public function getExposedHeaders() + { + return $this->exposedHeaders; + } + + /** + * Setter for maxAgeInSeconds + * + * @param int $maxAgeInSeconds the max age in seconds to be set. + */ + public function setMaxedAgeInSeconds($maxAgeInSeconds) + { + $this->maxAgeInSeconds = $maxAgeInSeconds; + } + + /** + * Getter for maxAgeInSeconds + * + * @return int + */ + public function getMaxedAgeInSeconds() + { + return $this->maxAgeInSeconds; + } +} diff --git a/3rdparty/microsoft/azure-storage-common/src/Common/Models/ContinuationToken.php b/3rdparty/microsoft/azure-storage-common/src/Common/Models/ContinuationToken.php new file mode 100644 index 00000000..3d4a8bf1 --- /dev/null +++ b/3rdparty/microsoft/azure-storage-common/src/Common/Models/ContinuationToken.php @@ -0,0 +1,82 @@ + + * @copyright 2017 Microsoft Corporation + * @license https://github.com/azure/azure-storage-php/LICENSE + * @link https://github.com/azure/azure-storage-php + */ + +namespace MicrosoftAzure\Storage\Common\Models; + +use MicrosoftAzure\Storage\Common\Internal\Validate; +use MicrosoftAzure\Storage\Common\Internal\Resources; +use MicrosoftAzure\Storage\Common\LocationMode; + +/** + * Provides functionality and data structure for continuation token. + * + * @category Microsoft + * @package MicrosoftAzure\Storage\Common\Models + * @author Azure Storage PHP SDK + * @copyright 2017 Microsoft Corporation + * @license https://github.com/azure/azure-storage-php/LICENSE + * @link https://github.com/azure/azure-storage-php + */ +class ContinuationToken +{ + private $location; + + public function __construct( + $location = '' + ) { + $this->setLocation($location); + } + + /** + * Setter for location + * + * @param string $location the location to be set. + */ + public function setLocation($location) + { + Validate::canCastAsString($location, 'location'); + Validate::isTrue( + $location == LocationMode::PRIMARY_ONLY || + $location == LocationMode::SECONDARY_ONLY || + $location == '', + sprintf( + Resources::INVALID_VALUE_MSG, + 'location', + LocationMode::PRIMARY_ONLY . ' or ' . LocationMode::SECONDARY_ONLY + ) + ); + + $this->location = $location; + } + + /** + * Getter for location + * + * @return string + */ + public function getLocation() + { + return $this->location; + } +} diff --git a/3rdparty/microsoft/azure-storage-common/src/Common/Models/GetServicePropertiesResult.php b/3rdparty/microsoft/azure-storage-common/src/Common/Models/GetServicePropertiesResult.php new file mode 100644 index 00000000..9a432f90 --- /dev/null +++ b/3rdparty/microsoft/azure-storage-common/src/Common/Models/GetServicePropertiesResult.php @@ -0,0 +1,78 @@ + + * @copyright 2016 Microsoft Corporation + * @license https://github.com/azure/azure-storage-php/LICENSE + * @link https://github.com/azure/azure-storage-php + */ + +namespace MicrosoftAzure\Storage\Common\Models; + +/** + * Result from calling GetServiceProperties REST wrapper. + * + * @category Microsoft + * @package MicrosoftAzure\Storage\Common\Models + * @author Azure Storage PHP SDK + * @copyright 2016 Microsoft Corporation + * @license https://github.com/azure/azure-storage-php/LICENSE + * @link https://github.com/azure/azure-storage-php + */ +class GetServicePropertiesResult +{ + private $_serviceProperties; + + /** + * Creates object from $parsedResponse. + * + * @internal + * @param array $parsedResponse XML response parsed into array. + * + * @return \MicrosoftAzure\Storage\Common\Models\GetServicePropertiesResult + */ + public static function create(array $parsedResponse) + { + $result = new GetServicePropertiesResult(); + $result->setValue(ServiceProperties::create($parsedResponse)); + + return $result; + } + + /** + * Gets service properties object. + * + * @return \MicrosoftAzure\Storage\Common\Models\ServiceProperties + */ + public function getValue() + { + return $this->_serviceProperties; + } + + /** + * Sets service properties object. + * + * @param ServiceProperties $serviceProperties object to use. + * + * @return void + */ + protected function setValue($serviceProperties) + { + $this->_serviceProperties = clone $serviceProperties; + } +} diff --git a/3rdparty/microsoft/azure-storage-common/src/Common/Models/GetServiceStatsResult.php b/3rdparty/microsoft/azure-storage-common/src/Common/Models/GetServiceStatsResult.php new file mode 100644 index 00000000..a7ec61ea --- /dev/null +++ b/3rdparty/microsoft/azure-storage-common/src/Common/Models/GetServiceStatsResult.php @@ -0,0 +1,118 @@ + + * @copyright 2017 Microsoft Corporation + * @license https://github.com/azure/azure-storage-php/LICENSE + * @link https://github.com/azure/azure-storage-php + */ + +namespace MicrosoftAzure\Storage\Common\Models; + +use MicrosoftAzure\Storage\Common\Internal\Resources; +use MicrosoftAzure\Storage\Common\Internal\Utilities; + +/** + * Result from calling get service stats REST wrapper. + * + * @category Microsoft + * @package MicrosoftAzure\Storage\Common\Models + * @author Azure Storage PHP SDK + * @copyright 2017 Microsoft Corporation + * @license https://github.com/azure/azure-storage-php/LICENSE + * @link https://github.com/azure/azure-storage-php + */ +class GetServiceStatsResult +{ + private $status; + private $lastSyncTime; + + /** + * Creates object from $parsedResponse. + * + * @internal + * @param array $parsedResponse XML response parsed into array. + * + * @return \MicrosoftAzure\Storage\Common\Models\GetServiceStatsResult + */ + public static function create(array $parsedResponse) + { + $result = new GetServiceStatsResult(); + if (Utilities::arrayKeyExistsInsensitive( + Resources::XTAG_GEO_REPLICATION, + $parsedResponse + )) { + $geoReplication = $parsedResponse[Resources::XTAG_GEO_REPLICATION]; + if (Utilities::arrayKeyExistsInsensitive( + Resources::XTAG_STATUS, + $geoReplication + )) { + $result->setStatus($geoReplication[Resources::XTAG_STATUS]); + } + + if (Utilities::arrayKeyExistsInsensitive( + Resources::XTAG_LAST_SYNC_TIME, + $geoReplication + )) { + $lastSyncTime = $geoReplication[Resources::XTAG_LAST_SYNC_TIME]; + $result->setLastSyncTime(Utilities::convertToDateTime($lastSyncTime)); + } + } + + return $result; + } + + /** + * Gets status of the result. + * + * @return string + */ + public function getStatus() + { + return $this->status; + } + + /** + * Gets the last sync time. + * @return \DateTime + */ + public function getLastSyncTime() + { + return $this->lastSyncTime; + } + + /** + * Sets status of the result. + * + * @return void + */ + protected function setStatus($status) + { + $this->status = $status; + } + + /** + * Sets the last sync time. + * + * @return void + */ + protected function setLastSyncTime(\DateTime $lastSyncTime) + { + $this->lastSyncTime = $lastSyncTime; + } +} diff --git a/3rdparty/microsoft/azure-storage-common/src/Common/Models/Logging.php b/3rdparty/microsoft/azure-storage-common/src/Common/Models/Logging.php new file mode 100644 index 00000000..dcabfe90 --- /dev/null +++ b/3rdparty/microsoft/azure-storage-common/src/Common/Models/Logging.php @@ -0,0 +1,198 @@ + + * @copyright 2016 Microsoft Corporation + * @license https://github.com/azure/azure-storage-php/LICENSE + * @link https://github.com/azure/azure-storage-php + */ + +namespace MicrosoftAzure\Storage\Common\Models; + +use MicrosoftAzure\Storage\Common\Internal\Utilities; + +/** + * Holds elements of queue properties logging field. + * + * @category Microsoft + * @package MicrosoftAzure\Storage\Common\Models + * @author Azure Storage PHP SDK + * @copyright 2016 Microsoft Corporation + * @license https://github.com/azure/azure-storage-php/LICENSE + * @link https://github.com/azure/azure-storage-php + */ +class Logging +{ + private $_version; + private $_delete; + private $_read; + private $_write; + private $_retentionPolicy; + + /** + * Creates object from $parsedResponse. + * + * @internal + * @param array $parsedResponse XML response parsed into array. + * + * @return Logging + */ + public static function create(array $parsedResponse) + { + $result = new Logging(); + $result->setVersion($parsedResponse['Version']); + $result->setDelete(Utilities::toBoolean($parsedResponse['Delete'])); + $result->setRead(Utilities::toBoolean($parsedResponse['Read'])); + $result->setWrite(Utilities::toBoolean($parsedResponse['Write'])); + $result->setRetentionPolicy( + RetentionPolicy::create($parsedResponse['RetentionPolicy']) + ); + + return $result; + } + + /** + * Gets the retention policy + * + * @return MicrosoftAzure\Storage\Common\Models\RetentionPolicy + * + */ + public function getRetentionPolicy() + { + return $this->_retentionPolicy; + } + + /** + * Sets retention policy + * + * @param RetentionPolicy $policy object to use + * + * @return void + */ + public function setRetentionPolicy(RetentionPolicy $policy) + { + $this->_retentionPolicy = $policy; + } + + /** + * Gets whether all write requests should be logged. + * + * @return bool. + */ + public function getWrite() + { + return $this->_write; + } + + /** + * Sets whether all write requests should be logged. + * + * @param bool $write new value. + * + * @return void + */ + public function setWrite($write) + { + $this->_write = $write; + } + + /** + * Gets whether all read requests should be logged. + * + * @return bool + */ + public function getRead() + { + return $this->_read; + } + + /** + * Sets whether all read requests should be logged. + * + * @param bool $read new value. + * + * @return void + */ + public function setRead($read) + { + $this->_read = $read; + } + + /** + * Gets whether all delete requests should be logged. + * + * @return void + */ + public function getDelete() + { + return $this->_delete; + } + + /** + * Sets whether all delete requests should be logged. + * + * @param bool $delete new value. + * + * @return void + */ + public function setDelete($delete) + { + $this->_delete = $delete; + } + + /** + * Gets the version of Storage Analytics to configure + * + * @return string + */ + public function getVersion() + { + return $this->_version; + } + + /** + * Sets the version of Storage Analytics to configure + * + * @param string $version new value. + * + * @return void + */ + public function setVersion($version) + { + $this->_version = $version; + } + + /** + * Converts this object to array with XML tags + * + * @internal + * @return array + */ + public function toArray() + { + return array( + 'Version' => $this->_version, + 'Delete' => Utilities::booleanToString($this->_delete), + 'Read' => Utilities::booleanToString($this->_read), + 'Write' => Utilities::booleanToString($this->_write), + 'RetentionPolicy' => !empty($this->_retentionPolicy) + ? $this->_retentionPolicy->toArray() + : null + ); + } +} diff --git a/3rdparty/microsoft/azure-storage-common/src/Common/Models/MarkerContinuationToken.php b/3rdparty/microsoft/azure-storage-common/src/Common/Models/MarkerContinuationToken.php new file mode 100644 index 00000000..6422b6fc --- /dev/null +++ b/3rdparty/microsoft/azure-storage-common/src/Common/Models/MarkerContinuationToken.php @@ -0,0 +1,72 @@ + + * @copyright 2017 Microsoft Corporation + * @license https://github.com/azure/azure-storage-php/LICENSE + * @link https://github.com/azure/azure-storage-php + */ + +namespace MicrosoftAzure\Storage\Common\Models; + +use MicrosoftAzure\Storage\Common\Internal\Validate; + +/** + * Provides functionality and data structure for continuation token that + * contains next marker. + * + * @category Microsoft + * @package MicrosoftAzure\Storage\Common\Models + * @author Azure Storage PHP SDK + * @copyright 2017 Microsoft Corporation + * @license https://github.com/azure/azure-storage-php/LICENSE + * @link https://github.com/azure/azure-storage-php + */ +class MarkerContinuationToken extends ContinuationToken +{ + private $nextMarker; + + public function __construct( + $nextMarker = '', + $location = '' + ) { + parent::__construct($location); + $this->setNextMarker($nextMarker); + } + + /** + * Setter for nextMarker + * + * @param string $nextMarker the next marker to be set. + */ + public function setNextMarker($nextMarker) + { + Validate::canCastAsString($nextMarker, 'nextMarker'); + $this->nextMarker = $nextMarker; + } + + /** + * Getter for nextMarker + * + * @return string + */ + public function getNextMarker() + { + return $this->nextMarker; + } +} diff --git a/3rdparty/microsoft/azure-storage-common/src/Common/Models/Metrics.php b/3rdparty/microsoft/azure-storage-common/src/Common/Models/Metrics.php new file mode 100644 index 00000000..df36d9ac --- /dev/null +++ b/3rdparty/microsoft/azure-storage-common/src/Common/Models/Metrics.php @@ -0,0 +1,181 @@ + + * @copyright 2016 Microsoft Corporation + * @license https://github.com/azure/azure-storage-php/LICENSE + * @link https://github.com/azure/azure-storage-php + */ + +namespace MicrosoftAzure\Storage\Common\Models; + +use MicrosoftAzure\Storage\Common\Internal\Utilities; + +/** + * Holds elements of queue properties metrics field. + * + * @category Microsoft + * @package MicrosoftAzure\Storage\Common\Models + * @author Azure Storage PHP SDK + * @copyright 2016 Microsoft Corporation + * @license https://github.com/azure/azure-storage-php/LICENSE + * @link https://github.com/azure/azure-storage-php + */ +class Metrics +{ + private $_version; + private $_enabled; + private $_includeAPIs; + private $_retentionPolicy; + + /** + * Creates object from $parsedResponse. + * + * @internal + * @param array $parsedResponse XML response parsed into array. + * + * @return Metrics + */ + public static function create(array $parsedResponse) + { + $result = new Metrics(); + $result->setVersion($parsedResponse['Version']); + $result->setEnabled(Utilities::toBoolean($parsedResponse['Enabled'])); + if ($result->getEnabled()) { + $result->setIncludeAPIs( + Utilities::toBoolean($parsedResponse['IncludeAPIs']) + ); + } + $result->setRetentionPolicy( + RetentionPolicy::create($parsedResponse['RetentionPolicy']) + ); + + return $result; + } + + /** + * Gets retention policy + * + * @return RetentionPolicy + * + */ + public function getRetentionPolicy() + { + return $this->_retentionPolicy; + } + + /** + * Sets retention policy + * + * @param RetentionPolicy $policy object to use + * + * @return void + */ + public function setRetentionPolicy(RetentionPolicy $policy) + { + $this->_retentionPolicy = $policy; + } + + /** + * Gets include APIs. + * + * @return bool + */ + public function getIncludeAPIs() + { + return $this->_includeAPIs; + } + + /** + * Sets include APIs. + * + * @param bool $includeAPIs value to use. + * + * @return void + */ + public function setIncludeAPIs($includeAPIs) + { + $this->_includeAPIs = $includeAPIs; + } + + /** + * Gets enabled. + * + * @return bool + */ + public function getEnabled() + { + return $this->_enabled; + } + + /** + * Sets enabled. + * + * @param bool $enabled value to use. + * + * @return void + */ + public function setEnabled($enabled) + { + $this->_enabled = $enabled; + } + + /** + * Gets version + * + * @return string + */ + public function getVersion() + { + return $this->_version; + } + + /** + * Sets version + * + * @param string $version new value. + * + * @return void + */ + public function setVersion($version) + { + $this->_version = $version; + } + + /** + * Converts this object to array with XML tags + * + * @internal + * @return array + */ + public function toArray() + { + $array = array( + 'Version' => $this->_version, + 'Enabled' => Utilities::booleanToString($this->_enabled) + ); + if ($this->_enabled) { + $array['IncludeAPIs'] = Utilities::booleanToString($this->_includeAPIs); + } + $array['RetentionPolicy'] = !empty($this->_retentionPolicy) + ? $this->_retentionPolicy->toArray() + : null; + + return $array; + } +} diff --git a/3rdparty/microsoft/azure-storage-common/src/Common/Models/Range.php b/3rdparty/microsoft/azure-storage-common/src/Common/Models/Range.php new file mode 100644 index 00000000..79a38672 --- /dev/null +++ b/3rdparty/microsoft/azure-storage-common/src/Common/Models/Range.php @@ -0,0 +1,142 @@ + + * @copyright 2016 Microsoft Corporation + * @license https://github.com/azure/azure-storage-php/LICENSE + * @link https://github.com/azure/azure-storage-php + */ + +namespace MicrosoftAzure\Storage\Common\Models; + +/** + * Holds info about resource+ range used in HTTP requests + * + * @category Microsoft + * @package MicrosoftAzure\Storage\Common\Models + * @author Azure Storage PHP SDK + * @copyright 2016 Microsoft Corporation + * @license https://github.com/azure/azure-storage-php/LICENSE + * @link https://github.com/azure/azure-storage-php + */ +class Range +{ + private $start; + private $end; + + /** + * Constructor + * + * @param integer $start the resource start value + * @param integer $end the resource end value + * + * @return Range + */ + public function __construct($start, $end = null) + { + $this->start = $start; + $this->end = $end; + } + + /** + * Sets resource start range + * + * @param integer $start the resource range start + * + * @return void + */ + public function setStart($start) + { + $this->start = $start; + } + + /** + * Gets resource start range + * + * @return integer + */ + public function getStart() + { + return $this->start; + } + + /** + * Sets resource end range + * + * @param integer $end the resource range end + * + * @return void + */ + public function setEnd($end) + { + $this->end = $end; + } + + /** + * Gets resource end range + * + * @return integer + */ + public function getEnd() + { + return $this->end; + } + + /** + * Gets resource range length + * + * @return integer + */ + public function getLength() + { + if ($this->end != null) { + return $this->end - $this->start + 1; + } else { + return null; + } + } + + /** + * Sets resource range length + * + * @param integer $value new resource range + * + * @return void + */ + public function setLength($value) + { + $this->end = $this->start + $value - 1; + } + + /** + * Constructs the range string according to the set start and end + * + * @return string + */ + public function getRangeString() + { + $rangeString = ''; + + $rangeString .= ('bytes=' . $this->start . '-'); + if ($this->end != null) { + $rangeString .= $this->end; + } + + return $rangeString; + } +} diff --git a/3rdparty/microsoft/azure-storage-common/src/Common/Models/RangeDiff.php b/3rdparty/microsoft/azure-storage-common/src/Common/Models/RangeDiff.php new file mode 100644 index 00000000..cf727ee4 --- /dev/null +++ b/3rdparty/microsoft/azure-storage-common/src/Common/Models/RangeDiff.php @@ -0,0 +1,75 @@ + + * @copyright 2017 Microsoft Corporation + * @license https://github.com/azure/azure-storage-php/LICENSE + * @link https://github.com/azure/azure-storage-php + */ + +namespace MicrosoftAzure\Storage\Common\Models; + +/** + * Holds info about page blob range diffs + * + * @category Microsoft + * @package MicrosoftAzure\Storage\Common\Models + * @author Azure Storage PHP SDK + * @copyright 2017 Microsoft Corporation + * @license https://github.com/azure/azure-storage-php/LICENSE + * @link https://github.com/azure/azure-storage-php + */ +class RangeDiff extends Range +{ + private $isClearedPageRange; + + /** + * Constructor + * + * @param integer $start the resource start value + * @param integer $end the resource end value + * @param bool $isClearedPageRange true if the page range is a cleared range, false otherwise. + */ + public function __construct($start, $end = null, $isClearedPageRange = false) + { + parent::__construct($start, $end); + $this->isClearedPageRange = $isClearedPageRange; + } + + /** + * True if the page range is a cleared range, false otherwise + * + * @return bool + */ + public function isClearedPageRange() + { + return $this->isClearedPageRange; + } + + /** + * Sets the isClearedPageRange property + * + * @param bool $isClearedPageRange + * + * @return bool + */ + public function setIsClearedPageRange($isClearedPageRange) + { + $this->isClearedPageRange = $isClearedPageRange; + } +} diff --git a/3rdparty/microsoft/azure-storage-common/src/Common/Models/RetentionPolicy.php b/3rdparty/microsoft/azure-storage-common/src/Common/Models/RetentionPolicy.php new file mode 100644 index 00000000..59d05c54 --- /dev/null +++ b/3rdparty/microsoft/azure-storage-common/src/Common/Models/RetentionPolicy.php @@ -0,0 +1,124 @@ + + * @copyright 2016 Microsoft Corporation + * @license https://github.com/azure/azure-storage-php/LICENSE + * @link https://github.com/azure/azure-storage-php + */ + +namespace MicrosoftAzure\Storage\Common\Models; + +use MicrosoftAzure\Storage\Common\Internal\Utilities; + +/** + * Holds elements of queue properties retention policy field. + * + * @category Microsoft + * @package MicrosoftAzure\Storage\Common\Models + * @author Azure Storage PHP SDK + * @copyright 2016 Microsoft Corporation + * @license https://github.com/azure/azure-storage-php/LICENSE + * @link https://github.com/azure/azure-storage-php + */ +class RetentionPolicy +{ + private $_enabled; + private $_days; + + /** + * Creates object from $parsedResponse. + * + * @param array $parsedResponse XML response parsed into array. + * + * @internal + * + * @return MicrosoftAzure\Storage\Common\Models\RetentionPolicy + */ + public static function create(array $parsedResponse = null) + { + $result = new RetentionPolicy(); + $result->setEnabled(Utilities::toBoolean($parsedResponse['Enabled'])); + if ($result->getEnabled()) { + $result->setDays(intval($parsedResponse['Days'])); + } + + return $result; + } + + /** + * Gets enabled. + * + * @return bool + */ + public function getEnabled() + { + return $this->_enabled; + } + + /** + * Sets enabled. + * + * @param bool $enabled value to use. + * + * @return void + */ + public function setEnabled($enabled) + { + $this->_enabled = $enabled; + } + + /** + * Gets days field. + * + * @return int + */ + public function getDays() + { + return $this->_days; + } + + /** + * Sets days field. + * + * @param int $days value to use. + * + * @return void + */ + public function setDays($days) + { + $this->_days = $days; + } + + /** + * Converts this object to array with XML tags + * + * @internal + * + * @return array + */ + public function toArray() + { + $array = array('Enabled' => Utilities::booleanToString($this->_enabled)); + if (isset($this->_days)) { + $array['Days'] = strval($this->_days); + } + + return $array; + } +} diff --git a/3rdparty/microsoft/azure-storage-common/src/Common/Models/ServiceOptions.php b/3rdparty/microsoft/azure-storage-common/src/Common/Models/ServiceOptions.php new file mode 100644 index 00000000..5825a429 --- /dev/null +++ b/3rdparty/microsoft/azure-storage-common/src/Common/Models/ServiceOptions.php @@ -0,0 +1,309 @@ + + * @copyright 2017 Microsoft Corporation + * @license https://github.com/azure/azure-storage-php/LICENSE + * @link https://github.com/azure/azure-storage-php + */ + +namespace MicrosoftAzure\Storage\Common\Models; + +use MicrosoftAzure\Storage\Common\LocationMode; +use MicrosoftAzure\Storage\Common\Internal\Resources; +use MicrosoftAzure\Storage\Common\Internal\Validate; +use MicrosoftAzure\Storage\Common\Middlewares\MiddlewareStack; +use MicrosoftAzure\Storage\Common\Middlewares\IMiddleware; + +/** + * This class provides the base structure of service options, granting user to + * send with different options for each individual API call. + * + * @category Microsoft + * @package MicrosoftAzure\Storage\Common\Models + * @author Azure Storage PHP SDK + * @copyright 2017 Microsoft Corporation + * @license https://github.com/azure/azure-storage-php/LICENSE + * @link https://github.com/azure/azure-storage-php + */ +class ServiceOptions +{ + /** + * The middlewares to be applied using the operation. + * @internal + */ + protected $middlewares; + + /** + * The middleware stack used for the operation. + * @internal + */ + protected $middlewareStack; + + /** + * The number of concurrency when performing concurrent requests. + * @internal + */ + protected $numberOfConcurrency; + + /** + * If streamming is used for the operation. + * @internal + */ + protected $isStreaming; + + /** + * The location mode of the operation. + * @internal + */ + protected $locationMode; + + /** + * If to decode the content of the response body. + * @internal + */ + protected $decodeContent; + + /** + * The timeout of the operation + * @internal + */ + protected $timeout; + + /** + * Initialize the properties to default value. + */ + public function __construct(ServiceOptions $options = null) + { + if ($options == null) { + $this->setNumberOfConcurrency(Resources::NUMBER_OF_CONCURRENCY); + $this->setLocationMode(LocationMode::PRIMARY_ONLY); + $this->setIsStreaming(false); + $this->setDecodeContent(false); + $this->middlewares = array(); + $this->middlewareStack = null; + } else { + $this->setNumberOfConcurrency($options->getNumberOfConcurrency()); + $this->setLocationMode($options->getLocationMode()); + $this->setIsStreaming($options->getIsStreaming()); + $this->setDecodeContent($options->getDecodeContent()); + $this->middlewares = $options->getMiddlewares(); + $this->middlewareStack = $options->getMiddlewareStack(); + } + } + + /** + * Push a middleware into the middlewares. + * @param callable|IMiddleware $middleware middleware to be pushed. + * + * @return void + */ + public function pushMiddleware($middleware) + { + self::validateIsMiddleware($middleware); + $this->middlewares[] = $middleware; + } + + /** + * Gets the middlewares. + * + * @return array + */ + public function getMiddlewares() + { + return $this->middlewares; + } + + /** + * Sets middlewares. + * + * @param array $middlewares value. + * + * @return void + */ + public function setMiddlewares(array $middlewares) + { + foreach ($middlewares as $middleware) { + self::validateIsMiddleware($middleware); + } + $this->middlewares = $middlewares; + } + + /** + * Gets the middleware stack + * + * @return MiddlewareStack + */ + public function getMiddlewareStack() + { + return $this->middlewareStack; + } + + /** + * Sets the middleware stack. + * + * @param MiddlewareStack $middlewareStack value. + * + * @return void + */ + public function setMiddlewareStack(MiddlewareStack $middlewareStack) + { + $this->middlewareStack = $middlewareStack; + } + + /** + * Gets the number of concurrency value + * + * @return int + */ + public function getNumberOfConcurrency() + { + return $this->numberOfConcurrency; + } + + /** + * Sets number of concurrency. + * + * @param int $numberOfConcurrency value. + * + * @return void + */ + public function setNumberOfConcurrency($numberOfConcurrency) + { + $this->numberOfConcurrency = $numberOfConcurrency; + } + + /** + * Gets the isStreaming value + * + * @return bool + */ + public function getIsStreaming() + { + return $this->isStreaming; + } + + /** + * Sets isStreaming. + * + * @param bool $isStreaming value. + * + * @return void + */ + public function setIsStreaming($isStreaming) + { + $this->isStreaming = $isStreaming; + } + + /** + * Gets the locationMode value + * + * @return string + */ + public function getLocationMode() + { + return $this->locationMode; + } + + /** + * Sets locationMode. + * + * @param string $locationMode value. + * + * @return void + */ + public function setLocationMode($locationMode) + { + $this->locationMode = $locationMode; + } + + /** + * Gets the decodeContent value + * + * @return bool + */ + public function getDecodeContent() + { + return $this->decodeContent; + } + + /** + * Sets decodeContent. + * + * @param bool $decodeContent value. + * + * @return void + */ + public function setDecodeContent($decodeContent) + { + $this->decodeContent = $decodeContent; + } + + /** + * Gets the timeout value + * + * @return string + */ + public function getTimeout() + { + return $this->timeout; + } + + /** + * Sets timeout. + * + * @param string $timeout value. + * + * @return void + */ + public function setTimeout($timeout) + { + $this->timeout = $timeout; + } + + /** + * Generate request options using the input options and saved properties. + * + * @param array $options The options to be merged for the request options. + * + * @return array + */ + public function generateRequestOptions(array $options) + { + $result = array(); + + return $result; + } + + /** + * Validate if the given middleware is of callable or IMiddleware. + * + * @param void $middleware the middleware to be validated. + * + * @return void + */ + private static function validateIsMiddleware($middleware) + { + if (!(is_callable($middleware) || $middleware instanceof IMiddleware)) { + Validate::isTrue( + false, + Resources::INVALID_TYPE_MSG . 'callable or IMiddleware' + ); + } + } +} diff --git a/3rdparty/microsoft/azure-storage-common/src/Common/Models/ServiceProperties.php b/3rdparty/microsoft/azure-storage-common/src/Common/Models/ServiceProperties.php new file mode 100644 index 00000000..b5c5e111 --- /dev/null +++ b/3rdparty/microsoft/azure-storage-common/src/Common/Models/ServiceProperties.php @@ -0,0 +1,279 @@ + + * @copyright 2016 Microsoft Corporation + * @license https://github.com/azure/azure-storage-php/LICENSE + * @link https://github.com/azure/azure-storage-php + */ + +namespace MicrosoftAzure\Storage\Common\Models; + +use MicrosoftAzure\Storage\Common\Internal\Resources; +use MicrosoftAzure\Storage\Common\Internal\Serialization\XmlSerializer; + +/** + * Encapsulates service properties + * + * @category Microsoft + * @package MicrosoftAzure\Storage\Common\Models + * @author Azure Storage PHP SDK + * @copyright 2016 Microsoft Corporation + * @license https://github.com/azure/azure-storage-php/LICENSE + * @link https://github.com/azure/azure-storage-php + */ +class ServiceProperties +{ + private $logging; + private $hourMetrics; + private $minuteMetrics; + private $corses; + private $defaultServiceVersion; + + private static $xmlRootName = 'StorageServiceProperties'; + + /** + * Creates ServiceProperties object from parsed XML response. + * + * @internal + * @param array $parsedResponse XML response parsed into array. + * + * @return ServiceProperties. + */ + public static function create(array $parsedResponse) + { + $result = new ServiceProperties(); + + if (array_key_exists(Resources::XTAG_DEFAULT_SERVICE_VERSION, $parsedResponse) && + $parsedResponse[Resources::XTAG_DEFAULT_SERVICE_VERSION] != null) { + $result->setDefaultServiceVersion($parsedResponse[Resources::XTAG_DEFAULT_SERVICE_VERSION]); + } + + if (array_key_exists(Resources::XTAG_LOGGING, $parsedResponse)) { + $result->setLogging(Logging::create($parsedResponse[Resources::XTAG_LOGGING])); + } + $result->setHourMetrics(Metrics::create($parsedResponse[Resources::XTAG_HOUR_METRICS])); + if (array_key_exists(Resources::XTAG_MINUTE_METRICS, $parsedResponse)) { + $result->setMinuteMetrics(Metrics::create($parsedResponse[Resources::XTAG_MINUTE_METRICS])); + } + if (array_key_exists(Resources::XTAG_CORS, $parsedResponse) && + $parsedResponse[Resources::XTAG_CORS] != null) { + //There could be multiple CORS rules, so need to extract them all. + $corses = array(); + $corsArray = + $parsedResponse[Resources::XTAG_CORS][Resources::XTAG_CORS_RULE]; + if (count(array_filter(array_keys($corsArray), 'is_string')) > 0) { + //single cors rule + $corses[] = CORS::create($corsArray); + } else { + //multiple cors rule + foreach ($corsArray as $cors) { + $corses[] = CORS::create($cors); + } + } + + $result->setCorses($corses); + } else { + $result->setCorses(array()); + } + + return $result; + } + + /** + * Gets logging element. + * + * @return Logging + */ + public function getLogging() + { + return $this->logging; + } + + /** + * Sets logging element. + * + * @param Logging $logging new element. + * + * @return void + */ + public function setLogging(Logging $logging) + { + $this->logging = clone $logging; + } + + /** + * Gets hour metrics element. + * + * @return Metrics + */ + public function getHourMetrics() + { + return $this->hourMetrics; + } + + /** + * Sets hour metrics element. + * + * @param Metrics $metrics new element. + * + * @return void + */ + public function setHourMetrics(Metrics $hourMetrics) + { + $this->hourMetrics = clone $hourMetrics; + } + + /** + * Gets minute metrics element. + * + * @return Metrics + */ + public function getMinuteMetrics() + { + return $this->minuteMetrics; + } + + /** + * Sets minute metrics element. + * + * @param Metrics $metrics new element. + * + * @return void + */ + public function setMinuteMetrics(Metrics $minuteMetrics) + { + $this->minuteMetrics = clone $minuteMetrics; + } + + /** + * Gets corses element. + * + * @return CORS[] + */ + public function getCorses() + { + return $this->corses; + } + + /** + * Sets corses element. + * + * @param CORS[] $corses new elements. + * + * @return void + */ + public function setCorses(array $corses) + { + $this->corses = $corses; + } + + /** + * Gets the default service version. + * + * @return string + */ + public function getDefaultServiceVersion() + { + return $this->defaultServiceVersion; + } + + /** + * Sets the default service version. This can obly be set for the blob service. + * + * @param string $defaultServiceVersion the default service version + * + * @return void + */ + public function setDefaultServiceVersion($defaultServiceVersion) + { + $this->defaultServiceVersion = $defaultServiceVersion; + } + + /** + * Converts this object to array with XML tags + * + * @internal + * @return array + */ + public function toArray() + { + $result = array(); + + if (!empty($this->getLogging())) { + $result[Resources::XTAG_LOGGING] = + $this->getLogging()->toArray(); + } + + if (!empty($this->getHourMetrics())) { + $result[Resources::XTAG_HOUR_METRICS] = + $this->getHourMetrics()->toArray(); + } + + if (!empty($this->getMinuteMetrics())) { + $result[Resources::XTAG_MINUTE_METRICS] = + $this->getMinuteMetrics()->toArray(); + } + + $corsesArray = $this->getCorsesArray(); + if (!empty($corsesArray)) { + $result[Resources::XTAG_CORS] =$corsesArray; + } + + if ($this->defaultServiceVersion != null) { + $result[Resources::XTAG_DEFAULT_SERVICE_VERSION] = $this->defaultServiceVersion; + } + + return $result; + } + + /** + * Gets the array that contains all the CORSes. + * + * @return array + */ + private function getCorsesArray() + { + $corsesArray = array(); + if (count($this->getCorses()) == 1) { + $corsesArray = array( + Resources::XTAG_CORS_RULE => $this->getCorses()[0]->toArray() + ); + } elseif ($this->getCorses() != array()) { + foreach ($this->getCorses() as $cors) { + $corsesArray[] = [Resources::XTAG_CORS_RULE => $cors->toArray()]; + } + } + + return $corsesArray; + } + + /** + * Converts this current object to XML representation. + * + * @internal + * @param XmlSerializer $xmlSerializer The XML serializer. + * + * @return string + */ + public function toXml(XmlSerializer $xmlSerializer) + { + $properties = array(XmlSerializer::ROOT_NAME => self::$xmlRootName); + return $xmlSerializer->serialize($this->toArray(), $properties); + } +} diff --git a/3rdparty/microsoft/azure-storage-common/src/Common/Models/SignedIdentifier.php b/3rdparty/microsoft/azure-storage-common/src/Common/Models/SignedIdentifier.php new file mode 100644 index 00000000..0b3a1a41 --- /dev/null +++ b/3rdparty/microsoft/azure-storage-common/src/Common/Models/SignedIdentifier.php @@ -0,0 +1,118 @@ + + * @copyright 2016 Microsoft Corporation + * @license https://github.com/azure/azure-storage-php/LICENSE + * @link https://github.com/azure/azure-storage-php + */ + +namespace MicrosoftAzure\Storage\Common\Models; + +use MicrosoftAzure\Storage\Common\Internal\Resources; + +/** + * Holds signed identifiers. + * + * @category Microsoft + * @package MicrosoftAzure\Storage\Common\Models + * @author Azure Storage PHP SDK + * @copyright 2016 Microsoft Corporation + * @license https://github.com/azure/azure-storage-php/LICENSE + * @link https://github.com/azure/azure-storage-php + */ +class SignedIdentifier +{ + private $id; + private $accessPolicy; + + /** + * Constructor + * + * @param string $id The id of this signed identifier. + * @param AccessPolicy|null $accessPolicy The access policy. + */ + public function __construct($id = '', AccessPolicy $accessPolicy = null) + { + $this->setId($id); + $this->setAccessPolicy($accessPolicy); + } + + /** + * Gets id. + * + * @return string + */ + public function getId() + { + return $this->id; + } + + /** + * Sets id. + * + * @param string $id value. + * + * @return void + */ + public function setId($id) + { + $this->id = $id; + } + + /** + * Gets accessPolicy. + * + * @return AccessPolicy + */ + public function getAccessPolicy() + { + return $this->accessPolicy; + } + + /** + * Sets accessPolicy. + * + * @param AccessPolicy|null $accessPolicy value. + * + * @return void + */ + public function setAccessPolicy(AccessPolicy $accessPolicy = null) + { + $this->accessPolicy = $accessPolicy; + } + + /** + * Converts this current object to XML representation. + * + * @internal + * + * @return array + */ + public function toArray() + { + $array = array(); + $accessPolicyArray = array(); + $accessPolicyArray[Resources::XTAG_SIGNED_ID] = $this->getId(); + $accessPolicyArray[Resources::XTAG_ACCESS_POLICY] = + $this->getAccessPolicy()->toArray(); + $array[Resources::XTAG_SIGNED_IDENTIFIER] = $accessPolicyArray; + + return $array; + } +} diff --git a/3rdparty/microsoft/azure-storage-common/src/Common/Models/TransactionalMD5Trait.php b/3rdparty/microsoft/azure-storage-common/src/Common/Models/TransactionalMD5Trait.php new file mode 100644 index 00000000..a015b6e9 --- /dev/null +++ b/3rdparty/microsoft/azure-storage-common/src/Common/Models/TransactionalMD5Trait.php @@ -0,0 +1,64 @@ + + * @copyright 2018 Microsoft Corporation + * @license https://github.com/azure/azure-storage-php/LICENSE + * @link https://github.com/azure/azure-storage-php + */ + +namespace MicrosoftAzure\Storage\Common\Models; + +/** + * Trait implementing setting and getting useTransactionalMD5 for + * option classes which need support transactional MD5 validation + * during data transferring. + * + * @category Microsoft + * @package MicrosoftAzure\Storage\Common\Models + * @author Azure Storage PHP SDK + * @copyright 2018 Microsoft Corporation + * @license https://github.com/azure/azure-storage-php/LICENSE + * @link https://github.com/azure/azure-storage-php + */ +trait TransactionalMD5Trait +{ + /** @var $useTransactionalMD5 boolean */ + private $useTransactionalMD5; + + /** + * Gets whether using transactional MD5 validation. + * + * @return boolean + */ + public function getUseTransactionalMD5() + { + return $this->useTransactionalMD5; + } + + /** + * Sets whether using transactional MD5 validation. + * + * @param boolean $useTransactionalMD5 whether enable transactional + * MD5 validation. + */ + public function setUseTransactionalMD5($useTransactionalMD5) + { + $this->useTransactionalMD5 = $useTransactionalMD5; + } +} diff --git a/3rdparty/microsoft/azure-storage-common/src/Common/SharedAccessSignatureHelper.php b/3rdparty/microsoft/azure-storage-common/src/Common/SharedAccessSignatureHelper.php new file mode 100644 index 00000000..d5cc81f4 --- /dev/null +++ b/3rdparty/microsoft/azure-storage-common/src/Common/SharedAccessSignatureHelper.php @@ -0,0 +1,331 @@ + + * @copyright Microsoft Corporation + * @license https://github.com/azure/azure-storage-php/LICENSE + * @link https://github.com/azure/azure-storage-php + */ + +namespace MicrosoftAzure\Storage\Common; + +use MicrosoftAzure\Storage\Common\Internal\Resources; +use MicrosoftAzure\Storage\Common\Internal\Utilities; +use MicrosoftAzure\Storage\Common\Internal\Validate; + +/** + * Provides methods to generate Azure Storage Shared Access Signature + * + * @category Microsoft + * @package MicrosoftAzure\Storage\Common + * @author Azure Storage PHP SDK + * @copyright 2017 Microsoft Corporation + * @license https://github.com/azure/azure-storage-php/LICENSE + * @link https://github.com/azure/azure-storage-php + */ +class SharedAccessSignatureHelper +{ + protected $accountName; + protected $accountKey; + + /** + * Constructor. + * + * @param string $accountName the name of the storage account. + * @param string $accountKey the shared key of the storage account + * + */ + public function __construct($accountName, $accountKey) + { + Validate::canCastAsString($accountName, 'accountName'); + Validate::notNullOrEmpty($accountName, 'accountName'); + + Validate::canCastAsString($accountKey, 'accountKey'); + Validate::notNullOrEmpty($accountKey, 'accountKey'); + + $this->accountName = urldecode($accountName); + $this->accountKey = $accountKey; + } + + /** + * Generates a shared access signature at the account level. + * + * @param string $signedVersion Specifies the signed version to use. + * @param string $signedPermissions Specifies the signed permissions for + * the account SAS. + * @param string $signedService Specifies the signed services + * accessible with the account SAS. + * @param string $signedResourceType Specifies the signed resource types + * that are accessible with the account + * SAS. + * @param \Datetime|string $signedExpiry The time at which the shared access + * signature becomes invalid, in an ISO + * 8601 format. + * @param \Datetime|string $signedStart The time at which the SAS becomes + * valid, in an ISO 8601 format. + * @param string $signedIP Specifies an IP address or a range + * of IP addresses from which to accept + * requests. + * @param string $signedProtocol Specifies the protocol permitted for + * a request made with the account SAS. + * + * @see Constructing an account SAS at + * https://docs.microsoft.com/en-us/rest/api/storageservices/fileservices/constructing-an-account-sas + * + * @return string + */ + public function generateAccountSharedAccessSignatureToken( + $signedVersion, + $signedPermissions, + $signedService, + $signedResourceType, + $signedExpiry, + $signedStart = "", + $signedIP = "", + $signedProtocol = "" + ) { + // check that version is valid + Validate::canCastAsString($signedVersion, 'signedVersion'); + Validate::notNullOrEmpty($signedVersion, 'signedVersion'); + Validate::isDateString($signedVersion, 'signedVersion'); + + // validate and sanitize signed service + $signedService = $this->validateAndSanitizeSignedService($signedService); + + // validate and sanitize signed resource type + $signedResourceType = $this->validateAndSanitizeSignedResourceType($signedResourceType); + + // validate and sanitize signed permissions + $signedPermissions = $this->validateAndSanitizeSignedPermissions($signedPermissions); + + // check that expiracy is valid + if ($signedExpiry instanceof \Datetime) { + $signedExpiry = Utilities::isoDate($signedExpiry); + } + Validate::canCastAsString($signedExpiry, 'signedExpiry'); + Validate::notNullOrEmpty($signedExpiry, 'signedExpiry'); + Validate::isDateString($signedExpiry, 'signedExpiry'); + + // check that signed start is valid + if ($signedStart instanceof \Datetime) { + $signedStart = Utilities::isoDate($signedStart); + } + Validate::canCastAsString($signedStart, 'signedStart'); + if (strlen($signedStart) > 0) { + Validate::isDateString($signedStart, 'signedStart'); + } + + // check that signed IP is valid + Validate::canCastAsString($signedIP, 'signedIP'); + + // validate and sanitize signed protocol + $signedProtocol = $this->validateAndSanitizeSignedProtocol($signedProtocol); + + // construct an array with the parameters to generate the shared access signature at the account level + $parameters = array(); + $parameters[] = $this->accountName; + $parameters[] = $signedPermissions; + $parameters[] = $signedService; + $parameters[] = $signedResourceType; + $parameters[] = $signedStart; + $parameters[] = $signedExpiry; + $parameters[] = $signedIP; + $parameters[] = $signedProtocol; + $parameters[] = $signedVersion; + + // implode the parameters into a string + $stringToSign = utf8_encode(implode("\n", $parameters) . "\n"); + + // decode the account key from base64 + $decodedAccountKey = base64_decode($this->accountKey); + + // create the signature with hmac sha256 + $signature = hash_hmac("sha256", $stringToSign, $decodedAccountKey, true); + + // encode the signature as base64 and url encode. + $sig = urlencode(base64_encode($signature)); + + //adding all the components for account SAS together. + $sas = 'sv=' . $signedVersion; + $sas .= '&ss=' . $signedService; + $sas .= '&srt=' . $signedResourceType; + $sas .= '&sp=' . $signedPermissions; + $sas .= '&se=' . $signedExpiry; + $sas .= $signedStart === ''? '' : '&st=' . $signedStart; + $sas .= $signedIP === ''? '' : '&sip=' . $signedIP; + $sas .= '&spr=' . $signedProtocol; + $sas .= '&sig=' . $sig; + + // return the signature + return $sas; + } + + /** + * Validates and sanitizes the signed service parameter + * + * @param string $signedService Specifies the signed services accessible + * with the account SAS. + * + * @return string + */ + protected function validateAndSanitizeSignedService($signedService) + { + // validate signed service is not null or empty + Validate::canCastAsString($signedService, 'signedService'); + Validate::notNullOrEmpty($signedService, 'signedService'); + + // The signed service should only be a combination of the letters b(lob) q(ueue) t(able) or f(ile) + $validServices = ['b', 'q', 't', 'f']; + + return $this->validateAndSanitizeStringWithArray( + strtolower($signedService), + $validServices + ); + } + + /** + * Validates and sanitizes the signed resource type parameter + * + * @param string $signedResourceType Specifies the signed resource types + * that are accessible with the account + * SAS. + * + * @return string + */ + protected function validateAndSanitizeSignedResourceType($signedResourceType) + { + // validate signed resource type is not null or empty + Validate::canCastAsString($signedResourceType, 'signedResourceType'); + Validate::notNullOrEmpty($signedResourceType, 'signedResourceType'); + + // The signed resource type should only be a combination of the letters s(ervice) c(container) or o(bject) + $validResourceTypes = ['s', 'c', 'o']; + + return $this->validateAndSanitizeStringWithArray( + strtolower($signedResourceType), + $validResourceTypes + ); + } + + /** + * Validates and sanitizes the signed permissions parameter + * + * @param string $signedPermissions Specifies the signed permissions for the + * account SAS. + * + * @return string + */ + protected function validateAndSanitizeSignedPermissions( + $signedPermissions + ) { + // validate signed permissions are not null or empty + Validate::canCastAsString($signedPermissions, 'signedPermissions'); + Validate::notNullOrEmpty($signedPermissions, 'signedPermissions'); + + $validPermissions = ['r', 'w', 'd', 'l', 'a', 'c', 'u', 'p']; + + return $this->validateAndSanitizeStringWithArray( + strtolower($signedPermissions), + $validPermissions + ); + } + + /** + * Validates and sanitizes the signed protocol parameter + * + * @param string $signedProtocol Specifies the signed protocol for the + * account SAS. + + * @return string + */ + protected function validateAndSanitizeSignedProtocol($signedProtocol) + { + Validate::canCastAsString($signedProtocol, 'signedProtocol'); + // sanitize string + $sanitizedSignedProtocol = strtolower($signedProtocol); + if (strlen($sanitizedSignedProtocol) > 0) { + if (strcmp($sanitizedSignedProtocol, "https") != 0 && strcmp($sanitizedSignedProtocol, "https,http") != 0) { + throw new \InvalidArgumentException(Resources::SIGNED_PROTOCOL_INVALID_VALIDATION_MSG); + } + } + + return $sanitizedSignedProtocol; + } + + /** + * Removes duplicate characters from a string + * + * @param string $input The input string. + + * @return string + */ + protected function validateAndSanitizeStringWithArray($input, array $array) + { + $result = ''; + foreach ($array as $value) { + if (strpos($input, $value) !== false) { + //append the valid permission to result. + $result .= $value; + //remove all the character that represents the permission. + $input = str_replace( + $value, + '', + $input + ); + } + } + + Validate::isTrue( + strlen($input) == 0, + sprintf( + Resources::STRING_NOT_WITH_GIVEN_COMBINATION, + implode(', ', $array) + ) + ); + return $result; + } + + + /** + * Generate the canonical resource using the given account name, service + * type and resource. + * + * @param string $accountName The account name of the service. + * @param string $service The service name of the service. + * @param string $resource The name of the resource. + * + * @return string + */ + protected static function generateCanonicalResource( + $accountName, + $service, + $resource + ) { + static $serviceMap = array( + Resources::RESOURCE_TYPE_BLOB => 'blob', + Resources::RESOURCE_TYPE_FILE => 'file', + Resources::RESOURCE_TYPE_QUEUE => 'queue', + Resources::RESOURCE_TYPE_TABLE => 'table', + ); + $serviceName = $serviceMap[$service]; + if (Utilities::startsWith($resource, '/')) { + $resource = substr($resource, 1); + } + return urldecode(sprintf('/%s/%s/%s', $serviceName, $accountName, $resource)); + } +} diff --git a/3rdparty/mlocati/ip-lib/LICENSE.txt b/3rdparty/mlocati/ip-lib/LICENSE.txt new file mode 100644 index 00000000..5c21e47c --- /dev/null +++ b/3rdparty/mlocati/ip-lib/LICENSE.txt @@ -0,0 +1,20 @@ +The MIT License (MIT) +Copyright (c) 2016 Michele Locati + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is furnished +to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +THE SOFTWARE. \ No newline at end of file diff --git a/3rdparty/mlocati/ip-lib/ip-lib.php b/3rdparty/mlocati/ip-lib/ip-lib.php new file mode 100644 index 00000000..c34d0b49 --- /dev/null +++ b/3rdparty/mlocati/ip-lib/ip-lib.php @@ -0,0 +1,13 @@ +range = $range; + $this->type = $type; + $this->exceptions = $exceptions; + } + + /** + * Get the range definition. + * + * @return \IPLib\Range\RangeInterface + */ + public function getRange() + { + return $this->range; + } + + /** + * Get the range type. + * + * @return int one of the \IPLib\Range\Type::T_ constants + */ + public function getType() + { + return $this->type; + } + + /** + * Get the list of exceptions for this range type. + * + * @return \IPLib\Address\AssignedRange[] + */ + public function getExceptions() + { + return $this->exceptions; + } + + /** + * Get the assigned type for a specific address. + * + * @param \IPLib\Address\AddressInterface $address + * + * @return int|null return NULL of the address is outside this address; a \IPLib\Range\Type::T_ constant otherwise + */ + public function getAddressType(AddressInterface $address) + { + $result = null; + if ($this->range->contains($address)) { + foreach ($this->exceptions as $exception) { + $result = $exception->getAddressType($address); + if ($result !== null) { + break; + } + } + if ($result === null) { + $result = $this->type; + } + } + + return $result; + } + + /** + * Get the assigned type for a specific address range. + * + * @param \IPLib\Range\RangeInterface $range + * + * @return int|false|null return NULL of the range is fully outside this range; false if it's partly crosses this range (or it contains mixed types); a \IPLib\Range\Type::T_ constant otherwise + */ + public function getRangeType(RangeInterface $range) + { + $myStart = $this->range->getComparableStartString(); + $rangeEnd = $range->getComparableEndString(); + if ($myStart > $rangeEnd) { + $result = null; + } else { + $myEnd = $this->range->getComparableEndString(); + $rangeStart = $range->getComparableStartString(); + if ($myEnd < $rangeStart) { + $result = null; + } elseif ($rangeStart < $myStart || $rangeEnd > $myEnd) { + $result = false; + } else { + $result = null; + foreach ($this->exceptions as $exception) { + $result = $exception->getRangeType($range); + if ($result !== null) { + break; + } + } + if ($result === null) { + $result = $this->getType(); + } + } + } + + return $result; + } +} diff --git a/3rdparty/mlocati/ip-lib/src/Address/IPv4.php b/3rdparty/mlocati/ip-lib/src/Address/IPv4.php new file mode 100644 index 00000000..1fa109de --- /dev/null +++ b/3rdparty/mlocati/ip-lib/src/Address/IPv4.php @@ -0,0 +1,573 @@ +address = $address; + $this->bytes = null; + $this->rangeType = null; + $this->comparableString = null; + } + + /** + * {@inheritdoc} + * + * @see \IPLib\Address\AddressInterface::__toString() + */ + public function __toString() + { + return $this->address; + } + + /** + * {@inheritdoc} + * + * @see \IPLib\Address\AddressInterface::getNumberOfBits() + */ + public static function getNumberOfBits() + { + return 32; + } + + /** + * @deprecated since 1.17.0: use the parseString() method instead. + * For upgrading: + * - if $mayIncludePort is true, use the ParseStringFlag::MAY_INCLUDE_PORT flag + * - if $supportNonDecimalIPv4 is true, use the ParseStringFlag::IPV4_MAYBE_NON_DECIMAL flag + * + * @param string|mixed $address the address to parse + * @param bool $mayIncludePort + * @param bool $supportNonDecimalIPv4 + * + * @return static|null + * + * @see \IPLib\Address\IPv4::parseString() + * @since 1.1.0 added the $mayIncludePort argument + * @since 1.10.0 added the $supportNonDecimalIPv4 argument + */ + public static function fromString($address, $mayIncludePort = true, $supportNonDecimalIPv4 = false) + { + return static::parseString($address, 0 | ($mayIncludePort ? ParseStringFlag::MAY_INCLUDE_PORT : 0) | ($supportNonDecimalIPv4 ? ParseStringFlag::IPV4_MAYBE_NON_DECIMAL : 0)); + } + + /** + * Parse a string and returns an IPv4 instance if the string is valid, or null otherwise. + * + * @param string|mixed $address the address to parse + * @param int $flags A combination or zero or more flags + * + * @return static|null + * + * @see \IPLib\ParseStringFlag + * @since 1.17.0 + */ + public static function parseString($address, $flags = 0) + { + if (!is_string($address)) { + return null; + } + $flags = (int) $flags; + $matches = null; + if ($flags & ParseStringFlag::ADDRESS_MAYBE_RDNS) { + if (preg_match('/^([12]?[0-9]{1,2}\.[12]?[0-9]{1,2}\.[12]?[0-9]{1,2}\.[12]?[0-9]{1,2})\.in-addr\.arpa\.?$/i', $address, $matches)) { + $address = implode('.', array_reverse(explode('.', $matches[1]))); + $flags = $flags & ~(ParseStringFlag::IPV4_MAYBE_NON_DECIMAL | ParseStringFlag::IPV4ADDRESS_MAYBE_NON_QUAD_DOTTED); + } + } + if ($flags & ParseStringFlag::IPV4ADDRESS_MAYBE_NON_QUAD_DOTTED) { + if (strpos($address, '.') === 0) { + return null; + } + $lengthNonHex = '{1,11}'; + $lengthHex = '{1,8}'; + $chunk234Optional = true; + } else { + if (!strpos($address, '.')) { + return null; + } + $lengthNonHex = '{1,3}'; + $lengthHex = '{1,2}'; + $chunk234Optional = false; + } + $rxChunk1 = "0?[0-9]{$lengthNonHex}"; + if ($flags & ParseStringFlag::IPV4_MAYBE_NON_DECIMAL) { + $rxChunk1 = "(?:0[Xx]0*[0-9A-Fa-f]{$lengthHex})|(?:{$rxChunk1})"; + $onlyDecimal = false; + } else { + $onlyDecimal = true; + } + $rxChunk1 = "0*?({$rxChunk1})"; + $rxChunk234 = "\.{$rxChunk1}"; + if ($chunk234Optional) { + $rxChunk234 = "(?:{$rxChunk234})?"; + } + $rx = "{$rxChunk1}{$rxChunk234}{$rxChunk234}{$rxChunk234}"; + if ($flags & ParseStringFlag::MAY_INCLUDE_PORT) { + $rx .= '(?::\d+)?'; + } + if (!preg_match('/^' . $rx . '$/', $address, $matches)) { + return null; + } + $math = new \IPLib\Service\UnsignedIntegerMath(); + $nums = array(); + $maxChunkIndex = count($matches) - 1; + for ($i = 1; $i <= $maxChunkIndex; $i++) { + $numBytes = $i === $maxChunkIndex ? 5 - $i : 1; + $chunkBytes = $math->getBytes($matches[$i], $numBytes, $onlyDecimal); + if ($chunkBytes === null) { + return null; + } + $nums = array_merge($nums, $chunkBytes); + } + + return new static(implode('.', $nums)); + } + + /** + * Parse an array of bytes and returns an IPv4 instance if the array is valid, or null otherwise. + * + * @param int[]|array $bytes + * + * @return static|null + */ + public static function fromBytes(array $bytes) + { + $result = null; + if (count($bytes) === 4) { + $chunks = array_map( + function ($byte) { + return (is_int($byte) && $byte >= 0 && $byte <= 255) ? (string) $byte : false; + }, + $bytes + ); + if (in_array(false, $chunks, true) === false) { + $result = new static(implode('.', $chunks)); + } + } + + return $result; + } + + /** + * {@inheritdoc} + * + * @see \IPLib\Address\AddressInterface::toString() + */ + public function toString($long = false) + { + if ($long) { + return $this->getComparableString(); + } + + return $this->address; + } + + /** + * Get the octal representation of this IP address. + * + * @param bool $long + * + * @return string + * + * @since 1.10.0 + * + * @example if $long == false: if the decimal representation is '0.7.8.255': '0.7.010.0377' + * @example if $long == true: if the decimal representation is '0.7.8.255': '0000.0007.0010.0377' + */ + public function toOctal($long = false) + { + $chunks = array(); + foreach ($this->getBytes() as $byte) { + if ($long) { + $chunks[] = sprintf('%04o', $byte); + } else { + $chunks[] = '0' . decoct($byte); + } + } + + return implode('.', $chunks); + } + + /** + * Get the hexadecimal representation of this IP address. + * + * @param bool $long + * + * @return string + * + * @since 1.10.0 + * + * @example if $long == false: if the decimal representation is '0.9.10.255': '0.9.0xa.0xff' + * @example if $long == true: if the decimal representation is '0.9.10.255': '0x00.0x09.0x0a.0xff' + */ + public function toHexadecimal($long = false) + { + $chunks = array(); + foreach ($this->getBytes() as $byte) { + if ($long) { + $chunks[] = sprintf('0x%02x', $byte); + } else { + $chunks[] = '0x' . dechex($byte); + } + } + + return implode('.', $chunks); + } + + /** + * {@inheritdoc} + * + * @see \IPLib\Address\AddressInterface::getBytes() + */ + public function getBytes() + { + if ($this->bytes === null) { + $this->bytes = array_map( + function ($chunk) { + return (int) $chunk; + }, + explode('.', $this->address) + ); + } + + return $this->bytes; + } + + /** + * {@inheritdoc} + * + * @see \IPLib\Address\AddressInterface::getBits() + */ + public function getBits() + { + $parts = array(); + foreach ($this->getBytes() as $byte) { + $parts[] = sprintf('%08b', $byte); + } + + return implode('', $parts); + } + + /** + * {@inheritdoc} + * + * @see \IPLib\Address\AddressInterface::getAddressType() + */ + public function getAddressType() + { + return Type::T_IPv4; + } + + /** + * {@inheritdoc} + * + * @see \IPLib\Address\AddressInterface::getDefaultReservedRangeType() + */ + public static function getDefaultReservedRangeType() + { + return RangeType::T_PUBLIC; + } + + /** + * {@inheritdoc} + * + * @see \IPLib\Address\AddressInterface::getReservedRanges() + */ + public static function getReservedRanges() + { + if (self::$reservedRanges === null) { + $reservedRanges = array(); + foreach (array( + // RFC 5735 + '0.0.0.0/8' => array(RangeType::T_THISNETWORK, array('0.0.0.0/32' => RangeType::T_UNSPECIFIED)), + // RFC 5735 + '10.0.0.0/8' => array(RangeType::T_PRIVATENETWORK), + // RFC 6598 + '100.64.0.0/10' => array(RangeType::T_CGNAT), + // RFC 5735 + '127.0.0.0/8' => array(RangeType::T_LOOPBACK), + // RFC 5735 + '169.254.0.0/16' => array(RangeType::T_LINKLOCAL), + // RFC 5735 + '172.16.0.0/12' => array(RangeType::T_PRIVATENETWORK), + // RFC 5735 + '192.0.0.0/24' => array(RangeType::T_RESERVED), + // RFC 5735 + '192.0.2.0/24' => array(RangeType::T_RESERVED), + // RFC 5735 + '192.88.99.0/24' => array(RangeType::T_ANYCASTRELAY), + // RFC 5735 + '192.168.0.0/16' => array(RangeType::T_PRIVATENETWORK), + // RFC 5735 + '198.18.0.0/15' => array(RangeType::T_RESERVED), + // RFC 5735 + '198.51.100.0/24' => array(RangeType::T_RESERVED), + // RFC 5735 + '203.0.113.0/24' => array(RangeType::T_RESERVED), + // RFC 5735 + '224.0.0.0/4' => array(RangeType::T_MULTICAST), + // RFC 5735 + '240.0.0.0/4' => array(RangeType::T_RESERVED, array('255.255.255.255/32' => RangeType::T_LIMITEDBROADCAST)), + ) as $range => $data) { + $exceptions = array(); + if (isset($data[1])) { + foreach ($data[1] as $exceptionRange => $exceptionType) { + $exceptions[] = new AssignedRange(Subnet::parseString($exceptionRange), $exceptionType); + } + } + $reservedRanges[] = new AssignedRange(Subnet::parseString($range), $data[0], $exceptions); + } + self::$reservedRanges = $reservedRanges; + } + + return self::$reservedRanges; + } + + /** + * {@inheritdoc} + * + * @see \IPLib\Address\AddressInterface::getRangeType() + */ + public function getRangeType() + { + if ($this->rangeType === null) { + $rangeType = null; + foreach (static::getReservedRanges() as $reservedRange) { + $rangeType = $reservedRange->getAddressType($this); + if ($rangeType !== null) { + break; + } + } + $this->rangeType = $rangeType === null ? static::getDefaultReservedRangeType() : $rangeType; + } + + return $this->rangeType; + } + + /** + * Create an IPv6 representation of this address (in 6to4 notation). + * + * @return \IPLib\Address\IPv6 + */ + public function toIPv6() + { + $myBytes = $this->getBytes(); + + return IPv6::parseString('2002:' . sprintf('%02x', $myBytes[0]) . sprintf('%02x', $myBytes[1]) . ':' . sprintf('%02x', $myBytes[2]) . sprintf('%02x', $myBytes[3]) . '::'); + } + + /** + * Create an IPv6 representation of this address (in IPv6 IPv4-mapped notation). + * + * @return \IPLib\Address\IPv6 + * + * @since 1.11.0 + */ + public function toIPv6IPv4Mapped() + { + return IPv6::fromBytes(array_merge(array(0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0xff, 0xff), $this->getBytes())); + } + + /** + * {@inheritdoc} + * + * @see \IPLib\Address\AddressInterface::getComparableString() + */ + public function getComparableString() + { + if ($this->comparableString === null) { + $chunks = array(); + foreach ($this->getBytes() as $byte) { + $chunks[] = sprintf('%03d', $byte); + } + $this->comparableString = implode('.', $chunks); + } + + return $this->comparableString; + } + + /** + * {@inheritdoc} + * + * @see \IPLib\Address\AddressInterface::matches() + */ + public function matches(RangeInterface $range) + { + return $range->contains($this); + } + + /** + * {@inheritdoc} + * + * @see \IPLib\Address\AddressInterface::getAddressAtOffset() + */ + public function getAddressAtOffset($n) + { + if (!is_int($n)) { + return null; + } + + $boundary = 256; + $mod = $n; + $bytes = $this->getBytes(); + for ($i = count($bytes) - 1; $i >= 0; $i--) { + $tmp = ($bytes[$i] + $mod) % $boundary; + $mod = (int) floor(($bytes[$i] + $mod) / $boundary); + if ($tmp < 0) { + $tmp += $boundary; + } + + $bytes[$i] = $tmp; + } + + if ($mod !== 0) { + return null; + } + + return static::fromBytes($bytes); + } + + /** + * {@inheritdoc} + * + * @see \IPLib\Address\AddressInterface::getNextAddress() + */ + public function getNextAddress() + { + return $this->getAddressAtOffset(1); + } + + /** + * {@inheritdoc} + * + * @see \IPLib\Address\AddressInterface::getPreviousAddress() + */ + public function getPreviousAddress() + { + return $this->getAddressAtOffset(-1); + } + + /** + * {@inheritdoc} + * + * @see \IPLib\Address\AddressInterface::getReverseDNSLookupName() + */ + public function getReverseDNSLookupName() + { + return implode( + '.', + array_reverse($this->getBytes()) + ) . '.in-addr.arpa'; + } + + /** + * {@inheritdoc} + * + * @see \IPLib\Address\AddressInterface::shift() + */ + public function shift($bits) + { + $bits = (int) $bits; + if ($bits === 0) { + return $this; + } + $absBits = abs($bits); + if ($absBits >= 32) { + return new self('0.0.0.0'); + } + $pad = str_repeat('0', $absBits); + $paddedBits = $this->getBits(); + if ($bits > 0) { + $paddedBits = $pad . substr($paddedBits, 0, -$bits); + } else { + $paddedBits = substr($paddedBits, $absBits) . $pad; + } + $bytes = array_map('bindec', str_split($paddedBits, 8)); + + return new static(implode('.', $bytes)); + } + + /** + * {@inheritdoc} + * + * @see \IPLib\Address\AddressInterface::add() + */ + public function add(AddressInterface $other) + { + if (!$other instanceof self) { + return null; + } + $myBytes = $this->getBytes(); + $otherBytes = $other->getBytes(); + $sum = array_fill(0, 4, 0); + $carry = 0; + for ($index = 3; $index >= 0; $index--) { + $byte = $myBytes[$index] + $otherBytes[$index] + $carry; + if ($byte > 0xFF) { + $carry = $byte >> 8; + $byte &= 0xFF; + } else { + $carry = 0; + } + $sum[$index] = $byte; + } + if ($carry !== 0) { + return null; + } + + return new static(implode('.', $sum)); + } +} diff --git a/3rdparty/mlocati/ip-lib/src/Address/IPv6.php b/3rdparty/mlocati/ip-lib/src/Address/IPv6.php new file mode 100644 index 00000000..ad7e2a7f --- /dev/null +++ b/3rdparty/mlocati/ip-lib/src/Address/IPv6.php @@ -0,0 +1,666 @@ +longAddress = $longAddress; + $this->shortAddress = null; + $this->bytes = null; + $this->words = null; + $this->rangeType = null; + } + + /** + * {@inheritdoc} + * + * @see \IPLib\Address\AddressInterface::__toString() + */ + public function __toString() + { + return $this->toString(); + } + + /** + * {@inheritdoc} + * + * @see \IPLib\Address\AddressInterface::getNumberOfBits() + */ + public static function getNumberOfBits() + { + return 128; + } + + /** + * @deprecated since 1.17.0: use the parseString() method instead. + * For upgrading: + * - if $mayIncludePort is true, use the ParseStringFlag::MAY_INCLUDE_PORT flag + * - if $mayIncludeZoneID is true, use the ParseStringFlag::MAY_INCLUDE_ZONEID flag + * + * @param string|mixed $address + * @param bool $mayIncludePort + * @param bool $mayIncludeZoneID + * + * @return static|null + * + * @see \IPLib\Address\IPv6::parseString() + * @since 1.1.0 added the $mayIncludePort argument + * @since 1.3.0 added the $mayIncludeZoneID argument + */ + public static function fromString($address, $mayIncludePort = true, $mayIncludeZoneID = true) + { + return static::parseString($address, 0 | ($mayIncludePort ? ParseStringFlag::MAY_INCLUDE_PORT : 0) | ($mayIncludeZoneID ? ParseStringFlag::MAY_INCLUDE_ZONEID : 0)); + } + + /** + * Parse a string and returns an IPv6 instance if the string is valid, or null otherwise. + * + * @param string|mixed $address the address to parse + * @param int $flags A combination or zero or more flags + * + * @return static|null + * + * @see \IPLib\ParseStringFlag + * @since 1.17.0 + */ + public static function parseString($address, $flags = 0) + { + if (!is_string($address)) { + return null; + } + $matches = null; + $flags = (int) $flags; + if ($flags & ParseStringFlag::ADDRESS_MAYBE_RDNS) { + if (preg_match('/^([0-9a-f](?:\.[0-9a-f]){31})\.ip6\.arpa\.?/i', $address, $matches)) { + $nibbles = array_reverse(explode('.', $matches[1])); + $quibbles = array(); + foreach (array_chunk($nibbles, 4) as $n) { + $quibbles[] = implode('', $n); + } + $address = implode(':', $quibbles); + } + } + $result = null; + if (is_string($address) && strpos($address, ':') !== false && strpos($address, ':::') === false) { + if ($flags & ParseStringFlag::MAY_INCLUDE_PORT && $address[0] === '[' && preg_match('/^\[(.+)]:\d+$/', $address, $matches)) { + $address = $matches[1]; + } + if ($flags & ParseStringFlag::MAY_INCLUDE_ZONEID) { + $percentagePos = strpos($address, '%'); + if ($percentagePos > 0) { + $address = substr($address, 0, $percentagePos); + } + } + if (preg_match('/^((?:[0-9a-f]*:+)+)(\d{1,3}\.\d{1,3}\.\d{1,3}\.\d{1,3})$/i', $address, $matches)) { + $address6 = static::parseString($matches[1] . '0:0'); + if ($address6 !== null) { + $address4 = IPv4::parseString($matches[2]); + if ($address4 !== null) { + $bytes4 = $address4->getBytes(); + $address6->longAddress = substr($address6->longAddress, 0, -9) . sprintf('%02x%02x:%02x%02x', $bytes4[0], $bytes4[1], $bytes4[2], $bytes4[3]); + $result = $address6; + } + } + } else { + if (strpos($address, '::') === false) { + $chunks = explode(':', $address); + } else { + $chunks = array(); + $parts = explode('::', $address); + if (count($parts) === 2) { + $before = ($parts[0] === '') ? array() : explode(':', $parts[0]); + $after = ($parts[1] === '') ? array() : explode(':', $parts[1]); + $missing = 8 - count($before) - count($after); + if ($missing >= 0) { + $chunks = $before; + if ($missing !== 0) { + $chunks = array_merge($chunks, array_fill(0, $missing, '0')); + } + $chunks = array_merge($chunks, $after); + } + } + } + if (count($chunks) === 8) { + $nums = array_map( + function ($chunk) { + return preg_match('/^[0-9A-Fa-f]{1,4}$/', $chunk) ? hexdec($chunk) : false; + }, + $chunks + ); + if (!in_array(false, $nums, true)) { + $longAddress = implode( + ':', + array_map( + function ($num) { + return sprintf('%04x', $num); + }, + $nums + ) + ); + $result = new static($longAddress); + } + } + } + } + + return $result; + } + + /** + * Parse an array of bytes and returns an IPv6 instance if the array is valid, or null otherwise. + * + * @param int[]|array $bytes + * + * @return static|null + */ + public static function fromBytes(array $bytes) + { + $result = null; + if (count($bytes) === 16) { + $address = ''; + for ($i = 0; $i < 16; $i++) { + if ($i !== 0 && $i % 2 === 0) { + $address .= ':'; + } + $byte = $bytes[$i]; + if (is_int($byte) && $byte >= 0 && $byte <= 255) { + $address .= sprintf('%02x', $byte); + } else { + $address = null; + break; + } + } + if ($address !== null) { + $result = new static($address); + } + } + + return $result; + } + + /** + * Parse an array of words and returns an IPv6 instance if the array is valid, or null otherwise. + * + * @param int[]|array $words + * + * @return static|null + */ + public static function fromWords(array $words) + { + $result = null; + if (count($words) === 8) { + $chunks = array(); + for ($i = 0; $i < 8; $i++) { + $word = $words[$i]; + if (is_int($word) && $word >= 0 && $word <= 0xffff) { + $chunks[] = sprintf('%04x', $word); + } else { + $chunks = null; + break; + } + } + if ($chunks !== null) { + $result = new static(implode(':', $chunks)); + } + } + + return $result; + } + + /** + * {@inheritdoc} + * + * @see \IPLib\Address\AddressInterface::toString() + */ + public function toString($long = false) + { + if ($long) { + $result = $this->longAddress; + } else { + if ($this->shortAddress === null) { + if (strpos($this->longAddress, '0000:0000:0000:0000:0000:ffff:') === 0) { + $lastBytes = array_slice($this->getBytes(), -4); + $this->shortAddress = '::ffff:' . implode('.', $lastBytes); + } else { + $chunks = array_map( + function ($word) { + return dechex($word); + }, + $this->getWords() + ); + $shortAddress = implode(':', $chunks); + $matches = null; + for ($i = 8; $i > 1; $i--) { + $search = '(?:^|:)' . rtrim(str_repeat('0:', $i), ':') . '(?:$|:)'; + if (preg_match('/^(.*?)' . $search . '(.*)$/', $shortAddress, $matches)) { + $shortAddress = $matches[1] . '::' . $matches[2]; + break; + } + } + $this->shortAddress = $shortAddress; + } + } + $result = $this->shortAddress; + } + + return $result; + } + + /** + * {@inheritdoc} + * + * @see \IPLib\Address\AddressInterface::getBytes() + */ + public function getBytes() + { + if ($this->bytes === null) { + $bytes = array(); + foreach ($this->getWords() as $word) { + $bytes[] = $word >> 8; + $bytes[] = $word & 0xff; + } + $this->bytes = $bytes; + } + + return $this->bytes; + } + + /** + * {@inheritdoc} + * + * @see \IPLib\Address\AddressInterface::getBits() + */ + public function getBits() + { + $parts = array(); + foreach ($this->getBytes() as $byte) { + $parts[] = sprintf('%08b', $byte); + } + + return implode('', $parts); + } + + /** + * Get the word list of the IP address. + * + * @return int[] + */ + public function getWords() + { + if ($this->words === null) { + $this->words = array_map( + function ($chunk) { + return hexdec($chunk); + }, + explode(':', $this->longAddress) + ); + } + + return $this->words; + } + + /** + * {@inheritdoc} + * + * @see \IPLib\Address\AddressInterface::getAddressType() + */ + public function getAddressType() + { + return Type::T_IPv6; + } + + /** + * {@inheritdoc} + * + * @see \IPLib\Address\AddressInterface::getDefaultReservedRangeType() + */ + public static function getDefaultReservedRangeType() + { + return RangeType::T_RESERVED; + } + + /** + * {@inheritdoc} + * + * @see \IPLib\Address\AddressInterface::getReservedRanges() + */ + public static function getReservedRanges() + { + if (self::$reservedRanges === null) { + $reservedRanges = array(); + foreach (array( + // RFC 4291 + '::/128' => array(RangeType::T_UNSPECIFIED), + // RFC 4291 + '::1/128' => array(RangeType::T_LOOPBACK), + // RFC 4291 + '100::/8' => array(RangeType::T_DISCARD, array('100::/64' => RangeType::T_DISCARDONLY)), + //'2002::/16' => array(RangeType::), + // RFC 4291 + '2000::/3' => array(RangeType::T_PUBLIC), + // RFC 4193 + 'fc00::/7' => array(RangeType::T_PRIVATENETWORK), + // RFC 4291 + 'fe80::/10' => array(RangeType::T_LINKLOCAL_UNICAST), + // RFC 4291 + 'ff00::/8' => array(RangeType::T_MULTICAST), + // RFC 4291 + //'::/8' => array(RangeType::T_RESERVED), + // RFC 4048 + //'200::/7' => array(RangeType::T_RESERVED), + // RFC 4291 + //'400::/6' => array(RangeType::T_RESERVED), + // RFC 4291 + //'800::/5' => array(RangeType::T_RESERVED), + // RFC 4291 + //'1000::/4' => array(RangeType::T_RESERVED), + // RFC 4291 + //'4000::/3' => array(RangeType::T_RESERVED), + // RFC 4291 + //'6000::/3' => array(RangeType::T_RESERVED), + // RFC 4291 + //'8000::/3' => array(RangeType::T_RESERVED), + // RFC 4291 + //'a000::/3' => array(RangeType::T_RESERVED), + // RFC 4291 + //'c000::/3' => array(RangeType::T_RESERVED), + // RFC 4291 + //'e000::/4' => array(RangeType::T_RESERVED), + // RFC 4291 + //'f000::/5' => array(RangeType::T_RESERVED), + // RFC 4291 + //'f800::/6' => array(RangeType::T_RESERVED), + // RFC 4291 + //'fe00::/9' => array(RangeType::T_RESERVED), + // RFC 3879 + //'fec0::/10' => array(RangeType::T_RESERVED), + ) as $range => $data) { + $exceptions = array(); + if (isset($data[1])) { + foreach ($data[1] as $exceptionRange => $exceptionType) { + $exceptions[] = new AssignedRange(Subnet::parseString($exceptionRange), $exceptionType); + } + } + $reservedRanges[] = new AssignedRange(Subnet::parseString($range), $data[0], $exceptions); + } + self::$reservedRanges = $reservedRanges; + } + + return self::$reservedRanges; + } + + /** + * {@inheritdoc} + * + * @see \IPLib\Address\AddressInterface::getRangeType() + */ + public function getRangeType() + { + if ($this->rangeType === null) { + $ipv4 = $this->toIPv4(); + if ($ipv4 !== null) { + $this->rangeType = $ipv4->getRangeType(); + } else { + $rangeType = null; + foreach (static::getReservedRanges() as $reservedRange) { + $rangeType = $reservedRange->getAddressType($this); + if ($rangeType !== null) { + break; + } + } + $this->rangeType = $rangeType === null ? static::getDefaultReservedRangeType() : $rangeType; + } + } + + return $this->rangeType; + } + + /** + * Create an IPv4 representation of this address (if possible, otherwise returns null). + * + * @return \IPLib\Address\IPv4|null + */ + public function toIPv4() + { + if (strpos($this->longAddress, '2002:') === 0) { + // 6to4 + return IPv4::fromBytes(array_slice($this->getBytes(), 2, 4)); + } + if (strpos($this->longAddress, '0000:0000:0000:0000:0000:ffff:') === 0) { + // IPv4-mapped IPv6 addresses + return IPv4::fromBytes(array_slice($this->getBytes(), -4)); + } + + return null; + } + + /** + * Render this IPv6 address in the "mixed" IPv6 (first 12 bytes) + IPv4 (last 4 bytes) mixed syntax. + * + * @param bool $ipV6Long render the IPv6 part in "long" format? + * @param bool $ipV4Long render the IPv4 part in "long" format? + * + * @return string + * + * @example '::13.1.68.3' + * @example '0000:0000:0000:0000:0000:0000:13.1.68.3' when $ipV6Long is true + * @example '::013.001.068.003' when $ipV4Long is true + * @example '0000:0000:0000:0000:0000:0000:013.001.068.003' when $ipV6Long and $ipV4Long are true + * + * @see https://tools.ietf.org/html/rfc4291#section-2.2 point 3. + * @since 1.9.0 + */ + public function toMixedIPv6IPv4String($ipV6Long = false, $ipV4Long = false) + { + $myBytes = $this->getBytes(); + $ipv6Bytes = array_merge(array_slice($myBytes, 0, 12), array(0xff, 0xff, 0xff, 0xff)); + $ipv6String = static::fromBytes($ipv6Bytes)->toString($ipV6Long); + $ipv4Bytes = array_slice($myBytes, 12, 4); + $ipv4String = IPv4::fromBytes($ipv4Bytes)->toString($ipV4Long); + + return preg_replace('/((ffff:ffff)|(\d+(\.\d+){3}))$/i', $ipv4String, $ipv6String); + } + + /** + * {@inheritdoc} + * + * @see \IPLib\Address\AddressInterface::getComparableString() + */ + public function getComparableString() + { + return $this->longAddress; + } + + /** + * {@inheritdoc} + * + * @see \IPLib\Address\AddressInterface::matches() + */ + public function matches(RangeInterface $range) + { + return $range->contains($this); + } + + /** + * {@inheritdoc} + * + * @see \IPLib\Address\AddressInterface::getAddressAtOffset() + */ + public function getAddressAtOffset($n) + { + if (!is_int($n)) { + return null; + } + + $boundary = 0x10000; + $mod = $n; + $words = $this->getWords(); + for ($i = count($words) - 1; $i >= 0; $i--) { + $tmp = ($words[$i] + $mod) % $boundary; + $mod = (int) floor(($words[$i] + $mod) / $boundary); + if ($tmp < 0) { + $tmp += $boundary; + } + + $words[$i] = $tmp; + } + + if ($mod !== 0) { + return null; + } + + return static::fromWords($words); + } + + /** + * {@inheritdoc} + * + * @see \IPLib\Address\AddressInterface::getNextAddress() + */ + public function getNextAddress() + { + return $this->getAddressAtOffset(1); + } + + /** + * {@inheritdoc} + * + * @see \IPLib\Address\AddressInterface::getPreviousAddress() + */ + public function getPreviousAddress() + { + return $this->getAddressAtOffset(-1); + } + + /** + * {@inheritdoc} + * + * @see \IPLib\Address\AddressInterface::getReverseDNSLookupName() + */ + public function getReverseDNSLookupName() + { + return implode( + '.', + array_reverse(str_split(str_replace(':', '', $this->toString(true)), 1)) + ) . '.ip6.arpa'; + } + + /** + * {@inheritdoc} + * + * @see \IPLib\Address\AddressInterface::shift() + */ + public function shift($bits) + { + $bits = (int) $bits; + if ($bits === 0) { + return $this; + } + $absBits = abs($bits); + if ($absBits >= 128) { + return new self('0000:0000:0000:0000:0000:0000:0000:0000'); + } + $pad = str_repeat('0', $absBits); + $paddedBits = $this->getBits(); + if ($bits > 0) { + $paddedBits = $pad . substr($paddedBits, 0, -$bits); + } else { + $paddedBits = substr($paddedBits, $absBits) . $pad; + } + $bytes = array_map('bindec', str_split($paddedBits, 16)); + + return static::fromWords($bytes); + } + + /** + * {@inheritdoc} + * + * @see \IPLib\Address\AddressInterface::add() + */ + public function add(AddressInterface $other) + { + if (!$other instanceof self) { + return null; + } + $myWords = $this->getWords(); + $otherWords = $other->getWords(); + $sum = array_fill(0, 7, 0); + $carry = 0; + for ($index = 7; $index >= 0; $index--) { + $word = $myWords[$index] + $otherWords[$index] + $carry; + if ($word > 0xFFFF) { + $carry = $word >> 16; + $word &= 0xFFFF; + } else { + $carry = 0; + } + $sum[$index] = $word; + } + if ($carry !== 0) { + return null; + } + + return static::fromWords($sum); + } +} diff --git a/3rdparty/mlocati/ip-lib/src/Address/Type.php b/3rdparty/mlocati/ip-lib/src/Address/Type.php new file mode 100644 index 00000000..17488f38 --- /dev/null +++ b/3rdparty/mlocati/ip-lib/src/Address/Type.php @@ -0,0 +1,44 @@ +getNumberOfBits())); + } + $numberOfBits = $from->getNumberOfBits(); + if ($to->getNumberOfBits() !== $numberOfBits) { + return null; + } + $calculator = new RangesFromBoundaryCalculator($numberOfBits); + + return $calculator->getRanges($from, $to); + } + + /** + * @param \IPLib\Address\AddressInterface|null $from + * @param \IPLib\Address\AddressInterface|null $to + * + * @return \IPLib\Range\RangeInterface|null + * + * @since 1.2.0 + */ + protected static function rangeFromBoundaryAddresses($from = null, $to = null) + { + if (!$from instanceof AddressInterface && !$to instanceof AddressInterface) { + $result = null; + } elseif (!$to instanceof AddressInterface) { + $result = Range\Single::fromAddress($from); + } elseif (!$from instanceof AddressInterface) { + $result = Range\Single::fromAddress($to); + } else { + $result = null; + $addressType = $from->getAddressType(); + if ($addressType === $to->getAddressType()) { + $cmp = strcmp($from->getComparableString(), $to->getComparableString()); + if ($cmp === 0) { + $result = Range\Single::fromAddress($from); + } else { + if ($cmp > 0) { + list($from, $to) = array($to, $from); + } + $fromBytes = $from->getBytes(); + $toBytes = $to->getBytes(); + $numBytes = count($fromBytes); + $sameBits = 0; + for ($byteIndex = 0; $byteIndex < $numBytes; $byteIndex++) { + $fromByte = $fromBytes[$byteIndex]; + $toByte = $toBytes[$byteIndex]; + if ($fromByte === $toByte) { + $sameBits += 8; + } else { + $differentBitsInByte = decbin($fromByte ^ $toByte); + $sameBits += 8 - strlen($differentBitsInByte); + break; + } + } + $result = static::parseRangeString($from->toString() . '/' . (string) $sameBits); + } + } + } + + return $result; + } + + /** + * @param string|\IPLib\Address\AddressInterface $from + * @param string|\IPLib\Address\AddressInterface $to + * @param int $flags + * + * @return \IPLib\Address\AddressInterface[]|null[]|false[] + */ + private static function parseBoundaries($from, $to, $flags = 0) + { + $result = array(); + foreach (array('from', 'to') as $param) { + $value = $$param; + if (!($value instanceof AddressInterface)) { + $value = (string) $value; + if ($value === '') { + $value = null; + } else { + $value = static::parseAddressString($value, $flags); + if ($value === null) { + $value = false; + } + } + } + $result[] = $value; + } + if ($result[0] && $result[1] && strcmp($result[0]->getComparableString(), $result[1]->getComparableString()) > 0) { + $result = array($result[1], $result[0]); + } + + return $result; + } +} diff --git a/3rdparty/mlocati/ip-lib/src/ParseStringFlag.php b/3rdparty/mlocati/ip-lib/src/ParseStringFlag.php new file mode 100644 index 00000000..860256e7 --- /dev/null +++ b/3rdparty/mlocati/ip-lib/src/ParseStringFlag.php @@ -0,0 +1,79 @@ + 5.0.0.1 + * @example 5.256 => 5.0.1.0 + * @example 5.0.256 => 5.0.1.0 + * @example 123456789 => 7.91.205.21 + */ + const IPV4_MAYBE_NON_DECIMAL = 4; + + /** + * Use this flag if IPv4 subnet ranges may be in compact form. + * + * @example 127/24 => 127.0.0.0/24 + * @example 10/8 => 10.0.0.0/8 + * @example 10/24 => 10.0.0.0/24 + * @example 10.10.10/24 => 10.10.10.0/24 + * + * @var int + */ + const IPV4SUBNET_MAYBE_COMPACT = 8; + + /** + * Use this flag if IPv4 addresses may be in non quad-dotted notation. + * This notation is accepted by the implementation of inet_aton and inet_addr of the libc implementation of GNU, Windows and Mac (but not Musl), but not by inet_pton and ip2long. + * + * @var int + * + * @example 5.1 => 5.0.0.1 + * @example 5.256 => 5.0.1.0 + * @example 5.0.256 => 5.0.1.0 + * @example 123456789 => 7.91.205.21 + * + * @see https://man7.org/linux/man-pages/man3/inet_addr.3.html#DESCRIPTION + * @see https://www.freebsd.org/cgi/man.cgi?query=inet_net&sektion=3&apropos=0&manpath=FreeBSD+12.2-RELEASE+and+Ports#end + * @see http://git.musl-libc.org/cgit/musl/tree/src/network/inet_aton.c?h=v1.2.2 + */ + const IPV4ADDRESS_MAYBE_NON_QUAD_DOTTED = 16; + + /** + * Use this flag if you want to accept parsing IPv4/IPv6 addresses in Reverse DNS Lookup Address format. + * + * @var int + * + * @since 1.18.0 + * + * @example 140.13.12.10.in-addr.arpa => 10.12.13.140 + * @example b.a.9.8.7.6.5.0.4.0.0.0.3.0.0.0.2.0.0.0.1.0.0.0.0.0.0.0.1.2.3.4.ip6.arpa => 4321:0:1:2:3:4:567:89ab + */ + const ADDRESS_MAYBE_RDNS = 32; +} diff --git a/3rdparty/mlocati/ip-lib/src/Range/AbstractRange.php b/3rdparty/mlocati/ip-lib/src/Range/AbstractRange.php new file mode 100644 index 00000000..24783ca5 --- /dev/null +++ b/3rdparty/mlocati/ip-lib/src/Range/AbstractRange.php @@ -0,0 +1,170 @@ +rangeType === null) { + $addressType = $this->getAddressType(); + if ($addressType === AddressType::T_IPv6 && Subnet::get6to4()->containsRange($this)) { + $this->rangeType = Factory::getRangeFromBoundaries($this->fromAddress->toIPv4(), $this->toAddress->toIPv4())->getRangeType(); + } else { + switch ($addressType) { + case AddressType::T_IPv4: + $defaultType = IPv4::getDefaultReservedRangeType(); + $reservedRanges = IPv4::getReservedRanges(); + break; + case AddressType::T_IPv6: + $defaultType = IPv6::getDefaultReservedRangeType(); + $reservedRanges = IPv6::getReservedRanges(); + break; + default: + throw new \Exception('@todo'); // @codeCoverageIgnore + } + $rangeType = null; + foreach ($reservedRanges as $reservedRange) { + $rangeType = $reservedRange->getRangeType($this); + if ($rangeType !== null) { + break; + } + } + $this->rangeType = $rangeType === null ? $defaultType : $rangeType; + } + } + + return $this->rangeType === false ? null : $this->rangeType; + } + + /** + * {@inheritdoc} + * + * @see \IPLib\Range\RangeInterface::getAddressAtOffset() + */ + public function getAddressAtOffset($n) + { + if (!is_int($n)) { + return null; + } + + $address = null; + if ($n >= 0) { + $start = Factory::parseAddressString($this->getComparableStartString()); + $address = $start->getAddressAtOffset($n); + } else { + $end = Factory::parseAddressString($this->getComparableEndString()); + $address = $end->getAddressAtOffset($n + 1); + } + + if ($address === null) { + return null; + } + + return $this->contains($address) ? $address : null; + } + + /** + * {@inheritdoc} + * + * @see \IPLib\Range\RangeInterface::contains() + */ + public function contains(AddressInterface $address) + { + $result = false; + if ($address->getAddressType() === $this->getAddressType()) { + $cmp = $address->getComparableString(); + $from = $this->getComparableStartString(); + if ($cmp >= $from) { + $to = $this->getComparableEndString(); + if ($cmp <= $to) { + $result = true; + } + } + } + + return $result; + } + + /** + * {@inheritdoc} + * + * @see \IPLib\Range\RangeInterface::containsRange() + */ + public function containsRange(RangeInterface $range) + { + $result = false; + if ($range->getAddressType() === $this->getAddressType()) { + $myStart = $this->getComparableStartString(); + $itsStart = $range->getComparableStartString(); + if ($itsStart >= $myStart) { + $myEnd = $this->getComparableEndString(); + $itsEnd = $range->getComparableEndString(); + if ($itsEnd <= $myEnd) { + $result = true; + } + } + } + + return $result; + } + + /** + * {@inheritdoc} + * + * @see \IPLib\Range\RangeInterface::split() + */ + public function split($networkPrefix, $forceSubnet = false) + { + $networkPrefix = (int) $networkPrefix; + $myNetworkPrefix = $this->getNetworkPrefix(); + if ($networkPrefix === $myNetworkPrefix) { + return array( + $forceSubnet ? $this->asSubnet() : $this, + ); + } + if ($networkPrefix < $myNetworkPrefix) { + throw new OutOfBoundsException("The value of the \$networkPrefix parameter can't be smaller than the network prefix of the range ({$myNetworkPrefix})"); + } + $startIp = $this->getStartAddress(); + $maxPrefix = $startIp::getNumberOfBits(); + if ($networkPrefix > $maxPrefix) { + throw new OutOfBoundsException("The value of the \$networkPrefix parameter can't be larger than the maximum network prefix of the range ({$maxPrefix})"); + } + if ($startIp instanceof IPv4) { + $one = IPv4::fromBytes(array(0, 0, 0, 1)); + } elseif ($startIp instanceof IPv6) { + $one = IPv6::fromBytes(array(0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1)); + } + $delta = $one->shift($networkPrefix - $maxPrefix); + $result = array(); + while (true) { + $range = Subnet::parseString("{$startIp}/{$networkPrefix}"); + if (!$forceSubnet && $this instanceof Pattern) { + $range = $range->asPattern() ?: $range; + } + $result[] = $range; + $startIp = $startIp->add($delta); + if ($startIp === null || !$this->contains($startIp)) { + break; + } + } + + return $result; + } +} diff --git a/3rdparty/mlocati/ip-lib/src/Range/Pattern.php b/3rdparty/mlocati/ip-lib/src/Range/Pattern.php new file mode 100644 index 00000000..f7afeebb --- /dev/null +++ b/3rdparty/mlocati/ip-lib/src/Range/Pattern.php @@ -0,0 +1,324 @@ +fromAddress = $fromAddress; + $this->toAddress = $toAddress; + $this->asterisksCount = $asterisksCount; + } + + /** + * {@inheritdoc} + * + * @see \IPLib\Range\RangeInterface::__toString() + */ + public function __toString() + { + return $this->toString(); + } + + /** + * @deprecated since 1.17.0: use the parseString() method instead. + * For upgrading: + * - if $supportNonDecimalIPv4 is true, use the ParseStringFlag::IPV4_MAYBE_NON_DECIMAL flag + * + * @param string|mixed $range + * @param bool $supportNonDecimalIPv4 + * + * @return static|null + * + * @see \IPLib\Range\Pattern::parseString() + * @since 1.10.0 added the $supportNonDecimalIPv4 argument + */ + public static function fromString($range, $supportNonDecimalIPv4 = false) + { + return static::parseString($range, ParseStringFlag::MAY_INCLUDE_PORT | ParseStringFlag::MAY_INCLUDE_ZONEID | ($supportNonDecimalIPv4 ? ParseStringFlag::IPV4_MAYBE_NON_DECIMAL : 0)); + } + + /** + * Try get the range instance starting from its string representation. + * + * @param string|mixed $range + * @param int $flags A combination or zero or more flags + * + * @return static|null + * + * @since 1.17.0 + * @see \IPLib\ParseStringFlag + */ + public static function parseString($range, $flags = 0) + { + if (!is_string($range) || strpos($range, '*') === false) { + return null; + } + if ($range === '*.*.*.*') { + return new static(IPv4::parseString('0.0.0.0'), IPv4::parseString('255.255.255.255'), 4); + } + if ($range === '*:*:*:*:*:*:*:*') { + return new static(IPv6::parseString('::'), IPv6::parseString('ffff:ffff:ffff:ffff:ffff:ffff:ffff:ffff'), 8); + } + $matches = null; + if (strpos($range, '.') !== false && preg_match('/^[^*]+((?:\.\*)+)$/', $range, $matches)) { + $asterisksCount = strlen($matches[1]) >> 1; + if ($asterisksCount > 0) { + $missingDots = 3 - substr_count($range, '.'); + if ($missingDots > 0) { + $range .= str_repeat('.*', $missingDots); + $asterisksCount += $missingDots; + } + } + $fromAddress = IPv4::parseString(str_replace('*', '0', $range), $flags); + if ($fromAddress === null) { + return null; + } + $fixedBytes = array_slice($fromAddress->getBytes(), 0, -$asterisksCount); + $otherBytes = array_fill(0, $asterisksCount, 255); + $toAddress = IPv4::fromBytes(array_merge($fixedBytes, $otherBytes)); + + return new static($fromAddress, $toAddress, $asterisksCount); + } + if (strpos($range, ':') !== false && preg_match('/^[^*]+((?::\*)+)$/', $range, $matches)) { + $asterisksCount = strlen($matches[1]) >> 1; + $fromAddress = IPv6::parseString(str_replace('*', '0', $range)); + if ($fromAddress === null) { + return null; + } + $fixedWords = array_slice($fromAddress->getWords(), 0, -$asterisksCount); + $otherWords = array_fill(0, $asterisksCount, 0xffff); + $toAddress = IPv6::fromWords(array_merge($fixedWords, $otherWords)); + + return new static($fromAddress, $toAddress, $asterisksCount); + } + + return null; + } + + /** + * {@inheritdoc} + * + * @see \IPLib\Range\RangeInterface::toString() + */ + public function toString($long = false) + { + if ($this->asterisksCount === 0) { + return $this->fromAddress->toString($long); + } + switch (true) { + case $this->fromAddress instanceof \IPLib\Address\IPv4: + $chunks = explode('.', $this->fromAddress->toString()); + $chunks = array_slice($chunks, 0, -$this->asterisksCount); + $chunks = array_pad($chunks, 4, '*'); + $result = implode('.', $chunks); + break; + case $this->fromAddress instanceof \IPLib\Address\IPv6: + if ($long) { + $chunks = explode(':', $this->fromAddress->toString(true)); + $chunks = array_slice($chunks, 0, -$this->asterisksCount); + $chunks = array_pad($chunks, 8, '*'); + $result = implode(':', $chunks); + } elseif ($this->asterisksCount === 8) { + $result = '*:*:*:*:*:*:*:*'; + } else { + $bytes = $this->toAddress->getBytes(); + $bytes = array_slice($bytes, 0, -$this->asterisksCount * 2); + $bytes = array_pad($bytes, 16, 1); + $address = IPv6::fromBytes($bytes); + $before = substr($address->toString(false), 0, -strlen(':101') * $this->asterisksCount); + $result = $before . str_repeat(':*', $this->asterisksCount); + } + break; + default: + throw new \Exception('@todo'); // @codeCoverageIgnore + } + + return $result; + } + + /** + * {@inheritdoc} + * + * @see \IPLib\Range\RangeInterface::getAddressType() + */ + public function getAddressType() + { + return $this->fromAddress->getAddressType(); + } + + /** + * {@inheritdoc} + * + * @see \IPLib\Range\RangeInterface::getStartAddress() + */ + public function getStartAddress() + { + return $this->fromAddress; + } + + /** + * {@inheritdoc} + * + * @see \IPLib\Range\RangeInterface::getEndAddress() + */ + public function getEndAddress() + { + return $this->toAddress; + } + + /** + * {@inheritdoc} + * + * @see \IPLib\Range\RangeInterface::getComparableStartString() + */ + public function getComparableStartString() + { + return $this->fromAddress->getComparableString(); + } + + /** + * {@inheritdoc} + * + * @see \IPLib\Range\RangeInterface::getComparableEndString() + */ + public function getComparableEndString() + { + return $this->toAddress->getComparableString(); + } + + /** + * {@inheritdoc} + * + * @see \IPLib\Range\RangeInterface::asSubnet() + * @since 1.8.0 + */ + public function asSubnet() + { + return new Subnet($this->getStartAddress(), $this->getEndAddress(), $this->getNetworkPrefix()); + } + + /** + * {@inheritdoc} + * + * @see \IPLib\Range\RangeInterface::asPattern() + */ + public function asPattern() + { + return $this; + } + + /** + * {@inheritdoc} + * + * @see \IPLib\Range\RangeInterface::getSubnetMask() + */ + public function getSubnetMask() + { + if ($this->getAddressType() !== AddressType::T_IPv4) { + return null; + } + switch ($this->asterisksCount) { + case 0: + $bytes = array(255, 255, 255, 255); + break; + case 4: + $bytes = array(0, 0, 0, 0); + break; + default: + $bytes = array_pad(array_fill(0, 4 - $this->asterisksCount, 255), 4, 0); + break; + } + + return IPv4::fromBytes($bytes); + } + + /** + * {@inheritdoc} + * + * @see \IPLib\Range\RangeInterface::getReverseDNSLookupName() + */ + public function getReverseDNSLookupName() + { + return $this->asterisksCount === 0 ? array($this->getStartAddress()->getReverseDNSLookupName()) : $this->asSubnet()->getReverseDNSLookupName(); + } + + /** + * {@inheritdoc} + * + * @see \IPLib\Range\RangeInterface::getSize() + */ + public function getSize() + { + $fromAddress = $this->fromAddress; + $maxPrefix = $fromAddress::getNumberOfBits(); + $prefix = $this->getNetworkPrefix(); + + return pow(2, ($maxPrefix - $prefix)); + } + + /** + * {@inheritdoc} + * + * @see \IPLib\Range\RangeInterface::getNetworkPrefix() + */ + public function getNetworkPrefix() + { + switch ($this->getAddressType()) { + case AddressType::T_IPv4: + return 8 * (4 - $this->asterisksCount); + case AddressType::T_IPv6: + return 16 * (8 - $this->asterisksCount); + } + } +} diff --git a/3rdparty/mlocati/ip-lib/src/Range/RangeInterface.php b/3rdparty/mlocati/ip-lib/src/Range/RangeInterface.php new file mode 100644 index 00000000..4c7c02d6 --- /dev/null +++ b/3rdparty/mlocati/ip-lib/src/Range/RangeInterface.php @@ -0,0 +1,186 @@ +address = $address; + } + + /** + * {@inheritdoc} + * + * @see \IPLib\Range\RangeInterface::__toString() + */ + public function __toString() + { + return $this->address->__toString(); + } + + /** + * @deprecated since 1.17.0: use the parseString() method instead. + * For upgrading: + * - if $supportNonDecimalIPv4 is true, use the ParseStringFlag::IPV4_MAYBE_NON_DECIMAL flag + * + * @param string|mixed $range + * @param bool $supportNonDecimalIPv4 + * + * @return static|null + * + * @see \IPLib\Range\Single::parseString() + * @since 1.10.0 added the $supportNonDecimalIPv4 argument + */ + public static function fromString($range, $supportNonDecimalIPv4 = false) + { + return static::parseString($range, ParseStringFlag::MAY_INCLUDE_PORT | ParseStringFlag::MAY_INCLUDE_ZONEID | ($supportNonDecimalIPv4 ? ParseStringFlag::IPV4_MAYBE_NON_DECIMAL : 0)); + } + + /** + * Try get the range instance starting from its string representation. + * + * @param string|mixed $range + * @param int $flags A combination or zero or more flags + * + * @return static|null + * + * @see \IPLib\ParseStringFlag + * @since 1.17.0 + */ + public static function parseString($range, $flags = 0) + { + $result = null; + $flags = (int) $flags; + $address = Factory::parseAddressString($range, $flags); + if ($address !== null) { + $result = new static($address); + } + + return $result; + } + + /** + * Create the range instance starting from an address instance. + * + * @param \IPLib\Address\AddressInterface $address + * + * @return static + * + * @since 1.2.0 + */ + public static function fromAddress(AddressInterface $address) + { + return new static($address); + } + + /** + * {@inheritdoc} + * + * @see \IPLib\Range\RangeInterface::toString() + */ + public function toString($long = false) + { + return $this->address->toString($long); + } + + /** + * {@inheritdoc} + * + * @see \IPLib\Range\RangeInterface::getAddressType() + */ + public function getAddressType() + { + return $this->address->getAddressType(); + } + + /** + * {@inheritdoc} + * + * @see \IPLib\Range\RangeInterface::getRangeType() + */ + public function getRangeType() + { + return $this->address->getRangeType(); + } + + /** + * {@inheritdoc} + * + * @see \IPLib\Range\RangeInterface::contains() + */ + public function contains(AddressInterface $address) + { + $result = false; + if ($address->getAddressType() === $this->getAddressType()) { + if ($address->toString(false) === $this->address->toString(false)) { + $result = true; + } + } + + return $result; + } + + /** + * {@inheritdoc} + * + * @see \IPLib\Range\RangeInterface::getStartAddress() + */ + public function getStartAddress() + { + return $this->address; + } + + /** + * {@inheritdoc} + * + * @see \IPLib\Range\RangeInterface::getEndAddress() + */ + public function getEndAddress() + { + return $this->address; + } + + /** + * {@inheritdoc} + * + * @see \IPLib\Range\RangeInterface::getComparableStartString() + */ + public function getComparableStartString() + { + return $this->address->getComparableString(); + } + + /** + * {@inheritdoc} + * + * @see \IPLib\Range\RangeInterface::getComparableEndString() + */ + public function getComparableEndString() + { + return $this->address->getComparableString(); + } + + /** + * {@inheritdoc} + * + * @see \IPLib\Range\RangeInterface::asSubnet() + */ + public function asSubnet() + { + return new Subnet($this->address, $this->address, $this->getNetworkPrefix()); + } + + /** + * {@inheritdoc} + * + * @see \IPLib\Range\RangeInterface::asPattern() + */ + public function asPattern() + { + return new Pattern($this->address, $this->address, 0); + } + + /** + * {@inheritdoc} + * + * @see \IPLib\Range\RangeInterface::getSubnetMask() + */ + public function getSubnetMask() + { + if ($this->getAddressType() !== AddressType::T_IPv4) { + return null; + } + + return IPv4::fromBytes(array(255, 255, 255, 255)); + } + + /** + * {@inheritdoc} + * + * @see \IPLib\Range\RangeInterface::getReverseDNSLookupName() + */ + public function getReverseDNSLookupName() + { + return array($this->getStartAddress()->getReverseDNSLookupName()); + } + + /** + * {@inheritdoc} + * + * @see \IPLib\Range\RangeInterface::getSize() + */ + public function getSize() + { + return 1; + } + + /** + * {@inheritdoc} + * + * @see \IPLib\Range\RangeInterface::getNetworkPrefix() + */ + public function getNetworkPrefix() + { + switch ($this->getAddressType()) { + case AddressType::T_IPv4: + return 32; + case AddressType::T_IPv6: + return 128; + } + } +} diff --git a/3rdparty/mlocati/ip-lib/src/Range/Subnet.php b/3rdparty/mlocati/ip-lib/src/Range/Subnet.php new file mode 100644 index 00000000..9c00a101 --- /dev/null +++ b/3rdparty/mlocati/ip-lib/src/Range/Subnet.php @@ -0,0 +1,353 @@ +fromAddress = $fromAddress; + $this->toAddress = $toAddress; + $this->networkPrefix = $networkPrefix; + } + + /** + * {@inheritdoc} + * + * @see \IPLib\Range\RangeInterface::__toString() + */ + public function __toString() + { + return $this->toString(); + } + + /** + * @deprecated since 1.17.0: use the parseString() method instead. + * For upgrading: + * - if $supportNonDecimalIPv4 is true, use the ParseStringFlag::IPV4_MAYBE_NON_DECIMAL flag + * + * @param string|mixed $range + * @param bool $supportNonDecimalIPv4 + * + * @return static|null + * + * @see \IPLib\Range\Subnet::parseString() + * @since 1.10.0 added the $supportNonDecimalIPv4 argument + */ + public static function fromString($range, $supportNonDecimalIPv4 = false) + { + return static::parseString($range, ParseStringFlag::MAY_INCLUDE_PORT | ParseStringFlag::MAY_INCLUDE_ZONEID | ($supportNonDecimalIPv4 ? ParseStringFlag::IPV4_MAYBE_NON_DECIMAL : 0)); + } + + /** + * Try get the range instance starting from its string representation. + * + * @param string|mixed $range + * @param int $flags A combination or zero or more flags + * + * @return static|null + * + * @see \IPLib\ParseStringFlag + * @since 1.17.0 + */ + public static function parseString($range, $flags = 0) + { + if (!is_string($range)) { + return null; + } + $parts = explode('/', $range); + if (count($parts) !== 2) { + return null; + } + $flags = (int) $flags; + if (strpos($parts[0], ':') === false && $flags & ParseStringFlag::IPV4SUBNET_MAYBE_COMPACT) { + $missingDots = 3 - substr_count($parts[0], '.'); + if ($missingDots > 0) { + $parts[0] .= str_repeat('.0', $missingDots); + } + } + $address = Factory::parseAddressString($parts[0], $flags); + if ($address === null) { + return null; + } + if (!preg_match('/^[0-9]{1,9}$/', $parts[1])) { + return null; + } + $networkPrefix = (int) $parts[1]; + $addressBytes = $address->getBytes(); + $totalBytes = count($addressBytes); + $numDifferentBits = $totalBytes * 8 - $networkPrefix; + if ($numDifferentBits < 0) { + return null; + } + $numSameBytes = $networkPrefix >> 3; + $sameBytes = array_slice($addressBytes, 0, $numSameBytes); + $differentBytesStart = ($totalBytes === $numSameBytes) ? array() : array_fill(0, $totalBytes - $numSameBytes, 0); + $differentBytesEnd = ($totalBytes === $numSameBytes) ? array() : array_fill(0, $totalBytes - $numSameBytes, 255); + $startSameBits = $networkPrefix % 8; + if ($startSameBits !== 0) { + $varyingByte = $addressBytes[$numSameBytes]; + $differentBytesStart[0] = $varyingByte & bindec(str_pad(str_repeat('1', $startSameBits), 8, '0', STR_PAD_RIGHT)); + $differentBytesEnd[0] = $differentBytesStart[0] + bindec(str_repeat('1', 8 - $startSameBits)); + } + + return new static( + Factory::addressFromBytes(array_merge($sameBytes, $differentBytesStart)), + Factory::addressFromBytes(array_merge($sameBytes, $differentBytesEnd)), + $networkPrefix + ); + } + + /** + * {@inheritdoc} + * + * @see \IPLib\Range\RangeInterface::toString() + */ + public function toString($long = false) + { + return $this->fromAddress->toString($long) . '/' . $this->networkPrefix; + } + + /** + * {@inheritdoc} + * + * @see \IPLib\Range\RangeInterface::getAddressType() + */ + public function getAddressType() + { + return $this->fromAddress->getAddressType(); + } + + /** + * {@inheritdoc} + * + * @see \IPLib\Range\RangeInterface::getStartAddress() + */ + public function getStartAddress() + { + return $this->fromAddress; + } + + /** + * {@inheritdoc} + * + * @see \IPLib\Range\RangeInterface::getEndAddress() + */ + public function getEndAddress() + { + return $this->toAddress; + } + + /** + * {@inheritdoc} + * + * @see \IPLib\Range\RangeInterface::getComparableStartString() + */ + public function getComparableStartString() + { + return $this->fromAddress->getComparableString(); + } + + /** + * {@inheritdoc} + * + * @see \IPLib\Range\RangeInterface::getComparableEndString() + */ + public function getComparableEndString() + { + return $this->toAddress->getComparableString(); + } + + /** + * {@inheritdoc} + * + * @see \IPLib\Range\RangeInterface::asSubnet() + */ + public function asSubnet() + { + return $this; + } + + /** + * {@inheritdoc} + * + * @see \IPLib\Range\RangeInterface::asPattern() + * @since 1.8.0 + */ + public function asPattern() + { + $address = $this->getStartAddress(); + $networkPrefix = $this->getNetworkPrefix(); + switch ($address->getAddressType()) { + case AddressType::T_IPv4: + return $networkPrefix % 8 === 0 ? new Pattern($address, $address, 4 - $networkPrefix / 8) : null; + case AddressType::T_IPv6: + return $networkPrefix % 16 === 0 ? new Pattern($address, $address, 8 - $networkPrefix / 16) : null; + } + } + + /** + * Get the 6to4 address IPv6 address range. + * + * @return self + * + * @since 1.5.0 + */ + public static function get6to4() + { + if (self::$sixToFour === null) { + self::$sixToFour = self::parseString('2002::/16'); + } + + return self::$sixToFour; + } + + /** + * {@inheritdoc} + * + * @see \IPLib\Range\RangeInterface::getNetworkPrefix() + * @since 1.7.0 + */ + public function getNetworkPrefix() + { + return $this->networkPrefix; + } + + /** + * {@inheritdoc} + * + * @see \IPLib\Range\RangeInterface::getSubnetMask() + */ + public function getSubnetMask() + { + if ($this->getAddressType() !== AddressType::T_IPv4) { + return null; + } + $bytes = array(); + $prefix = $this->getNetworkPrefix(); + while ($prefix >= 8) { + $bytes[] = 255; + $prefix -= 8; + } + if ($prefix !== 0) { + $bytes[] = bindec(str_pad(str_repeat('1', $prefix), 8, '0')); + } + $bytes = array_pad($bytes, 4, 0); + + return IPv4::fromBytes($bytes); + } + + /** + * {@inheritdoc} + * + * @see \IPLib\Range\RangeInterface::getReverseDNSLookupName() + */ + public function getReverseDNSLookupName() + { + switch ($this->getAddressType()) { + case AddressType::T_IPv4: + $unitSize = 8; // bytes + $maxUnits = 4; + $isHex = false; + $rxUnit = '\d+'; + break; + case AddressType::T_IPv6: + $unitSize = 4; // nibbles + $maxUnits = 32; + $isHex = true; + $rxUnit = '[0-9A-Fa-f]'; + break; + } + $totBits = $unitSize * $maxUnits; + $prefixUnits = (int) ($this->networkPrefix / $unitSize); + $extraBits = ($totBits - $this->networkPrefix) % $unitSize; + if ($extraBits !== 0) { + $prefixUnits += 1; + } + $numVariants = 1 << $extraBits; + $result = array(); + $unitsToRemove = $maxUnits - $prefixUnits; + $initialPointer = preg_replace("/^(({$rxUnit})\.){{$unitsToRemove}}/", '', $this->getStartAddress()->getReverseDNSLookupName()); + $chunks = explode('.', $initialPointer, 2); + for ($index = 0; $index < $numVariants; $index++) { + if ($index !== 0) { + $chunks[0] = $isHex ? dechex(1 + hexdec($chunks[0])) : (string) (1 + (int) $chunks[0]); + } + $result[] = implode('.', $chunks); + } + + return $result; + } + + /** + * {@inheritdoc} + * + * @see \IPLib\Range\RangeInterface::getSize() + */ + public function getSize() + { + $fromAddress = $this->fromAddress; + $maxPrefix = $fromAddress::getNumberOfBits(); + $prefix = $this->getNetworkPrefix(); + + return pow(2, ($maxPrefix - $prefix)); + } +} diff --git a/3rdparty/mlocati/ip-lib/src/Range/Type.php b/3rdparty/mlocati/ip-lib/src/Range/Type.php new file mode 100644 index 00000000..b2ba8eb7 --- /dev/null +++ b/3rdparty/mlocati/ip-lib/src/Range/Type.php @@ -0,0 +1,152 @@ +toSameLength($a, $b); + + return $a < $b ? -1 : ($a > $b ? 1 : 0); + } + + /** + * Add 1 to a non-negative integer represented in binary form. + * + * @param string $value + * + * @return string + */ + public function increment($value) + { + $lastZeroIndex = strrpos($value, '0'); + if ($lastZeroIndex === false) { + return '1' . str_repeat('0', strlen($value)); + } + + return ltrim(substr($value, 0, $lastZeroIndex), '0') . '1' . str_repeat('0', strlen($value) - $lastZeroIndex - 1); + } + + /** + * Calculate the bitwise AND of two non-negative integers represented in binary form. + * + * @param string $operand1 + * @param string $operand2 + * + * @return string + */ + public function andX($operand1, $operand2) + { + $operand1 = $this->reduce($operand1); + $operand2 = $this->reduce($operand2); + $numBits = min(strlen($operand1), strlen($operand2)); + $operand1 = substr(str_pad($operand1, $numBits, '0', STR_PAD_LEFT), -$numBits); + $operand2 = substr(str_pad($operand2, $numBits, '0', STR_PAD_LEFT), -$numBits); + $result = ''; + for ($index = 0; $index < $numBits; $index++) { + $result .= $operand1[$index] === '1' && $operand2[$index] === '1' ? '1' : '0'; + } + + return $this->reduce($result); + } + + /** + * Calculate the bitwise OR of two non-negative integers represented in binary form. + * + * @param string $operand1 + * @param string $operand2 + * + * @return string + */ + public function orX($operand1, $operand2) + { + list($operand1, $operand2, $numBits) = $this->toSameLength($operand1, $operand2); + $result = ''; + for ($index = 0; $index < $numBits; $index++) { + $result .= $operand1[$index] === '1' || $operand2[$index] === '1' ? '1' : '0'; + } + + return $result; + } + + /** + * Zero-padding of two non-negative integers represented in binary form, so that they have the same length. + * + * @param string $num1 + * @param string $num2 + * + * @return string[],int[] The first array element is $num1 (padded), the first array element is $num2 (padded), the third array element is the number of bits + */ + private function toSameLength($num1, $num2) + { + $num1 = $this->reduce($num1); + $num2 = $this->reduce($num2); + $numBits = max(strlen($num1), strlen($num2)); + + return array( + str_pad($num1, $numBits, '0', STR_PAD_LEFT), + str_pad($num2, $numBits, '0', STR_PAD_LEFT), + $numBits, + ); + } +} diff --git a/3rdparty/mlocati/ip-lib/src/Service/RangesFromBoundaryCalculator.php b/3rdparty/mlocati/ip-lib/src/Service/RangesFromBoundaryCalculator.php new file mode 100644 index 00000000..7a127e79 --- /dev/null +++ b/3rdparty/mlocati/ip-lib/src/Service/RangesFromBoundaryCalculator.php @@ -0,0 +1,168 @@ +math = new BinaryMath(); + $this->setNumBits($numBits); + } + + /** + * Calculate the subnets describing all (and only all) the addresses between two boundaries. + * + * @param \IPLib\Address\AddressInterface $from + * @param \IPLib\Address\AddressInterface $to + * + * @return \IPLib\Range\Subnet[]|null return NULL if the two addresses have an invalid number of bits (that is, different from the one passed to the constructor of this class) + */ + public function getRanges(AddressInterface $from, AddressInterface $to) + { + if ($from->getNumberOfBits() !== $this->numBits || $to->getNumberOfBits() !== $this->numBits) { + return null; + } + if ($from->getComparableString() > $to->getComparableString()) { + list($from, $to) = array($to, $from); + } + $result = array(); + $this->calculate($this->math->reduce($from->getBits()), $this->math->reduce($to->getBits()), $this->numBits, $result); + + return $result; + } + + /** + * Set the number of bits used to represent addresses (32 for IPv4, 128 for IPv6). + * + * @param int $numBits + */ + private function setNumBits($numBits) + { + $numBits = (int) $numBits; + $masks = array(); + $unmasks = array(); + for ($bit = 0; $bit < $numBits; $bit++) { + $masks[$bit] = str_repeat('1', $numBits - $bit) . str_repeat('0', $bit); + $unmasks[$bit] = $bit === 0 ? '0' : str_repeat('1', $bit); + } + $this->numBits = $numBits; + $this->masks = $masks; + $this->unmasks = $unmasks; + } + + /** + * Calculate the subnets. + * + * @param string $start the start address (represented in reduced bit form) + * @param string $end the end address (represented in reduced bit form) + * @param int $position the number of bits in the mask we are comparing at this cycle + * @param \IPLib\Range\Subnet[] $result found ranges will be added to this variable + */ + private function calculate($start, $end, $position, array &$result) + { + if ($start === $end) { + $result[] = $this->subnetFromBits($start, $this->numBits); + + return; + } + for ($index = $position - 1; $index >= 0; $index--) { + $startMasked = $this->math->andX($start, $this->masks[$index]); + $endMasked = $this->math->andX($end, $this->masks[$index]); + if ($startMasked !== $endMasked) { + $position = $index; + break; + } + } + if ($startMasked === $start && $this->math->andX($this->math->increment($end), $this->unmasks[$position]) === '0') { + $result[] = $this->subnetFromBits($start, $this->numBits - 1 - $position); + + return; + } + $middleAddress = $this->math->orX($start, $this->unmasks[$position]); + $this->calculate($start, $middleAddress, $position, $result); + $this->calculate($this->math->increment($middleAddress), $end, $position, $result); + } + + /** + * Create an address instance starting from its bits. + * + * @param string $bits the bits of the address (represented in reduced bit form) + * + * @return \IPLib\Address\AddressInterface + */ + private function addressFromBits($bits) + { + $bits = str_pad($bits, $this->numBits, '0', STR_PAD_LEFT); + $bytes = array(); + foreach (explode("\n", trim(chunk_split($bits, 8, "\n"))) as $byteBits) { + $bytes[] = bindec($byteBits); + } + + return Factory::addressFromBytes($bytes); + } + + /** + * Create an range instance starting from the bits if the address and the length of the network prefix. + * + * @param string $bits the bits of the address (represented in reduced bit form) + * @param int $networkPrefix the length of the network prefix + * + * @return \IPLib\Range\Subnet + */ + private function subnetFromBits($bits, $networkPrefix) + { + $startAddress = $this->addressFromBits($bits); + $numOnes = $this->numBits - $networkPrefix; + if ($numOnes === 0) { + return new Subnet($startAddress, $startAddress, $networkPrefix); + } + $endAddress = $this->addressFromBits(substr($bits, 0, -$numOnes) . str_repeat('1', $numOnes)); + + return new Subnet($startAddress, $endAddress, $networkPrefix); + } +} diff --git a/3rdparty/mlocati/ip-lib/src/Service/UnsignedIntegerMath.php b/3rdparty/mlocati/ip-lib/src/Service/UnsignedIntegerMath.php new file mode 100644 index 00000000..3179f746 --- /dev/null +++ b/3rdparty/mlocati/ip-lib/src/Service/UnsignedIntegerMath.php @@ -0,0 +1,171 @@ +getBytesFromDecimal($m[1], $numBytes); + } + } else { + if (preg_match('/^0[Xx]0*([0-9A-Fa-f]+)$/', $value, $m)) { + return $this->getBytesFromHexadecimal($m[1], $numBytes); + } + if (preg_match('/^0+([0-7]*)$/', $value, $m)) { + return $this->getBytesFromOctal($m[1], $numBytes); + } + if (preg_match('/^[1-9][0-9]*$/', $value)) { + return $this->getBytesFromDecimal($value, $numBytes); + } + } + + // Not a valid number + return null; + } + + /** + * @return int + */ + protected function getMaxSignedInt() + { + return PHP_INT_MAX; + } + + /** + * @param string $value never zero-length, never extra leading zeroes + * @param int $numBytes + * + * @return int[]|null + */ + private function getBytesFromBits($value, $numBytes) + { + $valueLength = strlen($value); + if ($valueLength > $numBytes << 3) { + // overflow + return null; + } + $remainderBits = $valueLength % 8; + if ($remainderBits !== 0) { + $value = str_pad($value, $valueLength + 8 - $remainderBits, '0', STR_PAD_LEFT); + } + $bytes = array_map('bindec', str_split($value, 8)); + + return array_pad($bytes, -$numBytes, 0); + } + + /** + * @param string $value may be zero-length, never extra leading zeroes + * @param int $numBytes + * + * @return int[]|null + */ + private function getBytesFromOctal($value, $numBytes) + { + if ($value === '') { + return array_fill(0, $numBytes, 0); + } + $bits = implode( + '', + array_map( + function ($octalDigit) { + return str_pad(decbin(octdec($octalDigit)), 3, '0', STR_PAD_LEFT); + }, + str_split($value, 1) + ) + ); + $bits = ltrim($bits, '0'); + + return $bits === '' ? array_fill(0, $numBytes, 0) : static::getBytesFromBits($bits, $numBytes); + } + + /** + * @param string $value never zero-length, never extra leading zeroes + * @param int $numBytes + * + * @return int[]|null + */ + private function getBytesFromDecimal($value, $numBytes) + { + $valueLength = strlen($value); + $maxSignedIntLength = strlen((string) $this->getMaxSignedInt()); + if ($valueLength < $maxSignedIntLength) { + return $this->getBytesFromBits(decbin((int) $value), $numBytes); + } + // Divide by two, so that we have 1 less bit + $carry = 0; + $halfValue = ltrim( + implode( + '', + array_map( + function ($digit) use (&$carry) { + $number = $carry + (int) $digit; + $carry = ($number % 2) * 10; + + return (string) $number >> 1; + }, + str_split($value, 1) + ) + ), + '0' + ); + $halfValueBytes = $this->getBytesFromDecimal($halfValue, $numBytes); + if ($halfValueBytes === null) { + return null; + } + $carry = $carry === 0 ? 0 : 1; + $result = array_fill(0, $numBytes, 0); + for ($index = $numBytes - 1; $index >= 0; $index--) { + $byte = $carry + ($halfValueBytes[$index] << 1); + if ($byte <= 0xFF) { + $carry = 0; + } else { + $carry = ($byte & ~0xFF) >> 8; + $byte -= 0x100; + } + $result[$index] = $byte; + } + if ($carry !== 0) { + // Overflow + return null; + } + + return $result; + } + + /** + * @param string $value never zero-length, never extra leading zeroes + * @param int $numBytes + * + * @return int[]|null + */ + private function getBytesFromHexadecimal($value, $numBytes) + { + $valueLength = strlen($value); + if ($valueLength > $numBytes << 1) { + // overflow + return null; + } + $value = str_pad($value, $valueLength + $valueLength % 2, '0', STR_PAD_LEFT); + $bytes = array_map('hexdec', str_split($value, 2)); + + return array_pad($bytes, -$numBytes, 0); + } +} diff --git a/3rdparty/mtdowling/jmespath.php/LICENSE b/3rdparty/mtdowling/jmespath.php/LICENSE new file mode 100644 index 00000000..5c970a42 --- /dev/null +++ b/3rdparty/mtdowling/jmespath.php/LICENSE @@ -0,0 +1,19 @@ +Copyright (c) 2014 Michael Dowling, https://github.com/mtdowling + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in +all copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +THE SOFTWARE. diff --git a/3rdparty/mtdowling/jmespath.php/src/AstRuntime.php b/3rdparty/mtdowling/jmespath.php/src/AstRuntime.php new file mode 100644 index 00000000..f5620be0 --- /dev/null +++ b/3rdparty/mtdowling/jmespath.php/src/AstRuntime.php @@ -0,0 +1,47 @@ +interpreter = new TreeInterpreter($fnDispatcher); + $this->parser = $parser ?: new Parser(); + } + + /** + * Returns data from the provided input that matches a given JMESPath + * expression. + * + * @param string $expression JMESPath expression to evaluate + * @param mixed $data Data to search. This data should be data that + * is similar to data returned from json_decode + * using associative arrays rather than objects. + * + * @return mixed Returns the matching data or null + */ + public function __invoke($expression, $data) + { + if (!isset($this->cache[$expression])) { + // Clear the AST cache when it hits 1024 entries + if (++$this->cachedCount > 1024) { + $this->cache = []; + $this->cachedCount = 0; + } + $this->cache[$expression] = $this->parser->parse($expression); + } + + return $this->interpreter->visit($this->cache[$expression], $data); + } +} diff --git a/3rdparty/mtdowling/jmespath.php/src/CompilerRuntime.php b/3rdparty/mtdowling/jmespath.php/src/CompilerRuntime.php new file mode 100644 index 00000000..b85f68e7 --- /dev/null +++ b/3rdparty/mtdowling/jmespath.php/src/CompilerRuntime.php @@ -0,0 +1,83 @@ +parser = $parser ?: new Parser(); + $this->compiler = new TreeCompiler(); + $dir = $dir ?: sys_get_temp_dir(); + + if (!is_dir($dir) && !mkdir($dir, 0755, true)) { + throw new \RuntimeException("Unable to create cache directory: $dir"); + } + + $this->cacheDir = realpath($dir); + $this->interpreter = new TreeInterpreter(); + } + + /** + * Returns data from the provided input that matches a given JMESPath + * expression. + * + * @param string $expression JMESPath expression to evaluate + * @param mixed $data Data to search. This data should be data that + * is similar to data returned from json_decode + * using associative arrays rather than objects. + * + * @return mixed Returns the matching data or null + * @throws \RuntimeException + */ + public function __invoke($expression, $data) + { + $functionName = 'jmespath_' . md5($expression); + + if (!function_exists($functionName)) { + $filename = "{$this->cacheDir}/{$functionName}.php"; + if (!file_exists($filename)) { + $this->compile($filename, $expression, $functionName); + } + require $filename; + } + + return $functionName($this->interpreter, $data); + } + + private function compile($filename, $expression, $functionName) + { + $code = $this->compiler->visit( + $this->parser->parse($expression), + $functionName, + $expression + ); + + if (!file_put_contents($filename, $code)) { + throw new \RuntimeException(sprintf( + 'Unable to write the compiled PHP code to: %s (%s)', + $filename, + var_export(error_get_last(), true) + )); + } + } +} diff --git a/3rdparty/mtdowling/jmespath.php/src/DebugRuntime.php b/3rdparty/mtdowling/jmespath.php/src/DebugRuntime.php new file mode 100644 index 00000000..40525617 --- /dev/null +++ b/3rdparty/mtdowling/jmespath.php/src/DebugRuntime.php @@ -0,0 +1,109 @@ +runtime = $runtime; + $this->out = $output ?: STDOUT; + $this->lexer = new Lexer(); + $this->parser = new Parser($this->lexer); + } + + public function __invoke($expression, $data) + { + if ($this->runtime instanceof CompilerRuntime) { + return $this->debugCompiled($expression, $data); + } + + return $this->debugInterpreted($expression, $data); + } + + private function debugInterpreted($expression, $data) + { + return $this->debugCallback( + function () use ($expression, $data) { + $runtime = $this->runtime; + return $runtime($expression, $data); + }, + $expression, + $data + ); + } + + private function debugCompiled($expression, $data) + { + $result = $this->debugCallback( + function () use ($expression, $data) { + $runtime = $this->runtime; + return $runtime($expression, $data); + }, + $expression, + $data + ); + $this->dumpCompiledCode($expression); + + return $result; + } + + private function dumpTokens($expression) + { + $lexer = new Lexer(); + fwrite($this->out, "Tokens\n======\n\n"); + $tokens = $lexer->tokenize($expression); + + foreach ($tokens as $t) { + fprintf( + $this->out, + "%3d %-13s %s\n", $t['pos'], $t['type'], + json_encode($t['value']) + ); + } + + fwrite($this->out, "\n"); + } + + private function dumpAst($expression) + { + $parser = new Parser(); + $ast = $parser->parse($expression); + fwrite($this->out, "AST\n========\n\n"); + fwrite($this->out, json_encode($ast, JSON_PRETTY_PRINT) . "\n"); + } + + private function dumpCompiledCode($expression) + { + fwrite($this->out, "Code\n========\n\n"); + $dir = sys_get_temp_dir(); + $hash = md5($expression); + $functionName = "jmespath_{$hash}"; + $filename = "{$dir}/{$functionName}.php"; + fwrite($this->out, "File: {$filename}\n\n"); + fprintf($this->out, file_get_contents($filename)); + } + + private function debugCallback(callable $debugFn, $expression, $data) + { + fprintf($this->out, "Expression\n==========\n\n%s\n\n", $expression); + $this->dumpTokens($expression); + $this->dumpAst($expression); + fprintf($this->out, "\nData\n====\n\n%s\n\n", json_encode($data, JSON_PRETTY_PRINT)); + $startTime = microtime(true); + $result = $debugFn(); + $total = microtime(true) - $startTime; + fprintf($this->out, "\nResult\n======\n\n%s\n\n", json_encode($result, JSON_PRETTY_PRINT)); + fwrite($this->out, "Time\n====\n\n"); + fprintf($this->out, "Total time: %f ms\n\n", $total); + + return $result; + } +} diff --git a/3rdparty/mtdowling/jmespath.php/src/Env.php b/3rdparty/mtdowling/jmespath.php/src/Env.php new file mode 100644 index 00000000..b22cf25f --- /dev/null +++ b/3rdparty/mtdowling/jmespath.php/src/Env.php @@ -0,0 +1,91 @@ +{'fn_' . $fn}($args); + } + + private function fn_abs(array $args) + { + $this->validate('abs', $args, [['number']]); + return abs($args[0]); + } + + private function fn_avg(array $args) + { + $this->validate('avg', $args, [['array']]); + $sum = $this->reduce('avg:0', $args[0], ['number'], function ($a, $b) { + return Utils::add($a, $b); + }); + return $args[0] ? ($sum / count($args[0])) : null; + } + + private function fn_ceil(array $args) + { + $this->validate('ceil', $args, [['number']]); + return ceil($args[0]); + } + + private function fn_contains(array $args) + { + $this->validate('contains', $args, [['string', 'array'], ['any']]); + if (is_array($args[0])) { + return in_array($args[1], $args[0]); + } elseif (is_string($args[1])) { + return mb_strpos($args[0], $args[1], 0, 'UTF-8') !== false; + } else { + return null; + } + } + + private function fn_ends_with(array $args) + { + $this->validate('ends_with', $args, [['string'], ['string']]); + list($search, $suffix) = $args; + return $suffix === '' || mb_substr($search, -mb_strlen($suffix, 'UTF-8'), null, 'UTF-8') === $suffix; + } + + private function fn_floor(array $args) + { + $this->validate('floor', $args, [['number']]); + return floor($args[0]); + } + + private function fn_not_null(array $args) + { + if (!$args) { + throw new \RuntimeException( + "not_null() expects 1 or more arguments, 0 were provided" + ); + } + + return array_reduce($args, function ($carry, $item) { + return $carry !== null ? $carry : $item; + }); + } + + private function fn_join(array $args) + { + $this->validate('join', $args, [['string'], ['array']]); + $fn = function ($a, $b, $i) use ($args) { + return $i ? ($a . $args[0] . $b) : $b; + }; + return $this->reduce('join:0', $args[1], ['string'], $fn); + } + + private function fn_keys(array $args) + { + $this->validate('keys', $args, [['object']]); + return array_keys((array) $args[0]); + } + + private function fn_length(array $args) + { + $this->validate('length', $args, [['string', 'array', 'object']]); + return is_string($args[0]) ? mb_strlen($args[0], 'UTF-8') : count((array) $args[0]); + } + + private function fn_max(array $args) + { + $this->validate('max', $args, [['array']]); + $fn = function ($a, $b) { + return $a >= $b ? $a : $b; + }; + return $this->reduce('max:0', $args[0], ['number', 'string'], $fn); + } + + private function fn_max_by(array $args) + { + $this->validate('max_by', $args, [['array'], ['expression']]); + $expr = $this->wrapExpression('max_by:1', $args[1], ['number', 'string']); + $fn = function ($carry, $item, $index) use ($expr) { + return $index + ? ($expr($carry) >= $expr($item) ? $carry : $item) + : $item; + }; + return $this->reduce('max_by:1', $args[0], ['any'], $fn); + } + + private function fn_min(array $args) + { + $this->validate('min', $args, [['array']]); + $fn = function ($a, $b, $i) { + return $i && $a <= $b ? $a : $b; + }; + return $this->reduce('min:0', $args[0], ['number', 'string'], $fn); + } + + private function fn_min_by(array $args) + { + $this->validate('min_by', $args, [['array'], ['expression']]); + $expr = $this->wrapExpression('min_by:1', $args[1], ['number', 'string']); + $i = -1; + $fn = function ($a, $b) use ($expr, &$i) { + return ++$i ? ($expr($a) <= $expr($b) ? $a : $b) : $b; + }; + return $this->reduce('min_by:1', $args[0], ['any'], $fn); + } + + private function fn_reverse(array $args) + { + $this->validate('reverse', $args, [['array', 'string']]); + if (is_array($args[0])) { + return array_reverse($args[0]); + } elseif (is_string($args[0])) { + return strrev($args[0]); + } else { + throw new \RuntimeException('Cannot reverse provided argument'); + } + } + + private function fn_sum(array $args) + { + $this->validate('sum', $args, [['array']]); + $fn = function ($a, $b) { + return Utils::add($a, $b); + }; + return $this->reduce('sum:0', $args[0], ['number'], $fn); + } + + private function fn_sort(array $args) + { + $this->validate('sort', $args, [['array']]); + $valid = ['string', 'number']; + return Utils::stableSort($args[0], function ($a, $b) use ($valid) { + $this->validateSeq('sort:0', $valid, $a, $b); + return strnatcmp($a, $b); + }); + } + + private function fn_sort_by(array $args) + { + $this->validate('sort_by', $args, [['array'], ['expression']]); + $expr = $args[1]; + $valid = ['string', 'number']; + return Utils::stableSort( + $args[0], + function ($a, $b) use ($expr, $valid) { + $va = $expr($a); + $vb = $expr($b); + $this->validateSeq('sort_by:0', $valid, $va, $vb); + return strnatcmp($va, $vb); + } + ); + } + + private function fn_starts_with(array $args) + { + $this->validate('starts_with', $args, [['string'], ['string']]); + list($search, $prefix) = $args; + return $prefix === '' || mb_strpos($search, $prefix, 0, 'UTF-8') === 0; + } + + private function fn_type(array $args) + { + $this->validateArity('type', count($args), 1); + return Utils::type($args[0]); + } + + private function fn_to_string(array $args) + { + $this->validateArity('to_string', count($args), 1); + $v = $args[0]; + if (is_string($v)) { + return $v; + } elseif (is_object($v) + && !($v instanceof \JsonSerializable) + && method_exists($v, '__toString') + ) { + return (string) $v; + } + + return json_encode($v); + } + + private function fn_to_number(array $args) + { + $this->validateArity('to_number', count($args), 1); + $value = $args[0]; + $type = Utils::type($value); + if ($type == 'number') { + return $value; + } elseif ($type == 'string' && is_numeric($value)) { + return mb_strpos($value, '.', 0, 'UTF-8') ? (float) $value : (int) $value; + } else { + return null; + } + } + + private function fn_values(array $args) + { + $this->validate('values', $args, [['array', 'object']]); + return array_values((array) $args[0]); + } + + private function fn_merge(array $args) + { + if (!$args) { + throw new \RuntimeException( + "merge() expects 1 or more arguments, 0 were provided" + ); + } + + return call_user_func_array('array_replace', $args); + } + + private function fn_to_array(array $args) + { + $this->validate('to_array', $args, [['any']]); + + return Utils::isArray($args[0]) ? $args[0] : [$args[0]]; + } + + private function fn_map(array $args) + { + $this->validate('map', $args, [['expression'], ['any']]); + $result = []; + foreach ($args[1] as $a) { + $result[] = $args[0]($a); + } + return $result; + } + + private function typeError($from, $msg) + { + if (mb_strpos($from, ':', 0, 'UTF-8')) { + list($fn, $pos) = explode(':', $from); + throw new \RuntimeException( + sprintf('Argument %d of %s %s', $pos, $fn, $msg) + ); + } else { + throw new \RuntimeException( + sprintf('Type error: %s %s', $from, $msg) + ); + } + } + + private function validateArity($from, $given, $expected) + { + if ($given != $expected) { + $err = "%s() expects {$expected} arguments, {$given} were provided"; + throw new \RuntimeException(sprintf($err, $from)); + } + } + + private function validate($from, $args, $types = []) + { + $this->validateArity($from, count($args), count($types)); + foreach ($args as $index => $value) { + if (!isset($types[$index]) || !$types[$index]) { + continue; + } + $this->validateType("{$from}:{$index}", $value, $types[$index]); + } + } + + private function validateType($from, $value, array $types) + { + if ($types[0] == 'any' + || in_array(Utils::type($value), $types) + || ($value === [] && in_array('object', $types)) + ) { + return; + } + $msg = 'must be one of the following types: ' . implode(', ', $types) + . '. ' . Utils::type($value) . ' found'; + $this->typeError($from, $msg); + } + + /** + * Validates value A and B, ensures they both are correctly typed, and of + * the same type. + * + * @param string $from String of function:argument_position + * @param array $types Array of valid value types. + * @param mixed $a Value A + * @param mixed $b Value B + */ + private function validateSeq($from, array $types, $a, $b) + { + $ta = Utils::type($a); + $tb = Utils::type($b); + + if ($ta !== $tb) { + $msg = "encountered a type mismatch in sequence: {$ta}, {$tb}"; + $this->typeError($from, $msg); + } + + $typeMatch = ($types && $types[0] == 'any') || in_array($ta, $types); + if (!$typeMatch) { + $msg = 'encountered a type error in sequence. The argument must be ' + . 'an array of ' . implode('|', $types) . ' types. ' + . "Found {$ta}, {$tb}."; + $this->typeError($from, $msg); + } + } + + /** + * Reduces and validates an array of values to a single value using a fn. + * + * @param string $from String of function:argument_position + * @param array $values Values to reduce. + * @param array $types Array of valid value types. + * @param callable $reduce Reduce function that accepts ($carry, $item). + * + * @return mixed + */ + private function reduce($from, array $values, array $types, callable $reduce) + { + $i = -1; + return array_reduce( + $values, + function ($carry, $item) use ($from, $types, $reduce, &$i) { + if (++$i > 0) { + $this->validateSeq($from, $types, $carry, $item); + } + return $reduce($carry, $item, $i); + } + ); + } + + /** + * Validates the return values of expressions as they are applied. + * + * @param string $from Function name : position + * @param callable $expr Expression function to validate. + * @param array $types Array of acceptable return type values. + * + * @return callable Returns a wrapped function + */ + private function wrapExpression($from, callable $expr, array $types) + { + list($fn, $pos) = explode(':', $from); + $from = "The expression return value of argument {$pos} of {$fn}"; + return function ($value) use ($from, $expr, $types) { + $value = $expr($value); + $this->validateType($from, $value, $types); + return $value; + }; + } + + /** @internal Pass function name validation off to runtime */ + public function __call($name, $args) + { + $name = str_replace('fn_', '', $name); + throw new \RuntimeException("Call to undefined function {$name}"); + } +} diff --git a/3rdparty/mtdowling/jmespath.php/src/JmesPath.php b/3rdparty/mtdowling/jmespath.php/src/JmesPath.php new file mode 100644 index 00000000..d24e5160 --- /dev/null +++ b/3rdparty/mtdowling/jmespath.php/src/JmesPath.php @@ -0,0 +1,17 @@ + self::STATE_LT, + '>' => self::STATE_GT, + '=' => self::STATE_EQ, + '!' => self::STATE_NOT, + '[' => self::STATE_LBRACKET, + '|' => self::STATE_PIPE, + '&' => self::STATE_AND, + '`' => self::STATE_JSON_LITERAL, + '"' => self::STATE_QUOTED_STRING, + "'" => self::STATE_STRING_LITERAL, + '-' => self::STATE_NUMBER, + '0' => self::STATE_NUMBER, + '1' => self::STATE_NUMBER, + '2' => self::STATE_NUMBER, + '3' => self::STATE_NUMBER, + '4' => self::STATE_NUMBER, + '5' => self::STATE_NUMBER, + '6' => self::STATE_NUMBER, + '7' => self::STATE_NUMBER, + '8' => self::STATE_NUMBER, + '9' => self::STATE_NUMBER, + ' ' => self::STATE_WHITESPACE, + "\t" => self::STATE_WHITESPACE, + "\n" => self::STATE_WHITESPACE, + "\r" => self::STATE_WHITESPACE, + '.' => self::STATE_SINGLE_CHAR, + '*' => self::STATE_SINGLE_CHAR, + ']' => self::STATE_SINGLE_CHAR, + ',' => self::STATE_SINGLE_CHAR, + ':' => self::STATE_SINGLE_CHAR, + '@' => self::STATE_SINGLE_CHAR, + '(' => self::STATE_SINGLE_CHAR, + ')' => self::STATE_SINGLE_CHAR, + '{' => self::STATE_SINGLE_CHAR, + '}' => self::STATE_SINGLE_CHAR, + '_' => self::STATE_IDENTIFIER, + 'A' => self::STATE_IDENTIFIER, + 'B' => self::STATE_IDENTIFIER, + 'C' => self::STATE_IDENTIFIER, + 'D' => self::STATE_IDENTIFIER, + 'E' => self::STATE_IDENTIFIER, + 'F' => self::STATE_IDENTIFIER, + 'G' => self::STATE_IDENTIFIER, + 'H' => self::STATE_IDENTIFIER, + 'I' => self::STATE_IDENTIFIER, + 'J' => self::STATE_IDENTIFIER, + 'K' => self::STATE_IDENTIFIER, + 'L' => self::STATE_IDENTIFIER, + 'M' => self::STATE_IDENTIFIER, + 'N' => self::STATE_IDENTIFIER, + 'O' => self::STATE_IDENTIFIER, + 'P' => self::STATE_IDENTIFIER, + 'Q' => self::STATE_IDENTIFIER, + 'R' => self::STATE_IDENTIFIER, + 'S' => self::STATE_IDENTIFIER, + 'T' => self::STATE_IDENTIFIER, + 'U' => self::STATE_IDENTIFIER, + 'V' => self::STATE_IDENTIFIER, + 'W' => self::STATE_IDENTIFIER, + 'X' => self::STATE_IDENTIFIER, + 'Y' => self::STATE_IDENTIFIER, + 'Z' => self::STATE_IDENTIFIER, + 'a' => self::STATE_IDENTIFIER, + 'b' => self::STATE_IDENTIFIER, + 'c' => self::STATE_IDENTIFIER, + 'd' => self::STATE_IDENTIFIER, + 'e' => self::STATE_IDENTIFIER, + 'f' => self::STATE_IDENTIFIER, + 'g' => self::STATE_IDENTIFIER, + 'h' => self::STATE_IDENTIFIER, + 'i' => self::STATE_IDENTIFIER, + 'j' => self::STATE_IDENTIFIER, + 'k' => self::STATE_IDENTIFIER, + 'l' => self::STATE_IDENTIFIER, + 'm' => self::STATE_IDENTIFIER, + 'n' => self::STATE_IDENTIFIER, + 'o' => self::STATE_IDENTIFIER, + 'p' => self::STATE_IDENTIFIER, + 'q' => self::STATE_IDENTIFIER, + 'r' => self::STATE_IDENTIFIER, + 's' => self::STATE_IDENTIFIER, + 't' => self::STATE_IDENTIFIER, + 'u' => self::STATE_IDENTIFIER, + 'v' => self::STATE_IDENTIFIER, + 'w' => self::STATE_IDENTIFIER, + 'x' => self::STATE_IDENTIFIER, + 'y' => self::STATE_IDENTIFIER, + 'z' => self::STATE_IDENTIFIER, + ]; + + /** @var array Valid identifier characters after first character */ + private $validIdentifier = [ + 'A' => true, 'B' => true, 'C' => true, 'D' => true, 'E' => true, + 'F' => true, 'G' => true, 'H' => true, 'I' => true, 'J' => true, + 'K' => true, 'L' => true, 'M' => true, 'N' => true, 'O' => true, + 'P' => true, 'Q' => true, 'R' => true, 'S' => true, 'T' => true, + 'U' => true, 'V' => true, 'W' => true, 'X' => true, 'Y' => true, + 'Z' => true, 'a' => true, 'b' => true, 'c' => true, 'd' => true, + 'e' => true, 'f' => true, 'g' => true, 'h' => true, 'i' => true, + 'j' => true, 'k' => true, 'l' => true, 'm' => true, 'n' => true, + 'o' => true, 'p' => true, 'q' => true, 'r' => true, 's' => true, + 't' => true, 'u' => true, 'v' => true, 'w' => true, 'x' => true, + 'y' => true, 'z' => true, '_' => true, '0' => true, '1' => true, + '2' => true, '3' => true, '4' => true, '5' => true, '6' => true, + '7' => true, '8' => true, '9' => true, + ]; + + /** @var array Valid number characters after the first character */ + private $numbers = [ + '0' => true, '1' => true, '2' => true, '3' => true, '4' => true, + '5' => true, '6' => true, '7' => true, '8' => true, '9' => true + ]; + + /** @var array Map of simple single character tokens */ + private $simpleTokens = [ + '.' => self::T_DOT, + '*' => self::T_STAR, + ']' => self::T_RBRACKET, + ',' => self::T_COMMA, + ':' => self::T_COLON, + '@' => self::T_CURRENT, + '(' => self::T_LPAREN, + ')' => self::T_RPAREN, + '{' => self::T_LBRACE, + '}' => self::T_RBRACE, + ]; + + /** + * Tokenize the JMESPath expression into an array of tokens hashes that + * contain a 'type', 'value', and 'key'. + * + * @param string $input JMESPath input + * + * @return array + * @throws SyntaxErrorException + */ + public function tokenize($input) + { + $tokens = []; + + if ($input === '') { + goto eof; + } + + $chars = str_split($input); + + while (false !== ($current = current($chars))) { + + // Every character must be in the transition character table. + if (!isset(self::$transitionTable[$current])) { + $tokens[] = [ + 'type' => self::T_UNKNOWN, + 'pos' => key($chars), + 'value' => $current + ]; + next($chars); + continue; + } + + $state = self::$transitionTable[$current]; + + if ($state === self::STATE_SINGLE_CHAR) { + + // Consume simple tokens like ".", ",", "@", etc. + $tokens[] = [ + 'type' => $this->simpleTokens[$current], + 'pos' => key($chars), + 'value' => $current + ]; + next($chars); + + } elseif ($state === self::STATE_IDENTIFIER) { + + // Consume identifiers + $start = key($chars); + $buffer = ''; + do { + $buffer .= $current; + $current = next($chars); + } while ($current !== false && isset($this->validIdentifier[$current])); + $tokens[] = [ + 'type' => self::T_IDENTIFIER, + 'value' => $buffer, + 'pos' => $start + ]; + + } elseif ($state === self::STATE_WHITESPACE) { + + // Skip whitespace + next($chars); + + } elseif ($state === self::STATE_LBRACKET) { + + // Consume "[", "[?", and "[]" + $position = key($chars); + $actual = next($chars); + if ($actual === ']') { + next($chars); + $tokens[] = [ + 'type' => self::T_FLATTEN, + 'pos' => $position, + 'value' => '[]' + ]; + } elseif ($actual === '?') { + next($chars); + $tokens[] = [ + 'type' => self::T_FILTER, + 'pos' => $position, + 'value' => '[?' + ]; + } else { + $tokens[] = [ + 'type' => self::T_LBRACKET, + 'pos' => $position, + 'value' => '[' + ]; + } + + } elseif ($state === self::STATE_STRING_LITERAL) { + + // Consume raw string literals + $t = $this->inside($chars, "'", self::T_LITERAL); + $t['value'] = str_replace("\\'", "'", $t['value']); + $tokens[] = $t; + + } elseif ($state === self::STATE_PIPE) { + + // Consume pipe and OR + $tokens[] = $this->matchOr($chars, '|', '|', self::T_OR, self::T_PIPE); + + } elseif ($state == self::STATE_JSON_LITERAL) { + + // Consume JSON literals + $token = $this->inside($chars, '`', self::T_LITERAL); + if ($token['type'] === self::T_LITERAL) { + $token['value'] = str_replace('\\`', '`', $token['value']); + $token = $this->parseJson($token); + } + $tokens[] = $token; + + } elseif ($state == self::STATE_NUMBER) { + + // Consume numbers + $start = key($chars); + $buffer = ''; + do { + $buffer .= $current; + $current = next($chars); + } while ($current !== false && isset($this->numbers[$current])); + $tokens[] = [ + 'type' => self::T_NUMBER, + 'value' => (int)$buffer, + 'pos' => $start + ]; + + } elseif ($state === self::STATE_QUOTED_STRING) { + + // Consume quoted identifiers + $token = $this->inside($chars, '"', self::T_QUOTED_IDENTIFIER); + if ($token['type'] === self::T_QUOTED_IDENTIFIER) { + $token['value'] = '"' . $token['value'] . '"'; + $token = $this->parseJson($token); + } + $tokens[] = $token; + + } elseif ($state === self::STATE_EQ) { + + // Consume equals + $tokens[] = $this->matchOr($chars, '=', '=', self::T_COMPARATOR, self::T_UNKNOWN); + + } elseif ($state == self::STATE_AND) { + + $tokens[] = $this->matchOr($chars, '&', '&', self::T_AND, self::T_EXPREF); + + } elseif ($state === self::STATE_NOT) { + + // Consume not equal + $tokens[] = $this->matchOr($chars, '!', '=', self::T_COMPARATOR, self::T_NOT); + + } else { + + // either '<' or '>' + // Consume less than and greater than + $tokens[] = $this->matchOr($chars, $current, '=', self::T_COMPARATOR, self::T_COMPARATOR); + + } + } + + eof: + $tokens[] = [ + 'type' => self::T_EOF, + 'pos' => mb_strlen($input, 'UTF-8'), + 'value' => null + ]; + + return $tokens; + } + + /** + * Returns a token based on whether or not the next token matches the + * expected value. If it does, a token of "$type" is returned. Otherwise, + * a token of "$orElse" type is returned. + * + * @param array $chars Array of characters by reference. + * @param string $current The current character. + * @param string $expected Expected character. + * @param string $type Expected result type. + * @param string $orElse Otherwise return a token of this type. + * + * @return array Returns a conditional token. + */ + private function matchOr(array &$chars, $current, $expected, $type, $orElse) + { + if (next($chars) === $expected) { + next($chars); + return [ + 'type' => $type, + 'pos' => key($chars) - 1, + 'value' => $current . $expected + ]; + } + + return [ + 'type' => $orElse, + 'pos' => key($chars) - 1, + 'value' => $current + ]; + } + + /** + * Returns a token the is the result of consuming inside of delimiter + * characters. Escaped delimiters will be adjusted before returning a + * value. If the token is not closed, "unknown" is returned. + * + * @param array $chars Array of characters by reference. + * @param string $delim The delimiter character. + * @param string $type Token type. + * + * @return array Returns the consumed token. + */ + private function inside(array &$chars, $delim, $type) + { + $position = key($chars); + $current = next($chars); + $buffer = ''; + + while ($current !== $delim) { + if ($current === '\\') { + $buffer .= '\\'; + $current = next($chars); + } + if ($current === false) { + // Unclosed delimiter + return [ + 'type' => self::T_UNKNOWN, + 'value' => $buffer, + 'pos' => $position + ]; + } + $buffer .= $current; + $current = next($chars); + } + + next($chars); + + return ['type' => $type, 'value' => $buffer, 'pos' => $position]; + } + + /** + * Parses a JSON token or sets the token type to "unknown" on error. + * + * @param array $token Token that needs parsing. + * + * @return array Returns a token with a parsed value. + */ + private function parseJson(array $token) + { + $value = json_decode($token['value'], true); + + if ($error = json_last_error()) { + // Legacy support for elided quotes. Try to parse again by adding + // quotes around the bad input value. + $value = json_decode('"' . $token['value'] . '"', true); + if ($error = json_last_error()) { + $token['type'] = self::T_UNKNOWN; + return $token; + } + } + + $token['value'] = $value; + return $token; + } +} diff --git a/3rdparty/mtdowling/jmespath.php/src/Parser.php b/3rdparty/mtdowling/jmespath.php/src/Parser.php new file mode 100644 index 00000000..d126de56 --- /dev/null +++ b/3rdparty/mtdowling/jmespath.php/src/Parser.php @@ -0,0 +1,519 @@ + T::T_EOF]; + private static $currentNode = ['type' => T::T_CURRENT]; + + private static $bp = [ + T::T_EOF => 0, + T::T_QUOTED_IDENTIFIER => 0, + T::T_IDENTIFIER => 0, + T::T_RBRACKET => 0, + T::T_RPAREN => 0, + T::T_COMMA => 0, + T::T_RBRACE => 0, + T::T_NUMBER => 0, + T::T_CURRENT => 0, + T::T_EXPREF => 0, + T::T_COLON => 0, + T::T_PIPE => 1, + T::T_OR => 2, + T::T_AND => 3, + T::T_COMPARATOR => 5, + T::T_FLATTEN => 9, + T::T_STAR => 20, + T::T_FILTER => 21, + T::T_DOT => 40, + T::T_NOT => 45, + T::T_LBRACE => 50, + T::T_LBRACKET => 55, + T::T_LPAREN => 60, + ]; + + /** @var array Acceptable tokens after a dot token */ + private static $afterDot = [ + T::T_IDENTIFIER => true, // foo.bar + T::T_QUOTED_IDENTIFIER => true, // foo."bar" + T::T_STAR => true, // foo.* + T::T_LBRACE => true, // foo[1] + T::T_LBRACKET => true, // foo{a: 0} + T::T_FILTER => true, // foo.[?bar==10] + ]; + + /** + * @param Lexer|null $lexer Lexer used to tokenize expressions + */ + public function __construct(?Lexer $lexer = null) + { + $this->lexer = $lexer ?: new Lexer(); + } + + /** + * Parses a JMESPath expression into an AST + * + * @param string $expression JMESPath expression to compile + * + * @return array Returns an array based AST + * @throws SyntaxErrorException + */ + public function parse($expression) + { + $this->expression = $expression; + $this->tokens = $this->lexer->tokenize($expression); + $this->tpos = -1; + $this->next(); + $result = $this->expr(); + + if ($this->token['type'] === T::T_EOF) { + return $result; + } + + throw $this->syntax('Did not reach the end of the token stream'); + } + + /** + * Parses an expression while rbp < lbp. + * + * @param int $rbp Right bound precedence + * + * @return array + */ + private function expr($rbp = 0) + { + $left = $this->{"nud_{$this->token['type']}"}(); + while ($rbp < self::$bp[$this->token['type']]) { + $left = $this->{"led_{$this->token['type']}"}($left); + } + + return $left; + } + + private function nud_identifier() + { + $token = $this->token; + $this->next(); + return ['type' => 'field', 'value' => $token['value']]; + } + + private function nud_quoted_identifier() + { + $token = $this->token; + $this->next(); + $this->assertNotToken(T::T_LPAREN); + return ['type' => 'field', 'value' => $token['value']]; + } + + private function nud_current() + { + $this->next(); + return self::$currentNode; + } + + private function nud_literal() + { + $token = $this->token; + $this->next(); + return ['type' => 'literal', 'value' => $token['value']]; + } + + private function nud_expref() + { + $this->next(); + return ['type' => T::T_EXPREF, 'children' => [$this->expr(self::$bp[T::T_EXPREF])]]; + } + + private function nud_not() + { + $this->next(); + return ['type' => T::T_NOT, 'children' => [$this->expr(self::$bp[T::T_NOT])]]; + } + + private function nud_lparen() + { + $this->next(); + $result = $this->expr(0); + if ($this->token['type'] !== T::T_RPAREN) { + throw $this->syntax('Unclosed `(`'); + } + $this->next(); + return $result; + } + + private function nud_lbrace() + { + static $validKeys = [T::T_QUOTED_IDENTIFIER => true, T::T_IDENTIFIER => true]; + $this->next($validKeys); + $pairs = []; + + do { + $pairs[] = $this->parseKeyValuePair(); + if ($this->token['type'] == T::T_COMMA) { + $this->next($validKeys); + } + } while ($this->token['type'] !== T::T_RBRACE); + + $this->next(); + + return['type' => 'multi_select_hash', 'children' => $pairs]; + } + + private function nud_flatten() + { + return $this->led_flatten(self::$currentNode); + } + + private function nud_filter() + { + return $this->led_filter(self::$currentNode); + } + + private function nud_star() + { + return $this->parseWildcardObject(self::$currentNode); + } + + private function nud_lbracket() + { + $this->next(); + $type = $this->token['type']; + if ($type == T::T_NUMBER || $type == T::T_COLON) { + return $this->parseArrayIndexExpression(); + } elseif ($type == T::T_STAR && $this->lookahead() == T::T_RBRACKET) { + return $this->parseWildcardArray(); + } else { + return $this->parseMultiSelectList(); + } + } + + private function led_lbracket(array $left) + { + static $nextTypes = [T::T_NUMBER => true, T::T_COLON => true, T::T_STAR => true]; + $this->next($nextTypes); + switch ($this->token['type']) { + case T::T_NUMBER: + case T::T_COLON: + return [ + 'type' => 'subexpression', + 'children' => [$left, $this->parseArrayIndexExpression()] + ]; + default: + return $this->parseWildcardArray($left); + } + } + + private function led_flatten(array $left) + { + $this->next(); + + return [ + 'type' => 'projection', + 'from' => 'array', + 'children' => [ + ['type' => T::T_FLATTEN, 'children' => [$left]], + $this->parseProjection(self::$bp[T::T_FLATTEN]) + ] + ]; + } + + private function led_dot(array $left) + { + $this->next(self::$afterDot); + + if ($this->token['type'] == T::T_STAR) { + return $this->parseWildcardObject($left); + } + + return [ + 'type' => 'subexpression', + 'children' => [$left, $this->parseDot(self::$bp[T::T_DOT])] + ]; + } + + private function led_or(array $left) + { + $this->next(); + return [ + 'type' => T::T_OR, + 'children' => [$left, $this->expr(self::$bp[T::T_OR])] + ]; + } + + private function led_and(array $left) + { + $this->next(); + return [ + 'type' => T::T_AND, + 'children' => [$left, $this->expr(self::$bp[T::T_AND])] + ]; + } + + private function led_pipe(array $left) + { + $this->next(); + return [ + 'type' => T::T_PIPE, + 'children' => [$left, $this->expr(self::$bp[T::T_PIPE])] + ]; + } + + private function led_lparen(array $left) + { + $args = []; + $this->next(); + + while ($this->token['type'] != T::T_RPAREN) { + $args[] = $this->expr(0); + if ($this->token['type'] == T::T_COMMA) { + $this->next(); + } + } + + $this->next(); + + return [ + 'type' => 'function', + 'value' => $left['value'], + 'children' => $args + ]; + } + + private function led_filter(array $left) + { + $this->next(); + $expression = $this->expr(); + if ($this->token['type'] != T::T_RBRACKET) { + throw $this->syntax('Expected a closing rbracket for the filter'); + } + + $this->next(); + $rhs = $this->parseProjection(self::$bp[T::T_FILTER]); + + return [ + 'type' => 'projection', + 'from' => 'array', + 'children' => [ + $left ?: self::$currentNode, + [ + 'type' => 'condition', + 'children' => [$expression, $rhs] + ] + ] + ]; + } + + private function led_comparator(array $left) + { + $token = $this->token; + $this->next(); + + return [ + 'type' => T::T_COMPARATOR, + 'value' => $token['value'], + 'children' => [$left, $this->expr(self::$bp[T::T_COMPARATOR])] + ]; + } + + private function parseProjection($bp) + { + $type = $this->token['type']; + if (self::$bp[$type] < 10) { + return self::$currentNode; + } elseif ($type == T::T_DOT) { + $this->next(self::$afterDot); + return $this->parseDot($bp); + } elseif ($type == T::T_LBRACKET || $type == T::T_FILTER) { + return $this->expr($bp); + } + + throw $this->syntax('Syntax error after projection'); + } + + private function parseDot($bp) + { + if ($this->token['type'] == T::T_LBRACKET) { + $this->next(); + return $this->parseMultiSelectList(); + } + + return $this->expr($bp); + } + + private function parseKeyValuePair() + { + static $validColon = [T::T_COLON => true]; + $key = $this->token['value']; + $this->next($validColon); + $this->next(); + + return [ + 'type' => 'key_val_pair', + 'value' => $key, + 'children' => [$this->expr()] + ]; + } + + private function parseWildcardObject(?array $left = null) + { + $this->next(); + + return [ + 'type' => 'projection', + 'from' => 'object', + 'children' => [ + $left ?: self::$currentNode, + $this->parseProjection(self::$bp[T::T_STAR]) + ] + ]; + } + + private function parseWildcardArray(?array $left = null) + { + static $getRbracket = [T::T_RBRACKET => true]; + $this->next($getRbracket); + $this->next(); + + return [ + 'type' => 'projection', + 'from' => 'array', + 'children' => [ + $left ?: self::$currentNode, + $this->parseProjection(self::$bp[T::T_STAR]) + ] + ]; + } + + /** + * Parses an array index expression (e.g., [0], [1:2:3] + */ + private function parseArrayIndexExpression() + { + static $matchNext = [ + T::T_NUMBER => true, + T::T_COLON => true, + T::T_RBRACKET => true + ]; + + $pos = 0; + $parts = [null, null, null]; + $expected = $matchNext; + + do { + if ($this->token['type'] == T::T_COLON) { + $pos++; + $expected = $matchNext; + } elseif ($this->token['type'] == T::T_NUMBER) { + $parts[$pos] = $this->token['value']; + $expected = [T::T_COLON => true, T::T_RBRACKET => true]; + } + $this->next($expected); + } while ($this->token['type'] != T::T_RBRACKET); + + // Consume the closing bracket + $this->next(); + + if ($pos === 0) { + // No colons were found so this is a simple index extraction + return ['type' => 'index', 'value' => $parts[0]]; + } + + if ($pos > 2) { + throw $this->syntax('Invalid array slice syntax: too many colons'); + } + + // Sliced array from start (e.g., [2:]) + return [ + 'type' => 'projection', + 'from' => 'array', + 'children' => [ + ['type' => 'slice', 'value' => $parts], + $this->parseProjection(self::$bp[T::T_STAR]) + ] + ]; + } + + private function parseMultiSelectList() + { + $nodes = []; + + do { + $nodes[] = $this->expr(); + if ($this->token['type'] == T::T_COMMA) { + $this->next(); + $this->assertNotToken(T::T_RBRACKET); + } + } while ($this->token['type'] !== T::T_RBRACKET); + $this->next(); + + return ['type' => 'multi_select_list', 'children' => $nodes]; + } + + private function syntax($msg) + { + return new SyntaxErrorException($msg, $this->token, $this->expression); + } + + private function lookahead() + { + return (!isset($this->tokens[$this->tpos + 1])) + ? T::T_EOF + : $this->tokens[$this->tpos + 1]['type']; + } + + private function next(?array $match = null) + { + if (!isset($this->tokens[$this->tpos + 1])) { + $this->token = self::$nullToken; + } else { + $this->token = $this->tokens[++$this->tpos]; + } + + if ($match && !isset($match[$this->token['type']])) { + throw $this->syntax($match); + } + } + + private function assertNotToken($type) + { + if ($this->token['type'] == $type) { + throw $this->syntax("Token {$this->tpos} not allowed to be $type"); + } + } + + /** + * @internal Handles undefined tokens without paying the cost of validation + */ + public function __call($method, $args) + { + $prefix = substr($method, 0, 4); + if ($prefix == 'nud_' || $prefix == 'led_') { + $token = substr($method, 4); + $message = "Unexpected \"$token\" token ($method). Expected one of" + . " the following tokens: " + . implode(', ', array_map(function ($i) { + return '"' . substr($i, 4) . '"'; + }, array_filter( + get_class_methods($this), + function ($i) use ($prefix) { + return strpos($i, $prefix) === 0; + } + ))); + throw $this->syntax($message); + } + + throw new \BadMethodCallException("Call to undefined method $method"); + } +} diff --git a/3rdparty/mtdowling/jmespath.php/src/SyntaxErrorException.php b/3rdparty/mtdowling/jmespath.php/src/SyntaxErrorException.php new file mode 100644 index 00000000..b9e376e1 --- /dev/null +++ b/3rdparty/mtdowling/jmespath.php/src/SyntaxErrorException.php @@ -0,0 +1,36 @@ +createTokenMessage($token, $expectedTypesOrMessage); + parent::__construct($message); + } + + private function createTokenMessage(array $token, array $valid) + { + return sprintf( + 'Expected one of the following: %s; found %s "%s"', + implode(', ', array_keys($valid)), + $token['type'], + $token['value'] + ); + } +} diff --git a/3rdparty/mtdowling/jmespath.php/src/TreeCompiler.php b/3rdparty/mtdowling/jmespath.php/src/TreeCompiler.php new file mode 100644 index 00000000..b5f06589 --- /dev/null +++ b/3rdparty/mtdowling/jmespath.php/src/TreeCompiler.php @@ -0,0 +1,419 @@ +vars = []; + $this->source = $this->indentation = ''; + $this->write("write('use JmesPath\\TreeInterpreter as Ti;') + ->write('use JmesPath\\FnDispatcher as Fd;') + ->write('use JmesPath\\Utils;') + ->write('') + ->write('function %s(Ti $interpreter, $value) {', $fnName) + ->indent() + ->dispatch($ast) + ->write('') + ->write('return $value;') + ->outdent() + ->write('}'); + + return $this->source; + } + + /** + * @param array $node + * @return mixed + */ + private function dispatch(array $node) + { + return $this->{"visit_{$node['type']}"}($node); + } + + /** + * Creates a monotonically incrementing unique variable name by prefix. + * + * @param string $prefix Variable name prefix + * + * @return string + */ + private function makeVar($prefix) + { + if (!isset($this->vars[$prefix])) { + $this->vars[$prefix] = 0; + return '$' . $prefix; + } + + return '$' . $prefix . ++$this->vars[$prefix]; + } + + /** + * Writes the given line of source code. Pass positional arguments to write + * that match the format of sprintf. + * + * @param string $str String to write + * @return $this + */ + private function write($str) + { + $this->source .= $this->indentation; + if (func_num_args() == 1) { + $this->source .= $str . "\n"; + return $this; + } + $this->source .= vsprintf($str, array_slice(func_get_args(), 1)) . "\n"; + return $this; + } + + /** + * Decreases the indentation level of code being written + * @return $this + */ + private function outdent() + { + $this->indentation = substr($this->indentation, 0, -4); + return $this; + } + + /** + * Increases the indentation level of code being written + * @return $this + */ + private function indent() + { + $this->indentation .= ' '; + return $this; + } + + private function visit_or(array $node) + { + $a = $this->makeVar('beforeOr'); + return $this + ->write('%s = $value;', $a) + ->dispatch($node['children'][0]) + ->write('if (!$value && $value !== "0" && $value !== 0) {') + ->indent() + ->write('$value = %s;', $a) + ->dispatch($node['children'][1]) + ->outdent() + ->write('}'); + } + + private function visit_and(array $node) + { + $a = $this->makeVar('beforeAnd'); + return $this + ->write('%s = $value;', $a) + ->dispatch($node['children'][0]) + ->write('if ($value || $value === "0" || $value === 0) {') + ->indent() + ->write('$value = %s;', $a) + ->dispatch($node['children'][1]) + ->outdent() + ->write('}'); + } + + private function visit_not(array $node) + { + return $this + ->write('// Visiting not node') + ->dispatch($node['children'][0]) + ->write('// Applying boolean not to result of not node') + ->write('$value = !Utils::isTruthy($value);'); + } + + private function visit_subexpression(array $node) + { + return $this + ->dispatch($node['children'][0]) + ->write('if ($value !== null) {') + ->indent() + ->dispatch($node['children'][1]) + ->outdent() + ->write('}'); + } + + private function visit_field(array $node) + { + $arr = '$value[' . var_export($node['value'], true) . ']'; + $obj = '$value->{' . var_export($node['value'], true) . '}'; + $this->write('if (is_array($value) || $value instanceof \\ArrayAccess) {') + ->indent() + ->write('$value = isset(%s) ? %s : null;', $arr, $arr) + ->outdent() + ->write('} elseif ($value instanceof \\stdClass) {') + ->indent() + ->write('$value = isset(%s) ? %s : null;', $obj, $obj) + ->outdent() + ->write("} else {") + ->indent() + ->write('$value = null;') + ->outdent() + ->write("}"); + + return $this; + } + + private function visit_index(array $node) + { + if ($node['value'] >= 0) { + $check = '$value[' . $node['value'] . ']'; + return $this->write( + '$value = (is_array($value) || $value instanceof \\ArrayAccess)' + . ' && isset(%s) ? %s : null;', + $check, $check + ); + } + + $a = $this->makeVar('count'); + return $this + ->write('if (is_array($value) || ($value instanceof \\ArrayAccess && $value instanceof \\Countable)) {') + ->indent() + ->write('%s = count($value) + %s;', $a, $node['value']) + ->write('$value = isset($value[%s]) ? $value[%s] : null;', $a, $a) + ->outdent() + ->write('} else {') + ->indent() + ->write('$value = null;') + ->outdent() + ->write('}'); + } + + private function visit_literal(array $node) + { + return $this->write('$value = %s;', var_export($node['value'], true)); + } + + private function visit_pipe(array $node) + { + return $this + ->dispatch($node['children'][0]) + ->dispatch($node['children'][1]); + } + + private function visit_multi_select_list(array $node) + { + return $this->visit_multi_select_hash($node); + } + + private function visit_multi_select_hash(array $node) + { + $listVal = $this->makeVar('list'); + $value = $this->makeVar('prev'); + $this->write('if ($value !== null) {') + ->indent() + ->write('%s = [];', $listVal) + ->write('%s = $value;', $value); + + $first = true; + foreach ($node['children'] as $child) { + if (!$first) { + $this->write('$value = %s;', $value); + } + $first = false; + if ($node['type'] == 'multi_select_hash') { + $this->dispatch($child['children'][0]); + $key = var_export($child['value'], true); + $this->write('%s[%s] = $value;', $listVal, $key); + } else { + $this->dispatch($child); + $this->write('%s[] = $value;', $listVal); + } + } + + return $this + ->write('$value = %s;', $listVal) + ->outdent() + ->write('}'); + } + + private function visit_function(array $node) + { + $value = $this->makeVar('val'); + $args = $this->makeVar('args'); + $this->write('%s = $value;', $value) + ->write('%s = [];', $args); + + foreach ($node['children'] as $arg) { + $this->dispatch($arg); + $this->write('%s[] = $value;', $args) + ->write('$value = %s;', $value); + } + + return $this->write( + '$value = Fd::getInstance()->__invoke("%s", %s);', + $node['value'], $args + ); + } + + private function visit_slice(array $node) + { + return $this + ->write('$value = !is_string($value) && !Utils::isArray($value)') + ->write(' ? null : Utils::slice($value, %s, %s, %s);', + var_export($node['value'][0], true), + var_export($node['value'][1], true), + var_export($node['value'][2], true) + ); + } + + private function visit_current(array $node) + { + return $this->write('// Visiting current node (no-op)'); + } + + private function visit_expref(array $node) + { + $child = var_export($node['children'][0], true); + return $this->write('$value = function ($value) use ($interpreter) {') + ->indent() + ->write('return $interpreter->visit(%s, $value);', $child) + ->outdent() + ->write('};'); + } + + private function visit_flatten(array $node) + { + $this->dispatch($node['children'][0]); + $merged = $this->makeVar('merged'); + $val = $this->makeVar('val'); + + $this + ->write('// Visiting merge node') + ->write('if (!Utils::isArray($value)) {') + ->indent() + ->write('$value = null;') + ->outdent() + ->write('} else {') + ->indent() + ->write('%s = [];', $merged) + ->write('foreach ($value as %s) {', $val) + ->indent() + ->write('if (is_array(%s) && array_key_exists(0, %s)) {', $val, $val) + ->indent() + ->write('%s = array_merge(%s, %s);', $merged, $merged, $val) + ->outdent() + ->write('} elseif (%s !== []) {', $val) + ->indent() + ->write('%s[] = %s;', $merged, $val) + ->outdent() + ->write('}') + ->outdent() + ->write('}') + ->write('$value = %s;', $merged) + ->outdent() + ->write('}'); + + return $this; + } + + private function visit_projection(array $node) + { + $val = $this->makeVar('val'); + $collected = $this->makeVar('collected'); + $this->write('// Visiting projection node') + ->dispatch($node['children'][0]) + ->write(''); + + if (!isset($node['from'])) { + $this->write('if (!is_array($value) || !($value instanceof \stdClass)) { $value = null; }'); + } elseif ($node['from'] == 'object') { + $this->write('if (!Utils::isObject($value)) { $value = null; }'); + } elseif ($node['from'] == 'array') { + $this->write('if (!Utils::isArray($value)) { $value = null; }'); + } + + $this->write('if ($value !== null) {') + ->indent() + ->write('%s = [];', $collected) + ->write('foreach ((array) $value as %s) {', $val) + ->indent() + ->write('$value = %s;', $val) + ->dispatch($node['children'][1]) + ->write('if ($value !== null) {') + ->indent() + ->write('%s[] = $value;', $collected) + ->outdent() + ->write('}') + ->outdent() + ->write('}') + ->write('$value = %s;', $collected) + ->outdent() + ->write('}'); + + return $this; + } + + private function visit_condition(array $node) + { + $value = $this->makeVar('beforeCondition'); + return $this + ->write('%s = $value;', $value) + ->write('// Visiting condition node') + ->dispatch($node['children'][0]) + ->write('// Checking result of condition node') + ->write('if (Utils::isTruthy($value)) {') + ->indent() + ->write('$value = %s;', $value) + ->dispatch($node['children'][1]) + ->outdent() + ->write('} else {') + ->indent() + ->write('$value = null;') + ->outdent() + ->write('}'); + } + + private function visit_comparator(array $node) + { + $value = $this->makeVar('val'); + $a = $this->makeVar('left'); + $b = $this->makeVar('right'); + + $this + ->write('// Visiting comparator node') + ->write('%s = $value;', $value) + ->dispatch($node['children'][0]) + ->write('%s = $value;', $a) + ->write('$value = %s;', $value) + ->dispatch($node['children'][1]) + ->write('%s = $value;', $b); + + if ($node['value'] == '==') { + $this->write('$value = Utils::isEqual(%s, %s);', $a, $b); + } elseif ($node['value'] == '!=') { + $this->write('$value = !Utils::isEqual(%s, %s);', $a, $b); + } else { + $this->write( + '$value = (is_int(%s) || is_float(%s)) && (is_int(%s) || is_float(%s)) && %s %s %s;', + $a, $a, $b, $b, $a, $node['value'], $b + ); + } + + return $this; + } + + /** @internal */ + public function __call($method, $args) + { + throw new \RuntimeException( + sprintf('Invalid node encountered: %s', json_encode($args[0])) + ); + } +} diff --git a/3rdparty/mtdowling/jmespath.php/src/TreeInterpreter.php b/3rdparty/mtdowling/jmespath.php/src/TreeInterpreter.php new file mode 100644 index 00000000..f4a8aeca --- /dev/null +++ b/3rdparty/mtdowling/jmespath.php/src/TreeInterpreter.php @@ -0,0 +1,235 @@ +fnDispatcher = $fnDispatcher ?: FnDispatcher::getInstance(); + } + + /** + * Visits each node in a JMESPath AST and returns the evaluated result. + * + * @param array $node JMESPath AST node + * @param mixed $data Data to evaluate + * + * @return mixed + */ + public function visit(array $node, $data) + { + return $this->dispatch($node, $data); + } + + /** + * Recursively traverses an AST using depth-first, pre-order traversal. + * The evaluation logic for each node type is embedded into a large switch + * statement to avoid the cost of "double dispatch". + * @return mixed + */ + private function dispatch(array $node, $value) + { + $dispatcher = $this->fnDispatcher; + + switch ($node['type']) { + + case 'field': + if (is_array($value) || $value instanceof \ArrayAccess) { + return isset($value[$node['value']]) ? $value[$node['value']] : null; + } elseif ($value instanceof \stdClass) { + return isset($value->{$node['value']}) ? $value->{$node['value']} : null; + } + return null; + + case 'subexpression': + return $this->dispatch( + $node['children'][1], + $this->dispatch($node['children'][0], $value) + ); + + case 'index': + if (!Utils::isArray($value)) { + return null; + } + $idx = $node['value'] >= 0 + ? $node['value'] + : $node['value'] + count($value); + return isset($value[$idx]) ? $value[$idx] : null; + + case 'projection': + $left = $this->dispatch($node['children'][0], $value); + switch ($node['from']) { + case 'object': + if (!Utils::isObject($left)) { + return null; + } + break; + case 'array': + if (!Utils::isArray($left)) { + return null; + } + break; + default: + if (!is_array($left) || !($left instanceof \stdClass)) { + return null; + } + } + + $collected = []; + foreach ((array) $left as $val) { + $result = $this->dispatch($node['children'][1], $val); + if ($result !== null) { + $collected[] = $result; + } + } + + return $collected; + + case 'flatten': + static $skipElement = []; + $value = $this->dispatch($node['children'][0], $value); + + if (!Utils::isArray($value)) { + return null; + } + + $merged = []; + foreach ($value as $values) { + // Only merge up arrays lists and not hashes + if (is_array($values) && array_key_exists(0, $values)) { + $merged = array_merge($merged, $values); + } elseif ($values !== $skipElement) { + $merged[] = $values; + } + } + + return $merged; + + case 'literal': + return $node['value']; + + case 'current': + return $value; + + case 'or': + $result = $this->dispatch($node['children'][0], $value); + return Utils::isTruthy($result) + ? $result + : $this->dispatch($node['children'][1], $value); + + case 'and': + $result = $this->dispatch($node['children'][0], $value); + return Utils::isTruthy($result) + ? $this->dispatch($node['children'][1], $value) + : $result; + + case 'not': + return !Utils::isTruthy( + $this->dispatch($node['children'][0], $value) + ); + + case 'pipe': + return $this->dispatch( + $node['children'][1], + $this->dispatch($node['children'][0], $value) + ); + + case 'multi_select_list': + if ($value === null) { + return null; + } + + $collected = []; + foreach ($node['children'] as $node) { + $collected[] = $this->dispatch($node, $value); + } + + return $collected; + + case 'multi_select_hash': + if ($value === null) { + return null; + } + + $collected = []; + foreach ($node['children'] as $node) { + $collected[$node['value']] = $this->dispatch( + $node['children'][0], + $value + ); + } + + return $collected; + + case 'comparator': + $left = $this->dispatch($node['children'][0], $value); + $right = $this->dispatch($node['children'][1], $value); + if ($node['value'] == '==') { + return Utils::isEqual($left, $right); + } elseif ($node['value'] == '!=') { + return !Utils::isEqual($left, $right); + } else { + return self::relativeCmp($left, $right, $node['value']); + } + + case 'condition': + return Utils::isTruthy($this->dispatch($node['children'][0], $value)) + ? $this->dispatch($node['children'][1], $value) + : null; + + case 'function': + $args = []; + foreach ($node['children'] as $arg) { + $args[] = $this->dispatch($arg, $value); + } + return $dispatcher($node['value'], $args); + + case 'slice': + return is_string($value) || Utils::isArray($value) + ? Utils::slice( + $value, + $node['value'][0], + $node['value'][1], + $node['value'][2] + ) : null; + + case 'expref': + $apply = $node['children'][0]; + return function ($value) use ($apply) { + return $this->visit($apply, $value); + }; + + default: + throw new \RuntimeException("Unknown node type: {$node['type']}"); + } + } + + /** + * @return bool + */ + private static function relativeCmp($left, $right, $cmp) + { + if (!(is_int($left) || is_float($left)) || !(is_int($right) || is_float($right))) { + return false; + } + + switch ($cmp) { + case '>': return $left > $right; + case '>=': return $left >= $right; + case '<': return $left < $right; + case '<=': return $left <= $right; + default: throw new \RuntimeException("Invalid comparison: $cmp"); + } + } +} diff --git a/3rdparty/mtdowling/jmespath.php/src/Utils.php b/3rdparty/mtdowling/jmespath.php/src/Utils.php new file mode 100644 index 00000000..9e69fef0 --- /dev/null +++ b/3rdparty/mtdowling/jmespath.php/src/Utils.php @@ -0,0 +1,258 @@ + 'boolean', + 'string' => 'string', + 'NULL' => 'null', + 'double' => 'number', + 'float' => 'number', + 'integer' => 'number' + ]; + + /** + * Returns true if the value is truthy + * + * @param mixed $value Value to check + * + * @return bool + */ + public static function isTruthy($value) + { + if (!$value) { + return $value === 0 || $value === '0'; + } elseif ($value instanceof \stdClass) { + return (bool) get_object_vars($value); + } else { + return true; + } + } + + /** + * Gets the JMESPath type equivalent of a PHP variable. + * + * @param mixed $arg PHP variable + * @return string Returns the JSON data type + * @throws \InvalidArgumentException when an unknown type is given. + */ + public static function type($arg) + { + $type = gettype($arg); + if (isset(self::$typeMap[$type])) { + return self::$typeMap[$type]; + } elseif ($type === 'array') { + if (empty($arg)) { + return 'array'; + } + reset($arg); + return key($arg) === 0 ? 'array' : 'object'; + } elseif ($arg instanceof \stdClass) { + return 'object'; + } elseif ($arg instanceof \Closure) { + return 'expression'; + } elseif ($arg instanceof \ArrayAccess + && $arg instanceof \Countable + ) { + return count($arg) == 0 || $arg->offsetExists(0) + ? 'array' + : 'object'; + } elseif (method_exists($arg, '__toString')) { + return 'string'; + } + + throw new \InvalidArgumentException( + 'Unable to determine JMESPath type from ' . get_class($arg) + ); + } + + /** + * Determine if the provided value is a JMESPath compatible object. + * + * @param mixed $value + * + * @return bool + */ + public static function isObject($value) + { + if (is_array($value)) { + return !$value || array_keys($value)[0] !== 0; + } + + // Handle array-like values. Must be empty or offset 0 does not exist + return $value instanceof \Countable && $value instanceof \ArrayAccess + ? count($value) == 0 || !$value->offsetExists(0) + : $value instanceof \stdClass; + } + + /** + * Determine if the provided value is a JMESPath compatible array. + * + * @param mixed $value + * + * @return bool + */ + public static function isArray($value) + { + if (is_array($value)) { + return !$value || array_keys($value)[0] === 0; + } + + // Handle array-like values. Must be empty or offset 0 exists. + return $value instanceof \Countable && $value instanceof \ArrayAccess + ? count($value) == 0 || $value->offsetExists(0) + : false; + } + + /** + * JSON aware value comparison function. + * + * @param mixed $a First value to compare + * @param mixed $b Second value to compare + * + * @return bool + */ + public static function isEqual($a, $b) + { + if ($a === $b) { + return true; + } elseif ($a instanceof \stdClass) { + return self::isEqual((array) $a, $b); + } elseif ($b instanceof \stdClass) { + return self::isEqual($a, (array) $b); + } else { + return false; + } + } + + /** + * Safely add together two values. + * + * @param mixed $a First value to add + * @param mixed $b Second value to add + * + * @return int|float + */ + public static function add($a, $b) + { + if (is_numeric($a)) { + if (is_numeric($b)) { + return $a + $b; + } else { + return $a; + } + } else { + if (is_numeric($b)) { + return $b; + } else { + return 0; + } + } + } + + /** + * JMESPath requires a stable sorting algorithm, so here we'll implement + * a simple Schwartzian transform that uses array index positions as tie + * breakers. + * + * @param array $data List or map of data to sort + * @param callable $sortFn Callable used to sort values + * + * @return array Returns the sorted array + * @link http://en.wikipedia.org/wiki/Schwartzian_transform + */ + public static function stableSort(array $data, callable $sortFn) + { + // Decorate each item by creating an array of [value, index] + array_walk($data, function (&$v, $k) { + $v = [$v, $k]; + }); + // Sort by the sort function and use the index as a tie-breaker + uasort($data, function ($a, $b) use ($sortFn) { + return $sortFn($a[0], $b[0]) ?: ($a[1] < $b[1] ? -1 : 1); + }); + + // Undecorate each item and return the resulting sorted array + return array_map(function ($v) { + return $v[0]; + }, array_values($data)); + } + + /** + * Creates a Python-style slice of a string or array. + * + * @param array|string $value Value to slice + * @param int|null $start Starting position + * @param int|null $stop Stop position + * @param int $step Step (1, 2, -1, -2, etc.) + * + * @return array|string + * @throws \InvalidArgumentException + */ + public static function slice($value, $start = null, $stop = null, $step = 1) + { + if (!is_array($value) && !is_string($value)) { + throw new \InvalidArgumentException('Expects string or array'); + } + + return self::sliceIndices($value, $start, $stop, $step); + } + + private static function adjustEndpoint($length, $endpoint, $step) + { + if ($endpoint < 0) { + $endpoint += $length; + if ($endpoint < 0) { + $endpoint = $step < 0 ? -1 : 0; + } + } elseif ($endpoint >= $length) { + $endpoint = $step < 0 ? $length - 1 : $length; + } + + return $endpoint; + } + + private static function adjustSlice($length, $start, $stop, $step) + { + if ($step === null) { + $step = 1; + } elseif ($step === 0) { + throw new \RuntimeException('step cannot be 0'); + } + + if ($start === null) { + $start = $step < 0 ? $length - 1 : 0; + } else { + $start = self::adjustEndpoint($length, $start, $step); + } + + if ($stop === null) { + $stop = $step < 0 ? -1 : $length; + } else { + $stop = self::adjustEndpoint($length, $stop, $step); + } + + return [$start, $stop, $step]; + } + + private static function sliceIndices($subject, $start, $stop, $step) + { + $type = gettype($subject); + $len = $type == 'string' ? mb_strlen($subject, 'UTF-8') : count($subject); + list($start, $stop, $step) = self::adjustSlice($len, $start, $stop, $step); + + $result = []; + if ($step > 0) { + for ($i = $start; $i < $stop; $i += $step) { + $result[] = $subject[$i]; + } + } else { + for ($i = $start; $i > $stop; $i += $step) { + $result[] = $subject[$i]; + } + } + + return $type == 'string' ? implode('', $result) : $result; + } +} diff --git a/3rdparty/paragonie/constant_time_encoding/LICENSE.txt b/3rdparty/paragonie/constant_time_encoding/LICENSE.txt new file mode 100644 index 00000000..91acaca6 --- /dev/null +++ b/3rdparty/paragonie/constant_time_encoding/LICENSE.txt @@ -0,0 +1,48 @@ +The MIT License (MIT) + +Copyright (c) 2016 - 2022 Paragon Initiative Enterprises + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. + +------------------------------------------------------------------------------ +This library was based on the work of Steve "Sc00bz" Thomas. +------------------------------------------------------------------------------ + +The MIT License (MIT) + +Copyright (c) 2014 Steve Thomas + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. + diff --git a/3rdparty/paragonie/constant_time_encoding/src/Base32.php b/3rdparty/paragonie/constant_time_encoding/src/Base32.php new file mode 100644 index 00000000..7508b3df --- /dev/null +++ b/3rdparty/paragonie/constant_time_encoding/src/Base32.php @@ -0,0 +1,519 @@ + 96 && $src < 123) $ret += $src - 97 + 1; // -64 + $ret += (((0x60 - $src) & ($src - 0x7b)) >> 8) & ($src - 96); + + // if ($src > 0x31 && $src < 0x38) $ret += $src - 24 + 1; // -23 + $ret += (((0x31 - $src) & ($src - 0x38)) >> 8) & ($src - 23); + + return $ret; + } + + /** + * Uses bitwise operators instead of table-lookups to turn 5-bit integers + * into 8-bit integers. + * + * Uppercase variant. + * + * @param int $src + * @return int + */ + protected static function decode5BitsUpper(int $src): int + { + $ret = -1; + + // if ($src > 64 && $src < 91) $ret += $src - 65 + 1; // -64 + $ret += (((0x40 - $src) & ($src - 0x5b)) >> 8) & ($src - 64); + + // if ($src > 0x31 && $src < 0x38) $ret += $src - 24 + 1; // -23 + $ret += (((0x31 - $src) & ($src - 0x38)) >> 8) & ($src - 23); + + return $ret; + } + + /** + * Uses bitwise operators instead of table-lookups to turn 8-bit integers + * into 5-bit integers. + * + * @param int $src + * @return string + */ + protected static function encode5Bits(int $src): string + { + $diff = 0x61; + + // if ($src > 25) $ret -= 72; + $diff -= ((25 - $src) >> 8) & 73; + + return \pack('C', $src + $diff); + } + + /** + * Uses bitwise operators instead of table-lookups to turn 8-bit integers + * into 5-bit integers. + * + * Uppercase variant. + * + * @param int $src + * @return string + */ + protected static function encode5BitsUpper(int $src): string + { + $diff = 0x41; + + // if ($src > 25) $ret -= 40; + $diff -= ((25 - $src) >> 8) & 41; + + return \pack('C', $src + $diff); + } + + /** + * @param string $encodedString + * @param bool $upper + * @return string + */ + public static function decodeNoPadding(string $encodedString, bool $upper = false): string + { + $srcLen = Binary::safeStrlen($encodedString); + if ($srcLen === 0) { + return ''; + } + if (($srcLen & 7) === 0) { + for ($j = 0; $j < 7 && $j < $srcLen; ++$j) { + if ($encodedString[$srcLen - $j - 1] === '=') { + throw new InvalidArgumentException( + "decodeNoPadding() doesn't tolerate padding" + ); + } + } + } + return static::doDecode( + $encodedString, + $upper, + true + ); + } + + /** + * Base32 decoding + * + * @param string $src + * @param bool $upper + * @param bool $strictPadding + * @return string + * + * @throws TypeError + * @psalm-suppress RedundantCondition + */ + protected static function doDecode( + string $src, + bool $upper = false, + bool $strictPadding = false + ): string { + // We do this to reduce code duplication: + $method = $upper + ? 'decode5BitsUpper' + : 'decode5Bits'; + + // Remove padding + $srcLen = Binary::safeStrlen($src); + if ($srcLen === 0) { + return ''; + } + if ($strictPadding) { + if (($srcLen & 7) === 0) { + for ($j = 0; $j < 7; ++$j) { + if ($src[$srcLen - 1] === '=') { + $srcLen--; + } else { + break; + } + } + } + if (($srcLen & 7) === 1) { + throw new RangeException( + 'Incorrect padding' + ); + } + } else { + $src = \rtrim($src, '='); + $srcLen = Binary::safeStrlen($src); + } + + $err = 0; + $dest = ''; + // Main loop (no padding): + for ($i = 0; $i + 8 <= $srcLen; $i += 8) { + /** @var array $chunk */ + $chunk = \unpack('C*', Binary::safeSubstr($src, $i, 8)); + /** @var int $c0 */ + $c0 = static::$method($chunk[1]); + /** @var int $c1 */ + $c1 = static::$method($chunk[2]); + /** @var int $c2 */ + $c2 = static::$method($chunk[3]); + /** @var int $c3 */ + $c3 = static::$method($chunk[4]); + /** @var int $c4 */ + $c4 = static::$method($chunk[5]); + /** @var int $c5 */ + $c5 = static::$method($chunk[6]); + /** @var int $c6 */ + $c6 = static::$method($chunk[7]); + /** @var int $c7 */ + $c7 = static::$method($chunk[8]); + + $dest .= \pack( + 'CCCCC', + (($c0 << 3) | ($c1 >> 2) ) & 0xff, + (($c1 << 6) | ($c2 << 1) | ($c3 >> 4)) & 0xff, + (($c3 << 4) | ($c4 >> 1) ) & 0xff, + (($c4 << 7) | ($c5 << 2) | ($c6 >> 3)) & 0xff, + (($c6 << 5) | ($c7 ) ) & 0xff + ); + $err |= ($c0 | $c1 | $c2 | $c3 | $c4 | $c5 | $c6 | $c7) >> 8; + } + // The last chunk, which may have padding: + if ($i < $srcLen) { + /** @var array $chunk */ + $chunk = \unpack('C*', Binary::safeSubstr($src, $i, $srcLen - $i)); + /** @var int $c0 */ + $c0 = static::$method($chunk[1]); + + if ($i + 6 < $srcLen) { + /** @var int $c1 */ + $c1 = static::$method($chunk[2]); + /** @var int $c2 */ + $c2 = static::$method($chunk[3]); + /** @var int $c3 */ + $c3 = static::$method($chunk[4]); + /** @var int $c4 */ + $c4 = static::$method($chunk[5]); + /** @var int $c5 */ + $c5 = static::$method($chunk[6]); + /** @var int $c6 */ + $c6 = static::$method($chunk[7]); + + $dest .= \pack( + 'CCCC', + (($c0 << 3) | ($c1 >> 2) ) & 0xff, + (($c1 << 6) | ($c2 << 1) | ($c3 >> 4)) & 0xff, + (($c3 << 4) | ($c4 >> 1) ) & 0xff, + (($c4 << 7) | ($c5 << 2) | ($c6 >> 3)) & 0xff + ); + $err |= ($c0 | $c1 | $c2 | $c3 | $c4 | $c5 | $c6) >> 8; + if ($strictPadding) { + $err |= ($c6 << 5) & 0xff; + } + } elseif ($i + 5 < $srcLen) { + /** @var int $c1 */ + $c1 = static::$method($chunk[2]); + /** @var int $c2 */ + $c2 = static::$method($chunk[3]); + /** @var int $c3 */ + $c3 = static::$method($chunk[4]); + /** @var int $c4 */ + $c4 = static::$method($chunk[5]); + /** @var int $c5 */ + $c5 = static::$method($chunk[6]); + + $dest .= \pack( + 'CCCC', + (($c0 << 3) | ($c1 >> 2) ) & 0xff, + (($c1 << 6) | ($c2 << 1) | ($c3 >> 4)) & 0xff, + (($c3 << 4) | ($c4 >> 1) ) & 0xff, + (($c4 << 7) | ($c5 << 2) ) & 0xff + ); + $err |= ($c0 | $c1 | $c2 | $c3 | $c4 | $c5) >> 8; + } elseif ($i + 4 < $srcLen) { + /** @var int $c1 */ + $c1 = static::$method($chunk[2]); + /** @var int $c2 */ + $c2 = static::$method($chunk[3]); + /** @var int $c3 */ + $c3 = static::$method($chunk[4]); + /** @var int $c4 */ + $c4 = static::$method($chunk[5]); + + $dest .= \pack( + 'CCC', + (($c0 << 3) | ($c1 >> 2) ) & 0xff, + (($c1 << 6) | ($c2 << 1) | ($c3 >> 4)) & 0xff, + (($c3 << 4) | ($c4 >> 1) ) & 0xff + ); + $err |= ($c0 | $c1 | $c2 | $c3 | $c4) >> 8; + if ($strictPadding) { + $err |= ($c4 << 7) & 0xff; + } + } elseif ($i + 3 < $srcLen) { + /** @var int $c1 */ + $c1 = static::$method($chunk[2]); + /** @var int $c2 */ + $c2 = static::$method($chunk[3]); + /** @var int $c3 */ + $c3 = static::$method($chunk[4]); + + $dest .= \pack( + 'CC', + (($c0 << 3) | ($c1 >> 2) ) & 0xff, + (($c1 << 6) | ($c2 << 1) | ($c3 >> 4)) & 0xff + ); + $err |= ($c0 | $c1 | $c2 | $c3) >> 8; + if ($strictPadding) { + $err |= ($c3 << 4) & 0xff; + } + } elseif ($i + 2 < $srcLen) { + /** @var int $c1 */ + $c1 = static::$method($chunk[2]); + /** @var int $c2 */ + $c2 = static::$method($chunk[3]); + + $dest .= \pack( + 'CC', + (($c0 << 3) | ($c1 >> 2) ) & 0xff, + (($c1 << 6) | ($c2 << 1) ) & 0xff + ); + $err |= ($c0 | $c1 | $c2) >> 8; + if ($strictPadding) { + $err |= ($c2 << 6) & 0xff; + } + } elseif ($i + 1 < $srcLen) { + /** @var int $c1 */ + $c1 = static::$method($chunk[2]); + + $dest .= \pack( + 'C', + (($c0 << 3) | ($c1 >> 2) ) & 0xff + ); + $err |= ($c0 | $c1) >> 8; + if ($strictPadding) { + $err |= ($c1 << 6) & 0xff; + } + } else { + $dest .= \pack( + 'C', + (($c0 << 3) ) & 0xff + ); + $err |= ($c0) >> 8; + } + } + $check = ($err === 0); + if (!$check) { + throw new RangeException( + 'Base32::doDecode() only expects characters in the correct base32 alphabet' + ); + } + return $dest; + } + + /** + * Base32 Encoding + * + * @param string $src + * @param bool $upper + * @param bool $pad + * @return string + * @throws TypeError + */ + protected static function doEncode(string $src, bool $upper = false, $pad = true): string + { + // We do this to reduce code duplication: + $method = $upper + ? 'encode5BitsUpper' + : 'encode5Bits'; + + $dest = ''; + $srcLen = Binary::safeStrlen($src); + + // Main loop (no padding): + for ($i = 0; $i + 5 <= $srcLen; $i += 5) { + /** @var array $chunk */ + $chunk = \unpack('C*', Binary::safeSubstr($src, $i, 5)); + $b0 = $chunk[1]; + $b1 = $chunk[2]; + $b2 = $chunk[3]; + $b3 = $chunk[4]; + $b4 = $chunk[5]; + $dest .= + static::$method( ($b0 >> 3) & 31) . + static::$method((($b0 << 2) | ($b1 >> 6)) & 31) . + static::$method((($b1 >> 1) ) & 31) . + static::$method((($b1 << 4) | ($b2 >> 4)) & 31) . + static::$method((($b2 << 1) | ($b3 >> 7)) & 31) . + static::$method((($b3 >> 2) ) & 31) . + static::$method((($b3 << 3) | ($b4 >> 5)) & 31) . + static::$method( $b4 & 31); + } + // The last chunk, which may have padding: + if ($i < $srcLen) { + /** @var array $chunk */ + $chunk = \unpack('C*', Binary::safeSubstr($src, $i, $srcLen - $i)); + $b0 = $chunk[1]; + if ($i + 3 < $srcLen) { + $b1 = $chunk[2]; + $b2 = $chunk[3]; + $b3 = $chunk[4]; + $dest .= + static::$method( ($b0 >> 3) & 31) . + static::$method((($b0 << 2) | ($b1 >> 6)) & 31) . + static::$method((($b1 >> 1) ) & 31) . + static::$method((($b1 << 4) | ($b2 >> 4)) & 31) . + static::$method((($b2 << 1) | ($b3 >> 7)) & 31) . + static::$method((($b3 >> 2) ) & 31) . + static::$method((($b3 << 3) ) & 31); + if ($pad) { + $dest .= '='; + } + } elseif ($i + 2 < $srcLen) { + $b1 = $chunk[2]; + $b2 = $chunk[3]; + $dest .= + static::$method( ($b0 >> 3) & 31) . + static::$method((($b0 << 2) | ($b1 >> 6)) & 31) . + static::$method((($b1 >> 1) ) & 31) . + static::$method((($b1 << 4) | ($b2 >> 4)) & 31) . + static::$method((($b2 << 1) ) & 31); + if ($pad) { + $dest .= '==='; + } + } elseif ($i + 1 < $srcLen) { + $b1 = $chunk[2]; + $dest .= + static::$method( ($b0 >> 3) & 31) . + static::$method((($b0 << 2) | ($b1 >> 6)) & 31) . + static::$method((($b1 >> 1) ) & 31) . + static::$method((($b1 << 4) ) & 31); + if ($pad) { + $dest .= '===='; + } + } else { + $dest .= + static::$method( ($b0 >> 3) & 31) . + static::$method( ($b0 << 2) & 31); + if ($pad) { + $dest .= '======'; + } + } + } + return $dest; + } +} diff --git a/3rdparty/paragonie/constant_time_encoding/src/Base32Hex.php b/3rdparty/paragonie/constant_time_encoding/src/Base32Hex.php new file mode 100644 index 00000000..b868dd04 --- /dev/null +++ b/3rdparty/paragonie/constant_time_encoding/src/Base32Hex.php @@ -0,0 +1,111 @@ + 0x30 && $src < 0x3a) ret += $src - 0x2e + 1; // -47 + $ret += (((0x2f - $src) & ($src - 0x3a)) >> 8) & ($src - 47); + + // if ($src > 0x60 && $src < 0x77) ret += $src - 0x61 + 10 + 1; // -86 + $ret += (((0x60 - $src) & ($src - 0x77)) >> 8) & ($src - 86); + + return $ret; + } + + /** + * Uses bitwise operators instead of table-lookups to turn 5-bit integers + * into 8-bit integers. + * + * @param int $src + * @return int + */ + protected static function decode5BitsUpper(int $src): int + { + $ret = -1; + + // if ($src > 0x30 && $src < 0x3a) ret += $src - 0x2e + 1; // -47 + $ret += (((0x2f - $src) & ($src - 0x3a)) >> 8) & ($src - 47); + + // if ($src > 0x40 && $src < 0x57) ret += $src - 0x41 + 10 + 1; // -54 + $ret += (((0x40 - $src) & ($src - 0x57)) >> 8) & ($src - 54); + + return $ret; + } + + /** + * Uses bitwise operators instead of table-lookups to turn 8-bit integers + * into 5-bit integers. + * + * @param int $src + * @return string + */ + protected static function encode5Bits(int $src): string + { + $src += 0x30; + + // if ($src > 0x39) $src += 0x61 - 0x3a; // 39 + $src += ((0x39 - $src) >> 8) & 39; + + return \pack('C', $src); + } + + /** + * Uses bitwise operators instead of table-lookups to turn 8-bit integers + * into 5-bit integers. + * + * Uppercase variant. + * + * @param int $src + * @return string + */ + protected static function encode5BitsUpper(int $src): string + { + $src += 0x30; + + // if ($src > 0x39) $src += 0x41 - 0x3a; // 7 + $src += ((0x39 - $src) >> 8) & 7; + + return \pack('C', $src); + } +} \ No newline at end of file diff --git a/3rdparty/paragonie/constant_time_encoding/src/Base64.php b/3rdparty/paragonie/constant_time_encoding/src/Base64.php new file mode 100644 index 00000000..f5716179 --- /dev/null +++ b/3rdparty/paragonie/constant_time_encoding/src/Base64.php @@ -0,0 +1,314 @@ + $chunk */ + $chunk = \unpack('C*', Binary::safeSubstr($src, $i, 3)); + $b0 = $chunk[1]; + $b1 = $chunk[2]; + $b2 = $chunk[3]; + + $dest .= + static::encode6Bits( $b0 >> 2 ) . + static::encode6Bits((($b0 << 4) | ($b1 >> 4)) & 63) . + static::encode6Bits((($b1 << 2) | ($b2 >> 6)) & 63) . + static::encode6Bits( $b2 & 63); + } + // The last chunk, which may have padding: + if ($i < $srcLen) { + /** @var array $chunk */ + $chunk = \unpack('C*', Binary::safeSubstr($src, $i, $srcLen - $i)); + $b0 = $chunk[1]; + if ($i + 1 < $srcLen) { + $b1 = $chunk[2]; + $dest .= + static::encode6Bits($b0 >> 2) . + static::encode6Bits((($b0 << 4) | ($b1 >> 4)) & 63) . + static::encode6Bits(($b1 << 2) & 63); + if ($pad) { + $dest .= '='; + } + } else { + $dest .= + static::encode6Bits( $b0 >> 2) . + static::encode6Bits(($b0 << 4) & 63); + if ($pad) { + $dest .= '=='; + } + } + } + return $dest; + } + + /** + * decode from base64 into binary + * + * Base64 character set "./[A-Z][a-z][0-9]" + * + * @param string $encodedString + * @param bool $strictPadding + * @return string + * + * @throws RangeException + * @throws TypeError + * @psalm-suppress RedundantCondition + */ + public static function decode(string $encodedString, bool $strictPadding = false): string + { + // Remove padding + $srcLen = Binary::safeStrlen($encodedString); + if ($srcLen === 0) { + return ''; + } + + if ($strictPadding) { + if (($srcLen & 3) === 0) { + if ($encodedString[$srcLen - 1] === '=') { + $srcLen--; + if ($encodedString[$srcLen - 1] === '=') { + $srcLen--; + } + } + } + if (($srcLen & 3) === 1) { + throw new RangeException( + 'Incorrect padding' + ); + } + if ($encodedString[$srcLen - 1] === '=') { + throw new RangeException( + 'Incorrect padding' + ); + } + } else { + $encodedString = \rtrim($encodedString, '='); + $srcLen = Binary::safeStrlen($encodedString); + } + + $err = 0; + $dest = ''; + // Main loop (no padding): + for ($i = 0; $i + 4 <= $srcLen; $i += 4) { + /** @var array $chunk */ + $chunk = \unpack('C*', Binary::safeSubstr($encodedString, $i, 4)); + $c0 = static::decode6Bits($chunk[1]); + $c1 = static::decode6Bits($chunk[2]); + $c2 = static::decode6Bits($chunk[3]); + $c3 = static::decode6Bits($chunk[4]); + + $dest .= \pack( + 'CCC', + ((($c0 << 2) | ($c1 >> 4)) & 0xff), + ((($c1 << 4) | ($c2 >> 2)) & 0xff), + ((($c2 << 6) | $c3 ) & 0xff) + ); + $err |= ($c0 | $c1 | $c2 | $c3) >> 8; + } + // The last chunk, which may have padding: + if ($i < $srcLen) { + /** @var array $chunk */ + $chunk = \unpack('C*', Binary::safeSubstr($encodedString, $i, $srcLen - $i)); + $c0 = static::decode6Bits($chunk[1]); + + if ($i + 2 < $srcLen) { + $c1 = static::decode6Bits($chunk[2]); + $c2 = static::decode6Bits($chunk[3]); + $dest .= \pack( + 'CC', + ((($c0 << 2) | ($c1 >> 4)) & 0xff), + ((($c1 << 4) | ($c2 >> 2)) & 0xff) + ); + $err |= ($c0 | $c1 | $c2) >> 8; + if ($strictPadding) { + $err |= ($c2 << 6) & 0xff; + } + } elseif ($i + 1 < $srcLen) { + $c1 = static::decode6Bits($chunk[2]); + $dest .= \pack( + 'C', + ((($c0 << 2) | ($c1 >> 4)) & 0xff) + ); + $err |= ($c0 | $c1) >> 8; + if ($strictPadding) { + $err |= ($c1 << 4) & 0xff; + } + } elseif ($strictPadding) { + $err |= 1; + } + } + $check = ($err === 0); + if (!$check) { + throw new RangeException( + 'Base64::decode() only expects characters in the correct base64 alphabet' + ); + } + return $dest; + } + + /** + * @param string $encodedString + * @return string + */ + public static function decodeNoPadding(string $encodedString): string + { + $srcLen = Binary::safeStrlen($encodedString); + if ($srcLen === 0) { + return ''; + } + if (($srcLen & 3) === 0) { + if ($encodedString[$srcLen - 1] === '=') { + throw new InvalidArgumentException( + "decodeNoPadding() doesn't tolerate padding" + ); + } + if (($srcLen & 3) > 1) { + if ($encodedString[$srcLen - 2] === '=') { + throw new InvalidArgumentException( + "decodeNoPadding() doesn't tolerate padding" + ); + } + } + } + return static::decode( + $encodedString, + true + ); + } + + /** + * Uses bitwise operators instead of table-lookups to turn 6-bit integers + * into 8-bit integers. + * + * Base64 character set: + * [A-Z] [a-z] [0-9] + / + * 0x41-0x5a, 0x61-0x7a, 0x30-0x39, 0x2b, 0x2f + * + * @param int $src + * @return int + */ + protected static function decode6Bits(int $src): int + { + $ret = -1; + + // if ($src > 0x40 && $src < 0x5b) $ret += $src - 0x41 + 1; // -64 + $ret += (((0x40 - $src) & ($src - 0x5b)) >> 8) & ($src - 64); + + // if ($src > 0x60 && $src < 0x7b) $ret += $src - 0x61 + 26 + 1; // -70 + $ret += (((0x60 - $src) & ($src - 0x7b)) >> 8) & ($src - 70); + + // if ($src > 0x2f && $src < 0x3a) $ret += $src - 0x30 + 52 + 1; // 5 + $ret += (((0x2f - $src) & ($src - 0x3a)) >> 8) & ($src + 5); + + // if ($src == 0x2b) $ret += 62 + 1; + $ret += (((0x2a - $src) & ($src - 0x2c)) >> 8) & 63; + + // if ($src == 0x2f) ret += 63 + 1; + $ret += (((0x2e - $src) & ($src - 0x30)) >> 8) & 64; + + return $ret; + } + + /** + * Uses bitwise operators instead of table-lookups to turn 8-bit integers + * into 6-bit integers. + * + * @param int $src + * @return string + */ + protected static function encode6Bits(int $src): string + { + $diff = 0x41; + + // if ($src > 25) $diff += 0x61 - 0x41 - 26; // 6 + $diff += ((25 - $src) >> 8) & 6; + + // if ($src > 51) $diff += 0x30 - 0x61 - 26; // -75 + $diff -= ((51 - $src) >> 8) & 75; + + // if ($src > 61) $diff += 0x2b - 0x30 - 10; // -15 + $diff -= ((61 - $src) >> 8) & 15; + + // if ($src > 62) $diff += 0x2f - 0x2b - 1; // 3 + $diff += ((62 - $src) >> 8) & 3; + + return \pack('C', $src + $diff); + } +} diff --git a/3rdparty/paragonie/constant_time_encoding/src/Base64DotSlash.php b/3rdparty/paragonie/constant_time_encoding/src/Base64DotSlash.php new file mode 100644 index 00000000..5e98a8f7 --- /dev/null +++ b/3rdparty/paragonie/constant_time_encoding/src/Base64DotSlash.php @@ -0,0 +1,88 @@ + 0x2d && $src < 0x30) ret += $src - 0x2e + 1; // -45 + $ret += (((0x2d - $src) & ($src - 0x30)) >> 8) & ($src - 45); + + // if ($src > 0x40 && $src < 0x5b) ret += $src - 0x41 + 2 + 1; // -62 + $ret += (((0x40 - $src) & ($src - 0x5b)) >> 8) & ($src - 62); + + // if ($src > 0x60 && $src < 0x7b) ret += $src - 0x61 + 28 + 1; // -68 + $ret += (((0x60 - $src) & ($src - 0x7b)) >> 8) & ($src - 68); + + // if ($src > 0x2f && $src < 0x3a) ret += $src - 0x30 + 54 + 1; // 7 + $ret += (((0x2f - $src) & ($src - 0x3a)) >> 8) & ($src + 7); + + return $ret; + } + + /** + * Uses bitwise operators instead of table-lookups to turn 8-bit integers + * into 6-bit integers. + * + * @param int $src + * @return string + */ + protected static function encode6Bits(int $src): string + { + $src += 0x2e; + + // if ($src > 0x2f) $src += 0x41 - 0x30; // 17 + $src += ((0x2f - $src) >> 8) & 17; + + // if ($src > 0x5a) $src += 0x61 - 0x5b; // 6 + $src += ((0x5a - $src) >> 8) & 6; + + // if ($src > 0x7a) $src += 0x30 - 0x7b; // -75 + $src -= ((0x7a - $src) >> 8) & 75; + + return \pack('C', $src); + } +} diff --git a/3rdparty/paragonie/constant_time_encoding/src/Base64DotSlashOrdered.php b/3rdparty/paragonie/constant_time_encoding/src/Base64DotSlashOrdered.php new file mode 100644 index 00000000..9780b14b --- /dev/null +++ b/3rdparty/paragonie/constant_time_encoding/src/Base64DotSlashOrdered.php @@ -0,0 +1,82 @@ + 0x2d && $src < 0x3a) ret += $src - 0x2e + 1; // -45 + $ret += (((0x2d - $src) & ($src - 0x3a)) >> 8) & ($src - 45); + + // if ($src > 0x40 && $src < 0x5b) ret += $src - 0x41 + 12 + 1; // -52 + $ret += (((0x40 - $src) & ($src - 0x5b)) >> 8) & ($src - 52); + + // if ($src > 0x60 && $src < 0x7b) ret += $src - 0x61 + 38 + 1; // -58 + $ret += (((0x60 - $src) & ($src - 0x7b)) >> 8) & ($src - 58); + + return $ret; + } + + /** + * Uses bitwise operators instead of table-lookups to turn 8-bit integers + * into 6-bit integers. + * + * @param int $src + * @return string + */ + protected static function encode6Bits(int $src): string + { + $src += 0x2e; + + // if ($src > 0x39) $src += 0x41 - 0x3a; // 7 + $src += ((0x39 - $src) >> 8) & 7; + + // if ($src > 0x5a) $src += 0x61 - 0x5b; // 6 + $src += ((0x5a - $src) >> 8) & 6; + + return \pack('C', $src); + } +} diff --git a/3rdparty/paragonie/constant_time_encoding/src/Base64UrlSafe.php b/3rdparty/paragonie/constant_time_encoding/src/Base64UrlSafe.php new file mode 100644 index 00000000..8192c63d --- /dev/null +++ b/3rdparty/paragonie/constant_time_encoding/src/Base64UrlSafe.php @@ -0,0 +1,95 @@ + 0x40 && $src < 0x5b) $ret += $src - 0x41 + 1; // -64 + $ret += (((0x40 - $src) & ($src - 0x5b)) >> 8) & ($src - 64); + + // if ($src > 0x60 && $src < 0x7b) $ret += $src - 0x61 + 26 + 1; // -70 + $ret += (((0x60 - $src) & ($src - 0x7b)) >> 8) & ($src - 70); + + // if ($src > 0x2f && $src < 0x3a) $ret += $src - 0x30 + 52 + 1; // 5 + $ret += (((0x2f - $src) & ($src - 0x3a)) >> 8) & ($src + 5); + + // if ($src == 0x2c) $ret += 62 + 1; + $ret += (((0x2c - $src) & ($src - 0x2e)) >> 8) & 63; + + // if ($src == 0x5f) ret += 63 + 1; + $ret += (((0x5e - $src) & ($src - 0x60)) >> 8) & 64; + + return $ret; + } + + /** + * Uses bitwise operators instead of table-lookups to turn 8-bit integers + * into 6-bit integers. + * + * @param int $src + * @return string + */ + protected static function encode6Bits(int $src): string + { + $diff = 0x41; + + // if ($src > 25) $diff += 0x61 - 0x41 - 26; // 6 + $diff += ((25 - $src) >> 8) & 6; + + // if ($src > 51) $diff += 0x30 - 0x61 - 26; // -75 + $diff -= ((51 - $src) >> 8) & 75; + + // if ($src > 61) $diff += 0x2d - 0x30 - 10; // -13 + $diff -= ((61 - $src) >> 8) & 13; + + // if ($src > 62) $diff += 0x5f - 0x2b - 1; // 3 + $diff += ((62 - $src) >> 8) & 49; + + return \pack('C', $src + $diff); + } +} diff --git a/3rdparty/paragonie/constant_time_encoding/src/Binary.php b/3rdparty/paragonie/constant_time_encoding/src/Binary.php new file mode 100644 index 00000000..828f3e0f --- /dev/null +++ b/3rdparty/paragonie/constant_time_encoding/src/Binary.php @@ -0,0 +1,90 @@ + $chunk */ + $chunk = \unpack('C', $binString[$i]); + $c = $chunk[1] & 0xf; + $b = $chunk[1] >> 4; + + $hex .= \pack( + 'CC', + (87 + $b + ((($b - 10) >> 8) & ~38)), + (87 + $c + ((($c - 10) >> 8) & ~38)) + ); + } + return $hex; + } + + /** + * Convert a binary string into a hexadecimal string without cache-timing + * leaks, returning uppercase letters (as per RFC 4648) + * + * @param string $binString (raw binary) + * @return string + * @throws TypeError + */ + public static function encodeUpper(string $binString): string + { + $hex = ''; + $len = Binary::safeStrlen($binString); + + for ($i = 0; $i < $len; ++$i) { + /** @var array $chunk */ + $chunk = \unpack('C', $binString[$i]); + $c = $chunk[1] & 0xf; + $b = $chunk[1] >> 4; + + $hex .= \pack( + 'CC', + (55 + $b + ((($b - 10) >> 8) & ~6)), + (55 + $c + ((($c - 10) >> 8) & ~6)) + ); + } + return $hex; + } + + /** + * Convert a hexadecimal string into a binary string without cache-timing + * leaks + * + * @param string $encodedString + * @param bool $strictPadding + * @return string (raw binary) + * @throws RangeException + */ + public static function decode( + string $encodedString, + bool $strictPadding = false + ): string { + $hex_pos = 0; + $bin = ''; + $c_acc = 0; + $hex_len = Binary::safeStrlen($encodedString); + $state = 0; + if (($hex_len & 1) !== 0) { + if ($strictPadding) { + throw new RangeException( + 'Expected an even number of hexadecimal characters' + ); + } else { + $encodedString = '0' . $encodedString; + ++$hex_len; + } + } + + /** @var array $chunk */ + $chunk = \unpack('C*', $encodedString); + while ($hex_pos < $hex_len) { + ++$hex_pos; + $c = $chunk[$hex_pos]; + $c_num = $c ^ 48; + $c_num0 = ($c_num - 10) >> 8; + $c_alpha = ($c & ~32) - 55; + $c_alpha0 = (($c_alpha - 10) ^ ($c_alpha - 16)) >> 8; + + if (($c_num0 | $c_alpha0) === 0) { + throw new RangeException( + 'Expected hexadecimal character' + ); + } + $c_val = ($c_num0 & $c_num) | ($c_alpha & $c_alpha0); + if ($state === 0) { + $c_acc = $c_val * 16; + } else { + $bin .= \pack('C', $c_acc | $c_val); + } + $state ^= 1; + } + return $bin; + } +} diff --git a/3rdparty/paragonie/constant_time_encoding/src/RFC4648.php b/3rdparty/paragonie/constant_time_encoding/src/RFC4648.php new file mode 100644 index 00000000..f124d65b --- /dev/null +++ b/3rdparty/paragonie/constant_time_encoding/src/RFC4648.php @@ -0,0 +1,186 @@ + "Zm9v" + * + * @param string $str + * @return string + * + * @throws TypeError + */ + public static function base64Encode(string $str): string + { + return Base64::encode($str); + } + + /** + * RFC 4648 Base64 decoding + * + * "Zm9v" -> "foo" + * + * @param string $str + * @return string + * + * @throws TypeError + */ + public static function base64Decode(string $str): string + { + return Base64::decode($str, true); + } + + /** + * RFC 4648 Base64 (URL Safe) encoding + * + * "foo" -> "Zm9v" + * + * @param string $str + * @return string + * + * @throws TypeError + */ + public static function base64UrlSafeEncode(string $str): string + { + return Base64UrlSafe::encode($str); + } + + /** + * RFC 4648 Base64 (URL Safe) decoding + * + * "Zm9v" -> "foo" + * + * @param string $str + * @return string + * + * @throws TypeError + */ + public static function base64UrlSafeDecode(string $str): string + { + return Base64UrlSafe::decode($str, true); + } + + /** + * RFC 4648 Base32 encoding + * + * "foo" -> "MZXW6===" + * + * @param string $str + * @return string + * + * @throws TypeError + */ + public static function base32Encode(string $str): string + { + return Base32::encodeUpper($str); + } + + /** + * RFC 4648 Base32 encoding + * + * "MZXW6===" -> "foo" + * + * @param string $str + * @return string + * + * @throws TypeError + */ + public static function base32Decode(string $str): string + { + return Base32::decodeUpper($str, true); + } + + /** + * RFC 4648 Base32-Hex encoding + * + * "foo" -> "CPNMU===" + * + * @param string $str + * @return string + * + * @throws TypeError + */ + public static function base32HexEncode(string $str): string + { + return Base32::encodeUpper($str); + } + + /** + * RFC 4648 Base32-Hex decoding + * + * "CPNMU===" -> "foo" + * + * @param string $str + * @return string + * + * @throws TypeError + */ + public static function base32HexDecode(string $str): string + { + return Base32::decodeUpper($str, true); + } + + /** + * RFC 4648 Base16 decoding + * + * "foo" -> "666F6F" + * + * @param string $str + * @return string + * + * @throws TypeError + */ + public static function base16Encode(string $str): string + { + return Hex::encodeUpper($str); + } + + /** + * RFC 4648 Base16 decoding + * + * "666F6F" -> "foo" + * + * @param string $str + * @return string + */ + public static function base16Decode(string $str): string + { + return Hex::decode($str, true); + } +} \ No newline at end of file diff --git a/3rdparty/pear/archive_tar/Archive/Tar.php b/3rdparty/pear/archive_tar/Archive/Tar.php new file mode 100644 index 00000000..03daa39f --- /dev/null +++ b/3rdparty/pear/archive_tar/Archive/Tar.php @@ -0,0 +1,2530 @@ + + * All rights reserved. + * + * Redistribution and use in source and binary forms, with or without + * modification, are permitted provided that the following conditions are met: + * + * * Redistributions of source code must retain the above copyright notice, + * this list of conditions and the following disclaimer. + * * Redistributions in binary form must reproduce the above copyright + * notice, this list of conditions and the following disclaimer in the + * documentation and/or other materials provided with the distribution. + * + * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" + * AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE + * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE + * DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE + * FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL + * DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR + * SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER + * CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, + * OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE + * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + * + * @category File_Formats + * @package Archive_Tar + * @author Vincent Blavet + * @copyright 1997-2010 The Authors + * @license http://www.opensource.org/licenses/bsd-license.php New BSD License + * @version CVS: $Id$ + * @link http://pear.php.net/package/Archive_Tar + */ + +// If the PEAR class cannot be loaded via the autoloader, +// then try to require_once it from the PHP include path. +if (!class_exists('PEAR')) { + require_once 'PEAR.php'; +} + +define('ARCHIVE_TAR_ATT_SEPARATOR', 90001); +define('ARCHIVE_TAR_END_BLOCK', pack("a512", '')); + +if (!function_exists('gzopen') && function_exists('gzopen64')) { + function gzopen($filename, $mode, $use_include_path = 0) + { + return gzopen64($filename, $mode, $use_include_path); + } +} + +if (!function_exists('gztell') && function_exists('gztell64')) { + function gztell($zp) + { + return gztell64($zp); + } +} + +if (!function_exists('gzseek') && function_exists('gzseek64')) { + function gzseek($zp, $offset, $whence = SEEK_SET) + { + return gzseek64($zp, $offset, $whence); + } +} + +/** + * Creates a (compressed) Tar archive + * + * @package Archive_Tar + * @author Vincent Blavet + * @license http://www.opensource.org/licenses/bsd-license.php New BSD License + * @version $Revision$ + */ +class Archive_Tar extends PEAR +{ + /** + * @var string Name of the Tar + */ + public $_tarname = ''; + + /** + * @var boolean if true, the Tar file will be gzipped + */ + public $_compress = false; + + /** + * @var string Type of compression : 'none', 'gz', 'bz2' or 'lzma2' + */ + public $_compress_type = 'none'; + + /** + * @var string Explode separator + */ + public $_separator = ' '; + + /** + * @var file descriptor + */ + public $_file = 0; + + /** + * @var string Local Tar name of a remote Tar (http:// or ftp://) + */ + public $_temp_tarname = ''; + + /** + * @var string regular expression for ignoring files or directories + */ + public $_ignore_regexp = ''; + + /** + * @var object PEAR_Error object + */ + public $error_object = null; + + /** + * Format for data extraction + * + * @var string + */ + public $_fmt = ''; + + /** + * @var int Length of the read buffer in bytes + */ + protected $buffer_length; + + /** + * Archive_Tar Class constructor. This flavour of the constructor only + * declare a new Archive_Tar object, identifying it by the name of the + * tar file. + * If the compress argument is set the tar will be read or created as a + * gzip or bz2 compressed TAR file. + * + * @param string $p_tarname The name of the tar archive to create + * @param string $p_compress can be null, 'gz', 'bz2' or 'lzma2'. This + * parameter indicates if gzip, bz2 or lzma2 compression + * is required. For compatibility reason the + * boolean value 'true' means 'gz'. + * @param int $buffer_length Length of the read buffer in bytes + * + * @return bool + */ + public function __construct($p_tarname, $p_compress = null, $buffer_length = 512) + { + parent::__construct(); + + $this->_compress = false; + $this->_compress_type = 'none'; + if (($p_compress === null) || ($p_compress == '')) { + if (@file_exists($p_tarname)) { + if ($fp = @fopen($p_tarname, "rb")) { + // look for gzip magic cookie + $data = fread($fp, 2); + fclose($fp); + if ($data == "\37\213") { + $this->_compress = true; + $this->_compress_type = 'gz'; + // No sure it's enought for a magic code .... + } elseif ($data == "BZ") { + $this->_compress = true; + $this->_compress_type = 'bz2'; + } elseif (file_get_contents($p_tarname, false, null, 1, 4) == '7zXZ') { + $this->_compress = true; + $this->_compress_type = 'lzma2'; + } + } + } else { + // probably a remote file or some file accessible + // through a stream interface + if (substr($p_tarname, -2) == 'gz') { + $this->_compress = true; + $this->_compress_type = 'gz'; + } elseif ((substr($p_tarname, -3) == 'bz2') || + (substr($p_tarname, -2) == 'bz') + ) { + $this->_compress = true; + $this->_compress_type = 'bz2'; + } else { + if (substr($p_tarname, -2) == 'xz') { + $this->_compress = true; + $this->_compress_type = 'lzma2'; + } + } + } + } else { + if (($p_compress === true) || ($p_compress == 'gz')) { + $this->_compress = true; + $this->_compress_type = 'gz'; + } else { + if ($p_compress == 'bz2') { + $this->_compress = true; + $this->_compress_type = 'bz2'; + } else { + if ($p_compress == 'lzma2') { + $this->_compress = true; + $this->_compress_type = 'lzma2'; + } else { + $this->_error( + "Unsupported compression type '$p_compress'\n" . + "Supported types are 'gz', 'bz2' and 'lzma2'.\n" + ); + return false; + } + } + } + } + $this->_tarname = $p_tarname; + if ($this->_compress) { // assert zlib or bz2 or xz extension support + if ($this->_compress_type == 'gz') { + $extname = 'zlib'; + } else { + if ($this->_compress_type == 'bz2') { + $extname = 'bz2'; + } else { + if ($this->_compress_type == 'lzma2') { + $extname = 'xz'; + } + } + } + + if (!extension_loaded($extname)) { + PEAR::loadExtension($extname); + } + if (!extension_loaded($extname)) { + $this->_error( + "The extension '$extname' couldn't be found.\n" . + "Please make sure your version of PHP was built " . + "with '$extname' support.\n" + ); + return false; + } + } + + + if (version_compare(PHP_VERSION, "5.5.0-dev") < 0) { + $this->_fmt = "a100filename/a8mode/a8uid/a8gid/a12size/a12mtime/" . + "a8checksum/a1typeflag/a100link/a6magic/a2version/" . + "a32uname/a32gname/a8devmajor/a8devminor/a131prefix"; + } else { + $this->_fmt = "Z100filename/Z8mode/Z8uid/Z8gid/Z12size/Z12mtime/" . + "Z8checksum/Z1typeflag/Z100link/Z6magic/Z2version/" . + "Z32uname/Z32gname/Z8devmajor/Z8devminor/Z131prefix"; + } + + + $this->buffer_length = $buffer_length; + } + + public function __destruct() + { + $this->_close(); + // ----- Look for a local copy to delete + if ($this->_temp_tarname != '' && (bool) preg_match('/^tar[[:alnum:]]*\.tmp$/', $this->_temp_tarname)) { + @unlink($this->_temp_tarname); + } + } + + /** + * This method creates the archive file and add the files / directories + * that are listed in $p_filelist. + * If a file with the same name exist and is writable, it is replaced + * by the new tar. + * The method return false and a PEAR error text. + * The $p_filelist parameter can be an array of string, each string + * representing a filename or a directory name with their path if + * needed. It can also be a single string with names separated by a + * single blank. + * For each directory added in the archive, the files and + * sub-directories are also added. + * See also createModify() method for more details. + * + * @param array $p_filelist An array of filenames and directory names, or a + * single string with names separated by a single + * blank space. + * + * @return bool true on success, false on error. + * @see createModify() + */ + public function create($p_filelist) + { + return $this->createModify($p_filelist, '', ''); + } + + /** + * This method add the files / directories that are listed in $p_filelist in + * the archive. If the archive does not exist it is created. + * The method return false and a PEAR error text. + * The files and directories listed are only added at the end of the archive, + * even if a file with the same name is already archived. + * See also createModify() method for more details. + * + * @param array $p_filelist An array of filenames and directory names, or a + * single string with names separated by a single + * blank space. + * + * @return bool true on success, false on error. + * @see createModify() + * @access public + */ + public function add($p_filelist) + { + return $this->addModify($p_filelist, '', ''); + } + + /** + * @param string $p_path + * @param bool $p_preserve + * @param bool $p_symlinks + * @return bool + */ + public function extract($p_path = '', $p_preserve = false, $p_symlinks = true) + { + return $this->extractModify($p_path, '', $p_preserve, $p_symlinks); + } + + /** + * @return array|int + */ + public function listContent() + { + $v_list_detail = array(); + + if ($this->_openRead()) { + if (!$this->_extractList('', $v_list_detail, "list", '', '')) { + unset($v_list_detail); + $v_list_detail = 0; + } + $this->_close(); + } + + return $v_list_detail; + } + + /** + * This method creates the archive file and add the files / directories + * that are listed in $p_filelist. + * If the file already exists and is writable, it is replaced by the + * new tar. It is a create and not an add. If the file exists and is + * read-only or is a directory it is not replaced. The method return + * false and a PEAR error text. + * The $p_filelist parameter can be an array of string, each string + * representing a filename or a directory name with their path if + * needed. It can also be a single string with names separated by a + * single blank. + * The path indicated in $p_remove_dir will be removed from the + * memorized path of each file / directory listed when this path + * exists. By default nothing is removed (empty path '') + * The path indicated in $p_add_dir will be added at the beginning of + * the memorized path of each file / directory listed. However it can + * be set to empty ''. The adding of a path is done after the removing + * of path. + * The path add/remove ability enables the user to prepare an archive + * for extraction in a different path than the origin files are. + * See also addModify() method for file adding properties. + * + * @param array $p_filelist An array of filenames and directory names, + * or a single string with names separated by + * a single blank space. + * @param string $p_add_dir A string which contains a path to be added + * to the memorized path of each element in + * the list. + * @param string $p_remove_dir A string which contains a path to be + * removed from the memorized path of each + * element in the list, when relevant. + * + * @return boolean true on success, false on error. + * @see addModify() + */ + public function createModify($p_filelist, $p_add_dir, $p_remove_dir = '') + { + $v_result = true; + + if (!$this->_openWrite()) { + return false; + } + + if ($p_filelist != '') { + if (is_array($p_filelist)) { + $v_list = $p_filelist; + } elseif (is_string($p_filelist)) { + $v_list = explode($this->_separator, $p_filelist); + } else { + $this->_cleanFile(); + $this->_error('Invalid file list'); + return false; + } + + $v_result = $this->_addList($v_list, $p_add_dir, $p_remove_dir); + } + + if ($v_result) { + $this->_writeFooter(); + $this->_close(); + } else { + $this->_cleanFile(); + } + + return $v_result; + } + + /** + * This method add the files / directories listed in $p_filelist at the + * end of the existing archive. If the archive does not yet exists it + * is created. + * The $p_filelist parameter can be an array of string, each string + * representing a filename or a directory name with their path if + * needed. It can also be a single string with names separated by a + * single blank. + * The path indicated in $p_remove_dir will be removed from the + * memorized path of each file / directory listed when this path + * exists. By default nothing is removed (empty path '') + * The path indicated in $p_add_dir will be added at the beginning of + * the memorized path of each file / directory listed. However it can + * be set to empty ''. The adding of a path is done after the removing + * of path. + * The path add/remove ability enables the user to prepare an archive + * for extraction in a different path than the origin files are. + * If a file/dir is already in the archive it will only be added at the + * end of the archive. There is no update of the existing archived + * file/dir. However while extracting the archive, the last file will + * replace the first one. This results in a none optimization of the + * archive size. + * If a file/dir does not exist the file/dir is ignored. However an + * error text is send to PEAR error. + * If a file/dir is not readable the file/dir is ignored. However an + * error text is send to PEAR error. + * + * @param array $p_filelist An array of filenames and directory + * names, or a single string with names + * separated by a single blank space. + * @param string $p_add_dir A string which contains a path to be + * added to the memorized path of each + * element in the list. + * @param string $p_remove_dir A string which contains a path to be + * removed from the memorized path of + * each element in the list, when + * relevant. + * + * @return bool true on success, false on error. + */ + public function addModify($p_filelist, $p_add_dir, $p_remove_dir = '') + { + $v_result = true; + + if (!$this->_isArchive()) { + $v_result = $this->createModify( + $p_filelist, + $p_add_dir, + $p_remove_dir + ); + } else { + if (is_array($p_filelist)) { + $v_list = $p_filelist; + } elseif (is_string($p_filelist)) { + $v_list = explode($this->_separator, $p_filelist); + } else { + $this->_error('Invalid file list'); + return false; + } + + $v_result = $this->_append($v_list, $p_add_dir, $p_remove_dir); + } + + return $v_result; + } + + /** + * This method add a single string as a file at the + * end of the existing archive. If the archive does not yet exists it + * is created. + * + * @param string $p_filename A string which contains the full + * filename path that will be associated + * with the string. + * @param string $p_string The content of the file added in + * the archive. + * @param bool|int $p_datetime A custom date/time (unix timestamp) + * for the file (optional). + * @param array $p_params An array of optional params: + * stamp => the datetime (replaces + * datetime above if it exists) + * mode => the permissions on the + * file (600 by default) + * type => is this a link? See the + * tar specification for details. + * (default = regular file) + * uid => the user ID of the file + * (default = 0 = root) + * gid => the group ID of the file + * (default = 0 = root) + * + * @return bool true on success, false on error. + */ + public function addString($p_filename, $p_string, $p_datetime = false, $p_params = array()) + { + $p_stamp = @$p_params["stamp"] ? $p_params["stamp"] : ($p_datetime ? $p_datetime : time()); + $p_mode = @$p_params["mode"] ? $p_params["mode"] : 0600; + $p_type = @$p_params["type"] ? $p_params["type"] : ""; + $p_uid = @$p_params["uid"] ? $p_params["uid"] : ""; + $p_gid = @$p_params["gid"] ? $p_params["gid"] : ""; + $v_result = true; + + if (!$this->_isArchive()) { + if (!$this->_openWrite()) { + return false; + } + $this->_close(); + } + + if (!$this->_openAppend()) { + return false; + } + + // Need to check the get back to the temporary file ? .... + $v_result = $this->_addString($p_filename, $p_string, $p_datetime, $p_params); + + $this->_writeFooter(); + + $this->_close(); + + return $v_result; + } + + /** + * This method extract all the content of the archive in the directory + * indicated by $p_path. When relevant the memorized path of the + * files/dir can be modified by removing the $p_remove_path path at the + * beginning of the file/dir path. + * While extracting a file, if the directory path does not exists it is + * created. + * While extracting a file, if the file already exists it is replaced + * without looking for last modification date. + * While extracting a file, if the file already exists and is write + * protected, the extraction is aborted. + * While extracting a file, if a directory with the same name already + * exists, the extraction is aborted. + * While extracting a directory, if a file with the same name already + * exists, the extraction is aborted. + * While extracting a file/directory if the destination directory exist + * and is write protected, or does not exist but can not be created, + * the extraction is aborted. + * If after extraction an extracted file does not show the correct + * stored file size, the extraction is aborted. + * When the extraction is aborted, a PEAR error text is set and false + * is returned. However the result can be a partial extraction that may + * need to be manually cleaned. + * + * @param string $p_path The path of the directory where the + * files/dir need to by extracted. + * @param string $p_remove_path Part of the memorized path that can be + * removed if present at the beginning of + * the file/dir path. + * @param boolean $p_preserve Preserve user/group ownership of files + * @param boolean $p_symlinks Allow symlinks. + * + * @return boolean true on success, false on error. + * @see extractList() + */ + public function extractModify($p_path, $p_remove_path, $p_preserve = false, $p_symlinks = true) + { + $v_result = true; + $v_list_detail = array(); + + if ($v_result = $this->_openRead()) { + $v_result = $this->_extractList( + $p_path, + $v_list_detail, + "complete", + 0, + $p_remove_path, + $p_preserve, + $p_symlinks + ); + $this->_close(); + } + + return $v_result; + } + + /** + * This method extract from the archive one file identified by $p_filename. + * The return value is a string with the file content, or NULL on error. + * + * @param string $p_filename The path of the file to extract in a string. + * + * @return a string with the file content or NULL. + */ + public function extractInString($p_filename) + { + if ($this->_openRead()) { + $v_result = $this->_extractInString($p_filename); + $this->_close(); + } else { + $v_result = null; + } + + return $v_result; + } + + /** + * This method extract from the archive only the files indicated in the + * $p_filelist. These files are extracted in the current directory or + * in the directory indicated by the optional $p_path parameter. + * If indicated the $p_remove_path can be used in the same way as it is + * used in extractModify() method. + * + * @param array $p_filelist An array of filenames and directory names, + * or a single string with names separated + * by a single blank space. + * @param string $p_path The path of the directory where the + * files/dir need to by extracted. + * @param string $p_remove_path Part of the memorized path that can be + * removed if present at the beginning of + * the file/dir path. + * @param boolean $p_preserve Preserve user/group ownership of files + * @param boolean $p_symlinks Allow symlinks. + * + * @return bool true on success, false on error. + * @see extractModify() + */ + public function extractList($p_filelist, $p_path = '', $p_remove_path = '', $p_preserve = false, $p_symlinks = true) + { + $v_result = true; + $v_list_detail = array(); + + if (is_array($p_filelist)) { + $v_list = $p_filelist; + } elseif (is_string($p_filelist)) { + $v_list = explode($this->_separator, $p_filelist); + } else { + $this->_error('Invalid string list'); + return false; + } + + if ($v_result = $this->_openRead()) { + $v_result = $this->_extractList( + $p_path, + $v_list_detail, + "partial", + $v_list, + $p_remove_path, + $p_preserve, + $p_symlinks + ); + $this->_close(); + } + + return $v_result; + } + + /** + * This method set specific attributes of the archive. It uses a variable + * list of parameters, in the format attribute code + attribute values : + * $arch->setAttribute(ARCHIVE_TAR_ATT_SEPARATOR, ','); + * + * @return bool true on success, false on error. + */ + public function setAttribute() + { + $v_result = true; + + // ----- Get the number of variable list of arguments + if (($v_size = func_num_args()) == 0) { + return true; + } + + // ----- Get the arguments + $v_att_list = func_get_args(); + + // ----- Read the attributes + $i = 0; + while ($i < $v_size) { + + // ----- Look for next option + switch ($v_att_list[$i]) { + // ----- Look for options that request a string value + case ARCHIVE_TAR_ATT_SEPARATOR : + // ----- Check the number of parameters + if (($i + 1) >= $v_size) { + $this->_error( + 'Invalid number of parameters for ' + . 'attribute ARCHIVE_TAR_ATT_SEPARATOR' + ); + return false; + } + + // ----- Get the value + $this->_separator = $v_att_list[$i + 1]; + $i++; + break; + + default : + $this->_error('Unknown attribute code ' . $v_att_list[$i] . ''); + return false; + } + + // ----- Next attribute + $i++; + } + + return $v_result; + } + + /** + * This method sets the regular expression for ignoring files and directories + * at import, for example: + * $arch->setIgnoreRegexp("#CVS|\.svn#"); + * + * @param string $regexp regular expression defining which files or directories to ignore + */ + public function setIgnoreRegexp($regexp) + { + $this->_ignore_regexp = $regexp; + } + + /** + * This method sets the regular expression for ignoring all files and directories + * matching the filenames in the array list at import, for example: + * $arch->setIgnoreList(array('CVS', '.svn', 'bin/tool')); + * + * @param array $list a list of file or directory names to ignore + * + * @access public + */ + public function setIgnoreList($list) + { + $list = str_replace(array('#', '.', '^', '$'), array('\#', '\.', '\^', '\$'), $list); + $regexp = '#/' . join('$|/', $list) . '#'; + $this->setIgnoreRegexp($regexp); + } + + /** + * @param string $p_message + */ + public function _error($p_message) + { + $this->error_object = $this->raiseError($p_message); + } + + /** + * @param string $p_message + */ + public function _warning($p_message) + { + $this->error_object = $this->raiseError($p_message); + } + + /** + * @param string $p_filename + * @return bool + */ + public function _isArchive($p_filename = null) + { + if ($p_filename == null) { + $p_filename = $this->_tarname; + } + clearstatcache(); + return @is_file($p_filename) && !@is_link($p_filename); + } + + /** + * @return bool + */ + public function _openWrite() + { + if ($this->_compress_type == 'gz' && function_exists('gzopen')) { + $this->_file = @gzopen($this->_tarname, "wb9"); + } else { + if ($this->_compress_type == 'bz2' && function_exists('bzopen')) { + $this->_file = @bzopen($this->_tarname, "w"); + } else { + if ($this->_compress_type == 'lzma2' && function_exists('xzopen')) { + $this->_file = @xzopen($this->_tarname, 'w'); + } else { + if ($this->_compress_type == 'none') { + $this->_file = @fopen($this->_tarname, "wb"); + } else { + $this->_error( + 'Unknown or missing compression type (' + . $this->_compress_type . ')' + ); + return false; + } + } + } + } + + if ($this->_file == 0) { + $this->_error( + 'Unable to open in write mode \'' + . $this->_tarname . '\'' + ); + return false; + } + + return true; + } + + /** + * @return bool + */ + public function _openRead() + { + if (strtolower(substr($this->_tarname, 0, 7)) == 'http://') { + + // ----- Look if a local copy need to be done + if ($this->_temp_tarname == '') { + $this->_temp_tarname = uniqid('tar') . '.tmp'; + if (!$v_file_from = @fopen($this->_tarname, 'rb')) { + $this->_error( + 'Unable to open in read mode \'' + . $this->_tarname . '\'' + ); + $this->_temp_tarname = ''; + return false; + } + if (!$v_file_to = @fopen($this->_temp_tarname, 'wb')) { + $this->_error( + 'Unable to open in write mode \'' + . $this->_temp_tarname . '\'' + ); + $this->_temp_tarname = ''; + return false; + } + while ($v_data = @fread($v_file_from, 1024)) { + @fwrite($v_file_to, $v_data); + } + @fclose($v_file_from); + @fclose($v_file_to); + } + + // ----- File to open if the local copy + $v_filename = $this->_temp_tarname; + } else { + // ----- File to open if the normal Tar file + + $v_filename = $this->_tarname; + } + + if ($this->_compress_type == 'gz' && function_exists('gzopen')) { + $this->_file = @gzopen($v_filename, "rb"); + } else { + if ($this->_compress_type == 'bz2' && function_exists('bzopen')) { + $this->_file = @bzopen($v_filename, "r"); + } else { + if ($this->_compress_type == 'lzma2' && function_exists('xzopen')) { + $this->_file = @xzopen($v_filename, "r"); + } else { + if ($this->_compress_type == 'none') { + $this->_file = @fopen($v_filename, "rb"); + } else { + $this->_error( + 'Unknown or missing compression type (' + . $this->_compress_type . ')' + ); + return false; + } + } + } + } + + if ($this->_file == 0) { + $this->_error('Unable to open in read mode \'' . $v_filename . '\''); + return false; + } + + return true; + } + + /** + * @return bool + */ + public function _openReadWrite() + { + if ($this->_compress_type == 'gz') { + $this->_file = @gzopen($this->_tarname, "r+b"); + } else { + if ($this->_compress_type == 'bz2') { + $this->_error( + 'Unable to open bz2 in read/write mode \'' + . $this->_tarname . '\' (limitation of bz2 extension)' + ); + return false; + } else { + if ($this->_compress_type == 'lzma2') { + $this->_error( + 'Unable to open lzma2 in read/write mode \'' + . $this->_tarname . '\' (limitation of lzma2 extension)' + ); + return false; + } else { + if ($this->_compress_type == 'none') { + $this->_file = @fopen($this->_tarname, "r+b"); + } else { + $this->_error( + 'Unknown or missing compression type (' + . $this->_compress_type . ')' + ); + return false; + } + } + } + } + + if ($this->_file == 0) { + $this->_error( + 'Unable to open in read/write mode \'' + . $this->_tarname . '\'' + ); + return false; + } + + return true; + } + + /** + * @return bool + */ + public function _close() + { + //if (isset($this->_file)) { + if (is_resource($this->_file)) { + if ($this->_compress_type == 'gz') { + @gzclose($this->_file); + } else { + if ($this->_compress_type == 'bz2') { + @bzclose($this->_file); + } else { + if ($this->_compress_type == 'lzma2') { + @xzclose($this->_file); + } else { + if ($this->_compress_type == 'none') { + @fclose($this->_file); + } else { + $this->_error( + 'Unknown or missing compression type (' + . $this->_compress_type . ')' + ); + } + } + } + } + + $this->_file = 0; + } + + // ----- Look if a local copy need to be erase + // Note that it might be interesting to keep the url for a time : ToDo + if ($this->_temp_tarname != '') { + @unlink($this->_temp_tarname); + $this->_temp_tarname = ''; + } + + return true; + } + + /** + * @return bool + */ + public function _cleanFile() + { + $this->_close(); + + // ----- Look for a local copy + if ($this->_temp_tarname != '') { + // ----- Remove the local copy but not the remote tarname + @unlink($this->_temp_tarname); + $this->_temp_tarname = ''; + } else { + // ----- Remove the local tarname file + @unlink($this->_tarname); + } + $this->_tarname = ''; + + return true; + } + + /** + * @param mixed $p_binary_data + * @param integer $p_len + * @return bool + */ + public function _writeBlock($p_binary_data, $p_len = null) + { + if (is_resource($this->_file)) { + if ($p_len === null) { + if ($this->_compress_type == 'gz') { + @gzputs($this->_file, $p_binary_data); + } else { + if ($this->_compress_type == 'bz2') { + @bzwrite($this->_file, $p_binary_data); + } else { + if ($this->_compress_type == 'lzma2') { + @xzwrite($this->_file, $p_binary_data); + } else { + if ($this->_compress_type == 'none') { + @fputs($this->_file, $p_binary_data); + } else { + $this->_error( + 'Unknown or missing compression type (' + . $this->_compress_type . ')' + ); + } + } + } + } + } else { + if ($this->_compress_type == 'gz') { + @gzputs($this->_file, $p_binary_data, $p_len); + } else { + if ($this->_compress_type == 'bz2') { + @bzwrite($this->_file, $p_binary_data, $p_len); + } else { + if ($this->_compress_type == 'lzma2') { + @xzwrite($this->_file, $p_binary_data, $p_len); + } else { + if ($this->_compress_type == 'none') { + @fputs($this->_file, $p_binary_data, $p_len); + } else { + $this->_error( + 'Unknown or missing compression type (' + . $this->_compress_type . ')' + ); + } + } + } + } + } + } + return true; + } + + /** + * @return null|string + */ + public function _readBlock() + { + $v_block = null; + if (is_resource($this->_file)) { + if ($this->_compress_type == 'gz') { + $v_block = @gzread($this->_file, 512); + } else { + if ($this->_compress_type == 'bz2') { + $v_block = @bzread($this->_file, 512); + } else { + if ($this->_compress_type == 'lzma2') { + $v_block = @xzread($this->_file, 512); + } else { + if ($this->_compress_type == 'none') { + $v_block = @fread($this->_file, 512); + } else { + $this->_error( + 'Unknown or missing compression type (' + . $this->_compress_type . ')' + ); + } + } + } + } + } + return $v_block; + } + + /** + * @param null $p_len + * @return bool + */ + public function _jumpBlock($p_len = null) + { + if (is_resource($this->_file)) { + if ($p_len === null) { + $p_len = 1; + } + + if ($this->_compress_type == 'gz') { + @gzseek($this->_file, gztell($this->_file) + ($p_len * 512)); + } else { + if ($this->_compress_type == 'bz2') { + // ----- Replace missing bztell() and bzseek() + for ($i = 0; $i < $p_len; $i++) { + $this->_readBlock(); + } + } else { + if ($this->_compress_type == 'lzma2') { + // ----- Replace missing xztell() and xzseek() + for ($i = 0; $i < $p_len; $i++) { + $this->_readBlock(); + } + } else { + if ($this->_compress_type == 'none') { + @fseek($this->_file, $p_len * 512, SEEK_CUR); + } else { + $this->_error( + 'Unknown or missing compression type (' + . $this->_compress_type . ')' + ); + } + } + } + } + } + return true; + } + + /** + * @return bool + */ + public function _writeFooter() + { + if (is_resource($this->_file)) { + // ----- Write the last 0 filled block for end of archive + $v_binary_data = pack('a1024', ''); + $this->_writeBlock($v_binary_data); + } + return true; + } + + /** + * @param array $p_list + * @param string $p_add_dir + * @param string $p_remove_dir + * @return bool + */ + public function _addList($p_list, $p_add_dir, $p_remove_dir) + { + $v_result = true; + $v_header = array(); + + // ----- Remove potential windows directory separator + $p_add_dir = $this->_translateWinPath($p_add_dir); + $p_remove_dir = $this->_translateWinPath($p_remove_dir, false); + + if (!$this->_file) { + $this->_error('Invalid file descriptor'); + return false; + } + + if (sizeof($p_list) == 0) { + return true; + } + + foreach ($p_list as $v_filename) { + if (!$v_result) { + break; + } + + // ----- Skip the current tar name + if ($v_filename == $this->_tarname) { + continue; + } + + if ($v_filename == '') { + continue; + } + + // ----- ignore files and directories matching the ignore regular expression + if ($this->_ignore_regexp && preg_match($this->_ignore_regexp, '/' . $v_filename)) { + $this->_warning("File '$v_filename' ignored"); + continue; + } + + if (!file_exists($v_filename) && !is_link($v_filename)) { + $this->_warning("File '$v_filename' does not exist"); + continue; + } + + // ----- Add the file or directory header + if (!$this->_addFile($v_filename, $v_header, $p_add_dir, $p_remove_dir)) { + return false; + } + + if (@is_dir($v_filename) && !@is_link($v_filename)) { + if (!($p_hdir = opendir($v_filename))) { + $this->_warning("Directory '$v_filename' can not be read"); + continue; + } + while (false !== ($p_hitem = readdir($p_hdir))) { + if (($p_hitem != '.') && ($p_hitem != '..')) { + if ($v_filename != ".") { + $p_temp_list[0] = $v_filename . '/' . $p_hitem; + } else { + $p_temp_list[0] = $p_hitem; + } + + $v_result = $this->_addList( + $p_temp_list, + $p_add_dir, + $p_remove_dir + ); + } + } + + unset($p_temp_list); + unset($p_hdir); + unset($p_hitem); + } + } + + return $v_result; + } + + /** + * @param string $p_filename + * @param mixed $p_header + * @param string $p_add_dir + * @param string $p_remove_dir + * @param null $v_stored_filename + * @return bool + */ + public function _addFile($p_filename, &$p_header, $p_add_dir, $p_remove_dir, $v_stored_filename = null) + { + if (!$this->_file) { + $this->_error('Invalid file descriptor'); + return false; + } + + if ($p_filename == '') { + $this->_error('Invalid file name'); + return false; + } + + if (is_null($v_stored_filename)) { + // ----- Calculate the stored filename + $p_filename = $this->_translateWinPath($p_filename, false); + $v_stored_filename = $p_filename; + + if (strcmp($p_filename, $p_remove_dir) == 0) { + return true; + } + + if ($p_remove_dir != '') { + if (substr($p_remove_dir, -1) != '/') { + $p_remove_dir .= '/'; + } + + if (substr($p_filename, 0, strlen($p_remove_dir)) == $p_remove_dir) { + $v_stored_filename = substr($p_filename, strlen($p_remove_dir)); + } + } + + $v_stored_filename = $this->_translateWinPath($v_stored_filename); + if ($p_add_dir != '') { + if (substr($p_add_dir, -1) == '/') { + $v_stored_filename = $p_add_dir . $v_stored_filename; + } else { + $v_stored_filename = $p_add_dir . '/' . $v_stored_filename; + } + } + + $v_stored_filename = $this->_pathReduction($v_stored_filename); + } + + if ($this->_isArchive($p_filename)) { + if (($v_file = @fopen($p_filename, "rb")) == 0) { + $this->_warning( + "Unable to open file '" . $p_filename + . "' in binary read mode" + ); + return true; + } + + if (!$this->_writeHeader($p_filename, $v_stored_filename)) { + return false; + } + + while (($v_buffer = fread($v_file, $this->buffer_length)) != '') { + $buffer_length = strlen("$v_buffer"); + if ($buffer_length != $this->buffer_length) { + $pack_size = ((int)($buffer_length / 512) + ($buffer_length % 512 !== 0 ? 1 : 0)) * 512; + $pack_format = sprintf('a%d', $pack_size); + } else { + $pack_format = sprintf('a%d', $this->buffer_length); + } + $v_binary_data = pack($pack_format, "$v_buffer"); + $this->_writeBlock($v_binary_data); + } + + fclose($v_file); + } else { + // ----- Only header for dir + if (!$this->_writeHeader($p_filename, $v_stored_filename)) { + return false; + } + } + + return true; + } + + /** + * @param string $p_filename + * @param string $p_string + * @param bool $p_datetime + * @param array $p_params + * @return bool + */ + public function _addString($p_filename, $p_string, $p_datetime = false, $p_params = array()) + { + $p_stamp = @$p_params["stamp"] ? $p_params["stamp"] : ($p_datetime ? $p_datetime : time()); + $p_mode = @$p_params["mode"] ? $p_params["mode"] : 0600; + $p_type = @$p_params["type"] ? $p_params["type"] : ""; + $p_uid = @$p_params["uid"] ? $p_params["uid"] : 0; + $p_gid = @$p_params["gid"] ? $p_params["gid"] : 0; + if (!$this->_file) { + $this->_error('Invalid file descriptor'); + return false; + } + + if ($p_filename == '') { + $this->_error('Invalid file name'); + return false; + } + + // ----- Calculate the stored filename + $p_filename = $this->_translateWinPath($p_filename, false); + + // ----- If datetime is not specified, set current time + if ($p_datetime === false) { + $p_datetime = time(); + } + + if (!$this->_writeHeaderBlock( + $p_filename, + strlen($p_string), + $p_stamp, + $p_mode, + $p_type, + $p_uid, + $p_gid + ) + ) { + return false; + } + + $i = 0; + while (($v_buffer = substr($p_string, (($i++) * 512), 512)) != '') { + $v_binary_data = pack("a512", $v_buffer); + $this->_writeBlock($v_binary_data); + } + + return true; + } + + /** + * @param string $p_filename + * @param string $p_stored_filename + * @return bool + */ + public function _writeHeader($p_filename, $p_stored_filename) + { + if ($p_stored_filename == '') { + $p_stored_filename = $p_filename; + } + + $v_reduced_filename = $this->_pathReduction($p_stored_filename); + + if (strlen($v_reduced_filename) > 99) { + if (!$this->_writeLongHeader($v_reduced_filename, false)) { + return false; + } + } + + $v_linkname = ''; + if (@is_link($p_filename)) { + $v_linkname = readlink($p_filename); + } + + if (strlen($v_linkname) > 99) { + if (!$this->_writeLongHeader($v_linkname, true)) { + return false; + } + } + + $v_info = lstat($p_filename); + $v_uid = sprintf("%07s", DecOct($v_info[4])); + $v_gid = sprintf("%07s", DecOct($v_info[5])); + $v_perms = sprintf("%07s", DecOct($v_info['mode'] & 000777)); + $v_mtime = sprintf("%011s", DecOct($v_info['mtime'])); + + if (@is_link($p_filename)) { + $v_typeflag = '2'; + $v_size = sprintf("%011s", DecOct(0)); + } elseif (@is_dir($p_filename)) { + $v_typeflag = "5"; + $v_size = sprintf("%011s", DecOct(0)); + } else { + $v_typeflag = '0'; + clearstatcache(); + $v_size = sprintf("%011s", DecOct($v_info['size'])); + } + + $v_magic = 'ustar '; + $v_version = ' '; + $v_uname = ''; + $v_gname = ''; + + if (function_exists('posix_getpwuid')) { + $userinfo = posix_getpwuid($v_info[4]); + $groupinfo = posix_getgrgid($v_info[5]); + + if (isset($userinfo['name'])) { + $v_uname = $userinfo['name']; + } + + if (isset($groupinfo['name'])) { + $v_gname = $groupinfo['name']; + } + } + + $v_devmajor = ''; + $v_devminor = ''; + $v_prefix = ''; + + $v_binary_data_first = pack( + "a100a8a8a8a12a12", + $v_reduced_filename, + $v_perms, + $v_uid, + $v_gid, + $v_size, + $v_mtime + ); + $v_binary_data_last = pack( + "a1a100a6a2a32a32a8a8a155a12", + $v_typeflag, + $v_linkname, + $v_magic, + $v_version, + $v_uname, + $v_gname, + $v_devmajor, + $v_devminor, + $v_prefix, + '' + ); + + // ----- Calculate the checksum + $v_checksum = 0; + // ..... First part of the header + for ($i = 0; $i < 148; $i++) { + $v_checksum += ord(substr($v_binary_data_first, $i, 1)); + } + // ..... Ignore the checksum value and replace it by ' ' (space) + for ($i = 148; $i < 156; $i++) { + $v_checksum += ord(' '); + } + // ..... Last part of the header + for ($i = 156, $j = 0; $i < 512; $i++, $j++) { + $v_checksum += ord(substr($v_binary_data_last, $j, 1)); + } + + // ----- Write the first 148 bytes of the header in the archive + $this->_writeBlock($v_binary_data_first, 148); + + // ----- Write the calculated checksum + $v_checksum = sprintf("%06s\0 ", DecOct($v_checksum)); + $v_binary_data = pack("a8", $v_checksum); + $this->_writeBlock($v_binary_data, 8); + + // ----- Write the last 356 bytes of the header in the archive + $this->_writeBlock($v_binary_data_last, 356); + + return true; + } + + /** + * @param string $p_filename + * @param int $p_size + * @param int $p_mtime + * @param int $p_perms + * @param string $p_type + * @param int $p_uid + * @param int $p_gid + * @return bool + */ + public function _writeHeaderBlock( + $p_filename, + $p_size, + $p_mtime = 0, + $p_perms = 0, + $p_type = '', + $p_uid = 0, + $p_gid = 0 + ) + { + $p_filename = $this->_pathReduction($p_filename); + + if (strlen($p_filename) > 99) { + if (!$this->_writeLongHeader($p_filename, false)) { + return false; + } + } + + if ($p_type == "5") { + $v_size = sprintf("%011s", DecOct(0)); + } else { + $v_size = sprintf("%011s", DecOct($p_size)); + } + + $v_uid = sprintf("%07s", DecOct($p_uid)); + $v_gid = sprintf("%07s", DecOct($p_gid)); + $v_perms = sprintf("%07s", DecOct($p_perms & 000777)); + + $v_mtime = sprintf("%11s", DecOct($p_mtime)); + + $v_linkname = ''; + + $v_magic = 'ustar '; + + $v_version = ' '; + + if (function_exists('posix_getpwuid')) { + $userinfo = posix_getpwuid($p_uid); + $groupinfo = posix_getgrgid($p_gid); + + if ($userinfo === false || $groupinfo === false) { + $v_uname = ''; + $v_gname = ''; + } else { + $v_uname = $userinfo['name']; + $v_gname = $groupinfo['name']; + } + } else { + $v_uname = ''; + $v_gname = ''; + } + + $v_devmajor = ''; + + $v_devminor = ''; + + $v_prefix = ''; + + $v_binary_data_first = pack( + "a100a8a8a8a12A12", + $p_filename, + $v_perms, + $v_uid, + $v_gid, + $v_size, + $v_mtime + ); + $v_binary_data_last = pack( + "a1a100a6a2a32a32a8a8a155a12", + $p_type, + $v_linkname, + $v_magic, + $v_version, + $v_uname, + $v_gname, + $v_devmajor, + $v_devminor, + $v_prefix, + '' + ); + + // ----- Calculate the checksum + $v_checksum = 0; + // ..... First part of the header + for ($i = 0; $i < 148; $i++) { + $v_checksum += ord(substr($v_binary_data_first, $i, 1)); + } + // ..... Ignore the checksum value and replace it by ' ' (space) + for ($i = 148; $i < 156; $i++) { + $v_checksum += ord(' '); + } + // ..... Last part of the header + for ($i = 156, $j = 0; $i < 512; $i++, $j++) { + $v_checksum += ord(substr($v_binary_data_last, $j, 1)); + } + + // ----- Write the first 148 bytes of the header in the archive + $this->_writeBlock($v_binary_data_first, 148); + + // ----- Write the calculated checksum + $v_checksum = sprintf("%06s ", DecOct($v_checksum)); + $v_binary_data = pack("a8", $v_checksum); + $this->_writeBlock($v_binary_data, 8); + + // ----- Write the last 356 bytes of the header in the archive + $this->_writeBlock($v_binary_data_last, 356); + + return true; + } + + /** + * @param string $p_filename + * @return bool + */ + public function _writeLongHeader($p_filename, $is_link = false) + { + $v_uid = sprintf("%07s", 0); + $v_gid = sprintf("%07s", 0); + $v_perms = sprintf("%07s", 0); + $v_size = sprintf("%'011s", DecOct(strlen($p_filename))); + $v_mtime = sprintf("%011s", 0); + $v_typeflag = ($is_link ? 'K' : 'L'); + $v_linkname = ''; + $v_magic = 'ustar '; + $v_version = ' '; + $v_uname = ''; + $v_gname = ''; + $v_devmajor = ''; + $v_devminor = ''; + $v_prefix = ''; + + $v_binary_data_first = pack( + "a100a8a8a8a12a12", + '././@LongLink', + $v_perms, + $v_uid, + $v_gid, + $v_size, + $v_mtime + ); + $v_binary_data_last = pack( + "a1a100a6a2a32a32a8a8a155a12", + $v_typeflag, + $v_linkname, + $v_magic, + $v_version, + $v_uname, + $v_gname, + $v_devmajor, + $v_devminor, + $v_prefix, + '' + ); + + // ----- Calculate the checksum + $v_checksum = 0; + // ..... First part of the header + for ($i = 0; $i < 148; $i++) { + $v_checksum += ord(substr($v_binary_data_first, $i, 1)); + } + // ..... Ignore the checksum value and replace it by ' ' (space) + for ($i = 148; $i < 156; $i++) { + $v_checksum += ord(' '); + } + // ..... Last part of the header + for ($i = 156, $j = 0; $i < 512; $i++, $j++) { + $v_checksum += ord(substr($v_binary_data_last, $j, 1)); + } + + // ----- Write the first 148 bytes of the header in the archive + $this->_writeBlock($v_binary_data_first, 148); + + // ----- Write the calculated checksum + $v_checksum = sprintf("%06s\0 ", DecOct($v_checksum)); + $v_binary_data = pack("a8", $v_checksum); + $this->_writeBlock($v_binary_data, 8); + + // ----- Write the last 356 bytes of the header in the archive + $this->_writeBlock($v_binary_data_last, 356); + + // ----- Write the filename as content of the block + $i = 0; + while (($v_buffer = substr($p_filename, (($i++) * 512), 512)) != '') { + $v_binary_data = pack("a512", "$v_buffer"); + $this->_writeBlock($v_binary_data); + } + + return true; + } + + /** + * @param mixed $v_binary_data + * @param mixed $v_header + * @return bool + */ + public function _readHeader($v_binary_data, &$v_header) + { + if (strlen($v_binary_data) == 0) { + $v_header['filename'] = ''; + return true; + } + + if (strlen($v_binary_data) != 512) { + $v_header['filename'] = ''; + $this->_error('Invalid block size : ' . strlen($v_binary_data)); + return false; + } + + if (!is_array($v_header)) { + $v_header = array(); + } + // ----- Calculate the checksum + $v_checksum = 0; + // ..... First part of the header + $v_binary_split = str_split($v_binary_data); + $v_checksum += array_sum(array_map('ord', array_slice($v_binary_split, 0, 148))); + $v_checksum += array_sum(array_map('ord', array(' ', ' ', ' ', ' ', ' ', ' ', ' ', ' ',))); + $v_checksum += array_sum(array_map('ord', array_slice($v_binary_split, 156, 512))); + + + $v_data = unpack($this->_fmt, $v_binary_data); + + if (strlen($v_data["prefix"]) > 0) { + $v_data["filename"] = "$v_data[prefix]/$v_data[filename]"; + } + + // ----- Extract the checksum + $v_data_checksum = trim($v_data['checksum']); + if (!preg_match('/^[0-7]*$/', $v_data_checksum)) { + $this->_error( + 'Invalid checksum for file "' . $v_data['filename'] + . '" : ' . $v_data_checksum . ' extracted' + ); + return false; + } + + $v_header['checksum'] = OctDec($v_data_checksum); + if ($v_header['checksum'] != $v_checksum) { + $v_header['filename'] = ''; + + // ----- Look for last block (empty block) + if (($v_checksum == 256) && ($v_header['checksum'] == 0)) { + return true; + } + + $this->_error( + 'Invalid checksum for file "' . $v_data['filename'] + . '" : ' . $v_checksum . ' calculated, ' + . $v_header['checksum'] . ' expected' + ); + return false; + } + + // ----- Extract the properties + $v_header['filename'] = rtrim($v_data['filename'], "\0"); + if ($this->_isMaliciousFilename($v_header['filename'])) { + $this->_error( + 'Malicious .tar detected, file "' . $v_header['filename'] . + '" will not install in desired directory tree' + ); + return false; + } + $v_header['mode'] = OctDec(trim($v_data['mode'])); + $v_header['uid'] = OctDec(trim($v_data['uid'])); + $v_header['gid'] = OctDec(trim($v_data['gid'])); + $v_header['size'] = $this->_tarRecToSize($v_data['size']); + $v_header['mtime'] = OctDec(trim($v_data['mtime'])); + if (($v_header['typeflag'] = $v_data['typeflag']) == "5") { + $v_header['size'] = 0; + } + $v_header['link'] = trim($v_data['link']); + /* ----- All these fields are removed form the header because + they do not carry interesting info + $v_header[magic] = trim($v_data[magic]); + $v_header[version] = trim($v_data[version]); + $v_header[uname] = trim($v_data[uname]); + $v_header[gname] = trim($v_data[gname]); + $v_header[devmajor] = trim($v_data[devmajor]); + $v_header[devminor] = trim($v_data[devminor]); + */ + + return true; + } + + /** + * Convert Tar record size to actual size + * + * @param string $tar_size + * @return size of tar record in bytes + */ + private function _tarRecToSize($tar_size) + { + /* + * First byte of size has a special meaning if bit 7 is set. + * + * Bit 7 indicates base-256 encoding if set. + * Bit 6 is the sign bit. + * Bits 5:0 are most significant value bits. + */ + $ch = ord($tar_size[0]); + if ($ch & 0x80) { + // Full 12-bytes record is required. + $rec_str = $tar_size . "\x00"; + + $size = ($ch & 0x40) ? -1 : 0; + $size = ($size << 6) | ($ch & 0x3f); + + for ($num_ch = 1; $num_ch < 12; ++$num_ch) { + $size = ($size * 256) + ord($rec_str[$num_ch]); + } + + return $size; + + } else { + return OctDec(trim($tar_size)); + } + } + + /** + * Detect and report a malicious file name + * + * @param string $file + * + * @return bool + */ + private function _isMaliciousFilename($file) + { + if (strpos($file, '://') !== false) { + return true; + } + if (strpos($file, '../') !== false || strpos($file, '..\\') !== false) { + return true; + } + return false; + } + + /** + * @param $v_header + * @return bool + */ + public function _readLongHeader(&$v_header) + { + $v_filename = ''; + $v_filesize = $v_header['size']; + $n = floor($v_header['size'] / 512); + for ($i = 0; $i < $n; $i++) { + $v_content = $this->_readBlock(); + $v_filename .= $v_content; + } + if (($v_header['size'] % 512) != 0) { + $v_content = $this->_readBlock(); + $v_filename .= $v_content; + } + + // ----- Read the next header + $v_binary_data = $this->_readBlock(); + + if (!$this->_readHeader($v_binary_data, $v_header)) { + return false; + } + + $v_filename = rtrim(substr($v_filename, 0, $v_filesize), "\0"); + $v_header['filename'] = $v_filename; + if ($this->_isMaliciousFilename($v_filename)) { + $this->_error( + 'Malicious .tar detected, file "' . $v_filename . + '" will not install in desired directory tree' + ); + return false; + } + + return true; + } + + /** + * This method extract from the archive one file identified by $p_filename. + * The return value is a string with the file content, or null on error. + * + * @param string $p_filename The path of the file to extract in a string. + * + * @return a string with the file content or null. + */ + private function _extractInString($p_filename) + { + $v_result_str = ""; + + while (strlen($v_binary_data = $this->_readBlock()) != 0) { + if (!$this->_readHeader($v_binary_data, $v_header)) { + return null; + } + + if ($v_header['filename'] == '') { + continue; + } + + switch ($v_header['typeflag']) { + case 'L': + { + if (!$this->_readLongHeader($v_header)) { + return null; + } + } + break; + + case 'K': + { + $v_link_header = $v_header; + if (!$this->_readLongHeader($v_link_header)) { + return null; + } + $v_header['link'] = $v_link_header['filename']; + } + break; + } + + if ($v_header['filename'] == $p_filename) { + if ($v_header['typeflag'] == "5") { + $this->_error( + 'Unable to extract in string a directory ' + . 'entry {' . $v_header['filename'] . '}' + ); + return null; + } else { + $n = floor($v_header['size'] / 512); + for ($i = 0; $i < $n; $i++) { + $v_result_str .= $this->_readBlock(); + } + if (($v_header['size'] % 512) != 0) { + $v_content = $this->_readBlock(); + $v_result_str .= substr( + $v_content, + 0, + ($v_header['size'] % 512) + ); + } + return $v_result_str; + } + } else { + $this->_jumpBlock(ceil(($v_header['size'] / 512))); + } + } + + return null; + } + + /** + * @param string $p_path + * @param string $p_list_detail + * @param string $p_mode + * @param string $p_file_list + * @param string $p_remove_path + * @param bool $p_preserve + * @param bool $p_symlinks + * @return bool + */ + public function _extractList( + $p_path, + &$p_list_detail, + $p_mode, + $p_file_list, + $p_remove_path, + $p_preserve = false, + $p_symlinks = true + ) + { + $v_result = true; + $v_nb = 0; + $v_extract_all = true; + $v_listing = false; + + $p_path = $this->_translateWinPath($p_path, false); + if ($p_path == '' || (substr($p_path, 0, 1) != '/' + && substr($p_path, 0, 3) != "../" && !strpos($p_path, ':')) + ) { + $p_path = "./" . $p_path; + } + $p_remove_path = $this->_translateWinPath($p_remove_path); + + // ----- Look for path to remove format (should end by /) + if (($p_remove_path != '') && (substr($p_remove_path, -1) != '/')) { + $p_remove_path .= '/'; + } + $p_remove_path_size = strlen($p_remove_path); + + switch ($p_mode) { + case "complete" : + $v_extract_all = true; + $v_listing = false; + break; + case "partial" : + $v_extract_all = false; + $v_listing = false; + break; + case "list" : + $v_extract_all = false; + $v_listing = true; + break; + default : + $this->_error('Invalid extract mode (' . $p_mode . ')'); + return false; + } + + clearstatcache(); + + while (strlen($v_binary_data = $this->_readBlock()) != 0) { + $v_extract_file = false; + $v_extraction_stopped = 0; + + if (!$this->_readHeader($v_binary_data, $v_header)) { + return false; + } + + if ($v_header['filename'] == '') { + continue; + } + + switch ($v_header['typeflag']) { + case 'L': + { + if (!$this->_readLongHeader($v_header)) { + return null; + } + } + break; + + case 'K': + { + $v_link_header = $v_header; + if (!$this->_readLongHeader($v_link_header)) { + return null; + } + $v_header['link'] = $v_link_header['filename']; + } + break; + } + + // ignore extended / pax headers + if ($v_header['typeflag'] == 'x' || $v_header['typeflag'] == 'g') { + $this->_jumpBlock(ceil(($v_header['size'] / 512))); + continue; + } + + if ((!$v_extract_all) && (is_array($p_file_list))) { + // ----- By default no unzip if the file is not found + $v_extract_file = false; + + for ($i = 0; $i < sizeof($p_file_list); $i++) { + // ----- Look if it is a directory + if (substr($p_file_list[$i], -1) == '/') { + // ----- Look if the directory is in the filename path + if ((strlen($v_header['filename']) > strlen($p_file_list[$i])) + && (substr($v_header['filename'], 0, strlen($p_file_list[$i])) + == $p_file_list[$i]) + ) { + $v_extract_file = true; + break; + } + } // ----- It is a file, so compare the file names + elseif ($p_file_list[$i] == $v_header['filename']) { + $v_extract_file = true; + break; + } + } + } else { + $v_extract_file = true; + } + + // ----- Look if this file need to be extracted + if (($v_extract_file) && (!$v_listing)) { + if (($p_remove_path != '') + && (substr($v_header['filename'] . '/', 0, $p_remove_path_size) + == $p_remove_path) + ) { + $v_header['filename'] = substr( + $v_header['filename'], + $p_remove_path_size + ); + if ($v_header['filename'] == '') { + continue; + } + } + if (($p_path != './') && ($p_path != '/')) { + while (substr($p_path, -1) == '/') { + $p_path = substr($p_path, 0, strlen($p_path) - 1); + } + + if (substr($v_header['filename'], 0, 1) == '/') { + $v_header['filename'] = $p_path . $v_header['filename']; + } else { + $v_header['filename'] = $p_path . '/' . $v_header['filename']; + } + } + if (file_exists($v_header['filename'])) { + if ((@is_dir($v_header['filename'])) + && ($v_header['typeflag'] == '') + ) { + $this->_error( + 'File ' . $v_header['filename'] + . ' already exists as a directory' + ); + return false; + } + if (($this->_isArchive($v_header['filename'])) + && ($v_header['typeflag'] == "5") + ) { + $this->_error( + 'Directory ' . $v_header['filename'] + . ' already exists as a file' + ); + return false; + } + if (!is_writeable($v_header['filename'])) { + $this->_error( + 'File ' . $v_header['filename'] + . ' already exists and is write protected' + ); + return false; + } + if (filemtime($v_header['filename']) > $v_header['mtime']) { + // To be completed : An error or silent no replace ? + } + } // ----- Check the directory availability and create it if necessary + elseif (($v_result + = $this->_dirCheck( + ($v_header['typeflag'] == "5" + ? $v_header['filename'] + : dirname($v_header['filename'])) + )) != 1 + ) { + $this->_error('Unable to create path for ' . $v_header['filename']); + return false; + } + + if ($v_extract_file) { + if ($v_header['typeflag'] == "5") { + if (!@file_exists($v_header['filename'])) { + if (!@mkdir($v_header['filename'], 0775)) { + $this->_error( + 'Unable to create directory {' + . $v_header['filename'] . '}' + ); + return false; + } + } + } elseif ($v_header['typeflag'] == "2") { + if (!$p_symlinks) { + $this->_warning('Symbolic links are not allowed. ' + . 'Unable to extract {' + . $v_header['filename'] . '}' + ); + return false; + } + $absolute_link = FALSE; + $link_depth = 0; + if (strpos($v_header['link'], "/") === 0 || strpos($v_header['link'], ':') !== FALSE) { + $absolute_link = TRUE; + } + else { + $s_filename = preg_replace('@^' . preg_quote($p_path) . '@', "", $v_header['filename']); + $s_linkname = str_replace('\\', '/', $v_header['link']); + foreach (explode("/", $s_filename) as $dir) { + if ($dir === "..") { + $link_depth--; + } elseif ($dir !== "" && $dir !== "." ) { + $link_depth++; + } + } + foreach (explode("/", $s_linkname) as $dir){ + if ($link_depth <= 0) { + break; + } + if ($dir === "..") { + $link_depth--; + } elseif ($dir !== "" && $dir !== ".") { + $link_depth++; + } + } + } + if ($absolute_link || $link_depth <= 0) { + $this->_error( + 'Out-of-path file extraction {' + . $v_header['filename'] . ' --> ' . + $v_header['link'] . '}' + ); + return false; + } + if (@file_exists($v_header['filename'])) { + @unlink($v_header['filename']); + } + if (!@symlink($v_header['link'], $v_header['filename'])) { + $this->_error( + 'Unable to extract symbolic link {' + . $v_header['filename'] . '}' + ); + return false; + } + } else { + if (($v_dest_file = @fopen($v_header['filename'], "wb")) == 0) { + $this->_error( + 'Error while opening {' . $v_header['filename'] + . '} in write binary mode' + ); + return false; + } else { + $n = floor($v_header['size'] / 512); + for ($i = 0; $i < $n; $i++) { + $v_content = $this->_readBlock(); + fwrite($v_dest_file, $v_content, 512); + } + if (($v_header['size'] % 512) != 0) { + $v_content = $this->_readBlock(); + fwrite($v_dest_file, $v_content, ($v_header['size'] % 512)); + } + + @fclose($v_dest_file); + + if ($p_preserve) { + @chown($v_header['filename'], $v_header['uid']); + @chgrp($v_header['filename'], $v_header['gid']); + } + + // ----- Change the file mode, mtime + @touch($v_header['filename'], $v_header['mtime']); + if ($v_header['mode'] & 0111) { + // make file executable, obey umask + $mode = fileperms($v_header['filename']) | (~umask() & 0111); + @chmod($v_header['filename'], $mode); + } + } + + // ----- Check the file size + clearstatcache(); + if (!is_file($v_header['filename'])) { + $this->_error( + 'Extracted file ' . $v_header['filename'] + . 'does not exist. Archive may be corrupted.' + ); + return false; + } + + $filesize = filesize($v_header['filename']); + if ($filesize != $v_header['size']) { + $this->_error( + 'Extracted file ' . $v_header['filename'] + . ' does not have the correct file size \'' + . $filesize + . '\' (' . $v_header['size'] + . ' expected). Archive may be corrupted.' + ); + return false; + } + } + } else { + $this->_jumpBlock(ceil(($v_header['size'] / 512))); + } + } else { + $this->_jumpBlock(ceil(($v_header['size'] / 512))); + } + + /* TBC : Seems to be unused ... + if ($this->_compress) + $v_end_of_file = @gzeof($this->_file); + else + $v_end_of_file = @feof($this->_file); + */ + + if ($v_listing || $v_extract_file || $v_extraction_stopped) { + // ----- Log extracted files + if (($v_file_dir = dirname($v_header['filename'])) + == $v_header['filename'] + ) { + $v_file_dir = ''; + } + if ((substr($v_header['filename'], 0, 1) == '/') && ($v_file_dir == '')) { + $v_file_dir = '/'; + } + + $p_list_detail[$v_nb++] = $v_header; + if (is_array($p_file_list) && (count($p_list_detail) == count($p_file_list))) { + return true; + } + } + } + + return true; + } + + /** + * @return bool + */ + public function _openAppend() + { + if (filesize($this->_tarname) == 0) { + return $this->_openWrite(); + } + + if ($this->_compress) { + $this->_close(); + + if (!@rename($this->_tarname, $this->_tarname . ".tmp")) { + $this->_error( + 'Error while renaming \'' . $this->_tarname + . '\' to temporary file \'' . $this->_tarname + . '.tmp\'' + ); + return false; + } + + if ($this->_compress_type == 'gz') { + $v_temp_tar = @gzopen($this->_tarname . ".tmp", "rb"); + } elseif ($this->_compress_type == 'bz2') { + $v_temp_tar = @bzopen($this->_tarname . ".tmp", "r"); + } elseif ($this->_compress_type == 'lzma2') { + $v_temp_tar = @xzopen($this->_tarname . ".tmp", "r"); + } + + + if ($v_temp_tar == 0) { + $this->_error( + 'Unable to open file \'' . $this->_tarname + . '.tmp\' in binary read mode' + ); + @rename($this->_tarname . ".tmp", $this->_tarname); + return false; + } + + if (!$this->_openWrite()) { + @rename($this->_tarname . ".tmp", $this->_tarname); + return false; + } + + if ($this->_compress_type == 'gz') { + $end_blocks = 0; + + while (!@gzeof($v_temp_tar)) { + $v_buffer = @gzread($v_temp_tar, 512); + if ($v_buffer == ARCHIVE_TAR_END_BLOCK || strlen($v_buffer) == 0) { + $end_blocks++; + // do not copy end blocks, we will re-make them + // after appending + continue; + } elseif ($end_blocks > 0) { + for ($i = 0; $i < $end_blocks; $i++) { + $this->_writeBlock(ARCHIVE_TAR_END_BLOCK); + } + $end_blocks = 0; + } + $v_binary_data = pack("a512", $v_buffer); + $this->_writeBlock($v_binary_data); + } + + @gzclose($v_temp_tar); + } elseif ($this->_compress_type == 'bz2') { + $end_blocks = 0; + + while (strlen($v_buffer = @bzread($v_temp_tar, 512)) > 0) { + if ($v_buffer == ARCHIVE_TAR_END_BLOCK || strlen($v_buffer) == 0) { + $end_blocks++; + // do not copy end blocks, we will re-make them + // after appending + continue; + } elseif ($end_blocks > 0) { + for ($i = 0; $i < $end_blocks; $i++) { + $this->_writeBlock(ARCHIVE_TAR_END_BLOCK); + } + $end_blocks = 0; + } + $v_binary_data = pack("a512", $v_buffer); + $this->_writeBlock($v_binary_data); + } + + @bzclose($v_temp_tar); + } elseif ($this->_compress_type == 'lzma2') { + $end_blocks = 0; + + while (strlen($v_buffer = @xzread($v_temp_tar, 512)) > 0) { + if ($v_buffer == ARCHIVE_TAR_END_BLOCK || strlen($v_buffer) == 0) { + $end_blocks++; + // do not copy end blocks, we will re-make them + // after appending + continue; + } elseif ($end_blocks > 0) { + for ($i = 0; $i < $end_blocks; $i++) { + $this->_writeBlock(ARCHIVE_TAR_END_BLOCK); + } + $end_blocks = 0; + } + $v_binary_data = pack("a512", $v_buffer); + $this->_writeBlock($v_binary_data); + } + + @xzclose($v_temp_tar); + } + + if (!@unlink($this->_tarname . ".tmp")) { + $this->_error( + 'Error while deleting temporary file \'' + . $this->_tarname . '.tmp\'' + ); + } + } else { + // ----- For not compressed tar, just add files before the last + // one or two 512 bytes block + if (!$this->_openReadWrite()) { + return false; + } + + clearstatcache(); + $v_size = filesize($this->_tarname); + + // We might have zero, one or two end blocks. + // The standard is two, but we should try to handle + // other cases. + fseek($this->_file, $v_size - 1024); + if (fread($this->_file, 512) == ARCHIVE_TAR_END_BLOCK) { + fseek($this->_file, $v_size - 1024); + } elseif (fread($this->_file, 512) == ARCHIVE_TAR_END_BLOCK) { + fseek($this->_file, $v_size - 512); + } + } + + return true; + } + + /** + * @param $p_filelist + * @param string $p_add_dir + * @param string $p_remove_dir + * @return bool + */ + public function _append($p_filelist, $p_add_dir = '', $p_remove_dir = '') + { + if (!$this->_openAppend()) { + return false; + } + + if ($this->_addList($p_filelist, $p_add_dir, $p_remove_dir)) { + $this->_writeFooter(); + } + + $this->_close(); + + return true; + } + + /** + * Check if a directory exists and create it (including parent + * dirs) if not. + * + * @param string $p_dir directory to check + * + * @return bool true if the directory exists or was created + */ + public function _dirCheck($p_dir) + { + clearstatcache(); + if ((@is_dir($p_dir)) || ($p_dir == '')) { + return true; + } + + $p_parent_dir = dirname($p_dir); + + if (($p_parent_dir != $p_dir) && + ($p_parent_dir != '') && + (!$this->_dirCheck($p_parent_dir)) + ) { + return false; + } + + if (!@mkdir($p_dir, 0775)) { + $this->_error("Unable to create directory '$p_dir'"); + return false; + } + + return true; + } + + /** + * Compress path by changing for example "/dir/foo/../bar" to "/dir/bar", + * rand emove double slashes. + * + * @param string $p_dir path to reduce + * + * @return string reduced path + */ + private function _pathReduction($p_dir) + { + $v_result = ''; + + // ----- Look for not empty path + if ($p_dir != '') { + // ----- Explode path by directory names + $v_list = explode('/', $p_dir); + + // ----- Study directories from last to first + for ($i = sizeof($v_list) - 1; $i >= 0; $i--) { + // ----- Look for current path + if ($v_list[$i] == ".") { + // ----- Ignore this directory + // Should be the first $i=0, but no check is done + } else { + if ($v_list[$i] == "..") { + // ----- Ignore it and ignore the $i-1 + $i--; + } else { + if (($v_list[$i] == '') + && ($i != (sizeof($v_list) - 1)) + && ($i != 0) + ) { + // ----- Ignore only the double '//' in path, + // but not the first and last / + } else { + $v_result = $v_list[$i] . ($i != (sizeof($v_list) - 1) ? '/' + . $v_result : ''); + } + } + } + } + } + + if (defined('OS_WINDOWS') && OS_WINDOWS) { + $v_result = strtr($v_result, '\\', '/'); + } + + return $v_result; + } + + /** + * @param $p_path + * @param bool $p_remove_disk_letter + * @return string + */ + public function _translateWinPath($p_path, $p_remove_disk_letter = true) + { + if (defined('OS_WINDOWS') && OS_WINDOWS) { + // ----- Look for potential disk letter + if (($p_remove_disk_letter) + && (($v_position = strpos($p_path, ':')) != false) + ) { + $p_path = substr($p_path, $v_position + 1); + } + // ----- Change potential windows directory separator + if ((strpos($p_path, '\\') > 0) || (substr($p_path, 0, 1) == '\\')) { + $p_path = strtr($p_path, '\\', '/'); + } + } + return $p_path; + } +} diff --git a/3rdparty/pear/archive_tar/package.xml b/3rdparty/pear/archive_tar/package.xml new file mode 100644 index 00000000..a997f41d --- /dev/null +++ b/3rdparty/pear/archive_tar/package.xml @@ -0,0 +1,749 @@ + + + Archive_Tar + pear.php.net +

Tar file management class + This class provides handling of tar files in PHP. +It supports creating, listing, extracting and adding to tar files. +Gzip support is available if PHP has the zlib extension built-in or +loaded. Bz2 compression is also supported with the bz2 extension loaded. +Also Lzma2 compressed archives are supported with xz extension. + + Vincent Blavet + vblavet + vincent@phpconcept.net + no + + + Greg Beaver + cellog + greg@chiaraquartet.net + no + + + Michiel Rook + mrook + mrook@php.net + no + + + Drew Webber + mcdruid + drew@mcdruid.co.uk + yes + + + Stig Bakken + ssb + stig@php.net + no + + 2024-03-16 + + 1.5.0 + 1.5.0 + + + stable + stable + + New BSD License + +* PR #42: fix @return true... to @return bool true... on some functions +* PR #46: use 775 default for mkdirs, to avoid world-write + + + + + + + + + + + + + PEAR + pear.php.net + 1.8.0 + 1.10.10 + + + + + 5.2.0 + + + 1.9.0 + + + + + + + + 1.5.0 + 1.5.0 + + + stable + stable + + 2024-03-16 + New BSD License + + * PR #42: fix @return true... to @return bool true... on some functions + * PR #46: use 775 default for mkdirs, to avoid world-write + + + + + 1.4.14 + 1.4.0 + + + stable + stable + + 2021-02-16 + New BSD License + + * Properly fix symbolic link path traversal (CVE-2021-32610) + + + + + 1.4.13 + 1.4.0 + + + stable + stable + + 2021-02-16 + New BSD License + + * Fix Bug #27010: Relative symlinks failing (out-of path file extraction) [mrook] + + + + + 1.4.12 + 1.4.0 + + + stable + stable + + 2021-01-18 + New BSD License + +* Fix Bug #27008: Symlink out-of-path write vulnerability (CVE-2020-36193) [mrook] + + + + + 1.4.11 + 1.4.0 + + + stable + stable + + 2020-11-19 + New BSD License + +* Fix Bug #27002: Filename manipulation vulnerabilities (CVE-2020-28948 / CVE-2020-28949) [mrook] + + + + + 1.4.10 + 1.4.0 + + + stable + stable + + 2020-09-15 + New BSD License + + * Fix block padding when the file buffer length is a multiple of 512 and smaller than Archive_Tar buffer length + * Don't try to copy username/groupname in chroot jail + + + + + 1.4.9 + 1.4.0 + + + stable + stable + + 2019-12-04 + New BSD License + +* Implement Feature #23861: Add option to disallow symlinks [mrook] + + + + + 1.4.8 + 1.4.0 + + + stable + stable + + 2019-10-21 + New BSD License + +* Fix Bug #23852: PHP 7.4 - Archive_Tar->_readHeader throws deprecation [mrook] + + + + + 1.4.7 + 1.4.0 + + + stable + stable + + 2019-04-08 + New BSD License + +* Improved performance by increasing read buffer size + + + + + 1.4.6 + 1.4.0 + + + stable + stable + + 2019-02-01 + New BSD License + +* Improve path traversal detection for forward and backward slashes + + + + + 1.4.5 + 1.4.0 + + + stable + stable + + 2019-01-02 + New BSD License + +* Fix Bug #23788: Relative symlinks are broken [mrook] + + + + + 1.4.4 + 1.4.0 + + + stable + stable + + 2018-12-20 + New BSD License + +* Fix Bug #21058: Long symlinks are not supported [mrook] + * Fix Bug #23782: Prevent phar:// files from being extracted [mrook] + + + + + 1.4.3 + 1.4.0 + + + stable + stable + + 2017-06-11 + New BSD License + +* Fix Bug #21218: Cannot use result of built-in function in write context in PHP + 7.2.0alpha1 [mrook] + + + + + 1.4.2 + 1.4.0 + + + stable + stable + + 2016-02-25 + New BSD License + +* Fix reading of archives with files > 8GB +* Performance optimizations +* Do not try to call require_once on PEAR.php if it has already been loaded by the autoloader + + + + + 1.4.1 + 1.4.0 + + + stable + stable + + 2015-08-05 + New BSD License + +* Update composer.json to use pear-core-minimal 1.10.0alpha2 + + + + + 1.4.0 + 1.4.0 + + + stable + stable + + 2015-07-20 + New BSD License + +* Add support for PHP 7 +* Drop support for PHP 4 +* Add visibility declarations to methods and properties + + + + + 1.3.16 + 1.3.1 + + + stable + stable + + 2015-04-14 + New BSD License + +* Fix Bug #20514: invalid package.xml; not installable with pyrus [mrook] + + + + + 1.3.15 + 1.3.1 + + + stable + stable + + 2015-03-05 + New BSD License + +* Fixes composer.json parse error + + + + + 1.3.14 + 1.3.1 + + + stable + stable + + 2015-02-26 + New BSD License + +* Fix Bug #18505: Possible incorrect handling of file names in TAR [mrook] + + + + + 1.3.13 + 1.3.1 + + + stable + stable + + 2014-09-02 + New BSD + License + +* Fix Bug #20382: gzopen fix [mrook] + + + + + 1.3.12 + 1.3.1 + + + stable + stable + + 2014-08-04 + New BSD + License + +* Fix Bug #19964: Memory leaking in Archive_Tar [mrook] + * Fix Bug #20246: Broken with php 5.5.9 [mrook] + * Fix Bug #20275: "pax_global_header" looks like a regular file + * [mrook] + * Implement Feature #19827: pass filename to _addFile function - downstream + * patch [mrook] + * Implement Feature #20132: Add custom mode/uid/gid to addString() [mrook] + + + + + 1.3.11 + 1.3.1 + + + stable + stable + + 2013-02-09 + New BSD + License + +* Fix Bug #19746: Broken with PHP 5.5 [mrook] + * Implement Feature #11258: Custom date/time in files added on-the-fly + * [mrook] + + + + + 1.3.10 + 1.3.1 + + + stable + stable + + 2012-04-10 + New BSD + License + +* Fix Bug #13361: Unable to add() some files (ex. mp3) [mrook] + * Fix Bug #19330: Class creates incorrect (non-readable) tar.gz file + * [mrook] + + + + + 1.3.9 + 1.3.1 + + + stable + stable + + 2012-02-27 + New BSD License + +* Fix Bug #16759: No error thrown from missing PHP zlib functions [mrook] + * Fix Bug #18877: Incorrect handling of backslashes in filenames on Linux [mrook] + * Fix Bug #19085: Error while packaging [mrook] + * Fix Bug #19289: Invalid tar file generated [mrook] + + + + + 1.3.8 + 1.3.1 + + + stable + stable + + 2011-10-14 + New BSD License + +* Fix Bug #17853: Test failure: dirtraversal.phpt [mrook] + * Fix Bug #18512: dead links are not saved in tar file [mrook] + * Fix Bug #18702: Unpacks incorrectly on long file names using header prefix [mrook] + * Implement Feature #10145: Patch to return a Pear Error Object on failure [mrook] + * Implement Feature #17491: Option to preserve permissions [mrook] + * Implement Feature #17813: Prevent PHP notice when extracting corrupted archive [mrook] + + + + + 1.3.7 + 1.3.1 + + + stable + stable + + 2010-04-26 + New BSD License + +PEAR compatibility update + + + + + 1.3.6 + 1.3.1 + + + stable + stable + + 2010-03-09 + New BSD License + +* Fix Bug #16963: extractList can't extract zipped files from big tar [mrook] + * Implement Feature #4013: Ignoring files and directories on creating an archive. [mrook] + + + + + 1.3.5 + 1.3.1 + + + stable + stable + + 2009-12-31 + New BSD License + +* Fix Bug #16958: Update 'compatible' tag in package.xml [mrook] + + + + + 1.3.4 + 1.3.1 + + + stable + stable + + 2009-12-30 + New BSD License + +* Fix Bug #11871: wrong result of ::listContent() if filename begins or ends with space [mrook] + * Fix Bug #12462: invalid tar magic [mrook] + * Fix Bug #13918: Long filenames may get up to 511 0x00 bytes appended on read [mrook] + * Fix Bug #16202: Bogus modification times [mrook] + * Implement Feature #16212: Die is not exception [mrook] + + + + + 1.3.3 + 1.3.1 + + + stable + stable + + 2009-03-27 + New BSD License + +Change the license to New BSD license + + minor bugfix release + * fix Bug #9921 compression with bzip2 fails [cellog] + * fix Bug #11594 _readLongHeader leaves 0 bytes in filename [jamessas] + * fix Bug #11769 Incorrect symlink handing [fajar99] + + + + + 1.3.2 + 1.3.1 + + + stable + stable + + 2007-01-03 + PHP License + +Correct Bug #4016 +Remove duplicate remove error display with '@' +Correct Bug #3909 : Check existence of OS_WINDOWS constant +Correct Bug #5452 fix for "lone zero block" when untarring packages +Change filemode (from pear-core/Archive/Tar.php v.1.21) +Correct Bug #6486 Can not extract symlinks +Correct Bug #6933 Archive_Tar (Tar file management class) Directory traversal +Correct Bug #8114 Files added on-the-fly not storing date +Correct Bug #9352 Bug on _dirCheck function over nfs path + + + + + 1.3.1 + 1.3.1 + + + stable + stable + + 2005-03-17 + PHP License + +Correct Bug #3855 + + + + + 1.3.0 + 1.3.0 + + + stable + stable + + 2005-03-06 + PHP License + +Bugs correction (2475, 2488, 2135, 2176) + + + + + 1.2 + 1.2 + + + stable + stable + + 2004-05-08 + PHP License + +Add support for other separator than the space char and bug + correction + + + + + 1.1 + 1.1 + + + stable + stable + + 2003-05-28 + PHP License + +* Add support for BZ2 compression +* Add support for add and extract without using temporary files : methods addString() and extractInString() + + + + + 1.0 + 1.0 + + + stable + stable + + 2003-01-24 + PHP License + +Change status to stable + + + + + 0.10-b1 + 0.10-b1 + + + beta + beta + + 2003-01-08 + PHP License + +Add support for long filenames (greater than 99 characters) + + + + + 0.9 + 0.9 + + + stable + stable + + 2002-05-27 + PHP License + +Auto-detect gzip'ed files + + + + + 0.4 + 0.4 + + + stable + stable + + 2002-05-20 + PHP License + +Windows bugfix: use forward slashes inside archives + + + + + 0.2 + 0.2 + + + stable + stable + + 2002-02-18 + PHP License + +From initial commit to stable + + + + + 0.3 + 0.3 + + + stable + stable + + 2002-04-13 + PHP License + +Windows bugfix: used wrong directory separators + + + + diff --git a/3rdparty/pear/console_getopt/Console/Getopt.php b/3rdparty/pear/console_getopt/Console/Getopt.php new file mode 100644 index 00000000..e5793bbb --- /dev/null +++ b/3rdparty/pear/console_getopt/Console/Getopt.php @@ -0,0 +1,365 @@ + + * @license http://opensource.org/licenses/bsd-license.php BSD-2-Clause + * @version CVS: $Id$ + * @link http://pear.php.net/package/Console_Getopt + */ + +require_once 'PEAR.php'; + +/** + * Command-line options parsing class. + * + * @category Console + * @package Console_Getopt + * @author Andrei Zmievski + * @license http://opensource.org/licenses/bsd-license.php BSD-2-Clause + * @link http://pear.php.net/package/Console_Getopt + */ +class Console_Getopt +{ + + /** + * Parses the command-line options. + * + * The first parameter to this function should be the list of command-line + * arguments without the leading reference to the running program. + * + * The second parameter is a string of allowed short options. Each of the + * option letters can be followed by a colon ':' to specify that the option + * requires an argument, or a double colon '::' to specify that the option + * takes an optional argument. + * + * The third argument is an optional array of allowed long options. The + * leading '--' should not be included in the option name. Options that + * require an argument should be followed by '=', and options that take an + * option argument should be followed by '=='. + * + * The return value is an array of two elements: the list of parsed + * options and the list of non-option command-line arguments. Each entry in + * the list of parsed options is a pair of elements - the first one + * specifies the option, and the second one specifies the option argument, + * if there was one. + * + * Long and short options can be mixed. + * + * Most of the semantics of this function are based on GNU getopt_long(). + * + * @param array $args an array of command-line arguments + * @param string $short_options specifies the list of allowed short options + * @param array $long_options specifies the list of allowed long options + * @param boolean $skip_unknown suppresses Console_Getopt: unrecognized option + * + * @return array two-element array containing the list of parsed options and + * the non-option arguments + */ + public static function getopt2($args, $short_options, $long_options = null, $skip_unknown = false) + { + return Console_Getopt::doGetopt(2, $args, $short_options, $long_options, $skip_unknown); + } + + /** + * This function expects $args to start with the script name (POSIX-style). + * Preserved for backwards compatibility. + * + * @param array $args an array of command-line arguments + * @param string $short_options specifies the list of allowed short options + * @param array $long_options specifies the list of allowed long options + * + * @see getopt2() + * @return array two-element array containing the list of parsed options and + * the non-option arguments + */ + public static function getopt($args, $short_options, $long_options = null, $skip_unknown = false) + { + return Console_Getopt::doGetopt(1, $args, $short_options, $long_options, $skip_unknown); + } + + /** + * The actual implementation of the argument parsing code. + * + * @param int $version Version to use + * @param array $args an array of command-line arguments + * @param string $short_options specifies the list of allowed short options + * @param array $long_options specifies the list of allowed long options + * @param boolean $skip_unknown suppresses Console_Getopt: unrecognized option + * + * @return array + */ + public static function doGetopt($version, $args, $short_options, $long_options = null, $skip_unknown = false) + { + // in case you pass directly readPHPArgv() as the first arg + if (PEAR::isError($args)) { + return $args; + } + + if (empty($args)) { + return array(array(), array()); + } + + $non_opts = $opts = array(); + + settype($args, 'array'); + + if ($long_options) { + sort($long_options); + } + + /* + * Preserve backwards compatibility with callers that relied on + * erroneous POSIX fix. + */ + if ($version < 2) { + if (isset($args[0][0]) && $args[0][0] != '-') { + array_shift($args); + } + } + + for ($i = 0; $i < count($args); $i++) { + $arg = $args[$i]; + /* The special element '--' means explicit end of + options. Treat the rest of the arguments as non-options + and end the loop. */ + if ($arg == '--') { + $non_opts = array_merge($non_opts, array_slice($args, $i + 1)); + break; + } + + if ($arg[0] != '-' || (strlen($arg) > 1 && $arg[1] == '-' && !$long_options)) { + $non_opts = array_merge($non_opts, array_slice($args, $i)); + break; + } elseif (strlen($arg) > 1 && $arg[1] == '-') { + $error = Console_Getopt::_parseLongOption(substr($arg, 2), + $long_options, + $opts, + $i, + $args, + $skip_unknown); + if (PEAR::isError($error)) { + return $error; + } + } elseif ($arg == '-') { + // - is stdin + $non_opts = array_merge($non_opts, array_slice($args, $i)); + break; + } else { + $error = Console_Getopt::_parseShortOption(substr($arg, 1), + $short_options, + $opts, + $i, + $args, + $skip_unknown); + if (PEAR::isError($error)) { + return $error; + } + } + } + + return array($opts, $non_opts); + } + + /** + * Parse short option + * + * @param string $arg Argument + * @param string[] $short_options Available short options + * @param string[][] &$opts + * @param int &$argIdx + * @param string[] $args + * @param boolean $skip_unknown suppresses Console_Getopt: unrecognized option + * + * @return void + */ + protected static function _parseShortOption($arg, $short_options, &$opts, &$argIdx, $args, $skip_unknown) + { + for ($i = 0; $i < strlen($arg); $i++) { + $opt = $arg[$i]; + $opt_arg = null; + + /* Try to find the short option in the specifier string. */ + if (($spec = strstr($short_options, $opt)) === false || $arg[$i] == ':') { + if ($skip_unknown === true) { + break; + } + + $msg = "Console_Getopt: unrecognized option -- $opt"; + return PEAR::raiseError($msg); + } + + if (strlen($spec) > 1 && $spec[1] == ':') { + if (strlen($spec) > 2 && $spec[2] == ':') { + if ($i + 1 < strlen($arg)) { + /* Option takes an optional argument. Use the remainder of + the arg string if there is anything left. */ + $opts[] = array($opt, substr($arg, $i + 1)); + break; + } + } else { + /* Option requires an argument. Use the remainder of the arg + string if there is anything left. */ + if ($i + 1 < strlen($arg)) { + $opts[] = array($opt, substr($arg, $i + 1)); + break; + } else if (isset($args[++$argIdx])) { + $opt_arg = $args[$argIdx]; + /* Else use the next argument. */; + if (Console_Getopt::_isShortOpt($opt_arg) + || Console_Getopt::_isLongOpt($opt_arg)) { + $msg = "option requires an argument --$opt"; + return PEAR::raiseError("Console_Getopt: " . $msg); + } + } else { + $msg = "option requires an argument --$opt"; + return PEAR::raiseError("Console_Getopt: " . $msg); + } + } + } + + $opts[] = array($opt, $opt_arg); + } + } + + /** + * Checks if an argument is a short option + * + * @param string $arg Argument to check + * + * @return bool + */ + protected static function _isShortOpt($arg) + { + return strlen($arg) == 2 && $arg[0] == '-' + && preg_match('/[a-zA-Z]/', $arg[1]); + } + + /** + * Checks if an argument is a long option + * + * @param string $arg Argument to check + * + * @return bool + */ + protected static function _isLongOpt($arg) + { + return strlen($arg) > 2 && $arg[0] == '-' && $arg[1] == '-' && + preg_match('/[a-zA-Z]+$/', substr($arg, 2)); + } + + /** + * Parse long option + * + * @param string $arg Argument + * @param string[] $long_options Available long options + * @param string[][] &$opts + * @param int &$argIdx + * @param string[] $args + * + * @return void|PEAR_Error + */ + protected static function _parseLongOption($arg, $long_options, &$opts, &$argIdx, $args, $skip_unknown) + { + @list($opt, $opt_arg) = explode('=', $arg, 2); + + $opt_len = strlen($opt); + + for ($i = 0; $i < count($long_options); $i++) { + $long_opt = $long_options[$i]; + $opt_start = substr($long_opt, 0, $opt_len); + + $long_opt_name = str_replace('=', '', $long_opt); + + /* Option doesn't match. Go on to the next one. */ + if ($long_opt_name != $opt) { + continue; + } + + $opt_rest = substr($long_opt, $opt_len); + + /* Check that the options uniquely matches one of the allowed + options. */ + if ($i + 1 < count($long_options)) { + $next_option_rest = substr($long_options[$i + 1], $opt_len); + } else { + $next_option_rest = ''; + } + + if ($opt_rest != '' && $opt[0] != '=' && + $i + 1 < count($long_options) && + $opt == substr($long_options[$i+1], 0, $opt_len) && + $next_option_rest != '' && + $next_option_rest[0] != '=') { + + $msg = "Console_Getopt: option --$opt is ambiguous"; + return PEAR::raiseError($msg); + } + + if (substr($long_opt, -1) == '=') { + if (substr($long_opt, -2) != '==') { + /* Long option requires an argument. + Take the next argument if one wasn't specified. */; + if (!strlen($opt_arg)) { + if (!isset($args[++$argIdx])) { + $msg = "Console_Getopt: option requires an argument --$opt"; + return PEAR::raiseError($msg); + } + $opt_arg = $args[$argIdx]; + } + + if (Console_Getopt::_isShortOpt($opt_arg) + || Console_Getopt::_isLongOpt($opt_arg)) { + $msg = "Console_Getopt: option requires an argument --$opt"; + return PEAR::raiseError($msg); + } + } + } else if ($opt_arg) { + $msg = "Console_Getopt: option --$opt doesn't allow an argument"; + return PEAR::raiseError($msg); + } + + $opts[] = array('--' . $opt, $opt_arg); + return; + } + + if ($skip_unknown === true) { + return; + } + + return PEAR::raiseError("Console_Getopt: unrecognized option --$opt"); + } + + /** + * Safely read the $argv PHP array across different PHP configurations. + * Will take care on register_globals and register_argc_argv ini directives + * + * @return mixed the $argv PHP array or PEAR error if not registered + */ + public static function readPHPArgv() + { + global $argv; + if (!is_array($argv)) { + if (!@is_array($_SERVER['argv'])) { + if (!@is_array($GLOBALS['HTTP_SERVER_VARS']['argv'])) { + $msg = "Could not read cmd args (register_argc_argv=Off?)"; + return PEAR::raiseError("Console_Getopt: " . $msg); + } + return $GLOBALS['HTTP_SERVER_VARS']['argv']; + } + return $_SERVER['argv']; + } + return $argv; + } + +} diff --git a/3rdparty/pear/console_getopt/LICENSE b/3rdparty/pear/console_getopt/LICENSE new file mode 100644 index 00000000..452b0883 --- /dev/null +++ b/3rdparty/pear/console_getopt/LICENSE @@ -0,0 +1,25 @@ +Copyright (c) 2001-2015, The PEAR developers +All rights reserved. + +Redistribution and use in source and binary forms, with or without +modification, are permitted provided that the following conditions are +met: + +1. Redistributions of source code must retain the above copyright +notice, this list of conditions and the following disclaimer. + +2. Redistributions in binary form must reproduce the above copyright +notice, this list of conditions and the following disclaimer in the +documentation and/or other materials provided with the distribution. + +THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS +"AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT +LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR +A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT +HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, +SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT +LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, +DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY +THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT +(INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE +OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. diff --git a/3rdparty/pear/console_getopt/package.xml b/3rdparty/pear/console_getopt/package.xml new file mode 100644 index 00000000..d3fd7840 --- /dev/null +++ b/3rdparty/pear/console_getopt/package.xml @@ -0,0 +1,302 @@ + + + Console_Getopt + pear.php.net + Command-line option parser + This is a PHP implementation of "getopt" supporting both +short and long options. + + Andrei Zmievski + andrei + andrei@php.net + no + + + Stig Bakken + ssb + stig@php.net + no + + + Greg Beaver + cellog + cellog@php.net + no + + + 2019-11-20 + + 1.4.3 + 1.4.0 + + + stable + stable + + BSD-2-Clause + + +* PR #4: Fix PHP 7.4 deprecation: array/string curly braces access +* PR #5: fix phplint warnings + + + + + + + + + + + + + + + + + + PEAR + pear.php.net + 1.4.0 + 1.999.999 + + + + + + 5.4.0 + + + 1.8.0 + + + + + + + + + + 2019-11-20 + + 1.4.3 + 1.4.0 + + + stable + stable + + BSD-2-Clause + + * PR #4: Fix PHP 7.4 deprecation: array/string curly braces access + * PR #5: fix phplint warnings + + + + + 2019-02-06 + + 1.4.2 + 1.4.0 + + + stable + stable + + BSD-2-Clause + + * Remove use of each(), which is removed in PHP 8 + + + + + 2015-07-20 + + 1.4.1 + 1.4.0 + + + stable + stable + + BSD-2-Clause + + * Fix unit test on PHP 7 [cweiske] + + + + + 2015-02-22 + + 1.4.0 + 1.4.0 + + + stable + stable + + BSD-2-Clause + + * Change license to BSD-2-Clause + * Set minimum PHP version to 5.4.0 + * Mark static methods with "static" keyword + + + + + 2011-03-07 + + 1.3.1 + 1.3.0 + + + stable + stable + + PHP License + + * Change the minimum PEAR installer dep to be lower + + + + + 2010-12-11 + + + 1.3.0 + 1.3.0 + + + stable + stable + + PHP License + + * Implement Request #13140: [PATCH] to skip unknown parameters. [patch by rquadling, improved on by dufuz] + + + + + 2007-06-12 + + 1.2.3 + 1.2.1 + + + stable + stable + + PHP License + +* fix Bug #11068: No way to read plain "-" option [cardoe] + + + + + 1.2.2 + 1.2.1 + + + stable + stable + + 2007-02-17 + PHP License + +* fix Bug #4475: An ambiguous error occurred when specifying similar longoption name. +* fix Bug #10055: Not failing properly on short options missing required values + + + + + 1.2.1 + 1.2.1 + + + stable + stable + + 2006-12-08 + PHP License + +Fixed bugs #4448 (Long parameter values truncated with longoption parameter) and #7444 (Trailing spaces after php closing tag) + + + + + 1.2 + 1.2 + + + stable + stable + + 2003-12-11 + PHP License + +Fix to preserve BC with 1.0 and allow correct behaviour for new users + + + + + 1.0 + 1.0 + + + stable + stable + + 2002-09-13 + PHP License + +Stable release + + + + + 0.11 + 0.11 + + + beta + beta + + 2002-05-26 + PHP License + +POSIX getopt compatibility fix: treat first element of args + array as command name + + + + + 0.10 + 0.10 + + + beta + beta + + 2002-05-12 + PHP License + +Packaging fix + + + + + 0.9 + 0.9 + + + beta + beta + + 2002-05-12 + PHP License + +Initial release + + + + diff --git a/3rdparty/pear/pear-core-minimal/src/OS/Guess.php b/3rdparty/pear/pear-core-minimal/src/OS/Guess.php new file mode 100644 index 00000000..0e37a095 --- /dev/null +++ b/3rdparty/pear/pear-core-minimal/src/OS/Guess.php @@ -0,0 +1,395 @@ + + * @author Gregory Beaver + * @copyright 1997-2009 The Authors + * @license http://opensource.org/licenses/bsd-license.php New BSD License + * @link http://pear.php.net/package/PEAR + * @since File available since PEAR 0.1 + */ + +// {{{ uname examples + +// php_uname() without args returns the same as 'uname -a', or a PHP-custom +// string for Windows. +// PHP versions prior to 4.3 return the uname of the host where PHP was built, +// as of 4.3 it returns the uname of the host running the PHP code. +// +// PC RedHat Linux 7.1: +// Linux host.example.com 2.4.2-2 #1 Sun Apr 8 20:41:30 EDT 2001 i686 unknown +// +// PC Debian Potato: +// Linux host 2.4.17 #2 SMP Tue Feb 12 15:10:04 CET 2002 i686 unknown +// +// PC FreeBSD 3.3: +// FreeBSD host.example.com 3.3-STABLE FreeBSD 3.3-STABLE #0: Mon Feb 21 00:42:31 CET 2000 root@example.com:/usr/src/sys/compile/CONFIG i386 +// +// PC FreeBSD 4.3: +// FreeBSD host.example.com 4.3-RELEASE FreeBSD 4.3-RELEASE #1: Mon Jun 25 11:19:43 EDT 2001 root@example.com:/usr/src/sys/compile/CONFIG i386 +// +// PC FreeBSD 4.5: +// FreeBSD host.example.com 4.5-STABLE FreeBSD 4.5-STABLE #0: Wed Feb 6 23:59:23 CET 2002 root@example.com:/usr/src/sys/compile/CONFIG i386 +// +// PC FreeBSD 4.5 w/uname from GNU shellutils: +// FreeBSD host.example.com 4.5-STABLE FreeBSD 4.5-STABLE #0: Wed Feb i386 unknown +// +// HP 9000/712 HP-UX 10: +// HP-UX iq B.10.10 A 9000/712 2008429113 two-user license +// +// HP 9000/712 HP-UX 10 w/uname from GNU shellutils: +// HP-UX host B.10.10 A 9000/712 unknown +// +// IBM RS6000/550 AIX 4.3: +// AIX host 3 4 000003531C00 +// +// AIX 4.3 w/uname from GNU shellutils: +// AIX host 3 4 000003531C00 unknown +// +// SGI Onyx IRIX 6.5 w/uname from GNU shellutils: +// IRIX64 host 6.5 01091820 IP19 mips +// +// SGI Onyx IRIX 6.5: +// IRIX64 host 6.5 01091820 IP19 +// +// SparcStation 20 Solaris 8 w/uname from GNU shellutils: +// SunOS host.example.com 5.8 Generic_108528-12 sun4m sparc +// +// SparcStation 20 Solaris 8: +// SunOS host.example.com 5.8 Generic_108528-12 sun4m sparc SUNW,SPARCstation-20 +// +// Mac OS X (Darwin) +// Darwin home-eden.local 7.5.0 Darwin Kernel Version 7.5.0: Thu Aug 5 19:26:16 PDT 2004; root:xnu/xnu-517.7.21.obj~3/RELEASE_PPC Power Macintosh +// +// Mac OS X early versions +// + +// }}} + +/* TODO: + * - define endianness, to allow matchSignature("bigend") etc. + */ + +/** + * Retrieves information about the current operating system + * + * This class uses php_uname() to grok information about the current OS + * + * @category pear + * @package PEAR + * @author Stig Bakken + * @author Gregory Beaver + * @copyright 1997-2020 The Authors + * @license http://opensource.org/licenses/bsd-license.php New BSD License + * @version Release: @package_version@ + * @link http://pear.php.net/package/PEAR + * @since Class available since Release 0.1 + */ +class OS_Guess +{ + var $sysname; + var $nodename; + var $cpu; + var $release; + var $extra; + + function __construct($uname = null) + { + list($this->sysname, + $this->release, + $this->cpu, + $this->extra, + $this->nodename) = $this->parseSignature($uname); + } + + function parseSignature($uname = null) + { + static $sysmap = array( + 'HP-UX' => 'hpux', + 'IRIX64' => 'irix', + ); + static $cpumap = array( + 'i586' => 'i386', + 'i686' => 'i386', + 'ppc' => 'powerpc', + ); + if ($uname === null) { + $uname = php_uname(); + } + $parts = preg_split('/\s+/', trim($uname)); + $n = count($parts); + + $release = $machine = $cpu = ''; + $sysname = $parts[0]; + $nodename = $parts[1]; + $cpu = $parts[$n-1]; + $extra = ''; + if ($cpu == 'unknown') { + $cpu = $parts[$n - 2]; + } + + switch ($sysname) { + case 'AIX' : + $release = "$parts[3].$parts[2]"; + break; + case 'Windows' : + $release = $parts[1]; + if ($release == '95/98') { + $release = '9x'; + } + $cpu = 'i386'; + break; + case 'Linux' : + $extra = $this->_detectGlibcVersion(); + // use only the first two digits from the kernel version + $release = preg_replace('/^([0-9]+\.[0-9]+).*/', '\1', $parts[2]); + break; + case 'Mac' : + $sysname = 'darwin'; + $nodename = $parts[2]; + $release = $parts[3]; + $cpu = $this->_determineIfPowerpc($cpu, $parts); + break; + case 'Darwin' : + $cpu = $this->_determineIfPowerpc($cpu, $parts); + $release = preg_replace('/^([0-9]+\.[0-9]+).*/', '\1', $parts[2]); + break; + default: + $release = preg_replace('/-.*/', '', $parts[2]); + break; + } + + if (isset($sysmap[$sysname])) { + $sysname = $sysmap[$sysname]; + } else { + $sysname = strtolower($sysname); + } + if (isset($cpumap[$cpu])) { + $cpu = $cpumap[$cpu]; + } + return array($sysname, $release, $cpu, $extra, $nodename); + } + + function _determineIfPowerpc($cpu, $parts) + { + $n = count($parts); + if ($cpu == 'Macintosh' && $parts[$n - 2] == 'Power') { + $cpu = 'powerpc'; + } + return $cpu; + } + + function _detectGlibcVersion() + { + static $glibc = false; + if ($glibc !== false) { + return $glibc; // no need to run this multiple times + } + $major = $minor = 0; + include_once "System.php"; + + // Let's try reading possible libc.so.6 symlinks + $libcs = array( + '/lib64/libc.so.6', + '/lib/libc.so.6', + '/lib/i386-linux-gnu/libc.so.6' + ); + $versions = array(); + foreach ($libcs as $file) { + $versions = $this->_readGlibCVersionFromSymlink($file); + if ($versions != []) { + list($major, $minor) = $versions; + break; + } + } + + // Use glibc's header file to + // get major and minor version number: + if (!($major && $minor)) { + $versions = $this->_readGlibCVersionFromFeaturesHeaderFile(); + } + if (is_array($versions) && $versions != []) { + list($major, $minor) = $versions; + } + + if (!($major && $minor)) { + return $glibc = ''; + } + + return $glibc = "glibc{$major}.{$minor}"; + } + + function _readGlibCVersionFromSymlink($file) + { + $versions = array(); + if (@is_link($file) + && (preg_match('/^libc-(.*)\.so$/', basename(readlink($file)), $matches)) + ) { + $versions = explode('.', $matches[1]); + } + return $versions; + } + + + function _readGlibCVersionFromFeaturesHeaderFile() + { + $features_header_file = '/usr/include/features.h'; + if (!(@file_exists($features_header_file) + && @is_readable($features_header_file)) + ) { + return array(); + } + if (!@file_exists('/usr/bin/cpp') || !@is_executable('/usr/bin/cpp')) { + return $this->_parseFeaturesHeaderFile($features_header_file); + } // no cpp + + return $this->_fromGlibCTest(); + } + + function _parseFeaturesHeaderFile($features_header_file) + { + $features_file = fopen($features_header_file, 'rb'); + while (!feof($features_file)) { + $line = fgets($features_file, 8192); + if (!$this->_IsADefinition($line)) { + continue; + } + if (strpos($line, '__GLIBC__')) { + // major version number #define __GLIBC__ version + $line = preg_split('/\s+/', $line); + $glibc_major = trim($line[2]); + if (isset($glibc_minor)) { + break; + } + continue; + } + + if (strpos($line, '__GLIBC_MINOR__')) { + // got the minor version number + // #define __GLIBC_MINOR__ version + $line = preg_split('/\s+/', $line); + $glibc_minor = trim($line[2]); + if (isset($glibc_major)) { + break; + } + } + } + fclose($features_file); + if (!isset($glibc_major) || !isset($glibc_minor)) { + return array(); + } + return array(trim($glibc_major), trim($glibc_minor)); + } + + function _IsADefinition($line) + { + if ($line === false) { + return false; + } + return strpos(trim($line), '#define') !== false; + } + + function _fromGlibCTest() + { + $major = null; + $minor = null; + + $tmpfile = System::mktemp("glibctest"); + $fp = fopen($tmpfile, "w"); + fwrite($fp, "#include \n__GLIBC__ __GLIBC_MINOR__\n"); + fclose($fp); + $cpp = popen("/usr/bin/cpp $tmpfile", "r"); + while ($line = fgets($cpp, 1024)) { + if ($line[0] == '#' || trim($line) == '') { + continue; + } + + if (list($major, $minor) = explode(' ', trim($line))) { + break; + } + } + pclose($cpp); + unlink($tmpfile); + if ($major !== null && $minor !== null) { + return [$major, $minor]; + } + } + + function getSignature() + { + if (empty($this->extra)) { + return "{$this->sysname}-{$this->release}-{$this->cpu}"; + } + return "{$this->sysname}-{$this->release}-{$this->cpu}-{$this->extra}"; + } + + function getSysname() + { + return $this->sysname; + } + + function getNodename() + { + return $this->nodename; + } + + function getCpu() + { + return $this->cpu; + } + + function getRelease() + { + return $this->release; + } + + function getExtra() + { + return $this->extra; + } + + function matchSignature($match) + { + $fragments = is_array($match) ? $match : explode('-', $match); + $n = count($fragments); + $matches = 0; + if ($n > 0) { + $matches += $this->_matchFragment($fragments[0], $this->sysname); + } + if ($n > 1) { + $matches += $this->_matchFragment($fragments[1], $this->release); + } + if ($n > 2) { + $matches += $this->_matchFragment($fragments[2], $this->cpu); + } + if ($n > 3) { + $matches += $this->_matchFragment($fragments[3], $this->extra); + } + return ($matches == $n); + } + + function _matchFragment($fragment, $value) + { + if (strcspn($fragment, '*?') < strlen($fragment)) { + $expression = str_replace( + array('*', '?', '/'), + array('.*', '.', '\\/'), + $fragment + ); + $reg = '/^' . $expression . '\\z/'; + return preg_match($reg, $value); + } + return ($fragment == '*' || !strcasecmp($fragment, $value)); + } +} +/* + * Local Variables: + * indent-tabs-mode: nil + * c-basic-offset: 4 + * End: + */ diff --git a/3rdparty/pear/pear-core-minimal/src/PEAR.php b/3rdparty/pear/pear-core-minimal/src/PEAR.php new file mode 100644 index 00000000..43c65dbd --- /dev/null +++ b/3rdparty/pear/pear-core-minimal/src/PEAR.php @@ -0,0 +1,1138 @@ + + * @author Stig Bakken + * @author Tomas V.V.Cox + * @author Greg Beaver + * @copyright 1997-2010 The Authors + * @license http://opensource.org/licenses/bsd-license.php New BSD License + * @link http://pear.php.net/package/PEAR + * @since File available since Release 0.1 + */ + +/**#@+ + * ERROR constants + */ +define('PEAR_ERROR_RETURN', 1); +define('PEAR_ERROR_PRINT', 2); +define('PEAR_ERROR_TRIGGER', 4); +define('PEAR_ERROR_DIE', 8); +define('PEAR_ERROR_CALLBACK', 16); +/** + * WARNING: obsolete + * @deprecated + */ +define('PEAR_ERROR_EXCEPTION', 32); +/**#@-*/ + +if (substr(PHP_OS, 0, 3) == 'WIN') { + define('OS_WINDOWS', true); + define('OS_UNIX', false); + define('PEAR_OS', 'Windows'); +} else { + define('OS_WINDOWS', false); + define('OS_UNIX', true); + define('PEAR_OS', 'Unix'); // blatant assumption +} + +$GLOBALS['_PEAR_default_error_mode'] = PEAR_ERROR_RETURN; +$GLOBALS['_PEAR_default_error_options'] = E_USER_NOTICE; +$GLOBALS['_PEAR_destructor_object_list'] = array(); +$GLOBALS['_PEAR_shutdown_funcs'] = array(); +$GLOBALS['_PEAR_error_handler_stack'] = array(); + +if(function_exists('ini_set')) { + @ini_set('track_errors', true); +} + +/** + * Base class for other PEAR classes. Provides rudimentary + * emulation of destructors. + * + * If you want a destructor in your class, inherit PEAR and make a + * destructor method called _yourclassname (same name as the + * constructor, but with a "_" prefix). Also, in your constructor you + * have to call the PEAR constructor: $this->PEAR();. + * The destructor method will be called without parameters. Note that + * at in some SAPI implementations (such as Apache), any output during + * the request shutdown (in which destructors are called) seems to be + * discarded. If you need to get any debug information from your + * destructor, use error_log(), syslog() or something similar. + * + * IMPORTANT! To use the emulated destructors you need to create the + * objects by reference: $obj =& new PEAR_child; + * + * @category pear + * @package PEAR + * @author Stig Bakken + * @author Tomas V.V. Cox + * @author Greg Beaver + * @copyright 1997-2006 The PHP Group + * @license http://opensource.org/licenses/bsd-license.php New BSD License + * @version Release: @package_version@ + * @link http://pear.php.net/package/PEAR + * @see PEAR_Error + * @since Class available since PHP 4.0.2 + * @link http://pear.php.net/manual/en/core.pear.php#core.pear.pear + */ +class PEAR +{ + /** + * Whether to enable internal debug messages. + * + * @var bool + * @access private + */ + var $_debug = false; + + /** + * Default error mode for this object. + * + * @var int + * @access private + */ + var $_default_error_mode = null; + + /** + * Default error options used for this object when error mode + * is PEAR_ERROR_TRIGGER. + * + * @var int + * @access private + */ + var $_default_error_options = null; + + /** + * Default error handler (callback) for this object, if error mode is + * PEAR_ERROR_CALLBACK. + * + * @var string + * @access private + */ + var $_default_error_handler = ''; + + /** + * Which class to use for error objects. + * + * @var string + * @access private + */ + var $_error_class = 'PEAR_Error'; + + /** + * An array of expected errors. + * + * @var array + * @access private + */ + var $_expected_errors = array(); + + /** + * List of methods that can be called both statically and non-statically. + * @var array + */ + protected static $bivalentMethods = array( + 'setErrorHandling' => true, + 'raiseError' => true, + 'throwError' => true, + 'pushErrorHandling' => true, + 'popErrorHandling' => true, + ); + + /** + * Constructor. Registers this object in + * $_PEAR_destructor_object_list for destructor emulation if a + * destructor object exists. + * + * @param string $error_class (optional) which class to use for + * error objects, defaults to PEAR_Error. + * @access public + * @return void + */ + function __construct($error_class = null) + { + $classname = strtolower(get_class($this)); + if ($this->_debug) { + print "PEAR constructor called, class=$classname\n"; + } + + if ($error_class !== null) { + $this->_error_class = $error_class; + } + + while ($classname && strcasecmp($classname, "pear")) { + $destructor = "_$classname"; + if (method_exists($this, $destructor)) { + global $_PEAR_destructor_object_list; + $_PEAR_destructor_object_list[] = $this; + if (!isset($GLOBALS['_PEAR_SHUTDOWN_REGISTERED'])) { + register_shutdown_function("_PEAR_call_destructors"); + $GLOBALS['_PEAR_SHUTDOWN_REGISTERED'] = true; + } + break; + } else { + $classname = get_parent_class($classname); + } + } + } + + /** + * Only here for backwards compatibility. + * E.g. Archive_Tar calls $this->PEAR() in its constructor. + * + * @param string $error_class Which class to use for error objects, + * defaults to PEAR_Error. + */ + public function PEAR($error_class = null) + { + self::__construct($error_class); + } + + /** + * Destructor (the emulated type of...). Does nothing right now, + * but is included for forward compatibility, so subclass + * destructors should always call it. + * + * See the note in the class desciption about output from + * destructors. + * + * @access public + * @return void + */ + function _PEAR() { + if ($this->_debug) { + printf("PEAR destructor called, class=%s\n", strtolower(get_class($this))); + } + } + + public function __call($method, $arguments) + { + if (!isset(self::$bivalentMethods[$method])) { + trigger_error( + 'Call to undefined method PEAR::' . $method . '()', E_USER_ERROR + ); + } + return call_user_func_array( + array(__CLASS__, '_' . $method), + array_merge(array($this), $arguments) + ); + } + + public static function __callStatic($method, $arguments) + { + if (!isset(self::$bivalentMethods[$method])) { + trigger_error( + 'Call to undefined method PEAR::' . $method . '()', E_USER_ERROR + ); + } + return call_user_func_array( + array(__CLASS__, '_' . $method), + array_merge(array(null), $arguments) + ); + } + + /** + * If you have a class that's mostly/entirely static, and you need static + * properties, you can use this method to simulate them. Eg. in your method(s) + * do this: $myVar = &PEAR::getStaticProperty('myclass', 'myVar'); + * You MUST use a reference, or they will not persist! + * + * @param string $class The calling classname, to prevent clashes + * @param string $var The variable to retrieve. + * @return mixed A reference to the variable. If not set it will be + * auto initialised to NULL. + */ + public static function &getStaticProperty($class, $var) + { + static $properties; + if (!isset($properties[$class])) { + $properties[$class] = array(); + } + + if (!array_key_exists($var, $properties[$class])) { + $properties[$class][$var] = null; + } + + return $properties[$class][$var]; + } + + /** + * Use this function to register a shutdown method for static + * classes. + * + * @param mixed $func The function name (or array of class/method) to call + * @param mixed $args The arguments to pass to the function + * + * @return void + */ + public static function registerShutdownFunc($func, $args = array()) + { + // if we are called statically, there is a potential + // that no shutdown func is registered. Bug #6445 + if (!isset($GLOBALS['_PEAR_SHUTDOWN_REGISTERED'])) { + register_shutdown_function("_PEAR_call_destructors"); + $GLOBALS['_PEAR_SHUTDOWN_REGISTERED'] = true; + } + $GLOBALS['_PEAR_shutdown_funcs'][] = array($func, $args); + } + + /** + * Tell whether a value is a PEAR error. + * + * @param mixed $data the value to test + * @param int $code if $data is an error object, return true + * only if $code is a string and + * $obj->getMessage() == $code or + * $code is an integer and $obj->getCode() == $code + * + * @return bool true if parameter is an error + */ + public static function isError($data, $code = null) + { + if (!is_a($data, 'PEAR_Error')) { + return false; + } + + if (is_null($code)) { + return true; + } elseif (is_string($code)) { + return $data->getMessage() == $code; + } + + return $data->getCode() == $code; + } + + /** + * Sets how errors generated by this object should be handled. + * Can be invoked both in objects and statically. If called + * statically, setErrorHandling sets the default behaviour for all + * PEAR objects. If called in an object, setErrorHandling sets + * the default behaviour for that object. + * + * @param object $object + * Object the method was called on (non-static mode) + * + * @param int $mode + * One of PEAR_ERROR_RETURN, PEAR_ERROR_PRINT, + * PEAR_ERROR_TRIGGER, PEAR_ERROR_DIE, + * PEAR_ERROR_CALLBACK or PEAR_ERROR_EXCEPTION. + * + * @param mixed $options + * When $mode is PEAR_ERROR_TRIGGER, this is the error level (one + * of E_USER_NOTICE, E_USER_WARNING or E_USER_ERROR). + * + * When $mode is PEAR_ERROR_CALLBACK, this parameter is expected + * to be the callback function or method. A callback + * function is a string with the name of the function, a + * callback method is an array of two elements: the element + * at index 0 is the object, and the element at index 1 is + * the name of the method to call in the object. + * + * When $mode is PEAR_ERROR_PRINT or PEAR_ERROR_DIE, this is + * a printf format string used when printing the error + * message. + * + * @access public + * @return void + * @see PEAR_ERROR_RETURN + * @see PEAR_ERROR_PRINT + * @see PEAR_ERROR_TRIGGER + * @see PEAR_ERROR_DIE + * @see PEAR_ERROR_CALLBACK + * @see PEAR_ERROR_EXCEPTION + * + * @since PHP 4.0.5 + */ + protected static function _setErrorHandling( + $object, $mode = null, $options = null + ) { + if ($object !== null) { + $setmode = &$object->_default_error_mode; + $setoptions = &$object->_default_error_options; + } else { + $setmode = &$GLOBALS['_PEAR_default_error_mode']; + $setoptions = &$GLOBALS['_PEAR_default_error_options']; + } + + switch ($mode) { + case PEAR_ERROR_EXCEPTION: + case PEAR_ERROR_RETURN: + case PEAR_ERROR_PRINT: + case PEAR_ERROR_TRIGGER: + case PEAR_ERROR_DIE: + case null: + $setmode = $mode; + $setoptions = $options; + break; + + case PEAR_ERROR_CALLBACK: + $setmode = $mode; + // class/object method callback + if (is_callable($options)) { + $setoptions = $options; + } else { + trigger_error("invalid error callback", E_USER_WARNING); + } + break; + + default: + trigger_error("invalid error mode", E_USER_WARNING); + break; + } + } + + /** + * This method is used to tell which errors you expect to get. + * Expected errors are always returned with error mode + * PEAR_ERROR_RETURN. Expected error codes are stored in a stack, + * and this method pushes a new element onto it. The list of + * expected errors are in effect until they are popped off the + * stack with the popExpect() method. + * + * Note that this method can not be called statically + * + * @param mixed $code a single error code or an array of error codes to expect + * + * @return int the new depth of the "expected errors" stack + * @access public + */ + function expectError($code = '*') + { + if (is_array($code)) { + array_push($this->_expected_errors, $code); + } else { + array_push($this->_expected_errors, array($code)); + } + return count($this->_expected_errors); + } + + /** + * This method pops one element off the expected error codes + * stack. + * + * @return array the list of error codes that were popped + */ + function popExpect() + { + return array_pop($this->_expected_errors); + } + + /** + * This method checks unsets an error code if available + * + * @param mixed error code + * @return bool true if the error code was unset, false otherwise + * @access private + * @since PHP 4.3.0 + */ + function _checkDelExpect($error_code) + { + $deleted = false; + foreach ($this->_expected_errors as $key => $error_array) { + if (in_array($error_code, $error_array)) { + unset($this->_expected_errors[$key][array_search($error_code, $error_array)]); + $deleted = true; + } + + // clean up empty arrays + if (0 == count($this->_expected_errors[$key])) { + unset($this->_expected_errors[$key]); + } + } + + return $deleted; + } + + /** + * This method deletes all occurrences of the specified element from + * the expected error codes stack. + * + * @param mixed $error_code error code that should be deleted + * @return mixed list of error codes that were deleted or error + * @access public + * @since PHP 4.3.0 + */ + function delExpect($error_code) + { + $deleted = false; + if ((is_array($error_code) && (0 != count($error_code)))) { + // $error_code is a non-empty array here; we walk through it trying + // to unset all values + foreach ($error_code as $key => $error) { + $deleted = $this->_checkDelExpect($error) ? true : false; + } + + return $deleted ? true : PEAR::raiseError("The expected error you submitted does not exist"); // IMPROVE ME + } elseif (!empty($error_code)) { + // $error_code comes alone, trying to unset it + if ($this->_checkDelExpect($error_code)) { + return true; + } + + return PEAR::raiseError("The expected error you submitted does not exist"); // IMPROVE ME + } + + // $error_code is empty + return PEAR::raiseError("The expected error you submitted is empty"); // IMPROVE ME + } + + /** + * This method is a wrapper that returns an instance of the + * configured error class with this object's default error + * handling applied. If the $mode and $options parameters are not + * specified, the object's defaults are used. + * + * @param mixed $message a text error message or a PEAR error object + * + * @param int $code a numeric error code (it is up to your class + * to define these if you want to use codes) + * + * @param int $mode One of PEAR_ERROR_RETURN, PEAR_ERROR_PRINT, + * PEAR_ERROR_TRIGGER, PEAR_ERROR_DIE, + * PEAR_ERROR_CALLBACK, PEAR_ERROR_EXCEPTION. + * + * @param mixed $options If $mode is PEAR_ERROR_TRIGGER, this parameter + * specifies the PHP-internal error level (one of + * E_USER_NOTICE, E_USER_WARNING or E_USER_ERROR). + * If $mode is PEAR_ERROR_CALLBACK, this + * parameter specifies the callback function or + * method. In other error modes this parameter + * is ignored. + * + * @param string $userinfo If you need to pass along for example debug + * information, this parameter is meant for that. + * + * @param string $error_class The returned error object will be + * instantiated from this class, if specified. + * + * @param bool $skipmsg If true, raiseError will only pass error codes, + * the error message parameter will be dropped. + * + * @return object a PEAR error object + * @see PEAR::setErrorHandling + * @since PHP 4.0.5 + */ + protected static function _raiseError($object, + $message = null, + $code = null, + $mode = null, + $options = null, + $userinfo = null, + $error_class = null, + $skipmsg = false) + { + // The error is yet a PEAR error object + if (is_object($message)) { + $code = $message->getCode(); + $userinfo = $message->getUserInfo(); + $error_class = $message->getType(); + $message->error_message_prefix = ''; + $message = $message->getMessage(); + } + + if ( + $object !== null && + isset($object->_expected_errors) && + count($object->_expected_errors) > 0 && + count($exp = end($object->_expected_errors)) + ) { + if ($exp[0] === "*" || + (is_int(reset($exp)) && in_array($code, $exp)) || + (is_string(reset($exp)) && in_array($message, $exp)) + ) { + $mode = PEAR_ERROR_RETURN; + } + } + + // No mode given, try global ones + if ($mode === null) { + // Class error handler + if ($object !== null && isset($object->_default_error_mode)) { + $mode = $object->_default_error_mode; + $options = $object->_default_error_options; + // Global error handler + } elseif (isset($GLOBALS['_PEAR_default_error_mode'])) { + $mode = $GLOBALS['_PEAR_default_error_mode']; + $options = $GLOBALS['_PEAR_default_error_options']; + } + } + + if ($error_class !== null) { + $ec = $error_class; + } elseif ($object !== null && isset($object->_error_class)) { + $ec = $object->_error_class; + } else { + $ec = 'PEAR_Error'; + } + + if ($skipmsg) { + $a = new $ec($code, $mode, $options, $userinfo); + } else { + $a = new $ec($message, $code, $mode, $options, $userinfo); + } + + return $a; + } + + /** + * Simpler form of raiseError with fewer options. In most cases + * message, code and userinfo are enough. + * + * @param mixed $message a text error message or a PEAR error object + * + * @param int $code a numeric error code (it is up to your class + * to define these if you want to use codes) + * + * @param string $userinfo If you need to pass along for example debug + * information, this parameter is meant for that. + * + * @return object a PEAR error object + * @see PEAR::raiseError + */ + protected static function _throwError($object, $message = null, $code = null, $userinfo = null) + { + if ($object !== null) { + $a = $object->raiseError($message, $code, null, null, $userinfo); + return $a; + } + + $a = PEAR::raiseError($message, $code, null, null, $userinfo); + return $a; + } + + public static function staticPushErrorHandling($mode, $options = null) + { + $stack = &$GLOBALS['_PEAR_error_handler_stack']; + $def_mode = &$GLOBALS['_PEAR_default_error_mode']; + $def_options = &$GLOBALS['_PEAR_default_error_options']; + $stack[] = array($def_mode, $def_options); + switch ($mode) { + case PEAR_ERROR_EXCEPTION: + case PEAR_ERROR_RETURN: + case PEAR_ERROR_PRINT: + case PEAR_ERROR_TRIGGER: + case PEAR_ERROR_DIE: + case null: + $def_mode = $mode; + $def_options = $options; + break; + + case PEAR_ERROR_CALLBACK: + $def_mode = $mode; + // class/object method callback + if (is_callable($options)) { + $def_options = $options; + } else { + trigger_error("invalid error callback", E_USER_WARNING); + } + break; + + default: + trigger_error("invalid error mode", E_USER_WARNING); + break; + } + $stack[] = array($mode, $options); + return true; + } + + public static function staticPopErrorHandling() + { + $stack = &$GLOBALS['_PEAR_error_handler_stack']; + $setmode = &$GLOBALS['_PEAR_default_error_mode']; + $setoptions = &$GLOBALS['_PEAR_default_error_options']; + array_pop($stack); + list($mode, $options) = $stack[sizeof($stack) - 1]; + array_pop($stack); + switch ($mode) { + case PEAR_ERROR_EXCEPTION: + case PEAR_ERROR_RETURN: + case PEAR_ERROR_PRINT: + case PEAR_ERROR_TRIGGER: + case PEAR_ERROR_DIE: + case null: + $setmode = $mode; + $setoptions = $options; + break; + + case PEAR_ERROR_CALLBACK: + $setmode = $mode; + // class/object method callback + if (is_callable($options)) { + $setoptions = $options; + } else { + trigger_error("invalid error callback", E_USER_WARNING); + } + break; + + default: + trigger_error("invalid error mode", E_USER_WARNING); + break; + } + return true; + } + + /** + * Push a new error handler on top of the error handler options stack. With this + * you can easily override the actual error handler for some code and restore + * it later with popErrorHandling. + * + * @param mixed $mode (same as setErrorHandling) + * @param mixed $options (same as setErrorHandling) + * + * @return bool Always true + * + * @see PEAR::setErrorHandling + */ + protected static function _pushErrorHandling($object, $mode, $options = null) + { + $stack = &$GLOBALS['_PEAR_error_handler_stack']; + if ($object !== null) { + $def_mode = &$object->_default_error_mode; + $def_options = &$object->_default_error_options; + } else { + $def_mode = &$GLOBALS['_PEAR_default_error_mode']; + $def_options = &$GLOBALS['_PEAR_default_error_options']; + } + $stack[] = array($def_mode, $def_options); + + if ($object !== null) { + $object->setErrorHandling($mode, $options); + } else { + PEAR::setErrorHandling($mode, $options); + } + $stack[] = array($mode, $options); + return true; + } + + /** + * Pop the last error handler used + * + * @return bool Always true + * + * @see PEAR::pushErrorHandling + */ + protected static function _popErrorHandling($object) + { + $stack = &$GLOBALS['_PEAR_error_handler_stack']; + array_pop($stack); + list($mode, $options) = $stack[sizeof($stack) - 1]; + array_pop($stack); + if ($object !== null) { + $object->setErrorHandling($mode, $options); + } else { + PEAR::setErrorHandling($mode, $options); + } + return true; + } + + /** + * OS independent PHP extension load. Remember to take care + * on the correct extension name for case sensitive OSes. + * + * @param string $ext The extension name + * @return bool Success or not on the dl() call + */ + public static function loadExtension($ext) + { + if (extension_loaded($ext)) { + return true; + } + + // if either returns true dl() will produce a FATAL error, stop that + if ( + function_exists('dl') === false || + ini_get('enable_dl') != 1 + ) { + return false; + } + + if (OS_WINDOWS) { + $suffix = '.dll'; + } elseif (PHP_OS == 'HP-UX') { + $suffix = '.sl'; + } elseif (PHP_OS == 'AIX') { + $suffix = '.a'; + } elseif (PHP_OS == 'OSX') { + $suffix = '.bundle'; + } else { + $suffix = '.so'; + } + + return @dl('php_'.$ext.$suffix) || @dl($ext.$suffix); + } + + /** + * Get SOURCE_DATE_EPOCH environment variable + * See https://reproducible-builds.org/specs/source-date-epoch/ + * + * @return int + * @access public + */ + static function getSourceDateEpoch() + { + if ($source_date_epoch = getenv('SOURCE_DATE_EPOCH')) { + if (preg_match('/^\d+$/', $source_date_epoch)) { + return (int) $source_date_epoch; + } else { + // "If the value is malformed, the build process SHOULD exit with a non-zero error code." + self::raiseError("Invalid SOURCE_DATE_EPOCH: $source_date_epoch"); + exit(1); + } + } else { + return time(); + } + } +} + +function _PEAR_call_destructors() +{ + global $_PEAR_destructor_object_list; + if (is_array($_PEAR_destructor_object_list) && + sizeof($_PEAR_destructor_object_list)) + { + reset($_PEAR_destructor_object_list); + + $destructLifoExists = PEAR::getStaticProperty('PEAR', 'destructlifo'); + + if ($destructLifoExists) { + $_PEAR_destructor_object_list = array_reverse($_PEAR_destructor_object_list); + } + + foreach ($_PEAR_destructor_object_list as $k => $objref) { + $classname = get_class($objref); + while ($classname) { + $destructor = "_$classname"; + if (method_exists($objref, $destructor)) { + $objref->$destructor(); + break; + } else { + $classname = get_parent_class($classname); + } + } + } + // Empty the object list to ensure that destructors are + // not called more than once. + $_PEAR_destructor_object_list = array(); + } + + // Now call the shutdown functions + if ( + isset($GLOBALS['_PEAR_shutdown_funcs']) && + is_array($GLOBALS['_PEAR_shutdown_funcs']) && + !empty($GLOBALS['_PEAR_shutdown_funcs']) + ) { + foreach ($GLOBALS['_PEAR_shutdown_funcs'] as $value) { + call_user_func_array($value[0], $value[1]); + } + } +} + +/** + * Standard PEAR error class for PHP 4 + * + * This class is supserseded by {@link PEAR_Exception} in PHP 5 + * + * @category pear + * @package PEAR + * @author Stig Bakken + * @author Tomas V.V. Cox + * @author Gregory Beaver + * @copyright 1997-2006 The PHP Group + * @license http://opensource.org/licenses/bsd-license.php New BSD License + * @version Release: @package_version@ + * @link http://pear.php.net/manual/en/core.pear.pear-error.php + * @see PEAR::raiseError(), PEAR::throwError() + * @since Class available since PHP 4.0.2 + */ +class PEAR_Error +{ + var $error_message_prefix = ''; + var $mode = PEAR_ERROR_RETURN; + var $level = E_USER_NOTICE; + var $code = -1; + var $message = ''; + var $userinfo = ''; + var $backtrace = null; + var $callback = null; + + /** + * PEAR_Error constructor + * + * @param string $message message + * + * @param int $code (optional) error code + * + * @param int $mode (optional) error mode, one of: PEAR_ERROR_RETURN, + * PEAR_ERROR_PRINT, PEAR_ERROR_DIE, PEAR_ERROR_TRIGGER, + * PEAR_ERROR_CALLBACK or PEAR_ERROR_EXCEPTION + * + * @param mixed $options (optional) error level, _OR_ in the case of + * PEAR_ERROR_CALLBACK, the callback function or object/method + * tuple. + * + * @param string $userinfo (optional) additional user/debug info + * + * @access public + * + */ + function __construct($message = 'unknown error', $code = null, + $mode = null, $options = null, $userinfo = null) + { + if ($mode === null) { + $mode = PEAR_ERROR_RETURN; + } + $this->message = $message; + $this->code = $code; + $this->mode = $mode; + $this->userinfo = $userinfo; + + $skiptrace = PEAR::getStaticProperty('PEAR_Error', 'skiptrace'); + + if (!$skiptrace) { + $this->backtrace = debug_backtrace(); + if (isset($this->backtrace[0]) && isset($this->backtrace[0]['object'])) { + unset($this->backtrace[0]['object']); + } + } + + if ($mode & PEAR_ERROR_CALLBACK) { + $this->level = E_USER_NOTICE; + $this->callback = $options; + } else { + if ($options === null) { + $options = E_USER_NOTICE; + } + + $this->level = $options; + $this->callback = null; + } + + if ($this->mode & PEAR_ERROR_PRINT) { + if (is_null($options) || is_int($options)) { + $format = "%s"; + } else { + $format = $options; + } + + printf($format, $this->getMessage()); + } + + if ($this->mode & PEAR_ERROR_TRIGGER) { + trigger_error($this->getMessage(), $this->level); + } + + if ($this->mode & PEAR_ERROR_DIE) { + $msg = $this->getMessage(); + if (is_null($options) || is_int($options)) { + $format = "%s"; + if (substr($msg, -1) != "\n") { + $msg .= "\n"; + } + } else { + $format = $options; + } + printf($format, $msg); + exit($code); + } + + if ($this->mode & PEAR_ERROR_CALLBACK && is_callable($this->callback)) { + call_user_func($this->callback, $this); + } + + if ($this->mode & PEAR_ERROR_EXCEPTION) { + trigger_error("PEAR_ERROR_EXCEPTION is obsolete, use class PEAR_Exception for exceptions", E_USER_WARNING); + eval('$e = new Exception($this->message, $this->code);throw($e);'); + } + } + + /** + * Only here for backwards compatibility. + * + * Class "Cache_Error" still uses it, among others. + * + * @param string $message Message + * @param int $code Error code + * @param int $mode Error mode + * @param mixed $options See __construct() + * @param string $userinfo Additional user/debug info + */ + public function PEAR_Error( + $message = 'unknown error', $code = null, $mode = null, + $options = null, $userinfo = null + ) { + self::__construct($message, $code, $mode, $options, $userinfo); + } + + /** + * Get the error mode from an error object. + * + * @return int error mode + * @access public + */ + function getMode() + { + return $this->mode; + } + + /** + * Get the callback function/method from an error object. + * + * @return mixed callback function or object/method array + * @access public + */ + function getCallback() + { + return $this->callback; + } + + /** + * Get the error message from an error object. + * + * @return string full error message + * @access public + */ + function getMessage() + { + return ($this->error_message_prefix . $this->message); + } + + /** + * Get error code from an error object + * + * @return int error code + * @access public + */ + function getCode() + { + return $this->code; + } + + /** + * Get the name of this error/exception. + * + * @return string error/exception name (type) + * @access public + */ + function getType() + { + return get_class($this); + } + + /** + * Get additional user-supplied information. + * + * @return string user-supplied information + * @access public + */ + function getUserInfo() + { + return $this->userinfo; + } + + /** + * Get additional debug information supplied by the application. + * + * @return string debug information + * @access public + */ + function getDebugInfo() + { + return $this->getUserInfo(); + } + + /** + * Get the call backtrace from where the error was generated. + * Supported with PHP 4.3.0 or newer. + * + * @param int $frame (optional) what frame to fetch + * @return array Backtrace, or NULL if not available. + * @access public + */ + function getBacktrace($frame = null) + { + if (defined('PEAR_IGNORE_BACKTRACE')) { + return null; + } + if ($frame === null) { + return $this->backtrace; + } + return $this->backtrace[$frame]; + } + + function addUserInfo($info) + { + if (empty($this->userinfo)) { + $this->userinfo = $info; + } else { + $this->userinfo .= " ** $info"; + } + } + + function __toString() + { + return $this->getMessage(); + } + + /** + * Make a string representation of this object. + * + * @return string a string with an object summary + * @access public + */ + function toString() + { + $modes = array(); + $levels = array(E_USER_NOTICE => 'notice', + E_USER_WARNING => 'warning', + E_USER_ERROR => 'error'); + if ($this->mode & PEAR_ERROR_CALLBACK) { + if (is_array($this->callback)) { + $callback = (is_object($this->callback[0]) ? + strtolower(get_class($this->callback[0])) : + $this->callback[0]) . '::' . + $this->callback[1]; + } else { + $callback = $this->callback; + } + return sprintf('[%s: message="%s" code=%d mode=callback '. + 'callback=%s prefix="%s" info="%s"]', + strtolower(get_class($this)), $this->message, $this->code, + $callback, $this->error_message_prefix, + $this->userinfo); + } + if ($this->mode & PEAR_ERROR_PRINT) { + $modes[] = 'print'; + } + if ($this->mode & PEAR_ERROR_TRIGGER) { + $modes[] = 'trigger'; + } + if ($this->mode & PEAR_ERROR_DIE) { + $modes[] = 'die'; + } + if ($this->mode & PEAR_ERROR_RETURN) { + $modes[] = 'return'; + } + return sprintf('[%s: message="%s" code=%d mode=%s level=%s '. + 'prefix="%s" info="%s"]', + strtolower(get_class($this)), $this->message, $this->code, + implode("|", $modes), $levels[$this->level], + $this->error_message_prefix, + $this->userinfo); + } +} + +/* + * Local Variables: + * mode: php + * tab-width: 4 + * c-basic-offset: 4 + * End: + */ diff --git a/3rdparty/pear/pear-core-minimal/src/PEAR/ErrorStack.php b/3rdparty/pear/pear-core-minimal/src/PEAR/ErrorStack.php new file mode 100644 index 00000000..6619fbb1 --- /dev/null +++ b/3rdparty/pear/pear-core-minimal/src/PEAR/ErrorStack.php @@ -0,0 +1,979 @@ + + * @copyright 2004-2008 Greg Beaver + * @license http://opensource.org/licenses/bsd-license.php New BSD License + * @link http://pear.php.net/package/PEAR_ErrorStack + */ + +/** + * Singleton storage + * + * Format: + *
+ * array(
+ *  'package1' => PEAR_ErrorStack object,
+ *  'package2' => PEAR_ErrorStack object,
+ *  ...
+ * )
+ * 
+ * @access private + * @global array $GLOBALS['_PEAR_ERRORSTACK_SINGLETON'] + */ +$GLOBALS['_PEAR_ERRORSTACK_SINGLETON'] = array(); + +/** + * Global error callback (default) + * + * This is only used if set to non-false. * is the default callback for + * all packages, whereas specific packages may set a default callback + * for all instances, regardless of whether they are a singleton or not. + * + * To exclude non-singletons, only set the local callback for the singleton + * @see PEAR_ErrorStack::setDefaultCallback() + * @access private + * @global array $GLOBALS['_PEAR_ERRORSTACK_DEFAULT_CALLBACK'] + */ +$GLOBALS['_PEAR_ERRORSTACK_DEFAULT_CALLBACK'] = array( + '*' => false, +); + +/** + * Global Log object (default) + * + * This is only used if set to non-false. Use to set a default log object for + * all stacks, regardless of instantiation order or location + * @see PEAR_ErrorStack::setDefaultLogger() + * @access private + * @global array $GLOBALS['_PEAR_ERRORSTACK_DEFAULT_LOGGER'] + */ +$GLOBALS['_PEAR_ERRORSTACK_DEFAULT_LOGGER'] = false; + +/** + * Global Overriding Callback + * + * This callback will override any error callbacks that specific loggers have set. + * Use with EXTREME caution + * @see PEAR_ErrorStack::staticPushCallback() + * @access private + * @global array $GLOBALS['_PEAR_ERRORSTACK_DEFAULT_LOGGER'] + */ +$GLOBALS['_PEAR_ERRORSTACK_OVERRIDE_CALLBACK'] = array(); + +/**#@+ + * One of four possible return values from the error Callback + * @see PEAR_ErrorStack::_errorCallback() + */ +/** + * If this is returned, then the error will be both pushed onto the stack + * and logged. + */ +define('PEAR_ERRORSTACK_PUSHANDLOG', 1); +/** + * If this is returned, then the error will only be pushed onto the stack, + * and not logged. + */ +define('PEAR_ERRORSTACK_PUSH', 2); +/** + * If this is returned, then the error will only be logged, but not pushed + * onto the error stack. + */ +define('PEAR_ERRORSTACK_LOG', 3); +/** + * If this is returned, then the error is completely ignored. + */ +define('PEAR_ERRORSTACK_IGNORE', 4); +/** + * If this is returned, then the error is logged and die() is called. + */ +define('PEAR_ERRORSTACK_DIE', 5); +/**#@-*/ + +/** + * Error code for an attempt to instantiate a non-class as a PEAR_ErrorStack in + * the singleton method. + */ +define('PEAR_ERRORSTACK_ERR_NONCLASS', 1); + +/** + * Error code for an attempt to pass an object into {@link PEAR_ErrorStack::getMessage()} + * that has no __toString() method + */ +define('PEAR_ERRORSTACK_ERR_OBJTOSTRING', 2); +/** + * Error Stack Implementation + * + * Usage: + * + * // global error stack + * $global_stack = &PEAR_ErrorStack::singleton('MyPackage'); + * // local error stack + * $local_stack = new PEAR_ErrorStack('MyPackage'); + * + * @author Greg Beaver + * @version @package_version@ + * @package PEAR_ErrorStack + * @category Debugging + * @copyright 2004-2008 Greg Beaver + * @license http://opensource.org/licenses/bsd-license.php New BSD License + * @link http://pear.php.net/package/PEAR_ErrorStack + */ +class PEAR_ErrorStack { + /** + * Errors are stored in the order that they are pushed on the stack. + * @since 0.4alpha Errors are no longer organized by error level. + * This renders pop() nearly unusable, and levels could be more easily + * handled in a callback anyway + * @var array + * @access private + */ + var $_errors = array(); + + /** + * Storage of errors by level. + * + * Allows easy retrieval and deletion of only errors from a particular level + * @since PEAR 1.4.0dev + * @var array + * @access private + */ + var $_errorsByLevel = array(); + + /** + * Package name this error stack represents + * @var string + * @access protected + */ + var $_package; + + /** + * Determines whether a PEAR_Error is thrown upon every error addition + * @var boolean + * @access private + */ + var $_compat = false; + + /** + * If set to a valid callback, this will be used to generate the error + * message from the error code, otherwise the message passed in will be + * used + * @var false|string|array + * @access private + */ + var $_msgCallback = false; + + /** + * If set to a valid callback, this will be used to generate the error + * context for an error. For PHP-related errors, this will be a file + * and line number as retrieved from debug_backtrace(), but can be + * customized for other purposes. The error might actually be in a separate + * configuration file, or in a database query. + * @var false|string|array + * @access protected + */ + var $_contextCallback = false; + + /** + * If set to a valid callback, this will be called every time an error + * is pushed onto the stack. The return value will be used to determine + * whether to allow an error to be pushed or logged. + * + * The return value must be one an PEAR_ERRORSTACK_* constant + * @see PEAR_ERRORSTACK_PUSHANDLOG, PEAR_ERRORSTACK_PUSH, PEAR_ERRORSTACK_LOG + * @var false|string|array + * @access protected + */ + var $_errorCallback = array(); + + /** + * PEAR::Log object for logging errors + * @var false|Log + * @access protected + */ + var $_logger = false; + + /** + * Error messages - designed to be overridden + * @var array + * @abstract + */ + var $_errorMsgs = array(); + + /** + * Set up a new error stack + * + * @param string $package name of the package this error stack represents + * @param callback $msgCallback callback used for error message generation + * @param callback $contextCallback callback used for context generation, + * defaults to {@link getFileLine()} + * @param boolean $throwPEAR_Error + */ + function __construct($package, $msgCallback = false, $contextCallback = false, + $throwPEAR_Error = false) + { + $this->_package = $package; + $this->setMessageCallback($msgCallback); + $this->setContextCallback($contextCallback); + $this->_compat = $throwPEAR_Error; + } + + /** + * Return a single error stack for this package. + * + * Note that all parameters are ignored if the stack for package $package + * has already been instantiated + * @param string $package name of the package this error stack represents + * @param callback $msgCallback callback used for error message generation + * @param callback $contextCallback callback used for context generation, + * defaults to {@link getFileLine()} + * @param boolean $throwPEAR_Error + * @param string $stackClass class to instantiate + * + * @return PEAR_ErrorStack + */ + public static function &singleton( + $package, $msgCallback = false, $contextCallback = false, + $throwPEAR_Error = false, $stackClass = 'PEAR_ErrorStack' + ) { + if (isset($GLOBALS['_PEAR_ERRORSTACK_SINGLETON'][$package])) { + return $GLOBALS['_PEAR_ERRORSTACK_SINGLETON'][$package]; + } + if (!class_exists($stackClass)) { + if (function_exists('debug_backtrace')) { + $trace = debug_backtrace(); + } + PEAR_ErrorStack::staticPush('PEAR_ErrorStack', PEAR_ERRORSTACK_ERR_NONCLASS, + 'exception', array('stackclass' => $stackClass), + 'stack class "%stackclass%" is not a valid class name (should be like PEAR_ErrorStack)', + false, $trace); + } + $GLOBALS['_PEAR_ERRORSTACK_SINGLETON'][$package] = + new $stackClass($package, $msgCallback, $contextCallback, $throwPEAR_Error); + + return $GLOBALS['_PEAR_ERRORSTACK_SINGLETON'][$package]; + } + + /** + * Internal error handler for PEAR_ErrorStack class + * + * Dies if the error is an exception (and would have died anyway) + * @access private + */ + function _handleError($err) + { + if ($err['level'] == 'exception') { + $message = $err['message']; + if (isset($_SERVER['REQUEST_URI'])) { + echo '
'; + } else { + echo "\n"; + } + var_dump($err['context']); + die($message); + } + } + + /** + * Set up a PEAR::Log object for all error stacks that don't have one + * @param Log $log + */ + public static function setDefaultLogger(&$log) + { + if (is_object($log) && method_exists($log, 'log') ) { + $GLOBALS['_PEAR_ERRORSTACK_DEFAULT_LOGGER'] = &$log; + } elseif (is_callable($log)) { + $GLOBALS['_PEAR_ERRORSTACK_DEFAULT_LOGGER'] = &$log; + } + } + + /** + * Set up a PEAR::Log object for this error stack + * @param Log $log + */ + function setLogger(&$log) + { + if (is_object($log) && method_exists($log, 'log') ) { + $this->_logger = &$log; + } elseif (is_callable($log)) { + $this->_logger = &$log; + } + } + + /** + * Set an error code => error message mapping callback + * + * This method sets the callback that can be used to generate error + * messages for any instance + * @param array|string Callback function/method + */ + function setMessageCallback($msgCallback) + { + if (!$msgCallback) { + $this->_msgCallback = array(&$this, 'getErrorMessage'); + } else { + if (is_callable($msgCallback)) { + $this->_msgCallback = $msgCallback; + } + } + } + + /** + * Get an error code => error message mapping callback + * + * This method returns the current callback that can be used to generate error + * messages + * @return array|string|false Callback function/method or false if none + */ + function getMessageCallback() + { + return $this->_msgCallback; + } + + /** + * Sets a default callback to be used by all error stacks + * + * This method sets the callback that can be used to generate error + * messages for a singleton + * @param array|string Callback function/method + * @param string Package name, or false for all packages + */ + public static function setDefaultCallback($callback = false, $package = false) + { + if (!is_callable($callback)) { + $callback = false; + } + $package = $package ? $package : '*'; + $GLOBALS['_PEAR_ERRORSTACK_DEFAULT_CALLBACK'][$package] = $callback; + } + + /** + * Set a callback that generates context information (location of error) for an error stack + * + * This method sets the callback that can be used to generate context + * information for an error. Passing in NULL will disable context generation + * and remove the expensive call to debug_backtrace() + * @param array|string|null Callback function/method + */ + function setContextCallback($contextCallback) + { + if ($contextCallback === null) { + return $this->_contextCallback = false; + } + if (!$contextCallback) { + $this->_contextCallback = array(&$this, 'getFileLine'); + } else { + if (is_callable($contextCallback)) { + $this->_contextCallback = $contextCallback; + } + } + } + + /** + * Set an error Callback + * If set to a valid callback, this will be called every time an error + * is pushed onto the stack. The return value will be used to determine + * whether to allow an error to be pushed or logged. + * + * The return value must be one of the ERRORSTACK_* constants. + * + * This functionality can be used to emulate PEAR's pushErrorHandling, and + * the PEAR_ERROR_CALLBACK mode, without affecting the integrity of + * the error stack or logging + * @see PEAR_ERRORSTACK_PUSHANDLOG, PEAR_ERRORSTACK_PUSH, PEAR_ERRORSTACK_LOG + * @see popCallback() + * @param string|array $cb + */ + function pushCallback($cb) + { + array_push($this->_errorCallback, $cb); + } + + /** + * Remove a callback from the error callback stack + * @see pushCallback() + * @return array|string|false + */ + function popCallback() + { + if (!count($this->_errorCallback)) { + return false; + } + return array_pop($this->_errorCallback); + } + + /** + * Set a temporary overriding error callback for every package error stack + * + * Use this to temporarily disable all existing callbacks (can be used + * to emulate the @ operator, for instance) + * @see PEAR_ERRORSTACK_PUSHANDLOG, PEAR_ERRORSTACK_PUSH, PEAR_ERRORSTACK_LOG + * @see staticPopCallback(), pushCallback() + * @param string|array $cb + */ + public static function staticPushCallback($cb) + { + array_push($GLOBALS['_PEAR_ERRORSTACK_OVERRIDE_CALLBACK'], $cb); + } + + /** + * Remove a temporary overriding error callback + * @see staticPushCallback() + * @return array|string|false + */ + public static function staticPopCallback() + { + $ret = array_pop($GLOBALS['_PEAR_ERRORSTACK_OVERRIDE_CALLBACK']); + if (!is_array($GLOBALS['_PEAR_ERRORSTACK_OVERRIDE_CALLBACK'])) { + $GLOBALS['_PEAR_ERRORSTACK_OVERRIDE_CALLBACK'] = array(); + } + return $ret; + } + + /** + * Add an error to the stack + * + * If the message generator exists, it is called with 2 parameters. + * - the current Error Stack object + * - an array that is in the same format as an error. Available indices + * are 'code', 'package', 'time', 'params', 'level', and 'context' + * + * Next, if the error should contain context information, this is + * handled by the context grabbing method. + * Finally, the error is pushed onto the proper error stack + * @param int $code Package-specific error code + * @param string $level Error level. This is NOT spell-checked + * @param array $params associative array of error parameters + * @param string $msg Error message, or a portion of it if the message + * is to be generated + * @param array $repackage If this error re-packages an error pushed by + * another package, place the array returned from + * {@link pop()} in this parameter + * @param array $backtrace Protected parameter: use this to pass in the + * {@link debug_backtrace()} that should be used + * to find error context + * @return PEAR_Error|array if compatibility mode is on, a PEAR_Error is also + * thrown. If a PEAR_Error is returned, the userinfo + * property is set to the following array: + * + * + * array( + * 'code' => $code, + * 'params' => $params, + * 'package' => $this->_package, + * 'level' => $level, + * 'time' => time(), + * 'context' => $context, + * 'message' => $msg, + * //['repackage' => $err] repackaged error array/Exception class + * ); + * + * + * Normally, the previous array is returned. + */ + function push($code, $level = 'error', $params = array(), $msg = false, + $repackage = false, $backtrace = false) + { + $context = false; + // grab error context + if ($this->_contextCallback) { + if (!$backtrace) { + $backtrace = debug_backtrace(); + } + $context = call_user_func($this->_contextCallback, $code, $params, $backtrace); + } + + // save error + $time = explode(' ', microtime()); + $time = $time[1] + $time[0]; + $err = array( + 'code' => $code, + 'params' => $params, + 'package' => $this->_package, + 'level' => $level, + 'time' => $time, + 'context' => $context, + 'message' => $msg, + ); + + if ($repackage) { + $err['repackage'] = $repackage; + } + + // set up the error message, if necessary + if ($this->_msgCallback) { + $msg = call_user_func_array($this->_msgCallback, + array(&$this, $err)); + $err['message'] = $msg; + } + $push = $log = true; + $die = false; + // try the overriding callback first + $callback = $this->staticPopCallback(); + if ($callback) { + $this->staticPushCallback($callback); + } + if (!is_callable($callback)) { + // try the local callback next + $callback = $this->popCallback(); + if (is_callable($callback)) { + $this->pushCallback($callback); + } else { + // try the default callback + $callback = isset($GLOBALS['_PEAR_ERRORSTACK_DEFAULT_CALLBACK'][$this->_package]) ? + $GLOBALS['_PEAR_ERRORSTACK_DEFAULT_CALLBACK'][$this->_package] : + $GLOBALS['_PEAR_ERRORSTACK_DEFAULT_CALLBACK']['*']; + } + } + if (is_callable($callback)) { + switch(call_user_func($callback, $err)){ + case PEAR_ERRORSTACK_IGNORE: + return $err; + break; + case PEAR_ERRORSTACK_PUSH: + $log = false; + break; + case PEAR_ERRORSTACK_LOG: + $push = false; + break; + case PEAR_ERRORSTACK_DIE: + $die = true; + break; + // anything else returned has the same effect as pushandlog + } + } + if ($push) { + array_unshift($this->_errors, $err); + if (!isset($this->_errorsByLevel[$err['level']])) { + $this->_errorsByLevel[$err['level']] = array(); + } + $this->_errorsByLevel[$err['level']][] = &$this->_errors[0]; + } + if ($log) { + if ($this->_logger || $GLOBALS['_PEAR_ERRORSTACK_DEFAULT_LOGGER']) { + $this->_log($err); + } + } + if ($die) { + die(); + } + if ($this->_compat && $push) { + return $this->raiseError($msg, $code, null, null, $err); + } + return $err; + } + + /** + * Static version of {@link push()} + * + * @param string $package Package name this error belongs to + * @param int $code Package-specific error code + * @param string $level Error level. This is NOT spell-checked + * @param array $params associative array of error parameters + * @param string $msg Error message, or a portion of it if the message + * is to be generated + * @param array $repackage If this error re-packages an error pushed by + * another package, place the array returned from + * {@link pop()} in this parameter + * @param array $backtrace Protected parameter: use this to pass in the + * {@link debug_backtrace()} that should be used + * to find error context + * @return PEAR_Error|array if compatibility mode is on, a PEAR_Error is also + * thrown. see docs for {@link push()} + */ + public static function staticPush( + $package, $code, $level = 'error', $params = array(), + $msg = false, $repackage = false, $backtrace = false + ) { + $s = &PEAR_ErrorStack::singleton($package); + if ($s->_contextCallback) { + if (!$backtrace) { + if (function_exists('debug_backtrace')) { + $backtrace = debug_backtrace(); + } + } + } + return $s->push($code, $level, $params, $msg, $repackage, $backtrace); + } + + /** + * Log an error using PEAR::Log + * @param array $err Error array + * @param array $levels Error level => Log constant map + * @access protected + */ + function _log($err) + { + if ($this->_logger) { + $logger = &$this->_logger; + } else { + $logger = &$GLOBALS['_PEAR_ERRORSTACK_DEFAULT_LOGGER']; + } + if (is_a($logger, 'Log')) { + $levels = array( + 'exception' => PEAR_LOG_CRIT, + 'alert' => PEAR_LOG_ALERT, + 'critical' => PEAR_LOG_CRIT, + 'error' => PEAR_LOG_ERR, + 'warning' => PEAR_LOG_WARNING, + 'notice' => PEAR_LOG_NOTICE, + 'info' => PEAR_LOG_INFO, + 'debug' => PEAR_LOG_DEBUG); + if (isset($levels[$err['level']])) { + $level = $levels[$err['level']]; + } else { + $level = PEAR_LOG_INFO; + } + $logger->log($err['message'], $level, $err); + } else { // support non-standard logs + call_user_func($logger, $err); + } + } + + + /** + * Pop an error off of the error stack + * + * @return false|array + * @since 0.4alpha it is no longer possible to specify a specific error + * level to return - the last error pushed will be returned, instead + */ + function pop() + { + $err = @array_shift($this->_errors); + if (!is_null($err)) { + @array_pop($this->_errorsByLevel[$err['level']]); + if (!count($this->_errorsByLevel[$err['level']])) { + unset($this->_errorsByLevel[$err['level']]); + } + } + return $err; + } + + /** + * Pop an error off of the error stack, static method + * + * @param string package name + * @return boolean + * @since PEAR1.5.0a1 + */ + static function staticPop($package) + { + if ($package) { + if (!isset($GLOBALS['_PEAR_ERRORSTACK_SINGLETON'][$package])) { + return false; + } + return $GLOBALS['_PEAR_ERRORSTACK_SINGLETON'][$package]->pop(); + } + } + + /** + * Determine whether there are any errors on the stack + * @param string|array Level name. Use to determine if any errors + * of level (string), or levels (array) have been pushed + * @return boolean + */ + function hasErrors($level = false) + { + if ($level) { + return isset($this->_errorsByLevel[$level]); + } + return count($this->_errors); + } + + /** + * Retrieve all errors since last purge + * + * @param boolean set in order to empty the error stack + * @param string level name, to return only errors of a particular severity + * @return array + */ + function getErrors($purge = false, $level = false) + { + if (!$purge) { + if ($level) { + if (!isset($this->_errorsByLevel[$level])) { + return array(); + } else { + return $this->_errorsByLevel[$level]; + } + } else { + return $this->_errors; + } + } + if ($level) { + $ret = $this->_errorsByLevel[$level]; + foreach ($this->_errorsByLevel[$level] as $i => $unused) { + // entries are references to the $_errors array + $this->_errorsByLevel[$level][$i] = false; + } + // array_filter removes all entries === false + $this->_errors = array_filter($this->_errors); + unset($this->_errorsByLevel[$level]); + return $ret; + } + $ret = $this->_errors; + $this->_errors = array(); + $this->_errorsByLevel = array(); + return $ret; + } + + /** + * Determine whether there are any errors on a single error stack, or on any error stack + * + * The optional parameter can be used to test the existence of any errors without the need of + * singleton instantiation + * @param string|false Package name to check for errors + * @param string Level name to check for a particular severity + * @return boolean + */ + public static function staticHasErrors($package = false, $level = false) + { + if ($package) { + if (!isset($GLOBALS['_PEAR_ERRORSTACK_SINGLETON'][$package])) { + return false; + } + return $GLOBALS['_PEAR_ERRORSTACK_SINGLETON'][$package]->hasErrors($level); + } + foreach ($GLOBALS['_PEAR_ERRORSTACK_SINGLETON'] as $package => $obj) { + if ($obj->hasErrors($level)) { + return true; + } + } + return false; + } + + /** + * Get a list of all errors since last purge, organized by package + * @since PEAR 1.4.0dev BC break! $level is now in the place $merge used to be + * @param boolean $purge Set to purge the error stack of existing errors + * @param string $level Set to a level name in order to retrieve only errors of a particular level + * @param boolean $merge Set to return a flat array, not organized by package + * @param array $sortfunc Function used to sort a merged array - default + * sorts by time, and should be good for most cases + * + * @return array + */ + public static function staticGetErrors( + $purge = false, $level = false, $merge = false, + $sortfunc = array('PEAR_ErrorStack', '_sortErrors') + ) { + $ret = array(); + if (!is_callable($sortfunc)) { + $sortfunc = array('PEAR_ErrorStack', '_sortErrors'); + } + foreach ($GLOBALS['_PEAR_ERRORSTACK_SINGLETON'] as $package => $obj) { + $test = $GLOBALS['_PEAR_ERRORSTACK_SINGLETON'][$package]->getErrors($purge, $level); + if ($test) { + if ($merge) { + $ret = array_merge($ret, $test); + } else { + $ret[$package] = $test; + } + } + } + if ($merge) { + usort($ret, $sortfunc); + } + return $ret; + } + + /** + * Error sorting function, sorts by time + * @access private + */ + public static function _sortErrors($a, $b) + { + if ($a['time'] == $b['time']) { + return 0; + } + if ($a['time'] < $b['time']) { + return 1; + } + return -1; + } + + /** + * Standard file/line number/function/class context callback + * + * This function uses a backtrace generated from {@link debug_backtrace()} + * and so will not work at all in PHP < 4.3.0. The frame should + * reference the frame that contains the source of the error. + * @return array|false either array('file' => file, 'line' => line, + * 'function' => function name, 'class' => class name) or + * if this doesn't work, then false + * @param unused + * @param integer backtrace frame. + * @param array Results of debug_backtrace() + */ + public static function getFileLine($code, $params, $backtrace = null) + { + if ($backtrace === null) { + return false; + } + $frame = 0; + $functionframe = 1; + if (!isset($backtrace[1])) { + $functionframe = 0; + } else { + while (isset($backtrace[$functionframe]['function']) && + $backtrace[$functionframe]['function'] == 'eval' && + isset($backtrace[$functionframe + 1])) { + $functionframe++; + } + } + if (isset($backtrace[$frame])) { + if (!isset($backtrace[$frame]['file'])) { + $frame++; + } + $funcbacktrace = $backtrace[$functionframe]; + $filebacktrace = $backtrace[$frame]; + $ret = array('file' => $filebacktrace['file'], + 'line' => $filebacktrace['line']); + // rearrange for eval'd code or create function errors + if (strpos($filebacktrace['file'], '(') && + preg_match(';^(.*?)\((\d+)\) : (.*?)\\z;', $filebacktrace['file'], + $matches)) { + $ret['file'] = $matches[1]; + $ret['line'] = $matches[2] + 0; + } + if (isset($funcbacktrace['function']) && isset($backtrace[1])) { + if ($funcbacktrace['function'] != 'eval') { + if ($funcbacktrace['function'] == '__lambda_func') { + $ret['function'] = 'create_function() code'; + } else { + $ret['function'] = $funcbacktrace['function']; + } + } + } + if (isset($funcbacktrace['class']) && isset($backtrace[1])) { + $ret['class'] = $funcbacktrace['class']; + } + return $ret; + } + return false; + } + + /** + * Standard error message generation callback + * + * This method may also be called by a custom error message generator + * to fill in template values from the params array, simply + * set the third parameter to the error message template string to use + * + * The special variable %__msg% is reserved: use it only to specify + * where a message passed in by the user should be placed in the template, + * like so: + * + * Error message: %msg% - internal error + * + * If the message passed like so: + * + * + * $stack->push(ERROR_CODE, 'error', array(), 'server error 500'); + * + * + * The returned error message will be "Error message: server error 500 - + * internal error" + * @param PEAR_ErrorStack + * @param array + * @param string|false Pre-generated error message template + * + * @return string + */ + public static function getErrorMessage(&$stack, $err, $template = false) + { + if ($template) { + $mainmsg = $template; + } else { + $mainmsg = $stack->getErrorMessageTemplate($err['code']); + } + $mainmsg = str_replace('%__msg%', $err['message'], $mainmsg); + if (is_array($err['params']) && count($err['params'])) { + foreach ($err['params'] as $name => $val) { + if (is_array($val)) { + // @ is needed in case $val is a multi-dimensional array + $val = @implode(', ', $val); + } + if (is_object($val)) { + if (method_exists($val, '__toString')) { + $val = $val->__toString(); + } else { + PEAR_ErrorStack::staticPush('PEAR_ErrorStack', PEAR_ERRORSTACK_ERR_OBJTOSTRING, + 'warning', array('obj' => get_class($val)), + 'object %obj% passed into getErrorMessage, but has no __toString() method'); + $val = 'Object'; + } + } + $mainmsg = str_replace('%' . $name . '%', $val, $mainmsg); + } + } + return $mainmsg; + } + + /** + * Standard Error Message Template generator from code + * @return string + */ + function getErrorMessageTemplate($code) + { + if (!isset($this->_errorMsgs[$code])) { + return '%__msg%'; + } + return $this->_errorMsgs[$code]; + } + + /** + * Set the Error Message Template array + * + * The array format must be: + *
+     * array(error code => 'message template',...)
+     * 
+ * + * Error message parameters passed into {@link push()} will be used as input + * for the error message. If the template is 'message %foo% was %bar%', and the + * parameters are array('foo' => 'one', 'bar' => 'six'), the error message returned will + * be 'message one was six' + * @return string + */ + function setErrorMessageTemplate($template) + { + $this->_errorMsgs = $template; + } + + + /** + * emulate PEAR::raiseError() + * + * @return PEAR_Error + */ + function raiseError() + { + require_once 'PEAR.php'; + $args = func_get_args(); + return call_user_func_array(array('PEAR', 'raiseError'), $args); + } +} +$stack = &PEAR_ErrorStack::singleton('PEAR_ErrorStack'); +$stack->pushCallback(array('PEAR_ErrorStack', '_handleError')); +?> diff --git a/3rdparty/pear/pear-core-minimal/src/System.php b/3rdparty/pear/pear-core-minimal/src/System.php new file mode 100644 index 00000000..a7ef4659 --- /dev/null +++ b/3rdparty/pear/pear-core-minimal/src/System.php @@ -0,0 +1,630 @@ + + * @copyright 1997-2009 The Authors + * @license http://opensource.org/licenses/bsd-license.php New BSD License + * @link http://pear.php.net/package/PEAR + * @since File available since Release 0.1 + */ + +/** + * base class + */ +require_once 'PEAR.php'; +require_once 'Console/Getopt.php'; + +$GLOBALS['_System_temp_files'] = array(); + +/** +* System offers cross platform compatible system functions +* +* Static functions for different operations. Should work under +* Unix and Windows. The names and usage has been taken from its respectively +* GNU commands. The functions will return (bool) false on error and will +* trigger the error with the PHP trigger_error() function (you can silence +* the error by prefixing a '@' sign after the function call, but this +* is not recommended practice. Instead use an error handler with +* {@link set_error_handler()}). +* +* Documentation on this class you can find in: +* http://pear.php.net/manual/ +* +* Example usage: +* if (!@System::rm('-r file1 dir1')) { +* print "could not delete file1 or dir1"; +* } +* +* In case you need to to pass file names with spaces, +* pass the params as an array: +* +* System::rm(array('-r', $file1, $dir1)); +* +* @category pear +* @package System +* @author Tomas V.V. Cox +* @copyright 1997-2006 The PHP Group +* @license http://opensource.org/licenses/bsd-license.php New BSD License +* @version Release: @package_version@ +* @link http://pear.php.net/package/PEAR +* @since Class available since Release 0.1 +* @static +*/ +class System +{ + /** + * returns the commandline arguments of a function + * + * @param string $argv the commandline + * @param string $short_options the allowed option short-tags + * @param string $long_options the allowed option long-tags + * @return array the given options and there values + */ + public static function _parseArgs($argv, $short_options, $long_options = null) + { + if (!is_array($argv) && $argv !== null) { + /* + // Quote all items that are a short option + $av = preg_split('/(\A| )--?[a-z0-9]+[ =]?((? $a) { + if (empty($a)) { + continue; + } + $argv[$k] = trim($a) ; + } + } + + return Console_Getopt::getopt2($argv, $short_options, $long_options); + } + + /** + * Output errors with PHP trigger_error(). You can silence the errors + * with prefixing a "@" sign to the function call: @System::mkdir(..); + * + * @param mixed $error a PEAR error or a string with the error message + * @return bool false + */ + protected static function raiseError($error) + { + if (PEAR::isError($error)) { + $error = $error->getMessage(); + } + trigger_error($error, E_USER_WARNING); + return false; + } + + /** + * Creates a nested array representing the structure of a directory + * + * System::_dirToStruct('dir1', 0) => + * Array + * ( + * [dirs] => Array + * ( + * [0] => dir1 + * ) + * + * [files] => Array + * ( + * [0] => dir1/file2 + * [1] => dir1/file3 + * ) + * ) + * @param string $sPath Name of the directory + * @param integer $maxinst max. deep of the lookup + * @param integer $aktinst starting deep of the lookup + * @param bool $silent if true, do not emit errors. + * @return array the structure of the dir + */ + protected static function _dirToStruct($sPath, $maxinst, $aktinst = 0, $silent = false) + { + $struct = array('dirs' => array(), 'files' => array()); + if (($dir = @opendir($sPath)) === false) { + if (!$silent) { + System::raiseError("Could not open dir $sPath"); + } + return $struct; // XXX could not open error + } + + $struct['dirs'][] = $sPath = realpath($sPath); // XXX don't add if '.' or '..' ? + $list = array(); + while (false !== ($file = readdir($dir))) { + if ($file != '.' && $file != '..') { + $list[] = $file; + } + } + + closedir($dir); + natsort($list); + if ($aktinst < $maxinst || $maxinst == 0) { + foreach ($list as $val) { + $path = $sPath . DIRECTORY_SEPARATOR . $val; + if (is_dir($path) && !is_link($path)) { + $tmp = System::_dirToStruct($path, $maxinst, $aktinst+1, $silent); + $struct = array_merge_recursive($struct, $tmp); + } else { + $struct['files'][] = $path; + } + } + } + + return $struct; + } + + /** + * Creates a nested array representing the structure of a directory and files + * + * @param array $files Array listing files and dirs + * @return array + * @static + * @see System::_dirToStruct() + */ + protected static function _multipleToStruct($files) + { + $struct = array('dirs' => array(), 'files' => array()); + settype($files, 'array'); + foreach ($files as $file) { + if (is_dir($file) && !is_link($file)) { + $tmp = System::_dirToStruct($file, 0); + $struct = array_merge_recursive($tmp, $struct); + } else { + if (!in_array($file, $struct['files'])) { + $struct['files'][] = $file; + } + } + } + return $struct; + } + + /** + * The rm command for removing files. + * Supports multiple files and dirs and also recursive deletes + * + * @param string $args the arguments for rm + * @return mixed PEAR_Error or true for success + * @static + * @access public + */ + public static function rm($args) + { + $opts = System::_parseArgs($args, 'rf'); // "f" does nothing but I like it :-) + if (PEAR::isError($opts)) { + return System::raiseError($opts); + } + foreach ($opts[0] as $opt) { + if ($opt[0] == 'r') { + $do_recursive = true; + } + } + $ret = true; + if (isset($do_recursive)) { + $struct = System::_multipleToStruct($opts[1]); + foreach ($struct['files'] as $file) { + if (!@unlink($file)) { + $ret = false; + } + } + + rsort($struct['dirs']); + foreach ($struct['dirs'] as $dir) { + if (!@rmdir($dir)) { + $ret = false; + } + } + } else { + foreach ($opts[1] as $file) { + $delete = (is_dir($file)) ? 'rmdir' : 'unlink'; + if (!@$delete($file)) { + $ret = false; + } + } + } + return $ret; + } + + /** + * Make directories. + * + * The -p option will create parent directories + * @param string $args the name of the director(y|ies) to create + * @return bool True for success + */ + public static function mkDir($args) + { + $opts = System::_parseArgs($args, 'pm:'); + if (PEAR::isError($opts)) { + return System::raiseError($opts); + } + + $mode = 0777; // default mode + foreach ($opts[0] as $opt) { + if ($opt[0] == 'p') { + $create_parents = true; + } elseif ($opt[0] == 'm') { + // if the mode is clearly an octal number (starts with 0) + // convert it to decimal + if (strlen($opt[1]) && $opt[1][0] == '0') { + $opt[1] = octdec($opt[1]); + } else { + // convert to int + $opt[1] += 0; + } + $mode = $opt[1]; + } + } + + $ret = true; + if (isset($create_parents)) { + foreach ($opts[1] as $dir) { + $dirstack = array(); + while ((!file_exists($dir) || !is_dir($dir)) && + $dir != DIRECTORY_SEPARATOR) { + array_unshift($dirstack, $dir); + $dir = dirname($dir); + } + + while ($newdir = array_shift($dirstack)) { + if (!is_writeable(dirname($newdir))) { + $ret = false; + break; + } + + if (!mkdir($newdir, $mode)) { + $ret = false; + } + } + } + } else { + foreach($opts[1] as $dir) { + if ((@file_exists($dir) || !is_dir($dir)) && !mkdir($dir, $mode)) { + $ret = false; + } + } + } + + return $ret; + } + + /** + * Concatenate files + * + * Usage: + * 1) $var = System::cat('sample.txt test.txt'); + * 2) System::cat('sample.txt test.txt > final.txt'); + * 3) System::cat('sample.txt test.txt >> final.txt'); + * + * Note: as the class use fopen, urls should work also + * + * @param string $args the arguments + * @return boolean true on success + */ + public static function &cat($args) + { + $ret = null; + $files = array(); + if (!is_array($args)) { + $args = preg_split('/\s+/', $args, -1, PREG_SPLIT_NO_EMPTY); + } + + $count_args = count($args); + for ($i = 0; $i < $count_args; $i++) { + if ($args[$i] == '>') { + $mode = 'wb'; + $outputfile = $args[$i+1]; + break; + } elseif ($args[$i] == '>>') { + $mode = 'ab+'; + $outputfile = $args[$i+1]; + break; + } else { + $files[] = $args[$i]; + } + } + $outputfd = false; + if (isset($mode)) { + if (!$outputfd = fopen($outputfile, $mode)) { + $err = System::raiseError("Could not open $outputfile"); + return $err; + } + $ret = true; + } + foreach ($files as $file) { + if (!$fd = fopen($file, 'r')) { + System::raiseError("Could not open $file"); + continue; + } + while ($cont = fread($fd, 2048)) { + if (is_resource($outputfd)) { + fwrite($outputfd, $cont); + } else { + $ret .= $cont; + } + } + fclose($fd); + } + if (is_resource($outputfd)) { + fclose($outputfd); + } + return $ret; + } + + /** + * Creates temporary files or directories. This function will remove + * the created files when the scripts finish its execution. + * + * Usage: + * 1) $tempfile = System::mktemp("prefix"); + * 2) $tempdir = System::mktemp("-d prefix"); + * 3) $tempfile = System::mktemp(); + * 4) $tempfile = System::mktemp("-t /var/tmp prefix"); + * + * prefix -> The string that will be prepended to the temp name + * (defaults to "tmp"). + * -d -> A temporary dir will be created instead of a file. + * -t -> The target dir where the temporary (file|dir) will be created. If + * this param is missing by default the env vars TMP on Windows or + * TMPDIR in Unix will be used. If these vars are also missing + * c:\windows\temp or /tmp will be used. + * + * @param string $args The arguments + * @return mixed the full path of the created (file|dir) or false + * @see System::tmpdir() + */ + public static function mktemp($args = null) + { + static $first_time = true; + $opts = System::_parseArgs($args, 't:d'); + if (PEAR::isError($opts)) { + return System::raiseError($opts); + } + + foreach ($opts[0] as $opt) { + if ($opt[0] == 'd') { + $tmp_is_dir = true; + } elseif ($opt[0] == 't') { + $tmpdir = $opt[1]; + } + } + + $prefix = (isset($opts[1][0])) ? $opts[1][0] : 'tmp'; + if (!isset($tmpdir)) { + $tmpdir = System::tmpdir(); + } + + if (!System::mkDir(array('-p', $tmpdir))) { + return false; + } + + $tmp = tempnam($tmpdir, $prefix); + if (isset($tmp_is_dir)) { + unlink($tmp); // be careful possible race condition here + if (!mkdir($tmp, 0700)) { + return System::raiseError("Unable to create temporary directory $tmpdir"); + } + } + + $GLOBALS['_System_temp_files'][] = $tmp; + if (isset($tmp_is_dir)) { + //$GLOBALS['_System_temp_files'][] = dirname($tmp); + } + + if ($first_time) { + PEAR::registerShutdownFunc(array('System', '_removeTmpFiles')); + $first_time = false; + } + + return $tmp; + } + + /** + * Remove temporary files created my mkTemp. This function is executed + * at script shutdown time + */ + public static function _removeTmpFiles() + { + if (count($GLOBALS['_System_temp_files'])) { + $delete = $GLOBALS['_System_temp_files']; + array_unshift($delete, '-r'); + System::rm($delete); + $GLOBALS['_System_temp_files'] = array(); + } + } + + /** + * Get the path of the temporal directory set in the system + * by looking in its environments variables. + * Note: php.ini-recommended removes the "E" from the variables_order setting, + * making unavaible the $_ENV array, that s why we do tests with _ENV + * + * @return string The temporary directory on the system + */ + public static function tmpdir() + { + if (OS_WINDOWS) { + if ($var = isset($_ENV['TMP']) ? $_ENV['TMP'] : getenv('TMP')) { + return $var; + } + if ($var = isset($_ENV['TEMP']) ? $_ENV['TEMP'] : getenv('TEMP')) { + return $var; + } + if ($var = isset($_ENV['USERPROFILE']) ? $_ENV['USERPROFILE'] : getenv('USERPROFILE')) { + return $var; + } + if ($var = isset($_ENV['windir']) ? $_ENV['windir'] : getenv('windir')) { + return $var; + } + return getenv('SystemRoot') . '\temp'; + } + if ($var = isset($_ENV['TMPDIR']) ? $_ENV['TMPDIR'] : getenv('TMPDIR')) { + return $var; + } + return realpath(function_exists('sys_get_temp_dir') ? sys_get_temp_dir() : '/tmp'); + } + + /** + * The "which" command (show the full path of a command) + * + * @param string $program The command to search for + * @param mixed $fallback Value to return if $program is not found + * + * @return mixed A string with the full path or false if not found + * @author Stig Bakken + */ + public static function which($program, $fallback = false) + { + // enforce API + if (!is_string($program) || '' == $program) { + return $fallback; + } + + // full path given + if (basename($program) != $program) { + $path_elements[] = dirname($program); + $program = basename($program); + } else { + $path = getenv('PATH'); + if (!$path) { + $path = getenv('Path'); // some OSes are just stupid enough to do this + } + + $path_elements = explode(PATH_SEPARATOR, $path); + } + + if (OS_WINDOWS) { + $exe_suffixes = getenv('PATHEXT') + ? explode(PATH_SEPARATOR, getenv('PATHEXT')) + : array('.exe','.bat','.cmd','.com'); + // allow passing a command.exe param + if (strpos($program, '.') !== false) { + array_unshift($exe_suffixes, ''); + } + } else { + $exe_suffixes = array(''); + } + + foreach ($exe_suffixes as $suff) { + foreach ($path_elements as $dir) { + $file = $dir . DIRECTORY_SEPARATOR . $program . $suff; + // It's possible to run a .bat on Windows that is_executable + // would return false for. The is_executable check is meaningless... + if (OS_WINDOWS) { + if (file_exists($file)) { + return $file; + } + } else { + if (is_executable($file)) { + return $file; + } + } + } + } + return $fallback; + } + + /** + * The "find" command + * + * Usage: + * + * System::find($dir); + * System::find("$dir -type d"); + * System::find("$dir -type f"); + * System::find("$dir -name *.php"); + * System::find("$dir -name *.php -name *.htm*"); + * System::find("$dir -maxdepth 1"); + * + * Params implemented: + * $dir -> Start the search at this directory + * -type d -> return only directories + * -type f -> return only files + * -maxdepth -> max depth of recursion + * -name -> search pattern (bash style). Multiple -name param allowed + * + * @param mixed Either array or string with the command line + * @return array Array of found files + */ + public static function find($args) + { + if (!is_array($args)) { + $args = preg_split('/\s+/', $args, -1, PREG_SPLIT_NO_EMPTY); + } + $dir = realpath(array_shift($args)); + if (!$dir) { + return array(); + } + $patterns = array(); + $depth = 0; + $do_files = $do_dirs = true; + $args_count = count($args); + for ($i = 0; $i < $args_count; $i++) { + switch ($args[$i]) { + case '-type': + if (in_array($args[$i+1], array('d', 'f'))) { + if ($args[$i+1] == 'd') { + $do_files = false; + } else { + $do_dirs = false; + } + } + $i++; + break; + case '-name': + $name = preg_quote($args[$i+1], '#'); + // our magic characters ? and * have just been escaped, + // so now we change the escaped versions to PCRE operators + $name = strtr($name, array('\?' => '.', '\*' => '.*')); + $patterns[] = '('.$name.')'; + $i++; + break; + case '-maxdepth': + $depth = $args[$i+1]; + break; + } + } + $path = System::_dirToStruct($dir, $depth, 0, true); + if ($do_files && $do_dirs) { + $files = array_merge($path['files'], $path['dirs']); + } elseif ($do_dirs) { + $files = $path['dirs']; + } else { + $files = $path['files']; + } + if (count($patterns)) { + $dsq = preg_quote(DIRECTORY_SEPARATOR, '#'); + $pattern = '#(^|'.$dsq.')'.implode('|', $patterns).'($|'.$dsq.')#'; + $ret = array(); + $files_count = count($files); + for ($i = 0; $i < $files_count; $i++) { + // only search in the part of the file below the current directory + $filepart = basename($files[$i]); + if (preg_match($pattern, $filepart)) { + $ret[] = $files[$i]; + } + } + return $ret; + } + return $files; + } +} diff --git a/3rdparty/pear/pear_exception/LICENSE b/3rdparty/pear/pear_exception/LICENSE new file mode 100644 index 00000000..a00a2421 --- /dev/null +++ b/3rdparty/pear/pear_exception/LICENSE @@ -0,0 +1,27 @@ +Copyright (c) 1997-2009, + Stig Bakken , + Gregory Beaver , + Helgi Þormar Þorbjörnsson , + Tomas V.V.Cox , + Martin Jansen . +All rights reserved. + +Redistribution and use in source and binary forms, with or without +modification, are permitted provided that the following conditions are met: + + * Redistributions of source code must retain the above copyright notice, + this list of conditions and the following disclaimer. + * Redistributions in binary form must reproduce the above copyright + notice, this list of conditions and the following disclaimer in the + documentation and/or other materials provided with the distribution. + +THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" +AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE +IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE +DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE +FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL +DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR +SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER +CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, +OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE +OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. diff --git a/3rdparty/pear/pear_exception/PEAR/Exception.php b/3rdparty/pear/pear_exception/PEAR/Exception.php new file mode 100644 index 00000000..a8ef6e08 --- /dev/null +++ b/3rdparty/pear/pear_exception/PEAR/Exception.php @@ -0,0 +1,456 @@ + + * @author Hans Lellelid + * @author Bertrand Mansion + * @author Greg Beaver + * @copyright 1997-2009 The Authors + * @license http://opensource.org/licenses/bsd-license.php New BSD License + * @link http://pear.php.net/package/PEAR_Exception + * @since File available since Release 1.0.0 + */ + + +/** + * Base PEAR_Exception Class + * + * 1) Features: + * + * - Nestable exceptions (throw new PEAR_Exception($msg, $prev_exception)) + * - Definable triggers, shot when exceptions occur + * - Pretty and informative error messages + * - Added more context info available (like class, method or cause) + * - cause can be a PEAR_Exception or an array of mixed + * PEAR_Exceptions/PEAR_ErrorStack warnings + * - callbacks for specific exception classes and their children + * + * 2) Ideas: + * + * - Maybe a way to define a 'template' for the output + * + * 3) Inherited properties from PHP Exception Class: + * + * protected $message + * protected $code + * protected $line + * protected $file + * private $trace + * + * 4) Inherited methods from PHP Exception Class: + * + * __clone + * __construct + * getMessage + * getCode + * getFile + * getLine + * getTraceSafe + * getTraceSafeAsString + * __toString + * + * 5) Usage example + * + * + * require_once 'PEAR/Exception.php'; + * + * class Test { + * function foo() { + * throw new PEAR_Exception('Error Message', ERROR_CODE); + * } + * } + * + * function myLogger($pear_exception) { + * echo $pear_exception->getMessage(); + * } + * // each time a exception is thrown the 'myLogger' will be called + * // (its use is completely optional) + * PEAR_Exception::addObserver('myLogger'); + * $test = new Test; + * try { + * $test->foo(); + * } catch (PEAR_Exception $e) { + * print $e; + * } + * + * + * @category PEAR + * @package PEAR_Exception + * @author Tomas V.V.Cox + * @author Hans Lellelid + * @author Bertrand Mansion + * @author Greg Beaver + * @copyright 1997-2009 The Authors + * @license http://opensource.org/licenses/bsd-license.php New BSD License + * @version Release: @package_version@ + * @link http://pear.php.net/package/PEAR_Exception + * @since Class available since Release 1.0.0 + */ +class PEAR_Exception extends Exception +{ + const OBSERVER_PRINT = -2; + const OBSERVER_TRIGGER = -4; + const OBSERVER_DIE = -8; + protected $cause; + private static $_observers = array(); + private static $_uniqueid = 0; + private $_trace; + + /** + * Supported signatures: + * - PEAR_Exception(string $message); + * - PEAR_Exception(string $message, int $code); + * - PEAR_Exception(string $message, Exception $cause); + * - PEAR_Exception(string $message, Exception $cause, int $code); + * - PEAR_Exception(string $message, PEAR_Error $cause); + * - PEAR_Exception(string $message, PEAR_Error $cause, int $code); + * - PEAR_Exception(string $message, array $causes); + * - PEAR_Exception(string $message, array $causes, int $code); + * + * @param string $message exception message + * @param int|Exception|PEAR_Error|array|null $p2 exception cause + * @param int|null $p3 exception code or null + */ + public function __construct($message, $p2 = null, $p3 = null) + { + if (is_int($p2)) { + $code = $p2; + $this->cause = null; + } elseif (is_object($p2) || is_array($p2)) { + // using is_object allows both Exception and PEAR_Error + if (is_object($p2) && !($p2 instanceof Exception)) { + if (!class_exists('PEAR_Error') || !($p2 instanceof PEAR_Error)) { + throw new PEAR_Exception( + 'exception cause must be Exception, ' . + 'array, or PEAR_Error' + ); + } + } + $code = $p3; + if (is_array($p2) && isset($p2['message'])) { + // fix potential problem of passing in a single warning + $p2 = array($p2); + } + $this->cause = $p2; + } else { + $code = null; + $this->cause = null; + } + parent::__construct($message, (int) $code); + $this->signal(); + } + + /** + * Add an exception observer + * + * @param mixed $callback - A valid php callback, see php func is_callable() + * - A PEAR_Exception::OBSERVER_* constant + * - An array(const PEAR_Exception::OBSERVER_*, + * mixed $options) + * @param string $label The name of the observer. Use this if you want + * to remove it later with removeObserver() + * + * @return void + */ + public static function addObserver($callback, $label = 'default') + { + self::$_observers[$label] = $callback; + } + + /** + * Remove an exception observer + * + * @param string $label Name of the observer + * + * @return void + */ + public static function removeObserver($label = 'default') + { + unset(self::$_observers[$label]); + } + + /** + * Generate a unique ID for an observer + * + * @return int unique identifier for an observer + */ + public static function getUniqueId() + { + return self::$_uniqueid++; + } + + /** + * Send a signal to all observers + * + * @return void + */ + protected function signal() + { + foreach (self::$_observers as $func) { + if (is_callable($func)) { + call_user_func($func, $this); + continue; + } + settype($func, 'array'); + switch ($func[0]) { + case self::OBSERVER_PRINT : + $f = (isset($func[1])) ? $func[1] : '%s'; + printf($f, $this->getMessage()); + break; + case self::OBSERVER_TRIGGER : + $f = (isset($func[1])) ? $func[1] : E_USER_NOTICE; + trigger_error($this->getMessage(), $f); + break; + case self::OBSERVER_DIE : + $f = (isset($func[1])) ? $func[1] : '%s'; + die(printf($f, $this->getMessage())); + break; + default: + trigger_error('invalid observer type', E_USER_WARNING); + } + } + } + + /** + * Return specific error information that can be used for more detailed + * error messages or translation. + * + * This method may be overridden in child exception classes in order + * to add functionality not present in PEAR_Exception and is a placeholder + * to define API + * + * The returned array must be an associative array of parameter => value like so: + *
+     * array('name' => $name, 'context' => array(...))
+     * 
+ * + * @return array + */ + public function getErrorData() + { + return array(); + } + + /** + * Returns the exception that caused this exception to be thrown + * + * @return Exception|array The context of the exception + */ + public function getCause() + { + return $this->cause; + } + + /** + * Function must be public to call on caused exceptions + * + * @param array $causes Array that gets filled. + * + * @return void + */ + public function getCauseMessage(&$causes) + { + $trace = $this->getTraceSafe(); + $cause = array('class' => get_class($this), + 'message' => $this->message, + 'file' => 'unknown', + 'line' => 'unknown'); + if (isset($trace[0])) { + if (isset($trace[0]['file'])) { + $cause['file'] = $trace[0]['file']; + $cause['line'] = $trace[0]['line']; + } + } + $causes[] = $cause; + if ($this->cause instanceof PEAR_Exception) { + $this->cause->getCauseMessage($causes); + } elseif ($this->cause instanceof Exception) { + $causes[] = array('class' => get_class($this->cause), + 'message' => $this->cause->getMessage(), + 'file' => $this->cause->getFile(), + 'line' => $this->cause->getLine()); + } elseif (class_exists('PEAR_Error') && $this->cause instanceof PEAR_Error) { + $causes[] = array('class' => get_class($this->cause), + 'message' => $this->cause->getMessage(), + 'file' => 'unknown', + 'line' => 'unknown'); + } elseif (is_array($this->cause)) { + foreach ($this->cause as $cause) { + if ($cause instanceof PEAR_Exception) { + $cause->getCauseMessage($causes); + } elseif ($cause instanceof Exception) { + $causes[] = array('class' => get_class($cause), + 'message' => $cause->getMessage(), + 'file' => $cause->getFile(), + 'line' => $cause->getLine()); + } elseif (class_exists('PEAR_Error') + && $cause instanceof PEAR_Error + ) { + $causes[] = array('class' => get_class($cause), + 'message' => $cause->getMessage(), + 'file' => 'unknown', + 'line' => 'unknown'); + } elseif (is_array($cause) && isset($cause['message'])) { + // PEAR_ErrorStack warning + $causes[] = array( + 'class' => $cause['package'], + 'message' => $cause['message'], + 'file' => isset($cause['context']['file']) ? + $cause['context']['file'] : + 'unknown', + 'line' => isset($cause['context']['line']) ? + $cause['context']['line'] : + 'unknown', + ); + } + } + } + } + + /** + * Build a backtrace and return it + * + * @return array Backtrace + */ + public function getTraceSafe() + { + if (!isset($this->_trace)) { + $this->_trace = $this->getTrace(); + if (empty($this->_trace)) { + $backtrace = debug_backtrace(); + $this->_trace = array($backtrace[count($backtrace)-1]); + } + } + return $this->_trace; + } + + /** + * Gets the first class of the backtrace + * + * @return string Class name + */ + public function getErrorClass() + { + $trace = $this->getTraceSafe(); + return $trace[0]['class']; + } + + /** + * Gets the first method of the backtrace + * + * @return string Method/function name + */ + public function getErrorMethod() + { + $trace = $this->getTraceSafe(); + return $trace[0]['function']; + } + + /** + * Converts the exception to a string (HTML or plain text) + * + * @return string String representation + * + * @see toHtml() + * @see toText() + */ + public function __toString() + { + if (isset($_SERVER['REQUEST_URI'])) { + return $this->toHtml(); + } + return $this->toText(); + } + + /** + * Generates a HTML representation of the exception + * + * @return string HTML code + */ + public function toHtml() + { + $trace = $this->getTraceSafe(); + $causes = array(); + $this->getCauseMessage($causes); + $html = '' . "\n"; + foreach ($causes as $i => $cause) { + $html .= '\n"; + } + $html .= '' . "\n" + . '' + . '' + . '' . "\n"; + + foreach ($trace as $k => $v) { + $html .= '' + . '' + . '' . "\n"; + } + $html .= '' + . '' + . '' . "\n" + . '
' + . str_repeat('-', $i) . ' ' . $cause['class'] . ': ' + . htmlspecialchars($cause['message']) + . ' in ' . $cause['file'] . ' ' + . 'on line ' . $cause['line'] . '' + . "
Exception trace
#FunctionLocation
' . $k . ''; + if (!empty($v['class'])) { + $html .= $v['class'] . $v['type']; + } + $html .= $v['function']; + $args = array(); + if (!empty($v['args'])) { + foreach ($v['args'] as $arg) { + if (is_null($arg)) { + $args[] = 'null'; + } else if (is_array($arg)) { + $args[] = 'Array'; + } else if (is_object($arg)) { + $args[] = 'Object('.get_class($arg).')'; + } else if (is_bool($arg)) { + $args[] = $arg ? 'true' : 'false'; + } else if (is_int($arg) || is_double($arg)) { + $args[] = $arg; + } else { + $arg = (string)$arg; + $str = htmlspecialchars(substr($arg, 0, 16)); + if (strlen($arg) > 16) { + $str .= '…'; + } + $args[] = "'" . $str . "'"; + } + } + } + $html .= '(' . implode(', ', $args) . ')' + . '' . (isset($v['file']) ? $v['file'] : 'unknown') + . ':' . (isset($v['line']) ? $v['line'] : 'unknown') + . '
' . ($k+1) . '{main} 
'; + return $html; + } + + /** + * Generates text representation of the exception and stack trace + * + * @return string + */ + public function toText() + { + $causes = array(); + $this->getCauseMessage($causes); + $causeMsg = ''; + foreach ($causes as $i => $cause) { + $causeMsg .= str_repeat(' ', $i) . $cause['class'] . ': ' + . $cause['message'] . ' in ' . $cause['file'] + . ' on line ' . $cause['line'] . "\n"; + } + return $causeMsg . $this->getTraceAsString(); + } +} +?> diff --git a/3rdparty/php-http/guzzle7-adapter/LICENSE b/3rdparty/php-http/guzzle7-adapter/LICENSE new file mode 100644 index 00000000..0d6ce83b --- /dev/null +++ b/3rdparty/php-http/guzzle7-adapter/LICENSE @@ -0,0 +1,19 @@ +Copyright (c) 2020 PHP HTTP Team + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is furnished +to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +THE SOFTWARE. diff --git a/3rdparty/php-http/guzzle7-adapter/src/Client.php b/3rdparty/php-http/guzzle7-adapter/src/Client.php new file mode 100644 index 00000000..51724a6c --- /dev/null +++ b/3rdparty/php-http/guzzle7-adapter/src/Client.php @@ -0,0 +1,69 @@ + + */ +final class Client implements HttpClient, HttpAsyncClient +{ + /** + * @var ClientInterface + */ + private $guzzle; + + public function __construct(?ClientInterface $guzzle = null) + { + if (!$guzzle) { + $guzzle = self::buildClient(); + } + + $this->guzzle = $guzzle; + } + + /** + * Factory method to create the Guzzle 7 adapter with custom Guzzle configuration. + */ + public static function createWithConfig(array $config): Client + { + return new self(self::buildClient($config)); + } + + public function sendRequest(RequestInterface $request): ResponseInterface + { + return $this->sendAsyncRequest($request)->wait(); + } + + public function sendAsyncRequest(RequestInterface $request) + { + $promise = $this->guzzle->sendAsync($request); + + return new Promise($promise, $request); + } + + /** + * Build the Guzzle client instance. + */ + private static function buildClient(array $config = []): GuzzleClient + { + $handlerStack = new HandlerStack(Utils::chooseHandler()); + $handlerStack->push(Middleware::prepareBody(), 'prepare_body'); + $config = array_merge(['handler' => $handlerStack], $config); + + return new GuzzleClient($config); + } +} diff --git a/3rdparty/php-http/guzzle7-adapter/src/Exception/UnexpectedValueException.php b/3rdparty/php-http/guzzle7-adapter/src/Exception/UnexpectedValueException.php new file mode 100644 index 00000000..f4731be0 --- /dev/null +++ b/3rdparty/php-http/guzzle7-adapter/src/Exception/UnexpectedValueException.php @@ -0,0 +1,9 @@ + + */ +final class Promise implements HttpPromise +{ + /** + * @var PromiseInterface + */ + private $promise; + + /** + * @var string State of the promise + */ + private $state; + + /** + * @var ResponseInterface + */ + private $response; + + /** + * @var HttplugException + */ + private $exception; + + /** + * @var RequestInterface + */ + private $request; + + public function __construct(PromiseInterface $promise, RequestInterface $request) + { + $this->request = $request; + $this->state = self::PENDING; + $this->promise = $promise->then(function ($response) { + $this->response = $response; + $this->state = self::FULFILLED; + + return $response; + }, function ($reason) { + $this->state = self::REJECTED; + + if ($reason instanceof HttplugException) { + $this->exception = $reason; + } elseif ($reason instanceof GuzzleExceptions\GuzzleException) { + $this->exception = $this->handleException($reason); + } elseif ($reason instanceof \Throwable) { + $this->exception = new HttplugException\TransferException('Invalid exception returned from Guzzle7', 0, $reason); + } else { + $this->exception = new UnexpectedValueException('Reason returned from Guzzle7 must be an Exception'); + } + + throw $this->exception; + }); + } + + public function then(?callable $onFulfilled = null, ?callable $onRejected = null) + { + return new static($this->promise->then($onFulfilled, $onRejected), $this->request); + } + + public function getState() + { + return $this->state; + } + + public function wait($unwrap = true) + { + $this->promise->wait(false); + + if ($unwrap) { + if (self::REJECTED === $this->getState()) { + throw $this->exception; + } + + return $this->response; + } + } + + /** + * Converts a Guzzle exception into an Httplug exception. + * + * @return HttplugException + */ + private function handleException(GuzzleExceptions\GuzzleException $exception) + { + if ($exception instanceof GuzzleExceptions\ConnectException) { + return new HttplugException\NetworkException($exception->getMessage(), $exception->getRequest(), $exception); + } + + if ($exception instanceof GuzzleExceptions\RequestException) { + // Make sure we have a response for the HttpException + if ($exception->hasResponse()) { + return new HttplugException\HttpException( + $exception->getMessage(), + $exception->getRequest(), + $exception->getResponse(), + $exception + ); + } + + return new HttplugException\RequestException($exception->getMessage(), $exception->getRequest(), $exception); + } + + return new HttplugException\TransferException($exception->getMessage(), 0, $exception); + } +} diff --git a/3rdparty/php-http/httplug/LICENSE b/3rdparty/php-http/httplug/LICENSE new file mode 100644 index 00000000..8cd264c6 --- /dev/null +++ b/3rdparty/php-http/httplug/LICENSE @@ -0,0 +1,20 @@ +Copyright (c) 2014 Eric GELOEN +Copyright (c) 2015 PHP HTTP Team + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is furnished +to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +THE SOFTWARE. diff --git a/3rdparty/php-http/httplug/puli.json b/3rdparty/php-http/httplug/puli.json new file mode 100644 index 00000000..41683315 --- /dev/null +++ b/3rdparty/php-http/httplug/puli.json @@ -0,0 +1,12 @@ +{ + "version": "1.0", + "name": "php-http/httplug", + "binding-types": { + "Http\\Client\\HttpAsyncClient": { + "description": "Async HTTP Client" + }, + "Http\\Client\\HttpClient": { + "description": "HTTP Client" + } + } +} diff --git a/3rdparty/php-http/httplug/src/Exception.php b/3rdparty/php-http/httplug/src/Exception.php new file mode 100644 index 00000000..4df164c9 --- /dev/null +++ b/3rdparty/php-http/httplug/src/Exception.php @@ -0,0 +1,14 @@ + + */ +interface Exception extends PsrClientException +{ +} diff --git a/3rdparty/php-http/httplug/src/Exception/HttpException.php b/3rdparty/php-http/httplug/src/Exception/HttpException.php new file mode 100644 index 00000000..8af32f12 --- /dev/null +++ b/3rdparty/php-http/httplug/src/Exception/HttpException.php @@ -0,0 +1,65 @@ + + */ +class HttpException extends RequestException +{ + /** + * @var ResponseInterface + */ + protected $response; + + /** + * @param string $message + */ + public function __construct( + $message, + RequestInterface $request, + ResponseInterface $response, + ?\Exception $previous = null + ) { + parent::__construct($message, $request, $previous); + + $this->response = $response; + $this->code = $response->getStatusCode(); + } + + /** + * Returns the response. + * + * @return ResponseInterface + */ + public function getResponse() + { + return $this->response; + } + + /** + * Factory method to create a new exception with a normalized error message. + */ + public static function create( + RequestInterface $request, + ResponseInterface $response, + ?\Exception $previous = null + ) { + $message = sprintf( + '[url] %s [http method] %s [status code] %s [reason phrase] %s', + $request->getRequestTarget(), + $request->getMethod(), + $response->getStatusCode(), + $response->getReasonPhrase() + ); + + return new static($message, $request, $response, $previous); + } +} diff --git a/3rdparty/php-http/httplug/src/Exception/NetworkException.php b/3rdparty/php-http/httplug/src/Exception/NetworkException.php new file mode 100644 index 00000000..ce5f4d7a --- /dev/null +++ b/3rdparty/php-http/httplug/src/Exception/NetworkException.php @@ -0,0 +1,28 @@ + + */ +class NetworkException extends TransferException implements PsrNetworkException +{ + use RequestAwareTrait; + + /** + * @param string $message + */ + public function __construct($message, RequestInterface $request, ?\Exception $previous = null) + { + $this->setRequest($request); + + parent::__construct($message, 0, $previous); + } +} diff --git a/3rdparty/php-http/httplug/src/Exception/RequestAwareTrait.php b/3rdparty/php-http/httplug/src/Exception/RequestAwareTrait.php new file mode 100644 index 00000000..f507982a --- /dev/null +++ b/3rdparty/php-http/httplug/src/Exception/RequestAwareTrait.php @@ -0,0 +1,23 @@ +request = $request; + } + + public function getRequest(): RequestInterface + { + return $this->request; + } +} diff --git a/3rdparty/php-http/httplug/src/Exception/RequestException.php b/3rdparty/php-http/httplug/src/Exception/RequestException.php new file mode 100644 index 00000000..dbed296a --- /dev/null +++ b/3rdparty/php-http/httplug/src/Exception/RequestException.php @@ -0,0 +1,29 @@ + + */ +class RequestException extends TransferException implements PsrRequestException +{ + use RequestAwareTrait; + + /** + * @param string $message + */ + public function __construct($message, RequestInterface $request, ?\Exception $previous = null) + { + $this->setRequest($request); + + parent::__construct($message, 0, $previous); + } +} diff --git a/3rdparty/php-http/httplug/src/Exception/TransferException.php b/3rdparty/php-http/httplug/src/Exception/TransferException.php new file mode 100644 index 00000000..a858cf5e --- /dev/null +++ b/3rdparty/php-http/httplug/src/Exception/TransferException.php @@ -0,0 +1,14 @@ + + */ +class TransferException extends \RuntimeException implements Exception +{ +} diff --git a/3rdparty/php-http/httplug/src/HttpAsyncClient.php b/3rdparty/php-http/httplug/src/HttpAsyncClient.php new file mode 100644 index 00000000..c3b9d61a --- /dev/null +++ b/3rdparty/php-http/httplug/src/HttpAsyncClient.php @@ -0,0 +1,25 @@ + + */ +interface HttpAsyncClient +{ + /** + * Sends a PSR-7 request in an asynchronous way. + * + * Exceptions related to processing the request are available from the returned Promise. + * + * @return Promise resolves a PSR-7 Response or fails with an Http\Client\Exception + * + * @throws \Exception If processing the request is impossible (eg. bad configuration). + */ + public function sendAsyncRequest(RequestInterface $request); +} diff --git a/3rdparty/php-http/httplug/src/HttpClient.php b/3rdparty/php-http/httplug/src/HttpClient.php new file mode 100644 index 00000000..22b94aaf --- /dev/null +++ b/3rdparty/php-http/httplug/src/HttpClient.php @@ -0,0 +1,17 @@ +response = $response; + } + + public function then(?callable $onFulfilled = null, ?callable $onRejected = null) + { + if (null === $onFulfilled) { + return $this; + } + + try { + return new self($onFulfilled($this->response)); + } catch (Exception $e) { + return new HttpRejectedPromise($e); + } + } + + public function getState() + { + return Promise::FULFILLED; + } + + public function wait($unwrap = true) + { + if ($unwrap) { + return $this->response; + } + } +} diff --git a/3rdparty/php-http/httplug/src/Promise/HttpRejectedPromise.php b/3rdparty/php-http/httplug/src/Promise/HttpRejectedPromise.php new file mode 100644 index 00000000..a489ad4f --- /dev/null +++ b/3rdparty/php-http/httplug/src/Promise/HttpRejectedPromise.php @@ -0,0 +1,49 @@ +exception = $exception; + } + + public function then(?callable $onFulfilled = null, ?callable $onRejected = null) + { + if (null === $onRejected) { + return $this; + } + + try { + $result = $onRejected($this->exception); + if ($result instanceof Promise) { + return $result; + } + + return new HttpFulfilledPromise($result); + } catch (Exception $e) { + return new self($e); + } + } + + public function getState() + { + return Promise::REJECTED; + } + + public function wait($unwrap = true) + { + if ($unwrap) { + throw $this->exception; + } + } +} diff --git a/3rdparty/php-http/promise/LICENSE b/3rdparty/php-http/promise/LICENSE new file mode 100644 index 00000000..4558d6f0 --- /dev/null +++ b/3rdparty/php-http/promise/LICENSE @@ -0,0 +1,19 @@ +Copyright (c) 2015-2016 PHP HTTP Team + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is furnished +to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +THE SOFTWARE. diff --git a/3rdparty/php-http/promise/src/FulfilledPromise.php b/3rdparty/php-http/promise/src/FulfilledPromise.php new file mode 100644 index 00000000..ed77d469 --- /dev/null +++ b/3rdparty/php-http/promise/src/FulfilledPromise.php @@ -0,0 +1,51 @@ + + */ +final class FulfilledPromise implements Promise +{ + /** + * @var mixed + */ + private $result; + + /** + * @param mixed $result + */ + public function __construct($result) + { + $this->result = $result; + } + + public function then(?callable $onFulfilled = null, ?callable $onRejected = null) + { + if (null === $onFulfilled) { + return $this; + } + + try { + return new self($onFulfilled($this->result)); + } catch (\Exception $e) { + return new RejectedPromise($e); + } + } + + public function getState() + { + return Promise::FULFILLED; + } + + public function wait($unwrap = true) + { + if ($unwrap) { + return $this->result; + } + + return null; + } +} diff --git a/3rdparty/php-http/promise/src/Promise.php b/3rdparty/php-http/promise/src/Promise.php new file mode 100644 index 00000000..e2403114 --- /dev/null +++ b/3rdparty/php-http/promise/src/Promise.php @@ -0,0 +1,69 @@ + + * @author Márk Sági-Kazár + */ +interface Promise +{ + /** + * Promise has not been fulfilled or rejected. + */ + const PENDING = 'pending'; + + /** + * Promise has been fulfilled. + */ + const FULFILLED = 'fulfilled'; + + /** + * Promise has been rejected. + */ + const REJECTED = 'rejected'; + + /** + * Adds behavior for when the promise is resolved or rejected (response will be available, or error happens). + * + * If you do not care about one of the cases, you can set the corresponding callable to null + * The callback will be called when the value arrived and never more than once. + * + * @param callable|null $onFulfilled called when a response will be available + * @param callable|null $onRejected called when an exception occurs + * + * @return Promise a new resolved promise with value of the executed callback (onFulfilled / onRejected) + */ + public function then(?callable $onFulfilled = null, ?callable $onRejected = null); + + /** + * Returns the state of the promise, one of PENDING, FULFILLED or REJECTED. + * + * @return string + */ + public function getState(); + + /** + * Wait for the promise to be fulfilled or rejected. + * + * When this method returns, the request has been resolved and if callables have been + * specified, the appropriate one has terminated. + * + * When $unwrap is true (the default), the response is returned, or the exception thrown + * on failure. Otherwise, nothing is returned or thrown. + * + * @param bool $unwrap Whether to return resolved value / throw reason or not + * + * @return ($unwrap is true ? mixed : null) Resolved value, null if $unwrap is set to false + * + * @throws \Throwable the rejection reason if $unwrap is set to true and the request failed + */ + public function wait($unwrap = true); +} diff --git a/3rdparty/php-http/promise/src/RejectedPromise.php b/3rdparty/php-http/promise/src/RejectedPromise.php new file mode 100644 index 00000000..8c046ce2 --- /dev/null +++ b/3rdparty/php-http/promise/src/RejectedPromise.php @@ -0,0 +1,48 @@ + + */ +final class RejectedPromise implements Promise +{ + /** + * @var \Throwable + */ + private $exception; + + public function __construct(\Throwable $exception) + { + $this->exception = $exception; + } + + public function then(?callable $onFulfilled = null, ?callable $onRejected = null) + { + if (null === $onRejected) { + return $this; + } + + try { + return new FulfilledPromise($onRejected($this->exception)); + } catch (\Exception $e) { + return new self($e); + } + } + + public function getState() + { + return Promise::REJECTED; + } + + public function wait($unwrap = true) + { + if ($unwrap) { + throw $this->exception; + } + + return null; + } +} diff --git a/3rdparty/php-opencloud/openstack/LICENSE b/3rdparty/php-opencloud/openstack/LICENSE new file mode 100644 index 00000000..f8ff464b --- /dev/null +++ b/3rdparty/php-opencloud/openstack/LICENSE @@ -0,0 +1,201 @@ + Apache License + Version 2.0, January 2004 + http://www.apache.org/licenses/ + + TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION + + 1. Definitions. + + "License" shall mean the terms and conditions for use, reproduction, + and distribution as defined by Sections 1 through 9 of this document. + + "Licensor" shall mean the copyright owner or entity authorized by + the copyright owner that is granting the License. + + "Legal Entity" shall mean the union of the acting entity and all + other entities that control, are controlled by, or are under common + control with that entity. For the purposes of this definition, + "control" means (i) the power, direct or indirect, to cause the + direction or management of such entity, whether by contract or + otherwise, or (ii) ownership of fifty percent (50%) or more of the + outstanding shares, or (iii) beneficial ownership of such entity. + + "You" (or "Your") shall mean an individual or Legal Entity + exercising permissions granted by this License. + + "Source" form shall mean the preferred form for making modifications, + including but not limited to software source code, documentation + source, and configuration files. + + "Object" form shall mean any form resulting from mechanical + transformation or translation of a Source form, including but + not limited to compiled object code, generated documentation, + and conversions to other media types. + + "Work" shall mean the work of authorship, whether in Source or + Object form, made available under the License, as indicated by a + copyright notice that is included in or attached to the work + (an example is provided in the Appendix below). + + "Derivative Works" shall mean any work, whether in Source or Object + form, that is based on (or derived from) the Work and for which the + editorial revisions, annotations, elaborations, or other modifications + represent, as a whole, an original work of authorship. For the purposes + of this License, Derivative Works shall not include works that remain + separable from, or merely link (or bind by name) to the interfaces of, + the Work and Derivative Works thereof. + + "Contribution" shall mean any work of authorship, including + the original version of the Work and any modifications or additions + to that Work or Derivative Works thereof, that is intentionally + submitted to Licensor for inclusion in the Work by the copyright owner + or by an individual or Legal Entity authorized to submit on behalf of + the copyright owner. For the purposes of this definition, "submitted" + means any form of electronic, verbal, or written communication sent + to the Licensor or its representatives, including but not limited to + communication on electronic mailing lists, source code control systems, + and issue tracking systems that are managed by, or on behalf of, the + Licensor for the purpose of discussing and improving the Work, but + excluding communication that is conspicuously marked or otherwise + designated in writing by the copyright owner as "Not a Contribution." + + "Contributor" shall mean Licensor and any individual or Legal Entity + on behalf of whom a Contribution has been received by Licensor and + subsequently incorporated within the Work. + + 2. Grant of Copyright License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + copyright license to reproduce, prepare Derivative Works of, + publicly display, publicly perform, sublicense, and distribute the + Work and such Derivative Works in Source or Object form. + + 3. Grant of Patent License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + (except as stated in this section) patent license to make, have made, + use, offer to sell, sell, import, and otherwise transfer the Work, + where such license applies only to those patent claims licensable + by such Contributor that are necessarily infringed by their + Contribution(s) alone or by combination of their Contribution(s) + with the Work to which such Contribution(s) was submitted. If You + institute patent litigation against any entity (including a + cross-claim or counterclaim in a lawsuit) alleging that the Work + or a Contribution incorporated within the Work constitutes direct + or contributory patent infringement, then any patent licenses + granted to You under this License for that Work shall terminate + as of the date such litigation is filed. + + 4. Redistribution. You may reproduce and distribute copies of the + Work or Derivative Works thereof in any medium, with or without + modifications, and in Source or Object form, provided that You + meet the following conditions: + + (a) You must give any other recipients of the Work or + Derivative Works a copy of this License; and + + (b) You must cause any modified files to carry prominent notices + stating that You changed the files; and + + (c) You must retain, in the Source form of any Derivative Works + that You distribute, all copyright, patent, trademark, and + attribution notices from the Source form of the Work, + excluding those notices that do not pertain to any part of + the Derivative Works; and + + (d) If the Work includes a "NOTICE" text file as part of its + distribution, then any Derivative Works that You distribute must + include a readable copy of the attribution notices contained + within such NOTICE file, excluding those notices that do not + pertain to any part of the Derivative Works, in at least one + of the following places: within a NOTICE text file distributed + as part of the Derivative Works; within the Source form or + documentation, if provided along with the Derivative Works; or, + within a display generated by the Derivative Works, if and + wherever such third-party notices normally appear. The contents + of the NOTICE file are for informational purposes only and + do not modify the License. You may add Your own attribution + notices within Derivative Works that You distribute, alongside + or as an addendum to the NOTICE text from the Work, provided + that such additional attribution notices cannot be construed + as modifying the License. + + You may add Your own copyright statement to Your modifications and + may provide additional or different license terms and conditions + for use, reproduction, or distribution of Your modifications, or + for any such Derivative Works as a whole, provided Your use, + reproduction, and distribution of the Work otherwise complies with + the conditions stated in this License. + + 5. Submission of Contributions. Unless You explicitly state otherwise, + any Contribution intentionally submitted for inclusion in the Work + by You to the Licensor shall be under the terms and conditions of + this License, without any additional terms or conditions. + Notwithstanding the above, nothing herein shall supersede or modify + the terms of any separate license agreement you may have executed + with Licensor regarding such Contributions. + + 6. Trademarks. This License does not grant permission to use the trade + names, trademarks, service marks, or product names of the Licensor, + except as required for reasonable and customary use in describing the + origin of the Work and reproducing the content of the NOTICE file. + + 7. Disclaimer of Warranty. Unless required by applicable law or + agreed to in writing, Licensor provides the Work (and each + Contributor provides its Contributions) on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or + implied, including, without limitation, any warranties or conditions + of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A + PARTICULAR PURPOSE. You are solely responsible for determining the + appropriateness of using or redistributing the Work and assume any + risks associated with Your exercise of permissions under this License. + + 8. Limitation of Liability. In no event and under no legal theory, + whether in tort (including negligence), contract, or otherwise, + unless required by applicable law (such as deliberate and grossly + negligent acts) or agreed to in writing, shall any Contributor be + liable to You for damages, including any direct, indirect, special, + incidental, or consequential damages of any character arising as a + result of this License or out of the use or inability to use the + Work (including but not limited to damages for loss of goodwill, + work stoppage, computer failure or malfunction, or any and all + other commercial damages or losses), even if such Contributor + has been advised of the possibility of such damages. + + 9. Accepting Warranty or Additional Liability. While redistributing + the Work or Derivative Works thereof, You may choose to offer, + and charge a fee for, acceptance of support, warranty, indemnity, + or other liability obligations and/or rights consistent with this + License. However, in accepting such obligations, You may act only + on Your own behalf and on Your sole responsibility, not on behalf + of any other Contributor, and only if You agree to indemnify, + defend, and hold each Contributor harmless for any liability + incurred by, or claims asserted against, such Contributor by reason + of your accepting any such warranty or additional liability. + + END OF TERMS AND CONDITIONS + + APPENDIX: How to apply the Apache License to your work. + + To apply the Apache License to your work, attach the following + boilerplate notice, with the fields enclosed by brackets "[]" + replaced with your own identifying information. (Don't include + the brackets!) The text should be enclosed in the appropriate + comment syntax for the file format. We also recommend that a + file or class name and description of purpose be included on the + same "printed page" as the copyright notice for easier + identification within third-party archives. + + Copyright 2016-2023 PHP OpenCloud Project + + Licensed under the Apache License, Version 2.0 (the "License"); + you may not use this file except in compliance with the License. + You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + + Unless required by applicable law or agreed to in writing, software + distributed under the License is distributed on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + See the License for the specific language governing permissions and + limitations under the License. \ No newline at end of file diff --git a/3rdparty/php-opencloud/openstack/src/BlockStorage/v2/Api.php b/3rdparty/php-opencloud/openstack/src/BlockStorage/v2/Api.php new file mode 100644 index 00000000..55d0ae0c --- /dev/null +++ b/3rdparty/php-opencloud/openstack/src/BlockStorage/v2/Api.php @@ -0,0 +1,349 @@ +params = new Params(); + } + + public function postVolumes(): array + { + return [ + 'method' => 'POST', + 'path' => 'volumes', + 'jsonKey' => 'volume', + 'params' => [ + 'availabilityZone' => $this->params->availabilityZone(), + 'sourceVolumeId' => $this->params->sourceVolId(), + 'description' => $this->params->desc(), + 'snapshotId' => $this->params->snapshotId(), + 'size' => $this->params->size(), + 'name' => $this->params->name('volume'), + 'imageId' => $this->params->imageRef(), + 'volumeType' => $this->params->volumeType(), + 'metadata' => $this->params->metadata(), + 'projectId' => $this->params->projectId(), + ], + ]; + } + + public function getVolumes(): array + { + return [ + 'method' => 'GET', + 'path' => 'volumes', + 'params' => [ + 'limit' => $this->params->limit(), + 'marker' => $this->params->marker(), + 'sort' => $this->params->sort(), + 'allTenants' => $this->params->allTenants(), + ], + ]; + } + + public function getVolumesDetail(): array + { + return [ + 'method' => 'GET', + 'path' => 'volumes/detail', + 'params' => [ + 'limit' => $this->params->limit(), + 'marker' => $this->params->marker(), + 'sort' => $this->params->sort(), + 'allTenants' => $this->params->allTenants(), + ], + ]; + } + + public function getVolume(): array + { + return [ + 'method' => 'GET', + 'path' => 'volumes/{id}', + 'params' => [ + 'id' => $this->params->idPath(), + ], + ]; + } + + public function putVolume(): array + { + return [ + 'method' => 'PUT', + 'path' => 'volumes/{id}', + 'jsonKey' => 'volume', + 'params' => [ + 'id' => $this->params->idPath(), + 'name' => $this->params->name('volume'), + 'description' => $this->params->desc(), + ], + ]; + } + + public function deleteVolume(): array + { + return [ + 'method' => 'DELETE', + 'path' => 'volumes/{id}', + 'params' => ['id' => $this->params->idPath()], + ]; + } + + public function getVolumeMetadata(): array + { + return [ + 'method' => 'GET', + 'path' => 'volumes/{id}/metadata', + 'params' => ['id' => $this->params->idPath()], + ]; + } + + public function putVolumeMetadata(): array + { + return [ + 'method' => 'PUT', + 'path' => 'volumes/{id}/metadata', + 'params' => [ + 'id' => $this->params->idPath(), + 'metadata' => $this->params->metadata(), + ], + ]; + } + + public function getTypes(): array + { + return [ + 'method' => 'GET', + 'path' => 'types', + 'params' => [], + ]; + } + + public function postTypes(): array + { + return [ + 'method' => 'POST', + 'path' => 'types', + 'jsonKey' => 'volume_type', + 'params' => [ + 'name' => $this->params->name('volume type'), + 'specs' => $this->params->typeSpecs(), + ], + ]; + } + + public function putType(): array + { + return [ + 'method' => 'PUT', + 'path' => 'types/{id}', + 'jsonKey' => 'volume_type', + 'params' => [ + 'id' => $this->params->idPath(), + 'name' => $this->params->name('volume type'), + 'specs' => $this->params->typeSpecs(), + ], + ]; + } + + public function getType(): array + { + return [ + 'method' => 'GET', + 'path' => 'types/{id}', + 'params' => ['id' => $this->params->idPath()], + ]; + } + + public function deleteType(): array + { + return [ + 'method' => 'DELETE', + 'path' => 'types/{id}', + 'params' => ['id' => $this->params->idPath()], + ]; + } + + public function postSnapshots(): array + { + return [ + 'method' => 'POST', + 'path' => 'snapshots', + 'jsonKey' => 'snapshot', + 'params' => [ + 'volumeId' => $this->params->volId(), + 'force' => $this->params->force(), + 'name' => $this->params->snapshotName(), + 'description' => $this->params->desc(), + ], + ]; + } + + public function getSnapshots(): array + { + return [ + 'method' => 'GET', + 'path' => 'snapshots', + 'params' => [ + 'marker' => $this->params->marker(), + 'limit' => $this->params->limit(), + 'sortDir' => $this->params->sortDir(), + 'sortKey' => $this->params->sortKey(), + 'allTenants' => $this->params->allTenants(), + ], + ]; + } + + public function getSnapshotsDetail(): array + { + $api = $this->getSnapshots(); + $api['path'] .= '/detail'; + + return $api; + } + + public function getSnapshot(): array + { + return [ + 'method' => 'GET', + 'path' => 'snapshots/{id}', + 'params' => ['id' => $this->params->idPath()], + ]; + } + + public function putSnapshot(): array + { + return [ + 'method' => 'PUT', + 'path' => 'snapshots/{id}', + 'jsonKey' => 'snapshot', + 'params' => [ + 'id' => $this->params->idPath(), + 'name' => $this->params->snapshotName(), + 'description' => $this->params->desc(), + ], + ]; + } + + public function deleteSnapshot(): array + { + return [ + 'method' => 'DELETE', + 'path' => 'snapshots/{id}', + 'params' => ['id' => $this->params->idPath()], + ]; + } + + public function getSnapshotMetadata(): array + { + return [ + 'method' => 'GET', + 'path' => 'snapshots/{id}/metadata', + 'params' => ['id' => $this->params->idPath()], + ]; + } + + public function putSnapshotMetadata(): array + { + return [ + 'method' => 'PUT', + 'path' => 'snapshots/{id}/metadata', + 'params' => [ + 'id' => $this->params->idPath(), + 'metadata' => $this->params->metadata(), + ], + ]; + } + + public function getQuotaSet(): array + { + return [ + 'method' => 'GET', + 'path' => 'os-quota-sets/{tenantId}', + 'params' => [ + 'tenantId' => $this->params->idPath('quota-sets'), + ], + ]; + } + + public function deleteQuotaSet(): array + { + return [ + 'method' => 'DELETE', + 'path' => 'os-quota-sets/{tenantId}', + 'jsonKey' => 'quota_set', + 'params' => [ + 'tenantId' => $this->params->idPath('quota-sets'), + ], + ]; + } + + public function putQuotaSet(): array + { + return [ + 'method' => 'PUT', + 'path' => 'os-quota-sets/{tenantId}', + 'jsonKey' => 'quota_set', + 'params' => [ + 'tenantId' => $this->params->idPath(), + 'backupGigabytes' => $this->params->quotaSetBackupGigabytes(), + 'backups' => $this->params->quotaSetBackups(), + 'gigabytes' => $this->params->quotaSetGigabytes(), + 'gigabytesIscsi' => $this->params->quotaSetGigabytesIscsi(), + 'perVolumeGigabytes' => $this->params->quotaSetPerVolumeGigabytes(), + 'snapshots' => $this->params->quotaSetSnapshots(), + 'snapshotsIscsi' => $this->params->quotaSetSnapshotsIscsi(), + 'volumes' => $this->params->quotaSetVolumes(), + 'volumesIscsi' => $this->params->quotaSetVolumesIscsi(), + ], + ]; + } + + public function postVolumeBootable(): array + { + return [ + 'method' => 'POST', + 'path' => 'volumes/{id}/action', + 'jsonKey' => 'os-set_bootable', + 'params' => [ + 'id' => $this->params->idPath(), + 'bootable' => $this->params->bootable(), + ], + ]; + } + + public function postImageMetadata(): array + { + return [ + 'method' => 'POST', + 'path' => 'volumes/{id}/action', + 'jsonKey' => 'os-set_image_metadata', + 'params' => [ + 'id' => $this->params->idPath(), + 'metadata' => $this->params->metadata(), + ], + ]; + } + + public function postResetStatus(): array + { + return [ + 'method' => 'POST', + 'path' => 'volumes/{id}/action', + 'jsonKey' => 'os-reset_status', + 'params' => [ + 'id' => $this->params->idPath(), + 'status' => $this->params->volumeStatus(), + 'migrationStatus' => $this->params->volumeMigrationStatus(), + 'attachStatus' => $this->params->volumeAttachStatus(), + ], + ]; + } +} diff --git a/3rdparty/php-opencloud/openstack/src/BlockStorage/v2/Models/QuotaSet.php b/3rdparty/php-opencloud/openstack/src/BlockStorage/v2/Models/QuotaSet.php new file mode 100644 index 00000000..c603bc28 --- /dev/null +++ b/3rdparty/php-opencloud/openstack/src/BlockStorage/v2/Models/QuotaSet.php @@ -0,0 +1,78 @@ + 'backupGigabytes', + 'gigabytes' => 'gigabytes', + 'gigabytes_iscsi' => 'gigabytesIscsi', + 'per_volume_gigabytes' => 'perVolumeGigabytes', + 'snapshots_iscsi' => 'snapshotsIscsi', + 'volumes_iscsi' => 'volumesIscsi', + 'id' => 'tenantId', + ]; + + protected $resourceKey = 'quota_set'; + + public function retrieve() + { + $response = $this->execute($this->api->getQuotaSet(), ['tenantId' => (string) $this->tenantId]); + $this->populateFromResponse($response); + } + + public function update() + { + $response = $this->executeWithState($this->api->putQuotaSet()); + $this->populateFromResponse($response); + } + + public function delete() + { + $response = $this->executeWithState($this->api->deleteQuotaSet()); + $this->populateFromResponse($response); + } +} diff --git a/3rdparty/php-opencloud/openstack/src/BlockStorage/v2/Models/Snapshot.php b/3rdparty/php-opencloud/openstack/src/BlockStorage/v2/Models/Snapshot.php new file mode 100644 index 00000000..f05a403b --- /dev/null +++ b/3rdparty/php-opencloud/openstack/src/BlockStorage/v2/Models/Snapshot.php @@ -0,0 +1,130 @@ + 'volumeId', + 'os-extended-snapshot-attributes:project_id' => 'projectId', + ]; + + protected function getAliases(): array + { + return parent::getAliases() + [ + 'created_at' => new Alias('createdAt', \DateTimeImmutable::class), + ]; + } + + public function populateFromResponse(ResponseInterface $response): self + { + parent::populateFromResponse($response); + $this->metadata = $this->parseMetadata($response); + + return $this; + } + + public function retrieve() + { + $response = $this->executeWithState($this->api->getSnapshot()); + $this->populateFromResponse($response); + } + + /** + * @param array $userOptions {@see \OpenStack\BlockStorage\v2\Api::postSnapshots} + */ + public function create(array $userOptions): Creatable + { + $response = $this->execute($this->api->postSnapshots(), $userOptions); + + return $this->populateFromResponse($response); + } + + public function update() + { + $this->executeWithState($this->api->putSnapshot()); + } + + public function delete() + { + $this->executeWithState($this->api->deleteSnapshot()); + } + + public function getMetadata(): array + { + $response = $this->executeWithState($this->api->getSnapshotMetadata()); + $this->metadata = $this->parseMetadata($response); + + return $this->metadata; + } + + public function mergeMetadata(array $metadata) + { + $this->getMetadata(); + $this->metadata = array_merge($this->metadata, $metadata); + $this->executeWithState($this->api->putSnapshotMetadata()); + } + + public function resetMetadata(array $metadata) + { + $this->metadata = $metadata; + $this->executeWithState($this->api->putSnapshotMetadata()); + } + + public function parseMetadata(ResponseInterface $response): array + { + $json = Utils::jsonDecode($response); + + return isset($json['metadata']) ? $json['metadata'] : []; + } +} diff --git a/3rdparty/php-opencloud/openstack/src/BlockStorage/v2/Models/Volume.php b/3rdparty/php-opencloud/openstack/src/BlockStorage/v2/Models/Volume.php new file mode 100644 index 00000000..1aec74ab --- /dev/null +++ b/3rdparty/php-opencloud/openstack/src/BlockStorage/v2/Models/Volume.php @@ -0,0 +1,184 @@ + 'availabilityZone', + 'source_volid' => 'sourceVolumeId', + 'snapshot_id' => 'snapshotId', + 'volume_type' => 'volumeTypeName', + 'os-vol-tenant-attr:tenant_id' => 'tenantId', + 'os-vol-host-attr:host' => 'host', + 'volume_image_metadata' => 'volumeImageMetadata', + ]; + + protected function getAliases(): array + { + return parent::getAliases() + [ + 'created_at' => new Alias('createdAt', \DateTimeImmutable::class), + ]; + } + + public function populateFromResponse(ResponseInterface $response): self + { + parent::populateFromResponse($response); + $this->metadata = $this->parseMetadata($response); + + return $this; + } + + public function retrieve() + { + $response = $this->executeWithState($this->api->getVolume()); + $this->populateFromResponse($response); + } + + /** + * @param array $userOptions {@see \OpenStack\BlockStorage\v2\Api::postVolumes} + */ + public function create(array $userOptions): Creatable + { + $response = $this->execute($this->api->postVolumes(), $userOptions); + + return $this->populateFromResponse($response); + } + + public function update() + { + $response = $this->executeWithState($this->api->putVolume()); + $this->populateFromResponse($response); + } + + public function delete() + { + $this->executeWithState($this->api->deleteVolume()); + } + + public function getMetadata(): array + { + $response = $this->executeWithState($this->api->getVolumeMetadata()); + $this->metadata = $this->parseMetadata($response); + + return $this->metadata; + } + + public function mergeMetadata(array $metadata) + { + $this->getMetadata(); + $this->metadata = array_merge($this->metadata, $metadata); + $this->executeWithState($this->api->putVolumeMetadata()); + } + + public function resetMetadata(array $metadata) + { + $this->metadata = $metadata; + $this->executeWithState($this->api->putVolumeMetadata()); + } + + public function parseMetadata(ResponseInterface $response): array + { + $json = Utils::jsonDecode($response); + + return isset($json['metadata']) ? $json['metadata'] : []; + } + + /** + * Update the bootable status for a volume, mark it as a bootable volume. + */ + public function setBootable(bool $bootable = true) + { + $this->execute($this->api->postVolumeBootable(), ['id' => $this->id, 'bootable' => $bootable]); + } + + /** + * Sets the image metadata for a volume. + */ + public function setImageMetadata(array $metadata) + { + $this->execute($this->api->postImageMetadata(), ['id' => $this->id, 'metadata' => $metadata]); + } + + /** + * Administrator only. Resets the status, attach status, and migration status for a volume. Specify the os-reset_status action in the request body. + * + * @see https://developer.openstack.org/api-ref/block-storage/v2/index.html#volume-actions-volumes-action + */ + public function resetStatus(array $options) + { + $options = array_merge($options, ['id' => $this->id]); + $this->execute($this->api->postResetStatus(), $options); + } +} diff --git a/3rdparty/php-opencloud/openstack/src/BlockStorage/v2/Models/VolumeAttachment.php b/3rdparty/php-opencloud/openstack/src/BlockStorage/v2/Models/VolumeAttachment.php new file mode 100644 index 00000000..48b6e374 --- /dev/null +++ b/3rdparty/php-opencloud/openstack/src/BlockStorage/v2/Models/VolumeAttachment.php @@ -0,0 +1,29 @@ +execute($this->api->postTypes(), $userOptions); + + return $this->populateFromResponse($response); + } + + public function retrieve() + { + $response = $this->executeWithState($this->api->getType()); + $this->populateFromResponse($response); + } + + public function update() + { + $this->executeWithState($this->api->putType()); + } + + public function delete() + { + $this->executeWithState($this->api->deleteType()); + } +} diff --git a/3rdparty/php-opencloud/openstack/src/BlockStorage/v2/Params.php b/3rdparty/php-opencloud/openstack/src/BlockStorage/v2/Params.php new file mode 100644 index 00000000..694864fe --- /dev/null +++ b/3rdparty/php-opencloud/openstack/src/BlockStorage/v2/Params.php @@ -0,0 +1,280 @@ + self::STRING_TYPE, + 'location' => self::JSON, + 'sentAs' => 'availability_zone', + 'description' => 'The availability zone where the entity will reside.', + ]; + } + + public function sourceVolId(): array + { + return [ + 'type' => self::STRING_TYPE, + 'location' => self::JSON, + 'sentAs' => 'source_volid', + 'description' => 'To create a volume from an existing volume, specify the ID of the existing volume. The '. + 'volume is created with the same size as the source volume.', + ]; + } + + public function desc(): array + { + return [ + 'type' => self::STRING_TYPE, + 'location' => self::JSON, + 'description' => 'A human-friendly description that describes the resource', + ]; + } + + public function snapshotId(): array + { + return [ + 'type' => self::STRING_TYPE, + 'location' => self::JSON, + 'sentAs' => 'snapshot_id', + 'description' => 'To create a volume from an existing snapshot, specify the ID of the existing volume '. + 'snapshot. The volume is created in same availability zone and with same size as the snapshot.', + ]; + } + + public function size(): array + { + return [ + 'type' => self::INT_TYPE, + 'location' => self::JSON, + 'required' => true, + 'description' => 'The size of the volume, in gibibytes (GiB).', + ]; + } + + public function imageRef(): array + { + return [ + 'type' => self::STRING_TYPE, + 'location' => self::JSON, + 'sentAs' => 'imageRef', + 'description' => 'The ID of the image from which you want to create the volume. Required to create a '. + 'bootable volume.', + ]; + } + + public function volumeType(): array + { + return [ + 'type' => self::STRING_TYPE, + 'location' => self::JSON, + 'sentAs' => 'volume_type', + 'description' => 'The associated volume type.', + ]; + } + + public function bootable(): array + { + return [ + 'type' => self::BOOL_TYPE, + 'location' => self::JSON, + 'description' => 'Enables or disables the bootable attribute. You can boot an instance from a bootable volume.', + ]; + } + + public function volumeStatus(): array + { + return [ + 'type' => self::STRING_TYPE, + 'location' => self::JSON, + 'required' => true, + 'description' => 'The volume status.', + ]; + } + + public function volumeMigrationStatus(): array + { + return [ + 'type' => self::STRING_TYPE, + 'location' => self::JSON, + 'required' => false, + 'description' => 'The volume migration status.', + 'sentAs' => 'migration_status', + ]; + } + + public function volumeAttachStatus(): array + { + return [ + 'type' => self::STRING_TYPE, + 'location' => self::JSON, + 'required' => false, + 'description' => 'The volume attach status.', + 'sentAs' => 'attach_status', + ]; + } + + public function metadata(): array + { + return [ + 'type' => self::OBJECT_TYPE, + 'location' => self::JSON, + 'description' => 'One or more metadata key and value pairs to associate with the volume.', + 'properties' => [ + 'type' => self::STRING_TYPE, + 'description' => << self::STRING_TYPE, + 'location' => self::QUERY, + 'description' => 'Comma-separated list of sort keys and optional sort directions in the form of '. + '[:]. A valid direction is asc (ascending) or desc (descending).', + ]; + } + + public function name(string $resource): array + { + return parent::name($resource) + [ + 'type' => self::STRING_TYPE, + 'location' => self::JSON, + ]; + } + + public function idPath(): array + { + return [ + 'type' => self::STRING_TYPE, + 'location' => self::URL, + 'description' => 'The UUID of the resource', + 'documented' => false, + ]; + } + + public function typeSpecs(): array + { + return [ + 'type' => self::OBJECT_TYPE, + 'location' => self::JSON, + 'description' => 'A key and value pair that contains additional specifications that are associated with '. + 'the volume type. Examples include capabilities, capacity, compression, and so on, depending on the '. + 'storage driver in use.', + ]; + } + + public function volId(): array + { + return [ + 'type' => self::STRING_TYPE, + 'location' => self::JSON, + 'required' => true, + 'sentAs' => 'volume_id', + 'description' => 'To create a snapshot from an existing volume, specify the ID of the existing volume.', + ]; + } + + public function force(): array + { + return [ + 'type' => self::BOOL_TYPE, + 'location' => self::JSON, + 'description' => 'Indicate whether to snapshot, even if the volume is attached. Default is false.', + ]; + } + + public function snapshotName(): array + { + return parent::name('snapshot') + [ + 'type' => self::STRING_TYPE, + 'location' => self::JSON, + ]; + } + + protected function quotaSetLimit($sentAs, $description): array + { + return [ + 'type' => self::INT_TYPE, + 'location' => self::JSON, + 'sentAs' => $sentAs, + 'description' => $description, + ]; + } + + public function quotaSetLimitInstances(): array + { + return $this->quotaSetLimit('instances', 'The number of allowed instances for each tenant.'); + } + + public function quotaSetBackupGigabytes(): array + { + return $this->quotaSetLimit('backup_gigabytes', 'Total size of back-up storage (GiB)'); + } + + public function quotaSetBackups(): array + { + return $this->quotaSetLimit('backups', 'The number of allowed back-ups'); + } + + public function quotaSetGigabytes(): array + { + return $this->quotaSetLimit('gigabytes', 'Total Size of Volumes and Snapshots (GiB)'); + } + + public function quotaSetGigabytesIscsi(): array + { + return $this->quotaSetLimit('gigabytes_iscsi', 'Total Size of Volumes and Snapshots iscsi (GiB)'); + } + + public function quotaSetTenantId(): array + { + return $this->quotaSetLimit('id', 'Tenant Id'); + } + + public function quotaSetPerVolumeGigabytes(): array + { + return $this->quotaSetLimit('per_volume_gigabytes', 'Allowed size per Volume (GiB)'); + } + + public function quotaSetSnapshots(): array + { + return $this->quotaSetLimit('snapshots', 'The number of allowed snapshots'); + } + + public function quotaSetSnapshotsIscsi(): array + { + return $this->quotaSetLimit('snapshots_iscsi', 'The number of allowed snapshots iscsi'); + } + + public function quotaSetVolumes(): array + { + return $this->quotaSetLimit('volumes', 'The number of allowed volumes'); + } + + public function quotaSetVolumesIscsi(): array + { + return $this->quotaSetLimit('volumes_iscsi', 'The number of allowed volumes iscsi'); + } + + public function projectId(): array + { + return [ + 'type' => self::STRING_TYPE, + 'location' => self::URL, + 'sentAs' => 'project_id', + 'description' => 'The UUID of the project in a multi-tenancy cloud.', + ]; + } +} diff --git a/3rdparty/php-opencloud/openstack/src/BlockStorage/v2/Service.php b/3rdparty/php-opencloud/openstack/src/BlockStorage/v2/Service.php new file mode 100644 index 00000000..b509a666 --- /dev/null +++ b/3rdparty/php-opencloud/openstack/src/BlockStorage/v2/Service.php @@ -0,0 +1,115 @@ +model(Volume::class)->create($userOptions); + } + + /** + * Lists all available volumes. + * + * @param bool $detail if set to TRUE, more information will be returned + * @param array $userOptions {@see Api::getVolumes} + * + * @return \Generator + */ + public function listVolumes(bool $detail = false, array $userOptions = []): \Generator + { + $def = (true === $detail) ? $this->api->getVolumesDetail() : $this->api->getVolumes(); + + return $this->model(Volume::class)->enumerate($def, $userOptions); + } + + /** + * @param string $volumeId the UUID of the volume being retrieved + */ + public function getVolume(string $volumeId): Volume + { + $volume = $this->model(Volume::class); + $volume->populateFromArray(['id' => $volumeId]); + + return $volume; + } + + /** + * @param array $userOptions {@see Api::postTypes} + */ + public function createVolumeType(array $userOptions): VolumeType + { + return $this->model(VolumeType::class)->create($userOptions); + } + + /** + * @return \Generator + */ + public function listVolumeTypes(): \Generator + { + return $this->model(VolumeType::class)->enumerate($this->api->getTypes(), []); + } + + public function getVolumeType(string $typeId): VolumeType + { + $type = $this->model(VolumeType::class); + $type->populateFromArray(['id' => $typeId]); + + return $type; + } + + /** + * @param array $userOptions {@see Api::postSnapshots} + */ + public function createSnapshot(array $userOptions): Snapshot + { + return $this->model(Snapshot::class)->create($userOptions); + } + + /** + * @return \Generator + */ + public function listSnapshots(bool $detail = false, array $userOptions = []): \Generator + { + $def = (true === $detail) ? $this->api->getSnapshotsDetail() : $this->api->getSnapshots(); + + return $this->model(Snapshot::class)->enumerate($def, $userOptions); + } + + public function getSnapshot(string $snapshotId): Snapshot + { + $snapshot = $this->model(Snapshot::class); + $snapshot->populateFromArray(['id' => $snapshotId]); + + return $snapshot; + } + + /** + * Shows A Quota for a tenant. + */ + public function getQuotaSet(string $tenantId): QuotaSet + { + $quotaSet = $this->model(QuotaSet::class); + $quotaSet->populateFromResponse($this->execute($this->api->getQuotaSet(), ['tenantId' => $tenantId])); + + return $quotaSet; + } +} diff --git a/3rdparty/php-opencloud/openstack/src/BlockStorage/v3/Api.php b/3rdparty/php-opencloud/openstack/src/BlockStorage/v3/Api.php new file mode 100644 index 00000000..c0ece274 --- /dev/null +++ b/3rdparty/php-opencloud/openstack/src/BlockStorage/v3/Api.php @@ -0,0 +1,7 @@ + true]); + } + + protected function notRequired(array $param): array + { + return array_merge($param, ['required' => false]); + } + + protected function query(array $param): array + { + return array_merge($param, ['location' => AbstractParams::QUERY]); + } + + protected function url(array $param): array + { + return array_merge($param, ['location' => AbstractParams::URL]); + } + + public function documented(array $param): array + { + return array_merge($param, ['required' => true]); + } +} diff --git a/3rdparty/php-opencloud/openstack/src/Common/Api/AbstractParams.php b/3rdparty/php-opencloud/openstack/src/Common/Api/AbstractParams.php new file mode 100644 index 00000000..a62317ee --- /dev/null +++ b/3rdparty/php-opencloud/openstack/src/Common/Api/AbstractParams.php @@ -0,0 +1,113 @@ + self::INT_TYPE, + 'location' => 'query', + 'description' => << 'string', + 'location' => 'query', + 'description' => << sprintf('The unique ID, or identifier, for the %s', $type), + 'type' => self::STRING_TYPE, + 'location' => self::JSON, + ]; + } + + public function idPath(): array + { + return [ + 'type' => self::STRING_TYPE, + 'location' => self::URL, + 'description' => 'The unique ID of the resource', + ]; + } + + public function name(string $resource): array + { + return [ + 'description' => sprintf('The name of the %s', $resource), + 'type' => self::STRING_TYPE, + 'location' => self::JSON, + ]; + } + + public function sortDir(): array + { + return [ + 'type' => self::STRING_TYPE, + 'location' => self::QUERY, + 'sentAs' => 'sort_dir', + 'description' => 'Sorts by one or more sets of attribute and sort direction combinations.', + 'enum' => ['asc', 'desc'], + ]; + } + + public function sortKey(): array + { + return [ + 'type' => self::STRING_TYPE, + 'location' => self::QUERY, + 'sentAs' => 'sort_key', + 'description' => 'Sorts by one or more sets of attribute and sort direction combinations.', + ]; + } + + public function allTenants(): array + { + return [ + 'type' => self::BOOL_TYPE, + 'location' => self::QUERY, + 'sentAs' => 'all_tenants', + 'description' => '(Admin only) Set this to true to pull volume information from all tenants.', + ]; + } +} diff --git a/3rdparty/php-opencloud/openstack/src/Common/Api/ApiInterface.php b/3rdparty/php-opencloud/openstack/src/Common/Api/ApiInterface.php new file mode 100644 index 00000000..86284b42 --- /dev/null +++ b/3rdparty/php-opencloud/openstack/src/Common/Api/ApiInterface.php @@ -0,0 +1,20 @@ +method = $definition['method']; + $this->path = $definition['path']; + + if (isset($definition['jsonKey'])) { + $this->jsonKey = $definition['jsonKey']; + } + + $this->params = self::toParamArray($definition['params']); + $this->skipAuth = $definition['skipAuth'] ?? false; + } + + public function getPath(): string + { + return $this->path; + } + + public function getMethod(): string + { + return $this->method; + } + + /** + * Indicates if operation must be run without authentication. This is useful for getting authentication tokens. + */ + public function getSkipAuth(): bool + { + return $this->skipAuth; + } + + /** + * Indicates whether this operation supports a parameter. + * + * @param string $key The name of a parameter + */ + public function hasParam(string $key): bool + { + return isset($this->params[$key]); + } + + /** + * @return Parameter + */ + public function getParam(string $name) + { + return isset($this->params[$name]) ? $this->params[$name] : null; + } + + public function getJsonKey(): string + { + return $this->jsonKey ?: ''; + } + + /** + * A convenience method that will take a generic array of data and convert it into an array of + * {@see Parameter} objects. + * + * @param array $data A generic data array + */ + public static function toParamArray(array $data): array + { + $params = []; + + foreach ($data as $name => $param) { + $params[$name] = new Parameter($param + ['name' => $name]); + } + + return $params; + } + + /** + * This method will validate all of the user-provided values and throw an exception if any + * failures are detected. This is useful for basic sanity-checking before a request is + * serialized and sent to the API. + * + * @param array $userValues The user-defined values + * + * @return bool TRUE if validation passes + * + * @throws \Exception If validate fails + */ + public function validate(array $userValues): bool + { + foreach ($this->params as $paramName => $param) { + if (array_key_exists($paramName, $userValues)) { + $param->validate($userValues[$paramName]); + } elseif ($param->isRequired()) { + throw new \Exception(sprintf('"%s" is a required option, but it was not provided', $paramName)); + } + } + + return true; + } +} diff --git a/3rdparty/php-opencloud/openstack/src/Common/Api/OperatorInterface.php b/3rdparty/php-opencloud/openstack/src/Common/Api/OperatorInterface.php new file mode 100644 index 00000000..56219f49 --- /dev/null +++ b/3rdparty/php-opencloud/openstack/src/Common/Api/OperatorInterface.php @@ -0,0 +1,56 @@ +client = $client; + $this->api = $api; + } + + /** + * Magic method for dictating how objects are rendered when var_dump is called. + * For the benefit of users, extremely verbose and heavy properties (such as HTTP clients) are + * removed to provide easier access to normal state, such as resource attributes. + * + * @codeCoverageIgnore + * + * @return array + */ + public function __debugInfo() + { + $excludedVars = ['client', 'errorBuilder', 'api']; + + $output = []; + + foreach (get_object_vars($this) as $key => $val) { + if (!in_array($key, $excludedVars)) { + $output[$key] = $val; + } + } + + return $output; + } + + /** + * Magic method which intercepts async calls, finds the sequential version, and wraps it in a + * {@see Promise} object. In order for this to happen, the called methods need to be in the + * following format: `createAsync`, where `create` is the sequential method being wrapped. + * + * @param string $methodName the name of the method being invoked + * @param array $args the arguments to be passed to the sequential method + * + * @return Promise + * + * @throws \RuntimeException If method does not exist + */ + public function __call($methodName, $args) + { + $e = function ($name) { + return new \RuntimeException(sprintf('%s::%s is not defined', get_class($this), $name)); + }; + + if ('Async' === substr($methodName, -5)) { + $realMethod = substr($methodName, 0, -5); + if (!method_exists($this, $realMethod)) { + throw $e($realMethod); + } + + $promise = new Promise( + function () use (&$promise, $realMethod, $args) { + $value = call_user_func_array([$this, $realMethod], $args); + $promise->resolve($value); + } + ); + + return $promise; + } + + throw $e($methodName); + } + + public function getOperation(array $definition): Operation + { + return new Operation($definition); + } + + protected function sendRequest(Operation $operation, array $userValues = [], bool $async = false) + { + $operation->validate($userValues); + + $options = (new RequestSerializer())->serializeOptions($operation, $userValues); + $method = $async ? 'requestAsync' : 'request'; + + $uri = Utils::uri_template($operation->getPath(), $userValues); + + if (isset($userValues['requestOptions'])) { + $options += $userValues['requestOptions']; + + // headers are always created in options, merge them + if (isset($userValues['requestOptions']['headers'])) { + $options['headers'] = array_merge($options['headers'], $userValues['requestOptions']['headers']); + } + } + + $options['openstack.skip_auth'] = $operation->getSkipAuth(); + + return $this->client->$method($operation->getMethod(), $uri, $options); + } + + public function execute(array $definition, array $userValues = []): ResponseInterface + { + return $this->sendRequest($this->getOperation($definition), $userValues); + } + + public function executeAsync(array $definition, array $userValues = []): PromiseInterface + { + return $this->sendRequest($this->getOperation($definition), $userValues, true); + } + + public function model(string $class, $data = null): ResourceInterface + { + $model = new $class($this->client, $this->api); + // @codeCoverageIgnoreStart + if (!$model instanceof ResourceInterface) { + throw new \RuntimeException(sprintf('%s does not implement %s', $class, ResourceInterface::class)); + } + // @codeCoverageIgnoreEnd + if ($data instanceof ResponseInterface) { + $model->populateFromResponse($data); + } elseif (is_array($data)) { + $model->populateFromArray($data); + } + + return $model; + } +} diff --git a/3rdparty/php-opencloud/openstack/src/Common/Api/Parameter.php b/3rdparty/php-opencloud/openstack/src/Common/Api/Parameter.php new file mode 100644 index 00000000..bfa8d2e0 --- /dev/null +++ b/3rdparty/php-opencloud/openstack/src/Common/Api/Parameter.php @@ -0,0 +1,373 @@ +hydrate($data); + + $this->required = (bool) $this->required; + + $this->stockLocation($data); + $this->stockItemSchema($data); + $this->stockProperties($data); + } + + private function stockLocation(array $data) + { + $this->location = isset($data['location']) ? $data['location'] : self::DEFAULT_LOCATION; + + if (!AbstractParams::isSupportedLocation($this->location)) { + throw new \RuntimeException(sprintf('%s is not a permitted location', $this->location)); + } + } + + private function stockItemSchema(array $data) + { + if (isset($data['items'])) { + $this->itemSchema = new Parameter($data['items']); + } + } + + private function stockProperties(array $data) + { + if (isset($data['properties'])) { + if ($this->name && false !== stripos($this->name, 'metadata')) { + $this->properties = new Parameter($data['properties']); + } else { + foreach ($data['properties'] as $name => $property) { + $this->properties[$name] = new Parameter($property + ['name' => $name]); + } + } + } + } + + /** + * Retrieve the name that will be used over the wire. + */ + public function getName(): string + { + return $this->sentAs ?: $this->name; + } + + /** + * Indicates whether the user must provide a value for this parameter. + */ + public function isRequired(): bool + { + return true === $this->required; + } + + /** + * Validates a given user value and checks whether it passes basic sanity checking, such as types. + * + * @param $userValues The value provided by the user + * + * @return bool TRUE if the validation passes + * + * @throws \Exception If validation fails + */ + public function validate($userValues): bool + { + $this->validateEnums($userValues); + $this->validateType($userValues); + + if ($this->isArray()) { + $this->validateArray($userValues); + } elseif ($this->isObject()) { + $this->validateObject($userValues); + } + + return true; + } + + private function validateEnums($userValues) + { + if (!empty($this->enum) && 'string' == $this->type && !in_array($userValues, $this->enum)) { + throw new \Exception(sprintf('The only permitted values are %s. You provided %s', implode(', ', $this->enum), print_r($userValues, true))); + } + } + + private function validateType($userValues) + { + if (!$this->hasCorrectType($userValues)) { + throw new \Exception(sprintf('The key provided "%s" has the wrong value type. You provided %s (%s) but was expecting %s', $this->name, print_r($userValues, true), gettype($userValues), $this->type)); + } + } + + private function validateArray($userValues) + { + foreach ($userValues as $userValue) { + $this->itemSchema->validate($userValue); + } + } + + private function validateObject($userValues) + { + foreach ($userValues as $key => $userValue) { + $property = $this->getNestedProperty($key); + $property->validate($userValue); + } + } + + /** + * Internal method which retrieves a nested property for object parameters. + * + * @param string $key The name of the child parameter + * + * @returns Parameter + * + * @throws \Exception + */ + private function getNestedProperty($key): Parameter + { + if ($this->name && false !== stripos($this->name, 'metadata') && $this->properties instanceof Parameter) { + return $this->properties; + } elseif (isset($this->properties[$key])) { + return $this->properties[$key]; + } else { + throw new \Exception(sprintf('The key provided "%s" is not defined', $key)); + } + } + + /** + * Internal method which indicates whether the user value is of the same type as the one expected + * by this parameter. + * + * @param $userValue The value being checked + */ + private function hasCorrectType($userValue): bool + { + // Helper fn to see whether an array is associative (i.e. a JSON object) + $isAssociative = function ($value) { + return is_array($value) && array_keys($value) !== range(0, count($value) - 1); + }; + + // For params defined as objects, we'll let the user get away with + // passing in an associative array - since it's effectively a hash + if ('object' == $this->type && $isAssociative($userValue)) { + return true; + } + + if (class_exists($this->type) || interface_exists($this->type)) { + return is_a($userValue, $this->type); + } + + if (!$this->type) { + return true; + } + + // allow string nulls + if ('string' == $this->type && null === $userValue) { + return true; + } + + return gettype($userValue) == $this->type; + } + + /** + * Indicates whether this parameter represents an array type. + */ + public function isArray(): bool + { + return 'array' == $this->type && $this->itemSchema instanceof Parameter; + } + + /** + * Indicates whether this parameter represents an object type. + */ + public function isObject(): bool + { + return 'object' == $this->type && !empty($this->properties); + } + + public function getLocation(): string + { + return $this->location; + } + + /** + * Verifies whether the given location matches the parameter's location. + */ + public function hasLocation($value): bool + { + return $this->location == $value; + } + + /** + * Retrieves the parameter's path. + * + * @return string|null + */ + public function getPath(): string + { + return $this->path; + } + + /** + * Retrieves the common schema that an array parameter applies to all its child elements. + * + * @return Parameter|null + */ + public function getItemSchema() + { + return $this->itemSchema; + } + + /** + * Sets the name of the parameter to a new value. + */ + public function setName(string $name) + { + $this->name = $name; + } + + /** + * Retrieves the child parameter for an object parameter. + * + * @param string $name The name of the child property + * + * @return Parameter|null + */ + public function getProperty(string $name) + { + if ($this->properties instanceof Parameter) { + $this->properties->setName($name); + + return $this->properties; + } + + return isset($this->properties[$name]) ? $this->properties[$name] : null; + } + + /** + * Retrieves the prefix for a parameter, if any. + * + * @return string|null + */ + public function getPrefix(): string + { + return $this->prefix; + } + + public function getPrefixedName(): string + { + return $this->prefix.$this->getName(); + } +} diff --git a/3rdparty/php-opencloud/openstack/src/Common/ArrayAccessTrait.php b/3rdparty/php-opencloud/openstack/src/Common/ArrayAccessTrait.php new file mode 100644 index 00000000..fa9e42f6 --- /dev/null +++ b/3rdparty/php-opencloud/openstack/src/Common/ArrayAccessTrait.php @@ -0,0 +1,58 @@ +internalState[] = $value; + } else { + $this->internalState[$offset] = $value; + } + } + + /** + * Checks whether an internal key exists. + */ + public function offsetExists(string $offset): bool + { + return isset($this->internalState[$offset]); + } + + /** + * Unsets an internal key. + */ + public function offsetUnset(string $offset) + { + unset($this->internalState[$offset]); + } + + /** + * Retrieves an internal key. + * + * @return mixed|null + */ + public function offsetGet(string $offset) + { + return $this->offsetExists($offset) ? $this->internalState[$offset] : null; + } +} diff --git a/3rdparty/php-opencloud/openstack/src/Common/Auth/AuthHandler.php b/3rdparty/php-opencloud/openstack/src/Common/Auth/AuthHandler.php new file mode 100644 index 00000000..4abf2b83 --- /dev/null +++ b/3rdparty/php-opencloud/openstack/src/Common/Auth/AuthHandler.php @@ -0,0 +1,72 @@ +nextHandler = $nextHandler; + $this->tokenGenerator = $tokenGenerator; + $this->token = $token; + } + + /** + * This method is invoked before every HTTP request is sent to the API. When this happens, it + * checks to see whether a token is set and valid, and then sets the ``X-Auth-Token`` header + * for the HTTP request before letting it continue on its merry way. + * + * @return mixed|void + */ + public function __invoke(RequestInterface $request, array $options) + { + $fn = $this->nextHandler; + + if (!isset($options['openstack.skip_auth'])) { + // Deprecated. Left for backward compatibility only. + if ($this->shouldIgnore($request)) { + return $fn($request, $options); + } + } elseif ($options['openstack.skip_auth']) { + return $fn($request, $options); + } + + if (!$this->token || $this->token->hasExpired()) { + $this->token = call_user_func($this->tokenGenerator); + } + + $modify = ['set_headers' => ['X-Auth-Token' => $this->token->getId()]]; + + return $fn(Utils::modifyRequest($request, $modify), $options); + } + + /** + * Internal method which prevents infinite recursion. For certain requests, like the initial + * auth call itself, we do NOT want to send a token. + */ + private function shouldIgnore(RequestInterface $request): bool + { + return false !== strpos((string) $request->getUri(), 'tokens') && 'POST' == $request->getMethod(); + } +} diff --git a/3rdparty/php-opencloud/openstack/src/Common/Auth/Catalog.php b/3rdparty/php-opencloud/openstack/src/Common/Auth/Catalog.php new file mode 100644 index 00000000..14747f03 --- /dev/null +++ b/3rdparty/php-opencloud/openstack/src/Common/Auth/Catalog.php @@ -0,0 +1,22 @@ +request = $request; + } + + public function setResponse(ResponseInterface $response) + { + $this->response = $response; + } + + public function getRequest(): RequestInterface + { + return $this->request; + } + + public function getResponse(): ResponseInterface + { + return $this->response; + } +} diff --git a/3rdparty/php-opencloud/openstack/src/Common/Error/BaseError.php b/3rdparty/php-opencloud/openstack/src/Common/Error/BaseError.php new file mode 100644 index 00000000..fe08ba59 --- /dev/null +++ b/3rdparty/php-opencloud/openstack/src/Common/Error/BaseError.php @@ -0,0 +1,12 @@ +client = $client ?: new Client(); + } + + /** + * Internal method used when outputting headers in the error description. + */ + private function header(string $name): string + { + return sprintf("%s\n%s\n", $name, str_repeat('~', strlen($name))); + } + + /** + * Before outputting custom links, it is validated to ensure that the user is not + * directed off to a broken link. If a 404 is detected, it is hidden. + * + * @param $link The proposed link + */ + private function linkIsValid(string $link): bool + { + $link = $this->docDomain.$link; + + try { + return $this->client->request('HEAD', $link)->getStatusCode() < 400; + } catch (ClientException $e) { + return false; + } + } + + /** + * @codeCoverageIgnore + */ + public function str(MessageInterface $message, int $verbosity = 0): string + { + if ($message instanceof RequestInterface) { + $msg = trim($message->getMethod().' '.$message->getRequestTarget()); + $msg .= ' HTTP/'.$message->getProtocolVersion(); + if (!$message->hasHeader('host')) { + $msg .= "\r\nHost: ".$message->getUri()->getHost(); + } + } else { + if ($message instanceof ResponseInterface) { + $msg = 'HTTP/'.$message->getProtocolVersion().' ' + .$message->getStatusCode().' ' + .$message->getReasonPhrase(); + } else { + throw new \InvalidArgumentException('Unknown message type'); + } + } + + if ($verbosity < 1) { + return $msg; + } + + foreach ($message->getHeaders() as $name => $values) { + $msg .= "\r\n{$name}: ".implode(', ', $values); + } + + if ($verbosity < 2) { + return $msg; + } + + $contentType = strtolower($message->getHeaderLine('content-type')); + if (false !== strpos($contentType, 'application/json')) { + $body = $message->getBody()->read(self::MAX_BODY_LENGTH); + $msg .= "\r\n\r\n".$body; + if ('' !== $message->getBody()->read(1)) { + $msg .= '...'; + } + } + + return trim($msg); + } + + /** + * Helper method responsible for constructing and returning {@see BadResponseError} exceptions. + * + * @param RequestInterface $request The faulty request + * @param ResponseInterface $response The error-filled response + */ + public function httpError(RequestInterface $request, ResponseInterface $response, int $verbosity = 0): BadResponseError + { + $message = $this->header('HTTP Error'); + + $message .= sprintf( + "The remote server returned a \"%d %s\" error for the following transaction:\n\n", + $response->getStatusCode(), + $response->getReasonPhrase() + ); + + $message .= $this->header('Request'); + $message .= $this->str($request, $verbosity).PHP_EOL.PHP_EOL; + + $message .= $this->header('Response'); + $message .= $this->str($response, $verbosity).PHP_EOL.PHP_EOL; + + $message .= $this->header('Further information'); + $message .= $this->getStatusCodeMessage($response->getStatusCode()); + + $message .= 'Visit http://docs.php-opencloud.com/en/latest/http-codes for more information about debugging ' + .'HTTP status codes, or file a support issue on https://github.com/php-opencloud/openstack/issues.'; + + $e = new BadResponseError($message); + $e->setRequest($request); + $e->setResponse($response); + + return $e; + } + + private function getStatusCodeMessage(int $statusCode): string + { + $errors = [ + 400 => 'Please ensure that your input values are valid and well-formed. ', + 401 => 'Please ensure that your authentication credentials are valid. ', + 404 => "Please ensure that the resource you're trying to access actually exists. ", + 500 => 'Please try this operation again once you know the remote server is operational. ', + ]; + + return isset($errors[$statusCode]) ? $errors[$statusCode] : ''; + } + + /** + * Helper method responsible for constructing and returning {@see UserInputError} exceptions. + * + * @param string $expectedType The type that was expected from the user + * @param mixed $userValue The incorrect value the user actually provided + * @param string|null $furtherLink a link to further information if necessary (optional) + */ + public function userInputError(string $expectedType, $userValue, ?string $furtherLink = null): UserInputError + { + $message = $this->header('User Input Error'); + + $message .= sprintf( + "%s was expected, but the following value was passed in:\n\n%s\n", + $expectedType, + print_r($userValue, true) + ); + + $message .= 'Please ensure that the value adheres to the expectation above. '; + + if ($furtherLink && $this->linkIsValid($furtherLink)) { + $message .= sprintf('Visit %s for more information about input arguments. ', $this->docDomain.$furtherLink); + } + + $message .= 'If you run into trouble, please open a support issue on https://github.com/php-opencloud/openstack/issues.'; + + return new UserInputError($message); + } +} diff --git a/3rdparty/php-opencloud/openstack/src/Common/Error/NotImplementedError.php b/3rdparty/php-opencloud/openstack/src/Common/Error/NotImplementedError.php new file mode 100644 index 00000000..7b29c0c5 --- /dev/null +++ b/3rdparty/php-opencloud/openstack/src/Common/Error/NotImplementedError.php @@ -0,0 +1,12 @@ + $val) { + $key = isset($aliases[$key]) ? $aliases[$key] : $key; + if (property_exists($this, $key)) { + $this->{$key} = $val; + } + } + } + + public function set(string $key, $property, array $data, ?callable $fn = null) + { + if (isset($data[$key]) && property_exists($this, $property)) { + $value = $fn ? call_user_func($fn, $data[$key]) : $data[$key]; + $this->$property = $value; + } + } +} diff --git a/3rdparty/php-opencloud/openstack/src/Common/JsonPath.php b/3rdparty/php-opencloud/openstack/src/Common/JsonPath.php new file mode 100644 index 00000000..18019c35 --- /dev/null +++ b/3rdparty/php-opencloud/openstack/src/Common/JsonPath.php @@ -0,0 +1,109 @@ +['foo' => ['bar' => ['baz' => 'some_value']]] + * + * and you wanted to insert or extract an element. Usually, you would use: + * + *
$array['foo']['bar']['baz'] = 'new_value';
+ * + * but sometimes you do not have access to the variable - so a string representation is needed. Using + * XPath-like syntax, this class allows you to do this: + * + *
$jsonPath = new JsonPath($array);
+ * $jsonPath->set('foo.bar.baz', 'new_value');
+ * $val = $jsonPath->get('foo.bar.baz');
+ * 
+ */ +class JsonPath +{ + /** @var array */ + private $jsonStructure; + + /** + * @param $structure The initial data structure to extract from and insert into. Typically this will be a + * multidimensional associative array; but well-formed JSON strings are also acceptable. + */ + public function __construct($structure) + { + $this->jsonStructure = is_string($structure) ? json_decode($structure, true) : $structure; + } + + /** + * Set a node in the structure. + * + * @param $path The XPath to use + * @param $value The new value of the node + */ + public function set(string $path, $value) + { + $this->jsonStructure = $this->setPath($path, $value, $this->jsonStructure); + } + + /** + * Internal method for recursive calls. + * + * @return mixed + */ + private function setPath(string $path, $value, array $json): array + { + $nodes = explode('.', $path); + $point = array_shift($nodes); + + if (!isset($json[$point])) { + $json[$point] = []; + } + + if (!empty($nodes)) { + $json[$point] = $this->setPath(implode('.', $nodes), $value, $json[$point]); + } else { + $json[$point] = $value; + } + + return $json; + } + + /** + * Return the updated structure. + */ + public function getStructure() + { + return $this->jsonStructure; + } + + /** + * Get a path's value. If no path can be matched, NULL is returned. + * + * @return mixed|null + */ + public function get(string $path) + { + return $this->getPath($path, $this->jsonStructure); + } + + /** + * Internal method for recursion. + */ + private function getPath(string $path, $json) + { + $nodes = explode('.', $path); + $point = array_shift($nodes); + + if (!isset($json[$point])) { + return null; + } + + if (empty($nodes)) { + return $json[$point]; + } else { + return $this->getPath(implode('.', $nodes), $json[$point]); + } + } +} diff --git a/3rdparty/php-opencloud/openstack/src/Common/JsonSchema/JsonPatch.php b/3rdparty/php-opencloud/openstack/src/Common/JsonSchema/JsonPatch.php new file mode 100644 index 00000000..cce7c328 --- /dev/null +++ b/3rdparty/php-opencloud/openstack/src/Common/JsonSchema/JsonPatch.php @@ -0,0 +1,120 @@ +makeDiff($src, $dest); + } + + public function makeDiff($srcStruct, $desStruct, string $path = ''): array + { + $changes = []; + + if (is_object($srcStruct)) { + $changes = $this->handleObject($srcStruct, $desStruct, $path); + } elseif (is_array($srcStruct)) { + $changes = $this->handleArray($srcStruct, $desStruct, $path); + } elseif ($srcStruct != $desStruct) { + $changes[] = $this->makePatch(self::OP_REPLACE, $path, $desStruct); + } + + return $changes; + } + + protected function handleArray(array $srcStruct, array $desStruct, string $path): array + { + $changes = []; + + if ($diff = $this->arrayDiff($desStruct, $srcStruct)) { + foreach ($diff as $key => $val) { + if (is_object($val)) { + $changes = array_merge($changes, $this->makeDiff($srcStruct[$key], $val, $this->path($path, $key))); + } else { + $op = array_key_exists($key, $srcStruct) && !in_array($srcStruct[$key], $desStruct, true) + ? self::OP_REPLACE : self::OP_ADD; + $changes[] = $this->makePatch($op, $this->path($path, $key), $val); + } + } + } elseif ($srcStruct != $desStruct) { + foreach ($srcStruct as $key => $val) { + if (!in_array($val, $desStruct, true)) { + $changes[] = $this->makePatch(self::OP_REMOVE, $this->path($path, $key)); + } + } + } + + return $changes; + } + + protected function handleObject(\stdClass $srcStruct, \stdClass $desStruct, string $path): array + { + $changes = []; + + if ($this->shouldPartiallyReplace($srcStruct, $desStruct)) { + foreach ($desStruct as $key => $val) { + if (!property_exists($srcStruct, $key)) { + $changes[] = $this->makePatch(self::OP_ADD, $this->path($path, $key), $val); + } elseif ($srcStruct->$key != $val) { + $changes = array_merge($changes, $this->makeDiff($srcStruct->$key, $val, $this->path($path, $key))); + } + } + } elseif ($this->shouldPartiallyReplace($desStruct, $srcStruct)) { + foreach ($srcStruct as $key => $val) { + if (!property_exists($desStruct, $key)) { + $changes[] = $this->makePatch(self::OP_REMOVE, $this->path($path, $key)); + } + } + } + + return $changes; + } + + protected function shouldPartiallyReplace(\stdClass $o1, \stdClass $o2): bool + { + // NOTE: count(stdClass) always returns 1 + return count(array_diff_key((array) $o1, (array) $o2)) < 1; + } + + protected function arrayDiff(array $a1, array $a2): array + { + $result = []; + + foreach ($a1 as $key => $val) { + if (!in_array($val, $a2, true)) { + $result[$key] = $val; + } + } + + return $result; + } + + protected function path(string $root, $path): string + { + $path = (string) $path; + + if ('_empty_' === $path) { + $path = ''; + } + + return rtrim($root, '/').'/'.ltrim($path, '/'); + } + + protected function makePatch(string $op, string $path, $val = null): array + { + switch ($op) { + default: + return ['op' => $op, 'path' => $path, 'value' => $val]; + case self::OP_REMOVE: + return ['op' => $op, 'path' => $path]; + } + } +} diff --git a/3rdparty/php-opencloud/openstack/src/Common/JsonSchema/Schema.php b/3rdparty/php-opencloud/openstack/src/Common/JsonSchema/Schema.php new file mode 100644 index 00000000..d2ac5cdd --- /dev/null +++ b/3rdparty/php-opencloud/openstack/src/Common/JsonSchema/Schema.php @@ -0,0 +1,78 @@ +body = (object) $body; + $this->validator = $validator ?: new Validator(); + } + + public function getPropertyPaths(): array + { + $paths = []; + + foreach ($this->body->properties as $propertyName => $property) { + $paths[] = sprintf('/%s', $propertyName); + } + + return $paths; + } + + public function normalizeObject($subject, array $aliases): \stdClass + { + $out = new \stdClass(); + + foreach ($this->body->properties as $propertyName => $property) { + $name = $aliases[$propertyName] ?? $propertyName; + + if (isset($property->readOnly) && true === $property->readOnly) { + continue; + } elseif (property_exists($subject, $name)) { + $out->$propertyName = $subject->$name; + } elseif (property_exists($subject, $propertyName)) { + $out->$propertyName = $subject->$propertyName; + } + } + + return $out; + } + + public function validate($data) + { + $this->validator->check($data, $this->body); + } + + public function isValid(): bool + { + return $this->validator->isValid(); + } + + public function getErrors(): array + { + return $this->validator->getErrors(); + } + + public function getErrorString(): string + { + $msg = "Provided values do not validate. Errors:\n"; + + foreach ($this->getErrors() as $error) { + $msg .= sprintf("[%s] %s\n", $error['property'], $error['message']); + } + + return $msg; + } +} diff --git a/3rdparty/php-opencloud/openstack/src/Common/Resource/AbstractResource.php b/3rdparty/php-opencloud/openstack/src/Common/Resource/AbstractResource.php new file mode 100644 index 00000000..75f2b1ba --- /dev/null +++ b/3rdparty/php-opencloud/openstack/src/Common/Resource/AbstractResource.php @@ -0,0 +1,162 @@ + 'fooBar' + * + * will extract FOO_BAR from the response, and save it as 'fooBar' in the resource. + * + * @var array + */ + protected $aliases = []; + + /** + * Populates the current resource from a response object. + * + * @return AbstractResource + */ + public function populateFromResponse(ResponseInterface $response) + { + if (0 === strpos($response->getHeaderLine('Content-Type'), 'application/json')) { + $json = Utils::jsonDecode($response); + if (!empty($json)) { + $this->populateFromArray(Utils::flattenJson($json, $this->resourceKey)); + } + } + + return $this; + } + + /** + * Populates the current resource from a data array. + * + * @return self + */ + public function populateFromArray(array $array) + { + $aliases = $this->getAliases(); + + foreach ($array as $key => $val) { + $alias = $aliases[$key] ?? false; + + if ($alias instanceof Alias) { + $key = $alias->propertyName; + $val = $alias->getValue($this, $val); + } + + if (property_exists($this, $key)) { + $this->{$key} = $val; + } + } + + return $this; + } + + /** + * Constructs alias objects. + * + * @return Alias[] + */ + protected function getAliases(): array + { + $aliases = []; + + foreach ((array) $this->aliases as $alias => $property) { + $aliases[$alias] = new Alias($property); + } + + return $aliases; + } + + /** + * Internal method which retrieves the values of provided keys. + * + * @return array + */ + protected function getAttrs(array $keys) + { + $output = []; + + foreach ($keys as $key) { + if (property_exists($this, $key) && $this->$key !== null) { + $output[$key] = $this->$key; + } + } + + return $output; + } + + public function model(string $class, $data = null): ResourceInterface + { + $model = new $class(); + + // @codeCoverageIgnoreStart + if (!$model instanceof ResourceInterface) { + throw new \RuntimeException(sprintf('%s does not implement %s', $class, ResourceInterface::class)); + } + // @codeCoverageIgnoreEnd + + if ($data instanceof ResponseInterface) { + $model->populateFromResponse($data); + } elseif (is_array($data)) { + $model->populateFromArray($data); + } + + return $model; + } + + public function serialize(): \stdClass + { + $output = new \stdClass(); + + foreach ((new \ReflectionClass($this))->getProperties(\ReflectionProperty::IS_PUBLIC) as $property) { + $name = $property->getName(); + $val = $this->{$name}; + + $fn = function ($val) { + if ($val instanceof Serializable) { + return $val->serialize(); + } elseif ($val instanceof \DateTimeImmutable) { + return $val->format('c'); + } else { + return $val; + } + }; + + if (is_array($val)) { + foreach ($val as $sk => $sv) { + $val[$sk] = $fn($sv); + } + } + + $output->{$name} = $fn($val); + } + + return $output; + } +} diff --git a/3rdparty/php-opencloud/openstack/src/Common/Resource/Alias.php b/3rdparty/php-opencloud/openstack/src/Common/Resource/Alias.php new file mode 100644 index 00000000..53944bab --- /dev/null +++ b/3rdparty/php-opencloud/openstack/src/Common/Resource/Alias.php @@ -0,0 +1,53 @@ +isList = $list; + $this->propertyName = $propertyName; + $this->className = $className && class_exists($className) ? $className : null; + } + + public function getValue(ResourceInterface $resource, $value) + { + if (null === $value || !$this->className) { + return $value; + } elseif ($this->isList && is_array($value)) { + $array = []; + foreach ($value as $subVal) { + $array[] = $resource->model($this->className, $subVal); + } + + return $array; + } elseif (\DateTimeImmutable::class === $this->className) { + return new \DateTimeImmutable($value); + } + + return $resource->model($this->className, $value); + } +} diff --git a/3rdparty/php-opencloud/openstack/src/Common/Resource/Creatable.php b/3rdparty/php-opencloud/openstack/src/Common/Resource/Creatable.php new file mode 100644 index 00000000..d07280b0 --- /dev/null +++ b/3rdparty/php-opencloud/openstack/src/Common/Resource/Creatable.php @@ -0,0 +1,18 @@ + 'val3', 'Baz' => 'val4']); is called, then the resource will have the following + * metadata: + * + * Foo: val3 + * Bar: val2 + * Baz: val4 + * + * You will notice that any metadata items which are not specified in the call are preserved. + * + * @param array $metadata The new metadata items + */ + public function mergeMetadata(array $metadata); + + /** + * Replaces all of the existing metadata items for a resource with a new set of values. Any metadata items which + * are not provided in the call are removed from the resource. For example, if the resource has this metadata + * already set:. + * + * Foo: val1 + * Bar: val2 + * + * and resetMetadata(['Foo' => 'val3', 'Baz' => 'val4']); is called, then the resource will have the following + * metadata: + * + * Foo: val3 + * Baz: val4 + * + * @param array $metadata The new metadata items + */ + public function resetMetadata(array $metadata); + + /** + * Extracts metadata from a response object and returns it in the form of an associative array. + */ + public function parseMetadata(ResponseInterface $response): array; +} diff --git a/3rdparty/php-opencloud/openstack/src/Common/Resource/HasWaiterTrait.php b/3rdparty/php-opencloud/openstack/src/Common/Resource/HasWaiterTrait.php new file mode 100644 index 00000000..b5ca6e46 --- /dev/null +++ b/3rdparty/php-opencloud/openstack/src/Common/Resource/HasWaiterTrait.php @@ -0,0 +1,123 @@ +retrieve(); + + if ($this->status == $status || $this->shouldHalt($timeout, $startTime)) { + break; + } + + sleep($sleepPeriod); + } + } + + /** + * Provides a blocking operation until the resource has reached a particular state. The method + * will enter a loop, executing the callback until TRUE is returned. This provides great + * flexibility. + * + * @param callable $fn An anonymous function that will be executed on every iteration. You can + * encapsulate your own logic to determine whether the resource has + * successfully transitioned. When TRUE is returned by the callback, + * the loop will end. + * @param int|bool $timeout The maximum timeout in seconds. If the total time taken by the waiter has reached + * or exceed this timeout, the blocking operation will immediately cease. If FALSE + * is provided, the timeout will never be considered. + * @param int $sleepPeriod the amount of time to pause between each HTTP request + */ + public function waitWithCallback(callable $fn, $timeout = 60, int $sleepPeriod = 1) + { + $startTime = time(); + + while (true) { + $this->retrieve(); + + $response = call_user_func_array($fn, [$this]); + + if (true === $response || $this->shouldHalt($timeout, $startTime)) { + break; + } + + sleep($sleepPeriod); + } + } + + /** + * Internal method used to identify whether a timeout has been exceeded. + * + * @param bool|int $timeout + * + * @return bool + */ + private function shouldHalt($timeout, int $startTime) + { + if (false === $timeout) { + return false; + } + + return time() - $startTime >= $timeout; + } + + /** + * Convenience method providing a blocking operation until the resource transitions to an + * ``ACTIVE`` status. + * + * @param int|bool $timeout The maximum timeout in seconds. If the total time taken by the waiter has reached + * or exceed this timeout, the blocking operation will immediately cease. If FALSE + * is provided, the timeout will never be considered. + */ + public function waitUntilActive($timeout = false) + { + $this->waitUntil('ACTIVE', $timeout); + } + + public function waitUntilDeleted($timeout = 60, int $sleepPeriod = 1) + { + $startTime = time(); + + while (true) { + try { + $this->retrieve(); + } catch (BadResponseError $e) { + if (404 === $e->getResponse()->getStatusCode()) { + break; + } + throw $e; + } + + if ($this->shouldHalt($timeout, $startTime)) { + break; + } + + sleep($sleepPeriod); + } + } +} diff --git a/3rdparty/php-opencloud/openstack/src/Common/Resource/Iterator.php b/3rdparty/php-opencloud/openstack/src/Common/Resource/Iterator.php new file mode 100644 index 00000000..76b18af4 --- /dev/null +++ b/3rdparty/php-opencloud/openstack/src/Common/Resource/Iterator.php @@ -0,0 +1,99 @@ +limit = isset($options['limit']) ? $options['limit'] : false; + $this->count = 0; + + if (isset($options['resourcesKey'])) { + $this->resourcesKey = $options['resourcesKey']; + } + + if (isset($options['markerKey'])) { + $this->markerKey = $options['markerKey']; + } + + if (isset($options['mapFn']) && is_callable($options['mapFn'])) { + $this->mapFn = $options['mapFn']; + } + + $this->requestFn = $requestFn; + $this->resourceFn = $resourceFn; + } + + private function fetchResources() + { + if ($this->shouldNotSendAnotherRequest()) { + return false; + } + + $response = call_user_func($this->requestFn, $this->currentMarker); + + $json = Utils::flattenJson(Utils::jsonDecode($response), $this->resourcesKey); + + if (204 === $response->getStatusCode() || empty($json)) { + return false; + } + + return $json; + } + + private function assembleResource(array $data) + { + $resource = call_user_func($this->resourceFn, $data); + + // Invoke user-provided fn if provided + if ($this->mapFn) { + call_user_func_array($this->mapFn, [&$resource]); + } + + // Update marker if operation supports it + if ($this->markerKey) { + $this->currentMarker = $resource->{$this->markerKey}; + } + + return $resource; + } + + private function totalReached() + { + return $this->limit && $this->count >= $this->limit; + } + + private function shouldNotSendAnotherRequest() + { + return $this->totalReached() || ($this->count > 0 && !$this->markerKey); + } + + public function __invoke() + { + while ($resources = $this->fetchResources()) { + foreach ($resources as $resourceData) { + if ($this->totalReached()) { + break; + } + + ++$this->count; + + yield $this->assembleResource($resourceData); + } + } + } +} diff --git a/3rdparty/php-opencloud/openstack/src/Common/Resource/Listable.php b/3rdparty/php-opencloud/openstack/src/Common/Resource/Listable.php new file mode 100644 index 00000000..899e80fc --- /dev/null +++ b/3rdparty/php-opencloud/openstack/src/Common/Resource/Listable.php @@ -0,0 +1,28 @@ + + */ + public function enumerate(array $def, array $userVals = [], ?callable $mapFn = null); +} diff --git a/3rdparty/php-opencloud/openstack/src/Common/Resource/OperatorResource.php b/3rdparty/php-opencloud/openstack/src/Common/Resource/OperatorResource.php new file mode 100644 index 00000000..ac940c76 --- /dev/null +++ b/3rdparty/php-opencloud/openstack/src/Common/Resource/OperatorResource.php @@ -0,0 +1,148 @@ +client, $this->api); + } + + /** + * @return \GuzzleHttp\Psr7\Uri:null + */ + protected function getHttpBaseUrl() + { + return $this->client->getConfig('base_uri'); + } + + public function executeWithState(array $definition) + { + return $this->execute($definition, $this->getAttrs(array_keys($definition['params']))); + } + + private function getResourcesKey(): string + { + $resourcesKey = $this->resourcesKey; + + if (!$resourcesKey) { + $class = substr(static::class, strrpos(static::class, '\\') + 1); + $resourcesKey = strtolower(preg_replace('/([a-z])([A-Z])/', '$1_$2', $class)).'s'; + } + + return $resourcesKey; + } + + /** + * Creates a generator for enumerating over a collection of resources returned by the request. + * + * @returns \Generator + */ + public function enumerate(array $def, array $userVals = [], ?callable $mapFn = null): \Generator + { + $operation = $this->getOperation($def); + + $requestFn = function ($marker) use ($operation, $userVals) { + if ($marker) { + $userVals['marker'] = $marker; + } + + return $this->sendRequest($operation, $userVals); + }; + + $resourceFn = function (array $data) { + $resource = $this->newInstance(); + $resource->populateFromArray($data); + + return $resource; + }; + + $opts = [ + 'limit' => isset($userVals['limit']) ? $userVals['limit'] : null, + 'resourcesKey' => $this->getResourcesKey(), + 'markerKey' => $this->markerKey, + 'mapFn' => $mapFn, + ]; + + $iterator = new Iterator($opts, $requestFn, $resourceFn); + + return $iterator(); + } + + /** + * Extracts multiple instances of the current resource from a response. + * + * @return array + */ + public function extractMultipleInstances(ResponseInterface $response, ?string $key = null): array + { + $key = $key ?: $this->getResourcesKey(); + $resourcesData = Utils::jsonDecode($response)[$key]; + + $resources = []; + + foreach ($resourcesData as $resourceData) { + $resources[] = $this->newInstance()->populateFromArray($resourceData); + } + + return $resources; + } + + protected function getService() + { + $class = static::class; + $service = substr($class, 0, strpos($class, 'Models') - 1).'\\Service'; + + return new $service($this->client, $this->api); + } + + public function model(string $class, $data = null): ResourceInterface + { + $model = new $class($this->client, $this->api); + + // @codeCoverageIgnoreStart + if (!$model instanceof ResourceInterface) { + throw new \RuntimeException(sprintf('%s does not implement %s', $class, ResourceInterface::class)); + } + // @codeCoverageIgnoreEnd + + if ($data instanceof ResponseInterface) { + $model->populateFromResponse($data); + } elseif (is_array($data)) { + $model->populateFromArray($data); + } + + return $model; + } +} diff --git a/3rdparty/php-opencloud/openstack/src/Common/Resource/ResourceInterface.php b/3rdparty/php-opencloud/openstack/src/Common/Resource/ResourceInterface.php new file mode 100644 index 00000000..4e51c97e --- /dev/null +++ b/3rdparty/php-opencloud/openstack/src/Common/Resource/ResourceInterface.php @@ -0,0 +1,37 @@ + $class the name of the model class + * @param mixed $data either a {@see ResponseInterface} or data array that will populate the newly + * created model class + * + * @return T + */ + public function model(string $class, $data = null): ResourceInterface; +} diff --git a/3rdparty/php-opencloud/openstack/src/Common/Resource/Retrievable.php b/3rdparty/php-opencloud/openstack/src/Common/Resource/Retrievable.php new file mode 100644 index 00000000..359f8b93 --- /dev/null +++ b/3rdparty/php-opencloud/openstack/src/Common/Resource/Retrievable.php @@ -0,0 +1,16 @@ + 'publicURL']; + + /** + * @param array $globalOptions options that will be applied to every service created by this builder. + * Eventually they will be merged (and if necessary overridden) by the + * service-specific options passed in + * @param string $rootNamespace API classes' root namespace + */ + public function __construct(array $globalOptions = [], $rootNamespace = 'OpenStack') + { + $this->globalOptions = $globalOptions; + $this->rootNamespace = $rootNamespace; + } + + private function getClasses($namespace) + { + $namespace = $this->rootNamespace.'\\'.$namespace; + $classes = [$namespace.'\\Api', $namespace.'\\Service']; + + foreach ($classes as $class) { + if (!class_exists($class)) { + throw new \RuntimeException(sprintf('%s does not exist', $class)); + } + } + + return $classes; + } + + /** + * This method will return an OpenStack service ready fully built and ready for use. There is + * some initial setup that may prohibit users from directly instantiating the service class + * directly - this setup includes the configuration of the HTTP client's base URL, and the + * attachment of an authentication handler. + * + * @param string $namespace The namespace of the service + * @param array $serviceOptions The service-specific options to use + */ + public function createService(string $namespace, array $serviceOptions = []): ServiceInterface + { + $options = $this->mergeOptions($serviceOptions); + + $this->stockAuthHandler($options); + $this->stockHttpClient($options, $namespace); + + [$apiClass, $serviceClass] = $this->getClasses($namespace); + + return new $serviceClass($options['httpClient'], new $apiClass()); + } + + private function stockHttpClient(array &$options, string $serviceName): void + { + if (!isset($options['httpClient']) || !($options['httpClient'] instanceof ClientInterface)) { + if (false !== stripos($serviceName, 'identity')) { + $baseUrl = $options['authUrl']; + $token = null; + } else { + [$token, $baseUrl] = $options['identityService']->authenticate($options); + } + + $stack = HandlerStackFactory::createWithOptions(array_merge($options, ['token' => $token])); + $microVersion = $options['microVersion'] ?? null; + + $options['httpClient'] = $this->httpClient($baseUrl, $stack, $options['catalogType'], $microVersion); + } + } + + /** + * @codeCoverageIgnore + */ + private function stockAuthHandler(array &$options): void + { + if (!isset($options['authHandler'])) { + $options['authHandler'] = function () use ($options) { + return $options['identityService']->authenticate($options)[0]; + }; + } + } + + private function httpClient(string $baseUrl, HandlerStack $stack, ?string $serviceType = null, ?string $microVersion = null): ClientInterface + { + $clientOptions = [ + 'base_uri' => Utils::normalizeUrl($baseUrl), + 'handler' => $stack, + ]; + + if ($microVersion && $serviceType) { + $clientOptions['headers']['OpenStack-API-Version'] = sprintf('%s %s', $serviceType, $microVersion); + } + + if (isset($this->globalOptions['requestOptions'])) { + $clientOptions = array_merge($this->globalOptions['requestOptions'], $clientOptions); + } + + return new Client($clientOptions); + } + + private function mergeOptions(array $serviceOptions): array + { + $options = array_merge($this->defaults, $this->globalOptions, $serviceOptions); + + if (!isset($options['authUrl'])) { + throw new \InvalidArgumentException('"authUrl" is a required option'); + } + + if (!isset($options['identityService']) || !($options['identityService'] instanceof IdentityService)) { + throw new \InvalidArgumentException(sprintf('"identityService" must be specified and implement %s', IdentityService::class)); + } + + return $options; + } +} diff --git a/3rdparty/php-opencloud/openstack/src/Common/Service/ServiceInterface.php b/3rdparty/php-opencloud/openstack/src/Common/Service/ServiceInterface.php new file mode 100644 index 00000000..b0c2b6d4 --- /dev/null +++ b/3rdparty/php-opencloud/openstack/src/Common/Service/ServiceInterface.php @@ -0,0 +1,14 @@ +push(Middleware::httpErrors(), 'http_errors'); + $stack->push(Middleware::prepareBody(), 'prepare_body'); + + return $stack; + } + + /** + * Creates a new HandlerStack with the given options. + * + * @param array{ + * handler: callable, + * authHandler: callable, + * token: \OpenStack\Common\Auth\Token, + * errorVerbosity: int, + * debugLog: bool, + * logger: \Psr\Log\LoggerInterface, + * messageFormatter: \GuzzleHttp\MessageFormatter + * } $options + */ + public static function createWithOptions(array $options): HandlerStack + { + $stack = new HandlerStack($options['handler'] ?? Utils::chooseHandler()); + $stack->push(Middleware::httpErrors($options['errorVerbosity'] ?? 0), 'http_errors'); + $stack->push(GuzzleMiddleware::prepareBody(), 'prepare_body'); + + if (!empty($options['authHandler'])) { + $stack->push(Middleware::authHandler($options['authHandler'], $options['token'] ?? null)); + } + + if (!empty($options['debugLog']) + && !empty($options['logger']) + && !empty($options['messageFormatter']) + ) { + $logMiddleware = GuzzleMiddleware::log($options['logger'], $options['messageFormatter']); + $stack->push($logMiddleware, 'logger'); + } + + return $stack; + } +} diff --git a/3rdparty/php-opencloud/openstack/src/Common/Transport/JsonSerializer.php b/3rdparty/php-opencloud/openstack/src/Common/Transport/JsonSerializer.php new file mode 100644 index 00000000..0ea5750a --- /dev/null +++ b/3rdparty/php-opencloud/openstack/src/Common/Transport/JsonSerializer.php @@ -0,0 +1,107 @@ +getName(); + if ($path = $param->getPath()) { + $jsonPath = new JsonPath($json); + $jsonPath->set(sprintf('%s.%s', $path, $name), $userValue); + $json = $jsonPath->getStructure(); + } elseif ($name) { + $json[$name] = $userValue; + } else { + $json[] = $userValue; + } + + return $json; + } + + /** + * Populates a value into an array-like structure. + * + * @param Parameter $param The schema that defines how the JSON field is being populated + * @param mixed $userValue The user value that is populating a JSON field + * + * @return array|mixed + */ + private function stockArrayJson(Parameter $param, array $userValue): array + { + $elems = []; + foreach ($userValue as $item) { + $elems = $this->stockJson($param->getItemSchema(), $item, $elems); + } + + return $elems; + } + + /** + * Populates a value into an object-like structure. + * + * @param Parameter $param The schema that defines how the JSON field is being populated + * @param mixed $userValue The user value that is populating a JSON field + */ + private function stockObjectJson(Parameter $param, \stdClass $userValue): array + { + $object = []; + foreach ($userValue as $key => $val) { + $object = $this->stockJson($param->getProperty($key), $val, $object); + } + + return $object; + } + + /** + * A generic method that will populate a JSON structure with a value according to a schema. It + * supports multiple types and will delegate accordingly. + * + * @param Parameter $param The schema that defines how the JSON field is being populated + * @param mixed $userValue The user value that is populating a JSON field + * @param array $json The existing JSON structure that will be populated + */ + public function stockJson(Parameter $param, $userValue, array $json): array + { + if ($param->isArray()) { + $userValue = $this->stockArrayJson($param, $userValue); + } elseif ($param->isObject()) { + $userValue = $this->stockObjectJson($param, $this->serializeObjectValue($userValue)); + } + + // Populate the final value + return $this->stockValue($param, $userValue, $json); + } + + private function serializeObjectValue($value) + { + if (is_object($value)) { + if ($value instanceof Serializable) { + $value = $value->serialize(); + } elseif (!($value instanceof \stdClass)) { + throw new \InvalidArgumentException(sprintf('When an object value is provided, it must either be \stdClass or implement the Serializable interface, you provided %s', print_r($value, true))); + } + } + + return (object) $value; + } +} diff --git a/3rdparty/php-opencloud/openstack/src/Common/Transport/Middleware.php b/3rdparty/php-opencloud/openstack/src/Common/Transport/Middleware.php new file mode 100644 index 00000000..c7eaed34 --- /dev/null +++ b/3rdparty/php-opencloud/openstack/src/Common/Transport/Middleware.php @@ -0,0 +1,88 @@ +then( + function (ResponseInterface $response) use ($request, $verbosity) { + if ($response->getStatusCode() < 400) { + return $response; + } + throw (new Builder())->httpError($request, $response, $verbosity); + } + ); + }; + }; + } + + public static function authHandler(callable $tokenGenerator, ?Token $token = null): callable + { + return function (callable $handler) use ($tokenGenerator, $token) { + return new AuthHandler($handler, $tokenGenerator, $token); + }; + } + + /** + * @codeCoverageIgnore + */ + public static function history(array &$container): callable + { + return GuzzleMiddleware::history($container); + } + + /** + * @codeCoverageIgnore + */ + public static function retry(callable $decider, ?callable $delay = null): callable + { + return GuzzleMiddleware::retry($decider, $delay); + } + + /** + * @codeCoverageIgnore + */ + public static function log(LoggerInterface $logger, MessageFormatter $formatter, $logLevel = LogLevel::INFO): callable + { + return GuzzleMiddleware::log($logger, $formatter, $logLevel); + } + + /** + * @codeCoverageIgnore + */ + public static function prepareBody(): callable + { + return GuzzleMiddleware::prepareBody(); + } + + /** + * @codeCoverageIgnore + */ + public static function mapRequest(callable $fn): callable + { + return GuzzleMiddleware::mapRequest($fn); + } + + /** + * @codeCoverageIgnore + */ + public static function mapResponse(callable $fn): callable + { + return GuzzleMiddleware::mapResponse($fn); + } +} diff --git a/3rdparty/php-opencloud/openstack/src/Common/Transport/RequestSerializer.php b/3rdparty/php-opencloud/openstack/src/Common/Transport/RequestSerializer.php new file mode 100644 index 00000000..20d29e66 --- /dev/null +++ b/3rdparty/php-opencloud/openstack/src/Common/Transport/RequestSerializer.php @@ -0,0 +1,92 @@ +jsonSerializer = $jsonSerializer ?: new JsonSerializer(); + } + + public function serializeOptions(Operation $operation, array $userValues = []): array + { + $options = ['headers' => []]; + + foreach ($userValues as $paramName => $paramValue) { + if (null === ($schema = $operation->getParam($paramName))) { + continue; + } + + $this->callStockingMethod($schema, $paramValue, $options); + } + + if (!empty($options['json'])) { + if ($key = $operation->getJsonKey()) { + $options['json'] = [$key => $options['json']]; + } + if (false !== strpos(json_encode($options['json']), '\/')) { + $options['body'] = json_encode($options['json'], JSON_UNESCAPED_SLASHES); + $options['headers']['Content-Type'] = 'application/json'; + unset($options['json']); + } + } + + return $options; + } + + private function callStockingMethod(Parameter $schema, $paramValue, array &$options) + { + $location = $schema->getLocation(); + + $methods = ['query', 'header', 'json', 'raw']; + if (!in_array($location, $methods)) { + return; + } + + $method = sprintf('stock%s', ucfirst($location)); + $this->$method($schema, $paramValue, $options); + } + + private function stockQuery(Parameter $schema, $paramValue, array &$options) + { + $options['query'][$schema->getName()] = $paramValue; + } + + private function stockHeader(Parameter $schema, $paramValue, array &$options) + { + $paramName = $schema->getName(); + + if (false !== stripos($paramName, 'metadata')) { + return $this->stockMetadataHeader($schema, $paramValue, $options); + } + + $options['headers'] += is_scalar($paramValue) ? [$schema->getPrefixedName() => $paramValue] : []; + } + + private function stockMetadataHeader(Parameter $schema, $paramValue, array &$options) + { + foreach ($paramValue as $key => $keyVal) { + $schema = $schema->getItemSchema() ?: new Parameter(['prefix' => $schema->getPrefix(), 'name' => $key]); + $this->stockHeader($schema, $keyVal, $options); + } + } + + private function stockJson(Parameter $schema, $paramValue, array &$options) + { + $json = isset($options['json']) ? $options['json'] : []; + $options['json'] = $this->jsonSerializer->stockJson($schema, $paramValue, $json); + } + + private function stockRaw(Parameter $schema, $paramValue, array &$options) + { + $options['body'] = $paramValue; + } +} diff --git a/3rdparty/php-opencloud/openstack/src/Common/Transport/Serializable.php b/3rdparty/php-opencloud/openstack/src/Common/Transport/Serializable.php new file mode 100644 index 00000000..ac15fc6a --- /dev/null +++ b/3rdparty/php-opencloud/openstack/src/Common/Transport/Serializable.php @@ -0,0 +1,11 @@ + 'JSON_ERROR_DEPTH - Maximum stack depth exceeded', + JSON_ERROR_STATE_MISMATCH => 'JSON_ERROR_STATE_MISMATCH - Underflow or the modes mismatch', + JSON_ERROR_CTRL_CHAR => 'JSON_ERROR_CTRL_CHAR - Unexpected control character found', + JSON_ERROR_SYNTAX => 'JSON_ERROR_SYNTAX - Syntax error, malformed JSON', + JSON_ERROR_UTF8 => 'JSON_ERROR_UTF8 - Malformed UTF-8 characters, possibly incorrectly encoded', + ]; + + $responseBody = (string) $response->getBody(); + + if (0 === strlen($responseBody)) { + return $responseBody; + } + + $data = json_decode($responseBody, $assoc); + + if (JSON_ERROR_NONE !== json_last_error()) { + $last = json_last_error(); + throw new \InvalidArgumentException('Unable to parse JSON data: '.(isset($jsonErrors[$last]) ? $jsonErrors[$last] : 'Unknown error')); + } + + return $data; + } + + /** + * Method for flattening a nested array. + * + * @param array $data The nested array + * @param string $key The key to extract + * + * @return array + */ + public static function flattenJson($data, ?string $key = null) + { + return (!empty($data) && $key && isset($data[$key])) ? $data[$key] : $data; + } + + /** + * Method for normalize an URL string. + * + * Append the http:// prefix if not present, and add a + * closing url separator when missing. + * + * @param string $url the url representation + */ + public static function normalizeUrl(string $url): string + { + if (false === strpos($url, 'http')) { + $url = 'http://'.$url; + } + + return rtrim($url, '/').'/'; + } + + /** + * Add an unlimited list of paths to a given URI. + */ + public static function addPaths(UriInterface $uri, ...$paths): UriInterface + { + return GuzzleUtils::uriFor(rtrim((string) $uri, '/').'/'.implode('/', $paths)); + } + + public static function appendPath(UriInterface $uri, $path): UriInterface + { + return GuzzleUtils::uriFor(rtrim((string) $uri, '/').'/'.$path); + } + + /** + * Expands a URI template. + * + * @param string $template URI template + * @param array $variables Template variables + */ + public static function uri_template($template, array $variables): string + { + if (extension_loaded('uri_template')) { + // @codeCoverageIgnoreStart + return \uri_template($template, $variables); + // @codeCoverageIgnoreEnd + } + + return UriTemplate::expand($template, $variables); + } +} diff --git a/3rdparty/php-opencloud/openstack/src/Compute/v2/Api.php b/3rdparty/php-opencloud/openstack/src/Compute/v2/Api.php new file mode 100644 index 00000000..ee4adfa0 --- /dev/null +++ b/3rdparty/php-opencloud/openstack/src/Compute/v2/Api.php @@ -0,0 +1,916 @@ +params = new Params(); + } + + public function getLimits(): array + { + return [ + 'method' => 'GET', + 'path' => 'limits', + 'params' => [], + ]; + } + + public function getFlavors(): array + { + return [ + 'method' => 'GET', + 'path' => 'flavors', + 'params' => [ + 'limit' => $this->params->limit(), + 'marker' => $this->params->marker(), + 'minDisk' => $this->params->minDisk(), + 'minRam' => $this->params->minRam(), + ], + ]; + } + + public function getFlavorsDetail(): array + { + $op = $this->getFlavors(); + $op['path'] .= '/detail'; + + return $op; + } + + public function getFlavor(): array + { + return [ + 'method' => 'GET', + 'path' => 'flavors/{id}', + 'params' => ['id' => $this->params->urlId('flavor')], + ]; + } + + public function postFlavors(): array + { + return [ + 'method' => 'POST', + 'path' => 'flavors', + 'jsonKey' => 'flavor', + 'params' => [ + 'id' => $this->notRequired($this->params->id('flavor')), + 'name' => $this->isRequired($this->params->name('flavor')), + 'ram' => $this->params->flavorRam(), + 'vcpus' => $this->params->flavorVcpus(), + 'swap' => $this->params->flavorSwap(), + 'disk' => $this->params->flavorDisk(), + ], + ]; + } + + public function deleteFlavor(): array + { + return [ + 'method' => 'DELETE', + 'path' => 'flavors/{id}', + 'params' => [ + 'id' => $this->params->idPath(), + ], + ]; + } + + public function getImages(): array + { + return [ + 'method' => 'GET', + 'path' => 'images', + 'params' => [ + 'limit' => $this->params->limit(), + 'marker' => $this->params->marker(), + 'name' => $this->params->flavorName(), + 'changesSince' => $this->params->filterChangesSince('image'), + 'server' => $this->params->flavorServer(), + 'status' => $this->params->filterStatus('image'), + 'type' => $this->params->flavorType(), + ], + ]; + } + + public function getImagesDetail(): array + { + $op = $this->getImages(); + $op['path'] .= '/detail'; + + return $op; + } + + public function getImage(): array + { + return [ + 'method' => 'GET', + 'path' => 'images/{id}', + 'params' => ['id' => $this->params->urlId('image')], + ]; + } + + public function deleteImage(): array + { + return [ + 'method' => 'DELETE', + 'path' => 'images/{id}', + 'params' => ['id' => $this->params->urlId('image')], + ]; + } + + public function getImageMetadata(): array + { + return [ + 'method' => 'GET', + 'path' => 'images/{id}/metadata', + 'params' => ['id' => $this->params->urlId('image')], + ]; + } + + public function putImageMetadata(): array + { + return [ + 'method' => 'PUT', + 'path' => 'images/{id}/metadata', + 'params' => [ + 'id' => $this->params->urlId('image'), + 'metadata' => $this->params->metadata(), + ], + ]; + } + + public function postImageMetadata(): array + { + return [ + 'method' => 'POST', + 'path' => 'images/{id}/metadata', + 'params' => [ + 'id' => $this->params->urlId('image'), + 'metadata' => $this->params->metadata(), + ], + ]; + } + + public function getImageMetadataKey(): array + { + return [ + 'method' => 'GET', + 'path' => 'images/{id}/metadata/{key}', + 'params' => [ + 'id' => $this->params->urlId('image'), + 'key' => $this->params->key(), + ], + ]; + } + + public function deleteImageMetadataKey(): array + { + return [ + 'method' => 'DELETE', + 'path' => 'images/{id}/metadata/{key}', + 'params' => [ + 'id' => $this->params->urlId('image'), + 'key' => $this->params->key(), + ], + ]; + } + + public function postServer(): array + { + return [ + 'path' => 'servers', + 'method' => 'POST', + 'jsonKey' => 'server', + 'params' => [ + 'imageId' => $this->notRequired($this->params->imageId()), + 'flavorId' => $this->params->flavorId(), + 'personality' => $this->params->personality(), + 'metadata' => $this->notRequired($this->params->metadata()), + 'name' => $this->isRequired($this->params->name('server')), + 'securityGroups' => $this->params->securityGroups(), + 'userData' => $this->params->userData(), + 'availabilityZone' => $this->params->availabilityZone(), + 'networks' => $this->params->networks(), + 'blockDeviceMapping' => $this->params->blockDeviceMapping(), + 'keyName' => $this->params->keyName(), + ], + ]; + } + + public function getServers(): array + { + return [ + 'method' => 'GET', + 'path' => 'servers', + 'params' => [ + 'limit' => $this->params->limit(), + 'marker' => $this->params->marker(), + 'changesSince' => $this->params->filterChangesSince('server'), + 'imageId' => $this->params->filterImage(), + 'flavorId' => $this->params->filterFlavor(), + 'name' => $this->params->filterName(), + 'status' => $this->params->filterStatus('server'), + 'host' => $this->params->filterHost(), + 'allTenants' => $this->params->allTenants(), + ], + ]; + } + + public function getServersDetail(): array + { + $definition = $this->getServers(); + $definition['path'] .= '/detail'; + + return $definition; + } + + public function getServer(): array + { + return [ + 'method' => 'GET', + 'path' => 'servers/{id}', + 'params' => [ + 'id' => $this->params->urlId('server'), + ], + ]; + } + + public function putServer(): array + { + return [ + 'method' => 'PUT', + 'path' => 'servers/{id}', + 'jsonKey' => 'server', + 'params' => [ + 'id' => $this->params->urlId('server'), + 'ipv4' => $this->params->ipv4(), + 'ipv6' => $this->params->ipv6(), + 'name' => $this->params->name('server'), + ], + ]; + } + + public function deleteServer(): array + { + return [ + 'method' => 'DELETE', + 'path' => 'servers/{id}', + 'params' => ['id' => $this->params->urlId('server')], + ]; + } + + public function changeServerPassword(): array + { + return [ + 'method' => 'POST', + 'path' => 'servers/{id}/action', + 'jsonKey' => 'changePassword', + 'params' => [ + 'id' => $this->params->urlId('server'), + 'password' => $this->params->password(), + ], + ]; + } + + public function resetServerState(): array + { + return [ + 'method' => 'POST', + 'path' => 'servers/{id}/action', + 'params' => [ + 'id' => $this->params->urlId('server'), + 'resetState' => $this->params->resetState(), + ], + ]; + } + + public function rebootServer(): array + { + return [ + 'method' => 'POST', + 'path' => 'servers/{id}/action', + 'jsonKey' => 'reboot', + 'params' => [ + 'id' => $this->params->urlId('server'), + 'type' => $this->params->rebootType(), + ], + ]; + } + + public function startServer(): array + { + return [ + 'method' => 'POST', + 'path' => 'servers/{id}/action', + 'params' => [ + 'id' => $this->params->urlId('server'), + 'os-start' => $this->params->nullAction(), + ], + ]; + } + + public function stopServer(): array + { + return [ + 'method' => 'POST', + 'path' => 'servers/{id}/action', + 'params' => [ + 'id' => $this->params->urlId('server'), + 'os-stop' => $this->params->nullAction(), + ], + ]; + } + + public function resumeServer(): array + { + return [ + 'method' => 'POST', + 'path' => 'servers/{id}/action', + 'params' => [ + 'id' => $this->params->urlId('server'), + 'resume' => $this->params->nullAction(), + ], + ]; + } + + public function suspendServer(): array + { + return [ + 'method' => 'POST', + 'path' => 'servers/{id}/action', + 'params' => [ + 'id' => $this->params->urlId('server'), + 'suspend' => $this->params->nullAction(), + ], + ]; + } + + public function rebuildServer(): array + { + return [ + 'method' => 'POST', + 'path' => 'servers/{id}/action', + 'jsonKey' => 'rebuild', + 'params' => [ + 'id' => $this->params->urlId('server'), + 'ipv4' => $this->params->ipv4(), + 'ipv6' => $this->params->ipv6(), + 'imageId' => $this->params->imageId(), + 'personality' => $this->params->personality(), + 'name' => $this->params->name('server'), + 'metadata' => $this->notRequired($this->params->metadata()), + 'adminPass' => $this->params->password(), + ], + ]; + } + + public function rescueServer(): array + { + return [ + 'method' => 'POST', + 'path' => 'servers/{id}/action', + 'jsonKey' => 'rescue', + 'params' => [ + 'id' => $this->params->urlId('server'), + 'imageId' => $this->params->rescueImageId(), + 'adminPass' => $this->notRequired($this->params->password()), + ], + ]; + } + + public function unrescueServer(): array + { + return [ + 'method' => 'POST', + 'path' => 'servers/{id}/action', + 'params' => [ + 'id' => $this->params->urlId('server'), + 'unrescue' => $this->params->nullAction(), + ], + ]; + } + + public function resizeServer(): array + { + return [ + 'method' => 'POST', + 'path' => 'servers/{id}/action', + 'jsonKey' => 'resize', + 'params' => [ + 'id' => $this->params->urlId('server'), + 'flavorId' => $this->params->flavorId(), + ], + ]; + } + + public function confirmServerResize(): array + { + return [ + 'method' => 'POST', + 'path' => 'servers/{id}/action', + 'params' => [ + 'id' => $this->params->urlId('server'), + 'confirmResize' => $this->params->nullAction(), + ], + ]; + } + + public function revertServerResize(): array + { + return [ + 'method' => 'POST', + 'path' => 'servers/{id}/action', + 'params' => [ + 'id' => $this->params->urlId('server'), + 'revertResize' => $this->params->nullAction(), + ], + ]; + } + + public function getConsoleOutput(): array + { + return [ + 'method' => 'POST', + 'path' => 'servers/{id}/action', + 'jsonKey' => 'os-getConsoleOutput', + 'params' => [ + 'id' => $this->params->urlId('server'), + 'length' => $this->notRequired($this->params->consoleLogLength()), + ], + ]; + } + + public function getAllConsoleOutput(): array + { + return [ + 'method' => 'POST', + 'path' => 'servers/{id}/action', + 'params' => [ + 'id' => $this->params->urlId('server'), + 'os-getConsoleOutput' => $this->params->emptyObject(), + ], + ]; + } + + public function createServerImage(): array + { + return [ + 'method' => 'POST', + 'path' => 'servers/{id}/action', + 'jsonKey' => 'createImage', + 'params' => [ + 'id' => $this->params->urlId('server'), + 'metadata' => $this->notRequired($this->params->metadata()), + 'name' => $this->isRequired($this->params->name('server')), + ], + ]; + } + + public function getVncConsole(): array + { + return [ + 'method' => 'POST', + 'path' => 'servers/{id}/action', + 'jsonKey' => 'os-getVNCConsole', + 'params' => [ + 'id' => $this->params->urlId('server'), + 'type' => $this->params->consoleType(), + ], + ]; + } + + public function getSpiceConsole(): array + { + return [ + 'method' => 'POST', + 'path' => 'servers/{id}/action', + 'jsonKey' => 'os-getSPICEConsole', + 'params' => [ + 'id' => $this->params->urlId('server'), + 'type' => $this->params->consoleType(), + ], + ]; + } + + public function getSerialConsole(): array + { + return [ + 'method' => 'POST', + 'path' => 'servers/{id}/action', + 'jsonKey' => 'os-getSerialConsole', + 'params' => [ + 'id' => $this->params->urlId('server'), + 'type' => $this->params->consoleType(), + ], + ]; + } + + public function getRDPConsole(): array + { + return [ + 'method' => 'POST', + 'path' => 'servers/{id}/action', + 'jsonKey' => 'os-getRDPConsole', + 'params' => [ + 'id' => $this->params->urlId('server'), + 'type' => $this->params->consoleType(), + ], + ]; + } + + public function getAddresses(): array + { + return [ + 'method' => 'GET', + 'path' => 'servers/{id}/ips', + 'params' => ['id' => $this->params->urlId('server')], + ]; + } + + public function getAddressesByNetwork(): array + { + return [ + 'method' => 'GET', + 'path' => 'servers/{id}/ips/{networkLabel}', + 'params' => [ + 'id' => $this->params->urlId('server'), + 'networkLabel' => $this->params->networkLabel(), + ], + ]; + } + + public function getInterfaceAttachments(): array + { + return [ + 'method' => 'GET', + 'path' => 'servers/{id}/os-interface', + 'jsonKey' => 'interfaceAttachments', + 'params' => [ + 'id' => $this->params->urlId('server'), + ], + ]; + } + + public function getInterfaceAttachment(): array + { + return [ + 'method' => 'GET', + 'path' => 'servers/{id}/os-interface/{portId}', + 'params' => [ + 'id' => $this->params->urlId('server'), + 'portId' => $this->params->portId(), + ], + ]; + } + + public function postInterfaceAttachment(): array + { + return [ + 'method' => 'POST', + 'path' => 'servers/{id}/os-interface', + 'jsonKey' => 'interfaceAttachment', + 'params' => [ + 'id' => $this->params->urlId('server'), + 'portId' => $this->notRequired($this->params->portId()), + 'networkId' => $this->notRequired($this->params->networkId()), + 'fixedIpAddresses' => $this->notRequired($this->params->fixedIpAddresses()), + 'tag' => $this->notRequired($this->params->tag()), + ], + ]; + } + + public function deleteInterfaceAttachment(): array + { + return [ + 'method' => 'DELETE', + 'path' => 'servers/{id}/os-interface/{portId}', + 'params' => [ + 'id' => $this->params->urlId('image'), + 'portId' => $this->params->portId(), + ], + ]; + } + + public function getServerMetadata(): array + { + return [ + 'method' => 'GET', + 'path' => 'servers/{id}/metadata', + 'params' => ['id' => $this->params->urlId('server')], + ]; + } + + public function putServerMetadata(): array + { + return [ + 'method' => 'PUT', + 'path' => 'servers/{id}/metadata', + 'params' => [ + 'id' => $this->params->urlId('server'), + 'metadata' => $this->params->metadata(), + ], + ]; + } + + public function postServerMetadata(): array + { + return [ + 'method' => 'POST', + 'path' => 'servers/{id}/metadata', + 'params' => [ + 'id' => $this->params->urlId('server'), + 'metadata' => $this->params->metadata(), + ], + ]; + } + + public function getServerMetadataKey(): array + { + return [ + 'method' => 'GET', + 'path' => 'servers/{id}/metadata/{key}', + 'params' => [ + 'id' => $this->params->urlId('server'), + 'key' => $this->params->key(), + ], + ]; + } + + public function deleteServerMetadataKey(): array + { + return [ + 'method' => 'DELETE', + 'path' => 'servers/{id}/metadata/{key}', + 'params' => [ + 'id' => $this->params->urlId('server'), + 'key' => $this->params->key(), + ], + ]; + } + + public function getKeypair(): array + { + return [ + 'method' => 'GET', + 'path' => 'os-keypairs/{name}', + 'params' => [ + 'name' => $this->isRequired($this->params->keypairName()), + 'userId' => $this->params->userId(), + ], + ]; + } + + public function getKeypairs(): array + { + return [ + 'method' => 'GET', + 'path' => 'os-keypairs', + 'params' => [ + 'userId' => $this->params->userId(), + ], + ]; + } + + public function postKeypair(): array + { + return [ + 'method' => 'POST', + 'path' => 'os-keypairs', + 'jsonKey' => 'keypair', + 'params' => [ + 'name' => $this->isRequired($this->params->name('keypair')), + 'publicKey' => $this->params->keypairPublicKey(), + 'type' => $this->params->keypairType(), + 'userId' => $this->params->keypairUserId(), + ], + ]; + } + + public function deleteKeypair(): array + { + return [ + 'method' => 'DELETE', + 'path' => 'os-keypairs/{name}', + 'params' => [ + 'name' => $this->isRequired($this->params->keypairName()), + ], + ]; + } + + public function postSecurityGroup(): array + { + return [ + 'method' => 'POST', + 'path' => 'servers/{id}/action', + 'jsonKey' => 'addSecurityGroup', + 'params' => [ + 'id' => $this->params->urlId('server'), + 'name' => $this->isRequired($this->params->name('securityGroup')), + ], + ]; + } + + public function deleteSecurityGroup(): array + { + return [ + 'method' => 'POST', + 'path' => 'servers/{id}/action', + 'jsonKey' => 'removeSecurityGroup', + 'params' => [ + 'id' => $this->params->urlId('server'), + 'name' => $this->isRequired($this->params->name('securityGroup')), + ], + ]; + } + + public function getSecurityGroups(): array + { + return [ + 'method' => 'GET', + 'path' => 'servers/{id}/os-security-groups', + 'jsonKey' => 'security_groups', + 'params' => [ + 'id' => $this->params->urlId('server'), + ], + ]; + } + + public function getVolumeAttachments(): array + { + return [ + 'method' => 'GET', + 'path' => 'servers/{id}/os-volume_attachments', + 'jsonKey' => 'volumeAttachments', + 'params' => [ + 'id' => $this->params->urlId('server'), + ], + ]; + } + + public function postVolumeAttachments(): array + { + return [ + 'method' => 'POST', + 'path' => 'servers/{id}/os-volume_attachments', + 'jsonKey' => 'volumeAttachment', + 'params' => [ + 'id' => $this->params->urlId('server'), + 'volumeId' => $this->params->volumeId(), + ], + ]; + } + + public function deleteVolumeAttachments(): array + { + return [ + 'method' => 'DELETE', + 'path' => 'servers/{id}/os-volume_attachments/{attachmentId}', + 'params' => [ + 'id' => $this->params->urlId('server'), + 'attachmentId' => $this->params->attachmentId(), + ], + ]; + } + + public function getHypervisorStatistics(): array + { + return [ + 'method' => 'GET', + 'path' => 'os-hypervisors/statistics', + 'params' => [ + ], + ]; + } + + public function getHypervisors(): array + { + return [ + 'method' => 'GET', + 'path' => 'os-hypervisors', + 'jsonKey' => 'hypervisors', + 'params' => [ + 'limit' => $this->params->limit(), + 'marker' => $this->params->marker(), + ], + ]; + } + + public function getHypervisorsDetail(): array + { + $definition = $this->getHypervisors(); + $definition['path'] .= '/detail'; + + return $definition; + } + + public function getHypervisor(): array + { + return [ + 'method' => 'GET', + 'path' => 'os-hypervisors/{id}', + 'params' => ['id' => $this->params->urlId('id')], + ]; + } + + public function getAvailabilityZones(): array + { + return [ + 'method' => 'GET', + 'path' => 'os-availability-zone/detail', + 'params' => [ + 'limit' => $this->params->limit(), + 'marker' => $this->params->marker(), + ], + ]; + } + + public function getHosts(): array + { + return [ + 'method' => 'GET', + 'path' => 'os-hosts', + 'params' => [ + 'limit' => $this->params->limit(), + 'marker' => $this->params->marker(), + ], + ]; + } + + public function getHost(): array + { + return [ + 'method' => 'GET', + 'path' => 'os-hosts/{name}', + 'params' => ['name' => $this->params->urlId('name')], + ]; + } + + public function getQuotaSet(): array + { + return [ + 'method' => 'GET', + 'path' => 'os-quota-sets/{tenantId}', + 'params' => [ + 'tenantId' => $this->params->urlId('quota-sets'), + ], + ]; + } + + public function getQuotaSetDetail(): array + { + $data = $this->getQuotaSet(); + $data['path'] .= '/detail'; + + return $data; + } + + public function deleteQuotaSet(): array + { + return [ + 'method' => 'DELETE', + 'path' => 'os-quota-sets/{tenantId}', + 'jsonKey' => 'quota_set', + 'params' => [ + 'tenantId' => $this->params->urlId('quota-sets'), + ], + ]; + } + + public function putQuotaSet(): array + { + return [ + 'method' => 'PUT', + 'path' => 'os-quota-sets/{tenantId}', + 'jsonKey' => 'quota_set', + 'params' => [ + 'tenantId' => $this->params->idPath(), + 'force' => $this->notRequired($this->params->quotaSetLimitForce()), + 'instances' => $this->notRequired($this->params->quotaSetLimitInstances()), + 'cores' => $this->notRequired($this->params->quotaSetLimitCores()), + 'fixedIps' => $this->notRequired($this->params->quotaSetLimitFixedIps()), + 'floatingIps' => $this->notRequired($this->params->quotaSetLimitFloatingIps()), + 'injectedFileContentBytes' => $this->notRequired($this->params->quotaSetLimitInjectedFileContentBytes()), + 'injectedFilePathBytes' => $this->notRequired($this->params->quotaSetLimitInjectedFilePathBytes()), + 'injectedFiles' => $this->notRequired($this->params->quotaSetLimitInjectedFiles()), + 'keyPairs' => $this->notRequired($this->params->quotaSetLimitKeyPairs()), + 'metadataItems' => $this->notRequired($this->params->quotaSetLimitMetadataItems()), + 'ram' => $this->notRequired($this->params->quotaSetLimitRam()), + 'securityGroupRules' => $this->notRequired($this->params->quotaSetLimitSecurityGroupRules()), + 'securityGroups' => $this->notRequired($this->params->quotaSetLimitSecurityGroups()), + 'serverGroups' => $this->notRequired($this->params->quotaSetLimitServerGroups()), + 'serverGroupMembers' => $this->notRequired($this->params->quotaSetLimitServerGroupMembers()), + ], + ]; + } +} diff --git a/3rdparty/php-opencloud/openstack/src/Compute/v2/Enum.php b/3rdparty/php-opencloud/openstack/src/Compute/v2/Enum.php new file mode 100644 index 00000000..378445e6 --- /dev/null +++ b/3rdparty/php-opencloud/openstack/src/Compute/v2/Enum.php @@ -0,0 +1,19 @@ + 'name', + 'zoneState' => 'state', + ]; +} diff --git a/3rdparty/php-opencloud/openstack/src/Compute/v2/Models/Fault.php b/3rdparty/php-opencloud/openstack/src/Compute/v2/Models/Fault.php new file mode 100644 index 00000000..f2fc1f5f --- /dev/null +++ b/3rdparty/php-opencloud/openstack/src/Compute/v2/Models/Fault.php @@ -0,0 +1,25 @@ +execute($this->api->getFlavor(), ['id' => (string) $this->id]); + $this->populateFromResponse($response); + } + + public function create(array $userOptions): Creatable + { + $response = $this->execute($this->api->postFlavors(), $userOptions); + + return $this->populateFromResponse($response); + } + + public function delete() + { + $this->execute($this->api->deleteFlavor(), ['id' => (string) $this->id]); + } +} diff --git a/3rdparty/php-opencloud/openstack/src/Compute/v2/Models/Host.php b/3rdparty/php-opencloud/openstack/src/Compute/v2/Models/Host.php new file mode 100644 index 00000000..002502a2 --- /dev/null +++ b/3rdparty/php-opencloud/openstack/src/Compute/v2/Models/Host.php @@ -0,0 +1,39 @@ + 'name', + ]; + + public function retrieve() + { + $response = $this->execute($this->api->getHost(), $this->getAttrs(['name'])); + $this->populateFromResponse($response); + } +} diff --git a/3rdparty/php-opencloud/openstack/src/Compute/v2/Models/Hypervisor.php b/3rdparty/php-opencloud/openstack/src/Compute/v2/Models/Hypervisor.php new file mode 100644 index 00000000..0490c938 --- /dev/null +++ b/3rdparty/php-opencloud/openstack/src/Compute/v2/Models/Hypervisor.php @@ -0,0 +1,102 @@ + 'hostIp', + 'free_disk_gb' => 'freeDiskGb', + 'free_ram_mb' => 'freeRamMb', + 'hypervisor_hostname' => 'hypervisorHostname', + 'hypervisor_type' => 'hypervisorType', + 'hypervisor_version' => 'hypervisorVersion', + 'local_gb' => 'localGb', + 'local_gb_used' => 'localGbUsed', + 'memory_mb' => 'memoryMb', + 'memory_mb_used' => 'memoryMbUsed', + 'running_vms' => 'runningVms', + 'vcpus_used' => 'vcpusUsed', + 'cpu_info' => 'cpuInfo', + 'current_workload' => 'currentWorkload', + 'disk_available_least' => 'diskAvailableLeast', + ]; + + public function retrieve() + { + $response = $this->execute($this->api->getHypervisor(), ['id' => (string) $this->id]); + $this->populateFromResponse($response); + } +} diff --git a/3rdparty/php-opencloud/openstack/src/Compute/v2/Models/HypervisorStatistic.php b/3rdparty/php-opencloud/openstack/src/Compute/v2/Models/HypervisorStatistic.php new file mode 100644 index 00000000..b9bf0e0f --- /dev/null +++ b/3rdparty/php-opencloud/openstack/src/Compute/v2/Models/HypervisorStatistic.php @@ -0,0 +1,29 @@ + new Alias('created', \DateTimeImmutable::class), + 'updated' => new Alias('updated', \DateTimeImmutable::class), + ]; + } + + public function retrieve() + { + $response = $this->execute($this->api->getImage(), ['id' => (string) $this->id]); + $this->populateFromResponse($response); + } + + public function delete() + { + $this->execute($this->api->deleteImage(), ['id' => (string) $this->id]); + } + + /** + * Retrieves metadata from the API. + */ + public function getMetadata(): array + { + $response = $this->execute($this->api->getImageMetadata(), ['id' => $this->id]); + + return $this->parseMetadata($response); + } + + /** + * Resets all the metadata for this image with the values provided. All existing metadata keys + * will either be replaced or removed. + * + * @param array $metadata {@see \OpenStack\Compute\v2\Api::putImageMetadata} + */ + public function resetMetadata(array $metadata) + { + $response = $this->execute($this->api->putImageMetadata(), ['id' => $this->id, 'metadata' => $metadata]); + $this->metadata = $this->parseMetadata($response); + } + + /** + * Merges the existing metadata for the image with the values provided. Any existing keys + * referenced in the user options will be replaced with the user's new values. All other + * existing keys will remain unaffected. + * + * @param array $metadata {@see \OpenStack\Compute\v2\Api::postImageMetadata} + */ + public function mergeMetadata(array $metadata) + { + $response = $this->execute($this->api->postImageMetadata(), ['id' => $this->id, 'metadata' => $metadata]); + $this->metadata = $this->parseMetadata($response); + } + + /** + * Retrieve the value for a specific metadata key. + * + * @param string $key {@see \OpenStack\Compute\v2\Api::getImageMetadataKey} + */ + public function getMetadataItem(string $key) + { + $response = $this->execute($this->api->getImageMetadataKey(), ['id' => $this->id, 'key' => $key]); + $value = $this->parseMetadata($response)[$key]; + $this->metadata[$key] = $value; + + return $value; + } + + /** + * Remove a specific metadata key. + * + * @param string $key {@see \OpenStack\Compute\v2\Api::deleteImageMetadataKey} + */ + public function deleteMetadataItem(string $key) + { + if (isset($this->metadata[$key])) { + unset($this->metadata[$key]); + } + + $this->execute($this->api->deleteImageMetadataKey(), ['id' => $this->id, 'key' => $key]); + } + + public function parseMetadata(ResponseInterface $response): array + { + return Utils::jsonDecode($response)['metadata']; + } +} diff --git a/3rdparty/php-opencloud/openstack/src/Compute/v2/Models/Keypair.php b/3rdparty/php-opencloud/openstack/src/Compute/v2/Models/Keypair.php new file mode 100644 index 00000000..8584be84 --- /dev/null +++ b/3rdparty/php-opencloud/openstack/src/Compute/v2/Models/Keypair.php @@ -0,0 +1,92 @@ + 'publicKey', + 'private_key' => 'privateKey', + 'user_id' => 'userId', + 'type' => 'type', + ]; + + protected $resourceKey = 'keypair'; + protected $resourcesKey = 'keypairs'; + + protected function getAliases(): array + { + return parent::getAliases() + [ + 'created_at' => new Alias('createdAt', \DateTimeImmutable::class), + ]; + } + + public function retrieve() + { + $response = $this->execute($this->api->getKeypair(), $this->getAttrs(['name', 'userId'])); + $this->populateFromResponse($response); + } + + public function create(array $userOptions): Creatable + { + $response = $this->execute($this->api->postKeypair(), $userOptions); + + return $this->populateFromResponse($response); + } + + public function populateFromArray(array $array): self + { + return parent::populateFromArray(Utils::flattenJson($array, $this->resourceKey)); + } + + public function delete() + { + $this->execute($this->api->deleteKeypair(), ['name' => (string) $this->name]); + } +} diff --git a/3rdparty/php-opencloud/openstack/src/Compute/v2/Models/Limit.php b/3rdparty/php-opencloud/openstack/src/Compute/v2/Models/Limit.php new file mode 100644 index 00000000..ad4051b6 --- /dev/null +++ b/3rdparty/php-opencloud/openstack/src/Compute/v2/Models/Limit.php @@ -0,0 +1,23 @@ + 'tenantId', + 'fixed_ips' => 'fixedIps', + 'floating_ips' => 'floatingIps', + 'injected_file_content_bytes' => 'injectedFileContentBytes', + 'injected_file_path_bytes' => 'injectedFilePathBytes', + 'injected_files' => 'injectedFiles', + 'key_pairs' => 'keyPairs', + 'metadata_items' => 'metadataItems', + 'security_group_rules' => 'securityGroupRules', + 'security_groups' => 'securityGroups', + 'server_group_members' => 'serverGroupMembers', + 'server_groups' => 'serverGroups', + ]; + + public function retrieve() + { + $response = $this->execute($this->api->getQuotaSet(), ['tenantId' => (string) $this->tenantId]); + $this->populateFromResponse($response); + } + + public function delete() + { + $response = $this->executeWithState($this->api->deleteQuotaSet()); + $this->populateFromResponse($response); + } + + public function update() + { + $response = $this->executeWithState($this->api->putQuotaSet()); + $this->populateFromResponse($response); + } +} diff --git a/3rdparty/php-opencloud/openstack/src/Compute/v2/Models/Server.php b/3rdparty/php-opencloud/openstack/src/Compute/v2/Models/Server.php new file mode 100644 index 00000000..bb7e73ae --- /dev/null +++ b/3rdparty/php-opencloud/openstack/src/Compute/v2/Models/Server.php @@ -0,0 +1,585 @@ + 'blockDeviceMapping', + 'accessIPv4' => 'ipv4', + 'accessIPv6' => 'ipv6', + 'tenant_id' => 'tenantId', + 'key_name' => 'keyName', + 'user_id' => 'userId', + 'security_groups' => 'securityGroups', + 'OS-EXT-STS:task_state' => 'taskState', + 'OS-EXT-STS:power_state' => 'powerState', + 'OS-EXT-STS:vm_state' => 'vmState', + 'OS-EXT-SRV-ATTR:hypervisor_hostname' => 'hypervisorHostname', + ]; + + protected function getAliases(): array + { + return parent::getAliases() + [ + 'image' => new Alias('image', Image::class), + 'flavor' => new Alias('flavor', Flavor::class), + 'created' => new Alias('created', \DateTimeImmutable::class), + 'updated' => new Alias('updated', \DateTimeImmutable::class), + ]; + } + + /** + * @param array $userOptions {@see \OpenStack\Compute\v2\Api::postServer} + */ + public function create(array $userOptions): Creatable + { + if (!isset($userOptions['imageId']) && !isset($userOptions['blockDeviceMapping'][0]['uuid'])) { + throw new \RuntimeException('imageId or blockDeviceMapping.uuid must be set.'); + } + + $response = $this->execute($this->api->postServer(), $userOptions); + + return $this->populateFromResponse($response); + } + + public function update() + { + $response = $this->execute($this->api->putServer(), $this->getAttrs(['id', 'name', 'ipv4', 'ipv6'])); + $this->populateFromResponse($response); + } + + public function delete() + { + $this->execute($this->api->deleteServer(), $this->getAttrs(['id'])); + } + + public function retrieve() + { + $response = $this->execute($this->api->getServer(), $this->getAttrs(['id'])); + $this->populateFromResponse($response); + } + + /** + * Changes the root password for a server. + * + * @param string $newPassword The new root password + */ + public function changePassword(string $newPassword) + { + $this->execute($this->api->changeServerPassword(), [ + 'id' => $this->id, + 'password' => $newPassword, + ]); + } + + /** + * Issue a resetState call to the server. + */ + public function resetState() + { + $this->execute($this->api->resetServerState(), [ + 'id' => $this->id, + 'resetState' => ['state' => 'active'], + ]); + } + + /** + * Reboots the server. + * + * @param string $type The type of reboot that will be performed. Either SOFT or HARD is supported. + */ + public function reboot(string $type = Enum::REBOOT_SOFT) + { + if (!in_array($type, [Enum::REBOOT_SOFT, Enum::REBOOT_HARD])) { + throw new \RuntimeException('Reboot type must either be SOFT or HARD'); + } + + $this->execute($this->api->rebootServer(), [ + 'id' => $this->id, + 'type' => $type, + ]); + } + + /** + * Starts server. + */ + public function start() + { + $this->execute($this->api->startServer(), [ + 'id' => $this->id, + 'os-start' => null, + ]); + } + + /** + * Stops server. + */ + public function stop() + { + $this->execute($this->api->stopServer(), [ + 'id' => $this->id, + 'os-stop' => null, + ]); + } + + /** + * Resumes server. + */ + public function resume(): void + { + $this->execute($this->api->resumeServer(), [ + 'id' => $this->id, + 'resume' => null, + ]); + } + + /** + * Suspends server. + */ + public function suspend(): void + { + $this->execute($this->api->suspendServer(), [ + 'id' => $this->id, + 'suspend' => null, + ]); + } + + /** + * Rebuilds the server. + * + * @param array $options {@see \OpenStack\Compute\v2\Api::rebuildServer} + */ + public function rebuild(array $options) + { + $options['id'] = $this->id; + $response = $this->execute($this->api->rebuildServer(), $options); + + $this->populateFromResponse($response); + } + + /** + * Rescues the server. + * + * @param array $options {@see \OpenStack\Compute\v2\Api::rescueServer} + */ + public function rescue(array $options): string + { + $options['id'] = $this->id; + $response = $this->execute($this->api->rescueServer(), $options); + + return Utils::jsonDecode($response)['adminPass']; + } + + /** + * Unrescues the server. + */ + public function unrescue() + { + $this->execute($this->api->unrescueServer(), ['unrescue' => null, 'id' => $this->id]); + } + + /** + * Resizes the server to a new flavor. Once this operation is complete and server has transitioned + * to an active state, you will either need to call {@see confirmResize()} or {@see revertResize()}. + * + * @param string $flavorId the UUID of the new flavor your server will be based on + */ + public function resize(string $flavorId) + { + $response = $this->execute($this->api->resizeServer(), [ + 'id' => $this->id, + 'flavorId' => $flavorId, + ]); + + $this->populateFromResponse($response); + } + + /** + * Confirms a previous resize operation. + */ + public function confirmResize() + { + $this->execute($this->api->confirmServerResize(), ['confirmResize' => null, 'id' => $this->id]); + } + + /** + * Reverts a previous resize operation. + */ + public function revertResize() + { + $this->execute($this->api->revertServerResize(), ['revertResize' => null, 'id' => $this->id]); + } + + /** + * Gets the console output of the server. + * + * @param int $length the number of lines, by default all lines will be returned + */ + public function getConsoleOutput(int $length = -1): string + { + $definition = -1 == $length ? $this->api->getAllConsoleOutput() : $this->api->getConsoleOutput(); + + $response = $this->execute($definition, [ + 'os-getConsoleOutput' => new \stdClass(), + 'id' => $this->id, + 'length' => $length, + ]); + + return Utils::jsonDecode($response)['output']; + } + + /** + * Gets a VNC console for a server. + * + * @param string $type the type of VNC console: novnc|xvpvnc. + * Defaults to novnc + */ + public function getVncConsole($type = Enum::CONSOLE_NOVNC): array + { + $response = $this->execute($this->api->getVncConsole(), ['id' => $this->id, 'type' => $type]); + + return Utils::jsonDecode($response)['console']; + } + + /** + * Gets a RDP console for a server. + * + * @param string $type the type of VNC console: rdp-html5 (default) + */ + public function getRDPConsole($type = Enum::CONSOLE_RDP_HTML5): array + { + $response = $this->execute($this->api->getRDPConsole(), ['id' => $this->id, 'type' => $type]); + + return Utils::jsonDecode($response)['console']; + } + + /** + * Gets a Spice console for a server. + * + * @param string $type the type of VNC console: spice-html5 + */ + public function getSpiceConsole($type = Enum::CONSOLE_SPICE_HTML5): array + { + $response = $this->execute($this->api->getSpiceConsole(), ['id' => $this->id, 'type' => $type]); + + return Utils::jsonDecode($response)['console']; + } + + /** + * Gets a serial console for a server. + * + * @param string $type the type of VNC console: serial + */ + public function getSerialConsole($type = Enum::CONSOLE_SERIAL): array + { + $response = $this->execute($this->api->getSerialConsole(), ['id' => $this->id, 'type' => $type]); + + return Utils::jsonDecode($response)['console']; + } + + /** + * Creates an image for the current server. + * + * @param array $options {@see \OpenStack\Compute\v2\Api::createServerImage} + */ + public function createImage(array $options) + { + $options['id'] = $this->id; + $this->execute($this->api->createServerImage(), $options); + } + + /** + * Iterates over all the IP addresses for this server. + * + * @param array $options {@see \OpenStack\Compute\v2\Api::getAddressesByNetwork} + * + * @return array An array containing to two keys: "public" and "private" + */ + public function listAddresses(array $options = []): array + { + $options['id'] = $this->id; + + $data = (isset($options['networkLabel'])) ? $this->api->getAddressesByNetwork() : $this->api->getAddresses(); + $response = $this->execute($data, $options); + + return Utils::jsonDecode($response)['addresses']; + } + + /** + * Returns Generator for InterfaceAttachment. + * + * @return \Generator + */ + public function listInterfaceAttachments(array $options = []): \Generator + { + return $this->model(InterfaceAttachment::class)->enumerate($this->api->getInterfaceAttachments(), ['id' => $this->id]); + } + + /** + * Gets an interface attachment. + * + * @param string $portId the unique ID of the port + */ + public function getInterfaceAttachment(string $portId): InterfaceAttachment + { + $response = $this->execute($this->api->getInterfaceAttachment(), [ + 'id' => $this->id, + 'portId' => $portId, + ]); + + return $this->model(InterfaceAttachment::class)->populateFromResponse($response); + } + + /** + * Creates an interface attachment. + * + * @param array $userOptions {@see \OpenStack\Compute\v2\Api::postInterfaceAttachment} + */ + public function createInterfaceAttachment(array $userOptions): InterfaceAttachment + { + if (!isset($userOptions['networkId']) && !isset($userOptions['portId'])) { + throw new \RuntimeException('networkId or portId must be set.'); + } + + $response = $this->execute($this->api->postInterfaceAttachment(), array_merge($userOptions, ['id' => $this->id])); + + return $this->model(InterfaceAttachment::class)->populateFromResponse($response); + } + + /** + * Detaches an interface attachment. + */ + public function detachInterface(string $portId) + { + $this->execute($this->api->deleteInterfaceAttachment(), [ + 'id' => $this->id, + 'portId' => $portId, + ]); + } + + /** + * Retrieves metadata from the API. + */ + public function getMetadata(): array + { + $response = $this->execute($this->api->getServerMetadata(), ['id' => $this->id]); + + return $this->parseMetadata($response); + } + + /** + * Resets all the metadata for this server with the values provided. All existing metadata keys + * will either be replaced or removed. + * + * @param array $metadata {@see \OpenStack\Compute\v2\Api::putServerMetadata} + */ + public function resetMetadata(array $metadata) + { + $response = $this->execute($this->api->putServerMetadata(), ['id' => $this->id, 'metadata' => $metadata]); + $this->metadata = $this->parseMetadata($response); + } + + /** + * Merges the existing metadata for the server with the values provided. Any existing keys + * referenced in the user options will be replaced with the user's new values. All other + * existing keys will remain unaffected. + * + * @param array $metadata {@see \OpenStack\Compute\v2\Api::postServerMetadata} + */ + public function mergeMetadata(array $metadata) + { + $response = $this->execute($this->api->postServerMetadata(), ['id' => $this->id, 'metadata' => $metadata]); + $this->metadata = $this->parseMetadata($response); + } + + /** + * Retrieve the value for a specific metadata key. + * + * @param string $key {@see \OpenStack\Compute\v2\Api::getServerMetadataKey} + */ + public function getMetadataItem(string $key) + { + $response = $this->execute($this->api->getServerMetadataKey(), ['id' => $this->id, 'key' => $key]); + $value = $this->parseMetadata($response)[$key]; + $this->metadata[$key] = $value; + + return $value; + } + + /** + * Remove a specific metadata key. + * + * @param string $key {@see \OpenStack\Compute\v2\Api::deleteServerMetadataKey} + */ + public function deleteMetadataItem(string $key) + { + if (isset($this->metadata[$key])) { + unset($this->metadata[$key]); + } + + $this->execute($this->api->deleteServerMetadataKey(), ['id' => $this->id, 'key' => $key]); + } + + /** + * Add security group to a server (addSecurityGroup action). + * + * @param array $options {@see \OpenStack\Compute\v2\Api::postSecurityGroup} + */ + public function addSecurityGroup(array $options): SecurityGroup + { + $options['id'] = $this->id; + + $response = $this->execute($this->api->postSecurityGroup(), $options); + + return $this->model(SecurityGroup::class)->populateFromResponse($response); + } + + /** + * Add security group to a server (addSecurityGroup action). + * + * @param array $options {@see \OpenStack\Compute\v2\Api::deleteSecurityGroup} + */ + public function removeSecurityGroup(array $options) + { + $options['id'] = $this->id; + $this->execute($this->api->deleteSecurityGroup(), $options); + } + + public function parseMetadata(ResponseInterface $response): array + { + return Utils::jsonDecode($response)['metadata']; + } + + /** + * Returns Generator for SecurityGroups. + * + * @return \Generator + */ + public function listSecurityGroups(): \Generator + { + return $this->model(SecurityGroup::class)->enumerate($this->api->getSecurityGroups(), ['id' => $this->id]); + } + + /** + * Returns Generator for VolumeAttachment. + * + * @return \Generator + */ + public function listVolumeAttachments(): \Generator + { + return $this->model(VolumeAttachment::class)->enumerate($this->api->getVolumeAttachments(), ['id' => $this->id]); + } + + /** + * Attach a volume and returns volume that was attached. + */ + public function attachVolume(string $volumeId): VolumeAttachment + { + $response = $this->execute($this->api->postVolumeAttachments(), ['id' => $this->id, 'volumeId' => $volumeId]); + + return $this->model(VolumeAttachment::class)->populateFromResponse($response); + } + + /** + * Detach a volume. + */ + public function detachVolume(string $attachmentId) + { + $this->execute($this->api->deleteVolumeAttachments(), ['id' => $this->id, 'attachmentId' => $attachmentId]); + } +} diff --git a/3rdparty/php-opencloud/openstack/src/Compute/v2/Params.php b/3rdparty/php-opencloud/openstack/src/Compute/v2/Params.php new file mode 100644 index 00000000..b8a8b73e --- /dev/null +++ b/3rdparty/php-opencloud/openstack/src/Compute/v2/Params.php @@ -0,0 +1,665 @@ + true, + 'location' => self::URL, + 'documented' => false, + ]); + } + + public function resetState(): array + { + return [ + 'type' => self::OBJECT_TYPE, + 'location' => self::JSON, + 'sentAs' => 'os-resetState', + 'required' => true, + 'properties' => [ + 'state' => ['type' => self::STRING_TYPE], + ], + ]; + } + + public function minDisk(): array + { + return [ + 'type' => self::INT_TYPE, + 'location' => self::QUERY, + 'description' => 'Return flavors that have a minimum disk space in GB.', + ]; + } + + public function minRam(): array + { + return [ + 'type' => self::INT_TYPE, + 'location' => self::QUERY, + 'description' => 'Return flavors that have a minimum RAM size in GB.', + ]; + } + + public function flavorName(): array + { + return [ + 'location' => self::QUERY, + 'description' => 'Return images which match a certain name.', + ]; + } + + public function filterChangesSince($type) + { + return [ + 'location' => self::QUERY, + 'sentAs' => 'changes-since', + 'description' => sprintf( + 'Return %ss which have been changed since a certain time. This value needs to be in an ISO 8601 format.', + $type + ), + ]; + } + + public function flavorServer(): array + { + return [ + 'location' => self::QUERY, + 'description' => sprintf('Return images which are associated with a server. This value needs to be in a URL format.'), + ]; + } + + public function filterStatus(string $type): array + { + return [ + 'location' => self::QUERY, + 'description' => sprintf( + 'Return %ss that have a particular status, such as "ACTIVE".', + $type + ), + ]; + } + + public function flavorType(): array + { + return [ + 'location' => self::QUERY, + 'description' => 'Return images that are of a particular type, such as "snapshot" or "backup".', + ]; + } + + public function key(): array + { + return [ + 'type' => self::STRING_TYPE, + 'location' => self::URL, + 'required' => true, + 'description' => 'The specific metadata key you are interacting with', + ]; + } + + public function ipv4(): array + { + return [ + 'type' => self::STRING_TYPE, + 'location' => self::JSON, + 'sentAs' => 'accessIPv4', + 'description' => 'The IP address (version 4) of the remote resource', + ]; + } + + public function ipv6(): array + { + return [ + 'type' => self::STRING_TYPE, + 'location' => self::JSON, + 'sentAs' => 'accessIPv6', + 'description' => 'The IP address (version 6) of the remote resource', + ]; + } + + public function imageId(): array + { + return [ + 'type' => self::STRING_TYPE, + 'required' => true, + 'sentAs' => 'imageRef', + 'description' => 'The UUID of the image to use for your server instance. This is not required in case of boot from volume. In all other cases it is required and must be a valid UUID', + ]; + } + + public function rescueImageId(): array + { + return [ + 'type' => self::STRING_TYPE, + 'required' => true, + 'sentAs' => 'rescue_image_ref', + 'description' => 'The image reference to use to rescue your server instance. Specify the image reference by ID or full URL. If you omit an image reference, default is the base image reference', + ]; + } + + public function flavorId(): array + { + return [ + 'type' => self::STRING_TYPE, + 'required' => true, + 'sentAs' => 'flavorRef', + 'description' => 'The unique ID of the flavor that this server will be based on', + ]; + } + + public function networkId(): array + { + return [ + 'type' => self::STRING_TYPE, + 'required' => true, + 'sentAs' => 'net_id', + 'description' => 'The unique ID of a network', + ]; + } + + public function portId(): array + { + return [ + 'type' => self::STRING_TYPE, + 'required' => true, + 'sentAs' => 'port_id', + 'description' => 'The unique ID of a port', + ]; + } + + public function tag(): array + { + return [ + 'type' => self::STRING_TYPE, + ]; + } + + public function fixedIpAddresses(): array + { + return [ + 'type' => self::ARRAY_TYPE, + 'sentAs' => 'fixed_ips', + 'description' => 'A list of ip addresses which this interface will be associated with', + 'items' => [ + 'type' => self::OBJECT_TYPE, + 'properties' => ['ip_address' => ['type' => self::STRING_TYPE]], + ], + ]; + } + + public function metadata(): array + { + return [ + 'type' => self::OBJECT_TYPE, + 'location' => self::JSON, + 'required' => true, + 'description' => 'An arbitrary key/value pairing that will be used for metadata.', + 'properties' => [ + 'type' => self::STRING_TYPE, + 'description' => << self::ARRAY_TYPE, + 'items' => [ + 'type' => self::OBJECT_TYPE, + 'properties' => [ + 'path' => [ + 'type' => self::STRING_TYPE, + 'description' => 'The path, on the filesystem, where the personality file will be placed', + ], + 'contents' => [ + 'type' => self::STRING_TYPE, + 'description' => 'Base64-encoded content of the personality file', + ], + ], + ], + 'description' => << self::ARRAY_TYPE, + 'sentAs' => 'security_groups', + 'description' => 'A list of security group objects which this server will be associated with', + 'items' => [ + 'type' => self::OBJECT_TYPE, + 'properties' => ['name' => $this->name('security group')], + ], + ]; + } + + public function userData(): array + { + return [ + 'type' => self::STRING_TYPE, + 'sentAs' => 'user_data', + 'description' => 'Configuration information or scripts to use upon launch. Must be Base64 encoded.', + ]; + } + + public function availabilityZone(): array + { + return [ + 'type' => self::STRING_TYPE, + 'sentAs' => 'availability_zone', + 'description' => 'The availability zone in which to launch the server.', + ]; + } + + public function networks(): array + { + return [ + 'type' => self::ARRAY_TYPE, + 'description' => << [ + 'type' => self::OBJECT_TYPE, + 'properties' => [ + 'uuid' => [ + 'type' => self::STRING_TYPE, + 'description' => << [ + 'type' => self::STRING_TYPE, + 'description' => << self::ARRAY_TYPE, + 'sentAs' => 'block_device_mapping_v2', + 'description' => << [ + 'type' => self::OBJECT_TYPE, + 'properties' => [ + 'uuid' => [ + 'type' => self::STRING_TYPE, + 'description' => 'The unique ID for the volume which the server is to be booted from.', + ], + 'bootIndex' => [ + 'type' => self::INT_TYPE, + 'sentAs' => 'boot_index', + 'description' => 'Indicates a number designating the boot order of the device. Use -1 for the boot volume, choose 0 for an attached volume.', + ], + 'deleteOnTermination' => [ + 'type' => self::BOOL_TYPE, + 'sentAs' => 'delete_on_termination', + 'description' => 'To delete the boot volume when the server stops, specify true. Otherwise, specify false.', + ], + 'guestFormat' => [ + 'type' => self::STRING_TYPE, + 'sentAs' => 'guest_format', + 'description' => 'Specifies the guest server disk file system format, such as "ephemeral" or "swap".', + ], + 'destinationType' => [ + 'type' => self::STRING_TYPE, + 'sentAs' => 'destination_type', + 'description' => 'Describes where the volume comes from. Choices are "local" or "volume". When using "volume" the volume ID', + ], + 'sourceType' => [ + 'type' => self::STRING_TYPE, + 'sentAs' => 'source_type', + 'description' => 'Describes the volume source type for the volume. Choices are "blank", "snapshot", "volume", or "image".', + ], + 'deviceName' => [ + 'type' => self::STRING_TYPE, + 'sentAs' => 'device_name', + 'description' => 'Describes a path to the device for the volume you want to use to boot the server.', + ], + 'volumeSize' => [ + 'type' => self::INT_TYPE, + 'sentAs' => 'volume_size', + 'description' => 'Size of the volume created if we are doing vol creation', + ], + 'volumeType' => [ + 'type' => self::STRING_TYPE, + 'sentAs' => 'volume_type', + 'description' => 'The type of volume which the compute service will create and attach to the server.', + ], + ], + ], + ]; + } + + public function filterHost(): array + { + return [ + 'type' => self::STRING_TYPE, + 'location' => self::QUERY, + 'description' => '', + ]; + } + + public function filterName(): array + { + return [ + 'type' => self::STRING_TYPE, + 'location' => self::QUERY, + 'description' => '', + ]; + } + + public function filterFlavor(): array + { + return [ + 'sentAs' => 'flavor', + 'type' => self::STRING_TYPE, + 'location' => self::QUERY, + 'description' => '', + ]; + } + + public function filterImage(): array + { + return [ + 'sentAs' => 'image', + 'type' => self::STRING_TYPE, + 'location' => self::QUERY, + 'description' => '', + ]; + } + + public function password(): array + { + return [ + 'sentAs' => 'adminPass', + 'type' => self::STRING_TYPE, + 'location' => self::JSON, + 'required' => true, + 'description' => '', + ]; + } + + public function rebootType(): array + { + return [ + 'type' => self::STRING_TYPE, + 'location' => self::JSON, + 'required' => true, + 'description' => '', + ]; + } + + public function nullAction(): array + { + return [ + 'type' => self::NULL_TYPE, + 'location' => self::JSON, + 'required' => true, + ]; + } + + public function networkLabel(): array + { + return [ + 'type' => self::STRING_TYPE, + 'location' => self::URL, + 'required' => true, + ]; + } + + public function keyName(): array + { + return [ + 'type' => self::STRING_TYPE, + 'required' => false, + 'sentAs' => 'key_name', + 'description' => 'The key name', + ]; + } + + public function keypairPublicKey(): array + { + return [ + 'type' => self::STRING_TYPE, + 'sentAs' => 'public_key', + 'location' => self::JSON, + 'description' => 'The public ssh key to import. If you omit this value, a key is generated.', + ]; + } + + public function keypairName(): array + { + return [ + 'location' => self::URL, + ]; + } + + public function userId(): array + { + return [ + 'type' => self::STRING_TYPE, + 'sentAs' => 'user_id', + 'location' => self::QUERY, + 'description' => 'This allows administrative users to operate key-pairs of specified user ID. Requires micro version 2.10.', + ]; + } + + public function keypairUserId(): array + { + return [ + 'type' => self::STRING_TYPE, + 'sentAs' => 'user_id', + 'location' => self::JSON, + 'description' => 'This allows administrative users to upload keys for other users than themselves. Requires micro version 2.10.', + ]; + } + + public function keypairType(): array + { + return [ + 'type' => self::STRING_TYPE, + 'location' => self::JSON, + 'description' => 'The type of the keypair. Allowed values are ssh or x509. Require micro version 2.2.', + ]; + } + + public function flavorRam(): array + { + return [ + 'type' => self::INT_TYPE, + 'location' => self::JSON, + ]; + } + + public function flavorVcpus(): array + { + return [ + 'type' => self::INT_TYPE, + 'location' => self::JSON, + ]; + } + + public function flavorDisk(): array + { + return [ + 'type' => self::INT_TYPE, + 'location' => self::JSON, + ]; + } + + public function flavorSwap(): array + { + return [ + 'type' => self::INT_TYPE, + 'location' => self::JSON, + ]; + } + + public function volumeId(): array + { + return [ + 'type' => self::STRING_TYPE, + 'location' => self::JSON, + ]; + } + + public function attachmentId(): array + { + return [ + 'type' => self::STRING_TYPE, + 'location' => self::URL, + 'required' => true, + ]; + } + + public function consoleType(): array + { + return [ + 'type' => self::STRING_TYPE, + 'location' => self::JSON, + 'required' => true, + ]; + } + + public function consoleLogLength(): array + { + return [ + 'type' => self::INT_TYPE, + 'location' => self::JSON, + 'required' => false, + ]; + } + + public function emptyObject(): array + { + return [ + 'type' => self::OBJECT_TYPE, + ]; + } + + protected function quotaSetLimit($sentAs, $description): array + { + return [ + 'type' => self::INT_TYPE, + 'location' => self::JSON, + 'sentAs' => $sentAs, + 'description' => $description, + ]; + } + + public function quotaSetLimitForce(): array + { + return [ + 'type' => self::BOOLEAN_TYPE, + 'location' => self::JSON, + 'sentAs' => 'force', + 'description' => 'You can force the update even if the quota has already been used and the reserved quota exceeds the new quota', + ]; + } + + public function quotaSetLimitInstances(): array + { + return $this->quotaSetLimit('instances', 'The number of allowed instances for each tenant.'); + } + + public function quotaSetLimitCores(): array + { + return $this->quotaSetLimit('cores', 'The number of allowed instance cores for each tenant.'); + } + + public function quotaSetLimitFixedIps(): array + { + return $this->quotaSetLimit('fixed_ips', 'The number of allowed fixed IP addresses for each tenant. Must be equal to or greater than the number of allowed instances.'); + } + + public function quotaSetLimitFloatingIps(): array + { + return $this->quotaSetLimit('floating_ips', 'The number of allowed floating IP addresses for each tenant.'); + } + + public function quotaSetLimitInjectedFileContentBytes(): array + { + return $this->quotaSetLimit('injected_file_content_bytes', 'The number of allowed bytes of content for each injected file.'); + } + + public function quotaSetLimitInjectedFilePathBytes(): array + { + return $this->quotaSetLimit('injected_file_path_bytes', 'The number of allowed bytes for each injected file path.'); + } + + public function quotaSetLimitInjectedFiles(): array + { + return $this->quotaSetLimit('injected_files', 'The number of allowed injected files for each tenant.'); + } + + public function quotaSetLimitKeyPairs(): array + { + return $this->quotaSetLimit('key_pairs', 'The number of allowed key pairs for each user.'); + } + + public function quotaSetLimitMetadataItems(): array + { + return $this->quotaSetLimit('metadata_items', 'The number of allowed metadata items for each instance.'); + } + + public function quotaSetLimitRam(): array + { + return $this->quotaSetLimit('ram', 'The amount of allowed instance RAM (in MB) for each tenant.'); + } + + public function quotaSetLimitSecurityGroupRules(): array + { + return $this->quotaSetLimit('security_group_rules', 'The number of allowed rules for each security group.'); + } + + public function quotaSetLimitSecurityGroups(): array + { + return $this->quotaSetLimit('security_groups', 'The number of allowed security groups for each tenant.'); + } + + public function quotaSetLimitServerGroups(): array + { + return $this->quotaSetLimit('server_groups', 'The number of allowed server groups for each tenant.'); + } + + public function quotaSetLimitServerGroupMembers(): array + { + return $this->quotaSetLimit('server_group_members', 'The number of allowed members for each server group.'); + } +} diff --git a/3rdparty/php-opencloud/openstack/src/Compute/v2/Service.php b/3rdparty/php-opencloud/openstack/src/Compute/v2/Service.php new file mode 100644 index 00000000..968b22eb --- /dev/null +++ b/3rdparty/php-opencloud/openstack/src/Compute/v2/Service.php @@ -0,0 +1,278 @@ +model(Server::class)->create($options); + } + + /** + * List servers. + * + * @param bool $detailed Determines whether detailed information will be returned. If FALSE is specified, only + * the ID, name and links attributes are returned, saving bandwidth. + * @param array $options {@see \OpenStack\Compute\v2\Api::getServers} + * @param callable|null $mapFn a callable function that will be invoked on every iteration of the list + * + * @return \Generator + */ + public function listServers(bool $detailed = false, array $options = [], ?callable $mapFn = null): \Generator + { + $def = (true === $detailed) ? $this->api->getServersDetail() : $this->api->getServers(); + + return $this->model(Server::class)->enumerate($def, $options, $mapFn); + } + + /** + * Retrieve a server object without calling the remote API. Any values provided in the array will populate the + * empty object, allowing you greater control without the expense of network transactions. To call the remote API + * and have the response populate the object, call {@see Server::retrieve}. For example:. + * + * $server = $service->getServer(['id' => '{serverId}']); + * + * @param array $options An array of attributes that will be set on the {@see Server} object. The array keys need to + * correspond to the class public properties. + */ + public function getServer(array $options = []): Server + { + $server = $this->model(Server::class); + $server->populateFromArray($options); + + return $server; + } + + /** + * List flavors. + * + * @param array $options {@see \OpenStack\Compute\v2\Api::getFlavors} + * @param callable|null $mapFn a callable function that will be invoked on every iteration of the list + * @param bool $detailed set to true to fetch flavors' details + * + * @return \Generator + */ + public function listFlavors(array $options = [], ?callable $mapFn = null, bool $detailed = false): \Generator + { + $def = true === $detailed ? $this->api->getFlavorsDetail() : $this->api->getFlavors(); + + return $this->model(Flavor::class)->enumerate($def, $options, $mapFn); + } + + /** + * Retrieve a flavor object without calling the remote API. Any values provided in the array will populate the + * empty object, allowing you greater control without the expense of network transactions. To call the remote API + * and have the response populate the object, call {@see Flavor::retrieve}. + * + * @param array $options An array of attributes that will be set on the {@see Flavor} object. The array keys need to + * correspond to the class public properties. + */ + public function getFlavor(array $options = []): Flavor + { + $flavor = $this->model(Flavor::class); + $flavor->populateFromArray($options); + + return $flavor; + } + + /** + * Create a new flavor resource. + * + * @param array $options {@see \OpenStack\Compute\v2\Api::postFlavors} + */ + public function createFlavor(array $options = []): Flavor + { + return $this->model(Flavor::class)->create($options); + } + + /** + * List images. + * + * @param array $options {@see \OpenStack\Compute\v2\Api::getImages} + * @param callable|null $mapFn a callable function that will be invoked on every iteration of the list + * + * @return \Generator + */ + public function listImages(array $options = [], ?callable $mapFn = null): \Generator + { + return $this->model(Image::class)->enumerate($this->api->getImages(), $options, $mapFn); + } + + /** + * Retrieve an image object without calling the remote API. Any values provided in the array will populate the + * empty object, allowing you greater control without the expense of network transactions. To call the remote API + * and have the response populate the object, call {@see Image::retrieve}. + * + * @param array $options An array of attributes that will be set on the {@see Image} object. The array keys need to + * correspond to the class public properties. + */ + public function getImage(array $options = []): Image + { + $image = $this->model(Image::class); + $image->populateFromArray($options); + + return $image; + } + + /** + * List key pairs. + * + * @param array $options {@see \OpenStack\Compute\v2\Api::getKeyPairs} + * @param callable|null $mapFn a callable function that will be invoked on every iteration of the list + * + * @return \Generator + */ + public function listKeypairs(array $options = [], ?callable $mapFn = null): \Generator + { + return $this->model(Keypair::class)->enumerate($this->api->getKeypairs(), $options, $mapFn); + } + + /** + * Create or import keypair. + */ + public function createKeypair(array $options): Keypair + { + return $this->model(Keypair::class)->create($options); + } + + /** + * Get keypair. + */ + public function getKeypair(array $options = []): Keypair + { + $keypair = $this->model(Keypair::class); + $keypair->populateFromArray($options); + + return $keypair; + } + + /** + * Shows rate and absolute limits for the tenant. + */ + public function getLimits(): Limit + { + $limits = $this->model(Limit::class); + $limits->populateFromResponse($this->execute($this->api->getLimits(), [])); + + return $limits; + } + + /** + * Shows summary statistics for all hypervisors over all compute nodes. + */ + public function getHypervisorStatistics(): HypervisorStatistic + { + $statistics = $this->model(HypervisorStatistic::class); + $statistics->populateFromResponse($this->execute($this->api->getHypervisorStatistics(), [])); + + return $statistics; + } + + /** + * List hypervisors. + * + * @param bool $detailed Determines whether detailed information will be returned. If FALSE is specified, only + * the ID, name and links attributes are returned, saving bandwidth. + * @param array $options {@see \OpenStack\Compute\v2\Api::getHypervisors} + * @param callable|null $mapFn a callable function that will be invoked on every iteration of the list + * + * @return \Generator + */ + public function listHypervisors(bool $detailed = false, array $options = [], ?callable $mapFn = null): \Generator + { + $def = (true === $detailed) ? $this->api->getHypervisorsDetail() : $this->api->getHypervisors(); + + return $this->model(Hypervisor::class)->enumerate($def, $options, $mapFn); + } + + /** + * Shows details for a given hypervisor. + */ + public function getHypervisor(array $options = []): Hypervisor + { + $hypervisor = $this->model(Hypervisor::class); + + return $hypervisor->populateFromArray($options); + } + + /** + * List hosts. + * + * @param array $options {@see \OpenStack\Compute\v2\Api::getHosts} + * @param callable|null $mapFn a callable function that will be invoked on every iteration of the list + * + * @return \Generator + */ + public function listHosts(array $options = [], ?callable $mapFn = null): \Generator + { + return $this->model(Host::class)->enumerate($this->api->getHosts(), $options, $mapFn); + } + + /** + * Retrieve a host object without calling the remote API. Any values provided in the array will populate the + * empty object, allowing you greater control without the expense of network transactions. To call the remote API + * and have the response populate the object, call {@see Host::retrieve}. For example:. + * + * $server = $service->getHost(['name' => '{name}']); + * + * @param array $options An array of attributes that will be set on the {@see Host} object. The array keys need to + * correspond to the class public properties. + */ + public function getHost(array $options = []): Host + { + $host = $this->model(Host::class); + $host->populateFromArray($options); + + return $host; + } + + /** + * List AZs. + * + * @param array $options {@see \OpenStack\Compute\v2\Api::getAvailabilityZones} + * @param callable|null $mapFn a callable function that will be invoked on every iteration of the list + * + * @return \Generator + */ + public function listAvailabilityZones(array $options = [], ?callable $mapFn = null): \Generator + { + return $this->model(AvailabilityZone::class)->enumerate($this->api->getAvailabilityZones(), $options, $mapFn); + } + + /** + * Shows A Quota for a tenant. + */ + public function getQuotaSet(string $tenantId, bool $detailed = false): QuotaSet + { + $quotaSet = $this->model(QuotaSet::class); + $quotaSet->populateFromResponse($this->execute($detailed ? $this->api->getQuotaSetDetail() : $this->api->getQuotaSet(), ['tenantId' => $tenantId])); + + return $quotaSet; + } +} diff --git a/3rdparty/php-opencloud/openstack/src/Identity/v2/Api.php b/3rdparty/php-opencloud/openstack/src/Identity/v2/Api.php new file mode 100644 index 00000000..f22c970d --- /dev/null +++ b/3rdparty/php-opencloud/openstack/src/Identity/v2/Api.php @@ -0,0 +1,42 @@ + 'POST', + 'path' => 'tokens', + 'skipAuth' => true, + 'params' => [ + 'username' => [ + 'type' => 'string', + 'required' => true, + 'path' => 'auth.passwordCredentials', + ], + 'password' => [ + 'type' => 'string', + 'required' => true, + 'path' => 'auth.passwordCredentials', + ], + 'tenantId' => [ + 'type' => 'string', + 'path' => 'auth', + ], + 'tenantName' => [ + 'type' => 'string', + 'path' => 'auth', + ], + ], + ]; + } +} diff --git a/3rdparty/php-opencloud/openstack/src/Identity/v2/Models/Catalog.php b/3rdparty/php-opencloud/openstack/src/Identity/v2/Models/Catalog.php new file mode 100644 index 00000000..91de2d2b --- /dev/null +++ b/3rdparty/php-opencloud/openstack/src/Identity/v2/Models/Catalog.php @@ -0,0 +1,58 @@ + new Alias('entries', Entry::class, true), + ]; + } + + public function populateFromResponse(ResponseInterface $response): self + { + $entries = Utils::jsonDecode($response)['access']['serviceCatalog']; + + foreach ($entries as $entry) { + $this->entries[] = $this->model(Entry::class, $entry); + } + + return $this; + } + + public function getServiceUrl( + string $serviceName, + string $serviceType, + string $region, + string $urlType = self::DEFAULT_URL_TYPE + ): string { + foreach ($this->entries as $entry) { + if ($entry->matches($serviceName, $serviceType) && ($url = $entry->getEndpointUrl($region, $urlType))) { + return $url; + } + } + + throw new \RuntimeException(sprintf("Endpoint URL could not be found in the catalog for this service.\nName: %s\nType: %s\nRegion: %s\nURL type: %s", $serviceName, $serviceType, $region, $urlType)); + } +} diff --git a/3rdparty/php-opencloud/openstack/src/Identity/v2/Models/Endpoint.php b/3rdparty/php-opencloud/openstack/src/Identity/v2/Models/Endpoint.php new file mode 100644 index 00000000..b318a58b --- /dev/null +++ b/3rdparty/php-opencloud/openstack/src/Identity/v2/Models/Endpoint.php @@ -0,0 +1,86 @@ + 'adminUrl', + 'internalURL' => 'internalUrl', + 'publicURL' => 'publicUrl', + ]; + + /** + * Indicates whether a given region is supported. + */ + public function supportsRegion(string $region): bool + { + return $this->region == $region; + } + + /** + * Indicates whether a given URL type is supported. + */ + public function supportsUrlType(string $urlType): bool + { + $supported = false; + + switch (strtolower($urlType)) { + case 'internalurl': + case 'publicurl': + case 'adminurl': + $supported = true; + break; + } + + return $supported; + } + + /** + * Retrieves a URL for the endpoint based on a specific type. + * + * @param string $urlType Either "internalURL", "publicURL" or "adminURL" (case insensitive) + * + * @return bool|string + */ + public function getUrl(string $urlType): string + { + $url = false; + + switch (strtolower($urlType)) { + case 'internalurl': + $url = $this->internalUrl; + break; + case 'publicurl': + $url = $this->publicUrl; + break; + case 'adminurl': + $url = $this->adminUrl; + break; + } + + return $url; + } +} diff --git a/3rdparty/php-opencloud/openstack/src/Identity/v2/Models/Entry.php b/3rdparty/php-opencloud/openstack/src/Identity/v2/Models/Entry.php new file mode 100644 index 00000000..2c101688 --- /dev/null +++ b/3rdparty/php-opencloud/openstack/src/Identity/v2/Models/Entry.php @@ -0,0 +1,54 @@ + new Alias('endpoints', Endpoint::class, true), + ]; + } + + /** + * Indicates whether this catalog entry matches a certain name and type. + * + * @return bool TRUE if it's a match, FALSE if not + */ + public function matches(string $name, string $type): bool + { + return $this->name == $name && $this->type == $type; + } + + /** + * Retrieves the catalog entry's URL according to a specific region and URL type. + */ + public function getEndpointUrl(string $region, string $urlType): string + { + foreach ($this->endpoints as $endpoint) { + if ($endpoint->supportsRegion($region) && $endpoint->supportsUrlType($urlType)) { + return $endpoint->getUrl($urlType); + } + } + + return ''; + } +} diff --git a/3rdparty/php-opencloud/openstack/src/Identity/v2/Models/Tenant.php b/3rdparty/php-opencloud/openstack/src/Identity/v2/Models/Tenant.php new file mode 100644 index 00000000..bd83cb6b --- /dev/null +++ b/3rdparty/php-opencloud/openstack/src/Identity/v2/Models/Tenant.php @@ -0,0 +1,14 @@ + new Alias('tenant', Tenant::class), + 'expires' => new Alias('expires', \DateTimeImmutable::class), + 'issued_at' => new Alias('issuedAt', \DateTimeImmutable::class), + ]; + } + + public function populateFromResponse(ResponseInterface $response): self + { + $this->populateFromArray(Utils::jsonDecode($response)['access']['token']); + + return $this; + } + + public function getId(): string + { + return $this->id; + } + + public function hasExpired(): bool + { + return $this->expires <= new \DateTimeImmutable('now', $this->expires->getTimezone()); + } +} diff --git a/3rdparty/php-opencloud/openstack/src/Identity/v2/Service.php b/3rdparty/php-opencloud/openstack/src/Identity/v2/Service.php new file mode 100644 index 00000000..620f6b94 --- /dev/null +++ b/3rdparty/php-opencloud/openstack/src/Identity/v2/Service.php @@ -0,0 +1,54 @@ +api->postToken(); + + $response = $this->execute($definition, array_intersect_key($options, $definition['params'])); + + $token = $this->model(Token::class, $response); + + $serviceUrl = $this->model(Catalog::class, $response)->getServiceUrl( + $options['catalogName'], + $options['catalogType'], + $options['region'], + $options['urlType'] + ); + + return [$token, $serviceUrl]; + } + + /** + * Generates a new authentication token. + * + * @param array $options {@see \OpenStack\Identity\v2\Api::postToken} + */ + public function generateToken(array $options = []): Token + { + $response = $this->execute($this->api->postToken(), $options); + + return $this->model(Token::class, $response); + } +} diff --git a/3rdparty/php-opencloud/openstack/src/Identity/v3/Api.php b/3rdparty/php-opencloud/openstack/src/Identity/v3/Api.php new file mode 100644 index 00000000..418684c9 --- /dev/null +++ b/3rdparty/php-opencloud/openstack/src/Identity/v3/Api.php @@ -0,0 +1,882 @@ +params = new Params(); + } + + public function postTokens(): array + { + return [ + 'method' => 'POST', + 'path' => 'auth/tokens', + 'skipAuth' => true, + 'params' => [ + 'methods' => $this->params->methods(), + 'user' => $this->params->user(), + 'application_credential' => $this->params->applicationCredential(), + 'tokenId' => $this->params->tokenBody(), + 'scope' => $this->params->scope(), + ], + ]; + } + + public function getTokens(): array + { + return [ + 'method' => 'GET', + 'path' => 'auth/tokens', + 'params' => ['tokenId' => $this->params->tokenId()], + ]; + } + + public function headTokens(): array + { + return [ + 'method' => 'HEAD', + 'path' => 'auth/tokens', + 'params' => ['tokenId' => $this->params->tokenId()], + ]; + } + + public function deleteTokens(): array + { + return [ + 'method' => 'DELETE', + 'path' => 'auth/tokens', + 'params' => ['tokenId' => $this->params->tokenId()], + ]; + } + + public function postServices(): array + { + return [ + 'method' => 'POST', + 'path' => 'services', + 'jsonKey' => 'service', + 'params' => [ + 'name' => $this->params->name('service'), + 'type' => $this->params->type('service'), + 'description' => $this->params->desc('service'), + ], + ]; + } + + public function getServices(): array + { + return [ + 'method' => 'GET', + 'path' => 'services', + 'params' => ['type' => $this->params->typeQuery()], + ]; + } + + public function getService(): array + { + return [ + 'method' => 'GET', + 'path' => 'services/{id}', + 'params' => ['id' => $this->params->idUrl('service')], + ]; + } + + public function patchService(): array + { + return [ + 'method' => 'PATCH', + 'path' => 'services/{id}', + 'jsonKey' => 'service', + 'params' => [ + 'id' => $this->params->idUrl('service'), + 'name' => $this->params->name('service'), + 'type' => $this->params->type('service'), + 'description' => $this->params->desc('service'), + ], + ]; + } + + public function deleteService(): array + { + return [ + 'method' => 'DELETE', + 'path' => 'services/{id}', + 'params' => ['id' => $this->params->idUrl('service')], + ]; + } + + public function postEndpoints(): array + { + return [ + 'method' => 'POST', + 'path' => 'endpoints', + 'jsonKey' => 'endpoint', + 'params' => [ + 'interface' => $this->params->interf(), + 'name' => $this->isRequired($this->params->name('endpoint')), + 'region' => $this->params->region(), + 'url' => $this->params->endpointUrl(), + 'serviceId' => $this->params->serviceId(), + ], + ]; + } + + public function getEndpoints(): array + { + return [ + 'method' => 'GET', + 'path' => 'endpoints', + 'params' => [ + 'interface' => $this->query($this->params->interf()), + 'serviceId' => $this->query($this->params->serviceId()), + ], + ]; + } + + public function getEndpoint(): array + { + return [ + 'method' => 'GET', + 'path' => 'endpoints/{id}', + 'params' => [ + 'id' => $this->params->idUrl('service'), + ], + ]; + } + + public function patchEndpoint(): array + { + return [ + 'method' => 'PATCH', + 'path' => 'endpoints/{id}', + 'jsonKey' => 'endpoint', + 'params' => [ + 'id' => $this->params->idUrl('endpoint'), + 'interface' => $this->params->interf(), + 'name' => $this->params->name('endpoint'), + 'region' => $this->params->region(), + 'url' => $this->params->endpointUrl(), + 'serviceId' => $this->params->serviceId(), + ], + ]; + } + + public function deleteEndpoint(): array + { + return [ + 'method' => 'DELETE', + 'path' => 'endpoints/{id}', + 'params' => ['id' => $this->params->idUrl('endpoint')], + ]; + } + + public function postDomains(): array + { + return [ + 'method' => 'POST', + 'path' => 'domains', + 'jsonKey' => 'domain', + 'params' => [ + 'name' => $this->isRequired($this->params->name('domain')), + 'enabled' => $this->params->enabled('domain'), + 'description' => $this->params->desc('domain'), + ], + ]; + } + + public function getDomains(): array + { + return [ + 'method' => 'GET', + 'path' => 'domains', + 'params' => [ + 'name' => $this->query($this->params->name('domain')), + 'enabled' => $this->query($this->params->enabled('domain')), + ], + ]; + } + + public function getDomain(): array + { + return [ + 'method' => 'GET', + 'path' => 'domains/{id}', + 'params' => ['id' => $this->params->idUrl('domain')], + ]; + } + + public function patchDomain(): array + { + return [ + 'method' => 'PATCH', + 'path' => 'domains/{id}', + 'jsonKey' => 'domain', + 'params' => [ + 'id' => $this->params->idUrl('domain'), + 'name' => $this->params->name('domain'), + 'enabled' => $this->params->enabled('domain'), + 'description' => $this->params->desc('domain'), + ], + ]; + } + + public function deleteDomain(): array + { + return [ + 'method' => 'DELETE', + 'path' => 'domains/{id}', + 'params' => ['id' => $this->params->idUrl('domain')], + ]; + } + + public function getUserRoles(): array + { + return [ + 'method' => 'GET', + 'path' => 'domains/{domainId}/users/{userId}/roles', + 'params' => [ + 'domainId' => $this->params->idUrl('domain'), + 'userId' => $this->params->idUrl('user'), + ], + ]; + } + + public function putUserRoles(): array + { + return [ + 'method' => 'PUT', + 'path' => 'domains/{domainId}/users/{userId}/roles/{roleId}', + 'params' => [ + 'domainId' => $this->params->idUrl('domain'), + 'userId' => $this->params->idUrl('user'), + 'roleId' => $this->params->idUrl('role'), + ], + ]; + } + + public function headUserRole(): array + { + return [ + 'method' => 'HEAD', + 'path' => 'domains/{domainId}/users/{userId}/roles/{roleId}', + 'params' => [ + 'domainId' => $this->params->idUrl('domain'), + 'userId' => $this->params->idUrl('user'), + 'roleId' => $this->params->idUrl('role'), + ], + ]; + } + + public function deleteUserRole(): array + { + return [ + 'method' => 'DELETE', + 'path' => 'domains/{domainId}/users/{userId}/roles/{roleId}', + 'params' => [ + 'domainId' => $this->params->idUrl('domain'), + 'userId' => $this->params->idUrl('user'), + 'roleId' => $this->params->idUrl('role'), + ], + ]; + } + + public function getGroupRoles(): array + { + return [ + 'method' => 'GET', + 'path' => 'domains/{domainId}/groups/{groupId}/roles', + 'params' => [ + 'domainId' => $this->params->idUrl('domain'), + 'groupId' => $this->params->idUrl('group'), + ], + ]; + } + + public function putGroupRole(): array + { + return [ + 'method' => 'PUT', + 'path' => 'domains/{domainId}/groups/{groupId}/roles/{roleId}', + 'params' => [ + 'domainId' => $this->params->idUrl('domain'), + 'groupId' => $this->params->idUrl('group'), + 'roleId' => $this->params->idUrl('role'), + ], + ]; + } + + public function headGroupRole(): array + { + return [ + 'method' => 'HEAD', + 'path' => 'domains/{domainId}/groups/{groupId}/roles/{roleId}', + 'params' => [ + 'domainId' => $this->params->idUrl('domain'), + 'groupId' => $this->params->idUrl('group'), + 'roleId' => $this->params->idUrl('role'), + ], + ]; + } + + public function deleteGroupRole(): array + { + return [ + 'method' => 'DELETE', + 'path' => 'domains/{domainId}/groups/{groupId}/roles/{roleId}', + 'params' => [ + 'domainId' => $this->params->idUrl('domain'), + 'groupId' => $this->params->idUrl('group'), + 'roleId' => $this->params->idUrl('role'), + ], + ]; + } + + public function postProjects(): array + { + return [ + 'method' => 'POST', + 'path' => 'projects', + 'jsonKey' => 'project', + 'params' => [ + 'description' => $this->params->desc('project'), + 'domainId' => $this->params->domainId('project'), + 'parentId' => $this->params->parentId(), + 'enabled' => $this->params->enabled('project'), + 'name' => $this->isRequired($this->params->name('project')), + ], + ]; + } + + public function getProjects(): array + { + return [ + 'method' => 'GET', + 'path' => 'projects', + 'params' => [ + 'domainId' => $this->query($this->params->domainId('project')), + 'enabled' => $this->query($this->params->enabled('project')), + 'name' => $this->query($this->params->name('project')), + ], + ]; + } + + public function getProject(): array + { + return [ + 'method' => 'GET', + 'path' => 'projects/{id}', + 'params' => ['id' => $this->params->idUrl('project')], + ]; + } + + public function patchProject(): array + { + return [ + 'method' => 'PATCH', + 'path' => 'projects/{id}', + 'jsonKey' => 'project', + 'params' => [ + 'id' => $this->params->idUrl('project'), + 'description' => $this->params->desc('project'), + 'domainId' => $this->params->domainId('project'), + 'parentId' => $this->params->parentId(), + 'enabled' => $this->params->enabled('project'), + 'name' => $this->params->name('project'), + ], + ]; + } + + public function deleteProject(): array + { + return [ + 'method' => 'DELETE', + 'path' => 'projects/{id}', + 'params' => ['id' => $this->params->idUrl('project')], + ]; + } + + public function getProjectUserRoles(): array + { + return [ + 'method' => 'GET', + 'path' => 'projects/{projectId}/users/{userId}/roles', + 'params' => [ + 'projectId' => $this->params->idUrl('project'), + 'userId' => $this->params->idUrl('user'), + ], + ]; + } + + public function putProjectUserRole(): array + { + return [ + 'method' => 'PUT', + 'path' => 'projects/{projectId}/users/{userId}/roles/{roleId}', + 'params' => [ + 'projectId' => $this->params->idUrl('project'), + 'userId' => $this->params->idUrl('user'), + 'roleId' => $this->params->idUrl('role'), + ], + ]; + } + + public function headProjectUserRole(): array + { + return [ + 'method' => 'HEAD', + 'path' => 'projects/{projectId}/users/{userId}/roles/{roleId}', + 'params' => [ + 'projectId' => $this->params->idUrl('project'), + 'userId' => $this->params->idUrl('user'), + 'roleId' => $this->params->idUrl('role'), + ], + ]; + } + + public function deleteProjectUserRole(): array + { + return [ + 'method' => 'DELETE', + 'path' => 'projects/{projectId}/users/{userId}/roles/{roleId}', + 'params' => [ + 'projectId' => $this->params->idUrl('project'), + 'userId' => $this->params->idUrl('user'), + 'roleId' => $this->params->idUrl('role'), + ], + ]; + } + + public function getProjectGroupRoles(): array + { + return [ + 'method' => 'GET', + 'path' => 'projects/{projectId}/groups/{groupId}/roles', + 'params' => [ + 'projectId' => $this->params->idUrl('project'), + 'groupId' => $this->params->idUrl('group'), + ], + ]; + } + + public function putProjectGroupRole(): array + { + return [ + 'method' => 'PUT', + 'path' => 'projects/{projectId}/groups/{groupId}/roles/{roleId}', + 'params' => [ + 'projectId' => $this->params->idUrl('project'), + 'groupId' => $this->params->idUrl('group'), + 'roleId' => $this->params->idUrl('role'), + ], + ]; + } + + public function headProjectGroupRole(): array + { + return [ + 'method' => 'HEAD', + 'path' => 'projects/{projectId}/groups/{groupId}/roles/{roleId}', + 'params' => [ + 'projectId' => $this->params->idUrl('project'), + 'groupId' => $this->params->idUrl('group'), + 'roleId' => $this->params->idUrl('role'), + ], + ]; + } + + public function deleteProjectGroupRole(): array + { + return [ + 'method' => 'DELETE', + 'path' => 'projects/{projectId}/groups/{groupId}/roles/{roleId}', + 'params' => [ + 'projectId' => $this->params->idUrl('project'), + 'groupId' => $this->params->idUrl('group'), + 'roleId' => $this->params->idUrl('role'), + ], + ]; + } + + public function postUsers(): array + { + return [ + 'method' => 'POST', + 'path' => 'users', + 'jsonKey' => 'user', + 'params' => [ + 'defaultProjectId' => $this->params->defaultProjectId(), + 'description' => $this->params->desc('user'), + 'domainId' => $this->params->domainId('user'), + 'email' => $this->params->email(), + 'enabled' => $this->params->enabled('user'), + 'name' => $this->isRequired($this->params->name('user')), + 'password' => $this->params->password(), + ], + ]; + } + + public function getUsers(): array + { + return [ + 'method' => 'GET', + 'path' => 'users', + 'params' => [ + 'domainId' => $this->query($this->params->domainId('user')), + 'enabled' => $this->query($this->params->enabled('user')), + 'name' => $this->query($this->params->name('user')), + ], + ]; + } + + public function getUser(): array + { + return [ + 'method' => 'GET', + 'path' => 'users/{id}', + 'params' => ['id' => $this->params->idUrl('user')], + ]; + } + + public function patchUser(): array + { + return [ + 'method' => 'PATCH', + 'path' => 'users/{id}', + 'jsonKey' => 'user', + 'params' => [ + 'id' => $this->params->idUrl('user'), + 'defaultProjectId' => $this->params->defaultProjectId(), + 'description' => $this->params->desc('user'), + 'email' => $this->params->email(), + 'enabled' => $this->params->enabled('user'), + 'name' => $this->params->name('user'), + 'password' => $this->params->password(), + ], + ]; + } + + public function deleteUser(): array + { + return [ + 'method' => 'DELETE', + 'path' => 'users/{id}', + 'params' => ['id' => $this->params->idUrl('user')], + ]; + } + + public function getUserGroups(): array + { + return [ + 'method' => 'GET', + 'path' => 'users/{id}/groups', + 'params' => ['id' => $this->params->idUrl('user')], + ]; + } + + public function getUserProjects(): array + { + return [ + 'method' => 'GET', + 'path' => 'users/{id}/projects', + 'params' => ['id' => $this->params->idUrl('user')], + ]; + } + + public function postGroups(): array + { + return [ + 'method' => 'POST', + 'path' => 'groups', + 'jsonKey' => 'group', + 'params' => [ + 'description' => $this->params->desc('group'), + 'domainId' => $this->params->domainId('group'), + 'name' => $this->params->name('group'), + ], + ]; + } + + public function getGroups(): array + { + return [ + 'method' => 'GET', + 'path' => 'groups', + 'params' => ['domainId' => $this->query($this->params->domainId('group'))], + ]; + } + + public function getGroup(): array + { + return [ + 'method' => 'GET', + 'path' => 'groups/{id}', + 'params' => ['id' => $this->params->idUrl('group')], + ]; + } + + public function patchGroup(): array + { + return [ + 'method' => 'PATCH', + 'path' => 'groups/{id}', + 'jsonKey' => 'group', + 'params' => [ + 'id' => $this->params->idUrl('group'), + 'description' => $this->params->desc('group'), + 'name' => $this->params->name('group'), + ], + ]; + } + + public function deleteGroup(): array + { + return [ + 'method' => 'DELETE', + 'path' => 'groups/{id}', + 'params' => ['id' => $this->params->idUrl('group')], + ]; + } + + public function getGroupUsers(): array + { + return [ + 'method' => 'GET', + 'path' => 'groups/{id}/users', + 'params' => ['id' => $this->params->idUrl('group')], + ]; + } + + public function putGroupUser(): array + { + return [ + 'method' => 'PUT', + 'path' => 'groups/{groupId}/users/{userId}', + 'params' => [ + 'groupId' => $this->params->idUrl('group'), + 'userId' => $this->params->idUrl('user'), + ], + ]; + } + + public function deleteGroupUser(): array + { + return [ + 'method' => 'DELETE', + 'path' => 'groups/{groupId}/users/{userId}', + 'params' => [ + 'groupId' => $this->params->idUrl('group'), + 'userId' => $this->params->idUrl('user'), + ], + ]; + } + + public function headGroupUser(): array + { + return [ + 'method' => 'HEAD', + 'path' => 'groups/{groupId}/users/{userId}', + 'params' => [ + 'groupId' => $this->params->idUrl('group'), + 'userId' => $this->params->idUrl('user'), + ], + ]; + } + + public function postCredentials(): array + { + return [ + 'method' => 'POST', + 'path' => 'credentials', + 'params' => [ + 'blob' => $this->params->blob(), + 'projectId' => $this->params->projectId(), + 'type' => $this->params->type('credential'), + 'userId' => $this->params->userId(), + ], + ]; + } + + public function getCredentials(): array + { + return [ + 'method' => 'GET', + 'path' => 'credentials', + 'params' => [], + ]; + } + + public function getCredential(): array + { + return [ + 'method' => 'GET', + 'path' => 'credentials/{id}', + 'params' => ['id' => $this->params->idUrl('credential')], + ]; + } + + public function patchCredential(): array + { + return [ + 'method' => 'PATCH', + 'path' => 'credentials/{id}', + 'params' => ['id' => $this->params->idUrl('credential')] + $this->postCredentials()['params'], + ]; + } + + public function deleteCredential(): array + { + return [ + 'method' => 'DELETE', + 'path' => 'credentials/{id}', + 'params' => ['id' => $this->params->idUrl('credential')], + ]; + } + + public function postRoles(): array + { + return [ + 'method' => 'POST', + 'path' => 'roles', + 'jsonKey' => 'role', + 'params' => ['name' => $this->isRequired($this->params->name('role'))], + ]; + } + + public function getRoles(): array + { + return [ + 'method' => 'GET', + 'path' => 'roles', + 'params' => ['name' => $this->query($this->params->name('role'))], + ]; + } + + public function deleteRole(): array + { + return [ + 'method' => 'DELETE', + 'path' => 'roles/{id}', + 'params' => ['id' => $this->params->idUrl('role')], + ]; + } + + public function getRoleAssignments(): array + { + return [ + 'method' => 'GET', + 'path' => 'role_assignments', + 'params' => [ + 'userId' => $this->params->userIdQuery(), + 'groupId' => $this->params->groupIdQuery(), + 'roleId' => $this->params->roleIdQuery(), + 'domainId' => $this->params->domainIdQuery(), + 'projectId' => $this->params->projectIdQuery(), + 'effective' => $this->params->effective(), + ], + ]; + } + + public function postPolicies(): array + { + return [ + 'method' => 'POST', + 'path' => 'policies', + 'jsonKey' => 'policy', + 'params' => [ + 'blob' => $this->params->blob(), + 'projectId' => $this->params->projectId('policy'), + 'type' => $this->params->type('policy'), + 'userId' => $this->params->userId('policy'), + ], + ]; + } + + public function getPolicies(): array + { + return [ + 'method' => 'GET', + 'path' => 'policies', + 'params' => ['type' => $this->query($this->params->type('policy'))], + ]; + } + + public function getPolicy(): array + { + return [ + 'method' => 'GET', + 'path' => 'policies/{id}', + 'params' => ['id' => $this->params->idUrl('policy')], + ]; + } + + public function patchPolicy(): array + { + return [ + 'method' => 'PATCH', + 'path' => 'policies/{id}', + 'jsonKey' => 'policy', + 'params' => [ + 'id' => $this->params->idUrl('policy'), + 'blob' => $this->params->blob(), + 'projectId' => $this->params->projectId('policy'), + 'type' => $this->params->type('policy'), + 'userId' => $this->params->userId(), + ], + ]; + } + + public function deletePolicy(): array + { + return [ + 'method' => 'DELETE', + 'path' => 'policies/{id}', + 'params' => ['id' => $this->params->idUrl('policy')], + ]; + } + + public function getApplicationCredential(): array + { + return [ + 'method' => 'GET', + 'path' => 'users/{userId}/application_credentials/{id}', + 'jsonKey' => 'application_credential', + 'params' => [ + 'id' => $this->params->idUrl('application_credential'), + 'userId' => $this->params->idUrl('user'), + ], + ]; + } + + public function postApplicationCredential(): array + { + return [ + 'method' => 'POST', + 'path' => 'users/{userId}/application_credentials', + 'jsonKey' => 'application_credential', + 'params' => [ + 'userId' => $this->params->idUrl('user'), + 'name' => $this->params->name('application_credential'), + 'description' => $this->params->desc('application_credential'), + ], + ]; + } + + public function deleteApplicationCredential(): array + { + return [ + 'method' => 'DELETE', + 'path' => 'users/{userId}/application_credentials/{id}', + 'params' => [ + 'id' => $this->params->idUrl('application_credential'), + 'userId' => $this->params->idUrl('user'), + ], + ]; + } +} diff --git a/3rdparty/php-opencloud/openstack/src/Identity/v3/Enum.php b/3rdparty/php-opencloud/openstack/src/Identity/v3/Enum.php new file mode 100644 index 00000000..60401a2b --- /dev/null +++ b/3rdparty/php-opencloud/openstack/src/Identity/v3/Enum.php @@ -0,0 +1,12 @@ + 'userId', + ]; + + protected $resourceKey = 'application_credential'; + protected $resourcesKey = 'application_credentials'; + + /** + * {@inheritdoc} + * + * @param array $userOptions {@see \OpenStack\Identity\v3\Api::postApplicationCredential} + */ + public function create(array $userOptions): Creatable + { + $response = $this->execute($this->api->postApplicationCredential(), $userOptions); + + return $this->populateFromResponse($response); + } + + /** + * {@inheritdoc} + */ + public function retrieve() + { + $response = $this->execute( + $this->api->getApplicationCredential(), + ['id' => $this->id, 'userId' => $this->userId] + ); + $this->populateFromResponse($response); + } + + /** + * {@inheritdoc} + */ + public function delete() + { + $this->executeWithState($this->api->deleteApplicationCredential()); + } +} diff --git a/3rdparty/php-opencloud/openstack/src/Identity/v3/Models/Assignment.php b/3rdparty/php-opencloud/openstack/src/Identity/v3/Models/Assignment.php new file mode 100644 index 00000000..26032a5f --- /dev/null +++ b/3rdparty/php-opencloud/openstack/src/Identity/v3/Models/Assignment.php @@ -0,0 +1,36 @@ + new Alias('role', Role::class), + 'user' => new Alias('user', User::class), + 'group' => new Alias('group', Group::class), + ]; + } +} diff --git a/3rdparty/php-opencloud/openstack/src/Identity/v3/Models/Catalog.php b/3rdparty/php-opencloud/openstack/src/Identity/v3/Models/Catalog.php new file mode 100644 index 00000000..d208d7b6 --- /dev/null +++ b/3rdparty/php-opencloud/openstack/src/Identity/v3/Models/Catalog.php @@ -0,0 +1,58 @@ + new Alias('services', Service::class, true), + ]; + } + + public function populateFromArray(array $data): self + { + foreach ($data as $service) { + $this->services[] = $this->model(Service::class, $service); + } + + return $this; + } + + /** + * Retrieve a base URL for a service, according to its catalog name, type, region. + * + * @param string $name the name of the service as it appears in the catalog + * @param string $type the type of the service as it appears in the catalog + * @param string $region the region of the service as it appears in the catalog + * @param string $urlType unused + * + * @return false|string FALSE if no URL found + */ + public function getServiceUrl(string $name, string $type, string $region, string $urlType): string + { + if (empty($this->services)) { + throw new \RuntimeException('No services are defined'); + } + + foreach ($this->services as $service) { + if (false !== ($url = $service->getUrl($name, $type, $region, $urlType))) { + return $url; + } + } + + throw new \RuntimeException(sprintf("Endpoint URL could not be found in the catalog for this service.\nName: %s\nType: %s\nRegion: %s\nURL type: %s", $name, $type, $region, $urlType)); + } +} diff --git a/3rdparty/php-opencloud/openstack/src/Identity/v3/Models/Credential.php b/3rdparty/php-opencloud/openstack/src/Identity/v3/Models/Credential.php new file mode 100644 index 00000000..294a3ec1 --- /dev/null +++ b/3rdparty/php-opencloud/openstack/src/Identity/v3/Models/Credential.php @@ -0,0 +1,65 @@ + 'projectId', + 'user_id' => 'userId', + ]; + + public function create(array $userOptions): Creatable + { + $response = $this->execute($this->api->postCredentials(), $userOptions); + + return $this->populateFromResponse($response); + } + + public function retrieve() + { + $response = $this->executeWithState($this->api->getCredential()); + $this->populateFromResponse($response); + } + + public function update() + { + $response = $this->executeWithState($this->api->patchCredential()); + $this->populateFromResponse($response); + } + + public function delete() + { + $this->executeWithState($this->api->deleteCredential()); + } +} diff --git a/3rdparty/php-opencloud/openstack/src/Identity/v3/Models/Domain.php b/3rdparty/php-opencloud/openstack/src/Identity/v3/Models/Domain.php new file mode 100644 index 00000000..332503e9 --- /dev/null +++ b/3rdparty/php-opencloud/openstack/src/Identity/v3/Models/Domain.php @@ -0,0 +1,148 @@ +execute($this->api->postDomains(), $data); + + return $this->populateFromResponse($response); + } + + public function retrieve() + { + $response = $this->executeWithState($this->api->getDomain()); + $this->populateFromResponse($response); + } + + public function update() + { + $response = $this->executeWithState($this->api->patchDomain()); + $this->populateFromResponse($response); + } + + public function delete() + { + $this->executeWithState($this->api->deleteDomain()); + } + + /** + * @param array $options {@see \OpenStack\Identity\v3\Api::getUserRoles} + * + * @return \Generator + */ + public function listUserRoles(array $options = []): \Generator + { + $options['domainId'] = $this->id; + + return $this->model(Role::class)->enumerate($this->api->getUserRoles(), $options); + } + + /** + * @param array $options {@see \OpenStack\Identity\v3\Api::putUserRoles} + */ + public function grantUserRole(array $options = []) + { + $this->execute($this->api->putUserRoles(), ['domainId' => $this->id] + $options); + } + + /** + * @param array $options {@see \OpenStack\Identity\v3\Api::headUserRole} + */ + public function checkUserRole(array $options = []): bool + { + try { + $this->execute($this->api->headUserRole(), ['domainId' => $this->id] + $options); + + return true; + } catch (BadResponseError $e) { + return false; + } + } + + /** + * @param array $options {@see \OpenStack\Identity\v3\Api::deleteUserRole} + */ + public function revokeUserRole(array $options = []) + { + $this->execute($this->api->deleteUserRole(), ['domainId' => $this->id] + $options); + } + + /** + * @param array $options {@see \OpenStack\Identity\v3\Api::getGroupRoles} + * + * @return \Generator + */ + public function listGroupRoles(array $options = []): \Generator + { + $options['domainId'] = $this->id; + + return $this->model(Role::class)->enumerate($this->api->getGroupRoles(), $options); + } + + /** + * @param array $options {@see \OpenStack\Identity\v3\Api::putGroupRole} + */ + public function grantGroupRole(array $options = []) + { + $this->execute($this->api->putGroupRole(), ['domainId' => $this->id] + $options); + } + + /** + * @param array $options {@see \OpenStack\Identity\v3\Api::headGroupRole} + */ + public function checkGroupRole(array $options = []): bool + { + try { + $this->execute($this->api->headGroupRole(), ['domainId' => $this->id] + $options); + + return true; + } catch (BadResponseError $e) { + return false; + } + } + + /** + * @param array $options {@see \OpenStack\Identity\v3\Api::deleteGroupRole} + */ + public function revokeGroupRole(array $options = []) + { + $this->execute($this->api->deleteGroupRole(), ['domainId' => $this->id] + $options); + } +} diff --git a/3rdparty/php-opencloud/openstack/src/Identity/v3/Models/Endpoint.php b/3rdparty/php-opencloud/openstack/src/Identity/v3/Models/Endpoint.php new file mode 100644 index 00000000..1f496fba --- /dev/null +++ b/3rdparty/php-opencloud/openstack/src/Identity/v3/Models/Endpoint.php @@ -0,0 +1,79 @@ + 'serviceId']; + + /** + * @param array $data {@see \OpenStack\Identity\v3\Api::postEndpoints} + */ + public function create(array $data): Creatable + { + $response = $this->execute($this->api->postEndpoints(), $data); + + return $this->populateFromResponse($response); + } + + public function retrieve() + { + $response = $this->executeWithState($this->api->getEndpoint()); + $this->populateFromResponse($response); + } + + public function update() + { + $response = $this->executeWithState($this->api->patchEndpoint()); + $this->populateFromResponse($response); + } + + public function delete() + { + $this->execute($this->api->deleteEndpoint(), $this->getAttrs(['id'])); + } + + public function regionMatches(string $value): bool + { + return in_array($this->region, ['*', $value]); + } + + public function interfaceMatches(string $value): bool + { + return $this->interface && $this->interface == $value; + } +} diff --git a/3rdparty/php-opencloud/openstack/src/Identity/v3/Models/Group.php b/3rdparty/php-opencloud/openstack/src/Identity/v3/Models/Group.php new file mode 100644 index 00000000..c6869dea --- /dev/null +++ b/3rdparty/php-opencloud/openstack/src/Identity/v3/Models/Group.php @@ -0,0 +1,108 @@ + 'domainId']; + + protected $resourceKey = 'group'; + protected $resourcesKey = 'groups'; + + /** + * @param array $data {@see \OpenStack\Identity\v3\Api::postGroups} + */ + public function create(array $data): Creatable + { + $response = $this->execute($this->api->postGroups(), $data); + + return $this->populateFromResponse($response); + } + + public function retrieve() + { + $response = $this->execute($this->api->getGroup(), ['id' => $this->id]); + $this->populateFromResponse($response); + } + + public function update() + { + $response = $this->executeWithState($this->api->patchGroup()); + $this->populateFromResponse($response); + } + + public function delete() + { + $this->execute($this->api->deleteGroup(), ['id' => $this->id]); + } + + /** + * @param array $options {@see \OpenStack\Identity\v3\Api::getGroupUsers} + * + * @return \Generator + */ + public function listUsers(array $options = []): \Generator + { + $options['id'] = $this->id; + + return $this->model(User::class)->enumerate($this->api->getGroupUsers(), $options); + } + + /** + * @param array $options {@see \OpenStack\Identity\v3\Api::putGroupUser} + */ + public function addUser(array $options) + { + $this->execute($this->api->putGroupUser(), ['groupId' => $this->id] + $options); + } + + /** + * @param array $options {@see \OpenStack\Identity\v3\Api::deleteGroupUser} + */ + public function removeUser(array $options) + { + $this->execute($this->api->deleteGroupUser(), ['groupId' => $this->id] + $options); + } + + /** + * @param array $options {@see \OpenStack\Identity\v3\Api::headGroupUser} + */ + public function checkMembership(array $options): bool + { + try { + $this->execute($this->api->headGroupUser(), ['groupId' => $this->id] + $options); + + return true; + } catch (BadResponseError $e) { + return false; + } + } +} diff --git a/3rdparty/php-opencloud/openstack/src/Identity/v3/Models/Policy.php b/3rdparty/php-opencloud/openstack/src/Identity/v3/Models/Policy.php new file mode 100644 index 00000000..1ccca4f0 --- /dev/null +++ b/3rdparty/php-opencloud/openstack/src/Identity/v3/Models/Policy.php @@ -0,0 +1,71 @@ + 'projectId', + 'user_id' => 'userId', + ]; + + /** + * @param array $data {@see \OpenStack\Identity\v3\Api::postPolicies} + */ + public function create(array $data): Creatable + { + $response = $this->execute($this->api->postPolicies(), $data); + + return $this->populateFromResponse($response); + } + + public function retrieve() + { + $response = $this->execute($this->api->getPolicy(), ['id' => $this->id]); + $this->populateFromResponse($response); + } + + public function update() + { + $response = $this->executeWithState($this->api->patchPolicy()); + $this->populateFromResponse($response); + } + + public function delete() + { + $this->execute($this->api->deletePolicy(), ['id' => $this->id]); + } +} diff --git a/3rdparty/php-opencloud/openstack/src/Identity/v3/Models/Project.php b/3rdparty/php-opencloud/openstack/src/Identity/v3/Models/Project.php new file mode 100644 index 00000000..8d58c223 --- /dev/null +++ b/3rdparty/php-opencloud/openstack/src/Identity/v3/Models/Project.php @@ -0,0 +1,160 @@ + 'domainId', + 'parent_id' => 'parentId', + ]; + + protected $resourceKey = 'project'; + protected $resourcesKey = 'projects'; + + /** + * @param array $data {@see \OpenStack\Identity\v3\Api::postProjects} + */ + public function create(array $data): Creatable + { + $response = $this->execute($this->api->postProjects(), $data); + $this->populateFromResponse($response); + + return $this; + } + + public function retrieve() + { + $response = $this->executeWithState($this->api->getProject()); + $this->populateFromResponse($response); + } + + public function update() + { + $response = $this->executeWithState($this->api->patchProject()); + $this->populateFromResponse($response); + } + + public function delete() + { + $this->executeWithState($this->api->deleteProject()); + } + + /** + * @param array $options {@see \OpenStack\Identity\v3\Api::getProjectUserRoles} + * + * @return \Generator + */ + public function listUserRoles(array $options = []): \Generator + { + $options['projectId'] = $this->id; + + return $this->model(Role::class)->enumerate($this->api->getProjectUserRoles(), $options); + } + + /** + * @param array $options {@see \OpenStack\Identity\v3\Api::putProjectUserRole} + */ + public function grantUserRole(array $options) + { + $this->execute($this->api->putProjectUserRole(), ['projectId' => $this->id] + $options); + } + + /** + * @param array $options {@see \OpenStack\Identity\v3\Api::headProjectUserRole} + */ + public function checkUserRole(array $options): bool + { + try { + $this->execute($this->api->headProjectUserRole(), ['projectId' => $this->id] + $options); + + return true; + } catch (BadResponseError $e) { + return false; + } + } + + /** + * @param array $options {@see \OpenStack\Identity\v3\Api::deleteProjectUserRole} + */ + public function revokeUserRole(array $options) + { + $this->execute($this->api->deleteProjectUserRole(), ['projectId' => $this->id] + $options); + } + + /** + * @param array $options {@see \OpenStack\Identity\v3\Api::getProjectGroupRoles} + * + * @return \Generator + */ + public function listGroupRoles(array $options = []): \Generator + { + $options['projectId'] = $this->id; + + return $this->model(Role::class)->enumerate($this->api->getProjectGroupRoles(), $options); + } + + /** + * @param array $options {@see \OpenStack\Identity\v3\Api::putProjectGroupRole} + */ + public function grantGroupRole(array $options) + { + $this->execute($this->api->putProjectGroupRole(), ['projectId' => $this->id] + $options); + } + + /** + * @param array $options {@see \OpenStack\Identity\v3\Api::headProjectGroupRole} + */ + public function checkGroupRole(array $options): bool + { + try { + $this->execute($this->api->headProjectGroupRole(), ['projectId' => $this->id] + $options); + + return true; + } catch (BadResponseError $e) { + return false; + } + } + + /** + * @param array $options {@see \OpenStack\Identity\v3\Api::deleteProjectGroupRole} + */ + public function revokeGroupRole(array $options) + { + $this->execute($this->api->deleteProjectGroupRole(), ['projectId' => $this->id] + $options); + } +} diff --git a/3rdparty/php-opencloud/openstack/src/Identity/v3/Models/Role.php b/3rdparty/php-opencloud/openstack/src/Identity/v3/Models/Role.php new file mode 100644 index 00000000..d2236311 --- /dev/null +++ b/3rdparty/php-opencloud/openstack/src/Identity/v3/Models/Role.php @@ -0,0 +1,43 @@ +execute($this->api->postRoles(), $data); + + return $this->populateFromResponse($response); + } + + public function delete() + { + $this->executeWithState($this->api->deleteRole()); + } +} diff --git a/3rdparty/php-opencloud/openstack/src/Identity/v3/Models/Service.php b/3rdparty/php-opencloud/openstack/src/Identity/v3/Models/Service.php new file mode 100644 index 00000000..6a257dce --- /dev/null +++ b/3rdparty/php-opencloud/openstack/src/Identity/v3/Models/Service.php @@ -0,0 +1,109 @@ + new Alias('endpoints', Endpoint::class, true), + ]; + } + + /** + * @param array $data {@see \OpenStack\Identity\v3\Api::postServices} + */ + public function create(array $data): Creatable + { + $response = $this->execute($this->api->postServices(), $data); + + return $this->populateFromResponse($response); + } + + public function retrieve() + { + $response = $this->executeWithState($this->api->getService()); + $this->populateFromResponse($response); + } + + public function update() + { + $response = $this->executeWithState($this->api->patchService()); + $this->populateFromResponse($response); + } + + public function delete() + { + $this->executeWithState($this->api->deleteService()); + } + + private function nameMatches(string $value): bool + { + return $this->name && $this->name == $value; + } + + private function typeMatches(string $value): bool + { + return $this->type && $this->type == $value; + } + + /** + * Retrieve the base URL for a service. + * + * @param string $name the name of the service as it appears in the catalog + * @param string $type the type of the service as it appears in the catalog + * @param string $region the region of the service as it appears in the catalog + * @param string $interface the interface of the service as it appears in the catalog + * + * @return string|false + */ + public function getUrl(string $name, string $type, string $region, string $interface) + { + if (!$this->nameMatches($name) || !$this->typeMatches($type)) { + return false; + } + + foreach ($this->endpoints as $endpoint) { + if ($endpoint->regionMatches($region) && $endpoint->interfaceMatches($interface)) { + return $endpoint->url; + } + } + + return false; + } +} diff --git a/3rdparty/php-opencloud/openstack/src/Identity/v3/Models/Token.php b/3rdparty/php-opencloud/openstack/src/Identity/v3/Models/Token.php new file mode 100644 index 00000000..d4054d7c --- /dev/null +++ b/3rdparty/php-opencloud/openstack/src/Identity/v3/Models/Token.php @@ -0,0 +1,150 @@ + new Alias('roles', Role::class, true), + 'expires_at' => new Alias('expires', \DateTimeImmutable::class), + 'project' => new Alias('project', Project::class), + 'catalog' => new Alias('catalog', Catalog::class), + 'user' => new Alias('user', User::class), + 'issued_at' => new Alias('issued', \DateTimeImmutable::class), + ]; + } + + public function populateFromResponse(ResponseInterface $response) + { + parent::populateFromResponse($response); + $this->id = $response->getHeaderLine('X-Subject-Token'); + + return $this; + } + + public function getId(): string + { + return $this->id; + } + + /** + * @return bool TRUE if the token has expired (and is invalid); FALSE otherwise + */ + public function hasExpired(): bool + { + return $this->expires <= new \DateTimeImmutable('now', $this->expires->getTimezone()); + } + + public function retrieve() + { + $response = $this->execute($this->api->getTokens(), ['tokenId' => $this->id]); + $this->populateFromResponse($response); + } + + /** + * @param array $userOptions {@see \OpenStack\Identity\v3\Api::postTokens} + */ + public function create(array $userOptions): Creatable + { + if (isset($userOptions['user'])) { + $userOptions['methods'] = ['password']; + if (!isset($userOptions['user']['id']) && empty($userOptions['user']['domain'])) { + throw new InvalidArgumentException('When authenticating with a username, you must also provide either the domain name '.'or domain ID to which the user belongs to. Alternatively, if you provide a user ID instead, '.'you do not need to provide domain information.'); + } + } elseif (isset($userOptions['application_credential'])) { + $userOptions['methods'] = ['application_credential']; + if (!isset($userOptions['application_credential']['id']) || !isset($userOptions['application_credential']['secret'])) { + throw new InvalidArgumentException('When authenticating with a application_credential, you must provide application credential ID '.' and application credential secret.'); + } + } elseif (isset($userOptions['tokenId'])) { + $userOptions['methods'] = ['token']; + } else { + throw new InvalidArgumentException('Either a user, tokenId or application_credential must be provided.'); + } + + $response = $this->execute($this->api->postTokens(), $userOptions); + $token = $this->populateFromResponse($response); + + // Cache response as an array to export if needed. + // Added key `id` which is auth token from HTTP header X-Subject-Token + $this->cachedToken = Utils::flattenJson(Utils::jsonDecode($response), $this->resourceKey); + $this->cachedToken['id'] = $token->id; + + return $token; + } + + /** + * Returns a serialized representation of an authentication token. + * + * Initialize OpenStack object using $params['cachedToken'] to reduce the amount of HTTP calls. + * + * This array is a modified version of response from `/auth/tokens`. Do not manually modify this array. + */ + public function export(): array + { + return $this->cachedToken; + } + + /** + * Checks if the token is valid. + * + * @return bool TRUE if the token is valid; FALSE otherwise + */ + public function validate(): bool + { + try { + $this->execute($this->api->headTokens(), ['tokenId' => $this->id]); + + return true; + } catch (BadResponseError $e) { + return false; + } + } +} diff --git a/3rdparty/php-opencloud/openstack/src/Identity/v3/Models/User.php b/3rdparty/php-opencloud/openstack/src/Identity/v3/Models/User.php new file mode 100644 index 00000000..28efc859 --- /dev/null +++ b/3rdparty/php-opencloud/openstack/src/Identity/v3/Models/User.php @@ -0,0 +1,113 @@ + 'domainId', + 'default_project_id' => 'defaultProjectId', + ]; + + protected $resourceKey = 'user'; + protected $resourcesKey = 'users'; + + /** + * @param array $data {@see \OpenStack\Identity\v3\Api::postUsers} + */ + public function create(array $data): Creatable + { + $response = $this->execute($this->api->postUsers(), $data); + + return $this->populateFromResponse($response); + } + + public function retrieve() + { + $response = $this->execute($this->api->getUser(), ['id' => $this->id]); + $this->populateFromResponse($response); + } + + public function update() + { + $response = $this->executeWithState($this->api->patchUser()); + $this->populateFromResponse($response); + } + + public function delete() + { + $this->execute($this->api->deleteUser(), ['id' => $this->id]); + } + + /** + * @return \Generator + */ + public function listGroups(): \Generator + { + return $this->model(Group::class)->enumerate($this->api->getUserGroups(), ['id' => $this->id]); + } + + /** + * @return \Generator + */ + public function listProjects(): \Generator + { + return $this->model(Project::class)->enumerate($this->api->getUserProjects(), ['id' => $this->id]); + } + + /** + * Creates a new application credential according to the provided options. + * + * @param array $options {@see \OpenStack\Identity\v3\Api::postApplicationCredential} + */ + public function createApplicationCredential(array $options): ApplicationCredential + { + return $this->model(ApplicationCredential::class)->create(['userId' => $this->id] + $options); + } + + /** + * Retrieves an application credential object and populates its unique identifier object. This operation will not + * perform a GET or HEAD request by default; you will need to call retrieve() if you want to pull in remote state + * from the API. + */ + public function getApplicationCredential(string $id): ApplicationCredential + { + return $this->model(ApplicationCredential::class, ['id' => $id, 'userId' => $this->id]); + } +} diff --git a/3rdparty/php-opencloud/openstack/src/Identity/v3/Params.php b/3rdparty/php-opencloud/openstack/src/Identity/v3/Params.php new file mode 100644 index 00000000..cba08f5f --- /dev/null +++ b/3rdparty/php-opencloud/openstack/src/Identity/v3/Params.php @@ -0,0 +1,336 @@ + self::ARRAY_TYPE, + 'path' => 'auth.identity', + 'items' => ['type' => self::STRING_TYPE], + 'description' => << self::OBJECT_TYPE, + 'path' => 'auth.identity', + 'properties' => [ + 'id' => [ + 'type' => self::STRING_TYPE, + 'description' => $this->id('application credential id'), + ], + 'secret' => [ + 'type' => self::STRING_TYPE, + 'description' => 'The secret of the application credential', + ], + ], + ]; + } + + public function user(): array + { + return [ + 'type' => self::OBJECT_TYPE, + 'path' => 'auth.identity.password', + 'properties' => [ + 'id' => [ + 'type' => self::STRING_TYPE, + 'description' => $this->id('user'), + ], + 'name' => [ + 'type' => self::STRING_TYPE, + 'description' => 'The username of the user', + ], + 'password' => [ + 'type' => self::STRING_TYPE, + 'description' => 'The password of the user', + ], + 'domain' => $this->domain(), + ], + ]; + } + + public function tokenBody(): array + { + return [ + 'path' => 'auth.identity.token', + 'sentAs' => 'id', + 'type' => self::STRING_TYPE, + 'description' => $this->id('token'), + ]; + } + + public function scope(): array + { + return [ + 'type' => self::OBJECT_TYPE, + 'path' => 'auth', + 'properties' => [ + 'project' => $this->project(), + 'domain' => $this->domain(), + ], + ]; + } + + public function typeQuery(): array + { + return [ + 'type' => 'string', + 'location' => 'query', + 'description' => 'Filters all the available services according to a given type', + ]; + } + + public function interf(): array + { + return [ + 'description' => << << << 'string', + 'sentAs' => 'service_id', + 'description' => $this->id('service')['description'].' that this endpoint belongs to', + ]; + } + + public function password(): array + { + return [ + 'description' => << 'The personal e-mail address of the user', + ]; + } + + public function effective(): array + { + return [ + 'type' => self::BOOL_TYPE, + 'location' => self::QUERY, + 'description' => << 'scope.project.id', + 'location' => 'query', + 'description' => 'Filter by project ID', + ]; + } + + public function domainIdQuery(): array + { + return [ + 'sentAs' => 'scope.domain.id', + 'location' => 'query', + 'description' => $this->id('domain')['description'].' associated with the role assignments', + ]; + } + + public function roleIdQuery(): array + { + return [ + 'sentAs' => 'role.id', + 'location' => 'query', + 'description' => 'Filter by role ID', + ]; + } + + public function groupIdQuery(): array + { + return [ + 'sentAs' => 'group.id', + 'location' => 'query', + 'description' => 'Filter by group ID', + ]; + } + + public function userIdQuery(): array + { + return [ + 'sentAs' => 'user.id', + 'location' => 'query', + 'description' => 'Filter by user ID', + ]; + } + + public function domain(): array + { + return [ + 'type' => 'object', + 'properties' => [ + 'id' => $this->id('domain'), + 'name' => $this->name('domain'), + ], + ]; + } + + public function project(): array + { + return [ + 'type' => 'object', + 'properties' => [ + 'id' => $this->id('project'), + 'name' => $this->name('project'), + 'domain' => $this->domain(), + ], + ]; + } + + public function idUrl($type) + { + return [ + 'required' => true, + 'location' => self::URL, + 'description' => sprintf('The unique ID, or identifier, for the %s', $type), + ]; + } + + public function tokenId(): array + { + return [ + 'location' => self::HEADER, + 'sentAs' => 'X-Subject-Token', + 'description' => 'The unique token ID', + ]; + } + + public function domainId($type) + { + return [ + 'sentAs' => 'domain_id', + 'description' => sprintf('%s associated with this %s', $this->id('domain')['description'], $type), + ]; + } + + public function parentId(): array + { + return [ + 'sentAs' => 'parent_id', + 'description' => << sprintf('The type of the %s', $resource), + ]; + } + + public function desc($resource) + { + return [ + 'description' => sprintf('A human-friendly summary that explains what the %s does', $resource), + ]; + } + + public function enabled($resource) + { + return [ + 'type' => self::BOOL_TYPE, + 'description' => sprintf( + 'Indicates whether this %s is enabled or not. If not, the %s will be unavailable for use.', + $resource, + $resource + ), + ]; + } + + public function defaultProjectId(): array + { + return [ + 'sentAs' => 'default_project_id', + 'description' => << 'project_id', + 'description' => $this->id('project'), + ]; + } + + public function userId(): array + { + return [ + 'sentAs' => 'user_id', + 'description' => $this->id('user'), + ]; + } + + public function blob(): array + { + return [ + 'type' => 'string', + 'description' => "This does something, but it's not explained in the docs (as of writing this)", + ]; + } +} diff --git a/3rdparty/php-opencloud/openstack/src/Identity/v3/Service.php b/3rdparty/php-opencloud/openstack/src/Identity/v3/Service.php new file mode 100644 index 00000000..5760436f --- /dev/null +++ b/3rdparty/php-opencloud/openstack/src/Identity/v3/Service.php @@ -0,0 +1,430 @@ +api->postTokens()['params']); + + if (!empty($options['cachedToken'])) { + $token = $this->generateTokenFromCache($options['cachedToken']); + + if ($token->hasExpired()) { + throw new \RuntimeException(sprintf('Cached token has expired on "%s".', $token->expires->format(\DateTime::ISO8601))); + } + } else { + $token = $this->generateToken($authOptions); + } + + $name = $options['catalogName']; + $type = $options['catalogType']; + $region = $options['region']; + $interface = $options['interface'] ?? Enum::INTERFACE_PUBLIC; + + if ($baseUrl = $token->catalog->getServiceUrl($name, $type, $region, $interface)) { + return [$token, $baseUrl]; + } + + throw new \RuntimeException(sprintf('No service found with type [%s] name [%s] region [%s] interface [%s]', $type, $name, $region, $interface)); + } + + /** + * Generates authentication token from cached token using `$token->export()`. + * + * @param array $cachedToken {@see \OpenStack\Identity\v3\Models\Token::export} + */ + public function generateTokenFromCache(array $cachedToken): Models\Token + { + return $this->model(Models\Token::class)->populateFromArray($cachedToken); + } + + /** + * Generates a new authentication token. + * + * @param array $options {@see \OpenStack\Identity\v3\Api::postTokens} + */ + public function generateToken(array $options): Models\Token + { + return $this->model(Models\Token::class)->create($options); + } + + /** + * Retrieves a token object and populates its unique identifier object. This operation will not perform a GET or + * HEAD request by default; you will need to call retrieve() if you want to pull in remote state from the API. + * + * @param string $id The unique ID of the token to retrieve + */ + public function getToken(string $id): Models\Token + { + return $this->model(Models\Token::class, ['id' => $id]); + } + + /** + * Validates a token, identified by its ID, and returns TRUE if its valid, FALSE if not. + * + * @param string $id The unique ID of the token + */ + public function validateToken(string $id): bool + { + try { + $this->execute($this->api->headTokens(), ['tokenId' => $id]); + + return true; + } catch (BadResponseError $e) { + return false; + } + } + + /** + * Revokes a token, identified by its ID. After this operation completes, users will not be able to use this token + * again for authentication. + * + * @param string $id The unique ID of the token + */ + public function revokeToken(string $id) + { + $this->execute($this->api->deleteTokens(), ['tokenId' => $id]); + } + + /** + * Creates a new service according to the provided options. + * + * @param array $options {@see \OpenStack\Identity\v3\Api::postServices} + */ + public function createService(array $options): Models\Service + { + return $this->model(Models\Service::class)->create($options); + } + + /** + * Returns a generator which will yield a collection of service objects. The elements which generators yield can be + * accessed using a foreach loop. Often the API will not return the full state of the resource in collections; you + * will need to use retrieve() to pull in the full state of the remote resource from the API. + * + * @param array $options {@see \OpenStack\Identity\v3\Api::getServices} + * + * @return \Generator + */ + public function listServices(array $options = []): \Generator + { + return $this->model(Models\Service::class)->enumerate($this->api->getServices(), $options); + } + + /** + * Retrieves a service object and populates its unique identifier object. This operation will not perform a GET or + * HEAD request by default; you will need to call retrieve() if you want to pull in remote state from the API. + * + * @param string $id The unique ID of the service + */ + public function getService(string $id): Models\Service + { + return $this->model(Models\Service::class, ['id' => $id]); + } + + /** + * Creates a new endpoint according to the provided options. + * + * @param array $options {@see \OpenStack\Identity\v3\Api::postEndpoints} + */ + public function createEndpoint(array $options): Models\Endpoint + { + return $this->model(Models\Endpoint::class)->create($options); + } + + /** + * Retrieves an endpoint object and populates its unique identifier object. This operation will not perform a GET or + * HEAD request by default; you will need to call retrieve() if you want to pull in remote state from the API. + * + * @param string $id The unique ID of the service + */ + public function getEndpoint(string $id): Models\Endpoint + { + return $this->model(Models\Endpoint::class, ['id' => $id]); + } + + /** + * Returns a generator which will yield a collection of endpoint objects. The elements which generators yield can be + * accessed using a foreach loop. Often the API will not return the full state of the resource in collections; you + * will need to use retrieve() to pull in the full state of the remote resource from the API. + * + * @param array $options {@see \OpenStack\Identity\v3\Api::getEndpoints} + * + * @return \Generator + */ + public function listEndpoints(array $options = []): \Generator + { + return $this->model(Models\Endpoint::class)->enumerate($this->api->getEndpoints(), $options); + } + + /** + * Creates a new domain according to the provided options. + * + * @param array $options {@see \OpenStack\Identity\v3\Api::postDomains} + */ + public function createDomain(array $options): Models\Domain + { + return $this->model(Models\Domain::class)->create($options); + } + + /** + * Returns a generator which will yield a collection of domain objects. The elements which generators yield can be + * accessed using a foreach loop. Often the API will not return the full state of the resource in collections; you + * will need to use retrieve() to pull in the full state of the remote resource from the API. + * + * @param array $options {@see \OpenStack\Identity\v3\Api::getDomains} + * + * @return \Generator + */ + public function listDomains(array $options = []): \Generator + { + return $this->model(Models\Domain::class)->enumerate($this->api->getDomains(), $options); + } + + /** + * Retrieves a domain object and populates its unique identifier object. This operation will not perform a GET or + * HEAD request by default; you will need to call retrieve() if you want to pull in remote state from the API. + * + * @param string $id The unique ID of the domain + */ + public function getDomain(string $id): Models\Domain + { + return $this->model(Models\Domain::class, ['id' => $id]); + } + + /** + * Creates a new project according to the provided options. + * + * @param array $options {@see \OpenStack\Identity\v3\Api::postProjects} + */ + public function createProject(array $options): Models\Project + { + return $this->model(Models\Project::class)->create($options); + } + + /** + * Returns a generator which will yield a collection of project objects. The elements which generators yield can be + * accessed using a foreach loop. Often the API will not return the full state of the resource in collections; you + * will need to use retrieve() to pull in the full state of the remote resource from the API. + * + * @param array $options {@see \OpenStack\Identity\v3\Api::getProjects} + * + * @return \Generator + */ + public function listProjects(array $options = []): \Generator + { + return $this->model(Models\Project::class)->enumerate($this->api->getProjects(), $options); + } + + /** + * Retrieves a project object and populates its unique identifier object. This operation will not perform a GET or + * HEAD request by default; you will need to call retrieve() if you want to pull in remote state from the API. + * + * @param string $id The unique ID of the project + */ + public function getProject(string $id): Models\Project + { + return $this->model(Models\Project::class, ['id' => $id]); + } + + /** + * Creates a new user according to the provided options. + * + * @param array $options {@see \OpenStack\Identity\v3\Api::postUsers} + */ + public function createUser(array $options): Models\User + { + return $this->model(Models\User::class)->create($options); + } + + /** + * Returns a generator which will yield a collection of user objects. The elements which generators yield can be + * accessed using a foreach loop. Often the API will not return the full state of the resource in collections; you + * will need to use retrieve() to pull in the full state of the remote resource from the API. + * + * @param array $options {@see \OpenStack\Identity\v3\Api::getUsers} + * + * @return \Generator + */ + public function listUsers(array $options = []): \Generator + { + return $this->model(Models\User::class)->enumerate($this->api->getUsers(), $options); + } + + /** + * Retrieves a user object and populates its unique identifier object. This operation will not perform a GET or + * HEAD request by default; you will need to call retrieve() if you want to pull in remote state from the API. + * + * @param string $id The unique ID of the user + */ + public function getUser(string $id): Models\User + { + return $this->model(Models\User::class, ['id' => $id]); + } + + /** + * Creates a new group according to the provided options. + * + * @param array $options {@see \OpenStack\Identity\v3\Api::postGroups} + */ + public function createGroup(array $options): Models\Group + { + return $this->model(Models\Group::class)->create($options); + } + + /** + * Returns a generator which will yield a collection of group objects. The elements which generators yield can be + * accessed using a foreach loop. Often the API will not return the full state of the resource in collections; you + * will need to use retrieve() to pull in the full state of the remote resource from the API. + * + * @param array $options {@see \OpenStack\Identity\v3\Api::getGroups} + * + * @return \Generator + */ + public function listGroups(array $options = []): \Generator + { + return $this->model(Models\Group::class)->enumerate($this->api->getGroups(), $options); + } + + /** + * Retrieves a group object and populates its unique identifier object. This operation will not perform a GET or + * HEAD request by default; you will need to call retrieve() if you want to pull in remote state from the API. + * + * @param string $id The unique ID of the group + */ + public function getGroup($id): Models\Group + { + return $this->model(Models\Group::class, ['id' => $id]); + } + + /** + * Creates a new credential according to the provided options. + * + * @param array $options {@see \OpenStack\Identity\v3\Api::postCredentials} + */ + public function createCredential(array $options): Models\Credential + { + return $this->model(Models\Credential::class)->create($options); + } + + /** + * Returns a generator which will yield a collection of credential objects. The elements which generators yield can + * be accessed using a foreach loop. Often the API will not return the full state of the resource in collections; + * you will need to use retrieve() to pull in the full state of the remote resource from the API. + * + * @return \Generator + */ + public function listCredentials(): \Generator + { + return $this->model(Models\Credential::class)->enumerate($this->api->getCredentials()); + } + + /** + * Retrieves a credential object and populates its unique identifier object. This operation will not perform a GET + * or HEAD request by default; you will need to call retrieve() if you want to pull in remote state from the API. + * + * @param string $id The unique ID of the credential + */ + public function getCredential(string $id): Models\Credential + { + return $this->model(Models\Credential::class, ['id' => $id]); + } + + /** + * Creates a new role according to the provided options. + * + * @param array $options {@see \OpenStack\Identity\v3\Api::postRoles} + */ + public function createRole(array $options): Models\Role + { + return $this->model(Models\Role::class)->create($options); + } + + /** + * Returns a generator which will yield a collection of role objects. The elements which generators yield can be + * accessed using a foreach loop. Often the API will not return the full state of the resource in collections; you + * will need to use retrieve() to pull in the full state of the remote resource from the API. + * + * @param array $options {@see \OpenStack\Identity\v3\Api::getRoles} + * + * @return \Generator + */ + public function listRoles(array $options = []): \Generator + { + return $this->model(Models\Role::class)->enumerate($this->api->getRoles(), $options); + } + + /** + * Returns a generator which will yield a collection of role assignment objects. The elements which generators + * yield can be accessed using a foreach loop. Often the API will not return the full state of the resource in + * collections; you will need to use retrieve() to pull in the full state of the remote resource from the API. + * + * @param array $options {@see \OpenStack\Identity\v3\Api::getRoleAssignments} + * + * @return \Generator + */ + public function listRoleAssignments(array $options = []): \Generator + { + return $this->model(Models\Assignment::class)->enumerate($this->api->getRoleAssignments(), $options); + } + + /** + * Creates a new policy according to the provided options. + * + * @param array $options {@see \OpenStack\Identity\v3\Api::postPolicies} + */ + public function createPolicy(array $options): Models\Policy + { + return $this->model(Models\Policy::class)->create($options); + } + + /** + * Returns a generator which will yield a collection of policy objects. The elements which generators yield can be + * accessed using a foreach loop. Often the API will not return the full state of the resource in collections; you + * will need to use retrieve() to pull in the full state of the remote resource from the API. + * + * @param array $options {@see \OpenStack\Identity\v3\Api::getPolicies} + * + * @return \Generator + */ + public function listPolicies(array $options = []): \Generator + { + return $this->model(Models\Policy::class)->enumerate($this->api->getPolicies(), $options); + } + + /** + * Retrieves a policy object and populates its unique identifier object. This operation will not perform a GET or + * HEAD request by default; you will need to call retrieve() if you want to pull in remote state from the API. + * + * @param string $id The unique ID of the policy + */ + public function getPolicy(string $id): Models\Policy + { + return $this->model(Models\Policy::class, ['id' => $id]); + } +} diff --git a/3rdparty/php-opencloud/openstack/src/Images/v2/Api.php b/3rdparty/php-opencloud/openstack/src/Images/v2/Api.php new file mode 100644 index 00000000..0616763f --- /dev/null +++ b/3rdparty/php-opencloud/openstack/src/Images/v2/Api.php @@ -0,0 +1,196 @@ +params = new Params(); + $this->basePath = 'v2/'; + } + + public function postImages(): array + { + return [ + 'method' => 'POST', + 'path' => $this->basePath.'images', + 'params' => [ + 'name' => $this->params->imageName(), + 'visibility' => $this->params->visibility(), + 'tags' => $this->params->tags(), + 'containerFormat' => $this->params->containerFormat(), + 'diskFormat' => $this->params->diskFormat(), + 'minDisk' => $this->params->minDisk(), + 'minRam' => $this->params->minRam(), + 'protected' => $this->params->protectedParam(), + ], + ]; + } + + public function getImages(): array + { + return [ + 'method' => 'GET', + 'path' => $this->basePath.'images', + 'params' => [ + 'limit' => $this->params->limit(), + 'marker' => $this->params->marker(), + 'sortKey' => $this->params->sortKey(), + 'sortDir' => $this->params->sortDir(), + 'name' => $this->params->queryName(), + 'visibility' => $this->params->queryVisibility(), + 'memberStatus' => $this->params->queryMemberStatus(), + 'owner' => $this->params->queryOwner(), + 'status' => $this->params->queryStatus(), + 'sizeMin' => $this->params->querySizeMin(), + 'sizeMax' => $this->params->querySizeMax(), + 'tag' => $this->params->queryTag(), + ], + ]; + } + + public function getImage(): array + { + return [ + 'method' => 'GET', + 'path' => $this->basePath.'images/{id}', + 'params' => ['id' => $this->params->idPath()], + ]; + } + + public function patchImage(): array + { + return [ + 'method' => 'PATCH', + 'path' => $this->basePath.'images/{id}', + 'params' => [ + 'id' => $this->params->idPath(), + 'patchDoc' => $this->params->patchDoc(), + 'contentType' => $this->params->contentType(), + ], + ]; + } + + public function deleteImage(): array + { + return [ + 'method' => 'DELETE', + 'path' => $this->basePath.'images/{id}', + 'params' => ['id' => $this->params->idPath()], + ]; + } + + public function reactivateImage(): array + { + return [ + 'method' => 'POST', + 'path' => $this->basePath.'images/{id}/actions/reactivate', + 'params' => ['id' => $this->params->idPath()], + ]; + } + + public function deactivateImage(): array + { + return [ + 'method' => 'POST', + 'path' => $this->basePath.'images/{id}/actions/deactivate', + 'params' => ['id' => $this->params->idPath()], + ]; + } + + public function postImageData(): array + { + return [ + 'method' => 'PUT', + 'path' => $this->basePath.'images/{id}/file', + 'params' => [ + 'id' => $this->params->idPath(), + 'data' => $this->params->data(), + 'contentType' => $this->params->contentType(), + ], + ]; + } + + public function getImageData(): array + { + return [ + 'method' => 'GET', + 'path' => $this->basePath.'images/{id}/file', + 'params' => ['id' => $this->params->idPath()], + ]; + } + + public function getImageSchema(): array + { + return [ + 'method' => 'GET', + 'path' => $this->basePath.'schemas/image', + 'params' => [], + ]; + } + + public function postImageMembers(): array + { + return [ + 'method' => 'POST', + 'path' => $this->basePath.'images/{imageId}/members', + 'params' => [ + 'imageId' => $this->params->idPath(), + 'id' => $this->params->memberId(), + ], + ]; + } + + public function getImageMembers(): array + { + return [ + 'method' => 'GET', + 'path' => $this->basePath.'images/{imageId}/members', + 'params' => ['imageId' => $this->params->idPath()], + ]; + } + + public function getImageMember(): array + { + return [ + 'method' => 'GET', + 'path' => $this->basePath.'images/{imageId}/members/{id}', + 'params' => [ + 'imageId' => $this->params->idPath(), + 'id' => $this->params->idPath(), + ], + ]; + } + + public function deleteImageMember(): array + { + return [ + 'method' => 'DELETE', + 'path' => $this->basePath.'images/{imageId}/members/{id}', + 'params' => [ + 'imageId' => $this->params->idPath(), + 'id' => $this->params->idPath(), + ], + ]; + } + + public function putImageMember(): array + { + return [ + 'method' => 'PUT', + 'path' => $this->basePath.'images/{imageId}/members/{id}', + 'params' => [ + 'imageId' => $this->params->idPath(), + 'id' => $this->params->idPath(), + 'status' => $this->params->status(), + ], + ]; + } +} diff --git a/3rdparty/php-opencloud/openstack/src/Images/v2/JsonPatch.php b/3rdparty/php-opencloud/openstack/src/Images/v2/JsonPatch.php new file mode 100644 index 00000000..0108ac85 --- /dev/null +++ b/3rdparty/php-opencloud/openstack/src/Images/v2/JsonPatch.php @@ -0,0 +1,66 @@ + $changeSet) { + if ('remove' == $changeSet['op'] && in_array($changeSet['path'], $restrictedProps)) { + unset($diff[$i]); + } + } + + return $diff; + } + + /** + * {@inheritdoc} + * + * We need to override the proper way to handle objects because Glance v2 does not + * support whole document replacement with empty JSON pointers. + */ + protected function handleObject(\stdClass $srcStruct, \stdClass $desStruct, string $path): array + { + $changes = []; + + foreach ($desStruct as $key => $val) { + if (!property_exists($srcStruct, $key)) { + $changes[] = $this->makePatch(self::OP_ADD, $this->path($path, $key), $val); + } elseif ($srcStruct->$key != $val) { + $changes = array_merge($changes, $this->makeDiff($srcStruct->$key, $val, $this->path($path, $key))); + } + } + + if ($this->shouldPartiallyReplace($desStruct, $srcStruct)) { + foreach ($srcStruct as $key => $val) { + if (!property_exists($desStruct, $key)) { + $changes[] = $this->makePatch(self::OP_REMOVE, $this->path($path, $key)); + } + } + } + + return $changes; + } + + protected function handleArray(array $srcStruct, array $desStruct, string $path): array + { + $changes = []; + + if ($srcStruct != $desStruct) { + if ($diff = $this->arrayDiff($desStruct, $srcStruct)) { + $changes[] = $this->makePatch(self::OP_REPLACE, $path, $desStruct); + } + foreach ($srcStruct as $key => $val) { + if (!in_array($val, $desStruct, true)) { + $changes[] = $this->makePatch(self::OP_REMOVE, $this->path($path, $key)); + } + } + } + + return $changes; + } +} diff --git a/3rdparty/php-opencloud/openstack/src/Images/v2/Models/Image.php b/3rdparty/php-opencloud/openstack/src/Images/v2/Models/Image.php new file mode 100644 index 00000000..9cff176b --- /dev/null +++ b/3rdparty/php-opencloud/openstack/src/Images/v2/Models/Image.php @@ -0,0 +1,225 @@ + 'containerFormat', + 'disk_format' => 'diskFormat', + 'min_disk' => 'minDisk', + 'owner' => 'ownerId', + 'min_ram' => 'minRam', + 'virtual_size' => 'virtualSize', + ]; + + protected function getAliases(): array + { + return parent::getAliases() + [ + 'created_at' => new Alias('createdAt', \DateTimeImmutable::class), + 'updated_at' => new Alias('updatedAt', \DateTimeImmutable::class), + 'fileUri' => new Alias('fileUri', \GuzzleHttp\Psr7\Uri::class), + 'schemaUri' => new Alias('schemaUri', \GuzzleHttp\Psr7\Uri::class), + ]; + } + + public function populateFromArray(array $data): self + { + parent::populateFromArray($data); + + $baseUri = $this->getHttpBaseUrl(); + + if (isset($data['file'])) { + $this->fileUri = Utils::appendPath($baseUri, $data['file']); + } + + if (isset($data['schema'])) { + $this->schemaUri = Utils::appendPath($baseUri, $data['schema']); + } + + return $this; + } + + public function create(array $data): Creatable + { + $response = $this->execute($this->api->postImages(), $data); + + return $this->populateFromResponse($response); + } + + public function retrieve() + { + $response = $this->executeWithState($this->api->getImage()); + $this->populateFromResponse($response); + } + + private function getSchema(): Schema + { + if (null === $this->jsonSchema) { + $response = $this->execute($this->api->getImageSchema()); + $this->jsonSchema = new Schema(Utils::jsonDecode($response, false)); + } + + return $this->jsonSchema; + } + + public function update(array $data) + { + // retrieve latest state so we can accurately produce a diff + $this->retrieve(); + + $schema = $this->getSchema(); + $data = (object) $data; + $aliasNames = array_map( + function (Alias $a) { + return $a->propertyName; + }, + $this->getAliases() + ); + + // formulate src and des structures + $des = $schema->normalizeObject($data, $aliasNames); + $src = $schema->normalizeObject($this, $aliasNames); + + // validate user input + $schema->validate($des); + if (!$schema->isValid()) { + throw new \RuntimeException($schema->getErrorString()); + } + + // formulate diff + $patch = new JsonPatch(); + $diff = $patch->disableRestrictedPropRemovals($patch->diff($src, $des), $schema->getPropertyPaths()); + $json = json_encode($diff, JSON_UNESCAPED_SLASHES); + + // execute patch operation + $response = $this->execute($this->api->patchImage(), [ + 'id' => $this->id, + 'patchDoc' => $json, + 'contentType' => 'application/openstack-images-v2.1-json-patch', + ]); + + $this->populateFromResponse($response); + } + + public function delete() + { + $this->executeWithState($this->api->deleteImage()); + } + + public function deactivate() + { + $this->executeWithState($this->api->deactivateImage()); + } + + public function reactivate() + { + $this->executeWithState($this->api->reactivateImage()); + } + + public function uploadData(StreamInterface $stream) + { + $this->execute($this->api->postImageData(), [ + 'id' => $this->id, + 'data' => $stream, + 'contentType' => 'application/octet-stream', + ]); + } + + public function downloadData(): StreamInterface + { + $response = $this->executeWithState($this->api->getImageData()); + + return $response->getBody(); + } + + public function addMember($memberId): Member + { + return $this->model(Member::class, ['imageId' => $this->id, 'id' => $memberId])->create([]); + } + + /** + * @return \Generator + */ + public function listMembers(): \Generator + { + return $this->model(Member::class)->enumerate($this->api->getImageMembers(), ['imageId' => $this->id]); + } + + public function getMember($memberId): Member + { + return $this->model(Member::class, ['imageId' => $this->id, 'id' => $memberId]); + } +} diff --git a/3rdparty/php-opencloud/openstack/src/Images/v2/Models/Member.php b/3rdparty/php-opencloud/openstack/src/Images/v2/Models/Member.php new file mode 100644 index 00000000..9023d281 --- /dev/null +++ b/3rdparty/php-opencloud/openstack/src/Images/v2/Models/Member.php @@ -0,0 +1,77 @@ + 'id', + 'image_id' => 'imageId', + ]; + + protected function getAliases(): array + { + return parent::getAliases() + [ + 'created_at' => new Alias('createdAt', \DateTimeImmutable::class), + 'updated_at' => new Alias('updatedAt', \DateTimeImmutable::class), + ]; + } + + public function create(array $userOptions): Creatable + { + $response = $this->executeWithState($this->api->postImageMembers()); + + return $this->populateFromResponse($response); + } + + public function retrieve() + { + $response = $this->executeWithState($this->api->getImageMember()); + $this->populateFromResponse($response); + } + + public function delete() + { + $this->executeWithState($this->api->deleteImageMember()); + } + + public function updateStatus($status) + { + $this->status = $status; + $this->executeWithState($this->api->putImageMember()); + } +} diff --git a/3rdparty/php-opencloud/openstack/src/Images/v2/Models/Schema.php b/3rdparty/php-opencloud/openstack/src/Images/v2/Models/Schema.php new file mode 100644 index 00000000..10373e6f --- /dev/null +++ b/3rdparty/php-opencloud/openstack/src/Images/v2/Models/Schema.php @@ -0,0 +1,25 @@ +type)) { + $data->type = 'object'; + } + + foreach ($data->properties as $propertyName => &$property) { + if (false !== strpos($property->description, 'READ-ONLY')) { + $property->readOnly = true; + } + } + + parent::__construct($data, $validator); + } +} diff --git a/3rdparty/php-opencloud/openstack/src/Images/v2/Params.php b/3rdparty/php-opencloud/openstack/src/Images/v2/Params.php new file mode 100644 index 00000000..4b8c31fd --- /dev/null +++ b/3rdparty/php-opencloud/openstack/src/Images/v2/Params.php @@ -0,0 +1,214 @@ +name('image'), [ + 'description' => 'Name for the image. The name of an image is not unique to an Image service node. The '. + 'API cannot expect users to know the names of images owned by others.', + 'required' => true, + ]); + } + + public function visibility(): array + { + return [ + 'location' => self::JSON, + 'type' => self::STRING_TYPE, + 'description' => 'Image visibility. Public or private. Default is public.', + 'enum' => ['private', 'public', 'community', 'shared'], + ]; + } + + public function tags(): array + { + return [ + 'location' => self::JSON, + 'type' => self::ARRAY_TYPE, + 'description' => 'Image tags', + 'items' => ['type' => self::STRING_TYPE], + ]; + } + + public function containerFormat(): array + { + return [ + 'location' => self::JSON, + 'type' => self::STRING_TYPE, + 'sentAs' => 'container_format', + 'description' => 'Format of the container. A valid value is ami, ari, aki, bare, ovf, or ova.', + 'enum' => ['ami', 'ari', 'aki', 'bare', 'ovf', 'ova'], + ]; + } + + public function diskFormat(): array + { + return [ + 'location' => self::JSON, + 'type' => self::STRING_TYPE, + 'sentAs' => 'disk_format', + 'description' => 'Format of the container. A valid value is ami, ari, aki, bare, ovf, or ova.', + 'enum' => ['ami', 'ari', 'aki', 'vhd', 'vmdk', 'raw', 'qcow2', 'vdi', 'iso'], + ]; + } + + public function minDisk(): array + { + return [ + 'location' => self::JSON, + 'type' => self::INT_TYPE, + 'sentAs' => 'min_disk', + 'description' => 'Amount of disk space in GB that is required to boot the image.', + ]; + } + + public function minRam(): array + { + return [ + 'location' => self::JSON, + 'type' => self::INT_TYPE, + 'sentAs' => 'min_ram', + 'description' => 'Amount of RAM in GB that is required to boot the image.', + ]; + } + + public function protectedParam(): array + { + return [ + 'location' => self::JSON, + 'type' => self::BOOL_TYPE, + 'description' => 'If true, image is not deletable.', + ]; + } + + public function queryName(): array + { + return [ + 'location' => self::QUERY, + 'type' => self::STRING_TYPE, + 'description' => 'Shows only images with this name. A valid value is the name of the image as a string.', + ]; + } + + public function queryVisibility(): array + { + return [ + 'location' => self::QUERY, + 'type' => self::STRING_TYPE, + 'description' => 'Shows only images with this image visibility value or values.', + 'enum' => ['public', 'private', 'shared'], + ]; + } + + public function queryMemberStatus(): array + { + return [ + 'location' => self::QUERY, + 'type' => self::STRING_TYPE, + 'description' => 'Shows only images with this member status.', + 'enum' => ['accepted', 'pending', 'rejected', 'all'], + ]; + } + + public function queryOwner(): array + { + return [ + 'location' => self::QUERY, + 'type' => self::STRING_TYPE, + 'description' => 'Shows only images that are shared with this owner.', + ]; + } + + public function queryStatus(): array + { + return [ + 'location' => self::QUERY, + 'type' => self::STRING_TYPE, + 'description' => 'Shows only images with this image status.', + 'enum' => ['queued', 'saving', 'active', 'killed', 'deleted', 'pending_delete'], + ]; + } + + public function querySizeMin(): array + { + return [ + 'location' => self::QUERY, + 'type' => self::INT_TYPE, + 'description' => 'Shows only images with this minimum image size.', + ]; + } + + public function querySizeMax(): array + { + return [ + 'location' => self::QUERY, + 'type' => self::INT_TYPE, + 'description' => 'Shows only images with this maximum image size.', + ]; + } + + public function queryTag(): array + { + return [ + 'location' => self::QUERY, + 'type' => self::STRING_TYPE, + 'description' => 'Image tag.', + ]; + } + + public function contentType(): array + { + return [ + 'location' => self::HEADER, + 'type' => self::STRING_TYPE, + 'sentAs' => 'Content-Type', + ]; + } + + public function patchDoc(): array + { + return [ + 'location' => self::RAW, + 'type' => self::STRING_TYPE, + 'required' => true, + 'documented' => false, + ]; + } + + public function data(): array + { + return [ + 'location' => self::RAW, + 'type' => StreamInterface::class, + 'required' => true, + 'documented' => false, + ]; + } + + public function memberId(): array + { + return [ + 'location' => self::JSON, + 'sentAs' => 'member', + 'type' => self::STRING_TYPE, + 'documented' => false, + ]; + } + + public function status(): array + { + return [ + 'location' => self::JSON, + 'type' => self::STRING_TYPE, + 'enum' => ['pending', 'accepted', 'rejected'], + ]; + } +} diff --git a/3rdparty/php-opencloud/openstack/src/Images/v2/Service.php b/3rdparty/php-opencloud/openstack/src/Images/v2/Service.php new file mode 100644 index 00000000..bcb8f434 --- /dev/null +++ b/3rdparty/php-opencloud/openstack/src/Images/v2/Service.php @@ -0,0 +1,38 @@ +model(Image::class)->create($data); + } + + /** + * @return \Generator + */ + public function listImages(array $data = []): \Generator + { + return $this->model(Image::class)->enumerate($this->api->getImages(), $data); + } + + /** + * @param null $id + */ + public function getImage($id = null): Image + { + $image = $this->model(Image::class); + $image->populateFromArray(['id' => $id]); + + return $image; + } +} diff --git a/3rdparty/php-opencloud/openstack/src/Metric/v1/Gnocchi/Api.php b/3rdparty/php-opencloud/openstack/src/Metric/v1/Gnocchi/Api.php new file mode 100644 index 00000000..9ffb23ac --- /dev/null +++ b/3rdparty/php-opencloud/openstack/src/Metric/v1/Gnocchi/Api.php @@ -0,0 +1,133 @@ +params = new Params(); + } + + public function getResources(): array + { + return [ + 'path' => $this->pathPrefix.'/resource/{type}', + 'method' => 'GET', + 'params' => [ + 'limit' => $this->params->limit(), + 'marker' => $this->params->marker(), + 'sort' => $this->params->sort(), + 'type' => $this->params->resourceType(), + ], + ]; + } + + public function getResource(): array + { + return [ + 'path' => $this->pathPrefix.'/resource/{type}/{id}', + 'method' => 'GET', + 'params' => [ + 'id' => $this->params->idUrl('resource'), + 'type' => $this->params->resourceType(), + ], + ]; + } + + public function searchResources(): array + { + return [ + 'path' => $this->pathPrefix.'/search/resource/{type}', + 'method' => 'POST', + 'params' => [ + 'limit' => $this->params->limit(), + 'marker' => $this->params->marker(), + 'sort' => $this->params->sort(), + 'type' => $this->params->resourceType(), + 'criteria' => $this->params->criteria(), + 'contentType' => $this->params->headerContentType(), + ], + ]; + } + + public function getResourceTypes(): array + { + return [ + 'path' => $this->pathPrefix.'/resource_type', + 'method' => 'GET', + 'params' => [], + ]; + } + + public function getMetric(): array + { + return [ + 'path' => $this->pathPrefix.'/metric/{id}', + 'method' => 'GET', + 'params' => [ + 'id' => $this->params->idUrl('metric'), + ], + ]; + } + + public function getMetrics(): array + { + return [ + 'path' => $this->pathPrefix.'/metric', + 'method' => 'GET', + 'params' => [ + 'limit' => $this->params->limit(), + 'marker' => $this->params->marker(), + 'sort' => $this->params->sort(), + ], + ]; + } + + public function getResourceMetrics(): array + { + return [ + 'path' => $this->pathPrefix.'/resource/generic/{resourceId}/metric', + 'method' => 'GET', + 'params' => [ + 'resourceId' => $this->params->idUrl('metric'), + ], + ]; + } + + public function getResourceMetric(): array + { + return [ + 'path' => $this->pathPrefix.'/resource/{type}/{resourceId}/metric/{metric}', + 'method' => 'GET', + 'params' => [ + 'resourceId' => $this->params->idUrl('resource'), + 'metric' => $this->params->idUrl('metric'), + 'type' => $this->params->resourceType(), + ], + ]; + } + + public function getResourceMetricMeasures(): array + { + return [ + 'path' => $this->pathPrefix.'/resource/{type}/{resourceId}/metric/{metric}/measures', + 'method' => 'GET', + 'params' => [ + 'resourceId' => $this->params->idUrl('resource'), + 'metric' => $this->params->idUrl('metric'), + 'type' => $this->params->resourceType(), + 'granularity' => $this->params->granularity(), + 'aggregation' => $this->params->aggregation(), + 'start' => $this->params->measureStart(), + 'stop' => $this->params->measureStop(), + ], + ]; + } +} diff --git a/3rdparty/php-opencloud/openstack/src/Metric/v1/Gnocchi/Models/Metric.php b/3rdparty/php-opencloud/openstack/src/Metric/v1/Gnocchi/Models/Metric.php new file mode 100644 index 00000000..9ad76011 --- /dev/null +++ b/3rdparty/php-opencloud/openstack/src/Metric/v1/Gnocchi/Models/Metric.php @@ -0,0 +1,55 @@ + 'createdByUserId', + 'created_by_project_id' => 'createdByProjectId', + 'archive_policy' => 'archivePolicy', + ]; + + protected function getAliases(): array + { + return parent::getAliases() + [ + 'resource' => new Alias('resource', Resource::class), + ]; + } + + public function retrieve() + { + $response = $this->executeWithState($this->api->getMetric()); + $this->populateFromResponse($response); + } +} diff --git a/3rdparty/php-opencloud/openstack/src/Metric/v1/Gnocchi/Models/Resource.php b/3rdparty/php-opencloud/openstack/src/Metric/v1/Gnocchi/Models/Resource.php new file mode 100644 index 00000000..06c2d694 --- /dev/null +++ b/3rdparty/php-opencloud/openstack/src/Metric/v1/Gnocchi/Models/Resource.php @@ -0,0 +1,150 @@ + 'createdByUserId', + 'started_at' => 'startedAt', + 'display_name' => 'displayName', + 'revision_end' => 'revisionEnd', + 'user_id' => 'userId', + 'created_by_project_id' => 'createdByProjectId', + 'image_ref' => 'imageRef', + 'flavor_id' => 'flavorId', + 'server_group' => 'serverGroup', + 'original_resource_id' => 'originalResourceId', + 'revision_start' => 'revisionStart', + 'project_id' => 'projectId', + 'ended_at' => 'endedAt', + ]; + + public function retrieve() + { + $response = $this->execute($this->api->getResource(), ['type' => $this->type, 'id' => $this->id]); + $this->populateFromResponse($response); + } + + public function getMetric(string $metric): Metric + { + $response = $this->execute( + $this->api->getResourceMetric(), + [ + 'resourceId' => $this->id, + 'metric' => $metric, + 'type' => $this->type, + ] + ); + $metric = $this->model(Metric::class)->populateFromResponse($response); + + return $metric; + } + + /** + * @param array $options {@see \OpenStack\Metric\v1\Gnocchi\Api::getResourceMetricMeasures} + */ + public function getMetricMeasures(array $options = []): array + { + $options = array_merge( + $options, + [ + 'resourceId' => $this->id, + 'type' => $this->type, + ] + ); + + $response = $this->execute($this->api->getResourceMetricMeasures(), $options); + + return Utils::jsonDecode($response); + } + + /** + * @param array $options {@see \OpenStack\Metric\v1\Gnocchi\Api::getResourceMetrics} + * + * @return \Generator + */ + public function listResourceMetrics(array $options = []): \Generator + { + $options['resourceId'] = $this->id; + + return $this->model(Metric::class)->enumerate($this->api->getResourceMetrics(), $options); + } +} diff --git a/3rdparty/php-opencloud/openstack/src/Metric/v1/Gnocchi/Models/ResourceType.php b/3rdparty/php-opencloud/openstack/src/Metric/v1/Gnocchi/Models/ResourceType.php new file mode 100644 index 00000000..56e9bf10 --- /dev/null +++ b/3rdparty/php-opencloud/openstack/src/Metric/v1/Gnocchi/Models/ResourceType.php @@ -0,0 +1,19 @@ + self::URL, + 'type' => self::STRING_TYPE, + 'description' => 'Resource type', + 'required' => true, + ]; + } + + public function sort(): array + { + return [ + 'location' => self::QUERY, + 'type' => self::STRING_TYPE, + 'description' => 'Sort criteria', + ]; + } + + public function criteria(): array + { + return [ + 'location' => self::RAW, + 'type' => self::STRING_TYPE, + 'description' => 'Filter resources based on attributes values. See http://gnocchi.xyz/stable_4.0/rest.html#searching-for-resources', + ]; + } + + public function headerContentType(): array + { + return [ + 'location' => self::HEADER, + 'type' => self::STRING_TYPE, + 'sentAs' => 'Content-Type', + 'description' => 'Override request header', + 'documented' => false, + ]; + } + + public function idUrl($type): array + { + return [ + 'required' => true, + 'location' => self::URL, + 'description' => sprintf('The unique ID, or identifier, for the %s', $type), + ]; + } + + public function granularity(): array + { + return [ + 'location' => self::QUERY, + 'type' => self::STRING_TYPE, + 'description' => 'Specify the granularity to retrieve, rather than all the granularities available', + ]; + } + + public function aggregation(): array + { + return [ + 'location' => self::QUERY, + 'type' => self::STRING_TYPE, + ]; + } + + private function measureTimestamp(string $sentAs): array + { + return [ + 'location' => self::QUERY, + 'type' => self::STRING_TYPE, + 'sentAs' => $sentAs, + 'description' => 'Measure start timestamp which can be either a floating number (UNIX epoch) or an ISO8601 formatted timestamp', + ]; + } + + public function measureStart(): array + { + return $this->measureTimestamp('start'); + } + + public function measureStop(): array + { + return $this->measureTimestamp('stop'); + } +} diff --git a/3rdparty/php-opencloud/openstack/src/Metric/v1/Gnocchi/Service.php b/3rdparty/php-opencloud/openstack/src/Metric/v1/Gnocchi/Service.php new file mode 100644 index 00000000..61be0e28 --- /dev/null +++ b/3rdparty/php-opencloud/openstack/src/Metric/v1/Gnocchi/Service.php @@ -0,0 +1,122 @@ + + */ + public function listResourceTypes(): \Generator + { + return $this->model(ResourceType::class)->enumerate($this->api->getResourceTypes(), []); + } + + /** + * Retrieves a collection of \OpenStack\Metric\v1\Gnocchi\Models\Resource type in a generator format. + * + * @param array $options {@see \OpenStack\Metric\v1\Gnocchi\Api::getResources} + * + * @return \Generator + */ + public function listResources(array $options = []): \Generator + { + $this->injectGenericType($options); + + return $this->model(Resource::class)->enumerate($this->api->getResources(), $options); + } + + /** + * Retrieves a Resource object and populates its unique identifier object. This operation will not perform a GET or + * HEAD request by default; you will need to call retrieve() if you want to pull in remote state from the API. + */ + public function getResource(array $options = []): Resource + { + $this->injectGenericType($options); + + /** @var resource $resource */ + $resource = $this->model(Resource::class); + $resource->populateFromArray($options); + + return $resource; + } + + /** + * Retrieves a collection of \OpenStack\Metric\v1\Gnocchi\Models\Resource type in a generator format. + * + * @param array $options {@see \OpenStack\Metric\v1\Gnocchi\Api::searchResources} + * + * @return \Generator + */ + public function searchResources(array $options = []): \Generator + { + $this->injectGenericType($options); + + /** + * $options['criteria'] must send as STRING + * This will check input $options and perform json_encode if needed. + */ + if (isset($options['criteria']) && !is_string($options['criteria'])) { + $options['criteria'] = json_encode($options['criteria']); + } + + /* + * We need to manually add content-type header to this request + * since searchResources method sends RAW request body. + */ + $options['contentType'] = 'application/json'; + + return $this->model(Resource::class)->enumerate($this->api->searchResources(), $options); + } + + /** + * Retrieves a Metric object and populates its unique identifier object. This operation will not perform a GET or + * HEAD request by default; you will need to call retrieve() if you want to pull in remote state from the API. + */ + public function getMetric(string $id): Metric + { + /** @var Metric $metric */ + $metric = $this->model(Metric::class); + $metric->populateFromArray(['id' => $id]); + + return $metric; + } + + /** + * Retrieves a collection of Metric type in a generator format. + * + * @param array $options {@see \OpenStack\Metric\v1\Gnocchi\Api::getMetrics} + * + * @return \Generator + */ + public function listMetrics(array $options = []): \Generator + { + return $this->model(Metric::class)->enumerate($this->api->getMetrics(), $options); + } + + /** + * If options does not have type, this will inject $options['type'] = 'generic'. + * + * @internal + */ + private function injectGenericType(array &$options) + { + if (empty($options) || !isset($options['type'])) { + $options['type'] = Resource::RESOURCE_TYPE_GENERIC; + } + } +} diff --git a/3rdparty/php-opencloud/openstack/src/Networking/v2/Api.php b/3rdparty/php-opencloud/openstack/src/Networking/v2/Api.php new file mode 100644 index 00000000..e9bfc27b --- /dev/null +++ b/3rdparty/php-opencloud/openstack/src/Networking/v2/Api.php @@ -0,0 +1,711 @@ +params = new Params(); + } + + public function getNetwork(): array + { + return [ + 'method' => 'GET', + 'path' => $this->pathPrefix.'/networks/{id}', + 'params' => ['id' => $this->params->urlId('network')], + ]; + } + + public function getNetworks(): array + { + return [ + 'method' => 'GET', + 'path' => $this->pathPrefix.'/networks', + 'params' => [ + 'name' => $this->params->queryName(), + 'tenantId' => $this->params->queryTenantId(), + 'status' => $this->params->queryStatus(), + 'routerExternal' => $this->params->queryRouterExternal(), + ], + ]; + } + + public function postNetwork(): array + { + return [ + 'path' => $this->pathPrefix.'/networks', + 'method' => 'POST', + 'jsonKey' => 'network', + 'params' => [ + 'name' => $this->params->name('network'), + 'shared' => $this->params->shared(), + 'adminStateUp' => $this->params->adminStateUp(), + 'routerAccessible' => $this->params->routerAccessibleJson(), + 'tenantId' => $this->params->tenantId(), + ], + ]; + } + + public function postNetworks(): array + { + return [ + 'path' => $this->pathPrefix.'/networks', + 'method' => 'POST', + 'jsonKey' => '', + 'params' => [ + 'networks' => [ + 'type' => 'array', + 'description' => 'List of networks', + 'items' => [ + 'type' => 'object', + 'properties' => [ + 'name' => $this->params->name('network'), + 'shared' => $this->params->shared(), + 'adminStateUp' => $this->params->adminStateUp(), + ], + ], + ], + ], + ]; + } + + public function putNetwork(): array + { + return [ + 'method' => 'PUT', + 'path' => $this->pathPrefix.'/networks/{id}', + 'jsonKey' => 'network', + 'params' => [ + 'id' => $this->params->urlId('network'), + 'name' => $this->params->name('network'), + 'shared' => $this->params->shared(), + 'adminStateUp' => $this->params->adminStateUp(), + ], + ]; + } + + public function deleteNetwork(): array + { + return [ + 'method' => 'DELETE', + 'path' => $this->pathPrefix.'/networks/{id}', + 'params' => ['id' => $this->params->urlId('network')], + ]; + } + + public function getSubnet(): array + { + return [ + 'method' => 'GET', + 'path' => $this->pathPrefix.'/subnets/{id}', + 'params' => ['id' => $this->params->urlId('network')], + ]; + } + + public function getSubnets(): array + { + return [ + 'method' => 'GET', + 'path' => $this->pathPrefix.'/subnets', + 'params' => [ + 'name' => $this->params->queryName(), + 'tenantId' => $this->params->queryTenantId(), + ], + ]; + } + + public function postSubnet(): array + { + return [ + 'path' => $this->pathPrefix.'/subnets', + 'method' => 'POST', + 'jsonKey' => 'subnet', + 'params' => [ + 'name' => $this->params->name('subnet'), + 'networkId' => $this->isRequired($this->params->networkId()), + 'ipVersion' => $this->isRequired($this->params->ipVersion()), + 'cidr' => $this->isRequired($this->params->cidr()), + 'tenantId' => $this->params->tenantId(), + 'gatewayIp' => $this->params->gatewayIp(), + 'enableDhcp' => $this->params->enableDhcp(), + 'dnsNameservers' => $this->params->dnsNameservers(), + 'allocationPools' => $this->params->allocationPools(), + 'hostRoutes' => $this->params->hostRoutes(), + ], + ]; + } + + public function postSubnets(): array + { + return [ + 'path' => $this->pathPrefix.'/subnets', + 'method' => 'POST', + 'params' => [ + 'subnets' => [ + 'type' => Params::ARRAY_TYPE, + 'description' => 'List of subnets', + 'items' => [ + 'type' => Params::OBJECT_TYPE, + 'properties' => $this->postSubnet()['params'], + ], + ], + ], + ]; + } + + public function putSubnet(): array + { + return [ + 'method' => 'PUT', + 'path' => $this->pathPrefix.'/subnets/{id}', + 'jsonKey' => 'subnet', + 'params' => [ + 'id' => $this->params->urlId('subnet'), + 'name' => $this->params->name('subnet'), + 'gatewayIp' => $this->params->gatewayIp(), + 'dnsNameservers' => $this->params->dnsNameservers(), + 'allocationPools' => $this->params->allocationPools(), + 'hostRoutes' => $this->params->hostRoutes(), + ], + ]; + } + + public function deleteSubnet(): array + { + return [ + 'method' => 'DELETE', + 'path' => $this->pathPrefix.'/subnets/{id}', + 'params' => ['id' => $this->params->urlId('subnet')], + ]; + } + + public function getPorts(): array + { + return [ + 'method' => 'GET', + 'path' => $this->pathPrefix.'/ports', + 'params' => [ + 'status' => $this->params->statusQuery(), + 'displayName' => $this->params->displayNameQuery(), + 'adminState' => $this->params->adminStateQuery(), + 'networkId' => $this->notRequired($this->params->networkId()), + 'tenantId' => $this->params->tenantIdQuery(), + 'deviceOwner' => $this->params->deviceOwnerQuery(), + 'macAddress' => $this->params->macAddrQuery(), + 'portId' => $this->params->portIdQuery(), + 'securityGroups' => $this->params->secGroupsQuery(), + 'deviceId' => $this->params->deviceIdQuery(), + ], + ]; + } + + public function postSinglePort(): array + { + return [ + 'method' => 'POST', + 'path' => $this->pathPrefix.'/ports', + 'jsonKey' => 'port', + 'params' => [ + 'name' => $this->params->name('port'), + 'adminStateUp' => $this->params->adminStateUp(), + 'tenantId' => $this->params->tenantId(), + 'macAddress' => $this->params->macAddr(), + 'fixedIps' => $this->params->fixedIps(), + 'subnetId' => $this->params->subnetId(), + 'ipAddress' => $this->params->ipAddress(), + 'securityGroups' => $this->params->secGroupIds(), + 'networkId' => $this->params->networkId(), + 'allowedAddressPairs' => $this->params->allowedAddrPairs(), + 'deviceOwner' => $this->params->deviceOwner(), + 'deviceId' => $this->params->deviceId(), + 'portSecurityEnabled' => $this->params->portSecurityEnabled(), + ], + ]; + } + + public function postMultiplePorts(): array + { + return [ + 'method' => 'POST', + 'path' => $this->pathPrefix.'/ports', + 'params' => [ + 'ports' => [ + 'type' => Params::ARRAY_TYPE, + 'items' => [ + 'type' => Params::OBJECT_TYPE, + 'properties' => $this->postSinglePort()['params'], + ], + ], + ], + ]; + } + + public function getPort(): array + { + return [ + 'method' => 'GET', + 'path' => $this->pathPrefix.'/ports/{id}', + 'params' => ['id' => $this->params->idPath()], + ]; + } + + public function putPort(): array + { + return [ + 'method' => 'PUT', + 'path' => $this->pathPrefix.'/ports/{id}', + 'jsonKey' => 'port', + 'params' => [ + 'id' => $this->params->idPath(), + 'name' => $this->params->name('port'), + 'adminStateUp' => $this->params->adminStateUp(), + 'tenantId' => $this->params->tenantId(), + 'macAddress' => $this->params->macAddr(), + 'fixedIps' => $this->params->fixedIps(), + 'subnetId' => $this->params->subnetId(), + 'ipAddress' => $this->params->ipAddress(), + 'securityGroups' => $this->params->secGroupIds(), + 'networkId' => $this->notRequired($this->params->networkId()), + 'allowedAddressPairs' => $this->params->allowedAddrPairs(), + 'deviceOwner' => $this->params->deviceOwner(), + 'deviceId' => $this->params->deviceId(), + ], + ]; + } + + public function deletePort(): array + { + return [ + 'method' => 'DELETE', + 'path' => $this->pathPrefix.'/ports/{id}', + 'params' => ['id' => $this->params->idPath()], + ]; + } + + public function getQuotas(): array + { + return [ + 'method' => 'GET', + 'path' => $this->pathPrefix.'/quotas', + 'params' => [], + ]; + } + + public function getQuota(): array + { + return [ + 'method' => 'GET', + 'path' => $this->pathPrefix.'/quotas/{tenantId}', + 'params' => [], + ]; + } + + public function getQuotaDefault(): array + { + return [ + 'method' => 'GET', + 'path' => $this->pathPrefix.'/quotas/{tenantId}/default', + 'params' => [], + ]; + } + + public function putQuota(): array + { + return [ + 'method' => 'PUT', + 'path' => $this->pathPrefix.'/quotas/{tenantId}', + 'jsonKey' => 'quota', + 'params' => [ + 'tenantId' => $this->params->idPath(), + 'floatingip' => $this->params->quotaLimitFloatingIp(), + 'network' => $this->params->quotaLimitNetwork(), + 'port' => $this->params->quotaLimitPort(), + 'rbacPolicy' => $this->params->quotaLimitRbacPolicy(), + 'router' => $this->params->quotaLimitRouter(), + 'securityGroup' => $this->params->quotaLimitSecurityGroup(), + 'securityGroupRule' => $this->params->quotaLimitSecurityGroupRule(), + 'subnet' => $this->params->quotaLimitSubnet(), + 'subnetpool' => $this->params->quotaLimitSubnetPool(), + ], + ]; + } + + public function deleteQuota(): array + { + return [ + 'method' => 'DELETE', + 'path' => $this->pathPrefix.'/quotas/{tenantId}', + 'params' => [ + 'tenantId' => $this->params->idPath(), + ], + ]; + } + + public function getLoadBalancers(): array + { + return [ + 'method' => 'GET', + 'path' => $this->pathPrefix.'/lbaas/loadbalancers', + 'params' => [], + ]; + } + + public function getLoadBalancer(): array + { + return [ + 'method' => 'GET', + 'path' => $this->pathPrefix.'/lbaas/loadbalancers/{id}', + 'params' => [], + ]; + } + + public function postLoadBalancer(): array + { + return [ + 'method' => 'POST', + 'path' => $this->pathPrefix.'/lbaas/loadbalancers', + 'jsonKey' => 'loadbalancer', + 'params' => [ + 'name' => $this->params->name('loadbalancer'), + 'description' => $this->params->descriptionJson(), + 'tenantId' => $this->params->tenantId(), + 'vipSubnetId' => $this->params->vipSubnetId(), + 'vipAddress' => $this->params->vipAddress(), + 'adminStateUp' => $this->params->adminStateUp(), + 'provider' => $this->params->provider(), + ], + ]; + } + + public function putLoadBalancer(): array + { + return [ + 'method' => 'PUT', + 'path' => $this->pathPrefix.'/lbaas/loadbalancers/{id}', + 'jsonKey' => 'loadbalancer', + 'params' => [ + 'id' => $this->params->idPath(), + 'name' => $this->params->name('loadbalancer'), + 'description' => $this->params->descriptionJson(), + 'AdminStateUp' => $this->params->adminStateUp(), + ], + ]; + } + + public function deleteLoadBalancer(): array + { + return [ + 'method' => 'DELETE', + 'path' => $this->pathPrefix.'/lbaas/loadbalancers/{id}', + 'params' => [ + 'id' => $this->params->idPath(), + ], + ]; + } + + public function getLoadBalancerListeners(): array + { + return [ + 'method' => 'GET', + 'path' => $this->pathPrefix.'/lbaas/listeners', + 'params' => [], + ]; + } + + public function getLoadBalancerListener(): array + { + return [ + 'method' => 'GET', + 'path' => $this->pathPrefix.'/lbaas/listeners/{id}', + 'params' => [], + ]; + } + + public function postLoadBalancerListener(): array + { + return [ + 'method' => 'POST', + 'path' => $this->pathPrefix.'/lbaas/listeners', + 'jsonKey' => 'listener', + 'params' => [ + 'name' => $this->params->name('listener'), + 'description' => $this->params->descriptionJson(), + 'loadbalancerId' => $this->params->loadbalancerId(), + 'protocol' => $this->params->protocol(), + 'protocolPort' => $this->params->protocolPort(), + 'tenantId' => $this->params->tenantId(), + 'adminStateUp' => $this->params->adminStateUp(), + 'connectionLimit' => $this->params->connectionLimit(), + ], + ]; + } + + public function putLoadBalancerListener(): array + { + return [ + 'method' => 'PUT', + 'path' => $this->pathPrefix.'/lbaas/listeners/{id}', + 'jsonKey' => 'listener', + 'params' => [ + 'id' => $this->params->idPath(), + 'name' => $this->params->name('listener'), + 'description' => $this->params->descriptionJson(), + 'adminStateUp' => $this->params->adminStateUp(), + 'connectionLimit' => $this->params->connectionLimit(), + ], + ]; + } + + public function deleteLoadBalancerListener(): array + { + return [ + 'method' => 'DELETE', + 'path' => $this->pathPrefix.'/lbaas/listeners/{id}', + 'params' => [ + 'id' => $this->params->idPath(), + ], + ]; + } + + public function getLoadBalancerPools(): array + { + return [ + 'method' => 'GET', + 'path' => $this->pathPrefix.'/lbaas/pools', + 'params' => [], + ]; + } + + public function getLoadBalancerPool(): array + { + return [ + 'method' => 'GET', + 'path' => $this->pathPrefix.'/lbaas/pools/{id}', + 'params' => [], + ]; + } + + public function postLoadBalancerPool(): array + { + return [ + 'method' => 'POST', + 'path' => $this->pathPrefix.'/lbaas/pools', + 'jsonKey' => 'pool', + 'params' => [ + 'name' => $this->params->name('pool'), + 'description' => $this->params->descriptionJson(), + 'adminStateUp' => $this->params->adminStateUp(), + 'protocol' => $this->params->protocol(), + 'lbAlgorithm' => $this->params->lbAlgorithm(), + 'listenerId' => $this->params->listenerId(), + 'sessionPersistence' => $this->params->sessionPersistence(), + ], + ]; + } + + public function putLoadBalancerPool(): array + { + return [ + 'method' => 'PUT', + 'path' => $this->pathPrefix.'/lbaas/pools/{id}', + 'jsonKey' => 'pool', + 'params' => [ + 'id' => $this->params->idPath(), + 'name' => $this->params->name('pool'), + 'description' => $this->params->descriptionJson(), + 'adminStateUp' => $this->params->adminStateUp(), + 'lbAlgorithm' => $this->params->lbAlgorithm(), + 'sessionPersistence' => $this->params->sessionPersistence(), + ], + ]; + } + + public function deleteLoadBalancerPool(): array + { + return [ + 'method' => 'DELETE', + 'path' => $this->pathPrefix.'/lbaas/pools/{id}', + 'params' => [ + 'id' => $this->params->idPath(), + ], + ]; + } + + public function getLoadBalancerMembers(): array + { + return [ + 'method' => 'GET', + 'path' => $this->pathPrefix.'/lbaas/pools/{poolId}/members', + 'params' => [ + 'poolId' => $this->params->poolId(), + ], + ]; + } + + public function getLoadBalancerMember(): array + { + return [ + 'method' => 'GET', + 'path' => $this->pathPrefix.'/lbaas/pools/{poolId}/members/{id}', + 'params' => [ + 'id' => $this->params->idPath('member'), + 'poolId' => $this->params->poolId(), + ], + ]; + } + + public function postLoadBalancerMember(): array + { + return [ + 'method' => 'POST', + 'path' => $this->pathPrefix.'/lbaas/pools/{poolId}/members', + 'jsonKey' => 'member', + 'params' => [ + 'poolId' => $this->params->poolId(), + 'address' => $this->params->address(), + 'protocolPort' => $this->params->protocolPort(), + 'adminStateUp' => $this->params->adminStateUp(), + 'weight' => $this->params->weight(), + 'subnetId' => $this->params->subnetId(), + ], + ]; + } + + public function putLoadBalancerMember(): array + { + return [ + 'method' => 'PUT', + 'path' => $this->pathPrefix.'/lbaas/pools/{poolId}/members/{id}', + 'jsonKey' => 'member', + 'params' => [ + 'poolId' => $this->params->poolId(), + 'id' => $this->params->idPath(), + 'weight' => $this->params->weight(), + 'adminStateUp' => $this->params->adminStateUp(), + ], + ]; + } + + public function deleteLoadBalancerMember(): array + { + return [ + 'method' => 'DELETE', + 'path' => $this->pathPrefix.'/lbaas/pools/{poolId}/members/{id}', + 'params' => [ + 'poolId' => $this->params->poolId(), + 'id' => $this->params->idPath(), + ], + ]; + } + + public function getLoadBalancerStats(): array + { + return [ + 'method' => 'GET', + 'path' => $this->pathPrefix.'/lbaas/loadbalancers/{loadbalancerId}/stats', + 'params' => [ + 'loadbalancerId' => $this->params->loadBalancerIdUrl(), + ], + ]; + } + + public function getLoadBalancerStatuses(): array + { + return [ + 'method' => 'GET', + 'path' => $this->pathPrefix.'/lbaas/loadbalancers/{loadbalancerId}/statuses', + 'params' => [ + 'loadbalancerId' => $this->params->loadBalancerIdUrl(), + ], + ]; + } + + public function getLoadBalancerHealthMonitors(): array + { + return [ + 'method' => 'GET', + 'path' => $this->pathPrefix.'/lbaas/healthmonitors', + 'params' => [], + ]; + } + + public function getLoadBalancerHealthMonitor(): array + { + return [ + 'method' => 'GET', + 'path' => $this->pathPrefix.'/lbaas/healthmonitors/{id}', + 'params' => [ + 'id' => $this->params->idPath(), + ], + ]; + } + + public function postLoadBalancerHealthMonitor(): array + { + return [ + 'method' => 'POST', + 'path' => $this->pathPrefix.'/lbaas/healthmonitors', + 'jsonKey' => 'healthmonitor', + 'params' => [ + 'type' => $this->params->type(), + 'delay' => $this->params->delay(), + 'timeout' => $this->params->timeout(), + 'maxRetries' => $this->params->maxRetries(), + 'poolId' => $this->params->poolIdJson(), + 'tenantId' => $this->params->tenantId(), + 'adminStateUp' => $this->params->adminStateUp(), + 'httpMethod' => $this->params->httpMethod(), + 'urlPath' => $this->params->urlPath(), + 'expectedCodes' => $this->params->expectedCodes(), + ], + ]; + } + + public function putLoadBalancerHealthMonitor(): array + { + return [ + 'method' => 'PUT', + 'path' => $this->pathPrefix.'/lbaas/healthmonitors/{id}', + 'jsonKey' => 'healthmonitor', + 'params' => [ + 'id' => $this->params->idPath(), + 'delay' => $this->params->delay(), + 'timeout' => $this->params->timeout(), + 'adminStateUp' => $this->params->adminStateUp(), + 'maxRetries' => $this->params->maxRetries(), + 'httpMethod' => $this->params->httpMethod(), + 'urlPath' => $this->params->urlPath(), + 'expectedCodes' => $this->params->expectedCodes(), + ], + ]; + } + + public function deleteLoadBalancerHealthMonitor(): array + { + return [ + 'method' => 'DELETE', + 'path' => $this->pathPrefix.'/lbaas/healthmonitors/{id}', + 'params' => [ + 'id' => $this->params->idPath(), + ], + ]; + } +} diff --git a/3rdparty/php-opencloud/openstack/src/Networking/v2/Extensions/Layer3/Api.php b/3rdparty/php-opencloud/openstack/src/Networking/v2/Extensions/Layer3/Api.php new file mode 100644 index 00000000..c7b3ec8e --- /dev/null +++ b/3rdparty/php-opencloud/openstack/src/Networking/v2/Extensions/Layer3/Api.php @@ -0,0 +1,180 @@ +params = new Params(); + } + + public function postFloatingIps(): array + { + return [ + 'method' => 'POST', + 'path' => $this->pathPrefix.'/floatingips', + 'jsonKey' => 'floatingip', + 'params' => [ + 'tenantId' => $this->params->tenantIdJson(), + 'description' => $this->notRequired($this->params->descriptionJson()), + 'floatingNetworkId' => $this->params->floatingNetworkIdJson(), + 'fixedIpAddress' => $this->params->fixedIpAddressJson(), + 'floatingIpAddress' => $this->params->floatingIpAddressJson(), + 'portId' => $this->params->portIdJson(), + ], + ]; + } + + public function getFloatingIps(): array + { + return [ + 'method' => 'GET', + 'path' => $this->pathPrefix.'/floatingips', + 'params' => [ + 'tenantId' => $this->params->queryTenantId(), + ], + ]; + } + + public function putFloatingIp(): array + { + return [ + 'method' => 'PUT', + 'path' => $this->pathPrefix.'/floatingips/{id}', + 'jsonKey' => 'floatingip', + 'params' => [ + 'id' => $this->params->idPath(), + 'description' => $this->notRequired($this->params->descriptionJson()), + 'floatingNetworkId' => $this->notRequired($this->params->floatingNetworkIdJson()), + 'fixedIpAddress' => $this->params->fixedIpAddressJson(), + 'floatingIpAddress' => $this->params->floatingIpAddressJson(), + 'portId' => $this->params->portIdJson(), + ], + ]; + } + + public function getFloatingIp(): array + { + return [ + 'method' => 'GET', + 'path' => $this->pathPrefix.'/floatingips/{id}', + 'params' => [ + 'id' => $this->params->idPath(), + 'portId' => $this->params->portIdJson(), + ], + ]; + } + + public function deleteFloatingIp(): array + { + return [ + 'method' => 'DELETE', + 'path' => $this->pathPrefix.'/floatingips/{id}', + 'params' => [ + 'id' => $this->params->idPath(), + ], + ]; + } + + public function postRouters(): array + { + return [ + 'method' => 'POST', + 'path' => $this->pathPrefix.'/routers', + 'jsonKey' => 'router', + 'params' => [ + 'name' => $this->params->nameJson(), + 'externalGatewayInfo' => $this->params->externalGatewayInfo(), + 'adminStateUp' => $this->params->adminStateUp(), + 'tenantId' => $this->params->tenantId(), + 'distributed' => $this->params->distributedJson(), + 'ha' => $this->params->haJson(), + ], + ]; + } + + public function getRouters(): array + { + return [ + 'method' => 'GET', + 'path' => $this->pathPrefix.'/routers', + 'params' => [ + 'name' => $this->params->queryName(), + 'tenantId' => $this->params->queryTenantId(), + ], + ]; + } + + public function putRouter(): array + { + return [ + 'method' => 'PUT', + 'path' => $this->pathPrefix.'/routers/{id}', + 'jsonKey' => 'router', + 'params' => [ + 'id' => $this->params->idPath(), + 'name' => $this->params->nameJson(), + 'externalGatewayInfo' => $this->params->externalGatewayInfo(), + 'adminStateUp' => $this->params->adminStateUp(), + ], + ]; + } + + public function getRouter(): array + { + return [ + 'method' => 'GET', + 'path' => $this->pathPrefix.'/routers/{id}', + 'params' => [ + 'id' => $this->params->idPath(), + ], + ]; + } + + public function deleteRouter(): array + { + return [ + 'method' => 'DELETE', + 'path' => $this->pathPrefix.'/routers/{id}', + 'params' => [ + 'id' => $this->params->idPath(), + ], + ]; + } + + public function putAddInterface() + { + return [ + 'method' => 'PUT', + 'path' => $this->pathPrefix.'/routers/{id}/add_router_interface', + 'params' => [ + 'id' => $this->params->idPath(), + 'subnetId' => $this->params->subnetId(), + 'portId' => $this->params->portIdJson(), + ], + ]; + } + + public function putRemoveInterface() + { + return [ + 'method' => 'PUT', + 'path' => $this->pathPrefix.'/routers/{id}/remove_router_interface', + 'params' => [ + 'id' => $this->params->idPath(), + 'subnetId' => $this->params->subnetId(), + 'portId' => $this->params->portIdJson(), + ], + ]; + } +} diff --git a/3rdparty/php-opencloud/openstack/src/Networking/v2/Extensions/Layer3/ApiTrait.php b/3rdparty/php-opencloud/openstack/src/Networking/v2/Extensions/Layer3/ApiTrait.php new file mode 100644 index 00000000..362f9bd7 --- /dev/null +++ b/3rdparty/php-opencloud/openstack/src/Networking/v2/Extensions/Layer3/ApiTrait.php @@ -0,0 +1,175 @@ + 'POST', + 'path' => $this->pathPrefix.'/floatingips', + 'jsonKey' => 'floatingip', + 'params' => [ + 'tenantId' => $this->params->tenantIdJson(), + 'description' => $this->notRequired($this->params->descriptionJson()), + 'floatingNetworkId' => $this->params->floatingNetworkIdJson(), + 'subnetId' => $this->notRequired($this->params->subnetIdJson()), + 'fixedIpAddress' => $this->params->fixedIpAddressJson(), + 'floatingIpAddress' => $this->params->floatingIpAddressJson(), + 'portId' => $this->params->portIdJson(), + ], + ]; + } + + public function getFloatingIps(): array + { + return [ + 'method' => 'GET', + 'path' => $this->pathPrefix.'/floatingips', + 'params' => [ + 'tenantId' => $this->params->queryTenantId(), + ], + ]; + } + + public function putFloatingIp(): array + { + return [ + 'method' => 'PUT', + 'path' => $this->pathPrefix.'/floatingips/{id}', + 'jsonKey' => 'floatingip', + 'params' => [ + 'id' => $this->params->idPath(), + 'description' => $this->notRequired($this->params->descriptionJson()), + 'floatingNetworkId' => $this->notRequired($this->params->floatingNetworkIdJson()), + 'fixedIpAddress' => $this->params->fixedIpAddressJson(), + 'floatingIpAddress' => $this->params->floatingIpAddressJson(), + 'portId' => $this->params->portIdJson(), + ], + ]; + } + + public function getFloatingIp(): array + { + return [ + 'method' => 'GET', + 'path' => $this->pathPrefix.'/floatingips/{id}', + 'params' => [ + 'id' => $this->params->idPath(), + 'portId' => $this->params->portIdJson(), + ], + ]; + } + + public function deleteFloatingIp(): array + { + return [ + 'method' => 'DELETE', + 'path' => $this->pathPrefix.'/floatingips/{id}', + 'params' => [ + 'id' => $this->params->idPath(), + ], + ]; + } + + public function postRouters(): array + { + return [ + 'method' => 'POST', + 'path' => $this->pathPrefix.'/routers', + 'jsonKey' => 'router', + 'params' => [ + 'name' => $this->params->nameJson(), + 'externalGatewayInfo' => $this->params->externalGatewayInfo(), + 'adminStateUp' => $this->params->adminStateUp(), + 'tenantId' => $this->params->tenantId(), + 'distributed' => $this->params->distributedJson(), + 'ha' => $this->params->haJson(), + ], + ]; + } + + public function getRouters(): array + { + return [ + 'method' => 'GET', + 'path' => $this->pathPrefix.'/routers', + 'params' => [ + 'name' => $this->params->queryName(), + 'tenantId' => $this->params->queryTenantId(), + ], + ]; + } + + public function putRouter(): array + { + return [ + 'method' => 'PUT', + 'path' => $this->pathPrefix.'/routers/{id}', + 'jsonKey' => 'router', + 'params' => [ + 'id' => $this->params->idPath(), + 'name' => $this->params->nameJson(), + 'externalGatewayInfo' => $this->params->externalGatewayInfo(), + 'adminStateUp' => $this->params->adminStateUp(), + ], + ]; + } + + public function getRouter(): array + { + return [ + 'method' => 'GET', + 'path' => $this->pathPrefix.'/routers/{id}', + 'params' => [ + 'id' => $this->params->idPath(), + ], + ]; + } + + public function deleteRouter(): array + { + return [ + 'method' => 'DELETE', + 'path' => $this->pathPrefix.'/routers/{id}', + 'params' => [ + 'id' => $this->params->idPath(), + ], + ]; + } + + public function putAddInterface(): array + { + return [ + 'method' => 'PUT', + 'path' => $this->pathPrefix.'/routers/{id}/add_router_interface', + 'params' => [ + 'id' => $this->params->idPath(), + 'subnetId' => $this->params->subnetId(), + 'portId' => $this->params->portIdJson(), + ], + ]; + } + + public function putRemoveInterface(): array + { + return [ + 'method' => 'PUT', + 'path' => $this->pathPrefix.'/routers/{id}/remove_router_interface', + 'params' => [ + 'id' => $this->params->idPath(), + 'subnetId' => $this->params->subnetId(), + 'portId' => $this->params->portIdJson(), + ], + ]; + } +} diff --git a/3rdparty/php-opencloud/openstack/src/Networking/v2/Extensions/Layer3/Models/FixedIp.php b/3rdparty/php-opencloud/openstack/src/Networking/v2/Extensions/Layer3/Models/FixedIp.php new file mode 100644 index 00000000..b22d163c --- /dev/null +++ b/3rdparty/php-opencloud/openstack/src/Networking/v2/Extensions/Layer3/Models/FixedIp.php @@ -0,0 +1,19 @@ + 'subnetId', + 'ip_address' => 'ip', + ]; +} diff --git a/3rdparty/php-opencloud/openstack/src/Networking/v2/Extensions/Layer3/Models/FloatingIp.php b/3rdparty/php-opencloud/openstack/src/Networking/v2/Extensions/Layer3/Models/FloatingIp.php new file mode 100644 index 00000000..2a0ecb92 --- /dev/null +++ b/3rdparty/php-opencloud/openstack/src/Networking/v2/Extensions/Layer3/Models/FloatingIp.php @@ -0,0 +1,88 @@ + 'floatingNetworkId', + 'router_id' => 'routerId', + 'fixed_ip_address' => 'fixedIpAddress', + 'floating_ip_address' => 'floatingIpAddress', + 'tenant_id' => 'tenantId', + 'port_id' => 'portId', + ]; + + protected $resourceKey = 'floatingip'; + protected $resourcesKey = 'floatingips'; + + /** + * {@inheritdoc} + */ + public function create(array $userOptions): Creatable + { + $response = $this->execute($this->api->postFloatingIps(), $userOptions); + + return $this->populateFromResponse($response); + } + + public function update() + { + $response = $this->executeWithState($this->api->putFloatingIp()); + $this->populateFromResponse($response); + } + + public function delete() + { + $this->executeWithState($this->api->deleteFloatingIp()); + } + + public function retrieve() + { + $response = $this->executeWithState($this->api->getFloatingIp()); + $this->populateFromResponse($response); + } + + public function associatePort(string $portId) + { + $this->execute($this->api->putFloatingIp(), ['id' => $this->id, 'portId' => $portId]); + } +} diff --git a/3rdparty/php-opencloud/openstack/src/Networking/v2/Extensions/Layer3/Models/GatewayInfo.php b/3rdparty/php-opencloud/openstack/src/Networking/v2/Extensions/Layer3/Models/GatewayInfo.php new file mode 100644 index 00000000..9fff1ed7 --- /dev/null +++ b/3rdparty/php-opencloud/openstack/src/Networking/v2/Extensions/Layer3/Models/GatewayInfo.php @@ -0,0 +1,30 @@ + 'networkId', + 'enable_snat' => 'enableSnat', + ]; + + protected function getAliases(): array + { + return parent::getAliases() + [ + 'external_fixed_ips' => new Alias('fixedIps', FixedIp::class, true), + ]; + } +} diff --git a/3rdparty/php-opencloud/openstack/src/Networking/v2/Extensions/Layer3/Models/Router.php b/3rdparty/php-opencloud/openstack/src/Networking/v2/Extensions/Layer3/Models/Router.php new file mode 100644 index 00000000..6fcb9a26 --- /dev/null +++ b/3rdparty/php-opencloud/openstack/src/Networking/v2/Extensions/Layer3/Models/Router.php @@ -0,0 +1,101 @@ + 'adminStateUp', + 'tenant_id' => 'tenantId', + ]; + + protected function getAliases(): array + { + return parent::getAliases() + [ + 'external_gateway_info' => new Alias('externalGatewayInfo', GatewayInfo::class), + ]; + } + + /** + * {@inheritdoc} + */ + public function create(array $userOptions): Creatable + { + $response = $this->execute($this->api->postRouters(), $userOptions); + + return $this->populateFromResponse($response); + } + + public function update() + { + $response = $this->executeWithState($this->api->putRouter()); + $this->populateFromResponse($response); + } + + public function retrieve() + { + $response = $this->executeWithState($this->api->getRouter()); + $this->populateFromResponse($response); + } + + public function delete() + { + $this->executeWithState($this->api->deleteRouter()); + } + + /** + * @param array $userOptions {@see \OpenStack\Networking\v2\Extensions\Layer3\Api::putAddInterface} + */ + public function addInterface(array $userOptions) + { + $userOptions['id'] = $this->id; + $this->execute($this->api->putAddInterface(), $userOptions); + } + + /** + * @param array $userOptions {@see \OpenStack\Networking\v2\Extensions\Layer3\Api::putRemoveInterface} + */ + public function removeInterface(array $userOptions) + { + $userOptions['id'] = $this->id; + $this->execute($this->api->putRemoveInterface(), $userOptions); + } +} diff --git a/3rdparty/php-opencloud/openstack/src/Networking/v2/Extensions/Layer3/Params.php b/3rdparty/php-opencloud/openstack/src/Networking/v2/Extensions/Layer3/Params.php new file mode 100644 index 00000000..ef6d2787 --- /dev/null +++ b/3rdparty/php-opencloud/openstack/src/Networking/v2/Extensions/Layer3/Params.php @@ -0,0 +1,130 @@ + self::STRING_TYPE, + 'description' => 'The description of the floating IP.', + 'sentAs' => 'description', + ]; + } + + public function tenantIdJson(): array + { + return [ + 'type' => self::STRING_TYPE, + 'description' => 'The UUID of the tenant. Only administrative users can specify a tenant UUID other than their own.', + 'sentAs' => 'tenant_id', + ]; + } + + public function floatingNetworkIdJson(): array + { + return [ + 'type' => self::STRING_TYPE, + 'description' => 'The UUID of the network associated with the floating IP.', + 'sentAs' => 'floating_network_id', + 'required' => true, + ]; + } + + public function fixedIpAddressJson(): array + { + return [ + 'type' => self::STRING_TYPE, + 'description' => 'The fixed IP address that is associated with the floating IP. To associate the floating IP with a fixed IP at creation time, you must specify the identifier of the internal port. If an internal port has multiple associated IP addresses, the service chooses the first IP address unless you explicitly define a fixed IP address in the fixed_ip_address parameter.', + 'sentAs' => 'fixed_ip_address', + ]; + } + + public function floatingIpAddressJson(): array + { + return [ + 'type' => self::STRING_TYPE, + 'description' => 'The floating IP address.', + 'sentAs' => 'floating_ip_address', + ]; + } + + public function portIdJson(): array + { + return [ + 'type' => self::STRING_TYPE, + 'description' => 'The UUID of the port.', + 'sentAs' => 'port_id', + ]; + } + + public function enableSnatJson(): array + { + return [ + 'type' => self::BOOL_TYPE, + 'description' => 'Enable Source NAT (SNAT) attribute, a part of ext-gw-mode extension. When a gateway is attached to a router using an L3 extension, Network Address Translation (NAT) is enabled for traffic generated by subnets attached to the router.', + 'location' => self::JSON, + 'sentAs' => 'enable_snat', + ]; + } + + public function ipJson(): array + { + return [ + 'type' => self::STRING_TYPE, + ]; + } + + public function externalFixedIpsJson(): array + { + return [ + 'type' => self::ARRAY_TYPE, + 'location' => self::JSON, + 'sentAs' => 'external_fixed_ips', + 'items' => [ + 'type' => self::OBJECT_TYPE, + 'properties' => [ + 'subnetId' => $this->subnetId(), + 'ip' => $this->ipJson(), + ], + ], + ]; + } + + public function externalGatewayInfo(): array + { + return [ + 'type' => self::OBJECT_TYPE, + 'sentAs' => 'external_gateway_info', + 'properties' => [ + 'networkId' => $this->networkId(), + 'enableSnat' => $this->enableSnatJson(), + 'fixedIps' => $this->externalFixedIpsJson(), + ], + ]; + } + + public function distributedJson(): array + { + return [ + 'type' => self::BOOL_TYPE, + 'location' => self::JSON, + 'description' => 'If true, indicates a distributed router.', + ]; + } + + public function haJson(): array + { + return [ + 'type' => self::BOOL_TYPE, + 'location' => self::JSON, + 'description' => 'If true, indicates a highly-available router.', + ]; + } +} diff --git a/3rdparty/php-opencloud/openstack/src/Networking/v2/Extensions/Layer3/ParamsTrait.php b/3rdparty/php-opencloud/openstack/src/Networking/v2/Extensions/Layer3/ParamsTrait.php new file mode 100644 index 00000000..27465655 --- /dev/null +++ b/3rdparty/php-opencloud/openstack/src/Networking/v2/Extensions/Layer3/ParamsTrait.php @@ -0,0 +1,130 @@ + self::STRING_TYPE, + 'description' => 'The description of the floating IP.', + 'sentAs' => 'description', + ]; + } + + public function floatingNetworkIdJson(): array + { + return [ + 'type' => self::STRING_TYPE, + 'description' => 'The UUID of the network associated with the floating IP.', + 'sentAs' => 'floating_network_id', + 'required' => true, + ]; + } + + public function fixedIpAddressJson(): array + { + return [ + 'type' => self::STRING_TYPE, + 'description' => 'The fixed IP address that is associated with the floating IP. To associate the floating IP with a fixed IP at creation time, you must specify the identifier of the internal port. If an internal port has multiple associated IP addresses, the service chooses the first IP address unless you explicitly define a fixed IP address in the fixed_ip_address parameter.', + 'sentAs' => 'fixed_ip_address', + ]; + } + + public function floatingIpAddressJson(): array + { + return [ + 'type' => self::STRING_TYPE, + 'description' => 'The floating IP address.', + 'sentAs' => 'floating_ip_address', + ]; + } + + public function subnetIdJson(): array + { + return [ + 'type' => self::STRING_TYPE, + 'description' => 'The UUID of the subnet of the floating Network associated with the floating IP.', + 'sentAs' => 'subnet', + ]; + } + + public function portIdJson(): array + { + return [ + 'type' => self::STRING_TYPE, + 'description' => 'The UUID of the port.', + 'sentAs' => 'port_id', + ]; + } + + public function enableSnatJson(): array + { + return [ + 'type' => self::BOOL_TYPE, + 'description' => 'Enable Source NAT (SNAT) attribute, a part of ext-gw-mode extension. When a gateway is attached to a router using an L3 extension, Network Address Translation (NAT) is enabled for traffic generated by subnets attached to the router.', + 'location' => self::JSON, + 'sentAs' => 'enable_snat', + ]; + } + + public function ipJson(): array + { + return [ + 'type' => self::STRING_TYPE, + ]; + } + + public function externalFixedIpsJson(): array + { + return [ + 'type' => self::ARRAY_TYPE, + 'location' => self::JSON, + 'sentAs' => 'external_fixed_ips', + 'items' => [ + 'type' => self::OBJECT_TYPE, + 'properties' => [ + 'subnetId' => $this->subnetId(), + 'ip' => $this->ipJson(), + ], + ], + ]; + } + + public function externalGatewayInfo(): array + { + return [ + 'type' => self::OBJECT_TYPE, + 'sentAs' => 'external_gateway_info', + 'properties' => [ + 'networkId' => $this->networkId(), + 'enableSnat' => $this->enableSnatJson(), + 'fixedIps' => $this->externalFixedIpsJson(), + ], + ]; + } + + public function distributedJson(): array + { + return [ + 'type' => self::BOOL_TYPE, + 'location' => self::JSON, + 'description' => 'If true, indicates a distributed router.', + ]; + } + + public function haJson(): array + { + return [ + 'type' => self::BOOL_TYPE, + 'location' => self::JSON, + 'description' => 'If true, indicates a highly-available router.', + ]; + } +} diff --git a/3rdparty/php-opencloud/openstack/src/Networking/v2/Extensions/Layer3/Service.php b/3rdparty/php-opencloud/openstack/src/Networking/v2/Extensions/Layer3/Service.php new file mode 100644 index 00000000..6d545809 --- /dev/null +++ b/3rdparty/php-opencloud/openstack/src/Networking/v2/Extensions/Layer3/Service.php @@ -0,0 +1,61 @@ +model(FloatingIp::class, $info); + } + + private function router(array $info = []): Router + { + return $this->model(Router::class, $info); + } + + public function createFloatingIp(array $options): FloatingIp + { + return $this->floatingIp()->create($options); + } + + public function getFloatingIp($id): FloatingIp + { + return $this->floatingIp(['id' => $id]); + } + + /** + * @return \Generator + */ + public function listFloatingIps(array $options = []): \Generator + { + return $this->floatingIp()->enumerate($this->api->getFloatingIps(), $options); + } + + public function createRouter(array $options): Router + { + return $this->router()->create($options); + } + + public function getRouter($id): Router + { + return $this->router(['id' => $id]); + } + + /** + * @return \Generator + */ + public function listRouters(array $options = []): \Generator + { + return $this->router()->enumerate($this->api->getRouters(), $options); + } +} diff --git a/3rdparty/php-opencloud/openstack/src/Networking/v2/Extensions/Layer3/ServiceTrait.php b/3rdparty/php-opencloud/openstack/src/Networking/v2/Extensions/Layer3/ServiceTrait.php new file mode 100644 index 00000000..785fca67 --- /dev/null +++ b/3rdparty/php-opencloud/openstack/src/Networking/v2/Extensions/Layer3/ServiceTrait.php @@ -0,0 +1,62 @@ +model(FloatingIp::class, $info); + } + + private function router(array $info = []): Router + { + return $this->model(Router::class, $info); + } + + public function createFloatingIp(array $options): FloatingIp + { + return $this->floatingIp()->create($options); + } + + public function getFloatingIp($id): FloatingIp + { + return $this->floatingIp(['id' => $id]); + } + + /** + * @return \Generator + */ + public function listFloatingIps(array $options = []): \Generator + { + return $this->floatingIp()->enumerate($this->api->getFloatingIps(), $options); + } + + public function createRouter(array $options): Router + { + return $this->router()->create($options); + } + + public function getRouter($id): Router + { + return $this->router(['id' => $id]); + } + + /** + * @return \Generator + */ + public function listRouters(array $options = []): \Generator + { + return $this->router()->enumerate($this->api->getRouters(), $options); + } +} diff --git a/3rdparty/php-opencloud/openstack/src/Networking/v2/Extensions/SecurityGroups/Api.php b/3rdparty/php-opencloud/openstack/src/Networking/v2/Extensions/SecurityGroups/Api.php new file mode 100644 index 00000000..9f2597f9 --- /dev/null +++ b/3rdparty/php-opencloud/openstack/src/Networking/v2/Extensions/SecurityGroups/Api.php @@ -0,0 +1,182 @@ +params = new Params(); + } + + /** + * Returns information about GET security-groups/{security_group_id} HTTP + * operation. + * + * @return array + */ + public function getSecurityGroups() + { + return [ + 'method' => 'GET', + 'path' => $this->pathPrefix.'/security-groups', + 'params' => [ + 'tenantId' => $this->params->queryTenantId(), + 'name' => $this->params->filterName(), + ], + ]; + } + + /** + * Returns information about POST security-groups HTTP operation. + * + * @return array + */ + public function postSecurityGroups() + { + return [ + 'method' => 'POST', + 'path' => $this->pathPrefix.'/security-groups', + 'jsonKey' => 'security_group', + 'params' => [ + 'description' => $this->params->descriptionJson(), + 'name' => $this->params->nameJson(), + ], + ]; + } + + /** + * Returns information about PUT security-groups HTTP operation. + * + * @return array + */ + public function putSecurityGroups() + { + return [ + 'method' => 'PUT', + 'path' => $this->pathPrefix.'/security-groups/{id}', + 'jsonKey' => 'security_group', + 'params' => [ + 'id' => $this->params->idPath(), + 'description' => $this->params->descriptionJson(), + 'name' => $this->params->nameJson(), + ], + ]; + } + + /** + * Returns information about GET security-groups/{security_group_id} HTTP + * operation. + * + * @return array + */ + public function getSecurityGroup() + { + return [ + 'method' => 'GET', + 'path' => $this->pathPrefix.'/security-groups/{id}', + 'params' => [ + 'id' => $this->params->idPath(), + ], + ]; + } + + /** + * Returns information about DELETE security-groups/{security_group_id} HTTP + * operation. + * + * @return array + */ + public function deleteSecurityGroup() + { + return [ + 'method' => 'DELETE', + 'path' => $this->pathPrefix.'/security-groups/{id}', + 'params' => [ + 'id' => $this->params->idPath(), + ], + ]; + } + + /** + * Returns information about GET security-group-rules HTTP operation. + * + * @return array + */ + public function getSecurityRules() + { + return [ + 'method' => 'GET', + 'path' => $this->pathPrefix.'/security-group-rules', + 'params' => [], + ]; + } + + /** + * Returns information about POST security-group-rules HTTP operation. + * + * @return array + */ + public function postSecurityRules() + { + return [ + 'method' => 'POST', + 'path' => $this->pathPrefix.'/security-group-rules', + 'jsonKey' => 'security_group_rule', + 'params' => [ + 'direction' => $this->params->directionJson(), + 'ethertype' => $this->params->ethertypeJson(), + 'securityGroupId' => $this->params->securityGroupIdJson(), + 'portRangeMin' => $this->params->portRangeMinJson(), + 'portRangeMax' => $this->params->portRangeMaxJson(), + 'protocol' => $this->params->protocolJson(), + 'remoteGroupId' => $this->params->remoteGroupIdJson(), + 'remoteIpPrefix' => $this->params->remoteIpPrefixJson(), + 'tenantId' => $this->params->tenantIdJson(), + ], + ]; + } + + /** + * Returns information about DELETE + * security-group-rules/{rules-security-groups-id} HTTP operation. + * + * @return array + */ + public function deleteSecurityRule() + { + return [ + 'method' => 'DELETE', + 'path' => $this->pathPrefix.'/security-group-rules/{id}', + 'params' => [ + 'id' => $this->params->idPath(), + ], + ]; + } + + /** + * Returns information about GET + * security-group-rules/{rules-security-groups-id} HTTP operation. + * + * @return array + */ + public function getSecurityRule() + { + return [ + 'method' => 'GET', + 'path' => $this->pathPrefix.'/security-group-rules/{id}', + 'params' => [ + 'id' => $this->params->idPath(), + ], + ]; + } +} diff --git a/3rdparty/php-opencloud/openstack/src/Networking/v2/Extensions/SecurityGroups/ApiTrait.php b/3rdparty/php-opencloud/openstack/src/Networking/v2/Extensions/SecurityGroups/ApiTrait.php new file mode 100644 index 00000000..6c734041 --- /dev/null +++ b/3rdparty/php-opencloud/openstack/src/Networking/v2/Extensions/SecurityGroups/ApiTrait.php @@ -0,0 +1,158 @@ + 'GET', + 'path' => $this->pathPrefix.'/security-groups', + 'params' => [ + 'tenantId' => $this->params->queryTenantId(), + 'name' => $this->params->filterName(), + ], + ]; + } + + /** + * Returns information about POST security-groups HTTP operation. + */ + public function postSecurityGroups(): array + { + return [ + 'method' => 'POST', + 'path' => $this->pathPrefix.'/security-groups', + 'jsonKey' => 'security_group', + 'params' => [ + 'description' => $this->params->descriptionJson(), + 'name' => $this->params->nameJson(), + ], + ]; + } + + /** + * Returns information about PUT security-groups HTTP operation. + */ + public function putSecurityGroups(): array + { + return [ + 'method' => 'PUT', + 'path' => $this->pathPrefix.'/security-groups/{id}', + 'jsonKey' => 'security_group', + 'params' => [ + 'id' => $this->params->idPath(), + 'description' => $this->params->descriptionJson(), + 'name' => $this->params->nameJson(), + ], + ]; + } + + /** + * Returns information about GET security-groups/{security_group_id} HTTP + * operation. + */ + public function getSecurityGroup(): array + { + return [ + 'method' => 'GET', + 'path' => $this->pathPrefix.'/security-groups/{id}', + 'params' => [ + 'id' => $this->params->idPath(), + ], + ]; + } + + /** + * Returns information about DELETE security-groups/{security_group_id} HTTP + * operation. + */ + public function deleteSecurityGroup(): array + { + return [ + 'method' => 'DELETE', + 'path' => $this->pathPrefix.'/security-groups/{id}', + 'params' => [ + 'id' => $this->params->idPath(), + ], + ]; + } + + /** + * Returns information about GET security-group-rules HTTP operation. + */ + public function getSecurityRules(): array + { + return [ + 'method' => 'GET', + 'path' => $this->pathPrefix.'/security-group-rules', + 'params' => [], + ]; + } + + /** + * Returns information about POST security-group-rules HTTP operation. + */ + public function postSecurityRules(): array + { + return [ + 'method' => 'POST', + 'path' => $this->pathPrefix.'/security-group-rules', + 'jsonKey' => 'security_group_rule', + 'params' => [ + 'direction' => $this->params->directionJson(), + 'ethertype' => $this->params->ethertypeJson(), + 'securityGroupId' => $this->params->securityGroupIdJson(), + 'portRangeMin' => $this->params->portRangeMinJson(), + 'portRangeMax' => $this->params->portRangeMaxJson(), + 'protocol' => $this->params->protocolJson(), + 'remoteGroupId' => $this->params->remoteGroupIdJson(), + 'remoteIpPrefix' => $this->params->remoteIpPrefixJson(), + 'tenantId' => $this->params->tenantIdJson(), + ], + ]; + } + + /** + * Returns information about DELETE + * security-group-rules/{rules-security-groups-id} HTTP operation. + */ + public function deleteSecurityRule(): array + { + return [ + 'method' => 'DELETE', + 'path' => $this->pathPrefix.'/security-group-rules/{id}', + 'params' => [ + 'id' => $this->params->idPath(), + ], + ]; + } + + /** + * Returns information about GET + * security-group-rules/{rules-security-groups-id} HTTP operation. + */ + public function getSecurityRule(): array + { + return [ + 'method' => 'GET', + 'path' => $this->pathPrefix.'/security-group-rules/{id}', + 'params' => [ + 'id' => $this->params->idPath(), + ], + ]; + } +} diff --git a/3rdparty/php-opencloud/openstack/src/Networking/v2/Extensions/SecurityGroups/Models/SecurityGroup.php b/3rdparty/php-opencloud/openstack/src/Networking/v2/Extensions/SecurityGroups/Models/SecurityGroup.php new file mode 100644 index 00000000..1b09a2e4 --- /dev/null +++ b/3rdparty/php-opencloud/openstack/src/Networking/v2/Extensions/SecurityGroups/Models/SecurityGroup.php @@ -0,0 +1,88 @@ + 'tenantId', + ]; + + protected $resourceKey = 'security_group'; + protected $resourcesKey = 'security_groups'; + + protected function getAliases(): array + { + return parent::getAliases() + [ + 'security_group_rules' => new Alias('securityGroupRules', SecurityGroupRule::class, true), + 'rules' => new Alias('securityGroupRules', SecurityGroupRule::class, true), + ]; + } + + /** + * {@inheritdoc} + */ + public function create(array $userOptions): Creatable + { + $response = $this->execute($this->api->postSecurityGroups(), $userOptions); + + return $this->populateFromResponse($response); + } + + public function delete() + { + $this->executeWithState($this->api->deleteSecurityGroup()); + } + + public function retrieve() + { + $response = $this->executeWithState($this->api->getSecurityGroup()); + $this->populateFromResponse($response); + } + + public function update() + { + $response = $this->executeWithState($this->api->putSecurityGroups()); + $this->populateFromResponse($response); + } +} diff --git a/3rdparty/php-opencloud/openstack/src/Networking/v2/Extensions/SecurityGroups/Models/SecurityGroupRule.php b/3rdparty/php-opencloud/openstack/src/Networking/v2/Extensions/SecurityGroups/Models/SecurityGroupRule.php new file mode 100644 index 00000000..4d0319dc --- /dev/null +++ b/3rdparty/php-opencloud/openstack/src/Networking/v2/Extensions/SecurityGroups/Models/SecurityGroupRule.php @@ -0,0 +1,103 @@ + 'portRangeMax', + 'port_range_min' => 'portRangeMin', + 'remote_group_id' => 'remoteGroupId', + 'remote_ip_prefix' => 'remoteIpPrefix', + 'security_group_id' => 'securityGroupId', + 'tenant_id' => 'tenantId', + ]; + + protected $resourceKey = 'security_group_rule'; + + protected $resourcesKey = 'security_group_rules'; + + /** + * {@inheritdoc} + */ + public function create(array $userOptions): Creatable + { + $response = $this->execute($this->api->postSecurityRules(), $userOptions); + + return $this->populateFromResponse($response); + } + + public function delete() + { + $this->executeWithState($this->api->deleteSecurityRule()); + } + + public function retrieve() + { + $response = $this->executeWithState($this->api->getSecurityRule()); + $this->populateFromResponse($response); + } +} diff --git a/3rdparty/php-opencloud/openstack/src/Networking/v2/Extensions/SecurityGroups/Params.php b/3rdparty/php-opencloud/openstack/src/Networking/v2/Extensions/SecurityGroups/Params.php new file mode 100644 index 00000000..ea700511 --- /dev/null +++ b/3rdparty/php-opencloud/openstack/src/Networking/v2/Extensions/SecurityGroups/Params.php @@ -0,0 +1,106 @@ + self::STRING_TYPE, + 'description' => 'Ingress or egress: the direction in which the security group rule is applied. For a compute instance, an ingress security group rule is applied to incoming (ingress) traffic for that instance. An egress rule is applied to traffic leaving the instance.', + ]; + } + + public function ethertypeJson(): array + { + return [ + 'type' => self::STRING_TYPE, + 'description' => 'Must be IPv4 or IPv6, and addresses represented in CIDR must match the ingress or egress rules.', + ]; + } + + public function idJson(): array + { + return [ + 'type' => self::STRING_TYPE, + 'description' => 'The UUID of the security group rule.', + ]; + } + + public function portRangeMaxJson(): array + { + return [ + 'type' => self::STRING_TYPE, + 'sentAs' => 'port_range_max', + 'description' => 'The maximum port number in the range that is matched by the security group rule. The port_range_min attribute constrains the port_range_max attribute. If the protocol is ICMP, this value must be an ICMP type.', + ]; + } + + public function portRangeMinJson(): array + { + return [ + 'sentAs' => 'port_range_min', + 'type' => self::STRING_TYPE, + 'description' => 'The minimum port number in the range that is matched by the security group rule. If the protocol is TCP or UDP, this value must be less than or equal to the port_range_max attribute value. If the protocol is ICMP, this value must be an ICMP type.', + ]; + } + + public function protocolJson(): array + { + return [ + 'type' => self::STRING_TYPE, + 'description' => 'The protocol that is matched by the security group rule. Value is null, icmp, icmpv6, tcp, or udp.', + ]; + } + + public function remoteGroupIdJson(): array + { + return [ + 'sentAs' => 'remote_group_id', + 'type' => self::STRING_TYPE, + 'description' => 'The remote group UUID to associate with this security group rule. You can specify either the remote_group_id or remote_ip_prefix attribute in the request body.', + ]; + } + + public function remoteIpPrefixJson(): array + { + return [ + 'sentAs' => 'remote_ip_prefix', + 'type' => self::STRING_TYPE, + 'description' => 'The remote IP prefix to associate with this security group rule. You can specify either the remote_group_id or remote_ip_prefix attribute in the request body. This attribute value matches the IP prefix as the source IP address of the IP packet.', + ]; + } + + public function securityGroupIdJson(): array + { + return [ + 'sentAs' => 'security_group_id', + 'type' => self::STRING_TYPE, + 'description' => 'The UUID of the security group.', + ]; + } + + public function tenantIdJson(): array + { + return [ + 'sentAs' => 'tenant_id', + 'type' => self::STRING_TYPE, + 'description' => 'The UUID of the tenant who owns the security group rule. Only administrative users can specify a tenant UUID other than their own.', + ]; + } + + public function filterName(): array + { + return [ + 'description' => sprintf('Filter the list result by the human-readable name of the resource'), + 'type' => self::STRING_TYPE, + 'location' => self::QUERY, + ]; + } +} diff --git a/3rdparty/php-opencloud/openstack/src/Networking/v2/Extensions/SecurityGroups/ParamsTrait.php b/3rdparty/php-opencloud/openstack/src/Networking/v2/Extensions/SecurityGroups/ParamsTrait.php new file mode 100644 index 00000000..90225a41 --- /dev/null +++ b/3rdparty/php-opencloud/openstack/src/Networking/v2/Extensions/SecurityGroups/ParamsTrait.php @@ -0,0 +1,97 @@ + self::STRING_TYPE, + 'description' => 'Ingress or egress: the direction in which the security group rule is applied. For a compute instance, an ingress security group rule is applied to incoming (ingress) traffic for that instance. An egress rule is applied to traffic leaving the instance.', + ]; + } + + public function ethertypeJson(): array + { + return [ + 'type' => self::STRING_TYPE, + 'description' => 'Must be IPv4 or IPv6, and addresses represented in CIDR must match the ingress or egress rules.', + ]; + } + + public function idJson(): array + { + return [ + 'type' => self::STRING_TYPE, + 'description' => 'The UUID of the security group rule.', + ]; + } + + public function portRangeMaxJson(): array + { + return [ + 'type' => self::STRING_TYPE, + 'sentAs' => 'port_range_max', + 'description' => 'The maximum port number in the range that is matched by the security group rule. The port_range_min attribute constrains the port_range_max attribute. If the protocol is ICMP, this value must be an ICMP type.', + ]; + } + + public function portRangeMinJson(): array + { + return [ + 'sentAs' => 'port_range_min', + 'type' => self::STRING_TYPE, + 'description' => 'The minimum port number in the range that is matched by the security group rule. If the protocol is TCP or UDP, this value must be less than or equal to the port_range_max attribute value. If the protocol is ICMP, this value must be an ICMP type.', + ]; + } + + public function protocolJson(): array + { + return [ + 'type' => self::STRING_TYPE, + 'description' => 'The protocol that is matched by the security group rule. Value is null, icmp, icmpv6, tcp, or udp.', + ]; + } + + public function remoteGroupIdJson(): array + { + return [ + 'sentAs' => 'remote_group_id', + 'type' => self::STRING_TYPE, + 'description' => 'The remote group UUID to associate with this security group rule. You can specify either the remote_group_id or remote_ip_prefix attribute in the request body.', + ]; + } + + public function remoteIpPrefixJson(): array + { + return [ + 'sentAs' => 'remote_ip_prefix', + 'type' => self::STRING_TYPE, + 'description' => 'The remote IP prefix to associate with this security group rule. You can specify either the remote_group_id or remote_ip_prefix attribute in the request body. This attribute value matches the IP prefix as the source IP address of the IP packet.', + ]; + } + + public function securityGroupIdJson(): array + { + return [ + 'sentAs' => 'security_group_id', + 'type' => self::STRING_TYPE, + 'description' => 'The UUID of the security group.', + ]; + } + + public function filterName(): array + { + return [ + 'description' => 'Filter the list result by the human-readable name of the resource', + 'type' => self::STRING_TYPE, + 'location' => self::QUERY, + ]; + } +} diff --git a/3rdparty/php-opencloud/openstack/src/Networking/v2/Extensions/SecurityGroups/Service.php b/3rdparty/php-opencloud/openstack/src/Networking/v2/Extensions/SecurityGroups/Service.php new file mode 100644 index 00000000..4a1b70b6 --- /dev/null +++ b/3rdparty/php-opencloud/openstack/src/Networking/v2/Extensions/SecurityGroups/Service.php @@ -0,0 +1,63 @@ +model(SecurityGroup::class, $info); + } + + private function securityGroupRule(array $info = []): SecurityGroupRule + { + return $this->model(SecurityGroupRule::class, $info); + } + + /** + * @return \Generator + */ + public function listSecurityGroups(array $options = []): \Generator + { + return $this->securityGroup()->enumerate($this->api->getSecurityGroups(), $options); + } + + public function createSecurityGroup(array $options): SecurityGroup + { + return $this->securityGroup()->create($options); + } + + public function getSecurityGroup(string $id): SecurityGroup + { + return $this->securityGroup(['id' => $id]); + } + + /** + * @return \Generator + */ + public function listSecurityGroupRules(): \Generator + { + return $this->securityGroupRule()->enumerate($this->api->getSecurityRules()); + } + + public function createSecurityGroupRule(array $options): SecurityGroupRule + { + return $this->securityGroupRule()->create($options); + } + + public function getSecurityGroupRule(string $id): SecurityGroupRule + { + return $this->securityGroupRule(['id' => $id]); + } +} diff --git a/3rdparty/php-opencloud/openstack/src/Networking/v2/Extensions/SecurityGroups/ServiceTrait.php b/3rdparty/php-opencloud/openstack/src/Networking/v2/Extensions/SecurityGroups/ServiceTrait.php new file mode 100644 index 00000000..0e0bd940 --- /dev/null +++ b/3rdparty/php-opencloud/openstack/src/Networking/v2/Extensions/SecurityGroups/ServiceTrait.php @@ -0,0 +1,62 @@ +model(SecurityGroup::class, $info); + } + + private function securityGroupRule(array $info = []): SecurityGroupRule + { + return $this->model(SecurityGroupRule::class, $info); + } + + /** + * @return \Generator + */ + public function listSecurityGroups(array $options = []): \Generator + { + return $this->securityGroup()->enumerate($this->api->getSecurityGroups(), $options); + } + + public function createSecurityGroup(array $options): SecurityGroup + { + return $this->securityGroup()->create($options); + } + + public function getSecurityGroup(string $id): SecurityGroup + { + return $this->securityGroup(['id' => $id]); + } + + /** + * @return \Generator + */ + public function listSecurityGroupRules(): \Generator + { + return $this->securityGroupRule()->enumerate($this->api->getSecurityRules()); + } + + public function createSecurityGroupRule(array $options): SecurityGroupRule + { + return $this->securityGroupRule()->create($options); + } + + public function getSecurityGroupRule(string $id): SecurityGroupRule + { + return $this->securityGroupRule(['id' => $id]); + } +} diff --git a/3rdparty/php-opencloud/openstack/src/Networking/v2/Models/InterfaceAttachment.php b/3rdparty/php-opencloud/openstack/src/Networking/v2/Models/InterfaceAttachment.php new file mode 100644 index 00000000..46b430d7 --- /dev/null +++ b/3rdparty/php-opencloud/openstack/src/Networking/v2/Models/InterfaceAttachment.php @@ -0,0 +1,52 @@ + 'portId', + 'net_id' => 'netId', + 'mac_addr' => 'macAddr', + 'subnet_id' => 'subnetId', + 'ip_address' => 'ipAddress', + 'fixed_ips' => 'fixedIps', + 'port_state' => 'portState', + 'server_id' => 'serverId', + ]; +} diff --git a/3rdparty/php-opencloud/openstack/src/Networking/v2/Models/LoadBalancer.php b/3rdparty/php-opencloud/openstack/src/Networking/v2/Models/LoadBalancer.php new file mode 100644 index 00000000..70df8063 --- /dev/null +++ b/3rdparty/php-opencloud/openstack/src/Networking/v2/Models/LoadBalancer.php @@ -0,0 +1,146 @@ + 'tenantId', + 'admin_state_up' => 'adminStateUp', + 'vip_address' => 'vipAddress', + 'vip_subnet_id' => 'vipSubnetId', + 'operating_status' => 'operatingStatus', + 'provisioning_status' => 'provisioningStatus', + ]; + + protected function getAliases(): array + { + return parent::getAliases() + [ + 'listeners' => new Alias('listeners', LoadBalancerListener::class, true), + ]; + } + + public function create(array $userOptions): Creatable + { + $response = $this->execute($this->api->postLoadBalancer(), $userOptions); + + return $this->populateFromResponse($response); + } + + public function retrieve() + { + $response = $this->execute($this->api->getLoadBalancer(), ['id' => (string) $this->id]); + $this->populateFromResponse($response); + } + + public function update() + { + $response = $this->executeWithState($this->api->putLoadBalancer()); + $this->populateFromResponse($response); + } + + public function delete() + { + $this->executeWithState($this->api->deleteLoadBalancer()); + } + + /** + * Add a listener to this load balancer. + */ + public function addListener(array $userOptions = []): LoadBalancerListener + { + $userOptions = array_merge(['loadbalancerId' => $this->id], $userOptions); + + return $this->model(LoadBalancerListener::class)->create($userOptions); + } + + /** + * Get stats for this loadbalancer. + */ + public function getStats(): LoadBalancerStat + { + $model = $this->model(LoadBalancerStat::class, ['loadbalancerId' => $this->id]); + $model->retrieve(); + + return $model; + } + + /** + * Get the status tree for this loadbalancer. + */ + public function getStatuses(): LoadBalancerStatus + { + $model = $this->model(LoadBalancerStatus::class, ['loadbalancerId' => $this->id]); + $model->retrieve(); + + return $model; + } +} diff --git a/3rdparty/php-opencloud/openstack/src/Networking/v2/Models/LoadBalancerHealthMonitor.php b/3rdparty/php-opencloud/openstack/src/Networking/v2/Models/LoadBalancerHealthMonitor.php new file mode 100644 index 00000000..bc25dfdd --- /dev/null +++ b/3rdparty/php-opencloud/openstack/src/Networking/v2/Models/LoadBalancerHealthMonitor.php @@ -0,0 +1,137 @@ + 'tenantId', + 'admin_state_up' => 'adminStateUp', + 'max_retries' => 'maxRetries', + 'http_method' => 'httpMethod', + 'url_path' => 'urlPath', + 'expected_codes' => 'expectedCodes', + 'pool_id' => 'poolId', + 'operating_status' => 'operatingStatus', + 'provisioning_status' => 'provisioningStatus', + ]; + + protected function getAliases(): array + { + return parent::getAliases() + [ + 'pools' => new Alias('pools', LoadBalancerPool::class, true), + ]; + } + + public function create(array $userOptions): Creatable + { + $response = $this->execute($this->api->postLoadBalancerHealthMonitor(), $userOptions); + + return $this->populateFromResponse($response); + } + + public function retrieve() + { + $response = $this->execute($this->api->getLoadBalancerHealthMonitor(), ['id' => (string) $this->id]); + $this->populateFromResponse($response); + } + + public function update() + { + $response = $this->executeWithState($this->api->putLoadBalancerHealthMonitor()); + $this->populateFromResponse($response); + } + + public function delete() + { + $this->executeWithState($this->api->deleteLoadBalancerHealthMonitor()); + } +} diff --git a/3rdparty/php-opencloud/openstack/src/Networking/v2/Models/LoadBalancerListener.php b/3rdparty/php-opencloud/openstack/src/Networking/v2/Models/LoadBalancerListener.php new file mode 100644 index 00000000..bc817506 --- /dev/null +++ b/3rdparty/php-opencloud/openstack/src/Networking/v2/Models/LoadBalancerListener.php @@ -0,0 +1,137 @@ + 'tenantId', + 'admin_state_up' => 'adminStateUp', + 'protocol_port' => 'protocolPort', + 'connection_limit' => 'connectionLimit', + 'default_pool_id' => 'defaultPoolId', + 'loadbalancer_id' => 'loadbalancerId', + 'operating_status' => 'operatingStatus', + 'provisioning_status' => 'provisioningStatus', + ]; + + protected function getAliases(): array + { + return parent::getAliases() + [ + 'pools' => new Alias('pools', LoadBalancerPool::class, true), + 'loadbalancers' => new Alias('loadbalancers', LoadBalancerPool::class, true), + ]; + } + + public function create(array $userOptions): Creatable + { + $response = $this->execute($this->api->postLoadBalancerListener(), $userOptions); + + return $this->populateFromResponse($response); + } + + public function retrieve() + { + $response = $this->execute($this->api->getLoadBalancerListener(), ['id' => (string) $this->id]); + $this->populateFromResponse($response); + } + + public function update() + { + $response = $this->executeWithState($this->api->putLoadBalancerListener()); + $this->populateFromResponse($response); + } + + public function delete() + { + $this->executeWithState($this->api->deleteLoadBalancerListener()); + } +} diff --git a/3rdparty/php-opencloud/openstack/src/Networking/v2/Models/LoadBalancerMember.php b/3rdparty/php-opencloud/openstack/src/Networking/v2/Models/LoadBalancerMember.php new file mode 100644 index 00000000..a4835bd1 --- /dev/null +++ b/3rdparty/php-opencloud/openstack/src/Networking/v2/Models/LoadBalancerMember.php @@ -0,0 +1,108 @@ + 'tenantId', + 'admin_state_up' => 'adminStateUp', + 'protocol_port' => 'protocolPort', + 'subnet_id' => 'subnetId', + 'pool_id' => 'poolId', + 'operating_status' => 'operatingStatus', + 'provisioning_status' => 'provisioningStatus', + ]; + + public function create(array $userOptions): Creatable + { + $userOptions = array_merge(['poolId' => $this->poolId], $userOptions); + $response = $this->execute($this->api->postLoadBalancerMember(), $userOptions); + + return $this->populateFromResponse($response); + } + + public function retrieve() + { + $response = $this->execute($this->api->getLoadBalancerMember(), ['poolId' => (string) $this->poolId, 'id' => (string) $this->id]); + $this->populateFromResponse($response); + } + + public function update() + { + $response = $this->executeWithState($this->api->putLoadBalancerMember(), ['poolId' => (string) $this->poolId, 'id' => (string) $this->id]); + $this->populateFromResponse($response); + } + + public function delete() + { + $this->executeWithState($this->api->deleteLoadBalancerMember(), ['poolId' => (string) $this->poolId, 'id' => (string) $this->id]); + } +} diff --git a/3rdparty/php-opencloud/openstack/src/Networking/v2/Models/LoadBalancerPool.php b/3rdparty/php-opencloud/openstack/src/Networking/v2/Models/LoadBalancerPool.php new file mode 100644 index 00000000..195c0e7c --- /dev/null +++ b/3rdparty/php-opencloud/openstack/src/Networking/v2/Models/LoadBalancerPool.php @@ -0,0 +1,174 @@ + 'tenantId', + 'admin_state_up' => 'adminStateUp', + 'lb_algorithm' => 'lbAlgorithm', + 'session_persistence' => 'sessionPersistence', + 'healthmonitor_id' => 'healthmonitorId', + 'loadbalancer_id' => 'loadbalancerId', + 'operating_status' => 'operatingStatus', + 'provisioning_status' => 'provisioningStatus', + ]; + + protected function getAliases(): array + { + return parent::getAliases() + [ + 'listeners' => new Alias('listeners', LoadBalancerListener::class, true), + 'members' => new Alias('members', LoadBalancerMember::class, true), + 'healthmonitors' => new Alias('healthmonitors', LoadBalancerHealthMonitor::class, true), + ]; + } + + public function create(array $userOptions): Creatable + { + $response = $this->execute($this->api->postLoadBalancerPool(), $userOptions); + + return $this->populateFromResponse($response); + } + + public function retrieve() + { + $response = $this->execute($this->api->getLoadBalancerPool(), ['id' => (string) $this->id]); + $this->populateFromResponse($response); + } + + public function update() + { + $response = $this->executeWithState($this->api->putLoadBalancerPool()); + $this->populateFromResponse($response); + } + + public function delete() + { + $this->executeWithState($this->api->deleteLoadBalancerPool()); + } + + /** + * Add a member to this pool. + */ + public function addMember(array $userOptions = []): LoadBalancerMember + { + $userOptions = array_merge(['poolId' => $this->id], $userOptions); + + return $this->model(LoadBalancerMember::class)->create($userOptions); + } + + /** + * Get an instance of a member. + */ + public function getMember(string $memberId): LoadBalancerMember + { + return $this->model(LoadBalancerMember::class, ['poolId' => $this->id, 'id' => $memberId]); + } + + /** + * Delete a member. + */ + public function deleteMember(string $memberId) + { + $this->model(LoadBalancerMember::class, ['poolId' => $this->id, 'id' => $memberId])->delete(); + } + + /** + * Add a healthmonitor to this load balancer pool. + */ + public function addHealthMonitor(array $userOptions = []): LoadBalancerHealthMonitor + { + $userOptions = array_merge(['poolId' => $this->id], $userOptions); + + return $this->model(LoadBalancerHealthMonitor::class)->create($userOptions); + } +} diff --git a/3rdparty/php-opencloud/openstack/src/Networking/v2/Models/LoadBalancerStat.php b/3rdparty/php-opencloud/openstack/src/Networking/v2/Models/LoadBalancerStat.php new file mode 100644 index 00000000..0bde1af1 --- /dev/null +++ b/3rdparty/php-opencloud/openstack/src/Networking/v2/Models/LoadBalancerStat.php @@ -0,0 +1,57 @@ + 'bytesIn', + 'bytes_out' => 'bytesOut', + 'total_connections' => 'totalConnections', + 'active_connections' => 'activeConnections', + 'loadbalancer_id' => 'loadbalancerId', + ]; + + public function retrieve() + { + $response = $this->execute($this->api->getLoadBalancerStats(), ['loadbalancerId' => (string) $this->loadbalancerId]); + $this->populateFromResponse($response); + } +} diff --git a/3rdparty/php-opencloud/openstack/src/Networking/v2/Models/LoadBalancerStatus.php b/3rdparty/php-opencloud/openstack/src/Networking/v2/Models/LoadBalancerStatus.php new file mode 100644 index 00000000..e89e301c --- /dev/null +++ b/3rdparty/php-opencloud/openstack/src/Networking/v2/Models/LoadBalancerStatus.php @@ -0,0 +1,70 @@ + 'loadbalancerId', + 'operating_status' => 'operatingStatus', + 'provisioning_status' => 'provisioningStatus', + ]; + + protected function getAliases(): array + { + return parent::getAliases() + [ + 'listeners' => new Alias('listeners', LoadBalancerListener::class, true), + ]; + } + + public function retrieve() + { + $response = $this->execute($this->api->getLoadBalancerStatuses(), ['loadbalancerId' => (string) $this->loadbalancerId]); + $json = Utils::jsonDecode($response); + $this->populateFromArray($json[$this->resourceKey]['loadbalancer']); + } +} diff --git a/3rdparty/php-opencloud/openstack/src/Networking/v2/Models/Network.php b/3rdparty/php-opencloud/openstack/src/Networking/v2/Models/Network.php new file mode 100644 index 00000000..b9741ef5 --- /dev/null +++ b/3rdparty/php-opencloud/openstack/src/Networking/v2/Models/Network.php @@ -0,0 +1,96 @@ + 'adminStateUp', + 'tenant_id' => 'tenantId', + 'router:external' => 'routerExternal', + ]; + + protected $resourceKey = 'network'; + protected $resourcesKey = 'networks'; + + public function retrieve() + { + $response = $this->execute($this->api->getNetwork(), ['id' => (string) $this->id]); + $this->populateFromResponse($response); + } + + /** + * Creates multiple networks in a single request. + * + * @param array $data {@see \OpenStack\Networking\v2\Api::postNetworks} + * + * @return Network[] + */ + public function bulkCreate(array $data): array + { + $response = $this->execute($this->api->postNetworks(), ['networks' => $data]); + + return $this->extractMultipleInstances($response); + } + + /** + * @param array $data {@see \OpenStack\Networking\v2\Api::postNetwork} + */ + public function create(array $data): Creatable + { + $response = $this->execute($this->api->postNetwork(), $data); + + return $this->populateFromResponse($response); + } + + public function update() + { + $response = $this->executeWithState($this->api->putNetwork()); + $this->populateFromResponse($response); + } + + public function delete() + { + $this->executeWithState($this->api->deleteNetwork()); + } +} diff --git a/3rdparty/php-opencloud/openstack/src/Networking/v2/Models/Port.php b/3rdparty/php-opencloud/openstack/src/Networking/v2/Models/Port.php new file mode 100644 index 00000000..9c3dd6d5 --- /dev/null +++ b/3rdparty/php-opencloud/openstack/src/Networking/v2/Models/Port.php @@ -0,0 +1,169 @@ + 'portSecurityEnabled', + 'admin_state_up' => 'adminStateUp', + 'display_name' => 'displayName', + 'network_id' => 'networkId', + 'tenant_id' => 'tenantId', + 'device_owner' => 'deviceOwner', + 'mac_address' => 'macAddress', + 'port_id' => 'portId', + 'security_groups' => 'securityGroups', + 'device_id' => 'deviceId', + 'fixed_ips' => 'fixedIps', + 'allowed_address_pairs' => 'allowedAddressPairs', + ]; + + protected $resourceKey = 'port'; + protected $resourcesKey = 'ports'; + + public function create(array $userOptions): Creatable + { + $response = $this->execute($this->api->postSinglePort(), $userOptions); + + return $this->populateFromResponse($response); + } + + public function bulkCreate(array $userOptions): array + { + $response = $this->execute($this->api->postMultiplePorts(), ['ports' => $userOptions]); + + return $this->extractMultipleInstances($response); + } + + public function retrieve() + { + $response = $this->execute($this->api->getPort(), ['id' => (string) $this->id]); + $this->populateFromResponse($response); + } + + public function update() + { + $response = $this->executeWithState($this->api->putPort()); + $this->populateFromResponse($response); + } + + public function delete() + { + $this->executeWithState($this->api->deletePort()); + } +} diff --git a/3rdparty/php-opencloud/openstack/src/Networking/v2/Models/Quota.php b/3rdparty/php-opencloud/openstack/src/Networking/v2/Models/Quota.php new file mode 100644 index 00000000..637b9444 --- /dev/null +++ b/3rdparty/php-opencloud/openstack/src/Networking/v2/Models/Quota.php @@ -0,0 +1,96 @@ + 'tenantId', + 'security_group_rule' => 'securityGroupRule', + 'security_group' => 'securityGroup', + 'rbac_policy' => 'rbacPolicy', + ]; + + public function retrieve() + { + $response = $this->execute($this->api->getQuota(), ['tenantId' => (string) $this->tenantId]); + $this->populateFromResponse($response); + } + + public function update() + { + $response = $this->executeWithState($this->api->putQuota()); + $this->populateFromResponse($response); + } + + public function delete() + { + $this->executeWithState($this->api->deleteQuota()); + } +} diff --git a/3rdparty/php-opencloud/openstack/src/Networking/v2/Models/Subnet.php b/3rdparty/php-opencloud/openstack/src/Networking/v2/Models/Subnet.php new file mode 100644 index 00000000..f1000c76 --- /dev/null +++ b/3rdparty/php-opencloud/openstack/src/Networking/v2/Models/Subnet.php @@ -0,0 +1,122 @@ + 'enableDhcp', + 'network_id' => 'networkId', + 'dns_nameservers' => 'dnsNameservers', + 'allocation_pools' => 'allocationPools', + 'host_routes' => 'hostRoutes', + 'ip_version' => 'ipVersion', + 'gateway_ip' => 'gatewayIp', + 'tenant_id' => 'tenantId', + ]; + + protected $resourceKey = 'subnet'; + protected $resourcesKey = 'subnets'; + + public function retrieve() + { + $response = $this->execute($this->api->getSubnet(), ['id' => (string) $this->id]); + $this->populateFromResponse($response); + } + + /** + * Creates multiple subnets in a single request. + * + * @param array $data {@see \OpenStack\Networking\v2\Api::postSubnets} + * + * @return Subnet[] + */ + public function bulkCreate(array $data): array + { + $response = $this->execute($this->api->postSubnets(), ['subnets' => $data]); + + return $this->extractMultipleInstances($response); + } + + /** + * @param array $data {@see \OpenStack\Networking\v2\Api::postSubnet} + */ + public function create(array $data): Creatable + { + $response = $this->execute($this->api->postSubnet(), $data); + + return $this->populateFromResponse($response); + } + + public function update() + { + $response = $this->executeWithState($this->api->putSubnet()); + $this->populateFromResponse($response); + } + + public function delete() + { + $this->executeWithState($this->api->deleteSubnet()); + } + + protected function getAttrs(array $keys) + { + $output = parent::getAttrs($keys); + + if ('' === $this->gatewayIp) { + $output['gatewayIp'] = null; + } + + return $output; + } +} diff --git a/3rdparty/php-opencloud/openstack/src/Networking/v2/Params.php b/3rdparty/php-opencloud/openstack/src/Networking/v2/Params.php new file mode 100644 index 00000000..2e68a505 --- /dev/null +++ b/3rdparty/php-opencloud/openstack/src/Networking/v2/Params.php @@ -0,0 +1,736 @@ + self::STRING_TYPE, + 'location' => self::JSON, + ]; + } + + /** + * Returns information about name parameter. + */ + public function nameJson(): array + { + return [ + 'type' => self::STRING_TYPE, + 'location' => self::JSON, + ]; + } + + public function urlId($type): array + { + return array_merge(parent::id($type), [ + 'required' => true, + 'location' => self::URL, + ]); + } + + public function shared(): array + { + return [ + 'type' => self::BOOL_TYPE, + 'location' => self::JSON, + 'description' => 'Indicates whether this network is shared across all tenants', + ]; + } + + public function adminStateUp(): array + { + return [ + 'type' => self::BOOL_TYPE, + 'location' => self::JSON, + 'sentAs' => 'admin_state_up', + 'description' => 'The administrative state of the network', + ]; + } + + public function portSecurityEnabled(): array + { + return [ + 'type' => self::BOOL_TYPE, + 'location' => self::JSON, + 'sentAs' => 'port_security_enabled', + 'description' => 'The port security status. A valid value is enabled (true) or disabled (false). If port security is enabled for the port, security + group rules and anti-spoofing rules are applied to the traffic on the port. If disabled, no such rules are applied.', + ]; + } + + public function networkId(): array + { + return [ + 'type' => self::STRING_TYPE, + 'required' => true, + 'sentAs' => 'network_id', + 'description' => 'The ID of the attached network', + ]; + } + + public function ipVersion(): array + { + return [ + 'type' => self::INT_TYPE, + 'required' => true, + 'sentAs' => 'ip_version', + 'description' => 'The IP version, which is 4 or 6', + ]; + } + + public function cidr(): array + { + return [ + 'type' => self::STRING_TYPE, + 'required' => true, + 'sentAs' => 'cidr', + 'description' => 'The CIDR', + ]; + } + + public function tenantId(): array + { + return [ + 'type' => self::STRING_TYPE, + 'sentAs' => 'tenant_id', + 'description' => 'The ID of the tenant who owns the network. Only administrative users can specify a tenant ID other than their own. You cannot change this value through authorization policies', + ]; + } + + public function projectId(): array + { + return [ + 'type' => self::STRING_TYPE, + 'sentAs' => 'project_id', + 'location' => self::QUERY, + 'description' => 'The ID of the tenant who owns the network. Only administrative users can specify a tenant ID other than their own. You cannot change this value through authorization policies', + ]; + } + + public function gatewayIp(): array + { + return [ + 'type' => self::STRING_TYPE, + 'sentAs' => 'gateway_ip', + 'description' => 'The gateway IP address', + ]; + } + + public function enableDhcp(): array + { + return [ + 'type' => self::BOOL_TYPE, + 'sentAs' => 'enable_dhcp', + 'description' => 'Set to true if DHCP is enabled and false if DHCP is disabled.', + ]; + } + + public function dnsNameservers(): array + { + return [ + 'type' => self::ARRAY_TYPE, + 'sentAs' => 'dns_nameservers', + 'description' => 'A list of DNS name servers for the subnet.', + 'items' => [ + 'type' => self::STRING_TYPE, + 'description' => 'The nameserver', + ], + ]; + } + + public function allocationPools(): array + { + return [ + 'type' => self::ARRAY_TYPE, + 'sentAs' => 'allocation_pools', + 'items' => [ + 'type' => self::OBJECT_TYPE, + 'properties' => [ + 'start' => [ + 'type' => self::STRING_TYPE, + 'description' => 'The start address for the allocation pools', + ], + 'end' => [ + 'type' => self::STRING_TYPE, + 'description' => 'The end address for the allocation pools', + ], + ], + ], + 'description' => 'The start and end addresses for the allocation pools', + ]; + } + + public function hostRoutes(): array + { + return [ + 'type' => self::ARRAY_TYPE, + 'sentAs' => 'host_routes', + 'items' => [ + 'type' => self::OBJECT_TYPE, + 'properties' => [ + 'destination' => [ + 'type' => self::STRING_TYPE, + 'description' => 'Destination for static route', + ], + 'nexthop' => [ + 'type' => self::STRING_TYPE, + 'description' => 'Nexthop for the destination', + ], + ], + ], + 'description' => 'A list of host route dictionaries for the subnet', + ]; + } + + public function statusQuery(): array + { + return [ + 'type' => self::STRING_TYPE, + 'location' => self::QUERY, + 'description' => 'Allows filtering by port status.', + 'enum' => ['ACTIVE', 'DOWN'], + ]; + } + + public function displayNameQuery(): array + { + return [ + 'type' => self::STRING_TYPE, + 'location' => self::QUERY, + 'sentAs' => 'display_name', + 'description' => 'Allows filtering by port name.', + ]; + } + + public function adminStateQuery(): array + { + return [ + 'type' => self::BOOL_TYPE, + 'location' => self::QUERY, + 'sentAs' => 'admin_state', + 'description' => 'Allows filtering by admin state.', + ]; + } + + public function networkIdQuery(): array + { + return [ + 'type' => self::STRING_TYPE, + 'location' => self::QUERY, + 'sentAs' => 'network_id', + 'description' => 'Allows filtering by network ID.', + ]; + } + + public function tenantIdQuery(): array + { + return [ + 'type' => self::STRING_TYPE, + 'location' => self::QUERY, + 'sentAs' => 'tenant_id', + 'description' => 'Allows filtering by tenant ID.', + ]; + } + + public function deviceOwnerQuery(): array + { + return [ + 'type' => self::STRING_TYPE, + 'location' => self::QUERY, + 'sentAs' => 'device_owner', + 'description' => 'Allows filtering by device owner.', + ]; + } + + public function macAddrQuery(): array + { + return [ + 'type' => self::STRING_TYPE, + 'location' => self::QUERY, + 'sentAs' => 'mac_address', + 'description' => 'Allows filtering by MAC address.', + ]; + } + + public function portIdQuery(): array + { + return [ + 'type' => self::STRING_TYPE, + 'location' => self::QUERY, + 'sentAs' => 'port_id', + 'description' => 'Allows filtering by port UUID.', + ]; + } + + public function secGroupsQuery(): array + { + return [ + 'type' => self::STRING_TYPE, + 'location' => self::QUERY, + 'sentAs' => 'security_groups', + 'description' => 'Allows filtering by device owner. Format should be a comma-delimeted list.', + ]; + } + + public function deviceIdQuery(): array + { + return [ + 'type' => self::STRING_TYPE, + 'location' => self::QUERY, + 'sentAs' => 'device_id', + 'description' => 'The UUID of the device that uses this port. For example, a virtual server.', + ]; + } + + public function macAddr(): array + { + return [ + 'type' => self::STRING_TYPE, + 'location' => self::JSON, + 'sentAs' => 'mac_address', + 'description' => 'The MAC address. If you specify an address that is not valid, a Bad Request (400) status code is returned. If you do not specify a MAC address, OpenStack Networking tries to allocate one. If a failure occurs, a Service Unavailable (503) response code is returned.', + ]; + } + + public function fixedIps(): array + { + return [ + 'type' => self::ARRAY_TYPE, + 'location' => self::JSON, + 'sentAs' => 'fixed_ips', + 'description' => 'The IP addresses for the port. If you would like to assign multiple IP addresses for the + port, specify multiple entries in this field. Each entry consists of IP address (ipAddress) + and the subnet ID from which the IP address is assigned (subnetId)', + 'items' => [ + 'type' => self::OBJECT_TYPE, + 'properties' => [ + 'ipAddress' => [ + 'type' => self::STRING_TYPE, + 'sentAs' => 'ip_address', + 'description' => 'If you specify only an IP address, OpenStack Networking tries to allocate the IP address if the address is a valid IP for any of the subnets on the specified network.', + ], + 'subnetId' => [ + 'type' => self::STRING_TYPE, + 'sentAs' => 'subnet_id', + 'description' => 'Subnet id. If you specify only a subnet ID, OpenStack Networking allocates an available IP from that subnet to the port.', + ], + ], + ], + ]; + } + + public function subnetId(): array + { + return [ + 'type' => self::STRING_TYPE, + 'location' => self::JSON, + 'sentAs' => 'subnet_id', + 'description' => 'If you specify only a subnet UUID, OpenStack Networking allocates an available IP from that subnet to the port. If you specify both a subnet UUID and an IP address, OpenStack Networking tries to allocate the address to the port.', + ]; + } + + public function ipAddress(): array + { + return [ + 'type' => self::STRING_TYPE, + 'location' => self::JSON, + 'sentAs' => 'ip_address', + 'description' => 'If you specify both a subnet UUID and an IP address, OpenStack Networking tries to allocate the address to the port.', + ]; + } + + public function secGroupIds(): array + { + return [ + 'type' => self::ARRAY_TYPE, + 'location' => self::JSON, + 'sentAs' => 'security_groups', + 'items' => [ + 'type' => self::STRING_TYPE, + 'description' => 'The UUID of the security group', + ], + ]; + } + + public function allowedAddrPairs(): array + { + return [ + 'type' => self::ARRAY_TYPE, + 'location' => self::JSON, + 'sentAs' => 'allowed_address_pairs', + 'description' => 'Address pairs extend the port attribute to enable you to specify arbitrary mac_address/ip_address(cidr) pairs that are allowed to pass through a port regardless of the subnet associated with the network.', + 'items' => [ + 'type' => self::OBJECT_TYPE, + 'description' => 'A MAC addr/IP addr pair', + 'properties' => [ + 'ipAddress' => [ + 'sentAs' => 'ip_address', + 'type' => self::STRING_TYPE, + 'location' => self::JSON, + ], + 'macAddress' => [ + 'sentAs' => 'mac_address', + 'type' => self::STRING_TYPE, + 'location' => self::JSON, + ], + ], + ], + ]; + } + + public function deviceOwner(): array + { + return [ + 'type' => self::STRING_TYPE, + 'location' => self::JSON, + 'sentAs' => 'device_owner', + 'description' => 'The UUID of the entity that uses this port. For example, a DHCP agent.', + ]; + } + + public function deviceId(): array + { + return [ + 'type' => self::STRING_TYPE, + 'location' => self::JSON, + 'sentAs' => 'device_id', + 'description' => 'The UUID of the device that uses this port. For example, a virtual server.', + ]; + } + + public function queryName(): array + { + return $this->queryFilter('name'); + } + + public function queryTenantId(): array + { + return $this->queryFilter('tenant_id'); + } + + public function queryStatus(): array + { + return $this->queryFilter('status'); + } + + private function queryFilter($field): array + { + return [ + 'type' => self::STRING_TYPE, + 'location' => self::QUERY, + 'sentAs' => $field, + 'description' => 'The Neutron API supports filtering based on all top level attributes of a resource. + Filters are applicable to all list requests.', + ]; + } + + public function routerAccessibleJson(): array + { + return [ + 'type' => self::BOOL_TYPE, + 'location' => self::JSON, + 'sentAs' => 'router:external', + 'description' => 'Indicates whether this network is externally accessible.', + ]; + } + + public function queryRouterExternal(): array + { + return [ + 'type' => self::BOOL_TYPE, + 'location' => self::QUERY, + 'sentAs' => 'router:external', + ]; + } + + protected function quotaLimit(string $sentAs, string $description): array + { + return [ + 'type' => self::INT_TYPE, + 'location' => self::JSON, + 'sentAs' => $sentAs, + 'description' => $description, + ]; + } + + public function quotaLimitFloatingIp(): array + { + return $this->quotaLimit('floatingip', 'The number of floating IP addresses allowed for each project. A value of -1 means no limit.'); + } + + public function quotaLimitNetwork(): array + { + return $this->quotaLimit('network', 'The number of networks allowed for each project. A value of -1 means no limit.'); + } + + public function quotaLimitPort(): array + { + return $this->quotaLimit('port', 'The number of ports allowed for each project. A value of -1 means no limit.'); + } + + public function quotaLimitRbacPolicy(): array + { + return $this->quotaLimit('rbac_policy', 'The number of role-based access control (RBAC) policies for each project. A value of -1 means no limit.'); + } + + public function quotaLimitRouter(): array + { + return $this->quotaLimit('router', 'The number of routers allowed for each project. A value of -1 means no limit.'); + } + + public function quotaLimitSecurityGroup(): array + { + return $this->quotaLimit('security_group', 'The number of security groups allowed for each project. A value of -1 means no limit.'); + } + + public function quotaLimitSecurityGroupRule(): array + { + return $this->quotaLimit('security_group_rule', 'The number of security group rules allowed for each project. A value of -1 means no limit.'); + } + + public function quotaLimitSubnet(): array + { + return $this->quotaLimit('subnet', 'The number of subnets allowed for each project. A value of -1 means no limit.'); + } + + public function quotaLimitSubnetPool(): array + { + return $this->quotaLimit('subnetpool', 'The number of subnet pools allowed for each project. A value of -1 means no limit.'); + } + + public function vipSubnetId(): array + { + return [ + 'type' => self::STRING_TYPE, + 'location' => self::JSON, + 'sentAs' => 'vip_subnet_id', + 'description' => 'The network on which to allocate the load balancer\'s vip address.', + ]; + } + + public function vipAddress(): array + { + return [ + 'type' => self::STRING_TYPE, + 'location' => self::JSON, + 'sentAs' => 'vip_address', + 'description' => 'The address to assign the load balancer\'s vip address to.', + ]; + } + + public function provider(): array + { + return [ + 'type' => self::STRING_TYPE, + 'location' => self::JSON, + 'description' => 'The name of a valid provider to provision the load balancer.', + ]; + } + + public function connectionLimit(): array + { + return [ + 'type' => self::INT_TYPE, + 'location' => self::JSON, + 'sentAs' => 'connection_limit', + 'description' => 'The number of connections allowed by this listener.', + ]; + } + + public function loadbalancerId(): array + { + return [ + 'type' => self::STRING_TYPE, + 'location' => self::JSON, + 'sentAs' => 'loadbalancer_id', + 'description' => 'The ID of a load balancer.', + ]; + } + + public function loadbalancerIdUrl(): array + { + return [ + 'type' => self::STRING_TYPE, + 'location' => self::URL, + 'description' => 'The ID of a load balancer.', + ]; + } + + public function protocol(): array + { + return [ + 'type' => self::STRING_TYPE, + 'location' => self::JSON, + 'description' => 'The protocol the frontend will be listening for. (TCP, HTTP, HTTPS)', + ]; + } + + public function protocolPort(): array + { + return [ + 'type' => self::INT_TYPE, + 'location' => self::JSON, + 'sentAs' => 'protocol_port', + 'description' => 'The port in which the frontend will be listening. (1-65535)', + ]; + } + + public function lbAlgorithm(): array + { + return [ + 'type' => self::STRING_TYPE, + 'location' => self::JSON, + 'sentAs' => 'lb_algorithm', + 'description' => 'The load balancing algorithm to distribute traffic to the pool\'s members. (ROUND_ROBIN|LEAST_CONNECTIONS|SOURCE_IP)', + ]; + } + + public function listenerId(): array + { + return [ + 'type' => self::STRING_TYPE, + 'location' => self::JSON, + 'sentAs' => 'listener_id', + 'description' => 'The listener in which this pool will become the default pool. There can only be one default pool for a listener.', + ]; + } + + public function sessionPersistence(): array + { + return [ + 'type' => self::OBJECT_TYPE, + 'location' => self::JSON, + 'sentAs' => 'session_persistence', + 'description' => 'The default value for this is an empty dictionary.', + ]; + } + + public function address(): array + { + return [ + 'type' => self::STRING_TYPE, + 'location' => self::JSON, + 'sentAs' => 'address', + 'description' => 'The IP Address of the member to receive traffic from the load balancer.', + ]; + } + + public function poolId(): array + { + return [ + 'type' => self::STRING_TYPE, + 'location' => self::URL, + 'description' => 'The ID of the load balancer pool.', + ]; + } + + public function poolIdJson(): array + { + return [ + 'type' => self::STRING_TYPE, + 'location' => self::JSON, + 'sentAs' => 'pool_id', + 'description' => 'The ID of the load balancer pool.', + ]; + } + + public function weight(): array + { + return [ + 'type' => self::INT_TYPE, + 'location' => self::JSON, + 'description' => 'The default value for this attribute will be 1.', + ]; + } + + public function delay(): array + { + return [ + 'type' => self::INT_TYPE, + 'location' => self::JSON, + 'description' => 'The interval in seconds between health checks.', + ]; + } + + public function timeout(): array + { + return [ + 'type' => self::INT_TYPE, + 'location' => self::JSON, + 'description' => 'The time in seconds that a health check times out.', + ]; + } + + public function maxRetries(): array + { + return [ + 'type' => self::INT_TYPE, + 'location' => self::JSON, + 'sentAs' => 'max_retries', + 'description' => 'Number of failed health checks before marked as OFFLINE.', + ]; + } + + public function httpMethod(): array + { + return [ + 'type' => self::STRING_TYPE, + 'location' => self::JSON, + 'sentAs' => 'http_method', + 'description' => 'The default value for this attribute is GET.', + ]; + } + + public function urlPath(): array + { + return [ + 'type' => self::STRING_TYPE, + 'location' => self::JSON, + 'sentAs' => 'url_path', + 'description' => 'The default value is "/"', + ]; + } + + public function expectedCodes(): array + { + return [ + 'type' => self::STRING_TYPE, + 'location' => self::JSON, + 'sentAs' => 'expected_codes', + 'description' => 'The expected http status codes to get from a successful health check. Defaults to 200. (comma separated)', + ]; + } + + public function type(): array + { + return [ + 'type' => self::STRING_TYPE, + 'location' => self::JSON, + 'description' => 'The type of health monitor. Must be one of TCP, HTTP, HTTPS', + ]; + } + + public function tenantIdJson(): array + { + return [ + 'type' => self::STRING_TYPE, + 'description' => 'The UUID of the tenant. Only administrative users can specify a tenant UUID other than their own.', + 'sentAs' => 'tenant_id', + ]; + } +} diff --git a/3rdparty/php-opencloud/openstack/src/Networking/v2/Service.php b/3rdparty/php-opencloud/openstack/src/Networking/v2/Service.php new file mode 100644 index 00000000..03ec9c73 --- /dev/null +++ b/3rdparty/php-opencloud/openstack/src/Networking/v2/Service.php @@ -0,0 +1,332 @@ +model(Network::class)->create($options); + } + + /** + * Create a new network resources. + * + * @param array $options {@see \OpenStack\Networking\v2\Api::postNetworks} + */ + public function createNetworks(array $options): array + { + return $this->model(Network::class)->bulkCreate($options); + } + + /** + * Retrieve a network object without calling the remote API. Any values provided in the array will populate the + * empty object, allowing you greater control without the expense of network transactions. To call the remote API + * and have the response populate the object, call {@see Network::retrieve}. + */ + public function getNetwork(string $id): Network + { + return $this->model(Network::class, ['id' => $id]); + } + + /** + * List networks. + * + * @param array $options {@see \OpenStack\Networking\v2\Api::getNetworks} + * + * @return \Generator + */ + public function listNetworks(array $options = []): \Generator + { + return $this->model(Network::class)->enumerate($this->api->getNetworks(), $options); + } + + /** + * Create a new subnet resource. + * + * @param array $options {@see \OpenStack\Networking\v2\Api::postSubnet} + */ + public function createSubnet(array $options): Subnet + { + return $this->model(Subnet::class)->create($options); + } + + /** + * Create a new subnet resources. + * + * @param array $options {@see \OpenStack\Networking\v2\Api::postSubnets} + * + * @return []Subnet + */ + public function createSubnets(array $options): array + { + return $this->model(Subnet::class)->bulkCreate($options); + } + + /** + * Retrieve a subnet object without calling the remote API. Any values provided in the array will populate the + * empty object, allowing you greater control without the expense of network transactions. To call the remote API + * and have the response populate the object, call {@see Subnet::retrieve}. + */ + public function getSubnet(string $id): Subnet + { + return $this->model(Subnet::class, ['id' => $id]); + } + + /** + * List subnets. + * + * @param array $options {@see \OpenStack\Networking\v2\Api::getSubnets} + * + * @return \Generator + */ + public function listSubnets(array $options = []): \Generator + { + return $this->model(Subnet::class)->enumerate($this->api->getSubnets(), $options); + } + + /** + * Create a new port resource. + * + * @param array $options {@see \OpenStack\Networking\v2\Api::postSinglePort} + */ + public function createPort(array $options): Port + { + return $this->model(Port::class)->create($options); + } + + /** + * Create new port resources. + * + * @param array $options {@see \OpenStack\Networking\v2\Api::postMultiplePorts} + * + * @return []Port + */ + public function createPorts(array $options): array + { + return $this->model(Port::class)->bulkCreate($options); + } + + /** + * Retrieve a subnet object without calling the remote API. Any values provided in the array will populate the + * empty object, allowing you greater control without the expense of network transactions. To call the remote API + * and have the response populate the object, call {@see Port::retrieve}. + */ + public function getPort(string $id): Port + { + return $this->model(Port::class, ['id' => $id]); + } + + /** + * List ports. + * + * @param array $options {@see \OpenStack\Networking\v2\Api::getPorts} + * + * @return \Generator + */ + public function listPorts(array $options = []): \Generator + { + return $this->model(Port::class)->enumerate($this->api->getPorts(), $options); + } + + /** + * Lists quotas for projects with non-default quota values. + * + * @return \Generator + */ + public function listQuotas(): \Generator + { + return $this->model(Quota::class)->enumerate($this->api->getQuotas(), []); + } + + /** + * Lists quotas for a project. + * + * Retrieve a quota object without calling the remote API. Any values provided in the array will populate the + * empty object, allowing you greater control without the expense of network transactions. To call the remote API + * and have the response populate the object, call {@see Quota::retrieve}. + */ + public function getQuota(string $tenantId): Quota + { + return $this->model(Quota::class, ['tenantId' => $tenantId]); + } + + /** + * Lists default quotas for a project. + */ + public function getDefaultQuota(string $tenantId): Quota + { + $quota = $this->model(Quota::class, ['tenantId' => $tenantId]); + $quota->populateFromResponse($this->execute($this->api->getQuotaDefault(), ['tenantId' => $tenantId])); + + return $quota; + } + + /** + * Lists loadbalancers for projects. + * + * @return \Generator + */ + public function listLoadBalancers(): \Generator + { + return $this->model(LoadBalancer::class)->enumerate($this->api->getLoadBalancers()); + } + + /** + * Retrieve an instance of a LoadBalancer object. + */ + public function getLoadBalancer(string $id): LoadBalancer + { + return $this->model(LoadBalancer::class, ['id' => $id]); + } + + /** + * Create a new loadbalancer resource. + * + * @param array $options {@see \OpenStack\Networking\v2\Api::postLoadBalancer} + */ + public function createLoadBalancer(array $options): LoadBalancer + { + return $this->model(LoadBalancer::class)->create($options); + } + + /** + * Lists loadbalancer listeners. + * + * @return \Generator + */ + public function listLoadBalancerListeners(): \Generator + { + return $this->model(LoadBalancerListener::class)->enumerate($this->api->getLoadBalancerListeners()); + } + + /** + * Retrieve an instance of a loadbalancer listener object. + */ + public function getLoadBalancerListener(string $id): LoadBalancerListener + { + return $this->model(LoadBalancerListener::class, ['id' => $id]); + } + + /** + * Create a new loadbalancer Listener resource. + * + * @param array $options {@see \OpenStack\Networking\v2\Api::postLoadBalancerListener} + */ + public function createLoadBalancerListener(array $options): LoadBalancerListener + { + return $this->model(LoadBalancerListener::class)->create($options); + } + + /** + * Lists loadbalancer pools. + * + * @return \Generator + */ + public function listLoadBalancerPools(): \Generator + { + return $this->model(LoadBalancerPool::class)->enumerate($this->api->getLoadBalancerPools()); + } + + /** + * Retrieve an instance of a loadbalancer Pool object. + */ + public function getLoadBalancerPool(string $id): LoadBalancerPool + { + return $this->model(LoadBalancerPool::class, ['id' => $id]); + } + + /** + * Create a new loadbalancer Pool resource. + * + * @param array $options {@see \OpenStack\Networking\v2\Api::postLoadBalancerPool} + */ + public function createLoadBalancerPool(array $options): LoadBalancerPool + { + return $this->model(LoadBalancerPool::class)->create($options); + } + + /** + * Lists loadbalancer members. + * + * @return \Generator + */ + public function listLoadBalancerMembers(string $poolId): \Generator + { + return $this->model(LoadBalancerPool::class, ['poolId' => $poolId])->enumerate($this->api->getLoadBalancerMembers()); + } + + /** + * Retrieve an instance of a loadbalancer Member object. + */ + public function getLoadBalancerMember(string $poolId, string $memberId): LoadBalancerMember + { + return $this->model(LoadBalancerMember::class, ['poolId' => $poolId, 'id' => $memberId]); + } + + /** + * Create a new loadbalancer member resource. + * + * @param array $options {@see \OpenStack\Networking\v2\Api::postLoadBalancerMember} + */ + public function createLoadBalancerMember(array $options): LoadBalancerMember + { + return $this->model(LoadBalancerMember::class)->create($options); + } + + /** + * Lists loadbalancer healthmonitors. + * + * @return \Generator + */ + public function listLoadBalancerHealthMonitors(): \Generator + { + return $this->model(LoadBalancerHealthMonitor::class)->enumerate($this->api->getLoadBalancerHealthMonitors()); + } + + /** + * Retrieve an instance of a loadbalancer healthmonitor object. + */ + public function getLoadBalancerHealthMonitor(string $id): LoadBalancerHealthMonitor + { + return $this->model(LoadBalancerHealthMonitor::class, ['id' => $id]); + } + + /** + * Create a new loadbalancer healthmonitor resource. + * + * @param array $options {@see \OpenStack\Networking\v2\Api::postLoadBalancerHealthMonitor} + */ + public function createLoadBalancerHealthMonitor(array $options): LoadBalancerHealthMonitor + { + return $this->model(LoadBalancerHealthMonitor::class)->create($options); + } +} diff --git a/3rdparty/php-opencloud/openstack/src/ObjectStore/v1/Api.php b/3rdparty/php-opencloud/openstack/src/ObjectStore/v1/Api.php new file mode 100644 index 00000000..2e6b20ed --- /dev/null +++ b/3rdparty/php-opencloud/openstack/src/ObjectStore/v1/Api.php @@ -0,0 +1,242 @@ +params = new Params(); + } + + public function getAccount(): array + { + return [ + 'method' => 'GET', + 'path' => '', + 'params' => [ + 'format' => $this->params->format(), + 'limit' => $this->params->limit(), + 'marker' => $this->params->marker(), + 'endMarker' => $this->params->endMarker(), + 'prefix' => $this->params->prefix(), + 'delimiter' => $this->params->delimiter(), + 'newest' => $this->params->newest(), + ], + ]; + } + + public function postAccount(): array + { + return [ + 'method' => 'POST', + 'path' => '', + 'params' => [ + 'tempUrlKey' => $this->params->tempUrlKey('account'), + 'tempUrlKey2' => $this->params->tempUrlKey2('account'), + 'metadata' => $this->params->metadata('account'), + 'removeMetadata' => $this->params->metadata('account', true), + ], + ]; + } + + public function headAccount(): array + { + return [ + 'method' => 'HEAD', + 'path' => '', + 'params' => [], + ]; + } + + public function getContainer(): array + { + return [ + 'method' => 'GET', + 'path' => '{name}', + 'params' => [ + 'name' => $this->params->containerName(), + 'format' => $this->params->format(), + 'limit' => $this->params->limit(), + 'marker' => $this->params->marker(), + 'endMarker' => $this->params->endMarker(), + 'prefix' => $this->params->prefix(), + 'path' => $this->params->path(), + 'delimiter' => $this->params->delimiter(), + 'newest' => $this->params->newest(), + ], + ]; + } + + public function putContainer(): array + { + return [ + 'method' => 'PUT', + 'path' => '{name}', + 'params' => [ + 'name' => $this->params->containerName(), + 'readAccess' => $this->params->readAccess('container'), + 'writeAccess' => $this->params->writeAccess('container'), + 'metadata' => $this->params->metadata('container'), + 'syncTo' => $this->params->syncTo(), + 'syncKey' => $this->params->syncKey(), + 'versionsLocation' => $this->params->versionsLocation(), + 'bytesQuota' => $this->params->bytesQuota(), + 'countQuota' => $this->params->countQuota(), + 'webDirectoryType' => $this->params->webDirType(), + 'detectContentType' => $this->params->detectContentType(), + ], + ]; + } + + public function deleteContainer(): array + { + return [ + 'method' => 'DELETE', + 'path' => '{name}', + 'params' => [ + 'name' => $this->params->containerName(), + ], + ]; + } + + public function postContainer(): array + { + return [ + 'method' => 'POST', + 'path' => '{name}', + 'params' => [ + 'name' => $this->params->containerName(), + 'readAccess' => $this->params->readAccess('container'), + 'writeAccess' => $this->params->writeAccess('container'), + 'metadata' => $this->params->metadata('container'), + 'removeMetadata' => $this->params->metadata('container', true), + 'syncTo' => $this->params->syncTo(), + 'syncKey' => $this->params->syncKey(), + 'versionsLocation' => $this->params->versionsLocation(), + 'removeVersionsLocation' => $this->params->removeVersionsLocation(), + 'bytesQuota' => $this->params->bytesQuota(), + 'countQuota' => $this->params->countQuota(), + 'webDirectoryType' => $this->params->webDirType(), + 'detectContentType' => $this->params->detectContentType(), + ], + ]; + } + + public function headContainer(): array + { + return [ + 'method' => 'HEAD', + 'path' => '{name}', + 'params' => ['name' => $this->params->containerName()], + ]; + } + + public function getObject(): array + { + return [ + 'method' => 'GET', + 'path' => '{containerName}/{+name}', + 'params' => [ + 'containerName' => $this->params->containerName(), + 'name' => $this->params->objectName(), + 'range' => $this->params->range(), + 'ifMatch' => $this->params->ifMatch(), + 'ifNoneMatch' => $this->params->ifNoneMatch(), + 'ifModifiedSince' => $this->params->ifModifiedSince(), + 'ifUnmodifiedSince' => $this->params->ifUnmodifiedSince(), + ], + ]; + } + + public function putObject(): array + { + return [ + 'method' => 'PUT', + 'path' => '{containerName}/{+name}', + 'params' => [ + 'containerName' => $this->params->containerName(), + 'name' => $this->params->objectName(), + 'content' => $this->params->content(), + 'stream' => $this->params->stream(), + 'contentType' => $this->params->contentType(), + 'detectContentType' => $this->params->detectContentType(), + 'copyFrom' => $this->params->copyFrom(), + 'ETag' => $this->params->etag(), + 'contentDisposition' => $this->params->contentDisposition(), + 'contentEncoding' => $this->params->contentEncoding(), + 'deleteAt' => $this->params->deleteAt(), + 'deleteAfter' => $this->params->deleteAfter(), + 'metadata' => $this->params->metadata('object'), + 'ifNoneMatch' => $this->params->ifNoneMatch(), + 'objectManifest' => $this->params->objectManifest(), + ], + ]; + } + + public function copyObject(): array + { + return [ + 'method' => 'COPY', + 'path' => '{containerName}/{+name}', + 'params' => [ + 'containerName' => $this->params->containerName(), + 'name' => $this->params->objectName(), + 'destination' => $this->params->destination(), + 'contentType' => $this->params->contentType(), + 'contentDisposition' => $this->params->contentDisposition(), + 'contentEncoding' => $this->params->contentEncoding(), + 'metadata' => $this->params->metadata('object'), + 'freshMetadata' => $this->params->freshMetadata(), + ], + ]; + } + + public function deleteObject(): array + { + return [ + 'method' => 'DELETE', + 'path' => '{containerName}/{+name}', + 'params' => [ + 'containerName' => $this->params->containerName(), + 'name' => $this->params->objectName(), + ], + ]; + } + + public function headObject(): array + { + return [ + 'method' => 'HEAD', + 'path' => '{containerName}/{+name}', + 'params' => [ + 'containerName' => $this->params->containerName(), + 'name' => $this->params->objectName(), + ], + ]; + } + + public function postObject(): array + { + return [ + 'method' => 'POST', + 'path' => '{containerName}/{+name}', + 'params' => [ + 'containerName' => $this->params->containerName(), + 'name' => $this->params->objectName(), + 'metadata' => $this->params->metadata('object'), + 'removeMetadata' => $this->params->metadata('object', true), + 'deleteAt' => $this->params->deleteAt(), + 'deleteAfter' => $this->params->deleteAfter(), + 'contentDisposition' => $this->params->contentDisposition(), + 'contentEncoding' => $this->params->contentEncoding(), + 'contentType' => $this->params->contentType(), + 'detectContentType' => $this->params->detectContentType(), + ], + ]; + } +} diff --git a/3rdparty/php-opencloud/openstack/src/ObjectStore/v1/Models/Account.php b/3rdparty/php-opencloud/openstack/src/ObjectStore/v1/Models/Account.php new file mode 100644 index 00000000..576c6b77 --- /dev/null +++ b/3rdparty/php-opencloud/openstack/src/ObjectStore/v1/Models/Account.php @@ -0,0 +1,84 @@ +containerCount = $response->getHeaderLine('X-Account-Container-Count'); + $this->objectCount = $response->getHeaderLine('X-Account-Object-Count'); + $this->bytesUsed = $response->getHeaderLine('X-Account-Bytes-Used'); + $this->tempUrl = $response->getHeaderLine('X-Account-Meta-Temp-URL-Key'); + $this->metadata = $this->parseMetadata($response); + + return $this; + } + + public function retrieve() + { + $response = $this->execute($this->api->headAccount()); + $this->populateFromResponse($response); + } + + public function mergeMetadata(array $metadata) + { + $response = $this->execute($this->api->postAccount(), ['metadata' => $metadata]); + $this->metadata = $this->parseMetadata($response); + } + + public function resetMetadata(array $metadata) + { + $options = [ + 'removeMetadata' => [], + 'metadata' => $metadata, + ]; + + foreach ($this->getMetadata() as $key => $val) { + if (!array_key_exists($key, $metadata)) { + $options['removeMetadata'][$key] = 'True'; + } + } + + $response = $this->execute($this->api->postAccount(), $options); + $this->metadata = $this->parseMetadata($response); + } + + public function getMetadata(): array + { + $response = $this->execute($this->api->headAccount()); + + return $this->parseMetadata($response); + } +} diff --git a/3rdparty/php-opencloud/openstack/src/ObjectStore/v1/Models/Container.php b/3rdparty/php-opencloud/openstack/src/ObjectStore/v1/Models/Container.php new file mode 100644 index 00000000..74f2e466 --- /dev/null +++ b/3rdparty/php-opencloud/openstack/src/ObjectStore/v1/Models/Container.php @@ -0,0 +1,247 @@ +objectCount = $response->getHeaderLine('X-Container-Object-Count'); + $this->bytesUsed = $response->getHeaderLine('X-Container-Bytes-Used'); + $this->metadata = $this->parseMetadata($response); + + return $this; + } + + /** + * Retrieves a collection of object resources in the form of a generator. + * + * @param array $options {@see \OpenStack\ObjectStore\v1\Api::getContainer} + * @param callable|null $mapFn allows a function to be mapped over each element + * + * @return \Generator + */ + public function listObjects(array $options = [], ?callable $mapFn = null): \Generator + { + $options = array_merge($options, ['name' => $this->name, 'format' => 'json']); + + $appendContainerNameFn = function (StorageObject $resource) use ($mapFn) { + $resource->containerName = $this->name; + if ($mapFn) { + call_user_func_array($mapFn, [&$resource]); + } + }; + + return $this->model(StorageObject::class)->enumerate($this->api->getContainer(), $options, $appendContainerNameFn); + } + + public function retrieve() + { + $response = $this->executeWithState($this->api->headContainer()); + $this->populateFromResponse($response); + } + + /** + * @param array $userOptions {@see \OpenStack\ObjectStore\v1\Api::putContainer} + * + * @return self + */ + public function create(array $userOptions): Creatable + { + $response = $this->execute($this->api->putContainer(), $userOptions); + + $this->populateFromResponse($response); + $this->name = $userOptions['name']; + + return $this; + } + + public function delete() + { + $this->executeWithState($this->api->deleteContainer()); + } + + public function mergeMetadata(array $metadata) + { + $response = $this->execute($this->api->postContainer(), ['name' => $this->name, 'metadata' => $metadata]); + $this->metadata = $this->parseMetadata($response); + } + + public function resetMetadata(array $metadata) + { + $options = [ + 'name' => $this->name, + 'removeMetadata' => [], + 'metadata' => $metadata, + ]; + + foreach ($this->getMetadata() as $key => $val) { + if (!array_key_exists($key, $metadata)) { + $options['removeMetadata'][$key] = 'True'; + } + } + + $response = $this->execute($this->api->postContainer(), $options); + $this->metadata = $this->parseMetadata($response); + } + + public function getMetadata(): array + { + $response = $this->executeWithState($this->api->headContainer()); + + return $this->parseMetadata($response); + } + + /** + * Retrieves an StorageObject and populates its `name` and `containerName` properties according to the name provided and + * the name of this container. A HTTP call will not be executed by default - you need to call + * {@see StorageObject::retrieve} or {@see StorageObject::download} on the returned StorageObject object to do that. + * + * @param string $name The name of the object + */ + public function getObject($name): StorageObject + { + return $this->model(StorageObject::class, ['containerName' => $this->name, 'name' => $name]); + } + + /** + * Identifies whether an object exists in this container. + * + * @param string $name the name of the object + * + * @return bool TRUE if the object exists, FALSE if it does not + * + * @throws BadResponseError for any other HTTP error which does not have a 404 Not Found status + * @throws \Exception for any other type of fatal error + */ + public function objectExists(string $name): bool + { + try { + $this->getObject($name)->retrieve(); + + return true; + } catch (BadResponseError $e) { + if (404 === $e->getResponse()->getStatusCode()) { + return false; + } + throw $e; + } + } + + /** + * Verifies if provied segment index format for DLOs is valid. + * + * @param string $fmt The format of segment index name, e.g. %05d for 00001, 00002, etc. + * + * @return bool TRUE if the format is valid, FALSE if it is not + */ + public function isValidSegmentIndexFormat($fmt) + { + $testValue1 = sprintf($fmt, 1); + $testValue2 = sprintf($fmt, 10); + + // Test if different results of the same string length + return ($testValue1 !== $testValue2) && (strlen($testValue1) === strlen($testValue2)); + } + + /** + * Creates a single object according to the values provided. + * + * @param array $data {@see \OpenStack\ObjectStore\v1\Api::putObject} + * + * @return object + */ + public function createObject(array $data): StorageObject + { + return $this->model(StorageObject::class)->create($data + ['containerName' => $this->name]); + } + + /** + * Creates a Dynamic Large Object by chunking a file into smaller segments and uploading them into a holding + * container. When this completes, a manifest file is uploaded which references the prefix of the segments, + * allowing concatenation when a request is executed against the manifest. + * + * @param array $data {@see \OpenStack\ObjectStore\v1\Api::putObject} + */ + public function createLargeObject(array $data): StorageObject + { + /** @var \Psr\Http\Message\StreamInterface $stream */ + $stream = $data['stream']; + + $segmentSize = isset($data['segmentSize']) ? $data['segmentSize'] : 1073741824; + $segmentContainer = isset($data['segmentContainer']) ? $data['segmentContainer'] : $this->name.'_segments'; + $segmentPrefix = isset($data['segmentPrefix']) + ? $data['segmentPrefix'] + : sprintf('%s/%s/%d', $data['name'], microtime(true), $stream->getSize()); + $segmentIndexFormat = isset($data['segmentIndexFormat']) ? $data['segmentIndexFormat'] : '%05d'; + + if (!$this->isValidSegmentIndexFormat($segmentIndexFormat)) { + throw new \InvalidArgumentException('The provided segmentIndexFormat is not valid.'); + } + + /** @var \OpenStack\ObjectStore\v1\Service $service */ + $service = $this->getService(); + if (!$service->containerExists($segmentContainer)) { + $service->createContainer(['name' => $segmentContainer]); + } + + $promises = []; + $count = 0; + $totalSegments = $stream->getSize() / $segmentSize; + + while (!$stream->eof() && $count < $totalSegments) { + $promises[] = $this->model(StorageObject::class)->createAsync([ + 'name' => sprintf('%s/'.$segmentIndexFormat, $segmentPrefix, ++$count), + 'stream' => new LimitStream($stream, $segmentSize, ($count - 1) * $segmentSize), + 'containerName' => $segmentContainer, + ]); + } + + /** @var Promise $p */ + $p = function_exists('\GuzzleHttp\Promise\all') + ? \GuzzleHttp\Promise\all($promises) + : \GuzzleHttp\Promise\Utils::all($promises); + $p->wait(); + + return $this->createObject([ + 'name' => $data['name'], + 'objectManifest' => sprintf('%s/%s', $segmentContainer, $segmentPrefix), + ]); + } +} diff --git a/3rdparty/php-opencloud/openstack/src/ObjectStore/v1/Models/MetadataTrait.php b/3rdparty/php-opencloud/openstack/src/ObjectStore/v1/Models/MetadataTrait.php new file mode 100644 index 00000000..b1e2beed --- /dev/null +++ b/3rdparty/php-opencloud/openstack/src/ObjectStore/v1/Models/MetadataTrait.php @@ -0,0 +1,24 @@ +getHeaders() as $header => $value) { + if (0 === stripos($header, static::METADATA_PREFIX)) { + $name = substr($header, strlen(static::METADATA_PREFIX)); + $metadata[$name] = $response->getHeader($header)[0]; + } + } + + return $metadata; + } +} diff --git a/3rdparty/php-opencloud/openstack/src/ObjectStore/v1/Models/StorageObject.php b/3rdparty/php-opencloud/openstack/src/ObjectStore/v1/Models/StorageObject.php new file mode 100644 index 00000000..c8a91f60 --- /dev/null +++ b/3rdparty/php-opencloud/openstack/src/ObjectStore/v1/Models/StorageObject.php @@ -0,0 +1,187 @@ + 'contentLength', + 'content_type' => 'contentType', + 'subdir' => 'name', + ]; + + protected function getAliases(): array + { + return parent::getAliases() + [ + 'last_modified' => new Alias('lastModified', \DateTimeImmutable::class), + ]; + } + + public function populateFromResponse(ResponseInterface $response): self + { + parent::populateFromResponse($response); + + $this->populateHeaders($response); + + return $this; + } + + /** + * @return $this + */ + private function populateHeaders(ResponseInterface $response): self + { + $this->hash = $response->getHeaderLine('ETag'); + $this->contentLength = $response->getHeaderLine('Content-Length'); + $this->lastModified = $response->getHeaderLine('Last-Modified'); + $this->contentType = $response->getHeaderLine('Content-Type'); + $this->metadata = $this->parseMetadata($response); + + return $this; + } + + /** + * Retrieves the public URI for this resource. + */ + public function getPublicUri(): Uri + { + return Utils::addPaths($this->getHttpBaseUrl(), $this->containerName, $this->name); + } + + /** + * @param array $data {@see \OpenStack\ObjectStore\v1\Api::putObject} + */ + public function create(array $data): Creatable + { + // Override containerName from input params only if local instance contains containerName attr + if ($this->containerName) { + $data['containerName'] = $this->containerName; + } + + $response = $this->execute($this->api->putObject(), $data); + $storageObject = $this->populateFromResponse($response); + + // Repopulate data for this newly created object instance + // due to the response from API does not contain object name and containerName + $storageObject = $storageObject->populateFromArray([ + 'name' => $data['name'], + 'containerName' => $data['containerName'], + ]); + + return $storageObject; + } + + public function retrieve() + { + $response = $this->executeWithState($this->api->headObject()); + $this->populateFromResponse($response); + } + + /** + * This call will perform a `GET` HTTP request for the given object and return back its content in the form of a + * Guzzle Stream object. Downloading an object will transfer all of the content for an object, and is therefore + * distinct from fetching its metadata (a `HEAD` request). The whole body of the object is fetched before the + * function returns, set the `'requestOptions'` key of {@param $data} to `['stream' => true]` to get the stream + * before the end of download. + * + * @param array $data {@see \OpenStack\ObjectStore\v1\Api::getObject} + */ + public function download(array $data = []): StreamInterface + { + $data += ['name' => $this->name, 'containerName' => $this->containerName]; + + /** @var ResponseInterface $response */ + $response = $this->execute($this->api->getObject(), $data); + $this->populateHeaders($response); + + return $response->getBody(); + } + + public function delete() + { + $this->executeWithState($this->api->deleteObject()); + } + + /** + * @param array $options {@see \OpenStack\ObjectStore\v1\Api::copyObject} + */ + public function copy(array $options) + { + $options += ['name' => $this->name, 'containerName' => $this->containerName]; + $this->execute($this->api->copyObject(), $options); + } + + public function mergeMetadata(array $metadata) + { + $options = [ + 'containerName' => $this->containerName, + 'name' => $this->name, + 'metadata' => array_merge($metadata, $this->getMetadata()), + ]; + + $response = $this->execute($this->api->postObject(), $options); + $this->metadata = $this->parseMetadata($response); + } + + public function resetMetadata(array $metadata) + { + $options = [ + 'containerName' => $this->containerName, + 'name' => $this->name, + 'metadata' => $metadata, + ]; + + $response = $this->execute($this->api->postObject(), $options); + $this->metadata = $this->parseMetadata($response); + } + + public function getMetadata(): array + { + $response = $this->executeWithState($this->api->headObject()); + $this->populateFromResponse($response); + + return $this->parseMetadata($response); + } +} diff --git a/3rdparty/php-opencloud/openstack/src/ObjectStore/v1/Params.php b/3rdparty/php-opencloud/openstack/src/ObjectStore/v1/Params.php new file mode 100644 index 00000000..bdff7a5c --- /dev/null +++ b/3rdparty/php-opencloud/openstack/src/ObjectStore/v1/Params.php @@ -0,0 +1,504 @@ + self::QUERY, + 'description' => << self::QUERY, + 'description' => << self::QUERY, + 'description' => << self::HEADER, + 'type' => self::BOOL_TYPE, + 'sentAs' => 'X-Newest', + 'description' => << self::HEADER, + 'sentAs' => sprintf('X-%s-Meta-Temp-URL-Key', ucfirst($type)), + 'description' => 'The secret key value for temporary URLs.', + ]; + } + + public function tempUrlKey2($type) + { + return [ + 'location' => self::HEADER, + 'sentAs' => sprintf('X-%s-Meta-Temp-URL-Key-2', ucfirst($type)), + 'description' => << self::URL, + 'required' => true, + 'description' => << 'query', + 'description' => << 'header', + 'sentAs' => sprintf('X-%s-Read', ucfirst($type)), + 'description' => << self::HEADER, + 'sentAs' => 'X-Container-Sync-To', + 'description' => << self::HEADER, + 'sentAs' => 'X-Container-Sync-Key', + 'description' => << self::HEADER, + 'sentAs' => sprintf('X-%s-Write', ucfirst($type)), + 'description' => 'Like `readAccess` parameter, but for write access.', + ]; + } + + public function metadata($type, $remove = false) + { + if (true == $remove) { + $type = 'Remove-'.ucfirst($type); + } + + return [ + 'location' => self::HEADER, + 'type' => self::OBJECT_TYPE, + 'prefix' => sprintf('X-%s-Meta-', ucfirst($type)), + 'properties' => [ + 'type' => self::STRING_TYPE, + ], + 'description' => << self::HEADER, + 'sentAs' => 'X-Versions-Location', + 'description' => << self::HEADER, + 'sentAs' => 'X-Container-Meta-Quota-Bytes', + 'description' => << self::HEADER, + 'sentAs' => 'X-Container-Meta-Quota-Count', + 'description' => << self::HEADER, + 'sentAs' => 'X-Container-Meta-Web-Directory-Type', + 'description' => << self::HEADER, + 'type' => self::BOOL_TYPE, + 'sentAs' => 'X-Detect-Content-Type', + 'description' => << self::HEADER, + 'sentAs' => 'X-Remove-Versions-Location', + 'description' => 'Set to any value to disable versioning.', + ]; + } + + public function objectName(): array + { + return [ + 'location' => self::URL, + 'required' => true, + 'description' => 'The unique name for the object', + ]; + } + + public function range(): array + { + return [ + 'location' => self::HEADER, + 'description' => << self::HEADER, + 'sentAs' => 'If-Match', + 'description' => << self::HEADER, + 'sentAs' => 'If-None-Match', + 'description' => << self::HEADER, + 'sentAs' => 'If-Modified-Since', + 'description' => << self::HEADER, + 'sentAs' => 'If-Unmodified-Since', + 'description' => << self::HEADER, + 'sentAs' => 'X-Delete-After', + 'description' => << self::HEADER, + 'sentAs' => 'X-Delete-At', + 'description' => 'The certain date, in UNIX Epoch timestamp format, when the object will be removed.', + ]; + } + + public function contentEncoding(): array + { + return [ + 'location' => self::HEADER, + 'sentAs' => 'Content-Encoding', + 'description' => << self::HEADER, + 'sentAs' => 'Content-Disposition', + 'description' => << self::HEADER, + 'sentAs' => 'X-Copy-From', + 'description' => << self::HEADER, + 'sentAs' => 'ETag', + 'description' => << self::HEADER, + 'sentAs' => 'Content-Type', + 'description' => 'The Content-Type entity-header field indicates the media type of the entity-body', + ]; + } + + public function destination(): array + { + return [ + 'location' => self::HEADER, + 'sentAs' => 'Destination', + 'description' => << self::HEADER, + 'type' => self::BOOL_TYPE, + 'description' => << self::RAW, + 'type' => self::STRING_TYPE, + 'description' => 'The content of the object in string form', + ]; + } + + public function stream(): array + { + return [ + 'location' => self::RAW, + 'type' => StreamInterface::class, + 'description' => 'The content of the object in string form', + ]; + } + + public function format(): array + { + return [ + 'location' => self::QUERY, + 'type' => self::STRING_TYPE, + 'description' => 'Defines the format of the collection. Will always default to `json`.', + ]; + } + + public function objectManifest(): array + { + return [ + 'location' => self::HEADER, + 'sentAs' => 'X-Object-Manifest', + 'type' => self::STRING_TYPE, + 'description' => <<model(Account::class); + } + + /** + * Retrieves a collection of container resources in a generator format. + * + * @param array $options {@see \OpenStack\ObjectStore\v1\Api::getAccount} + * @param callable|null $mapFn allows a function to be mapped over each element in the collection + * + * @return \Generator + */ + public function listContainers(array $options = [], ?callable $mapFn = null): \Generator + { + $options = array_merge($options, ['format' => 'json']); + + return $this->model(Container::class)->enumerate($this->api->getAccount(), $options, $mapFn); + } + + /** + * Retrieves a Container object and populates its name according to the value provided. Please note that the + * remote API is not contacted. + * + * @param string|null $name The unique name of the container + */ + public function getContainer(?string $name = null): Container + { + return $this->model(Container::class, ['name' => $name]); + } + + /** + * Creates a new container according to the values provided. + * + * @param array $data {@see \OpenStack\ObjectStore\v1\Api::putContainer} + */ + public function createContainer(array $data): Container + { + return $this->getContainer()->create($data); + } + + /** + * Checks the existence of a container. + * + * @param string $name The name of the container + * + * @return bool TRUE if exists, FALSE if it doesn't + * + * @throws BadResponseError Thrown for any non 404 status error + */ + public function containerExists(string $name): bool + { + try { + $this->execute($this->api->headContainer(), ['name' => $name]); + + return true; + } catch (BadResponseError $e) { + if (404 === $e->getResponse()->getStatusCode()) { + return false; + } + throw $e; + } + } +} diff --git a/3rdparty/php-opencloud/openstack/src/OpenStack.php b/3rdparty/php-opencloud/openstack/src/OpenStack.php new file mode 100644 index 00000000..254e66df --- /dev/null +++ b/3rdparty/php-opencloud/openstack/src/OpenStack.php @@ -0,0 +1,203 @@ + 2]; + $options = array_merge($defaults, $options); + + if (!isset($options['identityService'])) { + $options['identityService'] = $this->getDefaultIdentityService($options); + } + + $this->builder = $builder ?: new Builder($options, 'OpenStack'); + } + + private function getDefaultIdentityService(array $options): Service + { + if (!isset($options['authUrl'])) { + throw new \InvalidArgumentException("'authUrl' is a required option"); + } + + $stack = HandlerStackFactory::createWithOptions(array_merge($options, ['token' => null])); + + $clientOptions = [ + 'base_uri' => Utils::normalizeUrl($options['authUrl']), + 'handler' => $stack, + ]; + + if (isset($options['requestOptions'])) { + $clientOptions = array_merge($options['requestOptions'], $clientOptions); + } + + return Service::factory(new Client($clientOptions)); + } + + /** + * Creates a new Compute v2 service. + * + * @param array $options options that will be used in configuring the service + */ + public function computeV2(array $options = []): Compute\v2\Service + { + $defaults = ['catalogName' => 'nova', 'catalogType' => 'compute']; + + return $this->builder->createService('Compute\\v2', array_merge($defaults, $options)); + } + + /** + * Creates a new Networking v2 service. + * + * @param array $options options that will be used in configuring the service + */ + public function networkingV2(array $options = []): Networking\v2\Service + { + $defaults = ['catalogName' => 'neutron', 'catalogType' => 'network']; + + return $this->builder->createService('Networking\\v2', array_merge($defaults, $options)); + } + + /** + * Creates a new Networking v2 Layer 3 service. + * + * @param array $options options that will be used in configuring the service + * + * @deprecated Use networkingV2 instead + */ + public function networkingV2ExtLayer3(array $options = []): Networking\v2\Extensions\Layer3\Service + { + $defaults = ['catalogName' => 'neutron', 'catalogType' => 'network']; + + return $this->builder->createService('Networking\\v2\\Extensions\\Layer3', array_merge($defaults, $options)); + } + + /** + * Creates a new Networking v2 Layer 3 service. + * + * @param array $options options that will be used in configuring the service + * + * @deprecated Use networkingV2 instead + */ + public function networkingV2ExtSecGroups(array $options = []): Networking\v2\Extensions\SecurityGroups\Service + { + $defaults = ['catalogName' => 'neutron', 'catalogType' => 'network']; + + return $this->builder->createService('Networking\\v2\\Extensions\\SecurityGroups', array_merge($defaults, $options)); + } + + /** + * Creates a new Identity v2 service. + * + * @param array $options options that will be used in configuring the service + */ + public function identityV2(array $options = []): Identity\v2\Service + { + $defaults = ['catalogName' => 'keystone', 'catalogType' => 'identity']; + + return $this->builder->createService('Identity\\v2', array_merge($defaults, $options)); + } + + /** + * Creates a new Identity v3 service. + * + * @param array $options options that will be used in configuring the service + */ + public function identityV3(array $options = []): Service + { + $defaults = ['catalogName' => 'keystone', 'catalogType' => 'identity']; + + return $this->builder->createService('Identity\\v3', array_merge($defaults, $options)); + } + + /** + * Creates a new Object Store v1 service. + * + * @param array $options options that will be used in configuring the service + */ + public function objectStoreV1(array $options = []): ObjectStore\v1\Service + { + $defaults = ['catalogName' => 'swift', 'catalogType' => 'object-store']; + + return $this->builder->createService('ObjectStore\\v1', array_merge($defaults, $options)); + } + + /** + * Creates a new Block Storage v2 service. + * + * @param array $options options that will be used in configuring the service + * + * @deprecated Use blockStorageV3 instead + */ + public function blockStorageV2(array $options = []): BlockStorage\v2\Service + { + $defaults = ['catalogName' => 'cinderv2', 'catalogType' => 'volumev2']; + + return $this->builder->createService('BlockStorage\\v2', array_merge($defaults, $options)); + } + + /** + * Creates a new Block Storage v3 service. + * + * @param array $options options that will be used in configuring the service + */ + public function blockStorageV3(array $options = []): BlockStorage\v3\Service + { + $defaults = ['catalogName' => 'cinderv3', 'catalogType' => 'volumev3']; + + return $this->builder->createService('BlockStorage\\v3', array_merge($defaults, $options)); + } + + /** + * Creates a new Images v2 service. + * + * @param array $options options that will be used in configuring the service + */ + public function imagesV2(array $options = []): Images\v2\Service + { + $defaults = ['catalogName' => 'glance', 'catalogType' => 'image']; + + return $this->builder->createService('Images\\v2', array_merge($defaults, $options)); + } + + /** + * Creates a new Gnocchi Metric service v1. + */ + public function metricGnocchiV1(array $options = []): Metric\v1\Gnocchi\Service + { + $defaults = ['catalogName' => 'gnocchi', 'catalogType' => 'metric']; + + return $this->builder->createService('Metric\\v1\\Gnocchi', array_merge($defaults, $options)); + } +} diff --git a/3rdparty/phpseclib/phpseclib/AUTHORS b/3rdparty/phpseclib/phpseclib/AUTHORS new file mode 100644 index 00000000..9f10d267 --- /dev/null +++ b/3rdparty/phpseclib/phpseclib/AUTHORS @@ -0,0 +1,7 @@ +phpseclib Lead Developer: TerraFrost (Jim Wigginton) + +phpseclib Developers: monnerat (Patrick Monnerat) + bantu (Andreas Fischer) + petrich (Hans-Jürgen Petrich) + GrahamCampbell (Graham Campbell) + hc-jworman \ No newline at end of file diff --git a/3rdparty/phpseclib/phpseclib/LICENSE b/3rdparty/phpseclib/phpseclib/LICENSE new file mode 100644 index 00000000..e7214ebb --- /dev/null +++ b/3rdparty/phpseclib/phpseclib/LICENSE @@ -0,0 +1,20 @@ +Copyright (c) 2011-2019 TerraFrost and other contributors + +Permission is hereby granted, free of charge, to any person obtaining +a copy of this software and associated documentation files (the +"Software"), to deal in the Software without restriction, including +without limitation the rights to use, copy, modify, merge, publish, +distribute, sublicense, and/or sell copies of the Software, and to +permit persons to whom the Software is furnished to do so, subject to +the following conditions: + +The above copyright notice and this permission notice shall be +included in all copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, +EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF +MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND +NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE +LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION +OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION +WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. \ No newline at end of file diff --git a/3rdparty/phpseclib/phpseclib/phpseclib/Crypt/AES.php b/3rdparty/phpseclib/phpseclib/phpseclib/Crypt/AES.php new file mode 100644 index 00000000..9903db10 --- /dev/null +++ b/3rdparty/phpseclib/phpseclib/phpseclib/Crypt/AES.php @@ -0,0 +1,96 @@ + + * setKey('abcdefghijklmnop'); + * + * $size = 10 * 1024; + * $plaintext = ''; + * for ($i = 0; $i < $size; $i++) { + * $plaintext.= 'a'; + * } + * + * echo $aes->decrypt($aes->encrypt($plaintext)); + * ?> + * + * + * @category Crypt + * @package AES + * @author Jim Wigginton + * @copyright 2008 Jim Wigginton + * @license http://www.opensource.org/licenses/mit-license.html MIT License + * @link http://phpseclib.sourceforge.net + */ + +namespace phpseclib\Crypt; + +/** + * Pure-PHP implementation of AES. + * + * @package AES + * @author Jim Wigginton + * @access public + */ +class AES extends Rijndael +{ + /** + * Dummy function + * + * Since \phpseclib\Crypt\AES extends \phpseclib\Crypt\Rijndael, this function is, technically, available, but it doesn't do anything. + * + * @see \phpseclib\Crypt\Rijndael::setBlockLength() + * @access public + * @param int $length + */ + function setBlockLength($length) + { + return; + } + + /** + * Sets the key length + * + * Valid key lengths are 128, 192, and 256. If the length is less than 128, it will be rounded up to + * 128. If the length is greater than 128 and invalid, it will be rounded down to the closest valid amount. + * + * @see \phpseclib\Crypt\Rijndael:setKeyLength() + * @access public + * @param int $length + */ + function setKeyLength($length) + { + parent::setKeyLength($length); + switch ($this->key_length) { + case 20: + $this->key_length = 24; + break; + case 28: + $this->key_length = 32; + } + } +} diff --git a/3rdparty/phpseclib/phpseclib/phpseclib/Crypt/Base.php b/3rdparty/phpseclib/phpseclib/phpseclib/Crypt/Base.php new file mode 100644 index 00000000..ab5944cd --- /dev/null +++ b/3rdparty/phpseclib/phpseclib/phpseclib/Crypt/Base.php @@ -0,0 +1,2909 @@ + + * @author Hans-Juergen Petrich + * @copyright 2007 Jim Wigginton + * @license http://www.opensource.org/licenses/mit-license.html MIT License + * @link http://phpseclib.sourceforge.net + */ + +namespace phpseclib\Crypt; + +/** + * Base Class for all \phpseclib\Crypt\* cipher classes + * + * @package Base + * @author Jim Wigginton + * @author Hans-Juergen Petrich + */ +abstract class Base +{ + /**#@+ + * @access public + * @see \phpseclib\Crypt\Base::encrypt() + * @see \phpseclib\Crypt\Base::decrypt() + */ + /** + * Encrypt / decrypt using the Counter mode. + * + * Set to -1 since that's what Crypt/Random.php uses to index the CTR mode. + * + * @link http://en.wikipedia.org/wiki/Block_cipher_modes_of_operation#Counter_.28CTR.29 + */ + const MODE_CTR = -1; + /** + * Encrypt / decrypt using the Electronic Code Book mode. + * + * @link http://en.wikipedia.org/wiki/Block_cipher_modes_of_operation#Electronic_codebook_.28ECB.29 + */ + const MODE_ECB = 1; + /** + * Encrypt / decrypt using the Code Book Chaining mode. + * + * @link http://en.wikipedia.org/wiki/Block_cipher_modes_of_operation#Cipher-block_chaining_.28CBC.29 + */ + const MODE_CBC = 2; + /** + * Encrypt / decrypt using the Cipher Feedback mode. + * + * @link http://en.wikipedia.org/wiki/Block_cipher_modes_of_operation#Cipher_feedback_.28CFB.29 + */ + const MODE_CFB = 3; + /** + * Encrypt / decrypt using the Cipher Feedback mode (8bit) + */ + const MODE_CFB8 = 6; + /** + * Encrypt / decrypt using the Output Feedback mode (8bit) + */ + const MODE_OFB8 = 7; + /** + * Encrypt / decrypt using the Output Feedback mode. + * + * @link http://en.wikipedia.org/wiki/Block_cipher_modes_of_operation#Output_feedback_.28OFB.29 + */ + const MODE_OFB = 4; + /** + * Encrypt / decrypt using streaming mode. + */ + const MODE_STREAM = 5; + /**#@-*/ + + /** + * Whirlpool available flag + * + * @see \phpseclib\Crypt\Base::_hashInlineCryptFunction() + * @var bool + * @access private + */ + static $WHIRLPOOL_AVAILABLE; + + /**#@+ + * @access private + * @see \phpseclib\Crypt\Base::__construct() + */ + /** + * Base value for the internal implementation $engine switch + */ + const ENGINE_INTERNAL = 1; + /** + * Base value for the mcrypt implementation $engine switch + */ + const ENGINE_MCRYPT = 2; + /** + * Base value for the mcrypt implementation $engine switch + */ + const ENGINE_OPENSSL = 3; + /**#@-*/ + + /** + * The Encryption Mode + * + * @see self::__construct() + * @var int + * @access private + */ + var $mode; + + /** + * The Block Length of the block cipher + * + * @var int + * @access private + */ + var $block_size = 16; + + /** + * The Key + * + * @see self::setKey() + * @var string + * @access private + */ + var $key = "\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0"; + + /** + * The Initialization Vector + * + * @see self::setIV() + * @var string + * @access private + */ + var $iv = ''; + + /** + * A "sliding" Initialization Vector + * + * @see self::enableContinuousBuffer() + * @see self::_clearBuffers() + * @var string + * @access private + */ + var $encryptIV; + + /** + * A "sliding" Initialization Vector + * + * @see self::enableContinuousBuffer() + * @see self::_clearBuffers() + * @var string + * @access private + */ + var $decryptIV; + + /** + * Continuous Buffer status + * + * @see self::enableContinuousBuffer() + * @var bool + * @access private + */ + var $continuousBuffer = false; + + /** + * Encryption buffer for CTR, OFB and CFB modes + * + * @see self::encrypt() + * @see self::_clearBuffers() + * @var array + * @access private + */ + var $enbuffer; + + /** + * Decryption buffer for CTR, OFB and CFB modes + * + * @see self::decrypt() + * @see self::_clearBuffers() + * @var array + * @access private + */ + var $debuffer; + + /** + * mcrypt resource for encryption + * + * The mcrypt resource can be recreated every time something needs to be created or it can be created just once. + * Since mcrypt operates in continuous mode, by default, it'll need to be recreated when in non-continuous mode. + * + * @see self::encrypt() + * @var resource + * @access private + */ + var $enmcrypt; + + /** + * mcrypt resource for decryption + * + * The mcrypt resource can be recreated every time something needs to be created or it can be created just once. + * Since mcrypt operates in continuous mode, by default, it'll need to be recreated when in non-continuous mode. + * + * @see self::decrypt() + * @var resource + * @access private + */ + var $demcrypt; + + /** + * Does the enmcrypt resource need to be (re)initialized? + * + * @see \phpseclib\Crypt\Twofish::setKey() + * @see \phpseclib\Crypt\Twofish::setIV() + * @var bool + * @access private + */ + var $enchanged = true; + + /** + * Does the demcrypt resource need to be (re)initialized? + * + * @see \phpseclib\Crypt\Twofish::setKey() + * @see \phpseclib\Crypt\Twofish::setIV() + * @var bool + * @access private + */ + var $dechanged = true; + + /** + * mcrypt resource for CFB mode + * + * mcrypt's CFB mode, in (and only in) buffered context, + * is broken, so phpseclib implements the CFB mode by it self, + * even when the mcrypt php extension is available. + * + * In order to do the CFB-mode work (fast) phpseclib + * use a separate ECB-mode mcrypt resource. + * + * @link http://phpseclib.sourceforge.net/cfb-demo.phps + * @see self::encrypt() + * @see self::decrypt() + * @see self::_setupMcrypt() + * @var resource + * @access private + */ + var $ecb; + + /** + * Optimizing value while CFB-encrypting + * + * Only relevant if $continuousBuffer enabled + * and $engine == self::ENGINE_MCRYPT + * + * It's faster to re-init $enmcrypt if + * $buffer bytes > $cfb_init_len than + * using the $ecb resource furthermore. + * + * This value depends of the chosen cipher + * and the time it would be needed for it's + * initialization [by mcrypt_generic_init()] + * which, typically, depends on the complexity + * on its internaly Key-expanding algorithm. + * + * @see self::encrypt() + * @var int + * @access private + */ + var $cfb_init_len = 600; + + /** + * Does internal cipher state need to be (re)initialized? + * + * @see self::setKey() + * @see self::setIV() + * @see self::disableContinuousBuffer() + * @var bool + * @access private + */ + var $changed = true; + + /** + * Padding status + * + * @see self::enablePadding() + * @var bool + * @access private + */ + var $padding = true; + + /** + * Is the mode one that is paddable? + * + * @see self::__construct() + * @var bool + * @access private + */ + var $paddable = false; + + /** + * Holds which crypt engine internaly should be use, + * which will be determined automatically on __construct() + * + * Currently available $engines are: + * - self::ENGINE_OPENSSL (very fast, php-extension: openssl, extension_loaded('openssl') required) + * - self::ENGINE_MCRYPT (fast, php-extension: mcrypt, extension_loaded('mcrypt') required) + * - self::ENGINE_INTERNAL (slower, pure php-engine, no php-extension required) + * + * @see self::_setEngine() + * @see self::encrypt() + * @see self::decrypt() + * @var int + * @access private + */ + var $engine; + + /** + * Holds the preferred crypt engine + * + * @see self::_setEngine() + * @see self::setPreferredEngine() + * @var int + * @access private + */ + var $preferredEngine; + + /** + * The mcrypt specific name of the cipher + * + * Only used if $engine == self::ENGINE_MCRYPT + * + * @link http://www.php.net/mcrypt_module_open + * @link http://www.php.net/mcrypt_list_algorithms + * @see self::_setupMcrypt() + * @var string + * @access private + */ + var $cipher_name_mcrypt; + + /** + * The openssl specific name of the cipher + * + * Only used if $engine == self::ENGINE_OPENSSL + * + * @link http://www.php.net/openssl-get-cipher-methods + * @var string + * @access private + */ + var $cipher_name_openssl; + + /** + * The openssl specific name of the cipher in ECB mode + * + * If OpenSSL does not support the mode we're trying to use (CTR) + * it can still be emulated with ECB mode. + * + * @link http://www.php.net/openssl-get-cipher-methods + * @var string + * @access private + */ + var $cipher_name_openssl_ecb; + + /** + * The default salt used by setPassword() + * + * @see self::setPassword() + * @var string + * @access private + */ + var $password_default_salt = 'phpseclib/salt'; + + /** + * The name of the performance-optimized callback function + * + * Used by encrypt() / decrypt() + * only if $engine == self::ENGINE_INTERNAL + * + * @see self::encrypt() + * @see self::decrypt() + * @see self::_setupInlineCrypt() + * @see self::$use_inline_crypt + * @var Callback + * @access private + */ + var $inline_crypt; + + /** + * Holds whether performance-optimized $inline_crypt() can/should be used. + * + * @see self::encrypt() + * @see self::decrypt() + * @see self::inline_crypt + * @var mixed + * @access private + */ + var $use_inline_crypt = true; + + /** + * If OpenSSL can be used in ECB but not in CTR we can emulate CTR + * + * @see self::_openssl_ctr_process() + * @var bool + * @access private + */ + var $openssl_emulate_ctr = false; + + /** + * Determines what options are passed to openssl_encrypt/decrypt + * + * @see self::isValidEngine() + * @var mixed + * @access private + */ + var $openssl_options; + + /** + * Has the key length explicitly been set or should it be derived from the key, itself? + * + * @see self::setKeyLength() + * @var bool + * @access private + */ + var $explicit_key_length = false; + + /** + * Don't truncate / null pad key + * + * @see self::_clearBuffers() + * @var bool + * @access private + */ + var $skip_key_adjustment = false; + + /** + * Default Constructor. + * + * Determines whether or not the mcrypt extension should be used. + * + * $mode could be: + * + * - self::MODE_ECB + * + * - self::MODE_CBC + * + * - self::MODE_CTR + * + * - self::MODE_CFB + * + * - self::MODE_OFB + * + * If not explicitly set, self::MODE_CBC will be used. + * + * @param int $mode + * @access public + */ + function __construct($mode = self::MODE_CBC) + { + // $mode dependent settings + switch ($mode) { + case self::MODE_ECB: + $this->paddable = true; + $this->mode = self::MODE_ECB; + break; + case self::MODE_CTR: + case self::MODE_CFB: + case self::MODE_CFB8: + case self::MODE_OFB8: + case self::MODE_OFB: + case self::MODE_STREAM: + $this->mode = $mode; + break; + case self::MODE_CBC: + default: + $this->paddable = true; + $this->mode = self::MODE_CBC; + } + + $this->_setEngine(); + + // Determining whether inline crypting can be used by the cipher + if ($this->use_inline_crypt !== false) { + $this->use_inline_crypt = version_compare(PHP_VERSION, '5.3.0') >= 0 || function_exists('create_function'); + } + + if (!defined('PHP_INT_SIZE')) { + define('PHP_INT_SIZE', 4); + } + + if (!defined('CRYPT_BASE_USE_REG_INTVAL')) { + switch (true) { + // PHP_OS & "\xDF\xDF\xDF" == strtoupper(substr(PHP_OS, 0, 3)), but a lot faster + case (PHP_OS & "\xDF\xDF\xDF") === 'WIN': + case !function_exists('php_uname'): + case !is_string(php_uname('m')): + case (php_uname('m') & "\xDF\xDF\xDF") != 'ARM': + case PHP_INT_SIZE == 8: + define('CRYPT_BASE_USE_REG_INTVAL', true); + break; + case (php_uname('m') & "\xDF\xDF\xDF") == 'ARM': + switch (true) { + /* PHP 7.0.0 introduced a bug that affected 32-bit ARM processors: + + https://github.com/php/php-src/commit/716da71446ebbd40fa6cf2cea8a4b70f504cc3cd + + altho the changelogs make no mention of it, this bug was fixed with this commit: + + https://github.com/php/php-src/commit/c1729272b17a1fe893d1a54e423d3b71470f3ee8 + + affected versions of PHP are: 7.0.x, 7.1.0 - 7.1.23 and 7.2.0 - 7.2.11 */ + case PHP_VERSION_ID >= 70000 && PHP_VERSION_ID <= 70123: + case PHP_VERSION_ID >= 70200 && PHP_VERSION_ID <= 70211: + define('CRYPT_BASE_USE_REG_INTVAL', false); + break; + default: + define('CRYPT_BASE_USE_REG_INTVAL', true); + } + } + } + } + + /** + * Sets the initialization vector. (optional) + * + * SetIV is not required when self::MODE_ECB (or ie for AES: \phpseclib\Crypt\AES::MODE_ECB) is being used. If not explicitly set, it'll be assumed + * to be all zero's. + * + * @access public + * @param string $iv + * @internal Can be overwritten by a sub class, but does not have to be + */ + function setIV($iv) + { + if ($this->mode == self::MODE_ECB) { + return; + } + + $this->iv = $iv; + $this->changed = true; + } + + /** + * Sets the key length. + * + * Keys with explicitly set lengths need to be treated accordingly + * + * @access public + * @param int $length + */ + function setKeyLength($length) + { + $this->explicit_key_length = true; + $this->changed = true; + $this->_setEngine(); + } + + /** + * Returns the current key length in bits + * + * @access public + * @return int + */ + function getKeyLength() + { + return $this->key_length << 3; + } + + /** + * Returns the current block length in bits + * + * @access public + * @return int + */ + function getBlockLength() + { + return $this->block_size << 3; + } + + /** + * Sets the key. + * + * The min/max length(s) of the key depends on the cipher which is used. + * If the key not fits the length(s) of the cipher it will paded with null bytes + * up to the closest valid key length. If the key is more than max length, + * we trim the excess bits. + * + * If the key is not explicitly set, it'll be assumed to be all null bytes. + * + * @access public + * @param string $key + * @internal Could, but not must, extend by the child Crypt_* class + */ + function setKey($key) + { + if (!$this->explicit_key_length) { + $this->setKeyLength(strlen($key) << 3); + $this->explicit_key_length = false; + } + + $this->key = $key; + $this->changed = true; + $this->_setEngine(); + } + + /** + * Sets the password. + * + * Depending on what $method is set to, setPassword()'s (optional) parameters are as follows: + * {@link http://en.wikipedia.org/wiki/PBKDF2 pbkdf2} or pbkdf1: + * $hash, $salt, $count, $dkLen + * + * Where $hash (default = sha1) currently supports the following hashes: see: Crypt/Hash.php + * {@link https://en.wikipedia.org/wiki/Bcrypt bcypt}: + * $salt, $rounds, $keylen + * + * This is a modified version of bcrypt used by OpenSSH. + * + * @see Crypt/Hash.php + * @param string $password + * @param string $method + * @return bool + * @access public + * @internal Could, but not must, extend by the child Crypt_* class + */ + function setPassword($password, $method = 'pbkdf2') + { + $key = ''; + + switch ($method) { + case 'bcrypt': + $func_args = func_get_args(); + + if (!isset($func_args[2])) { + return false; + } + + $salt = $func_args[2]; + + $rounds = isset($func_args[3]) ? $func_args[3] : 16; + $keylen = isset($func_args[4]) ? $func_args[4] : $this->key_length; + + $bf = new Blowfish(); + $key = $bf->bcrypt_pbkdf($password, $salt, $keylen + $this->block_size, $rounds); + if (!$key) { + return false; + } + + $this->setKey(substr($key, 0, $keylen)); + $this->setIV(substr($key, $keylen)); + + return true; + default: // 'pbkdf2' or 'pbkdf1' + $func_args = func_get_args(); + + // Hash function + $hash = isset($func_args[2]) ? $func_args[2] : 'sha1'; + + // WPA and WPA2 use the SSID as the salt + $salt = isset($func_args[3]) ? $func_args[3] : $this->password_default_salt; + + // RFC2898#section-4.2 uses 1,000 iterations by default + // WPA and WPA2 use 4,096. + $count = isset($func_args[4]) ? $func_args[4] : 1000; + + // Keylength + if (isset($func_args[5])) { + $dkLen = $func_args[5]; + } else { + $dkLen = $method == 'pbkdf1' ? 2 * $this->key_length : $this->key_length; + } + + switch (true) { + case $method == 'pbkdf1': + $hashObj = new Hash(); + $hashObj->setHash($hash); + if ($dkLen > $hashObj->getLength()) { + user_error('Derived key too long'); + return false; + } + $t = $password . $salt; + for ($i = 0; $i < $count; ++$i) { + $t = $hashObj->hash($t); + } + $key = substr($t, 0, $dkLen); + + $this->setKey(substr($key, 0, $dkLen >> 1)); + $this->setIV(substr($key, $dkLen >> 1)); + + return true; + // Determining if php[>=5.5.0]'s hash_pbkdf2() function avail- and useable + case !function_exists('hash_pbkdf2'): + case !function_exists('hash_algos'): + case !in_array($hash, hash_algos()): + $i = 1; + $hmac = new Hash(); + $hmac->setHash($hash); + $hmac->setKey($password); + while (strlen($key) < $dkLen) { + $f = $u = $hmac->hash($salt . pack('N', $i++)); + for ($j = 2; $j <= $count; ++$j) { + $u = $hmac->hash($u); + $f^= $u; + } + $key.= $f; + } + $key = substr($key, 0, $dkLen); + break; + default: + $key = hash_pbkdf2($hash, $password, $salt, $count, $dkLen, true); + } + } + + $this->setKey($key); + + return true; + } + + /** + * Encrypts a message. + * + * $plaintext will be padded with additional bytes such that it's length is a multiple of the block size. Other cipher + * implementations may or may not pad in the same manner. Other common approaches to padding and the reasons why it's + * necessary are discussed in the following + * URL: + * + * {@link http://www.di-mgt.com.au/cryptopad.html http://www.di-mgt.com.au/cryptopad.html} + * + * An alternative to padding is to, separately, send the length of the file. This is what SSH, in fact, does. + * strlen($plaintext) will still need to be a multiple of the block size, however, arbitrary values can be added to make it that + * length. + * + * @see self::decrypt() + * @access public + * @param string $plaintext + * @return string $ciphertext + * @internal Could, but not must, extend by the child Crypt_* class + */ + function encrypt($plaintext) + { + if ($this->paddable) { + $plaintext = $this->_pad($plaintext); + } + + if ($this->engine === self::ENGINE_OPENSSL) { + if ($this->changed) { + $this->_clearBuffers(); + $this->changed = false; + } + switch ($this->mode) { + case self::MODE_STREAM: + return openssl_encrypt($plaintext, $this->cipher_name_openssl, $this->key, $this->openssl_options); + case self::MODE_ECB: + $result = @openssl_encrypt($plaintext, $this->cipher_name_openssl, $this->key, $this->openssl_options); + return !defined('OPENSSL_RAW_DATA') ? substr($result, 0, -$this->block_size) : $result; + case self::MODE_CBC: + $result = openssl_encrypt($plaintext, $this->cipher_name_openssl, $this->key, $this->openssl_options, $this->encryptIV); + if (!defined('OPENSSL_RAW_DATA')) { + $result = substr($result, 0, -$this->block_size); + } + if ($this->continuousBuffer) { + $this->encryptIV = substr($result, -$this->block_size); + } + return $result; + case self::MODE_CTR: + return $this->_openssl_ctr_process($plaintext, $this->encryptIV, $this->enbuffer); + case self::MODE_CFB: + // cfb loosely routines inspired by openssl's: + // {@link http://cvs.openssl.org/fileview?f=openssl/crypto/modes/cfb128.c&v=1.3.2.2.2.1} + $ciphertext = ''; + if ($this->continuousBuffer) { + $iv = &$this->encryptIV; + $pos = &$this->enbuffer['pos']; + } else { + $iv = $this->encryptIV; + $pos = 0; + } + $len = strlen($plaintext); + $i = 0; + if ($pos) { + $orig_pos = $pos; + $max = $this->block_size - $pos; + if ($len >= $max) { + $i = $max; + $len-= $max; + $pos = 0; + } else { + $i = $len; + $pos+= $len; + $len = 0; + } + // ie. $i = min($max, $len), $len-= $i, $pos+= $i, $pos%= $blocksize + $ciphertext = substr($iv, $orig_pos) ^ $plaintext; + $iv = substr_replace($iv, $ciphertext, $orig_pos, $i); + $plaintext = substr($plaintext, $i); + } + + $overflow = $len % $this->block_size; + + if ($overflow) { + $ciphertext.= openssl_encrypt(substr($plaintext, 0, -$overflow) . str_repeat("\0", $this->block_size), $this->cipher_name_openssl, $this->key, $this->openssl_options, $iv); + $iv = $this->_string_pop($ciphertext, $this->block_size); + + $size = $len - $overflow; + $block = $iv ^ substr($plaintext, -$overflow); + $iv = substr_replace($iv, $block, 0, $overflow); + $ciphertext.= $block; + $pos = $overflow; + } elseif ($len) { + $ciphertext = openssl_encrypt($plaintext, $this->cipher_name_openssl, $this->key, $this->openssl_options, $iv); + $iv = substr($ciphertext, -$this->block_size); + } + + return $ciphertext; + case self::MODE_CFB8: + $ciphertext = openssl_encrypt($plaintext, $this->cipher_name_openssl, $this->key, $this->openssl_options, $this->encryptIV); + if ($this->continuousBuffer) { + if (($len = strlen($ciphertext)) >= $this->block_size) { + $this->encryptIV = substr($ciphertext, -$this->block_size); + } else { + $this->encryptIV = substr($this->encryptIV, $len - $this->block_size) . substr($ciphertext, -$len); + } + } + return $ciphertext; + case self::MODE_OFB8: + // OpenSSL has built in support for cfb8 but not ofb8 + $ciphertext = ''; + $len = strlen($plaintext); + $iv = $this->encryptIV; + + for ($i = 0; $i < $len; ++$i) { + $xor = openssl_encrypt($iv, $this->cipher_name_openssl_ecb, $this->key, $this->openssl_options, $this->decryptIV); + $ciphertext.= $plaintext[$i] ^ $xor; + $iv = substr($iv, 1) . $xor[0]; + } + + if ($this->continuousBuffer) { + $this->encryptIV = $iv; + } + break; + case self::MODE_OFB: + return $this->_openssl_ofb_process($plaintext, $this->encryptIV, $this->enbuffer); + } + } + + if ($this->engine === self::ENGINE_MCRYPT) { + set_error_handler(array($this, 'do_nothing')); + + if ($this->changed) { + $this->_setupMcrypt(); + $this->changed = false; + } + if ($this->enchanged) { + mcrypt_generic_init($this->enmcrypt, $this->key, $this->encryptIV); + $this->enchanged = false; + } + + // re: {@link http://phpseclib.sourceforge.net/cfb-demo.phps} + // using mcrypt's default handing of CFB the above would output two different things. using phpseclib's + // rewritten CFB implementation the above outputs the same thing twice. + if ($this->mode == self::MODE_CFB && $this->continuousBuffer) { + $block_size = $this->block_size; + $iv = &$this->encryptIV; + $pos = &$this->enbuffer['pos']; + $len = strlen($plaintext); + $ciphertext = ''; + $i = 0; + if ($pos) { + $orig_pos = $pos; + $max = $block_size - $pos; + if ($len >= $max) { + $i = $max; + $len-= $max; + $pos = 0; + } else { + $i = $len; + $pos+= $len; + $len = 0; + } + $ciphertext = substr($iv, $orig_pos) ^ $plaintext; + $iv = substr_replace($iv, $ciphertext, $orig_pos, $i); + $this->enbuffer['enmcrypt_init'] = true; + } + if ($len >= $block_size) { + if ($this->enbuffer['enmcrypt_init'] === false || $len > $this->cfb_init_len) { + if ($this->enbuffer['enmcrypt_init'] === true) { + mcrypt_generic_init($this->enmcrypt, $this->key, $iv); + $this->enbuffer['enmcrypt_init'] = false; + } + $ciphertext.= mcrypt_generic($this->enmcrypt, substr($plaintext, $i, $len - $len % $block_size)); + $iv = substr($ciphertext, -$block_size); + $len%= $block_size; + } else { + while ($len >= $block_size) { + $iv = mcrypt_generic($this->ecb, $iv) ^ substr($plaintext, $i, $block_size); + $ciphertext.= $iv; + $len-= $block_size; + $i+= $block_size; + } + } + } + + if ($len) { + $iv = mcrypt_generic($this->ecb, $iv); + $block = $iv ^ substr($plaintext, -$len); + $iv = substr_replace($iv, $block, 0, $len); + $ciphertext.= $block; + $pos = $len; + } + + restore_error_handler(); + + return $ciphertext; + } + + $ciphertext = mcrypt_generic($this->enmcrypt, $plaintext); + + if (!$this->continuousBuffer) { + mcrypt_generic_init($this->enmcrypt, $this->key, $this->encryptIV); + } + + restore_error_handler(); + + return $ciphertext; + } + + if ($this->changed) { + $this->_setup(); + $this->changed = false; + } + if ($this->use_inline_crypt) { + $inline = $this->inline_crypt; + return $inline('encrypt', $this, $plaintext); + } + + $buffer = &$this->enbuffer; + $block_size = $this->block_size; + $ciphertext = ''; + switch ($this->mode) { + case self::MODE_ECB: + for ($i = 0; $i < strlen($plaintext); $i+=$block_size) { + $ciphertext.= $this->_encryptBlock(substr($plaintext, $i, $block_size)); + } + break; + case self::MODE_CBC: + $xor = $this->encryptIV; + for ($i = 0; $i < strlen($plaintext); $i+=$block_size) { + $block = substr($plaintext, $i, $block_size); + $block = $this->_encryptBlock($block ^ $xor); + $xor = $block; + $ciphertext.= $block; + } + if ($this->continuousBuffer) { + $this->encryptIV = $xor; + } + break; + case self::MODE_CTR: + $xor = $this->encryptIV; + if (strlen($buffer['ciphertext'])) { + for ($i = 0; $i < strlen($plaintext); $i+=$block_size) { + $block = substr($plaintext, $i, $block_size); + if (strlen($block) > strlen($buffer['ciphertext'])) { + $buffer['ciphertext'].= $this->_encryptBlock($xor); + $this->_increment_str($xor); + } + $key = $this->_string_shift($buffer['ciphertext'], $block_size); + $ciphertext.= $block ^ $key; + } + } else { + for ($i = 0; $i < strlen($plaintext); $i+=$block_size) { + $block = substr($plaintext, $i, $block_size); + $key = $this->_encryptBlock($xor); + $this->_increment_str($xor); + $ciphertext.= $block ^ $key; + } + } + if ($this->continuousBuffer) { + $this->encryptIV = $xor; + if ($start = strlen($plaintext) % $block_size) { + $buffer['ciphertext'] = substr($key, $start) . $buffer['ciphertext']; + } + } + break; + case self::MODE_CFB: + // cfb loosely routines inspired by openssl's: + // {@link http://cvs.openssl.org/fileview?f=openssl/crypto/modes/cfb128.c&v=1.3.2.2.2.1} + if ($this->continuousBuffer) { + $iv = &$this->encryptIV; + $pos = &$buffer['pos']; + } else { + $iv = $this->encryptIV; + $pos = 0; + } + $len = strlen($plaintext); + $i = 0; + if ($pos) { + $orig_pos = $pos; + $max = $block_size - $pos; + if ($len >= $max) { + $i = $max; + $len-= $max; + $pos = 0; + } else { + $i = $len; + $pos+= $len; + $len = 0; + } + // ie. $i = min($max, $len), $len-= $i, $pos+= $i, $pos%= $blocksize + $ciphertext = substr($iv, $orig_pos) ^ $plaintext; + $iv = substr_replace($iv, $ciphertext, $orig_pos, $i); + } + while ($len >= $block_size) { + $iv = $this->_encryptBlock($iv) ^ substr($plaintext, $i, $block_size); + $ciphertext.= $iv; + $len-= $block_size; + $i+= $block_size; + } + if ($len) { + $iv = $this->_encryptBlock($iv); + $block = $iv ^ substr($plaintext, $i); + $iv = substr_replace($iv, $block, 0, $len); + $ciphertext.= $block; + $pos = $len; + } + break; + case self::MODE_CFB8: + // compared to regular CFB, which encrypts a block at a time, + // here, we're encrypting a byte at a time + $ciphertext = ''; + $len = strlen($plaintext); + $iv = $this->encryptIV; + + for ($i = 0; $i < $len; ++$i) { + $ciphertext.= ($c = $plaintext[$i] ^ $this->_encryptBlock($iv)); + $iv = substr($iv, 1) . $c; + } + + if ($this->continuousBuffer) { + if ($len >= $block_size) { + $this->encryptIV = substr($ciphertext, -$block_size); + } else { + $this->encryptIV = substr($this->encryptIV, $len - $block_size) . substr($ciphertext, -$len); + } + } + break; + case self::MODE_OFB8: + $ciphertext = ''; + $len = strlen($plaintext); + $iv = $this->encryptIV; + + for ($i = 0; $i < $len; ++$i) { + $xor = $this->_encryptBlock($iv); + $ciphertext.= $plaintext[$i] ^ $xor; + $iv = substr($iv, 1) . $xor[0]; + } + + if ($this->continuousBuffer) { + $this->encryptIV = $iv; + } + break; + case self::MODE_OFB: + $xor = $this->encryptIV; + if (strlen($buffer['xor'])) { + for ($i = 0; $i < strlen($plaintext); $i+=$block_size) { + $block = substr($plaintext, $i, $block_size); + if (strlen($block) > strlen($buffer['xor'])) { + $xor = $this->_encryptBlock($xor); + $buffer['xor'].= $xor; + } + $key = $this->_string_shift($buffer['xor'], $block_size); + $ciphertext.= $block ^ $key; + } + } else { + for ($i = 0; $i < strlen($plaintext); $i+=$block_size) { + $xor = $this->_encryptBlock($xor); + $ciphertext.= substr($plaintext, $i, $block_size) ^ $xor; + } + $key = $xor; + } + if ($this->continuousBuffer) { + $this->encryptIV = $xor; + if ($start = strlen($plaintext) % $block_size) { + $buffer['xor'] = substr($key, $start) . $buffer['xor']; + } + } + break; + case self::MODE_STREAM: + $ciphertext = $this->_encryptBlock($plaintext); + break; + } + + return $ciphertext; + } + + /** + * Decrypts a message. + * + * If strlen($ciphertext) is not a multiple of the block size, null bytes will be added to the end of the string until + * it is. + * + * @see self::encrypt() + * @access public + * @param string $ciphertext + * @return string $plaintext + * @internal Could, but not must, extend by the child Crypt_* class + */ + function decrypt($ciphertext) + { + if ($this->paddable) { + // we pad with chr(0) since that's what mcrypt_generic does. to quote from {@link http://www.php.net/function.mcrypt-generic}: + // "The data is padded with "\0" to make sure the length of the data is n * blocksize." + $ciphertext = str_pad($ciphertext, strlen($ciphertext) + ($this->block_size - strlen($ciphertext) % $this->block_size) % $this->block_size, chr(0)); + } + + if ($this->engine === self::ENGINE_OPENSSL) { + if ($this->changed) { + $this->_clearBuffers(); + $this->changed = false; + } + switch ($this->mode) { + case self::MODE_STREAM: + $plaintext = openssl_decrypt($ciphertext, $this->cipher_name_openssl, $this->key, $this->openssl_options); + break; + case self::MODE_ECB: + if (!defined('OPENSSL_RAW_DATA')) { + $ciphertext.= @openssl_encrypt('', $this->cipher_name_openssl_ecb, $this->key, true); + } + $plaintext = openssl_decrypt($ciphertext, $this->cipher_name_openssl, $this->key, $this->openssl_options); + break; + case self::MODE_CBC: + if (!defined('OPENSSL_RAW_DATA')) { + $padding = str_repeat(chr($this->block_size), $this->block_size) ^ substr($ciphertext, -$this->block_size); + $ciphertext.= substr(@openssl_encrypt($padding, $this->cipher_name_openssl_ecb, $this->key, true), 0, $this->block_size); + $offset = 2 * $this->block_size; + } else { + $offset = $this->block_size; + } + $plaintext = openssl_decrypt($ciphertext, $this->cipher_name_openssl, $this->key, $this->openssl_options, $this->decryptIV); + if ($this->continuousBuffer) { + $this->decryptIV = substr($ciphertext, -$offset, $this->block_size); + } + break; + case self::MODE_CTR: + $plaintext = $this->_openssl_ctr_process($ciphertext, $this->decryptIV, $this->debuffer); + break; + case self::MODE_CFB: + // cfb loosely routines inspired by openssl's: + // {@link http://cvs.openssl.org/fileview?f=openssl/crypto/modes/cfb128.c&v=1.3.2.2.2.1} + $plaintext = ''; + if ($this->continuousBuffer) { + $iv = &$this->decryptIV; + $pos = &$this->debuffer['pos']; + } else { + $iv = $this->decryptIV; + $pos = 0; + } + $len = strlen($ciphertext); + $i = 0; + if ($pos) { + $orig_pos = $pos; + $max = $this->block_size - $pos; + if ($len >= $max) { + $i = $max; + $len-= $max; + $pos = 0; + } else { + $i = $len; + $pos+= $len; + $len = 0; + } + // ie. $i = min($max, $len), $len-= $i, $pos+= $i, $pos%= $this->blocksize + $plaintext = substr($iv, $orig_pos) ^ $ciphertext; + $iv = substr_replace($iv, substr($ciphertext, 0, $i), $orig_pos, $i); + $ciphertext = substr($ciphertext, $i); + } + $overflow = $len % $this->block_size; + if ($overflow) { + $plaintext.= openssl_decrypt(substr($ciphertext, 0, -$overflow), $this->cipher_name_openssl, $this->key, $this->openssl_options, $iv); + if ($len - $overflow) { + $iv = substr($ciphertext, -$overflow - $this->block_size, -$overflow); + } + $iv = openssl_encrypt(str_repeat("\0", $this->block_size), $this->cipher_name_openssl, $this->key, $this->openssl_options, $iv); + $plaintext.= $iv ^ substr($ciphertext, -$overflow); + $iv = substr_replace($iv, substr($ciphertext, -$overflow), 0, $overflow); + $pos = $overflow; + } elseif ($len) { + $plaintext.= openssl_decrypt($ciphertext, $this->cipher_name_openssl, $this->key, $this->openssl_options, $iv); + $iv = substr($ciphertext, -$this->block_size); + } + break; + case self::MODE_CFB8: + $plaintext = openssl_decrypt($ciphertext, $this->cipher_name_openssl, $this->key, $this->openssl_options, $this->decryptIV); + if ($this->continuousBuffer) { + if (($len = strlen($ciphertext)) >= $this->block_size) { + $this->decryptIV = substr($ciphertext, -$this->block_size); + } else { + $this->decryptIV = substr($this->decryptIV, $len - $this->block_size) . substr($ciphertext, -$len); + } + } + break; + case self::MODE_OFB8: + $plaintext = ''; + $len = strlen($ciphertext); + $iv = $this->decryptIV; + + for ($i = 0; $i < $len; ++$i) { + $xor = openssl_encrypt($iv, $this->cipher_name_openssl_ecb, $this->key, $this->openssl_options, $this->decryptIV); + $plaintext.= $ciphertext[$i] ^ $xor; + $iv = substr($iv, 1) . $xor[0]; + } + + if ($this->continuousBuffer) { + $this->decryptIV = $iv; + } + break; + case self::MODE_OFB: + $plaintext = $this->_openssl_ofb_process($ciphertext, $this->decryptIV, $this->debuffer); + } + + return $this->paddable ? $this->_unpad($plaintext) : $plaintext; + } + + if ($this->engine === self::ENGINE_MCRYPT) { + set_error_handler(array($this, 'do_nothing')); + $block_size = $this->block_size; + if ($this->changed) { + $this->_setupMcrypt(); + $this->changed = false; + } + if ($this->dechanged) { + mcrypt_generic_init($this->demcrypt, $this->key, $this->decryptIV); + $this->dechanged = false; + } + + if ($this->mode == self::MODE_CFB && $this->continuousBuffer) { + $iv = &$this->decryptIV; + $pos = &$this->debuffer['pos']; + $len = strlen($ciphertext); + $plaintext = ''; + $i = 0; + if ($pos) { + $orig_pos = $pos; + $max = $block_size - $pos; + if ($len >= $max) { + $i = $max; + $len-= $max; + $pos = 0; + } else { + $i = $len; + $pos+= $len; + $len = 0; + } + // ie. $i = min($max, $len), $len-= $i, $pos+= $i, $pos%= $blocksize + $plaintext = substr($iv, $orig_pos) ^ $ciphertext; + $iv = substr_replace($iv, substr($ciphertext, 0, $i), $orig_pos, $i); + } + if ($len >= $block_size) { + $cb = substr($ciphertext, $i, $len - $len % $block_size); + $plaintext.= mcrypt_generic($this->ecb, $iv . $cb) ^ $cb; + $iv = substr($cb, -$block_size); + $len%= $block_size; + } + if ($len) { + $iv = mcrypt_generic($this->ecb, $iv); + $plaintext.= $iv ^ substr($ciphertext, -$len); + $iv = substr_replace($iv, substr($ciphertext, -$len), 0, $len); + $pos = $len; + } + + restore_error_handler(); + + return $plaintext; + } + + $plaintext = mdecrypt_generic($this->demcrypt, $ciphertext); + + if (!$this->continuousBuffer) { + mcrypt_generic_init($this->demcrypt, $this->key, $this->decryptIV); + } + + restore_error_handler(); + + return $this->paddable ? $this->_unpad($plaintext) : $plaintext; + } + + if ($this->changed) { + $this->_setup(); + $this->changed = false; + } + if ($this->use_inline_crypt) { + $inline = $this->inline_crypt; + return $inline('decrypt', $this, $ciphertext); + } + + $block_size = $this->block_size; + + $buffer = &$this->debuffer; + $plaintext = ''; + switch ($this->mode) { + case self::MODE_ECB: + for ($i = 0; $i < strlen($ciphertext); $i+=$block_size) { + $plaintext.= $this->_decryptBlock(substr($ciphertext, $i, $block_size)); + } + break; + case self::MODE_CBC: + $xor = $this->decryptIV; + for ($i = 0; $i < strlen($ciphertext); $i+=$block_size) { + $block = substr($ciphertext, $i, $block_size); + $plaintext.= $this->_decryptBlock($block) ^ $xor; + $xor = $block; + } + if ($this->continuousBuffer) { + $this->decryptIV = $xor; + } + break; + case self::MODE_CTR: + $xor = $this->decryptIV; + if (strlen($buffer['ciphertext'])) { + for ($i = 0; $i < strlen($ciphertext); $i+=$block_size) { + $block = substr($ciphertext, $i, $block_size); + if (strlen($block) > strlen($buffer['ciphertext'])) { + $buffer['ciphertext'].= $this->_encryptBlock($xor); + $this->_increment_str($xor); + } + $key = $this->_string_shift($buffer['ciphertext'], $block_size); + $plaintext.= $block ^ $key; + } + } else { + for ($i = 0; $i < strlen($ciphertext); $i+=$block_size) { + $block = substr($ciphertext, $i, $block_size); + $key = $this->_encryptBlock($xor); + $this->_increment_str($xor); + $plaintext.= $block ^ $key; + } + } + if ($this->continuousBuffer) { + $this->decryptIV = $xor; + if ($start = strlen($ciphertext) % $block_size) { + $buffer['ciphertext'] = substr($key, $start) . $buffer['ciphertext']; + } + } + break; + case self::MODE_CFB: + if ($this->continuousBuffer) { + $iv = &$this->decryptIV; + $pos = &$buffer['pos']; + } else { + $iv = $this->decryptIV; + $pos = 0; + } + $len = strlen($ciphertext); + $i = 0; + if ($pos) { + $orig_pos = $pos; + $max = $block_size - $pos; + if ($len >= $max) { + $i = $max; + $len-= $max; + $pos = 0; + } else { + $i = $len; + $pos+= $len; + $len = 0; + } + // ie. $i = min($max, $len), $len-= $i, $pos+= $i, $pos%= $blocksize + $plaintext = substr($iv, $orig_pos) ^ $ciphertext; + $iv = substr_replace($iv, substr($ciphertext, 0, $i), $orig_pos, $i); + } + while ($len >= $block_size) { + $iv = $this->_encryptBlock($iv); + $cb = substr($ciphertext, $i, $block_size); + $plaintext.= $iv ^ $cb; + $iv = $cb; + $len-= $block_size; + $i+= $block_size; + } + if ($len) { + $iv = $this->_encryptBlock($iv); + $plaintext.= $iv ^ substr($ciphertext, $i); + $iv = substr_replace($iv, substr($ciphertext, $i), 0, $len); + $pos = $len; + } + break; + case self::MODE_CFB8: + $plaintext = ''; + $len = strlen($ciphertext); + $iv = $this->decryptIV; + + for ($i = 0; $i < $len; ++$i) { + $plaintext.= $ciphertext[$i] ^ $this->_encryptBlock($iv); + $iv = substr($iv, 1) . $ciphertext[$i]; + } + + if ($this->continuousBuffer) { + if ($len >= $block_size) { + $this->decryptIV = substr($ciphertext, -$block_size); + } else { + $this->decryptIV = substr($this->decryptIV, $len - $block_size) . substr($ciphertext, -$len); + } + } + break; + case self::MODE_OFB8: + $plaintext = ''; + $len = strlen($ciphertext); + $iv = $this->decryptIV; + + for ($i = 0; $i < $len; ++$i) { + $xor = $this->_encryptBlock($iv); + $plaintext.= $ciphertext[$i] ^ $xor; + $iv = substr($iv, 1) . $xor[0]; + } + + if ($this->continuousBuffer) { + $this->decryptIV = $iv; + } + break; + case self::MODE_OFB: + $xor = $this->decryptIV; + if (strlen($buffer['xor'])) { + for ($i = 0; $i < strlen($ciphertext); $i+=$block_size) { + $block = substr($ciphertext, $i, $block_size); + if (strlen($block) > strlen($buffer['xor'])) { + $xor = $this->_encryptBlock($xor); + $buffer['xor'].= $xor; + } + $key = $this->_string_shift($buffer['xor'], $block_size); + $plaintext.= $block ^ $key; + } + } else { + for ($i = 0; $i < strlen($ciphertext); $i+=$block_size) { + $xor = $this->_encryptBlock($xor); + $plaintext.= substr($ciphertext, $i, $block_size) ^ $xor; + } + $key = $xor; + } + if ($this->continuousBuffer) { + $this->decryptIV = $xor; + if ($start = strlen($ciphertext) % $block_size) { + $buffer['xor'] = substr($key, $start) . $buffer['xor']; + } + } + break; + case self::MODE_STREAM: + $plaintext = $this->_decryptBlock($ciphertext); + break; + } + return $this->paddable ? $this->_unpad($plaintext) : $plaintext; + } + + /** + * OpenSSL CTR Processor + * + * PHP's OpenSSL bindings do not operate in continuous mode so we'll wrap around it. Since the keystream + * for CTR is the same for both encrypting and decrypting this function is re-used by both Base::encrypt() + * and Base::decrypt(). Also, OpenSSL doesn't implement CTR for all of it's symmetric ciphers so this + * function will emulate CTR with ECB when necessary. + * + * @see self::encrypt() + * @see self::decrypt() + * @param string $plaintext + * @param string $encryptIV + * @param array $buffer + * @return string + * @access private + */ + function _openssl_ctr_process($plaintext, &$encryptIV, &$buffer) + { + $ciphertext = ''; + + $block_size = $this->block_size; + $key = $this->key; + + if ($this->openssl_emulate_ctr) { + $xor = $encryptIV; + if (strlen($buffer['ciphertext'])) { + for ($i = 0; $i < strlen($plaintext); $i+=$block_size) { + $block = substr($plaintext, $i, $block_size); + if (strlen($block) > strlen($buffer['ciphertext'])) { + $result = @openssl_encrypt($xor, $this->cipher_name_openssl_ecb, $key, $this->openssl_options); + $result = !defined('OPENSSL_RAW_DATA') ? substr($result, 0, -$this->block_size) : $result; + $buffer['ciphertext'].= $result; + } + $this->_increment_str($xor); + $otp = $this->_string_shift($buffer['ciphertext'], $block_size); + $ciphertext.= $block ^ $otp; + } + } else { + for ($i = 0; $i < strlen($plaintext); $i+=$block_size) { + $block = substr($plaintext, $i, $block_size); + $otp = @openssl_encrypt($xor, $this->cipher_name_openssl_ecb, $key, $this->openssl_options); + $otp = !defined('OPENSSL_RAW_DATA') ? substr($otp, 0, -$this->block_size) : $otp; + $this->_increment_str($xor); + $ciphertext.= $block ^ $otp; + } + } + if ($this->continuousBuffer) { + $encryptIV = $xor; + if ($start = strlen($plaintext) % $block_size) { + $buffer['ciphertext'] = substr($key, $start) . $buffer['ciphertext']; + } + } + + return $ciphertext; + } + + if (strlen($buffer['ciphertext'])) { + $ciphertext = $plaintext ^ $this->_string_shift($buffer['ciphertext'], strlen($plaintext)); + $plaintext = substr($plaintext, strlen($ciphertext)); + + if (!strlen($plaintext)) { + return $ciphertext; + } + } + + $overflow = strlen($plaintext) % $block_size; + if ($overflow) { + $plaintext2 = $this->_string_pop($plaintext, $overflow); // ie. trim $plaintext to a multiple of $block_size and put rest of $plaintext in $plaintext2 + $encrypted = openssl_encrypt($plaintext . str_repeat("\0", $block_size), $this->cipher_name_openssl, $key, $this->openssl_options, $encryptIV); + $temp = $this->_string_pop($encrypted, $block_size); + $ciphertext.= $encrypted . ($plaintext2 ^ $temp); + if ($this->continuousBuffer) { + $buffer['ciphertext'] = substr($temp, $overflow); + $encryptIV = $temp; + } + } elseif (!strlen($buffer['ciphertext'])) { + $ciphertext.= openssl_encrypt($plaintext . str_repeat("\0", $block_size), $this->cipher_name_openssl, $key, $this->openssl_options, $encryptIV); + $temp = $this->_string_pop($ciphertext, $block_size); + if ($this->continuousBuffer) { + $encryptIV = $temp; + } + } + if ($this->continuousBuffer) { + if (!defined('OPENSSL_RAW_DATA')) { + $encryptIV.= @openssl_encrypt('', $this->cipher_name_openssl_ecb, $key, $this->openssl_options); + } + $encryptIV = openssl_decrypt($encryptIV, $this->cipher_name_openssl_ecb, $key, $this->openssl_options); + if ($overflow) { + $this->_increment_str($encryptIV); + } + } + + return $ciphertext; + } + + /** + * OpenSSL OFB Processor + * + * PHP's OpenSSL bindings do not operate in continuous mode so we'll wrap around it. Since the keystream + * for OFB is the same for both encrypting and decrypting this function is re-used by both Base::encrypt() + * and Base::decrypt(). + * + * @see self::encrypt() + * @see self::decrypt() + * @param string $plaintext + * @param string $encryptIV + * @param array $buffer + * @return string + * @access private + */ + function _openssl_ofb_process($plaintext, &$encryptIV, &$buffer) + { + if (strlen($buffer['xor'])) { + $ciphertext = $plaintext ^ $buffer['xor']; + $buffer['xor'] = substr($buffer['xor'], strlen($ciphertext)); + $plaintext = substr($plaintext, strlen($ciphertext)); + } else { + $ciphertext = ''; + } + + $block_size = $this->block_size; + + $len = strlen($plaintext); + $key = $this->key; + $overflow = $len % $block_size; + + if (strlen($plaintext)) { + if ($overflow) { + $ciphertext.= openssl_encrypt(substr($plaintext, 0, -$overflow) . str_repeat("\0", $block_size), $this->cipher_name_openssl, $key, $this->openssl_options, $encryptIV); + $xor = $this->_string_pop($ciphertext, $block_size); + if ($this->continuousBuffer) { + $encryptIV = $xor; + } + $ciphertext.= $this->_string_shift($xor, $overflow) ^ substr($plaintext, -$overflow); + if ($this->continuousBuffer) { + $buffer['xor'] = $xor; + } + } else { + $ciphertext = openssl_encrypt($plaintext, $this->cipher_name_openssl, $key, $this->openssl_options, $encryptIV); + if ($this->continuousBuffer) { + $encryptIV = substr($ciphertext, -$block_size) ^ substr($plaintext, -$block_size); + } + } + } + + return $ciphertext; + } + + /** + * phpseclib <-> OpenSSL Mode Mapper + * + * May need to be overwritten by classes extending this one in some cases + * + * @return int + * @access private + */ + function _openssl_translate_mode() + { + switch ($this->mode) { + case self::MODE_ECB: + return 'ecb'; + case self::MODE_CBC: + return 'cbc'; + case self::MODE_CTR: + return 'ctr'; + case self::MODE_CFB: + return 'cfb'; + case self::MODE_CFB8: + return 'cfb8'; + case self::MODE_OFB: + return 'ofb'; + } + } + + /** + * Pad "packets". + * + * Block ciphers working by encrypting between their specified [$this->]block_size at a time + * If you ever need to encrypt or decrypt something that isn't of the proper length, it becomes necessary to + * pad the input so that it is of the proper length. + * + * Padding is enabled by default. Sometimes, however, it is undesirable to pad strings. Such is the case in SSH, + * where "packets" are padded with random bytes before being encrypted. Unpad these packets and you risk stripping + * away characters that shouldn't be stripped away. (SSH knows how many bytes are added because the length is + * transmitted separately) + * + * @see self::disablePadding() + * @access public + */ + function enablePadding() + { + $this->padding = true; + } + + /** + * Do not pad packets. + * + * @see self::enablePadding() + * @access public + */ + function disablePadding() + { + $this->padding = false; + } + + /** + * Treat consecutive "packets" as if they are a continuous buffer. + * + * Say you have a 32-byte plaintext $plaintext. Using the default behavior, the two following code snippets + * will yield different outputs: + * + * + * echo $rijndael->encrypt(substr($plaintext, 0, 16)); + * echo $rijndael->encrypt(substr($plaintext, 16, 16)); + * + * + * echo $rijndael->encrypt($plaintext); + * + * + * The solution is to enable the continuous buffer. Although this will resolve the above discrepancy, it creates + * another, as demonstrated with the following: + * + * + * $rijndael->encrypt(substr($plaintext, 0, 16)); + * echo $rijndael->decrypt($rijndael->encrypt(substr($plaintext, 16, 16))); + * + * + * echo $rijndael->decrypt($rijndael->encrypt(substr($plaintext, 16, 16))); + * + * + * With the continuous buffer disabled, these would yield the same output. With it enabled, they yield different + * outputs. The reason is due to the fact that the initialization vector's change after every encryption / + * decryption round when the continuous buffer is enabled. When it's disabled, they remain constant. + * + * Put another way, when the continuous buffer is enabled, the state of the \phpseclib\Crypt\*() object changes after each + * encryption / decryption round, whereas otherwise, it'd remain constant. For this reason, it's recommended that + * continuous buffers not be used. They do offer better security and are, in fact, sometimes required (SSH uses them), + * however, they are also less intuitive and more likely to cause you problems. + * + * @see self::disableContinuousBuffer() + * @access public + * @internal Could, but not must, extend by the child Crypt_* class + */ + function enableContinuousBuffer() + { + if ($this->mode == self::MODE_ECB) { + return; + } + + $this->continuousBuffer = true; + + $this->_setEngine(); + } + + /** + * Treat consecutive packets as if they are a discontinuous buffer. + * + * The default behavior. + * + * @see self::enableContinuousBuffer() + * @access public + * @internal Could, but not must, extend by the child Crypt_* class + */ + function disableContinuousBuffer() + { + if ($this->mode == self::MODE_ECB) { + return; + } + if (!$this->continuousBuffer) { + return; + } + + $this->continuousBuffer = false; + $this->changed = true; + + $this->_setEngine(); + } + + /** + * Test for engine validity + * + * @see self::__construct() + * @param int $engine + * @access public + * @return bool + */ + function isValidEngine($engine) + { + switch ($engine) { + case self::ENGINE_OPENSSL: + if ($this->mode == self::MODE_STREAM && $this->continuousBuffer) { + return false; + } + $this->openssl_emulate_ctr = false; + $result = $this->cipher_name_openssl && + extension_loaded('openssl') && + // PHP 5.3.0 - 5.3.2 did not let you set IV's + version_compare(PHP_VERSION, '5.3.3', '>='); + if (!$result) { + return false; + } + + // prior to PHP 5.4.0 OPENSSL_RAW_DATA and OPENSSL_ZERO_PADDING were not defined. instead of expecting an integer + // $options openssl_encrypt expected a boolean $raw_data. + if (!defined('OPENSSL_RAW_DATA')) { + $this->openssl_options = true; + } else { + $this->openssl_options = OPENSSL_RAW_DATA | OPENSSL_ZERO_PADDING; + } + + $methods = openssl_get_cipher_methods(); + if (in_array($this->cipher_name_openssl, $methods)) { + return true; + } + // not all of openssl's symmetric cipher's support ctr. for those + // that don't we'll emulate it + switch ($this->mode) { + case self::MODE_CTR: + if (in_array($this->cipher_name_openssl_ecb, $methods)) { + $this->openssl_emulate_ctr = true; + return true; + } + } + return false; + case self::ENGINE_MCRYPT: + set_error_handler(array($this, 'do_nothing')); + $result = $this->cipher_name_mcrypt && + extension_loaded('mcrypt') && + in_array($this->cipher_name_mcrypt, mcrypt_list_algorithms()); + restore_error_handler(); + return $result; + case self::ENGINE_INTERNAL: + return true; + } + + return false; + } + + /** + * Sets the preferred crypt engine + * + * Currently, $engine could be: + * + * - \phpseclib\Crypt\Base::ENGINE_OPENSSL [very fast] + * + * - \phpseclib\Crypt\Base::ENGINE_MCRYPT [fast] + * + * - \phpseclib\Crypt\Base::ENGINE_INTERNAL [slow] + * + * If the preferred crypt engine is not available the fastest available one will be used + * + * @see self::__construct() + * @param int $engine + * @access public + */ + function setPreferredEngine($engine) + { + switch ($engine) { + //case self::ENGINE_OPENSSL; + case self::ENGINE_MCRYPT: + case self::ENGINE_INTERNAL: + $this->preferredEngine = $engine; + break; + default: + $this->preferredEngine = self::ENGINE_OPENSSL; + } + + $this->_setEngine(); + } + + /** + * Returns the engine currently being utilized + * + * @see self::_setEngine() + * @access public + */ + function getEngine() + { + return $this->engine; + } + + /** + * Sets the engine as appropriate + * + * @see self::__construct() + * @access private + */ + function _setEngine() + { + $this->engine = null; + + $candidateEngines = array( + $this->preferredEngine, + self::ENGINE_OPENSSL, + self::ENGINE_MCRYPT + ); + foreach ($candidateEngines as $engine) { + if ($this->isValidEngine($engine)) { + $this->engine = $engine; + break; + } + } + if (!$this->engine) { + $this->engine = self::ENGINE_INTERNAL; + } + + if ($this->engine != self::ENGINE_MCRYPT && $this->enmcrypt) { + set_error_handler(array($this, 'do_nothing')); + // Closing the current mcrypt resource(s). _mcryptSetup() will, if needed, + // (re)open them with the module named in $this->cipher_name_mcrypt + mcrypt_module_close($this->enmcrypt); + mcrypt_module_close($this->demcrypt); + $this->enmcrypt = null; + $this->demcrypt = null; + + if ($this->ecb) { + mcrypt_module_close($this->ecb); + $this->ecb = null; + } + restore_error_handler(); + } + + $this->changed = true; + } + + /** + * Encrypts a block + * + * Note: Must be extended by the child \phpseclib\Crypt\* class + * + * @access private + * @param string $in + * @return string + */ + abstract function _encryptBlock($in); + + /** + * Decrypts a block + * + * Note: Must be extended by the child \phpseclib\Crypt\* class + * + * @access private + * @param string $in + * @return string + */ + abstract function _decryptBlock($in); + + /** + * Setup the key (expansion) + * + * Only used if $engine == self::ENGINE_INTERNAL + * + * Note: Must extend by the child \phpseclib\Crypt\* class + * + * @see self::_setup() + * @access private + */ + abstract function _setupKey(); + + /** + * Setup the self::ENGINE_INTERNAL $engine + * + * (re)init, if necessary, the internal cipher $engine and flush all $buffers + * Used (only) if $engine == self::ENGINE_INTERNAL + * + * _setup() will be called each time if $changed === true + * typically this happens when using one or more of following public methods: + * + * - setKey() + * + * - setIV() + * + * - disableContinuousBuffer() + * + * - First run of encrypt() / decrypt() with no init-settings + * + * @see self::setKey() + * @see self::setIV() + * @see self::disableContinuousBuffer() + * @access private + * @internal _setup() is always called before en/decryption. + * @internal Could, but not must, extend by the child Crypt_* class + */ + function _setup() + { + $this->_clearBuffers(); + $this->_setupKey(); + + if ($this->use_inline_crypt) { + $this->_setupInlineCrypt(); + } + } + + /** + * Setup the self::ENGINE_MCRYPT $engine + * + * (re)init, if necessary, the (ext)mcrypt resources and flush all $buffers + * Used (only) if $engine = self::ENGINE_MCRYPT + * + * _setupMcrypt() will be called each time if $changed === true + * typically this happens when using one or more of following public methods: + * + * - setKey() + * + * - setIV() + * + * - disableContinuousBuffer() + * + * - First run of encrypt() / decrypt() + * + * @see self::setKey() + * @see self::setIV() + * @see self::disableContinuousBuffer() + * @access private + * @internal Could, but not must, extend by the child Crypt_* class + */ + function _setupMcrypt() + { + $this->_clearBuffers(); + $this->enchanged = $this->dechanged = true; + + if (!isset($this->enmcrypt)) { + static $mcrypt_modes = array( + self::MODE_CTR => 'ctr', + self::MODE_ECB => MCRYPT_MODE_ECB, + self::MODE_CBC => MCRYPT_MODE_CBC, + self::MODE_CFB => 'ncfb', + self::MODE_CFB8 => MCRYPT_MODE_CFB, + self::MODE_OFB => MCRYPT_MODE_NOFB, + self::MODE_OFB8 => MCRYPT_MODE_OFB, + self::MODE_STREAM => MCRYPT_MODE_STREAM, + ); + + $this->demcrypt = mcrypt_module_open($this->cipher_name_mcrypt, '', $mcrypt_modes[$this->mode], ''); + $this->enmcrypt = mcrypt_module_open($this->cipher_name_mcrypt, '', $mcrypt_modes[$this->mode], ''); + + // we need the $ecb mcrypt resource (only) in MODE_CFB with enableContinuousBuffer() + // to workaround mcrypt's broken ncfb implementation in buffered mode + // see: {@link http://phpseclib.sourceforge.net/cfb-demo.phps} + if ($this->mode == self::MODE_CFB) { + $this->ecb = mcrypt_module_open($this->cipher_name_mcrypt, '', MCRYPT_MODE_ECB, ''); + } + } // else should mcrypt_generic_deinit be called? + + if ($this->mode == self::MODE_CFB) { + mcrypt_generic_init($this->ecb, $this->key, str_repeat("\0", $this->block_size)); + } + } + + /** + * Pads a string + * + * Pads a string using the RSA PKCS padding standards so that its length is a multiple of the blocksize. + * $this->block_size - (strlen($text) % $this->block_size) bytes are added, each of which is equal to + * chr($this->block_size - (strlen($text) % $this->block_size) + * + * If padding is disabled and $text is not a multiple of the blocksize, the string will be padded regardless + * and padding will, hence forth, be enabled. + * + * @see self::_unpad() + * @param string $text + * @access private + * @return string + */ + function _pad($text) + { + $length = strlen($text); + + if (!$this->padding) { + if ($length % $this->block_size == 0) { + return $text; + } else { + user_error("The plaintext's length ($length) is not a multiple of the block size ({$this->block_size})"); + $this->padding = true; + } + } + + $pad = $this->block_size - ($length % $this->block_size); + + return str_pad($text, $length + $pad, chr($pad)); + } + + /** + * Unpads a string. + * + * If padding is enabled and the reported padding length is invalid the encryption key will be assumed to be wrong + * and false will be returned. + * + * @see self::_pad() + * @param string $text + * @access private + * @return string + */ + function _unpad($text) + { + if (!$this->padding) { + return $text; + } + + $length = ord($text[strlen($text) - 1]); + + if (!$length || $length > $this->block_size) { + return false; + } + + return substr($text, 0, -$length); + } + + /** + * Clears internal buffers + * + * Clearing/resetting the internal buffers is done everytime + * after disableContinuousBuffer() or on cipher $engine (re)init + * ie after setKey() or setIV() + * + * @access public + * @internal Could, but not must, extend by the child Crypt_* class + */ + function _clearBuffers() + { + $this->enbuffer = $this->debuffer = array('ciphertext' => '', 'xor' => '', 'pos' => 0, 'enmcrypt_init' => true); + + // mcrypt's handling of invalid's $iv: + // $this->encryptIV = $this->decryptIV = strlen($this->iv) == $this->block_size ? $this->iv : str_repeat("\0", $this->block_size); + $this->encryptIV = $this->decryptIV = str_pad(substr($this->iv, 0, $this->block_size), $this->block_size, "\0"); + + if (!$this->skip_key_adjustment) { + $this->key = str_pad(substr($this->key, 0, $this->key_length), $this->key_length, "\0"); + } + } + + /** + * String Shift + * + * Inspired by array_shift + * + * @param string $string + * @param int $index + * @access private + * @return string + */ + function _string_shift(&$string, $index = 1) + { + $substr = substr($string, 0, $index); + $string = substr($string, $index); + return $substr; + } + + /** + * String Pop + * + * Inspired by array_pop + * + * @param string $string + * @param int $index + * @access private + * @return string + */ + function _string_pop(&$string, $index = 1) + { + $substr = substr($string, -$index); + $string = substr($string, 0, -$index); + return $substr; + } + + /** + * Increment the current string + * + * @see self::decrypt() + * @see self::encrypt() + * @param string $var + * @access private + */ + function _increment_str(&$var) + { + if (function_exists('sodium_increment')) { + $var = strrev($var); + sodium_increment($var); + $var = strrev($var); + return; + } + + for ($i = 4; $i <= strlen($var); $i+= 4) { + $temp = substr($var, -$i, 4); + switch ($temp) { + case "\xFF\xFF\xFF\xFF": + $var = substr_replace($var, "\x00\x00\x00\x00", -$i, 4); + break; + case "\x7F\xFF\xFF\xFF": + $var = substr_replace($var, "\x80\x00\x00\x00", -$i, 4); + return; + default: + $temp = unpack('Nnum', $temp); + $var = substr_replace($var, pack('N', $temp['num'] + 1), -$i, 4); + return; + } + } + + $remainder = strlen($var) % 4; + + if ($remainder == 0) { + return; + } + + $temp = unpack('Nnum', str_pad(substr($var, 0, $remainder), 4, "\0", STR_PAD_LEFT)); + $temp = substr(pack('N', $temp['num'] + 1), -$remainder); + $var = substr_replace($var, $temp, 0, $remainder); + } + + /** + * Setup the performance-optimized function for de/encrypt() + * + * Stores the created (or existing) callback function-name + * in $this->inline_crypt + * + * Internally for phpseclib developers: + * + * _setupInlineCrypt() would be called only if: + * + * - $engine == self::ENGINE_INTERNAL and + * + * - $use_inline_crypt === true + * + * - each time on _setup(), after(!) _setupKey() + * + * + * This ensures that _setupInlineCrypt() has always a + * full ready2go initializated internal cipher $engine state + * where, for example, the keys allready expanded, + * keys/block_size calculated and such. + * + * It is, each time if called, the responsibility of _setupInlineCrypt(): + * + * - to set $this->inline_crypt to a valid and fully working callback function + * as a (faster) replacement for encrypt() / decrypt() + * + * - NOT to create unlimited callback functions (for memory reasons!) + * no matter how often _setupInlineCrypt() would be called. At some + * point of amount they must be generic re-useable. + * + * - the code of _setupInlineCrypt() it self, + * and the generated callback code, + * must be, in following order: + * - 100% safe + * - 100% compatible to encrypt()/decrypt() + * - using only php5+ features/lang-constructs/php-extensions if + * compatibility (down to php4) or fallback is provided + * - readable/maintainable/understandable/commented and... not-cryptic-styled-code :-) + * - >= 10% faster than encrypt()/decrypt() [which is, by the way, + * the reason for the existence of _setupInlineCrypt() :-)] + * - memory-nice + * - short (as good as possible) + * + * Note: - _setupInlineCrypt() is using _createInlineCryptFunction() to create the full callback function code. + * - In case of using inline crypting, _setupInlineCrypt() must extend by the child \phpseclib\Crypt\* class. + * - The following variable names are reserved: + * - $_* (all variable names prefixed with an underscore) + * - $self (object reference to it self. Do not use $this, but $self instead) + * - $in (the content of $in has to en/decrypt by the generated code) + * - The callback function should not use the 'return' statement, but en/decrypt'ing the content of $in only + * + * + * @see self::_setup() + * @see self::_createInlineCryptFunction() + * @see self::encrypt() + * @see self::decrypt() + * @access private + * @internal If a Crypt_* class providing inline crypting it must extend _setupInlineCrypt() + */ + function _setupInlineCrypt() + { + // If, for any reason, an extending \phpseclib\Crypt\Base() \phpseclib\Crypt\* class + // not using inline crypting then it must be ensured that: $this->use_inline_crypt = false + // ie in the class var declaration of $use_inline_crypt in general for the \phpseclib\Crypt\* class, + // in the constructor at object instance-time + // or, if it's runtime-specific, at runtime + + $this->use_inline_crypt = false; + } + + /** + * Creates the performance-optimized function for en/decrypt() + * + * Internally for phpseclib developers: + * + * _createInlineCryptFunction(): + * + * - merge the $cipher_code [setup'ed by _setupInlineCrypt()] + * with the current [$this->]mode of operation code + * + * - create the $inline function, which called by encrypt() / decrypt() + * as its replacement to speed up the en/decryption operations. + * + * - return the name of the created $inline callback function + * + * - used to speed up en/decryption + * + * + * + * The main reason why can speed up things [up to 50%] this way are: + * + * - using variables more effective then regular. + * (ie no use of expensive arrays but integers $k_0, $k_1 ... + * or even, for example, the pure $key[] values hardcoded) + * + * - avoiding 1000's of function calls of ie _encryptBlock() + * but inlining the crypt operations. + * in the mode of operation for() loop. + * + * - full loop unroll the (sometimes key-dependent) rounds + * avoiding this way ++$i counters and runtime-if's etc... + * + * The basic code architectur of the generated $inline en/decrypt() + * lambda function, in pseudo php, is: + * + * + * +----------------------------------------------------------------------------------------------+ + * | callback $inline = create_function: | + * | lambda_function_0001_crypt_ECB($action, $text) | + * | { | + * | INSERT PHP CODE OF: | + * | $cipher_code['init_crypt']; // general init code. | + * | // ie: $sbox'es declarations used for | + * | // encrypt and decrypt'ing. | + * | | + * | switch ($action) { | + * | case 'encrypt': | + * | INSERT PHP CODE OF: | + * | $cipher_code['init_encrypt']; // encrypt sepcific init code. | + * | ie: specified $key or $box | + * | declarations for encrypt'ing. | + * | | + * | foreach ($ciphertext) { | + * | $in = $block_size of $ciphertext; | + * | | + * | INSERT PHP CODE OF: | + * | $cipher_code['encrypt_block']; // encrypt's (string) $in, which is always: | + * | // strlen($in) == $this->block_size | + * | // here comes the cipher algorithm in action | + * | // for encryption. | + * | // $cipher_code['encrypt_block'] has to | + * | // encrypt the content of the $in variable | + * | | + * | $plaintext .= $in; | + * | } | + * | return $plaintext; | + * | | + * | case 'decrypt': | + * | INSERT PHP CODE OF: | + * | $cipher_code['init_decrypt']; // decrypt sepcific init code | + * | ie: specified $key or $box | + * | declarations for decrypt'ing. | + * | foreach ($plaintext) { | + * | $in = $block_size of $plaintext; | + * | | + * | INSERT PHP CODE OF: | + * | $cipher_code['decrypt_block']; // decrypt's (string) $in, which is always | + * | // strlen($in) == $this->block_size | + * | // here comes the cipher algorithm in action | + * | // for decryption. | + * | // $cipher_code['decrypt_block'] has to | + * | // decrypt the content of the $in variable | + * | $ciphertext .= $in; | + * | } | + * | return $ciphertext; | + * | } | + * | } | + * +----------------------------------------------------------------------------------------------+ + * + * + * See also the \phpseclib\Crypt\*::_setupInlineCrypt()'s for + * productive inline $cipher_code's how they works. + * + * Structure of: + * + * $cipher_code = array( + * 'init_crypt' => (string) '', // optional + * 'init_encrypt' => (string) '', // optional + * 'init_decrypt' => (string) '', // optional + * 'encrypt_block' => (string) '', // required + * 'decrypt_block' => (string) '' // required + * ); + * + * + * @see self::_setupInlineCrypt() + * @see self::encrypt() + * @see self::decrypt() + * @param array $cipher_code + * @access private + * @return string (the name of the created callback function) + */ + function _createInlineCryptFunction($cipher_code) + { + $block_size = $this->block_size; + + // optional + $init_crypt = isset($cipher_code['init_crypt']) ? $cipher_code['init_crypt'] : ''; + $init_encrypt = isset($cipher_code['init_encrypt']) ? $cipher_code['init_encrypt'] : ''; + $init_decrypt = isset($cipher_code['init_decrypt']) ? $cipher_code['init_decrypt'] : ''; + // required + $encrypt_block = $cipher_code['encrypt_block']; + $decrypt_block = $cipher_code['decrypt_block']; + + // Generating mode of operation inline code, + // merged with the $cipher_code algorithm + // for encrypt- and decryption. + switch ($this->mode) { + case self::MODE_ECB: + $encrypt = $init_encrypt . ' + $_ciphertext = ""; + $_plaintext_len = strlen($_text); + + for ($_i = 0; $_i < $_plaintext_len; $_i+= '.$block_size.') { + $in = substr($_text, $_i, '.$block_size.'); + '.$encrypt_block.' + $_ciphertext.= $in; + } + + return $_ciphertext; + '; + + $decrypt = $init_decrypt . ' + $_plaintext = ""; + $_text = str_pad($_text, strlen($_text) + ('.$block_size.' - strlen($_text) % '.$block_size.') % '.$block_size.', chr(0)); + $_ciphertext_len = strlen($_text); + + for ($_i = 0; $_i < $_ciphertext_len; $_i+= '.$block_size.') { + $in = substr($_text, $_i, '.$block_size.'); + '.$decrypt_block.' + $_plaintext.= $in; + } + + return $self->_unpad($_plaintext); + '; + break; + case self::MODE_CTR: + $encrypt = $init_encrypt . ' + $_ciphertext = ""; + $_plaintext_len = strlen($_text); + $_xor = $self->encryptIV; + $_buffer = &$self->enbuffer; + if (strlen($_buffer["ciphertext"])) { + for ($_i = 0; $_i < $_plaintext_len; $_i+= '.$block_size.') { + $_block = substr($_text, $_i, '.$block_size.'); + if (strlen($_block) > strlen($_buffer["ciphertext"])) { + $in = $_xor; + '.$encrypt_block.' + $self->_increment_str($_xor); + $_buffer["ciphertext"].= $in; + } + $_key = $self->_string_shift($_buffer["ciphertext"], '.$block_size.'); + $_ciphertext.= $_block ^ $_key; + } + } else { + for ($_i = 0; $_i < $_plaintext_len; $_i+= '.$block_size.') { + $_block = substr($_text, $_i, '.$block_size.'); + $in = $_xor; + '.$encrypt_block.' + $self->_increment_str($_xor); + $_key = $in; + $_ciphertext.= $_block ^ $_key; + } + } + if ($self->continuousBuffer) { + $self->encryptIV = $_xor; + if ($_start = $_plaintext_len % '.$block_size.') { + $_buffer["ciphertext"] = substr($_key, $_start) . $_buffer["ciphertext"]; + } + } + + return $_ciphertext; + '; + + $decrypt = $init_encrypt . ' + $_plaintext = ""; + $_ciphertext_len = strlen($_text); + $_xor = $self->decryptIV; + $_buffer = &$self->debuffer; + + if (strlen($_buffer["ciphertext"])) { + for ($_i = 0; $_i < $_ciphertext_len; $_i+= '.$block_size.') { + $_block = substr($_text, $_i, '.$block_size.'); + if (strlen($_block) > strlen($_buffer["ciphertext"])) { + $in = $_xor; + '.$encrypt_block.' + $self->_increment_str($_xor); + $_buffer["ciphertext"].= $in; + } + $_key = $self->_string_shift($_buffer["ciphertext"], '.$block_size.'); + $_plaintext.= $_block ^ $_key; + } + } else { + for ($_i = 0; $_i < $_ciphertext_len; $_i+= '.$block_size.') { + $_block = substr($_text, $_i, '.$block_size.'); + $in = $_xor; + '.$encrypt_block.' + $self->_increment_str($_xor); + $_key = $in; + $_plaintext.= $_block ^ $_key; + } + } + if ($self->continuousBuffer) { + $self->decryptIV = $_xor; + if ($_start = $_ciphertext_len % '.$block_size.') { + $_buffer["ciphertext"] = substr($_key, $_start) . $_buffer["ciphertext"]; + } + } + + return $_plaintext; + '; + break; + case self::MODE_CFB: + $encrypt = $init_encrypt . ' + $_ciphertext = ""; + $_buffer = &$self->enbuffer; + + if ($self->continuousBuffer) { + $_iv = &$self->encryptIV; + $_pos = &$_buffer["pos"]; + } else { + $_iv = $self->encryptIV; + $_pos = 0; + } + $_len = strlen($_text); + $_i = 0; + if ($_pos) { + $_orig_pos = $_pos; + $_max = '.$block_size.' - $_pos; + if ($_len >= $_max) { + $_i = $_max; + $_len-= $_max; + $_pos = 0; + } else { + $_i = $_len; + $_pos+= $_len; + $_len = 0; + } + $_ciphertext = substr($_iv, $_orig_pos) ^ $_text; + $_iv = substr_replace($_iv, $_ciphertext, $_orig_pos, $_i); + } + while ($_len >= '.$block_size.') { + $in = $_iv; + '.$encrypt_block.'; + $_iv = $in ^ substr($_text, $_i, '.$block_size.'); + $_ciphertext.= $_iv; + $_len-= '.$block_size.'; + $_i+= '.$block_size.'; + } + if ($_len) { + $in = $_iv; + '.$encrypt_block.' + $_iv = $in; + $_block = $_iv ^ substr($_text, $_i); + $_iv = substr_replace($_iv, $_block, 0, $_len); + $_ciphertext.= $_block; + $_pos = $_len; + } + return $_ciphertext; + '; + + $decrypt = $init_encrypt . ' + $_plaintext = ""; + $_buffer = &$self->debuffer; + + if ($self->continuousBuffer) { + $_iv = &$self->decryptIV; + $_pos = &$_buffer["pos"]; + } else { + $_iv = $self->decryptIV; + $_pos = 0; + } + $_len = strlen($_text); + $_i = 0; + if ($_pos) { + $_orig_pos = $_pos; + $_max = '.$block_size.' - $_pos; + if ($_len >= $_max) { + $_i = $_max; + $_len-= $_max; + $_pos = 0; + } else { + $_i = $_len; + $_pos+= $_len; + $_len = 0; + } + $_plaintext = substr($_iv, $_orig_pos) ^ $_text; + $_iv = substr_replace($_iv, substr($_text, 0, $_i), $_orig_pos, $_i); + } + while ($_len >= '.$block_size.') { + $in = $_iv; + '.$encrypt_block.' + $_iv = $in; + $cb = substr($_text, $_i, '.$block_size.'); + $_plaintext.= $_iv ^ $cb; + $_iv = $cb; + $_len-= '.$block_size.'; + $_i+= '.$block_size.'; + } + if ($_len) { + $in = $_iv; + '.$encrypt_block.' + $_iv = $in; + $_plaintext.= $_iv ^ substr($_text, $_i); + $_iv = substr_replace($_iv, substr($_text, $_i), 0, $_len); + $_pos = $_len; + } + + return $_plaintext; + '; + break; + case self::MODE_CFB8: + $encrypt = $init_encrypt . ' + $_ciphertext = ""; + $_len = strlen($_text); + $_iv = $self->encryptIV; + + for ($_i = 0; $_i < $_len; ++$_i) { + $in = $_iv; + '.$encrypt_block.' + $_ciphertext.= ($_c = $_text[$_i] ^ $in); + $_iv = substr($_iv, 1) . $_c; + } + + if ($self->continuousBuffer) { + if ($_len >= '.$block_size.') { + $self->encryptIV = substr($_ciphertext, -'.$block_size.'); + } else { + $self->encryptIV = substr($self->encryptIV, $_len - '.$block_size.') . substr($_ciphertext, -$_len); + } + } + + return $_ciphertext; + '; + $decrypt = $init_encrypt . ' + $_plaintext = ""; + $_len = strlen($_text); + $_iv = $self->decryptIV; + + for ($_i = 0; $_i < $_len; ++$_i) { + $in = $_iv; + '.$encrypt_block.' + $_plaintext.= $_text[$_i] ^ $in; + $_iv = substr($_iv, 1) . $_text[$_i]; + } + + if ($self->continuousBuffer) { + if ($_len >= '.$block_size.') { + $self->decryptIV = substr($_text, -'.$block_size.'); + } else { + $self->decryptIV = substr($self->decryptIV, $_len - '.$block_size.') . substr($_text, -$_len); + } + } + + return $_plaintext; + '; + break; + case self::MODE_OFB8: + $encrypt = $init_encrypt . ' + $_ciphertext = ""; + $_len = strlen($_text); + $_iv = $self->encryptIV; + + for ($_i = 0; $_i < $_len; ++$_i) { + $in = $_iv; + '.$encrypt_block.' + $_ciphertext.= $_text[$_i] ^ $in; + $_iv = substr($_iv, 1) . $in[0]; + } + + if ($self->continuousBuffer) { + $self->encryptIV = $_iv; + } + + return $_ciphertext; + '; + $decrypt = $init_encrypt . ' + $_plaintext = ""; + $_len = strlen($_text); + $_iv = $self->decryptIV; + + for ($_i = 0; $_i < $_len; ++$_i) { + $in = $_iv; + '.$encrypt_block.' + $_plaintext.= $_text[$_i] ^ $in; + $_iv = substr($_iv, 1) . $in[0]; + } + + if ($self->continuousBuffer) { + $self->decryptIV = $_iv; + } + + return $_plaintext; + '; + break; + case self::MODE_OFB: + $encrypt = $init_encrypt . ' + $_ciphertext = ""; + $_plaintext_len = strlen($_text); + $_xor = $self->encryptIV; + $_buffer = &$self->enbuffer; + + if (strlen($_buffer["xor"])) { + for ($_i = 0; $_i < $_plaintext_len; $_i+= '.$block_size.') { + $_block = substr($_text, $_i, '.$block_size.'); + if (strlen($_block) > strlen($_buffer["xor"])) { + $in = $_xor; + '.$encrypt_block.' + $_xor = $in; + $_buffer["xor"].= $_xor; + } + $_key = $self->_string_shift($_buffer["xor"], '.$block_size.'); + $_ciphertext.= $_block ^ $_key; + } + } else { + for ($_i = 0; $_i < $_plaintext_len; $_i+= '.$block_size.') { + $in = $_xor; + '.$encrypt_block.' + $_xor = $in; + $_ciphertext.= substr($_text, $_i, '.$block_size.') ^ $_xor; + } + $_key = $_xor; + } + if ($self->continuousBuffer) { + $self->encryptIV = $_xor; + if ($_start = $_plaintext_len % '.$block_size.') { + $_buffer["xor"] = substr($_key, $_start) . $_buffer["xor"]; + } + } + return $_ciphertext; + '; + + $decrypt = $init_encrypt . ' + $_plaintext = ""; + $_ciphertext_len = strlen($_text); + $_xor = $self->decryptIV; + $_buffer = &$self->debuffer; + + if (strlen($_buffer["xor"])) { + for ($_i = 0; $_i < $_ciphertext_len; $_i+= '.$block_size.') { + $_block = substr($_text, $_i, '.$block_size.'); + if (strlen($_block) > strlen($_buffer["xor"])) { + $in = $_xor; + '.$encrypt_block.' + $_xor = $in; + $_buffer["xor"].= $_xor; + } + $_key = $self->_string_shift($_buffer["xor"], '.$block_size.'); + $_plaintext.= $_block ^ $_key; + } + } else { + for ($_i = 0; $_i < $_ciphertext_len; $_i+= '.$block_size.') { + $in = $_xor; + '.$encrypt_block.' + $_xor = $in; + $_plaintext.= substr($_text, $_i, '.$block_size.') ^ $_xor; + } + $_key = $_xor; + } + if ($self->continuousBuffer) { + $self->decryptIV = $_xor; + if ($_start = $_ciphertext_len % '.$block_size.') { + $_buffer["xor"] = substr($_key, $_start) . $_buffer["xor"]; + } + } + return $_plaintext; + '; + break; + case self::MODE_STREAM: + $encrypt = $init_encrypt . ' + $_ciphertext = ""; + '.$encrypt_block.' + return $_ciphertext; + '; + $decrypt = $init_decrypt . ' + $_plaintext = ""; + '.$decrypt_block.' + return $_plaintext; + '; + break; + // case self::MODE_CBC: + default: + $encrypt = $init_encrypt . ' + $_ciphertext = ""; + $_plaintext_len = strlen($_text); + + $in = $self->encryptIV; + + for ($_i = 0; $_i < $_plaintext_len; $_i+= '.$block_size.') { + $in = substr($_text, $_i, '.$block_size.') ^ $in; + '.$encrypt_block.' + $_ciphertext.= $in; + } + + if ($self->continuousBuffer) { + $self->encryptIV = $in; + } + + return $_ciphertext; + '; + + $decrypt = $init_decrypt . ' + $_plaintext = ""; + $_text = str_pad($_text, strlen($_text) + ('.$block_size.' - strlen($_text) % '.$block_size.') % '.$block_size.', chr(0)); + $_ciphertext_len = strlen($_text); + + $_iv = $self->decryptIV; + + for ($_i = 0; $_i < $_ciphertext_len; $_i+= '.$block_size.') { + $in = $_block = substr($_text, $_i, '.$block_size.'); + '.$decrypt_block.' + $_plaintext.= $in ^ $_iv; + $_iv = $_block; + } + + if ($self->continuousBuffer) { + $self->decryptIV = $_iv; + } + + return $self->_unpad($_plaintext); + '; + break; + } + + // Create the $inline function and return its name as string. Ready to run! + eval('$func = function ($_action, &$self, $_text) { ' . $init_crypt . 'if ($_action == "encrypt") { ' . $encrypt . ' } else { ' . $decrypt . ' } };'); + return $func; + } + + /** + * Holds the lambda_functions table (classwide) + * + * Each name of the lambda function, created from + * _setupInlineCrypt() && _createInlineCryptFunction() + * is stored, classwide (!), here for reusing. + * + * The string-based index of $function is a classwide + * unique value representing, at least, the $mode of + * operation (or more... depends of the optimizing level) + * for which $mode the lambda function was created. + * + * @access private + * @return array &$functions + */ + function &_getLambdaFunctions() + { + static $functions = array(); + return $functions; + } + + /** + * Generates a digest from $bytes + * + * @see self::_setupInlineCrypt() + * @access private + * @param string $bytes + * @return string + */ + function _hashInlineCryptFunction($bytes) + { + if (!isset(self::$WHIRLPOOL_AVAILABLE)) { + self::$WHIRLPOOL_AVAILABLE = extension_loaded('hash') && in_array('whirlpool', hash_algos()); + } + + $result = ''; + $hash = $bytes; + + switch (true) { + case self::$WHIRLPOOL_AVAILABLE: + foreach (str_split($bytes, 64) as $t) { + $hash = hash('whirlpool', $hash, true); + $result .= $t ^ $hash; + } + return $result . hash('whirlpool', $hash, true); + default: + $len = strlen($bytes); + for ($i = 0; $i < $len; $i+=20) { + $t = substr($bytes, $i, 20); + $hash = pack('H*', sha1($hash)); + $result .= $t ^ $hash; + } + return $result . pack('H*', sha1($hash)); + } + } + + /** + * Convert float to int + * + * On ARM CPUs converting floats to ints doesn't always work + * + * @access private + * @param string $x + * @return int + */ + function safe_intval($x) + { + if (is_int($x)) { + return $x; + } + return (fmod($x, 0x80000000) & 0x7FFFFFFF) | + ((fmod(floor($x / 0x80000000), 2) & 1) << 31); + } + + /** + * eval()'able string for in-line float to int + * + * @access private + * @return string + */ + function safe_intval_inline() + { + if (CRYPT_BASE_USE_REG_INTVAL) { + return PHP_INT_SIZE == 4 ? 'intval(%s)' : '%s'; + } + + $safeint = '(is_int($temp = %s) ? $temp : (fmod($temp, 0x80000000) & 0x7FFFFFFF) | '; + return $safeint . '((fmod(floor($temp / 0x80000000), 2) & 1) << 31))'; + } + + /** + * Dummy error handler to suppress mcrypt errors + * + * @access private + */ + function do_nothing() + { + } + + /** + * Is the continuous buffer enabled? + * + * @access public + * @return boolean + */ + function continuousBufferEnabled() + { + return $this->continuousBuffer; + } +} diff --git a/3rdparty/phpseclib/phpseclib/phpseclib/Crypt/Blowfish.php b/3rdparty/phpseclib/phpseclib/phpseclib/Crypt/Blowfish.php new file mode 100644 index 00000000..346c064b --- /dev/null +++ b/3rdparty/phpseclib/phpseclib/phpseclib/Crypt/Blowfish.php @@ -0,0 +1,993 @@ + unpack('N*', $x), $blocks); it jumps up by an additional + * ~90MB, yielding a 106x increase in memory usage. Consequently, it bcrypt calls a different + * _encryptBlock() then the regular Blowfish does. That said, the Blowfish _encryptBlock() is + * basically just a thin wrapper around the bcrypt _encryptBlock(), so there's that. + * + * This explains 3 of the 4 _encryptBlock() implementations. the last _encryptBlock() + * implementation can best be understood by doing Ctrl + F and searching for where + * CRYPT_BASE_USE_REG_INTVAL is defined. + * + * # phpseclib's three different _setupKey() implementations + * + * Every bcrypt round is the equivalent of encrypting 512KB of data. Since OpenSSH uses 16 + * rounds by default that's ~8MB of data that's essentially being encrypted whenever + * you use bcrypt. That's a lot of data, however, bcrypt operates within tighter constraints + * than regular Blowfish, so we can use that to our advantage. In particular, whereas Blowfish + * supports variable length keys, in bcrypt, the initial "key" is the sha512 hash of the + * password. sha512 hashes are 512 bits or 64 bytes long and thus the bcrypt keys are of a + * fixed length whereas Blowfish keys are not of a fixed length. + * + * bcrypt actually has two different key expansion steps. The first one (expandstate) is + * constantly XOR'ing every _encryptBlock() parameter against the salt prior _encryptBlock()'s + * being called. The second one (expand0state) is more similar to Blowfish's _setupKey() + * but it can still use the fixed length key optimization discussed above and can do away with + * the pack() / unpack() calls. + * + * I suppose _setupKey() could be made to be a thin wrapper around expandstate() but idk it's + * just a lot of work for very marginal benefits as _setupKey() is only called once for + * regular Blowfish vs the 128 times it's called --per round-- with bcrypt. + * + * # blowfish + bcrypt in the same class + * + * Altho there's a lot of Blowfish code that bcrypt doesn't re-use, bcrypt does re-use the + * initial S-boxes, the initial P-array and the int-only _encryptBlock() implementation. + * + * # Credit + * + * phpseclib's bcrypt implementation is based losely off of OpenSSH's implementation: + * + * https://github.com/openssh/openssh-portable/blob/master/openbsd-compat/bcrypt_pbkdf.c + * + * Here's a short example of how to use this library: + * + * setKey('12345678901234567890123456789012'); + * + * $plaintext = str_repeat('a', 1024); + * + * echo $blowfish->decrypt($blowfish->encrypt($plaintext)); + * ?> + * + * + * @category Crypt + * @package Blowfish + * @author Jim Wigginton + * @author Hans-Juergen Petrich + * @copyright 2007 Jim Wigginton + * @license http://www.opensource.org/licenses/mit-license.html MIT License + * @link http://phpseclib.sourceforge.net + */ + +namespace phpseclib\Crypt; + +/** + * Pure-PHP implementation of Blowfish. + * + * @package Blowfish + * @author Jim Wigginton + * @author Hans-Juergen Petrich + * @access public + */ +class Blowfish extends Base +{ + /** + * Block Length of the cipher + * + * @see \phpseclib\Crypt\Base::block_size + * @var int + * @access private + */ + var $block_size = 8; + + /** + * The mcrypt specific name of the cipher + * + * @see \phpseclib\Crypt\Base::cipher_name_mcrypt + * @var string + * @access private + */ + var $cipher_name_mcrypt = 'blowfish'; + + /** + * Optimizing value while CFB-encrypting + * + * @see \phpseclib\Crypt\Base::cfb_init_len + * @var int + * @access private + */ + var $cfb_init_len = 500; + + /** + * SHA512 Object + * + * @see self::bcrypt_pbkdf + * @var object + * @access private + */ + var $sha512; + + /** + * The fixed subkeys boxes ($sbox0 - $sbox3) with 256 entries each + * + * S-Box 0 + * + * @access private + * @var array + */ + var $sbox0 = array( + 0xd1310ba6, 0x98dfb5ac, 0x2ffd72db, 0xd01adfb7, 0xb8e1afed, 0x6a267e96, 0xba7c9045, 0xf12c7f99, + 0x24a19947, 0xb3916cf7, 0x0801f2e2, 0x858efc16, 0x636920d8, 0x71574e69, 0xa458fea3, 0xf4933d7e, + 0x0d95748f, 0x728eb658, 0x718bcd58, 0x82154aee, 0x7b54a41d, 0xc25a59b5, 0x9c30d539, 0x2af26013, + 0xc5d1b023, 0x286085f0, 0xca417918, 0xb8db38ef, 0x8e79dcb0, 0x603a180e, 0x6c9e0e8b, 0xb01e8a3e, + 0xd71577c1, 0xbd314b27, 0x78af2fda, 0x55605c60, 0xe65525f3, 0xaa55ab94, 0x57489862, 0x63e81440, + 0x55ca396a, 0x2aab10b6, 0xb4cc5c34, 0x1141e8ce, 0xa15486af, 0x7c72e993, 0xb3ee1411, 0x636fbc2a, + 0x2ba9c55d, 0x741831f6, 0xce5c3e16, 0x9b87931e, 0xafd6ba33, 0x6c24cf5c, 0x7a325381, 0x28958677, + 0x3b8f4898, 0x6b4bb9af, 0xc4bfe81b, 0x66282193, 0x61d809cc, 0xfb21a991, 0x487cac60, 0x5dec8032, + 0xef845d5d, 0xe98575b1, 0xdc262302, 0xeb651b88, 0x23893e81, 0xd396acc5, 0x0f6d6ff3, 0x83f44239, + 0x2e0b4482, 0xa4842004, 0x69c8f04a, 0x9e1f9b5e, 0x21c66842, 0xf6e96c9a, 0x670c9c61, 0xabd388f0, + 0x6a51a0d2, 0xd8542f68, 0x960fa728, 0xab5133a3, 0x6eef0b6c, 0x137a3be4, 0xba3bf050, 0x7efb2a98, + 0xa1f1651d, 0x39af0176, 0x66ca593e, 0x82430e88, 0x8cee8619, 0x456f9fb4, 0x7d84a5c3, 0x3b8b5ebe, + 0xe06f75d8, 0x85c12073, 0x401a449f, 0x56c16aa6, 0x4ed3aa62, 0x363f7706, 0x1bfedf72, 0x429b023d, + 0x37d0d724, 0xd00a1248, 0xdb0fead3, 0x49f1c09b, 0x075372c9, 0x80991b7b, 0x25d479d8, 0xf6e8def7, + 0xe3fe501a, 0xb6794c3b, 0x976ce0bd, 0x04c006ba, 0xc1a94fb6, 0x409f60c4, 0x5e5c9ec2, 0x196a2463, + 0x68fb6faf, 0x3e6c53b5, 0x1339b2eb, 0x3b52ec6f, 0x6dfc511f, 0x9b30952c, 0xcc814544, 0xaf5ebd09, + 0xbee3d004, 0xde334afd, 0x660f2807, 0x192e4bb3, 0xc0cba857, 0x45c8740f, 0xd20b5f39, 0xb9d3fbdb, + 0x5579c0bd, 0x1a60320a, 0xd6a100c6, 0x402c7279, 0x679f25fe, 0xfb1fa3cc, 0x8ea5e9f8, 0xdb3222f8, + 0x3c7516df, 0xfd616b15, 0x2f501ec8, 0xad0552ab, 0x323db5fa, 0xfd238760, 0x53317b48, 0x3e00df82, + 0x9e5c57bb, 0xca6f8ca0, 0x1a87562e, 0xdf1769db, 0xd542a8f6, 0x287effc3, 0xac6732c6, 0x8c4f5573, + 0x695b27b0, 0xbbca58c8, 0xe1ffa35d, 0xb8f011a0, 0x10fa3d98, 0xfd2183b8, 0x4afcb56c, 0x2dd1d35b, + 0x9a53e479, 0xb6f84565, 0xd28e49bc, 0x4bfb9790, 0xe1ddf2da, 0xa4cb7e33, 0x62fb1341, 0xcee4c6e8, + 0xef20cada, 0x36774c01, 0xd07e9efe, 0x2bf11fb4, 0x95dbda4d, 0xae909198, 0xeaad8e71, 0x6b93d5a0, + 0xd08ed1d0, 0xafc725e0, 0x8e3c5b2f, 0x8e7594b7, 0x8ff6e2fb, 0xf2122b64, 0x8888b812, 0x900df01c, + 0x4fad5ea0, 0x688fc31c, 0xd1cff191, 0xb3a8c1ad, 0x2f2f2218, 0xbe0e1777, 0xea752dfe, 0x8b021fa1, + 0xe5a0cc0f, 0xb56f74e8, 0x18acf3d6, 0xce89e299, 0xb4a84fe0, 0xfd13e0b7, 0x7cc43b81, 0xd2ada8d9, + 0x165fa266, 0x80957705, 0x93cc7314, 0x211a1477, 0xe6ad2065, 0x77b5fa86, 0xc75442f5, 0xfb9d35cf, + 0xebcdaf0c, 0x7b3e89a0, 0xd6411bd3, 0xae1e7e49, 0x00250e2d, 0x2071b35e, 0x226800bb, 0x57b8e0af, + 0x2464369b, 0xf009b91e, 0x5563911d, 0x59dfa6aa, 0x78c14389, 0xd95a537f, 0x207d5ba2, 0x02e5b9c5, + 0x83260376, 0x6295cfa9, 0x11c81968, 0x4e734a41, 0xb3472dca, 0x7b14a94a, 0x1b510052, 0x9a532915, + 0xd60f573f, 0xbc9bc6e4, 0x2b60a476, 0x81e67400, 0x08ba6fb5, 0x571be91f, 0xf296ec6b, 0x2a0dd915, + 0xb6636521, 0xe7b9f9b6, 0xff34052e, 0xc5855664, 0x53b02d5d, 0xa99f8fa1, 0x08ba4799, 0x6e85076a + ); + + /** + * S-Box 1 + * + * @access private + * @var array + */ + var $sbox1 = array( + 0x4b7a70e9, 0xb5b32944, 0xdb75092e, 0xc4192623, 0xad6ea6b0, 0x49a7df7d, 0x9cee60b8, 0x8fedb266, + 0xecaa8c71, 0x699a17ff, 0x5664526c, 0xc2b19ee1, 0x193602a5, 0x75094c29, 0xa0591340, 0xe4183a3e, + 0x3f54989a, 0x5b429d65, 0x6b8fe4d6, 0x99f73fd6, 0xa1d29c07, 0xefe830f5, 0x4d2d38e6, 0xf0255dc1, + 0x4cdd2086, 0x8470eb26, 0x6382e9c6, 0x021ecc5e, 0x09686b3f, 0x3ebaefc9, 0x3c971814, 0x6b6a70a1, + 0x687f3584, 0x52a0e286, 0xb79c5305, 0xaa500737, 0x3e07841c, 0x7fdeae5c, 0x8e7d44ec, 0x5716f2b8, + 0xb03ada37, 0xf0500c0d, 0xf01c1f04, 0x0200b3ff, 0xae0cf51a, 0x3cb574b2, 0x25837a58, 0xdc0921bd, + 0xd19113f9, 0x7ca92ff6, 0x94324773, 0x22f54701, 0x3ae5e581, 0x37c2dadc, 0xc8b57634, 0x9af3dda7, + 0xa9446146, 0x0fd0030e, 0xecc8c73e, 0xa4751e41, 0xe238cd99, 0x3bea0e2f, 0x3280bba1, 0x183eb331, + 0x4e548b38, 0x4f6db908, 0x6f420d03, 0xf60a04bf, 0x2cb81290, 0x24977c79, 0x5679b072, 0xbcaf89af, + 0xde9a771f, 0xd9930810, 0xb38bae12, 0xdccf3f2e, 0x5512721f, 0x2e6b7124, 0x501adde6, 0x9f84cd87, + 0x7a584718, 0x7408da17, 0xbc9f9abc, 0xe94b7d8c, 0xec7aec3a, 0xdb851dfa, 0x63094366, 0xc464c3d2, + 0xef1c1847, 0x3215d908, 0xdd433b37, 0x24c2ba16, 0x12a14d43, 0x2a65c451, 0x50940002, 0x133ae4dd, + 0x71dff89e, 0x10314e55, 0x81ac77d6, 0x5f11199b, 0x043556f1, 0xd7a3c76b, 0x3c11183b, 0x5924a509, + 0xf28fe6ed, 0x97f1fbfa, 0x9ebabf2c, 0x1e153c6e, 0x86e34570, 0xeae96fb1, 0x860e5e0a, 0x5a3e2ab3, + 0x771fe71c, 0x4e3d06fa, 0x2965dcb9, 0x99e71d0f, 0x803e89d6, 0x5266c825, 0x2e4cc978, 0x9c10b36a, + 0xc6150eba, 0x94e2ea78, 0xa5fc3c53, 0x1e0a2df4, 0xf2f74ea7, 0x361d2b3d, 0x1939260f, 0x19c27960, + 0x5223a708, 0xf71312b6, 0xebadfe6e, 0xeac31f66, 0xe3bc4595, 0xa67bc883, 0xb17f37d1, 0x018cff28, + 0xc332ddef, 0xbe6c5aa5, 0x65582185, 0x68ab9802, 0xeecea50f, 0xdb2f953b, 0x2aef7dad, 0x5b6e2f84, + 0x1521b628, 0x29076170, 0xecdd4775, 0x619f1510, 0x13cca830, 0xeb61bd96, 0x0334fe1e, 0xaa0363cf, + 0xb5735c90, 0x4c70a239, 0xd59e9e0b, 0xcbaade14, 0xeecc86bc, 0x60622ca7, 0x9cab5cab, 0xb2f3846e, + 0x648b1eaf, 0x19bdf0ca, 0xa02369b9, 0x655abb50, 0x40685a32, 0x3c2ab4b3, 0x319ee9d5, 0xc021b8f7, + 0x9b540b19, 0x875fa099, 0x95f7997e, 0x623d7da8, 0xf837889a, 0x97e32d77, 0x11ed935f, 0x16681281, + 0x0e358829, 0xc7e61fd6, 0x96dedfa1, 0x7858ba99, 0x57f584a5, 0x1b227263, 0x9b83c3ff, 0x1ac24696, + 0xcdb30aeb, 0x532e3054, 0x8fd948e4, 0x6dbc3128, 0x58ebf2ef, 0x34c6ffea, 0xfe28ed61, 0xee7c3c73, + 0x5d4a14d9, 0xe864b7e3, 0x42105d14, 0x203e13e0, 0x45eee2b6, 0xa3aaabea, 0xdb6c4f15, 0xfacb4fd0, + 0xc742f442, 0xef6abbb5, 0x654f3b1d, 0x41cd2105, 0xd81e799e, 0x86854dc7, 0xe44b476a, 0x3d816250, + 0xcf62a1f2, 0x5b8d2646, 0xfc8883a0, 0xc1c7b6a3, 0x7f1524c3, 0x69cb7492, 0x47848a0b, 0x5692b285, + 0x095bbf00, 0xad19489d, 0x1462b174, 0x23820e00, 0x58428d2a, 0x0c55f5ea, 0x1dadf43e, 0x233f7061, + 0x3372f092, 0x8d937e41, 0xd65fecf1, 0x6c223bdb, 0x7cde3759, 0xcbee7460, 0x4085f2a7, 0xce77326e, + 0xa6078084, 0x19f8509e, 0xe8efd855, 0x61d99735, 0xa969a7aa, 0xc50c06c2, 0x5a04abfc, 0x800bcadc, + 0x9e447a2e, 0xc3453484, 0xfdd56705, 0x0e1e9ec9, 0xdb73dbd3, 0x105588cd, 0x675fda79, 0xe3674340, + 0xc5c43465, 0x713e38d8, 0x3d28f89e, 0xf16dff20, 0x153e21e7, 0x8fb03d4a, 0xe6e39f2b, 0xdb83adf7 + ); + + /** + * S-Box 2 + * + * @access private + * @var array + */ + var $sbox2 = array( + 0xe93d5a68, 0x948140f7, 0xf64c261c, 0x94692934, 0x411520f7, 0x7602d4f7, 0xbcf46b2e, 0xd4a20068, + 0xd4082471, 0x3320f46a, 0x43b7d4b7, 0x500061af, 0x1e39f62e, 0x97244546, 0x14214f74, 0xbf8b8840, + 0x4d95fc1d, 0x96b591af, 0x70f4ddd3, 0x66a02f45, 0xbfbc09ec, 0x03bd9785, 0x7fac6dd0, 0x31cb8504, + 0x96eb27b3, 0x55fd3941, 0xda2547e6, 0xabca0a9a, 0x28507825, 0x530429f4, 0x0a2c86da, 0xe9b66dfb, + 0x68dc1462, 0xd7486900, 0x680ec0a4, 0x27a18dee, 0x4f3ffea2, 0xe887ad8c, 0xb58ce006, 0x7af4d6b6, + 0xaace1e7c, 0xd3375fec, 0xce78a399, 0x406b2a42, 0x20fe9e35, 0xd9f385b9, 0xee39d7ab, 0x3b124e8b, + 0x1dc9faf7, 0x4b6d1856, 0x26a36631, 0xeae397b2, 0x3a6efa74, 0xdd5b4332, 0x6841e7f7, 0xca7820fb, + 0xfb0af54e, 0xd8feb397, 0x454056ac, 0xba489527, 0x55533a3a, 0x20838d87, 0xfe6ba9b7, 0xd096954b, + 0x55a867bc, 0xa1159a58, 0xcca92963, 0x99e1db33, 0xa62a4a56, 0x3f3125f9, 0x5ef47e1c, 0x9029317c, + 0xfdf8e802, 0x04272f70, 0x80bb155c, 0x05282ce3, 0x95c11548, 0xe4c66d22, 0x48c1133f, 0xc70f86dc, + 0x07f9c9ee, 0x41041f0f, 0x404779a4, 0x5d886e17, 0x325f51eb, 0xd59bc0d1, 0xf2bcc18f, 0x41113564, + 0x257b7834, 0x602a9c60, 0xdff8e8a3, 0x1f636c1b, 0x0e12b4c2, 0x02e1329e, 0xaf664fd1, 0xcad18115, + 0x6b2395e0, 0x333e92e1, 0x3b240b62, 0xeebeb922, 0x85b2a20e, 0xe6ba0d99, 0xde720c8c, 0x2da2f728, + 0xd0127845, 0x95b794fd, 0x647d0862, 0xe7ccf5f0, 0x5449a36f, 0x877d48fa, 0xc39dfd27, 0xf33e8d1e, + 0x0a476341, 0x992eff74, 0x3a6f6eab, 0xf4f8fd37, 0xa812dc60, 0xa1ebddf8, 0x991be14c, 0xdb6e6b0d, + 0xc67b5510, 0x6d672c37, 0x2765d43b, 0xdcd0e804, 0xf1290dc7, 0xcc00ffa3, 0xb5390f92, 0x690fed0b, + 0x667b9ffb, 0xcedb7d9c, 0xa091cf0b, 0xd9155ea3, 0xbb132f88, 0x515bad24, 0x7b9479bf, 0x763bd6eb, + 0x37392eb3, 0xcc115979, 0x8026e297, 0xf42e312d, 0x6842ada7, 0xc66a2b3b, 0x12754ccc, 0x782ef11c, + 0x6a124237, 0xb79251e7, 0x06a1bbe6, 0x4bfb6350, 0x1a6b1018, 0x11caedfa, 0x3d25bdd8, 0xe2e1c3c9, + 0x44421659, 0x0a121386, 0xd90cec6e, 0xd5abea2a, 0x64af674e, 0xda86a85f, 0xbebfe988, 0x64e4c3fe, + 0x9dbc8057, 0xf0f7c086, 0x60787bf8, 0x6003604d, 0xd1fd8346, 0xf6381fb0, 0x7745ae04, 0xd736fccc, + 0x83426b33, 0xf01eab71, 0xb0804187, 0x3c005e5f, 0x77a057be, 0xbde8ae24, 0x55464299, 0xbf582e61, + 0x4e58f48f, 0xf2ddfda2, 0xf474ef38, 0x8789bdc2, 0x5366f9c3, 0xc8b38e74, 0xb475f255, 0x46fcd9b9, + 0x7aeb2661, 0x8b1ddf84, 0x846a0e79, 0x915f95e2, 0x466e598e, 0x20b45770, 0x8cd55591, 0xc902de4c, + 0xb90bace1, 0xbb8205d0, 0x11a86248, 0x7574a99e, 0xb77f19b6, 0xe0a9dc09, 0x662d09a1, 0xc4324633, + 0xe85a1f02, 0x09f0be8c, 0x4a99a025, 0x1d6efe10, 0x1ab93d1d, 0x0ba5a4df, 0xa186f20f, 0x2868f169, + 0xdcb7da83, 0x573906fe, 0xa1e2ce9b, 0x4fcd7f52, 0x50115e01, 0xa70683fa, 0xa002b5c4, 0x0de6d027, + 0x9af88c27, 0x773f8641, 0xc3604c06, 0x61a806b5, 0xf0177a28, 0xc0f586e0, 0x006058aa, 0x30dc7d62, + 0x11e69ed7, 0x2338ea63, 0x53c2dd94, 0xc2c21634, 0xbbcbee56, 0x90bcb6de, 0xebfc7da1, 0xce591d76, + 0x6f05e409, 0x4b7c0188, 0x39720a3d, 0x7c927c24, 0x86e3725f, 0x724d9db9, 0x1ac15bb4, 0xd39eb8fc, + 0xed545578, 0x08fca5b5, 0xd83d7cd3, 0x4dad0fc4, 0x1e50ef5e, 0xb161e6f8, 0xa28514d9, 0x6c51133c, + 0x6fd5c7e7, 0x56e14ec4, 0x362abfce, 0xddc6c837, 0xd79a3234, 0x92638212, 0x670efa8e, 0x406000e0 + ); + + /** + * S-Box 3 + * + * @access private + * @var array + */ + var $sbox3 = array( + 0x3a39ce37, 0xd3faf5cf, 0xabc27737, 0x5ac52d1b, 0x5cb0679e, 0x4fa33742, 0xd3822740, 0x99bc9bbe, + 0xd5118e9d, 0xbf0f7315, 0xd62d1c7e, 0xc700c47b, 0xb78c1b6b, 0x21a19045, 0xb26eb1be, 0x6a366eb4, + 0x5748ab2f, 0xbc946e79, 0xc6a376d2, 0x6549c2c8, 0x530ff8ee, 0x468dde7d, 0xd5730a1d, 0x4cd04dc6, + 0x2939bbdb, 0xa9ba4650, 0xac9526e8, 0xbe5ee304, 0xa1fad5f0, 0x6a2d519a, 0x63ef8ce2, 0x9a86ee22, + 0xc089c2b8, 0x43242ef6, 0xa51e03aa, 0x9cf2d0a4, 0x83c061ba, 0x9be96a4d, 0x8fe51550, 0xba645bd6, + 0x2826a2f9, 0xa73a3ae1, 0x4ba99586, 0xef5562e9, 0xc72fefd3, 0xf752f7da, 0x3f046f69, 0x77fa0a59, + 0x80e4a915, 0x87b08601, 0x9b09e6ad, 0x3b3ee593, 0xe990fd5a, 0x9e34d797, 0x2cf0b7d9, 0x022b8b51, + 0x96d5ac3a, 0x017da67d, 0xd1cf3ed6, 0x7c7d2d28, 0x1f9f25cf, 0xadf2b89b, 0x5ad6b472, 0x5a88f54c, + 0xe029ac71, 0xe019a5e6, 0x47b0acfd, 0xed93fa9b, 0xe8d3c48d, 0x283b57cc, 0xf8d56629, 0x79132e28, + 0x785f0191, 0xed756055, 0xf7960e44, 0xe3d35e8c, 0x15056dd4, 0x88f46dba, 0x03a16125, 0x0564f0bd, + 0xc3eb9e15, 0x3c9057a2, 0x97271aec, 0xa93a072a, 0x1b3f6d9b, 0x1e6321f5, 0xf59c66fb, 0x26dcf319, + 0x7533d928, 0xb155fdf5, 0x03563482, 0x8aba3cbb, 0x28517711, 0xc20ad9f8, 0xabcc5167, 0xccad925f, + 0x4de81751, 0x3830dc8e, 0x379d5862, 0x9320f991, 0xea7a90c2, 0xfb3e7bce, 0x5121ce64, 0x774fbe32, + 0xa8b6e37e, 0xc3293d46, 0x48de5369, 0x6413e680, 0xa2ae0810, 0xdd6db224, 0x69852dfd, 0x09072166, + 0xb39a460a, 0x6445c0dd, 0x586cdecf, 0x1c20c8ae, 0x5bbef7dd, 0x1b588d40, 0xccd2017f, 0x6bb4e3bb, + 0xdda26a7e, 0x3a59ff45, 0x3e350a44, 0xbcb4cdd5, 0x72eacea8, 0xfa6484bb, 0x8d6612ae, 0xbf3c6f47, + 0xd29be463, 0x542f5d9e, 0xaec2771b, 0xf64e6370, 0x740e0d8d, 0xe75b1357, 0xf8721671, 0xaf537d5d, + 0x4040cb08, 0x4eb4e2cc, 0x34d2466a, 0x0115af84, 0xe1b00428, 0x95983a1d, 0x06b89fb4, 0xce6ea048, + 0x6f3f3b82, 0x3520ab82, 0x011a1d4b, 0x277227f8, 0x611560b1, 0xe7933fdc, 0xbb3a792b, 0x344525bd, + 0xa08839e1, 0x51ce794b, 0x2f32c9b7, 0xa01fbac9, 0xe01cc87e, 0xbcc7d1f6, 0xcf0111c3, 0xa1e8aac7, + 0x1a908749, 0xd44fbd9a, 0xd0dadecb, 0xd50ada38, 0x0339c32a, 0xc6913667, 0x8df9317c, 0xe0b12b4f, + 0xf79e59b7, 0x43f5bb3a, 0xf2d519ff, 0x27d9459c, 0xbf97222c, 0x15e6fc2a, 0x0f91fc71, 0x9b941525, + 0xfae59361, 0xceb69ceb, 0xc2a86459, 0x12baa8d1, 0xb6c1075e, 0xe3056a0c, 0x10d25065, 0xcb03a442, + 0xe0ec6e0e, 0x1698db3b, 0x4c98a0be, 0x3278e964, 0x9f1f9532, 0xe0d392df, 0xd3a0342b, 0x8971f21e, + 0x1b0a7441, 0x4ba3348c, 0xc5be7120, 0xc37632d8, 0xdf359f8d, 0x9b992f2e, 0xe60b6f47, 0x0fe3f11d, + 0xe54cda54, 0x1edad891, 0xce6279cf, 0xcd3e7e6f, 0x1618b166, 0xfd2c1d05, 0x848fd2c5, 0xf6fb2299, + 0xf523f357, 0xa6327623, 0x93a83531, 0x56cccd02, 0xacf08162, 0x5a75ebb5, 0x6e163697, 0x88d273cc, + 0xde966292, 0x81b949d0, 0x4c50901b, 0x71c65614, 0xe6c6c7bd, 0x327a140a, 0x45e1d006, 0xc3f27b9a, + 0xc9aa53fd, 0x62a80f00, 0xbb25bfe2, 0x35bdd2f6, 0x71126905, 0xb2040222, 0xb6cbcf7c, 0xcd769c2b, + 0x53113ec0, 0x1640e3d3, 0x38abbd60, 0x2547adf0, 0xba38209c, 0xf746ce76, 0x77afa1c5, 0x20756060, + 0x85cbfe4e, 0x8ae88dd8, 0x7aaaf9b0, 0x4cf9aa7e, 0x1948c25c, 0x02fb8a8c, 0x01c36ae4, 0xd6ebe1f9, + 0x90d4f869, 0xa65cdea0, 0x3f09252d, 0xc208e69f, 0xb74e6132, 0xce77e25b, 0x578fdfe3, 0x3ac372e6 + ); + + /** + * P-Array consists of 18 32-bit subkeys + * + * @var array + * @access private + */ + var $parray = array( + 0x243f6a88, 0x85a308d3, 0x13198a2e, 0x03707344, 0xa4093822, 0x299f31d0, + 0x082efa98, 0xec4e6c89, 0x452821e6, 0x38d01377, 0xbe5466cf, 0x34e90c6c, + 0xc0ac29b7, 0xc97c50dd, 0x3f84d5b5, 0xb5470917, 0x9216d5d9, 0x8979fb1b + ); + + /** + * The BCTX-working Array + * + * Holds the expanded key [p] and the key-depended s-boxes [sb] + * + * @var array + * @access private + */ + var $bctx; + + /** + * Holds the last used key + * + * @var array + * @access private + */ + var $kl; + + /** + * The Key Length (in bytes) + * + * @see \phpseclib\Crypt\Base::setKeyLength() + * @var int + * @access private + * @internal The max value is 256 / 8 = 32, the min value is 128 / 8 = 16. Exists in conjunction with $Nk + * because the encryption / decryption / key schedule creation requires this number and not $key_length. We could + * derive this from $key_length or vice versa, but that'd mean we'd have to do multiple shift operations, so in lieu + * of that, we'll just precompute it once. + */ + var $key_length = 16; + + /** + * Default Constructor. + * + * Determines whether or not the mcrypt extension should be used. + * + * $mode could be: + * + * - CRYPT_MODE_ECB + * + * - CRYPT_MODE_CBC + * + * - CRYPT_MODE_CTR + * + * - CRYPT_MODE_CFB + * + * - CRYPT_MODE_OFB + * + * (or the alias constants of the chosen cipher, for example for AES: CRYPT_AES_MODE_ECB or CRYPT_AES_MODE_CBC ...) + * + * If not explicitly set, CRYPT_MODE_CBC will be used. + * + * @param int $mode + * @access public + */ + function __construct($mode = self::MODE_CBC) + { + parent::__construct($mode); + + $this->sbox0 = array_map('intval', $this->sbox0); + $this->sbox1 = array_map('intval', $this->sbox1); + $this->sbox2 = array_map('intval', $this->sbox2); + $this->sbox3 = array_map('intval', $this->sbox3); + $this->parray = array_map('intval', $this->parray); + } + + /** + * Sets the key length. + * + * Key lengths can be between 32 and 448 bits. + * + * @access public + * @param int $length + */ + function setKeyLength($length) + { + if ($length < 32) { + $this->key_length = 4; + } elseif ($length > 448) { + $this->key_length = 56; + } else { + $this->key_length = $length >> 3; + } + + parent::setKeyLength($length); + } + + /** + * Test for engine validity + * + * This is mainly just a wrapper to set things up for \phpseclib\Crypt\Base::isValidEngine() + * + * @see \phpseclib\Crypt\Base::isValidEngine() + * @param int $engine + * @access public + * @return bool + */ + function isValidEngine($engine) + { + if ($engine == self::ENGINE_OPENSSL) { + // quoting https://www.openssl.org/news/openssl-3.0-notes.html, OpenSSL 3.0.1 + // "Moved all variations of the EVP ciphers CAST5, BF, IDEA, SEED, RC2, RC4, RC5, and DES to the legacy provider" + // in theory openssl_get_cipher_methods() should catch this but, on GitHub Actions, at least, it does not + if (defined('OPENSSL_VERSION_TEXT') && version_compare(preg_replace('#OpenSSL (\d+\.\d+\.\d+) .*#', '$1', OPENSSL_VERSION_TEXT), '3.0.1', '>=')) { + return false; + } + if (version_compare(PHP_VERSION, '5.3.7') < 0 && $this->key_length != 16) { + return false; + } + if ($this->key_length < 16) { + return false; + } + $this->cipher_name_openssl_ecb = 'bf-ecb'; + $this->cipher_name_openssl = 'bf-' . $this->_openssl_translate_mode(); + } + + return parent::isValidEngine($engine); + } + + /** + * Setup the key (expansion) + * + * @see \phpseclib\Crypt\Base::_setupKey() + * @access private + */ + function _setupKey() + { + if (isset($this->kl['key']) && $this->key === $this->kl['key']) { + // already expanded + return; + } + $this->kl = array('key' => $this->key); + + /* key-expanding p[] and S-Box building sb[] */ + $this->bctx = array( + 'p' => array(), + 'sb' => array( + $this->sbox0, + $this->sbox1, + $this->sbox2, + $this->sbox3 + ) + ); + + // unpack binary string in unsigned chars + $key = array_values(unpack('C*', $this->key)); + $keyl = count($key); + // with bcrypt $keyl will always be 16 (because the key is the sha512 of the key you provide) + for ($j = 0, $i = 0; $i < 18; ++$i) { + // xor P1 with the first 32-bits of the key, xor P2 with the second 32-bits ... + for ($data = 0, $k = 0; $k < 4; ++$k) { + $data = ($data << 8) | $key[$j]; + if (++$j >= $keyl) { + $j = 0; + } + } + $this->bctx['p'][] = $this->parray[$i] ^ intval($data); + } + + // encrypt the zero-string, replace P1 and P2 with the encrypted data, + // encrypt P3 and P4 with the new P1 and P2, do it with all P-array and subkeys + $data = "\0\0\0\0\0\0\0\0"; + for ($i = 0; $i < 18; $i += 2) { + list($l, $r) = array_values(unpack('N*', $data = $this->_encryptBlock($data))); + $this->bctx['p'][$i ] = $l; + $this->bctx['p'][$i + 1] = $r; + } + for ($i = 0; $i < 4; ++$i) { + for ($j = 0; $j < 256; $j += 2) { + list($l, $r) = array_values(unpack('N*', $data = $this->_encryptBlock($data))); + $this->bctx['sb'][$i][$j ] = $l; + $this->bctx['sb'][$i][$j + 1] = $r; + } + } + } + + /** + * bcrypt + * + * @param string $sha2pass + * @param string $sha2salt + * @access private + * @return string + */ + function _bcrypt_hash($sha2pass, $sha2salt) + { + $p = $this->parray; + $sbox0 = $this->sbox0; + $sbox1 = $this->sbox1; + $sbox2 = $this->sbox2; + $sbox3 = $this->sbox3; + + $cdata = array_values(unpack('N*', 'OxychromaticBlowfishSwatDynamite')); + $sha2pass = array_values(unpack('N*', $sha2pass)); + $sha2salt = array_values(unpack('N*', $sha2salt)); + + $this->_expandstate($sha2salt, $sha2pass, $sbox0, $sbox1, $sbox2, $sbox3, $p); + for ($i = 0; $i < 64; $i++) { + $this->_expand0state($sha2salt, $sbox0, $sbox1, $sbox2, $sbox3, $p); + $this->_expand0state($sha2pass, $sbox0, $sbox1, $sbox2, $sbox3, $p); + } + + for ($i = 0; $i < 64; $i++) { + for ($j = 0; $j < 8; $j+= 2) { // count($cdata) == 8 + list($cdata[$j], $cdata[$j + 1]) = $this->_encryptBlockHelperFast($cdata[$j], $cdata[$j + 1], $sbox0, $sbox1, $sbox2, $sbox3, $p); + } + } + + $output = ''; + for ($i = 0; $i < count($cdata); $i++) { + $output.= pack('L*', $cdata[$i]); + } + return $output; + } + + /** + * Performs OpenSSH-style bcrypt + * + * @param string $pass + * @param string $salt + * @param int $keylen + * @param int $rounds + * @access public + * @return false|string + */ + function bcrypt_pbkdf($pass, $salt, $keylen, $rounds) + { + if (PHP_INT_SIZE == 4) { + user_error('bcrypt is far too slow to be practical on 32-bit versions of PHP'); + return false; + } + + if (!isset($this->sha512)) { + $this->sha512 = new Hash('sha512'); + } + + $sha2pass = $this->sha512->hash($pass); + $results = array(); + $count = 1; + while (32 * count($results) < $keylen) { + $countsalt = $salt . pack('N', $count++); + $sha2salt = $this->sha512->hash($countsalt); + $out = $tmpout = $this->_bcrypt_hash($sha2pass, $sha2salt); + for ($i = 1; $i < $rounds; $i++) { + $sha2salt = $this->sha512->hash($tmpout); + $tmpout = $this->_bcrypt_hash($sha2pass, $sha2salt); + $out^= $tmpout; + } + $results[] = $out; + } + $output = ''; + for ($i = 0; $i < 32; $i++) { + foreach ($results as $result) { + $output.= $result[$i]; + } + } + return substr($output, 0, $keylen); + } + + /** + * Key expansion without salt + * + * @access private + * @param int[] $key + * @param int[] $sbox0 + * @param int[] $sbox1 + * @param int[] $sbox2 + * @param int[] $sbox3 + * @param int[] $p + * @see self::_bcrypt_hash() + */ + function _expand0state($key, &$sbox0, &$sbox1, &$sbox2, &$sbox3, &$p) + { + // expand0state is basically the same thing as this: + //return $this->_expandstate(array_fill(0, 16, 0), $key); + // but this separate function eliminates a bunch of XORs and array lookups + + $p = array( + $p[0] ^ $key[0], + $p[1] ^ $key[1], + $p[2] ^ $key[2], + $p[3] ^ $key[3], + $p[4] ^ $key[4], + $p[5] ^ $key[5], + $p[6] ^ $key[6], + $p[7] ^ $key[7], + $p[8] ^ $key[8], + $p[9] ^ $key[9], + $p[10] ^ $key[10], + $p[11] ^ $key[11], + $p[12] ^ $key[12], + $p[13] ^ $key[13], + $p[14] ^ $key[14], + $p[15] ^ $key[15], + $p[16] ^ $key[0], + $p[17] ^ $key[1] + ); + + // @codingStandardsIgnoreStart + list( $p[0], $p[1]) = $this->_encryptBlockHelperFast( 0, 0, $sbox0, $sbox1, $sbox2, $sbox3, $p); + list( $p[2], $p[3]) = $this->_encryptBlockHelperFast($p[ 0], $p[ 1], $sbox0, $sbox1, $sbox2, $sbox3, $p); + list( $p[4], $p[5]) = $this->_encryptBlockHelperFast($p[ 2], $p[ 3], $sbox0, $sbox1, $sbox2, $sbox3, $p); + list( $p[6], $p[7]) = $this->_encryptBlockHelperFast($p[ 4], $p[ 5], $sbox0, $sbox1, $sbox2, $sbox3, $p); + list( $p[8], $p[9]) = $this->_encryptBlockHelperFast($p[ 6], $p[ 7], $sbox0, $sbox1, $sbox2, $sbox3, $p); + list($p[10], $p[11]) = $this->_encryptBlockHelperFast($p[ 8], $p[ 9], $sbox0, $sbox1, $sbox2, $sbox3, $p); + list($p[12], $p[13]) = $this->_encryptBlockHelperFast($p[10], $p[11], $sbox0, $sbox1, $sbox2, $sbox3, $p); + list($p[14], $p[15]) = $this->_encryptBlockHelperFast($p[12], $p[13], $sbox0, $sbox1, $sbox2, $sbox3, $p); + list($p[16], $p[17]) = $this->_encryptBlockHelperFast($p[14], $p[15], $sbox0, $sbox1, $sbox2, $sbox3, $p); + // @codingStandardsIgnoreEnd + + list($sbox0[0], $sbox0[1]) = $this->_encryptBlockHelperFast($p[16], $p[17], $sbox0, $sbox1, $sbox2, $sbox3, $p); + for ($i = 2; $i < 256; $i+= 2) { + list($sbox0[$i], $sbox0[$i + 1]) = $this->_encryptBlockHelperFast($sbox0[$i - 2], $sbox0[$i - 1], $sbox0, $sbox1, $sbox2, $sbox3, $p); + } + + list($sbox1[0], $sbox1[1]) = $this->_encryptBlockHelperFast($sbox0[254], $sbox0[255], $sbox0, $sbox1, $sbox2, $sbox3, $p); + for ($i = 2; $i < 256; $i+= 2) { + list($sbox1[$i], $sbox1[$i + 1]) = $this->_encryptBlockHelperFast($sbox1[$i - 2], $sbox1[$i - 1], $sbox0, $sbox1, $sbox2, $sbox3, $p); + } + + list($sbox2[0], $sbox2[1]) = $this->_encryptBlockHelperFast($sbox1[254], $sbox1[255], $sbox0, $sbox1, $sbox2, $sbox3, $p); + for ($i = 2; $i < 256; $i+= 2) { + list($sbox2[$i], $sbox2[$i + 1]) = $this->_encryptBlockHelperFast($sbox2[$i - 2], $sbox2[$i - 1], $sbox0, $sbox1, $sbox2, $sbox3, $p); + } + + list($sbox3[0], $sbox3[1]) = $this->_encryptBlockHelperFast($sbox2[254], $sbox2[255], $sbox0, $sbox1, $sbox2, $sbox3, $p); + for ($i = 2; $i < 256; $i+= 2) { + list($sbox3[$i], $sbox3[$i + 1]) = $this->_encryptBlockHelperFast($sbox3[$i - 2], $sbox3[$i - 1], $sbox0, $sbox1, $sbox2, $sbox3, $p); + } + } + + /** + * Key expansion with salt + * + * @access private + * @param int[] $data + * @param int[] $key + * @param int[] $sbox0 + * @param int[] $sbox1 + * @param int[] $sbox2 + * @param int[] $sbox3 + * @param int[] $p + * @see self::_bcrypt_hash() + */ + function _expandstate($data, $key, &$sbox0, &$sbox1, &$sbox2, &$sbox3, &$p) + { + $p = array( + $p[0] ^ $key[0], + $p[1] ^ $key[1], + $p[2] ^ $key[2], + $p[3] ^ $key[3], + $p[4] ^ $key[4], + $p[5] ^ $key[5], + $p[6] ^ $key[6], + $p[7] ^ $key[7], + $p[8] ^ $key[8], + $p[9] ^ $key[9], + $p[10] ^ $key[10], + $p[11] ^ $key[11], + $p[12] ^ $key[12], + $p[13] ^ $key[13], + $p[14] ^ $key[14], + $p[15] ^ $key[15], + $p[16] ^ $key[0], + $p[17] ^ $key[1] + ); + + // @codingStandardsIgnoreStart + list( $p[0], $p[1]) = $this->_encryptBlockHelperFast($data[ 0] , $data[ 1] , $sbox0, $sbox1, $sbox2, $sbox3, $p); + list( $p[2], $p[3]) = $this->_encryptBlockHelperFast($data[ 2] ^ $p[ 0], $data[ 3] ^ $p[ 1], $sbox0, $sbox1, $sbox2, $sbox3, $p); + list( $p[4], $p[5]) = $this->_encryptBlockHelperFast($data[ 4] ^ $p[ 2], $data[ 5] ^ $p[ 3], $sbox0, $sbox1, $sbox2, $sbox3, $p); + list( $p[6], $p[7]) = $this->_encryptBlockHelperFast($data[ 6] ^ $p[ 4], $data[ 7] ^ $p[ 5], $sbox0, $sbox1, $sbox2, $sbox3, $p); + list( $p[8], $p[9]) = $this->_encryptBlockHelperFast($data[ 8] ^ $p[ 6], $data[ 9] ^ $p[ 7], $sbox0, $sbox1, $sbox2, $sbox3, $p); + list($p[10], $p[11]) = $this->_encryptBlockHelperFast($data[10] ^ $p[ 8], $data[11] ^ $p[ 9], $sbox0, $sbox1, $sbox2, $sbox3, $p); + list($p[12], $p[13]) = $this->_encryptBlockHelperFast($data[12] ^ $p[10], $data[13] ^ $p[11], $sbox0, $sbox1, $sbox2, $sbox3, $p); + list($p[14], $p[15]) = $this->_encryptBlockHelperFast($data[14] ^ $p[12], $data[15] ^ $p[13], $sbox0, $sbox1, $sbox2, $sbox3, $p); + list($p[16], $p[17]) = $this->_encryptBlockHelperFast($data[ 0] ^ $p[14], $data[ 1] ^ $p[15], $sbox0, $sbox1, $sbox2, $sbox3, $p); + // @codingStandardsIgnoreEnd + + list($sbox0[0], $sbox0[1]) = $this->_encryptBlockHelperFast($data[2] ^ $p[16], $data[3] ^ $p[17], $sbox0, $sbox1, $sbox2, $sbox3, $p); + for ($i = 2, $j = 4; $i < 256; $i+= 2, $j = ($j + 2) % 16) { // instead of 16 maybe count($data) would be better? + list($sbox0[$i], $sbox0[$i + 1]) = $this->_encryptBlockHelperFast($data[$j] ^ $sbox0[$i - 2], $data[$j + 1] ^ $sbox0[$i - 1], $sbox0, $sbox1, $sbox2, $sbox3, $p); + } + + list($sbox1[0], $sbox1[1]) = $this->_encryptBlockHelperFast($data[2] ^ $sbox0[254], $data[3] ^ $sbox0[255], $sbox0, $sbox1, $sbox2, $sbox3, $p); + for ($i = 2, $j = 4; $i < 256; $i+= 2, $j = ($j + 2) % 16) { + list($sbox1[$i], $sbox1[$i + 1]) = $this->_encryptBlockHelperFast($data[$j] ^ $sbox1[$i - 2], $data[$j + 1] ^ $sbox1[$i - 1], $sbox0, $sbox1, $sbox2, $sbox3, $p); + } + + list($sbox2[0], $sbox2[1]) = $this->_encryptBlockHelperFast($data[2] ^ $sbox1[254], $data[3] ^ $sbox1[255], $sbox0, $sbox1, $sbox2, $sbox3, $p); + for ($i = 2, $j = 4; $i < 256; $i+= 2, $j = ($j + 2) % 16) { + list($sbox2[$i], $sbox2[$i + 1]) = $this->_encryptBlockHelperFast($data[$j] ^ $sbox2[$i - 2], $data[$j + 1] ^ $sbox2[$i - 1], $sbox0, $sbox1, $sbox2, $sbox3, $p); + } + + list($sbox3[0], $sbox3[1]) = $this->_encryptBlockHelperFast($data[2] ^ $sbox2[254], $data[3] ^ $sbox2[255], $sbox0, $sbox1, $sbox2, $sbox3, $p); + for ($i = 2, $j = 4; $i < 256; $i+= 2, $j = ($j + 2) % 16) { + list($sbox3[$i], $sbox3[$i + 1]) = $this->_encryptBlockHelperFast($data[$j] ^ $sbox3[$i - 2], $data[$j + 1] ^ $sbox3[$i - 1], $sbox0, $sbox1, $sbox2, $sbox3, $p); + } + } + + /** + * Encrypts a block + * + * @access private + * @param string $in + * @return string + */ + function _encryptBlock($in) + { + $p = $this->bctx["p"]; + // extract($this->bctx["sb"], EXTR_PREFIX_ALL, "sb"); // slower + $sb_0 = $this->bctx["sb"][0]; + $sb_1 = $this->bctx["sb"][1]; + $sb_2 = $this->bctx["sb"][2]; + $sb_3 = $this->bctx["sb"][3]; + + $in = unpack("N*", $in); + $l = $in[1]; + $r = $in[2]; + + list($r, $l) = PHP_INT_SIZE === 8 ? + $this->_encryptBlockHelperFast($l, $r, $sb_0, $sb_1, $sb_2, $sb_3, $p) : + $this->_encryptBlockHelperSlow($l, $r, $sb_0, $sb_1, $sb_2, $sb_3, $p); + + return pack("N*", $r, $l); + } + + /** + * Fast helper function for block encryption + * + * @access private + * @param int $x0 + * @param int $x1 + * @param int[] $sbox0 + * @param int[] $sbox1 + * @param int[] $sbox2 + * @param int[] $sbox3 + * @param int[] $p + * @return int[] + */ + function _encryptBlockHelperFast($x0, $x1, $sbox0, $sbox1, $sbox2, $sbox3, $p) + { + $x0 ^= $p[0]; + $x1 ^= ((($sbox0[($x0 & 0xFF000000) >> 24] + $sbox1[($x0 & 0xFF0000) >> 16]) ^ $sbox2[($x0 & 0xFF00) >> 8]) + $sbox3[$x0 & 0xFF]) ^ $p[1]; + $x0 ^= ((($sbox0[($x1 & 0xFF000000) >> 24] + $sbox1[($x1 & 0xFF0000) >> 16]) ^ $sbox2[($x1 & 0xFF00) >> 8]) + $sbox3[$x1 & 0xFF]) ^ $p[2]; + $x1 ^= ((($sbox0[($x0 & 0xFF000000) >> 24] + $sbox1[($x0 & 0xFF0000) >> 16]) ^ $sbox2[($x0 & 0xFF00) >> 8]) + $sbox3[$x0 & 0xFF]) ^ $p[3]; + $x0 ^= ((($sbox0[($x1 & 0xFF000000) >> 24] + $sbox1[($x1 & 0xFF0000) >> 16]) ^ $sbox2[($x1 & 0xFF00) >> 8]) + $sbox3[$x1 & 0xFF]) ^ $p[4]; + $x1 ^= ((($sbox0[($x0 & 0xFF000000) >> 24] + $sbox1[($x0 & 0xFF0000) >> 16]) ^ $sbox2[($x0 & 0xFF00) >> 8]) + $sbox3[$x0 & 0xFF]) ^ $p[5]; + $x0 ^= ((($sbox0[($x1 & 0xFF000000) >> 24] + $sbox1[($x1 & 0xFF0000) >> 16]) ^ $sbox2[($x1 & 0xFF00) >> 8]) + $sbox3[$x1 & 0xFF]) ^ $p[6]; + $x1 ^= ((($sbox0[($x0 & 0xFF000000) >> 24] + $sbox1[($x0 & 0xFF0000) >> 16]) ^ $sbox2[($x0 & 0xFF00) >> 8]) + $sbox3[$x0 & 0xFF]) ^ $p[7]; + $x0 ^= ((($sbox0[($x1 & 0xFF000000) >> 24] + $sbox1[($x1 & 0xFF0000) >> 16]) ^ $sbox2[($x1 & 0xFF00) >> 8]) + $sbox3[$x1 & 0xFF]) ^ $p[8]; + $x1 ^= ((($sbox0[($x0 & 0xFF000000) >> 24] + $sbox1[($x0 & 0xFF0000) >> 16]) ^ $sbox2[($x0 & 0xFF00) >> 8]) + $sbox3[$x0 & 0xFF]) ^ $p[9]; + $x0 ^= ((($sbox0[($x1 & 0xFF000000) >> 24] + $sbox1[($x1 & 0xFF0000) >> 16]) ^ $sbox2[($x1 & 0xFF00) >> 8]) + $sbox3[$x1 & 0xFF]) ^ $p[10]; + $x1 ^= ((($sbox0[($x0 & 0xFF000000) >> 24] + $sbox1[($x0 & 0xFF0000) >> 16]) ^ $sbox2[($x0 & 0xFF00) >> 8]) + $sbox3[$x0 & 0xFF]) ^ $p[11]; + $x0 ^= ((($sbox0[($x1 & 0xFF000000) >> 24] + $sbox1[($x1 & 0xFF0000) >> 16]) ^ $sbox2[($x1 & 0xFF00) >> 8]) + $sbox3[$x1 & 0xFF]) ^ $p[12]; + $x1 ^= ((($sbox0[($x0 & 0xFF000000) >> 24] + $sbox1[($x0 & 0xFF0000) >> 16]) ^ $sbox2[($x0 & 0xFF00) >> 8]) + $sbox3[$x0 & 0xFF]) ^ $p[13]; + $x0 ^= ((($sbox0[($x1 & 0xFF000000) >> 24] + $sbox1[($x1 & 0xFF0000) >> 16]) ^ $sbox2[($x1 & 0xFF00) >> 8]) + $sbox3[$x1 & 0xFF]) ^ $p[14]; + $x1 ^= ((($sbox0[($x0 & 0xFF000000) >> 24] + $sbox1[($x0 & 0xFF0000) >> 16]) ^ $sbox2[($x0 & 0xFF00) >> 8]) + $sbox3[$x0 & 0xFF]) ^ $p[15]; + $x0 ^= ((($sbox0[($x1 & 0xFF000000) >> 24] + $sbox1[($x1 & 0xFF0000) >> 16]) ^ $sbox2[($x1 & 0xFF00) >> 8]) + $sbox3[$x1 & 0xFF]) ^ $p[16]; + + return array($x1 & 0xFFFFFFFF ^ $p[17], $x0 & 0xFFFFFFFF); + } + + /** + * Slow helper function for block encryption + * + * @access private + * @param int $x0 + * @param int $x1 + * @param int[] $sbox0 + * @param int[] $sbox1 + * @param int[] $sbox2 + * @param int[] $sbox3 + * @param int[] $p + * @return int[] + */ + function _encryptBlockHelperSlow($x0, $x1, $sbox0, $sbox1, $sbox2, $sbox3, $p) + { + // -16777216 == intval(0xFF000000) on 32-bit PHP installs + $x0^= $p[0]; + $x1^= $this->safe_intval(($this->safe_intval($sbox0[(($x0 & -16777216) >> 24) & 0xFF] + $sbox1[($x0 & 0xFF0000) >> 16]) ^ $sbox2[($x0 & 0xFF00) >> 8]) + $sbox3[$x0 & 0xFF]) ^ $p[1]; + $x0^= $this->safe_intval(($this->safe_intval($sbox0[(($x1 & -16777216) >> 24) & 0xFF] + $sbox1[($x1 & 0xFF0000) >> 16]) ^ $sbox2[($x1 & 0xFF00) >> 8]) + $sbox3[$x1 & 0xFF]) ^ $p[2]; + $x1^= $this->safe_intval(($this->safe_intval($sbox0[(($x0 & -16777216) >> 24) & 0xFF] + $sbox1[($x0 & 0xFF0000) >> 16]) ^ $sbox2[($x0 & 0xFF00) >> 8]) + $sbox3[$x0 & 0xFF]) ^ $p[3]; + $x0^= $this->safe_intval(($this->safe_intval($sbox0[(($x1 & -16777216) >> 24) & 0xFF] + $sbox1[($x1 & 0xFF0000) >> 16]) ^ $sbox2[($x1 & 0xFF00) >> 8]) + $sbox3[$x1 & 0xFF]) ^ $p[4]; + $x1^= $this->safe_intval(($this->safe_intval($sbox0[(($x0 & -16777216) >> 24) & 0xFF] + $sbox1[($x0 & 0xFF0000) >> 16]) ^ $sbox2[($x0 & 0xFF00) >> 8]) + $sbox3[$x0 & 0xFF]) ^ $p[5]; + $x0^= $this->safe_intval(($this->safe_intval($sbox0[(($x1 & -16777216) >> 24) & 0xFF] + $sbox1[($x1 & 0xFF0000) >> 16]) ^ $sbox2[($x1 & 0xFF00) >> 8]) + $sbox3[$x1 & 0xFF]) ^ $p[6]; + $x1^= $this->safe_intval(($this->safe_intval($sbox0[(($x0 & -16777216) >> 24) & 0xFF] + $sbox1[($x0 & 0xFF0000) >> 16]) ^ $sbox2[($x0 & 0xFF00) >> 8]) + $sbox3[$x0 & 0xFF]) ^ $p[7]; + $x0^= $this->safe_intval(($this->safe_intval($sbox0[(($x1 & -16777216) >> 24) & 0xFF] + $sbox1[($x1 & 0xFF0000) >> 16]) ^ $sbox2[($x1 & 0xFF00) >> 8]) + $sbox3[$x1 & 0xFF]) ^ $p[8]; + $x1^= $this->safe_intval(($this->safe_intval($sbox0[(($x0 & -16777216) >> 24) & 0xFF] + $sbox1[($x0 & 0xFF0000) >> 16]) ^ $sbox2[($x0 & 0xFF00) >> 8]) + $sbox3[$x0 & 0xFF]) ^ $p[9]; + $x0^= $this->safe_intval(($this->safe_intval($sbox0[(($x1 & -16777216) >> 24) & 0xFF] + $sbox1[($x1 & 0xFF0000) >> 16]) ^ $sbox2[($x1 & 0xFF00) >> 8]) + $sbox3[$x1 & 0xFF]) ^ $p[10]; + $x1^= $this->safe_intval(($this->safe_intval($sbox0[(($x0 & -16777216) >> 24) & 0xFF] + $sbox1[($x0 & 0xFF0000) >> 16]) ^ $sbox2[($x0 & 0xFF00) >> 8]) + $sbox3[$x0 & 0xFF]) ^ $p[11]; + $x0^= $this->safe_intval(($this->safe_intval($sbox0[(($x1 & -16777216) >> 24) & 0xFF] + $sbox1[($x1 & 0xFF0000) >> 16]) ^ $sbox2[($x1 & 0xFF00) >> 8]) + $sbox3[$x1 & 0xFF]) ^ $p[12]; + $x1^= $this->safe_intval(($this->safe_intval($sbox0[(($x0 & -16777216) >> 24) & 0xFF] + $sbox1[($x0 & 0xFF0000) >> 16]) ^ $sbox2[($x0 & 0xFF00) >> 8]) + $sbox3[$x0 & 0xFF]) ^ $p[13]; + $x0^= $this->safe_intval(($this->safe_intval($sbox0[(($x1 & -16777216) >> 24) & 0xFF] + $sbox1[($x1 & 0xFF0000) >> 16]) ^ $sbox2[($x1 & 0xFF00) >> 8]) + $sbox3[$x1 & 0xFF]) ^ $p[14]; + $x1^= $this->safe_intval(($this->safe_intval($sbox0[(($x0 & -16777216) >> 24) & 0xFF] + $sbox1[($x0 & 0xFF0000) >> 16]) ^ $sbox2[($x0 & 0xFF00) >> 8]) + $sbox3[$x0 & 0xFF]) ^ $p[15]; + $x0^= $this->safe_intval(($this->safe_intval($sbox0[(($x1 & -16777216) >> 24) & 0xFF] + $sbox1[($x1 & 0xFF0000) >> 16]) ^ $sbox2[($x1 & 0xFF00) >> 8]) + $sbox3[$x1 & 0xFF]) ^ $p[16]; + + return array($x1 ^ $p[17], $x0); + } + + /** + * Decrypts a block + * + * @access private + * @param string $in + * @return string + */ + function _decryptBlock($in) + { + $p = $this->bctx["p"]; + $sb_0 = $this->bctx["sb"][0]; + $sb_1 = $this->bctx["sb"][1]; + $sb_2 = $this->bctx["sb"][2]; + $sb_3 = $this->bctx["sb"][3]; + + $in = unpack("N*", $in); + $l = $in[1]; + $r = $in[2]; + + for ($i = 17; $i > 2; $i-= 2) { + $l^= $p[$i]; + $r^= $this->safe_intval(($this->safe_intval($sb_0[$l >> 24 & 0xff] + $sb_1[$l >> 16 & 0xff]) ^ + $sb_2[$l >> 8 & 0xff]) + + $sb_3[$l & 0xff]); + + $r^= $p[$i - 1]; + $l^= $this->safe_intval(($this->safe_intval($sb_0[$r >> 24 & 0xff] + $sb_1[$r >> 16 & 0xff]) ^ + $sb_2[$r >> 8 & 0xff]) + + $sb_3[$r & 0xff]); + } + return pack("N*", $r ^ $p[0], $l ^ $p[1]); + } + + /** + * Setup the performance-optimized function for de/encrypt() + * + * @see \phpseclib\Crypt\Base::_setupInlineCrypt() + * @access private + */ + function _setupInlineCrypt() + { + $lambda_functions =& self::_getLambdaFunctions(); + + // We create max. 10 hi-optimized code for memory reason. Means: For each $key one ultra fast inline-crypt function. + // (Currently, for Blowfish, one generated $lambda_function cost on php5.5@32bit ~100kb unfreeable mem and ~180kb on php5.5@64bit) + // After that, we'll still create very fast optimized code but not the hi-ultimative code, for each $mode one. + $gen_hi_opt_code = (bool)(count($lambda_functions) < 10); + + // Generation of a unique hash for our generated code + $code_hash = "Crypt_Blowfish, {$this->mode}"; + if ($gen_hi_opt_code) { + $code_hash = str_pad($code_hash, 32) . $this->_hashInlineCryptFunction($this->key); + } + + $safeint = $this->safe_intval_inline(); + + if (!isset($lambda_functions[$code_hash])) { + switch (true) { + case $gen_hi_opt_code: + $p = $this->bctx['p']; + $init_crypt = ' + static $sb_0, $sb_1, $sb_2, $sb_3; + if (!$sb_0) { + $sb_0 = $self->bctx["sb"][0]; + $sb_1 = $self->bctx["sb"][1]; + $sb_2 = $self->bctx["sb"][2]; + $sb_3 = $self->bctx["sb"][3]; + } + '; + break; + default: + $p = array(); + for ($i = 0; $i < 18; ++$i) { + $p[] = '$p_' . $i; + } + $init_crypt = ' + list($sb_0, $sb_1, $sb_2, $sb_3) = $self->bctx["sb"]; + list(' . implode(',', $p) . ') = $self->bctx["p"]; + + '; + } + + // Generating encrypt code: + $encrypt_block = ' + $in = unpack("N*", $in); + $l = $in[1]; + $r = $in[2]; + '; + for ($i = 0; $i < 16; $i+= 2) { + $encrypt_block.= ' + $l^= ' . $p[$i] . '; + $r^= ' . sprintf($safeint, '(' . sprintf($safeint, '$sb_0[$l >> 24 & 0xff] + $sb_1[$l >> 16 & 0xff]') . ' ^ + $sb_2[$l >> 8 & 0xff]) + + $sb_3[$l & 0xff]') . '; + + $r^= ' . $p[$i + 1] . '; + $l^= ' . sprintf($safeint, '(' . sprintf($safeint, '$sb_0[$r >> 24 & 0xff] + $sb_1[$r >> 16 & 0xff]') . ' ^ + $sb_2[$r >> 8 & 0xff]) + + $sb_3[$r & 0xff]') . '; + '; + } + $encrypt_block.= ' + $in = pack("N*", + $r ^ ' . $p[17] . ', + $l ^ ' . $p[16] . ' + ); + '; + + // Generating decrypt code: + $decrypt_block = ' + $in = unpack("N*", $in); + $l = $in[1]; + $r = $in[2]; + '; + + for ($i = 17; $i > 2; $i-= 2) { + $decrypt_block.= ' + $l^= ' . $p[$i] . '; + $r^= ' . sprintf($safeint, '(' . sprintf($safeint, '$sb_0[$l >> 24 & 0xff] + $sb_1[$l >> 16 & 0xff]') . ' ^ + $sb_2[$l >> 8 & 0xff]) + + $sb_3[$l & 0xff]') . '; + + $r^= ' . $p[$i - 1] . '; + $l^= ' . sprintf($safeint, '(' . sprintf($safeint, '$sb_0[$r >> 24 & 0xff] + $sb_1[$r >> 16 & 0xff]') . ' ^ + $sb_2[$r >> 8 & 0xff]) + + $sb_3[$r & 0xff]') . '; + '; + } + + $decrypt_block.= ' + $in = pack("N*", + $r ^ ' . $p[0] . ', + $l ^ ' . $p[1] . ' + ); + '; + + $lambda_functions[$code_hash] = $this->_createInlineCryptFunction( + array( + 'init_crypt' => $init_crypt, + 'init_encrypt' => '', + 'init_decrypt' => '', + 'encrypt_block' => $encrypt_block, + 'decrypt_block' => $decrypt_block + ) + ); + } + $this->inline_crypt = $lambda_functions[$code_hash]; + } +} diff --git a/3rdparty/phpseclib/phpseclib/phpseclib/Crypt/DES.php b/3rdparty/phpseclib/phpseclib/phpseclib/Crypt/DES.php new file mode 100644 index 00000000..26bd385f --- /dev/null +++ b/3rdparty/phpseclib/phpseclib/phpseclib/Crypt/DES.php @@ -0,0 +1,1449 @@ + + * setKey('abcdefgh'); + * + * $size = 10 * 1024; + * $plaintext = ''; + * for ($i = 0; $i < $size; $i++) { + * $plaintext.= 'a'; + * } + * + * echo $des->decrypt($des->encrypt($plaintext)); + * ?> + * + * + * @category Crypt + * @package DES + * @author Jim Wigginton + * @copyright 2007 Jim Wigginton + * @license http://www.opensource.org/licenses/mit-license.html MIT License + * @link http://phpseclib.sourceforge.net + */ + +namespace phpseclib\Crypt; + +/** + * Pure-PHP implementation of DES. + * + * @package DES + * @author Jim Wigginton + * @access public + */ +class DES extends Base +{ + /**#@+ + * @access private + * @see \phpseclib\Crypt\DES::_setupKey() + * @see \phpseclib\Crypt\DES::_processBlock() + */ + /** + * Contains $keys[self::ENCRYPT] + */ + const ENCRYPT = 0; + /** + * Contains $keys[self::DECRYPT] + */ + const DECRYPT = 1; + /**#@-*/ + + /** + * Block Length of the cipher + * + * @see \phpseclib\Crypt\Base::block_size + * @var int + * @access private + */ + var $block_size = 8; + + /** + * Key Length (in bytes) + * + * @see \phpseclib\Crypt\Base::setKeyLength() + * @var int + * @access private + */ + var $key_length = 8; + + /** + * The mcrypt specific name of the cipher + * + * @see \phpseclib\Crypt\Base::cipher_name_mcrypt + * @var string + * @access private + */ + var $cipher_name_mcrypt = 'des'; + + /** + * The OpenSSL names of the cipher / modes + * + * @see \phpseclib\Crypt\Base::openssl_mode_names + * @var array + * @access private + */ + var $openssl_mode_names = array( + self::MODE_ECB => 'des-ecb', + self::MODE_CBC => 'des-cbc', + self::MODE_CFB => 'des-cfb', + self::MODE_OFB => 'des-ofb' + // self::MODE_CTR is undefined for DES + ); + + /** + * Optimizing value while CFB-encrypting + * + * @see \phpseclib\Crypt\Base::cfb_init_len + * @var int + * @access private + */ + var $cfb_init_len = 500; + + /** + * Switch for DES/3DES encryption + * + * Used only if $engine == self::ENGINE_INTERNAL + * + * @see self::_setupKey() + * @see self::_processBlock() + * @var int + * @access private + */ + var $des_rounds = 1; + + /** + * max possible size of $key + * + * @see self::setKey() + * @var string + * @access private + */ + var $key_length_max = 8; + + /** + * The Key Schedule + * + * @see self::_setupKey() + * @var array + * @access private + */ + var $keys; + + /** + * Shuffle table. + * + * For each byte value index, the entry holds an 8-byte string + * with each byte containing all bits in the same state as the + * corresponding bit in the index value. + * + * @see self::_processBlock() + * @see self::_setupKey() + * @var array + * @access private + */ + var $shuffle = array( + "\x00\x00\x00\x00\x00\x00\x00\x00", "\x00\x00\x00\x00\x00\x00\x00\xFF", + "\x00\x00\x00\x00\x00\x00\xFF\x00", "\x00\x00\x00\x00\x00\x00\xFF\xFF", + "\x00\x00\x00\x00\x00\xFF\x00\x00", "\x00\x00\x00\x00\x00\xFF\x00\xFF", + "\x00\x00\x00\x00\x00\xFF\xFF\x00", "\x00\x00\x00\x00\x00\xFF\xFF\xFF", + "\x00\x00\x00\x00\xFF\x00\x00\x00", "\x00\x00\x00\x00\xFF\x00\x00\xFF", + "\x00\x00\x00\x00\xFF\x00\xFF\x00", "\x00\x00\x00\x00\xFF\x00\xFF\xFF", + "\x00\x00\x00\x00\xFF\xFF\x00\x00", "\x00\x00\x00\x00\xFF\xFF\x00\xFF", + "\x00\x00\x00\x00\xFF\xFF\xFF\x00", "\x00\x00\x00\x00\xFF\xFF\xFF\xFF", + "\x00\x00\x00\xFF\x00\x00\x00\x00", "\x00\x00\x00\xFF\x00\x00\x00\xFF", + "\x00\x00\x00\xFF\x00\x00\xFF\x00", "\x00\x00\x00\xFF\x00\x00\xFF\xFF", + "\x00\x00\x00\xFF\x00\xFF\x00\x00", "\x00\x00\x00\xFF\x00\xFF\x00\xFF", + "\x00\x00\x00\xFF\x00\xFF\xFF\x00", "\x00\x00\x00\xFF\x00\xFF\xFF\xFF", + "\x00\x00\x00\xFF\xFF\x00\x00\x00", "\x00\x00\x00\xFF\xFF\x00\x00\xFF", + "\x00\x00\x00\xFF\xFF\x00\xFF\x00", "\x00\x00\x00\xFF\xFF\x00\xFF\xFF", + "\x00\x00\x00\xFF\xFF\xFF\x00\x00", "\x00\x00\x00\xFF\xFF\xFF\x00\xFF", + "\x00\x00\x00\xFF\xFF\xFF\xFF\x00", "\x00\x00\x00\xFF\xFF\xFF\xFF\xFF", + "\x00\x00\xFF\x00\x00\x00\x00\x00", "\x00\x00\xFF\x00\x00\x00\x00\xFF", + "\x00\x00\xFF\x00\x00\x00\xFF\x00", "\x00\x00\xFF\x00\x00\x00\xFF\xFF", + "\x00\x00\xFF\x00\x00\xFF\x00\x00", "\x00\x00\xFF\x00\x00\xFF\x00\xFF", + "\x00\x00\xFF\x00\x00\xFF\xFF\x00", "\x00\x00\xFF\x00\x00\xFF\xFF\xFF", + "\x00\x00\xFF\x00\xFF\x00\x00\x00", "\x00\x00\xFF\x00\xFF\x00\x00\xFF", + "\x00\x00\xFF\x00\xFF\x00\xFF\x00", "\x00\x00\xFF\x00\xFF\x00\xFF\xFF", + "\x00\x00\xFF\x00\xFF\xFF\x00\x00", "\x00\x00\xFF\x00\xFF\xFF\x00\xFF", + "\x00\x00\xFF\x00\xFF\xFF\xFF\x00", "\x00\x00\xFF\x00\xFF\xFF\xFF\xFF", + "\x00\x00\xFF\xFF\x00\x00\x00\x00", "\x00\x00\xFF\xFF\x00\x00\x00\xFF", + "\x00\x00\xFF\xFF\x00\x00\xFF\x00", "\x00\x00\xFF\xFF\x00\x00\xFF\xFF", + "\x00\x00\xFF\xFF\x00\xFF\x00\x00", "\x00\x00\xFF\xFF\x00\xFF\x00\xFF", + "\x00\x00\xFF\xFF\x00\xFF\xFF\x00", "\x00\x00\xFF\xFF\x00\xFF\xFF\xFF", + "\x00\x00\xFF\xFF\xFF\x00\x00\x00", "\x00\x00\xFF\xFF\xFF\x00\x00\xFF", + "\x00\x00\xFF\xFF\xFF\x00\xFF\x00", "\x00\x00\xFF\xFF\xFF\x00\xFF\xFF", + "\x00\x00\xFF\xFF\xFF\xFF\x00\x00", "\x00\x00\xFF\xFF\xFF\xFF\x00\xFF", + "\x00\x00\xFF\xFF\xFF\xFF\xFF\x00", "\x00\x00\xFF\xFF\xFF\xFF\xFF\xFF", + "\x00\xFF\x00\x00\x00\x00\x00\x00", "\x00\xFF\x00\x00\x00\x00\x00\xFF", + "\x00\xFF\x00\x00\x00\x00\xFF\x00", "\x00\xFF\x00\x00\x00\x00\xFF\xFF", + "\x00\xFF\x00\x00\x00\xFF\x00\x00", "\x00\xFF\x00\x00\x00\xFF\x00\xFF", + "\x00\xFF\x00\x00\x00\xFF\xFF\x00", "\x00\xFF\x00\x00\x00\xFF\xFF\xFF", + "\x00\xFF\x00\x00\xFF\x00\x00\x00", "\x00\xFF\x00\x00\xFF\x00\x00\xFF", + "\x00\xFF\x00\x00\xFF\x00\xFF\x00", "\x00\xFF\x00\x00\xFF\x00\xFF\xFF", + "\x00\xFF\x00\x00\xFF\xFF\x00\x00", "\x00\xFF\x00\x00\xFF\xFF\x00\xFF", + "\x00\xFF\x00\x00\xFF\xFF\xFF\x00", "\x00\xFF\x00\x00\xFF\xFF\xFF\xFF", + "\x00\xFF\x00\xFF\x00\x00\x00\x00", "\x00\xFF\x00\xFF\x00\x00\x00\xFF", + "\x00\xFF\x00\xFF\x00\x00\xFF\x00", "\x00\xFF\x00\xFF\x00\x00\xFF\xFF", + "\x00\xFF\x00\xFF\x00\xFF\x00\x00", "\x00\xFF\x00\xFF\x00\xFF\x00\xFF", + "\x00\xFF\x00\xFF\x00\xFF\xFF\x00", "\x00\xFF\x00\xFF\x00\xFF\xFF\xFF", + "\x00\xFF\x00\xFF\xFF\x00\x00\x00", "\x00\xFF\x00\xFF\xFF\x00\x00\xFF", + "\x00\xFF\x00\xFF\xFF\x00\xFF\x00", "\x00\xFF\x00\xFF\xFF\x00\xFF\xFF", + "\x00\xFF\x00\xFF\xFF\xFF\x00\x00", "\x00\xFF\x00\xFF\xFF\xFF\x00\xFF", + "\x00\xFF\x00\xFF\xFF\xFF\xFF\x00", "\x00\xFF\x00\xFF\xFF\xFF\xFF\xFF", + "\x00\xFF\xFF\x00\x00\x00\x00\x00", "\x00\xFF\xFF\x00\x00\x00\x00\xFF", + "\x00\xFF\xFF\x00\x00\x00\xFF\x00", "\x00\xFF\xFF\x00\x00\x00\xFF\xFF", + "\x00\xFF\xFF\x00\x00\xFF\x00\x00", "\x00\xFF\xFF\x00\x00\xFF\x00\xFF", + "\x00\xFF\xFF\x00\x00\xFF\xFF\x00", "\x00\xFF\xFF\x00\x00\xFF\xFF\xFF", + "\x00\xFF\xFF\x00\xFF\x00\x00\x00", "\x00\xFF\xFF\x00\xFF\x00\x00\xFF", + "\x00\xFF\xFF\x00\xFF\x00\xFF\x00", "\x00\xFF\xFF\x00\xFF\x00\xFF\xFF", + "\x00\xFF\xFF\x00\xFF\xFF\x00\x00", "\x00\xFF\xFF\x00\xFF\xFF\x00\xFF", + "\x00\xFF\xFF\x00\xFF\xFF\xFF\x00", "\x00\xFF\xFF\x00\xFF\xFF\xFF\xFF", + "\x00\xFF\xFF\xFF\x00\x00\x00\x00", "\x00\xFF\xFF\xFF\x00\x00\x00\xFF", + "\x00\xFF\xFF\xFF\x00\x00\xFF\x00", "\x00\xFF\xFF\xFF\x00\x00\xFF\xFF", + "\x00\xFF\xFF\xFF\x00\xFF\x00\x00", "\x00\xFF\xFF\xFF\x00\xFF\x00\xFF", + "\x00\xFF\xFF\xFF\x00\xFF\xFF\x00", "\x00\xFF\xFF\xFF\x00\xFF\xFF\xFF", + "\x00\xFF\xFF\xFF\xFF\x00\x00\x00", "\x00\xFF\xFF\xFF\xFF\x00\x00\xFF", + "\x00\xFF\xFF\xFF\xFF\x00\xFF\x00", "\x00\xFF\xFF\xFF\xFF\x00\xFF\xFF", + "\x00\xFF\xFF\xFF\xFF\xFF\x00\x00", "\x00\xFF\xFF\xFF\xFF\xFF\x00\xFF", + "\x00\xFF\xFF\xFF\xFF\xFF\xFF\x00", "\x00\xFF\xFF\xFF\xFF\xFF\xFF\xFF", + "\xFF\x00\x00\x00\x00\x00\x00\x00", "\xFF\x00\x00\x00\x00\x00\x00\xFF", + "\xFF\x00\x00\x00\x00\x00\xFF\x00", "\xFF\x00\x00\x00\x00\x00\xFF\xFF", + "\xFF\x00\x00\x00\x00\xFF\x00\x00", "\xFF\x00\x00\x00\x00\xFF\x00\xFF", + "\xFF\x00\x00\x00\x00\xFF\xFF\x00", "\xFF\x00\x00\x00\x00\xFF\xFF\xFF", + "\xFF\x00\x00\x00\xFF\x00\x00\x00", "\xFF\x00\x00\x00\xFF\x00\x00\xFF", + "\xFF\x00\x00\x00\xFF\x00\xFF\x00", "\xFF\x00\x00\x00\xFF\x00\xFF\xFF", + "\xFF\x00\x00\x00\xFF\xFF\x00\x00", "\xFF\x00\x00\x00\xFF\xFF\x00\xFF", + "\xFF\x00\x00\x00\xFF\xFF\xFF\x00", "\xFF\x00\x00\x00\xFF\xFF\xFF\xFF", + "\xFF\x00\x00\xFF\x00\x00\x00\x00", "\xFF\x00\x00\xFF\x00\x00\x00\xFF", + "\xFF\x00\x00\xFF\x00\x00\xFF\x00", "\xFF\x00\x00\xFF\x00\x00\xFF\xFF", + "\xFF\x00\x00\xFF\x00\xFF\x00\x00", "\xFF\x00\x00\xFF\x00\xFF\x00\xFF", + "\xFF\x00\x00\xFF\x00\xFF\xFF\x00", "\xFF\x00\x00\xFF\x00\xFF\xFF\xFF", + "\xFF\x00\x00\xFF\xFF\x00\x00\x00", "\xFF\x00\x00\xFF\xFF\x00\x00\xFF", + "\xFF\x00\x00\xFF\xFF\x00\xFF\x00", "\xFF\x00\x00\xFF\xFF\x00\xFF\xFF", + "\xFF\x00\x00\xFF\xFF\xFF\x00\x00", "\xFF\x00\x00\xFF\xFF\xFF\x00\xFF", + "\xFF\x00\x00\xFF\xFF\xFF\xFF\x00", "\xFF\x00\x00\xFF\xFF\xFF\xFF\xFF", + "\xFF\x00\xFF\x00\x00\x00\x00\x00", "\xFF\x00\xFF\x00\x00\x00\x00\xFF", + "\xFF\x00\xFF\x00\x00\x00\xFF\x00", "\xFF\x00\xFF\x00\x00\x00\xFF\xFF", + "\xFF\x00\xFF\x00\x00\xFF\x00\x00", "\xFF\x00\xFF\x00\x00\xFF\x00\xFF", + "\xFF\x00\xFF\x00\x00\xFF\xFF\x00", "\xFF\x00\xFF\x00\x00\xFF\xFF\xFF", + "\xFF\x00\xFF\x00\xFF\x00\x00\x00", "\xFF\x00\xFF\x00\xFF\x00\x00\xFF", + "\xFF\x00\xFF\x00\xFF\x00\xFF\x00", "\xFF\x00\xFF\x00\xFF\x00\xFF\xFF", + "\xFF\x00\xFF\x00\xFF\xFF\x00\x00", "\xFF\x00\xFF\x00\xFF\xFF\x00\xFF", + "\xFF\x00\xFF\x00\xFF\xFF\xFF\x00", "\xFF\x00\xFF\x00\xFF\xFF\xFF\xFF", + "\xFF\x00\xFF\xFF\x00\x00\x00\x00", "\xFF\x00\xFF\xFF\x00\x00\x00\xFF", + "\xFF\x00\xFF\xFF\x00\x00\xFF\x00", "\xFF\x00\xFF\xFF\x00\x00\xFF\xFF", + "\xFF\x00\xFF\xFF\x00\xFF\x00\x00", "\xFF\x00\xFF\xFF\x00\xFF\x00\xFF", + "\xFF\x00\xFF\xFF\x00\xFF\xFF\x00", "\xFF\x00\xFF\xFF\x00\xFF\xFF\xFF", + "\xFF\x00\xFF\xFF\xFF\x00\x00\x00", "\xFF\x00\xFF\xFF\xFF\x00\x00\xFF", + "\xFF\x00\xFF\xFF\xFF\x00\xFF\x00", "\xFF\x00\xFF\xFF\xFF\x00\xFF\xFF", + "\xFF\x00\xFF\xFF\xFF\xFF\x00\x00", "\xFF\x00\xFF\xFF\xFF\xFF\x00\xFF", + "\xFF\x00\xFF\xFF\xFF\xFF\xFF\x00", "\xFF\x00\xFF\xFF\xFF\xFF\xFF\xFF", + "\xFF\xFF\x00\x00\x00\x00\x00\x00", "\xFF\xFF\x00\x00\x00\x00\x00\xFF", + "\xFF\xFF\x00\x00\x00\x00\xFF\x00", "\xFF\xFF\x00\x00\x00\x00\xFF\xFF", + "\xFF\xFF\x00\x00\x00\xFF\x00\x00", "\xFF\xFF\x00\x00\x00\xFF\x00\xFF", + "\xFF\xFF\x00\x00\x00\xFF\xFF\x00", "\xFF\xFF\x00\x00\x00\xFF\xFF\xFF", + "\xFF\xFF\x00\x00\xFF\x00\x00\x00", "\xFF\xFF\x00\x00\xFF\x00\x00\xFF", + "\xFF\xFF\x00\x00\xFF\x00\xFF\x00", "\xFF\xFF\x00\x00\xFF\x00\xFF\xFF", + "\xFF\xFF\x00\x00\xFF\xFF\x00\x00", "\xFF\xFF\x00\x00\xFF\xFF\x00\xFF", + "\xFF\xFF\x00\x00\xFF\xFF\xFF\x00", "\xFF\xFF\x00\x00\xFF\xFF\xFF\xFF", + "\xFF\xFF\x00\xFF\x00\x00\x00\x00", "\xFF\xFF\x00\xFF\x00\x00\x00\xFF", + "\xFF\xFF\x00\xFF\x00\x00\xFF\x00", "\xFF\xFF\x00\xFF\x00\x00\xFF\xFF", + "\xFF\xFF\x00\xFF\x00\xFF\x00\x00", "\xFF\xFF\x00\xFF\x00\xFF\x00\xFF", + "\xFF\xFF\x00\xFF\x00\xFF\xFF\x00", "\xFF\xFF\x00\xFF\x00\xFF\xFF\xFF", + "\xFF\xFF\x00\xFF\xFF\x00\x00\x00", "\xFF\xFF\x00\xFF\xFF\x00\x00\xFF", + "\xFF\xFF\x00\xFF\xFF\x00\xFF\x00", "\xFF\xFF\x00\xFF\xFF\x00\xFF\xFF", + "\xFF\xFF\x00\xFF\xFF\xFF\x00\x00", "\xFF\xFF\x00\xFF\xFF\xFF\x00\xFF", + "\xFF\xFF\x00\xFF\xFF\xFF\xFF\x00", "\xFF\xFF\x00\xFF\xFF\xFF\xFF\xFF", + "\xFF\xFF\xFF\x00\x00\x00\x00\x00", "\xFF\xFF\xFF\x00\x00\x00\x00\xFF", + "\xFF\xFF\xFF\x00\x00\x00\xFF\x00", "\xFF\xFF\xFF\x00\x00\x00\xFF\xFF", + "\xFF\xFF\xFF\x00\x00\xFF\x00\x00", "\xFF\xFF\xFF\x00\x00\xFF\x00\xFF", + "\xFF\xFF\xFF\x00\x00\xFF\xFF\x00", "\xFF\xFF\xFF\x00\x00\xFF\xFF\xFF", + "\xFF\xFF\xFF\x00\xFF\x00\x00\x00", "\xFF\xFF\xFF\x00\xFF\x00\x00\xFF", + "\xFF\xFF\xFF\x00\xFF\x00\xFF\x00", "\xFF\xFF\xFF\x00\xFF\x00\xFF\xFF", + "\xFF\xFF\xFF\x00\xFF\xFF\x00\x00", "\xFF\xFF\xFF\x00\xFF\xFF\x00\xFF", + "\xFF\xFF\xFF\x00\xFF\xFF\xFF\x00", "\xFF\xFF\xFF\x00\xFF\xFF\xFF\xFF", + "\xFF\xFF\xFF\xFF\x00\x00\x00\x00", "\xFF\xFF\xFF\xFF\x00\x00\x00\xFF", + "\xFF\xFF\xFF\xFF\x00\x00\xFF\x00", "\xFF\xFF\xFF\xFF\x00\x00\xFF\xFF", + "\xFF\xFF\xFF\xFF\x00\xFF\x00\x00", "\xFF\xFF\xFF\xFF\x00\xFF\x00\xFF", + "\xFF\xFF\xFF\xFF\x00\xFF\xFF\x00", "\xFF\xFF\xFF\xFF\x00\xFF\xFF\xFF", + "\xFF\xFF\xFF\xFF\xFF\x00\x00\x00", "\xFF\xFF\xFF\xFF\xFF\x00\x00\xFF", + "\xFF\xFF\xFF\xFF\xFF\x00\xFF\x00", "\xFF\xFF\xFF\xFF\xFF\x00\xFF\xFF", + "\xFF\xFF\xFF\xFF\xFF\xFF\x00\x00", "\xFF\xFF\xFF\xFF\xFF\xFF\x00\xFF", + "\xFF\xFF\xFF\xFF\xFF\xFF\xFF\x00", "\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF" + ); + + /** + * IP mapping helper table. + * + * Indexing this table with each source byte performs the initial bit permutation. + * + * @var array + * @access private + */ + var $ipmap = array( + 0x00, 0x10, 0x01, 0x11, 0x20, 0x30, 0x21, 0x31, + 0x02, 0x12, 0x03, 0x13, 0x22, 0x32, 0x23, 0x33, + 0x40, 0x50, 0x41, 0x51, 0x60, 0x70, 0x61, 0x71, + 0x42, 0x52, 0x43, 0x53, 0x62, 0x72, 0x63, 0x73, + 0x04, 0x14, 0x05, 0x15, 0x24, 0x34, 0x25, 0x35, + 0x06, 0x16, 0x07, 0x17, 0x26, 0x36, 0x27, 0x37, + 0x44, 0x54, 0x45, 0x55, 0x64, 0x74, 0x65, 0x75, + 0x46, 0x56, 0x47, 0x57, 0x66, 0x76, 0x67, 0x77, + 0x80, 0x90, 0x81, 0x91, 0xA0, 0xB0, 0xA1, 0xB1, + 0x82, 0x92, 0x83, 0x93, 0xA2, 0xB2, 0xA3, 0xB3, + 0xC0, 0xD0, 0xC1, 0xD1, 0xE0, 0xF0, 0xE1, 0xF1, + 0xC2, 0xD2, 0xC3, 0xD3, 0xE2, 0xF2, 0xE3, 0xF3, + 0x84, 0x94, 0x85, 0x95, 0xA4, 0xB4, 0xA5, 0xB5, + 0x86, 0x96, 0x87, 0x97, 0xA6, 0xB6, 0xA7, 0xB7, + 0xC4, 0xD4, 0xC5, 0xD5, 0xE4, 0xF4, 0xE5, 0xF5, + 0xC6, 0xD6, 0xC7, 0xD7, 0xE6, 0xF6, 0xE7, 0xF7, + 0x08, 0x18, 0x09, 0x19, 0x28, 0x38, 0x29, 0x39, + 0x0A, 0x1A, 0x0B, 0x1B, 0x2A, 0x3A, 0x2B, 0x3B, + 0x48, 0x58, 0x49, 0x59, 0x68, 0x78, 0x69, 0x79, + 0x4A, 0x5A, 0x4B, 0x5B, 0x6A, 0x7A, 0x6B, 0x7B, + 0x0C, 0x1C, 0x0D, 0x1D, 0x2C, 0x3C, 0x2D, 0x3D, + 0x0E, 0x1E, 0x0F, 0x1F, 0x2E, 0x3E, 0x2F, 0x3F, + 0x4C, 0x5C, 0x4D, 0x5D, 0x6C, 0x7C, 0x6D, 0x7D, + 0x4E, 0x5E, 0x4F, 0x5F, 0x6E, 0x7E, 0x6F, 0x7F, + 0x88, 0x98, 0x89, 0x99, 0xA8, 0xB8, 0xA9, 0xB9, + 0x8A, 0x9A, 0x8B, 0x9B, 0xAA, 0xBA, 0xAB, 0xBB, + 0xC8, 0xD8, 0xC9, 0xD9, 0xE8, 0xF8, 0xE9, 0xF9, + 0xCA, 0xDA, 0xCB, 0xDB, 0xEA, 0xFA, 0xEB, 0xFB, + 0x8C, 0x9C, 0x8D, 0x9D, 0xAC, 0xBC, 0xAD, 0xBD, + 0x8E, 0x9E, 0x8F, 0x9F, 0xAE, 0xBE, 0xAF, 0xBF, + 0xCC, 0xDC, 0xCD, 0xDD, 0xEC, 0xFC, 0xED, 0xFD, + 0xCE, 0xDE, 0xCF, 0xDF, 0xEE, 0xFE, 0xEF, 0xFF + ); + + /** + * Inverse IP mapping helper table. + * Indexing this table with a byte value reverses the bit order. + * + * @var array + * @access private + */ + var $invipmap = array( + 0x00, 0x80, 0x40, 0xC0, 0x20, 0xA0, 0x60, 0xE0, + 0x10, 0x90, 0x50, 0xD0, 0x30, 0xB0, 0x70, 0xF0, + 0x08, 0x88, 0x48, 0xC8, 0x28, 0xA8, 0x68, 0xE8, + 0x18, 0x98, 0x58, 0xD8, 0x38, 0xB8, 0x78, 0xF8, + 0x04, 0x84, 0x44, 0xC4, 0x24, 0xA4, 0x64, 0xE4, + 0x14, 0x94, 0x54, 0xD4, 0x34, 0xB4, 0x74, 0xF4, + 0x0C, 0x8C, 0x4C, 0xCC, 0x2C, 0xAC, 0x6C, 0xEC, + 0x1C, 0x9C, 0x5C, 0xDC, 0x3C, 0xBC, 0x7C, 0xFC, + 0x02, 0x82, 0x42, 0xC2, 0x22, 0xA2, 0x62, 0xE2, + 0x12, 0x92, 0x52, 0xD2, 0x32, 0xB2, 0x72, 0xF2, + 0x0A, 0x8A, 0x4A, 0xCA, 0x2A, 0xAA, 0x6A, 0xEA, + 0x1A, 0x9A, 0x5A, 0xDA, 0x3A, 0xBA, 0x7A, 0xFA, + 0x06, 0x86, 0x46, 0xC6, 0x26, 0xA6, 0x66, 0xE6, + 0x16, 0x96, 0x56, 0xD6, 0x36, 0xB6, 0x76, 0xF6, + 0x0E, 0x8E, 0x4E, 0xCE, 0x2E, 0xAE, 0x6E, 0xEE, + 0x1E, 0x9E, 0x5E, 0xDE, 0x3E, 0xBE, 0x7E, 0xFE, + 0x01, 0x81, 0x41, 0xC1, 0x21, 0xA1, 0x61, 0xE1, + 0x11, 0x91, 0x51, 0xD1, 0x31, 0xB1, 0x71, 0xF1, + 0x09, 0x89, 0x49, 0xC9, 0x29, 0xA9, 0x69, 0xE9, + 0x19, 0x99, 0x59, 0xD9, 0x39, 0xB9, 0x79, 0xF9, + 0x05, 0x85, 0x45, 0xC5, 0x25, 0xA5, 0x65, 0xE5, + 0x15, 0x95, 0x55, 0xD5, 0x35, 0xB5, 0x75, 0xF5, + 0x0D, 0x8D, 0x4D, 0xCD, 0x2D, 0xAD, 0x6D, 0xED, + 0x1D, 0x9D, 0x5D, 0xDD, 0x3D, 0xBD, 0x7D, 0xFD, + 0x03, 0x83, 0x43, 0xC3, 0x23, 0xA3, 0x63, 0xE3, + 0x13, 0x93, 0x53, 0xD3, 0x33, 0xB3, 0x73, 0xF3, + 0x0B, 0x8B, 0x4B, 0xCB, 0x2B, 0xAB, 0x6B, 0xEB, + 0x1B, 0x9B, 0x5B, 0xDB, 0x3B, 0xBB, 0x7B, 0xFB, + 0x07, 0x87, 0x47, 0xC7, 0x27, 0xA7, 0x67, 0xE7, + 0x17, 0x97, 0x57, 0xD7, 0x37, 0xB7, 0x77, 0xF7, + 0x0F, 0x8F, 0x4F, 0xCF, 0x2F, 0xAF, 0x6F, 0xEF, + 0x1F, 0x9F, 0x5F, 0xDF, 0x3F, 0xBF, 0x7F, 0xFF + ); + + /** + * Pre-permuted S-box1 + * + * Each box ($sbox1-$sbox8) has been vectorized, then each value pre-permuted using the + * P table: concatenation can then be replaced by exclusive ORs. + * + * @var array + * @access private + */ + var $sbox1 = array( + 0x00808200, 0x00000000, 0x00008000, 0x00808202, + 0x00808002, 0x00008202, 0x00000002, 0x00008000, + 0x00000200, 0x00808200, 0x00808202, 0x00000200, + 0x00800202, 0x00808002, 0x00800000, 0x00000002, + 0x00000202, 0x00800200, 0x00800200, 0x00008200, + 0x00008200, 0x00808000, 0x00808000, 0x00800202, + 0x00008002, 0x00800002, 0x00800002, 0x00008002, + 0x00000000, 0x00000202, 0x00008202, 0x00800000, + 0x00008000, 0x00808202, 0x00000002, 0x00808000, + 0x00808200, 0x00800000, 0x00800000, 0x00000200, + 0x00808002, 0x00008000, 0x00008200, 0x00800002, + 0x00000200, 0x00000002, 0x00800202, 0x00008202, + 0x00808202, 0x00008002, 0x00808000, 0x00800202, + 0x00800002, 0x00000202, 0x00008202, 0x00808200, + 0x00000202, 0x00800200, 0x00800200, 0x00000000, + 0x00008002, 0x00008200, 0x00000000, 0x00808002 + ); + + /** + * Pre-permuted S-box2 + * + * @var array + * @access private + */ + var $sbox2 = array( + 0x40084010, 0x40004000, 0x00004000, 0x00084010, + 0x00080000, 0x00000010, 0x40080010, 0x40004010, + 0x40000010, 0x40084010, 0x40084000, 0x40000000, + 0x40004000, 0x00080000, 0x00000010, 0x40080010, + 0x00084000, 0x00080010, 0x40004010, 0x00000000, + 0x40000000, 0x00004000, 0x00084010, 0x40080000, + 0x00080010, 0x40000010, 0x00000000, 0x00084000, + 0x00004010, 0x40084000, 0x40080000, 0x00004010, + 0x00000000, 0x00084010, 0x40080010, 0x00080000, + 0x40004010, 0x40080000, 0x40084000, 0x00004000, + 0x40080000, 0x40004000, 0x00000010, 0x40084010, + 0x00084010, 0x00000010, 0x00004000, 0x40000000, + 0x00004010, 0x40084000, 0x00080000, 0x40000010, + 0x00080010, 0x40004010, 0x40000010, 0x00080010, + 0x00084000, 0x00000000, 0x40004000, 0x00004010, + 0x40000000, 0x40080010, 0x40084010, 0x00084000 + ); + + /** + * Pre-permuted S-box3 + * + * @var array + * @access private + */ + var $sbox3 = array( + 0x00000104, 0x04010100, 0x00000000, 0x04010004, + 0x04000100, 0x00000000, 0x00010104, 0x04000100, + 0x00010004, 0x04000004, 0x04000004, 0x00010000, + 0x04010104, 0x00010004, 0x04010000, 0x00000104, + 0x04000000, 0x00000004, 0x04010100, 0x00000100, + 0x00010100, 0x04010000, 0x04010004, 0x00010104, + 0x04000104, 0x00010100, 0x00010000, 0x04000104, + 0x00000004, 0x04010104, 0x00000100, 0x04000000, + 0x04010100, 0x04000000, 0x00010004, 0x00000104, + 0x00010000, 0x04010100, 0x04000100, 0x00000000, + 0x00000100, 0x00010004, 0x04010104, 0x04000100, + 0x04000004, 0x00000100, 0x00000000, 0x04010004, + 0x04000104, 0x00010000, 0x04000000, 0x04010104, + 0x00000004, 0x00010104, 0x00010100, 0x04000004, + 0x04010000, 0x04000104, 0x00000104, 0x04010000, + 0x00010104, 0x00000004, 0x04010004, 0x00010100 + ); + + /** + * Pre-permuted S-box4 + * + * @var array + * @access private + */ + var $sbox4 = array( + 0x80401000, 0x80001040, 0x80001040, 0x00000040, + 0x00401040, 0x80400040, 0x80400000, 0x80001000, + 0x00000000, 0x00401000, 0x00401000, 0x80401040, + 0x80000040, 0x00000000, 0x00400040, 0x80400000, + 0x80000000, 0x00001000, 0x00400000, 0x80401000, + 0x00000040, 0x00400000, 0x80001000, 0x00001040, + 0x80400040, 0x80000000, 0x00001040, 0x00400040, + 0x00001000, 0x00401040, 0x80401040, 0x80000040, + 0x00400040, 0x80400000, 0x00401000, 0x80401040, + 0x80000040, 0x00000000, 0x00000000, 0x00401000, + 0x00001040, 0x00400040, 0x80400040, 0x80000000, + 0x80401000, 0x80001040, 0x80001040, 0x00000040, + 0x80401040, 0x80000040, 0x80000000, 0x00001000, + 0x80400000, 0x80001000, 0x00401040, 0x80400040, + 0x80001000, 0x00001040, 0x00400000, 0x80401000, + 0x00000040, 0x00400000, 0x00001000, 0x00401040 + ); + + /** + * Pre-permuted S-box5 + * + * @var array + * @access private + */ + var $sbox5 = array( + 0x00000080, 0x01040080, 0x01040000, 0x21000080, + 0x00040000, 0x00000080, 0x20000000, 0x01040000, + 0x20040080, 0x00040000, 0x01000080, 0x20040080, + 0x21000080, 0x21040000, 0x00040080, 0x20000000, + 0x01000000, 0x20040000, 0x20040000, 0x00000000, + 0x20000080, 0x21040080, 0x21040080, 0x01000080, + 0x21040000, 0x20000080, 0x00000000, 0x21000000, + 0x01040080, 0x01000000, 0x21000000, 0x00040080, + 0x00040000, 0x21000080, 0x00000080, 0x01000000, + 0x20000000, 0x01040000, 0x21000080, 0x20040080, + 0x01000080, 0x20000000, 0x21040000, 0x01040080, + 0x20040080, 0x00000080, 0x01000000, 0x21040000, + 0x21040080, 0x00040080, 0x21000000, 0x21040080, + 0x01040000, 0x00000000, 0x20040000, 0x21000000, + 0x00040080, 0x01000080, 0x20000080, 0x00040000, + 0x00000000, 0x20040000, 0x01040080, 0x20000080 + ); + + /** + * Pre-permuted S-box6 + * + * @var array + * @access private + */ + var $sbox6 = array( + 0x10000008, 0x10200000, 0x00002000, 0x10202008, + 0x10200000, 0x00000008, 0x10202008, 0x00200000, + 0x10002000, 0x00202008, 0x00200000, 0x10000008, + 0x00200008, 0x10002000, 0x10000000, 0x00002008, + 0x00000000, 0x00200008, 0x10002008, 0x00002000, + 0x00202000, 0x10002008, 0x00000008, 0x10200008, + 0x10200008, 0x00000000, 0x00202008, 0x10202000, + 0x00002008, 0x00202000, 0x10202000, 0x10000000, + 0x10002000, 0x00000008, 0x10200008, 0x00202000, + 0x10202008, 0x00200000, 0x00002008, 0x10000008, + 0x00200000, 0x10002000, 0x10000000, 0x00002008, + 0x10000008, 0x10202008, 0x00202000, 0x10200000, + 0x00202008, 0x10202000, 0x00000000, 0x10200008, + 0x00000008, 0x00002000, 0x10200000, 0x00202008, + 0x00002000, 0x00200008, 0x10002008, 0x00000000, + 0x10202000, 0x10000000, 0x00200008, 0x10002008 + ); + + /** + * Pre-permuted S-box7 + * + * @var array + * @access private + */ + var $sbox7 = array( + 0x00100000, 0x02100001, 0x02000401, 0x00000000, + 0x00000400, 0x02000401, 0x00100401, 0x02100400, + 0x02100401, 0x00100000, 0x00000000, 0x02000001, + 0x00000001, 0x02000000, 0x02100001, 0x00000401, + 0x02000400, 0x00100401, 0x00100001, 0x02000400, + 0x02000001, 0x02100000, 0x02100400, 0x00100001, + 0x02100000, 0x00000400, 0x00000401, 0x02100401, + 0x00100400, 0x00000001, 0x02000000, 0x00100400, + 0x02000000, 0x00100400, 0x00100000, 0x02000401, + 0x02000401, 0x02100001, 0x02100001, 0x00000001, + 0x00100001, 0x02000000, 0x02000400, 0x00100000, + 0x02100400, 0x00000401, 0x00100401, 0x02100400, + 0x00000401, 0x02000001, 0x02100401, 0x02100000, + 0x00100400, 0x00000000, 0x00000001, 0x02100401, + 0x00000000, 0x00100401, 0x02100000, 0x00000400, + 0x02000001, 0x02000400, 0x00000400, 0x00100001 + ); + + /** + * Pre-permuted S-box8 + * + * @var array + * @access private + */ + var $sbox8 = array( + 0x08000820, 0x00000800, 0x00020000, 0x08020820, + 0x08000000, 0x08000820, 0x00000020, 0x08000000, + 0x00020020, 0x08020000, 0x08020820, 0x00020800, + 0x08020800, 0x00020820, 0x00000800, 0x00000020, + 0x08020000, 0x08000020, 0x08000800, 0x00000820, + 0x00020800, 0x00020020, 0x08020020, 0x08020800, + 0x00000820, 0x00000000, 0x00000000, 0x08020020, + 0x08000020, 0x08000800, 0x00020820, 0x00020000, + 0x00020820, 0x00020000, 0x08020800, 0x00000800, + 0x00000020, 0x08020020, 0x00000800, 0x00020820, + 0x08000800, 0x00000020, 0x08000020, 0x08020000, + 0x08020020, 0x08000000, 0x00020000, 0x08000820, + 0x00000000, 0x08020820, 0x00020020, 0x08000020, + 0x08020000, 0x08000800, 0x08000820, 0x00000000, + 0x08020820, 0x00020800, 0x00020800, 0x00000820, + 0x00000820, 0x00020020, 0x08000000, 0x08020800 + ); + + /** + * Test for engine validity + * + * This is mainly just a wrapper to set things up for \phpseclib\Crypt\Base::isValidEngine() + * + * @see \phpseclib\Crypt\Base::isValidEngine() + * @param int $engine + * @access public + * @return bool + */ + function isValidEngine($engine) + { + if ($this->key_length_max == 8) { + if ($engine == self::ENGINE_OPENSSL) { + // quoting https://www.openssl.org/news/openssl-3.0-notes.html, OpenSSL 3.0.1 + // "Moved all variations of the EVP ciphers CAST5, BF, IDEA, SEED, RC2, RC4, RC5, and DES to the legacy provider" + // in theory openssl_get_cipher_methods() should catch this but, on GitHub Actions, at least, it does not + if (defined('OPENSSL_VERSION_TEXT') && version_compare(preg_replace('#OpenSSL (\d+\.\d+\.\d+) .*#', '$1', OPENSSL_VERSION_TEXT), '3.0.1', '>=')) { + return false; + } + $this->cipher_name_openssl_ecb = 'des-ecb'; + $this->cipher_name_openssl = 'des-' . $this->_openssl_translate_mode(); + } + } + + return parent::isValidEngine($engine); + } + + /** + * Sets the key. + * + * Keys can be of any length. DES, itself, uses 64-bit keys (eg. strlen($key) == 8), however, we + * only use the first eight, if $key has more then eight characters in it, and pad $key with the + * null byte if it is less then eight characters long. + * + * DES also requires that every eighth bit be a parity bit, however, we'll ignore that. + * + * If the key is not explicitly set, it'll be assumed to be all zero's. + * + * @see \phpseclib\Crypt\Base::setKey() + * @access public + * @param string $key + */ + function setKey($key) + { + // We check/cut here only up to max length of the key. + // Key padding to the proper length will be done in _setupKey() + if (strlen($key) > $this->key_length_max) { + $key = substr($key, 0, $this->key_length_max); + } + + // Sets the key + parent::setKey($key); + } + + /** + * Encrypts a block + * + * @see \phpseclib\Crypt\Base::_encryptBlock() + * @see \phpseclib\Crypt\Base::encrypt() + * @see self::encrypt() + * @access private + * @param string $in + * @return string + */ + function _encryptBlock($in) + { + return $this->_processBlock($in, self::ENCRYPT); + } + + /** + * Decrypts a block + * + * @see \phpseclib\Crypt\Base::_decryptBlock() + * @see \phpseclib\Crypt\Base::decrypt() + * @see self::decrypt() + * @access private + * @param string $in + * @return string + */ + function _decryptBlock($in) + { + return $this->_processBlock($in, self::DECRYPT); + } + + /** + * Encrypts or decrypts a 64-bit block + * + * $mode should be either self::ENCRYPT or self::DECRYPT. See + * {@link http://en.wikipedia.org/wiki/Image:Feistel.png Feistel.png} to get a general + * idea of what this function does. + * + * @see self::_encryptBlock() + * @see self::_decryptBlock() + * @access private + * @param string $block + * @param int $mode + * @return string + */ + function _processBlock($block, $mode) + { + static $sbox1, $sbox2, $sbox3, $sbox4, $sbox5, $sbox6, $sbox7, $sbox8, $shuffleip, $shuffleinvip; + if (!$sbox1) { + $sbox1 = array_map("intval", $this->sbox1); + $sbox2 = array_map("intval", $this->sbox2); + $sbox3 = array_map("intval", $this->sbox3); + $sbox4 = array_map("intval", $this->sbox4); + $sbox5 = array_map("intval", $this->sbox5); + $sbox6 = array_map("intval", $this->sbox6); + $sbox7 = array_map("intval", $this->sbox7); + $sbox8 = array_map("intval", $this->sbox8); + /* Merge $shuffle with $[inv]ipmap */ + for ($i = 0; $i < 256; ++$i) { + $shuffleip[] = $this->shuffle[$this->ipmap[$i]]; + $shuffleinvip[] = $this->shuffle[$this->invipmap[$i]]; + } + } + + $keys = $this->keys[$mode]; + $ki = -1; + + // Do the initial IP permutation. + $t = unpack('Nl/Nr', $block); + list($l, $r) = array($t['l'], $t['r']); + $block = ($shuffleip[ $r & 0xFF] & "\x80\x80\x80\x80\x80\x80\x80\x80") | + ($shuffleip[($r >> 8) & 0xFF] & "\x40\x40\x40\x40\x40\x40\x40\x40") | + ($shuffleip[($r >> 16) & 0xFF] & "\x20\x20\x20\x20\x20\x20\x20\x20") | + ($shuffleip[($r >> 24) & 0xFF] & "\x10\x10\x10\x10\x10\x10\x10\x10") | + ($shuffleip[ $l & 0xFF] & "\x08\x08\x08\x08\x08\x08\x08\x08") | + ($shuffleip[($l >> 8) & 0xFF] & "\x04\x04\x04\x04\x04\x04\x04\x04") | + ($shuffleip[($l >> 16) & 0xFF] & "\x02\x02\x02\x02\x02\x02\x02\x02") | + ($shuffleip[($l >> 24) & 0xFF] & "\x01\x01\x01\x01\x01\x01\x01\x01"); + + // Extract L0 and R0. + $t = unpack('Nl/Nr', $block); + list($l, $r) = array($t['l'], $t['r']); + + for ($des_round = 0; $des_round < $this->des_rounds; ++$des_round) { + // Perform the 16 steps. + for ($i = 0; $i < 16; $i++) { + // start of "the Feistel (F) function" - see the following URL: + // http://en.wikipedia.org/wiki/Image:Data_Encryption_Standard_InfoBox_Diagram.png + // Merge key schedule. + $b1 = (($r >> 3) & 0x1FFFFFFF) ^ ($r << 29) ^ $keys[++$ki]; + $b2 = (($r >> 31) & 0x00000001) ^ ($r << 1) ^ $keys[++$ki]; + + // S-box indexing. + $t = $sbox1[($b1 >> 24) & 0x3F] ^ $sbox2[($b2 >> 24) & 0x3F] ^ + $sbox3[($b1 >> 16) & 0x3F] ^ $sbox4[($b2 >> 16) & 0x3F] ^ + $sbox5[($b1 >> 8) & 0x3F] ^ $sbox6[($b2 >> 8) & 0x3F] ^ + $sbox7[ $b1 & 0x3F] ^ $sbox8[ $b2 & 0x3F] ^ $l; + // end of "the Feistel (F) function" + + $l = $r; + $r = $t; + } + + // Last step should not permute L & R. + $t = $l; + $l = $r; + $r = $t; + } + + // Perform the inverse IP permutation. + return ($shuffleinvip[($r >> 24) & 0xFF] & "\x80\x80\x80\x80\x80\x80\x80\x80") | + ($shuffleinvip[($l >> 24) & 0xFF] & "\x40\x40\x40\x40\x40\x40\x40\x40") | + ($shuffleinvip[($r >> 16) & 0xFF] & "\x20\x20\x20\x20\x20\x20\x20\x20") | + ($shuffleinvip[($l >> 16) & 0xFF] & "\x10\x10\x10\x10\x10\x10\x10\x10") | + ($shuffleinvip[($r >> 8) & 0xFF] & "\x08\x08\x08\x08\x08\x08\x08\x08") | + ($shuffleinvip[($l >> 8) & 0xFF] & "\x04\x04\x04\x04\x04\x04\x04\x04") | + ($shuffleinvip[ $r & 0xFF] & "\x02\x02\x02\x02\x02\x02\x02\x02") | + ($shuffleinvip[ $l & 0xFF] & "\x01\x01\x01\x01\x01\x01\x01\x01"); + } + + /** + * Creates the key schedule + * + * @see \phpseclib\Crypt\Base::_setupKey() + * @access private + */ + function _setupKey() + { + if (isset($this->kl['key']) && $this->key === $this->kl['key'] && $this->des_rounds === $this->kl['des_rounds']) { + // already expanded + return; + } + $this->kl = array('key' => $this->key, 'des_rounds' => $this->des_rounds); + + static $shifts = array( // number of key bits shifted per round + 1, 1, 2, 2, 2, 2, 2, 2, 1, 2, 2, 2, 2, 2, 2, 1 + ); + + static $pc1map = array( + 0x00, 0x00, 0x08, 0x08, 0x04, 0x04, 0x0C, 0x0C, + 0x02, 0x02, 0x0A, 0x0A, 0x06, 0x06, 0x0E, 0x0E, + 0x10, 0x10, 0x18, 0x18, 0x14, 0x14, 0x1C, 0x1C, + 0x12, 0x12, 0x1A, 0x1A, 0x16, 0x16, 0x1E, 0x1E, + 0x20, 0x20, 0x28, 0x28, 0x24, 0x24, 0x2C, 0x2C, + 0x22, 0x22, 0x2A, 0x2A, 0x26, 0x26, 0x2E, 0x2E, + 0x30, 0x30, 0x38, 0x38, 0x34, 0x34, 0x3C, 0x3C, + 0x32, 0x32, 0x3A, 0x3A, 0x36, 0x36, 0x3E, 0x3E, + 0x40, 0x40, 0x48, 0x48, 0x44, 0x44, 0x4C, 0x4C, + 0x42, 0x42, 0x4A, 0x4A, 0x46, 0x46, 0x4E, 0x4E, + 0x50, 0x50, 0x58, 0x58, 0x54, 0x54, 0x5C, 0x5C, + 0x52, 0x52, 0x5A, 0x5A, 0x56, 0x56, 0x5E, 0x5E, + 0x60, 0x60, 0x68, 0x68, 0x64, 0x64, 0x6C, 0x6C, + 0x62, 0x62, 0x6A, 0x6A, 0x66, 0x66, 0x6E, 0x6E, + 0x70, 0x70, 0x78, 0x78, 0x74, 0x74, 0x7C, 0x7C, + 0x72, 0x72, 0x7A, 0x7A, 0x76, 0x76, 0x7E, 0x7E, + 0x80, 0x80, 0x88, 0x88, 0x84, 0x84, 0x8C, 0x8C, + 0x82, 0x82, 0x8A, 0x8A, 0x86, 0x86, 0x8E, 0x8E, + 0x90, 0x90, 0x98, 0x98, 0x94, 0x94, 0x9C, 0x9C, + 0x92, 0x92, 0x9A, 0x9A, 0x96, 0x96, 0x9E, 0x9E, + 0xA0, 0xA0, 0xA8, 0xA8, 0xA4, 0xA4, 0xAC, 0xAC, + 0xA2, 0xA2, 0xAA, 0xAA, 0xA6, 0xA6, 0xAE, 0xAE, + 0xB0, 0xB0, 0xB8, 0xB8, 0xB4, 0xB4, 0xBC, 0xBC, + 0xB2, 0xB2, 0xBA, 0xBA, 0xB6, 0xB6, 0xBE, 0xBE, + 0xC0, 0xC0, 0xC8, 0xC8, 0xC4, 0xC4, 0xCC, 0xCC, + 0xC2, 0xC2, 0xCA, 0xCA, 0xC6, 0xC6, 0xCE, 0xCE, + 0xD0, 0xD0, 0xD8, 0xD8, 0xD4, 0xD4, 0xDC, 0xDC, + 0xD2, 0xD2, 0xDA, 0xDA, 0xD6, 0xD6, 0xDE, 0xDE, + 0xE0, 0xE0, 0xE8, 0xE8, 0xE4, 0xE4, 0xEC, 0xEC, + 0xE2, 0xE2, 0xEA, 0xEA, 0xE6, 0xE6, 0xEE, 0xEE, + 0xF0, 0xF0, 0xF8, 0xF8, 0xF4, 0xF4, 0xFC, 0xFC, + 0xF2, 0xF2, 0xFA, 0xFA, 0xF6, 0xF6, 0xFE, 0xFE + ); + + // Mapping tables for the PC-2 transformation. + static $pc2mapc1 = array( + 0x00000000, 0x00000400, 0x00200000, 0x00200400, + 0x00000001, 0x00000401, 0x00200001, 0x00200401, + 0x02000000, 0x02000400, 0x02200000, 0x02200400, + 0x02000001, 0x02000401, 0x02200001, 0x02200401 + ); + static $pc2mapc2 = array( + 0x00000000, 0x00000800, 0x08000000, 0x08000800, + 0x00010000, 0x00010800, 0x08010000, 0x08010800, + 0x00000000, 0x00000800, 0x08000000, 0x08000800, + 0x00010000, 0x00010800, 0x08010000, 0x08010800, + 0x00000100, 0x00000900, 0x08000100, 0x08000900, + 0x00010100, 0x00010900, 0x08010100, 0x08010900, + 0x00000100, 0x00000900, 0x08000100, 0x08000900, + 0x00010100, 0x00010900, 0x08010100, 0x08010900, + 0x00000010, 0x00000810, 0x08000010, 0x08000810, + 0x00010010, 0x00010810, 0x08010010, 0x08010810, + 0x00000010, 0x00000810, 0x08000010, 0x08000810, + 0x00010010, 0x00010810, 0x08010010, 0x08010810, + 0x00000110, 0x00000910, 0x08000110, 0x08000910, + 0x00010110, 0x00010910, 0x08010110, 0x08010910, + 0x00000110, 0x00000910, 0x08000110, 0x08000910, + 0x00010110, 0x00010910, 0x08010110, 0x08010910, + 0x00040000, 0x00040800, 0x08040000, 0x08040800, + 0x00050000, 0x00050800, 0x08050000, 0x08050800, + 0x00040000, 0x00040800, 0x08040000, 0x08040800, + 0x00050000, 0x00050800, 0x08050000, 0x08050800, + 0x00040100, 0x00040900, 0x08040100, 0x08040900, + 0x00050100, 0x00050900, 0x08050100, 0x08050900, + 0x00040100, 0x00040900, 0x08040100, 0x08040900, + 0x00050100, 0x00050900, 0x08050100, 0x08050900, + 0x00040010, 0x00040810, 0x08040010, 0x08040810, + 0x00050010, 0x00050810, 0x08050010, 0x08050810, + 0x00040010, 0x00040810, 0x08040010, 0x08040810, + 0x00050010, 0x00050810, 0x08050010, 0x08050810, + 0x00040110, 0x00040910, 0x08040110, 0x08040910, + 0x00050110, 0x00050910, 0x08050110, 0x08050910, + 0x00040110, 0x00040910, 0x08040110, 0x08040910, + 0x00050110, 0x00050910, 0x08050110, 0x08050910, + 0x01000000, 0x01000800, 0x09000000, 0x09000800, + 0x01010000, 0x01010800, 0x09010000, 0x09010800, + 0x01000000, 0x01000800, 0x09000000, 0x09000800, + 0x01010000, 0x01010800, 0x09010000, 0x09010800, + 0x01000100, 0x01000900, 0x09000100, 0x09000900, + 0x01010100, 0x01010900, 0x09010100, 0x09010900, + 0x01000100, 0x01000900, 0x09000100, 0x09000900, + 0x01010100, 0x01010900, 0x09010100, 0x09010900, + 0x01000010, 0x01000810, 0x09000010, 0x09000810, + 0x01010010, 0x01010810, 0x09010010, 0x09010810, + 0x01000010, 0x01000810, 0x09000010, 0x09000810, + 0x01010010, 0x01010810, 0x09010010, 0x09010810, + 0x01000110, 0x01000910, 0x09000110, 0x09000910, + 0x01010110, 0x01010910, 0x09010110, 0x09010910, + 0x01000110, 0x01000910, 0x09000110, 0x09000910, + 0x01010110, 0x01010910, 0x09010110, 0x09010910, + 0x01040000, 0x01040800, 0x09040000, 0x09040800, + 0x01050000, 0x01050800, 0x09050000, 0x09050800, + 0x01040000, 0x01040800, 0x09040000, 0x09040800, + 0x01050000, 0x01050800, 0x09050000, 0x09050800, + 0x01040100, 0x01040900, 0x09040100, 0x09040900, + 0x01050100, 0x01050900, 0x09050100, 0x09050900, + 0x01040100, 0x01040900, 0x09040100, 0x09040900, + 0x01050100, 0x01050900, 0x09050100, 0x09050900, + 0x01040010, 0x01040810, 0x09040010, 0x09040810, + 0x01050010, 0x01050810, 0x09050010, 0x09050810, + 0x01040010, 0x01040810, 0x09040010, 0x09040810, + 0x01050010, 0x01050810, 0x09050010, 0x09050810, + 0x01040110, 0x01040910, 0x09040110, 0x09040910, + 0x01050110, 0x01050910, 0x09050110, 0x09050910, + 0x01040110, 0x01040910, 0x09040110, 0x09040910, + 0x01050110, 0x01050910, 0x09050110, 0x09050910 + ); + static $pc2mapc3 = array( + 0x00000000, 0x00000004, 0x00001000, 0x00001004, + 0x00000000, 0x00000004, 0x00001000, 0x00001004, + 0x10000000, 0x10000004, 0x10001000, 0x10001004, + 0x10000000, 0x10000004, 0x10001000, 0x10001004, + 0x00000020, 0x00000024, 0x00001020, 0x00001024, + 0x00000020, 0x00000024, 0x00001020, 0x00001024, + 0x10000020, 0x10000024, 0x10001020, 0x10001024, + 0x10000020, 0x10000024, 0x10001020, 0x10001024, + 0x00080000, 0x00080004, 0x00081000, 0x00081004, + 0x00080000, 0x00080004, 0x00081000, 0x00081004, + 0x10080000, 0x10080004, 0x10081000, 0x10081004, + 0x10080000, 0x10080004, 0x10081000, 0x10081004, + 0x00080020, 0x00080024, 0x00081020, 0x00081024, + 0x00080020, 0x00080024, 0x00081020, 0x00081024, + 0x10080020, 0x10080024, 0x10081020, 0x10081024, + 0x10080020, 0x10080024, 0x10081020, 0x10081024, + 0x20000000, 0x20000004, 0x20001000, 0x20001004, + 0x20000000, 0x20000004, 0x20001000, 0x20001004, + 0x30000000, 0x30000004, 0x30001000, 0x30001004, + 0x30000000, 0x30000004, 0x30001000, 0x30001004, + 0x20000020, 0x20000024, 0x20001020, 0x20001024, + 0x20000020, 0x20000024, 0x20001020, 0x20001024, + 0x30000020, 0x30000024, 0x30001020, 0x30001024, + 0x30000020, 0x30000024, 0x30001020, 0x30001024, + 0x20080000, 0x20080004, 0x20081000, 0x20081004, + 0x20080000, 0x20080004, 0x20081000, 0x20081004, + 0x30080000, 0x30080004, 0x30081000, 0x30081004, + 0x30080000, 0x30080004, 0x30081000, 0x30081004, + 0x20080020, 0x20080024, 0x20081020, 0x20081024, + 0x20080020, 0x20080024, 0x20081020, 0x20081024, + 0x30080020, 0x30080024, 0x30081020, 0x30081024, + 0x30080020, 0x30080024, 0x30081020, 0x30081024, + 0x00000002, 0x00000006, 0x00001002, 0x00001006, + 0x00000002, 0x00000006, 0x00001002, 0x00001006, + 0x10000002, 0x10000006, 0x10001002, 0x10001006, + 0x10000002, 0x10000006, 0x10001002, 0x10001006, + 0x00000022, 0x00000026, 0x00001022, 0x00001026, + 0x00000022, 0x00000026, 0x00001022, 0x00001026, + 0x10000022, 0x10000026, 0x10001022, 0x10001026, + 0x10000022, 0x10000026, 0x10001022, 0x10001026, + 0x00080002, 0x00080006, 0x00081002, 0x00081006, + 0x00080002, 0x00080006, 0x00081002, 0x00081006, + 0x10080002, 0x10080006, 0x10081002, 0x10081006, + 0x10080002, 0x10080006, 0x10081002, 0x10081006, + 0x00080022, 0x00080026, 0x00081022, 0x00081026, + 0x00080022, 0x00080026, 0x00081022, 0x00081026, + 0x10080022, 0x10080026, 0x10081022, 0x10081026, + 0x10080022, 0x10080026, 0x10081022, 0x10081026, + 0x20000002, 0x20000006, 0x20001002, 0x20001006, + 0x20000002, 0x20000006, 0x20001002, 0x20001006, + 0x30000002, 0x30000006, 0x30001002, 0x30001006, + 0x30000002, 0x30000006, 0x30001002, 0x30001006, + 0x20000022, 0x20000026, 0x20001022, 0x20001026, + 0x20000022, 0x20000026, 0x20001022, 0x20001026, + 0x30000022, 0x30000026, 0x30001022, 0x30001026, + 0x30000022, 0x30000026, 0x30001022, 0x30001026, + 0x20080002, 0x20080006, 0x20081002, 0x20081006, + 0x20080002, 0x20080006, 0x20081002, 0x20081006, + 0x30080002, 0x30080006, 0x30081002, 0x30081006, + 0x30080002, 0x30080006, 0x30081002, 0x30081006, + 0x20080022, 0x20080026, 0x20081022, 0x20081026, + 0x20080022, 0x20080026, 0x20081022, 0x20081026, + 0x30080022, 0x30080026, 0x30081022, 0x30081026, + 0x30080022, 0x30080026, 0x30081022, 0x30081026 + ); + static $pc2mapc4 = array( + 0x00000000, 0x00100000, 0x00000008, 0x00100008, + 0x00000200, 0x00100200, 0x00000208, 0x00100208, + 0x00000000, 0x00100000, 0x00000008, 0x00100008, + 0x00000200, 0x00100200, 0x00000208, 0x00100208, + 0x04000000, 0x04100000, 0x04000008, 0x04100008, + 0x04000200, 0x04100200, 0x04000208, 0x04100208, + 0x04000000, 0x04100000, 0x04000008, 0x04100008, + 0x04000200, 0x04100200, 0x04000208, 0x04100208, + 0x00002000, 0x00102000, 0x00002008, 0x00102008, + 0x00002200, 0x00102200, 0x00002208, 0x00102208, + 0x00002000, 0x00102000, 0x00002008, 0x00102008, + 0x00002200, 0x00102200, 0x00002208, 0x00102208, + 0x04002000, 0x04102000, 0x04002008, 0x04102008, + 0x04002200, 0x04102200, 0x04002208, 0x04102208, + 0x04002000, 0x04102000, 0x04002008, 0x04102008, + 0x04002200, 0x04102200, 0x04002208, 0x04102208, + 0x00000000, 0x00100000, 0x00000008, 0x00100008, + 0x00000200, 0x00100200, 0x00000208, 0x00100208, + 0x00000000, 0x00100000, 0x00000008, 0x00100008, + 0x00000200, 0x00100200, 0x00000208, 0x00100208, + 0x04000000, 0x04100000, 0x04000008, 0x04100008, + 0x04000200, 0x04100200, 0x04000208, 0x04100208, + 0x04000000, 0x04100000, 0x04000008, 0x04100008, + 0x04000200, 0x04100200, 0x04000208, 0x04100208, + 0x00002000, 0x00102000, 0x00002008, 0x00102008, + 0x00002200, 0x00102200, 0x00002208, 0x00102208, + 0x00002000, 0x00102000, 0x00002008, 0x00102008, + 0x00002200, 0x00102200, 0x00002208, 0x00102208, + 0x04002000, 0x04102000, 0x04002008, 0x04102008, + 0x04002200, 0x04102200, 0x04002208, 0x04102208, + 0x04002000, 0x04102000, 0x04002008, 0x04102008, + 0x04002200, 0x04102200, 0x04002208, 0x04102208, + 0x00020000, 0x00120000, 0x00020008, 0x00120008, + 0x00020200, 0x00120200, 0x00020208, 0x00120208, + 0x00020000, 0x00120000, 0x00020008, 0x00120008, + 0x00020200, 0x00120200, 0x00020208, 0x00120208, + 0x04020000, 0x04120000, 0x04020008, 0x04120008, + 0x04020200, 0x04120200, 0x04020208, 0x04120208, + 0x04020000, 0x04120000, 0x04020008, 0x04120008, + 0x04020200, 0x04120200, 0x04020208, 0x04120208, + 0x00022000, 0x00122000, 0x00022008, 0x00122008, + 0x00022200, 0x00122200, 0x00022208, 0x00122208, + 0x00022000, 0x00122000, 0x00022008, 0x00122008, + 0x00022200, 0x00122200, 0x00022208, 0x00122208, + 0x04022000, 0x04122000, 0x04022008, 0x04122008, + 0x04022200, 0x04122200, 0x04022208, 0x04122208, + 0x04022000, 0x04122000, 0x04022008, 0x04122008, + 0x04022200, 0x04122200, 0x04022208, 0x04122208, + 0x00020000, 0x00120000, 0x00020008, 0x00120008, + 0x00020200, 0x00120200, 0x00020208, 0x00120208, + 0x00020000, 0x00120000, 0x00020008, 0x00120008, + 0x00020200, 0x00120200, 0x00020208, 0x00120208, + 0x04020000, 0x04120000, 0x04020008, 0x04120008, + 0x04020200, 0x04120200, 0x04020208, 0x04120208, + 0x04020000, 0x04120000, 0x04020008, 0x04120008, + 0x04020200, 0x04120200, 0x04020208, 0x04120208, + 0x00022000, 0x00122000, 0x00022008, 0x00122008, + 0x00022200, 0x00122200, 0x00022208, 0x00122208, + 0x00022000, 0x00122000, 0x00022008, 0x00122008, + 0x00022200, 0x00122200, 0x00022208, 0x00122208, + 0x04022000, 0x04122000, 0x04022008, 0x04122008, + 0x04022200, 0x04122200, 0x04022208, 0x04122208, + 0x04022000, 0x04122000, 0x04022008, 0x04122008, + 0x04022200, 0x04122200, 0x04022208, 0x04122208 + ); + static $pc2mapd1 = array( + 0x00000000, 0x00000001, 0x08000000, 0x08000001, + 0x00200000, 0x00200001, 0x08200000, 0x08200001, + 0x00000002, 0x00000003, 0x08000002, 0x08000003, + 0x00200002, 0x00200003, 0x08200002, 0x08200003 + ); + static $pc2mapd2 = array( + 0x00000000, 0x00100000, 0x00000800, 0x00100800, + 0x00000000, 0x00100000, 0x00000800, 0x00100800, + 0x04000000, 0x04100000, 0x04000800, 0x04100800, + 0x04000000, 0x04100000, 0x04000800, 0x04100800, + 0x00000004, 0x00100004, 0x00000804, 0x00100804, + 0x00000004, 0x00100004, 0x00000804, 0x00100804, + 0x04000004, 0x04100004, 0x04000804, 0x04100804, + 0x04000004, 0x04100004, 0x04000804, 0x04100804, + 0x00000000, 0x00100000, 0x00000800, 0x00100800, + 0x00000000, 0x00100000, 0x00000800, 0x00100800, + 0x04000000, 0x04100000, 0x04000800, 0x04100800, + 0x04000000, 0x04100000, 0x04000800, 0x04100800, + 0x00000004, 0x00100004, 0x00000804, 0x00100804, + 0x00000004, 0x00100004, 0x00000804, 0x00100804, + 0x04000004, 0x04100004, 0x04000804, 0x04100804, + 0x04000004, 0x04100004, 0x04000804, 0x04100804, + 0x00000200, 0x00100200, 0x00000A00, 0x00100A00, + 0x00000200, 0x00100200, 0x00000A00, 0x00100A00, + 0x04000200, 0x04100200, 0x04000A00, 0x04100A00, + 0x04000200, 0x04100200, 0x04000A00, 0x04100A00, + 0x00000204, 0x00100204, 0x00000A04, 0x00100A04, + 0x00000204, 0x00100204, 0x00000A04, 0x00100A04, + 0x04000204, 0x04100204, 0x04000A04, 0x04100A04, + 0x04000204, 0x04100204, 0x04000A04, 0x04100A04, + 0x00000200, 0x00100200, 0x00000A00, 0x00100A00, + 0x00000200, 0x00100200, 0x00000A00, 0x00100A00, + 0x04000200, 0x04100200, 0x04000A00, 0x04100A00, + 0x04000200, 0x04100200, 0x04000A00, 0x04100A00, + 0x00000204, 0x00100204, 0x00000A04, 0x00100A04, + 0x00000204, 0x00100204, 0x00000A04, 0x00100A04, + 0x04000204, 0x04100204, 0x04000A04, 0x04100A04, + 0x04000204, 0x04100204, 0x04000A04, 0x04100A04, + 0x00020000, 0x00120000, 0x00020800, 0x00120800, + 0x00020000, 0x00120000, 0x00020800, 0x00120800, + 0x04020000, 0x04120000, 0x04020800, 0x04120800, + 0x04020000, 0x04120000, 0x04020800, 0x04120800, + 0x00020004, 0x00120004, 0x00020804, 0x00120804, + 0x00020004, 0x00120004, 0x00020804, 0x00120804, + 0x04020004, 0x04120004, 0x04020804, 0x04120804, + 0x04020004, 0x04120004, 0x04020804, 0x04120804, + 0x00020000, 0x00120000, 0x00020800, 0x00120800, + 0x00020000, 0x00120000, 0x00020800, 0x00120800, + 0x04020000, 0x04120000, 0x04020800, 0x04120800, + 0x04020000, 0x04120000, 0x04020800, 0x04120800, + 0x00020004, 0x00120004, 0x00020804, 0x00120804, + 0x00020004, 0x00120004, 0x00020804, 0x00120804, + 0x04020004, 0x04120004, 0x04020804, 0x04120804, + 0x04020004, 0x04120004, 0x04020804, 0x04120804, + 0x00020200, 0x00120200, 0x00020A00, 0x00120A00, + 0x00020200, 0x00120200, 0x00020A00, 0x00120A00, + 0x04020200, 0x04120200, 0x04020A00, 0x04120A00, + 0x04020200, 0x04120200, 0x04020A00, 0x04120A00, + 0x00020204, 0x00120204, 0x00020A04, 0x00120A04, + 0x00020204, 0x00120204, 0x00020A04, 0x00120A04, + 0x04020204, 0x04120204, 0x04020A04, 0x04120A04, + 0x04020204, 0x04120204, 0x04020A04, 0x04120A04, + 0x00020200, 0x00120200, 0x00020A00, 0x00120A00, + 0x00020200, 0x00120200, 0x00020A00, 0x00120A00, + 0x04020200, 0x04120200, 0x04020A00, 0x04120A00, + 0x04020200, 0x04120200, 0x04020A00, 0x04120A00, + 0x00020204, 0x00120204, 0x00020A04, 0x00120A04, + 0x00020204, 0x00120204, 0x00020A04, 0x00120A04, + 0x04020204, 0x04120204, 0x04020A04, 0x04120A04, + 0x04020204, 0x04120204, 0x04020A04, 0x04120A04 + ); + static $pc2mapd3 = array( + 0x00000000, 0x00010000, 0x02000000, 0x02010000, + 0x00000020, 0x00010020, 0x02000020, 0x02010020, + 0x00040000, 0x00050000, 0x02040000, 0x02050000, + 0x00040020, 0x00050020, 0x02040020, 0x02050020, + 0x00002000, 0x00012000, 0x02002000, 0x02012000, + 0x00002020, 0x00012020, 0x02002020, 0x02012020, + 0x00042000, 0x00052000, 0x02042000, 0x02052000, + 0x00042020, 0x00052020, 0x02042020, 0x02052020, + 0x00000000, 0x00010000, 0x02000000, 0x02010000, + 0x00000020, 0x00010020, 0x02000020, 0x02010020, + 0x00040000, 0x00050000, 0x02040000, 0x02050000, + 0x00040020, 0x00050020, 0x02040020, 0x02050020, + 0x00002000, 0x00012000, 0x02002000, 0x02012000, + 0x00002020, 0x00012020, 0x02002020, 0x02012020, + 0x00042000, 0x00052000, 0x02042000, 0x02052000, + 0x00042020, 0x00052020, 0x02042020, 0x02052020, + 0x00000010, 0x00010010, 0x02000010, 0x02010010, + 0x00000030, 0x00010030, 0x02000030, 0x02010030, + 0x00040010, 0x00050010, 0x02040010, 0x02050010, + 0x00040030, 0x00050030, 0x02040030, 0x02050030, + 0x00002010, 0x00012010, 0x02002010, 0x02012010, + 0x00002030, 0x00012030, 0x02002030, 0x02012030, + 0x00042010, 0x00052010, 0x02042010, 0x02052010, + 0x00042030, 0x00052030, 0x02042030, 0x02052030, + 0x00000010, 0x00010010, 0x02000010, 0x02010010, + 0x00000030, 0x00010030, 0x02000030, 0x02010030, + 0x00040010, 0x00050010, 0x02040010, 0x02050010, + 0x00040030, 0x00050030, 0x02040030, 0x02050030, + 0x00002010, 0x00012010, 0x02002010, 0x02012010, + 0x00002030, 0x00012030, 0x02002030, 0x02012030, + 0x00042010, 0x00052010, 0x02042010, 0x02052010, + 0x00042030, 0x00052030, 0x02042030, 0x02052030, + 0x20000000, 0x20010000, 0x22000000, 0x22010000, + 0x20000020, 0x20010020, 0x22000020, 0x22010020, + 0x20040000, 0x20050000, 0x22040000, 0x22050000, + 0x20040020, 0x20050020, 0x22040020, 0x22050020, + 0x20002000, 0x20012000, 0x22002000, 0x22012000, + 0x20002020, 0x20012020, 0x22002020, 0x22012020, + 0x20042000, 0x20052000, 0x22042000, 0x22052000, + 0x20042020, 0x20052020, 0x22042020, 0x22052020, + 0x20000000, 0x20010000, 0x22000000, 0x22010000, + 0x20000020, 0x20010020, 0x22000020, 0x22010020, + 0x20040000, 0x20050000, 0x22040000, 0x22050000, + 0x20040020, 0x20050020, 0x22040020, 0x22050020, + 0x20002000, 0x20012000, 0x22002000, 0x22012000, + 0x20002020, 0x20012020, 0x22002020, 0x22012020, + 0x20042000, 0x20052000, 0x22042000, 0x22052000, + 0x20042020, 0x20052020, 0x22042020, 0x22052020, + 0x20000010, 0x20010010, 0x22000010, 0x22010010, + 0x20000030, 0x20010030, 0x22000030, 0x22010030, + 0x20040010, 0x20050010, 0x22040010, 0x22050010, + 0x20040030, 0x20050030, 0x22040030, 0x22050030, + 0x20002010, 0x20012010, 0x22002010, 0x22012010, + 0x20002030, 0x20012030, 0x22002030, 0x22012030, + 0x20042010, 0x20052010, 0x22042010, 0x22052010, + 0x20042030, 0x20052030, 0x22042030, 0x22052030, + 0x20000010, 0x20010010, 0x22000010, 0x22010010, + 0x20000030, 0x20010030, 0x22000030, 0x22010030, + 0x20040010, 0x20050010, 0x22040010, 0x22050010, + 0x20040030, 0x20050030, 0x22040030, 0x22050030, + 0x20002010, 0x20012010, 0x22002010, 0x22012010, + 0x20002030, 0x20012030, 0x22002030, 0x22012030, + 0x20042010, 0x20052010, 0x22042010, 0x22052010, + 0x20042030, 0x20052030, 0x22042030, 0x22052030 + ); + static $pc2mapd4 = array( + 0x00000000, 0x00000400, 0x01000000, 0x01000400, + 0x00000000, 0x00000400, 0x01000000, 0x01000400, + 0x00000100, 0x00000500, 0x01000100, 0x01000500, + 0x00000100, 0x00000500, 0x01000100, 0x01000500, + 0x10000000, 0x10000400, 0x11000000, 0x11000400, + 0x10000000, 0x10000400, 0x11000000, 0x11000400, + 0x10000100, 0x10000500, 0x11000100, 0x11000500, + 0x10000100, 0x10000500, 0x11000100, 0x11000500, + 0x00080000, 0x00080400, 0x01080000, 0x01080400, + 0x00080000, 0x00080400, 0x01080000, 0x01080400, + 0x00080100, 0x00080500, 0x01080100, 0x01080500, + 0x00080100, 0x00080500, 0x01080100, 0x01080500, + 0x10080000, 0x10080400, 0x11080000, 0x11080400, + 0x10080000, 0x10080400, 0x11080000, 0x11080400, + 0x10080100, 0x10080500, 0x11080100, 0x11080500, + 0x10080100, 0x10080500, 0x11080100, 0x11080500, + 0x00000008, 0x00000408, 0x01000008, 0x01000408, + 0x00000008, 0x00000408, 0x01000008, 0x01000408, + 0x00000108, 0x00000508, 0x01000108, 0x01000508, + 0x00000108, 0x00000508, 0x01000108, 0x01000508, + 0x10000008, 0x10000408, 0x11000008, 0x11000408, + 0x10000008, 0x10000408, 0x11000008, 0x11000408, + 0x10000108, 0x10000508, 0x11000108, 0x11000508, + 0x10000108, 0x10000508, 0x11000108, 0x11000508, + 0x00080008, 0x00080408, 0x01080008, 0x01080408, + 0x00080008, 0x00080408, 0x01080008, 0x01080408, + 0x00080108, 0x00080508, 0x01080108, 0x01080508, + 0x00080108, 0x00080508, 0x01080108, 0x01080508, + 0x10080008, 0x10080408, 0x11080008, 0x11080408, + 0x10080008, 0x10080408, 0x11080008, 0x11080408, + 0x10080108, 0x10080508, 0x11080108, 0x11080508, + 0x10080108, 0x10080508, 0x11080108, 0x11080508, + 0x00001000, 0x00001400, 0x01001000, 0x01001400, + 0x00001000, 0x00001400, 0x01001000, 0x01001400, + 0x00001100, 0x00001500, 0x01001100, 0x01001500, + 0x00001100, 0x00001500, 0x01001100, 0x01001500, + 0x10001000, 0x10001400, 0x11001000, 0x11001400, + 0x10001000, 0x10001400, 0x11001000, 0x11001400, + 0x10001100, 0x10001500, 0x11001100, 0x11001500, + 0x10001100, 0x10001500, 0x11001100, 0x11001500, + 0x00081000, 0x00081400, 0x01081000, 0x01081400, + 0x00081000, 0x00081400, 0x01081000, 0x01081400, + 0x00081100, 0x00081500, 0x01081100, 0x01081500, + 0x00081100, 0x00081500, 0x01081100, 0x01081500, + 0x10081000, 0x10081400, 0x11081000, 0x11081400, + 0x10081000, 0x10081400, 0x11081000, 0x11081400, + 0x10081100, 0x10081500, 0x11081100, 0x11081500, + 0x10081100, 0x10081500, 0x11081100, 0x11081500, + 0x00001008, 0x00001408, 0x01001008, 0x01001408, + 0x00001008, 0x00001408, 0x01001008, 0x01001408, + 0x00001108, 0x00001508, 0x01001108, 0x01001508, + 0x00001108, 0x00001508, 0x01001108, 0x01001508, + 0x10001008, 0x10001408, 0x11001008, 0x11001408, + 0x10001008, 0x10001408, 0x11001008, 0x11001408, + 0x10001108, 0x10001508, 0x11001108, 0x11001508, + 0x10001108, 0x10001508, 0x11001108, 0x11001508, + 0x00081008, 0x00081408, 0x01081008, 0x01081408, + 0x00081008, 0x00081408, 0x01081008, 0x01081408, + 0x00081108, 0x00081508, 0x01081108, 0x01081508, + 0x00081108, 0x00081508, 0x01081108, 0x01081508, + 0x10081008, 0x10081408, 0x11081008, 0x11081408, + 0x10081008, 0x10081408, 0x11081008, 0x11081408, + 0x10081108, 0x10081508, 0x11081108, 0x11081508, + 0x10081108, 0x10081508, 0x11081108, 0x11081508 + ); + + $keys = array(); + for ($des_round = 0; $des_round < $this->des_rounds; ++$des_round) { + // pad the key and remove extra characters as appropriate. + $key = str_pad(substr($this->key, $des_round * 8, 8), 8, "\0"); + + // Perform the PC/1 transformation and compute C and D. + $t = unpack('Nl/Nr', $key); + list($l, $r) = array($t['l'], $t['r']); + $key = ($this->shuffle[$pc1map[ $r & 0xFF]] & "\x80\x80\x80\x80\x80\x80\x80\x00") | + ($this->shuffle[$pc1map[($r >> 8) & 0xFF]] & "\x40\x40\x40\x40\x40\x40\x40\x00") | + ($this->shuffle[$pc1map[($r >> 16) & 0xFF]] & "\x20\x20\x20\x20\x20\x20\x20\x00") | + ($this->shuffle[$pc1map[($r >> 24) & 0xFF]] & "\x10\x10\x10\x10\x10\x10\x10\x00") | + ($this->shuffle[$pc1map[ $l & 0xFF]] & "\x08\x08\x08\x08\x08\x08\x08\x00") | + ($this->shuffle[$pc1map[($l >> 8) & 0xFF]] & "\x04\x04\x04\x04\x04\x04\x04\x00") | + ($this->shuffle[$pc1map[($l >> 16) & 0xFF]] & "\x02\x02\x02\x02\x02\x02\x02\x00") | + ($this->shuffle[$pc1map[($l >> 24) & 0xFF]] & "\x01\x01\x01\x01\x01\x01\x01\x00"); + $key = unpack('Nc/Nd', $key); + $c = ( $key['c'] >> 4) & 0x0FFFFFFF; + $d = (($key['d'] >> 4) & 0x0FFFFFF0) | ($key['c'] & 0x0F); + + $keys[$des_round] = array( + self::ENCRYPT => array(), + self::DECRYPT => array_fill(0, 32, 0) + ); + for ($i = 0, $ki = 31; $i < 16; ++$i, $ki-= 2) { + $c <<= $shifts[$i]; + $c = ($c | ($c >> 28)) & 0x0FFFFFFF; + $d <<= $shifts[$i]; + $d = ($d | ($d >> 28)) & 0x0FFFFFFF; + + // Perform the PC-2 transformation. + $cp = $pc2mapc1[ $c >> 24 ] | $pc2mapc2[($c >> 16) & 0xFF] | + $pc2mapc3[($c >> 8) & 0xFF] | $pc2mapc4[ $c & 0xFF]; + $dp = $pc2mapd1[ $d >> 24 ] | $pc2mapd2[($d >> 16) & 0xFF] | + $pc2mapd3[($d >> 8) & 0xFF] | $pc2mapd4[ $d & 0xFF]; + + // Reorder: odd bytes/even bytes. Push the result in key schedule. + $val1 = ( $cp & intval(0xFF000000)) | (($cp << 8) & 0x00FF0000) | + (($dp >> 16) & 0x0000FF00) | (($dp >> 8) & 0x000000FF); + $val2 = (($cp << 8) & intval(0xFF000000)) | (($cp << 16) & 0x00FF0000) | + (($dp >> 8) & 0x0000FF00) | ( $dp & 0x000000FF); + $keys[$des_round][self::ENCRYPT][ ] = $val1; + $keys[$des_round][self::DECRYPT][$ki - 1] = $val1; + $keys[$des_round][self::ENCRYPT][ ] = $val2; + $keys[$des_round][self::DECRYPT][$ki ] = $val2; + } + } + + switch ($this->des_rounds) { + case 3: // 3DES keys + $this->keys = array( + self::ENCRYPT => array_merge( + $keys[0][self::ENCRYPT], + $keys[1][self::DECRYPT], + $keys[2][self::ENCRYPT] + ), + self::DECRYPT => array_merge( + $keys[2][self::DECRYPT], + $keys[1][self::ENCRYPT], + $keys[0][self::DECRYPT] + ) + ); + break; + // case 1: // DES keys + default: + $this->keys = array( + self::ENCRYPT => $keys[0][self::ENCRYPT], + self::DECRYPT => $keys[0][self::DECRYPT] + ); + } + } + + /** + * Setup the performance-optimized function for de/encrypt() + * + * @see \phpseclib\Crypt\Base::_setupInlineCrypt() + * @access private + */ + function _setupInlineCrypt() + { + $lambda_functions =& self::_getLambdaFunctions(); + + // Engine configuration for: + // - DES ($des_rounds == 1) or + // - 3DES ($des_rounds == 3) + $des_rounds = $this->des_rounds; + + // We create max. 10 hi-optimized code for memory reason. Means: For each $key one ultra fast inline-crypt function. + // (Currently, for DES, one generated $lambda_function cost on php5.5@32bit ~135kb unfreeable mem and ~230kb on php5.5@64bit) + // (Currently, for TripleDES, one generated $lambda_function cost on php5.5@32bit ~240kb unfreeable mem and ~340kb on php5.5@64bit) + // After that, we'll still create very fast optimized code but not the hi-ultimative code, for each $mode one + $gen_hi_opt_code = (bool)( count($lambda_functions) < 10 ); + + // Generation of a unique hash for our generated code + $code_hash = "Crypt_DES, $des_rounds, {$this->mode}"; + if ($gen_hi_opt_code) { + // For hi-optimized code, we create for each combination of + // $mode, $des_rounds and $this->key its own encrypt/decrypt function. + // After max 10 hi-optimized functions, we create generic + // (still very fast.. but not ultra) functions for each $mode/$des_rounds + // Currently 2 * 5 generic functions will be then max. possible. + $code_hash = str_pad($code_hash, 32) . $this->_hashInlineCryptFunction($this->key); + } + + // Is there a re-usable $lambda_functions in there? If not, we have to create it. + if (!isset($lambda_functions[$code_hash])) { + // Init code for both, encrypt and decrypt. + $init_crypt = 'static $sbox1, $sbox2, $sbox3, $sbox4, $sbox5, $sbox6, $sbox7, $sbox8, $shuffleip, $shuffleinvip; + if (!$sbox1) { + $sbox1 = array_map("intval", $self->sbox1); + $sbox2 = array_map("intval", $self->sbox2); + $sbox3 = array_map("intval", $self->sbox3); + $sbox4 = array_map("intval", $self->sbox4); + $sbox5 = array_map("intval", $self->sbox5); + $sbox6 = array_map("intval", $self->sbox6); + $sbox7 = array_map("intval", $self->sbox7); + $sbox8 = array_map("intval", $self->sbox8);' + /* Merge $shuffle with $[inv]ipmap */ . ' + for ($i = 0; $i < 256; ++$i) { + $shuffleip[] = $self->shuffle[$self->ipmap[$i]]; + $shuffleinvip[] = $self->shuffle[$self->invipmap[$i]]; + } + } + '; + + switch (true) { + case $gen_hi_opt_code: + // In Hi-optimized code mode, we use our [3]DES key schedule as hardcoded integers. + // No futher initialisation of the $keys schedule is necessary. + // That is the extra performance boost. + $k = array( + self::ENCRYPT => $this->keys[self::ENCRYPT], + self::DECRYPT => $this->keys[self::DECRYPT] + ); + $init_encrypt = ''; + $init_decrypt = ''; + break; + default: + // In generic optimized code mode, we have to use, as the best compromise [currently], + // our key schedule as $ke/$kd arrays. (with hardcoded indexes...) + $k = array( + self::ENCRYPT => array(), + self::DECRYPT => array() + ); + for ($i = 0, $c = count($this->keys[self::ENCRYPT]); $i < $c; ++$i) { + $k[self::ENCRYPT][$i] = '$ke[' . $i . ']'; + $k[self::DECRYPT][$i] = '$kd[' . $i . ']'; + } + $init_encrypt = '$ke = $self->keys[$self::ENCRYPT];'; + $init_decrypt = '$kd = $self->keys[$self::DECRYPT];'; + break; + } + + // Creating code for en- and decryption. + $crypt_block = array(); + foreach (array(self::ENCRYPT, self::DECRYPT) as $c) { + /* Do the initial IP permutation. */ + $crypt_block[$c] = ' + $in = unpack("N*", $in); + $l = $in[1]; + $r = $in[2]; + $in = unpack("N*", + ($shuffleip[ $r & 0xFF] & "\x80\x80\x80\x80\x80\x80\x80\x80") | + ($shuffleip[($r >> 8) & 0xFF] & "\x40\x40\x40\x40\x40\x40\x40\x40") | + ($shuffleip[($r >> 16) & 0xFF] & "\x20\x20\x20\x20\x20\x20\x20\x20") | + ($shuffleip[($r >> 24) & 0xFF] & "\x10\x10\x10\x10\x10\x10\x10\x10") | + ($shuffleip[ $l & 0xFF] & "\x08\x08\x08\x08\x08\x08\x08\x08") | + ($shuffleip[($l >> 8) & 0xFF] & "\x04\x04\x04\x04\x04\x04\x04\x04") | + ($shuffleip[($l >> 16) & 0xFF] & "\x02\x02\x02\x02\x02\x02\x02\x02") | + ($shuffleip[($l >> 24) & 0xFF] & "\x01\x01\x01\x01\x01\x01\x01\x01") + ); + ' . /* Extract L0 and R0 */ ' + $l = $in[1]; + $r = $in[2]; + '; + + $l = '$l'; + $r = '$r'; + + // Perform DES or 3DES. + for ($ki = -1, $des_round = 0; $des_round < $des_rounds; ++$des_round) { + // Perform the 16 steps. + for ($i = 0; $i < 16; ++$i) { + // start of "the Feistel (F) function" - see the following URL: + // http://en.wikipedia.org/wiki/Image:Data_Encryption_Standard_InfoBox_Diagram.png + // Merge key schedule. + $crypt_block[$c].= ' + $b1 = ((' . $r . ' >> 3) & 0x1FFFFFFF) ^ (' . $r . ' << 29) ^ ' . $k[$c][++$ki] . '; + $b2 = ((' . $r . ' >> 31) & 0x00000001) ^ (' . $r . ' << 1) ^ ' . $k[$c][++$ki] . ';' . + /* S-box indexing. */ + $l . ' = $sbox1[($b1 >> 24) & 0x3F] ^ $sbox2[($b2 >> 24) & 0x3F] ^ + $sbox3[($b1 >> 16) & 0x3F] ^ $sbox4[($b2 >> 16) & 0x3F] ^ + $sbox5[($b1 >> 8) & 0x3F] ^ $sbox6[($b2 >> 8) & 0x3F] ^ + $sbox7[ $b1 & 0x3F] ^ $sbox8[ $b2 & 0x3F] ^ ' . $l . '; + '; + // end of "the Feistel (F) function" + + // swap L & R + list($l, $r) = array($r, $l); + } + list($l, $r) = array($r, $l); + } + + // Perform the inverse IP permutation. + $crypt_block[$c].= '$in = + ($shuffleinvip[($l >> 24) & 0xFF] & "\x80\x80\x80\x80\x80\x80\x80\x80") | + ($shuffleinvip[($r >> 24) & 0xFF] & "\x40\x40\x40\x40\x40\x40\x40\x40") | + ($shuffleinvip[($l >> 16) & 0xFF] & "\x20\x20\x20\x20\x20\x20\x20\x20") | + ($shuffleinvip[($r >> 16) & 0xFF] & "\x10\x10\x10\x10\x10\x10\x10\x10") | + ($shuffleinvip[($l >> 8) & 0xFF] & "\x08\x08\x08\x08\x08\x08\x08\x08") | + ($shuffleinvip[($r >> 8) & 0xFF] & "\x04\x04\x04\x04\x04\x04\x04\x04") | + ($shuffleinvip[ $l & 0xFF] & "\x02\x02\x02\x02\x02\x02\x02\x02") | + ($shuffleinvip[ $r & 0xFF] & "\x01\x01\x01\x01\x01\x01\x01\x01"); + '; + } + + // Creates the inline-crypt function + $lambda_functions[$code_hash] = $this->_createInlineCryptFunction( + array( + 'init_crypt' => $init_crypt, + 'init_encrypt' => $init_encrypt, + 'init_decrypt' => $init_decrypt, + 'encrypt_block' => $crypt_block[self::ENCRYPT], + 'decrypt_block' => $crypt_block[self::DECRYPT] + ) + ); + } + + // Set the inline-crypt function as callback in: $this->inline_crypt + $this->inline_crypt = $lambda_functions[$code_hash]; + } +} diff --git a/3rdparty/phpseclib/phpseclib/phpseclib/Crypt/Hash.php b/3rdparty/phpseclib/phpseclib/phpseclib/Crypt/Hash.php new file mode 100644 index 00000000..5e5d13d4 --- /dev/null +++ b/3rdparty/phpseclib/phpseclib/phpseclib/Crypt/Hash.php @@ -0,0 +1,893 @@ + + * setKey('abcdefg'); + * + * echo base64_encode($hash->hash('abcdefg')); + * ?> + * + * + * @category Crypt + * @package Hash + * @author Jim Wigginton + * @copyright 2007 Jim Wigginton + * @license http://www.opensource.org/licenses/mit-license.html MIT License + * @link http://phpseclib.sourceforge.net + */ + +namespace phpseclib\Crypt; + +use phpseclib\Math\BigInteger; + +/** + * Pure-PHP implementations of keyed-hash message authentication codes (HMACs) and various cryptographic hashing functions. + * + * @package Hash + * @author Jim Wigginton + * @access public + */ +class Hash +{ + /**#@+ + * @access private + * @see \phpseclib\Crypt\Hash::__construct() + */ + /** + * Toggles the internal implementation + */ + const MODE_INTERNAL = 1; + /** + * Toggles the mhash() implementation, which has been deprecated on PHP 5.3.0+. + */ + const MODE_MHASH = 2; + /** + * Toggles the hash() implementation, which works on PHP 5.1.2+. + */ + const MODE_HASH = 3; + /**#@-*/ + + /** + * Hash Parameter + * + * @see self::setHash() + * @var int + * @access private + */ + var $hashParam; + + /** + * Byte-length of compression blocks / key (Internal HMAC) + * + * @see self::setAlgorithm() + * @var int + * @access private + */ + var $b; + + /** + * Byte-length of hash output (Internal HMAC) + * + * @see self::setHash() + * @var int + * @access private + */ + var $l = false; + + /** + * Hash Algorithm + * + * @see self::setHash() + * @var string + * @access private + */ + var $hash; + + /** + * Key + * + * @see self::setKey() + * @var string + * @access private + */ + var $key = false; + + /** + * Computed Key + * + * @see self::_computeKey() + * @var string + * @access private + */ + var $computedKey = false; + + /** + * Outer XOR (Internal HMAC) + * + * @see self::setKey() + * @var string + * @access private + */ + var $opad; + + /** + * Inner XOR (Internal HMAC) + * + * @see self::setKey() + * @var string + * @access private + */ + var $ipad; + + /** + * Engine + * + * @see self::setHash() + * @var string + * @access private + */ + var $engine; + + /** + * Default Constructor. + * + * @param string $hash + * @return \phpseclib\Crypt\Hash + * @access public + */ + function __construct($hash = 'sha1') + { + if (!defined('CRYPT_HASH_MODE')) { + switch (true) { + case extension_loaded('hash'): + define('CRYPT_HASH_MODE', self::MODE_HASH); + break; + case extension_loaded('mhash'): + define('CRYPT_HASH_MODE', self::MODE_MHASH); + break; + default: + define('CRYPT_HASH_MODE', self::MODE_INTERNAL); + } + } + + $this->setHash($hash); + } + + /** + * Sets the key for HMACs + * + * Keys can be of any length. + * + * @access public + * @param string $key + */ + function setKey($key = false) + { + $this->key = $key; + $this->_computeKey(); + } + + /** + * Pre-compute the key used by the HMAC + * + * Quoting http://tools.ietf.org/html/rfc2104#section-2, "Applications that use keys longer than B bytes + * will first hash the key using H and then use the resultant L byte string as the actual key to HMAC." + * + * As documented in https://www.reddit.com/r/PHP/comments/9nct2l/symfonypolyfill_hash_pbkdf2_correct_fix_for/ + * when doing an HMAC multiple times it's faster to compute the hash once instead of computing it during + * every call + * + * @access private + */ + function _computeKey() + { + if ($this->key === false) { + $this->computedKey = false; + return; + } + + if (strlen($this->key) <= $this->b) { + $this->computedKey = $this->key; + return; + } + + switch ($this->engine) { + case self::MODE_MHASH: + $this->computedKey = mhash($this->hash, $this->key); + break; + case self::MODE_HASH: + $this->computedKey = hash($this->hash, $this->key, true); + break; + case self::MODE_INTERNAL: + $this->computedKey = call_user_func($this->hash, $this->key); + } + } + + /** + * Gets the hash function. + * + * As set by the constructor or by the setHash() method. + * + * @access public + * @return string + */ + function getHash() + { + return $this->hashParam; + } + + /** + * Sets the hash function. + * + * @access public + * @param string $hash + */ + function setHash($hash) + { + $this->hashParam = $hash = strtolower($hash); + switch ($hash) { + case 'md5-96': + case 'sha1-96': + case 'sha256-96': + case 'sha512-96': + $hash = substr($hash, 0, -3); + $this->l = 12; // 96 / 8 = 12 + break; + case 'md2': + case 'md5': + $this->l = 16; + break; + case 'sha1': + $this->l = 20; + break; + case 'sha256': + $this->l = 32; + break; + case 'sha384': + $this->l = 48; + break; + case 'sha512': + $this->l = 64; + } + + switch ($hash) { + case 'md2-96': + case 'md2': + $this->b = 16; + case 'md5-96': + case 'sha1-96': + case 'sha224-96': + case 'sha256-96': + case 'md2': + case 'md5': + case 'sha1': + case 'sha224': + case 'sha256': + $this->b = 64; + break; + default: + $this->b = 128; + } + + switch ($hash) { + case 'md2': + $this->engine = CRYPT_HASH_MODE == self::MODE_HASH && in_array('md2', hash_algos()) ? + self::MODE_HASH : self::MODE_INTERNAL; + break; + case 'sha384': + case 'sha512': + $this->engine = CRYPT_HASH_MODE == self::MODE_MHASH ? self::MODE_INTERNAL : CRYPT_HASH_MODE; + break; + default: + $this->engine = CRYPT_HASH_MODE; + } + + switch ($this->engine) { + case self::MODE_MHASH: + switch ($hash) { + case 'md5': + $this->hash = MHASH_MD5; + break; + case 'sha256': + $this->hash = MHASH_SHA256; + break; + case 'sha1': + default: + $this->hash = MHASH_SHA1; + } + $this->_computeKey(self::MODE_MHASH); + return; + case self::MODE_HASH: + switch ($hash) { + case 'md5': + $this->hash = 'md5'; + return; + case 'md2': + case 'sha256': + case 'sha384': + case 'sha512': + $this->hash = $hash; + return; + case 'sha1': + default: + $this->hash = 'sha1'; + } + $this->_computeKey(self::MODE_HASH); + return; + } + + switch ($hash) { + case 'md2': + $this->hash = array($this, '_md2'); + break; + case 'md5': + $this->hash = array($this, '_md5'); + break; + case 'sha256': + $this->hash = array($this, '_sha256'); + break; + case 'sha384': + case 'sha512': + $this->hash = array($this, '_sha512'); + break; + case 'sha1': + default: + $this->hash = array($this, '_sha1'); + } + + $this->ipad = str_repeat(chr(0x36), $this->b); + $this->opad = str_repeat(chr(0x5C), $this->b); + + $this->_computeKey(self::MODE_INTERNAL); + } + + /** + * Compute the HMAC. + * + * @access public + * @param string $text + * @return string + */ + function hash($text) + { + if (!empty($this->key) || is_string($this->key)) { + switch ($this->engine) { + case self::MODE_MHASH: + $output = mhash($this->hash, $text, $this->computedKey); + break; + case self::MODE_HASH: + $output = hash_hmac($this->hash, $text, $this->computedKey, true); + break; + case self::MODE_INTERNAL: + $key = str_pad($this->computedKey, $this->b, chr(0)); // step 1 + $temp = $this->ipad ^ $key; // step 2 + $temp .= $text; // step 3 + $temp = call_user_func($this->hash, $temp); // step 4 + $output = $this->opad ^ $key; // step 5 + $output.= $temp; // step 6 + $output = call_user_func($this->hash, $output); // step 7 + } + } else { + switch ($this->engine) { + case self::MODE_MHASH: + $output = mhash($this->hash, $text); + break; + case self::MODE_HASH: + $output = hash($this->hash, $text, true); + break; + case self::MODE_INTERNAL: + $output = call_user_func($this->hash, $text); + } + } + + return substr($output, 0, $this->l); + } + + /** + * Returns the hash length (in bytes) + * + * @access public + * @return int + */ + function getLength() + { + return $this->l; + } + + /** + * Wrapper for MD5 + * + * @access private + * @param string $m + */ + function _md5($m) + { + return pack('H*', md5($m)); + } + + /** + * Wrapper for SHA1 + * + * @access private + * @param string $m + */ + function _sha1($m) + { + return pack('H*', sha1($m)); + } + + /** + * Pure-PHP implementation of MD2 + * + * See {@link http://tools.ietf.org/html/rfc1319 RFC1319}. + * + * @access private + * @param string $m + */ + function _md2($m) + { + static $s = array( + 41, 46, 67, 201, 162, 216, 124, 1, 61, 54, 84, 161, 236, 240, 6, + 19, 98, 167, 5, 243, 192, 199, 115, 140, 152, 147, 43, 217, 188, + 76, 130, 202, 30, 155, 87, 60, 253, 212, 224, 22, 103, 66, 111, 24, + 138, 23, 229, 18, 190, 78, 196, 214, 218, 158, 222, 73, 160, 251, + 245, 142, 187, 47, 238, 122, 169, 104, 121, 145, 21, 178, 7, 63, + 148, 194, 16, 137, 11, 34, 95, 33, 128, 127, 93, 154, 90, 144, 50, + 39, 53, 62, 204, 231, 191, 247, 151, 3, 255, 25, 48, 179, 72, 165, + 181, 209, 215, 94, 146, 42, 172, 86, 170, 198, 79, 184, 56, 210, + 150, 164, 125, 182, 118, 252, 107, 226, 156, 116, 4, 241, 69, 157, + 112, 89, 100, 113, 135, 32, 134, 91, 207, 101, 230, 45, 168, 2, 27, + 96, 37, 173, 174, 176, 185, 246, 28, 70, 97, 105, 52, 64, 126, 15, + 85, 71, 163, 35, 221, 81, 175, 58, 195, 92, 249, 206, 186, 197, + 234, 38, 44, 83, 13, 110, 133, 40, 132, 9, 211, 223, 205, 244, 65, + 129, 77, 82, 106, 220, 55, 200, 108, 193, 171, 250, 36, 225, 123, + 8, 12, 189, 177, 74, 120, 136, 149, 139, 227, 99, 232, 109, 233, + 203, 213, 254, 59, 0, 29, 57, 242, 239, 183, 14, 102, 88, 208, 228, + 166, 119, 114, 248, 235, 117, 75, 10, 49, 68, 80, 180, 143, 237, + 31, 26, 219, 153, 141, 51, 159, 17, 131, 20 + ); + + // Step 1. Append Padding Bytes + $pad = 16 - (strlen($m) & 0xF); + $m.= str_repeat(chr($pad), $pad); + + $length = strlen($m); + + // Step 2. Append Checksum + $c = str_repeat(chr(0), 16); + $l = chr(0); + for ($i = 0; $i < $length; $i+= 16) { + for ($j = 0; $j < 16; $j++) { + // RFC1319 incorrectly states that C[j] should be set to S[c xor L] + //$c[$j] = chr($s[ord($m[$i + $j] ^ $l)]); + // per , however, C[j] should be set to S[c xor L] xor C[j] + $c[$j] = chr($s[ord($m[$i + $j] ^ $l)] ^ ord($c[$j])); + $l = $c[$j]; + } + } + $m.= $c; + + $length+= 16; + + // Step 3. Initialize MD Buffer + $x = str_repeat(chr(0), 48); + + // Step 4. Process Message in 16-Byte Blocks + for ($i = 0; $i < $length; $i+= 16) { + for ($j = 0; $j < 16; $j++) { + $x[$j + 16] = $m[$i + $j]; + $x[$j + 32] = $x[$j + 16] ^ $x[$j]; + } + $t = chr(0); + for ($j = 0; $j < 18; $j++) { + for ($k = 0; $k < 48; $k++) { + $x[$k] = $t = $x[$k] ^ chr($s[ord($t)]); + //$t = $x[$k] = $x[$k] ^ chr($s[ord($t)]); + } + $t = chr(ord($t) + $j); + } + } + + // Step 5. Output + return substr($x, 0, 16); + } + + /** + * Pure-PHP implementation of SHA256 + * + * See {@link http://en.wikipedia.org/wiki/SHA_hash_functions#SHA-256_.28a_SHA-2_variant.29_pseudocode SHA-256 (a SHA-2 variant) pseudocode - Wikipedia}. + * + * @access private + * @param string $m + */ + function _sha256($m) + { + if (extension_loaded('suhosin')) { + return pack('H*', sha256($m)); + } + + // Initialize variables + $hash = array( + 0x6a09e667, 0xbb67ae85, 0x3c6ef372, 0xa54ff53a, 0x510e527f, 0x9b05688c, 0x1f83d9ab, 0x5be0cd19 + ); + // Initialize table of round constants + // (first 32 bits of the fractional parts of the cube roots of the first 64 primes 2..311) + static $k = array( + 0x428a2f98, 0x71374491, 0xb5c0fbcf, 0xe9b5dba5, 0x3956c25b, 0x59f111f1, 0x923f82a4, 0xab1c5ed5, + 0xd807aa98, 0x12835b01, 0x243185be, 0x550c7dc3, 0x72be5d74, 0x80deb1fe, 0x9bdc06a7, 0xc19bf174, + 0xe49b69c1, 0xefbe4786, 0x0fc19dc6, 0x240ca1cc, 0x2de92c6f, 0x4a7484aa, 0x5cb0a9dc, 0x76f988da, + 0x983e5152, 0xa831c66d, 0xb00327c8, 0xbf597fc7, 0xc6e00bf3, 0xd5a79147, 0x06ca6351, 0x14292967, + 0x27b70a85, 0x2e1b2138, 0x4d2c6dfc, 0x53380d13, 0x650a7354, 0x766a0abb, 0x81c2c92e, 0x92722c85, + 0xa2bfe8a1, 0xa81a664b, 0xc24b8b70, 0xc76c51a3, 0xd192e819, 0xd6990624, 0xf40e3585, 0x106aa070, + 0x19a4c116, 0x1e376c08, 0x2748774c, 0x34b0bcb5, 0x391c0cb3, 0x4ed8aa4a, 0x5b9cca4f, 0x682e6ff3, + 0x748f82ee, 0x78a5636f, 0x84c87814, 0x8cc70208, 0x90befffa, 0xa4506ceb, 0xbef9a3f7, 0xc67178f2 + ); + + // Pre-processing + $length = strlen($m); + // to round to nearest 56 mod 64, we'll add 64 - (length + (64 - 56)) % 64 + $m.= str_repeat(chr(0), 64 - (($length + 8) & 0x3F)); + $m[$length] = chr(0x80); + // we don't support hashing strings 512MB long + $m.= pack('N2', 0, $length << 3); + + // Process the message in successive 512-bit chunks + $chunks = str_split($m, 64); + foreach ($chunks as $chunk) { + $w = array(); + for ($i = 0; $i < 16; $i++) { + extract(unpack('Ntemp', $this->_string_shift($chunk, 4))); + $w[] = $temp; + } + + // Extend the sixteen 32-bit words into sixty-four 32-bit words + for ($i = 16; $i < 64; $i++) { + // @codingStandardsIgnoreStart + $s0 = $this->_rightRotate($w[$i - 15], 7) ^ + $this->_rightRotate($w[$i - 15], 18) ^ + $this->_rightShift( $w[$i - 15], 3); + $s1 = $this->_rightRotate($w[$i - 2], 17) ^ + $this->_rightRotate($w[$i - 2], 19) ^ + $this->_rightShift( $w[$i - 2], 10); + // @codingStandardsIgnoreEnd + $w[$i] = $this->_add($w[$i - 16], $s0, $w[$i - 7], $s1); + } + + // Initialize hash value for this chunk + list($a, $b, $c, $d, $e, $f, $g, $h) = $hash; + + // Main loop + for ($i = 0; $i < 64; $i++) { + $s0 = $this->_rightRotate($a, 2) ^ + $this->_rightRotate($a, 13) ^ + $this->_rightRotate($a, 22); + $maj = ($a & $b) ^ + ($a & $c) ^ + ($b & $c); + $t2 = $this->_add($s0, $maj); + + $s1 = $this->_rightRotate($e, 6) ^ + $this->_rightRotate($e, 11) ^ + $this->_rightRotate($e, 25); + $ch = ($e & $f) ^ + ($this->_not($e) & $g); + $t1 = $this->_add($h, $s1, $ch, $k[$i], $w[$i]); + + $h = $g; + $g = $f; + $f = $e; + $e = $this->_add($d, $t1); + $d = $c; + $c = $b; + $b = $a; + $a = $this->_add($t1, $t2); + } + + // Add this chunk's hash to result so far + $hash = array( + $this->_add($hash[0], $a), + $this->_add($hash[1], $b), + $this->_add($hash[2], $c), + $this->_add($hash[3], $d), + $this->_add($hash[4], $e), + $this->_add($hash[5], $f), + $this->_add($hash[6], $g), + $this->_add($hash[7], $h) + ); + } + + // Produce the final hash value (big-endian) + return pack('N8', $hash[0], $hash[1], $hash[2], $hash[3], $hash[4], $hash[5], $hash[6], $hash[7]); + } + + /** + * Pure-PHP implementation of SHA384 and SHA512 + * + * @access private + * @param string $m + */ + function _sha512($m) + { + static $init384, $init512, $k; + + if (!isset($k)) { + // Initialize variables + $init384 = array( // initial values for SHA384 + 'cbbb9d5dc1059ed8', '629a292a367cd507', '9159015a3070dd17', '152fecd8f70e5939', + '67332667ffc00b31', '8eb44a8768581511', 'db0c2e0d64f98fa7', '47b5481dbefa4fa4' + ); + $init512 = array( // initial values for SHA512 + '6a09e667f3bcc908', 'bb67ae8584caa73b', '3c6ef372fe94f82b', 'a54ff53a5f1d36f1', + '510e527fade682d1', '9b05688c2b3e6c1f', '1f83d9abfb41bd6b', '5be0cd19137e2179' + ); + + for ($i = 0; $i < 8; $i++) { + $init384[$i] = new BigInteger($init384[$i], 16); + $init384[$i]->setPrecision(64); + $init512[$i] = new BigInteger($init512[$i], 16); + $init512[$i]->setPrecision(64); + } + + // Initialize table of round constants + // (first 64 bits of the fractional parts of the cube roots of the first 80 primes 2..409) + $k = array( + '428a2f98d728ae22', '7137449123ef65cd', 'b5c0fbcfec4d3b2f', 'e9b5dba58189dbbc', + '3956c25bf348b538', '59f111f1b605d019', '923f82a4af194f9b', 'ab1c5ed5da6d8118', + 'd807aa98a3030242', '12835b0145706fbe', '243185be4ee4b28c', '550c7dc3d5ffb4e2', + '72be5d74f27b896f', '80deb1fe3b1696b1', '9bdc06a725c71235', 'c19bf174cf692694', + 'e49b69c19ef14ad2', 'efbe4786384f25e3', '0fc19dc68b8cd5b5', '240ca1cc77ac9c65', + '2de92c6f592b0275', '4a7484aa6ea6e483', '5cb0a9dcbd41fbd4', '76f988da831153b5', + '983e5152ee66dfab', 'a831c66d2db43210', 'b00327c898fb213f', 'bf597fc7beef0ee4', + 'c6e00bf33da88fc2', 'd5a79147930aa725', '06ca6351e003826f', '142929670a0e6e70', + '27b70a8546d22ffc', '2e1b21385c26c926', '4d2c6dfc5ac42aed', '53380d139d95b3df', + '650a73548baf63de', '766a0abb3c77b2a8', '81c2c92e47edaee6', '92722c851482353b', + 'a2bfe8a14cf10364', 'a81a664bbc423001', 'c24b8b70d0f89791', 'c76c51a30654be30', + 'd192e819d6ef5218', 'd69906245565a910', 'f40e35855771202a', '106aa07032bbd1b8', + '19a4c116b8d2d0c8', '1e376c085141ab53', '2748774cdf8eeb99', '34b0bcb5e19b48a8', + '391c0cb3c5c95a63', '4ed8aa4ae3418acb', '5b9cca4f7763e373', '682e6ff3d6b2b8a3', + '748f82ee5defb2fc', '78a5636f43172f60', '84c87814a1f0ab72', '8cc702081a6439ec', + '90befffa23631e28', 'a4506cebde82bde9', 'bef9a3f7b2c67915', 'c67178f2e372532b', + 'ca273eceea26619c', 'd186b8c721c0c207', 'eada7dd6cde0eb1e', 'f57d4f7fee6ed178', + '06f067aa72176fba', '0a637dc5a2c898a6', '113f9804bef90dae', '1b710b35131c471b', + '28db77f523047d84', '32caab7b40c72493', '3c9ebe0a15c9bebc', '431d67c49c100d4c', + '4cc5d4becb3e42b6', '597f299cfc657e2a', '5fcb6fab3ad6faec', '6c44198c4a475817' + ); + + for ($i = 0; $i < 80; $i++) { + $k[$i] = new BigInteger($k[$i], 16); + } + } + + $hash = $this->l == 48 ? $init384 : $init512; + + // Pre-processing + $length = strlen($m); + // to round to nearest 112 mod 128, we'll add 128 - (length + (128 - 112)) % 128 + $m.= str_repeat(chr(0), 128 - (($length + 16) & 0x7F)); + $m[$length] = chr(0x80); + // we don't support hashing strings 512MB long + $m.= pack('N4', 0, 0, 0, $length << 3); + + // Process the message in successive 1024-bit chunks + $chunks = str_split($m, 128); + foreach ($chunks as $chunk) { + $w = array(); + for ($i = 0; $i < 16; $i++) { + $temp = new BigInteger($this->_string_shift($chunk, 8), 256); + $temp->setPrecision(64); + $w[] = $temp; + } + + // Extend the sixteen 32-bit words into eighty 32-bit words + for ($i = 16; $i < 80; $i++) { + $temp = array( + $w[$i - 15]->bitwise_rightRotate(1), + $w[$i - 15]->bitwise_rightRotate(8), + $w[$i - 15]->bitwise_rightShift(7) + ); + $s0 = $temp[0]->bitwise_xor($temp[1]); + $s0 = $s0->bitwise_xor($temp[2]); + $temp = array( + $w[$i - 2]->bitwise_rightRotate(19), + $w[$i - 2]->bitwise_rightRotate(61), + $w[$i - 2]->bitwise_rightShift(6) + ); + $s1 = $temp[0]->bitwise_xor($temp[1]); + $s1 = $s1->bitwise_xor($temp[2]); + $w[$i] = $w[$i - 16]->copy(); + $w[$i] = $w[$i]->add($s0); + $w[$i] = $w[$i]->add($w[$i - 7]); + $w[$i] = $w[$i]->add($s1); + } + + // Initialize hash value for this chunk + $a = $hash[0]->copy(); + $b = $hash[1]->copy(); + $c = $hash[2]->copy(); + $d = $hash[3]->copy(); + $e = $hash[4]->copy(); + $f = $hash[5]->copy(); + $g = $hash[6]->copy(); + $h = $hash[7]->copy(); + + // Main loop + for ($i = 0; $i < 80; $i++) { + $temp = array( + $a->bitwise_rightRotate(28), + $a->bitwise_rightRotate(34), + $a->bitwise_rightRotate(39) + ); + $s0 = $temp[0]->bitwise_xor($temp[1]); + $s0 = $s0->bitwise_xor($temp[2]); + $temp = array( + $a->bitwise_and($b), + $a->bitwise_and($c), + $b->bitwise_and($c) + ); + $maj = $temp[0]->bitwise_xor($temp[1]); + $maj = $maj->bitwise_xor($temp[2]); + $t2 = $s0->add($maj); + + $temp = array( + $e->bitwise_rightRotate(14), + $e->bitwise_rightRotate(18), + $e->bitwise_rightRotate(41) + ); + $s1 = $temp[0]->bitwise_xor($temp[1]); + $s1 = $s1->bitwise_xor($temp[2]); + $temp = array( + $e->bitwise_and($f), + $g->bitwise_and($e->bitwise_not()) + ); + $ch = $temp[0]->bitwise_xor($temp[1]); + $t1 = $h->add($s1); + $t1 = $t1->add($ch); + $t1 = $t1->add($k[$i]); + $t1 = $t1->add($w[$i]); + + $h = $g->copy(); + $g = $f->copy(); + $f = $e->copy(); + $e = $d->add($t1); + $d = $c->copy(); + $c = $b->copy(); + $b = $a->copy(); + $a = $t1->add($t2); + } + + // Add this chunk's hash to result so far + $hash = array( + $hash[0]->add($a), + $hash[1]->add($b), + $hash[2]->add($c), + $hash[3]->add($d), + $hash[4]->add($e), + $hash[5]->add($f), + $hash[6]->add($g), + $hash[7]->add($h) + ); + } + + // Produce the final hash value (big-endian) + // (\phpseclib\Crypt\Hash::hash() trims the output for hashes but not for HMACs. as such, we trim the output here) + $temp = $hash[0]->toBytes() . $hash[1]->toBytes() . $hash[2]->toBytes() . $hash[3]->toBytes() . + $hash[4]->toBytes() . $hash[5]->toBytes(); + if ($this->l != 48) { + $temp.= $hash[6]->toBytes() . $hash[7]->toBytes(); + } + + return $temp; + } + + /** + * Right Rotate + * + * @access private + * @param int $int + * @param int $amt + * @see self::_sha256() + * @return int + */ + function _rightRotate($int, $amt) + { + $invamt = 32 - $amt; + $mask = (1 << $invamt) - 1; + return (($int << $invamt) & 0xFFFFFFFF) | (($int >> $amt) & $mask); + } + + /** + * Right Shift + * + * @access private + * @param int $int + * @param int $amt + * @see self::_sha256() + * @return int + */ + function _rightShift($int, $amt) + { + $mask = (1 << (32 - $amt)) - 1; + return ($int >> $amt) & $mask; + } + + /** + * Not + * + * @access private + * @param int $int + * @see self::_sha256() + * @return int + */ + function _not($int) + { + return ~$int & 0xFFFFFFFF; + } + + /** + * Add + * + * _sha256() adds multiple unsigned 32-bit integers. Since PHP doesn't support unsigned integers and since the + * possibility of overflow exists, care has to be taken. BigInteger could be used but this should be faster. + * + * @return int + * @see self::_sha256() + * @access private + */ + function _add() + { + static $mod; + if (!isset($mod)) { + $mod = pow(2, 32); + } + + $result = 0; + $arguments = func_get_args(); + foreach ($arguments as $argument) { + $result+= $argument < 0 ? ($argument & 0x7FFFFFFF) + 0x80000000 : $argument; + } + + if (function_exists('php_uname') && is_string(php_uname('m')) && (php_uname('m') & "\xDF\xDF\xDF") != 'ARM') { + return fmod($result, $mod); + } + + return (fmod($result, 0x80000000) & 0x7FFFFFFF) | + ((fmod(floor($result / 0x80000000), 2) & 1) << 31); + } + + /** + * String Shift + * + * Inspired by array_shift + * + * @param string $string + * @param int $index + * @return string + * @access private + */ + function _string_shift(&$string, $index = 1) + { + $substr = substr($string, 0, $index); + $string = substr($string, $index); + return $substr; + } +} diff --git a/3rdparty/phpseclib/phpseclib/phpseclib/Crypt/RC2.php b/3rdparty/phpseclib/phpseclib/phpseclib/Crypt/RC2.php new file mode 100644 index 00000000..e0511b32 --- /dev/null +++ b/3rdparty/phpseclib/phpseclib/phpseclib/Crypt/RC2.php @@ -0,0 +1,694 @@ + + * setKey('abcdefgh'); + * + * $plaintext = str_repeat('a', 1024); + * + * echo $rc2->decrypt($rc2->encrypt($plaintext)); + * ?> + * + * + * @category Crypt + * @package RC2 + * @author Patrick Monnerat + * @license http://www.opensource.org/licenses/mit-license.html MIT License + * @link http://phpseclib.sourceforge.net + */ + +namespace phpseclib\Crypt; + +/** + * Pure-PHP implementation of RC2. + * + * @package RC2 + * @access public + */ +class RC2 extends Base +{ + /** + * Block Length of the cipher + * + * @see \phpseclib\Crypt\Base::block_size + * @var int + * @access private + */ + var $block_size = 8; + + /** + * The Key + * + * @see \phpseclib\Crypt\Base::key + * @see self::setKey() + * @var string + * @access private + */ + var $key; + + /** + * The Original (unpadded) Key + * + * @see \phpseclib\Crypt\Base::key + * @see self::setKey() + * @see self::encrypt() + * @see self::decrypt() + * @var string + * @access private + */ + var $orig_key = ''; + + /** + * Don't truncate / null pad key + * + * @see \phpseclib\Crypt\Base::_clearBuffers() + * @var bool + * @access private + */ + var $skip_key_adjustment = true; + + /** + * Key Length (in bytes) + * + * @see \phpseclib\Crypt\RC2::setKeyLength() + * @var int + * @access private + */ + var $key_length = 16; // = 128 bits + + /** + * The mcrypt specific name of the cipher + * + * @see \phpseclib\Crypt\Base::cipher_name_mcrypt + * @var string + * @access private + */ + var $cipher_name_mcrypt = 'rc2'; + + /** + * Optimizing value while CFB-encrypting + * + * @see \phpseclib\Crypt\Base::cfb_init_len + * @var int + * @access private + */ + var $cfb_init_len = 500; + + /** + * The key length in bits. + * + * @see self::setKeyLength() + * @see self::setKey() + * @var int + * @access private + * @internal Should be in range [1..1024]. + * @internal Changing this value after setting the key has no effect. + */ + var $default_key_length = 1024; + + /** + * The key length in bits. + * + * @see self::isValidEnine() + * @see self::setKey() + * @var int + * @access private + * @internal Should be in range [1..1024]. + */ + var $current_key_length; + + /** + * The Key Schedule + * + * @see self::_setupKey() + * @var array + * @access private + */ + var $keys; + + /** + * Key expansion randomization table. + * Twice the same 256-value sequence to save a modulus in key expansion. + * + * @see self::setKey() + * @var array + * @access private + */ + var $pitable = array( + 0xD9, 0x78, 0xF9, 0xC4, 0x19, 0xDD, 0xB5, 0xED, + 0x28, 0xE9, 0xFD, 0x79, 0x4A, 0xA0, 0xD8, 0x9D, + 0xC6, 0x7E, 0x37, 0x83, 0x2B, 0x76, 0x53, 0x8E, + 0x62, 0x4C, 0x64, 0x88, 0x44, 0x8B, 0xFB, 0xA2, + 0x17, 0x9A, 0x59, 0xF5, 0x87, 0xB3, 0x4F, 0x13, + 0x61, 0x45, 0x6D, 0x8D, 0x09, 0x81, 0x7D, 0x32, + 0xBD, 0x8F, 0x40, 0xEB, 0x86, 0xB7, 0x7B, 0x0B, + 0xF0, 0x95, 0x21, 0x22, 0x5C, 0x6B, 0x4E, 0x82, + 0x54, 0xD6, 0x65, 0x93, 0xCE, 0x60, 0xB2, 0x1C, + 0x73, 0x56, 0xC0, 0x14, 0xA7, 0x8C, 0xF1, 0xDC, + 0x12, 0x75, 0xCA, 0x1F, 0x3B, 0xBE, 0xE4, 0xD1, + 0x42, 0x3D, 0xD4, 0x30, 0xA3, 0x3C, 0xB6, 0x26, + 0x6F, 0xBF, 0x0E, 0xDA, 0x46, 0x69, 0x07, 0x57, + 0x27, 0xF2, 0x1D, 0x9B, 0xBC, 0x94, 0x43, 0x03, + 0xF8, 0x11, 0xC7, 0xF6, 0x90, 0xEF, 0x3E, 0xE7, + 0x06, 0xC3, 0xD5, 0x2F, 0xC8, 0x66, 0x1E, 0xD7, + 0x08, 0xE8, 0xEA, 0xDE, 0x80, 0x52, 0xEE, 0xF7, + 0x84, 0xAA, 0x72, 0xAC, 0x35, 0x4D, 0x6A, 0x2A, + 0x96, 0x1A, 0xD2, 0x71, 0x5A, 0x15, 0x49, 0x74, + 0x4B, 0x9F, 0xD0, 0x5E, 0x04, 0x18, 0xA4, 0xEC, + 0xC2, 0xE0, 0x41, 0x6E, 0x0F, 0x51, 0xCB, 0xCC, + 0x24, 0x91, 0xAF, 0x50, 0xA1, 0xF4, 0x70, 0x39, + 0x99, 0x7C, 0x3A, 0x85, 0x23, 0xB8, 0xB4, 0x7A, + 0xFC, 0x02, 0x36, 0x5B, 0x25, 0x55, 0x97, 0x31, + 0x2D, 0x5D, 0xFA, 0x98, 0xE3, 0x8A, 0x92, 0xAE, + 0x05, 0xDF, 0x29, 0x10, 0x67, 0x6C, 0xBA, 0xC9, + 0xD3, 0x00, 0xE6, 0xCF, 0xE1, 0x9E, 0xA8, 0x2C, + 0x63, 0x16, 0x01, 0x3F, 0x58, 0xE2, 0x89, 0xA9, + 0x0D, 0x38, 0x34, 0x1B, 0xAB, 0x33, 0xFF, 0xB0, + 0xBB, 0x48, 0x0C, 0x5F, 0xB9, 0xB1, 0xCD, 0x2E, + 0xC5, 0xF3, 0xDB, 0x47, 0xE5, 0xA5, 0x9C, 0x77, + 0x0A, 0xA6, 0x20, 0x68, 0xFE, 0x7F, 0xC1, 0xAD, + 0xD9, 0x78, 0xF9, 0xC4, 0x19, 0xDD, 0xB5, 0xED, + 0x28, 0xE9, 0xFD, 0x79, 0x4A, 0xA0, 0xD8, 0x9D, + 0xC6, 0x7E, 0x37, 0x83, 0x2B, 0x76, 0x53, 0x8E, + 0x62, 0x4C, 0x64, 0x88, 0x44, 0x8B, 0xFB, 0xA2, + 0x17, 0x9A, 0x59, 0xF5, 0x87, 0xB3, 0x4F, 0x13, + 0x61, 0x45, 0x6D, 0x8D, 0x09, 0x81, 0x7D, 0x32, + 0xBD, 0x8F, 0x40, 0xEB, 0x86, 0xB7, 0x7B, 0x0B, + 0xF0, 0x95, 0x21, 0x22, 0x5C, 0x6B, 0x4E, 0x82, + 0x54, 0xD6, 0x65, 0x93, 0xCE, 0x60, 0xB2, 0x1C, + 0x73, 0x56, 0xC0, 0x14, 0xA7, 0x8C, 0xF1, 0xDC, + 0x12, 0x75, 0xCA, 0x1F, 0x3B, 0xBE, 0xE4, 0xD1, + 0x42, 0x3D, 0xD4, 0x30, 0xA3, 0x3C, 0xB6, 0x26, + 0x6F, 0xBF, 0x0E, 0xDA, 0x46, 0x69, 0x07, 0x57, + 0x27, 0xF2, 0x1D, 0x9B, 0xBC, 0x94, 0x43, 0x03, + 0xF8, 0x11, 0xC7, 0xF6, 0x90, 0xEF, 0x3E, 0xE7, + 0x06, 0xC3, 0xD5, 0x2F, 0xC8, 0x66, 0x1E, 0xD7, + 0x08, 0xE8, 0xEA, 0xDE, 0x80, 0x52, 0xEE, 0xF7, + 0x84, 0xAA, 0x72, 0xAC, 0x35, 0x4D, 0x6A, 0x2A, + 0x96, 0x1A, 0xD2, 0x71, 0x5A, 0x15, 0x49, 0x74, + 0x4B, 0x9F, 0xD0, 0x5E, 0x04, 0x18, 0xA4, 0xEC, + 0xC2, 0xE0, 0x41, 0x6E, 0x0F, 0x51, 0xCB, 0xCC, + 0x24, 0x91, 0xAF, 0x50, 0xA1, 0xF4, 0x70, 0x39, + 0x99, 0x7C, 0x3A, 0x85, 0x23, 0xB8, 0xB4, 0x7A, + 0xFC, 0x02, 0x36, 0x5B, 0x25, 0x55, 0x97, 0x31, + 0x2D, 0x5D, 0xFA, 0x98, 0xE3, 0x8A, 0x92, 0xAE, + 0x05, 0xDF, 0x29, 0x10, 0x67, 0x6C, 0xBA, 0xC9, + 0xD3, 0x00, 0xE6, 0xCF, 0xE1, 0x9E, 0xA8, 0x2C, + 0x63, 0x16, 0x01, 0x3F, 0x58, 0xE2, 0x89, 0xA9, + 0x0D, 0x38, 0x34, 0x1B, 0xAB, 0x33, 0xFF, 0xB0, + 0xBB, 0x48, 0x0C, 0x5F, 0xB9, 0xB1, 0xCD, 0x2E, + 0xC5, 0xF3, 0xDB, 0x47, 0xE5, 0xA5, 0x9C, 0x77, + 0x0A, 0xA6, 0x20, 0x68, 0xFE, 0x7F, 0xC1, 0xAD + ); + + /** + * Inverse key expansion randomization table. + * + * @see self::setKey() + * @var array + * @access private + */ + var $invpitable = array( + 0xD1, 0xDA, 0xB9, 0x6F, 0x9C, 0xC8, 0x78, 0x66, + 0x80, 0x2C, 0xF8, 0x37, 0xEA, 0xE0, 0x62, 0xA4, + 0xCB, 0x71, 0x50, 0x27, 0x4B, 0x95, 0xD9, 0x20, + 0x9D, 0x04, 0x91, 0xE3, 0x47, 0x6A, 0x7E, 0x53, + 0xFA, 0x3A, 0x3B, 0xB4, 0xA8, 0xBC, 0x5F, 0x68, + 0x08, 0xCA, 0x8F, 0x14, 0xD7, 0xC0, 0xEF, 0x7B, + 0x5B, 0xBF, 0x2F, 0xE5, 0xE2, 0x8C, 0xBA, 0x12, + 0xE1, 0xAF, 0xB2, 0x54, 0x5D, 0x59, 0x76, 0xDB, + 0x32, 0xA2, 0x58, 0x6E, 0x1C, 0x29, 0x64, 0xF3, + 0xE9, 0x96, 0x0C, 0x98, 0x19, 0x8D, 0x3E, 0x26, + 0xAB, 0xA5, 0x85, 0x16, 0x40, 0xBD, 0x49, 0x67, + 0xDC, 0x22, 0x94, 0xBB, 0x3C, 0xC1, 0x9B, 0xEB, + 0x45, 0x28, 0x18, 0xD8, 0x1A, 0x42, 0x7D, 0xCC, + 0xFB, 0x65, 0x8E, 0x3D, 0xCD, 0x2A, 0xA3, 0x60, + 0xAE, 0x93, 0x8A, 0x48, 0x97, 0x51, 0x15, 0xF7, + 0x01, 0x0B, 0xB7, 0x36, 0xB1, 0x2E, 0x11, 0xFD, + 0x84, 0x2D, 0x3F, 0x13, 0x88, 0xB3, 0x34, 0x24, + 0x1B, 0xDE, 0xC5, 0x1D, 0x4D, 0x2B, 0x17, 0x31, + 0x74, 0xA9, 0xC6, 0x43, 0x6D, 0x39, 0x90, 0xBE, + 0xC3, 0xB0, 0x21, 0x6B, 0xF6, 0x0F, 0xD5, 0x99, + 0x0D, 0xAC, 0x1F, 0x5C, 0x9E, 0xF5, 0xF9, 0x4C, + 0xD6, 0xDF, 0x89, 0xE4, 0x8B, 0xFF, 0xC7, 0xAA, + 0xE7, 0xED, 0x46, 0x25, 0xB6, 0x06, 0x5E, 0x35, + 0xB5, 0xEC, 0xCE, 0xE8, 0x6C, 0x30, 0x55, 0x61, + 0x4A, 0xFE, 0xA0, 0x79, 0x03, 0xF0, 0x10, 0x72, + 0x7C, 0xCF, 0x52, 0xA6, 0xA7, 0xEE, 0x44, 0xD3, + 0x9A, 0x57, 0x92, 0xD0, 0x5A, 0x7A, 0x41, 0x7F, + 0x0E, 0x00, 0x63, 0xF2, 0x4F, 0x05, 0x83, 0xC9, + 0xA1, 0xD4, 0xDD, 0xC4, 0x56, 0xF4, 0xD2, 0x77, + 0x81, 0x09, 0x82, 0x33, 0x9F, 0x07, 0x86, 0x75, + 0x38, 0x4E, 0x69, 0xF1, 0xAD, 0x23, 0x73, 0x87, + 0x70, 0x02, 0xC2, 0x1E, 0xB8, 0x0A, 0xFC, 0xE6 + ); + + /** + * Test for engine validity + * + * This is mainly just a wrapper to set things up for \phpseclib\Crypt\Base::isValidEngine() + * + * @see \phpseclib\Crypt\Base::__construct() + * @param int $engine + * @access public + * @return bool + */ + function isValidEngine($engine) + { + switch ($engine) { + case self::ENGINE_OPENSSL: + // quoting https://www.openssl.org/news/openssl-3.0-notes.html, OpenSSL 3.0.1 + // "Moved all variations of the EVP ciphers CAST5, BF, IDEA, SEED, RC2, RC4, RC5, and DES to the legacy provider" + // in theory openssl_get_cipher_methods() should catch this but, on GitHub Actions, at least, it does not + if (defined('OPENSSL_VERSION_TEXT') && version_compare(preg_replace('#OpenSSL (\d+\.\d+\.\d+) .*#', '$1', OPENSSL_VERSION_TEXT), '3.0.1', '>=')) { + return false; + } + if ($this->current_key_length != 128 || strlen($this->orig_key) < 16) { + return false; + } + $this->cipher_name_openssl_ecb = 'rc2-ecb'; + $this->cipher_name_openssl = 'rc2-' . $this->_openssl_translate_mode(); + } + + return parent::isValidEngine($engine); + } + + /** + * Sets the key length. + * + * Valid key lengths are 8 to 1024. + * Calling this function after setting the key has no effect until the next + * \phpseclib\Crypt\RC2::setKey() call. + * + * @access public + * @param int $length in bits + */ + function setKeyLength($length) + { + if ($length < 8) { + $this->default_key_length = 1; + } elseif ($length > 1024) { + $this->default_key_length = 128; + } else { + $this->default_key_length = $length; + } + $this->current_key_length = $this->default_key_length; + + parent::setKeyLength($length); + } + + /** + * Returns the current key length + * + * @access public + * @return int + */ + function getKeyLength() + { + return $this->current_key_length; + } + + /** + * Sets the key. + * + * Keys can be of any length. RC2, itself, uses 8 to 1024 bit keys (eg. + * strlen($key) <= 128), however, we only use the first 128 bytes if $key + * has more then 128 bytes in it, and set $key to a single null byte if + * it is empty. + * + * If the key is not explicitly set, it'll be assumed to be a single + * null byte. + * + * @see \phpseclib\Crypt\Base::setKey() + * @access public + * @param string $key + * @param int $t1 optional Effective key length in bits. + */ + function setKey($key, $t1 = 0) + { + $this->orig_key = $key; + + if ($t1 <= 0) { + $t1 = $this->default_key_length; + } elseif ($t1 > 1024) { + $t1 = 1024; + } + $this->current_key_length = $t1; + // Key byte count should be 1..128. + $key = strlen($key) ? substr($key, 0, 128) : "\x00"; + $t = strlen($key); + + // The mcrypt RC2 implementation only supports effective key length + // of 1024 bits. It is however possible to handle effective key + // lengths in range 1..1024 by expanding the key and applying + // inverse pitable mapping to the first byte before submitting it + // to mcrypt. + + // Key expansion. + $l = array_values(unpack('C*', $key)); + $t8 = ($t1 + 7) >> 3; + $tm = 0xFF >> (8 * $t8 - $t1); + + // Expand key. + $pitable = $this->pitable; + for ($i = $t; $i < 128; $i++) { + $l[$i] = $pitable[$l[$i - 1] + $l[$i - $t]]; + } + $i = 128 - $t8; + $l[$i] = $pitable[$l[$i] & $tm]; + while ($i--) { + $l[$i] = $pitable[$l[$i + 1] ^ $l[$i + $t8]]; + } + + // Prepare the key for mcrypt. + $l[0] = $this->invpitable[$l[0]]; + array_unshift($l, 'C*'); + + parent::setKey(call_user_func_array('pack', $l)); + } + + /** + * Encrypts a message. + * + * Mostly a wrapper for \phpseclib\Crypt\Base::encrypt, with some additional OpenSSL handling code + * + * @see self::decrypt() + * @access public + * @param string $plaintext + * @return string $ciphertext + */ + function encrypt($plaintext) + { + if ($this->engine == self::ENGINE_OPENSSL) { + $temp = $this->key; + $this->key = $this->orig_key; + $result = parent::encrypt($plaintext); + $this->key = $temp; + return $result; + } + + return parent::encrypt($plaintext); + } + + /** + * Decrypts a message. + * + * Mostly a wrapper for \phpseclib\Crypt\Base::decrypt, with some additional OpenSSL handling code + * + * @see self::encrypt() + * @access public + * @param string $ciphertext + * @return string $plaintext + */ + function decrypt($ciphertext) + { + if ($this->engine == self::ENGINE_OPENSSL) { + $temp = $this->key; + $this->key = $this->orig_key; + $result = parent::decrypt($ciphertext); + $this->key = $temp; + return $result; + } + + return parent::decrypt($ciphertext); + } + + /** + * Encrypts a block + * + * @see \phpseclib\Crypt\Base::_encryptBlock() + * @see \phpseclib\Crypt\Base::encrypt() + * @access private + * @param string $in + * @return string + */ + function _encryptBlock($in) + { + list($r0, $r1, $r2, $r3) = array_values(unpack('v*', $in)); + $keys = $this->keys; + $limit = 20; + $actions = array($limit => 44, 44 => 64); + $j = 0; + + for (;;) { + // Mixing round. + $r0 = (($r0 + $keys[$j++] + ((($r1 ^ $r2) & $r3) ^ $r1)) & 0xFFFF) << 1; + $r0 |= $r0 >> 16; + $r1 = (($r1 + $keys[$j++] + ((($r2 ^ $r3) & $r0) ^ $r2)) & 0xFFFF) << 2; + $r1 |= $r1 >> 16; + $r2 = (($r2 + $keys[$j++] + ((($r3 ^ $r0) & $r1) ^ $r3)) & 0xFFFF) << 3; + $r2 |= $r2 >> 16; + $r3 = (($r3 + $keys[$j++] + ((($r0 ^ $r1) & $r2) ^ $r0)) & 0xFFFF) << 5; + $r3 |= $r3 >> 16; + + if ($j === $limit) { + if ($limit === 64) { + break; + } + + // Mashing round. + $r0 += $keys[$r3 & 0x3F]; + $r1 += $keys[$r0 & 0x3F]; + $r2 += $keys[$r1 & 0x3F]; + $r3 += $keys[$r2 & 0x3F]; + $limit = $actions[$limit]; + } + } + + return pack('vvvv', $r0, $r1, $r2, $r3); + } + + /** + * Decrypts a block + * + * @see \phpseclib\Crypt\Base::_decryptBlock() + * @see \phpseclib\Crypt\Base::decrypt() + * @access private + * @param string $in + * @return string + */ + function _decryptBlock($in) + { + list($r0, $r1, $r2, $r3) = array_values(unpack('v*', $in)); + $keys = $this->keys; + $limit = 44; + $actions = array($limit => 20, 20 => 0); + $j = 64; + + for (;;) { + // R-mixing round. + $r3 = ($r3 | ($r3 << 16)) >> 5; + $r3 = ($r3 - $keys[--$j] - ((($r0 ^ $r1) & $r2) ^ $r0)) & 0xFFFF; + $r2 = ($r2 | ($r2 << 16)) >> 3; + $r2 = ($r2 - $keys[--$j] - ((($r3 ^ $r0) & $r1) ^ $r3)) & 0xFFFF; + $r1 = ($r1 | ($r1 << 16)) >> 2; + $r1 = ($r1 - $keys[--$j] - ((($r2 ^ $r3) & $r0) ^ $r2)) & 0xFFFF; + $r0 = ($r0 | ($r0 << 16)) >> 1; + $r0 = ($r0 - $keys[--$j] - ((($r1 ^ $r2) & $r3) ^ $r1)) & 0xFFFF; + + if ($j === $limit) { + if ($limit === 0) { + break; + } + + // R-mashing round. + $r3 = ($r3 - $keys[$r2 & 0x3F]) & 0xFFFF; + $r2 = ($r2 - $keys[$r1 & 0x3F]) & 0xFFFF; + $r1 = ($r1 - $keys[$r0 & 0x3F]) & 0xFFFF; + $r0 = ($r0 - $keys[$r3 & 0x3F]) & 0xFFFF; + $limit = $actions[$limit]; + } + } + + return pack('vvvv', $r0, $r1, $r2, $r3); + } + + /** + * Setup the \phpseclib\Crypt\Base::ENGINE_MCRYPT $engine + * + * @see \phpseclib\Crypt\Base::_setupMcrypt() + * @access private + */ + function _setupMcrypt() + { + if (!isset($this->key)) { + $this->setKey(''); + } + + parent::_setupMcrypt(); + } + + /** + * Creates the key schedule + * + * @see \phpseclib\Crypt\Base::_setupKey() + * @access private + */ + function _setupKey() + { + if (!isset($this->key)) { + $this->setKey(''); + } + + // Key has already been expanded in \phpseclib\Crypt\RC2::setKey(): + // Only the first value must be altered. + $l = unpack('Ca/Cb/v*', $this->key); + array_unshift($l, $this->pitable[$l['a']] | ($l['b'] << 8)); + unset($l['a']); + unset($l['b']); + $this->keys = $l; + } + + /** + * Setup the performance-optimized function for de/encrypt() + * + * @see \phpseclib\Crypt\Base::_setupInlineCrypt() + * @access private + */ + function _setupInlineCrypt() + { + $lambda_functions =& self::_getLambdaFunctions(); + + // The first 10 generated $lambda_functions will use the $keys hardcoded as integers + // for the mixing rounds, for better inline crypt performance [~20% faster]. + // But for memory reason we have to limit those ultra-optimized $lambda_functions to an amount of 10. + // (Currently, for Crypt_RC2, one generated $lambda_function cost on php5.5@32bit ~60kb unfreeable mem and ~100kb on php5.5@64bit) + $gen_hi_opt_code = (bool)(count($lambda_functions) < 10); + + // Generation of a unique hash for our generated code + $code_hash = "Crypt_RC2, {$this->mode}"; + if ($gen_hi_opt_code) { + $code_hash = str_pad($code_hash, 32) . $this->_hashInlineCryptFunction($this->key); + } + + // Is there a re-usable $lambda_functions in there? + // If not, we have to create it. + if (!isset($lambda_functions[$code_hash])) { + // Init code for both, encrypt and decrypt. + $init_crypt = '$keys = $self->keys;'; + + switch (true) { + case $gen_hi_opt_code: + $keys = $this->keys; + default: + $keys = array(); + foreach ($this->keys as $k => $v) { + $keys[$k] = '$keys[' . $k . ']'; + } + } + + // $in is the current 8 bytes block which has to be en/decrypt + $encrypt_block = $decrypt_block = ' + $in = unpack("v4", $in); + $r0 = $in[1]; + $r1 = $in[2]; + $r2 = $in[3]; + $r3 = $in[4]; + '; + + // Create code for encryption. + $limit = 20; + $actions = array($limit => 44, 44 => 64); + $j = 0; + + for (;;) { + // Mixing round. + $encrypt_block .= ' + $r0 = (($r0 + ' . $keys[$j++] . ' + + ((($r1 ^ $r2) & $r3) ^ $r1)) & 0xFFFF) << 1; + $r0 |= $r0 >> 16; + $r1 = (($r1 + ' . $keys[$j++] . ' + + ((($r2 ^ $r3) & $r0) ^ $r2)) & 0xFFFF) << 2; + $r1 |= $r1 >> 16; + $r2 = (($r2 + ' . $keys[$j++] . ' + + ((($r3 ^ $r0) & $r1) ^ $r3)) & 0xFFFF) << 3; + $r2 |= $r2 >> 16; + $r3 = (($r3 + ' . $keys[$j++] . ' + + ((($r0 ^ $r1) & $r2) ^ $r0)) & 0xFFFF) << 5; + $r3 |= $r3 >> 16;'; + + if ($j === $limit) { + if ($limit === 64) { + break; + } + + // Mashing round. + $encrypt_block .= ' + $r0 += $keys[$r3 & 0x3F]; + $r1 += $keys[$r0 & 0x3F]; + $r2 += $keys[$r1 & 0x3F]; + $r3 += $keys[$r2 & 0x3F];'; + $limit = $actions[$limit]; + } + } + + $encrypt_block .= '$in = pack("v4", $r0, $r1, $r2, $r3);'; + + // Create code for decryption. + $limit = 44; + $actions = array($limit => 20, 20 => 0); + $j = 64; + + for (;;) { + // R-mixing round. + $decrypt_block .= ' + $r3 = ($r3 | ($r3 << 16)) >> 5; + $r3 = ($r3 - ' . $keys[--$j] . ' - + ((($r0 ^ $r1) & $r2) ^ $r0)) & 0xFFFF; + $r2 = ($r2 | ($r2 << 16)) >> 3; + $r2 = ($r2 - ' . $keys[--$j] . ' - + ((($r3 ^ $r0) & $r1) ^ $r3)) & 0xFFFF; + $r1 = ($r1 | ($r1 << 16)) >> 2; + $r1 = ($r1 - ' . $keys[--$j] . ' - + ((($r2 ^ $r3) & $r0) ^ $r2)) & 0xFFFF; + $r0 = ($r0 | ($r0 << 16)) >> 1; + $r0 = ($r0 - ' . $keys[--$j] . ' - + ((($r1 ^ $r2) & $r3) ^ $r1)) & 0xFFFF;'; + + if ($j === $limit) { + if ($limit === 0) { + break; + } + + // R-mashing round. + $decrypt_block .= ' + $r3 = ($r3 - $keys[$r2 & 0x3F]) & 0xFFFF; + $r2 = ($r2 - $keys[$r1 & 0x3F]) & 0xFFFF; + $r1 = ($r1 - $keys[$r0 & 0x3F]) & 0xFFFF; + $r0 = ($r0 - $keys[$r3 & 0x3F]) & 0xFFFF;'; + $limit = $actions[$limit]; + } + } + + $decrypt_block .= '$in = pack("v4", $r0, $r1, $r2, $r3);'; + + // Creates the inline-crypt function + $lambda_functions[$code_hash] = $this->_createInlineCryptFunction( + array( + 'init_crypt' => $init_crypt, + 'encrypt_block' => $encrypt_block, + 'decrypt_block' => $decrypt_block + ) + ); + } + + // Set the inline-crypt function as callback in: $this->inline_crypt + $this->inline_crypt = $lambda_functions[$code_hash]; + } +} diff --git a/3rdparty/phpseclib/phpseclib/phpseclib/Crypt/RC4.php b/3rdparty/phpseclib/phpseclib/phpseclib/Crypt/RC4.php new file mode 100644 index 00000000..2e5c0556 --- /dev/null +++ b/3rdparty/phpseclib/phpseclib/phpseclib/Crypt/RC4.php @@ -0,0 +1,348 @@ + + * setKey('abcdefgh'); + * + * $size = 10 * 1024; + * $plaintext = ''; + * for ($i = 0; $i < $size; $i++) { + * $plaintext.= 'a'; + * } + * + * echo $rc4->decrypt($rc4->encrypt($plaintext)); + * ?> + * + * + * @category Crypt + * @package RC4 + * @author Jim Wigginton + * @copyright 2007 Jim Wigginton + * @license http://www.opensource.org/licenses/mit-license.html MIT License + * @link http://phpseclib.sourceforge.net + */ + +namespace phpseclib\Crypt; + +/** + * Pure-PHP implementation of RC4. + * + * @package RC4 + * @author Jim Wigginton + * @access public + */ +class RC4 extends Base +{ + /**#@+ + * @access private + * @see \phpseclib\Crypt\RC4::_crypt() + */ + const ENCRYPT = 0; + const DECRYPT = 1; + /**#@-*/ + + /** + * Block Length of the cipher + * + * RC4 is a stream cipher + * so we the block_size to 0 + * + * @see \phpseclib\Crypt\Base::block_size + * @var int + * @access private + */ + var $block_size = 0; + + /** + * Key Length (in bytes) + * + * @see \phpseclib\Crypt\RC4::setKeyLength() + * @var int + * @access private + */ + var $key_length = 128; // = 1024 bits + + /** + * The mcrypt specific name of the cipher + * + * @see \phpseclib\Crypt\Base::cipher_name_mcrypt + * @var string + * @access private + */ + var $cipher_name_mcrypt = 'arcfour'; + + /** + * Holds whether performance-optimized $inline_crypt() can/should be used. + * + * @see \phpseclib\Crypt\Base::inline_crypt + * @var mixed + * @access private + */ + var $use_inline_crypt = false; // currently not available + + /** + * The Key + * + * @see self::setKey() + * @var string + * @access private + */ + var $key; + + /** + * The Key Stream for decryption and encryption + * + * @see self::setKey() + * @var array + * @access private + */ + var $stream; + + /** + * Default Constructor. + * + * Determines whether or not the mcrypt extension should be used. + * + * @see \phpseclib\Crypt\Base::__construct() + * @return \phpseclib\Crypt\RC4 + * @access public + */ + function __construct() + { + parent::__construct(Base::MODE_STREAM); + } + + /** + * Test for engine validity + * + * This is mainly just a wrapper to set things up for \phpseclib\Crypt\Base::isValidEngine() + * + * @see \phpseclib\Crypt\Base::__construct() + * @param int $engine + * @access public + * @return bool + */ + function isValidEngine($engine) + { + if ($engine == self::ENGINE_OPENSSL) { + // quoting https://www.openssl.org/news/openssl-3.0-notes.html, OpenSSL 3.0.1 + // "Moved all variations of the EVP ciphers CAST5, BF, IDEA, SEED, RC2, RC4, RC5, and DES to the legacy provider" + // in theory openssl_get_cipher_methods() should catch this but, on GitHub Actions, at least, it does not + if (defined('OPENSSL_VERSION_TEXT') && version_compare(preg_replace('#OpenSSL (\d+\.\d+\.\d+) .*#', '$1', OPENSSL_VERSION_TEXT), '3.0.1', '>=')) { + return false; + } + if (version_compare(PHP_VERSION, '5.3.7') >= 0) { + $this->cipher_name_openssl = 'rc4-40'; + } else { + switch (strlen($this->key)) { + case 5: + $this->cipher_name_openssl = 'rc4-40'; + break; + case 8: + $this->cipher_name_openssl = 'rc4-64'; + break; + case 16: + $this->cipher_name_openssl = 'rc4'; + break; + default: + return false; + } + } + } + + return parent::isValidEngine($engine); + } + + /** + * Dummy function. + * + * Some protocols, such as WEP, prepend an "initialization vector" to the key, effectively creating a new key [1]. + * If you need to use an initialization vector in this manner, feel free to prepend it to the key, yourself, before + * calling setKey(). + * + * [1] WEP's initialization vectors (IV's) are used in a somewhat insecure way. Since, in that protocol, + * the IV's are relatively easy to predict, an attack described by + * {@link http://www.drizzle.com/~aboba/IEEE/rc4_ksaproc.pdf Scott Fluhrer, Itsik Mantin, and Adi Shamir} + * can be used to quickly guess at the rest of the key. The following links elaborate: + * + * {@link http://www.rsa.com/rsalabs/node.asp?id=2009 http://www.rsa.com/rsalabs/node.asp?id=2009} + * {@link http://en.wikipedia.org/wiki/Related_key_attack http://en.wikipedia.org/wiki/Related_key_attack} + * + * @param string $iv + * @see self::setKey() + * @access public + */ + function setIV($iv) + { + } + + /** + * Sets the key length + * + * Keys can be between 1 and 256 bytes long. + * + * @access public + * @param int $length + */ + function setKeyLength($length) + { + if ($length < 8) { + $this->key_length = 1; + } elseif ($length > 2048) { + $this->key_length = 256; + } else { + $this->key_length = $length >> 3; + } + + parent::setKeyLength($length); + } + + /** + * Encrypts a message. + * + * @see \phpseclib\Crypt\Base::decrypt() + * @see self::_crypt() + * @access public + * @param string $plaintext + * @return string $ciphertext + */ + function encrypt($plaintext) + { + if ($this->engine != self::ENGINE_INTERNAL) { + return parent::encrypt($plaintext); + } + return $this->_crypt($plaintext, self::ENCRYPT); + } + + /** + * Decrypts a message. + * + * $this->decrypt($this->encrypt($plaintext)) == $this->encrypt($this->encrypt($plaintext)). + * At least if the continuous buffer is disabled. + * + * @see \phpseclib\Crypt\Base::encrypt() + * @see self::_crypt() + * @access public + * @param string $ciphertext + * @return string $plaintext + */ + function decrypt($ciphertext) + { + if ($this->engine != self::ENGINE_INTERNAL) { + return parent::decrypt($ciphertext); + } + return $this->_crypt($ciphertext, self::DECRYPT); + } + + /** + * Encrypts a block + * + * @access private + * @param string $in + */ + function _encryptBlock($in) + { + // RC4 does not utilize this method + } + + /** + * Decrypts a block + * + * @access private + * @param string $in + */ + function _decryptBlock($in) + { + // RC4 does not utilize this method + } + + /** + * Setup the key (expansion) + * + * @see \phpseclib\Crypt\Base::_setupKey() + * @access private + */ + function _setupKey() + { + $key = $this->key; + $keyLength = strlen($key); + $keyStream = range(0, 255); + $j = 0; + for ($i = 0; $i < 256; $i++) { + $j = ($j + $keyStream[$i] + ord($key[$i % $keyLength])) & 255; + $temp = $keyStream[$i]; + $keyStream[$i] = $keyStream[$j]; + $keyStream[$j] = $temp; + } + + $this->stream = array(); + $this->stream[self::DECRYPT] = $this->stream[self::ENCRYPT] = array( + 0, // index $i + 0, // index $j + $keyStream + ); + } + + /** + * Encrypts or decrypts a message. + * + * @see self::encrypt() + * @see self::decrypt() + * @access private + * @param string $text + * @param int $mode + * @return string $text + */ + function _crypt($text, $mode) + { + if ($this->changed) { + $this->_setup(); + $this->changed = false; + } + + $stream = &$this->stream[$mode]; + if ($this->continuousBuffer) { + $i = &$stream[0]; + $j = &$stream[1]; + $keyStream = &$stream[2]; + } else { + $i = $stream[0]; + $j = $stream[1]; + $keyStream = $stream[2]; + } + + $len = strlen($text); + for ($k = 0; $k < $len; ++$k) { + $i = ($i + 1) & 255; + $ksi = $keyStream[$i]; + $j = ($j + $ksi) & 255; + $ksj = $keyStream[$j]; + + $keyStream[$i] = $ksj; + $keyStream[$j] = $ksi; + $text[$k] = $text[$k] ^ chr($keyStream[($ksj + $ksi) & 255]); + } + + return $text; + } +} diff --git a/3rdparty/phpseclib/phpseclib/phpseclib/Crypt/RSA.php b/3rdparty/phpseclib/phpseclib/phpseclib/Crypt/RSA.php new file mode 100644 index 00000000..fec68958 --- /dev/null +++ b/3rdparty/phpseclib/phpseclib/phpseclib/Crypt/RSA.php @@ -0,0 +1,3343 @@ + + * createKey()); + * + * $plaintext = 'terrafrost'; + * + * $rsa->loadKey($privatekey); + * $ciphertext = $rsa->encrypt($plaintext); + * + * $rsa->loadKey($publickey); + * echo $rsa->decrypt($ciphertext); + * ?> + * + * + * Here's an example of how to create signatures and verify signatures with this library: + * + * createKey()); + * + * $plaintext = 'terrafrost'; + * + * $rsa->loadKey($privatekey); + * $signature = $rsa->sign($plaintext); + * + * $rsa->loadKey($publickey); + * echo $rsa->verify($plaintext, $signature) ? 'verified' : 'unverified'; + * ?> + * + * + * @category Crypt + * @package RSA + * @author Jim Wigginton + * @copyright 2009 Jim Wigginton + * @license http://www.opensource.org/licenses/mit-license.html MIT License + * @link http://phpseclib.sourceforge.net + */ + +namespace phpseclib\Crypt; + +use phpseclib\Math\BigInteger; + +/** + * Pure-PHP PKCS#1 compliant implementation of RSA. + * + * @package RSA + * @author Jim Wigginton + * @access public + */ +class RSA +{ + /**#@+ + * @access public + * @see self::encrypt() + * @see self::decrypt() + */ + /** + * Use {@link http://en.wikipedia.org/wiki/Optimal_Asymmetric_Encryption_Padding Optimal Asymmetric Encryption Padding} + * (OAEP) for encryption / decryption. + * + * Uses sha1 by default. + * + * @see self::setHash() + * @see self::setMGFHash() + */ + const ENCRYPTION_OAEP = 1; + /** + * Use PKCS#1 padding. + * + * Although self::ENCRYPTION_OAEP offers more security, including PKCS#1 padding is necessary for purposes of backwards + * compatibility with protocols (like SSH-1) written before OAEP's introduction. + */ + const ENCRYPTION_PKCS1 = 2; + /** + * Do not use any padding + * + * Although this method is not recommended it can none-the-less sometimes be useful if you're trying to decrypt some legacy + * stuff, if you're trying to diagnose why an encrypted message isn't decrypting, etc. + */ + const ENCRYPTION_NONE = 3; + /**#@-*/ + + /**#@+ + * @access public + * @see self::sign() + * @see self::verify() + * @see self::setHash() + */ + /** + * Use the Probabilistic Signature Scheme for signing + * + * Uses sha1 by default. + * + * @see self::setSaltLength() + * @see self::setMGFHash() + */ + const SIGNATURE_PSS = 1; + /** + * Use the PKCS#1 scheme by default. + * + * Although self::SIGNATURE_PSS offers more security, including PKCS#1 signing is necessary for purposes of backwards + * compatibility with protocols (like SSH-2) written before PSS's introduction. + */ + const SIGNATURE_PKCS1 = 2; + /**#@-*/ + + /**#@+ + * @access private + * @see \phpseclib\Crypt\RSA::createKey() + */ + /** + * ASN1 Integer + */ + const ASN1_INTEGER = 2; + /** + * ASN1 Bit String + */ + const ASN1_BITSTRING = 3; + /** + * ASN1 Octet String + */ + const ASN1_OCTETSTRING = 4; + /** + * ASN1 Object Identifier + */ + const ASN1_OBJECT = 6; + /** + * ASN1 Sequence (with the constucted bit set) + */ + const ASN1_SEQUENCE = 48; + /**#@-*/ + + /**#@+ + * @access private + * @see \phpseclib\Crypt\RSA::__construct() + */ + /** + * To use the pure-PHP implementation + */ + const MODE_INTERNAL = 1; + /** + * To use the OpenSSL library + * + * (if enabled; otherwise, the internal implementation will be used) + */ + const MODE_OPENSSL = 2; + /**#@-*/ + + /**#@+ + * @access public + * @see \phpseclib\Crypt\RSA::createKey() + * @see \phpseclib\Crypt\RSA::setPrivateKeyFormat() + */ + /** + * PKCS#1 formatted private key + * + * Used by OpenSSH + */ + const PRIVATE_FORMAT_PKCS1 = 0; + /** + * PuTTY formatted private key + */ + const PRIVATE_FORMAT_PUTTY = 1; + /** + * XML formatted private key + */ + const PRIVATE_FORMAT_XML = 2; + /** + * PKCS#8 formatted private key + */ + const PRIVATE_FORMAT_PKCS8 = 8; + /** + * OpenSSH formatted private key + */ + const PRIVATE_FORMAT_OPENSSH = 9; + /**#@-*/ + + /**#@+ + * @access public + * @see \phpseclib\Crypt\RSA::createKey() + * @see \phpseclib\Crypt\RSA::setPublicKeyFormat() + */ + /** + * Raw public key + * + * An array containing two \phpseclib\Math\BigInteger objects. + * + * The exponent can be indexed with any of the following: + * + * 0, e, exponent, publicExponent + * + * The modulus can be indexed with any of the following: + * + * 1, n, modulo, modulus + */ + const PUBLIC_FORMAT_RAW = 3; + /** + * PKCS#1 formatted public key (raw) + * + * Used by File/X509.php + * + * Has the following header: + * + * -----BEGIN RSA PUBLIC KEY----- + * + * Analogous to ssh-keygen's pem format (as specified by -m) + */ + const PUBLIC_FORMAT_PKCS1 = 4; + const PUBLIC_FORMAT_PKCS1_RAW = 4; + /** + * XML formatted public key + */ + const PUBLIC_FORMAT_XML = 5; + /** + * OpenSSH formatted public key + * + * Place in $HOME/.ssh/authorized_keys + */ + const PUBLIC_FORMAT_OPENSSH = 6; + /** + * PKCS#1 formatted public key (encapsulated) + * + * Used by PHP's openssl_public_encrypt() and openssl's rsautl (when -pubin is set) + * + * Has the following header: + * + * -----BEGIN PUBLIC KEY----- + * + * Analogous to ssh-keygen's pkcs8 format (as specified by -m). Although PKCS8 + * is specific to private keys it's basically creating a DER-encoded wrapper + * for keys. This just extends that same concept to public keys (much like ssh-keygen) + */ + const PUBLIC_FORMAT_PKCS8 = 7; + /**#@-*/ + + /** + * Precomputed Zero + * + * @var \phpseclib\Math\BigInteger + * @access private + */ + var $zero; + + /** + * Precomputed One + * + * @var \phpseclib\Math\BigInteger + * @access private + */ + var $one; + + /** + * Private Key Format + * + * @var int + * @access private + */ + var $privateKeyFormat = self::PRIVATE_FORMAT_PKCS1; + + /** + * Public Key Format + * + * @var int + * @access public + */ + var $publicKeyFormat = self::PUBLIC_FORMAT_PKCS8; + + /** + * Modulus (ie. n) + * + * @var \phpseclib\Math\BigInteger + * @access private + */ + var $modulus; + + /** + * Modulus length + * + * @var \phpseclib\Math\BigInteger + * @access private + */ + var $k; + + /** + * Exponent (ie. e or d) + * + * @var \phpseclib\Math\BigInteger + * @access private + */ + var $exponent; + + /** + * Primes for Chinese Remainder Theorem (ie. p and q) + * + * @var array + * @access private + */ + var $primes; + + /** + * Exponents for Chinese Remainder Theorem (ie. dP and dQ) + * + * @var array + * @access private + */ + var $exponents; + + /** + * Coefficients for Chinese Remainder Theorem (ie. qInv) + * + * @var array + * @access private + */ + var $coefficients; + + /** + * Hash name + * + * @var string + * @access private + */ + var $hashName; + + /** + * Hash function + * + * @var \phpseclib\Crypt\Hash + * @access private + */ + var $hash; + + /** + * Length of hash function output + * + * @var int + * @access private + */ + var $hLen; + + /** + * Length of salt + * + * @var int + * @access private + */ + var $sLen; + + /** + * Hash function for the Mask Generation Function + * + * @var \phpseclib\Crypt\Hash + * @access private + */ + var $mgfHash; + + /** + * Length of MGF hash function output + * + * @var int + * @access private + */ + var $mgfHLen; + + /** + * Encryption mode + * + * @var int + * @access private + */ + var $encryptionMode = self::ENCRYPTION_OAEP; + + /** + * Signature mode + * + * @var int + * @access private + */ + var $signatureMode = self::SIGNATURE_PSS; + + /** + * Public Exponent + * + * @var mixed + * @access private + */ + var $publicExponent = false; + + /** + * Password + * + * @var string + * @access private + */ + var $password = false; + + /** + * Components + * + * For use with parsing XML formatted keys. PHP's XML Parser functions use utilized - instead of PHP's DOM functions - + * because PHP's XML Parser functions work on PHP4 whereas PHP's DOM functions - although surperior - don't. + * + * @see self::_start_element_handler() + * @var array + * @access private + */ + var $components = array(); + + /** + * Current String + * + * For use with parsing XML formatted keys. + * + * @see self::_character_handler() + * @see self::_stop_element_handler() + * @var mixed + * @access private + */ + var $current; + + /** + * OpenSSL configuration file name. + * + * Set to null to use system configuration file. + * @see self::createKey() + * @var mixed + * @Access public + */ + var $configFile; + + /** + * Public key comment field. + * + * @var string + * @access private + */ + var $comment = 'phpseclib-generated-key'; + + /** + * The constructor + * + * If you want to make use of the openssl extension, you'll need to set the mode manually, yourself. The reason + * \phpseclib\Crypt\RSA doesn't do it is because OpenSSL doesn't fail gracefully. openssl_pkey_new(), in particular, requires + * openssl.cnf be present somewhere and, unfortunately, the only real way to find out is too late. + * + * @return \phpseclib\Crypt\RSA + * @access public + */ + function __construct() + { + $this->configFile = dirname(__FILE__) . '/../openssl.cnf'; + + if (!defined('CRYPT_RSA_MODE')) { + switch (true) { + // Math/BigInteger's openssl requirements are a little less stringent than Crypt/RSA's. in particular, + // Math/BigInteger doesn't require an openssl.cfg file whereas Crypt/RSA does. so if Math/BigInteger + // can't use OpenSSL it can be pretty trivially assumed, then, that Crypt/RSA can't either. + case defined('MATH_BIGINTEGER_OPENSSL_DISABLE'): + define('CRYPT_RSA_MODE', self::MODE_INTERNAL); + break; + case function_exists('phpinfo') && extension_loaded('openssl') && file_exists($this->configFile): + // some versions of XAMPP have mismatched versions of OpenSSL which causes it not to work + $versions = array(); + + // avoid generating errors (even with suppression) when phpinfo() is disabled (common in production systems) + if (strpos(ini_get('disable_functions'), 'phpinfo') === false) { + ob_start(); + @phpinfo(); + $content = ob_get_contents(); + ob_end_clean(); + + preg_match_all('#OpenSSL (Header|Library) Version(.*)#im', $content, $matches); + + if (!empty($matches[1])) { + for ($i = 0; $i < count($matches[1]); $i++) { + $fullVersion = trim(str_replace('=>', '', strip_tags($matches[2][$i]))); + + // Remove letter part in OpenSSL version + if (!preg_match('/(\d+\.\d+\.\d+)/i', $fullVersion, $m)) { + $versions[$matches[1][$i]] = $fullVersion; + } else { + $versions[$matches[1][$i]] = $m[0]; + } + } + } + } + + // it doesn't appear that OpenSSL versions were reported upon until PHP 5.3+ + switch (true) { + case !isset($versions['Header']): + case !isset($versions['Library']): + case $versions['Header'] == $versions['Library']: + case version_compare($versions['Header'], '1.0.0') >= 0 && version_compare($versions['Library'], '1.0.0') >= 0: + define('CRYPT_RSA_MODE', self::MODE_OPENSSL); + break; + default: + define('CRYPT_RSA_MODE', self::MODE_INTERNAL); + define('MATH_BIGINTEGER_OPENSSL_DISABLE', true); + } + break; + default: + define('CRYPT_RSA_MODE', self::MODE_INTERNAL); + } + } + + $this->zero = new BigInteger(); + $this->one = new BigInteger(1); + + $this->hash = new Hash('sha1'); + $this->hLen = $this->hash->getLength(); + $this->hashName = 'sha1'; + $this->mgfHash = new Hash('sha1'); + $this->mgfHLen = $this->mgfHash->getLength(); + } + + /** + * Create public / private key pair + * + * Returns an array with the following three elements: + * - 'privatekey': The private key. + * - 'publickey': The public key. + * - 'partialkey': A partially computed key (if the execution time exceeded $timeout). + * Will need to be passed back to \phpseclib\Crypt\RSA::createKey() as the third parameter for further processing. + * + * @access public + * @param int $bits + * @param int $timeout + * @param array $partial + */ + function createKey($bits = 1024, $timeout = false, $partial = array()) + { + if (!defined('CRYPT_RSA_EXPONENT')) { + // http://en.wikipedia.org/wiki/65537_%28number%29 + define('CRYPT_RSA_EXPONENT', '65537'); + } + // per , this number ought not result in primes smaller + // than 256 bits. as a consequence if the key you're trying to create is 1024 bits and you've set CRYPT_RSA_SMALLEST_PRIME + // to 384 bits then you're going to get a 384 bit prime and a 640 bit prime (384 + 1024 % 384). at least if + // CRYPT_RSA_MODE is set to self::MODE_INTERNAL. if CRYPT_RSA_MODE is set to self::MODE_OPENSSL then + // CRYPT_RSA_SMALLEST_PRIME is ignored (ie. multi-prime RSA support is more intended as a way to speed up RSA key + // generation when there's a chance neither gmp nor OpenSSL are installed) + if (!defined('CRYPT_RSA_SMALLEST_PRIME')) { + define('CRYPT_RSA_SMALLEST_PRIME', 4096); + } + + // OpenSSL uses 65537 as the exponent and requires RSA keys be 384 bits minimum + if (CRYPT_RSA_MODE == self::MODE_OPENSSL && $bits >= 384 && CRYPT_RSA_EXPONENT == 65537) { + $config = array(); + if (isset($this->configFile)) { + $config['config'] = $this->configFile; + } + $rsa = openssl_pkey_new(array('private_key_bits' => $bits) + $config); + openssl_pkey_export($rsa, $privatekey, null, $config); + $publickey = openssl_pkey_get_details($rsa); + $publickey = $publickey['key']; + + $privatekey = call_user_func_array(array($this, '_convertPrivateKey'), array_values($this->_parseKey($privatekey, self::PRIVATE_FORMAT_PKCS1))); + $publickey = call_user_func_array(array($this, '_convertPublicKey'), array_values($this->_parseKey($publickey, self::PUBLIC_FORMAT_PKCS1))); + + // clear the buffer of error strings stemming from a minimalistic openssl.cnf + // https://github.com/php/php-src/issues/11054 talks about other errors this'll pick up + while (openssl_error_string() !== false) { + } + + return array( + 'privatekey' => $privatekey, + 'publickey' => $publickey, + 'partialkey' => false + ); + } + + static $e; + if (!isset($e)) { + $e = new BigInteger(CRYPT_RSA_EXPONENT); + } + + extract($this->_generateMinMax($bits)); + $absoluteMin = $min; + $temp = $bits >> 1; // divide by two to see how many bits P and Q would be + if ($temp > CRYPT_RSA_SMALLEST_PRIME) { + $num_primes = floor($bits / CRYPT_RSA_SMALLEST_PRIME); + $temp = CRYPT_RSA_SMALLEST_PRIME; + } else { + $num_primes = 2; + } + extract($this->_generateMinMax($temp + $bits % $temp)); + $finalMax = $max; + extract($this->_generateMinMax($temp)); + + $generator = new BigInteger(); + + $n = $this->one->copy(); + if (!empty($partial)) { + extract(unserialize($partial)); + } else { + $exponents = $coefficients = $primes = array(); + $lcm = array( + 'top' => $this->one->copy(), + 'bottom' => false + ); + } + + $start = time(); + $i0 = count($primes) + 1; + + do { + for ($i = $i0; $i <= $num_primes; $i++) { + if ($timeout !== false) { + $timeout-= time() - $start; + $start = time(); + if ($timeout <= 0) { + return array( + 'privatekey' => '', + 'publickey' => '', + 'partialkey' => serialize(array( + 'primes' => $primes, + 'coefficients' => $coefficients, + 'lcm' => $lcm, + 'exponents' => $exponents + )) + ); + } + } + + if ($i == $num_primes) { + list($min, $temp) = $absoluteMin->divide($n); + if (!$temp->equals($this->zero)) { + $min = $min->add($this->one); // ie. ceil() + } + $primes[$i] = $generator->randomPrime($min, $finalMax, $timeout); + } else { + $primes[$i] = $generator->randomPrime($min, $max, $timeout); + } + + if ($primes[$i] === false) { // if we've reached the timeout + if (count($primes) > 1) { + $partialkey = ''; + } else { + array_pop($primes); + $partialkey = serialize(array( + 'primes' => $primes, + 'coefficients' => $coefficients, + 'lcm' => $lcm, + 'exponents' => $exponents + )); + } + + return array( + 'privatekey' => '', + 'publickey' => '', + 'partialkey' => $partialkey + ); + } + + // the first coefficient is calculated differently from the rest + // ie. instead of being $primes[1]->modInverse($primes[2]), it's $primes[2]->modInverse($primes[1]) + if ($i > 2) { + $coefficients[$i] = $n->modInverse($primes[$i]); + } + + $n = $n->multiply($primes[$i]); + + $temp = $primes[$i]->subtract($this->one); + + // textbook RSA implementations use Euler's totient function instead of the least common multiple. + // see http://en.wikipedia.org/wiki/Euler%27s_totient_function + $lcm['top'] = $lcm['top']->multiply($temp); + $lcm['bottom'] = $lcm['bottom'] === false ? $temp : $lcm['bottom']->gcd($temp); + + $exponents[$i] = $e->modInverse($temp); + } + + list($temp) = $lcm['top']->divide($lcm['bottom']); + $gcd = $temp->gcd($e); + $i0 = 1; + } while (!$gcd->equals($this->one)); + + $d = $e->modInverse($temp); + + $coefficients[2] = $primes[2]->modInverse($primes[1]); + + // from : + // RSAPrivateKey ::= SEQUENCE { + // version Version, + // modulus INTEGER, -- n + // publicExponent INTEGER, -- e + // privateExponent INTEGER, -- d + // prime1 INTEGER, -- p + // prime2 INTEGER, -- q + // exponent1 INTEGER, -- d mod (p-1) + // exponent2 INTEGER, -- d mod (q-1) + // coefficient INTEGER, -- (inverse of q) mod p + // otherPrimeInfos OtherPrimeInfos OPTIONAL + // } + + return array( + 'privatekey' => $this->_convertPrivateKey($n, $e, $d, $primes, $exponents, $coefficients), + 'publickey' => $this->_convertPublicKey($n, $e), + 'partialkey' => false + ); + } + + /** + * Convert a private key to the appropriate format. + * + * @access private + * @see self::setPrivateKeyFormat() + * @param Math_BigInteger $n + * @param Math_BigInteger $e + * @param Math_BigInteger $d + * @param array $primes + * @param array $exponents + * @param array $coefficients + * @return string + */ + function _convertPrivateKey($n, $e, $d, $primes, $exponents, $coefficients) + { + $signed = $this->privateKeyFormat != self::PRIVATE_FORMAT_XML; + $num_primes = count($primes); + $raw = array( + 'version' => $num_primes == 2 ? chr(0) : chr(1), // two-prime vs. multi + 'modulus' => $n->toBytes($signed), + 'publicExponent' => $e->toBytes($signed), + 'privateExponent' => $d->toBytes($signed), + 'prime1' => $primes[1]->toBytes($signed), + 'prime2' => $primes[2]->toBytes($signed), + 'exponent1' => $exponents[1]->toBytes($signed), + 'exponent2' => $exponents[2]->toBytes($signed), + 'coefficient' => $coefficients[2]->toBytes($signed) + ); + + // if the format in question does not support multi-prime rsa and multi-prime rsa was used, + // call _convertPublicKey() instead. + switch ($this->privateKeyFormat) { + case self::PRIVATE_FORMAT_XML: + if ($num_primes != 2) { + return false; + } + return "\r\n" . + ' ' . base64_encode($raw['modulus']) . "\r\n" . + ' ' . base64_encode($raw['publicExponent']) . "\r\n" . + '

' . base64_encode($raw['prime1']) . "

\r\n" . + ' ' . base64_encode($raw['prime2']) . "\r\n" . + ' ' . base64_encode($raw['exponent1']) . "\r\n" . + ' ' . base64_encode($raw['exponent2']) . "\r\n" . + ' ' . base64_encode($raw['coefficient']) . "\r\n" . + ' ' . base64_encode($raw['privateExponent']) . "\r\n" . + '
'; + break; + case self::PRIVATE_FORMAT_PUTTY: + if ($num_primes != 2) { + return false; + } + $key = "PuTTY-User-Key-File-2: ssh-rsa\r\nEncryption: "; + $encryption = (!empty($this->password) || is_string($this->password)) ? 'aes256-cbc' : 'none'; + $key.= $encryption; + $key.= "\r\nComment: " . $this->comment . "\r\n"; + $public = pack( + 'Na*Na*Na*', + strlen('ssh-rsa'), + 'ssh-rsa', + strlen($raw['publicExponent']), + $raw['publicExponent'], + strlen($raw['modulus']), + $raw['modulus'] + ); + $source = pack( + 'Na*Na*Na*Na*', + strlen('ssh-rsa'), + 'ssh-rsa', + strlen($encryption), + $encryption, + strlen($this->comment), + $this->comment, + strlen($public), + $public + ); + $public = base64_encode($public); + $key.= "Public-Lines: " . ((strlen($public) + 63) >> 6) . "\r\n"; + $key.= chunk_split($public, 64); + $private = pack( + 'Na*Na*Na*Na*', + strlen($raw['privateExponent']), + $raw['privateExponent'], + strlen($raw['prime1']), + $raw['prime1'], + strlen($raw['prime2']), + $raw['prime2'], + strlen($raw['coefficient']), + $raw['coefficient'] + ); + if (empty($this->password) && !is_string($this->password)) { + $source.= pack('Na*', strlen($private), $private); + $hashkey = 'putty-private-key-file-mac-key'; + } else { + $private.= Random::string(16 - (strlen($private) & 15)); + $source.= pack('Na*', strlen($private), $private); + $sequence = 0; + $symkey = ''; + while (strlen($symkey) < 32) { + $temp = pack('Na*', $sequence++, $this->password); + $symkey.= pack('H*', sha1($temp)); + } + $symkey = substr($symkey, 0, 32); + $crypto = new AES(); + + $crypto->setKey($symkey); + $crypto->disablePadding(); + $private = $crypto->encrypt($private); + $hashkey = 'putty-private-key-file-mac-key' . $this->password; + } + + $private = base64_encode($private); + $key.= 'Private-Lines: ' . ((strlen($private) + 63) >> 6) . "\r\n"; + $key.= chunk_split($private, 64); + $hash = new Hash('sha1'); + $hash->setKey(pack('H*', sha1($hashkey))); + $key.= 'Private-MAC: ' . bin2hex($hash->hash($source)) . "\r\n"; + + return $key; + case self::PRIVATE_FORMAT_OPENSSH: + if ($num_primes != 2) { + return false; + } + $publicKey = pack('Na*Na*Na*', strlen('ssh-rsa'), 'ssh-rsa', strlen($raw['publicExponent']), $raw['publicExponent'], strlen($raw['modulus']), $raw['modulus']); + $privateKey = pack( + 'Na*Na*Na*Na*Na*Na*Na*', + strlen('ssh-rsa'), + 'ssh-rsa', + strlen($raw['modulus']), + $raw['modulus'], + strlen($raw['publicExponent']), + $raw['publicExponent'], + strlen($raw['privateExponent']), + $raw['privateExponent'], + strlen($raw['coefficient']), + $raw['coefficient'], + strlen($raw['prime1']), + $raw['prime1'], + strlen($raw['prime2']), + $raw['prime2'] + ); + $checkint = Random::string(4); + $paddedKey = pack( + 'a*Na*', + $checkint . $checkint . $privateKey, + strlen($this->comment), + $this->comment + ); + $paddingLength = (7 * strlen($paddedKey)) % 8; + for ($i = 1; $i <= $paddingLength; $i++) { + $paddedKey.= chr($i); + } + $key = pack( + 'Na*Na*Na*NNa*Na*', + strlen('none'), + 'none', + strlen('none'), + 'none', + 0, + '', + 1, + strlen($publicKey), + $publicKey, + strlen($paddedKey), + $paddedKey + ); + $key = "openssh-key-v1\0$key"; + + return "-----BEGIN OPENSSH PRIVATE KEY-----\n" . + chunk_split(base64_encode($key), 70, "\n") . + "-----END OPENSSH PRIVATE KEY-----\n"; + default: // eg. self::PRIVATE_FORMAT_PKCS1 + $components = array(); + foreach ($raw as $name => $value) { + $components[$name] = pack('Ca*a*', self::ASN1_INTEGER, $this->_encodeLength(strlen($value)), $value); + } + + $RSAPrivateKey = implode('', $components); + + if ($num_primes > 2) { + $OtherPrimeInfos = ''; + for ($i = 3; $i <= $num_primes; $i++) { + // OtherPrimeInfos ::= SEQUENCE SIZE(1..MAX) OF OtherPrimeInfo + // + // OtherPrimeInfo ::= SEQUENCE { + // prime INTEGER, -- ri + // exponent INTEGER, -- di + // coefficient INTEGER -- ti + // } + $OtherPrimeInfo = pack('Ca*a*', self::ASN1_INTEGER, $this->_encodeLength(strlen($primes[$i]->toBytes(true))), $primes[$i]->toBytes(true)); + $OtherPrimeInfo.= pack('Ca*a*', self::ASN1_INTEGER, $this->_encodeLength(strlen($exponents[$i]->toBytes(true))), $exponents[$i]->toBytes(true)); + $OtherPrimeInfo.= pack('Ca*a*', self::ASN1_INTEGER, $this->_encodeLength(strlen($coefficients[$i]->toBytes(true))), $coefficients[$i]->toBytes(true)); + $OtherPrimeInfos.= pack('Ca*a*', self::ASN1_SEQUENCE, $this->_encodeLength(strlen($OtherPrimeInfo)), $OtherPrimeInfo); + } + $RSAPrivateKey.= pack('Ca*a*', self::ASN1_SEQUENCE, $this->_encodeLength(strlen($OtherPrimeInfos)), $OtherPrimeInfos); + } + + $RSAPrivateKey = pack('Ca*a*', self::ASN1_SEQUENCE, $this->_encodeLength(strlen($RSAPrivateKey)), $RSAPrivateKey); + + if ($this->privateKeyFormat == self::PRIVATE_FORMAT_PKCS8) { + $rsaOID = pack('H*', '300d06092a864886f70d0101010500'); // hex version of MA0GCSqGSIb3DQEBAQUA + $RSAPrivateKey = pack( + 'Ca*a*Ca*a*', + self::ASN1_INTEGER, + "\01\00", + $rsaOID, + 4, + $this->_encodeLength(strlen($RSAPrivateKey)), + $RSAPrivateKey + ); + $RSAPrivateKey = pack('Ca*a*', self::ASN1_SEQUENCE, $this->_encodeLength(strlen($RSAPrivateKey)), $RSAPrivateKey); + if (!empty($this->password) || is_string($this->password)) { + $salt = Random::string(8); + $iterationCount = 2048; + + $crypto = new DES(); + $crypto->setPassword($this->password, 'pbkdf1', 'md5', $salt, $iterationCount); + $RSAPrivateKey = $crypto->encrypt($RSAPrivateKey); + + $parameters = pack( + 'Ca*a*Ca*N', + self::ASN1_OCTETSTRING, + $this->_encodeLength(strlen($salt)), + $salt, + self::ASN1_INTEGER, + $this->_encodeLength(4), + $iterationCount + ); + $pbeWithMD5AndDES_CBC = "\x2a\x86\x48\x86\xf7\x0d\x01\x05\x03"; + + $encryptionAlgorithm = pack( + 'Ca*a*Ca*a*', + self::ASN1_OBJECT, + $this->_encodeLength(strlen($pbeWithMD5AndDES_CBC)), + $pbeWithMD5AndDES_CBC, + self::ASN1_SEQUENCE, + $this->_encodeLength(strlen($parameters)), + $parameters + ); + + $RSAPrivateKey = pack( + 'Ca*a*Ca*a*', + self::ASN1_SEQUENCE, + $this->_encodeLength(strlen($encryptionAlgorithm)), + $encryptionAlgorithm, + self::ASN1_OCTETSTRING, + $this->_encodeLength(strlen($RSAPrivateKey)), + $RSAPrivateKey + ); + + $RSAPrivateKey = pack('Ca*a*', self::ASN1_SEQUENCE, $this->_encodeLength(strlen($RSAPrivateKey)), $RSAPrivateKey); + + $RSAPrivateKey = "-----BEGIN ENCRYPTED PRIVATE KEY-----\r\n" . + chunk_split(base64_encode($RSAPrivateKey), 64) . + '-----END ENCRYPTED PRIVATE KEY-----'; + } else { + $RSAPrivateKey = "-----BEGIN PRIVATE KEY-----\r\n" . + chunk_split(base64_encode($RSAPrivateKey), 64) . + '-----END PRIVATE KEY-----'; + } + return $RSAPrivateKey; + } + + if (!empty($this->password) || is_string($this->password)) { + $iv = Random::string(8); + $symkey = pack('H*', md5($this->password . $iv)); // symkey is short for symmetric key + $symkey.= substr(pack('H*', md5($symkey . $this->password . $iv)), 0, 8); + $des = new TripleDES(); + $des->setKey($symkey); + $des->setIV($iv); + $iv = strtoupper(bin2hex($iv)); + $RSAPrivateKey = "-----BEGIN RSA PRIVATE KEY-----\r\n" . + "Proc-Type: 4,ENCRYPTED\r\n" . + "DEK-Info: DES-EDE3-CBC,$iv\r\n" . + "\r\n" . + chunk_split(base64_encode($des->encrypt($RSAPrivateKey)), 64) . + '-----END RSA PRIVATE KEY-----'; + } else { + $RSAPrivateKey = "-----BEGIN RSA PRIVATE KEY-----\r\n" . + chunk_split(base64_encode($RSAPrivateKey), 64) . + '-----END RSA PRIVATE KEY-----'; + } + + return $RSAPrivateKey; + } + } + + /** + * Convert a public key to the appropriate format + * + * @access private + * @see self::setPublicKeyFormat() + * @param Math_BigInteger $n + * @param Math_BigInteger $e + * @return string|array + */ + function _convertPublicKey($n, $e) + { + $signed = $this->publicKeyFormat != self::PUBLIC_FORMAT_XML; + + $modulus = $n->toBytes($signed); + $publicExponent = $e->toBytes($signed); + + switch ($this->publicKeyFormat) { + case self::PUBLIC_FORMAT_RAW: + return array('e' => $e->copy(), 'n' => $n->copy()); + case self::PUBLIC_FORMAT_XML: + return "\r\n" . + ' ' . base64_encode($modulus) . "\r\n" . + ' ' . base64_encode($publicExponent) . "\r\n" . + ''; + break; + case self::PUBLIC_FORMAT_OPENSSH: + // from : + // string "ssh-rsa" + // mpint e + // mpint n + $RSAPublicKey = pack('Na*Na*Na*', strlen('ssh-rsa'), 'ssh-rsa', strlen($publicExponent), $publicExponent, strlen($modulus), $modulus); + $RSAPublicKey = 'ssh-rsa ' . base64_encode($RSAPublicKey) . ' ' . $this->comment; + + return $RSAPublicKey; + default: // eg. self::PUBLIC_FORMAT_PKCS1_RAW or self::PUBLIC_FORMAT_PKCS1 + // from : + // RSAPublicKey ::= SEQUENCE { + // modulus INTEGER, -- n + // publicExponent INTEGER -- e + // } + $components = array( + 'modulus' => pack('Ca*a*', self::ASN1_INTEGER, $this->_encodeLength(strlen($modulus)), $modulus), + 'publicExponent' => pack('Ca*a*', self::ASN1_INTEGER, $this->_encodeLength(strlen($publicExponent)), $publicExponent) + ); + + $RSAPublicKey = pack( + 'Ca*a*a*', + self::ASN1_SEQUENCE, + $this->_encodeLength(strlen($components['modulus']) + strlen($components['publicExponent'])), + $components['modulus'], + $components['publicExponent'] + ); + + if ($this->publicKeyFormat == self::PUBLIC_FORMAT_PKCS1_RAW) { + $RSAPublicKey = "-----BEGIN RSA PUBLIC KEY-----\r\n" . + chunk_split(base64_encode($RSAPublicKey), 64) . + '-----END RSA PUBLIC KEY-----'; + } else { + // sequence(oid(1.2.840.113549.1.1.1), null)) = rsaEncryption. + $rsaOID = pack('H*', '300d06092a864886f70d0101010500'); // hex version of MA0GCSqGSIb3DQEBAQUA + $RSAPublicKey = chr(0) . $RSAPublicKey; + $RSAPublicKey = chr(3) . $this->_encodeLength(strlen($RSAPublicKey)) . $RSAPublicKey; + + $RSAPublicKey = pack( + 'Ca*a*', + self::ASN1_SEQUENCE, + $this->_encodeLength(strlen($rsaOID . $RSAPublicKey)), + $rsaOID . $RSAPublicKey + ); + + $RSAPublicKey = "-----BEGIN PUBLIC KEY-----\r\n" . + chunk_split(base64_encode($RSAPublicKey), 64) . + '-----END PUBLIC KEY-----'; + } + + return $RSAPublicKey; + } + } + + /** + * Break a public or private key down into its constituant components + * + * @access private + * @see self::_convertPublicKey() + * @see self::_convertPrivateKey() + * @param string|array $key + * @param int $type + * @return array|bool + */ + function _parseKey($key, $type) + { + if ($type != self::PUBLIC_FORMAT_RAW && !is_string($key)) { + return false; + } + + switch ($type) { + case self::PUBLIC_FORMAT_RAW: + if (!is_array($key)) { + return false; + } + $components = array(); + switch (true) { + case isset($key['e']): + $components['publicExponent'] = $key['e']->copy(); + break; + case isset($key['exponent']): + $components['publicExponent'] = $key['exponent']->copy(); + break; + case isset($key['publicExponent']): + $components['publicExponent'] = $key['publicExponent']->copy(); + break; + case isset($key[0]): + $components['publicExponent'] = $key[0]->copy(); + } + switch (true) { + case isset($key['n']): + $components['modulus'] = $key['n']->copy(); + break; + case isset($key['modulo']): + $components['modulus'] = $key['modulo']->copy(); + break; + case isset($key['modulus']): + $components['modulus'] = $key['modulus']->copy(); + break; + case isset($key[1]): + $components['modulus'] = $key[1]->copy(); + } + return isset($components['modulus']) && isset($components['publicExponent']) ? $components : false; + case self::PRIVATE_FORMAT_PKCS1: + case self::PRIVATE_FORMAT_PKCS8: + case self::PUBLIC_FORMAT_PKCS1: + /* Although PKCS#1 proposes a format that public and private keys can use, encrypting them is + "outside the scope" of PKCS#1. PKCS#1 then refers you to PKCS#12 and PKCS#15 if you're wanting to + protect private keys, however, that's not what OpenSSL* does. OpenSSL protects private keys by adding + two new "fields" to the key - DEK-Info and Proc-Type. These fields are discussed here: + + http://tools.ietf.org/html/rfc1421#section-4.6.1.1 + http://tools.ietf.org/html/rfc1421#section-4.6.1.3 + + DES-EDE3-CBC as an algorithm, however, is not discussed anywhere, near as I can tell. + DES-CBC and DES-EDE are discussed in RFC1423, however, DES-EDE3-CBC isn't, nor is its key derivation + function. As is, the definitive authority on this encoding scheme isn't the IETF but rather OpenSSL's + own implementation. ie. the implementation *is* the standard and any bugs that may exist in that + implementation are part of the standard, as well. + + * OpenSSL is the de facto standard. It's utilized by OpenSSH and other projects */ + if (preg_match('#DEK-Info: (.+),(.+)#', $key, $matches)) { + $iv = pack('H*', trim($matches[2])); + $symkey = pack('H*', md5($this->password . substr($iv, 0, 8))); // symkey is short for symmetric key + $symkey.= pack('H*', md5($symkey . $this->password . substr($iv, 0, 8))); + // remove the Proc-Type / DEK-Info sections as they're no longer needed + $key = preg_replace('#^(?:Proc-Type|DEK-Info): .*#m', '', $key); + $ciphertext = $this->_extractBER($key); + if ($ciphertext === false) { + $ciphertext = $key; + } + switch ($matches[1]) { + case 'AES-256-CBC': + $crypto = new AES(); + break; + case 'AES-128-CBC': + $symkey = substr($symkey, 0, 16); + $crypto = new AES(); + break; + case 'DES-EDE3-CFB': + $crypto = new TripleDES(Base::MODE_CFB); + break; + case 'DES-EDE3-CBC': + $symkey = substr($symkey, 0, 24); + $crypto = new TripleDES(); + break; + case 'DES-CBC': + $crypto = new DES(); + break; + default: + return false; + } + $crypto->setKey($symkey); + $crypto->setIV($iv); + $decoded = $crypto->decrypt($ciphertext); + } else { + $decoded = $this->_extractBER($key); + } + + if ($decoded !== false) { + $key = $decoded; + } + + $components = array(); + + if (ord($this->_string_shift($key)) != self::ASN1_SEQUENCE) { + return false; + } + if ($this->_decodeLength($key) != strlen($key)) { + return false; + } + + $tag = ord($this->_string_shift($key)); + /* intended for keys for which OpenSSL's asn1parse returns the following: + + 0:d=0 hl=4 l= 631 cons: SEQUENCE + 4:d=1 hl=2 l= 1 prim: INTEGER :00 + 7:d=1 hl=2 l= 13 cons: SEQUENCE + 9:d=2 hl=2 l= 9 prim: OBJECT :rsaEncryption + 20:d=2 hl=2 l= 0 prim: NULL + 22:d=1 hl=4 l= 609 prim: OCTET STRING + + ie. PKCS8 keys*/ + + if ($tag == self::ASN1_INTEGER && substr($key, 0, 3) == "\x01\x00\x30") { + $this->_string_shift($key, 3); + $tag = self::ASN1_SEQUENCE; + } + + if ($tag == self::ASN1_SEQUENCE) { + $temp = $this->_string_shift($key, $this->_decodeLength($key)); + if (ord($this->_string_shift($temp)) != self::ASN1_OBJECT) { + return false; + } + $length = $this->_decodeLength($temp); + switch ($this->_string_shift($temp, $length)) { + case "\x2a\x86\x48\x86\xf7\x0d\x01\x01\x01": // rsaEncryption + case "\x2A\x86\x48\x86\xF7\x0D\x01\x01\x0A": // rsaPSS + break; + case "\x2a\x86\x48\x86\xf7\x0d\x01\x05\x03": // pbeWithMD5AndDES-CBC + /* + PBEParameter ::= SEQUENCE { + salt OCTET STRING (SIZE(8)), + iterationCount INTEGER } + */ + if (ord($this->_string_shift($temp)) != self::ASN1_SEQUENCE) { + return false; + } + if ($this->_decodeLength($temp) != strlen($temp)) { + return false; + } + $this->_string_shift($temp); // assume it's an octet string + $salt = $this->_string_shift($temp, $this->_decodeLength($temp)); + if (ord($this->_string_shift($temp)) != self::ASN1_INTEGER) { + return false; + } + $this->_decodeLength($temp); + list(, $iterationCount) = unpack('N', str_pad($temp, 4, chr(0), STR_PAD_LEFT)); + $this->_string_shift($key); // assume it's an octet string + $length = $this->_decodeLength($key); + if (strlen($key) != $length) { + return false; + } + + $crypto = new DES(); + $crypto->setPassword($this->password, 'pbkdf1', 'md5', $salt, $iterationCount); + $key = $crypto->decrypt($key); + if ($key === false) { + return false; + } + return $this->_parseKey($key, self::PRIVATE_FORMAT_PKCS1); + default: + return false; + } + /* intended for keys for which OpenSSL's asn1parse returns the following: + + 0:d=0 hl=4 l= 290 cons: SEQUENCE + 4:d=1 hl=2 l= 13 cons: SEQUENCE + 6:d=2 hl=2 l= 9 prim: OBJECT :rsaEncryption + 17:d=2 hl=2 l= 0 prim: NULL + 19:d=1 hl=4 l= 271 prim: BIT STRING */ + $tag = ord($this->_string_shift($key)); // skip over the BIT STRING / OCTET STRING tag + $this->_decodeLength($key); // skip over the BIT STRING / OCTET STRING length + // "The initial octet shall encode, as an unsigned binary integer wtih bit 1 as the least significant bit, the number of + // unused bits in the final subsequent octet. The number shall be in the range zero to seven." + // -- http://www.itu.int/ITU-T/studygroups/com17/languages/X.690-0207.pdf (section 8.6.2.2) + if ($tag == self::ASN1_BITSTRING) { + $this->_string_shift($key); + } + if (ord($this->_string_shift($key)) != self::ASN1_SEQUENCE) { + return false; + } + if ($this->_decodeLength($key) != strlen($key)) { + return false; + } + $tag = ord($this->_string_shift($key)); + } + if ($tag != self::ASN1_INTEGER) { + return false; + } + + $length = $this->_decodeLength($key); + $temp = $this->_string_shift($key, $length); + if (strlen($temp) != 1 || ord($temp) > 2) { + $components['modulus'] = new BigInteger($temp, 256); + $this->_string_shift($key); // skip over self::ASN1_INTEGER + $length = $this->_decodeLength($key); + $components[$type == self::PUBLIC_FORMAT_PKCS1 ? 'publicExponent' : 'privateExponent'] = new BigInteger($this->_string_shift($key, $length), 256); + + return $components; + } + if (ord($this->_string_shift($key)) != self::ASN1_INTEGER) { + return false; + } + $length = $this->_decodeLength($key); + $components['modulus'] = new BigInteger($this->_string_shift($key, $length), 256); + $this->_string_shift($key); + $length = $this->_decodeLength($key); + $components['publicExponent'] = new BigInteger($this->_string_shift($key, $length), 256); + $this->_string_shift($key); + $length = $this->_decodeLength($key); + $components['privateExponent'] = new BigInteger($this->_string_shift($key, $length), 256); + $this->_string_shift($key); + $length = $this->_decodeLength($key); + $components['primes'] = array(1 => new BigInteger($this->_string_shift($key, $length), 256)); + $this->_string_shift($key); + $length = $this->_decodeLength($key); + $components['primes'][] = new BigInteger($this->_string_shift($key, $length), 256); + $this->_string_shift($key); + $length = $this->_decodeLength($key); + $components['exponents'] = array(1 => new BigInteger($this->_string_shift($key, $length), 256)); + $this->_string_shift($key); + $length = $this->_decodeLength($key); + $components['exponents'][] = new BigInteger($this->_string_shift($key, $length), 256); + $this->_string_shift($key); + $length = $this->_decodeLength($key); + $components['coefficients'] = array(2 => new BigInteger($this->_string_shift($key, $length), 256)); + + if (!empty($key)) { + if (ord($this->_string_shift($key)) != self::ASN1_SEQUENCE) { + return false; + } + $this->_decodeLength($key); + while (!empty($key)) { + if (ord($this->_string_shift($key)) != self::ASN1_SEQUENCE) { + return false; + } + $this->_decodeLength($key); + $key = substr($key, 1); + $length = $this->_decodeLength($key); + $components['primes'][] = new BigInteger($this->_string_shift($key, $length), 256); + $this->_string_shift($key); + $length = $this->_decodeLength($key); + $components['exponents'][] = new BigInteger($this->_string_shift($key, $length), 256); + $this->_string_shift($key); + $length = $this->_decodeLength($key); + $components['coefficients'][] = new BigInteger($this->_string_shift($key, $length), 256); + } + } + + return $components; + case self::PUBLIC_FORMAT_OPENSSH: + $parts = explode(' ', $key, 3); + + $key = isset($parts[1]) ? base64_decode($parts[1]) : false; + if ($key === false) { + return false; + } + + $comment = isset($parts[2]) ? $parts[2] : false; + + $cleanup = substr($key, 0, 11) == "\0\0\0\7ssh-rsa"; + + if (strlen($key) <= 4) { + return false; + } + extract(unpack('Nlength', $this->_string_shift($key, 4))); + $publicExponent = new BigInteger($this->_string_shift($key, $length), -256); + if (strlen($key) <= 4) { + return false; + } + extract(unpack('Nlength', $this->_string_shift($key, 4))); + $modulus = new BigInteger($this->_string_shift($key, $length), -256); + + if ($cleanup && strlen($key)) { + if (strlen($key) <= 4) { + return false; + } + extract(unpack('Nlength', $this->_string_shift($key, 4))); + $realModulus = new BigInteger($this->_string_shift($key, $length), -256); + return strlen($key) ? false : array( + 'modulus' => $realModulus, + 'publicExponent' => $modulus, + 'comment' => $comment + ); + } else { + return strlen($key) ? false : array( + 'modulus' => $modulus, + 'publicExponent' => $publicExponent, + 'comment' => $comment + ); + } + // http://www.w3.org/TR/xmldsig-core/#sec-RSAKeyValue + // http://en.wikipedia.org/wiki/XML_Signature + case self::PRIVATE_FORMAT_XML: + case self::PUBLIC_FORMAT_XML: + if (!extension_loaded('xml')) { + return false; + } + + $this->components = array(); + + $xml = xml_parser_create('UTF-8'); + xml_set_object($xml, $this); + xml_set_element_handler($xml, '_start_element_handler', '_stop_element_handler'); + xml_set_character_data_handler($xml, '_data_handler'); + // add to account for "dangling" tags like ... that are sometimes added + if (!xml_parse($xml, '' . $key . '')) { + xml_parser_free($xml); + unset($xml); + return false; + } + + xml_parser_free($xml); + unset($xml); + + return isset($this->components['modulus']) && isset($this->components['publicExponent']) ? $this->components : false; + // see PuTTY's SSHPUBK.C and https://tartarus.org/~simon/putty-snapshots/htmldoc/AppendixC.html + case self::PRIVATE_FORMAT_PUTTY: + $components = array(); + $key = preg_split('#\r\n|\r|\n#', $key); + if ($this->_string_shift($key[0], strlen('PuTTY-User-Key-File-')) != 'PuTTY-User-Key-File-') { + return false; + } + $version = (int) $this->_string_shift($key[0], 3); // should be either "2: " or "3: 0" prior to int casting + if ($version != 2 && $version != 3) { + return false; + } + $type = rtrim($key[0]); + if ($type != 'ssh-rsa') { + return false; + } + $encryption = trim(preg_replace('#Encryption: (.+)#', '$1', $key[1])); + $comment = trim(preg_replace('#Comment: (.+)#', '$1', $key[2])); + + $publicLength = trim(preg_replace('#Public-Lines: (\d+)#', '$1', $key[3])); + $public = base64_decode(implode('', array_map('trim', array_slice($key, 4, $publicLength)))); + $public = substr($public, 11); + extract(unpack('Nlength', $this->_string_shift($public, 4))); + $components['publicExponent'] = new BigInteger($this->_string_shift($public, $length), -256); + extract(unpack('Nlength', $this->_string_shift($public, 4))); + $components['modulus'] = new BigInteger($this->_string_shift($public, $length), -256); + + $offset = $publicLength + 4; + switch ($encryption) { + case 'aes256-cbc': + $crypto = new AES(); + switch ($version) { + case 3: + if (!function_exists('sodium_crypto_pwhash')) { + return false; + } + $flavour = trim(preg_replace('#Key-Derivation: (.*)#', '$1', $key[$offset++])); + switch ($flavour) { + case 'Argon2i': + $flavour = SODIUM_CRYPTO_PWHASH_ALG_ARGON2I13; + break; + case 'Argon2id': + $flavour = SODIUM_CRYPTO_PWHASH_ALG_ARGON2ID13; + break; + default: + return false; + } + $memory = trim(preg_replace('#Argon2-Memory: (\d+)#', '$1', $key[$offset++])); + $passes = trim(preg_replace('#Argon2-Passes: (\d+)#', '$1', $key[$offset++])); + $parallelism = trim(preg_replace('#Argon2-Parallelism: (\d+)#', '$1', $key[$offset++])); + $salt = pack('H*', trim(preg_replace('#Argon2-Salt: ([0-9a-f]+)#', '$1', $key[$offset++]))); + + $length = 80; // keylen + ivlen + mac_keylen + $temp = sodium_crypto_pwhash($length, $this->password, $salt, $passes, $memory << 10, $flavour); + + $symkey = substr($temp, 0, 32); + $symiv = substr($temp, 32, 16); + break; + case 2: + $symkey = ''; + $sequence = 0; + while (strlen($symkey) < 32) { + $temp = pack('Na*', $sequence++, $this->password); + $symkey.= pack('H*', sha1($temp)); + } + $symkey = substr($symkey, 0, 32); + $symiv = str_repeat("\0", 16); + } + } + + $privateLength = trim(preg_replace('#Private-Lines: (\d+)#', '$1', $key[$offset++])); + $private = base64_decode(implode('', array_map('trim', array_slice($key, $offset, $privateLength)))); + + if ($encryption != 'none') { + $crypto->setKey($symkey); + $crypto->setIV($symiv); + $crypto->disablePadding(); + $private = $crypto->decrypt($private); + if ($private === false) { + return false; + } + } + + extract(unpack('Nlength', $this->_string_shift($private, 4))); + if (strlen($private) < $length) { + return false; + } + $components['privateExponent'] = new BigInteger($this->_string_shift($private, $length), -256); + extract(unpack('Nlength', $this->_string_shift($private, 4))); + if (strlen($private) < $length) { + return false; + } + $components['primes'] = array(1 => new BigInteger($this->_string_shift($private, $length), -256)); + extract(unpack('Nlength', $this->_string_shift($private, 4))); + if (strlen($private) < $length) { + return false; + } + $components['primes'][] = new BigInteger($this->_string_shift($private, $length), -256); + + $temp = $components['primes'][1]->subtract($this->one); + $components['exponents'] = array(1 => $components['publicExponent']->modInverse($temp)); + $temp = $components['primes'][2]->subtract($this->one); + $components['exponents'][] = $components['publicExponent']->modInverse($temp); + + extract(unpack('Nlength', $this->_string_shift($private, 4))); + if (strlen($private) < $length) { + return false; + } + $components['coefficients'] = array(2 => new BigInteger($this->_string_shift($private, $length), -256)); + + return $components; + case self::PRIVATE_FORMAT_OPENSSH: + $components = array(); + $decoded = $this->_extractBER($key); + $magic = $this->_string_shift($decoded, 15); + if ($magic !== "openssh-key-v1\0") { + return false; + } + extract(unpack('Nlength', $this->_string_shift($decoded, 4))); + if (strlen($decoded) < $length) { + return false; + } + $ciphername = $this->_string_shift($decoded, $length); + extract(unpack('Nlength', $this->_string_shift($decoded, 4))); + if (strlen($decoded) < $length) { + return false; + } + $kdfname = $this->_string_shift($decoded, $length); + extract(unpack('Nlength', $this->_string_shift($decoded, 4))); + if (strlen($decoded) < $length) { + return false; + } + $kdfoptions = $this->_string_shift($decoded, $length); + extract(unpack('Nnumkeys', $this->_string_shift($decoded, 4))); + if ($numkeys != 1 || ($ciphername != 'none' && $kdfname != 'bcrypt')) { + return false; + } + switch ($ciphername) { + case 'none': + break; + case 'aes256-ctr': + extract(unpack('Nlength', $this->_string_shift($kdfoptions, 4))); + if (strlen($kdfoptions) < $length) { + return false; + } + $salt = $this->_string_shift($kdfoptions, $length); + extract(unpack('Nrounds', $this->_string_shift($kdfoptions, 4))); + $crypto = new AES(AES::MODE_CTR); + $crypto->disablePadding(); + if (!$crypto->setPassword($this->password, 'bcrypt', $salt, $rounds, 32)) { + return false; + } + break; + default: + return false; + } + extract(unpack('Nlength', $this->_string_shift($decoded, 4))); + if (strlen($decoded) < $length) { + return false; + } + $publicKey = $this->_string_shift($decoded, $length); + extract(unpack('Nlength', $this->_string_shift($decoded, 4))); + if (strlen($decoded) < $length) { + return false; + } + + if ($this->_string_shift($publicKey, 11) !== "\0\0\0\7ssh-rsa") { + return false; + } + + $paddedKey = $this->_string_shift($decoded, $length); + if (isset($crypto)) { + $paddedKey = $crypto->decrypt($paddedKey); + } + + $checkint1 = $this->_string_shift($paddedKey, 4); + $checkint2 = $this->_string_shift($paddedKey, 4); + if (strlen($checkint1) != 4 || $checkint1 !== $checkint2) { + return false; + } + + if ($this->_string_shift($paddedKey, 11) !== "\0\0\0\7ssh-rsa") { + return false; + } + + $values = array( + &$components['modulus'], + &$components['publicExponent'], + &$components['privateExponent'], + &$components['coefficients'][2], + &$components['primes'][1], + &$components['primes'][2] + ); + + foreach ($values as &$value) { + extract(unpack('Nlength', $this->_string_shift($paddedKey, 4))); + if (strlen($paddedKey) < $length) { + return false; + } + $value = new BigInteger($this->_string_shift($paddedKey, $length), -256); + } + + extract(unpack('Nlength', $this->_string_shift($paddedKey, 4))); + if (strlen($paddedKey) < $length) { + return false; + } + $components['comment'] = $this->_string_shift($decoded, $length); + + $temp = $components['primes'][1]->subtract($this->one); + $components['exponents'] = array(1 => $components['publicExponent']->modInverse($temp)); + $temp = $components['primes'][2]->subtract($this->one); + $components['exponents'][] = $components['publicExponent']->modInverse($temp); + + return $components; + } + + return false; + } + + /** + * Returns the key size + * + * More specifically, this returns the size of the modulo in bits. + * + * @access public + * @return int + */ + function getSize() + { + return !isset($this->modulus) ? 0 : strlen($this->modulus->toBits()); + } + + /** + * Start Element Handler + * + * Called by xml_set_element_handler() + * + * @access private + * @param resource $parser + * @param string $name + * @param array $attribs + */ + function _start_element_handler($parser, $name, $attribs) + { + //$name = strtoupper($name); + switch ($name) { + case 'MODULUS': + $this->current = &$this->components['modulus']; + break; + case 'EXPONENT': + $this->current = &$this->components['publicExponent']; + break; + case 'P': + $this->current = &$this->components['primes'][1]; + break; + case 'Q': + $this->current = &$this->components['primes'][2]; + break; + case 'DP': + $this->current = &$this->components['exponents'][1]; + break; + case 'DQ': + $this->current = &$this->components['exponents'][2]; + break; + case 'INVERSEQ': + $this->current = &$this->components['coefficients'][2]; + break; + case 'D': + $this->current = &$this->components['privateExponent']; + } + $this->current = ''; + } + + /** + * Stop Element Handler + * + * Called by xml_set_element_handler() + * + * @access private + * @param resource $parser + * @param string $name + */ + function _stop_element_handler($parser, $name) + { + if (isset($this->current)) { + $this->current = new BigInteger(base64_decode($this->current), 256); + unset($this->current); + } + } + + /** + * Data Handler + * + * Called by xml_set_character_data_handler() + * + * @access private + * @param resource $parser + * @param string $data + */ + function _data_handler($parser, $data) + { + if (!isset($this->current) || is_object($this->current)) { + return; + } + $this->current.= trim($data); + } + + /** + * Loads a public or private key + * + * Returns true on success and false on failure (ie. an incorrect password was provided or the key was malformed) + * + * @access public + * @param string|RSA|array $key + * @param bool|int $type optional + * @return bool + */ + function loadKey($key, $type = false) + { + if ($key instanceof RSA) { + $this->privateKeyFormat = $key->privateKeyFormat; + $this->publicKeyFormat = $key->publicKeyFormat; + $this->k = $key->k; + $this->hLen = $key->hLen; + $this->sLen = $key->sLen; + $this->mgfHLen = $key->mgfHLen; + $this->encryptionMode = $key->encryptionMode; + $this->signatureMode = $key->signatureMode; + $this->password = $key->password; + $this->configFile = $key->configFile; + $this->comment = $key->comment; + + if (is_object($key->hash)) { + $this->hash = new Hash($key->hash->getHash()); + } + if (is_object($key->mgfHash)) { + $this->mgfHash = new Hash($key->mgfHash->getHash()); + } + + if (is_object($key->modulus)) { + $this->modulus = $key->modulus->copy(); + } + if (is_object($key->exponent)) { + $this->exponent = $key->exponent->copy(); + } + if (is_object($key->publicExponent)) { + $this->publicExponent = $key->publicExponent->copy(); + } + + $this->primes = array(); + $this->exponents = array(); + $this->coefficients = array(); + + foreach ($this->primes as $prime) { + $this->primes[] = $prime->copy(); + } + foreach ($this->exponents as $exponent) { + $this->exponents[] = $exponent->copy(); + } + foreach ($this->coefficients as $coefficient) { + $this->coefficients[] = $coefficient->copy(); + } + + return true; + } + + if ($type === false) { + $types = array( + self::PUBLIC_FORMAT_RAW, + self::PRIVATE_FORMAT_PKCS1, + self::PRIVATE_FORMAT_XML, + self::PRIVATE_FORMAT_PUTTY, + self::PUBLIC_FORMAT_OPENSSH, + self::PRIVATE_FORMAT_OPENSSH + ); + foreach ($types as $type) { + $components = $this->_parseKey($key, $type); + if ($components !== false) { + break; + } + } + } else { + $components = $this->_parseKey($key, $type); + } + + if ($components === false) { + $this->comment = null; + $this->modulus = null; + $this->k = null; + $this->exponent = null; + $this->primes = null; + $this->exponents = null; + $this->coefficients = null; + $this->publicExponent = null; + + return false; + } + + if (isset($components['comment']) && $components['comment'] !== false) { + $this->comment = $components['comment']; + } + $this->modulus = $components['modulus']; + $this->k = strlen($this->modulus->toBytes()); + $this->exponent = isset($components['privateExponent']) ? $components['privateExponent'] : $components['publicExponent']; + if (isset($components['primes'])) { + $this->primes = $components['primes']; + $this->exponents = $components['exponents']; + $this->coefficients = $components['coefficients']; + $this->publicExponent = $components['publicExponent']; + } else { + $this->primes = array(); + $this->exponents = array(); + $this->coefficients = array(); + $this->publicExponent = false; + } + + switch ($type) { + case self::PUBLIC_FORMAT_OPENSSH: + case self::PUBLIC_FORMAT_RAW: + $this->setPublicKey(); + break; + case self::PRIVATE_FORMAT_PKCS1: + switch (true) { + case strpos($key, '-BEGIN PUBLIC KEY-') !== false: + case strpos($key, '-BEGIN RSA PUBLIC KEY-') !== false: + $this->setPublicKey(); + } + } + + return true; + } + + /** + * Sets the password + * + * Private keys can be encrypted with a password. To unset the password, pass in the empty string or false. + * Or rather, pass in $password such that empty($password) && !is_string($password) is true. + * + * @see self::createKey() + * @see self::loadKey() + * @access public + * @param string $password + */ + function setPassword($password = false) + { + $this->password = $password; + } + + /** + * Defines the public key + * + * Some private key formats define the public exponent and some don't. Those that don't define it are problematic when + * used in certain contexts. For example, in SSH-2, RSA authentication works by sending the public key along with a + * message signed by the private key to the server. The SSH-2 server looks the public key up in an index of public keys + * and if it's present then proceeds to verify the signature. Problem is, if your private key doesn't include the public + * exponent this won't work unless you manually add the public exponent. phpseclib tries to guess if the key being used + * is the public key but in the event that it guesses incorrectly you might still want to explicitly set the key as being + * public. + * + * Do note that when a new key is loaded the index will be cleared. + * + * Returns true on success, false on failure + * + * @see self::getPublicKey() + * @access public + * @param string $key optional + * @param int $type optional + * @return bool + */ + function setPublicKey($key = false, $type = false) + { + // if a public key has already been loaded return false + if (!empty($this->publicExponent)) { + return false; + } + + if ($key === false && !empty($this->modulus)) { + $this->publicExponent = $this->exponent; + return true; + } + + if ($type === false) { + $types = array( + self::PUBLIC_FORMAT_RAW, + self::PUBLIC_FORMAT_PKCS1, + self::PUBLIC_FORMAT_XML, + self::PUBLIC_FORMAT_OPENSSH + ); + foreach ($types as $type) { + $components = $this->_parseKey($key, $type); + if ($components !== false) { + break; + } + } + } else { + $components = $this->_parseKey($key, $type); + } + + if ($components === false) { + return false; + } + + if (empty($this->modulus) || !$this->modulus->equals($components['modulus'])) { + $this->modulus = $components['modulus']; + $this->exponent = $this->publicExponent = $components['publicExponent']; + return true; + } + + $this->publicExponent = $components['publicExponent']; + + return true; + } + + /** + * Defines the private key + * + * If phpseclib guessed a private key was a public key and loaded it as such it might be desirable to force + * phpseclib to treat the key as a private key. This function will do that. + * + * Do note that when a new key is loaded the index will be cleared. + * + * Returns true on success, false on failure + * + * @see self::getPublicKey() + * @access public + * @param string $key optional + * @param int $type optional + * @return bool + */ + function setPrivateKey($key = false, $type = false) + { + if ($key === false && !empty($this->publicExponent)) { + $this->publicExponent = false; + return true; + } + + $rsa = new RSA(); + if (!$rsa->loadKey($key, $type)) { + return false; + } + $rsa->publicExponent = false; + + // don't overwrite the old key if the new key is invalid + $this->loadKey($rsa); + return true; + } + + /** + * Returns the public key + * + * The public key is only returned under two circumstances - if the private key had the public key embedded within it + * or if the public key was set via setPublicKey(). If the currently loaded key is supposed to be the public key this + * function won't return it since this library, for the most part, doesn't distinguish between public and private keys. + * + * @see self::getPublicKey() + * @access public + * @param int $type optional + */ + function getPublicKey($type = self::PUBLIC_FORMAT_PKCS8) + { + if (empty($this->modulus) || empty($this->publicExponent)) { + return false; + } + + $oldFormat = $this->publicKeyFormat; + $this->publicKeyFormat = $type; + $temp = $this->_convertPublicKey($this->modulus, $this->publicExponent); + $this->publicKeyFormat = $oldFormat; + return $temp; + } + + /** + * Returns the public key's fingerprint + * + * The public key's fingerprint is returned, which is equivalent to running `ssh-keygen -lf rsa.pub`. If there is + * no public key currently loaded, false is returned. + * Example output (md5): "c1:b1:30:29:d7:b8:de:6c:97:77:10:d7:46:41:63:87" (as specified by RFC 4716) + * + * @access public + * @param string $algorithm The hashing algorithm to be used. Valid options are 'md5' and 'sha256'. False is returned + * for invalid values. + * @return mixed + */ + function getPublicKeyFingerprint($algorithm = 'md5') + { + if (empty($this->modulus) || empty($this->publicExponent)) { + return false; + } + + $modulus = $this->modulus->toBytes(true); + $publicExponent = $this->publicExponent->toBytes(true); + + $RSAPublicKey = pack('Na*Na*Na*', strlen('ssh-rsa'), 'ssh-rsa', strlen($publicExponent), $publicExponent, strlen($modulus), $modulus); + + switch ($algorithm) { + case 'sha256': + $hash = new Hash('sha256'); + $base = base64_encode($hash->hash($RSAPublicKey)); + return substr($base, 0, strlen($base) - 1); + case 'md5': + return substr(chunk_split(md5($RSAPublicKey), 2, ':'), 0, -1); + default: + return false; + } + } + + /** + * Returns the private key + * + * The private key is only returned if the currently loaded key contains the constituent prime numbers. + * + * @see self::getPublicKey() + * @access public + * @param int $type optional + * @return mixed + */ + function getPrivateKey($type = self::PUBLIC_FORMAT_PKCS1) + { + if (empty($this->primes)) { + return false; + } + + $oldFormat = $this->privateKeyFormat; + $this->privateKeyFormat = $type; + $temp = $this->_convertPrivateKey($this->modulus, $this->publicExponent, $this->exponent, $this->primes, $this->exponents, $this->coefficients); + $this->privateKeyFormat = $oldFormat; + return $temp; + } + + /** + * Returns a minimalistic private key + * + * Returns the private key without the prime number constituants. Structurally identical to a public key that + * hasn't been set as the public key + * + * @see self::getPrivateKey() + * @access private + * @param int $mode optional + */ + function _getPrivatePublicKey($mode = self::PUBLIC_FORMAT_PKCS8) + { + if (empty($this->modulus) || empty($this->exponent)) { + return false; + } + + $oldFormat = $this->publicKeyFormat; + $this->publicKeyFormat = $mode; + $temp = $this->_convertPublicKey($this->modulus, $this->exponent); + $this->publicKeyFormat = $oldFormat; + return $temp; + } + + /** + * __toString() magic method + * + * @access public + * @return string + */ + function __toString() + { + $key = $this->getPrivateKey($this->privateKeyFormat); + if ($key !== false) { + return $key; + } + $key = $this->_getPrivatePublicKey($this->publicKeyFormat); + return $key !== false ? $key : ''; + } + + /** + * __clone() magic method + * + * @access public + * @return Crypt_RSA + */ + function __clone() + { + $key = new RSA(); + $key->loadKey($this); + return $key; + } + + /** + * Generates the smallest and largest numbers requiring $bits bits + * + * @access private + * @param int $bits + * @return array + */ + function _generateMinMax($bits) + { + $bytes = $bits >> 3; + $min = str_repeat(chr(0), $bytes); + $max = str_repeat(chr(0xFF), $bytes); + $msb = $bits & 7; + if ($msb) { + $min = chr(1 << ($msb - 1)) . $min; + $max = chr((1 << $msb) - 1) . $max; + } else { + $min[0] = chr(0x80); + } + + return array( + 'min' => new BigInteger($min, 256), + 'max' => new BigInteger($max, 256) + ); + } + + /** + * DER-decode the length + * + * DER supports lengths up to (2**8)**127, however, we'll only support lengths up to (2**8)**4. See + * {@link http://itu.int/ITU-T/studygroups/com17/languages/X.690-0207.pdf#p=13 X.690 paragraph 8.1.3} for more information. + * + * @access private + * @param string $string + * @return int + */ + function _decodeLength(&$string) + { + $length = ord($this->_string_shift($string)); + if ($length & 0x80) { // definite length, long form + $length&= 0x7F; + $temp = $this->_string_shift($string, $length); + list(, $length) = unpack('N', substr(str_pad($temp, 4, chr(0), STR_PAD_LEFT), -4)); + } + return $length; + } + + /** + * DER-encode the length + * + * DER supports lengths up to (2**8)**127, however, we'll only support lengths up to (2**8)**4. See + * {@link http://itu.int/ITU-T/studygroups/com17/languages/X.690-0207.pdf#p=13 X.690 paragraph 8.1.3} for more information. + * + * @access private + * @param int $length + * @return string + */ + function _encodeLength($length) + { + if ($length <= 0x7F) { + return chr($length); + } + + $temp = ltrim(pack('N', $length), chr(0)); + return pack('Ca*', 0x80 | strlen($temp), $temp); + } + + /** + * String Shift + * + * Inspired by array_shift + * + * @param string $string + * @param int $index + * @return string + * @access private + */ + function _string_shift(&$string, $index = 1) + { + $substr = substr($string, 0, $index); + $string = substr($string, $index); + return $substr; + } + + /** + * Determines the private key format + * + * @see self::createKey() + * @access public + * @param int $format + */ + function setPrivateKeyFormat($format) + { + $this->privateKeyFormat = $format; + } + + /** + * Determines the public key format + * + * @see self::createKey() + * @access public + * @param int $format + */ + function setPublicKeyFormat($format) + { + $this->publicKeyFormat = $format; + } + + /** + * Determines which hashing function should be used + * + * Used with signature production / verification and (if the encryption mode is self::ENCRYPTION_OAEP) encryption and + * decryption. If $hash isn't supported, sha1 is used. + * + * @access public + * @param string $hash + */ + function setHash($hash) + { + // \phpseclib\Crypt\Hash supports algorithms that PKCS#1 doesn't support. md5-96 and sha1-96, for example. + switch ($hash) { + case 'md2': + case 'md5': + case 'sha1': + case 'sha256': + case 'sha384': + case 'sha512': + $this->hash = new Hash($hash); + $this->hashName = $hash; + break; + default: + $this->hash = new Hash('sha1'); + $this->hashName = 'sha1'; + } + $this->hLen = $this->hash->getLength(); + } + + /** + * Determines which hashing function should be used for the mask generation function + * + * The mask generation function is used by self::ENCRYPTION_OAEP and self::SIGNATURE_PSS and although it's + * best if Hash and MGFHash are set to the same thing this is not a requirement. + * + * @access public + * @param string $hash + */ + function setMGFHash($hash) + { + // \phpseclib\Crypt\Hash supports algorithms that PKCS#1 doesn't support. md5-96 and sha1-96, for example. + switch ($hash) { + case 'md2': + case 'md5': + case 'sha1': + case 'sha256': + case 'sha384': + case 'sha512': + $this->mgfHash = new Hash($hash); + break; + default: + $this->mgfHash = new Hash('sha1'); + } + $this->mgfHLen = $this->mgfHash->getLength(); + } + + /** + * Determines the salt length + * + * To quote from {@link http://tools.ietf.org/html/rfc3447#page-38 RFC3447#page-38}: + * + * Typical salt lengths in octets are hLen (the length of the output + * of the hash function Hash) and 0. + * + * @access public + * @param int $sLen + */ + function setSaltLength($sLen) + { + $this->sLen = $sLen; + } + + /** + * Integer-to-Octet-String primitive + * + * See {@link http://tools.ietf.org/html/rfc3447#section-4.1 RFC3447#section-4.1}. + * + * @access private + * @param \phpseclib\Math\BigInteger $x + * @param int $xLen + * @return string + */ + function _i2osp($x, $xLen) + { + $x = $x->toBytes(); + if (strlen($x) > $xLen) { + user_error('Integer too large'); + return false; + } + return str_pad($x, $xLen, chr(0), STR_PAD_LEFT); + } + + /** + * Octet-String-to-Integer primitive + * + * See {@link http://tools.ietf.org/html/rfc3447#section-4.2 RFC3447#section-4.2}. + * + * @access private + * @param int|string|resource $x + * @return \phpseclib\Math\BigInteger + */ + function _os2ip($x) + { + return new BigInteger($x, 256); + } + + /** + * Exponentiate with or without Chinese Remainder Theorem + * + * See {@link http://tools.ietf.org/html/rfc3447#section-5.1.1 RFC3447#section-5.1.2}. + * + * @access private + * @param \phpseclib\Math\BigInteger $x + * @return \phpseclib\Math\BigInteger + */ + function _exponentiate($x) + { + switch (true) { + case empty($this->primes): + case $this->primes[1]->equals($this->zero): + case empty($this->coefficients): + case $this->coefficients[2]->equals($this->zero): + case empty($this->exponents): + case $this->exponents[1]->equals($this->zero): + return $x->modPow($this->exponent, $this->modulus); + } + + $num_primes = count($this->primes); + + if (defined('CRYPT_RSA_DISABLE_BLINDING')) { + $m_i = array( + 1 => $x->modPow($this->exponents[1], $this->primes[1]), + 2 => $x->modPow($this->exponents[2], $this->primes[2]) + ); + $h = $m_i[1]->subtract($m_i[2]); + $h = $h->multiply($this->coefficients[2]); + list(, $h) = $h->divide($this->primes[1]); + $m = $m_i[2]->add($h->multiply($this->primes[2])); + + $r = $this->primes[1]; + for ($i = 3; $i <= $num_primes; $i++) { + $m_i = $x->modPow($this->exponents[$i], $this->primes[$i]); + + $r = $r->multiply($this->primes[$i - 1]); + + $h = $m_i->subtract($m); + $h = $h->multiply($this->coefficients[$i]); + list(, $h) = $h->divide($this->primes[$i]); + + $m = $m->add($r->multiply($h)); + } + } else { + $smallest = $this->primes[1]; + for ($i = 2; $i <= $num_primes; $i++) { + if ($smallest->compare($this->primes[$i]) > 0) { + $smallest = $this->primes[$i]; + } + } + + $one = new BigInteger(1); + + $r = $one->random($one, $smallest->subtract($one)); + + $m_i = array( + 1 => $this->_blind($x, $r, 1), + 2 => $this->_blind($x, $r, 2) + ); + $h = $m_i[1]->subtract($m_i[2]); + $h = $h->multiply($this->coefficients[2]); + list(, $h) = $h->divide($this->primes[1]); + $m = $m_i[2]->add($h->multiply($this->primes[2])); + + $r = $this->primes[1]; + for ($i = 3; $i <= $num_primes; $i++) { + $m_i = $this->_blind($x, $r, $i); + + $r = $r->multiply($this->primes[$i - 1]); + + $h = $m_i->subtract($m); + $h = $h->multiply($this->coefficients[$i]); + list(, $h) = $h->divide($this->primes[$i]); + + $m = $m->add($r->multiply($h)); + } + } + + return $m; + } + + /** + * Performs RSA Blinding + * + * Protects against timing attacks by employing RSA Blinding. + * Returns $x->modPow($this->exponents[$i], $this->primes[$i]) + * + * @access private + * @param \phpseclib\Math\BigInteger $x + * @param \phpseclib\Math\BigInteger $r + * @param int $i + * @return \phpseclib\Math\BigInteger + */ + function _blind($x, $r, $i) + { + $x = $x->multiply($r->modPow($this->publicExponent, $this->primes[$i])); + $x = $x->modPow($this->exponents[$i], $this->primes[$i]); + + $r = $r->modInverse($this->primes[$i]); + $x = $x->multiply($r); + list(, $x) = $x->divide($this->primes[$i]); + + return $x; + } + + /** + * Performs blinded RSA equality testing + * + * Protects against a particular type of timing attack described. + * + * See {@link http://codahale.com/a-lesson-in-timing-attacks/ A Lesson In Timing Attacks (or, Don't use MessageDigest.isEquals)} + * + * Thanks for the heads up singpolyma! + * + * @access private + * @param string $x + * @param string $y + * @return bool + */ + function _equals($x, $y) + { + if (function_exists('hash_equals')) { + return hash_equals($x, $y); + } + + if (strlen($x) != strlen($y)) { + return false; + } + + $result = "\0"; + $x^= $y; + for ($i = 0; $i < strlen($x); $i++) { + $result|= $x[$i]; + } + + return $result === "\0"; + } + + /** + * RSAEP + * + * See {@link http://tools.ietf.org/html/rfc3447#section-5.1.1 RFC3447#section-5.1.1}. + * + * @access private + * @param \phpseclib\Math\BigInteger $m + * @return \phpseclib\Math\BigInteger + */ + function _rsaep($m) + { + if ($m->compare($this->zero) < 0 || $m->compare($this->modulus) > 0) { + user_error('Message representative out of range'); + return false; + } + return $this->_exponentiate($m); + } + + /** + * RSADP + * + * See {@link http://tools.ietf.org/html/rfc3447#section-5.1.2 RFC3447#section-5.1.2}. + * + * @access private + * @param \phpseclib\Math\BigInteger $c + * @return \phpseclib\Math\BigInteger + */ + function _rsadp($c) + { + if ($c->compare($this->zero) < 0 || $c->compare($this->modulus) > 0) { + user_error('Ciphertext representative out of range'); + return false; + } + return $this->_exponentiate($c); + } + + /** + * RSASP1 + * + * See {@link http://tools.ietf.org/html/rfc3447#section-5.2.1 RFC3447#section-5.2.1}. + * + * @access private + * @param \phpseclib\Math\BigInteger $m + * @return \phpseclib\Math\BigInteger + */ + function _rsasp1($m) + { + if ($m->compare($this->zero) < 0 || $m->compare($this->modulus) > 0) { + user_error('Message representative out of range'); + return false; + } + return $this->_exponentiate($m); + } + + /** + * RSAVP1 + * + * See {@link http://tools.ietf.org/html/rfc3447#section-5.2.2 RFC3447#section-5.2.2}. + * + * @access private + * @param \phpseclib\Math\BigInteger $s + * @return \phpseclib\Math\BigInteger + */ + function _rsavp1($s) + { + if ($s->compare($this->zero) < 0 || $s->compare($this->modulus) > 0) { + user_error('Signature representative out of range'); + return false; + } + return $this->_exponentiate($s); + } + + /** + * MGF1 + * + * See {@link http://tools.ietf.org/html/rfc3447#appendix-B.2.1 RFC3447#appendix-B.2.1}. + * + * @access private + * @param string $mgfSeed + * @param int $maskLen + * @return string + */ + function _mgf1($mgfSeed, $maskLen) + { + // if $maskLen would yield strings larger than 4GB, PKCS#1 suggests a "Mask too long" error be output. + + $t = ''; + $count = ceil($maskLen / $this->mgfHLen); + for ($i = 0; $i < $count; $i++) { + $c = pack('N', $i); + $t.= $this->mgfHash->hash($mgfSeed . $c); + } + + return substr($t, 0, $maskLen); + } + + /** + * RSAES-OAEP-ENCRYPT + * + * See {@link http://tools.ietf.org/html/rfc3447#section-7.1.1 RFC3447#section-7.1.1} and + * {http://en.wikipedia.org/wiki/Optimal_Asymmetric_Encryption_Padding OAES}. + * + * @access private + * @param string $m + * @param string $l + * @return string + */ + function _rsaes_oaep_encrypt($m, $l = '') + { + $mLen = strlen($m); + + // Length checking + + // if $l is larger than two million terrabytes and you're using sha1, PKCS#1 suggests a "Label too long" error + // be output. + + if ($mLen > $this->k - 2 * $this->hLen - 2) { + user_error('Message too long'); + return false; + } + + // EME-OAEP encoding + + $lHash = $this->hash->hash($l); + $ps = str_repeat(chr(0), $this->k - $mLen - 2 * $this->hLen - 2); + $db = $lHash . $ps . chr(1) . $m; + $seed = Random::string($this->hLen); + $dbMask = $this->_mgf1($seed, $this->k - $this->hLen - 1); + $maskedDB = $db ^ $dbMask; + $seedMask = $this->_mgf1($maskedDB, $this->hLen); + $maskedSeed = $seed ^ $seedMask; + $em = chr(0) . $maskedSeed . $maskedDB; + + // RSA encryption + + $m = $this->_os2ip($em); + $c = $this->_rsaep($m); + $c = $this->_i2osp($c, $this->k); + + // Output the ciphertext C + + return $c; + } + + /** + * RSAES-OAEP-DECRYPT + * + * See {@link http://tools.ietf.org/html/rfc3447#section-7.1.2 RFC3447#section-7.1.2}. The fact that the error + * messages aren't distinguishable from one another hinders debugging, but, to quote from RFC3447#section-7.1.2: + * + * Note. Care must be taken to ensure that an opponent cannot + * distinguish the different error conditions in Step 3.g, whether by + * error message or timing, or, more generally, learn partial + * information about the encoded message EM. Otherwise an opponent may + * be able to obtain useful information about the decryption of the + * ciphertext C, leading to a chosen-ciphertext attack such as the one + * observed by Manger [36]. + * + * As for $l... to quote from {@link http://tools.ietf.org/html/rfc3447#page-17 RFC3447#page-17}: + * + * Both the encryption and the decryption operations of RSAES-OAEP take + * the value of a label L as input. In this version of PKCS #1, L is + * the empty string; other uses of the label are outside the scope of + * this document. + * + * @access private + * @param string $c + * @param string $l + * @return string + */ + function _rsaes_oaep_decrypt($c, $l = '') + { + // Length checking + + // if $l is larger than two million terrabytes and you're using sha1, PKCS#1 suggests a "Label too long" error + // be output. + + if (strlen($c) != $this->k || $this->k < 2 * $this->hLen + 2) { + user_error('Decryption error'); + return false; + } + + // RSA decryption + + $c = $this->_os2ip($c); + $m = $this->_rsadp($c); + if ($m === false) { + user_error('Decryption error'); + return false; + } + $em = $this->_i2osp($m, $this->k); + + // EME-OAEP decoding + + $lHash = $this->hash->hash($l); + $y = ord($em[0]); + $maskedSeed = substr($em, 1, $this->hLen); + $maskedDB = substr($em, $this->hLen + 1); + $seedMask = $this->_mgf1($maskedDB, $this->hLen); + $seed = $maskedSeed ^ $seedMask; + $dbMask = $this->_mgf1($seed, $this->k - $this->hLen - 1); + $db = $maskedDB ^ $dbMask; + $lHash2 = substr($db, 0, $this->hLen); + $m = substr($db, $this->hLen); + $hashesMatch = $this->_equals($lHash, $lHash2); + $leadingZeros = 1; + $patternMatch = 0; + $offset = 0; + for ($i = 0; $i < strlen($m); $i++) { + $patternMatch|= $leadingZeros & ($m[$i] === "\1"); + $leadingZeros&= $m[$i] === "\0"; + $offset+= $patternMatch ? 0 : 1; + } + + // we do | instead of || to avoid https://en.wikipedia.org/wiki/Short-circuit_evaluation + // to protect against timing attacks + if (!$hashesMatch | !$patternMatch) { + user_error('Decryption error'); + return false; + } + + // Output the message M + + return substr($m, $offset + 1); + } + + /** + * Raw Encryption / Decryption + * + * Doesn't use padding and is not recommended. + * + * @access private + * @param string $m + * @return string + */ + function _raw_encrypt($m) + { + $temp = $this->_os2ip($m); + $temp = $this->_rsaep($temp); + return $this->_i2osp($temp, $this->k); + } + + /** + * RSAES-PKCS1-V1_5-ENCRYPT + * + * See {@link http://tools.ietf.org/html/rfc3447#section-7.2.1 RFC3447#section-7.2.1}. + * + * @access private + * @param string $m + * @return string + */ + function _rsaes_pkcs1_v1_5_encrypt($m) + { + $mLen = strlen($m); + + // Length checking + + if ($mLen > $this->k - 11) { + user_error('Message too long'); + return false; + } + + // EME-PKCS1-v1_5 encoding + + $psLen = $this->k - $mLen - 3; + $ps = ''; + while (strlen($ps) != $psLen) { + $temp = Random::string($psLen - strlen($ps)); + $temp = str_replace("\x00", '', $temp); + $ps.= $temp; + } + $type = 2; + // see the comments of _rsaes_pkcs1_v1_5_decrypt() to understand why this is being done + if (defined('CRYPT_RSA_PKCS15_COMPAT') && (!isset($this->publicExponent) || $this->exponent !== $this->publicExponent)) { + $type = 1; + // "The padding string PS shall consist of k-3-||D|| octets. ... for block type 01, they shall have value FF" + $ps = str_repeat("\xFF", $psLen); + } + $em = chr(0) . chr($type) . $ps . chr(0) . $m; + + // RSA encryption + $m = $this->_os2ip($em); + $c = $this->_rsaep($m); + $c = $this->_i2osp($c, $this->k); + + // Output the ciphertext C + + return $c; + } + + /** + * RSAES-PKCS1-V1_5-DECRYPT + * + * See {@link http://tools.ietf.org/html/rfc3447#section-7.2.2 RFC3447#section-7.2.2}. + * + * For compatibility purposes, this function departs slightly from the description given in RFC3447. + * The reason being that RFC2313#section-8.1 (PKCS#1 v1.5) states that ciphertext's encrypted by the + * private key should have the second byte set to either 0 or 1 and that ciphertext's encrypted by the + * public key should have the second byte set to 2. In RFC3447 (PKCS#1 v2.1), the second byte is supposed + * to be 2 regardless of which key is used. For compatibility purposes, we'll just check to make sure the + * second byte is 2 or less. If it is, we'll accept the decrypted string as valid. + * + * As a consequence of this, a private key encrypted ciphertext produced with \phpseclib\Crypt\RSA may not decrypt + * with a strictly PKCS#1 v1.5 compliant RSA implementation. Public key encrypted ciphertext's should but + * not private key encrypted ciphertext's. + * + * @access private + * @param string $c + * @return string + */ + function _rsaes_pkcs1_v1_5_decrypt($c) + { + // Length checking + + if (strlen($c) != $this->k) { // or if k < 11 + user_error('Decryption error'); + return false; + } + + // RSA decryption + + $c = $this->_os2ip($c); + $m = $this->_rsadp($c); + + if ($m === false) { + user_error('Decryption error'); + return false; + } + $em = $this->_i2osp($m, $this->k); + + // EME-PKCS1-v1_5 decoding + + if (ord($em[0]) != 0 || ord($em[1]) > 2) { + user_error('Decryption error'); + return false; + } + + $ps = substr($em, 2, strpos($em, chr(0), 2) - 2); + $m = substr($em, strlen($ps) + 3); + + if (strlen($ps) < 8) { + user_error('Decryption error'); + return false; + } + + // Output M + + return $m; + } + + /** + * EMSA-PSS-ENCODE + * + * See {@link http://tools.ietf.org/html/rfc3447#section-9.1.1 RFC3447#section-9.1.1}. + * + * @access private + * @param string $m + * @param int $emBits + */ + function _emsa_pss_encode($m, $emBits) + { + // if $m is larger than two million terrabytes and you're using sha1, PKCS#1 suggests a "Label too long" error + // be output. + + $emLen = ($emBits + 1) >> 3; // ie. ceil($emBits / 8) + $sLen = $this->sLen !== null ? $this->sLen : $this->hLen; + + $mHash = $this->hash->hash($m); + if ($emLen < $this->hLen + $sLen + 2) { + user_error('Encoding error'); + return false; + } + + $salt = Random::string($sLen); + $m2 = "\0\0\0\0\0\0\0\0" . $mHash . $salt; + $h = $this->hash->hash($m2); + $ps = str_repeat(chr(0), $emLen - $sLen - $this->hLen - 2); + $db = $ps . chr(1) . $salt; + $dbMask = $this->_mgf1($h, $emLen - $this->hLen - 1); + $maskedDB = $db ^ $dbMask; + $maskedDB[0] = ~chr(0xFF << ($emBits & 7)) & $maskedDB[0]; + $em = $maskedDB . $h . chr(0xBC); + + return $em; + } + + /** + * EMSA-PSS-VERIFY + * + * See {@link http://tools.ietf.org/html/rfc3447#section-9.1.2 RFC3447#section-9.1.2}. + * + * @access private + * @param string $m + * @param string $em + * @param int $emBits + * @return string + */ + function _emsa_pss_verify($m, $em, $emBits) + { + // if $m is larger than two million terrabytes and you're using sha1, PKCS#1 suggests a "Label too long" error + // be output. + + $emLen = ($emBits + 7) >> 3; // ie. ceil($emBits / 8); + $sLen = $this->sLen !== null ? $this->sLen : $this->hLen; + + $mHash = $this->hash->hash($m); + if ($emLen < $this->hLen + $sLen + 2) { + return false; + } + + if ($em[strlen($em) - 1] != chr(0xBC)) { + return false; + } + + $maskedDB = substr($em, 0, -$this->hLen - 1); + $h = substr($em, -$this->hLen - 1, $this->hLen); + $temp = chr(0xFF << ($emBits & 7)); + if ((~$maskedDB[0] & $temp) != $temp) { + return false; + } + $dbMask = $this->_mgf1($h, $emLen - $this->hLen - 1); + $db = $maskedDB ^ $dbMask; + $db[0] = ~chr(0xFF << ($emBits & 7)) & $db[0]; + $temp = $emLen - $this->hLen - $sLen - 2; + if (substr($db, 0, $temp) != str_repeat(chr(0), $temp) || ord($db[$temp]) != 1) { + return false; + } + $salt = substr($db, $temp + 1); // should be $sLen long + $m2 = "\0\0\0\0\0\0\0\0" . $mHash . $salt; + $h2 = $this->hash->hash($m2); + return $this->_equals($h, $h2); + } + + /** + * RSASSA-PSS-SIGN + * + * See {@link http://tools.ietf.org/html/rfc3447#section-8.1.1 RFC3447#section-8.1.1}. + * + * @access private + * @param string $m + * @return string + */ + function _rsassa_pss_sign($m) + { + // EMSA-PSS encoding + + $em = $this->_emsa_pss_encode($m, 8 * $this->k - 1); + + // RSA signature + + $m = $this->_os2ip($em); + $s = $this->_rsasp1($m); + $s = $this->_i2osp($s, $this->k); + + // Output the signature S + + return $s; + } + + /** + * RSASSA-PSS-VERIFY + * + * See {@link http://tools.ietf.org/html/rfc3447#section-8.1.2 RFC3447#section-8.1.2}. + * + * @access private + * @param string $m + * @param string $s + * @return string + */ + function _rsassa_pss_verify($m, $s) + { + // Length checking + + if (strlen($s) != $this->k) { + user_error('Invalid signature'); + return false; + } + + // RSA verification + + $modBits = strlen($this->modulus->toBits()); + + $s2 = $this->_os2ip($s); + $m2 = $this->_rsavp1($s2); + if ($m2 === false) { + user_error('Invalid signature'); + return false; + } + $em = $this->_i2osp($m2, $this->k); + if ($em === false) { + user_error('Invalid signature'); + return false; + } + + // EMSA-PSS verification + + return $this->_emsa_pss_verify($m, $em, $modBits - 1); + } + + /** + * EMSA-PKCS1-V1_5-ENCODE + * + * See {@link http://tools.ietf.org/html/rfc3447#section-9.2 RFC3447#section-9.2}. + * + * @access private + * @param string $m + * @param int $emLen + * @return string + */ + function _emsa_pkcs1_v1_5_encode($m, $emLen) + { + $h = $this->hash->hash($m); + if ($h === false) { + return false; + } + + // see http://tools.ietf.org/html/rfc3447#page-43 + switch ($this->hashName) { + case 'md2': + $t = pack('H*', '3020300c06082a864886f70d020205000410'); + break; + case 'md5': + $t = pack('H*', '3020300c06082a864886f70d020505000410'); + break; + case 'sha1': + $t = pack('H*', '3021300906052b0e03021a05000414'); + break; + case 'sha256': + $t = pack('H*', '3031300d060960864801650304020105000420'); + break; + case 'sha384': + $t = pack('H*', '3041300d060960864801650304020205000430'); + break; + case 'sha512': + $t = pack('H*', '3051300d060960864801650304020305000440'); + } + $t.= $h; + $tLen = strlen($t); + + if ($emLen < $tLen + 11) { + user_error('Intended encoded message length too short'); + return false; + } + + $ps = str_repeat(chr(0xFF), $emLen - $tLen - 3); + + $em = "\0\1$ps\0$t"; + + return $em; + } + + /** + * EMSA-PKCS1-V1_5-ENCODE (without NULL) + * + * Quoting https://tools.ietf.org/html/rfc8017#page-65, + * + * "The parameters field associated with id-sha1, id-sha224, id-sha256, + * id-sha384, id-sha512, id-sha512/224, and id-sha512/256 should + * generally be omitted, but if present, it shall have a value of type + * NULL" + * + * @access private + * @param string $m + * @param int $emLen + * @return string + */ + function _emsa_pkcs1_v1_5_encode_without_null($m, $emLen) + { + $h = $this->hash->hash($m); + if ($h === false) { + return false; + } + + switch ($this->hashName) { + case 'sha1': + $t = pack('H*', '301f300706052b0e03021a0414'); + break; + case 'sha256': + $t = pack('H*', '302f300b06096086480165030402010420'); + break; + case 'sha384': + $t = pack('H*', '303f300b06096086480165030402020430'); + break; + case 'sha512': + $t = pack('H*', '304f300b06096086480165030402030440'); + break; + default: + return false; + } + $t.= $h; + $tLen = strlen($t); + + if ($emLen < $tLen + 11) { + user_error('Intended encoded message length too short'); + return false; + } + + $ps = str_repeat(chr(0xFF), $emLen - $tLen - 3); + + $em = "\0\1$ps\0$t"; + + return $em; + } + + /** + * RSASSA-PKCS1-V1_5-SIGN + * + * See {@link http://tools.ietf.org/html/rfc3447#section-8.2.1 RFC3447#section-8.2.1}. + * + * @access private + * @param string $m + * @return string + */ + function _rsassa_pkcs1_v1_5_sign($m) + { + // EMSA-PKCS1-v1_5 encoding + + $em = $this->_emsa_pkcs1_v1_5_encode($m, $this->k); + if ($em === false) { + user_error('RSA modulus too short'); + return false; + } + + // RSA signature + + $m = $this->_os2ip($em); + $s = $this->_rsasp1($m); + $s = $this->_i2osp($s, $this->k); + + // Output the signature S + + return $s; + } + + /** + * RSASSA-PKCS1-V1_5-VERIFY + * + * See {@link http://tools.ietf.org/html/rfc3447#section-8.2.2 RFC3447#section-8.2.2}. + * + * @access private + * @param string $m + * @param string $s + * @return string + */ + function _rsassa_pkcs1_v1_5_verify($m, $s) + { + // Length checking + + if (strlen($s) != $this->k) { + user_error('Invalid signature'); + return false; + } + + // RSA verification + + $s = $this->_os2ip($s); + $m2 = $this->_rsavp1($s); + if ($m2 === false) { + user_error('Invalid signature'); + return false; + } + $em = $this->_i2osp($m2, $this->k); + if ($em === false) { + user_error('Invalid signature'); + return false; + } + + // EMSA-PKCS1-v1_5 encoding + + $em2 = $this->_emsa_pkcs1_v1_5_encode($m, $this->k); + $em3 = $this->_emsa_pkcs1_v1_5_encode_without_null($m, $this->k); + + if ($em2 === false && $em3 === false) { + user_error('RSA modulus too short'); + return false; + } + + // Compare + + return ($em2 !== false && $this->_equals($em, $em2)) || + ($em3 !== false && $this->_equals($em, $em3)); + } + + /** + * Set Encryption Mode + * + * Valid values include self::ENCRYPTION_OAEP and self::ENCRYPTION_PKCS1. + * + * @access public + * @param int $mode + */ + function setEncryptionMode($mode) + { + $this->encryptionMode = $mode; + } + + /** + * Set Signature Mode + * + * Valid values include self::SIGNATURE_PSS and self::SIGNATURE_PKCS1 + * + * @access public + * @param int $mode + */ + function setSignatureMode($mode) + { + $this->signatureMode = $mode; + } + + /** + * Set public key comment. + * + * @access public + * @param string $comment + */ + function setComment($comment) + { + $this->comment = $comment; + } + + /** + * Get public key comment. + * + * @access public + * @return string + */ + function getComment() + { + return $this->comment; + } + + /** + * Encryption + * + * Both self::ENCRYPTION_OAEP and self::ENCRYPTION_PKCS1 both place limits on how long $plaintext can be. + * If $plaintext exceeds those limits it will be broken up so that it does and the resultant ciphertext's will + * be concatenated together. + * + * @see self::decrypt() + * @access public + * @param string $plaintext + * @return string + */ + function encrypt($plaintext) + { + switch ($this->encryptionMode) { + case self::ENCRYPTION_NONE: + $plaintext = str_split($plaintext, $this->k); + $ciphertext = ''; + foreach ($plaintext as $m) { + $ciphertext.= $this->_raw_encrypt($m); + } + return $ciphertext; + case self::ENCRYPTION_PKCS1: + $length = $this->k - 11; + if ($length <= 0) { + return false; + } + + $plaintext = str_split($plaintext, $length); + $ciphertext = ''; + foreach ($plaintext as $m) { + $ciphertext.= $this->_rsaes_pkcs1_v1_5_encrypt($m); + } + return $ciphertext; + //case self::ENCRYPTION_OAEP: + default: + $length = $this->k - 2 * $this->hLen - 2; + if ($length <= 0) { + return false; + } + + $plaintext = str_split($plaintext, $length); + $ciphertext = ''; + foreach ($plaintext as $m) { + $ciphertext.= $this->_rsaes_oaep_encrypt($m); + } + return $ciphertext; + } + } + + /** + * Decryption + * + * @see self::encrypt() + * @access public + * @param string $ciphertext + * @return string + */ + function decrypt($ciphertext) + { + if ($this->k <= 0) { + return false; + } + + $ciphertext = str_split($ciphertext, $this->k); + $ciphertext[count($ciphertext) - 1] = str_pad($ciphertext[count($ciphertext) - 1], $this->k, chr(0), STR_PAD_LEFT); + + $plaintext = ''; + + switch ($this->encryptionMode) { + case self::ENCRYPTION_NONE: + $decrypt = '_raw_encrypt'; + break; + case self::ENCRYPTION_PKCS1: + $decrypt = '_rsaes_pkcs1_v1_5_decrypt'; + break; + //case self::ENCRYPTION_OAEP: + default: + $decrypt = '_rsaes_oaep_decrypt'; + } + + foreach ($ciphertext as $c) { + $temp = $this->$decrypt($c); + if ($temp === false) { + return false; + } + $plaintext.= $temp; + } + + return $plaintext; + } + + /** + * Create a signature + * + * @see self::verify() + * @access public + * @param string $message + * @return string + */ + function sign($message) + { + if (empty($this->modulus) || empty($this->exponent)) { + return false; + } + + switch ($this->signatureMode) { + case self::SIGNATURE_PKCS1: + return $this->_rsassa_pkcs1_v1_5_sign($message); + //case self::SIGNATURE_PSS: + default: + return $this->_rsassa_pss_sign($message); + } + } + + /** + * Verifies a signature + * + * @see self::sign() + * @access public + * @param string $message + * @param string $signature + * @return bool + */ + function verify($message, $signature) + { + if (empty($this->modulus) || empty($this->exponent)) { + return false; + } + + switch ($this->signatureMode) { + case self::SIGNATURE_PKCS1: + return $this->_rsassa_pkcs1_v1_5_verify($message, $signature); + //case self::SIGNATURE_PSS: + default: + return $this->_rsassa_pss_verify($message, $signature); + } + } + + /** + * Extract raw BER from Base64 encoding + * + * @access private + * @param string $str + * @return string + */ + function _extractBER($str) + { + /* X.509 certs are assumed to be base64 encoded but sometimes they'll have additional things in them + * above and beyond the ceritificate. + * ie. some may have the following preceding the -----BEGIN CERTIFICATE----- line: + * + * Bag Attributes + * localKeyID: 01 00 00 00 + * subject=/O=organization/OU=org unit/CN=common name + * issuer=/O=organization/CN=common name + */ + $temp = preg_replace('#.*?^-+[^-]+-+[\r\n ]*$#ms', '', $str, 1); + // remove the -----BEGIN CERTIFICATE----- and -----END CERTIFICATE----- stuff + $temp = preg_replace('#-+[^-]+-+#', '', $temp); + // remove new lines + $temp = str_replace(array("\r", "\n", ' '), '', $temp); + $temp = preg_match('#^[a-zA-Z\d/+]*={0,2}$#', $temp) ? base64_decode($temp) : false; + return $temp != false ? $temp : $str; + } +} diff --git a/3rdparty/phpseclib/phpseclib/phpseclib/Crypt/Random.php b/3rdparty/phpseclib/phpseclib/phpseclib/Crypt/Random.php new file mode 100644 index 00000000..e039340c --- /dev/null +++ b/3rdparty/phpseclib/phpseclib/phpseclib/Crypt/Random.php @@ -0,0 +1,280 @@ + + * + * + * + * @category Crypt + * @package Random + * @author Jim Wigginton + * @copyright 2007 Jim Wigginton + * @license http://www.opensource.org/licenses/mit-license.html MIT License + * @link http://phpseclib.sourceforge.net + */ + +namespace phpseclib\Crypt; + +/** + * Pure-PHP Random Number Generator + * + * @package Random + * @author Jim Wigginton + * @access public + */ +class Random +{ + /** + * Generate a random string. + * + * Although microoptimizations are generally discouraged as they impair readability this function is ripe with + * microoptimizations because this function has the potential of being called a huge number of times. + * eg. for RSA key generation. + * + * @param int $length + * @return string + */ + static function string($length) + { + if (!$length) { + return ''; + } + + if (version_compare(PHP_VERSION, '7.0.0', '>=')) { + try { + return \random_bytes($length); + } catch (\Throwable $e) { + // If a sufficient source of randomness is unavailable, random_bytes() will throw an + // object that implements the Throwable interface (Exception, TypeError, Error). + // We don't actually need to do anything here. The string() method should just continue + // as normal. Note, however, that if we don't have a sufficient source of randomness for + // random_bytes(), most of the other calls here will fail too, so we'll end up using + // the PHP implementation. + } + } + + if (strtoupper(substr(PHP_OS, 0, 3)) === 'WIN') { + // method 1. prior to PHP 5.3 this would call rand() on windows hence the function_exists('class_alias') call. + // ie. class_alias is a function that was introduced in PHP 5.3 + if (extension_loaded('mcrypt') && function_exists('class_alias')) { + return @mcrypt_create_iv($length); + } + // method 2. openssl_random_pseudo_bytes was introduced in PHP 5.3.0 but prior to PHP 5.3.4 there was, + // to quote , "possible blocking behavior". as of 5.3.4 + // openssl_random_pseudo_bytes and mcrypt_create_iv do the exact same thing on Windows. ie. they both + // call php_win32_get_random_bytes(): + // + // https://github.com/php/php-src/blob/7014a0eb6d1611151a286c0ff4f2238f92c120d6/ext/openssl/openssl.c#L5008 + // https://github.com/php/php-src/blob/7014a0eb6d1611151a286c0ff4f2238f92c120d6/ext/mcrypt/mcrypt.c#L1392 + // + // php_win32_get_random_bytes() is defined thusly: + // + // https://github.com/php/php-src/blob/7014a0eb6d1611151a286c0ff4f2238f92c120d6/win32/winutil.c#L80 + // + // we're calling it, all the same, in the off chance that the mcrypt extension is not available + if (extension_loaded('openssl') && version_compare(PHP_VERSION, '5.3.4', '>=')) { + return openssl_random_pseudo_bytes($length); + } + } else { + // method 1. the fastest + if (extension_loaded('openssl')) { + return openssl_random_pseudo_bytes($length); + } + // method 2 + static $fp = true; + if ($fp === true) { + // warning's will be output unles the error suppression operator is used. errors such as + // "open_basedir restriction in effect", "Permission denied", "No such file or directory", etc. + $fp = @fopen('/dev/urandom', 'rb'); + } + if ($fp !== true && $fp !== false) { // surprisingly faster than !is_bool() or is_resource() + $temp = fread($fp, $length); + if (strlen($temp) == $length) { + return $temp; + } + } + // method 3. pretty much does the same thing as method 2 per the following url: + // https://github.com/php/php-src/blob/7014a0eb6d1611151a286c0ff4f2238f92c120d6/ext/mcrypt/mcrypt.c#L1391 + // surprisingly slower than method 2. maybe that's because mcrypt_create_iv does a bunch of error checking that we're + // not doing. regardless, this'll only be called if this PHP script couldn't open /dev/urandom due to open_basedir + // restrictions or some such + if (extension_loaded('mcrypt')) { + return @mcrypt_create_iv($length, MCRYPT_DEV_URANDOM); + } + } + // at this point we have no choice but to use a pure-PHP CSPRNG + + // cascade entropy across multiple PHP instances by fixing the session and collecting all + // environmental variables, including the previous session data and the current session + // data. + // + // mt_rand seeds itself by looking at the PID and the time, both of which are (relatively) + // easy to guess at. linux uses mouse clicks, keyboard timings, etc, as entropy sources, but + // PHP isn't low level to be able to use those as sources and on a web server there's not likely + // going to be a ton of keyboard or mouse action. web servers do have one thing that we can use + // however, a ton of people visiting the website. obviously you don't want to base your seeding + // soley on parameters a potential attacker sends but (1) not everything in $_SERVER is controlled + // by the user and (2) this isn't just looking at the data sent by the current user - it's based + // on the data sent by all users. one user requests the page and a hash of their info is saved. + // another user visits the page and the serialization of their data is utilized along with the + // server envirnment stuff and a hash of the previous http request data (which itself utilizes + // a hash of the session data before that). certainly an attacker should be assumed to have + // full control over his own http requests. he, however, is not going to have control over + // everyone's http requests. + static $crypto = false, $v; + if ($crypto === false) { + // save old session data + $old_session_id = session_id(); + $old_use_cookies = ini_get('session.use_cookies'); + $old_session_cache_limiter = session_cache_limiter(); + $_OLD_SESSION = isset($_SESSION) ? $_SESSION : false; + if ($old_session_id != '') { + session_write_close(); + } + + session_id(1); + ini_set('session.use_cookies', 0); + session_cache_limiter(''); + session_start(); + + $v = $seed = $_SESSION['seed'] = pack('H*', sha1( + (isset($_SERVER) ? phpseclib_safe_serialize($_SERVER) : '') . + (isset($_POST) ? phpseclib_safe_serialize($_POST) : '') . + (isset($_GET) ? phpseclib_safe_serialize($_GET) : '') . + (isset($_COOKIE) ? phpseclib_safe_serialize($_COOKIE) : '') . + // as of PHP 8.1 $GLOBALS can't be accessed by reference, which eliminates + // the need for phpseclib_safe_serialize. see https://wiki.php.net/rfc/restrict_globals_usage + // for more info + (version_compare(PHP_VERSION, '8.1.0', '>=') ? serialize($GLOBALS) : phpseclib_safe_serialize($GLOBALS)) . + phpseclib_safe_serialize($_SESSION) . + phpseclib_safe_serialize($_OLD_SESSION) + )); + if (!isset($_SESSION['count'])) { + $_SESSION['count'] = 0; + } + $_SESSION['count']++; + + session_write_close(); + + // restore old session data + if ($old_session_id != '') { + session_id($old_session_id); + session_start(); + ini_set('session.use_cookies', $old_use_cookies); + session_cache_limiter($old_session_cache_limiter); + } else { + if ($_OLD_SESSION !== false) { + $_SESSION = $_OLD_SESSION; + unset($_OLD_SESSION); + } else { + unset($_SESSION); + } + } + + // in SSH2 a shared secret and an exchange hash are generated through the key exchange process. + // the IV client to server is the hash of that "nonce" with the letter A and for the encryption key it's the letter C. + // if the hash doesn't produce enough a key or an IV that's long enough concat successive hashes of the + // original hash and the current hash. we'll be emulating that. for more info see the following URL: + // + // http://tools.ietf.org/html/rfc4253#section-7.2 + // + // see the is_string($crypto) part for an example of how to expand the keys + $key = pack('H*', sha1($seed . 'A')); + $iv = pack('H*', sha1($seed . 'C')); + + // ciphers are used as per the nist.gov link below. also, see this link: + // + // http://en.wikipedia.org/wiki/Cryptographically_secure_pseudorandom_number_generator#Designs_based_on_cryptographic_primitives + switch (true) { + case class_exists('\phpseclib\Crypt\AES'): + $crypto = new AES(Base::MODE_CTR); + break; + case class_exists('\phpseclib\Crypt\Twofish'): + $crypto = new Twofish(Base::MODE_CTR); + break; + case class_exists('\phpseclib\Crypt\Blowfish'): + $crypto = new Blowfish(Base::MODE_CTR); + break; + case class_exists('\phpseclib\Crypt\TripleDES'): + $crypto = new TripleDES(Base::MODE_CTR); + break; + case class_exists('\phpseclib\Crypt\DES'): + $crypto = new DES(Base::MODE_CTR); + break; + case class_exists('\phpseclib\Crypt\RC4'): + $crypto = new RC4(); + break; + default: + user_error(__CLASS__ . ' requires at least one symmetric cipher be loaded'); + return false; + } + + $crypto->setKey($key); + $crypto->setIV($iv); + $crypto->enableContinuousBuffer(); + } + + //return $crypto->encrypt(str_repeat("\0", $length)); + + // the following is based off of ANSI X9.31: + // + // http://csrc.nist.gov/groups/STM/cavp/documents/rng/931rngext.pdf + // + // OpenSSL uses that same standard for it's random numbers: + // + // http://www.opensource.apple.com/source/OpenSSL/OpenSSL-38/openssl/fips-1.0/rand/fips_rand.c + // (do a search for "ANS X9.31 A.2.4") + $result = ''; + while (strlen($result) < $length) { + $i = $crypto->encrypt(microtime()); // strlen(microtime()) == 21 + $r = $crypto->encrypt($i ^ $v); // strlen($v) == 20 + $v = $crypto->encrypt($r ^ $i); // strlen($r) == 20 + $result.= $r; + } + return substr($result, 0, $length); + } +} + +if (!function_exists('phpseclib_safe_serialize')) { + /** + * Safely serialize variables + * + * If a class has a private __sleep() method it'll give a fatal error on PHP 5.2 and earlier. + * PHP 5.3 will emit a warning. + * + * @param mixed $arr + * @access public + */ + function phpseclib_safe_serialize(&$arr) + { + if (is_object($arr)) { + return ''; + } + if (!is_array($arr)) { + return serialize($arr); + } + // prevent circular array recursion + if (isset($arr['__phpseclib_marker'])) { + return ''; + } + $safearr = array(); + $arr['__phpseclib_marker'] = true; + foreach (array_keys($arr) as $key) { + // do not recurse on the '__phpseclib_marker' key itself, for smaller memory usage + if ($key !== '__phpseclib_marker') { + $safearr[$key] = phpseclib_safe_serialize($arr[$key]); + } + } + unset($arr['__phpseclib_marker']); + return serialize($safearr); + } +} diff --git a/3rdparty/phpseclib/phpseclib/phpseclib/Crypt/Rijndael.php b/3rdparty/phpseclib/phpseclib/phpseclib/Crypt/Rijndael.php new file mode 100644 index 00000000..4665738e --- /dev/null +++ b/3rdparty/phpseclib/phpseclib/phpseclib/Crypt/Rijndael.php @@ -0,0 +1,939 @@ + + * setKey('abcdefghijklmnop'); + * + * $size = 10 * 1024; + * $plaintext = ''; + * for ($i = 0; $i < $size; $i++) { + * $plaintext.= 'a'; + * } + * + * echo $rijndael->decrypt($rijndael->encrypt($plaintext)); + * ?> + * + * + * @category Crypt + * @package Rijndael + * @author Jim Wigginton + * @copyright 2008 Jim Wigginton + * @license http://www.opensource.org/licenses/mit-license.html MIT License + * @link http://phpseclib.sourceforge.net + */ + +namespace phpseclib\Crypt; + +/** + * Pure-PHP implementation of Rijndael. + * + * @package Rijndael + * @author Jim Wigginton + * @access public + */ +class Rijndael extends Base +{ + /** + * The mcrypt specific name of the cipher + * + * Mcrypt is useable for 128/192/256-bit $block_size/$key_length. For 160/224 not. + * \phpseclib\Crypt\Rijndael determines automatically whether mcrypt is useable + * or not for the current $block_size/$key_length. + * In case of, $cipher_name_mcrypt will be set dynamically at run time accordingly. + * + * @see \phpseclib\Crypt\Base::cipher_name_mcrypt + * @see \phpseclib\Crypt\Base::engine + * @see self::isValidEngine() + * @var string + * @access private + */ + var $cipher_name_mcrypt = 'rijndael-128'; + + /** + * The default salt used by setPassword() + * + * @see \phpseclib\Crypt\Base::password_default_salt + * @see \phpseclib\Crypt\Base::setPassword() + * @var string + * @access private + */ + var $password_default_salt = 'phpseclib'; + + /** + * The Key Schedule + * + * @see self::_setup() + * @var array + * @access private + */ + var $w; + + /** + * The Inverse Key Schedule + * + * @see self::_setup() + * @var array + * @access private + */ + var $dw; + + /** + * The Block Length divided by 32 + * + * @see self::setBlockLength() + * @var int + * @access private + * @internal The max value is 256 / 32 = 8, the min value is 128 / 32 = 4. Exists in conjunction with $block_size + * because the encryption / decryption / key schedule creation requires this number and not $block_size. We could + * derive this from $block_size or vice versa, but that'd mean we'd have to do multiple shift operations, so in lieu + * of that, we'll just precompute it once. + */ + var $Nb = 4; + + /** + * The Key Length (in bytes) + * + * @see self::setKeyLength() + * @var int + * @access private + * @internal The max value is 256 / 8 = 32, the min value is 128 / 8 = 16. Exists in conjunction with $Nk + * because the encryption / decryption / key schedule creation requires this number and not $key_length. We could + * derive this from $key_length or vice versa, but that'd mean we'd have to do multiple shift operations, so in lieu + * of that, we'll just precompute it once. + */ + var $key_length = 16; + + /** + * The Key Length divided by 32 + * + * @see self::setKeyLength() + * @var int + * @access private + * @internal The max value is 256 / 32 = 8, the min value is 128 / 32 = 4 + */ + var $Nk = 4; + + /** + * The Number of Rounds + * + * @var int + * @access private + * @internal The max value is 14, the min value is 10. + */ + var $Nr; + + /** + * Shift offsets + * + * @var array + * @access private + */ + var $c; + + /** + * Holds the last used key- and block_size information + * + * @var array + * @access private + */ + var $kl; + + /** + * Sets the key length. + * + * Valid key lengths are 128, 160, 192, 224, and 256. If the length is less than 128, it will be rounded up to + * 128. If the length is greater than 128 and invalid, it will be rounded down to the closest valid amount. + * + * Note: phpseclib extends Rijndael (and AES) for using 160- and 224-bit keys but they are officially not defined + * and the most (if not all) implementations are not able using 160/224-bit keys but round/pad them up to + * 192/256 bits as, for example, mcrypt will do. + * + * That said, if you want be compatible with other Rijndael and AES implementations, + * you should not setKeyLength(160) or setKeyLength(224). + * + * Additional: In case of 160- and 224-bit keys, phpseclib will/can, for that reason, not use + * the mcrypt php extension, even if available. + * This results then in slower encryption. + * + * @access public + * @param int $length + */ + function setKeyLength($length) + { + switch (true) { + case $length <= 128: + $this->key_length = 16; + break; + case $length <= 160: + $this->key_length = 20; + break; + case $length <= 192: + $this->key_length = 24; + break; + case $length <= 224: + $this->key_length = 28; + break; + default: + $this->key_length = 32; + } + + parent::setKeyLength($length); + } + + /** + * Sets the block length + * + * Valid block lengths are 128, 160, 192, 224, and 256. If the length is less than 128, it will be rounded up to + * 128. If the length is greater than 128 and invalid, it will be rounded down to the closest valid amount. + * + * @access public + * @param int $length + */ + function setBlockLength($length) + { + $length >>= 5; + if ($length > 8) { + $length = 8; + } elseif ($length < 4) { + $length = 4; + } + $this->Nb = $length; + $this->block_size = $length << 2; + $this->changed = true; + $this->_setEngine(); + } + + /** + * Test for engine validity + * + * This is mainly just a wrapper to set things up for \phpseclib\Crypt\Base::isValidEngine() + * + * @see \phpseclib\Crypt\Base::__construct() + * @param int $engine + * @access public + * @return bool + */ + function isValidEngine($engine) + { + switch ($engine) { + case self::ENGINE_OPENSSL: + if ($this->block_size != 16) { + return false; + } + $this->cipher_name_openssl_ecb = 'aes-' . ($this->key_length << 3) . '-ecb'; + $this->cipher_name_openssl = 'aes-' . ($this->key_length << 3) . '-' . $this->_openssl_translate_mode(); + break; + case self::ENGINE_MCRYPT: + $this->cipher_name_mcrypt = 'rijndael-' . ($this->block_size << 3); + if ($this->key_length % 8) { // is it a 160/224-bit key? + // mcrypt is not usable for them, only for 128/192/256-bit keys + return false; + } + } + + return parent::isValidEngine($engine); + } + + /** + * Encrypts a block + * + * @access private + * @param string $in + * @return string + */ + function _encryptBlock($in) + { + static $tables; + if (empty($tables)) { + $tables = &$this->_getTables(); + } + $t0 = $tables[0]; + $t1 = $tables[1]; + $t2 = $tables[2]; + $t3 = $tables[3]; + $sbox = $tables[4]; + + $state = array(); + $words = unpack('N*', $in); + + $c = $this->c; + $w = $this->w; + $Nb = $this->Nb; + $Nr = $this->Nr; + + // addRoundKey + $wc = $Nb - 1; + foreach ($words as $word) { + $state[] = $word ^ $w[++$wc]; + } + + // fips-197.pdf#page=19, "Figure 5. Pseudo Code for the Cipher", states that this loop has four components - + // subBytes, shiftRows, mixColumns, and addRoundKey. fips-197.pdf#page=30, "Implementation Suggestions Regarding + // Various Platforms" suggests that performs enhanced implementations are described in Rijndael-ammended.pdf. + // Rijndael-ammended.pdf#page=20, "Implementation aspects / 32-bit processor", discusses such an optimization. + // Unfortunately, the description given there is not quite correct. Per aes.spec.v316.pdf#page=19 [1], + // equation (7.4.7) is supposed to use addition instead of subtraction, so we'll do that here, as well. + + // [1] http://fp.gladman.plus.com/cryptography_technology/rijndael/aes.spec.v316.pdf + $temp = array(); + for ($round = 1; $round < $Nr; ++$round) { + $i = 0; // $c[0] == 0 + $j = $c[1]; + $k = $c[2]; + $l = $c[3]; + + while ($i < $Nb) { + $temp[$i] = $t0[$state[$i] >> 24 & 0x000000FF] ^ + $t1[$state[$j] >> 16 & 0x000000FF] ^ + $t2[$state[$k] >> 8 & 0x000000FF] ^ + $t3[$state[$l] & 0x000000FF] ^ + $w[++$wc]; + ++$i; + $j = ($j + 1) % $Nb; + $k = ($k + 1) % $Nb; + $l = ($l + 1) % $Nb; + } + $state = $temp; + } + + // subWord + for ($i = 0; $i < $Nb; ++$i) { + $state[$i] = $sbox[$state[$i] & 0x000000FF] | + ($sbox[$state[$i] >> 8 & 0x000000FF] << 8) | + ($sbox[$state[$i] >> 16 & 0x000000FF] << 16) | + ($sbox[$state[$i] >> 24 & 0x000000FF] << 24); + } + + // shiftRows + addRoundKey + $i = 0; // $c[0] == 0 + $j = $c[1]; + $k = $c[2]; + $l = $c[3]; + while ($i < $Nb) { + $temp[$i] = ($state[$i] & intval(0xFF000000)) ^ + ($state[$j] & 0x00FF0000) ^ + ($state[$k] & 0x0000FF00) ^ + ($state[$l] & 0x000000FF) ^ + $w[$i]; + ++$i; + $j = ($j + 1) % $Nb; + $k = ($k + 1) % $Nb; + $l = ($l + 1) % $Nb; + } + + switch ($Nb) { + case 8: + return pack('N*', $temp[0], $temp[1], $temp[2], $temp[3], $temp[4], $temp[5], $temp[6], $temp[7]); + case 7: + return pack('N*', $temp[0], $temp[1], $temp[2], $temp[3], $temp[4], $temp[5], $temp[6]); + case 6: + return pack('N*', $temp[0], $temp[1], $temp[2], $temp[3], $temp[4], $temp[5]); + case 5: + return pack('N*', $temp[0], $temp[1], $temp[2], $temp[3], $temp[4]); + default: + return pack('N*', $temp[0], $temp[1], $temp[2], $temp[3]); + } + } + + /** + * Decrypts a block + * + * @access private + * @param string $in + * @return string + */ + function _decryptBlock($in) + { + static $invtables; + if (empty($invtables)) { + $invtables = &$this->_getInvTables(); + } + $dt0 = $invtables[0]; + $dt1 = $invtables[1]; + $dt2 = $invtables[2]; + $dt3 = $invtables[3]; + $isbox = $invtables[4]; + + $state = array(); + $words = unpack('N*', $in); + + $c = $this->c; + $dw = $this->dw; + $Nb = $this->Nb; + $Nr = $this->Nr; + + // addRoundKey + $wc = $Nb - 1; + foreach ($words as $word) { + $state[] = $word ^ $dw[++$wc]; + } + + $temp = array(); + for ($round = $Nr - 1; $round > 0; --$round) { + $i = 0; // $c[0] == 0 + $j = $Nb - $c[1]; + $k = $Nb - $c[2]; + $l = $Nb - $c[3]; + + while ($i < $Nb) { + $temp[$i] = $dt0[$state[$i] >> 24 & 0x000000FF] ^ + $dt1[$state[$j] >> 16 & 0x000000FF] ^ + $dt2[$state[$k] >> 8 & 0x000000FF] ^ + $dt3[$state[$l] & 0x000000FF] ^ + $dw[++$wc]; + ++$i; + $j = ($j + 1) % $Nb; + $k = ($k + 1) % $Nb; + $l = ($l + 1) % $Nb; + } + $state = $temp; + } + + // invShiftRows + invSubWord + addRoundKey + $i = 0; // $c[0] == 0 + $j = $Nb - $c[1]; + $k = $Nb - $c[2]; + $l = $Nb - $c[3]; + + while ($i < $Nb) { + $word = ($state[$i] & intval(0xFF000000)) | + ($state[$j] & 0x00FF0000) | + ($state[$k] & 0x0000FF00) | + ($state[$l] & 0x000000FF); + + $temp[$i] = $dw[$i] ^ ($isbox[$word & 0x000000FF] | + ($isbox[$word >> 8 & 0x000000FF] << 8) | + ($isbox[$word >> 16 & 0x000000FF] << 16) | + ($isbox[$word >> 24 & 0x000000FF] << 24)); + ++$i; + $j = ($j + 1) % $Nb; + $k = ($k + 1) % $Nb; + $l = ($l + 1) % $Nb; + } + + switch ($Nb) { + case 8: + return pack('N*', $temp[0], $temp[1], $temp[2], $temp[3], $temp[4], $temp[5], $temp[6], $temp[7]); + case 7: + return pack('N*', $temp[0], $temp[1], $temp[2], $temp[3], $temp[4], $temp[5], $temp[6]); + case 6: + return pack('N*', $temp[0], $temp[1], $temp[2], $temp[3], $temp[4], $temp[5]); + case 5: + return pack('N*', $temp[0], $temp[1], $temp[2], $temp[3], $temp[4]); + default: + return pack('N*', $temp[0], $temp[1], $temp[2], $temp[3]); + } + } + + /** + * Setup the key (expansion) + * + * @see \phpseclib\Crypt\Base::_setupKey() + * @access private + */ + function _setupKey() + { + // Each number in $rcon is equal to the previous number multiplied by two in Rijndael's finite field. + // See http://en.wikipedia.org/wiki/Finite_field_arithmetic#Multiplicative_inverse + static $rcon; + + if (!isset($rcon)) { + $rcon = array(0, + 0x01000000, 0x02000000, 0x04000000, 0x08000000, 0x10000000, + 0x20000000, 0x40000000, 0x80000000, 0x1B000000, 0x36000000, + 0x6C000000, 0xD8000000, 0xAB000000, 0x4D000000, 0x9A000000, + 0x2F000000, 0x5E000000, 0xBC000000, 0x63000000, 0xC6000000, + 0x97000000, 0x35000000, 0x6A000000, 0xD4000000, 0xB3000000, + 0x7D000000, 0xFA000000, 0xEF000000, 0xC5000000, 0x91000000 + ); + $rcon = array_map('intval', $rcon); + } + + if (isset($this->kl['key']) && $this->key === $this->kl['key'] && $this->key_length === $this->kl['key_length'] && $this->block_size === $this->kl['block_size']) { + // already expanded + return; + } + $this->kl = array('key' => $this->key, 'key_length' => $this->key_length, 'block_size' => $this->block_size); + + $this->Nk = $this->key_length >> 2; + // see Rijndael-ammended.pdf#page=44 + $this->Nr = max($this->Nk, $this->Nb) + 6; + + // shift offsets for Nb = 5, 7 are defined in Rijndael-ammended.pdf#page=44, + // "Table 8: Shift offsets in Shiftrow for the alternative block lengths" + // shift offsets for Nb = 4, 6, 8 are defined in Rijndael-ammended.pdf#page=14, + // "Table 2: Shift offsets for different block lengths" + switch ($this->Nb) { + case 4: + case 5: + case 6: + $this->c = array(0, 1, 2, 3); + break; + case 7: + $this->c = array(0, 1, 2, 4); + break; + case 8: + $this->c = array(0, 1, 3, 4); + } + + $w = array_values(unpack('N*words', $this->key)); + + $length = $this->Nb * ($this->Nr + 1); + for ($i = $this->Nk; $i < $length; $i++) { + $temp = $w[$i - 1]; + if ($i % $this->Nk == 0) { + // according to , "the size of an integer is platform-dependent". + // on a 32-bit machine, it's 32-bits, and on a 64-bit machine, it's 64-bits. on a 32-bit machine, + // 0xFFFFFFFF << 8 == 0xFFFFFF00, but on a 64-bit machine, it equals 0xFFFFFFFF00. as such, doing 'and' + // with 0xFFFFFFFF (or 0xFFFFFF00) on a 32-bit machine is unnecessary, but on a 64-bit machine, it is. + $temp = (($temp << 8) & intval(0xFFFFFF00)) | (($temp >> 24) & 0x000000FF); // rotWord + $temp = $this->_subWord($temp) ^ $rcon[$i / $this->Nk]; + } elseif ($this->Nk > 6 && $i % $this->Nk == 4) { + $temp = $this->_subWord($temp); + } + $w[$i] = $w[$i - $this->Nk] ^ $temp; + } + + // convert the key schedule from a vector of $Nb * ($Nr + 1) length to a matrix with $Nr + 1 rows and $Nb columns + // and generate the inverse key schedule. more specifically, + // according to (section 5.3.3), + // "The key expansion for the Inverse Cipher is defined as follows: + // 1. Apply the Key Expansion. + // 2. Apply InvMixColumn to all Round Keys except the first and the last one." + // also, see fips-197.pdf#page=27, "5.3.5 Equivalent Inverse Cipher" + list($dt0, $dt1, $dt2, $dt3) = $this->_getInvTables(); + $temp = $this->w = $this->dw = array(); + for ($i = $row = $col = 0; $i < $length; $i++, $col++) { + if ($col == $this->Nb) { + if ($row == 0) { + $this->dw[0] = $this->w[0]; + } else { + // subWord + invMixColumn + invSubWord = invMixColumn + $j = 0; + while ($j < $this->Nb) { + $dw = $this->_subWord($this->w[$row][$j]); + $temp[$j] = $dt0[$dw >> 24 & 0x000000FF] ^ + $dt1[$dw >> 16 & 0x000000FF] ^ + $dt2[$dw >> 8 & 0x000000FF] ^ + $dt3[$dw & 0x000000FF]; + $j++; + } + $this->dw[$row] = $temp; + } + + $col = 0; + $row++; + } + $this->w[$row][$col] = $w[$i]; + } + + $this->dw[$row] = $this->w[$row]; + + // Converting to 1-dim key arrays (both ascending) + $this->dw = array_reverse($this->dw); + $w = array_pop($this->w); + $dw = array_pop($this->dw); + foreach ($this->w as $r => $wr) { + foreach ($wr as $c => $wc) { + $w[] = $wc; + $dw[] = $this->dw[$r][$c]; + } + } + $this->w = $w; + $this->dw = $dw; + } + + /** + * Performs S-Box substitutions + * + * @access private + * @param int $word + */ + function _subWord($word) + { + static $sbox; + if (empty($sbox)) { + list(, , , , $sbox) = $this->_getTables(); + } + + return $sbox[$word & 0x000000FF] | + ($sbox[$word >> 8 & 0x000000FF] << 8) | + ($sbox[$word >> 16 & 0x000000FF] << 16) | + ($sbox[$word >> 24 & 0x000000FF] << 24); + } + + /** + * Provides the mixColumns and sboxes tables + * + * @see self::_encryptBlock() + * @see self::_setupInlineCrypt() + * @see self::_subWord() + * @access private + * @return array &$tables + */ + function &_getTables() + { + static $tables; + if (empty($tables)) { + // according to (section 5.2.1), + // precomputed tables can be used in the mixColumns phase. in that example, they're assigned t0...t3, so + // those are the names we'll use. + $t3 = array_map('intval', array( + // with array_map('intval', ...) we ensure we have only int's and not + // some slower floats converted by php automatically on high values + 0x6363A5C6, 0x7C7C84F8, 0x777799EE, 0x7B7B8DF6, 0xF2F20DFF, 0x6B6BBDD6, 0x6F6FB1DE, 0xC5C55491, + 0x30305060, 0x01010302, 0x6767A9CE, 0x2B2B7D56, 0xFEFE19E7, 0xD7D762B5, 0xABABE64D, 0x76769AEC, + 0xCACA458F, 0x82829D1F, 0xC9C94089, 0x7D7D87FA, 0xFAFA15EF, 0x5959EBB2, 0x4747C98E, 0xF0F00BFB, + 0xADADEC41, 0xD4D467B3, 0xA2A2FD5F, 0xAFAFEA45, 0x9C9CBF23, 0xA4A4F753, 0x727296E4, 0xC0C05B9B, + 0xB7B7C275, 0xFDFD1CE1, 0x9393AE3D, 0x26266A4C, 0x36365A6C, 0x3F3F417E, 0xF7F702F5, 0xCCCC4F83, + 0x34345C68, 0xA5A5F451, 0xE5E534D1, 0xF1F108F9, 0x717193E2, 0xD8D873AB, 0x31315362, 0x15153F2A, + 0x04040C08, 0xC7C75295, 0x23236546, 0xC3C35E9D, 0x18182830, 0x9696A137, 0x05050F0A, 0x9A9AB52F, + 0x0707090E, 0x12123624, 0x80809B1B, 0xE2E23DDF, 0xEBEB26CD, 0x2727694E, 0xB2B2CD7F, 0x75759FEA, + 0x09091B12, 0x83839E1D, 0x2C2C7458, 0x1A1A2E34, 0x1B1B2D36, 0x6E6EB2DC, 0x5A5AEEB4, 0xA0A0FB5B, + 0x5252F6A4, 0x3B3B4D76, 0xD6D661B7, 0xB3B3CE7D, 0x29297B52, 0xE3E33EDD, 0x2F2F715E, 0x84849713, + 0x5353F5A6, 0xD1D168B9, 0x00000000, 0xEDED2CC1, 0x20206040, 0xFCFC1FE3, 0xB1B1C879, 0x5B5BEDB6, + 0x6A6ABED4, 0xCBCB468D, 0xBEBED967, 0x39394B72, 0x4A4ADE94, 0x4C4CD498, 0x5858E8B0, 0xCFCF4A85, + 0xD0D06BBB, 0xEFEF2AC5, 0xAAAAE54F, 0xFBFB16ED, 0x4343C586, 0x4D4DD79A, 0x33335566, 0x85859411, + 0x4545CF8A, 0xF9F910E9, 0x02020604, 0x7F7F81FE, 0x5050F0A0, 0x3C3C4478, 0x9F9FBA25, 0xA8A8E34B, + 0x5151F3A2, 0xA3A3FE5D, 0x4040C080, 0x8F8F8A05, 0x9292AD3F, 0x9D9DBC21, 0x38384870, 0xF5F504F1, + 0xBCBCDF63, 0xB6B6C177, 0xDADA75AF, 0x21216342, 0x10103020, 0xFFFF1AE5, 0xF3F30EFD, 0xD2D26DBF, + 0xCDCD4C81, 0x0C0C1418, 0x13133526, 0xECEC2FC3, 0x5F5FE1BE, 0x9797A235, 0x4444CC88, 0x1717392E, + 0xC4C45793, 0xA7A7F255, 0x7E7E82FC, 0x3D3D477A, 0x6464ACC8, 0x5D5DE7BA, 0x19192B32, 0x737395E6, + 0x6060A0C0, 0x81819819, 0x4F4FD19E, 0xDCDC7FA3, 0x22226644, 0x2A2A7E54, 0x9090AB3B, 0x8888830B, + 0x4646CA8C, 0xEEEE29C7, 0xB8B8D36B, 0x14143C28, 0xDEDE79A7, 0x5E5EE2BC, 0x0B0B1D16, 0xDBDB76AD, + 0xE0E03BDB, 0x32325664, 0x3A3A4E74, 0x0A0A1E14, 0x4949DB92, 0x06060A0C, 0x24246C48, 0x5C5CE4B8, + 0xC2C25D9F, 0xD3D36EBD, 0xACACEF43, 0x6262A6C4, 0x9191A839, 0x9595A431, 0xE4E437D3, 0x79798BF2, + 0xE7E732D5, 0xC8C8438B, 0x3737596E, 0x6D6DB7DA, 0x8D8D8C01, 0xD5D564B1, 0x4E4ED29C, 0xA9A9E049, + 0x6C6CB4D8, 0x5656FAAC, 0xF4F407F3, 0xEAEA25CF, 0x6565AFCA, 0x7A7A8EF4, 0xAEAEE947, 0x08081810, + 0xBABAD56F, 0x787888F0, 0x25256F4A, 0x2E2E725C, 0x1C1C2438, 0xA6A6F157, 0xB4B4C773, 0xC6C65197, + 0xE8E823CB, 0xDDDD7CA1, 0x74749CE8, 0x1F1F213E, 0x4B4BDD96, 0xBDBDDC61, 0x8B8B860D, 0x8A8A850F, + 0x707090E0, 0x3E3E427C, 0xB5B5C471, 0x6666AACC, 0x4848D890, 0x03030506, 0xF6F601F7, 0x0E0E121C, + 0x6161A3C2, 0x35355F6A, 0x5757F9AE, 0xB9B9D069, 0x86869117, 0xC1C15899, 0x1D1D273A, 0x9E9EB927, + 0xE1E138D9, 0xF8F813EB, 0x9898B32B, 0x11113322, 0x6969BBD2, 0xD9D970A9, 0x8E8E8907, 0x9494A733, + 0x9B9BB62D, 0x1E1E223C, 0x87879215, 0xE9E920C9, 0xCECE4987, 0x5555FFAA, 0x28287850, 0xDFDF7AA5, + 0x8C8C8F03, 0xA1A1F859, 0x89898009, 0x0D0D171A, 0xBFBFDA65, 0xE6E631D7, 0x4242C684, 0x6868B8D0, + 0x4141C382, 0x9999B029, 0x2D2D775A, 0x0F0F111E, 0xB0B0CB7B, 0x5454FCA8, 0xBBBBD66D, 0x16163A2C + )); + + foreach ($t3 as $t3i) { + $t0[] = (($t3i << 24) & intval(0xFF000000)) | (($t3i >> 8) & 0x00FFFFFF); + $t1[] = (($t3i << 16) & intval(0xFFFF0000)) | (($t3i >> 16) & 0x0000FFFF); + $t2[] = (($t3i << 8) & intval(0xFFFFFF00)) | (($t3i >> 24) & 0x000000FF); + } + + $tables = array( + // The Precomputed mixColumns tables t0 - t3 + $t0, + $t1, + $t2, + $t3, + // The SubByte S-Box + array( + 0x63, 0x7C, 0x77, 0x7B, 0xF2, 0x6B, 0x6F, 0xC5, 0x30, 0x01, 0x67, 0x2B, 0xFE, 0xD7, 0xAB, 0x76, + 0xCA, 0x82, 0xC9, 0x7D, 0xFA, 0x59, 0x47, 0xF0, 0xAD, 0xD4, 0xA2, 0xAF, 0x9C, 0xA4, 0x72, 0xC0, + 0xB7, 0xFD, 0x93, 0x26, 0x36, 0x3F, 0xF7, 0xCC, 0x34, 0xA5, 0xE5, 0xF1, 0x71, 0xD8, 0x31, 0x15, + 0x04, 0xC7, 0x23, 0xC3, 0x18, 0x96, 0x05, 0x9A, 0x07, 0x12, 0x80, 0xE2, 0xEB, 0x27, 0xB2, 0x75, + 0x09, 0x83, 0x2C, 0x1A, 0x1B, 0x6E, 0x5A, 0xA0, 0x52, 0x3B, 0xD6, 0xB3, 0x29, 0xE3, 0x2F, 0x84, + 0x53, 0xD1, 0x00, 0xED, 0x20, 0xFC, 0xB1, 0x5B, 0x6A, 0xCB, 0xBE, 0x39, 0x4A, 0x4C, 0x58, 0xCF, + 0xD0, 0xEF, 0xAA, 0xFB, 0x43, 0x4D, 0x33, 0x85, 0x45, 0xF9, 0x02, 0x7F, 0x50, 0x3C, 0x9F, 0xA8, + 0x51, 0xA3, 0x40, 0x8F, 0x92, 0x9D, 0x38, 0xF5, 0xBC, 0xB6, 0xDA, 0x21, 0x10, 0xFF, 0xF3, 0xD2, + 0xCD, 0x0C, 0x13, 0xEC, 0x5F, 0x97, 0x44, 0x17, 0xC4, 0xA7, 0x7E, 0x3D, 0x64, 0x5D, 0x19, 0x73, + 0x60, 0x81, 0x4F, 0xDC, 0x22, 0x2A, 0x90, 0x88, 0x46, 0xEE, 0xB8, 0x14, 0xDE, 0x5E, 0x0B, 0xDB, + 0xE0, 0x32, 0x3A, 0x0A, 0x49, 0x06, 0x24, 0x5C, 0xC2, 0xD3, 0xAC, 0x62, 0x91, 0x95, 0xE4, 0x79, + 0xE7, 0xC8, 0x37, 0x6D, 0x8D, 0xD5, 0x4E, 0xA9, 0x6C, 0x56, 0xF4, 0xEA, 0x65, 0x7A, 0xAE, 0x08, + 0xBA, 0x78, 0x25, 0x2E, 0x1C, 0xA6, 0xB4, 0xC6, 0xE8, 0xDD, 0x74, 0x1F, 0x4B, 0xBD, 0x8B, 0x8A, + 0x70, 0x3E, 0xB5, 0x66, 0x48, 0x03, 0xF6, 0x0E, 0x61, 0x35, 0x57, 0xB9, 0x86, 0xC1, 0x1D, 0x9E, + 0xE1, 0xF8, 0x98, 0x11, 0x69, 0xD9, 0x8E, 0x94, 0x9B, 0x1E, 0x87, 0xE9, 0xCE, 0x55, 0x28, 0xDF, + 0x8C, 0xA1, 0x89, 0x0D, 0xBF, 0xE6, 0x42, 0x68, 0x41, 0x99, 0x2D, 0x0F, 0xB0, 0x54, 0xBB, 0x16 + ) + ); + } + return $tables; + } + + /** + * Provides the inverse mixColumns and inverse sboxes tables + * + * @see self::_decryptBlock() + * @see self::_setupInlineCrypt() + * @see self::_setupKey() + * @access private + * @return array &$tables + */ + function &_getInvTables() + { + static $tables; + if (empty($tables)) { + $dt3 = array_map('intval', array( + 0xF4A75051, 0x4165537E, 0x17A4C31A, 0x275E963A, 0xAB6BCB3B, 0x9D45F11F, 0xFA58ABAC, 0xE303934B, + 0x30FA5520, 0x766DF6AD, 0xCC769188, 0x024C25F5, 0xE5D7FC4F, 0x2ACBD7C5, 0x35448026, 0x62A38FB5, + 0xB15A49DE, 0xBA1B6725, 0xEA0E9845, 0xFEC0E15D, 0x2F7502C3, 0x4CF01281, 0x4697A38D, 0xD3F9C66B, + 0x8F5FE703, 0x929C9515, 0x6D7AEBBF, 0x5259DA95, 0xBE832DD4, 0x7421D358, 0xE0692949, 0xC9C8448E, + 0xC2896A75, 0x8E7978F4, 0x583E6B99, 0xB971DD27, 0xE14FB6BE, 0x88AD17F0, 0x20AC66C9, 0xCE3AB47D, + 0xDF4A1863, 0x1A3182E5, 0x51336097, 0x537F4562, 0x6477E0B1, 0x6BAE84BB, 0x81A01CFE, 0x082B94F9, + 0x48685870, 0x45FD198F, 0xDE6C8794, 0x7BF8B752, 0x73D323AB, 0x4B02E272, 0x1F8F57E3, 0x55AB2A66, + 0xEB2807B2, 0xB5C2032F, 0xC57B9A86, 0x3708A5D3, 0x2887F230, 0xBFA5B223, 0x036ABA02, 0x16825CED, + 0xCF1C2B8A, 0x79B492A7, 0x07F2F0F3, 0x69E2A14E, 0xDAF4CD65, 0x05BED506, 0x34621FD1, 0xA6FE8AC4, + 0x2E539D34, 0xF355A0A2, 0x8AE13205, 0xF6EB75A4, 0x83EC390B, 0x60EFAA40, 0x719F065E, 0x6E1051BD, + 0x218AF93E, 0xDD063D96, 0x3E05AEDD, 0xE6BD464D, 0x548DB591, 0xC45D0571, 0x06D46F04, 0x5015FF60, + 0x98FB2419, 0xBDE997D6, 0x4043CC89, 0xD99E7767, 0xE842BDB0, 0x898B8807, 0x195B38E7, 0xC8EEDB79, + 0x7C0A47A1, 0x420FE97C, 0x841EC9F8, 0x00000000, 0x80868309, 0x2BED4832, 0x1170AC1E, 0x5A724E6C, + 0x0EFFFBFD, 0x8538560F, 0xAED51E3D, 0x2D392736, 0x0FD9640A, 0x5CA62168, 0x5B54D19B, 0x362E3A24, + 0x0A67B10C, 0x57E70F93, 0xEE96D2B4, 0x9B919E1B, 0xC0C54F80, 0xDC20A261, 0x774B695A, 0x121A161C, + 0x93BA0AE2, 0xA02AE5C0, 0x22E0433C, 0x1B171D12, 0x090D0B0E, 0x8BC7ADF2, 0xB6A8B92D, 0x1EA9C814, + 0xF1198557, 0x75074CAF, 0x99DDBBEE, 0x7F60FDA3, 0x01269FF7, 0x72F5BC5C, 0x663BC544, 0xFB7E345B, + 0x4329768B, 0x23C6DCCB, 0xEDFC68B6, 0xE4F163B8, 0x31DCCAD7, 0x63851042, 0x97224013, 0xC6112084, + 0x4A247D85, 0xBB3DF8D2, 0xF93211AE, 0x29A16DC7, 0x9E2F4B1D, 0xB230F3DC, 0x8652EC0D, 0xC1E3D077, + 0xB3166C2B, 0x70B999A9, 0x9448FA11, 0xE9642247, 0xFC8CC4A8, 0xF03F1AA0, 0x7D2CD856, 0x3390EF22, + 0x494EC787, 0x38D1C1D9, 0xCAA2FE8C, 0xD40B3698, 0xF581CFA6, 0x7ADE28A5, 0xB78E26DA, 0xADBFA43F, + 0x3A9DE42C, 0x78920D50, 0x5FCC9B6A, 0x7E466254, 0x8D13C2F6, 0xD8B8E890, 0x39F75E2E, 0xC3AFF582, + 0x5D80BE9F, 0xD0937C69, 0xD52DA96F, 0x2512B3CF, 0xAC993BC8, 0x187DA710, 0x9C636EE8, 0x3BBB7BDB, + 0x267809CD, 0x5918F46E, 0x9AB701EC, 0x4F9AA883, 0x956E65E6, 0xFFE67EAA, 0xBCCF0821, 0x15E8E6EF, + 0xE79BD9BA, 0x6F36CE4A, 0x9F09D4EA, 0xB07CD629, 0xA4B2AF31, 0x3F23312A, 0xA59430C6, 0xA266C035, + 0x4EBC3774, 0x82CAA6FC, 0x90D0B0E0, 0xA7D81533, 0x04984AF1, 0xECDAF741, 0xCD500E7F, 0x91F62F17, + 0x4DD68D76, 0xEFB04D43, 0xAA4D54CC, 0x9604DFE4, 0xD1B5E39E, 0x6A881B4C, 0x2C1FB8C1, 0x65517F46, + 0x5EEA049D, 0x8C355D01, 0x877473FA, 0x0B412EFB, 0x671D5AB3, 0xDBD25292, 0x105633E9, 0xD647136D, + 0xD7618C9A, 0xA10C7A37, 0xF8148E59, 0x133C89EB, 0xA927EECE, 0x61C935B7, 0x1CE5EDE1, 0x47B13C7A, + 0xD2DF599C, 0xF2733F55, 0x14CE7918, 0xC737BF73, 0xF7CDEA53, 0xFDAA5B5F, 0x3D6F14DF, 0x44DB8678, + 0xAFF381CA, 0x68C43EB9, 0x24342C38, 0xA3405FC2, 0x1DC37216, 0xE2250CBC, 0x3C498B28, 0x0D9541FF, + 0xA8017139, 0x0CB3DE08, 0xB4E49CD8, 0x56C19064, 0xCB84617B, 0x32B670D5, 0x6C5C7448, 0xB85742D0 + )); + + foreach ($dt3 as $dt3i) { + $dt0[] = (($dt3i << 24) & intval(0xFF000000)) | (($dt3i >> 8) & 0x00FFFFFF); + $dt1[] = (($dt3i << 16) & intval(0xFFFF0000)) | (($dt3i >> 16) & 0x0000FFFF); + $dt2[] = (($dt3i << 8) & intval(0xFFFFFF00)) | (($dt3i >> 24) & 0x000000FF); + }; + + $tables = array( + // The Precomputed inverse mixColumns tables dt0 - dt3 + $dt0, + $dt1, + $dt2, + $dt3, + // The inverse SubByte S-Box + array( + 0x52, 0x09, 0x6A, 0xD5, 0x30, 0x36, 0xA5, 0x38, 0xBF, 0x40, 0xA3, 0x9E, 0x81, 0xF3, 0xD7, 0xFB, + 0x7C, 0xE3, 0x39, 0x82, 0x9B, 0x2F, 0xFF, 0x87, 0x34, 0x8E, 0x43, 0x44, 0xC4, 0xDE, 0xE9, 0xCB, + 0x54, 0x7B, 0x94, 0x32, 0xA6, 0xC2, 0x23, 0x3D, 0xEE, 0x4C, 0x95, 0x0B, 0x42, 0xFA, 0xC3, 0x4E, + 0x08, 0x2E, 0xA1, 0x66, 0x28, 0xD9, 0x24, 0xB2, 0x76, 0x5B, 0xA2, 0x49, 0x6D, 0x8B, 0xD1, 0x25, + 0x72, 0xF8, 0xF6, 0x64, 0x86, 0x68, 0x98, 0x16, 0xD4, 0xA4, 0x5C, 0xCC, 0x5D, 0x65, 0xB6, 0x92, + 0x6C, 0x70, 0x48, 0x50, 0xFD, 0xED, 0xB9, 0xDA, 0x5E, 0x15, 0x46, 0x57, 0xA7, 0x8D, 0x9D, 0x84, + 0x90, 0xD8, 0xAB, 0x00, 0x8C, 0xBC, 0xD3, 0x0A, 0xF7, 0xE4, 0x58, 0x05, 0xB8, 0xB3, 0x45, 0x06, + 0xD0, 0x2C, 0x1E, 0x8F, 0xCA, 0x3F, 0x0F, 0x02, 0xC1, 0xAF, 0xBD, 0x03, 0x01, 0x13, 0x8A, 0x6B, + 0x3A, 0x91, 0x11, 0x41, 0x4F, 0x67, 0xDC, 0xEA, 0x97, 0xF2, 0xCF, 0xCE, 0xF0, 0xB4, 0xE6, 0x73, + 0x96, 0xAC, 0x74, 0x22, 0xE7, 0xAD, 0x35, 0x85, 0xE2, 0xF9, 0x37, 0xE8, 0x1C, 0x75, 0xDF, 0x6E, + 0x47, 0xF1, 0x1A, 0x71, 0x1D, 0x29, 0xC5, 0x89, 0x6F, 0xB7, 0x62, 0x0E, 0xAA, 0x18, 0xBE, 0x1B, + 0xFC, 0x56, 0x3E, 0x4B, 0xC6, 0xD2, 0x79, 0x20, 0x9A, 0xDB, 0xC0, 0xFE, 0x78, 0xCD, 0x5A, 0xF4, + 0x1F, 0xDD, 0xA8, 0x33, 0x88, 0x07, 0xC7, 0x31, 0xB1, 0x12, 0x10, 0x59, 0x27, 0x80, 0xEC, 0x5F, + 0x60, 0x51, 0x7F, 0xA9, 0x19, 0xB5, 0x4A, 0x0D, 0x2D, 0xE5, 0x7A, 0x9F, 0x93, 0xC9, 0x9C, 0xEF, + 0xA0, 0xE0, 0x3B, 0x4D, 0xAE, 0x2A, 0xF5, 0xB0, 0xC8, 0xEB, 0xBB, 0x3C, 0x83, 0x53, 0x99, 0x61, + 0x17, 0x2B, 0x04, 0x7E, 0xBA, 0x77, 0xD6, 0x26, 0xE1, 0x69, 0x14, 0x63, 0x55, 0x21, 0x0C, 0x7D + ) + ); + } + return $tables; + } + + /** + * Setup the performance-optimized function for de/encrypt() + * + * @see \phpseclib\Crypt\Base::_setupInlineCrypt() + * @access private + */ + function _setupInlineCrypt() + { + // Note: _setupInlineCrypt() will be called only if $this->changed === true + // So here we are'nt under the same heavy timing-stress as we are in _de/encryptBlock() or de/encrypt(). + // However...the here generated function- $code, stored as php callback in $this->inline_crypt, must work as fast as even possible. + + $lambda_functions =& self::_getLambdaFunctions(); + + // We create max. 10 hi-optimized code for memory reason. Means: For each $key one ultra fast inline-crypt function. + // (Currently, for Crypt_Rijndael/AES, one generated $lambda_function cost on php5.5@32bit ~80kb unfreeable mem and ~130kb on php5.5@64bit) + // After that, we'll still create very fast optimized code but not the hi-ultimative code, for each $mode one. + $gen_hi_opt_code = (bool)(count($lambda_functions) < 10); + + // Generation of a uniqe hash for our generated code + $code_hash = "Crypt_Rijndael, {$this->mode}, {$this->Nr}, {$this->Nb}"; + if ($gen_hi_opt_code) { + $code_hash = str_pad($code_hash, 32) . $this->_hashInlineCryptFunction($this->key); + } + + if (!isset($lambda_functions[$code_hash])) { + switch (true) { + case $gen_hi_opt_code: + // The hi-optimized $lambda_functions will use the key-words hardcoded for better performance. + $w = $this->w; + $dw = $this->dw; + $init_encrypt = ''; + $init_decrypt = ''; + break; + default: + for ($i = 0, $cw = count($this->w); $i < $cw; ++$i) { + $w[] = '$w[' . $i . ']'; + $dw[] = '$dw[' . $i . ']'; + } + $init_encrypt = '$w = $self->w;'; + $init_decrypt = '$dw = $self->dw;'; + } + + $Nr = $this->Nr; + $Nb = $this->Nb; + $c = $this->c; + + // Generating encrypt code: + $init_encrypt.= ' + if (empty($tables)) { + $tables = &$self->_getTables(); + } + $t0 = $tables[0]; + $t1 = $tables[1]; + $t2 = $tables[2]; + $t3 = $tables[3]; + $sbox = $tables[4]; + '; + + $s = 'e'; + $e = 's'; + $wc = $Nb - 1; + + // Preround: addRoundKey + $encrypt_block = '$in = unpack("N*", $in);'."\n"; + for ($i = 0; $i < $Nb; ++$i) { + $encrypt_block .= '$s'.$i.' = $in['.($i + 1).'] ^ '.$w[++$wc].";\n"; + } + + // Mainrounds: shiftRows + subWord + mixColumns + addRoundKey + for ($round = 1; $round < $Nr; ++$round) { + list($s, $e) = array($e, $s); + for ($i = 0; $i < $Nb; ++$i) { + $encrypt_block.= + '$'.$e.$i.' = + $t0[($'.$s.$i .' >> 24) & 0xff] ^ + $t1[($'.$s.(($i + $c[1]) % $Nb).' >> 16) & 0xff] ^ + $t2[($'.$s.(($i + $c[2]) % $Nb).' >> 8) & 0xff] ^ + $t3[ $'.$s.(($i + $c[3]) % $Nb).' & 0xff] ^ + '.$w[++$wc].";\n"; + } + } + + // Finalround: subWord + shiftRows + addRoundKey + for ($i = 0; $i < $Nb; ++$i) { + $encrypt_block.= + '$'.$e.$i.' = + $sbox[ $'.$e.$i.' & 0xff] | + ($sbox[($'.$e.$i.' >> 8) & 0xff] << 8) | + ($sbox[($'.$e.$i.' >> 16) & 0xff] << 16) | + ($sbox[($'.$e.$i.' >> 24) & 0xff] << 24);'."\n"; + } + $encrypt_block .= '$in = pack("N*"'."\n"; + for ($i = 0; $i < $Nb; ++$i) { + $encrypt_block.= ', + ($'.$e.$i .' & '.((int)0xFF000000).') ^ + ($'.$e.(($i + $c[1]) % $Nb).' & 0x00FF0000 ) ^ + ($'.$e.(($i + $c[2]) % $Nb).' & 0x0000FF00 ) ^ + ($'.$e.(($i + $c[3]) % $Nb).' & 0x000000FF ) ^ + '.$w[$i]."\n"; + } + $encrypt_block .= ');'; + + // Generating decrypt code: + $init_decrypt.= ' + if (empty($invtables)) { + $invtables = &$self->_getInvTables(); + } + $dt0 = $invtables[0]; + $dt1 = $invtables[1]; + $dt2 = $invtables[2]; + $dt3 = $invtables[3]; + $isbox = $invtables[4]; + '; + + $s = 'e'; + $e = 's'; + $wc = $Nb - 1; + + // Preround: addRoundKey + $decrypt_block = '$in = unpack("N*", $in);'."\n"; + for ($i = 0; $i < $Nb; ++$i) { + $decrypt_block .= '$s'.$i.' = $in['.($i + 1).'] ^ '.$dw[++$wc].';'."\n"; + } + + // Mainrounds: shiftRows + subWord + mixColumns + addRoundKey + for ($round = 1; $round < $Nr; ++$round) { + list($s, $e) = array($e, $s); + for ($i = 0; $i < $Nb; ++$i) { + $decrypt_block.= + '$'.$e.$i.' = + $dt0[($'.$s.$i .' >> 24) & 0xff] ^ + $dt1[($'.$s.(($Nb + $i - $c[1]) % $Nb).' >> 16) & 0xff] ^ + $dt2[($'.$s.(($Nb + $i - $c[2]) % $Nb).' >> 8) & 0xff] ^ + $dt3[ $'.$s.(($Nb + $i - $c[3]) % $Nb).' & 0xff] ^ + '.$dw[++$wc].";\n"; + } + } + + // Finalround: subWord + shiftRows + addRoundKey + for ($i = 0; $i < $Nb; ++$i) { + $decrypt_block.= + '$'.$e.$i.' = + $isbox[ $'.$e.$i.' & 0xff] | + ($isbox[($'.$e.$i.' >> 8) & 0xff] << 8) | + ($isbox[($'.$e.$i.' >> 16) & 0xff] << 16) | + ($isbox[($'.$e.$i.' >> 24) & 0xff] << 24);'."\n"; + } + $decrypt_block .= '$in = pack("N*"'."\n"; + for ($i = 0; $i < $Nb; ++$i) { + $decrypt_block.= ', + ($'.$e.$i. ' & '.((int)0xFF000000).') ^ + ($'.$e.(($Nb + $i - $c[1]) % $Nb).' & 0x00FF0000 ) ^ + ($'.$e.(($Nb + $i - $c[2]) % $Nb).' & 0x0000FF00 ) ^ + ($'.$e.(($Nb + $i - $c[3]) % $Nb).' & 0x000000FF ) ^ + '.$dw[$i]."\n"; + } + $decrypt_block .= ');'; + + $lambda_functions[$code_hash] = $this->_createInlineCryptFunction( + array( + 'init_crypt' => 'static $tables; static $invtables;', + 'init_encrypt' => $init_encrypt, + 'init_decrypt' => $init_decrypt, + 'encrypt_block' => $encrypt_block, + 'decrypt_block' => $decrypt_block + ) + ); + } + $this->inline_crypt = $lambda_functions[$code_hash]; + } +} diff --git a/3rdparty/phpseclib/phpseclib/phpseclib/Crypt/TripleDES.php b/3rdparty/phpseclib/phpseclib/phpseclib/Crypt/TripleDES.php new file mode 100644 index 00000000..bf2df95e --- /dev/null +++ b/3rdparty/phpseclib/phpseclib/phpseclib/Crypt/TripleDES.php @@ -0,0 +1,460 @@ + + * setKey('abcdefghijklmnopqrstuvwx'); + * + * $size = 10 * 1024; + * $plaintext = ''; + * for ($i = 0; $i < $size; $i++) { + * $plaintext.= 'a'; + * } + * + * echo $des->decrypt($des->encrypt($plaintext)); + * ?> + * + * + * @category Crypt + * @package TripleDES + * @author Jim Wigginton + * @copyright 2007 Jim Wigginton + * @license http://www.opensource.org/licenses/mit-license.html MIT License + * @link http://phpseclib.sourceforge.net + */ + +namespace phpseclib\Crypt; + +/** + * Pure-PHP implementation of Triple DES. + * + * @package TripleDES + * @author Jim Wigginton + * @access public + */ +class TripleDES extends DES +{ + /** + * Encrypt / decrypt using inner chaining + * + * Inner chaining is used by SSH-1 and is generally considered to be less secure then outer chaining (self::MODE_CBC3). + */ + const MODE_3CBC = -2; + + /** + * Encrypt / decrypt using outer chaining + * + * Outer chaining is used by SSH-2 and when the mode is set to \phpseclib\Crypt\Base::MODE_CBC. + */ + const MODE_CBC3 = self::MODE_CBC; + + /** + * Key Length (in bytes) + * + * @see \phpseclib\Crypt\TripleDES::setKeyLength() + * @var int + * @access private + */ + var $key_length = 24; + + /** + * The default salt used by setPassword() + * + * @see \phpseclib\Crypt\Base::password_default_salt + * @see \phpseclib\Crypt\Base::setPassword() + * @var string + * @access private + */ + var $password_default_salt = 'phpseclib'; + + /** + * The mcrypt specific name of the cipher + * + * @see \phpseclib\Crypt\DES::cipher_name_mcrypt + * @see \phpseclib\Crypt\Base::cipher_name_mcrypt + * @var string + * @access private + */ + var $cipher_name_mcrypt = 'tripledes'; + + /** + * Optimizing value while CFB-encrypting + * + * @see \phpseclib\Crypt\Base::cfb_init_len + * @var int + * @access private + */ + var $cfb_init_len = 750; + + /** + * max possible size of $key + * + * @see self::setKey() + * @see \phpseclib\Crypt\DES::setKey() + * @var string + * @access private + */ + var $key_length_max = 24; + + /** + * Internal flag whether using self::MODE_3CBC or not + * + * @var bool + * @access private + */ + var $mode_3cbc; + + /** + * The \phpseclib\Crypt\DES objects + * + * Used only if $mode_3cbc === true + * + * @var array + * @access private + */ + var $des; + + /** + * Default Constructor. + * + * Determines whether or not the mcrypt extension should be used. + * + * $mode could be: + * + * - \phpseclib\Crypt\Base::MODE_ECB + * + * - \phpseclib\Crypt\Base::MODE_CBC + * + * - \phpseclib\Crypt\Base::MODE_CTR + * + * - \phpseclib\Crypt\Base::MODE_CFB + * + * - \phpseclib\Crypt\Base::MODE_OFB + * + * - \phpseclib\Crypt\TripleDES::MODE_3CBC + * + * If not explicitly set, \phpseclib\Crypt\Base::MODE_CBC will be used. + * + * @see \phpseclib\Crypt\DES::__construct() + * @see \phpseclib\Crypt\Base::__construct() + * @param int $mode + * @access public + */ + function __construct($mode = self::MODE_CBC) + { + switch ($mode) { + // In case of self::MODE_3CBC, we init as CRYPT_DES_MODE_CBC + // and additional flag us internally as 3CBC + case self::MODE_3CBC: + parent::__construct(self::MODE_CBC); + $this->mode_3cbc = true; + + // This three $des'es will do the 3CBC work (if $key > 64bits) + $this->des = array( + new DES(self::MODE_CBC), + new DES(self::MODE_CBC), + new DES(self::MODE_CBC), + ); + + // we're going to be doing the padding, ourselves, so disable it in the \phpseclib\Crypt\DES objects + $this->des[0]->disablePadding(); + $this->des[1]->disablePadding(); + $this->des[2]->disablePadding(); + break; + // If not 3CBC, we init as usual + default: + parent::__construct($mode); + } + } + + /** + * Test for engine validity + * + * This is mainly just a wrapper to set things up for \phpseclib\Crypt\Base::isValidEngine() + * + * @see \phpseclib\Crypt\Base::__construct() + * @param int $engine + * @access public + * @return bool + */ + function isValidEngine($engine) + { + if ($engine == self::ENGINE_OPENSSL) { + $this->cipher_name_openssl_ecb = 'des-ede3'; + $mode = $this->_openssl_translate_mode(); + $this->cipher_name_openssl = $mode == 'ecb' ? 'des-ede3' : 'des-ede3-' . $mode; + } + + return parent::isValidEngine($engine); + } + + /** + * Sets the initialization vector. (optional) + * + * SetIV is not required when \phpseclib\Crypt\Base::MODE_ECB is being used. If not explicitly set, it'll be assumed + * to be all zero's. + * + * @see \phpseclib\Crypt\Base::setIV() + * @access public + * @param string $iv + */ + function setIV($iv) + { + parent::setIV($iv); + if ($this->mode_3cbc) { + $this->des[0]->setIV($iv); + $this->des[1]->setIV($iv); + $this->des[2]->setIV($iv); + } + } + + /** + * Sets the key length. + * + * Valid key lengths are 64, 128 and 192 + * + * @see \phpseclib\Crypt\Base:setKeyLength() + * @access public + * @param int $length + */ + function setKeyLength($length) + { + $length >>= 3; + switch (true) { + case $length <= 8: + $this->key_length = 8; + break; + case $length <= 16: + $this->key_length = 16; + break; + default: + $this->key_length = 24; + } + + parent::setKeyLength($length); + } + + /** + * Sets the key. + * + * Keys can be of any length. Triple DES, itself, can use 128-bit (eg. strlen($key) == 16) or + * 192-bit (eg. strlen($key) == 24) keys. This function pads and truncates $key as appropriate. + * + * DES also requires that every eighth bit be a parity bit, however, we'll ignore that. + * + * If the key is not explicitly set, it'll be assumed to be all null bytes. + * + * @access public + * @see \phpseclib\Crypt\DES::setKey() + * @see \phpseclib\Crypt\Base::setKey() + * @param string $key + */ + function setKey($key) + { + $length = $this->explicit_key_length ? $this->key_length : strlen($key); + if ($length > 8) { + $key = str_pad(substr($key, 0, 24), 24, chr(0)); + // if $key is between 64 and 128-bits, use the first 64-bits as the last, per this: + // http://php.net/function.mcrypt-encrypt#47973 + $key = $length <= 16 ? substr_replace($key, substr($key, 0, 8), 16) : substr($key, 0, 24); + } else { + $key = str_pad($key, 8, chr(0)); + } + parent::setKey($key); + + // And in case of self::MODE_3CBC: + // if key <= 64bits we not need the 3 $des to work, + // because we will then act as regular DES-CBC with just a <= 64bit key. + // So only if the key > 64bits (> 8 bytes) we will call setKey() for the 3 $des. + if ($this->mode_3cbc && $length > 8) { + $this->des[0]->setKey(substr($key, 0, 8)); + $this->des[1]->setKey(substr($key, 8, 8)); + $this->des[2]->setKey(substr($key, 16, 8)); + } + } + + /** + * Encrypts a message. + * + * @see \phpseclib\Crypt\Base::encrypt() + * @access public + * @param string $plaintext + * @return string $cipertext + */ + function encrypt($plaintext) + { + // parent::en/decrypt() is able to do all the work for all modes and keylengths, + // except for: self::MODE_3CBC (inner chaining CBC) with a key > 64bits + + // if the key is smaller then 8, do what we'd normally do + if ($this->mode_3cbc && strlen($this->key) > 8) { + return $this->des[2]->encrypt( + $this->des[1]->decrypt( + $this->des[0]->encrypt( + $this->_pad($plaintext) + ) + ) + ); + } + + return parent::encrypt($plaintext); + } + + /** + * Decrypts a message. + * + * @see \phpseclib\Crypt\Base::decrypt() + * @access public + * @param string $ciphertext + * @return string $plaintext + */ + function decrypt($ciphertext) + { + if ($this->mode_3cbc && strlen($this->key) > 8) { + return $this->_unpad( + $this->des[0]->decrypt( + $this->des[1]->encrypt( + $this->des[2]->decrypt( + str_pad($ciphertext, (strlen($ciphertext) + 7) & 0xFFFFFFF8, "\0") + ) + ) + ) + ); + } + + return parent::decrypt($ciphertext); + } + + /** + * Treat consecutive "packets" as if they are a continuous buffer. + * + * Say you have a 16-byte plaintext $plaintext. Using the default behavior, the two following code snippets + * will yield different outputs: + * + * + * echo $des->encrypt(substr($plaintext, 0, 8)); + * echo $des->encrypt(substr($plaintext, 8, 8)); + * + * + * echo $des->encrypt($plaintext); + * + * + * The solution is to enable the continuous buffer. Although this will resolve the above discrepancy, it creates + * another, as demonstrated with the following: + * + * + * $des->encrypt(substr($plaintext, 0, 8)); + * echo $des->decrypt($des->encrypt(substr($plaintext, 8, 8))); + * + * + * echo $des->decrypt($des->encrypt(substr($plaintext, 8, 8))); + * + * + * With the continuous buffer disabled, these would yield the same output. With it enabled, they yield different + * outputs. The reason is due to the fact that the initialization vector's change after every encryption / + * decryption round when the continuous buffer is enabled. When it's disabled, they remain constant. + * + * Put another way, when the continuous buffer is enabled, the state of the \phpseclib\Crypt\DES() object changes after each + * encryption / decryption round, whereas otherwise, it'd remain constant. For this reason, it's recommended that + * continuous buffers not be used. They do offer better security and are, in fact, sometimes required (SSH uses them), + * however, they are also less intuitive and more likely to cause you problems. + * + * @see \phpseclib\Crypt\Base::enableContinuousBuffer() + * @see self::disableContinuousBuffer() + * @access public + */ + function enableContinuousBuffer() + { + parent::enableContinuousBuffer(); + if ($this->mode_3cbc) { + $this->des[0]->enableContinuousBuffer(); + $this->des[1]->enableContinuousBuffer(); + $this->des[2]->enableContinuousBuffer(); + } + } + + /** + * Treat consecutive packets as if they are a discontinuous buffer. + * + * The default behavior. + * + * @see \phpseclib\Crypt\Base::disableContinuousBuffer() + * @see self::enableContinuousBuffer() + * @access public + */ + function disableContinuousBuffer() + { + parent::disableContinuousBuffer(); + if ($this->mode_3cbc) { + $this->des[0]->disableContinuousBuffer(); + $this->des[1]->disableContinuousBuffer(); + $this->des[2]->disableContinuousBuffer(); + } + } + + /** + * Creates the key schedule + * + * @see \phpseclib\Crypt\DES::_setupKey() + * @see \phpseclib\Crypt\Base::_setupKey() + * @access private + */ + function _setupKey() + { + switch (true) { + // if $key <= 64bits we configure our internal pure-php cipher engine + // to act as regular [1]DES, not as 3DES. mcrypt.so::tripledes does the same. + case strlen($this->key) <= 8: + $this->des_rounds = 1; + break; + + // otherwise, if $key > 64bits, we configure our engine to work as 3DES. + default: + $this->des_rounds = 3; + + // (only) if 3CBC is used we have, of course, to setup the $des[0-2] keys also separately. + if ($this->mode_3cbc) { + $this->des[0]->_setupKey(); + $this->des[1]->_setupKey(); + $this->des[2]->_setupKey(); + + // because $des[0-2] will, now, do all the work we can return here + // not need unnecessary stress parent::_setupKey() with our, now unused, $key. + return; + } + } + // setup our key + parent::_setupKey(); + } + + /** + * Sets the internal crypt engine + * + * @see \phpseclib\Crypt\Base::__construct() + * @see \phpseclib\Crypt\Base::setPreferredEngine() + * @param int $engine + * @access public + * @return int + */ + function setPreferredEngine($engine) + { + if ($this->mode_3cbc) { + $this->des[0]->setPreferredEngine($engine); + $this->des[1]->setPreferredEngine($engine); + $this->des[2]->setPreferredEngine($engine); + } + + return parent::setPreferredEngine($engine); + } +} diff --git a/3rdparty/phpseclib/phpseclib/phpseclib/Crypt/Twofish.php b/3rdparty/phpseclib/phpseclib/phpseclib/Crypt/Twofish.php new file mode 100644 index 00000000..1c020481 --- /dev/null +++ b/3rdparty/phpseclib/phpseclib/phpseclib/Crypt/Twofish.php @@ -0,0 +1,852 @@ + + * setKey('12345678901234567890123456789012'); + * + * $plaintext = str_repeat('a', 1024); + * + * echo $twofish->decrypt($twofish->encrypt($plaintext)); + * ?> + * + * + * @category Crypt + * @package Twofish + * @author Jim Wigginton + * @author Hans-Juergen Petrich + * @copyright 2007 Jim Wigginton + * @license http://www.opensource.org/licenses/mit-license.html MIT License + * @link http://phpseclib.sourceforge.net + */ + +namespace phpseclib\Crypt; + +/** + * Pure-PHP implementation of Twofish. + * + * @package Twofish + * @author Jim Wigginton + * @author Hans-Juergen Petrich + * @access public + */ +class Twofish extends Base +{ + /** + * The mcrypt specific name of the cipher + * + * @see \phpseclib\Crypt\Base::cipher_name_mcrypt + * @var string + * @access private + */ + var $cipher_name_mcrypt = 'twofish'; + + /** + * Optimizing value while CFB-encrypting + * + * @see \phpseclib\Crypt\Base::cfb_init_len + * @var int + * @access private + */ + var $cfb_init_len = 800; + + /** + * Q-Table + * + * @var array + * @access private + */ + var $q0 = array( + 0xA9, 0x67, 0xB3, 0xE8, 0x04, 0xFD, 0xA3, 0x76, + 0x9A, 0x92, 0x80, 0x78, 0xE4, 0xDD, 0xD1, 0x38, + 0x0D, 0xC6, 0x35, 0x98, 0x18, 0xF7, 0xEC, 0x6C, + 0x43, 0x75, 0x37, 0x26, 0xFA, 0x13, 0x94, 0x48, + 0xF2, 0xD0, 0x8B, 0x30, 0x84, 0x54, 0xDF, 0x23, + 0x19, 0x5B, 0x3D, 0x59, 0xF3, 0xAE, 0xA2, 0x82, + 0x63, 0x01, 0x83, 0x2E, 0xD9, 0x51, 0x9B, 0x7C, + 0xA6, 0xEB, 0xA5, 0xBE, 0x16, 0x0C, 0xE3, 0x61, + 0xC0, 0x8C, 0x3A, 0xF5, 0x73, 0x2C, 0x25, 0x0B, + 0xBB, 0x4E, 0x89, 0x6B, 0x53, 0x6A, 0xB4, 0xF1, + 0xE1, 0xE6, 0xBD, 0x45, 0xE2, 0xF4, 0xB6, 0x66, + 0xCC, 0x95, 0x03, 0x56, 0xD4, 0x1C, 0x1E, 0xD7, + 0xFB, 0xC3, 0x8E, 0xB5, 0xE9, 0xCF, 0xBF, 0xBA, + 0xEA, 0x77, 0x39, 0xAF, 0x33, 0xC9, 0x62, 0x71, + 0x81, 0x79, 0x09, 0xAD, 0x24, 0xCD, 0xF9, 0xD8, + 0xE5, 0xC5, 0xB9, 0x4D, 0x44, 0x08, 0x86, 0xE7, + 0xA1, 0x1D, 0xAA, 0xED, 0x06, 0x70, 0xB2, 0xD2, + 0x41, 0x7B, 0xA0, 0x11, 0x31, 0xC2, 0x27, 0x90, + 0x20, 0xF6, 0x60, 0xFF, 0x96, 0x5C, 0xB1, 0xAB, + 0x9E, 0x9C, 0x52, 0x1B, 0x5F, 0x93, 0x0A, 0xEF, + 0x91, 0x85, 0x49, 0xEE, 0x2D, 0x4F, 0x8F, 0x3B, + 0x47, 0x87, 0x6D, 0x46, 0xD6, 0x3E, 0x69, 0x64, + 0x2A, 0xCE, 0xCB, 0x2F, 0xFC, 0x97, 0x05, 0x7A, + 0xAC, 0x7F, 0xD5, 0x1A, 0x4B, 0x0E, 0xA7, 0x5A, + 0x28, 0x14, 0x3F, 0x29, 0x88, 0x3C, 0x4C, 0x02, + 0xB8, 0xDA, 0xB0, 0x17, 0x55, 0x1F, 0x8A, 0x7D, + 0x57, 0xC7, 0x8D, 0x74, 0xB7, 0xC4, 0x9F, 0x72, + 0x7E, 0x15, 0x22, 0x12, 0x58, 0x07, 0x99, 0x34, + 0x6E, 0x50, 0xDE, 0x68, 0x65, 0xBC, 0xDB, 0xF8, + 0xC8, 0xA8, 0x2B, 0x40, 0xDC, 0xFE, 0x32, 0xA4, + 0xCA, 0x10, 0x21, 0xF0, 0xD3, 0x5D, 0x0F, 0x00, + 0x6F, 0x9D, 0x36, 0x42, 0x4A, 0x5E, 0xC1, 0xE0 + ); + + /** + * Q-Table + * + * @var array + * @access private + */ + var $q1 = array( + 0x75, 0xF3, 0xC6, 0xF4, 0xDB, 0x7B, 0xFB, 0xC8, + 0x4A, 0xD3, 0xE6, 0x6B, 0x45, 0x7D, 0xE8, 0x4B, + 0xD6, 0x32, 0xD8, 0xFD, 0x37, 0x71, 0xF1, 0xE1, + 0x30, 0x0F, 0xF8, 0x1B, 0x87, 0xFA, 0x06, 0x3F, + 0x5E, 0xBA, 0xAE, 0x5B, 0x8A, 0x00, 0xBC, 0x9D, + 0x6D, 0xC1, 0xB1, 0x0E, 0x80, 0x5D, 0xD2, 0xD5, + 0xA0, 0x84, 0x07, 0x14, 0xB5, 0x90, 0x2C, 0xA3, + 0xB2, 0x73, 0x4C, 0x54, 0x92, 0x74, 0x36, 0x51, + 0x38, 0xB0, 0xBD, 0x5A, 0xFC, 0x60, 0x62, 0x96, + 0x6C, 0x42, 0xF7, 0x10, 0x7C, 0x28, 0x27, 0x8C, + 0x13, 0x95, 0x9C, 0xC7, 0x24, 0x46, 0x3B, 0x70, + 0xCA, 0xE3, 0x85, 0xCB, 0x11, 0xD0, 0x93, 0xB8, + 0xA6, 0x83, 0x20, 0xFF, 0x9F, 0x77, 0xC3, 0xCC, + 0x03, 0x6F, 0x08, 0xBF, 0x40, 0xE7, 0x2B, 0xE2, + 0x79, 0x0C, 0xAA, 0x82, 0x41, 0x3A, 0xEA, 0xB9, + 0xE4, 0x9A, 0xA4, 0x97, 0x7E, 0xDA, 0x7A, 0x17, + 0x66, 0x94, 0xA1, 0x1D, 0x3D, 0xF0, 0xDE, 0xB3, + 0x0B, 0x72, 0xA7, 0x1C, 0xEF, 0xD1, 0x53, 0x3E, + 0x8F, 0x33, 0x26, 0x5F, 0xEC, 0x76, 0x2A, 0x49, + 0x81, 0x88, 0xEE, 0x21, 0xC4, 0x1A, 0xEB, 0xD9, + 0xC5, 0x39, 0x99, 0xCD, 0xAD, 0x31, 0x8B, 0x01, + 0x18, 0x23, 0xDD, 0x1F, 0x4E, 0x2D, 0xF9, 0x48, + 0x4F, 0xF2, 0x65, 0x8E, 0x78, 0x5C, 0x58, 0x19, + 0x8D, 0xE5, 0x98, 0x57, 0x67, 0x7F, 0x05, 0x64, + 0xAF, 0x63, 0xB6, 0xFE, 0xF5, 0xB7, 0x3C, 0xA5, + 0xCE, 0xE9, 0x68, 0x44, 0xE0, 0x4D, 0x43, 0x69, + 0x29, 0x2E, 0xAC, 0x15, 0x59, 0xA8, 0x0A, 0x9E, + 0x6E, 0x47, 0xDF, 0x34, 0x35, 0x6A, 0xCF, 0xDC, + 0x22, 0xC9, 0xC0, 0x9B, 0x89, 0xD4, 0xED, 0xAB, + 0x12, 0xA2, 0x0D, 0x52, 0xBB, 0x02, 0x2F, 0xA9, + 0xD7, 0x61, 0x1E, 0xB4, 0x50, 0x04, 0xF6, 0xC2, + 0x16, 0x25, 0x86, 0x56, 0x55, 0x09, 0xBE, 0x91 + ); + + /** + * M-Table + * + * @var array + * @access private + */ + var $m0 = array( + 0xBCBC3275, 0xECEC21F3, 0x202043C6, 0xB3B3C9F4, 0xDADA03DB, 0x02028B7B, 0xE2E22BFB, 0x9E9EFAC8, + 0xC9C9EC4A, 0xD4D409D3, 0x18186BE6, 0x1E1E9F6B, 0x98980E45, 0xB2B2387D, 0xA6A6D2E8, 0x2626B74B, + 0x3C3C57D6, 0x93938A32, 0x8282EED8, 0x525298FD, 0x7B7BD437, 0xBBBB3771, 0x5B5B97F1, 0x474783E1, + 0x24243C30, 0x5151E20F, 0xBABAC6F8, 0x4A4AF31B, 0xBFBF4887, 0x0D0D70FA, 0xB0B0B306, 0x7575DE3F, + 0xD2D2FD5E, 0x7D7D20BA, 0x666631AE, 0x3A3AA35B, 0x59591C8A, 0x00000000, 0xCDCD93BC, 0x1A1AE09D, + 0xAEAE2C6D, 0x7F7FABC1, 0x2B2BC7B1, 0xBEBEB90E, 0xE0E0A080, 0x8A8A105D, 0x3B3B52D2, 0x6464BAD5, + 0xD8D888A0, 0xE7E7A584, 0x5F5FE807, 0x1B1B1114, 0x2C2CC2B5, 0xFCFCB490, 0x3131272C, 0x808065A3, + 0x73732AB2, 0x0C0C8173, 0x79795F4C, 0x6B6B4154, 0x4B4B0292, 0x53536974, 0x94948F36, 0x83831F51, + 0x2A2A3638, 0xC4C49CB0, 0x2222C8BD, 0xD5D5F85A, 0xBDBDC3FC, 0x48487860, 0xFFFFCE62, 0x4C4C0796, + 0x4141776C, 0xC7C7E642, 0xEBEB24F7, 0x1C1C1410, 0x5D5D637C, 0x36362228, 0x6767C027, 0xE9E9AF8C, + 0x4444F913, 0x1414EA95, 0xF5F5BB9C, 0xCFCF18C7, 0x3F3F2D24, 0xC0C0E346, 0x7272DB3B, 0x54546C70, + 0x29294CCA, 0xF0F035E3, 0x0808FE85, 0xC6C617CB, 0xF3F34F11, 0x8C8CE4D0, 0xA4A45993, 0xCACA96B8, + 0x68683BA6, 0xB8B84D83, 0x38382820, 0xE5E52EFF, 0xADAD569F, 0x0B0B8477, 0xC8C81DC3, 0x9999FFCC, + 0x5858ED03, 0x19199A6F, 0x0E0E0A08, 0x95957EBF, 0x70705040, 0xF7F730E7, 0x6E6ECF2B, 0x1F1F6EE2, + 0xB5B53D79, 0x09090F0C, 0x616134AA, 0x57571682, 0x9F9F0B41, 0x9D9D803A, 0x111164EA, 0x2525CDB9, + 0xAFAFDDE4, 0x4545089A, 0xDFDF8DA4, 0xA3A35C97, 0xEAEAD57E, 0x353558DA, 0xEDEDD07A, 0x4343FC17, + 0xF8F8CB66, 0xFBFBB194, 0x3737D3A1, 0xFAFA401D, 0xC2C2683D, 0xB4B4CCF0, 0x32325DDE, 0x9C9C71B3, + 0x5656E70B, 0xE3E3DA72, 0x878760A7, 0x15151B1C, 0xF9F93AEF, 0x6363BFD1, 0x3434A953, 0x9A9A853E, + 0xB1B1428F, 0x7C7CD133, 0x88889B26, 0x3D3DA65F, 0xA1A1D7EC, 0xE4E4DF76, 0x8181942A, 0x91910149, + 0x0F0FFB81, 0xEEEEAA88, 0x161661EE, 0xD7D77321, 0x9797F5C4, 0xA5A5A81A, 0xFEFE3FEB, 0x6D6DB5D9, + 0x7878AEC5, 0xC5C56D39, 0x1D1DE599, 0x7676A4CD, 0x3E3EDCAD, 0xCBCB6731, 0xB6B6478B, 0xEFEF5B01, + 0x12121E18, 0x6060C523, 0x6A6AB0DD, 0x4D4DF61F, 0xCECEE94E, 0xDEDE7C2D, 0x55559DF9, 0x7E7E5A48, + 0x2121B24F, 0x03037AF2, 0xA0A02665, 0x5E5E198E, 0x5A5A6678, 0x65654B5C, 0x62624E58, 0xFDFD4519, + 0x0606F48D, 0x404086E5, 0xF2F2BE98, 0x3333AC57, 0x17179067, 0x05058E7F, 0xE8E85E05, 0x4F4F7D64, + 0x89896AAF, 0x10109563, 0x74742FB6, 0x0A0A75FE, 0x5C5C92F5, 0x9B9B74B7, 0x2D2D333C, 0x3030D6A5, + 0x2E2E49CE, 0x494989E9, 0x46467268, 0x77775544, 0xA8A8D8E0, 0x9696044D, 0x2828BD43, 0xA9A92969, + 0xD9D97929, 0x8686912E, 0xD1D187AC, 0xF4F44A15, 0x8D8D1559, 0xD6D682A8, 0xB9B9BC0A, 0x42420D9E, + 0xF6F6C16E, 0x2F2FB847, 0xDDDD06DF, 0x23233934, 0xCCCC6235, 0xF1F1C46A, 0xC1C112CF, 0x8585EBDC, + 0x8F8F9E22, 0x7171A1C9, 0x9090F0C0, 0xAAAA539B, 0x0101F189, 0x8B8BE1D4, 0x4E4E8CED, 0x8E8E6FAB, + 0xABABA212, 0x6F6F3EA2, 0xE6E6540D, 0xDBDBF252, 0x92927BBB, 0xB7B7B602, 0x6969CA2F, 0x3939D9A9, + 0xD3D30CD7, 0xA7A72361, 0xA2A2AD1E, 0xC3C399B4, 0x6C6C4450, 0x07070504, 0x04047FF6, 0x272746C2, + 0xACACA716, 0xD0D07625, 0x50501386, 0xDCDCF756, 0x84841A55, 0xE1E15109, 0x7A7A25BE, 0x1313EF91 + ); + + /** + * M-Table + * + * @var array + * @access private + */ + var $m1 = array( + 0xA9D93939, 0x67901717, 0xB3719C9C, 0xE8D2A6A6, 0x04050707, 0xFD985252, 0xA3658080, 0x76DFE4E4, + 0x9A084545, 0x92024B4B, 0x80A0E0E0, 0x78665A5A, 0xE4DDAFAF, 0xDDB06A6A, 0xD1BF6363, 0x38362A2A, + 0x0D54E6E6, 0xC6432020, 0x3562CCCC, 0x98BEF2F2, 0x181E1212, 0xF724EBEB, 0xECD7A1A1, 0x6C774141, + 0x43BD2828, 0x7532BCBC, 0x37D47B7B, 0x269B8888, 0xFA700D0D, 0x13F94444, 0x94B1FBFB, 0x485A7E7E, + 0xF27A0303, 0xD0E48C8C, 0x8B47B6B6, 0x303C2424, 0x84A5E7E7, 0x54416B6B, 0xDF06DDDD, 0x23C56060, + 0x1945FDFD, 0x5BA33A3A, 0x3D68C2C2, 0x59158D8D, 0xF321ECEC, 0xAE316666, 0xA23E6F6F, 0x82165757, + 0x63951010, 0x015BEFEF, 0x834DB8B8, 0x2E918686, 0xD9B56D6D, 0x511F8383, 0x9B53AAAA, 0x7C635D5D, + 0xA63B6868, 0xEB3FFEFE, 0xA5D63030, 0xBE257A7A, 0x16A7ACAC, 0x0C0F0909, 0xE335F0F0, 0x6123A7A7, + 0xC0F09090, 0x8CAFE9E9, 0x3A809D9D, 0xF5925C5C, 0x73810C0C, 0x2C273131, 0x2576D0D0, 0x0BE75656, + 0xBB7B9292, 0x4EE9CECE, 0x89F10101, 0x6B9F1E1E, 0x53A93434, 0x6AC4F1F1, 0xB499C3C3, 0xF1975B5B, + 0xE1834747, 0xE66B1818, 0xBDC82222, 0x450E9898, 0xE26E1F1F, 0xF4C9B3B3, 0xB62F7474, 0x66CBF8F8, + 0xCCFF9999, 0x95EA1414, 0x03ED5858, 0x56F7DCDC, 0xD4E18B8B, 0x1C1B1515, 0x1EADA2A2, 0xD70CD3D3, + 0xFB2BE2E2, 0xC31DC8C8, 0x8E195E5E, 0xB5C22C2C, 0xE9894949, 0xCF12C1C1, 0xBF7E9595, 0xBA207D7D, + 0xEA641111, 0x77840B0B, 0x396DC5C5, 0xAF6A8989, 0x33D17C7C, 0xC9A17171, 0x62CEFFFF, 0x7137BBBB, + 0x81FB0F0F, 0x793DB5B5, 0x0951E1E1, 0xADDC3E3E, 0x242D3F3F, 0xCDA47676, 0xF99D5555, 0xD8EE8282, + 0xE5864040, 0xC5AE7878, 0xB9CD2525, 0x4D049696, 0x44557777, 0x080A0E0E, 0x86135050, 0xE730F7F7, + 0xA1D33737, 0x1D40FAFA, 0xAA346161, 0xED8C4E4E, 0x06B3B0B0, 0x706C5454, 0xB22A7373, 0xD2523B3B, + 0x410B9F9F, 0x7B8B0202, 0xA088D8D8, 0x114FF3F3, 0x3167CBCB, 0xC2462727, 0x27C06767, 0x90B4FCFC, + 0x20283838, 0xF67F0404, 0x60784848, 0xFF2EE5E5, 0x96074C4C, 0x5C4B6565, 0xB1C72B2B, 0xAB6F8E8E, + 0x9E0D4242, 0x9CBBF5F5, 0x52F2DBDB, 0x1BF34A4A, 0x5FA63D3D, 0x9359A4A4, 0x0ABCB9B9, 0xEF3AF9F9, + 0x91EF1313, 0x85FE0808, 0x49019191, 0xEE611616, 0x2D7CDEDE, 0x4FB22121, 0x8F42B1B1, 0x3BDB7272, + 0x47B82F2F, 0x8748BFBF, 0x6D2CAEAE, 0x46E3C0C0, 0xD6573C3C, 0x3E859A9A, 0x6929A9A9, 0x647D4F4F, + 0x2A948181, 0xCE492E2E, 0xCB17C6C6, 0x2FCA6969, 0xFCC3BDBD, 0x975CA3A3, 0x055EE8E8, 0x7AD0EDED, + 0xAC87D1D1, 0x7F8E0505, 0xD5BA6464, 0x1AA8A5A5, 0x4BB72626, 0x0EB9BEBE, 0xA7608787, 0x5AF8D5D5, + 0x28223636, 0x14111B1B, 0x3FDE7575, 0x2979D9D9, 0x88AAEEEE, 0x3C332D2D, 0x4C5F7979, 0x02B6B7B7, + 0xB896CACA, 0xDA583535, 0xB09CC4C4, 0x17FC4343, 0x551A8484, 0x1FF64D4D, 0x8A1C5959, 0x7D38B2B2, + 0x57AC3333, 0xC718CFCF, 0x8DF40606, 0x74695353, 0xB7749B9B, 0xC4F59797, 0x9F56ADAD, 0x72DAE3E3, + 0x7ED5EAEA, 0x154AF4F4, 0x229E8F8F, 0x12A2ABAB, 0x584E6262, 0x07E85F5F, 0x99E51D1D, 0x34392323, + 0x6EC1F6F6, 0x50446C6C, 0xDE5D3232, 0x68724646, 0x6526A0A0, 0xBC93CDCD, 0xDB03DADA, 0xF8C6BABA, + 0xC8FA9E9E, 0xA882D6D6, 0x2BCF6E6E, 0x40507070, 0xDCEB8585, 0xFE750A0A, 0x328A9393, 0xA48DDFDF, + 0xCA4C2929, 0x10141C1C, 0x2173D7D7, 0xF0CCB4B4, 0xD309D4D4, 0x5D108A8A, 0x0FE25151, 0x00000000, + 0x6F9A1919, 0x9DE01A1A, 0x368F9494, 0x42E6C7C7, 0x4AECC9C9, 0x5EFDD2D2, 0xC1AB7F7F, 0xE0D8A8A8 + ); + + /** + * M-Table + * + * @var array + * @access private + */ + var $m2 = array( + 0xBC75BC32, 0xECF3EC21, 0x20C62043, 0xB3F4B3C9, 0xDADBDA03, 0x027B028B, 0xE2FBE22B, 0x9EC89EFA, + 0xC94AC9EC, 0xD4D3D409, 0x18E6186B, 0x1E6B1E9F, 0x9845980E, 0xB27DB238, 0xA6E8A6D2, 0x264B26B7, + 0x3CD63C57, 0x9332938A, 0x82D882EE, 0x52FD5298, 0x7B377BD4, 0xBB71BB37, 0x5BF15B97, 0x47E14783, + 0x2430243C, 0x510F51E2, 0xBAF8BAC6, 0x4A1B4AF3, 0xBF87BF48, 0x0DFA0D70, 0xB006B0B3, 0x753F75DE, + 0xD25ED2FD, 0x7DBA7D20, 0x66AE6631, 0x3A5B3AA3, 0x598A591C, 0x00000000, 0xCDBCCD93, 0x1A9D1AE0, + 0xAE6DAE2C, 0x7FC17FAB, 0x2BB12BC7, 0xBE0EBEB9, 0xE080E0A0, 0x8A5D8A10, 0x3BD23B52, 0x64D564BA, + 0xD8A0D888, 0xE784E7A5, 0x5F075FE8, 0x1B141B11, 0x2CB52CC2, 0xFC90FCB4, 0x312C3127, 0x80A38065, + 0x73B2732A, 0x0C730C81, 0x794C795F, 0x6B546B41, 0x4B924B02, 0x53745369, 0x9436948F, 0x8351831F, + 0x2A382A36, 0xC4B0C49C, 0x22BD22C8, 0xD55AD5F8, 0xBDFCBDC3, 0x48604878, 0xFF62FFCE, 0x4C964C07, + 0x416C4177, 0xC742C7E6, 0xEBF7EB24, 0x1C101C14, 0x5D7C5D63, 0x36283622, 0x672767C0, 0xE98CE9AF, + 0x441344F9, 0x149514EA, 0xF59CF5BB, 0xCFC7CF18, 0x3F243F2D, 0xC046C0E3, 0x723B72DB, 0x5470546C, + 0x29CA294C, 0xF0E3F035, 0x088508FE, 0xC6CBC617, 0xF311F34F, 0x8CD08CE4, 0xA493A459, 0xCAB8CA96, + 0x68A6683B, 0xB883B84D, 0x38203828, 0xE5FFE52E, 0xAD9FAD56, 0x0B770B84, 0xC8C3C81D, 0x99CC99FF, + 0x580358ED, 0x196F199A, 0x0E080E0A, 0x95BF957E, 0x70407050, 0xF7E7F730, 0x6E2B6ECF, 0x1FE21F6E, + 0xB579B53D, 0x090C090F, 0x61AA6134, 0x57825716, 0x9F419F0B, 0x9D3A9D80, 0x11EA1164, 0x25B925CD, + 0xAFE4AFDD, 0x459A4508, 0xDFA4DF8D, 0xA397A35C, 0xEA7EEAD5, 0x35DA3558, 0xED7AEDD0, 0x431743FC, + 0xF866F8CB, 0xFB94FBB1, 0x37A137D3, 0xFA1DFA40, 0xC23DC268, 0xB4F0B4CC, 0x32DE325D, 0x9CB39C71, + 0x560B56E7, 0xE372E3DA, 0x87A78760, 0x151C151B, 0xF9EFF93A, 0x63D163BF, 0x345334A9, 0x9A3E9A85, + 0xB18FB142, 0x7C337CD1, 0x8826889B, 0x3D5F3DA6, 0xA1ECA1D7, 0xE476E4DF, 0x812A8194, 0x91499101, + 0x0F810FFB, 0xEE88EEAA, 0x16EE1661, 0xD721D773, 0x97C497F5, 0xA51AA5A8, 0xFEEBFE3F, 0x6DD96DB5, + 0x78C578AE, 0xC539C56D, 0x1D991DE5, 0x76CD76A4, 0x3EAD3EDC, 0xCB31CB67, 0xB68BB647, 0xEF01EF5B, + 0x1218121E, 0x602360C5, 0x6ADD6AB0, 0x4D1F4DF6, 0xCE4ECEE9, 0xDE2DDE7C, 0x55F9559D, 0x7E487E5A, + 0x214F21B2, 0x03F2037A, 0xA065A026, 0x5E8E5E19, 0x5A785A66, 0x655C654B, 0x6258624E, 0xFD19FD45, + 0x068D06F4, 0x40E54086, 0xF298F2BE, 0x335733AC, 0x17671790, 0x057F058E, 0xE805E85E, 0x4F644F7D, + 0x89AF896A, 0x10631095, 0x74B6742F, 0x0AFE0A75, 0x5CF55C92, 0x9BB79B74, 0x2D3C2D33, 0x30A530D6, + 0x2ECE2E49, 0x49E94989, 0x46684672, 0x77447755, 0xA8E0A8D8, 0x964D9604, 0x284328BD, 0xA969A929, + 0xD929D979, 0x862E8691, 0xD1ACD187, 0xF415F44A, 0x8D598D15, 0xD6A8D682, 0xB90AB9BC, 0x429E420D, + 0xF66EF6C1, 0x2F472FB8, 0xDDDFDD06, 0x23342339, 0xCC35CC62, 0xF16AF1C4, 0xC1CFC112, 0x85DC85EB, + 0x8F228F9E, 0x71C971A1, 0x90C090F0, 0xAA9BAA53, 0x018901F1, 0x8BD48BE1, 0x4EED4E8C, 0x8EAB8E6F, + 0xAB12ABA2, 0x6FA26F3E, 0xE60DE654, 0xDB52DBF2, 0x92BB927B, 0xB702B7B6, 0x692F69CA, 0x39A939D9, + 0xD3D7D30C, 0xA761A723, 0xA21EA2AD, 0xC3B4C399, 0x6C506C44, 0x07040705, 0x04F6047F, 0x27C22746, + 0xAC16ACA7, 0xD025D076, 0x50865013, 0xDC56DCF7, 0x8455841A, 0xE109E151, 0x7ABE7A25, 0x139113EF + ); + + /** + * M-Table + * + * @var array + * @access private + */ + var $m3 = array( + 0xD939A9D9, 0x90176790, 0x719CB371, 0xD2A6E8D2, 0x05070405, 0x9852FD98, 0x6580A365, 0xDFE476DF, + 0x08459A08, 0x024B9202, 0xA0E080A0, 0x665A7866, 0xDDAFE4DD, 0xB06ADDB0, 0xBF63D1BF, 0x362A3836, + 0x54E60D54, 0x4320C643, 0x62CC3562, 0xBEF298BE, 0x1E12181E, 0x24EBF724, 0xD7A1ECD7, 0x77416C77, + 0xBD2843BD, 0x32BC7532, 0xD47B37D4, 0x9B88269B, 0x700DFA70, 0xF94413F9, 0xB1FB94B1, 0x5A7E485A, + 0x7A03F27A, 0xE48CD0E4, 0x47B68B47, 0x3C24303C, 0xA5E784A5, 0x416B5441, 0x06DDDF06, 0xC56023C5, + 0x45FD1945, 0xA33A5BA3, 0x68C23D68, 0x158D5915, 0x21ECF321, 0x3166AE31, 0x3E6FA23E, 0x16578216, + 0x95106395, 0x5BEF015B, 0x4DB8834D, 0x91862E91, 0xB56DD9B5, 0x1F83511F, 0x53AA9B53, 0x635D7C63, + 0x3B68A63B, 0x3FFEEB3F, 0xD630A5D6, 0x257ABE25, 0xA7AC16A7, 0x0F090C0F, 0x35F0E335, 0x23A76123, + 0xF090C0F0, 0xAFE98CAF, 0x809D3A80, 0x925CF592, 0x810C7381, 0x27312C27, 0x76D02576, 0xE7560BE7, + 0x7B92BB7B, 0xE9CE4EE9, 0xF10189F1, 0x9F1E6B9F, 0xA93453A9, 0xC4F16AC4, 0x99C3B499, 0x975BF197, + 0x8347E183, 0x6B18E66B, 0xC822BDC8, 0x0E98450E, 0x6E1FE26E, 0xC9B3F4C9, 0x2F74B62F, 0xCBF866CB, + 0xFF99CCFF, 0xEA1495EA, 0xED5803ED, 0xF7DC56F7, 0xE18BD4E1, 0x1B151C1B, 0xADA21EAD, 0x0CD3D70C, + 0x2BE2FB2B, 0x1DC8C31D, 0x195E8E19, 0xC22CB5C2, 0x8949E989, 0x12C1CF12, 0x7E95BF7E, 0x207DBA20, + 0x6411EA64, 0x840B7784, 0x6DC5396D, 0x6A89AF6A, 0xD17C33D1, 0xA171C9A1, 0xCEFF62CE, 0x37BB7137, + 0xFB0F81FB, 0x3DB5793D, 0x51E10951, 0xDC3EADDC, 0x2D3F242D, 0xA476CDA4, 0x9D55F99D, 0xEE82D8EE, + 0x8640E586, 0xAE78C5AE, 0xCD25B9CD, 0x04964D04, 0x55774455, 0x0A0E080A, 0x13508613, 0x30F7E730, + 0xD337A1D3, 0x40FA1D40, 0x3461AA34, 0x8C4EED8C, 0xB3B006B3, 0x6C54706C, 0x2A73B22A, 0x523BD252, + 0x0B9F410B, 0x8B027B8B, 0x88D8A088, 0x4FF3114F, 0x67CB3167, 0x4627C246, 0xC06727C0, 0xB4FC90B4, + 0x28382028, 0x7F04F67F, 0x78486078, 0x2EE5FF2E, 0x074C9607, 0x4B655C4B, 0xC72BB1C7, 0x6F8EAB6F, + 0x0D429E0D, 0xBBF59CBB, 0xF2DB52F2, 0xF34A1BF3, 0xA63D5FA6, 0x59A49359, 0xBCB90ABC, 0x3AF9EF3A, + 0xEF1391EF, 0xFE0885FE, 0x01914901, 0x6116EE61, 0x7CDE2D7C, 0xB2214FB2, 0x42B18F42, 0xDB723BDB, + 0xB82F47B8, 0x48BF8748, 0x2CAE6D2C, 0xE3C046E3, 0x573CD657, 0x859A3E85, 0x29A96929, 0x7D4F647D, + 0x94812A94, 0x492ECE49, 0x17C6CB17, 0xCA692FCA, 0xC3BDFCC3, 0x5CA3975C, 0x5EE8055E, 0xD0ED7AD0, + 0x87D1AC87, 0x8E057F8E, 0xBA64D5BA, 0xA8A51AA8, 0xB7264BB7, 0xB9BE0EB9, 0x6087A760, 0xF8D55AF8, + 0x22362822, 0x111B1411, 0xDE753FDE, 0x79D92979, 0xAAEE88AA, 0x332D3C33, 0x5F794C5F, 0xB6B702B6, + 0x96CAB896, 0x5835DA58, 0x9CC4B09C, 0xFC4317FC, 0x1A84551A, 0xF64D1FF6, 0x1C598A1C, 0x38B27D38, + 0xAC3357AC, 0x18CFC718, 0xF4068DF4, 0x69537469, 0x749BB774, 0xF597C4F5, 0x56AD9F56, 0xDAE372DA, + 0xD5EA7ED5, 0x4AF4154A, 0x9E8F229E, 0xA2AB12A2, 0x4E62584E, 0xE85F07E8, 0xE51D99E5, 0x39233439, + 0xC1F66EC1, 0x446C5044, 0x5D32DE5D, 0x72466872, 0x26A06526, 0x93CDBC93, 0x03DADB03, 0xC6BAF8C6, + 0xFA9EC8FA, 0x82D6A882, 0xCF6E2BCF, 0x50704050, 0xEB85DCEB, 0x750AFE75, 0x8A93328A, 0x8DDFA48D, + 0x4C29CA4C, 0x141C1014, 0x73D72173, 0xCCB4F0CC, 0x09D4D309, 0x108A5D10, 0xE2510FE2, 0x00000000, + 0x9A196F9A, 0xE01A9DE0, 0x8F94368F, 0xE6C742E6, 0xECC94AEC, 0xFDD25EFD, 0xAB7FC1AB, 0xD8A8E0D8 + ); + + /** + * The Key Schedule Array + * + * @var array + * @access private + */ + var $K = array(); + + /** + * The Key depended S-Table 0 + * + * @var array + * @access private + */ + var $S0 = array(); + + /** + * The Key depended S-Table 1 + * + * @var array + * @access private + */ + var $S1 = array(); + + /** + * The Key depended S-Table 2 + * + * @var array + * @access private + */ + var $S2 = array(); + + /** + * The Key depended S-Table 3 + * + * @var array + * @access private + */ + var $S3 = array(); + + /** + * Holds the last used key + * + * @var array + * @access private + */ + var $kl; + + /** + * The Key Length (in bytes) + * + * @see Crypt_Twofish::setKeyLength() + * @var int + * @access private + */ + var $key_length = 16; + + /** + * Default Constructor. + * + * Determines whether or not the mcrypt extension should be used. + * + * $mode could be: + * + * - CRYPT_MODE_ECB + * + * - CRYPT_MODE_CBC + * + * - CRYPT_MODE_CTR + * + * - CRYPT_MODE_CFB + * + * - CRYPT_MODE_OFB + * + * (or the alias constants of the chosen cipher, for example for AES: CRYPT_AES_MODE_ECB or CRYPT_AES_MODE_CBC ...) + * + * If not explicitly set, CRYPT_MODE_CBC will be used. + * + * @param int $mode + * @access public + */ + function __construct($mode = self::MODE_CBC) + { + parent::__construct($mode); + + $this->m0 = array_map('intval', $this->m0); + $this->m1 = array_map('intval', $this->m1); + $this->m2 = array_map('intval', $this->m2); + $this->m3 = array_map('intval', $this->m3); + $this->q0 = array_map('intval', $this->q0); + $this->q1 = array_map('intval', $this->q1); + } + + /** + * Sets the key length. + * + * Valid key lengths are 128, 192 or 256 bits + * + * @access public + * @param int $length + */ + function setKeyLength($length) + { + switch (true) { + case $length <= 128: + $this->key_length = 16; + break; + case $length <= 192: + $this->key_length = 24; + break; + default: + $this->key_length = 32; + } + + parent::setKeyLength($length); + } + + /** + * Setup the key (expansion) + * + * @see \phpseclib\Crypt\Base::_setupKey() + * @access private + */ + function _setupKey() + { + if (isset($this->kl['key']) && $this->key === $this->kl['key']) { + // already expanded + return; + } + $this->kl = array('key' => $this->key); + + /* Key expanding and generating the key-depended s-boxes */ + $le_longs = unpack('V*', $this->key); + $key = unpack('C*', $this->key); + $m0 = $this->m0; + $m1 = $this->m1; + $m2 = $this->m2; + $m3 = $this->m3; + $q0 = $this->q0; + $q1 = $this->q1; + + $K = $S0 = $S1 = $S2 = $S3 = array(); + + switch (strlen($this->key)) { + case 16: + list($s7, $s6, $s5, $s4) = $this->_mdsrem($le_longs[1], $le_longs[2]); + list($s3, $s2, $s1, $s0) = $this->_mdsrem($le_longs[3], $le_longs[4]); + for ($i = 0, $j = 1; $i < 40; $i+= 2, $j+= 2) { + $A = $m0[$q0[$q0[$i] ^ $key[ 9]] ^ $key[1]] ^ + $m1[$q0[$q1[$i] ^ $key[10]] ^ $key[2]] ^ + $m2[$q1[$q0[$i] ^ $key[11]] ^ $key[3]] ^ + $m3[$q1[$q1[$i] ^ $key[12]] ^ $key[4]]; + $B = $m0[$q0[$q0[$j] ^ $key[13]] ^ $key[5]] ^ + $m1[$q0[$q1[$j] ^ $key[14]] ^ $key[6]] ^ + $m2[$q1[$q0[$j] ^ $key[15]] ^ $key[7]] ^ + $m3[$q1[$q1[$j] ^ $key[16]] ^ $key[8]]; + $B = ($B << 8) | ($B >> 24 & 0xff); + $A = $this->safe_intval($A + $B); + $K[] = $A; + $A = $this->safe_intval($A + $B); + $K[] = ($A << 9 | $A >> 23 & 0x1ff); + } + for ($i = 0; $i < 256; ++$i) { + $S0[$i] = $m0[$q0[$q0[$i] ^ $s4] ^ $s0]; + $S1[$i] = $m1[$q0[$q1[$i] ^ $s5] ^ $s1]; + $S2[$i] = $m2[$q1[$q0[$i] ^ $s6] ^ $s2]; + $S3[$i] = $m3[$q1[$q1[$i] ^ $s7] ^ $s3]; + } + break; + case 24: + list($sb, $sa, $s9, $s8) = $this->_mdsrem($le_longs[1], $le_longs[2]); + list($s7, $s6, $s5, $s4) = $this->_mdsrem($le_longs[3], $le_longs[4]); + list($s3, $s2, $s1, $s0) = $this->_mdsrem($le_longs[5], $le_longs[6]); + for ($i = 0, $j = 1; $i < 40; $i+= 2, $j+= 2) { + $A = $m0[$q0[$q0[$q1[$i] ^ $key[17]] ^ $key[ 9]] ^ $key[1]] ^ + $m1[$q0[$q1[$q1[$i] ^ $key[18]] ^ $key[10]] ^ $key[2]] ^ + $m2[$q1[$q0[$q0[$i] ^ $key[19]] ^ $key[11]] ^ $key[3]] ^ + $m3[$q1[$q1[$q0[$i] ^ $key[20]] ^ $key[12]] ^ $key[4]]; + $B = $m0[$q0[$q0[$q1[$j] ^ $key[21]] ^ $key[13]] ^ $key[5]] ^ + $m1[$q0[$q1[$q1[$j] ^ $key[22]] ^ $key[14]] ^ $key[6]] ^ + $m2[$q1[$q0[$q0[$j] ^ $key[23]] ^ $key[15]] ^ $key[7]] ^ + $m3[$q1[$q1[$q0[$j] ^ $key[24]] ^ $key[16]] ^ $key[8]]; + $B = ($B << 8) | ($B >> 24 & 0xff); + $A = $this->safe_intval($A + $B); + $K[] = $A; + $A = $this->safe_intval($A + $B); + $K[] = ($A << 9 | $A >> 23 & 0x1ff); + } + for ($i = 0; $i < 256; ++$i) { + $S0[$i] = $m0[$q0[$q0[$q1[$i] ^ $s8] ^ $s4] ^ $s0]; + $S1[$i] = $m1[$q0[$q1[$q1[$i] ^ $s9] ^ $s5] ^ $s1]; + $S2[$i] = $m2[$q1[$q0[$q0[$i] ^ $sa] ^ $s6] ^ $s2]; + $S3[$i] = $m3[$q1[$q1[$q0[$i] ^ $sb] ^ $s7] ^ $s3]; + } + break; + default: // 32 + list($sf, $se, $sd, $sc) = $this->_mdsrem($le_longs[1], $le_longs[2]); + list($sb, $sa, $s9, $s8) = $this->_mdsrem($le_longs[3], $le_longs[4]); + list($s7, $s6, $s5, $s4) = $this->_mdsrem($le_longs[5], $le_longs[6]); + list($s3, $s2, $s1, $s0) = $this->_mdsrem($le_longs[7], $le_longs[8]); + for ($i = 0, $j = 1; $i < 40; $i+= 2, $j+= 2) { + $A = $m0[$q0[$q0[$q1[$q1[$i] ^ $key[25]] ^ $key[17]] ^ $key[ 9]] ^ $key[1]] ^ + $m1[$q0[$q1[$q1[$q0[$i] ^ $key[26]] ^ $key[18]] ^ $key[10]] ^ $key[2]] ^ + $m2[$q1[$q0[$q0[$q0[$i] ^ $key[27]] ^ $key[19]] ^ $key[11]] ^ $key[3]] ^ + $m3[$q1[$q1[$q0[$q1[$i] ^ $key[28]] ^ $key[20]] ^ $key[12]] ^ $key[4]]; + $B = $m0[$q0[$q0[$q1[$q1[$j] ^ $key[29]] ^ $key[21]] ^ $key[13]] ^ $key[5]] ^ + $m1[$q0[$q1[$q1[$q0[$j] ^ $key[30]] ^ $key[22]] ^ $key[14]] ^ $key[6]] ^ + $m2[$q1[$q0[$q0[$q0[$j] ^ $key[31]] ^ $key[23]] ^ $key[15]] ^ $key[7]] ^ + $m3[$q1[$q1[$q0[$q1[$j] ^ $key[32]] ^ $key[24]] ^ $key[16]] ^ $key[8]]; + $B = ($B << 8) | ($B >> 24 & 0xff); + $A = $this->safe_intval($A + $B); + $K[] = $A; + $A = $this->safe_intval($A + $B); + $K[] = ($A << 9 | $A >> 23 & 0x1ff); + } + for ($i = 0; $i < 256; ++$i) { + $S0[$i] = $m0[$q0[$q0[$q1[$q1[$i] ^ $sc] ^ $s8] ^ $s4] ^ $s0]; + $S1[$i] = $m1[$q0[$q1[$q1[$q0[$i] ^ $sd] ^ $s9] ^ $s5] ^ $s1]; + $S2[$i] = $m2[$q1[$q0[$q0[$q0[$i] ^ $se] ^ $sa] ^ $s6] ^ $s2]; + $S3[$i] = $m3[$q1[$q1[$q0[$q1[$i] ^ $sf] ^ $sb] ^ $s7] ^ $s3]; + } + } + + $this->K = $K; + $this->S0 = $S0; + $this->S1 = $S1; + $this->S2 = $S2; + $this->S3 = $S3; + } + + /** + * _mdsrem function using by the twofish cipher algorithm + * + * @access private + * @param string $A + * @param string $B + * @return array + */ + function _mdsrem($A, $B) + { + // No gain by unrolling this loop. + for ($i = 0; $i < 8; ++$i) { + // Get most significant coefficient. + $t = 0xff & ($B >> 24); + + // Shift the others up. + $B = ($B << 8) | (0xff & ($A >> 24)); + $A<<= 8; + + $u = $t << 1; + + // Subtract the modular polynomial on overflow. + if ($t & 0x80) { + $u^= 0x14d; + } + + // Remove t * (a * x^2 + 1). + $B ^= $t ^ ($u << 16); + + // Form u = a*t + t/a = t*(a + 1/a). + $u^= 0x7fffffff & ($t >> 1); + + // Add the modular polynomial on underflow. + if ($t & 0x01) { + $u^= 0xa6 ; + } + + // Remove t * (a + 1/a) * (x^3 + x). + $B^= ($u << 24) | ($u << 8); + } + + return array( + 0xff & $B >> 24, + 0xff & $B >> 16, + 0xff & $B >> 8, + 0xff & $B); + } + + /** + * Encrypts a block + * + * @access private + * @param string $in + * @return string + */ + function _encryptBlock($in) + { + $S0 = $this->S0; + $S1 = $this->S1; + $S2 = $this->S2; + $S3 = $this->S3; + $K = $this->K; + + $in = unpack("V4", $in); + $R0 = $K[0] ^ $in[1]; + $R1 = $K[1] ^ $in[2]; + $R2 = $K[2] ^ $in[3]; + $R3 = $K[3] ^ $in[4]; + + $ki = 7; + while ($ki < 39) { + $t0 = $S0[ $R0 & 0xff] ^ + $S1[($R0 >> 8) & 0xff] ^ + $S2[($R0 >> 16) & 0xff] ^ + $S3[($R0 >> 24) & 0xff]; + $t1 = $S0[($R1 >> 24) & 0xff] ^ + $S1[ $R1 & 0xff] ^ + $S2[($R1 >> 8) & 0xff] ^ + $S3[($R1 >> 16) & 0xff]; + $R2^= $this->safe_intval($t0 + $t1 + $K[++$ki]); + $R2 = ($R2 >> 1 & 0x7fffffff) | ($R2 << 31); + $R3 = ((($R3 >> 31) & 1) | ($R3 << 1)) ^ $this->safe_intval($t0 + ($t1 << 1) + $K[++$ki]); + + $t0 = $S0[ $R2 & 0xff] ^ + $S1[($R2 >> 8) & 0xff] ^ + $S2[($R2 >> 16) & 0xff] ^ + $S3[($R2 >> 24) & 0xff]; + $t1 = $S0[($R3 >> 24) & 0xff] ^ + $S1[ $R3 & 0xff] ^ + $S2[($R3 >> 8) & 0xff] ^ + $S3[($R3 >> 16) & 0xff]; + $R0^= $this->safe_intval($t0 + $t1 + $K[++$ki]); + $R0 = ($R0 >> 1 & 0x7fffffff) | ($R0 << 31); + $R1 = ((($R1 >> 31) & 1) | ($R1 << 1)) ^ $this->safe_intval($t0 + ($t1 << 1) + $K[++$ki]); + } + + // @codingStandardsIgnoreStart + return pack("V4", $K[4] ^ $R2, + $K[5] ^ $R3, + $K[6] ^ $R0, + $K[7] ^ $R1); + // @codingStandardsIgnoreEnd + } + + /** + * Decrypts a block + * + * @access private + * @param string $in + * @return string + */ + function _decryptBlock($in) + { + $S0 = $this->S0; + $S1 = $this->S1; + $S2 = $this->S2; + $S3 = $this->S3; + $K = $this->K; + + $in = unpack("V4", $in); + $R0 = $K[4] ^ $in[1]; + $R1 = $K[5] ^ $in[2]; + $R2 = $K[6] ^ $in[3]; + $R3 = $K[7] ^ $in[4]; + + $ki = 40; + while ($ki > 8) { + $t0 = $S0[$R0 & 0xff] ^ + $S1[$R0 >> 8 & 0xff] ^ + $S2[$R0 >> 16 & 0xff] ^ + $S3[$R0 >> 24 & 0xff]; + $t1 = $S0[$R1 >> 24 & 0xff] ^ + $S1[$R1 & 0xff] ^ + $S2[$R1 >> 8 & 0xff] ^ + $S3[$R1 >> 16 & 0xff]; + $R3^= $this->safe_intval($t0 + ($t1 << 1) + $K[--$ki]); + $R3 = $R3 >> 1 & 0x7fffffff | $R3 << 31; + $R2 = ($R2 >> 31 & 0x1 | $R2 << 1) ^ $this->safe_intval($t0 + $t1 + $K[--$ki]); + + $t0 = $S0[$R2 & 0xff] ^ + $S1[$R2 >> 8 & 0xff] ^ + $S2[$R2 >> 16 & 0xff] ^ + $S3[$R2 >> 24 & 0xff]; + $t1 = $S0[$R3 >> 24 & 0xff] ^ + $S1[$R3 & 0xff] ^ + $S2[$R3 >> 8 & 0xff] ^ + $S3[$R3 >> 16 & 0xff]; + $R1^= $this->safe_intval($t0 + ($t1 << 1) + $K[--$ki]); + $R1 = $R1 >> 1 & 0x7fffffff | $R1 << 31; + $R0 = ($R0 >> 31 & 0x1 | $R0 << 1) ^ $this->safe_intval($t0 + $t1 + $K[--$ki]); + } + + // @codingStandardsIgnoreStart + return pack("V4", $K[0] ^ $R2, + $K[1] ^ $R3, + $K[2] ^ $R0, + $K[3] ^ $R1); + // @codingStandardsIgnoreEnd + } + + /** + * Setup the performance-optimized function for de/encrypt() + * + * @see \phpseclib\Crypt\Base::_setupInlineCrypt() + * @access private + */ + function _setupInlineCrypt() + { + $lambda_functions =& self::_getLambdaFunctions(); + + // Max. 10 Ultra-Hi-optimized inline-crypt functions. After that, we'll (still) create very fast code, but not the ultimate fast one. + // (Currently, for Crypt_Twofish, one generated $lambda_function cost on php5.5@32bit ~140kb unfreeable mem and ~240kb on php5.5@64bit) + $gen_hi_opt_code = (bool)(count($lambda_functions) < 10); + + // Generation of a unique hash for our generated code + $code_hash = "Crypt_Twofish, {$this->mode}"; + if ($gen_hi_opt_code) { + $code_hash = str_pad($code_hash, 32) . $this->_hashInlineCryptFunction($this->key); + } + + $safeint = $this->safe_intval_inline(); + + if (!isset($lambda_functions[$code_hash])) { + switch (true) { + case $gen_hi_opt_code: + $K = $this->K; + $init_crypt = ' + static $S0, $S1, $S2, $S3; + if (!$S0) { + for ($i = 0; $i < 256; ++$i) { + $S0[] = (int)$self->S0[$i]; + $S1[] = (int)$self->S1[$i]; + $S2[] = (int)$self->S2[$i]; + $S3[] = (int)$self->S3[$i]; + } + } + '; + break; + default: + $K = array(); + for ($i = 0; $i < 40; ++$i) { + $K[] = '$K_' . $i; + } + $init_crypt = ' + $S0 = $self->S0; + $S1 = $self->S1; + $S2 = $self->S2; + $S3 = $self->S3; + list(' . implode(',', $K) . ') = $self->K; + '; + } + + // Generating encrypt code: + $encrypt_block = ' + $in = unpack("V4", $in); + $R0 = '.$K[0].' ^ $in[1]; + $R1 = '.$K[1].' ^ $in[2]; + $R2 = '.$K[2].' ^ $in[3]; + $R3 = '.$K[3].' ^ $in[4]; + '; + for ($ki = 7, $i = 0; $i < 8; ++$i) { + $encrypt_block.= ' + $t0 = $S0[ $R0 & 0xff] ^ + $S1[($R0 >> 8) & 0xff] ^ + $S2[($R0 >> 16) & 0xff] ^ + $S3[($R0 >> 24) & 0xff]; + $t1 = $S0[($R1 >> 24) & 0xff] ^ + $S1[ $R1 & 0xff] ^ + $S2[($R1 >> 8) & 0xff] ^ + $S3[($R1 >> 16) & 0xff]; + $R2^= ' . sprintf($safeint, '$t0 + $t1 + ' . $K[++$ki]) . '; + $R2 = ($R2 >> 1 & 0x7fffffff) | ($R2 << 31); + $R3 = ((($R3 >> 31) & 1) | ($R3 << 1)) ^ ' . sprintf($safeint, '($t0 + ($t1 << 1) + ' . $K[++$ki] . ')') . '; + + $t0 = $S0[ $R2 & 0xff] ^ + $S1[($R2 >> 8) & 0xff] ^ + $S2[($R2 >> 16) & 0xff] ^ + $S3[($R2 >> 24) & 0xff]; + $t1 = $S0[($R3 >> 24) & 0xff] ^ + $S1[ $R3 & 0xff] ^ + $S2[($R3 >> 8) & 0xff] ^ + $S3[($R3 >> 16) & 0xff]; + $R0^= ' . sprintf($safeint, '($t0 + $t1 + ' . $K[++$ki] . ')') . '; + $R0 = ($R0 >> 1 & 0x7fffffff) | ($R0 << 31); + $R1 = ((($R1 >> 31) & 1) | ($R1 << 1)) ^ ' . sprintf($safeint, '($t0 + ($t1 << 1) + ' . $K[++$ki] . ')') . '; + '; + } + $encrypt_block.= ' + $in = pack("V4", ' . $K[4] . ' ^ $R2, + ' . $K[5] . ' ^ $R3, + ' . $K[6] . ' ^ $R0, + ' . $K[7] . ' ^ $R1); + '; + + // Generating decrypt code: + $decrypt_block = ' + $in = unpack("V4", $in); + $R0 = '.$K[4].' ^ $in[1]; + $R1 = '.$K[5].' ^ $in[2]; + $R2 = '.$K[6].' ^ $in[3]; + $R3 = '.$K[7].' ^ $in[4]; + '; + for ($ki = 40, $i = 0; $i < 8; ++$i) { + $decrypt_block.= ' + $t0 = $S0[$R0 & 0xff] ^ + $S1[$R0 >> 8 & 0xff] ^ + $S2[$R0 >> 16 & 0xff] ^ + $S3[$R0 >> 24 & 0xff]; + $t1 = $S0[$R1 >> 24 & 0xff] ^ + $S1[$R1 & 0xff] ^ + $S2[$R1 >> 8 & 0xff] ^ + $S3[$R1 >> 16 & 0xff]; + $R3^= ' . sprintf($safeint, '$t0 + ($t1 << 1) + ' . $K[--$ki]) . '; + $R3 = $R3 >> 1 & 0x7fffffff | $R3 << 31; + $R2 = ($R2 >> 31 & 0x1 | $R2 << 1) ^ ' . sprintf($safeint, '($t0 + $t1 + '.$K[--$ki] . ')') . '; + + $t0 = $S0[$R2 & 0xff] ^ + $S1[$R2 >> 8 & 0xff] ^ + $S2[$R2 >> 16 & 0xff] ^ + $S3[$R2 >> 24 & 0xff]; + $t1 = $S0[$R3 >> 24 & 0xff] ^ + $S1[$R3 & 0xff] ^ + $S2[$R3 >> 8 & 0xff] ^ + $S3[$R3 >> 16 & 0xff]; + $R1^= ' . sprintf($safeint, '$t0 + ($t1 << 1) + ' . $K[--$ki]) . '; + $R1 = $R1 >> 1 & 0x7fffffff | $R1 << 31; + $R0 = ($R0 >> 31 & 0x1 | $R0 << 1) ^ ' . sprintf($safeint, '($t0 + $t1 + '.$K[--$ki] . ')') . '; + '; + } + $decrypt_block.= ' + $in = pack("V4", ' . $K[0] . ' ^ $R2, + ' . $K[1] . ' ^ $R3, + ' . $K[2] . ' ^ $R0, + ' . $K[3] . ' ^ $R1); + '; + + $lambda_functions[$code_hash] = $this->_createInlineCryptFunction( + array( + 'init_crypt' => $init_crypt, + 'init_encrypt' => '', + 'init_decrypt' => '', + 'encrypt_block' => $encrypt_block, + 'decrypt_block' => $decrypt_block + ) + ); + } + $this->inline_crypt = $lambda_functions[$code_hash]; + } +} diff --git a/3rdparty/phpseclib/phpseclib/phpseclib/File/ANSI.php b/3rdparty/phpseclib/phpseclib/phpseclib/File/ANSI.php new file mode 100644 index 00000000..b6874d35 --- /dev/null +++ b/3rdparty/phpseclib/phpseclib/phpseclib/File/ANSI.php @@ -0,0 +1,577 @@ + + * @copyright 2012 Jim Wigginton + * @license http://www.opensource.org/licenses/mit-license.html MIT License + * @link http://phpseclib.sourceforge.net + */ + +namespace phpseclib\File; + +/** + * Pure-PHP ANSI Decoder + * + * @package ANSI + * @author Jim Wigginton + * @access public + */ +class ANSI +{ + /** + * Max Width + * + * @var int + * @access private + */ + var $max_x; + + /** + * Max Height + * + * @var int + * @access private + */ + var $max_y; + + /** + * Max History + * + * @var int + * @access private + */ + var $max_history; + + /** + * History + * + * @var array + * @access private + */ + var $history; + + /** + * History Attributes + * + * @var array + * @access private + */ + var $history_attrs; + + /** + * Current Column + * + * @var int + * @access private + */ + var $x; + + /** + * Current Row + * + * @var int + * @access private + */ + var $y; + + /** + * Old Column + * + * @var int + * @access private + */ + var $old_x; + + /** + * Old Row + * + * @var int + * @access private + */ + var $old_y; + + /** + * An empty attribute cell + * + * @var object + * @access private + */ + var $base_attr_cell; + + /** + * The current attribute cell + * + * @var object + * @access private + */ + var $attr_cell; + + /** + * An empty attribute row + * + * @var array + * @access private + */ + var $attr_row; + + /** + * The current screen text + * + * @var array + * @access private + */ + var $screen; + + /** + * The current screen attributes + * + * @var array + * @access private + */ + var $attrs; + + /** + * Current ANSI code + * + * @var string + * @access private + */ + var $ansi; + + /** + * Tokenization + * + * @var array + * @access private + */ + var $tokenization; + + /** + * Default Constructor. + * + * @return \phpseclib\File\ANSI + * @access public + */ + function __construct() + { + $attr_cell = new \stdClass(); + $attr_cell->bold = false; + $attr_cell->underline = false; + $attr_cell->blink = false; + $attr_cell->background = 'black'; + $attr_cell->foreground = 'white'; + $attr_cell->reverse = false; + $this->base_attr_cell = clone $attr_cell; + $this->attr_cell = clone $attr_cell; + + $this->setHistory(200); + $this->setDimensions(80, 24); + } + + /** + * Set terminal width and height + * + * Resets the screen as well + * + * @param int $x + * @param int $y + * @access public + */ + function setDimensions($x, $y) + { + $this->max_x = $x - 1; + $this->max_y = $y - 1; + $this->x = $this->y = 0; + $this->history = $this->history_attrs = array(); + $this->attr_row = array_fill(0, $this->max_x + 2, $this->base_attr_cell); + $this->screen = array_fill(0, $this->max_y + 1, ''); + $this->attrs = array_fill(0, $this->max_y + 1, $this->attr_row); + $this->ansi = ''; + } + + /** + * Set the number of lines that should be logged past the terminal height + * + * @param int $history + * @access public + */ + function setHistory($history) + { + $this->max_history = $history; + } + + /** + * Load a string + * + * @param string $source + * @access public + */ + function loadString($source) + { + $this->setDimensions($this->max_x + 1, $this->max_y + 1); + $this->appendString($source); + } + + /** + * Appdend a string + * + * @param string $source + * @access public + */ + function appendString($source) + { + $this->tokenization = array(''); + for ($i = 0; $i < strlen($source); $i++) { + if (strlen($this->ansi)) { + $this->ansi.= $source[$i]; + $chr = ord($source[$i]); + // http://en.wikipedia.org/wiki/ANSI_escape_code#Sequence_elements + // single character CSI's not currently supported + switch (true) { + case $this->ansi == "\x1B=": + $this->ansi = ''; + continue 2; + case strlen($this->ansi) == 2 && $chr >= 64 && $chr <= 95 && $chr != ord('['): + case strlen($this->ansi) > 2 && $chr >= 64 && $chr <= 126: + break; + default: + continue 2; + } + $this->tokenization[] = $this->ansi; + $this->tokenization[] = ''; + // http://ascii-table.com/ansi-escape-sequences-vt-100.php + switch ($this->ansi) { + case "\x1B[H": // Move cursor to upper left corner + $this->old_x = $this->x; + $this->old_y = $this->y; + $this->x = $this->y = 0; + break; + case "\x1B[J": // Clear screen from cursor down + $this->history = array_merge($this->history, array_slice(array_splice($this->screen, $this->y + 1), 0, $this->old_y)); + $this->screen = array_merge($this->screen, array_fill($this->y, $this->max_y, '')); + + $this->history_attrs = array_merge($this->history_attrs, array_slice(array_splice($this->attrs, $this->y + 1), 0, $this->old_y)); + $this->attrs = array_merge($this->attrs, array_fill($this->y, $this->max_y, $this->attr_row)); + + if (count($this->history) == $this->max_history) { + array_shift($this->history); + array_shift($this->history_attrs); + } + case "\x1B[K": // Clear screen from cursor right + $this->screen[$this->y] = substr($this->screen[$this->y], 0, $this->x); + + array_splice($this->attrs[$this->y], $this->x + 1, $this->max_x - $this->x, array_fill($this->x, $this->max_x - ($this->x - 1), $this->base_attr_cell)); + break; + case "\x1B[2K": // Clear entire line + $this->screen[$this->y] = str_repeat(' ', $this->x); + $this->attrs[$this->y] = $this->attr_row; + break; + case "\x1B[?1h": // set cursor key to application + case "\x1B[?25h": // show the cursor + case "\x1B(B": // set united states g0 character set + break; + case "\x1BE": // Move to next line + $this->_newLine(); + $this->x = 0; + break; + default: + switch (true) { + case preg_match('#\x1B\[(\d+)B#', $this->ansi, $match): // Move cursor down n lines + $this->old_y = $this->y; + $this->y+= $match[1]; + break; + case preg_match('#\x1B\[(\d+);(\d+)H#', $this->ansi, $match): // Move cursor to screen location v,h + $this->old_x = $this->x; + $this->old_y = $this->y; + $this->x = $match[2] - 1; + $this->y = $match[1] - 1; + break; + case preg_match('#\x1B\[(\d+)C#', $this->ansi, $match): // Move cursor right n lines + $this->old_x = $this->x; + $this->x+= $match[1]; + break; + case preg_match('#\x1B\[(\d+)D#', $this->ansi, $match): // Move cursor left n lines + $this->old_x = $this->x; + $this->x-= $match[1]; + if ($this->x < 0) { + $this->x = 0; + } + break; + case preg_match('#\x1B\[(\d+);(\d+)r#', $this->ansi, $match): // Set top and bottom lines of a window + break; + case preg_match('#\x1B\[(\d*(?:;\d*)*)m#', $this->ansi, $match): // character attributes + $attr_cell = &$this->attr_cell; + $mods = explode(';', $match[1]); + foreach ($mods as $mod) { + switch ($mod) { + case '': + case '0': // Turn off character attributes + $attr_cell = clone $this->base_attr_cell; + break; + case '1': // Turn bold mode on + $attr_cell->bold = true; + break; + case '4': // Turn underline mode on + $attr_cell->underline = true; + break; + case '5': // Turn blinking mode on + $attr_cell->blink = true; + break; + case '7': // Turn reverse video on + $attr_cell->reverse = !$attr_cell->reverse; + $temp = $attr_cell->background; + $attr_cell->background = $attr_cell->foreground; + $attr_cell->foreground = $temp; + break; + default: // set colors + //$front = $attr_cell->reverse ? &$attr_cell->background : &$attr_cell->foreground; + $front = &$attr_cell->{ $attr_cell->reverse ? 'background' : 'foreground' }; + //$back = $attr_cell->reverse ? &$attr_cell->foreground : &$attr_cell->background; + $back = &$attr_cell->{ $attr_cell->reverse ? 'foreground' : 'background' }; + switch ($mod) { + // @codingStandardsIgnoreStart + case '30': $front = 'black'; break; + case '31': $front = 'red'; break; + case '32': $front = 'green'; break; + case '33': $front = 'yellow'; break; + case '34': $front = 'blue'; break; + case '35': $front = 'magenta'; break; + case '36': $front = 'cyan'; break; + case '37': $front = 'white'; break; + + case '40': $back = 'black'; break; + case '41': $back = 'red'; break; + case '42': $back = 'green'; break; + case '43': $back = 'yellow'; break; + case '44': $back = 'blue'; break; + case '45': $back = 'magenta'; break; + case '46': $back = 'cyan'; break; + case '47': $back = 'white'; break; + // @codingStandardsIgnoreEnd + + default: + //user_error('Unsupported attribute: ' . $mod); + $this->ansi = ''; + break 2; + } + } + } + break; + default: + //user_error("{$this->ansi} is unsupported\r\n"); + } + } + $this->ansi = ''; + continue; + } + + $this->tokenization[count($this->tokenization) - 1].= $source[$i]; + switch ($source[$i]) { + case "\r": + $this->x = 0; + break; + case "\n": + $this->_newLine(); + break; + case "\x08": // backspace + if ($this->x) { + $this->x--; + $this->attrs[$this->y][$this->x] = clone $this->base_attr_cell; + $this->screen[$this->y] = substr_replace( + $this->screen[$this->y], + $source[$i], + $this->x, + 1 + ); + } + break; + case "\x0F": // shift + break; + case "\x1B": // start ANSI escape code + $this->tokenization[count($this->tokenization) - 1] = substr($this->tokenization[count($this->tokenization) - 1], 0, -1); + //if (!strlen($this->tokenization[count($this->tokenization) - 1])) { + // array_pop($this->tokenization); + //} + $this->ansi.= "\x1B"; + break; + default: + $this->attrs[$this->y][$this->x] = clone $this->attr_cell; + if ($this->x > strlen($this->screen[$this->y])) { + $this->screen[$this->y] = str_repeat(' ', $this->x); + } + $this->screen[$this->y] = substr_replace( + $this->screen[$this->y], + $source[$i], + $this->x, + 1 + ); + + if ($this->x > $this->max_x) { + $this->x = 0; + $this->_newLine(); + } else { + $this->x++; + } + } + } + } + + /** + * Add a new line + * + * Also update the $this->screen and $this->history buffers + * + * @access private + */ + function _newLine() + { + //if ($this->y < $this->max_y) { + // $this->y++; + //} + + while ($this->y >= $this->max_y) { + $this->history = array_merge($this->history, array(array_shift($this->screen))); + $this->screen[] = ''; + + $this->history_attrs = array_merge($this->history_attrs, array(array_shift($this->attrs))); + $this->attrs[] = $this->attr_row; + + if (count($this->history) >= $this->max_history) { + array_shift($this->history); + array_shift($this->history_attrs); + } + + $this->y--; + } + $this->y++; + } + + /** + * Returns the current coordinate without preformating + * + * @access private + * @return string + */ + function _processCoordinate($last_attr, $cur_attr, $char) + { + $output = ''; + + if ($last_attr != $cur_attr) { + $close = $open = ''; + if ($last_attr->foreground != $cur_attr->foreground) { + if ($cur_attr->foreground != 'white') { + $open.= ''; + } + if ($last_attr->foreground != 'white') { + $close = '' . $close; + } + } + if ($last_attr->background != $cur_attr->background) { + if ($cur_attr->background != 'black') { + $open.= ''; + } + if ($last_attr->background != 'black') { + $close = '' . $close; + } + } + if ($last_attr->bold != $cur_attr->bold) { + if ($cur_attr->bold) { + $open.= ''; + } else { + $close = '' . $close; + } + } + if ($last_attr->underline != $cur_attr->underline) { + if ($cur_attr->underline) { + $open.= ''; + } else { + $close = '' . $close; + } + } + if ($last_attr->blink != $cur_attr->blink) { + if ($cur_attr->blink) { + $open.= ''; + } else { + $close = '' . $close; + } + } + $output.= $close . $open; + } + + $output.= htmlspecialchars($char); + + return $output; + } + + /** + * Returns the current screen without preformating + * + * @access private + * @return string + */ + function _getScreen() + { + $output = ''; + $last_attr = $this->base_attr_cell; + for ($i = 0; $i <= $this->max_y; $i++) { + for ($j = 0; $j <= $this->max_x; $j++) { + $cur_attr = $this->attrs[$i][$j]; + $output.= $this->_processCoordinate($last_attr, $cur_attr, isset($this->screen[$i][$j]) ? $this->screen[$i][$j] : ''); + $last_attr = $this->attrs[$i][$j]; + } + $output.= "\r\n"; + } + $output = substr($output, 0, -2); + // close any remaining open tags + $output.= $this->_processCoordinate($last_attr, $this->base_attr_cell, ''); + return rtrim($output); + } + + /** + * Returns the current screen + * + * @access public + * @return string + */ + function getScreen() + { + return '
' . $this->_getScreen() . '
'; + } + + /** + * Returns the current screen and the x previous lines + * + * @access public + * @return string + */ + function getHistory() + { + $scrollback = ''; + $last_attr = $this->base_attr_cell; + for ($i = 0; $i < count($this->history); $i++) { + for ($j = 0; $j <= $this->max_x + 1; $j++) { + $cur_attr = $this->history_attrs[$i][$j]; + $scrollback.= $this->_processCoordinate($last_attr, $cur_attr, isset($this->history[$i][$j]) ? $this->history[$i][$j] : ''); + $last_attr = $this->history_attrs[$i][$j]; + } + $scrollback.= "\r\n"; + } + $base_attr_cell = $this->base_attr_cell; + $this->base_attr_cell = $last_attr; + $scrollback.= $this->_getScreen(); + $this->base_attr_cell = $base_attr_cell; + + return '
' . $scrollback . '
'; + } +} diff --git a/3rdparty/phpseclib/phpseclib/phpseclib/File/ASN1.php b/3rdparty/phpseclib/phpseclib/phpseclib/File/ASN1.php new file mode 100644 index 00000000..dba99de7 --- /dev/null +++ b/3rdparty/phpseclib/phpseclib/phpseclib/File/ASN1.php @@ -0,0 +1,1474 @@ + + * @copyright 2012 Jim Wigginton + * @license http://www.opensource.org/licenses/mit-license.html MIT License + * @link http://phpseclib.sourceforge.net + */ + +namespace phpseclib\File; + +use phpseclib\File\ASN1\Element; +use phpseclib\Math\BigInteger; +use DateTime; +use DateTimeZone; + +/** + * Pure-PHP ASN.1 Parser + * + * @package ASN1 + * @author Jim Wigginton + * @access public + */ +class ASN1 +{ + /**#@+ + * Tag Classes + * + * @access private + * @link http://www.itu.int/ITU-T/studygroups/com17/languages/X.690-0207.pdf#page=12 + */ + const CLASS_UNIVERSAL = 0; + const CLASS_APPLICATION = 1; + const CLASS_CONTEXT_SPECIFIC = 2; + const CLASS_PRIVATE = 3; + /**#@-*/ + + /**#@+ + * Tag Classes + * + * @access private + * @link http://www.obj-sys.com/asn1tutorial/node124.html + */ + const TYPE_BOOLEAN = 1; + const TYPE_INTEGER = 2; + const TYPE_BIT_STRING = 3; + const TYPE_OCTET_STRING = 4; + const TYPE_NULL = 5; + const TYPE_OBJECT_IDENTIFIER = 6; + //const TYPE_OBJECT_DESCRIPTOR = 7; + //const TYPE_INSTANCE_OF = 8; // EXTERNAL + const TYPE_REAL = 9; + const TYPE_ENUMERATED = 10; + //const TYPE_EMBEDDED = 11; + const TYPE_UTF8_STRING = 12; + //const TYPE_RELATIVE_OID = 13; + const TYPE_SEQUENCE = 16; // SEQUENCE OF + const TYPE_SET = 17; // SET OF + /**#@-*/ + /**#@+ + * More Tag Classes + * + * @access private + * @link http://www.obj-sys.com/asn1tutorial/node10.html + */ + const TYPE_NUMERIC_STRING = 18; + const TYPE_PRINTABLE_STRING = 19; + const TYPE_TELETEX_STRING = 20; // T61String + const TYPE_VIDEOTEX_STRING = 21; + const TYPE_IA5_STRING = 22; + const TYPE_UTC_TIME = 23; + const TYPE_GENERALIZED_TIME = 24; + const TYPE_GRAPHIC_STRING = 25; + const TYPE_VISIBLE_STRING = 26; // ISO646String + const TYPE_GENERAL_STRING = 27; + const TYPE_UNIVERSAL_STRING = 28; + //const TYPE_CHARACTER_STRING = 29; + const TYPE_BMP_STRING = 30; + /**#@-*/ + + /**#@+ + * Tag Aliases + * + * These tags are kinda place holders for other tags. + * + * @access private + */ + const TYPE_CHOICE = -1; + const TYPE_ANY = -2; + /**#@-*/ + + /** + * ASN.1 object identifier + * + * @var array + * @access private + * @link http://en.wikipedia.org/wiki/Object_identifier + */ + var $oids = array(); + + /** + * Default date format + * + * @var string + * @access private + * @link http://php.net/class.datetime + */ + var $format = 'D, d M Y H:i:s O'; + + /** + * Default date format + * + * @var array + * @access private + * @see self::setTimeFormat() + * @see self::asn1map() + * @link http://php.net/class.datetime + */ + var $encoded; + + /** + * Filters + * + * If the mapping type is self::TYPE_ANY what do we actually encode it as? + * + * @var array + * @access private + * @see self::_encode_der() + */ + var $filters; + + /** + * Current Location of most recent ASN.1 encode process + * + * Useful for debug purposes + * + * @var array + * @see self::encode_der() + */ + var $location; + + /** + * Type mapping table for the ANY type. + * + * Structured or unknown types are mapped to a \phpseclib\File\ASN1\Element. + * Unambiguous types get the direct mapping (int/real/bool). + * Others are mapped as a choice, with an extra indexing level. + * + * @var array + * @access public + */ + var $ANYmap = array( + self::TYPE_BOOLEAN => true, + self::TYPE_INTEGER => true, + self::TYPE_BIT_STRING => 'bitString', + self::TYPE_OCTET_STRING => 'octetString', + self::TYPE_NULL => 'null', + self::TYPE_OBJECT_IDENTIFIER => 'objectIdentifier', + self::TYPE_REAL => true, + self::TYPE_ENUMERATED => 'enumerated', + self::TYPE_UTF8_STRING => 'utf8String', + self::TYPE_NUMERIC_STRING => 'numericString', + self::TYPE_PRINTABLE_STRING => 'printableString', + self::TYPE_TELETEX_STRING => 'teletexString', + self::TYPE_VIDEOTEX_STRING => 'videotexString', + self::TYPE_IA5_STRING => 'ia5String', + self::TYPE_UTC_TIME => 'utcTime', + self::TYPE_GENERALIZED_TIME => 'generalTime', + self::TYPE_GRAPHIC_STRING => 'graphicString', + self::TYPE_VISIBLE_STRING => 'visibleString', + self::TYPE_GENERAL_STRING => 'generalString', + self::TYPE_UNIVERSAL_STRING => 'universalString', + //self::TYPE_CHARACTER_STRING => 'characterString', + self::TYPE_BMP_STRING => 'bmpString' + ); + + /** + * String type to character size mapping table. + * + * Non-convertable types are absent from this table. + * size == 0 indicates variable length encoding. + * + * @var array + * @access public + */ + var $stringTypeSize = array( + self::TYPE_UTF8_STRING => 0, + self::TYPE_BMP_STRING => 2, + self::TYPE_UNIVERSAL_STRING => 4, + self::TYPE_PRINTABLE_STRING => 1, + self::TYPE_TELETEX_STRING => 1, + self::TYPE_IA5_STRING => 1, + self::TYPE_VISIBLE_STRING => 1, + ); + + /** + * Parse BER-encoding + * + * Serves a similar purpose to openssl's asn1parse + * + * @param string $encoded + * @return array + * @access public + */ + function decodeBER($encoded) + { + if ($encoded instanceof Element) { + $encoded = $encoded->element; + } + + $this->encoded = $encoded; + // encapsulate in an array for BC with the old decodeBER + return array($this->_decode_ber($encoded)); + } + + /** + * Parse BER-encoding (Helper function) + * + * Sometimes we want to get the BER encoding of a particular tag. $start lets us do that without having to reencode. + * $encoded is passed by reference for the recursive calls done for self::TYPE_BIT_STRING and + * self::TYPE_OCTET_STRING. In those cases, the indefinite length is used. + * + * @param string $encoded + * @param int $start + * @param int $encoded_pos + * @return array + * @access private + */ + function _decode_ber($encoded, $start = 0, $encoded_pos = 0) + { + $current = array('start' => $start); + + if (!isset($encoded[$encoded_pos])) { + return false; + } + $type = ord($encoded[$encoded_pos++]); + $startOffset = 1; + + $constructed = ($type >> 5) & 1; + + $tag = $type & 0x1F; + if ($tag == 0x1F) { + $tag = 0; + // process septets (since the eighth bit is ignored, it's not an octet) + do { + if (!isset($encoded[$encoded_pos])) { + return false; + } + $temp = ord($encoded[$encoded_pos++]); + $startOffset++; + $loop = $temp >> 7; + $tag <<= 7; + $temp &= 0x7F; + // "bits 7 to 1 of the first subsequent octet shall not all be zero" + if ($startOffset == 2 && $temp == 0) { + return false; + } + $tag |= $temp; + } while ($loop); + } + + $start+= $startOffset; + + // Length, as discussed in paragraph 8.1.3 of X.690-0207.pdf#page=13 + if (!isset($encoded[$encoded_pos])) { + return false; + } + $length = ord($encoded[$encoded_pos++]); + $start++; + if ($length == 0x80) { // indefinite length + // "[A sender shall] use the indefinite form (see 8.1.3.6) if the encoding is constructed and is not all + // immediately available." -- paragraph 8.1.3.2.c + $length = strlen($encoded) - $encoded_pos; + } elseif ($length & 0x80) { // definite length, long form + // technically, the long form of the length can be represented by up to 126 octets (bytes), but we'll only + // support it up to four. + $length&= 0x7F; + $temp = substr($encoded, $encoded_pos, $length); + $encoded_pos += $length; + // tags of indefinte length don't really have a header length; this length includes the tag + $current+= array('headerlength' => $length + 2); + $start+= $length; + extract(unpack('Nlength', substr(str_pad($temp, 4, chr(0), STR_PAD_LEFT), -4))); + } else { + $current+= array('headerlength' => 2); + } + + if ($length > (strlen($encoded) - $encoded_pos)) { + return false; + } + + $content = substr($encoded, $encoded_pos, $length); + $content_pos = 0; + + // at this point $length can be overwritten. it's only accurate for definite length things as is + + /* Class is UNIVERSAL, APPLICATION, PRIVATE, or CONTEXT-SPECIFIC. The UNIVERSAL class is restricted to the ASN.1 + built-in types. It defines an application-independent data type that must be distinguishable from all other + data types. The other three classes are user defined. The APPLICATION class distinguishes data types that + have a wide, scattered use within a particular presentation context. PRIVATE distinguishes data types within + a particular organization or country. CONTEXT-SPECIFIC distinguishes members of a sequence or set, the + alternatives of a CHOICE, or universally tagged set members. Only the class number appears in braces for this + data type; the term CONTEXT-SPECIFIC does not appear. + + -- http://www.obj-sys.com/asn1tutorial/node12.html */ + $class = ($type >> 6) & 3; + switch ($class) { + case self::CLASS_APPLICATION: + case self::CLASS_PRIVATE: + case self::CLASS_CONTEXT_SPECIFIC: + if (!$constructed) { + return array( + 'type' => $class, + 'constant' => $tag, + 'content' => $content, + 'length' => $length + $start - $current['start'] + ); + } + + $newcontent = array(); + $remainingLength = $length; + while ($remainingLength > 0) { + $temp = $this->_decode_ber($content, $start, $content_pos); + if ($temp === false) { + break; + } + $length = $temp['length']; + // end-of-content octets - see paragraph 8.1.5 + if (substr($content, $content_pos + $length, 2) == "\0\0") { + $length+= 2; + $start+= $length; + $newcontent[] = $temp; + break; + } + $start+= $length; + $remainingLength-= $length; + $newcontent[] = $temp; + $content_pos += $length; + } + + return array( + 'type' => $class, + 'constant' => $tag, + // the array encapsulation is for BC with the old format + 'content' => $newcontent, + // the only time when $content['headerlength'] isn't defined is when the length is indefinite. + // the absence of $content['headerlength'] is how we know if something is indefinite or not. + // technically, it could be defined to be 2 and then another indicator could be used but whatever. + 'length' => $start - $current['start'] + ) + $current; + } + + $current+= array('type' => $tag); + + // decode UNIVERSAL tags + switch ($tag) { + case self::TYPE_BOOLEAN: + // "The contents octets shall consist of a single octet." -- paragraph 8.2.1 + if ($constructed || strlen($content) != 1) { + return false; + } + $current['content'] = (bool) ord($content[$content_pos]); + break; + case self::TYPE_INTEGER: + case self::TYPE_ENUMERATED: + if ($constructed) { + return false; + } + $current['content'] = new BigInteger(substr($content, $content_pos), -256); + break; + case self::TYPE_REAL: // not currently supported + return false; + case self::TYPE_BIT_STRING: + // The initial octet shall encode, as an unsigned binary integer with bit 1 as the least significant bit, + // the number of unused bits in the final subsequent octet. The number shall be in the range zero to + // seven. + if (!$constructed) { + $current['content'] = substr($content, $content_pos); + } else { + $temp = $this->_decode_ber($content, $start, $content_pos); + if ($temp === false) { + return false; + } + $length-= (strlen($content) - $content_pos); + $last = count($temp) - 1; + for ($i = 0; $i < $last; $i++) { + // all subtags should be bit strings + if ($temp[$i]['type'] != self::TYPE_BIT_STRING) { + return false; + } + $current['content'].= substr($temp[$i]['content'], 1); + } + // all subtags should be bit strings + if ($temp[$last]['type'] != self::TYPE_BIT_STRING) { + return false; + } + $current['content'] = $temp[$last]['content'][0] . $current['content'] . substr($temp[$i]['content'], 1); + } + break; + case self::TYPE_OCTET_STRING: + if (!$constructed) { + $current['content'] = substr($content, $content_pos); + } else { + $current['content'] = ''; + $length = 0; + while (substr($content, $content_pos, 2) != "\0\0") { + $temp = $this->_decode_ber($content, $length + $start, $content_pos); + if ($temp === false) { + return false; + } + $content_pos += $temp['length']; + // all subtags should be octet strings + if ($temp['type'] != self::TYPE_OCTET_STRING) { + return false; + } + $current['content'].= $temp['content']; + $length+= $temp['length']; + } + if (substr($content, $content_pos, 2) == "\0\0") { + $length+= 2; // +2 for the EOC + } + } + break; + case self::TYPE_NULL: + // "The contents octets shall not contain any octets." -- paragraph 8.8.2 + if ($constructed || strlen($content)) { + return false; + } + break; + case self::TYPE_SEQUENCE: + case self::TYPE_SET: + if (!$constructed) { + return false; + } + $offset = 0; + $current['content'] = array(); + $content_len = strlen($content); + while ($content_pos < $content_len) { + // if indefinite length construction was used and we have an end-of-content string next + // see paragraphs 8.1.1.3, 8.1.3.2, 8.1.3.6, 8.1.5, and (for an example) 8.6.4.2 + if (!isset($current['headerlength']) && substr($content, $content_pos, 2) == "\0\0") { + $length = $offset + 2; // +2 for the EOC + break 2; + } + $temp = $this->_decode_ber($content, $start + $offset, $content_pos); + if ($temp === false) { + return false; + } + $content_pos += $temp['length']; + $current['content'][] = $temp; + $offset+= $temp['length']; + } + break; + case self::TYPE_OBJECT_IDENTIFIER: + if ($constructed) { + return false; + } + $current['content'] = $this->_decodeOID(substr($content, $content_pos)); + if ($current['content'] === false) { + return false; + } + break; + /* Each character string type shall be encoded as if it had been declared: + [UNIVERSAL x] IMPLICIT OCTET STRING + + -- X.690-0207.pdf#page=23 (paragraph 8.21.3) + + Per that, we're not going to do any validation. If there are any illegal characters in the string, + we don't really care */ + case self::TYPE_NUMERIC_STRING: + // 0,1,2,3,4,5,6,7,8,9, and space + case self::TYPE_PRINTABLE_STRING: + // Upper and lower case letters, digits, space, apostrophe, left/right parenthesis, plus sign, comma, + // hyphen, full stop, solidus, colon, equal sign, question mark + case self::TYPE_TELETEX_STRING: + // The Teletex character set in CCITT's T61, space, and delete + // see http://en.wikipedia.org/wiki/Teletex#Character_sets + case self::TYPE_VIDEOTEX_STRING: + // The Videotex character set in CCITT's T.100 and T.101, space, and delete + case self::TYPE_VISIBLE_STRING: + // Printing character sets of international ASCII, and space + case self::TYPE_IA5_STRING: + // International Alphabet 5 (International ASCII) + case self::TYPE_GRAPHIC_STRING: + // All registered G sets, and space + case self::TYPE_GENERAL_STRING: + // All registered C and G sets, space and delete + case self::TYPE_UTF8_STRING: + // ???? + case self::TYPE_BMP_STRING: + if ($constructed) { + return false; + } + $current['content'] = substr($content, $content_pos); + break; + case self::TYPE_UTC_TIME: + case self::TYPE_GENERALIZED_TIME: + if ($constructed) { + return false; + } + $current['content'] = $this->_decodeTime(substr($content, $content_pos), $tag); + break; + default: + return false; + } + + $start+= $length; + + // ie. length is the length of the full TLV encoding - it's not just the length of the value + return $current + array('length' => $start - $current['start']); + } + + /** + * ASN.1 Map + * + * Provides an ASN.1 semantic mapping ($mapping) from a parsed BER-encoding to a human readable format. + * + * "Special" mappings may be applied on a per tag-name basis via $special. + * + * @param array $decoded + * @param array $mapping + * @param array $special + * @return array + * @access public + */ + function asn1map($decoded, $mapping, $special = array()) + { + if (!is_array($decoded)) { + return false; + } + + if (isset($mapping['explicit']) && is_array($decoded['content'])) { + $decoded = $decoded['content'][0]; + } + + switch (true) { + case $mapping['type'] == self::TYPE_ANY: + $intype = $decoded['type']; + if (isset($decoded['constant']) || !isset($this->ANYmap[$intype]) || (ord($this->encoded[$decoded['start']]) & 0x20)) { + return new Element(substr($this->encoded, $decoded['start'], $decoded['length'])); + } + $inmap = $this->ANYmap[$intype]; + if (is_string($inmap)) { + return array($inmap => $this->asn1map($decoded, array('type' => $intype) + $mapping, $special)); + } + break; + case $mapping['type'] == self::TYPE_CHOICE: + foreach ($mapping['children'] as $key => $option) { + switch (true) { + case isset($option['constant']) && $option['constant'] == $decoded['constant']: + case !isset($option['constant']) && $option['type'] == $decoded['type']: + $value = $this->asn1map($decoded, $option, $special); + break; + case !isset($option['constant']) && $option['type'] == self::TYPE_CHOICE: + $v = $this->asn1map($decoded, $option, $special); + if (isset($v)) { + $value = $v; + } + } + if (isset($value)) { + if (isset($special[$key])) { + $value = call_user_func($special[$key], $value); + } + return array($key => $value); + } + } + return null; + case isset($mapping['implicit']): + case isset($mapping['explicit']): + case $decoded['type'] == $mapping['type']: + break; + default: + // if $decoded['type'] and $mapping['type'] are both strings, but different types of strings, + // let it through + switch (true) { + case $decoded['type'] < 18: // self::TYPE_NUMERIC_STRING == 18 + case $decoded['type'] > 30: // self::TYPE_BMP_STRING == 30 + case $mapping['type'] < 18: + case $mapping['type'] > 30: + return null; + } + } + + if (isset($mapping['implicit'])) { + $decoded['type'] = $mapping['type']; + } + + switch ($decoded['type']) { + case self::TYPE_SEQUENCE: + $map = array(); + + // ignore the min and max + if (isset($mapping['min']) && isset($mapping['max'])) { + $child = $mapping['children']; + foreach ($decoded['content'] as $content) { + if (($map[] = $this->asn1map($content, $child, $special)) === null) { + return null; + } + } + + return $map; + } + + $n = count($decoded['content']); + $i = 0; + + foreach ($mapping['children'] as $key => $child) { + $maymatch = $i < $n; // Match only existing input. + if ($maymatch) { + $temp = $decoded['content'][$i]; + + if ($child['type'] != self::TYPE_CHOICE) { + // Get the mapping and input class & constant. + $childClass = $tempClass = self::CLASS_UNIVERSAL; + $constant = null; + if (isset($temp['constant'])) { + $tempClass = $temp['type']; + } + if (isset($child['class'])) { + $childClass = $child['class']; + $constant = $child['cast']; + } elseif (isset($child['constant'])) { + $childClass = self::CLASS_CONTEXT_SPECIFIC; + $constant = $child['constant']; + } + + if (isset($constant) && isset($temp['constant'])) { + // Can only match if constants and class match. + $maymatch = $constant == $temp['constant'] && $childClass == $tempClass; + } else { + // Can only match if no constant expected and type matches or is generic. + $maymatch = !isset($child['constant']) && array_search($child['type'], array($temp['type'], self::TYPE_ANY, self::TYPE_CHOICE)) !== false; + } + } + } + + if ($maymatch) { + // Attempt submapping. + $candidate = $this->asn1map($temp, $child, $special); + $maymatch = $candidate !== null; + } + + if ($maymatch) { + // Got the match: use it. + if (isset($special[$key])) { + $candidate = call_user_func($special[$key], $candidate); + } + $map[$key] = $candidate; + $i++; + } elseif (isset($child['default'])) { + $map[$key] = $child['default']; // Use default. + } elseif (!isset($child['optional'])) { + return null; // Syntax error. + } + } + + // Fail mapping if all input items have not been consumed. + return $i < $n ? null: $map; + + // the main diff between sets and sequences is the encapsulation of the foreach in another for loop + case self::TYPE_SET: + $map = array(); + + // ignore the min and max + if (isset($mapping['min']) && isset($mapping['max'])) { + $child = $mapping['children']; + foreach ($decoded['content'] as $content) { + if (($map[] = $this->asn1map($content, $child, $special)) === null) { + return null; + } + } + + return $map; + } + + for ($i = 0; $i < count($decoded['content']); $i++) { + $temp = $decoded['content'][$i]; + $tempClass = self::CLASS_UNIVERSAL; + if (isset($temp['constant'])) { + $tempClass = $temp['type']; + } + + foreach ($mapping['children'] as $key => $child) { + if (isset($map[$key])) { + continue; + } + $maymatch = true; + if ($child['type'] != self::TYPE_CHOICE) { + $childClass = self::CLASS_UNIVERSAL; + $constant = null; + if (isset($child['class'])) { + $childClass = $child['class']; + $constant = $child['cast']; + } elseif (isset($child['constant'])) { + $childClass = self::CLASS_CONTEXT_SPECIFIC; + $constant = $child['constant']; + } + + if (isset($constant) && isset($temp['constant'])) { + // Can only match if constants and class match. + $maymatch = $constant == $temp['constant'] && $childClass == $tempClass; + } else { + // Can only match if no constant expected and type matches or is generic. + $maymatch = !isset($child['constant']) && array_search($child['type'], array($temp['type'], self::TYPE_ANY, self::TYPE_CHOICE)) !== false; + } + } + + if ($maymatch) { + // Attempt submapping. + $candidate = $this->asn1map($temp, $child, $special); + $maymatch = $candidate !== null; + } + + if (!$maymatch) { + break; + } + + // Got the match: use it. + if (isset($special[$key])) { + $candidate = call_user_func($special[$key], $candidate); + } + $map[$key] = $candidate; + break; + } + } + + foreach ($mapping['children'] as $key => $child) { + if (!isset($map[$key])) { + if (isset($child['default'])) { + $map[$key] = $child['default']; + } elseif (!isset($child['optional'])) { + return null; + } + } + } + return $map; + case self::TYPE_OBJECT_IDENTIFIER: + return isset($this->oids[$decoded['content']]) ? $this->oids[$decoded['content']] : $decoded['content']; + case self::TYPE_UTC_TIME: + case self::TYPE_GENERALIZED_TIME: + // for explicitly tagged optional stuff + if (is_array($decoded['content'])) { + $decoded['content'] = $decoded['content'][0]['content']; + } + // for implicitly tagged optional stuff + // in theory, doing isset($mapping['implicit']) would work but malformed certs do exist + // in the wild that OpenSSL decodes without issue so we'll support them as well + if (!is_object($decoded['content'])) { + $decoded['content'] = $this->_decodeTime($decoded['content'], $decoded['type']); + } + return $decoded['content'] ? $decoded['content']->format($this->format) : false; + case self::TYPE_BIT_STRING: + if (isset($mapping['mapping'])) { + $offset = ord($decoded['content'][0]); + $size = (strlen($decoded['content']) - 1) * 8 - $offset; + /* + From X.680-0207.pdf#page=46 (21.7): + + "When a "NamedBitList" is used in defining a bitstring type ASN.1 encoding rules are free to add (or remove) + arbitrarily any trailing 0 bits to (or from) values that are being encoded or decoded. Application designers should + therefore ensure that different semantics are not associated with such values which differ only in the number of trailing + 0 bits." + */ + $bits = count($mapping['mapping']) == $size ? array() : array_fill(0, count($mapping['mapping']) - $size, false); + for ($i = strlen($decoded['content']) - 1; $i > 0; $i--) { + $current = ord($decoded['content'][$i]); + for ($j = $offset; $j < 8; $j++) { + $bits[] = (bool) ($current & (1 << $j)); + } + $offset = 0; + } + $values = array(); + $map = array_reverse($mapping['mapping']); + foreach ($map as $i => $value) { + if ($bits[$i]) { + $values[] = $value; + } + } + return $values; + } + case self::TYPE_OCTET_STRING: + return base64_encode($decoded['content']); + case self::TYPE_NULL: + return ''; + case self::TYPE_BOOLEAN: + return $decoded['content']; + case self::TYPE_NUMERIC_STRING: + case self::TYPE_PRINTABLE_STRING: + case self::TYPE_TELETEX_STRING: + case self::TYPE_VIDEOTEX_STRING: + case self::TYPE_IA5_STRING: + case self::TYPE_GRAPHIC_STRING: + case self::TYPE_VISIBLE_STRING: + case self::TYPE_GENERAL_STRING: + case self::TYPE_UNIVERSAL_STRING: + case self::TYPE_UTF8_STRING: + case self::TYPE_BMP_STRING: + return $decoded['content']; + case self::TYPE_INTEGER: + case self::TYPE_ENUMERATED: + $temp = $decoded['content']; + if (isset($mapping['implicit'])) { + $temp = new BigInteger($decoded['content'], -256); + } + if (isset($mapping['mapping'])) { + $temp = (int) $temp->toString(); + return isset($mapping['mapping'][$temp]) ? + $mapping['mapping'][$temp] : + false; + } + return $temp; + } + } + + /** + * ASN.1 Encode + * + * DER-encodes an ASN.1 semantic mapping ($mapping). Some libraries would probably call this function + * an ASN.1 compiler. + * + * "Special" mappings can be applied via $special. + * + * @param string $source + * @param string $mapping + * @param array $special + * @return string + * @access public + */ + function encodeDER($source, $mapping, $special = array()) + { + $this->location = array(); + return $this->_encode_der($source, $mapping, null, $special); + } + + /** + * ASN.1 Encode (Helper function) + * + * @param string $source + * @param string $mapping + * @param int $idx + * @param array $special + * @return string + * @access private + */ + function _encode_der($source, $mapping, $idx = null, $special = array()) + { + if ($source instanceof Element) { + return $source->element; + } + + // do not encode (implicitly optional) fields with value set to default + if (isset($mapping['default']) && $source === $mapping['default']) { + return ''; + } + + if (isset($idx)) { + if (isset($special[$idx])) { + $source = call_user_func($special[$idx], $source); + } + $this->location[] = $idx; + } + + $tag = $mapping['type']; + + switch ($tag) { + case self::TYPE_SET: // Children order is not important, thus process in sequence. + case self::TYPE_SEQUENCE: + $tag|= 0x20; // set the constructed bit + + // ignore the min and max + if (isset($mapping['min']) && isset($mapping['max'])) { + $value = array(); + $child = $mapping['children']; + + foreach ($source as $content) { + $temp = $this->_encode_der($content, $child, null, $special); + if ($temp === false) { + return false; + } + $value[]= $temp; + } + /* "The encodings of the component values of a set-of value shall appear in ascending order, the encodings being compared + as octet strings with the shorter components being padded at their trailing end with 0-octets. + NOTE - The padding octets are for comparison purposes only and do not appear in the encodings." + + -- sec 11.6 of http://www.itu.int/ITU-T/studygroups/com17/languages/X.690-0207.pdf */ + if ($mapping['type'] == self::TYPE_SET) { + sort($value); + } + $value = implode('', $value); + break; + } + + $value = ''; + foreach ($mapping['children'] as $key => $child) { + if (!array_key_exists($key, $source)) { + if (!isset($child['optional'])) { + return false; + } + continue; + } + + $temp = $this->_encode_der($source[$key], $child, $key, $special); + if ($temp === false) { + return false; + } + + // An empty child encoding means it has been optimized out. + // Else we should have at least one tag byte. + if ($temp === '') { + continue; + } + + // if isset($child['constant']) is true then isset($child['optional']) should be true as well + if (isset($child['constant'])) { + /* + From X.680-0207.pdf#page=58 (30.6): + + "The tagging construction specifies explicit tagging if any of the following holds: + ... + c) the "Tag Type" alternative is used and the value of "TagDefault" for the module is IMPLICIT TAGS or + AUTOMATIC TAGS, but the type defined by "Type" is an untagged choice type, an untagged open type, or + an untagged "DummyReference" (see ITU-T Rec. X.683 | ISO/IEC 8824-4, 8.3)." + */ + if (isset($child['explicit']) || $child['type'] == self::TYPE_CHOICE) { + $subtag = chr((self::CLASS_CONTEXT_SPECIFIC << 6) | 0x20 | $child['constant']); + $temp = $subtag . $this->_encodeLength(strlen($temp)) . $temp; + } else { + $subtag = chr((self::CLASS_CONTEXT_SPECIFIC << 6) | (ord($temp[0]) & 0x20) | $child['constant']); + $temp = $subtag . substr($temp, 1); + } + } + $value.= $temp; + } + break; + case self::TYPE_CHOICE: + $temp = false; + + foreach ($mapping['children'] as $key => $child) { + if (!isset($source[$key])) { + continue; + } + + $temp = $this->_encode_der($source[$key], $child, $key, $special); + if ($temp === false) { + return false; + } + + // An empty child encoding means it has been optimized out. + // Else we should have at least one tag byte. + if ($temp === '') { + continue; + } + + $tag = ord($temp[0]); + + // if isset($child['constant']) is true then isset($child['optional']) should be true as well + if (isset($child['constant'])) { + if (isset($child['explicit']) || $child['type'] == self::TYPE_CHOICE) { + $subtag = chr((self::CLASS_CONTEXT_SPECIFIC << 6) | 0x20 | $child['constant']); + $temp = $subtag . $this->_encodeLength(strlen($temp)) . $temp; + } else { + $subtag = chr((self::CLASS_CONTEXT_SPECIFIC << 6) | (ord($temp[0]) & 0x20) | $child['constant']); + $temp = $subtag . substr($temp, 1); + } + } + } + + if (isset($idx)) { + array_pop($this->location); + } + + if ($temp && isset($mapping['cast'])) { + $temp[0] = chr(($mapping['class'] << 6) | ($tag & 0x20) | $mapping['cast']); + } + + return $temp; + case self::TYPE_INTEGER: + case self::TYPE_ENUMERATED: + if (!isset($mapping['mapping'])) { + if (is_numeric($source)) { + $source = new BigInteger($source); + } + $value = $source->toBytes(true); + } else { + $value = array_search($source, $mapping['mapping']); + if ($value === false) { + return false; + } + $value = new BigInteger($value); + $value = $value->toBytes(true); + } + if (!strlen($value)) { + $value = chr(0); + } + break; + case self::TYPE_UTC_TIME: + case self::TYPE_GENERALIZED_TIME: + $format = $mapping['type'] == self::TYPE_UTC_TIME ? 'y' : 'Y'; + $format.= 'mdHis'; + // if $source does _not_ include timezone information within it then assume that the timezone is GMT + $date = new DateTime($source, new DateTimeZone('GMT')); + // if $source _does_ include timezone information within it then convert the time to GMT + $date->setTimezone(new DateTimeZone('GMT')); + $value = $date->format($format) . 'Z'; + break; + case self::TYPE_BIT_STRING: + if (isset($mapping['mapping'])) { + $bits = array_fill(0, count($mapping['mapping']), 0); + $size = 0; + for ($i = 0; $i < count($mapping['mapping']); $i++) { + if (in_array($mapping['mapping'][$i], $source)) { + $bits[$i] = 1; + $size = $i; + } + } + + if (isset($mapping['min']) && $mapping['min'] >= 1 && $size < $mapping['min']) { + $size = $mapping['min'] - 1; + } + + $offset = 8 - (($size + 1) & 7); + $offset = $offset !== 8 ? $offset : 0; + + $value = chr($offset); + + for ($i = $size + 1; $i < count($mapping['mapping']); $i++) { + unset($bits[$i]); + } + + $bits = implode('', array_pad($bits, $size + $offset + 1, 0)); + $bytes = explode(' ', rtrim(chunk_split($bits, 8, ' '))); + foreach ($bytes as $byte) { + $value.= chr(bindec($byte)); + } + + break; + } + case self::TYPE_OCTET_STRING: + /* The initial octet shall encode, as an unsigned binary integer with bit 1 as the least significant bit, + the number of unused bits in the final subsequent octet. The number shall be in the range zero to seven. + + -- http://www.itu.int/ITU-T/studygroups/com17/languages/X.690-0207.pdf#page=16 */ + $value = base64_decode($source); + break; + case self::TYPE_OBJECT_IDENTIFIER: + $value = $this->_encodeOID($source); + break; + case self::TYPE_ANY: + $loc = $this->location; + if (isset($idx)) { + array_pop($this->location); + } + + switch (true) { + case !isset($source): + return $this->_encode_der(null, array('type' => self::TYPE_NULL) + $mapping, null, $special); + case is_int($source): + case $source instanceof BigInteger: + return $this->_encode_der($source, array('type' => self::TYPE_INTEGER) + $mapping, null, $special); + case is_float($source): + return $this->_encode_der($source, array('type' => self::TYPE_REAL) + $mapping, null, $special); + case is_bool($source): + return $this->_encode_der($source, array('type' => self::TYPE_BOOLEAN) + $mapping, null, $special); + case is_array($source) && count($source) == 1: + $typename = implode('', array_keys($source)); + $outtype = array_search($typename, $this->ANYmap, true); + if ($outtype !== false) { + return $this->_encode_der($source[$typename], array('type' => $outtype) + $mapping, null, $special); + } + } + + $filters = $this->filters; + foreach ($loc as $part) { + if (!isset($filters[$part])) { + $filters = false; + break; + } + $filters = $filters[$part]; + } + if ($filters === false) { + user_error('No filters defined for ' . implode('/', $loc)); + return false; + } + return $this->_encode_der($source, $filters + $mapping, null, $special); + case self::TYPE_NULL: + $value = ''; + break; + case self::TYPE_NUMERIC_STRING: + case self::TYPE_TELETEX_STRING: + case self::TYPE_PRINTABLE_STRING: + case self::TYPE_UNIVERSAL_STRING: + case self::TYPE_UTF8_STRING: + case self::TYPE_BMP_STRING: + case self::TYPE_IA5_STRING: + case self::TYPE_VISIBLE_STRING: + case self::TYPE_VIDEOTEX_STRING: + case self::TYPE_GRAPHIC_STRING: + case self::TYPE_GENERAL_STRING: + $value = $source; + break; + case self::TYPE_BOOLEAN: + $value = $source ? "\xFF" : "\x00"; + break; + default: + user_error('Mapping provides no type definition for ' . implode('/', $this->location)); + return false; + } + + if (isset($idx)) { + array_pop($this->location); + } + + if (isset($mapping['cast'])) { + if (isset($mapping['explicit']) || $mapping['type'] == self::TYPE_CHOICE) { + $value = chr($tag) . $this->_encodeLength(strlen($value)) . $value; + $tag = ($mapping['class'] << 6) | 0x20 | $mapping['cast']; + } else { + $tag = ($mapping['class'] << 6) | (ord($temp[0]) & 0x20) | $mapping['cast']; + } + } + + return chr($tag) . $this->_encodeLength(strlen($value)) . $value; + } + + /** + * DER-encode the length + * + * DER supports lengths up to (2**8)**127, however, we'll only support lengths up to (2**8)**4. See + * {@link http://itu.int/ITU-T/studygroups/com17/languages/X.690-0207.pdf#p=13 X.690 paragraph 8.1.3} for more information. + * + * @access private + * @param int $length + * @return string + */ + function _encodeLength($length) + { + if ($length <= 0x7F) { + return chr($length); + } + + $temp = ltrim(pack('N', $length), chr(0)); + return pack('Ca*', 0x80 | strlen($temp), $temp); + } + + /** + * BER-decode the OID + * + * Called by _decode_ber() + * + * @access private + * @param string $content + * @return string + */ + function _decodeOID($content) + { + static $eighty; + if (!$eighty) { + $eighty = new BigInteger(80); + } + + $oid = array(); + $pos = 0; + $len = strlen($content); + // see https://github.com/openjdk/jdk/blob/2deb318c9f047ec5a4b160d66a4b52f93688ec42/src/java.base/share/classes/sun/security/util/ObjectIdentifier.java#L55 + if ($len > 4096) { + //user_error('Object Identifier size is limited to 4096 bytes'); + return false; + } + + if (ord($content[$len - 1]) & 0x80) { + return false; + } + + $n = new BigInteger(); + while ($pos < $len) { + $temp = ord($content[$pos++]); + $n = $n->bitwise_leftShift(7); + $n = $n->bitwise_or(new BigInteger($temp & 0x7F)); + if (~$temp & 0x80) { + $oid[] = $n; + $n = new BigInteger(); + } + } + $part1 = array_shift($oid); + $first = floor(ord($content[0]) / 40); + /* + "This packing of the first two object identifier components recognizes that only three values are allocated from the root + node, and at most 39 subsequent values from nodes reached by X = 0 and X = 1." + + -- https://www.itu.int/ITU-T/studygroups/com17/languages/X.690-0207.pdf#page=22 + */ + if ($first <= 2) { // ie. 0 <= ord($content[0]) < 120 (0x78) + array_unshift($oid, ord($content[0]) % 40); + array_unshift($oid, $first); + } else { + array_unshift($oid, $part1->subtract($eighty)); + array_unshift($oid, 2); + } + + return implode('.', $oid); + } + + /** + * DER-encode the OID + * + * Called by _encode_der() + * + * @access private + * @param string $source + * @return string + */ + function _encodeOID($source) + { + static $mask, $zero, $forty; + if (!$mask) { + $mask = new BigInteger(0x7F); + $zero = new BigInteger(); + $forty = new BigInteger(40); + } + + $oid = preg_match('#(?:\d+\.)+#', $source) ? $source : array_search($source, $this->oids); + if ($oid === false) { + user_error('Invalid OID'); + return false; + } + $parts = explode('.', $oid); + $part1 = array_shift($parts); + $part2 = array_shift($parts); + + $first = new BigInteger($part1); + $first = $first->multiply($forty); + $first = $first->add(new BigInteger($part2)); + + array_unshift($parts, $first->toString()); + + $value = ''; + foreach ($parts as $part) { + if (!$part) { + $temp = "\0"; + } else { + $temp = ''; + $part = new BigInteger($part); + while (!$part->equals($zero)) { + $submask = $part->bitwise_and($mask); + $submask->setPrecision(8); + $temp = (chr(0x80) | $submask->toBytes()) . $temp; + $part = $part->bitwise_rightShift(7); + } + $temp[strlen($temp) - 1] = $temp[strlen($temp) - 1] & chr(0x7F); + } + $value.= $temp; + } + + return $value; + } + + /** + * BER-decode the time + * + * Called by _decode_ber() and in the case of implicit tags asn1map(). + * + * @access private + * @param string $content + * @param int $tag + * @return string + */ + function _decodeTime($content, $tag) + { + /* UTCTime: + http://tools.ietf.org/html/rfc5280#section-4.1.2.5.1 + http://www.obj-sys.com/asn1tutorial/node15.html + + GeneralizedTime: + http://tools.ietf.org/html/rfc5280#section-4.1.2.5.2 + http://www.obj-sys.com/asn1tutorial/node14.html */ + + $format = 'YmdHis'; + + if ($tag == self::TYPE_UTC_TIME) { + // https://www.itu.int/ITU-T/studygroups/com17/languages/X.690-0207.pdf#page=28 says "the seconds + // element shall always be present" but none-the-less I've seen X509 certs where it isn't and if the + // browsers parse it phpseclib ought to too + if (preg_match('#^(\d{10})(Z|[+-]\d{4})$#', $content, $matches)) { + $content = $matches[1] . '00' . $matches[2]; + } + $prefix = substr($content, 0, 2) >= 50 ? '19' : '20'; + $content = $prefix . $content; + } elseif (strpos($content, '.') !== false) { + $format.= '.u'; + } + + if ($content[strlen($content) - 1] == 'Z') { + $content = substr($content, 0, -1) . '+0000'; + } + + if (strpos($content, '-') !== false || strpos($content, '+') !== false) { + $format.= 'O'; + } + + // error supression isn't necessary as of PHP 7.0: + // http://php.net/manual/en/migration70.other-changes.php + return @DateTime::createFromFormat($format, $content); + } + + /** + * Set the time format + * + * Sets the time / date format for asn1map(). + * + * @access public + * @param string $format + */ + function setTimeFormat($format) + { + $this->format = $format; + } + + /** + * Load OIDs + * + * Load the relevant OIDs for a particular ASN.1 semantic mapping. + * + * @access public + * @param array $oids + */ + function loadOIDs($oids) + { + $this->oids = $oids; + } + + /** + * Load filters + * + * See \phpseclib\File\X509, etc, for an example. + * + * @access public + * @param array $filters + */ + function loadFilters($filters) + { + $this->filters = $filters; + } + + /** + * String Shift + * + * Inspired by array_shift + * + * @param string $string + * @param int $index + * @return string + * @access private + */ + function _string_shift(&$string, $index = 1) + { + $substr = substr($string, 0, $index); + $string = substr($string, $index); + return $substr; + } + + /** + * String type conversion + * + * This is a lazy conversion, dealing only with character size. + * No real conversion table is used. + * + * @param string $in + * @param int $from + * @param int $to + * @return string + * @access public + */ + function convert($in, $from = self::TYPE_UTF8_STRING, $to = self::TYPE_UTF8_STRING) + { + if (!isset($this->stringTypeSize[$from]) || !isset($this->stringTypeSize[$to])) { + return false; + } + $insize = $this->stringTypeSize[$from]; + $outsize = $this->stringTypeSize[$to]; + $inlength = strlen($in); + $out = ''; + + for ($i = 0; $i < $inlength;) { + if ($inlength - $i < $insize) { + return false; + } + + // Get an input character as a 32-bit value. + $c = ord($in[$i++]); + switch (true) { + case $insize == 4: + $c = ($c << 8) | ord($in[$i++]); + $c = ($c << 8) | ord($in[$i++]); + case $insize == 2: + $c = ($c << 8) | ord($in[$i++]); + case $insize == 1: + break; + case ($c & 0x80) == 0x00: + break; + case ($c & 0x40) == 0x00: + return false; + default: + $bit = 6; + do { + if ($bit > 25 || $i >= $inlength || (ord($in[$i]) & 0xC0) != 0x80) { + return false; + } + $c = ($c << 6) | (ord($in[$i++]) & 0x3F); + $bit += 5; + $mask = 1 << $bit; + } while ($c & $bit); + $c &= $mask - 1; + break; + } + + // Convert and append the character to output string. + $v = ''; + switch (true) { + case $outsize == 4: + $v .= chr($c & 0xFF); + $c >>= 8; + $v .= chr($c & 0xFF); + $c >>= 8; + case $outsize == 2: + $v .= chr($c & 0xFF); + $c >>= 8; + case $outsize == 1: + $v .= chr($c & 0xFF); + $c >>= 8; + if ($c) { + return false; + } + break; + case ($c & (PHP_INT_SIZE == 8 ? 0x80000000 : (1 << 31))) != 0: + return false; + case $c >= 0x04000000: + $v .= chr(0x80 | ($c & 0x3F)); + $c = ($c >> 6) | 0x04000000; + case $c >= 0x00200000: + $v .= chr(0x80 | ($c & 0x3F)); + $c = ($c >> 6) | 0x00200000; + case $c >= 0x00010000: + $v .= chr(0x80 | ($c & 0x3F)); + $c = ($c >> 6) | 0x00010000; + case $c >= 0x00000800: + $v .= chr(0x80 | ($c & 0x3F)); + $c = ($c >> 6) | 0x00000800; + case $c >= 0x00000080: + $v .= chr(0x80 | ($c & 0x3F)); + $c = ($c >> 6) | 0x000000C0; + default: + $v .= chr($c); + break; + } + $out .= strrev($v); + } + return $out; + } +} diff --git a/3rdparty/phpseclib/phpseclib/phpseclib/File/ASN1/Element.php b/3rdparty/phpseclib/phpseclib/phpseclib/File/ASN1/Element.php new file mode 100644 index 00000000..68246e2b --- /dev/null +++ b/3rdparty/phpseclib/phpseclib/phpseclib/File/ASN1/Element.php @@ -0,0 +1,47 @@ + + * @copyright 2012 Jim Wigginton + * @license http://www.opensource.org/licenses/mit-license.html MIT License + * @link http://phpseclib.sourceforge.net + */ + +namespace phpseclib\File\ASN1; + +/** + * ASN.1 Element + * + * Bypass normal encoding rules in phpseclib\File\ASN1::encodeDER() + * + * @package ASN1 + * @author Jim Wigginton + * @access public + */ +class Element +{ + /** + * Raw element value + * + * @var string + * @access private + */ + var $element; + + /** + * Constructor + * + * @param string $encoded + * @return \phpseclib\File\ASN1\Element + * @access public + */ + function __construct($encoded) + { + $this->element = $encoded; + } +} diff --git a/3rdparty/phpseclib/phpseclib/phpseclib/File/X509.php b/3rdparty/phpseclib/phpseclib/phpseclib/File/X509.php new file mode 100644 index 00000000..7b8d96e2 --- /dev/null +++ b/3rdparty/phpseclib/phpseclib/phpseclib/File/X509.php @@ -0,0 +1,5121 @@ + + * @copyright 2012 Jim Wigginton + * @license http://www.opensource.org/licenses/mit-license.html MIT License + * @link http://phpseclib.sourceforge.net + */ + +namespace phpseclib\File; + +use phpseclib\Crypt\Hash; +use phpseclib\Crypt\Random; +use phpseclib\Crypt\RSA; +use phpseclib\File\ASN1\Element; +use phpseclib\Math\BigInteger; +use DateTime; +use DateTimeZone; + +/** + * Pure-PHP X.509 Parser + * + * @package X509 + * @author Jim Wigginton + * @access public + */ +class X509 +{ + /** + * Flag to only accept signatures signed by certificate authorities + * + * Not really used anymore but retained all the same to suppress E_NOTICEs from old installs + * + * @access public + */ + const VALIDATE_SIGNATURE_BY_CA = 1; + + /**#@+ + * @access public + * @see \phpseclib\File\X509::getDN() + */ + /** + * Return internal array representation + */ + const DN_ARRAY = 0; + /** + * Return string + */ + const DN_STRING = 1; + /** + * Return ASN.1 name string + */ + const DN_ASN1 = 2; + /** + * Return OpenSSL compatible array + */ + const DN_OPENSSL = 3; + /** + * Return canonical ASN.1 RDNs string + */ + const DN_CANON = 4; + /** + * Return name hash for file indexing + */ + const DN_HASH = 5; + /**#@-*/ + + /**#@+ + * @access public + * @see \phpseclib\File\X509::saveX509() + * @see \phpseclib\File\X509::saveCSR() + * @see \phpseclib\File\X509::saveCRL() + */ + /** + * Save as PEM + * + * ie. a base64-encoded PEM with a header and a footer + */ + const FORMAT_PEM = 0; + /** + * Save as DER + */ + const FORMAT_DER = 1; + /** + * Save as a SPKAC + * + * Only works on CSRs. Not currently supported. + */ + const FORMAT_SPKAC = 2; + /** + * Auto-detect the format + * + * Used only by the load*() functions + */ + const FORMAT_AUTO_DETECT = 3; + /**#@-*/ + + /** + * Attribute value disposition. + * If disposition is >= 0, this is the index of the target value. + */ + const ATTR_ALL = -1; // All attribute values (array). + const ATTR_APPEND = -2; // Add a value. + const ATTR_REPLACE = -3; // Clear first, then add a value. + + /** + * ASN.1 syntax for X.509 certificates + * + * @var array + * @access private + */ + var $Certificate; + + /**#@+ + * ASN.1 syntax for various extensions + * + * @access private + */ + var $DirectoryString; + var $PKCS9String; + var $AttributeValue; + var $Extensions; + var $KeyUsage; + var $ExtKeyUsageSyntax; + var $BasicConstraints; + var $KeyIdentifier; + var $CRLDistributionPoints; + var $AuthorityKeyIdentifier; + var $CertificatePolicies; + var $AuthorityInfoAccessSyntax; + var $SubjectInfoAccessSyntax; + var $SubjectAltName; + var $SubjectDirectoryAttributes; + var $PrivateKeyUsagePeriod; + var $IssuerAltName; + var $PolicyMappings; + var $NameConstraints; + + var $CPSuri; + var $UserNotice; + + var $netscape_cert_type; + var $netscape_comment; + var $netscape_ca_policy_url; + + var $Name; + var $RelativeDistinguishedName; + var $CRLNumber; + var $CRLReason; + var $IssuingDistributionPoint; + var $InvalidityDate; + var $CertificateIssuer; + var $HoldInstructionCode; + var $SignedPublicKeyAndChallenge; + /**#@-*/ + + /**#@+ + * ASN.1 syntax for various DN attributes + * + * @access private + */ + var $PostalAddress; + /**#@-*/ + + /** + * ASN.1 syntax for Certificate Signing Requests (RFC2986) + * + * @var array + * @access private + */ + var $CertificationRequest; + + /** + * ASN.1 syntax for Certificate Revocation Lists (RFC5280) + * + * @var array + * @access private + */ + var $CertificateList; + + /** + * Distinguished Name + * + * @var array + * @access private + */ + var $dn; + + /** + * Public key + * + * @var string + * @access private + */ + var $publicKey; + + /** + * Private key + * + * @var string + * @access private + */ + var $privateKey; + + /** + * Object identifiers for X.509 certificates + * + * @var array + * @access private + * @link http://en.wikipedia.org/wiki/Object_identifier + */ + var $oids; + + /** + * The certificate authorities + * + * @var array + * @access private + */ + var $CAs; + + /** + * The currently loaded certificate + * + * @var array + * @access private + */ + var $currentCert; + + /** + * The signature subject + * + * There's no guarantee \phpseclib\File\X509 is going to re-encode an X.509 cert in the same way it was originally + * encoded so we take save the portion of the original cert that the signature would have made for. + * + * @var string + * @access private + */ + var $signatureSubject; + + /** + * Certificate Start Date + * + * @var string + * @access private + */ + var $startDate; + + /** + * Certificate End Date + * + * @var string + * @access private + */ + var $endDate; + + /** + * Serial Number + * + * @var string + * @access private + */ + var $serialNumber; + + /** + * Key Identifier + * + * See {@link http://tools.ietf.org/html/rfc5280#section-4.2.1.1 RFC5280#section-4.2.1.1} and + * {@link http://tools.ietf.org/html/rfc5280#section-4.2.1.2 RFC5280#section-4.2.1.2}. + * + * @var string + * @access private + */ + var $currentKeyIdentifier; + + /** + * CA Flag + * + * @var bool + * @access private + */ + var $caFlag = false; + + /** + * SPKAC Challenge + * + * @var string + * @access private + */ + var $challenge; + + /** + * Recursion Limit + * + * @var int + * @access private + */ + static $recur_limit = 5; + + /** + * URL fetch flag + * + * @var bool + * @access private + */ + static $disable_url_fetch = false; + + /** + * Default Constructor. + * + * @return \phpseclib\File\X509 + * @access public + */ + function __construct() + { + // Explicitly Tagged Module, 1988 Syntax + // http://tools.ietf.org/html/rfc5280#appendix-A.1 + + $this->DirectoryString = array( + 'type' => ASN1::TYPE_CHOICE, + 'children' => array( + 'teletexString' => array('type' => ASN1::TYPE_TELETEX_STRING), + 'printableString' => array('type' => ASN1::TYPE_PRINTABLE_STRING), + 'universalString' => array('type' => ASN1::TYPE_UNIVERSAL_STRING), + 'utf8String' => array('type' => ASN1::TYPE_UTF8_STRING), + 'bmpString' => array('type' => ASN1::TYPE_BMP_STRING) + ) + ); + + $this->PKCS9String = array( + 'type' => ASN1::TYPE_CHOICE, + 'children' => array( + 'ia5String' => array('type' => ASN1::TYPE_IA5_STRING), + 'directoryString' => $this->DirectoryString + ) + ); + + $this->AttributeValue = array('type' => ASN1::TYPE_ANY); + + $AttributeType = array('type' => ASN1::TYPE_OBJECT_IDENTIFIER); + + $AttributeTypeAndValue = array( + 'type' => ASN1::TYPE_SEQUENCE, + 'children' => array( + 'type' => $AttributeType, + 'value'=> $this->AttributeValue + ) + ); + + /* + In practice, RDNs containing multiple name-value pairs (called "multivalued RDNs") are rare, + but they can be useful at times when either there is no unique attribute in the entry or you + want to ensure that the entry's DN contains some useful identifying information. + + - https://www.opends.org/wiki/page/DefinitionRelativeDistinguishedName + */ + $this->RelativeDistinguishedName = array( + 'type' => ASN1::TYPE_SET, + 'min' => 1, + 'max' => -1, + 'children' => $AttributeTypeAndValue + ); + + // http://tools.ietf.org/html/rfc5280#section-4.1.2.4 + $RDNSequence = array( + 'type' => ASN1::TYPE_SEQUENCE, + // RDNSequence does not define a min or a max, which means it doesn't have one + 'min' => 0, + 'max' => -1, + 'children' => $this->RelativeDistinguishedName + ); + + $this->Name = array( + 'type' => ASN1::TYPE_CHOICE, + 'children' => array( + 'rdnSequence' => $RDNSequence + ) + ); + + // http://tools.ietf.org/html/rfc5280#section-4.1.1.2 + $AlgorithmIdentifier = array( + 'type' => ASN1::TYPE_SEQUENCE, + 'children' => array( + 'algorithm' => array('type' => ASN1::TYPE_OBJECT_IDENTIFIER), + 'parameters' => array( + 'type' => ASN1::TYPE_ANY, + 'optional' => true + ) + ) + ); + + /* + A certificate using system MUST reject the certificate if it encounters + a critical extension it does not recognize; however, a non-critical + extension may be ignored if it is not recognized. + + http://tools.ietf.org/html/rfc5280#section-4.2 + */ + $Extension = array( + 'type' => ASN1::TYPE_SEQUENCE, + 'children' => array( + 'extnId' => array('type' => ASN1::TYPE_OBJECT_IDENTIFIER), + 'critical' => array( + 'type' => ASN1::TYPE_BOOLEAN, + 'optional' => true, + 'default' => false + ), + 'extnValue' => array('type' => ASN1::TYPE_OCTET_STRING) + ) + ); + + $this->Extensions = array( + 'type' => ASN1::TYPE_SEQUENCE, + 'min' => 1, + // technically, it's MAX, but we'll assume anything < 0 is MAX + 'max' => -1, + // if 'children' isn't an array then 'min' and 'max' must be defined + 'children' => $Extension + ); + + $SubjectPublicKeyInfo = array( + 'type' => ASN1::TYPE_SEQUENCE, + 'children' => array( + 'algorithm' => $AlgorithmIdentifier, + 'subjectPublicKey' => array('type' => ASN1::TYPE_BIT_STRING) + ) + ); + + $UniqueIdentifier = array('type' => ASN1::TYPE_BIT_STRING); + + $Time = array( + 'type' => ASN1::TYPE_CHOICE, + 'children' => array( + 'utcTime' => array('type' => ASN1::TYPE_UTC_TIME), + 'generalTime' => array('type' => ASN1::TYPE_GENERALIZED_TIME) + ) + ); + + // http://tools.ietf.org/html/rfc5280#section-4.1.2.5 + $Validity = array( + 'type' => ASN1::TYPE_SEQUENCE, + 'children' => array( + 'notBefore' => $Time, + 'notAfter' => $Time + ) + ); + + $CertificateSerialNumber = array('type' => ASN1::TYPE_INTEGER); + + $Version = array( + 'type' => ASN1::TYPE_INTEGER, + 'mapping' => array('v1', 'v2', 'v3') + ); + + // assert($TBSCertificate['children']['signature'] == $Certificate['children']['signatureAlgorithm']) + $TBSCertificate = array( + 'type' => ASN1::TYPE_SEQUENCE, + 'children' => array( + // technically, default implies optional, but we'll define it as being optional, none-the-less, just to + // reenforce that fact + 'version' => array( + 'constant' => 0, + 'optional' => true, + 'explicit' => true, + 'default' => 'v1' + ) + $Version, + 'serialNumber' => $CertificateSerialNumber, + 'signature' => $AlgorithmIdentifier, + 'issuer' => $this->Name, + 'validity' => $Validity, + 'subject' => $this->Name, + 'subjectPublicKeyInfo' => $SubjectPublicKeyInfo, + // implicit means that the T in the TLV structure is to be rewritten, regardless of the type + 'issuerUniqueID' => array( + 'constant' => 1, + 'optional' => true, + 'implicit' => true + ) + $UniqueIdentifier, + 'subjectUniqueID' => array( + 'constant' => 2, + 'optional' => true, + 'implicit' => true + ) + $UniqueIdentifier, + // doesn't use the EXPLICIT keyword but if + // it's not IMPLICIT, it's EXPLICIT + 'extensions' => array( + 'constant' => 3, + 'optional' => true, + 'explicit' => true + ) + $this->Extensions + ) + ); + + $this->Certificate = array( + 'type' => ASN1::TYPE_SEQUENCE, + 'children' => array( + 'tbsCertificate' => $TBSCertificate, + 'signatureAlgorithm' => $AlgorithmIdentifier, + 'signature' => array('type' => ASN1::TYPE_BIT_STRING) + ) + ); + + $this->KeyUsage = array( + 'type' => ASN1::TYPE_BIT_STRING, + 'mapping' => array( + 'digitalSignature', + 'nonRepudiation', + 'keyEncipherment', + 'dataEncipherment', + 'keyAgreement', + 'keyCertSign', + 'cRLSign', + 'encipherOnly', + 'decipherOnly' + ) + ); + + $this->BasicConstraints = array( + 'type' => ASN1::TYPE_SEQUENCE, + 'children' => array( + 'cA' => array( + 'type' => ASN1::TYPE_BOOLEAN, + 'optional' => true, + 'default' => false + ), + 'pathLenConstraint' => array( + 'type' => ASN1::TYPE_INTEGER, + 'optional' => true + ) + ) + ); + + $this->KeyIdentifier = array('type' => ASN1::TYPE_OCTET_STRING); + + $OrganizationalUnitNames = array( + 'type' => ASN1::TYPE_SEQUENCE, + 'min' => 1, + 'max' => 4, // ub-organizational-units + 'children' => array('type' => ASN1::TYPE_PRINTABLE_STRING) + ); + + $PersonalName = array( + 'type' => ASN1::TYPE_SET, + 'children' => array( + 'surname' => array( + 'type' => ASN1::TYPE_PRINTABLE_STRING, + 'constant' => 0, + 'optional' => true, + 'implicit' => true + ), + 'given-name' => array( + 'type' => ASN1::TYPE_PRINTABLE_STRING, + 'constant' => 1, + 'optional' => true, + 'implicit' => true + ), + 'initials' => array( + 'type' => ASN1::TYPE_PRINTABLE_STRING, + 'constant' => 2, + 'optional' => true, + 'implicit' => true + ), + 'generation-qualifier' => array( + 'type' => ASN1::TYPE_PRINTABLE_STRING, + 'constant' => 3, + 'optional' => true, + 'implicit' => true + ) + ) + ); + + $NumericUserIdentifier = array('type' => ASN1::TYPE_NUMERIC_STRING); + + $OrganizationName = array('type' => ASN1::TYPE_PRINTABLE_STRING); + + $PrivateDomainName = array( + 'type' => ASN1::TYPE_CHOICE, + 'children' => array( + 'numeric' => array('type' => ASN1::TYPE_NUMERIC_STRING), + 'printable' => array('type' => ASN1::TYPE_PRINTABLE_STRING) + ) + ); + + $TerminalIdentifier = array('type' => ASN1::TYPE_PRINTABLE_STRING); + + $NetworkAddress = array('type' => ASN1::TYPE_NUMERIC_STRING); + + $AdministrationDomainName = array( + 'type' => ASN1::TYPE_CHOICE, + // if class isn't present it's assumed to be \phpseclib\File\ASN1::CLASS_UNIVERSAL or + // (if constant is present) \phpseclib\File\ASN1::CLASS_CONTEXT_SPECIFIC + 'class' => ASN1::CLASS_APPLICATION, + 'cast' => 2, + 'children' => array( + 'numeric' => array('type' => ASN1::TYPE_NUMERIC_STRING), + 'printable' => array('type' => ASN1::TYPE_PRINTABLE_STRING) + ) + ); + + $CountryName = array( + 'type' => ASN1::TYPE_CHOICE, + // if class isn't present it's assumed to be \phpseclib\File\ASN1::CLASS_UNIVERSAL or + // (if constant is present) \phpseclib\File\ASN1::CLASS_CONTEXT_SPECIFIC + 'class' => ASN1::CLASS_APPLICATION, + 'cast' => 1, + 'children' => array( + 'x121-dcc-code' => array('type' => ASN1::TYPE_NUMERIC_STRING), + 'iso-3166-alpha2-code' => array('type' => ASN1::TYPE_PRINTABLE_STRING) + ) + ); + + $AnotherName = array( + 'type' => ASN1::TYPE_SEQUENCE, + 'children' => array( + 'type-id' => array('type' => ASN1::TYPE_OBJECT_IDENTIFIER), + 'value' => array( + 'type' => ASN1::TYPE_ANY, + 'constant' => 0, + 'optional' => true, + 'explicit' => true + ) + ) + ); + + $ExtensionAttribute = array( + 'type' => ASN1::TYPE_SEQUENCE, + 'children' => array( + 'extension-attribute-type' => array( + 'type' => ASN1::TYPE_PRINTABLE_STRING, + 'constant' => 0, + 'optional' => true, + 'implicit' => true + ), + 'extension-attribute-value' => array( + 'type' => ASN1::TYPE_ANY, + 'constant' => 1, + 'optional' => true, + 'explicit' => true + ) + ) + ); + + $ExtensionAttributes = array( + 'type' => ASN1::TYPE_SET, + 'min' => 1, + 'max' => 256, // ub-extension-attributes + 'children' => $ExtensionAttribute + ); + + $BuiltInDomainDefinedAttribute = array( + 'type' => ASN1::TYPE_SEQUENCE, + 'children' => array( + 'type' => array('type' => ASN1::TYPE_PRINTABLE_STRING), + 'value' => array('type' => ASN1::TYPE_PRINTABLE_STRING) + ) + ); + + $BuiltInDomainDefinedAttributes = array( + 'type' => ASN1::TYPE_SEQUENCE, + 'min' => 1, + 'max' => 4, // ub-domain-defined-attributes + 'children' => $BuiltInDomainDefinedAttribute + ); + + $BuiltInStandardAttributes = array( + 'type' => ASN1::TYPE_SEQUENCE, + 'children' => array( + 'country-name' => array('optional' => true) + $CountryName, + 'administration-domain-name' => array('optional' => true) + $AdministrationDomainName, + 'network-address' => array( + 'constant' => 0, + 'optional' => true, + 'implicit' => true + ) + $NetworkAddress, + 'terminal-identifier' => array( + 'constant' => 1, + 'optional' => true, + 'implicit' => true + ) + $TerminalIdentifier, + 'private-domain-name' => array( + 'constant' => 2, + 'optional' => true, + 'explicit' => true + ) + $PrivateDomainName, + 'organization-name' => array( + 'constant' => 3, + 'optional' => true, + 'implicit' => true + ) + $OrganizationName, + 'numeric-user-identifier' => array( + 'constant' => 4, + 'optional' => true, + 'implicit' => true + ) + $NumericUserIdentifier, + 'personal-name' => array( + 'constant' => 5, + 'optional' => true, + 'implicit' => true + ) + $PersonalName, + 'organizational-unit-names' => array( + 'constant' => 6, + 'optional' => true, + 'implicit' => true + ) + $OrganizationalUnitNames + ) + ); + + $ORAddress = array( + 'type' => ASN1::TYPE_SEQUENCE, + 'children' => array( + 'built-in-standard-attributes' => $BuiltInStandardAttributes, + 'built-in-domain-defined-attributes' => array('optional' => true) + $BuiltInDomainDefinedAttributes, + 'extension-attributes' => array('optional' => true) + $ExtensionAttributes + ) + ); + + $EDIPartyName = array( + 'type' => ASN1::TYPE_SEQUENCE, + 'children' => array( + 'nameAssigner' => array( + 'constant' => 0, + 'optional' => true, + 'implicit' => true + ) + $this->DirectoryString, + // partyName is technically required but \phpseclib\File\ASN1 doesn't currently support non-optional constants and + // setting it to optional gets the job done in any event. + 'partyName' => array( + 'constant' => 1, + 'optional' => true, + 'implicit' => true + ) + $this->DirectoryString + ) + ); + + $GeneralName = array( + 'type' => ASN1::TYPE_CHOICE, + 'children' => array( + 'otherName' => array( + 'constant' => 0, + 'optional' => true, + 'implicit' => true + ) + $AnotherName, + 'rfc822Name' => array( + 'type' => ASN1::TYPE_IA5_STRING, + 'constant' => 1, + 'optional' => true, + 'implicit' => true + ), + 'dNSName' => array( + 'type' => ASN1::TYPE_IA5_STRING, + 'constant' => 2, + 'optional' => true, + 'implicit' => true + ), + 'x400Address' => array( + 'constant' => 3, + 'optional' => true, + 'implicit' => true + ) + $ORAddress, + 'directoryName' => array( + 'constant' => 4, + 'optional' => true, + 'explicit' => true + ) + $this->Name, + 'ediPartyName' => array( + 'constant' => 5, + 'optional' => true, + 'implicit' => true + ) + $EDIPartyName, + 'uniformResourceIdentifier' => array( + 'type' => ASN1::TYPE_IA5_STRING, + 'constant' => 6, + 'optional' => true, + 'implicit' => true + ), + 'iPAddress' => array( + 'type' => ASN1::TYPE_OCTET_STRING, + 'constant' => 7, + 'optional' => true, + 'implicit' => true + ), + 'registeredID' => array( + 'type' => ASN1::TYPE_OBJECT_IDENTIFIER, + 'constant' => 8, + 'optional' => true, + 'implicit' => true + ) + ) + ); + + $GeneralNames = array( + 'type' => ASN1::TYPE_SEQUENCE, + 'min' => 1, + 'max' => -1, + 'children' => $GeneralName + ); + + $this->IssuerAltName = $GeneralNames; + + $ReasonFlags = array( + 'type' => ASN1::TYPE_BIT_STRING, + 'mapping' => array( + 'unused', + 'keyCompromise', + 'cACompromise', + 'affiliationChanged', + 'superseded', + 'cessationOfOperation', + 'certificateHold', + 'privilegeWithdrawn', + 'aACompromise' + ) + ); + + $DistributionPointName = array( + 'type' => ASN1::TYPE_CHOICE, + 'children' => array( + 'fullName' => array( + 'constant' => 0, + 'optional' => true, + 'implicit' => true + ) + $GeneralNames, + 'nameRelativeToCRLIssuer' => array( + 'constant' => 1, + 'optional' => true, + 'implicit' => true + ) + $this->RelativeDistinguishedName + ) + ); + + $DistributionPoint = array( + 'type' => ASN1::TYPE_SEQUENCE, + 'children' => array( + 'distributionPoint' => array( + 'constant' => 0, + 'optional' => true, + 'explicit' => true + ) + $DistributionPointName, + 'reasons' => array( + 'constant' => 1, + 'optional' => true, + 'implicit' => true + ) + $ReasonFlags, + 'cRLIssuer' => array( + 'constant' => 2, + 'optional' => true, + 'implicit' => true + ) + $GeneralNames + ) + ); + + $this->CRLDistributionPoints = array( + 'type' => ASN1::TYPE_SEQUENCE, + 'min' => 1, + 'max' => -1, + 'children' => $DistributionPoint + ); + + $this->AuthorityKeyIdentifier = array( + 'type' => ASN1::TYPE_SEQUENCE, + 'children' => array( + 'keyIdentifier' => array( + 'constant' => 0, + 'optional' => true, + 'implicit' => true + ) + $this->KeyIdentifier, + 'authorityCertIssuer' => array( + 'constant' => 1, + 'optional' => true, + 'implicit' => true + ) + $GeneralNames, + 'authorityCertSerialNumber' => array( + 'constant' => 2, + 'optional' => true, + 'implicit' => true + ) + $CertificateSerialNumber + ) + ); + + $PolicyQualifierId = array('type' => ASN1::TYPE_OBJECT_IDENTIFIER); + + $PolicyQualifierInfo = array( + 'type' => ASN1::TYPE_SEQUENCE, + 'children' => array( + 'policyQualifierId' => $PolicyQualifierId, + 'qualifier' => array('type' => ASN1::TYPE_ANY) + ) + ); + + $CertPolicyId = array('type' => ASN1::TYPE_OBJECT_IDENTIFIER); + + $PolicyInformation = array( + 'type' => ASN1::TYPE_SEQUENCE, + 'children' => array( + 'policyIdentifier' => $CertPolicyId, + 'policyQualifiers' => array( + 'type' => ASN1::TYPE_SEQUENCE, + 'min' => 0, + 'max' => -1, + 'optional' => true, + 'children' => $PolicyQualifierInfo + ) + ) + ); + + $this->CertificatePolicies = array( + 'type' => ASN1::TYPE_SEQUENCE, + 'min' => 1, + 'max' => -1, + 'children' => $PolicyInformation + ); + + $this->PolicyMappings = array( + 'type' => ASN1::TYPE_SEQUENCE, + 'min' => 1, + 'max' => -1, + 'children' => array( + 'type' => ASN1::TYPE_SEQUENCE, + 'children' => array( + 'issuerDomainPolicy' => $CertPolicyId, + 'subjectDomainPolicy' => $CertPolicyId + ) + ) + ); + + $KeyPurposeId = array('type' => ASN1::TYPE_OBJECT_IDENTIFIER); + + $this->ExtKeyUsageSyntax = array( + 'type' => ASN1::TYPE_SEQUENCE, + 'min' => 1, + 'max' => -1, + 'children' => $KeyPurposeId + ); + + $AccessDescription = array( + 'type' => ASN1::TYPE_SEQUENCE, + 'children' => array( + 'accessMethod' => array('type' => ASN1::TYPE_OBJECT_IDENTIFIER), + 'accessLocation' => $GeneralName + ) + ); + + $this->AuthorityInfoAccessSyntax = array( + 'type' => ASN1::TYPE_SEQUENCE, + 'min' => 1, + 'max' => -1, + 'children' => $AccessDescription + ); + + $this->SubjectInfoAccessSyntax = array( + 'type' => ASN1::TYPE_SEQUENCE, + 'min' => 1, + 'max' => -1, + 'children' => $AccessDescription + ); + + $this->SubjectAltName = $GeneralNames; + + $this->PrivateKeyUsagePeriod = array( + 'type' => ASN1::TYPE_SEQUENCE, + 'children' => array( + 'notBefore' => array( + 'constant' => 0, + 'optional' => true, + 'implicit' => true, + 'type' => ASN1::TYPE_GENERALIZED_TIME), + 'notAfter' => array( + 'constant' => 1, + 'optional' => true, + 'implicit' => true, + 'type' => ASN1::TYPE_GENERALIZED_TIME) + ) + ); + + $BaseDistance = array('type' => ASN1::TYPE_INTEGER); + + $GeneralSubtree = array( + 'type' => ASN1::TYPE_SEQUENCE, + 'children' => array( + 'base' => $GeneralName, + 'minimum' => array( + 'constant' => 0, + 'optional' => true, + 'implicit' => true, + 'default' => new BigInteger(0) + ) + $BaseDistance, + 'maximum' => array( + 'constant' => 1, + 'optional' => true, + 'implicit' => true, + ) + $BaseDistance + ) + ); + + $GeneralSubtrees = array( + 'type' => ASN1::TYPE_SEQUENCE, + 'min' => 1, + 'max' => -1, + 'children' => $GeneralSubtree + ); + + $this->NameConstraints = array( + 'type' => ASN1::TYPE_SEQUENCE, + 'children' => array( + 'permittedSubtrees' => array( + 'constant' => 0, + 'optional' => true, + 'implicit' => true + ) + $GeneralSubtrees, + 'excludedSubtrees' => array( + 'constant' => 1, + 'optional' => true, + 'implicit' => true + ) + $GeneralSubtrees + ) + ); + + $this->CPSuri = array('type' => ASN1::TYPE_IA5_STRING); + + $DisplayText = array( + 'type' => ASN1::TYPE_CHOICE, + 'children' => array( + 'ia5String' => array('type' => ASN1::TYPE_IA5_STRING), + 'visibleString' => array('type' => ASN1::TYPE_VISIBLE_STRING), + 'bmpString' => array('type' => ASN1::TYPE_BMP_STRING), + 'utf8String' => array('type' => ASN1::TYPE_UTF8_STRING) + ) + ); + + $NoticeReference = array( + 'type' => ASN1::TYPE_SEQUENCE, + 'children' => array( + 'organization' => $DisplayText, + 'noticeNumbers' => array( + 'type' => ASN1::TYPE_SEQUENCE, + 'min' => 1, + 'max' => 200, + 'children' => array('type' => ASN1::TYPE_INTEGER) + ) + ) + ); + + $this->UserNotice = array( + 'type' => ASN1::TYPE_SEQUENCE, + 'children' => array( + 'noticeRef' => array( + 'optional' => true, + 'implicit' => true + ) + $NoticeReference, + 'explicitText' => array( + 'optional' => true, + 'implicit' => true + ) + $DisplayText + ) + ); + + // mapping is from + $this->netscape_cert_type = array( + 'type' => ASN1::TYPE_BIT_STRING, + 'mapping' => array( + 'SSLClient', + 'SSLServer', + 'Email', + 'ObjectSigning', + 'Reserved', + 'SSLCA', + 'EmailCA', + 'ObjectSigningCA' + ) + ); + + $this->netscape_comment = array('type' => ASN1::TYPE_IA5_STRING); + $this->netscape_ca_policy_url = array('type' => ASN1::TYPE_IA5_STRING); + + // attribute is used in RFC2986 but we're using the RFC5280 definition + + $Attribute = array( + 'type' => ASN1::TYPE_SEQUENCE, + 'children' => array( + 'type' => $AttributeType, + 'value'=> array( + 'type' => ASN1::TYPE_SET, + 'min' => 1, + 'max' => -1, + 'children' => $this->AttributeValue + ) + ) + ); + + $this->SubjectDirectoryAttributes = array( + 'type' => ASN1::TYPE_SEQUENCE, + 'min' => 1, + 'max' => -1, + 'children' => $Attribute + ); + + // adapted from + + $Attributes = array( + 'type' => ASN1::TYPE_SET, + 'min' => 1, + 'max' => -1, + 'children' => $Attribute + ); + + $CertificationRequestInfo = array( + 'type' => ASN1::TYPE_SEQUENCE, + 'children' => array( + 'version' => array( + 'type' => ASN1::TYPE_INTEGER, + 'mapping' => array('v1') + ), + 'subject' => $this->Name, + 'subjectPKInfo' => $SubjectPublicKeyInfo, + 'attributes' => array( + 'constant' => 0, + 'optional' => true, + 'implicit' => true + ) + $Attributes, + ) + ); + + $this->CertificationRequest = array( + 'type' => ASN1::TYPE_SEQUENCE, + 'children' => array( + 'certificationRequestInfo' => $CertificationRequestInfo, + 'signatureAlgorithm' => $AlgorithmIdentifier, + 'signature' => array('type' => ASN1::TYPE_BIT_STRING) + ) + ); + + $RevokedCertificate = array( + 'type' => ASN1::TYPE_SEQUENCE, + 'children' => array( + 'userCertificate' => $CertificateSerialNumber, + 'revocationDate' => $Time, + 'crlEntryExtensions' => array( + 'optional' => true + ) + $this->Extensions + ) + ); + + $TBSCertList = array( + 'type' => ASN1::TYPE_SEQUENCE, + 'children' => array( + 'version' => array( + 'optional' => true, + 'default' => 'v1' + ) + $Version, + 'signature' => $AlgorithmIdentifier, + 'issuer' => $this->Name, + 'thisUpdate' => $Time, + 'nextUpdate' => array( + 'optional' => true + ) + $Time, + 'revokedCertificates' => array( + 'type' => ASN1::TYPE_SEQUENCE, + 'optional' => true, + 'min' => 0, + 'max' => -1, + 'children' => $RevokedCertificate + ), + 'crlExtensions' => array( + 'constant' => 0, + 'optional' => true, + 'explicit' => true + ) + $this->Extensions + ) + ); + + $this->CertificateList = array( + 'type' => ASN1::TYPE_SEQUENCE, + 'children' => array( + 'tbsCertList' => $TBSCertList, + 'signatureAlgorithm' => $AlgorithmIdentifier, + 'signature' => array('type' => ASN1::TYPE_BIT_STRING) + ) + ); + + $this->CRLNumber = array('type' => ASN1::TYPE_INTEGER); + + $this->CRLReason = array('type' => ASN1::TYPE_ENUMERATED, + 'mapping' => array( + 'unspecified', + 'keyCompromise', + 'cACompromise', + 'affiliationChanged', + 'superseded', + 'cessationOfOperation', + 'certificateHold', + // Value 7 is not used. + 8 => 'removeFromCRL', + 'privilegeWithdrawn', + 'aACompromise' + ) + ); + + $this->IssuingDistributionPoint = array('type' => ASN1::TYPE_SEQUENCE, + 'children' => array( + 'distributionPoint' => array( + 'constant' => 0, + 'optional' => true, + 'explicit' => true + ) + $DistributionPointName, + 'onlyContainsUserCerts' => array( + 'type' => ASN1::TYPE_BOOLEAN, + 'constant' => 1, + 'optional' => true, + 'default' => false, + 'implicit' => true + ), + 'onlyContainsCACerts' => array( + 'type' => ASN1::TYPE_BOOLEAN, + 'constant' => 2, + 'optional' => true, + 'default' => false, + 'implicit' => true + ), + 'onlySomeReasons' => array( + 'constant' => 3, + 'optional' => true, + 'implicit' => true + ) + $ReasonFlags, + 'indirectCRL' => array( + 'type' => ASN1::TYPE_BOOLEAN, + 'constant' => 4, + 'optional' => true, + 'default' => false, + 'implicit' => true + ), + 'onlyContainsAttributeCerts' => array( + 'type' => ASN1::TYPE_BOOLEAN, + 'constant' => 5, + 'optional' => true, + 'default' => false, + 'implicit' => true + ) + ) + ); + + $this->InvalidityDate = array('type' => ASN1::TYPE_GENERALIZED_TIME); + + $this->CertificateIssuer = $GeneralNames; + + $this->HoldInstructionCode = array('type' => ASN1::TYPE_OBJECT_IDENTIFIER); + + $PublicKeyAndChallenge = array( + 'type' => ASN1::TYPE_SEQUENCE, + 'children' => array( + 'spki' => $SubjectPublicKeyInfo, + 'challenge' => array('type' => ASN1::TYPE_IA5_STRING) + ) + ); + + $this->SignedPublicKeyAndChallenge = array( + 'type' => ASN1::TYPE_SEQUENCE, + 'children' => array( + 'publicKeyAndChallenge' => $PublicKeyAndChallenge, + 'signatureAlgorithm' => $AlgorithmIdentifier, + 'signature' => array('type' => ASN1::TYPE_BIT_STRING) + ) + ); + + $this->PostalAddress = array( + 'type' => ASN1::TYPE_SEQUENCE, + 'optional' => true, + 'min' => 1, + 'max' => -1, + 'children' => $this->DirectoryString + ); + + // OIDs from RFC5280 and those RFCs mentioned in RFC5280#section-4.1.1.2 + $this->oids = array( + '1.3.6.1.5.5.7' => 'id-pkix', + '1.3.6.1.5.5.7.1' => 'id-pe', + '1.3.6.1.5.5.7.2' => 'id-qt', + '1.3.6.1.5.5.7.3' => 'id-kp', + '1.3.6.1.5.5.7.48' => 'id-ad', + '1.3.6.1.5.5.7.2.1' => 'id-qt-cps', + '1.3.6.1.5.5.7.2.2' => 'id-qt-unotice', + '1.3.6.1.5.5.7.48.1' =>'id-ad-ocsp', + '1.3.6.1.5.5.7.48.2' => 'id-ad-caIssuers', + '1.3.6.1.5.5.7.48.3' => 'id-ad-timeStamping', + '1.3.6.1.5.5.7.48.5' => 'id-ad-caRepository', + '2.5.4' => 'id-at', + '2.5.4.41' => 'id-at-name', + '2.5.4.4' => 'id-at-surname', + '2.5.4.42' => 'id-at-givenName', + '2.5.4.43' => 'id-at-initials', + '2.5.4.44' => 'id-at-generationQualifier', + '2.5.4.3' => 'id-at-commonName', + '2.5.4.7' => 'id-at-localityName', + '2.5.4.8' => 'id-at-stateOrProvinceName', + '2.5.4.10' => 'id-at-organizationName', + '2.5.4.11' => 'id-at-organizationalUnitName', + '2.5.4.12' => 'id-at-title', + '2.5.4.13' => 'id-at-description', + '2.5.4.46' => 'id-at-dnQualifier', + '2.5.4.6' => 'id-at-countryName', + '2.5.4.5' => 'id-at-serialNumber', + '2.5.4.65' => 'id-at-pseudonym', + '2.5.4.17' => 'id-at-postalCode', + '2.5.4.9' => 'id-at-streetAddress', + '2.5.4.45' => 'id-at-uniqueIdentifier', + '2.5.4.72' => 'id-at-role', + '2.5.4.16' => 'id-at-postalAddress', + '1.3.6.1.4.1.311.60.2.1.3' => 'jurisdictionOfIncorporationCountryName', + '1.3.6.1.4.1.311.60.2.1.2' => 'jurisdictionOfIncorporationStateOrProvinceName', + '1.3.6.1.4.1.311.60.2.1.1' => 'jurisdictionLocalityName', + '2.5.4.15' => 'id-at-businessCategory', + + '0.9.2342.19200300.100.1.25' => 'id-domainComponent', + '1.2.840.113549.1.9' => 'pkcs-9', + '1.2.840.113549.1.9.1' => 'pkcs-9-at-emailAddress', + '2.5.29' => 'id-ce', + '2.5.29.35' => 'id-ce-authorityKeyIdentifier', + '2.5.29.14' => 'id-ce-subjectKeyIdentifier', + '2.5.29.15' => 'id-ce-keyUsage', + '2.5.29.16' => 'id-ce-privateKeyUsagePeriod', + '2.5.29.32' => 'id-ce-certificatePolicies', + '2.5.29.32.0' => 'anyPolicy', + + '2.5.29.33' => 'id-ce-policyMappings', + '2.5.29.17' => 'id-ce-subjectAltName', + '2.5.29.18' => 'id-ce-issuerAltName', + '2.5.29.9' => 'id-ce-subjectDirectoryAttributes', + '2.5.29.19' => 'id-ce-basicConstraints', + '2.5.29.30' => 'id-ce-nameConstraints', + '2.5.29.36' => 'id-ce-policyConstraints', + '2.5.29.31' => 'id-ce-cRLDistributionPoints', + '2.5.29.37' => 'id-ce-extKeyUsage', + '2.5.29.37.0' => 'anyExtendedKeyUsage', + '1.3.6.1.5.5.7.3.1' => 'id-kp-serverAuth', + '1.3.6.1.5.5.7.3.2' => 'id-kp-clientAuth', + '1.3.6.1.5.5.7.3.3' => 'id-kp-codeSigning', + '1.3.6.1.5.5.7.3.4' => 'id-kp-emailProtection', + '1.3.6.1.5.5.7.3.8' => 'id-kp-timeStamping', + '1.3.6.1.5.5.7.3.9' => 'id-kp-OCSPSigning', + '2.5.29.54' => 'id-ce-inhibitAnyPolicy', + '2.5.29.46' => 'id-ce-freshestCRL', + '1.3.6.1.5.5.7.1.1' => 'id-pe-authorityInfoAccess', + '1.3.6.1.5.5.7.1.11' => 'id-pe-subjectInfoAccess', + '2.5.29.20' => 'id-ce-cRLNumber', + '2.5.29.28' => 'id-ce-issuingDistributionPoint', + '2.5.29.27' => 'id-ce-deltaCRLIndicator', + '2.5.29.21' => 'id-ce-cRLReasons', + '2.5.29.29' => 'id-ce-certificateIssuer', + '2.5.29.23' => 'id-ce-holdInstructionCode', + '1.2.840.10040.2' => 'holdInstruction', + '1.2.840.10040.2.1' => 'id-holdinstruction-none', + '1.2.840.10040.2.2' => 'id-holdinstruction-callissuer', + '1.2.840.10040.2.3' => 'id-holdinstruction-reject', + '2.5.29.24' => 'id-ce-invalidityDate', + + '1.2.840.113549.2.2' => 'md2', + '1.2.840.113549.2.5' => 'md5', + '1.3.14.3.2.26' => 'id-sha1', + '1.2.840.10040.4.1' => 'id-dsa', + '1.2.840.10040.4.3' => 'id-dsa-with-sha1', + '1.2.840.113549.1.1' => 'pkcs-1', + '1.2.840.113549.1.1.1' => 'rsaEncryption', + '1.2.840.113549.1.1.2' => 'md2WithRSAEncryption', + '1.2.840.113549.1.1.4' => 'md5WithRSAEncryption', + '1.2.840.113549.1.1.5' => 'sha1WithRSAEncryption', + '1.2.840.10046.2.1' => 'dhpublicnumber', + '2.16.840.1.101.2.1.1.22' => 'id-keyExchangeAlgorithm', + '1.2.840.10045' => 'ansi-X9-62', + '1.2.840.10045.4' => 'id-ecSigType', + '1.2.840.10045.4.1' => 'ecdsa-with-SHA1', + '1.2.840.10045.1' => 'id-fieldType', + '1.2.840.10045.1.1' => 'prime-field', + '1.2.840.10045.1.2' => 'characteristic-two-field', + '1.2.840.10045.1.2.3' => 'id-characteristic-two-basis', + '1.2.840.10045.1.2.3.1' => 'gnBasis', + '1.2.840.10045.1.2.3.2' => 'tpBasis', + '1.2.840.10045.1.2.3.3' => 'ppBasis', + '1.2.840.10045.2' => 'id-publicKeyType', + '1.2.840.10045.2.1' => 'id-ecPublicKey', + '1.2.840.10045.3' => 'ellipticCurve', + '1.2.840.10045.3.0' => 'c-TwoCurve', + '1.2.840.10045.3.0.1' => 'c2pnb163v1', + '1.2.840.10045.3.0.2' => 'c2pnb163v2', + '1.2.840.10045.3.0.3' => 'c2pnb163v3', + '1.2.840.10045.3.0.4' => 'c2pnb176w1', + '1.2.840.10045.3.0.5' => 'c2pnb191v1', + '1.2.840.10045.3.0.6' => 'c2pnb191v2', + '1.2.840.10045.3.0.7' => 'c2pnb191v3', + '1.2.840.10045.3.0.8' => 'c2pnb191v4', + '1.2.840.10045.3.0.9' => 'c2pnb191v5', + '1.2.840.10045.3.0.10' => 'c2pnb208w1', + '1.2.840.10045.3.0.11' => 'c2pnb239v1', + '1.2.840.10045.3.0.12' => 'c2pnb239v2', + '1.2.840.10045.3.0.13' => 'c2pnb239v3', + '1.2.840.10045.3.0.14' => 'c2pnb239v4', + '1.2.840.10045.3.0.15' => 'c2pnb239v5', + '1.2.840.10045.3.0.16' => 'c2pnb272w1', + '1.2.840.10045.3.0.17' => 'c2pnb304w1', + '1.2.840.10045.3.0.18' => 'c2pnb359v1', + '1.2.840.10045.3.0.19' => 'c2pnb368w1', + '1.2.840.10045.3.0.20' => 'c2pnb431r1', + '1.2.840.10045.3.1' => 'primeCurve', + '1.2.840.10045.3.1.1' => 'prime192v1', + '1.2.840.10045.3.1.2' => 'prime192v2', + '1.2.840.10045.3.1.3' => 'prime192v3', + '1.2.840.10045.3.1.4' => 'prime239v1', + '1.2.840.10045.3.1.5' => 'prime239v2', + '1.2.840.10045.3.1.6' => 'prime239v3', + '1.2.840.10045.3.1.7' => 'prime256v1', + '1.2.840.113549.1.1.7' => 'id-RSAES-OAEP', + '1.2.840.113549.1.1.9' => 'id-pSpecified', + '1.2.840.113549.1.1.10' => 'id-RSASSA-PSS', + '1.2.840.113549.1.1.8' => 'id-mgf1', + '1.2.840.113549.1.1.14' => 'sha224WithRSAEncryption', + '1.2.840.113549.1.1.11' => 'sha256WithRSAEncryption', + '1.2.840.113549.1.1.12' => 'sha384WithRSAEncryption', + '1.2.840.113549.1.1.13' => 'sha512WithRSAEncryption', + '2.16.840.1.101.3.4.2.4' => 'id-sha224', + '2.16.840.1.101.3.4.2.1' => 'id-sha256', + '2.16.840.1.101.3.4.2.2' => 'id-sha384', + '2.16.840.1.101.3.4.2.3' => 'id-sha512', + '1.2.643.2.2.4' => 'id-GostR3411-94-with-GostR3410-94', + '1.2.643.2.2.3' => 'id-GostR3411-94-with-GostR3410-2001', + '1.2.643.2.2.20' => 'id-GostR3410-2001', + '1.2.643.2.2.19' => 'id-GostR3410-94', + // Netscape Object Identifiers from "Netscape Certificate Extensions" + '2.16.840.1.113730' => 'netscape', + '2.16.840.1.113730.1' => 'netscape-cert-extension', + '2.16.840.1.113730.1.1' => 'netscape-cert-type', + '2.16.840.1.113730.1.13' => 'netscape-comment', + '2.16.840.1.113730.1.8' => 'netscape-ca-policy-url', + // the following are X.509 extensions not supported by phpseclib + '1.3.6.1.5.5.7.1.12' => 'id-pe-logotype', + '1.2.840.113533.7.65.0' => 'entrustVersInfo', + '2.16.840.1.113733.1.6.9' => 'verisignPrivate', + // for Certificate Signing Requests + // see http://tools.ietf.org/html/rfc2985 + '1.2.840.113549.1.9.2' => 'pkcs-9-at-unstructuredName', // PKCS #9 unstructured name + '1.2.840.113549.1.9.7' => 'pkcs-9-at-challengePassword', // Challenge password for certificate revocations + '1.2.840.113549.1.9.14' => 'pkcs-9-at-extensionRequest' // Certificate extension request + ); + } + + /** + * Load X.509 certificate + * + * Returns an associative array describing the X.509 cert or a false if the cert failed to load + * + * @param string $cert + * @param int $mode + * @access public + * @return mixed + */ + function loadX509($cert, $mode = self::FORMAT_AUTO_DETECT) + { + if (is_array($cert) && isset($cert['tbsCertificate'])) { + unset($this->currentCert); + unset($this->currentKeyIdentifier); + $this->dn = $cert['tbsCertificate']['subject']; + if (!isset($this->dn)) { + return false; + } + $this->currentCert = $cert; + + $currentKeyIdentifier = $this->getExtension('id-ce-subjectKeyIdentifier'); + $this->currentKeyIdentifier = is_string($currentKeyIdentifier) ? $currentKeyIdentifier : null; + + unset($this->signatureSubject); + + return $cert; + } + + $asn1 = new ASN1(); + + if ($mode != self::FORMAT_DER) { + $newcert = $this->_extractBER($cert); + if ($mode == self::FORMAT_PEM && $cert == $newcert) { + return false; + } + $cert = $newcert; + } + + if ($cert === false) { + $this->currentCert = false; + return false; + } + + $asn1->loadOIDs($this->oids); + $decoded = $asn1->decodeBER($cert); + + if (!empty($decoded)) { + $x509 = $asn1->asn1map($decoded[0], $this->Certificate); + } + if (!isset($x509) || $x509 === false) { + $this->currentCert = false; + return false; + } + + $this->signatureSubject = substr($cert, $decoded[0]['content'][0]['start'], $decoded[0]['content'][0]['length']); + + if ($this->_isSubArrayValid($x509, 'tbsCertificate/extensions')) { + $this->_mapInExtensions($x509, 'tbsCertificate/extensions', $asn1); + } + $this->_mapInDNs($x509, 'tbsCertificate/issuer/rdnSequence', $asn1); + $this->_mapInDNs($x509, 'tbsCertificate/subject/rdnSequence', $asn1); + + $key = &$x509['tbsCertificate']['subjectPublicKeyInfo']['subjectPublicKey']; + $key = $this->_reformatKey($x509['tbsCertificate']['subjectPublicKeyInfo']['algorithm']['algorithm'], $key); + + $this->currentCert = $x509; + $this->dn = $x509['tbsCertificate']['subject']; + + $currentKeyIdentifier = $this->getExtension('id-ce-subjectKeyIdentifier'); + $this->currentKeyIdentifier = is_string($currentKeyIdentifier) ? $currentKeyIdentifier : null; + + return $x509; + } + + /** + * Save X.509 certificate + * + * @param array $cert + * @param int $format optional + * @access public + * @return string + */ + function saveX509($cert, $format = self::FORMAT_PEM) + { + if (!is_array($cert) || !isset($cert['tbsCertificate'])) { + return false; + } + + switch (true) { + // "case !$a: case !$b: break; default: whatever();" is the same thing as "if ($a && $b) whatever()" + case !($algorithm = $this->_subArray($cert, 'tbsCertificate/subjectPublicKeyInfo/algorithm/algorithm')): + case is_object($cert['tbsCertificate']['subjectPublicKeyInfo']['subjectPublicKey']): + break; + default: + switch ($algorithm) { + case 'rsaEncryption': + $cert['tbsCertificate']['subjectPublicKeyInfo']['subjectPublicKey'] + = base64_encode("\0" . base64_decode(preg_replace('#-.+-|[\r\n]#', '', $cert['tbsCertificate']['subjectPublicKeyInfo']['subjectPublicKey']))); + /* "[For RSA keys] the parameters field MUST have ASN.1 type NULL for this algorithm identifier." + -- https://tools.ietf.org/html/rfc3279#section-2.3.1 + + given that and the fact that RSA keys appear ot be the only key type for which the parameters field can be blank, + it seems like perhaps the ASN.1 description ought not say the parameters field is OPTIONAL, but whatever. + */ + $cert['tbsCertificate']['subjectPublicKeyInfo']['algorithm']['parameters'] = null; + // https://tools.ietf.org/html/rfc3279#section-2.2.1 + $cert['signatureAlgorithm']['parameters'] = null; + $cert['tbsCertificate']['signature']['parameters'] = null; + } + } + + $asn1 = new ASN1(); + $asn1->loadOIDs($this->oids); + + $filters = array(); + $type_utf8_string = array('type' => ASN1::TYPE_UTF8_STRING); + $filters['tbsCertificate']['signature']['parameters'] = $type_utf8_string; + $filters['tbsCertificate']['signature']['issuer']['rdnSequence']['value'] = $type_utf8_string; + $filters['tbsCertificate']['issuer']['rdnSequence']['value'] = $type_utf8_string; + $filters['tbsCertificate']['subject']['rdnSequence']['value'] = $type_utf8_string; + $filters['tbsCertificate']['subjectPublicKeyInfo']['algorithm']['parameters'] = $type_utf8_string; + $filters['signatureAlgorithm']['parameters'] = $type_utf8_string; + $filters['authorityCertIssuer']['directoryName']['rdnSequence']['value'] = $type_utf8_string; + //$filters['policyQualifiers']['qualifier'] = $type_utf8_string; + $filters['distributionPoint']['fullName']['directoryName']['rdnSequence']['value'] = $type_utf8_string; + $filters['directoryName']['rdnSequence']['value'] = $type_utf8_string; + + /* in the case of policyQualifiers/qualifier, the type has to be \phpseclib\File\ASN1::TYPE_IA5_STRING. + \phpseclib\File\ASN1::TYPE_PRINTABLE_STRING will cause OpenSSL's X.509 parser to spit out random + characters. + */ + $filters['policyQualifiers']['qualifier'] + = array('type' => ASN1::TYPE_IA5_STRING); + + $asn1->loadFilters($filters); + + $this->_mapOutExtensions($cert, 'tbsCertificate/extensions', $asn1); + $this->_mapOutDNs($cert, 'tbsCertificate/issuer/rdnSequence', $asn1); + $this->_mapOutDNs($cert, 'tbsCertificate/subject/rdnSequence', $asn1); + + $cert = $asn1->encodeDER($cert, $this->Certificate); + + switch ($format) { + case self::FORMAT_DER: + return $cert; + // case self::FORMAT_PEM: + default: + return "-----BEGIN CERTIFICATE-----\r\n" . chunk_split(base64_encode($cert), 64) . '-----END CERTIFICATE-----'; + } + } + + /** + * Map extension values from octet string to extension-specific internal + * format. + * + * @param array $root (by reference) + * @param string $path + * @param object $asn1 + * @access private + */ + function _mapInExtensions(&$root, $path, $asn1) + { + $extensions = &$this->_subArrayUnchecked($root, $path); + + if ($extensions) { + for ($i = 0; $i < count($extensions); $i++) { + $id = $extensions[$i]['extnId']; + $value = &$extensions[$i]['extnValue']; + $value = base64_decode($value); + /* [extnValue] contains the DER encoding of an ASN.1 value + corresponding to the extension type identified by extnID */ + $map = $this->_getMapping($id); + if (!is_bool($map)) { + $decoder = $id == 'id-ce-nameConstraints' ? + array($this, '_decodeNameConstraintIP') : + array($this, '_decodeIP'); + $decoded = $asn1->decodeBER($value); + $mapped = $asn1->asn1map($decoded[0], $map, array('iPAddress' => $decoder)); + $value = $mapped === false ? $decoded[0] : $mapped; + + if ($id == 'id-ce-certificatePolicies') { + for ($j = 0; $j < count($value); $j++) { + if (!isset($value[$j]['policyQualifiers'])) { + continue; + } + for ($k = 0; $k < count($value[$j]['policyQualifiers']); $k++) { + $subid = $value[$j]['policyQualifiers'][$k]['policyQualifierId']; + $map = $this->_getMapping($subid); + $subvalue = &$value[$j]['policyQualifiers'][$k]['qualifier']; + if ($map !== false) { + $decoded = $asn1->decodeBER($subvalue); + $mapped = $asn1->asn1map($decoded[0], $map); + $subvalue = $mapped === false ? $decoded[0] : $mapped; + } + } + } + } + } else { + $value = base64_encode($value); + } + } + } + } + + /** + * Map extension values from extension-specific internal format to + * octet string. + * + * @param array $root (by reference) + * @param string $path + * @param object $asn1 + * @access private + */ + function _mapOutExtensions(&$root, $path, $asn1) + { + $extensions = &$this->_subArray($root, $path); + + if (is_array($extensions)) { + $size = count($extensions); + for ($i = 0; $i < $size; $i++) { + if ($extensions[$i] instanceof Element) { + continue; + } + + $id = $extensions[$i]['extnId']; + $value = &$extensions[$i]['extnValue']; + + switch ($id) { + case 'id-ce-certificatePolicies': + for ($j = 0; $j < count($value); $j++) { + if (!isset($value[$j]['policyQualifiers'])) { + continue; + } + for ($k = 0; $k < count($value[$j]['policyQualifiers']); $k++) { + $subid = $value[$j]['policyQualifiers'][$k]['policyQualifierId']; + $map = $this->_getMapping($subid); + $subvalue = &$value[$j]['policyQualifiers'][$k]['qualifier']; + if ($map !== false) { + // by default \phpseclib\File\ASN1 will try to render qualifier as a \phpseclib\File\ASN1::TYPE_IA5_STRING since it's + // actual type is \phpseclib\File\ASN1::TYPE_ANY + $subvalue = new Element($asn1->encodeDER($subvalue, $map)); + } + } + } + break; + case 'id-ce-authorityKeyIdentifier': // use 00 as the serial number instead of an empty string + if (isset($value['authorityCertSerialNumber'])) { + if ($value['authorityCertSerialNumber']->toBytes() == '') { + $temp = chr((ASN1::CLASS_CONTEXT_SPECIFIC << 6) | 2) . "\1\0"; + $value['authorityCertSerialNumber'] = new Element($temp); + } + } + } + + /* [extnValue] contains the DER encoding of an ASN.1 value + corresponding to the extension type identified by extnID */ + $map = $this->_getMapping($id); + if (is_bool($map)) { + if (!$map) { + user_error($id . ' is not a currently supported extension'); + unset($extensions[$i]); + } + } else { + $temp = $asn1->encodeDER($value, $map, array('iPAddress' => array($this, '_encodeIP'))); + $value = base64_encode($temp); + } + } + } + } + + /** + * Map attribute values from ANY type to attribute-specific internal + * format. + * + * @param array $root (by reference) + * @param string $path + * @param object $asn1 + * @access private + */ + function _mapInAttributes(&$root, $path, $asn1) + { + $attributes = &$this->_subArray($root, $path); + + if (is_array($attributes)) { + for ($i = 0; $i < count($attributes); $i++) { + $id = $attributes[$i]['type']; + /* $value contains the DER encoding of an ASN.1 value + corresponding to the attribute type identified by type */ + $map = $this->_getMapping($id); + if (is_array($attributes[$i]['value'])) { + $values = &$attributes[$i]['value']; + for ($j = 0; $j < count($values); $j++) { + $value = $asn1->encodeDER($values[$j], $this->AttributeValue); + $decoded = $asn1->decodeBER($value); + if (!is_bool($map)) { + $mapped = $asn1->asn1map($decoded[0], $map); + if ($mapped !== false) { + $values[$j] = $mapped; + } + if ($id == 'pkcs-9-at-extensionRequest' && $this->_isSubArrayValid($values, $j)) { + $this->_mapInExtensions($values, $j, $asn1); + } + } elseif ($map) { + $values[$j] = base64_encode($value); + } + } + } + } + } + } + + /** + * Map attribute values from attribute-specific internal format to + * ANY type. + * + * @param array $root (by reference) + * @param string $path + * @param object $asn1 + * @access private + */ + function _mapOutAttributes(&$root, $path, $asn1) + { + $attributes = &$this->_subArray($root, $path); + + if (is_array($attributes)) { + $size = count($attributes); + for ($i = 0; $i < $size; $i++) { + /* [value] contains the DER encoding of an ASN.1 value + corresponding to the attribute type identified by type */ + $id = $attributes[$i]['type']; + $map = $this->_getMapping($id); + if ($map === false) { + user_error($id . ' is not a currently supported attribute', E_USER_NOTICE); + unset($attributes[$i]); + } elseif (is_array($attributes[$i]['value'])) { + $values = &$attributes[$i]['value']; + for ($j = 0; $j < count($values); $j++) { + switch ($id) { + case 'pkcs-9-at-extensionRequest': + $this->_mapOutExtensions($values, $j, $asn1); + break; + } + + if (!is_bool($map)) { + $temp = $asn1->encodeDER($values[$j], $map); + $decoded = $asn1->decodeBER($temp); + $values[$j] = $asn1->asn1map($decoded[0], $this->AttributeValue); + } + } + } + } + } + } + + /** + * Map DN values from ANY type to DN-specific internal + * format. + * + * @param array $root (by reference) + * @param string $path + * @param object $asn1 + * @access private + */ + function _mapInDNs(&$root, $path, $asn1) + { + $dns = &$this->_subArray($root, $path); + + if (is_array($dns)) { + for ($i = 0; $i < count($dns); $i++) { + for ($j = 0; $j < count($dns[$i]); $j++) { + $type = $dns[$i][$j]['type']; + $value = &$dns[$i][$j]['value']; + if (is_object($value) && $value instanceof Element) { + $map = $this->_getMapping($type); + if (!is_bool($map)) { + $decoded = $asn1->decodeBER($value); + $value = $asn1->asn1map($decoded[0], $map); + } + } + } + } + } + } + + /** + * Map DN values from DN-specific internal format to + * ANY type. + * + * @param array $root (by reference) + * @param string $path + * @param object $asn1 + * @access private + */ + function _mapOutDNs(&$root, $path, $asn1) + { + $dns = &$this->_subArray($root, $path); + + if (is_array($dns)) { + $size = count($dns); + for ($i = 0; $i < $size; $i++) { + for ($j = 0; $j < count($dns[$i]); $j++) { + $type = $dns[$i][$j]['type']; + $value = &$dns[$i][$j]['value']; + if (is_object($value) && $value instanceof Element) { + continue; + } + + $map = $this->_getMapping($type); + if (!is_bool($map)) { + $value = new Element($asn1->encodeDER($value, $map)); + } + } + } + } + } + + /** + * Associate an extension ID to an extension mapping + * + * @param string $extnId + * @access private + * @return mixed + */ + function _getMapping($extnId) + { + if (!is_string($extnId)) { // eg. if it's a \phpseclib\File\ASN1\Element object + return true; + } + + switch ($extnId) { + case 'id-ce-keyUsage': + return $this->KeyUsage; + case 'id-ce-basicConstraints': + return $this->BasicConstraints; + case 'id-ce-subjectKeyIdentifier': + return $this->KeyIdentifier; + case 'id-ce-cRLDistributionPoints': + return $this->CRLDistributionPoints; + case 'id-ce-authorityKeyIdentifier': + return $this->AuthorityKeyIdentifier; + case 'id-ce-certificatePolicies': + return $this->CertificatePolicies; + case 'id-ce-extKeyUsage': + return $this->ExtKeyUsageSyntax; + case 'id-pe-authorityInfoAccess': + return $this->AuthorityInfoAccessSyntax; + case 'id-pe-subjectInfoAccess': + return $this->SubjectInfoAccessSyntax; + case 'id-ce-subjectAltName': + return $this->SubjectAltName; + case 'id-ce-subjectDirectoryAttributes': + return $this->SubjectDirectoryAttributes; + case 'id-ce-privateKeyUsagePeriod': + return $this->PrivateKeyUsagePeriod; + case 'id-ce-issuerAltName': + return $this->IssuerAltName; + case 'id-ce-policyMappings': + return $this->PolicyMappings; + case 'id-ce-nameConstraints': + return $this->NameConstraints; + + case 'netscape-cert-type': + return $this->netscape_cert_type; + case 'netscape-comment': + return $this->netscape_comment; + case 'netscape-ca-policy-url': + return $this->netscape_ca_policy_url; + + // since id-qt-cps isn't a constructed type it will have already been decoded as a string by the time it gets + // back around to asn1map() and we don't want it decoded again. + //case 'id-qt-cps': + // return $this->CPSuri; + case 'id-qt-unotice': + return $this->UserNotice; + + // the following OIDs are unsupported but we don't want them to give notices when calling saveX509(). + case 'id-pe-logotype': // http://www.ietf.org/rfc/rfc3709.txt + case 'entrustVersInfo': + // http://support.microsoft.com/kb/287547 + case '1.3.6.1.4.1.311.20.2': // szOID_ENROLL_CERTTYPE_EXTENSION + case '1.3.6.1.4.1.311.21.1': // szOID_CERTSRV_CA_VERSION + // "SET Secure Electronic Transaction Specification" + // http://www.maithean.com/docs/set_bk3.pdf + case '2.23.42.7.0': // id-set-hashedRootKey + // "Certificate Transparency" + // https://tools.ietf.org/html/rfc6962 + case '1.3.6.1.4.1.11129.2.4.2': + // "Qualified Certificate statements" + // https://tools.ietf.org/html/rfc3739#section-3.2.6 + case '1.3.6.1.5.5.7.1.3': + return true; + + // CSR attributes + case 'pkcs-9-at-unstructuredName': + return $this->PKCS9String; + case 'pkcs-9-at-challengePassword': + return $this->DirectoryString; + case 'pkcs-9-at-extensionRequest': + return $this->Extensions; + + // CRL extensions. + case 'id-ce-cRLNumber': + return $this->CRLNumber; + case 'id-ce-deltaCRLIndicator': + return $this->CRLNumber; + case 'id-ce-issuingDistributionPoint': + return $this->IssuingDistributionPoint; + case 'id-ce-freshestCRL': + return $this->CRLDistributionPoints; + case 'id-ce-cRLReasons': + return $this->CRLReason; + case 'id-ce-invalidityDate': + return $this->InvalidityDate; + case 'id-ce-certificateIssuer': + return $this->CertificateIssuer; + case 'id-ce-holdInstructionCode': + return $this->HoldInstructionCode; + case 'id-at-postalAddress': + return $this->PostalAddress; + } + + return false; + } + + /** + * Load an X.509 certificate as a certificate authority + * + * @param string $cert + * @access public + * @return bool + */ + function loadCA($cert) + { + $olddn = $this->dn; + $oldcert = $this->currentCert; + $oldsigsubj = $this->signatureSubject; + $oldkeyid = $this->currentKeyIdentifier; + + $cert = $this->loadX509($cert); + if (!$cert) { + $this->dn = $olddn; + $this->currentCert = $oldcert; + $this->signatureSubject = $oldsigsubj; + $this->currentKeyIdentifier = $oldkeyid; + + return false; + } + + /* From RFC5280 "PKIX Certificate and CRL Profile": + + If the keyUsage extension is present, then the subject public key + MUST NOT be used to verify signatures on certificates or CRLs unless + the corresponding keyCertSign or cRLSign bit is set. */ + //$keyUsage = $this->getExtension('id-ce-keyUsage'); + //if ($keyUsage && !in_array('keyCertSign', $keyUsage)) { + // return false; + //} + + /* From RFC5280 "PKIX Certificate and CRL Profile": + + The cA boolean indicates whether the certified public key may be used + to verify certificate signatures. If the cA boolean is not asserted, + then the keyCertSign bit in the key usage extension MUST NOT be + asserted. If the basic constraints extension is not present in a + version 3 certificate, or the extension is present but the cA boolean + is not asserted, then the certified public key MUST NOT be used to + verify certificate signatures. */ + //$basicConstraints = $this->getExtension('id-ce-basicConstraints'); + //if (!$basicConstraints || !$basicConstraints['cA']) { + // return false; + //} + + $this->CAs[] = $cert; + + $this->dn = $olddn; + $this->currentCert = $oldcert; + $this->signatureSubject = $oldsigsubj; + + return true; + } + + /** + * Validate an X.509 certificate against a URL + * + * From RFC2818 "HTTP over TLS": + * + * Matching is performed using the matching rules specified by + * [RFC2459]. If more than one identity of a given type is present in + * the certificate (e.g., more than one dNSName name, a match in any one + * of the set is considered acceptable.) Names may contain the wildcard + * character * which is considered to match any single domain name + * component or component fragment. E.g., *.a.com matches foo.a.com but + * not bar.foo.a.com. f*.com matches foo.com but not bar.com. + * + * @param string $url + * @access public + * @return bool + */ + function validateURL($url) + { + if (!is_array($this->currentCert) || !isset($this->currentCert['tbsCertificate'])) { + return false; + } + + $components = parse_url($url); + if (!isset($components['host'])) { + return false; + } + + if ($names = $this->getExtension('id-ce-subjectAltName')) { + foreach ($names as $name) { + foreach ($name as $key => $value) { + $value = preg_quote($value); + $value = str_replace('\*', '[^.]*', $value); + switch ($key) { + case 'dNSName': + /* From RFC2818 "HTTP over TLS": + + If a subjectAltName extension of type dNSName is present, that MUST + be used as the identity. Otherwise, the (most specific) Common Name + field in the Subject field of the certificate MUST be used. Although + the use of the Common Name is existing practice, it is deprecated and + Certification Authorities are encouraged to use the dNSName instead. */ + if (preg_match('#^' . $value . '$#', $components['host'])) { + return true; + } + break; + case 'iPAddress': + /* From RFC2818 "HTTP over TLS": + + In some cases, the URI is specified as an IP address rather than a + hostname. In this case, the iPAddress subjectAltName must be present + in the certificate and must exactly match the IP in the URI. */ + if (preg_match('#(?:\d{1-3}\.){4}#', $components['host'] . '.') && preg_match('#^' . $value . '$#', $components['host'])) { + return true; + } + } + } + } + return false; + } + + if ($value = $this->getDNProp('id-at-commonName')) { + $value = str_replace(array('.', '*'), array('\.', '[^.]*'), $value[0]); + return preg_match('#^' . $value . '$#', $components['host']); + } + + return false; + } + + /** + * Validate a date + * + * If $date isn't defined it is assumed to be the current date. + * + * @param \DateTime|string $date optional + * @access public + */ + function validateDate($date = null) + { + if (!is_array($this->currentCert) || !isset($this->currentCert['tbsCertificate'])) { + return false; + } + + if (!isset($date)) { + $date = new DateTime(null, new DateTimeZone(@date_default_timezone_get())); + } + + $notBefore = $this->currentCert['tbsCertificate']['validity']['notBefore']; + $notBefore = isset($notBefore['generalTime']) ? $notBefore['generalTime'] : $notBefore['utcTime']; + + $notAfter = $this->currentCert['tbsCertificate']['validity']['notAfter']; + $notAfter = isset($notAfter['generalTime']) ? $notAfter['generalTime'] : $notAfter['utcTime']; + + if (is_string($date)) { + $date = new DateTime($date, new DateTimeZone(@date_default_timezone_get())); + } + + $notBefore = new DateTime($notBefore, new DateTimeZone(@date_default_timezone_get())); + $notAfter = new DateTime($notAfter, new DateTimeZone(@date_default_timezone_get())); + + switch (true) { + case $date < $notBefore: + case $date > $notAfter: + return false; + } + + return true; + } + + /** + * Fetches a URL + * + * @param string $url + * @access private + * @return bool|string + */ + static function _fetchURL($url) + { + if (self::$disable_url_fetch) { + return false; + } + + $parts = parse_url($url); + $data = ''; + switch ($parts['scheme']) { + case 'http': + $fsock = @fsockopen($parts['host'], isset($parts['port']) ? $parts['port'] : 80); + if (!$fsock) { + return false; + } + $path = $parts['path']; + if (isset($parts['query'])) { + $path.= '?' . $parts['query']; + } + fputs($fsock, "GET $path HTTP/1.0\r\n"); + fputs($fsock, "Host: $parts[host]\r\n\r\n"); + $line = fgets($fsock, 1024); + if (strlen($line) < 3) { + return false; + } + preg_match('#HTTP/1.\d (\d{3})#', $line, $temp); + if ($temp[1] != '200') { + return false; + } + + // skip the rest of the headers in the http response + while (!feof($fsock) && fgets($fsock, 1024) != "\r\n") { + } + + while (!feof($fsock)) { + $temp = fread($fsock, 1024); + if ($temp === false) { + return false; + } + $data.= $temp; + } + + break; + //case 'ftp': + //case 'ldap': + //default: + } + + return $data; + } + + /** + * Validates an intermediate cert as identified via authority info access extension + * + * See https://tools.ietf.org/html/rfc4325 for more info + * + * @param bool $caonly + * @param int $count + * @access private + * @return bool + */ + function _testForIntermediate($caonly, $count) + { + $opts = $this->getExtension('id-pe-authorityInfoAccess'); + if (!is_array($opts)) { + return false; + } + foreach ($opts as $opt) { + if ($opt['accessMethod'] == 'id-ad-caIssuers') { + // accessLocation is a GeneralName. GeneralName fields support stuff like email addresses, IP addresses, LDAP, + // etc, but we're only supporting URI's. URI's and LDAP are the only thing https://tools.ietf.org/html/rfc4325 + // discusses + if (isset($opt['accessLocation']['uniformResourceIdentifier'])) { + $url = $opt['accessLocation']['uniformResourceIdentifier']; + break; + } + } + } + + if (!isset($url)) { + return false; + } + + $cert = static::_fetchURL($url); + if (!is_string($cert)) { + return false; + } + + $parent = new static(); + $parent->CAs = $this->CAs; + /* + "Conforming applications that support HTTP or FTP for accessing + certificates MUST be able to accept .cer files and SHOULD be able + to accept .p7c files." -- https://tools.ietf.org/html/rfc4325 + + A .p7c file is 'a "certs-only" CMS message as specified in RFC 2797" + + These are currently unsupported + */ + if (!is_array($parent->loadX509($cert))) { + return false; + } + + if (!$parent->_validateSignatureCountable($caonly, ++$count)) { + return false; + } + + $this->CAs[] = $parent->currentCert; + //$this->loadCA($cert); + + return true; + } + + /** + * Validate a signature + * + * Works on X.509 certs, CSR's and CRL's. + * Returns true if the signature is verified, false if it is not correct or null on error + * + * By default returns false for self-signed certs. Call validateSignature(false) to make this support + * self-signed. + * + * The behavior of this function is inspired by {@link http://php.net/openssl-verify openssl_verify}. + * + * @param bool $caonly optional + * @access public + * @return mixed + */ + function validateSignature($caonly = true) + { + return $this->_validateSignatureCountable($caonly, 0); + } + + /** + * Validate a signature + * + * Performs said validation whilst keeping track of how many times validation method is called + * + * @param bool $caonly + * @param int $count + * @access private + * @return mixed + */ + function _validateSignatureCountable($caonly, $count) + { + if (!is_array($this->currentCert) || !isset($this->signatureSubject)) { + return null; + } + + if ($count == self::$recur_limit) { + return false; + } + + /* TODO: + "emailAddress attribute values are not case-sensitive (e.g., "subscriber@example.com" is the same as "SUBSCRIBER@EXAMPLE.COM")." + -- http://tools.ietf.org/html/rfc5280#section-4.1.2.6 + + implement pathLenConstraint in the id-ce-basicConstraints extension */ + + switch (true) { + case isset($this->currentCert['tbsCertificate']): + // self-signed cert + switch (true) { + case !defined('FILE_X509_IGNORE_TYPE') && $this->currentCert['tbsCertificate']['issuer'] === $this->currentCert['tbsCertificate']['subject']: + case defined('FILE_X509_IGNORE_TYPE') && $this->getIssuerDN(self::DN_STRING) === $this->getDN(self::DN_STRING): + $authorityKey = $this->getExtension('id-ce-authorityKeyIdentifier'); + $subjectKeyID = $this->getExtension('id-ce-subjectKeyIdentifier'); + switch (true) { + case !is_array($authorityKey): + case !$subjectKeyID: + case isset($authorityKey['keyIdentifier']) && $authorityKey['keyIdentifier'] === $subjectKeyID: + $signingCert = $this->currentCert; // working cert + } + } + + if (!empty($this->CAs)) { + for ($i = 0; $i < count($this->CAs); $i++) { + // even if the cert is a self-signed one we still want to see if it's a CA; + // if not, we'll conditionally return an error + $ca = $this->CAs[$i]; + switch (true) { + case !defined('FILE_X509_IGNORE_TYPE') && $this->currentCert['tbsCertificate']['issuer'] === $ca['tbsCertificate']['subject']: + case defined('FILE_X509_IGNORE_TYPE') && $this->getDN(self::DN_STRING, $this->currentCert['tbsCertificate']['issuer']) === $this->getDN(self::DN_STRING, $ca['tbsCertificate']['subject']): + $authorityKey = $this->getExtension('id-ce-authorityKeyIdentifier'); + $subjectKeyID = $this->getExtension('id-ce-subjectKeyIdentifier', $ca); + switch (true) { + case !is_array($authorityKey): + case !$subjectKeyID: + case isset($authorityKey['keyIdentifier']) && $authorityKey['keyIdentifier'] === $subjectKeyID: + if (is_array($authorityKey) && isset($authorityKey['authorityCertSerialNumber']) && !$authorityKey['authorityCertSerialNumber']->equals($ca['tbsCertificate']['serialNumber'])) { + break 2; // serial mismatch - check other ca + } + $signingCert = $ca; // working cert + break 3; + } + } + } + if (count($this->CAs) == $i && $caonly) { + return $this->_testForIntermediate($caonly, $count) && $this->validateSignature($caonly); + } + } elseif (!isset($signingCert) || $caonly) { + return $this->_testForIntermediate($caonly, $count) && $this->validateSignature($caonly); + } + return $this->_validateSignature( + $signingCert['tbsCertificate']['subjectPublicKeyInfo']['algorithm']['algorithm'], + $signingCert['tbsCertificate']['subjectPublicKeyInfo']['subjectPublicKey'], + $this->currentCert['signatureAlgorithm']['algorithm'], + substr(base64_decode($this->currentCert['signature']), 1), + $this->signatureSubject + ); + case isset($this->currentCert['certificationRequestInfo']): + return $this->_validateSignature( + $this->currentCert['certificationRequestInfo']['subjectPKInfo']['algorithm']['algorithm'], + $this->currentCert['certificationRequestInfo']['subjectPKInfo']['subjectPublicKey'], + $this->currentCert['signatureAlgorithm']['algorithm'], + substr(base64_decode($this->currentCert['signature']), 1), + $this->signatureSubject + ); + case isset($this->currentCert['publicKeyAndChallenge']): + return $this->_validateSignature( + $this->currentCert['publicKeyAndChallenge']['spki']['algorithm']['algorithm'], + $this->currentCert['publicKeyAndChallenge']['spki']['subjectPublicKey'], + $this->currentCert['signatureAlgorithm']['algorithm'], + substr(base64_decode($this->currentCert['signature']), 1), + $this->signatureSubject + ); + case isset($this->currentCert['tbsCertList']): + if (!empty($this->CAs)) { + for ($i = 0; $i < count($this->CAs); $i++) { + $ca = $this->CAs[$i]; + switch (true) { + case !defined('FILE_X509_IGNORE_TYPE') && $this->currentCert['tbsCertList']['issuer'] === $ca['tbsCertificate']['subject']: + case defined('FILE_X509_IGNORE_TYPE') && $this->getDN(self::DN_STRING, $this->currentCert['tbsCertList']['issuer']) === $this->getDN(self::DN_STRING, $ca['tbsCertificate']['subject']): + $authorityKey = $this->getExtension('id-ce-authorityKeyIdentifier'); + $subjectKeyID = $this->getExtension('id-ce-subjectKeyIdentifier', $ca); + switch (true) { + case !is_array($authorityKey): + case !$subjectKeyID: + case isset($authorityKey['keyIdentifier']) && $authorityKey['keyIdentifier'] === $subjectKeyID: + if (is_array($authorityKey) && isset($authorityKey['authorityCertSerialNumber']) && !$authorityKey['authorityCertSerialNumber']->equals($ca['tbsCertificate']['serialNumber'])) { + break 2; // serial mismatch - check other ca + } + $signingCert = $ca; // working cert + break 3; + } + } + } + } + if (!isset($signingCert)) { + return false; + } + return $this->_validateSignature( + $signingCert['tbsCertificate']['subjectPublicKeyInfo']['algorithm']['algorithm'], + $signingCert['tbsCertificate']['subjectPublicKeyInfo']['subjectPublicKey'], + $this->currentCert['signatureAlgorithm']['algorithm'], + substr(base64_decode($this->currentCert['signature']), 1), + $this->signatureSubject + ); + default: + return false; + } + } + + /** + * Validates a signature + * + * Returns true if the signature is verified, false if it is not correct or null on error + * + * @param string $publicKeyAlgorithm + * @param string $publicKey + * @param string $signatureAlgorithm + * @param string $signature + * @param string $signatureSubject + * @access private + * @return int + */ + function _validateSignature($publicKeyAlgorithm, $publicKey, $signatureAlgorithm, $signature, $signatureSubject) + { + switch ($publicKeyAlgorithm) { + case 'rsaEncryption': + $rsa = new RSA(); + $rsa->loadKey($publicKey); + + switch ($signatureAlgorithm) { + case 'md2WithRSAEncryption': + case 'md5WithRSAEncryption': + case 'sha1WithRSAEncryption': + case 'sha224WithRSAEncryption': + case 'sha256WithRSAEncryption': + case 'sha384WithRSAEncryption': + case 'sha512WithRSAEncryption': + $rsa->setHash(preg_replace('#WithRSAEncryption$#', '', $signatureAlgorithm)); + $rsa->setSignatureMode(RSA::SIGNATURE_PKCS1); + if (!@$rsa->verify($signatureSubject, $signature)) { + return false; + } + break; + default: + return null; + } + break; + default: + return null; + } + + return true; + } + + /** + * Sets the recursion limit + * + * When validating a signature it may be necessary to download intermediate certs from URI's. + * An intermediate cert that linked to itself would result in an infinite loop so to prevent + * that we set a recursion limit. A negative number means that there is no recursion limit. + * + * @param int $count + * @access public + */ + static function setRecurLimit($count) + { + self::$recur_limit = $count; + } + + /** + * Prevents URIs from being automatically retrieved + * + * @access public + */ + static function disableURLFetch() + { + self::$disable_url_fetch = true; + } + + /** + * Allows URIs to be automatically retrieved + * + * @access public + */ + static function enableURLFetch() + { + self::$disable_url_fetch = false; + } + + /** + * Reformat public keys + * + * Reformats a public key to a format supported by phpseclib (if applicable) + * + * @param string $algorithm + * @param string $key + * @access private + * @return string + */ + function _reformatKey($algorithm, $key) + { + switch ($algorithm) { + case 'rsaEncryption': + return + "-----BEGIN RSA PUBLIC KEY-----\r\n" . + // subjectPublicKey is stored as a bit string in X.509 certs. the first byte of a bit string represents how many bits + // in the last byte should be ignored. the following only supports non-zero stuff but as none of the X.509 certs Firefox + // uses as a cert authority actually use a non-zero bit I think it's safe to assume that none do. + chunk_split(base64_encode(substr(base64_decode($key), 1)), 64) . + '-----END RSA PUBLIC KEY-----'; + default: + return $key; + } + } + + /** + * Decodes an IP address + * + * Takes in a base64 encoded "blob" and returns a human readable IP address + * + * @param string $ip + * @access private + * @return string + */ + function _decodeIP($ip) + { + return inet_ntop(base64_decode($ip)); + } + + /** + * Decodes an IP address in a name constraints extension + * + * Takes in a base64 encoded "blob" and returns a human readable IP address / mask + * + * @param string $ip + * @access private + * @return array + */ + function _decodeNameConstraintIP($ip) + { + $ip = base64_decode($ip); + $size = strlen($ip) >> 1; + $mask = substr($ip, $size); + $ip = substr($ip, 0, $size); + return array(inet_ntop($ip), inet_ntop($mask)); + } + + /** + * Encodes an IP address + * + * Takes a human readable IP address into a base64-encoded "blob" + * + * @param string|array $ip + * @access private + * @return string + */ + function _encodeIP($ip) + { + return is_string($ip) ? + base64_encode(inet_pton($ip)) : + base64_encode(inet_pton($ip[0]) . inet_pton($ip[1])); + } + + /** + * "Normalizes" a Distinguished Name property + * + * @param string $propName + * @access private + * @return mixed + */ + function _translateDNProp($propName) + { + switch (strtolower($propName)) { + case 'jurisdictionofincorporationcountryname': + case 'jurisdictioncountryname': + case 'jurisdictionc': + return 'jurisdictionOfIncorporationCountryName'; + case 'jurisdictionofincorporationstateorprovincename': + case 'jurisdictionstateorprovincename': + case 'jurisdictionst': + return 'jurisdictionOfIncorporationStateOrProvinceName'; + case 'jurisdictionlocalityname': + case 'jurisdictionl': + return 'jurisdictionLocalityName'; + case 'id-at-businesscategory': + case 'businesscategory': + return 'id-at-businessCategory'; + case 'id-at-countryname': + case 'countryname': + case 'c': + return 'id-at-countryName'; + case 'id-at-organizationname': + case 'organizationname': + case 'o': + return 'id-at-organizationName'; + case 'id-at-dnqualifier': + case 'dnqualifier': + return 'id-at-dnQualifier'; + case 'id-at-commonname': + case 'commonname': + case 'cn': + return 'id-at-commonName'; + case 'id-at-stateorprovincename': + case 'stateorprovincename': + case 'state': + case 'province': + case 'provincename': + case 'st': + return 'id-at-stateOrProvinceName'; + case 'id-at-localityname': + case 'localityname': + case 'l': + return 'id-at-localityName'; + case 'id-emailaddress': + case 'emailaddress': + return 'pkcs-9-at-emailAddress'; + case 'id-at-serialnumber': + case 'serialnumber': + return 'id-at-serialNumber'; + case 'id-at-postalcode': + case 'postalcode': + return 'id-at-postalCode'; + case 'id-at-streetaddress': + case 'streetaddress': + return 'id-at-streetAddress'; + case 'id-at-name': + case 'name': + return 'id-at-name'; + case 'id-at-givenname': + case 'givenname': + return 'id-at-givenName'; + case 'id-at-surname': + case 'surname': + case 'sn': + return 'id-at-surname'; + case 'id-at-initials': + case 'initials': + return 'id-at-initials'; + case 'id-at-generationqualifier': + case 'generationqualifier': + return 'id-at-generationQualifier'; + case 'id-at-organizationalunitname': + case 'organizationalunitname': + case 'ou': + return 'id-at-organizationalUnitName'; + case 'id-at-pseudonym': + case 'pseudonym': + return 'id-at-pseudonym'; + case 'id-at-title': + case 'title': + return 'id-at-title'; + case 'id-at-description': + case 'description': + return 'id-at-description'; + case 'id-at-role': + case 'role': + return 'id-at-role'; + case 'id-at-uniqueidentifier': + case 'uniqueidentifier': + case 'x500uniqueidentifier': + return 'id-at-uniqueIdentifier'; + case 'postaladdress': + case 'id-at-postaladdress': + return 'id-at-postalAddress'; + default: + return false; + } + } + + /** + * Set a Distinguished Name property + * + * @param string $propName + * @param mixed $propValue + * @param string $type optional + * @access public + * @return bool + */ + function setDNProp($propName, $propValue, $type = 'utf8String') + { + if (empty($this->dn)) { + $this->dn = array('rdnSequence' => array()); + } + + if (($propName = $this->_translateDNProp($propName)) === false) { + return false; + } + + foreach ((array) $propValue as $v) { + if (!is_array($v) && isset($type)) { + $v = array($type => $v); + } + $this->dn['rdnSequence'][] = array( + array( + 'type' => $propName, + 'value'=> $v + ) + ); + } + + return true; + } + + /** + * Remove Distinguished Name properties + * + * @param string $propName + * @access public + */ + function removeDNProp($propName) + { + if (empty($this->dn)) { + return; + } + + if (($propName = $this->_translateDNProp($propName)) === false) { + return; + } + + $dn = &$this->dn['rdnSequence']; + $size = count($dn); + for ($i = 0; $i < $size; $i++) { + if ($dn[$i][0]['type'] == $propName) { + unset($dn[$i]); + } + } + + $dn = array_values($dn); + // fix for https://bugs.php.net/75433 affecting PHP 7.2 + if (!isset($dn[0])) { + $dn = array_splice($dn, 0, 0); + } + } + + /** + * Get Distinguished Name properties + * + * @param string $propName + * @param array $dn optional + * @param bool $withType optional + * @return mixed + * @access public + */ + function getDNProp($propName, $dn = null, $withType = false) + { + if (!isset($dn)) { + $dn = $this->dn; + } + + if (empty($dn)) { + return false; + } + + if (($propName = $this->_translateDNProp($propName)) === false) { + return false; + } + + $asn1 = new ASN1(); + $asn1->loadOIDs($this->oids); + $filters = array(); + $filters['value'] = array('type' => ASN1::TYPE_UTF8_STRING); + $asn1->loadFilters($filters); + $this->_mapOutDNs($dn, 'rdnSequence', $asn1); + $dn = $dn['rdnSequence']; + $result = array(); + for ($i = 0; $i < count($dn); $i++) { + if ($dn[$i][0]['type'] == $propName) { + $v = $dn[$i][0]['value']; + if (!$withType) { + if (is_array($v)) { + foreach ($v as $type => $s) { + $type = array_search($type, $asn1->ANYmap, true); + if ($type !== false && isset($asn1->stringTypeSize[$type])) { + $s = $asn1->convert($s, $type); + if ($s !== false) { + $v = $s; + break; + } + } + } + if (is_array($v)) { + $v = array_pop($v); // Always strip data type. + } + } elseif (is_object($v) && $v instanceof Element) { + $map = $this->_getMapping($propName); + if (!is_bool($map)) { + $decoded = $asn1->decodeBER($v); + $v = $asn1->asn1map($decoded[0], $map); + } + } + } + $result[] = $v; + } + } + + return $result; + } + + /** + * Set a Distinguished Name + * + * @param mixed $dn + * @param bool $merge optional + * @param string $type optional + * @access public + * @return bool + */ + function setDN($dn, $merge = false, $type = 'utf8String') + { + if (!$merge) { + $this->dn = null; + } + + if (is_array($dn)) { + if (isset($dn['rdnSequence'])) { + $this->dn = $dn; // No merge here. + return true; + } + + // handles stuff generated by openssl_x509_parse() + foreach ($dn as $prop => $value) { + if (!$this->setDNProp($prop, $value, $type)) { + return false; + } + } + return true; + } + + // handles everything else + $results = preg_split('#((?:^|, *|/)(?:C=|O=|OU=|CN=|L=|ST=|SN=|postalCode=|streetAddress=|emailAddress=|serialNumber=|organizationalUnitName=|title=|description=|role=|x500UniqueIdentifier=|postalAddress=))#', $dn, -1, PREG_SPLIT_DELIM_CAPTURE); + for ($i = 1; $i < count($results); $i+=2) { + $prop = trim($results[$i], ', =/'); + $value = $results[$i + 1]; + if (!$this->setDNProp($prop, $value, $type)) { + return false; + } + } + + return true; + } + + /** + * Get the Distinguished Name for a certificates subject + * + * @param mixed $format optional + * @param array $dn optional + * @access public + * @return bool + */ + function getDN($format = self::DN_ARRAY, $dn = null) + { + if (!isset($dn)) { + $dn = isset($this->currentCert['tbsCertList']) ? $this->currentCert['tbsCertList']['issuer'] : $this->dn; + } + + switch ((int) $format) { + case self::DN_ARRAY: + return $dn; + case self::DN_ASN1: + $asn1 = new ASN1(); + $asn1->loadOIDs($this->oids); + $filters = array(); + $filters['rdnSequence']['value'] = array('type' => ASN1::TYPE_UTF8_STRING); + $asn1->loadFilters($filters); + $this->_mapOutDNs($dn, 'rdnSequence', $asn1); + return $asn1->encodeDER($dn, $this->Name); + case self::DN_CANON: + // No SEQUENCE around RDNs and all string values normalized as + // trimmed lowercase UTF-8 with all spacing as one blank. + // constructed RDNs will not be canonicalized + $asn1 = new ASN1(); + $asn1->loadOIDs($this->oids); + $filters = array(); + $filters['value'] = array('type' => ASN1::TYPE_UTF8_STRING); + $asn1->loadFilters($filters); + $result = ''; + $this->_mapOutDNs($dn, 'rdnSequence', $asn1); + foreach ($dn['rdnSequence'] as $rdn) { + foreach ($rdn as $i => $attr) { + $attr = &$rdn[$i]; + if (is_array($attr['value'])) { + foreach ($attr['value'] as $type => $v) { + $type = array_search($type, $asn1->ANYmap, true); + if ($type !== false && isset($asn1->stringTypeSize[$type])) { + $v = $asn1->convert($v, $type); + if ($v !== false) { + $v = preg_replace('/\s+/', ' ', $v); + $attr['value'] = strtolower(trim($v)); + break; + } + } + } + } + } + $result .= $asn1->encodeDER($rdn, $this->RelativeDistinguishedName); + } + return $result; + case self::DN_HASH: + $dn = $this->getDN(self::DN_CANON, $dn); + $hash = new Hash('sha1'); + $hash = $hash->hash($dn); + extract(unpack('Vhash', $hash)); + return strtolower(bin2hex(pack('N', $hash))); + } + + // Default is to return a string. + $start = true; + $output = ''; + + $result = array(); + $asn1 = new ASN1(); + $asn1->loadOIDs($this->oids); + $filters = array(); + $filters['rdnSequence']['value'] = array('type' => ASN1::TYPE_UTF8_STRING); + $asn1->loadFilters($filters); + $this->_mapOutDNs($dn, 'rdnSequence', $asn1); + + foreach ($dn['rdnSequence'] as $field) { + $prop = $field[0]['type']; + $value = $field[0]['value']; + + $delim = ', '; + switch ($prop) { + case 'id-at-countryName': + $desc = 'C'; + break; + case 'id-at-stateOrProvinceName': + $desc = 'ST'; + break; + case 'id-at-organizationName': + $desc = 'O'; + break; + case 'id-at-organizationalUnitName': + $desc = 'OU'; + break; + case 'id-at-commonName': + $desc = 'CN'; + break; + case 'id-at-localityName': + $desc = 'L'; + break; + case 'id-at-surname': + $desc = 'SN'; + break; + case 'id-at-uniqueIdentifier': + $delim = '/'; + $desc = 'x500UniqueIdentifier'; + break; + case 'id-at-postalAddress': + $delim = '/'; + $desc = 'postalAddress'; + break; + default: + $delim = '/'; + $desc = preg_replace('#.+-([^-]+)$#', '$1', $prop); + } + + if (!$start) { + $output.= $delim; + } + if (is_array($value)) { + foreach ($value as $type => $v) { + $type = array_search($type, $asn1->ANYmap, true); + if ($type !== false && isset($asn1->stringTypeSize[$type])) { + $v = $asn1->convert($v, $type); + if ($v !== false) { + $value = $v; + break; + } + } + } + if (is_array($value)) { + $value = array_pop($value); // Always strip data type. + } + } elseif (is_object($value) && $value instanceof Element) { + $callback = function ($x) { + return "\x" . bin2hex($x[0]); + }; + $value = strtoupper(preg_replace_callback('#[^\x20-\x7E]#', $callback, $value->element)); + } + $output.= $desc . '=' . $value; + $result[$desc] = isset($result[$desc]) ? + array_merge((array) $result[$desc], array($value)) : + $value; + $start = false; + } + + return $format == self::DN_OPENSSL ? $result : $output; + } + + /** + * Get the Distinguished Name for a certificate/crl issuer + * + * @param int $format optional + * @access public + * @return mixed + */ + function getIssuerDN($format = self::DN_ARRAY) + { + switch (true) { + case !isset($this->currentCert) || !is_array($this->currentCert): + break; + case isset($this->currentCert['tbsCertificate']): + return $this->getDN($format, $this->currentCert['tbsCertificate']['issuer']); + case isset($this->currentCert['tbsCertList']): + return $this->getDN($format, $this->currentCert['tbsCertList']['issuer']); + } + + return false; + } + + /** + * Get the Distinguished Name for a certificate/csr subject + * Alias of getDN() + * + * @param int $format optional + * @access public + * @return mixed + */ + function getSubjectDN($format = self::DN_ARRAY) + { + switch (true) { + case !empty($this->dn): + return $this->getDN($format); + case !isset($this->currentCert) || !is_array($this->currentCert): + break; + case isset($this->currentCert['tbsCertificate']): + return $this->getDN($format, $this->currentCert['tbsCertificate']['subject']); + case isset($this->currentCert['certificationRequestInfo']): + return $this->getDN($format, $this->currentCert['certificationRequestInfo']['subject']); + } + + return false; + } + + /** + * Get an individual Distinguished Name property for a certificate/crl issuer + * + * @param string $propName + * @param bool $withType optional + * @access public + * @return mixed + */ + function getIssuerDNProp($propName, $withType = false) + { + switch (true) { + case !isset($this->currentCert) || !is_array($this->currentCert): + break; + case isset($this->currentCert['tbsCertificate']): + return $this->getDNProp($propName, $this->currentCert['tbsCertificate']['issuer'], $withType); + case isset($this->currentCert['tbsCertList']): + return $this->getDNProp($propName, $this->currentCert['tbsCertList']['issuer'], $withType); + } + + return false; + } + + /** + * Get an individual Distinguished Name property for a certificate/csr subject + * + * @param string $propName + * @param bool $withType optional + * @access public + * @return mixed + */ + function getSubjectDNProp($propName, $withType = false) + { + switch (true) { + case !empty($this->dn): + return $this->getDNProp($propName, null, $withType); + case !isset($this->currentCert) || !is_array($this->currentCert): + break; + case isset($this->currentCert['tbsCertificate']): + return $this->getDNProp($propName, $this->currentCert['tbsCertificate']['subject'], $withType); + case isset($this->currentCert['certificationRequestInfo']): + return $this->getDNProp($propName, $this->currentCert['certificationRequestInfo']['subject'], $withType); + } + + return false; + } + + /** + * Get the certificate chain for the current cert + * + * @access public + * @return mixed + */ + function getChain() + { + $chain = array($this->currentCert); + + if (!is_array($this->currentCert) || !isset($this->currentCert['tbsCertificate'])) { + return false; + } + if (empty($this->CAs)) { + return $chain; + } + while (true) { + $currentCert = $chain[count($chain) - 1]; + for ($i = 0; $i < count($this->CAs); $i++) { + $ca = $this->CAs[$i]; + if ($currentCert['tbsCertificate']['issuer'] === $ca['tbsCertificate']['subject']) { + $authorityKey = $this->getExtension('id-ce-authorityKeyIdentifier', $currentCert); + $subjectKeyID = $this->getExtension('id-ce-subjectKeyIdentifier', $ca); + switch (true) { + case !is_array($authorityKey): + case is_array($authorityKey) && isset($authorityKey['keyIdentifier']) && $authorityKey['keyIdentifier'] === $subjectKeyID: + if ($currentCert === $ca) { + break 3; + } + $chain[] = $ca; + break 2; + } + } + } + if ($i == count($this->CAs)) { + break; + } + } + foreach ($chain as $key => $value) { + $chain[$key] = new X509(); + $chain[$key]->loadX509($value); + } + return $chain; + } + + /** + * Set public key + * + * Key needs to be a \phpseclib\Crypt\RSA object + * + * @param object $key + * @access public + * @return bool + */ + function setPublicKey($key) + { + $key->setPublicKey(); + $this->publicKey = $key; + } + + /** + * Set private key + * + * Key needs to be a \phpseclib\Crypt\RSA object + * + * @param object $key + * @access public + */ + function setPrivateKey($key) + { + $this->privateKey = $key; + } + + /** + * Set challenge + * + * Used for SPKAC CSR's + * + * @param string $challenge + * @access public + */ + function setChallenge($challenge) + { + $this->challenge = $challenge; + } + + /** + * Gets the public key + * + * Returns a \phpseclib\Crypt\RSA object or a false. + * + * @access public + * @return mixed + */ + function getPublicKey() + { + if (isset($this->publicKey)) { + return $this->publicKey; + } + + if (isset($this->currentCert) && is_array($this->currentCert)) { + foreach (array('tbsCertificate/subjectPublicKeyInfo', 'certificationRequestInfo/subjectPKInfo') as $path) { + $keyinfo = $this->_subArray($this->currentCert, $path); + if (!empty($keyinfo)) { + break; + } + } + } + if (empty($keyinfo)) { + return false; + } + + $key = $keyinfo['subjectPublicKey']; + + switch ($keyinfo['algorithm']['algorithm']) { + case 'rsaEncryption': + $publicKey = new RSA(); + $publicKey->loadKey($key); + $publicKey->setPublicKey(); + break; + default: + return false; + } + + return $publicKey; + } + + /** + * Load a Certificate Signing Request + * + * @param string|array $csr + * @param int $mode + * @access public + * @return mixed + */ + function loadCSR($csr, $mode = self::FORMAT_AUTO_DETECT) + { + if (is_array($csr) && isset($csr['certificationRequestInfo'])) { + unset($this->currentCert); + unset($this->currentKeyIdentifier); + unset($this->signatureSubject); + $this->dn = $csr['certificationRequestInfo']['subject']; + if (!isset($this->dn)) { + return false; + } + + $this->currentCert = $csr; + return $csr; + } + + // see http://tools.ietf.org/html/rfc2986 + + $asn1 = new ASN1(); + + if ($mode != self::FORMAT_DER) { + $newcsr = $this->_extractBER($csr); + if ($mode == self::FORMAT_PEM && $csr == $newcsr) { + return false; + } + $csr = $newcsr; + } + $orig = $csr; + + if ($csr === false) { + $this->currentCert = false; + return false; + } + + $asn1->loadOIDs($this->oids); + $decoded = $asn1->decodeBER($csr); + + if (empty($decoded)) { + $this->currentCert = false; + return false; + } + + $csr = $asn1->asn1map($decoded[0], $this->CertificationRequest); + if (!isset($csr) || $csr === false) { + $this->currentCert = false; + return false; + } + + $this->_mapInAttributes($csr, 'certificationRequestInfo/attributes', $asn1); + $this->_mapInDNs($csr, 'certificationRequestInfo/subject/rdnSequence', $asn1); + + $this->dn = $csr['certificationRequestInfo']['subject']; + + $this->signatureSubject = substr($orig, $decoded[0]['content'][0]['start'], $decoded[0]['content'][0]['length']); + + $algorithm = &$csr['certificationRequestInfo']['subjectPKInfo']['algorithm']['algorithm']; + $key = &$csr['certificationRequestInfo']['subjectPKInfo']['subjectPublicKey']; + $key = $this->_reformatKey($algorithm, $key); + + switch ($algorithm) { + case 'rsaEncryption': + $this->publicKey = new RSA(); + $this->publicKey->loadKey($key); + $this->publicKey->setPublicKey(); + break; + default: + $this->publicKey = null; + } + + $this->currentKeyIdentifier = null; + $this->currentCert = $csr; + + return $csr; + } + + /** + * Save CSR request + * + * @param array $csr + * @param int $format optional + * @access public + * @return string + */ + function saveCSR($csr, $format = self::FORMAT_PEM) + { + if (!is_array($csr) || !isset($csr['certificationRequestInfo'])) { + return false; + } + + switch (true) { + case !($algorithm = $this->_subArray($csr, 'certificationRequestInfo/subjectPKInfo/algorithm/algorithm')): + case is_object($csr['certificationRequestInfo']['subjectPKInfo']['subjectPublicKey']): + break; + default: + switch ($algorithm) { + case 'rsaEncryption': + $csr['certificationRequestInfo']['subjectPKInfo']['subjectPublicKey'] + = base64_encode("\0" . base64_decode(preg_replace('#-.+-|[\r\n]#', '', $csr['certificationRequestInfo']['subjectPKInfo']['subjectPublicKey']))); + $csr['certificationRequestInfo']['subjectPKInfo']['algorithm']['parameters'] = null; + $csr['signatureAlgorithm']['parameters'] = null; + $csr['certificationRequestInfo']['signature']['parameters'] = null; + } + } + + $asn1 = new ASN1(); + + $asn1->loadOIDs($this->oids); + + $filters = array(); + $filters['certificationRequestInfo']['subject']['rdnSequence']['value'] + = array('type' => ASN1::TYPE_UTF8_STRING); + + $asn1->loadFilters($filters); + + $this->_mapOutDNs($csr, 'certificationRequestInfo/subject/rdnSequence', $asn1); + $this->_mapOutAttributes($csr, 'certificationRequestInfo/attributes', $asn1); + $csr = $asn1->encodeDER($csr, $this->CertificationRequest); + + switch ($format) { + case self::FORMAT_DER: + return $csr; + // case self::FORMAT_PEM: + default: + return "-----BEGIN CERTIFICATE REQUEST-----\r\n" . chunk_split(base64_encode($csr), 64) . '-----END CERTIFICATE REQUEST-----'; + } + } + + /** + * Load a SPKAC CSR + * + * SPKAC's are produced by the HTML5 keygen element: + * + * https://developer.mozilla.org/en-US/docs/HTML/Element/keygen + * + * @param string|array $spkac + * @access public + * @return mixed + */ + function loadSPKAC($spkac) + { + if (is_array($spkac) && isset($spkac['publicKeyAndChallenge'])) { + unset($this->currentCert); + unset($this->currentKeyIdentifier); + unset($this->signatureSubject); + $this->currentCert = $spkac; + return $spkac; + } + + // see http://www.w3.org/html/wg/drafts/html/master/forms.html#signedpublickeyandchallenge + + $asn1 = new ASN1(); + + // OpenSSL produces SPKAC's that are preceded by the string SPKAC= + $temp = preg_replace('#(?:SPKAC=)|[ \r\n\\\]#', '', $spkac); + $temp = preg_match('#^[a-zA-Z\d/+]*={0,2}$#', $temp) ? base64_decode($temp) : false; + if ($temp != false) { + $spkac = $temp; + } + $orig = $spkac; + + if ($spkac === false) { + $this->currentCert = false; + return false; + } + + $asn1->loadOIDs($this->oids); + $decoded = $asn1->decodeBER($spkac); + + if (empty($decoded)) { + $this->currentCert = false; + return false; + } + + $spkac = $asn1->asn1map($decoded[0], $this->SignedPublicKeyAndChallenge); + + if (!isset($spkac) || $spkac === false) { + $this->currentCert = false; + return false; + } + + $this->signatureSubject = substr($orig, $decoded[0]['content'][0]['start'], $decoded[0]['content'][0]['length']); + + $algorithm = &$spkac['publicKeyAndChallenge']['spki']['algorithm']['algorithm']; + $key = &$spkac['publicKeyAndChallenge']['spki']['subjectPublicKey']; + $key = $this->_reformatKey($algorithm, $key); + + switch ($algorithm) { + case 'rsaEncryption': + $this->publicKey = new RSA(); + $this->publicKey->loadKey($key); + $this->publicKey->setPublicKey(); + break; + default: + $this->publicKey = null; + } + + $this->currentKeyIdentifier = null; + $this->currentCert = $spkac; + + return $spkac; + } + + /** + * Save a SPKAC CSR request + * + * @param string|array $spkac + * @param int $format optional + * @access public + * @return string + */ + function saveSPKAC($spkac, $format = self::FORMAT_PEM) + { + if (!is_array($spkac) || !isset($spkac['publicKeyAndChallenge'])) { + return false; + } + + $algorithm = $this->_subArray($spkac, 'publicKeyAndChallenge/spki/algorithm/algorithm'); + switch (true) { + case !$algorithm: + case is_object($spkac['publicKeyAndChallenge']['spki']['subjectPublicKey']): + break; + default: + switch ($algorithm) { + case 'rsaEncryption': + $spkac['publicKeyAndChallenge']['spki']['subjectPublicKey'] + = base64_encode("\0" . base64_decode(preg_replace('#-.+-|[\r\n]#', '', $spkac['publicKeyAndChallenge']['spki']['subjectPublicKey']))); + } + } + + $asn1 = new ASN1(); + + $asn1->loadOIDs($this->oids); + $spkac = $asn1->encodeDER($spkac, $this->SignedPublicKeyAndChallenge); + + switch ($format) { + case self::FORMAT_DER: + return $spkac; + // case self::FORMAT_PEM: + default: + // OpenSSL's implementation of SPKAC requires the SPKAC be preceded by SPKAC= and since there are pretty much + // no other SPKAC decoders phpseclib will use that same format + return 'SPKAC=' . base64_encode($spkac); + } + } + + /** + * Load a Certificate Revocation List + * + * @param string $crl + * @param int $mode + * @access public + * @return mixed + */ + function loadCRL($crl, $mode = self::FORMAT_AUTO_DETECT) + { + if (is_array($crl) && isset($crl['tbsCertList'])) { + $this->currentCert = $crl; + unset($this->signatureSubject); + return $crl; + } + + $asn1 = new ASN1(); + + if ($mode != self::FORMAT_DER) { + $newcrl = $this->_extractBER($crl); + if ($mode == self::FORMAT_PEM && $crl == $newcrl) { + return false; + } + $crl = $newcrl; + } + $orig = $crl; + + if ($crl === false) { + $this->currentCert = false; + return false; + } + + $asn1->loadOIDs($this->oids); + $decoded = $asn1->decodeBER($crl); + + if (empty($decoded)) { + $this->currentCert = false; + return false; + } + + $crl = $asn1->asn1map($decoded[0], $this->CertificateList); + if (!isset($crl) || $crl === false) { + $this->currentCert = false; + return false; + } + + $this->signatureSubject = substr($orig, $decoded[0]['content'][0]['start'], $decoded[0]['content'][0]['length']); + + $this->_mapInDNs($crl, 'tbsCertList/issuer/rdnSequence', $asn1); + if ($this->_isSubArrayValid($crl, 'tbsCertList/crlExtensions')) { + $this->_mapInExtensions($crl, 'tbsCertList/crlExtensions', $asn1); + } + if ($this->_isSubArrayValid($crl, 'tbsCertList/revokedCertificates')) { + $rclist_ref = &$this->_subArrayUnchecked($crl, 'tbsCertList/revokedCertificates'); + if ($rclist_ref) { + $rclist = $crl['tbsCertList']['revokedCertificates']; + foreach ($rclist as $i => $extension) { + if ($this->_isSubArrayValid($rclist, "$i/crlEntryExtensions", $asn1)) { + $this->_mapInExtensions($rclist_ref, "$i/crlEntryExtensions", $asn1); + } + } + } + } + + $this->currentKeyIdentifier = null; + $this->currentCert = $crl; + + return $crl; + } + + /** + * Save Certificate Revocation List. + * + * @param array $crl + * @param int $format optional + * @access public + * @return string + */ + function saveCRL($crl, $format = self::FORMAT_PEM) + { + if (!is_array($crl) || !isset($crl['tbsCertList'])) { + return false; + } + + $asn1 = new ASN1(); + + $asn1->loadOIDs($this->oids); + + $filters = array(); + $filters['tbsCertList']['issuer']['rdnSequence']['value'] + = array('type' => ASN1::TYPE_UTF8_STRING); + $filters['tbsCertList']['signature']['parameters'] + = array('type' => ASN1::TYPE_UTF8_STRING); + $filters['signatureAlgorithm']['parameters'] + = array('type' => ASN1::TYPE_UTF8_STRING); + + if (empty($crl['tbsCertList']['signature']['parameters'])) { + $filters['tbsCertList']['signature']['parameters'] + = array('type' => ASN1::TYPE_NULL); + } + + if (empty($crl['signatureAlgorithm']['parameters'])) { + $filters['signatureAlgorithm']['parameters'] + = array('type' => ASN1::TYPE_NULL); + } + + $asn1->loadFilters($filters); + + $this->_mapOutDNs($crl, 'tbsCertList/issuer/rdnSequence', $asn1); + $this->_mapOutExtensions($crl, 'tbsCertList/crlExtensions', $asn1); + $rclist = &$this->_subArray($crl, 'tbsCertList/revokedCertificates'); + if (is_array($rclist)) { + foreach ($rclist as $i => $extension) { + $this->_mapOutExtensions($rclist, "$i/crlEntryExtensions", $asn1); + } + } + + $crl = $asn1->encodeDER($crl, $this->CertificateList); + + switch ($format) { + case self::FORMAT_DER: + return $crl; + // case self::FORMAT_PEM: + default: + return "-----BEGIN X509 CRL-----\r\n" . chunk_split(base64_encode($crl), 64) . '-----END X509 CRL-----'; + } + } + + /** + * Helper function to build a time field according to RFC 3280 section + * - 4.1.2.5 Validity + * - 5.1.2.4 This Update + * - 5.1.2.5 Next Update + * - 5.1.2.6 Revoked Certificates + * by choosing utcTime iff year of date given is before 2050 and generalTime else. + * + * @param string $date in format date('D, d M Y H:i:s O') + * @access private + * @return array + */ + function _timeField($date) + { + if ($date instanceof Element) { + return $date; + } + $dateObj = new DateTime($date, new DateTimeZone('GMT')); + $year = $dateObj->format('Y'); // the same way ASN1.php parses this + if ($year < 2050) { + return array('utcTime' => $date); + } else { + return array('generalTime' => $date); + } + } + + /** + * Sign an X.509 certificate + * + * $issuer's private key needs to be loaded. + * $subject can be either an existing X.509 cert (if you want to resign it), + * a CSR or something with the DN and public key explicitly set. + * + * @param \phpseclib\File\X509 $issuer + * @param \phpseclib\File\X509 $subject + * @param string $signatureAlgorithm optional + * @access public + * @return mixed + */ + function sign($issuer, $subject, $signatureAlgorithm = 'sha1WithRSAEncryption') + { + if (!is_object($issuer->privateKey) || empty($issuer->dn)) { + return false; + } + + if (isset($subject->publicKey) && !($subjectPublicKey = $subject->_formatSubjectPublicKey())) { + return false; + } + + $currentCert = isset($this->currentCert) ? $this->currentCert : null; + $signatureSubject = isset($this->signatureSubject) ? $this->signatureSubject: null; + + if (isset($subject->currentCert) && is_array($subject->currentCert) && isset($subject->currentCert['tbsCertificate'])) { + $this->currentCert = $subject->currentCert; + $this->currentCert['tbsCertificate']['signature']['algorithm'] = $signatureAlgorithm; + $this->currentCert['signatureAlgorithm']['algorithm'] = $signatureAlgorithm; + + if (!empty($this->startDate)) { + $this->currentCert['tbsCertificate']['validity']['notBefore'] = $this->_timeField($this->startDate); + } + if (!empty($this->endDate)) { + $this->currentCert['tbsCertificate']['validity']['notAfter'] = $this->_timeField($this->endDate); + } + if (!empty($this->serialNumber)) { + $this->currentCert['tbsCertificate']['serialNumber'] = $this->serialNumber; + } + if (!empty($subject->dn)) { + $this->currentCert['tbsCertificate']['subject'] = $subject->dn; + } + if (!empty($subject->publicKey)) { + $this->currentCert['tbsCertificate']['subjectPublicKeyInfo'] = $subjectPublicKey; + } + $this->removeExtension('id-ce-authorityKeyIdentifier'); + if (isset($subject->domains)) { + $this->removeExtension('id-ce-subjectAltName'); + } + } elseif (isset($subject->currentCert) && is_array($subject->currentCert) && isset($subject->currentCert['tbsCertList'])) { + return false; + } else { + if (!isset($subject->publicKey)) { + return false; + } + + $startDate = new DateTime('now', new DateTimeZone(@date_default_timezone_get())); + $startDate = !empty($this->startDate) ? $this->startDate : $startDate->format('D, d M Y H:i:s O'); + + $endDate = new DateTime('+1 year', new DateTimeZone(@date_default_timezone_get())); + $endDate = !empty($this->endDate) ? $this->endDate : $endDate->format('D, d M Y H:i:s O'); + + /* "The serial number MUST be a positive integer" + "Conforming CAs MUST NOT use serialNumber values longer than 20 octets." + -- https://tools.ietf.org/html/rfc5280#section-4.1.2.2 + + for the integer to be positive the leading bit needs to be 0 hence the + application of a bitmap + */ + $serialNumber = !empty($this->serialNumber) ? + $this->serialNumber : + new BigInteger(Random::string(20) & ("\x7F" . str_repeat("\xFF", 19)), 256); + + $this->currentCert = array( + 'tbsCertificate' => + array( + 'version' => 'v3', + 'serialNumber' => $serialNumber, // $this->setSerialNumber() + 'signature' => array('algorithm' => $signatureAlgorithm), + 'issuer' => false, // this is going to be overwritten later + 'validity' => array( + 'notBefore' => $this->_timeField($startDate), // $this->setStartDate() + 'notAfter' => $this->_timeField($endDate) // $this->setEndDate() + ), + 'subject' => $subject->dn, + 'subjectPublicKeyInfo' => $subjectPublicKey + ), + 'signatureAlgorithm' => array('algorithm' => $signatureAlgorithm), + 'signature' => false // this is going to be overwritten later + ); + + // Copy extensions from CSR. + $csrexts = $subject->getAttribute('pkcs-9-at-extensionRequest', 0); + + if (!empty($csrexts)) { + $this->currentCert['tbsCertificate']['extensions'] = $csrexts; + } + } + + $this->currentCert['tbsCertificate']['issuer'] = $issuer->dn; + + if (isset($issuer->currentKeyIdentifier)) { + $this->setExtension('id-ce-authorityKeyIdentifier', array( + //'authorityCertIssuer' => array( + // array( + // 'directoryName' => $issuer->dn + // ) + //), + 'keyIdentifier' => $issuer->currentKeyIdentifier + )); + //$extensions = &$this->currentCert['tbsCertificate']['extensions']; + //if (isset($issuer->serialNumber)) { + // $extensions[count($extensions) - 1]['authorityCertSerialNumber'] = $issuer->serialNumber; + //} + //unset($extensions); + } + + if (isset($subject->currentKeyIdentifier)) { + $this->setExtension('id-ce-subjectKeyIdentifier', $subject->currentKeyIdentifier); + } + + $altName = array(); + + if (isset($subject->domains) && count($subject->domains)) { + $altName = array_map(array('\phpseclib\File\X509', '_dnsName'), $subject->domains); + } + + if (isset($subject->ipAddresses) && count($subject->ipAddresses)) { + // should an IP address appear as the CN if no domain name is specified? idk + //$ips = count($subject->domains) ? $subject->ipAddresses : array_slice($subject->ipAddresses, 1); + $ipAddresses = array(); + foreach ($subject->ipAddresses as $ipAddress) { + $encoded = $subject->_ipAddress($ipAddress); + if ($encoded !== false) { + $ipAddresses[] = $encoded; + } + } + if (count($ipAddresses)) { + $altName = array_merge($altName, $ipAddresses); + } + } + + if (!empty($altName)) { + $this->setExtension('id-ce-subjectAltName', $altName); + } + + if ($this->caFlag) { + $keyUsage = $this->getExtension('id-ce-keyUsage'); + if (!$keyUsage) { + $keyUsage = array(); + } + + $this->setExtension( + 'id-ce-keyUsage', + array_values(array_unique(array_merge($keyUsage, array('cRLSign', 'keyCertSign')))) + ); + + $basicConstraints = $this->getExtension('id-ce-basicConstraints'); + if (!$basicConstraints) { + $basicConstraints = array(); + } + + $this->setExtension( + 'id-ce-basicConstraints', + array_unique(array_merge(array('cA' => true), $basicConstraints)), + true + ); + + if (!isset($subject->currentKeyIdentifier)) { + $this->setExtension('id-ce-subjectKeyIdentifier', base64_encode($this->computeKeyIdentifier($this->currentCert)), false, false); + } + } + + // resync $this->signatureSubject + // save $tbsCertificate in case there are any \phpseclib\File\ASN1\Element objects in it + $tbsCertificate = $this->currentCert['tbsCertificate']; + $this->loadX509($this->saveX509($this->currentCert)); + + $result = $this->_sign($issuer->privateKey, $signatureAlgorithm); + $result['tbsCertificate'] = $tbsCertificate; + + $this->currentCert = $currentCert; + $this->signatureSubject = $signatureSubject; + + return $result; + } + + /** + * Sign a CSR + * + * @access public + * @return mixed + */ + function signCSR($signatureAlgorithm = 'sha1WithRSAEncryption') + { + if (!is_object($this->privateKey) || empty($this->dn)) { + return false; + } + + $origPublicKey = $this->publicKey; + $class = get_class($this->privateKey); + $this->publicKey = new $class(); + $this->publicKey->loadKey($this->privateKey->getPublicKey()); + $this->publicKey->setPublicKey(); + if (!($publicKey = $this->_formatSubjectPublicKey())) { + return false; + } + $this->publicKey = $origPublicKey; + + $currentCert = isset($this->currentCert) ? $this->currentCert : null; + $signatureSubject = isset($this->signatureSubject) ? $this->signatureSubject: null; + + if (isset($this->currentCert) && is_array($this->currentCert) && isset($this->currentCert['certificationRequestInfo'])) { + $this->currentCert['signatureAlgorithm']['algorithm'] = $signatureAlgorithm; + if (!empty($this->dn)) { + $this->currentCert['certificationRequestInfo']['subject'] = $this->dn; + } + $this->currentCert['certificationRequestInfo']['subjectPKInfo'] = $publicKey; + } else { + $this->currentCert = array( + 'certificationRequestInfo' => + array( + 'version' => 'v1', + 'subject' => $this->dn, + 'subjectPKInfo' => $publicKey + ), + 'signatureAlgorithm' => array('algorithm' => $signatureAlgorithm), + 'signature' => false // this is going to be overwritten later + ); + } + + // resync $this->signatureSubject + // save $certificationRequestInfo in case there are any \phpseclib\File\ASN1\Element objects in it + $certificationRequestInfo = $this->currentCert['certificationRequestInfo']; + $this->loadCSR($this->saveCSR($this->currentCert)); + + $result = $this->_sign($this->privateKey, $signatureAlgorithm); + $result['certificationRequestInfo'] = $certificationRequestInfo; + + $this->currentCert = $currentCert; + $this->signatureSubject = $signatureSubject; + + return $result; + } + + /** + * Sign a SPKAC + * + * @access public + * @return mixed + */ + function signSPKAC($signatureAlgorithm = 'sha1WithRSAEncryption') + { + if (!is_object($this->privateKey)) { + return false; + } + + $origPublicKey = $this->publicKey; + $class = get_class($this->privateKey); + $this->publicKey = new $class(); + $this->publicKey->loadKey($this->privateKey->getPublicKey()); + $this->publicKey->setPublicKey(); + $publicKey = $this->_formatSubjectPublicKey(); + if (!$publicKey) { + return false; + } + $this->publicKey = $origPublicKey; + + $currentCert = isset($this->currentCert) ? $this->currentCert : null; + $signatureSubject = isset($this->signatureSubject) ? $this->signatureSubject: null; + + // re-signing a SPKAC seems silly but since everything else supports re-signing why not? + if (isset($this->currentCert) && is_array($this->currentCert) && isset($this->currentCert['publicKeyAndChallenge'])) { + $this->currentCert['signatureAlgorithm']['algorithm'] = $signatureAlgorithm; + $this->currentCert['publicKeyAndChallenge']['spki'] = $publicKey; + if (!empty($this->challenge)) { + // the bitwise AND ensures that the output is a valid IA5String + $this->currentCert['publicKeyAndChallenge']['challenge'] = $this->challenge & str_repeat("\x7F", strlen($this->challenge)); + } + } else { + $this->currentCert = array( + 'publicKeyAndChallenge' => + array( + 'spki' => $publicKey, + // quoting , + // "A challenge string that is submitted along with the public key. Defaults to an empty string if not specified." + // both Firefox and OpenSSL ("openssl spkac -key private.key") behave this way + // we could alternatively do this instead if we ignored the specs: + // Random::string(8) & str_repeat("\x7F", 8) + 'challenge' => !empty($this->challenge) ? $this->challenge : '' + ), + 'signatureAlgorithm' => array('algorithm' => $signatureAlgorithm), + 'signature' => false // this is going to be overwritten later + ); + } + + // resync $this->signatureSubject + // save $publicKeyAndChallenge in case there are any \phpseclib\File\ASN1\Element objects in it + $publicKeyAndChallenge = $this->currentCert['publicKeyAndChallenge']; + $this->loadSPKAC($this->saveSPKAC($this->currentCert)); + + $result = $this->_sign($this->privateKey, $signatureAlgorithm); + $result['publicKeyAndChallenge'] = $publicKeyAndChallenge; + + $this->currentCert = $currentCert; + $this->signatureSubject = $signatureSubject; + + return $result; + } + + /** + * Sign a CRL + * + * $issuer's private key needs to be loaded. + * + * @param \phpseclib\File\X509 $issuer + * @param \phpseclib\File\X509 $crl + * @param string $signatureAlgorithm optional + * @access public + * @return mixed + */ + function signCRL($issuer, $crl, $signatureAlgorithm = 'sha1WithRSAEncryption') + { + if (!is_object($issuer->privateKey) || empty($issuer->dn)) { + return false; + } + + $currentCert = isset($this->currentCert) ? $this->currentCert : null; + $signatureSubject = isset($this->signatureSubject) ? $this->signatureSubject : null; + + $thisUpdate = new DateTime('now', new DateTimeZone(@date_default_timezone_get())); + $thisUpdate = !empty($this->startDate) ? $this->startDate : $thisUpdate->format('D, d M Y H:i:s O'); + + if (isset($crl->currentCert) && is_array($crl->currentCert) && isset($crl->currentCert['tbsCertList'])) { + $this->currentCert = $crl->currentCert; + $this->currentCert['tbsCertList']['signature']['algorithm'] = $signatureAlgorithm; + $this->currentCert['signatureAlgorithm']['algorithm'] = $signatureAlgorithm; + } else { + $this->currentCert = array( + 'tbsCertList' => + array( + 'version' => 'v2', + 'signature' => array('algorithm' => $signatureAlgorithm), + 'issuer' => false, // this is going to be overwritten later + 'thisUpdate' => $this->_timeField($thisUpdate) // $this->setStartDate() + ), + 'signatureAlgorithm' => array('algorithm' => $signatureAlgorithm), + 'signature' => false // this is going to be overwritten later + ); + } + + $tbsCertList = &$this->currentCert['tbsCertList']; + $tbsCertList['issuer'] = $issuer->dn; + $tbsCertList['thisUpdate'] = $this->_timeField($thisUpdate); + + if (!empty($this->endDate)) { + $tbsCertList['nextUpdate'] = $this->_timeField($this->endDate); // $this->setEndDate() + } else { + unset($tbsCertList['nextUpdate']); + } + + if (!empty($this->serialNumber)) { + $crlNumber = $this->serialNumber; + } else { + $crlNumber = $this->getExtension('id-ce-cRLNumber'); + // "The CRL number is a non-critical CRL extension that conveys a + // monotonically increasing sequence number for a given CRL scope and + // CRL issuer. This extension allows users to easily determine when a + // particular CRL supersedes another CRL." + // -- https://tools.ietf.org/html/rfc5280#section-5.2.3 + $crlNumber = $crlNumber !== false ? $crlNumber->add(new BigInteger(1)) : null; + } + + $this->removeExtension('id-ce-authorityKeyIdentifier'); + $this->removeExtension('id-ce-issuerAltName'); + + // Be sure version >= v2 if some extension found. + $version = isset($tbsCertList['version']) ? $tbsCertList['version'] : 0; + if (!$version) { + if (!empty($tbsCertList['crlExtensions'])) { + $version = 1; // v2. + } elseif (!empty($tbsCertList['revokedCertificates'])) { + foreach ($tbsCertList['revokedCertificates'] as $cert) { + if (!empty($cert['crlEntryExtensions'])) { + $version = 1; // v2. + } + } + } + + if ($version) { + $tbsCertList['version'] = $version; + } + } + + // Store additional extensions. + if (!empty($tbsCertList['version'])) { // At least v2. + if (!empty($crlNumber)) { + $this->setExtension('id-ce-cRLNumber', $crlNumber); + } + + if (isset($issuer->currentKeyIdentifier)) { + $this->setExtension('id-ce-authorityKeyIdentifier', array( + //'authorityCertIssuer' => array( + // array( + // 'directoryName' => $issuer->dn + // ) + //), + 'keyIdentifier' => $issuer->currentKeyIdentifier + )); + //$extensions = &$tbsCertList['crlExtensions']; + //if (isset($issuer->serialNumber)) { + // $extensions[count($extensions) - 1]['authorityCertSerialNumber'] = $issuer->serialNumber; + //} + //unset($extensions); + } + + $issuerAltName = $this->getExtension('id-ce-subjectAltName', $issuer->currentCert); + + if ($issuerAltName !== false) { + $this->setExtension('id-ce-issuerAltName', $issuerAltName); + } + } + + if (empty($tbsCertList['revokedCertificates'])) { + unset($tbsCertList['revokedCertificates']); + } + + unset($tbsCertList); + + // resync $this->signatureSubject + // save $tbsCertList in case there are any \phpseclib\File\ASN1\Element objects in it + $tbsCertList = $this->currentCert['tbsCertList']; + $this->loadCRL($this->saveCRL($this->currentCert)); + + $result = $this->_sign($issuer->privateKey, $signatureAlgorithm); + $result['tbsCertList'] = $tbsCertList; + + $this->currentCert = $currentCert; + $this->signatureSubject = $signatureSubject; + + return $result; + } + + /** + * X.509 certificate signing helper function. + * + * @param \phpseclib\File\X509 $key + * @param string $signatureAlgorithm + * @access public + * @return mixed + */ + function _sign($key, $signatureAlgorithm) + { + if ($key instanceof RSA) { + switch ($signatureAlgorithm) { + case 'md2WithRSAEncryption': + case 'md5WithRSAEncryption': + case 'sha1WithRSAEncryption': + case 'sha224WithRSAEncryption': + case 'sha256WithRSAEncryption': + case 'sha384WithRSAEncryption': + case 'sha512WithRSAEncryption': + $key->setHash(preg_replace('#WithRSAEncryption$#', '', $signatureAlgorithm)); + $key->setSignatureMode(RSA::SIGNATURE_PKCS1); + + $this->currentCert['signature'] = base64_encode("\0" . $key->sign($this->signatureSubject)); + return $this->currentCert; + } + } + + return false; + } + + /** + * Set certificate start date + * + * @param string $date + * @access public + */ + function setStartDate($date) + { + if (!is_object($date) || !is_a($date, 'DateTime')) { + $date = new DateTime($date, new DateTimeZone(@date_default_timezone_get())); + } + + $this->startDate = $date->format('D, d M Y H:i:s O'); + } + + /** + * Set certificate end date + * + * @param string $date + * @access public + */ + function setEndDate($date) + { + /* + To indicate that a certificate has no well-defined expiration date, + the notAfter SHOULD be assigned the GeneralizedTime value of + 99991231235959Z. + + -- http://tools.ietf.org/html/rfc5280#section-4.1.2.5 + */ + if (strtolower($date) == 'lifetime') { + $temp = '99991231235959Z'; + $asn1 = new ASN1(); + $temp = chr(ASN1::TYPE_GENERALIZED_TIME) . $asn1->_encodeLength(strlen($temp)) . $temp; + $this->endDate = new Element($temp); + } else { + if (!is_object($date) || !is_a($date, 'DateTime')) { + $date = new DateTime($date, new DateTimeZone(@date_default_timezone_get())); + } + + $this->endDate = $date->format('D, d M Y H:i:s O'); + } + } + + /** + * Set Serial Number + * + * @param string $serial + * @param int $base optional + * @access public + */ + function setSerialNumber($serial, $base = -256) + { + $this->serialNumber = new BigInteger($serial, $base); + } + + /** + * Turns the certificate into a certificate authority + * + * @access public + */ + function makeCA() + { + $this->caFlag = true; + } + + /** + * Check for validity of subarray + * + * This is intended for use in conjunction with _subArrayUnchecked(), + * implementing the checks included in _subArray() but without copying + * a potentially large array by passing its reference by-value to is_array(). + * + * @param array $root + * @param string $path + * @return boolean + * @access private + */ + function _isSubArrayValid($root, $path) + { + if (!is_array($root)) { + return false; + } + + foreach (explode('/', $path) as $i) { + if (!is_array($root)) { + return false; + } + + if (!isset($root[$i])) { + return true; + } + + $root = $root[$i]; + } + + return true; + } + + /** + * Get a reference to a subarray + * + * This variant of _subArray() does no is_array() checking, + * so $root should be checked with _isSubArrayValid() first. + * + * This is here for performance reasons: + * Passing a reference (i.e. $root) by-value (i.e. to is_array()) + * creates a copy. If $root is an especially large array, this is expensive. + * + * @param array $root + * @param string $path absolute path with / as component separator + * @param bool $create optional + * @access private + * @return array|false + */ + function &_subArrayUnchecked(&$root, $path, $create = false) + { + $false = false; + + foreach (explode('/', $path) as $i) { + if (!isset($root[$i])) { + if (!$create) { + return $false; + } + + $root[$i] = array(); + } + + $root = &$root[$i]; + } + + return $root; + } + + /** + * Get a reference to a subarray + * + * @param array $root + * @param string $path absolute path with / as component separator + * @param bool $create optional + * @access private + * @return array|false + */ + function &_subArray(&$root, $path, $create = false) + { + $false = false; + + if (!is_array($root)) { + return $false; + } + + foreach (explode('/', $path) as $i) { + if (!is_array($root)) { + return $false; + } + + if (!isset($root[$i])) { + if (!$create) { + return $false; + } + + $root[$i] = array(); + } + + $root = &$root[$i]; + } + + return $root; + } + + /** + * Get a reference to an extension subarray + * + * @param array $root + * @param string $path optional absolute path with / as component separator + * @param bool $create optional + * @access private + * @return array|false + */ + function &_extensions(&$root, $path = null, $create = false) + { + if (!isset($root)) { + $root = $this->currentCert; + } + + switch (true) { + case !empty($path): + case !is_array($root): + break; + case isset($root['tbsCertificate']): + $path = 'tbsCertificate/extensions'; + break; + case isset($root['tbsCertList']): + $path = 'tbsCertList/crlExtensions'; + break; + case isset($root['certificationRequestInfo']): + $pth = 'certificationRequestInfo/attributes'; + $attributes = &$this->_subArray($root, $pth, $create); + + if (is_array($attributes)) { + foreach ($attributes as $key => $value) { + if ($value['type'] == 'pkcs-9-at-extensionRequest') { + $path = "$pth/$key/value/0"; + break 2; + } + } + if ($create) { + $key = count($attributes); + $attributes[] = array('type' => 'pkcs-9-at-extensionRequest', 'value' => array()); + $path = "$pth/$key/value/0"; + } + } + break; + } + + $extensions = &$this->_subArray($root, $path, $create); + + if (!is_array($extensions)) { + $false = false; + return $false; + } + + return $extensions; + } + + /** + * Remove an Extension + * + * @param string $id + * @param string $path optional + * @access private + * @return bool + */ + function _removeExtension($id, $path = null) + { + $extensions = &$this->_extensions($this->currentCert, $path); + + if (!is_array($extensions)) { + return false; + } + + $result = false; + foreach ($extensions as $key => $value) { + if ($value['extnId'] == $id) { + unset($extensions[$key]); + $result = true; + } + } + + $extensions = array_values($extensions); + // fix for https://bugs.php.net/75433 affecting PHP 7.2 + if (!isset($extensions[0])) { + $extensions = array_splice($extensions, 0, 0); + } + return $result; + } + + /** + * Get an Extension + * + * Returns the extension if it exists and false if not + * + * @param string $id + * @param array $cert optional + * @param string $path optional + * @access private + * @return mixed + */ + function _getExtension($id, $cert = null, $path = null) + { + $extensions = $this->_extensions($cert, $path); + + if (!is_array($extensions)) { + return false; + } + + foreach ($extensions as $key => $value) { + if ($value['extnId'] == $id) { + return $value['extnValue']; + } + } + + return false; + } + + /** + * Returns a list of all extensions in use + * + * @param array $cert optional + * @param string $path optional + * @access private + * @return array + */ + function _getExtensions($cert = null, $path = null) + { + $exts = $this->_extensions($cert, $path); + $extensions = array(); + + if (is_array($exts)) { + foreach ($exts as $extension) { + $extensions[] = $extension['extnId']; + } + } + + return $extensions; + } + + /** + * Set an Extension + * + * @param string $id + * @param mixed $value + * @param bool $critical optional + * @param bool $replace optional + * @param string $path optional + * @access private + * @return bool + */ + function _setExtension($id, $value, $critical = false, $replace = true, $path = null) + { + $extensions = &$this->_extensions($this->currentCert, $path, true); + + if (!is_array($extensions)) { + return false; + } + + $newext = array('extnId' => $id, 'critical' => $critical, 'extnValue' => $value); + + foreach ($extensions as $key => $value) { + if ($value['extnId'] == $id) { + if (!$replace) { + return false; + } + + $extensions[$key] = $newext; + return true; + } + } + + $extensions[] = $newext; + return true; + } + + /** + * Remove a certificate, CSR or CRL Extension + * + * @param string $id + * @access public + * @return bool + */ + function removeExtension($id) + { + return $this->_removeExtension($id); + } + + /** + * Get a certificate, CSR or CRL Extension + * + * Returns the extension if it exists and false if not + * + * @param string $id + * @param array $cert optional + * @access public + * @return mixed + */ + function getExtension($id, $cert = null) + { + return $this->_getExtension($id, $cert); + } + + /** + * Returns a list of all extensions in use in certificate, CSR or CRL + * + * @param array $cert optional + * @access public + * @return array + */ + function getExtensions($cert = null) + { + return $this->_getExtensions($cert); + } + + /** + * Set a certificate, CSR or CRL Extension + * + * @param string $id + * @param mixed $value + * @param bool $critical optional + * @param bool $replace optional + * @access public + * @return bool + */ + function setExtension($id, $value, $critical = false, $replace = true) + { + return $this->_setExtension($id, $value, $critical, $replace); + } + + /** + * Remove a CSR attribute. + * + * @param string $id + * @param int $disposition optional + * @access public + * @return bool + */ + function removeAttribute($id, $disposition = self::ATTR_ALL) + { + $attributes = &$this->_subArray($this->currentCert, 'certificationRequestInfo/attributes'); + + if (!is_array($attributes)) { + return false; + } + + $result = false; + foreach ($attributes as $key => $attribute) { + if ($attribute['type'] == $id) { + $n = count($attribute['value']); + switch (true) { + case $disposition == self::ATTR_APPEND: + case $disposition == self::ATTR_REPLACE: + return false; + case $disposition >= $n: + $disposition -= $n; + break; + case $disposition == self::ATTR_ALL: + case $n == 1: + unset($attributes[$key]); + $result = true; + break; + default: + unset($attributes[$key]['value'][$disposition]); + $attributes[$key]['value'] = array_values($attributes[$key]['value']); + $result = true; + break; + } + if ($result && $disposition != self::ATTR_ALL) { + break; + } + } + } + + $attributes = array_values($attributes); + return $result; + } + + /** + * Get a CSR attribute + * + * Returns the attribute if it exists and false if not + * + * @param string $id + * @param int $disposition optional + * @param array $csr optional + * @access public + * @return mixed + */ + function getAttribute($id, $disposition = self::ATTR_ALL, $csr = null) + { + if (empty($csr)) { + $csr = $this->currentCert; + } + + $attributes = $this->_subArray($csr, 'certificationRequestInfo/attributes'); + + if (!is_array($attributes)) { + return false; + } + + foreach ($attributes as $key => $attribute) { + if ($attribute['type'] == $id) { + $n = count($attribute['value']); + switch (true) { + case $disposition == self::ATTR_APPEND: + case $disposition == self::ATTR_REPLACE: + return false; + case $disposition == self::ATTR_ALL: + return $attribute['value']; + case $disposition >= $n: + $disposition -= $n; + break; + default: + return $attribute['value'][$disposition]; + } + } + } + + return false; + } + + /** + * Returns a list of all CSR attributes in use + * + * @param array $csr optional + * @access public + * @return array + */ + function getAttributes($csr = null) + { + if (empty($csr)) { + $csr = $this->currentCert; + } + + $attributes = $this->_subArray($csr, 'certificationRequestInfo/attributes'); + $attrs = array(); + + if (is_array($attributes)) { + foreach ($attributes as $attribute) { + $attrs[] = $attribute['type']; + } + } + + return $attrs; + } + + /** + * Set a CSR attribute + * + * @param string $id + * @param mixed $value + * @param bool $disposition optional + * @access public + * @return bool + */ + function setAttribute($id, $value, $disposition = self::ATTR_ALL) + { + $attributes = &$this->_subArray($this->currentCert, 'certificationRequestInfo/attributes', true); + + if (!is_array($attributes)) { + return false; + } + + switch ($disposition) { + case self::ATTR_REPLACE: + $disposition = self::ATTR_APPEND; + case self::ATTR_ALL: + $this->removeAttribute($id); + break; + } + + foreach ($attributes as $key => $attribute) { + if ($attribute['type'] == $id) { + $n = count($attribute['value']); + switch (true) { + case $disposition == self::ATTR_APPEND: + $last = $key; + break; + case $disposition >= $n: + $disposition -= $n; + break; + default: + $attributes[$key]['value'][$disposition] = $value; + return true; + } + } + } + + switch (true) { + case $disposition >= 0: + return false; + case isset($last): + $attributes[$last]['value'][] = $value; + break; + default: + $attributes[] = array('type' => $id, 'value' => $disposition == self::ATTR_ALL ? $value: array($value)); + break; + } + + return true; + } + + /** + * Sets the subject key identifier + * + * This is used by the id-ce-authorityKeyIdentifier and the id-ce-subjectKeyIdentifier extensions. + * + * @param string $value + * @access public + */ + function setKeyIdentifier($value) + { + if (empty($value)) { + unset($this->currentKeyIdentifier); + } else { + $this->currentKeyIdentifier = base64_encode($value); + } + } + + /** + * Compute a public key identifier. + * + * Although key identifiers may be set to any unique value, this function + * computes key identifiers from public key according to the two + * recommended methods (4.2.1.2 RFC 3280). + * Highly polymorphic: try to accept all possible forms of key: + * - Key object + * - \phpseclib\File\X509 object with public or private key defined + * - Certificate or CSR array + * - \phpseclib\File\ASN1\Element object + * - PEM or DER string + * + * @param mixed $key optional + * @param int $method optional + * @access public + * @return string binary key identifier + */ + function computeKeyIdentifier($key = null, $method = 1) + { + if (is_null($key)) { + $key = $this; + } + + switch (true) { + case is_string($key): + break; + case is_array($key) && isset($key['tbsCertificate']['subjectPublicKeyInfo']['subjectPublicKey']): + return $this->computeKeyIdentifier($key['tbsCertificate']['subjectPublicKeyInfo']['subjectPublicKey'], $method); + case is_array($key) && isset($key['certificationRequestInfo']['subjectPKInfo']['subjectPublicKey']): + return $this->computeKeyIdentifier($key['certificationRequestInfo']['subjectPKInfo']['subjectPublicKey'], $method); + case !is_object($key): + return false; + case $key instanceof Element: + // Assume the element is a bitstring-packed key. + $asn1 = new ASN1(); + $decoded = $asn1->decodeBER($key->element); + if (empty($decoded)) { + return false; + } + $raw = $asn1->asn1map($decoded[0], array('type' => ASN1::TYPE_BIT_STRING)); + if (empty($raw)) { + return false; + } + $raw = base64_decode($raw); + // If the key is private, compute identifier from its corresponding public key. + $key = new RSA(); + if (!$key->loadKey($raw)) { + return false; // Not an unencrypted RSA key. + } + if ($key->getPrivateKey() !== false) { // If private. + return $this->computeKeyIdentifier($key, $method); + } + $key = $raw; // Is a public key. + break; + case $key instanceof X509: + if (isset($key->publicKey)) { + return $this->computeKeyIdentifier($key->publicKey, $method); + } + if (isset($key->privateKey)) { + return $this->computeKeyIdentifier($key->privateKey, $method); + } + if (isset($key->currentCert['tbsCertificate']) || isset($key->currentCert['certificationRequestInfo'])) { + return $this->computeKeyIdentifier($key->currentCert, $method); + } + return false; + default: // Should be a key object (i.e.: \phpseclib\Crypt\RSA). + $key = $key->getPublicKey(RSA::PUBLIC_FORMAT_PKCS1); + break; + } + + // If in PEM format, convert to binary. + $key = $this->_extractBER($key); + + // Now we have the key string: compute its sha-1 sum. + $hash = new Hash('sha1'); + $hash = $hash->hash($key); + + if ($method == 2) { + $hash = substr($hash, -8); + $hash[0] = chr((ord($hash[0]) & 0x0F) | 0x40); + } + + return $hash; + } + + /** + * Format a public key as appropriate + * + * @access private + * @return array + */ + function _formatSubjectPublicKey() + { + if ($this->publicKey instanceof RSA) { + // the following two return statements do the same thing. i dunno.. i just prefer the later for some reason. + // the former is a good example of how to do fuzzing on the public key + //return new Element(base64_decode(preg_replace('#-.+-|[\r\n]#', '', $this->publicKey->getPublicKey()))); + return array( + 'algorithm' => array('algorithm' => 'rsaEncryption'), + 'subjectPublicKey' => $this->publicKey->getPublicKey(RSA::PUBLIC_FORMAT_PKCS1) + ); + } + + return false; + } + + /** + * Set the domain name's which the cert is to be valid for + * + * @access public + * @return array + */ + function setDomain() + { + $this->domains = func_get_args(); + $this->removeDNProp('id-at-commonName'); + $this->setDNProp('id-at-commonName', $this->domains[0]); + } + + /** + * Set the IP Addresses's which the cert is to be valid for + * + * @access public + */ + function setIPAddress() + { + $this->ipAddresses = func_get_args(); + /* + if (!isset($this->domains)) { + $this->removeDNProp('id-at-commonName'); + $this->setDNProp('id-at-commonName', $this->ipAddresses[0]); + } + */ + } + + /** + * Helper function to build domain array + * + * @access private + * @param string $domain + * @return array + */ + function _dnsName($domain) + { + return array('dNSName' => $domain); + } + + /** + * Helper function to build IP Address array + * + * (IPv6 is not currently supported) + * + * @access private + * @param string $address + * @return array + */ + function _iPAddress($address) + { + return array('iPAddress' => $address); + } + + /** + * Get the index of a revoked certificate. + * + * @param array $rclist + * @param string $serial + * @param bool $create optional + * @access private + * @return int|false + */ + function _revokedCertificate(&$rclist, $serial, $create = false) + { + $serial = new BigInteger($serial); + + foreach ($rclist as $i => $rc) { + if (!($serial->compare($rc['userCertificate']))) { + return $i; + } + } + + if (!$create) { + return false; + } + + $i = count($rclist); + $revocationDate = new DateTime('now', new DateTimeZone(@date_default_timezone_get())); + $rclist[] = array('userCertificate' => $serial, + 'revocationDate' => $this->_timeField($revocationDate->format('D, d M Y H:i:s O'))); + return $i; + } + + /** + * Revoke a certificate. + * + * @param string $serial + * @param string $date optional + * @access public + * @return bool + */ + function revoke($serial, $date = null) + { + if (isset($this->currentCert['tbsCertList'])) { + if (is_array($rclist = &$this->_subArray($this->currentCert, 'tbsCertList/revokedCertificates', true))) { + if ($this->_revokedCertificate($rclist, $serial) === false) { // If not yet revoked + if (($i = $this->_revokedCertificate($rclist, $serial, true)) !== false) { + if (!empty($date)) { + $rclist[$i]['revocationDate'] = $this->_timeField($date); + } + + return true; + } + } + } + } + + return false; + } + + /** + * Unrevoke a certificate. + * + * @param string $serial + * @access public + * @return bool + */ + function unrevoke($serial) + { + if (is_array($rclist = &$this->_subArray($this->currentCert, 'tbsCertList/revokedCertificates'))) { + if (($i = $this->_revokedCertificate($rclist, $serial)) !== false) { + unset($rclist[$i]); + $rclist = array_values($rclist); + return true; + } + } + + return false; + } + + /** + * Get a revoked certificate. + * + * @param string $serial + * @access public + * @return mixed + */ + function getRevoked($serial) + { + if (is_array($rclist = $this->_subArray($this->currentCert, 'tbsCertList/revokedCertificates'))) { + if (($i = $this->_revokedCertificate($rclist, $serial)) !== false) { + return $rclist[$i]; + } + } + + return false; + } + + /** + * List revoked certificates + * + * @param array $crl optional + * @access public + * @return array + */ + function listRevoked($crl = null) + { + if (!isset($crl)) { + $crl = $this->currentCert; + } + + if (!isset($crl['tbsCertList'])) { + return false; + } + + $result = array(); + + if (is_array($rclist = $this->_subArray($crl, 'tbsCertList/revokedCertificates'))) { + foreach ($rclist as $rc) { + $result[] = $rc['userCertificate']->toString(); + } + } + + return $result; + } + + /** + * Remove a Revoked Certificate Extension + * + * @param string $serial + * @param string $id + * @access public + * @return bool + */ + function removeRevokedCertificateExtension($serial, $id) + { + if (is_array($rclist = &$this->_subArray($this->currentCert, 'tbsCertList/revokedCertificates'))) { + if (($i = $this->_revokedCertificate($rclist, $serial)) !== false) { + return $this->_removeExtension($id, "tbsCertList/revokedCertificates/$i/crlEntryExtensions"); + } + } + + return false; + } + + /** + * Get a Revoked Certificate Extension + * + * Returns the extension if it exists and false if not + * + * @param string $serial + * @param string $id + * @param array $crl optional + * @access public + * @return mixed + */ + function getRevokedCertificateExtension($serial, $id, $crl = null) + { + if (!isset($crl)) { + $crl = $this->currentCert; + } + + if (is_array($rclist = $this->_subArray($crl, 'tbsCertList/revokedCertificates'))) { + if (($i = $this->_revokedCertificate($rclist, $serial)) !== false) { + return $this->_getExtension($id, $crl, "tbsCertList/revokedCertificates/$i/crlEntryExtensions"); + } + } + + return false; + } + + /** + * Returns a list of all extensions in use for a given revoked certificate + * + * @param string $serial + * @param array $crl optional + * @access public + * @return array + */ + function getRevokedCertificateExtensions($serial, $crl = null) + { + if (!isset($crl)) { + $crl = $this->currentCert; + } + + if (is_array($rclist = $this->_subArray($crl, 'tbsCertList/revokedCertificates'))) { + if (($i = $this->_revokedCertificate($rclist, $serial)) !== false) { + return $this->_getExtensions($crl, "tbsCertList/revokedCertificates/$i/crlEntryExtensions"); + } + } + + return false; + } + + /** + * Set a Revoked Certificate Extension + * + * @param string $serial + * @param string $id + * @param mixed $value + * @param bool $critical optional + * @param bool $replace optional + * @access public + * @return bool + */ + function setRevokedCertificateExtension($serial, $id, $value, $critical = false, $replace = true) + { + if (isset($this->currentCert['tbsCertList'])) { + if (is_array($rclist = &$this->_subArray($this->currentCert, 'tbsCertList/revokedCertificates', true))) { + if (($i = $this->_revokedCertificate($rclist, $serial, true)) !== false) { + return $this->_setExtension($id, $value, $critical, $replace, "tbsCertList/revokedCertificates/$i/crlEntryExtensions"); + } + } + } + + return false; + } + + /** + * Extract raw BER from Base64 encoding + * + * @access private + * @param string $str + * @return string + */ + function _extractBER($str) + { + /* X.509 certs are assumed to be base64 encoded but sometimes they'll have additional things in them + * above and beyond the ceritificate. + * ie. some may have the following preceding the -----BEGIN CERTIFICATE----- line: + * + * Bag Attributes + * localKeyID: 01 00 00 00 + * subject=/O=organization/OU=org unit/CN=common name + * issuer=/O=organization/CN=common name + */ + if (strlen($str) > ini_get('pcre.backtrack_limit')) { + $temp = $str; + } else { + $temp = preg_replace('#.*?^-+[^-]+-+[\r\n ]*$#ms', '', $str, 1); + $temp = preg_replace('#-+END.*[\r\n ]*.*#ms', '', $temp, 1); + } + // remove new lines + $temp = str_replace(array("\r", "\n", ' '), '', $temp); + // remove the -----BEGIN CERTIFICATE----- and -----END CERTIFICATE----- stuff + $temp = preg_replace('#^-+[^-]+-+|-+[^-]+-+$#', '', $temp); + $temp = preg_match('#^[a-zA-Z\d/+]*={0,2}$#', $temp) ? base64_decode($temp) : false; + return $temp != false ? $temp : $str; + } + + /** + * Returns the OID corresponding to a name + * + * What's returned in the associative array returned by loadX509() (or load*()) is either a name or an OID if + * no OID to name mapping is available. The problem with this is that what may be an unmapped OID in one version + * of phpseclib may not be unmapped in the next version, so apps that are looking at this OID may not be able + * to work from version to version. + * + * This method will return the OID if a name is passed to it and if no mapping is avialable it'll assume that + * what's being passed to it already is an OID and return that instead. A few examples. + * + * getOID('2.16.840.1.101.3.4.2.1') == '2.16.840.1.101.3.4.2.1' + * getOID('id-sha256') == '2.16.840.1.101.3.4.2.1' + * getOID('zzz') == 'zzz' + * + * @access public + * @return string + */ + function getOID($name) + { + static $reverseMap; + if (!isset($reverseMap)) { + $reverseMap = array_flip($this->oids); + } + return isset($reverseMap[$name]) ? $reverseMap[$name] : $name; + } +} diff --git a/3rdparty/phpseclib/phpseclib/phpseclib/Math/BigInteger.php b/3rdparty/phpseclib/phpseclib/phpseclib/Math/BigInteger.php new file mode 100644 index 00000000..7747a95b --- /dev/null +++ b/3rdparty/phpseclib/phpseclib/phpseclib/Math/BigInteger.php @@ -0,0 +1,3826 @@ +> and << cannot be used, nor can the modulo operator %, + * which only supports integers. Although this fact will slow this library down, the fact that such a high + * base is being used should more than compensate. + * + * Numbers are stored in {@link http://en.wikipedia.org/wiki/Endianness little endian} format. ie. + * (new \phpseclib\Math\BigInteger(pow(2, 26)))->value = array(0, 1) + * + * Useful resources are as follows: + * + * - {@link http://www.cacr.math.uwaterloo.ca/hac/about/chap14.pdf Handbook of Applied Cryptography (HAC)} + * - {@link http://math.libtomcrypt.com/files/tommath.pdf Multi-Precision Math (MPM)} + * - Java's BigInteger classes. See /j2se/src/share/classes/java/math in jdk-1_5_0-src-jrl.zip + * + * Here's an example of how to use this library: + * + * add($b); + * + * echo $c->toString(); // outputs 5 + * ?> + * + * + * @category Math + * @package BigInteger + * @author Jim Wigginton + * @copyright 2006 Jim Wigginton + * @license http://www.opensource.org/licenses/mit-license.html MIT License + */ + +namespace phpseclib\Math; + +use phpseclib\Crypt\Random; + +/** + * Pure-PHP arbitrary precision integer arithmetic library. Supports base-2, base-10, base-16, and base-256 + * numbers. + * + * @package BigInteger + * @author Jim Wigginton + * @access public + */ +class BigInteger +{ + /**#@+ + * Reduction constants + * + * @access private + * @see BigInteger::_reduce() + */ + /** + * @see BigInteger::_montgomery() + * @see BigInteger::_prepMontgomery() + */ + const MONTGOMERY = 0; + /** + * @see BigInteger::_barrett() + */ + const BARRETT = 1; + /** + * @see BigInteger::_mod2() + */ + const POWEROF2 = 2; + /** + * @see BigInteger::_remainder() + */ + const CLASSIC = 3; + /** + * @see BigInteger::__clone() + */ + const NONE = 4; + /**#@-*/ + + /**#@+ + * Array constants + * + * Rather than create a thousands and thousands of new BigInteger objects in repeated function calls to add() and + * multiply() or whatever, we'll just work directly on arrays, taking them in as parameters and returning them. + * + * @access private + */ + /** + * $result[self::VALUE] contains the value. + */ + const VALUE = 0; + /** + * $result[self::SIGN] contains the sign. + */ + const SIGN = 1; + /**#@-*/ + + /**#@+ + * @access private + * @see BigInteger::_montgomery() + * @see BigInteger::_barrett() + */ + /** + * Cache constants + * + * $cache[self::VARIABLE] tells us whether or not the cached data is still valid. + */ + const VARIABLE = 0; + /** + * $cache[self::DATA] contains the cached data. + */ + const DATA = 1; + /**#@-*/ + + /**#@+ + * Mode constants. + * + * @access private + * @see BigInteger::__construct() + */ + /** + * To use the pure-PHP implementation + */ + const MODE_INTERNAL = 1; + /** + * To use the BCMath library + * + * (if enabled; otherwise, the internal implementation will be used) + */ + const MODE_BCMATH = 2; + /** + * To use the GMP library + * + * (if present; otherwise, either the BCMath or the internal implementation will be used) + */ + const MODE_GMP = 3; + /**#@-*/ + + /** + * Karatsuba Cutoff + * + * At what point do we switch between Karatsuba multiplication and schoolbook long multiplication? + * + * @access private + */ + const KARATSUBA_CUTOFF = 25; + + /**#@+ + * Static properties used by the pure-PHP implementation. + * + * @see __construct() + */ + static $base; + static $baseFull; + static $maxDigit; + static $msb; + + /** + * $max10 in greatest $max10Len satisfying + * $max10 = 10**$max10Len <= 2**$base. + */ + static $max10; + + /** + * $max10Len in greatest $max10Len satisfying + * $max10 = 10**$max10Len <= 2**$base. + */ + static $max10Len; + static $maxDigit2; + /**#@-*/ + + /** + * Holds the BigInteger's value. + * + * @var array + * @access private + */ + var $value; + + /** + * Holds the BigInteger's magnitude. + * + * @var bool + * @access private + */ + var $is_negative = false; + + /** + * Precision + * + * @see self::setPrecision() + * @access private + */ + var $precision = -1; + + /** + * Precision Bitmask + * + * @see self::setPrecision() + * @access private + */ + var $bitmask = false; + + /** + * Mode independent value used for serialization. + * + * If the bcmath or gmp extensions are installed $this->value will be a non-serializable resource, hence the need for + * a variable that'll be serializable regardless of whether or not extensions are being used. Unlike $this->value, + * however, $this->hex is only calculated when $this->__sleep() is called. + * + * @see self::__sleep() + * @see self::__wakeup() + * @var string + * @access private + */ + var $hex; + + /** + * Converts base-2, base-10, base-16, and binary strings (base-256) to BigIntegers. + * + * If the second parameter - $base - is negative, then it will be assumed that the number's are encoded using + * two's compliment. The sole exception to this is -10, which is treated the same as 10 is. + * + * Here's an example: + * + * toString(); // outputs 50 + * ?> + * + * + * @param int|string|resource $x base-10 number or base-$base number if $base set. + * @param int $base + * @return \phpseclib\Math\BigInteger + * @access public + */ + function __construct($x = 0, $base = 10) + { + if (!defined('MATH_BIGINTEGER_MODE')) { + switch (true) { + case extension_loaded('gmp'): + define('MATH_BIGINTEGER_MODE', self::MODE_GMP); + break; + case extension_loaded('bcmath'): + define('MATH_BIGINTEGER_MODE', self::MODE_BCMATH); + break; + default: + define('MATH_BIGINTEGER_MODE', self::MODE_INTERNAL); + } + } + + if (extension_loaded('openssl') && !defined('MATH_BIGINTEGER_OPENSSL_DISABLE') && !defined('MATH_BIGINTEGER_OPENSSL_ENABLED')) { + // some versions of XAMPP have mismatched versions of OpenSSL which causes it not to work + $versions = array(); + + // avoid generating errors (even with suppression) when phpinfo() is disabled (common in production systems) + if (function_exists('phpinfo')) { + ob_start(); + @phpinfo(); + $content = ob_get_contents(); + ob_end_clean(); + + preg_match_all('#OpenSSL (Header|Library) Version(.*)#im', $content, $matches); + + if (!empty($matches[1])) { + for ($i = 0; $i < count($matches[1]); $i++) { + $fullVersion = trim(str_replace('=>', '', strip_tags($matches[2][$i]))); + + // Remove letter part in OpenSSL version + if (!preg_match('/(\d+\.\d+\.\d+)/i', $fullVersion, $m)) { + $versions[$matches[1][$i]] = $fullVersion; + } else { + $versions[$matches[1][$i]] = $m[0]; + } + } + } + } + + // it doesn't appear that OpenSSL versions were reported upon until PHP 5.3+ + switch (true) { + case !isset($versions['Header']): + case !isset($versions['Library']): + case $versions['Header'] == $versions['Library']: + case version_compare($versions['Header'], '1.0.0') >= 0 && version_compare($versions['Library'], '1.0.0') >= 0: + define('MATH_BIGINTEGER_OPENSSL_ENABLED', true); + break; + default: + define('MATH_BIGINTEGER_OPENSSL_DISABLE', true); + } + } + + if (!defined('PHP_INT_SIZE')) { + define('PHP_INT_SIZE', 4); + } + + if (empty(self::$base) && MATH_BIGINTEGER_MODE == self::MODE_INTERNAL) { + switch (PHP_INT_SIZE) { + case 8: // use 64-bit integers if int size is 8 bytes + self::$base = 31; + self::$baseFull = 0x80000000; + self::$maxDigit = 0x7FFFFFFF; + self::$msb = 0x40000000; + self::$max10 = 1000000000; + self::$max10Len = 9; + self::$maxDigit2 = pow(2, 62); + break; + //case 4: // use 64-bit floats if int size is 4 bytes + default: + self::$base = 26; + self::$baseFull = 0x4000000; + self::$maxDigit = 0x3FFFFFF; + self::$msb = 0x2000000; + self::$max10 = 10000000; + self::$max10Len = 7; + self::$maxDigit2 = pow(2, 52); // pow() prevents truncation + } + } + + switch (MATH_BIGINTEGER_MODE) { + case self::MODE_GMP: + switch (true) { + case is_resource($x) && get_resource_type($x) == 'GMP integer': + // PHP 5.6 switched GMP from using resources to objects + case $x instanceof \GMP: + $this->value = $x; + return; + } + $this->value = gmp_init(0); + break; + case self::MODE_BCMATH: + $this->value = '0'; + break; + default: + $this->value = array(); + } + + // '0' counts as empty() but when the base is 256 '0' is equal to ord('0') or 48 + // '0' is the only value like this per http://php.net/empty + if (empty($x) && (abs($base) != 256 || $x !== '0')) { + return; + } + + switch ($base) { + case -256: + if (ord($x[0]) & 0x80) { + $x = ~$x; + $this->is_negative = true; + } + case 256: + switch (MATH_BIGINTEGER_MODE) { + case self::MODE_GMP: + $this->value = function_exists('gmp_import') ? + gmp_import($x) : + gmp_init('0x' . bin2hex($x)); + if ($this->is_negative) { + $this->value = gmp_neg($this->value); + } + break; + case self::MODE_BCMATH: + // round $len to the nearest 4 (thanks, DavidMJ!) + $len = (strlen($x) + 3) & ~3; + + $x = str_pad($x, $len, chr(0), STR_PAD_LEFT); + + for ($i = 0; $i < $len; $i+= 4) { + $this->value = bcmul($this->value, '4294967296', 0); // 4294967296 == 2**32 + $this->value = bcadd($this->value, 0x1000000 * ord($x[$i]) + ((ord($x[$i + 1]) << 16) | (ord($x[$i + 2]) << 8) | ord($x[$i + 3])), 0); + } + + if ($this->is_negative) { + $this->value = '-' . $this->value; + } + + break; + // converts a base-2**8 (big endian / msb) number to base-2**26 (little endian / lsb) + default: + while (strlen($x)) { + $this->value[] = $this->_bytes2int($this->_base256_rshift($x, self::$base)); + } + } + + if ($this->is_negative) { + if (MATH_BIGINTEGER_MODE != self::MODE_INTERNAL) { + $this->is_negative = false; + } + $temp = $this->add(new static('-1')); + $this->value = $temp->value; + } + break; + case 16: + case -16: + if ($base > 0 && $x[0] == '-') { + $this->is_negative = true; + $x = substr($x, 1); + } + + $x = preg_replace('#^(?:0x)?([A-Fa-f0-9]*).*#s', '$1', $x); + + $is_negative = false; + if ($base < 0 && hexdec($x[0]) >= 8) { + $this->is_negative = $is_negative = true; + $x = bin2hex(~pack('H*', $x)); + } + + switch (MATH_BIGINTEGER_MODE) { + case self::MODE_GMP: + $temp = $this->is_negative ? '-0x' . $x : '0x' . $x; + $this->value = gmp_init($temp); + $this->is_negative = false; + break; + case self::MODE_BCMATH: + $x = (strlen($x) & 1) ? '0' . $x : $x; + $temp = new static(pack('H*', $x), 256); + $this->value = $this->is_negative ? '-' . $temp->value : $temp->value; + $this->is_negative = false; + break; + default: + $x = (strlen($x) & 1) ? '0' . $x : $x; + $temp = new static(pack('H*', $x), 256); + $this->value = $temp->value; + } + + if ($is_negative) { + $temp = $this->add(new static('-1')); + $this->value = $temp->value; + } + break; + case 10: + case -10: + // (?value = gmp_init($x); + break; + case self::MODE_BCMATH: + // explicitly casting $x to a string is necessary, here, since doing $x[0] on -1 yields different + // results then doing it on '-1' does (modInverse does $x[0]) + $this->value = $x === '-' ? '0' : (string) $x; + break; + default: + $temp = new static(); + + $multiplier = new static(); + $multiplier->value = array(self::$max10); + + if ($x[0] == '-') { + $this->is_negative = true; + $x = substr($x, 1); + } + + $x = str_pad($x, strlen($x) + ((self::$max10Len - 1) * strlen($x)) % self::$max10Len, 0, STR_PAD_LEFT); + while (strlen($x)) { + $temp = $temp->multiply($multiplier); + $temp = $temp->add(new static($this->_int2bytes(substr($x, 0, self::$max10Len)), 256)); + $x = substr($x, self::$max10Len); + } + + $this->value = $temp->value; + } + break; + case 2: // base-2 support originally implemented by Lluis Pamies - thanks! + case -2: + if ($base > 0 && $x[0] == '-') { + $this->is_negative = true; + $x = substr($x, 1); + } + + $x = preg_replace('#^([01]*).*#s', '$1', $x); + $x = str_pad($x, strlen($x) + (3 * strlen($x)) % 4, 0, STR_PAD_LEFT); + + $str = '0x'; + while (strlen($x)) { + $part = substr($x, 0, 4); + $str.= dechex(bindec($part)); + $x = substr($x, 4); + } + + if ($this->is_negative) { + $str = '-' . $str; + } + + $temp = new static($str, 8 * $base); // ie. either -16 or +16 + $this->value = $temp->value; + $this->is_negative = $temp->is_negative; + + break; + default: + // base not supported, so we'll let $this == 0 + } + } + + /** + * Converts a BigInteger to a byte string (eg. base-256). + * + * Negative numbers are saved as positive numbers, unless $twos_compliment is set to true, at which point, they're + * saved as two's compliment. + * + * Here's an example: + * + * toBytes(); // outputs chr(65) + * ?> + * + * + * @param bool $twos_compliment + * @return string + * @access public + * @internal Converts a base-2**26 number to base-2**8 + */ + function toBytes($twos_compliment = false) + { + if ($twos_compliment) { + $comparison = $this->compare(new static()); + if ($comparison == 0) { + return $this->precision > 0 ? str_repeat(chr(0), ($this->precision + 1) >> 3) : ''; + } + + $temp = $comparison < 0 ? $this->add(new static(1)) : $this->copy(); + $bytes = $temp->toBytes(); + + if (!strlen($bytes)) { // eg. if the number we're trying to convert is -1 + $bytes = chr(0); + } + + if ($this->precision <= 0 && (ord($bytes[0]) & 0x80)) { + $bytes = chr(0) . $bytes; + } + + return $comparison < 0 ? ~$bytes : $bytes; + } + + switch (MATH_BIGINTEGER_MODE) { + case self::MODE_GMP: + if (gmp_cmp($this->value, gmp_init(0)) == 0) { + return $this->precision > 0 ? str_repeat(chr(0), ($this->precision + 1) >> 3) : ''; + } + + if (function_exists('gmp_export')) { + $temp = gmp_export($this->value); + } else { + $temp = gmp_strval(gmp_abs($this->value), 16); + $temp = (strlen($temp) & 1) ? '0' . $temp : $temp; + $temp = pack('H*', $temp); + } + + return $this->precision > 0 ? + substr(str_pad($temp, $this->precision >> 3, chr(0), STR_PAD_LEFT), -($this->precision >> 3)) : + ltrim($temp, chr(0)); + case self::MODE_BCMATH: + if ($this->value === '0') { + return $this->precision > 0 ? str_repeat(chr(0), ($this->precision + 1) >> 3) : ''; + } + + $value = ''; + $current = $this->value; + + if ($current[0] == '-') { + $current = substr($current, 1); + } + + while (bccomp($current, '0', 0) > 0) { + $temp = bcmod($current, '16777216'); + $value = chr($temp >> 16) . chr($temp >> 8) . chr($temp) . $value; + $current = bcdiv($current, '16777216', 0); + } + + return $this->precision > 0 ? + substr(str_pad($value, $this->precision >> 3, chr(0), STR_PAD_LEFT), -($this->precision >> 3)) : + ltrim($value, chr(0)); + } + + if (!count($this->value)) { + return $this->precision > 0 ? str_repeat(chr(0), ($this->precision + 1) >> 3) : ''; + } + $result = $this->_int2bytes($this->value[count($this->value) - 1]); + + $temp = $this->copy(); + + for ($i = count($temp->value) - 2; $i >= 0; --$i) { + $temp->_base256_lshift($result, self::$base); + $result = $result | str_pad($temp->_int2bytes($temp->value[$i]), strlen($result), chr(0), STR_PAD_LEFT); + } + + return $this->precision > 0 ? + str_pad(substr($result, -(($this->precision + 7) >> 3)), ($this->precision + 7) >> 3, chr(0), STR_PAD_LEFT) : + $result; + } + + /** + * Converts a BigInteger to a hex string (eg. base-16)). + * + * Negative numbers are saved as positive numbers, unless $twos_compliment is set to true, at which point, they're + * saved as two's compliment. + * + * Here's an example: + * + * toHex(); // outputs '41' + * ?> + * + * + * @param bool $twos_compliment + * @return string + * @access public + * @internal Converts a base-2**26 number to base-2**8 + */ + function toHex($twos_compliment = false) + { + return bin2hex($this->toBytes($twos_compliment)); + } + + /** + * Converts a BigInteger to a bit string (eg. base-2). + * + * Negative numbers are saved as positive numbers, unless $twos_compliment is set to true, at which point, they're + * saved as two's compliment. + * + * Here's an example: + * + * toBits(); // outputs '1000001' + * ?> + * + * + * @param bool $twos_compliment + * @return string + * @access public + * @internal Converts a base-2**26 number to base-2**2 + */ + function toBits($twos_compliment = false) + { + $hex = $this->toHex($twos_compliment); + $bits = ''; + for ($i = strlen($hex) - 6, $start = strlen($hex) % 6; $i >= $start; $i-=6) { + $bits = str_pad(decbin(hexdec(substr($hex, $i, 6))), 24, '0', STR_PAD_LEFT) . $bits; + } + if ($start) { // hexdec('') == 0 + $bits = str_pad(decbin(hexdec(substr($hex, 0, $start))), 8 * $start, '0', STR_PAD_LEFT) . $bits; + } + $result = $this->precision > 0 ? substr($bits, -$this->precision) : ltrim($bits, '0'); + + if ($twos_compliment && $this->compare(new static()) > 0 && $this->precision <= 0) { + return '0' . $result; + } + + return $result; + } + + /** + * Converts a BigInteger to a base-10 number. + * + * Here's an example: + * + * toString(); // outputs 50 + * ?> + * + * + * @return string + * @access public + * @internal Converts a base-2**26 number to base-10**7 (which is pretty much base-10) + */ + function toString() + { + switch (MATH_BIGINTEGER_MODE) { + case self::MODE_GMP: + return gmp_strval($this->value); + case self::MODE_BCMATH: + if ($this->value === '0') { + return '0'; + } + + return ltrim($this->value, '0'); + } + + if (!count($this->value)) { + return '0'; + } + + $temp = $this->copy(); + $temp->bitmask = false; + $temp->is_negative = false; + + $divisor = new static(); + $divisor->value = array(self::$max10); + $result = ''; + while (count($temp->value)) { + list($temp, $mod) = $temp->divide($divisor); + $result = str_pad(isset($mod->value[0]) ? $mod->value[0] : '', self::$max10Len, '0', STR_PAD_LEFT) . $result; + } + $result = ltrim($result, '0'); + if (empty($result)) { + $result = '0'; + } + + if ($this->is_negative) { + $result = '-' . $result; + } + + return $result; + } + + /** + * Return the size of a BigInteger in bits + * + * @return int + */ + function getLength() + { + if (MATH_BIGINTEGER_MODE != self::MODE_INTERNAL) { + return strlen($this->toBits()); + } + + $max = count($this->value) - 1; + return $max != -1 ? + $max * self::$base + intval(ceil(log($this->value[$max] + 1, 2))) : + 0; + } + + /** + * Return the size of a BigInteger in bytes + * + * @return int + */ + function getLengthInBytes() + { + return (int) ceil($this->getLength() / 8); + } + + /** + * Copy an object + * + * PHP5 passes objects by reference while PHP4 passes by value. As such, we need a function to guarantee + * that all objects are passed by value, when appropriate. More information can be found here: + * + * {@link http://php.net/language.oop5.basic#51624} + * + * @access public + * @see self::__clone() + * @return \phpseclib\Math\BigInteger + */ + function copy() + { + $temp = new static(); + $temp->value = $this->value; + $temp->is_negative = $this->is_negative; + $temp->precision = $this->precision; + $temp->bitmask = $this->bitmask; + return $temp; + } + + /** + * __toString() magic method + * + * Will be called, automatically, if you're supporting just PHP5. If you're supporting PHP4, you'll need to call + * toString(). + * + * @access public + * @internal Implemented per a suggestion by Techie-Michael - thanks! + */ + function __toString() + { + return $this->toString(); + } + + /** + * __clone() magic method + * + * Although you can call BigInteger::__toString() directly in PHP5, you cannot call BigInteger::__clone() directly + * in PHP5. You can in PHP4 since it's not a magic method, but in PHP5, you have to call it by using the PHP5 + * only syntax of $y = clone $x. As such, if you're trying to write an application that works on both PHP4 and + * PHP5, call BigInteger::copy(), instead. + * + * @access public + * @see self::copy() + * @return \phpseclib\Math\BigInteger + */ + function __clone() + { + return $this->copy(); + } + + /** + * __sleep() magic method + * + * Will be called, automatically, when serialize() is called on a BigInteger object. + * + * @see self::__wakeup() + * @access public + */ + function __sleep() + { + $this->hex = $this->toHex(true); + $vars = array('hex'); + if ($this->precision > 0) { + $vars[] = 'precision'; + } + return $vars; + } + + /** + * __wakeup() magic method + * + * Will be called, automatically, when unserialize() is called on a BigInteger object. + * + * @see self::__sleep() + * @access public + */ + function __wakeup() + { + $temp = new static($this->hex, -16); + $this->value = $temp->value; + $this->is_negative = $temp->is_negative; + if ($this->precision > 0) { + // recalculate $this->bitmask + $this->setPrecision($this->precision); + } + } + + /** + * __debugInfo() magic method + * + * Will be called, automatically, when print_r() or var_dump() are called + * + * @access public + */ + function __debugInfo() + { + $opts = array(); + switch (MATH_BIGINTEGER_MODE) { + case self::MODE_GMP: + $engine = 'gmp'; + break; + case self::MODE_BCMATH: + $engine = 'bcmath'; + break; + case self::MODE_INTERNAL: + $engine = 'internal'; + $opts[] = PHP_INT_SIZE == 8 ? '64-bit' : '32-bit'; + } + if (MATH_BIGINTEGER_MODE != self::MODE_GMP && defined('MATH_BIGINTEGER_OPENSSL_ENABLED')) { + $opts[] = 'OpenSSL'; + } + if (!empty($opts)) { + $engine.= ' (' . implode('.', $opts) . ')'; + } + return array( + 'value' => '0x' . $this->toHex(true), + 'engine' => $engine + ); + } + + /** + * Adds two BigIntegers. + * + * Here's an example: + * + * add($b); + * + * echo $c->toString(); // outputs 30 + * ?> + * + * + * @param \phpseclib\Math\BigInteger $y + * @return \phpseclib\Math\BigInteger + * @access public + * @internal Performs base-2**52 addition + */ + function add($y) + { + switch (MATH_BIGINTEGER_MODE) { + case self::MODE_GMP: + $temp = new static(); + $temp->value = gmp_add($this->value, $y->value); + + return $this->_normalize($temp); + case self::MODE_BCMATH: + $temp = new static(); + $temp->value = bcadd($this->value, $y->value, 0); + + return $this->_normalize($temp); + } + + $temp = $this->_add($this->value, $this->is_negative, $y->value, $y->is_negative); + + $result = new static(); + $result->value = $temp[self::VALUE]; + $result->is_negative = $temp[self::SIGN]; + + return $this->_normalize($result); + } + + /** + * Performs addition. + * + * @param array $x_value + * @param bool $x_negative + * @param array $y_value + * @param bool $y_negative + * @return array + * @access private + */ + function _add($x_value, $x_negative, $y_value, $y_negative) + { + $x_size = count($x_value); + $y_size = count($y_value); + + if ($x_size == 0) { + return array( + self::VALUE => $y_value, + self::SIGN => $y_negative + ); + } elseif ($y_size == 0) { + return array( + self::VALUE => $x_value, + self::SIGN => $x_negative + ); + } + + // subtract, if appropriate + if ($x_negative != $y_negative) { + if ($x_value == $y_value) { + return array( + self::VALUE => array(), + self::SIGN => false + ); + } + + $temp = $this->_subtract($x_value, false, $y_value, false); + $temp[self::SIGN] = $this->_compare($x_value, false, $y_value, false) > 0 ? + $x_negative : $y_negative; + + return $temp; + } + + if ($x_size < $y_size) { + $size = $x_size; + $value = $y_value; + } else { + $size = $y_size; + $value = $x_value; + } + + $value[count($value)] = 0; // just in case the carry adds an extra digit + + $carry = 0; + for ($i = 0, $j = 1; $j < $size; $i+=2, $j+=2) { + $sum = $x_value[$j] * self::$baseFull + $x_value[$i] + $y_value[$j] * self::$baseFull + $y_value[$i] + $carry; + $carry = $sum >= self::$maxDigit2; // eg. floor($sum / 2**52); only possible values (in any base) are 0 and 1 + $sum = $carry ? $sum - self::$maxDigit2 : $sum; + + $temp = self::$base === 26 ? intval($sum / 0x4000000) : ($sum >> 31); + + $value[$i] = (int) ($sum - self::$baseFull * $temp); // eg. a faster alternative to fmod($sum, 0x4000000) + $value[$j] = $temp; + } + + if ($j == $size) { // ie. if $y_size is odd + $sum = $x_value[$i] + $y_value[$i] + $carry; + $carry = $sum >= self::$baseFull; + $value[$i] = $carry ? $sum - self::$baseFull : $sum; + ++$i; // ie. let $i = $j since we've just done $value[$i] + } + + if ($carry) { + for (; $value[$i] == self::$maxDigit; ++$i) { + $value[$i] = 0; + } + ++$value[$i]; + } + + return array( + self::VALUE => $this->_trim($value), + self::SIGN => $x_negative + ); + } + + /** + * Subtracts two BigIntegers. + * + * Here's an example: + * + * subtract($b); + * + * echo $c->toString(); // outputs -10 + * ?> + * + * + * @param \phpseclib\Math\BigInteger $y + * @return \phpseclib\Math\BigInteger + * @access public + * @internal Performs base-2**52 subtraction + */ + function subtract($y) + { + switch (MATH_BIGINTEGER_MODE) { + case self::MODE_GMP: + $temp = new static(); + $temp->value = gmp_sub($this->value, $y->value); + + return $this->_normalize($temp); + case self::MODE_BCMATH: + $temp = new static(); + $temp->value = bcsub($this->value, $y->value, 0); + + return $this->_normalize($temp); + } + + $temp = $this->_subtract($this->value, $this->is_negative, $y->value, $y->is_negative); + + $result = new static(); + $result->value = $temp[self::VALUE]; + $result->is_negative = $temp[self::SIGN]; + + return $this->_normalize($result); + } + + /** + * Performs subtraction. + * + * @param array $x_value + * @param bool $x_negative + * @param array $y_value + * @param bool $y_negative + * @return array + * @access private + */ + function _subtract($x_value, $x_negative, $y_value, $y_negative) + { + $x_size = count($x_value); + $y_size = count($y_value); + + if ($x_size == 0) { + return array( + self::VALUE => $y_value, + self::SIGN => !$y_negative + ); + } elseif ($y_size == 0) { + return array( + self::VALUE => $x_value, + self::SIGN => $x_negative + ); + } + + // add, if appropriate (ie. -$x - +$y or +$x - -$y) + if ($x_negative != $y_negative) { + $temp = $this->_add($x_value, false, $y_value, false); + $temp[self::SIGN] = $x_negative; + + return $temp; + } + + $diff = $this->_compare($x_value, $x_negative, $y_value, $y_negative); + + if (!$diff) { + return array( + self::VALUE => array(), + self::SIGN => false + ); + } + + // switch $x and $y around, if appropriate. + if ((!$x_negative && $diff < 0) || ($x_negative && $diff > 0)) { + $temp = $x_value; + $x_value = $y_value; + $y_value = $temp; + + $x_negative = !$x_negative; + + $x_size = count($x_value); + $y_size = count($y_value); + } + + // at this point, $x_value should be at least as big as - if not bigger than - $y_value + + $carry = 0; + for ($i = 0, $j = 1; $j < $y_size; $i+=2, $j+=2) { + $sum = $x_value[$j] * self::$baseFull + $x_value[$i] - $y_value[$j] * self::$baseFull - $y_value[$i] - $carry; + $carry = $sum < 0; // eg. floor($sum / 2**52); only possible values (in any base) are 0 and 1 + $sum = $carry ? $sum + self::$maxDigit2 : $sum; + + $temp = self::$base === 26 ? intval($sum / 0x4000000) : ($sum >> 31); + + $x_value[$i] = (int) ($sum - self::$baseFull * $temp); + $x_value[$j] = $temp; + } + + if ($j == $y_size) { // ie. if $y_size is odd + $sum = $x_value[$i] - $y_value[$i] - $carry; + $carry = $sum < 0; + $x_value[$i] = $carry ? $sum + self::$baseFull : $sum; + ++$i; + } + + if ($carry) { + for (; !$x_value[$i]; ++$i) { + $x_value[$i] = self::$maxDigit; + } + --$x_value[$i]; + } + + return array( + self::VALUE => $this->_trim($x_value), + self::SIGN => $x_negative + ); + } + + /** + * Multiplies two BigIntegers + * + * Here's an example: + * + * multiply($b); + * + * echo $c->toString(); // outputs 200 + * ?> + * + * + * @param \phpseclib\Math\BigInteger $x + * @return \phpseclib\Math\BigInteger + * @access public + */ + function multiply($x) + { + switch (MATH_BIGINTEGER_MODE) { + case self::MODE_GMP: + $temp = new static(); + $temp->value = gmp_mul($this->value, $x->value); + + return $this->_normalize($temp); + case self::MODE_BCMATH: + $temp = new static(); + $temp->value = bcmul($this->value, $x->value, 0); + + return $this->_normalize($temp); + } + + $temp = $this->_multiply($this->value, $this->is_negative, $x->value, $x->is_negative); + + $product = new static(); + $product->value = $temp[self::VALUE]; + $product->is_negative = $temp[self::SIGN]; + + return $this->_normalize($product); + } + + /** + * Performs multiplication. + * + * @param array $x_value + * @param bool $x_negative + * @param array $y_value + * @param bool $y_negative + * @return array + * @access private + */ + function _multiply($x_value, $x_negative, $y_value, $y_negative) + { + //if ( $x_value == $y_value ) { + // return array( + // self::VALUE => $this->_square($x_value), + // self::SIGN => $x_sign != $y_value + // ); + //} + + $x_length = count($x_value); + $y_length = count($y_value); + + if (!$x_length || !$y_length) { // a 0 is being multiplied + return array( + self::VALUE => array(), + self::SIGN => false + ); + } + + return array( + self::VALUE => min($x_length, $y_length) < 2 * self::KARATSUBA_CUTOFF ? + $this->_trim($this->_regularMultiply($x_value, $y_value)) : + $this->_trim($this->_karatsuba($x_value, $y_value)), + self::SIGN => $x_negative != $y_negative + ); + } + + /** + * Performs long multiplication on two BigIntegers + * + * Modeled after 'multiply' in MutableBigInteger.java. + * + * @param array $x_value + * @param array $y_value + * @return array + * @access private + */ + function _regularMultiply($x_value, $y_value) + { + $x_length = count($x_value); + $y_length = count($y_value); + + if (!$x_length || !$y_length) { // a 0 is being multiplied + return array(); + } + + if ($x_length < $y_length) { + $temp = $x_value; + $x_value = $y_value; + $y_value = $temp; + + $x_length = count($x_value); + $y_length = count($y_value); + } + + $product_value = $this->_array_repeat(0, $x_length + $y_length); + + // the following for loop could be removed if the for loop following it + // (the one with nested for loops) initially set $i to 0, but + // doing so would also make the result in one set of unnecessary adds, + // since on the outermost loops first pass, $product->value[$k] is going + // to always be 0 + + $carry = 0; + + for ($j = 0; $j < $x_length; ++$j) { // ie. $i = 0 + $temp = $x_value[$j] * $y_value[0] + $carry; // $product_value[$k] == 0 + $carry = self::$base === 26 ? intval($temp / 0x4000000) : ($temp >> 31); + $product_value[$j] = (int) ($temp - self::$baseFull * $carry); + } + + $product_value[$j] = $carry; + + // the above for loop is what the previous comment was talking about. the + // following for loop is the "one with nested for loops" + for ($i = 1; $i < $y_length; ++$i) { + $carry = 0; + + for ($j = 0, $k = $i; $j < $x_length; ++$j, ++$k) { + $temp = $product_value[$k] + $x_value[$j] * $y_value[$i] + $carry; + $carry = self::$base === 26 ? intval($temp / 0x4000000) : ($temp >> 31); + $product_value[$k] = (int) ($temp - self::$baseFull * $carry); + } + + $product_value[$k] = $carry; + } + + return $product_value; + } + + /** + * Performs Karatsuba multiplication on two BigIntegers + * + * See {@link http://en.wikipedia.org/wiki/Karatsuba_algorithm Karatsuba algorithm} and + * {@link http://math.libtomcrypt.com/files/tommath.pdf#page=120 MPM 5.2.3}. + * + * @param array $x_value + * @param array $y_value + * @return array + * @access private + */ + function _karatsuba($x_value, $y_value) + { + $m = min(count($x_value) >> 1, count($y_value) >> 1); + + if ($m < self::KARATSUBA_CUTOFF) { + return $this->_regularMultiply($x_value, $y_value); + } + + $x1 = array_slice($x_value, $m); + $x0 = array_slice($x_value, 0, $m); + $y1 = array_slice($y_value, $m); + $y0 = array_slice($y_value, 0, $m); + + $z2 = $this->_karatsuba($x1, $y1); + $z0 = $this->_karatsuba($x0, $y0); + + $z1 = $this->_add($x1, false, $x0, false); + $temp = $this->_add($y1, false, $y0, false); + $z1 = $this->_karatsuba($z1[self::VALUE], $temp[self::VALUE]); + $temp = $this->_add($z2, false, $z0, false); + $z1 = $this->_subtract($z1, false, $temp[self::VALUE], false); + + $z2 = array_merge(array_fill(0, 2 * $m, 0), $z2); + $z1[self::VALUE] = array_merge(array_fill(0, $m, 0), $z1[self::VALUE]); + + $xy = $this->_add($z2, false, $z1[self::VALUE], $z1[self::SIGN]); + $xy = $this->_add($xy[self::VALUE], $xy[self::SIGN], $z0, false); + + return $xy[self::VALUE]; + } + + /** + * Performs squaring + * + * @param array $x + * @return array + * @access private + */ + function _square($x = false) + { + return count($x) < 2 * self::KARATSUBA_CUTOFF ? + $this->_trim($this->_baseSquare($x)) : + $this->_trim($this->_karatsubaSquare($x)); + } + + /** + * Performs traditional squaring on two BigIntegers + * + * Squaring can be done faster than multiplying a number by itself can be. See + * {@link http://www.cacr.math.uwaterloo.ca/hac/about/chap14.pdf#page=7 HAC 14.2.4} / + * {@link http://math.libtomcrypt.com/files/tommath.pdf#page=141 MPM 5.3} for more information. + * + * @param array $value + * @return array + * @access private + */ + function _baseSquare($value) + { + if (empty($value)) { + return array(); + } + $square_value = $this->_array_repeat(0, 2 * count($value)); + + for ($i = 0, $max_index = count($value) - 1; $i <= $max_index; ++$i) { + $i2 = $i << 1; + + $temp = $square_value[$i2] + $value[$i] * $value[$i]; + $carry = self::$base === 26 ? intval($temp / 0x4000000) : ($temp >> 31); + $square_value[$i2] = (int) ($temp - self::$baseFull * $carry); + + // note how we start from $i+1 instead of 0 as we do in multiplication. + for ($j = $i + 1, $k = $i2 + 1; $j <= $max_index; ++$j, ++$k) { + $temp = $square_value[$k] + 2 * $value[$j] * $value[$i] + $carry; + $carry = self::$base === 26 ? intval($temp / 0x4000000) : ($temp >> 31); + $square_value[$k] = (int) ($temp - self::$baseFull * $carry); + } + + // the following line can yield values larger 2**15. at this point, PHP should switch + // over to floats. + $square_value[$i + $max_index + 1] = $carry; + } + + return $square_value; + } + + /** + * Performs Karatsuba "squaring" on two BigIntegers + * + * See {@link http://en.wikipedia.org/wiki/Karatsuba_algorithm Karatsuba algorithm} and + * {@link http://math.libtomcrypt.com/files/tommath.pdf#page=151 MPM 5.3.4}. + * + * @param array $value + * @return array + * @access private + */ + function _karatsubaSquare($value) + { + $m = count($value) >> 1; + + if ($m < self::KARATSUBA_CUTOFF) { + return $this->_baseSquare($value); + } + + $x1 = array_slice($value, $m); + $x0 = array_slice($value, 0, $m); + + $z2 = $this->_karatsubaSquare($x1); + $z0 = $this->_karatsubaSquare($x0); + + $z1 = $this->_add($x1, false, $x0, false); + $z1 = $this->_karatsubaSquare($z1[self::VALUE]); + $temp = $this->_add($z2, false, $z0, false); + $z1 = $this->_subtract($z1, false, $temp[self::VALUE], false); + + $z2 = array_merge(array_fill(0, 2 * $m, 0), $z2); + $z1[self::VALUE] = array_merge(array_fill(0, $m, 0), $z1[self::VALUE]); + + $xx = $this->_add($z2, false, $z1[self::VALUE], $z1[self::SIGN]); + $xx = $this->_add($xx[self::VALUE], $xx[self::SIGN], $z0, false); + + return $xx[self::VALUE]; + } + + /** + * Divides two BigIntegers. + * + * Returns an array whose first element contains the quotient and whose second element contains the + * "common residue". If the remainder would be positive, the "common residue" and the remainder are the + * same. If the remainder would be negative, the "common residue" is equal to the sum of the remainder + * and the divisor (basically, the "common residue" is the first positive modulo). + * + * Here's an example: + * + * divide($b); + * + * echo $quotient->toString(); // outputs 0 + * echo "\r\n"; + * echo $remainder->toString(); // outputs 10 + * ?> + * + * + * @param \phpseclib\Math\BigInteger $y + * @return array + * @access public + * @internal This function is based off of {@link http://www.cacr.math.uwaterloo.ca/hac/about/chap14.pdf#page=9 HAC 14.20}. + */ + function divide($y) + { + switch (MATH_BIGINTEGER_MODE) { + case self::MODE_GMP: + $quotient = new static(); + $remainder = new static(); + + list($quotient->value, $remainder->value) = gmp_div_qr($this->value, $y->value); + + if (gmp_sign($remainder->value) < 0) { + $remainder->value = gmp_add($remainder->value, gmp_abs($y->value)); + } + + return array($this->_normalize($quotient), $this->_normalize($remainder)); + case self::MODE_BCMATH: + $quotient = new static(); + $remainder = new static(); + + $quotient->value = bcdiv($this->value, $y->value, 0); + $remainder->value = bcmod($this->value, $y->value); + + if ($remainder->value[0] == '-') { + $remainder->value = bcadd($remainder->value, $y->value[0] == '-' ? substr($y->value, 1) : $y->value, 0); + } + + return array($this->_normalize($quotient), $this->_normalize($remainder)); + } + + if (count($y->value) == 1) { + list($q, $r) = $this->_divide_digit($this->value, $y->value[0]); + $quotient = new static(); + $remainder = new static(); + $quotient->value = $q; + $remainder->value = array($r); + $quotient->is_negative = $this->is_negative != $y->is_negative; + return array($this->_normalize($quotient), $this->_normalize($remainder)); + } + + static $zero; + if (!isset($zero)) { + $zero = new static(); + } + + $x = $this->copy(); + $y = $y->copy(); + + $x_sign = $x->is_negative; + $y_sign = $y->is_negative; + + $x->is_negative = $y->is_negative = false; + + $diff = $x->compare($y); + + if (!$diff) { + $temp = new static(); + $temp->value = array(1); + $temp->is_negative = $x_sign != $y_sign; + return array($this->_normalize($temp), $this->_normalize(new static())); + } + + if ($diff < 0) { + // if $x is negative, "add" $y. + if ($x_sign) { + $x = $y->subtract($x); + } + return array($this->_normalize(new static()), $this->_normalize($x)); + } + + // normalize $x and $y as described in HAC 14.23 / 14.24 + $msb = $y->value[count($y->value) - 1]; + for ($shift = 0; !($msb & self::$msb); ++$shift) { + $msb <<= 1; + } + $x->_lshift($shift); + $y->_lshift($shift); + $y_value = &$y->value; + + $x_max = count($x->value) - 1; + $y_max = count($y->value) - 1; + + $quotient = new static(); + $quotient_value = &$quotient->value; + $quotient_value = $this->_array_repeat(0, $x_max - $y_max + 1); + + static $temp, $lhs, $rhs; + if (!isset($temp)) { + $temp = new static(); + $lhs = new static(); + $rhs = new static(); + } + $temp_value = &$temp->value; + $rhs_value = &$rhs->value; + + // $temp = $y << ($x_max - $y_max-1) in base 2**26 + $temp_value = array_merge($this->_array_repeat(0, $x_max - $y_max), $y_value); + + while ($x->compare($temp) >= 0) { + // calculate the "common residue" + ++$quotient_value[$x_max - $y_max]; + $x = $x->subtract($temp); + $x_max = count($x->value) - 1; + } + + for ($i = $x_max; $i >= $y_max + 1; --$i) { + $x_value = &$x->value; + $x_window = array( + isset($x_value[$i]) ? $x_value[$i] : 0, + isset($x_value[$i - 1]) ? $x_value[$i - 1] : 0, + isset($x_value[$i - 2]) ? $x_value[$i - 2] : 0 + ); + $y_window = array( + $y_value[$y_max], + ($y_max > 0) ? $y_value[$y_max - 1] : 0 + ); + + $q_index = $i - $y_max - 1; + if ($x_window[0] == $y_window[0]) { + $quotient_value[$q_index] = self::$maxDigit; + } else { + $quotient_value[$q_index] = $this->_safe_divide( + $x_window[0] * self::$baseFull + $x_window[1], + $y_window[0] + ); + } + + $temp_value = array($y_window[1], $y_window[0]); + + $lhs->value = array($quotient_value[$q_index]); + $lhs = $lhs->multiply($temp); + + $rhs_value = array($x_window[2], $x_window[1], $x_window[0]); + + while ($lhs->compare($rhs) > 0) { + --$quotient_value[$q_index]; + + $lhs->value = array($quotient_value[$q_index]); + $lhs = $lhs->multiply($temp); + } + + $adjust = $this->_array_repeat(0, $q_index); + $temp_value = array($quotient_value[$q_index]); + $temp = $temp->multiply($y); + $temp_value = &$temp->value; + if (count($temp_value)) { + $temp_value = array_merge($adjust, $temp_value); + } + + $x = $x->subtract($temp); + + if ($x->compare($zero) < 0) { + $temp_value = array_merge($adjust, $y_value); + $x = $x->add($temp); + + --$quotient_value[$q_index]; + } + + $x_max = count($x_value) - 1; + } + + // unnormalize the remainder + $x->_rshift($shift); + + $quotient->is_negative = $x_sign != $y_sign; + + // calculate the "common residue", if appropriate + if ($x_sign) { + $y->_rshift($shift); + $x = $y->subtract($x); + } + + return array($this->_normalize($quotient), $this->_normalize($x)); + } + + /** + * Divides a BigInteger by a regular integer + * + * abc / x = a00 / x + b0 / x + c / x + * + * @param array $dividend + * @param array $divisor + * @return array + * @access private + */ + function _divide_digit($dividend, $divisor) + { + $carry = 0; + $result = array(); + + for ($i = count($dividend) - 1; $i >= 0; --$i) { + $temp = self::$baseFull * $carry + $dividend[$i]; + $result[$i] = $this->_safe_divide($temp, $divisor); + $carry = (int) ($temp - $divisor * $result[$i]); + } + + return array($result, $carry); + } + + /** + * Performs modular exponentiation. + * + * Here's an example: + * + * modPow($b, $c); + * + * echo $c->toString(); // outputs 10 + * ?> + * + * + * @param \phpseclib\Math\BigInteger $e + * @param \phpseclib\Math\BigInteger $n + * @return \phpseclib\Math\BigInteger + * @access public + * @internal The most naive approach to modular exponentiation has very unreasonable requirements, and + * and although the approach involving repeated squaring does vastly better, it, too, is impractical + * for our purposes. The reason being that division - by far the most complicated and time-consuming + * of the basic operations (eg. +,-,*,/) - occurs multiple times within it. + * + * Modular reductions resolve this issue. Although an individual modular reduction takes more time + * then an individual division, when performed in succession (with the same modulo), they're a lot faster. + * + * The two most commonly used modular reductions are Barrett and Montgomery reduction. Montgomery reduction, + * although faster, only works when the gcd of the modulo and of the base being used is 1. In RSA, when the + * base is a power of two, the modulo - a product of two primes - is always going to have a gcd of 1 (because + * the product of two odd numbers is odd), but what about when RSA isn't used? + * + * In contrast, Barrett reduction has no such constraint. As such, some bigint implementations perform a + * Barrett reduction after every operation in the modpow function. Others perform Barrett reductions when the + * modulo is even and Montgomery reductions when the modulo is odd. BigInteger.java's modPow method, however, + * uses a trick involving the Chinese Remainder Theorem to factor the even modulo into two numbers - one odd and + * the other, a power of two - and recombine them, later. This is the method that this modPow function uses. + * {@link http://islab.oregonstate.edu/papers/j34monex.pdf Montgomery Reduction with Even Modulus} elaborates. + */ + function modPow($e, $n) + { + $n = $this->bitmask !== false && $this->bitmask->compare($n) < 0 ? $this->bitmask : $n->abs(); + + if ($e->compare(new static()) < 0) { + $e = $e->abs(); + + $temp = $this->modInverse($n); + if ($temp === false) { + return false; + } + + return $this->_normalize($temp->modPow($e, $n)); + } + + if (MATH_BIGINTEGER_MODE == self::MODE_GMP) { + $temp = new static(); + $temp->value = gmp_powm($this->value, $e->value, $n->value); + + return $this->_normalize($temp); + } + + if ($this->compare(new static()) < 0 || $this->compare($n) > 0) { + list(, $temp) = $this->divide($n); + return $temp->modPow($e, $n); + } + + if (defined('MATH_BIGINTEGER_OPENSSL_ENABLED')) { + $components = array( + 'modulus' => $n->toBytes(true), + 'publicExponent' => $e->toBytes(true) + ); + + $components = array( + 'modulus' => pack('Ca*a*', 2, $this->_encodeASN1Length(strlen($components['modulus'])), $components['modulus']), + 'publicExponent' => pack('Ca*a*', 2, $this->_encodeASN1Length(strlen($components['publicExponent'])), $components['publicExponent']) + ); + + $RSAPublicKey = pack( + 'Ca*a*a*', + 48, + $this->_encodeASN1Length(strlen($components['modulus']) + strlen($components['publicExponent'])), + $components['modulus'], + $components['publicExponent'] + ); + + $rsaOID = pack('H*', '300d06092a864886f70d0101010500'); // hex version of MA0GCSqGSIb3DQEBAQUA + $RSAPublicKey = chr(0) . $RSAPublicKey; + $RSAPublicKey = chr(3) . $this->_encodeASN1Length(strlen($RSAPublicKey)) . $RSAPublicKey; + + $encapsulated = pack( + 'Ca*a*', + 48, + $this->_encodeASN1Length(strlen($rsaOID . $RSAPublicKey)), + $rsaOID . $RSAPublicKey + ); + + $RSAPublicKey = "-----BEGIN PUBLIC KEY-----\r\n" . + chunk_split(base64_encode($encapsulated)) . + '-----END PUBLIC KEY-----'; + + $plaintext = str_pad($this->toBytes(), strlen($n->toBytes(true)) - 1, "\0", STR_PAD_LEFT); + + if (openssl_public_encrypt($plaintext, $result, $RSAPublicKey, OPENSSL_NO_PADDING)) { + return new static($result, 256); + } + } + + if (MATH_BIGINTEGER_MODE == self::MODE_BCMATH) { + $temp = new static(); + $temp->value = bcpowmod($this->value, $e->value, $n->value, 0); + + return $this->_normalize($temp); + } + + if (empty($e->value)) { + $temp = new static(); + $temp->value = array(1); + return $this->_normalize($temp); + } + + if ($e->value == array(1)) { + list(, $temp) = $this->divide($n); + return $this->_normalize($temp); + } + + if ($e->value == array(2)) { + $temp = new static(); + $temp->value = $this->_square($this->value); + list(, $temp) = $temp->divide($n); + return $this->_normalize($temp); + } + + return $this->_normalize($this->_slidingWindow($e, $n, self::BARRETT)); + + // the following code, although not callable, can be run independently of the above code + // although the above code performed better in my benchmarks the following could might + // perform better under different circumstances. in lieu of deleting it it's just been + // made uncallable + + // is the modulo odd? + if ($n->value[0] & 1) { + return $this->_normalize($this->_slidingWindow($e, $n, self::MONTGOMERY)); + } + // if it's not, it's even + + // find the lowest set bit (eg. the max pow of 2 that divides $n) + for ($i = 0; $i < count($n->value); ++$i) { + if ($n->value[$i]) { + $temp = decbin($n->value[$i]); + $j = strlen($temp) - strrpos($temp, '1') - 1; + $j+= 26 * $i; + break; + } + } + // at this point, 2^$j * $n/(2^$j) == $n + + $mod1 = $n->copy(); + $mod1->_rshift($j); + $mod2 = new static(); + $mod2->value = array(1); + $mod2->_lshift($j); + + $part1 = ($mod1->value != array(1)) ? $this->_slidingWindow($e, $mod1, self::MONTGOMERY) : new static(); + $part2 = $this->_slidingWindow($e, $mod2, self::POWEROF2); + + $y1 = $mod2->modInverse($mod1); + $y2 = $mod1->modInverse($mod2); + + $result = $part1->multiply($mod2); + $result = $result->multiply($y1); + + $temp = $part2->multiply($mod1); + $temp = $temp->multiply($y2); + + $result = $result->add($temp); + list(, $result) = $result->divide($n); + + return $this->_normalize($result); + } + + /** + * Performs modular exponentiation. + * + * Alias for modPow(). + * + * @param \phpseclib\Math\BigInteger $e + * @param \phpseclib\Math\BigInteger $n + * @return \phpseclib\Math\BigInteger + * @access public + */ + function powMod($e, $n) + { + return $this->modPow($e, $n); + } + + /** + * Sliding Window k-ary Modular Exponentiation + * + * Based on {@link http://www.cacr.math.uwaterloo.ca/hac/about/chap14.pdf#page=27 HAC 14.85} / + * {@link http://math.libtomcrypt.com/files/tommath.pdf#page=210 MPM 7.7}. In a departure from those algorithims, + * however, this function performs a modular reduction after every multiplication and squaring operation. + * As such, this function has the same preconditions that the reductions being used do. + * + * @param \phpseclib\Math\BigInteger $e + * @param \phpseclib\Math\BigInteger $n + * @param int $mode + * @return \phpseclib\Math\BigInteger + * @access private + */ + function _slidingWindow($e, $n, $mode) + { + static $window_ranges = array(7, 25, 81, 241, 673, 1793); // from BigInteger.java's oddModPow function + //static $window_ranges = array(0, 7, 36, 140, 450, 1303, 3529); // from MPM 7.3.1 + + $e_value = $e->value; + $e_length = count($e_value) - 1; + $e_bits = decbin($e_value[$e_length]); + for ($i = $e_length - 1; $i >= 0; --$i) { + $e_bits.= str_pad(decbin($e_value[$i]), self::$base, '0', STR_PAD_LEFT); + } + + $e_length = strlen($e_bits); + + // calculate the appropriate window size. + // $window_size == 3 if $window_ranges is between 25 and 81, for example. + for ($i = 0, $window_size = 1; $i < count($window_ranges) && $e_length > $window_ranges[$i]; ++$window_size, ++$i) { + } + + $n_value = $n->value; + + // precompute $this^0 through $this^$window_size + $powers = array(); + $powers[1] = $this->_prepareReduce($this->value, $n_value, $mode); + $powers[2] = $this->_squareReduce($powers[1], $n_value, $mode); + + // we do every other number since substr($e_bits, $i, $j+1) (see below) is supposed to end + // in a 1. ie. it's supposed to be odd. + $temp = 1 << ($window_size - 1); + for ($i = 1; $i < $temp; ++$i) { + $i2 = $i << 1; + $powers[$i2 + 1] = $this->_multiplyReduce($powers[$i2 - 1], $powers[2], $n_value, $mode); + } + + $result = array(1); + $result = $this->_prepareReduce($result, $n_value, $mode); + + for ($i = 0; $i < $e_length;) { + if (!$e_bits[$i]) { + $result = $this->_squareReduce($result, $n_value, $mode); + ++$i; + } else { + for ($j = $window_size - 1; $j > 0; --$j) { + if (!empty($e_bits[$i + $j])) { + break; + } + } + + // eg. the length of substr($e_bits, $i, $j + 1) + for ($k = 0; $k <= $j; ++$k) { + $result = $this->_squareReduce($result, $n_value, $mode); + } + + $result = $this->_multiplyReduce($result, $powers[bindec(substr($e_bits, $i, $j + 1))], $n_value, $mode); + + $i += $j + 1; + } + } + + $temp = new static(); + $temp->value = $this->_reduce($result, $n_value, $mode); + + return $temp; + } + + /** + * Modular reduction + * + * For most $modes this will return the remainder. + * + * @see self::_slidingWindow() + * @access private + * @param array $x + * @param array $n + * @param int $mode + * @return array + */ + function _reduce($x, $n, $mode) + { + switch ($mode) { + case self::MONTGOMERY: + return $this->_montgomery($x, $n); + case self::BARRETT: + return $this->_barrett($x, $n); + case self::POWEROF2: + $lhs = new static(); + $lhs->value = $x; + $rhs = new static(); + $rhs->value = $n; + return $x->_mod2($n); + case self::CLASSIC: + $lhs = new static(); + $lhs->value = $x; + $rhs = new static(); + $rhs->value = $n; + list(, $temp) = $lhs->divide($rhs); + return $temp->value; + case self::NONE: + return $x; + default: + // an invalid $mode was provided + } + } + + /** + * Modular reduction preperation + * + * @see self::_slidingWindow() + * @access private + * @param array $x + * @param array $n + * @param int $mode + * @return array + */ + function _prepareReduce($x, $n, $mode) + { + if ($mode == self::MONTGOMERY) { + return $this->_prepMontgomery($x, $n); + } + return $this->_reduce($x, $n, $mode); + } + + /** + * Modular multiply + * + * @see self::_slidingWindow() + * @access private + * @param array $x + * @param array $y + * @param array $n + * @param int $mode + * @return array + */ + function _multiplyReduce($x, $y, $n, $mode) + { + if ($mode == self::MONTGOMERY) { + return $this->_montgomeryMultiply($x, $y, $n); + } + $temp = $this->_multiply($x, false, $y, false); + return $this->_reduce($temp[self::VALUE], $n, $mode); + } + + /** + * Modular square + * + * @see self::_slidingWindow() + * @access private + * @param array $x + * @param array $n + * @param int $mode + * @return array + */ + function _squareReduce($x, $n, $mode) + { + if ($mode == self::MONTGOMERY) { + return $this->_montgomeryMultiply($x, $x, $n); + } + return $this->_reduce($this->_square($x), $n, $mode); + } + + /** + * Modulos for Powers of Two + * + * Calculates $x%$n, where $n = 2**$e, for some $e. Since this is basically the same as doing $x & ($n-1), + * we'll just use this function as a wrapper for doing that. + * + * @see self::_slidingWindow() + * @access private + * @param \phpseclib\Math\BigInteger $n + * @return \phpseclib\Math\BigInteger + */ + function _mod2($n) + { + $temp = new static(); + $temp->value = array(1); + return $this->bitwise_and($n->subtract($temp)); + } + + /** + * Barrett Modular Reduction + * + * See {@link http://www.cacr.math.uwaterloo.ca/hac/about/chap14.pdf#page=14 HAC 14.3.3} / + * {@link http://math.libtomcrypt.com/files/tommath.pdf#page=165 MPM 6.2.5} for more information. Modified slightly, + * so as not to require negative numbers (initially, this script didn't support negative numbers). + * + * Employs "folding", as described at + * {@link http://www.cosic.esat.kuleuven.be/publications/thesis-149.pdf#page=66 thesis-149.pdf#page=66}. To quote from + * it, "the idea [behind folding] is to find a value x' such that x (mod m) = x' (mod m), with x' being smaller than x." + * + * Unfortunately, the "Barrett Reduction with Folding" algorithm described in thesis-149.pdf is not, as written, all that + * usable on account of (1) its not using reasonable radix points as discussed in + * {@link http://math.libtomcrypt.com/files/tommath.pdf#page=162 MPM 6.2.2} and (2) the fact that, even with reasonable + * radix points, it only works when there are an even number of digits in the denominator. The reason for (2) is that + * (x >> 1) + (x >> 1) != x / 2 + x / 2. If x is even, they're the same, but if x is odd, they're not. See the in-line + * comments for details. + * + * @see self::_slidingWindow() + * @access private + * @param array $n + * @param array $m + * @return array + */ + function _barrett($n, $m) + { + static $cache = array( + self::VARIABLE => array(), + self::DATA => array() + ); + + $m_length = count($m); + + // if ($this->_compare($n, $this->_square($m)) >= 0) { + if (count($n) > 2 * $m_length) { + $lhs = new static(); + $rhs = new static(); + $lhs->value = $n; + $rhs->value = $m; + list(, $temp) = $lhs->divide($rhs); + return $temp->value; + } + + // if (m.length >> 1) + 2 <= m.length then m is too small and n can't be reduced + if ($m_length < 5) { + return $this->_regularBarrett($n, $m); + } + + // n = 2 * m.length + + if (($key = array_search($m, $cache[self::VARIABLE])) === false) { + $key = count($cache[self::VARIABLE]); + $cache[self::VARIABLE][] = $m; + + $lhs = new static(); + $lhs_value = &$lhs->value; + $lhs_value = $this->_array_repeat(0, $m_length + ($m_length >> 1)); + $lhs_value[] = 1; + $rhs = new static(); + $rhs->value = $m; + + list($u, $m1) = $lhs->divide($rhs); + $u = $u->value; + $m1 = $m1->value; + + $cache[self::DATA][] = array( + 'u' => $u, // m.length >> 1 (technically (m.length >> 1) + 1) + 'm1'=> $m1 // m.length + ); + } else { + extract($cache[self::DATA][$key]); + } + + $cutoff = $m_length + ($m_length >> 1); + $lsd = array_slice($n, 0, $cutoff); // m.length + (m.length >> 1) + $msd = array_slice($n, $cutoff); // m.length >> 1 + $lsd = $this->_trim($lsd); + $temp = $this->_multiply($msd, false, $m1, false); + $n = $this->_add($lsd, false, $temp[self::VALUE], false); // m.length + (m.length >> 1) + 1 + + if ($m_length & 1) { + return $this->_regularBarrett($n[self::VALUE], $m); + } + + // (m.length + (m.length >> 1) + 1) - (m.length - 1) == (m.length >> 1) + 2 + $temp = array_slice($n[self::VALUE], $m_length - 1); + // if even: ((m.length >> 1) + 2) + (m.length >> 1) == m.length + 2 + // if odd: ((m.length >> 1) + 2) + (m.length >> 1) == (m.length - 1) + 2 == m.length + 1 + $temp = $this->_multiply($temp, false, $u, false); + // if even: (m.length + 2) - ((m.length >> 1) + 1) = m.length - (m.length >> 1) + 1 + // if odd: (m.length + 1) - ((m.length >> 1) + 1) = m.length - (m.length >> 1) + $temp = array_slice($temp[self::VALUE], ($m_length >> 1) + 1); + // if even: (m.length - (m.length >> 1) + 1) + m.length = 2 * m.length - (m.length >> 1) + 1 + // if odd: (m.length - (m.length >> 1)) + m.length = 2 * m.length - (m.length >> 1) + $temp = $this->_multiply($temp, false, $m, false); + + // at this point, if m had an odd number of digits, we'd be subtracting a 2 * m.length - (m.length >> 1) digit + // number from a m.length + (m.length >> 1) + 1 digit number. ie. there'd be an extra digit and the while loop + // following this comment would loop a lot (hence our calling _regularBarrett() in that situation). + + $result = $this->_subtract($n[self::VALUE], false, $temp[self::VALUE], false); + + while ($this->_compare($result[self::VALUE], $result[self::SIGN], $m, false) >= 0) { + $result = $this->_subtract($result[self::VALUE], $result[self::SIGN], $m, false); + } + + return $result[self::VALUE]; + } + + /** + * (Regular) Barrett Modular Reduction + * + * For numbers with more than four digits BigInteger::_barrett() is faster. The difference between that and this + * is that this function does not fold the denominator into a smaller form. + * + * @see self::_slidingWindow() + * @access private + * @param array $x + * @param array $n + * @return array + */ + function _regularBarrett($x, $n) + { + static $cache = array( + self::VARIABLE => array(), + self::DATA => array() + ); + + $n_length = count($n); + + if (count($x) > 2 * $n_length) { + $lhs = new static(); + $rhs = new static(); + $lhs->value = $x; + $rhs->value = $n; + list(, $temp) = $lhs->divide($rhs); + return $temp->value; + } + + if (($key = array_search($n, $cache[self::VARIABLE])) === false) { + $key = count($cache[self::VARIABLE]); + $cache[self::VARIABLE][] = $n; + $lhs = new static(); + $lhs_value = &$lhs->value; + $lhs_value = $this->_array_repeat(0, 2 * $n_length); + $lhs_value[] = 1; + $rhs = new static(); + $rhs->value = $n; + list($temp, ) = $lhs->divide($rhs); // m.length + $cache[self::DATA][] = $temp->value; + } + + // 2 * m.length - (m.length - 1) = m.length + 1 + $temp = array_slice($x, $n_length - 1); + // (m.length + 1) + m.length = 2 * m.length + 1 + $temp = $this->_multiply($temp, false, $cache[self::DATA][$key], false); + // (2 * m.length + 1) - (m.length - 1) = m.length + 2 + $temp = array_slice($temp[self::VALUE], $n_length + 1); + + // m.length + 1 + $result = array_slice($x, 0, $n_length + 1); + // m.length + 1 + $temp = $this->_multiplyLower($temp, false, $n, false, $n_length + 1); + // $temp == array_slice($temp->_multiply($temp, false, $n, false)->value, 0, $n_length + 1) + + if ($this->_compare($result, false, $temp[self::VALUE], $temp[self::SIGN]) < 0) { + $corrector_value = $this->_array_repeat(0, $n_length + 1); + $corrector_value[count($corrector_value)] = 1; + $result = $this->_add($result, false, $corrector_value, false); + $result = $result[self::VALUE]; + } + + // at this point, we're subtracting a number with m.length + 1 digits from another number with m.length + 1 digits + $result = $this->_subtract($result, false, $temp[self::VALUE], $temp[self::SIGN]); + while ($this->_compare($result[self::VALUE], $result[self::SIGN], $n, false) > 0) { + $result = $this->_subtract($result[self::VALUE], $result[self::SIGN], $n, false); + } + + return $result[self::VALUE]; + } + + /** + * Performs long multiplication up to $stop digits + * + * If you're going to be doing array_slice($product->value, 0, $stop), some cycles can be saved. + * + * @see self::_regularBarrett() + * @param array $x_value + * @param bool $x_negative + * @param array $y_value + * @param bool $y_negative + * @param int $stop + * @return array + * @access private + */ + function _multiplyLower($x_value, $x_negative, $y_value, $y_negative, $stop) + { + $x_length = count($x_value); + $y_length = count($y_value); + + if (!$x_length || !$y_length) { // a 0 is being multiplied + return array( + self::VALUE => array(), + self::SIGN => false + ); + } + + if ($x_length < $y_length) { + $temp = $x_value; + $x_value = $y_value; + $y_value = $temp; + + $x_length = count($x_value); + $y_length = count($y_value); + } + + $product_value = $this->_array_repeat(0, $x_length + $y_length); + + // the following for loop could be removed if the for loop following it + // (the one with nested for loops) initially set $i to 0, but + // doing so would also make the result in one set of unnecessary adds, + // since on the outermost loops first pass, $product->value[$k] is going + // to always be 0 + + $carry = 0; + + for ($j = 0; $j < $x_length; ++$j) { // ie. $i = 0, $k = $i + $temp = $x_value[$j] * $y_value[0] + $carry; // $product_value[$k] == 0 + $carry = self::$base === 26 ? intval($temp / 0x4000000) : ($temp >> 31); + $product_value[$j] = (int) ($temp - self::$baseFull * $carry); + } + + if ($j < $stop) { + $product_value[$j] = $carry; + } + + // the above for loop is what the previous comment was talking about. the + // following for loop is the "one with nested for loops" + + for ($i = 1; $i < $y_length; ++$i) { + $carry = 0; + + for ($j = 0, $k = $i; $j < $x_length && $k < $stop; ++$j, ++$k) { + $temp = $product_value[$k] + $x_value[$j] * $y_value[$i] + $carry; + $carry = self::$base === 26 ? intval($temp / 0x4000000) : ($temp >> 31); + $product_value[$k] = (int) ($temp - self::$baseFull * $carry); + } + + if ($k < $stop) { + $product_value[$k] = $carry; + } + } + + return array( + self::VALUE => $this->_trim($product_value), + self::SIGN => $x_negative != $y_negative + ); + } + + /** + * Montgomery Modular Reduction + * + * ($x->_prepMontgomery($n))->_montgomery($n) yields $x % $n. + * {@link http://math.libtomcrypt.com/files/tommath.pdf#page=170 MPM 6.3} provides insights on how this can be + * improved upon (basically, by using the comba method). gcd($n, 2) must be equal to one for this function + * to work correctly. + * + * @see self::_prepMontgomery() + * @see self::_slidingWindow() + * @access private + * @param array $x + * @param array $n + * @return array + */ + function _montgomery($x, $n) + { + static $cache = array( + self::VARIABLE => array(), + self::DATA => array() + ); + + if (($key = array_search($n, $cache[self::VARIABLE])) === false) { + $key = count($cache[self::VARIABLE]); + $cache[self::VARIABLE][] = $x; + $cache[self::DATA][] = $this->_modInverse67108864($n); + } + + $k = count($n); + + $result = array(self::VALUE => $x); + + for ($i = 0; $i < $k; ++$i) { + $temp = $result[self::VALUE][$i] * $cache[self::DATA][$key]; + $temp = $temp - self::$baseFull * (self::$base === 26 ? intval($temp / 0x4000000) : ($temp >> 31)); + $temp = $this->_regularMultiply(array($temp), $n); + $temp = array_merge($this->_array_repeat(0, $i), $temp); + $result = $this->_add($result[self::VALUE], false, $temp, false); + } + + $result[self::VALUE] = array_slice($result[self::VALUE], $k); + + if ($this->_compare($result, false, $n, false) >= 0) { + $result = $this->_subtract($result[self::VALUE], false, $n, false); + } + + return $result[self::VALUE]; + } + + /** + * Montgomery Multiply + * + * Interleaves the montgomery reduction and long multiplication algorithms together as described in + * {@link http://www.cacr.math.uwaterloo.ca/hac/about/chap14.pdf#page=13 HAC 14.36} + * + * @see self::_prepMontgomery() + * @see self::_montgomery() + * @access private + * @param array $x + * @param array $y + * @param array $m + * @return array + */ + function _montgomeryMultiply($x, $y, $m) + { + $temp = $this->_multiply($x, false, $y, false); + return $this->_montgomery($temp[self::VALUE], $m); + + // the following code, although not callable, can be run independently of the above code + // although the above code performed better in my benchmarks the following could might + // perform better under different circumstances. in lieu of deleting it it's just been + // made uncallable + + static $cache = array( + self::VARIABLE => array(), + self::DATA => array() + ); + + if (($key = array_search($m, $cache[self::VARIABLE])) === false) { + $key = count($cache[self::VARIABLE]); + $cache[self::VARIABLE][] = $m; + $cache[self::DATA][] = $this->_modInverse67108864($m); + } + + $n = max(count($x), count($y), count($m)); + $x = array_pad($x, $n, 0); + $y = array_pad($y, $n, 0); + $m = array_pad($m, $n, 0); + $a = array(self::VALUE => $this->_array_repeat(0, $n + 1)); + for ($i = 0; $i < $n; ++$i) { + $temp = $a[self::VALUE][0] + $x[$i] * $y[0]; + $temp = $temp - self::$baseFull * (self::$base === 26 ? intval($temp / 0x4000000) : ($temp >> 31)); + $temp = $temp * $cache[self::DATA][$key]; + $temp = $temp - self::$baseFull * (self::$base === 26 ? intval($temp / 0x4000000) : ($temp >> 31)); + $temp = $this->_add($this->_regularMultiply(array($x[$i]), $y), false, $this->_regularMultiply(array($temp), $m), false); + $a = $this->_add($a[self::VALUE], false, $temp[self::VALUE], false); + $a[self::VALUE] = array_slice($a[self::VALUE], 1); + } + if ($this->_compare($a[self::VALUE], false, $m, false) >= 0) { + $a = $this->_subtract($a[self::VALUE], false, $m, false); + } + return $a[self::VALUE]; + } + + /** + * Prepare a number for use in Montgomery Modular Reductions + * + * @see self::_montgomery() + * @see self::_slidingWindow() + * @access private + * @param array $x + * @param array $n + * @return array + */ + function _prepMontgomery($x, $n) + { + $lhs = new static(); + $lhs->value = array_merge($this->_array_repeat(0, count($n)), $x); + $rhs = new static(); + $rhs->value = $n; + + list(, $temp) = $lhs->divide($rhs); + return $temp->value; + } + + /** + * Modular Inverse of a number mod 2**26 (eg. 67108864) + * + * Based off of the bnpInvDigit function implemented and justified in the following URL: + * + * {@link http://www-cs-students.stanford.edu/~tjw/jsbn/jsbn.js} + * + * The following URL provides more info: + * + * {@link http://groups.google.com/group/sci.crypt/msg/7a137205c1be7d85} + * + * As for why we do all the bitmasking... strange things can happen when converting from floats to ints. For + * instance, on some computers, var_dump((int) -4294967297) yields int(-1) and on others, it yields + * int(-2147483648). To avoid problems stemming from this, we use bitmasks to guarantee that ints aren't + * auto-converted to floats. The outermost bitmask is present because without it, there's no guarantee that + * the "residue" returned would be the so-called "common residue". We use fmod, in the last step, because the + * maximum possible $x is 26 bits and the maximum $result is 16 bits. Thus, we have to be able to handle up to + * 40 bits, which only 64-bit floating points will support. + * + * Thanks to Pedro Gimeno Fortea for input! + * + * @see self::_montgomery() + * @access private + * @param array $x + * @return int + */ + function _modInverse67108864($x) // 2**26 == 67,108,864 + { + $x = -$x[0]; + $result = $x & 0x3; // x**-1 mod 2**2 + $result = ($result * (2 - $x * $result)) & 0xF; // x**-1 mod 2**4 + $result = ($result * (2 - ($x & 0xFF) * $result)) & 0xFF; // x**-1 mod 2**8 + $result = ($result * ((2 - ($x & 0xFFFF) * $result) & 0xFFFF)) & 0xFFFF; // x**-1 mod 2**16 + $result = fmod($result * (2 - fmod($x * $result, self::$baseFull)), self::$baseFull); // x**-1 mod 2**26 + return $result & self::$maxDigit; + } + + /** + * Calculates modular inverses. + * + * Say you have (30 mod 17 * x mod 17) mod 17 == 1. x can be found using modular inverses. + * + * Here's an example: + * + * modInverse($b); + * echo $c->toString(); // outputs 4 + * + * echo "\r\n"; + * + * $d = $a->multiply($c); + * list(, $d) = $d->divide($b); + * echo $d; // outputs 1 (as per the definition of modular inverse) + * ?> + * + * + * @param \phpseclib\Math\BigInteger $n + * @return \phpseclib\Math\BigInteger|false + * @access public + * @internal See {@link http://www.cacr.math.uwaterloo.ca/hac/about/chap14.pdf#page=21 HAC 14.64} for more information. + */ + function modInverse($n) + { + switch (MATH_BIGINTEGER_MODE) { + case self::MODE_GMP: + $temp = new static(); + $temp->value = gmp_invert($this->value, $n->value); + + return ($temp->value === false) ? false : $this->_normalize($temp); + } + + static $zero, $one; + if (!isset($zero)) { + $zero = new static(); + $one = new static(1); + } + + // $x mod -$n == $x mod $n. + $n = $n->abs(); + + if ($this->compare($zero) < 0) { + $temp = $this->abs(); + $temp = $temp->modInverse($n); + return $this->_normalize($n->subtract($temp)); + } + + extract($this->extendedGCD($n)); + + if (!$gcd->equals($one)) { + return false; + } + + $x = $x->compare($zero) < 0 ? $x->add($n) : $x; + + return $this->compare($zero) < 0 ? $this->_normalize($n->subtract($x)) : $this->_normalize($x); + } + + /** + * Calculates the greatest common divisor and Bezout's identity. + * + * Say you have 693 and 609. The GCD is 21. Bezout's identity states that there exist integers x and y such that + * 693*x + 609*y == 21. In point of fact, there are actually an infinite number of x and y combinations and which + * combination is returned is dependent upon which mode is in use. See + * {@link http://en.wikipedia.org/wiki/B%C3%A9zout%27s_identity Bezout's identity - Wikipedia} for more information. + * + * Here's an example: + * + * extendedGCD($b)); + * + * echo $gcd->toString() . "\r\n"; // outputs 21 + * echo $a->toString() * $x->toString() + $b->toString() * $y->toString(); // outputs 21 + * ?> + * + * + * @param \phpseclib\Math\BigInteger $n + * @return \phpseclib\Math\BigInteger + * @access public + * @internal Calculates the GCD using the binary xGCD algorithim described in + * {@link http://www.cacr.math.uwaterloo.ca/hac/about/chap14.pdf#page=19 HAC 14.61}. As the text above 14.61 notes, + * the more traditional algorithim requires "relatively costly multiple-precision divisions". + */ + function extendedGCD($n) + { + switch (MATH_BIGINTEGER_MODE) { + case self::MODE_GMP: + extract(gmp_gcdext($this->value, $n->value)); + + return array( + 'gcd' => $this->_normalize(new static($g)), + 'x' => $this->_normalize(new static($s)), + 'y' => $this->_normalize(new static($t)) + ); + case self::MODE_BCMATH: + // it might be faster to use the binary xGCD algorithim here, as well, but (1) that algorithim works + // best when the base is a power of 2 and (2) i don't think it'd make much difference, anyway. as is, + // the basic extended euclidean algorithim is what we're using. + + $u = $this->value; + $v = $n->value; + + $a = '1'; + $b = '0'; + $c = '0'; + $d = '1'; + + while (bccomp($v, '0', 0) != 0) { + $q = bcdiv($u, $v, 0); + + $temp = $u; + $u = $v; + $v = bcsub($temp, bcmul($v, $q, 0), 0); + + $temp = $a; + $a = $c; + $c = bcsub($temp, bcmul($a, $q, 0), 0); + + $temp = $b; + $b = $d; + $d = bcsub($temp, bcmul($b, $q, 0), 0); + } + + return array( + 'gcd' => $this->_normalize(new static($u)), + 'x' => $this->_normalize(new static($a)), + 'y' => $this->_normalize(new static($b)) + ); + } + + $y = $n->copy(); + $x = $this->copy(); + $g = new static(); + $g->value = array(1); + + while (!(($x->value[0] & 1)|| ($y->value[0] & 1))) { + $x->_rshift(1); + $y->_rshift(1); + $g->_lshift(1); + } + + $u = $x->copy(); + $v = $y->copy(); + + $a = new static(); + $b = new static(); + $c = new static(); + $d = new static(); + + $a->value = $d->value = $g->value = array(1); + $b->value = $c->value = array(); + + while (!empty($u->value)) { + while (!($u->value[0] & 1)) { + $u->_rshift(1); + if ((!empty($a->value) && ($a->value[0] & 1)) || (!empty($b->value) && ($b->value[0] & 1))) { + $a = $a->add($y); + $b = $b->subtract($x); + } + $a->_rshift(1); + $b->_rshift(1); + } + + while (!($v->value[0] & 1)) { + $v->_rshift(1); + if ((!empty($d->value) && ($d->value[0] & 1)) || (!empty($c->value) && ($c->value[0] & 1))) { + $c = $c->add($y); + $d = $d->subtract($x); + } + $c->_rshift(1); + $d->_rshift(1); + } + + if ($u->compare($v) >= 0) { + $u = $u->subtract($v); + $a = $a->subtract($c); + $b = $b->subtract($d); + } else { + $v = $v->subtract($u); + $c = $c->subtract($a); + $d = $d->subtract($b); + } + } + + return array( + 'gcd' => $this->_normalize($g->multiply($v)), + 'x' => $this->_normalize($c), + 'y' => $this->_normalize($d) + ); + } + + /** + * Calculates the greatest common divisor + * + * Say you have 693 and 609. The GCD is 21. + * + * Here's an example: + * + * extendedGCD($b); + * + * echo $gcd->toString() . "\r\n"; // outputs 21 + * ?> + * + * + * @param \phpseclib\Math\BigInteger $n + * @return \phpseclib\Math\BigInteger + * @access public + */ + function gcd($n) + { + extract($this->extendedGCD($n)); + return $gcd; + } + + /** + * Absolute value. + * + * @return \phpseclib\Math\BigInteger + * @access public + */ + function abs() + { + $temp = new static(); + + switch (MATH_BIGINTEGER_MODE) { + case self::MODE_GMP: + $temp->value = gmp_abs($this->value); + break; + case self::MODE_BCMATH: + $temp->value = (bccomp($this->value, '0', 0) < 0) ? substr($this->value, 1) : $this->value; + break; + default: + $temp->value = $this->value; + } + + return $temp; + } + + /** + * Compares two numbers. + * + * Although one might think !$x->compare($y) means $x != $y, it, in fact, means the opposite. The reason for this is + * demonstrated thusly: + * + * $x > $y: $x->compare($y) > 0 + * $x < $y: $x->compare($y) < 0 + * $x == $y: $x->compare($y) == 0 + * + * Note how the same comparison operator is used. If you want to test for equality, use $x->equals($y). + * + * @param \phpseclib\Math\BigInteger $y + * @return int that is < 0 if $this is less than $y; > 0 if $this is greater than $y, and 0 if they are equal. + * @access public + * @see self::equals() + * @internal Could return $this->subtract($x), but that's not as fast as what we do do. + */ + function compare($y) + { + switch (MATH_BIGINTEGER_MODE) { + case self::MODE_GMP: + $r = gmp_cmp($this->value, $y->value); + if ($r < -1) { + $r = -1; + } + if ($r > 1) { + $r = 1; + } + return $r; + case self::MODE_BCMATH: + return bccomp($this->value, $y->value, 0); + } + + return $this->_compare($this->value, $this->is_negative, $y->value, $y->is_negative); + } + + /** + * Compares two numbers. + * + * @param array $x_value + * @param bool $x_negative + * @param array $y_value + * @param bool $y_negative + * @return int + * @see self::compare() + * @access private + */ + function _compare($x_value, $x_negative, $y_value, $y_negative) + { + if ($x_negative != $y_negative) { + return (!$x_negative && $y_negative) ? 1 : -1; + } + + $result = $x_negative ? -1 : 1; + + if (count($x_value) != count($y_value)) { + return (count($x_value) > count($y_value)) ? $result : -$result; + } + $size = max(count($x_value), count($y_value)); + + $x_value = array_pad($x_value, $size, 0); + $y_value = array_pad($y_value, $size, 0); + + for ($i = count($x_value) - 1; $i >= 0; --$i) { + if ($x_value[$i] != $y_value[$i]) { + return ($x_value[$i] > $y_value[$i]) ? $result : -$result; + } + } + + return 0; + } + + /** + * Tests the equality of two numbers. + * + * If you need to see if one number is greater than or less than another number, use BigInteger::compare() + * + * @param \phpseclib\Math\BigInteger $x + * @return bool + * @access public + * @see self::compare() + */ + function equals($x) + { + switch (MATH_BIGINTEGER_MODE) { + case self::MODE_GMP: + return gmp_cmp($this->value, $x->value) == 0; + default: + return $this->value === $x->value && $this->is_negative == $x->is_negative; + } + } + + /** + * Set Precision + * + * Some bitwise operations give different results depending on the precision being used. Examples include left + * shift, not, and rotates. + * + * @param int $bits + * @access public + */ + function setPrecision($bits) + { + $this->precision = $bits; + if (MATH_BIGINTEGER_MODE != self::MODE_BCMATH) { + $this->bitmask = new static(chr((1 << ($bits & 0x7)) - 1) . str_repeat(chr(0xFF), $bits >> 3), 256); + } else { + $this->bitmask = new static(bcpow('2', $bits, 0)); + } + + $temp = $this->_normalize($this); + $this->value = $temp->value; + } + + /** + * Logical And + * + * @param \phpseclib\Math\BigInteger $x + * @access public + * @internal Implemented per a request by Lluis Pamies i Juarez + * @return \phpseclib\Math\BigInteger + */ + function bitwise_and($x) + { + switch (MATH_BIGINTEGER_MODE) { + case self::MODE_GMP: + $temp = new static(); + $temp->value = gmp_and($this->value, $x->value); + + return $this->_normalize($temp); + case self::MODE_BCMATH: + $left = $this->toBytes(); + $right = $x->toBytes(); + + $length = max(strlen($left), strlen($right)); + + $left = str_pad($left, $length, chr(0), STR_PAD_LEFT); + $right = str_pad($right, $length, chr(0), STR_PAD_LEFT); + + return $this->_normalize(new static($left & $right, 256)); + } + + $result = $this->copy(); + + $length = min(count($x->value), count($this->value)); + + $result->value = array_slice($result->value, 0, $length); + + for ($i = 0; $i < $length; ++$i) { + $result->value[$i]&= $x->value[$i]; + } + + return $this->_normalize($result); + } + + /** + * Logical Or + * + * @param \phpseclib\Math\BigInteger $x + * @access public + * @internal Implemented per a request by Lluis Pamies i Juarez + * @return \phpseclib\Math\BigInteger + */ + function bitwise_or($x) + { + switch (MATH_BIGINTEGER_MODE) { + case self::MODE_GMP: + $temp = new static(); + $temp->value = gmp_or($this->value, $x->value); + + return $this->_normalize($temp); + case self::MODE_BCMATH: + $left = $this->toBytes(); + $right = $x->toBytes(); + + $length = max(strlen($left), strlen($right)); + + $left = str_pad($left, $length, chr(0), STR_PAD_LEFT); + $right = str_pad($right, $length, chr(0), STR_PAD_LEFT); + + return $this->_normalize(new static($left | $right, 256)); + } + + $length = max(count($this->value), count($x->value)); + $result = $this->copy(); + $result->value = array_pad($result->value, $length, 0); + $x->value = array_pad($x->value, $length, 0); + + for ($i = 0; $i < $length; ++$i) { + $result->value[$i]|= $x->value[$i]; + } + + return $this->_normalize($result); + } + + /** + * Logical Exclusive-Or + * + * @param \phpseclib\Math\BigInteger $x + * @access public + * @internal Implemented per a request by Lluis Pamies i Juarez + * @return \phpseclib\Math\BigInteger + */ + function bitwise_xor($x) + { + switch (MATH_BIGINTEGER_MODE) { + case self::MODE_GMP: + $temp = new static(); + $temp->value = gmp_xor(gmp_abs($this->value), gmp_abs($x->value)); + return $this->_normalize($temp); + case self::MODE_BCMATH: + $left = $this->toBytes(); + $right = $x->toBytes(); + + $length = max(strlen($left), strlen($right)); + + $left = str_pad($left, $length, chr(0), STR_PAD_LEFT); + $right = str_pad($right, $length, chr(0), STR_PAD_LEFT); + + return $this->_normalize(new static($left ^ $right, 256)); + } + + $length = max(count($this->value), count($x->value)); + $result = $this->copy(); + $result->is_negative = false; + $result->value = array_pad($result->value, $length, 0); + $x->value = array_pad($x->value, $length, 0); + + for ($i = 0; $i < $length; ++$i) { + $result->value[$i]^= $x->value[$i]; + } + + return $this->_normalize($result); + } + + /** + * Logical Not + * + * @access public + * @internal Implemented per a request by Lluis Pamies i Juarez + * @return \phpseclib\Math\BigInteger + */ + function bitwise_not() + { + // calculuate "not" without regard to $this->precision + // (will always result in a smaller number. ie. ~1 isn't 1111 1110 - it's 0) + $temp = $this->toBytes(); + if ($temp == '') { + return $this->_normalize(new static()); + } + $pre_msb = decbin(ord($temp[0])); + $temp = ~$temp; + $msb = decbin(ord($temp[0])); + if (strlen($msb) == 8) { + $msb = substr($msb, strpos($msb, '0')); + } + $temp[0] = chr(bindec($msb)); + + // see if we need to add extra leading 1's + $current_bits = strlen($pre_msb) + 8 * strlen($temp) - 8; + $new_bits = $this->precision - $current_bits; + if ($new_bits <= 0) { + return $this->_normalize(new static($temp, 256)); + } + + // generate as many leading 1's as we need to. + $leading_ones = chr((1 << ($new_bits & 0x7)) - 1) . str_repeat(chr(0xFF), $new_bits >> 3); + $this->_base256_lshift($leading_ones, $current_bits); + + $temp = str_pad($temp, strlen($leading_ones), chr(0), STR_PAD_LEFT); + + return $this->_normalize(new static($leading_ones | $temp, 256)); + } + + /** + * Logical Right Shift + * + * Shifts BigInteger's by $shift bits, effectively dividing by 2**$shift. + * + * @param int $shift + * @return \phpseclib\Math\BigInteger + * @access public + * @internal The only version that yields any speed increases is the internal version. + */ + function bitwise_rightShift($shift) + { + $temp = new static(); + + switch (MATH_BIGINTEGER_MODE) { + case self::MODE_GMP: + static $two; + + if (!isset($two)) { + $two = gmp_init('2'); + } + + $temp->value = gmp_div_q($this->value, gmp_pow($two, $shift)); + + break; + case self::MODE_BCMATH: + $temp->value = bcdiv($this->value, bcpow('2', $shift, 0), 0); + + break; + default: // could just replace _lshift with this, but then all _lshift() calls would need to be rewritten + // and I don't want to do that... + $temp->value = $this->value; + $temp->_rshift($shift); + } + + return $this->_normalize($temp); + } + + /** + * Logical Left Shift + * + * Shifts BigInteger's by $shift bits, effectively multiplying by 2**$shift. + * + * @param int $shift + * @return \phpseclib\Math\BigInteger + * @access public + * @internal The only version that yields any speed increases is the internal version. + */ + function bitwise_leftShift($shift) + { + $temp = new static(); + + switch (MATH_BIGINTEGER_MODE) { + case self::MODE_GMP: + static $two; + + if (!isset($two)) { + $two = gmp_init('2'); + } + + $temp->value = gmp_mul($this->value, gmp_pow($two, $shift)); + + break; + case self::MODE_BCMATH: + $temp->value = bcmul($this->value, bcpow('2', $shift, 0), 0); + + break; + default: // could just replace _rshift with this, but then all _lshift() calls would need to be rewritten + // and I don't want to do that... + $temp->value = $this->value; + $temp->_lshift($shift); + } + + return $this->_normalize($temp); + } + + /** + * Logical Left Rotate + * + * Instead of the top x bits being dropped they're appended to the shifted bit string. + * + * @param int $shift + * @return \phpseclib\Math\BigInteger + * @access public + */ + function bitwise_leftRotate($shift) + { + $bits = $this->toBytes(); + + if ($this->precision > 0) { + $precision = $this->precision; + if (MATH_BIGINTEGER_MODE == self::MODE_BCMATH) { + $mask = $this->bitmask->subtract(new static(1)); + $mask = $mask->toBytes(); + } else { + $mask = $this->bitmask->toBytes(); + } + } else { + $temp = ord($bits[0]); + for ($i = 0; $temp >> $i; ++$i) { + } + $precision = 8 * strlen($bits) - 8 + $i; + $mask = chr((1 << ($precision & 0x7)) - 1) . str_repeat(chr(0xFF), $precision >> 3); + } + + if ($shift < 0) { + $shift+= $precision; + } + $shift%= $precision; + + if (!$shift) { + return $this->copy(); + } + + $left = $this->bitwise_leftShift($shift); + $left = $left->bitwise_and(new static($mask, 256)); + $right = $this->bitwise_rightShift($precision - $shift); + $result = MATH_BIGINTEGER_MODE != self::MODE_BCMATH ? $left->bitwise_or($right) : $left->add($right); + return $this->_normalize($result); + } + + /** + * Logical Right Rotate + * + * Instead of the bottom x bits being dropped they're prepended to the shifted bit string. + * + * @param int $shift + * @return \phpseclib\Math\BigInteger + * @access public + */ + function bitwise_rightRotate($shift) + { + return $this->bitwise_leftRotate(-$shift); + } + + /** + * Generates a random BigInteger + * + * Byte length is equal to $length. Uses \phpseclib\Crypt\Random if it's loaded and mt_rand if it's not. + * + * @param int $size + * @return \phpseclib\Math\BigInteger + * @access private + */ + function _random_number_helper($size) + { + if (class_exists('\phpseclib\Crypt\Random')) { + $random = Random::string($size); + } else { + $random = ''; + + if ($size & 1) { + $random.= chr(mt_rand(0, 255)); + } + + $blocks = $size >> 1; + for ($i = 0; $i < $blocks; ++$i) { + // mt_rand(-2147483648, 0x7FFFFFFF) always produces -2147483648 on some systems + $random.= pack('n', mt_rand(0, 0xFFFF)); + } + } + + return new static($random, 256); + } + + /** + * Generate a random number + * + * Returns a random number between $min and $max where $min and $max + * can be defined using one of the two methods: + * + * $min->random($max) + * $max->random($min) + * + * @param \phpseclib\Math\BigInteger $arg1 + * @param \phpseclib\Math\BigInteger $arg2 + * @return \phpseclib\Math\BigInteger + * @access public + * @internal The API for creating random numbers used to be $a->random($min, $max), where $a was a BigInteger object. + * That method is still supported for BC purposes. + */ + function random($arg1, $arg2 = false) + { + if ($arg1 === false) { + return false; + } + + if ($arg2 === false) { + $max = $arg1; + $min = $this; + } else { + $min = $arg1; + $max = $arg2; + } + + $compare = $max->compare($min); + + if (!$compare) { + return $this->_normalize($min); + } elseif ($compare < 0) { + // if $min is bigger then $max, swap $min and $max + $temp = $max; + $max = $min; + $min = $temp; + } + + static $one; + if (!isset($one)) { + $one = new static(1); + } + + $max = $max->subtract($min->subtract($one)); + $size = strlen(ltrim($max->toBytes(), chr(0))); + + /* + doing $random % $max doesn't work because some numbers will be more likely to occur than others. + eg. if $max is 140 and $random's max is 255 then that'd mean both $random = 5 and $random = 145 + would produce 5 whereas the only value of random that could produce 139 would be 139. ie. + not all numbers would be equally likely. some would be more likely than others. + + creating a whole new random number until you find one that is within the range doesn't work + because, for sufficiently small ranges, the likelihood that you'd get a number within that range + would be pretty small. eg. with $random's max being 255 and if your $max being 1 the probability + would be pretty high that $random would be greater than $max. + + phpseclib works around this using the technique described here: + + http://crypto.stackexchange.com/questions/5708/creating-a-small-number-from-a-cryptographically-secure-random-string + */ + $random_max = new static(chr(1) . str_repeat("\0", $size), 256); + $random = $this->_random_number_helper($size); + + list($max_multiple) = $random_max->divide($max); + $max_multiple = $max_multiple->multiply($max); + + while ($random->compare($max_multiple) >= 0) { + $random = $random->subtract($max_multiple); + $random_max = $random_max->subtract($max_multiple); + $random = $random->bitwise_leftShift(8); + $random = $random->add($this->_random_number_helper(1)); + $random_max = $random_max->bitwise_leftShift(8); + list($max_multiple) = $random_max->divide($max); + $max_multiple = $max_multiple->multiply($max); + } + list(, $random) = $random->divide($max); + + return $this->_normalize($random->add($min)); + } + + /** + * Generate a random prime number. + * + * If there's not a prime within the given range, false will be returned. + * If more than $timeout seconds have elapsed, give up and return false. + * + * @param \phpseclib\Math\BigInteger $arg1 + * @param \phpseclib\Math\BigInteger $arg2 + * @param int $timeout + * @return Math_BigInteger|false + * @access public + * @internal See {@link http://www.cacr.math.uwaterloo.ca/hac/about/chap4.pdf#page=15 HAC 4.44}. + */ + function randomPrime($arg1, $arg2 = false, $timeout = false) + { + if ($arg1 === false) { + return false; + } + + if ($arg2 === false) { + $max = $arg1; + $min = $this; + } else { + $min = $arg1; + $max = $arg2; + } + + $compare = $max->compare($min); + + if (!$compare) { + return $min->isPrime() ? $min : false; + } elseif ($compare < 0) { + // if $min is bigger then $max, swap $min and $max + $temp = $max; + $max = $min; + $min = $temp; + } + + $length = $max->getLength(); + if ($length > 8196) { + user_error('Generation of random prime numbers larger than 8196 has been disabled'); + } + + static $one, $two; + if (!isset($one)) { + $one = new static(1); + $two = new static(2); + } + + $start = time(); + + $x = $this->random($min, $max); + + // gmp_nextprime() requires PHP 5 >= 5.2.0 per . + if (MATH_BIGINTEGER_MODE == self::MODE_GMP && extension_loaded('gmp')) { + $p = new static(); + $p->value = gmp_nextprime($x->value); + + if ($p->compare($max) <= 0) { + return $p; + } + + if (!$min->equals($x)) { + $x = $x->subtract($one); + } + + return $x->randomPrime($min, $x); + } + + if ($x->equals($two)) { + return $x; + } + + $x->_make_odd(); + if ($x->compare($max) > 0) { + // if $x > $max then $max is even and if $min == $max then no prime number exists between the specified range + if ($min->equals($max)) { + return false; + } + $x = $min->copy(); + $x->_make_odd(); + } + + $initial_x = $x->copy(); + + while (true) { + if ($timeout !== false && time() - $start > $timeout) { + return false; + } + + if ($x->isPrime()) { + return $x; + } + + $x = $x->add($two); + + if ($x->compare($max) > 0) { + $x = $min->copy(); + if ($x->equals($two)) { + return $x; + } + $x->_make_odd(); + } + + if ($x->equals($initial_x)) { + return false; + } + } + } + + /** + * Make the current number odd + * + * If the current number is odd it'll be unchanged. If it's even, one will be added to it. + * + * @see self::randomPrime() + * @access private + */ + function _make_odd() + { + switch (MATH_BIGINTEGER_MODE) { + case self::MODE_GMP: + gmp_setbit($this->value, 0); + break; + case self::MODE_BCMATH: + if ($this->value[strlen($this->value) - 1] % 2 == 0) { + $this->value = bcadd($this->value, '1'); + } + break; + default: + $this->value[0] |= 1; + } + } + + /** + * Checks a numer to see if it's prime + * + * Assuming the $t parameter is not set, this function has an error rate of 2**-80. The main motivation for the + * $t parameter is distributability. BigInteger::randomPrime() can be distributed across multiple pageloads + * on a website instead of just one. + * + * @param \phpseclib\Math\BigInteger $t + * @return bool + * @access public + * @internal Uses the + * {@link http://en.wikipedia.org/wiki/Miller%E2%80%93Rabin_primality_test Miller-Rabin primality test}. See + * {@link http://www.cacr.math.uwaterloo.ca/hac/about/chap4.pdf#page=8 HAC 4.24}. + */ + function isPrime($t = false) + { + $length = $this->getLength(); + // OpenSSL limits RSA keys to 16384 bits. The length of an RSA key is equal to the length of the modulo, which is + // produced by multiplying the primes p and q by one another. The largest number two 8196 bit primes can produce is + // a 16384 bit number so, basically, 8196 bit primes are the largest OpenSSL will generate and if that's the largest + // that it'll generate it also stands to reason that that's the largest you'll be able to test primality on + if ($length > 8196) { + user_error('Primality testing is not supported for numbers larger than 8196 bits'); + } + + if (!$t) { + // see HAC 4.49 "Note (controlling the error probability)" + // @codingStandardsIgnoreStart + if ($length >= 163) { $t = 2; } // floor(1300 / 8) + else if ($length >= 106) { $t = 3; } // floor( 850 / 8) + else if ($length >= 81 ) { $t = 4; } // floor( 650 / 8) + else if ($length >= 68 ) { $t = 5; } // floor( 550 / 8) + else if ($length >= 56 ) { $t = 6; } // floor( 450 / 8) + else if ($length >= 50 ) { $t = 7; } // floor( 400 / 8) + else if ($length >= 43 ) { $t = 8; } // floor( 350 / 8) + else if ($length >= 37 ) { $t = 9; } // floor( 300 / 8) + else if ($length >= 31 ) { $t = 12; } // floor( 250 / 8) + else if ($length >= 25 ) { $t = 15; } // floor( 200 / 8) + else if ($length >= 18 ) { $t = 18; } // floor( 150 / 8) + else { $t = 27; } + // @codingStandardsIgnoreEnd + } + + // ie. gmp_testbit($this, 0) + // ie. isEven() or !isOdd() + switch (MATH_BIGINTEGER_MODE) { + case self::MODE_GMP: + return gmp_prob_prime($this->value, $t) != 0; + case self::MODE_BCMATH: + if ($this->value === '2') { + return true; + } + if ($this->value[strlen($this->value) - 1] % 2 == 0) { + return false; + } + break; + default: + if ($this->value == array(2)) { + return true; + } + if (~$this->value[0] & 1) { + return false; + } + } + + static $primes, $zero, $one, $two; + + if (!isset($primes)) { + $primes = array( + 3, 5, 7, 11, 13, 17, 19, 23, 29, 31, 37, 41, 43, 47, 53, 59, + 61, 67, 71, 73, 79, 83, 89, 97, 101, 103, 107, 109, 113, 127, 131, 137, + 139, 149, 151, 157, 163, 167, 173, 179, 181, 191, 193, 197, 199, 211, 223, 227, + 229, 233, 239, 241, 251, 257, 263, 269, 271, 277, 281, 283, 293, 307, 311, 313, + 317, 331, 337, 347, 349, 353, 359, 367, 373, 379, 383, 389, 397, 401, 409, 419, + 421, 431, 433, 439, 443, 449, 457, 461, 463, 467, 479, 487, 491, 499, 503, 509, + 521, 523, 541, 547, 557, 563, 569, 571, 577, 587, 593, 599, 601, 607, 613, 617, + 619, 631, 641, 643, 647, 653, 659, 661, 673, 677, 683, 691, 701, 709, 719, 727, + 733, 739, 743, 751, 757, 761, 769, 773, 787, 797, 809, 811, 821, 823, 827, 829, + 839, 853, 857, 859, 863, 877, 881, 883, 887, 907, 911, 919, 929, 937, 941, 947, + 953, 967, 971, 977, 983, 991, 997 + ); + + if (MATH_BIGINTEGER_MODE != self::MODE_INTERNAL) { + for ($i = 0; $i < count($primes); ++$i) { + $primes[$i] = new static($primes[$i]); + } + } + + $zero = new static(); + $one = new static(1); + $two = new static(2); + } + + if ($this->equals($one)) { + return false; + } + + // see HAC 4.4.1 "Random search for probable primes" + if (MATH_BIGINTEGER_MODE != self::MODE_INTERNAL) { + foreach ($primes as $prime) { + list(, $r) = $this->divide($prime); + if ($r->equals($zero)) { + return $this->equals($prime); + } + } + } else { + $value = $this->value; + foreach ($primes as $prime) { + list(, $r) = $this->_divide_digit($value, $prime); + if (!$r) { + return count($value) == 1 && $value[0] == $prime; + } + } + } + + $n = $this->copy(); + $n_1 = $n->subtract($one); + $n_2 = $n->subtract($two); + + $r = $n_1->copy(); + $r_value = $r->value; + // ie. $s = gmp_scan1($n, 0) and $r = gmp_div_q($n, gmp_pow(gmp_init('2'), $s)); + if (MATH_BIGINTEGER_MODE == self::MODE_BCMATH) { + $s = 0; + // if $n was 1, $r would be 0 and this would be an infinite loop, hence our $this->equals($one) check earlier + while ($r->value[strlen($r->value) - 1] % 2 == 0) { + $r->value = bcdiv($r->value, '2', 0); + ++$s; + } + } else { + for ($i = 0, $r_length = count($r_value); $i < $r_length; ++$i) { + $temp = ~$r_value[$i] & 0xFFFFFF; + for ($j = 1; ($temp >> $j) & 1; ++$j) { + } + if ($j != 25) { + break; + } + } + $s = 26 * $i + $j; + $r->_rshift($s); + } + + for ($i = 0; $i < $t; ++$i) { + $a = $this->random($two, $n_2); + $y = $a->modPow($r, $n); + + if (!$y->equals($one) && !$y->equals($n_1)) { + for ($j = 1; $j < $s && !$y->equals($n_1); ++$j) { + $y = $y->modPow($two, $n); + if ($y->equals($one)) { + return false; + } + } + + if (!$y->equals($n_1)) { + return false; + } + } + } + return true; + } + + /** + * Logical Left Shift + * + * Shifts BigInteger's by $shift bits. + * + * @param int $shift + * @access private + */ + function _lshift($shift) + { + if ($shift == 0) { + return; + } + + $num_digits = (int) ($shift / self::$base); + $shift %= self::$base; + $shift = 1 << $shift; + + $carry = 0; + + for ($i = 0; $i < count($this->value); ++$i) { + $temp = $this->value[$i] * $shift + $carry; + $carry = self::$base === 26 ? intval($temp / 0x4000000) : ($temp >> 31); + $this->value[$i] = (int) ($temp - $carry * self::$baseFull); + } + + if ($carry) { + $this->value[count($this->value)] = $carry; + } + + while ($num_digits--) { + array_unshift($this->value, 0); + } + } + + /** + * Logical Right Shift + * + * Shifts BigInteger's by $shift bits. + * + * @param int $shift + * @access private + */ + function _rshift($shift) + { + if ($shift == 0) { + return; + } + + $num_digits = (int) ($shift / self::$base); + $shift %= self::$base; + $carry_shift = self::$base - $shift; + $carry_mask = (1 << $shift) - 1; + + if ($num_digits) { + $this->value = array_slice($this->value, $num_digits); + } + + $carry = 0; + + for ($i = count($this->value) - 1; $i >= 0; --$i) { + $temp = $this->value[$i] >> $shift | $carry; + $carry = ($this->value[$i] & $carry_mask) << $carry_shift; + $this->value[$i] = $temp; + } + + $this->value = $this->_trim($this->value); + } + + /** + * Normalize + * + * Removes leading zeros and truncates (if necessary) to maintain the appropriate precision + * + * @param \phpseclib\Math\BigInteger $result + * @return \phpseclib\Math\BigInteger + * @see self::_trim() + * @access private + */ + function _normalize($result) + { + $result->precision = $this->precision; + $result->bitmask = $this->bitmask; + + switch (MATH_BIGINTEGER_MODE) { + case self::MODE_GMP: + if ($this->bitmask !== false) { + $flip = gmp_cmp($result->value, gmp_init(0)) < 0; + if ($flip) { + $result->value = gmp_neg($result->value); + } + $result->value = gmp_and($result->value, $result->bitmask->value); + if ($flip) { + $result->value = gmp_neg($result->value); + } + } + + return $result; + case self::MODE_BCMATH: + if (!empty($result->bitmask->value)) { + $result->value = bcmod($result->value, $result->bitmask->value); + } + + return $result; + } + + $value = &$result->value; + + if (!count($value)) { + $result->is_negative = false; + return $result; + } + + $value = $this->_trim($value); + + if (!empty($result->bitmask->value)) { + $length = min(count($value), count($this->bitmask->value)); + $value = array_slice($value, 0, $length); + + for ($i = 0; $i < $length; ++$i) { + $value[$i] = $value[$i] & $this->bitmask->value[$i]; + } + } + + return $result; + } + + /** + * Trim + * + * Removes leading zeros + * + * @param array $value + * @return \phpseclib\Math\BigInteger + * @access private + */ + function _trim($value) + { + for ($i = count($value) - 1; $i >= 0; --$i) { + if ($value[$i]) { + break; + } + unset($value[$i]); + } + + return $value; + } + + /** + * Array Repeat + * + * @param array $input + * @param mixed $multiplier + * @return array + * @access private + */ + function _array_repeat($input, $multiplier) + { + return ($multiplier) ? array_fill(0, $multiplier, $input) : array(); + } + + /** + * Logical Left Shift + * + * Shifts binary strings $shift bits, essentially multiplying by 2**$shift. + * + * @param string $x (by reference) + * @param int $shift + * @return string + * @access private + */ + function _base256_lshift(&$x, $shift) + { + if ($shift == 0) { + return; + } + + $num_bytes = $shift >> 3; // eg. floor($shift/8) + $shift &= 7; // eg. $shift % 8 + + $carry = 0; + for ($i = strlen($x) - 1; $i >= 0; --$i) { + $temp = ord($x[$i]) << $shift | $carry; + $x[$i] = chr($temp); + $carry = $temp >> 8; + } + $carry = ($carry != 0) ? chr($carry) : ''; + $x = $carry . $x . str_repeat(chr(0), $num_bytes); + } + + /** + * Logical Right Shift + * + * Shifts binary strings $shift bits, essentially dividing by 2**$shift and returning the remainder. + * + * @param string $x (by referenc) + * @param int $shift + * @return string + * @access private + */ + function _base256_rshift(&$x, $shift) + { + if ($shift == 0) { + $x = ltrim($x, chr(0)); + return ''; + } + + $num_bytes = $shift >> 3; // eg. floor($shift/8) + $shift &= 7; // eg. $shift % 8 + + $remainder = ''; + if ($num_bytes) { + $start = $num_bytes > strlen($x) ? -strlen($x) : -$num_bytes; + $remainder = substr($x, $start); + $x = substr($x, 0, -$num_bytes); + } + + $carry = 0; + $carry_shift = 8 - $shift; + for ($i = 0; $i < strlen($x); ++$i) { + $temp = (ord($x[$i]) >> $shift) | $carry; + $carry = (ord($x[$i]) << $carry_shift) & 0xFF; + $x[$i] = chr($temp); + } + $x = ltrim($x, chr(0)); + + $remainder = chr($carry >> $carry_shift) . $remainder; + + return ltrim($remainder, chr(0)); + } + + // one quirk about how the following functions are implemented is that PHP defines N to be an unsigned long + // at 32-bits, while java's longs are 64-bits. + + /** + * Converts 32-bit integers to bytes. + * + * @param int $x + * @return string + * @access private + */ + function _int2bytes($x) + { + return ltrim(pack('N', $x), chr(0)); + } + + /** + * Converts bytes to 32-bit integers + * + * @param string $x + * @return int + * @access private + */ + function _bytes2int($x) + { + $temp = unpack('Nint', str_pad($x, 4, chr(0), STR_PAD_LEFT)); + return $temp['int']; + } + + /** + * DER-encode an integer + * + * The ability to DER-encode integers is needed to create RSA public keys for use with OpenSSL + * + * @see self::modPow() + * @access private + * @param int $length + * @return string + */ + function _encodeASN1Length($length) + { + if ($length <= 0x7F) { + return chr($length); + } + + $temp = ltrim(pack('N', $length), chr(0)); + return pack('Ca*', 0x80 | strlen($temp), $temp); + } + + /** + * Single digit division + * + * Even if int64 is being used the division operator will return a float64 value + * if the dividend is not evenly divisible by the divisor. Since a float64 doesn't + * have the precision of int64 this is a problem so, when int64 is being used, + * we'll guarantee that the dividend is divisible by first subtracting the remainder. + * + * @access private + * @param int $x + * @param int $y + * @return int + */ + function _safe_divide($x, $y) + { + if (self::$base === 26) { + return (int) ($x / $y); + } + + // self::$base === 31 + return ($x - ($x % $y)) / $y; + } +} diff --git a/3rdparty/phpseclib/phpseclib/phpseclib/Net/SCP.php b/3rdparty/phpseclib/phpseclib/phpseclib/Net/SCP.php new file mode 100644 index 00000000..ee6e1c9d --- /dev/null +++ b/3rdparty/phpseclib/phpseclib/phpseclib/Net/SCP.php @@ -0,0 +1,349 @@ + + * login('username', 'password')) { + * exit('bad login'); + * } + * $scp = new \phpseclib\Net\SCP($ssh); + * + * $scp->put('abcd', str_repeat('x', 1024*1024)); + * ?> + * + * + * @category Net + * @package SCP + * @author Jim Wigginton + * @copyright 2010 Jim Wigginton + * @license http://www.opensource.org/licenses/mit-license.html MIT License + * @link http://phpseclib.sourceforge.net + */ + +namespace phpseclib\Net; + +/** + * Pure-PHP implementations of SCP. + * + * @package SCP + * @author Jim Wigginton + * @access public + */ +class SCP +{ + /**#@+ + * @access public + * @see \phpseclib\Net\SCP::put() + */ + /** + * Reads data from a local file. + */ + const SOURCE_LOCAL_FILE = 1; + /** + * Reads data from a string. + */ + const SOURCE_STRING = 2; + /**#@-*/ + + /**#@+ + * @access private + * @see \phpseclib\Net\SCP::_send() + * @see \phpseclib\Net\SCP::_receive() + */ + /** + * SSH1 is being used. + */ + const MODE_SSH1 = 1; + /** + * SSH2 is being used. + */ + const MODE_SSH2 = 2; + /**#@-*/ + + /** + * SSH Object + * + * @var object + * @access private + */ + var $ssh; + + /** + * Packet Size + * + * @var int + * @access private + */ + var $packet_size; + + /** + * Mode + * + * @var int + * @access private + */ + var $mode; + + /** + * Default Constructor. + * + * Connects to an SSH server + * + * @param \phpseclib\Net\SSH1|\phpseclib\Net\SSH2 $ssh + * @return \phpseclib\Net\SCP + * @access public + */ + function __construct($ssh) + { + if ($ssh instanceof SSH2) { + $this->mode = self::MODE_SSH2; + } elseif ($ssh instanceof SSH1) { + $this->packet_size = 50000; + $this->mode = self::MODE_SSH1; + } else { + return; + } + + $this->ssh = $ssh; + } + + /** + * Uploads a file to the SCP server. + * + * By default, \phpseclib\Net\SCP::put() does not read from the local filesystem. $data is dumped directly into $remote_file. + * So, for example, if you set $data to 'filename.ext' and then do \phpseclib\Net\SCP::get(), you will get a file, twelve bytes + * long, containing 'filename.ext' as its contents. + * + * Setting $mode to self::SOURCE_LOCAL_FILE will change the above behavior. With self::SOURCE_LOCAL_FILE, $remote_file will + * contain as many bytes as filename.ext does on your local filesystem. If your filename.ext is 1MB then that is how + * large $remote_file will be, as well. + * + * Currently, only binary mode is supported. As such, if the line endings need to be adjusted, you will need to take + * care of that, yourself. + * + * @param string $remote_file + * @param string $data + * @param int $mode + * @param callable $callback + * @return bool + * @access public + */ + function put($remote_file, $data, $mode = self::SOURCE_STRING, $callback = null) + { + if (!isset($this->ssh)) { + return false; + } + + if (empty($remote_file)) { + user_error('remote_file cannot be blank', E_USER_NOTICE); + return false; + } + + if (!$this->ssh->exec('scp -t ' . escapeshellarg($remote_file), false)) { // -t = to + return false; + } + + $temp = $this->_receive(); + if ($temp !== chr(0)) { + return false; + } + + if ($this->mode == self::MODE_SSH2) { + $this->packet_size = $this->ssh->packet_size_client_to_server[SSH2::CHANNEL_EXEC] - 4; + } + + $remote_file = basename($remote_file); + + if ($mode == self::SOURCE_STRING) { + $size = strlen($data); + } else { + if (!is_file($data)) { + user_error("$data is not a valid file", E_USER_NOTICE); + return false; + } + + $fp = @fopen($data, 'rb'); + if (!$fp) { + return false; + } + $size = filesize($data); + } + + $this->_send('C0644 ' . $size . ' ' . $remote_file . "\n"); + + $temp = $this->_receive(); + if ($temp !== chr(0)) { + return false; + } + + $sent = 0; + while ($sent < $size) { + $temp = $mode & self::SOURCE_STRING ? substr($data, $sent, $this->packet_size) : fread($fp, $this->packet_size); + $this->_send($temp); + $sent+= strlen($temp); + + if (is_callable($callback)) { + call_user_func($callback, $sent); + } + } + $this->_close(); + + if ($mode != self::SOURCE_STRING) { + fclose($fp); + } + + return true; + } + + /** + * Downloads a file from the SCP server. + * + * Returns a string containing the contents of $remote_file if $local_file is left undefined or a boolean false if + * the operation was unsuccessful. If $local_file is defined, returns true or false depending on the success of the + * operation + * + * @param string $remote_file + * @param string $local_file + * @return mixed + * @access public + */ + function get($remote_file, $local_file = false) + { + if (!isset($this->ssh)) { + return false; + } + + if (!$this->ssh->exec('scp -f ' . escapeshellarg($remote_file), false)) { // -f = from + return false; + } + + $this->_send("\0"); + + if (!preg_match('#(?[^ ]+) (?\d+) (?.+)#', rtrim($this->_receive()), $info)) { + return false; + } + + $this->_send("\0"); + + $size = 0; + + if ($local_file !== false) { + $fp = @fopen($local_file, 'wb'); + if (!$fp) { + return false; + } + } + + $content = ''; + while ($size < $info['size']) { + $data = $this->_receive(); + + // Terminate the loop in case the server repeatedly sends an empty response + if ($data === false) { + user_error('No data received from server', E_USER_NOTICE); + return false; + } + + // SCP usually seems to split stuff out into 16k chunks + $size+= strlen($data); + + if ($local_file === false) { + $content.= $data; + } else { + fputs($fp, $data); + } + } + + $this->_close(); + + if ($local_file !== false) { + fclose($fp); + return true; + } + + return $content; + } + + /** + * Sends a packet to an SSH server + * + * @param string $data + * @access private + */ + function _send($data) + { + switch ($this->mode) { + case self::MODE_SSH2: + $this->ssh->_send_channel_packet(SSH2::CHANNEL_EXEC, $data); + break; + case self::MODE_SSH1: + $data = pack('CNa*', NET_SSH1_CMSG_STDIN_DATA, strlen($data), $data); + $this->ssh->_send_binary_packet($data); + } + } + + /** + * Receives a packet from an SSH server + * + * @return string + * @access private + */ + function _receive() + { + switch ($this->mode) { + case self::MODE_SSH2: + return $this->ssh->_get_channel_packet(SSH2::CHANNEL_EXEC, true); + case self::MODE_SSH1: + if (!$this->ssh->bitmap) { + return false; + } + while (true) { + $response = $this->ssh->_get_binary_packet(); + switch ($response[SSH1::RESPONSE_TYPE]) { + case NET_SSH1_SMSG_STDOUT_DATA: + if (strlen($response[SSH1::RESPONSE_DATA]) < 4) { + return false; + } + extract(unpack('Nlength', $response[SSH1::RESPONSE_DATA])); + return $this->ssh->_string_shift($response[SSH1::RESPONSE_DATA], $length); + case NET_SSH1_SMSG_STDERR_DATA: + break; + case NET_SSH1_SMSG_EXITSTATUS: + $this->ssh->_send_binary_packet(chr(NET_SSH1_CMSG_EXIT_CONFIRMATION)); + fclose($this->ssh->fsock); + $this->ssh->bitmap = 0; + return false; + default: + user_error('Unknown packet received', E_USER_NOTICE); + return false; + } + } + } + } + + /** + * Closes the connection to an SSH server + * + * @access private + */ + function _close() + { + switch ($this->mode) { + case self::MODE_SSH2: + $this->ssh->_close_channel(SSH2::CHANNEL_EXEC, true); + break; + case self::MODE_SSH1: + $this->ssh->disconnect(); + } + } +} diff --git a/3rdparty/phpseclib/phpseclib/phpseclib/Net/SFTP.php b/3rdparty/phpseclib/phpseclib/phpseclib/Net/SFTP.php new file mode 100644 index 00000000..28b56806 --- /dev/null +++ b/3rdparty/phpseclib/phpseclib/phpseclib/Net/SFTP.php @@ -0,0 +1,3898 @@ + + * login('username', 'password')) { + * exit('Login Failed'); + * } + * + * echo $sftp->pwd() . "\r\n"; + * $sftp->put('filename.ext', 'hello, world!'); + * print_r($sftp->nlist()); + * ?> + * + * + * @category Net + * @package SFTP + * @author Jim Wigginton + * @copyright 2009 Jim Wigginton + * @license http://www.opensource.org/licenses/mit-license.html MIT License + * @link http://phpseclib.sourceforge.net + */ + +namespace phpseclib\Net; + +/** + * Pure-PHP implementations of SFTP. + * + * @package SFTP + * @author Jim Wigginton + * @access public + */ +class SFTP extends SSH2 +{ + /** + * SFTP channel constant + * + * \phpseclib\Net\SSH2::exec() uses 0 and \phpseclib\Net\SSH2::read() / \phpseclib\Net\SSH2::write() use 1. + * + * @see \phpseclib\Net\SSH2::_send_channel_packet() + * @see \phpseclib\Net\SSH2::_get_channel_packet() + * @access private + */ + const CHANNEL = 0x100; + + /**#@+ + * @access public + * @see \phpseclib\Net\SFTP::put() + */ + /** + * Reads data from a local file. + */ + const SOURCE_LOCAL_FILE = 1; + /** + * Reads data from a string. + */ + // this value isn't really used anymore but i'm keeping it reserved for historical reasons + const SOURCE_STRING = 2; + /** + * Reads data from callback: + * function callback($length) returns string to proceed, null for EOF + */ + const SOURCE_CALLBACK = 16; + /** + * Resumes an upload + */ + const RESUME = 4; + /** + * Append a local file to an already existing remote file + */ + const RESUME_START = 8; + /**#@-*/ + + /** + * Packet Types + * + * @see self::__construct() + * @var array + * @access private + */ + var $packet_types = array(); + + /** + * Status Codes + * + * @see self::__construct() + * @var array + * @access private + */ + var $status_codes = array(); + + /** + * The Request ID + * + * The request ID exists in the off chance that a packet is sent out-of-order. Of course, this library doesn't support + * concurrent actions, so it's somewhat academic, here. + * + * @var boolean + * @see self::_send_sftp_packet() + * @access private + */ + var $use_request_id = false; + + /** + * The Packet Type + * + * The request ID exists in the off chance that a packet is sent out-of-order. Of course, this library doesn't support + * concurrent actions, so it's somewhat academic, here. + * + * @var int + * @see self::_get_sftp_packet() + * @access private + */ + var $packet_type = -1; + + /** + * Packet Buffer + * + * @var string + * @see self::_get_sftp_packet() + * @access private + */ + var $packet_buffer = ''; + + /** + * Extensions supported by the server + * + * @var array + * @see self::_initChannel() + * @access private + */ + var $extensions = array(); + + /** + * Server SFTP version + * + * @var int + * @see self::_initChannel() + * @access private + */ + var $version; + + /** + * Default Server SFTP version + * + * @var int + * @see self::_initChannel() + * @access private + */ + var $defaultVersion; + + /** + * Preferred SFTP version + * + * @var int + * @see self::_initChannel() + * @access private + */ + var $preferredVersion = 3; + + /** + * Current working directory + * + * @var string + * @see self::realpath() + * @see self::chdir() + * @access private + */ + var $pwd = false; + + /** + * Packet Type Log + * + * @see self::getLog() + * @var array + * @access private + */ + var $packet_type_log = array(); + + /** + * Packet Log + * + * @see self::getLog() + * @var array + * @access private + */ + var $packet_log = array(); + + /** + * Error information + * + * @see self::getSFTPErrors() + * @see self::getLastSFTPError() + * @var array + * @access private + */ + var $sftp_errors = array(); + + /** + * Stat Cache + * + * Rather than always having to open a directory and close it immediately there after to see if a file is a directory + * we'll cache the results. + * + * @see self::_update_stat_cache() + * @see self::_remove_from_stat_cache() + * @see self::_query_stat_cache() + * @var array + * @access private + */ + var $stat_cache = array(); + + /** + * Max SFTP Packet Size + * + * @see self::__construct() + * @see self::get() + * @var array + * @access private + */ + var $max_sftp_packet; + + /** + * Stat Cache Flag + * + * @see self::disableStatCache() + * @see self::enableStatCache() + * @var bool + * @access private + */ + var $use_stat_cache = true; + + /** + * Sort Options + * + * @see self::_comparator() + * @see self::setListOrder() + * @var array + * @access private + */ + var $sortOptions = array(); + + /** + * Canonicalization Flag + * + * Determines whether or not paths should be canonicalized before being + * passed on to the remote server. + * + * @see self::enablePathCanonicalization() + * @see self::disablePathCanonicalization() + * @see self::realpath() + * @var bool + * @access private + */ + var $canonicalize_paths = true; + + /** + * Request Buffers + * + * @see self::_get_sftp_packet() + * @var array + * @access private + */ + var $requestBuffer = array(); + + /** + * Preserve timestamps on file downloads / uploads + * + * @see self::get() + * @see self::put() + * @var bool + * @access private + */ + var $preserveTime = false; + + /** + * Arbitrary Length Packets Flag + * + * Determines whether or not packets of any length should be allowed, + * in cases where the server chooses the packet length (such as + * directory listings). By default, packets are only allowed to be + * 256 * 1024 bytes (SFTP_MAX_MSG_LENGTH from OpenSSH's sftp-common.h) + * + * @see self::enableArbitraryLengthPackets() + * @see self::_get_sftp_packet() + * @var bool + * @access private + */ + var $allow_arbitrary_length_packets = false; + + /** + * Was the last packet due to the channels being closed or not? + * + * @see self::get() + * @see self::get_sftp_packet() + * @var bool + * @access private + */ + var $channel_close = false; + + /** + * Has the SFTP channel been partially negotiated? + * + * @var bool + * @access private + */ + var $partial_init = false; + + /** + * http://tools.ietf.org/html/draft-ietf-secsh-filexfer-13#section-7.1 + * the order, in this case, matters quite a lot - see \phpseclib3\Net\SFTP::_parseAttributes() to understand why + * + * @var array + * @access private + */ + var $attributes = array(); + + /** + * @var array + * @access private + */ + var $open_flags = array(); + + /** + * SFTPv5+ changed the flags up: + * https://datatracker.ietf.org/doc/html/draft-ietf-secsh-filexfer-13#section-8.1.1.3 + * + * @var array + * @access private + */ + var $open_flags5 = array(); + + /** + * http://tools.ietf.org/html/draft-ietf-secsh-filexfer-04#section-5.2 + * see \phpseclib\Net\SFTP::_parseLongname() for an explanation + * + * @var array + */ + var $file_types = array(); + + /** + * Default Constructor. + * + * Connects to an SFTP server + * + * @param string $host + * @param int $port + * @param int $timeout + * @return \phpseclib\Net\SFTP + * @access public + */ + function __construct($host, $port = 22, $timeout = 10) + { + parent::__construct($host, $port, $timeout); + + $this->max_sftp_packet = 1 << 15; + + $this->packet_types = array( + 1 => 'NET_SFTP_INIT', + 2 => 'NET_SFTP_VERSION', + 3 => 'NET_SFTP_OPEN', + 4 => 'NET_SFTP_CLOSE', + 5 => 'NET_SFTP_READ', + 6 => 'NET_SFTP_WRITE', + 7 => 'NET_SFTP_LSTAT', + 9 => 'NET_SFTP_SETSTAT', + 10 => 'NET_SFTP_FSETSTAT', + 11 => 'NET_SFTP_OPENDIR', + 12 => 'NET_SFTP_READDIR', + 13 => 'NET_SFTP_REMOVE', + 14 => 'NET_SFTP_MKDIR', + 15 => 'NET_SFTP_RMDIR', + 16 => 'NET_SFTP_REALPATH', + 17 => 'NET_SFTP_STAT', + 18 => 'NET_SFTP_RENAME', + 19 => 'NET_SFTP_READLINK', + 20 => 'NET_SFTP_SYMLINK', + 21 => 'NET_SFTP_LINK', + + 101=> 'NET_SFTP_STATUS', + 102=> 'NET_SFTP_HANDLE', + 103=> 'NET_SFTP_DATA', + 104=> 'NET_SFTP_NAME', + 105=> 'NET_SFTP_ATTRS', + + 200=> 'NET_SFTP_EXTENDED' + ); + $this->status_codes = array( + 0 => 'NET_SFTP_STATUS_OK', + 1 => 'NET_SFTP_STATUS_EOF', + 2 => 'NET_SFTP_STATUS_NO_SUCH_FILE', + 3 => 'NET_SFTP_STATUS_PERMISSION_DENIED', + 4 => 'NET_SFTP_STATUS_FAILURE', + 5 => 'NET_SFTP_STATUS_BAD_MESSAGE', + 6 => 'NET_SFTP_STATUS_NO_CONNECTION', + 7 => 'NET_SFTP_STATUS_CONNECTION_LOST', + 8 => 'NET_SFTP_STATUS_OP_UNSUPPORTED', + 9 => 'NET_SFTP_STATUS_INVALID_HANDLE', + 10 => 'NET_SFTP_STATUS_NO_SUCH_PATH', + 11 => 'NET_SFTP_STATUS_FILE_ALREADY_EXISTS', + 12 => 'NET_SFTP_STATUS_WRITE_PROTECT', + 13 => 'NET_SFTP_STATUS_NO_MEDIA', + 14 => 'NET_SFTP_STATUS_NO_SPACE_ON_FILESYSTEM', + 15 => 'NET_SFTP_STATUS_QUOTA_EXCEEDED', + 16 => 'NET_SFTP_STATUS_UNKNOWN_PRINCIPAL', + 17 => 'NET_SFTP_STATUS_LOCK_CONFLICT', + 18 => 'NET_SFTP_STATUS_DIR_NOT_EMPTY', + 19 => 'NET_SFTP_STATUS_NOT_A_DIRECTORY', + 20 => 'NET_SFTP_STATUS_INVALID_FILENAME', + 21 => 'NET_SFTP_STATUS_LINK_LOOP', + 22 => 'NET_SFTP_STATUS_CANNOT_DELETE', + 23 => 'NET_SFTP_STATUS_INVALID_PARAMETER', + 24 => 'NET_SFTP_STATUS_FILE_IS_A_DIRECTORY', + 25 => 'NET_SFTP_STATUS_BYTE_RANGE_LOCK_CONFLICT', + 26 => 'NET_SFTP_STATUS_BYTE_RANGE_LOCK_REFUSED', + 27 => 'NET_SFTP_STATUS_DELETE_PENDING', + 28 => 'NET_SFTP_STATUS_FILE_CORRUPT', + 29 => 'NET_SFTP_STATUS_OWNER_INVALID', + 30 => 'NET_SFTP_STATUS_GROUP_INVALID', + 31 => 'NET_SFTP_STATUS_NO_MATCHING_BYTE_RANGE_LOCK' + ); + // http://tools.ietf.org/html/draft-ietf-secsh-filexfer-13#section-7.1 + // the order, in this case, matters quite a lot - see \phpseclib\Net\SFTP::_parseAttributes() to understand why + $this->attributes = array( + 0x00000001 => 'NET_SFTP_ATTR_SIZE', + 0x00000002 => 'NET_SFTP_ATTR_UIDGID', // defined in SFTPv3, removed in SFTPv4+ + 0x00000080 => 'NET_SFTP_ATTR_OWNERGROUP', // defined in SFTPv4+ + 0x00000004 => 'NET_SFTP_ATTR_PERMISSIONS', + 0x00000008 => 'NET_SFTP_ATTR_ACCESSTIME', + 0x00000010 => 'NET_SFTP_ATTR_CREATETIME', // SFTPv4+ + 0x00000020 => 'NET_SFTP_ATTR_MODIFYTIME', + 0x00000040 => 'NET_SFTP_ATTR_ACL', + 0x00000100 => 'NET_SFTP_ATTR_SUBSECOND_TIMES', + 0x00000200 => 'NET_SFTP_ATTR_BITS', // SFTPv5+ + 0x00000400 => 'NET_SFTP_ATTR_ALLOCATION_SIZE', // SFTPv6+ + 0x00000800 => 'NET_SFTP_ATTR_TEXT_HINT', + 0x00001000 => 'NET_SFTP_ATTR_MIME_TYPE', + 0x00002000 => 'NET_SFTP_ATTR_LINK_COUNT', + 0x00004000 => 'NET_SFTP_ATTR_UNTRANSLATED_NAME', + 0x00008000 => 'NET_SFTP_ATTR_CTIME', + // 0x80000000 will yield a floating point on 32-bit systems and converting floating points to integers + // yields inconsistent behavior depending on how php is compiled. so we left shift -1 (which, in + // two's compliment, consists of all 1 bits) by 31. on 64-bit systems this'll yield 0xFFFFFFFF80000000. + // that's not a problem, however, and 'anded' and a 32-bit number, as all the leading 1 bits are ignored. + (PHP_INT_SIZE == 4 ? (-1 << 31) : 0x80000000) => 'NET_SFTP_ATTR_EXTENDED' + ); + $this->open_flags = array( + 0x00000001 => 'NET_SFTP_OPEN_READ', + 0x00000002 => 'NET_SFTP_OPEN_WRITE', + 0x00000004 => 'NET_SFTP_OPEN_APPEND', + 0x00000008 => 'NET_SFTP_OPEN_CREATE', + 0x00000010 => 'NET_SFTP_OPEN_TRUNCATE', + 0x00000020 => 'NET_SFTP_OPEN_EXCL', + 0x00000040 => 'NET_SFTP_OPEN_TEXT' // defined in SFTPv4 + ); + // SFTPv5+ changed the flags up: + // https://datatracker.ietf.org/doc/html/draft-ietf-secsh-filexfer-13#section-8.1.1.3 + $this->open_flags5 = array( + // when SSH_FXF_ACCESS_DISPOSITION is a 3 bit field that controls how the file is opened + 0x00000000 => 'NET_SFTP_OPEN_CREATE_NEW', + 0x00000001 => 'NET_SFTP_OPEN_CREATE_TRUNCATE', + 0x00000002 => 'NET_SFTP_OPEN_OPEN_EXISTING', + 0x00000003 => 'NET_SFTP_OPEN_OPEN_OR_CREATE', + 0x00000004 => 'NET_SFTP_OPEN_TRUNCATE_EXISTING', + // the rest of the flags are not supported + 0x00000008 => 'NET_SFTP_OPEN_APPEND_DATA', // "the offset field of SS_FXP_WRITE requests is ignored" + 0x00000010 => 'NET_SFTP_OPEN_APPEND_DATA_ATOMIC', + 0x00000020 => 'NET_SFTP_OPEN_TEXT_MODE', + 0x00000040 => 'NET_SFTP_OPEN_BLOCK_READ', + 0x00000080 => 'NET_SFTP_OPEN_BLOCK_WRITE', + 0x00000100 => 'NET_SFTP_OPEN_BLOCK_DELETE', + 0x00000200 => 'NET_SFTP_OPEN_BLOCK_ADVISORY', + 0x00000400 => 'NET_SFTP_OPEN_NOFOLLOW', + 0x00000800 => 'NET_SFTP_OPEN_DELETE_ON_CLOSE', + 0x00001000 => 'NET_SFTP_OPEN_ACCESS_AUDIT_ALARM_INFO', + 0x00002000 => 'NET_SFTP_OPEN_ACCESS_BACKUP', + 0x00004000 => 'NET_SFTP_OPEN_BACKUP_STREAM', + 0x00008000 => 'NET_SFTP_OPEN_OVERRIDE_OWNER', + ); + // http://tools.ietf.org/html/draft-ietf-secsh-filexfer-04#section-5.2 + // see \phpseclib\Net\SFTP::_parseLongname() for an explanation + $this->file_types = array( + 1 => 'NET_SFTP_TYPE_REGULAR', + 2 => 'NET_SFTP_TYPE_DIRECTORY', + 3 => 'NET_SFTP_TYPE_SYMLINK', + 4 => 'NET_SFTP_TYPE_SPECIAL', + 5 => 'NET_SFTP_TYPE_UNKNOWN', + // the followin types were first defined for use in SFTPv5+ + // http://tools.ietf.org/html/draft-ietf-secsh-filexfer-05#section-5.2 + 6 => 'NET_SFTP_TYPE_SOCKET', + 7 => 'NET_SFTP_TYPE_CHAR_DEVICE', + 8 => 'NET_SFTP_TYPE_BLOCK_DEVICE', + 9 => 'NET_SFTP_TYPE_FIFO' + ); + $this->_define_array( + $this->packet_types, + $this->status_codes, + $this->attributes, + $this->open_flags, + $this->open_flags5, + $this->file_types + ); + + if (!defined('NET_SFTP_QUEUE_SIZE')) { + define('NET_SFTP_QUEUE_SIZE', 32); + } + if (!defined('NET_SFTP_UPLOAD_QUEUE_SIZE')) { + define('NET_SFTP_UPLOAD_QUEUE_SIZE', 1024); + } + } + + /** + * Check a few things before SFTP functions are called + * + * @return bool + * @access public + */ + function _precheck() + { + if (!($this->bitmap & SSH2::MASK_LOGIN)) { + return false; + } + + if ($this->pwd === false) { + return $this->_init_sftp_connection(); + } + + return true; + } + + /** + * Partially initialize an SFTP connection + * + * @return bool + * @access public + */ + function _partial_init_sftp_connection() + { + $this->window_size_server_to_client[self::CHANNEL] = $this->window_size; + + $packet = pack( + 'CNa*N3', + NET_SSH2_MSG_CHANNEL_OPEN, + strlen('session'), + 'session', + self::CHANNEL, + $this->window_size, + 0x4000 + ); + + if (!$this->_send_binary_packet($packet)) { + return false; + } + + $this->channel_status[self::CHANNEL] = NET_SSH2_MSG_CHANNEL_OPEN; + + $response = $this->_get_channel_packet(self::CHANNEL, true); + if ($response === false) { + return false; + } elseif ($response === true && $this->isTimeout()) { + return false; + } + + $packet = pack( + 'CNNa*CNa*', + NET_SSH2_MSG_CHANNEL_REQUEST, + $this->server_channels[self::CHANNEL], + strlen('subsystem'), + 'subsystem', + 1, + strlen('sftp'), + 'sftp' + ); + if (!$this->_send_binary_packet($packet)) { + return false; + } + + $this->channel_status[self::CHANNEL] = NET_SSH2_MSG_CHANNEL_REQUEST; + + $response = $this->_get_channel_packet(self::CHANNEL, true); + if ($response === false) { + // from PuTTY's psftp.exe + $command = "test -x /usr/lib/sftp-server && exec /usr/lib/sftp-server\n" . + "test -x /usr/local/lib/sftp-server && exec /usr/local/lib/sftp-server\n" . + "exec sftp-server"; + // we don't do $this->exec($command, false) because exec() operates on a different channel and plus the SSH_MSG_CHANNEL_OPEN that exec() does + // is redundant + $packet = pack( + 'CNNa*CNa*', + NET_SSH2_MSG_CHANNEL_REQUEST, + $this->server_channels[self::CHANNEL], + strlen('exec'), + 'exec', + 1, + strlen($command), + $command + ); + if (!$this->_send_binary_packet($packet)) { + return false; + } + + $this->channel_status[self::CHANNEL] = NET_SSH2_MSG_CHANNEL_REQUEST; + + $response = $this->_get_channel_packet(self::CHANNEL, true); + if ($response === false) { + return false; + } + } elseif ($response === true && $this->isTimeout()) { + return false; + } + + $this->channel_status[self::CHANNEL] = NET_SSH2_MSG_CHANNEL_DATA; + + if (!$this->_send_sftp_packet(NET_SFTP_INIT, "\0\0\0\3")) { + return false; + } + + $response = $this->_get_sftp_packet(); + if ($this->packet_type != NET_SFTP_VERSION) { + user_error('Expected SSH_FXP_VERSION'); + return false; + } + + $this->use_request_id = true; + + if (strlen($response) < 4) { + return false; + } + extract(unpack('Nversion', $this->_string_shift($response, 4))); + $this->defaultVersion = $version; + while (!empty($response)) { + if (strlen($response) < 4) { + return false; + } + extract(unpack('Nlength', $this->_string_shift($response, 4))); + $key = $this->_string_shift($response, $length); + if (strlen($response) < 4) { + return false; + } + extract(unpack('Nlength', $this->_string_shift($response, 4))); + $value = $this->_string_shift($response, $length); + $this->extensions[$key] = $value; + } + + $this->partial_init = true; + + return true; + } + + /** + * (Re)initializes the SFTP channel + * + * @return bool + * @access private + */ + function _init_sftp_connection() + { + if (!$this->partial_init && !$this->_partial_init_sftp_connection()) { + return false; + } + + /* + A Note on SFTPv4/5/6 support: + states the following: + + "If the client wishes to interoperate with servers that support noncontiguous version + numbers it SHOULD send '3'" + + Given that the server only sends its version number after the client has already done so, the above + seems to be suggesting that v3 should be the default version. This makes sense given that v3 is the + most popular. + + states the following; + + "If the server did not send the "versions" extension, or the version-from-list was not included, the + server MAY send a status response describing the failure, but MUST then close the channel without + processing any further requests." + + So what do you do if you have a client whose initial SSH_FXP_INIT packet says it implements v3 and + a server whose initial SSH_FXP_VERSION reply says it implements v4 and only v4? If it only implements + v4, the "versions" extension is likely not going to have been sent so version re-negotiation as discussed + in draft-ietf-secsh-filexfer-13 would be quite impossible. As such, what \phpseclib\Net\SFTP would do is close the + channel and reopen it with a new and updated SSH_FXP_INIT packet. + */ + $this->version = $this->defaultVersion; + if (isset($this->extensions['versions']) && (!$this->preferredVersion || $this->preferredVersion != $this->version)) { + $versions = explode(',', $this->extensions['versions']); + $supported = array(6, 5, 4); + if ($this->preferredVersion) { + $supported = array_diff($supported, array($this->preferredVersion)); + array_unshift($supported, $this->preferredVersion); + } + foreach ($supported as $ver) { + if (in_array($ver, $versions)) { + if ($ver === $this->version) { + break; + } + $this->version = (int) $ver; + $packet = pack('Na*Na*', strlen('version-select'), 'version-select', strlen($ver), $ver); + if (!$this->_send_sftp_packet(NET_SFTP_EXTENDED, $packet)) { + return false; + } + $response = $this->_get_sftp_packet(); + if ($this->packet_type != NET_SFTP_STATUS) { + user_error('Expected SSH_FXP_STATUS'); + return false; + } + + if (strlen($response) < 4) { + return false; + } + extract(unpack('Nstatus', $this->_string_shift($response, 4))); + if ($status != NET_SFTP_STATUS_OK) { + $this->_logError($response, $status); + return false; + } + + break; + } + } + } + + /* + SFTPv4+ defines a 'newline' extension. SFTPv3 seems to have unofficial support for it via 'newline@vandyke.com', + however, I'm not sure what 'newline@vandyke.com' is supposed to do (the fact that it's unofficial means that it's + not in the official SFTPv3 specs) and 'newline@vandyke.com' / 'newline' are likely not drop-in substitutes for + one another due to the fact that 'newline' comes with a SSH_FXF_TEXT bitmask whereas it seems unlikely that + 'newline@vandyke.com' would. + */ + /* + if (isset($this->extensions['newline@vandyke.com'])) { + $this->extensions['newline'] = $this->extensions['newline@vandyke.com']; + unset($this->extensions['newline@vandyke.com']); + } + */ + + if ($this->version < 2 || $this->version > 6) { + return false; + } + + $this->pwd = true; + $this->pwd = $this->_realpath('.'); + if ($this->pwd === false) { + if (!$this->canonicalize_paths) { + user_error('Unable to canonicalize current working directory'); + return false; + } + $this->canonicalize_paths = false; + $this->_reset_connection(NET_SSH2_DISCONNECT_CONNECTION_LOST); + } + + $this->_update_stat_cache($this->pwd, array()); + + return true; + } + + /** + * Disable the stat cache + * + * @access public + */ + function disableStatCache() + { + $this->use_stat_cache = false; + } + + /** + * Enable the stat cache + * + * @access public + */ + function enableStatCache() + { + $this->use_stat_cache = true; + } + + /** + * Clear the stat cache + * + * @access public + */ + function clearStatCache() + { + $this->stat_cache = array(); + } + + /** + * Enable path canonicalization + * + * @access public + */ + function enablePathCanonicalization() + { + $this->canonicalize_paths = true; + } + + /** + * Disable path canonicalization + * + * If this is enabled then $sftp->pwd() will not return the canonicalized absolute path + * + * @access public + */ + function disablePathCanonicalization() + { + $this->canonicalize_paths = false; + } + + /** + * Enable arbitrary length packets + * + * @access public + */ + function enableArbitraryLengthPackets() + { + $this->allow_arbitrary_length_packets = true; + } + + /** + * Disable arbitrary length packets + * + * @access public + */ + function disableArbitraryLengthPackets() + { + $this->allow_arbitrary_length_packets = false; + } + + /** + * Returns the current directory name + * + * @return mixed + * @access public + */ + function pwd() + { + if (!$this->_precheck()) { + return false; + } + + return $this->pwd; + } + + /** + * Logs errors + * + * @param string $response + * @param int $status + * @access public + */ + function _logError($response, $status = -1) + { + if ($status == -1) { + if (strlen($response) < 4) { + return; + } + extract(unpack('Nstatus', $this->_string_shift($response, 4))); + } + + $error = $this->status_codes[$status]; + + if ($this->version > 2) { + extract(unpack('Nlength', $this->_string_shift($response, 4))); + $this->sftp_errors[] = $error . ': ' . $this->_string_shift($response, $length); + } else { + $this->sftp_errors[] = $error; + } + } + + /** + * Returns canonicalized absolute pathname + * + * realpath() expands all symbolic links and resolves references to '/./', '/../' and extra '/' characters in the input + * path and returns the canonicalized absolute pathname. + * + * @param string $path + * @return mixed + * @access public + */ + function realpath($path) + { + if (!$this->_precheck()) { + return false; + } + + return $this->_realpath($path); + } + + /** + * Canonicalize the Server-Side Path Name + * + * SFTP doesn't provide a mechanism by which the current working directory can be changed, so we'll emulate it. Returns + * the absolute (canonicalized) path. + * + * If canonicalize_paths has been disabled using disablePathCanonicalization(), $path is returned as-is. + * + * @see self::chdir() + * @see self::disablePathCanonicalization() + * @param string $path + * @return mixed + * @access private + */ + function _realpath($path) + { + if (!$this->canonicalize_paths) { + if ($this->pwd === true) { + return '.'; + } + if (!strlen($path) || $path[0] != '/') { + $path = $this->pwd . '/' . $path; + } + + $parts = explode('/', $path); + $afterPWD = $beforePWD = array(); + foreach ($parts as $part) { + switch ($part) { + //case '': // some SFTP servers /require/ double /'s. see https://github.com/phpseclib/phpseclib/pull/1137 + case '.': + break; + case '..': + if (!empty($afterPWD)) { + array_pop($afterPWD); + } else { + $beforePWD[] = '..'; + } + break; + default: + $afterPWD[] = $part; + } + } + + $beforePWD = count($beforePWD) ? implode('/', $beforePWD) : '.'; + return $beforePWD . '/' . implode('/', $afterPWD); + } + + if ($this->pwd === true) { + // http://tools.ietf.org/html/draft-ietf-secsh-filexfer-13#section-8.9 + if (!$this->_send_sftp_packet(NET_SFTP_REALPATH, pack('Na*', strlen($path), $path))) { + return false; + } + + $response = $this->_get_sftp_packet(); + switch ($this->packet_type) { + case NET_SFTP_NAME: + // although SSH_FXP_NAME is implemented differently in SFTPv3 than it is in SFTPv4+, the following + // should work on all SFTP versions since the only part of the SSH_FXP_NAME packet the following looks + // at is the first part and that part is defined the same in SFTP versions 3 through 6. + $this->_string_shift($response, 4); // skip over the count - it should be 1, anyway + if (strlen($response) < 4) { + return false; + } + extract(unpack('Nlength', $this->_string_shift($response, 4))); + return $this->_string_shift($response, $length); + case NET_SFTP_STATUS: + $this->_logError($response); + return false; + default: + return false; + } + } + + if (!strlen($path) || $path[0] != '/') { + $path = $this->pwd . '/' . $path; + } + + $path = explode('/', $path); + $new = array(); + foreach ($path as $dir) { + if (!strlen($dir)) { + continue; + } + switch ($dir) { + case '..': + array_pop($new); + case '.': + break; + default: + $new[] = $dir; + } + } + + return '/' . implode('/', $new); + } + + /** + * Changes the current directory + * + * @param string $dir + * @return bool + * @access public + */ + function chdir($dir) + { + if (!$this->_precheck()) { + return false; + } + + // assume current dir if $dir is empty + if ($dir === '') { + $dir = './'; + // suffix a slash if needed + } elseif ($dir[strlen($dir) - 1] != '/') { + $dir.= '/'; + } + + $dir = $this->_realpath($dir); + + // confirm that $dir is, in fact, a valid directory + if ($this->use_stat_cache && is_array($this->_query_stat_cache($dir))) { + $this->pwd = $dir; + return true; + } + + // we could do a stat on the alleged $dir to see if it's a directory but that doesn't tell us + // the currently logged in user has the appropriate permissions or not. maybe you could see if + // the file's uid / gid match the currently logged in user's uid / gid but how there's no easy + // way to get those with SFTP + + if (!$this->_send_sftp_packet(NET_SFTP_OPENDIR, pack('Na*', strlen($dir), $dir))) { + return false; + } + + // see \phpseclib\Net\SFTP::nlist() for a more thorough explanation of the following + $response = $this->_get_sftp_packet(); + switch ($this->packet_type) { + case NET_SFTP_HANDLE: + $handle = substr($response, 4); + break; + case NET_SFTP_STATUS: + $this->_logError($response); + return false; + default: + user_error('Expected SSH_FXP_HANDLE or SSH_FXP_STATUS'); + return false; + } + + if (!$this->_close_handle($handle)) { + return false; + } + + $this->_update_stat_cache($dir, array()); + + $this->pwd = $dir; + return true; + } + + /** + * Returns a list of files in the given directory + * + * @param string $dir + * @param bool $recursive + * @return mixed + * @access public + */ + function nlist($dir = '.', $recursive = false) + { + return $this->_nlist_helper($dir, $recursive, ''); + } + + /** + * Helper method for nlist + * + * @param string $dir + * @param bool $recursive + * @param string $relativeDir + * @return mixed + * @access private + */ + function _nlist_helper($dir, $recursive, $relativeDir) + { + $files = $this->_list($dir, false); + + // If we get an int back, then that is an "unexpected" status. + // We do not have a file list, so return false. + if (is_int($files)) { + return false; + } + + if (!$recursive || $files === false) { + return $files; + } + + $result = array(); + foreach ($files as $value) { + if ($value == '.' || $value == '..') { + if ($relativeDir == '') { + $result[] = $value; + } + continue; + } + if (is_array($this->_query_stat_cache($this->_realpath($dir . '/' . $value)))) { + $temp = $this->_nlist_helper($dir . '/' . $value, true, $relativeDir . $value . '/'); + $temp = is_array($temp) ? $temp : array(); + $result = array_merge($result, $temp); + } else { + $result[] = $relativeDir . $value; + } + } + + return $result; + } + + /** + * Returns a detailed list of files in the given directory + * + * @param string $dir + * @param bool $recursive + * @return mixed + * @access public + */ + function rawlist($dir = '.', $recursive = false) + { + $files = $this->_list($dir, true); + + // If we get an int back, then that is an "unexpected" status. + // We do not have a file list, so return false. + if (is_int($files)) { + return false; + } + + if (!$recursive || $files === false) { + return $files; + } + + static $depth = 0; + + foreach ($files as $key => $value) { + if ($depth != 0 && $key == '..') { + unset($files[$key]); + continue; + } + $is_directory = false; + if ($key != '.' && $key != '..') { + if ($this->use_stat_cache) { + $is_directory = is_array($this->_query_stat_cache($this->_realpath($dir . '/' . $key))); + } else { + $stat = $this->lstat($dir . '/' . $key); + $is_directory = $stat && $stat['type'] === NET_SFTP_TYPE_DIRECTORY; + } + } + + if ($is_directory) { + $depth++; + $files[$key] = $this->rawlist($dir . '/' . $key, true); + $depth--; + } else { + $files[$key] = (object) $value; + } + } + + return $files; + } + + /** + * Reads a list, be it detailed or not, of files in the given directory + * + * @param string $dir + * @param bool $raw + * @return mixed + * @access private + */ + function _list($dir, $raw = true) + { + if (!$this->_precheck()) { + return false; + } + + $dir = $this->_realpath($dir . '/'); + if ($dir === false) { + return false; + } + + // http://tools.ietf.org/html/draft-ietf-secsh-filexfer-13#section-8.1.2 + if (!$this->_send_sftp_packet(NET_SFTP_OPENDIR, pack('Na*', strlen($dir), $dir))) { + return false; + } + + $response = $this->_get_sftp_packet(); + switch ($this->packet_type) { + case NET_SFTP_HANDLE: + // http://tools.ietf.org/html/draft-ietf-secsh-filexfer-13#section-9.2 + // since 'handle' is the last field in the SSH_FXP_HANDLE packet, we'll just remove the first four bytes that + // represent the length of the string and leave it at that + $handle = substr($response, 4); + break; + case NET_SFTP_STATUS: + // presumably SSH_FX_NO_SUCH_FILE or SSH_FX_PERMISSION_DENIED + if (strlen($response) < 4) { + return false; + } + extract(unpack('Nstatus', $this->_string_shift($response, 4))); + $this->_logError($response, $status); + return $status; + default: + user_error('Expected SSH_FXP_HANDLE or SSH_FXP_STATUS'); + return false; + } + + $this->_update_stat_cache($dir, array()); + + $contents = array(); + while (true) { + // http://tools.ietf.org/html/draft-ietf-secsh-filexfer-13#section-8.2.2 + // why multiple SSH_FXP_READDIR packets would be sent when the response to a single one can span arbitrarily many + // SSH_MSG_CHANNEL_DATA messages is not known to me. + if (!$this->_send_sftp_packet(NET_SFTP_READDIR, pack('Na*', strlen($handle), $handle))) { + return false; + } + + $response = $this->_get_sftp_packet(); + switch ($this->packet_type) { + case NET_SFTP_NAME: + if (strlen($response) < 4) { + return false; + } + extract(unpack('Ncount', $this->_string_shift($response, 4))); + for ($i = 0; $i < $count; $i++) { + if (strlen($response) < 4) { + return false; + } + extract(unpack('Nlength', $this->_string_shift($response, 4))); + $shortname = $this->_string_shift($response, $length); + // SFTPv4 "removed the long filename from the names structure-- it can now be + // built from information available in the attrs structure." + if ($this->version < 4) { + if (strlen($response) < 4) { + return false; + } + extract(unpack('Nlength', $this->_string_shift($response, 4))); + $longname = $this->_string_shift($response, $length); + } + $attributes = $this->_parseAttributes($response); + if (!isset($attributes['type']) && $this->version < 4) { + $fileType = $this->_parseLongname($longname); + if ($fileType) { + $attributes['type'] = $fileType; + } + } + $contents[$shortname] = $attributes + array('filename' => $shortname); + + if (isset($attributes['type']) && $attributes['type'] == NET_SFTP_TYPE_DIRECTORY && ($shortname != '.' && $shortname != '..')) { + $this->_update_stat_cache($dir . '/' . $shortname, array()); + } else { + if ($shortname == '..') { + $temp = $this->_realpath($dir . '/..') . '/.'; + } else { + $temp = $dir . '/' . $shortname; + } + $this->_update_stat_cache($temp, (object) array('lstat' => $attributes)); + } + // SFTPv6 has an optional boolean end-of-list field, but we'll ignore that, since the + // final SSH_FXP_STATUS packet should tell us that, already. + } + break; + case NET_SFTP_STATUS: + if (strlen($response) < 4) { + return false; + } + extract(unpack('Nstatus', $this->_string_shift($response, 4))); + if ($status != NET_SFTP_STATUS_EOF) { + $this->_logError($response, $status); + return $status; + } + break 2; + default: + user_error('Expected SSH_FXP_NAME or SSH_FXP_STATUS'); + return false; + } + } + + if (!$this->_close_handle($handle)) { + return false; + } + + if (count($this->sortOptions)) { + uasort($contents, array(&$this, '_comparator')); + } + + return $raw ? $contents : array_map('strval', array_keys($contents)); + } + + /** + * Compares two rawlist entries using parameters set by setListOrder() + * + * Intended for use with uasort() + * + * @param array $a + * @param array $b + * @return int + * @access private + */ + function _comparator($a, $b) + { + switch (true) { + case $a['filename'] === '.' || $b['filename'] === '.': + if ($a['filename'] === $b['filename']) { + return 0; + } + return $a['filename'] === '.' ? -1 : 1; + case $a['filename'] === '..' || $b['filename'] === '..': + if ($a['filename'] === $b['filename']) { + return 0; + } + return $a['filename'] === '..' ? -1 : 1; + case isset($a['type']) && $a['type'] === NET_SFTP_TYPE_DIRECTORY: + if (!isset($b['type'])) { + return 1; + } + if ($b['type'] !== $a['type']) { + return -1; + } + break; + case isset($b['type']) && $b['type'] === NET_SFTP_TYPE_DIRECTORY: + return 1; + } + foreach ($this->sortOptions as $sort => $order) { + if (!isset($a[$sort]) || !isset($b[$sort])) { + if (isset($a[$sort])) { + return -1; + } + if (isset($b[$sort])) { + return 1; + } + return 0; + } + switch ($sort) { + case 'filename': + $result = strcasecmp($a['filename'], $b['filename']); + if ($result) { + return $order === SORT_DESC ? -$result : $result; + } + break; + case 'permissions': + case 'mode': + $a[$sort]&= 07777; + $b[$sort]&= 07777; + default: + if ($a[$sort] === $b[$sort]) { + break; + } + return $order === SORT_ASC ? $a[$sort] - $b[$sort] : $b[$sort] - $a[$sort]; + } + } + } + + /** + * Defines how nlist() and rawlist() will be sorted - if at all. + * + * If sorting is enabled directories and files will be sorted independently with + * directories appearing before files in the resultant array that is returned. + * + * Any parameter returned by stat is a valid sort parameter for this function. + * Filename comparisons are case insensitive. + * + * Examples: + * + * $sftp->setListOrder('filename', SORT_ASC); + * $sftp->setListOrder('size', SORT_DESC, 'filename', SORT_ASC); + * $sftp->setListOrder(true); + * Separates directories from files but doesn't do any sorting beyond that + * $sftp->setListOrder(); + * Don't do any sort of sorting + * + * @access public + */ + function setListOrder() + { + $this->sortOptions = array(); + $args = func_get_args(); + if (empty($args)) { + return; + } + $len = count($args) & 0x7FFFFFFE; + for ($i = 0; $i < $len; $i+=2) { + $this->sortOptions[$args[$i]] = $args[$i + 1]; + } + if (!count($this->sortOptions)) { + $this->sortOptions = array('bogus' => true); + } + } + + /** + * Returns the file size, in bytes, or false, on failure + * + * Files larger than 4GB will show up as being exactly 4GB. + * + * @param string $filename + * @return mixed + * @access public + */ + function size($filename) + { + $result = $this->stat($filename); + if ($result === false) { + return false; + } + return isset($result['size']) ? $result['size'] : -1; + } + + /** + * Save files / directories to cache + * + * @param string $path + * @param mixed $value + * @access private + */ + function _update_stat_cache($path, $value) + { + if ($this->use_stat_cache === false) { + return; + } + + // preg_replace('#^/|/(?=/)|/$#', '', $dir) == str_replace('//', '/', trim($path, '/')) + $dirs = explode('/', preg_replace('#^/|/(?=/)|/$#', '', $path)); + + $temp = &$this->stat_cache; + $max = count($dirs) - 1; + foreach ($dirs as $i => $dir) { + // if $temp is an object that means one of two things. + // 1. a file was deleted and changed to a directory behind phpseclib's back + // 2. it's a symlink. when lstat is done it's unclear what it's a symlink to + if (is_object($temp)) { + $temp = array(); + } + if (!isset($temp[$dir])) { + $temp[$dir] = array(); + } + if ($i === $max) { + if (is_object($temp[$dir]) && is_object($value)) { + if (!isset($value->stat) && isset($temp[$dir]->stat)) { + $value->stat = $temp[$dir]->stat; + } + if (!isset($value->lstat) && isset($temp[$dir]->lstat)) { + $value->lstat = $temp[$dir]->lstat; + } + } + $temp[$dir] = $value; + break; + } + $temp = &$temp[$dir]; + } + } + + /** + * Remove files / directories from cache + * + * @param string $path + * @return bool + * @access private + */ + function _remove_from_stat_cache($path) + { + $dirs = explode('/', preg_replace('#^/|/(?=/)|/$#', '', $path)); + + $temp = &$this->stat_cache; + $max = count($dirs) - 1; + foreach ($dirs as $i => $dir) { + if (!is_array($temp)) { + return false; + } + if ($i === $max) { + unset($temp[$dir]); + return true; + } + if (!isset($temp[$dir])) { + return false; + } + $temp = &$temp[$dir]; + } + } + + /** + * Checks cache for path + * + * Mainly used by file_exists + * + * @param string $path + * @return mixed + * @access private + */ + function _query_stat_cache($path) + { + $dirs = explode('/', preg_replace('#^/|/(?=/)|/$#', '', $path)); + + $temp = &$this->stat_cache; + foreach ($dirs as $dir) { + if (!is_array($temp)) { + return null; + } + if (!isset($temp[$dir])) { + return null; + } + $temp = &$temp[$dir]; + } + return $temp; + } + + /** + * Returns general information about a file. + * + * Returns an array on success and false otherwise. + * + * @param string $filename + * @return mixed + * @access public + */ + function stat($filename) + { + if (!$this->_precheck()) { + return false; + } + + $filename = $this->_realpath($filename); + if ($filename === false) { + return false; + } + + if ($this->use_stat_cache) { + $result = $this->_query_stat_cache($filename); + if (is_array($result) && isset($result['.']) && isset($result['.']->stat)) { + return $result['.']->stat; + } + if (is_object($result) && isset($result->stat)) { + return $result->stat; + } + } + + $stat = $this->_stat($filename, NET_SFTP_STAT); + if ($stat === false) { + $this->_remove_from_stat_cache($filename); + return false; + } + if (isset($stat['type'])) { + if ($stat['type'] == NET_SFTP_TYPE_DIRECTORY) { + $filename.= '/.'; + } + $this->_update_stat_cache($filename, (object) array('stat' => $stat)); + return $stat; + } + + $pwd = $this->pwd; + $stat['type'] = $this->chdir($filename) ? + NET_SFTP_TYPE_DIRECTORY : + NET_SFTP_TYPE_REGULAR; + $this->pwd = $pwd; + + if ($stat['type'] == NET_SFTP_TYPE_DIRECTORY) { + $filename.= '/.'; + } + $this->_update_stat_cache($filename, (object) array('stat' => $stat)); + + return $stat; + } + + /** + * Returns general information about a file or symbolic link. + * + * Returns an array on success and false otherwise. + * + * @param string $filename + * @return mixed + * @access public + */ + function lstat($filename) + { + if (!$this->_precheck()) { + return false; + } + + $filename = $this->_realpath($filename); + if ($filename === false) { + return false; + } + + if ($this->use_stat_cache) { + $result = $this->_query_stat_cache($filename); + if (is_array($result) && isset($result['.']) && isset($result['.']->lstat)) { + return $result['.']->lstat; + } + if (is_object($result) && isset($result->lstat)) { + return $result->lstat; + } + } + + $lstat = $this->_stat($filename, NET_SFTP_LSTAT); + if ($lstat === false) { + $this->_remove_from_stat_cache($filename); + return false; + } + if (isset($lstat['type'])) { + if ($lstat['type'] == NET_SFTP_TYPE_DIRECTORY) { + $filename.= '/.'; + } + $this->_update_stat_cache($filename, (object) array('lstat' => $lstat)); + return $lstat; + } + + $stat = $this->_stat($filename, NET_SFTP_STAT); + + if ($lstat != $stat) { + $lstat = array_merge($lstat, array('type' => NET_SFTP_TYPE_SYMLINK)); + $this->_update_stat_cache($filename, (object) array('lstat' => $lstat)); + return $stat; + } + + $pwd = $this->pwd; + $lstat['type'] = $this->chdir($filename) ? + NET_SFTP_TYPE_DIRECTORY : + NET_SFTP_TYPE_REGULAR; + $this->pwd = $pwd; + + if ($lstat['type'] == NET_SFTP_TYPE_DIRECTORY) { + $filename.= '/.'; + } + $this->_update_stat_cache($filename, (object) array('lstat' => $lstat)); + + return $lstat; + } + + /** + * Returns general information about a file or symbolic link + * + * Determines information without calling \phpseclib\Net\SFTP::realpath(). + * The second parameter can be either NET_SFTP_STAT or NET_SFTP_LSTAT. + * + * @param string $filename + * @param int $type + * @return mixed + * @access private + */ + function _stat($filename, $type) + { + // SFTPv4+ adds an additional 32-bit integer field - flags - to the following: + $packet = pack('Na*', strlen($filename), $filename); + if (!$this->_send_sftp_packet($type, $packet)) { + return false; + } + + $response = $this->_get_sftp_packet(); + switch ($this->packet_type) { + case NET_SFTP_ATTRS: + return $this->_parseAttributes($response); + case NET_SFTP_STATUS: + $this->_logError($response); + return false; + } + + user_error('Expected SSH_FXP_ATTRS or SSH_FXP_STATUS'); + return false; + } + + /** + * Truncates a file to a given length + * + * @param string $filename + * @param int $new_size + * @return bool + * @access public + */ + function truncate($filename, $new_size) + { + $attr = pack('N3', NET_SFTP_ATTR_SIZE, $new_size / 4294967296, $new_size); // 4294967296 == 0x100000000 == 1<<32 + + return $this->_setstat($filename, $attr, false); + } + + /** + * Sets access and modification time of file. + * + * If the file does not exist, it will be created. + * + * @param string $filename + * @param int $time + * @param int $atime + * @return bool + * @access public + */ + function touch($filename, $time = null, $atime = null) + { + if (!$this->_precheck()) { + return false; + } + + $filename = $this->_realpath($filename); + if ($filename === false) { + return false; + } + + if (!isset($time)) { + $time = time(); + } + if (!isset($atime)) { + $atime = $time; + } + + if ($this->version < 4) { + $attr = pack('N3', NET_SFTP_ATTR_ACCESSTIME, $atime, $time); + } else { + $attr = pack( + 'N5', + NET_SFTP_ATTR_ACCESSTIME | NET_SFTP_ATTR_MODIFYTIME, + $atime / 4294967296, + $atime, + $time / 4294967296, + $time + ); + } + + $packet = pack('Na*', strlen($filename), $filename); + $packet.= $this->version >= 5 ? + pack('N2', 0, NET_SFTP_OPEN_OPEN_EXISTING) : + pack('N', NET_SFTP_OPEN_WRITE | NET_SFTP_OPEN_CREATE | NET_SFTP_OPEN_EXCL); + $packet.= $attr; + + if (!$this->_send_sftp_packet(NET_SFTP_OPEN, $packet)) { + return false; + } + + $response = $this->_get_sftp_packet(); + switch ($this->packet_type) { + case NET_SFTP_HANDLE: + return $this->_close_handle(substr($response, 4)); + case NET_SFTP_STATUS: + $this->_logError($response); + break; + default: + user_error('Expected SSH_FXP_HANDLE or SSH_FXP_STATUS'); + return false; + } + + return $this->_setstat($filename, $attr, false); + } + + /** + * Changes file or directory owner + * + * $uid should be an int for SFTPv3 and a string for SFTPv4+. Ideally the string + * would be of the form "user@dns_domain" but it does not need to be. + * `$sftp->getSupportedVersions()['version']` will return the specific version + * that's being used. + * + * Returns true on success or false on error. + * + * @param string $filename + * @param int|string $uid + * @param bool $recursive + * @return bool + * @access public + */ + function chown($filename, $uid, $recursive = false) + { + /* + quoting , + + "To avoid a representation that is tied to a particular underlying + implementation at the client or server, the use of UTF-8 strings has + been chosen. The string should be of the form "user@dns_domain". + This will allow for a client and server that do not use the same + local representation the ability to translate to a common syntax that + can be interpreted by both. In the case where there is no + translation available to the client or server, the attribute value + must be constructed without the "@"." + + phpseclib _could_ auto append the dns_domain to $uid BUT what if it shouldn't + have one? phpseclib would have no way of knowing so rather than guess phpseclib + will just use whatever value the user provided + */ + + $attr = $this->version < 4 ? + // quoting , + // "if the owner or group is specified as -1, then that ID is not changed" + pack('N3', NET_SFTP_ATTR_UIDGID, $uid, -1) : + // quoting , + // "If either the owner or group field is zero length, the field should be + // considered absent, and no change should be made to that specific field + // during a modification operation" + pack('NNa*Na*', NET_SFTP_ATTR_OWNERGROUP, strlen($uid), $uid, 0, ''); + + return $this->_setstat($filename, $attr, $recursive); + } + + /** + * Changes file or directory group + * + * $gid should be an int for SFTPv3 and a string for SFTPv4+. Ideally the string + * would be of the form "user@dns_domain" but it does not need to be. + * `$sftp->getSupportedVersions()['version']` will return the specific version + * that's being used. + * + * Returns true on success or false on error. + * + * @param string $filename + * @param int|string $gid + * @param bool $recursive + * @return bool + * @access public + */ + function chgrp($filename, $gid, $recursive = false) + { + $attr = $this->version < 4 ? + pack('N3', NET_SFTP_ATTR_UIDGID, -1, $gid) : + pack('NNa*Na*', NET_SFTP_ATTR_OWNERGROUP, 0, '', strlen($gid), $gid); + + return $this->_setstat($filename, $attr, $recursive); + } + + /** + * Set permissions on a file. + * + * Returns the new file permissions on success or false on error. + * If $recursive is true than this just returns true or false. + * + * @param int $mode + * @param string $filename + * @param bool $recursive + * @return mixed + * @access public + */ + function chmod($mode, $filename, $recursive = false) + { + if (is_string($mode) && is_int($filename)) { + $temp = $mode; + $mode = $filename; + $filename = $temp; + } + + $attr = pack('N2', NET_SFTP_ATTR_PERMISSIONS, $mode & 07777); + if (!$this->_setstat($filename, $attr, $recursive)) { + return false; + } + if ($recursive) { + return true; + } + + $filename = $this->realpath($filename); + // rather than return what the permissions *should* be, we'll return what they actually are. this will also + // tell us if the file actually exists. + // incidentally, SFTPv4+ adds an additional 32-bit integer field - flags - to the following: + $packet = pack('Na*', strlen($filename), $filename); + if (!$this->_send_sftp_packet(NET_SFTP_STAT, $packet)) { + return false; + } + + $response = $this->_get_sftp_packet(); + switch ($this->packet_type) { + case NET_SFTP_ATTRS: + $attrs = $this->_parseAttributes($response); + return $attrs['permissions']; + case NET_SFTP_STATUS: + $this->_logError($response); + return false; + } + + user_error('Expected SSH_FXP_ATTRS or SSH_FXP_STATUS'); + return false; + } + + /** + * Sets information about a file + * + * @param string $filename + * @param string $attr + * @param bool $recursive + * @return bool + * @access private + */ + function _setstat($filename, $attr, $recursive) + { + if (!$this->_precheck()) { + return false; + } + + $filename = $this->_realpath($filename); + if ($filename === false) { + return false; + } + + $this->_remove_from_stat_cache($filename); + + if ($recursive) { + $i = 0; + $result = $this->_setstat_recursive($filename, $attr, $i); + $this->_read_put_responses($i); + return $result; + } + + $packet = $this->version >= 4 ? + pack('Na*a*Ca*', strlen($filename), $filename, substr($attr, 0, 4), NET_SFTP_TYPE_UNKNOWN, substr($attr, 4)) : + pack('Na*a*', strlen($filename), $filename, $attr); + if (!$this->_send_sftp_packet(NET_SFTP_SETSTAT, $packet)) { + return false; + } + + /* + "Because some systems must use separate system calls to set various attributes, it is possible that a failure + response will be returned, but yet some of the attributes may be have been successfully modified. If possible, + servers SHOULD avoid this situation; however, clients MUST be aware that this is possible." + + -- http://tools.ietf.org/html/draft-ietf-secsh-filexfer-13#section-8.6 + */ + $response = $this->_get_sftp_packet(); + if ($this->packet_type != NET_SFTP_STATUS) { + user_error('Expected SSH_FXP_STATUS'); + return false; + } + + if (strlen($response) < 4) { + return false; + } + extract(unpack('Nstatus', $this->_string_shift($response, 4))); + if ($status != NET_SFTP_STATUS_OK) { + $this->_logError($response, $status); + return false; + } + + return true; + } + + /** + * Recursively sets information on directories on the SFTP server + * + * Minimizes directory lookups and SSH_FXP_STATUS requests for speed. + * + * @param string $path + * @param string $attr + * @param int $i + * @return bool + * @access private + */ + function _setstat_recursive($path, $attr, &$i) + { + if (!$this->_read_put_responses($i)) { + return false; + } + $i = 0; + $entries = $this->_list($path, true); + + if ($entries === false || is_int($entries)) { + return $this->_setstat($path, $attr, false); + } + + // normally $entries would have at least . and .. but it might not if the directories + // permissions didn't allow reading + if (empty($entries)) { + return false; + } + + unset($entries['.'], $entries['..']); + foreach ($entries as $filename => $props) { + if (!isset($props['type'])) { + return false; + } + + $temp = $path . '/' . $filename; + if ($props['type'] == NET_SFTP_TYPE_DIRECTORY) { + if (!$this->_setstat_recursive($temp, $attr, $i)) { + return false; + } + } else { + $packet = $this->version >= 4 ? + pack('Na*Ca*', strlen($temp), $temp, NET_SFTP_TYPE_UNKNOWN, $attr) : + pack('Na*a*', strlen($temp), $temp, $attr); + if (!$this->_send_sftp_packet(NET_SFTP_SETSTAT, $packet)) { + return false; + } + + $i++; + + if ($i >= NET_SFTP_QUEUE_SIZE) { + if (!$this->_read_put_responses($i)) { + return false; + } + $i = 0; + } + } + } + + $packet = $this->version >= 4 ? + pack('Na*Ca*', strlen($temp), $temp, NET_SFTP_TYPE_UNKNOWN, $attr) : + pack('Na*a*', strlen($temp), $temp, $attr); + if (!$this->_send_sftp_packet(NET_SFTP_SETSTAT, $packet)) { + return false; + } + + $i++; + + if ($i >= NET_SFTP_QUEUE_SIZE) { + if (!$this->_read_put_responses($i)) { + return false; + } + $i = 0; + } + + return true; + } + + /** + * Return the target of a symbolic link + * + * @param string $link + * @return mixed + * @access public + */ + function readlink($link) + { + if (!$this->_precheck()) { + return false; + } + + $link = $this->_realpath($link); + + if (!$this->_send_sftp_packet(NET_SFTP_READLINK, pack('Na*', strlen($link), $link))) { + return false; + } + + $response = $this->_get_sftp_packet(); + switch ($this->packet_type) { + case NET_SFTP_NAME: + break; + case NET_SFTP_STATUS: + $this->_logError($response); + return false; + default: + user_error('Expected SSH_FXP_NAME or SSH_FXP_STATUS'); + return false; + } + + if (strlen($response) < 4) { + return false; + } + extract(unpack('Ncount', $this->_string_shift($response, 4))); + // the file isn't a symlink + if (!$count) { + return false; + } + + if (strlen($response) < 4) { + return false; + } + extract(unpack('Nlength', $this->_string_shift($response, 4))); + return $this->_string_shift($response, $length); + } + + /** + * Create a symlink + * + * symlink() creates a symbolic link to the existing target with the specified name link. + * + * @param string $target + * @param string $link + * @return bool + * @access public + */ + function symlink($target, $link) + { + if (!$this->_precheck()) { + return false; + } + + //$target = $this->_realpath($target); + $link = $this->_realpath($link); + + /* quoting https://datatracker.ietf.org/doc/html/draft-ietf-secsh-filexfer-09#section-12.1 : + + Changed the SYMLINK packet to be LINK and give it the ability to + create hard links. Also change it's packet number because many + implementation implemented SYMLINK with the arguments reversed. + Hopefully the new argument names make it clear which way is which. + */ + if ($this->version == 6) { + $type = NET_SFTP_LINK; + $packet = pack('Na*Na*C', strlen($link), $link, strlen($target), $target, 1); + } else { + $type = NET_SFTP_SYMLINK; + /* quoting http://bxr.su/OpenBSD/usr.bin/ssh/PROTOCOL#347 : + + 3.1. sftp: Reversal of arguments to SSH_FXP_SYMLINK + + When OpenSSH's sftp-server was implemented, the order of the arguments + to the SSH_FXP_SYMLINK method was inadvertently reversed. Unfortunately, + the reversal was not noticed until the server was widely deployed. Since + fixing this to follow the specification would cause incompatibility, the + current order was retained. For correct operation, clients should send + SSH_FXP_SYMLINK as follows: + + uint32 id + string targetpath + string linkpath */ + $packet = substr($this->server_identifier, 0, 15) == 'SSH-2.0-OpenSSH' ? + pack('Na*Na*', strlen($target), $target, strlen($link), $link) : + pack('Na*Na*', strlen($link), $link, strlen($target), $target); + } + if (!$this->_send_sftp_packet($type, $packet)) { + return false; + } + + $response = $this->_get_sftp_packet(); + if ($this->packet_type != NET_SFTP_STATUS) { + user_error('Expected SSH_FXP_STATUS'); + return false; + } + + if (strlen($response) < 4) { + return false; + } + extract(unpack('Nstatus', $this->_string_shift($response, 4))); + if ($status != NET_SFTP_STATUS_OK) { + $this->_logError($response, $status); + return false; + } + + return true; + } + + /** + * Creates a directory. + * + * @param string $dir + * @param int $mode + * @param bool $recursive + * @return bool + * @access public + */ + function mkdir($dir, $mode = -1, $recursive = false) + { + if (!$this->_precheck()) { + return false; + } + + $dir = $this->_realpath($dir); + + if ($recursive) { + $dirs = explode('/', preg_replace('#/(?=/)|/$#', '', $dir)); + if (empty($dirs[0])) { + array_shift($dirs); + $dirs[0] = '/' . $dirs[0]; + } + for ($i = 0; $i < count($dirs); $i++) { + $temp = array_slice($dirs, 0, $i + 1); + $temp = implode('/', $temp); + $result = $this->_mkdir_helper($temp, $mode); + } + return $result; + } + + return $this->_mkdir_helper($dir, $mode); + } + + /** + * Helper function for directory creation + * + * @param string $dir + * @param int $mode + * @return bool + * @access private + */ + function _mkdir_helper($dir, $mode) + { + // send SSH_FXP_MKDIR without any attributes (that's what the \0\0\0\0 is doing) + if (!$this->_send_sftp_packet(NET_SFTP_MKDIR, pack('Na*a*', strlen($dir), $dir, "\0\0\0\0"))) { + return false; + } + + $response = $this->_get_sftp_packet(); + if ($this->packet_type != NET_SFTP_STATUS) { + user_error('Expected SSH_FXP_STATUS'); + return false; + } + + if (strlen($response) < 4) { + return false; + } + extract(unpack('Nstatus', $this->_string_shift($response, 4))); + if ($status != NET_SFTP_STATUS_OK) { + $this->_logError($response, $status); + return false; + } + + if ($mode !== -1) { + $this->chmod($mode, $dir); + } + + return true; + } + + /** + * Removes a directory. + * + * @param string $dir + * @return bool + * @access public + */ + function rmdir($dir) + { + if (!$this->_precheck()) { + return false; + } + + $dir = $this->_realpath($dir); + if ($dir === false) { + return false; + } + + if (!$this->_send_sftp_packet(NET_SFTP_RMDIR, pack('Na*', strlen($dir), $dir))) { + return false; + } + + $response = $this->_get_sftp_packet(); + if ($this->packet_type != NET_SFTP_STATUS) { + user_error('Expected SSH_FXP_STATUS'); + return false; + } + + if (strlen($response) < 4) { + return false; + } + extract(unpack('Nstatus', $this->_string_shift($response, 4))); + if ($status != NET_SFTP_STATUS_OK) { + // presumably SSH_FX_NO_SUCH_FILE or SSH_FX_PERMISSION_DENIED? + $this->_logError($response, $status); + return false; + } + + $this->_remove_from_stat_cache($dir); + // the following will do a soft delete, which would be useful if you deleted a file + // and then tried to do a stat on the deleted file. the above, in contrast, does + // a hard delete + //$this->_update_stat_cache($dir, false); + + return true; + } + + /** + * Uploads a file to the SFTP server. + * + * By default, \phpseclib\Net\SFTP::put() does not read from the local filesystem. $data is dumped directly into $remote_file. + * So, for example, if you set $data to 'filename.ext' and then do \phpseclib\Net\SFTP::get(), you will get a file, twelve bytes + * long, containing 'filename.ext' as its contents. + * + * Setting $mode to self::SOURCE_LOCAL_FILE will change the above behavior. With self::SOURCE_LOCAL_FILE, $remote_file will + * contain as many bytes as filename.ext does on your local filesystem. If your filename.ext is 1MB then that is how + * large $remote_file will be, as well. + * + * Setting $mode to self::SOURCE_CALLBACK will use $data as callback function, which gets only one parameter -- number + * of bytes to return, and returns a string if there is some data or null if there is no more data + * + * If $data is a resource then it'll be used as a resource instead. + * + * Currently, only binary mode is supported. As such, if the line endings need to be adjusted, you will need to take + * care of that, yourself. + * + * $mode can take an additional two parameters - self::RESUME and self::RESUME_START. These are bitwise AND'd with + * $mode. So if you want to resume upload of a 300mb file on the local file system you'd set $mode to the following: + * + * self::SOURCE_LOCAL_FILE | self::RESUME + * + * If you wanted to simply append the full contents of a local file to the full contents of a remote file you'd replace + * self::RESUME with self::RESUME_START. + * + * If $mode & (self::RESUME | self::RESUME_START) then self::RESUME_START will be assumed. + * + * $start and $local_start give you more fine grained control over this process and take precident over self::RESUME + * when they're non-negative. ie. $start could let you write at the end of a file (like self::RESUME) or in the middle + * of one. $local_start could let you start your reading from the end of a file (like self::RESUME_START) or in the + * middle of one. + * + * Setting $local_start to > 0 or $mode | self::RESUME_START doesn't do anything unless $mode | self::SOURCE_LOCAL_FILE. + * + * @param string $remote_file + * @param string|resource $data + * @param int $mode + * @param int $start + * @param int $local_start + * @param callable|null $progressCallback + * @return bool + * @access public + * @internal ASCII mode for SFTPv4/5/6 can be supported by adding a new function - \phpseclib\Net\SFTP::setMode(). + */ + function put($remote_file, $data, $mode = self::SOURCE_STRING, $start = -1, $local_start = -1, $progressCallback = null) + { + if (!$this->_precheck()) { + return false; + } + + $remote_file = $this->_realpath($remote_file); + if ($remote_file === false) { + return false; + } + + $this->_remove_from_stat_cache($remote_file); + + if ($this->version >= 5) { + $flags = NET_SFTP_OPEN_OPEN_OR_CREATE; + } else { + $flags = NET_SFTP_OPEN_WRITE | NET_SFTP_OPEN_CREATE; + // according to the SFTP specs, NET_SFTP_OPEN_APPEND should "force all writes to append data at the end of the file." + // in practice, it doesn't seem to do that. + //$flags|= ($mode & SFTP::RESUME) ? NET_SFTP_OPEN_APPEND : NET_SFTP_OPEN_TRUNCATE; + } + + if ($start >= 0) { + $offset = $start; + } elseif ($mode & (self::RESUME | self::RESUME_START)) { + // if NET_SFTP_OPEN_APPEND worked as it should _size() wouldn't need to be called + $size = $this->size($remote_file); + $offset = $size !== false ? $size : 0; + } else { + $offset = 0; + if ($this->version >= 5) { + $flags = NET_SFTP_OPEN_CREATE_TRUNCATE; + } else { + $flags|= NET_SFTP_OPEN_TRUNCATE; + } + } + + $packet = pack('Na*', strlen($remote_file), $remote_file); + $packet.= $this->version >= 5 ? + pack('N3', 0, $flags, 0) : + pack('N2', $flags, 0); + if (!$this->_send_sftp_packet(NET_SFTP_OPEN, $packet)) { + return false; + } + + $response = $this->_get_sftp_packet(); + switch ($this->packet_type) { + case NET_SFTP_HANDLE: + $handle = substr($response, 4); + break; + case NET_SFTP_STATUS: + $this->_logError($response); + return false; + default: + user_error('Expected SSH_FXP_HANDLE or SSH_FXP_STATUS'); + return false; + } + + // http://tools.ietf.org/html/draft-ietf-secsh-filexfer-13#section-8.2.3 + $dataCallback = false; + switch (true) { + case $mode & self::SOURCE_CALLBACK: + if (!is_callable($data)) { + user_error("\$data should be is_callable() if you specify SOURCE_CALLBACK flag"); + } + $dataCallback = $data; + // do nothing + break; + case is_resource($data): + $mode = $mode & ~self::SOURCE_LOCAL_FILE; + $info = stream_get_meta_data($data); + if (isset($info['wrapper_type']) && $info['wrapper_type'] == 'PHP' && $info['stream_type'] == 'Input') { + $fp = fopen('php://memory', 'w+'); + stream_copy_to_stream($data, $fp); + rewind($fp); + } else { + $fp = $data; + } + break; + case $mode & self::SOURCE_LOCAL_FILE: + if (!is_file($data)) { + user_error("$data is not a valid file"); + return false; + } + $fp = @fopen($data, 'rb'); + if (!$fp) { + return false; + } + } + + if (isset($fp)) { + $stat = fstat($fp); + $size = !empty($stat) ? $stat['size'] : 0; + + if ($local_start >= 0) { + fseek($fp, $local_start); + $size-= $local_start; + } elseif ($mode & self::RESUME) { + fseek($fp, $offset); + $size-= $offset; + } + + } elseif ($dataCallback) { + $size = 0; + } else { + $size = strlen($data); + } + + $sent = 0; + $size = $size < 0 ? ($size & 0x7FFFFFFF) + 0x80000000 : $size; + + $sftp_packet_size = $this->max_sftp_packet; + // make the SFTP packet be exactly the SFTP packet size by including the bytes in the NET_SFTP_WRITE packets "header" + $sftp_packet_size-= strlen($handle) + 25; + $i = $j = 0; + while ($dataCallback || ($size === 0 || $sent < $size)) { + if ($dataCallback) { + $temp = call_user_func($dataCallback, $sftp_packet_size); + if (is_null($temp)) { + break; + } + } else { + $temp = isset($fp) ? fread($fp, $sftp_packet_size) : substr($data, $sent, $sftp_packet_size); + if ($temp === false || $temp === '') { + break; + } + } + + $subtemp = $offset + $sent; + $packet = pack('Na*N3a*', strlen($handle), $handle, $subtemp / 4294967296, $subtemp, strlen($temp), $temp); + if (!$this->_send_sftp_packet(NET_SFTP_WRITE, $packet, $j)) { + if ($mode & self::SOURCE_LOCAL_FILE) { + fclose($fp); + } + return false; + } + $sent+= strlen($temp); + if (is_callable($progressCallback)) { + call_user_func($progressCallback, $sent); + } + + $i++; + $j++; + + if ($i == NET_SFTP_UPLOAD_QUEUE_SIZE) { + if (!$this->_read_put_responses($i)) { + $i = 0; + break; + } + $i = 0; + } + } + + $result = $this->_close_handle($handle); + + if (!$this->_read_put_responses($i)) { + if ($mode & self::SOURCE_LOCAL_FILE) { + fclose($fp); + } + $this->_close_handle($handle); + return false; + } + + if ($mode & SFTP::SOURCE_LOCAL_FILE) { + if (isset($fp) && is_resource($fp)) { + fclose($fp); + } + + if ($this->preserveTime) { + $stat = stat($data); + if ($this->version < 4) { + $attr = pack('N3', NET_SFTP_ATTR_ACCESSTIME, $stat['atime'], $stat['mtime']); + } else { + $attr = pack( + 'N5', + NET_SFTP_ATTR_ACCESSTIME | NET_SFTP_ATTR_MODIFYTIME, + $stat['atime'] / 4294967296, + $stat['atime'], + $stat['mtime'] / 4294967296, + $stat['mtime'] + ); + } + + if (!$this->_setstat($remote_file, $attr, false)) { + user_error('Error setting file time'); + } + } + } + + return $result; + } + + /** + * Reads multiple successive SSH_FXP_WRITE responses + * + * Sending an SSH_FXP_WRITE packet and immediately reading its response isn't as efficient as blindly sending out $i + * SSH_FXP_WRITEs, in succession, and then reading $i responses. + * + * @param int $i + * @return bool + * @access private + */ + function _read_put_responses($i) + { + while ($i--) { + $response = $this->_get_sftp_packet(); + if ($this->packet_type != NET_SFTP_STATUS) { + user_error('Expected SSH_FXP_STATUS'); + return false; + } + + if (strlen($response) < 4) { + return false; + } + extract(unpack('Nstatus', $this->_string_shift($response, 4))); + if ($status != NET_SFTP_STATUS_OK) { + $this->_logError($response, $status); + break; + } + } + + return $i < 0; + } + + /** + * Close handle + * + * @param string $handle + * @return bool + * @access private + */ + function _close_handle($handle) + { + if (!$this->_send_sftp_packet(NET_SFTP_CLOSE, pack('Na*', strlen($handle), $handle))) { + return false; + } + + // "The client MUST release all resources associated with the handle regardless of the status." + // -- http://tools.ietf.org/html/draft-ietf-secsh-filexfer-13#section-8.1.3 + $response = $this->_get_sftp_packet(); + if ($this->packet_type != NET_SFTP_STATUS) { + user_error('Expected SSH_FXP_STATUS'); + return false; + } + + if (strlen($response) < 4) { + return false; + } + extract(unpack('Nstatus', $this->_string_shift($response, 4))); + if ($status != NET_SFTP_STATUS_OK) { + $this->_logError($response, $status); + return false; + } + + return true; + } + + /** + * Downloads a file from the SFTP server. + * + * Returns a string containing the contents of $remote_file if $local_file is left undefined or a boolean false if + * the operation was unsuccessful. If $local_file is defined, returns true or false depending on the success of the + * operation. + * + * $offset and $length can be used to download files in chunks. + * + * @param string $remote_file + * @param string $local_file + * @param int $offset + * @param int $length + * @param callable|null $progressCallback + * @return mixed + * @access public + */ + function get($remote_file, $local_file = false, $offset = 0, $length = -1, $progressCallback = null) + { + if (!$this->_precheck()) { + return false; + } + + $remote_file = $this->_realpath($remote_file); + if ($remote_file === false) { + return false; + } + + $packet = pack('Na*', strlen($remote_file), $remote_file); + $packet.= $this->version >= 5 ? + pack('N3', 0, NET_SFTP_OPEN_OPEN_EXISTING, 0) : + pack('N2', NET_SFTP_OPEN_READ, 0); + if (!$this->_send_sftp_packet(NET_SFTP_OPEN, $packet)) { + return false; + } + + $response = $this->_get_sftp_packet(); + switch ($this->packet_type) { + case NET_SFTP_HANDLE: + $handle = substr($response, 4); + break; + case NET_SFTP_STATUS: // presumably SSH_FX_NO_SUCH_FILE or SSH_FX_PERMISSION_DENIED + $this->_logError($response); + return false; + default: + user_error('Expected SSH_FXP_HANDLE or SSH_FXP_STATUS'); + return false; + } + + if (is_resource($local_file)) { + $fp = $local_file; + $stat = fstat($fp); + $res_offset = $stat['size']; + } else { + $res_offset = 0; + if ($local_file !== false && !is_callable($local_file)) { + $fp = fopen($local_file, 'wb'); + if (!$fp) { + return false; + } + } else { + $content = ''; + } + } + + $fclose_check = $local_file !== false && !is_callable($local_file) && !is_resource($local_file); + + $start = $offset; + $read = 0; + while (true) { + $i = 0; + + while ($i < NET_SFTP_QUEUE_SIZE && ($length < 0 || $read < $length)) { + $tempoffset = $start + $read; + + $packet_size = $length > 0 ? min($this->max_sftp_packet, $length - $read) : $this->max_sftp_packet; + + $packet = pack('Na*N3', strlen($handle), $handle, $tempoffset / 4294967296, $tempoffset, $packet_size); + if (!$this->_send_sftp_packet(NET_SFTP_READ, $packet, $i)) { + if ($fclose_check) { + fclose($fp); + } + return false; + } + $packet = null; + $read+= $packet_size; + $i++; + } + + if (!$i) { + break; + } + + $packets_sent = $i - 1; + + $clear_responses = false; + while ($i > 0) { + $i--; + + if ($clear_responses) { + $this->_get_sftp_packet($packets_sent - $i); + continue; + } else { + $response = $this->_get_sftp_packet($packets_sent - $i); + } + + switch ($this->packet_type) { + case NET_SFTP_DATA: + $temp = substr($response, 4); + $offset+= strlen($temp); + if ($local_file === false) { + $content.= $temp; + } elseif (is_callable($local_file)) { + $local_file($temp); + } else { + fputs($fp, $temp); + } + if (is_callable($progressCallback)) { + call_user_func($progressCallback, $offset); + } + $temp = null; + break; + case NET_SFTP_STATUS: + // could, in theory, return false if !strlen($content) but we'll hold off for the time being + $this->_logError($response); + $clear_responses = true; // don't break out of the loop yet, so we can read the remaining responses + break; + default: + if ($fclose_check) { + fclose($fp); + } + // maybe the file was successfully transferred, maybe it wasn't + if ($this->channel_close) { + $this->partial_init = false; + $this->_init_sftp_connection(); + return false; + } else { + user_error('Expected SSH_FX_DATA or SSH_FXP_STATUS'); + } + } + $response = null; + } + + if ($clear_responses) { + break; + } + } + + if ($fclose_check) { + fclose($fp); + + if ($this->preserveTime) { + $stat = $this->stat($remote_file); + touch($local_file, $stat['mtime'], $stat['atime']); + } + } + + if (!$this->_close_handle($handle)) { + return false; + } + + // if $content isn't set that means a file was written to + return isset($content) ? $content : true; + } + + /** + * Deletes a file on the SFTP server. + * + * @param string $path + * @param bool $recursive + * @return bool + * @access public + */ + function delete($path, $recursive = true) + { + if (!$this->_precheck()) { + return false; + } + + if (is_object($path)) { + // It's an object. Cast it as string before we check anything else. + $path = (string) $path; + } + + if (!is_string($path) || $path == '') { + return false; + } + + $path = $this->_realpath($path); + if ($path === false) { + return false; + } + + // http://tools.ietf.org/html/draft-ietf-secsh-filexfer-13#section-8.3 + if (!$this->_send_sftp_packet(NET_SFTP_REMOVE, pack('Na*', strlen($path), $path))) { + return false; + } + + $response = $this->_get_sftp_packet(); + if ($this->packet_type != NET_SFTP_STATUS) { + user_error('Expected SSH_FXP_STATUS'); + return false; + } + + // if $status isn't SSH_FX_OK it's probably SSH_FX_NO_SUCH_FILE or SSH_FX_PERMISSION_DENIED + if (strlen($response) < 4) { + return false; + } + extract(unpack('Nstatus', $this->_string_shift($response, 4))); + if ($status != NET_SFTP_STATUS_OK) { + $this->_logError($response, $status); + if (!$recursive) { + return false; + } + $i = 0; + $result = $this->_delete_recursive($path, $i); + $this->_read_put_responses($i); + return $result; + } + + $this->_remove_from_stat_cache($path); + + return true; + } + + /** + * Recursively deletes directories on the SFTP server + * + * Minimizes directory lookups and SSH_FXP_STATUS requests for speed. + * + * @param string $path + * @param int $i + * @return bool + * @access private + */ + function _delete_recursive($path, &$i) + { + if (!$this->_read_put_responses($i)) { + return false; + } + $i = 0; + $entries = $this->_list($path, true); + + // The folder does not exist at all, so we cannot delete it. + if ($entries === NET_SFTP_STATUS_NO_SUCH_FILE) { + return false; + } + + // Normally $entries would have at least . and .. but it might not if the directories + // permissions didn't allow reading. If this happens then default to an empty list of files. + if ($entries === false || is_int($entries)) { + $entries = array(); + } + + unset($entries['.'], $entries['..']); + foreach ($entries as $filename => $props) { + if (!isset($props['type'])) { + return false; + } + + $temp = $path . '/' . $filename; + if ($props['type'] == NET_SFTP_TYPE_DIRECTORY) { + if (!$this->_delete_recursive($temp, $i)) { + return false; + } + } else { + if (!$this->_send_sftp_packet(NET_SFTP_REMOVE, pack('Na*', strlen($temp), $temp))) { + return false; + } + $this->_remove_from_stat_cache($temp); + + $i++; + + if ($i >= NET_SFTP_QUEUE_SIZE) { + if (!$this->_read_put_responses($i)) { + return false; + } + $i = 0; + } + } + } + + if (!$this->_send_sftp_packet(NET_SFTP_RMDIR, pack('Na*', strlen($path), $path))) { + return false; + } + $this->_remove_from_stat_cache($path); + + $i++; + + if ($i >= NET_SFTP_QUEUE_SIZE) { + if (!$this->_read_put_responses($i)) { + return false; + } + $i = 0; + } + + return true; + } + + /** + * Checks whether a file or directory exists + * + * @param string $path + * @return bool + * @access public + */ + function file_exists($path) + { + if ($this->use_stat_cache) { + if (!$this->_precheck()) { + return false; + } + + $path = $this->_realpath($path); + + $result = $this->_query_stat_cache($path); + + if (isset($result)) { + // return true if $result is an array or if it's an stdClass object + return $result !== false; + } + } + + return $this->stat($path) !== false; + } + + /** + * Tells whether the filename is a directory + * + * @param string $path + * @return bool + * @access public + */ + function is_dir($path) + { + $result = $this->_get_stat_cache_prop($path, 'type'); + if ($result === false) { + return false; + } + return $result === NET_SFTP_TYPE_DIRECTORY; + } + + /** + * Tells whether the filename is a regular file + * + * @param string $path + * @return bool + * @access public + */ + function is_file($path) + { + $result = $this->_get_stat_cache_prop($path, 'type'); + if ($result === false) { + return false; + } + return $result === NET_SFTP_TYPE_REGULAR; + } + + /** + * Tells whether the filename is a symbolic link + * + * @param string $path + * @return bool + * @access public + */ + function is_link($path) + { + $result = $this->_get_lstat_cache_prop($path, 'type'); + if ($result === false) { + return false; + } + return $result === NET_SFTP_TYPE_SYMLINK; + } + + /** + * Tells whether a file exists and is readable + * + * @param string $path + * @return bool + * @access public + */ + function is_readable($path) + { + if (!$this->_precheck()) { + return false; + } + + $path = $this->_realpath($path); + + $packet = pack('Na*N2', strlen($path), $path, NET_SFTP_OPEN_READ, 0); + if (!$this->_send_sftp_packet(NET_SFTP_OPEN, $packet)) { + return false; + } + + $response = $this->_get_sftp_packet(); + switch ($this->packet_type) { + case NET_SFTP_HANDLE: + return true; + case NET_SFTP_STATUS: // presumably SSH_FX_NO_SUCH_FILE or SSH_FX_PERMISSION_DENIED + return false; + default: + user_error('Expected SSH_FXP_HANDLE or SSH_FXP_STATUS'); + return false; + } + } + + /** + * Tells whether the filename is writable + * + * @param string $path + * @return bool + * @access public + */ + function is_writable($path) + { + if (!$this->_precheck()) { + return false; + } + + $path = $this->_realpath($path); + + $packet = pack('Na*N2', strlen($path), $path, NET_SFTP_OPEN_WRITE, 0); + if (!$this->_send_sftp_packet(NET_SFTP_OPEN, $packet)) { + return false; + } + + $response = $this->_get_sftp_packet(); + switch ($this->packet_type) { + case NET_SFTP_HANDLE: + return true; + case NET_SFTP_STATUS: // presumably SSH_FX_NO_SUCH_FILE or SSH_FX_PERMISSION_DENIED + return false; + default: + user_error('Expected SSH_FXP_HANDLE or SSH_FXP_STATUS'); + return false; + } + } + + /** + * Tells whether the filename is writeable + * + * Alias of is_writable + * + * @param string $path + * @return bool + * @access public + */ + function is_writeable($path) + { + return $this->is_writable($path); + } + + /** + * Gets last access time of file + * + * @param string $path + * @return mixed + * @access public + */ + function fileatime($path) + { + return $this->_get_stat_cache_prop($path, 'atime'); + } + + /** + * Gets file modification time + * + * @param string $path + * @return mixed + * @access public + */ + function filemtime($path) + { + return $this->_get_stat_cache_prop($path, 'mtime'); + } + + /** + * Gets file permissions + * + * @param string $path + * @return mixed + * @access public + */ + function fileperms($path) + { + return $this->_get_stat_cache_prop($path, 'permissions'); + } + + /** + * Gets file owner + * + * @param string $path + * @return mixed + * @access public + */ + function fileowner($path) + { + return $this->_get_stat_cache_prop($path, 'uid'); + } + + /** + * Gets file group + * + * @param string $path + * @return mixed + * @access public + */ + function filegroup($path) + { + return $this->_get_stat_cache_prop($path, 'gid'); + } + + /** + * Gets file size + * + * @param string $path + * @return mixed + * @access public + */ + function filesize($path) + { + return $this->_get_stat_cache_prop($path, 'size'); + } + + /** + * Gets file type + * + * @param string $path + * @return mixed + * @access public + */ + function filetype($path) + { + $type = $this->_get_stat_cache_prop($path, 'type'); + if ($type === false) { + return false; + } + + switch ($type) { + case NET_SFTP_TYPE_BLOCK_DEVICE: + return 'block'; + case NET_SFTP_TYPE_CHAR_DEVICE: + return 'char'; + case NET_SFTP_TYPE_DIRECTORY: + return 'dir'; + case NET_SFTP_TYPE_FIFO: + return 'fifo'; + case NET_SFTP_TYPE_REGULAR: + return 'file'; + case NET_SFTP_TYPE_SYMLINK: + return 'link'; + default: + return false; + } + } + + /** + * Return a stat properity + * + * Uses cache if appropriate. + * + * @param string $path + * @param string $prop + * @return mixed + * @access private + */ + function _get_stat_cache_prop($path, $prop) + { + return $this->_get_xstat_cache_prop($path, $prop, 'stat'); + } + + /** + * Return an lstat properity + * + * Uses cache if appropriate. + * + * @param string $path + * @param string $prop + * @return mixed + * @access private + */ + function _get_lstat_cache_prop($path, $prop) + { + return $this->_get_xstat_cache_prop($path, $prop, 'lstat'); + } + + /** + * Return a stat or lstat properity + * + * Uses cache if appropriate. + * + * @param string $path + * @param string $prop + * @param mixed $type + * @return mixed + * @access private + */ + function _get_xstat_cache_prop($path, $prop, $type) + { + if (!$this->_precheck()) { + return false; + } + + if ($this->use_stat_cache) { + $path = $this->_realpath($path); + + $result = $this->_query_stat_cache($path); + + if (is_object($result) && isset($result->$type)) { + return $result->{$type}[$prop]; + } + } + + $result = $this->$type($path); + + if ($result === false || !isset($result[$prop])) { + return false; + } + + return $result[$prop]; + } + + /** + * Renames a file or a directory on the SFTP server. + * + * If the file already exists this will return false + * + * @param string $oldname + * @param string $newname + * @return bool + * @access public + */ + function rename($oldname, $newname) + { + if (!$this->_precheck()) { + return false; + } + + $oldname = $this->_realpath($oldname); + $newname = $this->_realpath($newname); + if ($oldname === false || $newname === false) { + return false; + } + + // http://tools.ietf.org/html/draft-ietf-secsh-filexfer-13#section-8.3 + $packet = pack('Na*Na*', strlen($oldname), $oldname, strlen($newname), $newname); + if ($this->version >= 5) { + /* quoting https://datatracker.ietf.org/doc/html/draft-ietf-secsh-filexfer-05#section-6.5 , + + 'flags' is 0 or a combination of: + + SSH_FXP_RENAME_OVERWRITE 0x00000001 + SSH_FXP_RENAME_ATOMIC 0x00000002 + SSH_FXP_RENAME_NATIVE 0x00000004 + + (none of these are currently supported) */ + $packet.= "\0\0\0\0"; + } + if (!$this->_send_sftp_packet(NET_SFTP_RENAME, $packet)) { + return false; + } + + $response = $this->_get_sftp_packet(); + if ($this->packet_type != NET_SFTP_STATUS) { + user_error('Expected SSH_FXP_STATUS'); + return false; + } + + // if $status isn't SSH_FX_OK it's probably SSH_FX_NO_SUCH_FILE or SSH_FX_PERMISSION_DENIED + if (strlen($response) < 4) { + return false; + } + extract(unpack('Nstatus', $this->_string_shift($response, 4))); + if ($status != NET_SFTP_STATUS_OK) { + $this->_logError($response, $status); + return false; + } + + // don't move the stat cache entry over since this operation could very well change the + // atime and mtime attributes + //$this->_update_stat_cache($newname, $this->_query_stat_cache($oldname)); + $this->_remove_from_stat_cache($oldname); + $this->_remove_from_stat_cache($newname); + + return true; + } + + /** + * Parse Time + * + * See '7.7. Times' of draft-ietf-secsh-filexfer-13 for more info. + * + * @param string $key + * @param int $flags + * @param string $response + * @return array + * @access private + */ + function _parseTime($key, $flags, &$response) + { + if (strlen($response) < 8) { + user_error('Malformed file attributes'); + return array(); + } + $attr = array(); + $attr[$key] = hexdec(bin2hex($this->_string_shift($response, 8))); + if ($flags & NET_SFTP_ATTR_SUBSECOND_TIMES) { + $attr+= extract(unpack('N' . $key . '_nseconds', $this->_string_shift($response, 4))); + } + return $attr; + } + + /** + * Parse Attributes + * + * See '7. File Attributes' of draft-ietf-secsh-filexfer-13 for more info. + * + * @param string $response + * @return array + * @access private + */ + function _parseAttributes(&$response) + { + if ($this->version >= 4) { + $length = 5; + $format = 'Nflags/Ctype'; + } else { + $length = 4; + $format = 'Nflags'; + } + + $attr = array(); + if (strlen($response) < $length) { + user_error('Malformed file attributes'); + return array(); + } + extract(unpack($format, $this->_string_shift($response, $length))); + if (isset($type)) { + $attr['type'] = $type; + } + foreach ($this->attributes as $key => $value) { + switch ($flags & $key) { + case NET_SFTP_ATTR_UIDGID: + if ($this->version > 3) { + continue 2; + } + break; + case NET_SFTP_ATTR_CREATETIME: + case NET_SFTP_ATTR_MODIFYTIME: + case NET_SFTP_ATTR_ACL: + case NET_SFTP_ATTR_OWNERGROUP: + case NET_SFTP_ATTR_SUBSECOND_TIMES: + if ($this->version < 4) { + continue 2; + } + break; + case NET_SFTP_ATTR_BITS: + if ($this->version < 5) { + continue 2; + } + break; + case NET_SFTP_ATTR_ALLOCATION_SIZE: + case NET_SFTP_ATTR_TEXT_HINT: + case NET_SFTP_ATTR_MIME_TYPE: + case NET_SFTP_ATTR_LINK_COUNT: + case NET_SFTP_ATTR_UNTRANSLATED_NAME: + case NET_SFTP_ATTR_CTIME: + if ($this->version < 6) { + continue 2; + } + } + switch ($flags & $key) { + case NET_SFTP_ATTR_SIZE: // 0x00000001 + // The size attribute is defined as an unsigned 64-bit integer. + // The following will use floats on 32-bit platforms, if necessary. + // As can be seen in the BigInteger class, floats are generally + // IEEE 754 binary64 "double precision" on such platforms and + // as such can represent integers of at least 2^50 without loss + // of precision. Interpreted in filesize, 2^50 bytes = 1024 TiB. + $attr['size'] = hexdec(bin2hex($this->_string_shift($response, 8))); + break; + case NET_SFTP_ATTR_UIDGID: // 0x00000002 (SFTPv3 or earlier) + if (strlen($response) < 8) { + user_error('Malformed file attributes'); + return $attr; + } + $attr+= unpack('Nuid/Ngid', $this->_string_shift($response, 8)); + break; + case NET_SFTP_ATTR_PERMISSIONS: // 0x00000004 + if (strlen($response) < 4) { + user_error('Malformed file attributes'); + return $attr; + } + $attr+= unpack('Npermissions', $this->_string_shift($response, 4)); + // mode == permissions; permissions was the original array key and is retained for bc purposes. + // mode was added because that's the more industry standard terminology + $attr+= array('mode' => $attr['permissions']); + $fileType = $this->_parseMode($attr['permissions']); + if ($fileType !== false) { + $attr+= array('type' => $fileType); + } + break; + case NET_SFTP_ATTR_ACCESSTIME: // 0x00000008 + if ($this->version >= 4) { + $attr+= $this->_parseTime('atime', $flags, $response); + break; + } + if (strlen($response) < 8) { + user_error('Malformed file attributes'); + return $attr; + } + $attr+= unpack('Natime/Nmtime', $this->_string_shift($response, 8)); + break; + case NET_SFTP_ATTR_CREATETIME: // 0x00000010 (SFTPv4+) + $attr+= $this->_parseTime('createtime', $flags, $response); + break; + case NET_SFTP_ATTR_MODIFYTIME: // 0x00000020 + $attr+= $this->_parseTime('mtime', $flags, $response); + break; + case NET_SFTP_ATTR_ACL: // 0x00000040 + // access control list + // see https://datatracker.ietf.org/doc/html/draft-ietf-secsh-filexfer-04#section-5.7 + // currently unsupported + if (strlen($response) < 4) { + user_error('Malformed file attributes'); + return $attr; + } + extract(unpack('Ncount', $this->_string_shift($response, 4))); + for ($i = 0; $i < $count; $i++) { + if (strlen($response) < 16) { + user_error('Malformed file attributes'); + return $attr; + } + extract(unpack('Ntype/Nflag/Nmask/Nlength', $this->_string_shift($response, 16))); + if (strlen($response) < $length) { + user_error('Malformed file attributes'); + return $attr; + } + $this->_string_shift($response, $length); // who + } + break; + case NET_SFTP_ATTR_OWNERGROUP: // 0x00000080 + if (strlen($response) < 4) { + user_error('Malformed file attributes'); + return $attr; + } + extract(unpack('Nlength', $this->_string_shift($response, 4))); + if (strlen($response) < $length) { + user_error('Malformed file attributes'); + return $attr; + } + $attr['owner'] = $this->_string_shift($response, $length); + + if (strlen($response) < 4) { + user_error('Malformed file attributes'); + return $attr; + } + extract(unpack('Nlength', $this->_string_shift($response, 4))); + if (strlen($response) < $length) { + user_error('Malformed file attributes'); + return $attr; + } + $attr['group'] = $this->_string_shift($response, $length); + break; + case NET_SFTP_ATTR_SUBSECOND_TIMES: // 0x00000100 + break; + case NET_SFTP_ATTR_BITS: // 0x00000200 (SFTPv5+) + // see https://datatracker.ietf.org/doc/html/draft-ietf-secsh-filexfer-05#section-5.8 + // currently unsupported + // tells if you file is: + // readonly, system, hidden, case inensitive, archive, encrypted, compressed, sparse + // append only, immutable, sync + if (strlen($response) < 8) { + user_error('Malformed file attributes'); + return $attr; + } + extract(unpack('Nattrib-bits/Nattrib-bits-valid', $this->_string_shift($response, 8))); + break; + case NET_SFTP_ATTR_ALLOCATION_SIZE: // 0x00000400 (SFTPv6+) + // see https://datatracker.ietf.org/doc/html/draft-ietf-secsh-filexfer-13#section-7.4 + // represents the number of bytes htat the file consumes on the disk. will + // usually be larger than the 'size' field + $attr['allocation-size'] = hexdec(bin2hex($this->_string_shift($response, 8))); + break; + case NET_SFTP_ATTR_TEXT_HINT: // 0x00000800 + // https://datatracker.ietf.org/doc/html/draft-ietf-secsh-filexfer-13#section-7.10 + // currently unsupported + // tells if file is "known text", "guessed text", "known binary", "guessed binary" + extract(unpack('Ctext-hint', $this->_string_shift($response))); + break; + case NET_SFTP_ATTR_MIME_TYPE: // 0x00001000 + // see https://datatracker.ietf.org/doc/html/draft-ietf-secsh-filexfer-13#section-7.11 + if (strlen($response) < 4) { + user_error('Malformed file attributes'); + return $attr; + } + extract(unpack('Nlength', $this->_string_shift($response, 4))); + if (strlen($response) < $length) { + user_error('Malformed file attributes'); + return $attr; + } + $attr['mime-type'] = $this->_string_shift($response, $length); + break; + case NET_SFTP_ATTR_LINK_COUNT: // 0x00002000 + // see https://datatracker.ietf.org/doc/html/draft-ietf-secsh-filexfer-13#section-7.12 + if (strlen($response) < 4) { + user_error('Malformed file attributes'); + return $attr; + } + $attr+= unpack('Nlink-count', $this->_string_shift($response, 4)); + break; + case NET_SFTP_ATTR_UNTRANSLATED_NAME:// 0x00004000 + // see https://datatracker.ietf.org/doc/html/draft-ietf-secsh-filexfer-13#section-7.13 + if (strlen($response) < 4) { + user_error('Malformed file attributes'); + return $attr; + } + extract(unpack('Nlength', $this->_string_shift($response, 4))); + if (strlen($response) < $length) { + user_error('Malformed file attributes'); + return $attr; + } + $attr['untranslated-name'] = $this->_string_shift($response, $length); + break; + case NET_SFTP_ATTR_CTIME: // 0x00008000 + // 'ctime' contains the last time the file attributes were changed. The + // exact meaning of this field depends on the server. + $attr+= $this->_parseTime('ctime', $flags, $response); + break; + case NET_SFTP_ATTR_EXTENDED: // 0x80000000 + if (strlen($response) < 4) { + user_error('Malformed file attributes'); + return $attr; + } + extract(unpack('Ncount', $this->_string_shift($response, 4))); + for ($i = 0; $i < $count; $i++) { + if (strlen($response) < 4) { + user_error('Malformed file attributes'); + return $attr; + } + extract(unpack('Nlength', $this->_string_shift($response, 4))); + $key = $this->_string_shift($response, $length); + if (strlen($response) < 4) { + user_error('Malformed file attributes'); + return $attr; + } + extract(unpack('Nlength', $this->_string_shift($response, 4))); + $attr[$key] = $this->_string_shift($response, $length); + } + } + } + return $attr; + } + + /** + * Attempt to identify the file type + * + * Quoting the SFTP RFC, "Implementations MUST NOT send bits that are not defined" but they seem to anyway + * + * @param int $mode + * @return int + * @access private + */ + function _parseMode($mode) + { + // values come from http://lxr.free-electrons.com/source/include/uapi/linux/stat.h#L12 + // see, also, http://linux.die.net/man/2/stat + switch ($mode & 0170000) {// ie. 1111 0000 0000 0000 + case 0000000: // no file type specified - figure out the file type using alternative means + return false; + case 0040000: + return NET_SFTP_TYPE_DIRECTORY; + case 0100000: + return NET_SFTP_TYPE_REGULAR; + case 0120000: + return NET_SFTP_TYPE_SYMLINK; + // new types introduced in SFTPv5+ + // http://tools.ietf.org/html/draft-ietf-secsh-filexfer-05#section-5.2 + case 0010000: // named pipe (fifo) + return NET_SFTP_TYPE_FIFO; + case 0020000: // character special + return NET_SFTP_TYPE_CHAR_DEVICE; + case 0060000: // block special + return NET_SFTP_TYPE_BLOCK_DEVICE; + case 0140000: // socket + return NET_SFTP_TYPE_SOCKET; + case 0160000: // whiteout + // "SPECIAL should be used for files that are of + // a known type which cannot be expressed in the protocol" + return NET_SFTP_TYPE_SPECIAL; + default: + return NET_SFTP_TYPE_UNKNOWN; + } + } + + /** + * Parse Longname + * + * SFTPv3 doesn't provide any easy way of identifying a file type. You could try to open + * a file as a directory and see if an error is returned or you could try to parse the + * SFTPv3-specific longname field of the SSH_FXP_NAME packet. That's what this function does. + * The result is returned using the + * {@link http://tools.ietf.org/html/draft-ietf-secsh-filexfer-04#section-5.2 SFTPv4 type constants}. + * + * If the longname is in an unrecognized format bool(false) is returned. + * + * @param string $longname + * @return mixed + * @access private + */ + function _parseLongname($longname) + { + // http://en.wikipedia.org/wiki/Unix_file_types + // http://en.wikipedia.org/wiki/Filesystem_permissions#Notation_of_traditional_Unix_permissions + if (preg_match('#^[^/]([r-][w-][xstST-]){3}#', $longname)) { + switch ($longname[0]) { + case '-': + return NET_SFTP_TYPE_REGULAR; + case 'd': + return NET_SFTP_TYPE_DIRECTORY; + case 'l': + return NET_SFTP_TYPE_SYMLINK; + default: + return NET_SFTP_TYPE_SPECIAL; + } + } + + return false; + } + + /** + * Sends SFTP Packets + * + * See '6. General Packet Format' of draft-ietf-secsh-filexfer-13 for more info. + * + * @param int $type + * @param string $data + * @param int $request_id + * @see self::_get_sftp_packet() + * @see self::_send_channel_packet() + * @return bool + * @access private + */ + function _send_sftp_packet($type, $data, $request_id = 1) + { + // in SSH2.php the timeout is cumulative per function call. eg. exec() will + // timeout after 10s. but for SFTP.php it's cumulative per packet + $this->curTimeout = $this->timeout; + + $packet = $this->use_request_id ? + pack('NCNa*', strlen($data) + 5, $type, $request_id, $data) : + pack('NCa*', strlen($data) + 1, $type, $data); + + $start = strtok(microtime(), ' ') + strtok(''); // http://php.net/microtime#61838 + $result = $this->_send_channel_packet(self::CHANNEL, $packet); + $stop = strtok(microtime(), ' ') + strtok(''); + + if (defined('NET_SFTP_LOGGING')) { + $packet_type = '-> ' . $this->packet_types[$type] . + ' (' . round($stop - $start, 4) . 's)'; + if (NET_SFTP_LOGGING == self::LOG_REALTIME) { + switch (PHP_SAPI) { + case 'cli': + $start = $stop = "\r\n"; + break; + default: + $start = '
';
+                        $stop = '
'; + } + echo $start . $this->_format_log(array($data), array($packet_type)) . $stop; + @flush(); + @ob_flush(); + } else { + $this->packet_type_log[] = $packet_type; + if (NET_SFTP_LOGGING == self::LOG_COMPLEX) { + $this->packet_log[] = $data; + } + } + } + + return $result; + } + + /** + * Resets a connection for re-use + * + * @param int $reason + * @access private + */ + function _reset_connection($reason) + { + parent::_reset_connection($reason); + $this->use_request_id = false; + $this->pwd = false; + $this->requestBuffer = array(); + $this->partial_init = false; + } + + /** + * Receives SFTP Packets + * + * See '6. General Packet Format' of draft-ietf-secsh-filexfer-13 for more info. + * + * Incidentally, the number of SSH_MSG_CHANNEL_DATA messages has no bearing on the number of SFTP packets present. + * There can be one SSH_MSG_CHANNEL_DATA messages containing two SFTP packets or there can be two SSH_MSG_CHANNEL_DATA + * messages containing one SFTP packet. + * + * @see self::_send_sftp_packet() + * @return string + * @access private + */ + function _get_sftp_packet($request_id = null) + { + $this->channel_close = false; + + if (isset($request_id) && isset($this->requestBuffer[$request_id])) { + $this->packet_type = $this->requestBuffer[$request_id]['packet_type']; + $temp = $this->requestBuffer[$request_id]['packet']; + unset($this->requestBuffer[$request_id]); + return $temp; + } + + // in SSH2.php the timeout is cumulative per function call. eg. exec() will + // timeout after 10s. but for SFTP.php it's cumulative per packet + $this->curTimeout = $this->timeout; + + $start = strtok(microtime(), ' ') + strtok(''); // http://php.net/microtime#61838 + + // SFTP packet length + while (strlen($this->packet_buffer) < 4) { + $temp = $this->_get_channel_packet(self::CHANNEL, true); + if ($temp === true) { + if ($this->channel_status[self::CHANNEL] === NET_SSH2_MSG_CHANNEL_CLOSE) { + $this->channel_close = true; + } + $this->packet_type = false; + $this->packet_buffer = ''; + return false; + } + if ($temp === false) { + return false; + } + $this->packet_buffer.= $temp; + } + if (strlen($this->packet_buffer) < 4) { + return false; + } + extract(unpack('Nlength', $this->_string_shift($this->packet_buffer, 4))); + $tempLength = $length; + $tempLength-= strlen($this->packet_buffer); + + // 256 * 1024 is what SFTP_MAX_MSG_LENGTH is set to in OpenSSH's sftp-common.h + if (!$this->allow_arbitrary_length_packets && !$this->use_request_id && $tempLength > 256 * 1024) { + user_error('Invalid SFTP packet size'); + return false; + } + + // SFTP packet type and data payload + while ($tempLength > 0) { + $temp = $this->_get_channel_packet(self::CHANNEL, true); + if (is_bool($temp)) { + if ($temp && $this->channel_status[self::CHANNEL] === NET_SSH2_MSG_CHANNEL_CLOSE) { + $this->channel_close = true; + } + $this->packet_type = false; + $this->packet_buffer = ''; + return false; + } + $this->packet_buffer.= $temp; + $tempLength-= strlen($temp); + } + + $stop = strtok(microtime(), ' ') + strtok(''); + + $this->packet_type = ord($this->_string_shift($this->packet_buffer)); + + if ($this->use_request_id) { + extract(unpack('Npacket_id', $this->_string_shift($this->packet_buffer, 4))); // remove the request id + $length-= 5; // account for the request id and the packet type + } else { + $length-= 1; // account for the packet type + } + + $packet = $this->_string_shift($this->packet_buffer, $length); + + if (defined('NET_SFTP_LOGGING')) { + $packet_type = '<- ' . $this->packet_types[$this->packet_type] . + ' (' . round($stop - $start, 4) . 's)'; + if (NET_SFTP_LOGGING == self::LOG_REALTIME) { + switch (PHP_SAPI) { + case 'cli': + $start = $stop = "\r\n"; + break; + default: + $start = '
';
+                        $stop = '
'; + } + echo $start . $this->_format_log(array($packet), array($packet_type)) . $stop; + @flush(); + @ob_flush(); + } else { + $this->packet_type_log[] = $packet_type; + if (NET_SFTP_LOGGING == self::LOG_COMPLEX) { + $this->packet_log[] = $packet; + } + } + } + + if (isset($request_id) && $this->use_request_id && $packet_id != $request_id) { + $this->requestBuffer[$packet_id] = array( + 'packet_type' => $this->packet_type, + 'packet' => $packet + ); + return $this->_get_sftp_packet($request_id); + } + + return $packet; + } + + /** + * Returns a log of the packets that have been sent and received. + * + * Returns a string if NET_SFTP_LOGGING == NET_SFTP_LOG_COMPLEX, an array if NET_SFTP_LOGGING == NET_SFTP_LOG_SIMPLE and false if !defined('NET_SFTP_LOGGING') + * + * @access public + * @return string or Array + */ + function getSFTPLog() + { + if (!defined('NET_SFTP_LOGGING')) { + return false; + } + + switch (NET_SFTP_LOGGING) { + case self::LOG_COMPLEX: + return $this->_format_log($this->packet_log, $this->packet_type_log); + break; + //case self::LOG_SIMPLE: + default: + return $this->packet_type_log; + } + } + + /** + * Returns all errors on the SFTP layer + * + * @return array + * @access public + */ + function getSFTPErrors() + { + return $this->sftp_errors; + } + + /** + * Returns the last error on the SFTP layer + * + * @return string + * @access public + */ + function getLastSFTPError() + { + return count($this->sftp_errors) ? $this->sftp_errors[count($this->sftp_errors) - 1] : ''; + } + + /** + * Get supported SFTP versions + * + * @return array + * @access public + */ + function getSupportedVersions() + { + if (!($this->bitmap & SSH2::MASK_LOGIN)) { + return false; + } + + if (!$this->partial_init) { + $this->_partial_init_sftp_connection(); + } + + $temp = array('version' => $this->defaultVersion); + if (isset($this->extensions['versions'])) { + $temp['extensions'] = $this->extensions['versions']; + } + return $temp; + } + + /** + * Get supported SFTP versions + * + * @return array + * @access public + */ + function getNegotiatedVersion() + { + if (!$this->_precheck()) { + return false; + } + + return $this->version; + } + + /** + * Set preferred version + * + * If you're preferred version isn't supported then the highest supported + * version of SFTP will be utilized. Set to null or false or int(0) to + * unset the preferred version + * + * @param int $version + * @access public + */ + function setPreferredVersion($version) + { + $this->preferredVersion = $version; + } + + /** + * Disconnect + * + * @param int $reason + * @return bool + * @access private + */ + function _disconnect($reason) + { + $this->pwd = false; + parent::_disconnect($reason); + } + + /** + * Enable Date Preservation + * + * @access public + */ + function enableDatePreservation() + { + $this->preserveTime = true; + } + + /** + * Disable Date Preservation + * + * @access public + */ + function disableDatePreservation() + { + $this->preserveTime = false; + } +} diff --git a/3rdparty/phpseclib/phpseclib/phpseclib/Net/SFTP/Stream.php b/3rdparty/phpseclib/phpseclib/phpseclib/Net/SFTP/Stream.php new file mode 100644 index 00000000..ec9e5841 --- /dev/null +++ b/3rdparty/phpseclib/phpseclib/phpseclib/Net/SFTP/Stream.php @@ -0,0 +1,796 @@ + + * @copyright 2013 Jim Wigginton + * @license http://www.opensource.org/licenses/mit-license.html MIT License + * @link http://phpseclib.sourceforge.net + */ + +namespace phpseclib\Net\SFTP; + +use phpseclib\Crypt\RSA; +use phpseclib\Net\SFTP; + +/** + * SFTP Stream Wrapper + * + * @package SFTP + * @author Jim Wigginton + * @access public + */ +class Stream +{ + /** + * SFTP instances + * + * Rather than re-create the connection we re-use instances if possible + * + * @var array + */ + static $instances; + + /** + * SFTP instance + * + * @var object + * @access private + */ + var $sftp; + + /** + * Path + * + * @var string + * @access private + */ + var $path; + + /** + * Mode + * + * @var string + * @access private + */ + var $mode; + + /** + * Position + * + * @var int + * @access private + */ + var $pos; + + /** + * Size + * + * @var int + * @access private + */ + var $size; + + /** + * Directory entries + * + * @var array + * @access private + */ + var $entries; + + /** + * EOF flag + * + * @var bool + * @access private + */ + var $eof; + + /** + * Context resource + * + * Technically this needs to be publically accessible so PHP can set it directly + * + * @var resource + * @access public + */ + var $context; + + /** + * Notification callback function + * + * @var callable + * @access public + */ + var $notification; + + /** + * Registers this class as a URL wrapper. + * + * @param string $protocol The wrapper name to be registered. + * @return bool True on success, false otherwise. + * @access public + */ + static function register($protocol = 'sftp') + { + if (in_array($protocol, stream_get_wrappers(), true)) { + return false; + } + return stream_wrapper_register($protocol, get_called_class()); + } + + /** + * The Constructor + * + * @access public + */ + function __construct() + { + if (defined('NET_SFTP_STREAM_LOGGING')) { + echo "__construct()\r\n"; + } + } + + /** + * Path Parser + * + * Extract a path from a URI and actually connect to an SSH server if appropriate + * + * If "notification" is set as a context parameter the message code for successful login is + * NET_SSH2_MSG_USERAUTH_SUCCESS. For a failed login it's NET_SSH2_MSG_USERAUTH_FAILURE. + * + * @param string $path + * @return string + * @access private + */ + function _parse_path($path) + { + $orig = $path; + extract(parse_url($path) + array('port' => 22)); + if (isset($query)) { + $path.= '?' . $query; + } elseif (preg_match('/(\?|\?#)$/', $orig)) { + $path.= '?'; + } + if (isset($fragment)) { + $path.= '#' . $fragment; + } elseif ($orig[strlen($orig) - 1] == '#') { + $path.= '#'; + } + + if (!isset($host)) { + return false; + } + + if (isset($this->context)) { + $context = stream_context_get_params($this->context); + if (isset($context['notification'])) { + $this->notification = $context['notification']; + } + } + + if ($host[0] == '$') { + $host = substr($host, 1); + global ${$host}; + if (($$host instanceof SFTP) === false) { + return false; + } + $this->sftp = $$host; + } else { + if (isset($this->context)) { + $context = stream_context_get_options($this->context); + } + if (isset($context[$scheme]['session'])) { + $sftp = $context[$scheme]['session']; + } + if (isset($context[$scheme]['sftp'])) { + $sftp = $context[$scheme]['sftp']; + } + if (isset($sftp) && $sftp instanceof SFTP) { + $this->sftp = $sftp; + return $path; + } + if (isset($context[$scheme]['username'])) { + $user = $context[$scheme]['username']; + } + if (isset($context[$scheme]['password'])) { + $pass = $context[$scheme]['password']; + } + if (isset($context[$scheme]['privkey']) && $context[$scheme]['privkey'] instanceof RSA) { + $pass = $context[$scheme]['privkey']; + } + + if (!isset($user) || !isset($pass)) { + return false; + } + + // casting $pass to a string is necessary in the event that it's a \phpseclib\Crypt\RSA object + if (isset(self::$instances[$host][$port][$user][(string) $pass])) { + $this->sftp = self::$instances[$host][$port][$user][(string) $pass]; + } else { + $this->sftp = new SFTP($host, $port); + $this->sftp->disableStatCache(); + if (isset($this->notification) && is_callable($this->notification)) { + /* if !is_callable($this->notification) we could do this: + + user_error('fopen(): failed to call user notifier', E_USER_WARNING); + + the ftp wrapper gives errors like that when the notifier isn't callable. + i've opted not to do that, however, since the ftp wrapper gives the line + on which the fopen occurred as the line number - not the line that the + user_error is on. + */ + call_user_func($this->notification, STREAM_NOTIFY_CONNECT, STREAM_NOTIFY_SEVERITY_INFO, '', 0, 0, 0); + call_user_func($this->notification, STREAM_NOTIFY_AUTH_REQUIRED, STREAM_NOTIFY_SEVERITY_INFO, '', 0, 0, 0); + if (!$this->sftp->login($user, $pass)) { + call_user_func($this->notification, STREAM_NOTIFY_AUTH_RESULT, STREAM_NOTIFY_SEVERITY_ERR, 'Login Failure', NET_SSH2_MSG_USERAUTH_FAILURE, 0, 0); + return false; + } + call_user_func($this->notification, STREAM_NOTIFY_AUTH_RESULT, STREAM_NOTIFY_SEVERITY_INFO, 'Login Success', NET_SSH2_MSG_USERAUTH_SUCCESS, 0, 0); + } else { + if (!$this->sftp->login($user, $pass)) { + return false; + } + } + self::$instances[$host][$port][$user][(string) $pass] = $this->sftp; + } + } + + return $path; + } + + /** + * Opens file or URL + * + * @param string $path + * @param string $mode + * @param int $options + * @param string $opened_path + * @return bool + * @access public + */ + function _stream_open($path, $mode, $options, &$opened_path) + { + $path = $this->_parse_path($path); + + if ($path === false) { + return false; + } + $this->path = $path; + + $this->size = $this->sftp->size($path); + $this->mode = preg_replace('#[bt]$#', '', $mode); + $this->eof = false; + + if ($this->size === false) { + if ($this->mode[0] == 'r') { + return false; + } else { + $this->sftp->touch($path); + $this->size = 0; + } + } else { + switch ($this->mode[0]) { + case 'x': + return false; + case 'w': + $this->sftp->truncate($path, 0); + $this->size = 0; + } + } + + $this->pos = $this->mode[0] != 'a' ? 0 : $this->size; + + return true; + } + + /** + * Read from stream + * + * @param int $count + * @return mixed + * @access public + */ + function _stream_read($count) + { + switch ($this->mode) { + case 'w': + case 'a': + case 'x': + case 'c': + return false; + } + + // commented out because some files - eg. /dev/urandom - will say their size is 0 when in fact it's kinda infinite + //if ($this->pos >= $this->size) { + // $this->eof = true; + // return false; + //} + + $result = $this->sftp->get($this->path, false, $this->pos, $count); + if (isset($this->notification) && is_callable($this->notification)) { + if ($result === false) { + call_user_func($this->notification, STREAM_NOTIFY_FAILURE, STREAM_NOTIFY_SEVERITY_ERR, $this->sftp->getLastSFTPError(), NET_SFTP_OPEN, 0, 0); + return 0; + } + // seems that PHP calls stream_read in 8k chunks + call_user_func($this->notification, STREAM_NOTIFY_PROGRESS, STREAM_NOTIFY_SEVERITY_INFO, '', 0, strlen($result), $this->size); + } + + if (empty($result)) { // ie. false or empty string + $this->eof = true; + return false; + } + $this->pos+= strlen($result); + + return $result; + } + + /** + * Write to stream + * + * @param string $data + * @return mixed + * @access public + */ + function _stream_write($data) + { + switch ($this->mode) { + case 'r': + return false; + } + + $result = $this->sftp->put($this->path, $data, SFTP::SOURCE_STRING, $this->pos); + if (isset($this->notification) && is_callable($this->notification)) { + if (!$result) { + call_user_func($this->notification, STREAM_NOTIFY_FAILURE, STREAM_NOTIFY_SEVERITY_ERR, $this->sftp->getLastSFTPError(), NET_SFTP_OPEN, 0, 0); + return 0; + } + // seems that PHP splits up strings into 8k blocks before calling stream_write + call_user_func($this->notification, STREAM_NOTIFY_PROGRESS, STREAM_NOTIFY_SEVERITY_INFO, '', 0, strlen($data), strlen($data)); + } + + if ($result === false) { + return false; + } + $this->pos+= strlen($data); + if ($this->pos > $this->size) { + $this->size = $this->pos; + } + $this->eof = false; + return strlen($data); + } + + /** + * Retrieve the current position of a stream + * + * @return int + * @access public + */ + function _stream_tell() + { + return $this->pos; + } + + /** + * Tests for end-of-file on a file pointer + * + * In my testing there are four classes functions that normally effect the pointer: + * fseek, fputs / fwrite, fgets / fread and ftruncate. + * + * Only fgets / fread, however, results in feof() returning true. do fputs($fp, 'aaa') on a blank file and feof() + * will return false. do fread($fp, 1) and feof() will then return true. do fseek($fp, 10) on ablank file and feof() + * will return false. do fread($fp, 1) and feof() will then return true. + * + * @return bool + * @access public + */ + function _stream_eof() + { + return $this->eof; + } + + /** + * Seeks to specific location in a stream + * + * @param int $offset + * @param int $whence + * @return bool + * @access public + */ + function _stream_seek($offset, $whence) + { + switch ($whence) { + case SEEK_SET: + if ($offset < 0) { + return false; + } + break; + case SEEK_CUR: + $offset+= $this->pos; + break; + case SEEK_END: + $offset+= $this->size; + } + + $this->pos = $offset; + $this->eof = false; + return true; + } + + /** + * Change stream options + * + * @param string $path + * @param int $option + * @param mixed $var + * @return bool + * @access public + */ + function _stream_metadata($path, $option, $var) + { + $path = $this->_parse_path($path); + if ($path === false) { + return false; + } + + // stream_metadata was introduced in PHP 5.4.0 but as of 5.4.11 the constants haven't been defined + // see http://www.php.net/streamwrapper.stream-metadata and https://bugs.php.net/64246 + // and https://github.com/php/php-src/blob/master/main/php_streams.h#L592 + switch ($option) { + case 1: // PHP_STREAM_META_TOUCH + $time = isset($var[0]) ? $var[0] : null; + $atime = isset($var[1]) ? $var[1] : null; + return $this->sftp->touch($path, $time, $atime); + case 2: // PHP_STREAM_OWNER_NAME + case 3: // PHP_STREAM_GROUP_NAME + return false; + case 4: // PHP_STREAM_META_OWNER + return $this->sftp->chown($path, $var); + case 5: // PHP_STREAM_META_GROUP + return $this->sftp->chgrp($path, $var); + case 6: // PHP_STREAM_META_ACCESS + return $this->sftp->chmod($path, $var) !== false; + } + } + + /** + * Retrieve the underlaying resource + * + * @param int $cast_as + * @return resource + * @access public + */ + function _stream_cast($cast_as) + { + return $this->sftp->fsock; + } + + /** + * Advisory file locking + * + * @param int $operation + * @return bool + * @access public + */ + function _stream_lock($operation) + { + return false; + } + + /** + * Renames a file or directory + * + * Attempts to rename oldname to newname, moving it between directories if necessary. + * If newname exists, it will be overwritten. This is a departure from what \phpseclib\Net\SFTP + * does. + * + * @param string $path_from + * @param string $path_to + * @return bool + * @access public + */ + function _rename($path_from, $path_to) + { + $path1 = parse_url($path_from); + $path2 = parse_url($path_to); + unset($path1['path'], $path2['path']); + if ($path1 != $path2) { + return false; + } + + $path_from = $this->_parse_path($path_from); + $path_to = parse_url($path_to); + if ($path_from === false) { + return false; + } + + $path_to = $path_to['path']; // the $component part of parse_url() was added in PHP 5.1.2 + // "It is an error if there already exists a file with the name specified by newpath." + // -- http://tools.ietf.org/html/draft-ietf-secsh-filexfer-02#section-6.5 + if (!$this->sftp->rename($path_from, $path_to)) { + if ($this->sftp->stat($path_to)) { + return $this->sftp->delete($path_to, true) && $this->sftp->rename($path_from, $path_to); + } + return false; + } + + return true; + } + + /** + * Open directory handle + * + * The only $options is "whether or not to enforce safe_mode (0x04)". Since safe mode was deprecated in 5.3 and + * removed in 5.4 I'm just going to ignore it. + * + * Also, nlist() is the best that this function is realistically going to be able to do. When an SFTP client + * sends a SSH_FXP_READDIR packet you don't generally get info on just one file but on multiple files. Quoting + * the SFTP specs: + * + * The SSH_FXP_NAME response has the following format: + * + * uint32 id + * uint32 count + * repeats count times: + * string filename + * string longname + * ATTRS attrs + * + * @param string $path + * @param int $options + * @return bool + * @access public + */ + function _dir_opendir($path, $options) + { + $path = $this->_parse_path($path); + if ($path === false) { + return false; + } + $this->pos = 0; + $this->entries = $this->sftp->nlist($path); + return $this->entries !== false; + } + + /** + * Read entry from directory handle + * + * @return mixed + * @access public + */ + function _dir_readdir() + { + if (isset($this->entries[$this->pos])) { + return $this->entries[$this->pos++]; + } + return false; + } + + /** + * Rewind directory handle + * + * @return bool + * @access public + */ + function _dir_rewinddir() + { + $this->pos = 0; + return true; + } + + /** + * Close directory handle + * + * @return bool + * @access public + */ + function _dir_closedir() + { + return true; + } + + /** + * Create a directory + * + * Only valid $options is STREAM_MKDIR_RECURSIVE + * + * @param string $path + * @param int $mode + * @param int $options + * @return bool + * @access public + */ + function _mkdir($path, $mode, $options) + { + $path = $this->_parse_path($path); + if ($path === false) { + return false; + } + + return $this->sftp->mkdir($path, $mode, $options & STREAM_MKDIR_RECURSIVE); + } + + /** + * Removes a directory + * + * Only valid $options is STREAM_MKDIR_RECURSIVE per , however, + * does not have a $recursive parameter as mkdir() does so I don't know how + * STREAM_MKDIR_RECURSIVE is supposed to be set. Also, when I try it out with rmdir() I get 8 as + * $options. What does 8 correspond to? + * + * @param string $path + * @param int $options + * @return bool + * @access public + */ + function _rmdir($path, $options) + { + $path = $this->_parse_path($path); + if ($path === false) { + return false; + } + + return $this->sftp->rmdir($path); + } + + /** + * Flushes the output + * + * See . Always returns true because \phpseclib\Net\SFTP doesn't cache stuff before writing + * + * @return bool + * @access public + */ + function _stream_flush() + { + return true; + } + + /** + * Retrieve information about a file resource + * + * @return mixed + * @access public + */ + function _stream_stat() + { + $results = $this->sftp->stat($this->path); + if ($results === false) { + return false; + } + return $results; + } + + /** + * Delete a file + * + * @param string $path + * @return bool + * @access public + */ + function _unlink($path) + { + $path = $this->_parse_path($path); + if ($path === false) { + return false; + } + + return $this->sftp->delete($path, false); + } + + /** + * Retrieve information about a file + * + * Ignores the STREAM_URL_STAT_QUIET flag because the entirety of \phpseclib\Net\SFTP\Stream is quiet by default + * might be worthwhile to reconstruct bits 12-16 (ie. the file type) if mode doesn't have them but we'll + * cross that bridge when and if it's reached + * + * @param string $path + * @param int $flags + * @return mixed + * @access public + */ + function _url_stat($path, $flags) + { + $path = $this->_parse_path($path); + if ($path === false) { + return false; + } + + $results = $flags & STREAM_URL_STAT_LINK ? $this->sftp->lstat($path) : $this->sftp->stat($path); + if ($results === false) { + return false; + } + + return $results; + } + + /** + * Truncate stream + * + * @param int $new_size + * @return bool + * @access public + */ + function _stream_truncate($new_size) + { + if (!$this->sftp->truncate($this->path, $new_size)) { + return false; + } + + $this->eof = false; + $this->size = $new_size; + + return true; + } + + /** + * Change stream options + * + * STREAM_OPTION_WRITE_BUFFER isn't supported for the same reason stream_flush isn't. + * The other two aren't supported because of limitations in \phpseclib\Net\SFTP. + * + * @param int $option + * @param int $arg1 + * @param int $arg2 + * @return bool + * @access public + */ + function _stream_set_option($option, $arg1, $arg2) + { + return false; + } + + /** + * Close an resource + * + * @access public + */ + function _stream_close() + { + } + + /** + * __call Magic Method + * + * When you're utilizing an SFTP stream you're not calling the methods in this class directly - PHP is calling them for you. + * Which kinda begs the question... what methods is PHP calling and what parameters is it passing to them? This function + * lets you figure that out. + * + * If NET_SFTP_STREAM_LOGGING is defined all calls will be output on the screen and then (regardless of whether or not + * NET_SFTP_STREAM_LOGGING is enabled) the parameters will be passed through to the appropriate method. + * + * @param string $name + * @param array $arguments + * @return mixed + * @access public + */ + function __call($name, $arguments) + { + if (defined('NET_SFTP_STREAM_LOGGING')) { + echo $name . '('; + $last = count($arguments) - 1; + foreach ($arguments as $i => $argument) { + var_export($argument); + if ($i != $last) { + echo ','; + } + } + echo ")\r\n"; + } + $name = '_' . $name; + if (!method_exists($this, $name)) { + return false; + } + return call_user_func_array(array($this, $name), $arguments); + } +} diff --git a/3rdparty/phpseclib/phpseclib/phpseclib/Net/SSH1.php b/3rdparty/phpseclib/phpseclib/phpseclib/Net/SSH1.php new file mode 100644 index 00000000..fc8d2acd --- /dev/null +++ b/3rdparty/phpseclib/phpseclib/phpseclib/Net/SSH1.php @@ -0,0 +1,1662 @@ + + * login('username', 'password')) { + * exit('Login Failed'); + * } + * + * echo $ssh->exec('ls -la'); + * ?> + * + * + * Here's another short example: + * + * login('username', 'password')) { + * exit('Login Failed'); + * } + * + * echo $ssh->read('username@username:~$'); + * $ssh->write("ls -la\n"); + * echo $ssh->read('username@username:~$'); + * ?> + * + * + * More information on the SSHv1 specification can be found by reading + * {@link http://www.snailbook.com/docs/protocol-1.5.txt protocol-1.5.txt}. + * + * @category Net + * @package SSH1 + * @author Jim Wigginton + * @copyright 2007 Jim Wigginton + * @license http://www.opensource.org/licenses/mit-license.html MIT License + * @link http://phpseclib.sourceforge.net + */ + +namespace phpseclib\Net; + +use phpseclib\Crypt\DES; +use phpseclib\Crypt\Random; +use phpseclib\Crypt\TripleDES; +use phpseclib\Math\BigInteger; + +/** + * Pure-PHP implementation of SSHv1. + * + * @package SSH1 + * @author Jim Wigginton + * @access public + */ +class SSH1 +{ + /**#@+ + * Encryption Methods + * + * @see \phpseclib\Net\SSH1::getSupportedCiphers() + * @access public + */ + /** + * No encryption + * + * Not supported. + */ + const CIPHER_NONE = 0; + /** + * IDEA in CFB mode + * + * Not supported. + */ + const CIPHER_IDEA = 1; + /** + * DES in CBC mode + */ + const CIPHER_DES = 2; + /** + * Triple-DES in CBC mode + * + * All implementations are required to support this + */ + const CIPHER_3DES = 3; + /** + * TRI's Simple Stream encryption CBC + * + * Not supported nor is it defined in the official SSH1 specs. OpenSSH, however, does define it (see cipher.h), + * although it doesn't use it (see cipher.c) + */ + const CIPHER_BROKEN_TSS = 4; + /** + * RC4 + * + * Not supported. + * + * @internal According to the SSH1 specs: + * + * "The first 16 bytes of the session key are used as the key for + * the server to client direction. The remaining 16 bytes are used + * as the key for the client to server direction. This gives + * independent 128-bit keys for each direction." + * + * This library currently only supports encryption when the same key is being used for both directions. This is + * because there's only one $crypto object. Two could be added ($encrypt and $decrypt, perhaps). + */ + const CIPHER_RC4 = 5; + /** + * Blowfish + * + * Not supported nor is it defined in the official SSH1 specs. OpenSSH, however, defines it (see cipher.h) and + * uses it (see cipher.c) + */ + const CIPHER_BLOWFISH = 6; + /**#@-*/ + + /**#@+ + * Authentication Methods + * + * @see \phpseclib\Net\SSH1::getSupportedAuthentications() + * @access public + */ + /** + * .rhosts or /etc/hosts.equiv + */ + const AUTH_RHOSTS = 1; + /** + * pure RSA authentication + */ + const AUTH_RSA = 2; + /** + * password authentication + * + * This is the only method that is supported by this library. + */ + const AUTH_PASSWORD = 3; + /** + * .rhosts with RSA host authentication + */ + const AUTH_RHOSTS_RSA = 4; + /**#@-*/ + + /**#@+ + * Terminal Modes + * + * @link http://3sp.com/content/developer/maverick-net/docs/Maverick.SSH.PseudoTerminalModesMembers.html + * @access private + */ + const TTY_OP_END = 0; + /**#@-*/ + + /** + * The Response Type + * + * @see \phpseclib\Net\SSH1::_get_binary_packet() + * @access private + */ + const RESPONSE_TYPE = 1; + + /** + * The Response Data + * + * @see \phpseclib\Net\SSH1::_get_binary_packet() + * @access private + */ + const RESPONSE_DATA = 2; + + /**#@+ + * Execution Bitmap Masks + * + * @see \phpseclib\Net\SSH1::bitmap + * @access private + */ + const MASK_CONSTRUCTOR = 0x00000001; + const MASK_CONNECTED = 0x00000002; + const MASK_LOGIN = 0x00000004; + const MASK_SHELL = 0x00000008; + /**#@-*/ + + /**#@+ + * @access public + * @see \phpseclib\Net\SSH1::getLog() + */ + /** + * Returns the message numbers + */ + const LOG_SIMPLE = 1; + /** + * Returns the message content + */ + const LOG_COMPLEX = 2; + /** + * Outputs the content real-time + */ + const LOG_REALTIME = 3; + /** + * Dumps the content real-time to a file + */ + const LOG_REALTIME_FILE = 4; + /** + * Make sure that the log never gets larger than this + */ + const LOG_MAX_SIZE = 1048576; // 1024 * 1024 + /**#@-*/ + + /**#@+ + * @access public + * @see \phpseclib\Net\SSH1::read() + */ + /** + * Returns when a string matching $expect exactly is found + */ + const READ_SIMPLE = 1; + /** + * Returns when a string matching the regular expression $expect is found + */ + const READ_REGEX = 2; + /**#@-*/ + + /** + * The SSH identifier + * + * @var string + * @access private + */ + var $identifier = 'SSH-1.5-phpseclib'; + + /** + * The Socket Object + * + * @var object + * @access private + */ + var $fsock; + + /** + * The cryptography object + * + * @var object + * @access private + */ + var $crypto = false; + + /** + * Execution Bitmap + * + * The bits that are set represent functions that have been called already. This is used to determine + * if a requisite function has been successfully executed. If not, an error should be thrown. + * + * @var int + * @access private + */ + var $bitmap = 0; + + /** + * The Server Key Public Exponent + * + * Logged for debug purposes + * + * @see self::getServerKeyPublicExponent() + * @var string + * @access private + */ + var $server_key_public_exponent; + + /** + * The Server Key Public Modulus + * + * Logged for debug purposes + * + * @see self::getServerKeyPublicModulus() + * @var string + * @access private + */ + var $server_key_public_modulus; + + /** + * The Host Key Public Exponent + * + * Logged for debug purposes + * + * @see self::getHostKeyPublicExponent() + * @var string + * @access private + */ + var $host_key_public_exponent; + + /** + * The Host Key Public Modulus + * + * Logged for debug purposes + * + * @see self::getHostKeyPublicModulus() + * @var string + * @access private + */ + var $host_key_public_modulus; + + /** + * Supported Ciphers + * + * Logged for debug purposes + * + * @see self::getSupportedCiphers() + * @var array + * @access private + */ + var $supported_ciphers = array( + self::CIPHER_NONE => 'No encryption', + self::CIPHER_IDEA => 'IDEA in CFB mode', + self::CIPHER_DES => 'DES in CBC mode', + self::CIPHER_3DES => 'Triple-DES in CBC mode', + self::CIPHER_BROKEN_TSS => 'TRI\'s Simple Stream encryption CBC', + self::CIPHER_RC4 => 'RC4', + self::CIPHER_BLOWFISH => 'Blowfish' + ); + + /** + * Supported Authentications + * + * Logged for debug purposes + * + * @see self::getSupportedAuthentications() + * @var array + * @access private + */ + var $supported_authentications = array( + self::AUTH_RHOSTS => '.rhosts or /etc/hosts.equiv', + self::AUTH_RSA => 'pure RSA authentication', + self::AUTH_PASSWORD => 'password authentication', + self::AUTH_RHOSTS_RSA => '.rhosts with RSA host authentication' + ); + + /** + * Server Identification + * + * @see self::getServerIdentification() + * @var string + * @access private + */ + var $server_identification = ''; + + /** + * Protocol Flags + * + * @see self::__construct() + * @var array + * @access private + */ + var $protocol_flags = array(); + + /** + * Protocol Flag Log + * + * @see self::getLog() + * @var array + * @access private + */ + var $protocol_flags_log = array(); + + /** + * Message Log + * + * @see self::getLog() + * @var array + * @access private + */ + var $message_log = array(); + + /** + * Real-time log file pointer + * + * @see self::_append_log() + * @var resource + * @access private + */ + var $realtime_log_file; + + /** + * Real-time log file size + * + * @see self::_append_log() + * @var int + * @access private + */ + var $realtime_log_size; + + /** + * Real-time log file wrap boolean + * + * @see self::_append_log() + * @var bool + * @access private + */ + var $realtime_log_wrap; + + /** + * Interactive Buffer + * + * @see self::read() + * @var array + * @access private + */ + var $interactiveBuffer = ''; + + /** + * Current log size + * + * Should never exceed self::LOG_MAX_SIZE + * + * @see self::_send_binary_packet() + * @see self::_get_binary_packet() + * @var int + * @access private + */ + var $log_size; + + /** + * Timeout + * + * @see self::setTimeout() + * @access private + */ + var $timeout; + + /** + * Current Timeout + * + * @see self::_get_channel_packet() + * @access private + */ + var $curTimeout; + + /** + * Log Boundary + * + * @see self::_format_log() + * @access private + */ + var $log_boundary = ':'; + + /** + * Log Long Width + * + * @see self::_format_log() + * @access private + */ + var $log_long_width = 65; + + /** + * Log Short Width + * + * @see self::_format_log() + * @access private + */ + var $log_short_width = 16; + + /** + * Hostname + * + * @see self::__construct() + * @see self::_connect() + * @var string + * @access private + */ + var $host; + + /** + * Port Number + * + * @see self::__construct() + * @see self::_connect() + * @var int + * @access private + */ + var $port; + + /** + * Timeout for initial connection + * + * Set by the constructor call. Calling setTimeout() is optional. If it's not called functions like + * exec() won't timeout unless some PHP setting forces it too. The timeout specified in the constructor, + * however, is non-optional. There will be a timeout, whether or not you set it. If you don't it'll be + * 10 seconds. It is used by fsockopen() in that function. + * + * @see self::__construct() + * @see self::_connect() + * @var int + * @access private + */ + var $connectionTimeout; + + /** + * Default cipher + * + * @see self::__construct() + * @see self::_connect() + * @var int + * @access private + */ + var $cipher; + + /** + * Default Constructor. + * + * Connects to an SSHv1 server + * + * @param string $host + * @param int $port + * @param int $timeout + * @param int $cipher + * @return \phpseclib\Net\SSH1 + * @access public + */ + function __construct($host, $port = 22, $timeout = 10, $cipher = self::CIPHER_3DES) + { + $this->protocol_flags = array( + 1 => 'NET_SSH1_MSG_DISCONNECT', + 2 => 'NET_SSH1_SMSG_PUBLIC_KEY', + 3 => 'NET_SSH1_CMSG_SESSION_KEY', + 4 => 'NET_SSH1_CMSG_USER', + 9 => 'NET_SSH1_CMSG_AUTH_PASSWORD', + 10 => 'NET_SSH1_CMSG_REQUEST_PTY', + 12 => 'NET_SSH1_CMSG_EXEC_SHELL', + 13 => 'NET_SSH1_CMSG_EXEC_CMD', + 14 => 'NET_SSH1_SMSG_SUCCESS', + 15 => 'NET_SSH1_SMSG_FAILURE', + 16 => 'NET_SSH1_CMSG_STDIN_DATA', + 17 => 'NET_SSH1_SMSG_STDOUT_DATA', + 18 => 'NET_SSH1_SMSG_STDERR_DATA', + 19 => 'NET_SSH1_CMSG_EOF', + 20 => 'NET_SSH1_SMSG_EXITSTATUS', + 33 => 'NET_SSH1_CMSG_EXIT_CONFIRMATION' + ); + + $this->_define_array($this->protocol_flags); + + $this->host = $host; + $this->port = $port; + $this->connectionTimeout = $timeout; + $this->cipher = $cipher; + } + + /** + * Connect to an SSHv1 server + * + * @return bool + * @access private + */ + function _connect() + { + $this->fsock = @fsockopen($this->host, $this->port, $errno, $errstr, $this->connectionTimeout); + if (!$this->fsock) { + user_error(rtrim("Cannot connect to {$this->host}:{$this->port}. Error $errno. $errstr")); + return false; + } + + $this->server_identification = $init_line = fgets($this->fsock, 255); + + if (defined('NET_SSH1_LOGGING')) { + $this->_append_log('<-', $this->server_identification); + $this->_append_log('->', $this->identifier . "\r\n"); + } + + if (!preg_match('#SSH-([0-9\.]+)-(.+)#', $init_line, $parts)) { + user_error('Can only connect to SSH servers'); + return false; + } + if ($parts[1][0] != 1) { + user_error("Cannot connect to SSH $parts[1] servers"); + return false; + } + + fputs($this->fsock, $this->identifier."\r\n"); + + $response = $this->_get_binary_packet(); + if ($response[self::RESPONSE_TYPE] != NET_SSH1_SMSG_PUBLIC_KEY) { + user_error('Expected SSH_SMSG_PUBLIC_KEY'); + return false; + } + + $anti_spoofing_cookie = $this->_string_shift($response[self::RESPONSE_DATA], 8); + + $this->_string_shift($response[self::RESPONSE_DATA], 4); + + if (strlen($response[self::RESPONSE_DATA]) < 2) { + return false; + } + $temp = unpack('nlen', $this->_string_shift($response[self::RESPONSE_DATA], 2)); + $server_key_public_exponent = new BigInteger($this->_string_shift($response[self::RESPONSE_DATA], ceil($temp['len'] / 8)), 256); + $this->server_key_public_exponent = $server_key_public_exponent; + + if (strlen($response[self::RESPONSE_DATA]) < 2) { + return false; + } + $temp = unpack('nlen', $this->_string_shift($response[self::RESPONSE_DATA], 2)); + $server_key_public_modulus = new BigInteger($this->_string_shift($response[self::RESPONSE_DATA], ceil($temp['len'] / 8)), 256); + + $this->server_key_public_modulus = $server_key_public_modulus; + + $this->_string_shift($response[self::RESPONSE_DATA], 4); + + if (strlen($response[self::RESPONSE_DATA]) < 2) { + return false; + } + $temp = unpack('nlen', $this->_string_shift($response[self::RESPONSE_DATA], 2)); + $host_key_public_exponent = new BigInteger($this->_string_shift($response[self::RESPONSE_DATA], ceil($temp['len'] / 8)), 256); + $this->host_key_public_exponent = $host_key_public_exponent; + + if (strlen($response[self::RESPONSE_DATA]) < 2) { + return false; + } + $temp = unpack('nlen', $this->_string_shift($response[self::RESPONSE_DATA], 2)); + $host_key_public_modulus = new BigInteger($this->_string_shift($response[self::RESPONSE_DATA], ceil($temp['len'] / 8)), 256); + + $this->host_key_public_modulus = $host_key_public_modulus; + + $this->_string_shift($response[self::RESPONSE_DATA], 4); + + // get a list of the supported ciphers + if (strlen($response[self::RESPONSE_DATA]) < 4) { + return false; + } + extract(unpack('Nsupported_ciphers_mask', $this->_string_shift($response[self::RESPONSE_DATA], 4))); + + foreach ($this->supported_ciphers as $mask => $name) { + if (($supported_ciphers_mask & (1 << $mask)) == 0) { + unset($this->supported_ciphers[$mask]); + } + } + + // get a list of the supported authentications + if (strlen($response[self::RESPONSE_DATA]) < 4) { + return false; + } + extract(unpack('Nsupported_authentications_mask', $this->_string_shift($response[self::RESPONSE_DATA], 4))); + foreach ($this->supported_authentications as $mask => $name) { + if (($supported_authentications_mask & (1 << $mask)) == 0) { + unset($this->supported_authentications[$mask]); + } + } + + $session_id = pack('H*', md5($host_key_public_modulus->toBytes() . $server_key_public_modulus->toBytes() . $anti_spoofing_cookie)); + + $session_key = Random::string(32); + $double_encrypted_session_key = $session_key ^ str_pad($session_id, 32, chr(0)); + + if ($server_key_public_modulus->compare($host_key_public_modulus) < 0) { + $double_encrypted_session_key = $this->_rsa_crypt( + $double_encrypted_session_key, + array( + $server_key_public_exponent, + $server_key_public_modulus + ) + ); + $double_encrypted_session_key = $this->_rsa_crypt( + $double_encrypted_session_key, + array( + $host_key_public_exponent, + $host_key_public_modulus + ) + ); + } else { + $double_encrypted_session_key = $this->_rsa_crypt( + $double_encrypted_session_key, + array( + $host_key_public_exponent, + $host_key_public_modulus + ) + ); + $double_encrypted_session_key = $this->_rsa_crypt( + $double_encrypted_session_key, + array( + $server_key_public_exponent, + $server_key_public_modulus + ) + ); + } + + $cipher = isset($this->supported_ciphers[$this->cipher]) ? $this->cipher : self::CIPHER_3DES; + $data = pack('C2a*na*N', NET_SSH1_CMSG_SESSION_KEY, $cipher, $anti_spoofing_cookie, 8 * strlen($double_encrypted_session_key), $double_encrypted_session_key, 0); + + if (!$this->_send_binary_packet($data)) { + user_error('Error sending SSH_CMSG_SESSION_KEY'); + return false; + } + + switch ($cipher) { + //case self::CIPHER_NONE: + // $this->crypto = new \phpseclib\Crypt\Null(); + // break; + case self::CIPHER_DES: + $this->crypto = new DES(); + $this->crypto->disablePadding(); + $this->crypto->enableContinuousBuffer(); + $this->crypto->setKey(substr($session_key, 0, 8)); + break; + case self::CIPHER_3DES: + $this->crypto = new TripleDES(TripleDES::MODE_3CBC); + $this->crypto->disablePadding(); + $this->crypto->enableContinuousBuffer(); + $this->crypto->setKey(substr($session_key, 0, 24)); + break; + //case self::CIPHER_RC4: + // $this->crypto = new RC4(); + // $this->crypto->enableContinuousBuffer(); + // $this->crypto->setKey(substr($session_key, 0, 16)); + // break; + } + + $response = $this->_get_binary_packet(); + + if ($response[self::RESPONSE_TYPE] != NET_SSH1_SMSG_SUCCESS) { + user_error('Expected SSH_SMSG_SUCCESS'); + return false; + } + + $this->bitmap = self::MASK_CONNECTED; + + return true; + } + + /** + * Login + * + * @param string $username + * @param string $password + * @return bool + * @access public + */ + function login($username, $password = '') + { + if (!($this->bitmap & self::MASK_CONSTRUCTOR)) { + $this->bitmap |= self::MASK_CONSTRUCTOR; + if (!$this->_connect()) { + return false; + } + } + + if (!($this->bitmap & self::MASK_CONNECTED)) { + return false; + } + + $data = pack('CNa*', NET_SSH1_CMSG_USER, strlen($username), $username); + + if (!$this->_send_binary_packet($data)) { + user_error('Error sending SSH_CMSG_USER'); + return false; + } + + $response = $this->_get_binary_packet(); + + if ($response === true) { + return false; + } + if ($response[self::RESPONSE_TYPE] == NET_SSH1_SMSG_SUCCESS) { + $this->bitmap |= self::MASK_LOGIN; + return true; + } elseif ($response[self::RESPONSE_TYPE] != NET_SSH1_SMSG_FAILURE) { + user_error('Expected SSH_SMSG_SUCCESS or SSH_SMSG_FAILURE'); + return false; + } + + $data = pack('CNa*', NET_SSH1_CMSG_AUTH_PASSWORD, strlen($password), $password); + + if (!$this->_send_binary_packet($data)) { + user_error('Error sending SSH_CMSG_AUTH_PASSWORD'); + return false; + } + + // remove the username and password from the last logged packet + if (defined('NET_SSH1_LOGGING') && NET_SSH1_LOGGING == self::LOG_COMPLEX) { + $data = pack('CNa*', NET_SSH1_CMSG_AUTH_PASSWORD, strlen('password'), 'password'); + $this->message_log[count($this->message_log) - 1] = $data; + } + + $response = $this->_get_binary_packet(); + + if ($response === true) { + return false; + } + if ($response[self::RESPONSE_TYPE] == NET_SSH1_SMSG_SUCCESS) { + $this->bitmap |= self::MASK_LOGIN; + return true; + } elseif ($response[self::RESPONSE_TYPE] == NET_SSH1_SMSG_FAILURE) { + return false; + } else { + user_error('Expected SSH_SMSG_SUCCESS or SSH_SMSG_FAILURE'); + return false; + } + } + + /** + * Set Timeout + * + * $ssh->exec('ping 127.0.0.1'); on a Linux host will never return and will run indefinitely. setTimeout() makes it so it'll timeout. + * Setting $timeout to false or 0 will mean there is no timeout. + * + * @param mixed $timeout + */ + function setTimeout($timeout) + { + $this->timeout = $this->curTimeout = $timeout; + } + + /** + * Executes a command on a non-interactive shell, returns the output, and quits. + * + * An SSH1 server will close the connection after a command has been executed on a non-interactive shell. SSH2 + * servers don't, however, this isn't an SSH2 client. The way this works, on the server, is by initiating a + * shell with the -s option, as discussed in the following links: + * + * {@link http://www.faqs.org/docs/bashman/bashref_65.html http://www.faqs.org/docs/bashman/bashref_65.html} + * {@link http://www.faqs.org/docs/bashman/bashref_62.html http://www.faqs.org/docs/bashman/bashref_62.html} + * + * To execute further commands, a new \phpseclib\Net\SSH1 object will need to be created. + * + * Returns false on failure and the output, otherwise. + * + * @see self::interactiveRead() + * @see self::interactiveWrite() + * @param string $cmd + * @param bool $block + * @return mixed + * @access public + */ + function exec($cmd, $block = true) + { + if (!($this->bitmap & self::MASK_LOGIN)) { + user_error('Operation disallowed prior to login()'); + return false; + } + + $data = pack('CNa*', NET_SSH1_CMSG_EXEC_CMD, strlen($cmd), $cmd); + + if (!$this->_send_binary_packet($data)) { + user_error('Error sending SSH_CMSG_EXEC_CMD'); + return false; + } + + if (!$block) { + return true; + } + + $output = ''; + $response = $this->_get_binary_packet(); + + if ($response !== false) { + do { + $output.= substr($response[self::RESPONSE_DATA], 4); + $response = $this->_get_binary_packet(); + } while (is_array($response) && $response[self::RESPONSE_TYPE] != NET_SSH1_SMSG_EXITSTATUS); + } + + $data = pack('C', NET_SSH1_CMSG_EXIT_CONFIRMATION); + + // i don't think it's really all that important if this packet gets sent or not. + $this->_send_binary_packet($data); + + fclose($this->fsock); + + // reset the execution bitmap - a new \phpseclib\Net\SSH1 object needs to be created. + $this->bitmap = 0; + + return $output; + } + + /** + * Creates an interactive shell + * + * @see self::interactiveRead() + * @see self::interactiveWrite() + * @return bool + * @access private + */ + function _initShell() + { + // connect using the sample parameters in protocol-1.5.txt. + // according to wikipedia.org's entry on text terminals, "the fundamental type of application running on a text + // terminal is a command line interpreter or shell". thus, opening a terminal session to run the shell. + $data = pack('CNa*N4C', NET_SSH1_CMSG_REQUEST_PTY, strlen('vt100'), 'vt100', 24, 80, 0, 0, self::TTY_OP_END); + + if (!$this->_send_binary_packet($data)) { + user_error('Error sending SSH_CMSG_REQUEST_PTY'); + return false; + } + + $response = $this->_get_binary_packet(); + + if ($response === true) { + return false; + } + if ($response[self::RESPONSE_TYPE] != NET_SSH1_SMSG_SUCCESS) { + user_error('Expected SSH_SMSG_SUCCESS'); + return false; + } + + $data = pack('C', NET_SSH1_CMSG_EXEC_SHELL); + + if (!$this->_send_binary_packet($data)) { + user_error('Error sending SSH_CMSG_EXEC_SHELL'); + return false; + } + + $this->bitmap |= self::MASK_SHELL; + + //stream_set_blocking($this->fsock, 0); + + return true; + } + + /** + * Inputs a command into an interactive shell. + * + * @see self::interactiveWrite() + * @param string $cmd + * @return bool + * @access public + */ + function write($cmd) + { + return $this->interactiveWrite($cmd); + } + + /** + * Returns the output of an interactive shell when there's a match for $expect + * + * $expect can take the form of a string literal or, if $mode == self::READ_REGEX, + * a regular expression. + * + * @see self::write() + * @param string $expect + * @param int $mode + * @return bool + * @access public + */ + function read($expect, $mode = self::READ_SIMPLE) + { + if (!($this->bitmap & self::MASK_LOGIN)) { + user_error('Operation disallowed prior to login()'); + return false; + } + + if (!($this->bitmap & self::MASK_SHELL) && !$this->_initShell()) { + user_error('Unable to initiate an interactive shell session'); + return false; + } + + $match = $expect; + while (true) { + if ($mode == self::READ_REGEX) { + preg_match($expect, $this->interactiveBuffer, $matches); + $match = isset($matches[0]) ? $matches[0] : ''; + } + $pos = strlen($match) ? strpos($this->interactiveBuffer, $match) : false; + if ($pos !== false) { + return $this->_string_shift($this->interactiveBuffer, $pos + strlen($match)); + } + $response = $this->_get_binary_packet(); + + if ($response === true) { + return $this->_string_shift($this->interactiveBuffer, strlen($this->interactiveBuffer)); + } + $this->interactiveBuffer.= substr($response[self::RESPONSE_DATA], 4); + } + } + + /** + * Inputs a command into an interactive shell. + * + * @see self::interactiveRead() + * @param string $cmd + * @return bool + * @access public + */ + function interactiveWrite($cmd) + { + if (!($this->bitmap & self::MASK_LOGIN)) { + user_error('Operation disallowed prior to login()'); + return false; + } + + if (!($this->bitmap & self::MASK_SHELL) && !$this->_initShell()) { + user_error('Unable to initiate an interactive shell session'); + return false; + } + + $data = pack('CNa*', NET_SSH1_CMSG_STDIN_DATA, strlen($cmd), $cmd); + + if (!$this->_send_binary_packet($data)) { + user_error('Error sending SSH_CMSG_STDIN'); + return false; + } + + return true; + } + + /** + * Returns the output of an interactive shell when no more output is available. + * + * Requires PHP 4.3.0 or later due to the use of the stream_select() function. If you see stuff like + * "^[[00m", you're seeing ANSI escape codes. According to + * {@link http://support.microsoft.com/kb/101875 How to Enable ANSI.SYS in a Command Window}, "Windows NT + * does not support ANSI escape sequences in Win32 Console applications", so if you're a Windows user, + * there's not going to be much recourse. + * + * @see self::interactiveRead() + * @return string + * @access public + */ + function interactiveRead() + { + if (!($this->bitmap & self::MASK_LOGIN)) { + user_error('Operation disallowed prior to login()'); + return false; + } + + if (!($this->bitmap & self::MASK_SHELL) && !$this->_initShell()) { + user_error('Unable to initiate an interactive shell session'); + return false; + } + + $read = array($this->fsock); + $write = $except = null; + if (stream_select($read, $write, $except, 0)) { + $response = $this->_get_binary_packet(); + return substr($response[self::RESPONSE_DATA], 4); + } else { + return ''; + } + } + + /** + * Disconnect + * + * @access public + */ + function disconnect() + { + $this->_disconnect(); + } + + /** + * Destructor. + * + * Will be called, automatically, if you're supporting just PHP5. If you're supporting PHP4, you'll need to call + * disconnect(). + * + * @access public + */ + function __destruct() + { + $this->_disconnect(); + } + + /** + * Disconnect + * + * @param string $msg + * @access private + */ + function _disconnect($msg = 'Client Quit') + { + if ($this->bitmap) { + $data = pack('C', NET_SSH1_CMSG_EOF); + $this->_send_binary_packet($data); + /* + $response = $this->_get_binary_packet(); + if ($response === true) { + $response = array(self::RESPONSE_TYPE => -1); + } + switch ($response[self::RESPONSE_TYPE]) { + case NET_SSH1_SMSG_EXITSTATUS: + $data = pack('C', NET_SSH1_CMSG_EXIT_CONFIRMATION); + break; + default: + $data = pack('CNa*', NET_SSH1_MSG_DISCONNECT, strlen($msg), $msg); + } + */ + $data = pack('CNa*', NET_SSH1_MSG_DISCONNECT, strlen($msg), $msg); + + $this->_send_binary_packet($data); + fclose($this->fsock); + $this->bitmap = 0; + } + } + + /** + * Gets Binary Packets + * + * See 'The Binary Packet Protocol' of protocol-1.5.txt for more info. + * + * Also, this function could be improved upon by adding detection for the following exploit: + * http://www.securiteam.com/securitynews/5LP042K3FY.html + * + * @see self::_send_binary_packet() + * @return array + * @access private + */ + function _get_binary_packet() + { + if (feof($this->fsock)) { + //user_error('connection closed prematurely'); + return false; + } + + if ($this->curTimeout) { + $read = array($this->fsock); + $write = $except = null; + + $start = strtok(microtime(), ' ') + strtok(''); // http://php.net/microtime#61838 + $sec = floor($this->curTimeout); + $usec = 1000000 * ($this->curTimeout - $sec); + // on windows this returns a "Warning: Invalid CRT parameters detected" error + if (!@stream_select($read, $write, $except, $sec, $usec) && !count($read)) { + //$this->_disconnect('Timeout'); + return true; + } + $elapsed = strtok(microtime(), ' ') + strtok('') - $start; + $this->curTimeout-= $elapsed; + } + + $start = strtok(microtime(), ' ') + strtok(''); // http://php.net/microtime#61838 + $data = fread($this->fsock, 4); + if (strlen($data) < 4) { + return false; + } + $temp = unpack('Nlength', $data); + + $padding_length = 8 - ($temp['length'] & 7); + $length = $temp['length'] + $padding_length; + $raw = ''; + + while ($length > 0) { + $temp = fread($this->fsock, $length); + if (strlen($temp) != $length) { + return false; + } + $raw.= $temp; + $length-= strlen($temp); + } + $stop = strtok(microtime(), ' ') + strtok(''); + + if (strlen($raw) && $this->crypto !== false) { + $raw = $this->crypto->decrypt($raw); + } + + $padding = substr($raw, 0, $padding_length); + $type = $raw[$padding_length]; + $data = substr($raw, $padding_length + 1, -4); + + if (strlen($raw) < 4) { + return false; + } + $temp = unpack('Ncrc', substr($raw, -4)); + + //if ( $temp['crc'] != $this->_crc($padding . $type . $data) ) { + // user_error('Bad CRC in packet from server'); + // return false; + //} + + $type = ord($type); + + if (defined('NET_SSH1_LOGGING')) { + $temp = isset($this->protocol_flags[$type]) ? $this->protocol_flags[$type] : 'UNKNOWN'; + $temp = '<- ' . $temp . + ' (' . round($stop - $start, 4) . 's)'; + $this->_append_log($temp, $data); + } + + return array( + self::RESPONSE_TYPE => $type, + self::RESPONSE_DATA => $data + ); + } + + /** + * Sends Binary Packets + * + * Returns true on success, false on failure. + * + * @see self::_get_binary_packet() + * @param string $data + * @return bool + * @access private + */ + function _send_binary_packet($data) + { + if (feof($this->fsock)) { + //user_error('connection closed prematurely'); + return false; + } + + $length = strlen($data) + 4; + + $padding = Random::string(8 - ($length & 7)); + + $orig = $data; + $data = $padding . $data; + $data.= pack('N', $this->_crc($data)); + + if ($this->crypto !== false) { + $data = $this->crypto->encrypt($data); + } + + $packet = pack('Na*', $length, $data); + + $start = strtok(microtime(), ' ') + strtok(''); // http://php.net/microtime#61838 + $result = strlen($packet) == fputs($this->fsock, $packet); + $stop = strtok(microtime(), ' ') + strtok(''); + + if (defined('NET_SSH1_LOGGING')) { + $temp = isset($this->protocol_flags[ord($orig[0])]) ? $this->protocol_flags[ord($orig[0])] : 'UNKNOWN'; + $temp = '-> ' . $temp . + ' (' . round($stop - $start, 4) . 's)'; + $this->_append_log($temp, $orig); + } + + return $result; + } + + /** + * Cyclic Redundancy Check (CRC) + * + * PHP's crc32 function is implemented slightly differently than the one that SSH v1 uses, so + * we've reimplemented it. A more detailed discussion of the differences can be found after + * $crc_lookup_table's initialization. + * + * @see self::_get_binary_packet() + * @see self::_send_binary_packet() + * @param string $data + * @return int + * @access private + */ + function _crc($data) + { + static $crc_lookup_table = array( + 0x00000000, 0x77073096, 0xEE0E612C, 0x990951BA, + 0x076DC419, 0x706AF48F, 0xE963A535, 0x9E6495A3, + 0x0EDB8832, 0x79DCB8A4, 0xE0D5E91E, 0x97D2D988, + 0x09B64C2B, 0x7EB17CBD, 0xE7B82D07, 0x90BF1D91, + 0x1DB71064, 0x6AB020F2, 0xF3B97148, 0x84BE41DE, + 0x1ADAD47D, 0x6DDDE4EB, 0xF4D4B551, 0x83D385C7, + 0x136C9856, 0x646BA8C0, 0xFD62F97A, 0x8A65C9EC, + 0x14015C4F, 0x63066CD9, 0xFA0F3D63, 0x8D080DF5, + 0x3B6E20C8, 0x4C69105E, 0xD56041E4, 0xA2677172, + 0x3C03E4D1, 0x4B04D447, 0xD20D85FD, 0xA50AB56B, + 0x35B5A8FA, 0x42B2986C, 0xDBBBC9D6, 0xACBCF940, + 0x32D86CE3, 0x45DF5C75, 0xDCD60DCF, 0xABD13D59, + 0x26D930AC, 0x51DE003A, 0xC8D75180, 0xBFD06116, + 0x21B4F4B5, 0x56B3C423, 0xCFBA9599, 0xB8BDA50F, + 0x2802B89E, 0x5F058808, 0xC60CD9B2, 0xB10BE924, + 0x2F6F7C87, 0x58684C11, 0xC1611DAB, 0xB6662D3D, + 0x76DC4190, 0x01DB7106, 0x98D220BC, 0xEFD5102A, + 0x71B18589, 0x06B6B51F, 0x9FBFE4A5, 0xE8B8D433, + 0x7807C9A2, 0x0F00F934, 0x9609A88E, 0xE10E9818, + 0x7F6A0DBB, 0x086D3D2D, 0x91646C97, 0xE6635C01, + 0x6B6B51F4, 0x1C6C6162, 0x856530D8, 0xF262004E, + 0x6C0695ED, 0x1B01A57B, 0x8208F4C1, 0xF50FC457, + 0x65B0D9C6, 0x12B7E950, 0x8BBEB8EA, 0xFCB9887C, + 0x62DD1DDF, 0x15DA2D49, 0x8CD37CF3, 0xFBD44C65, + 0x4DB26158, 0x3AB551CE, 0xA3BC0074, 0xD4BB30E2, + 0x4ADFA541, 0x3DD895D7, 0xA4D1C46D, 0xD3D6F4FB, + 0x4369E96A, 0x346ED9FC, 0xAD678846, 0xDA60B8D0, + 0x44042D73, 0x33031DE5, 0xAA0A4C5F, 0xDD0D7CC9, + 0x5005713C, 0x270241AA, 0xBE0B1010, 0xC90C2086, + 0x5768B525, 0x206F85B3, 0xB966D409, 0xCE61E49F, + 0x5EDEF90E, 0x29D9C998, 0xB0D09822, 0xC7D7A8B4, + 0x59B33D17, 0x2EB40D81, 0xB7BD5C3B, 0xC0BA6CAD, + 0xEDB88320, 0x9ABFB3B6, 0x03B6E20C, 0x74B1D29A, + 0xEAD54739, 0x9DD277AF, 0x04DB2615, 0x73DC1683, + 0xE3630B12, 0x94643B84, 0x0D6D6A3E, 0x7A6A5AA8, + 0xE40ECF0B, 0x9309FF9D, 0x0A00AE27, 0x7D079EB1, + 0xF00F9344, 0x8708A3D2, 0x1E01F268, 0x6906C2FE, + 0xF762575D, 0x806567CB, 0x196C3671, 0x6E6B06E7, + 0xFED41B76, 0x89D32BE0, 0x10DA7A5A, 0x67DD4ACC, + 0xF9B9DF6F, 0x8EBEEFF9, 0x17B7BE43, 0x60B08ED5, + 0xD6D6A3E8, 0xA1D1937E, 0x38D8C2C4, 0x4FDFF252, + 0xD1BB67F1, 0xA6BC5767, 0x3FB506DD, 0x48B2364B, + 0xD80D2BDA, 0xAF0A1B4C, 0x36034AF6, 0x41047A60, + 0xDF60EFC3, 0xA867DF55, 0x316E8EEF, 0x4669BE79, + 0xCB61B38C, 0xBC66831A, 0x256FD2A0, 0x5268E236, + 0xCC0C7795, 0xBB0B4703, 0x220216B9, 0x5505262F, + 0xC5BA3BBE, 0xB2BD0B28, 0x2BB45A92, 0x5CB36A04, + 0xC2D7FFA7, 0xB5D0CF31, 0x2CD99E8B, 0x5BDEAE1D, + 0x9B64C2B0, 0xEC63F226, 0x756AA39C, 0x026D930A, + 0x9C0906A9, 0xEB0E363F, 0x72076785, 0x05005713, + 0x95BF4A82, 0xE2B87A14, 0x7BB12BAE, 0x0CB61B38, + 0x92D28E9B, 0xE5D5BE0D, 0x7CDCEFB7, 0x0BDBDF21, + 0x86D3D2D4, 0xF1D4E242, 0x68DDB3F8, 0x1FDA836E, + 0x81BE16CD, 0xF6B9265B, 0x6FB077E1, 0x18B74777, + 0x88085AE6, 0xFF0F6A70, 0x66063BCA, 0x11010B5C, + 0x8F659EFF, 0xF862AE69, 0x616BFFD3, 0x166CCF45, + 0xA00AE278, 0xD70DD2EE, 0x4E048354, 0x3903B3C2, + 0xA7672661, 0xD06016F7, 0x4969474D, 0x3E6E77DB, + 0xAED16A4A, 0xD9D65ADC, 0x40DF0B66, 0x37D83BF0, + 0xA9BCAE53, 0xDEBB9EC5, 0x47B2CF7F, 0x30B5FFE9, + 0xBDBDF21C, 0xCABAC28A, 0x53B39330, 0x24B4A3A6, + 0xBAD03605, 0xCDD70693, 0x54DE5729, 0x23D967BF, + 0xB3667A2E, 0xC4614AB8, 0x5D681B02, 0x2A6F2B94, + 0xB40BBE37, 0xC30C8EA1, 0x5A05DF1B, 0x2D02EF8D + ); + + // For this function to yield the same output as PHP's crc32 function, $crc would have to be + // set to 0xFFFFFFFF, initially - not 0x00000000 as it currently is. + $crc = 0x00000000; + $length = strlen($data); + + for ($i=0; $i<$length; $i++) { + // We AND $crc >> 8 with 0x00FFFFFF because we want the eight newly added bits to all + // be zero. PHP, unfortunately, doesn't always do this. 0x80000000 >> 8, as an example, + // yields 0xFF800000 - not 0x00800000. The following link elaborates: + // http://www.php.net/manual/en/language.operators.bitwise.php#57281 + $crc = (($crc >> 8) & 0x00FFFFFF) ^ $crc_lookup_table[($crc & 0xFF) ^ ord($data[$i])]; + } + + // In addition to having to set $crc to 0xFFFFFFFF, initially, the return value must be XOR'd with + // 0xFFFFFFFF for this function to return the same thing that PHP's crc32 function would. + return $crc; + } + + /** + * String Shift + * + * Inspired by array_shift + * + * @param string $string + * @param int $index + * @return string + * @access private + */ + function _string_shift(&$string, $index = 1) + { + $substr = substr($string, 0, $index); + $string = substr($string, $index); + return $substr; + } + + /** + * RSA Encrypt + * + * Returns mod(pow($m, $e), $n), where $n should be the product of two (large) primes $p and $q and where $e + * should be a number with the property that gcd($e, ($p - 1) * ($q - 1)) == 1. Could just make anything that + * calls this call modexp, instead, but I think this makes things clearer, maybe... + * + * @see self::__construct() + * @param BigInteger $m + * @param array $key + * @return BigInteger + * @access private + */ + function _rsa_crypt($m, $key) + { + /* + $rsa = new RSA(); + $rsa->loadKey($key, RSA::PUBLIC_FORMAT_RAW); + $rsa->setEncryptionMode(RSA::ENCRYPTION_PKCS1); + return $rsa->encrypt($m); + */ + + // To quote from protocol-1.5.txt: + // The most significant byte (which is only partial as the value must be + // less than the public modulus, which is never a power of two) is zero. + // + // The next byte contains the value 2 (which stands for public-key + // encrypted data in the PKCS standard [PKCS#1]). Then, there are non- + // zero random bytes to fill any unused space, a zero byte, and the data + // to be encrypted in the least significant bytes, the last byte of the + // data in the least significant byte. + + // Presumably the part of PKCS#1 they're refering to is "Section 7.2.1 Encryption Operation", + // under "7.2 RSAES-PKCS1-v1.5" and "7 Encryption schemes" of the following URL: + // ftp://ftp.rsasecurity.com/pub/pkcs/pkcs-1/pkcs-1v2-1.pdf + $modulus = $key[1]->toBytes(); + $length = strlen($modulus) - strlen($m) - 3; + $random = ''; + while (strlen($random) != $length) { + $block = Random::string($length - strlen($random)); + $block = str_replace("\x00", '', $block); + $random.= $block; + } + $temp = chr(0) . chr(2) . $random . chr(0) . $m; + + $m = new BigInteger($temp, 256); + $m = $m->modPow($key[0], $key[1]); + + return $m->toBytes(); + } + + /** + * Define Array + * + * Takes any number of arrays whose indices are integers and whose values are strings and defines a bunch of + * named constants from it, using the value as the name of the constant and the index as the value of the constant. + * If any of the constants that would be defined already exists, none of the constants will be defined. + * + * @access private + */ + function _define_array() + { + $args = func_get_args(); + foreach ($args as $arg) { + foreach ($arg as $key => $value) { + if (!defined($value)) { + define($value, $key); + } else { + break 2; + } + } + } + } + + /** + * Returns a log of the packets that have been sent and received. + * + * Returns a string if NET_SSH1_LOGGING == self::LOG_COMPLEX, an array if NET_SSH1_LOGGING == self::LOG_SIMPLE and false if !defined('NET_SSH1_LOGGING') + * + * @access public + * @return array|false|string + */ + function getLog() + { + if (!defined('NET_SSH1_LOGGING')) { + return false; + } + + switch (NET_SSH1_LOGGING) { + case self::LOG_SIMPLE: + return $this->protocol_flags_log; + break; + case self::LOG_COMPLEX: + return $this->_format_log($this->message_log, $this->protocol_flags_log); + break; + default: + return false; + } + } + + /** + * Formats a log for printing + * + * @param array $message_log + * @param array $message_number_log + * @access private + * @return string + */ + function _format_log($message_log, $message_number_log) + { + $output = ''; + for ($i = 0; $i < count($message_log); $i++) { + $output.= $message_number_log[$i] . "\r\n"; + $current_log = $message_log[$i]; + $j = 0; + do { + if (strlen($current_log)) { + $output.= str_pad(dechex($j), 7, '0', STR_PAD_LEFT) . '0 '; + } + $fragment = $this->_string_shift($current_log, $this->log_short_width); + $hex = substr(preg_replace_callback('#.#s', array($this, '_format_log_helper'), $fragment), strlen($this->log_boundary)); + // replace non ASCII printable characters with dots + // http://en.wikipedia.org/wiki/ASCII#ASCII_printable_characters + // also replace < with a . since < messes up the output on web browsers + $raw = preg_replace('#[^\x20-\x7E]|<#', '.', $fragment); + $output.= str_pad($hex, $this->log_long_width - $this->log_short_width, ' ') . $raw . "\r\n"; + $j++; + } while (strlen($current_log)); + $output.= "\r\n"; + } + + return $output; + } + + /** + * Helper function for _format_log + * + * For use with preg_replace_callback() + * + * @param array $matches + * @access private + * @return string + */ + function _format_log_helper($matches) + { + return $this->log_boundary . str_pad(dechex(ord($matches[0])), 2, '0', STR_PAD_LEFT); + } + + /** + * Return the server key public exponent + * + * Returns, by default, the base-10 representation. If $raw_output is set to true, returns, instead, + * the raw bytes. This behavior is similar to PHP's md5() function. + * + * @param bool $raw_output + * @return string + * @access public + */ + function getServerKeyPublicExponent($raw_output = false) + { + return $raw_output ? $this->server_key_public_exponent->toBytes() : $this->server_key_public_exponent->toString(); + } + + /** + * Return the server key public modulus + * + * Returns, by default, the base-10 representation. If $raw_output is set to true, returns, instead, + * the raw bytes. This behavior is similar to PHP's md5() function. + * + * @param bool $raw_output + * @return string + * @access public + */ + function getServerKeyPublicModulus($raw_output = false) + { + return $raw_output ? $this->server_key_public_modulus->toBytes() : $this->server_key_public_modulus->toString(); + } + + /** + * Return the host key public exponent + * + * Returns, by default, the base-10 representation. If $raw_output is set to true, returns, instead, + * the raw bytes. This behavior is similar to PHP's md5() function. + * + * @param bool $raw_output + * @return string + * @access public + */ + function getHostKeyPublicExponent($raw_output = false) + { + return $raw_output ? $this->host_key_public_exponent->toBytes() : $this->host_key_public_exponent->toString(); + } + + /** + * Return the host key public modulus + * + * Returns, by default, the base-10 representation. If $raw_output is set to true, returns, instead, + * the raw bytes. This behavior is similar to PHP's md5() function. + * + * @param bool $raw_output + * @return string + * @access public + */ + function getHostKeyPublicModulus($raw_output = false) + { + return $raw_output ? $this->host_key_public_modulus->toBytes() : $this->host_key_public_modulus->toString(); + } + + /** + * Return a list of ciphers supported by SSH1 server. + * + * Just because a cipher is supported by an SSH1 server doesn't mean it's supported by this library. If $raw_output + * is set to true, returns, instead, an array of constants. ie. instead of array('Triple-DES in CBC mode'), you'll + * get array(self::CIPHER_3DES). + * + * @param bool $raw_output + * @return array + * @access public + */ + function getSupportedCiphers($raw_output = false) + { + return $raw_output ? array_keys($this->supported_ciphers) : array_values($this->supported_ciphers); + } + + /** + * Return a list of authentications supported by SSH1 server. + * + * Just because a cipher is supported by an SSH1 server doesn't mean it's supported by this library. If $raw_output + * is set to true, returns, instead, an array of constants. ie. instead of array('password authentication'), you'll + * get array(self::AUTH_PASSWORD). + * + * @param bool $raw_output + * @return array + * @access public + */ + function getSupportedAuthentications($raw_output = false) + { + return $raw_output ? array_keys($this->supported_authentications) : array_values($this->supported_authentications); + } + + /** + * Return the server identification. + * + * @return string + * @access public + */ + function getServerIdentification() + { + return rtrim($this->server_identification); + } + + /** + * Logs data packets + * + * Makes sure that only the last 1MB worth of packets will be logged + * + * @param int $protocol_flags + * @param string $message + * @access private + */ + function _append_log($protocol_flags, $message) + { + switch (NET_SSH1_LOGGING) { + // useful for benchmarks + case self::LOG_SIMPLE: + $this->protocol_flags_log[] = $protocol_flags; + break; + // the most useful log for SSH1 + case self::LOG_COMPLEX: + $this->protocol_flags_log[] = $protocol_flags; + $this->_string_shift($message); + $this->log_size+= strlen($message); + $this->message_log[] = $message; + while ($this->log_size > self::LOG_MAX_SIZE) { + $this->log_size-= strlen(array_shift($this->message_log)); + array_shift($this->protocol_flags_log); + } + break; + // dump the output out realtime; packets may be interspersed with non packets, + // passwords won't be filtered out and select other packets may not be correctly + // identified + case self::LOG_REALTIME: + echo "
\r\n" . $this->_format_log(array($message), array($protocol_flags)) . "\r\n
\r\n"; + @flush(); + @ob_flush(); + break; + // basically the same thing as self::LOG_REALTIME with the caveat that self::LOG_REALTIME_FILE + // needs to be defined and that the resultant log file will be capped out at self::LOG_MAX_SIZE. + // the earliest part of the log file is denoted by the first <<< START >>> and is not going to necessarily + // at the beginning of the file + case self::LOG_REALTIME_FILE: + if (!isset($this->realtime_log_file)) { + // PHP doesn't seem to like using constants in fopen() + $filename = self::LOG_REALTIME_FILE; + $fp = fopen($filename, 'w'); + $this->realtime_log_file = $fp; + } + if (!is_resource($this->realtime_log_file)) { + break; + } + $entry = $this->_format_log(array($message), array($protocol_flags)); + if ($this->realtime_log_wrap) { + $temp = "<<< START >>>\r\n"; + $entry.= $temp; + fseek($this->realtime_log_file, ftell($this->realtime_log_file) - strlen($temp)); + } + $this->realtime_log_size+= strlen($entry); + if ($this->realtime_log_size > self::LOG_MAX_SIZE) { + fseek($this->realtime_log_file, 0); + $this->realtime_log_size = strlen($entry); + $this->realtime_log_wrap = true; + } + fputs($this->realtime_log_file, $entry); + } + } +} diff --git a/3rdparty/phpseclib/phpseclib/phpseclib/Net/SSH2.php b/3rdparty/phpseclib/phpseclib/phpseclib/Net/SSH2.php new file mode 100644 index 00000000..607cc214 --- /dev/null +++ b/3rdparty/phpseclib/phpseclib/phpseclib/Net/SSH2.php @@ -0,0 +1,5529 @@ + + * login('username', 'password')) { + * exit('Login Failed'); + * } + * + * echo $ssh->exec('pwd'); + * echo $ssh->exec('ls -la'); + * ?> + * + * + * + * setPassword('whatever'); + * $key->loadKey(file_get_contents('privatekey')); + * + * $ssh = new \phpseclib\Net\SSH2('www.domain.tld'); + * if (!$ssh->login('username', $key)) { + * exit('Login Failed'); + * } + * + * echo $ssh->read('username@username:~$'); + * $ssh->write("ls -la\n"); + * echo $ssh->read('username@username:~$'); + * ?> + * + * + * @category Net + * @package SSH2 + * @author Jim Wigginton + * @copyright 2007 Jim Wigginton + * @license http://www.opensource.org/licenses/mit-license.html MIT License + * @link http://phpseclib.sourceforge.net + */ + +namespace phpseclib\Net; + +use phpseclib\Crypt\Base; +use phpseclib\Crypt\Blowfish; +use phpseclib\Crypt\Hash; +use phpseclib\Crypt\Random; +use phpseclib\Crypt\RC4; +use phpseclib\Crypt\Rijndael; +use phpseclib\Crypt\RSA; +use phpseclib\Crypt\TripleDES; +use phpseclib\Crypt\Twofish; +use phpseclib\Math\BigInteger; // Used to do Diffie-Hellman key exchange and DSA/RSA signature verification. +use phpseclib\System\SSH\Agent; + +/** + * Pure-PHP implementation of SSHv2. + * + * @package SSH2 + * @author Jim Wigginton + * @access public + */ +class SSH2 +{ + /**#@+ + * Compression Types + * + * @access private + */ + /** + * No compression + */ + const NET_SSH2_COMPRESSION_NONE = 1; + /** + * zlib compression + */ + const NET_SSH2_COMPRESSION_ZLIB = 2; + /** + * zlib@openssh.com + */ + const NET_SSH2_COMPRESSION_ZLIB_AT_OPENSSH = 3; + /**#@-*/ + + /**#@+ + * Execution Bitmap Masks + * + * @see \phpseclib\Net\SSH2::bitmap + * @access private + */ + const MASK_CONSTRUCTOR = 0x00000001; + const MASK_CONNECTED = 0x00000002; + const MASK_LOGIN_REQ = 0x00000004; + const MASK_LOGIN = 0x00000008; + const MASK_SHELL = 0x00000010; + const MASK_WINDOW_ADJUST = 0x00000020; + /**#@-*/ + + /**#@+ + * Channel constants + * + * RFC4254 refers not to client and server channels but rather to sender and recipient channels. we don't refer + * to them in that way because RFC4254 toggles the meaning. the client sends a SSH_MSG_CHANNEL_OPEN message with + * a sender channel and the server sends a SSH_MSG_CHANNEL_OPEN_CONFIRMATION in response, with a sender and a + * recepient channel. at first glance, you might conclude that SSH_MSG_CHANNEL_OPEN_CONFIRMATION's sender channel + * would be the same thing as SSH_MSG_CHANNEL_OPEN's sender channel, but it's not, per this snipet: + * The 'recipient channel' is the channel number given in the original + * open request, and 'sender channel' is the channel number allocated by + * the other side. + * + * @see \phpseclib\Net\SSH2::_send_channel_packet() + * @see \phpseclib\Net\SSH2::_get_channel_packet() + * @access private + */ + const CHANNEL_EXEC = 1; // PuTTy uses 0x100 + const CHANNEL_SHELL = 2; + const CHANNEL_SUBSYSTEM = 3; + const CHANNEL_AGENT_FORWARD = 4; + const CHANNEL_KEEP_ALIVE = 5; + /**#@-*/ + + /**#@+ + * @access public + * @see \phpseclib\Net\SSH2::getLog() + */ + /** + * Returns the message numbers + */ + const LOG_SIMPLE = 1; + /** + * Returns the message content + */ + const LOG_COMPLEX = 2; + /** + * Outputs the content real-time + */ + const LOG_REALTIME = 3; + /** + * Dumps the content real-time to a file + */ + const LOG_REALTIME_FILE = 4; + /** + * Make sure that the log never gets larger than this + */ + const LOG_MAX_SIZE = 1048576; // 1024 * 1024 + /**#@-*/ + + /**#@+ + * @access public + * @see \phpseclib\Net\SSH2::read() + */ + /** + * Returns when a string matching $expect exactly is found + */ + const READ_SIMPLE = 1; + /** + * Returns when a string matching the regular expression $expect is found + */ + const READ_REGEX = 2; + /** + * Returns whenever a data packet is received. + * + * Some data packets may only contain a single character so it may be necessary + * to call read() multiple times when using this option + */ + const READ_NEXT = 3; + /**#@-*/ + + /** + * The SSH identifier + * + * @var string + * @access private + */ + var $identifier; + + /** + * The Socket Object + * + * @var object + * @access private + */ + var $fsock; + + /** + * Execution Bitmap + * + * The bits that are set represent functions that have been called already. This is used to determine + * if a requisite function has been successfully executed. If not, an error should be thrown. + * + * @var int + * @access private + */ + var $bitmap = 0; + + /** + * Error information + * + * @see self::getErrors() + * @see self::getLastError() + * @var string + * @access private + */ + var $errors = array(); + + /** + * Server Identifier + * + * @see self::getServerIdentification() + * @var array|false + * @access private + */ + var $server_identifier = false; + + /** + * Key Exchange Algorithms + * + * @see self::getKexAlgorithims() + * @var array|false + * @access private + */ + var $kex_algorithms = false; + + /** + * Key Exchange Algorithm + * + * @see self::getMethodsNegotiated() + * @var string|false + * @access private + */ + var $kex_algorithm = false; + + /** + * Minimum Diffie-Hellman Group Bit Size in RFC 4419 Key Exchange Methods + * + * @see self::_key_exchange() + * @var int + * @access private + */ + var $kex_dh_group_size_min = 1536; + + /** + * Preferred Diffie-Hellman Group Bit Size in RFC 4419 Key Exchange Methods + * + * @see self::_key_exchange() + * @var int + * @access private + */ + var $kex_dh_group_size_preferred = 2048; + + /** + * Maximum Diffie-Hellman Group Bit Size in RFC 4419 Key Exchange Methods + * + * @see self::_key_exchange() + * @var int + * @access private + */ + var $kex_dh_group_size_max = 4096; + + /** + * Server Host Key Algorithms + * + * @see self::getServerHostKeyAlgorithms() + * @var array|false + * @access private + */ + var $server_host_key_algorithms = false; + + /** + * Supported Private Key Algorithms + * + * In theory this should be the same as the Server Host Key Algorithms but, in practice, + * some servers (eg. Azure) will support rsa-sha2-512 as a server host key algorithm but + * not a private key algorithm + * + * @see self::privatekey_login() + * @var array|false + */ + var $supported_private_key_algorithms = false; + + /** + * Encryption Algorithms: Client to Server + * + * @see self::getEncryptionAlgorithmsClient2Server() + * @var array|false + * @access private + */ + var $encryption_algorithms_client_to_server = false; + + /** + * Encryption Algorithms: Server to Client + * + * @see self::getEncryptionAlgorithmsServer2Client() + * @var array|false + * @access private + */ + var $encryption_algorithms_server_to_client = false; + + /** + * MAC Algorithms: Client to Server + * + * @see self::getMACAlgorithmsClient2Server() + * @var array|false + * @access private + */ + var $mac_algorithms_client_to_server = false; + + /** + * MAC Algorithms: Server to Client + * + * @see self::getMACAlgorithmsServer2Client() + * @var array|false + * @access private + */ + var $mac_algorithms_server_to_client = false; + + /** + * Compression Algorithms: Client to Server + * + * @see self::getCompressionAlgorithmsClient2Server() + * @var array|false + * @access private + */ + var $compression_algorithms_client_to_server = false; + + /** + * Compression Algorithms: Server to Client + * + * @see self::getCompressionAlgorithmsServer2Client() + * @var array|false + * @access private + */ + var $compression_algorithms_server_to_client = false; + + /** + * Languages: Server to Client + * + * @see self::getLanguagesServer2Client() + * @var array|false + * @access private + */ + var $languages_server_to_client = false; + + /** + * Languages: Client to Server + * + * @see self::getLanguagesClient2Server() + * @var array|false + * @access private + */ + var $languages_client_to_server = false; + + /** + * Preferred Algorithms + * + * @see self::setPreferredAlgorithms() + * @var array + * @access private + */ + var $preferred = array(); + + /** + * Block Size for Server to Client Encryption + * + * "Note that the length of the concatenation of 'packet_length', + * 'padding_length', 'payload', and 'random padding' MUST be a multiple + * of the cipher block size or 8, whichever is larger. This constraint + * MUST be enforced, even when using stream ciphers." + * + * -- http://tools.ietf.org/html/rfc4253#section-6 + * + * @see self::__construct() + * @see self::_send_binary_packet() + * @var int + * @access private + */ + var $encrypt_block_size = 8; + + /** + * Block Size for Client to Server Encryption + * + * @see self::__construct() + * @see self::_get_binary_packet() + * @var int + * @access private + */ + var $decrypt_block_size = 8; + + /** + * Server to Client Encryption Object + * + * @see self::_get_binary_packet() + * @var object + * @access private + */ + var $decrypt = false; + + /** + * Decryption Algorithm Name + * + * @var string|null + * @access private + */ + var $decryptName; + + /** + * Client to Server Encryption Object + * + * @see self::_send_binary_packet() + * @var object + * @access private + */ + var $encrypt = false; + + /** + * Encryption Algorithm Name + * + * @var string|null + * @access private + */ + var $encryptName; + + /** + * Client to Server HMAC Object + * + * @see self::_send_binary_packet() + * @var object + * @access private + */ + var $hmac_create = false; + + /** + * Client to Server HMAC Name + * + * @var string|false + */ + private $hmac_create_name; + + /** + * Server to Client HMAC Object + * + * @see self::_get_binary_packet() + * @var object + * @access private + */ + var $hmac_check = false; + + /** + * Server to Client HMAC Name + * + * @var string|false + */ + var $hmac_check_name; + + /** + * Size of server to client HMAC + * + * We need to know how big the HMAC will be for the server to client direction so that we know how many bytes to read. + * For the client to server side, the HMAC object will make the HMAC as long as it needs to be. All we need to do is + * append it. + * + * @see self::_get_binary_packet() + * @var int + * @access private + */ + var $hmac_size = false; + + /** + * Server Public Host Key + * + * @see self::getServerPublicHostKey() + * @var string + * @access private + */ + var $server_public_host_key; + + /** + * Session identifier + * + * "The exchange hash H from the first key exchange is additionally + * used as the session identifier, which is a unique identifier for + * this connection." + * + * -- http://tools.ietf.org/html/rfc4253#section-7.2 + * + * @see self::_key_exchange() + * @var string + * @access private + */ + var $session_id = false; + + /** + * Exchange hash + * + * The current exchange hash + * + * @see self::_key_exchange() + * @var string + * @access private + */ + var $exchange_hash = false; + + /** + * Message Numbers + * + * @see self::__construct() + * @var array + * @access private + */ + var $message_numbers = array(); + + /** + * Disconnection Message 'reason codes' defined in RFC4253 + * + * @see self::__construct() + * @var array + * @access private + */ + var $disconnect_reasons = array(); + + /** + * SSH_MSG_CHANNEL_OPEN_FAILURE 'reason codes', defined in RFC4254 + * + * @see self::__construct() + * @var array + * @access private + */ + var $channel_open_failure_reasons = array(); + + /** + * Terminal Modes + * + * @link http://tools.ietf.org/html/rfc4254#section-8 + * @see self::__construct() + * @var array + * @access private + */ + var $terminal_modes = array(); + + /** + * SSH_MSG_CHANNEL_EXTENDED_DATA's data_type_codes + * + * @link http://tools.ietf.org/html/rfc4254#section-5.2 + * @see self::__construct() + * @var array + * @access private + */ + var $channel_extended_data_type_codes = array(); + + /** + * Send Sequence Number + * + * See 'Section 6.4. Data Integrity' of rfc4253 for more info. + * + * @see self::_send_binary_packet() + * @var int + * @access private + */ + var $send_seq_no = 0; + + /** + * Get Sequence Number + * + * See 'Section 6.4. Data Integrity' of rfc4253 for more info. + * + * @see self::_get_binary_packet() + * @var int + * @access private + */ + var $get_seq_no = 0; + + /** + * Server Channels + * + * Maps client channels to server channels + * + * @see self::_get_channel_packet() + * @see self::exec() + * @var array + * @access private + */ + var $server_channels = array(); + + /** + * Channel Buffers + * + * If a client requests a packet from one channel but receives two packets from another those packets should + * be placed in a buffer + * + * @see self::_get_channel_packet() + * @see self::exec() + * @var array + * @access private + */ + var $channel_buffers = array(); + + /** + * Channel Status + * + * Contains the type of the last sent message + * + * @see self::_get_channel_packet() + * @var array + * @access private + */ + var $channel_status = array(); + + /** + * Packet Size + * + * Maximum packet size indexed by channel + * + * @see self::_send_channel_packet() + * @var array + * @access private + */ + var $packet_size_client_to_server = array(); + + /** + * Message Number Log + * + * @see self::getLog() + * @var array + * @access private + */ + var $message_number_log = array(); + + /** + * Message Log + * + * @see self::getLog() + * @var array + * @access private + */ + var $message_log = array(); + + /** + * The Window Size + * + * Bytes the other party can send before it must wait for the window to be adjusted (0x7FFFFFFF = 2GB) + * + * @var int + * @see self::_send_channel_packet() + * @see self::exec() + * @access private + */ + var $window_size = 0x7FFFFFFF; + + /** + * What we resize the window to + * + * When PuTTY resizes the window it doesn't add an additional 0x7FFFFFFF bytes - it adds 0x40000000 bytes. + * Some SFTP clients (GoAnywhere) don't support adding 0x7FFFFFFF to the window size after the fact so + * we'll just do what PuTTY does + * + * @var int + * @see self::_send_channel_packet() + * @see self::exec() + * @access private + */ + var $window_resize = 0x40000000; + + /** + * Window size, server to client + * + * Window size indexed by channel + * + * @see self::_send_channel_packet() + * @var array + * @access private + */ + var $window_size_server_to_client = array(); + + /** + * Window size, client to server + * + * Window size indexed by channel + * + * @see self::_get_channel_packet() + * @var array + * @access private + */ + var $window_size_client_to_server = array(); + + /** + * Server signature + * + * Verified against $this->session_id + * + * @see self::getServerPublicHostKey() + * @var string + * @access private + */ + var $signature = ''; + + /** + * Server signature format + * + * ssh-rsa or ssh-dss. + * + * @see self::getServerPublicHostKey() + * @var string + * @access private + */ + var $signature_format = ''; + + /** + * Interactive Buffer + * + * @see self::read() + * @var array + * @access private + */ + var $interactiveBuffer = ''; + + /** + * Current log size + * + * Should never exceed self::LOG_MAX_SIZE + * + * @see self::_send_binary_packet() + * @see self::_get_binary_packet() + * @var int + * @access private + */ + var $log_size; + + /** + * Timeout + * + * @see self::setTimeout() + * @access private + */ + var $timeout; + + /** + * Current Timeout + * + * @see self::_get_channel_packet() + * @access private + */ + var $curTimeout; + + /** + * Keep Alive Interval + * + * @see self::setKeepAlive() + * @access private + */ + var $keepAlive; + + /** + * Real-time log file pointer + * + * @see self::_append_log() + * @var resource + * @access private + */ + var $realtime_log_file; + + /** + * Real-time log file size + * + * @see self::_append_log() + * @var int + * @access private + */ + var $realtime_log_size; + + /** + * Has the signature been validated? + * + * @see self::getServerPublicHostKey() + * @var bool + * @access private + */ + var $signature_validated = false; + + /** + * Real-time log file wrap boolean + * + * @see self::_append_log() + * @access private + */ + var $realtime_log_wrap; + + /** + * Flag to suppress stderr from output + * + * @see self::enableQuietMode() + * @access private + */ + var $quiet_mode = false; + + /** + * Time of first network activity + * + * @var int + * @access private + */ + var $last_packet; + + /** + * Exit status returned from ssh if any + * + * @var int + * @access private + */ + var $exit_status; + + /** + * Flag to request a PTY when using exec() + * + * @var bool + * @see self::enablePTY() + * @access private + */ + var $request_pty = false; + + /** + * Flag set while exec() is running when using enablePTY() + * + * @var bool + * @access private + */ + var $in_request_pty_exec = false; + + /** + * Flag set after startSubsystem() is called + * + * @var bool + * @access private + */ + var $in_subsystem; + + /** + * Contents of stdError + * + * @var string + * @access private + */ + var $stdErrorLog; + + /** + * The Last Interactive Response + * + * @see self::_keyboard_interactive_process() + * @var string + * @access private + */ + var $last_interactive_response = ''; + + /** + * Keyboard Interactive Request / Responses + * + * @see self::_keyboard_interactive_process() + * @var array + * @access private + */ + var $keyboard_requests_responses = array(); + + /** + * Banner Message + * + * Quoting from the RFC, "in some jurisdictions, sending a warning message before + * authentication may be relevant for getting legal protection." + * + * @see self::_filter() + * @see self::getBannerMessage() + * @var string + * @access private + */ + var $banner_message = ''; + + /** + * Did read() timeout or return normally? + * + * @see self::isTimeout() + * @var bool + * @access private + */ + var $is_timeout = false; + + /** + * Log Boundary + * + * @see self::_format_log() + * @var string + * @access private + */ + var $log_boundary = ':'; + + /** + * Log Long Width + * + * @see self::_format_log() + * @var int + * @access private + */ + var $log_long_width = 65; + + /** + * Log Short Width + * + * @see self::_format_log() + * @var int + * @access private + */ + var $log_short_width = 16; + + /** + * Hostname + * + * @see self::__construct() + * @see self::_connect() + * @var string + * @access private + */ + var $host; + + /** + * Port Number + * + * @see self::__construct() + * @see self::_connect() + * @var int + * @access private + */ + var $port; + + /** + * Number of columns for terminal window size + * + * @see self::getWindowColumns() + * @see self::setWindowColumns() + * @see self::setWindowSize() + * @var int + * @access private + */ + var $windowColumns = 80; + + /** + * Number of columns for terminal window size + * + * @see self::getWindowRows() + * @see self::setWindowRows() + * @see self::setWindowSize() + * @var int + * @access private + */ + var $windowRows = 24; + + /** + * Crypto Engine + * + * @see self::setCryptoEngine() + * @see self::_key_exchange() + * @var int + * @access private + */ + var $crypto_engine = false; + + /** + * A System_SSH_Agent for use in the SSH2 Agent Forwarding scenario + * + * @var System_SSH_Agent + * @access private + */ + var $agent; + + /** + * Send the identification string first? + * + * @var bool + * @access private + */ + var $send_id_string_first = true; + + /** + * Send the key exchange initiation packet first? + * + * @var bool + * @access private + */ + var $send_kex_first = true; + + /** + * Some versions of OpenSSH incorrectly calculate the key size + * + * @var bool + * @access private + */ + var $bad_key_size_fix = false; + + /** + * Should we try to re-connect to re-establish keys? + * + * @var bool + * @access private + */ + var $retry_connect = false; + + /** + * Binary Packet Buffer + * + * @var string|false + * @access private + */ + var $binary_packet_buffer = false; + + /** + * Preferred Signature Format + * + * @var string|false + * @access private + */ + var $preferred_signature_format = false; + + /** + * Authentication Credentials + * + * @var array + * @access private + */ + var $auth = array(); + + /** + * The authentication methods that may productively continue authentication. + * + * @see https://tools.ietf.org/html/rfc4252#section-5.1 + * @var array|null + * @access private + */ + var $auth_methods_to_continue = null; + + /** + * Compression method + * + * @var int + * @access private + */ + var $compress = self::NET_SSH2_COMPRESSION_NONE; + + /** + * Decompression method + * + * @var resource|object + * @access private + */ + var $decompress = self::NET_SSH2_COMPRESSION_NONE; + + /** + * Compression context + * + * @var int + * @access private + */ + var $compress_context; + + /** + * Decompression context + * + * @var resource|object + * @access private + */ + var $decompress_context; + + /** + * Regenerate Compression Context + * + * @var bool + * @access private + */ + var $regenerate_compression_context = false; + + /** + * Regenerate Decompression Context + * + * @var bool + * @access private + */ + var $regenerate_decompression_context = false; + + /** + * Smart multi-factor authentication flag + * + * @var bool + * @access private + */ + var $smartMFA = true; + + /** + * Extra packets counter + * + * @var bool + * @access private + */ + var $extra_packets; + + /** + * Default Constructor. + * + * $host can either be a string, representing the host, or a stream resource. + * If $host is a stream resource then $port doesn't do anything, altho $timeout + * still will be used + * + * @param mixed $host + * @param int $port + * @param int $timeout + * @see self::login() + * @return \phpseclib\Net\SSH2 + * @access public + */ + function __construct($host, $port = 22, $timeout = 10) + { + $this->message_numbers = array( + 1 => 'NET_SSH2_MSG_DISCONNECT', + 2 => 'NET_SSH2_MSG_IGNORE', + 3 => 'NET_SSH2_MSG_UNIMPLEMENTED', + 4 => 'NET_SSH2_MSG_DEBUG', + 5 => 'NET_SSH2_MSG_SERVICE_REQUEST', + 6 => 'NET_SSH2_MSG_SERVICE_ACCEPT', + 7 => 'NET_SSH2_MSG_EXT_INFO', // RFC 8308 + 20 => 'NET_SSH2_MSG_KEXINIT', + 21 => 'NET_SSH2_MSG_NEWKEYS', + 30 => 'NET_SSH2_MSG_KEXDH_INIT', + 31 => 'NET_SSH2_MSG_KEXDH_REPLY', + 50 => 'NET_SSH2_MSG_USERAUTH_REQUEST', + 51 => 'NET_SSH2_MSG_USERAUTH_FAILURE', + 52 => 'NET_SSH2_MSG_USERAUTH_SUCCESS', + 53 => 'NET_SSH2_MSG_USERAUTH_BANNER', + + 80 => 'NET_SSH2_MSG_GLOBAL_REQUEST', + 81 => 'NET_SSH2_MSG_REQUEST_SUCCESS', + 82 => 'NET_SSH2_MSG_REQUEST_FAILURE', + 90 => 'NET_SSH2_MSG_CHANNEL_OPEN', + 91 => 'NET_SSH2_MSG_CHANNEL_OPEN_CONFIRMATION', + 92 => 'NET_SSH2_MSG_CHANNEL_OPEN_FAILURE', + 93 => 'NET_SSH2_MSG_CHANNEL_WINDOW_ADJUST', + 94 => 'NET_SSH2_MSG_CHANNEL_DATA', + 95 => 'NET_SSH2_MSG_CHANNEL_EXTENDED_DATA', + 96 => 'NET_SSH2_MSG_CHANNEL_EOF', + 97 => 'NET_SSH2_MSG_CHANNEL_CLOSE', + 98 => 'NET_SSH2_MSG_CHANNEL_REQUEST', + 99 => 'NET_SSH2_MSG_CHANNEL_SUCCESS', + 100 => 'NET_SSH2_MSG_CHANNEL_FAILURE' + ); + $this->disconnect_reasons = array( + 1 => 'NET_SSH2_DISCONNECT_HOST_NOT_ALLOWED_TO_CONNECT', + 2 => 'NET_SSH2_DISCONNECT_PROTOCOL_ERROR', + 3 => 'NET_SSH2_DISCONNECT_KEY_EXCHANGE_FAILED', + 4 => 'NET_SSH2_DISCONNECT_RESERVED', + 5 => 'NET_SSH2_DISCONNECT_MAC_ERROR', + 6 => 'NET_SSH2_DISCONNECT_COMPRESSION_ERROR', + 7 => 'NET_SSH2_DISCONNECT_SERVICE_NOT_AVAILABLE', + 8 => 'NET_SSH2_DISCONNECT_PROTOCOL_VERSION_NOT_SUPPORTED', + 9 => 'NET_SSH2_DISCONNECT_HOST_KEY_NOT_VERIFIABLE', + 10 => 'NET_SSH2_DISCONNECT_CONNECTION_LOST', + 11 => 'NET_SSH2_DISCONNECT_BY_APPLICATION', + 12 => 'NET_SSH2_DISCONNECT_TOO_MANY_CONNECTIONS', + 13 => 'NET_SSH2_DISCONNECT_AUTH_CANCELLED_BY_USER', + 14 => 'NET_SSH2_DISCONNECT_NO_MORE_AUTH_METHODS_AVAILABLE', + 15 => 'NET_SSH2_DISCONNECT_ILLEGAL_USER_NAME' + ); + $this->channel_open_failure_reasons = array( + 1 => 'NET_SSH2_OPEN_ADMINISTRATIVELY_PROHIBITED' + ); + $this->terminal_modes = array( + 0 => 'NET_SSH2_TTY_OP_END' + ); + $this->channel_extended_data_type_codes = array( + 1 => 'NET_SSH2_EXTENDED_DATA_STDERR' + ); + + $this->_define_array( + $this->message_numbers, + $this->disconnect_reasons, + $this->channel_open_failure_reasons, + $this->terminal_modes, + $this->channel_extended_data_type_codes, + array(60 => 'NET_SSH2_MSG_USERAUTH_PASSWD_CHANGEREQ'), + array(60 => 'NET_SSH2_MSG_USERAUTH_PK_OK'), + array(60 => 'NET_SSH2_MSG_USERAUTH_INFO_REQUEST', + 61 => 'NET_SSH2_MSG_USERAUTH_INFO_RESPONSE'), + // RFC 4419 - diffie-hellman-group-exchange-sha{1,256} + array(30 => 'NET_SSH2_MSG_KEXDH_GEX_REQUEST_OLD', + 31 => 'NET_SSH2_MSG_KEXDH_GEX_GROUP', + 32 => 'NET_SSH2_MSG_KEXDH_GEX_INIT', + 33 => 'NET_SSH2_MSG_KEXDH_GEX_REPLY', + 34 => 'NET_SSH2_MSG_KEXDH_GEX_REQUEST'), + // RFC 5656 - Elliptic Curves (for curve25519-sha256@libssh.org) + array(30 => 'NET_SSH2_MSG_KEX_ECDH_INIT', + 31 => 'NET_SSH2_MSG_KEX_ECDH_REPLY') + ); + + $this->timeout = $timeout; + + if (is_resource($host)) { + $this->fsock = $host; + return; + } + + if (is_string($host)) { + $this->host = $host; + $this->port = $port; + } + } + + /** + * Set Crypto Engine Mode + * + * Possible $engine values: + * CRYPT_MODE_INTERNAL, CRYPT_MODE_MCRYPT + * + * @param int $engine + * @access public + */ + function setCryptoEngine($engine) + { + $this->crypto_engine = $engine; + } + + /** + * Send Identification String First + * + * https://tools.ietf.org/html/rfc4253#section-4.2 says "when the connection has been established, + * both sides MUST send an identification string". It does not say which side sends it first. In + * theory it shouldn't matter but it is a fact of life that some SSH servers are simply buggy + * + * @access public + */ + function sendIdentificationStringFirst() + { + $this->send_id_string_first = true; + } + + /** + * Send Identification String Last + * + * https://tools.ietf.org/html/rfc4253#section-4.2 says "when the connection has been established, + * both sides MUST send an identification string". It does not say which side sends it first. In + * theory it shouldn't matter but it is a fact of life that some SSH servers are simply buggy + * + * @access public + */ + function sendIdentificationStringLast() + { + $this->send_id_string_first = false; + } + + /** + * Send SSH_MSG_KEXINIT First + * + * https://tools.ietf.org/html/rfc4253#section-7.1 says "key exchange begins by each sending + * sending the [SSH_MSG_KEXINIT] packet". It does not say which side sends it first. In theory + * it shouldn't matter but it is a fact of life that some SSH servers are simply buggy + * + * @access public + */ + function sendKEXINITFirst() + { + $this->send_kex_first = true; + } + + /** + * Send SSH_MSG_KEXINIT Last + * + * https://tools.ietf.org/html/rfc4253#section-7.1 says "key exchange begins by each sending + * sending the [SSH_MSG_KEXINIT] packet". It does not say which side sends it first. In theory + * it shouldn't matter but it is a fact of life that some SSH servers are simply buggy + * + * @access public + */ + function sendKEXINITLast() + { + $this->send_kex_first = false; + } + + /** + * Connect to an SSHv2 server + * + * @return bool + * @access private + */ + function _connect() + { + if ($this->bitmap & self::MASK_CONSTRUCTOR) { + return false; + } + + $this->bitmap |= self::MASK_CONSTRUCTOR; + + $this->curTimeout = $this->timeout; + + $this->last_packet = microtime(true); + + if (!is_resource($this->fsock)) { + $start = microtime(true); + // with stream_select a timeout of 0 means that no timeout takes place; + // with fsockopen a timeout of 0 means that you instantly timeout + // to resolve this incompatibility a timeout of 100,000 will be used for fsockopen if timeout is 0 + $this->fsock = @fsockopen($this->host, $this->port, $errno, $errstr, $this->curTimeout == 0 ? 100000 : $this->curTimeout); + if (!$this->fsock) { + $host = $this->host . ':' . $this->port; + user_error(rtrim("Cannot connect to $host. Error $errno. $errstr")); + return false; + } + $elapsed = microtime(true) - $start; + + if ($this->curTimeout) { + $this->curTimeout-= $elapsed; + if ($this->curTimeout < 0) { + $this->is_timeout = true; + return false; + } + } + } + + $this->identifier = $this->_generate_identifier(); + + if ($this->send_id_string_first) { + fputs($this->fsock, $this->identifier . "\r\n"); + } + + /* According to the SSH2 specs, + + "The server MAY send other lines of data before sending the version + string. Each line SHOULD be terminated by a Carriage Return and Line + Feed. Such lines MUST NOT begin with "SSH-", and SHOULD be encoded + in ISO-10646 UTF-8 [RFC3629] (language is not specified). Clients + MUST be able to process such lines." */ + $data = ''; + while (!feof($this->fsock) && !preg_match('#(.*)^(SSH-(\d\.\d+).*)#ms', $data, $matches)) { + $line = ''; + while (true) { + if ($this->curTimeout) { + if ($this->curTimeout < 0) { + $this->is_timeout = true; + return false; + } + $read = array($this->fsock); + $write = $except = null; + $start = microtime(true); + $sec = (int) floor($this->curTimeout); + $usec = (int) (1000000 * ($this->curTimeout - $sec)); + // on windows this returns a "Warning: Invalid CRT parameters detected" error + // the !count() is done as a workaround for + if (!@stream_select($read, $write, $except, $sec, $usec) && !count($read)) { + $this->is_timeout = true; + return false; + } + $elapsed = microtime(true) - $start; + $this->curTimeout-= $elapsed; + } + + $temp = stream_get_line($this->fsock, 255, "\n"); + if (strlen($temp) == 255) { + continue; + } + + if ($temp === false) { + return false; + } + + $line.= "$temp\n"; + + // quoting RFC4253, "Implementers who wish to maintain + // compatibility with older, undocumented versions of this protocol may + // want to process the identification string without expecting the + // presence of the carriage return character for reasons described in + // Section 5 of this document." + + //if (substr($line, -2) == "\r\n") { + // break; + //} + + break; + } + + $data.= $line; + } + + if (feof($this->fsock)) { + $this->bitmap = 0; + user_error('Connection closed by server'); + return false; + } + + $extra = $matches[1]; + + if (defined('NET_SSH2_LOGGING')) { + $this->_append_log('<-', $matches[0]); + $this->_append_log('->', $this->identifier . "\r\n"); + } + + $this->server_identifier = trim($temp, "\r\n"); + if (strlen($extra)) { + $this->errors[] = $data; + } + + if (version_compare($matches[3], '1.99', '<')) { + user_error("Cannot connect to SSH $matches[3] servers"); + return false; + } + + if (!$this->send_id_string_first) { + fputs($this->fsock, $this->identifier . "\r\n"); + } + + if (!$this->send_kex_first) { + $response = $this->_get_binary_packet(); + if ($response === false) { + $this->bitmap = 0; + user_error('Connection closed by server'); + return false; + } + + if (!strlen($response) || ord($response[0]) != NET_SSH2_MSG_KEXINIT) { + user_error('Expected SSH_MSG_KEXINIT'); + return false; + } + + if (!$this->_key_exchange($response)) { + return false; + } + } + + if ($this->send_kex_first && !$this->_key_exchange()) { + return false; + } + + $this->bitmap|= self::MASK_CONNECTED; + + return true; + } + + /** + * Generates the SSH identifier + * + * You should overwrite this method in your own class if you want to use another identifier + * + * @access protected + * @return string + */ + function _generate_identifier() + { + $identifier = 'SSH-2.0-phpseclib_2.0'; + + $ext = array(); + if (function_exists('sodium_crypto_box_publickey_from_secretkey')) { + $ext[] = 'libsodium'; + } + + if (extension_loaded('openssl')) { + $ext[] = 'openssl'; + } elseif (extension_loaded('mcrypt')) { + $ext[] = 'mcrypt'; + } + + if (extension_loaded('gmp')) { + $ext[] = 'gmp'; + } elseif (extension_loaded('bcmath')) { + $ext[] = 'bcmath'; + } + + if (!empty($ext)) { + $identifier .= ' (' . implode(', ', $ext) . ')'; + } + + return $identifier; + } + + /** + * Key Exchange + * + * @param string $kexinit_payload_server optional + * @access private + */ + function _key_exchange($kexinit_payload_server = false) + { + $preferred = $this->preferred; + $send_kex = true; + + $kex_algorithms = isset($preferred['kex']) ? + $preferred['kex'] : + $this->getSupportedKEXAlgorithms(); + $server_host_key_algorithms = isset($preferred['hostkey']) ? + $preferred['hostkey'] : + $this->getSupportedHostKeyAlgorithms(); + $s2c_encryption_algorithms = isset($preferred['server_to_client']['crypt']) ? + $preferred['server_to_client']['crypt'] : + $this->getSupportedEncryptionAlgorithms(); + $c2s_encryption_algorithms = isset($preferred['client_to_server']['crypt']) ? + $preferred['client_to_server']['crypt'] : + $this->getSupportedEncryptionAlgorithms(); + $s2c_mac_algorithms = isset($preferred['server_to_client']['mac']) ? + $preferred['server_to_client']['mac'] : + $this->getSupportedMACAlgorithms(); + $c2s_mac_algorithms = isset($preferred['client_to_server']['mac']) ? + $preferred['client_to_server']['mac'] : + $this->getSupportedMACAlgorithms(); + $s2c_compression_algorithms = isset($preferred['server_to_client']['comp']) ? + $preferred['server_to_client']['comp'] : + $this->getSupportedCompressionAlgorithms(); + $c2s_compression_algorithms = isset($preferred['client_to_server']['comp']) ? + $preferred['client_to_server']['comp'] : + $this->getSupportedCompressionAlgorithms(); + + $kex_algorithms = array_merge($kex_algorithms, array('ext-info-c', 'kex-strict-c-v00@openssh.com')); + + // some SSH servers have buggy implementations of some of the above algorithms + switch (true) { + case $this->server_identifier == 'SSH-2.0-SSHD': + case substr($this->server_identifier, 0, 13) == 'SSH-2.0-DLINK': + if (!isset($preferred['server_to_client']['mac'])) { + $s2c_mac_algorithms = array_values(array_diff( + $s2c_mac_algorithms, + array('hmac-sha1-96', 'hmac-md5-96') + )); + } + if (!isset($preferred['client_to_server']['mac'])) { + $c2s_mac_algorithms = array_values(array_diff( + $c2s_mac_algorithms, + array('hmac-sha1-96', 'hmac-md5-96') + )); + } + } + + $str_kex_algorithms = implode(',', $kex_algorithms); + $str_server_host_key_algorithms = implode(',', $server_host_key_algorithms); + $encryption_algorithms_server_to_client = implode(',', $s2c_encryption_algorithms); + $encryption_algorithms_client_to_server = implode(',', $c2s_encryption_algorithms); + $mac_algorithms_server_to_client = implode(',', $s2c_mac_algorithms); + $mac_algorithms_client_to_server = implode(',', $c2s_mac_algorithms); + $compression_algorithms_server_to_client = implode(',', $s2c_compression_algorithms); + $compression_algorithms_client_to_server = implode(',', $c2s_compression_algorithms); + + $client_cookie = Random::string(16); + + $kexinit_payload_client = pack( + 'Ca*Na*Na*Na*Na*Na*Na*Na*Na*Na*Na*CN', + NET_SSH2_MSG_KEXINIT, + $client_cookie, + strlen($str_kex_algorithms), + $str_kex_algorithms, + strlen($str_server_host_key_algorithms), + $str_server_host_key_algorithms, + strlen($encryption_algorithms_client_to_server), + $encryption_algorithms_client_to_server, + strlen($encryption_algorithms_server_to_client), + $encryption_algorithms_server_to_client, + strlen($mac_algorithms_client_to_server), + $mac_algorithms_client_to_server, + strlen($mac_algorithms_server_to_client), + $mac_algorithms_server_to_client, + strlen($compression_algorithms_client_to_server), + $compression_algorithms_client_to_server, + strlen($compression_algorithms_server_to_client), + $compression_algorithms_server_to_client, + 0, + '', + 0, + '', + 0, + 0 + ); + + if ($kexinit_payload_server === false) { + if (!$this->_send_binary_packet($kexinit_payload_client)) { + return false; + } + + $this->extra_packets = 0; + $kexinit_payload_server = $this->_get_binary_packet(); + if ($kexinit_payload_server === false) { + $this->bitmap = 0; + user_error('Connection closed by server'); + return false; + } + + if (!strlen($kexinit_payload_server) || ord($kexinit_payload_server[0]) != NET_SSH2_MSG_KEXINIT) { + user_error('Expected SSH_MSG_KEXINIT'); + return false; + } + + $send_kex = false; + } + + $response = $kexinit_payload_server; + $this->_string_shift($response, 1); // skip past the message number (it should be SSH_MSG_KEXINIT) + $server_cookie = $this->_string_shift($response, 16); + + if (strlen($response) < 4) { + return false; + } + $temp = unpack('Nlength', $this->_string_shift($response, 4)); + $this->kex_algorithms = explode(',', $this->_string_shift($response, $temp['length'])); + if (in_array('kex-strict-s-v00@openssh.com', $this->kex_algorithms)) { + if ($this->session_id === false && $this->extra_packets) { + user_error('Possible Terrapin Attack detected'); + return $this->_disconnect(NET_SSH2_DISCONNECT_KEY_EXCHANGE_FAILED); + } + } + + if (strlen($response) < 4) { + return false; + } + $temp = unpack('Nlength', $this->_string_shift($response, 4)); + $this->server_host_key_algorithms = explode(',', $this->_string_shift($response, $temp['length'])); + + $this->supported_private_key_algorithms = $this->server_host_key_algorithms; + + if (strlen($response) < 4) { + return false; + } + $temp = unpack('Nlength', $this->_string_shift($response, 4)); + $this->encryption_algorithms_client_to_server = explode(',', $this->_string_shift($response, $temp['length'])); + + if (strlen($response) < 4) { + return false; + } + $temp = unpack('Nlength', $this->_string_shift($response, 4)); + $this->encryption_algorithms_server_to_client = explode(',', $this->_string_shift($response, $temp['length'])); + + if (strlen($response) < 4) { + return false; + } + $temp = unpack('Nlength', $this->_string_shift($response, 4)); + $this->mac_algorithms_client_to_server = explode(',', $this->_string_shift($response, $temp['length'])); + + if (strlen($response) < 4) { + return false; + } + $temp = unpack('Nlength', $this->_string_shift($response, 4)); + $this->mac_algorithms_server_to_client = explode(',', $this->_string_shift($response, $temp['length'])); + + if (strlen($response) < 4) { + return false; + } + $temp = unpack('Nlength', $this->_string_shift($response, 4)); + $this->compression_algorithms_client_to_server = explode(',', $this->_string_shift($response, $temp['length'])); + + if (strlen($response) < 4) { + return false; + } + $temp = unpack('Nlength', $this->_string_shift($response, 4)); + $this->compression_algorithms_server_to_client = explode(',', $this->_string_shift($response, $temp['length'])); + + if (strlen($response) < 4) { + return false; + } + $temp = unpack('Nlength', $this->_string_shift($response, 4)); + $this->languages_client_to_server = explode(',', $this->_string_shift($response, $temp['length'])); + + if (strlen($response) < 4) { + return false; + } + $temp = unpack('Nlength', $this->_string_shift($response, 4)); + $this->languages_server_to_client = explode(',', $this->_string_shift($response, $temp['length'])); + + if (!strlen($response)) { + return false; + } + extract(unpack('Cfirst_kex_packet_follows', $this->_string_shift($response, 1))); + $first_kex_packet_follows = $first_kex_packet_follows != 0; + + if ($send_kex && !$this->_send_binary_packet($kexinit_payload_client)) { + return false; + } + + // we need to decide upon the symmetric encryption algorithms before we do the diffie-hellman key exchange + // we don't initialize any crypto-objects, yet - we do that, later. for now, we need the lengths to make the + // diffie-hellman key exchange as fast as possible + $decrypt = $this->_array_intersect_first($s2c_encryption_algorithms, $this->encryption_algorithms_server_to_client); + $decryptKeyLength = $this->_encryption_algorithm_to_key_size($decrypt); + if ($decryptKeyLength === null) { + user_error('No compatible server to client encryption algorithms found'); + return $this->_disconnect(NET_SSH2_DISCONNECT_KEY_EXCHANGE_FAILED); + } + + $encrypt = $this->_array_intersect_first($c2s_encryption_algorithms, $this->encryption_algorithms_client_to_server); + $encryptKeyLength = $this->_encryption_algorithm_to_key_size($encrypt); + if ($encryptKeyLength === null) { + user_error('No compatible client to server encryption algorithms found'); + return $this->_disconnect(NET_SSH2_DISCONNECT_KEY_EXCHANGE_FAILED); + } + + // through diffie-hellman key exchange a symmetric key is obtained + $this->kex_algorithm = $kex_algorithm = $this->_array_intersect_first($kex_algorithms, $this->kex_algorithms); + if ($kex_algorithm === false) { + user_error('No compatible key exchange algorithms found'); + return $this->_disconnect(NET_SSH2_DISCONNECT_KEY_EXCHANGE_FAILED); + } + + $server_host_key_algorithm = $this->_array_intersect_first($server_host_key_algorithms, $this->server_host_key_algorithms); + if ($server_host_key_algorithm === false) { + user_error('No compatible server host key algorithms found'); + return $this->_disconnect(NET_SSH2_DISCONNECT_KEY_EXCHANGE_FAILED); + } + + $mac_algorithm_in = $this->_array_intersect_first($s2c_mac_algorithms, $this->mac_algorithms_server_to_client); + if ($mac_algorithm_in === false) { + user_error('No compatible server to client message authentication algorithms found'); + return $this->_disconnect(NET_SSH2_DISCONNECT_KEY_EXCHANGE_FAILED); + } + + $compression_map = array( + 'none' => self::NET_SSH2_COMPRESSION_NONE, + 'zlib' => self::NET_SSH2_COMPRESSION_ZLIB, + 'zlib@openssh.com' => self::NET_SSH2_COMPRESSION_ZLIB_AT_OPENSSH + ); + + $compression_algorithm_out = $this->_array_intersect_first($c2s_compression_algorithms, $this->compression_algorithms_client_to_server); + if ($compression_algorithm_out === false) { + user_error('No compatible client to server compression algorithms found'); + return $this->_disconnect(NET_SSH2_DISCONNECT_KEY_EXCHANGE_FAILED); + } + $this->compress = $compression_map[$compression_algorithm_out]; + + $compression_algorithm_in = $this->_array_intersect_first($s2c_compression_algorithms, $this->compression_algorithms_server_to_client); + if ($compression_algorithm_in === false) { + user_error('No compatible server to client compression algorithms found'); + return $this->_disconnect(NET_SSH2_DISCONNECT_KEY_EXCHANGE_FAILED); + } + $this->decompress = $compression_map[$compression_algorithm_in]; + + // Only relevant in diffie-hellman-group-exchange-sha{1,256}, otherwise empty. + $exchange_hash_rfc4419 = ''; + + if ($kex_algorithm === 'curve25519-sha256@libssh.org') { + $x = Random::string(32); + $eBytes = sodium_crypto_box_publickey_from_secretkey($x); + $clientKexInitMessage = 'NET_SSH2_MSG_KEX_ECDH_INIT'; + $serverKexReplyMessage = 'NET_SSH2_MSG_KEX_ECDH_REPLY'; + $kexHash = new Hash('sha256'); + } else { + if (strpos($kex_algorithm, 'diffie-hellman-group-exchange') === 0) { + $dh_group_sizes_packed = pack( + 'NNN', + $this->kex_dh_group_size_min, + $this->kex_dh_group_size_preferred, + $this->kex_dh_group_size_max + ); + $packet = pack( + 'Ca*', + NET_SSH2_MSG_KEXDH_GEX_REQUEST, + $dh_group_sizes_packed + ); + if (!$this->_send_binary_packet($packet)) { + return false; + } + $this->_updateLogHistory('UNKNOWN (34)', 'NET_SSH2_MSG_KEXDH_GEX_REQUEST'); + + $response = $this->_get_binary_packet(); + if ($response === false) { + $this->bitmap = 0; + user_error('Connection closed by server'); + return false; + } + extract(unpack('Ctype', $this->_string_shift($response, 1))); + if ($type != NET_SSH2_MSG_KEXDH_GEX_GROUP) { + user_error('Expected SSH_MSG_KEX_DH_GEX_GROUP'); + return false; + } + $this->_updateLogHistory('NET_SSH2_MSG_KEXDH_REPLY', 'NET_SSH2_MSG_KEXDH_GEX_GROUP'); + + if (strlen($response) < 4) { + return false; + } + extract(unpack('NprimeLength', $this->_string_shift($response, 4))); + $primeBytes = $this->_string_shift($response, $primeLength); + $prime = new BigInteger($primeBytes, -256); + + if (strlen($response) < 4) { + return false; + } + extract(unpack('NgLength', $this->_string_shift($response, 4))); + $gBytes = $this->_string_shift($response, $gLength); + $g = new BigInteger($gBytes, -256); + + $exchange_hash_rfc4419 = pack( + 'a*Na*Na*', + $dh_group_sizes_packed, + $primeLength, + $primeBytes, + $gLength, + $gBytes + ); + + $clientKexInitMessage = 'NET_SSH2_MSG_KEXDH_GEX_INIT'; + $serverKexReplyMessage = 'NET_SSH2_MSG_KEXDH_GEX_REPLY'; + } else { + switch ($kex_algorithm) { + // see http://tools.ietf.org/html/rfc2409#section-6.2 and + // http://tools.ietf.org/html/rfc2412, appendex E + case 'diffie-hellman-group1-sha1': + $prime = 'FFFFFFFFFFFFFFFFC90FDAA22168C234C4C6628B80DC1CD129024E088A67CC74' . + '020BBEA63B139B22514A08798E3404DDEF9519B3CD3A431B302B0A6DF25F1437' . + '4FE1356D6D51C245E485B576625E7EC6F44C42E9A637ED6B0BFF5CB6F406B7ED' . + 'EE386BFB5A899FA5AE9F24117C4B1FE649286651ECE65381FFFFFFFFFFFFFFFF'; + break; + // see http://tools.ietf.org/html/rfc3526#section-3 + case 'diffie-hellman-group14-sha1': + $prime = 'FFFFFFFFFFFFFFFFC90FDAA22168C234C4C6628B80DC1CD129024E088A67CC74' . + '020BBEA63B139B22514A08798E3404DDEF9519B3CD3A431B302B0A6DF25F1437' . + '4FE1356D6D51C245E485B576625E7EC6F44C42E9A637ED6B0BFF5CB6F406B7ED' . + 'EE386BFB5A899FA5AE9F24117C4B1FE649286651ECE45B3DC2007CB8A163BF05' . + '98DA48361C55D39A69163FA8FD24CF5F83655D23DCA3AD961C62F356208552BB' . + '9ED529077096966D670C354E4ABC9804F1746C08CA18217C32905E462E36CE3B' . + 'E39E772C180E86039B2783A2EC07A28FB5C55DF06F4C52C9DE2BCBF695581718' . + '3995497CEA956AE515D2261898FA051015728E5A8AACAA68FFFFFFFFFFFFFFFF'; + break; + } + // For both diffie-hellman-group1-sha1 and diffie-hellman-group14-sha1 + // the generator field element is 2 (decimal) and the hash function is sha1. + $g = new BigInteger(2); + $prime = new BigInteger($prime, 16); + $clientKexInitMessage = 'NET_SSH2_MSG_KEXDH_INIT'; + $serverKexReplyMessage = 'NET_SSH2_MSG_KEXDH_REPLY'; + } + + switch ($kex_algorithm) { + case 'diffie-hellman-group-exchange-sha256': + $kexHash = new Hash('sha256'); + break; + default: + $kexHash = new Hash('sha1'); + } + + /* To increase the speed of the key exchange, both client and server may + reduce the size of their private exponents. It should be at least + twice as long as the key material that is generated from the shared + secret. For more details, see the paper by van Oorschot and Wiener + [VAN-OORSCHOT]. + + -- http://tools.ietf.org/html/rfc4419#section-6.2 */ + $one = new BigInteger(1); + $keyLength = min($kexHash->getLength(), max($encryptKeyLength, $decryptKeyLength)); + $max = $one->bitwise_leftShift(16 * $keyLength); // 2 * 8 * $keyLength + $max = $max->subtract($one); + + $x = $one->random($one, $max); + $e = $g->modPow($x, $prime); + + $eBytes = $e->toBytes(true); + } + $data = pack('CNa*', constant($clientKexInitMessage), strlen($eBytes), $eBytes); + + if (!$this->_send_binary_packet($data)) { + $this->bitmap = 0; + user_error('Connection closed by server'); + return false; + } + switch ($clientKexInitMessage) { + case 'NET_SSH2_MSG_KEX_ECDH_INIT': + $this->_updateLogHistory('NET_SSH2_MSG_KEXDH_INIT', 'NET_SSH2_MSG_KEX_ECDH_INIT'); + break; + case 'NET_SSH2_MSG_KEXDH_GEX_INIT': + $this->_updateLogHistory('UNKNOWN (32)', 'NET_SSH2_MSG_KEXDH_GEX_INIT'); + } + + $response = $this->_get_binary_packet(); + if ($response === false) { + $this->bitmap = 0; + user_error('Connection closed by server'); + return false; + } + if (!strlen($response)) { + return false; + } + extract(unpack('Ctype', $this->_string_shift($response, 1))); + + if ($type != constant($serverKexReplyMessage)) { + user_error("Expected $serverKexReplyMessage"); + return false; + } + switch ($serverKexReplyMessage) { + case 'NET_SSH2_MSG_KEX_ECDH_REPLY': + $this->_updateLogHistory('NET_SSH2_MSG_KEXDH_REPLY', 'NET_SSH2_MSG_KEX_ECDH_REPLY'); + break; + case 'NET_SSH2_MSG_KEXDH_GEX_REPLY': + $this->_updateLogHistory('UNKNOWN (33)', 'NET_SSH2_MSG_KEXDH_GEX_REPLY'); + } + + if (strlen($response) < 4) { + return false; + } + $temp = unpack('Nlength', $this->_string_shift($response, 4)); + $this->server_public_host_key = $server_public_host_key = $this->_string_shift($response, $temp['length']); + + if (strlen($server_public_host_key) < 4) { + return false; + } + $temp = unpack('Nlength', $this->_string_shift($server_public_host_key, 4)); + $public_key_format = $this->_string_shift($server_public_host_key, $temp['length']); + + if (strlen($response) < 4) { + return false; + } + $temp = unpack('Nlength', $this->_string_shift($response, 4)); + $fBytes = $this->_string_shift($response, $temp['length']); + + if (strlen($response) < 4) { + return false; + } + $temp = unpack('Nlength', $this->_string_shift($response, 4)); + $this->signature = $this->_string_shift($response, $temp['length']); + + if (strlen($this->signature) < 4) { + return false; + } + $temp = unpack('Nlength', $this->_string_shift($this->signature, 4)); + $this->signature_format = $this->_string_shift($this->signature, $temp['length']); + + if ($kex_algorithm === 'curve25519-sha256@libssh.org') { + if (strlen($fBytes) !== 32) { + user_error('Received curve25519 public key of invalid length.'); + return false; + } + $key = new BigInteger(sodium_crypto_scalarmult($x, $fBytes), 256); + // sodium_compat doesn't emulate sodium_memzero + // also, with v1 of libsodium API the extension identifies itself as + // libsodium whereas v2 of the libsodium API (what PHP 7.2+ includes) + // identifies itself as sodium. sodium_compat uses the v1 API to + // emulate the v2 API if it's the v1 API that's available + if (extension_loaded('sodium') || extension_loaded('libsodium')) { + sodium_memzero($x); + } + } else { + $f = new BigInteger($fBytes, -256); + $key = $f->modPow($x, $prime); + } + $keyBytes = $key->toBytes(true); + + $this->exchange_hash = pack( + 'Na*Na*Na*Na*Na*a*Na*Na*Na*', + strlen($this->identifier), + $this->identifier, + strlen($this->server_identifier), + $this->server_identifier, + strlen($kexinit_payload_client), + $kexinit_payload_client, + strlen($kexinit_payload_server), + $kexinit_payload_server, + strlen($this->server_public_host_key), + $this->server_public_host_key, + $exchange_hash_rfc4419, + strlen($eBytes), + $eBytes, + strlen($fBytes), + $fBytes, + strlen($keyBytes), + $keyBytes + ); + + $this->exchange_hash = $kexHash->hash($this->exchange_hash); + + if ($this->session_id === false) { + $this->session_id = $this->exchange_hash; + } + + switch ($server_host_key_algorithm) { + case 'ssh-dss': + $expected_key_format = 'ssh-dss'; + break; + //case 'rsa-sha2-256': + //case 'rsa-sha2-512': + //case 'ssh-rsa': + default: + $expected_key_format = 'ssh-rsa'; + } + + if ($public_key_format != $expected_key_format || $this->signature_format != $server_host_key_algorithm) { + switch (true) { + case $this->signature_format == $server_host_key_algorithm: + case $server_host_key_algorithm != 'rsa-sha2-256' && $server_host_key_algorithm != 'rsa-sha2-512': + case $this->signature_format != 'ssh-rsa': + user_error('Server Host Key Algorithm Mismatch'); + return $this->_disconnect(NET_SSH2_DISCONNECT_KEY_EXCHANGE_FAILED); + } + } + + $packet = pack( + 'C', + NET_SSH2_MSG_NEWKEYS + ); + + if (!$this->_send_binary_packet($packet)) { + return false; + } + + $response = $this->_get_binary_packet(); + + if ($response === false) { + $this->bitmap = 0; + user_error('Connection closed by server'); + return false; + } + + if (!strlen($response)) { + return false; + } + extract(unpack('Ctype', $this->_string_shift($response, 1))); + + if ($type != NET_SSH2_MSG_NEWKEYS) { + user_error('Expected SSH_MSG_NEWKEYS'); + return false; + } + + if (in_array('kex-strict-s-v00@openssh.com', $this->kex_algorithms)) { + $this->get_seq_no = $this->send_seq_no = 0; + } + + $keyBytes = pack('Na*', strlen($keyBytes), $keyBytes); + + $this->encrypt = $this->_encryption_algorithm_to_crypt_instance($encrypt); + if ($this->encrypt) { + if ($this->crypto_engine) { + $this->encrypt->setPreferredEngine($this->crypto_engine); + } + if ($this->encrypt->block_size) { + $this->encrypt_block_size = $this->encrypt->block_size; + } + $this->encrypt->enableContinuousBuffer(); + $this->encrypt->disablePadding(); + + if ($this->encrypt->getBlockLength()) { + $this->encrypt_block_size = $this->encrypt->getBlockLength() >> 3; + } + + $iv = $kexHash->hash($keyBytes . $this->exchange_hash . 'A' . $this->session_id); + while ($this->encrypt_block_size > strlen($iv)) { + $iv.= $kexHash->hash($keyBytes . $this->exchange_hash . $iv); + } + $this->encrypt->setIV(substr($iv, 0, $this->encrypt_block_size)); + + $key = $kexHash->hash($keyBytes . $this->exchange_hash . 'C' . $this->session_id); + while ($encryptKeyLength > strlen($key)) { + $key.= $kexHash->hash($keyBytes . $this->exchange_hash . $key); + } + $this->encrypt->setKey(substr($key, 0, $encryptKeyLength)); + + $this->encryptName = $encrypt; + } + + $this->decrypt = $this->_encryption_algorithm_to_crypt_instance($decrypt); + if ($this->decrypt) { + if ($this->crypto_engine) { + $this->decrypt->setPreferredEngine($this->crypto_engine); + } + if ($this->decrypt->block_size) { + $this->decrypt_block_size = $this->decrypt->block_size; + } + $this->decrypt->enableContinuousBuffer(); + $this->decrypt->disablePadding(); + + if ($this->decrypt->getBlockLength()) { + $this->decrypt_block_size = $this->decrypt->getBlockLength() >> 3; + } + + $iv = $kexHash->hash($keyBytes . $this->exchange_hash . 'B' . $this->session_id); + while ($this->decrypt_block_size > strlen($iv)) { + $iv.= $kexHash->hash($keyBytes . $this->exchange_hash . $iv); + } + $this->decrypt->setIV(substr($iv, 0, $this->decrypt_block_size)); + + $key = $kexHash->hash($keyBytes . $this->exchange_hash . 'D' . $this->session_id); + while ($decryptKeyLength > strlen($key)) { + $key.= $kexHash->hash($keyBytes . $this->exchange_hash . $key); + } + $this->decrypt->setKey(substr($key, 0, $decryptKeyLength)); + + $this->decryptName = $decrypt; + } + + /* The "arcfour128" algorithm is the RC4 cipher, as described in + [SCHNEIER], using a 128-bit key. The first 1536 bytes of keystream + generated by the cipher MUST be discarded, and the first byte of the + first encrypted packet MUST be encrypted using the 1537th byte of + keystream. + + -- http://tools.ietf.org/html/rfc4345#section-4 */ + if ($encrypt == 'arcfour128' || $encrypt == 'arcfour256') { + $this->encrypt->encrypt(str_repeat("\0", 1536)); + } + if ($decrypt == 'arcfour128' || $decrypt == 'arcfour256') { + $this->decrypt->decrypt(str_repeat("\0", 1536)); + } + + $mac_algorithm_out = $this->_array_intersect_first($c2s_mac_algorithms, $this->mac_algorithms_client_to_server); + if ($mac_algorithm_out === false) { + user_error('No compatible client to server message authentication algorithms found'); + return $this->_disconnect(NET_SSH2_DISCONNECT_KEY_EXCHANGE_FAILED); + } + + $createKeyLength = 0; // ie. $mac_algorithm == 'none' + switch ($mac_algorithm_out) { + case 'hmac-sha2-256': + $this->hmac_create = new Hash('sha256'); + $createKeyLength = 32; + break; + case 'hmac-sha1': + $this->hmac_create = new Hash('sha1'); + $createKeyLength = 20; + break; + case 'hmac-sha1-96': + $this->hmac_create = new Hash('sha1-96'); + $createKeyLength = 20; + break; + case 'hmac-md5': + $this->hmac_create = new Hash('md5'); + $createKeyLength = 16; + break; + case 'hmac-md5-96': + $this->hmac_create = new Hash('md5-96'); + $createKeyLength = 16; + } + $this->hmac_create_name = $mac_algorithm_out; + + $checkKeyLength = 0; + $this->hmac_size = 0; + switch ($mac_algorithm_in) { + case 'hmac-sha2-256': + $this->hmac_check = new Hash('sha256'); + $checkKeyLength = 32; + $this->hmac_size = 32; + break; + case 'hmac-sha1': + $this->hmac_check = new Hash('sha1'); + $checkKeyLength = 20; + $this->hmac_size = 20; + break; + case 'hmac-sha1-96': + $this->hmac_check = new Hash('sha1-96'); + $checkKeyLength = 20; + $this->hmac_size = 12; + break; + case 'hmac-md5': + $this->hmac_check = new Hash('md5'); + $checkKeyLength = 16; + $this->hmac_size = 16; + break; + case 'hmac-md5-96': + $this->hmac_check = new Hash('md5-96'); + $checkKeyLength = 16; + $this->hmac_size = 12; + } + $this->hmac_check_name = $mac_algorithm_in; + + $key = $kexHash->hash($keyBytes . $this->exchange_hash . 'E' . $this->session_id); + while ($createKeyLength > strlen($key)) { + $key.= $kexHash->hash($keyBytes . $this->exchange_hash . $key); + } + $this->hmac_create->setKey(substr($key, 0, $createKeyLength)); + + $key = $kexHash->hash($keyBytes . $this->exchange_hash . 'F' . $this->session_id); + while ($checkKeyLength > strlen($key)) { + $key.= $kexHash->hash($keyBytes . $this->exchange_hash . $key); + } + $this->hmac_check->setKey(substr($key, 0, $checkKeyLength)); + + $this->regenerate_compression_context = $this->regenerate_decompression_context = true; + + return true; + } + + /** + * Maps an encryption algorithm name to the number of key bytes. + * + * @param string $algorithm Name of the encryption algorithm + * @return int|null Number of bytes as an integer or null for unknown + * @access private + */ + function _encryption_algorithm_to_key_size($algorithm) + { + if ($this->bad_key_size_fix && $this->_bad_algorithm_candidate($algorithm)) { + return 16; + } + + switch ($algorithm) { + case 'none': + return 0; + case 'aes128-cbc': + case 'aes128-ctr': + case 'arcfour': + case 'arcfour128': + case 'blowfish-cbc': + case 'blowfish-ctr': + case 'twofish128-cbc': + case 'twofish128-ctr': + return 16; + case '3des-cbc': + case '3des-ctr': + case 'aes192-cbc': + case 'aes192-ctr': + case 'twofish192-cbc': + case 'twofish192-ctr': + return 24; + case 'aes256-cbc': + case 'aes256-ctr': + case 'arcfour256': + case 'twofish-cbc': + case 'twofish256-cbc': + case 'twofish256-ctr': + return 32; + } + return null; + } + + /** + * Maps an encryption algorithm name to an instance of a subclass of + * \phpseclib\Crypt\Base. + * + * @param string $algorithm Name of the encryption algorithm + * @return mixed Instance of \phpseclib\Crypt\Base or null for unknown + * @access private + */ + function _encryption_algorithm_to_crypt_instance($algorithm) + { + switch ($algorithm) { + case '3des-cbc': + return new TripleDES(); + case '3des-ctr': + return new TripleDES(Base::MODE_CTR); + case 'aes256-cbc': + case 'aes192-cbc': + case 'aes128-cbc': + return new Rijndael(); + case 'aes256-ctr': + case 'aes192-ctr': + case 'aes128-ctr': + return new Rijndael(Base::MODE_CTR); + case 'blowfish-cbc': + return new Blowfish(); + case 'blowfish-ctr': + return new Blowfish(Base::MODE_CTR); + case 'twofish128-cbc': + case 'twofish192-cbc': + case 'twofish256-cbc': + case 'twofish-cbc': + return new Twofish(); + case 'twofish128-ctr': + case 'twofish192-ctr': + case 'twofish256-ctr': + return new Twofish(Base::MODE_CTR); + case 'arcfour': + case 'arcfour128': + case 'arcfour256': + return new RC4(); + } + return null; + } + + /** + * Tests whether or not proposed algorithm has a potential for issues + * + * @link https://www.chiark.greenend.org.uk/~sgtatham/putty/wishlist/ssh2-aesctr-openssh.html + * @link https://bugzilla.mindrot.org/show_bug.cgi?id=1291 + * @param string $algorithm Name of the encryption algorithm + * @return bool + * @access private + */ + function _bad_algorithm_candidate($algorithm) + { + switch ($algorithm) { + case 'arcfour256': + case 'aes192-ctr': + case 'aes256-ctr': + return true; + } + + return false; + } + + /** + * Login + * + * The $password parameter can be a plaintext password, a \phpseclib\Crypt\RSA object or an array + * + * @param string $username + * @return bool + * @see self::_login() + * @access public + */ + function login($username) + { + $args = func_get_args(); + if (!$this->retry_connect) { + $this->auth[] = $args; + } + + // try logging with 'none' as an authentication method first since that's what + // PuTTY does + if (substr($this->server_identifier, 0, 15) != 'SSH-2.0-CoreFTP' && $this->auth_methods_to_continue === null) { + if ($this->_login($username)) { + return true; + } + if (count($args) == 1) { + return false; + } + } + return call_user_func_array(array(&$this, '_login'), $args); + } + + /** + * Login Helper + * + * @param string $username + * @return bool + * @see self::_login_helper() + * @access private + */ + function _login($username) + { + if (!($this->bitmap & self::MASK_CONSTRUCTOR)) { + if (!$this->_connect()) { + return false; + } + } + + $args = array_slice(func_get_args(), 1); + if (empty($args)) { + return $this->_login_helper($username); + } + + while (count($args)) { + if (!$this->auth_methods_to_continue || !$this->smartMFA) { + $newargs = $args; + $args = array(); + } else { + $newargs = array(); + foreach ($this->auth_methods_to_continue as $method) { + switch ($method) { + case 'publickey': + foreach ($args as $key => $arg) { + if (is_object($arg)) { + $newargs[] = $arg; + unset($args[$key]); + break; + } + } + break; + case 'keyboard-interactive': + $hasArray = $hasString = false; + foreach ($args as $arg) { + if ($hasArray || is_array($arg)) { + $hasArray = true; + break; + } + if ($hasString || is_string($arg)) { + $hasString = true; + break; + } + } + if ($hasArray && $hasString) { + foreach ($args as $key => $arg) { + if (is_array($arg)) { + $newargs[] = $arg; + break 2; + } + } + } + case 'password': + foreach ($args as $key => $arg) { + $newargs[] = $arg; + unset($args[$key]); + break; + } + } + } + } + + if (!count($newargs)) { + return false; + } + + foreach ($newargs as $arg) { + if ($this->_login_helper($username, $arg)) { + return true; + } + } + } + return false; + } + + /** + * Login Helper + * + * @param string $username + * @param string $password + * @return bool + * @access private + * @internal It might be worthwhile, at some point, to protect against {@link http://tools.ietf.org/html/rfc4251#section-9.3.9 traffic analysis} + * by sending dummy SSH_MSG_IGNORE messages. + */ + function _login_helper($username, $password = null) + { + if (!($this->bitmap & self::MASK_CONNECTED)) { + return false; + } + + if (!($this->bitmap & self::MASK_LOGIN_REQ)) { + $packet = pack( + 'CNa*', + NET_SSH2_MSG_SERVICE_REQUEST, + strlen('ssh-userauth'), + 'ssh-userauth' + ); + + if (!$this->_send_binary_packet($packet)) { + return false; + } + + $response = $this->_get_binary_packet(); + if ($response === false) { + if ($this->retry_connect) { + $this->retry_connect = false; + if (!$this->_connect()) { + return false; + } + return $this->_login_helper($username, $password); + } + $this->bitmap = 0; + user_error('Connection closed by server'); + return false; + } + + if (strlen($response) < 4) { + return false; + } + extract(unpack('Ctype', $this->_string_shift($response, 1))); + + if ($type == NET_SSH2_MSG_EXT_INFO) { + if (strlen($response) < 4) { + return false; + } + $nr_extensions = unpack('Nlength', $this->_string_shift($response, 4)); + for ($i = 0; $i < $nr_extensions['length']; $i++) { + if (strlen($response) < 4) { + return false; + } + $temp = unpack('Nlength', $this->_string_shift($response, 4)); + $extension_name = $this->_string_shift($response, $temp['length']); + if ($extension_name == 'server-sig-algs') { + if (strlen($response) < 4) { + return false; + } + $temp = unpack('Nlength', $this->_string_shift($response, 4)); + $this->supported_private_key_algorithms = explode(',', $this->_string_shift($response, $temp['length'])); + } + } + + $response = $this->_get_binary_packet(); + if ($response === false) { + $this->bitmap = 0; + user_error('Connection closed by server'); + return false; + } + extract(unpack('Ctype', $this->_string_shift($response, 1))); + } + + if ($type != NET_SSH2_MSG_SERVICE_ACCEPT) { + user_error('Expected SSH_MSG_SERVICE_ACCEPT'); + return false; + } + $this->bitmap |= self::MASK_LOGIN_REQ; + } + + if (strlen($this->last_interactive_response)) { + return !is_string($password) && !is_array($password) ? false : $this->_keyboard_interactive_process($password); + } + + if ($password instanceof RSA) { + return $this->_privatekey_login($username, $password); + } elseif ($password instanceof Agent) { + return $this->_ssh_agent_login($username, $password); + } + + if (is_array($password)) { + if ($this->_keyboard_interactive_login($username, $password)) { + $this->bitmap |= self::MASK_LOGIN; + return true; + } + return false; + } + + if (!isset($password)) { + $packet = pack( + 'CNa*Na*Na*', + NET_SSH2_MSG_USERAUTH_REQUEST, + strlen($username), + $username, + strlen('ssh-connection'), + 'ssh-connection', + strlen('none'), + 'none' + ); + + if (!$this->_send_binary_packet($packet)) { + return false; + } + + $response = $this->_get_binary_packet(); + if ($response === false) { + $this->bitmap = 0; + user_error('Connection closed by server'); + return false; + } + + if (!strlen($response)) { + return false; + } + extract(unpack('Ctype', $this->_string_shift($response, 1))); + + switch ($type) { + case NET_SSH2_MSG_USERAUTH_SUCCESS: + $this->bitmap |= self::MASK_LOGIN; + return true; + case NET_SSH2_MSG_USERAUTH_FAILURE: + extract(unpack('Nmethodlistlen', $this->_string_shift($response, 4))); + $this->auth_methods_to_continue = explode(',', $this->_string_shift($response, $methodlistlen)); + default: + return false; + } + } + + $packet = pack( + 'CNa*Na*Na*CNa*', + NET_SSH2_MSG_USERAUTH_REQUEST, + strlen($username), + $username, + strlen('ssh-connection'), + 'ssh-connection', + strlen('password'), + 'password', + 0, + strlen($password), + $password + ); + + // remove the username and password from the logged packet + if (!defined('NET_SSH2_LOGGING')) { + $logged = null; + } else { + $logged = pack( + 'CNa*Na*Na*CNa*', + NET_SSH2_MSG_USERAUTH_REQUEST, + strlen('username'), + 'username', + strlen('ssh-connection'), + 'ssh-connection', + strlen('password'), + 'password', + 0, + strlen('password'), + 'password' + ); + } + + if (!$this->_send_binary_packet($packet, $logged)) { + return false; + } + + $response = $this->_get_binary_packet(); + if ($response === false) { + $this->bitmap = 0; + user_error('Connection closed by server'); + return false; + } + + if (!strlen($response)) { + return false; + } + extract(unpack('Ctype', $this->_string_shift($response, 1))); + + switch ($type) { + case NET_SSH2_MSG_USERAUTH_PASSWD_CHANGEREQ: // in theory, the password can be changed + $this->_updateLogHistory('UNKNOWN (60)', 'NET_SSH2_MSG_USERAUTH_PASSWD_CHANGEREQ'); + if (strlen($response) < 4) { + return false; + } + extract(unpack('Nlength', $this->_string_shift($response, 4))); + $this->errors[] = 'SSH_MSG_USERAUTH_PASSWD_CHANGEREQ: ' . $this->_string_shift($response, $length); + return $this->_disconnect(NET_SSH2_DISCONNECT_AUTH_CANCELLED_BY_USER); + case NET_SSH2_MSG_USERAUTH_FAILURE: + // can we use keyboard-interactive authentication? if not then either the login is bad or the server employees + // multi-factor authentication + if (strlen($response) < 4) { + return false; + } + extract(unpack('Nlength', $this->_string_shift($response, 4))); + $auth_methods = explode(',', $this->_string_shift($response, $length)); + $this->auth_methods_to_continue = $auth_methods; + if (!strlen($response)) { + return false; + } + extract(unpack('Cpartial_success', $this->_string_shift($response, 1))); + $partial_success = $partial_success != 0; + + if (!$partial_success && in_array('keyboard-interactive', $auth_methods)) { + if ($this->_keyboard_interactive_login($username, $password)) { + $this->bitmap |= self::MASK_LOGIN; + return true; + } + return false; + } + return false; + case NET_SSH2_MSG_USERAUTH_SUCCESS: + $this->bitmap |= self::MASK_LOGIN; + return true; + } + + return false; + } + + /** + * Login via keyboard-interactive authentication + * + * See {@link http://tools.ietf.org/html/rfc4256 RFC4256} for details. This is not a full-featured keyboard-interactive authenticator. + * + * @param string $username + * @param string $password + * @return bool + * @access private + */ + function _keyboard_interactive_login($username, $password) + { + $packet = pack( + 'CNa*Na*Na*Na*Na*', + NET_SSH2_MSG_USERAUTH_REQUEST, + strlen($username), + $username, + strlen('ssh-connection'), + 'ssh-connection', + strlen('keyboard-interactive'), + 'keyboard-interactive', + 0, + '', + 0, + '' + ); + + if (!$this->_send_binary_packet($packet)) { + return false; + } + + return $this->_keyboard_interactive_process($password); + } + + /** + * Handle the keyboard-interactive requests / responses. + * + * @return bool + * @access private + */ + function _keyboard_interactive_process() + { + $responses = func_get_args(); + + if (strlen($this->last_interactive_response)) { + $response = $this->last_interactive_response; + } else { + $orig = $response = $this->_get_binary_packet(); + if ($response === false) { + $this->bitmap = 0; + user_error('Connection closed by server'); + return false; + } + } + + if (!strlen($response)) { + return false; + } + extract(unpack('Ctype', $this->_string_shift($response, 1))); + + switch ($type) { + case NET_SSH2_MSG_USERAUTH_INFO_REQUEST: + if (strlen($response) < 4) { + return false; + } + extract(unpack('Nlength', $this->_string_shift($response, 4))); + $this->_string_shift($response, $length); // name; may be empty + if (strlen($response) < 4) { + return false; + } + extract(unpack('Nlength', $this->_string_shift($response, 4))); + $this->_string_shift($response, $length); // instruction; may be empty + if (strlen($response) < 4) { + return false; + } + extract(unpack('Nlength', $this->_string_shift($response, 4))); + $this->_string_shift($response, $length); // language tag; may be empty + if (strlen($response) < 4) { + return false; + } + extract(unpack('Nnum_prompts', $this->_string_shift($response, 4))); + + for ($i = 0; $i < count($responses); $i++) { + if (is_array($responses[$i])) { + foreach ($responses[$i] as $key => $value) { + $this->keyboard_requests_responses[$key] = $value; + } + unset($responses[$i]); + } + } + $responses = array_values($responses); + + if (isset($this->keyboard_requests_responses)) { + for ($i = 0; $i < $num_prompts; $i++) { + if (strlen($response) < 4) { + return false; + } + extract(unpack('Nlength', $this->_string_shift($response, 4))); + // prompt - ie. "Password: "; must not be empty + $prompt = $this->_string_shift($response, $length); + //$echo = $this->_string_shift($response) != chr(0); + foreach ($this->keyboard_requests_responses as $key => $value) { + if (substr($prompt, 0, strlen($key)) == $key) { + $responses[] = $value; + break; + } + } + } + } + + // see http://tools.ietf.org/html/rfc4256#section-3.2 + if (strlen($this->last_interactive_response)) { + $this->last_interactive_response = ''; + } else { + $this->_updateLogHistory('UNKNOWN (60)', 'NET_SSH2_MSG_USERAUTH_INFO_REQUEST'); + } + + if (!count($responses) && $num_prompts) { + $this->last_interactive_response = $orig; + return false; + } + + /* + After obtaining the requested information from the user, the client + MUST respond with an SSH_MSG_USERAUTH_INFO_RESPONSE message. + */ + // see http://tools.ietf.org/html/rfc4256#section-3.4 + $packet = $logged = pack('CN', NET_SSH2_MSG_USERAUTH_INFO_RESPONSE, count($responses)); + for ($i = 0; $i < count($responses); $i++) { + $packet.= pack('Na*', strlen($responses[$i]), $responses[$i]); + $logged.= pack('Na*', strlen('dummy-answer'), 'dummy-answer'); + } + + if (!$this->_send_binary_packet($packet, $logged)) { + return false; + } + + $this->_updateLogHistory('UNKNOWN (61)', 'NET_SSH2_MSG_USERAUTH_INFO_RESPONSE'); + + /* + After receiving the response, the server MUST send either an + SSH_MSG_USERAUTH_SUCCESS, SSH_MSG_USERAUTH_FAILURE, or another + SSH_MSG_USERAUTH_INFO_REQUEST message. + */ + // maybe phpseclib should force close the connection after x request / responses? unless something like that is done + // there could be an infinite loop of request / responses. + return $this->_keyboard_interactive_process(); + case NET_SSH2_MSG_USERAUTH_SUCCESS: + return true; + case NET_SSH2_MSG_USERAUTH_FAILURE: + extract(unpack('Nmethodlistlen', $this->_string_shift($response, 4))); + $this->auth_methods_to_continue = explode(',', $this->_string_shift($response, $methodlistlen)); + return false; + } + + return false; + } + + /** + * Login with an ssh-agent provided key + * + * @param string $username + * @param \phpseclib\System\SSH\Agent $agent + * @return bool + * @access private + */ + function _ssh_agent_login($username, $agent) + { + $this->agent = $agent; + $keys = $agent->requestIdentities(); + foreach ($keys as $key) { + if ($this->_privatekey_login($username, $key)) { + return true; + } + } + + return false; + } + + /** + * Login with an RSA private key + * + * @param string $username + * @param \phpseclib\Crypt\RSA $privatekey + * @return bool + * @access private + * @internal It might be worthwhile, at some point, to protect against {@link http://tools.ietf.org/html/rfc4251#section-9.3.9 traffic analysis} + * by sending dummy SSH_MSG_IGNORE messages. + */ + function _privatekey_login($username, $privatekey) + { + // see http://tools.ietf.org/html/rfc4253#page-15 + $publickey = $privatekey->getPublicKey(RSA::PUBLIC_FORMAT_RAW); + if ($publickey === false) { + return false; + } + + $publickey = array( + 'e' => $publickey['e']->toBytes(true), + 'n' => $publickey['n']->toBytes(true) + ); + $publickey = pack( + 'Na*Na*Na*', + strlen('ssh-rsa'), + 'ssh-rsa', + strlen($publickey['e']), + $publickey['e'], + strlen($publickey['n']), + $publickey['n'] + ); + + $algos = array('rsa-sha2-256', 'rsa-sha2-512', 'ssh-rsa'); + if (isset($this->preferred['hostkey'])) { + $algos = array_intersect($algos, $this->preferred['hostkey']); + } + $algo = $this->_array_intersect_first($algos, $this->supported_private_key_algorithms); + + switch ($algo) { + case 'rsa-sha2-512': + $hash = 'sha512'; + $signatureType = 'rsa-sha2-512'; + break; + case 'rsa-sha2-256': + $hash = 'sha256'; + $signatureType = 'rsa-sha2-256'; + break; + //case 'ssh-rsa': + default: + $hash = 'sha1'; + $signatureType = 'ssh-rsa'; + } + + $part1 = pack( + 'CNa*Na*Na*', + NET_SSH2_MSG_USERAUTH_REQUEST, + strlen($username), + $username, + strlen('ssh-connection'), + 'ssh-connection', + strlen('publickey'), + 'publickey' + ); + $part2 = pack('Na*Na*', strlen($signatureType), $signatureType, strlen($publickey), $publickey); + + $packet = $part1 . chr(0) . $part2; + if (!$this->_send_binary_packet($packet)) { + return false; + } + + $response = $this->_get_binary_packet(); + if ($response === false) { + $this->bitmap = 0; + user_error('Connection closed by server'); + return false; + } + + if (!strlen($response)) { + return false; + } + extract(unpack('Ctype', $this->_string_shift($response, 1))); + + switch ($type) { + case NET_SSH2_MSG_USERAUTH_FAILURE: + if (strlen($response) < 4) { + return false; + } + extract(unpack('Nmethodlistlen', $this->_string_shift($response, 4))); + $auth_methods = explode(',', $this->_string_shift($response, $methodlistlen)); + if (in_array('publickey', $auth_methods) && substr($signatureType, 0, 9) == 'rsa-sha2-') { + $this->supported_private_key_algorithms = array_diff($this->supported_private_key_algorithms, array('rsa-sha2-256', 'rsa-sha2-512')); + return $this->_privatekey_login($username, $privatekey); + } + $this->auth_methods_to_continue = $auth_methods; + $this->errors[] = 'SSH_MSG_USERAUTH_FAILURE'; + return false; + case NET_SSH2_MSG_USERAUTH_PK_OK: + // we'll just take it on faith that the public key blob and the public key algorithm name are as + // they should be + $this->_updateLogHistory('UNKNOWN (60)', 'NET_SSH2_MSG_USERAUTH_PK_OK'); + break; + case NET_SSH2_MSG_USERAUTH_SUCCESS: + $this->bitmap |= self::MASK_LOGIN; + return true; + default: + user_error('Unexpected response to publickey authentication pt 1'); + return $this->_disconnect(NET_SSH2_DISCONNECT_BY_APPLICATION); + } + + $packet = $part1 . chr(1) . $part2; + $privatekey->setSignatureMode(RSA::SIGNATURE_PKCS1); + $privatekey->setHash($hash); + $signature = $privatekey->sign(pack('Na*a*', strlen($this->session_id), $this->session_id, $packet)); + $signature = pack('Na*Na*', strlen($signatureType), $signatureType, strlen($signature), $signature); + $packet.= pack('Na*', strlen($signature), $signature); + + if (!$this->_send_binary_packet($packet)) { + return false; + } + + $response = $this->_get_binary_packet(); + if ($response === false) { + $this->bitmap = 0; + user_error('Connection closed by server'); + return false; + } + + if (!strlen($response)) { + return false; + } + extract(unpack('Ctype', $this->_string_shift($response, 1))); + + switch ($type) { + case NET_SSH2_MSG_USERAUTH_FAILURE: + // either the login is bad or the server employs multi-factor authentication + extract(unpack('Nmethodlistlen', $this->_string_shift($response, 4))); + $this->auth_methods_to_continue = explode(',', $this->_string_shift($response, $methodlistlen)); + return false; + case NET_SSH2_MSG_USERAUTH_SUCCESS: + $this->bitmap |= self::MASK_LOGIN; + return true; + } + + user_error('Unexpected response to publickey authentication pt 2'); + return $this->_disconnect(NET_SSH2_DISCONNECT_BY_APPLICATION); + } + + /** + * Return the currently configured timeout + * + * @return int + */ + function getTimeout() + { + return $this->timeout; + } + + /** + * Set Timeout + * + * $ssh->exec('ping 127.0.0.1'); on a Linux host will never return and will run indefinitely. setTimeout() makes it so it'll timeout. + * Setting $timeout to false or 0 will mean there is no timeout. + * + * @param mixed $timeout + * @access public + */ + function setTimeout($timeout) + { + $this->timeout = $this->curTimeout = $timeout; + } + + /** + * Set Keep Alive + * + * Sends an SSH2_MSG_IGNORE message every x seconds, if x is a positive non-zero number. + * + * @param int $interval + * @access public + */ + function setKeepAlive($interval) + { + $this->keepAlive = $interval; + } + + /** + * Get the output from stdError + * + * @access public + */ + function getStdError() + { + return $this->stdErrorLog; + } + + /** + * Execute Command + * + * If $callback is set to false then \phpseclib\Net\SSH2::_get_channel_packet(self::CHANNEL_EXEC) will need to be called manually. + * In all likelihood, this is not a feature you want to be taking advantage of. + * + * @param string $command + * @param Callback $callback + * @return string + * @access public + */ + function exec($command, $callback = null) + { + $this->curTimeout = $this->timeout; + $this->is_timeout = false; + $this->stdErrorLog = ''; + + if (!$this->isAuthenticated()) { + return false; + } + + if ($this->in_request_pty_exec) { + user_error('If you want to run multiple exec()\'s you will need to disable (and re-enable if appropriate) a PTY for each one.'); + return false; + } + + // RFC4254 defines the (client) window size as "bytes the other party can send before it must wait for the window to + // be adjusted". 0x7FFFFFFF is, at 2GB, the max size. technically, it should probably be decremented, but, + // honestly, if you're transferring more than 2GB, you probably shouldn't be using phpseclib, anyway. + // see http://tools.ietf.org/html/rfc4254#section-5.2 for more info + $this->window_size_server_to_client[self::CHANNEL_EXEC] = $this->window_size; + // 0x8000 is the maximum max packet size, per http://tools.ietf.org/html/rfc4253#section-6.1, although since PuTTy + // uses 0x4000, that's what will be used here, as well. + $packet_size = 0x4000; + + $packet = pack( + 'CNa*N3', + NET_SSH2_MSG_CHANNEL_OPEN, + strlen('session'), + 'session', + self::CHANNEL_EXEC, + $this->window_size_server_to_client[self::CHANNEL_EXEC], + $packet_size + ); + + if (!$this->_send_binary_packet($packet)) { + return false; + } + + $this->channel_status[self::CHANNEL_EXEC] = NET_SSH2_MSG_CHANNEL_OPEN; + + $response = $this->_get_channel_packet(self::CHANNEL_EXEC); + if ($response === false) { + return false; + } + + if ($this->request_pty === true) { + $terminal_modes = pack('C', NET_SSH2_TTY_OP_END); + $packet = pack( + 'CNNa*CNa*N5a*', + NET_SSH2_MSG_CHANNEL_REQUEST, + $this->server_channels[self::CHANNEL_EXEC], + strlen('pty-req'), + 'pty-req', + 1, + strlen('vt100'), + 'vt100', + $this->windowColumns, + $this->windowRows, + 0, + 0, + strlen($terminal_modes), + $terminal_modes + ); + + if (!$this->_send_binary_packet($packet)) { + return false; + } + + $this->channel_status[self::CHANNEL_EXEC] = NET_SSH2_MSG_CHANNEL_REQUEST; + if (!$this->_get_channel_packet(self::CHANNEL_EXEC)) { + user_error('Unable to request pseudo-terminal'); + return $this->_disconnect(NET_SSH2_DISCONNECT_BY_APPLICATION); + } + + $this->in_request_pty_exec = true; + } + + // sending a pty-req SSH_MSG_CHANNEL_REQUEST message is unnecessary and, in fact, in most cases, slows things + // down. the one place where it might be desirable is if you're doing something like \phpseclib\Net\SSH2::exec('ping localhost &'). + // with a pty-req SSH_MSG_CHANNEL_REQUEST, exec() will return immediately and the ping process will then + // then immediately terminate. without such a request exec() will loop indefinitely. the ping process won't end but + // neither will your script. + + // although, in theory, the size of SSH_MSG_CHANNEL_REQUEST could exceed the maximum packet size established by + // SSH_MSG_CHANNEL_OPEN_CONFIRMATION, RFC4254#section-5.1 states that the "maximum packet size" refers to the + // "maximum size of an individual data packet". ie. SSH_MSG_CHANNEL_DATA. RFC4254#section-5.2 corroborates. + $packet = pack( + 'CNNa*CNa*', + NET_SSH2_MSG_CHANNEL_REQUEST, + $this->server_channels[self::CHANNEL_EXEC], + strlen('exec'), + 'exec', + 1, + strlen($command), + $command + ); + if (!$this->_send_binary_packet($packet)) { + return false; + } + + $this->channel_status[self::CHANNEL_EXEC] = NET_SSH2_MSG_CHANNEL_REQUEST; + + $response = $this->_get_channel_packet(self::CHANNEL_EXEC); + if ($response === false) { + return false; + } + + $this->channel_status[self::CHANNEL_EXEC] = NET_SSH2_MSG_CHANNEL_DATA; + + if ($callback === false || $this->in_request_pty_exec) { + return true; + } + + $output = ''; + while (true) { + $temp = $this->_get_channel_packet(self::CHANNEL_EXEC); + switch (true) { + case $temp === true: + return is_callable($callback) ? true : $output; + case $temp === false: + return false; + default: + if (is_callable($callback)) { + if (call_user_func($callback, $temp) === true) { + $this->_close_channel(self::CHANNEL_EXEC); + return true; + } + } else { + $output.= $temp; + } + } + } + } + + /** + * Creates an interactive shell + * + * @see self::read() + * @see self::write() + * @return bool + * @access private + */ + function _initShell() + { + if ($this->in_request_pty_exec === true) { + return true; + } + + $this->window_size_server_to_client[self::CHANNEL_SHELL] = $this->window_size; + $packet_size = 0x4000; + + $packet = pack( + 'CNa*N3', + NET_SSH2_MSG_CHANNEL_OPEN, + strlen('session'), + 'session', + self::CHANNEL_SHELL, + $this->window_size_server_to_client[self::CHANNEL_SHELL], + $packet_size + ); + + if (!$this->_send_binary_packet($packet)) { + return false; + } + + $this->channel_status[self::CHANNEL_SHELL] = NET_SSH2_MSG_CHANNEL_OPEN; + + $response = $this->_get_channel_packet(self::CHANNEL_SHELL); + if ($response === false) { + return false; + } + + $terminal_modes = pack('C', NET_SSH2_TTY_OP_END); + $packet = pack( + 'CNNa*CNa*N5a*', + NET_SSH2_MSG_CHANNEL_REQUEST, + $this->server_channels[self::CHANNEL_SHELL], + strlen('pty-req'), + 'pty-req', + 1, + strlen('vt100'), + 'vt100', + $this->windowColumns, + $this->windowRows, + 0, + 0, + strlen($terminal_modes), + $terminal_modes + ); + + if (!$this->_send_binary_packet($packet)) { + return false; + } + + $this->channel_status[self::CHANNEL_SHELL] = NET_SSH2_MSG_CHANNEL_REQUEST; + + if (!$this->_get_channel_packet(self::CHANNEL_SHELL)) { + user_error('Unable to request pseudo-terminal'); + return $this->_disconnect(NET_SSH2_DISCONNECT_BY_APPLICATION); + } + + $packet = pack( + 'CNNa*C', + NET_SSH2_MSG_CHANNEL_REQUEST, + $this->server_channels[self::CHANNEL_SHELL], + strlen('shell'), + 'shell', + 1 + ); + if (!$this->_send_binary_packet($packet)) { + return false; + } + + $response = $this->_get_channel_packet(self::CHANNEL_SHELL); + if ($response === false) { + return false; + } + + $this->channel_status[self::CHANNEL_SHELL] = NET_SSH2_MSG_CHANNEL_DATA; + + $this->bitmap |= self::MASK_SHELL; + + return true; + } + + /** + * Return the channel to be used with read() / write() + * + * @see self::read() + * @see self::write() + * @return int + * @access public + */ + function _get_interactive_channel() + { + switch (true) { + case $this->in_subsystem: + return self::CHANNEL_SUBSYSTEM; + case $this->in_request_pty_exec: + return self::CHANNEL_EXEC; + default: + return self::CHANNEL_SHELL; + } + } + + /** + * Return an available open channel + * + * @return int + * @access public + */ + function _get_open_channel() + { + $channel = self::CHANNEL_EXEC; + do { + if (isset($this->channel_status[$channel]) && $this->channel_status[$channel] == NET_SSH2_MSG_CHANNEL_OPEN) { + return $channel; + } + } while ($channel++ < self::CHANNEL_SUBSYSTEM); + + return false; + } + + /** + * Returns the output of an interactive shell + * + * Returns when there's a match for $expect, which can take the form of a string literal or, + * if $mode == self::READ_REGEX, a regular expression. + * + * @see self::write() + * @param string $expect + * @param int $mode + * @return string|bool + * @access public + */ + function read($expect = '', $mode = self::READ_SIMPLE) + { + $this->curTimeout = $this->timeout; + $this->is_timeout = false; + + if (!$this->isAuthenticated()) { + user_error('Operation disallowed prior to login()'); + return false; + } + + if (!($this->bitmap & self::MASK_SHELL) && !$this->_initShell()) { + user_error('Unable to initiate an interactive shell session'); + return false; + } + + $channel = $this->_get_interactive_channel(); + + if ($mode == self::READ_NEXT) { + return $this->_get_channel_packet($channel); + } + + $match = $expect; + while (true) { + if ($mode == self::READ_REGEX) { + preg_match($expect, substr($this->interactiveBuffer, -1024), $matches); + $match = isset($matches[0]) ? $matches[0] : ''; + } + $pos = strlen($match) ? strpos($this->interactiveBuffer, $match) : false; + if ($pos !== false) { + return $this->_string_shift($this->interactiveBuffer, $pos + strlen($match)); + } + $response = $this->_get_channel_packet($channel); + if (is_bool($response)) { + $this->in_request_pty_exec = false; + return $response ? $this->_string_shift($this->interactiveBuffer, strlen($this->interactiveBuffer)) : false; + } + + $this->interactiveBuffer.= $response; + } + } + + /** + * Inputs a command into an interactive shell. + * + * @see self::read() + * @param string $cmd + * @return bool + * @access public + */ + function write($cmd) + { + if (!$this->isAuthenticated()) { + user_error('Operation disallowed prior to login()'); + return false; + } + + if (!($this->bitmap & self::MASK_SHELL) && !$this->_initShell()) { + user_error('Unable to initiate an interactive shell session'); + return false; + } + + return $this->_send_channel_packet($this->_get_interactive_channel(), $cmd); + } + + /** + * Start a subsystem. + * + * Right now only one subsystem at a time is supported. To support multiple subsystem's stopSubsystem() could accept + * a string that contained the name of the subsystem, but at that point, only one subsystem of each type could be opened. + * To support multiple subsystem's of the same name maybe it'd be best if startSubsystem() generated a new channel id and + * returns that and then that that was passed into stopSubsystem() but that'll be saved for a future date and implemented + * if there's sufficient demand for such a feature. + * + * @see self::stopSubsystem() + * @param string $subsystem + * @return bool + * @access public + */ + function startSubsystem($subsystem) + { + $this->window_size_server_to_client[self::CHANNEL_SUBSYSTEM] = $this->window_size; + + $packet = pack( + 'CNa*N3', + NET_SSH2_MSG_CHANNEL_OPEN, + strlen('session'), + 'session', + self::CHANNEL_SUBSYSTEM, + $this->window_size, + 0x4000 + ); + + if (!$this->_send_binary_packet($packet)) { + return false; + } + + $this->channel_status[self::CHANNEL_SUBSYSTEM] = NET_SSH2_MSG_CHANNEL_OPEN; + + $response = $this->_get_channel_packet(self::CHANNEL_SUBSYSTEM); + if ($response === false) { + return false; + } + + $packet = pack( + 'CNNa*CNa*', + NET_SSH2_MSG_CHANNEL_REQUEST, + $this->server_channels[self::CHANNEL_SUBSYSTEM], + strlen('subsystem'), + 'subsystem', + 1, + strlen($subsystem), + $subsystem + ); + if (!$this->_send_binary_packet($packet)) { + return false; + } + + $this->channel_status[self::CHANNEL_SUBSYSTEM] = NET_SSH2_MSG_CHANNEL_REQUEST; + + $response = $this->_get_channel_packet(self::CHANNEL_SUBSYSTEM); + + if ($response === false) { + return false; + } + + $this->channel_status[self::CHANNEL_SUBSYSTEM] = NET_SSH2_MSG_CHANNEL_DATA; + + $this->bitmap |= self::MASK_SHELL; + $this->in_subsystem = true; + + return true; + } + + /** + * Stops a subsystem. + * + * @see self::startSubsystem() + * @return bool + * @access public + */ + function stopSubsystem() + { + $this->in_subsystem = false; + $this->_close_channel(self::CHANNEL_SUBSYSTEM); + return true; + } + + /** + * Closes a channel + * + * If read() timed out you might want to just close the channel and have it auto-restart on the next read() call + * + * @access public + */ + function reset() + { + $this->_close_channel($this->_get_interactive_channel()); + } + + /** + * Is timeout? + * + * Did exec() or read() return because they timed out or because they encountered the end? + * + * @access public + */ + function isTimeout() + { + return $this->is_timeout; + } + + /** + * Disconnect + * + * @access public + */ + function disconnect() + { + $this->_disconnect(NET_SSH2_DISCONNECT_BY_APPLICATION); + if (isset($this->realtime_log_file) && is_resource($this->realtime_log_file)) { + fclose($this->realtime_log_file); + } + } + + /** + * Destructor. + * + * Will be called, automatically, if you're supporting just PHP5. If you're supporting PHP4, you'll need to call + * disconnect(). + * + * @access public + */ + function __destruct() + { + $this->disconnect(); + } + + /** + * Is the connection still active? + * + * @return bool + * @access public + */ + function isConnected() + { + return ($this->bitmap & self::MASK_CONNECTED) && is_resource($this->fsock) && !feof($this->fsock); + } + + /** + * Have you successfully been logged in? + * + * @return bool + * @access public + */ + function isAuthenticated() + { + return (bool) ($this->bitmap & self::MASK_LOGIN); + } + + /** + * Pings a server connection, or tries to reconnect if the connection has gone down + * + * Inspired by http://php.net/manual/en/mysqli.ping.php + * + * @return bool + * @access public + */ + function ping() + { + if (!$this->isAuthenticated()) { + if (!empty($this->auth)) { + return $this->_reconnect(); + } + return false; + } + + $this->window_size_server_to_client[self::CHANNEL_KEEP_ALIVE] = $this->window_size; + $packet_size = 0x4000; + $packet = pack( + 'CNa*N3', + NET_SSH2_MSG_CHANNEL_OPEN, + strlen('session'), + 'session', + self::CHANNEL_KEEP_ALIVE, + $this->window_size_server_to_client[self::CHANNEL_KEEP_ALIVE], + $packet_size + ); + + if (!@$this->_send_binary_packet($packet)) { + return $this->_reconnect(); + } + + $this->channel_status[self::CHANNEL_KEEP_ALIVE] = NET_SSH2_MSG_CHANNEL_OPEN; + + $response = @$this->_get_channel_packet(self::CHANNEL_KEEP_ALIVE); + if ($response !== false) { + $this->_close_channel(self::CHANNEL_KEEP_ALIVE); + return true; + } + + return $this->_reconnect(); + } + + /** + * In situ reconnect method + * + * @return boolean + * @access private + */ + function _reconnect() + { + $this->_reset_connection(NET_SSH2_DISCONNECT_CONNECTION_LOST); + $this->retry_connect = true; + if (!$this->_connect()) { + return false; + } + foreach ($this->auth as $auth) { + $result = call_user_func_array(array(&$this, 'login'), $auth); + } + return $result; + } + + /** + * Resets a connection for re-use + * + * @param int $reason + * @access private + */ + function _reset_connection($reason) + { + $this->_disconnect($reason); + $this->decrypt = $this->encrypt = false; + $this->decrypt_block_size = $this->encrypt_block_size = 8; + $this->hmac_check = $this->hmac_create = false; + $this->hmac_size = false; + $this->session_id = false; + $this->retry_connect = true; + $this->get_seq_no = $this->send_seq_no = 0; + } + + /** + * Gets Binary Packets + * + * See '6. Binary Packet Protocol' of rfc4253 for more info. + * + * @see self::_send_binary_packet() + * @return string + * @access private + */ + function _get_binary_packet($skip_channel_filter = false) + { + if ($skip_channel_filter) { + $read = array($this->fsock); + $write = $except = null; + + if (!$this->curTimeout) { + if ($this->keepAlive <= 0) { + @stream_select($read, $write, $except, null); + } else { + if (!@stream_select($read, $write, $except, $this->keepAlive) && !count($read)) { + $this->_send_binary_packet(pack('CN', NET_SSH2_MSG_IGNORE, 0)); + return $this->_get_binary_packet(true); + } + } + } else { + if ($this->curTimeout < 0) { + $this->is_timeout = true; + return true; + } + + $read = array($this->fsock); + $write = $except = null; + + $start = microtime(true); + + if ($this->keepAlive > 0 && $this->keepAlive < $this->curTimeout) { + if (!@stream_select($read, $write, $except, $this->keepAlive) && !count($read)) { + $this->_send_binary_packet(pack('CN', NET_SSH2_MSG_IGNORE, 0)); + $elapsed = microtime(true) - $start; + $this->curTimeout-= $elapsed; + return $this->_get_binary_packet(true); + } + $elapsed = microtime(true) - $start; + $this->curTimeout-= $elapsed; + } + + $sec = (int)floor($this->curTimeout); + $usec = (int)(1000000 * ($this->curTimeout - $sec)); + + // on windows this returns a "Warning: Invalid CRT parameters detected" error + if (!@stream_select($read, $write, $except, $sec, $usec) && !count($read)) { + $this->is_timeout = true; + return true; + } + $elapsed = microtime(true) - $start; + $this->curTimeout-= $elapsed; + } + } + + if (!is_resource($this->fsock) || feof($this->fsock)) { + $this->bitmap = 0; + $str = 'Connection closed (by server) prematurely'; + if (isset($elapsed)) { + $str.= ' ' . $elapsed . 's'; + } + user_error($str); + return false; + } + + $start = microtime(true); + $sec = (int) floor($this->curTimeout); + $usec = (int) (1000000 * ($this->curTimeout - $sec)); + stream_set_timeout($this->fsock, $sec, $usec); + $raw = stream_get_contents($this->fsock, $this->decrypt_block_size); + + if (!strlen($raw)) { + user_error('No data received from server'); + return false; + } + + if ($this->decrypt !== false) { + $raw = $this->decrypt->decrypt($raw); + } + if ($raw === false) { + user_error('Unable to decrypt content'); + return false; + } + + if (strlen($raw) < 5) { + return false; + } + extract(unpack('Npacket_length/Cpadding_length', $this->_string_shift($raw, 5))); + + $remaining_length = $packet_length + 4 - $this->decrypt_block_size; + + // quoting , + // "implementations SHOULD check that the packet length is reasonable" + // PuTTY uses 0x9000 as the actual max packet size and so to shall we + if ($remaining_length < -$this->decrypt_block_size || $remaining_length > 0x9000 || $remaining_length % $this->decrypt_block_size != 0) { + if (!$this->bad_key_size_fix && $this->_bad_algorithm_candidate($this->decryptName) && !($this->bitmap & SSH2::MASK_LOGIN)) { + $this->bad_key_size_fix = true; + $this->_reset_connection(NET_SSH2_DISCONNECT_KEY_EXCHANGE_FAILED); + return false; + } + user_error('Invalid size'); + return false; + } + + $buffer = ''; + while ($remaining_length > 0) { + $temp = stream_get_contents($this->fsock, $remaining_length); + if ($temp === false || feof($this->fsock)) { + $this->bitmap = 0; + user_error('Error reading from socket'); + return false; + } + $buffer.= $temp; + $remaining_length-= strlen($temp); + } + + $stop = microtime(true); + if (strlen($buffer)) { + $raw.= $this->decrypt !== false ? $this->decrypt->decrypt($buffer) : $buffer; + } + + $payload = $this->_string_shift($raw, $packet_length - $padding_length - 1); + $padding = $this->_string_shift($raw, $padding_length); // should leave $raw empty + + if ($this->hmac_check !== false) { + $hmac = stream_get_contents($this->fsock, $this->hmac_size); + if ($hmac === false || strlen($hmac) != $this->hmac_size) { + $this->bitmap = 0; + user_error('Error reading socket'); + return false; + } elseif ($hmac != $this->hmac_check->hash(pack('NNCa*', $this->get_seq_no, $packet_length, $padding_length, $payload . $padding))) { + user_error('Invalid HMAC'); + return false; + } + } + + switch ($this->decompress) { + case self::NET_SSH2_COMPRESSION_ZLIB_AT_OPENSSH: + if (!$this->isAuthenticated()) { + break; + } + case self::NET_SSH2_COMPRESSION_ZLIB: + if ($this->regenerate_decompression_context) { + $this->regenerate_decompression_context = false; + + $cmf = ord($payload[0]); + $cm = $cmf & 0x0F; + if ($cm != 8) { // deflate + user_error("Only CM = 8 ('deflate') is supported ($cm)"); + } + $cinfo = ($cmf & 0xF0) >> 4; + if ($cinfo > 7) { + user_error("CINFO above 7 is not allowed ($cinfo)"); + } + $windowSize = 1 << ($cinfo + 8); + + $flg = ord($payload[1]); + //$fcheck = $flg && 0x0F; + if ((($cmf << 8) | $flg) % 31) { + user_error('fcheck failed'); + } + $fdict = boolval($flg & 0x20); + $flevel = ($flg & 0xC0) >> 6; + + $this->decompress_context = inflate_init(ZLIB_ENCODING_RAW, array('window' => $cinfo + 8)); + $payload = substr($payload, 2); + } + if ($this->decompress_context) { + $payload = inflate_add($this->decompress_context, $payload, ZLIB_PARTIAL_FLUSH); + } + } + + $this->get_seq_no++; + + if (defined('NET_SSH2_LOGGING')) { + $current = microtime(true); + $message_number = isset($this->message_numbers[ord($payload[0])]) ? $this->message_numbers[ord($payload[0])] : 'UNKNOWN (' . ord($payload[0]) . ')'; + $message_number = '<- ' . $message_number . + ' (since last: ' . round($current - $this->last_packet, 4) . ', network: ' . round($stop - $start, 4) . 's)'; + $this->_append_log($message_number, $payload); + $this->last_packet = $current; + } + + return $this->_filter($payload, $skip_channel_filter); + } + + /** + * Filter Binary Packets + * + * Because some binary packets need to be ignored... + * + * @see self::_get_binary_packet() + * @return string + * @access private + */ + function _filter($payload, $skip_channel_filter) + { + switch (ord($payload[0])) { + case NET_SSH2_MSG_DISCONNECT: + $this->_string_shift($payload, 1); + if (strlen($payload) < 8) { + return false; + } + extract(unpack('Nreason_code/Nlength', $this->_string_shift($payload, 8))); + $this->errors[] = 'SSH_MSG_DISCONNECT: ' . $this->disconnect_reasons[$reason_code] . "\r\n" . $this->_string_shift($payload, $length); + $this->bitmap = 0; + return false; + case NET_SSH2_MSG_IGNORE: + $this->extra_packets++; + $payload = $this->_get_binary_packet($skip_channel_filter); + break; + case NET_SSH2_MSG_DEBUG: + $this->extra_packets++; + $this->_string_shift($payload, 2); + if (strlen($payload) < 4) { + return false; + } + extract(unpack('Nlength', $this->_string_shift($payload, 4))); + $this->errors[] = 'SSH_MSG_DEBUG: ' . $this->_string_shift($payload, $length); + $payload = $this->_get_binary_packet($skip_channel_filter); + break; + case NET_SSH2_MSG_UNIMPLEMENTED: + return false; + case NET_SSH2_MSG_KEXINIT: + // this is here for key re-exchanges after the initial key exchange + if ($this->session_id !== false) { + $this->send_kex_first = false; + if (!$this->_key_exchange($payload)) { + $this->bitmap = 0; + return false; + } + $payload = $this->_get_binary_packet($skip_channel_filter); + } + } + + // see http://tools.ietf.org/html/rfc4252#section-5.4; only called when the encryption has been activated and when we haven't already logged in + if (($this->bitmap & self::MASK_CONNECTED) && !$this->isAuthenticated() && ord($payload[0]) == NET_SSH2_MSG_USERAUTH_BANNER) { + $this->_string_shift($payload, 1); + if (strlen($payload) < 4) { + return false; + } + extract(unpack('Nlength', $this->_string_shift($payload, 4))); + $this->banner_message = $this->_string_shift($payload, $length); + $payload = $this->_get_binary_packet(); + } + + // only called when we've already logged in + if (($this->bitmap & self::MASK_CONNECTED) && $this->isAuthenticated()) { + if (is_bool($payload)) { + return $payload; + } + + switch (ord($payload[0])) { + case NET_SSH2_MSG_CHANNEL_REQUEST: + if (strlen($payload) == 31) { + extract(unpack('cpacket_type/Nchannel/Nlength', $payload)); + if (substr($payload, 9, $length) == 'keepalive@openssh.com' && isset($this->server_channels[$channel])) { + if (ord(substr($payload, 9 + $length))) { // want reply + $this->_send_binary_packet(pack('CN', NET_SSH2_MSG_CHANNEL_SUCCESS, $this->server_channels[$channel])); + } + $payload = $this->_get_binary_packet($skip_channel_filter); + } + } + break; + case NET_SSH2_MSG_CHANNEL_DATA: + case NET_SSH2_MSG_CHANNEL_EXTENDED_DATA: + case NET_SSH2_MSG_CHANNEL_CLOSE: + case NET_SSH2_MSG_CHANNEL_EOF: + if (!$skip_channel_filter && !empty($this->server_channels)) { + $this->binary_packet_buffer = $payload; + $this->_get_channel_packet(true); + $payload = $this->_get_binary_packet(); + } + break; + case NET_SSH2_MSG_GLOBAL_REQUEST: // see http://tools.ietf.org/html/rfc4254#section-4 + if (strlen($payload) < 4) { + return false; + } + extract(unpack('Nlength', $this->_string_shift($payload, 4))); + $this->errors[] = 'SSH_MSG_GLOBAL_REQUEST: ' . $this->_string_shift($payload, $length); + + if (!$this->_send_binary_packet(pack('C', NET_SSH2_MSG_REQUEST_FAILURE))) { + return $this->_disconnect(NET_SSH2_DISCONNECT_BY_APPLICATION); + } + + $payload = $this->_get_binary_packet($skip_channel_filter); + break; + case NET_SSH2_MSG_CHANNEL_OPEN: // see http://tools.ietf.org/html/rfc4254#section-5.1 + $this->_string_shift($payload, 1); + if (strlen($payload) < 4) { + return false; + } + extract(unpack('Nlength', $this->_string_shift($payload, 4))); + $data = $this->_string_shift($payload, $length); + if (strlen($payload) < 4) { + return false; + } + extract(unpack('Nserver_channel', $this->_string_shift($payload, 4))); + switch ($data) { + case 'auth-agent': + case 'auth-agent@openssh.com': + if (isset($this->agent)) { + $new_channel = self::CHANNEL_AGENT_FORWARD; + + if (strlen($payload) < 8) { + return false; + } + extract(unpack('Nremote_window_size', $this->_string_shift($payload, 4))); + extract(unpack('Nremote_maximum_packet_size', $this->_string_shift($payload, 4))); + + $this->packet_size_client_to_server[$new_channel] = $remote_window_size; + $this->window_size_server_to_client[$new_channel] = $remote_maximum_packet_size; + $this->window_size_client_to_server[$new_channel] = $this->window_size; + + $packet_size = 0x4000; + + $packet = pack( + 'CN4', + NET_SSH2_MSG_CHANNEL_OPEN_CONFIRMATION, + $server_channel, + $new_channel, + $packet_size, + $packet_size + ); + + $this->server_channels[$new_channel] = $server_channel; + $this->channel_status[$new_channel] = NET_SSH2_MSG_CHANNEL_OPEN_CONFIRMATION; + if (!$this->_send_binary_packet($packet)) { + return false; + } + } + break; + default: + $packet = pack( + 'CN3a*Na*', + NET_SSH2_MSG_REQUEST_FAILURE, + $server_channel, + NET_SSH2_OPEN_ADMINISTRATIVELY_PROHIBITED, + 0, + '', + 0, + '' + ); + + if (!$this->_send_binary_packet($packet)) { + return $this->_disconnect(NET_SSH2_DISCONNECT_BY_APPLICATION); + } + } + $payload = $this->_get_binary_packet($skip_channel_filter); + break; + case NET_SSH2_MSG_CHANNEL_WINDOW_ADJUST: + $this->_string_shift($payload, 1); + if (strlen($payload) < 8) { + return false; + } + extract(unpack('Nchannel', $this->_string_shift($payload, 4))); + extract(unpack('Nwindow_size', $this->_string_shift($payload, 4))); + $this->window_size_client_to_server[$channel]+= $window_size; + + $payload = ($this->bitmap & self::MASK_WINDOW_ADJUST) ? true : $this->_get_binary_packet($skip_channel_filter); + } + } + + return $payload; + } + + /** + * Enable Quiet Mode + * + * Suppress stderr from output + * + * @access public + */ + function enableQuietMode() + { + $this->quiet_mode = true; + } + + /** + * Disable Quiet Mode + * + * Show stderr in output + * + * @access public + */ + function disableQuietMode() + { + $this->quiet_mode = false; + } + + /** + * Returns whether Quiet Mode is enabled or not + * + * @see self::enableQuietMode() + * @see self::disableQuietMode() + * @access public + * @return bool + */ + function isQuietModeEnabled() + { + return $this->quiet_mode; + } + + /** + * Enable request-pty when using exec() + * + * @access public + */ + function enablePTY() + { + $this->request_pty = true; + } + + /** + * Disable request-pty when using exec() + * + * @access public + */ + function disablePTY() + { + if ($this->in_request_pty_exec) { + $this->_close_channel(self::CHANNEL_EXEC); + $this->in_request_pty_exec = false; + } + $this->request_pty = false; + } + + /** + * Returns whether request-pty is enabled or not + * + * @see self::enablePTY() + * @see self::disablePTY() + * @access public + * @return bool + */ + function isPTYEnabled() + { + return $this->request_pty; + } + + /** + * Gets channel data + * + * Returns the data as a string if it's available and false if not. + * + * @param int $client_channel + * @param bool $skip_extended + * @return mixed|bool + * @access private + */ + function _get_channel_packet($client_channel, $skip_extended = false) + { + if (!empty($this->channel_buffers[$client_channel])) { + switch ($this->channel_status[$client_channel]) { + case NET_SSH2_MSG_CHANNEL_REQUEST: + foreach ($this->channel_buffers[$client_channel] as $i => $packet) { + switch (ord($packet[0])) { + case NET_SSH2_MSG_CHANNEL_SUCCESS: + case NET_SSH2_MSG_CHANNEL_FAILURE: + unset($this->channel_buffers[$client_channel][$i]); + return substr($packet, 1); + } + } + break; + default: + return substr(array_shift($this->channel_buffers[$client_channel]), 1); + } + } + + while (true) { + if ($this->binary_packet_buffer !== false) { + $response = $this->binary_packet_buffer; + $this->binary_packet_buffer = false; + } else { + $response = $this->_get_binary_packet(true); + if ($response === true && $this->is_timeout) { + if ($client_channel == self::CHANNEL_EXEC && !$this->request_pty) { + $this->_close_channel($client_channel); + } + return true; + } + if ($response === false) { + $this->bitmap = 0; + user_error('Connection closed by server'); + return false; + } + } + + if ($client_channel == -1 && $response === true) { + return true; + } + if (!strlen($response)) { + return false; + } + extract(unpack('Ctype', $this->_string_shift($response, 1))); + + if (strlen($response) < 4) { + return false; + } + if ($type == NET_SSH2_MSG_CHANNEL_OPEN) { + extract(unpack('Nlength', $this->_string_shift($response, 4))); + } else { + extract(unpack('Nchannel', $this->_string_shift($response, 4))); + } + + // will not be setup yet on incoming channel open request + if (isset($channel) && isset($this->channel_status[$channel]) && isset($this->window_size_server_to_client[$channel])) { + $this->window_size_server_to_client[$channel]-= strlen($response); + + // resize the window, if appropriate + if ($this->window_size_server_to_client[$channel] < 0) { + // PuTTY does something more analogous to the following: + //if ($this->window_size_server_to_client[$channel] < 0x3FFFFFFF) { + $packet = pack('CNN', NET_SSH2_MSG_CHANNEL_WINDOW_ADJUST, $this->server_channels[$channel], $this->window_resize); + if (!$this->_send_binary_packet($packet)) { + return false; + } + $this->window_size_server_to_client[$channel]+= $this->window_resize; + } + + switch ($type) { + case NET_SSH2_MSG_CHANNEL_EXTENDED_DATA: + /* + if ($client_channel == self::CHANNEL_EXEC) { + $this->_send_channel_packet($client_channel, chr(0)); + } + */ + // currently, there's only one possible value for $data_type_code: NET_SSH2_EXTENDED_DATA_STDERR + if (strlen($response) < 8) { + return false; + } + extract(unpack('Ndata_type_code/Nlength', $this->_string_shift($response, 8))); + $data = $this->_string_shift($response, $length); + $this->stdErrorLog.= $data; + if ($skip_extended || $this->quiet_mode) { + continue 2; + } + if ($client_channel == $channel && $this->channel_status[$channel] == NET_SSH2_MSG_CHANNEL_DATA) { + return $data; + } + $this->channel_buffers[$channel][] = chr($type) . $data; + + continue 2; + case NET_SSH2_MSG_CHANNEL_REQUEST: + if ($this->channel_status[$channel] == NET_SSH2_MSG_CHANNEL_CLOSE) { + continue 2; + } + if (strlen($response) < 4) { + return false; + } + extract(unpack('Nlength', $this->_string_shift($response, 4))); + $value = $this->_string_shift($response, $length); + switch ($value) { + case 'exit-signal': + $this->_string_shift($response, 1); + if (strlen($response) < 4) { + return false; + } + extract(unpack('Nlength', $this->_string_shift($response, 4))); + $this->errors[] = 'SSH_MSG_CHANNEL_REQUEST (exit-signal): ' . $this->_string_shift($response, $length); + $this->_string_shift($response, 1); + if (strlen($response) < 4) { + return false; + } + extract(unpack('Nlength', $this->_string_shift($response, 4))); + if ($length) { + $this->errors[count($this->errors)].= "\r\n" . $this->_string_shift($response, $length); + } + + $this->_send_binary_packet(pack('CN', NET_SSH2_MSG_CHANNEL_EOF, $this->server_channels[$client_channel])); + $this->_send_binary_packet(pack('CN', NET_SSH2_MSG_CHANNEL_CLOSE, $this->server_channels[$channel])); + + $this->channel_status[$channel] = NET_SSH2_MSG_CHANNEL_EOF; + + continue 3; + case 'exit-status': + if (strlen($response) < 5) { + return false; + } + extract(unpack('Cfalse/Nexit_status', $this->_string_shift($response, 5))); + $this->exit_status = $exit_status; + + // "The client MAY ignore these messages." + // -- http://tools.ietf.org/html/rfc4254#section-6.10 + + continue 3; + default: + // "Some systems may not implement signals, in which case they SHOULD ignore this message." + // -- http://tools.ietf.org/html/rfc4254#section-6.9 + continue 3; + } + } + + switch ($this->channel_status[$channel]) { + case NET_SSH2_MSG_CHANNEL_OPEN: + switch ($type) { + case NET_SSH2_MSG_CHANNEL_OPEN_CONFIRMATION: + if (strlen($response) < 4) { + return false; + } + extract(unpack('Nserver_channel', $this->_string_shift($response, 4))); + $this->server_channels[$channel] = $server_channel; + if (strlen($response) < 4) { + return false; + } + extract(unpack('Nwindow_size', $this->_string_shift($response, 4))); + if ($window_size < 0) { + $window_size&= 0x7FFFFFFF; + $window_size+= 0x80000000; + } + $this->window_size_client_to_server[$channel] = $window_size; + if (strlen($response) < 4) { + return false; + } + $temp = unpack('Npacket_size_client_to_server', $this->_string_shift($response, 4)); + $this->packet_size_client_to_server[$channel] = $temp['packet_size_client_to_server']; + $result = $client_channel == $channel ? true : $this->_get_channel_packet($client_channel, $skip_extended); + $this->_on_channel_open(); + return $result; + case NET_SSH2_MSG_CHANNEL_OPEN_FAILURE: + user_error('Unable to open channel'); + return $this->_disconnect(NET_SSH2_DISCONNECT_BY_APPLICATION); + default: + if ($client_channel == $channel) { + user_error('Unexpected response to open request'); + return $this->_disconnect(NET_SSH2_DISCONNECT_BY_APPLICATION); + } + return $this->_get_channel_packet($client_channel, $skip_extended); + } + break; + case NET_SSH2_MSG_CHANNEL_REQUEST: + switch ($type) { + case NET_SSH2_MSG_CHANNEL_SUCCESS: + return true; + case NET_SSH2_MSG_CHANNEL_FAILURE: + return false; + case NET_SSH2_MSG_CHANNEL_DATA: + if (strlen($response) < 4) { + return false; + } + extract(unpack('Nlength', $this->_string_shift($response, 4))); + $data = $this->_string_shift($response, $length); + $this->channel_buffers[$channel][] = chr($type) . $data; + return $this->_get_channel_packet($client_channel, $skip_extended); + default: + user_error('Unable to fulfill channel request'); + return $this->_disconnect(NET_SSH2_DISCONNECT_BY_APPLICATION); + } + case NET_SSH2_MSG_CHANNEL_CLOSE: + return $type == NET_SSH2_MSG_CHANNEL_CLOSE ? true : $this->_get_channel_packet($client_channel, $skip_extended); + } + } + + // ie. $this->channel_status[$channel] == NET_SSH2_MSG_CHANNEL_DATA + + switch ($type) { + case NET_SSH2_MSG_CHANNEL_DATA: + /* + if ($channel == self::CHANNEL_EXEC) { + // SCP requires null packets, such as this, be sent. further, in the case of the ssh.com SSH server + // this actually seems to make things twice as fast. more to the point, the message right after + // SSH_MSG_CHANNEL_DATA (usually SSH_MSG_IGNORE) won't block for as long as it would have otherwise. + // in OpenSSH it slows things down but only by a couple thousandths of a second. + $this->_send_channel_packet($channel, chr(0)); + } + */ + if (strlen($response) < 4) { + return false; + } + extract(unpack('Nlength', $this->_string_shift($response, 4))); + $data = $this->_string_shift($response, $length); + + if ($channel == self::CHANNEL_AGENT_FORWARD) { + $agent_response = $this->agent->_forward_data($data); + if (!is_bool($agent_response)) { + $this->_send_channel_packet($channel, $agent_response); + } + break; + } + + if ($client_channel == $channel) { + return $data; + } + $this->channel_buffers[$channel][] = chr($type) . $data; + break; + case NET_SSH2_MSG_CHANNEL_CLOSE: + $this->curTimeout = 5; + + if ($this->bitmap & self::MASK_SHELL) { + $this->bitmap&= ~self::MASK_SHELL; + } + if ($this->channel_status[$channel] != NET_SSH2_MSG_CHANNEL_EOF) { + $this->_send_binary_packet(pack('CN', NET_SSH2_MSG_CHANNEL_CLOSE, $this->server_channels[$channel])); + } + + $this->channel_status[$channel] = NET_SSH2_MSG_CHANNEL_CLOSE; + if ($client_channel == $channel) { + return true; + } + case NET_SSH2_MSG_CHANNEL_EOF: + break; + default: + user_error("Error reading channel data ($type)"); + return $this->_disconnect(NET_SSH2_DISCONNECT_BY_APPLICATION); + } + } + } + + /** + * Sends Binary Packets + * + * See '6. Binary Packet Protocol' of rfc4253 for more info. + * + * @param string $data + * @param string $logged + * @see self::_get_binary_packet() + * @return bool + * @access private + */ + function _send_binary_packet($data, $logged = null) + { + if (!is_resource($this->fsock) || feof($this->fsock)) { + $this->bitmap = 0; + user_error('Connection closed prematurely'); + return false; + } + + if (!isset($logged)) { + $logged = $data; + } + + switch ($this->compress) { + case self::NET_SSH2_COMPRESSION_ZLIB_AT_OPENSSH: + if (!$this->isAuthenticated()) { + break; + } + case self::NET_SSH2_COMPRESSION_ZLIB: + if (!$this->regenerate_compression_context) { + $header = ''; + } else { + $this->regenerate_compression_context = false; + $this->compress_context = deflate_init(ZLIB_ENCODING_RAW, array('window' => 15)); + $header = "\x78\x9C"; + } + if ($this->compress_context) { + $data = $header . deflate_add($this->compress_context, $data, ZLIB_PARTIAL_FLUSH); + } + } + + // 4 (packet length) + 1 (padding length) + 4 (minimal padding amount) == 9 + $packet_length = strlen($data) + 9; + // round up to the nearest $this->encrypt_block_size + $packet_length+= (($this->encrypt_block_size - 1) * $packet_length) % $this->encrypt_block_size; + // subtracting strlen($data) is obvious - subtracting 5 is necessary because of packet_length and padding_length + $padding_length = $packet_length - strlen($data) - 5; + $padding = Random::string($padding_length); + + // we subtract 4 from packet_length because the packet_length field isn't supposed to include itself + $packet = pack('NCa*', $packet_length - 4, $padding_length, $data . $padding); + + $hmac = $this->hmac_create !== false ? $this->hmac_create->hash(pack('Na*', $this->send_seq_no, $packet)) : ''; + $this->send_seq_no++; + + if ($this->encrypt !== false) { + $packet = $this->encrypt->encrypt($packet); + } + + $packet.= $hmac; + + $start = microtime(true); + $result = strlen($packet) == @fputs($this->fsock, $packet); + $stop = microtime(true); + + if (defined('NET_SSH2_LOGGING')) { + $current = microtime(true); + $message_number = isset($this->message_numbers[ord($logged[0])]) ? $this->message_numbers[ord($logged[0])] : 'UNKNOWN (' . ord($logged[0]) . ')'; + $message_number = '-> ' . $message_number . + ' (since last: ' . round($current - $this->last_packet, 4) . ', network: ' . round($stop - $start, 4) . 's)'; + $this->_append_log($message_number, $logged); + $this->last_packet = $current; + } + + return $result; + } + + /** + * Logs data packets + * + * Makes sure that only the last 1MB worth of packets will be logged + * + * @param string $message_number + * @param string $message + * @access private + */ + function _append_log($message_number, $message) + { + // remove the byte identifying the message type from all but the first two messages (ie. the identification strings) + if (strlen($message_number) > 2) { + $this->_string_shift($message); + } + + switch (NET_SSH2_LOGGING) { + // useful for benchmarks + case self::LOG_SIMPLE: + $this->message_number_log[] = $message_number; + break; + // the most useful log for SSH2 + case self::LOG_COMPLEX: + $this->message_number_log[] = $message_number; + $this->log_size+= strlen($message); + $this->message_log[] = $message; + while ($this->log_size > self::LOG_MAX_SIZE) { + $this->log_size-= strlen(array_shift($this->message_log)); + array_shift($this->message_number_log); + } + break; + // dump the output out realtime; packets may be interspersed with non packets, + // passwords won't be filtered out and select other packets may not be correctly + // identified + case self::LOG_REALTIME: + switch (PHP_SAPI) { + case 'cli': + $start = $stop = "\r\n"; + break; + default: + $start = '
';
+                        $stop = '
'; + } + echo $start . $this->_format_log(array($message), array($message_number)) . $stop; + @flush(); + @ob_flush(); + break; + // basically the same thing as self::LOG_REALTIME with the caveat that self::LOG_REALTIME_FILE + // needs to be defined and that the resultant log file will be capped out at self::LOG_MAX_SIZE. + // the earliest part of the log file is denoted by the first <<< START >>> and is not going to necessarily + // at the beginning of the file + case self::LOG_REALTIME_FILE: + if (!isset($this->realtime_log_file)) { + // PHP doesn't seem to like using constants in fopen() + $filename = self::LOG_REALTIME_FILENAME; + $fp = fopen($filename, 'w'); + $this->realtime_log_file = $fp; + } + if (!is_resource($this->realtime_log_file)) { + break; + } + $entry = $this->_format_log(array($message), array($message_number)); + if ($this->realtime_log_wrap) { + $temp = "<<< START >>>\r\n"; + $entry.= $temp; + fseek($this->realtime_log_file, ftell($this->realtime_log_file) - strlen($temp)); + } + $this->realtime_log_size+= strlen($entry); + if ($this->realtime_log_size > self::LOG_MAX_SIZE) { + fseek($this->realtime_log_file, 0); + $this->realtime_log_size = strlen($entry); + $this->realtime_log_wrap = true; + } + fputs($this->realtime_log_file, $entry); + } + } + + /** + * Sends channel data + * + * Spans multiple SSH_MSG_CHANNEL_DATAs if appropriate + * + * @param int $client_channel + * @param string $data + * @return bool + * @access private + */ + function _send_channel_packet($client_channel, $data) + { + while (strlen($data)) { + if (!$this->window_size_client_to_server[$client_channel]) { + $this->bitmap^= self::MASK_WINDOW_ADJUST; + // using an invalid channel will let the buffers be built up for the valid channels + $this->_get_channel_packet(-1); + $this->bitmap^= self::MASK_WINDOW_ADJUST; + } + + /* The maximum amount of data allowed is determined by the maximum + packet size for the channel, and the current window size, whichever + is smaller. + -- http://tools.ietf.org/html/rfc4254#section-5.2 */ + $max_size = min( + $this->packet_size_client_to_server[$client_channel], + $this->window_size_client_to_server[$client_channel] + ); + + $temp = $this->_string_shift($data, $max_size); + $packet = pack( + 'CN2a*', + NET_SSH2_MSG_CHANNEL_DATA, + $this->server_channels[$client_channel], + strlen($temp), + $temp + ); + $this->window_size_client_to_server[$client_channel]-= strlen($temp); + if (!$this->_send_binary_packet($packet)) { + return false; + } + } + + return true; + } + + /** + * Closes and flushes a channel + * + * \phpseclib\Net\SSH2 doesn't properly close most channels. For exec() channels are normally closed by the server + * and for SFTP channels are presumably closed when the client disconnects. This functions is intended + * for SCP more than anything. + * + * @param int $client_channel + * @param bool $want_reply + * @return bool + * @access private + */ + function _close_channel($client_channel, $want_reply = false) + { + // see http://tools.ietf.org/html/rfc4254#section-5.3 + + $this->_send_binary_packet(pack('CN', NET_SSH2_MSG_CHANNEL_EOF, $this->server_channels[$client_channel])); + + if (!$want_reply) { + $this->_send_binary_packet(pack('CN', NET_SSH2_MSG_CHANNEL_CLOSE, $this->server_channels[$client_channel])); + } + + $this->channel_status[$client_channel] = NET_SSH2_MSG_CHANNEL_CLOSE; + + $this->curTimeout = 5; + + while (!is_bool($this->_get_channel_packet($client_channel))) { + } + + if ($this->is_timeout) { + $this->disconnect(); + } + + if ($want_reply) { + $this->_send_binary_packet(pack('CN', NET_SSH2_MSG_CHANNEL_CLOSE, $this->server_channels[$client_channel])); + } + + if ($this->bitmap & self::MASK_SHELL) { + $this->bitmap&= ~self::MASK_SHELL; + } + } + + /** + * Disconnect + * + * @param int $reason + * @return bool + * @access private + */ + function _disconnect($reason) + { + if ($this->bitmap & self::MASK_CONNECTED) { + $data = pack('CNNa*Na*', NET_SSH2_MSG_DISCONNECT, $reason, 0, '', 0, ''); + $this->_send_binary_packet($data); + } + + $this->bitmap = 0; + if (is_resource($this->fsock) && get_resource_type($this->fsock) == 'stream') { + fclose($this->fsock); + } + + return false; + } + + /** + * String Shift + * + * Inspired by array_shift + * + * @param string $string + * @param int $index + * @return string + * @access private + */ + function _string_shift(&$string, $index = 1) + { + $substr = substr($string, 0, $index); + $string = substr($string, $index); + return $substr; + } + + /** + * Define Array + * + * Takes any number of arrays whose indices are integers and whose values are strings and defines a bunch of + * named constants from it, using the value as the name of the constant and the index as the value of the constant. + * If any of the constants that would be defined already exists, none of the constants will be defined. + * + * @access private + */ + function _define_array() + { + $args = func_get_args(); + foreach ($args as $arg) { + foreach ($arg as $key => $value) { + if (!defined($value)) { + define($value, $key); + } else { + break 2; + } + } + } + } + + /** + * Returns a log of the packets that have been sent and received. + * + * Returns a string if NET_SSH2_LOGGING == self::LOG_COMPLEX, an array if NET_SSH2_LOGGING == self::LOG_SIMPLE and false if !defined('NET_SSH2_LOGGING') + * + * @access public + * @return array|false|string + */ + function getLog() + { + if (!defined('NET_SSH2_LOGGING')) { + return false; + } + + switch (NET_SSH2_LOGGING) { + case self::LOG_SIMPLE: + return $this->message_number_log; + case self::LOG_COMPLEX: + $log = $this->_format_log($this->message_log, $this->message_number_log); + return PHP_SAPI == 'cli' ? $log : '
' . $log . '
'; + default: + return false; + } + } + + /** + * Formats a log for printing + * + * @param array $message_log + * @param array $message_number_log + * @access private + * @return string + */ + function _format_log($message_log, $message_number_log) + { + $output = ''; + for ($i = 0; $i < count($message_log); $i++) { + $output.= $message_number_log[$i] . "\r\n"; + $current_log = $message_log[$i]; + $j = 0; + do { + if (strlen($current_log)) { + $output.= str_pad(dechex($j), 7, '0', STR_PAD_LEFT) . '0 '; + } + $fragment = $this->_string_shift($current_log, $this->log_short_width); + $hex = substr(preg_replace_callback('#.#s', array($this, '_format_log_helper'), $fragment), strlen($this->log_boundary)); + // replace non ASCII printable characters with dots + // http://en.wikipedia.org/wiki/ASCII#ASCII_printable_characters + // also replace < with a . since < messes up the output on web browsers + $raw = preg_replace('#[^\x20-\x7E]|<#', '.', $fragment); + $output.= str_pad($hex, $this->log_long_width - $this->log_short_width, ' ') . $raw . "\r\n"; + $j++; + } while (strlen($current_log)); + $output.= "\r\n"; + } + + return $output; + } + + /** + * Helper function for _format_log + * + * For use with preg_replace_callback() + * + * @param array $matches + * @access private + * @return string + */ + function _format_log_helper($matches) + { + return $this->log_boundary . str_pad(dechex(ord($matches[0])), 2, '0', STR_PAD_LEFT); + } + + /** + * Helper function for agent->_on_channel_open() + * + * Used when channels are created to inform agent + * of said channel opening. Must be called after + * channel open confirmation received + * + * @access private + */ + function _on_channel_open() + { + if (isset($this->agent)) { + $this->agent->_on_channel_open($this); + } + } + + /** + * Returns the first value of the intersection of two arrays or false if + * the intersection is empty. The order is defined by the first parameter. + * + * @param array $array1 + * @param array $array2 + * @return mixed False if intersection is empty, else intersected value. + * @access private + */ + function _array_intersect_first($array1, $array2) + { + foreach ($array1 as $value) { + if (in_array($value, $array2)) { + return $value; + } + } + return false; + } + + /** + * Returns all errors / debug messages on the SSH layer + * + * If you are looking for messages from the SFTP layer, please see SFTP::getSFTPErrors() + * + * @return string[] + * @access public + */ + function getErrors() + { + return $this->errors; + } + + /** + * Returns the last error received on the SSH layer + * + * If you are looking for messages from the SFTP layer, please see SFTP::getLastSFTPError() + * + * @return string + * @access public + */ + function getLastError() + { + $count = count($this->errors); + + if ($count > 0) { + return $this->errors[$count - 1]; + } + } + + /** + * Return the server identification. + * + * @return string + * @access public + */ + function getServerIdentification() + { + $this->_connect(); + + return $this->server_identifier; + } + + /** + * Return a list of the key exchange algorithms the server supports. + * + * @return array + * @access public + */ + function getKexAlgorithms() + { + $this->_connect(); + + return $this->kex_algorithms; + } + + /** + * Return a list of the host key (public key) algorithms the server supports. + * + * @return array + * @access public + */ + function getServerHostKeyAlgorithms() + { + $this->_connect(); + + return $this->server_host_key_algorithms; + } + + /** + * Return a list of the (symmetric key) encryption algorithms the server supports, when receiving stuff from the client. + * + * @return array + * @access public + */ + function getEncryptionAlgorithmsClient2Server() + { + $this->_connect(); + + return $this->encryption_algorithms_client_to_server; + } + + /** + * Return a list of the (symmetric key) encryption algorithms the server supports, when sending stuff to the client. + * + * @return array + * @access public + */ + function getEncryptionAlgorithmsServer2Client() + { + $this->_connect(); + + return $this->encryption_algorithms_server_to_client; + } + + /** + * Return a list of the MAC algorithms the server supports, when receiving stuff from the client. + * + * @return array + * @access public + */ + function getMACAlgorithmsClient2Server() + { + $this->_connect(); + + return $this->mac_algorithms_client_to_server; + } + + /** + * Return a list of the MAC algorithms the server supports, when sending stuff to the client. + * + * @return array + * @access public + */ + function getMACAlgorithmsServer2Client() + { + $this->_connect(); + + return $this->mac_algorithms_server_to_client; + } + + /** + * Return a list of the compression algorithms the server supports, when receiving stuff from the client. + * + * @return array + * @access public + */ + function getCompressionAlgorithmsClient2Server() + { + $this->_connect(); + + return $this->compression_algorithms_client_to_server; + } + + /** + * Return a list of the compression algorithms the server supports, when sending stuff to the client. + * + * @return array + * @access public + */ + function getCompressionAlgorithmsServer2Client() + { + $this->_connect(); + + return $this->compression_algorithms_server_to_client; + } + + /** + * Return a list of the languages the server supports, when sending stuff to the client. + * + * @return array + * @access public + */ + function getLanguagesServer2Client() + { + $this->_connect(); + + return $this->languages_server_to_client; + } + + /** + * Return a list of the languages the server supports, when receiving stuff from the client. + * + * @return array + * @access public + */ + function getLanguagesClient2Server() + { + $this->_connect(); + + return $this->languages_client_to_server; + } + + /** + * Returns a list of algorithms the server supports + * + * @return array + * @access public + */ + function getServerAlgorithms() + { + $this->_connect(); + + return array( + 'kex' => $this->kex_algorithms, + 'hostkey' => $this->server_host_key_algorithms, + 'client_to_server' => array( + 'crypt' => $this->encryption_algorithms_client_to_server, + 'mac' => $this->mac_algorithms_client_to_server, + 'comp' => $this->compression_algorithms_client_to_server, + 'lang' => $this->languages_client_to_server + ), + 'server_to_client' => array( + 'crypt' => $this->encryption_algorithms_server_to_client, + 'mac' => $this->mac_algorithms_server_to_client, + 'comp' => $this->compression_algorithms_server_to_client, + 'lang' => $this->languages_server_to_client + ) + ); + } + + /** + * Returns a list of KEX algorithms that phpseclib supports + * + * @return array + * @access public + */ + function getSupportedKEXAlgorithms() + { + $kex_algorithms = array( + // Elliptic Curve Diffie-Hellman Key Agreement (ECDH) using + // Curve25519. See doc/curve25519-sha256@libssh.org.txt in the + // libssh repository for more information. + 'curve25519-sha256@libssh.org', + + 'diffie-hellman-group-exchange-sha256',// RFC 4419 + 'diffie-hellman-group-exchange-sha1', // RFC 4419 + + // Diffie-Hellman Key Agreement (DH) using integer modulo prime + // groups. + 'diffie-hellman-group14-sha1', // REQUIRED + 'diffie-hellman-group1-sha1', // REQUIRED + ); + + if (!function_exists('sodium_crypto_box_publickey_from_secretkey')) { + $kex_algorithms = array_diff( + $kex_algorithms, + array('curve25519-sha256@libssh.org') + ); + } + + return $kex_algorithms; + } + + /** + * Returns a list of host key algorithms that phpseclib supports + * + * @return array + * @access public + */ + function getSupportedHostKeyAlgorithms() + { + return array( + 'rsa-sha2-256', // RFC 8332 + 'rsa-sha2-512', // RFC 8332 + 'ssh-rsa', // RECOMMENDED sign Raw RSA Key + 'ssh-dss' // REQUIRED sign Raw DSS Key + ); + } + + /** + * Returns a list of symmetric key algorithms that phpseclib supports + * + * @return array + * @access public + */ + function getSupportedEncryptionAlgorithms() + { + $algos = array( + // from : + 'arcfour256', + 'arcfour128', + + //'arcfour', // OPTIONAL the ARCFOUR stream cipher with a 128-bit key + + // CTR modes from : + 'aes128-ctr', // RECOMMENDED AES (Rijndael) in SDCTR mode, with 128-bit key + 'aes192-ctr', // RECOMMENDED AES with 192-bit key + 'aes256-ctr', // RECOMMENDED AES with 256-bit key + + 'twofish128-ctr', // OPTIONAL Twofish in SDCTR mode, with 128-bit key + 'twofish192-ctr', // OPTIONAL Twofish with 192-bit key + 'twofish256-ctr', // OPTIONAL Twofish with 256-bit key + + 'aes128-cbc', // RECOMMENDED AES with a 128-bit key + 'aes192-cbc', // OPTIONAL AES with a 192-bit key + 'aes256-cbc', // OPTIONAL AES in CBC mode, with a 256-bit key + + 'twofish128-cbc', // OPTIONAL Twofish with a 128-bit key + 'twofish192-cbc', // OPTIONAL Twofish with a 192-bit key + 'twofish256-cbc', + 'twofish-cbc', // OPTIONAL alias for "twofish256-cbc" + // (this is being retained for historical reasons) + + 'blowfish-ctr', // OPTIONAL Blowfish in SDCTR mode + + 'blowfish-cbc', // OPTIONAL Blowfish in CBC mode + + '3des-ctr', // RECOMMENDED Three-key 3DES in SDCTR mode + + '3des-cbc', // REQUIRED three-key 3DES in CBC mode + + //'none' // OPTIONAL no encryption; NOT RECOMMENDED + ); + + if ($this->crypto_engine) { + $engines = array($this->crypto_engine); + } else { + $engines = array( + Base::ENGINE_OPENSSL, + Base::ENGINE_MCRYPT, + Base::ENGINE_INTERNAL + ); + } + + $ciphers = array(); + foreach ($engines as $engine) { + foreach ($algos as $algo) { + $obj = $this->_encryption_algorithm_to_crypt_instance($algo); + if ($obj instanceof Rijndael) { + $obj->setKeyLength(preg_replace('#[^\d]#', '', $algo)); + } + switch ($algo) { + case 'arcfour128': + case 'arcfour256': + if ($engine != Base::ENGINE_INTERNAL) { + continue 2; + } + } + if ($obj->isValidEngine($engine)) { + $algos = array_diff($algos, array($algo)); + $ciphers[] = $algo; + } + } + } + + return $ciphers; + } + + /** + * Returns a list of MAC algorithms that phpseclib supports + * + * @return array + * @access public + */ + function getSupportedMACAlgorithms() + { + return array( + // from : + 'hmac-sha2-256',// RECOMMENDED HMAC-SHA256 (digest length = key length = 32) + + 'hmac-sha1-96', // RECOMMENDED first 96 bits of HMAC-SHA1 (digest length = 12, key length = 20) + 'hmac-sha1', // REQUIRED HMAC-SHA1 (digest length = key length = 20) + 'hmac-md5-96', // OPTIONAL first 96 bits of HMAC-MD5 (digest length = 12, key length = 16) + 'hmac-md5', // OPTIONAL HMAC-MD5 (digest length = key length = 16) + //'none' // OPTIONAL no MAC; NOT RECOMMENDED + ); + } + + /** + * Returns a list of compression algorithms that phpseclib supports + * + * @return array + * @access public + */ + function getSupportedCompressionAlgorithms() + { + $algos = array('none'); // REQUIRED no compression + if (function_exists('deflate_init')) { + $algos[] = 'zlib@openssh.com'; // https://datatracker.ietf.org/doc/html/draft-miller-secsh-compression-delayed + $algos[] = 'zlib'; + } + return $algos; + } + + /** + * Return list of negotiated algorithms + * + * Uses the same format as https://www.php.net/ssh2-methods-negotiated + * + * @return array + * @access public + */ + function getAlgorithmsNegotiated() + { + $this->_connect(); + + $compression_map = array( + self::NET_SSH2_COMPRESSION_NONE => 'none', + self::NET_SSH2_COMPRESSION_ZLIB => 'zlib', + self::NET_SSH2_COMPRESSION_ZLIB_AT_OPENSSH => 'zlib@openssh.com' + ); + + return array( + 'kex' => $this->kex_algorithm, + 'hostkey' => $this->signature_format, + 'client_to_server' => array( + 'crypt' => $this->encryptName, + 'mac' => $this->hmac_create_name, + 'comp' => $compression_map[$this->compress], + ), + 'server_to_client' => array( + 'crypt' => $this->decryptName, + 'mac' => $this->hmac_check_name, + 'comp' => $compression_map[$this->decompress], + ) + ); + } + + /** + * Accepts an associative array with up to four parameters as described at + * + * + * @param array $methods + * @access public + */ + function setPreferredAlgorithms($methods) + { + $preferred = $methods; + + if (isset($preferred['kex'])) { + $preferred['kex'] = array_intersect( + $preferred['kex'], + $this->getSupportedKEXAlgorithms() + ); + } + + if (isset($preferred['hostkey'])) { + $preferred['hostkey'] = array_intersect( + $preferred['hostkey'], + $this->getSupportedHostKeyAlgorithms() + ); + } + + $keys = array('client_to_server', 'server_to_client'); + foreach ($keys as $key) { + if (isset($preferred[$key])) { + $a = &$preferred[$key]; + if (isset($a['crypt'])) { + $a['crypt'] = array_intersect( + $a['crypt'], + $this->getSupportedEncryptionAlgorithms() + ); + } + if (isset($a['comp'])) { + $a['comp'] = array_intersect( + $a['comp'], + $this->getSupportedCompressionAlgorithms() + ); + } + if (isset($a['mac'])) { + $a['mac'] = array_intersect( + $a['mac'], + $this->getSupportedMACAlgorithms() + ); + } + } + } + + $keys = array( + 'kex', + 'hostkey', + 'client_to_server/crypt', + 'client_to_server/comp', + 'client_to_server/mac', + 'server_to_client/crypt', + 'server_to_client/comp', + 'server_to_client/mac', + ); + foreach ($keys as $key) { + $p = $preferred; + $m = $methods; + + $subkeys = explode('/', $key); + foreach ($subkeys as $subkey) { + if (!isset($p[$subkey])) { + continue 2; + } + $p = $p[$subkey]; + $m = $m[$subkey]; + } + + if (count($p) != count($m)) { + $diff = array_diff($m, $p); + $msg = count($diff) == 1 ? + ' is not a supported algorithm' : + ' are not supported algorithms'; + user_error(implode(', ', $diff) . $msg); + return false; + } + } + + $this->preferred = $preferred; + } + + /** + * Returns the banner message. + * + * Quoting from the RFC, "in some jurisdictions, sending a warning message before + * authentication may be relevant for getting legal protection." + * + * @return string + * @access public + */ + function getBannerMessage() + { + return $this->banner_message; + } + + /** + * Returns the server public host key. + * + * Caching this the first time you connect to a server and checking the result on subsequent connections + * is recommended. Returns false if the server signature is not signed correctly with the public host key. + * + * @return mixed + * @access public + */ + function getServerPublicHostKey() + { + if (!($this->bitmap & self::MASK_CONSTRUCTOR)) { + if (!$this->_connect()) { + return false; + } + } + + $signature = $this->signature; + $server_public_host_key = $this->server_public_host_key; + + if (strlen($server_public_host_key) < 4) { + return false; + } + extract(unpack('Nlength', $this->_string_shift($server_public_host_key, 4))); + $this->_string_shift($server_public_host_key, $length); + + if ($this->signature_validated) { + return $this->bitmap ? + $this->signature_format . ' ' . base64_encode($this->server_public_host_key) : + false; + } + + $this->signature_validated = true; + + switch ($this->signature_format) { + case 'ssh-dss': + $zero = new BigInteger(); + + if (strlen($server_public_host_key) < 4) { + return false; + } + $temp = unpack('Nlength', $this->_string_shift($server_public_host_key, 4)); + $p = new BigInteger($this->_string_shift($server_public_host_key, $temp['length']), -256); + + if (strlen($server_public_host_key) < 4) { + return false; + } + $temp = unpack('Nlength', $this->_string_shift($server_public_host_key, 4)); + $q = new BigInteger($this->_string_shift($server_public_host_key, $temp['length']), -256); + + if (strlen($server_public_host_key) < 4) { + return false; + } + $temp = unpack('Nlength', $this->_string_shift($server_public_host_key, 4)); + $g = new BigInteger($this->_string_shift($server_public_host_key, $temp['length']), -256); + + if (strlen($server_public_host_key) < 4) { + return false; + } + $temp = unpack('Nlength', $this->_string_shift($server_public_host_key, 4)); + $y = new BigInteger($this->_string_shift($server_public_host_key, $temp['length']), -256); + + /* The value for 'dss_signature_blob' is encoded as a string containing + r, followed by s (which are 160-bit integers, without lengths or + padding, unsigned, and in network byte order). */ + $temp = unpack('Nlength', $this->_string_shift($signature, 4)); + if ($temp['length'] != 40) { + user_error('Invalid signature'); + return $this->_disconnect(NET_SSH2_DISCONNECT_KEY_EXCHANGE_FAILED); + } + + $r = new BigInteger($this->_string_shift($signature, 20), 256); + $s = new BigInteger($this->_string_shift($signature, 20), 256); + + switch (true) { + case $r->equals($zero): + case $r->compare($q) >= 0: + case $s->equals($zero): + case $s->compare($q) >= 0: + user_error('Invalid signature'); + return $this->_disconnect(NET_SSH2_DISCONNECT_KEY_EXCHANGE_FAILED); + } + + $w = $s->modInverse($q); + + $u1 = $w->multiply(new BigInteger(sha1($this->exchange_hash), 16)); + list(, $u1) = $u1->divide($q); + + $u2 = $w->multiply($r); + list(, $u2) = $u2->divide($q); + + $g = $g->modPow($u1, $p); + $y = $y->modPow($u2, $p); + + $v = $g->multiply($y); + list(, $v) = $v->divide($p); + list(, $v) = $v->divide($q); + + if (!$v->equals($r)) { + user_error('Bad server signature'); + return $this->_disconnect(NET_SSH2_DISCONNECT_HOST_KEY_NOT_VERIFIABLE); + } + + break; + case 'ssh-rsa': + case 'rsa-sha2-256': + case 'rsa-sha2-512': + if (strlen($server_public_host_key) < 4) { + return false; + } + $temp = unpack('Nlength', $this->_string_shift($server_public_host_key, 4)); + $e = new BigInteger($this->_string_shift($server_public_host_key, $temp['length']), -256); + + if (strlen($server_public_host_key) < 4) { + return false; + } + $temp = unpack('Nlength', $this->_string_shift($server_public_host_key, 4)); + $rawN = $this->_string_shift($server_public_host_key, $temp['length']); + $n = new BigInteger($rawN, -256); + $nLength = strlen(ltrim($rawN, "\0")); + + /* + if (strlen($signature) < 4) { + return false; + } + $temp = unpack('Nlength', $this->_string_shift($signature, 4)); + $signature = $this->_string_shift($signature, $temp['length']); + + $rsa = new RSA(); + switch ($this->signature_format) { + case 'rsa-sha2-512': + $hash = 'sha512'; + break; + case 'rsa-sha2-256': + $hash = 'sha256'; + break; + //case 'ssh-rsa': + default: + $hash = 'sha1'; + } + $rsa->setHash($hash); + $rsa->setSignatureMode(RSA::SIGNATURE_PKCS1); + $rsa->loadKey(array('e' => $e, 'n' => $n), RSA::PUBLIC_FORMAT_RAW); + + if (!$rsa->verify($this->exchange_hash, $signature)) { + user_error('Bad server signature'); + return $this->_disconnect(NET_SSH2_DISCONNECT_HOST_KEY_NOT_VERIFIABLE); + } + */ + + if (strlen($signature) < 4) { + return false; + } + $temp = unpack('Nlength', $this->_string_shift($signature, 4)); + $s = new BigInteger($this->_string_shift($signature, $temp['length']), 256); + + // validate an RSA signature per "8.2 RSASSA-PKCS1-v1_5", "5.2.2 RSAVP1", and "9.1 EMSA-PSS" in the + // following URL: + // ftp://ftp.rsasecurity.com/pub/pkcs/pkcs-1/pkcs-1v2-1.pdf + + // also, see SSHRSA.c (rsa2_verifysig) in PuTTy's source. + + if ($s->compare(new BigInteger()) < 0 || $s->compare($n->subtract(new BigInteger(1))) > 0) { + user_error('Invalid signature'); + return $this->_disconnect(NET_SSH2_DISCONNECT_KEY_EXCHANGE_FAILED); + } + + $s = $s->modPow($e, $n); + $s = $s->toBytes(); + + switch ($this->signature_format) { + case 'rsa-sha2-512': + $hash = 'sha512'; + break; + case 'rsa-sha2-256': + $hash = 'sha256'; + break; + //case 'ssh-rsa': + default: + $hash = 'sha1'; + } + $hashObj = new Hash($hash); + switch ($this->signature_format) { + case 'rsa-sha2-512': + $h = pack('N5a*', 0x00305130, 0x0D060960, 0x86480165, 0x03040203, 0x05000440, $hashObj->hash($this->exchange_hash)); + break; + case 'rsa-sha2-256': + $h = pack('N5a*', 0x00303130, 0x0D060960, 0x86480165, 0x03040201, 0x05000420, $hashObj->hash($this->exchange_hash)); + break; + //case 'ssh-rsa': + default: + $hash = 'sha1'; + $h = pack('N4a*', 0x00302130, 0x0906052B, 0x0E03021A, 0x05000414, $hashObj->hash($this->exchange_hash)); + } + $h = chr(0x01) . str_repeat(chr(0xFF), $nLength - 2 - strlen($h)) . $h; + + if ($s != $h) { + user_error('Bad server signature'); + return $this->_disconnect(NET_SSH2_DISCONNECT_HOST_KEY_NOT_VERIFIABLE); + } + break; + default: + user_error('Unsupported signature format'); + return $this->_disconnect(NET_SSH2_DISCONNECT_HOST_KEY_NOT_VERIFIABLE); + } + + return $this->signature_format . ' ' . base64_encode($this->server_public_host_key); + } + + /** + * Returns the exit status of an SSH command or false. + * + * @return false|int + * @access public + */ + function getExitStatus() + { + if (is_null($this->exit_status)) { + return false; + } + return $this->exit_status; + } + + /** + * Returns the number of columns for the terminal window size. + * + * @return int + * @access public + */ + function getWindowColumns() + { + return $this->windowColumns; + } + + /** + * Returns the number of rows for the terminal window size. + * + * @return int + * @access public + */ + function getWindowRows() + { + return $this->windowRows; + } + + /** + * Sets the number of columns for the terminal window size. + * + * @param int $value + * @access public + */ + function setWindowColumns($value) + { + $this->windowColumns = $value; + } + + /** + * Sets the number of rows for the terminal window size. + * + * @param int $value + * @access public + */ + function setWindowRows($value) + { + $this->windowRows = $value; + } + + /** + * Sets the number of columns and rows for the terminal window size. + * + * @param int $columns + * @param int $rows + * @access public + */ + function setWindowSize($columns = 80, $rows = 24) + { + $this->windowColumns = $columns; + $this->windowRows = $rows; + } + + /** + * Update packet types in log history + * + * @param string $old + * @param string $new + * @access private + */ + function _updateLogHistory($old, $new) + { + if (defined('NET_SSH2_LOGGING') && NET_SSH2_LOGGING == self::LOG_COMPLEX) { + $this->message_number_log[count($this->message_number_log) - 1] = str_replace( + $old, + $new, + $this->message_number_log[count($this->message_number_log) - 1] + ); + } + } + + /** + * Return the list of authentication methods that may productively continue authentication. + * + * @see https://tools.ietf.org/html/rfc4252#section-5.1 + * @return array|null + */ + function getAuthMethodsToContinue() + { + return $this->auth_methods_to_continue; + } + + /** + * Enables "smart" multi-factor authentication (MFA) + */ + function enableSmartMFA() + { + $this->smartMFA = true; + } + + /** + * Disables "smart" multi-factor authentication (MFA) + */ + function disableSmartMFA() + { + $this->smartMFA = false; + } +} diff --git a/3rdparty/phpseclib/phpseclib/phpseclib/System/SSH/Agent.php b/3rdparty/phpseclib/phpseclib/phpseclib/System/SSH/Agent.php new file mode 100644 index 00000000..ec1d9773 --- /dev/null +++ b/3rdparty/phpseclib/phpseclib/phpseclib/System/SSH/Agent.php @@ -0,0 +1,361 @@ + + * login('username', $agent)) { + * exit('Login Failed'); + * } + * + * echo $ssh->exec('pwd'); + * echo $ssh->exec('ls -la'); + * ?> + * + * + * @category System + * @package SSH\Agent + * @author Jim Wigginton + * @copyright 2014 Jim Wigginton + * @license http://www.opensource.org/licenses/mit-license.html MIT License + * @link http://phpseclib.sourceforge.net + * @internal See http://api.libssh.org/rfc/PROTOCOL.agent + */ + +namespace phpseclib\System\SSH; + +use phpseclib\Crypt\RSA; +use phpseclib\System\SSH\Agent\Identity; + +/** + * Pure-PHP ssh-agent client identity factory + * + * requestIdentities() method pumps out \phpseclib\System\SSH\Agent\Identity objects + * + * @package SSH\Agent + * @author Jim Wigginton + * @access public + */ +class Agent +{ + /**#@+ + * Message numbers + * + * @access private + */ + // to request SSH1 keys you have to use SSH_AGENTC_REQUEST_RSA_IDENTITIES (1) + const SSH_AGENTC_REQUEST_IDENTITIES = 11; + // this is the SSH2 response; the SSH1 response is SSH_AGENT_RSA_IDENTITIES_ANSWER (2). + const SSH_AGENT_IDENTITIES_ANSWER = 12; + // the SSH1 request is SSH_AGENTC_RSA_CHALLENGE (3) + const SSH_AGENTC_SIGN_REQUEST = 13; + // the SSH1 response is SSH_AGENT_RSA_RESPONSE (4) + const SSH_AGENT_SIGN_RESPONSE = 14; + /**#@-*/ + + /**@+ + * Agent forwarding status + * + * @access private + */ + // no forwarding requested and not active + const FORWARD_NONE = 0; + // request agent forwarding when opportune + const FORWARD_REQUEST = 1; + // forwarding has been request and is active + const FORWARD_ACTIVE = 2; + /**#@-*/ + + /** + * Unused + */ + const SSH_AGENT_FAILURE = 5; + + /** + * Socket Resource + * + * @var resource + * @access private + */ + var $fsock; + + /** + * Agent forwarding status + * + * @access private + */ + var $forward_status = self::FORWARD_NONE; + + /** + * Buffer for accumulating forwarded authentication + * agent data arriving on SSH data channel destined + * for agent unix socket + * + * @access private + */ + var $socket_buffer = ''; + + /** + * Tracking the number of bytes we are expecting + * to arrive for the agent socket on the SSH data + * channel + */ + var $expected_bytes = 0; + + /** + * Default Constructor + * + * @return \phpseclib\System\SSH\Agent + * @access public + */ + function __construct($address = null) + { + if (!$address) { + switch (true) { + case isset($_SERVER['SSH_AUTH_SOCK']): + $address = $_SERVER['SSH_AUTH_SOCK']; + break; + case isset($_ENV['SSH_AUTH_SOCK']): + $address = $_ENV['SSH_AUTH_SOCK']; + break; + default: + user_error('SSH_AUTH_SOCK not found'); + return false; + } + } + + if (in_array('unix', stream_get_transports())) { + $this->fsock = fsockopen('unix://' . $address, 0, $errno, $errstr); + if (!$this->fsock) { + user_error("Unable to connect to ssh-agent (Error $errno: $errstr)"); + } + } else { + if (substr($address, 0, 9) != '\\\\.\\pipe\\' || strpos(substr($address, 9), '\\') !== false) { + user_error('Address is not formatted as a named pipe should be'); + } else { + $this->fsock = fopen($address, 'r+b'); + if (!$this->fsock) { + user_error('Unable to open address'); + } + } + } + } + + /** + * Request Identities + * + * See "2.5.2 Requesting a list of protocol 2 keys" + * Returns an array containing zero or more \phpseclib\System\SSH\Agent\Identity objects + * + * @return array + * @access public + */ + function requestIdentities() + { + if (!$this->fsock) { + return array(); + } + + $packet = pack('NC', 1, self::SSH_AGENTC_REQUEST_IDENTITIES); + if (strlen($packet) != fputs($this->fsock, $packet)) { + user_error('Connection closed while requesting identities'); + return array(); + } + + $temp = fread($this->fsock, 4); + if (strlen($temp) != 4) { + user_error('Connection closed while requesting identities'); + return array(); + } + $length = current(unpack('N', $temp)); + $type = ord(fread($this->fsock, 1)); + if ($type != self::SSH_AGENT_IDENTITIES_ANSWER) { + user_error('Unable to request identities'); + return array(); + } + + $identities = array(); + $temp = fread($this->fsock, 4); + if (strlen($temp) != 4) { + user_error('Connection closed while requesting identities'); + return array(); + } + $keyCount = current(unpack('N', $temp)); + for ($i = 0; $i < $keyCount; $i++) { + $temp = fread($this->fsock, 4); + if (strlen($temp) != 4) { + user_error('Connection closed while requesting identities'); + return array(); + } + $length = current(unpack('N', $temp)); + $key_blob = fread($this->fsock, $length); + if (strlen($key_blob) != $length) { + user_error('Connection closed while requesting identities'); + return array(); + } + $key_str = 'ssh-rsa ' . base64_encode($key_blob); + $temp = fread($this->fsock, 4); + if (strlen($temp) != 4) { + user_error('Connection closed while requesting identities'); + return array(); + } + $length = current(unpack('N', $temp)); + if ($length) { + $temp = fread($this->fsock, $length); + if (strlen($temp) != $length) { + user_error('Connection closed while requesting identities'); + return array(); + } + $key_str.= ' ' . $temp; + } + $length = current(unpack('N', substr($key_blob, 0, 4))); + $key_type = substr($key_blob, 4, $length); + switch ($key_type) { + case 'ssh-rsa': + $key = new RSA(); + $key->loadKey($key_str); + break; + case 'ssh-dss': + // not currently supported + break; + } + // resources are passed by reference by default + if (isset($key)) { + $identity = new Identity($this->fsock); + $identity->setPublicKey($key); + $identity->setPublicKeyBlob($key_blob); + $identities[] = $identity; + unset($key); + } + } + + return $identities; + } + + /** + * Signal that agent forwarding should + * be requested when a channel is opened + * + * @return bool + * @access public + */ + function startSSHForwarding() + { + if ($this->forward_status == self::FORWARD_NONE) { + $this->forward_status = self::FORWARD_REQUEST; + } + } + + /** + * Request agent forwarding of remote server + * + * @param Net_SSH2 $ssh + * @return bool + * @access private + */ + function _request_forwarding($ssh) + { + $request_channel = $ssh->_get_open_channel(); + if ($request_channel === false) { + return false; + } + + $packet = pack( + 'CNNa*C', + NET_SSH2_MSG_CHANNEL_REQUEST, + $ssh->server_channels[$request_channel], + strlen('auth-agent-req@openssh.com'), + 'auth-agent-req@openssh.com', + 1 + ); + + $ssh->channel_status[$request_channel] = NET_SSH2_MSG_CHANNEL_REQUEST; + + if (!$ssh->_send_binary_packet($packet)) { + return false; + } + + $response = $ssh->_get_channel_packet($request_channel); + if ($response === false) { + return false; + } + + $ssh->channel_status[$request_channel] = NET_SSH2_MSG_CHANNEL_OPEN; + $this->forward_status = self::FORWARD_ACTIVE; + + return true; + } + + /** + * On successful channel open + * + * This method is called upon successful channel + * open to give the SSH Agent an opportunity + * to take further action. i.e. request agent forwarding + * + * @param Net_SSH2 $ssh + * @access private + */ + function _on_channel_open($ssh) + { + if ($this->forward_status == self::FORWARD_REQUEST) { + $this->_request_forwarding($ssh); + } + } + + /** + * Forward data to SSH Agent and return data reply + * + * @param string $data + * @return data from SSH Agent + * @access private + */ + function _forward_data($data) + { + if ($this->expected_bytes > 0) { + $this->socket_buffer.= $data; + $this->expected_bytes -= strlen($data); + } else { + $agent_data_bytes = current(unpack('N', $data)); + $current_data_bytes = strlen($data); + $this->socket_buffer = $data; + if ($current_data_bytes != $agent_data_bytes + 4) { + $this->expected_bytes = ($agent_data_bytes + 4) - $current_data_bytes; + return false; + } + } + + if (strlen($this->socket_buffer) != fwrite($this->fsock, $this->socket_buffer)) { + user_error('Connection closed attempting to forward data to SSH agent'); + return false; + } + + $this->socket_buffer = ''; + $this->expected_bytes = 0; + + $temp = fread($this->fsock, 4); + if (strlen($temp) != 4) { + user_error('Connection closed while reading data response'); + return false; + } + $agent_reply_bytes = current(unpack('N', $temp)); + + $agent_reply_data = fread($this->fsock, $agent_reply_bytes); + if (strlen($agent_reply_data) != $agent_reply_bytes) { + user_error('Connection closed while reading data response'); + return false; + } + $agent_reply_data = current(unpack('a*', $agent_reply_data)); + + return pack('Na*', $agent_reply_bytes, $agent_reply_data); + } +} diff --git a/3rdparty/phpseclib/phpseclib/phpseclib/System/SSH/Agent/Identity.php b/3rdparty/phpseclib/phpseclib/phpseclib/System/SSH/Agent/Identity.php new file mode 100644 index 00000000..68b6bfdf --- /dev/null +++ b/3rdparty/phpseclib/phpseclib/phpseclib/System/SSH/Agent/Identity.php @@ -0,0 +1,241 @@ + + * @copyright 2009 Jim Wigginton + * @license http://www.opensource.org/licenses/mit-license.html MIT License + * @link http://phpseclib.sourceforge.net + * @internal See http://api.libssh.org/rfc/PROTOCOL.agent + */ + +namespace phpseclib\System\SSH\Agent; + +use phpseclib\System\SSH\Agent; + +/** + * Pure-PHP ssh-agent client identity object + * + * Instantiation should only be performed by \phpseclib\System\SSH\Agent class. + * This could be thought of as implementing an interface that phpseclib\Crypt\RSA + * implements. ie. maybe a Net_SSH_Auth_PublicKey interface or something. + * The methods in this interface would be getPublicKey and sign since those are the + * methods phpseclib looks for to perform public key authentication. + * + * @package SSH\Agent + * @author Jim Wigginton + * @access internal + */ +class Identity +{ + /**@+ + * Signature Flags + * + * See https://tools.ietf.org/html/draft-miller-ssh-agent-00#section-5.3 + * + * @access private + */ + const SSH_AGENT_RSA2_256 = 2; + const SSH_AGENT_RSA2_512 = 4; + /**#@-*/ + + /** + * Key Object + * + * @var \phpseclib\Crypt\RSA + * @access private + * @see self::getPublicKey() + */ + var $key; + + /** + * Key Blob + * + * @var string + * @access private + * @see self::sign() + */ + var $key_blob; + + /** + * Socket Resource + * + * @var resource + * @access private + * @see self::sign() + */ + var $fsock; + + /** + * Signature flags + * + * @var int + * @access private + * @see self::sign() + * @see self::setHash() + */ + var $flags = 0; + + /** + * Default Constructor. + * + * @param resource $fsock + * @return \phpseclib\System\SSH\Agent\Identity + * @access private + */ + function __construct($fsock) + { + $this->fsock = $fsock; + } + + /** + * Set Public Key + * + * Called by \phpseclib\System\SSH\Agent::requestIdentities() + * + * @param \phpseclib\Crypt\RSA $key + * @access private + */ + function setPublicKey($key) + { + $this->key = $key; + $this->key->setPublicKey(); + } + + /** + * Set Public Key + * + * Called by \phpseclib\System\SSH\Agent::requestIdentities(). The key blob could be extracted from $this->key + * but this saves a small amount of computation. + * + * @param string $key_blob + * @access private + */ + function setPublicKeyBlob($key_blob) + { + $this->key_blob = $key_blob; + } + + /** + * Get Public Key + * + * Wrapper for $this->key->getPublicKey() + * + * @param int $format optional + * @return mixed + * @access public + */ + function getPublicKey($format = null) + { + return !isset($format) ? $this->key->getPublicKey() : $this->key->getPublicKey($format); + } + + /** + * Set Signature Mode + * + * Doesn't do anything as ssh-agent doesn't let you pick and choose the signature mode. ie. + * ssh-agent's only supported mode is \phpseclib\Crypt\RSA::SIGNATURE_PKCS1 + * + * @param int $mode + * @access public + */ + function setSignatureMode($mode) + { + } + + /** + * Set Hash + * + * ssh-agent doesn't support using hashes for RSA other than SHA1 + * + * @param string $hash + * @access public + */ + function setHash($hash) + { + $this->flags = 0; + switch ($hash) { + case 'sha1': + break; + case 'sha256': + $this->flags = self::SSH_AGENT_RSA2_256; + break; + case 'sha512': + $this->flags = self::SSH_AGENT_RSA2_512; + break; + default: + user_error('The only supported hashes for RSA are sha1, sha256 and sha512'); + } + } + + /** + * Create a signature + * + * See "2.6.2 Protocol 2 private key signature request" + * + * @param string $message + * @return string + * @access public + */ + function sign($message) + { + // the last parameter (currently 0) is for flags and ssh-agent only defines one flag (for ssh-dss): SSH_AGENT_OLD_SIGNATURE + $packet = pack('CNa*Na*N', Agent::SSH_AGENTC_SIGN_REQUEST, strlen($this->key_blob), $this->key_blob, strlen($message), $message, $this->flags); + $packet = pack('Na*', strlen($packet), $packet); + if (strlen($packet) != fputs($this->fsock, $packet)) { + user_error('Connection closed during signing'); + return false; + } + + $temp = fread($this->fsock, 4); + if (strlen($temp) != 4) { + user_error('Connection closed during signing'); + return false; + } + $length = current(unpack('N', $temp)); + $type = ord(fread($this->fsock, 1)); + if ($type != Agent::SSH_AGENT_SIGN_RESPONSE) { + user_error('Unable to retrieve signature'); + return false; + } + + $signature_blob = fread($this->fsock, $length - 1); + if (strlen($signature_blob) != $length - 1) { + user_error('Connection closed during signing'); + return false; + } + $length = current(unpack('N', $this->_string_shift($signature_blob, 4))); + if ($length != strlen($signature_blob)) { + user_error('Malformed signature blob'); + } + $length = current(unpack('N', $this->_string_shift($signature_blob, 4))); + if ($length > strlen($signature_blob) + 4) { + user_error('Malformed signature blob'); + } + $type = $this->_string_shift($signature_blob, $length); + $this->_string_shift($signature_blob, 4); + + return $signature_blob; + } + + /** + * String Shift + * + * Inspired by array_shift + * + * @param string $string + * @param int $index + * @return string + * @access private + */ + function _string_shift(&$string, $index = 1) + { + $substr = substr($string, 0, $index); + $string = substr($string, $index); + return $substr; + } +} diff --git a/3rdparty/phpseclib/phpseclib/phpseclib/bootstrap.php b/3rdparty/phpseclib/phpseclib/phpseclib/bootstrap.php new file mode 100644 index 00000000..547688f9 --- /dev/null +++ b/3rdparty/phpseclib/phpseclib/phpseclib/bootstrap.php @@ -0,0 +1,17 @@ +factories = new \SplObjectStorage(); + $this->protected = new \SplObjectStorage(); + + foreach ($values as $key => $value) { + $this->offsetSet($key, $value); + } + } + + /** + * Sets a parameter or an object. + * + * Objects must be defined as Closures. + * + * Allowing any PHP callable leads to difficult to debug problems + * as function names (strings) are callable (creating a function with + * the same name as an existing parameter would break your container). + * + * @param string $id The unique identifier for the parameter or object + * @param mixed $value The value of the parameter or a closure to define an object + * + * @return void + * + * @throws FrozenServiceException Prevent override of a frozen service + */ + #[\ReturnTypeWillChange] + public function offsetSet($id, $value) + { + if (isset($this->frozen[$id])) { + throw new FrozenServiceException($id); + } + + $this->values[$id] = $value; + $this->keys[$id] = true; + } + + /** + * Gets a parameter or an object. + * + * @param string $id The unique identifier for the parameter or object + * + * @return mixed The value of the parameter or an object + * + * @throws UnknownIdentifierException If the identifier is not defined + */ + #[\ReturnTypeWillChange] + public function offsetGet($id) + { + if (!isset($this->keys[$id])) { + throw new UnknownIdentifierException($id); + } + + if ( + isset($this->raw[$id]) + || !\is_object($this->values[$id]) + || isset($this->protected[$this->values[$id]]) + || !\method_exists($this->values[$id], '__invoke') + ) { + return $this->values[$id]; + } + + if (isset($this->factories[$this->values[$id]])) { + return $this->values[$id]($this); + } + + $raw = $this->values[$id]; + $val = $this->values[$id] = $raw($this); + $this->raw[$id] = $raw; + + $this->frozen[$id] = true; + + return $val; + } + + /** + * Checks if a parameter or an object is set. + * + * @param string $id The unique identifier for the parameter or object + * + * @return bool + */ + #[\ReturnTypeWillChange] + public function offsetExists($id) + { + return isset($this->keys[$id]); + } + + /** + * Unsets a parameter or an object. + * + * @param string $id The unique identifier for the parameter or object + * + * @return void + */ + #[\ReturnTypeWillChange] + public function offsetUnset($id) + { + if (isset($this->keys[$id])) { + if (\is_object($this->values[$id])) { + unset($this->factories[$this->values[$id]], $this->protected[$this->values[$id]]); + } + + unset($this->values[$id], $this->frozen[$id], $this->raw[$id], $this->keys[$id]); + } + } + + /** + * Marks a callable as being a factory service. + * + * @param callable $callable A service definition to be used as a factory + * + * @return callable The passed callable + * + * @throws ExpectedInvokableException Service definition has to be a closure or an invokable object + */ + public function factory($callable) + { + if (!\is_object($callable) || !\method_exists($callable, '__invoke')) { + throw new ExpectedInvokableException('Service definition is not a Closure or invokable object.'); + } + + $this->factories->attach($callable); + + return $callable; + } + + /** + * Protects a callable from being interpreted as a service. + * + * This is useful when you want to store a callable as a parameter. + * + * @param callable $callable A callable to protect from being evaluated + * + * @return callable The passed callable + * + * @throws ExpectedInvokableException Service definition has to be a closure or an invokable object + */ + public function protect($callable) + { + if (!\is_object($callable) || !\method_exists($callable, '__invoke')) { + throw new ExpectedInvokableException('Callable is not a Closure or invokable object.'); + } + + $this->protected->attach($callable); + + return $callable; + } + + /** + * Gets a parameter or the closure defining an object. + * + * @param string $id The unique identifier for the parameter or object + * + * @return mixed The value of the parameter or the closure defining an object + * + * @throws UnknownIdentifierException If the identifier is not defined + */ + public function raw($id) + { + if (!isset($this->keys[$id])) { + throw new UnknownIdentifierException($id); + } + + if (isset($this->raw[$id])) { + return $this->raw[$id]; + } + + return $this->values[$id]; + } + + /** + * Extends an object definition. + * + * Useful when you want to extend an existing object definition, + * without necessarily loading that object. + * + * @param string $id The unique identifier for the object + * @param callable $callable A service definition to extend the original + * + * @return callable The wrapped callable + * + * @throws UnknownIdentifierException If the identifier is not defined + * @throws FrozenServiceException If the service is frozen + * @throws InvalidServiceIdentifierException If the identifier belongs to a parameter + * @throws ExpectedInvokableException If the extension callable is not a closure or an invokable object + */ + public function extend($id, $callable) + { + if (!isset($this->keys[$id])) { + throw new UnknownIdentifierException($id); + } + + if (isset($this->frozen[$id])) { + throw new FrozenServiceException($id); + } + + if (!\is_object($this->values[$id]) || !\method_exists($this->values[$id], '__invoke')) { + throw new InvalidServiceIdentifierException($id); + } + + if (isset($this->protected[$this->values[$id]])) { + @\trigger_error(\sprintf('How Pimple behaves when extending protected closures will be fixed in Pimple 4. Are you sure "%s" should be protected?', $id), E_USER_DEPRECATED); + } + + if (!\is_object($callable) || !\method_exists($callable, '__invoke')) { + throw new ExpectedInvokableException('Extension service definition is not a Closure or invokable object.'); + } + + $factory = $this->values[$id]; + + $extended = function ($c) use ($callable, $factory) { + return $callable($factory($c), $c); + }; + + if (isset($this->factories[$factory])) { + $this->factories->detach($factory); + $this->factories->attach($extended); + } + + return $this[$id] = $extended; + } + + /** + * Returns all defined value names. + * + * @return array An array of value names + */ + public function keys() + { + return \array_keys($this->values); + } + + /** + * Registers a service provider. + * + * @param array $values An array of values that customizes the provider + * + * @return static + */ + public function register(ServiceProviderInterface $provider, array $values = []) + { + $provider->register($this); + + foreach ($values as $key => $value) { + $this[$key] = $value; + } + + return $this; + } +} diff --git a/3rdparty/pimple/pimple/src/Pimple/Exception/ExpectedInvokableException.php b/3rdparty/pimple/pimple/src/Pimple/Exception/ExpectedInvokableException.php new file mode 100644 index 00000000..7228421b --- /dev/null +++ b/3rdparty/pimple/pimple/src/Pimple/Exception/ExpectedInvokableException.php @@ -0,0 +1,38 @@ + + */ +class ExpectedInvokableException extends \InvalidArgumentException implements ContainerExceptionInterface +{ +} diff --git a/3rdparty/pimple/pimple/src/Pimple/Exception/FrozenServiceException.php b/3rdparty/pimple/pimple/src/Pimple/Exception/FrozenServiceException.php new file mode 100644 index 00000000..e4d2f6d3 --- /dev/null +++ b/3rdparty/pimple/pimple/src/Pimple/Exception/FrozenServiceException.php @@ -0,0 +1,45 @@ + + */ +class FrozenServiceException extends \RuntimeException implements ContainerExceptionInterface +{ + /** + * @param string $id Identifier of the frozen service + */ + public function __construct($id) + { + parent::__construct(\sprintf('Cannot override frozen service "%s".', $id)); + } +} diff --git a/3rdparty/pimple/pimple/src/Pimple/Exception/InvalidServiceIdentifierException.php b/3rdparty/pimple/pimple/src/Pimple/Exception/InvalidServiceIdentifierException.php new file mode 100644 index 00000000..91e82f98 --- /dev/null +++ b/3rdparty/pimple/pimple/src/Pimple/Exception/InvalidServiceIdentifierException.php @@ -0,0 +1,45 @@ + + */ +class InvalidServiceIdentifierException extends \InvalidArgumentException implements NotFoundExceptionInterface +{ + /** + * @param string $id The invalid identifier + */ + public function __construct($id) + { + parent::__construct(\sprintf('Identifier "%s" does not contain an object definition.', $id)); + } +} diff --git a/3rdparty/pimple/pimple/src/Pimple/Exception/UnknownIdentifierException.php b/3rdparty/pimple/pimple/src/Pimple/Exception/UnknownIdentifierException.php new file mode 100644 index 00000000..fb6b626e --- /dev/null +++ b/3rdparty/pimple/pimple/src/Pimple/Exception/UnknownIdentifierException.php @@ -0,0 +1,45 @@ + + */ +class UnknownIdentifierException extends \InvalidArgumentException implements NotFoundExceptionInterface +{ + /** + * @param string $id The unknown identifier + */ + public function __construct($id) + { + parent::__construct(\sprintf('Identifier "%s" is not defined.', $id)); + } +} diff --git a/3rdparty/pimple/pimple/src/Pimple/Psr11/Container.php b/3rdparty/pimple/pimple/src/Pimple/Psr11/Container.php new file mode 100644 index 00000000..e18592eb --- /dev/null +++ b/3rdparty/pimple/pimple/src/Pimple/Psr11/Container.php @@ -0,0 +1,55 @@ + + */ +final class Container implements ContainerInterface +{ + private $pimple; + + public function __construct(PimpleContainer $pimple) + { + $this->pimple = $pimple; + } + + public function get(string $id) + { + return $this->pimple[$id]; + } + + public function has(string $id): bool + { + return isset($this->pimple[$id]); + } +} diff --git a/3rdparty/pimple/pimple/src/Pimple/Psr11/ServiceLocator.php b/3rdparty/pimple/pimple/src/Pimple/Psr11/ServiceLocator.php new file mode 100644 index 00000000..714b8826 --- /dev/null +++ b/3rdparty/pimple/pimple/src/Pimple/Psr11/ServiceLocator.php @@ -0,0 +1,75 @@ + + */ +class ServiceLocator implements ContainerInterface +{ + private $container; + private $aliases = []; + + /** + * @param PimpleContainer $container The Container instance used to locate services + * @param array $ids Array of service ids that can be located. String keys can be used to define aliases + */ + public function __construct(PimpleContainer $container, array $ids) + { + $this->container = $container; + + foreach ($ids as $key => $id) { + $this->aliases[\is_int($key) ? $id : $key] = $id; + } + } + + /** + * {@inheritdoc} + */ + public function get(string $id) + { + if (!isset($this->aliases[$id])) { + throw new UnknownIdentifierException($id); + } + + return $this->container[$this->aliases[$id]]; + } + + /** + * {@inheritdoc} + */ + public function has(string $id): bool + { + return isset($this->aliases[$id]) && isset($this->container[$this->aliases[$id]]); + } +} diff --git a/3rdparty/pimple/pimple/src/Pimple/ServiceIterator.php b/3rdparty/pimple/pimple/src/Pimple/ServiceIterator.php new file mode 100644 index 00000000..ebafac16 --- /dev/null +++ b/3rdparty/pimple/pimple/src/Pimple/ServiceIterator.php @@ -0,0 +1,89 @@ + + */ +final class ServiceIterator implements \Iterator +{ + private $container; + private $ids; + + public function __construct(Container $container, array $ids) + { + $this->container = $container; + $this->ids = $ids; + } + + /** + * @return void + */ + #[\ReturnTypeWillChange] + public function rewind() + { + \reset($this->ids); + } + + /** + * @return mixed + */ + #[\ReturnTypeWillChange] + public function current() + { + return $this->container[\current($this->ids)]; + } + + /** + * @return mixed + */ + #[\ReturnTypeWillChange] + public function key() + { + return \current($this->ids); + } + + /** + * @return void + */ + #[\ReturnTypeWillChange] + public function next() + { + \next($this->ids); + } + + /** + * @return bool + */ + #[\ReturnTypeWillChange] + public function valid() + { + return null !== \key($this->ids); + } +} diff --git a/3rdparty/pimple/pimple/src/Pimple/ServiceProviderInterface.php b/3rdparty/pimple/pimple/src/Pimple/ServiceProviderInterface.php new file mode 100644 index 00000000..abf90d82 --- /dev/null +++ b/3rdparty/pimple/pimple/src/Pimple/ServiceProviderInterface.php @@ -0,0 +1,44 @@ +getHeaders() as $name => $values) { + * echo $name . ": " . implode(", ", $values); + * } + * + * // Emit headers iteratively: + * foreach ($message->getHeaders() as $name => $values) { + * foreach ($values as $value) { + * header(sprintf('%s: %s', $name, $value), false); + * } + * } + * + * While header names are not case-sensitive, getHeaders() will preserve the + * exact case in which headers were originally specified. + * + * @return string[][] Returns an associative array of the message's headers. Each + * key MUST be a header name, and each value MUST be an array of strings + * for that header. + */ + public function getHeaders(): array; + + /** + * Checks if a header exists by the given case-insensitive name. + * + * @param string $name Case-insensitive header field name. + * @return bool Returns true if any header names match the given header + * name using a case-insensitive string comparison. Returns false if + * no matching header name is found in the message. + */ + public function hasHeader(string $name): bool; + + /** + * Retrieves a message header value by the given case-insensitive name. + * + * This method returns an array of all the header values of the given + * case-insensitive header name. + * + * If the header does not appear in the message, this method MUST return an + * empty array. + * + * @param string $name Case-insensitive header field name. + * @return string[] An array of string values as provided for the given + * header. If the header does not appear in the message, this method MUST + * return an empty array. + */ + public function getHeader(string $name): array; + + /** + * Retrieves a comma-separated string of the values for a single header. + * + * This method returns all of the header values of the given + * case-insensitive header name as a string concatenated together using + * a comma. + * + * NOTE: Not all header values may be appropriately represented using + * comma concatenation. For such headers, use getHeader() instead + * and supply your own delimiter when concatenating. + * + * If the header does not appear in the message, this method MUST return + * an empty string. + * + * @param string $name Case-insensitive header field name. + * @return string A string of values as provided for the given header + * concatenated together using a comma. If the header does not appear in + * the message, this method MUST return an empty string. + */ + public function getHeaderLine(string $name): string; + + /** + * Return an instance with the provided value replacing the specified header. + * + * While header names are case-insensitive, the casing of the header will + * be preserved by this function, and returned from getHeaders(). + * + * This method MUST be implemented in such a way as to retain the + * immutability of the message, and MUST return an instance that has the + * new and/or updated header and value. + * + * @param string $name Case-insensitive header field name. + * @param string|string[] $value Header value(s). + * @return static + * @throws \InvalidArgumentException for invalid header names or values. + */ + public function withHeader(string $name, $value): MessageInterface; + + /** + * Return an instance with the specified header appended with the given value. + * + * Existing values for the specified header will be maintained. The new + * value(s) will be appended to the existing list. If the header did not + * exist previously, it will be added. + * + * This method MUST be implemented in such a way as to retain the + * immutability of the message, and MUST return an instance that has the + * new header and/or value. + * + * @param string $name Case-insensitive header field name to add. + * @param string|string[] $value Header value(s). + * @return static + * @throws \InvalidArgumentException for invalid header names or values. + */ + public function withAddedHeader(string $name, $value): MessageInterface; + + /** + * Return an instance without the specified header. + * + * Header resolution MUST be done without case-sensitivity. + * + * This method MUST be implemented in such a way as to retain the + * immutability of the message, and MUST return an instance that removes + * the named header. + * + * @param string $name Case-insensitive header field name to remove. + * @return static + */ + public function withoutHeader(string $name): MessageInterface; + + /** + * Gets the body of the message. + * + * @return StreamInterface Returns the body as a stream. + */ + public function getBody(): StreamInterface; + + /** + * Return an instance with the specified message body. + * + * The body MUST be a StreamInterface object. + * + * This method MUST be implemented in such a way as to retain the + * immutability of the message, and MUST return a new instance that has the + * new body stream. + * + * @param StreamInterface $body Body. + * @return static + * @throws \InvalidArgumentException When the body is not valid. + */ + public function withBody(StreamInterface $body): MessageInterface; +} diff --git a/3rdparty/psr/http-message/src/RequestInterface.php b/3rdparty/psr/http-message/src/RequestInterface.php new file mode 100644 index 00000000..33f85e55 --- /dev/null +++ b/3rdparty/psr/http-message/src/RequestInterface.php @@ -0,0 +1,130 @@ +getQuery()` + * or from the `QUERY_STRING` server param. + * + * @return array + */ + public function getQueryParams(): array; + + /** + * Return an instance with the specified query string arguments. + * + * These values SHOULD remain immutable over the course of the incoming + * request. They MAY be injected during instantiation, such as from PHP's + * $_GET superglobal, or MAY be derived from some other value such as the + * URI. In cases where the arguments are parsed from the URI, the data + * MUST be compatible with what PHP's parse_str() would return for + * purposes of how duplicate query parameters are handled, and how nested + * sets are handled. + * + * Setting query string arguments MUST NOT change the URI stored by the + * request, nor the values in the server params. + * + * This method MUST be implemented in such a way as to retain the + * immutability of the message, and MUST return an instance that has the + * updated query string arguments. + * + * @param array $query Array of query string arguments, typically from + * $_GET. + * @return static + */ + public function withQueryParams(array $query): ServerRequestInterface; + + /** + * Retrieve normalized file upload data. + * + * This method returns upload metadata in a normalized tree, with each leaf + * an instance of Psr\Http\Message\UploadedFileInterface. + * + * These values MAY be prepared from $_FILES or the message body during + * instantiation, or MAY be injected via withUploadedFiles(). + * + * @return array An array tree of UploadedFileInterface instances; an empty + * array MUST be returned if no data is present. + */ + public function getUploadedFiles(): array; + + /** + * Create a new instance with the specified uploaded files. + * + * This method MUST be implemented in such a way as to retain the + * immutability of the message, and MUST return an instance that has the + * updated body parameters. + * + * @param array $uploadedFiles An array tree of UploadedFileInterface instances. + * @return static + * @throws \InvalidArgumentException if an invalid structure is provided. + */ + public function withUploadedFiles(array $uploadedFiles): ServerRequestInterface; + + /** + * Retrieve any parameters provided in the request body. + * + * If the request Content-Type is either application/x-www-form-urlencoded + * or multipart/form-data, and the request method is POST, this method MUST + * return the contents of $_POST. + * + * Otherwise, this method may return any results of deserializing + * the request body content; as parsing returns structured content, the + * potential types MUST be arrays or objects only. A null value indicates + * the absence of body content. + * + * @return null|array|object The deserialized body parameters, if any. + * These will typically be an array or object. + */ + public function getParsedBody(); + + /** + * Return an instance with the specified body parameters. + * + * These MAY be injected during instantiation. + * + * If the request Content-Type is either application/x-www-form-urlencoded + * or multipart/form-data, and the request method is POST, use this method + * ONLY to inject the contents of $_POST. + * + * The data IS NOT REQUIRED to come from $_POST, but MUST be the results of + * deserializing the request body content. Deserialization/parsing returns + * structured data, and, as such, this method ONLY accepts arrays or objects, + * or a null value if nothing was available to parse. + * + * As an example, if content negotiation determines that the request data + * is a JSON payload, this method could be used to create a request + * instance with the deserialized parameters. + * + * This method MUST be implemented in such a way as to retain the + * immutability of the message, and MUST return an instance that has the + * updated body parameters. + * + * @param null|array|object $data The deserialized body data. This will + * typically be in an array or object. + * @return static + * @throws \InvalidArgumentException if an unsupported argument type is + * provided. + */ + public function withParsedBody($data): ServerRequestInterface; + + /** + * Retrieve attributes derived from the request. + * + * The request "attributes" may be used to allow injection of any + * parameters derived from the request: e.g., the results of path + * match operations; the results of decrypting cookies; the results of + * deserializing non-form-encoded message bodies; etc. Attributes + * will be application and request specific, and CAN be mutable. + * + * @return array Attributes derived from the request. + */ + public function getAttributes(): array; + + /** + * Retrieve a single derived request attribute. + * + * Retrieves a single derived request attribute as described in + * getAttributes(). If the attribute has not been previously set, returns + * the default value as provided. + * + * This method obviates the need for a hasAttribute() method, as it allows + * specifying a default value to return if the attribute is not found. + * + * @see getAttributes() + * @param string $name The attribute name. + * @param mixed $default Default value to return if the attribute does not exist. + * @return mixed + */ + public function getAttribute(string $name, $default = null); + + /** + * Return an instance with the specified derived request attribute. + * + * This method allows setting a single derived request attribute as + * described in getAttributes(). + * + * This method MUST be implemented in such a way as to retain the + * immutability of the message, and MUST return an instance that has the + * updated attribute. + * + * @see getAttributes() + * @param string $name The attribute name. + * @param mixed $value The value of the attribute. + * @return static + */ + public function withAttribute(string $name, $value): ServerRequestInterface; + + /** + * Return an instance that removes the specified derived request attribute. + * + * This method allows removing a single derived request attribute as + * described in getAttributes(). + * + * This method MUST be implemented in such a way as to retain the + * immutability of the message, and MUST return an instance that removes + * the attribute. + * + * @see getAttributes() + * @param string $name The attribute name. + * @return static + */ + public function withoutAttribute(string $name): ServerRequestInterface; +} diff --git a/3rdparty/psr/http-message/src/StreamInterface.php b/3rdparty/psr/http-message/src/StreamInterface.php new file mode 100644 index 00000000..a62aabb8 --- /dev/null +++ b/3rdparty/psr/http-message/src/StreamInterface.php @@ -0,0 +1,158 @@ + + * [user-info@]host[:port] + * + * + * If the port component is not set or is the standard port for the current + * scheme, it SHOULD NOT be included. + * + * @see https://tools.ietf.org/html/rfc3986#section-3.2 + * @return string The URI authority, in "[user-info@]host[:port]" format. + */ + public function getAuthority(): string; + + /** + * Retrieve the user information component of the URI. + * + * If no user information is present, this method MUST return an empty + * string. + * + * If a user is present in the URI, this will return that value; + * additionally, if the password is also present, it will be appended to the + * user value, with a colon (":") separating the values. + * + * The trailing "@" character is not part of the user information and MUST + * NOT be added. + * + * @return string The URI user information, in "username[:password]" format. + */ + public function getUserInfo(): string; + + /** + * Retrieve the host component of the URI. + * + * If no host is present, this method MUST return an empty string. + * + * The value returned MUST be normalized to lowercase, per RFC 3986 + * Section 3.2.2. + * + * @see http://tools.ietf.org/html/rfc3986#section-3.2.2 + * @return string The URI host. + */ + public function getHost(): string; + + /** + * Retrieve the port component of the URI. + * + * If a port is present, and it is non-standard for the current scheme, + * this method MUST return it as an integer. If the port is the standard port + * used with the current scheme, this method SHOULD return null. + * + * If no port is present, and no scheme is present, this method MUST return + * a null value. + * + * If no port is present, but a scheme is present, this method MAY return + * the standard port for that scheme, but SHOULD return null. + * + * @return null|int The URI port. + */ + public function getPort(): ?int; + + /** + * Retrieve the path component of the URI. + * + * The path can either be empty or absolute (starting with a slash) or + * rootless (not starting with a slash). Implementations MUST support all + * three syntaxes. + * + * Normally, the empty path "" and absolute path "/" are considered equal as + * defined in RFC 7230 Section 2.7.3. But this method MUST NOT automatically + * do this normalization because in contexts with a trimmed base path, e.g. + * the front controller, this difference becomes significant. It's the task + * of the user to handle both "" and "/". + * + * The value returned MUST be percent-encoded, but MUST NOT double-encode + * any characters. To determine what characters to encode, please refer to + * RFC 3986, Sections 2 and 3.3. + * + * As an example, if the value should include a slash ("/") not intended as + * delimiter between path segments, that value MUST be passed in encoded + * form (e.g., "%2F") to the instance. + * + * @see https://tools.ietf.org/html/rfc3986#section-2 + * @see https://tools.ietf.org/html/rfc3986#section-3.3 + * @return string The URI path. + */ + public function getPath(): string; + + /** + * Retrieve the query string of the URI. + * + * If no query string is present, this method MUST return an empty string. + * + * The leading "?" character is not part of the query and MUST NOT be + * added. + * + * The value returned MUST be percent-encoded, but MUST NOT double-encode + * any characters. To determine what characters to encode, please refer to + * RFC 3986, Sections 2 and 3.4. + * + * As an example, if a value in a key/value pair of the query string should + * include an ampersand ("&") not intended as a delimiter between values, + * that value MUST be passed in encoded form (e.g., "%26") to the instance. + * + * @see https://tools.ietf.org/html/rfc3986#section-2 + * @see https://tools.ietf.org/html/rfc3986#section-3.4 + * @return string The URI query string. + */ + public function getQuery(): string; + + /** + * Retrieve the fragment component of the URI. + * + * If no fragment is present, this method MUST return an empty string. + * + * The leading "#" character is not part of the fragment and MUST NOT be + * added. + * + * The value returned MUST be percent-encoded, but MUST NOT double-encode + * any characters. To determine what characters to encode, please refer to + * RFC 3986, Sections 2 and 3.5. + * + * @see https://tools.ietf.org/html/rfc3986#section-2 + * @see https://tools.ietf.org/html/rfc3986#section-3.5 + * @return string The URI fragment. + */ + public function getFragment(): string; + + /** + * Return an instance with the specified scheme. + * + * This method MUST retain the state of the current instance, and return + * an instance that contains the specified scheme. + * + * Implementations MUST support the schemes "http" and "https" case + * insensitively, and MAY accommodate other schemes if required. + * + * An empty scheme is equivalent to removing the scheme. + * + * @param string $scheme The scheme to use with the new instance. + * @return static A new instance with the specified scheme. + * @throws \InvalidArgumentException for invalid or unsupported schemes. + */ + public function withScheme(string $scheme): UriInterface; + + /** + * Return an instance with the specified user information. + * + * This method MUST retain the state of the current instance, and return + * an instance that contains the specified user information. + * + * Password is optional, but the user information MUST include the + * user; an empty string for the user is equivalent to removing user + * information. + * + * @param string $user The user name to use for authority. + * @param null|string $password The password associated with $user. + * @return static A new instance with the specified user information. + */ + public function withUserInfo(string $user, ?string $password = null): UriInterface; + + /** + * Return an instance with the specified host. + * + * This method MUST retain the state of the current instance, and return + * an instance that contains the specified host. + * + * An empty host value is equivalent to removing the host. + * + * @param string $host The hostname to use with the new instance. + * @return static A new instance with the specified host. + * @throws \InvalidArgumentException for invalid hostnames. + */ + public function withHost(string $host): UriInterface; + + /** + * Return an instance with the specified port. + * + * This method MUST retain the state of the current instance, and return + * an instance that contains the specified port. + * + * Implementations MUST raise an exception for ports outside the + * established TCP and UDP port ranges. + * + * A null value provided for the port is equivalent to removing the port + * information. + * + * @param null|int $port The port to use with the new instance; a null value + * removes the port information. + * @return static A new instance with the specified port. + * @throws \InvalidArgumentException for invalid ports. + */ + public function withPort(?int $port): UriInterface; + + /** + * Return an instance with the specified path. + * + * This method MUST retain the state of the current instance, and return + * an instance that contains the specified path. + * + * The path can either be empty or absolute (starting with a slash) or + * rootless (not starting with a slash). Implementations MUST support all + * three syntaxes. + * + * If the path is intended to be domain-relative rather than path relative then + * it must begin with a slash ("/"). Paths not starting with a slash ("/") + * are assumed to be relative to some base path known to the application or + * consumer. + * + * Users can provide both encoded and decoded path characters. + * Implementations ensure the correct encoding as outlined in getPath(). + * + * @param string $path The path to use with the new instance. + * @return static A new instance with the specified path. + * @throws \InvalidArgumentException for invalid paths. + */ + public function withPath(string $path): UriInterface; + + /** + * Return an instance with the specified query string. + * + * This method MUST retain the state of the current instance, and return + * an instance that contains the specified query string. + * + * Users can provide both encoded and decoded query characters. + * Implementations ensure the correct encoding as outlined in getQuery(). + * + * An empty query string value is equivalent to removing the query string. + * + * @param string $query The query string to use with the new instance. + * @return static A new instance with the specified query string. + * @throws \InvalidArgumentException for invalid query strings. + */ + public function withQuery(string $query): UriInterface; + + /** + * Return an instance with the specified URI fragment. + * + * This method MUST retain the state of the current instance, and return + * an instance that contains the specified URI fragment. + * + * Users can provide both encoded and decoded fragment characters. + * Implementations ensure the correct encoding as outlined in getFragment(). + * + * An empty fragment value is equivalent to removing the fragment. + * + * @param string $fragment The fragment to use with the new instance. + * @return static A new instance with the specified fragment. + */ + public function withFragment(string $fragment): UriInterface; + + /** + * Return the string representation as a URI reference. + * + * Depending on which components of the URI are present, the resulting + * string is either a full URI or relative reference according to RFC 3986, + * Section 4.1. The method concatenates the various components of the URI, + * using the appropriate delimiters: + * + * - If a scheme is present, it MUST be suffixed by ":". + * - If an authority is present, it MUST be prefixed by "//". + * - The path can be concatenated without delimiters. But there are two + * cases where the path has to be adjusted to make the URI reference + * valid as PHP does not allow to throw an exception in __toString(): + * - If the path is rootless and an authority is present, the path MUST + * be prefixed by "/". + * - If the path is starting with more than one "/" and no authority is + * present, the starting slashes MUST be reduced to one. + * - If a query is present, it MUST be prefixed by "?". + * - If a fragment is present, it MUST be prefixed by "#". + * + * @see http://tools.ietf.org/html/rfc3986#section-4.1 + * @return string + */ + public function __toString(): string; +} diff --git a/3rdparty/psr/log/LICENSE b/3rdparty/psr/log/LICENSE new file mode 100644 index 00000000..474c952b --- /dev/null +++ b/3rdparty/psr/log/LICENSE @@ -0,0 +1,19 @@ +Copyright (c) 2012 PHP Framework Interoperability Group + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in +all copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +THE SOFTWARE. diff --git a/3rdparty/psr/log/src/AbstractLogger.php b/3rdparty/psr/log/src/AbstractLogger.php new file mode 100644 index 00000000..d60a091a --- /dev/null +++ b/3rdparty/psr/log/src/AbstractLogger.php @@ -0,0 +1,15 @@ +logger = $logger; + } +} diff --git a/3rdparty/psr/log/src/LoggerInterface.php b/3rdparty/psr/log/src/LoggerInterface.php new file mode 100644 index 00000000..cb4cf648 --- /dev/null +++ b/3rdparty/psr/log/src/LoggerInterface.php @@ -0,0 +1,98 @@ +log(LogLevel::EMERGENCY, $message, $context); + } + + /** + * Action must be taken immediately. + * + * Example: Entire website down, database unavailable, etc. This should + * trigger the SMS alerts and wake you up. + */ + public function alert(string|\Stringable $message, array $context = []): void + { + $this->log(LogLevel::ALERT, $message, $context); + } + + /** + * Critical conditions. + * + * Example: Application component unavailable, unexpected exception. + */ + public function critical(string|\Stringable $message, array $context = []): void + { + $this->log(LogLevel::CRITICAL, $message, $context); + } + + /** + * Runtime errors that do not require immediate action but should typically + * be logged and monitored. + */ + public function error(string|\Stringable $message, array $context = []): void + { + $this->log(LogLevel::ERROR, $message, $context); + } + + /** + * Exceptional occurrences that are not errors. + * + * Example: Use of deprecated APIs, poor use of an API, undesirable things + * that are not necessarily wrong. + */ + public function warning(string|\Stringable $message, array $context = []): void + { + $this->log(LogLevel::WARNING, $message, $context); + } + + /** + * Normal but significant events. + */ + public function notice(string|\Stringable $message, array $context = []): void + { + $this->log(LogLevel::NOTICE, $message, $context); + } + + /** + * Interesting events. + * + * Example: User logs in, SQL logs. + */ + public function info(string|\Stringable $message, array $context = []): void + { + $this->log(LogLevel::INFO, $message, $context); + } + + /** + * Detailed debug information. + */ + public function debug(string|\Stringable $message, array $context = []): void + { + $this->log(LogLevel::DEBUG, $message, $context); + } + + /** + * Logs with an arbitrary level. + * + * @param mixed $level + * + * @throws \Psr\Log\InvalidArgumentException + */ + abstract public function log($level, string|\Stringable $message, array $context = []): void; +} diff --git a/3rdparty/psr/log/src/NullLogger.php b/3rdparty/psr/log/src/NullLogger.php new file mode 100644 index 00000000..de0561e2 --- /dev/null +++ b/3rdparty/psr/log/src/NullLogger.php @@ -0,0 +1,26 @@ +logger) { }` + * blocks. + */ +class NullLogger extends AbstractLogger +{ + /** + * Logs with an arbitrary level. + * + * @param mixed[] $context + * + * @throws \Psr\Log\InvalidArgumentException + */ + public function log($level, string|\Stringable $message, array $context = []): void + { + // noop + } +} diff --git a/3rdparty/punic/punic/LIBPHONENUMBER-LICENSE.txt b/3rdparty/punic/punic/LIBPHONENUMBER-LICENSE.txt new file mode 100644 index 00000000..d9a10c0d --- /dev/null +++ b/3rdparty/punic/punic/LIBPHONENUMBER-LICENSE.txt @@ -0,0 +1,176 @@ + Apache License + Version 2.0, January 2004 + http://www.apache.org/licenses/ + + TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION + + 1. Definitions. + + "License" shall mean the terms and conditions for use, reproduction, + and distribution as defined by Sections 1 through 9 of this document. + + "Licensor" shall mean the copyright owner or entity authorized by + the copyright owner that is granting the License. + + "Legal Entity" shall mean the union of the acting entity and all + other entities that control, are controlled by, or are under common + control with that entity. For the purposes of this definition, + "control" means (i) the power, direct or indirect, to cause the + direction or management of such entity, whether by contract or + otherwise, or (ii) ownership of fifty percent (50%) or more of the + outstanding shares, or (iii) beneficial ownership of such entity. + + "You" (or "Your") shall mean an individual or Legal Entity + exercising permissions granted by this License. + + "Source" form shall mean the preferred form for making modifications, + including but not limited to software source code, documentation + source, and configuration files. + + "Object" form shall mean any form resulting from mechanical + transformation or translation of a Source form, including but + not limited to compiled object code, generated documentation, + and conversions to other media types. + + "Work" shall mean the work of authorship, whether in Source or + Object form, made available under the License, as indicated by a + copyright notice that is included in or attached to the work + (an example is provided in the Appendix below). + + "Derivative Works" shall mean any work, whether in Source or Object + form, that is based on (or derived from) the Work and for which the + editorial revisions, annotations, elaborations, or other modifications + represent, as a whole, an original work of authorship. For the purposes + of this License, Derivative Works shall not include works that remain + separable from, or merely link (or bind by name) to the interfaces of, + the Work and Derivative Works thereof. + + "Contribution" shall mean any work of authorship, including + the original version of the Work and any modifications or additions + to that Work or Derivative Works thereof, that is intentionally + submitted to Licensor for inclusion in the Work by the copyright owner + or by an individual or Legal Entity authorized to submit on behalf of + the copyright owner. For the purposes of this definition, "submitted" + means any form of electronic, verbal, or written communication sent + to the Licensor or its representatives, including but not limited to + communication on electronic mailing lists, source code control systems, + and issue tracking systems that are managed by, or on behalf of, the + Licensor for the purpose of discussing and improving the Work, but + excluding communication that is conspicuously marked or otherwise + designated in writing by the copyright owner as "Not a Contribution." + + "Contributor" shall mean Licensor and any individual or Legal Entity + on behalf of whom a Contribution has been received by Licensor and + subsequently incorporated within the Work. + + 2. Grant of Copyright License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + copyright license to reproduce, prepare Derivative Works of, + publicly display, publicly perform, sublicense, and distribute the + Work and such Derivative Works in Source or Object form. + + 3. Grant of Patent License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + (except as stated in this section) patent license to make, have made, + use, offer to sell, sell, import, and otherwise transfer the Work, + where such license applies only to those patent claims licensable + by such Contributor that are necessarily infringed by their + Contribution(s) alone or by combination of their Contribution(s) + with the Work to which such Contribution(s) was submitted. If You + institute patent litigation against any entity (including a + cross-claim or counterclaim in a lawsuit) alleging that the Work + or a Contribution incorporated within the Work constitutes direct + or contributory patent infringement, then any patent licenses + granted to You under this License for that Work shall terminate + as of the date such litigation is filed. + + 4. Redistribution. You may reproduce and distribute copies of the + Work or Derivative Works thereof in any medium, with or without + modifications, and in Source or Object form, provided that You + meet the following conditions: + + (a) You must give any other recipients of the Work or + Derivative Works a copy of this License; and + + (b) You must cause any modified files to carry prominent notices + stating that You changed the files; and + + (c) You must retain, in the Source form of any Derivative Works + that You distribute, all copyright, patent, trademark, and + attribution notices from the Source form of the Work, + excluding those notices that do not pertain to any part of + the Derivative Works; and + + (d) If the Work includes a "NOTICE" text file as part of its + distribution, then any Derivative Works that You distribute must + include a readable copy of the attribution notices contained + within such NOTICE file, excluding those notices that do not + pertain to any part of the Derivative Works, in at least one + of the following places: within a NOTICE text file distributed + as part of the Derivative Works; within the Source form or + documentation, if provided along with the Derivative Works; or, + within a display generated by the Derivative Works, if and + wherever such third-party notices normally appear. The contents + of the NOTICE file are for informational purposes only and + do not modify the License. You may add Your own attribution + notices within Derivative Works that You distribute, alongside + or as an addendum to the NOTICE text from the Work, provided + that such additional attribution notices cannot be construed + as modifying the License. + + You may add Your own copyright statement to Your modifications and + may provide additional or different license terms and conditions + for use, reproduction, or distribution of Your modifications, or + for any such Derivative Works as a whole, provided Your use, + reproduction, and distribution of the Work otherwise complies with + the conditions stated in this License. + + 5. Submission of Contributions. Unless You explicitly state otherwise, + any Contribution intentionally submitted for inclusion in the Work + by You to the Licensor shall be under the terms and conditions of + this License, without any additional terms or conditions. + Notwithstanding the above, nothing herein shall supersede or modify + the terms of any separate license agreement you may have executed + with Licensor regarding such Contributions. + + 6. Trademarks. This License does not grant permission to use the trade + names, trademarks, service marks, or product names of the Licensor, + except as required for reasonable and customary use in describing the + origin of the Work and reproducing the content of the NOTICE file. + + 7. Disclaimer of Warranty. Unless required by applicable law or + agreed to in writing, Licensor provides the Work (and each + Contributor provides its Contributions) on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or + implied, including, without limitation, any warranties or conditions + of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A + PARTICULAR PURPOSE. You are solely responsible for determining the + appropriateness of using or redistributing the Work and assume any + risks associated with Your exercise of permissions under this License. + + 8. Limitation of Liability. In no event and under no legal theory, + whether in tort (including negligence), contract, or otherwise, + unless required by applicable law (such as deliberate and grossly + negligent acts) or agreed to in writing, shall any Contributor be + liable to You for damages, including any direct, indirect, special, + incidental, or consequential damages of any character arising as a + result of this License or out of the use or inability to use the + Work (including but not limited to damages for loss of goodwill, + work stoppage, computer failure or malfunction, or any and all + other commercial damages or losses), even if such Contributor + has been advised of the possibility of such damages. + + 9. Accepting Warranty or Additional Liability. While redistributing + the Work or Derivative Works thereof, You may choose to offer, + and charge a fee for, acceptance of support, warranty, indemnity, + or other liability obligations and/or rights consistent with this + License. However, in accepting such obligations, You may act only + on Your own behalf and on Your sole responsibility, not on behalf + of any other Contributor, and only if You agree to indemnify, + defend, and hold each Contributor harmless for any liability + incurred by, or claims asserted against, such Contributor by reason + of your accepting any such warranty or additional liability. + + END OF TERMS AND CONDITIONS diff --git a/3rdparty/punic/punic/LICENSE.txt b/3rdparty/punic/punic/LICENSE.txt new file mode 100644 index 00000000..c0b0eb6e --- /dev/null +++ b/3rdparty/punic/punic/LICENSE.txt @@ -0,0 +1,24 @@ +The MIT License (MIT) + +Copyright (c) 2014-2015 Michele Locati + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in +all copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +THE SOFTWARE. + + +The \Punic\Calendar::convertPhpToIsoFormat function is taken from ZendFramework 1 - Copyright (c) 2005-2015, Zend Technologies USA, Inc. diff --git a/3rdparty/punic/punic/UNICODE-LICENSE.txt b/3rdparty/punic/punic/UNICODE-LICENSE.txt new file mode 100644 index 00000000..a121510c --- /dev/null +++ b/3rdparty/punic/punic/UNICODE-LICENSE.txt @@ -0,0 +1,50 @@ +UNICODE, INC. LICENSE AGREEMENT - DATA FILES AND SOFTWARE + + Unicode Data Files include all data files under the directories +http://www.unicode.org/Public/, http://www.unicode.org/reports/, and +http://www.unicode.org/cldr/data/. Unicode Data Files do not include PDF +online code charts under the directory http://www.unicode.org/Public/. +Software includes any source code published in the Unicode Standard or under +the directories http://www.unicode.org/Public/, +http://www.unicode.org/reports/, and http://www.unicode.org/cldr/data/. + + NOTICE TO USER: Carefully read the following legal agreement. BY +DOWNLOADING, INSTALLING, COPYING OR OTHERWISE USING UNICODE INC.'S DATA FILES +("DATA FILES"), AND/OR SOFTWARE ("SOFTWARE"), YOU UNEQUIVOCALLY ACCEPT, AND +AGREE TO BE BOUND BY, ALL OF THE TERMS AND CONDITIONS OF THIS AGREEMENT. IF +YOU DO NOT AGREE, DO NOT DOWNLOAD, INSTALL, COPY, DISTRIBUTE OR USE THE DATA +FILES OR SOFTWARE. + + COPYRIGHT AND PERMISSION NOTICE + + Copyright © 1991-2014 Unicode, Inc. All rights reserved. Distributed under +the Terms of Use in http://www.unicode.org/copyright.html. + + Permission is hereby granted, free of charge, to any person obtaining a +copy of the Unicode data files and any associated documentation (the "Data +Files") or Unicode software and any associated documentation (the "Software") +to deal in the Data Files or Software without restriction, including without +limitation the rights to use, copy, modify, merge, publish, distribute, and/or +sell copies of the Data Files or Software, and to permit persons to whom the +Data Files or Software are furnished to do so, provided that (a) the above +copyright notice(s) and this permission notice appear with all copies of the +Data Files or Software, (b) both the above copyright notice(s) and this +permission notice appear in associated documentation, and (c) there is clear +notice in each modified Data File or in the Software as well as in the +documentation associated with the Data File(s) or Software that the data or +software has been modified. + + THE DATA FILES AND SOFTWARE ARE PROVIDED "AS IS", WITHOUT WARRANTY OF ANY +KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF +MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT OF THIRD +PARTY RIGHTS. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR HOLDERS INCLUDED IN +THIS NOTICE BE LIABLE FOR ANY CLAIM, OR ANY SPECIAL INDIRECT OR CONSEQUENTIAL +DAMAGES, OR ANY DAMAGES WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR +PROFITS, WHETHER IN AN ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS +ACTION, ARISING OUT OF OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THE +DATA FILES OR SOFTWARE. + + Except as contained in this notice, the name of a copyright holder shall +not be used in advertising or otherwise to promote the sale, use or other +dealings in these Data Files or Software without prior written authorization +of the copyright holder. diff --git a/3rdparty/punic/punic/punic.php b/3rdparty/punic/punic/punic.php new file mode 100644 index 00000000..1e5d80c3 --- /dev/null +++ b/3rdparty/punic/punic/punic.php @@ -0,0 +1,13 @@ + 'decodeEra', + 'y' => 'decodeYear', + 'Y' => 'decodeYearWeekOfYear', + 'u' => 'decodeYearExtended', + 'U' => 'decodeYearCyclicName', + 'r' => 'decodeYearRelatedGregorian', + 'Q' => 'decodeQuarter', + 'q' => 'decodeQuarterAlone', + 'M' => 'decodeMonth', + 'L' => 'decodeMonthAlone', + 'w' => 'decodeWeekOfYear', + 'W' => 'decodeWeekOfMonth', + 'd' => 'decodeDayOfMonth', + 'D' => 'decodeDayOfYear', + 'F' => 'decodeWeekdayInMonth', + 'g' => 'decodeModifiedGiulianDay', + 'E' => 'decodeDayOfWeek', + 'e' => 'decodeDayOfWeekLocal', + 'c' => 'decodeDayOfWeekLocalAlone', + 'a' => 'decodeDayperiod', + 'b' => 'decodeDayperiod', + 'B' => 'decodeVariableDayperiod', + 'h' => 'decodeHour12', + 'H' => 'decodeHour24', + 'K' => 'decodeHour12From0', + 'k' => 'decodeHour24From1', + 'm' => 'decodeMinute', + 's' => 'decodeSecond', + 'S' => 'decodeFractionsOfSeconds', + 'A' => 'decodeMsecInDay', + 'z' => 'decodeTimezoneNoLocationSpecific', + 'Z' => 'decodeTimezoneDelta', + 'O' => 'decodeTimezoneShortGMT', + 'v' => 'decodeTimezoneNoLocationGeneric', + 'V' => 'decodeTimezoneID', + 'X' => 'decodeTimezoneWithTimeZ', + 'x' => 'decodeTimezoneWithTime', + 'P' => 'decodePunicExtension', + ); + + /** + * Cache for the tokenizeFormat method. + * + * @var array + */ + private static $tokenizerCache = array(); + + /** + * Convert a date/time representation to a {@link https://www.php.net/manual/class.datetime.php \DateTime} instance. + * + * @param number|\DateTime|\DateTimeInterface|string $value A Unix timestamp, a `\DateTimeInterface` instance or a string accepted by {@link https://www.php.net/manual/function.strtotime.php strtotime}. + * @param string|\DateTimeZone $toTimezone the timezone to set; leave empty to use the value of $fromTimezone (if it's empty we'll use the default timezone or the timezone associated to $value if it's already a `\DateTimeInterface`) + * @param string|\DateTimeZone $fromTimezone the original timezone of $value; leave empty to use the default timezone (or the timezone associated to $value if it's already a `\DateTimeInterface`) + * + * @throws \Punic\Exception\BadArgumentType throws an exception if $value is not empty and can't be converted to a `\DateTime` instance or if $toTimezone is not empty and is not valid + * + * @return \DateTime|null returns null if $value is empty, a `\DateTime` instance otherwise + * + * @example Convert a Unix timestamp to a \DateTime instance with the current time zone: + * ```php + * \Punic\Calendar::toDateTime(1409648286); + * ``` + * @example Convert a Unix timestamp to a \DateTime instance with a specific time zone: + * ```php + * \Punic\Calendar::toDateTime(1409648286, 'Europe/Rome'); + * \Punic\Calendar::toDateTime(1409648286, new \DateTimeZone('Europe/Rome')); + * ``` + * @example Convert a string to a \DateTime instance with the current time zone: + * ```php + * \Punic\Calendar::toDateTime('2014-03-07 13:30'); + * ``` + * @example Convert a string to a \DateTime instance with a specific time zone: + * ```php + * \Punic\Calendar::toDateTime('2014-03-07 13:30', 'Europe/Rome'); + * ``` + * Please remark that in this case '2014-03-07 13:30' is converted to a \DateTime instance with the current timezone, and after we change the timezone. + * So, if your system default timezone is 'America/Los_Angeles' (GMT -8), the resulting date/time will be '2014-03-07 22:30 GMT+1' since it'll be converted to 'Europe/Rome' (GMT +1) + */ + public static function toDateTime($value, $toTimezone = '', $fromTimezone = '') + { + $result = null; + if ((!empty($value)) || ($value === 0) || ($value === '0')) { + $tzFrom = null; + if (!empty($fromTimezone)) { + if (is_string($fromTimezone)) { + try { + $tzFrom = new DateTimeZone($fromTimezone); + } catch (\Exception $x) { + throw new Exception\BadArgumentType($fromTimezone, '\\DateTimeZone', $x); + } + } elseif ($fromTimezone instanceof DateTimeZone) { + $tzFrom = $fromTimezone; + } else { + throw new Exception\BadArgumentType($fromTimezone, '\\DateTimeZone'); + } + } + if (is_numeric($value)) { + $result = new DateTime(); + $result->setTimestamp($value); + if ($tzFrom !== null) { + $result->setTimezone($tzFrom); + } + } elseif ($value instanceof DateTimeInterface || $value instanceof DateTime) { + $result = new DateTime('now', $value->getTimezone()); + $result->setTimestamp($value->getTimestamp()); + if ($tzFrom !== null) { + $result->setTimezone($tzFrom); + } + } elseif (is_string($value)) { + try { + if ($tzFrom === null) { + $result = new DateTime($value); + } else { + $result = new DateTime($value, $tzFrom); + } + } catch (\Exception $x) { + throw new Exception\BadArgumentType($value, '\\DateTime', $x); + } + } else { + throw new Exception\BadArgumentType($value, '\\DateTime'); + } + if ($result) { + if (!empty($toTimezone)) { + if (is_string($toTimezone)) { + try { + $result->setTimezone(new DateTimeZone($toTimezone)); + } catch (\Exception $x) { + throw new Exception\BadArgumentType($toTimezone, '\\DateTimeZone', $x); + } + } elseif ($toTimezone instanceof DateTimeZone) { + $result->setTimezone($toTimezone); + } else { + throw new Exception\BadArgumentType($toTimezone, '\\DateTimeZone'); + } + } + } + } + + return $result; + } + + /** + * Converts a format string from {@link https://www.php.net/manual/function.date.php#refsect1-function.date-parameters PHP's date format} to {@link https://www.unicode.org/reports/tr35/tr35-dates.html#Date_Field_Symbol_Table ISO format}. + * The following extra format chunks are introduced: + * - 'P': ISO-8601 numeric representation of the day of the week (same as 'e' but not locale dependent) + * - 'PP': Numeric representation of the day of the week, from 0 (for Sunday) to 6 (for Saturday) + * - 'PPP': English ordinal suffix for the day of the month + * - 'PPPP': The day of the year (starting from 0) + * - 'PPPPP': Number of days in the given month + * - 'PPPPPP': Whether it's a leap year: 1 if it is a leap year, 0 otherwise. + * - 'PPPPPPP': Lowercase Ante meridiem and Post meridiem (English only, for other locales it's the same as 'a') + * - 'PPPPPPPP': Swatch Internet time + * - 'PPPPPPPPP': Microseconds + * - 'PPPPPPPPPP': Whether or not the date is in daylight saving time 1 if Daylight Saving Time, 0 otherwise. + * - 'PPPPPPPPPPP': Timezone offset in seconds + * - 'PPPPPPPPPPPP': RFC 2822 formatted date (Example: 'Thu, 21 Dec 2000 16:01:07 +0200') + * - 'PPPPPPPPPPPPP': Seconds since the Unix Epoch (January 1 1970 00:00:00 GMT). + * + * @param string $format the PHP date/time format string to convert + * + * @return string returns the ISO date/time format corresponding to the specified PHP date/time format + */ + public static function convertPhpToIsoFormat($format) + { + static $cache = array(); + static $convert = array( + 'd' => 'dd', + 'D' => 'EE', + 'j' => 'd', + 'l' => 'EEEE', + 'N' => 'P', + 'S' => 'PPP', + 'w' => 'PP', + 'z' => 'PPPP', + 'W' => 'ww', + 'F' => 'MMMM', + 'm' => 'MM', + 'M' => 'MMM', + 'n' => 'M', + 't' => 'PPPPP', + 'L' => 'PPPPPP', + 'o' => 'YYYY', + 'Y' => 'yyyy', + 'y' => 'yy', + 'a' => 'PPPPPPP', + 'b' => 'PPPPPPP', + 'A' => 'a', + 'B' => 'PPPPPPPP', + 'g' => 'h', + 'G' => 'H', + 'h' => 'hh', + 'H' => 'HH', + 'i' => 'mm', + 's' => 'ss', + 'e' => 'VV', + 'I' => 'I', + 'O' => 'Z', + 'P' => 'ZZZZZ', + 'T' => 'z', + 'Z' => 'PPPPPPPPPPP', + 'c' => 'yyyy-MM-ddTHH:mm:ssZZZZZ', + 'r' => 'PPPPPPPPPPPP', + 'U' => 'U', + 'u' => 'PPPPPPPPP', + 'I' => 'PPPPPPPPPP', + 'U' => 'PPPPPPPPPPPPP', + ); + if (!is_string($format)) { + return ''; + } + if (!isset($cache[$format])) { + $escaped = false; + $inEscapedString = false; + $converted = array(); + foreach (str_split($format) as $char) { + if (!$escaped && $char == '\\') { + // Next char will be escaped: let's remember it + $escaped = true; + } elseif ($escaped) { + if (!$inEscapedString) { + // First escaped string: start the quoted chunk + $converted[] = "'"; + $inEscapedString = true; + } + // Since the previous char was a \ and we are in the quoted + // chunk, let's simply add $char as it is + $converted[] = $char; + $escaped = false; + } elseif ($char == "'") { + // Single quotes need to be escaped like this + $converted[] = "''"; + } else { + if ($inEscapedString) { + // Close the single-quoted chunk + $converted[] = "'"; + $inEscapedString = false; + } + // Convert the unescaped char if needed + if (isset($convert[$char])) { + $converted[] = $convert[$char]; + } else { + $converted[] = $char; + } + } + } + $cache[$format] = implode('', $converted); + } + + return $cache[$format]; + } + + /** + * Try to convert a date, time or date/time {@link https://www.unicode.org/reports/tr35/tr35-dates.html#Date_Field_Symbol_Table ISO format string} to a {@link https://www.php.net/manual/function.date.php#refsect1-function.date-parameters PHP date/time format}. + * + * @param string $isoDateTimeFormat The PHP date/time format + * + * @return string|null if the format is not possible (the ISO placeholders are much more than the PHP ones), null will be returned + */ + public static function tryConvertIsoToPhpFormat($isoDateTimeFormat) + { + $result = null; + if (is_string($isoDateTimeFormat)) { + $result = ''; + if ($isoDateTimeFormat !== '') { + $tokens = self::tokenizeFormat($isoDateTimeFormat); + foreach ($tokens as $token) { + $chunk = null; + if (is_string($token)) { + $chunk = preg_replace('/([a-zA-Z\\\\])/', '\\\\\\1', $token); + } else { + switch ($token[0]) { + case 'decodeYear': + switch ($token[1]) { + case 2: + $chunk .= 'y'; + break; + default: + $chunk .= 'Y'; + break; + } + break; + case 'decodeYearWeekOfYear': + $chunk = 'o'; + break; + case 'decodeYearExtended': + case 'decodeYearRelatedGregorian': + $chunk = 'Y'; + break; + case 'decodeMonth': + case 'decodeMonthAlone': + switch ($token[1]) { + case 1: + $chunk = 'n'; + break; + case 2: + $chunk = 'm'; + break; + case 3: + $chunk = 'M'; + break; + case 4: + $chunk = 'F'; + break; + } + break; + case 'decodeWeekOfYear': + switch ($token[1]) { + case 1: + case 2: + $chunk = 'W'; + break; + } + break; + case 'decodeDayOfMonth': + switch ($token[1]) { + case 1: + $chunk = 'j'; + break; + case 2: + $chunk = 'd'; + break; + } + break; + case 'decodeDayOfYear': + switch ($token[1]) { + case 1: + case 2: + case 3: + $chunk = 'z'; + break; + } + break; + case 'decodeDayOfWeek': + switch ($token[1]) { + case 1: + case 2: + case 3: + $chunk = 'D'; + break; + case 4: + $chunk = 'l'; + break; + } + break; + case 'decodeDayOfWeekLocal': + case 'decodeDayOfWeekLocalAlone': + switch ($token[1]) { + case 1: + case 2: + $chunk = 'N'; + break; + case 3: + $chunk = 'D'; + break; + case 4: + $chunk = 'l'; + break; + } + break; + case 'decodeDayperiod': + case 'decodeVariableDayperiod': + if ($token[1] <= 4) { + $chunk = 'A'; + } + break; + case 'decodeHour12': + switch ($token[1]) { + case 1: + $chunk = 'g'; + break; + case 2: + $chunk = 'h'; + break; + } + break; + case 'decodeHour24': + switch ($token[1]) { + case 1: + $chunk = 'G'; + break; + case 2: + $chunk = 'H'; + break; + } + break; + case 'decodeMinute': + switch ($token[1]) { + case 1: + case 2: + $chunk = 'i'; + break; + } + break; + case 'decodeSecond': + switch ($token[1]) { + case 1: + case 2: + $chunk = 's'; + break; + } + break; + case 'decodeFractionsOfSeconds': + switch ($token[1]) { + case 3: + if (version_compare(PHP_VERSION, '7') >= 0) { + $chunk = 'v'; + } + break; + case 6: + $chunk = 'u'; + break; + } + break; + case 'decodeTimezoneNoLocationSpecific': + switch ($token[1]) { + case 1: + case 2: + case 3: + $chunk = 'T'; + break; + case 4: + $chunk = '\\G\\M\\TP'; + break; + } + break; + case 'decodeTimezoneDelta': + switch ($token[1]) { + case 1: + case 2: + case 3: + $chunk = 'O'; + break; + case 4: + $chunk = '\\G\\M\\TP'; + break; + case 5: + $chunk = 'P'; + break; + } + break; + case 'decodeTimezoneShortGMT': + switch ($token[1]) { + case 1: + case 4: + $chunk = '\\G\\M\\TP'; + break; + } + break; + case 'decodeTimezoneID': + switch ($token[1]) { + case 2: + $chunk = 'e'; + break; + } + break; + case 'decodeTimezoneWithTimeZ': + case 'decodeTimezoneWithTime': + switch ($token[1]) { + case 1: + case 2: + case 4: + $chunk = 'O'; + break; + case 3: + case 5: + $chunk = 'P'; + break; + } + break; + case 'decodePunicExtension': + switch ($token[1]) { + case 1: + $chunk = 'N'; + break; + case 2: + $chunk = 'w'; + break; + case 3: + $chunk = 'S'; + break; + case 4: + $chunk = 'z'; + break; + case 5: + $chunk = 't'; + break; + case 6: + $chunk = 'L'; + break; + case 7: + $chunk = 'a'; + break; + case 8: + $chunk = 'B'; + break; + case 9: + $chunk = 'u'; + break; + case 10: + $chunk = 'I'; + break; + case 11: + $chunk = 'Z'; + break; + case 12: + $chunk = 'r'; + break; + case 13: + $chunk = 'U'; + break; + } + break; + } + } + if ($chunk === null) { + $result = null; + break; + } + $result .= $chunk; + } + } + } + + return $result; + } + + /** + * Get the name of an era. + * + * @param number|\DateTime|\DateTimeInterface $value the year number or the \DateTimeInterface instance for which you want the name of the era + * @param string $width the format name; it can be 'wide' (eg 'Before Christ'), 'abbreviated' (eg 'BC') or 'narrow' (eg 'B') + * @param string $locale The locale to use. If empty we'll use the default locale set with {@link \Punic\Data::setDefaultLocale()}. + * + * @throws \Punic\Exception\BadArgumentType throws a BadArgumentType exception if $value is not valid + * @throws \Punic\Exception\ValueNotInList throws a ValueNotInList exception if $width is not valid + * @throws \Punic\Exception throws a generic exception in case of other problems (for instance if you specify an invalid locale) + * + * @return string returns an empty string if $value is empty, the name of the era otherwise + */ + public static function getEraName($value, $width = 'abbreviated', $locale = '') + { + $result = ''; + if ((!empty($value)) || ($value === 0) || ($value === '0')) { + $year = null; + if (is_numeric($value)) { + $year = (int) $value; + } elseif ($value instanceof DateTimeInterface || $value instanceof DateTime) { + $year = (int) $value->format('Y'); + } + if ($year === null) { + throw new Exception\BadArgumentType($value, 'year number'); + } + $data = Data::get('calendar', $locale); + $data = $data['eras']; + if (!isset($data[$width])) { + throw new Exception\ValueNotInList($width, array_keys($data)); + } + $result = $data[$width][($year < 0) ? '0' : '1']; + } + + return $result; + } + + /** + * Get the name of a month. + * + * @param number|\DateTime|\DateTimeInterface $value the month number (1-12) or a \DateTimeInterface instance for which you want the name of the month + * @param string $width the format name; it can be 'wide' (eg 'January'), 'abbreviated' (eg 'Jan') or 'narrow' (eg 'J') + * @param string $locale The locale to use. If empty we'll use the default locale set with {@link \Punic\Data::setDefaultLocale()}. + * @param bool $standAlone set to true to return the form used independently (such as in calendar header), set to false if the month name will be part of a date + * + * @throws \Punic\Exception\BadArgumentType throws a BadArgumentType exception if $value is not valid + * @throws \Punic\Exception\ValueNotInList throws a ValueNotInList exception if $width is not valid + * @throws \Punic\Exception throws a generic exception in case of other problems (for instance if you specify an invalid locale) + * + * @return string returns an empty string if $value is empty, the name of the month otherwise + */ + public static function getMonthName($value, $width = 'wide', $locale = '', $standAlone = false) + { + $result = ''; + if ((!empty($value)) || ($value === 0) || ($value === '0')) { + $month = null; + if (is_numeric($value)) { + $month = (int) $value; + } elseif ($value instanceof DateTimeInterface || $value instanceof DateTime) { + $month = (int) $value->format('n'); + } + if (($month === null) || (($month < 1) || ($month > 12))) { + throw new Exception\BadArgumentType($value, 'month number'); + } + $data = Data::get('calendar', $locale); + $data = $data['months'][$standAlone ? 'stand-alone' : 'format']; + if (!isset($data[$width])) { + throw new Exception\ValueNotInList($width, array_keys($data)); + } + $result = $data[$width][$month]; + } + + return $result; + } + + /** + * Get the name of a week day. + * + * @param number|\DateTime|\DateTimeInterface $value a week day number (from 0-Sunday to 6-Saturday) or a \DateTimeInterface instance for which you want the name of the day of the week + * @param string $width the format name; it can be 'wide' (eg 'Sunday'), 'abbreviated' (eg 'Sun'), 'short' (eg 'Su') or 'narrow' (eg 'S') + * @param string $locale The locale to use. If empty we'll use the default locale set with {@link \Punic\Data::setDefaultLocale()}. + * @param bool $standAlone set to true to return the form used independently (such as in calendar header), set to false if the week day name will be part of a date + * + * @throws \Punic\Exception\BadArgumentType throws a BadArgumentType exception if $value is not valid + * @throws \Punic\Exception\ValueNotInList throws a ValueNotInList exception if $width is not valid + * @throws \Punic\Exception throws a generic exception in case of other problems (for instance if you specify an invalid locale) + * + * @return string returns an empty string if $value is empty, the name of the week day name otherwise + */ + public static function getWeekdayName($value, $width = 'wide', $locale = '', $standAlone = false) + { + $result = ''; + if ((!empty($value)) || ($value === 0) || ($value === '0')) { + $weekday = null; + if (is_numeric($value)) { + $weekday = (int) $value; + } elseif ($value instanceof DateTimeInterface || $value instanceof DateTime) { + $weekday = (int) $value->format('w'); + } + if (($weekday === null) || (($weekday < 0) || ($weekday > 6))) { + throw new Exception\BadArgumentType($value, 'weekday number'); + } + $weekday = self::$weekdayDictionary[$weekday]; + $data = Data::get('calendar', $locale); + $data = $data['days'][$standAlone ? 'stand-alone' : 'format']; + if (!isset($data[$width])) { + throw new Exception\ValueNotInList($width, array_keys($data)); + } + $result = $data[$width][$weekday]; + } + + return $result; + } + + /** + * Get the name of a quarter. + * + * @param number|\DateTime|\DateTimeInterface $value a quarter number (from 1 to 4) or a \DateTimeInterface instance for which you want the name of the day of the quarter + * @param string $width the format name; it can be 'wide' (eg '1st quarter'), 'abbreviated' (eg 'Q1') or 'narrow' (eg '1') + * @param string $locale The locale to use. If empty we'll use the default locale set with {@link \Punic\Data::setDefaultLocale()}. + * @param bool $standAlone set to true to return the form used independently (such as in calendar header), set to false if the quarter name will be part of a date + * + * @throws \Punic\Exception\BadArgumentType throws a BadArgumentType exception if $value is not valid + * @throws \Punic\Exception\ValueNotInList throws a ValueNotInList exception if $width is not valid + * @throws \Punic\Exception throws a generic exception in case of other problems (for instance if you specify an invalid locale) + * + * @return string returns an empty string if $value is empty, the name of the quarter name otherwise + */ + public static function getQuarterName($value, $width = 'wide', $locale = '', $standAlone = false) + { + $result = ''; + if ((!empty($value)) || ($value === 0) || ($value === '0')) { + $quarter = null; + if (is_numeric($value)) { + $quarter = (int) $value; + } elseif ($value instanceof DateTimeInterface || $value instanceof DateTime) { + $quarter = 1 + (int) floor(((int) $value->format('n') - 1) / 3); + } + if (($quarter === null) || (($quarter < 1) || ($quarter > 4))) { + throw new Exception\BadArgumentType($value, 'quarter number'); + } + $data = Data::get('calendar', $locale); + $data = $data['quarters'][$standAlone ? 'stand-alone' : 'format']; + if (!isset($data[$width])) { + throw new Exception\ValueNotInList($width, array_keys($data)); + } + $result = $data[$width][$quarter]; + } + + return $result; + } + + /** + * Get the name of a day period (AM/PM). + * + * @param number|string|\DateTime|\DateTimeInterface $value an hour (from 0 to 23), a standard period name ('am' or 'pm', lower or upper case) a \DateTimeInterface instance for which you want the name of the day period + * @param string $width the format name; it can be 'wide' (eg 'AM'), 'abbreviated' (eg 'AM') or 'narrow' (eg 'a') + * @param string $locale The locale to use. If empty we'll use the default locale set with {@link \Punic\Data::setDefaultLocale()}. + * @param bool $standAlone set to true to return the form used independently (such as in calendar header), set to false if the day period name will be part of a date + * + * @throws \Punic\Exception\BadArgumentType throws a BadArgumentType exception if $value is not valid + * @throws \Punic\Exception\ValueNotInList throws a ValueNotInList exception if $width is not valid + * @throws \Punic\Exception throws a generic exception in case of other problems (for instance if you specify an invalid locale) + * + * @return string returns an empty string if $value is empty, the name of the day period name otherwise + */ + public static function getDayperiodName($value, $width = 'wide', $locale = '', $standAlone = false) + { + static $dictionary = array('am', 'pm'); + $result = ''; + if ((!empty($value)) || ($value === 0) || ($value === '0')) { + $dayperiod = null; + $hours = null; + if (is_numeric($value)) { + $hours = (int) $value; + } elseif (is_string($value)) { + $s = strtolower($value); + if (in_array($s, $dictionary, true)) { + $dayperiod = $s; + } + } elseif ($value instanceof DateTimeInterface || $value instanceof DateTime) { + $dayperiod = $value->format('a'); + } + if (($hours !== null) && ($hours >= 0) && ($hours <= 23)) { + $dayperiod = ($hours < 12) ? 'am' : 'pm'; + } + if ($dayperiod === null) { + throw new Exception\BadArgumentType($value, 'day period'); + } + $data = Data::get('calendar', $locale); + $data = $data['dayPeriods'][$standAlone ? 'stand-alone' : 'format']; + if (!isset($data[$width])) { + throw new Exception\ValueNotInList($width, array_keys($data)); + } + $result = $data[$width][$dayperiod]; + } + + return $result; + } + + /** + * Get the name of a variable day period ("morning", "afternoon", etc.). + * + * The available periods, their start/end time and their names are locale-specific. + * + * @param number|string|\DateTime|\DateTimeInterface $value an hour (from 0 to 23), a \DateTimeInterface instance for which you want the name of the day period + * @param string $width the format name; it can be 'wide', 'abbreviated' or 'narrow' + * @param string $locale The locale to use. If empty we'll use the default locale set with {@link \Punic\Data::setDefaultLocale()}. + * @param bool $standAlone set to true to return the form used independently (e.g. "morning"), set to false if the day period name will be part of a date (e.g. "in the morning") + * + * @throws \Punic\Exception\BadArgumentType throws a BadArgumentType exception if $value is not valid + * @throws \Punic\Exception\ValueNotInList throws a ValueNotInList exception if $width is not valid + * @throws \Punic\Exception throws a generic exception in case of other problems (for instance if you specify an invalid locale) + * + * @return string returns an empty string if $value is empty, the name of the day period name otherwise + * + * @see https://www.unicode.org/reports/tr35/tr35-dates.html#Variable_periods + */ + public static function getVariableDayperiodName($value, $width = 'wide', $locale = '', $standAlone = false) + { + $result = ''; + if ((!empty($value)) || ($value === 0) || ($value === '0')) { + $data = Data::get('calendar', $locale); + $data = $data['dayPeriods'][$standAlone ? 'stand-alone' : 'format']; + if (!isset($data[$width])) { + throw new Exception\ValueNotInList($width, array_keys($data)); + } + $data = $data[$width]; + + $hours = null; + $dayperiod = null; + if (is_numeric($value)) { + $hours = (int) $value; + } elseif ($value instanceof DateTimeInterface || $value instanceof DateTime) { + $hours = (int) $value->format('G'); + } + + if (($hours !== null) && ($hours >= 0) && ($hours <= 23)) { + $dayperiods = Data::getLanguageNode(Data::getGeneric('dayPeriods'), $locale); + $time = sprintf('%02d:00', $hours); + foreach ($dayperiods as $dayperiod => $rule) { + if ($time < $rule['before']) { + break; + } + } + } + if ($dayperiod === null) { + throw new Exception\BadArgumentType($value, 'day period'); + } + + $result = $data[$dayperiod]; + } + + return $result; + } + + /** + * Returns the localized name of a timezone, no location-specific. + * + * @param string|\DateTime|\DateTimeInterface|\DateTimeZone $value the PHP name of a timezone, a `\DateTimeInterface` instance or a `\DateTimeZone` instance for which you want the localized timezone name + * @param string $width the format name; it can be 'long' (eg 'Greenwich Mean Time') or 'short' (eg 'GMT') + * @param string $kind set to 'daylight' to retrieve the daylight saving time name, set to 'standard' to retrieve the standard time, set to 'generic' to retrieve the generic name, set to '' to determine automatically the dst (if $value is \DateTime) or the generic (otherwise) + * @param string $locale The locale to use. If empty we'll use the default locale set with {@link \Punic\Data::setDefaultLocale()}. + * + * @throws \Punic\Exception throws a generic exception in case of problems (for instance if you specify an invalid locale) + * + * @return string returns an empty string if the timezone has not been found (maybe we don't have the data in the specified $width), the timezone name otherwise + */ + public static function getTimezoneNameNoLocationSpecific($value, $width = 'long', $kind = '', $locale = '') + { + $cacheKey = json_encode(array($value, $width, $kind, empty($locale) ? Data::getDefaultLocale() : $locale)); + if (isset(self::$timezoneCache[$cacheKey])) { + return self::$timezoneCache[$cacheKey]; + } + + $result = ''; + if (!empty($value)) { + $receivedPhpName = ''; + $date = '9999-12-31'; + if (is_string($value)) { + $receivedPhpName = $value; + } elseif ($value instanceof DateTimeInterface || $value instanceof DateTime) { + $receivedPhpName = static::getTimezoneNameFromDatetime($value); + $date = $value->format('Y-m-d H:i'); + if (empty($kind)) { + if ((int) $value->format('I') === 1) { + $kind = 'daylight'; + } else { + $kind = 'standard'; + } + } + } elseif ($value instanceof DateTimeZone) { + $receivedPhpName = static::getTimezoneNameFromTimezone($value); + } + if ($receivedPhpName !== '') { + $timezoneID = static::getTimezoneCanonicalID($receivedPhpName); + $timeZoneNames = Data::get('timeZoneNames', $locale); + $path = array_merge(array('zone'), explode('/', $timezoneID), array($width, array($kind, 'generic', 'standard'))); + $name = Data::getArrayValue($timeZoneNames, $path); + if ($name !== null) { + $result = $name; + } else { + $metaZones = Data::getGeneric('metaZones'); + $metazoneCode = ''; + $path = array_merge(array('metazoneInfo'), explode('/', $timezoneID)); + $tzInfo = Data::getArrayValue($metaZones, $path); + if (is_array($tzInfo)) { + foreach ($tzInfo as $tz) { + if (is_array($tz) && isset($tz['mzone'])) { + if (isset($tz['from']) && (strcmp($date, $tz['from']) < 0)) { + continue; + } + if (isset($tz['to']) && (strcmp($date, $tz['to']) >= 0)) { + continue; + } + $metazoneCode = $tz['mzone']; + break; + } + } + } + if ($metazoneCode === '') { + foreach ($metaZones['metazones'] as $metazone) { + if (strcasecmp($timezoneID, $metazone['type']) === 0) { + $metazoneCode = $metazone['other']; + } + } + } + if ($metazoneCode !== '') { + $data = Data::get('timeZoneNames', $locale); + if (isset($data['metazone'])) { + $data = $data['metazone']; + if (isset($data[$metazoneCode])) { + $data = $data[$metazoneCode]; + if (isset($data[$width])) { + $data = $data[$width]; + $lookFor = array(); + if (!empty($kind)) { + $lookFor[] = $kind; + } + $lookFor[] = 'generic'; + $lookFor[] = 'standard'; + $lookFor[] = 'daylight'; + foreach ($lookFor as $lf) { + if (isset($data[$lf])) { + $result = $data[$lf]; + break; + } + } + } + } + } + } + } + } + } + + self::$timezoneCache[$cacheKey] = $result; + + return $result; + } + + /** + * Returns the localized name of a timezone, location-specific. + * + * @param string|\DateTime|\DateTimeInterface|\DateTimeZone $value the php name of a timezone, or a \DateTime instance or a \DateTimeZone instance for which you want the localized timezone name + * @param string $locale The locale to use. If empty we'll use the default locale set with {@link \Punic\Data::setDefaultLocale()}. + * + * @return string returns an empty string if the timezone has not been found, the timezone name otherwise + * + * @see https://www.unicode.org/reports/tr35/tr35-dates.html#Time_Zone_Goals + */ + public static function getTimezoneNameLocationSpecific($value, $locale = '') + { + $result = ''; + if (!empty($value)) { + if (is_string($value)) { + $timezoneID = static::getTimezoneCanonicalID($value); + $timezone = null; + try { + $timezone = new DateTimeZone($timezoneID); + } catch (\Exception $x) { + return ''; + } + $location = $timezone->getLocation(); + } elseif ($value instanceof DateTimeInterface || $value instanceof DateTime) { + $timezone = $value->getTimezone(); + $location = self::getTimezoneLocationFromDatetime($value); + } elseif ($value instanceof DateTimeZone) { + $timezone = $value; + $location = $timezone->getLocation(); + } else { + throw new Exception\BadArgumentType($value, 'string, DateTime, or DateTimeZone'); + } + + $name = ''; + if (isset($location['country_code']) && $location['country_code'] !== '??') { + $data = Data::getGeneric('primaryZones'); + if (isset($data[$location['country_code']]) + || count(DateTimeZone::listIdentifiers(DateTimeZone::PER_COUNTRY, $location['country_code'])) === 1) { + $name = Territory::getName($location['country_code'], $locale); + } + } + + if ($name === '' && substr($timezone->getName(), 0, 7) !== 'Etc/GMT') { + $name = static::getTimezoneExemplarCity($value, false, $locale); + } + + if ($name !== '') { + $data = Data::get('timeZoneNames', $locale); + $result = sprintf($data['regionFormat'], $name); + } + } + + return $result; + } + + /** + * Returns the localized name of an exemplar city for a specific timezone. + * + * @param string|\DateTime|\DateTimeInterface|\DateTimeZone $value The PHP name of a timezone, a `\DateTimeInterface` instance or a `\DateTimeZone` instance + * @param bool $returnUnknownIfNotFound true If the exemplar city is not found, shall we return the translation of 'Unknown City'? + * @param string $locale The locale to use. If empty we'll use the default locale set in \Punic\Data + * + * @return string Returns an empty string if the exemplar city hasn't been found and $returnUnknownIfNotFound is false + */ + public static function getTimezoneExemplarCity($value, $returnUnknownIfNotFound = true, $locale = '') + { + $result = ''; + $locale = empty($locale) ? Data::getDefaultLocale() : $locale; + if (!empty($value)) { + if (is_string($value)) { + $receivedPhpName = $value; + } elseif ($value instanceof DateTimeInterface || $value instanceof DateTime) { + $receivedPhpName = static::getTimezoneNameFromDatetime($value); + } elseif ($value instanceof DateTimeZone) { + $receivedPhpName = static::getTimezoneNameFromTimezone($value); + } else { + $receivedPhpName = ''; + } + if ($receivedPhpName !== '') { + $timezoneID = static::getTimezoneCanonicalID($receivedPhpName); + $timeZoneNames = Data::get('timeZoneNames', $locale); + $path = array_merge(array('zone'), explode('/', $timezoneID), array('exemplarCity')); + $exemplarCity = Data::getArrayValue($timeZoneNames, $path); + if ($exemplarCity !== null) { + $result = $exemplarCity; + } + } + } + if ($result === '' && $returnUnknownIfNotFound) { + $result = 'Unknown City'; + $s = static::getTimezoneExemplarCity('Etc/Unknown', false, $locale); + if ($s !== '') { + $result = $s; + } + } + + return $result; + } + + /** + * Returns true if a locale has a 12-hour clock, false if 24-hour clock. + * + * @param string $locale The locale to use. If empty we'll use the default locale set in \Punic\Data + * + * @throws \Punic\Exception Throws an exception in case of problems + * + * @return bool + */ + public static function has12HoursClock($locale = '') + { + static $cache = array(); + $locale = empty($locale) ? Data::getDefaultLocale() : $locale; + if (!isset($cache[$locale])) { + $format = static::getTimeFormat('short', $locale); + $format = str_replace("''", '', $format); + $cache[$locale] = (strpos($format, 'a') === false) ? false : true; + } + + return $cache[$locale]; + } + + /** + * Retrieve the first weekday for a specific locale (from 0-Sunday to 6-Saturday). + * + * @param string $locale The locale to use. If empty we'll use the default locale set in \Punic\Data + * + * @return int Returns a number from 0 (Sunday) to 7 (Saturday) + */ + public static function getFirstWeekday($locale = '') + { + static $cache = array(); + $locale = empty($locale) ? Data::getDefaultLocale() : $locale; + if (!isset($cache[$locale])) { + $result = 0; + $data = Data::getGeneric('weekData'); + $i = Data::getTerritoryNode($data['firstDay'], $locale); + if (is_int($i)) { + $result = $i; + } + $cache[$locale] = $result; + } + + return $cache[$locale]; + } + + /** + * Returns the sorted list of weekdays, starting from {@link getFirstWeekday}. + * + * @param string|false $namesWidth If false you'll get only the list of weekday identifiers (for instance: [0, 1, 2, 3, 4, 5, 6]), + * If it's a string it must be one accepted by {@link getWeekdayName}, and you'll get an array like this: [{id: 0, name: 'Monday', ..., {id: 6, name: 'Sunday'}] + * @param string $locale The locale to use. If empty we'll use the default locale set in \Punic\Data + * + * @return array + */ + public static function getSortedWeekdays($namesWidth = false, $locale = '') + { + $codes = array(); + $code = static::getFirstWeekday($locale); + for ($count = 0; $count < 7; $count++) { + $codes[] = $code; + $code++; + if ($code === 7) { + $code = 0; + } + } + if (empty($namesWidth)) { + $result = $codes; + } else { + $result = array(); + foreach ($codes as $code) { + $result[] = array('id' => $code, 'name' => static::getWeekdayName($code, $namesWidth, $locale, true)); + } + } + + return $result; + } + + /** + * Get the ISO format for a date. + * + * @param string $width The format name; it can be 'full' (eg 'EEEE, MMMM d, y' - 'Wednesday, August 20, 2014'), 'long' (eg 'MMMM d, y' - 'August 20, 2014'), 'medium' (eg 'MMM d, y' - 'August 20, 2014') or 'short' (eg 'M/d/yy' - '8/20/14'), + * or a skeleton pattern prefixed by '~', e.g. '~yMd'. + * @param string $locale The locale to use. If empty we'll use the default locale set in \Punic\Data + * + * @throws Exception Throws an exception in case of problems + * + * @return string Returns the requested ISO format + * + * @see https://cldr.unicode.org/translation/date-time/datetime-patterns + * @see https://cldr.unicode.org/translation/date-time + * @see https://www.unicode.org/reports/tr35/tr35-dates.html#Date_Format_Patterns + */ + public static function getDateFormat($width, $locale = '') + { + if ($width !== '' && $width[0] === '~') { + return self::getSkeletonFormat(substr($width, 1), $locale); + } + + $data = Data::get('calendar', $locale); + $data = $data['dateFormats']; + if (!isset($data[$width])) { + throw new Exception\ValueNotInList($width, array_keys($data)); + } + + return $data[$width]; + } + + /** + * Get the ISO format for a time. + * + * @param string $width The format name; it can be 'full' (eg 'h:mm:ss a zzzz' - '11:42:13 AM GMT+2:00'), 'long' (eg 'h:mm:ss a z' - '11:42:13 AM GMT+2:00'), 'medium' (eg 'h:mm:ss a' - '11:42:13 AM') or 'short' (eg 'h:mm a' - '11:42 AM'), + * or a skeleton pattern prefixed by '~', e.g. '~Hm'. + * @param string $locale The locale to use. If empty we'll use the default locale set in \Punic\Data + * + * @throws \Punic\Exception Throws an exception in case of problems + * + * @return string Returns the requested ISO format + * + * @see https://cldr.unicode.org/translation/date-time/datetime-patterns + * @see https://cldr.unicode.org/translation/date-time + * @see https://www.unicode.org/reports/tr35/tr35-dates.html#Date_Format_Patterns + */ + public static function getTimeFormat($width, $locale = '') + { + if ($width !== '' && $width[0] === '~') { + return self::getSkeletonFormat(substr($width, 1), $locale); + } + + $data = Data::get('calendar', $locale); + $data = $data['timeFormats']; + if (!isset($data[$width])) { + throw new Exception\ValueNotInList($width, array_keys($data)); + } + + return $data[$width]; + } + + /** + * Get the ISO format for a date/time. + * + * @param string $width The format name; it can be 'full', 'long', 'medium', 'short' or a combination for date+time like 'full|short' or a combination for format+date+time like 'full|full|short', + * or a skeleton pattern prefixed by '~', e.g. '~yMd'. + * @param string $locale The locale to use. If empty we'll use the default locale set in \Punic\Data + * + * @throws \Punic\Exception Throws an exception in case of problems + * + * @return string Returns the requested ISO format + * + * @see https://cldr.unicode.org/translation/date-time/datetime-patterns + * @see https://cldr.unicode.org/translation/date-time + * @see https://www.unicode.org/reports/tr35/tr35-dates.html#Date_Format_Patterns + */ + public static function getDatetimeFormat($width, $locale = '') + { + return static::getDatetimeFormatReal($width, $locale); + } + + /** + * Get the ISO format based on a skeleton. + * + * @param string $skeleton The locale-independent skeleton, e.g. "yMMMd" or "Hm". + * @param string $locale The locale to use. If empty we'll use the default locale set in \Punic\Data + * + * @throws \Punic\Exception Throws an exception in case of problems + * + * @return string Returns the requested ISO format + * + * @see https://cldr.unicode.org/translation/date-time/datetime-patterns#h.j31ghafvbgku + * @see https://www.unicode.org/reports/tr35/tr35-dates.html#availableFormats_appendItems + */ + public static function getSkeletonFormat($skeleton, $locale = '') + { + static $cache = array(); + if (empty($locale)) { + $locale = Data::getDefaultLocale(); + } + if (isset($cache[$locale][$skeleton])) { + return $cache[$locale][$skeleton]; + } + $data = Data::get('calendar', $locale); + $data = $data['dateTimeFormats']['availableFormats']; + if (isset($data[$skeleton])) { + $format = $data[$skeleton]; + } else { + list($preprocessedSkeleton, $replacements) = self::preprocessSkeleton($skeleton, $locale); + + $match = self::getBestMatchingSkeleton($preprocessedSkeleton, array_keys($data)); + + if (!$match) { + // If skeleton contains both date and time fields, try matching date and time separately. + $dateLength = strspn($preprocessedSkeleton, 'GyYurUQqMLlwWEcedDFg'); + if ($dateLength > 0 && $dateLength < strlen($preprocessedSkeleton)) { + $dateSkeleton = substr($preprocessedSkeleton, 0, $dateLength); + $timeSkeleton = substr($preprocessedSkeleton, $dateLength); + + return self::getDatetimeFormat('~' . $dateSkeleton . '|~' . $timeSkeleton, $locale); + } + + throw new Exception('Matching skeleton not found: ' . $skeleton); + } + + list($matchSkeleton, $countAdjustments) = $match; + + $format = self::postprocessSkeletonFormat($data[$matchSkeleton], $countAdjustments, $replacements, $locale); + } + + $cache[$locale][$skeleton] = $format; + + return $format; + } + + /** + * Get the ISO format for a date/time interval. + * + * @param string $skeleton The locale-independent skeleton, e.g. "yMMMd" or "Hm". + * @param string $greatestDifference The calendar field with the greatest distance between the two dates. Must be one of the fields mentioned in $differenceFields. + * @param string $locale The locale to use. If empty we'll use the default locale set in \Punic\Data. + * + * @throws \Punic\Exception Throws an exception in case of problems + * + * @return array an array with two entries: + * - string: The ISO interval format + * - bool|null: Whether the earliest date is the first of the two dates in the pattern, + * or null if the dates are identical within the granularity specified by the skeleton + * + * @see https://www.unicode.org/reports/tr35/tr35-dates.html#intervalFormats + */ + public static function getIntervalFormat($skeleton, $greatestDifference, $locale = '') + { + static $cache = array(); + if (empty($locale)) { + $locale = Data::getDefaultLocale(); + } + if (isset($cache[$locale][$skeleton][$greatestDifference])) { + return $cache[$locale][$skeleton][$greatestDifference]; + } + + $data = Data::get('calendar', $locale); + $data = $data['dateTimeFormats']['intervalFormats']; + + if (isset($data[$skeleton])) { + $preprocessedSkeleton = $skeleton; + $replacements = array(); + $match = array($skeleton, 0); + } else { + list($preprocessedSkeleton, $replacements) = self::preprocessSkeleton($skeleton, $locale); + + $match = self::getBestMatchingSkeleton($preprocessedSkeleton, array_keys($data)); + } + + if ($match) { + list($matchSkeleton, $sWidth) = $match; + } + + // The spec does not unambiguously define "greatest difference". + $adjustedGreatestDifference = self::adjustGreatestDifference($greatestDifference, $preprocessedSkeleton); + + if ($adjustedGreatestDifference === '') { + // Greatest difference is less than skeleton granularity, so we display the + // interval as a single date. + return array(self::getSkeletonFormat($skeleton, $locale), null); + } + + $earliestFirst = null; + if ($match && isset($data[$matchSkeleton][$adjustedGreatestDifference])) { + $format = $data[$matchSkeleton][$adjustedGreatestDifference]; + + if (substr($format, 0, 12) === 'latestFirst:') { + $format = substr($format, 12); + $earliestFirst = false; + } elseif (substr($format, 0, 14) === 'earliestFirst:') { + $format = substr($format, 14); + $earliestFirst = true; + } + + $format = self::postprocessSkeletonFormat($format, $sWidth, $replacements, $locale); + } else { + // Either there was no matching skeleton, or there was no pattern matching + // the specific greatest difference, so format using the fallback format. + // If skeleton contains both date and time fields, and the difference is les + // than a day, format using "date - date time", otherwise use "date time - + // date time". This is not mandated by UTS #35, but ICU4J does this. + $dateLength = strspn($preprocessedSkeleton, self::$dateFields); + if ($dateLength > 0 && $dateLength < strlen($preprocessedSkeleton) && strspn($adjustedGreatestDifference, self::$dateFields) === 0) { + $timeSkeleton = substr($preprocessedSkeleton, $dateLength); + + $wholeFormat = self::getSkeletonFormat($preprocessedSkeleton, $locale); + $timeFormat = self::getSkeletonFormat($timeSkeleton, $locale); + + $format = sprintf($data['intervalFormatFallback'], $wholeFormat, $timeFormat); + } else { + $wholeFormat = self::getSkeletonFormat($preprocessedSkeleton, $locale); + $format = sprintf($data['intervalFormatFallback'], $wholeFormat, $wholeFormat); + } + } + + // If pattern does not declare an order, use the order define by the fallback pattern. + if ($earliestFirst === null) { + $earliestFirst = strpos($data['intervalFormatFallback'], '%1') < strpos($data['intervalFormatFallback'], '%2'); + } + + $result = array($format, $earliestFirst); + + $cache[$locale][$skeleton][$greatestDifference] = $result; + + return $result; + } + + /** + * Returns the difference in days between two dates (or between a date and today). + * + * @param \DateTime|\DateTimeInterface $dateEnd The first date + * @param \DateTime|\DateTimeInterface|null $dateStart The final date (if it has a timezone different than $dateEnd, we'll use the one of $dateEnd) + * + * @throws Exception\BadArgumentType + * + * @return int Returns the difference $dateEnd - $dateStart in days + */ + public static function getDeltaDays($dateEnd, $dateStart = null) + { + if (!($dateEnd instanceof DateTimeInterface || $dateEnd instanceof DateTime)) { + throw new Exception\BadArgumentType($dateEnd, '\\DateTime'); + } + if (empty($dateStart) && ($dateStart !== 0) && ($dateStart !== '0')) { + $dateStart = new DateTime('now', $dateEnd->getTimezone()); + } + if (!($dateStart instanceof DateTimeInterface || $dateStart instanceof DateTime)) { + throw new Exception\BadArgumentType($dateStart, '\\DateTime'); + } + if ($dateStart->getOffset() !== $dateEnd->getOffset()) { + $dateStart = new DateTime('@' . $dateStart->getTimestamp()); + $dateStart->setTimezone($dateEnd->getTimezone()); + } + $utc = new DateTimeZone('UTC'); + $dateEndUTC = new DateTime($dateEnd->format('Y-m-d'), $utc); + $dateStartUTC = new DateTime($dateStart->format('Y-m-d'), $utc); + $seconds = $dateEndUTC->getTimestamp() - $dateStartUTC->getTimestamp(); + + return (int) (round($seconds / 86400)); + } + + /** + * Describe an interval between two dates (eg '2 days and 4 hours'). + * + * @param \DateTime|\DateTimeInterface $dateEnd The first date + * @param \DateTime|\DateTimeInterface|null $dateStart The final date (if it has a timezone different than $dateEnd, we'll use the one of $dateEnd) + * @param int $maxParts The maximum parts (eg with 2 you may have '2 days and 4 hours', with 3 '2 days, 4 hours and 24 minutes') + * @param string $width The format name; it can be 'long' (eg '3 seconds'), 'short' (eg '3 s') or 'narrow' (eg '3s') + * @param string $locale The locale to use. If empty we'll use the default locale set in \Punic\Data + * + * @throws Exception\BadArgumentType + * + * @return string + */ + public static function describeInterval($dateEnd, $dateStart = null, $maxParts = 2, $width = 'short', $locale = '') + { + if (!($dateEnd instanceof DateTimeInterface || $dateEnd instanceof DateTime)) { + throw new Exception\BadArgumentType($dateEnd, '\\DateTime'); + } + if (empty($dateStart) && ($dateStart !== 0) && ($dateStart !== '0')) { + $dateStart = new DateTime('now', $dateEnd->getTimezone()); + } + if (!($dateStart instanceof DateTimeInterface || $dateStart instanceof DateTime)) { + throw new Exception\BadArgumentType($dateStart, '\\DateTime'); + } + if ($dateStart->getOffset() !== $dateEnd->getOffset()) { + $dateStart = new DateTime('@' . $dateStart->getTimestamp()); + $dateStart->setTimezone($dateEnd->getTimezone()); + } + $utc = new DateTimeZone('UTC'); + $dateEndUTC = new DateTime($dateEnd->format('Y-m-d H:i:s'), $utc); + $dateStartUTC = new DateTime($dateStart->format('Y-m-d H:i:s'), $utc); + + $parts = array(); + $data = Data::get('dateFields', $locale); + if ($dateEndUTC->getTimestamp() == $dateStartUTC->getTimestamp()) { + $parts[] = $data['second']['relative-type-0']; + } else { + $diff = $dateStartUTC->diff($dateEndUTC, true); + $mostFar = 0; + $maxDistance = 3; + if (($mostFar < $maxDistance) && ($diff->y > 0)) { + $parts[] = Unit::format($diff->y, 'duration/year', $width, $locale); + $mostFar = 0; + } elseif (!empty($parts)) { + $mostFar++; + } + if (($mostFar < $maxDistance) && ($diff->m > 0)) { + $parts[] = Unit::format($diff->m, 'duration/month', $width, $locale); + $mostFar = 0; + } elseif (!empty($parts)) { + $mostFar++; + } + if (($mostFar < $maxDistance) && ($diff->d > 0)) { + $parts[] = Unit::format($diff->d, 'duration/day', $width, $locale); + $mostFar = 0; + } elseif (!empty($parts)) { + $mostFar++; + } + if (($mostFar < $maxDistance) && ($diff->h > 0)) { + $parts[] = Unit::format($diff->h, 'duration/hour', $width, $locale); + $mostFar = 0; + } elseif (!empty($parts)) { + $mostFar++; + } + if (($mostFar < $maxDistance) && ($diff->i > 0)) { + $parts[] = Unit::format($diff->i, 'duration/minute', $width, $locale); + $mostFar = 0; + } elseif (!empty($parts)) { + $mostFar++; + } + if (empty($parts) || ($diff->s > 0)) { + $parts[] = Unit::format($diff->s, 'duration/second', $width, $locale); + } + if ($maxParts < count($parts)) { + $parts = array_slice($parts, 0, $maxParts); + } + } + switch ($width) { + case 'narrow': + case 'short': + $joined = Misc::joinUnits($parts, $width, $locale); + break; + default: + $joined = Misc::joinAnd($parts, '', $locale); + break; + } + + return $joined; + } + + /** + * Format a date. + * + * @param \DateTime|\DateTimeInterface $value The \DateTimeInterface instance for which you want the localized textual representation + * @param string $width The format name; it can be 'full' (eg 'EEEE, MMMM d, y' - 'Wednesday, August 20, 2014'), 'long' (eg 'MMMM d, y' - 'August 20, 2014'), 'medium' (eg 'MMM d, y' - 'August 20, 2014') or 'short' (eg 'M/d/yy' - '8/20/14'), + * or a skeleton pattern prefixed by '~', e.g. '~yMd'. + * You can also append a caret ('^') or an asterisk ('*') to $width. If so, special day names may be used (like 'Today', 'Yesterday', 'Tomorrow' with '^' and 'today', 'yesterday', 'tomorrow' width '*') instead of the date. + * @param string $locale The locale to use. If empty we'll use the default locale set in \Punic\Data + * + * @throws \Punic\Exception Throws an exception in case of problems + * + * @return string Returns an empty string if $value is empty, the localized textual representation otherwise + * + * @see https://cldr.unicode.org/translation/date-time/datetime-patterns + * @see https://cldr.unicode.org/translation/date-time + * @see https://www.unicode.org/reports/tr35/tr35-dates.html#Date_Format_Patterns + */ + public static function formatDate($value, $width, $locale = '') + { + $c = is_string($width) ? @substr($width, -1) : ''; + if (($c === '^') || ($c === '*')) { + $dayName = static::getDateRelativeName($value, ($c === '^') ? true : false, $locale); + if ($dayName !== '') { + return $dayName; + } + $width = substr($width, 0, -1); + } + + return static::format( + $value, + static::getDateFormat($width, $locale), + $locale + ); + } + + /** + * Format a date (extended version: various date/time representations - see toDateTime()). + * + * @param number|\DateTime|\DateTimeInterface|string $value A Unix timestamp, a `\DateTimeInterface` instance or a string accepted by {@link https://www.php.net/manual/function.strtotime.php strtotime}. + * @param string $width The format name; it can be 'full' (eg 'EEEE, MMMM d, y' - 'Wednesday, August 20, 2014'), 'long' (eg 'MMMM d, y' - 'August 20, 2014'), 'medium' (eg 'MMM d, y' - 'August 20, 2014') or 'short' (eg 'M/d/yy' - '8/20/14'), + * or a skeleton pattern prefixed by '~', e.g. '~yMd'. + * You can also append a caret ('^') or an asterisk ('*') to $width. If so, special day names may be used (like 'Today', 'Yesterday', 'Tomorrow' with '^' and 'today', 'yesterday', 'tomorrow' width '*') instead of the date. + * @param string|\DateTimeZone $toTimezone The timezone to set; leave empty to use the default timezone (or the timezone associated to $value if it's already a \DateTime) + * @param string $locale The locale to use. If empty we'll use the default locale set in \Punic\Data + * + * @throws \Punic\Exception Throws an exception in case of problems + * + * @return string Returns an empty string if $value is empty, the localized textual representation otherwise + * + * @see toDateTime() + * @see https://cldr.unicode.org/translation/date-time/datetime-patterns + * @see https://cldr.unicode.org/translation/date-time + * @see https://www.unicode.org/reports/tr35/tr35-dates.html#Date_Format_Patterns + */ + public static function formatDateEx($value, $width, $toTimezone = '', $locale = '') + { + return static::formatDate( + static::toDateTime($value, $toTimezone), + $width, + $locale + ); + } + + /** + * Format a time. + * + * @param \DateTime|\DateTimeInterface $value The \DateTimeInterface instance for which you want the localized textual representation + * @param string $width The format name; it can be 'full' (eg 'h:mm:ss a zzzz' - '11:42:13 AM GMT+2:00'), 'long' (eg 'h:mm:ss a z' - '11:42:13 AM GMT+2:00'), 'medium' (eg 'h:mm:ss a' - '11:42:13 AM') or 'short' (eg 'h:mm a' - '11:42 AM'), + * or a skeleton pattern prefixed by '~', e.g. '~Hm'. + * @param string $locale The locale to use. If empty we'll use the default locale set in \Punic\Data + * + * @throws \Punic\Exception Throws an exception in case of problems + * + * @return string Returns an empty string if $value is empty, the localized textual representation otherwise + * + * @see https://cldr.unicode.org/translation/date-time/datetime-patterns + * @see https://cldr.unicode.org/translation/date-time + * @see https://www.unicode.org/reports/tr35/tr35-dates.html#Date_Format_Patterns + */ + public static function formatTime($value, $width, $locale = '') + { + return static::format( + $value, + static::getTimeFormat($width, $locale), + $locale + ); + } + + /** + * Format a time (extended version: various date/time representations - see toDateTime()). + * + * @param number|\DateTime|\DateTimeInterface|string $value A Unix timestamp, a `\DateTimeInterface` instance or a string accepted by {@link https://www.php.net/manual/function.strtotime.php strtotime}. + * @param string $width The format name; it can be 'full' (eg 'h:mm:ss a zzzz' - '11:42:13 AM GMT+2:00'), 'long' (eg 'h:mm:ss a z' - '11:42:13 AM GMT+2:00'), 'medium' (eg 'h:mm:ss a' - '11:42:13 AM') or 'short' (eg 'h:mm a' - '11:42 AM'), + * or a skeleton pattern prefixed by '~', e.g. '~Hm'. + * @param string|\DateTimeZone $toTimezone The timezone to set; leave empty to use the default timezone (or the timezone associated to $value if it's already a \DateTime) + * @param string $locale The locale to use. If empty we'll use the default locale set in \Punic\Data + * + * @throws \Punic\Exception Throws an exception in case of problems + * + * @return string Returns an empty string if $value is empty, the localized textual representation otherwise + * + * @see toDateTime() + * @see https://cldr.unicode.org/translation/date-time/datetime-patterns + * @see https://cldr.unicode.org/translation/date-time + * @see https://www.unicode.org/reports/tr35/tr35-dates.html#Date_Format_Patterns + */ + public static function formatTimeEx($value, $width, $toTimezone = '', $locale = '') + { + return static::formatTime( + static::toDateTime($value, $toTimezone), + $width, + $locale + ); + } + + /** + * Format a date/time. + * + * @param \DateTime|\DateTimeInterface $value The \DateTimeInterface instance for which you want the localized textual representation + * @param string $width The format name; it can be 'full', 'long', 'medium', 'short' or a skeleton pattern prefixed by '~', + * or a combination for date+time like 'full|short' or a combination for format+date+time like 'full|full|short' + * You can also append an asterisk ('*') to the date part of $width. If so, special day names may be used (like 'Today', 'Yesterday', 'Tomorrow') instead of the date part. + * @param string $locale The locale to use. If empty we'll use the default locale set in \Punic\Data + * + * @throws \Punic\Exception Throws an exception in case of problems + * + * @return string Returns an empty string if $value is empty, the localized textual representation otherwise + * + * @see https://cldr.unicode.org/translation/date-time/datetime-patterns + * @see https://cldr.unicode.org/translation/date-time + * @see https://www.unicode.org/reports/tr35/tr35-dates.html#Date_Format_Patterns + */ + public static function formatDatetime($value, $width, $locale = '') + { + $overrideDateFormat = ''; + if (is_string($width)) { + $chunks = explode('|', $width); + switch (count($chunks)) { + case 1: + case 2: + $dateFormat = $chunks[0]; + break; + case 3: + $dateFormat = $chunks[1]; + break; + default: + $dateFormat = ''; + break; + } + $c = $dateFormat !== '' ? @substr($dateFormat, -1) : ''; + if (($c === '^') || ($c === '*')) { + $dayName = static::getDateRelativeName($value, ($c === '^') ? true : false, $locale); + if ($dayName !== '') { + $overrideDateFormat = "'{$dayName}'"; + } + } + } + + return static::format( + $value, + static::getDatetimeFormatReal($width, $locale, $overrideDateFormat), + $locale + ); + } + + /** + * Format a date/time (extended version: various date/time representations - see toDateTime()). + * + * @param number|\DateTime|\DateTimeInterface|string $value A Unix timestamp, a `\DateTimeInterface` instance or a string accepted by {@link https://www.php.net/manual/function.strtotime.php strtotime}. + * @param string $width The format name; it can be 'full', 'long', 'medium', 'short' or a combination for date+time like 'full|short' or a combination for format+date+time like 'full|full|short' + * You can also append an asterisk ('*') to the date part of $width. If so, special day names may be used (like 'Today', 'Yesterday', 'Tomorrow') instead of the date part. + * @param string|\DateTimeZone $toTimezone The timezone to set; leave empty to use the default timezone (or the timezone associated to $value if it's already a \DateTime) + * @param string $locale The locale to use. If empty we'll use the default locale set in \Punic\Data + * + * @throws \Punic\Exception Throws an exception in case of problems + * + * @return string Returns an empty string if $value is empty, the localized textual representation otherwise + * + * @see toDateTime() + * @see https://cldr.unicode.org/translation/date-time/datetime-patterns + * @see https://cldr.unicode.org/translation/date-time + * @see https://www.unicode.org/reports/tr35/tr35-dates.html#Date_Format_Patterns + */ + public static function formatDatetimeEx($value, $width, $toTimezone = '', $locale = '') + { + return static::formatDatetime( + static::toDateTime($value, $toTimezone), + $width, + $locale + ); + } + + /** + * Format a date/time interval. + * + * @param \DateTime|\DateTimeInterface $earliest the first date of the interval + * @param \DateTime|\DateTimeInterface $latest The last date of the + * @param string $skeleton The locale-independent skeleton, e.g. "yMMMd" or "Hm". + * @param string $locale The locale to use. If empty we'll use the default locale set in \Punic\Data. + * + * @return string Returns the localized textual representation of the interval + * + * @see https://www.unicode.org/reports/tr35/tr35-dates.html#intervalFormats + */ + public static function formatInterval($earliest, $latest, $skeleton, $locale = '') + { + $greatestDifference = self::getGreatestDifference($earliest, $latest); + + list($format, $earliestFirst) = static::getIntervalFormat($skeleton, $greatestDifference, $locale); + + if ($earliestFirst === null) { + return static::format($earliest, $format, $locale); + } + list($format1, $format2) = static::splitIntervalFormat($format); + + return static::format( + $earliestFirst ? $earliest : $latest, + $format1, + $locale + ) . static::format( + $earliestFirst ? $latest : $earliest, + $format2, + $locale + ); + } + + /** + * Format a date/time interval (extended version: various date/time representations - see toDateTime()). + * + * @param number|\DateTime|\DateTimeInterface|string $earliest An Unix timestamp, a `\DateTime` instance or a string accepted by {@link https://www.php.net/manual/function.strtotime.php strtotime}. + * @param number|\DateTime|\DateTimeInterface|string $latest An Unix timestamp, a `\DateTime` instance or a string accepted by {@link https://www.php.net/manual/function.strtotime.php strtotime}. + * @param string $skeleton The locale-independent skeleton, e.g. "yMMMd" or "Hm". + * @param string|\DateTimeZone $toTimezone The timezone to set; leave empty to use the default timezone (or the timezone associated to $value if it's already a \DateTime) + * @param string $locale The locale to use. If empty we'll use the default locale set in \Punic\Data. + * + * @return string Returns the localized textual representation of the interval + */ + public static function formatIntervalEx($earliest, $latest, $skeleton, $toTimezone = '', $locale = '') + { + return static::formatInterval( + static::toDateTime($earliest, $toTimezone), + static::toDateTime($latest, $toTimezone), + $skeleton, + $locale + ); + } + + /** + * Format a date and/or time. + * + * @param \DateTime|\DateTimeInterface $value The \DateTimeInterface instance for which you want the localized textual representation + * @param string $format The ISO format that specify how to render the date/time. The following extra format chunks are available: + * - 'P': ISO-8601 numeric representation of the day of the week (same as 'e' but not locale dependent) + * - 'PP': Numeric representation of the day of the week, from 0 (for Sunday) to 6 (for Saturday) + * - 'PPP': English ordinal suffix for the day of the month + * - 'PPPP': The day of the year (starting from 0) + * - 'PPPPP': Number of days in the given month + * - 'PPPPPP': Whether it's a leap year: 1 if it is a leap year, 0 otherwise. + * - 'PPPPPPP': Lowercase Ante meridiem and Post meridiem (English only, for other locales it's the same as 'a') + * - 'PPPPPPPP': Swatch Internet time + * - 'PPPPPPPPP': Microseconds + * - 'PPPPPPPPPP': Whether or not the date is in daylight saving time 1 if Daylight Saving Time, 0 otherwise. + * - 'PPPPPPPPPPP': Timezone offset in seconds + * - 'PPPPPPPPPPPP': RFC 2822 formatted date (Example: 'Thu, 21 Dec 2000 16:01:07 +0200') + * - 'PPPPPPPPPPPPP': Seconds since the Unix Epoch (January 1 1970 00:00:00 GMT) + * @param string $locale The locale to use. If empty we'll use the default locale set in \Punic\Data + * + * @throws \Punic\Exception Throws an exception in case of problems + * + * @return string Returns an empty string if $value is empty, the localized textual representation otherwise + * + * @see https://cldr.unicode.org/translation/date-time/datetime-patterns + * @see https://cldr.unicode.org/translation/date-time + * @see https://www.unicode.org/reports/tr35/tr35-dates.html#Date_Format_Patterns + */ + public static function format($value, $format, $locale = '') + { + $result = ''; + if (!empty($value)) { + if (!($value instanceof DateTimeInterface || $value instanceof DateTime)) { + throw new Exception\BadArgumentType($value, '\\DateTime'); + } + if (!is_string($format) || $format === '') { + throw new Exception\BadArgumentType($format, 'date/time ISO format'); + } + $decoder = self::tokenizeFormat($format); + if (empty($locale)) { + $locale = Data::getDefaultLocale(); + } + foreach ($decoder as $chunk) { + if (is_string($chunk)) { + $result .= $chunk; + } else { + $functionName = $chunk[0]; + $count = $chunk[1]; + $result .= static::$functionName($value, $count, $locale); + } + } + } + + return $result; + } + + /** + * Format a date and/or time (extended version: various date/time representations - see toDateTime()). + * + * @param number|\DateTime|\DateTimeInterface|string $value A Unix timestamp, a `\DateTimeInterface` instance or a string accepted by {@link https://www.php.net/manual/function.strtotime.php strtotime}. + * @param string $format The ISO format that specify how to render the date/time. The following extra format chunks are valid: + * - 'P': ISO-8601 numeric representation of the day of the week (same as 'e' but not locale dependent) + * - 'PP': Numeric representation of the day of the week, from 0 (for Sunday) to 6 (for Saturday) + * - 'PPP': English ordinal suffix for the day of the month + * - 'PPPP': The day of the year (starting from 0) + * - 'PPPPP': Number of days in the given month + * - 'PPPPPP': Whether it's a leap year: 1 if it is a leap year, 0 otherwise. + * - 'PPPPPPP': Lowercase Ante meridiem and Post meridiem (English only, for other locales it's the same as 'a') + * - 'PPPPPPPP': Swatch Internet time + * - 'PPPPPPPPP': Microseconds + * - 'PPPPPPPPPP': Whether or not the date is in daylight saving time 1 if Daylight Saving Time, 0 otherwise. + * - 'PPPPPPPPPPP': Timezone offset in seconds + * - 'PPPPPPPPPPPP': RFC 2822 formatted date (Example: 'Thu, 21 Dec 2000 16:01:07 +0200') + * - 'PPPPPPPPPPPPP': Seconds since the Unix Epoch (January 1 1970 00:00:00 GMT) + * @param string|\DateTimeZone $toTimezone The timezone to set; leave empty to use the default timezone (or the timezone associated to $value if it's already a \DateTime) + * @param string $locale The locale to use. If empty we'll use the default locale set in \Punic\Data + * + * @throws \Punic\Exception Throws an exception in case of problems + * + * @return string Returns an empty string if $value is empty, the localized textual representation otherwise + * + * @see https://cldr.unicode.org/translation/date-time/datetime-patterns + * @see https://cldr.unicode.org/translation/date-time + * @see https://www.unicode.org/reports/tr35/tr35-dates.html#Date_Format_Patterns + */ + public static function formatEx($value, $format, $toTimezone = '', $locale = '') + { + return static::format( + static::toDateTime($value, $toTimezone), + $format, + $locale + ); + } + + /** + * Retrieve the relative day name (eg 'yesterday', 'tomorrow'), if available. + * + * @param \DateTime|\DateTimeInterface $datetime The date for which you want the relative day name + * @param bool $ucFirst Force first letter to be upper case? + * @param string $locale The locale to use. If empty we'll use the default locale set in \Punic\Data + * + * @return string Returns the relative name if available, otherwise returns an empty string + */ + public static function getDateRelativeName($datetime, $ucFirst = false, $locale = '') + { + $result = ''; + $deltaDays = static::getDeltaDays($datetime); + $data = Data::get('dateFields', $locale); + if (isset($data['day'])) { + $data = $data['day']; + $key = "relative-type-{$deltaDays}"; + if (isset($data[$key])) { + $result = $data[$key]; + if ($ucFirst) { + $result = Misc::fixCase($result, 'titlecase-firstword'); + } + } + } + + return $result; + } + + /** + * @param string $width + * @param string $locale + * @param string $overrideDateFormat + * @param string $overrideTimeFormat + * + * @throws Exception\BadArgumentType + * @throws Exception\ValueNotInList + * + * @return string + */ + protected static function getDatetimeFormatReal($width, $locale = '', $overrideDateFormat = '', $overrideTimeFormat = '') + { + $chunks = explode('|', @str_replace(array('*', '^'), '', $width)); + switch (count($chunks)) { + case 1: + if ($width !== '' && $width[0] === '~') { + return self::getSkeletonFormat(substr($width, 1), $locale); + } + $timeWidth = $dateWidth = $wholeWidth = $chunks[0]; + break; + case 2: + $dateWidth = $chunks[0]; + $timeWidth = $chunks[1]; + $wholeWidth = self::getDatetimeWidth($dateWidth, $timeWidth); + break; + case 3: + $wholeWidth = $chunks[0]; + $dateWidth = $chunks[1]; + $timeWidth = $chunks[2]; + break; + default: + throw new Exception\BadArgumentType($width, 'pipe-separated list of strings (from 1 to 3 chunks)'); + } + $data = Data::get('calendar', $locale); + $data = $data['dateTimeFormats']; + if (!isset($data[$wholeWidth])) { + throw new Exception\ValueNotInList($wholeWidth, array_keys(array_filter($data, 'is_string'))); + } + + return sprintf( + $data[$wholeWidth], + $overrideTimeFormat !== '' ? $overrideTimeFormat : static::getTimeFormat($timeWidth, $locale), + $overrideDateFormat !== '' ? $overrideDateFormat : static::getDateFormat($dateWidth, $locale) + ); + } + + /** + * @param string $dateWidth + * + * @return string + */ + protected static function getDatetimeWidth($dateWidth) + { + if ($dateWidth === '' || $dateWidth[0] !== '~') { + return $dateWidth; + } + + // Select fullWidth according to UTS #35, part 4, section 2.6.2.2. + // Strip string literal text. + $dateWidth = preg_replace("@'.*?'@", '', $dateWidth); + if (strpos($dateWidth, 'MMMM') !== false && strpos($dateWidth, 'MMMMM') == false + || strpos($dateWidth, 'LLLL') !== false && strpos($dateWidth, 'LLLLL') == false) { + if (strpos($dateWidth, 'E') !== false + || strpos($dateWidth, 'e') !== false + || strpos($dateWidth, 'c') !== false) { + $wholeWidth = 'full'; + } else { + $wholeWidth = 'long'; + } + } elseif (strpos($dateWidth, 'MMM') !== false || strpos($dateWidth, 'LLL') !== false) { + $wholeWidth = 'medium'; + } else { + $wholeWidth = 'short'; + } + + return $wholeWidth; + } + + /** + * Rudimentary implementation of skeleton matching algorithm in #UTS 35, part 2, section 2.6.2.1. + * + * Limitations: + * - No matching of different but equivalent fields (e.g. H, k, h, K). + * - Distance calculation ignores difference between numeric and text fields. + * - No support for appendItems. + * + * @see https://www.unicode.org/reports/tr35/tr35-dates.html#Matching_Skeletons + * + * @param string $requestedSkeleton + * @param string[] $availableSkeletons + * + * @return array + */ + protected static function getBestMatchingSkeleton($requestedSkeleton, $availableSkeletons) + { + if (in_array($requestedSkeleton, $availableSkeletons)) { + return array($requestedSkeleton, array()); + } + + $requestedFields = array_values(array_unique(str_split($requestedSkeleton, 1))); + $requestedLength = strlen($requestedSkeleton); + + // UTS 35, part 2, section 2.6.2.1, step 4: If patterns match apart from second + // fraction field, adjust for this afterwards. + $sWidth = substr_count($requestedSkeleton, 'S'); + if ($sWidth) { + $requestedLengthWithoutMs = $requestedLength - $sWidth; + $requestedFieldsWithoutMs = array_values(array_diff($requestedFields, array('S'))); + } + + $candidateSkeletons = array(); + foreach ($availableSkeletons as $skeleton) { + $fields = array_values(array_unique(str_split($skeleton, 1))); + + if ($fields === $requestedFields) { + $candidateSkeletons[$skeleton] = abs(strlen($skeleton) - $requestedLength); + } elseif ($sWidth && $fields === $requestedFieldsWithoutMs) { + $candidateSkeletons[$skeleton] = abs(strlen($skeleton) - $requestedLengthWithoutMs); + } + } + + if (!$candidateSkeletons) { + return false; + } + + asort($candidateSkeletons); + $matchSkeleton = key($candidateSkeletons); + + $countAdjustments = array(); + foreach ($requestedFields as $field) { + $count = substr_count($requestedSkeleton, $field); + if ($count !== substr_count($matchSkeleton, $field)) { + $countAdjustments[$field] = $count; + } + } + + return array( + $matchSkeleton, + $countAdjustments, + ); + } + + /** + * Replace special input skeleton fields (j, J, C) with locale-specific substitutions. + * + * @see https://www.unicode.org/reports/tr35/tr35-dates.html#Date_Field_Symbol_Table + * + * @param string $skeleton + * @param string $locale + * + * @return array + */ + protected static function preprocessSkeleton($skeleton, $locale) + { + $replacements = array(); + $match = (string) strpbrk($skeleton, 'jJC'); + if ($match !== '') { + $field = $match[0]; + $timeData = Data::getGeneric('timeData'); + $time = Data::getTerritoryNode($timeData, $locale); + + if ($field === 'J') { + $skeleton = str_replace('J', 'H', $skeleton); + $replacements['h'] = $replacements['H'] = $time['preferred'][0]; + } else { + $index = strpos($skeleton, $field); + $count = strspn($skeleton, $field, $index); + $fieldA = 'a'; + if ($field === 'j') { + $fieldH = $time['preferred'][0]; + } else { // $field === 'C' + $fieldH = $time['allowed'][0][0]; + $match = (string) strpbrk($time['allowed'][0], 'bB'); + if ($match !== '') { + $fieldA = $match[0]; + } + } + // 'j' maps to 'h a', 'jj' to 'hh a', 'jjj' to 'h aaaa', 'jjjj' to 'h aaaa', etc. + $countH = 2 - $count % 2; + $countA = ($count <= 2 ? 1 : ($count <= 4 ? 4 : 5)); + $skeleton = substr($skeleton, 0, $index) . str_repeat($fieldH, $countH) . substr($skeleton, $index + $count); + $replacements['a'] = str_repeat($fieldA, $countA); + } + } + + return array($skeleton, $replacements); + } + + /** + * Replace special input skeleton fields, adjust field widths, and add second fraction to format pattern. + * + * @see https://www.unicode.org/reports/tr35/tr35-dates.html#Date_Field_Symbol_Table + * @see https://www.unicode.org/reports/tr35/tr35-dates.html#Matching_Skeletons + * + * @param string $format + * @param array $countAdjustments + * @param array $replacements + * @param string $locale + * + * @return string + */ + protected static function postprocessSkeletonFormat($format, $countAdjustments, $replacements, $locale) + { + static $countFields = array( + 'q' => 'Q', + 'L' => 'M', + 'e' => 'E', + 'c' => 'E', + 'b' => 'b', + 'b' => 'B', + 'K' => 'h', + 'k' => 'H', + ); + + $postprocessedFormat = ''; + $quoted = false; + $length = strlen($format); + for ($index = 0; $index < $length; $index++) { + $char = $format[$index]; + if ($char === "'") { + $quoted = !$quoted; + $postprocessedFormat .= $char; + } elseif (!$quoted) { + $count = 1; + for ($j = $index + 1; ($j < $length) && ($format[$j] === $char); $j++) { + $count++; + $index++; + } + + $countField = isset($countFields[$char]) ? $countFields[$char] : $char; + if (isset($countAdjustments[$countField])) { + $count = $countAdjustments[$countField]; + } + if (isset($replacements[$char])) { + $char = $replacements[$char]; + } + $postprocessedFormat .= str_repeat($char, $count); + + // Add second fraction if requested, and format does not contain it already. + if ($char === 's' && isset($countAdjustments['S']) && strpos($format, 'S') === false) { + $data = Data::get('numbers', $locale); + $decimal = $data['symbols']['decimal']; + $postprocessedFormat .= $decimal . str_repeat('S', $countAdjustments['S']); + } + } else { + $postprocessedFormat .= $char; + } + } + + return $postprocessedFormat; + } + + /** + * Return the most significant field where the two dates differ. For fractional seconds, + * 'S' is returned if the differ on the first decimal, 'SS' for the second decimal etc. + * If the dates are identical, the empty string is returned. + * + * @param \DateTime|\DateTimeInterface $value1 + * @param \DateTime|\DateTimeInterface $value2 + * + * @return string + */ + protected static function getGreatestDifference($value1, $value2) + { + if (!($value1 instanceof DateTimeInterface || $value1 instanceof DateTime)) { + throw new Exception\BadArgumentType($value1, '\\DateTime'); + } + if (!($value2 instanceof DateTimeInterface || $value2 instanceof DateTime)) { + throw new Exception\BadArgumentType($value2, '\\DateTime'); + } + + $length = strlen(self::$differenceFields); + for ($index = 0; $index < $length; $index++) { + $field = self::$differenceFields[$index]; + // We just need to check if this field is the same for the two dates, + // so any width and locale will do. + $function = self::$decoderFunctions[$field]; + if (static::$function($value1, 1, 'en') !== static::$function($value2, 1, 'en')) { + return $field; + } + } + + // Find the decimal where fractional seconds differ. + for ($count = 2; $count < 6; $count++) { + if (static::$function($value1, $count, 'en') !== static::$function($value2, $count, 'en')) { + return str_repeat('S', $count); + } + } + + // Dates are identical. + return ''; + } + + /** + * Adjust greatest difference to the fields used in the skeleton. + * + * The spec does not go into much detail on how to do this. This logic works + * with the skeletons currently provided in the data files but may need adjustment + * if e.g. skeletons including weeks (w) are added. + * + * @param string $greatestDifference + * @param string $skeleton + * + * @return string + */ + protected static function adjustGreatestDifference($greatestDifference, $skeleton) + { + if ($greatestDifference === '') { + return ''; + } + + // Adjust index for fractional second width (S). + $greatestDifferenceIndex = strpos(self::$differenceFields, $greatestDifference[0]) + strlen($greatestDifference) - 1; + if ($greatestDifferenceIndex === false) { + throw new Exception\ValueNotInList($greatestDifference, str_split(self::$differenceFields, 1)); + } + + // Strip fields that do not represent an interval. + $normalizedSkeleton = str_replace(array('z', 'Z', 'O', 'v', 'V', 'X', 'x', 'P'), '', $skeleton); + $skeletonGranularity = substr($normalizedSkeleton, -1); + if ($skeletonGranularity === 'h') { + $skeletonGranularity = 'H'; + } + $skeletonGranularityIndex = strpos(self::$differenceFields, $skeletonGranularity); + if ($skeletonGranularityIndex === false) { + throw new Exception\ValueNotInList($skeletonGranularity, str_split(self::$differenceFields, 1)); + } + + $adjustedGreatestDifference = $greatestDifference; + if ($adjustedGreatestDifference === 'a' && strpos($skeleton, 'H') !== false) { + // With a 24-hour clock we do not care about dayperiods. + $adjustedGreatestDifference = 'H'; + } elseif ($adjustedGreatestDifference === 'H' && strpos($skeleton, 'h') !== false) { + // 12-hour clock skeletons use h to indicate hour. + $adjustedGreatestDifference = 'h'; + } elseif ($adjustedGreatestDifference === 'Q' && strpos($skeleton, 'Q') === false) { + // Ignore quarter, if it is not part of the skeleton. + $adjustedGreatestDifference = 'M'; + } elseif ($adjustedGreatestDifference[0] === 'S') { + $skeletonGranularityIndex += substr_count($skeleton, 'S') - 1; + } + + if ($greatestDifferenceIndex > $skeletonGranularityIndex) { + // The dates are identical or only differ on less significant fields + // not included in the skeleton. + return ''; + } + + return $adjustedGreatestDifference; + } + + /** + * Splits an interval format into two datetime formats. + * + * @param string $format + * + * @return string[] An array containing two entries, each representing a datetime format + * + * @see https://www.unicode.org/reports/tr35/tr35-dates.html#intervalFormats + */ + protected static function splitIntervalFormat($format) + { + $functionNames = array(array()); + $index = 0; + + // Split on the first recurring field. + $tokens = self::tokenizeFormat($format); + foreach ($tokens as $token) { + if (is_array($token)) { + if (in_array($token[0], $functionNames)) { + $index = $token[2]; + + return array( + substr($format, 0, $index), + substr($format, $index), + ); + } + $functionNames[] = $token[0]; + } + } + + throw new \Punic\Exception('No recurring field found in format: ' . $format); + } + + /** + * @param \DateTime|\DateTimeInterface $value + * @param int $count + * @param string $locale + * @param bool $standAlone + * + * @return string + */ + protected static function decodeDayOfWeek($value, $count, $locale, $standAlone = false) + { + switch ($count) { + case 1: + case 2: + case 3: + return static::getWeekdayName($value, 'abbreviated', $locale, $standAlone); + case 4: + return static::getWeekdayName($value, 'wide', $locale, $standAlone); + case 5: + return static::getWeekdayName($value, 'narrow', $locale, $standAlone); + case 6: + return static::getWeekdayName($value, 'short', $locale, $standAlone); + default: + throw new Exception\ValueNotInList($count, array(1, 2, 3, 4, 5, 6)); + } + } + + /** + * @param \DateTime|\DateTimeInterface $value + * @param int $count + * @param string $locale + * @param bool $standAlone + * + * @return string + */ + protected static function decodeDayOfWeekLocal($value, $count, $locale, $standAlone = false) + { + switch ($count) { + case 1: + case 2: + $weekDay = (int) ($value->format('w')); + $firstWeekdayForCountry = static::getFirstWeekday($locale); + $localWeekday = 1 + ((7 + $weekDay - $firstWeekdayForCountry) % 7); + + return str_pad((string) $localWeekday, $count, '0', STR_PAD_LEFT); + default: + return static::decodeDayOfWeek($value, $count, $locale, $standAlone); + } + } + + /** + * @param \DateTime|\DateTimeInterface $value + * @param int $count + * @param string $locale + * + * @return string + */ + protected static function decodeDayOfWeekLocalAlone($value, $count, $locale) + { + return static::decodeDayOfWeekLocal($value, $count, $locale, true); + } + + /** + * @param \DateTime|\DateTimeInterface $value + * @param int $count + * @param string $locale + * + * @return string + */ + protected static function decodeDayOfMonth($value, $count, $locale) + { + switch ($count) { + case 1: + return $value->format('j'); + case 2: + return $value->format('d'); + default: + throw new Exception\ValueNotInList($count, array(1, 2)); + } + } + + /** + * @param \DateTime|\DateTimeInterface $value + * @param int $count + * @param string $locale + * @param bool $standAlone + * + * @return string + */ + protected static function decodeMonth($value, $count, $locale, $standAlone = false) + { + switch ($count) { + case 1: + return $value->format('n'); + case 2: + return $value->format('m'); + case 3: + return static::getMonthName($value, 'abbreviated', $locale, $standAlone); + case 4: + return static::getMonthName($value, 'wide', $locale, $standAlone); + case 5: + return static::getMonthName($value, 'narrow', $locale, $standAlone); + default: + throw new Exception\ValueNotInList($count, array(1, 2, 3, 4, 5)); + } + } + + /** + * @param \DateTime|\DateTimeInterface $value + * @param int $count + * @param string $locale + * + * @return string + */ + protected static function decodeMonthAlone($value, $count, $locale) + { + return static::decodeMonth($value, $count, $locale, true); + } + + /** + * @param \DateTime|\DateTimeInterface $value + * @param int $count + * @param string $locale + * + * @return string + */ + protected static function decodeYear($value, $count, $locale) + { + switch ($count) { + case 1: + return (string) ((int) ($value->format('Y'))); + case 2: + return $value->format('y'); + default: + $s = $value->format('Y'); + if (!isset($s[$count])) { + $s = str_pad($s, $count, '0', STR_PAD_LEFT); + } + + return $s; + } + } + + /** + * @param \DateTime|\DateTimeInterface $value + * @param int $count + * @param string $locale + * + * @return string + */ + protected static function decodeHour12($value, $count, $locale) + { + switch ($count) { + case 1: + return $value->format('g'); + case 2: + return $value->format('h'); + default: + throw new Exception\ValueNotInList($count, array(1, 2)); + } + } + + /** + * @param \DateTime|\DateTimeInterface $value + * @param int $count + * @param string $locale + * + * @return string + */ + protected static function decodeDayperiod($value, $count, $locale) + { + switch ($count) { + case 1: + case 2: + case 3: + return static::getDayperiodName($value, 'abbreviated', $locale); + case 4: + return static::getDayperiodName($value, 'wide', $locale); + case 5: + return static::getDayperiodName($value, 'narrow', $locale); + default: + throw new Exception\ValueNotInList($count, array(1)); + } + } + + /** + * @param \DateTime|\DateTimeInterface $value + * @param int $count + * @param string $locale + * + * @return string + */ + protected static function decodeVariableDayperiod($value, $count, $locale) + { + switch ($count) { + case 1: + case 2: + case 3: + return static::getVariableDayperiodName($value, 'abbreviated', $locale); + case 4: + return static::getVariableDayperiodName($value, 'abbreviated', $locale); + case 5: + return static::getVariableDayperiodName($value, 'abbreviated', $locale); + default: + throw new Exception\ValueNotInList($count, array(1)); + } + } + + /** + * @param \DateTime|\DateTimeInterface $value + * @param int $count + * @param string $locale + * + * @return string + */ + protected static function decodeHour24($value, $count, $locale) + { + switch ($count) { + case 1: + return $value->format('G'); + case 2: + return $value->format('H'); + default: + throw new Exception\ValueNotInList($count, array(1, 2)); + } + } + + /** + * @param \DateTime|\DateTimeInterface $value + * @param int $count + * @param string $locale + * + * @return string + */ + protected static function decodeHour12From0($value, $count, $locale) + { + switch ($count) { + case 1: + case 2: + return str_pad((string) ((int) ($value->format('G')) % 12), $count, '0', STR_PAD_LEFT); + default: + throw new Exception\ValueNotInList($count, array(1, 2)); + } + } + + /** + * @param \DateTime|\DateTimeInterface $value + * @param int $count + * @param string $locale + * + * @return string + */ + protected static function decodeHour24From1($value, $count, $locale) + { + switch ($count) { + case 1: + case 2: + return str_pad((string) (1 + (int) ($value->format('G'))), $count, '0', STR_PAD_LEFT); + default: + throw new Exception\ValueNotInList($count, array(1, 2)); + } + } + + /** + * @param \DateTime|\DateTimeInterface $value + * @param int $count + * @param string $locale + * + * @return string + */ + protected static function decodeMinute($value, $count, $locale) + { + switch ($count) { + case 1: + return (string) ((int) ($value->format('i'))); + case 2: + return $value->format('i'); + default: + throw new Exception\ValueNotInList($count, array(1, 2)); + } + } + + /** + * @param \DateTime|\DateTimeInterface $value + * @param int $count + * @param string $locale + * + * @return string + */ + protected static function decodeSecond($value, $count, $locale) + { + switch ($count) { + case 1: + return (string) ((int) ($value->format('s'))); + case 2: + return $value->format('s'); + default: + throw new Exception\ValueNotInList($count, array(1, 2)); + } + } + + /** + * @param \DateTime|\DateTimeInterface $value + * @param int $count + * @param string $locale + * + * @return string + */ + protected static function decodeTimezoneNoLocationSpecific($value, $count, $locale) + { + switch ($count) { + case 1: + case 2: + case 3: + $tz = static::getTimezoneNameNoLocationSpecific($value, 'short', '', $locale); + if ($tz === '') { + $tz = static::decodeTimezoneShortGMT($value, 1, $locale); + } + break; + case 4: + $tz = static::getTimezoneNameNoLocationSpecific($value, 'long', '', $locale); + if ($tz === '') { + $tz = static::decodeTimezoneShortGMT($value, 4, $locale); + } + break; + default: + throw new Exception\ValueNotInList($count, array(1, 2, 3, 4)); + } + + return $tz; + } + + /** + * @param \DateTime|\DateTimeInterface $value + * @param int $count + * @param string $locale + * + * @return string + */ + protected static function decodeTimezoneShortGMT($value, $count, $locale) + { + $offset = $value->getOffset(); + $sign = ($offset < 0) ? '-' : '+'; + $seconds = abs($offset); + $hours = (int) (floor($seconds / 3600)); + $seconds -= $hours * 3600; + $minutes = (int) (floor($seconds / 60)); + $data = Data::get('timeZoneNames', $locale); + $format = isset($data['gmtFormat']) ? $data['gmtFormat'] : 'GMT%1$s'; + switch ($count) { + case 1: + return sprintf($format, $sign . $hours . (($minutes === 0) ? '' : (':' . substr('0' . $minutes, -2)))); + case 4: + return sprintf($format, $sign . substr('0' . $hours, -2) . ':' . substr('0' . $minutes, -2)); + default: + throw new Exception\ValueNotInList($count, array(1, 4)); + } + } + + /** + * @param \DateTime|\DateTimeInterface $value + * @param int $count + * @param string $locale + * + * @return string + */ + protected static function decodeEra($value, $count, $locale) + { + switch ($count) { + case 1: + case 2: + case 3: + return static::getEraName($value, 'abbreviated', $locale); + case 4: + return static::getEraName($value, 'wide', $locale); + case 5: + return static::getEraName($value, 'narrow', $locale); + default: + throw new Exception\ValueNotInList($count, array(1, 2, 3, 4, 5)); + } + } + + /** + * @param \DateTime|\DateTimeInterface $value + * @param int $count + * @param string $locale + * + * @return string + */ + protected static function decodeYearWeekOfYear($value, $count, $locale) + { + $y = $value->format('o'); + if ($count === 2) { + $y = substr('0' . $y, -2); + } else { + if (!isset($y[$count])) { + $y = str_pad($y, $count, '0', STR_PAD_LEFT); + } + } + + return $y; + } + + /** + * Note: we assume Gregorian calendar here. + * + * @param \DateTime|\DateTimeInterface $value + * @param int $count + * @param string $locale + * + * @return string + */ + protected static function decodeYearExtended($value, $count, $locale) + { + return static::decodeYear($value, $count, $locale); + } + + /** + * Note: we assume Gregorian calendar here. + * + * @param \DateTime|\DateTimeInterface $value + * @param int $count + * @param string $locale + * + * @return string + */ + protected static function decodeYearRelatedGregorian($value, $count, $locale) + { + return static::decodeYearExtended($value, $count, $locale); + } + + /** + * @param DateTime|DateTimeInterface $value + * @param int $count + * @param string $locale + * @param bool $standAlone + * + * @return string + */ + protected static function decodeQuarter($value, $count, $locale, $standAlone = false) + { + $quarter = 1 + (int) (floor(((int) ($value->format('n')) - 1) / 3)); + switch ($count) { + case 1: + return (string) $quarter; + case 2: + return '0' . (string) $quarter; + case 3: + return static::getQuarterName($quarter, 'abbreviated', $locale, $standAlone); + case 4: + return static::getQuarterName($quarter, 'wide', $locale, $standAlone); + case 5: + return static::getQuarterName($quarter, 'narrow', $locale, $standAlone); + default: + throw new Exception\ValueNotInList($count, array(1, 2, 3, 4, 5)); + } + } + + /** + * @param \DateTime|\DateTimeInterface $value + * @param int $count + * @param string $locale + * + * @return string + */ + protected static function decodeQuarterAlone($value, $count, $locale) + { + return static::decodeQuarter($value, $count, $locale, true); + } + + /** + * @param \DateTime|\DateTimeInterface $value + * @param int $count + * @param string $locale + * + * @return string + */ + protected static function decodeWeekOfYear($value, $count, $locale) + { + switch ($count) { + case 1: + return (string) ((int) ($value->format('W'))); + case 2: + return $value->format('W'); + default: + throw new Exception\ValueNotInList($count, array(1, 2)); + } + } + + /** + * @param \DateTime|\DateTimeInterface $value + * @param int $count + * @param string $locale + * + * @return string + */ + protected static function decodeDayOfYear($value, $count, $locale) + { + switch ($count) { + case 1: + case 2: + case 3: + return str_pad((string) (1 + $value->format('z')), $count, '0', STR_PAD_LEFT); + default: + throw new Exception\ValueNotInList($count, array(1, 2, 3)); + } + } + + /** + * @param \DateTime|\DateTimeInterface $value + * @param int $count + * @param string $locale + * + * @return string + */ + protected static function decodeWeekdayInMonth($value, $count, $locale) + { + switch ($count) { + case 1: + case 2: + case 3: + $dom = (int) ($value->format('j')); + $wim = 1 + (int) (floor(($dom - 1) / 7)); + + return str_pad((string) $wim, $count, '0', STR_PAD_LEFT); + default: + throw new Exception\ValueNotInList($count, array(1, 2, 3)); + } + } + + /** + * @param \DateTime|\DateTimeInterface $value + * @param int $count + * @param string $locale + * + * @return string + */ + protected static function decodeFractionsOfSeconds($value, $count, $locale) + { + return substr(str_pad($value->format('u'), $count, '0', STR_PAD_RIGHT), 0, $count); + } + + /** + * @param \DateTime|\DateTimeInterface $value + * @param int $count + * @param string $locale + * + * @return string + */ + protected static function decodeMsecInDay($value, $count, $locale) + { + $hours = (int) ($value->format('G')); + $minutes = $hours * 60 + (int) ($value->format('i')); + $seconds = $minutes * 60 + (int) ($value->format('s')); + $milliseconds = $seconds * 1000 + (int) (floor((int) ($value->format('u')) / 1000)); + + return str_pad((string) $milliseconds, $count, '0', STR_PAD_LEFT); + } + + /** + * @param \DateTime|\DateTimeInterface $value + * @param int $count + * @param string $locale + * + * @return string + */ + protected static function decodeTimezoneDelta($value, $count, $locale) + { + $offset = $value->getOffset(); + $sign = ($offset < 0) ? '-' : '+'; + $seconds = abs($offset); + $hours = (int) (floor($seconds / 3600)); + $seconds -= $hours * 3600; + $minutes = (int) (floor($seconds / 60)); + $seconds -= $minutes * 60; + $partsWithoutSeconds = array(); + $partsWithoutSeconds[] = $sign . substr('0' . (string) $hours, -2); + $partsWithoutSeconds[] = substr('0' . (string) $minutes, -2); + $partsMaybeWithSeconds = $partsWithoutSeconds; + /* @TZWS + if ($seconds > 0) { + $partsMaybeWithSeconds[] = substr('0' . strval($seconds), -2); + } + */ + switch ($count) { + case 1: + case 2: + case 3: + return implode('', $partsMaybeWithSeconds); + case 4: + $data = Data::get('timeZoneNames', $locale); + $format = isset($data['gmtFormat']) ? $data['gmtFormat'] : 'GMT%1$s'; + + return sprintf($format, implode(':', $partsWithoutSeconds)); + case 5: + return implode(':', $partsMaybeWithSeconds); + default: + throw new Exception\ValueNotInList($count, array(1, 2, 3, 4, 5)); + } + } + + /** + * @param \DateTime|\DateTimeInterface $value + * @param int $count + * @param string $locale + * + * @return string + */ + protected static function decodeTimezoneNoLocationGeneric($value, $count, $locale) + { + switch ($count) { + case 1: + $tz = static::getTimezoneNameNoLocationSpecific($value, 'short', 'generic', $locale); + if ($tz === '') { + $tz = static::decodeTimezoneID($value, 4, $locale); + } + break; + case 4: + $tz = static::getTimezoneNameNoLocationSpecific($value, 'long', 'generic', $locale); + if ($tz === '') { + $tz = static::decodeTimezoneID($value, 4, $locale); + } + break; + default: + throw new Exception\ValueNotInList($count, array(1, 4)); + } + + return $tz; + } + + /** + * @param \DateTime|\DateTimeInterface $value + * @param int $count + * @param string $locale + * + * @return string + */ + protected static function decodeTimezoneID($value, $count, $locale) + { + switch ($count) { + case 1: + $result = 'unk'; + break; + case 2: + $result = static::getTimezoneNameFromDatetime($value); + break; + case 3: + $result = static::getTimezoneExemplarCity($value, true, $locale); + break; + case 4: + $result = static::getTimezoneNameLocationSpecific($value, $locale); + if ($result === '') { + $result = static::decodeTimezoneShortGMT($value, 4, $locale); + } + break; + default: + throw new Exception\ValueNotInList($count, array(1, 2, 3, 4)); + } + + return $result; + } + + /** + * @param \DateTime|\DateTimeInterface $value + * @param int $count + * @param string $locale + * @param bool $zForZero + * + * @return string + */ + protected static function decodeTimezoneWithTime($value, $count, $locale, $zForZero = false) + { + $offset = $value->getOffset(); + $useZ = ($zForZero && ($offset === 0)) ? true : false; + $sign = ($offset < 0) ? '-' : '+'; + $seconds = abs($offset); + $hours = (int) (floor($seconds / 3600)); + $seconds -= $hours * 3600; + $minutes = (int) (floor($seconds / 60)); + $seconds -= $minutes * 60; + $hours2 = $sign . substr('0' . (string) $hours, -2); + $minutes2 = substr('0' . (string) $minutes, -2); + /* + * TZWS + * $seconds2 = substr('0' . strval($seconds), -2); + */ + $hmMaybe = array($hours2); + if ($minutes > 0) { + $hmMaybe[] = $minutes2; + } + $hmsMaybe = array($hours2, $minutes2); + /* @TZWS + if ($seconds > 0) { + $hmsMaybe[] = $seconds2; + } + */ + switch ($count) { + case 1: + $result = $useZ ? 'Z' : implode('', $hmMaybe); + break; + case 2: + $result = $useZ ? 'Z' : "{$hours2}{$minutes2}"; + break; + case 3: + $result = $useZ ? 'Z' : "{$hours2}:{$minutes2}"; + break; + case 4: + $result = $useZ ? 'Z' : implode('', $hmsMaybe); + break; + case 5: + $result = $useZ ? 'Z' : implode(':', $hmsMaybe); + break; + default: + throw new Exception\ValueNotInList($count, array(1, 2, 3, 4, 5)); + } + + return $result; + } + + /** + * @param \DateTime|\DateTimeInterface $value + * @param int $count + * @param string $locale + * + * @return string + */ + protected static function decodeTimezoneWithTimeZ($value, $count, $locale) + { + return static::decodeTimezoneWithTime($value, $count, $locale, true); + } + + /** + * @todo + * + * @param \DateTime|\DateTimeInterface $value + * @param int $count + * @param string $locale + * + * @return string + */ + protected static function decodeWeekOfMonth($value, $count, $locale) + { + throw new Exception\NotImplemented(__METHOD__); + } + + /** + * @todo + */ + protected static function decodeYearCyclicName() + { + throw new Exception\NotImplemented(__METHOD__); + } + + /** + * @todo + */ + protected static function decodeModifiedGiulianDay() + { + throw new Exception\NotImplemented(__METHOD__); + } + + /** + * @param string $timezoneID + * + * @return string + */ + protected static function getTimezoneCanonicalID($timezoneID) + { + $timeZones = Data::getGeneric('timeZones'); + if (isset($timeZones['aliases'][$timezoneID])) { + $timezoneID = $timeZones['aliases'][$timezoneID]; + } + + return $timezoneID; + } + + /** + * @param \DateTime|\DateTimeInterface $value + * @param int $count + * @param string $locale + * + * @return string + */ + protected static function decodePunicExtension($value, $count, $locale) + { + switch ($count) { + case 1: + return $value->format('N'); + case 2: + return $value->format('w'); + case 3: + return (stripos($locale, 'en') === 0) ? $value->format('S') : ''; + case 4: + return $value->format('z'); + case 5: + return $value->format('t'); + case 6: + return $value->format('L'); + case 7: + $result = self::decodeDayperiod($value, 1, $locale); + if (stripos($locale, 'en') === 0) { + $result = strtolower($result); + } + + return $result; + case 8: + return $value->format('B'); + case 9: + return $value->format('u'); + case 10: + return $value->format('I'); + case 11: + return $value->format('Z'); + case 12: + return $value->format('r'); + case 13: + return $value->format('U'); + default: + throw new Exception\ValueNotInList($count, array(1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13)); + } + } + + /** + * @param \DateTime|\DateTimeInterface $dt + * + * @return string + */ + protected static function getTimezoneNameFromDatetime($dt) + { + if (defined('\HHVM_VERSION')) { + $result = $dt->format('e'); + if (!preg_match('/[0-9][0-9]/', $result)) { + $result = $dt->getTimezone()->getName(); + } + } else { + $result = $dt->getTimezone()->getName(); + } + + return $result; + } + + /** + * @return string + */ + protected static function getTimezoneNameFromTimezone(DateTimeZone $tz) + { + if (defined('\HHVM_VERSION')) { + $testDT = new DateTime('now', $tz); + $result = $testDT->format('e'); + if (!preg_match('/[0-9][0-9]/', $result)) { + $result = $tz->getName(); + } + } else { + $result = $tz->getName(); + } + + return $result; + } + + /** + * @param \DateTime|\DateTimeInterface $dt + * + * @return array|false + */ + protected static function getTimezoneLocationFromDatetime($dt) + { + if (defined('\HHVM_VERSION')) { + if (!preg_match('/[0-9][0-9]/', $dt->format('e'))) { + $result = $dt->getTimezone()->getLocation(); + } else { + $result = false; + } + } else { + $result = $dt->getTimezone()->getLocation(); + } + + return $result; + } + + /** + * Tokenize an ISO date/time format string. + * + * @param string $format + * + * @return array + */ + protected static function tokenizeFormat($format) + { + if (isset(self::$tokenizerCache[$format])) { + $result = self::$tokenizerCache[$format]; + } else { + $result = array(); + $length = strlen($format); + $lengthM1 = $length - 1; + $quoted = false; + for ($index = 0; $index < $length; $index++) { + $char = $format[$index]; + if ($char === "'") { + if (($index < $lengthM1) && ($format[$index + 1] === "'")) { + $result[] = "'"; + $index++; + } else { + $quoted = !$quoted; + } + } elseif ($quoted) { + $result[] = $char; + } else { + $count = 1; + for ($j = $index + 1; ($j < $length) && ($format[$j] === $char); $j++) { + $count++; + } + if (isset(self::$decoderFunctions[$char])) { + $result[] = array(self::$decoderFunctions[$char], $count, $index); + } else { + $result[] = str_repeat($char, $count); + } + $index += $count - 1; + } + } + self::$tokenizerCache[$format] = $result; + } + + return $result; + } +} diff --git a/3rdparty/punic/punic/src/Comparer.php b/3rdparty/punic/punic/src/Comparer.php new file mode 100644 index 00000000..c2ad2fc4 --- /dev/null +++ b/3rdparty/punic/punic/src/Comparer.php @@ -0,0 +1,171 @@ +cache = array(); + $this->locale = (string) $locale !== '' ? $locale : \Punic\Data::getDefaultLocale(); + $this->caseSensitive = (bool) $caseSensitive; + if (class_exists('\Collator')) { + try { + $this->collator = new Collator($this->locale); + } catch (PHPException $x) { + } + } + $this->iconv = function_exists('iconv'); + } + + /** + * Compare two strings. + * + * @param string $a + * @param string $b + * + * @return int + */ + public function compare($a, $b) + { + $result = null; + if (isset($this->collator)) { + try { + $a = (string) $a; + $b = (string) $b; + if ($this->caseSensitive) { + $result = $this->collator->compare($a, $b); + } else { + $array = array($a, $b); + if ($this->sort($array) === false) { + $result = false; + } else { + $ia = array_search($a, $array); + if ($ia === 1) { + $result = 1; + } else { + $ib = array_search($b, $array); + if ($ib === 1) { + $result = -1; + } else { + $result = 0; + } + } + } + } + } catch (PHPException $x) { + } + } + if ($result === null) { + $a = $this->normalize($a); + $b = $this->normalize($b); + + $result = $this->caseSensitive ? strnatcmp($a, $b) : strnatcasecmp($a, $b); + } + + return $result; + } + + /** + * @param array $array + * @param bool $keepKeys + * + * @return bool + */ + public function sort(&$array, $keepKeys = false) + { + $me = $this; + $result = null; + if (isset($this->collator)) { + try { + if ($keepKeys) { + $result = $this->collator->asort($array, Collator::SORT_STRING); + } else { + $result = $this->collator->sort($array, Collator::SORT_STRING); + } + } catch (PHPException $x) { + } + } + if ($result === null) { + if ($keepKeys) { + $result = uasort( + $array, + function ($a, $b) use ($me) { + return $me->compare($a, $b); + } + ); + } else { + $result = usort( + $array, + function ($a, $b) use ($me) { + return $me->compare($a, $b); + } + ); + } + } + + return $result; + } + + /** + * @param string $str + * + * @return string + */ + private function normalize($str) + { + $str = (string) $str; + if (!isset($this->cache[$str])) { + $this->cache[$str] = $str; + if ($str !== '') { + if ($this->iconv) { + $previousErrorHandler = set_error_handler(function () {}, E_NOTICE); + $transliterated = @iconv('UTF-8', 'ASCII//TRANSLIT', $str); + set_error_handler($previousErrorHandler); + if ($transliterated !== false) { + $this->cache[$str] = $transliterated; + } + } + } + } + + return $this->cache[$str]; + } +} diff --git a/3rdparty/punic/punic/src/Currency.php b/3rdparty/punic/punic/src/Currency.php new file mode 100644 index 00000000..882c892c --- /dev/null +++ b/3rdparty/punic/punic/src/Currency.php @@ -0,0 +1,252 @@ + $info) { + $result[$code] = $info['name']; + } + if ((!$alsoUnused) || (!$alsoNotTender)) { + $data = Data::getGeneric('currencyData'); + $usedCurrencies = array(); + $tenderCurrencies = array(); + foreach ($data['regions'] as $usages) { + foreach ($usages as $usage) { + if (!isset($usage['to'])) { + $usedCurrencies[$usage['currency']] = true; + } + if ((!isset($usage['notTender'])) || (!$usage['notTender'])) { + $tenderCurrencies[$usage['currency']] = true; + } + } + } + if (!$alsoUnused) { + $result = array_intersect_key($result, $usedCurrencies); + } + if (!$alsoNotTender) { + $result = array_intersect_key($result, $tenderCurrencies); + } + } + + return $result; + } + + /** + * Returns the name of a currency given its code. + * + * @param string $currencyCode The currency code + * @param number|string|null $quantity The quantity identifier. Allowed values: + *
    + *
  • `null` to return the standard name, not associated to any quantity
  • + *
  • `number` to return the name following the plural rule for the specified quantity
  • + *
  • string `'zero'|'one'|'two'|'few'|'many'|'other'` the plural rule + *
+ * @param string $locale The locale to use. If empty we'll use the default locale set with {@link \Punic\Data::setDefaultLocale()}. + * + * @return string Returns an empty string if $currencyCode is not valid, the localized currency name otherwise + */ + public static function getName($currencyCode, $quantity = null, $locale = '') + { + $result = ''; + $data = static::getLocaleData($currencyCode, $locale); + if (is_array($data)) { + $result = $data['name']; + if (($quantity !== null) && isset($data['pluralName'])) { + if (in_array($quantity, array('zero', 'one', 'two', 'few', 'many', 'other'))) { + $pluralRule = $quantity; + } else { + $pluralRule = Plural::getRuleOfType($quantity, Plural::RULETYPE_CARDINAL, $locale); + } + if (!isset($data['pluralName'][$pluralRule])) { + $pluralRule = 'other'; + } + $result = $data['pluralName'][$pluralRule]; + } + } + + return $result; + } + + /** + * Returns the name of a currency given its code. + * + * @param string $currencyCode The currency code + * @param string $which Which symbol flavor do you prefer? 'narrow' for narrow symbols, 'alt' for alternative. Other values: standard/default symbol + * @param string $locale The locale to use. If empty we'll use the default locale set with {@link \Punic\Data::setDefaultLocale()}. + * + * @return string Returns an empty string if $currencyCode is not valid, the localized currency name otherwise + */ + public static function getSymbol($currencyCode, $which = '', $locale = '') + { + $result = ''; + $data = static::getLocaleData($currencyCode, $locale); + if (is_array($data)) { + switch ($which) { + case 'narrow': + if (isset($data['symbolNarrow'])) { + $result = $data['symbolNarrow']; + } + + break; + case 'alt': + if (isset($data['symbolAlt'])) { + $result = $data['symbolAlt']; + } + break; + } + if ($result === '' && $which !== 'alt') { + if (isset($data['symbol'])) { + $result = $data['symbol']; + } + if ($result === '') { + $result = $currencyCode; + } + } + } + + return $result; + } + + /** + * Returns the ISO 4217 code for a currency given its currency code. + * + * Historical currencies are not supported. + * + * @param string $currencyCode The 3-letter currency code + * + * @return string Returns the numeric ISO 427 code, or an empty string if $currencyCode is not valid + * + * @see http://unicode.org/reports/tr35/tr35-info.html#Supplemental_Code_Mapping + */ + public static function getNumericCode($currencyCode) + { + $codeMappings = Data::getGeneric('codeMappings'); + $currencies = $codeMappings['currencies']; + + if (isset($currencies[$currencyCode]['numeric'])) { + return $currencies[$currencyCode]['numeric']; + } + + return ''; + } + + /** + * Returns the currency code given its ISO 4217 code. + * + * Historical currencies are not supported. + * + * @param string $code The numeric ISO 427 code + * + * @return string Returns the 3-letter currency code, or an empty string if $code is not valid + * + * @see http://unicode.org/reports/tr35/tr35-info.html#Supplemental_Code_Mapping + */ + public static function getByNumericCode($code) + { + $codeMappings = Data::getGeneric('codeMappings'); + $currencies = $codeMappings['currencies']; + + foreach ($currencies as $currencyCode => $currency) { + if (isset($currency['numeric']) && $currency['numeric'] == $code) { + return $currencyCode; + } + } + + return ''; + } + + /** + * Return the history for the currencies used in a territory. + * + * @param string $territoryCode The territory code + * + * @return array Return a list of items with these keys: + *
    + *
  • string `currency`: the currency code (always present)
  • + *
  • string `from`: start date of the currency validity in the territory (not present if no start date) - Format is YYYY-MM-DD
  • + *
  • string `to`: end date of the currency validity in the territory (not present if no end date) - Format is YYYY-MM-DD
  • + *
  • bool `tender`: true if the currency was or is legal tender, false otherwise (always present)
  • + *
+ */ + public static function getCurrencyHistoryForTerritory($territoryCode) + { + $result = array(); + if (preg_match('/^[A-Z]{2}|[0-9]{3}$/', $territoryCode)) { + $data = Data::getGeneric('currencyData'); + if (isset($data['regions'][$territoryCode])) { + foreach ($data['regions'][$territoryCode] as $c) { + if (isset($c['notTender'])) { + $c['tender'] = !$c['notTender']; + unset($c['notTender']); + } else { + $c['tender'] = true; + } + $result[] = $c; + } + } + } + + return $result; + } + + /** + * Return the currency to be used in a territory. + * + * @param string $territoryCode The territory code + * + * @return string Returns an empty string if $territoryCode is not valid or we don't have info about it, the currency code otherwise + */ + public static function getCurrencyForTerritory($territoryCode) + { + $result = ''; + $history = static::getCurrencyHistoryForTerritory($territoryCode); + if (!empty($history)) { + $today = @date('Y-m-d'); + foreach ($history as $c) { + if ((!isset($c['to'])) || (strcmp($c['to'], $today) >= 0)) { + $result = $c['currency']; + if ($c['tender']) { + break; + } + } + } + } + + return $result; + } + + /** + * @param string $currencyCode + * @param string $locale + * + * @return array|null + */ + protected static function getLocaleData($currencyCode, $locale) + { + $result = null; + if (is_string($currencyCode) && (strlen($currencyCode) === 3)) { + $data = Data::get('currencies', $locale); + if (isset($data[$currencyCode])) { + $result = $data[$currencyCode]; + } + } + + return $result; + } +} diff --git a/3rdparty/punic/punic/src/Data.php b/3rdparty/punic/punic/src/Data.php new file mode 100644 index 00000000..dd142526 --- /dev/null +++ b/3rdparty/punic/punic/src/Data.php @@ -0,0 +1,756 @@ + $language, + 'script' => $script, + 'territory' => $territory, + 'parentLocale' => $parentLocale, + ); + } + } + } + } + + return $result; + } + + /** + * Get value from nested array. + * + * @param array $data the nested array to descend into + * @param array $path Path of array keys. Each part of the path may be a string or an array of alternative strings. + * + * @return mixed|null + */ + public static function getArrayValue(array $data, array $path) + { + $alternatives = array_shift($path); + if ($alternatives === null) { + return $data; + } + foreach ((array) $alternatives as $alternative) { + if (array_key_exists($alternative, $data)) { + $data = $data[$alternative]; + + return is_array($data) ? self::getArrayValue($data, $path) : ($path ? null : $data); + } + } + + return null; + } + + /** + * @deprecated + * + * @param string $territory + * + * @return string + */ + protected static function getParentTerritory($territory) + { + return Territory::getParentTerritoryCode($territory); + } + + /** + * @deprecated + * + * @param string $parentTerritory + * + * @return array + */ + protected static function expandTerritoryGroup($parentTerritory) + { + return Territory::getChildTerritoryCodes($parentTerritory, true); + } + + /** + * Returns the path of the locale-specific data, looking also for the fallback locale. + * + * @param string $locale The locale for which you want the data folder + * + * @return string Returns an empty string if the folder is not found, the absolute path to the folder otherwise + */ + protected static function getLocaleFolder($locale) + { + static $cache = array(); + $result = ''; + if (is_string($locale)) { + $key = $locale . '/' . static::$fallbackLocale; + if (!isset($cache[$key])) { + $dir = static::getDataDirectory(); + foreach (static::getLocaleAlternatives($locale) as $alternative) { + if (is_dir($dir . DIRECTORY_SEPARATOR . $alternative)) { + $result = $alternative; + break; + } + } + $cache[$key] = $result; + } + $result = $cache[$key]; + } + + return $result; + } + + /** + * Returns a list of locale identifiers associated to a locale. + * + * @param string $locale The locale for which you want the alternatives + * @param bool $addFallback Set to true to add the fallback locale to the result, false otherwise + * + * @return array + */ + protected static function getLocaleAlternatives($locale, $addFallback = true) + { + $result = array(); + $localeInfo = static::explodeLocale($locale); + if (!is_array($localeInfo)) { + throw new Exception\InvalidLocale($locale); + } + $language = $localeInfo['language']; + $script = $localeInfo['script']; + $territory = $localeInfo['territory']; + $parentLocale = $localeInfo['parentLocale']; + if ($territory === '') { + $fullLocale = static::guessFullLocale($language, $script); + if ($fullLocale !== '') { + $localeInfo = static::explodeLocale($fullLocale); + $language = $localeInfo['language']; + $script = $localeInfo['script']; + $territory = $localeInfo['territory']; + $parentLocale = $localeInfo['parentLocale']; + } + } + $territories = array(); + while ($territory !== '') { + $territories[] = $territory; + $territory = Territory::getParentTerritoryCode($territory); + } + if ($script !== '') { + foreach ($territories as $territory) { + $result[] = "{$language}-{$script}-{$territory}"; + } + } + if ($script !== '') { + $result[] = "{$language}-{$script}"; + } + foreach ($territories as $territory) { + $result[] = "{$language}-{$territory}"; + if ("{$language}-{$territory}" === 'en-US') { + $result[] = 'root'; + } + } + if ($parentLocale !== '') { + $result = array_merge($result, static::getLocaleAlternatives($parentLocale, false)); + } + $result[] = $language; + if ($addFallback && ($locale !== static::$fallbackLocale)) { + $result = array_merge($result, static::getLocaleAlternatives(static::$fallbackLocale, false)); + } + for ($i = count($result) - 1; $i > 1; $i--) { + for ($j = 0; $j < $i; $j++) { + if ($result[$i] === $result[$j]) { + array_splice($result, $i, 1); + break; + } + } + } + if ($locale !== 'root') { + $i = array_search('root', $result, true); + if ($i !== false) { + array_splice($result, $i, 1); + $result[] = 'root'; + } + } + + return $result; + } + + protected static function merge(array $data, array $overrides) + { + foreach ($overrides as $key => $override) { + if (isset($data[$key])) { + if (gettype($override) !== gettype($data[$key])) { + throw new Exception\InvalidOverride($data[$key], $override); + } + if (is_array($data[$key])) { + $data[$key] = static::merge($data[$key], $override); + } else { + $data[$key] = $override; + } + } else { + $data[$key] = $override; + } + } + + return $data; + } +} diff --git a/3rdparty/punic/punic/src/Exception.php b/3rdparty/punic/punic/src/Exception.php new file mode 100644 index 00000000..37280730 --- /dev/null +++ b/3rdparty/punic/punic/src/Exception.php @@ -0,0 +1,91 @@ +argumentValue = $argumentValue; + $this->destinationTypeDescription = $destinationTypeDescription; + $type = gettype($argumentValue); + switch ($type) { + case 'boolean': + $shownName = $argumentValue ? 'TRUE' : 'FALSE'; + break; + case 'integer': + case 'double': + $shownName = (string) $argumentValue; + break; + case 'string': + $shownName = "'{$argumentValue}'"; + break; + case 'object': + $shownName = get_class($argumentValue); + break; + default: + $shownName = $type; + break; + } + $message = "Can't convert {$shownName} to a {$destinationTypeDescription}"; + parent::__construct($message, \Punic\Exception::BAD_ARGUMENT_TYPE, $previous); + } + + /** + * Retrieves the value of the invalid argument. + */ + public function getArgumentValue() + { + return $this->argumentValue; + } + + /** + * Retrieves the destination type (or a list of destination types). + * + * @return string|array + */ + public function getDestinationTypeDescription() + { + return $this->destinationTypeDescription; + } +} diff --git a/3rdparty/punic/punic/src/Exception/BadDataFileContents.php b/3rdparty/punic/punic/src/Exception/BadDataFileContents.php new file mode 100644 index 00000000..5eb2a5ab --- /dev/null +++ b/3rdparty/punic/punic/src/Exception/BadDataFileContents.php @@ -0,0 +1,48 @@ +dataFilePath = $dataFilePath; + $this->dataFileContents = $dataFileContents; + $message = "The file '{$dataFilePath}' contains malformed data"; + parent::__construct($message, \Punic\Exception::BAD_DATA_FILE_CONTENTS, $previous); + } + + /** + * Retrieves the path to the data file. + * + * @return string + */ + public function getDataFilePath() + { + return $this->dataFilePath; + } + + /** + * Retrieves the malformed contents of the file. + * + * @return string + */ + public function getDataFileContents() + { + return $this->dataFileContents; + } +} diff --git a/3rdparty/punic/punic/src/Exception/DataFileNotFound.php b/3rdparty/punic/punic/src/Exception/DataFileNotFound.php new file mode 100644 index 00000000..5e1f8082 --- /dev/null +++ b/3rdparty/punic/punic/src/Exception/DataFileNotFound.php @@ -0,0 +1,72 @@ +identifier = $identifier; + if (empty($locale) && empty($fallbackLocale)) { + $this->locale = ''; + $this->fallbackLocale = ''; + $message = "Unable to find the data file '{$identifier}'"; + } else { + $this->locale = $locale; + $this->fallbackLocale = $fallbackLocale; + if (@strcasecmp($locale, $fallbackLocale) === 0) { + $message = "Unable to find the data file '{$identifier}' for '{$locale}'"; + } else { + $message = "Unable to find the data file '{$identifier}', neither for '{$locale}' nor for '{$fallbackLocale}'"; + } + } + parent::__construct($message, \Punic\Exception::DATA_FILE_NOT_FOUND, $previous); + } + + /** + * Retrieves the bad data file identifier. + * + * @return string + */ + public function getIdentifier() + { + return $this->identifier; + } + + /** + * Retrieves the preferred locale. + * + * @return string + */ + public function getLocale() + { + return $this->locale; + } + + /** + * Retrieves the fallback locale. + * + * @return string + */ + public function getFallbackLocale() + { + return $this->fallbackLocale; + } +} diff --git a/3rdparty/punic/punic/src/Exception/DataFileNotReadable.php b/3rdparty/punic/punic/src/Exception/DataFileNotReadable.php new file mode 100644 index 00000000..2b42d786 --- /dev/null +++ b/3rdparty/punic/punic/src/Exception/DataFileNotReadable.php @@ -0,0 +1,34 @@ +dataFilePath = $dataFilePath; + $message = "Unable to read from the data file '{$dataFilePath}'"; + parent::__construct($message, \Punic\Exception::DATA_FILE_NOT_READABLE, $previous); + } + + /** + * Retrieves the path to the unreadable file. + * + * @return string + */ + public function getDataFilePath() + { + return $this->dataFilePath; + } +} diff --git a/3rdparty/punic/punic/src/Exception/DataFolderNotFound.php b/3rdparty/punic/punic/src/Exception/DataFolderNotFound.php new file mode 100644 index 00000000..b2df867a --- /dev/null +++ b/3rdparty/punic/punic/src/Exception/DataFolderNotFound.php @@ -0,0 +1,52 @@ +locale = $locale; + $this->fallbackLocale = $fallbackLocale; + if (@strcasecmp($locale, $fallbackLocale) === 0) { + $message = "Unable to find the specified locale folder for '{$locale}'"; + } else { + $message = "Unable to find the specified locale folder, neither for '{$locale}' nor for '{$fallbackLocale}'"; + } + parent::__construct($message, \Punic\Exception::DATA_FOLDER_NOT_FOUND, $previous); + } + + /** + * Retrieves the preferred locale. + * + * @return string + */ + public function getLocale() + { + return $this->locale; + } + + /** + * Retrieves the fallback locale. + * + * @return string + */ + public function getFallbackLocale() + { + return $this->fallbackLocale; + } +} diff --git a/3rdparty/punic/punic/src/Exception/InvalidDataFile.php b/3rdparty/punic/punic/src/Exception/InvalidDataFile.php new file mode 100644 index 00000000..a523f37b --- /dev/null +++ b/3rdparty/punic/punic/src/Exception/InvalidDataFile.php @@ -0,0 +1,37 @@ +identifier = $identifier; + $type = gettype($identifier); + if ($type === 'string') { + $message = "'{$identifier}' is not a valid data file identifier"; + } else { + $message = "A valid identifier should be a string, {$type} received"; + } + parent::__construct($message, \Punic\Exception::INVALID_DATAFILE, $previous); + } + + /** + * Retrieves the bad data file identifier. + */ + public function getIdentifier() + { + return $this->identifier; + } +} diff --git a/3rdparty/punic/punic/src/Exception/InvalidLocale.php b/3rdparty/punic/punic/src/Exception/InvalidLocale.php new file mode 100644 index 00000000..994853cf --- /dev/null +++ b/3rdparty/punic/punic/src/Exception/InvalidLocale.php @@ -0,0 +1,37 @@ +locale = $locale; + $type = gettype($locale); + if ($type === 'string') { + $message = "'{$locale}' is not a valid locale identifier"; + } else { + $message = "A valid locale should be a string, {$type} received"; + } + parent::__construct($message, \Punic\Exception::INVALID_LOCALE, $previous); + } + + /** + * Retrieves the bad locale. + */ + public function getLocale() + { + return $this->locale; + } +} diff --git a/3rdparty/punic/punic/src/Exception/InvalidOverride.php b/3rdparty/punic/punic/src/Exception/InvalidOverride.php new file mode 100644 index 00000000..98eacf62 --- /dev/null +++ b/3rdparty/punic/punic/src/Exception/InvalidOverride.php @@ -0,0 +1,36 @@ +dataToString($data) . ' with ' . $this->dataToString($override); + parent::__construct($message, \Punic\Exception::INVALID_OVERRIDE, $previous); + } + + /** + * Convert override data to a string. + * + * @return string + */ + protected function dataToString($data) + { + if (is_array($data)) { + return 'array with keys ' . implode(', ', array_keys($data)); + } + + return gettype($data) . ' value ' . $data; + } +} diff --git a/3rdparty/punic/punic/src/Exception/NotImplemented.php b/3rdparty/punic/punic/src/Exception/NotImplemented.php new file mode 100644 index 00000000..ff216991 --- /dev/null +++ b/3rdparty/punic/punic/src/Exception/NotImplemented.php @@ -0,0 +1,34 @@ +function = $function; + $message = "{$function} is not implemented"; + parent::__construct($message, \Punic\Exception::NOT_IMPLEMENTED, $previous); + } + + /** + * Retrieves the name of the not implemented function/method. + * + * @return string + */ + public function getFunction() + { + return $this->function; + } +} diff --git a/3rdparty/punic/punic/src/Exception/ValueNotInList.php b/3rdparty/punic/punic/src/Exception/ValueNotInList.php new file mode 100644 index 00000000..27ed5442 --- /dev/null +++ b/3rdparty/punic/punic/src/Exception/ValueNotInList.php @@ -0,0 +1,46 @@ + $allowedValues The list of valid values + * @param \Exception|null $previous The previous exception used for the exception chaining + */ + public function __construct($value, $allowedValues, $previous = null) + { + $this->value = $value; + $this->allowedValues = $allowedValues; + $message = "'{$value}' is not valid. Acceptable values are: '" . implode("', '", $allowedValues) . "'"; + parent::__construct($message, \Punic\Exception::VALUE_NOT_IN_LIST, $previous); + } + + /** + * Retrieves the invalid value. + */ + public function getValue() + { + return $this->value; + } + + /** + * Retrieves the list of valid values. + * + * @return array + */ + public function getAllowedValues() + { + return $this->allowedValues; + } +} diff --git a/3rdparty/punic/punic/src/Language.php b/3rdparty/punic/punic/src/Language.php new file mode 100644 index 00000000..19fb5d95 --- /dev/null +++ b/3rdparty/punic/punic/src/Language.php @@ -0,0 +1,94 @@ +`'titlecase-words'` all words in the phrase should be title case + *
  • `'titlecase-firstword'` the first word should be title case
  • + *
  • `'lowercase-words'` all words in the phrase should be lower case
  • + * + * + * @return string + * + * @see http://cldr.unicode.org/development/development-process/design-proposals/consistent-casing + */ + public static function fixCase($string, $case) + { + $result = $string; + if (is_string($string) && is_string($case) && $string !== '') { + switch ($case) { + case 'titlecase-words': + if (function_exists('mb_strtoupper') && (@preg_match('/\pL/u', 'a'))) { + $result = preg_replace_callback('/\\pL+/u', function ($m) { + $s = $m[0]; + $l = mb_strlen($s, 'UTF-8'); + if ($l === 1) { + $s = mb_strtoupper($s, 'UTF-8'); + } else { + $s = mb_strtoupper(mb_substr($s, 0, 1, 'UTF-8'), 'UTF-8') . mb_substr($s, 1, $l - 1, 'UTF-8'); + } + + return $s; + }, $string); + } + break; + case 'titlecase-firstword': + if (function_exists('mb_strlen')) { + $l = mb_strlen($string, 'UTF-8'); + if ($l === 1) { + $result = mb_strtoupper($string, 'UTF-8'); + } elseif ($l > 1) { + $result = mb_strtoupper(mb_substr($string, 0, 1, 'UTF-8'), 'UTF-8') . mb_substr($string, 1, $l - 1, 'UTF-8'); + } + } + break; + case 'lowercase-words': + if (function_exists('mb_strtolower')) { + $result = mb_strtolower($string, 'UTF-8'); + } + break; + } + } + + return $result; + } + + /** + * Parse the browser HTTP_ACCEPT_LANGUAGE header and return the found locales, sorted in descending order by the quality values. + * + * @param bool $ignoreCache Set to true if you want to ignore the cache + * + * @return array Array keys are the found locales, array values are the relative quality value (from 0 to 1) + */ + public static function getBrowserLocales($ignoreCache = false) + { + static $result; + if (!isset($result) || $ignoreCache) { + $httpAcceptLanguages = @getenv('HTTP_ACCEPT_LANGUAGE'); + if ((!is_string($httpAcceptLanguages)) || (strlen($httpAcceptLanguages) === 0)) { + if (isset($_SERVER) && is_array($_SERVER) && isset($_SERVER['HTTP_ACCEPT_LANGUAGE'])) { + $httpAcceptLanguages = $_SERVER['HTTP_ACCEPT_LANGUAGE']; + } + } + $result = self::parseHttpAcceptLanguage($httpAcceptLanguages); + } + + return $result; + } + + /** + * Parse the value of an HTTP_ACCEPT_LANGUAGE header and return the found locales, sorted in descending order by the quality values. + * + * @param string $httpAcceptLanguages + * + * @return array Array keys are the found locales, array values are the relative quality value (from 0 to 1) + */ + public static function parseHttpAcceptLanguage($httpAcceptLanguages) + { + $result = array(); + if (is_string($httpAcceptLanguages) && $httpAcceptLanguages !== '') { + $m = null; + foreach (explode(',', $httpAcceptLanguages) as $httpAcceptLanguage) { + if (preg_match('/^([a-z]{2,3}(?:[_\\-][a-z]+)*)(?:\\s*;(?:\\s*q(?:\\s*=(?:\\s*([\\d.]+))?)?)?)?$/', strtolower(trim($httpAcceptLanguage, " \t")), $m)) { + if (count($m) > 2) { + if (strpos($m[2], '.') === 0) { + $m[2] = '0' . $m[2]; + } + if (preg_match('/^[01](\\.\\d*)?$/', $m[2])) { + $quality = round(@(float) $m[2], 4); + } else { + $quality = -1; + } + } else { + $quality = 1; + } + if (($quality >= 0) && ($quality <= 1)) { + $found = array(); + $chunks = explode('-', str_replace('_', '-', $m[1])); + $numChunks = count($chunks); + if ($numChunks === 1) { + $found[] = $m[1]; + } else { + $base = $chunks[0]; + for ($i = 1; $i < $numChunks; $i++) { + if (strlen($chunks[$i]) === 4) { + $base .= '-' . ucfirst($chunks[$i]); + break; + } + } + for ($i = 1; $i < $numChunks; $i++) { + if (preg_match('/^[a-z][a-z]$/', $chunks[$i])) { + $found[] = $base . '-' . strtoupper($chunks[$i]); + } elseif (preg_match('/^\\d{3}$/', $chunks[$i])) { + $found[] = $base . '-' . $chunks[$i]; + } + } + if (empty($found)) { + $found[] = $base; + } + } + foreach ($found as $f) { + if (isset($result[$f])) { + if ($result[$f] < $quality) { + $result[$f] = $quality; + } + } else { + $result[$f] = $quality; + } + } + } + } + } + } + arsort($result, SORT_NUMERIC); + + return $result; + } + + /** + * Retrieve the character order (right-to-left or left-to-right). + * + * @param string $locale The locale to use. If empty we'll use the default locale set in \Punic\Data + * + * @return string Return 'left-to-right' or 'right-to-left' + */ + public static function getCharacterOrder($locale = '') + { + $data = Data::get('layout', $locale); + + return $data['characterOrder']; + } + + /** + * Retrieve the line order (top-to-bottom or bottom-to-top). + * + * @param string $locale The locale to use. If empty we'll use the default locale set in \Punic\Data + * + * @return string Return 'top-to-bottom' or 'bottom-to-top' + */ + public static function getLineOrder($locale = '') + { + $data = Data::get('layout', $locale); + + return $data['lineOrder']; + } + + /** + * @deprecated use joinAnd($list, '', $locale) + * + * @param array|\Traversable $list + * @param string $locale + * + * @return string + */ + public static function join($list, $locale = '') + { + return static::joinInternal($list, 'standard', '', $locale); + } + + /** + * Concatenates a list of items returning a localized string. + * + * @param array|\Traversable $list The list to concatenate + * @param string $type The type of list; 'standard' (e.g. '1, 2, and 3'), 'or' ('1, 2, or 3') or 'unit' ('3 ft, 2 in'). + * @param string $width The preferred width ('' for default, or 'short' or 'narrow') + * @param string $locale The locale to use. If empty we'll use the default locale set in \Punic\Data + * + * @return string returns an empty string if $list is not an array of it it's empty, the joined items otherwise + */ + protected static function joinInternal($list, $type, $width, $locale) + { + $result = ''; + if ($list instanceof Traversable) { + $list = iterator_to_array($list); + } + if (is_array($list)) { + switch ((string) $width) { + case 'narrow': + $suffixes = array('-narrow', '-short', ''); + break; + case 'short': + $suffixes = array('-short', '-narrow', ''); + break; + case '': + $suffixes = array('', '-short', '-narrow'); + break; + default: + throw new \Punic\Exception\ValueNotInList($width, array('', 'short', 'narrow')); + } + + $list = array_values($list); + $n = count($list); + switch ($n) { + case 0: + break; + case 1: + $result = (string) $list[0]; + break; + default: + $allData = Data::get('listPatterns', $locale); + $data = null; + foreach ($suffixes as $suffix) { + $key = $type . $suffix; + if (isset($allData[$key])) { + $data = $allData[$key]; + break; + } + } + if ($data === null) { + $types = array_unique(array_map(function ($key) { + return strtok($key, '-'); + }, array_keys($allData))); + throw new \Punic\Exception\ValueNotInList($type, $types); + } + if (isset($data[$n])) { + $result = vsprintf($data[$n], $list); + } else { + $result = sprintf($data['end'], $list[$n - 2], $list[$n - 1]); + if ($n > 2) { + for ($index = $n - 3; $index > 0; $index--) { + $result = sprintf($data['middle'], $list[$index], $result); + } + $result = sprintf($data['start'], $list[0], $result); + } + } + break; + } + } + + return $result; + } +} diff --git a/3rdparty/punic/punic/src/Number.php b/3rdparty/punic/punic/src/Number.php new file mode 100644 index 00000000..ce02fcec --- /dev/null +++ b/3rdparty/punic/punic/src/Number.php @@ -0,0 +1,442 @@ + 1 ? $full[1] : ''; + $len = strlen($intPart); + if (($groupLength > 0) && ($len > $groupLength)) { + $groupSign = $data['symbols']['group']; + $preLength = 1 + (($len - 1) % 3); + $pre = substr($intPart, 0, $preLength); + $intPart = $pre . $groupSign . implode($groupSign, str_split(substr($intPart, $preLength), $groupLength)); + } + $result = $sign . $intPart; + if ($precision === null) { + if ($floatPath !== '') { + $result .= $decimal . $floatPath; + } + } elseif ($precision > 0) { + $result .= $decimal . substr(str_pad($floatPath, $precision, '0', STR_PAD_RIGHT), 0, $precision); + } + } + } + + return $result; + } + + /** + * Localize a percentage (for instance, converts 12.345 to '1,234.5%' in case of English and to '1.234,5 %' in case of Danish). + * + * @param int|float|string $value The string value to convert + * @param int|null $precision The wanted precision (well use {@link http://php.net/manual/function.round.php}) + * @param string $locale The locale to use. If empty we'll use the default locale set in \Punic\Data + * + * @return string Returns an empty string $value is not a number, otherwise returns the localized representation of the percentage + */ + public static function formatPercent($value, $precision = null, $locale = '') + { + $result = ''; + if (is_numeric($value)) { + $data = Data::get('numbers', $locale); + + if ($precision === null) { + $precision = self::getPrecision($value); + } + $formatted = self::format(100 * abs($value), $precision, $locale); + + $format = $data['percentFormats']['standard'][$value >= 0 ? 'positive' : 'negative']; + $sign = $data['symbols']['percentSign']; + + $result = sprintf($format, $formatted, $sign); + } + + return $result; + } + + /** + * Localize a currency amount (for instance, converts 12.345 to '1,234.5%' in case of English and to '1.234,5 %' in case of Danish). + * + * @param int|float|string $value The string value to convert + * @param string $currencyCode The 3-letter currency code + * @param string $kind The currency variant, either "standard" or "accounting" + * @param int|null $precision The wanted precision (well use {@link http://php.net/manual/function.round.php}) + * @param string $which The currency symbol to use, "" for default, "long" for the currency name, "narrow", "alt" for alternative, or "code" for the 3-letter currency code + * @param string $locale The locale to use. If empty we'll use the default locale set in \Punic\Data + * + * @return string Returns an empty string $value is not a number, otherwise returns the localized representation of the amount + */ + public static function formatCurrency($value, $currencyCode, $kind = 'standard', $precision = null, $which = '', $locale = '') + { + $result = ''; + if (is_numeric($value)) { + $data = Data::get('numbers', $locale); + $data = $data['currencyFormats']; + + if ($precision === null) { + $currencyData = Data::getGeneric('currencyData'); + if (isset($currencyData['fractions'][$currencyCode]['digits'])) { + $precision = $currencyData['fractions'][$currencyCode]['digits']; + } else { + $precision = $currencyData['fractionsDefault']['digits']; + } + } + $formatted = self::format(abs($value), $precision, $locale); + + if (!isset($data[$kind])) { + throw new Exception\ValueNotInList($kind, array_keys($data)); + } + $format = $data[$kind][$value >= 0 ? 'positive' : 'negative']; + + $symbol = null; + switch ($which) { + case 'long': + $value = number_format($value, $precision, '.', ''); + $symbol = Currency::getName($currencyCode, $value, $locale); + break; + case 'code': + $symbol = $currencyCode; + break; + default: + $symbol = Currency::getSymbol($currencyCode, $which, $locale); + break; + } + if (!$symbol) { + $symbol = $currencyCode; + } + + if ($which === 'long') { + $pluralRule = 'count-' . Plural::getRuleOfType($value, Plural::RULETYPE_CARDINAL, $locale); + if (!isset($data['unitPattern'][$pluralRule])) { + $pluralRule = 'count-other'; + } + $unitPattern = $data['unitPattern'][$pluralRule]; + + $result = sprintf($unitPattern, $formatted, $symbol); + } else { + list($before, $after) = explode('%2$s', $format); + if ($after + && preg_match($data['currencySpacing']['afterCurrency']['currency'], $symbol) + && preg_match($data['currencySpacing']['afterCurrency']['surrounding'], sprintf($after, $formatted))) { + $symbol .= $data['currencySpacing']['afterCurrency']['insertBetween']; + } + if ($before + && preg_match($data['currencySpacing']['beforeCurrency']['currency'], $symbol) + && preg_match($data['currencySpacing']['beforeCurrency']['surrounding'], sprintf($before, $formatted))) { + $symbol = $data['currencySpacing']['beforeCurrency']['insertBetween'] . $symbol; + } + + $result = sprintf($format, $formatted, $symbol); + } + } + + return $result; + } + + /** + * Convert a localized representation of a number to a number (for instance, converts the string '1,234' to 1234 in case of English and to 1.234 in case of Italian). + * + * @param string $value The string value to convert + * @param string $locale The locale to use. If empty we'll use the default locale set in \Punic\Data + * + * @return int|float|null Returns null if $value is not valid, the numeric value otherwise + */ + public static function unformat($value, $locale = '') + { + $result = null; + if (is_int($value) || is_float($value)) { + $result = $value; + } elseif (is_string($value) && $value !== '') { + $data = Data::get('numbers', $locale); + $plus = $data['symbols']['plusSign']; + $plusQ = preg_quote($plus); + $minus = $data['symbols']['minusSign']; + $minusQ = preg_quote($minus); + $decimal = $data['symbols']['decimal']; + $decimalQ = preg_quote($decimal); + $group = $data['symbols']['group']; + $groupQ = preg_quote($group); + $ok = true; + $m = null; + if (preg_match('/^' . "({$plusQ}|{$minusQ})?(\\d+(?:{$groupQ}\\d+)*)" . '$/', $value, $m)) { + $sign = $m[1]; + $int = $m[2]; + $float = null; + } elseif (preg_match('/^' . "({$plusQ}|{$minusQ})?(\\d+(?:{$groupQ}\\d+)*){$decimalQ}" . '$/', $value, $m)) { + $sign = $m[1]; + $int = $m[2]; + $float = ''; + } elseif (preg_match('/^' . "({$plusQ}|{$minusQ})?(\\d+(?:{$groupQ}\\d+)*){$decimalQ}(\\d+)" . '$/', $value, $m)) { + $sign = $m[1]; + $int = $m[2]; + $float = $m[3]; + } elseif (preg_match('/^' . "({$plusQ}|{$minusQ})?{$decimalQ}(\\d+)" . '$/', $value, $m)) { + $sign = $m[1]; + $int = '0'; + $float = $m[2]; + } else { + $ok = false; + $float = $int = $sign = null; + } + if ($ok) { + if ($sign === $minus) { + $sign = '-'; + } else { + $sign = ''; + } + $int = str_replace($group, '', $int); + if ($float === null) { + $result = (int) "{$sign}{$int}"; + } else { + $result = (float) "{$sign}{$int}.{$float}"; + } + } + } + + return $result; + } + + /** + * Spell out a number (e.g. "one hundred twenty-three" or "twenty-third") or convert to a different numbering system, e.g Roman numerals. + * + * Some types are language-dependent and reflect e.g. gender and case. Refer to the CLDR XML source for supported types. + * + * Available numbering systems are specified in the "root" locale. + * + * @param int|float|string $value The value to localize/spell out + * @param string $type The format type, e.g. "spellout-numbering", "spellout-numbering-year", "spellout-cardinal", "digits-ordinal", "roman-upper". + * @param string $locale The locale to use. If empty we'll use the default locale set in \Punic\Data + * + * @return string The spelled number + * + * @see https://www.unicode.org/repos/cldr/trunk/common/rbnf/ + * @see https://www.unicode.org/repos/cldr/trunk/common/rbnf/root.xml + */ + public static function spellOut($value, $type, $locale) + { + return self::formatRbnf($value, $type, null, $locale); + } + + /** + * This method should not be called from outside this class. + * + * It is declared public for compatibility with PHP 5.3. + * + * @param int|float|string $value + * @param string $type + * @param int $base + * @param string $locale + * + * @internal + */ + public static function formatRbnf($value, $type, $base, $locale) + { + $data = Data::get('rbnf', $locale); + if (!isset($data[$type])) { + $data += Data::get('rbnf', 'root', true); + } + if (!isset($data[$type])) { + throw new Exception\ValueNotInList($type, array_keys($data)); + } + $data = $data[$type]; + + list($rule, $left, $right, $prevBase) = self::getRbnfRule($value, $data, $base); + + return preg_replace_callback('/([<←>→=])(.*?)\1\1?|\$\((.*?),(.*?)\)\$/u', function ($match) use ($value, $left, $right, $type, $prevBase, $locale) { + if (isset($match[4])) { + $rule = Plural::getRuleOfType($left, $match[3] ? $match[3] : Plural::RULETYPE_CARDINAL, $locale); + $match2 = null; + if (preg_match('/' . $rule . '{(.*?)}/', $match[4], $match2)) { + return $match2[1]; + } + } else { + $base = null; + if ($match[2]) { + if ($match[2][0] !== '%') { + $i = strpos($match[2], '.'); + if ($i === false) { + $precision = 0; + } elseif ($match[2][$i + 1] === '#') { + $precision = null; + } else { + $precision = strspn($match[2], '0', $i + 1); + } + + return Number::format($value, $precision, $locale); + } + $type = substr($match[2], 1); + } + + switch ($match[1]) { + case '=': + break; + case '<': + case '←': + $value = $left; + break; + case '>': + case '→': + $value = $right; + if ($match[0] == '>>>' || $match[0] == '→→→') { + $base = $prevBase; + } + break; + } + + return implode(' ', array_map(function ($v) use ($type, $base, $locale) { + return Number::formatRbnf($v, $type, $base, $locale); + }, (array) $value)); + } + }, $rule); + } + + protected static function getRbnfRule($value, $data, $base = null) + { + $left = 0; + $right = 0; + $prevBase = 0; + if (!is_numeric($value)) { + $rule = ''; + } elseif (is_nan($value) && isset($data['NaN'])) { + $rule = $data['NaN']['rule']; + } elseif ($value < 0) { + $right = -$value; + $rule = $data['-x']['rule']; + } elseif (is_infinite($value) && isset($data['Inf'])) { + $rule = $data['Inf']['rule']; + } elseif (strpos($value, '.') !== false && isset($data['x.x'])) { + list($left, $right) = explode('.', $value); + $right = str_split($right); + if ($left == 0 && isset($data['0.x'])) { + $rule = $data['0.x']['rule']; + } else { + $rule = $data['x.x']['rule']; + } + } else { + $bases = array_keys($data['integer']); + if ($base) { + $i = array_search($base, $bases); + } else { + for ($i = count($bases) - 1; $i >= 0; $i--) { + $base = $bases[$i]; + if ($base <= $value) { + break; + } + } + } + $prevBase = $i > 0 ? $bases[$i - 1] : null; + + $r = $data['integer'][$base] + array('radix' => 10); + $rule = $r['rule']; + $radix = $r['radix']; + + // Add .5 to avoid floating-point rounding error in PHP 5. $base is + // an integer, so adding a number < 1 will not break anything. + $divisor = pow($radix, floor(abs(log($base + .5, $radix)))); + + $right = fmod($value, $divisor); + $left = floor($value / $divisor); + + if ($right) { + $rule = str_replace(array('[', ']'), '', $rule); + } else { + $rule = preg_replace('/\[.*?\]/', '', $rule); + } + } + + return array($rule, $left, $right, $prevBase); + } + + private static function getPrecision($value) + { + $precision = null; + if (is_string($value)) { + $i = strrpos($value, '.'); + if ($i !== false) { + $precision = strlen($value) - $i - 1; + } + } + + return $precision; + } +} diff --git a/3rdparty/punic/punic/src/Phone.php b/3rdparty/punic/punic/src/Phone.php new file mode 100644 index 00000000..9b1c2746 --- /dev/null +++ b/3rdparty/punic/punic/src/Phone.php @@ -0,0 +1,77 @@ + $prefixes) { + if (in_array($prefix, $prefixes)) { + $result[] = $territoryCode; + } + } + } + + return $result; + } + + /** + * Retrieve the max length of the country calling codes. + * + * @return int + */ + public static function getMaxPrefixLength() + { + static $result; + if (!isset($result)) { + $maxLen = 0; + $data = Data::getGeneric('telephoneCodeData'); + foreach ($data as $prefixes) { + foreach ($prefixes as $prefix) { + $len = strlen($prefix); + if ($maxLen < $len) { + $maxLen = $len; + } + } + } + $result = $maxLen; + } + + return $result; + } +} diff --git a/3rdparty/punic/punic/src/Plural.php b/3rdparty/punic/punic/src/Plural.php new file mode 100644 index 00000000..1c66a5a3 --- /dev/null +++ b/3rdparty/punic/punic/src/Plural.php @@ -0,0 +1,193 @@ + Returns a list containing some the following values: 'zero', 'one', 'two', 'few', 'many', 'other' ('other' will be always there) + */ + public static function getRules($locale = '') + { + $node = Data::getLanguageNode(Data::getGeneric('plurals'), $locale); + + return array_merge( + array_keys($node), + array('other') + ); + } + + /** + * @deprecated Use getRuleOfType with a Plural::RULETYPE_CARDINAL type + * + * @param string|int|float $number + * @param string $locale + * + * @throws \Punic\Exception\BadArgumentType + * @throws \Exception + * + * @return string + */ + public static function getRule($number, $locale = '') + { + return self::getRuleOfType($number, self::RULETYPE_CARDINAL); + } + + /** + * Return the plural rule ('zero', 'one', 'two', 'few', 'many' or 'other') for a number and a locale. + * + * @param string|int|float $number The number to check the plural rule for for + * @param string $type The type of plural rules (one of the \Punic\Plural::RULETYPE_... constants) + * @param string $locale The locale to use. If empty we'll use the default locale set in \Punic\Data + * + * @throws \Punic\Exception\BadArgumentType Throws a \Punic\Exception\BadArgumentType if $number is not a valid number + * @throws \Punic\Exception\ValueNotInList Throws a \Punic\Exception\ValueNotInList if $type is not valid + * @throws \Exception Throws a \Exception if there were problems calculating the plural rule + * + * @return string Returns one of the following values: 'zero', 'one', 'two', 'few', 'many', 'other' + */ + public static function getRuleOfType($number, $type, $locale = '') + { + if (is_int($number)) { + $intPartAbs = (string) abs($number); + $floatPart = ''; + } elseif (is_float($number)) { + $s = (string) $number; + if (strpos($s, '.') === false) { + $intPart = $s; + $floatPart = ''; + } else { + list($intPart, $floatPart) = explode('.', $s); + } + $intPartAbs = (string) abs((int) $intPart); + } elseif (is_string($number) && $number !== '') { + $m = null; + if (preg_match('/^[+|\\-]?\\d+\\.?$/', $number)) { + $v = (int) $number; + $intPartAbs = (string) abs($v); + $floatPart = ''; + } elseif (preg_match('/^(\\d*)\\.(\\d+)$/', $number, $m)) { + list($intPart, $floatPart) = explode('.', $number); + $v = @(int) $intPart; + $intPartAbs = (string) abs($v); + } else { + throw new Exception\BadArgumentType($number, 'number'); + } + } else { + throw new Exception\BadArgumentType($number, 'number'); + } + // 'n' => '%1$s', // absolute value of the source number (integer and decimals). + $v1 = $intPartAbs . (strlen($floatPart) ? ".{$floatPart}" : ''); + // 'i' => '%2$s', // integer digits of n + $v2 = $intPartAbs; + // 'v' => '%3$s', // number of visible fraction digits in n, with trailing zeros. + $v3 = strlen($floatPart); + // 'w' => '%4$s', // number of visible fraction digits in n, without trailing zeros. + $v4 = strlen(rtrim($floatPart, '0')); + // 'f' => '%5$s', // visible fractional digits in n, with trailing zeros. + $v5 = strlen($floatPart) ? (string) ((int) $floatPart) : '0'; + // 't' => '%6$s', // visible fractional digits in n, without trailing zeros. + $v6 = trim($floatPart, '0'); + if ($v6 === '') { + $v6 = '0'; + } + // 'c' => compact decimal exponent value: exponent of the power of 10 used in compact decimal formatting. + // 'e' => currently, synonym for ‘c’. however, may be redefined in the future. + // Not yet supported + $v7 = '0'; + $result = 'other'; + $identifierMap = array( + self::RULETYPE_CARDINAL => 'plurals', + self::RULETYPE_ORDINAL => 'ordinals', + ); + if (!isset($identifierMap[$type])) { + throw new Exception\ValueNotInList($type, array_keys($identifierMap)); + } + $identifier = $identifierMap[$type]; + $node = Data::getLanguageNode(Data::getGeneric($identifier), $locale); + foreach ($node as $rule => $formulaPattern) { + $formula = sprintf($formulaPattern, $v1, $v2, $v3, $v4, $v5, $v6, $v7); + $check = str_replace(array('static::inRange(', ' and ', ' or ', ', false, ', ', true, ', ', array('), ' , ', $formula); + if (preg_match('/[a-z]/', $check)) { + throw new PHPException('Bad formula!'); + } + // fix for difference in modulo (%) in the definition and the one implemented in PHP for decimal numbers + while (preg_match('/(\\d+\\.\\d+) % (\\d+(\\.\\d+)?)/', $formula, $m)) { + list(, $decimalPart) = explode('.', $m[1], 2); + $decimals = strlen(rtrim($decimalPart, '0')); + if ($decimals > 0) { + $pow = (int) pow(10, $decimals); + $repl = '(' . (string) ((int) ((float) $m[1] * $pow)) . ' % ' . (string) ((int) ((float) ($m[2] * $pow))) . ') / ' . $pow; + } else { + $repl = (string) ((int) $m[1]) . ' % ' . $m[2]; + } + $formula = str_replace($m[0], $repl, $formula); + } + $formulaResult = @eval("return ({$formula}) ? 'yes' : 'no';"); + if ($formulaResult === 'yes') { + $result = $rule; + break; + } + if ($formulaResult !== 'no') { + throw new PHPException('There was a problem in the formula ' . $formulaPattern); + } + } + + return $result; + } + + /** + * @param int|string|array $value + * @param bool $mustBeIncluded + * + * @return bool + */ + protected static function inRange($value, $mustBeIncluded) + { + if (is_int($value)) { + $isInt = true; + } elseif ($value == (int) $value) { + $isInt = true; + } else { + $isInt = false; + } + $rangeValues = (func_num_args() > 2) ? array_slice(func_get_args(), 2) : array(); + $included = false; + foreach ($rangeValues as $rangeValue) { + if (is_array($rangeValue)) { + if ($isInt && ($value >= $rangeValue[0]) && ($value <= $rangeValue[1])) { + $included = true; + break; + } + } elseif ($value == $rangeValue) { + $included = true; + break; + } + } + + return $included == $mustBeIncluded; + } +} diff --git a/3rdparty/punic/punic/src/Script.php b/3rdparty/punic/punic/src/Script.php new file mode 100644 index 00000000..f359c685 --- /dev/null +++ b/3rdparty/punic/punic/src/Script.php @@ -0,0 +1,141 @@ + $scriptData) { + $result[$scriptCode] = self::extractScriptName($scriptData, $preferredVariant); + } + $comparer = new Comparer($locale); + $comparer->sort($result, true); + + return $result; + } + + /** + * @param string|array $scriptData + * @param string|mixed $preferredVariant + * + * @return string + */ + private static function extractScriptName($scriptData, $preferredVariant) + { + if (is_string($scriptData)) { + return $scriptData; + } + if (!is_string($preferredVariant) || $preferredVariant === '' || !isset($scriptData[$preferredVariant])) { + return $scriptData['']; + } + return $scriptData[$preferredVariant][0]; + } +} diff --git a/3rdparty/punic/punic/src/Territory.php b/3rdparty/punic/punic/src/Territory.php new file mode 100644 index 00000000..92a02843 --- /dev/null +++ b/3rdparty/punic/punic/src/Territory.php @@ -0,0 +1,573 @@ + 0) { + $territoryCode = strtoupper($territoryCode); + $data = Data::get('territories', $locale); + } else { + $territoryCode = strtolower($territoryCode); + $data = Data::get('subdivisions', $locale); + } + if (isset($data[$territoryCode])) { + $result = $data[$territoryCode]; + } + } + + return $result; + } + + /** + * Retrieve the code of a territory in a different coding system. + * + * @param string $territoryCode The territory code + * @param string $type The type of code to return. "alpha3" for ISO 3166-1 alpha-3 codes, "numeric" for UN M.49, or "fips10" for FIPS 10 codes + * + * @throws \Punic\Exception\ValueNotInList throws a ValueNotInList exception if $type is not valid + * + * @return string|array returns the code for the specified territory, or an empty string if the code is not defined for the territory or the territory is unknown + * + * @see http://unicode.org/reports/tr35/tr35-info.html#Supplemental_Code_Mapping + */ + public static function getCode($territoryCode, $type) + { + $codeMappings = Data::getGeneric('codeMappings'); + $territories = $codeMappings['territories']; + + if (!in_array($type, static::$CODE_TYPES)) { + throw new Exception\ValueNotInList($type, static::$CODE_TYPES); + } + + if (!isset($territories[$territoryCode])) { + $result = ''; + } elseif (isset($territories[$territoryCode][$type])) { + $result = $territories[$territoryCode][$type]; + } elseif ($type !== 'numeric' && $type !== 'alpha3') { + $result = $territoryCode; + } else { + $result = ''; + } + + return $result; + } + + /** + * Retrieve the territory code given its code in a different coding system. + * + * @param string $code The code + * @param string $type The type of code provided. "alpha3" for ISO 3166-1 alpha-3 codes, "numeric" for UN M.49, or "fips10" for FIPS 10 codes + * + * @throws \Punic\Exception\ValueNotInList throws a ValueNotInList exception if $type is not valid + * + * @return string returns the code for the specified territory, or null if the code is unknown + * + * @see http://unicode.org/reports/tr35/tr35-info.html#Supplemental_Code_Mapping + */ + public static function getByCode($code, $type) + { + $codeMappings = Data::getGeneric('codeMappings'); + $territories = $codeMappings['territories']; + + if (!in_array($type, static::$CODE_TYPES)) { + throw new Exception\ValueNotInList($type, static::$CODE_TYPES); + } + + foreach ($territories as $territoryCode => $territory) { + $c = isset($territory[$type]) ? $territory[$type] : $territoryCode; + if (is_array($c)) { + if (in_array($code, $c)) { + return $territoryCode; + } + } elseif ($code == $c) { + return $territoryCode; + } + } + + return null; + } + + /** + * Return the list of continents in the form of an array with key=ID, value=name. + * + * @param string $locale The locale to use. If empty we'll use the default locale set in \Punic\Data + * + * @return array + */ + public static function getContinents($locale = '') + { + return static::getList('C', $locale); + } + + /** + * Return the list of countries in the form of an array with key=ID, value=name. + * + * @param string $locale The locale to use. If empty we'll use the default locale set in \Punic\Data + * + * @return array + */ + public static function getCountries($locale = '') + { + return static::getList('c', $locale); + } + + /** + * Return a list of continents and relative countries. The resulting array is in the following form (JSON representation): + * ```json + * { + * "002": { + * "name": "Africa", + * "children": { + * "DZ": {"name": "Algeria"}, + * "AO": {"name": "Angola"}, + * ... + * } + * }, + * "150": { + * "name": "Europe", + * "children": { + * "AL": {"name": "Albania"}, + * "AD": {"name": "Andorra"}, + * ... + * } + * } + * ... + * } + * ``` + * The arrays are sorted by territory name. + * + * @param string $locale The locale to use. If empty we'll use the default locale set in \Punic\Data + * + * @return array + */ + public static function getContinentsAndCountries($locale = '') + { + return static::getList('Cc', $locale); + } + + /** + * Return a list of some specified territory/subdivision, structured or not. + * $levels control which data you want to retrieve. It can be one or more of the following values: + *
      + *
    • 'W': world
    • + *
    • 'C': continents
    • + *
    • 'S': sub-continents
    • + *
    • 'c': countries
    • + *
    • 's': subdivisions
    • + *
    + * If only one level is specified you'll get a flat list (like the one returned by {@link getContinents}). + * If one or more levels are specified, you'll get a structured list (like the one returned by {@link getContinentsAndCountries}). + * + * @param string $levels A string with one or more of the characters: 'W' (for world), 'C' (for continents), 'S' (for sub-continents), 'c' (for countries) + * @param string $locale The locale to use. If empty we'll use the default locale set in \Punic\Data + * + * @throws Exception\BadArgumentType + * + * @return array + * + * @see http://www.unicode.org/cldr/charts/latest/supplemental/territory_containment_un_m_49.html + * @see http://www.unicode.org/cldr/charts/latest/supplemental/territory_subdivisions.html + */ + public static function getList($levels = 'W', $locale = '') + { + static $levelMap = array('W' => 0, 'C' => 1, 'S' => 2, 'c' => 3, 's' => 4); + $decodedLevels = array(); + $n = is_string($levels) ? strlen($levels) : 0; + if ($n > 0) { + for ($i = 0; $i < $n; $i++) { + $l = substr($levels, $i, 1); + if (!isset($levelMap[$l])) { + $decodedLevels = array(); + break; + } + if (!in_array($levelMap[$l], $decodedLevels, true)) { + $decodedLevels[] = $levelMap[$l]; + } + } + } + if (count($decodedLevels) === 0) { + throw new Exception\BadArgumentType($levels, "list of territory kinds: it should be a list of one or more of the codes '" . implode("', '", array_keys($levelMap)) . "'"); + } + $struct = self::filterStructure(self::getStructure(), $decodedLevels, 0); + $flatList = (count($decodedLevels) > 1) ? false : true; + $data = Data::get('territories', $locale); + if (strpos($levels, 's') !== false) { + $data += Data::get('subdivisions', $locale); + } + $finalized = self::finalizeWithNames($data, $struct, $flatList); + + if ($flatList) { + $sorter = new Comparer(); + $sorter->sort($finalized, true); + } else { + $finalized = static::sort($finalized); + } + + return $finalized; + } + + /** + * Return a list of territory identifiers for which we have some info (languages, population, literacy level, Gross Domestic Product). + * + * @return array The list of territory IDs for which we have some info + */ + public static function getTerritoriesWithInfo() + { + return array_keys(Data::getGeneric('territoryInfo')); + } + + /** + * Return the list of languages spoken in a territory. + * + * @param string $territoryCode The territory code + * @param string $filterStatuses Filter language status. + *
      + *
    • If empty no filter will be applied
    • + *
    • 'o' to include official languages
    • + *
    • 'r' to include official regional languages
    • + *
    • 'f' to include de facto official languages
    • + *
    • 'm' to include official minority languages
    • + *
    • 'u' to include unofficial or unknown languages
    • + *
    + * @param string $onlyCodes Set to true to retrieve only the language codes. If set to false (default) you'll receive a list of arrays with these keys: + *
      + *
    • string id: the language identifier
    • + *
    • string status: 'o' for official; 'r' for official regional; 'f' for de facto official; 'm' for official minority; 'u' for unofficial or unknown
    • + *
    • number population: the amount of people speaking the language (%)
    • + *
    • number|null writing: the amount of people able to write (%). May be null if no data is available
    • + *
    + * + * @return array|null Return the languages spoken in the specified territory, as described by the $onlyCodes parameter (or null if $territoryCode is not valid or no data is available) + */ + public static function getLanguages($territoryCode, $filterStatuses = '', $onlyCodes = false) + { + $result = null; + $info = self::getTerritoryInfo($territoryCode); + if (is_array($info)) { + $result = array(); + foreach ($info['languages'] as $languageID => $languageInfo) { + if (!isset($languageInfo['status'])) { + $languageInfo['status'] = 'u'; + } + if ((strlen($filterStatuses) === 0) || (stripos($filterStatuses, $languageInfo['status']) !== false)) { + if (!isset($languageInfo['writing'])) { + $languageInfo['writing'] = null; + } + if ($onlyCodes) { + $result[] = $languageID; + } else { + $result[] = array_merge(array('id' => $languageID), $languageInfo); + } + } + } + } + + return $result; + } + + /** + * Return the population of a specific territory. + * + * @param string $territoryCode The territory code + * + * @return number|null Return the size of the population of the specified territory (or null if $territoryCode is not valid or no data is available) + */ + public static function getPopulation($territoryCode) + { + $result = null; + $info = self::getTerritoryInfo($territoryCode); + if (is_array($info)) { + $result = $info['population']; + } + + return $result; + } + + /** + * Return the literacy level for a specific territory, in %. + * + * @param string $territoryCode The territory code + * + * @return number|null Return the % of literacy lever of the specified territory (or null if $territoryCode is not valid or no data is available) + */ + public static function getLiteracyLevel($territoryCode) + { + $result = null; + $info = self::getTerritoryInfo($territoryCode); + if (is_array($info)) { + $result = $info['literacy']; + } + + return $result; + } + + /** + * Return the GDP (Gross Domestic Product) for a specific territory, in US$. + * + * @param string $territoryCode The territory code + * + * @return number|null Return the GDP of the specified territory (or null if $territoryCode is not valid or no data is available) + */ + public static function getGrossDomesticProduct($territoryCode) + { + $result = null; + $info = self::getTerritoryInfo($territoryCode); + if (is_array($info)) { + $result = $info['gdp']; + } + + return $result; + } + + /** + * Return a list of territory IDs where a specific language is spoken, sorted by the total number of people speaking that language. + * + * @param string $languageID The language identifier + * @param float $threshold The minimum percentage (from 0 to 100) to consider a language as spoken in a Country + * + * @return array + */ + public static function getTerritoriesForLanguage($languageID, $threshold = 0) + { + $peopleInTerritory = array(); + foreach (Data::getGeneric('territoryInfo') as $territoryID => $territoryInfo) { + $percentage = null; + foreach ($territoryInfo['languages'] as $langID => $langInfo) { + if ((strcasecmp($languageID, $langID) === 0) || (stripos($langID, $languageID . '_') === 0)) { + if ($percentage === null) { + $percentage = $langInfo['population']; + } else { + $percentage += $langInfo['population']; + } + } + } + if ($percentage !== null && $percentage >= $threshold) { + $peopleInTerritory[$territoryID] = $territoryInfo['population'] * $percentage; + } + } + arsort($peopleInTerritory, SORT_NUMERIC); + return array_keys($peopleInTerritory); + } + + /** + * Return the code of the territory/subdivision that contains a territory/subdivision. + * + * @param string $childTerritoryCode + * + * @return string return the parent territory/subdivision code, or an empty string if $childTerritoryCode is the World (001) or if it's invalid + */ + public static function getParentTerritoryCode($childTerritoryCode) + { + $result = ''; + if (is_string($childTerritoryCode) && preg_match('/^[a-z0-9]{2,5}$/i', $childTerritoryCode)) { + if (strlen($childTerritoryCode) == 2 || (int) $childTerritoryCode > 0) { + $childTerritoryCode = strtoupper($childTerritoryCode); + $data = Data::getGeneric('territoryContainment'); + } else { + $childTerritoryCode = strtolower($childTerritoryCode); + $data = Data::getGeneric('subdivisionContainment'); + } + foreach ($data as $parentTerritoryCode => $parentTerritoryInfo) { + if (in_array($childTerritoryCode, $parentTerritoryInfo['contains'], false)) { + $result = is_int($parentTerritoryCode) ? substr('00' . $parentTerritoryCode, -3) : $parentTerritoryCode; + if ($result === '001' || static::getParentTerritoryCode($result) !== '') { + break; + } + } + } + } + + return $result; + } + + /** + * Retrieve the child territories/subdivisions of a parent territory. + * + * @param string $parentTerritoryCode + * @param bool $expandSubGroups set to true to expand the sub-groups, false to retrieve them + * @param bool $expandSubdivisions set to true to expand countries into subdivisions, false to retrieve them + * + * @return array Return the list of territory codes that are children of $parentTerritoryCode (if $parentTerritoryCode is invalid you'll get an empty list) + */ + public static function getChildTerritoryCodes($parentTerritoryCode, $expandSubGroups = false, $expandSubdivisions = false) + { + $result = array(); + if (is_string($parentTerritoryCode) && preg_match('/^[a-z0-9]{2,5}$/i', $parentTerritoryCode)) { + if (strlen($parentTerritoryCode) == 2 || (int) $parentTerritoryCode > 0) { + $parentTerritoryCode = strtoupper($parentTerritoryCode); + } else { + $parentTerritoryCode = strtolower($parentTerritoryCode); + $expandSubdivisions = true; + } + $data = Data::getGeneric('territoryContainment'); + if ($expandSubdivisions) { + $data += Data::getGeneric('subdivisionContainment'); + } + if (isset($data[$parentTerritoryCode])) { + $children = $data[$parentTerritoryCode]['contains']; + if ($expandSubGroups) { + foreach ($children as $child) { + $grandChildren = static::getChildTerritoryCodes($child, true, $expandSubdivisions); + if (empty($grandChildren)) { + $result[] = $child; + } else { + $result = array_merge($result, $grandChildren); + } + } + } else { + $result = $children; + } + } + } + + return $result; + } + + /** + * @param string $territoryCode + * + * @return array|null + */ + protected static function getTerritoryInfo($territoryCode) + { + $result = null; + if (preg_match('/^[a-z0-9]{2,3}$/i', $territoryCode)) { + $territoryCode = strtoupper($territoryCode); + $data = Data::getGeneric('territoryInfo'); + if (isset($data[$territoryCode])) { + $result = $data[$territoryCode]; + } + } + + return $result; + } + + /** + * @return array + */ + protected static function getStructure() + { + static $cache = null; + if ($cache === null) { + $data = Data::getGeneric('territoryContainment') + Data::getGeneric('subdivisionContainment'); + $result = static::fillStructure($data, '001', 0); + $cache = $result; + } else { + $result = $cache; + } + + return $result; + } + + /** + * @param array $data + * @param string $id + * @param int $level + * + * @return array + */ + protected static function fillStructure($data, $id, $level) + { + $item = array('id' => $id, 'level' => $level, 'children' => array()); + if (isset($data[$id])) { + foreach ($data[$id]['contains'] as $childID) { + $item['children'][] = static::fillStructure($data, $childID, $level + 1); + } + } + + return $item; + } + + /** + * @param array $data + * @param array $list + * @param bool $flatList + * + * @return array + */ + protected static function finalizeWithNames($data, $list, $flatList) + { + $result = array(); + foreach ($list as $item) { + $name = $data[$item['id']]; + if ($flatList) { + $result[$item['id']] = $name; + } else { + $result[$item['id']] = array('name' => $name); + if (count($item['children']) > 0) { + $result[$item['id']]['children'] = static::finalizeWithNames($data, $item['children'], $flatList); + } + } + } + + return $result; + } + + /** + * @param array $parent + * @param int[] $levels + * + * @return array + */ + protected static function filterStructure($parent, $levels) + { + $thisResult = array(); + if (in_array($parent['level'], $levels, true)) { + $thisResult[0] = $parent; + $thisResult[0]['children'] = array(); + $addToSub = true; + } else { + $addToSub = false; + } + + $subList = array(); + foreach ($parent['children'] as $child) { + $subList = array_merge($subList, static::filterStructure($child, $levels)); + } + if ($addToSub) { + $thisResult[0]['children'] = $subList; + } else { + $thisResult = $subList; + } + + return $thisResult; + } + + /** + * @param array $list + * + * @return array + */ + protected static function sort($list) + { + foreach (array_keys($list) as $i) { + if (isset($list[$i]['children'])) { + $list[$i]['children'] = static::sort($list[$i]['children']); + } + } + $sorter = new \Punic\Comparer(); + uasort($list, function ($a, $b) use ($sorter) { + return $sorter->compare($a['name'], $b['name']); + }); + + return $list; + } +} diff --git a/3rdparty/punic/punic/src/Unit.php b/3rdparty/punic/punic/src/Unit.php new file mode 100644 index 00000000..f1c8817b --- /dev/null +++ b/3rdparty/punic/punic/src/Unit.php @@ -0,0 +1,355 @@ + $units) { + if ($width[0] !== '_') { + foreach ($units as $category => $units) { + if ($category[0] !== '_') { + $unitIDs = array_keys($units); + if (isset($categories[$category])) { + $categories[$category] = array_unique(array_merge($categories[$category], $unitIDs)); + } else { + $categories[$category] = array_keys($units); + } + } + } + } + } + ksort($categories); + foreach (array_keys($categories) as $category) { + sort($categories[$category]); + } + + return $categories; + } + + /** + * Get the localized name of a unit. + * + * @param string $unit The unit identifier (eg 'duration/millisecond' or 'millisecond') + * @param string $width The format name; it can be 'long' ('milliseconds'), 'short' (eg 'millisecs') or 'narrow' (eg 'msec') + * @param string $locale The locale to use. If empty we'll use the default locale set in \Punic\Data + * + * @throws Exception\ValueNotInList + * + * @return string + */ + public static function getName($unit, $width = 'short', $locale = '') + { + $data = self::getDataForWidth($width, $locale); + $unitData = self::getDataForUnit($data, $unit); + + return $unitData['_name']; + } + + /** + * Get the "per" localized format string of a unit. + * + * @param string $unit The unit identifier (eg 'duration/minute' or 'minute') + * @param string $width The format name; it can be 'long' ('%1$s per minute'), 'short' (eg '%1$s/min') or 'narrow' (eg '%1$s/min') + * @param string $locale The locale to use. If empty we'll use the default locale set in \Punic\Data + * + * @throws Exception\ValueNotInList + * + * @return string + */ + public static function getPerFormat($unit, $width = 'short', $locale = '') + { + $data = self::getDataForWidth($width, $locale); + $unitData = self::getDataForUnit($data, $unit); + + if (isset($unitData['_per'])) { + return $unitData['_per']; + } + $pluralRule = Plural::getRuleOfType(1, Plural::RULETYPE_CARDINAL, $locale); + $name = trim(sprintf($unitData[$pluralRule], '')); + + return sprintf($data['_compoundPattern'], '%1$s', $name); + } + + /** + * Format a unit string. + * + * @param int|float|string $number The unit amount + * @param string $unit The unit identifier (eg 'duration/millisecond' or 'millisecond') + * @param string $width The format name; it can be 'long' (eg '3 milliseconds'), 'short' (eg '3 ms') or 'narrow' (eg '3ms'). You can also add a precision specifier ('long,2' or just '2') + * @param string $locale The locale to use. If empty we'll use the default locale set in \Punic\Data + * + * @throws Exception\ValueNotInList + * + * @return string + */ + public static function format($number, $unit, $width = 'short', $locale = '') + { + $precision = null; + $m = null; + if (is_int($width)) { + $precision = $width; + $width = 'short'; + } elseif (is_string($width) && preg_match('/^(?:(.*),)?([+\\-]?\\d+)$/', $width, $m)) { + $precision = (int) $m[2]; + $width = (string) $m[1]; + if ($width === '') { + $width = 'short'; + } + } + $data = self::getDataForWidth($width, $locale); + $rules = self::getDataForUnit($data, $unit); + $pluralRule = Plural::getRuleOfType($number, Plural::RULETYPE_CARDINAL, $locale); + //@codeCoverageIgnoreStart + // These checks aren't necessary since $pluralRule should always be in $rules, but they don't hurt ;) + if (!isset($rules[$pluralRule])) { + if (isset($rules['other'])) { + $pluralRule = 'other'; + } else { + $availableRules = array_keys($rules); + $pluralRule = $availableRules[0]; + } + } + //@codeCoverageIgnoreEnd + return sprintf($rules[$pluralRule], Number::format($number, $precision, $locale)); + } + + /** + * Retrieve the measurement systems and their localized names. + * + * @param string $locale The locale to use. If empty we'll use the default locale set in \Punic\Data + * + * @return array The array keys are the measurement system codes (eg 'metric', 'US', 'UK'), the values are the localized measurement system names (eg 'Metric', 'US', 'UK' for English) + */ + public static function getMeasurementSystems($locale = '') + { + return Data::get('measurementSystemNames', $locale); + } + + /** + * Retrieve the measurement system for a specific territory. + * + * @param string $territoryCode The territory code (eg. 'US' for 'United States of America'). + * + * @return string Return the measurement system code (eg: 'metric') for the specified territory. If $territoryCode is not valid we'll return an empty string. + */ + public static function getMeasurementSystemFor($territoryCode) + { + $result = ''; + if (is_string($territoryCode) && preg_match('/^[a-z0-9]{2,3}$/i', $territoryCode)) { + $territoryCode = strtoupper($territoryCode); + $data = Data::getGeneric('measurementData'); + while ($territoryCode !== '') { + if (isset($data['measurementSystem'][$territoryCode])) { + $result = $data['measurementSystem'][$territoryCode]; + break; + } + $territoryCode = Territory::getParentTerritoryCode($territoryCode); + } + } + + return $result; + } + + /** + * Returns the list of countries that use a specific measurement system. + * + * @param string $measurementSystem The measurement system identifier ('metric', 'US' or 'UK') + * + * @return array The list of country IDs that use the specified measurement system (if $measurementSystem is invalid you'll get an empty array) + */ + public static function getCountriesWithMeasurementSystem($measurementSystem) + { + $result = array(); + if (is_string($measurementSystem) && $measurementSystem !== '') { + $someGroup = false; + $data = Data::getGeneric('measurementData'); + foreach ($data['measurementSystem'] as $territory => $ms) { + if (strcasecmp($measurementSystem, $ms) === 0) { + $children = Territory::getChildTerritoryCodes($territory, true); + if (empty($children)) { + $result[] = $territory; + } else { + $someGroup = true; + $result = array_merge($result, $children); + } + } + } + if ($someGroup) { + $otherCountries = array(); + foreach ($data['measurementSystem'] as $territory => $ms) { + if (($territory !== '001') && (strcasecmp($measurementSystem, $ms) !== 0)) { + $children = Territory::getChildTerritoryCodes($territory, true); + if (empty($children)) { + $otherCountries[] = $territory; + } else { + $otherCountries = array_merge($otherCountries, $children); + } + } + } + $result = array_values(array_diff($result, $otherCountries)); + } + } + + return $result; + } + + /** + * Retrieve the standard paper size for a specific territory. + * + * @param string $territoryCode The territory code (eg. 'US' for 'United States of America'). + * + * @return string Return the standard paper size (eg: 'A4' or 'US-Letter') for the specified territory. If $territoryCode is not valid we'll return an empty string. + */ + public static function getPaperSizeFor($territoryCode) + { + $result = ''; + if (is_string($territoryCode) && preg_match('/^[a-z0-9]{2,3}$/i', $territoryCode)) { + $territoryCode = strtoupper($territoryCode); + $data = Data::getGeneric('measurementData'); + while ($territoryCode !== '') { + if (isset($data['paperSize'][$territoryCode])) { + $result = $data['paperSize'][$territoryCode]; + break; + } + $territoryCode = Territory::getParentTerritoryCode($territoryCode); + } + } + + return $result; + } + + /** + * Returns the list of countries that use a specific paper size by default. + * + * @param string $paperSize The paper size identifier ('A4' or 'US-Letter') + * + * @return array The list of country IDs that use the specified paper size (if $paperSize is invalid you'll get an empty array) + */ + public static function getCountriesWithPaperSize($paperSize) + { + $result = array(); + if (is_string($paperSize) && $paperSize !== '') { + $someGroup = false; + $data = Data::getGeneric('measurementData'); + foreach ($data['paperSize'] as $territory => $ms) { + if (strcasecmp($paperSize, $ms) === 0) { + $children = Territory::getChildTerritoryCodes($territory, true); + if (empty($children)) { + $result[] = $territory; + } else { + $someGroup = true; + $result = array_merge($result, $children); + } + } + } + if ($someGroup) { + $otherCountries = array(); + foreach ($data['paperSize'] as $territory => $ms) { + if (($territory !== '001') && (strcasecmp($paperSize, $ms) !== 0)) { + $children = Territory::getChildTerritoryCodes($territory, true); + if (empty($children)) { + $otherCountries[] = $territory; + } else { + $otherCountries = array_merge($otherCountries, $children); + } + } + } + $result = array_values(array_diff($result, $otherCountries)); + } + } + + return $result; + } + + /** + * Get the width-specific unit data. + * + * @param string $width the data width + * @param string $locale The locale to use. If empty we'll use the default locale set in \Punic\Data + * + * @throws Exception\ValueNotInList + * + * @return array + */ + private static function getDataForWidth($width, $locale = '') + { + $data = Data::get('units', $locale); + if ($width[0] === '_' || !isset($data[$width])) { + $widths = array(); + foreach (array_keys($data) as $w) { + if (strpos($w, '_') !== 0) { + $widths[] = $w; + } + } + throw new Exception\ValueNotInList($width, $widths); + } + + return $data[$width]; + } + + /** + * Get a unit-specific data. + * + * @param array $data the width-specific data + * @param string $unit The unit identifier (eg 'duration/millisecond' or 'millisecond') + * + * @throws Exception\ValueNotInList + * + * @return array + */ + private static function getDataForUnit(array $data, $unit) + { + $chunks = explode('/', $unit, 2); + if (isset($chunks[1])) { + list($unitCategory, $unitID) = $chunks; + } else { + $unitCategory = null; + $unitID = null; + foreach (array_keys($data) as $c) { + if ($c[0] !== '_') { + if (isset($data[$c][$unit])) { + if ($unitCategory === null) { + $unitCategory = $c; + $unitID = $unit; + } else { + $unitCategory = null; + break; + } + } + } + } + } + if ( + $unitCategory === null || $unitCategory[0] === '_' + || !isset($data[$unitCategory]) + || $unitID === null || $unitID[0] === '_' + || !isset($data[$unitCategory][$unitID]) + ) { + $units = array(); + foreach ($data as $c => $us) { + if (strpos($c, '_') === false) { + foreach (array_keys($us) as $u) { + if (strpos($c, '_') === false) { + $units[] = "{$c}/{$u}"; + } + } + } + } + throw new \Punic\Exception\ValueNotInList($unit, $units); + } + + return $data[$unitCategory][$unitID]; + } +} diff --git a/3rdparty/ralouphie/getallheaders/LICENSE b/3rdparty/ralouphie/getallheaders/LICENSE new file mode 100644 index 00000000..be5540c2 --- /dev/null +++ b/3rdparty/ralouphie/getallheaders/LICENSE @@ -0,0 +1,21 @@ +The MIT License (MIT) + +Copyright (c) 2014 Ralph Khattar + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. diff --git a/3rdparty/ralouphie/getallheaders/src/getallheaders.php b/3rdparty/ralouphie/getallheaders/src/getallheaders.php new file mode 100644 index 00000000..c7285a5b --- /dev/null +++ b/3rdparty/ralouphie/getallheaders/src/getallheaders.php @@ -0,0 +1,46 @@ + 'Content-Type', + 'CONTENT_LENGTH' => 'Content-Length', + 'CONTENT_MD5' => 'Content-Md5', + ); + + foreach ($_SERVER as $key => $value) { + if (substr($key, 0, 5) === 'HTTP_') { + $key = substr($key, 5); + if (!isset($copy_server[$key]) || !isset($_SERVER[$key])) { + $key = str_replace(' ', '-', ucwords(strtolower(str_replace('_', ' ', $key)))); + $headers[$key] = $value; + } + } elseif (isset($copy_server[$key])) { + $headers[$copy_server[$key]] = $value; + } + } + + if (!isset($headers['Authorization'])) { + if (isset($_SERVER['REDIRECT_HTTP_AUTHORIZATION'])) { + $headers['Authorization'] = $_SERVER['REDIRECT_HTTP_AUTHORIZATION']; + } elseif (isset($_SERVER['PHP_AUTH_USER'])) { + $basic_pass = isset($_SERVER['PHP_AUTH_PW']) ? $_SERVER['PHP_AUTH_PW'] : ''; + $headers['Authorization'] = 'Basic ' . base64_encode($_SERVER['PHP_AUTH_USER'] . ':' . $basic_pass); + } elseif (isset($_SERVER['PHP_AUTH_DIGEST'])) { + $headers['Authorization'] = $_SERVER['PHP_AUTH_DIGEST']; + } + } + + return $headers; + } + +} diff --git a/3rdparty/sabre/dav/LICENSE b/3rdparty/sabre/dav/LICENSE new file mode 100644 index 00000000..fd3539e3 --- /dev/null +++ b/3rdparty/sabre/dav/LICENSE @@ -0,0 +1,27 @@ +Copyright (C) 2007-2016 fruux GmbH (https://fruux.com/). + +All rights reserved. + +Redistribution and use in source and binary forms, with or without modification, +are permitted provided that the following conditions are met: + + * Redistributions of source code must retain the above copyright notice, + this list of conditions and the following disclaimer. + * Redistributions in binary form must reproduce the above copyright notice, + this list of conditions and the following disclaimer in the documentation + and/or other materials provided with the distribution. + * Neither the name of SabreDAV nor the names of its contributors + may be used to endorse or promote products derived from this software + without specific prior written permission. + + THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" + AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE + IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE + ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE + LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR + CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF + SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS + INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN + CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) + ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE + POSSIBILITY OF SUCH DAMAGE. diff --git a/3rdparty/sabre/dav/lib/CalDAV/Backend/AbstractBackend.php b/3rdparty/sabre/dav/lib/CalDAV/Backend/AbstractBackend.php new file mode 100644 index 00000000..c761bff5 --- /dev/null +++ b/3rdparty/sabre/dav/lib/CalDAV/Backend/AbstractBackend.php @@ -0,0 +1,216 @@ +getCalendarObject($calendarId, $uri); + }, $uris); + } + + /** + * Performs a calendar-query on the contents of this calendar. + * + * The calendar-query is defined in RFC4791 : CalDAV. Using the + * calendar-query it is possible for a client to request a specific set of + * object, based on contents of iCalendar properties, date-ranges and + * iCalendar component types (VTODO, VEVENT). + * + * This method should just return a list of (relative) urls that match this + * query. + * + * The list of filters are specified as an array. The exact array is + * documented by \Sabre\CalDAV\CalendarQueryParser. + * + * Note that it is extremely likely that getCalendarObject for every path + * returned from this method will be called almost immediately after. You + * may want to anticipate this to speed up these requests. + * + * This method provides a default implementation, which parses *all* the + * iCalendar objects in the specified calendar. + * + * This default may well be good enough for personal use, and calendars + * that aren't very large. But if you anticipate high usage, big calendars + * or high loads, you are strongly advised to optimize certain paths. + * + * The best way to do so is override this method and to optimize + * specifically for 'common filters'. + * + * Requests that are extremely common are: + * * requests for just VEVENTS + * * requests for just VTODO + * * requests with a time-range-filter on either VEVENT or VTODO. + * + * ..and combinations of these requests. It may not be worth it to try to + * handle every possible situation and just rely on the (relatively + * easy to use) CalendarQueryValidator to handle the rest. + * + * Note that especially time-range-filters may be difficult to parse. A + * time-range filter specified on a VEVENT must for instance also handle + * recurrence rules correctly. + * A good example of how to interpret all these filters can also simply + * be found in \Sabre\CalDAV\CalendarQueryFilter. This class is as correct + * as possible, so it gives you a good idea on what type of stuff you need + * to think of. + * + * @param mixed $calendarId + * + * @return array + */ + public function calendarQuery($calendarId, array $filters) + { + $result = []; + $objects = $this->getCalendarObjects($calendarId); + + foreach ($objects as $object) { + if ($this->validateFilterForObject($object, $filters)) { + $result[] = $object['uri']; + } + } + + return $result; + } + + /** + * This method validates if a filter (as passed to calendarQuery) matches + * the given object. + * + * @return bool + */ + protected function validateFilterForObject(array $object, array $filters) + { + // Unfortunately, setting the 'calendardata' here is optional. If + // it was excluded, we actually need another call to get this as + // well. + if (!isset($object['calendardata'])) { + $object = $this->getCalendarObject($object['calendarid'], $object['uri']); + } + + $vObject = VObject\Reader::read($object['calendardata']); + + $validator = new CalDAV\CalendarQueryValidator(); + $result = $validator->validate($vObject, $filters); + + // Destroy circular references so PHP will GC the object. + $vObject->destroy(); + + return $result; + } + + /** + * Searches through all of a users calendars and calendar objects to find + * an object with a specific UID. + * + * This method should return the path to this object, relative to the + * calendar home, so this path usually only contains two parts: + * + * calendarpath/objectpath.ics + * + * If the uid is not found, return null. + * + * This method should only consider * objects that the principal owns, so + * any calendars owned by other principals that also appear in this + * collection should be ignored. + * + * @param string $principalUri + * @param string $uid + * + * @return string|null + */ + public function getCalendarObjectByUID($principalUri, $uid) + { + // Note: this is a super slow naive implementation of this method. You + // are highly recommended to optimize it, if your backend allows it. + foreach ($this->getCalendarsForUser($principalUri) as $calendar) { + // We must ignore calendars owned by other principals. + if ($calendar['principaluri'] !== $principalUri) { + continue; + } + + // Ignore calendars that are shared. + if (isset($calendar['{http://sabredav.org/ns}owner-principal']) && $calendar['{http://sabredav.org/ns}owner-principal'] !== $principalUri) { + continue; + } + + $results = $this->calendarQuery( + $calendar['id'], + [ + 'name' => 'VCALENDAR', + 'prop-filters' => [], + 'comp-filters' => [ + [ + 'name' => 'VEVENT', + 'is-not-defined' => false, + 'time-range' => null, + 'comp-filters' => [], + 'prop-filters' => [ + [ + 'name' => 'UID', + 'is-not-defined' => false, + 'time-range' => null, + 'text-match' => [ + 'value' => $uid, + 'negate-condition' => false, + 'collation' => 'i;octet', + ], + 'param-filters' => [], + ], + ], + ], + ], + ] + ); + if ($results) { + // We have a match + return $calendar['uri'].'/'.$results[0]; + } + } + } +} diff --git a/3rdparty/sabre/dav/lib/CalDAV/Backend/BackendInterface.php b/3rdparty/sabre/dav/lib/CalDAV/Backend/BackendInterface.php new file mode 100644 index 00000000..ccaa2519 --- /dev/null +++ b/3rdparty/sabre/dav/lib/CalDAV/Backend/BackendInterface.php @@ -0,0 +1,273 @@ + 'displayname', + '{urn:ietf:params:xml:ns:caldav}calendar-description' => 'description', + '{urn:ietf:params:xml:ns:caldav}calendar-timezone' => 'timezone', + '{http://apple.com/ns/ical/}calendar-order' => 'calendarorder', + '{http://apple.com/ns/ical/}calendar-color' => 'calendarcolor', + ]; + + /** + * List of subscription properties, and how they map to database fieldnames. + * + * @var array + */ + public $subscriptionPropertyMap = [ + '{DAV:}displayname' => 'displayname', + '{http://apple.com/ns/ical/}refreshrate' => 'refreshrate', + '{http://apple.com/ns/ical/}calendar-order' => 'calendarorder', + '{http://apple.com/ns/ical/}calendar-color' => 'calendarcolor', + '{http://calendarserver.org/ns/}subscribed-strip-todos' => 'striptodos', + '{http://calendarserver.org/ns/}subscribed-strip-alarms' => 'stripalarms', + '{http://calendarserver.org/ns/}subscribed-strip-attachments' => 'stripattachments', + ]; + + /** + * Creates the backend. + */ + public function __construct(\PDO $pdo) + { + $this->pdo = $pdo; + } + + /** + * Returns a list of calendars for a principal. + * + * Every project is an array with the following keys: + * * id, a unique id that will be used by other functions to modify the + * calendar. This can be the same as the uri or a database key. + * * uri. This is just the 'base uri' or 'filename' of the calendar. + * * principaluri. The owner of the calendar. Almost always the same as + * principalUri passed to this method. + * + * Furthermore it can contain webdav properties in clark notation. A very + * common one is '{DAV:}displayname'. + * + * Many clients also require: + * {urn:ietf:params:xml:ns:caldav}supported-calendar-component-set + * For this property, you can just return an instance of + * Sabre\CalDAV\Xml\Property\SupportedCalendarComponentSet. + * + * If you return {http://sabredav.org/ns}read-only and set the value to 1, + * ACL will automatically be put in read-only mode. + * + * @param string $principalUri + * + * @return array + */ + public function getCalendarsForUser($principalUri) + { + $fields = array_values($this->propertyMap); + $fields[] = 'calendarid'; + $fields[] = 'uri'; + $fields[] = 'synctoken'; + $fields[] = 'components'; + $fields[] = 'principaluri'; + $fields[] = 'transparent'; + $fields[] = 'access'; + + // Making fields a comma-delimited list + $fields = implode(', ', $fields); + $stmt = $this->pdo->prepare(<<calendarInstancesTableName}.id as id, $fields FROM {$this->calendarInstancesTableName} + LEFT JOIN {$this->calendarTableName} ON + {$this->calendarInstancesTableName}.calendarid = {$this->calendarTableName}.id +WHERE principaluri = ? ORDER BY calendarorder ASC +SQL + ); + $stmt->execute([$principalUri]); + + $calendars = []; + while ($row = $stmt->fetch(\PDO::FETCH_ASSOC)) { + $components = []; + if ($row['components']) { + $components = explode(',', $row['components']); + } + + $calendar = [ + 'id' => [(int) $row['calendarid'], (int) $row['id']], + 'uri' => $row['uri'], + 'principaluri' => $row['principaluri'], + '{'.CalDAV\Plugin::NS_CALENDARSERVER.'}getctag' => 'http://sabre.io/ns/sync/'.($row['synctoken'] ? $row['synctoken'] : '0'), + '{http://sabredav.org/ns}sync-token' => $row['synctoken'] ? $row['synctoken'] : '0', + '{'.CalDAV\Plugin::NS_CALDAV.'}supported-calendar-component-set' => new CalDAV\Xml\Property\SupportedCalendarComponentSet($components), + '{'.CalDAV\Plugin::NS_CALDAV.'}schedule-calendar-transp' => new CalDAV\Xml\Property\ScheduleCalendarTransp($row['transparent'] ? 'transparent' : 'opaque'), + 'share-resource-uri' => '/ns/share/'.$row['calendarid'], + ]; + + $calendar['share-access'] = (int) $row['access']; + // 1 = owner, 2 = readonly, 3 = readwrite + if ($row['access'] > 1) { + // We need to find more information about the original owner. + //$stmt2 = $this->pdo->prepare('SELECT principaluri FROM ' . $this->calendarInstancesTableName . ' WHERE access = 1 AND id = ?'); + //$stmt2->execute([$row['id']]); + + // read-only is for backwards compatibility. Might go away in + // the future. + $calendar['read-only'] = \Sabre\DAV\Sharing\Plugin::ACCESS_READ === (int) $row['access']; + } + + foreach ($this->propertyMap as $xmlName => $dbName) { + $calendar[$xmlName] = $row[$dbName]; + } + + $calendars[] = $calendar; + } + + return $calendars; + } + + /** + * Creates a new calendar for a principal. + * + * If the creation was a success, an id must be returned that can be used + * to reference this calendar in other methods, such as updateCalendar. + * + * @param string $principalUri + * @param string $calendarUri + * + * @return string + */ + public function createCalendar($principalUri, $calendarUri, array $properties) + { + $fieldNames = [ + 'principaluri', + 'uri', + 'transparent', + 'calendarid', + ]; + $values = [ + ':principaluri' => $principalUri, + ':uri' => $calendarUri, + ':transparent' => 0, + ]; + + $sccs = '{urn:ietf:params:xml:ns:caldav}supported-calendar-component-set'; + if (!isset($properties[$sccs])) { + // Default value + $components = 'VEVENT,VTODO'; + } else { + if (!($properties[$sccs] instanceof CalDAV\Xml\Property\SupportedCalendarComponentSet)) { + throw new DAV\Exception('The '.$sccs.' property must be of type: \Sabre\CalDAV\Xml\Property\SupportedCalendarComponentSet'); + } + $components = implode(',', $properties[$sccs]->getValue()); + } + $transp = '{'.CalDAV\Plugin::NS_CALDAV.'}schedule-calendar-transp'; + if (isset($properties[$transp])) { + $values[':transparent'] = 'transparent' === $properties[$transp]->getValue() ? 1 : 0; + } + $stmt = $this->pdo->prepare('INSERT INTO '.$this->calendarTableName.' (synctoken, components) VALUES (1, ?)'); + $stmt->execute([$components]); + + $calendarId = $this->pdo->lastInsertId( + $this->calendarTableName.'_id_seq' + ); + + $values[':calendarid'] = $calendarId; + + foreach ($this->propertyMap as $xmlName => $dbName) { + if (isset($properties[$xmlName])) { + $values[':'.$dbName] = $properties[$xmlName]; + $fieldNames[] = $dbName; + } + } + + $stmt = $this->pdo->prepare('INSERT INTO '.$this->calendarInstancesTableName.' ('.implode(', ', $fieldNames).') VALUES ('.implode(', ', array_keys($values)).')'); + + $stmt->execute($values); + + return [ + $calendarId, + $this->pdo->lastInsertId($this->calendarInstancesTableName.'_id_seq'), + ]; + } + + /** + * Updates properties for a calendar. + * + * The list of mutations is stored in a Sabre\DAV\PropPatch object. + * To do the actual updates, you must tell this object which properties + * you're going to process with the handle() method. + * + * Calling the handle method is like telling the PropPatch object "I + * promise I can handle updating this property". + * + * Read the PropPatch documentation for more info and examples. + * + * @param mixed $calendarId + */ + public function updateCalendar($calendarId, PropPatch $propPatch) + { + if (!is_array($calendarId)) { + throw new \InvalidArgumentException('The value passed to $calendarId is expected to be an array with a calendarId and an instanceId'); + } + list($calendarId, $instanceId) = $calendarId; + + $supportedProperties = array_keys($this->propertyMap); + $supportedProperties[] = '{'.CalDAV\Plugin::NS_CALDAV.'}schedule-calendar-transp'; + + $propPatch->handle($supportedProperties, function ($mutations) use ($calendarId, $instanceId) { + $newValues = []; + foreach ($mutations as $propertyName => $propertyValue) { + switch ($propertyName) { + case '{'.CalDAV\Plugin::NS_CALDAV.'}schedule-calendar-transp': + $fieldName = 'transparent'; + $newValues[$fieldName] = 'transparent' === $propertyValue->getValue(); + break; + default: + $fieldName = $this->propertyMap[$propertyName]; + $newValues[$fieldName] = $propertyValue; + break; + } + } + $valuesSql = []; + foreach ($newValues as $fieldName => $value) { + $valuesSql[] = $fieldName.' = ?'; + } + + $stmt = $this->pdo->prepare('UPDATE '.$this->calendarInstancesTableName.' SET '.implode(', ', $valuesSql).' WHERE id = ?'); + $newValues['id'] = $instanceId; + $stmt->execute(array_values($newValues)); + + $this->addChange($calendarId, '', 2); + + return true; + }); + } + + /** + * Delete a calendar and all it's objects. + * + * @param mixed $calendarId + */ + public function deleteCalendar($calendarId) + { + if (!is_array($calendarId)) { + throw new \InvalidArgumentException('The value passed to $calendarId is expected to be an array with a calendarId and an instanceId'); + } + list($calendarId, $instanceId) = $calendarId; + + $stmt = $this->pdo->prepare('SELECT access FROM '.$this->calendarInstancesTableName.' where id = ?'); + $stmt->execute([$instanceId]); + $access = (int) $stmt->fetchColumn(); + + if (\Sabre\DAV\Sharing\Plugin::ACCESS_SHAREDOWNER === $access) { + /** + * If the user is the owner of the calendar, we delete all data and all + * instances. + **/ + $stmt = $this->pdo->prepare('DELETE FROM '.$this->calendarObjectTableName.' WHERE calendarid = ?'); + $stmt->execute([$calendarId]); + + $stmt = $this->pdo->prepare('DELETE FROM '.$this->calendarChangesTableName.' WHERE calendarid = ?'); + $stmt->execute([$calendarId]); + + $stmt = $this->pdo->prepare('DELETE FROM '.$this->calendarInstancesTableName.' WHERE calendarid = ?'); + $stmt->execute([$calendarId]); + + $stmt = $this->pdo->prepare('DELETE FROM '.$this->calendarTableName.' WHERE id = ?'); + $stmt->execute([$calendarId]); + } else { + /** + * If it was an instance of a shared calendar, we only delete that + * instance. + */ + $stmt = $this->pdo->prepare('DELETE FROM '.$this->calendarInstancesTableName.' WHERE id = ?'); + $stmt->execute([$instanceId]); + } + } + + /** + * Returns all calendar objects within a calendar. + * + * Every item contains an array with the following keys: + * * calendardata - The iCalendar-compatible calendar data + * * uri - a unique key which will be used to construct the uri. This can + * be any arbitrary string, but making sure it ends with '.ics' is a + * good idea. This is only the basename, or filename, not the full + * path. + * * lastmodified - a timestamp of the last modification time + * * etag - An arbitrary string, surrounded by double-quotes. (e.g.: + * ' "abcdef"') + * * size - The size of the calendar objects, in bytes. + * * component - optional, a string containing the type of object, such + * as 'vevent' or 'vtodo'. If specified, this will be used to populate + * the Content-Type header. + * + * Note that the etag is optional, but it's highly encouraged to return for + * speed reasons. + * + * The calendardata is also optional. If it's not returned + * 'getCalendarObject' will be called later, which *is* expected to return + * calendardata. + * + * If neither etag or size are specified, the calendardata will be + * used/fetched to determine these numbers. If both are specified the + * amount of times this is needed is reduced by a great degree. + * + * @param mixed $calendarId + * + * @return array + */ + public function getCalendarObjects($calendarId) + { + if (!is_array($calendarId)) { + throw new \InvalidArgumentException('The value passed to $calendarId is expected to be an array with a calendarId and an instanceId'); + } + list($calendarId, $instanceId) = $calendarId; + + $stmt = $this->pdo->prepare('SELECT id, uri, lastmodified, etag, calendarid, size, componenttype FROM '.$this->calendarObjectTableName.' WHERE calendarid = ?'); + $stmt->execute([$calendarId]); + + $result = []; + foreach ($stmt->fetchAll(\PDO::FETCH_ASSOC) as $row) { + $result[] = [ + 'id' => $row['id'], + 'uri' => $row['uri'], + 'lastmodified' => (int) $row['lastmodified'], + 'etag' => '"'.$row['etag'].'"', + 'size' => (int) $row['size'], + 'component' => strtolower($row['componenttype']), + ]; + } + + return $result; + } + + /** + * Returns information from a single calendar object, based on it's object + * uri. + * + * The object uri is only the basename, or filename and not a full path. + * + * The returned array must have the same keys as getCalendarObjects. The + * 'calendardata' object is required here though, while it's not required + * for getCalendarObjects. + * + * This method must return null if the object did not exist. + * + * @param mixed $calendarId + * @param string $objectUri + * + * @return array|null + */ + public function getCalendarObject($calendarId, $objectUri) + { + if (!is_array($calendarId)) { + throw new \InvalidArgumentException('The value passed to $calendarId is expected to be an array with a calendarId and an instanceId'); + } + list($calendarId, $instanceId) = $calendarId; + + $stmt = $this->pdo->prepare('SELECT id, uri, lastmodified, etag, calendarid, size, calendardata, componenttype FROM '.$this->calendarObjectTableName.' WHERE calendarid = ? AND uri = ?'); + $stmt->execute([$calendarId, $objectUri]); + $row = $stmt->fetch(\PDO::FETCH_ASSOC); + + if (!$row) { + return null; + } + + return [ + 'id' => $row['id'], + 'uri' => $row['uri'], + 'lastmodified' => (int) $row['lastmodified'], + 'etag' => '"'.$row['etag'].'"', + 'size' => (int) $row['size'], + 'calendardata' => $row['calendardata'], + 'component' => strtolower($row['componenttype']), + ]; + } + + /** + * Returns a list of calendar objects. + * + * This method should work identical to getCalendarObject, but instead + * return all the calendar objects in the list as an array. + * + * If the backend supports this, it may allow for some speed-ups. + * + * @param mixed $calendarId + * + * @return array + */ + public function getMultipleCalendarObjects($calendarId, array $uris) + { + if (!is_array($calendarId)) { + throw new \InvalidArgumentException('The value passed to $calendarId is expected to be an array with a calendarId and an instanceId'); + } + list($calendarId, $instanceId) = $calendarId; + + $result = []; + foreach (array_chunk($uris, 900) as $chunk) { + $query = 'SELECT id, uri, lastmodified, etag, calendarid, size, calendardata, componenttype FROM '.$this->calendarObjectTableName.' WHERE calendarid = ? AND uri IN ('; + // Inserting a whole bunch of question marks + $query .= implode(',', array_fill(0, count($chunk), '?')); + $query .= ')'; + + $stmt = $this->pdo->prepare($query); + $stmt->execute(array_merge([$calendarId], $chunk)); + + while ($row = $stmt->fetch(\PDO::FETCH_ASSOC)) { + $result[] = [ + 'id' => $row['id'], + 'uri' => $row['uri'], + 'lastmodified' => (int) $row['lastmodified'], + 'etag' => '"'.$row['etag'].'"', + 'size' => (int) $row['size'], + 'calendardata' => $row['calendardata'], + 'component' => strtolower($row['componenttype']), + ]; + } + } + + return $result; + } + + /** + * Creates a new calendar object. + * + * The object uri is only the basename, or filename and not a full path. + * + * It is possible return an etag from this function, which will be used in + * the response to this PUT request. Note that the ETag must be surrounded + * by double-quotes. + * + * However, you should only really return this ETag if you don't mangle the + * calendar-data. If the result of a subsequent GET to this object is not + * the exact same as this request body, you should omit the ETag. + * + * @param mixed $calendarId + * @param string $objectUri + * @param string $calendarData + * + * @return string|null + */ + public function createCalendarObject($calendarId, $objectUri, $calendarData) + { + if (!is_array($calendarId)) { + throw new \InvalidArgumentException('The value passed to $calendarId is expected to be an array with a calendarId and an instanceId'); + } + list($calendarId, $instanceId) = $calendarId; + + $extraData = $this->getDenormalizedData($calendarData); + + $stmt = $this->pdo->prepare('INSERT INTO '.$this->calendarObjectTableName.' (calendarid, uri, calendardata, lastmodified, etag, size, componenttype, firstoccurence, lastoccurence, uid) VALUES (?,?,?,?,?,?,?,?,?,?)'); + $stmt->execute([ + $calendarId, + $objectUri, + $calendarData, + time(), + $extraData['etag'], + $extraData['size'], + $extraData['componentType'], + $extraData['firstOccurence'], + $extraData['lastOccurence'], + $extraData['uid'], + ]); + $this->addChange($calendarId, $objectUri, 1); + + return '"'.$extraData['etag'].'"'; + } + + /** + * Updates an existing calendarobject, based on it's uri. + * + * The object uri is only the basename, or filename and not a full path. + * + * It is possible return an etag from this function, which will be used in + * the response to this PUT request. Note that the ETag must be surrounded + * by double-quotes. + * + * However, you should only really return this ETag if you don't mangle the + * calendar-data. If the result of a subsequent GET to this object is not + * the exact same as this request body, you should omit the ETag. + * + * @param mixed $calendarId + * @param string $objectUri + * @param string $calendarData + * + * @return string|null + */ + public function updateCalendarObject($calendarId, $objectUri, $calendarData) + { + if (!is_array($calendarId)) { + throw new \InvalidArgumentException('The value passed to $calendarId is expected to be an array with a calendarId and an instanceId'); + } + list($calendarId, $instanceId) = $calendarId; + + $extraData = $this->getDenormalizedData($calendarData); + + $stmt = $this->pdo->prepare('UPDATE '.$this->calendarObjectTableName.' SET calendardata = ?, lastmodified = ?, etag = ?, size = ?, componenttype = ?, firstoccurence = ?, lastoccurence = ?, uid = ? WHERE calendarid = ? AND uri = ?'); + $stmt->execute([$calendarData, time(), $extraData['etag'], $extraData['size'], $extraData['componentType'], $extraData['firstOccurence'], $extraData['lastOccurence'], $extraData['uid'], $calendarId, $objectUri]); + + $this->addChange($calendarId, $objectUri, 2); + + return '"'.$extraData['etag'].'"'; + } + + /** + * Parses some information from calendar objects, used for optimized + * calendar-queries. + * + * Returns an array with the following keys: + * * etag - An md5 checksum of the object without the quotes. + * * size - Size of the object in bytes + * * componentType - VEVENT, VTODO or VJOURNAL + * * firstOccurence + * * lastOccurence + * * uid - value of the UID property + * + * @param string $calendarData + * + * @return array + */ + protected function getDenormalizedData($calendarData) + { + $vObject = VObject\Reader::read($calendarData); + $componentType = null; + $component = null; + $firstOccurence = null; + $lastOccurence = null; + $uid = null; + foreach ($vObject->getComponents() as $component) { + if ('VTIMEZONE' !== $component->name) { + $componentType = $component->name; + $uid = (string) $component->UID; + break; + } + } + if (!$componentType) { + throw new \Sabre\DAV\Exception\BadRequest('Calendar objects must have a VJOURNAL, VEVENT or VTODO component'); + } + if ('VEVENT' === $componentType) { + $firstOccurence = $component->DTSTART->getDateTime()->getTimeStamp(); + // Finding the last occurence is a bit harder + if (!isset($component->RRULE)) { + if (isset($component->DTEND)) { + $lastOccurence = $component->DTEND->getDateTime()->getTimeStamp(); + } elseif (isset($component->DURATION)) { + $endDate = clone $component->DTSTART->getDateTime(); + $endDate = $endDate->add(VObject\DateTimeParser::parse($component->DURATION->getValue())); + $lastOccurence = $endDate->getTimeStamp(); + } elseif (!$component->DTSTART->hasTime()) { + $endDate = clone $component->DTSTART->getDateTime(); + $endDate = $endDate->modify('+1 day'); + $lastOccurence = $endDate->getTimeStamp(); + } else { + $lastOccurence = $firstOccurence; + } + } else { + $it = new VObject\Recur\EventIterator($vObject, (string) $component->UID); + $maxDate = new \DateTime(self::MAX_DATE); + if ($it->isInfinite()) { + $lastOccurence = $maxDate->getTimeStamp(); + } else { + $end = $it->getDtEnd(); + while ($it->valid() && $end < $maxDate) { + $end = $it->getDtEnd(); + $it->next(); + } + $lastOccurence = $end->getTimeStamp(); + } + } + + // Ensure Occurence values are positive + if ($firstOccurence < 0) { + $firstOccurence = 0; + } + if ($lastOccurence < 0) { + $lastOccurence = 0; + } + } + + // Destroy circular references to PHP will GC the object. + $vObject->destroy(); + + return [ + 'etag' => md5($calendarData), + 'size' => strlen($calendarData), + 'componentType' => $componentType, + 'firstOccurence' => $firstOccurence, + 'lastOccurence' => $lastOccurence, + 'uid' => $uid, + ]; + } + + /** + * Deletes an existing calendar object. + * + * The object uri is only the basename, or filename and not a full path. + * + * @param mixed $calendarId + * @param string $objectUri + */ + public function deleteCalendarObject($calendarId, $objectUri) + { + if (!is_array($calendarId)) { + throw new \InvalidArgumentException('The value passed to $calendarId is expected to be an array with a calendarId and an instanceId'); + } + list($calendarId, $instanceId) = $calendarId; + + $stmt = $this->pdo->prepare('DELETE FROM '.$this->calendarObjectTableName.' WHERE calendarid = ? AND uri = ?'); + $stmt->execute([$calendarId, $objectUri]); + + $this->addChange($calendarId, $objectUri, 3); + } + + /** + * Performs a calendar-query on the contents of this calendar. + * + * The calendar-query is defined in RFC4791 : CalDAV. Using the + * calendar-query it is possible for a client to request a specific set of + * object, based on contents of iCalendar properties, date-ranges and + * iCalendar component types (VTODO, VEVENT). + * + * This method should just return a list of (relative) urls that match this + * query. + * + * The list of filters are specified as an array. The exact array is + * documented by \Sabre\CalDAV\CalendarQueryParser. + * + * Note that it is extremely likely that getCalendarObject for every path + * returned from this method will be called almost immediately after. You + * may want to anticipate this to speed up these requests. + * + * This method provides a default implementation, which parses *all* the + * iCalendar objects in the specified calendar. + * + * This default may well be good enough for personal use, and calendars + * that aren't very large. But if you anticipate high usage, big calendars + * or high loads, you are strongly advised to optimize certain paths. + * + * The best way to do so is override this method and to optimize + * specifically for 'common filters'. + * + * Requests that are extremely common are: + * * requests for just VEVENTS + * * requests for just VTODO + * * requests with a time-range-filter on a VEVENT. + * + * ..and combinations of these requests. It may not be worth it to try to + * handle every possible situation and just rely on the (relatively + * easy to use) CalendarQueryValidator to handle the rest. + * + * Note that especially time-range-filters may be difficult to parse. A + * time-range filter specified on a VEVENT must for instance also handle + * recurrence rules correctly. + * A good example of how to interpret all these filters can also simply + * be found in \Sabre\CalDAV\CalendarQueryFilter. This class is as correct + * as possible, so it gives you a good idea on what type of stuff you need + * to think of. + * + * This specific implementation (for the PDO) backend optimizes filters on + * specific components, and VEVENT time-ranges. + * + * @param mixed $calendarId + * + * @return array + */ + public function calendarQuery($calendarId, array $filters) + { + if (!is_array($calendarId)) { + throw new \InvalidArgumentException('The value passed to $calendarId is expected to be an array with a calendarId and an instanceId'); + } + list($calendarId, $instanceId) = $calendarId; + + $componentType = null; + $requirePostFilter = true; + $timeRange = null; + + // if no filters were specified, we don't need to filter after a query + if (!$filters['prop-filters'] && !$filters['comp-filters']) { + $requirePostFilter = false; + } + + // Figuring out if there's a component filter + if (count($filters['comp-filters']) > 0 && !$filters['comp-filters'][0]['is-not-defined']) { + $componentType = $filters['comp-filters'][0]['name']; + + // Checking if we need post-filters + $has_time_range = array_key_exists('time-range', $filters['comp-filters'][0]) && $filters['comp-filters'][0]['time-range']; + if (!$filters['prop-filters'] && !$filters['comp-filters'][0]['comp-filters'] && !$has_time_range && !$filters['comp-filters'][0]['prop-filters']) { + $requirePostFilter = false; + } + // There was a time-range filter + if ('VEVENT' == $componentType && $has_time_range) { + $timeRange = $filters['comp-filters'][0]['time-range']; + + // If start time OR the end time is not specified, we can do a + // 100% accurate mysql query. + if (!$filters['prop-filters'] && !$filters['comp-filters'][0]['comp-filters'] && !$filters['comp-filters'][0]['prop-filters'] && $timeRange) { + if ((array_key_exists('start', $timeRange) && !$timeRange['start']) || (array_key_exists('end', $timeRange) && !$timeRange['end'])) { + $requirePostFilter = false; + } + } + } + } + + if ($requirePostFilter) { + $query = 'SELECT uri, calendardata FROM '.$this->calendarObjectTableName.' WHERE calendarid = :calendarid'; + } else { + $query = 'SELECT uri FROM '.$this->calendarObjectTableName.' WHERE calendarid = :calendarid'; + } + + $values = [ + 'calendarid' => $calendarId, + ]; + + if ($componentType) { + $query .= ' AND componenttype = :componenttype'; + $values['componenttype'] = $componentType; + } + + if ($timeRange && array_key_exists('start', $timeRange) && $timeRange['start']) { + $query .= ' AND lastoccurence > :startdate'; + $values['startdate'] = $timeRange['start']->getTimeStamp(); + } + if ($timeRange && array_key_exists('end', $timeRange) && $timeRange['end']) { + $query .= ' AND firstoccurence < :enddate'; + $values['enddate'] = $timeRange['end']->getTimeStamp(); + } + + $stmt = $this->pdo->prepare($query); + $stmt->execute($values); + + $result = []; + while ($row = $stmt->fetch(\PDO::FETCH_ASSOC)) { + if ($requirePostFilter) { + if (!$this->validateFilterForObject($row, $filters)) { + continue; + } + } + $result[] = $row['uri']; + } + + return $result; + } + + /** + * Searches through all of a users calendars and calendar objects to find + * an object with a specific UID. + * + * This method should return the path to this object, relative to the + * calendar home, so this path usually only contains two parts: + * + * calendarpath/objectpath.ics + * + * If the uid is not found, return null. + * + * This method should only consider * objects that the principal owns, so + * any calendars owned by other principals that also appear in this + * collection should be ignored. + * + * @param string $principalUri + * @param string $uid + * + * @return string|null + */ + public function getCalendarObjectByUID($principalUri, $uid) + { + $query = <<calendarObjectTableName AS calendarobjects +LEFT JOIN + $this->calendarInstancesTableName AS calendar_instances + ON calendarobjects.calendarid = calendar_instances.calendarid +WHERE + calendar_instances.principaluri = ? + AND + calendarobjects.uid = ? + AND + calendar_instances.access = 1 +SQL; + + $stmt = $this->pdo->prepare($query); + $stmt->execute([$principalUri, $uid]); + + if ($row = $stmt->fetch(\PDO::FETCH_ASSOC)) { + return $row['calendaruri'].'/'.$row['objecturi']; + } + } + + /** + * The getChanges method returns all the changes that have happened, since + * the specified syncToken in the specified calendar. + * + * This function should return an array, such as the following: + * + * [ + * 'syncToken' => 'The current synctoken', + * 'added' => [ + * 'new.txt', + * ], + * 'modified' => [ + * 'modified.txt', + * ], + * 'deleted' => [ + * 'foo.php.bak', + * 'old.txt' + * ] + * ]; + * + * The returned syncToken property should reflect the *current* syncToken + * of the calendar, as reported in the {http://sabredav.org/ns}sync-token + * property this is needed here too, to ensure the operation is atomic. + * + * If the $syncToken argument is specified as null, this is an initial + * sync, and all members should be reported. + * + * The modified property is an array of nodenames that have changed since + * the last token. + * + * The deleted property is an array with nodenames, that have been deleted + * from collection. + * + * The $syncLevel argument is basically the 'depth' of the report. If it's + * 1, you only have to report changes that happened only directly in + * immediate descendants. If it's 2, it should also include changes from + * the nodes below the child collections. (grandchildren) + * + * The $limit argument allows a client to specify how many results should + * be returned at most. If the limit is not specified, it should be treated + * as infinite. + * + * If the limit (infinite or not) is higher than you're willing to return, + * you should throw a Sabre\DAV\Exception\TooMuchMatches() exception. + * + * If the syncToken is expired (due to data cleanup) or unknown, you must + * return null. + * + * The limit is 'suggestive'. You are free to ignore it. + * + * @param mixed $calendarId + * @param string $syncToken + * @param int $syncLevel + * @param int $limit + * + * @return array|null + */ + public function getChangesForCalendar($calendarId, $syncToken, $syncLevel, $limit = null) + { + if (!is_array($calendarId)) { + throw new \InvalidArgumentException('The value passed to $calendarId is expected to be an array with a calendarId and an instanceId'); + } + list($calendarId, $instanceId) = $calendarId; + + $result = [ + 'added' => [], + 'modified' => [], + 'deleted' => [], + ]; + + if ($syncToken) { + $query = 'SELECT uri, operation, synctoken FROM '.$this->calendarChangesTableName.' WHERE synctoken >= ? AND calendarid = ? ORDER BY synctoken'; + if ($limit > 0) { + // Fetch one more raw to detect result truncation + $query .= ' LIMIT '.((int) $limit + 1); + } + + // Fetching all changes + $stmt = $this->pdo->prepare($query); + $stmt->execute([$syncToken, $calendarId]); + + $changes = []; + + // This loop ensures that any duplicates are overwritten, only the + // last change on a node is relevant. + while ($row = $stmt->fetch(\PDO::FETCH_ASSOC)) { + $changes[$row['uri']] = $row; + } + $currentToken = null; + + $result_count = 0; + foreach ($changes as $uri => $operation) { + if (!is_null($limit) && $result_count >= $limit) { + $result['result_truncated'] = true; + break; + } + + if (null === $currentToken || $currentToken < $operation['synctoken'] + 1) { + // SyncToken in CalDAV perspective is consistently the next number of the last synced change event in this class. + $currentToken = $operation['synctoken'] + 1; + } + + ++$result_count; + switch ($operation['operation']) { + case 1: + $result['added'][] = $uri; + break; + case 2: + $result['modified'][] = $uri; + break; + case 3: + $result['deleted'][] = $uri; + break; + } + } + + if (!is_null($currentToken)) { + $result['syncToken'] = $currentToken; + } else { + // This means returned value is equivalent to syncToken + $result['syncToken'] = $syncToken; + } + } else { + // Current synctoken + $stmt = $this->pdo->prepare('SELECT synctoken FROM '.$this->calendarTableName.' WHERE id = ?'); + $stmt->execute([$calendarId]); + $currentToken = $stmt->fetchColumn(0); + + if (is_null($currentToken)) { + return null; + } + $result['syncToken'] = $currentToken; + + // No synctoken supplied, this is the initial sync. + $query = 'SELECT uri FROM '.$this->calendarObjectTableName.' WHERE calendarid = ?'; + $stmt = $this->pdo->prepare($query); + $stmt->execute([$calendarId]); + + $result['added'] = $stmt->fetchAll(\PDO::FETCH_COLUMN); + } + + return $result; + } + + /** + * Adds a change record to the calendarchanges table. + * + * @param mixed $calendarId + * @param string $objectUri + * @param int $operation 1 = add, 2 = modify, 3 = delete + */ + protected function addChange($calendarId, $objectUri, $operation) + { + $stmt = $this->pdo->prepare('INSERT INTO '.$this->calendarChangesTableName.' (uri, synctoken, calendarid, operation) SELECT ?, synctoken, ?, ? FROM '.$this->calendarTableName.' WHERE id = ?'); + $stmt->execute([ + $objectUri, + $calendarId, + $operation, + $calendarId, + ]); + $stmt = $this->pdo->prepare('UPDATE '.$this->calendarTableName.' SET synctoken = synctoken + 1 WHERE id = ?'); + $stmt->execute([ + $calendarId, + ]); + } + + /** + * Returns a list of subscriptions for a principal. + * + * Every subscription is an array with the following keys: + * * id, a unique id that will be used by other functions to modify the + * subscription. This can be the same as the uri or a database key. + * * uri. This is just the 'base uri' or 'filename' of the subscription. + * * principaluri. The owner of the subscription. Almost always the same as + * principalUri passed to this method. + * * source. Url to the actual feed + * + * Furthermore, all the subscription info must be returned too: + * + * 1. {DAV:}displayname + * 2. {http://apple.com/ns/ical/}refreshrate + * 3. {http://calendarserver.org/ns/}subscribed-strip-todos (omit if todos + * should not be stripped). + * 4. {http://calendarserver.org/ns/}subscribed-strip-alarms (omit if alarms + * should not be stripped). + * 5. {http://calendarserver.org/ns/}subscribed-strip-attachments (omit if + * attachments should not be stripped). + * 7. {http://apple.com/ns/ical/}calendar-color + * 8. {http://apple.com/ns/ical/}calendar-order + * 9. {urn:ietf:params:xml:ns:caldav}supported-calendar-component-set + * (should just be an instance of + * Sabre\CalDAV\Property\SupportedCalendarComponentSet, with a bunch of + * default components). + * + * @param string $principalUri + * + * @return array + */ + public function getSubscriptionsForUser($principalUri) + { + $fields = array_values($this->subscriptionPropertyMap); + $fields[] = 'id'; + $fields[] = 'uri'; + $fields[] = 'source'; + $fields[] = 'principaluri'; + $fields[] = 'lastmodified'; + + // Making fields a comma-delimited list + $fields = implode(', ', $fields); + $stmt = $this->pdo->prepare('SELECT '.$fields.' FROM '.$this->calendarSubscriptionsTableName.' WHERE principaluri = ? ORDER BY calendarorder ASC'); + $stmt->execute([$principalUri]); + + $subscriptions = []; + while ($row = $stmt->fetch(\PDO::FETCH_ASSOC)) { + $subscription = [ + 'id' => $row['id'], + 'uri' => $row['uri'], + 'principaluri' => $row['principaluri'], + 'source' => $row['source'], + 'lastmodified' => $row['lastmodified'], + + '{'.CalDAV\Plugin::NS_CALDAV.'}supported-calendar-component-set' => new CalDAV\Xml\Property\SupportedCalendarComponentSet(['VTODO', 'VEVENT']), + ]; + + foreach ($this->subscriptionPropertyMap as $xmlName => $dbName) { + if (!is_null($row[$dbName])) { + $subscription[$xmlName] = $row[$dbName]; + } + } + + $subscriptions[] = $subscription; + } + + return $subscriptions; + } + + /** + * Creates a new subscription for a principal. + * + * If the creation was a success, an id must be returned that can be used to reference + * this subscription in other methods, such as updateSubscription. + * + * @param string $principalUri + * @param string $uri + * + * @return mixed + */ + public function createSubscription($principalUri, $uri, array $properties) + { + $fieldNames = [ + 'principaluri', + 'uri', + 'source', + 'lastmodified', + ]; + + if (!isset($properties['{http://calendarserver.org/ns/}source'])) { + throw new Forbidden('The {http://calendarserver.org/ns/}source property is required when creating subscriptions'); + } + + $values = [ + ':principaluri' => $principalUri, + ':uri' => $uri, + ':source' => $properties['{http://calendarserver.org/ns/}source']->getHref(), + ':lastmodified' => time(), + ]; + + foreach ($this->subscriptionPropertyMap as $xmlName => $dbName) { + if (isset($properties[$xmlName])) { + $values[':'.$dbName] = $properties[$xmlName]; + $fieldNames[] = $dbName; + } + } + + $stmt = $this->pdo->prepare('INSERT INTO '.$this->calendarSubscriptionsTableName.' ('.implode(', ', $fieldNames).') VALUES ('.implode(', ', array_keys($values)).')'); + $stmt->execute($values); + + return $this->pdo->lastInsertId( + $this->calendarSubscriptionsTableName.'_id_seq' + ); + } + + /** + * Updates a subscription. + * + * The list of mutations is stored in a Sabre\DAV\PropPatch object. + * To do the actual updates, you must tell this object which properties + * you're going to process with the handle() method. + * + * Calling the handle method is like telling the PropPatch object "I + * promise I can handle updating this property". + * + * Read the PropPatch documentation for more info and examples. + * + * @param mixed $subscriptionId + */ + public function updateSubscription($subscriptionId, PropPatch $propPatch) + { + $supportedProperties = array_keys($this->subscriptionPropertyMap); + $supportedProperties[] = '{http://calendarserver.org/ns/}source'; + + $propPatch->handle($supportedProperties, function ($mutations) use ($subscriptionId) { + $newValues = []; + + foreach ($mutations as $propertyName => $propertyValue) { + if ('{http://calendarserver.org/ns/}source' === $propertyName) { + $newValues['source'] = $propertyValue->getHref(); + } else { + $fieldName = $this->subscriptionPropertyMap[$propertyName]; + $newValues[$fieldName] = $propertyValue; + } + } + + // Now we're generating the sql query. + $valuesSql = []; + foreach ($newValues as $fieldName => $value) { + $valuesSql[] = $fieldName.' = ?'; + } + + $stmt = $this->pdo->prepare('UPDATE '.$this->calendarSubscriptionsTableName.' SET '.implode(', ', $valuesSql).', lastmodified = ? WHERE id = ?'); + $newValues['lastmodified'] = time(); + $newValues['id'] = $subscriptionId; + $stmt->execute(array_values($newValues)); + + return true; + }); + } + + /** + * Deletes a subscription. + * + * @param mixed $subscriptionId + */ + public function deleteSubscription($subscriptionId) + { + $stmt = $this->pdo->prepare('DELETE FROM '.$this->calendarSubscriptionsTableName.' WHERE id = ?'); + $stmt->execute([$subscriptionId]); + } + + /** + * Returns a single scheduling object. + * + * The returned array should contain the following elements: + * * uri - A unique basename for the object. This will be used to + * construct a full uri. + * * calendardata - The iCalendar object + * * lastmodified - The last modification date. Can be an int for a unix + * timestamp, or a PHP DateTime object. + * * etag - A unique token that must change if the object changed. + * * size - The size of the object, in bytes. + * + * @param string $principalUri + * @param string $objectUri + * + * @return array + */ + public function getSchedulingObject($principalUri, $objectUri) + { + $stmt = $this->pdo->prepare('SELECT uri, calendardata, lastmodified, etag, size FROM '.$this->schedulingObjectTableName.' WHERE principaluri = ? AND uri = ?'); + $stmt->execute([$principalUri, $objectUri]); + $row = $stmt->fetch(\PDO::FETCH_ASSOC); + + if (!$row) { + return null; + } + + return [ + 'uri' => $row['uri'], + 'calendardata' => $row['calendardata'], + 'lastmodified' => $row['lastmodified'], + 'etag' => '"'.$row['etag'].'"', + 'size' => (int) $row['size'], + ]; + } + + /** + * Returns all scheduling objects for the inbox collection. + * + * These objects should be returned as an array. Every item in the array + * should follow the same structure as returned from getSchedulingObject. + * + * The main difference is that 'calendardata' is optional. + * + * @param string $principalUri + * + * @return array + */ + public function getSchedulingObjects($principalUri) + { + $stmt = $this->pdo->prepare('SELECT id, calendardata, uri, lastmodified, etag, size FROM '.$this->schedulingObjectTableName.' WHERE principaluri = ?'); + $stmt->execute([$principalUri]); + + $result = []; + foreach ($stmt->fetchAll(\PDO::FETCH_ASSOC) as $row) { + $result[] = [ + 'calendardata' => $row['calendardata'], + 'uri' => $row['uri'], + 'lastmodified' => $row['lastmodified'], + 'etag' => '"'.$row['etag'].'"', + 'size' => (int) $row['size'], + ]; + } + + return $result; + } + + /** + * Deletes a scheduling object. + * + * @param string $principalUri + * @param string $objectUri + */ + public function deleteSchedulingObject($principalUri, $objectUri) + { + $stmt = $this->pdo->prepare('DELETE FROM '.$this->schedulingObjectTableName.' WHERE principaluri = ? AND uri = ?'); + $stmt->execute([$principalUri, $objectUri]); + } + + /** + * Creates a new scheduling object. This should land in a users' inbox. + * + * @param string $principalUri + * @param string $objectUri + * @param string|resource $objectData + */ + public function createSchedulingObject($principalUri, $objectUri, $objectData) + { + $stmt = $this->pdo->prepare('INSERT INTO '.$this->schedulingObjectTableName.' (principaluri, calendardata, uri, lastmodified, etag, size) VALUES (?, ?, ?, ?, ?, ?)'); + + if (is_resource($objectData)) { + $objectData = stream_get_contents($objectData); + } + + $stmt->execute([$principalUri, $objectData, $objectUri, time(), md5($objectData), strlen($objectData)]); + } + + /** + * Updates the list of shares. + * + * @param mixed $calendarId + * @param \Sabre\DAV\Xml\Element\Sharee[] $sharees + */ + public function updateInvites($calendarId, array $sharees) + { + if (!is_array($calendarId)) { + throw new \InvalidArgumentException('The value passed to $calendarId is expected to be an array with a calendarId and an instanceId'); + } + $currentInvites = $this->getInvites($calendarId); + list($calendarId, $instanceId) = $calendarId; + + $removeStmt = $this->pdo->prepare('DELETE FROM '.$this->calendarInstancesTableName.' WHERE calendarid = ? AND share_href = ? AND access IN (2,3)'); + $updateStmt = $this->pdo->prepare('UPDATE '.$this->calendarInstancesTableName.' SET access = ?, share_displayname = ?, share_invitestatus = ? WHERE calendarid = ? AND share_href = ?'); + + $insertStmt = $this->pdo->prepare(' +INSERT INTO '.$this->calendarInstancesTableName.' + ( + calendarid, + principaluri, + access, + displayname, + uri, + description, + calendarorder, + calendarcolor, + timezone, + transparent, + share_href, + share_displayname, + share_invitestatus + ) + SELECT + ?, + ?, + ?, + displayname, + ?, + description, + calendarorder, + calendarcolor, + timezone, + 1, + ?, + ?, + ? + FROM '.$this->calendarInstancesTableName.' WHERE id = ?'); + + foreach ($sharees as $sharee) { + if (\Sabre\DAV\Sharing\Plugin::ACCESS_NOACCESS === $sharee->access) { + // if access was set no NOACCESS, it means access for an + // existing sharee was removed. + $removeStmt->execute([$calendarId, $sharee->href]); + continue; + } + + if (is_null($sharee->principal)) { + // If the server could not determine the principal automatically, + // we will mark the invite status as invalid. + $sharee->inviteStatus = \Sabre\DAV\Sharing\Plugin::INVITE_INVALID; + } else { + // Because sabre/dav does not yet have an invitation system, + // every invite is automatically accepted for now. + $sharee->inviteStatus = \Sabre\DAV\Sharing\Plugin::INVITE_ACCEPTED; + } + + foreach ($currentInvites as $oldSharee) { + if ($oldSharee->href === $sharee->href) { + // This is an update + $sharee->properties = array_merge( + $oldSharee->properties, + $sharee->properties + ); + $updateStmt->execute([ + $sharee->access, + isset($sharee->properties['{DAV:}displayname']) ? $sharee->properties['{DAV:}displayname'] : null, + $sharee->inviteStatus ?: $oldSharee->inviteStatus, + $calendarId, + $sharee->href, + ]); + continue 2; + } + } + // If we got here, it means it was a new sharee + $insertStmt->execute([ + $calendarId, + $sharee->principal, + $sharee->access, + \Sabre\DAV\UUIDUtil::getUUID(), + $sharee->href, + isset($sharee->properties['{DAV:}displayname']) ? $sharee->properties['{DAV:}displayname'] : null, + $sharee->inviteStatus ?: \Sabre\DAV\Sharing\Plugin::INVITE_NORESPONSE, + $instanceId, + ]); + } + } + + /** + * Returns the list of people whom a calendar is shared with. + * + * Every item in the returned list must be a Sharee object with at + * least the following properties set: + * $href + * $shareAccess + * $inviteStatus + * + * and optionally: + * $properties + * + * @param mixed $calendarId + * + * @return \Sabre\DAV\Xml\Element\Sharee[] + */ + public function getInvites($calendarId) + { + if (!is_array($calendarId)) { + throw new \InvalidArgumentException('The value passed to getInvites() is expected to be an array with a calendarId and an instanceId'); + } + list($calendarId, $instanceId) = $calendarId; + + $query = <<calendarInstancesTableName} +WHERE + calendarid = ? +SQL; + + $stmt = $this->pdo->prepare($query); + $stmt->execute([$calendarId]); + + $result = []; + while ($row = $stmt->fetch(\PDO::FETCH_ASSOC)) { + $result[] = new Sharee([ + 'href' => isset($row['share_href']) ? $row['share_href'] : \Sabre\HTTP\encodePath($row['principaluri']), + 'access' => (int) $row['access'], + /// Everyone is always immediately accepted, for now. + 'inviteStatus' => (int) $row['share_invitestatus'], + 'properties' => !empty($row['share_displayname']) + ? ['{DAV:}displayname' => $row['share_displayname']] + : [], + 'principal' => $row['principaluri'], + ]); + } + + return $result; + } + + /** + * Publishes a calendar. + * + * @param mixed $calendarId + * @param bool $value + */ + public function setPublishStatus($calendarId, $value) + { + throw new DAV\Exception\NotImplemented('Not implemented'); + } +} diff --git a/3rdparty/sabre/dav/lib/CalDAV/Backend/SchedulingSupport.php b/3rdparty/sabre/dav/lib/CalDAV/Backend/SchedulingSupport.php new file mode 100644 index 00000000..69467e55 --- /dev/null +++ b/3rdparty/sabre/dav/lib/CalDAV/Backend/SchedulingSupport.php @@ -0,0 +1,66 @@ +pdo = $pdo; + } + + /** + * Returns a list of calendars for a principal. + * + * Every project is an array with the following keys: + * * id, a unique id that will be used by other functions to modify the + * calendar. This can be the same as the uri or a database key. + * * uri. This is just the 'base uri' or 'filename' of the calendar. + * * principaluri. The owner of the calendar. Almost always the same as + * principalUri passed to this method. + * + * Furthermore it can contain webdav properties in clark notation. A very + * common one is '{DAV:}displayname'. + * + * Many clients also require: + * {urn:ietf:params:xml:ns:caldav}supported-calendar-component-set + * For this property, you can just return an instance of + * Sabre\CalDAV\Xml\Property\SupportedCalendarComponentSet. + * + * If you return {http://sabredav.org/ns}read-only and set the value to 1, + * ACL will automatically be put in read-only mode. + * + * @param string $principalUri + * + * @return array + */ + public function getCalendarsForUser($principalUri) + { + // Making fields a comma-delimited list + $stmt = $this->pdo->prepare('SELECT id, uri FROM simple_calendars WHERE principaluri = ? ORDER BY id ASC'); + $stmt->execute([$principalUri]); + + $calendars = []; + while ($row = $stmt->fetch(\PDO::FETCH_ASSOC)) { + $calendars[] = [ + 'id' => $row['id'], + 'uri' => $row['uri'], + 'principaluri' => $principalUri, + ]; + } + + return $calendars; + } + + /** + * Creates a new calendar for a principal. + * + * If the creation was a success, an id must be returned that can be used + * to reference this calendar in other methods, such as updateCalendar. + * + * @param string $principalUri + * @param string $calendarUri + * + * @return string + */ + public function createCalendar($principalUri, $calendarUri, array $properties) + { + $stmt = $this->pdo->prepare('INSERT INTO simple_calendars (principaluri, uri) VALUES (?, ?)'); + $stmt->execute([$principalUri, $calendarUri]); + + return $this->pdo->lastInsertId(); + } + + /** + * Delete a calendar and all it's objects. + * + * @param string $calendarId + */ + public function deleteCalendar($calendarId) + { + $stmt = $this->pdo->prepare('DELETE FROM simple_calendarobjects WHERE calendarid = ?'); + $stmt->execute([$calendarId]); + + $stmt = $this->pdo->prepare('DELETE FROM simple_calendars WHERE id = ?'); + $stmt->execute([$calendarId]); + } + + /** + * Returns all calendar objects within a calendar. + * + * Every item contains an array with the following keys: + * * calendardata - The iCalendar-compatible calendar data + * * uri - a unique key which will be used to construct the uri. This can + * be any arbitrary string, but making sure it ends with '.ics' is a + * good idea. This is only the basename, or filename, not the full + * path. + * * lastmodified - a timestamp of the last modification time + * * etag - An arbitrary string, surrounded by double-quotes. (e.g.: + * ' "abcdef"') + * * size - The size of the calendar objects, in bytes. + * * component - optional, a string containing the type of object, such + * as 'vevent' or 'vtodo'. If specified, this will be used to populate + * the Content-Type header. + * + * Note that the etag is optional, but it's highly encouraged to return for + * speed reasons. + * + * The calendardata is also optional. If it's not returned + * 'getCalendarObject' will be called later, which *is* expected to return + * calendardata. + * + * If neither etag or size are specified, the calendardata will be + * used/fetched to determine these numbers. If both are specified the + * amount of times this is needed is reduced by a great degree. + * + * @param string $calendarId + * + * @return array + */ + public function getCalendarObjects($calendarId) + { + $stmt = $this->pdo->prepare('SELECT id, uri, calendardata FROM simple_calendarobjects WHERE calendarid = ?'); + $stmt->execute([$calendarId]); + + $result = []; + foreach ($stmt->fetchAll(\PDO::FETCH_ASSOC) as $row) { + $result[] = [ + 'id' => $row['id'], + 'uri' => $row['uri'], + 'etag' => '"'.md5($row['calendardata']).'"', + 'calendarid' => $calendarId, + 'size' => strlen($row['calendardata']), + 'calendardata' => $row['calendardata'], + ]; + } + + return $result; + } + + /** + * Returns information from a single calendar object, based on it's object + * uri. + * + * The object uri is only the basename, or filename and not a full path. + * + * The returned array must have the same keys as getCalendarObjects. The + * 'calendardata' object is required here though, while it's not required + * for getCalendarObjects. + * + * This method must return null if the object did not exist. + * + * @param string $calendarId + * @param string $objectUri + * + * @return array|null + */ + public function getCalendarObject($calendarId, $objectUri) + { + $stmt = $this->pdo->prepare('SELECT id, uri, calendardata FROM simple_calendarobjects WHERE calendarid = ? AND uri = ?'); + $stmt->execute([$calendarId, $objectUri]); + $row = $stmt->fetch(\PDO::FETCH_ASSOC); + + if (!$row) { + return null; + } + + return [ + 'id' => $row['id'], + 'uri' => $row['uri'], + 'etag' => '"'.md5($row['calendardata']).'"', + 'calendarid' => $calendarId, + 'size' => strlen($row['calendardata']), + 'calendardata' => $row['calendardata'], + ]; + } + + /** + * Creates a new calendar object. + * + * The object uri is only the basename, or filename and not a full path. + * + * It is possible return an etag from this function, which will be used in + * the response to this PUT request. Note that the ETag must be surrounded + * by double-quotes. + * + * However, you should only really return this ETag if you don't mangle the + * calendar-data. If the result of a subsequent GET to this object is not + * the exact same as this request body, you should omit the ETag. + * + * @param mixed $calendarId + * @param string $objectUri + * @param string $calendarData + * + * @return string|null + */ + public function createCalendarObject($calendarId, $objectUri, $calendarData) + { + $stmt = $this->pdo->prepare('INSERT INTO simple_calendarobjects (calendarid, uri, calendardata) VALUES (?,?,?)'); + $stmt->execute([ + $calendarId, + $objectUri, + $calendarData, + ]); + + return '"'.md5($calendarData).'"'; + } + + /** + * Updates an existing calendarobject, based on it's uri. + * + * The object uri is only the basename, or filename and not a full path. + * + * It is possible return an etag from this function, which will be used in + * the response to this PUT request. Note that the ETag must be surrounded + * by double-quotes. + * + * However, you should only really return this ETag if you don't mangle the + * calendar-data. If the result of a subsequent GET to this object is not + * the exact same as this request body, you should omit the ETag. + * + * @param mixed $calendarId + * @param string $objectUri + * @param string $calendarData + * + * @return string|null + */ + public function updateCalendarObject($calendarId, $objectUri, $calendarData) + { + $stmt = $this->pdo->prepare('UPDATE simple_calendarobjects SET calendardata = ? WHERE calendarid = ? AND uri = ?'); + $stmt->execute([$calendarData, $calendarId, $objectUri]); + + return '"'.md5($calendarData).'"'; + } + + /** + * Deletes an existing calendar object. + * + * The object uri is only the basename, or filename and not a full path. + * + * @param string $calendarId + * @param string $objectUri + */ + public function deleteCalendarObject($calendarId, $objectUri) + { + $stmt = $this->pdo->prepare('DELETE FROM simple_calendarobjects WHERE calendarid = ? AND uri = ?'); + $stmt->execute([$calendarId, $objectUri]); + } +} diff --git a/3rdparty/sabre/dav/lib/CalDAV/Backend/SubscriptionSupport.php b/3rdparty/sabre/dav/lib/CalDAV/Backend/SubscriptionSupport.php new file mode 100644 index 00000000..7655c2e1 --- /dev/null +++ b/3rdparty/sabre/dav/lib/CalDAV/Backend/SubscriptionSupport.php @@ -0,0 +1,89 @@ + 'The current synctoken', + * 'added' => [ + * 'new.txt', + * ], + * 'modified' => [ + * 'modified.txt', + * ], + * 'deleted' => [ + * 'foo.php.bak', + * 'old.txt' + * ] + * ); + * + * The returned syncToken property should reflect the *current* syncToken + * of the calendar, as reported in the {http://sabredav.org/ns}sync-token + * property This is * needed here too, to ensure the operation is atomic. + * + * If the $syncToken argument is specified as null, this is an initial + * sync, and all members should be reported. + * + * The modified property is an array of nodenames that have changed since + * the last token. + * + * The deleted property is an array with nodenames, that have been deleted + * from collection. + * + * The $syncLevel argument is basically the 'depth' of the report. If it's + * 1, you only have to report changes that happened only directly in + * immediate descendants. If it's 2, it should also include changes from + * the nodes below the child collections. (grandchildren) + * + * The $limit argument allows a client to specify how many results should + * be returned at most. If the limit is not specified, it should be treated + * as infinite. + * + * If the limit (infinite or not) is higher than you're willing to return, + * you should throw a Sabre\DAV\Exception\TooMuchMatches() exception. + * + * If the syncToken is expired (due to data cleanup) or unknown, you must + * return null. + * + * The limit is 'suggestive'. You are free to ignore it. + * + * @param string $calendarId + * @param string $syncToken + * @param int $syncLevel + * @param int $limit + * + * @return array|null + */ + public function getChangesForCalendar($calendarId, $syncToken, $syncLevel, $limit = null); +} diff --git a/3rdparty/sabre/dav/lib/CalDAV/Calendar.php b/3rdparty/sabre/dav/lib/CalDAV/Calendar.php new file mode 100644 index 00000000..ba8c704a --- /dev/null +++ b/3rdparty/sabre/dav/lib/CalDAV/Calendar.php @@ -0,0 +1,460 @@ +caldavBackend = $caldavBackend; + $this->calendarInfo = $calendarInfo; + } + + /** + * Returns the name of the calendar. + * + * @return string + */ + public function getName() + { + return $this->calendarInfo['uri']; + } + + /** + * Updates properties on this node. + * + * This method received a PropPatch object, which contains all the + * information about the update. + * + * To update specific properties, call the 'handle' method on this object. + * Read the PropPatch documentation for more information. + */ + public function propPatch(PropPatch $propPatch) + { + return $this->caldavBackend->updateCalendar($this->calendarInfo['id'], $propPatch); + } + + /** + * Returns the list of properties. + * + * @param array $requestedProperties + * + * @return array + */ + public function getProperties($requestedProperties) + { + $response = []; + + foreach ($this->calendarInfo as $propName => $propValue) { + if (!is_null($propValue) && '{' === $propName[0]) { + $response[$propName] = $this->calendarInfo[$propName]; + } + } + + return $response; + } + + /** + * Returns a calendar object. + * + * The contained calendar objects are for example Events or Todo's. + * + * @param string $name + * + * @return \Sabre\CalDAV\ICalendarObject + */ + public function getChild($name) + { + $obj = $this->caldavBackend->getCalendarObject($this->calendarInfo['id'], $name); + + if (!$obj) { + throw new DAV\Exception\NotFound('Calendar object not found'); + } + $obj['acl'] = $this->getChildACL(); + + return new CalendarObject($this->caldavBackend, $this->calendarInfo, $obj); + } + + /** + * Returns the full list of calendar objects. + * + * @return array + */ + public function getChildren() + { + $objs = $this->caldavBackend->getCalendarObjects($this->calendarInfo['id']); + $children = []; + foreach ($objs as $obj) { + $obj['acl'] = $this->getChildACL(); + $children[] = new CalendarObject($this->caldavBackend, $this->calendarInfo, $obj); + } + + return $children; + } + + /** + * This method receives a list of paths in it's first argument. + * It must return an array with Node objects. + * + * If any children are not found, you do not have to return them. + * + * @param string[] $paths + * + * @return array + */ + public function getMultipleChildren(array $paths) + { + $objs = $this->caldavBackend->getMultipleCalendarObjects($this->calendarInfo['id'], $paths); + $children = []; + foreach ($objs as $obj) { + $obj['acl'] = $this->getChildACL(); + $children[] = new CalendarObject($this->caldavBackend, $this->calendarInfo, $obj); + } + + return $children; + } + + /** + * Checks if a child-node exists. + * + * @param string $name + * + * @return bool + */ + public function childExists($name) + { + $obj = $this->caldavBackend->getCalendarObject($this->calendarInfo['id'], $name); + if (!$obj) { + return false; + } else { + return true; + } + } + + /** + * Creates a new directory. + * + * We actually block this, as subdirectories are not allowed in calendars. + * + * @param string $name + */ + public function createDirectory($name) + { + throw new DAV\Exception\MethodNotAllowed('Creating collections in calendar objects is not allowed'); + } + + /** + * Creates a new file. + * + * The contents of the new file must be a valid ICalendar string. + * + * @param string $name + * @param resource $data + * + * @return string|null + */ + public function createFile($name, $data = null) + { + if (is_resource($data)) { + $data = stream_get_contents($data); + } + + return $this->caldavBackend->createCalendarObject($this->calendarInfo['id'], $name, $data); + } + + /** + * Deletes the calendar. + */ + public function delete() + { + $this->caldavBackend->deleteCalendar($this->calendarInfo['id']); + } + + /** + * Renames the calendar. Note that most calendars use the + * {DAV:}displayname to display a name to display a name. + * + * @param string $newName + */ + public function setName($newName) + { + throw new DAV\Exception\MethodNotAllowed('Renaming calendars is not yet supported'); + } + + /** + * Returns the last modification date as a unix timestamp. + */ + public function getLastModified() + { + return null; + } + + /** + * Returns the owner principal. + * + * This must be a url to a principal, or null if there's no owner + * + * @return string|null + */ + public function getOwner() + { + return $this->calendarInfo['principaluri']; + } + + /** + * Returns a list of ACE's for this node. + * + * Each ACE has the following properties: + * * 'privilege', a string such as {DAV:}read or {DAV:}write. These are + * currently the only supported privileges + * * 'principal', a url to the principal who owns the node + * * 'protected' (optional), indicating that this ACE is not allowed to + * be updated. + * + * @return array + */ + public function getACL() + { + $acl = [ + [ + 'privilege' => '{DAV:}read', + 'principal' => $this->getOwner(), + 'protected' => true, + ], + [ + 'privilege' => '{DAV:}read', + 'principal' => $this->getOwner().'/calendar-proxy-write', + 'protected' => true, + ], + [ + 'privilege' => '{DAV:}read', + 'principal' => $this->getOwner().'/calendar-proxy-read', + 'protected' => true, + ], + [ + 'privilege' => '{'.Plugin::NS_CALDAV.'}read-free-busy', + 'principal' => '{DAV:}authenticated', + 'protected' => true, + ], + ]; + if (empty($this->calendarInfo['{http://sabredav.org/ns}read-only'])) { + $acl[] = [ + 'privilege' => '{DAV:}write', + 'principal' => $this->getOwner(), + 'protected' => true, + ]; + $acl[] = [ + 'privilege' => '{DAV:}write', + 'principal' => $this->getOwner().'/calendar-proxy-write', + 'protected' => true, + ]; + } + + return $acl; + } + + /** + * This method returns the ACL's for calendar objects in this calendar. + * The result of this method automatically gets passed to the + * calendar-object nodes in the calendar. + * + * @return array + */ + public function getChildACL() + { + $acl = [ + [ + 'privilege' => '{DAV:}read', + 'principal' => $this->getOwner(), + 'protected' => true, + ], + + [ + 'privilege' => '{DAV:}read', + 'principal' => $this->getOwner().'/calendar-proxy-write', + 'protected' => true, + ], + [ + 'privilege' => '{DAV:}read', + 'principal' => $this->getOwner().'/calendar-proxy-read', + 'protected' => true, + ], + ]; + if (empty($this->calendarInfo['{http://sabredav.org/ns}read-only'])) { + $acl[] = [ + 'privilege' => '{DAV:}write', + 'principal' => $this->getOwner(), + 'protected' => true, + ]; + $acl[] = [ + 'privilege' => '{DAV:}write', + 'principal' => $this->getOwner().'/calendar-proxy-write', + 'protected' => true, + ]; + } + + return $acl; + } + + /** + * Performs a calendar-query on the contents of this calendar. + * + * The calendar-query is defined in RFC4791 : CalDAV. Using the + * calendar-query it is possible for a client to request a specific set of + * object, based on contents of iCalendar properties, date-ranges and + * iCalendar component types (VTODO, VEVENT). + * + * This method should just return a list of (relative) urls that match this + * query. + * + * The list of filters are specified as an array. The exact array is + * documented by Sabre\CalDAV\CalendarQueryParser. + * + * @return array + */ + public function calendarQuery(array $filters) + { + return $this->caldavBackend->calendarQuery($this->calendarInfo['id'], $filters); + } + + /** + * This method returns the current sync-token for this collection. + * This can be any string. + * + * If null is returned from this function, the plugin assumes there's no + * sync information available. + * + * @return string|null + */ + public function getSyncToken() + { + if ( + $this->caldavBackend instanceof Backend\SyncSupport && + isset($this->calendarInfo['{DAV:}sync-token']) + ) { + return $this->calendarInfo['{DAV:}sync-token']; + } + if ( + $this->caldavBackend instanceof Backend\SyncSupport && + isset($this->calendarInfo['{http://sabredav.org/ns}sync-token']) + ) { + return $this->calendarInfo['{http://sabredav.org/ns}sync-token']; + } + } + + /** + * The getChanges method returns all the changes that have happened, since + * the specified syncToken and the current collection. + * + * This function should return an array, such as the following: + * + * [ + * 'syncToken' => 'The current synctoken', + * 'added' => [ + * 'new.txt', + * ], + * 'modified' => [ + * 'modified.txt', + * ], + * 'deleted' => [ + * 'foo.php.bak', + * 'old.txt' + * ], + * 'result_truncated' : true + * ]; + * + * The syncToken property should reflect the *current* syncToken of the + * collection, as reported getSyncToken(). This is needed here too, to + * ensure the operation is atomic. + * + * If the syncToken is specified as null, this is an initial sync, and all + * members should be reported. + * + * If result is truncated due to server limitation or limit by client, + * set result_truncated to true, otherwise set to false or do not add the key. + * + * The modified property is an array of nodenames that have changed since + * the last token. + * + * The deleted property is an array with nodenames, that have been deleted + * from collection. + * + * The second argument is basically the 'depth' of the report. If it's 1, + * you only have to report changes that happened only directly in immediate + * descendants. If it's 2, it should also include changes from the nodes + * below the child collections. (grandchildren) + * + * The third (optional) argument allows a client to specify how many + * results should be returned at most. If the limit is not specified, it + * should be treated as infinite. + * + * If the limit (infinite or not) is higher than you're willing to return, + * the result should be truncated to fit the limit. + * Note that even when the result is truncated, syncToken must be consistent + * with the truncated result, not the result before truncation. + * (See RFC6578 Section 3.6 for detail) + * + * If the syncToken is expired (due to data cleanup) or unknown, you must + * return null. + * + * The limit is 'suggestive'. You are free to ignore it. + * TODO: RFC6578 Section 3.7 says that the server must fail when the server + * cannot truncate according to the limit, so it may not be just suggestive. + * + * @param string $syncToken + * @param int $syncLevel + * @param int $limit + * + * @return array|null + */ + public function getChanges($syncToken, $syncLevel, $limit = null) + { + if (!$this->caldavBackend instanceof Backend\SyncSupport) { + return null; + } + + return $this->caldavBackend->getChangesForCalendar( + $this->calendarInfo['id'], + $syncToken, + $syncLevel, + $limit + ); + } +} diff --git a/3rdparty/sabre/dav/lib/CalDAV/CalendarHome.php b/3rdparty/sabre/dav/lib/CalDAV/CalendarHome.php new file mode 100644 index 00000000..49c54a37 --- /dev/null +++ b/3rdparty/sabre/dav/lib/CalDAV/CalendarHome.php @@ -0,0 +1,356 @@ +caldavBackend = $caldavBackend; + $this->principalInfo = $principalInfo; + } + + /** + * Returns the name of this object. + * + * @return string + */ + public function getName() + { + list(, $name) = Uri\split($this->principalInfo['uri']); + + return $name; + } + + /** + * Updates the name of this object. + * + * @param string $name + */ + public function setName($name) + { + throw new DAV\Exception\Forbidden(); + } + + /** + * Deletes this object. + */ + public function delete() + { + throw new DAV\Exception\Forbidden(); + } + + /** + * Returns the last modification date. + * + * @return int + */ + public function getLastModified() + { + return null; + } + + /** + * Creates a new file under this object. + * + * This is currently not allowed + * + * @param string $name + * @param resource $data + */ + public function createFile($name, $data = null) + { + throw new DAV\Exception\MethodNotAllowed('Creating new files in this collection is not supported'); + } + + /** + * Creates a new directory under this object. + * + * This is currently not allowed. + * + * @param string $filename + */ + public function createDirectory($filename) + { + throw new DAV\Exception\MethodNotAllowed('Creating new collections in this collection is not supported'); + } + + /** + * Returns a single calendar, by name. + * + * @param string $name + * + * @return Calendar + */ + public function getChild($name) + { + // Special nodes + if ('inbox' === $name && $this->caldavBackend instanceof Backend\SchedulingSupport) { + return new Schedule\Inbox($this->caldavBackend, $this->principalInfo['uri']); + } + if ('outbox' === $name && $this->caldavBackend instanceof Backend\SchedulingSupport) { + return new Schedule\Outbox($this->principalInfo['uri']); + } + if ('notifications' === $name && $this->caldavBackend instanceof Backend\NotificationSupport) { + return new Notifications\Collection($this->caldavBackend, $this->principalInfo['uri']); + } + + // Calendars + foreach ($this->caldavBackend->getCalendarsForUser($this->principalInfo['uri']) as $calendar) { + if ($calendar['uri'] === $name) { + if ($this->caldavBackend instanceof Backend\SharingSupport) { + return new SharedCalendar($this->caldavBackend, $calendar); + } else { + return new Calendar($this->caldavBackend, $calendar); + } + } + } + + if ($this->caldavBackend instanceof Backend\SubscriptionSupport) { + foreach ($this->caldavBackend->getSubscriptionsForUser($this->principalInfo['uri']) as $subscription) { + if ($subscription['uri'] === $name) { + return new Subscriptions\Subscription($this->caldavBackend, $subscription); + } + } + } + + throw new NotFound('Node with name \''.$name.'\' could not be found'); + } + + /** + * Checks if a calendar exists. + * + * @param string $name + * + * @return bool + */ + public function childExists($name) + { + try { + return (bool) $this->getChild($name); + } catch (NotFound $e) { + return false; + } + } + + /** + * Returns a list of calendars. + * + * @return array + */ + public function getChildren() + { + $calendars = $this->caldavBackend->getCalendarsForUser($this->principalInfo['uri']); + $objs = []; + foreach ($calendars as $calendar) { + if ($this->caldavBackend instanceof Backend\SharingSupport) { + $objs[] = new SharedCalendar($this->caldavBackend, $calendar); + } else { + $objs[] = new Calendar($this->caldavBackend, $calendar); + } + } + + if ($this->caldavBackend instanceof Backend\SchedulingSupport) { + $objs[] = new Schedule\Inbox($this->caldavBackend, $this->principalInfo['uri']); + $objs[] = new Schedule\Outbox($this->principalInfo['uri']); + } + + // We're adding a notifications node, if it's supported by the backend. + if ($this->caldavBackend instanceof Backend\NotificationSupport) { + $objs[] = new Notifications\Collection($this->caldavBackend, $this->principalInfo['uri']); + } + + // If the backend supports subscriptions, we'll add those as well, + if ($this->caldavBackend instanceof Backend\SubscriptionSupport) { + foreach ($this->caldavBackend->getSubscriptionsForUser($this->principalInfo['uri']) as $subscription) { + $objs[] = new Subscriptions\Subscription($this->caldavBackend, $subscription); + } + } + + return $objs; + } + + /** + * Creates a new calendar or subscription. + * + * @param string $name + * + * @throws DAV\Exception\InvalidResourceType + */ + public function createExtendedCollection($name, MkCol $mkCol) + { + $isCalendar = false; + $isSubscription = false; + foreach ($mkCol->getResourceType() as $rt) { + switch ($rt) { + case '{DAV:}collection': + case '{http://calendarserver.org/ns/}shared-owner': + // ignore + break; + case '{urn:ietf:params:xml:ns:caldav}calendar': + $isCalendar = true; + break; + case '{http://calendarserver.org/ns/}subscribed': + $isSubscription = true; + break; + default: + throw new DAV\Exception\InvalidResourceType('Unknown resourceType: '.$rt); + } + } + + $properties = $mkCol->getRemainingValues(); + $mkCol->setRemainingResultCode(201); + + if ($isSubscription) { + if (!$this->caldavBackend instanceof Backend\SubscriptionSupport) { + throw new DAV\Exception\InvalidResourceType('This backend does not support subscriptions'); + } + $this->caldavBackend->createSubscription($this->principalInfo['uri'], $name, $properties); + } elseif ($isCalendar) { + $this->caldavBackend->createCalendar($this->principalInfo['uri'], $name, $properties); + } else { + throw new DAV\Exception\InvalidResourceType('You can only create calendars and subscriptions in this collection'); + } + } + + /** + * Returns the owner of the calendar home. + * + * @return string + */ + public function getOwner() + { + return $this->principalInfo['uri']; + } + + /** + * Returns a list of ACE's for this node. + * + * Each ACE has the following properties: + * * 'privilege', a string such as {DAV:}read or {DAV:}write. These are + * currently the only supported privileges + * * 'principal', a url to the principal who owns the node + * * 'protected' (optional), indicating that this ACE is not allowed to + * be updated. + * + * @return array + */ + public function getACL() + { + return [ + [ + 'privilege' => '{DAV:}read', + 'principal' => $this->principalInfo['uri'], + 'protected' => true, + ], + [ + 'privilege' => '{DAV:}write', + 'principal' => $this->principalInfo['uri'], + 'protected' => true, + ], + [ + 'privilege' => '{DAV:}read', + 'principal' => $this->principalInfo['uri'].'/calendar-proxy-write', + 'protected' => true, + ], + [ + 'privilege' => '{DAV:}write', + 'principal' => $this->principalInfo['uri'].'/calendar-proxy-write', + 'protected' => true, + ], + [ + 'privilege' => '{DAV:}read', + 'principal' => $this->principalInfo['uri'].'/calendar-proxy-read', + 'protected' => true, + ], + ]; + } + + /** + * This method is called when a user replied to a request to share. + * + * This method should return the url of the newly created calendar if the + * share was accepted. + * + * @param string $href The sharee who is replying (often a mailto: address) + * @param int $status One of the SharingPlugin::STATUS_* constants + * @param string $calendarUri The url to the calendar thats being shared + * @param string $inReplyTo The unique id this message is a response to + * @param string $summary A description of the reply + * + * @return string|null + */ + public function shareReply($href, $status, $calendarUri, $inReplyTo, $summary = null) + { + if (!$this->caldavBackend instanceof Backend\SharingSupport) { + throw new DAV\Exception\NotImplemented('Sharing support is not implemented by this backend.'); + } + + return $this->caldavBackend->shareReply($href, $status, $calendarUri, $inReplyTo, $summary); + } + + /** + * Searches through all of a users calendars and calendar objects to find + * an object with a specific UID. + * + * This method should return the path to this object, relative to the + * calendar home, so this path usually only contains two parts: + * + * calendarpath/objectpath.ics + * + * If the uid is not found, return null. + * + * This method should only consider * objects that the principal owns, so + * any calendars owned by other principals that also appear in this + * collection should be ignored. + * + * @param string $uid + * + * @return string|null + */ + public function getCalendarObjectByUID($uid) + { + return $this->caldavBackend->getCalendarObjectByUID($this->principalInfo['uri'], $uid); + } +} diff --git a/3rdparty/sabre/dav/lib/CalDAV/CalendarObject.php b/3rdparty/sabre/dav/lib/CalDAV/CalendarObject.php new file mode 100644 index 00000000..671f4b5d --- /dev/null +++ b/3rdparty/sabre/dav/lib/CalDAV/CalendarObject.php @@ -0,0 +1,223 @@ +caldavBackend = $caldavBackend; + + if (!isset($objectData['uri'])) { + throw new \InvalidArgumentException('The objectData argument must contain an \'uri\' property'); + } + + $this->calendarInfo = $calendarInfo; + $this->objectData = $objectData; + } + + /** + * Returns the uri for this object. + * + * @return string + */ + public function getName() + { + return $this->objectData['uri']; + } + + /** + * Returns the ICalendar-formatted object. + * + * @return string + */ + public function get() + { + // Pre-populating the 'calendardata' is optional, if we don't have it + // already we fetch it from the backend. + if (!isset($this->objectData['calendardata'])) { + $this->objectData = $this->caldavBackend->getCalendarObject($this->calendarInfo['id'], $this->objectData['uri']); + } + + return $this->objectData['calendardata']; + } + + /** + * Updates the ICalendar-formatted object. + * + * @param string|resource $calendarData + * + * @return string + */ + public function put($calendarData) + { + if (is_resource($calendarData)) { + $calendarData = stream_get_contents($calendarData); + } + $etag = $this->caldavBackend->updateCalendarObject($this->calendarInfo['id'], $this->objectData['uri'], $calendarData); + $this->objectData['calendardata'] = $calendarData; + $this->objectData['etag'] = $etag; + + return $etag; + } + + /** + * Deletes the calendar object. + */ + public function delete() + { + $this->caldavBackend->deleteCalendarObject($this->calendarInfo['id'], $this->objectData['uri']); + } + + /** + * Returns the mime content-type. + * + * @return string + */ + public function getContentType() + { + $mime = 'text/calendar; charset=utf-8'; + if (isset($this->objectData['component']) && $this->objectData['component']) { + $mime .= '; component='.$this->objectData['component']; + } + + return $mime; + } + + /** + * Returns an ETag for this object. + * + * The ETag is an arbitrary string, but MUST be surrounded by double-quotes. + * + * @return string + */ + public function getETag() + { + if (isset($this->objectData['etag'])) { + return $this->objectData['etag']; + } else { + return '"'.md5($this->get()).'"'; + } + } + + /** + * Returns the last modification date as a unix timestamp. + * + * @return int + */ + public function getLastModified() + { + return $this->objectData['lastmodified']; + } + + /** + * Returns the size of this object in bytes. + * + * @return int + */ + public function getSize() + { + if (array_key_exists('size', $this->objectData)) { + return $this->objectData['size']; + } else { + return strlen($this->get()); + } + } + + /** + * Returns the owner principal. + * + * This must be a url to a principal, or null if there's no owner + * + * @return string|null + */ + public function getOwner() + { + return $this->calendarInfo['principaluri']; + } + + /** + * Returns a list of ACE's for this node. + * + * Each ACE has the following properties: + * * 'privilege', a string such as {DAV:}read or {DAV:}write. These are + * currently the only supported privileges + * * 'principal', a url to the principal who owns the node + * * 'protected' (optional), indicating that this ACE is not allowed to + * be updated. + * + * @return array + */ + public function getACL() + { + // An alternative acl may be specified in the object data. + if (isset($this->objectData['acl'])) { + return $this->objectData['acl']; + } + + // The default ACL + return [ + [ + 'privilege' => '{DAV:}all', + 'principal' => $this->calendarInfo['principaluri'], + 'protected' => true, + ], + [ + 'privilege' => '{DAV:}all', + 'principal' => $this->calendarInfo['principaluri'].'/calendar-proxy-write', + 'protected' => true, + ], + [ + 'privilege' => '{DAV:}read', + 'principal' => $this->calendarInfo['principaluri'].'/calendar-proxy-read', + 'protected' => true, + ], + ]; + } +} diff --git a/3rdparty/sabre/dav/lib/CalDAV/CalendarQueryValidator.php b/3rdparty/sabre/dav/lib/CalDAV/CalendarQueryValidator.php new file mode 100644 index 00000000..ee525da7 --- /dev/null +++ b/3rdparty/sabre/dav/lib/CalDAV/CalendarQueryValidator.php @@ -0,0 +1,354 @@ +name !== $filters['name']) { + return false; + } + + return + $this->validateCompFilters($vObject, $filters['comp-filters']) && + $this->validatePropFilters($vObject, $filters['prop-filters']); + } + + /** + * This method checks the validity of comp-filters. + * + * A list of comp-filters needs to be specified. Also the parent of the + * component we're checking should be specified, not the component to check + * itself. + * + * @return bool + */ + protected function validateCompFilters(VObject\Component $parent, array $filters) + { + foreach ($filters as $filter) { + $isDefined = isset($parent->{$filter['name']}); + + if ($filter['is-not-defined']) { + if ($isDefined) { + return false; + } else { + continue; + } + } + if (!$isDefined) { + return false; + } + + if (array_key_exists('time-range', $filter) && $filter['time-range']) { + foreach ($parent->{$filter['name']} as $subComponent) { + $start = null; + $end = null; + if (array_key_exists('start', $filter['time-range'])) { + $start = $filter['time-range']['start']; + } + if (array_key_exists('end', $filter['time-range'])) { + $end = $filter['time-range']['end']; + } + if ($this->validateTimeRange($subComponent, $start, $end)) { + continue 2; + } + } + + return false; + } + + if (!$filter['comp-filters'] && !$filter['prop-filters']) { + continue; + } + + // If there are sub-filters, we need to find at least one component + // for which the subfilters hold true. + foreach ($parent->{$filter['name']} as $subComponent) { + if ( + $this->validateCompFilters($subComponent, $filter['comp-filters']) && + $this->validatePropFilters($subComponent, $filter['prop-filters'])) { + // We had a match, so this comp-filter succeeds + continue 2; + } + } + + // If we got here it means there were sub-comp-filters or + // sub-prop-filters and there was no match. This means this filter + // needs to return false. + return false; + } + + // If we got here it means we got through all comp-filters alive so the + // filters were all true. + return true; + } + + /** + * This method checks the validity of prop-filters. + * + * A list of prop-filters needs to be specified. Also the parent of the + * property we're checking should be specified, not the property to check + * itself. + * + * @return bool + */ + protected function validatePropFilters(VObject\Component $parent, array $filters) + { + foreach ($filters as $filter) { + $isDefined = isset($parent->{$filter['name']}); + + if ($filter['is-not-defined']) { + if ($isDefined) { + return false; + } else { + continue; + } + } + if (!$isDefined) { + return false; + } + + if (array_key_exists('time-range', $filter) && $filter['time-range']) { + foreach ($parent->{$filter['name']} as $subComponent) { + $start = null; + $end = null; + if (array_key_exists('start', $filter['time-range'])) { + $start = $filter['time-range']['start']; + } + if (array_key_exists('end', $filter['time-range'])) { + $end = $filter['time-range']['end']; + } + if ($this->validateTimeRange($subComponent, $start, $end)) { + continue 2; + } + } + + return false; + } + + if (!$filter['param-filters'] && !$filter['text-match']) { + continue; + } + + // If there are sub-filters, we need to find at least one property + // for which the subfilters hold true. + foreach ($parent->{$filter['name']} as $subComponent) { + if ( + $this->validateParamFilters($subComponent, $filter['param-filters']) && + (!$filter['text-match'] || $this->validateTextMatch($subComponent, $filter['text-match'])) + ) { + // We had a match, so this prop-filter succeeds + continue 2; + } + } + + // If we got here it means there were sub-param-filters or + // text-match filters and there was no match. This means the + // filter needs to return false. + return false; + } + + // If we got here it means we got through all prop-filters alive so the + // filters were all true. + return true; + } + + /** + * This method checks the validity of param-filters. + * + * A list of param-filters needs to be specified. Also the parent of the + * parameter we're checking should be specified, not the parameter to check + * itself. + * + * @return bool + */ + protected function validateParamFilters(VObject\Property $parent, array $filters) + { + foreach ($filters as $filter) { + $isDefined = isset($parent[$filter['name']]); + + if ($filter['is-not-defined']) { + if ($isDefined) { + return false; + } else { + continue; + } + } + if (!$isDefined) { + return false; + } + + if (!$filter['text-match']) { + continue; + } + + // If there are sub-filters, we need to find at least one parameter + // for which the subfilters hold true. + foreach ($parent[$filter['name']]->getParts() as $paramPart) { + if ($this->validateTextMatch($paramPart, $filter['text-match'])) { + // We had a match, so this param-filter succeeds + continue 2; + } + } + + // If we got here it means there was a text-match filter and there + // were no matches. This means the filter needs to return false. + return false; + } + + // If we got here it means we got through all param-filters alive so the + // filters were all true. + return true; + } + + /** + * This method checks the validity of a text-match. + * + * A single text-match should be specified as well as the specific property + * or parameter we need to validate. + * + * @param VObject\Node|string $check value to check against + * + * @return bool + */ + protected function validateTextMatch($check, array $textMatch) + { + if ($check instanceof VObject\Node) { + $check = $check->getValue(); + } + + $isMatching = \Sabre\DAV\StringUtil::textMatch($check, $textMatch['value'], $textMatch['collation']); + + return $textMatch['negate-condition'] xor $isMatching; + } + + /** + * Validates if a component matches the given time range. + * + * This is all based on the rules specified in rfc4791, which are quite + * complex. + * + * @param DateTime $start + * @param DateTime $end + * + * @return bool + */ + protected function validateTimeRange(VObject\Node $component, $start, $end) + { + if (is_null($start)) { + $start = new DateTime('1900-01-01'); + } + if (is_null($end)) { + $end = new DateTime('3000-01-01'); + } + + switch ($component->name) { + case 'VEVENT': + case 'VTODO': + case 'VJOURNAL': + return $component->isInTimeRange($start, $end); + + case 'VALARM': + // If the valarm is wrapped in a recurring event, we need to + // expand the recursions, and validate each. + // + // Our datamodel doesn't easily allow us to do this straight + // in the VALARM component code, so this is a hack, and an + // expensive one too. + if ('VEVENT' === $component->parent->name && $component->parent->RRULE) { + // Fire up the iterator! + $it = new VObject\Recur\EventIterator($component->parent->parent, (string) $component->parent->UID); + while ($it->valid()) { + $expandedEvent = $it->getEventObject(); + + // We need to check from these expanded alarms, which + // one is the first to trigger. Based on this, we can + // determine if we can 'give up' expanding events. + $firstAlarm = null; + if (null !== $expandedEvent->VALARM) { + foreach ($expandedEvent->VALARM as $expandedAlarm) { + $effectiveTrigger = $expandedAlarm->getEffectiveTriggerTime(); + if ($expandedAlarm->isInTimeRange($start, $end)) { + return true; + } + + if ('DATE-TIME' === (string) $expandedAlarm->TRIGGER['VALUE']) { + // This is an alarm with a non-relative trigger + // time, likely created by a buggy client. The + // implication is that every alarm in this + // recurring event trigger at the exact same + // time. It doesn't make sense to traverse + // further. + } else { + // We store the first alarm as a means to + // figure out when we can stop traversing. + if (!$firstAlarm || $effectiveTrigger < $firstAlarm) { + $firstAlarm = $effectiveTrigger; + } + } + } + } + if (is_null($firstAlarm)) { + // No alarm was found. + // + // Or technically: No alarm that will change for + // every instance of the recurrence was found, + // which means we can assume there was no match. + return false; + } + if ($firstAlarm > $end) { + return false; + } + $it->next(); + } + + return false; + } else { + return $component->isInTimeRange($start, $end); + } + + // no break + case 'VFREEBUSY': + throw new \Sabre\DAV\Exception\NotImplemented('time-range filters are currently not supported on '.$component->name.' components'); + case 'COMPLETED': + case 'CREATED': + case 'DTEND': + case 'DTSTAMP': + case 'DTSTART': + case 'DUE': + case 'LAST-MODIFIED': + return $start <= $component->getDateTime() && $end >= $component->getDateTime(); + + default: + throw new \Sabre\DAV\Exception\BadRequest('You cannot create a time-range filter on a '.$component->name.' component'); + } + } +} diff --git a/3rdparty/sabre/dav/lib/CalDAV/CalendarRoot.php b/3rdparty/sabre/dav/lib/CalDAV/CalendarRoot.php new file mode 100644 index 00000000..3038d218 --- /dev/null +++ b/3rdparty/sabre/dav/lib/CalDAV/CalendarRoot.php @@ -0,0 +1,75 @@ +caldavBackend = $caldavBackend; + } + + /** + * Returns the nodename. + * + * We're overriding this, because the default will be the 'principalPrefix', + * and we want it to be Sabre\CalDAV\Plugin::CALENDAR_ROOT + * + * @return string + */ + public function getName() + { + return Plugin::CALENDAR_ROOT; + } + + /** + * This method returns a node for a principal. + * + * The passed array contains principal information, and is guaranteed to + * at least contain a uri item. Other properties may or may not be + * supplied by the authentication backend. + * + * @return \Sabre\DAV\INode + */ + public function getChildForPrincipal(array $principal) + { + return new CalendarHome($this->caldavBackend, $principal); + } +} diff --git a/3rdparty/sabre/dav/lib/CalDAV/Exception/InvalidComponentType.php b/3rdparty/sabre/dav/lib/CalDAV/Exception/InvalidComponentType.php new file mode 100644 index 00000000..e94378a6 --- /dev/null +++ b/3rdparty/sabre/dav/lib/CalDAV/Exception/InvalidComponentType.php @@ -0,0 +1,31 @@ +ownerDocument; + + $np = $doc->createElementNS(CalDAV\Plugin::NS_CALDAV, 'cal:supported-calendar-component'); + $errorNode->appendChild($np); + } +} diff --git a/3rdparty/sabre/dav/lib/CalDAV/ICSExportPlugin.php b/3rdparty/sabre/dav/lib/CalDAV/ICSExportPlugin.php new file mode 100644 index 00000000..9171e36e --- /dev/null +++ b/3rdparty/sabre/dav/lib/CalDAV/ICSExportPlugin.php @@ -0,0 +1,377 @@ +server = $server; + $server->on('method:GET', [$this, 'httpGet'], 90); + $server->on('browserButtonActions', function ($path, $node, &$actions) { + if ($node instanceof ICalendar) { + $actions .= ''; + } + }); + } + + /** + * Intercepts GET requests on calendar urls ending with ?export. + * + * @throws BadRequest + * @throws DAV\Exception\NotFound + * @throws VObject\InvalidDataException + * + * @return bool + */ + public function httpGet(RequestInterface $request, ResponseInterface $response) + { + $queryParams = $request->getQueryParameters(); + if (!array_key_exists('export', $queryParams)) { + return; + } + + $path = $request->getPath(); + + $node = $this->server->getProperties($path, [ + '{DAV:}resourcetype', + '{DAV:}displayname', + '{http://sabredav.org/ns}sync-token', + '{DAV:}sync-token', + '{http://apple.com/ns/ical/}calendar-color', + ]); + + if (!isset($node['{DAV:}resourcetype']) || !$node['{DAV:}resourcetype']->is('{'.Plugin::NS_CALDAV.'}calendar')) { + return; + } + // Marking the transactionType, for logging purposes. + $this->server->transactionType = 'get-calendar-export'; + + $properties = $node; + + $start = null; + $end = null; + $expand = false; + $componentType = false; + if (isset($queryParams['start'])) { + if (!ctype_digit($queryParams['start'])) { + throw new BadRequest('The start= parameter must contain a unix timestamp'); + } + $start = DateTime::createFromFormat('U', $queryParams['start']); + } + if (isset($queryParams['end'])) { + if (!ctype_digit($queryParams['end'])) { + throw new BadRequest('The end= parameter must contain a unix timestamp'); + } + $end = DateTime::createFromFormat('U', $queryParams['end']); + } + if (isset($queryParams['expand']) && (bool) $queryParams['expand']) { + if (!$start || !$end) { + throw new BadRequest('If you\'d like to expand recurrences, you must specify both a start= and end= parameter.'); + } + $expand = true; + $componentType = 'VEVENT'; + } + if (isset($queryParams['componentType'])) { + if (!in_array($queryParams['componentType'], ['VEVENT', 'VTODO', 'VJOURNAL'])) { + throw new BadRequest('You are not allowed to search for components of type: '.$queryParams['componentType'].' here'); + } + $componentType = $queryParams['componentType']; + } + + $format = \Sabre\HTTP\negotiateContentType( + $request->getHeader('Accept'), + [ + 'text/calendar', + 'application/calendar+json', + ] + ); + + if (isset($queryParams['accept'])) { + if ('application/calendar+json' === $queryParams['accept'] || 'jcal' === $queryParams['accept']) { + $format = 'application/calendar+json'; + } + } + if (!$format) { + $format = 'text/calendar'; + } + + $this->generateResponse($path, $start, $end, $expand, $componentType, $format, $properties, $response); + + // Returning false to break the event chain + return false; + } + + /** + * This method is responsible for generating the actual, full response. + * + * @param string $path + * @param DateTime|null $start + * @param DateTime|null $end + * @param bool $expand + * @param string $componentType + * @param string $format + * @param array $properties + * + * @throws DAV\Exception\NotFound + * @throws VObject\InvalidDataException + */ + protected function generateResponse($path, $start, $end, $expand, $componentType, $format, $properties, ResponseInterface $response) + { + $calDataProp = '{'.Plugin::NS_CALDAV.'}calendar-data'; + $calendarNode = $this->server->tree->getNodeForPath($path); + + $blobs = []; + if ($start || $end || $componentType) { + // If there was a start or end filter, we need to enlist + // calendarQuery for speed. + $queryResult = $calendarNode->calendarQuery([ + 'name' => 'VCALENDAR', + 'comp-filters' => [ + [ + 'name' => $componentType, + 'comp-filters' => [], + 'prop-filters' => [], + 'is-not-defined' => false, + 'time-range' => [ + 'start' => $start, + 'end' => $end, + ], + ], + ], + 'prop-filters' => [], + 'is-not-defined' => false, + 'time-range' => null, + ]); + + // queryResult is just a list of base urls. We need to prefix the + // calendar path. + $queryResult = array_map( + function ($item) use ($path) { + return $path.'/'.$item; + }, + $queryResult + ); + $nodes = $this->server->getPropertiesForMultiplePaths($queryResult, [$calDataProp]); + unset($queryResult); + } else { + $nodes = $this->server->getPropertiesForPath($path, [$calDataProp], 1); + } + + // Flattening the arrays + foreach ($nodes as $node) { + if (isset($node[200][$calDataProp])) { + $blobs[$node['href']] = $node[200][$calDataProp]; + } + } + unset($nodes); + + $mergedCalendar = $this->mergeObjects( + $properties, + $blobs + ); + + if ($expand) { + $calendarTimeZone = null; + // We're expanding, and for that we need to figure out the + // calendar's timezone. + $tzProp = '{'.Plugin::NS_CALDAV.'}calendar-timezone'; + $tzResult = $this->server->getProperties($path, [$tzProp]); + if (isset($tzResult[$tzProp])) { + // This property contains a VCALENDAR with a single + // VTIMEZONE. + $vtimezoneObj = VObject\Reader::read($tzResult[$tzProp]); + $calendarTimeZone = $vtimezoneObj->VTIMEZONE->getTimeZone(); + // Destroy circular references to PHP will GC the object. + $vtimezoneObj->destroy(); + unset($vtimezoneObj); + } else { + // Defaulting to UTC. + $calendarTimeZone = new DateTimeZone('UTC'); + } + + $mergedCalendar = $mergedCalendar->expand($start, $end, $calendarTimeZone); + } + + $filenameExtension = '.ics'; + + switch ($format) { + case 'text/calendar': + $mergedCalendar = $mergedCalendar->serialize(); + $filenameExtension = '.ics'; + break; + case 'application/calendar+json': + $mergedCalendar = json_encode($mergedCalendar->jsonSerialize()); + $filenameExtension = '.json'; + break; + } + + $filename = preg_replace( + '/[^a-zA-Z0-9-_ ]/um', + '', + $calendarNode->getName() + ); + $filename .= '-'.date('Y-m-d').$filenameExtension; + + $response->setHeader('Content-Disposition', 'attachment; filename="'.$filename.'"'); + $response->setHeader('Content-Type', $format); + + $response->setStatus(200); + $response->setBody($mergedCalendar); + } + + /** + * Merges all calendar objects, and builds one big iCalendar blob. + * + * @param array $properties Some CalDAV properties + * + * @return VObject\Component\VCalendar + */ + public function mergeObjects(array $properties, array $inputObjects) + { + $calendar = new VObject\Component\VCalendar(); + $calendar->VERSION = '2.0'; + if (DAV\Server::$exposeVersion) { + $calendar->PRODID = '-//SabreDAV//SabreDAV '.DAV\Version::VERSION.'//EN'; + } else { + $calendar->PRODID = '-//SabreDAV//SabreDAV//EN'; + } + if (isset($properties['{DAV:}displayname'])) { + $calendar->{'X-WR-CALNAME'} = $properties['{DAV:}displayname']; + } + if (isset($properties['{http://apple.com/ns/ical/}calendar-color'])) { + $calendar->{'X-APPLE-CALENDAR-COLOR'} = $properties['{http://apple.com/ns/ical/}calendar-color']; + } + + $collectedTimezones = []; + + $timezones = []; + $objects = []; + + foreach ($inputObjects as $href => $inputObject) { + $nodeComp = VObject\Reader::read($inputObject); + + foreach ($nodeComp->children() as $child) { + switch ($child->name) { + case 'VEVENT': + case 'VTODO': + case 'VJOURNAL': + $objects[] = clone $child; + break; + + // VTIMEZONE is special, because we need to filter out the duplicates + case 'VTIMEZONE': + // Naively just checking tzid. + if (in_array((string) $child->TZID, $collectedTimezones)) { + break; + } + + $timezones[] = clone $child; + $collectedTimezones[] = $child->TZID; + break; + } + } + // Destroy circular references to PHP will GC the object. + $nodeComp->destroy(); + unset($nodeComp); + } + + foreach ($timezones as $tz) { + $calendar->add($tz); + } + foreach ($objects as $obj) { + $calendar->add($obj); + } + + return $calendar; + } + + /** + * Returns a plugin name. + * + * Using this name other plugins will be able to access other plugins + * using \Sabre\DAV\Server::getPlugin + * + * @return string + */ + public function getPluginName() + { + return 'ics-export'; + } + + /** + * Returns a bunch of meta-data about the plugin. + * + * Providing this information is optional, and is mainly displayed by the + * Browser plugin. + * + * The description key in the returned array may contain html and will not + * be sanitized. + * + * @return array + */ + public function getPluginInfo() + { + return [ + 'name' => $this->getPluginName(), + 'description' => 'Adds the ability to export CalDAV calendars as a single iCalendar file.', + 'link' => 'http://sabre.io/dav/ics-export-plugin/', + ]; + } +} diff --git a/3rdparty/sabre/dav/lib/CalDAV/ICalendar.php b/3rdparty/sabre/dav/lib/CalDAV/ICalendar.php new file mode 100644 index 00000000..8636e0ba --- /dev/null +++ b/3rdparty/sabre/dav/lib/CalDAV/ICalendar.php @@ -0,0 +1,20 @@ +caldavBackend = $caldavBackend; + $this->principalUri = $principalUri; + } + + /** + * Returns all notifications for a principal. + * + * @return array + */ + public function getChildren() + { + $children = []; + $notifications = $this->caldavBackend->getNotificationsForPrincipal($this->principalUri); + + foreach ($notifications as $notification) { + $children[] = new Node( + $this->caldavBackend, + $this->principalUri, + $notification + ); + } + + return $children; + } + + /** + * Returns the name of this object. + * + * @return string + */ + public function getName() + { + return 'notifications'; + } + + /** + * Returns the owner principal. + * + * This must be a url to a principal, or null if there's no owner + * + * @return string|null + */ + public function getOwner() + { + return $this->principalUri; + } +} diff --git a/3rdparty/sabre/dav/lib/CalDAV/Notifications/ICollection.php b/3rdparty/sabre/dav/lib/CalDAV/Notifications/ICollection.php new file mode 100644 index 00000000..b12fb390 --- /dev/null +++ b/3rdparty/sabre/dav/lib/CalDAV/Notifications/ICollection.php @@ -0,0 +1,25 @@ +caldavBackend = $caldavBackend; + $this->principalUri = $principalUri; + $this->notification = $notification; + } + + /** + * Returns the path name for this notification. + * + * @return string + */ + public function getName() + { + return $this->notification->getId().'.xml'; + } + + /** + * Returns the etag for the notification. + * + * The etag must be surrounded by literal double-quotes. + * + * @return string + */ + public function getETag() + { + return $this->notification->getETag(); + } + + /** + * This method must return an xml element, using the + * Sabre\CalDAV\Xml\Notification\NotificationInterface classes. + * + * @return NotificationInterface + */ + public function getNotificationType() + { + return $this->notification; + } + + /** + * Deletes this notification. + */ + public function delete() + { + $this->caldavBackend->deleteNotification($this->getOwner(), $this->notification); + } + + /** + * Returns the owner principal. + * + * This must be an url to a principal, or null if there's no owner + * + * @return string|null + */ + public function getOwner() + { + return $this->principalUri; + } +} diff --git a/3rdparty/sabre/dav/lib/CalDAV/Notifications/Plugin.php b/3rdparty/sabre/dav/lib/CalDAV/Notifications/Plugin.php new file mode 100644 index 00000000..56b2fe93 --- /dev/null +++ b/3rdparty/sabre/dav/lib/CalDAV/Notifications/Plugin.php @@ -0,0 +1,161 @@ +server = $server; + $server->on('method:GET', [$this, 'httpGet'], 90); + $server->on('propFind', [$this, 'propFind']); + + $server->xml->namespaceMap[self::NS_CALENDARSERVER] = 'cs'; + $server->resourceTypeMapping['\\Sabre\\CalDAV\\Notifications\\ICollection'] = '{'.self::NS_CALENDARSERVER.'}notification'; + + array_push($server->protectedProperties, + '{'.self::NS_CALENDARSERVER.'}notification-URL', + '{'.self::NS_CALENDARSERVER.'}notificationtype' + ); + } + + /** + * PropFind. + */ + public function propFind(PropFind $propFind, BaseINode $node) + { + $caldavPlugin = $this->server->getPlugin('caldav'); + + if ($node instanceof DAVACL\IPrincipal) { + $principalUrl = $node->getPrincipalUrl(); + + // notification-URL property + $propFind->handle('{'.self::NS_CALENDARSERVER.'}notification-URL', function () use ($principalUrl, $caldavPlugin) { + $notificationPath = $caldavPlugin->getCalendarHomeForPrincipal($principalUrl).'/notifications/'; + + return new DAV\Xml\Property\Href($notificationPath); + }); + } + + if ($node instanceof INode) { + $propFind->handle( + '{'.self::NS_CALENDARSERVER.'}notificationtype', + [$node, 'getNotificationType'] + ); + } + } + + /** + * This event is triggered before the usual GET request handler. + * + * We use this to intercept GET calls to notification nodes, and return the + * proper response. + */ + public function httpGet(RequestInterface $request, ResponseInterface $response) + { + $path = $request->getPath(); + + try { + $node = $this->server->tree->getNodeForPath($path); + } catch (DAV\Exception\NotFound $e) { + return; + } + + if (!$node instanceof INode) { + return; + } + + $writer = $this->server->xml->getWriter(); + $writer->contextUri = $this->server->getBaseUri(); + $writer->openMemory(); + $writer->startDocument('1.0', 'UTF-8'); + $writer->startElement('{http://calendarserver.org/ns/}notification'); + $node->getNotificationType()->xmlSerializeFull($writer); + $writer->endElement(); + + $response->setHeader('Content-Type', 'application/xml'); + $response->setHeader('ETag', $node->getETag()); + $response->setStatus(200); + $response->setBody($writer->outputMemory()); + + // Return false to break the event chain. + return false; + } + + /** + * Returns a bunch of meta-data about the plugin. + * + * Providing this information is optional, and is mainly displayed by the + * Browser plugin. + * + * The description key in the returned array may contain html and will not + * be sanitized. + * + * @return array + */ + public function getPluginInfo() + { + return [ + 'name' => $this->getPluginName(), + 'description' => 'Adds support for caldav-notifications, which is required to enable caldav-sharing.', + 'link' => 'http://sabre.io/dav/caldav-sharing/', + ]; + } +} diff --git a/3rdparty/sabre/dav/lib/CalDAV/Plugin.php b/3rdparty/sabre/dav/lib/CalDAV/Plugin.php new file mode 100644 index 00000000..ccb722f8 --- /dev/null +++ b/3rdparty/sabre/dav/lib/CalDAV/Plugin.php @@ -0,0 +1,1011 @@ +server->tree->getNodeForPath($parent); + + if ($node instanceof DAV\IExtendedCollection) { + try { + $node->getChild($name); + } catch (DAV\Exception\NotFound $e) { + return ['MKCALENDAR']; + } + } + + return []; + } + + /** + * Returns the path to a principal's calendar home. + * + * The return url must not end with a slash. + * This function should return null in case a principal did not have + * a calendar home. + * + * @param string $principalUrl + * + * @return string + */ + public function getCalendarHomeForPrincipal($principalUrl) + { + // The default behavior for most sabre/dav servers is that there is a + // principals root node, which contains users directly under it. + // + // This function assumes that there are two components in a principal + // path. If there's more, we don't return a calendar home. This + // excludes things like the calendar-proxy-read principal (which it + // should). + $parts = explode('/', trim($principalUrl, '/')); + if (2 !== count($parts)) { + return; + } + if ('principals' !== $parts[0]) { + return; + } + + return self::CALENDAR_ROOT.'/'.$parts[1]; + } + + /** + * Returns a list of features for the DAV: HTTP header. + * + * @return array + */ + public function getFeatures() + { + return ['calendar-access', 'calendar-proxy']; + } + + /** + * Returns a plugin name. + * + * Using this name other plugins will be able to access other plugins + * using DAV\Server::getPlugin + * + * @return string + */ + public function getPluginName() + { + return 'caldav'; + } + + /** + * Returns a list of reports this plugin supports. + * + * This will be used in the {DAV:}supported-report-set property. + * Note that you still need to subscribe to the 'report' event to actually + * implement them + * + * @param string $uri + * + * @return array + */ + public function getSupportedReportSet($uri) + { + $node = $this->server->tree->getNodeForPath($uri); + + $reports = []; + if ($node instanceof ICalendarObjectContainer || $node instanceof ICalendarObject) { + $reports[] = '{'.self::NS_CALDAV.'}calendar-multiget'; + $reports[] = '{'.self::NS_CALDAV.'}calendar-query'; + } + if ($node instanceof ICalendar) { + $reports[] = '{'.self::NS_CALDAV.'}free-busy-query'; + } + // iCal has a bug where it assumes that sync support is enabled, only + // if we say we support it on the calendar-home, even though this is + // not actually the case. + if ($node instanceof CalendarHome && $this->server->getPlugin('sync')) { + $reports[] = '{DAV:}sync-collection'; + } + + return $reports; + } + + /** + * Initializes the plugin. + */ + public function initialize(DAV\Server $server) + { + $this->server = $server; + + $server->on('method:MKCALENDAR', [$this, 'httpMkCalendar']); + $server->on('report', [$this, 'report']); + $server->on('propFind', [$this, 'propFind']); + $server->on('onHTMLActionsPanel', [$this, 'htmlActionsPanel']); + $server->on('beforeCreateFile', [$this, 'beforeCreateFile']); + $server->on('beforeWriteContent', [$this, 'beforeWriteContent']); + $server->on('afterMethod:GET', [$this, 'httpAfterGET']); + $server->on('getSupportedPrivilegeSet', [$this, 'getSupportedPrivilegeSet']); + + $server->xml->namespaceMap[self::NS_CALDAV] = 'cal'; + $server->xml->namespaceMap[self::NS_CALENDARSERVER] = 'cs'; + + $server->xml->elementMap['{'.self::NS_CALDAV.'}supported-calendar-component-set'] = 'Sabre\\CalDAV\\Xml\\Property\\SupportedCalendarComponentSet'; + $server->xml->elementMap['{'.self::NS_CALDAV.'}calendar-query'] = 'Sabre\\CalDAV\\Xml\\Request\\CalendarQueryReport'; + $server->xml->elementMap['{'.self::NS_CALDAV.'}calendar-multiget'] = 'Sabre\\CalDAV\\Xml\\Request\\CalendarMultiGetReport'; + $server->xml->elementMap['{'.self::NS_CALDAV.'}free-busy-query'] = 'Sabre\\CalDAV\\Xml\\Request\\FreeBusyQueryReport'; + $server->xml->elementMap['{'.self::NS_CALDAV.'}mkcalendar'] = 'Sabre\\CalDAV\\Xml\\Request\\MkCalendar'; + $server->xml->elementMap['{'.self::NS_CALDAV.'}schedule-calendar-transp'] = 'Sabre\\CalDAV\\Xml\\Property\\ScheduleCalendarTransp'; + $server->xml->elementMap['{'.self::NS_CALDAV.'}supported-calendar-component-set'] = 'Sabre\\CalDAV\\Xml\\Property\\SupportedCalendarComponentSet'; + + $server->resourceTypeMapping['\\Sabre\\CalDAV\\ICalendar'] = '{urn:ietf:params:xml:ns:caldav}calendar'; + + $server->resourceTypeMapping['\\Sabre\\CalDAV\\Principal\\IProxyRead'] = '{http://calendarserver.org/ns/}calendar-proxy-read'; + $server->resourceTypeMapping['\\Sabre\\CalDAV\\Principal\\IProxyWrite'] = '{http://calendarserver.org/ns/}calendar-proxy-write'; + + array_push($server->protectedProperties, + '{'.self::NS_CALDAV.'}supported-calendar-component-set', + '{'.self::NS_CALDAV.'}supported-calendar-data', + '{'.self::NS_CALDAV.'}max-resource-size', + '{'.self::NS_CALDAV.'}min-date-time', + '{'.self::NS_CALDAV.'}max-date-time', + '{'.self::NS_CALDAV.'}max-instances', + '{'.self::NS_CALDAV.'}max-attendees-per-instance', + '{'.self::NS_CALDAV.'}calendar-home-set', + '{'.self::NS_CALDAV.'}supported-collation-set', + '{'.self::NS_CALDAV.'}calendar-data', + + // CalendarServer extensions + '{'.self::NS_CALENDARSERVER.'}getctag', + '{'.self::NS_CALENDARSERVER.'}calendar-proxy-read-for', + '{'.self::NS_CALENDARSERVER.'}calendar-proxy-write-for' + ); + + if ($aclPlugin = $server->getPlugin('acl')) { + $aclPlugin->principalSearchPropertySet['{'.self::NS_CALDAV.'}calendar-user-address-set'] = 'Calendar address'; + } + } + + /** + * This functions handles REPORT requests specific to CalDAV. + * + * @param string $reportName + * @param mixed $report + * @param mixed $path + * + * @return bool|null + */ + public function report($reportName, $report, $path) + { + switch ($reportName) { + case '{'.self::NS_CALDAV.'}calendar-multiget': + $this->server->transactionType = 'report-calendar-multiget'; + $this->calendarMultiGetReport($report); + + return false; + case '{'.self::NS_CALDAV.'}calendar-query': + $this->server->transactionType = 'report-calendar-query'; + $this->calendarQueryReport($report); + + return false; + case '{'.self::NS_CALDAV.'}free-busy-query': + $this->server->transactionType = 'report-free-busy-query'; + $this->freeBusyQueryReport($report); + + return false; + } + } + + /** + * This function handles the MKCALENDAR HTTP method, which creates + * a new calendar. + * + * @return bool + */ + public function httpMkCalendar(RequestInterface $request, ResponseInterface $response) + { + $body = $request->getBodyAsString(); + $path = $request->getPath(); + + $properties = []; + + if ($body) { + try { + $mkcalendar = $this->server->xml->expect( + '{urn:ietf:params:xml:ns:caldav}mkcalendar', + $body + ); + } catch (\Sabre\Xml\ParseException $e) { + throw new BadRequest($e->getMessage(), 0, $e); + } + $properties = $mkcalendar->getProperties(); + } + + // iCal abuses MKCALENDAR since iCal 10.9.2 to create server-stored + // subscriptions. Before that it used MKCOL which was the correct way + // to do this. + // + // If the body had a {DAV:}resourcetype, it means we stumbled upon this + // request, and we simply use it instead of the pre-defined list. + if (isset($properties['{DAV:}resourcetype'])) { + $resourceType = $properties['{DAV:}resourcetype']->getValue(); + } else { + $resourceType = ['{DAV:}collection', '{urn:ietf:params:xml:ns:caldav}calendar']; + } + + $this->server->createCollection($path, new MkCol($resourceType, $properties)); + + $response->setStatus(201); + $response->setHeader('Content-Length', 0); + + // This breaks the method chain. + return false; + } + + /** + * PropFind. + * + * This method handler is invoked before any after properties for a + * resource are fetched. This allows us to add in any CalDAV specific + * properties. + */ + public function propFind(DAV\PropFind $propFind, DAV\INode $node) + { + $ns = '{'.self::NS_CALDAV.'}'; + + if ($node instanceof ICalendarObjectContainer) { + $propFind->handle($ns.'max-resource-size', $this->maxResourceSize); + $propFind->handle($ns.'supported-calendar-data', function () { + return new Xml\Property\SupportedCalendarData(); + }); + $propFind->handle($ns.'supported-collation-set', function () { + return new Xml\Property\SupportedCollationSet(); + }); + } + + if ($node instanceof DAVACL\IPrincipal) { + $principalUrl = $node->getPrincipalUrl(); + + $propFind->handle('{'.self::NS_CALDAV.'}calendar-home-set', function () use ($principalUrl) { + $calendarHomePath = $this->getCalendarHomeForPrincipal($principalUrl); + if (is_null($calendarHomePath)) { + return null; + } + + return new LocalHref($calendarHomePath.'/'); + }); + // The calendar-user-address-set property is basically mapped to + // the {DAV:}alternate-URI-set property. + $propFind->handle('{'.self::NS_CALDAV.'}calendar-user-address-set', function () use ($node) { + $addresses = $node->getAlternateUriSet(); + $addresses[] = $this->server->getBaseUri().$node->getPrincipalUrl().'/'; + + return new LocalHref($addresses); + }); + // For some reason somebody thought it was a good idea to add + // another one of these properties. We're supporting it too. + $propFind->handle('{'.self::NS_CALENDARSERVER.'}email-address-set', function () use ($node) { + $addresses = $node->getAlternateUriSet(); + $emails = []; + foreach ($addresses as $address) { + if ('mailto:' === substr($address, 0, 7)) { + $emails[] = substr($address, 7); + } + } + + return new Xml\Property\EmailAddressSet($emails); + }); + + // These two properties are shortcuts for ical to easily find + // other principals this principal has access to. + $propRead = '{'.self::NS_CALENDARSERVER.'}calendar-proxy-read-for'; + $propWrite = '{'.self::NS_CALENDARSERVER.'}calendar-proxy-write-for'; + + if (404 === $propFind->getStatus($propRead) || 404 === $propFind->getStatus($propWrite)) { + $aclPlugin = $this->server->getPlugin('acl'); + $membership = $aclPlugin->getPrincipalMembership($propFind->getPath()); + $readList = []; + $writeList = []; + + foreach ($membership as $group) { + $groupNode = $this->server->tree->getNodeForPath($group); + + $listItem = Uri\split($group)[0].'/'; + + // If the node is either ap proxy-read or proxy-write + // group, we grab the parent principal and add it to the + // list. + if ($groupNode instanceof Principal\IProxyRead) { + $readList[] = $listItem; + } + if ($groupNode instanceof Principal\IProxyWrite) { + $writeList[] = $listItem; + } + } + + $propFind->set($propRead, new LocalHref($readList)); + $propFind->set($propWrite, new LocalHref($writeList)); + } + } // instanceof IPrincipal + + if ($node instanceof ICalendarObject) { + // The calendar-data property is not supposed to be a 'real' + // property, but in large chunks of the spec it does act as such. + // Therefore we simply expose it as a property. + $propFind->handle('{'.self::NS_CALDAV.'}calendar-data', function () use ($node) { + $val = $node->get(); + if (is_resource($val)) { + $val = stream_get_contents($val); + } + + // Taking out \r to not screw up the xml output + return str_replace("\r", '', $val); + }); + } + } + + /** + * This function handles the calendar-multiget REPORT. + * + * This report is used by the client to fetch the content of a series + * of urls. Effectively avoiding a lot of redundant requests. + * + * @param CalendarMultiGetReport $report + */ + public function calendarMultiGetReport($report) + { + $needsJson = 'application/calendar+json' === $report->contentType; + + $timeZones = []; + $propertyList = []; + + $paths = array_map( + [$this->server, 'calculateUri'], + $report->hrefs + ); + + foreach ($this->server->getPropertiesForMultiplePaths($paths, $report->properties) as $uri => $objProps) { + if (($needsJson || $report->expand) && isset($objProps[200]['{'.self::NS_CALDAV.'}calendar-data'])) { + $vObject = VObject\Reader::read($objProps[200]['{'.self::NS_CALDAV.'}calendar-data']); + + if ($report->expand) { + // We're expanding, and for that we need to figure out the + // calendar's timezone. + list($calendarPath) = Uri\split($uri); + if (!isset($timeZones[$calendarPath])) { + // Checking the calendar-timezone property. + $tzProp = '{'.self::NS_CALDAV.'}calendar-timezone'; + $tzResult = $this->server->getProperties($calendarPath, [$tzProp]); + if (isset($tzResult[$tzProp])) { + // This property contains a VCALENDAR with a single + // VTIMEZONE. + $vtimezoneObj = VObject\Reader::read($tzResult[$tzProp]); + $timeZone = $vtimezoneObj->VTIMEZONE->getTimeZone(); + } else { + // Defaulting to UTC. + $timeZone = new DateTimeZone('UTC'); + } + $timeZones[$calendarPath] = $timeZone; + } + + $vObject = $vObject->expand($report->expand['start'], $report->expand['end'], $timeZones[$calendarPath]); + } + if ($needsJson) { + $objProps[200]['{'.self::NS_CALDAV.'}calendar-data'] = json_encode($vObject->jsonSerialize()); + } else { + $objProps[200]['{'.self::NS_CALDAV.'}calendar-data'] = $vObject->serialize(); + } + // Destroy circular references so PHP will garbage collect the + // object. + $vObject->destroy(); + } + + $propertyList[] = $objProps; + } + + $prefer = $this->server->getHTTPPrefer(); + + $this->server->httpResponse->setStatus(207); + $this->server->httpResponse->setHeader('Content-Type', 'application/xml; charset=utf-8'); + $this->server->httpResponse->setHeader('Vary', 'Brief,Prefer'); + $this->server->httpResponse->setBody($this->server->generateMultiStatus($propertyList, 'minimal' === $prefer['return'])); + } + + /** + * This function handles the calendar-query REPORT. + * + * This report is used by clients to request calendar objects based on + * complex conditions. + * + * @param Xml\Request\CalendarQueryReport $report + */ + public function calendarQueryReport($report) + { + $path = $this->server->getRequestUri(); + + $needsJson = 'application/calendar+json' === $report->contentType; + + $node = $this->server->tree->getNodeForPath($this->server->getRequestUri()); + $depth = $this->server->getHTTPDepth(0); + + // The default result is an empty array + $result = []; + + $calendarTimeZone = null; + if ($report->expand) { + // We're expanding, and for that we need to figure out the + // calendar's timezone. + $tzProp = '{'.self::NS_CALDAV.'}calendar-timezone'; + $tzResult = $this->server->getProperties($path, [$tzProp]); + if (isset($tzResult[$tzProp])) { + // This property contains a VCALENDAR with a single + // VTIMEZONE. + $vtimezoneObj = VObject\Reader::read($tzResult[$tzProp]); + $calendarTimeZone = $vtimezoneObj->VTIMEZONE->getTimeZone(); + + // Destroy circular references so PHP will garbage collect the + // object. + $vtimezoneObj->destroy(); + } else { + // Defaulting to UTC. + $calendarTimeZone = new DateTimeZone('UTC'); + } + } + + // The calendarobject was requested directly. In this case we handle + // this locally. + if (0 == $depth && $node instanceof ICalendarObject) { + $requestedCalendarData = true; + $requestedProperties = $report->properties; + + if (!in_array('{urn:ietf:params:xml:ns:caldav}calendar-data', $requestedProperties)) { + // We always retrieve calendar-data, as we need it for filtering. + $requestedProperties[] = '{urn:ietf:params:xml:ns:caldav}calendar-data'; + + // If calendar-data wasn't explicitly requested, we need to remove + // it after processing. + $requestedCalendarData = false; + } + + $properties = $this->server->getPropertiesForPath( + $path, + $requestedProperties, + 0 + ); + + // This array should have only 1 element, the first calendar + // object. + $properties = current($properties); + + // If there wasn't any calendar-data returned somehow, we ignore + // this. + if (isset($properties[200]['{urn:ietf:params:xml:ns:caldav}calendar-data'])) { + $validator = new CalendarQueryValidator(); + + $vObject = VObject\Reader::read($properties[200]['{urn:ietf:params:xml:ns:caldav}calendar-data']); + if ($validator->validate($vObject, $report->filters)) { + // If the client didn't require the calendar-data property, + // we won't give it back. + if (!$requestedCalendarData) { + unset($properties[200]['{urn:ietf:params:xml:ns:caldav}calendar-data']); + } else { + if ($report->expand) { + $vObject = $vObject->expand($report->expand['start'], $report->expand['end'], $calendarTimeZone); + } + if ($needsJson) { + $properties[200]['{'.self::NS_CALDAV.'}calendar-data'] = json_encode($vObject->jsonSerialize()); + } elseif ($report->expand) { + $properties[200]['{'.self::NS_CALDAV.'}calendar-data'] = $vObject->serialize(); + } + } + + $result = [$properties]; + } + // Destroy circular references so PHP will garbage collect the + // object. + $vObject->destroy(); + } + } + + if ($node instanceof ICalendarObjectContainer && 0 === $depth) { + if (0 === strpos((string) $this->server->httpRequest->getHeader('User-Agent'), 'MSFT-')) { + // Microsoft clients incorrectly supplied depth as 0, when it actually + // should have set depth to 1. We're implementing a workaround here + // to deal with this. + // + // This targets at least the following clients: + // Windows 10 + // Windows Phone 8, 10 + $depth = 1; + } else { + throw new BadRequest('A calendar-query REPORT on a calendar with a Depth: 0 is undefined. Set Depth to 1'); + } + } + + // If we're dealing with a calendar, the calendar itself is responsible + // for the calendar-query. + if ($node instanceof ICalendarObjectContainer && 1 == $depth) { + $nodePaths = $node->calendarQuery($report->filters); + + foreach ($nodePaths as $path) { + list($properties) = + $this->server->getPropertiesForPath($this->server->getRequestUri().'/'.$path, $report->properties); + + if (($needsJson || $report->expand)) { + $vObject = VObject\Reader::read($properties[200]['{'.self::NS_CALDAV.'}calendar-data']); + + if ($report->expand) { + $vObject = $vObject->expand($report->expand['start'], $report->expand['end'], $calendarTimeZone); + } + + if ($needsJson) { + $properties[200]['{'.self::NS_CALDAV.'}calendar-data'] = json_encode($vObject->jsonSerialize()); + } else { + $properties[200]['{'.self::NS_CALDAV.'}calendar-data'] = $vObject->serialize(); + } + + // Destroy circular references so PHP will garbage collect the + // object. + $vObject->destroy(); + } + $result[] = $properties; + } + } + + $prefer = $this->server->getHTTPPrefer(); + + $this->server->httpResponse->setStatus(207); + $this->server->httpResponse->setHeader('Content-Type', 'application/xml; charset=utf-8'); + $this->server->httpResponse->setHeader('Vary', 'Brief,Prefer'); + $this->server->httpResponse->setBody($this->server->generateMultiStatus($result, 'minimal' === $prefer['return'])); + } + + /** + * This method is responsible for parsing the request and generating the + * response for the CALDAV:free-busy-query REPORT. + */ + protected function freeBusyQueryReport(Xml\Request\FreeBusyQueryReport $report) + { + $uri = $this->server->getRequestUri(); + + $acl = $this->server->getPlugin('acl'); + if ($acl) { + $acl->checkPrivileges($uri, '{'.self::NS_CALDAV.'}read-free-busy'); + } + + $calendar = $this->server->tree->getNodeForPath($uri); + if (!$calendar instanceof ICalendar) { + throw new DAV\Exception\NotImplemented('The free-busy-query REPORT is only implemented on calendars'); + } + + $tzProp = '{'.self::NS_CALDAV.'}calendar-timezone'; + + // Figuring out the default timezone for the calendar, for floating + // times. + $calendarProps = $this->server->getProperties($uri, [$tzProp]); + + if (isset($calendarProps[$tzProp])) { + $vtimezoneObj = VObject\Reader::read($calendarProps[$tzProp]); + $calendarTimeZone = $vtimezoneObj->VTIMEZONE->getTimeZone(); + // Destroy circular references so PHP will garbage collect the object. + $vtimezoneObj->destroy(); + } else { + $calendarTimeZone = new DateTimeZone('UTC'); + } + + // Doing a calendar-query first, to make sure we get the most + // performance. + $urls = $calendar->calendarQuery([ + 'name' => 'VCALENDAR', + 'comp-filters' => [ + [ + 'name' => 'VEVENT', + 'comp-filters' => [], + 'prop-filters' => [], + 'is-not-defined' => false, + 'time-range' => [ + 'start' => $report->start, + 'end' => $report->end, + ], + ], + ], + 'prop-filters' => [], + 'is-not-defined' => false, + 'time-range' => null, + ]); + + $objects = array_map(function ($url) use ($calendar) { + $obj = $calendar->getChild($url)->get(); + + return $obj; + }, $urls); + + $generator = new VObject\FreeBusyGenerator(); + $generator->setObjects($objects); + $generator->setTimeRange($report->start, $report->end); + $generator->setTimeZone($calendarTimeZone); + $result = $generator->getResult(); + $result = $result->serialize(); + + $this->server->httpResponse->setStatus(200); + $this->server->httpResponse->setHeader('Content-Type', 'text/calendar'); + $this->server->httpResponse->setHeader('Content-Length', strlen($result)); + $this->server->httpResponse->setBody($result); + } + + /** + * This method is triggered before a file gets updated with new content. + * + * This plugin uses this method to ensure that CalDAV objects receive + * valid calendar data. + * + * @param string $path + * @param resource $data + * @param bool $modified should be set to true, if this event handler + * changed &$data + */ + public function beforeWriteContent($path, DAV\IFile $node, &$data, &$modified) + { + if (!$node instanceof ICalendarObject) { + return; + } + + // We're only interested in ICalendarObject nodes that are inside of a + // real calendar. This is to avoid triggering validation and scheduling + // for non-calendars (such as an inbox). + list($parent) = Uri\split($path); + $parentNode = $this->server->tree->getNodeForPath($parent); + + if (!$parentNode instanceof ICalendar) { + return; + } + + $this->validateICalendar( + $data, + $path, + $modified, + $this->server->httpRequest, + $this->server->httpResponse, + false + ); + } + + /** + * This method is triggered before a new file is created. + * + * This plugin uses this method to ensure that newly created calendar + * objects contain valid calendar data. + * + * @param string $path + * @param resource $data + * @param bool $modified should be set to true, if this event handler + * changed &$data + */ + public function beforeCreateFile($path, &$data, DAV\ICollection $parentNode, &$modified) + { + if (!$parentNode instanceof ICalendar) { + return; + } + + $this->validateICalendar( + $data, + $path, + $modified, + $this->server->httpRequest, + $this->server->httpResponse, + true + ); + } + + /** + * Checks if the submitted iCalendar data is in fact, valid. + * + * An exception is thrown if it's not. + * + * @param resource|string $data + * @param string $path + * @param bool $modified should be set to true, if this event handler + * changed &$data + * @param RequestInterface $request the http request + * @param ResponseInterface $response the http response + * @param bool $isNew is the item a new one, or an update + */ + protected function validateICalendar(&$data, $path, &$modified, RequestInterface $request, ResponseInterface $response, $isNew) + { + // If it's a stream, we convert it to a string first. + if (is_resource($data)) { + $data = stream_get_contents($data); + } + + $before = $data; + + try { + // If the data starts with a [, we can reasonably assume we're dealing + // with a jCal object. + if ('[' === substr($data, 0, 1)) { + $vobj = VObject\Reader::readJson($data); + + // Converting $data back to iCalendar, as that's what we + // technically support everywhere. + $data = $vobj->serialize(); + $modified = true; + } else { + $vobj = VObject\Reader::read($data); + } + } catch (VObject\ParseException $e) { + throw new DAV\Exception\UnsupportedMediaType('This resource only supports valid iCalendar 2.0 data. Parse error: '.$e->getMessage()); + } + + if ('VCALENDAR' !== $vobj->name) { + throw new DAV\Exception\UnsupportedMediaType('This collection can only support iCalendar objects.'); + } + + $sCCS = '{urn:ietf:params:xml:ns:caldav}supported-calendar-component-set'; + + // Get the Supported Components for the target calendar + list($parentPath) = Uri\split($path); + $calendarProperties = $this->server->getProperties($parentPath, [$sCCS]); + + if (isset($calendarProperties[$sCCS])) { + $supportedComponents = $calendarProperties[$sCCS]->getValue(); + } else { + $supportedComponents = ['VJOURNAL', 'VTODO', 'VEVENT']; + } + + $foundType = null; + + foreach ($vobj->getComponents() as $component) { + switch ($component->name) { + case 'VTIMEZONE': + continue 2; + case 'VEVENT': + case 'VTODO': + case 'VJOURNAL': + $foundType = $component->name; + break; + } + } + + if (!$foundType || !in_array($foundType, $supportedComponents)) { + throw new Exception\InvalidComponentType('iCalendar objects must at least have a component of type '.implode(', ', $supportedComponents)); + } + + $options = VObject\Node::PROFILE_CALDAV; + $prefer = $this->server->getHTTPPrefer(); + + if ('strict' !== $prefer['handling']) { + $options |= VObject\Node::REPAIR; + } + + $messages = $vobj->validate($options); + + $highestLevel = 0; + $warningMessage = null; + + // $messages contains a list of problems with the vcard, along with + // their severity. + foreach ($messages as $message) { + if ($message['level'] > $highestLevel) { + // Recording the highest reported error level. + $highestLevel = $message['level']; + $warningMessage = $message['message']; + } + switch ($message['level']) { + case 1: + // Level 1 means that there was a problem, but it was repaired. + $modified = true; + break; + case 2: + // Level 2 means a warning, but not critical + break; + case 3: + // Level 3 means a critical error + throw new DAV\Exception\UnsupportedMediaType('Validation error in iCalendar: '.$message['message']); + } + } + if ($warningMessage) { + $response->setHeader( + 'X-Sabre-Ew-Gross', + 'iCalendar validation warning: '.$warningMessage + ); + } + + // We use an extra variable to allow event handles to tell us whether + // the object was modified or not. + // + // This helps us determine if we need to re-serialize the object. + $subModified = false; + + $this->server->emit( + 'calendarObjectChange', + [ + $request, + $response, + $vobj, + $parentPath, + &$subModified, + $isNew, + ] + ); + + if ($modified || $subModified) { + // An event handler told us that it modified the object. + $data = $vobj->serialize(); + + // Using md5 to figure out if there was an *actual* change. + if (!$modified && 0 !== strcmp($data, $before)) { + $modified = true; + } + } + + // Destroy circular references so PHP will garbage collect the object. + $vobj->destroy(); + } + + /** + * This method is triggered whenever a subsystem requests the privileges + * that are supported on a particular node. + */ + public function getSupportedPrivilegeSet(INode $node, array &$supportedPrivilegeSet) + { + if ($node instanceof ICalendar) { + $supportedPrivilegeSet['{DAV:}read']['aggregates']['{'.self::NS_CALDAV.'}read-free-busy'] = [ + 'abstract' => false, + 'aggregates' => [], + ]; + } + } + + /** + * This method is used to generate HTML output for the + * DAV\Browser\Plugin. This allows us to generate an interface users + * can use to create new calendars. + * + * @param string $output + * + * @return bool + */ + public function htmlActionsPanel(DAV\INode $node, &$output) + { + if (!$node instanceof CalendarHome) { + return; + } + + $output .= '
    +

    Create new calendar

    + + +
    +
    + +
    + '; + + return false; + } + + /** + * This event is triggered after GET requests. + * + * This is used to transform data into jCal, if this was requested. + */ + public function httpAfterGet(RequestInterface $request, ResponseInterface $response) + { + $contentType = $response->getHeader('Content-Type'); + if (null === $contentType || false === strpos($contentType, 'text/calendar')) { + return; + } + + $result = HTTP\negotiateContentType( + $request->getHeader('Accept'), + ['text/calendar', 'application/calendar+json'] + ); + + if ('application/calendar+json' !== $result) { + // Do nothing + return; + } + + // Transforming. + $vobj = VObject\Reader::read($response->getBody()); + + $jsonBody = json_encode($vobj->jsonSerialize()); + $response->setBody($jsonBody); + + // Destroy circular references so PHP will garbage collect the object. + $vobj->destroy(); + + $response->setHeader('Content-Type', 'application/calendar+json'); + $response->setHeader('Content-Length', strlen($jsonBody)); + } + + /** + * Returns a bunch of meta-data about the plugin. + * + * Providing this information is optional, and is mainly displayed by the + * Browser plugin. + * + * The description key in the returned array may contain html and will not + * be sanitized. + * + * @return array + */ + public function getPluginInfo() + { + return [ + 'name' => $this->getPluginName(), + 'description' => 'Adds support for CalDAV (rfc4791)', + 'link' => 'http://sabre.io/dav/caldav/', + ]; + } +} diff --git a/3rdparty/sabre/dav/lib/CalDAV/Principal/Collection.php b/3rdparty/sabre/dav/lib/CalDAV/Principal/Collection.php new file mode 100644 index 00000000..6d023033 --- /dev/null +++ b/3rdparty/sabre/dav/lib/CalDAV/Principal/Collection.php @@ -0,0 +1,32 @@ +principalBackend, $principalInfo); + } +} diff --git a/3rdparty/sabre/dav/lib/CalDAV/Principal/IProxyRead.php b/3rdparty/sabre/dav/lib/CalDAV/Principal/IProxyRead.php new file mode 100644 index 00000000..96e6991c --- /dev/null +++ b/3rdparty/sabre/dav/lib/CalDAV/Principal/IProxyRead.php @@ -0,0 +1,21 @@ +principalInfo = $principalInfo; + $this->principalBackend = $principalBackend; + } + + /** + * Returns this principals name. + * + * @return string + */ + public function getName() + { + return 'calendar-proxy-read'; + } + + /** + * Returns the last modification time. + */ + public function getLastModified() + { + return null; + } + + /** + * Deletes the current node. + * + * @throws DAV\Exception\Forbidden + */ + public function delete() + { + throw new DAV\Exception\Forbidden('Permission denied to delete node'); + } + + /** + * Renames the node. + * + * @param string $name The new name + * + * @throws DAV\Exception\Forbidden + */ + public function setName($name) + { + throw new DAV\Exception\Forbidden('Permission denied to rename file'); + } + + /** + * Returns a list of alternative urls for a principal. + * + * This can for example be an email address, or ldap url. + * + * @return array + */ + public function getAlternateUriSet() + { + return []; + } + + /** + * Returns the full principal url. + * + * @return string + */ + public function getPrincipalUrl() + { + return $this->principalInfo['uri'].'/'.$this->getName(); + } + + /** + * Returns the list of group members. + * + * If this principal is a group, this function should return + * all member principal uri's for the group. + * + * @return array + */ + public function getGroupMemberSet() + { + return $this->principalBackend->getGroupMemberSet($this->getPrincipalUrl()); + } + + /** + * Returns the list of groups this principal is member of. + * + * If this principal is a member of a (list of) groups, this function + * should return a list of principal uri's for it's members. + * + * @return array + */ + public function getGroupMembership() + { + return $this->principalBackend->getGroupMembership($this->getPrincipalUrl()); + } + + /** + * Sets a list of group members. + * + * If this principal is a group, this method sets all the group members. + * The list of members is always overwritten, never appended to. + * + * This method should throw an exception if the members could not be set. + */ + public function setGroupMemberSet(array $principals) + { + $this->principalBackend->setGroupMemberSet($this->getPrincipalUrl(), $principals); + } + + /** + * Returns the displayname. + * + * This should be a human readable name for the principal. + * If none is available, return the nodename. + * + * @return string + */ + public function getDisplayName() + { + return $this->getName(); + } +} diff --git a/3rdparty/sabre/dav/lib/CalDAV/Principal/ProxyWrite.php b/3rdparty/sabre/dav/lib/CalDAV/Principal/ProxyWrite.php new file mode 100644 index 00000000..2d1ce7c4 --- /dev/null +++ b/3rdparty/sabre/dav/lib/CalDAV/Principal/ProxyWrite.php @@ -0,0 +1,161 @@ +principalInfo = $principalInfo; + $this->principalBackend = $principalBackend; + } + + /** + * Returns this principals name. + * + * @return string + */ + public function getName() + { + return 'calendar-proxy-write'; + } + + /** + * Returns the last modification time. + */ + public function getLastModified() + { + return null; + } + + /** + * Deletes the current node. + * + * @throws DAV\Exception\Forbidden + */ + public function delete() + { + throw new DAV\Exception\Forbidden('Permission denied to delete node'); + } + + /** + * Renames the node. + * + * @param string $name The new name + * + * @throws DAV\Exception\Forbidden + */ + public function setName($name) + { + throw new DAV\Exception\Forbidden('Permission denied to rename file'); + } + + /** + * Returns a list of alternative urls for a principal. + * + * This can for example be an email address, or ldap url. + * + * @return array + */ + public function getAlternateUriSet() + { + return []; + } + + /** + * Returns the full principal url. + * + * @return string + */ + public function getPrincipalUrl() + { + return $this->principalInfo['uri'].'/'.$this->getName(); + } + + /** + * Returns the list of group members. + * + * If this principal is a group, this function should return + * all member principal uri's for the group. + * + * @return array + */ + public function getGroupMemberSet() + { + return $this->principalBackend->getGroupMemberSet($this->getPrincipalUrl()); + } + + /** + * Returns the list of groups this principal is member of. + * + * If this principal is a member of a (list of) groups, this function + * should return a list of principal uri's for it's members. + * + * @return array + */ + public function getGroupMembership() + { + return $this->principalBackend->getGroupMembership($this->getPrincipalUrl()); + } + + /** + * Sets a list of group members. + * + * If this principal is a group, this method sets all the group members. + * The list of members is always overwritten, never appended to. + * + * This method should throw an exception if the members could not be set. + */ + public function setGroupMemberSet(array $principals) + { + $this->principalBackend->setGroupMemberSet($this->getPrincipalUrl(), $principals); + } + + /** + * Returns the displayname. + * + * This should be a human readable name for the principal. + * If none is available, return the nodename. + * + * @return string + */ + public function getDisplayName() + { + return $this->getName(); + } +} diff --git a/3rdparty/sabre/dav/lib/CalDAV/Principal/User.php b/3rdparty/sabre/dav/lib/CalDAV/Principal/User.php new file mode 100644 index 00000000..88bf4b4f --- /dev/null +++ b/3rdparty/sabre/dav/lib/CalDAV/Principal/User.php @@ -0,0 +1,136 @@ +principalBackend->getPrincipalByPath($this->getPrincipalURL().'/'.$name); + if (!$principal) { + throw new DAV\Exception\NotFound('Node with name '.$name.' was not found'); + } + if ('calendar-proxy-read' === $name) { + return new ProxyRead($this->principalBackend, $this->principalProperties); + } + + if ('calendar-proxy-write' === $name) { + return new ProxyWrite($this->principalBackend, $this->principalProperties); + } + + throw new DAV\Exception\NotFound('Node with name '.$name.' was not found'); + } + + /** + * Returns an array with all the child nodes. + * + * @return DAV\INode[] + */ + public function getChildren() + { + $r = []; + if ($this->principalBackend->getPrincipalByPath($this->getPrincipalURL().'/calendar-proxy-read')) { + $r[] = new ProxyRead($this->principalBackend, $this->principalProperties); + } + if ($this->principalBackend->getPrincipalByPath($this->getPrincipalURL().'/calendar-proxy-write')) { + $r[] = new ProxyWrite($this->principalBackend, $this->principalProperties); + } + + return $r; + } + + /** + * Returns whether or not the child node exists. + * + * @param string $name + * + * @return bool + */ + public function childExists($name) + { + try { + $this->getChild($name); + + return true; + } catch (DAV\Exception\NotFound $e) { + return false; + } + } + + /** + * Returns a list of ACE's for this node. + * + * Each ACE has the following properties: + * * 'privilege', a string such as {DAV:}read or {DAV:}write. These are + * currently the only supported privileges + * * 'principal', a url to the principal who owns the node + * * 'protected' (optional), indicating that this ACE is not allowed to + * be updated. + * + * @return array + */ + public function getACL() + { + $acl = parent::getACL(); + $acl[] = [ + 'privilege' => '{DAV:}read', + 'principal' => $this->principalProperties['uri'].'/calendar-proxy-read', + 'protected' => true, + ]; + $acl[] = [ + 'privilege' => '{DAV:}read', + 'principal' => $this->principalProperties['uri'].'/calendar-proxy-write', + 'protected' => true, + ]; + + return $acl; + } +} diff --git a/3rdparty/sabre/dav/lib/CalDAV/Schedule/IInbox.php b/3rdparty/sabre/dav/lib/CalDAV/Schedule/IInbox.php new file mode 100644 index 00000000..64a94bec --- /dev/null +++ b/3rdparty/sabre/dav/lib/CalDAV/Schedule/IInbox.php @@ -0,0 +1,17 @@ +senderEmail = $senderEmail; + } + + /* + * This initializes the plugin. + * + * This function is called by Sabre\DAV\Server, after + * addPlugin is called. + * + * This method should set up the required event subscriptions. + * + * @param DAV\Server $server + * @return void + */ + public function initialize(DAV\Server $server) + { + $server->on('schedule', [$this, 'schedule'], 120); + } + + /** + * Returns a plugin name. + * + * Using this name other plugins will be able to access other plugins + * using \Sabre\DAV\Server::getPlugin + * + * @return string + */ + public function getPluginName() + { + return 'imip'; + } + + /** + * Event handler for the 'schedule' event. + */ + public function schedule(ITip\Message $iTipMessage) + { + // Not sending any emails if the system considers the update + // insignificant. + if (!$iTipMessage->significantChange) { + if (!$iTipMessage->scheduleStatus) { + $iTipMessage->scheduleStatus = '1.0;We got the message, but it\'s not significant enough to warrant an email'; + } + + return; + } + + $summary = $iTipMessage->message->VEVENT->SUMMARY; + + if ('mailto' !== parse_url($iTipMessage->sender, PHP_URL_SCHEME)) { + return; + } + + if ('mailto' !== parse_url($iTipMessage->recipient, PHP_URL_SCHEME)) { + return; + } + + $sender = substr($iTipMessage->sender, 7); + $recipient = substr($iTipMessage->recipient, 7); + + if ($iTipMessage->senderName) { + $sender = $iTipMessage->senderName.' <'.$sender.'>'; + } + if ($iTipMessage->recipientName && $iTipMessage->recipientName != $recipient) { + $recipient = $iTipMessage->recipientName.' <'.$recipient.'>'; + } + + $subject = 'SabreDAV iTIP message'; + switch (strtoupper($iTipMessage->method)) { + case 'REPLY': + $subject = 'Re: '.$summary; + break; + case 'REQUEST': + $subject = 'Invitation: '.$summary; + break; + case 'CANCEL': + $subject = 'Cancelled: '.$summary; + break; + } + + $headers = [ + 'Reply-To: '.$sender, + 'From: '.$iTipMessage->senderName.' <'.$this->senderEmail.'>', + 'MIME-Version: 1.0', + 'Content-Type: text/calendar; charset=UTF-8; method='.$iTipMessage->method, + ]; + if (DAV\Server::$exposeVersion) { + $headers[] = 'X-Sabre-Version: '.DAV\Version::VERSION; + } + $this->mail( + $recipient, + $subject, + $iTipMessage->message->serialize(), + $headers + ); + $iTipMessage->scheduleStatus = '1.1; Scheduling message is sent via iMip'; + } + + // @codeCoverageIgnoreStart + // This is deemed untestable in a reasonable manner + + /** + * This function is responsible for sending the actual email. + * + * @param string $to Recipient email address + * @param string $subject Subject of the email + * @param string $body iCalendar body + * @param array $headers List of headers + */ + protected function mail($to, $subject, $body, array $headers) + { + mail($to, $subject, $body, implode("\r\n", $headers)); + } + + // @codeCoverageIgnoreEnd + + /** + * Returns a bunch of meta-data about the plugin. + * + * Providing this information is optional, and is mainly displayed by the + * Browser plugin. + * + * The description key in the returned array may contain html and will not + * be sanitized. + * + * @return array + */ + public function getPluginInfo() + { + return [ + 'name' => $this->getPluginName(), + 'description' => 'Email delivery (rfc6047) for CalDAV scheduling', + 'link' => 'http://sabre.io/dav/scheduling/', + ]; + } +} diff --git a/3rdparty/sabre/dav/lib/CalDAV/Schedule/IOutbox.php b/3rdparty/sabre/dav/lib/CalDAV/Schedule/IOutbox.php new file mode 100644 index 00000000..384b503d --- /dev/null +++ b/3rdparty/sabre/dav/lib/CalDAV/Schedule/IOutbox.php @@ -0,0 +1,17 @@ +caldavBackend = $caldavBackend; + $this->principalUri = $principalUri; + } + + /** + * Returns the name of the node. + * + * This is used to generate the url. + * + * @return string + */ + public function getName() + { + return 'inbox'; + } + + /** + * Returns an array with all the child nodes. + * + * @return \Sabre\DAV\INode[] + */ + public function getChildren() + { + $objs = $this->caldavBackend->getSchedulingObjects($this->principalUri); + $children = []; + foreach ($objs as $obj) { + //$obj['acl'] = $this->getACL(); + $obj['principaluri'] = $this->principalUri; + $children[] = new SchedulingObject($this->caldavBackend, $obj); + } + + return $children; + } + + /** + * Creates a new file in the directory. + * + * Data will either be supplied as a stream resource, or in certain cases + * as a string. Keep in mind that you may have to support either. + * + * After successful creation of the file, you may choose to return the ETag + * of the new file here. + * + * The returned ETag must be surrounded by double-quotes (The quotes should + * be part of the actual string). + * + * If you cannot accurately determine the ETag, you should not return it. + * If you don't store the file exactly as-is (you're transforming it + * somehow) you should also not return an ETag. + * + * This means that if a subsequent GET to this new file does not exactly + * return the same contents of what was submitted here, you are strongly + * recommended to omit the ETag. + * + * @param string $name Name of the file + * @param resource|string $data Initial payload + * + * @return string|null + */ + public function createFile($name, $data = null) + { + $this->caldavBackend->createSchedulingObject($this->principalUri, $name, $data); + } + + /** + * Returns the owner principal. + * + * This must be a url to a principal, or null if there's no owner + * + * @return string|null + */ + public function getOwner() + { + return $this->principalUri; + } + + /** + * Returns a list of ACE's for this node. + * + * Each ACE has the following properties: + * * 'privilege', a string such as {DAV:}read or {DAV:}write. These are + * currently the only supported privileges + * * 'principal', a url to the principal who owns the node + * * 'protected' (optional), indicating that this ACE is not allowed to + * be updated. + * + * @return array + */ + public function getACL() + { + return [ + [ + 'privilege' => '{DAV:}read', + 'principal' => '{DAV:}authenticated', + 'protected' => true, + ], + [ + 'privilege' => '{DAV:}write-properties', + 'principal' => $this->getOwner(), + 'protected' => true, + ], + [ + 'privilege' => '{DAV:}unbind', + 'principal' => $this->getOwner(), + 'protected' => true, + ], + [ + 'privilege' => '{DAV:}unbind', + 'principal' => $this->getOwner().'/calendar-proxy-write', + 'protected' => true, + ], + [ + 'privilege' => '{'.CalDAV\Plugin::NS_CALDAV.'}schedule-deliver', + 'principal' => '{DAV:}authenticated', + 'protected' => true, + ], + ]; + } + + /** + * Performs a calendar-query on the contents of this calendar. + * + * The calendar-query is defined in RFC4791 : CalDAV. Using the + * calendar-query it is possible for a client to request a specific set of + * object, based on contents of iCalendar properties, date-ranges and + * iCalendar component types (VTODO, VEVENT). + * + * This method should just return a list of (relative) urls that match this + * query. + * + * The list of filters are specified as an array. The exact array is + * documented by \Sabre\CalDAV\CalendarQueryParser. + * + * @return array + */ + public function calendarQuery(array $filters) + { + $result = []; + $validator = new CalDAV\CalendarQueryValidator(); + + $objects = $this->caldavBackend->getSchedulingObjects($this->principalUri); + foreach ($objects as $object) { + $vObject = VObject\Reader::read($object['calendardata']); + if ($validator->validate($vObject, $filters)) { + $result[] = $object['uri']; + } + + // Destroy circular references to PHP will GC the object. + $vObject->destroy(); + } + + return $result; + } +} diff --git a/3rdparty/sabre/dav/lib/CalDAV/Schedule/Outbox.php b/3rdparty/sabre/dav/lib/CalDAV/Schedule/Outbox.php new file mode 100644 index 00000000..1442c4cc --- /dev/null +++ b/3rdparty/sabre/dav/lib/CalDAV/Schedule/Outbox.php @@ -0,0 +1,119 @@ +principalUri = $principalUri; + } + + /** + * Returns the name of the node. + * + * This is used to generate the url. + * + * @return string + */ + public function getName() + { + return 'outbox'; + } + + /** + * Returns an array with all the child nodes. + * + * @return \Sabre\DAV\INode[] + */ + public function getChildren() + { + return []; + } + + /** + * Returns the owner principal. + * + * This must be a url to a principal, or null if there's no owner + * + * @return string|null + */ + public function getOwner() + { + return $this->principalUri; + } + + /** + * Returns a list of ACE's for this node. + * + * Each ACE has the following properties: + * * 'privilege', a string such as {DAV:}read or {DAV:}write. These are + * currently the only supported privileges + * * 'principal', a url to the principal who owns the node + * * 'protected' (optional), indicating that this ACE is not allowed to + * be updated. + * + * @return array + */ + public function getACL() + { + return [ + [ + 'privilege' => '{'.CalDAV\Plugin::NS_CALDAV.'}schedule-send', + 'principal' => $this->getOwner(), + 'protected' => true, + ], + [ + 'privilege' => '{DAV:}read', + 'principal' => $this->getOwner(), + 'protected' => true, + ], + [ + 'privilege' => '{'.CalDAV\Plugin::NS_CALDAV.'}schedule-send', + 'principal' => $this->getOwner().'/calendar-proxy-write', + 'protected' => true, + ], + [ + 'privilege' => '{DAV:}read', + 'principal' => $this->getOwner().'/calendar-proxy-read', + 'protected' => true, + ], + [ + 'privilege' => '{DAV:}read', + 'principal' => $this->getOwner().'/calendar-proxy-write', + 'protected' => true, + ], + ]; + } +} diff --git a/3rdparty/sabre/dav/lib/CalDAV/Schedule/Plugin.php b/3rdparty/sabre/dav/lib/CalDAV/Schedule/Plugin.php new file mode 100644 index 00000000..5bca56d4 --- /dev/null +++ b/3rdparty/sabre/dav/lib/CalDAV/Schedule/Plugin.php @@ -0,0 +1,1006 @@ +server = $server; + $server->on('method:POST', [$this, 'httpPost']); + $server->on('propFind', [$this, 'propFind']); + $server->on('propPatch', [$this, 'propPatch']); + $server->on('calendarObjectChange', [$this, 'calendarObjectChange']); + $server->on('beforeUnbind', [$this, 'beforeUnbind']); + $server->on('schedule', [$this, 'scheduleLocalDelivery']); + $server->on('getSupportedPrivilegeSet', [$this, 'getSupportedPrivilegeSet']); + + $ns = '{'.self::NS_CALDAV.'}'; + + /* + * This information ensures that the {DAV:}resourcetype property has + * the correct values. + */ + $server->resourceTypeMapping['\\Sabre\\CalDAV\\Schedule\\IOutbox'] = $ns.'schedule-outbox'; + $server->resourceTypeMapping['\\Sabre\\CalDAV\\Schedule\\IInbox'] = $ns.'schedule-inbox'; + + /* + * Properties we protect are made read-only by the server. + */ + array_push($server->protectedProperties, + $ns.'schedule-inbox-URL', + $ns.'schedule-outbox-URL', + $ns.'calendar-user-address-set', + $ns.'calendar-user-type', + $ns.'schedule-default-calendar-URL' + ); + } + + /** + * Use this method to tell the server this plugin defines additional + * HTTP methods. + * + * This method is passed a uri. It should only return HTTP methods that are + * available for the specified uri. + * + * @param string $uri + * + * @return array + */ + public function getHTTPMethods($uri) + { + try { + $node = $this->server->tree->getNodeForPath($uri); + } catch (NotFound $e) { + return []; + } + + if ($node instanceof IOutbox) { + return ['POST']; + } + + return []; + } + + /** + * This method handles POST request for the outbox. + * + * @return bool + */ + public function httpPost(RequestInterface $request, ResponseInterface $response) + { + // Checking if this is a text/calendar content type + $contentType = $request->getHeader('Content-Type'); + if (!$contentType || 0 !== strpos($contentType, 'text/calendar')) { + return; + } + + $path = $request->getPath(); + + // Checking if we're talking to an outbox + try { + $node = $this->server->tree->getNodeForPath($path); + } catch (NotFound $e) { + return; + } + if (!$node instanceof IOutbox) { + return; + } + + $this->server->transactionType = 'post-caldav-outbox'; + $this->outboxRequest($node, $request, $response); + + // Returning false breaks the event chain and tells the server we've + // handled the request. + return false; + } + + /** + * This method handler is invoked during fetching of properties. + * + * We use this event to add calendar-auto-schedule-specific properties. + */ + public function propFind(PropFind $propFind, INode $node) + { + if ($node instanceof DAVACL\IPrincipal) { + $caldavPlugin = $this->server->getPlugin('caldav'); + $principalUrl = $node->getPrincipalUrl(); + + // schedule-outbox-URL property + $propFind->handle('{'.self::NS_CALDAV.'}schedule-outbox-URL', function () use ($principalUrl, $caldavPlugin) { + $calendarHomePath = $caldavPlugin->getCalendarHomeForPrincipal($principalUrl); + if (!$calendarHomePath) { + return null; + } + $outboxPath = $calendarHomePath.'/outbox/'; + + return new LocalHref($outboxPath); + }); + // schedule-inbox-URL property + $propFind->handle('{'.self::NS_CALDAV.'}schedule-inbox-URL', function () use ($principalUrl, $caldavPlugin) { + $calendarHomePath = $caldavPlugin->getCalendarHomeForPrincipal($principalUrl); + if (!$calendarHomePath) { + return null; + } + $inboxPath = $calendarHomePath.'/inbox/'; + + return new LocalHref($inboxPath); + }); + + $propFind->handle('{'.self::NS_CALDAV.'}schedule-default-calendar-URL', function () use ($principalUrl, $caldavPlugin) { + // We don't support customizing this property yet, so in the + // meantime we just grab the first calendar in the home-set. + $calendarHomePath = $caldavPlugin->getCalendarHomeForPrincipal($principalUrl); + + if (!$calendarHomePath) { + return null; + } + + $sccs = '{'.self::NS_CALDAV.'}supported-calendar-component-set'; + + $result = $this->server->getPropertiesForPath($calendarHomePath, [ + '{DAV:}resourcetype', + '{DAV:}share-access', + $sccs, + ], 1); + + foreach ($result as $child) { + if (!isset($child[200]['{DAV:}resourcetype']) || !$child[200]['{DAV:}resourcetype']->is('{'.self::NS_CALDAV.'}calendar')) { + // Node is either not a calendar + continue; + } + if (isset($child[200]['{DAV:}share-access'])) { + $shareAccess = $child[200]['{DAV:}share-access']->getValue(); + if (Sharing\Plugin::ACCESS_NOTSHARED !== $shareAccess && Sharing\Plugin::ACCESS_SHAREDOWNER !== $shareAccess) { + // Node is a shared node, not owned by the relevant + // user. + continue; + } + } + if (!isset($child[200][$sccs]) || in_array('VEVENT', $child[200][$sccs]->getValue())) { + // Either there is no supported-calendar-component-set + // (which is fine) or we found one that supports VEVENT. + return new LocalHref($child['href']); + } + } + }); + + // The server currently reports every principal to be of type + // 'INDIVIDUAL' + $propFind->handle('{'.self::NS_CALDAV.'}calendar-user-type', function () { + return 'INDIVIDUAL'; + }); + } + + // Mapping the old property to the new property. + $propFind->handle('{http://calendarserver.org/ns/}calendar-availability', function () use ($propFind, $node) { + // In case it wasn't clear, the only difference is that we map the + // old property to a different namespace. + $availProp = '{'.self::NS_CALDAV.'}calendar-availability'; + $subPropFind = new PropFind( + $propFind->getPath(), + [$availProp] + ); + + $this->server->getPropertiesByNode( + $subPropFind, + $node + ); + + $propFind->set( + '{http://calendarserver.org/ns/}calendar-availability', + $subPropFind->get($availProp), + $subPropFind->getStatus($availProp) + ); + }); + } + + /** + * This method is called during property updates. + * + * @param string $path + */ + public function propPatch($path, PropPatch $propPatch) + { + // Mapping the old property to the new property. + $propPatch->handle('{http://calendarserver.org/ns/}calendar-availability', function ($value) use ($path) { + $availProp = '{'.self::NS_CALDAV.'}calendar-availability'; + $subPropPatch = new PropPatch([$availProp => $value]); + $this->server->emit('propPatch', [$path, $subPropPatch]); + $subPropPatch->commit(); + + return $subPropPatch->getResult()[$availProp]; + }); + } + + /** + * This method is triggered whenever there was a calendar object gets + * created or updated. + * + * @param RequestInterface $request HTTP request + * @param ResponseInterface $response HTTP Response + * @param VCalendar $vCal Parsed iCalendar object + * @param mixed $calendarPath Path to calendar collection + * @param mixed $modified the iCalendar object has been touched + * @param mixed $isNew Whether this was a new item or we're updating one + */ + public function calendarObjectChange(RequestInterface $request, ResponseInterface $response, VCalendar $vCal, $calendarPath, &$modified, $isNew) + { + if (!$this->scheduleReply($this->server->httpRequest)) { + return; + } + + $calendarNode = $this->server->tree->getNodeForPath($calendarPath); + + $addresses = $this->getAddressesForPrincipal( + $calendarNode->getOwner() + ); + + if (!$isNew) { + $node = $this->server->tree->getNodeForPath($request->getPath()); + $oldObj = Reader::read($node->get()); + } else { + $oldObj = null; + } + + $this->processICalendarChange($oldObj, $vCal, $addresses, [], $modified); + + if ($oldObj) { + // Destroy circular references so PHP will GC the object. + $oldObj->destroy(); + } + } + + /** + * This method is responsible for delivering the ITip message. + */ + public function deliver(ITip\Message $iTipMessage) + { + $this->server->emit('schedule', [$iTipMessage]); + if (!$iTipMessage->scheduleStatus) { + $iTipMessage->scheduleStatus = '5.2;There was no system capable of delivering the scheduling message'; + } + // In case the change was considered 'insignificant', we are going to + // remove any error statuses, if any. See ticket #525. + list($baseCode) = explode('.', $iTipMessage->scheduleStatus); + if (!$iTipMessage->significantChange && in_array($baseCode, ['3', '5'])) { + $iTipMessage->scheduleStatus = null; + } + } + + /** + * This method is triggered before a file gets deleted. + * + * We use this event to make sure that when this happens, attendees get + * cancellations, and organizers get 'DECLINED' statuses. + * + * @param string $path + */ + public function beforeUnbind($path) + { + // FIXME: We shouldn't trigger this functionality when we're issuing a + // MOVE. This is a hack. + if ('MOVE' === $this->server->httpRequest->getMethod()) { + return; + } + + $node = $this->server->tree->getNodeForPath($path); + + if (!$node instanceof ICalendarObject || $node instanceof ISchedulingObject) { + return; + } + + if (!$this->scheduleReply($this->server->httpRequest)) { + return; + } + + $addresses = $this->getAddressesForPrincipal( + $node->getOwner() + ); + + $broker = $this->createITipBroker(); + $messages = $broker->parseEvent(null, $addresses, $node->get()); + + foreach ($messages as $message) { + $this->deliver($message); + } + } + + /** + * Event handler for the 'schedule' event. + * + * This handler attempts to look at local accounts to deliver the + * scheduling object. + */ + public function scheduleLocalDelivery(ITip\Message $iTipMessage) + { + $aclPlugin = $this->server->getPlugin('acl'); + + // Local delivery is not available if the ACL plugin is not loaded. + if (!$aclPlugin) { + return; + } + + $caldavNS = '{'.self::NS_CALDAV.'}'; + + $principalUri = $aclPlugin->getPrincipalByUri($iTipMessage->recipient); + if (!$principalUri) { + $iTipMessage->scheduleStatus = '3.7;Could not find principal.'; + + return; + } + + // We found a principal URL, now we need to find its inbox. + // Unfortunately we may not have sufficient privileges to find this, so + // we are temporarily turning off ACL to let this come through. + // + // Once we support PHP 5.5, this should be wrapped in a try..finally + // block so we can ensure that this privilege gets added again after. + $this->server->removeListener('propFind', [$aclPlugin, 'propFind']); + + $result = $this->server->getProperties( + $principalUri, + [ + '{DAV:}principal-URL', + $caldavNS.'calendar-home-set', + $caldavNS.'schedule-inbox-URL', + $caldavNS.'schedule-default-calendar-URL', + '{http://sabredav.org/ns}email-address', + ] + ); + + // Re-registering the ACL event + $this->server->on('propFind', [$aclPlugin, 'propFind'], 20); + + if (!isset($result[$caldavNS.'schedule-inbox-URL'])) { + $iTipMessage->scheduleStatus = '5.2;Could not find local inbox'; + + return; + } + if (!isset($result[$caldavNS.'calendar-home-set'])) { + $iTipMessage->scheduleStatus = '5.2;Could not locate a calendar-home-set'; + + return; + } + if (!isset($result[$caldavNS.'schedule-default-calendar-URL'])) { + $iTipMessage->scheduleStatus = '5.2;Could not find a schedule-default-calendar-URL property'; + + return; + } + + $calendarPath = $result[$caldavNS.'schedule-default-calendar-URL']->getHref(); + $homePath = $result[$caldavNS.'calendar-home-set']->getHref(); + $inboxPath = $result[$caldavNS.'schedule-inbox-URL']->getHref(); + + if ('REPLY' === $iTipMessage->method) { + $privilege = 'schedule-deliver-reply'; + } else { + $privilege = 'schedule-deliver-invite'; + } + + if (!$aclPlugin->checkPrivileges($inboxPath, $caldavNS.$privilege, DAVACL\Plugin::R_PARENT, false)) { + $iTipMessage->scheduleStatus = '3.8;insufficient privileges: '.$privilege.' is required on the recipient schedule inbox.'; + + return; + } + + // Next, we're going to find out if the item already exits in one of + // the users' calendars. + $uid = $iTipMessage->uid; + + $newFileName = 'sabredav-'.\Sabre\DAV\UUIDUtil::getUUID().'.ics'; + + $home = $this->server->tree->getNodeForPath($homePath); + $inbox = $this->server->tree->getNodeForPath($inboxPath); + + $currentObject = null; + $objectNode = null; + $oldICalendarData = null; + $isNewNode = false; + + $result = $home->getCalendarObjectByUID($uid); + if ($result) { + // There was an existing object, we need to update probably. + $objectPath = $homePath.'/'.$result; + $objectNode = $this->server->tree->getNodeForPath($objectPath); + $oldICalendarData = $objectNode->get(); + $currentObject = Reader::read($oldICalendarData); + } else { + $isNewNode = true; + } + + $broker = $this->createITipBroker(); + $newObject = $broker->processMessage($iTipMessage, $currentObject); + + $inbox->createFile($newFileName, $iTipMessage->message->serialize()); + + if (!$newObject) { + // We received an iTip message referring to a UID that we don't + // have in any calendars yet, and processMessage did not give us a + // calendarobject back. + // + // The implication is that processMessage did not understand the + // iTip message. + $iTipMessage->scheduleStatus = '5.0;iTip message was not processed by the server, likely because we didn\'t understand it.'; + + return; + } + + // Note that we are bypassing ACL on purpose by calling this directly. + // We may need to look a bit deeper into this later. Supporting ACL + // here would be nice. + if ($isNewNode) { + $calendar = $this->server->tree->getNodeForPath($calendarPath); + $calendar->createFile($newFileName, $newObject->serialize()); + } else { + // If the message was a reply, we may have to inform other + // attendees of this attendees status. Therefore we're shooting off + // another itipMessage. + if ('REPLY' === $iTipMessage->method) { + $this->processICalendarChange( + $oldICalendarData, + $newObject, + [$iTipMessage->recipient], + [$iTipMessage->sender] + ); + } + $objectNode->put($newObject->serialize()); + } + $iTipMessage->scheduleStatus = '1.2;Message delivered locally'; + } + + /** + * This method is triggered whenever a subsystem requests the privileges + * that are supported on a particular node. + * + * We need to add a number of privileges for scheduling purposes. + */ + public function getSupportedPrivilegeSet(INode $node, array &$supportedPrivilegeSet) + { + $ns = '{'.self::NS_CALDAV.'}'; + if ($node instanceof IOutbox) { + $supportedPrivilegeSet[$ns.'schedule-send'] = [ + 'abstract' => false, + 'aggregates' => [ + $ns.'schedule-send-invite' => [ + 'abstract' => false, + 'aggregates' => [], + ], + $ns.'schedule-send-reply' => [ + 'abstract' => false, + 'aggregates' => [], + ], + $ns.'schedule-send-freebusy' => [ + 'abstract' => false, + 'aggregates' => [], + ], + // Privilege from an earlier scheduling draft, but still + // used by some clients. + $ns.'schedule-post-vevent' => [ + 'abstract' => false, + 'aggregates' => [], + ], + ], + ]; + } + if ($node instanceof IInbox) { + $supportedPrivilegeSet[$ns.'schedule-deliver'] = [ + 'abstract' => false, + 'aggregates' => [ + $ns.'schedule-deliver-invite' => [ + 'abstract' => false, + 'aggregates' => [], + ], + $ns.'schedule-deliver-reply' => [ + 'abstract' => false, + 'aggregates' => [], + ], + $ns.'schedule-query-freebusy' => [ + 'abstract' => false, + 'aggregates' => [], + ], + ], + ]; + } + } + + /** + * This method looks at an old iCalendar object, a new iCalendar object and + * starts sending scheduling messages based on the changes. + * + * A list of addresses needs to be specified, so the system knows who made + * the update, because the behavior may be different based on if it's an + * attendee or an organizer. + * + * This method may update $newObject to add any status changes. + * + * @param VCalendar|string|null $oldObject + * @param array $ignore any addresses to not send messages to + * @param bool $modified a marker to indicate that the original object modified by this process + */ + protected function processICalendarChange($oldObject, VCalendar $newObject, array $addresses, array $ignore = [], &$modified = false) + { + $broker = $this->createITipBroker(); + $messages = $broker->parseEvent($newObject, $addresses, $oldObject); + + if ($messages) { + $modified = true; + } + + foreach ($messages as $message) { + if (in_array($message->recipient, $ignore)) { + continue; + } + + $this->deliver($message); + + if (isset($newObject->VEVENT->ORGANIZER) && ($newObject->VEVENT->ORGANIZER->getNormalizedValue() === $message->recipient)) { + if ($message->scheduleStatus) { + $newObject->VEVENT->ORGANIZER['SCHEDULE-STATUS'] = $message->getScheduleStatus(); + } + unset($newObject->VEVENT->ORGANIZER['SCHEDULE-FORCE-SEND']); + } else { + if (isset($newObject->VEVENT->ATTENDEE)) { + foreach ($newObject->VEVENT->ATTENDEE as $attendee) { + if ($attendee->getNormalizedValue() === $message->recipient) { + if ($message->scheduleStatus) { + $attendee['SCHEDULE-STATUS'] = $message->getScheduleStatus(); + } + unset($attendee['SCHEDULE-FORCE-SEND']); + break; + } + } + } + } + } + } + + /** + * Returns a list of addresses that are associated with a principal. + * + * @param string $principal + * + * @return array + */ + protected function getAddressesForPrincipal($principal) + { + $CUAS = '{'.self::NS_CALDAV.'}calendar-user-address-set'; + + $properties = $this->server->getProperties( + $principal, + [$CUAS] + ); + + // If we can't find this information, we'll stop processing + if (!isset($properties[$CUAS])) { + return []; + } + + $addresses = $properties[$CUAS]->getHrefs(); + + return $addresses; + } + + /** + * This method handles POST requests to the schedule-outbox. + * + * Currently, two types of requests are supported: + * * FREEBUSY requests from RFC 6638 + * * Simple iTIP messages from draft-desruisseaux-caldav-sched-04 + * + * The latter is from an expired early draft of the CalDAV scheduling + * extensions, but iCal depends on a feature from that spec, so we + * implement it. + */ + public function outboxRequest(IOutbox $outboxNode, RequestInterface $request, ResponseInterface $response) + { + $outboxPath = $request->getPath(); + + // Parsing the request body + try { + $vObject = VObject\Reader::read($request->getBody()); + } catch (VObject\ParseException $e) { + throw new BadRequest('The request body must be a valid iCalendar object. Parse error: '.$e->getMessage()); + } + + // The incoming iCalendar object must have a METHOD property, and a + // component. The combination of both determines what type of request + // this is. + $componentType = null; + foreach ($vObject->getComponents() as $component) { + if ('VTIMEZONE' !== $component->name) { + $componentType = $component->name; + break; + } + } + if (is_null($componentType)) { + throw new BadRequest('We expected at least one VTODO, VJOURNAL, VFREEBUSY or VEVENT component'); + } + + // Validating the METHOD + $method = strtoupper((string) $vObject->METHOD); + if (!$method) { + throw new BadRequest('A METHOD property must be specified in iTIP messages'); + } + + // So we support one type of request: + // + // REQUEST with a VFREEBUSY component + + $acl = $this->server->getPlugin('acl'); + + if ('VFREEBUSY' === $componentType && 'REQUEST' === $method) { + $acl && $acl->checkPrivileges($outboxPath, '{'.self::NS_CALDAV.'}schedule-send-freebusy'); + $this->handleFreeBusyRequest($outboxNode, $vObject, $request, $response); + + // Destroy circular references so PHP can GC the object. + $vObject->destroy(); + unset($vObject); + } else { + throw new NotImplemented('We only support VFREEBUSY (REQUEST) on this endpoint'); + } + } + + /** + * This method is responsible for parsing a free-busy query request and + * returning its result in $response. + */ + protected function handleFreeBusyRequest(IOutbox $outbox, VObject\Component $vObject, RequestInterface $request, ResponseInterface $response) + { + $vFreeBusy = $vObject->VFREEBUSY; + $organizer = $vFreeBusy->ORGANIZER; + + $organizer = (string) $organizer; + + // Validating if the organizer matches the owner of the inbox. + $owner = $outbox->getOwner(); + + $caldavNS = '{'.self::NS_CALDAV.'}'; + + $uas = $caldavNS.'calendar-user-address-set'; + $props = $this->server->getProperties($owner, [$uas]); + + if (empty($props[$uas]) || !in_array($organizer, $props[$uas]->getHrefs())) { + throw new Forbidden('The organizer in the request did not match any of the addresses for the owner of this inbox'); + } + + if (!isset($vFreeBusy->ATTENDEE)) { + throw new BadRequest('You must at least specify 1 attendee'); + } + + $attendees = []; + foreach ($vFreeBusy->ATTENDEE as $attendee) { + $attendees[] = (string) $attendee; + } + + if (!isset($vFreeBusy->DTSTART) || !isset($vFreeBusy->DTEND)) { + throw new BadRequest('DTSTART and DTEND must both be specified'); + } + + $startRange = $vFreeBusy->DTSTART->getDateTime(); + $endRange = $vFreeBusy->DTEND->getDateTime(); + + $results = []; + foreach ($attendees as $attendee) { + $results[] = $this->getFreeBusyForEmail($attendee, $startRange, $endRange, $vObject); + } + + $dom = new \DOMDocument('1.0', 'utf-8'); + $dom->formatOutput = true; + $scheduleResponse = $dom->createElement('cal:schedule-response'); + foreach ($this->server->xml->namespaceMap as $namespace => $prefix) { + $scheduleResponse->setAttribute('xmlns:'.$prefix, $namespace); + } + $dom->appendChild($scheduleResponse); + + foreach ($results as $result) { + $xresponse = $dom->createElement('cal:response'); + + $recipient = $dom->createElement('cal:recipient'); + $recipientHref = $dom->createElement('d:href'); + + $recipientHref->appendChild($dom->createTextNode($result['href'])); + $recipient->appendChild($recipientHref); + $xresponse->appendChild($recipient); + + $reqStatus = $dom->createElement('cal:request-status'); + $reqStatus->appendChild($dom->createTextNode($result['request-status'])); + $xresponse->appendChild($reqStatus); + + if (isset($result['calendar-data'])) { + $calendardata = $dom->createElement('cal:calendar-data'); + $calendardata->appendChild($dom->createTextNode(str_replace("\r\n", "\n", $result['calendar-data']->serialize()))); + $xresponse->appendChild($calendardata); + } + $scheduleResponse->appendChild($xresponse); + } + + $response->setStatus(200); + $response->setHeader('Content-Type', 'application/xml'); + $response->setBody($dom->saveXML()); + } + + /** + * Returns free-busy information for a specific address. The returned + * data is an array containing the following properties:. + * + * calendar-data : A VFREEBUSY VObject + * request-status : an iTip status code. + * href: The principal's email address, as requested + * + * The following request status codes may be returned: + * * 2.0;description + * * 3.7;description + * + * @param string $email address + * + * @return array + */ + protected function getFreeBusyForEmail($email, \DateTimeInterface $start, \DateTimeInterface $end, VObject\Component $request) + { + $caldavNS = '{'.self::NS_CALDAV.'}'; + + $aclPlugin = $this->server->getPlugin('acl'); + if ('mailto:' === substr($email, 0, 7)) { + $email = substr($email, 7); + } + + $result = $aclPlugin->principalSearch( + ['{http://sabredav.org/ns}email-address' => $email], + [ + '{DAV:}principal-URL', + $caldavNS.'calendar-home-set', + $caldavNS.'schedule-inbox-URL', + '{http://sabredav.org/ns}email-address', + ] + ); + + if (!count($result)) { + return [ + 'request-status' => '3.7;Could not find principal', + 'href' => 'mailto:'.$email, + ]; + } + + if (!isset($result[0][200][$caldavNS.'calendar-home-set'])) { + return [ + 'request-status' => '3.7;No calendar-home-set property found', + 'href' => 'mailto:'.$email, + ]; + } + if (!isset($result[0][200][$caldavNS.'schedule-inbox-URL'])) { + return [ + 'request-status' => '3.7;No schedule-inbox-URL property found', + 'href' => 'mailto:'.$email, + ]; + } + $homeSet = $result[0][200][$caldavNS.'calendar-home-set']->getHref(); + $inboxUrl = $result[0][200][$caldavNS.'schedule-inbox-URL']->getHref(); + + // Do we have permission? + $aclPlugin->checkPrivileges($inboxUrl, $caldavNS.'schedule-query-freebusy'); + + // Grabbing the calendar list + $objects = []; + $calendarTimeZone = new DateTimeZone('UTC'); + + foreach ($this->server->tree->getNodeForPath($homeSet)->getChildren() as $node) { + if (!$node instanceof ICalendar) { + continue; + } + + $sct = $caldavNS.'schedule-calendar-transp'; + $ctz = $caldavNS.'calendar-timezone'; + $props = $node->getProperties([$sct, $ctz]); + + if (isset($props[$sct]) && ScheduleCalendarTransp::TRANSPARENT == $props[$sct]->getValue()) { + // If a calendar is marked as 'transparent', it means we must + // ignore it for free-busy purposes. + continue; + } + + if (isset($props[$ctz])) { + $vtimezoneObj = VObject\Reader::read($props[$ctz]); + $calendarTimeZone = $vtimezoneObj->VTIMEZONE->getTimeZone(); + + // Destroy circular references so PHP can garbage collect the object. + $vtimezoneObj->destroy(); + } + + // Getting the list of object uris within the time-range + $urls = $node->calendarQuery([ + 'name' => 'VCALENDAR', + 'comp-filters' => [ + [ + 'name' => 'VEVENT', + 'comp-filters' => [], + 'prop-filters' => [], + 'is-not-defined' => false, + 'time-range' => [ + 'start' => $start, + 'end' => $end, + ], + ], + ], + 'prop-filters' => [], + 'is-not-defined' => false, + 'time-range' => null, + ]); + + $calObjects = array_map(function ($url) use ($node) { + $obj = $node->getChild($url)->get(); + + return $obj; + }, $urls); + + $objects = array_merge($objects, $calObjects); + } + + $inboxProps = $this->server->getProperties( + $inboxUrl, + $caldavNS.'calendar-availability' + ); + + $vcalendar = new VObject\Component\VCalendar(); + $vcalendar->METHOD = 'REPLY'; + + $generator = new VObject\FreeBusyGenerator(); + $generator->setObjects($objects); + $generator->setTimeRange($start, $end); + $generator->setBaseObject($vcalendar); + $generator->setTimeZone($calendarTimeZone); + + if ($inboxProps) { + $generator->setVAvailability( + VObject\Reader::read( + $inboxProps[$caldavNS.'calendar-availability'] + ) + ); + } + + $result = $generator->getResult(); + + $vcalendar->VFREEBUSY->ATTENDEE = 'mailto:'.$email; + $vcalendar->VFREEBUSY->UID = (string) $request->VFREEBUSY->UID; + $vcalendar->VFREEBUSY->ORGANIZER = clone $request->VFREEBUSY->ORGANIZER; + + return [ + 'calendar-data' => $result, + 'request-status' => '2.0;Success', + 'href' => 'mailto:'.$email, + ]; + } + + /** + * This method checks the 'Schedule-Reply' header + * and returns false if it's 'F', otherwise true. + * + * @return bool + */ + protected function scheduleReply(RequestInterface $request) + { + $scheduleReply = $request->getHeader('Schedule-Reply'); + + return 'F' !== $scheduleReply; + } + + /** + * Returns a bunch of meta-data about the plugin. + * + * Providing this information is optional, and is mainly displayed by the + * Browser plugin. + * + * The description key in the returned array may contain html and will not + * be sanitized. + * + * @return array + */ + public function getPluginInfo() + { + return [ + 'name' => $this->getPluginName(), + 'description' => 'Adds calendar-auto-schedule, as defined in rfc6638', + 'link' => 'http://sabre.io/dav/scheduling/', + ]; + } + + /** + * Returns an instance of the iTip\Broker. + */ + protected function createITipBroker(): Broker + { + return new Broker(); + } +} diff --git a/3rdparty/sabre/dav/lib/CalDAV/Schedule/SchedulingObject.php b/3rdparty/sabre/dav/lib/CalDAV/Schedule/SchedulingObject.php new file mode 100644 index 00000000..b40f28a9 --- /dev/null +++ b/3rdparty/sabre/dav/lib/CalDAV/Schedule/SchedulingObject.php @@ -0,0 +1,130 @@ +objectData['calendardata'])) { + $this->objectData = $this->caldavBackend->getSchedulingObject($this->objectData['principaluri'], $this->objectData['uri']); + } + + return $this->objectData['calendardata']; + } + + /** + * Updates the ICalendar-formatted object. + * + * @param string|resource $calendarData + * + * @return string + */ + public function put($calendarData) + { + throw new MethodNotAllowed('Updating scheduling objects is not supported'); + } + + /** + * Deletes the scheduling message. + */ + public function delete() + { + $this->caldavBackend->deleteSchedulingObject($this->objectData['principaluri'], $this->objectData['uri']); + } + + /** + * Returns the owner principal. + * + * This must be a url to a principal, or null if there's no owner + * + * @return string|null + */ + public function getOwner() + { + return $this->objectData['principaluri']; + } + + /** + * Returns a list of ACE's for this node. + * + * Each ACE has the following properties: + * * 'privilege', a string such as {DAV:}read or {DAV:}write. These are + * currently the only supported privileges + * * 'principal', a url to the principal who owns the node + * * 'protected' (optional), indicating that this ACE is not allowed to + * be updated. + * + * @return array + */ + public function getACL() + { + // An alternative acl may be specified in the object data. + // + + if (isset($this->objectData['acl'])) { + return $this->objectData['acl']; + } + + // The default ACL + return [ + [ + 'privilege' => '{DAV:}all', + 'principal' => '{DAV:}owner', + 'protected' => true, + ], + [ + 'privilege' => '{DAV:}all', + 'principal' => $this->objectData['principaluri'].'/calendar-proxy-write', + 'protected' => true, + ], + [ + 'privilege' => '{DAV:}read', + 'principal' => $this->objectData['principaluri'].'/calendar-proxy-read', + 'protected' => true, + ], + ]; + } +} diff --git a/3rdparty/sabre/dav/lib/CalDAV/SharedCalendar.php b/3rdparty/sabre/dav/lib/CalDAV/SharedCalendar.php new file mode 100644 index 00000000..818392f5 --- /dev/null +++ b/3rdparty/sabre/dav/lib/CalDAV/SharedCalendar.php @@ -0,0 +1,219 @@ +calendarInfo['share-access']) ? $this->calendarInfo['share-access'] : SPlugin::ACCESS_NOTSHARED; + } + + /** + * This function must return a URI that uniquely identifies the shared + * resource. This URI should be identical across instances, and is + * also used in several other XML bodies to connect invites to + * resources. + * + * This may simply be a relative reference to the original shared instance, + * but it could also be a urn. As long as it's a valid URI and unique. + * + * @return string + */ + public function getShareResourceUri() + { + return $this->calendarInfo['share-resource-uri']; + } + + /** + * Updates the list of sharees. + * + * Every item must be a Sharee object. + * + * @param \Sabre\DAV\Xml\Element\Sharee[] $sharees + */ + public function updateInvites(array $sharees) + { + $this->caldavBackend->updateInvites($this->calendarInfo['id'], $sharees); + } + + /** + * Returns the list of people whom this resource is shared with. + * + * Every item in the returned array must be a Sharee object with + * at least the following properties set: + * + * * $href + * * $shareAccess + * * $inviteStatus + * + * and optionally: + * + * * $properties + * + * @return \Sabre\DAV\Xml\Element\Sharee[] + */ + public function getInvites() + { + return $this->caldavBackend->getInvites($this->calendarInfo['id']); + } + + /** + * Marks this calendar as published. + * + * Publishing a calendar should automatically create a read-only, public, + * subscribable calendar. + * + * @param bool $value + */ + public function setPublishStatus($value) + { + $this->caldavBackend->setPublishStatus($this->calendarInfo['id'], $value); + } + + /** + * Returns a list of ACE's for this node. + * + * Each ACE has the following properties: + * * 'privilege', a string such as {DAV:}read or {DAV:}write. These are + * currently the only supported privileges + * * 'principal', a url to the principal who owns the node + * * 'protected' (optional), indicating that this ACE is not allowed to + * be updated. + * + * @return array + */ + public function getACL() + { + $acl = []; + + switch ($this->getShareAccess()) { + case SPlugin::ACCESS_NOTSHARED: + case SPlugin::ACCESS_SHAREDOWNER: + $acl[] = [ + 'privilege' => '{DAV:}share', + 'principal' => $this->calendarInfo['principaluri'], + 'protected' => true, + ]; + $acl[] = [ + 'privilege' => '{DAV:}share', + 'principal' => $this->calendarInfo['principaluri'].'/calendar-proxy-write', + 'protected' => true, + ]; + // no break intentional! + case SPlugin::ACCESS_READWRITE: + $acl[] = [ + 'privilege' => '{DAV:}write', + 'principal' => $this->calendarInfo['principaluri'], + 'protected' => true, + ]; + $acl[] = [ + 'privilege' => '{DAV:}write', + 'principal' => $this->calendarInfo['principaluri'].'/calendar-proxy-write', + 'protected' => true, + ]; + // no break intentional! + case SPlugin::ACCESS_READ: + $acl[] = [ + 'privilege' => '{DAV:}write-properties', + 'principal' => $this->calendarInfo['principaluri'], + 'protected' => true, + ]; + $acl[] = [ + 'privilege' => '{DAV:}write-properties', + 'principal' => $this->calendarInfo['principaluri'].'/calendar-proxy-write', + 'protected' => true, + ]; + $acl[] = [ + 'privilege' => '{DAV:}read', + 'principal' => $this->calendarInfo['principaluri'], + 'protected' => true, + ]; + $acl[] = [ + 'privilege' => '{DAV:}read', + 'principal' => $this->calendarInfo['principaluri'].'/calendar-proxy-read', + 'protected' => true, + ]; + $acl[] = [ + 'privilege' => '{DAV:}read', + 'principal' => $this->calendarInfo['principaluri'].'/calendar-proxy-write', + 'protected' => true, + ]; + $acl[] = [ + 'privilege' => '{'.Plugin::NS_CALDAV.'}read-free-busy', + 'principal' => '{DAV:}authenticated', + 'protected' => true, + ]; + break; + } + + return $acl; + } + + /** + * This method returns the ACL's for calendar objects in this calendar. + * The result of this method automatically gets passed to the + * calendar-object nodes in the calendar. + * + * @return array + */ + public function getChildACL() + { + $acl = []; + + switch ($this->getShareAccess()) { + case SPlugin::ACCESS_NOTSHARED: + case SPlugin::ACCESS_SHAREDOWNER: + case SPlugin::ACCESS_READWRITE: + $acl[] = [ + 'privilege' => '{DAV:}write', + 'principal' => $this->calendarInfo['principaluri'], + 'protected' => true, + ]; + $acl[] = [ + 'privilege' => '{DAV:}write', + 'principal' => $this->calendarInfo['principaluri'].'/calendar-proxy-write', + 'protected' => true, + ]; + // no break intentional + case SPlugin::ACCESS_READ: + $acl[] = [ + 'privilege' => '{DAV:}read', + 'principal' => $this->calendarInfo['principaluri'], + 'protected' => true, + ]; + $acl[] = [ + 'privilege' => '{DAV:}read', + 'principal' => $this->calendarInfo['principaluri'].'/calendar-proxy-write', + 'protected' => true, + ]; + $acl[] = [ + 'privilege' => '{DAV:}read', + 'principal' => $this->calendarInfo['principaluri'].'/calendar-proxy-read', + 'protected' => true, + ]; + break; + } + + return $acl; + } +} diff --git a/3rdparty/sabre/dav/lib/CalDAV/SharingPlugin.php b/3rdparty/sabre/dav/lib/CalDAV/SharingPlugin.php new file mode 100644 index 00000000..bacfe044 --- /dev/null +++ b/3rdparty/sabre/dav/lib/CalDAV/SharingPlugin.php @@ -0,0 +1,346 @@ +server = $server; + + if (is_null($this->server->getPlugin('sharing'))) { + throw new \LogicException('The generic "sharing" plugin must be loaded before the caldav sharing plugin. Call $server->addPlugin(new \Sabre\DAV\Sharing\Plugin()); before this one.'); + } + + array_push( + $this->server->protectedProperties, + '{'.Plugin::NS_CALENDARSERVER.'}invite', + '{'.Plugin::NS_CALENDARSERVER.'}allowed-sharing-modes', + '{'.Plugin::NS_CALENDARSERVER.'}shared-url' + ); + + $this->server->xml->elementMap['{'.Plugin::NS_CALENDARSERVER.'}share'] = 'Sabre\\CalDAV\\Xml\\Request\\Share'; + $this->server->xml->elementMap['{'.Plugin::NS_CALENDARSERVER.'}invite-reply'] = 'Sabre\\CalDAV\\Xml\\Request\\InviteReply'; + + $this->server->on('propFind', [$this, 'propFindEarly']); + $this->server->on('propFind', [$this, 'propFindLate'], 150); + $this->server->on('propPatch', [$this, 'propPatch'], 40); + $this->server->on('method:POST', [$this, 'httpPost']); + } + + /** + * This event is triggered when properties are requested for a certain + * node. + * + * This allows us to inject any properties early. + */ + public function propFindEarly(DAV\PropFind $propFind, DAV\INode $node) + { + if ($node instanceof ISharedCalendar) { + $propFind->handle('{'.Plugin::NS_CALENDARSERVER.'}invite', function () use ($node) { + return new Xml\Property\Invite( + $node->getInvites() + ); + }); + } + } + + /** + * This method is triggered *after* all properties have been retrieved. + * This allows us to inject the correct resourcetype for calendars that + * have been shared. + */ + public function propFindLate(DAV\PropFind $propFind, DAV\INode $node) + { + if ($node instanceof ISharedCalendar) { + $shareAccess = $node->getShareAccess(); + if ($rt = $propFind->get('{DAV:}resourcetype')) { + switch ($shareAccess) { + case \Sabre\DAV\Sharing\Plugin::ACCESS_SHAREDOWNER: + $rt->add('{'.Plugin::NS_CALENDARSERVER.'}shared-owner'); + break; + case \Sabre\DAV\Sharing\Plugin::ACCESS_READ: + case \Sabre\DAV\Sharing\Plugin::ACCESS_READWRITE: + $rt->add('{'.Plugin::NS_CALENDARSERVER.'}shared'); + break; + } + } + $propFind->handle('{'.Plugin::NS_CALENDARSERVER.'}allowed-sharing-modes', function () { + return new Xml\Property\AllowedSharingModes(true, false); + }); + } + } + + /** + * This method is triggered when a user attempts to update a node's + * properties. + * + * A previous draft of the sharing spec stated that it was possible to use + * PROPPATCH to remove 'shared-owner' from the resourcetype, thus unsharing + * the calendar. + * + * Even though this is no longer in the current spec, we keep this around + * because OS X 10.7 may still make use of this feature. + * + * @param string $path + */ + public function propPatch($path, DAV\PropPatch $propPatch) + { + $node = $this->server->tree->getNodeForPath($path); + if (!$node instanceof ISharedCalendar) { + return; + } + + if (\Sabre\DAV\Sharing\Plugin::ACCESS_SHAREDOWNER === $node->getShareAccess() || \Sabre\DAV\Sharing\Plugin::ACCESS_NOTSHARED === $node->getShareAccess()) { + $propPatch->handle('{DAV:}resourcetype', function ($value) use ($node) { + if ($value->is('{'.Plugin::NS_CALENDARSERVER.'}shared-owner')) { + return false; + } + $shares = $node->getInvites(); + foreach ($shares as $share) { + $share->access = DAV\Sharing\Plugin::ACCESS_NOACCESS; + } + $node->updateInvites($shares); + + return true; + }); + } + } + + /** + * We intercept this to handle POST requests on calendars. + * + * @return bool|null + */ + public function httpPost(RequestInterface $request, ResponseInterface $response) + { + $path = $request->getPath(); + + // Only handling xml + $contentType = $request->getHeader('Content-Type'); + if (null === $contentType) { + return; + } + if (false === strpos($contentType, 'application/xml') && false === strpos($contentType, 'text/xml')) { + return; + } + + // Making sure the node exists + try { + $node = $this->server->tree->getNodeForPath($path); + } catch (DAV\Exception\NotFound $e) { + return; + } + + $requestBody = $request->getBodyAsString(); + + // If this request handler could not deal with this POST request, it + // will return 'null' and other plugins get a chance to handle the + // request. + // + // However, we already requested the full body. This is a problem, + // because a body can only be read once. This is why we preemptively + // re-populated the request body with the existing data. + $request->setBody($requestBody); + + $message = $this->server->xml->parse($requestBody, $request->getUrl(), $documentType); + + switch ($documentType) { + // Both the DAV:share-resource and CALENDARSERVER:share requests + // behave identically. + case '{'.Plugin::NS_CALENDARSERVER.'}share': + $sharingPlugin = $this->server->getPlugin('sharing'); + $sharingPlugin->shareResource($path, $message->sharees); + + $response->setStatus(200); + // Adding this because sending a response body may cause issues, + // and I wanted some type of indicator the response was handled. + $response->setHeader('X-Sabre-Status', 'everything-went-well'); + + // Breaking the event chain + return false; + + // The invite-reply document is sent when the user replies to an + // invitation of a calendar share. + case '{'.Plugin::NS_CALENDARSERVER.'}invite-reply': + // This only works on the calendar-home-root node. + if (!$node instanceof CalendarHome) { + return; + } + $this->server->transactionType = 'post-invite-reply'; + + // Getting ACL info + $acl = $this->server->getPlugin('acl'); + + // If there's no ACL support, we allow everything + if ($acl) { + $acl->checkPrivileges($path, '{DAV:}write'); + } + + $url = $node->shareReply( + $message->href, + $message->status, + $message->calendarUri, + $message->inReplyTo, + $message->summary + ); + + $response->setStatus(200); + // Adding this because sending a response body may cause issues, + // and I wanted some type of indicator the response was handled. + $response->setHeader('X-Sabre-Status', 'everything-went-well'); + + if ($url) { + $writer = $this->server->xml->getWriter(); + $writer->contextUri = $request->getUrl(); + $writer->openMemory(); + $writer->startDocument(); + $writer->startElement('{'.Plugin::NS_CALENDARSERVER.'}shared-as'); + $writer->write(new LocalHref($url)); + $writer->endElement(); + $response->setHeader('Content-Type', 'application/xml'); + $response->setBody($writer->outputMemory()); + } + + // Breaking the event chain + return false; + + case '{'.Plugin::NS_CALENDARSERVER.'}publish-calendar': + // We can only deal with IShareableCalendar objects + if (!$node instanceof ISharedCalendar) { + return; + } + $this->server->transactionType = 'post-publish-calendar'; + + // Getting ACL info + $acl = $this->server->getPlugin('acl'); + + // If there's no ACL support, we allow everything + if ($acl) { + $acl->checkPrivileges($path, '{DAV:}share'); + } + + $node->setPublishStatus(true); + + // iCloud sends back the 202, so we will too. + $response->setStatus(202); + + // Adding this because sending a response body may cause issues, + // and I wanted some type of indicator the response was handled. + $response->setHeader('X-Sabre-Status', 'everything-went-well'); + + // Breaking the event chain + return false; + + case '{'.Plugin::NS_CALENDARSERVER.'}unpublish-calendar': + // We can only deal with IShareableCalendar objects + if (!$node instanceof ISharedCalendar) { + return; + } + $this->server->transactionType = 'post-unpublish-calendar'; + + // Getting ACL info + $acl = $this->server->getPlugin('acl'); + + // If there's no ACL support, we allow everything + if ($acl) { + $acl->checkPrivileges($path, '{DAV:}share'); + } + + $node->setPublishStatus(false); + + $response->setStatus(200); + + // Adding this because sending a response body may cause issues, + // and I wanted some type of indicator the response was handled. + $response->setHeader('X-Sabre-Status', 'everything-went-well'); + + // Breaking the event chain + return false; + } + } + + /** + * Returns a bunch of meta-data about the plugin. + * + * Providing this information is optional, and is mainly displayed by the + * Browser plugin. + * + * The description key in the returned array may contain html and will not + * be sanitized. + * + * @return array + */ + public function getPluginInfo() + { + return [ + 'name' => $this->getPluginName(), + 'description' => 'Adds support for caldav-sharing.', + 'link' => 'http://sabre.io/dav/caldav-sharing/', + ]; + } +} diff --git a/3rdparty/sabre/dav/lib/CalDAV/Subscriptions/ISubscription.php b/3rdparty/sabre/dav/lib/CalDAV/Subscriptions/ISubscription.php new file mode 100644 index 00000000..e83082c5 --- /dev/null +++ b/3rdparty/sabre/dav/lib/CalDAV/Subscriptions/ISubscription.php @@ -0,0 +1,41 @@ +resourceTypeMapping['Sabre\\CalDAV\\Subscriptions\\ISubscription'] = + '{http://calendarserver.org/ns/}subscribed'; + + $server->xml->elementMap['{http://calendarserver.org/ns/}source'] = + 'Sabre\\DAV\\Xml\\Property\\Href'; + + $server->on('propFind', [$this, 'propFind'], 150); + } + + /** + * This method should return a list of server-features. + * + * This is for example 'versioning' and is added to the DAV: header + * in an OPTIONS response. + * + * @return array + */ + public function getFeatures() + { + return ['calendarserver-subscribed']; + } + + /** + * Triggered after properties have been fetched. + */ + public function propFind(PropFind $propFind, INode $node) + { + // There's a bunch of properties that must appear as a self-closing + // xml-element. This event handler ensures that this will be the case. + $props = [ + '{http://calendarserver.org/ns/}subscribed-strip-alarms', + '{http://calendarserver.org/ns/}subscribed-strip-attachments', + '{http://calendarserver.org/ns/}subscribed-strip-todos', + ]; + + foreach ($props as $prop) { + if (200 === $propFind->getStatus($prop)) { + $propFind->set($prop, '', 200); + } + } + } + + /** + * Returns a plugin name. + * + * Using this name other plugins will be able to access other plugins + * using \Sabre\DAV\Server::getPlugin + * + * @return string + */ + public function getPluginName() + { + return 'subscriptions'; + } + + /** + * Returns a bunch of meta-data about the plugin. + * + * Providing this information is optional, and is mainly displayed by the + * Browser plugin. + * + * The description key in the returned array may contain html and will not + * be sanitized. + * + * @return array + */ + public function getPluginInfo() + { + return [ + 'name' => $this->getPluginName(), + 'description' => 'This plugin allows users to store iCalendar subscriptions in their calendar-home.', + 'link' => null, + ]; + } +} diff --git a/3rdparty/sabre/dav/lib/CalDAV/Subscriptions/Subscription.php b/3rdparty/sabre/dav/lib/CalDAV/Subscriptions/Subscription.php new file mode 100644 index 00000000..8d56e644 --- /dev/null +++ b/3rdparty/sabre/dav/lib/CalDAV/Subscriptions/Subscription.php @@ -0,0 +1,204 @@ +caldavBackend = $caldavBackend; + $this->subscriptionInfo = $subscriptionInfo; + + $required = [ + 'id', + 'uri', + 'principaluri', + 'source', + ]; + + foreach ($required as $r) { + if (!isset($subscriptionInfo[$r])) { + throw new \InvalidArgumentException('The '.$r.' field is required when creating a subscription node'); + } + } + } + + /** + * Returns the name of the node. + * + * This is used to generate the url. + * + * @return string + */ + public function getName() + { + return $this->subscriptionInfo['uri']; + } + + /** + * Returns the last modification time. + * + * @return int|null + */ + public function getLastModified() + { + if (isset($this->subscriptionInfo['lastmodified'])) { + return $this->subscriptionInfo['lastmodified']; + } + } + + /** + * Deletes the current node. + */ + public function delete() + { + $this->caldavBackend->deleteSubscription( + $this->subscriptionInfo['id'] + ); + } + + /** + * Returns an array with all the child nodes. + * + * @return \Sabre\DAV\INode[] + */ + public function getChildren() + { + return []; + } + + /** + * Updates properties on this node. + * + * This method received a PropPatch object, which contains all the + * information about the update. + * + * To update specific properties, call the 'handle' method on this object. + * Read the PropPatch documentation for more information. + */ + public function propPatch(PropPatch $propPatch) + { + return $this->caldavBackend->updateSubscription( + $this->subscriptionInfo['id'], + $propPatch + ); + } + + /** + * Returns a list of properties for this nodes. + * + * The properties list is a list of propertynames the client requested, + * encoded in clark-notation {xmlnamespace}tagname. + * + * If the array is empty, it means 'all properties' were requested. + * + * Note that it's fine to liberally give properties back, instead of + * conforming to the list of requested properties. + * The Server class will filter out the extra. + * + * @param array $properties + * + * @return array + */ + public function getProperties($properties) + { + $r = []; + + foreach ($properties as $prop) { + switch ($prop) { + case '{http://calendarserver.org/ns/}source': + $r[$prop] = new Href($this->subscriptionInfo['source']); + break; + default: + if (array_key_exists($prop, $this->subscriptionInfo)) { + $r[$prop] = $this->subscriptionInfo[$prop]; + } + break; + } + } + + return $r; + } + + /** + * Returns the owner principal. + * + * This must be a url to a principal, or null if there's no owner + * + * @return string|null + */ + public function getOwner() + { + return $this->subscriptionInfo['principaluri']; + } + + /** + * Returns a list of ACE's for this node. + * + * Each ACE has the following properties: + * * 'privilege', a string such as {DAV:}read or {DAV:}write. These are + * currently the only supported privileges + * * 'principal', a url to the principal who owns the node + * * 'protected' (optional), indicating that this ACE is not allowed to + * be updated. + * + * @return array + */ + public function getACL() + { + return [ + [ + 'privilege' => '{DAV:}all', + 'principal' => $this->getOwner(), + 'protected' => true, + ], + [ + 'privilege' => '{DAV:}all', + 'principal' => $this->getOwner().'/calendar-proxy-write', + 'protected' => true, + ], + [ + 'privilege' => '{DAV:}read', + 'principal' => $this->getOwner().'/calendar-proxy-read', + 'protected' => true, + ], + ]; + } +} diff --git a/3rdparty/sabre/dav/lib/CalDAV/Xml/Filter/CalendarData.php b/3rdparty/sabre/dav/lib/CalDAV/Xml/Filter/CalendarData.php new file mode 100644 index 00000000..c9656d8a --- /dev/null +++ b/3rdparty/sabre/dav/lib/CalDAV/Xml/Filter/CalendarData.php @@ -0,0 +1,80 @@ +next(); + * + * $reader->parseInnerTree() will parse the entire sub-tree, and advance to + * the next element. + * + * @return mixed + */ + public static function xmlDeserialize(Reader $reader) + { + $result = [ + 'contentType' => $reader->getAttribute('content-type') ?: 'text/calendar', + 'version' => $reader->getAttribute('version') ?: '2.0', + ]; + + $elems = (array) $reader->parseInnerTree(); + foreach ($elems as $elem) { + switch ($elem['name']) { + case '{'.Plugin::NS_CALDAV.'}expand': + $result['expand'] = [ + 'start' => isset($elem['attributes']['start']) ? DateTimeParser::parseDateTime($elem['attributes']['start']) : null, + 'end' => isset($elem['attributes']['end']) ? DateTimeParser::parseDateTime($elem['attributes']['end']) : null, + ]; + + if (!$result['expand']['start'] || !$result['expand']['end']) { + throw new BadRequest('The "start" and "end" attributes are required when expanding calendar-data'); + } + if ($result['expand']['end'] <= $result['expand']['start']) { + throw new BadRequest('The end-date must be larger than the start-date when expanding calendar-data'); + } + break; + } + } + + return $result; + } +} diff --git a/3rdparty/sabre/dav/lib/CalDAV/Xml/Filter/CompFilter.php b/3rdparty/sabre/dav/lib/CalDAV/Xml/Filter/CompFilter.php new file mode 100644 index 00000000..929000bb --- /dev/null +++ b/3rdparty/sabre/dav/lib/CalDAV/Xml/Filter/CompFilter.php @@ -0,0 +1,94 @@ +next(); + * + * $reader->parseInnerTree() will parse the entire sub-tree, and advance to + * the next element. + * + * @return mixed + */ + public static function xmlDeserialize(Reader $reader) + { + $result = [ + 'name' => null, + 'is-not-defined' => false, + 'comp-filters' => [], + 'prop-filters' => [], + 'time-range' => false, + ]; + + $att = $reader->parseAttributes(); + $result['name'] = $att['name']; + + $elems = $reader->parseInnerTree(); + + if (is_array($elems)) { + foreach ($elems as $elem) { + switch ($elem['name']) { + case '{'.Plugin::NS_CALDAV.'}comp-filter': + $result['comp-filters'][] = $elem['value']; + break; + case '{'.Plugin::NS_CALDAV.'}prop-filter': + $result['prop-filters'][] = $elem['value']; + break; + case '{'.Plugin::NS_CALDAV.'}is-not-defined': + $result['is-not-defined'] = true; + break; + case '{'.Plugin::NS_CALDAV.'}time-range': + if ('VCALENDAR' === $result['name']) { + throw new BadRequest('You cannot add time-range filters on the VCALENDAR component'); + } + $result['time-range'] = [ + 'start' => isset($elem['attributes']['start']) ? DateTimeParser::parseDateTime($elem['attributes']['start']) : null, + 'end' => isset($elem['attributes']['end']) ? DateTimeParser::parseDateTime($elem['attributes']['end']) : null, + ]; + if ($result['time-range']['start'] && $result['time-range']['end'] && $result['time-range']['end'] <= $result['time-range']['start']) { + throw new BadRequest('The end-date must be larger than the start-date'); + } + break; + } + } + } + + return $result; + } +} diff --git a/3rdparty/sabre/dav/lib/CalDAV/Xml/Filter/ParamFilter.php b/3rdparty/sabre/dav/lib/CalDAV/Xml/Filter/ParamFilter.php new file mode 100644 index 00000000..1e6dd594 --- /dev/null +++ b/3rdparty/sabre/dav/lib/CalDAV/Xml/Filter/ParamFilter.php @@ -0,0 +1,79 @@ +next(); + * + * $reader->parseInnerTree() will parse the entire sub-tree, and advance to + * the next element. + * + * @return mixed + */ + public static function xmlDeserialize(Reader $reader) + { + $result = [ + 'name' => null, + 'is-not-defined' => false, + 'text-match' => null, + ]; + + $att = $reader->parseAttributes(); + $result['name'] = $att['name']; + + $elems = $reader->parseInnerTree(); + + if (is_array($elems)) { + foreach ($elems as $elem) { + switch ($elem['name']) { + case '{'.Plugin::NS_CALDAV.'}is-not-defined': + $result['is-not-defined'] = true; + break; + case '{'.Plugin::NS_CALDAV.'}text-match': + $result['text-match'] = [ + 'negate-condition' => isset($elem['attributes']['negate-condition']) && 'yes' === $elem['attributes']['negate-condition'], + 'collation' => isset($elem['attributes']['collation']) ? $elem['attributes']['collation'] : 'i;ascii-casemap', + 'value' => $elem['value'], + ]; + break; + } + } + } + + return $result; + } +} diff --git a/3rdparty/sabre/dav/lib/CalDAV/Xml/Filter/PropFilter.php b/3rdparty/sabre/dav/lib/CalDAV/Xml/Filter/PropFilter.php new file mode 100644 index 00000000..f1d66cc0 --- /dev/null +++ b/3rdparty/sabre/dav/lib/CalDAV/Xml/Filter/PropFilter.php @@ -0,0 +1,95 @@ +next(); + * + * $reader->parseInnerTree() will parse the entire sub-tree, and advance to + * the next element. + * + * @return mixed + */ + public static function xmlDeserialize(Reader $reader) + { + $result = [ + 'name' => null, + 'is-not-defined' => false, + 'param-filters' => [], + 'text-match' => null, + 'time-range' => [], + ]; + + $att = $reader->parseAttributes(); + $result['name'] = $att['name']; + + $elems = $reader->parseInnerTree(); + + if (is_array($elems)) { + foreach ($elems as $elem) { + switch ($elem['name']) { + case '{'.Plugin::NS_CALDAV.'}param-filter': + $result['param-filters'][] = $elem['value']; + break; + case '{'.Plugin::NS_CALDAV.'}is-not-defined': + $result['is-not-defined'] = true; + break; + case '{'.Plugin::NS_CALDAV.'}time-range': + $result['time-range'] = [ + 'start' => isset($elem['attributes']['start']) ? DateTimeParser::parseDateTime($elem['attributes']['start']) : null, + 'end' => isset($elem['attributes']['end']) ? DateTimeParser::parseDateTime($elem['attributes']['end']) : null, + ]; + if ($result['time-range']['start'] && $result['time-range']['end'] && $result['time-range']['end'] <= $result['time-range']['start']) { + throw new BadRequest('The end-date must be larger than the start-date'); + } + break; + case '{'.Plugin::NS_CALDAV.'}text-match': + $result['text-match'] = [ + 'negate-condition' => isset($elem['attributes']['negate-condition']) && 'yes' === $elem['attributes']['negate-condition'], + 'collation' => isset($elem['attributes']['collation']) ? $elem['attributes']['collation'] : 'i;ascii-casemap', + 'value' => $elem['value'], + ]; + break; + } + } + } + + return $result; + } +} diff --git a/3rdparty/sabre/dav/lib/CalDAV/Xml/Notification/Invite.php b/3rdparty/sabre/dav/lib/CalDAV/Xml/Notification/Invite.php new file mode 100644 index 00000000..2dbb0f49 --- /dev/null +++ b/3rdparty/sabre/dav/lib/CalDAV/Xml/Notification/Invite.php @@ -0,0 +1,290 @@ + $value) { + if (!property_exists($this, $key)) { + throw new \InvalidArgumentException('Unknown option: '.$key); + } + $this->$key = $value; + } + } + + /** + * The xmlSerialize method is called during xml writing. + * + * Use the $writer argument to write its own xml serialization. + * + * An important note: do _not_ create a parent element. Any element + * implementing XmlSerializable should only ever write what's considered + * its 'inner xml'. + * + * The parent of the current element is responsible for writing a + * containing element. + * + * This allows serializers to be re-used for different element names. + * + * If you are opening new elements, you must also close them again. + */ + public function xmlSerialize(Writer $writer) + { + $writer->writeElement('{'.CalDAV\Plugin::NS_CALENDARSERVER.'}invite-notification'); + } + + /** + * This method serializes the entire notification, as it is used in the + * response body. + */ + public function xmlSerializeFull(Writer $writer) + { + $cs = '{'.CalDAV\Plugin::NS_CALENDARSERVER.'}'; + + $this->dtStamp->setTimezone(new \DateTimeZone('GMT')); + $writer->writeElement($cs.'dtstamp', $this->dtStamp->format('Ymd\\THis\\Z')); + + $writer->startElement($cs.'invite-notification'); + + $writer->writeElement($cs.'uid', $this->id); + $writer->writeElement('{DAV:}href', $this->href); + + switch ($this->type) { + case DAV\Sharing\Plugin::INVITE_ACCEPTED: + $writer->writeElement($cs.'invite-accepted'); + break; + case DAV\Sharing\Plugin::INVITE_NORESPONSE: + $writer->writeElement($cs.'invite-noresponse'); + break; + } + + $writer->writeElement($cs.'hosturl', [ + '{DAV:}href' => $writer->contextUri.$this->hostUrl, + ]); + + if ($this->summary) { + $writer->writeElement($cs.'summary', $this->summary); + } + + $writer->startElement($cs.'access'); + if ($this->readOnly) { + $writer->writeElement($cs.'read'); + } else { + $writer->writeElement($cs.'read-write'); + } + $writer->endElement(); // access + + $writer->startElement($cs.'organizer'); + // If the organizer contains a 'mailto:' part, it means it should be + // treated as absolute. + if ('mailto:' === strtolower(substr($this->organizer, 0, 7))) { + $writer->writeElement('{DAV:}href', $this->organizer); + } else { + $writer->writeElement('{DAV:}href', $writer->contextUri.$this->organizer); + } + if ($this->commonName) { + $writer->writeElement($cs.'common-name', $this->commonName); + } + if ($this->firstName) { + $writer->writeElement($cs.'first-name', $this->firstName); + } + if ($this->lastName) { + $writer->writeElement($cs.'last-name', $this->lastName); + } + $writer->endElement(); // organizer + + if ($this->commonName) { + $writer->writeElement($cs.'organizer-cn', $this->commonName); + } + if ($this->firstName) { + $writer->writeElement($cs.'organizer-first', $this->firstName); + } + if ($this->lastName) { + $writer->writeElement($cs.'organizer-last', $this->lastName); + } + if ($this->supportedComponents) { + $writer->writeElement('{'.CalDAV\Plugin::NS_CALDAV.'}supported-calendar-component-set', $this->supportedComponents); + } + + $writer->endElement(); // invite-notification + } + + /** + * Returns a unique id for this notification. + * + * This is just the base url. This should generally be some kind of unique + * id. + * + * @return string + */ + public function getId() + { + return $this->id; + } + + /** + * Returns the ETag for this notification. + * + * The ETag must be surrounded by literal double-quotes. + * + * @return string + */ + public function getETag() + { + return $this->etag; + } +} diff --git a/3rdparty/sabre/dav/lib/CalDAV/Xml/Notification/InviteReply.php b/3rdparty/sabre/dav/lib/CalDAV/Xml/Notification/InviteReply.php new file mode 100644 index 00000000..dbdba3b0 --- /dev/null +++ b/3rdparty/sabre/dav/lib/CalDAV/Xml/Notification/InviteReply.php @@ -0,0 +1,199 @@ + $value) { + if (!property_exists($this, $key)) { + throw new \InvalidArgumentException('Unknown option: '.$key); + } + $this->$key = $value; + } + } + + /** + * The xmlSerialize method is called during xml writing. + * + * Use the $writer argument to write its own xml serialization. + * + * An important note: do _not_ create a parent element. Any element + * implementing XmlSerializable should only ever write what's considered + * its 'inner xml'. + * + * The parent of the current element is responsible for writing a + * containing element. + * + * This allows serializers to be re-used for different element names. + * + * If you are opening new elements, you must also close them again. + */ + public function xmlSerialize(Writer $writer) + { + $writer->writeElement('{'.CalDAV\Plugin::NS_CALENDARSERVER.'}invite-reply'); + } + + /** + * This method serializes the entire notification, as it is used in the + * response body. + */ + public function xmlSerializeFull(Writer $writer) + { + $cs = '{'.CalDAV\Plugin::NS_CALENDARSERVER.'}'; + + $this->dtStamp->setTimezone(new \DateTimeZone('GMT')); + $writer->writeElement($cs.'dtstamp', $this->dtStamp->format('Ymd\\THis\\Z')); + + $writer->startElement($cs.'invite-reply'); + + $writer->writeElement($cs.'uid', $this->id); + $writer->writeElement($cs.'in-reply-to', $this->inReplyTo); + $writer->writeElement('{DAV:}href', $this->href); + + switch ($this->type) { + case DAV\Sharing\Plugin::INVITE_ACCEPTED: + $writer->writeElement($cs.'invite-accepted'); + break; + case DAV\Sharing\Plugin::INVITE_DECLINED: + $writer->writeElement($cs.'invite-declined'); + break; + } + + $writer->writeElement($cs.'hosturl', [ + '{DAV:}href' => $writer->contextUri.$this->hostUrl, + ]); + + if ($this->summary) { + $writer->writeElement($cs.'summary', $this->summary); + } + $writer->endElement(); // invite-reply + } + + /** + * Returns a unique id for this notification. + * + * This is just the base url. This should generally be some kind of unique + * id. + * + * @return string + */ + public function getId() + { + return $this->id; + } + + /** + * Returns the ETag for this notification. + * + * The ETag must be surrounded by literal double-quotes. + * + * @return string + */ + public function getETag() + { + return $this->etag; + } +} diff --git a/3rdparty/sabre/dav/lib/CalDAV/Xml/Notification/NotificationInterface.php b/3rdparty/sabre/dav/lib/CalDAV/Xml/Notification/NotificationInterface.php new file mode 100644 index 00000000..e1b393f8 --- /dev/null +++ b/3rdparty/sabre/dav/lib/CalDAV/Xml/Notification/NotificationInterface.php @@ -0,0 +1,43 @@ +id = $id; + $this->type = $type; + $this->description = $description; + $this->href = $href; + $this->etag = $etag; + } + + /** + * The serialize method is called during xml writing. + * + * It should use the $writer argument to encode this object into XML. + * + * Important note: it is not needed to create the parent element. The + * parent element is already created, and we only have to worry about + * attributes, child elements and text (if any). + * + * Important note 2: If you are writing any new elements, you are also + * responsible for closing them. + */ + public function xmlSerialize(Writer $writer) + { + switch ($this->type) { + case self::TYPE_LOW: + $type = 'low'; + break; + case self::TYPE_MEDIUM: + $type = 'medium'; + break; + default: + case self::TYPE_HIGH: + $type = 'high'; + break; + } + + $writer->startElement('{'.Plugin::NS_CALENDARSERVER.'}systemstatus'); + $writer->writeAttribute('type', $type); + $writer->endElement(); + } + + /** + * This method serializes the entire notification, as it is used in the + * response body. + */ + public function xmlSerializeFull(Writer $writer) + { + $cs = '{'.Plugin::NS_CALENDARSERVER.'}'; + switch ($this->type) { + case self::TYPE_LOW: + $type = 'low'; + break; + case self::TYPE_MEDIUM: + $type = 'medium'; + break; + default: + case self::TYPE_HIGH: + $type = 'high'; + break; + } + + $writer->startElement($cs.'systemstatus'); + $writer->writeAttribute('type', $type); + + if ($this->description) { + $writer->writeElement($cs.'description', $this->description); + } + if ($this->href) { + $writer->writeElement('{DAV:}href', $this->href); + } + + $writer->endElement(); // systemstatus + } + + /** + * Returns a unique id for this notification. + * + * This is just the base url. This should generally be some kind of unique + * id. + * + * @return string + */ + public function getId() + { + return $this->id; + } + + /* + * Returns the ETag for this notification. + * + * The ETag must be surrounded by literal double-quotes. + * + * @return string + */ + public function getETag() + { + return $this->etag; + } +} diff --git a/3rdparty/sabre/dav/lib/CalDAV/Xml/Property/AllowedSharingModes.php b/3rdparty/sabre/dav/lib/CalDAV/Xml/Property/AllowedSharingModes.php new file mode 100644 index 00000000..58acb6d5 --- /dev/null +++ b/3rdparty/sabre/dav/lib/CalDAV/Xml/Property/AllowedSharingModes.php @@ -0,0 +1,81 @@ +canBeShared = $canBeShared; + $this->canBePublished = $canBePublished; + } + + /** + * The xmlSerialize method is called during xml writing. + * + * Use the $writer argument to write its own xml serialization. + * + * An important note: do _not_ create a parent element. Any element + * implementing XmlSerializable should only ever write what's considered + * its 'inner xml'. + * + * The parent of the current element is responsible for writing a + * containing element. + * + * This allows serializers to be re-used for different element names. + * + * If you are opening new elements, you must also close them again. + */ + public function xmlSerialize(Writer $writer) + { + if ($this->canBeShared) { + $writer->writeElement('{'.Plugin::NS_CALENDARSERVER.'}can-be-shared'); + } + if ($this->canBePublished) { + $writer->writeElement('{'.Plugin::NS_CALENDARSERVER.'}can-be-published'); + } + } +} diff --git a/3rdparty/sabre/dav/lib/CalDAV/Xml/Property/EmailAddressSet.php b/3rdparty/sabre/dav/lib/CalDAV/Xml/Property/EmailAddressSet.php new file mode 100644 index 00000000..84f7ae02 --- /dev/null +++ b/3rdparty/sabre/dav/lib/CalDAV/Xml/Property/EmailAddressSet.php @@ -0,0 +1,71 @@ +emails = $emails; + } + + /** + * Returns the email addresses. + * + * @return array + */ + public function getValue() + { + return $this->emails; + } + + /** + * The xmlSerialize method is called during xml writing. + * + * Use the $writer argument to write its own xml serialization. + * + * An important note: do _not_ create a parent element. Any element + * implementing XmlSerializable should only ever write what's considered + * its 'inner xml'. + * + * The parent of the current element is responsible for writing a + * containing element. + * + * This allows serializers to be re-used for different element names. + * + * If you are opening new elements, you must also close them again. + */ + public function xmlSerialize(Writer $writer) + { + foreach ($this->emails as $email) { + $writer->writeElement('{http://calendarserver.org/ns/}email-address', $email); + } + } +} diff --git a/3rdparty/sabre/dav/lib/CalDAV/Xml/Property/Invite.php b/3rdparty/sabre/dav/lib/CalDAV/Xml/Property/Invite.php new file mode 100644 index 00000000..c389ca82 --- /dev/null +++ b/3rdparty/sabre/dav/lib/CalDAV/Xml/Property/Invite.php @@ -0,0 +1,120 @@ +sharees = $sharees; + } + + /** + * Returns the list of users, as it was passed to the constructor. + * + * @return array + */ + public function getValue() + { + return $this->sharees; + } + + /** + * The xmlSerialize method is called during xml writing. + * + * Use the $writer argument to write its own xml serialization. + * + * An important note: do _not_ create a parent element. Any element + * implementing XmlSerializable should only ever write what's considered + * its 'inner xml'. + * + * The parent of the current element is responsible for writing a + * containing element. + * + * This allows serializers to be re-used for different element names. + * + * If you are opening new elements, you must also close them again. + */ + public function xmlSerialize(Writer $writer) + { + $cs = '{'.Plugin::NS_CALENDARSERVER.'}'; + + foreach ($this->sharees as $sharee) { + if (DAV\Sharing\Plugin::ACCESS_SHAREDOWNER === $sharee->access) { + $writer->startElement($cs.'organizer'); + } else { + $writer->startElement($cs.'user'); + + switch ($sharee->inviteStatus) { + case DAV\Sharing\Plugin::INVITE_ACCEPTED: + $writer->writeElement($cs.'invite-accepted'); + break; + case DAV\Sharing\Plugin::INVITE_DECLINED: + $writer->writeElement($cs.'invite-declined'); + break; + case DAV\Sharing\Plugin::INVITE_NORESPONSE: + $writer->writeElement($cs.'invite-noresponse'); + break; + case DAV\Sharing\Plugin::INVITE_INVALID: + $writer->writeElement($cs.'invite-invalid'); + break; + } + + $writer->startElement($cs.'access'); + switch ($sharee->access) { + case DAV\Sharing\Plugin::ACCESS_READWRITE: + $writer->writeElement($cs.'read-write'); + break; + case DAV\Sharing\Plugin::ACCESS_READ: + $writer->writeElement($cs.'read'); + break; + } + $writer->endElement(); // access + } + + $href = new DAV\Xml\Property\Href($sharee->href); + $href->xmlSerialize($writer); + + if (isset($sharee->properties['{DAV:}displayname'])) { + $writer->writeElement($cs.'common-name', $sharee->properties['{DAV:}displayname']); + } + if ($sharee->comment) { + $writer->writeElement($cs.'summary', $sharee->comment); + } + $writer->endElement(); // organizer or user + } + } +} diff --git a/3rdparty/sabre/dav/lib/CalDAV/Xml/Property/ScheduleCalendarTransp.php b/3rdparty/sabre/dav/lib/CalDAV/Xml/Property/ScheduleCalendarTransp.php new file mode 100644 index 00000000..15952202 --- /dev/null +++ b/3rdparty/sabre/dav/lib/CalDAV/Xml/Property/ScheduleCalendarTransp.php @@ -0,0 +1,124 @@ +value = $value; + } + + /** + * Returns the current value. + * + * @return string + */ + public function getValue() + { + return $this->value; + } + + /** + * The xmlSerialize method is called during xml writing. + * + * Use the $writer argument to write its own xml serialization. + * + * An important note: do _not_ create a parent element. Any element + * implementing XmlSerializable should only ever write what's considered + * its 'inner xml'. + * + * The parent of the current element is responsible for writing a + * containing element. + * + * This allows serializers to be re-used for different element names. + * + * If you are opening new elements, you must also close them again. + */ + public function xmlSerialize(Writer $writer) + { + switch ($this->value) { + case self::TRANSPARENT: + $writer->writeElement('{'.Plugin::NS_CALDAV.'}transparent'); + break; + case self::OPAQUE: + $writer->writeElement('{'.Plugin::NS_CALDAV.'}opaque'); + break; + } + } + + /** + * The deserialize method is called during xml parsing. + * + * This method is called statically, this is because in theory this method + * may be used as a type of constructor, or factory method. + * + * Often you want to return an instance of the current class, but you are + * free to return other data as well. + * + * You are responsible for advancing the reader to the next element. Not + * doing anything will result in a never-ending loop. + * + * If you just want to skip parsing for this element altogether, you can + * just call $reader->next(); + * + * $reader->parseInnerTree() will parse the entire sub-tree, and advance to + * the next element. + * + * @return mixed + */ + public static function xmlDeserialize(Reader $reader) + { + $elems = Deserializer\enum($reader, Plugin::NS_CALDAV); + + if (in_array('transparent', $elems)) { + $value = self::TRANSPARENT; + } else { + $value = self::OPAQUE; + } + + return new self($value); + } +} diff --git a/3rdparty/sabre/dav/lib/CalDAV/Xml/Property/SupportedCalendarComponentSet.php b/3rdparty/sabre/dav/lib/CalDAV/Xml/Property/SupportedCalendarComponentSet.php new file mode 100644 index 00000000..d86e7b77 --- /dev/null +++ b/3rdparty/sabre/dav/lib/CalDAV/Xml/Property/SupportedCalendarComponentSet.php @@ -0,0 +1,118 @@ +components = $components; + } + + /** + * Returns the list of supported components. + * + * @return array + */ + public function getValue() + { + return $this->components; + } + + /** + * The xmlSerialize method is called during xml writing. + * + * Use the $writer argument to write its own xml serialization. + * + * An important note: do _not_ create a parent element. Any element + * implementing XmlSerializable should only ever write what's considered + * its 'inner xml'. + * + * The parent of the current element is responsible for writing a + * containing element. + * + * This allows serializers to be re-used for different element names. + * + * If you are opening new elements, you must also close them again. + */ + public function xmlSerialize(Writer $writer) + { + foreach ($this->components as $component) { + $writer->startElement('{'.Plugin::NS_CALDAV.'}comp'); + $writer->writeAttributes(['name' => $component]); + $writer->endElement(); + } + } + + /** + * The deserialize method is called during xml parsing. + * + * This method is called statically, this is because in theory this method + * may be used as a type of constructor, or factory method. + * + * Often you want to return an instance of the current class, but you are + * free to return other data as well. + * + * You are responsible for advancing the reader to the next element. Not + * doing anything will result in a never-ending loop. + * + * If you just want to skip parsing for this element altogether, you can + * just call $reader->next(); + * + * $reader->parseInnerTree() will parse the entire sub-tree, and advance to + * the next element. + * + * @return mixed + */ + public static function xmlDeserialize(Reader $reader) + { + $elems = $reader->parseInnerTree(); + + $components = []; + + foreach ((array) $elems as $elem) { + if ($elem['name'] === '{'.Plugin::NS_CALDAV.'}comp') { + $components[] = $elem['attributes']['name']; + } + } + + if (!$components) { + throw new ParseException('supported-calendar-component-set must have at least one CALDAV:comp element'); + } + + return new self($components); + } +} diff --git a/3rdparty/sabre/dav/lib/CalDAV/Xml/Property/SupportedCalendarData.php b/3rdparty/sabre/dav/lib/CalDAV/Xml/Property/SupportedCalendarData.php new file mode 100644 index 00000000..5b089330 --- /dev/null +++ b/3rdparty/sabre/dav/lib/CalDAV/Xml/Property/SupportedCalendarData.php @@ -0,0 +1,57 @@ +startElement('{'.Plugin::NS_CALDAV.'}calendar-data'); + $writer->writeAttributes([ + 'content-type' => 'text/calendar', + 'version' => '2.0', + ]); + $writer->endElement(); // calendar-data + $writer->startElement('{'.Plugin::NS_CALDAV.'}calendar-data'); + $writer->writeAttributes([ + 'content-type' => 'application/calendar+json', + ]); + $writer->endElement(); // calendar-data + } +} diff --git a/3rdparty/sabre/dav/lib/CalDAV/Xml/Property/SupportedCollationSet.php b/3rdparty/sabre/dav/lib/CalDAV/Xml/Property/SupportedCollationSet.php new file mode 100644 index 00000000..c5ffeee3 --- /dev/null +++ b/3rdparty/sabre/dav/lib/CalDAV/Xml/Property/SupportedCollationSet.php @@ -0,0 +1,54 @@ +writeElement('{'.Plugin::NS_CALDAV.'}supported-collation', $collation); + } + } +} diff --git a/3rdparty/sabre/dav/lib/CalDAV/Xml/Request/CalendarMultiGetReport.php b/3rdparty/sabre/dav/lib/CalDAV/Xml/Request/CalendarMultiGetReport.php new file mode 100644 index 00000000..4771a207 --- /dev/null +++ b/3rdparty/sabre/dav/lib/CalDAV/Xml/Request/CalendarMultiGetReport.php @@ -0,0 +1,119 @@ +next(); + * + * $reader->parseInnerTree() will parse the entire sub-tree, and advance to + * the next element. + * + * @return mixed + */ + public static function xmlDeserialize(Reader $reader) + { + $elems = $reader->parseInnerTree([ + '{urn:ietf:params:xml:ns:caldav}calendar-data' => 'Sabre\\CalDAV\\Xml\\Filter\\CalendarData', + '{DAV:}prop' => 'Sabre\\Xml\\Element\\KeyValue', + ]); + + $newProps = [ + 'hrefs' => [], + 'properties' => [], + ]; + + foreach ($elems as $elem) { + switch ($elem['name']) { + case '{DAV:}prop': + $newProps['properties'] = array_keys($elem['value']); + if (isset($elem['value']['{'.Plugin::NS_CALDAV.'}calendar-data'])) { + $newProps += $elem['value']['{'.Plugin::NS_CALDAV.'}calendar-data']; + } + break; + case '{DAV:}href': + $newProps['hrefs'][] = Uri\resolve($reader->contextUri, $elem['value']); + break; + } + } + + $obj = new self(); + foreach ($newProps as $key => $value) { + $obj->$key = $value; + } + + return $obj; + } +} diff --git a/3rdparty/sabre/dav/lib/CalDAV/Xml/Request/CalendarQueryReport.php b/3rdparty/sabre/dav/lib/CalDAV/Xml/Request/CalendarQueryReport.php new file mode 100644 index 00000000..5a4df467 --- /dev/null +++ b/3rdparty/sabre/dav/lib/CalDAV/Xml/Request/CalendarQueryReport.php @@ -0,0 +1,137 @@ +next(); + * + * $reader->parseInnerTree() will parse the entire sub-tree, and advance to + * the next element. + * + * @return mixed + */ + public static function xmlDeserialize(Reader $reader) + { + $elems = $reader->parseInnerTree([ + '{urn:ietf:params:xml:ns:caldav}comp-filter' => 'Sabre\\CalDAV\\Xml\\Filter\\CompFilter', + '{urn:ietf:params:xml:ns:caldav}prop-filter' => 'Sabre\\CalDAV\\Xml\\Filter\\PropFilter', + '{urn:ietf:params:xml:ns:caldav}param-filter' => 'Sabre\\CalDAV\\Xml\\Filter\\ParamFilter', + '{urn:ietf:params:xml:ns:caldav}calendar-data' => 'Sabre\\CalDAV\\Xml\\Filter\\CalendarData', + '{DAV:}prop' => 'Sabre\\Xml\\Element\\KeyValue', + ]); + + $newProps = [ + 'filters' => null, + 'properties' => [], + ]; + + if (!is_array($elems)) { + $elems = []; + } + + foreach ($elems as $elem) { + switch ($elem['name']) { + case '{DAV:}prop': + $newProps['properties'] = array_keys($elem['value']); + if (isset($elem['value']['{'.Plugin::NS_CALDAV.'}calendar-data'])) { + $newProps += $elem['value']['{'.Plugin::NS_CALDAV.'}calendar-data']; + } + break; + case '{'.Plugin::NS_CALDAV.'}filter': + foreach ($elem['value'] as $subElem) { + if ($subElem['name'] === '{'.Plugin::NS_CALDAV.'}comp-filter') { + if (!is_null($newProps['filters'])) { + throw new BadRequest('Only one top-level comp-filter may be defined'); + } + $newProps['filters'] = $subElem['value']; + } + } + break; + } + } + + if (is_null($newProps['filters'])) { + throw new BadRequest('The {'.Plugin::NS_CALDAV.'}filter element is required for this request'); + } + + $obj = new self(); + foreach ($newProps as $key => $value) { + $obj->$key = $value; + } + + return $obj; + } +} diff --git a/3rdparty/sabre/dav/lib/CalDAV/Xml/Request/FreeBusyQueryReport.php b/3rdparty/sabre/dav/lib/CalDAV/Xml/Request/FreeBusyQueryReport.php new file mode 100644 index 00000000..17df05a7 --- /dev/null +++ b/3rdparty/sabre/dav/lib/CalDAV/Xml/Request/FreeBusyQueryReport.php @@ -0,0 +1,90 @@ +next(); + * + * $reader->parseInnerTree() will parse the entire sub-tree, and advance to + * the next element. + * + * @return mixed + */ + public static function xmlDeserialize(Reader $reader) + { + $timeRange = '{'.Plugin::NS_CALDAV.'}time-range'; + + $start = null; + $end = null; + + foreach ((array) $reader->parseInnerTree([]) as $elem) { + if ($elem['name'] !== $timeRange) { + continue; + } + + $start = empty($elem['attributes']['start']) ?: $elem['attributes']['start']; + $end = empty($elem['attributes']['end']) ?: $elem['attributes']['end']; + } + if (!$start && !$end) { + throw new BadRequest('The freebusy report must have a time-range element'); + } + if ($start) { + $start = DateTimeParser::parseDateTime($start); + } + if ($end) { + $end = DateTimeParser::parseDateTime($end); + } + $result = new self(); + $result->start = $start; + $result->end = $end; + + return $result; + } +} diff --git a/3rdparty/sabre/dav/lib/CalDAV/Xml/Request/InviteReply.php b/3rdparty/sabre/dav/lib/CalDAV/Xml/Request/InviteReply.php new file mode 100644 index 00000000..166721eb --- /dev/null +++ b/3rdparty/sabre/dav/lib/CalDAV/Xml/Request/InviteReply.php @@ -0,0 +1,145 @@ +href = $href; + $this->calendarUri = $calendarUri; + $this->inReplyTo = $inReplyTo; + $this->summary = $summary; + $this->status = $status; + } + + /** + * The deserialize method is called during xml parsing. + * + * This method is called statically, this is because in theory this method + * may be used as a type of constructor, or factory method. + * + * Often you want to return an instance of the current class, but you are + * free to return other data as well. + * + * You are responsible for advancing the reader to the next element. Not + * doing anything will result in a never-ending loop. + * + * If you just want to skip parsing for this element altogether, you can + * just call $reader->next(); + * + * $reader->parseInnerTree() will parse the entire sub-tree, and advance to + * the next element. + * + * @return mixed + */ + public static function xmlDeserialize(Reader $reader) + { + $elems = KeyValue::xmlDeserialize($reader); + + $href = null; + $calendarUri = null; + $inReplyTo = null; + $summary = null; + $status = null; + + foreach ($elems as $name => $value) { + switch ($name) { + case '{'.Plugin::NS_CALENDARSERVER.'}hosturl': + foreach ($value as $bla) { + if ('{DAV:}href' === $bla['name']) { + $calendarUri = $bla['value']; + } + } + break; + case '{'.Plugin::NS_CALENDARSERVER.'}invite-accepted': + $status = DAV\Sharing\Plugin::INVITE_ACCEPTED; + break; + case '{'.Plugin::NS_CALENDARSERVER.'}invite-declined': + $status = DAV\Sharing\Plugin::INVITE_DECLINED; + break; + case '{'.Plugin::NS_CALENDARSERVER.'}in-reply-to': + $inReplyTo = $value; + break; + case '{'.Plugin::NS_CALENDARSERVER.'}summary': + $summary = $value; + break; + case '{DAV:}href': + $href = $value; + break; + } + } + if (is_null($calendarUri)) { + throw new BadRequest('The {http://calendarserver.org/ns/}hosturl/{DAV:}href element must exist'); + } + + return new self($href, $calendarUri, $inReplyTo, $summary, $status); + } +} diff --git a/3rdparty/sabre/dav/lib/CalDAV/Xml/Request/MkCalendar.php b/3rdparty/sabre/dav/lib/CalDAV/Xml/Request/MkCalendar.php new file mode 100644 index 00000000..b5701e2e --- /dev/null +++ b/3rdparty/sabre/dav/lib/CalDAV/Xml/Request/MkCalendar.php @@ -0,0 +1,77 @@ +properties; + } + + /** + * The deserialize method is called during xml parsing. + * + * This method is called statically, this is because in theory this method + * may be used as a type of constructor, or factory method. + * + * Often you want to return an instance of the current class, but you are + * free to return other data as well. + * + * You are responsible for advancing the reader to the next element. Not + * doing anything will result in a never-ending loop. + * + * If you just want to skip parsing for this element altogether, you can + * just call $reader->next(); + * + * $reader->parseInnerTree() will parse the entire sub-tree, and advance to + * the next element. + * + * @return mixed + */ + public static function xmlDeserialize(Reader $reader) + { + $self = new self(); + + $elementMap = $reader->elementMap; + $elementMap['{DAV:}prop'] = 'Sabre\DAV\Xml\Element\Prop'; + $elementMap['{DAV:}set'] = 'Sabre\Xml\Element\KeyValue'; + $elems = $reader->parseInnerTree($elementMap); + + foreach ($elems as $elem) { + if ('{DAV:}set' === $elem['name']) { + $self->properties = array_merge($self->properties, $elem['value']['{DAV:}prop']); + } + } + + return $self; + } +} diff --git a/3rdparty/sabre/dav/lib/CalDAV/Xml/Request/Share.php b/3rdparty/sabre/dav/lib/CalDAV/Xml/Request/Share.php new file mode 100644 index 00000000..d597b76f --- /dev/null +++ b/3rdparty/sabre/dav/lib/CalDAV/Xml/Request/Share.php @@ -0,0 +1,107 @@ +sharees = $sharees; + } + + /** + * The deserialize method is called during xml parsing. + * + * This method is called statically, this is because in theory this method + * may be used as a type of constructor, or factory method. + * + * Often you want to return an instance of the current class, but you are + * free to return other data as well. + * + * You are responsible for advancing the reader to the next element. Not + * doing anything will result in a never-ending loop. + * + * If you just want to skip parsing for this element altogether, you can + * just call $reader->next(); + * + * $reader->parseInnerTree() will parse the entire sub-tree, and advance to + * the next element. + * + * @return mixed + */ + public static function xmlDeserialize(Reader $reader) + { + $elems = $reader->parseGetElements([ + '{'.Plugin::NS_CALENDARSERVER.'}set' => 'Sabre\\Xml\\Element\\KeyValue', + '{'.Plugin::NS_CALENDARSERVER.'}remove' => 'Sabre\\Xml\\Element\\KeyValue', + ]); + + $sharees = []; + + foreach ($elems as $elem) { + switch ($elem['name']) { + case '{'.Plugin::NS_CALENDARSERVER.'}set': + $sharee = $elem['value']; + + $sumElem = '{'.Plugin::NS_CALENDARSERVER.'}summary'; + $commonName = '{'.Plugin::NS_CALENDARSERVER.'}common-name'; + + $properties = []; + if (isset($sharee[$commonName])) { + $properties['{DAV:}displayname'] = $sharee[$commonName]; + } + + $access = array_key_exists('{'.Plugin::NS_CALENDARSERVER.'}read-write', $sharee) + ? \Sabre\DAV\Sharing\Plugin::ACCESS_READWRITE + : \Sabre\DAV\Sharing\Plugin::ACCESS_READ; + + $sharees[] = new Sharee([ + 'href' => $sharee['{DAV:}href'], + 'properties' => $properties, + 'access' => $access, + 'comment' => isset($sharee[$sumElem]) ? $sharee[$sumElem] : null, + ]); + break; + + case '{'.Plugin::NS_CALENDARSERVER.'}remove': + $sharees[] = new Sharee([ + 'href' => $elem['value']['{DAV:}href'], + 'access' => \Sabre\DAV\Sharing\Plugin::ACCESS_NOACCESS, + ]); + break; + } + } + + return new self($sharees); + } +} diff --git a/3rdparty/sabre/dav/lib/CardDAV/AddressBook.php b/3rdparty/sabre/dav/lib/CardDAV/AddressBook.php new file mode 100644 index 00000000..f5744f64 --- /dev/null +++ b/3rdparty/sabre/dav/lib/CardDAV/AddressBook.php @@ -0,0 +1,335 @@ +carddavBackend = $carddavBackend; + $this->addressBookInfo = $addressBookInfo; + } + + /** + * Returns the name of the addressbook. + * + * @return string + */ + public function getName() + { + return $this->addressBookInfo['uri']; + } + + /** + * Returns a card. + * + * @param string $name + * + * @return Card + */ + public function getChild($name) + { + $obj = $this->carddavBackend->getCard($this->addressBookInfo['id'], $name); + if (!$obj) { + throw new DAV\Exception\NotFound('Card not found'); + } + + return new Card($this->carddavBackend, $this->addressBookInfo, $obj); + } + + /** + * Returns the full list of cards. + * + * @return array + */ + public function getChildren() + { + $objs = $this->carddavBackend->getCards($this->addressBookInfo['id']); + $children = []; + foreach ($objs as $obj) { + $obj['acl'] = $this->getChildACL(); + $children[] = new Card($this->carddavBackend, $this->addressBookInfo, $obj); + } + + return $children; + } + + /** + * This method receives a list of paths in it's first argument. + * It must return an array with Node objects. + * + * If any children are not found, you do not have to return them. + * + * @param string[] $paths + * + * @return array + */ + public function getMultipleChildren(array $paths) + { + $objs = $this->carddavBackend->getMultipleCards($this->addressBookInfo['id'], $paths); + $children = []; + foreach ($objs as $obj) { + $obj['acl'] = $this->getChildACL(); + $children[] = new Card($this->carddavBackend, $this->addressBookInfo, $obj); + } + + return $children; + } + + /** + * Creates a new directory. + * + * We actually block this, as subdirectories are not allowed in addressbooks. + * + * @param string $name + */ + public function createDirectory($name) + { + throw new DAV\Exception\MethodNotAllowed('Creating collections in addressbooks is not allowed'); + } + + /** + * Creates a new file. + * + * The contents of the new file must be a valid VCARD. + * + * This method may return an ETag. + * + * @param string $name + * @param resource $data + * + * @return string|null + */ + public function createFile($name, $data = null) + { + if (is_resource($data)) { + $data = stream_get_contents($data); + } + // Converting to UTF-8, if needed + $data = DAV\StringUtil::ensureUTF8($data); + + return $this->carddavBackend->createCard($this->addressBookInfo['id'], $name, $data); + } + + /** + * Deletes the entire addressbook. + */ + public function delete() + { + $this->carddavBackend->deleteAddressBook($this->addressBookInfo['id']); + } + + /** + * Renames the addressbook. + * + * @param string $newName + */ + public function setName($newName) + { + throw new DAV\Exception\MethodNotAllowed('Renaming addressbooks is not yet supported'); + } + + /** + * Returns the last modification date as a unix timestamp. + */ + public function getLastModified() + { + return null; + } + + /** + * Updates properties on this node. + * + * This method received a PropPatch object, which contains all the + * information about the update. + * + * To update specific properties, call the 'handle' method on this object. + * Read the PropPatch documentation for more information. + */ + public function propPatch(DAV\PropPatch $propPatch) + { + return $this->carddavBackend->updateAddressBook($this->addressBookInfo['id'], $propPatch); + } + + /** + * Returns a list of properties for this nodes. + * + * The properties list is a list of propertynames the client requested, + * encoded in clark-notation {xmlnamespace}tagname + * + * If the array is empty, it means 'all properties' were requested. + * + * @param array $properties + * + * @return array + */ + public function getProperties($properties) + { + $response = []; + foreach ($properties as $propertyName) { + if (isset($this->addressBookInfo[$propertyName])) { + $response[$propertyName] = $this->addressBookInfo[$propertyName]; + } + } + + return $response; + } + + /** + * Returns the owner principal. + * + * This must be a url to a principal, or null if there's no owner + * + * @return string|null + */ + public function getOwner() + { + return $this->addressBookInfo['principaluri']; + } + + /** + * This method returns the ACL's for card nodes in this address book. + * The result of this method automatically gets passed to the + * card nodes in this address book. + * + * @return array + */ + public function getChildACL() + { + return [ + [ + 'privilege' => '{DAV:}all', + 'principal' => $this->getOwner(), + 'protected' => true, + ], + ]; + } + + /** + * This method returns the current sync-token for this collection. + * This can be any string. + * + * If null is returned from this function, the plugin assumes there's no + * sync information available. + * + * @return string|null + */ + public function getSyncToken() + { + if ( + $this->carddavBackend instanceof Backend\SyncSupport && + isset($this->addressBookInfo['{DAV:}sync-token']) + ) { + return $this->addressBookInfo['{DAV:}sync-token']; + } + if ( + $this->carddavBackend instanceof Backend\SyncSupport && + isset($this->addressBookInfo['{http://sabredav.org/ns}sync-token']) + ) { + return $this->addressBookInfo['{http://sabredav.org/ns}sync-token']; + } + } + + /** + * The getChanges method returns all the changes that have happened, since + * the specified syncToken and the current collection. + * + * This function should return an array, such as the following: + * + * [ + * 'syncToken' => 'The current synctoken', + * 'added' => [ + * 'new.txt', + * ], + * 'modified' => [ + * 'modified.txt', + * ], + * 'deleted' => [ + * 'foo.php.bak', + * 'old.txt' + * ] + * ]; + * + * The syncToken property should reflect the *current* syncToken of the + * collection, as reported getSyncToken(). This is needed here too, to + * ensure the operation is atomic. + * + * If the syncToken is specified as null, this is an initial sync, and all + * members should be reported. + * + * The modified property is an array of nodenames that have changed since + * the last token. + * + * The deleted property is an array with nodenames, that have been deleted + * from collection. + * + * The second argument is basically the 'depth' of the report. If it's 1, + * you only have to report changes that happened only directly in immediate + * descendants. If it's 2, it should also include changes from the nodes + * below the child collections. (grandchildren) + * + * The third (optional) argument allows a client to specify how many + * results should be returned at most. If the limit is not specified, it + * should be treated as infinite. + * + * If the limit (infinite or not) is higher than you're willing to return, + * you should throw a Sabre\DAV\Exception\TooMuchMatches() exception. + * + * If the syncToken is expired (due to data cleanup) or unknown, you must + * return null. + * + * The limit is 'suggestive'. You are free to ignore it. + * + * @param string $syncToken + * @param int $syncLevel + * @param int $limit + * + * @return array|null + */ + public function getChanges($syncToken, $syncLevel, $limit = null) + { + if (!$this->carddavBackend instanceof Backend\SyncSupport) { + return null; + } + + return $this->carddavBackend->getChangesForAddressBook( + $this->addressBookInfo['id'], + $syncToken, + $syncLevel, + $limit + ); + } +} diff --git a/3rdparty/sabre/dav/lib/CardDAV/AddressBookHome.php b/3rdparty/sabre/dav/lib/CardDAV/AddressBookHome.php new file mode 100644 index 00000000..d7365fbe --- /dev/null +++ b/3rdparty/sabre/dav/lib/CardDAV/AddressBookHome.php @@ -0,0 +1,178 @@ +carddavBackend = $carddavBackend; + $this->principalUri = $principalUri; + } + + /** + * Returns the name of this object. + * + * @return string + */ + public function getName() + { + list(, $name) = Uri\split($this->principalUri); + + return $name; + } + + /** + * Updates the name of this object. + * + * @param string $name + */ + public function setName($name) + { + throw new DAV\Exception\MethodNotAllowed(); + } + + /** + * Deletes this object. + */ + public function delete() + { + throw new DAV\Exception\MethodNotAllowed(); + } + + /** + * Returns the last modification date. + * + * @return int + */ + public function getLastModified() + { + return null; + } + + /** + * Creates a new file under this object. + * + * This is currently not allowed + * + * @param string $name + * @param resource $data + */ + public function createFile($name, $data = null) + { + throw new DAV\Exception\MethodNotAllowed('Creating new files in this collection is not supported'); + } + + /** + * Creates a new directory under this object. + * + * This is currently not allowed. + * + * @param string $filename + */ + public function createDirectory($filename) + { + throw new DAV\Exception\MethodNotAllowed('Creating new collections in this collection is not supported'); + } + + /** + * Returns a single addressbook, by name. + * + * @param string $name + * + * @todo needs optimizing + * + * @return AddressBook + */ + public function getChild($name) + { + foreach ($this->getChildren() as $child) { + if ($name == $child->getName()) { + return $child; + } + } + throw new DAV\Exception\NotFound('Addressbook with name \''.$name.'\' could not be found'); + } + + /** + * Returns a list of addressbooks. + * + * @return array + */ + public function getChildren() + { + $addressbooks = $this->carddavBackend->getAddressBooksForUser($this->principalUri); + $objs = []; + foreach ($addressbooks as $addressbook) { + $objs[] = new AddressBook($this->carddavBackend, $addressbook); + } + + return $objs; + } + + /** + * Creates a new address book. + * + * @param string $name + * + * @throws DAV\Exception\InvalidResourceType + */ + public function createExtendedCollection($name, MkCol $mkCol) + { + if (!$mkCol->hasResourceType('{'.Plugin::NS_CARDDAV.'}addressbook')) { + throw new DAV\Exception\InvalidResourceType('Unknown resourceType for this collection'); + } + $properties = $mkCol->getRemainingValues(); + $mkCol->setRemainingResultCode(201); + $this->carddavBackend->createAddressBook($this->principalUri, $name, $properties); + } + + /** + * Returns the owner principal. + * + * This must be a url to a principal, or null if there's no owner + * + * @return string|null + */ + public function getOwner() + { + return $this->principalUri; + } +} diff --git a/3rdparty/sabre/dav/lib/CardDAV/AddressBookRoot.php b/3rdparty/sabre/dav/lib/CardDAV/AddressBookRoot.php new file mode 100644 index 00000000..ee1721a4 --- /dev/null +++ b/3rdparty/sabre/dav/lib/CardDAV/AddressBookRoot.php @@ -0,0 +1,75 @@ +carddavBackend = $carddavBackend; + parent::__construct($principalBackend, $principalPrefix); + } + + /** + * Returns the name of the node. + * + * @return string + */ + public function getName() + { + return Plugin::ADDRESSBOOK_ROOT; + } + + /** + * This method returns a node for a principal. + * + * The passed array contains principal information, and is guaranteed to + * at least contain a uri item. Other properties may or may not be + * supplied by the authentication backend. + * + * @return \Sabre\DAV\INode + */ + public function getChildForPrincipal(array $principal) + { + return new AddressBookHome($this->carddavBackend, $principal['uri']); + } +} diff --git a/3rdparty/sabre/dav/lib/CardDAV/Backend/AbstractBackend.php b/3rdparty/sabre/dav/lib/CardDAV/Backend/AbstractBackend.php new file mode 100644 index 00000000..a900c625 --- /dev/null +++ b/3rdparty/sabre/dav/lib/CardDAV/Backend/AbstractBackend.php @@ -0,0 +1,38 @@ +getCard($addressBookId, $uri); + }, $uris); + } +} diff --git a/3rdparty/sabre/dav/lib/CardDAV/Backend/BackendInterface.php b/3rdparty/sabre/dav/lib/CardDAV/Backend/BackendInterface.php new file mode 100644 index 00000000..f9955ac8 --- /dev/null +++ b/3rdparty/sabre/dav/lib/CardDAV/Backend/BackendInterface.php @@ -0,0 +1,194 @@ +pdo = $pdo; + } + + /** + * Returns the list of addressbooks for a specific user. + * + * @param string $principalUri + * + * @return array + */ + public function getAddressBooksForUser($principalUri) + { + $stmt = $this->pdo->prepare('SELECT id, uri, displayname, principaluri, description, synctoken FROM '.$this->addressBooksTableName.' WHERE principaluri = ?'); + $stmt->execute([$principalUri]); + + $addressBooks = []; + + foreach ($stmt->fetchAll() as $row) { + $addressBooks[] = [ + 'id' => $row['id'], + 'uri' => $row['uri'], + 'principaluri' => $row['principaluri'], + '{DAV:}displayname' => $row['displayname'], + '{'.CardDAV\Plugin::NS_CARDDAV.'}addressbook-description' => $row['description'], + '{http://calendarserver.org/ns/}getctag' => $row['synctoken'], + '{http://sabredav.org/ns}sync-token' => $row['synctoken'] ? $row['synctoken'] : '0', + ]; + } + + return $addressBooks; + } + + /** + * Updates properties for an address book. + * + * The list of mutations is stored in a Sabre\DAV\PropPatch object. + * To do the actual updates, you must tell this object which properties + * you're going to process with the handle() method. + * + * Calling the handle method is like telling the PropPatch object "I + * promise I can handle updating this property". + * + * Read the PropPatch documentation for more info and examples. + * + * @param string $addressBookId + */ + public function updateAddressBook($addressBookId, PropPatch $propPatch) + { + $supportedProperties = [ + '{DAV:}displayname', + '{'.CardDAV\Plugin::NS_CARDDAV.'}addressbook-description', + ]; + + $propPatch->handle($supportedProperties, function ($mutations) use ($addressBookId) { + $updates = []; + foreach ($mutations as $property => $newValue) { + switch ($property) { + case '{DAV:}displayname': + $updates['displayname'] = $newValue; + break; + case '{'.CardDAV\Plugin::NS_CARDDAV.'}addressbook-description': + $updates['description'] = $newValue; + break; + } + } + $query = 'UPDATE '.$this->addressBooksTableName.' SET '; + $first = true; + foreach ($updates as $key => $value) { + if ($first) { + $first = false; + } else { + $query .= ', '; + } + $query .= ' '.$key.' = :'.$key.' '; + } + $query .= ' WHERE id = :addressbookid'; + + $stmt = $this->pdo->prepare($query); + $updates['addressbookid'] = $addressBookId; + + $stmt->execute($updates); + + $this->addChange($addressBookId, '', 2); + + return true; + }); + } + + /** + * Creates a new address book. + * + * @param string $principalUri + * @param string $url just the 'basename' of the url + * + * @return int Last insert id + */ + public function createAddressBook($principalUri, $url, array $properties) + { + $values = [ + 'displayname' => null, + 'description' => null, + 'principaluri' => $principalUri, + 'uri' => $url, + ]; + + foreach ($properties as $property => $newValue) { + switch ($property) { + case '{DAV:}displayname': + $values['displayname'] = $newValue; + break; + case '{'.CardDAV\Plugin::NS_CARDDAV.'}addressbook-description': + $values['description'] = $newValue; + break; + default: + throw new DAV\Exception\BadRequest('Unknown property: '.$property); + } + } + + $query = 'INSERT INTO '.$this->addressBooksTableName.' (uri, displayname, description, principaluri, synctoken) VALUES (:uri, :displayname, :description, :principaluri, 1)'; + $stmt = $this->pdo->prepare($query); + $stmt->execute($values); + + return $this->pdo->lastInsertId( + $this->addressBooksTableName.'_id_seq' + ); + } + + /** + * Deletes an entire addressbook and all its contents. + * + * @param int $addressBookId + */ + public function deleteAddressBook($addressBookId) + { + $stmt = $this->pdo->prepare('DELETE FROM '.$this->cardsTableName.' WHERE addressbookid = ?'); + $stmt->execute([$addressBookId]); + + $stmt = $this->pdo->prepare('DELETE FROM '.$this->addressBooksTableName.' WHERE id = ?'); + $stmt->execute([$addressBookId]); + + $stmt = $this->pdo->prepare('DELETE FROM '.$this->addressBookChangesTableName.' WHERE addressbookid = ?'); + $stmt->execute([$addressBookId]); + } + + /** + * Returns all cards for a specific addressbook id. + * + * This method should return the following properties for each card: + * * carddata - raw vcard data + * * uri - Some unique url + * * lastmodified - A unix timestamp + * + * It's recommended to also return the following properties: + * * etag - A unique etag. This must change every time the card changes. + * * size - The size of the card in bytes. + * + * If these last two properties are provided, less time will be spent + * calculating them. If they are specified, you can also omit carddata. + * This may speed up certain requests, especially with large cards. + * + * @param mixed $addressbookId + * + * @return array + */ + public function getCards($addressbookId) + { + $stmt = $this->pdo->prepare('SELECT id, uri, lastmodified, etag, size FROM '.$this->cardsTableName.' WHERE addressbookid = ?'); + $stmt->execute([$addressbookId]); + + $result = []; + while ($row = $stmt->fetch(\PDO::FETCH_ASSOC)) { + $row['etag'] = '"'.$row['etag'].'"'; + $row['lastmodified'] = (int) $row['lastmodified']; + $result[] = $row; + } + + return $result; + } + + /** + * Returns a specific card. + * + * The same set of properties must be returned as with getCards. The only + * exception is that 'carddata' is absolutely required. + * + * If the card does not exist, you must return false. + * + * @param mixed $addressBookId + * @param string $cardUri + * + * @return array + */ + public function getCard($addressBookId, $cardUri) + { + $stmt = $this->pdo->prepare('SELECT id, carddata, uri, lastmodified, etag, size FROM '.$this->cardsTableName.' WHERE addressbookid = ? AND uri = ? LIMIT 1'); + $stmt->execute([$addressBookId, $cardUri]); + + $result = $stmt->fetch(\PDO::FETCH_ASSOC); + + if (!$result) { + return false; + } + + $result['etag'] = '"'.$result['etag'].'"'; + $result['lastmodified'] = (int) $result['lastmodified']; + + return $result; + } + + /** + * Returns a list of cards. + * + * This method should work identical to getCard, but instead return all the + * cards in the list as an array. + * + * If the backend supports this, it may allow for some speed-ups. + * + * @param mixed $addressBookId + * + * @return array + */ + public function getMultipleCards($addressBookId, array $uris) + { + $query = 'SELECT id, uri, lastmodified, etag, size, carddata FROM '.$this->cardsTableName.' WHERE addressbookid = ? AND uri IN ('; + // Inserting a whole bunch of question marks + $query .= implode(',', array_fill(0, count($uris), '?')); + $query .= ')'; + + $stmt = $this->pdo->prepare($query); + $stmt->execute(array_merge([$addressBookId], $uris)); + $result = []; + while ($row = $stmt->fetch(\PDO::FETCH_ASSOC)) { + $row['etag'] = '"'.$row['etag'].'"'; + $row['lastmodified'] = (int) $row['lastmodified']; + $result[] = $row; + } + + return $result; + } + + /** + * Creates a new card. + * + * The addressbook id will be passed as the first argument. This is the + * same id as it is returned from the getAddressBooksForUser method. + * + * The cardUri is a base uri, and doesn't include the full path. The + * cardData argument is the vcard body, and is passed as a string. + * + * It is possible to return an ETag from this method. This ETag is for the + * newly created resource, and must be enclosed with double quotes (that + * is, the string itself must contain the double quotes). + * + * You should only return the ETag if you store the carddata as-is. If a + * subsequent GET request on the same card does not have the same body, + * byte-by-byte and you did return an ETag here, clients tend to get + * confused. + * + * If you don't return an ETag, you can just return null. + * + * @param mixed $addressBookId + * @param string $cardUri + * @param string $cardData + * + * @return string|null + */ + public function createCard($addressBookId, $cardUri, $cardData) + { + $stmt = $this->pdo->prepare('INSERT INTO '.$this->cardsTableName.' (carddata, uri, lastmodified, addressbookid, size, etag) VALUES (?, ?, ?, ?, ?, ?)'); + + $etag = md5($cardData); + + $stmt->execute([ + $cardData, + $cardUri, + time(), + $addressBookId, + strlen($cardData), + $etag, + ]); + + $this->addChange($addressBookId, $cardUri, 1); + + return '"'.$etag.'"'; + } + + /** + * Updates a card. + * + * The addressbook id will be passed as the first argument. This is the + * same id as it is returned from the getAddressBooksForUser method. + * + * The cardUri is a base uri, and doesn't include the full path. The + * cardData argument is the vcard body, and is passed as a string. + * + * It is possible to return an ETag from this method. This ETag should + * match that of the updated resource, and must be enclosed with double + * quotes (that is: the string itself must contain the actual quotes). + * + * You should only return the ETag if you store the carddata as-is. If a + * subsequent GET request on the same card does not have the same body, + * byte-by-byte and you did return an ETag here, clients tend to get + * confused. + * + * If you don't return an ETag, you can just return null. + * + * @param mixed $addressBookId + * @param string $cardUri + * @param string $cardData + * + * @return string|null + */ + public function updateCard($addressBookId, $cardUri, $cardData) + { + $stmt = $this->pdo->prepare('UPDATE '.$this->cardsTableName.' SET carddata = ?, lastmodified = ?, size = ?, etag = ? WHERE uri = ? AND addressbookid =?'); + + $etag = md5($cardData); + $stmt->execute([ + $cardData, + time(), + strlen($cardData), + $etag, + $cardUri, + $addressBookId, + ]); + + $this->addChange($addressBookId, $cardUri, 2); + + return '"'.$etag.'"'; + } + + /** + * Deletes a card. + * + * @param mixed $addressBookId + * @param string $cardUri + * + * @return bool + */ + public function deleteCard($addressBookId, $cardUri) + { + $stmt = $this->pdo->prepare('DELETE FROM '.$this->cardsTableName.' WHERE addressbookid = ? AND uri = ?'); + $stmt->execute([$addressBookId, $cardUri]); + + $this->addChange($addressBookId, $cardUri, 3); + + return 1 === $stmt->rowCount(); + } + + /** + * The getChanges method returns all the changes that have happened, since + * the specified syncToken in the specified address book. + * + * This function should return an array, such as the following: + * + * [ + * 'syncToken' => 'The current synctoken', + * 'added' => [ + * 'new.txt', + * ], + * 'modified' => [ + * 'updated.txt', + * ], + * 'deleted' => [ + * 'foo.php.bak', + * 'old.txt' + * ] + * ]; + * + * The returned syncToken property should reflect the *current* syncToken + * of the addressbook, as reported in the {http://sabredav.org/ns}sync-token + * property. This is needed here too, to ensure the operation is atomic. + * + * If the $syncToken argument is specified as null, this is an initial + * sync, and all members should be reported. + * + * The modified property is an array of nodenames that have changed since + * the last token. + * + * The deleted property is an array with nodenames, that have been deleted + * from collection. + * + * The $syncLevel argument is basically the 'depth' of the report. If it's + * 1, you only have to report changes that happened only directly in + * immediate descendants. If it's 2, it should also include changes from + * the nodes below the child collections. (grandchildren) + * + * The $limit argument allows a client to specify how many results should + * be returned at most. If the limit is not specified, it should be treated + * as infinite. + * + * If the limit (infinite or not) is higher than you're willing to return, + * you should throw a Sabre\DAV\Exception\TooMuchMatches() exception. + * + * If the syncToken is expired (due to data cleanup) or unknown, you must + * return null. + * + * The limit is 'suggestive'. You are free to ignore it. + * + * @param string $addressBookId + * @param string $syncToken + * @param int $syncLevel + * @param int $limit + * + * @return array|null + */ + public function getChangesForAddressBook($addressBookId, $syncToken, $syncLevel, $limit = null) + { + // Current synctoken + $stmt = $this->pdo->prepare('SELECT synctoken FROM '.$this->addressBooksTableName.' WHERE id = ?'); + $stmt->execute([$addressBookId]); + $currentToken = $stmt->fetchColumn(0); + + if (is_null($currentToken)) { + return null; + } + + $result = [ + 'syncToken' => $currentToken, + 'added' => [], + 'modified' => [], + 'deleted' => [], + ]; + + if ($syncToken) { + $query = 'SELECT uri, operation FROM '.$this->addressBookChangesTableName.' WHERE synctoken >= ? AND synctoken < ? AND addressbookid = ? ORDER BY synctoken'; + if ($limit > 0) { + $query .= ' LIMIT '.(int) $limit; + } + + // Fetching all changes + $stmt = $this->pdo->prepare($query); + $stmt->execute([$syncToken, $currentToken, $addressBookId]); + + $changes = []; + + // This loop ensures that any duplicates are overwritten, only the + // last change on a node is relevant. + while ($row = $stmt->fetch(\PDO::FETCH_ASSOC)) { + $changes[$row['uri']] = $row['operation']; + } + + foreach ($changes as $uri => $operation) { + switch ($operation) { + case 1: + $result['added'][] = $uri; + break; + case 2: + $result['modified'][] = $uri; + break; + case 3: + $result['deleted'][] = $uri; + break; + } + } + } else { + // No synctoken supplied, this is the initial sync. + $query = 'SELECT uri FROM '.$this->cardsTableName.' WHERE addressbookid = ?'; + $stmt = $this->pdo->prepare($query); + $stmt->execute([$addressBookId]); + + $result['added'] = $stmt->fetchAll(\PDO::FETCH_COLUMN); + } + + return $result; + } + + /** + * Adds a change record to the addressbookchanges table. + * + * @param mixed $addressBookId + * @param string $objectUri + * @param int $operation 1 = add, 2 = modify, 3 = delete + */ + protected function addChange($addressBookId, $objectUri, $operation) + { + $stmt = $this->pdo->prepare('INSERT INTO '.$this->addressBookChangesTableName.' (uri, synctoken, addressbookid, operation) SELECT ?, synctoken, ?, ? FROM '.$this->addressBooksTableName.' WHERE id = ?'); + $stmt->execute([ + $objectUri, + $addressBookId, + $operation, + $addressBookId, + ]); + $stmt = $this->pdo->prepare('UPDATE '.$this->addressBooksTableName.' SET synctoken = synctoken + 1 WHERE id = ?'); + $stmt->execute([ + $addressBookId, + ]); + } +} diff --git a/3rdparty/sabre/dav/lib/CardDAV/Backend/SyncSupport.php b/3rdparty/sabre/dav/lib/CardDAV/Backend/SyncSupport.php new file mode 100644 index 00000000..6aaad141 --- /dev/null +++ b/3rdparty/sabre/dav/lib/CardDAV/Backend/SyncSupport.php @@ -0,0 +1,83 @@ + 'The current synctoken', + * 'added' => [ + * 'new.txt', + * ], + * 'modified' => [ + * 'modified.txt', + * ], + * 'deleted' => [ + * 'foo.php.bak', + * 'old.txt' + * ] + * ]; + * + * The returned syncToken property should reflect the *current* syncToken + * of the calendar, as reported in the {http://sabredav.org/ns}sync-token + * property. This is needed here too, to ensure the operation is atomic. + * + * If the $syncToken argument is specified as null, this is an initial + * sync, and all members should be reported. + * + * The modified property is an array of nodenames that have changed since + * the last token. + * + * The deleted property is an array with nodenames, that have been deleted + * from collection. + * + * The $syncLevel argument is basically the 'depth' of the report. If it's + * 1, you only have to report changes that happened only directly in + * immediate descendants. If it's 2, it should also include changes from + * the nodes below the child collections. (grandchildren) + * + * The $limit argument allows a client to specify how many results should + * be returned at most. If the limit is not specified, it should be treated + * as infinite. + * + * If the limit (infinite or not) is higher than you're willing to return, + * you should throw a Sabre\DAV\Exception\TooMuchMatches() exception. + * + * If the syncToken is expired (due to data cleanup) or unknown, you must + * return null. + * + * The limit is 'suggestive'. You are free to ignore it. + * + * @param string $addressBookId + * @param string $syncToken + * @param int $syncLevel + * @param int $limit + * + * @return array|null + */ + public function getChangesForAddressBook($addressBookId, $syncToken, $syncLevel, $limit = null); +} diff --git a/3rdparty/sabre/dav/lib/CardDAV/Card.php b/3rdparty/sabre/dav/lib/CardDAV/Card.php new file mode 100644 index 00000000..c9cd2bbf --- /dev/null +++ b/3rdparty/sabre/dav/lib/CardDAV/Card.php @@ -0,0 +1,202 @@ +carddavBackend = $carddavBackend; + $this->addressBookInfo = $addressBookInfo; + $this->cardData = $cardData; + } + + /** + * Returns the uri for this object. + * + * @return string + */ + public function getName() + { + return $this->cardData['uri']; + } + + /** + * Returns the VCard-formatted object. + * + * @return string + */ + public function get() + { + // Pre-populating 'carddata' is optional. If we don't yet have it + // already, we fetch it from the backend. + if (!isset($this->cardData['carddata'])) { + $this->cardData = $this->carddavBackend->getCard($this->addressBookInfo['id'], $this->cardData['uri']); + } + + return $this->cardData['carddata']; + } + + /** + * Updates the VCard-formatted object. + * + * @param string $cardData + * + * @return string|null + */ + public function put($cardData) + { + if (is_resource($cardData)) { + $cardData = stream_get_contents($cardData); + } + + // Converting to UTF-8, if needed + $cardData = DAV\StringUtil::ensureUTF8($cardData); + + $etag = $this->carddavBackend->updateCard($this->addressBookInfo['id'], $this->cardData['uri'], $cardData); + $this->cardData['carddata'] = $cardData; + $this->cardData['etag'] = $etag; + + return $etag; + } + + /** + * Deletes the card. + */ + public function delete() + { + $this->carddavBackend->deleteCard($this->addressBookInfo['id'], $this->cardData['uri']); + } + + /** + * Returns the mime content-type. + * + * @return string + */ + public function getContentType() + { + return 'text/vcard; charset=utf-8'; + } + + /** + * Returns an ETag for this object. + * + * @return string + */ + public function getETag() + { + if (isset($this->cardData['etag'])) { + return $this->cardData['etag']; + } else { + $data = $this->get(); + if (is_string($data)) { + return '"'.md5($data).'"'; + } else { + // We refuse to calculate the md5 if it's a stream. + return null; + } + } + } + + /** + * Returns the last modification date as a unix timestamp. + * + * @return int + */ + public function getLastModified() + { + return isset($this->cardData['lastmodified']) ? $this->cardData['lastmodified'] : null; + } + + /** + * Returns the size of this object in bytes. + * + * @return int + */ + public function getSize() + { + if (array_key_exists('size', $this->cardData)) { + return $this->cardData['size']; + } else { + return strlen($this->get()); + } + } + + /** + * Returns the owner principal. + * + * This must be a url to a principal, or null if there's no owner + * + * @return string|null + */ + public function getOwner() + { + return $this->addressBookInfo['principaluri']; + } + + /** + * Returns a list of ACE's for this node. + * + * Each ACE has the following properties: + * * 'privilege', a string such as {DAV:}read or {DAV:}write. These are + * currently the only supported privileges + * * 'principal', a url to the principal who owns the node + * * 'protected' (optional), indicating that this ACE is not allowed to + * be updated. + * + * @return array + */ + public function getACL() + { + // An alternative acl may be specified through the cardData array. + if (isset($this->cardData['acl'])) { + return $this->cardData['acl']; + } + + return [ + [ + 'privilege' => '{DAV:}all', + 'principal' => $this->addressBookInfo['principaluri'], + 'protected' => true, + ], + ]; + } +} diff --git a/3rdparty/sabre/dav/lib/CardDAV/IAddressBook.php b/3rdparty/sabre/dav/lib/CardDAV/IAddressBook.php new file mode 100644 index 00000000..3f489f4e --- /dev/null +++ b/3rdparty/sabre/dav/lib/CardDAV/IAddressBook.php @@ -0,0 +1,20 @@ +on('propFind', [$this, 'propFindEarly']); + $server->on('propFind', [$this, 'propFindLate'], 150); + $server->on('report', [$this, 'report']); + $server->on('onHTMLActionsPanel', [$this, 'htmlActionsPanel']); + $server->on('beforeWriteContent', [$this, 'beforeWriteContent']); + $server->on('beforeCreateFile', [$this, 'beforeCreateFile']); + $server->on('afterMethod:GET', [$this, 'httpAfterGet']); + + $server->xml->namespaceMap[self::NS_CARDDAV] = 'card'; + + $server->xml->elementMap['{'.self::NS_CARDDAV.'}addressbook-query'] = 'Sabre\\CardDAV\\Xml\\Request\\AddressBookQueryReport'; + $server->xml->elementMap['{'.self::NS_CARDDAV.'}addressbook-multiget'] = 'Sabre\\CardDAV\\Xml\\Request\\AddressBookMultiGetReport'; + + /* Mapping Interfaces to {DAV:}resourcetype values */ + $server->resourceTypeMapping['Sabre\\CardDAV\\IAddressBook'] = '{'.self::NS_CARDDAV.'}addressbook'; + $server->resourceTypeMapping['Sabre\\CardDAV\\IDirectory'] = '{'.self::NS_CARDDAV.'}directory'; + + /* Adding properties that may never be changed */ + $server->protectedProperties[] = '{'.self::NS_CARDDAV.'}supported-address-data'; + $server->protectedProperties[] = '{'.self::NS_CARDDAV.'}max-resource-size'; + $server->protectedProperties[] = '{'.self::NS_CARDDAV.'}addressbook-home-set'; + $server->protectedProperties[] = '{'.self::NS_CARDDAV.'}supported-collation-set'; + + $server->xml->elementMap['{http://calendarserver.org/ns/}me-card'] = 'Sabre\\DAV\\Xml\\Property\\Href'; + + $this->server = $server; + } + + /** + * Returns a list of supported features. + * + * This is used in the DAV: header in the OPTIONS and PROPFIND requests. + * + * @return array + */ + public function getFeatures() + { + return ['addressbook']; + } + + /** + * Returns a list of reports this plugin supports. + * + * This will be used in the {DAV:}supported-report-set property. + * Note that you still need to subscribe to the 'report' event to actually + * implement them + * + * @param string $uri + * + * @return array + */ + public function getSupportedReportSet($uri) + { + $node = $this->server->tree->getNodeForPath($uri); + if ($node instanceof IAddressBook || $node instanceof ICard) { + return [ + '{'.self::NS_CARDDAV.'}addressbook-multiget', + '{'.self::NS_CARDDAV.'}addressbook-query', + ]; + } + + return []; + } + + /** + * Adds all CardDAV-specific properties. + */ + public function propFindEarly(DAV\PropFind $propFind, DAV\INode $node) + { + $ns = '{'.self::NS_CARDDAV.'}'; + + if ($node instanceof IAddressBook) { + $propFind->handle($ns.'max-resource-size', $this->maxResourceSize); + $propFind->handle($ns.'supported-address-data', function () { + return new Xml\Property\SupportedAddressData(); + }); + $propFind->handle($ns.'supported-collation-set', function () { + return new Xml\Property\SupportedCollationSet(); + }); + } + if ($node instanceof DAVACL\IPrincipal) { + $path = $propFind->getPath(); + + $propFind->handle('{'.self::NS_CARDDAV.'}addressbook-home-set', function () use ($path) { + return new LocalHref($this->getAddressBookHomeForPrincipal($path).'/'); + }); + + if ($this->directories) { + $propFind->handle('{'.self::NS_CARDDAV.'}directory-gateway', function () { + return new LocalHref($this->directories); + }); + } + } + + if ($node instanceof ICard) { + // The address-data property is not supposed to be a 'real' + // property, but in large chunks of the spec it does act as such. + // Therefore we simply expose it as a property. + $propFind->handle('{'.self::NS_CARDDAV.'}address-data', function () use ($node) { + $val = $node->get(); + if (is_resource($val)) { + $val = stream_get_contents($val); + } + + return $val; + }); + } + } + + /** + * This functions handles REPORT requests specific to CardDAV. + * + * @param string $reportName + * @param \DOMNode $dom + * @param mixed $path + * + * @return bool + */ + public function report($reportName, $dom, $path) + { + switch ($reportName) { + case '{'.self::NS_CARDDAV.'}addressbook-multiget': + $this->server->transactionType = 'report-addressbook-multiget'; + $this->addressbookMultiGetReport($dom); + + return false; + case '{'.self::NS_CARDDAV.'}addressbook-query': + $this->server->transactionType = 'report-addressbook-query'; + $this->addressBookQueryReport($dom); + + return false; + default: + return; + } + } + + /** + * Returns the addressbook home for a given principal. + * + * @param string $principal + * + * @return string + */ + protected function getAddressbookHomeForPrincipal($principal) + { + list(, $principalId) = Uri\split($principal); + + return self::ADDRESSBOOK_ROOT.'/'.$principalId; + } + + /** + * This function handles the addressbook-multiget REPORT. + * + * This report is used by the client to fetch the content of a series + * of urls. Effectively avoiding a lot of redundant requests. + * + * @param Xml\Request\AddressBookMultiGetReport $report + */ + public function addressbookMultiGetReport($report) + { + $contentType = $report->contentType; + $version = $report->version; + if ($version) { + $contentType .= '; version='.$version; + } + + $vcardType = $this->negotiateVCard( + $contentType + ); + + $propertyList = []; + $paths = array_map( + [$this->server, 'calculateUri'], + $report->hrefs + ); + foreach ($this->server->getPropertiesForMultiplePaths($paths, $report->properties) as $props) { + if (isset($props['200']['{'.self::NS_CARDDAV.'}address-data'])) { + $props['200']['{'.self::NS_CARDDAV.'}address-data'] = $this->convertVCard( + $props[200]['{'.self::NS_CARDDAV.'}address-data'], + $vcardType + ); + } + $propertyList[] = $props; + } + + $prefer = $this->server->getHTTPPrefer(); + + $this->server->httpResponse->setStatus(207); + $this->server->httpResponse->setHeader('Content-Type', 'application/xml; charset=utf-8'); + $this->server->httpResponse->setHeader('Vary', 'Brief,Prefer'); + $this->server->httpResponse->setBody($this->server->generateMultiStatus($propertyList, 'minimal' === $prefer['return'])); + } + + /** + * This method is triggered before a file gets updated with new content. + * + * This plugin uses this method to ensure that Card nodes receive valid + * vcard data. + * + * @param string $path + * @param resource $data + * @param bool $modified should be set to true, if this event handler + * changed &$data + */ + public function beforeWriteContent($path, DAV\IFile $node, &$data, &$modified) + { + if (!$node instanceof ICard) { + return; + } + + $this->validateVCard($data, $modified); + } + + /** + * This method is triggered before a new file is created. + * + * This plugin uses this method to ensure that Card nodes receive valid + * vcard data. + * + * @param string $path + * @param resource $data + * @param bool $modified should be set to true, if this event handler + * changed &$data + */ + public function beforeCreateFile($path, &$data, DAV\ICollection $parentNode, &$modified) + { + if (!$parentNode instanceof IAddressBook) { + return; + } + + $this->validateVCard($data, $modified); + } + + /** + * Checks if the submitted iCalendar data is in fact, valid. + * + * An exception is thrown if it's not. + * + * @param resource|string $data + * @param bool $modified should be set to true, if this event handler + * changed &$data + */ + protected function validateVCard(&$data, &$modified) + { + // If it's a stream, we convert it to a string first. + if (is_resource($data)) { + $data = stream_get_contents($data); + } + + $before = $data; + + try { + // If the data starts with a [, we can reasonably assume we're dealing + // with a jCal object. + if ('[' === substr($data, 0, 1)) { + $vobj = VObject\Reader::readJson($data); + + // Converting $data back to iCalendar, as that's what we + // technically support everywhere. + $data = $vobj->serialize(); + $modified = true; + } else { + $vobj = VObject\Reader::read($data); + } + } catch (VObject\ParseException $e) { + throw new DAV\Exception\UnsupportedMediaType('This resource only supports valid vCard or jCard data. Parse error: '.$e->getMessage()); + } + + if ('VCARD' !== $vobj->name) { + throw new DAV\Exception\UnsupportedMediaType('This collection can only support vcard objects.'); + } + + $options = VObject\Node::PROFILE_CARDDAV; + $prefer = $this->server->getHTTPPrefer(); + + if ('strict' !== $prefer['handling']) { + $options |= VObject\Node::REPAIR; + } + + $messages = $vobj->validate($options); + + $highestLevel = 0; + $warningMessage = null; + + // $messages contains a list of problems with the vcard, along with + // their severity. + foreach ($messages as $message) { + if ($message['level'] > $highestLevel) { + // Recording the highest reported error level. + $highestLevel = $message['level']; + $warningMessage = $message['message']; + } + + switch ($message['level']) { + case 1: + // Level 1 means that there was a problem, but it was repaired. + $modified = true; + break; + case 2: + // Level 2 means a warning, but not critical + break; + case 3: + // Level 3 means a critical error + throw new DAV\Exception\UnsupportedMediaType('Validation error in vCard: '.$message['message']); + } + } + if ($warningMessage) { + $this->server->httpResponse->setHeader( + 'X-Sabre-Ew-Gross', + 'vCard validation warning: '.$warningMessage + ); + + // Re-serializing object. + $data = $vobj->serialize(); + if (!$modified && 0 !== strcmp($data, $before)) { + // This ensures that the system does not send an ETag back. + $modified = true; + } + } + + // Destroy circular references to PHP will GC the object. + $vobj->destroy(); + } + + /** + * This function handles the addressbook-query REPORT. + * + * This report is used by the client to filter an addressbook based on a + * complex query. + * + * @param Xml\Request\AddressBookQueryReport $report + */ + protected function addressbookQueryReport($report) + { + $depth = $this->server->getHTTPDepth(0); + + if (0 == $depth) { + $candidateNodes = [ + $this->server->tree->getNodeForPath($this->server->getRequestUri()), + ]; + if (!$candidateNodes[0] instanceof ICard) { + throw new ReportNotSupported('The addressbook-query report is not supported on this url with Depth: 0'); + } + } else { + $candidateNodes = $this->server->tree->getChildren($this->server->getRequestUri()); + } + + $contentType = $report->contentType; + if ($report->version) { + $contentType .= '; version='.$report->version; + } + + $vcardType = $this->negotiateVCard( + $contentType + ); + + $validNodes = []; + foreach ($candidateNodes as $node) { + if (!$node instanceof ICard) { + continue; + } + + $blob = $node->get(); + if (is_resource($blob)) { + $blob = stream_get_contents($blob); + } + + if (!$this->validateFilters($blob, $report->filters, $report->test)) { + continue; + } + + $validNodes[] = $node; + + if ($report->limit && $report->limit <= count($validNodes)) { + // We hit the maximum number of items, we can stop now. + break; + } + } + + $result = []; + foreach ($validNodes as $validNode) { + if (0 == $depth) { + $href = $this->server->getRequestUri(); + } else { + $href = $this->server->getRequestUri().'/'.$validNode->getName(); + } + + list($props) = $this->server->getPropertiesForPath($href, $report->properties, 0); + + if (isset($props[200]['{'.self::NS_CARDDAV.'}address-data'])) { + $props[200]['{'.self::NS_CARDDAV.'}address-data'] = $this->convertVCard( + $props[200]['{'.self::NS_CARDDAV.'}address-data'], + $vcardType, + $report->addressDataProperties + ); + } + $result[] = $props; + } + + $prefer = $this->server->getHTTPPrefer(); + + $this->server->httpResponse->setStatus(207); + $this->server->httpResponse->setHeader('Content-Type', 'application/xml; charset=utf-8'); + $this->server->httpResponse->setHeader('Vary', 'Brief,Prefer'); + $this->server->httpResponse->setBody($this->server->generateMultiStatus($result, 'minimal' === $prefer['return'])); + } + + /** + * Validates if a vcard makes it throught a list of filters. + * + * @param string $vcardData + * @param string $test anyof or allof (which means OR or AND) + * + * @return bool + */ + public function validateFilters($vcardData, array $filters, $test) + { + if (!$filters) { + return true; + } + $vcard = VObject\Reader::read($vcardData); + + foreach ($filters as $filter) { + $isDefined = isset($vcard->{$filter['name']}); + if ($filter['is-not-defined']) { + if ($isDefined) { + $success = false; + } else { + $success = true; + } + } elseif ((!$filter['param-filters'] && !$filter['text-matches']) || !$isDefined) { + // We only need to check for existence + $success = $isDefined; + } else { + $vProperties = $vcard->select($filter['name']); + + $results = []; + if ($filter['param-filters']) { + $results[] = $this->validateParamFilters($vProperties, $filter['param-filters'], $filter['test']); + } + if ($filter['text-matches']) { + $texts = []; + foreach ($vProperties as $vProperty) { + $texts[] = $vProperty->getValue(); + } + + $results[] = $this->validateTextMatches($texts, $filter['text-matches'], $filter['test']); + } + + if (1 === count($results)) { + $success = $results[0]; + } else { + if ('anyof' === $filter['test']) { + $success = $results[0] || $results[1]; + } else { + $success = $results[0] && $results[1]; + } + } + } // else + + // There are two conditions where we can already determine whether + // or not this filter succeeds. + if ('anyof' === $test && $success) { + // Destroy circular references to PHP will GC the object. + $vcard->destroy(); + + return true; + } + if ('allof' === $test && !$success) { + // Destroy circular references to PHP will GC the object. + $vcard->destroy(); + + return false; + } + } // foreach + + // Destroy circular references to PHP will GC the object. + $vcard->destroy(); + + // If we got all the way here, it means we haven't been able to + // determine early if the test failed or not. + // + // This implies for 'anyof' that the test failed, and for 'allof' that + // we succeeded. Sounds weird, but makes sense. + return 'allof' === $test; + } + + /** + * Validates if a param-filter can be applied to a specific property. + * + * @todo currently we're only validating the first parameter of the passed + * property. Any subsequence parameters with the same name are + * ignored. + * + * @param string $test + * + * @return bool + */ + protected function validateParamFilters(array $vProperties, array $filters, $test) + { + foreach ($filters as $filter) { + $isDefined = false; + foreach ($vProperties as $vProperty) { + $isDefined = isset($vProperty[$filter['name']]); + if ($isDefined) { + break; + } + } + + if ($filter['is-not-defined']) { + if ($isDefined) { + $success = false; + } else { + $success = true; + } + + // If there's no text-match, we can just check for existence + } elseif (!$filter['text-match'] || !$isDefined) { + $success = $isDefined; + } else { + $success = false; + foreach ($vProperties as $vProperty) { + // If we got all the way here, we'll need to validate the + // text-match filter. + if (isset($vProperty[$filter['name']])) { + $success = DAV\StringUtil::textMatch( + $vProperty[$filter['name']]->getValue(), + $filter['text-match']['value'], + $filter['text-match']['collation'], + $filter['text-match']['match-type'] + ); + if ($filter['text-match']['negate-condition']) { + $success = !$success; + } + } + if ($success) { + break; + } + } + } // else + + // There are two conditions where we can already determine whether + // or not this filter succeeds. + if ('anyof' === $test && $success) { + return true; + } + if ('allof' === $test && !$success) { + return false; + } + } + + // If we got all the way here, it means we haven't been able to + // determine early if the test failed or not. + // + // This implies for 'anyof' that the test failed, and for 'allof' that + // we succeeded. Sounds weird, but makes sense. + return 'allof' === $test; + } + + /** + * Validates if a text-filter can be applied to a specific property. + * + * @param string $test + * + * @return bool + */ + protected function validateTextMatches(array $texts, array $filters, $test) + { + foreach ($filters as $filter) { + $success = false; + foreach ($texts as $haystack) { + $success = DAV\StringUtil::textMatch($haystack, $filter['value'], $filter['collation'], $filter['match-type']); + if ($filter['negate-condition']) { + $success = !$success; + } + + // Breaking on the first match + if ($success) { + break; + } + } + + if ($success && 'anyof' === $test) { + return true; + } + + if (!$success && 'allof' == $test) { + return false; + } + } + + // If we got all the way here, it means we haven't been able to + // determine early if the test failed or not. + // + // This implies for 'anyof' that the test failed, and for 'allof' that + // we succeeded. Sounds weird, but makes sense. + return 'allof' === $test; + } + + /** + * This event is triggered when fetching properties. + * + * This event is scheduled late in the process, after most work for + * propfind has been done. + */ + public function propFindLate(DAV\PropFind $propFind, DAV\INode $node) + { + // If the request was made using the SOGO connector, we must rewrite + // the content-type property. By default SabreDAV will send back + // text/x-vcard; charset=utf-8, but for SOGO we must strip that last + // part. + if (false === strpos((string) $this->server->httpRequest->getHeader('User-Agent'), 'Thunderbird')) { + return; + } + $contentType = $propFind->get('{DAV:}getcontenttype'); + if (null !== $contentType) { + list($part) = explode(';', $contentType); + if ('text/x-vcard' === $part || 'text/vcard' === $part) { + $propFind->set('{DAV:}getcontenttype', 'text/x-vcard'); + } + } + } + + /** + * This method is used to generate HTML output for the + * Sabre\DAV\Browser\Plugin. This allows us to generate an interface users + * can use to create new addressbooks. + * + * @param string $output + * + * @return bool + */ + public function htmlActionsPanel(DAV\INode $node, &$output) + { + if (!$node instanceof AddressBookHome) { + return; + } + + $output .= '
    +

    Create new address book

    + + +
    +
    + +
    + '; + + return false; + } + + /** + * This event is triggered after GET requests. + * + * This is used to transform data into jCal, if this was requested. + */ + public function httpAfterGet(RequestInterface $request, ResponseInterface $response) + { + $contentType = $response->getHeader('Content-Type'); + if (null === $contentType || false === strpos($contentType, 'text/vcard')) { + return; + } + + $target = $this->negotiateVCard($request->getHeader('Accept'), $mimeType); + + $newBody = $this->convertVCard( + $response->getBody(), + $target + ); + + $response->setBody($newBody); + $response->setHeader('Content-Type', $mimeType.'; charset=utf-8'); + $response->setHeader('Content-Length', strlen($newBody)); + } + + /** + * This helper function performs the content-type negotiation for vcards. + * + * It will return one of the following strings: + * 1. vcard3 + * 2. vcard4 + * 3. jcard + * + * It defaults to vcard3. + * + * @param string $input + * @param string $mimeType + * + * @return string + */ + protected function negotiateVCard($input, &$mimeType = null) + { + $result = HTTP\negotiateContentType( + $input, + [ + // Most often used mime-type. Version 3 + 'text/x-vcard', + // The correct standard mime-type. Defaults to version 3 as + // well. + 'text/vcard', + // vCard 4 + 'text/vcard; version=4.0', + // vCard 3 + 'text/vcard; version=3.0', + // jCard + 'application/vcard+json', + ] + ); + + $mimeType = $result; + switch ($result) { + default: + case 'text/x-vcard': + case 'text/vcard': + case 'text/vcard; version=3.0': + $mimeType = 'text/vcard'; + + return 'vcard3'; + case 'text/vcard; version=4.0': + return 'vcard4'; + case 'application/vcard+json': + return 'jcard'; + + // @codeCoverageIgnoreStart + } + // @codeCoverageIgnoreEnd + } + + /** + * Converts a vcard blob to a different version, or jcard. + * + * @param string|resource $data + * @param string $target + * @param array $propertiesFilter + * + * @return string + */ + protected function convertVCard($data, $target, ?array $propertiesFilter = null) + { + if (is_resource($data)) { + $data = stream_get_contents($data); + } + $input = VObject\Reader::read($data); + if (!empty($propertiesFilter)) { + $propertiesFilter = array_merge(['UID', 'VERSION', 'FN'], $propertiesFilter); + $keys = array_unique(array_map(function ($child) { + return $child->name; + }, $input->children())); + $keys = array_diff($keys, $propertiesFilter); + foreach ($keys as $key) { + unset($input->$key); + } + $data = $input->serialize(); + } + $output = null; + try { + switch ($target) { + default: + case 'vcard3': + if (VObject\Document::VCARD30 === $input->getDocumentType()) { + // Do nothing + return $data; + } + $output = $input->convert(VObject\Document::VCARD30); + + return $output->serialize(); + case 'vcard4': + if (VObject\Document::VCARD40 === $input->getDocumentType()) { + // Do nothing + return $data; + } + $output = $input->convert(VObject\Document::VCARD40); + + return $output->serialize(); + case 'jcard': + $output = $input->convert(VObject\Document::VCARD40); + + return json_encode($output); + } + } finally { + // Destroy circular references to PHP will GC the object. + $input->destroy(); + if (!is_null($output)) { + $output->destroy(); + } + } + } + + /** + * Returns a plugin name. + * + * Using this name other plugins will be able to access other plugins + * using DAV\Server::getPlugin + * + * @return string + */ + public function getPluginName() + { + return 'carddav'; + } + + /** + * Returns a bunch of meta-data about the plugin. + * + * Providing this information is optional, and is mainly displayed by the + * Browser plugin. + * + * The description key in the returned array may contain html and will not + * be sanitized. + * + * @return array + */ + public function getPluginInfo() + { + return [ + 'name' => $this->getPluginName(), + 'description' => 'Adds support for CardDAV (rfc6352)', + 'link' => 'http://sabre.io/dav/carddav/', + ]; + } +} diff --git a/3rdparty/sabre/dav/lib/CardDAV/VCFExportPlugin.php b/3rdparty/sabre/dav/lib/CardDAV/VCFExportPlugin.php new file mode 100644 index 00000000..431391e0 --- /dev/null +++ b/3rdparty/sabre/dav/lib/CardDAV/VCFExportPlugin.php @@ -0,0 +1,165 @@ +server = $server; + $this->server->on('method:GET', [$this, 'httpGet'], 90); + $server->on('browserButtonActions', function ($path, $node, &$actions) { + if ($node instanceof IAddressBook) { + $actions .= ''; + } + }); + } + + /** + * Intercepts GET requests on addressbook urls ending with ?export. + * + * @return bool + */ + public function httpGet(RequestInterface $request, ResponseInterface $response) + { + $queryParams = $request->getQueryParameters(); + if (!array_key_exists('export', $queryParams)) { + return; + } + + $path = $request->getPath(); + + $node = $this->server->tree->getNodeForPath($path); + + if (!($node instanceof IAddressBook)) { + return; + } + + $this->server->transactionType = 'get-addressbook-export'; + + // Checking ACL, if available. + if ($aclPlugin = $this->server->getPlugin('acl')) { + $aclPlugin->checkPrivileges($path, '{DAV:}read'); + } + + $nodes = $this->server->getPropertiesForPath($path, [ + '{'.Plugin::NS_CARDDAV.'}address-data', + ], 1); + + $format = 'text/directory'; + + $output = null; + $filenameExtension = null; + + switch ($format) { + case 'text/directory': + $output = $this->generateVCF($nodes); + $filenameExtension = '.vcf'; + break; + } + + $filename = preg_replace( + '/[^a-zA-Z0-9-_ ]/um', + '', + $node->getName() + ); + $filename .= '-'.date('Y-m-d').$filenameExtension; + + $response->setHeader('Content-Disposition', 'attachment; filename="'.$filename.'"'); + $response->setHeader('Content-Type', $format); + + $response->setStatus(200); + $response->setBody($output); + + // Returning false to break the event chain + return false; + } + + /** + * Merges all vcard objects, and builds one big vcf export. + * + * @return string + */ + public function generateVCF(array $nodes) + { + $output = ''; + + foreach ($nodes as $node) { + if (!isset($node[200]['{'.Plugin::NS_CARDDAV.'}address-data'])) { + continue; + } + $nodeData = $node[200]['{'.Plugin::NS_CARDDAV.'}address-data']; + + // Parsing this node so VObject can clean up the output. + $vcard = VObject\Reader::read($nodeData); + $output .= $vcard->serialize(); + + // Destroy circular references to PHP will GC the object. + $vcard->destroy(); + } + + return $output; + } + + /** + * Returns a plugin name. + * + * Using this name other plugins will be able to access other plugins + * using \Sabre\DAV\Server::getPlugin + * + * @return string + */ + public function getPluginName() + { + return 'vcf-export'; + } + + /** + * Returns a bunch of meta-data about the plugin. + * + * Providing this information is optional, and is mainly displayed by the + * Browser plugin. + * + * The description key in the returned array may contain html and will not + * be sanitized. + * + * @return array + */ + public function getPluginInfo() + { + return [ + 'name' => $this->getPluginName(), + 'description' => 'Adds the ability to export CardDAV addressbooks as a single vCard file.', + 'link' => 'http://sabre.io/dav/vcf-export-plugin/', + ]; + } +} diff --git a/3rdparty/sabre/dav/lib/CardDAV/Xml/Filter/AddressData.php b/3rdparty/sabre/dav/lib/CardDAV/Xml/Filter/AddressData.php new file mode 100644 index 00000000..b60fcebb --- /dev/null +++ b/3rdparty/sabre/dav/lib/CardDAV/Xml/Filter/AddressData.php @@ -0,0 +1,66 @@ +next(); + * + * $reader->parseInnerTree() will parse the entire sub-tree, and advance to + * the next element. + * + * @return mixed + */ + public static function xmlDeserialize(Reader $reader) + { + $result = [ + 'contentType' => $reader->getAttribute('content-type') ?: 'text/vcard', + 'version' => $reader->getAttribute('version') ?: '3.0', + ]; + + $elems = (array) $reader->parseInnerTree(); + $elems = array_filter($elems, function ($element) { + return '{urn:ietf:params:xml:ns:carddav}prop' === $element['name'] && + isset($element['attributes']['name']); + }); + $result['addressDataProperties'] = array_map(function ($element) { + return $element['attributes']['name']; + }, $elems); + + return $result; + } +} diff --git a/3rdparty/sabre/dav/lib/CardDAV/Xml/Filter/ParamFilter.php b/3rdparty/sabre/dav/lib/CardDAV/Xml/Filter/ParamFilter.php new file mode 100644 index 00000000..0a7ec065 --- /dev/null +++ b/3rdparty/sabre/dav/lib/CardDAV/Xml/Filter/ParamFilter.php @@ -0,0 +1,86 @@ +next(); + * + * $reader->parseInnerTree() will parse the entire sub-tree, and advance to + * the next element. + * + * @return mixed + */ + public static function xmlDeserialize(Reader $reader) + { + $result = [ + 'name' => null, + 'is-not-defined' => false, + 'text-match' => null, + ]; + + $att = $reader->parseAttributes(); + $result['name'] = $att['name']; + + $elems = $reader->parseInnerTree(); + + if (is_array($elems)) { + foreach ($elems as $elem) { + switch ($elem['name']) { + case '{'.Plugin::NS_CARDDAV.'}is-not-defined': + $result['is-not-defined'] = true; + break; + case '{'.Plugin::NS_CARDDAV.'}text-match': + $matchType = isset($elem['attributes']['match-type']) ? $elem['attributes']['match-type'] : 'contains'; + + if (!in_array($matchType, ['contains', 'equals', 'starts-with', 'ends-with'])) { + throw new BadRequest('Unknown match-type: '.$matchType); + } + $result['text-match'] = [ + 'negate-condition' => isset($elem['attributes']['negate-condition']) && 'yes' === $elem['attributes']['negate-condition'], + 'collation' => isset($elem['attributes']['collation']) ? $elem['attributes']['collation'] : 'i;unicode-casemap', + 'value' => $elem['value'], + 'match-type' => $matchType, + ]; + break; + } + } + } + + return $result; + } +} diff --git a/3rdparty/sabre/dav/lib/CardDAV/Xml/Filter/PropFilter.php b/3rdparty/sabre/dav/lib/CardDAV/Xml/Filter/PropFilter.php new file mode 100644 index 00000000..5dedac80 --- /dev/null +++ b/3rdparty/sabre/dav/lib/CardDAV/Xml/Filter/PropFilter.php @@ -0,0 +1,95 @@ +next(); + * + * $reader->parseInnerTree() will parse the entire sub-tree, and advance to + * the next element. + * + * @return mixed + */ + public static function xmlDeserialize(Reader $reader) + { + $result = [ + 'name' => null, + 'test' => 'anyof', + 'is-not-defined' => false, + 'param-filters' => [], + 'text-matches' => [], + ]; + + $att = $reader->parseAttributes(); + $result['name'] = $att['name']; + + if (isset($att['test']) && 'allof' === $att['test']) { + $result['test'] = 'allof'; + } + + $elems = $reader->parseInnerTree(); + + if (is_array($elems)) { + foreach ($elems as $elem) { + switch ($elem['name']) { + case '{'.Plugin::NS_CARDDAV.'}param-filter': + $result['param-filters'][] = $elem['value']; + break; + case '{'.Plugin::NS_CARDDAV.'}is-not-defined': + $result['is-not-defined'] = true; + break; + case '{'.Plugin::NS_CARDDAV.'}text-match': + $matchType = isset($elem['attributes']['match-type']) ? $elem['attributes']['match-type'] : 'contains'; + + if (!in_array($matchType, ['contains', 'equals', 'starts-with', 'ends-with'])) { + throw new BadRequest('Unknown match-type: '.$matchType); + } + $result['text-matches'][] = [ + 'negate-condition' => isset($elem['attributes']['negate-condition']) && 'yes' === $elem['attributes']['negate-condition'], + 'collation' => isset($elem['attributes']['collation']) ? $elem['attributes']['collation'] : 'i;unicode-casemap', + 'value' => $elem['value'], + 'match-type' => $matchType, + ]; + break; + } + } + } + + return $result; + } +} diff --git a/3rdparty/sabre/dav/lib/CardDAV/Xml/Property/SupportedAddressData.php b/3rdparty/sabre/dav/lib/CardDAV/Xml/Property/SupportedAddressData.php new file mode 100644 index 00000000..536c5a19 --- /dev/null +++ b/3rdparty/sabre/dav/lib/CardDAV/Xml/Property/SupportedAddressData.php @@ -0,0 +1,77 @@ + 'text/vcard', 'version' => '3.0'], + ['contentType' => 'text/vcard', 'version' => '4.0'], + ['contentType' => 'application/vcard+json', 'version' => '4.0'], + ]; + } + + $this->supportedData = $supportedData; + } + + /** + * The xmlSerialize method is called during xml writing. + * + * Use the $writer argument to write its own xml serialization. + * + * An important note: do _not_ create a parent element. Any element + * implementing XmlSerializable should only ever write what's considered + * its 'inner xml'. + * + * The parent of the current element is responsible for writing a + * containing element. + * + * This allows serializers to be re-used for different element names. + * + * If you are opening new elements, you must also close them again. + */ + public function xmlSerialize(Writer $writer) + { + foreach ($this->supportedData as $supported) { + $writer->startElement('{'.Plugin::NS_CARDDAV.'}address-data-type'); + $writer->writeAttributes([ + 'content-type' => $supported['contentType'], + 'version' => $supported['version'], + ]); + $writer->endElement(); // address-data-type + } + } +} diff --git a/3rdparty/sabre/dav/lib/CardDAV/Xml/Property/SupportedCollationSet.php b/3rdparty/sabre/dav/lib/CardDAV/Xml/Property/SupportedCollationSet.php new file mode 100644 index 00000000..b19eddd9 --- /dev/null +++ b/3rdparty/sabre/dav/lib/CardDAV/Xml/Property/SupportedCollationSet.php @@ -0,0 +1,44 @@ +writeElement('{urn:ietf:params:xml:ns:carddav}supported-collation', $coll); + } + } +} diff --git a/3rdparty/sabre/dav/lib/CardDAV/Xml/Request/AddressBookMultiGetReport.php b/3rdparty/sabre/dav/lib/CardDAV/Xml/Request/AddressBookMultiGetReport.php new file mode 100644 index 00000000..491f9690 --- /dev/null +++ b/3rdparty/sabre/dav/lib/CardDAV/Xml/Request/AddressBookMultiGetReport.php @@ -0,0 +1,116 @@ +next(); + * + * $reader->parseInnerTree() will parse the entire sub-tree, and advance to + * the next element. + * + * @return mixed + */ + public static function xmlDeserialize(Reader $reader) + { + $elems = $reader->parseInnerTree([ + '{urn:ietf:params:xml:ns:carddav}address-data' => 'Sabre\\CardDAV\\Xml\\Filter\\AddressData', + '{DAV:}prop' => 'Sabre\\Xml\\Element\\KeyValue', + ]); + + $newProps = [ + 'hrefs' => [], + 'properties' => [], + ]; + + foreach ($elems as $elem) { + switch ($elem['name']) { + case '{DAV:}prop': + $newProps['properties'] = array_keys($elem['value']); + if (isset($elem['value']['{'.Plugin::NS_CARDDAV.'}address-data'])) { + $newProps += $elem['value']['{'.Plugin::NS_CARDDAV.'}address-data']; + } + break; + case '{DAV:}href': + $newProps['hrefs'][] = Uri\resolve($reader->contextUri, $elem['value']); + break; + } + } + + $obj = new self(); + foreach ($newProps as $key => $value) { + $obj->$key = $value; + } + + return $obj; + } +} diff --git a/3rdparty/sabre/dav/lib/CardDAV/Xml/Request/AddressBookQueryReport.php b/3rdparty/sabre/dav/lib/CardDAV/Xml/Request/AddressBookQueryReport.php new file mode 100644 index 00000000..02402f6c --- /dev/null +++ b/3rdparty/sabre/dav/lib/CardDAV/Xml/Request/AddressBookQueryReport.php @@ -0,0 +1,193 @@ +next(); + * + * $reader->parseInnerTree() will parse the entire sub-tree, and advance to + * the next element. + * + * @return mixed + */ + public static function xmlDeserialize(Reader $reader) + { + $elems = (array) $reader->parseInnerTree([ + '{urn:ietf:params:xml:ns:carddav}prop-filter' => 'Sabre\\CardDAV\\Xml\\Filter\\PropFilter', + '{urn:ietf:params:xml:ns:carddav}param-filter' => 'Sabre\\CardDAV\\Xml\\Filter\\ParamFilter', + '{urn:ietf:params:xml:ns:carddav}address-data' => 'Sabre\\CardDAV\\Xml\\Filter\\AddressData', + '{DAV:}prop' => 'Sabre\\Xml\\Element\\KeyValue', + ]); + + $newProps = [ + 'filters' => null, + 'properties' => [], + 'test' => 'anyof', + 'limit' => null, + ]; + + if (!is_array($elems)) { + $elems = []; + } + + foreach ($elems as $elem) { + switch ($elem['name']) { + case '{DAV:}prop': + $newProps['properties'] = array_keys($elem['value']); + if (isset($elem['value']['{'.Plugin::NS_CARDDAV.'}address-data'])) { + $newProps += $elem['value']['{'.Plugin::NS_CARDDAV.'}address-data']; + } + break; + case '{'.Plugin::NS_CARDDAV.'}filter': + if (!is_null($newProps['filters'])) { + throw new BadRequest('You can only include 1 {'.Plugin::NS_CARDDAV.'}filter element'); + } + if (isset($elem['attributes']['test'])) { + $newProps['test'] = $elem['attributes']['test']; + if ('allof' !== $newProps['test'] && 'anyof' !== $newProps['test']) { + throw new BadRequest('The "test" attribute must be one of "allof" or "anyof"'); + } + } + + $newProps['filters'] = []; + foreach ((array) $elem['value'] as $subElem) { + if ($subElem['name'] === '{'.Plugin::NS_CARDDAV.'}prop-filter') { + $newProps['filters'][] = $subElem['value']; + } + } + break; + case '{'.Plugin::NS_CARDDAV.'}limit': + foreach ($elem['value'] as $child) { + if ($child['name'] === '{'.Plugin::NS_CARDDAV.'}nresults') { + $newProps['limit'] = (int) $child['value']; + } + } + break; + } + } + + if (is_null($newProps['filters'])) { + /* + * We are supposed to throw this error, but KDE sometimes does not + * include the filter element, and we need to treat it as if no + * filters are supplied + */ + //throw new BadRequest('The {' . Plugin::NS_CARDDAV . '}filter element is required for this request'); + $newProps['filters'] = []; + } + + $obj = new self(); + foreach ($newProps as $key => $value) { + $obj->$key = $value; + } + + return $obj; + } +} diff --git a/3rdparty/sabre/dav/lib/DAV/Auth/Backend/AbstractBasic.php b/3rdparty/sabre/dav/lib/DAV/Auth/Backend/AbstractBasic.php new file mode 100644 index 00000000..3132333b --- /dev/null +++ b/3rdparty/sabre/dav/lib/DAV/Auth/Backend/AbstractBasic.php @@ -0,0 +1,136 @@ +realm = $realm; + } + + /** + * When this method is called, the backend must check if authentication was + * successful. + * + * The returned value must be one of the following + * + * [true, "principals/username"] + * [false, "reason for failure"] + * + * If authentication was successful, it's expected that the authentication + * backend returns a so-called principal url. + * + * Examples of a principal url: + * + * principals/admin + * principals/user1 + * principals/users/joe + * principals/uid/123457 + * + * If you don't use WebDAV ACL (RFC3744) we recommend that you simply + * return a string such as: + * + * principals/users/[username] + * + * @return array + */ + public function check(RequestInterface $request, ResponseInterface $response) + { + $auth = new HTTP\Auth\Basic( + $this->realm, + $request, + $response + ); + + $userpass = $auth->getCredentials(); + if (!$userpass) { + return [false, "No 'Authorization: Basic' header found. Either the client didn't send one, or the server is misconfigured"]; + } + if (!$this->validateUserPass($userpass[0], $userpass[1])) { + return [false, 'Username or password was incorrect']; + } + + return [true, $this->principalPrefix.$userpass[0]]; + } + + /** + * This method is called when a user could not be authenticated, and + * authentication was required for the current request. + * + * This gives you the opportunity to set authentication headers. The 401 + * status code will already be set. + * + * In this case of Basic Auth, this would for example mean that the + * following header needs to be set: + * + * $response->addHeader('WWW-Authenticate', 'Basic realm=SabreDAV'); + * + * Keep in mind that in the case of multiple authentication backends, other + * WWW-Authenticate headers may already have been set, and you'll want to + * append your own WWW-Authenticate header instead of overwriting the + * existing one. + */ + public function challenge(RequestInterface $request, ResponseInterface $response) + { + $auth = new HTTP\Auth\Basic( + $this->realm, + $request, + $response + ); + $auth->requireLogin(); + } +} diff --git a/3rdparty/sabre/dav/lib/DAV/Auth/Backend/AbstractBearer.php b/3rdparty/sabre/dav/lib/DAV/Auth/Backend/AbstractBearer.php new file mode 100644 index 00000000..b6817479 --- /dev/null +++ b/3rdparty/sabre/dav/lib/DAV/Auth/Backend/AbstractBearer.php @@ -0,0 +1,130 @@ +realm = $realm; + } + + /** + * When this method is called, the backend must check if authentication was + * successful. + * + * The returned value must be one of the following + * + * [true, "principals/username"] + * [false, "reason for failure"] + * + * If authentication was successful, it's expected that the authentication + * backend returns a so-called principal url. + * + * Examples of a principal url: + * + * principals/admin + * principals/user1 + * principals/users/joe + * principals/uid/123457 + * + * If you don't use WebDAV ACL (RFC3744) we recommend that you simply + * return a string such as: + * + * principals/users/[username] + * + * @return array + */ + public function check(RequestInterface $request, ResponseInterface $response) + { + $auth = new HTTP\Auth\Bearer( + $this->realm, + $request, + $response + ); + + $bearerToken = $auth->getToken($request); + if (!$bearerToken) { + return [false, "No 'Authorization: Bearer' header found. Either the client didn't send one, or the server is mis-configured"]; + } + $principalUrl = $this->validateBearerToken($bearerToken); + if (!$principalUrl) { + return [false, 'Bearer token was incorrect']; + } + + return [true, $principalUrl]; + } + + /** + * This method is called when a user could not be authenticated, and + * authentication was required for the current request. + * + * This gives you the opportunity to set authentication headers. The 401 + * status code will already be set. + * + * In this case of Bearer Auth, this would for example mean that the + * following header needs to be set: + * + * $response->addHeader('WWW-Authenticate', 'Bearer realm=SabreDAV'); + * + * Keep in mind that in the case of multiple authentication backends, other + * WWW-Authenticate headers may already have been set, and you'll want to + * append your own WWW-Authenticate header instead of overwriting the + * existing one. + */ + public function challenge(RequestInterface $request, ResponseInterface $response) + { + $auth = new HTTP\Auth\Bearer( + $this->realm, + $request, + $response + ); + $auth->requireLogin(); + } +} diff --git a/3rdparty/sabre/dav/lib/DAV/Auth/Backend/AbstractDigest.php b/3rdparty/sabre/dav/lib/DAV/Auth/Backend/AbstractDigest.php new file mode 100644 index 00000000..297655da --- /dev/null +++ b/3rdparty/sabre/dav/lib/DAV/Auth/Backend/AbstractDigest.php @@ -0,0 +1,160 @@ +realm = $realm; + } + + /** + * Returns a users digest hash based on the username and realm. + * + * If the user was not known, null must be returned. + * + * @param string $realm + * @param string $username + * + * @return string|null + */ + abstract public function getDigestHash($realm, $username); + + /** + * When this method is called, the backend must check if authentication was + * successful. + * + * The returned value must be one of the following + * + * [true, "principals/username"] + * [false, "reason for failure"] + * + * If authentication was successful, it's expected that the authentication + * backend returns a so-called principal url. + * + * Examples of a principal url: + * + * principals/admin + * principals/user1 + * principals/users/joe + * principals/uid/123457 + * + * If you don't use WebDAV ACL (RFC3744) we recommend that you simply + * return a string such as: + * + * principals/users/[username] + * + * @return array + */ + public function check(RequestInterface $request, ResponseInterface $response) + { + $digest = new HTTP\Auth\Digest( + $this->realm, + $request, + $response + ); + $digest->init(); + + $username = $digest->getUsername(); + + // No username was given + if (!$username) { + return [false, "No 'Authorization: Digest' header found. Either the client didn't send one, or the server is misconfigured"]; + } + + $hash = $this->getDigestHash($this->realm, $username); + // If this was false, the user account didn't exist + if (false === $hash || is_null($hash)) { + return [false, 'Username or password was incorrect']; + } + if (!is_string($hash)) { + throw new DAV\Exception('The returned value from getDigestHash must be a string or null'); + } + + // If this was false, the password or part of the hash was incorrect. + if (!$digest->validateA1($hash)) { + return [false, 'Username or password was incorrect']; + } + + return [true, $this->principalPrefix.$username]; + } + + /** + * This method is called when a user could not be authenticated, and + * authentication was required for the current request. + * + * This gives you the opportunity to set authentication headers. The 401 + * status code will already be set. + * + * In this case of Basic Auth, this would for example mean that the + * following header needs to be set: + * + * $response->addHeader('WWW-Authenticate', 'Basic realm=SabreDAV'); + * + * Keep in mind that in the case of multiple authentication backends, other + * WWW-Authenticate headers may already have been set, and you'll want to + * append your own WWW-Authenticate header instead of overwriting the + * existing one. + */ + public function challenge(RequestInterface $request, ResponseInterface $response) + { + $auth = new HTTP\Auth\Digest( + $this->realm, + $request, + $response + ); + $auth->init(); + + $oldStatus = $response->getStatus() ?: 200; + $auth->requireLogin(); + + // Preventing the digest utility from modifying the http status code, + // this should be handled by the main plugin. + $response->setStatus($oldStatus); + } +} diff --git a/3rdparty/sabre/dav/lib/DAV/Auth/Backend/Apache.php b/3rdparty/sabre/dav/lib/DAV/Auth/Backend/Apache.php new file mode 100644 index 00000000..ebf67cab --- /dev/null +++ b/3rdparty/sabre/dav/lib/DAV/Auth/Backend/Apache.php @@ -0,0 +1,93 @@ +getRawServerValue('REMOTE_USER'); + if (is_null($remoteUser)) { + $remoteUser = $request->getRawServerValue('REDIRECT_REMOTE_USER'); + } + if (is_null($remoteUser)) { + $remoteUser = $request->getRawServerValue('PHP_AUTH_USER'); + } + if (is_null($remoteUser)) { + return [false, 'No REMOTE_USER, REDIRECT_REMOTE_USER, or PHP_AUTH_USER property was found in the PHP $_SERVER super-global. This likely means your server is not configured correctly']; + } + + return [true, $this->principalPrefix.$remoteUser]; + } + + /** + * This method is called when a user could not be authenticated, and + * authentication was required for the current request. + * + * This gives you the opportunity to set authentication headers. The 401 + * status code will already be set. + * + * In this case of Basic Auth, this would for example mean that the + * following header needs to be set: + * + * $response->addHeader('WWW-Authenticate', 'Basic realm=SabreDAV'); + * + * Keep in mind that in the case of multiple authentication backends, other + * WWW-Authenticate headers may already have been set, and you'll want to + * append your own WWW-Authenticate header instead of overwriting the + * existing one. + */ + public function challenge(RequestInterface $request, ResponseInterface $response) + { + } +} diff --git a/3rdparty/sabre/dav/lib/DAV/Auth/Backend/BackendInterface.php b/3rdparty/sabre/dav/lib/DAV/Auth/Backend/BackendInterface.php new file mode 100644 index 00000000..133eac92 --- /dev/null +++ b/3rdparty/sabre/dav/lib/DAV/Auth/Backend/BackendInterface.php @@ -0,0 +1,65 @@ +addHeader('WWW-Authenticate', 'Basic realm=SabreDAV'); + * + * Keep in mind that in the case of multiple authentication backends, other + * WWW-Authenticate headers may already have been set, and you'll want to + * append your own WWW-Authenticate header instead of overwriting the + * existing one. + */ + public function challenge(RequestInterface $request, ResponseInterface $response); +} diff --git a/3rdparty/sabre/dav/lib/DAV/Auth/Backend/BasicCallBack.php b/3rdparty/sabre/dav/lib/DAV/Auth/Backend/BasicCallBack.php new file mode 100644 index 00000000..5a8bb98c --- /dev/null +++ b/3rdparty/sabre/dav/lib/DAV/Auth/Backend/BasicCallBack.php @@ -0,0 +1,56 @@ +callBack = $callBack; + } + + /** + * Validates a username and password. + * + * This method should return true or false depending on if login + * succeeded. + * + * @param string $username + * @param string $password + * + * @return bool + */ + protected function validateUserPass($username, $password) + { + $cb = $this->callBack; + + return $cb($username, $password); + } +} diff --git a/3rdparty/sabre/dav/lib/DAV/Auth/Backend/File.php b/3rdparty/sabre/dav/lib/DAV/Auth/Backend/File.php new file mode 100644 index 00000000..ea2d3967 --- /dev/null +++ b/3rdparty/sabre/dav/lib/DAV/Auth/Backend/File.php @@ -0,0 +1,74 @@ +loadFile($filename); + } + } + + /** + * Loads an htdigest-formatted file. This method can be called multiple times if + * more than 1 file is used. + * + * @param string $filename + */ + public function loadFile($filename) + { + foreach (file($filename, FILE_IGNORE_NEW_LINES) as $line) { + if (2 !== substr_count($line, ':')) { + throw new DAV\Exception('Malformed htdigest file. Every line should contain 2 colons'); + } + list($username, $realm, $A1) = explode(':', $line); + + if (!preg_match('/^[a-zA-Z0-9]{32}$/', $A1)) { + throw new DAV\Exception('Malformed htdigest file. Invalid md5 hash'); + } + $this->users[$realm.':'.$username] = $A1; + } + } + + /** + * Returns a users' information. + * + * @param string $realm + * @param string $username + * + * @return string + */ + public function getDigestHash($realm, $username) + { + return isset($this->users[$realm.':'.$username]) ? $this->users[$realm.':'.$username] : false; + } +} diff --git a/3rdparty/sabre/dav/lib/DAV/Auth/Backend/IMAP.php b/3rdparty/sabre/dav/lib/DAV/Auth/Backend/IMAP.php new file mode 100644 index 00000000..3a183111 --- /dev/null +++ b/3rdparty/sabre/dav/lib/DAV/Auth/Backend/IMAP.php @@ -0,0 +1,82 @@ +mailbox = $mailbox; + } + + /** + * Connects to an IMAP server and tries to authenticate. + * + * @param string $username + * @param string $password + * + * @return bool + */ + protected function imapOpen($username, $password) + { + $success = false; + + try { + $imap = imap_open($this->mailbox, $username, $password, OP_HALFOPEN | OP_READONLY, 1); + if ($imap) { + $success = true; + } + } catch (\ErrorException $e) { + error_log($e->getMessage()); + } + + $errors = imap_errors(); + if ($errors) { + foreach ($errors as $error) { + error_log($error); + } + } + + if (isset($imap) && $imap) { + imap_close($imap); + } + + return $success; + } + + /** + * Validates a username and password by trying to authenticate against IMAP. + * + * @param string $username + * @param string $password + * + * @return bool + */ + protected function validateUserPass($username, $password) + { + return $this->imapOpen($username, $password); + } +} diff --git a/3rdparty/sabre/dav/lib/DAV/Auth/Backend/PDO.php b/3rdparty/sabre/dav/lib/DAV/Auth/Backend/PDO.php new file mode 100644 index 00000000..9a06912d --- /dev/null +++ b/3rdparty/sabre/dav/lib/DAV/Auth/Backend/PDO.php @@ -0,0 +1,55 @@ +pdo = $pdo; + } + + /** + * Returns the digest hash for a user. + * + * @param string $realm + * @param string $username + * + * @return string|null + */ + public function getDigestHash($realm, $username) + { + $stmt = $this->pdo->prepare('SELECT digesta1 FROM '.$this->tableName.' WHERE username = ?'); + $stmt->execute([$username]); + + return $stmt->fetchColumn() ?: null; + } +} diff --git a/3rdparty/sabre/dav/lib/DAV/Auth/Backend/PDOBasicAuth.php b/3rdparty/sabre/dav/lib/DAV/Auth/Backend/PDOBasicAuth.php new file mode 100644 index 00000000..d142cbfb --- /dev/null +++ b/3rdparty/sabre/dav/lib/DAV/Auth/Backend/PDOBasicAuth.php @@ -0,0 +1,114 @@ +pdo = $pdo; + if (isset($options['tableName'])) { + $this->tableName = $options['tableName']; + } else { + $this->tableName = 'users'; + } + if (isset($options['digestColumn'])) { + $this->digestColumn = $options['digestColumn']; + } else { + $this->digestColumn = 'digest'; + } + if (isset($options['uuidColumn'])) { + $this->uuidColumn = $options['uuidColumn']; + } else { + $this->uuidColumn = 'username'; + } + if (isset($options['digestPrefix'])) { + $this->digestPrefix = $options['digestPrefix']; + } + } + + /** + * Validates a username and password. + * + * This method should return true or false depending on if login + * succeeded. + * + * @param string $username + * @param string $password + * + * @return bool + */ + public function validateUserPass($username, $password) + { + $stmt = $this->pdo->prepare('SELECT '.$this->digestColumn.' FROM '.$this->tableName.' WHERE '.$this->uuidColumn.' = ?'); + $stmt->execute([$username]); + $result = $stmt->fetchAll(); + + if (!count($result)) { + return false; + } else { + $digest = $result[0][$this->digestColumn]; + + if (isset($this->digestPrefix)) { + $digest = substr($digest, strlen($this->digestPrefix)); + } + + if (password_verify($password, $digest)) { + return true; + } + + return false; + } + } +} diff --git a/3rdparty/sabre/dav/lib/DAV/Auth/Plugin.php b/3rdparty/sabre/dav/lib/DAV/Auth/Plugin.php new file mode 100644 index 00000000..47fbe205 --- /dev/null +++ b/3rdparty/sabre/dav/lib/DAV/Auth/Plugin.php @@ -0,0 +1,255 @@ +addBackend($authBackend); + } + } + + /** + * Adds an authentication backend to the plugin. + */ + public function addBackend(Backend\BackendInterface $authBackend) + { + $this->backends[] = $authBackend; + } + + /** + * Initializes the plugin. This function is automatically called by the server. + */ + public function initialize(Server $server) + { + $server->on('beforeMethod:*', [$this, 'beforeMethod'], 10); + } + + /** + * Returns a plugin name. + * + * Using this name other plugins will be able to access other plugins + * using DAV\Server::getPlugin + * + * @return string + */ + public function getPluginName() + { + return 'auth'; + } + + /** + * Returns the currently logged-in principal. + * + * This will return a string such as: + * + * principals/username + * principals/users/username + * + * This method will return null if nobody is logged in. + * + * @return string|null + */ + public function getCurrentPrincipal() + { + return $this->currentPrincipal; + } + + /** + * This method is called before any HTTP method and forces users to be authenticated. + */ + public function beforeMethod(RequestInterface $request, ResponseInterface $response) + { + if ($this->currentPrincipal) { + // We already have authentication information. This means that the + // event has already fired earlier, and is now likely fired for a + // sub-request. + // + // We don't want to authenticate users twice, so we simply don't do + // anything here. See Issue #700 for additional reasoning. + // + // This is not a perfect solution, but will be fixed once the + // "currently authenticated principal" is information that's not + // not associated with the plugin, but rather per-request. + // + // See issue #580 for more information about that. + return; + } + + $authResult = $this->check($request, $response); + + if ($authResult[0]) { + // Auth was successful + $this->currentPrincipal = $authResult[1]; + $this->loginFailedReasons = null; + + return; + } + + // If we got here, it means that no authentication backend was + // successful in authenticating the user. + $this->currentPrincipal = null; + $this->loginFailedReasons = $authResult[1]; + + if ($this->autoRequireLogin) { + $this->challenge($request, $response); + throw new NotAuthenticated(implode(', ', $authResult[1])); + } + } + + /** + * Checks authentication credentials, and logs the user in if possible. + * + * This method returns an array. The first item in the array is a boolean + * indicating if login was successful. + * + * If login was successful, the second item in the array will contain the + * current principal url/path of the logged in user. + * + * If login was not successful, the second item in the array will contain a + * an array with strings. The strings are a list of reasons why login was + * unsuccessful. For every auth backend there will be one reason, so usually + * there's just one. + * + * @return array + */ + public function check(RequestInterface $request, ResponseInterface $response) + { + if (!$this->backends) { + throw new \Sabre\DAV\Exception('No authentication backends were configured on this server.'); + } + $reasons = []; + foreach ($this->backends as $backend) { + $result = $backend->check( + $request, + $response + ); + + if (!is_array($result) || 2 !== count($result) || !is_bool($result[0]) || !is_string($result[1])) { + throw new \Sabre\DAV\Exception('The authentication backend did not return a correct value from the check() method.'); + } + + if ($result[0]) { + $this->currentPrincipal = $result[1]; + // Exit early + return [true, $result[1]]; + } + $reasons[] = $result[1]; + } + + return [false, $reasons]; + } + + /** + * This method sends authentication challenges to the user. + * + * This method will for example cause a HTTP Basic backend to set a + * WWW-Authorization header, indicating to the client that it should + * authenticate. + */ + public function challenge(RequestInterface $request, ResponseInterface $response) + { + foreach ($this->backends as $backend) { + $backend->challenge($request, $response); + } + } + + /** + * List of reasons why login failed for the last login operation. + * + * @var string[]|null + */ + protected $loginFailedReasons; + + /** + * Returns a list of reasons why login was unsuccessful. + * + * This method will return the login failed reasons for the last login + * operation. One for each auth backend. + * + * This method returns null if the last authentication attempt was + * successful, or if there was no authentication attempt yet. + * + * @return string[]|null + */ + public function getLoginFailedReasons() + { + return $this->loginFailedReasons; + } + + /** + * Returns a bunch of meta-data about the plugin. + * + * Providing this information is optional, and is mainly displayed by the + * Browser plugin. + * + * The description key in the returned array may contain html and will not + * be sanitized. + * + * @return array + */ + public function getPluginInfo() + { + return [ + 'name' => $this->getPluginName(), + 'description' => 'Generic authentication plugin', + 'link' => 'http://sabre.io/dav/authentication/', + ]; + } +} diff --git a/3rdparty/sabre/dav/lib/DAV/Browser/GuessContentType.php b/3rdparty/sabre/dav/lib/DAV/Browser/GuessContentType.php new file mode 100644 index 00000000..5cda0b84 --- /dev/null +++ b/3rdparty/sabre/dav/lib/DAV/Browser/GuessContentType.php @@ -0,0 +1,93 @@ + 'image/jpeg', + 'gif' => 'image/gif', + 'png' => 'image/png', + + // groupware + 'ics' => 'text/calendar', + 'vcf' => 'text/vcard', + + // text + 'txt' => 'text/plain', + ]; + + /** + * Initializes the plugin. + */ + public function initialize(DAV\Server $server) + { + // Using a relatively low priority (200) to allow other extensions + // to set the content-type first. + $server->on('propFind', [$this, 'propFind'], 200); + } + + /** + * Our PROPFIND handler. + * + * Here we set a contenttype, if the node didn't already have one. + */ + public function propFind(PropFind $propFind, INode $node) + { + $propFind->handle('{DAV:}getcontenttype', function () use ($propFind) { + list(, $fileName) = Uri\split($propFind->getPath()); + + return $this->getContentType($fileName); + }); + } + + /** + * Simple method to return the contenttype. + * + * @param string $fileName + * + * @return string + */ + protected function getContentType($fileName) + { + if (null !== $fileName) { + // Just grabbing the extension + $extension = strtolower(substr($fileName, strrpos($fileName, '.') + 1)); + if (isset($this->extensionMap[$extension])) { + return $this->extensionMap[$extension]; + } + } + + return 'application/octet-stream'; + } +} diff --git a/3rdparty/sabre/dav/lib/DAV/Browser/HtmlOutput.php b/3rdparty/sabre/dav/lib/DAV/Browser/HtmlOutput.php new file mode 100644 index 00000000..be5a2845 --- /dev/null +++ b/3rdparty/sabre/dav/lib/DAV/Browser/HtmlOutput.php @@ -0,0 +1,34 @@ +baseUri = $baseUri; + $this->namespaceMap = $namespaceMap; + } + + /** + * Generates a 'full' url based on a relative one. + * + * For relative urls, the base of the application is taken as the reference + * url, not the 'current url of the current request'. + * + * Absolute urls are left alone. + * + * @param string $path + * + * @return string + */ + public function fullUrl($path) + { + return Uri\resolve($this->baseUri, $path); + } + + /** + * Escape string for HTML output. + * + * @param scalar $input + * + * @return string + */ + public function h($input) + { + return htmlspecialchars((string) $input, ENT_COMPAT, 'UTF-8'); + } + + /** + * Generates a full -tag. + * + * Url is automatically expanded. If label is not specified, we re-use the + * url. + * + * @param string $url + * @param string $label + * + * @return string + */ + public function link($url, $label = null) + { + $url = $this->h($this->fullUrl($url)); + + return ''.($label ? $this->h($label) : $url).''; + } + + /** + * This method takes an xml element in clark-notation, and turns it into a + * shortened version with a prefix, if it was a known namespace. + * + * @param string $element + * + * @return string + */ + public function xmlName($element) + { + list($ns, $localName) = XmlService::parseClarkNotation($element); + if (isset($this->namespaceMap[$ns])) { + $propName = $this->namespaceMap[$ns].':'.$localName; + } else { + $propName = $element; + } + + return ''.$this->h($propName).''; + } +} diff --git a/3rdparty/sabre/dav/lib/DAV/Browser/MapGetToPropFind.php b/3rdparty/sabre/dav/lib/DAV/Browser/MapGetToPropFind.php new file mode 100644 index 00000000..0bbe70c6 --- /dev/null +++ b/3rdparty/sabre/dav/lib/DAV/Browser/MapGetToPropFind.php @@ -0,0 +1,58 @@ +server = $server; + $this->server->on('method:GET', [$this, 'httpGet'], 90); + } + + /** + * This method intercepts GET requests to non-files, and changes it into an HTTP PROPFIND request. + * + * @return bool + */ + public function httpGet(RequestInterface $request, ResponseInterface $response) + { + $node = $this->server->tree->getNodeForPath($request->getPath()); + if ($node instanceof DAV\IFile) { + return; + } + + $subRequest = clone $request; + $subRequest->setMethod('PROPFIND'); + + $this->server->invokeMethod($subRequest, $response); + + return false; + } +} diff --git a/3rdparty/sabre/dav/lib/DAV/Browser/Plugin.php b/3rdparty/sabre/dav/lib/DAV/Browser/Plugin.php new file mode 100644 index 00000000..a8a6f430 --- /dev/null +++ b/3rdparty/sabre/dav/lib/DAV/Browser/Plugin.php @@ -0,0 +1,789 @@ +enablePost = $enablePost; + } + + /** + * Initializes the plugin and subscribes to events. + */ + public function initialize(DAV\Server $server) + { + $this->server = $server; + $this->server->on('method:GET', [$this, 'httpGetEarly'], 90); + $this->server->on('method:GET', [$this, 'httpGet'], 200); + $this->server->on('onHTMLActionsPanel', [$this, 'htmlActionsPanel'], 200); + if ($this->enablePost) { + $this->server->on('method:POST', [$this, 'httpPOST']); + } + } + + /** + * This method intercepts GET requests that have ?sabreAction=info + * appended to the URL. + */ + public function httpGetEarly(RequestInterface $request, ResponseInterface $response) + { + $params = $request->getQueryParameters(); + if (isset($params['sabreAction']) && 'info' === $params['sabreAction']) { + return $this->httpGet($request, $response); + } + } + + /** + * This method intercepts GET requests to collections and returns the html. + * + * @return bool + */ + public function httpGet(RequestInterface $request, ResponseInterface $response) + { + // We're not using straight-up $_GET, because we want everything to be + // unit testable. + $getVars = $request->getQueryParameters(); + + // CSP headers + $response->setHeader('Content-Security-Policy', "default-src 'none'; img-src 'self'; style-src 'self'; font-src 'self';"); + + $sabreAction = isset($getVars['sabreAction']) ? $getVars['sabreAction'] : null; + + switch ($sabreAction) { + case 'asset': + // Asset handling, such as images + $this->serveAsset(isset($getVars['assetName']) ? $getVars['assetName'] : null); + + return false; + default: + case 'info': + try { + $this->server->tree->getNodeForPath($request->getPath()); + } catch (DAV\Exception\NotFound $e) { + // We're simply stopping when the file isn't found to not interfere + // with other plugins. + return; + } + + $response->setStatus(200); + $response->setHeader('Content-Type', 'text/html; charset=utf-8'); + + $response->setBody( + $this->generateDirectoryIndex($request->getPath()) + ); + + return false; + + case 'plugins': + $response->setStatus(200); + $response->setHeader('Content-Type', 'text/html; charset=utf-8'); + + $response->setBody( + $this->generatePluginListing() + ); + + return false; + } + } + + /** + * Handles POST requests for tree operations. + * + * @return bool + */ + public function httpPOST(RequestInterface $request, ResponseInterface $response) + { + $contentType = $request->getHeader('Content-Type'); + if (!\is_string($contentType)) { + return; + } + list($contentType) = explode(';', $contentType); + if ('application/x-www-form-urlencoded' !== $contentType && + 'multipart/form-data' !== $contentType) { + return; + } + $postVars = $request->getPostData(); + + if (!isset($postVars['sabreAction'])) { + return; + } + + $uri = $request->getPath(); + + if ($this->server->emit('onBrowserPostAction', [$uri, $postVars['sabreAction'], $postVars])) { + switch ($postVars['sabreAction']) { + case 'mkcol': + if (isset($postVars['name']) && trim($postVars['name'])) { + // Using basename() because we won't allow slashes + list(, $folderName) = Uri\split(trim($postVars['name'])); + + if (isset($postVars['resourceType'])) { + $resourceType = explode(',', $postVars['resourceType']); + } else { + $resourceType = ['{DAV:}collection']; + } + + $properties = []; + foreach ($postVars as $varName => $varValue) { + // Any _POST variable in clark notation is treated + // like a property. + if ('{' === $varName[0]) { + // PHP will convert any dots to underscores. + // This leaves us with no way to differentiate + // the two. + // Therefore we replace the string *DOT* with a + // real dot. * is not allowed in uris so we + // should be good. + $varName = str_replace('*DOT*', '.', $varName); + $properties[$varName] = $varValue; + } + } + + $mkCol = new MkCol( + $resourceType, + $properties + ); + $this->server->createCollection($uri.'/'.$folderName, $mkCol); + } + break; + + // @codeCoverageIgnoreStart + case 'put': + if ($_FILES) { + $file = current($_FILES); + } else { + break; + } + + list(, $newName) = Uri\split(trim($file['name'])); + if (isset($postVars['name']) && trim($postVars['name'])) { + $newName = trim($postVars['name']); + } + + // Making sure we only have a 'basename' component + list(, $newName) = Uri\split($newName); + + if (is_uploaded_file($file['tmp_name'])) { + $this->server->createFile($uri.'/'.$newName, fopen($file['tmp_name'], 'r')); + } + break; + // @codeCoverageIgnoreEnd + } + } + $response->setHeader('Location', $request->getUrl()); + $response->setStatus(302); + + return false; + } + + /** + * Escapes a string for html. + * + * @param string $value + * + * @return string + */ + public function escapeHTML($value) + { + return htmlspecialchars($value, ENT_QUOTES, 'UTF-8'); + } + + /** + * Generates the html directory index for a given url. + * + * @param string $path + * + * @return string + */ + public function generateDirectoryIndex($path) + { + $html = $this->generateHeader($path ? $path : '/', $path); + + $node = $this->server->tree->getNodeForPath($path); + if ($node instanceof DAV\ICollection) { + $html .= "

    Nodes

    \n"; + $html .= ''; + + $subNodes = $this->server->getPropertiesForChildren($path, [ + '{DAV:}displayname', + '{DAV:}resourcetype', + '{DAV:}getcontenttype', + '{DAV:}getcontentlength', + '{DAV:}getlastmodified', + ]); + + foreach ($subNodes as $subPath => $subProps) { + $subNode = $this->server->tree->getNodeForPath($subPath); + $fullPath = $this->server->getBaseUri().HTTP\encodePath($subPath); + list(, $displayPath) = Uri\split($subPath); + + $subNodes[$subPath]['subNode'] = $subNode; + $subNodes[$subPath]['fullPath'] = $fullPath; + $subNodes[$subPath]['displayPath'] = $displayPath; + } + uasort($subNodes, [$this, 'compareNodes']); + + foreach ($subNodes as $subProps) { + $type = [ + 'string' => 'Unknown', + 'icon' => 'cog', + ]; + if (isset($subProps['{DAV:}resourcetype'])) { + $type = $this->mapResourceType($subProps['{DAV:}resourcetype']->getValue(), $subProps['subNode']); + } + + $html .= ''; + $html .= ''; + $html .= ''; + $html .= ''; + + $buttonActions = ''; + if ($subProps['subNode'] instanceof DAV\IFile) { + $buttonActions = ''; + } + $this->server->emit('browserButtonActions', [$subProps['fullPath'], $subProps['subNode'], &$buttonActions]); + + $html .= ''; + $html .= ''; + } + + $html .= '
    '.$this->escapeHTML($subProps['displayPath']).''.$this->escapeHTML($type['string']).''; + if (isset($subProps['{DAV:}getcontentlength'])) { + $html .= $this->escapeHTML($subProps['{DAV:}getcontentlength'].' bytes'); + } + $html .= ''; + if (isset($subProps['{DAV:}getlastmodified'])) { + $lastMod = $subProps['{DAV:}getlastmodified']->getTime(); + $html .= $this->escapeHTML($lastMod->format('F j, Y, g:i a')); + } + $html .= ''; + if (isset($subProps['{DAV:}displayname'])) { + $html .= $this->escapeHTML($subProps['{DAV:}displayname']); + } + $html .= ''.$buttonActions.'
    '; + } + + $html .= '
    '; + $html .= '

    Properties

    '; + $html .= ''; + + // Allprops request + $propFind = new PropFindAll($path); + $properties = $this->server->getPropertiesByNode($propFind, $node); + + $properties = $propFind->getResultForMultiStatus()[200]; + + foreach ($properties as $propName => $propValue) { + if (!in_array($propName, $this->uninterestingProperties)) { + $html .= $this->drawPropertyRow($propName, $propValue); + } + } + + $html .= '
    '; + $html .= '
    '; + + /* Start of generating actions */ + + $output = ''; + if ($this->enablePost) { + $this->server->emit('onHTMLActionsPanel', [$node, &$output, $path]); + } + + if ($output) { + $html .= '

    Actions

    '; + $html .= "
    \n"; + $html .= $output; + $html .= "
    \n"; + $html .= "
    \n"; + } + + $html .= $this->generateFooter(); + + $this->server->httpResponse->setHeader('Content-Security-Policy', "default-src 'none'; img-src 'self'; style-src 'self'; font-src 'self';"); + + return $html; + } + + /** + * Generates the 'plugins' page. + * + * @return string + */ + public function generatePluginListing() + { + $html = $this->generateHeader('Plugins'); + + $html .= '

    Plugins

    '; + $html .= ''; + foreach ($this->server->getPlugins() as $plugin) { + $info = $plugin->getPluginInfo(); + $html .= ''; + $html .= ''; + $html .= ''; + } + $html .= '
    '.$info['name'].''.$info['description'].''; + if (isset($info['link']) && $info['link']) { + $html .= ''; + } + $html .= '
    '; + $html .= '
    '; + + /* Start of generating actions */ + + $html .= $this->generateFooter(); + + return $html; + } + + /** + * Generates the first block of HTML, including the tag and page + * header. + * + * Returns footer. + * + * @param string $title + * @param string $path + * + * @return string + */ + public function generateHeader($title, $path = null) + { + $version = ''; + if (DAV\Server::$exposeVersion) { + $version = DAV\Version::VERSION; + } + + $vars = [ + 'title' => $this->escapeHTML($title), + 'favicon' => $this->escapeHTML($this->getAssetUrl('favicon.ico')), + 'style' => $this->escapeHTML($this->getAssetUrl('sabredav.css')), + 'iconstyle' => $this->escapeHTML($this->getAssetUrl('openiconic/open-iconic.css')), + 'logo' => $this->escapeHTML($this->getAssetUrl('sabredav.png')), + 'baseUrl' => $this->server->getBaseUri(), + ]; + + $html = << + + + $vars[title] - sabre/dav $version + + + + + + +
    + +
    + + '; + + return $html; + } + + /** + * Generates the page footer. + * + * Returns html. + * + * @return string + */ + public function generateFooter() + { + $version = ''; + if (DAV\Server::$exposeVersion) { + $version = DAV\Version::VERSION; + } + $year = date('Y'); + + return <<Generated by SabreDAV $version (c)2007-$year http://sabre.io/ + + +HTML; + } + + /** + * This method is used to generate the 'actions panel' output for + * collections. + * + * This specifically generates the interfaces for creating new files, and + * creating new directories. + * + * @param mixed $output + * @param string $path + */ + public function htmlActionsPanel(DAV\INode $node, &$output, $path) + { + if (!$node instanceof DAV\ICollection) { + return; + } + + // We also know fairly certain that if an object is a non-extended + // SimpleCollection, we won't need to show the panel either. + if ('Sabre\\DAV\\SimpleCollection' === get_class($node)) { + return; + } + + $output .= << +

    Create new folder

    + +
    + + +
    +

    Upload file

    + +
    +
    + +
    +HTML; + } + + /** + * This method takes a path/name of an asset and turns it into url + * suitable for http access. + * + * @param string $assetName + * + * @return string + */ + protected function getAssetUrl($assetName) + { + return $this->server->getBaseUri().'?sabreAction=asset&assetName='.urlencode($assetName); + } + + /** + * This method returns a local pathname to an asset. + * + * @param string $assetName + * + * @throws DAV\Exception\NotFound + * + * @return string + */ + protected function getLocalAssetPath($assetName) + { + $assetDir = __DIR__.'/assets/'; + $path = $assetDir.$assetName; + + // Making sure people aren't trying to escape from the base path. + $path = str_replace('\\', '/', $path); + if (false !== strpos($path, '/../') || '/..' === strrchr($path, '/')) { + throw new DAV\Exception\NotFound('Path does not exist, or escaping from the base path was detected'); + } + $realPath = realpath($path); + if ($realPath && 0 === strpos($realPath, realpath($assetDir)) && file_exists($path)) { + return $path; + } + throw new DAV\Exception\NotFound('Path does not exist, or escaping from the base path was detected'); + } + + /** + * This method reads an asset from disk and generates a full http response. + * + * @param string $assetName + */ + protected function serveAsset($assetName) + { + $assetPath = $this->getLocalAssetPath($assetName); + + // Rudimentary mime type detection + $mime = 'application/octet-stream'; + $map = [ + 'ico' => 'image/vnd.microsoft.icon', + 'png' => 'image/png', + 'css' => 'text/css', + ]; + + $ext = substr($assetName, strrpos($assetName, '.') + 1); + if (isset($map[$ext])) { + $mime = $map[$ext]; + } + + $this->server->httpResponse->setHeader('Content-Type', $mime); + $this->server->httpResponse->setHeader('Content-Length', filesize($assetPath)); + $this->server->httpResponse->setHeader('Cache-Control', 'public, max-age=1209600'); + $this->server->httpResponse->setStatus(200); + $this->server->httpResponse->setBody(fopen($assetPath, 'r')); + } + + /** + * Sort helper function: compares two directory entries based on type and + * display name. Collections sort above other types. + * + * @param array $a + * @param array $b + * + * @return int + */ + protected function compareNodes($a, $b) + { + $typeA = (isset($a['{DAV:}resourcetype'])) + ? (in_array('{DAV:}collection', $a['{DAV:}resourcetype']->getValue())) + : false; + + $typeB = (isset($b['{DAV:}resourcetype'])) + ? (in_array('{DAV:}collection', $b['{DAV:}resourcetype']->getValue())) + : false; + + // If same type, sort alphabetically by filename: + if ($typeA === $typeB) { + return strnatcasecmp($a['displayPath'], $b['displayPath']); + } + + return ($typeA < $typeB) ? 1 : -1; + } + + /** + * Maps a resource type to a human-readable string and icon. + * + * @param DAV\INode $node + * + * @return array + */ + private function mapResourceType(array $resourceTypes, $node) + { + if (!$resourceTypes) { + if ($node instanceof DAV\IFile) { + return [ + 'string' => 'File', + 'icon' => 'file', + ]; + } else { + return [ + 'string' => 'Unknown', + 'icon' => 'cog', + ]; + } + } + + $types = [ + '{http://calendarserver.org/ns/}calendar-proxy-write' => [ + 'string' => 'Proxy-Write', + 'icon' => 'people', + ], + '{http://calendarserver.org/ns/}calendar-proxy-read' => [ + 'string' => 'Proxy-Read', + 'icon' => 'people', + ], + '{urn:ietf:params:xml:ns:caldav}schedule-outbox' => [ + 'string' => 'Outbox', + 'icon' => 'inbox', + ], + '{urn:ietf:params:xml:ns:caldav}schedule-inbox' => [ + 'string' => 'Inbox', + 'icon' => 'inbox', + ], + '{urn:ietf:params:xml:ns:caldav}calendar' => [ + 'string' => 'Calendar', + 'icon' => 'calendar', + ], + '{http://calendarserver.org/ns/}shared-owner' => [ + 'string' => 'Shared', + 'icon' => 'calendar', + ], + '{http://calendarserver.org/ns/}subscribed' => [ + 'string' => 'Subscription', + 'icon' => 'calendar', + ], + '{urn:ietf:params:xml:ns:carddav}directory' => [ + 'string' => 'Directory', + 'icon' => 'globe', + ], + '{urn:ietf:params:xml:ns:carddav}addressbook' => [ + 'string' => 'Address book', + 'icon' => 'book', + ], + '{DAV:}principal' => [ + 'string' => 'Principal', + 'icon' => 'person', + ], + '{DAV:}collection' => [ + 'string' => 'Collection', + 'icon' => 'folder', + ], + ]; + + $info = [ + 'string' => [], + 'icon' => 'cog', + ]; + foreach ($resourceTypes as $k => $resourceType) { + if (isset($types[$resourceType])) { + $info['string'][] = $types[$resourceType]['string']; + } else { + $info['string'][] = $resourceType; + } + } + foreach ($types as $key => $resourceInfo) { + if (in_array($key, $resourceTypes)) { + $info['icon'] = $resourceInfo['icon']; + break; + } + } + $info['string'] = implode(', ', $info['string']); + + return $info; + } + + /** + * Draws a table row for a property. + * + * @param string $name + * @param mixed $value + * + * @return string + */ + private function drawPropertyRow($name, $value) + { + $html = new HtmlOutputHelper( + $this->server->getBaseUri(), + $this->server->xml->namespaceMap + ); + + return ''.$html->xmlName($name).''.$this->drawPropertyValue($html, $value).''; + } + + /** + * Draws a table row for a property. + * + * @param HtmlOutputHelper $html + * @param mixed $value + * + * @return string + */ + private function drawPropertyValue($html, $value) + { + if (is_scalar($value)) { + return $html->h($value); + } elseif ($value instanceof HtmlOutput) { + return $value->toHtml($html); + } elseif ($value instanceof \Sabre\Xml\XmlSerializable) { + // There's no default html output for this property, we're going + // to output the actual xml serialization instead. + $xml = $this->server->xml->write('{DAV:}root', $value, $this->server->getBaseUri()); + // removing first and last line, as they contain our root + // element. + $xml = explode("\n", $xml); + $xml = array_slice($xml, 2, -2); + + return '
    '.$html->h(implode("\n", $xml)).'
    '; + } else { + return 'unknown'; + } + } + + /** + * Returns a plugin name. + * + * Using this name other plugins will be able to access other plugins; + * using \Sabre\DAV\Server::getPlugin + * + * @return string + */ + public function getPluginName() + { + return 'browser'; + } + + /** + * Returns a bunch of meta-data about the plugin. + * + * Providing this information is optional, and is mainly displayed by the + * Browser plugin. + * + * The description key in the returned array may contain html and will not + * be sanitized. + * + * @return array + */ + public function getPluginInfo() + { + return [ + 'name' => $this->getPluginName(), + 'description' => 'Generates HTML indexes and debug information for your sabre/dav server', + 'link' => 'http://sabre.io/dav/browser-plugin/', + ]; + } +} diff --git a/3rdparty/sabre/dav/lib/DAV/Browser/PropFindAll.php b/3rdparty/sabre/dav/lib/DAV/Browser/PropFindAll.php new file mode 100644 index 00000000..34702bdd --- /dev/null +++ b/3rdparty/sabre/dav/lib/DAV/Browser/PropFindAll.php @@ -0,0 +1,128 @@ +handle('{DAV:}displayname', function() { + * return 'hello'; + * }); + * + * Note that handle will only work the first time. If null is returned, the + * value is ignored. + * + * It's also possible to not pass a callback, but immediately pass a value + * + * @param string $propertyName + * @param mixed $valueOrCallBack + */ + public function handle($propertyName, $valueOrCallBack) + { + if (is_callable($valueOrCallBack)) { + $value = $valueOrCallBack(); + } else { + $value = $valueOrCallBack; + } + if (!is_null($value)) { + $this->result[$propertyName] = [200, $value]; + } + } + + /** + * Sets the value of the property. + * + * If status is not supplied, the status will default to 200 for non-null + * properties, and 404 for null properties. + * + * @param string $propertyName + * @param mixed $value + * @param int $status + */ + public function set($propertyName, $value, $status = null) + { + if (is_null($status)) { + $status = is_null($value) ? 404 : 200; + } + $this->result[$propertyName] = [$status, $value]; + } + + /** + * Returns the current value for a property. + * + * @param string $propertyName + * + * @return mixed + */ + public function get($propertyName) + { + return isset($this->result[$propertyName]) ? $this->result[$propertyName][1] : null; + } + + /** + * Returns the current status code for a property name. + * + * If the property does not appear in the list of requested properties, + * null will be returned. + * + * @param string $propertyName + * + * @return int|null + */ + public function getStatus($propertyName) + { + return isset($this->result[$propertyName]) ? $this->result[$propertyName][0] : 404; + } + + /** + * Returns all propertynames that have a 404 status, and thus don't have a + * value yet. + * + * @return array + */ + public function get404Properties() + { + $result = []; + foreach ($this->result as $propertyName => $stuff) { + if (404 === $stuff[0]) { + $result[] = $propertyName; + } + } + // If there's nothing in this list, we're adding one fictional item. + if (!$result) { + $result[] = '{http://sabredav.org/ns}idk'; + } + + return $result; + } +} diff --git a/3rdparty/sabre/dav/lib/DAV/Browser/assets/favicon.ico b/3rdparty/sabre/dav/lib/DAV/Browser/assets/favicon.ico new file mode 100644 index 0000000000000000000000000000000000000000..2b2c10a22cc7a57c4dc5d7156f184448f2bee92b GIT binary patch literal 4286 zcmc&&O-NKx6uz%14NNBzA}E}JHil3^6a|4&P_&6!RGSD}1rgCAC@`9dTC_@vqNoT8 z0%;dQR0K_{iUM;bf#3*%o1h^C2N7@IH_nmc?Va~t5U6~f`_BE&`Of`&_n~tUev3uN zziw!)bL*XR-2hy!51_yCgT8fb3s`VC=e=KcI5&)PGGQlpSAh?}1mK&Pg8c>z0Y`y$ zAT_6qJ%yV?|0!S$5WO@z3+`QD17OyXL4PyiM}RavtA7Tu7p)pn^p7Ks@m6m7)A}X$ z4Y+@;NrHYq_;V@RoZ|;69MPx!46Ftg*Tc~711C+J`JMuUfYwNBzXPB9sZm3WK9272 z&x|>@f_EO{b3cubqjOyc~J3I$d_lHIpN}q z!{kjX{c{12XF=~Z$w$kazXHB!b53>u!rx}_$e&dD`xNgv+MR&p2yN1xb0>&9t@28Z zV&5u#j_D=P9mI#){2s8@eGGj(?>gooo<%RT14>`VSZ&_l6GlGnan=^bemD56rRN{? zSAqZD$i;oS9SF6#f5I`#^C&hW@13s_lc3LUl(PWmHcop2{vr^kO`kP(*4!m=3Hn3e#Oc!a2;iDn+FbXzcOHEQ zbXZ)u93cj1WA=KS+M>jZ=oYyXq}1?ZdsjsX0A zkJXCvi~cfO@2ffd7r^;>=SsL-3U%l5HRoEZ#0r%`7%&% ziLTXJqU*JeXt3H5`AS#h(dpfl+`Ox|)*~QS%h&VO!d#)!>r3U5_YsDi2fY6Sd&vw% literal 0 HcmV?d00001 diff --git a/3rdparty/sabre/dav/lib/DAV/Browser/assets/openiconic/ICON-LICENSE b/3rdparty/sabre/dav/lib/DAV/Browser/assets/openiconic/ICON-LICENSE new file mode 100644 index 00000000..2199f4a6 --- /dev/null +++ b/3rdparty/sabre/dav/lib/DAV/Browser/assets/openiconic/ICON-LICENSE @@ -0,0 +1,21 @@ +The MIT License (MIT) + +Copyright (c) 2014 Waybury + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in +all copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +THE SOFTWARE. \ No newline at end of file diff --git a/3rdparty/sabre/dav/lib/DAV/Browser/assets/openiconic/open-iconic.css b/3rdparty/sabre/dav/lib/DAV/Browser/assets/openiconic/open-iconic.css new file mode 100644 index 00000000..e7486740 --- /dev/null +++ b/3rdparty/sabre/dav/lib/DAV/Browser/assets/openiconic/open-iconic.css @@ -0,0 +1,510 @@ +@font-face { + font-family: 'Icons'; + src: url('?sabreAction=asset&assetName=openiconic/open-iconic.eot'); + src: url('?sabreAction=asset&assetName=openiconic/open-iconic.eot?#iconic-sm') format('embedded-opentype'), url('?sabreAction=asset&assetName=openiconic/open-iconic.woff') format('woff'), url('?sabreAction=asset&assetName=openiconic/open-iconic.ttf') format('truetype'), url('?sabreAction=asset&assetName=openiconic/open-iconic.otf') format('opentype'), url('?sabreAction=asset&assetName=openiconic/open-iconic.svg#iconic-sm') format('svg'); + font-weight: normal; + font-style: normal; +} + +.oi[data-glyph].oi-text-replace { + font-size: 0; + line-height: 0; +} + +.oi[data-glyph].oi-text-replace:before { + width: 1em; + text-align: center; +} + +.oi[data-glyph]:before { + font-family: 'Icons'; + display: inline-block; + speak: none; + line-height: 1; + vertical-align: baseline; + font-weight: normal; + font-style: normal; + -webkit-font-smoothing: antialiased; + -moz-osx-font-smoothing: grayscale; +} + +.oi[data-glyph]:empty:before { + width: 1em; + text-align: center; + box-sizing: content-box; +} + +.oi[data-glyph].oi-align-left:before { + text-align: left; +} + +.oi[data-glyph].oi-align-right:before { + text-align: right; +} + +.oi[data-glyph].oi-align-center:before { + text-align: center; +} + +.oi[data-glyph].oi-flip-horizontal:before { + -webkit-transform: scale(-1, 1); + -ms-transform: scale(-1, 1); + transform: scale(-1, 1); +} +.oi[data-glyph].oi-flip-vertical:before { + -webkit-transform: scale(1, -1); + -ms-transform: scale(-1, 1); + transform: scale(1, -1); +} +.oi[data-glyph].oi-flip-horizontal-vertical:before { + -webkit-transform: scale(-1, -1); + -ms-transform: scale(-1, 1); + transform: scale(-1, -1); +} + + +.oi[data-glyph=account-login]:before { content:'\e000'; } + +.oi[data-glyph=account-logout]:before { content:'\e001'; } + +.oi[data-glyph=action-redo]:before { content:'\e002'; } + +.oi[data-glyph=action-undo]:before { content:'\e003'; } + +.oi[data-glyph=align-center]:before { content:'\e004'; } + +.oi[data-glyph=align-left]:before { content:'\e005'; } + +.oi[data-glyph=align-right]:before { content:'\e006'; } + +.oi[data-glyph=aperture]:before { content:'\e007'; } + +.oi[data-glyph=arrow-bottom]:before { content:'\e008'; } + +.oi[data-glyph=arrow-circle-bottom]:before { content:'\e009'; } + +.oi[data-glyph=arrow-circle-left]:before { content:'\e00a'; } + +.oi[data-glyph=arrow-circle-right]:before { content:'\e00b'; } + +.oi[data-glyph=arrow-circle-top]:before { content:'\e00c'; } + +.oi[data-glyph=arrow-left]:before { content:'\e00d'; } + +.oi[data-glyph=arrow-right]:before { content:'\e00e'; } + +.oi[data-glyph=arrow-thick-bottom]:before { content:'\e00f'; } + +.oi[data-glyph=arrow-thick-left]:before { content:'\e010'; } + +.oi[data-glyph=arrow-thick-right]:before { content:'\e011'; } + +.oi[data-glyph=arrow-thick-top]:before { content:'\e012'; } + +.oi[data-glyph=arrow-top]:before { content:'\e013'; } + +.oi[data-glyph=audio-spectrum]:before { content:'\e014'; } + +.oi[data-glyph=audio]:before { content:'\e015'; } + +.oi[data-glyph=badge]:before { content:'\e016'; } + +.oi[data-glyph=ban]:before { content:'\e017'; } + +.oi[data-glyph=bar-chart]:before { content:'\e018'; } + +.oi[data-glyph=basket]:before { content:'\e019'; } + +.oi[data-glyph=battery-empty]:before { content:'\e01a'; } + +.oi[data-glyph=battery-full]:before { content:'\e01b'; } + +.oi[data-glyph=beaker]:before { content:'\e01c'; } + +.oi[data-glyph=bell]:before { content:'\e01d'; } + +.oi[data-glyph=bluetooth]:before { content:'\e01e'; } + +.oi[data-glyph=bold]:before { content:'\e01f'; } + +.oi[data-glyph=bolt]:before { content:'\e020'; } + +.oi[data-glyph=book]:before { content:'\e021'; } + +.oi[data-glyph=bookmark]:before { content:'\e022'; } + +.oi[data-glyph=box]:before { content:'\e023'; } + +.oi[data-glyph=briefcase]:before { content:'\e024'; } + +.oi[data-glyph=british-pound]:before { content:'\e025'; } + +.oi[data-glyph=browser]:before { content:'\e026'; } + +.oi[data-glyph=brush]:before { content:'\e027'; } + +.oi[data-glyph=bug]:before { content:'\e028'; } + +.oi[data-glyph=bullhorn]:before { content:'\e029'; } + +.oi[data-glyph=calculator]:before { content:'\e02a'; } + +.oi[data-glyph=calendar]:before { content:'\e02b'; } + +.oi[data-glyph=camera-slr]:before { content:'\e02c'; } + +.oi[data-glyph=caret-bottom]:before { content:'\e02d'; } + +.oi[data-glyph=caret-left]:before { content:'\e02e'; } + +.oi[data-glyph=caret-right]:before { content:'\e02f'; } + +.oi[data-glyph=caret-top]:before { content:'\e030'; } + +.oi[data-glyph=cart]:before { content:'\e031'; } + +.oi[data-glyph=chat]:before { content:'\e032'; } + +.oi[data-glyph=check]:before { content:'\e033'; } + +.oi[data-glyph=chevron-bottom]:before { content:'\e034'; } + +.oi[data-glyph=chevron-left]:before { content:'\e035'; } + +.oi[data-glyph=chevron-right]:before { content:'\e036'; } + +.oi[data-glyph=chevron-top]:before { content:'\e037'; } + +.oi[data-glyph=circle-check]:before { content:'\e038'; } + +.oi[data-glyph=circle-x]:before { content:'\e039'; } + +.oi[data-glyph=clipboard]:before { content:'\e03a'; } + +.oi[data-glyph=clock]:before { content:'\e03b'; } + +.oi[data-glyph=cloud-download]:before { content:'\e03c'; } + +.oi[data-glyph=cloud-upload]:before { content:'\e03d'; } + +.oi[data-glyph=cloud]:before { content:'\e03e'; } + +.oi[data-glyph=cloudy]:before { content:'\e03f'; } + +.oi[data-glyph=code]:before { content:'\e040'; } + +.oi[data-glyph=cog]:before { content:'\e041'; } + +.oi[data-glyph=collapse-down]:before { content:'\e042'; } + +.oi[data-glyph=collapse-left]:before { content:'\e043'; } + +.oi[data-glyph=collapse-right]:before { content:'\e044'; } + +.oi[data-glyph=collapse-up]:before { content:'\e045'; } + +.oi[data-glyph=command]:before { content:'\e046'; } + +.oi[data-glyph=comment-square]:before { content:'\e047'; } + +.oi[data-glyph=compass]:before { content:'\e048'; } + +.oi[data-glyph=contrast]:before { content:'\e049'; } + +.oi[data-glyph=copywriting]:before { content:'\e04a'; } + +.oi[data-glyph=credit-card]:before { content:'\e04b'; } + +.oi[data-glyph=crop]:before { content:'\e04c'; } + +.oi[data-glyph=dashboard]:before { content:'\e04d'; } + +.oi[data-glyph=data-transfer-download]:before { content:'\e04e'; } + +.oi[data-glyph=data-transfer-upload]:before { content:'\e04f'; } + +.oi[data-glyph=delete]:before { content:'\e050'; } + +.oi[data-glyph=dial]:before { content:'\e051'; } + +.oi[data-glyph=document]:before { content:'\e052'; } + +.oi[data-glyph=dollar]:before { content:'\e053'; } + +.oi[data-glyph=double-quote-sans-left]:before { content:'\e054'; } + +.oi[data-glyph=double-quote-sans-right]:before { content:'\e055'; } + +.oi[data-glyph=double-quote-serif-left]:before { content:'\e056'; } + +.oi[data-glyph=double-quote-serif-right]:before { content:'\e057'; } + +.oi[data-glyph=droplet]:before { content:'\e058'; } + +.oi[data-glyph=eject]:before { content:'\e059'; } + +.oi[data-glyph=elevator]:before { content:'\e05a'; } + +.oi[data-glyph=ellipses]:before { content:'\e05b'; } + +.oi[data-glyph=envelope-closed]:before { content:'\e05c'; } + +.oi[data-glyph=envelope-open]:before { content:'\e05d'; } + +.oi[data-glyph=euro]:before { content:'\e05e'; } + +.oi[data-glyph=excerpt]:before { content:'\e05f'; } + +.oi[data-glyph=expand-down]:before { content:'\e060'; } + +.oi[data-glyph=expand-left]:before { content:'\e061'; } + +.oi[data-glyph=expand-right]:before { content:'\e062'; } + +.oi[data-glyph=expand-up]:before { content:'\e063'; } + +.oi[data-glyph=external-link]:before { content:'\e064'; } + +.oi[data-glyph=eye]:before { content:'\e065'; } + +.oi[data-glyph=eyedropper]:before { content:'\e066'; } + +.oi[data-glyph=file]:before { content:'\e067'; } + +.oi[data-glyph=fire]:before { content:'\e068'; } + +.oi[data-glyph=flag]:before { content:'\e069'; } + +.oi[data-glyph=flash]:before { content:'\e06a'; } + +.oi[data-glyph=folder]:before { content:'\e06b'; } + +.oi[data-glyph=fork]:before { content:'\e06c'; } + +.oi[data-glyph=fullscreen-enter]:before { content:'\e06d'; } + +.oi[data-glyph=fullscreen-exit]:before { content:'\e06e'; } + +.oi[data-glyph=globe]:before { content:'\e06f'; } + +.oi[data-glyph=graph]:before { content:'\e070'; } + +.oi[data-glyph=grid-four-up]:before { content:'\e071'; } + +.oi[data-glyph=grid-three-up]:before { content:'\e072'; } + +.oi[data-glyph=grid-two-up]:before { content:'\e073'; } + +.oi[data-glyph=hard-drive]:before { content:'\e074'; } + +.oi[data-glyph=header]:before { content:'\e075'; } + +.oi[data-glyph=headphones]:before { content:'\e076'; } + +.oi[data-glyph=heart]:before { content:'\e077'; } + +.oi[data-glyph=home]:before { content:'\e078'; } + +.oi[data-glyph=image]:before { content:'\e079'; } + +.oi[data-glyph=inbox]:before { content:'\e07a'; } + +.oi[data-glyph=infinity]:before { content:'\e07b'; } + +.oi[data-glyph=info]:before { content:'\e07c'; } + +.oi[data-glyph=italic]:before { content:'\e07d'; } + +.oi[data-glyph=justify-center]:before { content:'\e07e'; } + +.oi[data-glyph=justify-left]:before { content:'\e07f'; } + +.oi[data-glyph=justify-right]:before { content:'\e080'; } + +.oi[data-glyph=key]:before { content:'\e081'; } + +.oi[data-glyph=laptop]:before { content:'\e082'; } + +.oi[data-glyph=layers]:before { content:'\e083'; } + +.oi[data-glyph=lightbulb]:before { content:'\e084'; } + +.oi[data-glyph=link-broken]:before { content:'\e085'; } + +.oi[data-glyph=link-intact]:before { content:'\e086'; } + +.oi[data-glyph=list-rich]:before { content:'\e087'; } + +.oi[data-glyph=list]:before { content:'\e088'; } + +.oi[data-glyph=location]:before { content:'\e089'; } + +.oi[data-glyph=lock-locked]:before { content:'\e08a'; } + +.oi[data-glyph=lock-unlocked]:before { content:'\e08b'; } + +.oi[data-glyph=loop-circular]:before { content:'\e08c'; } + +.oi[data-glyph=loop-square]:before { content:'\e08d'; } + +.oi[data-glyph=loop]:before { content:'\e08e'; } + +.oi[data-glyph=magnifying-glass]:before { content:'\e08f'; } + +.oi[data-glyph=map-marker]:before { content:'\e090'; } + +.oi[data-glyph=map]:before { content:'\e091'; } + +.oi[data-glyph=media-pause]:before { content:'\e092'; } + +.oi[data-glyph=media-play]:before { content:'\e093'; } + +.oi[data-glyph=media-record]:before { content:'\e094'; } + +.oi[data-glyph=media-skip-backward]:before { content:'\e095'; } + +.oi[data-glyph=media-skip-forward]:before { content:'\e096'; } + +.oi[data-glyph=media-step-backward]:before { content:'\e097'; } + +.oi[data-glyph=media-step-forward]:before { content:'\e098'; } + +.oi[data-glyph=media-stop]:before { content:'\e099'; } + +.oi[data-glyph=medical-cross]:before { content:'\e09a'; } + +.oi[data-glyph=menu]:before { content:'\e09b'; } + +.oi[data-glyph=microphone]:before { content:'\e09c'; } + +.oi[data-glyph=minus]:before { content:'\e09d'; } + +.oi[data-glyph=monitor]:before { content:'\e09e'; } + +.oi[data-glyph=moon]:before { content:'\e09f'; } + +.oi[data-glyph=move]:before { content:'\e0a0'; } + +.oi[data-glyph=musical-note]:before { content:'\e0a1'; } + +.oi[data-glyph=paperclip]:before { content:'\e0a2'; } + +.oi[data-glyph=pencil]:before { content:'\e0a3'; } + +.oi[data-glyph=people]:before { content:'\e0a4'; } + +.oi[data-glyph=person]:before { content:'\e0a5'; } + +.oi[data-glyph=phone]:before { content:'\e0a6'; } + +.oi[data-glyph=pie-chart]:before { content:'\e0a7'; } + +.oi[data-glyph=pin]:before { content:'\e0a8'; } + +.oi[data-glyph=play-circle]:before { content:'\e0a9'; } + +.oi[data-glyph=plus]:before { content:'\e0aa'; } + +.oi[data-glyph=power-standby]:before { content:'\e0ab'; } + +.oi[data-glyph=print]:before { content:'\e0ac'; } + +.oi[data-glyph=project]:before { content:'\e0ad'; } + +.oi[data-glyph=pulse]:before { content:'\e0ae'; } + +.oi[data-glyph=puzzle-piece]:before { content:'\e0af'; } + +.oi[data-glyph=question-mark]:before { content:'\e0b0'; } + +.oi[data-glyph=rain]:before { content:'\e0b1'; } + +.oi[data-glyph=random]:before { content:'\e0b2'; } + +.oi[data-glyph=reload]:before { content:'\e0b3'; } + +.oi[data-glyph=resize-both]:before { content:'\e0b4'; } + +.oi[data-glyph=resize-height]:before { content:'\e0b5'; } + +.oi[data-glyph=resize-width]:before { content:'\e0b6'; } + +.oi[data-glyph=rss-alt]:before { content:'\e0b7'; } + +.oi[data-glyph=rss]:before { content:'\e0b8'; } + +.oi[data-glyph=script]:before { content:'\e0b9'; } + +.oi[data-glyph=share-boxed]:before { content:'\e0ba'; } + +.oi[data-glyph=share]:before { content:'\e0bb'; } + +.oi[data-glyph=shield]:before { content:'\e0bc'; } + +.oi[data-glyph=signal]:before { content:'\e0bd'; } + +.oi[data-glyph=signpost]:before { content:'\e0be'; } + +.oi[data-glyph=sort-ascending]:before { content:'\e0bf'; } + +.oi[data-glyph=sort-descending]:before { content:'\e0c0'; } + +.oi[data-glyph=spreadsheet]:before { content:'\e0c1'; } + +.oi[data-glyph=star]:before { content:'\e0c2'; } + +.oi[data-glyph=sun]:before { content:'\e0c3'; } + +.oi[data-glyph=tablet]:before { content:'\e0c4'; } + +.oi[data-glyph=tag]:before { content:'\e0c5'; } + +.oi[data-glyph=tags]:before { content:'\e0c6'; } + +.oi[data-glyph=target]:before { content:'\e0c7'; } + +.oi[data-glyph=task]:before { content:'\e0c8'; } + +.oi[data-glyph=terminal]:before { content:'\e0c9'; } + +.oi[data-glyph=text]:before { content:'\e0ca'; } + +.oi[data-glyph=thumb-down]:before { content:'\e0cb'; } + +.oi[data-glyph=thumb-up]:before { content:'\e0cc'; } + +.oi[data-glyph=timer]:before { content:'\e0cd'; } + +.oi[data-glyph=transfer]:before { content:'\e0ce'; } + +.oi[data-glyph=trash]:before { content:'\e0cf'; } + +.oi[data-glyph=underline]:before { content:'\e0d0'; } + +.oi[data-glyph=vertical-align-bottom]:before { content:'\e0d1'; } + +.oi[data-glyph=vertical-align-center]:before { content:'\e0d2'; } + +.oi[data-glyph=vertical-align-top]:before { content:'\e0d3'; } + +.oi[data-glyph=video]:before { content:'\e0d4'; } + +.oi[data-glyph=volume-high]:before { content:'\e0d5'; } + +.oi[data-glyph=volume-low]:before { content:'\e0d6'; } + +.oi[data-glyph=volume-off]:before { content:'\e0d7'; } + +.oi[data-glyph=warning]:before { content:'\e0d8'; } + +.oi[data-glyph=wifi]:before { content:'\e0d9'; } + +.oi[data-glyph=wrench]:before { content:'\e0da'; } + +.oi[data-glyph=x]:before { content:'\e0db'; } + +.oi[data-glyph=yen]:before { content:'\e0dc'; } + +.oi[data-glyph=zoom-in]:before { content:'\e0dd'; } + +.oi[data-glyph=zoom-out]:before { content:'\e0de'; } diff --git a/3rdparty/sabre/dav/lib/DAV/Browser/assets/openiconic/open-iconic.eot b/3rdparty/sabre/dav/lib/DAV/Browser/assets/openiconic/open-iconic.eot new file mode 100644 index 0000000000000000000000000000000000000000..7ca7c170f1a7780c7ed612e469d746adad4fabeb GIT binary patch literal 23144 zcmdsfd3;>eeeeC9JBvmdX=XH=Bx|Huq#0|mEX^*FEZoYPLI_uavict*V{?@TO zMkW-`8y`~?`wHZ(8ar|*sx;95Q5460cy8M@a&Y4EWz?ix|5@Bu?7IB}JD+&;moFlT zv2NJCdwfKrExSW__7i;aoZ)!Dh8|D=_bt2cICSS{9#tA}|E!{@_usyMY~;Hy|K)1b z|54<9>yD8-Cn&2tjC2w2NB51~G5)JjaZFJL@xHiwV*kNIzw~BwMcIz$wlO)OsC{De z@{6~4mj1g^rAARs`R+$fJT`Z|{D>Nt`4#3;p?b6)z5IwWpz^pBH9osEe9M17lR2*| zyUo=T$RnAz0#nX^Hlfp`Vn@F!L^tjyj4R!vq?GTL(*wUeO9Du5*||njzJ6Xg|C;R8 z0KUhO&Ff_SMHM2dE@77Pwf7>(kQRbP~cYFu*RbH+< zUEW_YJ%7CK_Fj1zPSeHtA@`&1QgvN* zclFxp+p8z5zghj`>R;5@Y8q=+)ZAHfs%E<8hc&l5?ewnpj(Sgf zzv+F$x6^l@?~U4M?b_PowO_Bj=&$v+`v?4c{a^6E9#8_&z>2`Ffd>N9fu97g4!#&# z651PjHdG4zbJ!7X4{r(|37-xBxUQ)#UALlcux@AFvAR#xovJ%q_pQ3~_1*RT^#|%d zQU7%P>kZuv0}T%}oNah@fnz~&!Qg_u3m#qYtp&emY;3%}@##o=M2kESIUSjf{2=mH zQ?_Ym)2Eug)bx6@t+}arRrAi~C!3#-c1CZC-Wh#7`orjNV@EJ#yGx%3MW#4d0uU0)$(@zSTAHszP; zuQ=>KS^BgpkW{{+a<-kbpLROvt))+c{C}Cw_%gm!#+PXL!LyG(DuOd_Hqg&xo%m9t z;4e-E=!9avSPq|Na^{erPP(YOX;PoiN+o@QCdKs3YK<;xQ&XB|?5MYwbp~VjmntjE z6_l)^nx;(|TT_!|JwQ;=P=qqhR3?{A#vQ@z%M^bZ4QeO8cS20R{7|}7O7A{#V)sMn zD(~9aa!NE5Onyi;<8+j3vpo-w4t0aPM*6e1+Dtf%`iSr^QuPQI_U3tF0vU9o9N!`Le*GMm=NjI<4i_=) z&DJq#l+e%3uccIauR7$C(3N$})le#-XYbJm;{B2-FOL#)#f@R0yhV>gJXa)JZ8a!g zKPQHG)~>Fro>ufam8BNa8bUGAp#FJTnC|&TSoJXEK zg8y3nrabpu-sykj_v?QJ?qHx4@E2Luql6_-Gj;?RS|CixFoiN{^z}{J@aCKPGjG!5 z@v}vGV$(_e=1n8V&#F<*#KVuAJwCFDo+#d>-&{lzRbxi^i+QwNaY3V0K$_q;#=QCx zV0CYur;8wzb0y}oXOIEH0(qgB$#>ANkW#O-vT92CaHuwn1@Q$OiC8?D^CfZ_e?IJt z*_f3ELh1Z6%C?lH>A_kr{eAczEl@M_i<*FT&xM-0Ns8+K(jI}xBaghOQK}(bZ`AY? zHGx14?bE2<_o1gsk)Rfg%>P2a1U?oY_hX%7X)y6%A{aoAa=vWX7xTHq7hIa=n%Uuk z#1Ye#&zD|sxx`wR#)1gN%V-?j{K^{UN|zOt?Oy(@oXZwv5lUtAeQJysx`TqXVen96 zO0tE#KbMF*67i%x5p+77vBh*#gI~4V=^~FN{fWBxV85>P560_0ktPQPBlPK#V`VRu zjyc4~YonN#2lanj*tfWUaIk;TvV~OrAT>o!Reo^K8&E6Dd0(}H@hfRGjpk!$IG6Qj znDD6o$s_l#dh7hE`?XTvU>|61@R9wwty^>Z9~qo^uC1?+>yfk}l33qrsJc9p89M|* zBIisMQiX6KSa1dlQ)wFh+0XP->5UtwrZ#Sb^qx|qmrg!>>5`d&y$lv)(i!RsDiP2M zBqVps*+g<$av)z{HkpV8LCCnOKI2Lk2aj~F+Q`7#7v(aDr-@)qXN29ILi0-5KJ`3hmQ918kk zaYrDO!A@WrF7#7DL)%(97@2vM*GMF1UhN-J9UT#_3yh~T5}bLJG1^eSNctlk;MgW_ z<2{d^0-{Sna5AHZ0KP)Ls1{}blg|JnNlN7xsq;J3=6dU2U20U*cI}$^ipw>-XHjn$ zy-uMabQvTLGB-#JTda@3*21U^tRa>z7_^VkxAnc?LRVJ@Bd!mA_q#N&f&dU zI=#5Qv0i=Z>F^Eo=FA!Ib^c>_2~S13phou&r_;@ezQ&`C+Q=^LnESfTTJ^f6%{MP! z!?R`XqX0PdLIvyOmpdsq%T+MA5U&u3rC_X-BpW+sIc`&Jb{>;4%)lQ@4l>2})G2dc z6+>d$c*>&}p2)3Ox!L~`jAF==k0{VoB0#Tc>=ze|EJw z9QyxuxaxhI^X_UoI`f--n;+S~deyS^YD8^wxb%;?Z0`JKU%iJa^&^`L`yWXr?p$Bs zc0hxd{{mXkw6Y5$&E_-dW-4@1Kc$<<;S3j$!OX)$m}-Na1e{@GK$j~*# z!Zkxpp;gXEu)4ufOn5Mv@#sQZ1f{AHYg^-qkJ#)EyZyRgBY&X&nT#g~5>;p_ zVp|xEM}mPSPoijN>zLpV)@53$0Iy)`#=>`y&)(`w1>??esD_d$H4&F&E=ISyTsP{+ zD1IqyAE>BFrYd&b=yE+Hrd>17xy1GtPwMWw)cU9WDQ}IRysl?lK$YA8?|H`Rez}X9 zl?3KK#x0pr0`yGSKNLtUI0EtkMuADB2rp|> zo!6?=6oTfC_gn4ZYPH9z4UYwqO8%LxmE_)}j~JOoGjsB|PKjdQbj!H|)%MZcxbnDJ zPI+PO9*APOi;^ZFp@*NTunpTP231 zKWA(p30`XPM&BS$GxJxlJm4;@sxL-hmlI-0pRk_6QiFBCGx8Cua+2vm0!zMZJ@dKD zXZi$}oL8rdVl+vUtRC?*L(Vj#J#z+pkixcfsF`AE4?qMKH;HYqm<$1oO)PO(i+NoD zF>EVUH`tRj9dd3}gH`&e%Ai`4udbXTI|Z9)t1%brI6DWpcVh-mhQpU543_HV4OttZ*8s#fqlWSqU<989HKeF`+Jm%$;32QxP z+jNmL0U)|2gbxQA=6KO@BkJn5y6HLu+codxcfv$uj>UMC;iurM+SYL+@RXXA%c;qQfVrn zMq%vmZtn?3(u};)J({k&DUIQmp*vSZC`0ir|VBpH&4)9y})C1VA!2V7k8g1 z&2K{TAft~ebSmf%lp$!V&Tt~tnh84-1>W&eM1XsXA<4oGZ55g^|IM4rW&_~?5`>-2 z6!c(|raY8bjQv$uOu6OQj^Tm@hGGE3YC;9*#O{FR?acA1gXBVe!?=^X$a6|6ay5NAQaQ5 z@f7x_d9#$?d=Ww;n5Kj65zsRz;1Z}BI&LRR3;@x0C}9+L;{{^}MB2XyLx+4n%{lG7 z?<9YgG|g+21>O%ryu@>iMoLf_)Ud1atqUcF@nt%@Q(_A&IcS%p)y(4{RPxd*%Nu zi7gi!hGc^@X3mJeH@k;zC1H`#CB{a^o^+XrkI|mV=bgY`hHhRk5m>$WRaMPf=b4{_ zgV$Eo@Fo@024gSjA7g(1WzLgbo$YTev|I3WKV$}WtB;Ki38-^Wio!k$Qw)mYi&~n? zNf<}NY5f!pvu>Bx^nPT6GiaQJ{PKC=wF-l$!%~0cYFYE(%;pPyf;9(&d~=0V(t2ji zws!Zw2nk?*SiEL}QAeAbqF7cFU z7{11qzW&V+9-GDVo1X){9{*g!(2#%6RV-hw+QVa!^eQog9U6-p{R=R?@;{){R%R0N z1BIfKCTCUy6tR6)N&tqq454^>u>PP}{70SG)0aQXrYQxc7|>HD9R{%`a!IIj(vTLm zHxcXmuq`EEO-ezc0y(f?hdfWFTDyqldLNavjXlv$t-^Py(oV(O*Q__a9MwQr4uzX{AV3h=tkIf~i^V-88t+QgnyiRL}uVqp>es(r7 zuU%OeYQautYf}Ql-4$ z!6VpDeQm+AeIZz0VN#jt85)kcjod8Ej!;IOr<9x48d$ zZBaqfHXSS8aig}PMZ5R&CKizGSu%$00Q02`W~pF)SPN%&B`<-sOBtBV{(;oW=Hhqb zB~&7DUOA4r68ji<>AkVfyfVO823~q^jP<+l5_%?ikfbrm+g;3anR4L4g=}N>73dR= z&>@Z2mUTQ0x*+>ru(+)1X_t)oF!u2RSXXkqu1&UPs<9>Ftk3p4ER>N-v)_eRneAMF zT6sE|9=m!?Pm9at2yfnT|C)6lxoMrxr@Gs^2CmWWm3D|yWo=_5*i<7$l9-O=R;3Kr^Wd`q zU~KM40@gJGHlP%%2Vn?s+Ni3kGvpaw)p2)wXXnbBYa_vWPrrV`t^b;iF82mMtbh9+ zS}LZ=ck{}Qj?UwqtA=apf|0;V{iK(?bl38lkSFpZ{iXY8skMjM4#jd~PGXcx31i#w zb}oOvLE1!_1=ZNXT{bWwTb3k8O2jx_s#vfl*U_prkA(s=j6Q_ zwRwvxb{D&DUN^Y*rY_@V;g%JvR<7El<BmN6ieAmntSMgT4u)Si%qHi*74#=gevLdda{_h+0LHA8BqkbslFG&Kd8cmFD82Zofl_?m3P&fLU8eHK1L z8y6W%7}i_H2f$6tS$Y2%b_$-)Zr~(oGj#^p-UX4^1>6pRk5zTz10ia79;B=<PmRh&yoU5>5wf%gMMi1Vb|ZJ6U=a@~|Rzi9)}cO0rQq ziHllDLPHLR(%~i*)?~}JLX%t8heFjMS4XYgrdD~p4u4&&raj=G+KQ{IlSOYaz2TmA zH5h5??pZ+8+~ljSwg*-dsZ|cY+WTUn*RJ+CRF~6J=?~5tXYTLxdlD{NP1swdIvqCO zRqI5evMH}RszY})hpt)@sB;DuHhDT@3#)2t>MDa_hrO!WSu6Z^R5rUS>}sIeQ5EVc zlc5}q0a;CKi30o%#>5LF9B;zT97}UWLi>(Ji}`AXbV?nuTrMntJtG0Fu|rv`WRwCh z*>Zy)TJnLfuoTV8i}!PH$q_JH_F3a%{D1eGc@k$<^pqt41SAneu?ODX|9P^Xdu!o< z^7I@gK(NRxJ}6IPyYe3kFzdTque92g{0x#JX2*xGbJ)=EAR9Q5SsOnA3*gHv4NE*- z4Zm`X(LUR+4Gqng^+}JGVW+orR==3?gD0F}Rv?Cg5&z6yzvwfy8NdED&Zmuj_Gjs@ z`TburwU=3*0i2zY+y-W0Y5N1t&c%YbL&HGT(gp}N!b-k%#}3O5U@qw4?EXv36t4O? zC98r5QmQDJ0(RyYxE?4o@Y}O& z17IKc;nRmrH#izQdmPPxk73Be*B%BcKE-KdXvbwcUWt2Pjs&6xOO%EWr;CzOQXc8k zzo>t)j~%Z^2Jv~aPyak!*UR0T>mztM%)roVq2I!(3IM>=z_9`6RQ(Q+%_P4s_{)?R z99aMw@cRldwZLNKGmsP)w8AwY4CLO#<}5E;1$}+KXU3NaX9^jgUtIG0XB>XezpR7P zoCiu`hpfR1Aza`KV0+|3)RGSOvvrq6dol%Q07H$i^dExzbx{obP@?$KEqgM0g39^YHMj>}3UnW7!P#gQf4=BG`Usd9I_pJcpvqax-rF|FQimBG}65#Rs0j zc6^@MloJ*hsNI-hi&N0^-$Ue{x6#22C+(o^raKFveGUhxn9_~j-CeW(E0cynL+n$V z^+RVE9>SFbrQ65Y5ZIXAg!ij9n~dVX24o+*7T5yX6hqkcr=$mZiiX+UY*DB|qt+gg zwqo8TtqHGhDzlX*i7aiTU??YwMeW2v#Zi zBc7@n_|a^3@Fg%ML(=U=4vmtrd|*5~>@6giAAnNp;r;h{{EfX`<|Cd1bEH#S)QC04Z4NaAovO)MB?M}b zgOWqd{v%aX3B&d8B7th=o>{}4!ZN;x4$gRiIY!XL4n|ty2Z$Qp1ke}cH;W!4#j=M( zbl74juQ7^2|%Dk27f75{bH68K1t z3^H?=)COypv};34+kG8w&F;MB`iEYo;k}cP8z&pWP##+I`g*@aPquGQ$wKdKc@^;eeyO)y)Z(IRwksK4{BwuLaQEZNZ5&u%Fz z#sSNS^$}rFf8s)u$;WNz}?_oAzFE&7cVIZD+h8nEhi3!1f09wA5)1|a| zB4Q+l&m@zsJUwv6uz$+AHt1KrXIiFb-{S_^Mryog>UyxXN_z(R0A$(j&X#Dlrt-4M zezV_D+GJ&Y86TSi5gBXQx$)-Q7_g^#{*3|7j$M&^0U9Z+CYT?PZo-$5lVsTV*mlCu znRR%z42#V}V)IbcwDuBY7hdN+Uj8iB;On3-fKMsuB7*taGLg+YXC3}#1TJttI}hNO z0CmLJPXHSTze%B*3TY$jI%iX50065N?U7I3mc|~M=xa;g_Q`;lw)M5y5KD*%LJ?c? zR~!bAW=%t?Bk6soYxnlGwe{_-J-sh&ga+{3%-W&lcqF5{Seos-9IuQnoQw5?Z)2{1 zjFSzlH7~~R6884Y8AL9^nHtz)ME2=!KrikAU>Met0T*b5`*8VbwG!*dc52h&D766a z67q&FobFw>@OvLVec{t$dil!J7wSLonG2^s{5`p@hTm9np9G*DSP(9Jsw5FivC}X? z!)%cOvyuPtB{`3E!&jpoNdv6#L7QXs142D#bI#rP`fV^ZvHLi@K>znYFjKa`(4B8! zLn7!DIj&bK+Vz7*k{-6z#2ff0O<&SkQ|G)Ci9meo7t)l;p2!CFthsB;8;@_H zP%qAS0JdCg8Q9h0UFlu3JYEsEf2uK)G*TlT+oe!m($`O3+Yujr3K zu|2d^)3zQ$Io{JaF^TbT%pvG#UaTRQg%yzJr&kd8pY}t8{e1}^)TpJ{!f8wwWt6eu zW0!SGdSY8CNC0c@qdcZW8n08-8g{Z}lZNA7{YT@p@Sdk$e(0_d^6I;O`Qz+Tl++JW z+pfK}-?;aom$#Dl`xnl&eQ@muB^`ZM^2G%D3Fa^x#V&WF&zbTWEW?PQyGeq5He^9 zX|pn|NPOx5@&L?TpLhXL8Lk^=&fMsdw;Vd-(tqwMm0Z-|LeHTC$V&N4hGk0w<*{_) z5%5Wdu=(MT1mIn_G{Zr6WiOTd7%@d$4fDL8N#2UcS_=8W3S_``3NcS9hnh@5m1?GZ zEv;$T<5}Vv@O=KCsy8GvtJ}6U77tCkRL>4EeVzW*)=#xgHfoK}-uS0K`UCy9LtoP3 zjvhknNZFdeL?@z3H&K6#9X{&^yGk;|LIrW?*jrj`n!Cs;R=&nB2Cx zwa%$k_N*G<+3(9=m%n9kLZhGQjM5XO|;)z{achl5^&1o1nAxYK0_^!%gHg0?V`J|?P=LHqXFMSDayz({; ze#q_<%qJh_NEgB&#iTb+laGJksDAE_8;-IQ{zE&~9Dee1Z@wv}k0Nv54FH=s@5S*S zW__mIf@Piko`j7IM<5XAX?=qK8xb5v3OClCkh~scaHcKDha7VWs5B-1lyNB8U#V*i zvE`oQ&2fY;5yUY+i=**;$dcuJhR?9Ew>oVL)nZ$TmIQ($IgkZcY^zh$_QShNkL$m9 z@)b&!zI+#A8`S9CKh%G8>PZSbuIaD7a@UtxLBro1O;&<;v7Z)u#Kx6&2~-AAGFvNZ zw^U{Ad0I8C-SYT99Q)|c@7FGzmik_sX-S*bpMUmdisK)8_wF(7$Yl^ zSH^fg%?K>81PE@^#Ls~&3{$~0af*b`_2?6zN}7ZOaWkevqy&tiZ6#?@yQH6!I8@$< zWZN9pz@D$=^8>u2Atfx69akHZBC@En37@LyVR8KUq^Nc{(IntQ^C^W%u$#qcsNhm4p-oYvN}V)`rq{BHTy zd)9OMphS~DmEXcAFZ$1AB!!{wn}L%vFO|PxybgamXRedqZ2dx7njwsQBj=~(Pi0yT zi@zj2@mc^eyloB2uy0)4$;UfqK8o)xbn$=6o~Tjev~a%o3C@RJfr006q>XnZBJKq2 zV=@w#V+Ek=yR8E&@_6}0J|l>uDU6-jMlb~GWo`<5g>&t#_(~Nldkn7+NOjxexNbVC zw;K}!%YpHrwCR|P#Fg=`bF^!%p<-cLt2%2%to~gYmHA=07%=C-D0gKDrV+$~2TQii z>Lj3V(_?@b6&${U)fArYLRi3+{V;YWV?pdu5U1p2k0IB}D3>LC;veA?^YjrTuo63i zLx}kx(A0p26B(PBcCB+xn`9P(S<7f1o)@5UNDQ3TDtDB!A2p#y(|v7I>=vDXonWf1 z4`)SSL*N6I>=zZuzBXfQs%et}uQXyz(&1VoXam4e1~GtX0Yid^Rpbs*Z zMA}WEEJ;Qxt)((n5a)rwKMgynG?KzXIy+C`V;{C_n35Hf){A4(Xz~DqYT?(4VN?x&t9ATers*era_Tqb+b1rnNuMIz5-B@vh{n|L=C*_X80~k$pa9jaemf8kdl~=TA zTv@0AmvEmDi!tw7`88KbjN(gwjlX33CG-ot$n~{K;~V{X0q557(Ofq|pVTzI!|BHx zaV?(%K3^h(7>1n%E<{ZFCaW3 zGJT~CywZHcouwr_qA=1$*fk2{|2a*l#*W-@a0<3y0KbM|-TEhsAKOvhLLP zo~Uza4&QAL+~#v=s_M$d;(3>9(jeREoh%ismhJD*$Q5Gz+l!R(va17nP{ za*^$d(0~Z25W8p@!5pVT7vK-&2H|{6s(b=erKF88d0@gbJN!8Jy48=FwhVabsK)8Gk+nT(ju}IT%&Udx#Pg6FJbn{#z^`;e2g>! z+5NSd7C?vp!%VA)BlxwMwkfqVYNqWG$DhI}hOJIit!^`6HdXkyAIYapa}(emAZQ@!OM!@NYs{ z0*t>HznaEB4SGmkjd#27W1u_n_CAz5qyQ3Kbzpqt(6|;I{Xld}*M_d>=6(AQ?1>)T zb7*&T?f!j-*6u&BYdo6n>W(hledy4{lEsU6B6la}b{*W=wQu|o>L~jS()DBe_Z?Kw zS>snph1pHk`_0&KMs1crsCj4SV6h6v0&bqcxD^`FVa)=XY?idGM%Nb(9`r^>8tcLdWQZRJxhO1&(UAdH2oz#PcP8d>96QT`UZWI zUZQW&x2Z&bjb9b~4*e~?Os~*)>3j5j`T_lSI!Av;KcpYgkLmA~w@<3*=@y9C>q(2A zjOfXVo}B2(i=Kk$DTxwNbeMxZjtGMy<225B9j%FoXF%wrXVs!k?9qg5s?`cnK6;k zL}pxMc8YAb$o7bAT4XaKn-$p{oUbBV5ZR*0_KNI?$c~Ean8<1(J1(+2MXp=qdPFWQ zav9jrMJ^|Dd66rKTv6nDMQ%joMn!H+4o_-J;MV3TaWuh(cBra-xtIg@Py)MWI&|Mnqv$6vjkB z6NPb6*eQzLqSzyfX;I9GVpbG$qL>%Of+!Y6u~!sFL~&FU$3#&R#c@&GDSEp_Z;$9r zi{6ar&5GWf=*^4Xg6J)Z-d@o=B6>$f@0jS-MDMuh-6=-8#Ym4BNsEz;7|Du}oEXWA zk%Aa0ijiJ1G9pGs#mJZ#(ZtA3G1@Igd&Fp3jAq1WR*dGvXkLsK#As2B_KMLFF*+(n z$Hb^6M#shIPBGRk#(Kn9T8w4HSXPYX#8_U86~tIkjP;7K5ivF@#>T{$CdS6a*iNB! z3#~_JX`y9=mK9n~XnCO(gjN(i^Ey7PQWIG;<SQt;ScwXs*-`HK=76p0$;!^O`vT-g6sA9SOvHx!L(@xe7gk-ya|LxS+E zDBmc1qGI6lm7?G|&fqXu`)_rbqT~LC7K`IzbFWq?F6BeiDtb`hB1O6SLQ%;ae@}tZ zdPX^OTHbvKz6)`hp4t3FD&`2nlwId*2PupEWfk}Qv%jn<7Uc(jIY_x#z0+R~R<2N| z`^&l#rf%?;ElQ|*6f39(Ux8cE7|pVZm3`M=)|76_=l*h#GEDi&Uk+A!sCoXfuJpn< z&1)@6gt}81tdw9Bxk@R%3-J7T$}D_mDt*yIUzCO`Q4+R5CGg#>CPIkr_o(N;8T|o+ylr?;9K0XF_Rdalbq67?0ZVRNHsj_`XvL zOVP(rC12iX8jATvQ?Q%puc5s%5p@%>|94N`4^KZ*j*G@QSZm>eV~bG|@gI*; z97;;pN9z|ka!TpM(#eGdagqHZ6Jlc$W8!1u(CpG4mGdnXWfbmGg87&zA3hR2(=3`Z zsf;QtnKrR#N@QH$SPWi4lVUWVf`{lbwxZ&~DKUWoO4=a$06+Vw=T2I2c^r(9fCI4M z_{{0wz`19X-+qf!{Vf&M(wU&q_W#e1UjqDANwVqEkE#S?2356N=b2YEOkzLpzZCD! zs`nSodoIXp4|aqoJ;pNT-*vMty`_C``>T4d;480!#J#$geyn>h<)&WuRP|84pzBr0 zpCBxc4gq0R4CM+icsC^sa;S$Au0$x;DA!?G9azR2l_=#FFk7^8yCj!*kYuuw3Szxe zxl0+K3{vg}u@3*CfhJIMd0UC-_)uBeJH>y$U zEovV%TD@KEtH!GFYNDE~rmFqaJJq|?0qP+2ZZ$(4q7GBWk|CQO~I&EAg(ZOe0*$tTzq_dLVRL;QhaiJN_=X3T6}JNUVMIhL40BS_=MPmxP=l9G~>Qj$`W(votM@{;nC3X%$w#wW)n$0f%nCnP5(CnYB* zrzEE)rzPhm=OyPS7bF)Zk57qBiA#x3Nk~adNlHmhNl8gfNlVF1$xF#kDM%?y8J`-P z8kZWMnvj~9nv|NHnv$BDnwFZInwOfNT98_pIzBBnEiNrSEg>y2Eh#NIEhQ~AEiElK zEiWxUtst#1ZG3KQZd`7BZbEKiZc=V?Zc1)yZdz_`ZeDJFZb9z&yx6?By!gC?yu`eu zyyU!;ywtq3yxhFJy!^a^yu!Tk`LX$N`SJM)`HA^S`N{by`KkG7`MLRd`T6+;`Gxu8 z3t|i63gQbA3K9#F3X%&_3Q`Nw3UUkb3i1mI3JRqZ{eM?A=Y_Xl%_!<(kjWAd3InM; z3u37Oz8D0}dbe^9SnytTIf$ngTdNFb&uMlHmk3yd)0mFe)WKQP(7r+rnacBtAA8m)i>0`>Jjx# z^{D!m`nLLx`mXw(`o4Nh{XjjgeyE;MKT`jso>Wh%e^x(M|Dt}PeyV<^ey)C@eyN^T zzf#Yre^vjceyx6^eye_`{$2fE{XzYQ`cJhDr2V7nQGZhZOFgUpOZ{2>1*FbZuiCEu zstVPocBucY{-!CKs%ct~7Od%-MY94;U7>|&p;|ZXN-a#gO1oO?uJzDtTDWG{BD9{` zHQKe>b=vh>FYN}+p*gikt+#fgc9RyR-K^cBxwJmoty;8pn|8YvqxDsmX?JL`TAUWI zC1{CSl9sHcXsKG7)=%rN-KnK(cWHmn251AdLE2#LZtWf|L%UZSq7BuCY4>UOYY%7- zYQwcm?IG=9ZG@JkjnqbIk7$o-+1g{;<64e3S{tLBA-Q`Kq+~fHmk*NVZb*f%FsK+@ zGFRKKy{UZ}1b7+LKWI$Q{Ggq|LxN`pza7l=(R!nP(lW)e$I@Ya%DTk*k+sbTH=em7 z{ECHFd=>I@=!npR-LC0&sM|Z;KJWI=ZtYi=T)FMaFRtWa5n)MTnPJ6Yi^IOVD)Or5 zufFo?Yp))Dbyat@`%T?5x=-lt?$N);q#kuWUhmM|ye>?Y*J* z(Hmd6X;ak7sE=;G^5)(*kG*;A&40fo?v|Oibhr{+J6x~3-gk*U_C7Iv2K5=;XKJ6d zefHft@z&B?KZ*{Io*2FVwqdtTx!r#Iq}%t#*kYcHamT#U_u9Ts_1$qt=p8#^uZ_Dl zZbjTL@saTl#LtL-Hz6jWHsPDZs}g4=?o9L~MJ7Fzv@7W^$%B%gO@2A~yObMJvQxIE z{5>^2bsYpsLE6cF;r#~md$-@&{;vM(`oGuzvpaj=`OKXi>G!4AryooIr7vspW;dx# zP5kMmpr#0Z%KL&iWohy)uzR!l)4ptL81wKP`cvC8Jwf4Tcx!v6Ju<`>C35U8UzE!m z<+Ab|9Q9xTW^s?+tCw!f&u3)eG``N6<~dxvyn;yG_Lcju2V zrY>!oWg8z232n@yi`vMO=!Z`B4)Gs_vG(R5Zxo(0gAJSc@MMF3Easi$WqRhz8Jh<% zLo|wD`yU29b_)|0R`Tq$no@(m$4hjt!MocOA4 z3>M+~VWIykiHDaO`}ipPyCPfX*Vb|UllQr5gq_<&3wA z9LBtEJ#6E2EUStNOmHhbVzd3G5SPo2IY*U6w9-arv10yKk`MwdgRwN|46Smk)bK(p z<~iS8Fq+7XQMU}=yp{U9^u}gOJR5b0hV-q6@m7z@TfJ>|)GkLI_26|AA-Vl$y>2?2 z<8!;ba+Ixnx$Yl5QP_pyAHhSm$ZqBEX=Lb{2&h#YL4AwAYmUBT-#G7e(|J?~+?D3A zl^^NcdkZE_w9tGp<^x63%*qKnKmAyKyTF*t7M}ABusi_@Gbbx5M6|l>zBZ4y4YzUG zZw5iSaBt8#aiWJ9(S!92#c&4=9(^+t8vYKrP7O9roYb@4nDl4%CHwNFzZ`zU@GdxM z&ls#9&U$GOOJOOK24{^lP7Kyhojl-ST*Ke!p9Y;WeEmeE{fQ?^N3krHwK@CcC#auU z>lbWc+u8P&+t;r*_{2ZiGw#;c&ssT^6|tfPrL$&HKQF($`89Tg9VvbNiIB8Iw>fZEh%5L5_V)DG zCK$dHUy45AwdrrO4;b&qufx`l8Qd+p+jl*-aX1^wL_cvIwtgrM-}u-r>W=17%Bdi5 zb4O--v!^2ybLfqVpcV1Dt+aYrWFkE&(?C-G=?n@__4dXB&09LRvT}hT=82smoG}-c z$*1YF>dV(Vyw6+K*VnIidNq$ngjB%T0EX#0Kz=%~u=u5$W5vsP8Rmpj_;vMjHzUhd>0thS%<{1)5y zF8;7(xx1#y;rrTU_qu&iKDQ+dv!(umpZ+-XJfgOmoLy6J0`Ar z^G3s$V|nxEWBe+{jO8mCPbP+^fk@FIw758-IoVB*#H8JuJ8|0M>=C2A*~0!f?NIK{ zv=1Nof&I(iTHLonbGSM(?OFTB96r2%|6zy8)X`vpXe=2M)dM~rFn~u3xqUp~}lhokzPwv`cpd`wMs+2o}s!4}8();yJdT+na-3*2DY9WI26N z)~qpOvK%h&L2E!QIhiN(OmdT9^KRd{ZXY`sct(-qvU@Ca8C~LZS?Den+s~(u4@pfO zG9=Y0qAlj5w~{DmHN^zTv6Uo>UlxK;JV73upXt^MDko2_EHKE~qHgf93aTCXtXR(J^#<~%;*@o!uve?>qn5AOO)JHNhjPm(R01uExn9ub0 zPwhIs=4}H#hyhrG$`5B8VXw12bvxH>T(_}du#QZ<%h201t#TDQGQkXsi5+;b%YJJJ z&xzozn4PksqO!6gC)Viv(urya32z%%yUhXxtU|zT2x_0x{tASHFH~%pH)no{ezNp% z*5<*+)>ZnlXP;YMz0BYnyrFt^%`?v}t}*hObziy9VIP$F;237@&&W2W*ZrJX1%Cv+Ef~n za~W%4IxBs)tY*5Ap07_|R9?!aGdjgqvenCKmm44K(zlG>RnT0rYs2=ftkGaRiY;R` z>`VQbY80xmEn8Y$vw7pXt<0dYJ6R%bv4~a4$r0DuvE*H*1w zw30QlO`Gtwv3&FNWk!0FUbFGpO>DE#BB@JCn|m!)>lUnASZ{Fa5q)j#ss^@>(FwMY zRk^Dc8=~jXqD}gzr_*_V5z6qFF48~iXG|#*JwMQ&uBoW5WW{V|F)K9)HwJmnVNn;j zneT`1m@Y-I_cN1(ePtpW3@C;{mXoL!!%Wisj-3@-^}3Q5Dz?^bt*gV%7|b*D>NRzB zYnZ{RA#l1&NI_&HbdV&}b9AIS&SkQic4@h9~MRlq` z1CpiYeJ>7(UFxZKDFp<$QQiqh9-GG- zLnC2}7cW_4#E4fbDIOV$SzKS4I!aYMKRC)ht2y2Pv>ae7^Yq~F&_Heby$6GfHcK^GO=^-yvy~ZdJ`{;|t(R+y0tWi&oCXFh22Rvci+7czW%O)w3}hIgkXVYMcSp z$ba>XvfoCa;I_N_1Q48<6jCfH!n zkUiBVcI`4S^b)gWxSk&?Y=CaKE2MpRP|nAY}YQ@`H?0Vp!;CkKm< zu?^q@PM-9*%=3O*hzt$%pJBf}Bn-op0+(nG=1O=fY{F){6r9-utaB?W<~j%P){m`g zD(dDs20$#3-V>j??DABH!#cOFVvVESVIgjGwmbX{iJ<_hu-GQr)El~_aEA5v;ad0n6|>YsdJ<4fyyuQ<%RbK7Zt(=(&r9^8}$#vqc# zRi>rX3i|~!=55nqkJ$kNJosZkSC~N^h#ZGhN$MVzAPUe>E3d{Z%RM;+n(f&lv}nEv zZ5cecMTE{T5~11XAvAl{@|c5l!q&=nYL`sYz6bv^SV9idKY%NT*Hs1=mTdycoU^+{Iv;r%MF;lJq~<%%gHromU_X!m;IPX@P}rlpcr>6>pshCwbr;M-vIWWN3s`@& zcv4Ma+Q>JBx%XpRH~~Cl@U>p>((q^!zW(-7Wx|)W6;o^qm-c4AaOtQg;T!4~C<{%2 z;teH@n$uN9LXeXrhZRYjNwD4Ja=|0vb-QQ*UEWbvk0-muz6 z$Ay3*``Fq1=gL=98sEn1w$lTuO7wu~ba>4EEJsOI?6-R5it^{?Goun<6>!xLZEY{L z^0SBRaWwnzh>+LBG*zU-gO&LERQwv}@)eS%;O>EuY;QxSZ7?n<<-;~T!CkBNVW6WsiAG6bA3E2OEwRfN@Fz`%KXiqE2`~!U(NRYI$5GAL;#8j5RxHDknx3}?2L-MjbU56TU`a+tnmal=$L*T}Zcn?HXZ z*vT@VEv#N#ZTx`F*u;h@Tjp#oZ>)NO86Sf{KDL}Va^!@w#mYV$_vX-gL##Ze&#jrU zjje&ESiNf1Y6q{jtYRzNHEtuu66!gtEJjh{t2b^O^?VD8lj=vUc zL${(HT(acy*v{a-qSb0UK~9S^$suT*6G0VkQpG#Tn7A^UHG&5wB~6^DS5$~vQO87) zA=3Eu;(DHj?Rso!4#tZ_Ew8JnGsOMAVGXmogeOh9*6M%JAKo!+Lk6r|lc3LQ zoS<-;02dhA6mz8!Xig}#%V|!zCg;`S-H~=pE%}(H$RKP`Y?gZqildW{GaIdH=FP}dHj?pqQU>jD|*VitrURb>VGwi*M ze}#T$R+Ynb;Frqp-R}$ab!TEL-^y0ido{26?l8T!V#C7K0ULOA<+=s6M&BQfeW1T} zl>IaF=!iG_u!kQuh7ODQ9+G=@?R*Gr_!*WOZVT5%k)8JxJ;e!Z;rtp%JaWR4SO^F@ z(~tmblHYUV^d4vcDz||bps|3&pnMq!nV<~fE4T8;`0M`#4wI11!yLpucH74D4GlOo zn%-`~Xyl0?xP#uU=q(!}V=T=l$i6SuS^DcFW8|=*xb1a7X_7Gs{n8|Z z=Oj{s1d@ICuz{C@p?z!{;mw6*Y-{x(#DK@~jq2)sf~Lh|04qotM<|oJgN%~*lwinq zf(>oXdTWLurdVy?WX{OTVndApesZw4C$?m7S+Sz-IWqEcEMI>3;g`<0thRUFYG^*p zJ}?MIQ8y0TDStPaz*1!}L2@j!*{sEL78yLmYCF@Zxq~JK&z}~8XqUa8WCp?r{X#%r z)%LUtgv;Bw*OVv-yad0*b}W2h!Sor^X4POjwv3Hu#krL)Ox`u2&jU+FyHo!*_XIOu zWwkTh)3W{$|CoyvvI#3Dt=_b6)VDc}rJF}r6s=siec|?{FBsL!mMyDg8`!q-YpU1R z+`(;ow>GS5eq`?9`L7=;eSY<}x`r1ftvAXx==-<-aorJ&yIiRD^<`_Ot|~TWmg!T9 z7ayuvIj=5z-lDaY>)+h+iuBs6?ISPxaYbzWb>Av}PvDaErTUqC5kG9m+*48=s-hdYr zjzGT`)O!%CAR(|h4!}zS!QrHD0q7-?hhth&Up8F*9s=rcn@ARlzO)|9Xc~A}E|&@6 z;fp5q6sRZT5@P`G0w~ zK?va5&P`xhN@Lb`DjVPpJwo%RA$sc$W?7rEcRyk9AH+ZT5B#6{?%kVSV~3%!V!sne zlZcHKNSe4%mMlfgjg1&46HXE!8boPGVdL@Xo8%Z2vdaU%$z@bSdo$D<;wrH8q|cm2 z%iuaEDd{5r1Z*1gMiyV^W$mc;uUxgca_!ZDWd*{Z&fxHeNsE|~OQKz(Kz|=(B-usJ zj9dEjIdKc`>HLhncjV{?M~A*M;tTLx&l5lW_+!RTqNlSTdvL@fPo?{Rs{=Sg_-uAtWNZ7t1z2&fdPf--7v;{~lN(!`WaY>`lVf#svob4O-vWNG9eEed_iByMwx}{!m@f7RR^YwJ`=$+z` zy96BeVFV~dvSs-y{ZszvXZ(>*cs6hh0a=NOqzxCa-~47Ac4koi-?zDJ7~UU$zbFh( zT7u}rv|6wa@*X64cHYNPUOl&Zt|8tMZxJOC8j$qlS-xCf<6h&g$C91TCosJ^FwO0E z681*(fHBCg!-?k5wpVfM~Q^(5c-9Y6oE&t$+F{ z<2}+pDQcXw{)yUIH8a?(rG_s|ye*xgdU`q&JwCnb$cWF%|Fm`c%57^mZ8G-l(^uEl zHL$g2WMU4RQBgUM9%bUhd#duA^7pV~O-*L!wYAUHvpTcg61I5h67up@vV|3k0VVc| zy+FCN;?^*};vYZ!nC|IetMGn>0FgPou0x|ozI0gQ#*JSz^_i*9&Z%R}jDG;5mIA2G zuA8~Ga&2Y9+!nXd;y$tA-FGiDUc*0NtWAz6w`V_l@2ug&&H1{_$S*VMONZRlv};-8 z(#EA579t>QYptzX(<$&Xn*u-6m(5$7{cLB`#I$ew!$i@ydr?jv!b~S4*)ViB%e>=G7hxwV?I@kuqpOT@ewReVNGve&^Cf z%9uSy*)MHDvQTn3(oNuc?B{d^r@~=%8)+=e#(CSB))t}}j-_V4(27WC zO&}6#cwe!KqrQU>ZitpI!@?o2#3%h<$;fZjXo)Y;u zpdUh5aDN06g+L>qe?lz8XjD%S&KibL4N!bGxpqxh&|*4s$)`&i@0TAu-aZtY#HG<5 zOqK0dz60DjZo!1HY%E3U;j#(X>ToaRuo_*$9w@IRPcJb9A~920%GD#Cd0 zX-03W?bmFJL{4xL%Ij?BvVXDg-dniJq3!#?O-K641maec>+nrpG=$N+25^wULv(~h zGDI1q6UcNa62|PBUEp(@C;<%V^54GAygfW`)h6afGLwW27cvgsB}5A$f^>p(Gma9( z$+s=yMMV+>lkaOwSpzyj5R=?fOjVhdI-SjpC}TxqF%Ql9R@UpT$PTn_mlhNUD*~gYm9*ayX(a&mI4R-B^ zgey8XcGLrtGgtTo0f=scVrSFUkU~rkIuH+eugY0cM`=2qgi6X{r4HX1-?&jZ@s*FHUshS=-vqF{vfu3 zDBjNgWbzrtM>3A2VY$Cs^hB;+`WC|%Bd)P$jeKGtvLkQbn|xxV!6)+xGT9X4m-K&W z@?4IeD@i?A_PLHwoRe}{rG&t0nz1wqT(R{2gdh{SQfM5l&%1gceuyW4prvbB3JCi0 zYa-mh)IyjksfA1rp3HP4$w*=HVyU!l4iPg&l)Z4W-v7RleV7O~0xFb4m0}wS@fZBa zk7@54g*zTd-#uABI(pCDtiM4TQffrMrgX5+U;fk414h#h$*)q&K|7Lq#Laq1%gz+j zR^Qifp_utGyZrevrqut=pU}7~kl2D@62Y6^M|6pgA3Q*?cVv2yKjm$UFbfn6#Uy&H zfKV3|!gx8>bI_2n;flm#bGH@(+j^WN+-dl=2=q+CxOlZ;;@{gh# zxFsuEKV#*z>MFQ(=YzS#JhYl;VXKy|T(Z1k%ZvtOLLGhe)uTsW&3(whie}ANFwI@% zrnY<@+O1+Mmo5iWZrxJRFr$%~O>St8}Qk&{sZ{? zUoLN$)iA5Jl$Eg5$)(w2v&UxK$HWyziD#>oo!+^*Wnas_58h||3U+Ar8{_tj+jCz- zDl@VX*&}zHlmSGZB&y^myqE1YnSNmVB9Q-J*k1EvVfIKv_DfF~wlB7A+F8GwHLwkf z);*2W>)t^;_6uqo^Pb%&1P%q7VY!hkrrpqIfMgq@@h zi`L{~#VW?RI<i& zJcKVo&J<$)D{l}D;;&Bl4!jQt!G-Su2e2fa>d&tg*NXmhiZ>#yw@fser^s9a3k*6n1d3GbF8Mr?HMm37xzoAn{Ay$ z($&)QN{%*%?RLtkvf3b9t+v~x5+TYn4I>yOAA|hzCrrq9qWAn=6Pg^}QI@7%yPBN5 zyEH$6ygn@e zSU3Z2Bnz6HfYe8}X!rIP?ZP%n^vDw7j~J7!==Zp|<@3m04~X7ESL!3k{+Nm+ip4cG|{#b zaNXK=Q!IjBzyJf1|C*+Bx5tArEO|+!=2VzeLi$rGv;pK?plKx1G@T3bp3>nU=k9VB zboEA<7f>gdLGwd;Q`(i6EFrSg>)11mY|4g7xF!jtz?U@%koE^nh z`36OMH}egO_G0V$^7>g0AGtc+K5Q0VeIC}C<~WH2azEfC^+zc+I$lYFm(}D95Dd

    r5p_BLAu#&L59BB`)Vtc`5zf(^6auU$X0O^jSUb@kM3 z^S0M+t6kk#xxI2*0l$`QHMVZrO7aZBS5m=^QG6eBR<575db-pjBn8cmPk1vSt7a~l zGk?L1c~hgs$XPQD+nJfON){9`1NQw{>qfSIb?thCo#p!)B|ye25k+NNw>lA<-MWWg zQ@g!(`@F`fwT5rJXu*q5qzBz?)qw?&Kn!6WtGH{Lh!9wWyDx)t_b+^P-YZ7TM7{2* zWlyommq6TrGsy7*Ypi;HfwAicy{xvRrliJLF0%#j8XiPQuy|<(KJ3Yc84WXDWDN}s z8~AJd7x*o~f*>X6Ilt$t&E;+T9gmlM8v$7;QlJxI(0dK^0h0Pc$}Zb$QgirC3cO2U zS`il87qUP^W%retk*9!J zo^;w}jR6f&#J-h~AEkeg2!r(p-AzUY6m)nhy?Qh-#G4#3FvK?K$Thlrb85vxMG= z;=?Yy5%q*8BGwA$A8Lo1wclB7jh+Z<+qo90wt)=dP7iJRjb6k}2I7r~h(Yu<5ZO=| zTMICXjAmjIq-PE~TILp!vx46H0_Raw5NJ$#TI8yOxhQ!8?75W>_Snwyzd^>`9b!92 zQJA%?eo6gequTP2l9%bJF^^E?&p23?D?W9CaO6b4MC1sq--=I4kqC1@r z%sq+M29Y+lPc-QT1?>Jk1r0N{RbXD(wiOK<4BpI}^~MD+RK3VJ8M{1~_VtL^tubO^ zOzr=}4?1Fd)mRYUs166I1>T!(fh2nU^W-yWU*Z=TQquY-<1IH4sh|1$%QL6HJe~Sw z%9;KKkK~bh|1-&7rky^0`l~NLKVyhU-dj)ZpO%ataX6j!Rr2Si{RmEwjspm2pww1T zMxdUMJ+!h}A|E}$VjN>7DLV$zM3h2@W4R>h|Dp?CYQQtZn0N;-ay;XeF&&SIrOc||#bpJM7zo%mglNAX)A)9|Yw zi|`{2Tkz8gZ{Vj9&f-T5uEviN#NY=92IJ=frfVzkI{>e1?*{b@8Vdacg^%afs&_Y*HV_&uY8 zQKDKAJ@{n66RL-j|Gjha@+CG~=y|KU^WdBS{4)4{si7Q-cYToRX2sMHkhOMsIH#0b zp;FFULgAYyAO*3ehmuyIH<1AoLq#CJgN}i)LoW>;$y$g2x)GGeMK9#|)d_A8MMI@w z;{{n+g6s?hAd(Y7Mh@^2q8*60;t9-~1>Tk<6(sE=`D`E(sH0Hw!ysM|8pwc`lYm;X z2Z4Jf(gmnTxFS@#p?uT3>JRU9Q_GlVh2KeaUc30+Q=w8Pi4hpz2!DGl64|IUA{eGOFE}CI4>~1l(-?NY%6Mnnl%1jI&Xfi&RCO)$xL4&EjV-j#Pyb zBq1-HlV#2vIdPofcI)%1<{_e^5=WD2}{@nFCs{2o`y|}Oc f&0~6oE_2;^JyG_5=y&0iW9ggm%ZFWmXYv067aL~9 literal 0 HcmV?d00001 diff --git a/3rdparty/sabre/dav/lib/DAV/Browser/assets/openiconic/open-iconic.svg b/3rdparty/sabre/dav/lib/DAV/Browser/assets/openiconic/open-iconic.svg new file mode 100644 index 00000000..0792c003 --- /dev/null +++ b/3rdparty/sabre/dav/lib/DAV/Browser/assets/openiconic/open-iconic.svg @@ -0,0 +1,543 @@ + + + + + +Created by FontForge 20120731 at Wed Apr 30 22:56:47 2014 + By P.J. Onori +Created by P.J. Onori with FontForge 2.0 (http://fontforge.sf.net) + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/3rdparty/sabre/dav/lib/DAV/Browser/assets/openiconic/open-iconic.ttf b/3rdparty/sabre/dav/lib/DAV/Browser/assets/openiconic/open-iconic.ttf new file mode 100644 index 0000000000000000000000000000000000000000..0f94acd1ebc42d7ae7cf1ada87d4301d73d64e30 GIT binary patch literal 25568 zcmdsfd3;<~eeXTz&Z3b<(u`))Hqsr9G^1rCOS8+eWm~pv*-mU(UNXv}XrwD|iEY*K z5{v^W0RwIl6q~9Ep~TM#u7Na+Q7ib=&q0OUx4y8cdmXBXFe&64@ zcSa*;A^m*b`{zlTImNjs)5{{a- zOh^)|MtOX4UwT@qeDl*N--*w_0er$eGlK7k?ZHVulN~>&mw?~q$yNu`Y-Ka3#Yru-G524(=d*7iu zKlcTx7Uf|{l26=zU^4wu^hwM${A0}j&3)-Rr&-$aza?qp4B9LE)BC1=Il+&i`~v3q z==6a*4*iQQl9QzED<#P=Iel-BM6OYc3gOYODQVi-n)zor9|M!W+^XugeN5hwwdxRLrC|QbdXR z)2Cj4whSd|RWBJN1*rkQD8KRg)vNfHu3lBN=Xk_S3j;Ea3i=fP6wg*%|NmE>6K+Vq z;4VPUEJ+c2#2$>;{k1=M<_llouKBlW+0Qh{$OHa^7b1lPtBjl_db{M7>Hv(e$J`P(dyIitw8iamn*A+~E*3QUDwvtg z>`f2lp1V2oeD2#Oo|(43o;y1|_DAh(BRkad=mekZ``M-N^vTzo0_Sf#{Do+DGI!Q~ zbVvQxy(QXxW0!F=agQ88n=%&3s@eaX$HH>f+(icRBW{$eppC#AoS@C51Z*vKzlHhj zm)LceF6F*r3AHD37pXsu@ZMus7~x)Fj2W#*?&9K6vf1|=LmUaac8rA) zvRSO`eb&I}wnU2iqsKybZ5!yH$Hx@GiiufUG|bOkG9_-2%Ad-u)=|{UXROj5oJ1Co9Z3M>}gPtV^`9aB-OzrkoRF5&##?Nk@lY z*2b2I-D(Te+T`(}PHRJJrbUS@U>t$pd=6_b+%JI8FXKZgi@5x3Mpd(_N(t_mxMZLS z=Rsknfg<`CO~CP#R4P@0&If>f9+S)IF$2HjEpD4R7>@>xg(`va!FVhhJUW<44IW*= z_a3EO?vcUz`oTW7y{``$o*LTmk>fjt4u5gWXA?(rPahuI@!Qmzq54o;{ZQY?_N9H> zM*4>8+d}mkPP3T;PKVu2Q&S-7aEBB2p!vvSNARb{Z;sR2MJe}(4oB{7>@pDLVwR}vc~k(gfL`dP(;ckK zE!9XZQV)(i?r~Rna3FT4$?t0l#O(f9#1Z$HeFh?>PIoxIoJE_9^6b7U8~Z!Y-Kw)< z{udQa_3ld*)iX@V{cF3|+wNtRB9RqV!E(8JHFv7Q>8xP;RaRsF$m3bBOLch{e_>r5 zpFACR;GBJ75OJ5^<;03&_Nd41vs?I=Evjmn-(dmA;d7>2`I}7srhw7!GMmi4CG6%} zhio*m%e0&D{nbq){W+3x_u7Ij1j@mT+3M@|g9{l>rvHQP7R z2b>#u_&{vSme_%ZN9Lbx>+2Ik&=(Jgacne_BVq(Y@W;%-L@?n=xDsYpVm8die)hB6 z+3?1Vv$GpFf_u-(%9Ya(UAdxH;4Xs%>3D{*T#^^K0uD*bGB+>*mKf029u4??E+8^e z*{-GR^2m|Us?Dc%&%cI6wXu*&i8|Y#O7+vC*Crb8+_3wUibpeTeY6##o~h$0TDqV= zg0_IeF?)o8R)h$Xb=pESTBA`!Tfp-iK6}E0ncXhCugTTn|d z(ToBh zO(z6oNoXe}(8dTTHyLDuk@lo*)A>h&g3MBxg+%RZJjLglu*NsrYbsf3?#SlEfrkVBJJ%;@9^l~BzvQog2Jga7qwz?%ktMoV zKMOZ7li8C%1rd)*22g9{#A)_u7pG|nS_GDx=`c)Yx6|yEo#sH$OvD8+TEPOb4kogF zJe~-4F-x7J!g`Ol-qgH)$NFYtef>RUr7o|n><*_9I052g+EnTGx@?uJnpdvyx*YW~ zWBvVsfl8ap>#p2n3LB}FanR^IWVNxcS1INJw>NctGI4#X!98I1y2@)!Nq;4P*`%}@ zyy#WtU)$2;|D?faG8%7ic|CU7CXX15Gl8bSkiQI5c@3>flh@^JsPrd|q*ZYG;9TaU z63`05ZXA3Evm0CNL06O6n4lkX+CF} zf7Zgczj!)ly-Tin(h;;(IGD}yw1xB)Oh9*@WOawQMU9dlu=mkSI+p-GGu)pDeZUlr zdY)*%&Np&a65myk$O>H{i(FwLZ3Ywp^$ z$Xz&9yN`ifPV-c5I{!=(G7+9JJ0n$25It~UL6`H->=yl5Zkl>78q>mjY=+H{dPL7O zKGTT#bO_kNg$-eMBlCqTAw=MC1GolDn8tu{iG>{2tfzHg!-i~mtuesn+~zH^t1Nf6 z)FoHM%S&gOk+~Y!77Z5bMZyDBioI;eJzrW`S=x5Kt%fz^wiXD9w>?ioNWL=R+{J0z z{bs1bK_7%u!8xGv0w#077qAf=uh`$9T#i0AY}R9vKyvsJmT-IeIsg58H|KuTKGd{f zv}W+W>(>YGKg8$m8$s2evi`>Eb@vTzM3n}kuz)*wpTm>zSlH~9E3*VZo)Vb_IY;C1 z#3Masj~SrWTFMHgXeQ1=$N@B#g@OB{Iya5SbetlSKhMIP7G@5bgLE`B3LOrmI!#q@ z_sk@179?3T_7qeqtWX_#oDCFoqNG;FB)}A1wlR}LlJB3u!m&w+B|&puvAQxeJDdMo z7%5+aM>LcePtXE8NhQDsVx;);fqW7ZHAX-PhyWLOPc+VO`WwqCau+MgHs=0iBfE@3 zMcMoje9H=GD5_BtcCE{1_3AQyvW$#-MRJfCvw{lfNvJrPfc^fL!&05KIW zC&LKHXq>&c`&37-<#|X>xnI>E8L@w6gKS_X?()uNv$IZFfw9B3y~pJZljIfdiHscC zaS*aHcq%mj3tcq7Q)*l^f6(mDPwjym4gqvI^Xqfom(9K|X1wuA7FP$vB0pC)tIyMa z1%emgia7`uHQBzZXa*rzi>+ycruuXJBS&Z(d`=s!KLx_H<6sf~lt~T1=CB9$46slD z#+ZlK!f01MeEn?~+QQ+QQ>>c+v@kB@F?Mjw>JKM(pUN(tLY%R-Mj1L4^amDU&{oYJ zf3PLuG5ZsA;{}TV_2wx-!ZmFbnlb$>f@Py7;Xx$um>Zd^5|=bs$^1)jzY0+ z3(cx!HbT}DQY^$-jbh7+Cgm^gV-|gMEnT(;X|vD;gal0ei73!si|1L?2U{hr6RdPv zlbf|(1fb$Rg|!(y!r-Ry5G+lfi4+i;hh%c_ebCh)cw?`$Tv{t_#to!%hj}or$yU%n z1Wy2l3QQGb0OC|sz?evaP+v(uAs%hk80d25gD5P7P=HS*5cY@ZvJ@A-0;b^#vpd?o zz-M5<6<{@V+z@dL2%_)OL+fru1MLQQ+rI)shiD(Bns&N(fXpROT7B-E%M60Gl9G9TiIBXb_HPo0t&;L$e9u3wBL<6G?kIoi{`N(sc8LE`inXUzSz8{RYuaffu$t>f zH7J8hRj4n91D%xzG(Gyx)5^oa9E~UXIB5k@C-g#E?ya3UxvqF|j+!Z&~ zK&Kw*Gu>z-z6Nkzd$as#vR^^a4K?w~$Pmuq@&-z`jp{Ftlwl zuD<<-(77QI!`x5cTIeU|DT`6F{%N}=wb#ziKvEGjb)Vc)#Pl`eHXAEq z?}DU;UN5TA=hen3)44BcvaqB1qb;6T@ zV#L?`SXSNGqlDBF`<2ql>QcsfmaUI&TvV64D(XOI-E|Rl%v4z#E^CliSvN(((e9FhPCdMpP~~{0kXbaZ7z?A|rjxbFkL4D!q0DiNLJJoe zTDusAwj(mMrmL5P{k%$Q!kRn{2EiR-(SA7rWeUV!vse-KuuxNZ>*!zt_>&kMZ7sjG z-Fsw&B$zf>+M3Ec4jwqQoG@wmsRIW)%5xjMu8||SPkn8!qI{4FKHr0@e&D}u%EmX|Pykn@V=m}_MpOz35KH&&WsdToa7@cc_p^;S>{7t0K_M7rjMlei%7dmCKb3xRKv(w?p>jrz8Ef$k!^NtgP>ppq& zI=fxAwsj3%uihi<5ZThIdatXYLa-!0=Zmde8ME80ZM!zFg4osAXma=W4u+dl%^+c_ zXmEM!t4al{)935twHjbk+$On-=1Sb&WwbU_nw3JYe8#qb$p2 zcjeeX$4A>kp;fn3d0jP?{kc=t+^^Wt6*kw$a^Jd}E#q^{e#@$kj?nSYz*t4K%j;Z~ zJ8fe&cGrpuccu47xtBi9mgVm-*`bJUEO3lsE}>mJy3WPzYlTgeNKl2r(`5h=GDMl6 zNPZvXvn6$dv5t=At2)bSMAp~S+oP~*kzdi0yC}-CVz4Dw?oM{yvTkJU&0Sidb$I2# zs)0?pd9Ao}La)$isiezg&^KWe^E*JC%pPCN4}2!-Lo^{Tb${n2?Op@AU~d#i$0$n2C z35#{AzvueOBe0(LRbJmiYA}j@{V7}3P)IMmMh}W*8!j@q@?r}aPEBT0 zlL<)|H=AHv4m6qFFeKy8Ox!CGhZVVtC;H`JfQ;G!Byu7E4cX%kdm3a|lk>I}HZ#25 z?Jjp)I;xBYxvbJ=a#Z^&+MOm=RdQW*o z-20-x*C_XyWQ)17)Zr=^XHJA1m41t%!ec9w%_f6=^*Zh^ZHUXJa`(PQ_v)q2YO}Mo zp)%xaEvu@iE_HcK#UUJOgi;YPg##oVv6wH^!JgrV*4QB}ks?w8GT91^9_IN2rLYtg_{9e)=lKYT zE%~gGXn*frSj2G(tfwdgK|mmZf;;fe^)KT6wA#EJP@G?o2@ot&kPnLUxUTd=3@ra$ ztXJme74!_8!e_*X(goR2^B`+-B9S(F0un$;0f&V=T@JspkEDIFUuzngJ!%&oEzM4! zx0*TltOGRRJdpyPa(Nx|dmX$_*Jd2Kr>LH7bdWzQ_qxOJl&-xLXa>$ZBB%|_!ov0k zIXmWap@fMcRSO#+$OtL<`4TxSJ0NpG4=49uSmcoAE}{nJa6N|<=jL_I!Ud%w%}r}* zzRYG^uzvNmN|}TK*zA< zv8{(86`!R%D%9g*9-EMRV2*@D4U#AfA7%>+2u^vVFZY$)SNh2DdSnEj$NF+#VmBbL zuy~E2BxG3lL0c~ceJ*O|7Ur=NLaKK?tz|;bZm0gphsAE1{ z1I$3&Oi^>W*F>-t(E4`ga<|g z13`}r4X9^ZO3TPWoV%#GfZ2@ZFwRXcF7K_XX>cJ}CF}53mQ}!yW-x*-fheKQ<>gOn zuHjkD;j0z0#f7Bot%7@vd7ai;(`6Cl!eF4+@S*|y%m_Js1MLsiH5k-d5NRYtb#0Sy zX-0zq_{a-ICL})r3e+M12HtQD4k(&U5N@IgVtg+Dw{S-6<4x{F`_iSem38n@&o(qP z?x^ditao{i@y|0fwBgjuyO>?8Qed%L;v(;Xgw_>(o zyVuL>LMYT%Vvz6K;4N`tgyQN(o1?mY=@*yYO9~g^no({*yg`8M(iBUj?J$VN0?Wt- z|NnRj|;nHOXEidE;uo}7q&=K|IWOS7rcV%kP#nlDxzTTo<(elXaZ*c?kO!x=l8&CNbD9Sx6rl5 z(uqi7`2rUtic=9Q*L@VA4dkaF5e4aMh!}*d_?vA@;UhURLd0Q49jRK{uBz=iFQ9lG zyVxyy5M{}88LcmyS-Nd07|W3n5EK|@fdB3A+7lkW!~O}B*hyj9zS;Qzw*2Ai#j!-iPZ2;YjZBv6E5iiW}F!?P5>i5-zh zc;!k~4dVj|G(V1j1#C3%3dJx6T?ii&!w@ZY1iNAUZ@FbYxJTa$D z*qw<&S8zD?p}gl=6U>Feav^LOOpCUVmCW(v%t!VsinZge!>5zSuM<+5P8Y#bf#n|1 zi!LEw(MDXp7Laf*PMgFRLO9Zzj+37DMGRz<^W zS(*Pwxx_60@`|$oX6y5+m)()-58c()3e(Ec4fXxxmdeLC;23c}B>M}y0L71z7^nq} z(p|y!3FozTfClWvnh>BFh!>t=g#7IUK=22FmeOLp6gE#pjQH@GA=DLb51iNRp90nz z{)%?GWxCLgCL|lF)=t;;U~3ij3}%NQOLlj%M3Xg@j!mqabV6a172`$hWDZ1RY~Id| zmJ55pohJBe8!YTy5_bU_DXb=d4^Y={j|d=XHa@bQkmyW0yj&!UEt13*Nl|m^OW<8- zPHVjSd7Qxuz%K}&g4209(Y56~8aEdl{(1y1PCjSoYcn905D|pjxAdI7lR0$1%Q??oBPta-gT|7e(c<(2l?EJ zRp&0%eCTtR&VB4vaju$8Ku{k)L_LroB=lB^-xVaMp`VSBMFzx1{NYPbo_v8)Z5)9E zr0_wTBlQDJJ!UX36zu&5n3~9a?5WHB`zQ3A!I`@AOoubC_N=bX}4lTBVt?HzXogIhgThxmBp0|T@(W^8li7t(C>V7Ru<3zG2u)a={o6?2(xq3L zcMUb$R@w&p0_KWp^A)cb>^pZU%p%cKQRkk)yN2I9GtAt*c*g@`%jM>wUCp*tw!sxm zC2m{Ee>RMG8ye)?uc9-NXw=_x2bE<{u;TzJ923xaVYwnHv4@VGb+kVVc{YZ>aw{ey^k z;J??@mJLnKKfCq^Yn$3~);3uIV%u6nAK8&okvn|uTlN)!a|1*DvSZse%AO0r##6Lz z5pR@_7n6%7W6P=gVaw>RMu!i){l)=VIsMA%)34+nfns}Ti>hupL|dm#iCAQcIRqXp ziZukW$Oq&(*eeM94?Cd2{=N_&WF@cI!fDJdizs8w$1cVc_(Zl+pa9O?&f)-xFkS~) zi^oisO*YnK%l&AIwch>s%MaX@X13g}fBA8A8G7dKU~RkhR(<`R2VUO7Y~R0hvF*ca zKP>R*^MWq=u}%<&LKM5WjdpX;u8|BahVEt|?2{P-rVhbrlaSAFBemFdJz_oE*QRMb z2-AjOzkm$S70OFpTejZU7 zmYe3!-((R*3Z1d!es0NTEv(jpl|u&*gOV>SZ%cy(gt!y!fZk;A=pPD6fV^uJW;p1s zZ^ zcDw?FS2FvpGQXkm1xux2ymi7z`5%l}wH^gsU z;^i(wm8q)CVe?pPYT~zcH8{(B?$+wInx>ZOPV45Lf%!Gy(hD@L5!S|a(oFzgA?6G> z6>I2%(ru>22z{@g$qS78ovd81X?aH*EeLK!k9}iTows~==Ww~VF4`HRc(_`ftAnLG zpDoI>FX-`p^dKLC7a1;Zk6L?Htmv@@mJW-MKP&NiVOz`+U!{_!=A&d-RMK$o^8)vR zCn%Qa6qWmLgs_SCy(s=eA5Z67u&k5cli?!65eSL% zTyC2Fv#G^&G8DK--Xxw@)V8W zA$k_m8G46}ywzcYy9(EeH~SGBNr5a#ajnj>wjX{pdnWgb$6jH9>{stXY=f+P^oO}0 zoqdcsZAUb*Y5q@dwv0VhjAyU0(AJ7SFBlFfjFSV^Wsqy&tiZCPPayOO&or2nFJ%Ax6@mCH|WU=_*SYu5eAa0lY}ER}aOG`{im zElXmtC0ky4y|Lkr`J4IXV^5vl(A=n)1^*-Z(p^9IOr;n4j`_O&5W*lgcgkw0P;7A<+t$7=kVmUJYJB&iNKLcYR#?Jf8UCi3}c-IBo&7UFJmBf4N`Qh>>l3(AoQJEhThXHdQjB?k8U}`}uc(8O^K_>xz>mCEd zsNm&0SWV&SPIx$6*$?4nk}QZj3gi^D>=D%FD~fpuz44FmiADN|7Fda!K_SHSBGBxR ziZ?Pg5$@*SbJ`@T5X@SH^YFYtDhJ2F+ginrQu3qvWhLC#HcM{NY1j#7+xqaX2y6)S zLM8b{d7!UN+ncQ0WI!vm7?ZH4N(R>Kk_IQZOSHD8!k0VS-r(40%Z(FIWmAjuks3#%*JVAkVSOW;sdv8Ch_~OTwRvEV zA)8Sn>8{WS{4RR0Lx>_|Rt$<*Cr$N;`HhmMhC?0&a;agrzpG7{4sBy@QbQiU8==9u zA3H1C8!9a?-hy*9m6VLx$oRN>NvLV%a#Ba``DT@D%{}6%Y-q2v%og|#)*xa@gxwU% zk|3nQS}I}%@jejfr)DP=Mp9Tv3vdD*tJK~<0CRov(Pp$t4J}UmJ(Bg zTtaI?EXJa=^eIc0Pv9%}I(>=xvsf2sk>#mU?HlWP9`CK=qgrl)KB=ly!rPBGAuWah zy?>v4rnx0qbD#i!iVV(>b_DE zyu$Q~J8?^RL}8@!l4}&k|BI>+-?UH&%~m4xX*1*{oE%-nAe2R43}HxQwj>_}tXhar z@(0Y&hK0qWlCez#11E^Bwge_V`q2r0Yjy3a?b}z$)gHI3bhP_wXj>%il~$j9_0ejJ zYO>#U?`?LID$ACruPJVkbsQu+y_vW|erDslOmfYb3u6cwO85I0M$p%$uRz;lfiIF> z5gHJKRES%&oWUF?Ll>YAi;ckf7?9}=P?-e`48Q{sp5NiXd#_s@cpH@@3A(s6CTI@n zx+PdU^!+OM^GKNt8Y!TbGV#1={ZH7LFURhC=84b7*s+6?v5nhr*cczbPDt>4Zr{jF zW2qPWw=Y?;y&skw!7w$NfS5@i#;=u{uwo4AiU=|Wb&edbN7@;>hI*^F;+GjlGKp?~8n^erjvr-tKSJ?Hp;oS1aA8mH3>NzH1w7f{2AU z2hyh^4*bdx3B@m7-4MSK^`8R&q~f0(hzKR&U(8*EQAIC<4dLd2>Ja?%ex)GY`}g)N z37G&IDfy)F|8*c|gFhggCKeH%rV`^W-P5$oU=*IFZxW>+Uumn7V}eKvEFiCw0Zlcp z(=OReUZ*`^3Vqcj^6JsuU1+7_#lpHeKKQ!mMXwLA9^r}Ad|>alnm7?=f;T>2|Nft| zTPv6SyHtjohM%JU|8)jXd{}tr$b-x2DSpsLR<1rQ4apy;qTHmN9Od<&pD{)JPb!Ha zI*CSozYpZT_k3RbjPk{0((gaWxcNTC?elf-*Pq%K_bG0B?OKYT{lCn=+q(00`Sb(N zkJBq|`(Ar3Zu3F%ziXWLS|`=N;~CZ{>h$zo+W^0c`xWPl*9%&?Xut2(uDB27@xI2Q zepL5C`(8WN|IIS3VG*p0&l7q5rwaV13ij4b(q8F=bWZxF^mA5^0Ha}ckey;rvTv{- zvtO_ruj0)-4xinF{AvEC%;in;w0u&2MgEPU+Av~J4W|re4bK^VYwR)}G@dhcnZ`^H zn!asznw!ks=6>^a=Fgg+Fu!Vk)BGPLr6u(x%SzOe&zF4LQfgUdx!H2RC1d$bskJmv zy0Y}f(r=djq|8>&?~^)=arnUR~Z*zODSu^2f_xD8E?#iwZ+U zYsGlQ$%@A+o~!tA#cwLBD#t6&Rz6#K#pbkyY#VI%+a9-l*S^VqyZyUWB~@KjYSsCw zpHy9OR5`jGBaTy!Z#(|kS>kMU4ml4wA9H@g6?J{t-Q*r|pK)j0ueyKfsq(}L;qRHPtnNnj345)O@by+qKoT-L;2mAFO?`_Lp@^ z-O{>|x)XIz)LpDM)NiZ*oVUt*o%fLUe(z)6XT5JW)HiHuINmVZ@a=|QG@2UQ;itH_ z@topR)+po3rK*;-^A-qCkfSaMDEGiB=n>zt!{PG5=qBUY1($ z+Jn}{fS{Kndfq5iurKI&{Femw5;!R{kX3{*O3O27>3^f=IW+zMq3319g#Aj-8>A{W zq34ZKJ-b`anYtfRN21&(4IrLPpLi1QYF&e8SM;r_n_H3N0bUJC20p{IS6>{ z5sOzaGC@&;q_kt|;2nDo>{oiay0Li)O{UR&KNhiQZwIEQ_IKttsAHsb^&a{^sPFRg z52#}jXPn8*fdAfMf`zAqSy(A6lh#OsfY3T=NLtUVtQ_v}N@hbeNfmQ2Cv!13Y$?^O zhSjn+0Rz6m24FoV5`|0Hptepb!>>OXB+T86R%^}vrTL>sN)tk!nU$)Y&+Y* zZeS@k%5G#gv76Z~Y>eH?##x$8ut}z}DYldCV!PQMwwK+;ZfE=0es+LOv)==Ky@MTM zhuIN!l-_pUF zX*>Yxe42fh{UMuWf5aYS53$d&huP=ZAG0s8N7xzm-`H99D0_^3k)30I!p^fVNoUxX z*;m-(>`&Pf?5ixpE`YlK8T%T0iapK#oIS(-f<4RrlFhNdV$ZSX*$eEi*^BJ!p#Lwi zZ?bQ(Ec+YwZT21ZUG_42g?*2`%D&Hj!2UbC$o`i7ko^c;;_sw)0+si4b3|YFgn3Vd z_e6P5jQ7NOPlER(c~39zN%Niw-ZRO2RNgbidv@}0HxKu4c#pyn9***GjECbqoZ#Uk z5BKtLnujNNc#?-z9-iXiojlUbBR%k4@<@b7qC67gkvNYecqGXqy*!fUkqI7|M;1dmSgsLG>LJi3#|x_PXJ$HF`o zfj^YTVmub-u>_ALd90Vm(mXc7W0O3l^4JuQ?d0)p9`E7tFpo!gJj&xS9*^VSIq`Us z$9s7^&Epe1KFQ-Mk5BRVPM+xIi5{K^^F)LvqC64fi8xOrcp}LYy*!cTi3y&VTU4^M`9GQyKlo{aHioF@}JndHe{o=o%P1W!)#q{@?1Jh_wicJtmI-W%q< z5#Af+y)oV!=e-Huo8-N{yf@8zCwT89?^Sv46z|>1)7?DX!_#4&j_`Dpr(--F=jjAb zCwaP;r_($=!PApGt@89vKGDr5diX?`Pek}cluyL?M4V3~_(YOV^zw-`pP1kilYBzu z6H|O*C!g%*lRbPg%qJs!GRh}od@{}_6MQnsCwuv1nomyf$w@w`^2sSaxs$8iTL3* zyfBOs!(asDHDHzAllExB@ ztA|q=qkkY{@(GZZ$t|hz z;kxw5Xv#l|XDhd+@Su)XuTRyPF@!Sa)g4cQ?5-Xk=*$>V^bh!zjN~6kXYQWBWEn$y zXT}s#XhnQ*GQ$QZ#@C2Nn6*l&!+$|Cw2Ui*C;e#xn%JXMM{vj}b(k$bT}E#4r`Kqm zOF|cn#=#6ruj$NKLKsg`GM3dF2n5LZ2SzicR2o646s68gX-Ij-AWhKMBqqv~tsYlS zjw_imthX~$7TUNib-~Qmj5cS=r~G$zW~`x&TT&ahYL#_9)H_6ddFX;J1lkik(5oCI7kt)<=PN4I>wPJ2#Wc#C*C zifydJ;@6D>ehVPG0857-TVI`#`k!Qs32;|nEylrZDJf(14=Cdp_o?!72J7a)z{&9o z<;IT8zK*&kY`zi=D>^zew$KGesXcUoQ(6_eAXDlHT`*AU1XL+?g)W#Vb%!pPDfNUd zlu%k7x?rKSCX^}Z_<(cQV(uFBsl(iq)?;o;y_lQQ2Fy)qBj%=5!Q7PkFgK-5n43~R z=BBhcq%0Ho(-Oj#%g2?~0O&X!KpJVLx!u>934}5&9hnwjUJyvO4!Ds=J%2jsS59tE zU0aD0?##3n;K|&XV0(tSx&`>!inqJ?QF};QZLH*QIOKFUh&uL(l2rr)2u z(9T>~RS0{)3i7SSfk5eGXQngM3Wx^IQdDmSOVZl?iQs>Sx0van_hqN<|_K;z?%Uuk?UYMmjgt>ux_!CVsrooP>lU;sAS#}a*^(;JAy0kGe1|k%L9ZF`L z?$OFEsXyb2tknG({?k|O=m6d95>N=thZ+5Af!M2orMN{>heoFO>T%VdkyocxTwcC9 zU5EVmC~g9pr74j-O3g$2d^!@mkVVeSFA1n(O`VyTKI7^AAF$3uRli?+WO z^l8~>*M$Ic0<#l^Y5;$sI$5X|t@B+MwdoCIx;yeu77e;IliN(>`o&@s0V`2ci zD-*!{eF7qcFL?lFfD3-0Y8Nn9TjMg^Mo_Z?V&%8^yD?|oe+^j(I&@c;`IB|NqJ#5| z>ML1JTh4>7pLXdZ_|ii3)fP6k0vmH_H|9x+E{CHl6T$^q`5yJFKnIx9k?BO6fly`% zlGOy8K|oJg3o@RE#u^X?+?dq>-eBlSNm_=?T4Wey)`gyAqGkvgQL~=fEXSh_)P^z} zsSRbWqc)Vep4#*xvx(YJW;3;+%rLc~%ob|Xhs+4Iq0CllLz!*VhBDi!O+PX_s10Rq zpf;3AQ5(vPQk$j7+(>OGa}%|p%+1t>GPi^>;R2Y)sE~=H`c{!kAU7_s2GvOv(xFVG z(0YOjqV=T6QEOG?sOeNF6D>5|Nd?h#m&j4m-6BU#_k=RBLesre5KV6rIcj>l$WhaM zp^i++R7P$dzLUszCn)d6XQb>17%{tl^e;+aKAu!SCD&wR|8%BeP*L!>V#~&iY3q%t zOtdc3Iyw%KYdffye$6K`+`k6$k*WR3(wzP^`lO-NGB~p4f{CrU(9AxyC6!tEsni9N cyavdL5`+vYLY% literal 0 HcmV?d00001 diff --git a/3rdparty/sabre/dav/lib/DAV/Browser/assets/openiconic/open-iconic.woff b/3rdparty/sabre/dav/lib/DAV/Browser/assets/openiconic/open-iconic.woff new file mode 100644 index 0000000000000000000000000000000000000000..793176af47c13605a36275252d9ce11fda817f33 GIT binary patch literal 12404 zcmY*1BH0K$K@cJKf3|Jwh5k&sZ62LM2rzE#|B;M>TJA}XpfvV3cD-(2Dw zhHQ|^#wK<~j^A3+Hy8P?MF2<#;U+&_iN4#b-x~P;fD7_wVdV0kZ}Xc&|A((H0GNfX zr}?*b002;C0su0Wg*lh~mS#q#006cBw};^y93V=Tn3mt-w^sYj3BN%G83hhvY3J(k zt<8RO6951#G$RLqYwKY0?W1A)9&`9V%uIEO*%^6!AD8z1+eh>tK-d83_C|JQ0015S zx5pX)0QI{v2K91uaB&3y=)=DG#rJ8OTq3i6dSsax8XB4c0$H0Qr-M%JFB6XjECGs) zP!RyYe|<*(Mz{h3k^=(5gU0*=0`dVOtQ<_GtXWVIEVV4Gy#av0G323eB{~O29(A?h;2T}qOxfbLOyUzy`5`t(7G+5{}$ouOH zOgi-I3nML`1_yf$+fWWtf`X`T&>t2XCzKxm!1ILi{JyI1`TgF5e9tMUM^>^ok}KX) zdd*c7+kTzdM;#mP#;sI}scg#S>g`Fc3Ar;Wlaui%FcDn17oEpWM5`B$%xEFFzF4-y zSKcN|z;5j+IZLRa(Q!RVmCLsh=qa0OHhm5w>+O@Wl|>KN;d{xOzm-zGLpuV6bp3Xf z9WQ|h!f|VPm7V_f)v@#Z_Nj+;eCFDw*K=fwb&dK%RFJ6pFTzZO{fa{)5IVWQL5a@k#+fX31y>Ua=cf4lEkv0vPm@W7=*U~*SM(g07(2~c2$bhvoMK2UBkQzA{ zbjzp5Omajl3^|r?O73!0X^(wGm%fjwQN28tpkH+88bBSO2@$IN~hKpxxK8HT*tlpt&7vGy8bRnU)BkZ=gqzNZu(0L7H;j? z%V=f9ey2B3f?mC*f8V+5i;Mm`U|v{runCevRCRDWu+&54RnI{TF>-8s7FpVRPkgEG~VlMdSGX~3XgpT zJof6m{Zk1o+XPE{uj5DH1O{lo6|uG`=VvV_0c+P02~Oo4f{*A5p#~F?>mSgrz9O-N zKG}nH+0;p(K1mQ{Tpv-=06w4Zpx)q})pO|#_s{ozMqU9sd7`la*=)o#$=YVCaN_(_a#Lq-{oRUH_1J3yY z+$V&mw$BjpolQ_5)k24~eljGi{M0Nmmv;LTo^8X}IeAFFztktsba7$ol7Z8z4HZ*M zhcbP1RDt*!VtLmh4rD|naBYo$p28A*+T&)5V-?hB&?HCNL?xNc1O(=m*p*4VWyz%+ zlgE6K2zNL=Up6{=H2ACq{5+g@I+80WJ0MwqBo?1`nH2Lc6pvL;8e$bfuxJ)S(4%6a zmg!LyU<6s+jwVX(Dl%^B^>#00y&8xT-#eT@9uv48xKks_$QZgy%Miy`Li+k<+WNiO z-M_YsZQi;R9)uJ8nbiHc`STrrCmQMRA=WfOj~$e!@k6lZ1Qpqj$M)0hRK2QR?)p*Q zF!q+-vu*E0I*@!o^j=gym==cCInPeOFL6DBYW)@HsM~IVka>z> zE9*IAK)|_0fhQ12z@)UEp(0vvxx3-j1A3Xex=f1H8QUuf=_tvNkIHE$+4YCc8s(`} z{S+haF?o20#Jn;x`xV&NA^V~%Z^Zo1oi7Udg?$~kV&@Q4;7AA|k#i;}_)rO%CSCB| zSa6(N5uda+`rr#wTN~@3H=ilYNS<#oKl2ZcABU%6Z<=f*$lBxvoWWD);ZAb9-HXcz zXwLKRvEB2Io?h*pKTUvE{L%$y=|?l0w~8DaBvAV>#Q02ugnl7 zpvuE2tXY$ye(FPQlBSP60cvWA#?&y+%hl;lmS*7(Swbfb$#J@S$RCD4;*6?6NeucG zzWWmk+43MVgv9ol7*RS4ej0zC-LAE;BFOJZ`;g-9X>F@spxz)E$^H*Qiy$ z=+-FPbLszU(tALm%k4aLm3|+MOLtiS7VOWJOCL|ji9&*n9t0#-9|kB1b$xQd!Crv2 zLaS#B)W~2c&L`ZsaCsVK(8H;Cbw7P=3zye)%`cH+V4dK=5s0lXk0%Bf2T1{Tc$nA? zn!oUV@T?s~CfAS9vZK$~Boy%bFBla`M~l7} zRh)~PMDzR6={^1(DRSVdM&rrDN6(Rd$Sq->9%;pplv7wzI3{^uDLS0?dQxt$8tYF~ zOpInH)PWmu>%y=|;;{JI(mNyOWX$-sUi|1pwO{_Trs1^c3!-8=Xm_4KjgH_PySL^? zkM?##d#jWsp$T6jq-;d_)xWW&&b>Wt77ML3zuP@T#t#=U{e(0~KM4Yl`fB*la&A7h z0k2NFEv@7Q8r698`kLVq&0*!6>(o&cA578VR{|}t#X1Qq4-^Roj^5z0$B^3Ufj<7Z zKfRAs!WT{JAu?39e79iK5hXvylokK5U*lI!1XjR!K&Wuiy#rvpyn?K_iD^)P(#5Z) zfeGC1Z}!sb4uquPW<#uN2eQSPh!?3v$88W+hY;Z#Z}Zd(-htO197%{TB4kATi~;CP z&pvR;$gPpa)nteF&a)|~LNnk`ikOVZ5rnkSojVD5C8>KBUi%$RaMGQMB6jBS0#H(B ztJq!J6g#u!P3PJb#2x&t7f(t}eRV>S)64MfoMTw4q61};LU@g7Imhg*{I=( z+$_JXQMye!nIW$$ae7fYnxvuz*#-TfO@Xxtp)hE@3^uF$%f1LL10b~A+t>Sw zpwDb^nOGC?^IH0|=m1?pkSVD#4uK9KEFjpGB(463jsr#5%H#OYe+;&*y0-<>SAUs| z433>Q7xy>|wm3TD63V+ebd>p?kBw1#3?4}px*P}>>8g>b{Lru719!&ADBZ`W*jJXI zDIn=rYhZk}?;Nl|*a;>Bo$rY_2dP<05bCe4SS}G8Y)l(2?$dE}y4lV{YvPHc^Ql}^ zqdCbJc!gh6SqPWV{*|nNQJ%^uGGY`5kex61F>z(R~_6ego57uTa+%|L*b;WfiZElU7m)42Oav?YQ$ghxhr4`#uU0zQN*0Z@|&*P}9ugAm!V#C*DyB zm>ET%QC=EyqNn*-SRv79iZrwb02U)zbOW+>4%G8>_r2Hal#ht=-u;JUNsz(TbVqpa znDr7Ma;@KEmbi_y+EE#YpK6D+r}Ve`Ip2 zCeL}M?L7MJM%%oHsunYB4cY%3x2P~3VFiIc_!YnwKdie+S-1fXQ<9jL30aW^{ysD<2cx}b#s+W z!3YNxNR}Bs-eJ7aNyT_rV^p#9T^}3ba$Y%qiXr~SODzkN+9Hqe$rNsk3YRc&l`~cZ zL0>LZXr+>6mEBg-(3@bH93jD|%ck%gcSWH*av%o^v{tQ&E)hZ~KS_le51<@h=n_R2 zP+n7-n-_(-Ht9g-lnygbCYKwdsZGhnnaw~hT&Mq zY8$Qo)CvPlM5*vAyf}fgo|vYKP4>;=EYOOZRZS2QJVnRq4@};(qe$>Wfmt}r6;?<6>&vjS$J3g0(Ze;AG z^6)Cc{Y-qZ-_Gz)HyvBB%VvT{Q*Oi#_ArVdvQn2kbZX&(IIgg_RM#rV{n04%9Aeg0 zO?l3V7gHGh?EIPR``!zIpz6)T3bUD@G+K07d~3uD;qEkrwQG&YgnK!1*f)=E>b;Gf zcwsThP4rr&sk7o@R*%J+b}&vD=nEEtH{TVuJv{3ko06a0jdZAN#0VtFkal>{6|D&A zKNhl2BG)l(hEU5h*#AClccEq2D}J3s95R`z$UD%C{X^+KF?%X#%Ux>Vr* zf`NC}os(phAmO+o>C7PuS^+}N^NdXC zJVSikJWA1w%q*cIT+X)#y8bC_qL#)V>vI{&$GqT@H{7veU**rpYwnU={G2|Oznx@B(E6P78Z9M_cYNLE zp0~7FJ-#4mD45JrZ(`s$X|0e>u;5r}cOY&w>}V76>$t9))E3fd+DQQ+%VSsmB!fs6 zlhKV>^{k2EK~-xd0fH~sITsdUB}i*7d=JXWAk%B6?n7?-kv-4GfIgJ>J2zD!(EhKIlr% zA3J~eT}IGvX@9PASQ;BaW>1{6(?|`3_h5zkX%D%H9tjhF_Ya0XpEW3WH*(xsQw)3gE*5kIieWy#j3rmLL(H=MSJ%<(r8_`I<}#n0Z1%wC^sK zPv(6$bJ@1}jKL>B8qH-j@C>uv#gw{9nF0d~8i8t=d|5YR?_A@&h}X2-J1x&8?y!AXEm&~A4Az|M(OC%02Augl@EW>j5!@yDTsurgsyU&4*} zftmK!2URM@%Y>lxjgs+PfmeZboSAvT0w$N9qC$PER73cPzaC-@B=Ih^0U>q)$$p=; zF~#t3(o0r2M_XdrdIPxnURhX5SR$GPCr5^e0()~xik<;e$8a9$=>w?N>MmunBrl2O+Tzl9}do4~qOg+D0<|K4LMGG647RlDJ-d(%Yv~o&|o*YI5 z3j<;=nnfiR9w!rqLJnIW6-Lz+5rTd-X`HV3XSOsu5YIp^Ahc49jeI;dQEkhr%SM-M z9ycCxozK2|2#73l>15!iMPw(x(p(||R=EE3`=6(FbTyu~s1zNpt=(gA4Mng6ipcxF z)P!?>AL(84xgGq3^XluqI6>+*UzxlREC~0T@+!T0Zk^qb<8$%)PSulm=;#b_Ft}>EOl}wiMQupG7Us3p<$J3B9E_nJjZM_P9aI zhCfG7hqUG?P-S3xb6jWN#rq^o~W6!H#$=dCeztbmq`6wGaVEp#1n-ez@{|TY5$7UD<)Wm60v()Qo878fSO6D(HE*Xcf0zOZ*hakU)rwM*r zBk&xgpCf@`3j|qte~ZwHgq6wHPR0#eK(bb_@)-XDZIpacwlPRox<@pY z1`1aw2DqXOpKefQCXxb9Yy1%Fpc0 zfoAcK#=WS8*~UVSxB*)nnH!owf5#SEgs25T9O-%nN2l~RbawblWEiqi%Jc!ozwsX7Z7_U1aKxUuCO8wvv>vSoE&W~N_Xx%J zls;aNtSFJ9g8p~2q@`V#`i?dTg25va7m!5mBPI?Dy!+qlJOwN$VJIpe^xf>l+m}Wt z+-&|K-Z^cd6_bA_SVCD@$1CidlaSdjF40pg6e zn8(-p0593>8nl)|jS>-rJJdG8-u8GW*J@_DD?4q!f)SvhwsLq0Pc;UG#FB$Pl~(={ zpJN~{;aa(2)vTLrlHQ=qmK7MojYMk&6_|{F;8|J!O*=Or*^Il-=HEPH&|9WG+YQ57 z?SAH(M`xX!`|!s$Zf1_X0~2*k+sS4LDbe{LNhX067Y_Sted{h>s`-L#Mau%-HoX8$ zKEPaENjZJJ>kv(u_eH8tx*oEtVpXMf-D$p7;x!w6QtqA1%MEM?-nB(45(;~~yz!gl z-2>B0@@@XPZfNyXKlARn1;)%f>5kXdo7&sXU_;_7ETmz9l@|K_W68v6=m|fyt=?5m zV9#9JeEiL<_J+?5DmX$cO79rz^qytEXlu^i6D()f%$9@;t{O$8J6*ttZwIY}0Iy#` zf}ck`f3$a3pKs%clfpl1wq}cQRy@q(C^|@uUf8LnplTA1CEAxlHL*Es;bmmbl%+S5 zRO;E}05<%)@XFy1=8RIF1_!6i1qi8kp1NS78c=F3*6vuccRsv{HcnLV)!H`q!|JpE+k-U)>Av6iFg0#!Rc4v zcn5vPbeDIG4nrxzKt%Iy#|!~5+xzC~%Ko}HeBh5ntbwd@#t@b;1!yvDFJtIQcVEEh zt$x%~enqrnd;>Orvnih5i@k{d^skEX92%XyazgEoxBcX@?Ih&sM>jz`kmF>({Y__AJ#EY~<0JJ6%bT9i}-^ zbr0HkM`Am08YFM1Bawj{b53&4af-RZbStp)AMXUqkGJV&S=|rw_8h}CRw-nYtW^A_ zHvApsOu8yYydp~E6!;E-mjte(0B2^7-t|>GzQ|Shb5(Da41_R_zD%wIAC0XReR|9vyt1^J47SA*}J#UkC9m%rs}>f(F$XRbS(<-BhwY zn*xsdd~r@iFo06iiDr-2A4qS@=A^l+ck47=$5a-3Fi~a%27xC|Y{mlv|=AkrnM^kq-n1dt)gyqL%X`})To1| z7jGCa*DSn1yo`{Px^xQsZBmBnI=|?4CXKzq&76vK0BW+J@v;^x4wv4SE*6Zw*}B*} zK2Itmx$lR?4Y}S^`J^gWiQuJBbX3xwB%AXmY;p-W<|A|T-Z2W?ic7b%!eFI05b4Ee z!Cu?+%dCC%nb#$s24>N(eqWO^Z3D?rzd-QrNZep#Yd9V=pX;J^&Yr>*@}3z!Lcb>Had&qZS4iHx=r>5s|~WY#YxWFLQqF#KsE-4oBQ;qiJy$5a> zqb$Ou3{nzeBbv|VPZZCRU|lCs{GRkHW=7fsQnC#1{?X8HJ+fFKV=3Vra0fGj`UeTz z)SM%Bd%2+PyIowPPc#!~v_PVmDH+ClfSrpdHrKb1G%+Q`6r|gF>A-W}y5G)xzS;Vt z&+m4oe(xj{@_fDgxb?ijl{-wL`*|S|%|X}*b|-SMq%^uHk_UV*v+Jov*CoI~jAk)! zj%>g3Z=Q$#aZg0;Vaq1vg@Gc>pDqnPxgse3kI?I=zmU?N6y(0w;gh)qL?+`oelkGR zPVm>u99)O=?w?dl*4(xu)=4bUQ~oxZi0rH3yfnmhUDTn2DfEj%9Iz*W9U*qu*4bDKTm zG4t|oFDdl7`A!$2qeyrzchzKQ;eFA=Zk)K4wx+Jb#g}EcBRnjBLO>eIO3dmaGvA#$ zPh8YxShUaMKS@SC4_7aH(K101VY{Awp)#m66)uAcxqKz!78dzvYV$)_M~ymOrxN!h^@LC7{I@ z0-=e1D;5hUN5qTZrJr;T2p;7=_#JCjZK$qQuzI&kTpPrBdP==F4xvJ z7|y0hbt#;lWJB;^TM#RT{Zk80t^*8#jIo1XvH_c+DwgfK9{~WQ`ADQ% z@{H0wZ!F@u7`=guVS7!%xc-g93(nY4(Ij|GfCB{HmJI)#Q{rPB_p;VyQ>r$ODSM4` z;XKF^*kTjCl-5?G`jb429PR!3ol$vl^{ zh6FMD`cy2+{xQw)?b**)ABGqKq2bH0``3{)sz6mH zmI42*ui7l`fh}t^q-ab{TirY{A`RGl)Y~0GVMeN8DWkQeYkt}hWSv{D; zpD;})s5$D)rQ&vLdPQ7~<1vS(XIeqSVdazh=#5Kan7w#^$f&~160*JER%^RCtWyeY zR-D27KWknKg>I%sn20tia{YfRGhqCHdw1JWU&`gO-PQMcLa`YG$u*gV|2P+vt?HUc zgDT#|I@-bYCtYX@LXJ4}=3&~NMMH4-6Tz1#W-b;nkh^(sH`+#`3#cdn;ZHOiQn0 z$%JO`j`5I7gW=ik$ZmyhMiiSxX%3ZEx-!FoCZw=hdISVOO2KQo%p*%+zA&+1DW-w) z441WFU{Qs++G0=I2PGhN$slS!5gxDS zDnyyIkteXbUG6w1Oo$4qpvVZk+9P2(D>GyJrp4&O;up}#Gqy+mmVNY6%iaw3bjkpY z0+MUdn1Zp9K-EDm&INzm?<*mhXXloT3v$E8$s%gCdcG9-G!1X{+`s%V!}SN z?O;SafjvXG37UU)%4(S8ubTYT7lsaH4DGP>%71iM$p)AJ!Os?(CQXIXgcl>>&}|vT zMC|$<+2H5NSJwVUW?K9!%T;>=!%^do`-ukDeAF(0UY@JEH&^t z#nh6_FSDZXA07!B2nv_SQzr0yxnB3EdG#0tUOvk%RUz`^*O4vSXT&y$ek|(9m_S%q z9ndu-s*s+RyD4~ric{?70pw8XFKJIaEoG$+4v zd}0-k&G;v-`4gMoAiEjaO}hv4&#W!X^xCA!QnGlz_B?r-z zX1S*5R=4L>cZzw-jjoETsTpZVci#6jhg~GQ9seA_d3=tIdCzR_{V{o`tCiqMVtY*6 zN1lGph-imGZh3AtdI?InW*U?QuCB?w(A_lNS>_|Rp=irJA|36<+@tuam=)B`e5s4t zA17peEicz-jrfbzS3O+VIpl%Gp;9Dk5HseS6~ms;x#wi4OVs_WL~4{p?Q-E)n6)~y z2~sKzZ`BsRr-l~Fw{>35>#dG-2Tc;6XCc4Nfr$Ra%#E4OC$WwqxIW$<9}A^fPyO*B zG>CBb)l+e={!#0-dyweL@&$r7`|fyFZ*VM$B$|K5(cPgIOH#gh$Z6^;QLG~DbI$8S) zTn}yMcp&6UznC?}47~fy!nqg-PBNIygH@rHtJ!zdpjenOpaosWeFDbjAwaf&LLg2< zO>%Y^pFPrqSK)DS^*9gWvUW&p(!(s~YG( zk7p<${KJrT8XO2F)q9@#z!oc}4>!ZzM| zzMWv-^R)d{J#ypejXZH{2MZP{>k98V8x9hMN9BC7+1-DJdtDegnW2l^|5v(=ZW!?? zXne47^S{iXsi7fYFOW>P+T&Md4gBVJC=ey|zhEKh$yY;qa_=5Kyd04-0977l|NpXq z-+ql3!OXs(wh$2Lv}9~-9Hl-{fZR{8WX4V)y0*;1nKYnn<%;cN%6+K}L=O(bDI2YWV%P|e?!$p~9$gbp6JUS7 zigW@`vN4tV+aZ#tdI10~BpSN!SRsJsf14D*91sY|0dxbl0iPgjAQB*!AQ2$tAmbpL zAUB}kpqQXEpnRYTppKxmpwpo5V7OrBU}0bdU`ODv;7s7w;6C8x;O*ec5XcY`5D^gF z-)YC6khPF|P;^i{P*PADP^M6oP{U9sQ18%)&?L}o&}z^=(2dZ0FeET)Fs?8qFr%=r zuw<|Tuv)Oru-mW?a8z&-aE@>}aO3d!@Lcdp@UHMV@S_OW2uuhP2(}1u2!jZ#2p5Rp zi1>&?h#rUqh@FVbh&M>ENHjY%vQ`%%o8j~EE+5Yte;r5 zSgY8C*euvW*eclO*k0H%*ag_N*u&VnI7~PKILn75 zRgcZt_h7m)GH3TmjTgQTg)mvd-)?cbQPC*>>{A zW~HFp03DTedQto?dg&&+SEx_Gy|3%+7rTd$`&D*_W}I zTbiOdI^1rfInnM@iiL8cXu3@O(zZd7X2~)C_-1iFlTKbRGj)Q{C%Fs`^Dt~61uqyerhQm%v?856_F;6knp6Rwnft{gP3_(ZOBRj%X( zu54DW#0IWR7p~NMu3Wg*IBmVbA-#TEhC>#HW7z5gn(8Cm>O-FDV@Uf03i~5W`$G=< zW5lZihN~mOt3!dSW00d8BBWcU*c+rwTE-%89)~j_XUeNhYGamNnTk-a%rR!ZMzS%Xu|~l= z8e>N3oShZl;H)q`0ntX4dQh(ye|}i5mxy;T?2PuRusy2lNqK!3VjR&u0s zjb3u*T^^rCPN^%6#85dGMh&W3fsD!+jd-zU8I8P|5>AEOqWf92SH*VO^I6cpQh~CW zv&et?zUn)RNN1Gg+2zVjEJMe~y*{b<||7 ywW~@Wt<|f_5Ufp_!b25;<_y`n5cEZ7k=e-*v_93=5WMN&tpM*Mo6tJ&y1+Yn zgo9iR9c$ps0<0=RQh=ln;y#E2crSpp8h*V3&QfrYd++-F4d}fFgHuK@z*%x_KOE%4 ztO#lvY6=E5Bt=L{kVLR8fSEpgu~R&>4!pJd1I&}){RKJz*1)J79Pj=C^Xbd^6-p?OFxt+=7P6SKrDfi%b->Xze97ZRd7WF zhuVAk* z>{bvL4X2X`x)prI-n#?5TZi$^BB9wL=!ifU&E#t;rCOrR-$MZ;D$8kUre2P)c$7l&rm2x>+1|4jf(!CO8OHCq5*2w-tpg3pJlc2-WOz-oD+n}&yS zLW4&#Xl7Sc!D$899vQDxl>@}_Of_#9@_rzm58D9~ITQFBjO~US^YB4nj8__HV8K+C zP%VPa5*!ZjPzIRCQ%QcHEJ3d8Rcwz4r)tjTh^cx4L2Ci1W*I@OlKy3OFGL ztHfYcHwb`TJq1;%qgHo`XSy?J)&pO6B);Z1BuqDH%Gi^ZJLON?pKkD;l!^H8nE;-S zoP;J5aAPLcP$?Oq@HHGId7vqnQ{dPPqiYh7m~FfJ{Wgxjwue zIti^&aV(gEnZSGnhXDM;87~LkgWyeoF)QJBMHAS2(aU;809GSY!dwBsY&iIAIHFe! zm)ZpMoaBfQt}GZetF}Z6NM4Bfr${3>?F4n2k$9@a1C>Si7>lW;Sv1quwAJ%ul z4GZBIYj6vAD~AC!2Re4am;yXlbP}dtuQ;*gb!e2V(j`^x)+YA5U}6aWD%2<7y^2c( zLyv%z=(c{o0R7bDU7W)#zD9*6^YArd9CMP?x9 zUN|y@pqvpWerhw;u)7<+ih@GH48AijrA7eDd*Ji!Q0yj|z*4wcN{FVC?0^}P?ZCWl zr-WIS%KPXL?#q&4=h)qE>|Tq87p3~F`XEgFIk-`$b81YCdjej=;DE9Bdwh5^Op{yPPED%c8B3)cco;fnXMkA` zmq?)28dc1e8kS-bsGIww{J+4l>%(Ot5UteE2K-RToFBu7cLd#@1Y`#bWZM})s3f7y z25(LSm~Qz$fy27sDJ!44IUQm<38CW60Fxb?A$%zmujRxmE#hRbZ0zMw@UChA;wp_w zskX5PP8GGP1M|K5)x5r;N^Z59RgvR212|3=sY3$)%n<%lkeJE{cS5vY& z0le4?r(4fVtM`oETYOH#c9G+uCdFT!ij~ayPJm4k{0dHu;GeFI7Rhl)w&5jwcuVi? zkvdM>5?#Q&+ykco{J{ZoTPnq4BUmOIrQNzY5L?bvLr8kzjlW9yA`(0yg1;A9%XuS^ zUQ}9QRI{W`Xi|~g;-&zO&DhIa=WK`LVI;V())J+-+J{;!K%1MytXm~$A%!*o1M<#!5nhs;7miu%Bn~To&w}t%f9x(g;sc7ge z8H0nWH4XNngsj%A`QTYv-I~V9QF&M;fjE>wFYD=AKYXwlE)mr_DS|DnfH=ospRs)n z8m0`G$?eX#p|N&~1E`jv(hEPfJHUqpNDp`+%xqOJHLPr`CH^!lmV`Jlf>&AvyW`DR z!A>8(j1QOQO?*^Ti)ynGj9o?PI==10c_PCd&L-(+;{Mpru;Q;Q3u?3h{%&0b1c7~2irt>%n;10S7cvot#ES_6>)$)+^ zz?>CuMg+SWt7*T!@~s6B_rN#ztln(+Mkqh3wBy$CT8*AYVYg56=1qs-!IB6)$ z6Ae%I!Zjg$kZ*FYt)Qj8K z>4YD&Y@95Eb9>;ww)qycNinA`i_5UH6TT&-%FM97UDOu68oTn&Sd{`K{h+5xH>R#D zzaG5R^2N(=vlNFFDaG6MOl-ZPpi46RZ}!C_O(>V-pdzKrJGKIE`cU=?`EnZ^&;=_> zXwOKsEV6VsmBwOJQJHN+9@|0?d0n;Jxl6*3RInwn=axmnlans5}@xBDN^@6R2 zi?CxUeBaLNiG6u-UUk zetRcn1x)OOha#wz(!SG>1*PiWWN68Kw+|p&*-AbI@M<1@IuFJH%mQ!g{sOTMyftF% z1sJQ~`~V(}V0%g4A#t894$Au_Ls~kQa#Za1a!oCgdu3nqTnM-HOHUZ!PVgSy-){&F zG#FeChveX_5PlUvzk+x35D%ELJ}IV;tP|mTQk3qcv^gspUJGGy7hE|Tj$Pnv%w~O* z?`+xUiCxYh+PpRNv88Z&0N>FtS;1o(N&#%t@Vp%Bw5e)mYxsF5T(}*MSqTR>rhr%W b0mT0S5)@H#d7`>U00000NkvXXu0mjfEz&h@ literal 0 HcmV?d00001 diff --git a/3rdparty/sabre/dav/lib/DAV/Client.php b/3rdparty/sabre/dav/lib/DAV/Client.php new file mode 100644 index 00000000..1028a6b9 --- /dev/null +++ b/3rdparty/sabre/dav/lib/DAV/Client.php @@ -0,0 +1,485 @@ +xml->elementMap. + * It's deprecated as of version 3.0.0, and should no longer be used. + * + * @deprecated + * + * @var array + */ + public $propertyMap = []; + + /** + * Base URI. + * + * This URI will be used to resolve relative urls. + * + * @var string + */ + protected $baseUri; + + /** + * Basic authentication. + */ + const AUTH_BASIC = 1; + + /** + * Digest authentication. + */ + const AUTH_DIGEST = 2; + + /** + * NTLM authentication. + */ + const AUTH_NTLM = 4; + + /** + * Identity encoding, which basically does not nothing. + */ + const ENCODING_IDENTITY = 1; + + /** + * Deflate encoding. + */ + const ENCODING_DEFLATE = 2; + + /** + * Gzip encoding. + */ + const ENCODING_GZIP = 4; + + /** + * Sends all encoding headers. + */ + const ENCODING_ALL = 7; + + /** + * Content-encoding. + * + * @var int + */ + protected $encoding = self::ENCODING_IDENTITY; + + /** + * Constructor. + * + * Settings are provided through the 'settings' argument. The following + * settings are supported: + * + * * baseUri + * * userName (optional) + * * password (optional) + * * proxy (optional) + * * authType (optional) + * * encoding (optional) + * + * authType must be a bitmap, using self::AUTH_BASIC, self::AUTH_DIGEST + * and self::AUTH_NTLM. If you know which authentication method will be + * used, it's recommended to set it, as it will save a great deal of + * requests to 'discover' this information. + * + * Encoding is a bitmap with one of the ENCODING constants. + */ + public function __construct(array $settings) + { + if (!isset($settings['baseUri'])) { + throw new \InvalidArgumentException('A baseUri must be provided'); + } + + parent::__construct(); + + $this->baseUri = $settings['baseUri']; + + if (isset($settings['proxy'])) { + $this->addCurlSetting(CURLOPT_PROXY, $settings['proxy']); + } + + if (isset($settings['userName'])) { + $userName = $settings['userName']; + $password = isset($settings['password']) ? $settings['password'] : ''; + + if (isset($settings['authType'])) { + $curlType = 0; + if ($settings['authType'] & self::AUTH_BASIC) { + $curlType |= CURLAUTH_BASIC; + } + if ($settings['authType'] & self::AUTH_DIGEST) { + $curlType |= CURLAUTH_DIGEST; + } + if ($settings['authType'] & self::AUTH_NTLM) { + $curlType |= CURLAUTH_NTLM; + } + } else { + $curlType = CURLAUTH_BASIC | CURLAUTH_DIGEST; + } + + $this->addCurlSetting(CURLOPT_HTTPAUTH, $curlType); + $this->addCurlSetting(CURLOPT_USERPWD, $userName.':'.$password); + } + + if (isset($settings['encoding'])) { + $encoding = $settings['encoding']; + + $encodings = []; + if ($encoding & self::ENCODING_IDENTITY) { + $encodings[] = 'identity'; + } + if ($encoding & self::ENCODING_DEFLATE) { + $encodings[] = 'deflate'; + } + if ($encoding & self::ENCODING_GZIP) { + $encodings[] = 'gzip'; + } + $this->addCurlSetting(CURLOPT_ENCODING, implode(',', $encodings)); + } + + $this->addCurlSetting(CURLOPT_USERAGENT, 'sabre-dav/'.Version::VERSION.' (http://sabre.io/)'); + + $this->xml = new Xml\Service(); + // BC + $this->propertyMap = &$this->xml->elementMap; + } + + /** + * Does a PROPFIND request with filtered response returning only available properties. + * + * The list of requested properties must be specified as an array, in clark + * notation. + * + * Depth should be either 0 or 1. A depth of 1 will cause a request to be + * made to the server to also return all child resources. + * + * For depth 0, just the array of properties for the resource is returned. + * + * For depth 1, the returned array will contain a list of resource names as keys, + * and an array of properties as values. + * + * The array of properties will contain the properties as keys with their values as the value. + * Only properties that are actually returned from the server without error will be + * returned, anything else is discarded. + * + * @param 1|0 $depth + */ + public function propFind($url, array $properties, $depth = 0): array + { + $result = $this->doPropFind($url, $properties, $depth); + + // If depth was 0, we only return the top item + if (0 === $depth) { + reset($result); + $result = current($result); + + return isset($result[200]) ? $result[200] : []; + } + + $newResult = []; + foreach ($result as $href => $statusList) { + $newResult[$href] = isset($statusList[200]) ? $statusList[200] : []; + } + + return $newResult; + } + + /** + * Does a PROPFIND request with unfiltered response. + * + * The list of requested properties must be specified as an array, in clark + * notation. + * + * Depth should be either 0 or 1. A depth of 1 will cause a request to be + * made to the server to also return all child resources. + * + * For depth 0, just the multi-level array of status and properties for the resource is returned. + * + * For depth 1, the returned array will contain a list of resources as keys and + * a multi-level array containing status and properties as value. + * + * The multi-level array of status and properties is formatted the same as what is + * documented for parseMultiStatus. + * + * All properties that are actually returned from the server are returned by this method. + * + * @param 1|0 $depth + */ + public function propFindUnfiltered(string $url, array $properties, int $depth = 0): array + { + $result = $this->doPropFind($url, $properties, $depth); + + // If depth was 0, we only return the top item + if (0 === $depth) { + reset($result); + + return current($result); + } else { + return $result; + } + } + + /** + * Does a PROPFIND request. + * + * The list of requested properties must be specified as an array, in clark + * notation. + * + * Depth should be either 0 or 1. A depth of 1 will cause a request to be + * made to the server to also return all child resources. + * + * The returned array will contain a list of resources as keys and + * a multi-level array containing status and properties as value. + * + * The multi-level array of status and properties is formatted the same as what is + * documented for parseMultiStatus. + * + * @param 1|0 $depth + */ + private function doPropFind($url, array $properties, $depth = 0): array + { + $dom = new \DOMDocument('1.0', 'UTF-8'); + $dom->formatOutput = true; + $root = $dom->createElementNS('DAV:', 'd:propfind'); + $prop = $dom->createElement('d:prop'); + + foreach ($properties as $property) { + list( + $namespace, + $elementName + ) = \Sabre\Xml\Service::parseClarkNotation($property); + + if ('DAV:' === $namespace) { + $element = $dom->createElement('d:'.$elementName); + } else { + $element = $dom->createElementNS($namespace, 'x:'.$elementName); + } + + $prop->appendChild($element); + } + + $dom->appendChild($root)->appendChild($prop); + $body = $dom->saveXML(); + + $url = $this->getAbsoluteUrl($url); + + $request = new HTTP\Request('PROPFIND', $url, [ + 'Depth' => $depth, + 'Content-Type' => 'application/xml', + ], $body); + + $response = $this->send($request); + + if ((int) $response->getStatus() >= 400) { + throw new HTTP\ClientHttpException($response); + } + + return $this->parseMultiStatus($response->getBodyAsString()); + } + + /** + * Updates a list of properties on the server. + * + * The list of properties must have clark-notation properties for the keys, + * and the actual (string) value for the value. If the value is null, an + * attempt is made to delete the property. + * + * @param string $url + * + * @return bool + */ + public function propPatch($url, array $properties) + { + $propPatch = new Xml\Request\PropPatch(); + $propPatch->properties = $properties; + $xml = $this->xml->write( + '{DAV:}propertyupdate', + $propPatch + ); + + $url = $this->getAbsoluteUrl($url); + $request = new HTTP\Request('PROPPATCH', $url, [ + 'Content-Type' => 'application/xml', + ], $xml); + $response = $this->send($request); + + if ($response->getStatus() >= 400) { + throw new HTTP\ClientHttpException($response); + } + + if (207 === $response->getStatus()) { + // If it's a 207, the request could still have failed, but the + // information is hidden in the response body. + $result = $this->parseMultiStatus($response->getBodyAsString()); + + $errorProperties = []; + foreach ($result as $href => $statusList) { + foreach ($statusList as $status => $properties) { + if ($status >= 400) { + foreach ($properties as $propName => $propValue) { + $errorProperties[] = $propName.' ('.$status.')'; + } + } + } + } + if ($errorProperties) { + throw new HTTP\ClientException('PROPPATCH failed. The following properties errored: '.implode(', ', $errorProperties)); + } + } + + return true; + } + + /** + * Performs an HTTP options request. + * + * This method returns all the features from the 'DAV:' header as an array. + * If there was no DAV header, or no contents this method will return an + * empty array. + * + * @return array + */ + public function options() + { + $request = new HTTP\Request('OPTIONS', $this->getAbsoluteUrl('')); + $response = $this->send($request); + + $dav = $response->getHeader('Dav'); + if (!$dav) { + return []; + } + + $features = explode(',', $dav); + foreach ($features as &$v) { + $v = trim($v); + } + + return $features; + } + + /** + * Performs an actual HTTP request, and returns the result. + * + * If the specified url is relative, it will be expanded based on the base + * url. + * + * The returned array contains 3 keys: + * * body - the response body + * * httpCode - a HTTP code (200, 404, etc) + * * headers - a list of response http headers. The header names have + * been lowercased. + * + * For large uploads, it's highly recommended to specify body as a stream + * resource. You can easily do this by simply passing the result of + * fopen(..., 'r'). + * + * This method will throw an exception if an HTTP error was received. Any + * HTTP status code above 399 is considered an error. + * + * Note that it is no longer recommended to use this method, use the send() + * method instead. + * + * @param string $method + * @param string $url + * @param string|resource|null $body + * + * @throws clientException, in case a curl error occurred + * + * @return array + */ + public function request($method, $url = '', $body = null, array $headers = []) + { + $url = $this->getAbsoluteUrl($url); + + $response = $this->send(new HTTP\Request($method, $url, $headers, $body)); + + return [ + 'body' => $response->getBodyAsString(), + 'statusCode' => (int) $response->getStatus(), + 'headers' => array_change_key_case($response->getHeaders()), + ]; + } + + /** + * Returns the full url based on the given url (which may be relative). All + * urls are expanded based on the base url as given by the server. + * + * @param string $url + * + * @return string + */ + public function getAbsoluteUrl($url) + { + return Uri\resolve( + $this->baseUri, + (string) $url + ); + } + + /** + * Parses a WebDAV multistatus response body. + * + * This method returns an array with the following structure + * + * [ + * 'url/to/resource' => [ + * '200' => [ + * '{DAV:}property1' => 'value1', + * '{DAV:}property2' => 'value2', + * ], + * '404' => [ + * '{DAV:}property1' => null, + * '{DAV:}property2' => null, + * ], + * ], + * 'url/to/resource2' => [ + * .. etc .. + * ] + * ] + * + * @param string $body xml body + * + * @return array + */ + public function parseMultiStatus($body) + { + $multistatus = $this->xml->expect('{DAV:}multistatus', $body); + + $result = []; + + foreach ($multistatus->getResponses() as $response) { + $result[$response->getHref()] = $response->getResponseProperties(); + } + + return $result; + } +} diff --git a/3rdparty/sabre/dav/lib/DAV/Collection.php b/3rdparty/sabre/dav/lib/DAV/Collection.php new file mode 100644 index 00000000..2728bb27 --- /dev/null +++ b/3rdparty/sabre/dav/lib/DAV/Collection.php @@ -0,0 +1,106 @@ +getChildren() as $child) { + if ($child->getName() === $name) { + return $child; + } + } + throw new Exception\NotFound('File not found: '.$name); + } + + /** + * Checks is a child-node exists. + * + * It is generally a good idea to try and override this. Usually it can be optimized. + * + * @param string $name + * + * @return bool + */ + public function childExists($name) + { + try { + $this->getChild($name); + + return true; + } catch (Exception\NotFound $e) { + return false; + } + } + + /** + * Creates a new file in the directory. + * + * Data will either be supplied as a stream resource, or in certain cases + * as a string. Keep in mind that you may have to support either. + * + * After successful creation of the file, you may choose to return the ETag + * of the new file here. + * + * The returned ETag must be surrounded by double-quotes (The quotes should + * be part of the actual string). + * + * If you cannot accurately determine the ETag, you should not return it. + * If you don't store the file exactly as-is (you're transforming it + * somehow) you should also not return an ETag. + * + * This means that if a subsequent GET to this new file does not exactly + * return the same contents of what was submitted here, you are strongly + * recommended to omit the ETag. + * + * @param string $name Name of the file + * @param resource|string $data Initial payload + * + * @return string|null + */ + public function createFile($name, $data = null) + { + throw new Exception\Forbidden('Permission denied to create file (filename '.$name.')'); + } + + /** + * Creates a new subdirectory. + * + * @param string $name + * + * @throws Exception\Forbidden + */ + public function createDirectory($name) + { + throw new Exception\Forbidden('Permission denied to create directory'); + } +} diff --git a/3rdparty/sabre/dav/lib/DAV/CorePlugin.php b/3rdparty/sabre/dav/lib/DAV/CorePlugin.php new file mode 100644 index 00000000..dbd8976b --- /dev/null +++ b/3rdparty/sabre/dav/lib/DAV/CorePlugin.php @@ -0,0 +1,907 @@ +server = $server; + $server->on('method:GET', [$this, 'httpGet']); + $server->on('method:OPTIONS', [$this, 'httpOptions']); + $server->on('method:HEAD', [$this, 'httpHead']); + $server->on('method:DELETE', [$this, 'httpDelete']); + $server->on('method:PROPFIND', [$this, 'httpPropFind']); + $server->on('method:PROPPATCH', [$this, 'httpPropPatch']); + $server->on('method:PUT', [$this, 'httpPut']); + $server->on('method:MKCOL', [$this, 'httpMkcol']); + $server->on('method:MOVE', [$this, 'httpMove']); + $server->on('method:COPY', [$this, 'httpCopy']); + $server->on('method:REPORT', [$this, 'httpReport']); + + $server->on('propPatch', [$this, 'propPatchProtectedPropertyCheck'], 90); + $server->on('propPatch', [$this, 'propPatchNodeUpdate'], 200); + $server->on('propFind', [$this, 'propFind']); + $server->on('propFind', [$this, 'propFindNode'], 120); + $server->on('propFind', [$this, 'propFindLate'], 200); + + $server->on('exception', [$this, 'exception']); + } + + /** + * Returns a plugin name. + * + * Using this name other plugins will be able to access other plugins + * using DAV\Server::getPlugin + * + * @return string + */ + public function getPluginName() + { + return 'core'; + } + + /** + * This is the default implementation for the GET method. + * + * @return bool + */ + public function httpGet(RequestInterface $request, ResponseInterface $response) + { + $path = $request->getPath(); + $node = $this->server->tree->getNodeForPath($path); + + if (!$node instanceof IFile) { + return; + } + + if ('HEAD' === $request->getHeader('X-Sabre-Original-Method')) { + $body = ''; + } else { + $body = $node->get(); + + // Converting string into stream, if needed. + if (is_string($body)) { + $stream = fopen('php://temp', 'r+'); + fwrite($stream, $body); + rewind($stream); + $body = $stream; + } + } + + /* + * TODO: getetag, getlastmodified, getsize should also be used using + * this method + */ + $httpHeaders = $this->server->getHTTPHeaders($path); + + /* ContentType needs to get a default, because many webservers will otherwise + * default to text/html, and we don't want this for security reasons. + */ + if (!isset($httpHeaders['Content-Type'])) { + $httpHeaders['Content-Type'] = 'application/octet-stream'; + } + + if (isset($httpHeaders['Content-Length'])) { + $nodeSize = $httpHeaders['Content-Length']; + + // Need to unset Content-Length, because we'll handle that during figuring out the range + unset($httpHeaders['Content-Length']); + } else { + $nodeSize = null; + } + + $response->addHeaders($httpHeaders); + + $range = $this->server->getHTTPRange(); + $ifRange = $request->getHeader('If-Range'); + $ignoreRangeHeader = false; + + // If ifRange is set, and range is specified, we first need to check + // the precondition. + if ($nodeSize && $range && $ifRange) { + // if IfRange is parsable as a date we'll treat it as a DateTime + // otherwise, we must treat it as an etag. + try { + $ifRangeDate = new \DateTime($ifRange); + + // It's a date. We must check if the entity is modified since + // the specified date. + if (!isset($httpHeaders['Last-Modified'])) { + $ignoreRangeHeader = true; + } else { + $modified = new \DateTime($httpHeaders['Last-Modified']); + if ($modified > $ifRangeDate) { + $ignoreRangeHeader = true; + } + } + } catch (\Exception $e) { + // It's an entity. We can do a simple comparison. + if (!isset($httpHeaders['ETag'])) { + $ignoreRangeHeader = true; + } elseif ($httpHeaders['ETag'] !== $ifRange) { + $ignoreRangeHeader = true; + } + } + } + + // We're only going to support HTTP ranges if the backend provided a filesize + if (!$ignoreRangeHeader && $nodeSize && $range) { + // Determining the exact byte offsets + if (!is_null($range[0])) { + $start = $range[0]; + $end = $range[1] ? $range[1] : $nodeSize - 1; + if ($start >= $nodeSize) { + throw new Exception\RequestedRangeNotSatisfiable('The start offset ('.$range[0].') exceeded the size of the entity ('.$nodeSize.')'); + } + if ($end < $start) { + throw new Exception\RequestedRangeNotSatisfiable('The end offset ('.$range[1].') is lower than the start offset ('.$range[0].')'); + } + if ($end >= $nodeSize) { + $end = $nodeSize - 1; + } + } else { + $start = $nodeSize - $range[1]; + $end = $nodeSize - 1; + + if ($start < 0) { + $start = 0; + } + } + + // Streams may advertise themselves as seekable, but still not + // actually allow fseek. We'll manually go forward in the stream + // if fseek failed. + if (!stream_get_meta_data($body)['seekable'] || -1 === fseek($body, $start, SEEK_SET)) { + $consumeBlock = 8192; + for ($consumed = 0; $start - $consumed > 0;) { + if (feof($body)) { + throw new Exception\RequestedRangeNotSatisfiable('The start offset ('.$start.') exceeded the size of the entity ('.$consumed.')'); + } + $consumed += strlen(fread($body, min($start - $consumed, $consumeBlock))); + } + } + + $response->setHeader('Content-Length', $end - $start + 1); + $response->setHeader('Content-Range', 'bytes '.$start.'-'.$end.'/'.$nodeSize); + $response->setStatus(206); + $response->setBody($body); + } else { + if ($nodeSize) { + $response->setHeader('Content-Length', $nodeSize); + } + $response->setStatus(200); + $response->setBody($body); + } + // Sending back false will interrupt the event chain and tell the server + // we've handled this method. + return false; + } + + /** + * HTTP OPTIONS. + * + * @return bool + */ + public function httpOptions(RequestInterface $request, ResponseInterface $response) + { + $methods = $this->server->getAllowedMethods($request->getPath()); + + $response->setHeader('Allow', strtoupper(implode(', ', $methods))); + $features = ['1', '3', 'extended-mkcol']; + + foreach ($this->server->getPlugins() as $plugin) { + $features = array_merge($features, $plugin->getFeatures()); + } + + $response->setHeader('DAV', implode(', ', $features)); + $response->setHeader('MS-Author-Via', 'DAV'); + $response->setHeader('Accept-Ranges', 'bytes'); + $response->setHeader('Content-Length', '0'); + $response->setStatus(200); + + // Sending back false will interrupt the event chain and tell the server + // we've handled this method. + return false; + } + + /** + * HTTP HEAD. + * + * This method is normally used to take a peak at a url, and only get the + * HTTP response headers, without the body. This is used by clients to + * determine if a remote file was changed, so they can use a local cached + * version, instead of downloading it again + * + * @return bool + */ + public function httpHead(RequestInterface $request, ResponseInterface $response) + { + // This is implemented by changing the HEAD request to a GET request, + // and telling the request handler that is doesn't need to create the body. + $subRequest = clone $request; + $subRequest->setMethod('GET'); + $subRequest->setHeader('X-Sabre-Original-Method', 'HEAD'); + + try { + $this->server->invokeMethod($subRequest, $response, false); + } catch (Exception\NotImplemented $e) { + // Some clients may do HEAD requests on collections, however, GET + // requests and HEAD requests _may_ not be defined on a collection, + // which would trigger a 501. + // This breaks some clients though, so we're transforming these + // 501s into 200s. + $response->setStatus(200); + $response->setBody(''); + $response->setHeader('Content-Type', 'text/plain'); + $response->setHeader('X-Sabre-Real-Status', $e->getHTTPCode()); + } + + // Sending back false will interrupt the event chain and tell the server + // we've handled this method. + return false; + } + + /** + * HTTP Delete. + * + * The HTTP delete method, deletes a given uri + */ + public function httpDelete(RequestInterface $request, ResponseInterface $response) + { + $path = $request->getPath(); + + if (!$this->server->emit('beforeUnbind', [$path])) { + return false; + } + $this->server->tree->delete($path); + $this->server->emit('afterUnbind', [$path]); + + $response->setStatus(204); + $response->setHeader('Content-Length', '0'); + + // Sending back false will interrupt the event chain and tell the server + // we've handled this method. + return false; + } + + /** + * WebDAV PROPFIND. + * + * This WebDAV method requests information about an uri resource, or a list of resources + * If a client wants to receive the properties for a single resource it will add an HTTP Depth: header with a 0 value + * If the value is 1, it means that it also expects a list of sub-resources (e.g.: files in a directory) + * + * The request body contains an XML data structure that has a list of properties the client understands + * The response body is also an xml document, containing information about every uri resource and the requested properties + * + * It has to return a HTTP 207 Multi-status status code + */ + public function httpPropFind(RequestInterface $request, ResponseInterface $response) + { + $path = $request->getPath(); + + $requestBody = $request->getBodyAsString(); + if (strlen($requestBody)) { + try { + $propFindXml = $this->server->xml->expect('{DAV:}propfind', $requestBody); + } catch (ParseException $e) { + throw new BadRequest($e->getMessage(), 0, $e); + } + } else { + $propFindXml = new Xml\Request\PropFind(); + $propFindXml->allProp = true; + $propFindXml->properties = []; + } + + $depth = $this->server->getHTTPDepth(1); + // The only two options for the depth of a propfind is 0 or 1 - as long as depth infinity is not enabled + if (!$this->server->enablePropfindDepthInfinity && 0 != $depth) { + $depth = 1; + } + + $newProperties = $this->server->getPropertiesIteratorForPath($path, $propFindXml->properties, $depth); + + // This is a multi-status response + $response->setStatus(207); + $response->setHeader('Content-Type', 'application/xml; charset=utf-8'); + $response->setHeader('Vary', 'Brief,Prefer'); + + // Normally this header is only needed for OPTIONS responses, however.. + // iCal seems to also depend on these being set for PROPFIND. Since + // this is not harmful, we'll add it. + $features = ['1', '3', 'extended-mkcol']; + foreach ($this->server->getPlugins() as $plugin) { + $features = array_merge($features, $plugin->getFeatures()); + } + $response->setHeader('DAV', implode(', ', $features)); + + $prefer = $this->server->getHTTPPrefer(); + $minimal = 'minimal' === $prefer['return']; + + $data = $this->server->generateMultiStatus($newProperties, $minimal); + $response->setBody($data); + + // Sending back false will interrupt the event chain and tell the server + // we've handled this method. + return false; + } + + /** + * WebDAV PROPPATCH. + * + * This method is called to update properties on a Node. The request is an XML body with all the mutations. + * In this XML body it is specified which properties should be set/updated and/or deleted + * + * @return bool + */ + public function httpPropPatch(RequestInterface $request, ResponseInterface $response) + { + $path = $request->getPath(); + + try { + $propPatch = $this->server->xml->expect('{DAV:}propertyupdate', $request->getBody()); + } catch (ParseException $e) { + throw new BadRequest($e->getMessage(), 0, $e); + } + $newProperties = $propPatch->properties; + + $result = $this->server->updateProperties($path, $newProperties); + + $prefer = $this->server->getHTTPPrefer(); + $response->setHeader('Vary', 'Brief,Prefer'); + + if ('minimal' === $prefer['return']) { + // If return-minimal is specified, we only have to check if the + // request was successful, and don't need to return the + // multi-status. + $ok = true; + foreach ($result as $prop => $code) { + if ((int) $code > 299) { + $ok = false; + } + } + + if ($ok) { + $response->setStatus(204); + + return false; + } + } + + $response->setStatus(207); + $response->setHeader('Content-Type', 'application/xml; charset=utf-8'); + + // Reorganizing the result for generateMultiStatus + $multiStatus = []; + foreach ($result as $propertyName => $code) { + if (isset($multiStatus[$code])) { + $multiStatus[$code][$propertyName] = null; + } else { + $multiStatus[$code] = [$propertyName => null]; + } + } + $multiStatus['href'] = $path; + + $response->setBody( + $this->server->generateMultiStatus([$multiStatus]) + ); + + // Sending back false will interrupt the event chain and tell the server + // we've handled this method. + return false; + } + + /** + * HTTP PUT method. + * + * This HTTP method updates a file, or creates a new one. + * + * If a new resource was created, a 201 Created status code should be returned. If an existing resource is updated, it's a 204 No Content + * + * @return bool + */ + public function httpPut(RequestInterface $request, ResponseInterface $response) + { + $body = $request->getBodyAsStream(); + $path = $request->getPath(); + + // Intercepting Content-Range + if ($request->getHeader('Content-Range')) { + /* + An origin server that allows PUT on a given target resource MUST send + a 400 (Bad Request) response to a PUT request that contains a + Content-Range header field. + + Reference: http://tools.ietf.org/html/rfc7231#section-4.3.4 + */ + throw new Exception\BadRequest('Content-Range on PUT requests are forbidden.'); + } + + // Intercepting the Finder problem + if (($expected = $request->getHeader('X-Expected-Entity-Length')) && $expected > 0) { + /* + Many webservers will not cooperate well with Finder PUT requests, + because it uses 'Chunked' transfer encoding for the request body. + + The symptom of this problem is that Finder sends files to the + server, but they arrive as 0-length files in PHP. + + If we don't do anything, the user might think they are uploading + files successfully, but they end up empty on the server. Instead, + we throw back an error if we detect this. + + The reason Finder uses Chunked, is because it thinks the files + might change as it's being uploaded, and therefore the + Content-Length can vary. + + Instead it sends the X-Expected-Entity-Length header with the size + of the file at the very start of the request. If this header is set, + but we don't get a request body we will fail the request to + protect the end-user. + */ + + // Only reading first byte + $firstByte = fread($body, 1); + if (1 !== strlen($firstByte)) { + throw new Exception\Forbidden('This server is not compatible with OS/X finder. Consider using a different WebDAV client or webserver.'); + } + + // The body needs to stay intact, so we copy everything to a + // temporary stream. + + $newBody = fopen('php://temp', 'r+'); + fwrite($newBody, $firstByte); + stream_copy_to_stream($body, $newBody); + rewind($newBody); + + $body = $newBody; + } + + if ($this->server->tree->nodeExists($path)) { + $node = $this->server->tree->getNodeForPath($path); + + // If the node is a collection, we'll deny it + if (!($node instanceof IFile)) { + throw new Exception\Conflict('PUT is not allowed on non-files.'); + } + if (!$this->server->updateFile($path, $body, $etag)) { + return false; + } + + $response->setHeader('Content-Length', '0'); + if ($etag) { + $response->setHeader('ETag', $etag); + } + $response->setStatus(204); + } else { + $etag = null; + // If we got here, the resource didn't exist yet. + if (!$this->server->createFile($path, $body, $etag)) { + // For one reason or another the file was not created. + return false; + } + + $response->setHeader('Content-Length', '0'); + if ($etag) { + $response->setHeader('ETag', $etag); + } + $response->setStatus(201); + } + + // Sending back false will interrupt the event chain and tell the server + // we've handled this method. + return false; + } + + /** + * WebDAV MKCOL. + * + * The MKCOL method is used to create a new collection (directory) on the server + * + * @return bool + */ + public function httpMkcol(RequestInterface $request, ResponseInterface $response) + { + $requestBody = $request->getBodyAsString(); + $path = $request->getPath(); + + if ($requestBody) { + $contentType = $request->getHeader('Content-Type'); + if (null === $contentType || (0 !== strpos($contentType, 'application/xml') && 0 !== strpos($contentType, 'text/xml'))) { + // We must throw 415 for unsupported mkcol bodies + throw new Exception\UnsupportedMediaType('The request body for the MKCOL request must have an xml Content-Type'); + } + + try { + $mkcol = $this->server->xml->expect('{DAV:}mkcol', $requestBody); + } catch (\Sabre\Xml\ParseException $e) { + throw new Exception\BadRequest($e->getMessage(), 0, $e); + } + + $properties = $mkcol->getProperties(); + + if (!isset($properties['{DAV:}resourcetype'])) { + throw new Exception\BadRequest('The mkcol request must include a {DAV:}resourcetype property'); + } + $resourceType = $properties['{DAV:}resourcetype']->getValue(); + unset($properties['{DAV:}resourcetype']); + } else { + $properties = []; + $resourceType = ['{DAV:}collection']; + } + + $mkcol = new MkCol($resourceType, $properties); + + $result = $this->server->createCollection($path, $mkcol); + + if (is_array($result)) { + $response->setStatus(207); + $response->setHeader('Content-Type', 'application/xml; charset=utf-8'); + + $response->setBody( + $this->server->generateMultiStatus([$result]) + ); + } else { + $response->setHeader('Content-Length', '0'); + $response->setStatus(201); + } + + // Sending back false will interrupt the event chain and tell the server + // we've handled this method. + return false; + } + + /** + * WebDAV HTTP MOVE method. + * + * This method moves one uri to a different uri. A lot of the actual request processing is done in getCopyMoveInfo + * + * @return bool + */ + public function httpMove(RequestInterface $request, ResponseInterface $response) + { + $path = $request->getPath(); + + $moveInfo = $this->server->getCopyAndMoveInfo($request); + + if ($moveInfo['destinationExists']) { + if (!$this->server->emit('beforeUnbind', [$moveInfo['destination']])) { + return false; + } + } + if (!$this->server->emit('beforeUnbind', [$path])) { + return false; + } + if (!$this->server->emit('beforeBind', [$moveInfo['destination']])) { + return false; + } + if (!$this->server->emit('beforeMove', [$path, $moveInfo['destination']])) { + return false; + } + + if ($moveInfo['destinationExists']) { + $this->server->tree->delete($moveInfo['destination']); + $this->server->emit('afterUnbind', [$moveInfo['destination']]); + } + + $this->server->tree->move($path, $moveInfo['destination']); + + // Its important afterMove is called before afterUnbind, because it + // allows systems to transfer data from one path to another. + // PropertyStorage uses this. If afterUnbind was first, it would clean + // up all the properties before it has a chance. + $this->server->emit('afterMove', [$path, $moveInfo['destination']]); + $this->server->emit('afterUnbind', [$path]); + $this->server->emit('afterBind', [$moveInfo['destination']]); + + // If a resource was overwritten we should send a 204, otherwise a 201 + $response->setHeader('Content-Length', '0'); + $response->setStatus($moveInfo['destinationExists'] ? 204 : 201); + + // Sending back false will interrupt the event chain and tell the server + // we've handled this method. + return false; + } + + /** + * WebDAV HTTP COPY method. + * + * This method copies one uri to a different uri, and works much like the MOVE request + * A lot of the actual request processing is done in getCopyMoveInfo + * + * @return bool + */ + public function httpCopy(RequestInterface $request, ResponseInterface $response) + { + $path = $request->getPath(); + + $copyInfo = $this->server->getCopyAndMoveInfo($request); + + if (!$this->server->emit('beforeBind', [$copyInfo['destination']])) { + return false; + } + if (!$this->server->emit('beforeCopy', [$path, $copyInfo['destination']])) { + return false; + } + + if ($copyInfo['destinationExists']) { + if (!$this->server->emit('beforeUnbind', [$copyInfo['destination']])) { + return false; + } + $this->server->tree->delete($copyInfo['destination']); + } + + $this->server->tree->copy($path, $copyInfo['destination']); + $this->server->emit('afterCopy', [$path, $copyInfo['destination']]); + $this->server->emit('afterBind', [$copyInfo['destination']]); + + // If a resource was overwritten we should send a 204, otherwise a 201 + $response->setHeader('Content-Length', '0'); + $response->setStatus($copyInfo['destinationExists'] ? 204 : 201); + + // Sending back false will interrupt the event chain and tell the server + // we've handled this method. + return false; + } + + /** + * HTTP REPORT method implementation. + * + * Although the REPORT method is not part of the standard WebDAV spec (it's from rfc3253) + * It's used in a lot of extensions, so it made sense to implement it into the core. + * + * @return bool + */ + public function httpReport(RequestInterface $request, ResponseInterface $response) + { + $path = $request->getPath(); + + $result = $this->server->xml->parse( + $request->getBody(), + $request->getUrl(), + $rootElementName + ); + + if ($this->server->emit('report', [$rootElementName, $result, $path])) { + // If emit returned true, it means the report was not supported + throw new Exception\ReportNotSupported(); + } + + // Sending back false will interrupt the event chain and tell the server + // we've handled this method. + return false; + } + + /** + * This method is called during property updates. + * + * Here we check if a user attempted to update a protected property and + * ensure that the process fails if this is the case. + * + * @param string $path + */ + public function propPatchProtectedPropertyCheck($path, PropPatch $propPatch) + { + // Comparing the mutation list to the list of protected properties. + $mutations = $propPatch->getMutations(); + + $protected = array_intersect( + $this->server->protectedProperties, + array_keys($mutations) + ); + + if ($protected) { + $propPatch->setResultCode($protected, 403); + } + } + + /** + * This method is called during property updates. + * + * Here we check if a node implements IProperties and let the node handle + * updating of (some) properties. + * + * @param string $path + */ + public function propPatchNodeUpdate($path, PropPatch $propPatch) + { + // This should trigger a 404 if the node doesn't exist. + $node = $this->server->tree->getNodeForPath($path); + + if ($node instanceof IProperties) { + $node->propPatch($propPatch); + } + } + + /** + * This method is called when properties are retrieved. + * + * Here we add all the default properties. + */ + public function propFind(PropFind $propFind, INode $node) + { + $propFind->handle('{DAV:}getlastmodified', function () use ($node) { + $lm = $node->getLastModified(); + if ($lm) { + return new Xml\Property\GetLastModified($lm); + } + }); + + if ($node instanceof IFile) { + $propFind->handle('{DAV:}getcontentlength', [$node, 'getSize']); + $propFind->handle('{DAV:}getetag', [$node, 'getETag']); + $propFind->handle('{DAV:}getcontenttype', [$node, 'getContentType']); + } + + if ($node instanceof IQuota) { + $quotaInfo = null; + $propFind->handle('{DAV:}quota-used-bytes', function () use (&$quotaInfo, $node) { + $quotaInfo = $node->getQuotaInfo(); + + return $quotaInfo[0]; + }); + $propFind->handle('{DAV:}quota-available-bytes', function () use (&$quotaInfo, $node) { + if (!$quotaInfo) { + $quotaInfo = $node->getQuotaInfo(); + } + + return $quotaInfo[1]; + }); + } + + $propFind->handle('{DAV:}supported-report-set', function () use ($propFind) { + $reports = []; + foreach ($this->server->getPlugins() as $plugin) { + $reports = array_merge($reports, $plugin->getSupportedReportSet($propFind->getPath())); + } + + return new Xml\Property\SupportedReportSet($reports); + }); + $propFind->handle('{DAV:}resourcetype', function () use ($node) { + return new Xml\Property\ResourceType($this->server->getResourceTypeForNode($node)); + }); + $propFind->handle('{DAV:}supported-method-set', function () use ($propFind) { + return new Xml\Property\SupportedMethodSet( + $this->server->getAllowedMethods($propFind->getPath()) + ); + }); + } + + /** + * Fetches properties for a node. + * + * This event is called a bit later, so plugins have a chance first to + * populate the result. + */ + public function propFindNode(PropFind $propFind, INode $node) + { + if ($node instanceof IProperties && $propertyNames = $propFind->get404Properties()) { + $nodeProperties = $node->getProperties($propertyNames); + foreach ($nodeProperties as $propertyName => $propertyValue) { + $propFind->set($propertyName, $propertyValue, 200); + } + } + } + + /** + * This method is called when properties are retrieved. + * + * This specific handler is called very late in the process, because we + * want other systems to first have a chance to handle the properties. + */ + public function propFindLate(PropFind $propFind, INode $node) + { + $propFind->handle('{http://calendarserver.org/ns/}getctag', function () use ($propFind) { + // If we already have a sync-token from the current propFind + // request, we can re-use that. + $val = $propFind->get('{http://sabredav.org/ns}sync-token'); + if ($val) { + return $val; + } + + $val = $propFind->get('{DAV:}sync-token'); + if ($val && is_scalar($val)) { + return $val; + } + if ($val && $val instanceof Xml\Property\Href) { + return substr($val->getHref(), strlen(Sync\Plugin::SYNCTOKEN_PREFIX)); + } + + // If we got here, the earlier two properties may simply not have + // been part of the earlier request. We're going to fetch them. + $result = $this->server->getProperties($propFind->getPath(), [ + '{http://sabredav.org/ns}sync-token', + '{DAV:}sync-token', + ]); + + if (isset($result['{http://sabredav.org/ns}sync-token'])) { + return $result['{http://sabredav.org/ns}sync-token']; + } + if (isset($result['{DAV:}sync-token'])) { + $val = $result['{DAV:}sync-token']; + if (is_scalar($val)) { + return $val; + } elseif ($val instanceof Xml\Property\Href) { + return substr($val->getHref(), strlen(Sync\Plugin::SYNCTOKEN_PREFIX)); + } + } + }); + } + + /** + * Listens for exception events, and automatically logs them. + * + * @param Exception $e + */ + public function exception($e) + { + $logLevel = \Psr\Log\LogLevel::CRITICAL; + if ($e instanceof \Sabre\DAV\Exception) { + // If it's a standard sabre/dav exception, it means we have a http + // status code available. + $code = $e->getHTTPCode(); + + if ($code >= 400 && $code < 500) { + // user error + $logLevel = \Psr\Log\LogLevel::INFO; + } else { + // Server-side error. We mark it's as an error, but it's not + // critical. + $logLevel = \Psr\Log\LogLevel::ERROR; + } + } + + $this->server->getLogger()->log( + $logLevel, + 'Uncaught exception', + [ + 'exception' => $e, + ] + ); + } + + /** + * Returns a bunch of meta-data about the plugin. + * + * Providing this information is optional, and is mainly displayed by the + * Browser plugin. + * + * The description key in the returned array may contain html and will not + * be sanitized. + * + * @return array + */ + public function getPluginInfo() + { + return [ + 'name' => $this->getPluginName(), + 'description' => 'The Core plugin provides a lot of the basic functionality required by WebDAV, such as a default implementation for all HTTP and WebDAV methods.', + 'link' => null, + ]; + } +} diff --git a/3rdparty/sabre/dav/lib/DAV/Exception.php b/3rdparty/sabre/dav/lib/DAV/Exception.php new file mode 100644 index 00000000..9fc1d16b --- /dev/null +++ b/3rdparty/sabre/dav/lib/DAV/Exception.php @@ -0,0 +1,50 @@ +lock) { + $error = $errorNode->ownerDocument->createElementNS('DAV:', 'd:no-conflicting-lock'); + $errorNode->appendChild($error); + $error->appendChild($errorNode->ownerDocument->createElementNS('DAV:', 'd:href', $this->lock->uri)); + } + } +} diff --git a/3rdparty/sabre/dav/lib/DAV/Exception/Forbidden.php b/3rdparty/sabre/dav/lib/DAV/Exception/Forbidden.php new file mode 100644 index 00000000..2f882c39 --- /dev/null +++ b/3rdparty/sabre/dav/lib/DAV/Exception/Forbidden.php @@ -0,0 +1,29 @@ +ownerDocument->createElementNS('DAV:', 'd:valid-resourcetype'); + $errorNode->appendChild($error); + } +} diff --git a/3rdparty/sabre/dav/lib/DAV/Exception/InvalidSyncToken.php b/3rdparty/sabre/dav/lib/DAV/Exception/InvalidSyncToken.php new file mode 100644 index 00000000..f28d20f4 --- /dev/null +++ b/3rdparty/sabre/dav/lib/DAV/Exception/InvalidSyncToken.php @@ -0,0 +1,34 @@ +ownerDocument->createElementNS('DAV:', 'd:valid-sync-token'); + $errorNode->appendChild($error); + } +} diff --git a/3rdparty/sabre/dav/lib/DAV/Exception/LengthRequired.php b/3rdparty/sabre/dav/lib/DAV/Exception/LengthRequired.php new file mode 100644 index 00000000..9d26fcb1 --- /dev/null +++ b/3rdparty/sabre/dav/lib/DAV/Exception/LengthRequired.php @@ -0,0 +1,30 @@ +ownerDocument->createElementNS('DAV:', 'd:lock-token-matches-request-uri'); + $errorNode->appendChild($error); + } +} diff --git a/3rdparty/sabre/dav/lib/DAV/Exception/Locked.php b/3rdparty/sabre/dav/lib/DAV/Exception/Locked.php new file mode 100644 index 00000000..24fad709 --- /dev/null +++ b/3rdparty/sabre/dav/lib/DAV/Exception/Locked.php @@ -0,0 +1,68 @@ +lock = $lock; + } + + /** + * Returns the HTTP statuscode for this exception. + * + * @return int + */ + public function getHTTPCode() + { + return 423; + } + + /** + * This method allows the exception to include additional information into the WebDAV error response. + */ + public function serialize(DAV\Server $server, \DOMElement $errorNode) + { + if ($this->lock) { + $error = $errorNode->ownerDocument->createElementNS('DAV:', 'd:lock-token-submitted'); + $errorNode->appendChild($error); + + $href = $errorNode->ownerDocument->createElementNS('DAV:', 'd:href'); + $href->appendChild($errorNode->ownerDocument->createTextNode($this->lock->uri)); + $error->appendChild( + $href + ); + } + } +} diff --git a/3rdparty/sabre/dav/lib/DAV/Exception/MethodNotAllowed.php b/3rdparty/sabre/dav/lib/DAV/Exception/MethodNotAllowed.php new file mode 100644 index 00000000..dbf42ed9 --- /dev/null +++ b/3rdparty/sabre/dav/lib/DAV/Exception/MethodNotAllowed.php @@ -0,0 +1,46 @@ +getAllowedMethods($server->getRequestUri()); + + return [ + 'Allow' => strtoupper(implode(', ', $methods)), + ]; + } +} diff --git a/3rdparty/sabre/dav/lib/DAV/Exception/NotAuthenticated.php b/3rdparty/sabre/dav/lib/DAV/Exception/NotAuthenticated.php new file mode 100644 index 00000000..0a5ba9b5 --- /dev/null +++ b/3rdparty/sabre/dav/lib/DAV/Exception/NotAuthenticated.php @@ -0,0 +1,30 @@ +header = $header; + } + + /** + * Returns the HTTP statuscode for this exception. + * + * @return int + */ + public function getHTTPCode() + { + return 412; + } + + /** + * This method allows the exception to include additional information into the WebDAV error response. + */ + public function serialize(DAV\Server $server, \DOMElement $errorNode) + { + if ($this->header) { + $prop = $errorNode->ownerDocument->createElement('s:header'); + $prop->nodeValue = $this->header; + $errorNode->appendChild($prop); + } + } +} diff --git a/3rdparty/sabre/dav/lib/DAV/Exception/ReportNotSupported.php b/3rdparty/sabre/dav/lib/DAV/Exception/ReportNotSupported.php new file mode 100644 index 00000000..a483838e --- /dev/null +++ b/3rdparty/sabre/dav/lib/DAV/Exception/ReportNotSupported.php @@ -0,0 +1,28 @@ +ownerDocument->createElementNS('DAV:', 'd:supported-report'); + $errorNode->appendChild($error); + } +} diff --git a/3rdparty/sabre/dav/lib/DAV/Exception/RequestedRangeNotSatisfiable.php b/3rdparty/sabre/dav/lib/DAV/Exception/RequestedRangeNotSatisfiable.php new file mode 100644 index 00000000..6ccb5b8c --- /dev/null +++ b/3rdparty/sabre/dav/lib/DAV/Exception/RequestedRangeNotSatisfiable.php @@ -0,0 +1,30 @@ + + * @copyright Copyright (C) fruux GmbH (https://fruux.com/) + * @license http://sabre.io/license/ Modified BSD License + */ +class ServiceUnavailable extends DAV\Exception +{ + /** + * Returns the HTTP statuscode for this exception. + * + * @return int + */ + public function getHTTPCode() + { + return 503; + } +} diff --git a/3rdparty/sabre/dav/lib/DAV/Exception/TooManyMatches.php b/3rdparty/sabre/dav/lib/DAV/Exception/TooManyMatches.php new file mode 100644 index 00000000..ef6f5024 --- /dev/null +++ b/3rdparty/sabre/dav/lib/DAV/Exception/TooManyMatches.php @@ -0,0 +1,34 @@ +ownerDocument->createElementNS('DAV:', 'd:number-of-matches-within-limits'); + $errorNode->appendChild($error); + } +} diff --git a/3rdparty/sabre/dav/lib/DAV/Exception/UnsupportedMediaType.php b/3rdparty/sabre/dav/lib/DAV/Exception/UnsupportedMediaType.php new file mode 100644 index 00000000..bc9da30d --- /dev/null +++ b/3rdparty/sabre/dav/lib/DAV/Exception/UnsupportedMediaType.php @@ -0,0 +1,30 @@ +path.'/'.$name; + file_put_contents($newPath, $data); + clearstatcache(true, $newPath); + } + + /** + * Creates a new subdirectory. + * + * @param string $name + */ + public function createDirectory($name) + { + $newPath = $this->path.'/'.$name; + mkdir($newPath); + clearstatcache(true, $newPath); + } + + /** + * Returns a specific child node, referenced by its name. + * + * This method must throw DAV\Exception\NotFound if the node does not + * exist. + * + * @param string $name + * + * @throws DAV\Exception\NotFound + * + * @return DAV\INode + */ + public function getChild($name) + { + $path = $this->path.'/'.$name; + + if (!file_exists($path)) { + throw new DAV\Exception\NotFound('File with name '.$path.' could not be located'); + } + if (is_dir($path)) { + return new self($path); + } else { + return new File($path); + } + } + + /** + * Returns an array with all the child nodes. + * + * @return DAV\INode[] + */ + public function getChildren() + { + $nodes = []; + $iterator = new \FilesystemIterator( + $this->path, + \FilesystemIterator::CURRENT_AS_SELF + | \FilesystemIterator::SKIP_DOTS + ); + foreach ($iterator as $entry) { + $nodes[] = $this->getChild($entry->getFilename()); + } + + return $nodes; + } + + /** + * Checks if a child exists. + * + * @param string $name + * + * @return bool + */ + public function childExists($name) + { + $path = $this->path.'/'.$name; + + return file_exists($path); + } + + /** + * Deletes all files in this directory, and then itself. + */ + public function delete() + { + foreach ($this->getChildren() as $child) { + $child->delete(); + } + rmdir($this->path); + } + + /** + * Returns available diskspace information. + * + * @return array + */ + public function getQuotaInfo() + { + $absolute = realpath($this->path); + + return [ + disk_total_space($absolute) - disk_free_space($absolute), + disk_free_space($absolute), + ]; + } +} diff --git a/3rdparty/sabre/dav/lib/DAV/FS/File.php b/3rdparty/sabre/dav/lib/DAV/FS/File.php new file mode 100644 index 00000000..b78a8013 --- /dev/null +++ b/3rdparty/sabre/dav/lib/DAV/FS/File.php @@ -0,0 +1,87 @@ +path, $data); + clearstatcache(true, $this->path); + } + + /** + * Returns the data. + * + * @return resource + */ + public function get() + { + return fopen($this->path, 'r'); + } + + /** + * Delete the current file. + */ + public function delete() + { + unlink($this->path); + } + + /** + * Returns the size of the node, in bytes. + * + * @return int + */ + public function getSize() + { + return filesize($this->path); + } + + /** + * Returns the ETag for a file. + * + * An ETag is a unique identifier representing the current version of the file. If the file changes, the ETag MUST change. + * The ETag is an arbitrary string, but MUST be surrounded by double-quotes. + * + * Return null if the ETag can not effectively be determined + * + * @return mixed + */ + public function getETag() + { + return '"'.sha1( + fileinode($this->path). + filesize($this->path). + filemtime($this->path) + ).'"'; + } + + /** + * Returns the mime-type for a file. + * + * If null is returned, we'll assume application/octet-stream + * + * @return mixed + */ + public function getContentType() + { + return null; + } +} diff --git a/3rdparty/sabre/dav/lib/DAV/FS/Node.php b/3rdparty/sabre/dav/lib/DAV/FS/Node.php new file mode 100644 index 00000000..32aa7475 --- /dev/null +++ b/3rdparty/sabre/dav/lib/DAV/FS/Node.php @@ -0,0 +1,96 @@ +path = $path; + $this->overrideName = $overrideName; + } + + /** + * Returns the name of the node. + * + * @return string + */ + public function getName() + { + if ($this->overrideName) { + return $this->overrideName; + } + + list(, $name) = Uri\split($this->path); + + return $name; + } + + /** + * Renames the node. + * + * @param string $name The new name + */ + public function setName($name) + { + if ($this->overrideName) { + throw new Forbidden('This node cannot be renamed'); + } + + list($parentPath) = Uri\split($this->path); + list(, $newName) = Uri\split($name); + + $newPath = $parentPath.'/'.$newName; + rename($this->path, $newPath); + + $this->path = $newPath; + } + + /** + * Returns the last modification time, as a unix timestamp. + * + * @return int + */ + public function getLastModified() + { + return filemtime($this->path); + } +} diff --git a/3rdparty/sabre/dav/lib/DAV/FSExt/Directory.php b/3rdparty/sabre/dav/lib/DAV/FSExt/Directory.php new file mode 100644 index 00000000..d6aea009 --- /dev/null +++ b/3rdparty/sabre/dav/lib/DAV/FSExt/Directory.php @@ -0,0 +1,212 @@ +path.'/'.$name; + file_put_contents($newPath, $data); + clearstatcache(true, $newPath); + + return '"'.sha1( + fileinode($newPath). + filesize($newPath). + filemtime($newPath) + ).'"'; + } + + /** + * Creates a new subdirectory. + * + * @param string $name + */ + public function createDirectory($name) + { + // We're not allowing dots + if ('.' == $name || '..' == $name) { + throw new DAV\Exception\Forbidden('Permission denied to . and ..'); + } + $newPath = $this->path.'/'.$name; + mkdir($newPath); + clearstatcache(true, $newPath); + } + + /** + * Returns a specific child node, referenced by its name. + * + * This method must throw Sabre\DAV\Exception\NotFound if the node does not + * exist. + * + * @param string $name + * + * @throws DAV\Exception\NotFound + * + * @return DAV\INode + */ + public function getChild($name) + { + $path = $this->path.'/'.$name; + + if (!file_exists($path)) { + throw new DAV\Exception\NotFound('File could not be located'); + } + if ('.' == $name || '..' == $name) { + throw new DAV\Exception\Forbidden('Permission denied to . and ..'); + } + if (is_dir($path)) { + return new self($path); + } else { + return new File($path); + } + } + + /** + * Checks if a child exists. + * + * @param string $name + * + * @return bool + */ + public function childExists($name) + { + if ('.' == $name || '..' == $name) { + throw new DAV\Exception\Forbidden('Permission denied to . and ..'); + } + $path = $this->path.'/'.$name; + + return file_exists($path); + } + + /** + * Returns an array with all the child nodes. + * + * @return DAV\INode[] + */ + public function getChildren() + { + $nodes = []; + $iterator = new \FilesystemIterator( + $this->path, + \FilesystemIterator::CURRENT_AS_SELF + | \FilesystemIterator::SKIP_DOTS + ); + + foreach ($iterator as $entry) { + $nodes[] = $this->getChild($entry->getFilename()); + } + + return $nodes; + } + + /** + * Deletes all files in this directory, and then itself. + * + * @return bool + */ + public function delete() + { + // Deleting all children + foreach ($this->getChildren() as $child) { + $child->delete(); + } + + // Removing the directory itself + rmdir($this->path); + + return true; + } + + /** + * Returns available diskspace information. + * + * @return array + */ + public function getQuotaInfo() + { + $total = disk_total_space(realpath($this->path)); + $free = disk_free_space(realpath($this->path)); + + return [ + $total - $free, + $free, + ]; + } + + /** + * Moves a node into this collection. + * + * It is up to the implementors to: + * 1. Create the new resource. + * 2. Remove the old resource. + * 3. Transfer any properties or other data. + * + * Generally you should make very sure that your collection can easily move + * the move. + * + * If you don't, just return false, which will trigger sabre/dav to handle + * the move itself. If you return true from this function, the assumption + * is that the move was successful. + * + * @param string $targetName new local file/collection name + * @param string $sourcePath Full path to source node + * @param DAV\INode $sourceNode Source node itself + * + * @return bool + */ + public function moveInto($targetName, $sourcePath, DAV\INode $sourceNode) + { + // We only support FSExt\Directory or FSExt\File objects, so + // anything else we want to quickly reject. + if (!$sourceNode instanceof self && !$sourceNode instanceof File) { + return false; + } + + // PHP allows us to access protected properties from other objects, as + // long as they are defined in a class that has a shared inheritance + // with the current class. + return rename($sourceNode->path, $this->path.'/'.$targetName); + } +} diff --git a/3rdparty/sabre/dav/lib/DAV/FSExt/File.php b/3rdparty/sabre/dav/lib/DAV/FSExt/File.php new file mode 100644 index 00000000..74849b56 --- /dev/null +++ b/3rdparty/sabre/dav/lib/DAV/FSExt/File.php @@ -0,0 +1,153 @@ +path, $data); + clearstatcache(true, $this->path); + + return $this->getETag(); + } + + /** + * Updates the file based on a range specification. + * + * The first argument is the data, which is either a readable stream + * resource or a string. + * + * The second argument is the type of update we're doing. + * This is either: + * * 1. append (default) + * * 2. update based on a start byte + * * 3. update based on an end byte + *; + * The third argument is the start or end byte. + * + * After a successful put operation, you may choose to return an ETag. The + * ETAG must always be surrounded by double-quotes. These quotes must + * appear in the actual string you're returning. + * + * Clients may use the ETag from a PUT request to later on make sure that + * when they update the file, the contents haven't changed in the mean + * time. + * + * @param resource|string $data + * @param int $rangeType + * @param int $offset + * + * @return string|null + */ + public function patch($data, $rangeType, $offset = null) + { + switch ($rangeType) { + case 1: + $f = fopen($this->path, 'a'); + break; + case 2: + $f = fopen($this->path, 'c'); + fseek($f, $offset); + break; + case 3: + $f = fopen($this->path, 'c'); + fseek($f, $offset, SEEK_END); + break; + default: + $f = fopen($this->path, 'a'); + break; + } + if (is_string($data)) { + fwrite($f, $data); + } else { + stream_copy_to_stream($data, $f); + } + fclose($f); + clearstatcache(true, $this->path); + + return $this->getETag(); + } + + /** + * Returns the data. + * + * @return resource + */ + public function get() + { + return fopen($this->path, 'r'); + } + + /** + * Delete the current file. + * + * @return bool + */ + public function delete() + { + return unlink($this->path); + } + + /** + * Returns the ETag for a file. + * + * An ETag is a unique identifier representing the current version of the file. If the file changes, the ETag MUST change. + * The ETag is an arbitrary string, but MUST be surrounded by double-quotes. + * + * Return null if the ETag can not effectively be determined + * + * @return string|null + */ + public function getETag() + { + return '"'.sha1( + fileinode($this->path). + filesize($this->path). + filemtime($this->path) + ).'"'; + } + + /** + * Returns the mime-type for a file. + * + * If null is returned, we'll assume application/octet-stream + * + * @return string|null + */ + public function getContentType() + { + return null; + } + + /** + * Returns the size of the file, in bytes. + * + * @return int + */ + public function getSize() + { + return filesize($this->path); + } +} diff --git a/3rdparty/sabre/dav/lib/DAV/File.php b/3rdparty/sabre/dav/lib/DAV/File.php new file mode 100644 index 00000000..daf83aa4 --- /dev/null +++ b/3rdparty/sabre/dav/lib/DAV/File.php @@ -0,0 +1,93 @@ +locksFile = $locksFile; + } + + /** + * Returns a list of Sabre\DAV\Locks\LockInfo objects. + * + * This method should return all the locks for a particular uri, including + * locks that might be set on a parent uri. + * + * If returnChildLocks is set to true, this method should also look for + * any locks in the subtree of the uri for locks. + * + * @param string $uri + * @param bool $returnChildLocks + * + * @return array + */ + public function getLocks($uri, $returnChildLocks) + { + $newLocks = []; + + $locks = $this->getData(); + + foreach ($locks as $lock) { + if ($lock->uri === $uri || + //deep locks on parents + (0 != $lock->depth && 0 === strpos($uri, $lock->uri.'/')) || + + // locks on children + ($returnChildLocks && (0 === strpos($lock->uri, $uri.'/')))) { + $newLocks[] = $lock; + } + } + + // Checking if we can remove any of these locks + foreach ($newLocks as $k => $lock) { + if (time() > $lock->timeout + $lock->created) { + unset($newLocks[$k]); + } + } + + return $newLocks; + } + + /** + * Locks a uri. + * + * @param string $uri + * + * @return bool + */ + public function lock($uri, LockInfo $lockInfo) + { + // We're making the lock timeout 30 minutes + $lockInfo->timeout = 1800; + $lockInfo->created = time(); + $lockInfo->uri = $uri; + + $locks = $this->getData(); + + foreach ($locks as $k => $lock) { + if ( + ($lock->token == $lockInfo->token) || + (time() > $lock->timeout + $lock->created) + ) { + unset($locks[$k]); + } + } + $locks[] = $lockInfo; + $this->putData($locks); + + return true; + } + + /** + * Removes a lock from a uri. + * + * @param string $uri + * + * @return bool + */ + public function unlock($uri, LockInfo $lockInfo) + { + $locks = $this->getData(); + foreach ($locks as $k => $lock) { + if ($lock->token == $lockInfo->token) { + unset($locks[$k]); + $this->putData($locks); + + return true; + } + } + + return false; + } + + /** + * Loads the lockdata from the filesystem. + * + * @return array + */ + protected function getData() + { + if (!file_exists($this->locksFile)) { + return []; + } + + // opening up the file, and creating a shared lock + $handle = fopen($this->locksFile, 'r'); + flock($handle, LOCK_SH); + + // Reading data until the eof + $data = stream_get_contents($handle); + + // We're all good + flock($handle, LOCK_UN); + fclose($handle); + + // Unserializing and checking if the resource file contains data for this file + $data = unserialize($data); + if (!$data) { + return []; + } + + return $data; + } + + /** + * Saves the lockdata. + */ + protected function putData(array $newData) + { + // opening up the file, and creating an exclusive lock + $handle = fopen($this->locksFile, 'a+'); + flock($handle, LOCK_EX); + + // We can only truncate and rewind once the lock is acquired. + ftruncate($handle, 0); + rewind($handle); + + fwrite($handle, serialize($newData)); + flock($handle, LOCK_UN); + fclose($handle); + } +} diff --git a/3rdparty/sabre/dav/lib/DAV/Locks/Backend/PDO.php b/3rdparty/sabre/dav/lib/DAV/Locks/Backend/PDO.php new file mode 100644 index 00000000..3f425f98 --- /dev/null +++ b/3rdparty/sabre/dav/lib/DAV/Locks/Backend/PDO.php @@ -0,0 +1,172 @@ +pdo = $pdo; + } + + /** + * Returns a list of Sabre\DAV\Locks\LockInfo objects. + * + * This method should return all the locks for a particular uri, including + * locks that might be set on a parent uri. + * + * If returnChildLocks is set to true, this method should also look for + * any locks in the subtree of the uri for locks. + * + * @param string $uri + * @param bool $returnChildLocks + * + * @return array + */ + public function getLocks($uri, $returnChildLocks) + { + // NOTE: the following 10 lines or so could be easily replaced by + // pure sql. MySQL's non-standard string concatenation prevents us + // from doing this though. + $query = 'SELECT owner, token, timeout, created, scope, depth, uri FROM '.$this->tableName.' WHERE (created > (? - timeout)) AND ((uri = ?)'; + $params = [time(), $uri]; + + // We need to check locks for every part in the uri. + $uriParts = explode('/', $uri); + + // We already covered the last part of the uri + array_pop($uriParts); + + $currentPath = ''; + + foreach ($uriParts as $part) { + if ($currentPath) { + $currentPath .= '/'; + } + $currentPath .= $part; + + $query .= ' OR (depth!=0 AND uri = ?)'; + $params[] = $currentPath; + } + + if ($returnChildLocks) { + $query .= ' OR (uri LIKE ?)'; + $params[] = $uri.'/%'; + } + $query .= ')'; + + $stmt = $this->pdo->prepare($query); + $stmt->execute($params); + $result = $stmt->fetchAll(); + + $lockList = []; + foreach ($result as $row) { + $lockInfo = new LockInfo(); + $lockInfo->owner = $row['owner']; + $lockInfo->token = $row['token']; + $lockInfo->timeout = $row['timeout']; + $lockInfo->created = $row['created']; + $lockInfo->scope = $row['scope']; + $lockInfo->depth = $row['depth']; + $lockInfo->uri = $row['uri']; + $lockList[] = $lockInfo; + } + + return $lockList; + } + + /** + * Locks a uri. + * + * @param string $uri + * + * @return bool + */ + public function lock($uri, LockInfo $lockInfo) + { + // We're making the lock timeout 30 minutes + $lockInfo->timeout = 30 * 60; + $lockInfo->created = time(); + $lockInfo->uri = $uri; + + $locks = $this->getLocks($uri, false); + $exists = false; + foreach ($locks as $lock) { + if ($lock->token == $lockInfo->token) { + $exists = true; + } + } + + if ($exists) { + $stmt = $this->pdo->prepare('UPDATE '.$this->tableName.' SET owner = ?, timeout = ?, scope = ?, depth = ?, uri = ?, created = ? WHERE token = ?'); + $stmt->execute([ + $lockInfo->owner, + $lockInfo->timeout, + $lockInfo->scope, + $lockInfo->depth, + $uri, + $lockInfo->created, + $lockInfo->token, + ]); + } else { + $stmt = $this->pdo->prepare('INSERT INTO '.$this->tableName.' (owner,timeout,scope,depth,uri,created,token) VALUES (?,?,?,?,?,?,?)'); + $stmt->execute([ + $lockInfo->owner, + $lockInfo->timeout, + $lockInfo->scope, + $lockInfo->depth, + $uri, + $lockInfo->created, + $lockInfo->token, + ]); + } + + return true; + } + + /** + * Removes a lock from a uri. + * + * @param string $uri + * + * @return bool + */ + public function unlock($uri, LockInfo $lockInfo) + { + $stmt = $this->pdo->prepare('DELETE FROM '.$this->tableName.' WHERE uri = ? AND token = ?'); + $stmt->execute([$uri, $lockInfo->token]); + + return 1 === $stmt->rowCount(); + } +} diff --git a/3rdparty/sabre/dav/lib/DAV/Locks/LockInfo.php b/3rdparty/sabre/dav/lib/DAV/Locks/LockInfo.php new file mode 100644 index 00000000..df822756 --- /dev/null +++ b/3rdparty/sabre/dav/lib/DAV/Locks/LockInfo.php @@ -0,0 +1,82 @@ +addPlugin($lockPlugin); + * + * @copyright Copyright (C) fruux GmbH (https://fruux.com/) + * @author Evert Pot (http://evertpot.com/) + * @license http://sabre.io/license/ Modified BSD License + */ +class Plugin extends DAV\ServerPlugin +{ + /** + * locksBackend. + * + * @var Backend\BackendInterface + */ + protected $locksBackend; + + /** + * server. + * + * @var DAV\Server + */ + protected $server; + + /** + * __construct. + */ + public function __construct(Backend\BackendInterface $locksBackend) + { + $this->locksBackend = $locksBackend; + } + + /** + * Initializes the plugin. + * + * This method is automatically called by the Server class after addPlugin. + */ + public function initialize(DAV\Server $server) + { + $this->server = $server; + + $this->server->xml->elementMap['{DAV:}lockinfo'] = 'Sabre\\DAV\\Xml\\Request\\Lock'; + + $server->on('method:LOCK', [$this, 'httpLock']); + $server->on('method:UNLOCK', [$this, 'httpUnlock']); + $server->on('validateTokens', [$this, 'validateTokens']); + $server->on('propFind', [$this, 'propFind']); + $server->on('afterUnbind', [$this, 'afterUnbind']); + } + + /** + * Returns a plugin name. + * + * Using this name other plugins will be able to access other plugins + * using Sabre\DAV\Server::getPlugin + * + * @return string + */ + public function getPluginName() + { + return 'locks'; + } + + /** + * This method is called after most properties have been found + * it allows us to add in any Lock-related properties. + */ + public function propFind(DAV\PropFind $propFind, DAV\INode $node) + { + $propFind->handle('{DAV:}supportedlock', function () { + return new DAV\Xml\Property\SupportedLock(); + }); + $propFind->handle('{DAV:}lockdiscovery', function () use ($propFind) { + return new DAV\Xml\Property\LockDiscovery( + $this->getLocks($propFind->getPath()) + ); + }); + } + + /** + * Use this method to tell the server this plugin defines additional + * HTTP methods. + * + * This method is passed a uri. It should only return HTTP methods that are + * available for the specified uri. + * + * @param string $uri + * + * @return array + */ + public function getHTTPMethods($uri) + { + return ['LOCK', 'UNLOCK']; + } + + /** + * Returns a list of features for the HTTP OPTIONS Dav: header. + * + * In this case this is only the number 2. The 2 in the Dav: header + * indicates the server supports locks. + * + * @return array + */ + public function getFeatures() + { + return [2]; + } + + /** + * Returns all lock information on a particular uri. + * + * This function should return an array with Sabre\DAV\Locks\LockInfo objects. If there are no locks on a file, return an empty array. + * + * Additionally there is also the possibility of locks on parent nodes, so we'll need to traverse every part of the tree + * If the $returnChildLocks argument is set to true, we'll also traverse all the children of the object + * for any possible locks and return those as well. + * + * @param string $uri + * @param bool $returnChildLocks + * + * @return array + */ + public function getLocks($uri, $returnChildLocks = false) + { + return $this->locksBackend->getLocks($uri, $returnChildLocks); + } + + /** + * Locks an uri. + * + * The WebDAV lock request can be operated to either create a new lock on a file, or to refresh an existing lock + * If a new lock is created, a full XML body should be supplied, containing information about the lock such as the type + * of lock (shared or exclusive) and the owner of the lock + * + * If a lock is to be refreshed, no body should be supplied and there should be a valid If header containing the lock + * + * Additionally, a lock can be requested for a non-existent file. In these case we're obligated to create an empty file as per RFC4918:S7.3 + * + * @return bool + */ + public function httpLock(RequestInterface $request, ResponseInterface $response) + { + $uri = $request->getPath(); + + $existingLocks = $this->getLocks($uri); + + if ($body = $request->getBodyAsString()) { + // This is a new lock request + + $existingLock = null; + // Checking if there's already non-shared locks on the uri. + foreach ($existingLocks as $existingLock) { + if (LockInfo::EXCLUSIVE === $existingLock->scope) { + throw new DAV\Exception\ConflictingLock($existingLock); + } + } + + $lockInfo = $this->parseLockRequest($body); + $lockInfo->depth = $this->server->getHTTPDepth(); + $lockInfo->uri = $uri; + if ($existingLock && LockInfo::SHARED != $lockInfo->scope) { + throw new DAV\Exception\ConflictingLock($existingLock); + } + } else { + // Gonna check if this was a lock refresh. + $existingLocks = $this->getLocks($uri); + $conditions = $this->server->getIfConditions($request); + $found = null; + + foreach ($existingLocks as $existingLock) { + foreach ($conditions as $condition) { + foreach ($condition['tokens'] as $token) { + if ($token['token'] === 'opaquelocktoken:'.$existingLock->token) { + $found = $existingLock; + break 3; + } + } + } + } + + // If none were found, this request is in error. + if (is_null($found)) { + if ($existingLocks) { + throw new DAV\Exception\Locked(reset($existingLocks)); + } else { + throw new DAV\Exception\BadRequest('An xml body is required for lock requests'); + } + } + + // This must have been a lock refresh + $lockInfo = $found; + + // The resource could have been locked through another uri. + if ($uri != $lockInfo->uri) { + $uri = $lockInfo->uri; + } + } + + if ($timeout = $this->getTimeoutHeader()) { + $lockInfo->timeout = $timeout; + } + + $newFile = false; + + // If we got this far.. we should go check if this node actually exists. If this is not the case, we need to create it first + try { + $this->server->tree->getNodeForPath($uri); + + // We need to call the beforeWriteContent event for RFC3744 + // Edit: looks like this is not used, and causing problems now. + // + // See Issue 222 + // $this->server->emit('beforeWriteContent',array($uri)); + } catch (DAV\Exception\NotFound $e) { + // It didn't, lets create it + $this->server->createFile($uri, fopen('php://memory', 'r')); + $newFile = true; + } + + $this->lockNode($uri, $lockInfo); + + $response->setHeader('Content-Type', 'application/xml; charset=utf-8'); + $response->setHeader('Lock-Token', 'token.'>'); + $response->setStatus($newFile ? 201 : 200); + $response->setBody($this->generateLockResponse($lockInfo)); + + // Returning false will interrupt the event chain and mark this method + // as 'handled'. + return false; + } + + /** + * Unlocks a uri. + * + * This WebDAV method allows you to remove a lock from a node. The client should provide a valid locktoken through the Lock-token http header + * The server should return 204 (No content) on success + */ + public function httpUnlock(RequestInterface $request, ResponseInterface $response) + { + $lockToken = $request->getHeader('Lock-Token'); + + // If the locktoken header is not supplied, we need to throw a bad request exception + if (!$lockToken) { + throw new DAV\Exception\BadRequest('No lock token was supplied'); + } + $path = $request->getPath(); + $locks = $this->getLocks($path); + + // Windows sometimes forgets to include < and > in the Lock-Token + // header + if ('<' !== $lockToken[0]) { + $lockToken = '<'.$lockToken.'>'; + } + + foreach ($locks as $lock) { + if ('token.'>' == $lockToken) { + $this->unlockNode($path, $lock); + $response->setHeader('Content-Length', '0'); + $response->setStatus(204); + + // Returning false will break the method chain, and mark the + // method as 'handled'. + return false; + } + } + + // If we got here, it means the locktoken was invalid + throw new DAV\Exception\LockTokenMatchesRequestUri(); + } + + /** + * This method is called after a node is deleted. + * + * We use this event to clean up any locks that still exist on the node. + * + * @param string $path + */ + public function afterUnbind($path) + { + $locks = $this->getLocks($path, $includeChildren = true); + foreach ($locks as $lock) { + // don't delete a lock on a parent dir + if (0 !== strpos($lock->uri, $path)) { + continue; + } + $this->unlockNode($path, $lock); + } + } + + /** + * Locks a uri. + * + * All the locking information is supplied in the lockInfo object. The object has a suggested timeout, but this can be safely ignored + * It is important that if the existing timeout is ignored, the property is overwritten, as this needs to be sent back to the client + * + * @param string $uri + * + * @return bool + */ + public function lockNode($uri, LockInfo $lockInfo) + { + if (!$this->server->emit('beforeLock', [$uri, $lockInfo])) { + return; + } + + return $this->locksBackend->lock($uri, $lockInfo); + } + + /** + * Unlocks a uri. + * + * This method removes a lock from a uri. It is assumed all the supplied information is correct and verified + * + * @param string $uri + * + * @return bool + */ + public function unlockNode($uri, LockInfo $lockInfo) + { + if (!$this->server->emit('beforeUnlock', [$uri, $lockInfo])) { + return; + } + + return $this->locksBackend->unlock($uri, $lockInfo); + } + + /** + * Returns the contents of the HTTP Timeout header. + * + * The method formats the header into an integer. + * + * @return int + */ + public function getTimeoutHeader() + { + $header = $this->server->httpRequest->getHeader('Timeout'); + + if ($header) { + if (0 === stripos($header, 'second-')) { + $header = (int) (substr($header, 7)); + } elseif (0 === stripos($header, 'infinite')) { + $header = LockInfo::TIMEOUT_INFINITE; + } else { + throw new DAV\Exception\BadRequest('Invalid HTTP timeout header'); + } + } else { + $header = 0; + } + + return $header; + } + + /** + * Generates the response for successful LOCK requests. + * + * @return string + */ + protected function generateLockResponse(LockInfo $lockInfo) + { + return $this->server->xml->write('{DAV:}prop', [ + '{DAV:}lockdiscovery' => new DAV\Xml\Property\LockDiscovery([$lockInfo]), + ], $this->server->getBaseUri()); + } + + /** + * The validateTokens event is triggered before every request. + * + * It's a moment where this plugin can check all the supplied lock tokens + * in the If: header, and check if they are valid. + * + * In addition, it will also ensure that it checks any missing lokens that + * must be present in the request, and reject requests without the proper + * tokens. + * + * @param mixed $conditions + */ + public function validateTokens(RequestInterface $request, &$conditions) + { + // First we need to gather a list of locks that must be satisfied. + $mustLocks = []; + $method = $request->getMethod(); + + // Methods not in that list are operations that doesn't alter any + // resources, and we don't need to check the lock-states for. + switch ($method) { + case 'DELETE': + $mustLocks = array_merge($mustLocks, $this->getLocks( + $request->getPath(), + true + )); + break; + case 'MKCOL': + case 'MKCALENDAR': + case 'PROPPATCH': + case 'PUT': + case 'PATCH': + $mustLocks = array_merge($mustLocks, $this->getLocks( + $request->getPath(), + false + )); + break; + case 'MOVE': + $mustLocks = array_merge($mustLocks, $this->getLocks( + $request->getPath(), + true + )); + $mustLocks = array_merge($mustLocks, $this->getLocks( + $this->server->calculateUri($request->getHeader('Destination')), + false + )); + break; + case 'COPY': + $mustLocks = array_merge($mustLocks, $this->getLocks( + $this->server->calculateUri($request->getHeader('Destination')), + false + )); + break; + case 'LOCK': + //Temporary measure.. figure out later why this is needed + // Here we basically ignore all incoming tokens... + foreach ($conditions as $ii => $condition) { + foreach ($condition['tokens'] as $jj => $token) { + $conditions[$ii]['tokens'][$jj]['validToken'] = true; + } + } + + return; + } + + // It's possible that there's identical locks, because of shared + // parents. We're removing the duplicates here. + $tmp = []; + foreach ($mustLocks as $lock) { + $tmp[$lock->token] = $lock; + } + $mustLocks = array_values($tmp); + + foreach ($conditions as $kk => $condition) { + foreach ($condition['tokens'] as $ii => $token) { + // Lock tokens always start with opaquelocktoken: + if ('opaquelocktoken:' !== substr($token['token'], 0, 16)) { + continue; + } + + $checkToken = substr($token['token'], 16); + // Looping through our list with locks. + foreach ($mustLocks as $jj => $mustLock) { + if ($mustLock->token == $checkToken) { + // We have a match! + // Removing this one from mustlocks + unset($mustLocks[$jj]); + + // Marking the condition as valid. + $conditions[$kk]['tokens'][$ii]['validToken'] = true; + + // Advancing to the next token + continue 2; + } + } + + // If we got here, it means that there was a + // lock-token, but it was not in 'mustLocks'. + // + // This is an edge-case, as it could mean that token + // was specified with a url that was not 'required' to + // check. So we're doing one extra lookup to make sure + // we really don't know this token. + // + // This also gets triggered when the user specified a + // lock-token that was expired. + $oddLocks = $this->getLocks($condition['uri']); + foreach ($oddLocks as $oddLock) { + if ($oddLock->token === $checkToken) { + // We have a hit! + $conditions[$kk]['tokens'][$ii]['validToken'] = true; + continue 2; + } + } + + // If we get all the way here, the lock-token was + // really unknown. + } + } + + // If there's any locks left in the 'mustLocks' array, it means that + // the resource was locked and we must block it. + if ($mustLocks) { + throw new DAV\Exception\Locked(reset($mustLocks)); + } + } + + /** + * Parses a webdav lock xml body, and returns a new Sabre\DAV\Locks\LockInfo object. + * + * @param string $body + * + * @return LockInfo + */ + protected function parseLockRequest($body) + { + $result = $this->server->xml->expect( + '{DAV:}lockinfo', + $body + ); + + $lockInfo = new LockInfo(); + + $lockInfo->owner = $result->owner; + $lockInfo->token = DAV\UUIDUtil::getUUID(); + $lockInfo->scope = $result->scope; + + return $lockInfo; + } + + /** + * Returns a bunch of meta-data about the plugin. + * + * Providing this information is optional, and is mainly displayed by the + * Browser plugin. + * + * The description key in the returned array may contain html and will not + * be sanitized. + * + * @return array + */ + public function getPluginInfo() + { + return [ + 'name' => $this->getPluginName(), + 'description' => 'The locks plugin turns this server into a class-2 WebDAV server and adds support for LOCK and UNLOCK', + 'link' => 'http://sabre.io/dav/locks/', + ]; + } +} diff --git a/3rdparty/sabre/dav/lib/DAV/MkCol.php b/3rdparty/sabre/dav/lib/DAV/MkCol.php new file mode 100644 index 00000000..f3c5ea5c --- /dev/null +++ b/3rdparty/sabre/dav/lib/DAV/MkCol.php @@ -0,0 +1,71 @@ +resourceType = $resourceType; + parent::__construct($mutations); + } + + /** + * Returns the resourcetype of the new collection. + * + * @return string[] + */ + public function getResourceType() + { + return $this->resourceType; + } + + /** + * Returns true or false if the MKCOL operation has at least the specified + * resource type. + * + * If the resourcetype is specified as an array, all resourcetypes are + * checked. + * + * @param string|string[] $resourceType + * + * @return bool + */ + public function hasResourceType($resourceType) + { + return 0 === count(array_diff((array) $resourceType, $this->resourceType)); + } +} diff --git a/3rdparty/sabre/dav/lib/DAV/Mount/Plugin.php b/3rdparty/sabre/dav/lib/DAV/Mount/Plugin.php new file mode 100644 index 00000000..b7f4851f --- /dev/null +++ b/3rdparty/sabre/dav/lib/DAV/Mount/Plugin.php @@ -0,0 +1,78 @@ +server = $server; + $this->server->on('method:GET', [$this, 'httpGet'], 90); + } + + /** + * 'beforeMethod' event handles. This event handles intercepts GET requests ending + * with ?mount. + * + * @return bool + */ + public function httpGet(RequestInterface $request, ResponseInterface $response) + { + $queryParams = $request->getQueryParameters(); + if (!array_key_exists('mount', $queryParams)) { + return; + } + + $currentUri = $request->getAbsoluteUrl(); + + // Stripping off everything after the ? + list($currentUri) = explode('?', $currentUri); + + $this->davMount($response, $currentUri); + + // Returning false to break the event chain + return false; + } + + /** + * Generates the davmount response. + * + * @param string $uri absolute uri + */ + public function davMount(ResponseInterface $response, $uri) + { + $response->setStatus(200); + $response->setHeader('Content-Type', 'application/davmount+xml'); + ob_start(); + echo '', "\n"; + echo "\n"; + echo ' ', htmlspecialchars($uri, ENT_NOQUOTES, 'UTF-8'), "\n"; + echo ''; + $response->setBody(ob_get_clean()); + } +} diff --git a/3rdparty/sabre/dav/lib/DAV/Node.php b/3rdparty/sabre/dav/lib/DAV/Node.php new file mode 100644 index 00000000..948060d9 --- /dev/null +++ b/3rdparty/sabre/dav/lib/DAV/Node.php @@ -0,0 +1,51 @@ +addPlugin($patchPlugin); + * + * @copyright Copyright (C) fruux GmbH (https://fruux.com/) + * @author Jean-Tiare LE BIGOT (http://www.jtlebi.fr/) + * @license http://sabre.io/license/ Modified BSD License + */ +class Plugin extends DAV\ServerPlugin +{ + const RANGE_APPEND = 1; + const RANGE_START = 2; + const RANGE_END = 3; + + /** + * Reference to server. + * + * @var DAV\Server + */ + protected $server; + + /** + * Initializes the plugin. + * + * This method is automatically called by the Server class after addPlugin. + */ + public function initialize(DAV\Server $server) + { + $this->server = $server; + $server->on('method:PATCH', [$this, 'httpPatch']); + } + + /** + * Returns a plugin name. + * + * Using this name other plugins will be able to access other plugins + * using DAV\Server::getPlugin + * + * @return string + */ + public function getPluginName() + { + return 'partialupdate'; + } + + /** + * Use this method to tell the server this plugin defines additional + * HTTP methods. + * + * This method is passed a uri. It should only return HTTP methods that are + * available for the specified uri. + * + * We claim to support PATCH method (partirl update) if and only if + * - the node exist + * - the node implements our partial update interface + * + * @param string $uri + * + * @return array + */ + public function getHTTPMethods($uri) + { + $tree = $this->server->tree; + + if ($tree->nodeExists($uri)) { + $node = $tree->getNodeForPath($uri); + if ($node instanceof IPatchSupport) { + return ['PATCH']; + } + } + + return []; + } + + /** + * Returns a list of features for the HTTP OPTIONS Dav: header. + * + * @return array + */ + public function getFeatures() + { + return ['sabredav-partialupdate']; + } + + /** + * Patch an uri. + * + * The WebDAV patch request can be used to modify only a part of an + * existing resource. If the resource does not exist yet and the first + * offset is not 0, the request fails + */ + public function httpPatch(RequestInterface $request, ResponseInterface $response) + { + $path = $request->getPath(); + + // Get the node. Will throw a 404 if not found + $node = $this->server->tree->getNodeForPath($path); + if (!$node instanceof IPatchSupport) { + throw new DAV\Exception\MethodNotAllowed('The target resource does not support the PATCH method.'); + } + + $range = $this->getHTTPUpdateRange($request); + + if (!$range) { + throw new DAV\Exception\BadRequest('No valid "X-Update-Range" found in the headers'); + } + + $contentType = strtolower( + (string) $request->getHeader('Content-Type') + ); + + if ('application/x-sabredav-partialupdate' != $contentType) { + throw new DAV\Exception\UnsupportedMediaType('Unknown Content-Type header "'.$contentType.'"'); + } + + $len = $this->server->httpRequest->getHeader('Content-Length'); + if (!$len) { + throw new DAV\Exception\LengthRequired('A Content-Length header is required'); + } + switch ($range[0]) { + case self::RANGE_START: + // Calculate the end-range if it doesn't exist. + if (!$range[2]) { + $range[2] = $range[1] + $len - 1; + } else { + if ($range[2] < $range[1]) { + throw new DAV\Exception\RequestedRangeNotSatisfiable('The end offset ('.$range[2].') is lower than the start offset ('.$range[1].')'); + } + if ($range[2] - $range[1] + 1 != $len) { + throw new DAV\Exception\RequestedRangeNotSatisfiable('Actual data length ('.$len.') is not consistent with begin ('.$range[1].') and end ('.$range[2].') offsets'); + } + } + break; + } + + if (!$this->server->emit('beforeWriteContent', [$path, $node, null])) { + return; + } + + $body = $this->server->httpRequest->getBody(); + + $etag = $node->patch($body, $range[0], isset($range[1]) ? $range[1] : null); + + $this->server->emit('afterWriteContent', [$path, $node]); + + $response->setHeader('Content-Length', '0'); + if ($etag) { + $response->setHeader('ETag', $etag); + } + $response->setStatus(204); + + // Breaks the event chain + return false; + } + + /** + * Returns the HTTP custom range update header. + * + * This method returns null if there is no well-formed HTTP range request + * header. It returns array(1) if it was an append request, array(2, + * $start, $end) if it's a start and end range, lastly it's array(3, + * $endoffset) if the offset was negative, and should be calculated from + * the end of the file. + * + * Examples: + * + * null - invalid + * [1] - append + * [2,10,15] - update bytes 10, 11, 12, 13, 14, 15 + * [2,10,null] - update bytes 10 until the end of the patch body + * [3,-5] - update from 5 bytes from the end of the file. + * + * @return array|null + */ + public function getHTTPUpdateRange(RequestInterface $request) + { + $range = $request->getHeader('X-Update-Range'); + if (is_null($range)) { + return null; + } + + // Matching "Range: bytes=1234-5678: both numbers are optional + + if (!preg_match('/^(append)|(?:bytes=([0-9]+)-([0-9]*))|(?:bytes=(-[0-9]+))$/i', $range, $matches)) { + return null; + } + + if ('append' === $matches[1]) { + return [self::RANGE_APPEND]; + } elseif (strlen($matches[2]) > 0) { + return [self::RANGE_START, (int) $matches[2], (int) $matches[3] ?: null]; + } else { + return [self::RANGE_END, (int) $matches[4]]; + } + } +} diff --git a/3rdparty/sabre/dav/lib/DAV/PropFind.php b/3rdparty/sabre/dav/lib/DAV/PropFind.php new file mode 100644 index 00000000..e9ffb07c --- /dev/null +++ b/3rdparty/sabre/dav/lib/DAV/PropFind.php @@ -0,0 +1,335 @@ +path = $path; + $this->properties = $properties; + $this->depth = $depth; + $this->requestType = $requestType; + + if (self::ALLPROPS === $requestType) { + $this->properties = [ + '{DAV:}getlastmodified', + '{DAV:}getcontentlength', + '{DAV:}resourcetype', + '{DAV:}quota-used-bytes', + '{DAV:}quota-available-bytes', + '{DAV:}getetag', + '{DAV:}getcontenttype', + ]; + } + + foreach ($this->properties as $propertyName) { + // Seeding properties with 404's. + $this->result[$propertyName] = [404, null]; + } + $this->itemsLeft = count($this->result); + } + + /** + * Handles a specific property. + * + * This method checks whether the specified property was requested in this + * PROPFIND request, and if so, it will call the callback and use the + * return value for it's value. + * + * Example: + * + * $propFind->handle('{DAV:}displayname', function() { + * return 'hello'; + * }); + * + * Note that handle will only work the first time. If null is returned, the + * value is ignored. + * + * It's also possible to not pass a callback, but immediately pass a value + * + * @param string $propertyName + * @param mixed $valueOrCallBack + */ + public function handle($propertyName, $valueOrCallBack) + { + if ($this->itemsLeft && isset($this->result[$propertyName]) && 404 === $this->result[$propertyName][0]) { + if (is_callable($valueOrCallBack)) { + $value = $valueOrCallBack(); + } else { + $value = $valueOrCallBack; + } + if (!is_null($value)) { + --$this->itemsLeft; + $this->result[$propertyName] = [200, $value]; + } + } + } + + /** + * Sets the value of the property. + * + * If status is not supplied, the status will default to 200 for non-null + * properties, and 404 for null properties. + * + * @param string $propertyName + * @param mixed $value + * @param int $status + */ + public function set($propertyName, $value, $status = null) + { + if (is_null($status)) { + $status = is_null($value) ? 404 : 200; + } + // If this is an ALLPROPS request and the property is + // unknown, add it to the result; else ignore it: + if (!isset($this->result[$propertyName])) { + if (self::ALLPROPS === $this->requestType) { + $this->result[$propertyName] = [$status, $value]; + } + + return; + } + if (404 !== $status && 404 === $this->result[$propertyName][0]) { + --$this->itemsLeft; + } elseif (404 === $status && 404 !== $this->result[$propertyName][0]) { + ++$this->itemsLeft; + } + $this->result[$propertyName] = [$status, $value]; + } + + /** + * Returns the current value for a property. + * + * @param string $propertyName + * + * @return mixed + */ + public function get($propertyName) + { + return isset($this->result[$propertyName]) ? $this->result[$propertyName][1] : null; + } + + /** + * Returns the current status code for a property name. + * + * If the property does not appear in the list of requested properties, + * null will be returned. + * + * @param string $propertyName + * + * @return int|null + */ + public function getStatus($propertyName) + { + return isset($this->result[$propertyName]) ? $this->result[$propertyName][0] : null; + } + + /** + * Updates the path for this PROPFIND. + * + * @param string $path + */ + public function setPath($path) + { + $this->path = $path; + } + + /** + * Returns the path this PROPFIND request is for. + * + * @return string + */ + public function getPath() + { + return $this->path; + } + + /** + * Returns the depth of this propfind request. + * + * @return int + */ + public function getDepth() + { + return $this->depth; + } + + /** + * Updates the depth of this propfind request. + * + * @param int $depth + */ + public function setDepth($depth) + { + $this->depth = $depth; + } + + /** + * Returns all propertynames that have a 404 status, and thus don't have a + * value yet. + * + * @return array + */ + public function get404Properties() + { + if (0 === $this->itemsLeft) { + return []; + } + $result = []; + foreach ($this->result as $propertyName => $stuff) { + if (404 === $stuff[0]) { + $result[] = $propertyName; + } + } + + return $result; + } + + /** + * Returns the full list of requested properties. + * + * This returns just their names, not a status or value. + * + * @return array + */ + public function getRequestedProperties() + { + return $this->properties; + } + + /** + * Returns true if this was an '{DAV:}allprops' request. + * + * @return bool + */ + public function isAllProps() + { + return self::ALLPROPS === $this->requestType; + } + + /** + * Returns a result array that's often used in multistatus responses. + * + * The array uses status codes as keys, and property names and value pairs + * as the value of the top array.. such as : + * + * [ + * 200 => [ '{DAV:}displayname' => 'foo' ], + * ] + * + * @return array + */ + public function getResultForMultiStatus() + { + $r = [ + 200 => [], + 404 => [], + ]; + foreach ($this->result as $propertyName => $info) { + if (!isset($r[$info[0]])) { + $r[$info[0]] = [$propertyName => $info[1]]; + } else { + $r[$info[0]][$propertyName] = $info[1]; + } + } + // Removing the 404's for multi-status requests. + if (self::ALLPROPS === $this->requestType) { + unset($r[404]); + } + + return $r; + } + + /** + * The path that we're fetching properties for. + * + * @var string + */ + protected $path; + + /** + * The Depth of the request. + * + * 0 means only the current item. 1 means the current item + its children. + * It can also be DEPTH_INFINITY if this is enabled in the server. + * + * @var int + */ + protected $depth = 0; + + /** + * The type of request. See the TYPE constants. + */ + protected $requestType; + + /** + * A list of requested properties. + * + * @var array + */ + protected $properties = []; + + /** + * The result of the operation. + * + * The keys in this array are property names. + * The values are an array with two elements: the http status code and then + * optionally a value. + * + * Example: + * + * [ + * "{DAV:}owner" : [404], + * "{DAV:}displayname" : [200, "Admin"] + * ] + * + * @var array + */ + protected $result = []; + + /** + * This is used as an internal counter for the number of properties that do + * not yet have a value. + * + * @var int + */ + protected $itemsLeft; +} diff --git a/3rdparty/sabre/dav/lib/DAV/PropPatch.php b/3rdparty/sabre/dav/lib/DAV/PropPatch.php new file mode 100644 index 00000000..092909de --- /dev/null +++ b/3rdparty/sabre/dav/lib/DAV/PropPatch.php @@ -0,0 +1,337 @@ +mutations = $mutations; + } + + /** + * Call this function if you wish to handle updating certain properties. + * For instance, your class may be responsible for handling updates for the + * {DAV:}displayname property. + * + * In that case, call this method with the first argument + * "{DAV:}displayname" and a second argument that's a method that does the + * actual updating. + * + * It's possible to specify more than one property as an array. + * + * The callback must return a boolean or an it. If the result is true, the + * operation was considered successful. If it's false, it's consided + * failed. + * + * If the result is an integer, we'll use that integer as the http status + * code associated with the operation. + * + * @param string|string[] $properties + */ + public function handle($properties, callable $callback) + { + $usedProperties = []; + foreach ((array) $properties as $propertyName) { + if (array_key_exists($propertyName, $this->mutations) && !isset($this->result[$propertyName])) { + $usedProperties[] = $propertyName; + // HTTP Accepted + $this->result[$propertyName] = 202; + } + } + + // Only registering if there's any unhandled properties. + if (!$usedProperties) { + return; + } + $this->propertyUpdateCallbacks[] = [ + // If the original argument to this method was a string, we need + // to also make sure that it stays that way, so the commit function + // knows how to format the arguments to the callback. + is_string($properties) ? $properties : $usedProperties, + $callback, + ]; + } + + /** + * Call this function if you wish to handle _all_ properties that haven't + * been handled by anything else yet. Note that you effectively claim with + * this that you promise to process _all_ properties that are coming in. + */ + public function handleRemaining(callable $callback) + { + $properties = $this->getRemainingMutations(); + if (!$properties) { + // Nothing to do, don't register callback + return; + } + + foreach ($properties as $propertyName) { + // HTTP Accepted + $this->result[$propertyName] = 202; + + $this->propertyUpdateCallbacks[] = [ + $properties, + $callback, + ]; + } + } + + /** + * Sets the result code for one or more properties. + * + * @param string|string[] $properties + * @param int $resultCode + */ + public function setResultCode($properties, $resultCode) + { + foreach ((array) $properties as $propertyName) { + $this->result[$propertyName] = $resultCode; + } + + if ($resultCode >= 400) { + $this->failed = true; + } + } + + /** + * Sets the result code for all properties that did not have a result yet. + * + * @param int $resultCode + */ + public function setRemainingResultCode($resultCode) + { + $this->setResultCode( + $this->getRemainingMutations(), + $resultCode + ); + } + + /** + * Returns the list of properties that don't have a result code yet. + * + * This method returns a list of property names, but not its values. + * + * @return string[] + */ + public function getRemainingMutations() + { + $remaining = []; + foreach ($this->mutations as $propertyName => $propValue) { + if (!isset($this->result[$propertyName])) { + $remaining[] = $propertyName; + } + } + + return $remaining; + } + + /** + * Returns the list of properties that don't have a result code yet. + * + * This method returns list of properties and their values. + * + * @return array + */ + public function getRemainingValues() + { + $remaining = []; + foreach ($this->mutations as $propertyName => $propValue) { + if (!isset($this->result[$propertyName])) { + $remaining[$propertyName] = $propValue; + } + } + + return $remaining; + } + + /** + * Performs the actual update, and calls all callbacks. + * + * This method returns true or false depending on if the operation was + * successful. + * + * @return bool + */ + public function commit() + { + // First we validate if every property has a handler + foreach ($this->mutations as $propertyName => $value) { + if (!isset($this->result[$propertyName])) { + $this->failed = true; + $this->result[$propertyName] = 403; + } + } + + foreach ($this->propertyUpdateCallbacks as $callbackInfo) { + if ($this->failed) { + break; + } + if (is_string($callbackInfo[0])) { + $this->doCallbackSingleProp($callbackInfo[0], $callbackInfo[1]); + } else { + $this->doCallbackMultiProp($callbackInfo[0], $callbackInfo[1]); + } + } + + /* + * If anywhere in this operation updating a property failed, we must + * update all other properties accordingly. + */ + if ($this->failed) { + foreach ($this->result as $propertyName => $status) { + if (202 === $status) { + // Failed dependency + $this->result[$propertyName] = 424; + } + } + } + + return !$this->failed; + } + + /** + * Executes a property callback with the single-property syntax. + * + * @param string $propertyName + */ + private function doCallBackSingleProp($propertyName, callable $callback) + { + $result = $callback($this->mutations[$propertyName]); + if (is_bool($result)) { + if ($result) { + if (is_null($this->mutations[$propertyName])) { + // Delete + $result = 204; + } else { + // Update + $result = 200; + } + } else { + // Fail + $result = 403; + } + } + if (!is_int($result)) { + throw new UnexpectedValueException('A callback sent to handle() did not return an int or a bool'); + } + $this->result[$propertyName] = $result; + if ($result >= 400) { + $this->failed = true; + } + } + + /** + * Executes a property callback with the multi-property syntax. + */ + private function doCallBackMultiProp(array $propertyList, callable $callback) + { + $argument = []; + foreach ($propertyList as $propertyName) { + $argument[$propertyName] = $this->mutations[$propertyName]; + } + + $result = $callback($argument); + + if (is_array($result)) { + foreach ($propertyList as $propertyName) { + if (!isset($result[$propertyName])) { + $resultCode = 500; + } else { + $resultCode = $result[$propertyName]; + } + if ($resultCode >= 400) { + $this->failed = true; + } + $this->result[$propertyName] = $resultCode; + } + } elseif (true === $result) { + // Success + foreach ($argument as $propertyName => $propertyValue) { + $this->result[$propertyName] = is_null($propertyValue) ? 204 : 200; + } + } elseif (false === $result) { + // Fail :( + $this->failed = true; + foreach ($propertyList as $propertyName) { + $this->result[$propertyName] = 403; + } + } else { + throw new UnexpectedValueException('A callback sent to handle() did not return an array or a bool'); + } + } + + /** + * Returns the result of the operation. + * + * @return array + */ + public function getResult() + { + return $this->result; + } + + /** + * Returns the full list of mutations. + * + * @return array + */ + public function getMutations() + { + return $this->mutations; + } +} diff --git a/3rdparty/sabre/dav/lib/DAV/PropertyStorage/Backend/BackendInterface.php b/3rdparty/sabre/dav/lib/DAV/PropertyStorage/Backend/BackendInterface.php new file mode 100644 index 00000000..64a8825c --- /dev/null +++ b/3rdparty/sabre/dav/lib/DAV/PropertyStorage/Backend/BackendInterface.php @@ -0,0 +1,75 @@ +isAllProps(). + * + * @param string $path + */ + public function propFind($path, PropFind $propFind); + + /** + * Updates properties for a path. + * + * This method received a PropPatch object, which contains all the + * information about the update. + * + * Usually you would want to call 'handleRemaining' on this object, to get; + * a list of all properties that need to be stored. + * + * @param string $path + */ + public function propPatch($path, PropPatch $propPatch); + + /** + * This method is called after a node is deleted. + * + * This allows a backend to clean up all associated properties. + * + * The delete method will get called once for the deletion of an entire + * tree. + * + * @param string $path + */ + public function delete($path); + + /** + * This method is called after a successful MOVE. + * + * This should be used to migrate all properties from one path to another. + * Note that entire collections may be moved, so ensure that all properties + * for children are also moved along. + * + * @param string $source + * @param string $destination + */ + public function move($source, $destination); +} diff --git a/3rdparty/sabre/dav/lib/DAV/PropertyStorage/Backend/PDO.php b/3rdparty/sabre/dav/lib/DAV/PropertyStorage/Backend/PDO.php new file mode 100644 index 00000000..89603319 --- /dev/null +++ b/3rdparty/sabre/dav/lib/DAV/PropertyStorage/Backend/PDO.php @@ -0,0 +1,224 @@ +pdo = $pdo; + } + + /** + * Fetches properties for a path. + * + * This method received a PropFind object, which contains all the + * information about the properties that need to be fetched. + * + * Usually you would just want to call 'get404Properties' on this object, + * as this will give you the _exact_ list of properties that need to be + * fetched, and haven't yet. + * + * However, you can also support the 'allprops' property here. In that + * case, you should check for $propFind->isAllProps(). + * + * @param string $path + */ + public function propFind($path, PropFind $propFind) + { + if (!$propFind->isAllProps() && 0 === count($propFind->get404Properties())) { + return; + } + + $query = 'SELECT name, value, valuetype FROM '.$this->tableName.' WHERE path = ?'; + $stmt = $this->pdo->prepare($query); + $stmt->execute([$path]); + + while ($row = $stmt->fetch(\PDO::FETCH_ASSOC)) { + if ('resource' === gettype($row['value'])) { + $row['value'] = stream_get_contents($row['value']); + } + switch ($row['valuetype']) { + case null: + case self::VT_STRING: + $propFind->set($row['name'], $row['value']); + break; + case self::VT_XML: + $propFind->set($row['name'], new Complex($row['value'])); + break; + case self::VT_OBJECT: + $propFind->set($row['name'], unserialize($row['value'])); + break; + } + } + } + + /** + * Updates properties for a path. + * + * This method received a PropPatch object, which contains all the + * information about the update. + * + * Usually you would want to call 'handleRemaining' on this object, to get; + * a list of all properties that need to be stored. + * + * @param string $path + */ + public function propPatch($path, PropPatch $propPatch) + { + $propPatch->handleRemaining(function ($properties) use ($path) { + if ('pgsql' === $this->pdo->getAttribute(\PDO::ATTR_DRIVER_NAME)) { + $updateSql = <<tableName} (path, name, valuetype, value) +VALUES (:path, :name, :valuetype, :value) +ON CONFLICT (path, name) +DO UPDATE SET valuetype = :valuetype, value = :value +SQL; + } else { + $updateSql = <<tableName} (path, name, valuetype, value) +VALUES (:path, :name, :valuetype, :value) +SQL; + } + + $updateStmt = $this->pdo->prepare($updateSql); + $deleteStmt = $this->pdo->prepare('DELETE FROM '.$this->tableName.' WHERE path = ? AND name = ?'); + + foreach ($properties as $name => $value) { + if (!is_null($value)) { + if (is_scalar($value)) { + $valueType = self::VT_STRING; + } elseif ($value instanceof Complex) { + $valueType = self::VT_XML; + $value = $value->getXml(); + } else { + $valueType = self::VT_OBJECT; + $value = serialize($value); + } + + $updateStmt->bindParam('path', $path, \PDO::PARAM_STR); + $updateStmt->bindParam('name', $name, \PDO::PARAM_STR); + $updateStmt->bindParam('valuetype', $valueType, \PDO::PARAM_INT); + $updateStmt->bindParam('value', $value, \PDO::PARAM_LOB); + + $updateStmt->execute(); + } else { + $deleteStmt->execute([$path, $name]); + } + } + + return true; + }); + } + + /** + * This method is called after a node is deleted. + * + * This allows a backend to clean up all associated properties. + * + * The delete method will get called once for the deletion of an entire + * tree. + * + * @param string $path + */ + public function delete($path) + { + $stmt = $this->pdo->prepare('DELETE FROM '.$this->tableName." WHERE path = ? OR path LIKE ? ESCAPE '='"); + $childPath = strtr( + $path, + [ + '=' => '==', + '%' => '=%', + '_' => '=_', + ] + ).'/%'; + + $stmt->execute([$path, $childPath]); + } + + /** + * This method is called after a successful MOVE. + * + * This should be used to migrate all properties from one path to another. + * Note that entire collections may be moved, so ensure that all properties + * for children are also moved along. + * + * @param string $source + * @param string $destination + */ + public function move($source, $destination) + { + // I don't know a way to write this all in a single sql query that's + // also compatible across db engines, so we're letting PHP do all the + // updates. Much slower, but it should still be pretty fast in most + // cases. + $select = $this->pdo->prepare('SELECT id, path FROM '.$this->tableName.' WHERE path = ? OR path LIKE ?'); + $select->execute([$source, $source.'/%']); + + $update = $this->pdo->prepare('UPDATE '.$this->tableName.' SET path = ? WHERE id = ?'); + while ($row = $select->fetch(\PDO::FETCH_ASSOC)) { + // Sanity check. SQL may select too many records, such as records + // with different cases. + if ($row['path'] !== $source && 0 !== strpos($row['path'], $source.'/')) { + continue; + } + + $trailingPart = substr($row['path'], strlen($source) + 1); + $newPath = $destination; + if ($trailingPart) { + $newPath .= '/'.$trailingPart; + } + $update->execute([$newPath, $row['id']]); + } + } +} diff --git a/3rdparty/sabre/dav/lib/DAV/PropertyStorage/Plugin.php b/3rdparty/sabre/dav/lib/DAV/PropertyStorage/Plugin.php new file mode 100644 index 00000000..da47ec9a --- /dev/null +++ b/3rdparty/sabre/dav/lib/DAV/PropertyStorage/Plugin.php @@ -0,0 +1,176 @@ +backend = $backend; + } + + /** + * This initializes the plugin. + * + * This function is called by Sabre\DAV\Server, after + * addPlugin is called. + * + * This method should set up the required event subscriptions. + */ + public function initialize(Server $server) + { + $server->on('propFind', [$this, 'propFind'], 130); + $server->on('propPatch', [$this, 'propPatch'], 300); + $server->on('afterMove', [$this, 'afterMove']); + $server->on('afterUnbind', [$this, 'afterUnbind']); + } + + /** + * Called during PROPFIND operations. + * + * If there's any requested properties that don't have a value yet, this + * plugin will look in the property storage backend to find them. + */ + public function propFind(PropFind $propFind, INode $node) + { + $path = $propFind->getPath(); + $pathFilter = $this->pathFilter; + if ($pathFilter && !$pathFilter($path)) { + return; + } + $this->backend->propFind($propFind->getPath(), $propFind); + } + + /** + * Called during PROPPATCH operations. + * + * If there's any updated properties that haven't been stored, the + * propertystorage backend can handle it. + * + * @param string $path + */ + public function propPatch($path, PropPatch $propPatch) + { + $pathFilter = $this->pathFilter; + if ($pathFilter && !$pathFilter($path)) { + return; + } + $this->backend->propPatch($path, $propPatch); + } + + /** + * Called after a node is deleted. + * + * This allows the backend to clean up any properties still in the + * database. + * + * @param string $path + */ + public function afterUnbind($path) + { + $pathFilter = $this->pathFilter; + if ($pathFilter && !$pathFilter($path)) { + return; + } + $this->backend->delete($path); + } + + /** + * Called after a node is moved. + * + * This allows the backend to move all the associated properties. + * + * @param string $source + * @param string $destination + */ + public function afterMove($source, $destination) + { + $pathFilter = $this->pathFilter; + if ($pathFilter && !$pathFilter($source)) { + return; + } + // If the destination is filtered, afterUnbind will handle cleaning up + // the properties. + if ($pathFilter && !$pathFilter($destination)) { + return; + } + + $this->backend->move($source, $destination); + } + + /** + * Returns a plugin name. + * + * Using this name other plugins will be able to access other plugins + * using \Sabre\DAV\Server::getPlugin + * + * @return string + */ + public function getPluginName() + { + return 'property-storage'; + } + + /** + * Returns a bunch of meta-data about the plugin. + * + * Providing this information is optional, and is mainly displayed by the + * Browser plugin. + * + * The description key in the returned array may contain html and will not + * be sanitized. + * + * @return array + */ + public function getPluginInfo() + { + return [ + 'name' => $this->getPluginName(), + 'description' => 'This plugin allows any arbitrary WebDAV property to be set on any resource.', + 'link' => 'http://sabre.io/dav/property-storage/', + ]; + } +} diff --git a/3rdparty/sabre/dav/lib/DAV/Server.php b/3rdparty/sabre/dav/lib/DAV/Server.php new file mode 100644 index 00000000..3133e54a --- /dev/null +++ b/3rdparty/sabre/dav/lib/DAV/Server.php @@ -0,0 +1,1682 @@ + '{DAV:}collection', + ]; + + /** + * This property allows the usage of Depth: infinity on PROPFIND requests. + * + * By default Depth: infinity is treated as Depth: 1. Allowing Depth: + * infinity is potentially risky, as it allows a single client to do a full + * index of the webdav server, which is an easy DoS attack vector. + * + * Only turn this on if you know what you're doing. + * + * @var bool + */ + public $enablePropfindDepthInfinity = false; + + /** + * Reference to the XML utility object. + * + * @var Xml\Service + */ + public $xml; + + /** + * If this setting is turned off, SabreDAV's version number will be hidden + * from various places. + * + * Some people feel this is a good security measure. + * + * @var bool + */ + public static $exposeVersion = true; + + /** + * If this setting is turned on, any multi status response on any PROPFIND will be streamed to the output buffer. + * This will be beneficial for large result sets which will no longer consume a large amount of memory as well as + * send back data to the client earlier. + * + * @var bool + */ + public static $streamMultiStatus = false; + + /** + * Sets up the server. + * + * If a Sabre\DAV\Tree object is passed as an argument, it will + * use it as the directory tree. If a Sabre\DAV\INode is passed, it + * will create a Sabre\DAV\Tree and use the node as the root. + * + * If nothing is passed, a Sabre\DAV\SimpleCollection is created in + * a Sabre\DAV\Tree. + * + * If an array is passed, we automatically create a root node, and use + * the nodes in the array as top-level children. + * + * @param Tree|INode|array|null $treeOrNode The tree object + * + * @throws Exception + */ + public function __construct($treeOrNode = null, ?HTTP\Sapi $sapi = null) + { + if ($treeOrNode instanceof Tree) { + $this->tree = $treeOrNode; + } elseif ($treeOrNode instanceof INode) { + $this->tree = new Tree($treeOrNode); + } elseif (is_array($treeOrNode)) { + $root = new SimpleCollection('root', $treeOrNode); + $this->tree = new Tree($root); + } elseif (is_null($treeOrNode)) { + $root = new SimpleCollection('root'); + $this->tree = new Tree($root); + } else { + throw new Exception('Invalid argument passed to constructor. Argument must either be an instance of Sabre\\DAV\\Tree, Sabre\\DAV\\INode, an array or null'); + } + + $this->xml = new Xml\Service(); + $this->sapi = $sapi ?? new HTTP\Sapi(); + $this->httpResponse = new HTTP\Response(); + $this->httpRequest = $this->sapi->getRequest(); + $this->addPlugin(new CorePlugin()); + } + + /** + * Starts the DAV Server. + */ + public function start() + { + try { + // If nginx (pre-1.2) is used as a proxy server, and SabreDAV as an + // origin, we must make sure we send back HTTP/1.0 if this was + // requested. + // This is mainly because nginx doesn't support Chunked Transfer + // Encoding, and this forces the webserver SabreDAV is running on, + // to buffer entire responses to calculate Content-Length. + $this->httpResponse->setHTTPVersion($this->httpRequest->getHTTPVersion()); + + // Setting the base url + $this->httpRequest->setBaseUrl($this->getBaseUri()); + $this->invokeMethod($this->httpRequest, $this->httpResponse); + } catch (\Throwable $e) { + try { + $this->emit('exception', [$e]); + } catch (\Exception $ignore) { + } + $DOM = new \DOMDocument('1.0', 'utf-8'); + $DOM->formatOutput = true; + + $error = $DOM->createElementNS('DAV:', 'd:error'); + $error->setAttribute('xmlns:s', self::NS_SABREDAV); + $DOM->appendChild($error); + + $h = function ($v) { + return htmlspecialchars((string) $v, ENT_NOQUOTES, 'UTF-8'); + }; + + if (self::$exposeVersion) { + $error->appendChild($DOM->createElement('s:sabredav-version', $h(Version::VERSION))); + } + + $error->appendChild($DOM->createElement('s:exception', $h(get_class($e)))); + $error->appendChild($DOM->createElement('s:message', $h($e->getMessage()))); + if ($this->debugExceptions) { + $error->appendChild($DOM->createElement('s:file', $h($e->getFile()))); + $error->appendChild($DOM->createElement('s:line', $h($e->getLine()))); + $error->appendChild($DOM->createElement('s:code', $h($e->getCode()))); + $error->appendChild($DOM->createElement('s:stacktrace', $h($e->getTraceAsString()))); + } + + if ($this->debugExceptions) { + $previous = $e; + while ($previous = $previous->getPrevious()) { + $xPrevious = $DOM->createElement('s:previous-exception'); + $xPrevious->appendChild($DOM->createElement('s:exception', $h(get_class($previous)))); + $xPrevious->appendChild($DOM->createElement('s:message', $h($previous->getMessage()))); + $xPrevious->appendChild($DOM->createElement('s:file', $h($previous->getFile()))); + $xPrevious->appendChild($DOM->createElement('s:line', $h($previous->getLine()))); + $xPrevious->appendChild($DOM->createElement('s:code', $h($previous->getCode()))); + $xPrevious->appendChild($DOM->createElement('s:stacktrace', $h($previous->getTraceAsString()))); + $error->appendChild($xPrevious); + } + } + + if ($e instanceof Exception) { + $httpCode = $e->getHTTPCode(); + $e->serialize($this, $error); + $headers = $e->getHTTPHeaders($this); + } else { + $httpCode = 500; + $headers = []; + } + $headers['Content-Type'] = 'application/xml; charset=utf-8'; + + $this->httpResponse->setStatus($httpCode); + $this->httpResponse->setHeaders($headers); + $this->httpResponse->setBody($DOM->saveXML()); + $this->sapi->sendResponse($this->httpResponse); + } + } + + /** + * Alias of start(). + * + * @deprecated + */ + public function exec() + { + $this->start(); + } + + /** + * Sets the base server uri. + * + * @param string $uri + */ + public function setBaseUri($uri) + { + // If the baseUri does not end with a slash, we must add it + if ('/' !== $uri[strlen($uri) - 1]) { + $uri .= '/'; + } + + $this->baseUri = $uri; + } + + /** + * Returns the base responding uri. + * + * @return string + */ + public function getBaseUri() + { + if (is_null($this->baseUri)) { + $this->baseUri = $this->guessBaseUri(); + } + + return $this->baseUri; + } + + /** + * This method attempts to detect the base uri. + * Only the PATH_INFO variable is considered. + * + * If this variable is not set, the root (/) is assumed. + * + * @return string + */ + public function guessBaseUri() + { + $pathInfo = $this->httpRequest->getRawServerValue('PATH_INFO'); + $uri = $this->httpRequest->getRawServerValue('REQUEST_URI'); + + // If PATH_INFO is found, we can assume it's accurate. + if (!empty($pathInfo)) { + // We need to make sure we ignore the QUERY_STRING part + if ($pos = strpos($uri, '?')) { + $uri = substr($uri, 0, $pos); + } + + // PATH_INFO is only set for urls, such as: /example.php/path + // in that case PATH_INFO contains '/path'. + // Note that REQUEST_URI is percent encoded, while PATH_INFO is + // not, Therefore they are only comparable if we first decode + // REQUEST_INFO as well. + $decodedUri = HTTP\decodePath($uri); + + // A simple sanity check: + if (substr($decodedUri, strlen($decodedUri) - strlen($pathInfo)) === $pathInfo) { + $baseUri = substr($decodedUri, 0, strlen($decodedUri) - strlen($pathInfo)); + + return rtrim($baseUri, '/').'/'; + } + + throw new Exception('The REQUEST_URI ('.$uri.') did not end with the contents of PATH_INFO ('.$pathInfo.'). This server might be misconfigured.'); + } + + // The last fallback is that we're just going to assume the server root. + return '/'; + } + + /** + * Adds a plugin to the server. + * + * For more information, console the documentation of Sabre\DAV\ServerPlugin + */ + public function addPlugin(ServerPlugin $plugin) + { + $this->plugins[$plugin->getPluginName()] = $plugin; + $plugin->initialize($this); + } + + /** + * Returns an initialized plugin by it's name. + * + * This function returns null if the plugin was not found. + * + * @param string $name + * + * @return ServerPlugin + */ + public function getPlugin($name) + { + if (isset($this->plugins[$name])) { + return $this->plugins[$name]; + } + + return null; + } + + /** + * Returns all plugins. + * + * @return array + */ + public function getPlugins() + { + return $this->plugins; + } + + /** + * Returns the PSR-3 logger object. + * + * @return LoggerInterface + */ + public function getLogger() + { + if (!$this->logger) { + $this->logger = new NullLogger(); + } + + return $this->logger; + } + + /** + * Handles a http request, and execute a method based on its name. + * + * @param bool $sendResponse whether to send the HTTP response to the DAV client + */ + public function invokeMethod(RequestInterface $request, ResponseInterface $response, $sendResponse = true) + { + $method = $request->getMethod(); + + if (!$this->emit('beforeMethod:'.$method, [$request, $response])) { + return; + } + + if (self::$exposeVersion) { + $response->setHeader('X-Sabre-Version', Version::VERSION); + } + + $this->transactionType = strtolower($method); + + if (!$this->checkPreconditions($request, $response)) { + $this->sapi->sendResponse($response); + + return; + } + + if ($this->emit('method:'.$method, [$request, $response])) { + $exMessage = 'There was no plugin in the system that was willing to handle this '.$method.' method.'; + if ('GET' === $method) { + $exMessage .= ' Enable the Browser plugin to get a better result here.'; + } + + // Unsupported method + throw new Exception\NotImplemented($exMessage); + } + + if (!$this->emit('afterMethod:'.$method, [$request, $response])) { + return; + } + + if (null === $response->getStatus()) { + throw new Exception('No subsystem set a valid HTTP status code. Something must have interrupted the request without providing further detail.'); + } + if ($sendResponse) { + $this->sapi->sendResponse($response); + $this->emit('afterResponse', [$request, $response]); + } + } + + // {{{ HTTP/WebDAV protocol helpers + + /** + * Returns an array with all the supported HTTP methods for a specific uri. + * + * @param string $path + * + * @return array + */ + public function getAllowedMethods($path) + { + $methods = [ + 'OPTIONS', + 'GET', + 'HEAD', + 'DELETE', + 'PROPFIND', + 'PUT', + 'PROPPATCH', + 'COPY', + 'MOVE', + 'REPORT', + ]; + + // The MKCOL is only allowed on an unmapped uri + try { + $this->tree->getNodeForPath($path); + } catch (Exception\NotFound $e) { + $methods[] = 'MKCOL'; + } + + // We're also checking if any of the plugins register any new methods + foreach ($this->plugins as $plugin) { + $methods = array_merge($methods, $plugin->getHTTPMethods($path)); + } + array_unique($methods); + + return $methods; + } + + /** + * Gets the uri for the request, keeping the base uri into consideration. + * + * @return string + */ + public function getRequestUri() + { + return $this->calculateUri($this->httpRequest->getUrl()); + } + + /** + * Turns a URI such as the REQUEST_URI into a local path. + * + * This method: + * * strips off the base path + * * normalizes the path + * * uri-decodes the path + * + * @param string $uri + * + * @throws Exception\Forbidden A permission denied exception is thrown whenever there was an attempt to supply a uri outside of the base uri + * + * @return string + */ + public function calculateUri($uri) + { + if ('' != $uri && '/' != $uri[0] && strpos($uri, '://')) { + $uri = parse_url($uri, PHP_URL_PATH); + } + + $uri = Uri\normalize(preg_replace('|/+|', '/', $uri)); + $baseUri = Uri\normalize($this->getBaseUri()); + + if (0 === strpos($uri, $baseUri)) { + return trim(HTTP\decodePath(substr($uri, strlen($baseUri))), '/'); + + // A special case, if the baseUri was accessed without a trailing + // slash, we'll accept it as well. + } elseif ($uri.'/' === $baseUri) { + return ''; + } else { + throw new Exception\Forbidden('Requested uri ('.$uri.') is out of base uri ('.$this->getBaseUri().')'); + } + } + + /** + * Returns the HTTP depth header. + * + * This method returns the contents of the HTTP depth request header. If the depth header was 'infinity' it will return the Sabre\DAV\Server::DEPTH_INFINITY object + * It is possible to supply a default depth value, which is used when the depth header has invalid content, or is completely non-existent + * + * @param mixed $default + * + * @return int + */ + public function getHTTPDepth($default = self::DEPTH_INFINITY) + { + // If its not set, we'll grab the default + $depth = $this->httpRequest->getHeader('Depth'); + + if (is_null($depth)) { + return $default; + } + + if ('infinity' == $depth) { + return self::DEPTH_INFINITY; + } + + // If its an unknown value. we'll grab the default + if (!ctype_digit($depth)) { + return $default; + } + + return (int) $depth; + } + + /** + * Returns the HTTP range header. + * + * This method returns null if there is no well-formed HTTP range request + * header or array($start, $end). + * + * The first number is the offset of the first byte in the range. + * The second number is the offset of the last byte in the range. + * + * If the second offset is null, it should be treated as the offset of the last byte of the entity + * If the first offset is null, the second offset should be used to retrieve the last x bytes of the entity + * + * @return int[]|null + */ + public function getHTTPRange() + { + $range = $this->httpRequest->getHeader('range'); + if (is_null($range)) { + return null; + } + + // Matching "Range: bytes=1234-5678: both numbers are optional + + if (!preg_match('/^bytes=([0-9]*)-([0-9]*)$/i', $range, $matches)) { + return null; + } + + if ('' === $matches[1] && '' === $matches[2]) { + return null; + } + + return [ + '' !== $matches[1] ? (int) $matches[1] : null, + '' !== $matches[2] ? (int) $matches[2] : null, + ]; + } + + /** + * Returns the HTTP Prefer header information. + * + * The prefer header is defined in: + * http://tools.ietf.org/html/draft-snell-http-prefer-14 + * + * This method will return an array with options. + * + * Currently, the following options may be returned: + * [ + * 'return-asynch' => true, + * 'return-minimal' => true, + * 'return-representation' => true, + * 'wait' => 30, + * 'strict' => true, + * 'lenient' => true, + * ] + * + * This method also supports the Brief header, and will also return + * 'return-minimal' if the brief header was set to 't'. + * + * For the boolean options, false will be returned if the headers are not + * specified. For the integer options it will be 'null'. + * + * @return array + */ + public function getHTTPPrefer() + { + $result = [ + // can be true or false + 'respond-async' => false, + // Could be set to 'representation' or 'minimal'. + 'return' => null, + // Used as a timeout, is usually a number. + 'wait' => null, + // can be 'strict' or 'lenient'. + 'handling' => false, + ]; + + if ($prefer = $this->httpRequest->getHeader('Prefer')) { + $result = array_merge( + $result, + HTTP\parsePrefer($prefer) + ); + } elseif ('t' == $this->httpRequest->getHeader('Brief')) { + $result['return'] = 'minimal'; + } + + return $result; + } + + /** + * Returns information about Copy and Move requests. + * + * This function is created to help getting information about the source and the destination for the + * WebDAV MOVE and COPY HTTP request. It also validates a lot of information and throws proper exceptions + * + * The returned value is an array with the following keys: + * * destination - Destination path + * * destinationExists - Whether or not the destination is an existing url (and should therefore be overwritten) + * + * @throws Exception\BadRequest upon missing or broken request headers + * @throws Exception\UnsupportedMediaType when trying to copy into a + * non-collection + * @throws Exception\PreconditionFailed if overwrite is set to false, but + * the destination exists + * @throws Exception\Forbidden when source and destination paths are + * identical + * @throws Exception\Conflict when trying to copy a node into its own + * subtree + * + * @return array + */ + public function getCopyAndMoveInfo(RequestInterface $request) + { + // Collecting the relevant HTTP headers + if (!$request->getHeader('Destination')) { + throw new Exception\BadRequest('The destination header was not supplied'); + } + $destination = $this->calculateUri($request->getHeader('Destination')); + $overwrite = $request->getHeader('Overwrite'); + if (!$overwrite) { + $overwrite = 'T'; + } + if ('T' == strtoupper($overwrite)) { + $overwrite = true; + } elseif ('F' == strtoupper($overwrite)) { + $overwrite = false; + } + // We need to throw a bad request exception, if the header was invalid + else { + throw new Exception\BadRequest('The HTTP Overwrite header should be either T or F'); + } + list($destinationDir) = Uri\split($destination); + + try { + $destinationParent = $this->tree->getNodeForPath($destinationDir); + if (!($destinationParent instanceof ICollection)) { + throw new Exception\UnsupportedMediaType('The destination node is not a collection'); + } + } catch (Exception\NotFound $e) { + // If the destination parent node is not found, we throw a 409 + throw new Exception\Conflict('The destination node is not found'); + } + + try { + $destinationNode = $this->tree->getNodeForPath($destination); + + // If this succeeded, it means the destination already exists + // we'll need to throw precondition failed in case overwrite is false + if (!$overwrite) { + throw new Exception\PreconditionFailed('The destination node already exists, and the overwrite header is set to false', 'Overwrite'); + } + } catch (Exception\NotFound $e) { + // Destination didn't exist, we're all good + $destinationNode = false; + } + + $requestPath = $request->getPath(); + if ($destination === $requestPath) { + throw new Exception\Forbidden('Source and destination uri are identical.'); + } + if (substr($destination, 0, strlen($requestPath) + 1) === $requestPath.'/') { + throw new Exception\Conflict('The destination may not be part of the same subtree as the source path.'); + } + + // These are the three relevant properties we need to return + return [ + 'destination' => $destination, + 'destinationExists' => (bool) $destinationNode, + 'destinationNode' => $destinationNode, + ]; + } + + /** + * Returns a list of properties for a path. + * + * This is a simplified version getPropertiesForPath. If you aren't + * interested in status codes, but you just want to have a flat list of + * properties, use this method. + * + * Please note though that any problems related to retrieving properties, + * such as permission issues will just result in an empty array being + * returned. + * + * @param string $path + * @param array $propertyNames + * + * @return array + */ + public function getProperties($path, $propertyNames) + { + $result = $this->getPropertiesForPath($path, $propertyNames, 0); + if (isset($result[0][200])) { + return $result[0][200]; + } else { + return []; + } + } + + /** + * A kid-friendly way to fetch properties for a node's children. + * + * The returned array will be indexed by the path of the of child node. + * Only properties that are actually found will be returned. + * + * The parent node will not be returned. + * + * @param string $path + * @param array $propertyNames + * + * @return array + */ + public function getPropertiesForChildren($path, $propertyNames) + { + $result = []; + foreach ($this->getPropertiesForPath($path, $propertyNames, 1) as $k => $row) { + // Skipping the parent path + if (0 === $k) { + continue; + } + + $result[$row['href']] = $row[200]; + } + + return $result; + } + + /** + * Returns a list of HTTP headers for a particular resource. + * + * The generated http headers are based on properties provided by the + * resource. The method basically provides a simple mapping between + * DAV property and HTTP header. + * + * The headers are intended to be used for HEAD and GET requests. + * + * @param string $path + * + * @return array + */ + public function getHTTPHeaders($path) + { + $propertyMap = [ + '{DAV:}getcontenttype' => 'Content-Type', + '{DAV:}getcontentlength' => 'Content-Length', + '{DAV:}getlastmodified' => 'Last-Modified', + '{DAV:}getetag' => 'ETag', + ]; + + $properties = $this->getProperties($path, array_keys($propertyMap)); + + $headers = []; + foreach ($propertyMap as $property => $header) { + if (!isset($properties[$property])) { + continue; + } + + if (is_scalar($properties[$property])) { + $headers[$header] = $properties[$property]; + + // GetLastModified gets special cased + } elseif ($properties[$property] instanceof Xml\Property\GetLastModified) { + $headers[$header] = HTTP\toDate($properties[$property]->getTime()); + } + } + + return $headers; + } + + /** + * Small helper to support PROPFIND with DEPTH_INFINITY. + * + * @param array $yieldFirst + * + * @return \Traversable + */ + private function generatePathNodes(PropFind $propFind, ?array $yieldFirst = null) + { + if (null !== $yieldFirst) { + yield $yieldFirst; + } + $newDepth = $propFind->getDepth(); + $path = $propFind->getPath(); + + if (self::DEPTH_INFINITY !== $newDepth) { + --$newDepth; + } + + $propertyNames = $propFind->getRequestedProperties(); + $propFindType = !$propFind->isAllProps() ? PropFind::NORMAL : PropFind::ALLPROPS; + + foreach ($this->tree->getChildren($path) as $childNode) { + if ('' !== $path) { + $subPath = $path.'/'.$childNode->getName(); + } else { + $subPath = $childNode->getName(); + } + $subPropFind = new PropFind($subPath, $propertyNames, $newDepth, $propFindType); + + yield [ + $subPropFind, + $childNode, + ]; + + if ((self::DEPTH_INFINITY === $newDepth || $newDepth >= 1) && $childNode instanceof ICollection) { + foreach ($this->generatePathNodes($subPropFind) as $subItem) { + yield $subItem; + } + } + } + } + + /** + * Returns a list of properties for a given path. + * + * The path that should be supplied should have the baseUrl stripped out + * The list of properties should be supplied in Clark notation. If the list is empty + * 'allprops' is assumed. + * + * If a depth of 1 is requested child elements will also be returned. + * + * @param string $path + * @param array $propertyNames + * @param int $depth + * + * @return array + * + * @deprecated Use getPropertiesIteratorForPath() instead (as it's more memory efficient) + * @see getPropertiesIteratorForPath() + */ + public function getPropertiesForPath($path, $propertyNames = [], $depth = 0) + { + return iterator_to_array($this->getPropertiesIteratorForPath($path, $propertyNames, $depth)); + } + + /** + * Returns a list of properties for a given path. + * + * The path that should be supplied should have the baseUrl stripped out + * The list of properties should be supplied in Clark notation. If the list is empty + * 'allprops' is assumed. + * + * If a depth of 1 is requested child elements will also be returned. + * + * @param string $path + * @param array $propertyNames + * @param int $depth + * + * @return \Iterator + */ + public function getPropertiesIteratorForPath($path, $propertyNames = [], $depth = 0) + { + // The only two options for the depth of a propfind is 0 or 1 - as long as depth infinity is not enabled + if (!$this->enablePropfindDepthInfinity && 0 != $depth) { + $depth = 1; + } + + $path = trim($path, '/'); + + $propFindType = $propertyNames ? PropFind::NORMAL : PropFind::ALLPROPS; + $propFind = new PropFind($path, (array) $propertyNames, $depth, $propFindType); + + $parentNode = $this->tree->getNodeForPath($path); + + $propFindRequests = [[ + $propFind, + $parentNode, + ]]; + + if (($depth > 0 || self::DEPTH_INFINITY === $depth) && $parentNode instanceof ICollection) { + $propFindRequests = $this->generatePathNodes(clone $propFind, current($propFindRequests)); + } + + foreach ($propFindRequests as $propFindRequest) { + list($propFind, $node) = $propFindRequest; + $r = $this->getPropertiesByNode($propFind, $node); + if ($r) { + $result = $propFind->getResultForMultiStatus(); + $result['href'] = $propFind->getPath(); + + // WebDAV recommends adding a slash to the path, if the path is + // a collection. + // Furthermore, iCal also demands this to be the case for + // principals. This is non-standard, but we support it. + $resourceType = $this->getResourceTypeForNode($node); + if (in_array('{DAV:}collection', $resourceType) || in_array('{DAV:}principal', $resourceType)) { + $result['href'] .= '/'; + } + yield $result; + } + } + } + + /** + * Returns a list of properties for a list of paths. + * + * The path that should be supplied should have the baseUrl stripped out + * The list of properties should be supplied in Clark notation. If the list is empty + * 'allprops' is assumed. + * + * The result is returned as an array, with paths for it's keys. + * The result may be returned out of order. + * + * @return array + */ + public function getPropertiesForMultiplePaths(array $paths, array $propertyNames = []) + { + $result = [ + ]; + + $nodes = $this->tree->getMultipleNodes($paths); + + foreach ($nodes as $path => $node) { + $propFind = new PropFind($path, $propertyNames); + $r = $this->getPropertiesByNode($propFind, $node); + if ($r) { + $result[$path] = $propFind->getResultForMultiStatus(); + $result[$path]['href'] = $path; + + $resourceType = $this->getResourceTypeForNode($node); + if (in_array('{DAV:}collection', $resourceType) || in_array('{DAV:}principal', $resourceType)) { + $result[$path]['href'] .= '/'; + } + } + } + + return $result; + } + + /** + * Determines all properties for a node. + * + * This method tries to grab all properties for a node. This method is used + * internally getPropertiesForPath and a few others. + * + * It could be useful to call this, if you already have an instance of your + * target node and simply want to run through the system to get a correct + * list of properties. + * + * @return bool + */ + public function getPropertiesByNode(PropFind $propFind, INode $node) + { + return $this->emit('propFind', [$propFind, $node]); + } + + /** + * This method is invoked by sub-systems creating a new file. + * + * Currently this is done by HTTP PUT and HTTP LOCK (in the Locks_Plugin). + * It was important to get this done through a centralized function, + * allowing plugins to intercept this using the beforeCreateFile event. + * + * This method will return true if the file was actually created + * + * @param string $uri + * @param resource $data + * @param string $etag + * + * @return bool + */ + public function createFile($uri, $data, &$etag = null) + { + list($dir, $name) = Uri\split($uri); + + if (!$this->emit('beforeBind', [$uri])) { + return false; + } + + try { + $parent = $this->tree->getNodeForPath($dir); + } catch (Exception\NotFound $e) { + throw new Exception\Conflict('Files cannot be created in non-existent collections'); + } + + if (!$parent instanceof ICollection) { + throw new Exception\Conflict('Files can only be created as children of collections'); + } + + // It is possible for an event handler to modify the content of the + // body, before it gets written. If this is the case, $modified + // should be set to true. + // + // If $modified is true, we must not send back an ETag. + $modified = false; + if (!$this->emit('beforeCreateFile', [$uri, &$data, $parent, &$modified])) { + return false; + } + + $etag = $parent->createFile($name, $data); + + if ($modified) { + $etag = null; + } + + $this->tree->markDirty($dir.'/'.$name); + + $this->emit('afterBind', [$uri]); + $this->emit('afterCreateFile', [$uri, $parent]); + + return true; + } + + /** + * This method is invoked by sub-systems updating a file. + * + * This method will return true if the file was actually updated + * + * @param string $uri + * @param resource $data + * @param string $etag + * + * @return bool + */ + public function updateFile($uri, $data, &$etag = null) + { + $node = $this->tree->getNodeForPath($uri); + + // It is possible for an event handler to modify the content of the + // body, before it gets written. If this is the case, $modified + // should be set to true. + // + // If $modified is true, we must not send back an ETag. + $modified = false; + if (!$this->emit('beforeWriteContent', [$uri, $node, &$data, &$modified])) { + return false; + } + + $etag = $node->put($data); + if ($modified) { + $etag = null; + } + $this->emit('afterWriteContent', [$uri, $node]); + + return true; + } + + /** + * This method is invoked by sub-systems creating a new directory. + * + * @param string $uri + */ + public function createDirectory($uri) + { + $this->createCollection($uri, new MkCol(['{DAV:}collection'], [])); + } + + /** + * Use this method to create a new collection. + * + * @param string $uri The new uri + * + * @return array|null + */ + public function createCollection($uri, MkCol $mkCol) + { + list($parentUri, $newName) = Uri\split($uri); + + // Making sure the parent exists + try { + $parent = $this->tree->getNodeForPath($parentUri); + } catch (Exception\NotFound $e) { + throw new Exception\Conflict('Parent node does not exist'); + } + + // Making sure the parent is a collection + if (!$parent instanceof ICollection) { + throw new Exception\Conflict('Parent node is not a collection'); + } + + // Making sure the child does not already exist + try { + $parent->getChild($newName); + + // If we got here.. it means there's already a node on that url, and we need to throw a 405 + throw new Exception\MethodNotAllowed('The resource you tried to create already exists'); + } catch (Exception\NotFound $e) { + // NotFound is the expected behavior. + } + + if (!$this->emit('beforeBind', [$uri])) { + return; + } + + if ($parent instanceof IExtendedCollection) { + /* + * If the parent is an instance of IExtendedCollection, it means that + * we can pass the MkCol object directly as it may be able to store + * properties immediately. + */ + $parent->createExtendedCollection($newName, $mkCol); + } else { + /* + * If the parent is a standard ICollection, it means only + * 'standard' collections can be created, so we should fail any + * MKCOL operation that carries extra resourcetypes. + */ + if (count($mkCol->getResourceType()) > 1) { + throw new Exception\InvalidResourceType('The {DAV:}resourcetype you specified is not supported here.'); + } + + $parent->createDirectory($newName); + } + + // If there are any properties that have not been handled/stored, + // we ask the 'propPatch' event to handle them. This will allow for + // example the propertyStorage system to store properties upon MKCOL. + if ($mkCol->getRemainingMutations()) { + $this->emit('propPatch', [$uri, $mkCol]); + } + $success = $mkCol->commit(); + + if (!$success) { + $result = $mkCol->getResult(); + + $formattedResult = [ + 'href' => $uri, + ]; + + foreach ($result as $propertyName => $status) { + if (!isset($formattedResult[$status])) { + $formattedResult[$status] = []; + } + $formattedResult[$status][$propertyName] = null; + } + + return $formattedResult; + } + + $this->tree->markDirty($parentUri); + $this->emit('afterBind', [$uri]); + $this->emit('afterCreateCollection', [$uri]); + } + + /** + * This method updates a resource's properties. + * + * The properties array must be a list of properties. Array-keys are + * property names in clarknotation, array-values are it's values. + * If a property must be deleted, the value should be null. + * + * Note that this request should either completely succeed, or + * completely fail. + * + * The response is an array with properties for keys, and http status codes + * as their values. + * + * @param string $path + * + * @return array + */ + public function updateProperties($path, array $properties) + { + $propPatch = new PropPatch($properties); + $this->emit('propPatch', [$path, $propPatch]); + $propPatch->commit(); + + return $propPatch->getResult(); + } + + /** + * This method checks the main HTTP preconditions. + * + * Currently these are: + * * If-Match + * * If-None-Match + * * If-Modified-Since + * * If-Unmodified-Since + * + * The method will return true if all preconditions are met + * The method will return false, or throw an exception if preconditions + * failed. If false is returned the operation should be aborted, and + * the appropriate HTTP response headers are already set. + * + * Normally this method will throw 412 Precondition Failed for failures + * related to If-None-Match, If-Match and If-Unmodified Since. It will + * set the status to 304 Not Modified for If-Modified_since. + * + * @return bool + */ + public function checkPreconditions(RequestInterface $request, ResponseInterface $response) + { + $path = $request->getPath(); + $node = null; + $lastMod = null; + $etag = null; + + if ($ifMatch = $request->getHeader('If-Match')) { + // If-Match contains an entity tag. Only if the entity-tag + // matches we are allowed to make the request succeed. + // If the entity-tag is '*' we are only allowed to make the + // request succeed if a resource exists at that url. + try { + $node = $this->tree->getNodeForPath($path); + } catch (Exception\NotFound $e) { + throw new Exception\PreconditionFailed('An If-Match header was specified and the resource did not exist', 'If-Match'); + } + + // Only need to check entity tags if they are not * + if ('*' !== $ifMatch) { + // There can be multiple ETags + $ifMatch = explode(',', $ifMatch); + $haveMatch = false; + foreach ($ifMatch as $ifMatchItem) { + // Stripping any extra spaces + $ifMatchItem = trim($ifMatchItem, ' '); + + $etag = $node instanceof IFile ? $node->getETag() : null; + if ($etag === $ifMatchItem) { + $haveMatch = true; + } else { + // Evolution has a bug where it sometimes prepends the " + // with a \. This is our workaround. + if (str_replace('\\"', '"', $ifMatchItem) === $etag) { + $haveMatch = true; + } + } + } + if (!$haveMatch) { + if ($etag) { + $response->setHeader('ETag', $etag); + } + throw new Exception\PreconditionFailed('An If-Match header was specified, but none of the specified ETags matched.', 'If-Match'); + } + } + } + + if ($ifNoneMatch = $request->getHeader('If-None-Match')) { + // The If-None-Match header contains an ETag. + // Only if the ETag does not match the current ETag, the request will succeed + // The header can also contain *, in which case the request + // will only succeed if the entity does not exist at all. + $nodeExists = true; + if (!$node) { + try { + $node = $this->tree->getNodeForPath($path); + } catch (Exception\NotFound $e) { + $nodeExists = false; + } + } + if ($nodeExists) { + $haveMatch = false; + if ('*' === $ifNoneMatch) { + $haveMatch = true; + } else { + // There might be multiple ETags + $ifNoneMatch = explode(',', $ifNoneMatch); + $etag = $node instanceof IFile ? $node->getETag() : null; + + foreach ($ifNoneMatch as $ifNoneMatchItem) { + // Stripping any extra spaces + $ifNoneMatchItem = trim($ifNoneMatchItem, ' '); + + if ($etag === $ifNoneMatchItem) { + $haveMatch = true; + } + } + } + + if ($haveMatch) { + if ($etag) { + $response->setHeader('ETag', $etag); + } + if ('GET' === $request->getMethod()) { + $response->setStatus(304); + + return false; + } else { + throw new Exception\PreconditionFailed('An If-None-Match header was specified, but the ETag matched (or * was specified).', 'If-None-Match'); + } + } + } + } + + if (!$ifNoneMatch && ($ifModifiedSince = $request->getHeader('If-Modified-Since'))) { + // The If-Modified-Since header contains a date. We + // will only return the entity if it has been changed since + // that date. If it hasn't been changed, we return a 304 + // header + // Note that this header only has to be checked if there was no If-None-Match header + // as per the HTTP spec. + $date = HTTP\parseDate($ifModifiedSince); + + if ($date) { + if (is_null($node)) { + $node = $this->tree->getNodeForPath($path); + } + $lastMod = $node->getLastModified(); + if ($lastMod) { + $lastMod = new \DateTime('@'.$lastMod); + if ($lastMod <= $date) { + $response->setStatus(304); + $response->setHeader('Last-Modified', HTTP\toDate($lastMod)); + + return false; + } + } + } + } + + if ($ifUnmodifiedSince = $request->getHeader('If-Unmodified-Since')) { + // The If-Unmodified-Since will allow allow the request if the + // entity has not changed since the specified date. + $date = HTTP\parseDate($ifUnmodifiedSince); + + // We must only check the date if it's valid + if ($date) { + if (is_null($node)) { + $node = $this->tree->getNodeForPath($path); + } + $lastMod = $node->getLastModified(); + if ($lastMod) { + $lastMod = new \DateTime('@'.$lastMod); + if ($lastMod > $date) { + throw new Exception\PreconditionFailed('An If-Unmodified-Since header was specified, but the entity has been changed since the specified date.', 'If-Unmodified-Since'); + } + } + } + } + + // Now the hardest, the If: header. The If: header can contain multiple + // urls, ETags and so-called 'state tokens'. + // + // Examples of state tokens include lock-tokens (as defined in rfc4918) + // and sync-tokens (as defined in rfc6578). + // + // The only proper way to deal with these, is to emit events, that a + // Sync and Lock plugin can pick up. + $ifConditions = $this->getIfConditions($request); + + foreach ($ifConditions as $kk => $ifCondition) { + foreach ($ifCondition['tokens'] as $ii => $token) { + $ifConditions[$kk]['tokens'][$ii]['validToken'] = false; + } + } + + // Plugins are responsible for validating all the tokens. + // If a plugin deemed a token 'valid', it will set 'validToken' to + // true. + $this->emit('validateTokens', [$request, &$ifConditions]); + + // Now we're going to analyze the result. + + // Every ifCondition needs to validate to true, so we exit as soon as + // we have an invalid condition. + foreach ($ifConditions as $ifCondition) { + $uri = $ifCondition['uri']; + $tokens = $ifCondition['tokens']; + + // We only need 1 valid token for the condition to succeed. + foreach ($tokens as $token) { + $tokenValid = $token['validToken'] || !$token['token']; + + $etagValid = false; + if (!$token['etag']) { + $etagValid = true; + } + // Checking the ETag, only if the token was already deemed + // valid and there is one. + if ($token['etag'] && $tokenValid) { + // The token was valid, and there was an ETag. We must + // grab the current ETag and check it. + $node = $this->tree->getNodeForPath($uri); + $etagValid = $node instanceof IFile && $node->getETag() == $token['etag']; + } + + if (($tokenValid && $etagValid) ^ $token['negate']) { + // Both were valid, so we can go to the next condition. + continue 2; + } + } + + // If we ended here, it means there was no valid ETag + token + // combination found for the current condition. This means we fail! + throw new Exception\PreconditionFailed('Failed to find a valid token/etag combination for '.$uri, 'If'); + } + + return true; + } + + /** + * This method is created to extract information from the WebDAV HTTP 'If:' header. + * + * The If header can be quite complex, and has a bunch of features. We're using a regex to extract all relevant information + * The function will return an array, containing structs with the following keys + * + * * uri - the uri the condition applies to. + * * tokens - The lock token. another 2 dimensional array containing 3 elements + * + * Example 1: + * + * If: () + * + * Would result in: + * + * [ + * [ + * 'uri' => '/request/uri', + * 'tokens' => [ + * [ + * [ + * 'negate' => false, + * 'token' => 'opaquelocktoken:181d4fae-7d8c-11d0-a765-00a0c91e6bf2', + * 'etag' => "" + * ] + * ] + * ], + * ] + * ] + * + * Example 2: + * + * If: (Not ["Im An ETag"]) (["Another ETag"]) (Not ["Path2 ETag"]) + * + * Would result in: + * + * [ + * [ + * 'uri' => 'path', + * 'tokens' => [ + * [ + * [ + * 'negate' => true, + * 'token' => 'opaquelocktoken:181d4fae-7d8c-11d0-a765-00a0c91e6bf2', + * 'etag' => '"Im An ETag"' + * ], + * [ + * 'negate' => false, + * 'token' => '', + * 'etag' => '"Another ETag"' + * ] + * ] + * ], + * ], + * [ + * 'uri' => 'path2', + * 'tokens' => [ + * [ + * [ + * 'negate' => true, + * 'token' => '', + * 'etag' => '"Path2 ETag"' + * ] + * ] + * ], + * ], + * ] + * + * @return array + */ + public function getIfConditions(RequestInterface $request) + { + $header = $request->getHeader('If'); + if (!$header) { + return []; + } + + $matches = []; + + $regex = '/(?:\<(?P.*?)\>\s)?\((?PNot\s)?(?:\<(?P[^\>]*)\>)?(?:\s?)(?:\[(?P[^\]]*)\])?\)/im'; + preg_match_all($regex, $header, $matches, PREG_SET_ORDER); + + $conditions = []; + + foreach ($matches as $match) { + // If there was no uri specified in this match, and there were + // already conditions parsed, we add the condition to the list of + // conditions for the previous uri. + if (!$match['uri'] && count($conditions)) { + $conditions[count($conditions) - 1]['tokens'][] = [ + 'negate' => $match['not'] ? true : false, + 'token' => $match['token'], + 'etag' => isset($match['etag']) ? $match['etag'] : '', + ]; + } else { + if (!$match['uri']) { + $realUri = $request->getPath(); + } else { + $realUri = $this->calculateUri($match['uri']); + } + + $conditions[] = [ + 'uri' => $realUri, + 'tokens' => [ + [ + 'negate' => $match['not'] ? true : false, + 'token' => $match['token'], + 'etag' => isset($match['etag']) ? $match['etag'] : '', + ], + ], + ]; + } + } + + return $conditions; + } + + /** + * Returns an array with resourcetypes for a node. + * + * @return array + */ + public function getResourceTypeForNode(INode $node) + { + $result = []; + foreach ($this->resourceTypeMapping as $className => $resourceType) { + if ($node instanceof $className) { + $result[] = $resourceType; + } + } + + return $result; + } + + // }}} + // {{{ XML Readers & Writers + + /** + * Returns a callback generating a WebDAV propfind response body based on a list of nodes. + * + * If 'strip404s' is set to true, all 404 responses will be removed. + * + * @param array|\Traversable $fileProperties The list with nodes + * @param bool $strip404s + * + * @return callable|string + */ + public function generateMultiStatus($fileProperties, $strip404s = false) + { + $this->emit('beforeMultiStatus', [&$fileProperties]); + + $w = $this->xml->getWriter(); + if (self::$streamMultiStatus) { + return function () use ($fileProperties, $strip404s, $w) { + $w->openUri('php://output'); + $this->writeMultiStatus($w, $fileProperties, $strip404s); + $w->flush(); + }; + } + $w->openMemory(); + $this->writeMultiStatus($w, $fileProperties, $strip404s); + + return $w->outputMemory(); + } + + /** + * @param $fileProperties + */ + private function writeMultiStatus(Writer $w, $fileProperties, bool $strip404s) + { + $w->contextUri = $this->baseUri; + $w->startDocument(); + + $w->startElement('{DAV:}multistatus'); + + foreach ($fileProperties as $entry) { + $href = $entry['href']; + unset($entry['href']); + if ($strip404s) { + unset($entry[404]); + } + $response = new Xml\Element\Response( + ltrim($href, '/'), + $entry + ); + $w->write([ + 'name' => '{DAV:}response', + 'value' => $response, + ]); + } + $w->endElement(); + $w->endDocument(); + } +} diff --git a/3rdparty/sabre/dav/lib/DAV/ServerPlugin.php b/3rdparty/sabre/dav/lib/DAV/ServerPlugin.php new file mode 100644 index 00000000..70acb01e --- /dev/null +++ b/3rdparty/sabre/dav/lib/DAV/ServerPlugin.php @@ -0,0 +1,105 @@ + $this->getPluginName(), + 'description' => null, + 'link' => null, + ]; + } +} diff --git a/3rdparty/sabre/dav/lib/DAV/Sharing/ISharedNode.php b/3rdparty/sabre/dav/lib/DAV/Sharing/ISharedNode.php new file mode 100644 index 00000000..a746ac75 --- /dev/null +++ b/3rdparty/sabre/dav/lib/DAV/Sharing/ISharedNode.php @@ -0,0 +1,69 @@ +server = $server; + + $server->xml->elementMap['{DAV:}share-resource'] = 'Sabre\\DAV\\Xml\\Request\\ShareResource'; + + array_push( + $server->protectedProperties, + '{DAV:}share-mode' + ); + + $server->on('method:POST', [$this, 'httpPost']); + $server->on('propFind', [$this, 'propFind']); + $server->on('getSupportedPrivilegeSet', [$this, 'getSupportedPrivilegeSet']); + $server->on('onHTMLActionsPanel', [$this, 'htmlActionsPanel']); + $server->on('onBrowserPostAction', [$this, 'browserPostAction']); + } + + /** + * Updates the list of sharees on a shared resource. + * + * The sharees array is a list of people that are to be added modified + * or removed in the list of shares. + * + * @param string $path + * @param Sharee[] $sharees + */ + public function shareResource($path, array $sharees) + { + $node = $this->server->tree->getNodeForPath($path); + + if (!$node instanceof ISharedNode) { + throw new Forbidden('Sharing is not allowed on this node'); + } + + // Getting ACL info + $acl = $this->server->getPlugin('acl'); + + // If there's no ACL support, we allow everything + if ($acl) { + $acl->checkPrivileges($path, '{DAV:}share'); + } + + foreach ($sharees as $sharee) { + // We're going to attempt to get a local principal uri for a share + // href by emitting the getPrincipalByUri event. + $principal = null; + $this->server->emit('getPrincipalByUri', [$sharee->href, &$principal]); + $sharee->principal = $principal; + } + $node->updateInvites($sharees); + } + + /** + * This event is triggered when properties are requested for nodes. + * + * This allows us to inject any sharings-specific properties. + */ + public function propFind(PropFind $propFind, INode $node) + { + if ($node instanceof ISharedNode) { + $propFind->handle('{DAV:}share-access', function () use ($node) { + return new Property\ShareAccess($node->getShareAccess()); + }); + $propFind->handle('{DAV:}invite', function () use ($node) { + return new Property\Invite($node->getInvites()); + }); + $propFind->handle('{DAV:}share-resource-uri', function () use ($node) { + return new Property\Href($node->getShareResourceUri()); + }); + } + } + + /** + * We intercept this to handle POST requests on shared resources. + * + * @return bool|null + */ + public function httpPost(RequestInterface $request, ResponseInterface $response) + { + $path = $request->getPath(); + $contentType = $request->getHeader('Content-Type'); + if (null === $contentType) { + return; + } + + // We're only interested in the davsharing content type. + if (false === strpos($contentType, 'application/davsharing+xml')) { + return; + } + + $message = $this->server->xml->parse( + $request->getBody(), + $request->getUrl(), + $documentType + ); + + switch ($documentType) { + case '{DAV:}share-resource': + $this->shareResource($path, $message->sharees); + $response->setStatus(200); + // Adding this because sending a response body may cause issues, + // and I wanted some type of indicator the response was handled. + $response->setHeader('X-Sabre-Status', 'everything-went-well'); + + // Breaking the event chain + return false; + + default: + throw new BadRequest('Unexpected document type: '.$documentType.' for this Content-Type'); + } + } + + /** + * This method is triggered whenever a subsystem requests the privileges + * hat are supported on a particular node. + * + * We need to add a number of privileges for scheduling purposes. + */ + public function getSupportedPrivilegeSet(INode $node, array &$supportedPrivilegeSet) + { + if ($node instanceof ISharedNode) { + $supportedPrivilegeSet['{DAV:}share'] = [ + 'abstract' => false, + 'aggregates' => [], + ]; + } + } + + /** + * Returns a bunch of meta-data about the plugin. + * + * Providing this information is optional, and is mainly displayed by the + * Browser plugin. + * + * The description key in the returned array may contain html and will not + * be sanitized. + * + * @return array + */ + public function getPluginInfo() + { + return [ + 'name' => $this->getPluginName(), + 'description' => 'This plugin implements WebDAV resource sharing', + 'link' => 'https://github.com/evert/webdav-sharing', + ]; + } + + /** + * This method is used to generate HTML output for the + * DAV\Browser\Plugin. + * + * @param string $output + * @param string $path + * + * @return bool|null + */ + public function htmlActionsPanel(INode $node, &$output, $path) + { + if (!$node instanceof ISharedNode) { + return; + } + + $aclPlugin = $this->server->getPlugin('acl'); + if ($aclPlugin) { + if (!$aclPlugin->checkPrivileges($path, '{DAV:}share', \Sabre\DAVACL\Plugin::R_PARENT, false)) { + // Sharing is not permitted, we will not draw this interface. + return; + } + } + + $output .= '

    +

    Share this resource

    + +
    + +
    + + + '; + } + + /** + * This method is triggered for POST actions generated by the browser + * plugin. + * + * @param string $path + * @param string $action + * @param array $postVars + */ + public function browserPostAction($path, $action, $postVars) + { + if ('share' !== $action) { + return; + } + + if (empty($postVars['href'])) { + throw new BadRequest('The "href" POST parameter is required'); + } + if (empty($postVars['access'])) { + throw new BadRequest('The "access" POST parameter is required'); + } + + $accessMap = [ + 'readwrite' => self::ACCESS_READWRITE, + 'read' => self::ACCESS_READ, + 'no-access' => self::ACCESS_NOACCESS, + ]; + + if (!isset($accessMap[$postVars['access']])) { + throw new BadRequest('The "access" POST must be readwrite, read or no-access'); + } + $sharee = new Sharee([ + 'href' => $postVars['href'], + 'access' => $accessMap[$postVars['access']], + ]); + + $this->shareResource( + $path, + [$sharee] + ); + + return false; + } +} diff --git a/3rdparty/sabre/dav/lib/DAV/SimpleCollection.php b/3rdparty/sabre/dav/lib/DAV/SimpleCollection.php new file mode 100644 index 00000000..3cd14d9b --- /dev/null +++ b/3rdparty/sabre/dav/lib/DAV/SimpleCollection.php @@ -0,0 +1,109 @@ +name = $name; + foreach ($children as $key => $child) { + if (is_string($child)) { + $child = new SimpleFile($key, $child); + } elseif (is_array($child)) { + $child = new self($key, $child); + } elseif (!$child instanceof INode) { + throw new InvalidArgumentException('Children must be specified as strings, arrays or instances of Sabre\DAV\INode'); + } + $this->addChild($child); + } + } + + /** + * Adds a new childnode to this collection. + */ + public function addChild(INode $child) + { + $this->children[$child->getName()] = $child; + } + + /** + * Returns the name of the collection. + * + * @return string + */ + public function getName() + { + return $this->name; + } + + /** + * Returns a child object, by its name. + * + * This method makes use of the getChildren method to grab all the child nodes, and compares the name. + * Generally its wise to override this, as this can usually be optimized + * + * This method must throw Sabre\DAV\Exception\NotFound if the node does not + * exist. + * + * @param string $name + * + * @throws Exception\NotFound + * + * @return INode + */ + public function getChild($name) + { + if (isset($this->children[$name])) { + return $this->children[$name]; + } + throw new Exception\NotFound('File not found: '.$name.' in \''.$this->getName().'\''); + } + + /** + * Returns a list of children for this collection. + * + * @return INode[] + */ + public function getChildren() + { + return array_values($this->children); + } +} diff --git a/3rdparty/sabre/dav/lib/DAV/SimpleFile.php b/3rdparty/sabre/dav/lib/DAV/SimpleFile.php new file mode 100644 index 00000000..ca808b67 --- /dev/null +++ b/3rdparty/sabre/dav/lib/DAV/SimpleFile.php @@ -0,0 +1,118 @@ +name = $name; + $this->contents = $contents; + $this->mimeType = $mimeType; + } + + /** + * Returns the node name for this file. + * + * This name is used to construct the url. + * + * @return string + */ + public function getName() + { + return $this->name; + } + + /** + * Returns the data. + * + * This method may either return a string or a readable stream resource + * + * @return mixed + */ + public function get() + { + return $this->contents; + } + + /** + * Returns the size of the file, in bytes. + * + * @return int + */ + public function getSize() + { + return strlen($this->contents); + } + + /** + * Returns the ETag for a file. + * + * An ETag is a unique identifier representing the current version of the file. If the file changes, the ETag MUST change. + * The ETag is an arbitrary string, but MUST be surrounded by double-quotes. + * + * Return null if the ETag can not effectively be determined + * + * @return string + */ + public function getETag() + { + return '"'.sha1($this->contents).'"'; + } + + /** + * Returns the mime-type for a file. + * + * If null is returned, we'll assume application/octet-stream + * + * @return string + */ + public function getContentType() + { + return $this->mimeType; + } +} diff --git a/3rdparty/sabre/dav/lib/DAV/StringUtil.php b/3rdparty/sabre/dav/lib/DAV/StringUtil.php new file mode 100644 index 00000000..edfb7fa5 --- /dev/null +++ b/3rdparty/sabre/dav/lib/DAV/StringUtil.php @@ -0,0 +1,86 @@ + 'The current synctoken', + * 'added' => [ + * 'new.txt', + * ], + * 'modified' => [ + * 'modified.txt', + * ], + * 'deleted' => array( + * 'foo.php.bak', + * 'old.txt' + * ) + * ]; + * + * The syncToken property should reflect the *current* syncToken of the + * collection, as reported getSyncToken(). This is needed here too, to + * ensure the operation is atomic. + * + * If the syncToken is specified as null, this is an initial sync, and all + * members should be reported. + * + * The modified property is an array of nodenames that have changed since + * the last token. + * + * The deleted property is an array with nodenames, that have been deleted + * from collection. + * + * The second argument is basically the 'depth' of the report. If it's 1, + * you only have to report changes that happened only directly in immediate + * descendants. If it's 2, it should also include changes from the nodes + * below the child collections. (grandchildren) + * + * The third (optional) argument allows a client to specify how many + * results should be returned at most. If the limit is not specified, it + * should be treated as infinite. + * + * If the limit (infinite or not) is higher than you're willing to return, + * you should throw a Sabre\DAV\Exception\TooMuchMatches() exception. + * + * If the syncToken is expired (due to data cleanup) or unknown, you must + * return null. + * + * The limit is 'suggestive'. You are free to ignore it. + * + * @param string $syncToken + * @param int $syncLevel + * @param int $limit + * + * @return array|null + */ + public function getChanges($syncToken, $syncLevel, $limit = null); +} diff --git a/3rdparty/sabre/dav/lib/DAV/Sync/Plugin.php b/3rdparty/sabre/dav/lib/DAV/Sync/Plugin.php new file mode 100644 index 00000000..8609f759 --- /dev/null +++ b/3rdparty/sabre/dav/lib/DAV/Sync/Plugin.php @@ -0,0 +1,249 @@ +server = $server; + $server->xml->elementMap['{DAV:}sync-collection'] = 'Sabre\\DAV\\Xml\\Request\\SyncCollectionReport'; + + $self = $this; + + $server->on('report', function ($reportName, $dom, $uri) use ($self) { + if ('{DAV:}sync-collection' === $reportName) { + $this->server->transactionType = 'report-sync-collection'; + $self->syncCollection($uri, $dom); + + return false; + } + }); + + $server->on('propFind', [$this, 'propFind']); + $server->on('validateTokens', [$this, 'validateTokens']); + } + + /** + * Returns a list of reports this plugin supports. + * + * This will be used in the {DAV:}supported-report-set property. + * Note that you still need to subscribe to the 'report' event to actually + * implement them + * + * @param string $uri + * + * @return array + */ + public function getSupportedReportSet($uri) + { + $node = $this->server->tree->getNodeForPath($uri); + if ($node instanceof ISyncCollection && $node->getSyncToken()) { + return [ + '{DAV:}sync-collection', + ]; + } + + return []; + } + + /** + * This method handles the {DAV:}sync-collection HTTP REPORT. + * + * @param string $uri + */ + public function syncCollection($uri, SyncCollectionReport $report) + { + // Getting the data + $node = $this->server->tree->getNodeForPath($uri); + if (!$node instanceof ISyncCollection) { + throw new DAV\Exception\ReportNotSupported('The {DAV:}sync-collection REPORT is not supported on this url.'); + } + $token = $node->getSyncToken(); + if (!$token) { + throw new DAV\Exception\ReportNotSupported('No sync information is available at this node'); + } + + $syncToken = $report->syncToken; + if (!is_null($syncToken)) { + // Sync-token must start with our prefix + if (self::SYNCTOKEN_PREFIX !== substr($syncToken, 0, strlen(self::SYNCTOKEN_PREFIX))) { + throw new DAV\Exception\InvalidSyncToken('Invalid or unknown sync token'); + } + + $syncToken = substr($syncToken, strlen(self::SYNCTOKEN_PREFIX)); + } + $changeInfo = $node->getChanges($syncToken, $report->syncLevel, $report->limit); + + if (is_null($changeInfo)) { + throw new DAV\Exception\InvalidSyncToken('Invalid or unknown sync token'); + } + + if (!array_key_exists('result_truncated', $changeInfo)) { + $changeInfo['result_truncated'] = false; + } + + // Encoding the response + $this->sendSyncCollectionResponse( + $changeInfo['syncToken'], + $uri, + $changeInfo['added'], + $changeInfo['modified'], + $changeInfo['deleted'], + $report->properties, + $changeInfo['result_truncated'] + ); + } + + /** + * Sends the response to a sync-collection request. + * + * @param string $syncToken + * @param string $collectionUrl + */ + protected function sendSyncCollectionResponse($syncToken, $collectionUrl, array $added, array $modified, array $deleted, array $properties, bool $resultTruncated = false) + { + $fullPaths = []; + + // Pre-fetching children, if this is possible. + foreach (array_merge($added, $modified) as $item) { + $fullPath = $collectionUrl.'/'.$item; + $fullPaths[] = $fullPath; + } + + $responses = []; + foreach ($this->server->getPropertiesForMultiplePaths($fullPaths, $properties) as $fullPath => $props) { + // The 'Property_Response' class is responsible for generating a + // single {DAV:}response xml element. + $responses[] = new DAV\Xml\Element\Response($fullPath, $props); + } + + // Deleted items also show up as 'responses'. They have no properties, + // and a single {DAV:}status element set as 'HTTP/1.1 404 Not Found'. + foreach ($deleted as $item) { + $fullPath = $collectionUrl.'/'.$item; + $responses[] = new DAV\Xml\Element\Response($fullPath, [], 404); + } + if ($resultTruncated) { + $responses[] = new DAV\Xml\Element\Response($collectionUrl.'/', [], 507); + } + + $multiStatus = new DAV\Xml\Response\MultiStatus($responses, self::SYNCTOKEN_PREFIX.$syncToken); + + $this->server->httpResponse->setStatus(207); + $this->server->httpResponse->setHeader('Content-Type', 'application/xml; charset=utf-8'); + $this->server->httpResponse->setBody( + $this->server->xml->write('{DAV:}multistatus', $multiStatus, $this->server->getBaseUri()) + ); + } + + /** + * This method is triggered whenever properties are requested for a node. + * We intercept this to see if we must return a {DAV:}sync-token. + */ + public function propFind(DAV\PropFind $propFind, DAV\INode $node) + { + $propFind->handle('{DAV:}sync-token', function () use ($node) { + if (!$node instanceof ISyncCollection || !$token = $node->getSyncToken()) { + return; + } + + return self::SYNCTOKEN_PREFIX.$token; + }); + } + + /** + * The validateTokens event is triggered before every request. + * + * It's a moment where this plugin can check all the supplied lock tokens + * in the If: header, and check if they are valid. + * + * @param array $conditions + */ + public function validateTokens(RequestInterface $request, &$conditions) + { + foreach ($conditions as $kk => $condition) { + foreach ($condition['tokens'] as $ii => $token) { + // Sync-tokens must always start with our designated prefix. + if (self::SYNCTOKEN_PREFIX !== substr($token['token'], 0, strlen(self::SYNCTOKEN_PREFIX))) { + continue; + } + + // Checking if the token is a match. + $node = $this->server->tree->getNodeForPath($condition['uri']); + + if ( + $node instanceof ISyncCollection && + $node->getSyncToken() == substr($token['token'], strlen(self::SYNCTOKEN_PREFIX)) + ) { + $conditions[$kk]['tokens'][$ii]['validToken'] = true; + } + } + } + } + + /** + * Returns a bunch of meta-data about the plugin. + * + * Providing this information is optional, and is mainly displayed by the + * Browser plugin. + * + * The description key in the returned array may contain html and will not + * be sanitized. + * + * @return array + */ + public function getPluginInfo() + { + return [ + 'name' => $this->getPluginName(), + 'description' => 'Adds support for WebDAV Collection Sync (rfc6578)', + 'link' => 'http://sabre.io/dav/sync/', + ]; + } +} diff --git a/3rdparty/sabre/dav/lib/DAV/TemporaryFileFilterPlugin.php b/3rdparty/sabre/dav/lib/DAV/TemporaryFileFilterPlugin.php new file mode 100644 index 00000000..9f8ec5b5 --- /dev/null +++ b/3rdparty/sabre/dav/lib/DAV/TemporaryFileFilterPlugin.php @@ -0,0 +1,298 @@ +dataDir = $dataDir; + } + + /** + * Initialize the plugin. + * + * This is called automatically be the Server class after this plugin is + * added with Sabre\DAV\Server::addPlugin() + */ + public function initialize(Server $server) + { + $this->server = $server; + $server->on('beforeMethod:*', [$this, 'beforeMethod']); + $server->on('beforeCreateFile', [$this, 'beforeCreateFile']); + } + + /** + * This method is called before any HTTP method handler. + * + * This method intercepts any GET, DELETE, PUT and PROPFIND calls to + * filenames that are known to match the 'temporary file' regex. + * + * @return bool + */ + public function beforeMethod(RequestInterface $request, ResponseInterface $response) + { + if (!$tempLocation = $this->isTempFile($request->getPath())) { + return; + } + + switch ($request->getMethod()) { + case 'GET': + return $this->httpGet($request, $response, $tempLocation); + case 'PUT': + return $this->httpPut($request, $response, $tempLocation); + case 'PROPFIND': + return $this->httpPropfind($request, $response, $tempLocation); + case 'DELETE': + return $this->httpDelete($request, $response, $tempLocation); + } + + return; + } + + /** + * This method is invoked if some subsystem creates a new file. + * + * This is used to deal with HTTP LOCK requests which create a new + * file. + * + * @param string $uri + * @param resource $data + * @param bool $modified should be set to true, if this event handler + * changed &$data + * + * @return bool + */ + public function beforeCreateFile($uri, $data, ICollection $parent, $modified) + { + if ($tempPath = $this->isTempFile($uri)) { + $hR = $this->server->httpResponse; + $hR->setHeader('X-Sabre-Temp', 'true'); + file_put_contents($tempPath, $data); + + return false; + } + + return; + } + + /** + * This method will check if the url matches the temporary file pattern + * if it does, it will return an path based on $this->dataDir for the + * temporary file storage. + * + * @param string $path + * + * @return bool|string + */ + protected function isTempFile($path) + { + // We're only interested in the basename. + list(, $tempPath) = Uri\split($path); + + if (null === $tempPath) { + return false; + } + + foreach ($this->temporaryFilePatterns as $tempFile) { + if (preg_match($tempFile, $tempPath)) { + return $this->getDataDir().'/sabredav_'.md5($path).'.tempfile'; + } + } + + return false; + } + + /** + * This method handles the GET method for temporary files. + * If the file doesn't exist, it will return false which will kick in + * the regular system for the GET method. + * + * @param string $tempLocation + * + * @return bool + */ + public function httpGet(RequestInterface $request, ResponseInterface $hR, $tempLocation) + { + if (!file_exists($tempLocation)) { + return; + } + + $hR->setHeader('Content-Type', 'application/octet-stream'); + $hR->setHeader('Content-Length', filesize($tempLocation)); + $hR->setHeader('X-Sabre-Temp', 'true'); + $hR->setStatus(200); + $hR->setBody(fopen($tempLocation, 'r')); + + return false; + } + + /** + * This method handles the PUT method. + * + * @param string $tempLocation + * + * @return bool + */ + public function httpPut(RequestInterface $request, ResponseInterface $hR, $tempLocation) + { + $hR->setHeader('X-Sabre-Temp', 'true'); + + $newFile = !file_exists($tempLocation); + + if (!$newFile && ($this->server->httpRequest->getHeader('If-None-Match'))) { + throw new Exception\PreconditionFailed('The resource already exists, and an If-None-Match header was supplied'); + } + + file_put_contents($tempLocation, $this->server->httpRequest->getBody()); + $hR->setStatus($newFile ? 201 : 200); + + return false; + } + + /** + * This method handles the DELETE method. + * + * If the file didn't exist, it will return false, which will make the + * standard HTTP DELETE handler kick in. + * + * @param string $tempLocation + * + * @return bool + */ + public function httpDelete(RequestInterface $request, ResponseInterface $hR, $tempLocation) + { + if (!file_exists($tempLocation)) { + return; + } + + unlink($tempLocation); + $hR->setHeader('X-Sabre-Temp', 'true'); + $hR->setStatus(204); + + return false; + } + + /** + * This method handles the PROPFIND method. + * + * It's a very lazy method, it won't bother checking the request body + * for which properties were requested, and just sends back a default + * set of properties. + * + * @param string $tempLocation + * + * @return bool + */ + public function httpPropfind(RequestInterface $request, ResponseInterface $hR, $tempLocation) + { + if (!file_exists($tempLocation)) { + return; + } + + $hR->setHeader('X-Sabre-Temp', 'true'); + $hR->setStatus(207); + $hR->setHeader('Content-Type', 'application/xml; charset=utf-8'); + + $properties = [ + 'href' => $request->getPath(), + 200 => [ + '{DAV:}getlastmodified' => new Xml\Property\GetLastModified(filemtime($tempLocation)), + '{DAV:}getcontentlength' => filesize($tempLocation), + '{DAV:}resourcetype' => new Xml\Property\ResourceType(null), + '{'.Server::NS_SABREDAV.'}tempFile' => true, + ], + ]; + + $data = $this->server->generateMultiStatus([$properties]); + $hR->setBody($data); + + return false; + } + + /** + * This method returns the directory where the temporary files should be stored. + * + * @return string + */ + protected function getDataDir() + { + return $this->dataDir; + } +} diff --git a/3rdparty/sabre/dav/lib/DAV/Tree.php b/3rdparty/sabre/dav/lib/DAV/Tree.php new file mode 100644 index 00000000..1483e1bc --- /dev/null +++ b/3rdparty/sabre/dav/lib/DAV/Tree.php @@ -0,0 +1,342 @@ +rootNode = $rootNode; + } + + /** + * Returns the INode object for the requested path. + * + * @param string $path + * + * @return INode + */ + public function getNodeForPath($path) + { + $path = trim($path, '/'); + if (isset($this->cache[$path])) { + return $this->cache[$path]; + } + + // Is it the root node? + if (!strlen($path)) { + return $this->rootNode; + } + + $node = $this->rootNode; + + // look for any cached parent and collect the parts below the parent + $parts = []; + $remainingPath = $path; + do { + list($remainingPath, $baseName) = Uri\split($remainingPath); + array_unshift($parts, $baseName); + + if (isset($this->cache[$remainingPath])) { + $node = $this->cache[$remainingPath]; + break; + } + } while ('' !== $remainingPath); + + while (count($parts)) { + if (!($node instanceof ICollection)) { + throw new Exception\NotFound('Could not find node at path: '.$path); + } + + if ($node instanceof INodeByPath) { + $targetNode = $node->getNodeForPath(implode('/', $parts)); + if ($targetNode instanceof Node) { + $node = $targetNode; + break; + } + } + + $part = array_shift($parts); + if ('' !== $part) { + $node = $node->getChild($part); + } + } + + $this->cache[$path] = $node; + + return $node; + } + + /** + * This function allows you to check if a node exists. + * + * Implementors of this class should override this method to make + * it cheaper. + * + * @param string $path + * + * @return bool + */ + public function nodeExists($path) + { + try { + // The root always exists + if ('' === $path) { + return true; + } + + list($parent, $base) = Uri\split($path); + + $parentNode = $this->getNodeForPath($parent); + if (!$parentNode instanceof ICollection) { + return false; + } + + return $parentNode->childExists($base); + } catch (Exception\NotFound $e) { + return false; + } + } + + /** + * Copies a file from path to another. + * + * @param string $sourcePath The source location + * @param string $destinationPath The full destination path + */ + public function copy($sourcePath, $destinationPath) + { + $sourceNode = $this->getNodeForPath($sourcePath); + + // grab the dirname and basename components + list($destinationDir, $destinationName) = Uri\split($destinationPath); + + $destinationParent = $this->getNodeForPath($destinationDir); + // Check if the target can handle the copy itself. If not, we do it ourselves. + if (!$destinationParent instanceof ICopyTarget || !$destinationParent->copyInto($destinationName, $sourcePath, $sourceNode)) { + $this->copyNode($sourceNode, $destinationParent, $destinationName); + } + + $this->markDirty($destinationDir); + } + + /** + * Moves a file from one location to another. + * + * @param string $sourcePath The path to the file which should be moved + * @param string $destinationPath The full destination path, so not just the destination parent node + */ + public function move($sourcePath, $destinationPath) + { + list($sourceDir) = Uri\split($sourcePath); + list($destinationDir, $destinationName) = Uri\split($destinationPath); + + if ($sourceDir === $destinationDir) { + // If this is a 'local' rename, it means we can just trigger a rename. + $sourceNode = $this->getNodeForPath($sourcePath); + $sourceNode->setName($destinationName); + } else { + $newParentNode = $this->getNodeForPath($destinationDir); + $moveSuccess = false; + if ($newParentNode instanceof IMoveTarget) { + // The target collection may be able to handle the move + $sourceNode = $this->getNodeForPath($sourcePath); + $moveSuccess = $newParentNode->moveInto($destinationName, $sourcePath, $sourceNode); + } + if (!$moveSuccess) { + $this->copy($sourcePath, $destinationPath); + $this->getNodeForPath($sourcePath)->delete(); + } + } + $this->markDirty($sourceDir); + $this->markDirty($destinationDir); + } + + /** + * Deletes a node from the tree. + * + * @param string $path + */ + public function delete($path) + { + $node = $this->getNodeForPath($path); + $node->delete(); + + list($parent) = Uri\split($path); + $this->markDirty($parent); + } + + /** + * Returns a list of childnodes for a given path. + * + * @param string $path + * + * @return \Traversable + */ + public function getChildren($path) + { + $node = $this->getNodeForPath($path); + $basePath = trim($path, '/'); + if ('' !== $basePath) { + $basePath .= '/'; + } + + foreach ($node->getChildren() as $child) { + $this->cache[$basePath.$child->getName()] = $child; + yield $child; + } + } + + /** + * This method is called with every tree update. + * + * Examples of tree updates are: + * * node deletions + * * node creations + * * copy + * * move + * * renaming nodes + * + * If Tree classes implement a form of caching, this will allow + * them to make sure caches will be expired. + * + * If a path is passed, it is assumed that the entire subtree is dirty + * + * @param string $path + */ + public function markDirty($path) + { + // We don't care enough about sub-paths + // flushing the entire cache + $path = trim($path, '/'); + foreach ($this->cache as $nodePath => $node) { + if ('' === $path || $nodePath == $path || 0 === strpos((string) $nodePath, $path.'/')) { + unset($this->cache[$nodePath]); + } + } + } + + /** + * This method tells the tree system to pre-fetch and cache a list of + * children of a single parent. + * + * There are a bunch of operations in the WebDAV stack that request many + * children (based on uris), and sometimes fetching many at once can + * optimize this. + * + * This method returns an array with the found nodes. It's keys are the + * original paths. The result may be out of order. + * + * @param array $paths list of nodes that must be fetched + * + * @return array + */ + public function getMultipleNodes($paths) + { + // Finding common parents + $parents = []; + foreach ($paths as $path) { + list($parent, $node) = Uri\split($path); + if (!isset($parents[$parent])) { + $parents[$parent] = [$node]; + } else { + $parents[$parent][] = $node; + } + } + + $result = []; + + foreach ($parents as $parent => $children) { + $parentNode = $this->getNodeForPath($parent); + if ($parentNode instanceof IMultiGet) { + foreach ($parentNode->getMultipleChildren($children) as $childNode) { + $fullPath = $parent.'/'.$childNode->getName(); + $result[$fullPath] = $childNode; + $this->cache[$fullPath] = $childNode; + } + } else { + foreach ($children as $child) { + $fullPath = $parent.'/'.$child; + $result[$fullPath] = $this->getNodeForPath($fullPath); + } + } + } + + return $result; + } + + /** + * copyNode. + * + * @param string $destinationName + */ + protected function copyNode(INode $source, ICollection $destinationParent, $destinationName = null) + { + if ('' === (string) $destinationName) { + $destinationName = $source->getName(); + } + + $destination = null; + + if ($source instanceof IFile) { + $data = $source->get(); + + // If the body was a string, we need to convert it to a stream + if (is_string($data)) { + $stream = fopen('php://temp', 'r+'); + fwrite($stream, $data); + rewind($stream); + $data = $stream; + } + $destinationParent->createFile($destinationName, $data); + $destination = $destinationParent->getChild($destinationName); + } elseif ($source instanceof ICollection) { + $destinationParent->createDirectory($destinationName); + + $destination = $destinationParent->getChild($destinationName); + foreach ($source->getChildren() as $child) { + $this->copyNode($child, $destination); + } + } + if ($source instanceof IProperties && $destination instanceof IProperties) { + $props = $source->getProperties([]); + $propPatch = new PropPatch($props); + $destination->propPatch($propPatch); + $propPatch->commit(); + } + } +} diff --git a/3rdparty/sabre/dav/lib/DAV/UUIDUtil.php b/3rdparty/sabre/dav/lib/DAV/UUIDUtil.php new file mode 100644 index 00000000..8c36e1b0 --- /dev/null +++ b/3rdparty/sabre/dav/lib/DAV/UUIDUtil.php @@ -0,0 +1,66 @@ +value array. + * + * @copyright Copyright (C) fruux GmbH (https://fruux.com/) + * @author Evert Pot (http://evertpot.com/) + * @license http://sabre.io/license/ Modified BSD License + */ +class Prop implements XmlDeserializable +{ + /** + * The deserialize method is called during xml parsing. + * + * This method is called statically, this is because in theory this method + * may be used as a type of constructor, or factory method. + * + * Often you want to return an instance of the current class, but you are + * free to return other data as well. + * + * You are responsible for advancing the reader to the next element. Not + * doing anything will result in a never-ending loop. + * + * If you just want to skip parsing for this element altogether, you can + * just call $reader->next(); + * + * $reader->parseInnerTree() will parse the entire sub-tree, and advance to + * the next element. + * + * @return mixed + */ + public static function xmlDeserialize(Reader $reader) + { + // If there's no children, we don't do anything. + if ($reader->isEmptyElement) { + $reader->next(); + + return []; + } + + $values = []; + + $reader->read(); + do { + if (Reader::ELEMENT === $reader->nodeType) { + $clark = $reader->getClark(); + $values[$clark] = self::parseCurrentElement($reader)['value']; + } else { + $reader->read(); + } + } while (Reader::END_ELEMENT !== $reader->nodeType); + + $reader->read(); + + return $values; + } + + /** + * This function behaves similar to Sabre\Xml\Reader::parseCurrentElement, + * but instead of creating deep xml array structures, it will turn any + * top-level element it doesn't recognize into either a string, or an + * XmlFragment class. + * + * This method returns arn array with 2 properties: + * * name - A clark-notation XML element name. + * * value - The parsed value. + * + * @return array + */ + private static function parseCurrentElement(Reader $reader) + { + $name = $reader->getClark(); + + if (array_key_exists($name, $reader->elementMap)) { + $deserializer = $reader->elementMap[$name]; + if (is_subclass_of($deserializer, 'Sabre\\Xml\\XmlDeserializable')) { + $value = call_user_func([$deserializer, 'xmlDeserialize'], $reader); + } elseif (is_callable($deserializer)) { + $value = call_user_func($deserializer, $reader); + } else { + $type = gettype($deserializer); + if ('string' === $type) { + $type .= ' ('.$deserializer.')'; + } elseif ('object' === $type) { + $type .= ' ('.get_class($deserializer).')'; + } + throw new \LogicException('Could not use this type as a deserializer: '.$type); + } + } else { + $value = Complex::xmlDeserialize($reader); + } + + return [ + 'name' => $name, + 'value' => $value, + ]; + } +} diff --git a/3rdparty/sabre/dav/lib/DAV/Xml/Element/Response.php b/3rdparty/sabre/dav/lib/DAV/Xml/Element/Response.php new file mode 100644 index 00000000..df929146 --- /dev/null +++ b/3rdparty/sabre/dav/lib/DAV/Xml/Element/Response.php @@ -0,0 +1,267 @@ +href = $href; + $this->responseProperties = $responseProperties; + $this->httpStatus = $httpStatus; + } + + /** + * Returns the url. + * + * @return string + */ + public function getHref() + { + return $this->href; + } + + /** + * Returns the httpStatus value. + * + * @return string + */ + public function getHttpStatus() + { + return $this->httpStatus; + } + + /** + * Returns the property list. + * + * @return array + */ + public function getResponseProperties() + { + return $this->responseProperties; + } + + /** + * The serialize method is called during xml writing. + * + * It should use the $writer argument to encode this object into XML. + * + * Important note: it is not needed to create the parent element. The + * parent element is already created, and we only have to worry about + * attributes, child elements and text (if any). + * + * Important note 2: If you are writing any new elements, you are also + * responsible for closing them. + */ + public function xmlSerialize(Writer $writer) + { + /* + * Accordingly to the RFC the element looks like: + * + * + * So the response + * - MUST contain a href and + * - EITHER a status and additional href(s) + * OR one or more propstat(s) + */ + $writer->writeElement('{DAV:}href', $writer->contextUri.\Sabre\HTTP\encodePath($this->getHref())); + + $empty = true; + $httpStatus = $this->getHTTPStatus(); + + // Add propstat elements + foreach ($this->getResponseProperties() as $status => $properties) { + // Skipping empty lists + if (!$properties || (!is_int($status) && !ctype_digit($status))) { + continue; + } + $empty = false; + $writer->startElement('{DAV:}propstat'); + $writer->writeElement('{DAV:}prop', $properties); + $writer->writeElement('{DAV:}status', 'HTTP/1.1 '.$status.' '.\Sabre\HTTP\Response::$statusCodes[$status]); + $writer->endElement(); // {DAV:}propstat + } + + // The WebDAV spec only allows the status element on responses _without_ a propstat + if ($empty) { + if (null !== $httpStatus) { + $writer->writeElement('{DAV:}status', 'HTTP/1.1 '.$httpStatus.' '.\Sabre\HTTP\Response::$statusCodes[$httpStatus]); + } else { + /* + * The WebDAV spec _requires_ at least one DAV:propstat to appear for + * every DAV:response if there is no status. + * In some circumstances however, there are no properties to encode. + * + * In those cases we MUST specify at least one DAV:propstat anyway, with + * no properties. + */ + $writer->writeElement('{DAV:}propstat', [ + '{DAV:}prop' => [], + '{DAV:}status' => 'HTTP/1.1 418 '.\Sabre\HTTP\Response::$statusCodes[418], + ]); + } + } + } + + /** + * The deserialize method is called during xml parsing. + * + * This method is called statically, this is because in theory this method + * may be used as a type of constructor, or factory method. + * + * Often you want to return an instance of the current class, but you are + * free to return other data as well. + * + * You are responsible for advancing the reader to the next element. Not + * doing anything will result in a never-ending loop. + * + * If you just want to skip parsing for this element altogether, you can + * just call $reader->next(); + * + * $reader->parseInnerTree() will parse the entire sub-tree, and advance to + * the next element. + * + * @return mixed + */ + public static function xmlDeserialize(Reader $reader) + { + $reader->pushContext(); + + $reader->elementMap['{DAV:}propstat'] = 'Sabre\\Xml\\Element\\KeyValue'; + + // We are overriding the parser for {DAV:}prop. This deserializer is + // almost identical to the one for Sabre\Xml\Element\KeyValue. + // + // The difference is that if there are any child-elements inside of + // {DAV:}prop, that have no value, normally any deserializers are + // called. But we don't want this, because a singular element without + // child-elements implies 'no value' in {DAV:}prop, so we want to skip + // deserializers and just set null for those. + $reader->elementMap['{DAV:}prop'] = function (Reader $reader) { + if ($reader->isEmptyElement) { + $reader->next(); + + return []; + } + + if (!$reader->read()) { + $reader->next(); + + return []; + } + + if (Reader::END_ELEMENT === $reader->nodeType) { + $reader->next(); + + return []; + } + + $values = []; + + do { + if (Reader::ELEMENT === $reader->nodeType) { + $clark = $reader->getClark(); + + if ($reader->isEmptyElement) { + $values[$clark] = null; + $reader->next(); + } else { + $values[$clark] = $reader->parseCurrentElement()['value']; + } + } else { + if (!$reader->read()) { + break; + } + } + } while (Reader::END_ELEMENT !== $reader->nodeType); + + $reader->read(); + + return $values; + }; + $elems = $reader->parseInnerTree(); + $reader->popContext(); + + $href = null; + $propertyLists = []; + $statusCode = null; + + foreach ($elems as $elem) { + switch ($elem['name']) { + case '{DAV:}href': + $href = $elem['value']; + break; + case '{DAV:}propstat': + $status = $elem['value']['{DAV:}status']; + list(, $status) = explode(' ', $status, 3); + $properties = isset($elem['value']['{DAV:}prop']) ? $elem['value']['{DAV:}prop'] : []; + if ($properties) { + $propertyLists[$status] = $properties; + } + break; + case '{DAV:}status': + list(, $statusCode) = explode(' ', $elem['value'], 3); + break; + } + } + + return new self($href, $propertyLists, $statusCode); + } +} diff --git a/3rdparty/sabre/dav/lib/DAV/Xml/Element/Sharee.php b/3rdparty/sabre/dav/lib/DAV/Xml/Element/Sharee.php new file mode 100644 index 00000000..33564d8f --- /dev/null +++ b/3rdparty/sabre/dav/lib/DAV/Xml/Element/Sharee.php @@ -0,0 +1,189 @@ + $v) { + if (property_exists($this, $k)) { + $this->$k = $v; + } else { + throw new \InvalidArgumentException('Unknown property: '.$k); + } + } + } + + /** + * The xmlSerialize method is called during xml writing. + * + * Use the $writer argument to write its own xml serialization. + * + * An important note: do _not_ create a parent element. Any element + * implementing XmlSerializable should only ever write what's considered + * its 'inner xml'. + * + * The parent of the current element is responsible for writing a + * containing element. + * + * This allows serializers to be re-used for different element names. + * + * If you are opening new elements, you must also close them again. + */ + public function xmlSerialize(Writer $writer) + { + $writer->write([ + new Href($this->href), + '{DAV:}prop' => $this->properties, + '{DAV:}share-access' => new ShareAccess($this->access), + ]); + switch ($this->inviteStatus) { + case Plugin::INVITE_NORESPONSE: + $writer->writeElement('{DAV:}invite-noresponse'); + break; + case Plugin::INVITE_ACCEPTED: + $writer->writeElement('{DAV:}invite-accepted'); + break; + case Plugin::INVITE_DECLINED: + $writer->writeElement('{DAV:}invite-declined'); + break; + case Plugin::INVITE_INVALID: + $writer->writeElement('{DAV:}invite-invalid'); + break; + } + } + + /** + * The deserialize method is called during xml parsing. + * + * This method is called statically, this is because in theory this method + * may be used as a type of constructor, or factory method. + * + * Often you want to return an instance of the current class, but you are + * free to return other data as well. + * + * You are responsible for advancing the reader to the next element. Not + * doing anything will result in a never-ending loop. + * + * If you just want to skip parsing for this element altogether, you can + * just call $reader->next(); + * + * $reader->parseInnerTree() will parse the entire sub-tree, and advance to + * the next element. + * + * @return mixed + */ + public static function xmlDeserialize(Reader $reader) + { + // Temporarily override configuration + $reader->pushContext(); + $reader->elementMap['{DAV:}share-access'] = 'Sabre\DAV\Xml\Property\ShareAccess'; + $reader->elementMap['{DAV:}prop'] = 'Sabre\Xml\Deserializer\keyValue'; + + $elems = Deserializer\keyValue($reader, 'DAV:'); + + // Restore previous configuration + $reader->popContext(); + + $sharee = new self(); + if (!isset($elems['href'])) { + throw new BadRequest('Every {DAV:}sharee must have a {DAV:}href child-element'); + } + $sharee->href = $elems['href']; + + if (isset($elems['prop'])) { + $sharee->properties = $elems['prop']; + } + if (isset($elems['comment'])) { + $sharee->comment = $elems['comment']; + } + if (!isset($elems['share-access'])) { + throw new BadRequest('Every {DAV:}sharee must have a {DAV:}share-access child element'); + } + $sharee->access = $elems['share-access']->getValue(); + + return $sharee; + } +} diff --git a/3rdparty/sabre/dav/lib/DAV/Xml/Property/Complex.php b/3rdparty/sabre/dav/lib/DAV/Xml/Property/Complex.php new file mode 100644 index 00000000..787d30d9 --- /dev/null +++ b/3rdparty/sabre/dav/lib/DAV/Xml/Property/Complex.php @@ -0,0 +1,87 @@ +next(); + * + * $reader->parseInnerTree() will parse the entire sub-tree, and advance to + * the next element. + * + * @return mixed + */ + public static function xmlDeserialize(Reader $reader) + { + $xml = $reader->readInnerXml(); + + if (Reader::ELEMENT === $reader->nodeType && $reader->isEmptyElement) { + // Easy! + $reader->next(); + + return null; + } + // Now we have a copy of the inner xml, we need to traverse it to get + // all the strings. If there's no non-string data, we just return the + // string, otherwise we return an instance of this class. + $reader->read(); + + $nonText = false; + $text = ''; + + while (true) { + switch ($reader->nodeType) { + case Reader::ELEMENT: + $nonText = true; + $reader->next(); + continue 2; + case Reader::TEXT: + case Reader::CDATA: + $text .= $reader->value; + break; + case Reader::END_ELEMENT: + break 2; + } + $reader->read(); + } + + // Make sure we advance the cursor one step further. + $reader->read(); + + if ($nonText) { + $new = new self($xml); + + return $new; + } else { + return $text; + } + } +} diff --git a/3rdparty/sabre/dav/lib/DAV/Xml/Property/GetLastModified.php b/3rdparty/sabre/dav/lib/DAV/Xml/Property/GetLastModified.php new file mode 100644 index 00000000..efc15c29 --- /dev/null +++ b/3rdparty/sabre/dav/lib/DAV/Xml/Property/GetLastModified.php @@ -0,0 +1,103 @@ +time = clone $time; + } else { + $this->time = new DateTime('@'.$time); + } + + // Setting timezone to UTC + $this->time->setTimezone(new DateTimeZone('UTC')); + } + + /** + * getTime. + * + * @return DateTime + */ + public function getTime() + { + return $this->time; + } + + /** + * The serialize method is called during xml writing. + * + * It should use the $writer argument to encode this object into XML. + * + * Important note: it is not needed to create the parent element. The + * parent element is already created, and we only have to worry about + * attributes, child elements and text (if any). + * + * Important note 2: If you are writing any new elements, you are also + * responsible for closing them. + */ + public function xmlSerialize(Writer $writer) + { + $writer->write( + HTTP\toDate($this->time) + ); + } + + /** + * The deserialize method is called during xml parsing. + * + * This method is called statically, this is because in theory this method + * may be used as a type of constructor, or factory method. + * + * Often you want to return an instance of the current class, but you are + * free to return other data as well. + * + * Important note 2: You are responsible for advancing the reader to the + * next element. Not doing anything will result in a never-ending loop. + * + * If you just want to skip parsing for this element altogether, you can + * just call $reader->next(); + * + * $reader->parseInnerTree() will parse the entire sub-tree, and advance to + * the next element. + * + * @return mixed + */ + public static function xmlDeserialize(Reader $reader) + { + return new self(new DateTime($reader->parseInnerTree())); + } +} diff --git a/3rdparty/sabre/dav/lib/DAV/Xml/Property/Href.php b/3rdparty/sabre/dav/lib/DAV/Xml/Property/Href.php new file mode 100644 index 00000000..d4e43da7 --- /dev/null +++ b/3rdparty/sabre/dav/lib/DAV/Xml/Property/Href.php @@ -0,0 +1,166 @@ +hrefs = $hrefs; + } + + /** + * Returns the first Href. + * + * @return string|null + */ + public function getHref() + { + return $this->hrefs[0] ?? null; + } + + /** + * Returns the hrefs as an array. + * + * @return array + */ + public function getHrefs() + { + return $this->hrefs; + } + + /** + * The xmlSerialize method is called during xml writing. + * + * Use the $writer argument to write its own xml serialization. + * + * An important note: do _not_ create a parent element. Any element + * implementing XmlSerializable should only ever write what's considered + * its 'inner xml'. + * + * The parent of the current element is responsible for writing a + * containing element. + * + * This allows serializers to be re-used for different element names. + * + * If you are opening new elements, you must also close them again. + */ + public function xmlSerialize(Writer $writer) + { + foreach ($this->getHrefs() as $href) { + $href = Uri\resolve($writer->contextUri, $href); + $writer->writeElement('{DAV:}href', $href); + } + } + + /** + * Generate html representation for this value. + * + * The html output is 100% trusted, and no effort is being made to sanitize + * it. It's up to the implementor to sanitize user provided values. + * + * The output must be in UTF-8. + * + * The baseUri parameter is a url to the root of the application, and can + * be used to construct local links. + * + * @return string + */ + public function toHtml(HtmlOutputHelper $html) + { + $links = []; + foreach ($this->getHrefs() as $href) { + $links[] = $html->link($href); + } + + return implode('
    ', $links); + } + + /** + * The deserialize method is called during xml parsing. + * + * This method is called statically, this is because in theory this method + * may be used as a type of constructor, or factory method. + * + * Often you want to return an instance of the current class, but you are + * free to return other data as well. + * + * You are responsible for advancing the reader to the next element. Not + * doing anything will result in a never-ending loop. + * + * If you just want to skip parsing for this element altogether, you can + * just call $reader->next(); + * + * $reader->parseInnerTree() will parse the entire sub-tree, and advance to + * the next element. + * + * @return mixed + */ + public static function xmlDeserialize(Reader $reader) + { + $hrefs = []; + foreach ((array) $reader->parseInnerTree() as $elem) { + if ('{DAV:}href' !== $elem['name']) { + continue; + } + + $hrefs[] = $elem['value']; + } + if ($hrefs) { + return new self($hrefs); + } + } +} diff --git a/3rdparty/sabre/dav/lib/DAV/Xml/Property/Invite.php b/3rdparty/sabre/dav/lib/DAV/Xml/Property/Invite.php new file mode 100644 index 00000000..e3f0a611 --- /dev/null +++ b/3rdparty/sabre/dav/lib/DAV/Xml/Property/Invite.php @@ -0,0 +1,66 @@ +sharees = $sharees; + } + + /** + * The xmlSerialize method is called during xml writing. + * + * Use the $writer argument to write its own xml serialization. + * + * An important note: do _not_ create a parent element. Any element + * implementing XmlSerializable should only ever write what's considered + * its 'inner xml'. + * + * The parent of the current element is responsible for writing a + * containing element. + * + * This allows serializers to be re-used for different element names. + * + * If you are opening new elements, you must also close them again. + */ + public function xmlSerialize(Writer $writer) + { + foreach ($this->sharees as $sharee) { + $writer->writeElement('{DAV:}sharee', $sharee); + } + } +} diff --git a/3rdparty/sabre/dav/lib/DAV/Xml/Property/LocalHref.php b/3rdparty/sabre/dav/lib/DAV/Xml/Property/LocalHref.php new file mode 100644 index 00000000..cb794974 --- /dev/null +++ b/3rdparty/sabre/dav/lib/DAV/Xml/Property/LocalHref.php @@ -0,0 +1,48 @@ +locks = $locks; + } + + /** + * The serialize method is called during xml writing. + * + * It should use the $writer argument to encode this object into XML. + * + * Important note: it is not needed to create the parent element. The + * parent element is already created, and we only have to worry about + * attributes, child elements and text (if any). + * + * Important note 2: If you are writing any new elements, you are also + * responsible for closing them. + */ + public function xmlSerialize(Writer $writer) + { + foreach ($this->locks as $lock) { + $writer->startElement('{DAV:}activelock'); + + $writer->startElement('{DAV:}lockscope'); + if (LockInfo::SHARED === $lock->scope) { + $writer->writeElement('{DAV:}shared'); + } else { + $writer->writeElement('{DAV:}exclusive'); + } + + $writer->endElement(); // {DAV:}lockscope + + $writer->startElement('{DAV:}locktype'); + $writer->writeElement('{DAV:}write'); + $writer->endElement(); // {DAV:}locktype + + if (!self::$hideLockRoot) { + $writer->startElement('{DAV:}lockroot'); + $writer->writeElement('{DAV:}href', $writer->contextUri.$lock->uri); + $writer->endElement(); // {DAV:}lockroot + } + $writer->writeElement('{DAV:}depth', (DAV\Server::DEPTH_INFINITY == $lock->depth ? 'infinity' : $lock->depth)); + $writer->writeElement('{DAV:}timeout', (LockInfo::TIMEOUT_INFINITE === $lock->timeout ? 'Infinite' : 'Second-'.$lock->timeout)); + + // optional according to https://tools.ietf.org/html/rfc4918#section-6.5 + if (null !== $lock->token && '' !== $lock->token) { + $writer->startElement('{DAV:}locktoken'); + $writer->writeElement('{DAV:}href', 'opaquelocktoken:'.$lock->token); + $writer->endElement(); // {DAV:}locktoken + } + + if ($lock->owner) { + $writer->writeElement('{DAV:}owner', new XmlFragment($lock->owner)); + } + $writer->endElement(); // {DAV:}activelock + } + } +} diff --git a/3rdparty/sabre/dav/lib/DAV/Xml/Property/ResourceType.php b/3rdparty/sabre/dav/lib/DAV/Xml/Property/ResourceType.php new file mode 100644 index 00000000..75ddcba3 --- /dev/null +++ b/3rdparty/sabre/dav/lib/DAV/Xml/Property/ResourceType.php @@ -0,0 +1,120 @@ +value; + } + + /** + * Checks if the principal contains a certain value. + * + * @param string $type + * + * @return bool + */ + public function is($type) + { + return in_array($type, $this->value); + } + + /** + * Adds a resourcetype value to this property. + * + * @param string $type + */ + public function add($type) + { + $this->value[] = $type; + $this->value = array_unique($this->value); + } + + /** + * The deserialize method is called during xml parsing. + * + * This method is called statically, this is because in theory this method + * may be used as a type of constructor, or factory method. + * + * Often you want to return an instance of the current class, but you are + * free to return other data as well. + * + * Important note 2: You are responsible for advancing the reader to the + * next element. Not doing anything will result in a never-ending loop. + * + * If you just want to skip parsing for this element altogether, you can + * just call $reader->next(); + * + * $reader->parseInnerTree() will parse the entire sub-tree, and advance to + * the next element. + * + * @return mixed + */ + public static function xmlDeserialize(Reader $reader) + { + return new self(parent::xmlDeserialize($reader)); + } + + /** + * Generate html representation for this value. + * + * The html output is 100% trusted, and no effort is being made to sanitize + * it. It's up to the implementor to sanitize user provided values. + * + * The output must be in UTF-8. + * + * The baseUri parameter is a url to the root of the application, and can + * be used to construct local links. + * + * @return string + */ + public function toHtml(HtmlOutputHelper $html) + { + return implode( + ', ', + array_map([$html, 'xmlName'], $this->getValue()) + ); + } +} diff --git a/3rdparty/sabre/dav/lib/DAV/Xml/Property/ShareAccess.php b/3rdparty/sabre/dav/lib/DAV/Xml/Property/ShareAccess.php new file mode 100644 index 00000000..fdd55558 --- /dev/null +++ b/3rdparty/sabre/dav/lib/DAV/Xml/Property/ShareAccess.php @@ -0,0 +1,135 @@ +value = $shareAccess; + } + + /** + * Returns the current value. + * + * @return int + */ + public function getValue() + { + return $this->value; + } + + /** + * The xmlSerialize method is called during xml writing. + * + * Use the $writer argument to write its own xml serialization. + * + * An important note: do _not_ create a parent element. Any element + * implementing XmlSerializable should only ever write what's considered + * its 'inner xml'. + * + * The parent of the current element is responsible for writing a + * containing element. + * + * This allows serializers to be re-used for different element names. + * + * If you are opening new elements, you must also close them again. + */ + public function xmlSerialize(Writer $writer) + { + switch ($this->value) { + case SharingPlugin::ACCESS_NOTSHARED: + $writer->writeElement('{DAV:}not-shared'); + break; + case SharingPlugin::ACCESS_SHAREDOWNER: + $writer->writeElement('{DAV:}shared-owner'); + break; + case SharingPlugin::ACCESS_READ: + $writer->writeElement('{DAV:}read'); + break; + case SharingPlugin::ACCESS_READWRITE: + $writer->writeElement('{DAV:}read-write'); + break; + case SharingPlugin::ACCESS_NOACCESS: + $writer->writeElement('{DAV:}no-access'); + break; + } + } + + /** + * The deserialize method is called during xml parsing. + * + * This method is called statically, this is because in theory this method + * may be used as a type of constructor, or factory method. + * + * Often you want to return an instance of the current class, but you are + * free to return other data as well. + * + * You are responsible for advancing the reader to the next element. Not + * doing anything will result in a never-ending loop. + * + * If you just want to skip parsing for this element altogether, you can + * just call $reader->next(); + * + * $reader->parseInnerTree() will parse the entire sub-tree, and advance to + * the next element. + * + * @return mixed + */ + public static function xmlDeserialize(Reader $reader) + { + $elems = $reader->parseInnerTree(); + foreach ($elems as $elem) { + switch ($elem['name']) { + case '{DAV:}not-shared': + return new self(SharingPlugin::ACCESS_NOTSHARED); + case '{DAV:}shared-owner': + return new self(SharingPlugin::ACCESS_SHAREDOWNER); + case '{DAV:}read': + return new self(SharingPlugin::ACCESS_READ); + case '{DAV:}read-write': + return new self(SharingPlugin::ACCESS_READWRITE); + case '{DAV:}no-access': + return new self(SharingPlugin::ACCESS_NOACCESS); + } + } + throw new BadRequest('Invalid value for {DAV:}share-access element'); + } +} diff --git a/3rdparty/sabre/dav/lib/DAV/Xml/Property/SupportedLock.php b/3rdparty/sabre/dav/lib/DAV/Xml/Property/SupportedLock.php new file mode 100644 index 00000000..100829c6 --- /dev/null +++ b/3rdparty/sabre/dav/lib/DAV/Xml/Property/SupportedLock.php @@ -0,0 +1,52 @@ +writeElement('{DAV:}lockentry', [ + '{DAV:}lockscope' => ['{DAV:}exclusive' => null], + '{DAV:}locktype' => ['{DAV:}write' => null], + ]); + $writer->writeElement('{DAV:}lockentry', [ + '{DAV:}lockscope' => ['{DAV:}shared' => null], + '{DAV:}locktype' => ['{DAV:}write' => null], + ]); + } +} diff --git a/3rdparty/sabre/dav/lib/DAV/Xml/Property/SupportedMethodSet.php b/3rdparty/sabre/dav/lib/DAV/Xml/Property/SupportedMethodSet.php new file mode 100644 index 00000000..63440109 --- /dev/null +++ b/3rdparty/sabre/dav/lib/DAV/Xml/Property/SupportedMethodSet.php @@ -0,0 +1,114 @@ +methods = $methods; + } + + /** + * Returns the list of supported http methods. + * + * @return string[] + */ + public function getValue() + { + return $this->methods; + } + + /** + * Returns true or false if the property contains a specific method. + * + * @param string $methodName + * + * @return bool + */ + public function has($methodName) + { + return in_array( + $methodName, + $this->methods + ); + } + + /** + * The xmlSerialize method is called during xml writing. + * + * Use the $writer argument to write its own xml serialization. + * + * An important note: do _not_ create a parent element. Any element + * implementing XmlSerializable should only ever write what's considered + * its 'inner xml'. + * + * The parent of the current element is responsible for writing a + * containing element. + * + * This allows serializers to be re-used for different element names. + * + * If you are opening new elements, you must also close them again. + */ + public function xmlSerialize(Writer $writer) + { + foreach ($this->getValue() as $val) { + $writer->startElement('{DAV:}supported-method'); + $writer->writeAttribute('name', $val); + $writer->endElement(); + } + } + + /** + * Generate html representation for this value. + * + * The html output is 100% trusted, and no effort is being made to sanitize + * it. It's up to the implementor to sanitize user provided values. + * + * The output must be in UTF-8. + * + * The baseUri parameter is a url to the root of the application, and can + * be used to construct local links. + * + * @return string + */ + public function toHtml(HtmlOutputHelper $html) + { + return implode( + ', ', + array_map([$html, 'h'], $this->getValue()) + ); + } +} diff --git a/3rdparty/sabre/dav/lib/DAV/Xml/Property/SupportedReportSet.php b/3rdparty/sabre/dav/lib/DAV/Xml/Property/SupportedReportSet.php new file mode 100644 index 00000000..0b4990e9 --- /dev/null +++ b/3rdparty/sabre/dav/lib/DAV/Xml/Property/SupportedReportSet.php @@ -0,0 +1,144 @@ +addReport($reports); + } + } + + /** + * Adds a report to this property. + * + * The report must be a string in clark-notation. + * Multiple reports can be specified as an array. + * + * @param mixed $report + */ + public function addReport($report) + { + $report = (array) $report; + + foreach ($report as $r) { + if (!preg_match('/^{([^}]*)}(.*)$/', $r)) { + throw new DAV\Exception('Reportname must be in clark-notation'); + } + $this->reports[] = $r; + } + } + + /** + * Returns the list of supported reports. + * + * @return string[] + */ + public function getValue() + { + return $this->reports; + } + + /** + * Returns true or false if the property contains a specific report. + * + * @param string $reportName + * + * @return bool + */ + public function has($reportName) + { + return in_array( + $reportName, + $this->reports + ); + } + + /** + * The xmlSerialize method is called during xml writing. + * + * Use the $writer argument to write its own xml serialization. + * + * An important note: do _not_ create a parent element. Any element + * implementing XmlSerializable should only ever write what's considered + * its 'inner xml'. + * + * The parent of the current element is responsible for writing a + * containing element. + * + * This allows serializers to be re-used for different element names. + * + * If you are opening new elements, you must also close them again. + */ + public function xmlSerialize(Writer $writer) + { + foreach ($this->getValue() as $val) { + $writer->startElement('{DAV:}supported-report'); + $writer->startElement('{DAV:}report'); + $writer->writeElement($val); + $writer->endElement(); + $writer->endElement(); + } + } + + /** + * Generate html representation for this value. + * + * The html output is 100% trusted, and no effort is being made to sanitize + * it. It's up to the implementor to sanitize user provided values. + * + * The output must be in UTF-8. + * + * The baseUri parameter is a url to the root of the application, and can + * be used to construct local links. + * + * @return string + */ + public function toHtml(HtmlOutputHelper $html) + { + return implode( + ', ', + array_map([$html, 'xmlName'], $this->getValue()) + ); + } +} diff --git a/3rdparty/sabre/dav/lib/DAV/Xml/Request/Lock.php b/3rdparty/sabre/dav/lib/DAV/Xml/Request/Lock.php new file mode 100644 index 00000000..57d12ef9 --- /dev/null +++ b/3rdparty/sabre/dav/lib/DAV/Xml/Request/Lock.php @@ -0,0 +1,84 @@ +next(); + * + * $reader->parseInnerTree() will parse the entire sub-tree, and advance to + * the next element. + * + * @return mixed + */ + public static function xmlDeserialize(Reader $reader) + { + $reader->pushContext(); + $reader->elementMap['{DAV:}owner'] = 'Sabre\\Xml\\Element\\XmlFragment'; + + $values = KeyValue::xmlDeserialize($reader); + + $reader->popContext(); + + $new = new self(); + $new->owner = !empty($values['{DAV:}owner']) ? $values['{DAV:}owner']->getXml() : null; + $new->scope = LockInfo::SHARED; + + if (isset($values['{DAV:}lockscope'])) { + foreach ($values['{DAV:}lockscope'] as $elem) { + if ('{DAV:}exclusive' === $elem['name']) { + $new->scope = LockInfo::EXCLUSIVE; + } + } + } + + return $new; + } +} diff --git a/3rdparty/sabre/dav/lib/DAV/Xml/Request/MkCol.php b/3rdparty/sabre/dav/lib/DAV/Xml/Request/MkCol.php new file mode 100644 index 00000000..e0d7e90a --- /dev/null +++ b/3rdparty/sabre/dav/lib/DAV/Xml/Request/MkCol.php @@ -0,0 +1,80 @@ +value array with properties that are supposed to get set + * during creation of the new collection. + * + * @return array + */ + public function getProperties() + { + return $this->properties; + } + + /** + * The deserialize method is called during xml parsing. + * + * This method is called statically, this is because in theory this method + * may be used as a type of constructor, or factory method. + * + * Often you want to return an instance of the current class, but you are + * free to return other data as well. + * + * You are responsible for advancing the reader to the next element. Not + * doing anything will result in a never-ending loop. + * + * If you just want to skip parsing for this element altogether, you can + * just call $reader->next(); + * + * $reader->parseInnerTree() will parse the entire sub-tree, and advance to + * the next element. + * + * @return mixed + */ + public static function xmlDeserialize(Reader $reader) + { + $self = new self(); + + $elementMap = $reader->elementMap; + $elementMap['{DAV:}prop'] = 'Sabre\DAV\Xml\Element\Prop'; + $elementMap['{DAV:}set'] = 'Sabre\Xml\Element\KeyValue'; + $elementMap['{DAV:}remove'] = 'Sabre\Xml\Element\KeyValue'; + + $elems = $reader->parseInnerTree($elementMap); + + foreach ($elems as $elem) { + if ('{DAV:}set' === $elem['name']) { + $self->properties = array_merge($self->properties, $elem['value']['{DAV:}prop']); + } + } + + return $self; + } +} diff --git a/3rdparty/sabre/dav/lib/DAV/Xml/Request/PropFind.php b/3rdparty/sabre/dav/lib/DAV/Xml/Request/PropFind.php new file mode 100644 index 00000000..505e7c79 --- /dev/null +++ b/3rdparty/sabre/dav/lib/DAV/Xml/Request/PropFind.php @@ -0,0 +1,79 @@ +next(); + * + * $reader->parseInnerTree() will parse the entire sub-tree, and advance to + * the next element. + * + * @return mixed + */ + public static function xmlDeserialize(Reader $reader) + { + $self = new self(); + + $reader->pushContext(); + $reader->elementMap['{DAV:}prop'] = 'Sabre\Xml\Element\Elements'; + + foreach (KeyValue::xmlDeserialize($reader) as $k => $v) { + switch ($k) { + case '{DAV:}prop': + $self->properties = $v; + break; + case '{DAV:}allprop': + $self->allProp = true; + } + } + + $reader->popContext(); + + return $self; + } +} diff --git a/3rdparty/sabre/dav/lib/DAV/Xml/Request/PropPatch.php b/3rdparty/sabre/dav/lib/DAV/Xml/Request/PropPatch.php new file mode 100644 index 00000000..4a270950 --- /dev/null +++ b/3rdparty/sabre/dav/lib/DAV/Xml/Request/PropPatch.php @@ -0,0 +1,109 @@ +properties as $propertyName => $propertyValue) { + if (is_null($propertyValue)) { + $writer->startElement('{DAV:}remove'); + $writer->write(['{DAV:}prop' => [$propertyName => $propertyValue]]); + $writer->endElement(); + } else { + $writer->startElement('{DAV:}set'); + $writer->write(['{DAV:}prop' => [$propertyName => $propertyValue]]); + $writer->endElement(); + } + } + } + + /** + * The deserialize method is called during xml parsing. + * + * This method is called statically, this is because in theory this method + * may be used as a type of constructor, or factory method. + * + * Often you want to return an instance of the current class, but you are + * free to return other data as well. + * + * You are responsible for advancing the reader to the next element. Not + * doing anything will result in a never-ending loop. + * + * If you just want to skip parsing for this element altogether, you can + * just call $reader->next(); + * + * $reader->parseInnerTree() will parse the entire sub-tree, and advance to + * the next element. + * + * @return mixed + */ + public static function xmlDeserialize(Reader $reader) + { + $self = new self(); + + $elementMap = $reader->elementMap; + $elementMap['{DAV:}prop'] = 'Sabre\DAV\Xml\Element\Prop'; + $elementMap['{DAV:}set'] = 'Sabre\Xml\Element\KeyValue'; + $elementMap['{DAV:}remove'] = 'Sabre\Xml\Element\KeyValue'; + + $elems = $reader->parseInnerTree($elementMap); + + foreach ($elems as $elem) { + if ('{DAV:}set' === $elem['name']) { + $self->properties = array_merge($self->properties, $elem['value']['{DAV:}prop']); + } + if ('{DAV:}remove' === $elem['name']) { + // Ensuring there are no values. + foreach ($elem['value']['{DAV:}prop'] as $remove => $value) { + $self->properties[$remove] = null; + } + } + } + + return $self; + } +} diff --git a/3rdparty/sabre/dav/lib/DAV/Xml/Request/ShareResource.php b/3rdparty/sabre/dav/lib/DAV/Xml/Request/ShareResource.php new file mode 100644 index 00000000..79d7dc82 --- /dev/null +++ b/3rdparty/sabre/dav/lib/DAV/Xml/Request/ShareResource.php @@ -0,0 +1,80 @@ +sharees = $sharees; + } + + /** + * The deserialize method is called during xml parsing. + * + * This method is called statically, this is because in theory this method + * may be used as a type of constructor, or factory method. + * + * Often you want to return an instance of the current class, but you are + * free to return other data as well. + * + * You are responsible for advancing the reader to the next element. Not + * doing anything will result in a never-ending loop. + * + * If you just want to skip parsing for this element altogether, you can + * just call $reader->next(); + * + * $reader->parseInnerTree() will parse the entire sub-tree, and advance to + * the next element. + * + * @return mixed + */ + public static function xmlDeserialize(Reader $reader) + { + $elems = $reader->parseInnerTree([ + '{DAV:}sharee' => 'Sabre\DAV\Xml\Element\Sharee', + '{DAV:}share-access' => 'Sabre\DAV\Xml\Property\ShareAccess', + '{DAV:}prop' => 'Sabre\Xml\Deserializer\keyValue', + ]); + + $sharees = []; + + foreach ($elems as $elem) { + if ('{DAV:}sharee' !== $elem['name']) { + continue; + } + $sharees[] = $elem['value']; + } + + return new self($sharees); + } +} diff --git a/3rdparty/sabre/dav/lib/DAV/Xml/Request/SyncCollectionReport.php b/3rdparty/sabre/dav/lib/DAV/Xml/Request/SyncCollectionReport.php new file mode 100644 index 00000000..8dd95765 --- /dev/null +++ b/3rdparty/sabre/dav/lib/DAV/Xml/Request/SyncCollectionReport.php @@ -0,0 +1,118 @@ +next(); + * + * $reader->parseInnerTree() will parse the entire sub-tree, and advance to + * the next element. + * + * @return mixed + */ + public static function xmlDeserialize(Reader $reader) + { + $self = new self(); + + $reader->pushContext(); + + $reader->elementMap['{DAV:}prop'] = 'Sabre\Xml\Element\Elements'; + $elems = KeyValue::xmlDeserialize($reader); + + $reader->popContext(); + + $required = [ + '{DAV:}sync-token', + '{DAV:}prop', + ]; + + foreach ($required as $elem) { + if (!array_key_exists($elem, $elems)) { + throw new BadRequest('The '.$elem.' element in the {DAV:}sync-collection report is required'); + } + } + + $self->properties = $elems['{DAV:}prop']; + $self->syncToken = $elems['{DAV:}sync-token']; + + if (isset($elems['{DAV:}limit'])) { + $nresults = null; + foreach ($elems['{DAV:}limit'] as $child) { + if ('{DAV:}nresults' === $child['name']) { + $nresults = (int) $child['value']; + } + } + $self->limit = $nresults; + } + + if (isset($elems['{DAV:}sync-level'])) { + $value = $elems['{DAV:}sync-level']; + if ('infinity' === $value) { + $value = \Sabre\DAV\Server::DEPTH_INFINITY; + } + $self->syncLevel = $value; + } + + return $self; + } +} diff --git a/3rdparty/sabre/dav/lib/DAV/Xml/Response/MultiStatus.php b/3rdparty/sabre/dav/lib/DAV/Xml/Response/MultiStatus.php new file mode 100644 index 00000000..e824cda4 --- /dev/null +++ b/3rdparty/sabre/dav/lib/DAV/Xml/Response/MultiStatus.php @@ -0,0 +1,136 @@ +responses = $responses; + $this->syncToken = $syncToken; + } + + /** + * Returns the response list. + * + * @return \Sabre\DAV\Xml\Element\Response[] + */ + public function getResponses() + { + return $this->responses; + } + + /** + * Returns the sync-token, if available. + * + * @return string|null + */ + public function getSyncToken() + { + return $this->syncToken; + } + + /** + * The serialize method is called during xml writing. + * + * It should use the $writer argument to encode this object into XML. + * + * Important note: it is not needed to create the parent element. The + * parent element is already created, and we only have to worry about + * attributes, child elements and text (if any). + * + * Important note 2: If you are writing any new elements, you are also + * responsible for closing them. + */ + public function xmlSerialize(Writer $writer) + { + foreach ($this->getResponses() as $response) { + $writer->writeElement('{DAV:}response', $response); + } + if ($syncToken = $this->getSyncToken()) { + $writer->writeElement('{DAV:}sync-token', $syncToken); + } + } + + /** + * The deserialize method is called during xml parsing. + * + * This method is called statically, this is because in theory this method + * may be used as a type of constructor, or factory method. + * + * Often you want to return an instance of the current class, but you are + * free to return other data as well. + * + * You are responsible for advancing the reader to the next element. Not + * doing anything will result in a never-ending loop. + * + * If you just want to skip parsing for this element altogether, you can + * just call $reader->next(); + * + * $reader->parseInnerTree() will parse the entire sub-tree, and advance to + * the next element. + * + * @return mixed + */ + public static function xmlDeserialize(Reader $reader) + { + $elementMap = $reader->elementMap; + $elementMap['{DAV:}prop'] = 'Sabre\\DAV\\Xml\\Element\\Prop'; + $elements = $reader->parseInnerTree($elementMap); + + $responses = []; + $syncToken = null; + + if ($elements) { + foreach ($elements as $elem) { + if ('{DAV:}response' === $elem['name']) { + $responses[] = $elem['value']; + } + if ('{DAV:}sync-token' === $elem['name']) { + $syncToken = $elem['value']; + } + } + } + + return new self($responses, $syncToken); + } +} diff --git a/3rdparty/sabre/dav/lib/DAV/Xml/Service.php b/3rdparty/sabre/dav/lib/DAV/Xml/Service.php new file mode 100644 index 00000000..4406b022 --- /dev/null +++ b/3rdparty/sabre/dav/lib/DAV/Xml/Service.php @@ -0,0 +1,47 @@ + 'Sabre\\DAV\\Xml\\Response\\MultiStatus', + '{DAV:}response' => 'Sabre\\DAV\\Xml\\Element\\Response', + + // Requests + '{DAV:}propfind' => 'Sabre\\DAV\\Xml\\Request\\PropFind', + '{DAV:}propertyupdate' => 'Sabre\\DAV\\Xml\\Request\\PropPatch', + '{DAV:}mkcol' => 'Sabre\\DAV\\Xml\\Request\\MkCol', + + // Properties + '{DAV:}resourcetype' => 'Sabre\\DAV\\Xml\\Property\\ResourceType', + ]; + + /** + * This is a default list of namespaces. + * + * If you are defining your own custom namespace, add it here to reduce + * bandwidth and improve legibility of xml bodies. + * + * @var array + */ + public $namespaceMap = [ + 'DAV:' => 'd', + 'http://sabredav.org/ns' => 's', + ]; +} diff --git a/3rdparty/sabre/dav/lib/DAVACL/ACLTrait.php b/3rdparty/sabre/dav/lib/DAVACL/ACLTrait.php new file mode 100644 index 00000000..98c1ce33 --- /dev/null +++ b/3rdparty/sabre/dav/lib/DAVACL/ACLTrait.php @@ -0,0 +1,94 @@ + '{DAV:}all', + 'principal' => '{DAV:}owner', + 'protected' => true, + ], + ]; + } + + /** + * Updates the ACL. + * + * This method will receive a list of new ACE's as an array argument. + */ + public function setACL(array $acl) + { + throw new \Sabre\DAV\Exception\Forbidden('Setting ACL is not supported on this node'); + } + + /** + * Returns the list of supported privileges for this node. + * + * The returned data structure is a list of nested privileges. + * See Sabre\DAVACL\Plugin::getDefaultSupportedPrivilegeSet for a simple + * standard structure. + * + * If null is returned from this method, the default privilege set is used, + * which is fine for most common usecases. + * + * @return array|null + */ + public function getSupportedPrivilegeSet() + { + return null; + } +} diff --git a/3rdparty/sabre/dav/lib/DAVACL/AbstractPrincipalCollection.php b/3rdparty/sabre/dav/lib/DAVACL/AbstractPrincipalCollection.php new file mode 100644 index 00000000..d26f7d27 --- /dev/null +++ b/3rdparty/sabre/dav/lib/DAVACL/AbstractPrincipalCollection.php @@ -0,0 +1,178 @@ +principalPrefix = $principalPrefix; + $this->principalBackend = $principalBackend; + } + + /** + * This method returns a node for a principal. + * + * The passed array contains principal information, and is guaranteed to + * at least contain a uri item. Other properties may or may not be + * supplied by the authentication backend. + * + * @return DAV\INode + */ + abstract public function getChildForPrincipal(array $principalInfo); + + /** + * Returns the name of this collection. + * + * @return string + */ + public function getName() + { + list(, $name) = Uri\split($this->principalPrefix); + + return $name; + } + + /** + * Return the list of users. + * + * @return array + */ + public function getChildren() + { + if ($this->disableListing) { + throw new DAV\Exception\MethodNotAllowed('Listing members of this collection is disabled'); + } + $children = []; + foreach ($this->principalBackend->getPrincipalsByPrefix($this->principalPrefix) as $principalInfo) { + $children[] = $this->getChildForPrincipal($principalInfo); + } + + return $children; + } + + /** + * Returns a child object, by its name. + * + * @param string $name + * + * @throws DAV\Exception\NotFound + * + * @return DAV\INode + */ + public function getChild($name) + { + $principalInfo = $this->principalBackend->getPrincipalByPath($this->principalPrefix.'/'.$name); + if (!$principalInfo) { + throw new DAV\Exception\NotFound('Principal with name '.$name.' not found'); + } + + return $this->getChildForPrincipal($principalInfo); + } + + /** + * This method is used to search for principals matching a set of + * properties. + * + * This search is specifically used by RFC3744's principal-property-search + * REPORT. You should at least allow searching on + * http://sabredav.org/ns}email-address. + * + * The actual search should be a unicode-non-case-sensitive search. The + * keys in searchProperties are the WebDAV property names, while the values + * are the property values to search on. + * + * By default, if multiple properties are submitted to this method, the + * various properties should be combined with 'AND'. If $test is set to + * 'anyof', it should be combined using 'OR'. + * + * This method should simply return a list of 'child names', which may be + * used to call $this->getChild in the future. + * + * @param string $test + * + * @return array + */ + public function searchPrincipals(array $searchProperties, $test = 'allof') + { + $result = $this->principalBackend->searchPrincipals($this->principalPrefix, $searchProperties, $test); + $r = []; + + foreach ($result as $row) { + list(, $r[]) = Uri\split($row); + } + + return $r; + } + + /** + * Finds a principal by its URI. + * + * This method may receive any type of uri, but mailto: addresses will be + * the most common. + * + * Implementation of this API is optional. It is currently used by the + * CalDAV system to find principals based on their email addresses. If this + * API is not implemented, some features may not work correctly. + * + * This method must return a relative principal path, or null, if the + * principal was not found or you refuse to find it. + * + * @param string $uri + * + * @return string + */ + public function findByUri($uri) + { + return $this->principalBackend->findByUri($uri, $this->principalPrefix); + } +} diff --git a/3rdparty/sabre/dav/lib/DAVACL/Exception/AceConflict.php b/3rdparty/sabre/dav/lib/DAVACL/Exception/AceConflict.php new file mode 100644 index 00000000..0fc3f778 --- /dev/null +++ b/3rdparty/sabre/dav/lib/DAVACL/Exception/AceConflict.php @@ -0,0 +1,31 @@ +ownerDocument; + + $np = $doc->createElementNS('DAV:', 'd:no-ace-conflict'); + $errorNode->appendChild($np); + } +} diff --git a/3rdparty/sabre/dav/lib/DAVACL/Exception/NeedPrivileges.php b/3rdparty/sabre/dav/lib/DAVACL/Exception/NeedPrivileges.php new file mode 100644 index 00000000..af1f01c2 --- /dev/null +++ b/3rdparty/sabre/dav/lib/DAVACL/Exception/NeedPrivileges.php @@ -0,0 +1,73 @@ +uri = $uri; + $this->privileges = $privileges; + + parent::__construct('User did not have the required privileges ('.implode(',', $privileges).') for path "'.$uri.'"'); + } + + /** + * Adds in extra information in the xml response. + * + * This method adds the {DAV:}need-privileges element as defined in rfc3744 + */ + public function serialize(DAV\Server $server, \DOMElement $errorNode) + { + $doc = $errorNode->ownerDocument; + + $np = $doc->createElementNS('DAV:', 'd:need-privileges'); + $errorNode->appendChild($np); + + foreach ($this->privileges as $privilege) { + $resource = $doc->createElementNS('DAV:', 'd:resource'); + $np->appendChild($resource); + + $resource->appendChild($doc->createElementNS('DAV:', 'd:href', $server->getBaseUri().$this->uri)); + + $priv = $doc->createElementNS('DAV:', 'd:privilege'); + $resource->appendChild($priv); + + preg_match('/^{([^}]*)}(.*)$/', $privilege, $privilegeParts); + $priv->appendChild($doc->createElementNS($privilegeParts[1], 'd:'.$privilegeParts[2])); + } + } +} diff --git a/3rdparty/sabre/dav/lib/DAVACL/Exception/NoAbstract.php b/3rdparty/sabre/dav/lib/DAVACL/Exception/NoAbstract.php new file mode 100644 index 00000000..b9c66169 --- /dev/null +++ b/3rdparty/sabre/dav/lib/DAVACL/Exception/NoAbstract.php @@ -0,0 +1,31 @@ +ownerDocument; + + $np = $doc->createElementNS('DAV:', 'd:no-abstract'); + $errorNode->appendChild($np); + } +} diff --git a/3rdparty/sabre/dav/lib/DAVACL/Exception/NotRecognizedPrincipal.php b/3rdparty/sabre/dav/lib/DAVACL/Exception/NotRecognizedPrincipal.php new file mode 100644 index 00000000..d4e72849 --- /dev/null +++ b/3rdparty/sabre/dav/lib/DAVACL/Exception/NotRecognizedPrincipal.php @@ -0,0 +1,31 @@ +ownerDocument; + + $np = $doc->createElementNS('DAV:', 'd:recognized-principal'); + $errorNode->appendChild($np); + } +} diff --git a/3rdparty/sabre/dav/lib/DAVACL/Exception/NotSupportedPrivilege.php b/3rdparty/sabre/dav/lib/DAVACL/Exception/NotSupportedPrivilege.php new file mode 100644 index 00000000..c04c5faa --- /dev/null +++ b/3rdparty/sabre/dav/lib/DAVACL/Exception/NotSupportedPrivilege.php @@ -0,0 +1,31 @@ +ownerDocument; + + $np = $doc->createElementNS('DAV:', 'd:not-supported-privilege'); + $errorNode->appendChild($np); + } +} diff --git a/3rdparty/sabre/dav/lib/DAVACL/FS/Collection.php b/3rdparty/sabre/dav/lib/DAVACL/FS/Collection.php new file mode 100644 index 00000000..85b04e2b --- /dev/null +++ b/3rdparty/sabre/dav/lib/DAVACL/FS/Collection.php @@ -0,0 +1,109 @@ +acl = $acl; + $this->owner = $owner; + } + + /** + * Returns a specific child node, referenced by its name. + * + * This method must throw Sabre\DAV\Exception\NotFound if the node does not + * exist. + * + * @param string $name + * + * @throws NotFound + * + * @return \Sabre\DAV\INode + */ + public function getChild($name) + { + $path = $this->path.'/'.$name; + + if (!file_exists($path)) { + throw new NotFound('File could not be located'); + } + if ('.' == $name || '..' == $name) { + throw new Forbidden('Permission denied to . and ..'); + } + if (is_dir($path)) { + return new self($path, $this->acl, $this->owner); + } else { + return new File($path, $this->acl, $this->owner); + } + } + + /** + * Returns the owner principal. + * + * This must be a url to a principal, or null if there's no owner + * + * @return string|null + */ + public function getOwner() + { + return $this->owner; + } + + /** + * Returns a list of ACE's for this node. + * + * Each ACE has the following properties: + * * 'privilege', a string such as {DAV:}read or {DAV:}write. These are + * currently the only supported privileges + * * 'principal', a url to the principal who owns the node + * * 'protected' (optional), indicating that this ACE is not allowed to + * be updated. + * + * @return array + */ + public function getACL() + { + return $this->acl; + } +} diff --git a/3rdparty/sabre/dav/lib/DAVACL/FS/File.php b/3rdparty/sabre/dav/lib/DAVACL/FS/File.php new file mode 100644 index 00000000..5506aa2c --- /dev/null +++ b/3rdparty/sabre/dav/lib/DAVACL/FS/File.php @@ -0,0 +1,78 @@ +acl = $acl; + $this->owner = $owner; + } + + /** + * Returns the owner principal. + * + * This must be a url to a principal, or null if there's no owner + * + * @return string|null + */ + public function getOwner() + { + return $this->owner; + } + + /** + * Returns a list of ACE's for this node. + * + * Each ACE has the following properties: + * * 'privilege', a string such as {DAV:}read or {DAV:}write. These are + * currently the only supported privileges + * * 'principal', a url to the principal who owns the node + * * 'protected' (optional), indicating that this ACE is not allowed to + * be updated. + * + * @return array + */ + public function getACL() + { + return $this->acl; + } +} diff --git a/3rdparty/sabre/dav/lib/DAVACL/FS/HomeCollection.php b/3rdparty/sabre/dav/lib/DAVACL/FS/HomeCollection.php new file mode 100644 index 00000000..fa476e09 --- /dev/null +++ b/3rdparty/sabre/dav/lib/DAVACL/FS/HomeCollection.php @@ -0,0 +1,123 @@ +storagePath = $storagePath; + } + + /** + * Returns the name of the node. + * + * This is used to generate the url. + * + * @return string + */ + public function getName() + { + return $this->collectionName; + } + + /** + * Returns a principals' collection of files. + * + * The passed array contains principal information, and is guaranteed to + * at least contain a uri item. Other properties may or may not be + * supplied by the authentication backend. + * + * @return \Sabre\DAV\INode + */ + public function getChildForPrincipal(array $principalInfo) + { + $owner = $principalInfo['uri']; + $acl = [ + [ + 'privilege' => '{DAV:}all', + 'principal' => '{DAV:}owner', + 'protected' => true, + ], + ]; + + list(, $principalBaseName) = Uri\split($owner); + + $path = $this->storagePath.'/'.$principalBaseName; + + if (!is_dir($path)) { + mkdir($path, 0777, true); + } + + return new Collection( + $path, + $acl, + $owner + ); + } + + /** + * Returns a list of ACE's for this node. + * + * Each ACE has the following properties: + * * 'privilege', a string such as {DAV:}read or {DAV:}write. These are + * currently the only supported privileges + * * 'principal', a url to the principal who owns the node + * * 'protected' (optional), indicating that this ACE is not allowed to + * be updated. + * + * @return array + */ + public function getACL() + { + return [ + [ + 'principal' => '{DAV:}authenticated', + 'privilege' => '{DAV:}read', + 'protected' => true, + ], + ]; + } +} diff --git a/3rdparty/sabre/dav/lib/DAVACL/IACL.php b/3rdparty/sabre/dav/lib/DAVACL/IACL.php new file mode 100644 index 00000000..291fb24a --- /dev/null +++ b/3rdparty/sabre/dav/lib/DAVACL/IACL.php @@ -0,0 +1,72 @@ +getChild in the future. + * + * @param string $test + * + * @return array + */ + public function searchPrincipals(array $searchProperties, $test = 'allof'); + + /** + * Finds a principal by its URI. + * + * This method may receive any type of uri, but mailto: addresses will be + * the most common. + * + * Implementation of this API is optional. It is currently used by the + * CalDAV system to find principals based on their email addresses. If this + * API is not implemented, some features may not work correctly. + * + * This method must return a relative principal path, or null, if the + * principal was not found or you refuse to find it. + * + * @param string $uri + * + * @return string + */ + public function findByUri($uri); +} diff --git a/3rdparty/sabre/dav/lib/DAVACL/Plugin.php b/3rdparty/sabre/dav/lib/DAVACL/Plugin.php new file mode 100644 index 00000000..f0497844 --- /dev/null +++ b/3rdparty/sabre/dav/lib/DAVACL/Plugin.php @@ -0,0 +1,1545 @@ + 'Display name', + '{http://sabredav.org/ns}email-address' => 'Email address', + ]; + + /** + * Any principal uri's added here, will automatically be added to the list + * of ACL's. They will effectively receive {DAV:}all privileges, as a + * protected privilege. + * + * @var array + */ + public $adminPrincipals = []; + + /** + * The ACL plugin allows privileges to be assigned to users that are not + * logged in. To facilitate that, it modifies the auth plugin's behavior + * to only require login when a privileged operation was denied. + * + * Unauthenticated access can be considered a security concern, so it's + * possible to turn this feature off to harden the server's security. + * + * @var bool + */ + public $allowUnauthenticatedAccess = true; + + /** + * Returns a list of features added by this plugin. + * + * This list is used in the response of a HTTP OPTIONS request. + * + * @return array + */ + public function getFeatures() + { + return ['access-control', 'calendarserver-principal-property-search']; + } + + /** + * Returns a list of available methods for a given url. + * + * @param string $uri + * + * @return array + */ + public function getMethods($uri) + { + return ['ACL']; + } + + /** + * Returns a plugin name. + * + * Using this name other plugins will be able to access other plugins + * using Sabre\DAV\Server::getPlugin + * + * @return string + */ + public function getPluginName() + { + return 'acl'; + } + + /** + * Returns a list of reports this plugin supports. + * + * This will be used in the {DAV:}supported-report-set property. + * Note that you still need to subscribe to the 'report' event to actually + * implement them + * + * @param string $uri + * + * @return array + */ + public function getSupportedReportSet($uri) + { + return [ + '{DAV:}expand-property', + '{DAV:}principal-match', + '{DAV:}principal-property-search', + '{DAV:}principal-search-property-set', + ]; + } + + /** + * Checks if the current user has the specified privilege(s). + * + * You can specify a single privilege, or a list of privileges. + * This method will throw an exception if the privilege is not available + * and return true otherwise. + * + * @param string $uri + * @param array|string $privileges + * @param int $recursion + * @param bool $throwExceptions if set to false, this method won't throw exceptions + * + * @throws NeedPrivileges + * @throws NotAuthenticated + * + * @return bool + */ + public function checkPrivileges($uri, $privileges, $recursion = self::R_PARENT, $throwExceptions = true) + { + if (!is_array($privileges)) { + $privileges = [$privileges]; + } + + $acl = $this->getCurrentUserPrivilegeSet($uri); + + $failed = []; + foreach ($privileges as $priv) { + if (!in_array($priv, $acl)) { + $failed[] = $priv; + } + } + + if ($failed) { + if ($this->allowUnauthenticatedAccess && is_null($this->getCurrentUserPrincipal())) { + // We are not authenticated. Kicking in the Auth plugin. + $authPlugin = $this->server->getPlugin('auth'); + $reasons = $authPlugin->getLoginFailedReasons(); + $authPlugin->challenge( + $this->server->httpRequest, + $this->server->httpResponse + ); + throw new NotAuthenticated(implode(', ', $reasons).'. Login was needed for privilege: '.implode(', ', $failed).' on '.$uri); + } + if ($throwExceptions) { + throw new NeedPrivileges($uri, $failed); + } else { + return false; + } + } + + return true; + } + + /** + * Returns the standard users' principal. + * + * This is one authoritative principal url for the current user. + * This method will return null if the user wasn't logged in. + * + * @return string|null + */ + public function getCurrentUserPrincipal() + { + /** @var $authPlugin \Sabre\DAV\Auth\Plugin */ + $authPlugin = $this->server->getPlugin('auth'); + if (!$authPlugin) { + return null; + } + + return $authPlugin->getCurrentPrincipal(); + } + + /** + * Returns a list of principals that's associated to the current + * user, either directly or through group membership. + * + * @return array + */ + public function getCurrentUserPrincipals() + { + $currentUser = $this->getCurrentUserPrincipal(); + + if (is_null($currentUser)) { + return []; + } + + return array_merge( + [$currentUser], + $this->getPrincipalMembership($currentUser) + ); + } + + /** + * Sets the default ACL rules. + * + * These rules are used for all nodes that don't implement the IACL interface. + */ + public function setDefaultAcl(array $acl) + { + $this->defaultAcl = $acl; + } + + /** + * Returns the default ACL rules. + * + * These rules are used for all nodes that don't implement the IACL interface. + * + * @return array + */ + public function getDefaultAcl() + { + return $this->defaultAcl; + } + + /** + * The default ACL rules. + * + * These rules are used for nodes that don't implement IACL. These default + * set of rules allow anyone to do anything, as long as they are + * authenticated. + * + * @var array + */ + protected $defaultAcl = [ + [ + 'principal' => '{DAV:}authenticated', + 'protected' => true, + 'privilege' => '{DAV:}all', + ], + ]; + + /** + * This array holds a cache for all the principals that are associated with + * a single principal. + * + * @var array + */ + protected $principalMembershipCache = []; + + /** + * Returns all the principal groups the specified principal is a member of. + * + * @param string $mainPrincipal + * + * @return array + */ + public function getPrincipalMembership($mainPrincipal) + { + // First check our cache + if (isset($this->principalMembershipCache[$mainPrincipal])) { + return $this->principalMembershipCache[$mainPrincipal]; + } + + $check = [$mainPrincipal]; + $principals = []; + + while (count($check)) { + $principal = array_shift($check); + + $node = $this->server->tree->getNodeForPath($principal); + if ($node instanceof IPrincipal) { + foreach ($node->getGroupMembership() as $groupMember) { + if (!in_array($groupMember, $principals)) { + $check[] = $groupMember; + $principals[] = $groupMember; + } + } + } + } + + // Store the result in the cache + $this->principalMembershipCache[$mainPrincipal] = $principals; + + return $principals; + } + + /** + * Find out of a principal equals another principal. + * + * This is a quick way to find out whether a principal URI is part of a + * group, or any subgroups. + * + * The first argument is the principal URI you want to check against. For + * example the principal group, and the second argument is the principal of + * which you want to find out of it is the same as the first principal, or + * in a member of the first principal's group or subgroups. + * + * So the arguments are not interchangeable. If principal A is in group B, + * passing 'B', 'A' will yield true, but 'A', 'B' is false. + * + * If the second argument is not passed, we will use the current user + * principal. + * + * @param string $checkPrincipal + * @param string $currentPrincipal + * + * @return bool + */ + public function principalMatchesPrincipal($checkPrincipal, $currentPrincipal = null) + { + if (is_null($currentPrincipal)) { + $currentPrincipal = $this->getCurrentUserPrincipal(); + } + if ($currentPrincipal === $checkPrincipal) { + return true; + } + if (is_null($currentPrincipal)) { + return false; + } + + return in_array( + $checkPrincipal, + $this->getPrincipalMembership($currentPrincipal) + ); + } + + /** + * Returns a tree of supported privileges for a resource. + * + * The returned array structure should be in this form: + * + * [ + * [ + * 'privilege' => '{DAV:}read', + * 'abstract' => false, + * 'aggregates' => [] + * ] + * ] + * + * Privileges can be nested using "aggregates". Doing so means that + * if you assign someone the aggregating privilege, all the + * sub-privileges will automatically be granted. + * + * Marking a privilege as abstract means that the privilege cannot be + * directly assigned, but must be assigned via the parent privilege. + * + * So a more complex version might look like this: + * + * [ + * [ + * 'privilege' => '{DAV:}read', + * 'abstract' => false, + * 'aggregates' => [ + * [ + * 'privilege' => '{DAV:}read-acl', + * 'abstract' => false, + * 'aggregates' => [], + * ] + * ] + * ] + * ] + * + * @param string|INode $node + * + * @return array + */ + public function getSupportedPrivilegeSet($node) + { + if (is_string($node)) { + $node = $this->server->tree->getNodeForPath($node); + } + + $supportedPrivileges = null; + if ($node instanceof IACL) { + $supportedPrivileges = $node->getSupportedPrivilegeSet(); + } + + if (is_null($supportedPrivileges)) { + // Default + $supportedPrivileges = [ + '{DAV:}read' => [ + 'abstract' => false, + 'aggregates' => [ + '{DAV:}read-acl' => [ + 'abstract' => false, + 'aggregates' => [], + ], + '{DAV:}read-current-user-privilege-set' => [ + 'abstract' => false, + 'aggregates' => [], + ], + ], + ], + '{DAV:}write' => [ + 'abstract' => false, + 'aggregates' => [ + '{DAV:}write-properties' => [ + 'abstract' => false, + 'aggregates' => [], + ], + '{DAV:}write-content' => [ + 'abstract' => false, + 'aggregates' => [], + ], + '{DAV:}unlock' => [ + 'abstract' => false, + 'aggregates' => [], + ], + ], + ], + ]; + if ($node instanceof DAV\ICollection) { + $supportedPrivileges['{DAV:}write']['aggregates']['{DAV:}bind'] = [ + 'abstract' => false, + 'aggregates' => [], + ]; + $supportedPrivileges['{DAV:}write']['aggregates']['{DAV:}unbind'] = [ + 'abstract' => false, + 'aggregates' => [], + ]; + } + if ($node instanceof IACL) { + $supportedPrivileges['{DAV:}write']['aggregates']['{DAV:}write-acl'] = [ + 'abstract' => false, + 'aggregates' => [], + ]; + } + } + + $this->server->emit( + 'getSupportedPrivilegeSet', + [$node, &$supportedPrivileges] + ); + + return $supportedPrivileges; + } + + /** + * Returns the supported privilege set as a flat list. + * + * This is much easier to parse. + * + * The returned list will be index by privilege name. + * The value is a struct containing the following properties: + * - aggregates + * - abstract + * - concrete + * + * @param string|INode $node + * + * @return array + */ + final public function getFlatPrivilegeSet($node) + { + $privs = [ + 'abstract' => false, + 'aggregates' => $this->getSupportedPrivilegeSet($node), + ]; + + $fpsTraverse = null; + $fpsTraverse = function ($privName, $privInfo, $concrete, &$flat) use (&$fpsTraverse) { + $myPriv = [ + 'privilege' => $privName, + 'abstract' => isset($privInfo['abstract']) && $privInfo['abstract'], + 'aggregates' => [], + 'concrete' => isset($privInfo['abstract']) && $privInfo['abstract'] ? $concrete : $privName, + ]; + + if (isset($privInfo['aggregates'])) { + foreach ($privInfo['aggregates'] as $subPrivName => $subPrivInfo) { + $myPriv['aggregates'][] = $subPrivName; + } + } + + $flat[$privName] = $myPriv; + + if (isset($privInfo['aggregates'])) { + foreach ($privInfo['aggregates'] as $subPrivName => $subPrivInfo) { + $fpsTraverse($subPrivName, $subPrivInfo, $myPriv['concrete'], $flat); + } + } + }; + + $flat = []; + $fpsTraverse('{DAV:}all', $privs, null, $flat); + + return $flat; + } + + /** + * Returns the full ACL list. + * + * Either a uri or a INode may be passed. + * + * null will be returned if the node doesn't support ACLs. + * + * @param string|DAV\INode $node + * + * @return array + */ + public function getAcl($node) + { + if (is_string($node)) { + $node = $this->server->tree->getNodeForPath($node); + } + if (!$node instanceof IACL) { + return $this->getDefaultAcl(); + } + $acl = $node->getACL(); + foreach ($this->adminPrincipals as $adminPrincipal) { + $acl[] = [ + 'principal' => $adminPrincipal, + 'privilege' => '{DAV:}all', + 'protected' => true, + ]; + } + + return $acl; + } + + /** + * Returns a list of privileges the current user has + * on a particular node. + * + * Either a uri or a DAV\INode may be passed. + * + * null will be returned if the node doesn't support ACLs. + * + * @param string|DAV\INode $node + * + * @return array + */ + public function getCurrentUserPrivilegeSet($node) + { + if (is_string($node)) { + $node = $this->server->tree->getNodeForPath($node); + } + + $acl = $this->getACL($node); + + $collected = []; + + $isAuthenticated = null !== $this->getCurrentUserPrincipal(); + + foreach ($acl as $ace) { + $principal = $ace['principal']; + + switch ($principal) { + case '{DAV:}owner': + $owner = $node->getOwner(); + if ($owner && $this->principalMatchesPrincipal($owner)) { + $collected[] = $ace; + } + break; + + // 'all' matches for every user + case '{DAV:}all': + $collected[] = $ace; + break; + + case '{DAV:}authenticated': + // Authenticated users only + if ($isAuthenticated) { + $collected[] = $ace; + } + break; + + case '{DAV:}unauthenticated': + // Unauthenticated users only + if (!$isAuthenticated) { + $collected[] = $ace; + } + break; + + default: + if ($this->principalMatchesPrincipal($ace['principal'])) { + $collected[] = $ace; + } + break; + } + } + + // Now we deduct all aggregated privileges. + $flat = $this->getFlatPrivilegeSet($node); + + $collected2 = []; + while (count($collected)) { + $current = array_pop($collected); + $collected2[] = $current['privilege']; + + if (!isset($flat[$current['privilege']])) { + // Ignoring privileges that are not in the supported-privileges list. + $this->server->getLogger()->debug('A node has the "'.$current['privilege'].'" in its ACL list, but this privilege was not reported in the supportedPrivilegeSet list. This will be ignored.'); + continue; + } + foreach ($flat[$current['privilege']]['aggregates'] as $subPriv) { + $collected2[] = $subPriv; + $collected[] = $flat[$subPriv]; + } + } + + return array_values(array_unique($collected2)); + } + + /** + * Returns a principal based on its uri. + * + * Returns null if the principal could not be found. + * + * @param string $uri + * + * @return string|null + */ + public function getPrincipalByUri($uri) + { + $result = null; + $collections = $this->principalCollectionSet; + foreach ($collections as $collection) { + try { + $principalCollection = $this->server->tree->getNodeForPath($collection); + } catch (NotFound $e) { + // Ignore and move on + continue; + } + + if (!$principalCollection instanceof IPrincipalCollection) { + // Not a principal collection, we're simply going to ignore + // this. + continue; + } + + $result = $principalCollection->findByUri($uri); + if ($result) { + return $result; + } + } + } + + /** + * Principal property search. + * + * This method can search for principals matching certain values in + * properties. + * + * This method will return a list of properties for the matched properties. + * + * @param array $searchProperties The properties to search on. This is a + * key-value list. The keys are property + * names, and the values the strings to + * match them on. + * @param array $requestedProperties this is the list of properties to + * return for every match + * @param string $collectionUri the principal collection to search on. + * If this is omitted, the standard + * principal collection-set will be used + * @param string $test "allof" to use AND to search the + * properties. 'anyof' for OR. + * + * @return array This method returns an array structure similar to + * Sabre\DAV\Server::getPropertiesForPath. Returned + * properties are index by a HTTP status code. + */ + public function principalSearch(array $searchProperties, array $requestedProperties, $collectionUri = null, $test = 'allof') + { + if (!is_null($collectionUri)) { + $uris = [$collectionUri]; + } else { + $uris = $this->principalCollectionSet; + } + + $lookupResults = []; + foreach ($uris as $uri) { + $principalCollection = $this->server->tree->getNodeForPath($uri); + if (!$principalCollection instanceof IPrincipalCollection) { + // Not a principal collection, we're simply going to ignore + // this. + continue; + } + + $results = $principalCollection->searchPrincipals($searchProperties, $test); + foreach ($results as $result) { + $lookupResults[] = rtrim($uri, '/').'/'.$result; + } + } + + $matches = []; + + foreach ($lookupResults as $lookupResult) { + list($matches[]) = $this->server->getPropertiesForPath($lookupResult, $requestedProperties, 0); + } + + return $matches; + } + + /** + * Sets up the plugin. + * + * This method is automatically called by the server class. + */ + public function initialize(DAV\Server $server) + { + if ($this->allowUnauthenticatedAccess) { + $authPlugin = $server->getPlugin('auth'); + if (!$authPlugin) { + throw new \Exception('The Auth plugin must be loaded before the ACL plugin if you want to allow unauthenticated access.'); + } + $authPlugin->autoRequireLogin = false; + } + + $this->server = $server; + $server->on('propFind', [$this, 'propFind'], 20); + $server->on('beforeMethod:*', [$this, 'beforeMethod'], 20); + $server->on('beforeBind', [$this, 'beforeBind'], 20); + $server->on('beforeUnbind', [$this, 'beforeUnbind'], 20); + $server->on('propPatch', [$this, 'propPatch']); + $server->on('beforeUnlock', [$this, 'beforeUnlock'], 20); + $server->on('report', [$this, 'report']); + $server->on('method:ACL', [$this, 'httpAcl']); + $server->on('onHTMLActionsPanel', [$this, 'htmlActionsPanel']); + $server->on('getPrincipalByUri', function ($principal, &$uri) { + $uri = $this->getPrincipalByUri($principal); + + // Break event chain + if ($uri) { + return false; + } + }); + + array_push($server->protectedProperties, + '{DAV:}alternate-URI-set', + '{DAV:}principal-URL', + '{DAV:}group-membership', + '{DAV:}principal-collection-set', + '{DAV:}current-user-principal', + '{DAV:}supported-privilege-set', + '{DAV:}current-user-privilege-set', + '{DAV:}acl', + '{DAV:}acl-restrictions', + '{DAV:}inherited-acl-set', + '{DAV:}owner', + '{DAV:}group' + ); + + // Automatically mapping nodes implementing IPrincipal to the + // {DAV:}principal resourcetype. + $server->resourceTypeMapping['Sabre\\DAVACL\\IPrincipal'] = '{DAV:}principal'; + + // Mapping the group-member-set property to the HrefList property + // class. + $server->xml->elementMap['{DAV:}group-member-set'] = 'Sabre\\DAV\\Xml\\Property\\Href'; + $server->xml->elementMap['{DAV:}acl'] = 'Sabre\\DAVACL\\Xml\\Property\\Acl'; + $server->xml->elementMap['{DAV:}acl-principal-prop-set'] = 'Sabre\\DAVACL\\Xml\\Request\\AclPrincipalPropSetReport'; + $server->xml->elementMap['{DAV:}expand-property'] = 'Sabre\\DAVACL\\Xml\\Request\\ExpandPropertyReport'; + $server->xml->elementMap['{DAV:}principal-property-search'] = 'Sabre\\DAVACL\\Xml\\Request\\PrincipalPropertySearchReport'; + $server->xml->elementMap['{DAV:}principal-search-property-set'] = 'Sabre\\DAVACL\\Xml\\Request\\PrincipalSearchPropertySetReport'; + $server->xml->elementMap['{DAV:}principal-match'] = 'Sabre\\DAVACL\\Xml\\Request\\PrincipalMatchReport'; + } + + /* {{{ Event handlers */ + + /** + * Triggered before any method is handled. + */ + public function beforeMethod(RequestInterface $request, ResponseInterface $response) + { + $method = $request->getMethod(); + $path = $request->getPath(); + + $exists = $this->server->tree->nodeExists($path); + + // If the node doesn't exists, none of these checks apply + if (!$exists) { + return; + } + + switch ($method) { + case 'GET': + case 'HEAD': + case 'OPTIONS': + // For these 3 we only need to know if the node is readable. + $this->checkPrivileges($path, '{DAV:}read'); + break; + + case 'PUT': + case 'LOCK': + // This method requires the write-content priv if the node + // already exists, and bind on the parent if the node is being + // created. + // The bind privilege is handled in the beforeBind event. + $this->checkPrivileges($path, '{DAV:}write-content'); + break; + + case 'UNLOCK': + // Unlock is always allowed at the moment. + break; + + case 'PROPPATCH': + $this->checkPrivileges($path, '{DAV:}write-properties'); + break; + + case 'ACL': + $this->checkPrivileges($path, '{DAV:}write-acl'); + break; + + case 'COPY': + case 'MOVE': + // Copy requires read privileges on the entire source tree. + // If the target exists write-content normally needs to be + // checked, however, we're deleting the node beforehand and + // creating a new one after, so this is handled by the + // beforeUnbind event. + // + // The creation of the new node is handled by the beforeBind + // event. + // + // If MOVE is used beforeUnbind will also be used to check if + // the sourcenode can be deleted. + $this->checkPrivileges($path, '{DAV:}read', self::R_RECURSIVE); + break; + } + } + + /** + * Triggered before a new node is created. + * + * This allows us to check permissions for any operation that creates a + * new node, such as PUT, MKCOL, MKCALENDAR, LOCK, COPY and MOVE. + * + * @param string $uri + */ + public function beforeBind($uri) + { + list($parentUri) = Uri\split($uri); + $this->checkPrivileges($parentUri, '{DAV:}bind'); + } + + /** + * Triggered before a node is deleted. + * + * This allows us to check permissions for any operation that will delete + * an existing node. + * + * @param string $uri + */ + public function beforeUnbind($uri) + { + list($parentUri) = Uri\split($uri); + $this->checkPrivileges($parentUri, '{DAV:}unbind', self::R_RECURSIVEPARENTS); + } + + /** + * Triggered before a node is unlocked. + * + * @param string $uri + * @TODO: not yet implemented + */ + public function beforeUnlock($uri, DAV\Locks\LockInfo $lock) + { + } + + /** + * Triggered before properties are looked up in specific nodes. + * + * @TODO really should be broken into multiple methods, or even a class. + */ + public function propFind(DAV\PropFind $propFind, DAV\INode $node) + { + $path = $propFind->getPath(); + + // Checking the read permission + if (!$this->checkPrivileges($path, '{DAV:}read', self::R_PARENT, false)) { + // User is not allowed to read properties + + // Returning false causes the property-fetching system to pretend + // that the node does not exist, and will cause it to be hidden + // from listings such as PROPFIND or the browser plugin. + if ($this->hideNodesFromListings) { + return false; + } + + // Otherwise we simply mark every property as 403. + foreach ($propFind->getRequestedProperties() as $requestedProperty) { + $propFind->set($requestedProperty, null, 403); + } + + return; + } + + /* Adding principal properties */ + if ($node instanceof IPrincipal) { + $propFind->handle('{DAV:}alternate-URI-set', function () use ($node) { + return new Href($node->getAlternateUriSet()); + }); + $propFind->handle('{DAV:}principal-URL', function () use ($node) { + return new Href($node->getPrincipalUrl().'/'); + }); + $propFind->handle('{DAV:}group-member-set', function () use ($node) { + $members = $node->getGroupMemberSet(); + foreach ($members as $k => $member) { + $members[$k] = rtrim($member, '/').'/'; + } + + return new Href($members); + }); + $propFind->handle('{DAV:}group-membership', function () use ($node) { + $members = $node->getGroupMembership(); + foreach ($members as $k => $member) { + $members[$k] = rtrim($member, '/').'/'; + } + + return new Href($members); + }); + $propFind->handle('{DAV:}displayname', [$node, 'getDisplayName']); + } + + $propFind->handle('{DAV:}principal-collection-set', function () { + $val = $this->principalCollectionSet; + // Ensuring all collections end with a slash + foreach ($val as $k => $v) { + $val[$k] = $v.'/'; + } + + return new Href($val); + }); + $propFind->handle('{DAV:}current-user-principal', function () { + if ($url = $this->getCurrentUserPrincipal()) { + return new Xml\Property\Principal(Xml\Property\Principal::HREF, $url.'/'); + } else { + return new Xml\Property\Principal(Xml\Property\Principal::UNAUTHENTICATED); + } + }); + $propFind->handle('{DAV:}supported-privilege-set', function () use ($node) { + return new Xml\Property\SupportedPrivilegeSet($this->getSupportedPrivilegeSet($node)); + }); + $propFind->handle('{DAV:}current-user-privilege-set', function () use ($node, $propFind, $path) { + if (!$this->checkPrivileges($path, '{DAV:}read-current-user-privilege-set', self::R_PARENT, false)) { + $propFind->set('{DAV:}current-user-privilege-set', null, 403); + } else { + $val = $this->getCurrentUserPrivilegeSet($node); + + return new Xml\Property\CurrentUserPrivilegeSet($val); + } + }); + $propFind->handle('{DAV:}acl', function () use ($node, $propFind, $path) { + /* The ACL property contains all the permissions */ + if (!$this->checkPrivileges($path, '{DAV:}read-acl', self::R_PARENT, false)) { + $propFind->set('{DAV:}acl', null, 403); + } else { + $acl = $this->getACL($node); + + return new Xml\Property\Acl($this->getACL($node)); + } + }); + $propFind->handle('{DAV:}acl-restrictions', function () { + return new Xml\Property\AclRestrictions(); + }); + + /* Adding ACL properties */ + if ($node instanceof IACL) { + $propFind->handle('{DAV:}owner', function () use ($node) { + return new Href($node->getOwner().'/'); + }); + } + } + + /** + * This method intercepts PROPPATCH methods and make sure the + * group-member-set is updated correctly. + * + * @param string $path + */ + public function propPatch($path, DAV\PropPatch $propPatch) + { + $propPatch->handle('{DAV:}group-member-set', function ($value) use ($path) { + if (is_null($value)) { + $memberSet = []; + } elseif ($value instanceof Href) { + $memberSet = array_map( + [$this->server, 'calculateUri'], + $value->getHrefs() + ); + } else { + throw new DAV\Exception('The group-member-set property MUST be an instance of Sabre\DAV\Property\HrefList or null'); + } + $node = $this->server->tree->getNodeForPath($path); + if (!($node instanceof IPrincipal)) { + // Fail + return false; + } + + $node->setGroupMemberSet($memberSet); + // We must also clear our cache, just in case + + $this->principalMembershipCache = []; + + return true; + }); + } + + /** + * This method handles HTTP REPORT requests. + * + * @param string $reportName + * @param mixed $report + * @param mixed $path + */ + public function report($reportName, $report, $path) + { + switch ($reportName) { + case '{DAV:}principal-property-search': + $this->server->transactionType = 'report-principal-property-search'; + $this->principalPropertySearchReport($path, $report); + + return false; + case '{DAV:}principal-search-property-set': + $this->server->transactionType = 'report-principal-search-property-set'; + $this->principalSearchPropertySetReport($path, $report); + + return false; + case '{DAV:}expand-property': + $this->server->transactionType = 'report-expand-property'; + $this->expandPropertyReport($path, $report); + + return false; + case '{DAV:}principal-match': + $this->server->transactionType = 'report-principal-match'; + $this->principalMatchReport($path, $report); + + return false; + case '{DAV:}acl-principal-prop-set': + $this->server->transactionType = 'acl-principal-prop-set'; + $this->aclPrincipalPropSetReport($path, $report); + + return false; + } + } + + /** + * This method is responsible for handling the 'ACL' event. + * + * @return bool + */ + public function httpAcl(RequestInterface $request, ResponseInterface $response) + { + $path = $request->getPath(); + $body = $request->getBodyAsString(); + + if (!$body) { + throw new DAV\Exception\BadRequest('XML body expected in ACL request'); + } + + $acl = $this->server->xml->expect('{DAV:}acl', $body); + $newAcl = $acl->getPrivileges(); + + // Normalizing urls + foreach ($newAcl as $k => $newAce) { + $newAcl[$k]['principal'] = $this->server->calculateUri($newAce['principal']); + } + $node = $this->server->tree->getNodeForPath($path); + + if (!$node instanceof IACL) { + throw new DAV\Exception\MethodNotAllowed('This node does not support the ACL method'); + } + + $oldAcl = $this->getACL($node); + + $supportedPrivileges = $this->getFlatPrivilegeSet($node); + + /* Checking if protected principals from the existing principal set are + not overwritten. */ + foreach ($oldAcl as $oldAce) { + if (!isset($oldAce['protected']) || !$oldAce['protected']) { + continue; + } + + $found = false; + foreach ($newAcl as $newAce) { + if ( + $newAce['privilege'] === $oldAce['privilege'] && + $newAce['principal'] === $oldAce['principal'] && + $newAce['protected'] + ) { + $found = true; + } + } + + if (!$found) { + throw new Exception\AceConflict('This resource contained a protected {DAV:}ace, but this privilege did not occur in the ACL request'); + } + } + + foreach ($newAcl as $newAce) { + // Do we recognize the privilege + if (!isset($supportedPrivileges[$newAce['privilege']])) { + throw new Exception\NotSupportedPrivilege('The privilege you specified ('.$newAce['privilege'].') is not recognized by this server'); + } + + if ($supportedPrivileges[$newAce['privilege']]['abstract']) { + throw new Exception\NoAbstract('The privilege you specified ('.$newAce['privilege'].') is an abstract privilege'); + } + + // Looking up the principal + try { + $principal = $this->server->tree->getNodeForPath($newAce['principal']); + } catch (NotFound $e) { + throw new Exception\NotRecognizedPrincipal('The specified principal ('.$newAce['principal'].') does not exist'); + } + if (!($principal instanceof IPrincipal)) { + throw new Exception\NotRecognizedPrincipal('The specified uri ('.$newAce['principal'].') is not a principal'); + } + } + $node->setACL($newAcl); + + $response->setStatus(200); + + // Breaking the event chain, because we handled this method. + return false; + } + + /* }}} */ + + /* Reports {{{ */ + + /** + * The principal-match report is defined in RFC3744, section 9.3. + * + * This report allows a client to figure out based on the current user, + * or a principal URL, the principal URL and principal URLs of groups that + * principal belongs to. + * + * @param string $path + */ + protected function principalMatchReport($path, Xml\Request\PrincipalMatchReport $report) + { + $depth = $this->server->getHTTPDepth(0); + if (0 !== $depth) { + throw new BadRequest('The principal-match report is only defined on Depth: 0'); + } + + $currentPrincipals = $this->getCurrentUserPrincipals(); + + $result = []; + + if (Xml\Request\PrincipalMatchReport::SELF === $report->type) { + // Finding all principals under the request uri that match the + // current principal. + foreach ($currentPrincipals as $currentPrincipal) { + if ($currentPrincipal === $path || 0 === strpos($currentPrincipal, $path.'/')) { + $result[] = $currentPrincipal; + } + } + } else { + // We need to find all resources that have a property that matches + // one of the current principals. + $candidates = $this->server->getPropertiesForPath( + $path, + [$report->principalProperty], + 1 + ); + + foreach ($candidates as $candidate) { + if (!isset($candidate[200][$report->principalProperty])) { + continue; + } + + $hrefs = $candidate[200][$report->principalProperty]; + + if (!$hrefs instanceof Href) { + continue; + } + + foreach ($hrefs->getHrefs() as $href) { + if (in_array(trim($href, '/'), $currentPrincipals)) { + $result[] = $candidate['href']; + continue 2; + } + } + } + } + + $responses = []; + + foreach ($result as $item) { + $properties = []; + + if ($report->properties) { + $foo = $this->server->getPropertiesForPath($item, $report->properties); + $foo = $foo[0]; + $item = $foo['href']; + unset($foo['href']); + $properties = $foo; + } + + $responses[] = new DAV\Xml\Element\Response( + $item, + $properties, + '200' + ); + } + + $this->server->httpResponse->setHeader('Content-Type', 'application/xml; charset=utf-8'); + $this->server->httpResponse->setStatus(207); + $this->server->httpResponse->setBody( + $this->server->xml->write( + '{DAV:}multistatus', + $responses, + $this->server->getBaseUri() + ) + ); + } + + /** + * The expand-property report is defined in RFC3253 section 3.8. + * + * This report is very similar to a standard PROPFIND. The difference is + * that it has the additional ability to look at properties containing a + * {DAV:}href element, follow that property and grab additional elements + * there. + * + * Other rfc's, such as ACL rely on this report, so it made sense to put + * it in this plugin. + * + * @param string $path + * @param Xml\Request\ExpandPropertyReport $report + */ + protected function expandPropertyReport($path, $report) + { + $depth = $this->server->getHTTPDepth(0); + + $result = $this->expandProperties($path, $report->properties, $depth); + + $xml = $this->server->xml->write( + '{DAV:}multistatus', + new DAV\Xml\Response\MultiStatus($result), + $this->server->getBaseUri() + ); + $this->server->httpResponse->setHeader('Content-Type', 'application/xml; charset=utf-8'); + $this->server->httpResponse->setStatus(207); + $this->server->httpResponse->setBody($xml); + } + + /** + * This method expands all the properties and returns + * a list with property values. + * + * @param array $path + * @param array $requestedProperties the list of required properties + * @param int $depth + * + * @return array + */ + protected function expandProperties($path, array $requestedProperties, $depth) + { + $foundProperties = $this->server->getPropertiesForPath($path, array_keys($requestedProperties), $depth); + + $result = []; + + foreach ($foundProperties as $node) { + foreach ($requestedProperties as $propertyName => $childRequestedProperties) { + // We're only traversing if sub-properties were requested + if (!is_array($childRequestedProperties) || 0 === count($childRequestedProperties)) { + continue; + } + + // We only have to do the expansion if the property was found + // and it contains an href element. + if (!array_key_exists($propertyName, $node[200])) { + continue; + } + + if (!$node[200][$propertyName] instanceof DAV\Xml\Property\Href) { + continue; + } + + $childHrefs = $node[200][$propertyName]->getHrefs(); + $childProps = []; + + foreach ($childHrefs as $href) { + // Gathering the result of the children + $childProps[] = [ + 'name' => '{DAV:}response', + 'value' => $this->expandProperties($href, $childRequestedProperties, 0)[0], + ]; + } + + // Replacing the property with its expanded form. + $node[200][$propertyName] = $childProps; + } + $result[] = new DAV\Xml\Element\Response($node['href'], $node); + } + + return $result; + } + + /** + * principalSearchPropertySetReport. + * + * This method responsible for handing the + * {DAV:}principal-search-property-set report. This report returns a list + * of properties the client may search on, using the + * {DAV:}principal-property-search report. + * + * @param string $path + * @param Xml\Request\PrincipalSearchPropertySetReport $report + */ + protected function principalSearchPropertySetReport($path, $report) + { + $httpDepth = $this->server->getHTTPDepth(0); + if (0 !== $httpDepth) { + throw new DAV\Exception\BadRequest('This report is only defined when Depth: 0'); + } + + $writer = $this->server->xml->getWriter(); + $writer->openMemory(); + $writer->startDocument(); + + $writer->startElement('{DAV:}principal-search-property-set'); + + foreach ($this->principalSearchPropertySet as $propertyName => $description) { + $writer->startElement('{DAV:}principal-search-property'); + $writer->startElement('{DAV:}prop'); + + $writer->writeElement($propertyName); + + $writer->endElement(); // prop + + if ($description) { + $writer->write([[ + 'name' => '{DAV:}description', + 'value' => $description, + 'attributes' => ['xml:lang' => 'en'], + ]]); + } + + $writer->endElement(); // principal-search-property + } + + $writer->endElement(); // principal-search-property-set + + $this->server->httpResponse->setHeader('Content-Type', 'application/xml; charset=utf-8'); + $this->server->httpResponse->setStatus(200); + $this->server->httpResponse->setBody($writer->outputMemory()); + } + + /** + * principalPropertySearchReport. + * + * This method is responsible for handing the + * {DAV:}principal-property-search report. This report can be used for + * clients to search for groups of principals, based on the value of one + * or more properties. + * + * @param string $path + */ + protected function principalPropertySearchReport($path, Xml\Request\PrincipalPropertySearchReport $report) + { + if ($report->applyToPrincipalCollectionSet) { + $path = null; + } + if (0 !== $this->server->getHttpDepth('0')) { + throw new BadRequest('Depth must be 0'); + } + $result = $this->principalSearch( + $report->searchProperties, + $report->properties, + $path, + $report->test + ); + + $prefer = $this->server->getHTTPPrefer(); + + $this->server->httpResponse->setStatus(207); + $this->server->httpResponse->setHeader('Content-Type', 'application/xml; charset=utf-8'); + $this->server->httpResponse->setHeader('Vary', 'Brief,Prefer'); + $this->server->httpResponse->setBody($this->server->generateMultiStatus($result, 'minimal' === $prefer['return'])); + } + + /** + * aclPrincipalPropSet REPORT. + * + * This method is responsible for handling the {DAV:}acl-principal-prop-set + * REPORT, as defined in: + * + * https://tools.ietf.org/html/rfc3744#section-9.2 + * + * This REPORT allows a user to quickly fetch information about all + * principals specified in the access control list. Most commonly this + * is used to for example generate a UI with ACL rules, allowing you + * to show names for principals for every entry. + * + * @param string $path + */ + protected function aclPrincipalPropSetReport($path, Xml\Request\AclPrincipalPropSetReport $report) + { + if (0 !== $this->server->getHTTPDepth(0)) { + throw new BadRequest('The {DAV:}acl-principal-prop-set REPORT only supports Depth 0'); + } + + // Fetching ACL rules for the given path. We're using the property + // API and not the local getACL, because it will ensure that all + // business rules and restrictions are applied. + $acl = $this->server->getProperties($path, '{DAV:}acl'); + + if (!$acl || !isset($acl['{DAV:}acl'])) { + throw new Forbidden('Could not fetch ACL rules for this path'); + } + + $principals = []; + foreach ($acl['{DAV:}acl']->getPrivileges() as $ace) { + if ('{' === $ace['principal'][0]) { + // It's not a principal, it's one of the special rules such as {DAV:}authenticated + continue; + } + + $principals[] = $ace['principal']; + } + + $properties = $this->server->getPropertiesForMultiplePaths( + $principals, + $report->properties + ); + + $this->server->httpResponse->setStatus(207); + $this->server->httpResponse->setHeader('Content-Type', 'application/xml; charset=utf-8'); + $this->server->httpResponse->setBody( + $this->server->generateMultiStatus($properties) + ); + } + + /* }}} */ + + /** + * This method is used to generate HTML output for the + * DAV\Browser\Plugin. This allows us to generate an interface users + * can use to create new calendars. + * + * @param string $output + * + * @return bool + */ + public function htmlActionsPanel(DAV\INode $node, &$output) + { + if (!$node instanceof PrincipalCollection) { + return; + } + + $output .= '
    +

    Create new principal

    + + +
    +
    +
    + +
    + '; + + return false; + } + + /** + * Returns a bunch of meta-data about the plugin. + * + * Providing this information is optional, and is mainly displayed by the + * Browser plugin. + * + * The description key in the returned array may contain html and will not + * be sanitized. + * + * @return array + */ + public function getPluginInfo() + { + return [ + 'name' => $this->getPluginName(), + 'description' => 'Adds support for WebDAV ACL (rfc3744)', + 'link' => 'http://sabre.io/dav/acl/', + ]; + } +} diff --git a/3rdparty/sabre/dav/lib/DAVACL/Principal.php b/3rdparty/sabre/dav/lib/DAVACL/Principal.php new file mode 100644 index 00000000..ada38ab7 --- /dev/null +++ b/3rdparty/sabre/dav/lib/DAVACL/Principal.php @@ -0,0 +1,199 @@ +principalBackend = $principalBackend; + $this->principalProperties = $principalProperties; + } + + /** + * Returns the full principal url. + * + * @return string + */ + public function getPrincipalUrl() + { + return $this->principalProperties['uri']; + } + + /** + * Returns a list of alternative urls for a principal. + * + * This can for example be an email address, or ldap url. + * + * @return array + */ + public function getAlternateUriSet() + { + $uris = []; + if (isset($this->principalProperties['{DAV:}alternate-URI-set'])) { + $uris = $this->principalProperties['{DAV:}alternate-URI-set']; + } + + if (isset($this->principalProperties['{http://sabredav.org/ns}email-address'])) { + $uris[] = 'mailto:'.$this->principalProperties['{http://sabredav.org/ns}email-address']; + } + + return array_unique($uris); + } + + /** + * Returns the list of group members. + * + * If this principal is a group, this function should return + * all member principal uri's for the group. + * + * @return array + */ + public function getGroupMemberSet() + { + return $this->principalBackend->getGroupMemberSet($this->principalProperties['uri']); + } + + /** + * Returns the list of groups this principal is member of. + * + * If this principal is a member of a (list of) groups, this function + * should return a list of principal uri's for it's members. + * + * @return array + */ + public function getGroupMembership() + { + return $this->principalBackend->getGroupMemberShip($this->principalProperties['uri']); + } + + /** + * Sets a list of group members. + * + * If this principal is a group, this method sets all the group members. + * The list of members is always overwritten, never appended to. + * + * This method should throw an exception if the members could not be set. + */ + public function setGroupMemberSet(array $groupMembers) + { + $this->principalBackend->setGroupMemberSet($this->principalProperties['uri'], $groupMembers); + } + + /** + * Returns this principals name. + * + * @return string + */ + public function getName() + { + $uri = $this->principalProperties['uri']; + list(, $name) = Uri\split($uri); + + return $name; + } + + /** + * Returns the name of the user. + * + * @return string + */ + public function getDisplayName() + { + if (isset($this->principalProperties['{DAV:}displayname'])) { + return $this->principalProperties['{DAV:}displayname']; + } else { + return $this->getName(); + } + } + + /** + * Returns a list of properties. + * + * @param array $requestedProperties + * + * @return array + */ + public function getProperties($requestedProperties) + { + $newProperties = []; + foreach ($requestedProperties as $propName) { + if (isset($this->principalProperties[$propName])) { + $newProperties[$propName] = $this->principalProperties[$propName]; + } + } + + return $newProperties; + } + + /** + * Updates properties on this node. + * + * This method received a PropPatch object, which contains all the + * information about the update. + * + * To update specific properties, call the 'handle' method on this object. + * Read the PropPatch documentation for more information. + */ + public function propPatch(DAV\PropPatch $propPatch) + { + return $this->principalBackend->updatePrincipal( + $this->principalProperties['uri'], + $propPatch + ); + } + + /** + * Returns the owner principal. + * + * This must be a url to a principal, or null if there's no owner + * + * @return string|null + */ + public function getOwner() + { + return $this->principalProperties['uri']; + } +} diff --git a/3rdparty/sabre/dav/lib/DAVACL/PrincipalBackend/AbstractBackend.php b/3rdparty/sabre/dav/lib/DAVACL/PrincipalBackend/AbstractBackend.php new file mode 100644 index 00000000..f61a0c95 --- /dev/null +++ b/3rdparty/sabre/dav/lib/DAVACL/PrincipalBackend/AbstractBackend.php @@ -0,0 +1,54 @@ +searchPrincipals( + $principalPrefix, + ['{http://sabredav.org/ns}email-address' => substr($uri, 7)] + ); + + if ($result) { + return $result[0]; + } + } +} diff --git a/3rdparty/sabre/dav/lib/DAVACL/PrincipalBackend/BackendInterface.php b/3rdparty/sabre/dav/lib/DAVACL/PrincipalBackend/BackendInterface.php new file mode 100644 index 00000000..2fd31913 --- /dev/null +++ b/3rdparty/sabre/dav/lib/DAVACL/PrincipalBackend/BackendInterface.php @@ -0,0 +1,143 @@ + [ + 'dbField' => 'displayname', + ], + + /* + * This is the users' primary email-address. + */ + '{http://sabredav.org/ns}email-address' => [ + 'dbField' => 'email', + ], + ]; + + /** + * Sets up the backend. + */ + public function __construct(\PDO $pdo) + { + $this->pdo = $pdo; + } + + /** + * Returns a list of principals based on a prefix. + * + * This prefix will often contain something like 'principals'. You are only + * expected to return principals that are in this base path. + * + * You are expected to return at least a 'uri' for every user, you can + * return any additional properties if you wish so. Common properties are: + * {DAV:}displayname + * {http://sabredav.org/ns}email-address - This is a custom SabreDAV + * field that's actually injected in a number of other properties. If + * you have an email address, use this property. + * + * @param string $prefixPath + * + * @return array + */ + public function getPrincipalsByPrefix($prefixPath) + { + $fields = [ + 'uri', + ]; + + foreach ($this->fieldMap as $key => $value) { + $fields[] = $value['dbField']; + } + $result = $this->pdo->query('SELECT '.implode(',', $fields).' FROM '.$this->tableName); + + $principals = []; + + while ($row = $result->fetch(\PDO::FETCH_ASSOC)) { + // Checking if the principal is in the prefix + list($rowPrefix) = Uri\split($row['uri']); + if ($rowPrefix !== $prefixPath) { + continue; + } + + $principal = [ + 'uri' => $row['uri'], + ]; + foreach ($this->fieldMap as $key => $value) { + if ($row[$value['dbField']]) { + $principal[$key] = $row[$value['dbField']]; + } + } + $principals[] = $principal; + } + + return $principals; + } + + /** + * Returns a specific principal, specified by it's path. + * The returned structure should be the exact same as from + * getPrincipalsByPrefix. + * + * @param string $path + * + * @return array + */ + public function getPrincipalByPath($path) + { + $fields = [ + 'id', + 'uri', + ]; + + foreach ($this->fieldMap as $key => $value) { + $fields[] = $value['dbField']; + } + $stmt = $this->pdo->prepare('SELECT '.implode(',', $fields).' FROM '.$this->tableName.' WHERE uri = ?'); + $stmt->execute([$path]); + + $row = $stmt->fetch(\PDO::FETCH_ASSOC); + if (!$row) { + return; + } + + $principal = [ + 'id' => $row['id'], + 'uri' => $row['uri'], + ]; + foreach ($this->fieldMap as $key => $value) { + if ($row[$value['dbField']]) { + $principal[$key] = $row[$value['dbField']]; + } + } + + return $principal; + } + + /** + * Updates one ore more webdav properties on a principal. + * + * The list of mutations is stored in a Sabre\DAV\PropPatch object. + * To do the actual updates, you must tell this object which properties + * you're going to process with the handle() method. + * + * Calling the handle method is like telling the PropPatch object "I + * promise I can handle updating this property". + * + * Read the PropPatch documentation for more info and examples. + * + * @param string $path + */ + public function updatePrincipal($path, DAV\PropPatch $propPatch) + { + $propPatch->handle(array_keys($this->fieldMap), function ($properties) use ($path) { + $query = 'UPDATE '.$this->tableName.' SET '; + $first = true; + + $values = []; + + foreach ($properties as $key => $value) { + $dbField = $this->fieldMap[$key]['dbField']; + + if (!$first) { + $query .= ', '; + } + $first = false; + $query .= $dbField.' = :'.$dbField; + $values[$dbField] = $value; + } + + $query .= ' WHERE uri = :uri'; + $values['uri'] = $path; + + $stmt = $this->pdo->prepare($query); + $stmt->execute($values); + + return true; + }); + } + + /** + * This method is used to search for principals matching a set of + * properties. + * + * This search is specifically used by RFC3744's principal-property-search + * REPORT. + * + * The actual search should be a unicode-non-case-sensitive search. The + * keys in searchProperties are the WebDAV property names, while the values + * are the property values to search on. + * + * By default, if multiple properties are submitted to this method, the + * various properties should be combined with 'AND'. If $test is set to + * 'anyof', it should be combined using 'OR'. + * + * This method should simply return an array with full principal uri's. + * + * If somebody attempted to search on a property the backend does not + * support, you should simply return 0 results. + * + * You can also just return 0 results if you choose to not support + * searching at all, but keep in mind that this may stop certain features + * from working. + * + * @param string $prefixPath + * @param string $test + * + * @return array + */ + public function searchPrincipals($prefixPath, array $searchProperties, $test = 'allof') + { + if (0 == count($searchProperties)) { + return []; + } //No criteria + + $query = 'SELECT uri FROM '.$this->tableName.' WHERE '; + $values = []; + foreach ($searchProperties as $property => $value) { + switch ($property) { + case '{DAV:}displayname': + $column = 'displayname'; + break; + case '{http://sabredav.org/ns}email-address': + $column = 'email'; + break; + default: + // Unsupported property + return []; + } + if (count($values) > 0) { + $query .= (0 == strcmp($test, 'anyof') ? ' OR ' : ' AND '); + } + $query .= 'lower('.$column.') LIKE lower(?)'; + $values[] = '%'.$value.'%'; + } + $stmt = $this->pdo->prepare($query); + $stmt->execute($values); + + $principals = []; + while ($row = $stmt->fetch(\PDO::FETCH_ASSOC)) { + // Checking if the principal is in the prefix + list($rowPrefix) = Uri\split($row['uri']); + if ($rowPrefix !== $prefixPath) { + continue; + } + + $principals[] = $row['uri']; + } + + return $principals; + } + + /** + * Finds a principal by its URI. + * + * This method may receive any type of uri, but mailto: addresses will be + * the most common. + * + * Implementation of this API is optional. It is currently used by the + * CalDAV system to find principals based on their email addresses. If this + * API is not implemented, some features may not work correctly. + * + * This method must return a relative principal path, or null, if the + * principal was not found or you refuse to find it. + * + * @param string $uri + * @param string $principalPrefix + * + * @return string + */ + public function findByUri($uri, $principalPrefix) + { + $uriParts = Uri\parse($uri); + + // Only two types of uri are supported : + // - the "mailto:" scheme with some non-empty address + // - a principals uri, in the form "principals/NAME" + // In both cases, `path` must not be empty. + if (empty($uriParts['path'])) { + return null; + } + + $uri = null; + if ('mailto' === $uriParts['scheme']) { + $query = 'SELECT uri FROM '.$this->tableName.' WHERE lower(email)=lower(?)'; + $stmt = $this->pdo->prepare($query); + $stmt->execute([$uriParts['path']]); + + while ($row = $stmt->fetch(\PDO::FETCH_ASSOC)) { + // Checking if the principal is in the prefix + list($rowPrefix) = Uri\split($row['uri']); + if ($rowPrefix !== $principalPrefix) { + continue; + } + + $uri = $row['uri']; + break; //Stop on first match + } + } else { + $pathParts = Uri\split($uriParts['path']); // We can do this since $uriParts['path'] is not null + + if (2 === count($pathParts) && $pathParts[0] === $principalPrefix) { + // Checking that this uri exists + $query = 'SELECT * FROM '.$this->tableName.' WHERE uri = ?'; + $stmt = $this->pdo->prepare($query); + $stmt->execute([$uriParts['path']]); + $rows = $stmt->fetchAll(); + + if (count($rows) > 0) { + $uri = $uriParts['path']; + } + } + } + + return $uri; + } + + /** + * Returns the list of members for a group-principal. + * + * @param string $principal + * + * @return array + */ + public function getGroupMemberSet($principal) + { + $principal = $this->getPrincipalByPath($principal); + if (!$principal) { + throw new DAV\Exception('Principal not found'); + } + $stmt = $this->pdo->prepare('SELECT principals.uri as uri FROM '.$this->groupMembersTableName.' AS groupmembers LEFT JOIN '.$this->tableName.' AS principals ON groupmembers.member_id = principals.id WHERE groupmembers.principal_id = ?'); + $stmt->execute([$principal['id']]); + + $result = []; + while ($row = $stmt->fetch(\PDO::FETCH_ASSOC)) { + $result[] = $row['uri']; + } + + return $result; + } + + /** + * Returns the list of groups a principal is a member of. + * + * @param string $principal + * + * @return array + */ + public function getGroupMembership($principal) + { + $principal = $this->getPrincipalByPath($principal); + if (!$principal) { + throw new DAV\Exception('Principal not found'); + } + $stmt = $this->pdo->prepare('SELECT principals.uri as uri FROM '.$this->groupMembersTableName.' AS groupmembers LEFT JOIN '.$this->tableName.' AS principals ON groupmembers.principal_id = principals.id WHERE groupmembers.member_id = ?'); + $stmt->execute([$principal['id']]); + + $result = []; + while ($row = $stmt->fetch(\PDO::FETCH_ASSOC)) { + $result[] = $row['uri']; + } + + return $result; + } + + /** + * Updates the list of group members for a group principal. + * + * The principals should be passed as a list of uri's. + * + * @param string $principal + */ + public function setGroupMemberSet($principal, array $members) + { + // Grabbing the list of principal id's. + $stmt = $this->pdo->prepare('SELECT id, uri FROM '.$this->tableName.' WHERE uri IN (? '.str_repeat(', ? ', count($members)).');'); + $stmt->execute(array_merge([$principal], $members)); + + $memberIds = []; + $principalId = null; + + while ($row = $stmt->fetch(\PDO::FETCH_ASSOC)) { + if ($row['uri'] == $principal) { + $principalId = $row['id']; + } else { + $memberIds[] = $row['id']; + } + } + if (!$principalId) { + throw new DAV\Exception('Principal not found'); + } + // Wiping out old members + $stmt = $this->pdo->prepare('DELETE FROM '.$this->groupMembersTableName.' WHERE principal_id = ?;'); + $stmt->execute([$principalId]); + + foreach ($memberIds as $memberId) { + $stmt = $this->pdo->prepare('INSERT INTO '.$this->groupMembersTableName.' (principal_id, member_id) VALUES (?, ?);'); + $stmt->execute([$principalId, $memberId]); + } + } + + /** + * Creates a new principal. + * + * This method receives a full path for the new principal. The mkCol object + * contains any additional webdav properties specified during the creation + * of the principal. + * + * @param string $path + */ + public function createPrincipal($path, MkCol $mkCol) + { + $stmt = $this->pdo->prepare('INSERT INTO '.$this->tableName.' (uri) VALUES (?)'); + $stmt->execute([$path]); + $this->updatePrincipal($path, $mkCol); + } +} diff --git a/3rdparty/sabre/dav/lib/DAVACL/PrincipalCollection.php b/3rdparty/sabre/dav/lib/DAVACL/PrincipalCollection.php new file mode 100644 index 00000000..b823b6ce --- /dev/null +++ b/3rdparty/sabre/dav/lib/DAVACL/PrincipalCollection.php @@ -0,0 +1,96 @@ +principalBackend, $principal); + } + + /** + * Creates a new collection. + * + * This method will receive a MkCol object with all the information about + * the new collection that's being created. + * + * The MkCol object contains information about the resourceType of the new + * collection. If you don't support the specified resourceType, you should + * throw Exception\InvalidResourceType. + * + * The object also contains a list of WebDAV properties for the new + * collection. + * + * You should call the handle() method on this object to specify exactly + * which properties you are storing. This allows the system to figure out + * exactly which properties you didn't store, which in turn allows other + * plugins (such as the propertystorage plugin) to handle storing the + * property for you. + * + * @param string $name + * + * @throws InvalidResourceType + */ + public function createExtendedCollection($name, MkCol $mkCol) + { + if (!$mkCol->hasResourceType('{DAV:}principal')) { + throw new InvalidResourceType('Only resources of type {DAV:}principal may be created here'); + } + + $this->principalBackend->createPrincipal( + $this->principalPrefix.'/'.$name, + $mkCol + ); + } + + /** + * Returns a list of ACE's for this node. + * + * Each ACE has the following properties: + * * 'privilege', a string such as {DAV:}read or {DAV:}write. These are + * currently the only supported privileges + * * 'principal', a url to the principal who owns the node + * * 'protected' (optional), indicating that this ACE is not allowed to + * be updated. + * + * @return array + */ + public function getACL() + { + return [ + [ + 'principal' => '{DAV:}authenticated', + 'privilege' => '{DAV:}read', + 'protected' => true, + ], + ]; + } +} diff --git a/3rdparty/sabre/dav/lib/DAVACL/Xml/Property/Acl.php b/3rdparty/sabre/dav/lib/DAVACL/Xml/Property/Acl.php new file mode 100644 index 00000000..c6e236dc --- /dev/null +++ b/3rdparty/sabre/dav/lib/DAVACL/Xml/Property/Acl.php @@ -0,0 +1,257 @@ +privileges = $privileges; + $this->prefixBaseUrl = $prefixBaseUrl; + } + + /** + * Returns the list of privileges for this property. + * + * @return array + */ + public function getPrivileges() + { + return $this->privileges; + } + + /** + * The xmlSerialize method is called during xml writing. + * + * Use the $writer argument to write its own xml serialization. + * + * An important note: do _not_ create a parent element. Any element + * implementing XmlSerializable should only ever write what's considered + * its 'inner xml'. + * + * The parent of the current element is responsible for writing a + * containing element. + * + * This allows serializers to be re-used for different element names. + * + * If you are opening new elements, you must also close them again. + */ + public function xmlSerialize(Writer $writer) + { + foreach ($this->privileges as $ace) { + $this->serializeAce($writer, $ace); + } + } + + /** + * Generate html representation for this value. + * + * The html output is 100% trusted, and no effort is being made to sanitize + * it. It's up to the implementor to sanitize user provided values. + * + * The output must be in UTF-8. + * + * The baseUri parameter is a url to the root of the application, and can + * be used to construct local links. + * + * @return string + */ + public function toHtml(HtmlOutputHelper $html) + { + ob_start(); + echo ''; + echo ''; + foreach ($this->privileges as $privilege) { + echo ''; + // if it starts with a {, it's a special principal + if ('{' === $privilege['principal'][0]) { + echo ''; + } else { + echo ''; + } + echo ''; + echo ''; + echo ''; + } + echo '
    PrincipalPrivilege
    ', $html->xmlName($privilege['principal']), '', $html->link($privilege['principal']), '', $html->xmlName($privilege['privilege']), ''; + if (!empty($privilege['protected'])) { + echo '(protected)'; + } + echo '
    '; + + return ob_get_clean(); + } + + /** + * The deserialize method is called during xml parsing. + * + * This method is called statically, this is because in theory this method + * may be used as a type of constructor, or factory method. + * + * Often you want to return an instance of the current class, but you are + * free to return other data as well. + * + * Important note 2: You are responsible for advancing the reader to the + * next element. Not doing anything will result in a never-ending loop. + * + * If you just want to skip parsing for this element altogether, you can + * just call $reader->next(); + * + * $reader->parseInnerTree() will parse the entire sub-tree, and advance to + * the next element. + * + * @return mixed + */ + public static function xmlDeserialize(Reader $reader) + { + $elementMap = [ + '{DAV:}ace' => 'Sabre\Xml\Element\KeyValue', + '{DAV:}privilege' => 'Sabre\Xml\Element\Elements', + '{DAV:}principal' => 'Sabre\DAVACL\Xml\Property\Principal', + ]; + + $privileges = []; + + foreach ((array) $reader->parseInnerTree($elementMap) as $element) { + if ('{DAV:}ace' !== $element['name']) { + continue; + } + $ace = $element['value']; + + if (empty($ace['{DAV:}principal'])) { + throw new DAV\Exception\BadRequest('Each {DAV:}ace element must have one {DAV:}principal element'); + } + $principal = $ace['{DAV:}principal']; + + switch ($principal->getType()) { + case Principal::HREF: + $principal = $principal->getHref(); + break; + case Principal::AUTHENTICATED: + $principal = '{DAV:}authenticated'; + break; + case Principal::UNAUTHENTICATED: + $principal = '{DAV:}unauthenticated'; + break; + case Principal::ALL: + $principal = '{DAV:}all'; + break; + } + + $protected = array_key_exists('{DAV:}protected', $ace); + + if (!isset($ace['{DAV:}grant'])) { + throw new DAV\Exception\NotImplemented('Every {DAV:}ace element must have a {DAV:}grant element. {DAV:}deny is not yet supported'); + } + foreach ($ace['{DAV:}grant'] as $elem) { + if ('{DAV:}privilege' !== $elem['name']) { + continue; + } + + foreach ($elem['value'] as $priv) { + $privileges[] = [ + 'principal' => $principal, + 'protected' => $protected, + 'privilege' => $priv, + ]; + } + } + } + + return new self($privileges); + } + + /** + * Serializes a single access control entry. + */ + private function serializeAce(Writer $writer, array $ace) + { + $writer->startElement('{DAV:}ace'); + + switch ($ace['principal']) { + case '{DAV:}authenticated': + $principal = new Principal(Principal::AUTHENTICATED); + break; + case '{DAV:}unauthenticated': + $principal = new Principal(Principal::UNAUTHENTICATED); + break; + case '{DAV:}all': + $principal = new Principal(Principal::ALL); + break; + default: + $principal = new Principal(Principal::HREF, $ace['principal']); + break; + } + + $writer->writeElement('{DAV:}principal', $principal); + $writer->startElement('{DAV:}grant'); + $writer->startElement('{DAV:}privilege'); + + $writer->writeElement($ace['privilege']); + + $writer->endElement(); // privilege + $writer->endElement(); // grant + + if (!empty($ace['protected'])) { + $writer->writeElement('{DAV:}protected'); + } + + $writer->endElement(); // ace + } +} diff --git a/3rdparty/sabre/dav/lib/DAVACL/Xml/Property/AclRestrictions.php b/3rdparty/sabre/dav/lib/DAVACL/Xml/Property/AclRestrictions.php new file mode 100644 index 00000000..b5629c80 --- /dev/null +++ b/3rdparty/sabre/dav/lib/DAVACL/Xml/Property/AclRestrictions.php @@ -0,0 +1,42 @@ +writeElement('{DAV:}grant-only'); + $writer->writeElement('{DAV:}no-invert'); + } +} diff --git a/3rdparty/sabre/dav/lib/DAVACL/Xml/Property/CurrentUserPrivilegeSet.php b/3rdparty/sabre/dav/lib/DAVACL/Xml/Property/CurrentUserPrivilegeSet.php new file mode 100644 index 00000000..e38a45c6 --- /dev/null +++ b/3rdparty/sabre/dav/lib/DAVACL/Xml/Property/CurrentUserPrivilegeSet.php @@ -0,0 +1,145 @@ +privileges = $privileges; + } + + /** + * The xmlSerialize method is called during xml writing. + * + * Use the $writer argument to write its own xml serialization. + * + * An important note: do _not_ create a parent element. Any element + * implementing XmlSerializable should only ever write what's considered + * its 'inner xml'. + * + * The parent of the current element is responsible for writing a + * containing element. + * + * This allows serializers to be re-used for different element names. + * + * If you are opening new elements, you must also close them again. + */ + public function xmlSerialize(Writer $writer) + { + foreach ($this->privileges as $privName) { + $writer->startElement('{DAV:}privilege'); + $writer->writeElement($privName); + $writer->endElement(); + } + } + + /** + * Returns true or false, whether the specified principal appears in the + * list. + * + * @param string $privilegeName + * + * @return bool + */ + public function has($privilegeName) + { + return in_array($privilegeName, $this->privileges); + } + + /** + * Returns the list of privileges. + * + * @return array + */ + public function getValue() + { + return $this->privileges; + } + + /** + * The deserialize method is called during xml parsing. + * + * This method is called statically, this is because in theory this method + * may be used as a type of constructor, or factory method. + * + * Often you want to return an instance of the current class, but you are + * free to return other data as well. + * + * You are responsible for advancing the reader to the next element. Not + * doing anything will result in a never-ending loop. + * + * If you just want to skip parsing for this element altogether, you can + * just call $reader->next(); + * + * $reader->parseInnerTree() will parse the entire sub-tree, and advance to + * the next element. + * + * @return mixed + */ + public static function xmlDeserialize(Reader $reader) + { + $result = []; + + $tree = $reader->parseInnerTree(['{DAV:}privilege' => 'Sabre\\Xml\\Element\\Elements']); + foreach ($tree as $element) { + if ('{DAV:}privilege' !== $element['name']) { + continue; + } + $result[] = $element['value'][0]; + } + + return new self($result); + } + + /** + * Generate html representation for this value. + * + * The html output is 100% trusted, and no effort is being made to sanitize + * it. It's up to the implementor to sanitize user provided values. + * + * The output must be in UTF-8. + * + * The baseUri parameter is a url to the root of the application, and can + * be used to construct local links. + * + * @return string + */ + public function toHtml(HtmlOutputHelper $html) + { + return implode( + ', ', + array_map([$html, 'xmlName'], $this->getValue()) + ); + } +} diff --git a/3rdparty/sabre/dav/lib/DAVACL/Xml/Property/Principal.php b/3rdparty/sabre/dav/lib/DAVACL/Xml/Property/Principal.php new file mode 100644 index 00000000..5b9ee451 --- /dev/null +++ b/3rdparty/sabre/dav/lib/DAVACL/Xml/Property/Principal.php @@ -0,0 +1,186 @@ +type = $type; + if (self::HREF === $type && is_null($href)) { + throw new DAV\Exception('The href argument must be specified for the HREF principal type.'); + } + if ($href) { + $href = rtrim($href, '/').'/'; + parent::__construct($href); + } + } + + /** + * Returns the principal type. + * + * @return int + */ + public function getType() + { + return $this->type; + } + + /** + * The xmlSerialize method is called during xml writing. + * + * Use the $writer argument to write its own xml serialization. + * + * An important note: do _not_ create a parent element. Any element + * implementing XmlSerializable should only ever write what's considered + * its 'inner xml'. + * + * The parent of the current element is responsible for writing a + * containing element. + * + * This allows serializers to be re-used for different element names. + * + * If you are opening new elements, you must also close them again. + */ + public function xmlSerialize(Writer $writer) + { + switch ($this->type) { + case self::UNAUTHENTICATED: + $writer->writeElement('{DAV:}unauthenticated'); + break; + case self::AUTHENTICATED: + $writer->writeElement('{DAV:}authenticated'); + break; + case self::HREF: + parent::xmlSerialize($writer); + break; + case self::ALL: + $writer->writeElement('{DAV:}all'); + break; + } + } + + /** + * Generate html representation for this value. + * + * The html output is 100% trusted, and no effort is being made to sanitize + * it. It's up to the implementor to sanitize user provided values. + * + * The output must be in UTF-8. + * + * The baseUri parameter is a url to the root of the application, and can + * be used to construct local links. + * + * @return string + */ + public function toHtml(HtmlOutputHelper $html) + { + switch ($this->type) { + case self::UNAUTHENTICATED: + return 'unauthenticated'; + case self::AUTHENTICATED: + return 'authenticated'; + case self::HREF: + return parent::toHtml($html); + case self::ALL: + return 'all'; + } + + return 'unknown'; + } + + /** + * The deserialize method is called during xml parsing. + * + * This method is called statically, this is because in theory this method + * may be used as a type of constructor, or factory method. + * + * Often you want to return an instance of the current class, but you are + * free to return other data as well. + * + * Important note 2: You are responsible for advancing the reader to the + * next element. Not doing anything will result in a never-ending loop. + * + * If you just want to skip parsing for this element altogether, you can + * just call $reader->next(); + * + * $reader->parseInnerTree() will parse the entire sub-tree, and advance to + * the next element. + * + * @return mixed + */ + public static function xmlDeserialize(Reader $reader) + { + $tree = $reader->parseInnerTree()[0]; + + switch ($tree['name']) { + case '{DAV:}unauthenticated': + return new self(self::UNAUTHENTICATED); + case '{DAV:}authenticated': + return new self(self::AUTHENTICATED); + case '{DAV:}href': + return new self(self::HREF, $tree['value']); + case '{DAV:}all': + return new self(self::ALL); + default: + throw new BadRequest('Unknown or unsupported principal type: '.$tree['name']); + } + } +} diff --git a/3rdparty/sabre/dav/lib/DAVACL/Xml/Property/SupportedPrivilegeSet.php b/3rdparty/sabre/dav/lib/DAVACL/Xml/Property/SupportedPrivilegeSet.php new file mode 100644 index 00000000..6e7514bd --- /dev/null +++ b/3rdparty/sabre/dav/lib/DAVACL/Xml/Property/SupportedPrivilegeSet.php @@ -0,0 +1,146 @@ +privileges = $privileges; + } + + /** + * Returns the privilege value. + * + * @return array + */ + public function getValue() + { + return $this->privileges; + } + + /** + * The xmlSerialize method is called during xml writing. + * + * Use the $writer argument to write its own xml serialization. + * + * An important note: do _not_ create a parent element. Any element + * implementing XmlSerializable should only ever write what's considered + * its 'inner xml'. + * + * The parent of the current element is responsible for writing a + * containing element. + * + * This allows serializers to be re-used for different element names. + * + * If you are opening new elements, you must also close them again. + */ + public function xmlSerialize(Writer $writer) + { + $this->serializePriv($writer, '{DAV:}all', ['aggregates' => $this->privileges]); + } + + /** + * Generate html representation for this value. + * + * The html output is 100% trusted, and no effort is being made to sanitize + * it. It's up to the implementor to sanitize user provided values. + * + * The output must be in UTF-8. + * + * The baseUri parameter is a url to the root of the application, and can + * be used to construct local links. + * + * @return string + */ + public function toHtml(HtmlOutputHelper $html) + { + $traverse = function ($privName, $priv) use (&$traverse, $html) { + echo '
  • '; + echo $html->xmlName($privName); + if (isset($priv['abstract']) && $priv['abstract']) { + echo ' (abstract)'; + } + if (isset($priv['description'])) { + echo ' '.$html->h($priv['description']); + } + if (isset($priv['aggregates'])) { + echo "\n
      \n"; + foreach ($priv['aggregates'] as $subPrivName => $subPriv) { + $traverse($subPrivName, $subPriv); + } + echo '
    '; + } + echo "
  • \n"; + }; + + ob_start(); + echo '
      '; + $traverse('{DAV:}all', ['aggregates' => $this->getValue()]); + echo "
    \n"; + + return ob_get_clean(); + } + + /** + * Serializes a property. + * + * This is a recursive function. + * + * @param string $privName + * @param array $privilege + */ + private function serializePriv(Writer $writer, $privName, $privilege) + { + $writer->startElement('{DAV:}supported-privilege'); + + $writer->startElement('{DAV:}privilege'); + $writer->writeElement($privName); + $writer->endElement(); // privilege + + if (!empty($privilege['abstract'])) { + $writer->writeElement('{DAV:}abstract'); + } + if (!empty($privilege['description'])) { + $writer->writeElement('{DAV:}description', $privilege['description']); + } + if (isset($privilege['aggregates'])) { + foreach ($privilege['aggregates'] as $subPrivName => $subPrivilege) { + $this->serializePriv($writer, $subPrivName, $subPrivilege); + } + } + + $writer->endElement(); // supported-privilege + } +} diff --git a/3rdparty/sabre/dav/lib/DAVACL/Xml/Request/AclPrincipalPropSetReport.php b/3rdparty/sabre/dav/lib/DAVACL/Xml/Request/AclPrincipalPropSetReport.php new file mode 100644 index 00000000..4fc61273 --- /dev/null +++ b/3rdparty/sabre/dav/lib/DAVACL/Xml/Request/AclPrincipalPropSetReport.php @@ -0,0 +1,66 @@ +next(); + * + * $reader->parseInnerTree() will parse the entire sub-tree, and advance to + * the next element. + * + * @return mixed + */ + public static function xmlDeserialize(Reader $reader) + { + $reader->pushContext(); + $reader->elementMap['{DAV:}prop'] = 'Sabre\Xml\Deserializer\enum'; + + $elems = Deserializer\keyValue( + $reader, + 'DAV:' + ); + + $reader->popContext(); + + $report = new self(); + + if (!empty($elems['prop'])) { + $report->properties = $elems['prop']; + } + + return $report; + } +} diff --git a/3rdparty/sabre/dav/lib/DAVACL/Xml/Request/ExpandPropertyReport.php b/3rdparty/sabre/dav/lib/DAVACL/Xml/Request/ExpandPropertyReport.php new file mode 100644 index 00000000..70a7e220 --- /dev/null +++ b/3rdparty/sabre/dav/lib/DAVACL/Xml/Request/ExpandPropertyReport.php @@ -0,0 +1,100 @@ +next(); + * + * $reader->parseInnerTree() will parse the entire sub-tree, and advance to + * the next element. + * + * @return mixed + */ + public static function xmlDeserialize(Reader $reader) + { + $elems = $reader->parseInnerTree(); + + $obj = new self(); + $obj->properties = self::traverse($elems); + + return $obj; + } + + /** + * This method is used by deserializeXml, to recursively parse the + * {DAV:}property elements. + * + * @param array $elems + * + * @return array + */ + private static function traverse($elems) + { + $result = []; + + foreach ($elems as $elem) { + if ('{DAV:}property' !== $elem['name']) { + continue; + } + + $namespace = isset($elem['attributes']['namespace']) ? + $elem['attributes']['namespace'] : + 'DAV:'; + + $propName = '{'.$namespace.'}'.$elem['attributes']['name']; + + $value = null; + if (is_array($elem['value'])) { + $value = self::traverse($elem['value']); + } + + $result[$propName] = $value; + } + + return $result; + } +} diff --git a/3rdparty/sabre/dav/lib/DAVACL/Xml/Request/PrincipalMatchReport.php b/3rdparty/sabre/dav/lib/DAVACL/Xml/Request/PrincipalMatchReport.php new file mode 100644 index 00000000..b4958245 --- /dev/null +++ b/3rdparty/sabre/dav/lib/DAVACL/Xml/Request/PrincipalMatchReport.php @@ -0,0 +1,106 @@ +next(); + * + * $reader->parseInnerTree() will parse the entire sub-tree, and advance to + * the next element. + * + * @return mixed + */ + public static function xmlDeserialize(Reader $reader) + { + $reader->pushContext(); + $reader->elementMap['{DAV:}prop'] = 'Sabre\Xml\Deserializer\enum'; + + $elems = Deserializer\keyValue( + $reader, + 'DAV:' + ); + + $reader->popContext(); + + $principalMatch = new self(); + + if (array_key_exists('self', $elems)) { + $principalMatch->type = self::SELF; + } + + if (array_key_exists('principal-property', $elems)) { + $principalMatch->type = self::PRINCIPAL_PROPERTY; + $principalMatch->principalProperty = $elems['principal-property'][0]['name']; + } + + if (!empty($elems['prop'])) { + $principalMatch->properties = $elems['prop']; + } + + return $principalMatch; + } +} diff --git a/3rdparty/sabre/dav/lib/DAVACL/Xml/Request/PrincipalPropertySearchReport.php b/3rdparty/sabre/dav/lib/DAVACL/Xml/Request/PrincipalPropertySearchReport.php new file mode 100644 index 00000000..bddceca8 --- /dev/null +++ b/3rdparty/sabre/dav/lib/DAVACL/Xml/Request/PrincipalPropertySearchReport.php @@ -0,0 +1,122 @@ +next(); + * + * $reader->parseInnerTree() will parse the entire sub-tree, and advance to + * the next element. + * + * @return mixed + */ + public static function xmlDeserialize(Reader $reader) + { + $self = new self(); + + $foundSearchProp = false; + $self->test = 'allof'; + if ('anyof' === $reader->getAttribute('test')) { + $self->test = 'anyof'; + } + + $elemMap = [ + '{DAV:}property-search' => 'Sabre\\Xml\\Element\\KeyValue', + '{DAV:}prop' => 'Sabre\\Xml\\Element\\KeyValue', + ]; + + foreach ($reader->parseInnerTree($elemMap) as $elem) { + switch ($elem['name']) { + case '{DAV:}prop': + $self->properties = array_keys($elem['value']); + break; + case '{DAV:}property-search': + $foundSearchProp = true; + // This property has two sub-elements: + // {DAV:}prop - The property to be searched on. This may + // also be more than one + // {DAV:}match - The value to match with + if (!isset($elem['value']['{DAV:}prop']) || !isset($elem['value']['{DAV:}match'])) { + throw new BadRequest('The {DAV:}property-search element must contain one {DAV:}match and one {DAV:}prop element'); + } + foreach ($elem['value']['{DAV:}prop'] as $propName => $discard) { + $self->searchProperties[$propName] = $elem['value']['{DAV:}match']; + } + break; + case '{DAV:}apply-to-principal-collection-set': + $self->applyToPrincipalCollectionSet = true; + break; + } + } + if (!$foundSearchProp) { + throw new BadRequest('The {DAV:}principal-property-search report must contain at least 1 {DAV:}property-search element'); + } + + return $self; + } +} diff --git a/3rdparty/sabre/dav/lib/DAVACL/Xml/Request/PrincipalSearchPropertySetReport.php b/3rdparty/sabre/dav/lib/DAVACL/Xml/Request/PrincipalSearchPropertySetReport.php new file mode 100644 index 00000000..7f15d8a4 --- /dev/null +++ b/3rdparty/sabre/dav/lib/DAVACL/Xml/Request/PrincipalSearchPropertySetReport.php @@ -0,0 +1,58 @@ +next(); + * + * $reader->parseInnerTree() will parse the entire sub-tree, and advance to + * the next element. + * + * @return mixed + */ + public static function xmlDeserialize(Reader $reader) + { + if (!$reader->isEmptyElement) { + throw new BadRequest('The {DAV:}principal-search-property-set element must be empty'); + } + + // The element is actually empty, so there's not much to do. + $reader->next(); + + $self = new self(); + + return $self; + } +} diff --git a/3rdparty/sabre/event/LICENSE b/3rdparty/sabre/event/LICENSE new file mode 100644 index 00000000..962a4926 --- /dev/null +++ b/3rdparty/sabre/event/LICENSE @@ -0,0 +1,27 @@ +Copyright (C) 2013-2016 fruux GmbH (https://fruux.com/) + +All rights reserved. + +Redistribution and use in source and binary forms, with or without modification, +are permitted provided that the following conditions are met: + + * Redistributions of source code must retain the above copyright notice, + this list of conditions and the following disclaimer. + * Redistributions in binary form must reproduce the above copyright notice, + this list of conditions and the following disclaimer in the documentation + and/or other materials provided with the distribution. + * Neither the name Sabre nor the names of its contributors + may be used to endorse or promote products derived from this software + without specific prior written permission. + + THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" + AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE + IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE + ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE + LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR + CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF + SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS + INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN + CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) + ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE + POSSIBILITY OF SUCH DAMAGE. diff --git a/3rdparty/sabre/event/lib/Emitter.php b/3rdparty/sabre/event/lib/Emitter.php new file mode 100644 index 00000000..e1f23fc8 --- /dev/null +++ b/3rdparty/sabre/event/lib/Emitter.php @@ -0,0 +1,19 @@ +listeners[$eventName])) { + $this->listeners[$eventName] = [ + true, // If there's only one item, it's sorted + [$priority], + [$callBack], + ]; + } else { + $this->listeners[$eventName][0] = false; // marked as unsorted + $this->listeners[$eventName][1][] = $priority; + $this->listeners[$eventName][2][] = $callBack; + } + } + + /** + * Subscribe to an event exactly once. + */ + public function once(string $eventName, callable $callBack, int $priority = 100) + { + $wrapper = null; + $wrapper = function () use ($eventName, $callBack, &$wrapper) { + $this->removeListener($eventName, $wrapper); + + return \call_user_func_array($callBack, \func_get_args()); + }; + + $this->on($eventName, $wrapper, $priority); + } + + /** + * Emits an event. + * + * This method will return true if 0 or more listeners were successfully + * handled. false is returned if one of the events broke the event chain. + * + * If the continueCallBack is specified, this callback will be called every + * time before the next event handler is called. + * + * If the continueCallback returns false, event propagation stops. This + * allows you to use the eventEmitter as a means for listeners to implement + * functionality in your application, and break the event loop as soon as + * some condition is fulfilled. + * + * Note that returning false from an event subscriber breaks propagation + * and returns false, but if the continue-callback stops propagation, this + * is still considered a 'successful' operation and returns true. + * + * Lastly, if there are 5 event handlers for an event. The continueCallback + * will be called at most 4 times. + */ + public function emit(string $eventName, array $arguments = [], ?callable $continueCallBack = null): bool + { + if (\is_null($continueCallBack)) { + foreach ($this->listeners($eventName) as $listener) { + $result = \call_user_func_array($listener, $arguments); + if (false === $result) { + return false; + } + } + } else { + $listeners = $this->listeners($eventName); + $counter = \count($listeners); + + foreach ($listeners as $listener) { + --$counter; + $result = \call_user_func_array($listener, $arguments); + if (false === $result) { + return false; + } + + if ($counter > 0) { + if (!$continueCallBack()) { + break; + } + } + } + } + + return true; + } + + /** + * Returns the list of listeners for an event. + * + * The list is returned as an array, and the list of events are sorted by + * their priority. + * + * @return callable[] + */ + public function listeners(string $eventName): array + { + if (!isset($this->listeners[$eventName])) { + return []; + } + + // The list is not sorted + if (!$this->listeners[$eventName][0]) { + // Sorting + \array_multisort($this->listeners[$eventName][1], SORT_NUMERIC, $this->listeners[$eventName][2]); + + // Marking the listeners as sorted + $this->listeners[$eventName][0] = true; + } + + return $this->listeners[$eventName][2]; + } + + /** + * Removes a specific listener from an event. + * + * If the listener could not be found, this method will return false. If it + * was removed it will return true. + */ + public function removeListener(string $eventName, callable $listener): bool + { + if (!isset($this->listeners[$eventName])) { + return false; + } + foreach ($this->listeners[$eventName][2] as $index => $check) { + if ($check === $listener) { + unset($this->listeners[$eventName][1][$index]); + unset($this->listeners[$eventName][2][$index]); + + return true; + } + } + + return false; + } + + /** + * Removes all listeners. + * + * If the eventName argument is specified, all listeners for that event are + * removed. If it is not specified, every listener for every event is + * removed. + */ + public function removeAllListeners(?string $eventName = null) + { + if (!\is_null($eventName)) { + unset($this->listeners[$eventName]); + } else { + $this->listeners = []; + } + } + + /** + * The list of listeners. + * + * @var array + */ + protected $listeners = []; +} diff --git a/3rdparty/sabre/event/lib/EventEmitter.php b/3rdparty/sabre/event/lib/EventEmitter.php new file mode 100644 index 00000000..865c99b5 --- /dev/null +++ b/3rdparty/sabre/event/lib/EventEmitter.php @@ -0,0 +1,20 @@ +timers) { + // Special case when the timers array was empty. + $this->timers[] = [$triggerTime, $cb]; + + return; + } + + // We need to insert these values in the timers array, but the timers + // array must be in reverse-order of trigger times. + // + // So here we search the array for the insertion point. + $index = count($this->timers) - 1; + while (true) { + if ($triggerTime < $this->timers[$index][0]) { + array_splice( + $this->timers, + $index + 1, + 0, + [[$triggerTime, $cb]] + ); + break; + } elseif (0 === $index) { + array_unshift($this->timers, [$triggerTime, $cb]); + break; + } + --$index; + } + } + + /** + * Executes a function every x seconds. + * + * The value this function returns can be used to stop the interval with + * clearInterval. + */ + public function setInterval(callable $cb, float $timeout): array + { + $keepGoing = true; + $f = null; + + $f = function () use ($cb, &$f, $timeout, &$keepGoing) { + if ($keepGoing) { + $cb(); + $this->setTimeout($f, $timeout); + } + }; + $this->setTimeout($f, $timeout); + + // Really the only thing that matters is returning the $keepGoing + // boolean value. + // + // We need to pack it in an array to allow returning by reference. + // Because I'm worried people will be confused by using a boolean as a + // sort of identifier, I added an extra string. + return ['I\'m an implementation detail', &$keepGoing]; + } + + /** + * Stops a running interval. + */ + public function clearInterval(array $intervalId) + { + $intervalId[1] = false; + } + + /** + * Runs a function immediately at the next iteration of the loop. + */ + public function nextTick(callable $cb) + { + $this->nextTick[] = $cb; + } + + /** + * Adds a read stream. + * + * The callback will be called as soon as there is something to read from + * the stream. + * + * You MUST call removeReadStream after you are done with the stream, to + * prevent the eventloop from never stopping. + * + * @param resource $stream + */ + public function addReadStream($stream, callable $cb) + { + $this->readStreams[(int) $stream] = $stream; + $this->readCallbacks[(int) $stream] = $cb; + } + + /** + * Adds a write stream. + * + * The callback will be called as soon as the system reports it's ready to + * receive writes on the stream. + * + * You MUST call removeWriteStream after you are done with the stream, to + * prevent the eventloop from never stopping. + * + * @param resource $stream + */ + public function addWriteStream($stream, callable $cb) + { + $this->writeStreams[(int) $stream] = $stream; + $this->writeCallbacks[(int) $stream] = $cb; + } + + /** + * Stop watching a stream for reads. + * + * @param resource $stream + */ + public function removeReadStream($stream) + { + unset( + $this->readStreams[(int) $stream], + $this->readCallbacks[(int) $stream] + ); + } + + /** + * Stop watching a stream for writes. + * + * @param resource $stream + */ + public function removeWriteStream($stream) + { + unset( + $this->writeStreams[(int) $stream], + $this->writeCallbacks[(int) $stream] + ); + } + + /** + * Runs the loop. + * + * This function will run continuously, until there's no more events to + * handle. + */ + public function run() + { + $this->running = true; + + do { + $hasEvents = $this->tick(true); + } while ($this->running && $hasEvents); + $this->running = false; + } + + /** + * Executes all pending events. + * + * If $block is turned true, this function will block until any event is + * triggered. + * + * If there are now timeouts, nextTick callbacks or events in the loop at + * all, this function will exit immediately. + * + * This function will return true if there are _any_ events left in the + * loop after the tick. + */ + public function tick(bool $block = false): bool + { + $this->runNextTicks(); + $nextTimeout = $this->runTimers(); + + // Calculating how long runStreams should at most wait. + if (!$block) { + // Don't wait + $streamWait = 0; + } elseif ($this->nextTick) { + // There's a pending 'nextTick'. Don't wait. + $streamWait = 0; + } elseif (is_numeric($nextTimeout)) { + // Wait until the next Timeout should trigger. + $streamWait = $nextTimeout; + } else { + // Wait indefinitely + $streamWait = null; + } + + $this->runStreams($streamWait); + + return $this->readStreams || $this->writeStreams || $this->nextTick || $this->timers; + } + + /** + * Stops a running eventloop. + */ + public function stop() + { + $this->running = false; + } + + /** + * Executes all 'nextTick' callbacks. + * + * return void + */ + protected function runNextTicks() + { + $nextTick = $this->nextTick; + $this->nextTick = []; + + foreach ($nextTick as $cb) { + $cb(); + } + } + + /** + * Runs all pending timers. + * + * After running the timer callbacks, this function returns the number of + * seconds until the next timer should be executed. + * + * If there's no more pending timers, this function returns null. + * + * @return float|null + */ + protected function runTimers() + { + $now = microtime(true); + while (($timer = array_pop($this->timers)) && $timer[0] < $now) { + $timer[1](); + } + // Add the last timer back to the array. + if ($timer) { + $this->timers[] = $timer; + + return max(0, $timer[0] - microtime(true)); + } + } + + /** + * Runs all pending stream events. + * + * If $timeout is 0, it will return immediately. If $timeout is null, it + * will wait indefinitely. + * + * @param float|null $timeout + */ + protected function runStreams($timeout) + { + if ($this->readStreams || $this->writeStreams) { + $read = $this->readStreams; + $write = $this->writeStreams; + $except = null; + // stream_select changes behavior in 8.1 to forbid passing non-null microseconds when the seconds are null. + // Older versions of php don't allow passing null to microseconds. + if (null !== $timeout ? stream_select($read, $write, $except, 0, (int) ($timeout * 1000000)) : stream_select($read, $write, $except, null)) { + // See PHP Bug https://bugs.php.net/bug.php?id=62452 + // Fixed in PHP7 + foreach ($read as $readStream) { + $readCb = $this->readCallbacks[(int) $readStream]; + $readCb(); + } + foreach ($write as $writeStream) { + $writeCb = $this->writeCallbacks[(int) $writeStream]; + $writeCb(); + } + } + } elseif ($this->running && ($this->nextTick || $this->timers)) { + usleep(null !== $timeout ? intval($timeout * 1000000) : 200000); + } + } + + /** + * Is the main loop active. + * + * @var bool + */ + protected $running = false; + + /** + * A list of timers, added by setTimeout. + * + * @var array + */ + protected $timers = []; + + /** + * A list of 'nextTick' callbacks. + * + * @var callable[] + */ + protected $nextTick = []; + + /** + * List of readable streams for stream_select, indexed by stream id. + * + * @var resource[] + */ + protected $readStreams = []; + + /** + * List of writable streams for stream_select, indexed by stream id. + * + * @var resource[] + */ + protected $writeStreams = []; + + /** + * List of read callbacks, indexed by stream id. + * + * @var callable[] + */ + protected $readCallbacks = []; + + /** + * List of write callbacks, indexed by stream id. + * + * @var callable[] + */ + protected $writeCallbacks = []; +} diff --git a/3rdparty/sabre/event/lib/Loop/functions.php b/3rdparty/sabre/event/lib/Loop/functions.php new file mode 100644 index 00000000..9412a77f --- /dev/null +++ b/3rdparty/sabre/event/lib/Loop/functions.php @@ -0,0 +1,143 @@ +setTimeout($cb, $timeout); +} + +/** + * Executes a function every x seconds. + * + * The value this function returns can be used to stop the interval with + * clearInterval. + */ +function setInterval(callable $cb, float $timeout): array +{ + return instance()->setInterval($cb, $timeout); +} + +/** + * Stops a running interval. + */ +function clearInterval(array $intervalId) +{ + instance()->clearInterval($intervalId); +} + +/** + * Runs a function immediately at the next iteration of the loop. + */ +function nextTick(callable $cb) +{ + instance()->nextTick($cb); +} + +/** + * Adds a read stream. + * + * The callback will be called as soon as there is something to read from + * the stream. + * + * You MUST call removeReadStream after you are done with the stream, to + * prevent the eventloop from never stopping. + * + * @param resource $stream + */ +function addReadStream($stream, callable $cb) +{ + instance()->addReadStream($stream, $cb); +} + +/** + * Adds a write stream. + * + * The callback will be called as soon as the system reports it's ready to + * receive writes on the stream. + * + * You MUST call removeWriteStream after you are done with the stream, to + * prevent the eventloop from never stopping. + * + * @param resource $stream + */ +function addWriteStream($stream, callable $cb) +{ + instance()->addWriteStream($stream, $cb); +} + +/** + * Stop watching a stream for reads. + * + * @param resource $stream + */ +function removeReadStream($stream) +{ + instance()->removeReadStream($stream); +} + +/** + * Stop watching a stream for writes. + * + * @param resource $stream + */ +function removeWriteStream($stream) +{ + instance()->removeWriteStream($stream); +} + +/** + * Runs the loop. + * + * This function will run continuously, until there's no more events to + * handle. + */ +function run() +{ + instance()->run(); +} + +/** + * Executes all pending events. + * + * If $block is turned true, this function will block until any event is + * triggered. + * + * If there are now timeouts, nextTick callbacks or events in the loop at + * all, this function will exit immediately. + * + * This function will return true if there are _any_ events left in the + * loop after the tick. + */ +function tick(bool $block = false): bool +{ + return instance()->tick($block); +} + +/** + * Stops a running eventloop. + */ +function stop() +{ + instance()->stop(); +} + +/** + * Retrieves or sets the global Loop object. + */ +function instance(?Loop $newLoop = null): Loop +{ + static $loop; + if ($newLoop) { + $loop = $newLoop; + } elseif (!$loop) { + $loop = new Loop(); + } + + return $loop; +} diff --git a/3rdparty/sabre/event/lib/Promise.php b/3rdparty/sabre/event/lib/Promise.php new file mode 100644 index 00000000..66903fb9 --- /dev/null +++ b/3rdparty/sabre/event/lib/Promise.php @@ -0,0 +1,253 @@ +fulfill and $this->reject. + * Using the executor is optional. + */ + public function __construct(?callable $executor = null) + { + if ($executor) { + $executor( + [$this, 'fulfill'], + [$this, 'reject'] + ); + } + } + + /** + * This method allows you to specify the callback that will be called after + * the promise has been fulfilled or rejected. + * + * Both arguments are optional. + * + * This method returns a new promise, which can be used for chaining. + * If either the onFulfilled or onRejected callback is called, you may + * return a result from this callback. + * + * If the result of this callback is yet another promise, the result of + * _that_ promise will be used to set the result of the returned promise. + * + * If either of the callbacks return any other value, the returned promise + * is automatically fulfilled with that value. + * + * If either of the callbacks throw an exception, the returned promise will + * be rejected and the exception will be passed back. + */ + public function then(?callable $onFulfilled = null, ?callable $onRejected = null): Promise + { + // This new subPromise will be returned from this function, and will + // be fulfilled with the result of the onFulfilled or onRejected event + // handlers. + $subPromise = new self(); + + switch ($this->state) { + case self::PENDING: + // The operation is pending, so we keep a reference to the + // event handlers so we can call them later. + $this->subscribers[] = [$subPromise, $onFulfilled, $onRejected]; + break; + case self::FULFILLED: + // The async operation is already fulfilled, so we trigger the + // onFulfilled callback asap. + $this->invokeCallback($subPromise, $onFulfilled); + break; + case self::REJECTED: + // The async operation failed, so we call the onRejected + // callback asap. + $this->invokeCallback($subPromise, $onRejected); + break; + } + + return $subPromise; + } + + /** + * Add a callback for when this promise is rejected. + * + * Its usage is identical to then(). However, the otherwise() function is + * preferred. + */ + public function otherwise(callable $onRejected): Promise + { + return $this->then(null, $onRejected); + } + + /** + * Marks this promise as fulfilled and sets its return value. + */ + public function fulfill($value = null) + { + if (self::PENDING !== $this->state) { + throw new PromiseAlreadyResolvedException('This promise is already resolved, and you\'re not allowed to resolve a promise more than once'); + } + $this->state = self::FULFILLED; + $this->value = $value; + foreach ($this->subscribers as $subscriber) { + $this->invokeCallback($subscriber[0], $subscriber[1]); + } + } + + /** + * Marks this promise as rejected, and set its rejection reason. + */ + public function reject(\Throwable $reason) + { + if (self::PENDING !== $this->state) { + throw new PromiseAlreadyResolvedException('This promise is already resolved, and you\'re not allowed to resolve a promise more than once'); + } + $this->state = self::REJECTED; + $this->value = $reason; + foreach ($this->subscribers as $subscriber) { + $this->invokeCallback($subscriber[0], $subscriber[2]); + } + } + + /** + * Stops execution until this promise is resolved. + * + * This method stops execution completely. If the promise is successful with + * a value, this method will return this value. If the promise was + * rejected, this method will throw an exception. + * + * This effectively turns the asynchronous operation into a synchronous + * one. In PHP it might be useful to call this on the last promise in a + * chain. + * + * @psalm-return TReturn + */ + public function wait() + { + $hasEvents = true; + while (self::PENDING === $this->state) { + if (!$hasEvents) { + throw new \LogicException('There were no more events in the loop. This promise will never be fulfilled.'); + } + + // As long as the promise is not fulfilled, we tell the event loop + // to handle events, and to block. + $hasEvents = Loop\tick(true); + } + + if (self::FULFILLED === $this->state) { + // If the state of this promise is fulfilled, we can return the value. + return $this->value; + } else { + // If we got here, it means that the asynchronous operation + // errored. Therefore we need to throw an exception. + throw $this->value; + } + } + + /** + * A list of subscribers. Subscribers are the callbacks that want us to let + * them know if the callback was fulfilled or rejected. + * + * @var array + */ + protected $subscribers = []; + + /** + * The result of the promise. + * + * If the promise was fulfilled, this will be the result value. If the + * promise was rejected, this property hold the rejection reason. + */ + protected $value; + + /** + * This method is used to call either an onFulfilled or onRejected callback. + * + * This method makes sure that the result of these callbacks are handled + * correctly, and any chained promises are also correctly fulfilled or + * rejected. + */ + private function invokeCallback(Promise $subPromise, ?callable $callBack = null) + { + // We use 'nextTick' to ensure that the event handlers are always + // triggered outside of the calling stack in which they were originally + // passed to 'then'. + // + // This makes the order of execution more predictable. + Loop\nextTick(function () use ($callBack, $subPromise) { + if (is_callable($callBack)) { + try { + $result = $callBack($this->value); + if ($result instanceof self) { + // If the callback (onRejected or onFulfilled) + // returned a promise, we only fulfill or reject the + // chained promise once that promise has also been + // resolved. + $result->then([$subPromise, 'fulfill'], [$subPromise, 'reject']); + } else { + // If the callback returned any other value, we + // immediately fulfill the chained promise. + $subPromise->fulfill($result); + } + } catch (\Throwable $e) { + // If the event handler threw an exception, we need to make sure that + // the chained promise is rejected as well. + $subPromise->reject($e); + } + } else { + if (self::FULFILLED === $this->state) { + $subPromise->fulfill($this->value); + } else { + $subPromise->reject($this->value); + } + } + }); + } +} diff --git a/3rdparty/sabre/event/lib/Promise/functions.php b/3rdparty/sabre/event/lib/Promise/functions.php new file mode 100644 index 00000000..67e80cbe --- /dev/null +++ b/3rdparty/sabre/event/lib/Promise/functions.php @@ -0,0 +1,125 @@ + $subPromise) { + $subPromise->then( + function ($result) use ($promiseIndex, &$completeResult, &$successCount, $success, $promises) { + $completeResult[$promiseIndex] = $result; + ++$successCount; + if ($successCount === count($promises)) { + $success($completeResult); + } + + return $result; + } + )->otherwise( + function ($reason) use ($fail) { + $fail($reason); + } + ); + } + }); +} + +/** + * The race function returns a promise that resolves or rejects as soon as + * one of the promises in the argument resolves or rejects. + * + * The returned promise will resolve or reject with the value or reason of + * that first promise. + * + * @param Promise[] $promises + */ +function race(array $promises): Promise +{ + return new Promise(function ($success, $fail) use ($promises) { + $alreadyDone = false; + foreach ($promises as $promise) { + $promise->then( + function ($result) use ($success, &$alreadyDone) { + if ($alreadyDone) { + return; + } + $alreadyDone = true; + $success($result); + }, + function ($reason) use ($fail, &$alreadyDone) { + if ($alreadyDone) { + return; + } + $alreadyDone = true; + $fail($reason); + } + ); + } + }); +} + +/** + * Returns a Promise that resolves with the given value. + * + * If the value is a promise, the returned promise will attach itself to that + * promise and eventually get the same state as the followed promise. + */ +function resolve($value): Promise +{ + if ($value instanceof Promise) { + return $value->then(); + } else { + $promise = new Promise(); + $promise->fulfill($value); + + return $promise; + } +} + +/** + * Returns a Promise that will reject with the given reason. + */ +function reject(\Throwable $reason): Promise +{ + $promise = new Promise(); + $promise->reject($reason); + + return $promise; +} diff --git a/3rdparty/sabre/event/lib/PromiseAlreadyResolvedException.php b/3rdparty/sabre/event/lib/PromiseAlreadyResolvedException.php new file mode 100644 index 00000000..abb6c108 --- /dev/null +++ b/3rdparty/sabre/event/lib/PromiseAlreadyResolvedException.php @@ -0,0 +1,17 @@ +wildcardListeners; + } else { + $listeners = &$this->listeners; + } + + // Always fully reset the listener index. This is fairly sane for most + // applications, because there's a clear "event registering" and "event + // emitting" phase, but can be slow if there's a lot adding and removing + // of listeners during emitting of events. + $this->listenerIndex = []; + + if (!isset($listeners[$eventName])) { + $listeners[$eventName] = []; + } + $listeners[$eventName][] = [$priority, $callBack]; + } + + /** + * Subscribe to an event exactly once. + */ + public function once(string $eventName, callable $callBack, int $priority = 100) + { + $wrapper = null; + $wrapper = function () use ($eventName, $callBack, &$wrapper) { + $this->removeListener($eventName, $wrapper); + + return \call_user_func_array($callBack, \func_get_args()); + }; + + $this->on($eventName, $wrapper, $priority); + } + + /** + * Emits an event. + * + * This method will return true if 0 or more listeners were successfully + * handled. false is returned if one of the events broke the event chain. + * + * If the continueCallBack is specified, this callback will be called every + * time before the next event handler is called. + * + * If the continueCallback returns false, event propagation stops. This + * allows you to use the eventEmitter as a means for listeners to implement + * functionality in your application, and break the event loop as soon as + * some condition is fulfilled. + * + * Note that returning false from an event subscriber breaks propagation + * and returns false, but if the continue-callback stops propagation, this + * is still considered a 'successful' operation and returns true. + * + * Lastly, if there are 5 event handlers for an event. The continueCallback + * will be called at most 4 times. + */ + public function emit(string $eventName, array $arguments = [], ?callable $continueCallBack = null): bool + { + if (\is_null($continueCallBack)) { + foreach ($this->listeners($eventName) as $listener) { + $result = \call_user_func_array($listener, $arguments); + if (false === $result) { + return false; + } + } + } else { + $listeners = $this->listeners($eventName); + $counter = \count($listeners); + + foreach ($listeners as $listener) { + --$counter; + $result = \call_user_func_array($listener, $arguments); + if (false === $result) { + return false; + } + + if ($counter > 0) { + if (!$continueCallBack()) { + break; + } + } + } + } + + return true; + } + + /** + * Returns the list of listeners for an event. + * + * The list is returned as an array, and the list of events are sorted by + * their priority. + * + * @return callable[] + */ + public function listeners(string $eventName): array + { + if (!\array_key_exists($eventName, $this->listenerIndex)) { + // Create a new index. + $listeners = []; + $listenersPriority = []; + if (isset($this->listeners[$eventName])) { + foreach ($this->listeners[$eventName] as $listener) { + $listenersPriority[] = $listener[0]; + $listeners[] = $listener[1]; + } + } + + foreach ($this->wildcardListeners as $wcEvent => $wcListeners) { + // Wildcard match + if (\substr($eventName, 0, \strlen($wcEvent)) === $wcEvent) { + foreach ($wcListeners as $listener) { + $listenersPriority[] = $listener[0]; + $listeners[] = $listener[1]; + } + } + } + + // Sorting by priority + \array_multisort($listenersPriority, SORT_NUMERIC, $listeners); + + // Creating index + $this->listenerIndex[$eventName] = $listeners; + } + + return $this->listenerIndex[$eventName]; + } + + /** + * Removes a specific listener from an event. + * + * If the listener could not be found, this method will return false. If it + * was removed it will return true. + */ + public function removeListener(string $eventName, callable $listener): bool + { + // If it ends with a wildcard, we use the wildcardListeners array + if ('*' === $eventName[\strlen($eventName) - 1]) { + $eventName = \substr($eventName, 0, -1); + $listeners = &$this->wildcardListeners; + } else { + $listeners = &$this->listeners; + } + + if (!isset($listeners[$eventName])) { + return false; + } + + foreach ($listeners[$eventName] as $index => $check) { + if ($check[1] === $listener) { + // Remove listener + unset($listeners[$eventName][$index]); + // Reset index + $this->listenerIndex = []; + + return true; + } + } + + return false; + } + + /** + * Removes all listeners. + * + * If the eventName argument is specified, all listeners for that event are + * removed. If it is not specified, every listener for every event is + * removed. + */ + public function removeAllListeners(?string $eventName = null) + { + if (\is_null($eventName)) { + $this->listeners = []; + $this->wildcardListeners = []; + } else { + if ('*' === $eventName[\strlen($eventName) - 1]) { + // Wildcard event + unset($this->wildcardListeners[\substr($eventName, 0, -1)]); + } else { + unset($this->listeners[$eventName]); + } + } + + // Reset index + $this->listenerIndex = []; + } + + /** + * The list of listeners. + */ + protected $listeners = []; + + /** + * The list of "wildcard listeners". + */ + protected $wildcardListeners = []; + + /** + * An index of listeners for a specific event name. This helps speeding + * up emitting events after all listeners have been set. + * + * If the list of listeners changes though, the index clears. + */ + protected $listenerIndex = []; +} diff --git a/3rdparty/sabre/event/lib/coroutine.php b/3rdparty/sabre/event/lib/coroutine.php new file mode 100644 index 00000000..f664efa7 --- /dev/null +++ b/3rdparty/sabre/event/lib/coroutine.php @@ -0,0 +1,122 @@ +request('GET', '/foo'); + * $promise->then(function($value) { + * + * return $httpClient->request('DELETE','/foo'); + * + * })->then(function($value) { + * + * return $httpClient->request('PUT', '/foo'); + * + * })->error(function($reason) { + * + * echo "Failed because: $reason\n"; + * + * }); + * + * Example with coroutines: + * + * coroutine(function() { + * + * try { + * yield $httpClient->request('GET', '/foo'); + * yield $httpClient->request('DELETE', /foo'); + * yield $httpClient->request('PUT', '/foo'); + * } catch(\Throwable $reason) { + * echo "Failed because: $reason\n"; + * } + * + * }); + * + * @psalm-template TReturn + * + * @psalm-param callable():\Generator $gen + * + * @psalm-return Promise + * + * @copyright Copyright (C) fruux GmbH (https://fruux.com/) + * @author Evert Pot (http://evertpot.com/) + * @license http://sabre.io/license/ Modified BSD License + */ +function coroutine(callable $gen): Promise +{ + $generator = $gen(); + if (!$generator instanceof \Generator) { + throw new \InvalidArgumentException('You must pass a generator function'); + } + + // This is the value we're returning. + $promise = new Promise(); + + /** + * So tempted to use the mythical y-combinator here, but it's not needed in + * PHP. + */ + $advanceGenerator = function () use (&$advanceGenerator, $generator, $promise) { + while ($generator->valid()) { + $yieldedValue = $generator->current(); + if ($yieldedValue instanceof Promise) { + $yieldedValue->then( + function ($value) use ($generator, &$advanceGenerator) { + $generator->send($value); + $advanceGenerator(); + }, + function (\Throwable $reason) use ($generator, $advanceGenerator) { + $generator->throw($reason); + $advanceGenerator(); + } + )->otherwise(function (\Throwable $reason) use ($promise) { + // This error handler would be called, if something in the + // generator throws an exception, and it's not caught + // locally. + $promise->reject($reason); + }); + // We need to break out of the loop, because $advanceGenerator + // will be called asynchronously when the promise has a result. + break; + } else { + // If the value was not a promise, we'll just let it pass through. + $generator->send($yieldedValue); + } + } + + // If the generator is at the end, and we didn't run into an exception, + // We're grabbing the "return" value and fulfilling our top-level + // promise with its value. + if (!$generator->valid() && Promise::PENDING === $promise->state) { + $returnValue = $generator->getReturn(); + + // The return value is a promise. + if ($returnValue instanceof Promise) { + $returnValue->then(function ($value) use ($promise) { + $promise->fulfill($value); + }, function (\Throwable $reason) use ($promise) { + $promise->reject($reason); + }); + } else { + $promise->fulfill($returnValue); + } + } + }; + + try { + $advanceGenerator(); + } catch (\Throwable $e) { + $promise->reject($e); + } + + return $promise; +} diff --git a/3rdparty/sabre/http/LICENSE b/3rdparty/sabre/http/LICENSE new file mode 100644 index 00000000..864041b2 --- /dev/null +++ b/3rdparty/sabre/http/LICENSE @@ -0,0 +1,27 @@ +Copyright (C) 2009-2017 fruux GmbH (https://fruux.com/) + +All rights reserved. + +Redistribution and use in source and binary forms, with or without modification, +are permitted provided that the following conditions are met: + + * Redistributions of source code must retain the above copyright notice, + this list of conditions and the following disclaimer. + * Redistributions in binary form must reproduce the above copyright notice, + this list of conditions and the following disclaimer in the documentation + and/or other materials provided with the distribution. + * Neither the name Sabre nor the names of its contributors + may be used to endorse or promote products derived from this software + without specific prior written permission. + + THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" + AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE + IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE + ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE + LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR + CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF + SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS + INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN + CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) + ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE + POSSIBILITY OF SUCH DAMAGE. diff --git a/3rdparty/sabre/http/lib/Auth/AWS.php b/3rdparty/sabre/http/lib/Auth/AWS.php new file mode 100644 index 00000000..2690c634 --- /dev/null +++ b/3rdparty/sabre/http/lib/Auth/AWS.php @@ -0,0 +1,220 @@ +request->getHeader('Authorization'); + + if (null === $authHeader) { + $this->errorCode = self::ERR_NOAWSHEADER; + + return false; + } + $authHeader = explode(' ', $authHeader); + + if ('AWS' !== $authHeader[0] || !isset($authHeader[1])) { + $this->errorCode = self::ERR_NOAWSHEADER; + + return false; + } + + list($this->accessKey, $this->signature) = explode(':', $authHeader[1]); + + return true; + } + + /** + * Returns the username for the request. + */ + public function getAccessKey(): string + { + return $this->accessKey; + } + + /** + * Validates the signature based on the secretKey. + */ + public function validate(string $secretKey): bool + { + $contentMD5 = $this->request->getHeader('Content-MD5'); + + if ($contentMD5) { + // We need to validate the integrity of the request + $body = $this->request->getBody(); + $this->request->setBody($body); + + if ($contentMD5 !== base64_encode(md5((string) $body, true))) { + // content-md5 header did not match md5 signature of body + $this->errorCode = self::ERR_MD5CHECKSUMWRONG; + + return false; + } + } + + if (!$requestDate = $this->request->getHeader('x-amz-date')) { + $requestDate = $this->request->getHeader('Date'); + } + + if (!$this->validateRFC2616Date((string) $requestDate)) { + return false; + } + + $amzHeaders = $this->getAmzHeaders(); + + $signature = base64_encode( + $this->hmacsha1($secretKey, + $this->request->getMethod()."\n". + $contentMD5."\n". + $this->request->getHeader('Content-type')."\n". + $requestDate."\n". + $amzHeaders. + $this->request->getUrl() + ) + ); + + if ($this->signature !== $signature) { + $this->errorCode = self::ERR_INVALIDSIGNATURE; + + return false; + } + + return true; + } + + /** + * Returns an HTTP 401 header, forcing login. + * + * This should be called when username and password are incorrect, or not supplied at all + */ + public function requireLogin() + { + $this->response->addHeader('WWW-Authenticate', 'AWS'); + $this->response->setStatus(401); + } + + /** + * Makes sure the supplied value is a valid RFC2616 date. + * + * If we would just use strtotime to get a valid timestamp, we have no way of checking if a + * user just supplied the word 'now' for the date header. + * + * This function also makes sure the Date header is within 15 minutes of the operating + * system date, to prevent replay attacks. + */ + protected function validateRFC2616Date(string $dateHeader): bool + { + $date = HTTP\parseDate($dateHeader); + + // Unknown format + if (!$date) { + $this->errorCode = self::ERR_INVALIDDATEFORMAT; + + return false; + } + + $min = new \DateTime('-15 minutes'); + $max = new \DateTime('+15 minutes'); + + // We allow 15 minutes around the current date/time + if ($date > $max || $date < $min) { + $this->errorCode = self::ERR_REQUESTTIMESKEWED; + + return false; + } + + return true; + } + + /** + * Returns a list of AMZ headers. + */ + protected function getAmzHeaders(): string + { + $amzHeaders = []; + $headers = $this->request->getHeaders(); + foreach ($headers as $headerName => $headerValue) { + if (0 === strpos(strtolower($headerName), 'x-amz-')) { + $amzHeaders[strtolower($headerName)] = str_replace(["\r\n"], [' '], $headerValue[0])."\n"; + } + } + ksort($amzHeaders); + + $headerStr = ''; + foreach ($amzHeaders as $h => $v) { + $headerStr .= $h.':'.$v; + } + + return $headerStr; + } + + /** + * Generates an HMAC-SHA1 signature. + */ + private function hmacsha1(string $key, string $message): string + { + if (function_exists('hash_hmac')) { + return hash_hmac('sha1', $message, $key, true); + } + + $blocksize = 64; + if (strlen($key) > $blocksize) { + $key = pack('H*', sha1($key)); + } + $key = str_pad($key, $blocksize, chr(0x00)); + $ipad = str_repeat(chr(0x36), $blocksize); + $opad = str_repeat(chr(0x5C), $blocksize); + $hmac = pack('H*', sha1(($key ^ $opad).pack('H*', sha1(($key ^ $ipad).$message)))); + + return $hmac; + } +} diff --git a/3rdparty/sabre/http/lib/Auth/AbstractAuth.php b/3rdparty/sabre/http/lib/Auth/AbstractAuth.php new file mode 100644 index 00000000..07f451bc --- /dev/null +++ b/3rdparty/sabre/http/lib/Auth/AbstractAuth.php @@ -0,0 +1,65 @@ +realm = $realm; + $this->request = $request; + $this->response = $response; + } + + /** + * This method sends the needed HTTP header and status code (401) to force + * the user to login. + */ + abstract public function requireLogin(); + + /** + * Returns the HTTP realm. + */ + public function getRealm(): string + { + return $this->realm; + } +} diff --git a/3rdparty/sabre/http/lib/Auth/Basic.php b/3rdparty/sabre/http/lib/Auth/Basic.php new file mode 100644 index 00000000..c1bad1a5 --- /dev/null +++ b/3rdparty/sabre/http/lib/Auth/Basic.php @@ -0,0 +1,60 @@ +request->getHeader('Authorization'); + + if (!$auth) { + return null; + } + + if ('basic ' !== strtolower(substr($auth, 0, 6))) { + return null; + } + + $credentials = explode(':', base64_decode(substr($auth, 6)), 2); + + if (2 !== count($credentials)) { + return null; + } + + return $credentials; + } + + /** + * This method sends the needed HTTP header and status code (401) to force + * the user to login. + */ + public function requireLogin() + { + $this->response->addHeader('WWW-Authenticate', 'Basic realm="'.$this->realm.'", charset="UTF-8"'); + $this->response->setStatus(401); + } +} diff --git a/3rdparty/sabre/http/lib/Auth/Bearer.php b/3rdparty/sabre/http/lib/Auth/Bearer.php new file mode 100644 index 00000000..580e2394 --- /dev/null +++ b/3rdparty/sabre/http/lib/Auth/Bearer.php @@ -0,0 +1,53 @@ +request->getHeader('Authorization'); + + if (!$auth) { + return null; + } + + if ('bearer ' !== strtolower(substr($auth, 0, 7))) { + return null; + } + + return substr($auth, 7); + } + + /** + * This method sends the needed HTTP header and status code (401) to force + * authentication. + */ + public function requireLogin() + { + $this->response->addHeader('WWW-Authenticate', 'Bearer realm="'.$this->realm.'"'); + $this->response->setStatus(401); + } +} diff --git a/3rdparty/sabre/http/lib/Auth/Digest.php b/3rdparty/sabre/http/lib/Auth/Digest.php new file mode 100644 index 00000000..08fa34f9 --- /dev/null +++ b/3rdparty/sabre/http/lib/Auth/Digest.php @@ -0,0 +1,208 @@ +nonce = uniqid(); + $this->opaque = md5($realm); + parent::__construct($realm, $request, $response); + } + + /** + * Gathers all information from the headers. + * + * This method needs to be called prior to anything else. + */ + public function init() + { + $digest = $this->getDigest(); + $this->digestParts = $this->parseDigest((string) $digest); + } + + /** + * Sets the quality of protection value. + * + * Possible values are: + * Sabre\HTTP\DigestAuth::QOP_AUTH + * Sabre\HTTP\DigestAuth::QOP_AUTHINT + * + * Multiple values can be specified using logical OR. + * + * QOP_AUTHINT ensures integrity of the request body, but this is not + * supported by most HTTP clients. QOP_AUTHINT also requires the entire + * request body to be md5'ed, which can put strains on CPU and memory. + */ + public function setQOP(int $qop) + { + $this->qop = $qop; + } + + /** + * Validates the user. + * + * The A1 parameter should be md5($username . ':' . $realm . ':' . $password); + */ + public function validateA1(string $A1): bool + { + $this->A1 = $A1; + + return $this->validate(); + } + + /** + * Validates authentication through a password. The actual password must be provided here. + * It is strongly recommended not store the password in plain-text and use validateA1 instead. + */ + public function validatePassword(string $password): bool + { + $this->A1 = md5($this->digestParts['username'].':'.$this->realm.':'.$password); + + return $this->validate(); + } + + /** + * Returns the username for the request. + * Returns null if there were none. + * + * @return string|null + */ + public function getUsername() + { + return $this->digestParts['username'] ?? null; + } + + /** + * Validates the digest challenge. + */ + protected function validate(): bool + { + if (!is_array($this->digestParts)) { + return false; + } + + $A2 = $this->request->getMethod().':'.$this->digestParts['uri']; + + if ('auth-int' === $this->digestParts['qop']) { + // Making sure we support this qop value + if (!($this->qop & self::QOP_AUTHINT)) { + return false; + } + // We need to add an md5 of the entire request body to the A2 part of the hash + $body = $this->request->getBody(); + $this->request->setBody($body); + $A2 .= ':'.md5($body); + } elseif (!($this->qop & self::QOP_AUTH)) { + return false; + } + + $A2 = md5($A2); + + $validResponse = md5("{$this->A1}:{$this->digestParts['nonce']}:{$this->digestParts['nc']}:{$this->digestParts['cnonce']}:{$this->digestParts['qop']}:{$A2}"); + + return $this->digestParts['response'] === $validResponse; + } + + /** + * Returns an HTTP 401 header, forcing login. + * + * This should be called when username and password are incorrect, or not supplied at all + */ + public function requireLogin() + { + $qop = ''; + switch ($this->qop) { + case self::QOP_AUTH: + $qop = 'auth'; + break; + case self::QOP_AUTHINT: + $qop = 'auth-int'; + break; + case self::QOP_AUTH | self::QOP_AUTHINT: + $qop = 'auth,auth-int'; + break; + } + + $this->response->addHeader('WWW-Authenticate', 'Digest realm="'.$this->realm.'",qop="'.$qop.'",nonce="'.$this->nonce.'",opaque="'.$this->opaque.'"'); + $this->response->setStatus(401); + } + + /** + * This method returns the full digest string. + * + * It should be compatible with mod_php format and other webservers. + * + * If the header could not be found, null will be returned + */ + public function getDigest() + { + return $this->request->getHeader('Authorization'); + } + + /** + * Parses the different pieces of the digest string into an array. + * + * This method returns false if an incomplete digest was supplied + * + * @return bool|array + */ + protected function parseDigest(string $digest) + { + // protect against missing data + $needed_parts = ['nonce' => 1, 'nc' => 1, 'cnonce' => 1, 'qop' => 1, 'username' => 1, 'uri' => 1, 'response' => 1]; + $data = []; + + preg_match_all('@(\w+)=(?:(?:")([^"]+)"|([^\s,$]+))@', $digest, $matches, PREG_SET_ORDER); + + foreach ($matches as $m) { + $data[$m[1]] = $m[2] ?: $m[3]; + unset($needed_parts[$m[1]]); + } + + return $needed_parts ? false : $data; + } +} diff --git a/3rdparty/sabre/http/lib/Client.php b/3rdparty/sabre/http/lib/Client.php new file mode 100644 index 00000000..c00f9e1b --- /dev/null +++ b/3rdparty/sabre/http/lib/Client.php @@ -0,0 +1,620 @@ +curlSettings = [ + CURLOPT_RETURNTRANSFER => true, + CURLOPT_NOBODY => false, + CURLOPT_USERAGENT => 'sabre-http/'.Version::VERSION.' (http://sabre.io/)', + ]; + if ($separatedHeaders) { + $this->curlSettings[CURLOPT_HEADERFUNCTION] = [$this, 'receiveCurlHeader']; + } else { + $this->curlSettings[CURLOPT_HEADER] = true; + } + } + + protected function receiveCurlHeader($curlHandle, $headerLine) + { + $this->headerLinesMap[(int) $curlHandle][] = $headerLine; + + return strlen($headerLine); + } + + /** + * Sends a request to a HTTP server, and returns a response. + */ + public function send(RequestInterface $request): ResponseInterface + { + $this->emit('beforeRequest', [$request]); + + $retryCount = 0; + $redirects = 0; + + do { + $doRedirect = false; + $retry = false; + + try { + $response = $this->doRequest($request); + + $code = $response->getStatus(); + + // We are doing in-PHP redirects, because curl's + // FOLLOW_LOCATION throws errors when PHP is configured with + // open_basedir. + // + // https://github.com/fruux/sabre-http/issues/12 + if ($redirects < $this->maxRedirects && in_array($code, [301, 302, 307, 308])) { + $oldLocation = $request->getUrl(); + + // Creating a new instance of the request object. + $request = clone $request; + + // Setting the new location + $request->setUrl(Uri\resolve( + $oldLocation, + $response->getHeader('Location') + )); + + $doRedirect = true; + ++$redirects; + } + + // This was a HTTP error + if ($code >= 400) { + $this->emit('error', [$request, $response, &$retry, $retryCount]); + $this->emit('error:'.$code, [$request, $response, &$retry, $retryCount]); + } + } catch (ClientException $e) { + $this->emit('exception', [$request, $e, &$retry, $retryCount]); + + // If retry was still set to false, it means no event handler + // dealt with the problem. In this case we just re-throw the + // exception. + if (!$retry) { + throw $e; + } + } + + if ($retry) { + ++$retryCount; + } + } while ($retry || $doRedirect); + + $this->emit('afterRequest', [$request, $response]); + + if ($this->throwExceptions && $code >= 400) { + throw new ClientHttpException($response); + } + + return $response; + } + + /** + * Sends a HTTP request asynchronously. + * + * Due to the nature of PHP, you must from time to time poll to see if any + * new responses came in. + * + * After calling sendAsync, you must therefore occasionally call the poll() + * method, or wait(). + */ + public function sendAsync(RequestInterface $request, ?callable $success = null, ?callable $error = null) + { + $this->emit('beforeRequest', [$request]); + $this->sendAsyncInternal($request, $success, $error); + $this->poll(); + } + + /** + * This method checks if any http requests have gotten results, and if so, + * call the appropriate success or error handlers. + * + * This method will return true if there are still requests waiting to + * return, and false if all the work is done. + */ + public function poll(): bool + { + // nothing to do? + if (!$this->curlMultiMap) { + return false; + } + + do { + $r = curl_multi_exec( + $this->curlMultiHandle, + $stillRunning + ); + } while (CURLM_CALL_MULTI_PERFORM === $r); + + $messagesInQueue = 0; + do { + messageQueue: + + $status = curl_multi_info_read( + $this->curlMultiHandle, + $messagesInQueue + ); + + if ($status && CURLMSG_DONE === $status['msg']) { + $resourceId = (int) $status['handle']; + list( + $request, + $successCallback, + $errorCallback, + $retryCount) = $this->curlMultiMap[$resourceId]; + unset($this->curlMultiMap[$resourceId]); + + $curlHandle = $status['handle']; + $curlResult = $this->parseResponse(curl_multi_getcontent($curlHandle), $curlHandle); + $retry = false; + + if (self::STATUS_CURLERROR === $curlResult['status']) { + $e = new ClientException($curlResult['curl_errmsg'], $curlResult['curl_errno']); + $this->emit('exception', [$request, $e, &$retry, $retryCount]); + + if ($retry) { + ++$retryCount; + $this->sendAsyncInternal($request, $successCallback, $errorCallback, $retryCount); + goto messageQueue; + } + + $curlResult['request'] = $request; + + if ($errorCallback) { + $errorCallback($curlResult); + } + } elseif (self::STATUS_HTTPERROR === $curlResult['status']) { + $this->emit('error', [$request, $curlResult['response'], &$retry, $retryCount]); + $this->emit('error:'.$curlResult['http_code'], [$request, $curlResult['response'], &$retry, $retryCount]); + + if ($retry) { + ++$retryCount; + $this->sendAsyncInternal($request, $successCallback, $errorCallback, $retryCount); + goto messageQueue; + } + + $curlResult['request'] = $request; + + if ($errorCallback) { + $errorCallback($curlResult); + } + } else { + $this->emit('afterRequest', [$request, $curlResult['response']]); + + if ($successCallback) { + $successCallback($curlResult['response']); + } + } + } + } while ($messagesInQueue > 0); + + return count($this->curlMultiMap) > 0; + } + + /** + * Processes every HTTP request in the queue, and waits till they are all + * completed. + */ + public function wait() + { + do { + curl_multi_select($this->curlMultiHandle); + $stillRunning = $this->poll(); + } while ($stillRunning); + } + + /** + * If this is set to true, the Client will automatically throw exceptions + * upon HTTP errors. + * + * This means that if a response came back with a status code greater than + * or equal to 400, we will throw a ClientHttpException. + * + * This only works for the send() method. Throwing exceptions for + * sendAsync() is not supported. + */ + public function setThrowExceptions(bool $throwExceptions) + { + $this->throwExceptions = $throwExceptions; + } + + /** + * Adds a CURL setting. + * + * These settings will be included in every HTTP request. + */ + public function addCurlSetting(int $name, $value) + { + $this->curlSettings[$name] = $value; + } + + /** + * This method is responsible for performing a single request. + */ + protected function doRequest(RequestInterface $request): ResponseInterface + { + $settings = $this->createCurlSettingsArray($request); + + if (!$this->curlHandle) { + $this->curlHandle = curl_init(); + } else { + curl_reset($this->curlHandle); + } + + curl_setopt_array($this->curlHandle, $settings); + $response = $this->curlExec($this->curlHandle); + $response = $this->parseResponse($response, $this->curlHandle); + if (self::STATUS_CURLERROR === $response['status']) { + throw new ClientException($response['curl_errmsg'], $response['curl_errno']); + } + + return $response['response']; + } + + /** + * Cached curl handle. + * + * By keeping this resource around for the lifetime of this object, things + * like persistent connections are possible. + * + * @var resource + */ + private $curlHandle; + + /** + * Handler for curl_multi requests. + * + * The first time sendAsync is used, this will be created. + * + * @var resource + */ + private $curlMultiHandle; + + /** + * Has a list of curl handles, as well as their associated success and + * error callbacks. + * + * @var array + */ + private $curlMultiMap = []; + + /** + * Turns a RequestInterface object into an array with settings that can be + * fed to curl_setopt. + */ + protected function createCurlSettingsArray(RequestInterface $request): array + { + $settings = $this->curlSettings; + + switch ($request->getMethod()) { + case 'HEAD': + $settings[CURLOPT_NOBODY] = true; + $settings[CURLOPT_CUSTOMREQUEST] = 'HEAD'; + break; + case 'GET': + $settings[CURLOPT_CUSTOMREQUEST] = 'GET'; + break; + default: + $body = $request->getBody(); + if (is_resource($body)) { + $bodyStat = fstat($body); + + // This needs to be set to PUT, regardless of the actual + // method used. Without it, INFILE will be ignored for some + // reason. + $settings[CURLOPT_PUT] = true; + $settings[CURLOPT_INFILE] = $body; + if (false !== $bodyStat && array_key_exists('size', $bodyStat)) { + $settings[CURLOPT_INFILESIZE] = $bodyStat['size']; + } + } else { + // For security we cast this to a string. If somehow an array could + // be passed here, it would be possible for an attacker to use @ to + // post local files. + $settings[CURLOPT_POSTFIELDS] = (string) $body; + } + $settings[CURLOPT_CUSTOMREQUEST] = $request->getMethod(); + break; + } + + $nHeaders = []; + foreach ($request->getHeaders() as $key => $values) { + foreach ($values as $value) { + $nHeaders[] = $key.': '.$value; + } + } + + if ([] !== $nHeaders) { + $settings[CURLOPT_HTTPHEADER] = $nHeaders; + } + $settings[CURLOPT_URL] = $request->getUrl(); + // FIXME: CURLOPT_PROTOCOLS is currently unsupported by HHVM + if (defined('CURLOPT_PROTOCOLS')) { + $settings[CURLOPT_PROTOCOLS] = CURLPROTO_HTTP | CURLPROTO_HTTPS; + } + // FIXME: CURLOPT_REDIR_PROTOCOLS is currently unsupported by HHVM + if (defined('CURLOPT_REDIR_PROTOCOLS')) { + $settings[CURLOPT_REDIR_PROTOCOLS] = CURLPROTO_HTTP | CURLPROTO_HTTPS; + } + + return $settings; + } + + public const STATUS_SUCCESS = 0; + public const STATUS_CURLERROR = 1; + public const STATUS_HTTPERROR = 2; + + private function parseResponse(string $response, $curlHandle): array + { + $settings = $this->curlSettings; + $separatedHeaders = isset($settings[CURLOPT_HEADERFUNCTION]) && (bool) $settings[CURLOPT_HEADERFUNCTION]; + + if ($separatedHeaders) { + $resourceId = (int) $curlHandle; + if (isset($this->headerLinesMap[$resourceId])) { + $headers = $this->headerLinesMap[$resourceId]; + } else { + $headers = []; + } + $response = $this->parseCurlResponse($headers, $response, $curlHandle); + } else { + $response = $this->parseCurlResult($response, $curlHandle); + } + + return $response; + } + + /** + * Parses the result of a curl call in a format that's a bit more + * convenient to work with. + * + * The method returns an array with the following elements: + * * status - one of the 3 STATUS constants. + * * curl_errno - A curl error number. Only set if status is + * STATUS_CURLERROR. + * * curl_errmsg - A current error message. Only set if status is + * STATUS_CURLERROR. + * * response - Response object. Only set if status is STATUS_SUCCESS, or + * STATUS_HTTPERROR. + * * http_code - HTTP status code, as an int. Only set if Only set if + * status is STATUS_SUCCESS, or STATUS_HTTPERROR + * + * @param resource $curlHandle + */ + protected function parseCurlResponse(array $headerLines, string $body, $curlHandle): array + { + list( + $curlInfo, + $curlErrNo, + $curlErrMsg + ) = $this->curlStuff($curlHandle); + + if ($curlErrNo) { + return [ + 'status' => self::STATUS_CURLERROR, + 'curl_errno' => $curlErrNo, + 'curl_errmsg' => $curlErrMsg, + ]; + } + + $response = new Response(); + $response->setStatus($curlInfo['http_code']); + $response->setBody($body); + + foreach ($headerLines as $header) { + $parts = explode(':', $header, 2); + if (2 === count($parts)) { + $response->addHeader(trim($parts[0]), trim($parts[1])); + } + } + + $httpCode = $response->getStatus(); + + return [ + 'status' => $httpCode >= 400 ? self::STATUS_HTTPERROR : self::STATUS_SUCCESS, + 'response' => $response, + 'http_code' => $httpCode, + ]; + } + + /** + * Parses the result of a curl call in a format that's a bit more + * convenient to work with. + * + * The method returns an array with the following elements: + * * status - one of the 3 STATUS constants. + * * curl_errno - A curl error number. Only set if status is + * STATUS_CURLERROR. + * * curl_errmsg - A current error message. Only set if status is + * STATUS_CURLERROR. + * * response - Response object. Only set if status is STATUS_SUCCESS, or + * STATUS_HTTPERROR. + * * http_code - HTTP status code, as an int. Only set if Only set if + * status is STATUS_SUCCESS, or STATUS_HTTPERROR + * + * @deprecated Use parseCurlResponse instead + * + * @param resource $curlHandle + */ + protected function parseCurlResult(string $response, $curlHandle): array + { + list( + $curlInfo, + $curlErrNo, + $curlErrMsg + ) = $this->curlStuff($curlHandle); + + if ($curlErrNo) { + return [ + 'status' => self::STATUS_CURLERROR, + 'curl_errno' => $curlErrNo, + 'curl_errmsg' => $curlErrMsg, + ]; + } + + $headerBlob = substr($response, 0, $curlInfo['header_size']); + // In the case of 204 No Content, strlen($response) == $curlInfo['header_size]. + // This will cause substr($response, $curlInfo['header_size']) return FALSE instead of NULL + // An exception will be thrown when calling getBodyAsString then + $responseBody = substr($response, $curlInfo['header_size']) ?: ''; + + unset($response); + + // In the case of 100 Continue, or redirects we'll have multiple lists + // of headers for each separate HTTP response. We can easily split this + // because they are separated by \r\n\r\n + $headerBlob = explode("\r\n\r\n", trim($headerBlob, "\r\n")); + + // We only care about the last set of headers + $headerBlob = $headerBlob[count($headerBlob) - 1]; + + // Splitting headers + $headerBlob = explode("\r\n", $headerBlob); + + return $this->parseCurlResponse($headerBlob, $responseBody, $curlHandle); + } + + /** + * Sends an asynchronous HTTP request. + * + * We keep this in a separate method, so we can call it without triggering + * the beforeRequest event and don't do the poll(). + */ + protected function sendAsyncInternal(RequestInterface $request, callable $success, callable $error, int $retryCount = 0) + { + if (!$this->curlMultiHandle) { + $this->curlMultiHandle = curl_multi_init(); + } + $curl = curl_init(); + curl_setopt_array( + $curl, + $this->createCurlSettingsArray($request) + ); + curl_multi_add_handle($this->curlMultiHandle, $curl); + + $resourceId = (int) $curl; + $this->headerLinesMap[$resourceId] = []; + $this->curlMultiMap[$resourceId] = [ + $request, + $success, + $error, + $retryCount, + ]; + } + + // @codeCoverageIgnoreStart + + /** + * Calls curl_exec. + * + * This method exists so it can easily be overridden and mocked. + * + * @param resource $curlHandle + */ + protected function curlExec($curlHandle): string + { + $this->headerLinesMap[(int) $curlHandle] = []; + + $result = curl_exec($curlHandle); + if (false === $result) { + $result = ''; + } + + return $result; + } + + /** + * Returns a bunch of information about a curl request. + * + * This method exists so it can easily be overridden and mocked. + * + * @param resource $curlHandle + */ + protected function curlStuff($curlHandle): array + { + return [ + curl_getinfo($curlHandle), + curl_errno($curlHandle), + curl_error($curlHandle), + ]; + } + + // @codeCoverageIgnoreEnd +} diff --git a/3rdparty/sabre/http/lib/ClientException.php b/3rdparty/sabre/http/lib/ClientException.php new file mode 100644 index 00000000..2ca4a28e --- /dev/null +++ b/3rdparty/sabre/http/lib/ClientException.php @@ -0,0 +1,17 @@ +response = $response; + parent::__construct($response->getStatusText(), $response->getStatus()); + } + + /** + * The http status code for the error. + */ + public function getHttpStatus(): int + { + return $this->response->getStatus(); + } + + /** + * Returns the full response object. + */ + public function getResponse(): ResponseInterface + { + return $this->response; + } +} diff --git a/3rdparty/sabre/http/lib/HttpException.php b/3rdparty/sabre/http/lib/HttpException.php new file mode 100644 index 00000000..80b3ae66 --- /dev/null +++ b/3rdparty/sabre/http/lib/HttpException.php @@ -0,0 +1,31 @@ +getBody(); + if (is_callable($this->body)) { + $body = $this->getBodyAsString(); + } + if (is_string($body) || null === $body) { + $stream = fopen('php://temp', 'r+'); + fwrite($stream, (string) $body); + rewind($stream); + + return $stream; + } + + return $body; + } + + /** + * Returns the body as a string. + * + * Note that because the underlying data may be based on a stream, this + * method could only work correctly the first time. + */ + public function getBodyAsString(): string + { + $body = $this->getBody(); + if (is_string($body)) { + return $body; + } + if (null === $body) { + return ''; + } + if (is_callable($body)) { + ob_start(); + $body(); + + return ob_get_clean(); + } + /** + * @var string|int|null + */ + $contentLength = $this->getHeader('Content-Length'); + if (null !== $contentLength && (is_int($contentLength) || ctype_digit($contentLength))) { + return stream_get_contents($body, (int) $contentLength); + } + + return stream_get_contents($body); + } + + /** + * Returns the message body, as its internal representation. + * + * This could be either a string, a stream or a callback writing the body to php://output. + * + * @return resource|string|callable + */ + public function getBody() + { + return $this->body; + } + + /** + * Replaces the body resource with a new stream, string or a callback writing the body to php://output. + * + * @param resource|string|callable $body + */ + public function setBody($body) + { + $this->body = $body; + } + + /** + * Returns all the HTTP headers as an array. + * + * Every header is returned as an array, with one or more values. + */ + public function getHeaders(): array + { + $result = []; + foreach ($this->headers as $headerInfo) { + $result[$headerInfo[0]] = $headerInfo[1]; + } + + return $result; + } + + /** + * Will return true or false, depending on if a HTTP header exists. + */ + public function hasHeader(string $name): bool + { + return isset($this->headers[strtolower($name)]); + } + + /** + * Returns a specific HTTP header, based on its name. + * + * The name must be treated as case-insensitive. + * If the header does not exist, this method must return null. + * + * If a header appeared more than once in a HTTP request, this method will + * concatenate all the values with a comma. + * + * Note that this not make sense for all headers. Some, such as + * `Set-Cookie` cannot be logically combined with a comma. In those cases + * you *should* use getHeaderAsArray(). + * + * @return string|null + */ + public function getHeader(string $name) + { + $name = strtolower($name); + + if (isset($this->headers[$name])) { + return implode(',', $this->headers[$name][1]); + } + + return null; + } + + /** + * Returns a HTTP header as an array. + * + * For every time the HTTP header appeared in the request or response, an + * item will appear in the array. + * + * If the header did not exist, this method will return an empty array. + * + * @return string[] + */ + public function getHeaderAsArray(string $name): array + { + $name = strtolower($name); + + if (isset($this->headers[$name])) { + return $this->headers[$name][1]; + } + + return []; + } + + /** + * Updates a HTTP header. + * + * The case-sensitivity of the name value must be retained as-is. + * + * If the header already existed, it will be overwritten. + * + * @param string|string[] $value + */ + public function setHeader(string $name, $value) + { + $this->headers[strtolower($name)] = [$name, (array) $value]; + } + + /** + * Sets a new set of HTTP headers. + * + * The headers array should contain headernames for keys, and their value + * should be specified as either a string or an array. + * + * Any header that already existed will be overwritten. + */ + public function setHeaders(array $headers) + { + foreach ($headers as $name => $value) { + $this->setHeader($name, $value); + } + } + + /** + * Adds a HTTP header. + * + * This method will not overwrite any existing HTTP header, but instead add + * another value. Individual values can be retrieved with + * getHeadersAsArray. + * + * @param string|string[] $value + */ + public function addHeader(string $name, $value) + { + $lName = strtolower($name); + if (isset($this->headers[$lName])) { + $this->headers[$lName][1] = array_merge( + $this->headers[$lName][1], + (array) $value + ); + } else { + $this->headers[$lName] = [ + $name, + (array) $value, + ]; + } + } + + /** + * Adds a new set of HTTP headers. + * + * Any existing headers will not be overwritten. + */ + public function addHeaders(array $headers) + { + foreach ($headers as $name => $value) { + $this->addHeader($name, $value); + } + } + + /** + * Removes a HTTP header. + * + * The specified header name must be treated as case-insensitive. + * This method should return true if the header was successfully deleted, + * and false if the header did not exist. + */ + public function removeHeader(string $name): bool + { + $name = strtolower($name); + if (!isset($this->headers[$name])) { + return false; + } + unset($this->headers[$name]); + + return true; + } + + /** + * Sets the HTTP version. + * + * Should be 1.0, 1.1 or 2.0. + */ + public function setHttpVersion(string $version) + { + $this->httpVersion = $version; + } + + /** + * Returns the HTTP version. + */ + public function getHttpVersion(): string + { + return $this->httpVersion; + } +} diff --git a/3rdparty/sabre/http/lib/MessageDecoratorTrait.php b/3rdparty/sabre/http/lib/MessageDecoratorTrait.php new file mode 100644 index 00000000..191ba0f7 --- /dev/null +++ b/3rdparty/sabre/http/lib/MessageDecoratorTrait.php @@ -0,0 +1,206 @@ +inner->getBodyAsStream(); + } + + /** + * Returns the body as a string. + * + * Note that because the underlying data may be based on a stream, this + * method could only work correctly the first time. + */ + public function getBodyAsString(): string + { + return $this->inner->getBodyAsString(); + } + + /** + * Returns the message body, as its internal representation. + * + * This could be either a string or a stream. + * + * @return resource|string + */ + public function getBody() + { + return $this->inner->getBody(); + } + + /** + * Updates the body resource with a new stream. + * + * @param resource|string|callable $body + */ + public function setBody($body) + { + $this->inner->setBody($body); + } + + /** + * Returns all the HTTP headers as an array. + * + * Every header is returned as an array, with one or more values. + */ + public function getHeaders(): array + { + return $this->inner->getHeaders(); + } + + /** + * Will return true or false, depending on if a HTTP header exists. + */ + public function hasHeader(string $name): bool + { + return $this->inner->hasHeader($name); + } + + /** + * Returns a specific HTTP header, based on its name. + * + * The name must be treated as case-insensitive. + * If the header does not exist, this method must return null. + * + * If a header appeared more than once in a HTTP request, this method will + * concatenate all the values with a comma. + * + * Note that this not make sense for all headers. Some, such as + * `Set-Cookie` cannot be logically combined with a comma. In those cases + * you *should* use getHeaderAsArray(). + * + * @return string|null + */ + public function getHeader(string $name) + { + return $this->inner->getHeader($name); + } + + /** + * Returns a HTTP header as an array. + * + * For every time the HTTP header appeared in the request or response, an + * item will appear in the array. + * + * If the header did not exist, this method will return an empty array. + */ + public function getHeaderAsArray(string $name): array + { + return $this->inner->getHeaderAsArray($name); + } + + /** + * Updates a HTTP header. + * + * The case-sensitivity of the name value must be retained as-is. + * + * If the header already existed, it will be overwritten. + * + * @param string|string[] $value + */ + public function setHeader(string $name, $value) + { + $this->inner->setHeader($name, $value); + } + + /** + * Sets a new set of HTTP headers. + * + * The headers array should contain headernames for keys, and their value + * should be specified as either a string or an array. + * + * Any header that already existed will be overwritten. + */ + public function setHeaders(array $headers) + { + $this->inner->setHeaders($headers); + } + + /** + * Adds a HTTP header. + * + * This method will not overwrite any existing HTTP header, but instead add + * another value. Individual values can be retrieved with + * getHeadersAsArray. + * + * @param string|string[] $value + */ + public function addHeader(string $name, $value) + { + $this->inner->addHeader($name, $value); + } + + /** + * Adds a new set of HTTP headers. + * + * Any existing headers will not be overwritten. + */ + public function addHeaders(array $headers) + { + $this->inner->addHeaders($headers); + } + + /** + * Removes a HTTP header. + * + * The specified header name must be treated as case-insensitive. + * This method should return true if the header was successfully deleted, + * and false if the header did not exist. + */ + public function removeHeader(string $name): bool + { + return $this->inner->removeHeader($name); + } + + /** + * Sets the HTTP version. + * + * Should be 1.0, 1.1 or 2.0. + */ + public function setHttpVersion(string $version) + { + $this->inner->setHttpVersion($version); + } + + /** + * Returns the HTTP version. + */ + public function getHttpVersion(): string + { + return $this->inner->getHttpVersion(); + } +} diff --git a/3rdparty/sabre/http/lib/MessageInterface.php b/3rdparty/sabre/http/lib/MessageInterface.php new file mode 100644 index 00000000..4531654b --- /dev/null +++ b/3rdparty/sabre/http/lib/MessageInterface.php @@ -0,0 +1,151 @@ +setMethod($method); + $this->setUrl($url); + $this->setHeaders($headers); + $this->setBody($body); + } + + /** + * Returns the current HTTP method. + */ + public function getMethod(): string + { + return $this->method; + } + + /** + * Sets the HTTP method. + */ + public function setMethod(string $method) + { + $this->method = $method; + } + + /** + * Returns the request url. + */ + public function getUrl(): string + { + return $this->url; + } + + /** + * Sets the request url. + */ + public function setUrl(string $url) + { + $this->url = $url; + } + + /** + * Returns the list of query parameters. + * + * This is equivalent to PHP's $_GET superglobal. + */ + public function getQueryParameters(): array + { + $url = $this->getUrl(); + if (false === ($index = strpos($url, '?'))) { + return []; + } + + parse_str(substr($url, $index + 1), $queryParams); + + return $queryParams; + } + + protected $absoluteUrl; + + /** + * Sets the absolute url. + */ + public function setAbsoluteUrl(string $url) + { + $this->absoluteUrl = $url; + } + + /** + * Returns the absolute url. + */ + public function getAbsoluteUrl(): string + { + if (!$this->absoluteUrl) { + // Guessing we're a http endpoint. + $this->absoluteUrl = 'http://'. + ($this->getHeader('Host') ?? 'localhost'). + $this->getUrl(); + } + + return $this->absoluteUrl; + } + + /** + * Base url. + * + * @var string + */ + protected $baseUrl = '/'; + + /** + * Sets a base url. + * + * This url is used for relative path calculations. + */ + public function setBaseUrl(string $url) + { + $this->baseUrl = $url; + } + + /** + * Returns the current base url. + */ + public function getBaseUrl(): string + { + return $this->baseUrl; + } + + /** + * Returns the relative path. + * + * This is being calculated using the base url. This path will not start + * with a slash, so it will always return something like + * 'example/path.html'. + * + * If the full path is equal to the base url, this method will return an + * empty string. + * + * This method will also urldecode the path, and if the url was encoded as + * ISO-8859-1, it will convert it to UTF-8. + * + * If the path is outside of the base url, a LogicException will be thrown. + */ + public function getPath(): string + { + // Removing duplicated slashes. + $uri = str_replace('//', '/', $this->getUrl()); + + $uri = Uri\normalize($uri); + $baseUri = Uri\normalize($this->getBaseUrl()); + + if (0 === strpos($uri, $baseUri)) { + // We're not interested in the query part (everything after the ?). + list($uri) = explode('?', $uri); + + return trim(decodePath(substr($uri, strlen($baseUri))), '/'); + } + + if ($uri.'/' === $baseUri) { + return ''; + } + // A special case, if the baseUri was accessed without a trailing + // slash, we'll accept it as well. + + throw new \LogicException('Requested uri ('.$this->getUrl().') is out of base uri ('.$this->getBaseUrl().')'); + } + + /** + * Equivalent of PHP's $_POST. + * + * @var array + */ + protected $postData = []; + + /** + * Sets the post data. + * + * This is equivalent to PHP's $_POST superglobal. + * + * This would not have been needed, if POST data was accessible as + * php://input, but unfortunately we need to special case it. + */ + public function setPostData(array $postData) + { + $this->postData = $postData; + } + + /** + * Returns the POST data. + * + * This is equivalent to PHP's $_POST superglobal. + */ + public function getPostData(): array + { + return $this->postData; + } + + /** + * An array containing the raw _SERVER array. + * + * @var array + */ + protected $rawServerData; + + /** + * Returns an item from the _SERVER array. + * + * If the value does not exist in the array, null is returned. + * + * @return string|null + */ + public function getRawServerValue(string $valueName) + { + return $this->rawServerData[$valueName] ?? null; + } + + /** + * Sets the _SERVER array. + */ + public function setRawServerData(array $data) + { + $this->rawServerData = $data; + } + + /** + * Serializes the request object as a string. + * + * This is useful for debugging purposes. + */ + public function __toString(): string + { + $out = $this->getMethod().' '.$this->getUrl().' HTTP/'.$this->getHttpVersion()."\r\n"; + + foreach ($this->getHeaders() as $key => $value) { + foreach ($value as $v) { + if ('Authorization' === $key) { + list($v) = explode(' ', $v, 2); + $v .= ' REDACTED'; + } + $out .= $key.': '.$v."\r\n"; + } + } + $out .= "\r\n"; + $out .= $this->getBodyAsString(); + + return $out; + } +} diff --git a/3rdparty/sabre/http/lib/RequestDecorator.php b/3rdparty/sabre/http/lib/RequestDecorator.php new file mode 100644 index 00000000..23e790e7 --- /dev/null +++ b/3rdparty/sabre/http/lib/RequestDecorator.php @@ -0,0 +1,179 @@ +inner = $inner; + } + + /** + * Returns the current HTTP method. + */ + public function getMethod(): string + { + return $this->inner->getMethod(); + } + + /** + * Sets the HTTP method. + */ + public function setMethod(string $method) + { + $this->inner->setMethod($method); + } + + /** + * Returns the request url. + */ + public function getUrl(): string + { + return $this->inner->getUrl(); + } + + /** + * Sets the request url. + */ + public function setUrl(string $url) + { + $this->inner->setUrl($url); + } + + /** + * Returns the absolute url. + */ + public function getAbsoluteUrl(): string + { + return $this->inner->getAbsoluteUrl(); + } + + /** + * Sets the absolute url. + */ + public function setAbsoluteUrl(string $url) + { + $this->inner->setAbsoluteUrl($url); + } + + /** + * Returns the current base url. + */ + public function getBaseUrl(): string + { + return $this->inner->getBaseUrl(); + } + + /** + * Sets a base url. + * + * This url is used for relative path calculations. + * + * The base url should default to / + */ + public function setBaseUrl(string $url) + { + $this->inner->setBaseUrl($url); + } + + /** + * Returns the relative path. + * + * This is being calculated using the base url. This path will not start + * with a slash, so it will always return something like + * 'example/path.html'. + * + * If the full path is equal to the base url, this method will return an + * empty string. + * + * This method will also urldecode the path, and if the url was encoded as + * ISO-8859-1, it will convert it to UTF-8. + * + * If the path is outside of the base url, a LogicException will be thrown. + */ + public function getPath(): string + { + return $this->inner->getPath(); + } + + /** + * Returns the list of query parameters. + * + * This is equivalent to PHP's $_GET superglobal. + */ + public function getQueryParameters(): array + { + return $this->inner->getQueryParameters(); + } + + /** + * Returns the POST data. + * + * This is equivalent to PHP's $_POST superglobal. + */ + public function getPostData(): array + { + return $this->inner->getPostData(); + } + + /** + * Sets the post data. + * + * This is equivalent to PHP's $_POST superglobal. + * + * This would not have been needed, if POST data was accessible as + * php://input, but unfortunately we need to special case it. + */ + public function setPostData(array $postData) + { + $this->inner->setPostData($postData); + } + + /** + * Returns an item from the _SERVER array. + * + * If the value does not exist in the array, null is returned. + * + * @return string|null + */ + public function getRawServerValue(string $valueName) + { + return $this->inner->getRawServerValue($valueName); + } + + /** + * Sets the _SERVER array. + */ + public function setRawServerData(array $data) + { + $this->inner->setRawServerData($data); + } + + /** + * Serializes the request object as a string. + * + * This is useful for debugging purposes. + */ + public function __toString(): string + { + return $this->inner->__toString(); + } +} diff --git a/3rdparty/sabre/http/lib/RequestInterface.php b/3rdparty/sabre/http/lib/RequestInterface.php new file mode 100644 index 00000000..5ec7777c --- /dev/null +++ b/3rdparty/sabre/http/lib/RequestInterface.php @@ -0,0 +1,114 @@ + 'Continue', + 101 => 'Switching Protocols', + 102 => 'Processing', + 200 => 'OK', + 201 => 'Created', + 202 => 'Accepted', + 203 => 'Non-Authoritative Information', + 204 => 'No Content', + 205 => 'Reset Content', + 206 => 'Partial Content', + 207 => 'Multi-Status', // RFC 4918 + 208 => 'Already Reported', // RFC 5842 + 226 => 'IM Used', // RFC 3229 + 300 => 'Multiple Choices', + 301 => 'Moved Permanently', + 302 => 'Found', + 303 => 'See Other', + 304 => 'Not Modified', + 305 => 'Use Proxy', + 307 => 'Temporary Redirect', + 308 => 'Permanent Redirect', + 400 => 'Bad Request', + 401 => 'Unauthorized', + 402 => 'Payment Required', + 403 => 'Forbidden', + 404 => 'Not Found', + 405 => 'Method Not Allowed', + 406 => 'Not Acceptable', + 407 => 'Proxy Authentication Required', + 408 => 'Request Timeout', + 409 => 'Conflict', + 410 => 'Gone', + 411 => 'Length Required', + 412 => 'Precondition failed', + 413 => 'Request Entity Too Large', + 414 => 'Request-URI Too Long', + 415 => 'Unsupported Media Type', + 416 => 'Requested Range Not Satisfiable', + 417 => 'Expectation Failed', + 418 => 'I\'m a teapot', // RFC 2324 + 421 => 'Misdirected Request', // RFC7540 (HTTP/2) + 422 => 'Unprocessable Entity', // RFC 4918 + 423 => 'Locked', // RFC 4918 + 424 => 'Failed Dependency', // RFC 4918 + 426 => 'Upgrade Required', + 428 => 'Precondition Required', // RFC 6585 + 429 => 'Too Many Requests', // RFC 6585 + 431 => 'Request Header Fields Too Large', // RFC 6585 + 451 => 'Unavailable For Legal Reasons', // draft-tbray-http-legally-restricted-status + 500 => 'Internal Server Error', + 501 => 'Not Implemented', + 502 => 'Bad Gateway', + 503 => 'Service Unavailable', + 504 => 'Gateway Timeout', + 505 => 'HTTP Version not supported', + 506 => 'Variant Also Negotiates', + 507 => 'Insufficient Storage', // RFC 4918 + 508 => 'Loop Detected', // RFC 5842 + 509 => 'Bandwidth Limit Exceeded', // non-standard + 510 => 'Not extended', + 511 => 'Network Authentication Required', // RFC 6585 + ]; + + /** + * HTTP status code. + * + * @var int + */ + protected $status; + + /** + * HTTP status text. + * + * @var string + */ + protected $statusText; + + /** + * Creates the response object. + * + * @param string|int $status + * @param resource $body + */ + public function __construct($status = 500, ?array $headers = null, $body = null) + { + if (null !== $status) { + $this->setStatus($status); + } + if (null !== $headers) { + $this->setHeaders($headers); + } + if (null !== $body) { + $this->setBody($body); + } + } + + /** + * Returns the current HTTP status code. + */ + public function getStatus(): int + { + return $this->status; + } + + /** + * Returns the human-readable status string. + * + * In the case of a 200, this may for example be 'OK'. + */ + public function getStatusText(): string + { + return $this->statusText; + } + + /** + * Sets the HTTP status code. + * + * This can be either the full HTTP status code with human-readable string, + * for example: "403 I can't let you do that, Dave". + * + * Or just the code, in which case the appropriate default message will be + * added. + * + * @param string|int $status + * + * @throws \InvalidArgumentException + */ + public function setStatus($status) + { + if (is_int($status) || ctype_digit($status)) { + $statusCode = $status; + $statusText = self::$statusCodes[$status] ?? 'Unknown'; + } else { + list( + $statusCode, + $statusText + ) = explode(' ', $status, 2); + $statusCode = (int) $statusCode; + } + if ($statusCode < 100 || $statusCode > 999) { + throw new \InvalidArgumentException('The HTTP status code must be exactly 3 digits'); + } + + $this->status = $statusCode; + $this->statusText = $statusText; + } + + /** + * Serializes the response object as a string. + * + * This is useful for debugging purposes. + */ + public function __toString(): string + { + $str = 'HTTP/'.$this->httpVersion.' '.$this->getStatus().' '.$this->getStatusText()."\r\n"; + foreach ($this->getHeaders() as $key => $value) { + foreach ($value as $v) { + $str .= $key.': '.$v."\r\n"; + } + } + $str .= "\r\n"; + $str .= $this->getBodyAsString(); + + return $str; + } +} diff --git a/3rdparty/sabre/http/lib/ResponseDecorator.php b/3rdparty/sabre/http/lib/ResponseDecorator.php new file mode 100644 index 00000000..30b9ec0e --- /dev/null +++ b/3rdparty/sabre/http/lib/ResponseDecorator.php @@ -0,0 +1,72 @@ +inner = $inner; + } + + /** + * Returns the current HTTP status code. + */ + public function getStatus(): int + { + return $this->inner->getStatus(); + } + + /** + * Returns the human-readable status string. + * + * In the case of a 200, this may for example be 'OK'. + */ + public function getStatusText(): string + { + return $this->inner->getStatusText(); + } + + /** + * Sets the HTTP status code. + * + * This can be either the full HTTP status code with human-readable string, + * for example: "403 I can't let you do that, Dave". + * + * Or just the code, in which case the appropriate default message will be + * added. + * + * @param string|int $status + */ + public function setStatus($status) + { + $this->inner->setStatus($status); + } + + /** + * Serializes the request object as a string. + * + * This is useful for debugging purposes. + */ + public function __toString(): string + { + return $this->inner->__toString(); + } +} diff --git a/3rdparty/sabre/http/lib/ResponseInterface.php b/3rdparty/sabre/http/lib/ResponseInterface.php new file mode 100644 index 00000000..fd73d25c --- /dev/null +++ b/3rdparty/sabre/http/lib/ResponseInterface.php @@ -0,0 +1,42 @@ +setBody(fopen('php://input', 'r')); + $r->setPostData($_POST); + + return $r; + } + + /** + * Sends the HTTP response back to a HTTP client. + * + * This calls php's header() function and streams the body to php://output. + */ + public static function sendResponse(ResponseInterface $response) + { + header('HTTP/'.$response->getHttpVersion().' '.$response->getStatus().' '.$response->getStatusText()); + foreach ($response->getHeaders() as $key => $value) { + foreach ($value as $k => $v) { + if (0 === $k) { + header($key.': '.$v); + } else { + header($key.': '.$v, false); + } + } + } + + $body = $response->getBody(); + if (null === $body) { + return; + } + + if (is_callable($body)) { + $body(); + + return; + } + + $contentLength = $response->getHeader('Content-Length'); + if (null !== $contentLength) { + $output = fopen('php://output', 'wb'); + if (is_resource($body) && 'stream' == get_resource_type($body)) { + // a workaround to make PHP more possible to use mmap based copy, see https://github.com/sabre-io/http/pull/119 + $left = (int) $contentLength; + // copy with 4MiB chunks + $chunk_size = 4 * 1024 * 1024; + stream_set_chunk_size($output, $chunk_size); + // If this is a partial response, flush the beginning bytes until the first position that is a multiple of the page size. + $contentRange = $response->getHeader('Content-Range'); + // Matching "Content-Range: bytes 1234-5678/7890" + if (null !== $contentRange && preg_match('/^bytes\s([0-9]+)-([0-9]+)\//i', $contentRange, $matches)) { + // 4kB should be the default page size on most architectures + $pageSize = 4096; + $offset = (int) $matches[1]; + $delta = ($offset % $pageSize) > 0 ? ($pageSize - $offset % $pageSize) : 0; + if ($delta > 0) { + $left -= stream_copy_to_stream($body, $output, min($delta, $left)); + } + } + while ($left > 0) { + $copied = stream_copy_to_stream($body, $output, min($left, $chunk_size)); + // stream_copy_to_stream($src, $dest, $maxLength) must return the number of bytes copied or false in case of failure + // But when the $maxLength is greater than the total number of bytes remaining in the stream, + // It returns the negative number of bytes copied + // So break the loop in such cases. + if ($copied <= 0) { + break; + } + // Abort on client disconnect. + // With ignore_user_abort(true), the script is not aborted on client disconnect. + // To avoid reading the entire stream and dismissing the data afterward, check between the chunks if the client is still there. + if (1 === ignore_user_abort() && 1 === connection_aborted()) { + break; + } + $left -= $copied; + } + } else { + fwrite($output, $body, (int) $contentLength); + } + } else { + file_put_contents('php://output', $body); + } + + if (is_resource($body)) { + fclose($body); + } + } + + /** + * This static method will create a new Request object, based on a PHP + * $_SERVER array. + * + * REQUEST_URI and REQUEST_METHOD are required. + */ + public static function createFromServerArray(array $serverArray): Request + { + $headers = []; + $method = null; + $url = null; + $httpVersion = '1.1'; + + $protocol = 'http'; + $hostName = 'localhost'; + + foreach ($serverArray as $key => $value) { + $key = (string) $key; + switch ($key) { + case 'SERVER_PROTOCOL': + if ('HTTP/1.0' === $value) { + $httpVersion = '1.0'; + } elseif ('HTTP/2.0' === $value) { + $httpVersion = '2.0'; + } + break; + case 'REQUEST_METHOD': + $method = $value; + break; + case 'REQUEST_URI': + $url = $value; + break; + + // These sometimes show up without a HTTP_ prefix + case 'CONTENT_TYPE': + $headers['Content-Type'] = $value; + break; + case 'CONTENT_LENGTH': + $headers['Content-Length'] = $value; + break; + + // mod_php on apache will put credentials in these variables. + // (fast)cgi does not usually do this, however. + case 'PHP_AUTH_USER': + if (isset($serverArray['PHP_AUTH_PW'])) { + $headers['Authorization'] = 'Basic '.base64_encode($value.':'.$serverArray['PHP_AUTH_PW']); + } + break; + + // Similarly, mod_php may also screw around with digest auth. + case 'PHP_AUTH_DIGEST': + $headers['Authorization'] = 'Digest '.$value; + break; + + // Apache may prefix the HTTP_AUTHORIZATION header with + // REDIRECT_, if mod_rewrite was used. + case 'REDIRECT_HTTP_AUTHORIZATION': + $headers['Authorization'] = $value; + break; + + case 'HTTP_HOST': + $hostName = $value; + $headers['Host'] = $value; + break; + + case 'HTTPS': + if (!empty($value) && 'off' !== $value) { + $protocol = 'https'; + } + break; + + default: + if ('HTTP_' === substr($key, 0, 5)) { + // It's a HTTP header + + // Normalizing it to be prettier + $header = strtolower(substr($key, 5)); + + // Transforming dashes into spaces, and upper-casing + // every first letter. + $header = ucwords(str_replace('_', ' ', $header)); + + // Turning spaces into dashes. + $header = str_replace(' ', '-', $header); + $headers[$header] = $value; + } + break; + } + } + + if (null === $url) { + throw new \InvalidArgumentException('The _SERVER array must have a REQUEST_URI key'); + } + + if (null === $method) { + throw new \InvalidArgumentException('The _SERVER array must have a REQUEST_METHOD key'); + } + $r = new Request($method, $url, $headers); + $r->setHttpVersion($httpVersion); + $r->setRawServerData($serverArray); + $r->setAbsoluteUrl($protocol.'://'.$hostName.$url); + + return $r; + } +} diff --git a/3rdparty/sabre/http/lib/Version.php b/3rdparty/sabre/http/lib/Version.php new file mode 100644 index 00000000..4ac82f6d --- /dev/null +++ b/3rdparty/sabre/http/lib/Version.php @@ -0,0 +1,20 @@ +setTimezone(new \DateTimeZone('GMT')); + + return $dateTime->format('D, d M Y H:i:s \G\M\T'); +} + +/** + * This function can be used to aid with content negotiation. + * + * It takes 2 arguments, the $acceptHeaderValue, which usually comes from + * an Accept header, and $availableOptions, which contains an array of + * items that the server can support. + * + * The result of this function will be the 'best possible option'. If no + * best possible option could be found, null is returned. + * + * When it's null you can according to the spec either return a default, or + * you can choose to emit 406 Not Acceptable. + * + * The method also accepts sending 'null' for the $acceptHeaderValue, + * implying that no accept header was sent. + * + * @param string|null $acceptHeaderValue + * + * @return string|null + */ +function negotiateContentType($acceptHeaderValue, array $availableOptions) +{ + if (!$acceptHeaderValue) { + // Grabbing the first in the list. + return reset($availableOptions); + } + + $proposals = array_map( + 'Sabre\HTTP\parseMimeType', + explode(',', $acceptHeaderValue) + ); + + // Ensuring array keys are reset. + $availableOptions = array_values($availableOptions); + + $options = array_map( + 'Sabre\HTTP\parseMimeType', + $availableOptions + ); + + $lastQuality = 0; + $lastSpecificity = 0; + $lastOptionIndex = 0; + $lastChoice = null; + + foreach ($proposals as $proposal) { + // Ignoring broken values. + if (null === $proposal) { + continue; + } + + // If the quality is lower we don't have to bother comparing. + if ($proposal['quality'] < $lastQuality) { + continue; + } + + foreach ($options as $optionIndex => $option) { + if ('*' !== $proposal['type'] && $proposal['type'] !== $option['type']) { + // no match on type. + continue; + } + if ('*' !== $proposal['subType'] && $proposal['subType'] !== $option['subType']) { + // no match on subtype. + continue; + } + + // Any parameters appearing on the options must appear on + // proposals. + foreach ($option['parameters'] as $paramName => $paramValue) { + if (!array_key_exists($paramName, $proposal['parameters'])) { + continue 2; + } + if ($paramValue !== $proposal['parameters'][$paramName]) { + continue 2; + } + } + + // If we got here, we have a match on parameters, type and + // subtype. We need to calculate a score for how specific the + // match was. + $specificity = + ('*' !== $proposal['type'] ? 20 : 0) + + ('*' !== $proposal['subType'] ? 10 : 0) + + count($option['parameters']); + + // Does this entry win? + if ( + ($proposal['quality'] > $lastQuality) + || ($proposal['quality'] === $lastQuality && $specificity > $lastSpecificity) + || ($proposal['quality'] === $lastQuality && $specificity === $lastSpecificity && $optionIndex < $lastOptionIndex) + ) { + $lastQuality = $proposal['quality']; + $lastSpecificity = $specificity; + $lastOptionIndex = $optionIndex; + $lastChoice = $availableOptions[$optionIndex]; + } + } + } + + return $lastChoice; +} + +/** + * Parses the Prefer header, as defined in RFC7240. + * + * Input can be given as a single header value (string) or multiple headers + * (array of string). + * + * This method will return a key->value array with the various Prefer + * parameters. + * + * Prefer: return=minimal will result in: + * + * [ 'return' => 'minimal' ] + * + * Prefer: foo, wait=10 will result in: + * + * [ 'foo' => true, 'wait' => '10'] + * + * This method also supports the formats from older drafts of RFC7240, and + * it will automatically map them to the new values, as the older values + * are still pretty common. + * + * Parameters are currently discarded. There's no known prefer value that + * uses them. + * + * @param string|string[] $input + */ +function parsePrefer($input): array +{ + $token = '[!#$%&\'*+\-.^_`~A-Za-z0-9]+'; + + // Work in progress + $word = '(?: [a-zA-Z0-9]+ | "[a-zA-Z0-9]*" )'; + + $regex = << $token) # Prefer property name +\s* # Optional space +(?: = \s* # Prefer property value + (? $word) +)? +(?: \s* ; (?: .*))? # Prefer parameters (ignored) +$ +/x +REGEX; + + $output = []; + foreach (getHeaderValues($input) as $value) { + if (!preg_match($regex, $value, $matches)) { + // Ignore + continue; + } + + // Mapping old values to their new counterparts + switch ($matches['name']) { + case 'return-asynch': + $output['respond-async'] = true; + break; + case 'return-representation': + $output['return'] = 'representation'; + break; + case 'return-minimal': + $output['return'] = 'minimal'; + break; + case 'strict': + $output['handling'] = 'strict'; + break; + case 'lenient': + $output['handling'] = 'lenient'; + break; + default: + if (isset($matches['value'])) { + $value = trim($matches['value'], '"'); + } else { + $value = true; + } + $output[strtolower($matches['name'])] = empty($value) ? true : $value; + break; + } + } + + return $output; +} + +/** + * This method splits up headers into all their individual values. + * + * A HTTP header may have more than one header, such as this: + * Cache-Control: private, no-store + * + * Header values are always split with a comma. + * + * You can pass either a string, or an array. The resulting value is always + * an array with each spliced value. + * + * If the second headers argument is set, this value will simply be merged + * in. This makes it quicker to merge an old list of values with a new set. + * + * @param string|string[] $values + * @param string|string[] $values2 + */ +function getHeaderValues($values, $values2 = null): array +{ + $values = (array) $values; + if ($values2) { + $values = array_merge($values, (array) $values2); + } + + $result = []; + foreach ($values as $l1) { + foreach (explode(',', $l1) as $l2) { + $result[] = trim($l2); + } + } + + return $result; +} + +/** + * Parses a mime-type and splits it into:. + * + * 1. type + * 2. subtype + * 3. quality + * 4. parameters + */ +function parseMimeType(string $str): array +{ + $parameters = []; + // If no q= parameter appears, then quality = 1. + $quality = 1; + + $parts = explode(';', $str); + + // The first part is the mime-type. + $mimeType = trim(array_shift($parts)); + + if ('*' === $mimeType) { + $mimeType = '*/*'; + } + + $mimeType = explode('/', $mimeType); + if (2 !== count($mimeType)) { + // Illegal value + var_dump($mimeType); + exit; + // throw new InvalidArgumentException('Not a valid mime-type: '.$str); + } + list($type, $subType) = $mimeType; + + foreach ($parts as $part) { + $part = trim($part); + if (strpos($part, '=')) { + list($partName, $partValue) = + explode('=', $part, 2); + } else { + $partName = $part; + $partValue = null; + } + + // The quality parameter, if it appears, also marks the end of + // the parameter list. Anything after the q= counts as an + // 'accept extension' and could introduce new semantics in + // content-negotiation. + if ('q' !== $partName) { + $parameters[$partName] = $part; + } else { + $quality = (float) $partValue; + break; // Stop parsing parts + } + } + + return [ + 'type' => $type, + 'subType' => $subType, + 'quality' => $quality, + 'parameters' => $parameters, + ]; +} + +/** + * Encodes the path of a url. + * + * slashes (/) are treated as path-separators. + */ +function encodePath(string $path): string +{ + return preg_replace_callback('/([^A-Za-z0-9_\-\.~\(\)\/:@])/', function ($match) { + return '%'.sprintf('%02x', ord($match[0])); + }, $path); +} + +/** + * Encodes a 1 segment of a path. + * + * Slashes are considered part of the name, and are encoded as %2f + */ +function encodePathSegment(string $pathSegment): string +{ + return preg_replace_callback('/([^A-Za-z0-9_\-\.~\(\):@])/', function ($match) { + return '%'.sprintf('%02x', ord($match[0])); + }, $pathSegment); +} + +/** + * Decodes a url-encoded path. + */ +function decodePath(string $path): string +{ + return decodePathSegment($path); +} + +/** + * Decodes a url-encoded path segment. + */ +function decodePathSegment(string $path): string +{ + $path = rawurldecode($path); + + if (!mb_check_encoding($path, 'UTF-8') && mb_check_encoding($path, 'ISO-8859-1')) { + $path = mb_convert_encoding($path, 'UTF-8', 'ISO-8859-1'); + } + + return $path; +} diff --git a/3rdparty/sabre/uri/LICENSE b/3rdparty/sabre/uri/LICENSE new file mode 100644 index 00000000..ae2c9929 --- /dev/null +++ b/3rdparty/sabre/uri/LICENSE @@ -0,0 +1,27 @@ +Copyright (C) 2014-2019 fruux GmbH (https://fruux.com/) + +All rights reserved. + +Redistribution and use in source and binary forms, with or without modification, +are permitted provided that the following conditions are met: + + * Redistributions of source code must retain the above copyright notice, + this list of conditions and the following disclaimer. + * Redistributions in binary form must reproduce the above copyright notice, + this list of conditions and the following disclaimer in the documentation + and/or other materials provided with the distribution. + * Neither the name Sabre nor the names of its contributors + may be used to endorse or promote products derived from this software + without specific prior written permission. + + THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" + AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE + IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE + ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE + LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR + CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF + SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS + INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN + CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) + ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE + POSSIBILITY OF SUCH DAMAGE. diff --git a/3rdparty/sabre/uri/lib/InvalidUriException.php b/3rdparty/sabre/uri/lib/InvalidUriException.php new file mode 100644 index 00000000..7f37ca54 --- /dev/null +++ b/3rdparty/sabre/uri/lib/InvalidUriException.php @@ -0,0 +1,19 @@ + 0) { + // If the path starts with a slash + if ('/' === $delta['path'][0]) { + $path = $delta['path']; + } else { + // Removing last component from base path. + $path = (string) $base['path']; + $length = strrpos($path, '/'); + if (false !== $length) { + $path = substr($path, 0, $length); + } + $path .= '/'.$delta['path']; + } + } else { + $path = $base['path'] ?? '/'; + if ('' === $path) { + $path = '/'; + } + } + // Removing .. and . + $pathParts = explode('/', $path); + $newPathParts = []; + foreach ($pathParts as $pathPart) { + switch ($pathPart) { + // case '' : + case '.': + break; + case '..': + array_pop($newPathParts); + break; + default: + $newPathParts[] = $pathPart; + break; + } + } + + $path = implode('/', $newPathParts); + + // If the source url ended with a /, we want to preserve that. + $newParts['path'] = 0 === strpos($path, '/') ? $path : '/'.$path; + // From PHP 8, no "?" query at all causes 'query' to be null. + // An empty query "http://example.com/foo?" causes 'query' to be the empty string + if (null !== $delta['query'] && '' !== $delta['query']) { + $newParts['query'] = $delta['query']; + } elseif (isset($base['query']) && null === $delta['host'] && null === $delta['path']) { + // Keep the old query if host and path didn't change + $newParts['query'] = $base['query']; + } + // From PHP 8, no "#" fragment at all causes 'fragment' to be null. + // An empty fragment "http://example.com/foo#" causes 'fragment' to be the empty string + if (null !== $delta['fragment'] && '' !== $delta['fragment']) { + $newParts['fragment'] = $delta['fragment']; + } + + return build($newParts); +} + +/** + * Takes a URI or partial URI as its argument, and normalizes it. + * + * After normalizing a URI, you can safely compare it to other URIs. + * This function will for instance convert a %7E into a tilde, according to + * rfc3986. + * + * It will also change a %3a into a %3A. + * + * @throws InvalidUriException + */ +function normalize(string $uri): string +{ + $parts = parse($uri); + + if (null !== $parts['path']) { + $pathParts = explode('/', ltrim($parts['path'], '/')); + $newPathParts = []; + foreach ($pathParts as $pathPart) { + switch ($pathPart) { + case '.': + // skip + break; + case '..': + // One level up in the hierarchy + array_pop($newPathParts); + break; + default: + // Ensuring that everything is correctly percent-encoded. + $newPathParts[] = rawurlencode(rawurldecode($pathPart)); + break; + } + } + $parts['path'] = '/'.implode('/', $newPathParts); + } + + if (null !== $parts['scheme']) { + $parts['scheme'] = strtolower($parts['scheme']); + $defaultPorts = [ + 'http' => '80', + 'https' => '443', + ]; + + if (null !== $parts['port'] && isset($defaultPorts[$parts['scheme']]) && $defaultPorts[$parts['scheme']] == $parts['port']) { + // Removing default ports. + unset($parts['port']); + } + // A few HTTP specific rules. + switch ($parts['scheme']) { + case 'http': + case 'https': + if (null === $parts['path']) { + // An empty path is equivalent to / in http. + $parts['path'] = '/'; + } + break; + } + } + + if (null !== $parts['host']) { + $parts['host'] = strtolower($parts['host']); + } + + return build($parts); +} + +/** + * Parses a URI and returns its individual components. + * + * This method largely behaves the same as PHP's parse_url, except that it will + * return an array with all the array keys, including the ones that are not + * set by parse_url, which makes it a bit easier to work with. + * + * Unlike PHP's parse_url, it will also convert any non-ascii characters to + * percent-encoded strings. PHP's parse_url corrupts these characters on OS X. + * + * In the return array, key "port" is an int value. Other keys have a string value. + * "Unused" keys have value null. + * + * @return array{scheme: string|null, host: string|null, path: string|null, port: positive-int|null, user: string|null, query: string|null, fragment: string|null} + * + * @throws InvalidUriException + */ +function parse(string $uri): array +{ + // Normally a URI must be ASCII. However, often it's not and + // parse_url might corrupt these strings. + // + // For that reason we take any non-ascii characters from the uri and + // uriencode them first. + $uri = preg_replace_callback( + '/[^[:ascii:]]/u', + function ($matches) { + return rawurlencode($matches[0]); + }, + $uri + ); + + if (null === $uri) { + throw new InvalidUriException('Invalid, or could not parse URI'); + } + + $result = parse_url($uri); + if (false === $result) { + $result = _parse_fallback($uri); + } + + /* + * phpstan is not able to process all the things that happen while this function + * constructs the result array. It only understands the $result is + * non-empty-array + * + * But the detail of the returned array is correctly specified in the PHPdoc + * above the function call. + * + * @phpstan-ignore-next-line + */ + return + $result + [ + 'scheme' => null, + 'host' => null, + 'path' => null, + 'port' => null, + 'user' => null, + 'query' => null, + 'fragment' => null, + ]; +} + +/** + * This function takes the components returned from PHP's parse_url, and uses + * it to generate a new uri. + * + * @param array $parts + */ +function build(array $parts): string +{ + $uri = ''; + + $authority = ''; + if (isset($parts['host'])) { + $authority = $parts['host']; + if (isset($parts['user'])) { + $authority = $parts['user'].'@'.$authority; + } + if (isset($parts['port'])) { + $authority = $authority.':'.$parts['port']; + } + } + + if (isset($parts['scheme'])) { + // If there's a scheme, there's also a host. + $uri = $parts['scheme'].':'; + } + if ('' !== $authority || (isset($parts['scheme']) && 'file' === $parts['scheme'])) { + // No scheme, but there is a host. + $uri .= '//'.$authority; + } + + if (isset($parts['path'])) { + $uri .= $parts['path']; + } + if (isset($parts['query'])) { + $uri .= '?'.$parts['query']; + } + if (isset($parts['fragment'])) { + $uri .= '#'.$parts['fragment']; + } + + return $uri; +} + +/** + * Returns the 'dirname' and 'basename' for a path. + * + * The reason there is a custom function for this purpose, is because + * basename() is locale aware (behaviour changes if C locale or a UTF-8 locale + * is used) and we need a method that just operates on UTF-8 characters. + * + * In addition basename and dirname are platform aware, and will treat + * backslash (\) as a directory separator on Windows. + * + * This method returns the 2 components as an array. + * + * If there is no dirname, it will return an empty string. Any / appearing at + * the end of the string is stripped off. + * + * @return array + */ +function split(string $path): array +{ + $matches = []; + if (1 === preg_match('/^(?:(?:(.*)(?:\/+))?([^\/]+))(?:\/?)$/u', $path, $matches)) { + return [$matches[1], $matches[2]]; + } + + return [null, null]; +} + +/** + * This function is another implementation of parse_url, except this one is + * fully written in PHP. + * + * The reason is that the PHP bug team is not willing to admit that there are + * bugs in the parse_url implementation. + * + * This function is only called if the main parse method fails. It's pretty + * crude and probably slow, so the original parse_url is usually preferred. + * + * @return array{scheme: string|null, host: string|null, path: string|null, port: positive-int|null, user: string|null, query: string|null, fragment: string|null} + * + * @throws InvalidUriException + */ +function _parse_fallback(string $uri): array +{ + // Normally a URI must be ASCII, however. However, often it's not and + // parse_url might corrupt these strings. + // + // For that reason we take any non-ascii characters from the uri and + // uriencode them first. + $uri = preg_replace_callback( + '/[^[:ascii:]]/u', + function ($matches) { + return rawurlencode($matches[0]); + }, + $uri + ); + + if (null === $uri) { + throw new InvalidUriException('Invalid, or could not parse URI'); + } + + $result = [ + 'scheme' => null, + 'host' => null, + 'port' => null, + 'user' => null, + 'path' => null, + 'fragment' => null, + 'query' => null, + ]; + + if (1 === preg_match('% ^([A-Za-z][A-Za-z0-9+-\.]+): %x', $uri, $matches)) { + $result['scheme'] = $matches[1]; + // Take what's left. + $uri = substr($uri, strlen($result['scheme']) + 1); + if (false === $uri) { + // There was nothing left. + $uri = ''; + } + } + + // Taking off a fragment part + if (false !== strpos($uri, '#')) { + list($uri, $result['fragment']) = explode('#', $uri, 2); + } + // Taking off the query part + if (false !== strpos($uri, '?')) { + list($uri, $result['query']) = explode('?', $uri, 2); + } + + if ('///' === substr($uri, 0, 3)) { + // The triple slash uris are a bit unusual, but we have special handling + // for them. + $path = substr($uri, 2); + if (false === $path) { + throw new \RuntimeException('The string cannot be false'); + } + $result['path'] = $path; + $result['host'] = ''; + } elseif ('//' === substr($uri, 0, 2)) { + // Uris that have an authority part. + $regex = '%^ + // + (?: (? [^:@]+) (: (? [^@]+)) @)? + (? ( [^:/]* | \[ [^\]]+ \] )) + (?: : (? [0-9]+))? + (? / .*)? + $%x'; + if (1 !== preg_match($regex, $uri, $matches)) { + throw new InvalidUriException('Invalid, or could not parse URI'); + } + if (isset($matches['host']) && '' !== $matches['host']) { + $result['host'] = $matches['host']; + } + if (isset($matches['port'])) { + $port = (int) $matches['port']; + if ($port > 0) { + $result['port'] = $port; + } + } + if (isset($matches['path'])) { + $result['path'] = $matches['path']; + } + if (isset($matches['user']) && '' !== $matches['user']) { + $result['user'] = $matches['user']; + } + if (isset($matches['pass']) && '' !== $matches['pass']) { + $result['pass'] = $matches['pass']; + } + } else { + $result['path'] = $uri; + } + + return $result; +} diff --git a/3rdparty/sabre/vobject/LICENSE b/3rdparty/sabre/vobject/LICENSE new file mode 100644 index 00000000..a99c8da1 --- /dev/null +++ b/3rdparty/sabre/vobject/LICENSE @@ -0,0 +1,27 @@ +Copyright (C) 2011-2016 fruux GmbH (https://fruux.com/) + +All rights reserved. + +Redistribution and use in source and binary forms, with or without modification, +are permitted provided that the following conditions are met: + + * Redistributions of source code must retain the above copyright notice, + this list of conditions and the following disclaimer. + * Redistributions in binary form must reproduce the above copyright notice, + this list of conditions and the following disclaimer in the documentation + and/or other materials provided with the distribution. + * Neither the name Sabre nor the names of its contributors + may be used to endorse or promote products derived from this software + without specific prior written permission. + + THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" + AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE + IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE + ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE + LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR + CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF + SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS + INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN + CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) + ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE + POSSIBILITY OF SUCH DAMAGE. diff --git a/3rdparty/sabre/vobject/lib/BirthdayCalendarGenerator.php b/3rdparty/sabre/vobject/lib/BirthdayCalendarGenerator.php new file mode 100644 index 00000000..fade50e1 --- /dev/null +++ b/3rdparty/sabre/vobject/lib/BirthdayCalendarGenerator.php @@ -0,0 +1,172 @@ +setObjects($objects); + } + } + + /** + * Sets the input objects. + * + * You must either supply a vCard as a string or as a Component/VCard object. + * It's also possible to supply an array of strings or objects. + * + * @param mixed $objects + */ + public function setObjects($objects) + { + if (!is_array($objects)) { + $objects = [$objects]; + } + + $this->objects = []; + foreach ($objects as $object) { + if (is_string($object)) { + $vObj = Reader::read($object); + if (!$vObj instanceof Component\VCard) { + throw new \InvalidArgumentException('String could not be parsed as \\Sabre\\VObject\\Component\\VCard by setObjects'); + } + + $this->objects[] = $vObj; + } elseif ($object instanceof Component\VCard) { + $this->objects[] = $object; + } else { + throw new \InvalidArgumentException('You can only pass strings or \\Sabre\\VObject\\Component\\VCard arguments to setObjects'); + } + } + } + + /** + * Sets the output format for the SUMMARY. + * + * @param string $format + */ + public function setFormat($format) + { + $this->format = $format; + } + + /** + * Parses the input data and returns a VCALENDAR. + * + * @return Component/VCalendar + */ + public function getResult() + { + $calendar = new VCalendar(); + + foreach ($this->objects as $object) { + // Skip if there is no BDAY property. + if (!$object->select('BDAY')) { + continue; + } + + // We've seen clients (ez-vcard) putting "BDAY:" properties + // without a value into vCards. If we come across those, we'll + // skip them. + if (empty($object->BDAY->getValue())) { + continue; + } + + // We're always converting to vCard 4.0 so we can rely on the + // VCardConverter handling the X-APPLE-OMIT-YEAR property for us. + $object = $object->convert(Document::VCARD40); + + // Skip if the card has no FN property. + if (!isset($object->FN)) { + continue; + } + + // Skip if the BDAY property is not of the right type. + if (!$object->BDAY instanceof Property\VCard\DateAndOrTime) { + continue; + } + + // Skip if we can't parse the BDAY value. + try { + $dateParts = DateTimeParser::parseVCardDateTime($object->BDAY->getValue()); + } catch (InvalidDataException $e) { + continue; + } + + // Set a year if it's not set. + $unknownYear = false; + + if (!$dateParts['year']) { + $object->BDAY = self::DEFAULT_YEAR.'-'.$dateParts['month'].'-'.$dateParts['date']; + + $unknownYear = true; + } + + // Create event. + $event = $calendar->add('VEVENT', [ + 'SUMMARY' => sprintf($this->format, $object->FN->getValue()), + 'DTSTART' => new \DateTime($object->BDAY->getValue()), + 'RRULE' => 'FREQ=YEARLY', + 'TRANSP' => 'TRANSPARENT', + ]); + + // add VALUE=date + $event->DTSTART['VALUE'] = 'DATE'; + + // Add X-SABRE-BDAY property. + if ($unknownYear) { + $event->add('X-SABRE-BDAY', 'BDAY', [ + 'X-SABRE-VCARD-UID' => $object->UID->getValue(), + 'X-SABRE-VCARD-FN' => $object->FN->getValue(), + 'X-SABRE-OMIT-YEAR' => self::DEFAULT_YEAR, + ]); + } else { + $event->add('X-SABRE-BDAY', 'BDAY', [ + 'X-SABRE-VCARD-UID' => $object->UID->getValue(), + 'X-SABRE-VCARD-FN' => $object->FN->getValue(), + ]); + } + } + + return $calendar; + } +} diff --git a/3rdparty/sabre/vobject/lib/Cli.php b/3rdparty/sabre/vobject/lib/Cli.php new file mode 100644 index 00000000..3bde16f9 --- /dev/null +++ b/3rdparty/sabre/vobject/lib/Cli.php @@ -0,0 +1,705 @@ +stderr) { + $this->stderr = fopen('php://stderr', 'w'); + } + if (!$this->stdout) { + $this->stdout = fopen('php://stdout', 'w'); + } + if (!$this->stdin) { + $this->stdin = fopen('php://stdin', 'r'); + } + + // @codeCoverageIgnoreEnd + + try { + list($options, $positional) = $this->parseArguments($argv); + + if (isset($options['q'])) { + $this->quiet = true; + } + $this->log($this->colorize('green', 'sabre/vobject ').$this->colorize('yellow', Version::VERSION)); + + foreach ($options as $name => $value) { + switch ($name) { + case 'q': + // Already handled earlier. + break; + case 'h': + case 'help': + $this->showHelp(); + + return 0; + break; + case 'format': + switch ($value) { + // jcard/jcal documents + case 'jcard': + case 'jcal': + // specific document versions + case 'vcard21': + case 'vcard30': + case 'vcard40': + case 'icalendar20': + // specific formats + case 'json': + case 'mimedir': + // icalendar/vcad + case 'icalendar': + case 'vcard': + $this->format = $value; + break; + + default: + throw new InvalidArgumentException('Unknown format: '.$value); + } + break; + case 'pretty': + if (version_compare(PHP_VERSION, '5.4.0') >= 0) { + $this->pretty = true; + } + break; + case 'forgiving': + $this->forgiving = true; + break; + case 'inputformat': + switch ($value) { + // json formats + case 'jcard': + case 'jcal': + case 'json': + $this->inputFormat = 'json'; + break; + + // mimedir formats + case 'mimedir': + case 'icalendar': + case 'vcard': + case 'vcard21': + case 'vcard30': + case 'vcard40': + case 'icalendar20': + $this->inputFormat = 'mimedir'; + break; + + default: + throw new InvalidArgumentException('Unknown format: '.$value); + } + break; + default: + throw new InvalidArgumentException('Unknown option: '.$name); + } + } + + if (0 === count($positional)) { + $this->showHelp(); + + return 1; + } + + if (1 === count($positional)) { + throw new InvalidArgumentException('Inputfile is a required argument'); + } + + if (count($positional) > 3) { + throw new InvalidArgumentException('Too many arguments'); + } + + if (!in_array($positional[0], ['validate', 'repair', 'convert', 'color'])) { + throw new InvalidArgumentException('Unknown command: '.$positional[0]); + } + } catch (InvalidArgumentException $e) { + $this->showHelp(); + $this->log('Error: '.$e->getMessage(), 'red'); + + return 1; + } + + $command = $positional[0]; + + $this->inputPath = $positional[1]; + $this->outputPath = isset($positional[2]) ? $positional[2] : '-'; + + if ('-' !== $this->outputPath) { + $this->stdout = fopen($this->outputPath, 'w'); + } + + if (!$this->inputFormat) { + if ('.json' === substr($this->inputPath, -5)) { + $this->inputFormat = 'json'; + } else { + $this->inputFormat = 'mimedir'; + } + } + if (!$this->format) { + if ('.json' === substr($this->outputPath, -5)) { + $this->format = 'json'; + } else { + $this->format = 'mimedir'; + } + } + + $realCode = 0; + + try { + while ($input = $this->readInput()) { + $returnCode = $this->$command($input); + if (0 !== $returnCode) { + $realCode = $returnCode; + } + } + } catch (EofException $e) { + // end of file + } catch (\Exception $e) { + $this->log('Error: '.$e->getMessage(), 'red'); + + return 2; + } + + return $realCode; + } + + /** + * Shows the help message. + */ + protected function showHelp() + { + $this->log('Usage:', 'yellow'); + $this->log(' vobject [options] command [arguments]'); + $this->log(''); + $this->log('Options:', 'yellow'); + $this->log($this->colorize('green', ' -q ')."Don't output anything."); + $this->log($this->colorize('green', ' -help -h ').'Display this help message.'); + $this->log($this->colorize('green', ' --format ').'Convert to a specific format. Must be one of: vcard, vcard21,'); + $this->log($this->colorize('green', ' --forgiving ').'Makes the parser less strict.'); + $this->log(' vcard30, vcard40, icalendar20, jcal, jcard, json, mimedir.'); + $this->log($this->colorize('green', ' --inputformat ').'If the input format cannot be guessed from the extension, it'); + $this->log(' must be specified here.'); + // Only PHP 5.4 and up + if (version_compare(PHP_VERSION, '5.4.0') >= 0) { + $this->log($this->colorize('green', ' --pretty ').'json pretty-print.'); + } + $this->log(''); + $this->log('Commands:', 'yellow'); + $this->log($this->colorize('green', ' validate').' source_file Validates a file for correctness.'); + $this->log($this->colorize('green', ' repair').' source_file [output_file] Repairs a file.'); + $this->log($this->colorize('green', ' convert').' source_file [output_file] Converts a file.'); + $this->log($this->colorize('green', ' color').' source_file Colorize a file, useful for debugging.'); + $this->log( + <<log('Examples:', 'yellow'); + $this->log(' vobject convert contact.vcf contact.json'); + $this->log(' vobject convert --format=vcard40 old.vcf new.vcf'); + $this->log(' vobject convert --inputformat=json --format=mimedir - -'); + $this->log(' vobject color calendar.ics'); + $this->log(''); + $this->log('https://github.com/fruux/sabre-vobject', 'purple'); + } + + /** + * Validates a VObject file. + * + * @return int + */ + protected function validate(Component $vObj) + { + $returnCode = 0; + + switch ($vObj->name) { + case 'VCALENDAR': + $this->log('iCalendar: '.(string) $vObj->VERSION); + break; + case 'VCARD': + $this->log('vCard: '.(string) $vObj->VERSION); + break; + } + + $warnings = $vObj->validate(); + if (!count($warnings)) { + $this->log(' No warnings!'); + } else { + $levels = [ + 1 => 'REPAIRED', + 2 => 'WARNING', + 3 => 'ERROR', + ]; + $returnCode = 2; + foreach ($warnings as $warn) { + $extra = ''; + if ($warn['node'] instanceof Property) { + $extra = ' (property: "'.$warn['node']->name.'")'; + } + $this->log(' ['.$levels[$warn['level']].'] '.$warn['message'].$extra); + } + } + + return $returnCode; + } + + /** + * Repairs a VObject file. + * + * @return int + */ + protected function repair(Component $vObj) + { + $returnCode = 0; + + switch ($vObj->name) { + case 'VCALENDAR': + $this->log('iCalendar: '.(string) $vObj->VERSION); + break; + case 'VCARD': + $this->log('vCard: '.(string) $vObj->VERSION); + break; + } + + $warnings = $vObj->validate(Node::REPAIR); + if (!count($warnings)) { + $this->log(' No warnings!'); + } else { + $levels = [ + 1 => 'REPAIRED', + 2 => 'WARNING', + 3 => 'ERROR', + ]; + $returnCode = 2; + foreach ($warnings as $warn) { + $extra = ''; + if ($warn['node'] instanceof Property) { + $extra = ' (property: "'.$warn['node']->name.'")'; + } + $this->log(' ['.$levels[$warn['level']].'] '.$warn['message'].$extra); + } + } + fwrite($this->stdout, $vObj->serialize()); + + return $returnCode; + } + + /** + * Converts a vObject file to a new format. + * + * @param Component $vObj + * + * @return int + */ + protected function convert($vObj) + { + $json = false; + $convertVersion = null; + $forceInput = null; + + switch ($this->format) { + case 'json': + $json = true; + if ('VCARD' === $vObj->name) { + $convertVersion = Document::VCARD40; + } + break; + case 'jcard': + $json = true; + $forceInput = 'VCARD'; + $convertVersion = Document::VCARD40; + break; + case 'jcal': + $json = true; + $forceInput = 'VCALENDAR'; + break; + case 'mimedir': + case 'icalendar': + case 'icalendar20': + case 'vcard': + break; + case 'vcard21': + $convertVersion = Document::VCARD21; + break; + case 'vcard30': + $convertVersion = Document::VCARD30; + break; + case 'vcard40': + $convertVersion = Document::VCARD40; + break; + } + + if ($forceInput && $vObj->name !== $forceInput) { + throw new \Exception('You cannot convert a '.strtolower($vObj->name).' to '.$this->format); + } + if ($convertVersion) { + $vObj = $vObj->convert($convertVersion); + } + if ($json) { + $jsonOptions = 0; + if ($this->pretty) { + $jsonOptions = JSON_PRETTY_PRINT; + } + fwrite($this->stdout, json_encode($vObj->jsonSerialize(), $jsonOptions)); + } else { + fwrite($this->stdout, $vObj->serialize()); + } + + return 0; + } + + /** + * Colorizes a file. + * + * @param Component $vObj + */ + protected function color($vObj) + { + $this->serializeComponent($vObj); + } + + /** + * Returns an ansi color string for a color name. + * + * @param string $color + * + * @return string + */ + protected function colorize($color, $str, $resetTo = 'default') + { + $colors = [ + 'cyan' => '1;36', + 'red' => '1;31', + 'yellow' => '1;33', + 'blue' => '0;34', + 'green' => '0;32', + 'default' => '0', + 'purple' => '0;35', + ]; + + return "\033[".$colors[$color].'m'.$str."\033[".$colors[$resetTo].'m'; + } + + /** + * Writes out a string in specific color. + * + * @param string $color + * @param string $str + */ + protected function cWrite($color, $str) + { + fwrite($this->stdout, $this->colorize($color, $str)); + } + + protected function serializeComponent(Component $vObj) + { + $this->cWrite('cyan', 'BEGIN'); + $this->cWrite('red', ':'); + $this->cWrite('yellow', $vObj->name."\n"); + + /** + * Gives a component a 'score' for sorting purposes. + * + * This is solely used by the childrenSort method. + * + * A higher score means the item will be lower in the list. + * To avoid score collisions, each "score category" has a reasonable + * space to accommodate elements. The $key is added to the $score to + * preserve the original relative order of elements. + * + * @param int $key + * @param array $array + * + * @return int + */ + $sortScore = function ($key, $array) { + if ($array[$key] instanceof Component) { + // We want to encode VTIMEZONE first, this is a personal + // preference. + if ('VTIMEZONE' === $array[$key]->name) { + $score = 300000000; + + return $score + $key; + } else { + $score = 400000000; + + return $score + $key; + } + } else { + // Properties get encoded first + // VCARD version 4.0 wants the VERSION property to appear first + if ($array[$key] instanceof Property) { + if ('VERSION' === $array[$key]->name) { + $score = 100000000; + + return $score + $key; + } else { + // All other properties + $score = 200000000; + + return $score + $key; + } + } + } + }; + + $children = $vObj->children(); + $tmp = $children; + uksort( + $children, + function ($a, $b) use ($sortScore, $tmp) { + $sA = $sortScore($a, $tmp); + $sB = $sortScore($b, $tmp); + + return $sA - $sB; + } + ); + + foreach ($children as $child) { + if ($child instanceof Component) { + $this->serializeComponent($child); + } else { + $this->serializeProperty($child); + } + } + + $this->cWrite('cyan', 'END'); + $this->cWrite('red', ':'); + $this->cWrite('yellow', $vObj->name."\n"); + } + + /** + * Colorizes a property. + */ + protected function serializeProperty(Property $property) + { + if ($property->group) { + $this->cWrite('default', $property->group); + $this->cWrite('red', '.'); + } + + $this->cWrite('yellow', $property->name); + + foreach ($property->parameters as $param) { + $this->cWrite('red', ';'); + $this->cWrite('blue', $param->serialize()); + } + $this->cWrite('red', ':'); + + if ($property instanceof Property\Binary) { + $this->cWrite('default', 'embedded binary stripped. ('.strlen($property->getValue()).' bytes)'); + } else { + $parts = $property->getParts(); + $first1 = true; + // Looping through property values + foreach ($parts as $part) { + if ($first1) { + $first1 = false; + } else { + $this->cWrite('red', $property->delimiter); + } + $first2 = true; + // Looping through property sub-values + foreach ((array) $part as $subPart) { + if ($first2) { + $first2 = false; + } else { + // The sub-value delimiter is always comma + $this->cWrite('red', ','); + } + + $subPart = strtr( + $subPart, + [ + '\\' => $this->colorize('purple', '\\\\', 'green'), + ';' => $this->colorize('purple', '\;', 'green'), + ',' => $this->colorize('purple', '\,', 'green'), + "\n" => $this->colorize('purple', "\\n\n\t", 'green'), + "\r" => '', + ] + ); + + $this->cWrite('green', $subPart); + } + } + } + $this->cWrite('default', "\n"); + } + + /** + * Parses the list of arguments. + */ + protected function parseArguments(array $argv) + { + $positional = []; + $options = []; + + for ($ii = 0; $ii < count($argv); ++$ii) { + // Skipping the first argument. + if (0 === $ii) { + continue; + } + + $v = $argv[$ii]; + + if ('--' === substr($v, 0, 2)) { + // This is a long-form option. + $optionName = substr($v, 2); + $optionValue = true; + if (strpos($optionName, '=')) { + list($optionName, $optionValue) = explode('=', $optionName); + } + $options[$optionName] = $optionValue; + } elseif ('-' === substr($v, 0, 1) && strlen($v) > 1) { + // This is a short-form option. + foreach (str_split(substr($v, 1)) as $option) { + $options[$option] = true; + } + } else { + $positional[] = $v; + } + } + + return [$options, $positional]; + } + + protected $parser; + + /** + * Reads the input file. + * + * @return Component + */ + protected function readInput() + { + if (!$this->parser) { + if ('-' !== $this->inputPath) { + $this->stdin = fopen($this->inputPath, 'r'); + } + + if ('mimedir' === $this->inputFormat) { + $this->parser = new Parser\MimeDir($this->stdin, ($this->forgiving ? Reader::OPTION_FORGIVING : 0)); + } else { + $this->parser = new Parser\Json($this->stdin, ($this->forgiving ? Reader::OPTION_FORGIVING : 0)); + } + } + + return $this->parser->parse(); + } + + /** + * Sends a message to STDERR. + * + * @param string $msg + */ + protected function log($msg, $color = 'default') + { + if (!$this->quiet) { + if ('default' !== $color) { + $msg = $this->colorize($color, $msg); + } + fwrite($this->stderr, $msg."\n"); + } + } +} diff --git a/3rdparty/sabre/vobject/lib/Component.php b/3rdparty/sabre/vobject/lib/Component.php new file mode 100644 index 00000000..ca82ad49 --- /dev/null +++ b/3rdparty/sabre/vobject/lib/Component.php @@ -0,0 +1,672 @@ + + */ + protected $children = []; + + /** + * Creates a new component. + * + * You can specify the children either in key=>value syntax, in which case + * properties will automatically be created, or you can just pass a list of + * Component and Property object. + * + * By default, a set of sensible values will be added to the component. For + * an iCalendar object, this may be something like CALSCALE:GREGORIAN. To + * ensure that this does not happen, set $defaults to false. + * + * @param string|null $name such as VCALENDAR, VEVENT + * @param bool $defaults + */ + public function __construct(Document $root, $name, array $children = [], $defaults = true) + { + $this->name = isset($name) ? strtoupper($name) : ''; + $this->root = $root; + + if ($defaults) { + // This is a terribly convoluted way to do this, but this ensures + // that the order of properties as they are specified in both + // defaults and the childrens list, are inserted in the object in a + // natural way. + $list = $this->getDefaults(); + $nodes = []; + foreach ($children as $key => $value) { + if ($value instanceof Node) { + if (isset($list[$value->name])) { + unset($list[$value->name]); + } + $nodes[] = $value; + } else { + $list[$key] = $value; + } + } + foreach ($list as $key => $value) { + $this->add($key, $value); + } + foreach ($nodes as $node) { + $this->add($node); + } + } else { + foreach ($children as $k => $child) { + if ($child instanceof Node) { + // Component or Property + $this->add($child); + } else { + // Property key=>value + $this->add($k, $child); + } + } + } + } + + /** + * Adds a new property or component, and returns the new item. + * + * This method has 3 possible signatures: + * + * add(Component $comp) // Adds a new component + * add(Property $prop) // Adds a new property + * add($name, $value, array $parameters = []) // Adds a new property + * add($name, array $children = []) // Adds a new component + * by name. + * + * @return Node + */ + public function add() + { + $arguments = func_get_args(); + + if ($arguments[0] instanceof Node) { + if (isset($arguments[1])) { + throw new \InvalidArgumentException('The second argument must not be specified, when passing a VObject Node'); + } + $arguments[0]->parent = $this; + $newNode = $arguments[0]; + } elseif (is_string($arguments[0])) { + $newNode = call_user_func_array([$this->root, 'create'], $arguments); + } else { + throw new \InvalidArgumentException('The first argument must either be a \\Sabre\\VObject\\Node or a string'); + } + + $name = $newNode->name; + if (isset($this->children[$name])) { + $this->children[$name][] = $newNode; + } else { + $this->children[$name] = [$newNode]; + } + + return $newNode; + } + + /** + * This method removes a component or property from this component. + * + * You can either specify the item by name (like DTSTART), in which case + * all properties/components with that name will be removed, or you can + * pass an instance of a property or component, in which case only that + * exact item will be removed. + * + * @param string|Property|Component $item + */ + public function remove($item) + { + if (is_string($item)) { + // If there's no dot in the name, it's an exact property name and + // we can just wipe out all those properties. + // + if (false === strpos($item, '.')) { + unset($this->children[strtoupper($item)]); + + return; + } + // If there was a dot, we need to ask select() to help us out and + // then we just call remove recursively. + foreach ($this->select($item) as $child) { + $this->remove($child); + } + } else { + foreach ($this->select($item->name) as $k => $child) { + if ($child === $item) { + unset($this->children[$item->name][$k]); + + return; + } + } + + throw new \InvalidArgumentException('The item you passed to remove() was not a child of this component'); + } + } + + /** + * Returns a flat list of all the properties and components in this + * component. + * + * @return array + */ + public function children() + { + $result = []; + foreach ($this->children as $childGroup) { + $result = array_merge($result, $childGroup); + } + + return $result; + } + + /** + * This method only returns a list of sub-components. Properties are + * ignored. + * + * @return array + */ + public function getComponents() + { + $result = []; + + foreach ($this->children as $childGroup) { + foreach ($childGroup as $child) { + if ($child instanceof self) { + $result[] = $child; + } + } + } + + return $result; + } + + /** + * Returns an array with elements that match the specified name. + * + * This function is also aware of MIME-Directory groups (as they appear in + * vcards). This means that if a property is grouped as "HOME.EMAIL", it + * will also be returned when searching for just "EMAIL". If you want to + * search for a property in a specific group, you can select on the entire + * string ("HOME.EMAIL"). If you want to search on a specific property that + * has not been assigned a group, specify ".EMAIL". + * + * @param string $name + * + * @return array + */ + public function select($name) + { + $group = null; + $name = strtoupper($name); + if (false !== strpos($name, '.')) { + list($group, $name) = explode('.', $name, 2); + } + if ('' === $name) { + $name = null; + } + + if (!is_null($name)) { + $result = isset($this->children[$name]) ? $this->children[$name] : []; + + if (is_null($group)) { + return $result; + } else { + // If we have a group filter as well, we need to narrow it down + // more. + return array_filter( + $result, + function ($child) use ($group) { + return $child instanceof Property && (null !== $child->group ? strtoupper($child->group) : '') === $group; + } + ); + } + } + + // If we got to this point, it means there was no 'name' specified for + // searching, implying that this is a group-only search. + $result = []; + foreach ($this->children as $childGroup) { + foreach ($childGroup as $child) { + if ($child instanceof Property && (null !== $child->group ? strtoupper($child->group) : '') === $group) { + $result[] = $child; + } + } + } + + return $result; + } + + /** + * Turns the object back into a serialized blob. + * + * @return string + */ + public function serialize() + { + $str = 'BEGIN:'.$this->name."\r\n"; + + /** + * Gives a component a 'score' for sorting purposes. + * + * This is solely used by the childrenSort method. + * + * A higher score means the item will be lower in the list. + * To avoid score collisions, each "score category" has a reasonable + * space to accommodate elements. The $key is added to the $score to + * preserve the original relative order of elements. + * + * @param int $key + * @param array $array + * + * @return int + */ + $sortScore = function ($key, $array) { + if ($array[$key] instanceof Component) { + // We want to encode VTIMEZONE first, this is a personal + // preference. + if ('VTIMEZONE' === $array[$key]->name) { + $score = 300000000; + + return $score + $key; + } else { + $score = 400000000; + + return $score + $key; + } + } else { + // Properties get encoded first + // VCARD version 4.0 wants the VERSION property to appear first + if ($array[$key] instanceof Property) { + if ('VERSION' === $array[$key]->name) { + $score = 100000000; + + return $score + $key; + } else { + // All other properties + $score = 200000000; + + return $score + $key; + } + } + } + }; + + $children = $this->children(); + $tmp = $children; + uksort( + $children, + function ($a, $b) use ($sortScore, $tmp) { + $sA = $sortScore($a, $tmp); + $sB = $sortScore($b, $tmp); + + return $sA - $sB; + } + ); + + foreach ($children as $child) { + $str .= $child->serialize(); + } + $str .= 'END:'.$this->name."\r\n"; + + return $str; + } + + /** + * This method returns an array, with the representation as it should be + * encoded in JSON. This is used to create jCard or jCal documents. + * + * @return array + */ + #[\ReturnTypeWillChange] + public function jsonSerialize() + { + $components = []; + $properties = []; + + foreach ($this->children as $childGroup) { + foreach ($childGroup as $child) { + if ($child instanceof self) { + $components[] = $child->jsonSerialize(); + } else { + $properties[] = $child->jsonSerialize(); + } + } + } + + return [ + strtolower($this->name), + $properties, + $components, + ]; + } + + /** + * This method serializes the data into XML. This is used to create xCard or + * xCal documents. + * + * @param Xml\Writer $writer XML writer + */ + public function xmlSerialize(Xml\Writer $writer): void + { + $components = []; + $properties = []; + + foreach ($this->children as $childGroup) { + foreach ($childGroup as $child) { + if ($child instanceof self) { + $components[] = $child; + } else { + $properties[] = $child; + } + } + } + + $writer->startElement(strtolower($this->name)); + + if (!empty($properties)) { + $writer->startElement('properties'); + + foreach ($properties as $property) { + $property->xmlSerialize($writer); + } + + $writer->endElement(); + } + + if (!empty($components)) { + $writer->startElement('components'); + + foreach ($components as $component) { + $component->xmlSerialize($writer); + } + + $writer->endElement(); + } + + $writer->endElement(); + } + + /** + * This method should return a list of default property values. + * + * @return array + */ + protected function getDefaults() + { + return []; + } + + /* Magic property accessors {{{ */ + + /** + * Using 'get' you will either get a property or component. + * + * If there were no child-elements found with the specified name, + * null is returned. + * + * To use this, this may look something like this: + * + * $event = $calendar->VEVENT; + * + * @param string $name + * + * @return Property|null + */ + public function __get($name) + { + if ('children' === $name) { + throw new \RuntimeException('Starting sabre/vobject 4.0 the children property is now protected. You should use the children() method instead'); + } + + $matches = $this->select($name); + if (0 === count($matches)) { + return; + } else { + $firstMatch = current($matches); + /* @var $firstMatch Property */ + $firstMatch->setIterator(new ElementList(array_values($matches))); + + return $firstMatch; + } + } + + /** + * This method checks if a sub-element with the specified name exists. + * + * @param string $name + * + * @return bool + */ + public function __isset($name) + { + $matches = $this->select($name); + + return count($matches) > 0; + } + + /** + * Using the setter method you can add properties or subcomponents. + * + * You can either pass a Component, Property + * object, or a string to automatically create a Property. + * + * If the item already exists, it will be removed. If you want to add + * a new item with the same name, always use the add() method. + * + * @param string $name + * @param mixed $value + */ + public function __set($name, $value) + { + $name = strtoupper($name); + $this->remove($name); + if ($value instanceof self || $value instanceof Property) { + $this->add($value); + } else { + $this->add($name, $value); + } + } + + /** + * Removes all properties and components within this component with the + * specified name. + * + * @param string $name + */ + public function __unset($name) + { + $this->remove($name); + } + + /* }}} */ + + /** + * This method is automatically called when the object is cloned. + * Specifically, this will ensure all child elements are also cloned. + */ + public function __clone() + { + foreach ($this->children as $childName => $childGroup) { + foreach ($childGroup as $key => $child) { + $clonedChild = clone $child; + $clonedChild->parent = $this; + $clonedChild->root = $this->root; + $this->children[$childName][$key] = $clonedChild; + } + } + } + + /** + * A simple list of validation rules. + * + * This is simply a list of properties, and how many times they either + * must or must not appear. + * + * Possible values per property: + * * 0 - Must not appear. + * * 1 - Must appear exactly once. + * * + - Must appear at least once. + * * * - Can appear any number of times. + * * ? - May appear, but not more than once. + * + * It is also possible to specify defaults and severity levels for + * violating the rule. + * + * See the VEVENT implementation for getValidationRules for a more complex + * example. + * + * @var array + */ + public function getValidationRules() + { + return []; + } + + /** + * Validates the node for correctness. + * + * The following options are supported: + * Node::REPAIR - May attempt to automatically repair the problem. + * Node::PROFILE_CARDDAV - Validate the vCard for CardDAV purposes. + * Node::PROFILE_CALDAV - Validate the iCalendar for CalDAV purposes. + * + * This method returns an array with detected problems. + * Every element has the following properties: + * + * * level - problem level. + * * message - A human-readable string describing the issue. + * * node - A reference to the problematic node. + * + * The level means: + * 1 - The issue was repaired (only happens if REPAIR was turned on). + * 2 - A warning. + * 3 - An error. + * + * @param int $options + * + * @return array + */ + public function validate($options = 0) + { + $rules = $this->getValidationRules(); + $defaults = $this->getDefaults(); + + $propertyCounters = []; + + $messages = []; + + foreach ($this->children() as $child) { + $name = strtoupper($child->name); + if (!isset($propertyCounters[$name])) { + $propertyCounters[$name] = 1; + } else { + ++$propertyCounters[$name]; + } + $messages = array_merge($messages, $child->validate($options)); + } + + foreach ($rules as $propName => $rule) { + switch ($rule) { + case '0': + if (isset($propertyCounters[$propName])) { + $messages[] = [ + 'level' => 3, + 'message' => $propName.' MUST NOT appear in a '.$this->name.' component', + 'node' => $this, + ]; + } + break; + case '1': + if (!isset($propertyCounters[$propName]) || 1 !== $propertyCounters[$propName]) { + $repaired = false; + if ($options & self::REPAIR && isset($defaults[$propName])) { + $this->add($propName, $defaults[$propName]); + $repaired = true; + } + $messages[] = [ + 'level' => $repaired ? 1 : 3, + 'message' => $propName.' MUST appear exactly once in a '.$this->name.' component', + 'node' => $this, + ]; + } + break; + case '+': + if (!isset($propertyCounters[$propName]) || $propertyCounters[$propName] < 1) { + $messages[] = [ + 'level' => 3, + 'message' => $propName.' MUST appear at least once in a '.$this->name.' component', + 'node' => $this, + ]; + } + break; + case '*': + break; + case '?': + if (isset($propertyCounters[$propName]) && $propertyCounters[$propName] > 1) { + $level = 3; + + // We try to repair the same property appearing multiple times with the exact same value + // by removing the duplicates and keeping only one property + if ($options & self::REPAIR) { + $properties = array_unique($this->select($propName), SORT_REGULAR); + + if (1 === count($properties)) { + $this->remove($propName); + $this->add($properties[0]); + + $level = 1; + } + } + + $messages[] = [ + 'level' => $level, + 'message' => $propName.' MUST NOT appear more than once in a '.$this->name.' component', + 'node' => $this, + ]; + } + break; + } + } + + return $messages; + } + + /** + * Call this method on a document if you're done using it. + * + * It's intended to remove all circular references, so PHP can easily clean + * it up. + */ + public function destroy() + { + parent::destroy(); + foreach ($this->children as $childGroup) { + foreach ($childGroup as $child) { + $child->destroy(); + } + } + $this->children = []; + } +} diff --git a/3rdparty/sabre/vobject/lib/Component/Available.php b/3rdparty/sabre/vobject/lib/Component/Available.php new file mode 100644 index 00000000..5510b9e0 --- /dev/null +++ b/3rdparty/sabre/vobject/lib/Component/Available.php @@ -0,0 +1,123 @@ +DTSTART->getDateTime(); + if (isset($this->DTEND)) { + $effectiveEnd = $this->DTEND->getDateTime(); + } else { + $effectiveEnd = $effectiveStart->add(VObject\DateTimeParser::parseDuration($this->DURATION)); + } + + return [$effectiveStart, $effectiveEnd]; + } + + /** + * A simple list of validation rules. + * + * This is simply a list of properties, and how many times they either + * must or must not appear. + * + * Possible values per property: + * * 0 - Must not appear. + * * 1 - Must appear exactly once. + * * + - Must appear at least once. + * * * - Can appear any number of times. + * * ? - May appear, but not more than once. + * + * @var array + */ + public function getValidationRules() + { + return [ + 'UID' => 1, + 'DTSTART' => 1, + 'DTSTAMP' => 1, + + 'DTEND' => '?', + 'DURATION' => '?', + + 'CREATED' => '?', + 'DESCRIPTION' => '?', + 'LAST-MODIFIED' => '?', + 'RECURRENCE-ID' => '?', + 'RRULE' => '?', + 'SUMMARY' => '?', + + 'CATEGORIES' => '*', + 'COMMENT' => '*', + 'CONTACT' => '*', + 'EXDATE' => '*', + 'RDATE' => '*', + + 'AVAILABLE' => '*', + ]; + } + + /** + * Validates the node for correctness. + * + * The following options are supported: + * Node::REPAIR - May attempt to automatically repair the problem. + * Node::PROFILE_CARDDAV - Validate the vCard for CardDAV purposes. + * Node::PROFILE_CALDAV - Validate the iCalendar for CalDAV purposes. + * + * This method returns an array with detected problems. + * Every element has the following properties: + * + * * level - problem level. + * * message - A human-readable string describing the issue. + * * node - A reference to the problematic node. + * + * The level means: + * 1 - The issue was repaired (only happens if REPAIR was turned on). + * 2 - A warning. + * 3 - An error. + * + * @param int $options + * + * @return array + */ + public function validate($options = 0) + { + $result = parent::validate($options); + + if (isset($this->DTEND) && isset($this->DURATION)) { + $result[] = [ + 'level' => 3, + 'message' => 'DTEND and DURATION cannot both be present', + 'node' => $this, + ]; + } + + return $result; + } +} diff --git a/3rdparty/sabre/vobject/lib/Component/VAlarm.php b/3rdparty/sabre/vobject/lib/Component/VAlarm.php new file mode 100644 index 00000000..bd00eb60 --- /dev/null +++ b/3rdparty/sabre/vobject/lib/Component/VAlarm.php @@ -0,0 +1,138 @@ +TRIGGER; + if (!isset($trigger['VALUE']) || 'DURATION' === strtoupper($trigger['VALUE'])) { + $triggerDuration = VObject\DateTimeParser::parseDuration($this->TRIGGER); + $related = (isset($trigger['RELATED']) && 'END' == strtoupper($trigger['RELATED'])) ? 'END' : 'START'; + + $parentComponent = $this->parent; + if ('START' === $related) { + if ('VTODO' === $parentComponent->name) { + $propName = 'DUE'; + } else { + $propName = 'DTSTART'; + } + + $effectiveTrigger = $parentComponent->$propName->getDateTime(); + $effectiveTrigger = $effectiveTrigger->add($triggerDuration); + } else { + if ('VTODO' === $parentComponent->name) { + $endProp = 'DUE'; + } elseif ('VEVENT' === $parentComponent->name) { + $endProp = 'DTEND'; + } else { + throw new InvalidDataException('time-range filters on VALARM components are only supported when they are a child of VTODO or VEVENT'); + } + + if (isset($parentComponent->$endProp)) { + $effectiveTrigger = $parentComponent->$endProp->getDateTime(); + $effectiveTrigger = $effectiveTrigger->add($triggerDuration); + } elseif (isset($parentComponent->DURATION)) { + $effectiveTrigger = $parentComponent->DTSTART->getDateTime(); + $duration = VObject\DateTimeParser::parseDuration($parentComponent->DURATION); + $effectiveTrigger = $effectiveTrigger->add($duration); + $effectiveTrigger = $effectiveTrigger->add($triggerDuration); + } else { + $effectiveTrigger = $parentComponent->DTSTART->getDateTime(); + $effectiveTrigger = $effectiveTrigger->add($triggerDuration); + } + } + } else { + $effectiveTrigger = $trigger->getDateTime(); + } + + return $effectiveTrigger; + } + + /** + * Returns true or false depending on if the event falls in the specified + * time-range. This is used for filtering purposes. + * + * The rules used to determine if an event falls within the specified + * time-range is based on the CalDAV specification. + * + * @param DateTime $start + * @param DateTime $end + * + * @return bool + */ + public function isInTimeRange(DateTimeInterface $start, DateTimeInterface $end) + { + $effectiveTrigger = $this->getEffectiveTriggerTime(); + + if (isset($this->DURATION)) { + $duration = VObject\DateTimeParser::parseDuration($this->DURATION); + $repeat = (string) $this->REPEAT; + if (!$repeat) { + $repeat = 1; + } + + $period = new \DatePeriod($effectiveTrigger, $duration, (int) $repeat); + + foreach ($period as $occurrence) { + if ($start <= $occurrence && $end > $occurrence) { + return true; + } + } + + return false; + } else { + return $start <= $effectiveTrigger && $end > $effectiveTrigger; + } + } + + /** + * A simple list of validation rules. + * + * This is simply a list of properties, and how many times they either + * must or must not appear. + * + * Possible values per property: + * * 0 - Must not appear. + * * 1 - Must appear exactly once. + * * + - Must appear at least once. + * * * - Can appear any number of times. + * * ? - May appear, but not more than once. + * + * @var array + */ + public function getValidationRules() + { + return [ + 'ACTION' => 1, + 'TRIGGER' => 1, + + 'DURATION' => '?', + 'REPEAT' => '?', + + 'ATTACH' => '?', + ]; + } +} diff --git a/3rdparty/sabre/vobject/lib/Component/VAvailability.php b/3rdparty/sabre/vobject/lib/Component/VAvailability.php new file mode 100644 index 00000000..04ec38dc --- /dev/null +++ b/3rdparty/sabre/vobject/lib/Component/VAvailability.php @@ -0,0 +1,149 @@ +getEffectiveStartEnd(); + + return + (is_null($effectiveStart) || $start < $effectiveEnd) && + (is_null($effectiveEnd) || $end > $effectiveStart) + ; + } + + /** + * Returns the 'effective start' and 'effective end' of this VAVAILABILITY + * component. + * + * We use the DTSTART and DTEND or DURATION to determine this. + * + * The returned value is an array containing DateTimeImmutable instances. + * If either the start or end is 'unbounded' its value will be null + * instead. + * + * @return array + */ + public function getEffectiveStartEnd() + { + $effectiveStart = null; + $effectiveEnd = null; + + if (isset($this->DTSTART)) { + $effectiveStart = $this->DTSTART->getDateTime(); + } + if (isset($this->DTEND)) { + $effectiveEnd = $this->DTEND->getDateTime(); + } elseif ($effectiveStart && isset($this->DURATION)) { + $effectiveEnd = $effectiveStart->add(VObject\DateTimeParser::parseDuration($this->DURATION)); + } + + return [$effectiveStart, $effectiveEnd]; + } + + /** + * A simple list of validation rules. + * + * This is simply a list of properties, and how many times they either + * must or must not appear. + * + * Possible values per property: + * * 0 - Must not appear. + * * 1 - Must appear exactly once. + * * + - Must appear at least once. + * * * - Can appear any number of times. + * * ? - May appear, but not more than once. + * + * @var array + */ + public function getValidationRules() + { + return [ + 'UID' => 1, + 'DTSTAMP' => 1, + + 'BUSYTYPE' => '?', + 'CLASS' => '?', + 'CREATED' => '?', + 'DESCRIPTION' => '?', + 'DTSTART' => '?', + 'LAST-MODIFIED' => '?', + 'ORGANIZER' => '?', + 'PRIORITY' => '?', + 'SEQUENCE' => '?', + 'SUMMARY' => '?', + 'URL' => '?', + 'DTEND' => '?', + 'DURATION' => '?', + + 'CATEGORIES' => '*', + 'COMMENT' => '*', + 'CONTACT' => '*', + ]; + } + + /** + * Validates the node for correctness. + * + * The following options are supported: + * Node::REPAIR - May attempt to automatically repair the problem. + * Node::PROFILE_CARDDAV - Validate the vCard for CardDAV purposes. + * Node::PROFILE_CALDAV - Validate the iCalendar for CalDAV purposes. + * + * This method returns an array with detected problems. + * Every element has the following properties: + * + * * level - problem level. + * * message - A human-readable string describing the issue. + * * node - A reference to the problematic node. + * + * The level means: + * 1 - The issue was repaired (only happens if REPAIR was turned on). + * 2 - A warning. + * 3 - An error. + * + * @param int $options + * + * @return array + */ + public function validate($options = 0) + { + $result = parent::validate($options); + + if (isset($this->DTEND) && isset($this->DURATION)) { + $result[] = [ + 'level' => 3, + 'message' => 'DTEND and DURATION cannot both be present', + 'node' => $this, + ]; + } + + return $result; + } +} diff --git a/3rdparty/sabre/vobject/lib/Component/VCalendar.php b/3rdparty/sabre/vobject/lib/Component/VCalendar.php new file mode 100644 index 00000000..017aed70 --- /dev/null +++ b/3rdparty/sabre/vobject/lib/Component/VCalendar.php @@ -0,0 +1,528 @@ + self::class, + 'VALARM' => VAlarm::class, + 'VEVENT' => VEvent::class, + 'VFREEBUSY' => VFreeBusy::class, + 'VAVAILABILITY' => VAvailability::class, + 'AVAILABLE' => Available::class, + 'VJOURNAL' => VJournal::class, + 'VTIMEZONE' => VTimeZone::class, + 'VTODO' => VTodo::class, + ]; + + /** + * List of value-types, and which classes they map to. + * + * @var array + */ + public static $valueMap = [ + 'BINARY' => VObject\Property\Binary::class, + 'BOOLEAN' => VObject\Property\Boolean::class, + 'CAL-ADDRESS' => VObject\Property\ICalendar\CalAddress::class, + 'DATE' => VObject\Property\ICalendar\Date::class, + 'DATE-TIME' => VObject\Property\ICalendar\DateTime::class, + 'DURATION' => VObject\Property\ICalendar\Duration::class, + 'FLOAT' => VObject\Property\FloatValue::class, + 'INTEGER' => VObject\Property\IntegerValue::class, + 'PERIOD' => VObject\Property\ICalendar\Period::class, + 'RECUR' => VObject\Property\ICalendar\Recur::class, + 'TEXT' => VObject\Property\Text::class, + 'TIME' => VObject\Property\Time::class, + 'UNKNOWN' => VObject\Property\Unknown::class, // jCard / jCal-only. + 'URI' => VObject\Property\Uri::class, + 'UTC-OFFSET' => VObject\Property\UtcOffset::class, + ]; + + /** + * List of properties, and which classes they map to. + * + * @var array + */ + public static $propertyMap = [ + // Calendar properties + 'CALSCALE' => VObject\Property\FlatText::class, + 'METHOD' => VObject\Property\FlatText::class, + 'PRODID' => VObject\Property\FlatText::class, + 'VERSION' => VObject\Property\FlatText::class, + + // Component properties + 'ATTACH' => VObject\Property\Uri::class, + 'CATEGORIES' => VObject\Property\Text::class, + 'CLASS' => VObject\Property\FlatText::class, + 'COMMENT' => VObject\Property\FlatText::class, + 'DESCRIPTION' => VObject\Property\FlatText::class, + 'GEO' => VObject\Property\FloatValue::class, + 'LOCATION' => VObject\Property\FlatText::class, + 'PERCENT-COMPLETE' => VObject\Property\IntegerValue::class, + 'PRIORITY' => VObject\Property\IntegerValue::class, + 'RESOURCES' => VObject\Property\Text::class, + 'STATUS' => VObject\Property\FlatText::class, + 'SUMMARY' => VObject\Property\FlatText::class, + + // Date and Time Component Properties + 'COMPLETED' => VObject\Property\ICalendar\DateTime::class, + 'DTEND' => VObject\Property\ICalendar\DateTime::class, + 'DUE' => VObject\Property\ICalendar\DateTime::class, + 'DTSTART' => VObject\Property\ICalendar\DateTime::class, + 'DURATION' => VObject\Property\ICalendar\Duration::class, + 'FREEBUSY' => VObject\Property\ICalendar\Period::class, + 'TRANSP' => VObject\Property\FlatText::class, + + // Time Zone Component Properties + 'TZID' => VObject\Property\FlatText::class, + 'TZNAME' => VObject\Property\FlatText::class, + 'TZOFFSETFROM' => VObject\Property\UtcOffset::class, + 'TZOFFSETTO' => VObject\Property\UtcOffset::class, + 'TZURL' => VObject\Property\Uri::class, + + // Relationship Component Properties + 'ATTENDEE' => VObject\Property\ICalendar\CalAddress::class, + 'CONTACT' => VObject\Property\FlatText::class, + 'ORGANIZER' => VObject\Property\ICalendar\CalAddress::class, + 'RECURRENCE-ID' => VObject\Property\ICalendar\DateTime::class, + 'RELATED-TO' => VObject\Property\FlatText::class, + 'URL' => VObject\Property\Uri::class, + 'UID' => VObject\Property\FlatText::class, + + // Recurrence Component Properties + 'EXDATE' => VObject\Property\ICalendar\DateTime::class, + 'RDATE' => VObject\Property\ICalendar\DateTime::class, + 'RRULE' => VObject\Property\ICalendar\Recur::class, + 'EXRULE' => VObject\Property\ICalendar\Recur::class, // Deprecated since rfc5545 + + // Alarm Component Properties + 'ACTION' => VObject\Property\FlatText::class, + 'REPEAT' => VObject\Property\IntegerValue::class, + 'TRIGGER' => VObject\Property\ICalendar\Duration::class, + + // Change Management Component Properties + 'CREATED' => VObject\Property\ICalendar\DateTime::class, + 'DTSTAMP' => VObject\Property\ICalendar\DateTime::class, + 'LAST-MODIFIED' => VObject\Property\ICalendar\DateTime::class, + 'SEQUENCE' => VObject\Property\IntegerValue::class, + + // Request Status + 'REQUEST-STATUS' => VObject\Property\Text::class, + + // Additions from draft-daboo-valarm-extensions-04 + 'ALARM-AGENT' => VObject\Property\Text::class, + 'ACKNOWLEDGED' => VObject\Property\ICalendar\DateTime::class, + 'PROXIMITY' => VObject\Property\Text::class, + 'DEFAULT-ALARM' => VObject\Property\Boolean::class, + + // Additions from draft-daboo-calendar-availability-05 + 'BUSYTYPE' => VObject\Property\Text::class, + ]; + + /** + * Returns the current document type. + * + * @return int + */ + public function getDocumentType() + { + return self::ICALENDAR20; + } + + /** + * Returns a list of all 'base components'. For instance, if an Event has + * a recurrence rule, and one instance is overridden, the overridden event + * will have the same UID, but will be excluded from this list. + * + * VTIMEZONE components will always be excluded. + * + * @param string $componentName filter by component name + * + * @return VObject\Component[] + */ + public function getBaseComponents($componentName = null) + { + $isBaseComponent = function ($component) { + if (!$component instanceof VObject\Component) { + return false; + } + if ('VTIMEZONE' === $component->name) { + return false; + } + if (isset($component->{'RECURRENCE-ID'})) { + return false; + } + + return true; + }; + + if ($componentName) { + // Early exit + return array_filter( + $this->select($componentName), + $isBaseComponent + ); + } + + $components = []; + foreach ($this->children as $childGroup) { + foreach ($childGroup as $child) { + if (!$child instanceof Component) { + // If one child is not a component, they all are so we skip + // the entire group. + continue 2; + } + if ($isBaseComponent($child)) { + $components[] = $child; + } + } + } + + return $components; + } + + /** + * Returns the first component that is not a VTIMEZONE, and does not have + * an RECURRENCE-ID. + * + * If there is no such component, null will be returned. + * + * @param string $componentName filter by component name + * + * @return VObject\Component|null + */ + public function getBaseComponent($componentName = null) + { + $isBaseComponent = function ($component) { + if (!$component instanceof VObject\Component) { + return false; + } + if ('VTIMEZONE' === $component->name) { + return false; + } + if (isset($component->{'RECURRENCE-ID'})) { + return false; + } + + return true; + }; + + if ($componentName) { + foreach ($this->select($componentName) as $child) { + if ($isBaseComponent($child)) { + return $child; + } + } + + return null; + } + + // Searching all components + foreach ($this->children as $childGroup) { + foreach ($childGroup as $child) { + if ($isBaseComponent($child)) { + return $child; + } + } + } + + return null; + } + + /** + * Expand all events in this VCalendar object and return a new VCalendar + * with the expanded events. + * + * If this calendar object, has events with recurrence rules, this method + * can be used to expand the event into multiple sub-events. + * + * Each event will be stripped from its recurrence information, and only + * the instances of the event in the specified timerange will be left + * alone. + * + * In addition, this method will cause timezone information to be stripped, + * and normalized to UTC. + * + * @param DateTimeZone $timeZone reference timezone for floating dates and + * times + * + * @return VCalendar + */ + public function expand(DateTimeInterface $start, DateTimeInterface $end, ?DateTimeZone $timeZone = null) + { + $newChildren = []; + $recurringEvents = []; + + if (!$timeZone) { + $timeZone = new DateTimeZone('UTC'); + } + + $stripTimezones = function (Component $component) use ($timeZone, &$stripTimezones) { + foreach ($component->children() as $componentChild) { + if ($componentChild instanceof Property\ICalendar\DateTime && $componentChild->hasTime()) { + $dt = $componentChild->getDateTimes($timeZone); + // We only need to update the first timezone, because + // setDateTimes will match all other timezones to the + // first. + $dt[0] = $dt[0]->setTimeZone(new DateTimeZone('UTC')); + $componentChild->setDateTimes($dt); + } elseif ($componentChild instanceof Component) { + $stripTimezones($componentChild); + } + } + + return $component; + }; + + foreach ($this->children() as $child) { + if ($child instanceof Property && 'PRODID' !== $child->name) { + // We explicitly want to ignore PRODID, because we want to + // overwrite it with our own. + $newChildren[] = clone $child; + } elseif ($child instanceof Component && 'VTIMEZONE' !== $child->name) { + // We're also stripping all VTIMEZONE objects because we're + // converting everything to UTC. + if ('VEVENT' === $child->name && (isset($child->{'RECURRENCE-ID'}) || isset($child->RRULE) || isset($child->RDATE))) { + // Handle these a bit later. + $uid = (string) $child->UID; + if (!$uid) { + throw new InvalidDataException('Every VEVENT object must have a UID property'); + } + if (isset($recurringEvents[$uid])) { + $recurringEvents[$uid][] = clone $child; + } else { + $recurringEvents[$uid] = [clone $child]; + } + } elseif ('VEVENT' === $child->name && $child->isInTimeRange($start, $end)) { + $newChildren[] = $stripTimezones(clone $child); + } + } + } + + foreach ($recurringEvents as $events) { + try { + $it = new EventIterator($events, null, $timeZone); + } catch (NoInstancesException $e) { + // This event is recurring, but it doesn't have a single + // instance. We are skipping this event from the output + // entirely. + continue; + } + $it->fastForward($start); + + while ($it->valid() && $it->getDTStart() < $end) { + if ($it->getDTEnd() > $start) { + $newChildren[] = $stripTimezones($it->getEventObject()); + } + $it->next(); + } + } + + return new self($newChildren); + } + + /** + * This method should return a list of default property values. + * + * @return array + */ + protected function getDefaults() + { + return [ + 'VERSION' => '2.0', + 'PRODID' => '-//Sabre//Sabre VObject '.VObject\Version::VERSION.'//EN', + 'CALSCALE' => 'GREGORIAN', + ]; + } + + /** + * A simple list of validation rules. + * + * This is simply a list of properties, and how many times they either + * must or must not appear. + * + * Possible values per property: + * * 0 - Must not appear. + * * 1 - Must appear exactly once. + * * + - Must appear at least once. + * * * - Can appear any number of times. + * * ? - May appear, but not more than once. + * + * @var array + */ + public function getValidationRules() + { + return [ + 'PRODID' => 1, + 'VERSION' => 1, + + 'CALSCALE' => '?', + 'METHOD' => '?', + ]; + } + + /** + * Validates the node for correctness. + * + * The following options are supported: + * Node::REPAIR - May attempt to automatically repair the problem. + * Node::PROFILE_CARDDAV - Validate the vCard for CardDAV purposes. + * Node::PROFILE_CALDAV - Validate the iCalendar for CalDAV purposes. + * + * This method returns an array with detected problems. + * Every element has the following properties: + * + * * level - problem level. + * * message - A human-readable string describing the issue. + * * node - A reference to the problematic node. + * + * The level means: + * 1 - The issue was repaired (only happens if REPAIR was turned on). + * 2 - A warning. + * 3 - An error. + * + * @param int $options + * + * @return array + */ + public function validate($options = 0) + { + $warnings = parent::validate($options); + + if ($ver = $this->VERSION) { + if ('2.0' !== (string) $ver) { + $warnings[] = [ + 'level' => 3, + 'message' => 'Only iCalendar version 2.0 as defined in rfc5545 is supported.', + 'node' => $this, + ]; + } + } + + $uidList = []; + $componentsFound = 0; + $componentTypes = []; + + foreach ($this->children() as $child) { + if ($child instanceof Component) { + ++$componentsFound; + + if (!in_array($child->name, ['VEVENT', 'VTODO', 'VJOURNAL'])) { + continue; + } + $componentTypes[] = $child->name; + + $uid = (string) $child->UID; + $isMaster = isset($child->{'RECURRENCE-ID'}) ? 0 : 1; + if (isset($uidList[$uid])) { + ++$uidList[$uid]['count']; + if ($isMaster && $uidList[$uid]['hasMaster']) { + $warnings[] = [ + 'level' => 3, + 'message' => 'More than one master object was found for the object with UID '.$uid, + 'node' => $this, + ]; + } + $uidList[$uid]['hasMaster'] += $isMaster; + } else { + $uidList[$uid] = [ + 'count' => 1, + 'hasMaster' => $isMaster, + ]; + } + } + } + + if (0 === $componentsFound) { + $warnings[] = [ + 'level' => 3, + 'message' => 'An iCalendar object must have at least 1 component.', + 'node' => $this, + ]; + } + + if ($options & self::PROFILE_CALDAV) { + if (count($uidList) > 1) { + $warnings[] = [ + 'level' => 3, + 'message' => 'A calendar object on a CalDAV server may only have components with the same UID.', + 'node' => $this, + ]; + } + if (0 === count($componentTypes)) { + $warnings[] = [ + 'level' => 3, + 'message' => 'A calendar object on a CalDAV server must have at least 1 component (VTODO, VEVENT, VJOURNAL).', + 'node' => $this, + ]; + } + if (count(array_unique($componentTypes)) > 1) { + $warnings[] = [ + 'level' => 3, + 'message' => 'A calendar object on a CalDAV server may only have 1 type of component (VEVENT, VTODO or VJOURNAL).', + 'node' => $this, + ]; + } + + if (isset($this->METHOD)) { + $warnings[] = [ + 'level' => 3, + 'message' => 'A calendar object on a CalDAV server MUST NOT have a METHOD property.', + 'node' => $this, + ]; + } + } + + return $warnings; + } + + /** + * Returns all components with a specific UID value. + * + * @return array + */ + public function getByUID($uid) + { + return array_filter($this->getComponents(), function ($item) use ($uid) { + if (!$itemUid = $item->select('UID')) { + return false; + } + $itemUid = current($itemUid)->getValue(); + + return $uid === $itemUid; + }); + } +} diff --git a/3rdparty/sabre/vobject/lib/Component/VCard.php b/3rdparty/sabre/vobject/lib/Component/VCard.php new file mode 100644 index 00000000..82fab82b --- /dev/null +++ b/3rdparty/sabre/vobject/lib/Component/VCard.php @@ -0,0 +1,541 @@ + VCard::class, + ]; + + /** + * List of value-types, and which classes they map to. + * + * @var array + */ + public static $valueMap = [ + 'BINARY' => VObject\Property\Binary::class, + 'BOOLEAN' => VObject\Property\Boolean::class, + 'CONTENT-ID' => VObject\Property\FlatText::class, // vCard 2.1 only + 'DATE' => VObject\Property\VCard\Date::class, + 'DATE-TIME' => VObject\Property\VCard\DateTime::class, + 'DATE-AND-OR-TIME' => VObject\Property\VCard\DateAndOrTime::class, // vCard only + 'FLOAT' => VObject\Property\FloatValue::class, + 'INTEGER' => VObject\Property\IntegerValue::class, + 'LANGUAGE-TAG' => VObject\Property\VCard\LanguageTag::class, + 'PHONE-NUMBER' => VObject\Property\VCard\PhoneNumber::class, // vCard 3.0 only + 'TIMESTAMP' => VObject\Property\VCard\TimeStamp::class, + 'TEXT' => VObject\Property\Text::class, + 'TIME' => VObject\Property\Time::class, + 'UNKNOWN' => VObject\Property\Unknown::class, // jCard / jCal-only. + 'URI' => VObject\Property\Uri::class, + 'URL' => VObject\Property\Uri::class, // vCard 2.1 only + 'UTC-OFFSET' => VObject\Property\UtcOffset::class, + ]; + + /** + * List of properties, and which classes they map to. + * + * @var array + */ + public static $propertyMap = [ + // vCard 2.1 properties and up + 'N' => VObject\Property\Text::class, + 'FN' => VObject\Property\FlatText::class, + 'PHOTO' => VObject\Property\Binary::class, + 'BDAY' => VObject\Property\VCard\DateAndOrTime::class, + 'ADR' => VObject\Property\Text::class, + 'LABEL' => VObject\Property\FlatText::class, // Removed in vCard 4.0 + 'TEL' => VObject\Property\FlatText::class, + 'EMAIL' => VObject\Property\FlatText::class, + 'MAILER' => VObject\Property\FlatText::class, // Removed in vCard 4.0 + 'GEO' => VObject\Property\FlatText::class, + 'TITLE' => VObject\Property\FlatText::class, + 'ROLE' => VObject\Property\FlatText::class, + 'LOGO' => VObject\Property\Binary::class, + // 'AGENT' => 'Sabre\\VObject\\Property\\', // Todo: is an embedded vCard. Probably rare, so + // not supported at the moment + 'ORG' => VObject\Property\Text::class, + 'NOTE' => VObject\Property\FlatText::class, + 'REV' => VObject\Property\VCard\TimeStamp::class, + 'SOUND' => VObject\Property\FlatText::class, + 'URL' => VObject\Property\Uri::class, + 'UID' => VObject\Property\FlatText::class, + 'VERSION' => VObject\Property\FlatText::class, + 'KEY' => VObject\Property\FlatText::class, + 'TZ' => VObject\Property\Text::class, + + // vCard 3.0 properties + 'CATEGORIES' => VObject\Property\Text::class, + 'SORT-STRING' => VObject\Property\FlatText::class, + 'PRODID' => VObject\Property\FlatText::class, + 'NICKNAME' => VObject\Property\Text::class, + 'CLASS' => VObject\Property\FlatText::class, // Removed in vCard 4.0 + + // rfc2739 properties + 'FBURL' => VObject\Property\Uri::class, + 'CAPURI' => VObject\Property\Uri::class, + 'CALURI' => VObject\Property\Uri::class, + 'CALADRURI' => VObject\Property\Uri::class, + + // rfc4770 properties + 'IMPP' => VObject\Property\Uri::class, + + // vCard 4.0 properties + 'SOURCE' => VObject\Property\Uri::class, + 'XML' => VObject\Property\FlatText::class, + 'ANNIVERSARY' => VObject\Property\VCard\DateAndOrTime::class, + 'CLIENTPIDMAP' => VObject\Property\Text::class, + 'LANG' => VObject\Property\VCard\LanguageTag::class, + 'GENDER' => VObject\Property\Text::class, + 'KIND' => VObject\Property\FlatText::class, + 'MEMBER' => VObject\Property\Uri::class, + 'RELATED' => VObject\Property\Uri::class, + + // rfc6474 properties + 'BIRTHPLACE' => VObject\Property\FlatText::class, + 'DEATHPLACE' => VObject\Property\FlatText::class, + 'DEATHDATE' => VObject\Property\VCard\DateAndOrTime::class, + + // rfc6715 properties + 'EXPERTISE' => VObject\Property\FlatText::class, + 'HOBBY' => VObject\Property\FlatText::class, + 'INTEREST' => VObject\Property\FlatText::class, + 'ORG-DIRECTORY' => VObject\Property\FlatText::class, + ]; + + /** + * Returns the current document type. + * + * @return int + */ + public function getDocumentType() + { + if (!$this->version) { + $version = (string) $this->VERSION; + + switch ($version) { + case '2.1': + $this->version = self::VCARD21; + break; + case '3.0': + $this->version = self::VCARD30; + break; + case '4.0': + $this->version = self::VCARD40; + break; + default: + // We don't want to cache the version if it's unknown, + // because we might get a version property in a bit. + return self::UNKNOWN; + } + } + + return $this->version; + } + + /** + * Converts the document to a different vcard version. + * + * Use one of the VCARD constants for the target. This method will return + * a copy of the vcard in the new version. + * + * At the moment the only supported conversion is from 3.0 to 4.0. + * + * If input and output version are identical, a clone is returned. + * + * @param int $target + * + * @return VCard + */ + public function convert($target) + { + $converter = new VObject\VCardConverter(); + + return $converter->convert($this, $target); + } + + /** + * VCards with version 2.1, 3.0 and 4.0 are found. + * + * If the VCARD doesn't know its version, 2.1 is assumed. + */ + const DEFAULT_VERSION = self::VCARD21; + + /** + * Validates the node for correctness. + * + * The following options are supported: + * Node::REPAIR - May attempt to automatically repair the problem. + * + * This method returns an array with detected problems. + * Every element has the following properties: + * + * * level - problem level. + * * message - A human-readable string describing the issue. + * * node - A reference to the problematic node. + * + * The level means: + * 1 - The issue was repaired (only happens if REPAIR was turned on) + * 2 - An inconsequential issue + * 3 - A severe issue. + * + * @param int $options + * + * @return array + */ + public function validate($options = 0) + { + $warnings = []; + + $versionMap = [ + self::VCARD21 => '2.1', + self::VCARD30 => '3.0', + self::VCARD40 => '4.0', + ]; + + $version = $this->select('VERSION'); + if (1 === count($version)) { + $version = (string) $this->VERSION; + if ('2.1' !== $version && '3.0' !== $version && '4.0' !== $version) { + $warnings[] = [ + 'level' => 3, + 'message' => 'Only vcard version 4.0 (RFC6350), version 3.0 (RFC2426) or version 2.1 (icm-vcard-2.1) are supported.', + 'node' => $this, + ]; + if ($options & self::REPAIR) { + $this->VERSION = $versionMap[self::DEFAULT_VERSION]; + } + } + if ('2.1' === $version && ($options & self::PROFILE_CARDDAV)) { + $warnings[] = [ + 'level' => 3, + 'message' => 'CardDAV servers are not allowed to accept vCard 2.1.', + 'node' => $this, + ]; + } + } + $uid = $this->select('UID'); + if (0 === count($uid)) { + if ($options & self::PROFILE_CARDDAV) { + // Required for CardDAV + $warningLevel = 3; + $message = 'vCards on CardDAV servers MUST have a UID property.'; + } else { + // Not required for regular vcards + $warningLevel = 2; + $message = 'Adding a UID to a vCard property is recommended.'; + } + if ($options & self::REPAIR) { + $this->UID = VObject\UUIDUtil::getUUID(); + $warningLevel = 1; + } + $warnings[] = [ + 'level' => $warningLevel, + 'message' => $message, + 'node' => $this, + ]; + } + + $fn = $this->select('FN'); + if (1 !== count($fn)) { + $repaired = false; + if (($options & self::REPAIR) && 0 === count($fn)) { + // We're going to try to see if we can use the contents of the + // N property. + if (isset($this->N)) { + $value = explode(';', (string) $this->N); + if (isset($value[1]) && $value[1]) { + $this->FN = $value[1].' '.$value[0]; + } else { + $this->FN = $value[0]; + } + $repaired = true; + + // Otherwise, the ORG property may work + } elseif (isset($this->ORG)) { + $this->FN = (string) $this->ORG; + $repaired = true; + + // Otherwise, the NICKNAME property may work + } elseif (isset($this->NICKNAME)) { + $this->FN = (string) $this->NICKNAME; + $repaired = true; + + // Otherwise, the EMAIL property may work + } elseif (isset($this->EMAIL)) { + $this->FN = (string) $this->EMAIL; + $repaired = true; + } + } + $warnings[] = [ + 'level' => $repaired ? 1 : 3, + 'message' => 'The FN property must appear in the VCARD component exactly 1 time', + 'node' => $this, + ]; + } + + return array_merge( + parent::validate($options), + $warnings + ); + } + + /** + * A simple list of validation rules. + * + * This is simply a list of properties, and how many times they either + * must or must not appear. + * + * Possible values per property: + * * 0 - Must not appear. + * * 1 - Must appear exactly once. + * * + - Must appear at least once. + * * * - Can appear any number of times. + * * ? - May appear, but not more than once. + * + * @var array + */ + public function getValidationRules() + { + return [ + 'ADR' => '*', + 'ANNIVERSARY' => '?', + 'BDAY' => '?', + 'CALADRURI' => '*', + 'CALURI' => '*', + 'CATEGORIES' => '*', + 'CLIENTPIDMAP' => '*', + 'EMAIL' => '*', + 'FBURL' => '*', + 'IMPP' => '*', + 'GENDER' => '?', + 'GEO' => '*', + 'KEY' => '*', + 'KIND' => '?', + 'LANG' => '*', + 'LOGO' => '*', + 'MEMBER' => '*', + 'N' => '?', + 'NICKNAME' => '*', + 'NOTE' => '*', + 'ORG' => '*', + 'PHOTO' => '*', + 'PRODID' => '?', + 'RELATED' => '*', + 'REV' => '?', + 'ROLE' => '*', + 'SOUND' => '*', + 'SOURCE' => '*', + 'TEL' => '*', + 'TITLE' => '*', + 'TZ' => '*', + 'URL' => '*', + 'VERSION' => '1', + 'XML' => '*', + + // FN is commented out, because it's already handled by the + // validate function, which may also try to repair it. + // 'FN' => '+', + 'UID' => '?', + ]; + } + + /** + * Returns a preferred field. + * + * VCards can indicate whether a field such as ADR, TEL or EMAIL is + * preferred by specifying TYPE=PREF (vcard 2.1, 3) or PREF=x (vcard 4, x + * being a number between 1 and 100). + * + * If neither of those parameters are specified, the first is returned, if + * a field with that name does not exist, null is returned. + * + * @param string $fieldName + * + * @return VObject\Property|null + */ + public function preferred($propertyName) + { + $preferred = null; + $lastPref = 101; + foreach ($this->select($propertyName) as $field) { + $pref = 101; + if (isset($field['TYPE']) && $field['TYPE']->has('PREF')) { + $pref = 1; + } elseif (isset($field['PREF'])) { + $pref = $field['PREF']->getValue(); + } + + if ($pref < $lastPref || is_null($preferred)) { + $preferred = $field; + $lastPref = $pref; + } + } + + return $preferred; + } + + /** + * Returns a property with a specific TYPE value (ADR, TEL, or EMAIL). + * + * This function will return null if the property does not exist. If there are + * multiple properties with the same TYPE value, only one will be returned. + * + * @param string $propertyName + * @param string $type + * + * @return VObject\Property|null + */ + public function getByType($propertyName, $type) + { + foreach ($this->select($propertyName) as $field) { + if (isset($field['TYPE']) && $field['TYPE']->has($type)) { + return $field; + } + } + } + + /** + * This method should return a list of default property values. + * + * @return array + */ + protected function getDefaults() + { + return [ + 'VERSION' => '4.0', + 'PRODID' => '-//Sabre//Sabre VObject '.VObject\Version::VERSION.'//EN', + 'UID' => 'sabre-vobject-'.VObject\UUIDUtil::getUUID(), + ]; + } + + /** + * This method returns an array, with the representation as it should be + * encoded in json. This is used to create jCard or jCal documents. + * + * @return array + */ + #[\ReturnTypeWillChange] + public function jsonSerialize() + { + // A vcard does not have sub-components, so we're overriding this + // method to remove that array element. + $properties = []; + + foreach ($this->children() as $child) { + $properties[] = $child->jsonSerialize(); + } + + return [ + strtolower($this->name), + $properties, + ]; + } + + /** + * This method serializes the data into XML. This is used to create xCard or + * xCal documents. + * + * @param Xml\Writer $writer XML writer + */ + public function xmlSerialize(Xml\Writer $writer): void + { + $propertiesByGroup = []; + + foreach ($this->children() as $property) { + $group = $property->group; + + if (!isset($propertiesByGroup[$group])) { + $propertiesByGroup[$group] = []; + } + + $propertiesByGroup[$group][] = $property; + } + + $writer->startElement(strtolower($this->name)); + + foreach ($propertiesByGroup as $group => $properties) { + if (!empty($group)) { + $writer->startElement('group'); + $writer->writeAttribute('name', strtolower($group)); + } + + foreach ($properties as $property) { + switch ($property->name) { + case 'VERSION': + break; + + case 'XML': + $value = $property->getParts(); + $fragment = new Xml\Element\XmlFragment($value[0]); + $writer->write($fragment); + break; + + default: + $property->xmlSerialize($writer); + break; + } + } + + if (!empty($group)) { + $writer->endElement(); + } + } + + $writer->endElement(); + } + + /** + * Returns the default class for a property name. + * + * @param string $propertyName + * + * @return string + */ + public function getClassNameForPropertyName($propertyName) + { + $className = parent::getClassNameForPropertyName($propertyName); + + // In vCard 4, BINARY no longer exists, and we need URI instead. + if (VObject\Property\Binary::class == $className && self::VCARD40 === $this->getDocumentType()) { + return VObject\Property\Uri::class; + } + + return $className; + } +} diff --git a/3rdparty/sabre/vobject/lib/Component/VEvent.php b/3rdparty/sabre/vobject/lib/Component/VEvent.php new file mode 100644 index 00000000..6ea93ed5 --- /dev/null +++ b/3rdparty/sabre/vobject/lib/Component/VEvent.php @@ -0,0 +1,140 @@ +RRULE) { + try { + $it = new EventIterator($this, null, $start->getTimezone()); + } catch (NoInstancesException $e) { + // If we've caught this exception, there are no instances + // for the event that fall into the specified time-range. + return false; + } + + $it->fastForward($start); + + // We fast-forwarded to a spot where the end-time of the + // recurrence instance exceeded the start of the requested + // time-range. + // + // If the starttime of the recurrence did not exceed the + // end of the time range as well, we have a match. + return $it->getDTStart() < $end && $it->getDTEnd() > $start; + } + + $effectiveStart = $this->DTSTART->getDateTime($start->getTimezone()); + if (isset($this->DTEND)) { + // The DTEND property is considered non inclusive. So for a 3 day + // event in july, dtstart and dtend would have to be July 1st and + // July 4th respectively. + // + // See: + // http://tools.ietf.org/html/rfc5545#page-54 + $effectiveEnd = $this->DTEND->getDateTime($end->getTimezone()); + } elseif (isset($this->DURATION)) { + $effectiveEnd = $effectiveStart->add(VObject\DateTimeParser::parseDuration($this->DURATION)); + } elseif (!$this->DTSTART->hasTime()) { + $effectiveEnd = $effectiveStart->modify('+1 day'); + } else { + $effectiveEnd = $effectiveStart; + } + + return + ($start < $effectiveEnd) && ($end > $effectiveStart) + ; + } + + /** + * This method should return a list of default property values. + * + * @return array + */ + protected function getDefaults() + { + return [ + 'UID' => 'sabre-vobject-'.VObject\UUIDUtil::getUUID(), + 'DTSTAMP' => gmdate('Ymd\\THis\\Z'), + ]; + } + + /** + * A simple list of validation rules. + * + * This is simply a list of properties, and how many times they either + * must or must not appear. + * + * Possible values per property: + * * 0 - Must not appear. + * * 1 - Must appear exactly once. + * * + - Must appear at least once. + * * * - Can appear any number of times. + * * ? - May appear, but not more than once. + * + * @var array + */ + public function getValidationRules() + { + $hasMethod = isset($this->parent->METHOD); + + return [ + 'UID' => 1, + 'DTSTAMP' => 1, + 'DTSTART' => $hasMethod ? '?' : '1', + 'CLASS' => '?', + 'CREATED' => '?', + 'DESCRIPTION' => '?', + 'GEO' => '?', + 'LAST-MODIFIED' => '?', + 'LOCATION' => '?', + 'ORGANIZER' => '?', + 'PRIORITY' => '?', + 'SEQUENCE' => '?', + 'STATUS' => '?', + 'SUMMARY' => '?', + 'TRANSP' => '?', + 'URL' => '?', + 'RECURRENCE-ID' => '?', + 'RRULE' => '?', + 'DTEND' => '?', + 'DURATION' => '?', + + 'ATTACH' => '*', + 'ATTENDEE' => '*', + 'CATEGORIES' => '*', + 'COMMENT' => '*', + 'CONTACT' => '*', + 'EXDATE' => '*', + 'REQUEST-STATUS' => '*', + 'RELATED-TO' => '*', + 'RESOURCES' => '*', + 'RDATE' => '*', + ]; + } +} diff --git a/3rdparty/sabre/vobject/lib/Component/VFreeBusy.php b/3rdparty/sabre/vobject/lib/Component/VFreeBusy.php new file mode 100644 index 00000000..fef418b5 --- /dev/null +++ b/3rdparty/sabre/vobject/lib/Component/VFreeBusy.php @@ -0,0 +1,93 @@ +select('FREEBUSY') as $freebusy) { + // We are only interested in FBTYPE=BUSY (the default), + // FBTYPE=BUSY-TENTATIVE or FBTYPE=BUSY-UNAVAILABLE. + if (isset($freebusy['FBTYPE']) && 'BUSY' !== strtoupper(substr((string) $freebusy['FBTYPE'], 0, 4))) { + continue; + } + + // The freebusy component can hold more than 1 value, separated by + // commas. + $periods = explode(',', (string) $freebusy); + + foreach ($periods as $period) { + // Every period is formatted as [start]/[end]. The start is an + // absolute UTC time, the end may be an absolute UTC time, or + // duration (relative) value. + list($busyStart, $busyEnd) = explode('/', $period); + + $busyStart = VObject\DateTimeParser::parse($busyStart); + $busyEnd = VObject\DateTimeParser::parse($busyEnd); + if ($busyEnd instanceof \DateInterval) { + $busyEnd = $busyStart->add($busyEnd); + } + + if ($start < $busyEnd && $end > $busyStart) { + return false; + } + } + } + + return true; + } + + /** + * A simple list of validation rules. + * + * This is simply a list of properties, and how many times they either + * must or must not appear. + * + * Possible values per property: + * * 0 - Must not appear. + * * 1 - Must appear exactly once. + * * + - Must appear at least once. + * * * - Can appear any number of times. + * * ? - May appear, but not more than once. + * + * @var array + */ + public function getValidationRules() + { + return [ + 'UID' => 1, + 'DTSTAMP' => 1, + + 'CONTACT' => '?', + 'DTSTART' => '?', + 'DTEND' => '?', + 'ORGANIZER' => '?', + 'URL' => '?', + + 'ATTENDEE' => '*', + 'COMMENT' => '*', + 'FREEBUSY' => '*', + 'REQUEST-STATUS' => '*', + ]; + } +} diff --git a/3rdparty/sabre/vobject/lib/Component/VJournal.php b/3rdparty/sabre/vobject/lib/Component/VJournal.php new file mode 100644 index 00000000..9b7f1b87 --- /dev/null +++ b/3rdparty/sabre/vobject/lib/Component/VJournal.php @@ -0,0 +1,101 @@ +DTSTART) ? $this->DTSTART->getDateTime() : null; + if ($dtstart) { + $effectiveEnd = $dtstart; + if (!$this->DTSTART->hasTime()) { + $effectiveEnd = $effectiveEnd->modify('+1 day'); + } + + return $start <= $effectiveEnd && $end > $dtstart; + } + + return false; + } + + /** + * A simple list of validation rules. + * + * This is simply a list of properties, and how many times they either + * must or must not appear. + * + * Possible values per property: + * * 0 - Must not appear. + * * 1 - Must appear exactly once. + * * + - Must appear at least once. + * * * - Can appear any number of times. + * * ? - May appear, but not more than once. + * + * @var array + */ + public function getValidationRules() + { + return [ + 'UID' => 1, + 'DTSTAMP' => 1, + + 'CLASS' => '?', + 'CREATED' => '?', + 'DTSTART' => '?', + 'LAST-MODIFIED' => '?', + 'ORGANIZER' => '?', + 'RECURRENCE-ID' => '?', + 'SEQUENCE' => '?', + 'STATUS' => '?', + 'SUMMARY' => '?', + 'URL' => '?', + + 'RRULE' => '?', + + 'ATTACH' => '*', + 'ATTENDEE' => '*', + 'CATEGORIES' => '*', + 'COMMENT' => '*', + 'CONTACT' => '*', + 'DESCRIPTION' => '*', + 'EXDATE' => '*', + 'RELATED-TO' => '*', + 'RDATE' => '*', + ]; + } + + /** + * This method should return a list of default property values. + * + * @return array + */ + protected function getDefaults() + { + return [ + 'UID' => 'sabre-vobject-'.VObject\UUIDUtil::getUUID(), + 'DTSTAMP' => gmdate('Ymd\\THis\\Z'), + ]; + } +} diff --git a/3rdparty/sabre/vobject/lib/Component/VTimeZone.php b/3rdparty/sabre/vobject/lib/Component/VTimeZone.php new file mode 100644 index 00000000..21c06237 --- /dev/null +++ b/3rdparty/sabre/vobject/lib/Component/VTimeZone.php @@ -0,0 +1,63 @@ +TZID, $this->root); + } + + /** + * A simple list of validation rules. + * + * This is simply a list of properties, and how many times they either + * must or must not appear. + * + * Possible values per property: + * * 0 - Must not appear. + * * 1 - Must appear exactly once. + * * + - Must appear at least once. + * * * - Can appear any number of times. + * * ? - May appear, but not more than once. + * + * @var array + */ + public function getValidationRules() + { + return [ + 'TZID' => 1, + + 'LAST-MODIFIED' => '?', + 'TZURL' => '?', + + // At least 1 STANDARD or DAYLIGHT must appear. + // + // The validator is not specific yet to pick this up, so these + // rules are too loose. + 'STANDARD' => '*', + 'DAYLIGHT' => '*', + ]; + } +} diff --git a/3rdparty/sabre/vobject/lib/Component/VTodo.php b/3rdparty/sabre/vobject/lib/Component/VTodo.php new file mode 100644 index 00000000..6f022ba6 --- /dev/null +++ b/3rdparty/sabre/vobject/lib/Component/VTodo.php @@ -0,0 +1,181 @@ +DTSTART) ? $this->DTSTART->getDateTime() : null; + $duration = isset($this->DURATION) ? VObject\DateTimeParser::parseDuration($this->DURATION) : null; + $due = isset($this->DUE) ? $this->DUE->getDateTime() : null; + $completed = isset($this->COMPLETED) ? $this->COMPLETED->getDateTime() : null; + $created = isset($this->CREATED) ? $this->CREATED->getDateTime() : null; + + if ($dtstart) { + if ($duration) { + $effectiveEnd = $dtstart->add($duration); + + return $start <= $effectiveEnd && $end > $dtstart; + } elseif ($due) { + return + ($start < $due || $start <= $dtstart) && + ($end > $dtstart || $end >= $due); + } else { + return $start <= $dtstart && $end > $dtstart; + } + } + if ($due) { + return $start < $due && $end >= $due; + } + if ($completed && $created) { + return + ($start <= $created || $start <= $completed) && + ($end >= $created || $end >= $completed); + } + if ($completed) { + return $start <= $completed && $end >= $completed; + } + if ($created) { + return $end > $created; + } + + return true; + } + + /** + * A simple list of validation rules. + * + * This is simply a list of properties, and how many times they either + * must or must not appear. + * + * Possible values per property: + * * 0 - Must not appear. + * * 1 - Must appear exactly once. + * * + - Must appear at least once. + * * * - Can appear any number of times. + * * ? - May appear, but not more than once. + * + * @var array + */ + public function getValidationRules() + { + return [ + 'UID' => 1, + 'DTSTAMP' => 1, + + 'CLASS' => '?', + 'COMPLETED' => '?', + 'CREATED' => '?', + 'DESCRIPTION' => '?', + 'DTSTART' => '?', + 'GEO' => '?', + 'LAST-MODIFIED' => '?', + 'LOCATION' => '?', + 'ORGANIZER' => '?', + 'PERCENT' => '?', + 'PRIORITY' => '?', + 'RECURRENCE-ID' => '?', + 'SEQUENCE' => '?', + 'STATUS' => '?', + 'SUMMARY' => '?', + 'URL' => '?', + + 'RRULE' => '?', + 'DUE' => '?', + 'DURATION' => '?', + + 'ATTACH' => '*', + 'ATTENDEE' => '*', + 'CATEGORIES' => '*', + 'COMMENT' => '*', + 'CONTACT' => '*', + 'EXDATE' => '*', + 'REQUEST-STATUS' => '*', + 'RELATED-TO' => '*', + 'RESOURCES' => '*', + 'RDATE' => '*', + ]; + } + + /** + * Validates the node for correctness. + * + * The following options are supported: + * Node::REPAIR - May attempt to automatically repair the problem. + * + * This method returns an array with detected problems. + * Every element has the following properties: + * + * * level - problem level. + * * message - A human-readable string describing the issue. + * * node - A reference to the problematic node. + * + * The level means: + * 1 - The issue was repaired (only happens if REPAIR was turned on) + * 2 - An inconsequential issue + * 3 - A severe issue. + * + * @param int $options + * + * @return array + */ + public function validate($options = 0) + { + $result = parent::validate($options); + if (isset($this->DUE) && isset($this->DTSTART)) { + $due = $this->DUE; + $dtStart = $this->DTSTART; + + if ($due->getValueType() !== $dtStart->getValueType()) { + $result[] = [ + 'level' => 3, + 'message' => 'The value type (DATE or DATE-TIME) must be identical for DUE and DTSTART', + 'node' => $due, + ]; + } elseif ($due->getDateTime() < $dtStart->getDateTime()) { + $result[] = [ + 'level' => 3, + 'message' => 'DUE must occur after DTSTART', + 'node' => $due, + ]; + } + } + + return $result; + } + + /** + * This method should return a list of default property values. + * + * @return array + */ + protected function getDefaults() + { + return [ + 'UID' => 'sabre-vobject-'.VObject\UUIDUtil::getUUID(), + 'DTSTAMP' => date('Ymd\\THis\\Z'), + ]; + } +} diff --git a/3rdparty/sabre/vobject/lib/DateTimeParser.php b/3rdparty/sabre/vobject/lib/DateTimeParser.php new file mode 100644 index 00000000..69072ef8 --- /dev/null +++ b/3rdparty/sabre/vobject/lib/DateTimeParser.php @@ -0,0 +1,560 @@ +\+|-)?P((?\d+)W)?((?\d+)D)?(T((?\d+)H)?((?\d+)M)?((?\d+)S)?)?$/', $duration, $matches); + if (!$result) { + throw new InvalidDataException('The supplied iCalendar duration value is incorrect: '.$duration); + } + + if (!$asString) { + $invert = false; + + if (isset($matches['plusminus']) && '-' === $matches['plusminus']) { + $invert = true; + } + + $parts = [ + 'week', + 'day', + 'hour', + 'minute', + 'second', + ]; + + foreach ($parts as $part) { + $matches[$part] = isset($matches[$part]) && $matches[$part] ? (int) $matches[$part] : 0; + } + + // We need to re-construct the $duration string, because weeks and + // days are not supported by DateInterval in the same string. + $duration = 'P'; + $days = $matches['day']; + + if ($matches['week']) { + $days += $matches['week'] * 7; + } + + if ($days) { + $duration .= $days.'D'; + } + + if ($matches['minute'] || $matches['second'] || $matches['hour']) { + $duration .= 'T'; + + if ($matches['hour']) { + $duration .= $matches['hour'].'H'; + } + + if ($matches['minute']) { + $duration .= $matches['minute'].'M'; + } + + if ($matches['second']) { + $duration .= $matches['second'].'S'; + } + } + + if ('P' === $duration) { + $duration = 'PT0S'; + } + + $iv = new DateInterval($duration); + + if ($invert) { + $iv->invert = true; + } + + return $iv; + } + + $parts = [ + 'week', + 'day', + 'hour', + 'minute', + 'second', + ]; + + $newDur = ''; + + foreach ($parts as $part) { + if (isset($matches[$part]) && $matches[$part]) { + $newDur .= ' '.$matches[$part].' '.$part.'s'; + } + } + + $newDur = ('-' === $matches['plusminus'] ? '-' : '+').trim($newDur); + + if ('+' === $newDur) { + $newDur = '+0 seconds'; + } + + return $newDur; + } + + /** + * Parses either a Date or DateTime, or Duration value. + * + * @param string $date + * @param DateTimeZone|string $referenceTz + * + * @return DateTimeImmutable|DateInterval + */ + public static function parse($date, $referenceTz = null) + { + if ('P' === $date[0] || ('-' === $date[0] && 'P' === $date[1])) { + return self::parseDuration($date); + } elseif (8 === strlen($date)) { + return self::parseDate($date, $referenceTz); + } else { + return self::parseDateTime($date, $referenceTz); + } + } + + /** + * This method parses a vCard date and or time value. + * + * This can be used for the DATE, DATE-TIME, TIMESTAMP and + * DATE-AND-OR-TIME value. + * + * This method returns an array, not a DateTime value. + * + * The elements in the array are in the following order: + * year, month, date, hour, minute, second, timezone + * + * Almost any part of the string may be omitted. It's for example legal to + * just specify seconds, leave out the year, etc. + * + * Timezone is either returned as 'Z' or as '+0800' + * + * For any non-specified values null is returned. + * + * List of date formats that are supported: + * YYYY + * YYYY-MM + * YYYYMMDD + * --MMDD + * ---DD + * + * YYYY-MM-DD + * --MM-DD + * ---DD + * + * List of supported time formats: + * + * HH + * HHMM + * HHMMSS + * -MMSS + * --SS + * + * HH + * HH:MM + * HH:MM:SS + * -MM:SS + * --SS + * + * A full basic-format date-time string looks like : + * 20130603T133901 + * + * A full extended-format date-time string looks like : + * 2013-06-03T13:39:01 + * + * Times may be postfixed by a timezone offset. This can be either 'Z' for + * UTC, or a string like -0500 or +1100. + * + * @param string $date + * + * @return array + */ + public static function parseVCardDateTime($date) + { + $regex = '/^ + (?: # date part + (?: + (?: (? [0-9]{4}) (?: -)?| --) + (? [0-9]{2})? + |---) + (? [0-9]{2})? + )? + (?:T # time part + (? [0-9]{2} | -) + (? [0-9]{2} | -)? + (? [0-9]{2})? + + (?: \.[0-9]{3})? # milliseconds + (?P # timezone offset + + Z | (?: \+|-)(?: [0-9]{4}) + + )? + + )? + $/x'; + + if (!preg_match($regex, $date, $matches)) { + // Attempting to parse the extended format. + $regex = '/^ + (?: # date part + (?: (? [0-9]{4}) - | -- ) + (? [0-9]{2}) - + (? [0-9]{2}) + )? + (?:T # time part + + (?: (? [0-9]{2}) : | -) + (?: (? [0-9]{2}) : | -)? + (? [0-9]{2})? + + (?: \.[0-9]{3})? # milliseconds + (?P # timezone offset + + Z | (?: \+|-)(?: [0-9]{2}:[0-9]{2}) + + )? + + )? + $/x'; + + if (!preg_match($regex, $date, $matches)) { + throw new InvalidDataException('Invalid vCard date-time string: '.$date); + } + } + $parts = [ + 'year', + 'month', + 'date', + 'hour', + 'minute', + 'second', + 'timezone', + ]; + + $result = []; + foreach ($parts as $part) { + if (empty($matches[$part])) { + $result[$part] = null; + } elseif ('-' === $matches[$part] || '--' === $matches[$part]) { + $result[$part] = null; + } else { + $result[$part] = $matches[$part]; + } + } + + return $result; + } + + /** + * This method parses a vCard TIME value. + * + * This method returns an array, not a DateTime value. + * + * The elements in the array are in the following order: + * hour, minute, second, timezone + * + * Almost any part of the string may be omitted. It's for example legal to + * just specify seconds, leave out the hour etc. + * + * Timezone is either returned as 'Z' or as '+08:00' + * + * For any non-specified values null is returned. + * + * List of supported time formats: + * + * HH + * HHMM + * HHMMSS + * -MMSS + * --SS + * + * HH + * HH:MM + * HH:MM:SS + * -MM:SS + * --SS + * + * A full basic-format time string looks like : + * 133901 + * + * A full extended-format time string looks like : + * 13:39:01 + * + * Times may be postfixed by a timezone offset. This can be either 'Z' for + * UTC, or a string like -0500 or +11:00. + * + * @param string $date + * + * @return array + */ + public static function parseVCardTime($date) + { + $regex = '/^ + (? [0-9]{2} | -) + (? [0-9]{2} | -)? + (? [0-9]{2})? + + (?: \.[0-9]{3})? # milliseconds + (?P # timezone offset + + Z | (?: \+|-)(?: [0-9]{4}) + + )? + $/x'; + + if (!preg_match($regex, $date, $matches)) { + // Attempting to parse the extended format. + $regex = '/^ + (?: (? [0-9]{2}) : | -) + (?: (? [0-9]{2}) : | -)? + (? [0-9]{2})? + + (?: \.[0-9]{3})? # milliseconds + (?P # timezone offset + + Z | (?: \+|-)(?: [0-9]{2}:[0-9]{2}) + + )? + $/x'; + + if (!preg_match($regex, $date, $matches)) { + throw new InvalidDataException('Invalid vCard time string: '.$date); + } + } + $parts = [ + 'hour', + 'minute', + 'second', + 'timezone', + ]; + + $result = []; + foreach ($parts as $part) { + if (empty($matches[$part])) { + $result[$part] = null; + } elseif ('-' === $matches[$part]) { + $result[$part] = null; + } else { + $result[$part] = $matches[$part]; + } + } + + return $result; + } + + /** + * This method parses a vCard date and or time value. + * + * This can be used for the DATE, DATE-TIME and + * DATE-AND-OR-TIME value. + * + * This method returns an array, not a DateTime value. + * The elements in the array are in the following order: + * year, month, date, hour, minute, second, timezone + * Almost any part of the string may be omitted. It's for example legal to + * just specify seconds, leave out the year, etc. + * + * Timezone is either returned as 'Z' or as '+0800' + * + * For any non-specified values null is returned. + * + * List of date formats that are supported: + * 20150128 + * 2015-01 + * --01 + * --0128 + * ---28 + * + * List of supported time formats: + * 13 + * 1353 + * 135301 + * -53 + * -5301 + * --01 (unreachable, see the tests) + * --01Z + * --01+1234 + * + * List of supported date-time formats: + * 20150128T13 + * --0128T13 + * ---28T13 + * ---28T1353 + * ---28T135301 + * ---28T13Z + * ---28T13+1234 + * + * See the regular expressions for all the possible patterns. + * + * Times may be postfixed by a timezone offset. This can be either 'Z' for + * UTC, or a string like -0500 or +1100. + * + * @param string $date + * + * @return array + */ + public static function parseVCardDateAndOrTime($date) + { + // \d{8}|\d{4}-\d\d|--\d\d(\d\d)?|---\d\d + $valueDate = '/^(?J)(?:'. + '(?\d{4})(?\d\d)(?\d\d)'. + '|(?\d{4})-(?\d\d)'. + '|--(?\d\d)(?\d\d)?'. + '|---(?\d\d)'. + ')$/'; + + // (\d\d(\d\d(\d\d)?)?|-\d\d(\d\d)?|--\d\d)(Z|[+\-]\d\d(\d\d)?)? + $valueTime = '/^(?J)(?:'. + '((?\d\d)((?\d\d)(?\d\d)?)?'. + '|-(?\d\d)(?\d\d)?'. + '|--(?\d\d))'. + '(?(Z|[+\-]\d\d(\d\d)?))?'. + ')$/'; + + // (\d{8}|--\d{4}|---\d\d)T\d\d(\d\d(\d\d)?)?(Z|[+\-]\d\d(\d\d?)? + $valueDateTime = '/^(?:'. + '((?\d{4})(?\d\d)(?\d\d)'. + '|--(?\d\d)(?\d\d)'. + '|---(?\d\d))'. + 'T'. + '(?\d\d)((?\d\d)(?\d\d)?)?'. + '(?(Z|[+\-]\d\d(\d\d?)))?'. + ')$/'; + + // date-and-or-time is date | date-time | time + // in this strict order. + + if (0 === preg_match($valueDate, $date, $matches) + && 0 === preg_match($valueDateTime, $date, $matches) + && 0 === preg_match($valueTime, $date, $matches)) { + throw new InvalidDataException('Invalid vCard date-time string: '.$date); + } + + $parts = [ + 'year' => null, + 'month' => null, + 'date' => null, + 'hour' => null, + 'minute' => null, + 'second' => null, + 'timezone' => null, + ]; + + // The $valueDateTime expression has a bug with (?J) so we simulate it. + $parts['date0'] = &$parts['date']; + $parts['date1'] = &$parts['date']; + $parts['date2'] = &$parts['date']; + $parts['month0'] = &$parts['month']; + $parts['month1'] = &$parts['month']; + $parts['year0'] = &$parts['year']; + + foreach ($parts as $part => &$value) { + if (!empty($matches[$part])) { + $value = $matches[$part]; + } + } + + unset($parts['date0']); + unset($parts['date1']); + unset($parts['date2']); + unset($parts['month0']); + unset($parts['month1']); + unset($parts['year0']); + + return $parts; + } +} diff --git a/3rdparty/sabre/vobject/lib/Document.php b/3rdparty/sabre/vobject/lib/Document.php new file mode 100644 index 00000000..d2131f47 --- /dev/null +++ b/3rdparty/sabre/vobject/lib/Document.php @@ -0,0 +1,262 @@ +value syntax, in which case + * properties will automatically be created, or you can just pass a list of + * Component and Property object. + * + * By default, a set of sensible values will be added to the component. For + * an iCalendar object, this may be something like CALSCALE:GREGORIAN. To + * ensure that this does not happen, set $defaults to false. + * + * @param string $name + * @param array $children + * @param bool $defaults + * + * @return Component + */ + public function createComponent($name, ?array $children = null, $defaults = true) + { + $name = strtoupper($name); + $class = Component::class; + + if (isset(static::$componentMap[$name])) { + $class = static::$componentMap[$name]; + } + if (is_null($children)) { + $children = []; + } + + return new $class($this, $name, $children, $defaults); + } + + /** + * Factory method for creating new properties. + * + * This method automatically searches for the correct property class, based + * on its name. + * + * You can specify the parameters either in key=>value syntax, in which case + * parameters will automatically be created, or you can just pass a list of + * Parameter objects. + * + * @param string $name + * @param mixed $value + * @param array $parameters + * @param string $valueType Force a specific valuetype, such as URI or TEXT + */ + public function createProperty($name, $value = null, ?array $parameters = null, $valueType = null, ?int $lineIndex = null, ?string $lineString = null): Property + { + // If there's a . in the name, it means it's prefixed by a groupname. + if (false !== ($i = strpos($name, '.'))) { + $group = substr($name, 0, $i); + $name = strtoupper(substr($name, $i + 1)); + } else { + $name = strtoupper($name); + $group = null; + } + + $class = null; + + if ($valueType) { + // The valueType argument comes first to figure out the correct + // class. + $class = $this->getClassNameForPropertyValue($valueType); + } + + if (is_null($class)) { + // If a VALUE parameter is supplied, we should use that. + if (isset($parameters['VALUE'])) { + $class = $this->getClassNameForPropertyValue($parameters['VALUE']); + if (is_null($class)) { + throw new InvalidDataException('Unsupported VALUE parameter for '.$name.' property. You supplied "'.$parameters['VALUE'].'"'); + } + } else { + $class = $this->getClassNameForPropertyName($name); + } + } + if (is_null($parameters)) { + $parameters = []; + } + + return new $class($this, $name, $value, $parameters, $group, $lineIndex, $lineString); + } + + /** + * This method returns a full class-name for a value parameter. + * + * For instance, DTSTART may have VALUE=DATE. In that case we will look in + * our valueMap table and return the appropriate class name. + * + * This method returns null if we don't have a specialized class. + * + * @param string $valueParam + * + * @return string|null + */ + public function getClassNameForPropertyValue($valueParam) + { + $valueParam = strtoupper($valueParam); + if (isset(static::$valueMap[$valueParam])) { + return static::$valueMap[$valueParam]; + } + } + + /** + * Returns the default class for a property name. + * + * @param string $propertyName + * + * @return string + */ + public function getClassNameForPropertyName($propertyName) + { + if (isset(static::$propertyMap[$propertyName])) { + return static::$propertyMap[$propertyName]; + } else { + return Property\Unknown::class; + } + } +} diff --git a/3rdparty/sabre/vobject/lib/ElementList.php b/3rdparty/sabre/vobject/lib/ElementList.php new file mode 100644 index 00000000..86051264 --- /dev/null +++ b/3rdparty/sabre/vobject/lib/ElementList.php @@ -0,0 +1,48 @@ +vevent where there's multiple VEVENT objects. + * + * @copyright Copyright (C) fruux GmbH (https://fruux.com/) + * @author Evert Pot (http://evertpot.com/) + * @license http://sabre.io/license/ Modified BSD License + */ +class ElementList extends ArrayIterator +{ + /* {{{ ArrayAccess Interface */ + + /** + * Sets an item through ArrayAccess. + * + * @param int $offset + * @param mixed $value + */ + #[\ReturnTypeWillChange] + public function offsetSet($offset, $value) + { + throw new LogicException('You can not add new objects to an ElementList'); + } + + /** + * Sets an item through ArrayAccess. + * + * This method just forwards the request to the inner iterator + * + * @param int $offset + */ + #[\ReturnTypeWillChange] + public function offsetUnset($offset) + { + throw new LogicException('You can not remove objects from an ElementList'); + } + + /* }}} */ +} diff --git a/3rdparty/sabre/vobject/lib/EofException.php b/3rdparty/sabre/vobject/lib/EofException.php new file mode 100644 index 00000000..837af7eb --- /dev/null +++ b/3rdparty/sabre/vobject/lib/EofException.php @@ -0,0 +1,15 @@ +start = $start; + $this->end = $end; + $this->data = []; + + $this->data[] = [ + 'start' => $this->start, + 'end' => $this->end, + 'type' => 'FREE', + ]; + } + + /** + * Adds free or busytime to the data. + * + * @param int $start + * @param int $end + * @param string $type FREE, BUSY, BUSY-UNAVAILABLE or BUSY-TENTATIVE + */ + public function add($start, $end, $type) + { + if ($start > $this->end || $end < $this->start) { + // This new data is outside our timerange. + return; + } + + if ($start < $this->start) { + // The item starts before our requested time range + $start = $this->start; + } + if ($end > $this->end) { + // The item ends after our requested time range + $end = $this->end; + } + + // Finding out where we need to insert the new item. + $currentIndex = 0; + while ($start > $this->data[$currentIndex]['end']) { + ++$currentIndex; + } + + // The standard insertion point will be one _after_ the first + // overlapping item. + $insertStartIndex = $currentIndex + 1; + + $newItem = [ + 'start' => $start, + 'end' => $end, + 'type' => $type, + ]; + + $precedingItem = $this->data[$insertStartIndex - 1]; + if ($this->data[$insertStartIndex - 1]['start'] === $start) { + // The old item starts at the exact same point as the new item. + --$insertStartIndex; + } + + // Now we know where to insert the item, we need to know where it + // starts overlapping with items on the tail end. We need to start + // looking one item before the insertStartIndex, because it's possible + // that the new item 'sits inside' the previous old item. + if ($insertStartIndex > 0) { + $currentIndex = $insertStartIndex - 1; + } else { + $currentIndex = 0; + } + + while ($end > $this->data[$currentIndex]['end']) { + ++$currentIndex; + } + + // What we are about to insert into the array + $newItems = [ + $newItem, + ]; + + // This is the amount of items that are completely overwritten by the + // new item. + $itemsToDelete = $currentIndex - $insertStartIndex; + if ($this->data[$currentIndex]['end'] <= $end) { + ++$itemsToDelete; + } + + // If itemsToDelete was -1, it means that the newly inserted item is + // actually sitting inside an existing one. This means we need to split + // the item at the current position in two and insert the new item in + // between. + if (-1 === $itemsToDelete) { + $itemsToDelete = 0; + if ($newItem['end'] < $precedingItem['end']) { + $newItems[] = [ + 'start' => $newItem['end'] + 1, + 'end' => $precedingItem['end'], + 'type' => $precedingItem['type'], + ]; + } + } + + array_splice( + $this->data, + $insertStartIndex, + $itemsToDelete, + $newItems + ); + + $doMerge = false; + $mergeOffset = $insertStartIndex; + $mergeItem = $newItem; + $mergeDelete = 1; + + if (isset($this->data[$insertStartIndex - 1])) { + // Updating the start time of the previous item. + $this->data[$insertStartIndex - 1]['end'] = $start; + + // If the previous and the current are of the same type, we can + // merge them into one item. + if ($this->data[$insertStartIndex - 1]['type'] === $this->data[$insertStartIndex]['type']) { + $doMerge = true; + --$mergeOffset; + ++$mergeDelete; + $mergeItem['start'] = $this->data[$insertStartIndex - 1]['start']; + } + } + if (isset($this->data[$insertStartIndex + 1])) { + // Updating the start time of the next item. + $this->data[$insertStartIndex + 1]['start'] = $end; + + // If the next and the current are of the same type, we can + // merge them into one item. + if ($this->data[$insertStartIndex + 1]['type'] === $this->data[$insertStartIndex]['type']) { + $doMerge = true; + ++$mergeDelete; + $mergeItem['end'] = $this->data[$insertStartIndex + 1]['end']; + } + } + if ($doMerge) { + array_splice( + $this->data, + $mergeOffset, + $mergeDelete, + [$mergeItem] + ); + } + } + + public function getData() + { + return $this->data; + } +} diff --git a/3rdparty/sabre/vobject/lib/FreeBusyGenerator.php b/3rdparty/sabre/vobject/lib/FreeBusyGenerator.php new file mode 100644 index 00000000..56ae166f --- /dev/null +++ b/3rdparty/sabre/vobject/lib/FreeBusyGenerator.php @@ -0,0 +1,549 @@ +setTimeRange($start, $end); + + if ($objects) { + $this->setObjects($objects); + } + if (is_null($timeZone)) { + $timeZone = new DateTimeZone('UTC'); + } + $this->setTimeZone($timeZone); + } + + /** + * Sets the VCALENDAR object. + * + * If this is set, it will not be generated for you. You are responsible + * for setting things like the METHOD, CALSCALE, VERSION, etc.. + * + * The VFREEBUSY object will be automatically added though. + */ + public function setBaseObject(Document $vcalendar) + { + $this->baseObject = $vcalendar; + } + + /** + * Sets a VAVAILABILITY document. + */ + public function setVAvailability(Document $vcalendar) + { + $this->vavailability = $vcalendar; + } + + /** + * Sets the input objects. + * + * You must either specify a vcalendar object as a string, or as the parse + * Component. + * It's also possible to specify multiple objects as an array. + * + * @param mixed $objects + */ + public function setObjects($objects) + { + if (!is_array($objects)) { + $objects = [$objects]; + } + + $this->objects = []; + foreach ($objects as $object) { + if (is_string($object) || is_resource($object)) { + $this->objects[] = Reader::read($object); + } elseif ($object instanceof Component) { + $this->objects[] = $object; + } else { + throw new \InvalidArgumentException('You can only pass strings or \\Sabre\\VObject\\Component arguments to setObjects'); + } + } + } + + /** + * Sets the time range. + * + * Any freebusy object falling outside of this time range will be ignored. + * + * @param DateTimeInterface $start + * @param DateTimeInterface $end + */ + public function setTimeRange(?DateTimeInterface $start = null, ?DateTimeInterface $end = null) + { + if (!$start) { + $start = new DateTimeImmutable(Settings::$minDate); + } + if (!$end) { + $end = new DateTimeImmutable(Settings::$maxDate); + } + $this->start = $start; + $this->end = $end; + } + + /** + * Sets the reference timezone for floating times. + */ + public function setTimeZone(DateTimeZone $timeZone) + { + $this->timeZone = $timeZone; + } + + /** + * Parses the input data and returns a correct VFREEBUSY object, wrapped in + * a VCALENDAR. + * + * @return Component + */ + public function getResult() + { + $fbData = new FreeBusyData( + $this->start->getTimeStamp(), + $this->end->getTimeStamp() + ); + if ($this->vavailability) { + $this->calculateAvailability($fbData, $this->vavailability); + } + + $this->calculateBusy($fbData, $this->objects); + + return $this->generateFreeBusyCalendar($fbData); + } + + /** + * This method takes a VAVAILABILITY component and figures out all the + * available times. + */ + protected function calculateAvailability(FreeBusyData $fbData, VCalendar $vavailability) + { + $vavailComps = iterator_to_array($vavailability->VAVAILABILITY); + usort( + $vavailComps, + function ($a, $b) { + // We need to order the components by priority. Priority 1 + // comes first, up until priority 9. Priority 0 comes after + // priority 9. No priority implies priority 0. + // + // Yes, I'm serious. + $priorityA = isset($a->PRIORITY) ? (int) $a->PRIORITY->getValue() : 0; + $priorityB = isset($b->PRIORITY) ? (int) $b->PRIORITY->getValue() : 0; + + if (0 === $priorityA) { + $priorityA = 10; + } + if (0 === $priorityB) { + $priorityB = 10; + } + + return $priorityA - $priorityB; + } + ); + + // Now we go over all the VAVAILABILITY components and figure if + // there's any we don't need to consider. + // + // This is can be because of one of two reasons: either the + // VAVAILABILITY component falls outside the time we are interested in, + // or a different VAVAILABILITY component with a higher priority has + // already completely covered the time-range. + $old = $vavailComps; + $new = []; + + foreach ($old as $vavail) { + list($compStart, $compEnd) = $vavail->getEffectiveStartEnd(); + + // We don't care about datetimes that are earlier or later than the + // start and end of the freebusy report, so this gets normalized + // first. + if (is_null($compStart) || $compStart < $this->start) { + $compStart = $this->start; + } + if (is_null($compEnd) || $compEnd > $this->end) { + $compEnd = $this->end; + } + + // If the item fell out of the timerange, we can just skip it. + if ($compStart > $this->end || $compEnd < $this->start) { + continue; + } + + // Going through our existing list of components to see if there's + // a higher priority component that already fully covers this one. + foreach ($new as $higherVavail) { + list($higherStart, $higherEnd) = $higherVavail->getEffectiveStartEnd(); + if ( + (is_null($higherStart) || $higherStart < $compStart) && + (is_null($higherEnd) || $higherEnd > $compEnd) + ) { + // Component is fully covered by a higher priority + // component. We can skip this component. + continue 2; + } + } + + // We're keeping it! + $new[] = $vavail; + } + + // Lastly, we need to traverse the remaining components and fill in the + // freebusydata slots. + // + // We traverse the components in reverse, because we want the higher + // priority components to override the lower ones. + foreach (array_reverse($new) as $vavail) { + $busyType = isset($vavail->BUSYTYPE) ? strtoupper($vavail->BUSYTYPE) : 'BUSY-UNAVAILABLE'; + list($vavailStart, $vavailEnd) = $vavail->getEffectiveStartEnd(); + + // Making the component size no larger than the requested free-busy + // report range. + if (!$vavailStart || $vavailStart < $this->start) { + $vavailStart = $this->start; + } + if (!$vavailEnd || $vavailEnd > $this->end) { + $vavailEnd = $this->end; + } + + // Marking the entire time range of the VAVAILABILITY component as + // busy. + $fbData->add( + $vavailStart->getTimeStamp(), + $vavailEnd->getTimeStamp(), + $busyType + ); + + // Looping over the AVAILABLE components. + if (isset($vavail->AVAILABLE)) { + foreach ($vavail->AVAILABLE as $available) { + list($availStart, $availEnd) = $available->getEffectiveStartEnd(); + $fbData->add( + $availStart->getTimeStamp(), + $availEnd->getTimeStamp(), + 'FREE' + ); + + if ($available->RRULE) { + // Our favourite thing: recurrence!! + + $rruleIterator = new Recur\RRuleIterator( + $available->RRULE->getValue(), + $availStart + ); + $rruleIterator->fastForward($vavailStart); + + $startEndDiff = $availStart->diff($availEnd); + + while ($rruleIterator->valid()) { + $recurStart = $rruleIterator->current(); + $recurEnd = $recurStart->add($startEndDiff); + + if ($recurStart > $vavailEnd) { + // We're beyond the legal timerange. + break; + } + + if ($recurEnd > $vavailEnd) { + // Truncating the end if it exceeds the + // VAVAILABILITY end. + $recurEnd = $vavailEnd; + } + + $fbData->add( + $recurStart->getTimeStamp(), + $recurEnd->getTimeStamp(), + 'FREE' + ); + + $rruleIterator->next(); + } + } + } + } + } + } + + /** + * This method takes an array of iCalendar objects and applies its busy + * times on fbData. + * + * @param VCalendar[] $objects + */ + protected function calculateBusy(FreeBusyData $fbData, array $objects) + { + foreach ($objects as $key => $object) { + foreach ($object->getBaseComponents() as $component) { + switch ($component->name) { + case 'VEVENT': + $FBTYPE = 'BUSY'; + if (isset($component->TRANSP) && ('TRANSPARENT' === strtoupper($component->TRANSP))) { + break; + } + if (isset($component->STATUS)) { + $status = strtoupper($component->STATUS); + if ('CANCELLED' === $status) { + break; + } + if ('TENTATIVE' === $status) { + $FBTYPE = 'BUSY-TENTATIVE'; + } + } + + $times = []; + + if ($component->RRULE) { + try { + $iterator = new EventIterator($object, (string) $component->UID, $this->timeZone); + } catch (NoInstancesException $e) { + // This event is recurring, but it doesn't have a single + // instance. We are skipping this event from the output + // entirely. + unset($this->objects[$key]); + break; + } + + if ($this->start) { + $iterator->fastForward($this->start); + } + + $maxRecurrences = Settings::$maxRecurrences; + + while ($iterator->valid() && --$maxRecurrences) { + $startTime = $iterator->getDTStart(); + if ($this->end && $startTime > $this->end) { + break; + } + $times[] = [ + $iterator->getDTStart(), + $iterator->getDTEnd(), + ]; + + $iterator->next(); + } + } else { + $startTime = $component->DTSTART->getDateTime($this->timeZone); + if ($this->end && $startTime > $this->end) { + break; + } + $endTime = null; + if (isset($component->DTEND)) { + $endTime = $component->DTEND->getDateTime($this->timeZone); + } elseif (isset($component->DURATION)) { + $duration = DateTimeParser::parseDuration((string) $component->DURATION); + $endTime = clone $startTime; + $endTime = $endTime->add($duration); + } elseif (!$component->DTSTART->hasTime()) { + $endTime = clone $startTime; + $endTime = $endTime->modify('+1 day'); + } else { + // The event had no duration (0 seconds) + break; + } + + $times[] = [$startTime, $endTime]; + } + + foreach ($times as $time) { + if ($this->end && $time[0] > $this->end) { + break; + } + if ($this->start && $time[1] < $this->start) { + break; + } + + $fbData->add( + $time[0]->getTimeStamp(), + $time[1]->getTimeStamp(), + $FBTYPE + ); + } + break; + + case 'VFREEBUSY': + foreach ($component->FREEBUSY as $freebusy) { + $fbType = isset($freebusy['FBTYPE']) ? strtoupper($freebusy['FBTYPE']) : 'BUSY'; + + // Skipping intervals marked as 'free' + if ('FREE' === $fbType) { + continue; + } + + $values = explode(',', $freebusy); + foreach ($values as $value) { + list($startTime, $endTime) = explode('/', $value); + $startTime = DateTimeParser::parseDateTime($startTime); + + if ('P' === substr($endTime, 0, 1) || '-P' === substr($endTime, 0, 2)) { + $duration = DateTimeParser::parseDuration($endTime); + $endTime = clone $startTime; + $endTime = $endTime->add($duration); + } else { + $endTime = DateTimeParser::parseDateTime($endTime); + } + + if ($this->start && $this->start > $endTime) { + continue; + } + if ($this->end && $this->end < $startTime) { + continue; + } + $fbData->add( + $startTime->getTimeStamp(), + $endTime->getTimeStamp(), + $fbType + ); + } + } + break; + } + } + } + } + + /** + * This method takes a FreeBusyData object and generates the VCALENDAR + * object associated with it. + * + * @return VCalendar + */ + protected function generateFreeBusyCalendar(FreeBusyData $fbData) + { + if ($this->baseObject) { + $calendar = $this->baseObject; + } else { + $calendar = new VCalendar(); + } + + $vfreebusy = $calendar->createComponent('VFREEBUSY'); + $calendar->add($vfreebusy); + + if ($this->start) { + $dtstart = $calendar->createProperty('DTSTART'); + $dtstart->setDateTime($this->start); + $vfreebusy->add($dtstart); + } + if ($this->end) { + $dtend = $calendar->createProperty('DTEND'); + $dtend->setDateTime($this->end); + $vfreebusy->add($dtend); + } + + $tz = new \DateTimeZone('UTC'); + $dtstamp = $calendar->createProperty('DTSTAMP'); + $dtstamp->setDateTime(new DateTimeImmutable('now', $tz)); + $vfreebusy->add($dtstamp); + + foreach ($fbData->getData() as $busyTime) { + $busyType = strtoupper($busyTime['type']); + + // Ignoring all the FREE parts, because those are already assumed. + if ('FREE' === $busyType) { + continue; + } + + $busyTime[0] = new \DateTimeImmutable('@'.$busyTime['start'], $tz); + $busyTime[1] = new \DateTimeImmutable('@'.$busyTime['end'], $tz); + + $prop = $calendar->createProperty( + 'FREEBUSY', + $busyTime[0]->format('Ymd\\THis\\Z').'/'.$busyTime[1]->format('Ymd\\THis\\Z') + ); + + // Only setting FBTYPE if it's not BUSY, because BUSY is the + // default anyway. + if ('BUSY' !== $busyType) { + $prop['FBTYPE'] = $busyType; + } + $vfreebusy->add($prop); + } + + return $calendar; + } +} diff --git a/3rdparty/sabre/vobject/lib/ITip/Broker.php b/3rdparty/sabre/vobject/lib/ITip/Broker.php new file mode 100644 index 00000000..9d68fc4c --- /dev/null +++ b/3rdparty/sabre/vobject/lib/ITip/Broker.php @@ -0,0 +1,986 @@ +component) { + return false; + } + + switch ($itipMessage->method) { + case 'REQUEST': + return $this->processMessageRequest($itipMessage, $existingObject); + + case 'CANCEL': + return $this->processMessageCancel($itipMessage, $existingObject); + + case 'REPLY': + return $this->processMessageReply($itipMessage, $existingObject); + + default: + // Unsupported iTip message + return; + } + + return $existingObject; + } + + /** + * This function parses a VCALENDAR object and figure out if any messages + * need to be sent. + * + * A VCALENDAR object will be created from the perspective of either an + * attendee, or an organizer. You must pass a string identifying the + * current user, so we can figure out who in the list of attendees or the + * organizer we are sending this message on behalf of. + * + * It's possible to specify the current user as an array, in case the user + * has more than one identifying href (such as multiple emails). + * + * It $oldCalendar is specified, it is assumed that the operation is + * updating an existing event, which means that we need to look at the + * differences between events, and potentially send old attendees + * cancellations, and current attendees updates. + * + * If $calendar is null, but $oldCalendar is specified, we treat the + * operation as if the user has deleted an event. If the user was an + * organizer, this means that we need to send cancellation notices to + * people. If the user was an attendee, we need to make sure that the + * organizer gets the 'declined' message. + * + * @param VCalendar|string $calendar + * @param string|array $userHref + * @param VCalendar|string|null $oldCalendar + * + * @return array + */ + public function parseEvent($calendar, $userHref, $oldCalendar = null) + { + if ($oldCalendar) { + if (is_string($oldCalendar)) { + $oldCalendar = Reader::read($oldCalendar); + } + if (!isset($oldCalendar->VEVENT)) { + // We only support events at the moment + return []; + } + + $oldEventInfo = $this->parseEventInfo($oldCalendar); + } else { + $oldEventInfo = [ + 'organizer' => null, + 'significantChangeHash' => '', + 'attendees' => [], + ]; + } + + $userHref = (array) $userHref; + + if (!is_null($calendar)) { + if (is_string($calendar)) { + $calendar = Reader::read($calendar); + } + if (!isset($calendar->VEVENT)) { + // We only support events at the moment + return []; + } + $eventInfo = $this->parseEventInfo($calendar); + if (!$eventInfo['attendees'] && !$oldEventInfo['attendees']) { + // If there were no attendees on either side of the equation, + // we don't need to do anything. + return []; + } + if (!$eventInfo['organizer'] && !$oldEventInfo['organizer']) { + // There was no organizer before or after the change. + return []; + } + + $baseCalendar = $calendar; + + // If the new object didn't have an organizer, the organizer + // changed the object from a scheduling object to a non-scheduling + // object. We just copy the info from the old object. + if (!$eventInfo['organizer'] && $oldEventInfo['organizer']) { + $eventInfo['organizer'] = $oldEventInfo['organizer']; + $eventInfo['organizerName'] = $oldEventInfo['organizerName']; + } + } else { + // The calendar object got deleted, we need to process this as a + // cancellation / decline. + if (!$oldCalendar) { + // No old and no new calendar, there's no thing to do. + return []; + } + + $eventInfo = $oldEventInfo; + + if (in_array($eventInfo['organizer'], $userHref)) { + // This is an organizer deleting the event. + $eventInfo['attendees'] = []; + // Increasing the sequence, but only if the organizer deleted + // the event. + ++$eventInfo['sequence']; + } else { + // This is an attendee deleting the event. + foreach ($eventInfo['attendees'] as $key => $attendee) { + if (in_array($attendee['href'], $userHref)) { + $eventInfo['attendees'][$key]['instances'] = ['master' => ['id' => 'master', 'partstat' => 'DECLINED'], + ]; + } + } + } + $baseCalendar = $oldCalendar; + } + + if (in_array($eventInfo['organizer'], $userHref)) { + return $this->parseEventForOrganizer($baseCalendar, $eventInfo, $oldEventInfo); + } elseif ($oldCalendar) { + // We need to figure out if the user is an attendee, but we're only + // doing so if there's an oldCalendar, because we only want to + // process updates, not creation of new events. + foreach ($eventInfo['attendees'] as $attendee) { + if (in_array($attendee['href'], $userHref)) { + return $this->parseEventForAttendee($baseCalendar, $eventInfo, $oldEventInfo, $attendee['href']); + } + } + } + + return []; + } + + /** + * Processes incoming REQUEST messages. + * + * This is message from an organizer, and is either a new event + * invite, or an update to an existing one. + * + * @param VCalendar $existingObject + * + * @return VCalendar|null + */ + protected function processMessageRequest(Message $itipMessage, ?VCalendar $existingObject = null) + { + if (!$existingObject) { + // This is a new invite, and we're just going to copy over + // all the components from the invite. + $existingObject = new VCalendar(); + foreach ($itipMessage->message->getComponents() as $component) { + $existingObject->add(clone $component); + } + } else { + // We need to update an existing object with all the new + // information. We can just remove all existing components + // and create new ones. + foreach ($existingObject->getComponents() as $component) { + $existingObject->remove($component); + } + foreach ($itipMessage->message->getComponents() as $component) { + $existingObject->add(clone $component); + } + } + + return $existingObject; + } + + /** + * Processes incoming CANCEL messages. + * + * This is a message from an organizer, and means that either an + * attendee got removed from an event, or an event got cancelled + * altogether. + * + * @param VCalendar $existingObject + * + * @return VCalendar|null + */ + protected function processMessageCancel(Message $itipMessage, ?VCalendar $existingObject = null) + { + if (!$existingObject) { + // The event didn't exist in the first place, so we're just + // ignoring this message. + } else { + foreach ($existingObject->VEVENT as $vevent) { + $vevent->STATUS = 'CANCELLED'; + $vevent->SEQUENCE = $itipMessage->sequence; + } + } + + return $existingObject; + } + + /** + * Processes incoming REPLY messages. + * + * The message is a reply. This is for example an attendee telling + * an organizer he accepted the invite, or declined it. + * + * @param VCalendar $existingObject + * + * @return VCalendar|null + */ + protected function processMessageReply(Message $itipMessage, ?VCalendar $existingObject = null) + { + // A reply can only be processed based on an existing object. + // If the object is not available, the reply is ignored. + if (!$existingObject) { + return; + } + $instances = []; + $requestStatus = '2.0'; + + // Finding all the instances the attendee replied to. + foreach ($itipMessage->message->VEVENT as $vevent) { + // Use the Unix timestamp returned by getTimestamp as a unique identifier for the recurrence. + // The Unix timestamp will be the same for an event, even if the reply from the attendee + // used a different format/timezone to express the event date-time. + $recurId = isset($vevent->{'RECURRENCE-ID'}) ? $vevent->{'RECURRENCE-ID'}->getDateTime()->getTimestamp() : 'master'; + $attendee = $vevent->ATTENDEE; + $instances[$recurId] = $attendee['PARTSTAT']->getValue(); + if (isset($vevent->{'REQUEST-STATUS'})) { + $requestStatus = $vevent->{'REQUEST-STATUS'}->getValue(); + list($requestStatus) = explode(';', $requestStatus); + } + } + + // Now we need to loop through the original organizer event, to find + // all the instances where we have a reply for. + $masterObject = null; + foreach ($existingObject->VEVENT as $vevent) { + // Use the Unix timestamp returned by getTimestamp as a unique identifier for the recurrence. + $recurId = isset($vevent->{'RECURRENCE-ID'}) ? $vevent->{'RECURRENCE-ID'}->getDateTime()->getTimestamp() : 'master'; + if ('master' === $recurId) { + $masterObject = $vevent; + } + if (isset($instances[$recurId])) { + $attendeeFound = false; + if (isset($vevent->ATTENDEE)) { + foreach ($vevent->ATTENDEE as $attendee) { + if ($attendee->getValue() === $itipMessage->sender) { + $attendeeFound = true; + $attendee['PARTSTAT'] = $instances[$recurId]; + $attendee['SCHEDULE-STATUS'] = $requestStatus; + // Un-setting the RSVP status, because we now know + // that the attendee already replied. + unset($attendee['RSVP']); + break; + } + } + } + if (!$attendeeFound) { + // Adding a new attendee. The iTip documentation calls this + // a party crasher. + $attendee = $vevent->add('ATTENDEE', $itipMessage->sender, [ + 'PARTSTAT' => $instances[$recurId], + ]); + if ($itipMessage->senderName) { + $attendee['CN'] = $itipMessage->senderName; + } + } + unset($instances[$recurId]); + } + } + + if (!$masterObject) { + // No master object, we can't add new instances. + return; + } + // If we got replies to instances that did not exist in the + // original list, it means that new exceptions must be created. + foreach ($instances as $recurId => $partstat) { + $recurrenceIterator = new EventIterator($existingObject, $itipMessage->uid); + $found = false; + $iterations = 1000; + do { + $newObject = $recurrenceIterator->getEventObject(); + $recurrenceIterator->next(); + + // Compare the Unix timestamp returned by getTimestamp with the previously calculated timestamp. + // If they are the same, then this is a matching recurrence, even though its date-time may have + // been expressed in a different format/timezone. + if (isset($newObject->{'RECURRENCE-ID'}) && $newObject->{'RECURRENCE-ID'}->getDateTime()->getTimestamp() === $recurId) { + $found = true; + } + --$iterations; + } while ($recurrenceIterator->valid() && !$found && $iterations); + + // Invalid recurrence id. Skipping this object. + if (!$found) { + continue; + } + + unset( + $newObject->RRULE, + $newObject->EXDATE, + $newObject->RDATE + ); + $attendeeFound = false; + if (isset($newObject->ATTENDEE)) { + foreach ($newObject->ATTENDEE as $attendee) { + if ($attendee->getValue() === $itipMessage->sender) { + $attendeeFound = true; + $attendee['PARTSTAT'] = $partstat; + break; + } + } + } + if (!$attendeeFound) { + // Adding a new attendee + $attendee = $newObject->add('ATTENDEE', $itipMessage->sender, [ + 'PARTSTAT' => $partstat, + ]); + if ($itipMessage->senderName) { + $attendee['CN'] = $itipMessage->senderName; + } + } + $existingObject->add($newObject); + } + + return $existingObject; + } + + /** + * This method is used in cases where an event got updated, and we + * potentially need to send emails to attendees to let them know of updates + * in the events. + * + * We will detect which attendees got added, which got removed and create + * specific messages for these situations. + * + * @return array + */ + protected function parseEventForOrganizer(VCalendar $calendar, array $eventInfo, array $oldEventInfo) + { + // Merging attendee lists. + $attendees = []; + foreach ($oldEventInfo['attendees'] as $attendee) { + $attendees[$attendee['href']] = [ + 'href' => $attendee['href'], + 'oldInstances' => $attendee['instances'], + 'newInstances' => [], + 'name' => $attendee['name'], + 'forceSend' => null, + ]; + } + foreach ($eventInfo['attendees'] as $attendee) { + if (isset($attendees[$attendee['href']])) { + $attendees[$attendee['href']]['name'] = $attendee['name']; + $attendees[$attendee['href']]['newInstances'] = $attendee['instances']; + $attendees[$attendee['href']]['forceSend'] = $attendee['forceSend']; + } else { + $attendees[$attendee['href']] = [ + 'href' => $attendee['href'], + 'oldInstances' => [], + 'newInstances' => $attendee['instances'], + 'name' => $attendee['name'], + 'forceSend' => $attendee['forceSend'], + ]; + } + } + + $messages = []; + + foreach ($attendees as $attendee) { + // An organizer can also be an attendee. We should not generate any + // messages for those. + if ($attendee['href'] === $eventInfo['organizer']) { + continue; + } + + $message = new Message(); + $message->uid = $eventInfo['uid']; + $message->component = 'VEVENT'; + $message->sequence = $eventInfo['sequence']; + $message->sender = $eventInfo['organizer']; + $message->senderName = $eventInfo['organizerName']; + $message->recipient = $attendee['href']; + $message->recipientName = $attendee['name']; + + // Creating the new iCalendar body. + $icalMsg = new VCalendar(); + + foreach ($calendar->select('VTIMEZONE') as $timezone) { + $icalMsg->add(clone $timezone); + } + + if (!$attendee['newInstances'] || 'CANCELLED' === $eventInfo['status']) { + // If there are no instances the attendee is a part of, it means + // the attendee was removed and we need to send them a CANCEL message. + // Also If the meeting STATUS property was changed to CANCELLED + // we need to send the attendee a CANCEL message. + $message->method = 'CANCEL'; + + $icalMsg->METHOD = $message->method; + + $event = $icalMsg->add('VEVENT', [ + 'UID' => $message->uid, + 'SEQUENCE' => $message->sequence, + 'DTSTAMP' => gmdate('Ymd\\THis\\Z'), + ]); + if (isset($calendar->VEVENT->SUMMARY)) { + $event->add('SUMMARY', $calendar->VEVENT->SUMMARY->getValue()); + } + $event->add(clone $calendar->VEVENT->DTSTART); + if (isset($calendar->VEVENT->DTEND)) { + $event->add(clone $calendar->VEVENT->DTEND); + } elseif (isset($calendar->VEVENT->DURATION)) { + $event->add(clone $calendar->VEVENT->DURATION); + } + $org = $event->add('ORGANIZER', $eventInfo['organizer']); + if ($eventInfo['organizerName']) { + $org['CN'] = $eventInfo['organizerName']; + } + $event->add('ATTENDEE', $attendee['href'], [ + 'CN' => $attendee['name'], + ]); + $message->significantChange = true; + } else { + // The attendee gets the updated event body + $message->method = 'REQUEST'; + + $icalMsg->METHOD = $message->method; + + // We need to find out that this change is significant. If it's + // not, systems may opt to not send messages. + // + // We do this based on the 'significantChangeHash' which is + // some value that changes if there's a certain set of + // properties changed in the event, or simply if there's a + // difference in instances that the attendee is invited to. + + $oldAttendeeInstances = array_keys($attendee['oldInstances']); + $newAttendeeInstances = array_keys($attendee['newInstances']); + + $message->significantChange = + 'REQUEST' === $attendee['forceSend'] || + count($oldAttendeeInstances) != count($newAttendeeInstances) || + count(array_diff($oldAttendeeInstances, $newAttendeeInstances)) > 0 || + $oldEventInfo['significantChangeHash'] !== $eventInfo['significantChangeHash']; + + foreach ($attendee['newInstances'] as $instanceId => $instanceInfo) { + $currentEvent = clone $eventInfo['instances'][$instanceId]; + if ('master' === $instanceId) { + // We need to find a list of events that the attendee + // is not a part of to add to the list of exceptions. + $exceptions = []; + foreach ($eventInfo['instances'] as $instanceId => $vevent) { + if (!isset($attendee['newInstances'][$instanceId])) { + $exceptions[] = $instanceId; + } + } + + // If there were exceptions, we need to add it to an + // existing EXDATE property, if it exists. + if ($exceptions) { + if (isset($currentEvent->EXDATE)) { + $currentEvent->EXDATE->setParts(array_merge( + $currentEvent->EXDATE->getParts(), + $exceptions + )); + } else { + $currentEvent->EXDATE = $exceptions; + } + } + + // Cleaning up any scheduling information that + // shouldn't be sent along. + unset($currentEvent->ORGANIZER['SCHEDULE-FORCE-SEND']); + unset($currentEvent->ORGANIZER['SCHEDULE-STATUS']); + + foreach ($currentEvent->ATTENDEE as $attendee) { + unset($attendee['SCHEDULE-FORCE-SEND']); + unset($attendee['SCHEDULE-STATUS']); + + // We're adding PARTSTAT=NEEDS-ACTION to ensure that + // iOS shows an "Inbox Item" + if (!isset($attendee['PARTSTAT'])) { + $attendee['PARTSTAT'] = 'NEEDS-ACTION'; + } + } + } + + $currentEvent->DTSTAMP = gmdate('Ymd\\THis\\Z'); + $icalMsg->add($currentEvent); + } + } + + $message->message = $icalMsg; + $messages[] = $message; + } + + return $messages; + } + + /** + * Parse an event update for an attendee. + * + * This function figures out if we need to send a reply to an organizer. + * + * @param string $attendee + * + * @return Message[] + */ + protected function parseEventForAttendee(VCalendar $calendar, array $eventInfo, array $oldEventInfo, $attendee) + { + if ($this->scheduleAgentServerRules && 'CLIENT' === $eventInfo['organizerScheduleAgent']) { + return []; + } + + // Don't bother generating messages for events that have already been + // cancelled. + if ('CANCELLED' === $eventInfo['status']) { + return []; + } + + $oldInstances = !empty($oldEventInfo['attendees'][$attendee]['instances']) ? + $oldEventInfo['attendees'][$attendee]['instances'] : + []; + + $instances = []; + foreach ($oldInstances as $instance) { + $instances[$instance['id']] = [ + 'id' => $instance['id'], + 'oldstatus' => $instance['partstat'], + 'newstatus' => null, + ]; + } + foreach ($eventInfo['attendees'][$attendee]['instances'] as $instance) { + if (isset($instances[$instance['id']])) { + $instances[$instance['id']]['newstatus'] = $instance['partstat']; + } else { + $instances[$instance['id']] = [ + 'id' => $instance['id'], + 'oldstatus' => null, + 'newstatus' => $instance['partstat'], + ]; + } + } + + // We need to also look for differences in EXDATE. If there are new + // items in EXDATE, it means that an attendee deleted instances of an + // event, which means we need to send DECLINED specifically for those + // instances. + // We only need to do that though, if the master event is not declined. + if (isset($instances['master']) && 'DECLINED' !== $instances['master']['newstatus']) { + foreach ($eventInfo['exdate'] as $exDate) { + if (!in_array($exDate, $oldEventInfo['exdate'])) { + if (isset($instances[$exDate])) { + $instances[$exDate]['newstatus'] = 'DECLINED'; + } else { + $instances[$exDate] = [ + 'id' => $exDate, + 'oldstatus' => null, + 'newstatus' => 'DECLINED', + ]; + } + } + } + } + + // Gathering a few extra properties for each instance. + foreach ($instances as $recurId => $instanceInfo) { + if (isset($eventInfo['instances'][$recurId])) { + $instances[$recurId]['dtstart'] = clone $eventInfo['instances'][$recurId]->DTSTART; + } else { + $instances[$recurId]['dtstart'] = $recurId; + } + } + + $message = new Message(); + $message->uid = $eventInfo['uid']; + $message->method = 'REPLY'; + $message->component = 'VEVENT'; + $message->sequence = $eventInfo['sequence']; + $message->sender = $attendee; + $message->senderName = $eventInfo['attendees'][$attendee]['name']; + $message->recipient = $eventInfo['organizer']; + $message->recipientName = $eventInfo['organizerName']; + + $icalMsg = new VCalendar(); + $icalMsg->METHOD = 'REPLY'; + + foreach ($calendar->select('VTIMEZONE') as $timezone) { + $icalMsg->add(clone $timezone); + } + + $hasReply = false; + + foreach ($instances as $instance) { + if ($instance['oldstatus'] == $instance['newstatus'] && 'REPLY' !== $eventInfo['organizerForceSend']) { + // Skip + continue; + } + + $event = $icalMsg->add('VEVENT', [ + 'UID' => $message->uid, + 'SEQUENCE' => $message->sequence, + ]); + $summary = isset($calendar->VEVENT->SUMMARY) ? $calendar->VEVENT->SUMMARY->getValue() : ''; + // Adding properties from the correct source instance + if (isset($eventInfo['instances'][$instance['id']])) { + $instanceObj = $eventInfo['instances'][$instance['id']]; + $event->add(clone $instanceObj->DTSTART); + if (isset($instanceObj->DTEND)) { + $event->add(clone $instanceObj->DTEND); + } elseif (isset($instanceObj->DURATION)) { + $event->add(clone $instanceObj->DURATION); + } + if (isset($instanceObj->SUMMARY)) { + $event->add('SUMMARY', $instanceObj->SUMMARY->getValue()); + } elseif ($summary) { + $event->add('SUMMARY', $summary); + } + } else { + // This branch of the code is reached, when a reply is + // generated for an instance of a recurring event, through the + // fact that the instance has disappeared by showing up in + // EXDATE + $dt = DateTimeParser::parse($instance['id'], $eventInfo['timezone']); + // Treat is as a DATE field + if (strlen($instance['id']) <= 8) { + $event->add('DTSTART', $dt, ['VALUE' => 'DATE']); + } else { + $event->add('DTSTART', $dt); + } + if ($summary) { + $event->add('SUMMARY', $summary); + } + } + if ('master' !== $instance['id']) { + $dt = DateTimeParser::parse($instance['id'], $eventInfo['timezone']); + // Treat is as a DATE field + if (strlen($instance['id']) <= 8) { + $event->add('RECURRENCE-ID', $dt, ['VALUE' => 'DATE']); + } else { + $event->add('RECURRENCE-ID', $dt); + } + } + $organizer = $event->add('ORGANIZER', $message->recipient); + if ($message->recipientName) { + $organizer['CN'] = $message->recipientName; + } + $attendee = $event->add('ATTENDEE', $message->sender, [ + 'PARTSTAT' => $instance['newstatus'], + ]); + if ($message->senderName) { + $attendee['CN'] = $message->senderName; + } + $hasReply = true; + } + + if ($hasReply) { + $message->message = $icalMsg; + + return [$message]; + } else { + return []; + } + } + + /** + * Returns attendee information and information about instances of an + * event. + * + * Returns an array with the following keys: + * + * 1. uid + * 2. organizer + * 3. organizerName + * 4. organizerScheduleAgent + * 5. organizerForceSend + * 6. instances + * 7. attendees + * 8. sequence + * 9. exdate + * 10. timezone - strictly the timezone on which the recurrence rule is + * based on. + * 11. significantChangeHash + * 12. status + * + * @param VCalendar $calendar + * + * @return array + */ + protected function parseEventInfo(?VCalendar $calendar = null) + { + $uid = null; + $organizer = null; + $organizerName = null; + $organizerForceSend = null; + $sequence = null; + $timezone = null; + $status = null; + $organizerScheduleAgent = 'SERVER'; + + $significantChangeHash = ''; + + // Now we need to collect a list of attendees, and which instances they + // are a part of. + $attendees = []; + + $instances = []; + $exdate = []; + + $significantChangeEventProperties = []; + + foreach ($calendar->VEVENT as $vevent) { + $eventSignificantChangeHash = ''; + $rrule = []; + + if (is_null($uid)) { + $uid = $vevent->UID->getValue(); + } else { + if ($uid !== $vevent->UID->getValue()) { + throw new ITipException('If a calendar contained more than one event, they must have the same UID.'); + } + } + + if (!isset($vevent->DTSTART)) { + throw new ITipException('An event MUST have a DTSTART property.'); + } + + if (isset($vevent->ORGANIZER)) { + if (is_null($organizer)) { + $organizer = $vevent->ORGANIZER->getNormalizedValue(); + $organizerName = isset($vevent->ORGANIZER['CN']) ? $vevent->ORGANIZER['CN'] : null; + } else { + if (strtoupper($organizer) !== strtoupper($vevent->ORGANIZER->getNormalizedValue())) { + throw new SameOrganizerForAllComponentsException('Every instance of the event must have the same organizer.'); + } + } + $organizerForceSend = + isset($vevent->ORGANIZER['SCHEDULE-FORCE-SEND']) ? + strtoupper($vevent->ORGANIZER['SCHEDULE-FORCE-SEND']) : + null; + $organizerScheduleAgent = + isset($vevent->ORGANIZER['SCHEDULE-AGENT']) ? + strtoupper((string) $vevent->ORGANIZER['SCHEDULE-AGENT']) : + 'SERVER'; + } + if (is_null($sequence) && isset($vevent->SEQUENCE)) { + $sequence = $vevent->SEQUENCE->getValue(); + } + if (isset($vevent->EXDATE)) { + foreach ($vevent->select('EXDATE') as $val) { + $exdate = array_merge($exdate, $val->getParts()); + } + sort($exdate); + } + if (isset($vevent->RRULE)) { + foreach ($vevent->select('RRULE') as $rr) { + foreach ($rr->getParts() as $key => $val) { + // ignore default values (https://github.com/sabre-io/vobject/issues/126) + if ('INTERVAL' === $key && 1 == $val) { + continue; + } + if (is_array($val)) { + $val = implode(',', $val); + } + $rrule[] = "$key=$val"; + } + } + sort($rrule); + } + if (isset($vevent->STATUS)) { + $status = strtoupper($vevent->STATUS->getValue()); + } + + $recurId = isset($vevent->{'RECURRENCE-ID'}) ? $vevent->{'RECURRENCE-ID'}->getValue() : 'master'; + if (is_null($timezone)) { + if ('master' === $recurId) { + $timezone = $vevent->DTSTART->getDateTime()->getTimeZone(); + } else { + $timezone = $vevent->{'RECURRENCE-ID'}->getDateTime()->getTimeZone(); + } + } + if (isset($vevent->ATTENDEE)) { + foreach ($vevent->ATTENDEE as $attendee) { + if ($this->scheduleAgentServerRules && + isset($attendee['SCHEDULE-AGENT']) && + 'CLIENT' === strtoupper($attendee['SCHEDULE-AGENT']->getValue()) + ) { + continue; + } + $partStat = + isset($attendee['PARTSTAT']) ? + strtoupper($attendee['PARTSTAT']) : + 'NEEDS-ACTION'; + + $forceSend = + isset($attendee['SCHEDULE-FORCE-SEND']) ? + strtoupper($attendee['SCHEDULE-FORCE-SEND']) : + null; + + if (isset($attendees[$attendee->getNormalizedValue()])) { + $attendees[$attendee->getNormalizedValue()]['instances'][$recurId] = [ + 'id' => $recurId, + 'partstat' => $partStat, + 'forceSend' => $forceSend, + ]; + } else { + $attendees[$attendee->getNormalizedValue()] = [ + 'href' => $attendee->getNormalizedValue(), + 'instances' => [ + $recurId => [ + 'id' => $recurId, + 'partstat' => $partStat, + ], + ], + 'name' => isset($attendee['CN']) ? (string) $attendee['CN'] : null, + 'forceSend' => $forceSend, + ]; + } + } + $instances[$recurId] = $vevent; + } + + foreach ($this->significantChangeProperties as $prop) { + if (isset($vevent->$prop)) { + $propertyValues = $vevent->select($prop); + + $eventSignificantChangeHash .= $prop.':'; + + if ('EXDATE' === $prop) { + $eventSignificantChangeHash .= implode(',', $exdate).';'; + } elseif ('RRULE' === $prop) { + $eventSignificantChangeHash .= implode(',', $rrule).';'; + } else { + foreach ($propertyValues as $val) { + $eventSignificantChangeHash .= $val->getValue().';'; + } + } + } + } + $significantChangeEventProperties[] = $eventSignificantChangeHash; + } + + asort($significantChangeEventProperties); + + foreach ($significantChangeEventProperties as $eventSignificantChangeHash) { + $significantChangeHash .= $eventSignificantChangeHash; + } + $significantChangeHash = md5($significantChangeHash); + + return compact( + 'uid', + 'organizer', + 'organizerName', + 'organizerScheduleAgent', + 'organizerForceSend', + 'instances', + 'attendees', + 'sequence', + 'exdate', + 'timezone', + 'significantChangeHash', + 'status' + ); + } +} diff --git a/3rdparty/sabre/vobject/lib/ITip/ITipException.php b/3rdparty/sabre/vobject/lib/ITip/ITipException.php new file mode 100644 index 00000000..94956361 --- /dev/null +++ b/3rdparty/sabre/vobject/lib/ITip/ITipException.php @@ -0,0 +1,16 @@ +scheduleStatus) { + return false; + } else { + list($scheduleStatus) = explode(';', $this->scheduleStatus); + + return $scheduleStatus; + } + } +} diff --git a/3rdparty/sabre/vobject/lib/ITip/SameOrganizerForAllComponentsException.php b/3rdparty/sabre/vobject/lib/ITip/SameOrganizerForAllComponentsException.php new file mode 100644 index 00000000..4c48625b --- /dev/null +++ b/3rdparty/sabre/vobject/lib/ITip/SameOrganizerForAllComponentsException.php @@ -0,0 +1,18 @@ +parent = null; + $this->root = null; + } + + /* {{{ IteratorAggregator interface */ + + /** + * Returns the iterator for this object. + * + * @return ElementList + */ + #[\ReturnTypeWillChange] + public function getIterator() + { + if (!is_null($this->iterator)) { + return $this->iterator; + } + + return new ElementList([$this]); + } + + /** + * Sets the overridden iterator. + * + * Note that this is not actually part of the iterator interface + */ + public function setIterator(ElementList $iterator) + { + $this->iterator = $iterator; + } + + /** + * Validates the node for correctness. + * + * The following options are supported: + * Node::REPAIR - May attempt to automatically repair the problem. + * + * This method returns an array with detected problems. + * Every element has the following properties: + * + * * level - problem level. + * * message - A human-readable string describing the issue. + * * node - A reference to the problematic node. + * + * The level means: + * 1 - The issue was repaired (only happens if REPAIR was turned on) + * 2 - An inconsequential issue + * 3 - A severe issue. + * + * @param int $options + * + * @return array + */ + public function validate($options = 0) + { + return []; + } + + /* }}} */ + + /* {{{ Countable interface */ + + /** + * Returns the number of elements. + * + * @return int + */ + #[\ReturnTypeWillChange] + public function count() + { + $it = $this->getIterator(); + + return $it->count(); + } + + /* }}} */ + + /* {{{ ArrayAccess Interface */ + + /** + * Checks if an item exists through ArrayAccess. + * + * This method just forwards the request to the inner iterator + * + * @param int $offset + * + * @return bool + */ + #[\ReturnTypeWillChange] + public function offsetExists($offset) + { + $iterator = $this->getIterator(); + + return $iterator->offsetExists($offset); + } + + /** + * Gets an item through ArrayAccess. + * + * This method just forwards the request to the inner iterator + * + * @param int $offset + * + * @return mixed + */ + #[\ReturnTypeWillChange] + public function offsetGet($offset) + { + $iterator = $this->getIterator(); + + return $iterator->offsetGet($offset); + } + + /** + * Sets an item through ArrayAccess. + * + * This method just forwards the request to the inner iterator + * + * @param int $offset + * @param mixed $value + */ + #[\ReturnTypeWillChange] + public function offsetSet($offset, $value) + { + $iterator = $this->getIterator(); + $iterator->offsetSet($offset, $value); + + // @codeCoverageIgnoreStart + // + // This method always throws an exception, so we ignore the closing + // brace + } + + // @codeCoverageIgnoreEnd + + /** + * Sets an item through ArrayAccess. + * + * This method just forwards the request to the inner iterator + * + * @param int $offset + */ + #[\ReturnTypeWillChange] + public function offsetUnset($offset) + { + $iterator = $this->getIterator(); + $iterator->offsetUnset($offset); + + // @codeCoverageIgnoreStart + // + // This method always throws an exception, so we ignore the closing + // brace + } + + // @codeCoverageIgnoreEnd + + /* }}} */ +} diff --git a/3rdparty/sabre/vobject/lib/PHPUnitAssertions.php b/3rdparty/sabre/vobject/lib/PHPUnitAssertions.php new file mode 100644 index 00000000..45c0a21c --- /dev/null +++ b/3rdparty/sabre/vobject/lib/PHPUnitAssertions.php @@ -0,0 +1,75 @@ +fail('Input must be a string, stream or VObject component'); + } + unset($input->PRODID); + if ($input instanceof Component\VCalendar && 'GREGORIAN' === (string) $input->CALSCALE) { + unset($input->CALSCALE); + } + + return $input; + }; + + $expected = $getObj($expected)->serialize(); + $actual = $getObj($actual)->serialize(); + + // Finding wildcards in expected. + preg_match_all('|^([A-Z]+):\\*\\*ANY\\*\\*\r$|m', $expected, $matches, PREG_SET_ORDER); + + foreach ($matches as $match) { + $actual = preg_replace( + '|^'.preg_quote($match[1], '|').':(.*)\r$|m', + $match[1].':**ANY**'."\r", + $actual + ); + } + + $this->assertEquals( + $expected, + $actual, + $message + ); + } +} diff --git a/3rdparty/sabre/vobject/lib/Parameter.php b/3rdparty/sabre/vobject/lib/Parameter.php new file mode 100644 index 00000000..0f0b5860 --- /dev/null +++ b/3rdparty/sabre/vobject/lib/Parameter.php @@ -0,0 +1,368 @@ +root = $root; + if (is_null($name)) { + $this->noName = true; + $this->name = static::guessParameterNameByValue($value); + } else { + $this->name = strtoupper($name); + } + + // If guessParameterNameByValue() returns an empty string + // above, we're actually dealing with a parameter that has no value. + // In that case we have to move the value to the name. + if ('' === $this->name) { + $this->noName = false; + $this->name = strtoupper($value); + } else { + $this->setValue($value); + } + } + + /** + * Try to guess property name by value, can be used for vCard 2.1 nameless parameters. + * + * Figuring out what the name should have been. Note that a ton of + * these are rather silly in 2014 and would probably rarely be + * used, but we like to be complete. + * + * @param string $value + * + * @return string + */ + public static function guessParameterNameByValue($value) + { + switch (strtoupper($value)) { + // Encodings + case '7-BIT': + case 'QUOTED-PRINTABLE': + case 'BASE64': + $name = 'ENCODING'; + break; + + // Common types + case 'WORK': + case 'HOME': + case 'PREF': + // Delivery Label Type + case 'DOM': + case 'INTL': + case 'POSTAL': + case 'PARCEL': + // Telephone types + case 'VOICE': + case 'FAX': + case 'MSG': + case 'CELL': + case 'PAGER': + case 'BBS': + case 'MODEM': + case 'CAR': + case 'ISDN': + case 'VIDEO': + // EMAIL types (lol) + case 'AOL': + case 'APPLELINK': + case 'ATTMAIL': + case 'CIS': + case 'EWORLD': + case 'INTERNET': + case 'IBMMAIL': + case 'MCIMAIL': + case 'POWERSHARE': + case 'PRODIGY': + case 'TLX': + case 'X400': + // Photo / Logo format types + case 'GIF': + case 'CGM': + case 'WMF': + case 'BMP': + case 'DIB': + case 'PICT': + case 'TIFF': + case 'PDF': + case 'PS': + case 'JPEG': + case 'MPEG': + case 'MPEG2': + case 'AVI': + case 'QTIME': + // Sound Digital Audio Type + case 'WAVE': + case 'PCM': + case 'AIFF': + // Key types + case 'X509': + case 'PGP': + $name = 'TYPE'; + break; + + // Value types + case 'INLINE': + case 'URL': + case 'CONTENT-ID': + case 'CID': + $name = 'VALUE'; + break; + + default: + $name = ''; + } + + return $name; + } + + /** + * Updates the current value. + * + * This may be either a single, or multiple strings in an array. + * + * @param string|array $value + */ + public function setValue($value) + { + $this->value = $value; + } + + /** + * Returns the current value. + * + * This method will always return a string, or null. If there were multiple + * values, it will automatically concatenate them (separated by comma). + * + * @return string|null + */ + public function getValue() + { + if (is_array($this->value)) { + return implode(',', $this->value); + } else { + return $this->value; + } + } + + /** + * Sets multiple values for this parameter. + */ + public function setParts(array $value) + { + $this->value = $value; + } + + /** + * Returns all values for this parameter. + * + * If there were no values, an empty array will be returned. + * + * @return array + */ + public function getParts() + { + if (is_array($this->value)) { + return $this->value; + } elseif (is_null($this->value)) { + return []; + } else { + return [$this->value]; + } + } + + /** + * Adds a value to this parameter. + * + * If the argument is specified as an array, all items will be added to the + * parameter value list. + * + * @param string|array $part + */ + public function addValue($part) + { + if (is_null($this->value)) { + $this->value = $part; + } else { + $this->value = array_merge((array) $this->value, (array) $part); + } + } + + /** + * Checks if this parameter contains the specified value. + * + * This is a case-insensitive match. It makes sense to call this for for + * instance the TYPE parameter, to see if it contains a keyword such as + * 'WORK' or 'FAX'. + * + * @param string $value + * + * @return bool + */ + public function has($value) + { + return in_array( + strtolower($value), + array_map('strtolower', (array) $this->value) + ); + } + + /** + * Turns the object back into a serialized blob. + * + * @return string + */ + public function serialize() + { + $value = $this->getParts(); + + if (0 === count($value)) { + return $this->name.'='; + } + + if (Document::VCARD21 === $this->root->getDocumentType() && $this->noName) { + return implode(';', $value); + } + + return $this->name.'='.array_reduce( + $value, + function ($out, $item) { + if (!is_null($out)) { + $out .= ','; + } + + // If there's no special characters in the string, we'll use the simple + // format. + // + // The list of special characters is defined as: + // + // Any character except CONTROL, DQUOTE, ";", ":", "," + // + // by the iCalendar spec: + // https://tools.ietf.org/html/rfc5545#section-3.1 + // + // And we add ^ to that because of: + // https://tools.ietf.org/html/rfc6868 + // + // But we've found that iCal (7.0, shipped with OSX 10.9) + // severely trips on + characters not being quoted, so we + // added + as well. + if (!preg_match('#(?: [\n":;\^,\+] )#x', $item)) { + return $out.$item; + } else { + // Enclosing in double-quotes, and using RFC6868 for encoding any + // special characters + $out .= '"'.strtr( + $item, + [ + '^' => '^^', + "\n" => '^n', + '"' => '^\'', + ] + ).'"'; + + return $out; + } + } + ); + } + + /** + * This method returns an array, with the representation as it should be + * encoded in JSON. This is used to create jCard or jCal documents. + * + * @return array + */ + #[\ReturnTypeWillChange] + public function jsonSerialize() + { + return $this->value; + } + + /** + * This method serializes the data into XML. This is used to create xCard or + * xCal documents. + * + * @param Xml\Writer $writer XML writer + */ + public function xmlSerialize(Xml\Writer $writer): void + { + foreach (explode(',', $this->value) as $value) { + $writer->writeElement('text', $value); + } + } + + /** + * Called when this object is being cast to a string. + * + * @return string + */ + public function __toString() + { + return (string) $this->getValue(); + } + + /** + * Returns the iterator for this object. + * + * @return ElementList + */ + #[\ReturnTypeWillChange] + public function getIterator() + { + if (!is_null($this->iterator)) { + return $this->iterator; + } + + return $this->iterator = new ArrayIterator((array) $this->value); + } +} diff --git a/3rdparty/sabre/vobject/lib/ParseException.php b/3rdparty/sabre/vobject/lib/ParseException.php new file mode 100644 index 00000000..a8f497b2 --- /dev/null +++ b/3rdparty/sabre/vobject/lib/ParseException.php @@ -0,0 +1,14 @@ +setInput($input); + } + if (is_null($this->input)) { + throw new EofException('End of input stream, or no input supplied'); + } + + if (0 !== $options) { + $this->options = $options; + } + + switch ($this->input[0]) { + case 'vcalendar': + $this->root = new VCalendar([], false); + break; + case 'vcard': + $this->root = new VCard([], false); + break; + default: + throw new ParseException('The root component must either be a vcalendar, or a vcard'); + } + foreach ($this->input[1] as $prop) { + $this->root->add($this->parseProperty($prop)); + } + if (isset($this->input[2])) { + foreach ($this->input[2] as $comp) { + $this->root->add($this->parseComponent($comp)); + } + } + + // Resetting the input so we can throw an feof exception the next time. + $this->input = null; + + return $this->root; + } + + /** + * Parses a component. + * + * @return \Sabre\VObject\Component + */ + public function parseComponent(array $jComp) + { + // We can remove $self from PHP 5.4 onward. + $self = $this; + + $properties = array_map( + function ($jProp) use ($self) { + return $self->parseProperty($jProp); + }, + $jComp[1] + ); + + if (isset($jComp[2])) { + $components = array_map( + function ($jComp) use ($self) { + return $self->parseComponent($jComp); + }, + $jComp[2] + ); + } else { + $components = []; + } + + return $this->root->createComponent( + $jComp[0], + array_merge($properties, $components), + $defaults = false + ); + } + + /** + * Parses properties. + * + * @return \Sabre\VObject\Property + */ + public function parseProperty(array $jProp) + { + list( + $propertyName, + $parameters, + $valueType + ) = $jProp; + + $propertyName = strtoupper($propertyName); + + // This is the default class we would be using if we didn't know the + // value type. We're using this value later in this function. + $defaultPropertyClass = $this->root->getClassNameForPropertyName($propertyName); + + $parameters = (array) $parameters; + + $value = array_slice($jProp, 3); + + $valueType = strtoupper($valueType); + + if (isset($parameters['group'])) { + $propertyName = $parameters['group'].'.'.$propertyName; + unset($parameters['group']); + } + + $prop = $this->root->createProperty($propertyName, null, $parameters, $valueType); + $prop->setJsonValue($value); + + // We have to do something awkward here. FlatText as well as Text + // represents TEXT values. We have to normalize these here. In the + // future we can get rid of FlatText once we're allowed to break BC + // again. + if (FlatText::class === $defaultPropertyClass) { + $defaultPropertyClass = Text::class; + } + + // If the value type we received (e.g.: TEXT) was not the default value + // type for the given property (e.g.: BDAY), we need to add a VALUE= + // parameter. + if ($defaultPropertyClass !== get_class($prop)) { + $prop['VALUE'] = $valueType; + } + + return $prop; + } + + /** + * Sets the input data. + * + * @param resource|string|array $input + */ + public function setInput($input) + { + if (is_resource($input)) { + $input = stream_get_contents($input); + } + if (is_string($input)) { + $input = json_decode($input); + } + $this->input = $input; + } +} diff --git a/3rdparty/sabre/vobject/lib/Parser/MimeDir.php b/3rdparty/sabre/vobject/lib/Parser/MimeDir.php new file mode 100644 index 00000000..d484d6a3 --- /dev/null +++ b/3rdparty/sabre/vobject/lib/Parser/MimeDir.php @@ -0,0 +1,689 @@ +root = null; + + if (!is_null($input)) { + $this->setInput($input); + } + + if (!\is_resource($this->input)) { + // Null was passed as input, but there was no existing input buffer + // There is nothing to parse. + throw new ParseException('No input provided to parse'); + } + + if (0 !== $options) { + $this->options = $options; + } + + $this->parseDocument(); + + return $this->root; + } + + /** + * By default all input will be assumed to be UTF-8. + * + * However, both iCalendar and vCard might be encoded using different + * character sets. The character set is usually set in the mime-type. + * + * If this is the case, use setEncoding to specify that a different + * encoding will be used. If this is set, the parser will automatically + * convert all incoming data to UTF-8. + * + * @param string $charset + */ + public function setCharset($charset) + { + if (!in_array($charset, self::$SUPPORTED_CHARSETS)) { + throw new \InvalidArgumentException('Unsupported encoding. (Supported encodings: '.implode(', ', self::$SUPPORTED_CHARSETS).')'); + } + $this->charset = $charset; + } + + /** + * Sets the input buffer. Must be a string or stream. + * + * @param resource|string $input + */ + public function setInput($input) + { + // Resetting the parser + $this->lineIndex = 0; + $this->startLine = 0; + + if (is_string($input)) { + // Converting to a stream. + $stream = fopen('php://temp', 'r+'); + fwrite($stream, $input); + rewind($stream); + $this->input = $stream; + } elseif (is_resource($input)) { + $this->input = $input; + } else { + throw new \InvalidArgumentException('This parser can only read from strings or streams.'); + } + } + + /** + * Parses an entire document. + */ + protected function parseDocument() + { + $line = $this->readLine(); + + // BOM is ZERO WIDTH NO-BREAK SPACE (U+FEFF). + // It's 0xEF 0xBB 0xBF in UTF-8 hex. + if (3 <= strlen($line) + && 0xef === ord($line[0]) + && 0xbb === ord($line[1]) + && 0xbf === ord($line[2])) { + $line = substr($line, 3); + } + + switch (strtoupper($line)) { + case 'BEGIN:VCALENDAR': + $class = VCalendar::$componentMap['VCALENDAR']; + break; + case 'BEGIN:VCARD': + $class = VCard::$componentMap['VCARD']; + break; + default: + throw new ParseException('This parser only supports VCARD and VCALENDAR files'); + } + + $this->root = new $class([], false); + + while (true) { + // Reading until we hit END: + try { + $line = $this->readLine(); + } catch (EofException $oEx) { + $line = 'END:'.$this->root->name; + } + if ('END:' === strtoupper(substr($line, 0, 4))) { + break; + } + $result = $this->parseLine($line); + if ($result) { + $this->root->add($result); + } + } + + $name = strtoupper(substr($line, 4)); + if ($name !== $this->root->name) { + throw new ParseException('Invalid MimeDir file. expected: "END:'.$this->root->name.'" got: "END:'.$name.'"'); + } + } + + /** + * Parses a line, and if it hits a component, it will also attempt to parse + * the entire component. + * + * @param string $line Unfolded line + * + * @return Node + */ + protected function parseLine($line) + { + // Start of a new component + if ('BEGIN:' === strtoupper(substr($line, 0, 6))) { + if (substr($line, 6) === $this->root->name) { + throw new ParseException('Invalid MimeDir file. Unexpected component: "'.$line.'" in document type '.$this->root->name); + } + $component = $this->root->createComponent(substr($line, 6), [], false); + + while (true) { + // Reading until we hit END: + $line = $this->readLine(); + if ('END:' === strtoupper(substr($line, 0, 4))) { + break; + } + $result = $this->parseLine($line); + if ($result) { + $component->add($result); + } + } + + $name = strtoupper(substr($line, 4)); + if ($name !== $component->name) { + throw new ParseException('Invalid MimeDir file. expected: "END:'.$component->name.'" got: "END:'.$name.'"'); + } + + return $component; + } else { + // Property reader + $property = $this->readProperty($line); + if (!$property) { + // Ignored line + return false; + } + + return $property; + } + } + + /** + * We need to look ahead 1 line every time to see if we need to 'unfold' + * the next line. + * + * If that was not the case, we store it here. + * + * @var string|null + */ + protected $lineBuffer; + + /** + * The real current line number. + */ + protected $lineIndex = 0; + + /** + * In the case of unfolded lines, this property holds the line number for + * the start of the line. + * + * @var int + */ + protected $startLine = 0; + + /** + * Contains a 'raw' representation of the current line. + * + * @var string + */ + protected $rawLine; + + /** + * Reads a single line from the buffer. + * + * This method strips any newlines and also takes care of unfolding. + * + * @throws \Sabre\VObject\EofException + * + * @return string + */ + protected function readLine() + { + if (!\is_null($this->lineBuffer)) { + $rawLine = $this->lineBuffer; + $this->lineBuffer = null; + } else { + do { + $eof = \feof($this->input); + + $rawLine = \fgets($this->input); + + if ($eof || (\feof($this->input) && false === $rawLine)) { + throw new EofException('End of document reached prematurely'); + } + if (false === $rawLine) { + throw new ParseException('Error reading from input stream'); + } + $rawLine = \rtrim($rawLine, "\r\n"); + } while ('' === $rawLine); // Skipping empty lines + ++$this->lineIndex; + } + $line = $rawLine; + + $this->startLine = $this->lineIndex; + + // Looking ahead for folded lines. + while (true) { + $nextLine = \rtrim(\fgets($this->input), "\r\n"); + ++$this->lineIndex; + if (!$nextLine) { + break; + } + if ("\t" === $nextLine[0] || ' ' === $nextLine[0]) { + $curLine = \substr($nextLine, 1); + $line .= $curLine; + $rawLine .= "\n ".$curLine; + } else { + $this->lineBuffer = $nextLine; + break; + } + } + $this->rawLine = $rawLine; + + return $line; + } + + /** + * Reads a property or component from a line. + */ + protected function readProperty($line) + { + if ($this->options & self::OPTION_FORGIVING) { + $propNameToken = 'A-Z0-9\-\._\\/'; + } else { + $propNameToken = 'A-Z0-9\-\.'; + } + + $paramNameToken = 'A-Z0-9\-'; + $safeChar = '^";:,'; + $qSafeChar = '^"'; + + $regex = "/ + ^(?P [$propNameToken]+ ) (?=[;:]) # property name + | + (?<=:)(?P .+)$ # property value + | + ;(?P [$paramNameToken]+) (?=[=;:]) # parameter name + | + (=|,)(?P # parameter value + (?: [$safeChar]*) | + \"(?: [$qSafeChar]+)\" + ) (?=[;:,]) + /xi"; + + //echo $regex, "\n"; exit(); + preg_match_all($regex, $line, $matches, PREG_SET_ORDER); + + $property = [ + 'name' => null, + 'parameters' => [], + 'value' => null, + ]; + + $lastParam = null; + + /* + * Looping through all the tokens. + * + * Note that we are looping through them in reverse order, because if a + * sub-pattern matched, the subsequent named patterns will not show up + * in the result. + */ + foreach ($matches as $match) { + if (isset($match['paramValue'])) { + if ($match['paramValue'] && '"' === $match['paramValue'][0]) { + $value = substr($match['paramValue'], 1, -1); + } else { + $value = $match['paramValue']; + } + + $value = $this->unescapeParam($value); + + if (is_null($lastParam)) { + if ($this->options & self::OPTION_IGNORE_INVALID_LINES) { + // When the property can't be matched and the configuration + // option is set to ignore invalid lines, we ignore this line + // This can happen when servers provide faulty data as iCloud + // frequently does with X-APPLE-STRUCTURED-LOCATION + continue; + } + throw new ParseException('Invalid Mimedir file. Line starting at '.$this->startLine.' did not follow iCalendar/vCard conventions'); + } + if (is_null($property['parameters'][$lastParam])) { + $property['parameters'][$lastParam] = $value; + } elseif (is_array($property['parameters'][$lastParam])) { + $property['parameters'][$lastParam][] = $value; + } elseif ($property['parameters'][$lastParam] === $value) { + // When the current value of the parameter is the same as the + // new one, then we can leave the current parameter as it is. + } else { + $property['parameters'][$lastParam] = [ + $property['parameters'][$lastParam], + $value, + ]; + } + continue; + } + if (isset($match['paramName'])) { + $lastParam = strtoupper($match['paramName']); + if (!isset($property['parameters'][$lastParam])) { + $property['parameters'][$lastParam] = null; + } + continue; + } + if (isset($match['propValue'])) { + $property['value'] = $match['propValue']; + continue; + } + if (isset($match['name']) && $match['name']) { + $property['name'] = strtoupper($match['name']); + continue; + } + + // @codeCoverageIgnoreStart + throw new \LogicException('This code should not be reachable'); + // @codeCoverageIgnoreEnd + } + + if (is_null($property['value'])) { + $property['value'] = ''; + } + if (!$property['name']) { + if ($this->options & self::OPTION_IGNORE_INVALID_LINES) { + return false; + } + throw new ParseException('Invalid Mimedir file. Line starting at '.$this->startLine.' did not follow iCalendar/vCard conventions'); + } + + // vCard 2.1 states that parameters may appear without a name, and only + // a value. We can deduce the value based on its name. + // + // Our parser will get those as parameters without a value instead, so + // we're filtering these parameters out first. + $namedParameters = []; + $namelessParameters = []; + + foreach ($property['parameters'] as $name => $value) { + if (!is_null($value)) { + $namedParameters[$name] = $value; + } else { + $namelessParameters[] = $name; + } + } + + $propObj = $this->root->createProperty($property['name'], null, $namedParameters, null, $this->startLine, $line); + + foreach ($namelessParameters as $namelessParameter) { + $propObj->add(null, $namelessParameter); + } + + if (isset($propObj['ENCODING']) && 'QUOTED-PRINTABLE' === strtoupper($propObj['ENCODING'])) { + $propObj->setQuotedPrintableValue($this->extractQuotedPrintableValue()); + } else { + $charset = $this->charset; + if (Document::VCARD21 === $this->root->getDocumentType() && isset($propObj['CHARSET'])) { + // vCard 2.1 allows the character set to be specified per property. + $charset = (string) $propObj['CHARSET']; + } + switch (strtolower($charset)) { + case 'utf-8': + break; + case 'windows-1252': + case 'iso-8859-1': + $property['value'] = mb_convert_encoding($property['value'], 'UTF-8', $charset); + break; + default: + throw new ParseException('Unsupported CHARSET: '.$propObj['CHARSET']); + } + $propObj->setRawMimeDirValue($property['value']); + } + + return $propObj; + } + + /** + * Unescapes a property value. + * + * vCard 2.1 says: + * * Semi-colons must be escaped in some property values, specifically + * ADR, ORG and N. + * * Semi-colons must be escaped in parameter values, because semi-colons + * are also use to separate values. + * * No mention of escaping backslashes with another backslash. + * * newlines are not escaped either, instead QUOTED-PRINTABLE is used to + * span values over more than 1 line. + * + * vCard 3.0 says: + * * (rfc2425) Backslashes, newlines (\n or \N) and comma's must be + * escaped, all time time. + * * Comma's are used for delimiters in multiple values + * * (rfc2426) Adds to to this that the semi-colon MUST also be escaped, + * as in some properties semi-colon is used for separators. + * * Properties using semi-colons: N, ADR, GEO, ORG + * * Both ADR and N's individual parts may be broken up further with a + * comma. + * * Properties using commas: NICKNAME, CATEGORIES + * + * vCard 4.0 (rfc6350) says: + * * Commas must be escaped. + * * Semi-colons may be escaped, an unescaped semi-colon _may_ be a + * delimiter, depending on the property. + * * Backslashes must be escaped + * * Newlines must be escaped as either \N or \n. + * * Some compound properties may contain multiple parts themselves, so a + * comma within a semi-colon delimited property may also be unescaped + * to denote multiple parts _within_ the compound property. + * * Text-properties using semi-colons: N, ADR, ORG, CLIENTPIDMAP. + * * Text-properties using commas: NICKNAME, RELATED, CATEGORIES, PID. + * + * Even though the spec says that commas must always be escaped, the + * example for GEO in Section 6.5.2 seems to violate this. + * + * iCalendar 2.0 (rfc5545) says: + * * Commas or semi-colons may be used as delimiters, depending on the + * property. + * * Commas, semi-colons, backslashes, newline (\N or \n) are always + * escaped, unless they are delimiters. + * * Colons shall not be escaped. + * * Commas can be considered the 'default delimiter' and is described as + * the delimiter in cases where the order of the multiple values is + * insignificant. + * * Semi-colons are described as the delimiter for 'structured values'. + * They are specifically used in Semi-colons are used as a delimiter in + * REQUEST-STATUS, RRULE, GEO and EXRULE. EXRULE is deprecated however. + * + * Now for the parameters + * + * If delimiter is not set (empty string) this method will just return a string. + * If it's a comma or a semi-colon the string will be split on those + * characters, and always return an array. + * + * @param string $input + * @param string $delimiter + * + * @return string|string[] + */ + public static function unescapeValue($input, $delimiter = ';') + { + $regex = '# (?: (\\\\ (?: \\\\ | N | n | ; | , ) )'; + if ($delimiter) { + $regex .= ' | ('.$delimiter.')'; + } + $regex .= ') #x'; + + $matches = preg_split($regex, $input, -1, PREG_SPLIT_DELIM_CAPTURE | PREG_SPLIT_NO_EMPTY); + + $resultArray = []; + $result = ''; + + foreach ($matches as $match) { + switch ($match) { + case '\\\\': + $result .= '\\'; + break; + case '\N': + case '\n': + $result .= "\n"; + break; + case '\;': + $result .= ';'; + break; + case '\,': + $result .= ','; + break; + case $delimiter: + $resultArray[] = $result; + $result = ''; + break; + default: + $result .= $match; + break; + } + } + + $resultArray[] = $result; + + return $delimiter ? $resultArray : $result; + } + + /** + * Unescapes a parameter value. + * + * vCard 2.1: + * * Does not mention a mechanism for this. In addition, double quotes + * are never used to wrap values. + * * This means that parameters can simply not contain colons or + * semi-colons. + * + * vCard 3.0 (rfc2425, rfc2426): + * * Parameters _may_ be surrounded by double quotes. + * * If this is not the case, semi-colon, colon and comma may simply not + * occur (the comma used for multiple parameter values though). + * * If it is surrounded by double-quotes, it may simply not contain + * double-quotes. + * * This means that a parameter can in no case encode double-quotes, or + * newlines. + * + * vCard 4.0 (rfc6350) + * * Behavior seems to be identical to vCard 3.0 + * + * iCalendar 2.0 (rfc5545) + * * Behavior seems to be identical to vCard 3.0 + * + * Parameter escaping mechanism (rfc6868) : + * * This rfc describes a new way to escape parameter values. + * * New-line is encoded as ^n + * * ^ is encoded as ^^. + * * " is encoded as ^' + * + * @param string $input + */ + private function unescapeParam($input) + { + return + preg_replace_callback( + '#(\^(\^|n|\'))#', + function ($matches) { + switch ($matches[2]) { + case 'n': + return "\n"; + case '^': + return '^'; + case '\'': + return '"'; + + // @codeCoverageIgnoreStart + } + // @codeCoverageIgnoreEnd + }, + $input + ); + } + + /** + * Gets the full quoted printable value. + * + * We need a special method for this, because newlines have both a meaning + * in vCards, and in QuotedPrintable. + * + * This method does not do any decoding. + * + * @return string + */ + private function extractQuotedPrintableValue() + { + // We need to parse the raw line again to get the start of the value. + // + // We are basically looking for the first colon (:), but we need to + // skip over the parameters first, as they may contain one. + $regex = '/^ + (?: [^:])+ # Anything but a colon + (?: "[^"]")* # A parameter in double quotes + : # start of the value we really care about + (.*)$ + /xs'; + + preg_match($regex, $this->rawLine, $matches); + + $value = $matches[1]; + // Removing the first whitespace character from every line. Kind of + // like unfolding, but we keep the newline. + $value = str_replace("\n ", "\n", $value); + + // Microsoft products don't always correctly fold lines, they may be + // missing a whitespace. So if 'forgiving' is turned on, we will take + // those as well. + if ($this->options & self::OPTION_FORGIVING) { + while ('=' === substr($value, -1) && $this->lineBuffer) { + // Reading the line + $this->readLine(); + // Grabbing the raw form + $value .= "\n".$this->rawLine; + } + } + + return $value; + } +} diff --git a/3rdparty/sabre/vobject/lib/Parser/Parser.php b/3rdparty/sabre/vobject/lib/Parser/Parser.php new file mode 100644 index 00000000..b7b61143 --- /dev/null +++ b/3rdparty/sabre/vobject/lib/Parser/Parser.php @@ -0,0 +1,75 @@ +setInput($input); + } + $this->options = $options; + } + + /** + * This method starts the parsing process. + * + * If the input was not supplied during construction, it's possible to pass + * it here instead. + * + * If either input or options are not supplied, the defaults will be used. + * + * @param mixed $input + * @param int $options + * + * @return array + */ + abstract public function parse($input = null, $options = 0); + + /** + * Sets the input data. + * + * @param mixed $input + */ + abstract public function setInput($input); +} diff --git a/3rdparty/sabre/vobject/lib/Parser/XML.php b/3rdparty/sabre/vobject/lib/Parser/XML.php new file mode 100644 index 00000000..78773173 --- /dev/null +++ b/3rdparty/sabre/vobject/lib/Parser/XML.php @@ -0,0 +1,377 @@ +setInput($input); + } + + if (0 !== $options) { + $this->options = $options; + } + + if (is_null($this->input)) { + throw new EofException('End of input stream, or no input supplied'); + } + + switch ($this->input['name']) { + case '{'.self::XCAL_NAMESPACE.'}icalendar': + $this->root = new VCalendar([], false); + $this->pointer = &$this->input['value'][0]; + $this->parseVCalendarComponents($this->root); + break; + + case '{'.self::XCARD_NAMESPACE.'}vcards': + foreach ($this->input['value'] as &$vCard) { + $this->root = new VCard(['version' => '4.0'], false); + $this->pointer = &$vCard; + $this->parseVCardComponents($this->root); + + // We just parse the first element. + break; + } + break; + + default: + throw new ParseException('Unsupported XML standard'); + } + + return $this->root; + } + + /** + * Parse a xCalendar component. + */ + protected function parseVCalendarComponents(Component $parentComponent) + { + foreach ($this->pointer['value'] ?: [] as $children) { + switch (static::getTagName($children['name'])) { + case 'properties': + $this->pointer = &$children['value']; + $this->parseProperties($parentComponent); + break; + + case 'components': + $this->pointer = &$children; + $this->parseComponent($parentComponent); + break; + } + } + } + + /** + * Parse a xCard component. + */ + protected function parseVCardComponents(Component $parentComponent) + { + $this->pointer = &$this->pointer['value']; + $this->parseProperties($parentComponent); + } + + /** + * Parse xCalendar and xCard properties. + * + * @param string $propertyNamePrefix + */ + protected function parseProperties(Component $parentComponent, $propertyNamePrefix = '') + { + foreach ($this->pointer ?: [] as $xmlProperty) { + list($namespace, $tagName) = SabreXml\Service::parseClarkNotation($xmlProperty['name']); + + $propertyName = $tagName; + $propertyValue = []; + $propertyParameters = []; + $propertyType = 'text'; + + // A property which is not part of the standard. + if (self::XCAL_NAMESPACE !== $namespace + && self::XCARD_NAMESPACE !== $namespace) { + $propertyName = 'xml'; + $value = '<'.$tagName.' xmlns="'.$namespace.'"'; + + foreach ($xmlProperty['attributes'] as $attributeName => $attributeValue) { + $value .= ' '.$attributeName.'="'.str_replace('"', '\"', $attributeValue).'"'; + } + + $value .= '>'.$xmlProperty['value'].''; + + $propertyValue = [$value]; + + $this->createProperty( + $parentComponent, + $propertyName, + $propertyParameters, + $propertyType, + $propertyValue + ); + + continue; + } + + // xCard group. + if ('group' === $propertyName) { + if (!isset($xmlProperty['attributes']['name'])) { + continue; + } + + $this->pointer = &$xmlProperty['value']; + $this->parseProperties( + $parentComponent, + strtoupper($xmlProperty['attributes']['name']).'.' + ); + + continue; + } + + // Collect parameters. + foreach ($xmlProperty['value'] as $i => $xmlPropertyChild) { + if (!is_array($xmlPropertyChild) + || 'parameters' !== static::getTagName($xmlPropertyChild['name'])) { + continue; + } + + $xmlParameters = $xmlPropertyChild['value']; + + foreach ($xmlParameters as $xmlParameter) { + $propertyParameterValues = []; + + foreach ($xmlParameter['value'] as $xmlParameterValues) { + $propertyParameterValues[] = $xmlParameterValues['value']; + } + + $propertyParameters[static::getTagName($xmlParameter['name'])] + = implode(',', $propertyParameterValues); + } + + array_splice($xmlProperty['value'], $i, 1); + } + + $propertyNameExtended = ($this->root instanceof VCalendar + ? 'xcal' + : 'xcard').':'.$propertyName; + + switch ($propertyNameExtended) { + case 'xcal:geo': + $propertyType = 'float'; + $propertyValue['latitude'] = 0; + $propertyValue['longitude'] = 0; + + foreach ($xmlProperty['value'] as $xmlRequestChild) { + $propertyValue[static::getTagName($xmlRequestChild['name'])] + = $xmlRequestChild['value']; + } + break; + + case 'xcal:request-status': + $propertyType = 'text'; + + foreach ($xmlProperty['value'] as $xmlRequestChild) { + $propertyValue[static::getTagName($xmlRequestChild['name'])] + = $xmlRequestChild['value']; + } + break; + + case 'xcal:freebusy': + $propertyType = 'freebusy'; + // We don't break because we only want to set + // another property type. + + // no break + case 'xcal:categories': + case 'xcal:resources': + case 'xcal:exdate': + foreach ($xmlProperty['value'] as $specialChild) { + $propertyValue[static::getTagName($specialChild['name'])] + = $specialChild['value']; + } + break; + + case 'xcal:rdate': + $propertyType = 'date-time'; + + foreach ($xmlProperty['value'] as $specialChild) { + $tagName = static::getTagName($specialChild['name']); + + if ('period' === $tagName) { + $propertyParameters['value'] = 'PERIOD'; + $propertyValue[] = implode('/', $specialChild['value']); + } else { + $propertyValue[] = $specialChild['value']; + } + } + break; + + default: + $propertyType = static::getTagName($xmlProperty['value'][0]['name']); + + foreach ($xmlProperty['value'] as $value) { + $propertyValue[] = $value['value']; + } + + if ('date' === $propertyType) { + $propertyParameters['value'] = 'DATE'; + } + break; + } + + $this->createProperty( + $parentComponent, + $propertyNamePrefix.$propertyName, + $propertyParameters, + $propertyType, + $propertyValue + ); + } + } + + /** + * Parse a component. + */ + protected function parseComponent(Component $parentComponent) + { + $components = $this->pointer['value'] ?: []; + + foreach ($components as $component) { + $componentName = static::getTagName($component['name']); + $currentComponent = $this->root->createComponent( + $componentName, + null, + false + ); + + $this->pointer = &$component; + $this->parseVCalendarComponents($currentComponent); + + $parentComponent->add($currentComponent); + } + } + + /** + * Create a property. + * + * @param string $name + * @param array $parameters + * @param string $type + * @param mixed $value + */ + protected function createProperty(Component $parentComponent, $name, $parameters, $type, $value) + { + $property = $this->root->createProperty( + $name, + null, + $parameters, + $type + ); + $parentComponent->add($property); + $property->setXmlValue($value); + } + + /** + * Sets the input data. + * + * @param resource|string $input + */ + public function setInput($input) + { + if (is_resource($input)) { + $input = stream_get_contents($input); + } + + if (is_string($input)) { + $reader = new SabreXml\Reader(); + $reader->elementMap['{'.self::XCAL_NAMESPACE.'}period'] + = XML\Element\KeyValue::class; + $reader->elementMap['{'.self::XCAL_NAMESPACE.'}recur'] + = XML\Element\KeyValue::class; + $reader->xml($input); + $input = $reader->parse(); + } + + $this->input = $input; + } + + /** + * Get tag name from a Clark notation. + * + * @param string $clarkedTagName + * + * @return string + */ + protected static function getTagName($clarkedTagName) + { + list(, $tagName) = SabreXml\Service::parseClarkNotation($clarkedTagName); + + return $tagName; + } +} diff --git a/3rdparty/sabre/vobject/lib/Parser/XML/Element/KeyValue.php b/3rdparty/sabre/vobject/lib/Parser/XML/Element/KeyValue.php new file mode 100644 index 00000000..e0177251 --- /dev/null +++ b/3rdparty/sabre/vobject/lib/Parser/XML/Element/KeyValue.php @@ -0,0 +1,63 @@ +next(); + * + * $reader->parseInnerTree() will parse the entire sub-tree, and advance to + * the next element. + * + * @param XML\Reader $reader + */ + public static function xmlDeserialize(SabreXml\Reader $reader): array + { + // If there's no children, we don't do anything. + if ($reader->isEmptyElement) { + $reader->next(); + + return []; + } + + $values = []; + $reader->read(); + + do { + if (SabreXml\Reader::ELEMENT === $reader->nodeType) { + $name = $reader->localName; + $values[$name] = $reader->parseCurrentElement()['value']; + } else { + $reader->read(); + } + } while (SabreXml\Reader::END_ELEMENT !== $reader->nodeType); + + $reader->read(); + + return $values; + } +} diff --git a/3rdparty/sabre/vobject/lib/Property.php b/3rdparty/sabre/vobject/lib/Property.php new file mode 100644 index 00000000..f52760f9 --- /dev/null +++ b/3rdparty/sabre/vobject/lib/Property.php @@ -0,0 +1,642 @@ +value syntax. + * + * @param Component $root The root document + * @param string $name + * @param string|array|null $value + * @param array $parameters List of parameters + * @param string $group The vcard property group + */ + public function __construct(Component $root, $name, $value = null, array $parameters = [], $group = null, ?int $lineIndex = null, ?string $lineString = null) + { + $this->name = $name; + $this->group = $group; + + $this->root = $root; + + foreach ($parameters as $k => $v) { + $this->add($k, $v); + } + + if (!is_null($value)) { + $this->setValue($value); + } + + if (!is_null($lineIndex)) { + $this->lineIndex = $lineIndex; + } + + if (!is_null($lineString)) { + $this->lineString = $lineString; + } + } + + /** + * Updates the current value. + * + * This may be either a single, or multiple strings in an array. + * + * @param string|array $value + */ + public function setValue($value) + { + $this->value = $value; + } + + /** + * Returns the current value. + * + * This method will always return a singular value. If this was a + * multi-value object, some decision will be made first on how to represent + * it as a string. + * + * To get the correct multi-value version, use getParts. + * + * @return string + */ + public function getValue() + { + if (is_array($this->value)) { + if (0 == count($this->value)) { + return; + } elseif (1 === count($this->value)) { + return $this->value[0]; + } else { + return $this->getRawMimeDirValue(); + } + } else { + return $this->value; + } + } + + /** + * Sets a multi-valued property. + */ + public function setParts(array $parts) + { + $this->value = $parts; + } + + /** + * Returns a multi-valued property. + * + * This method always returns an array, if there was only a single value, + * it will still be wrapped in an array. + * + * @return array + */ + public function getParts() + { + if (is_null($this->value)) { + return []; + } elseif (is_array($this->value)) { + return $this->value; + } else { + return [$this->value]; + } + } + + /** + * Adds a new parameter. + * + * If a parameter with same name already existed, the values will be + * combined. + * If nameless parameter is added, we try to guess its name. + * + * @param string $name + * @param string|array|null $value + */ + public function add($name, $value = null) + { + $noName = false; + if (null === $name) { + $name = Parameter::guessParameterNameByValue($value); + $noName = true; + } + + if (isset($this->parameters[strtoupper($name)])) { + $this->parameters[strtoupper($name)]->addValue($value); + } else { + $param = new Parameter($this->root, $name, $value); + $param->noName = $noName; + $this->parameters[$param->name] = $param; + } + } + + /** + * Returns an iterable list of children. + * + * @return array + */ + public function parameters() + { + return $this->parameters; + } + + /** + * Returns the type of value. + * + * This corresponds to the VALUE= parameter. Every property also has a + * 'default' valueType. + * + * @return string + */ + abstract public function getValueType(); + + /** + * Sets a raw value coming from a mimedir (iCalendar/vCard) file. + * + * This has been 'unfolded', so only 1 line will be passed. Unescaping is + * not yet done, but parameters are not included. + * + * @param string $val + */ + abstract public function setRawMimeDirValue($val); + + /** + * Returns a raw mime-dir representation of the value. + * + * @return string + */ + abstract public function getRawMimeDirValue(); + + /** + * Turns the object back into a serialized blob. + * + * @return string + */ + public function serialize() + { + $str = $this->name; + if ($this->group) { + $str = $this->group.'.'.$this->name; + } + + foreach ($this->parameters() as $param) { + $str .= ';'.$param->serialize(); + } + + $str .= ':'.$this->getRawMimeDirValue(); + + $str = \preg_replace( + '/( + (?:^.)? # 1 additional byte in first line because of missing single space (see next line) + .{1,74} # max 75 bytes per line (1 byte is used for a single space added after every CRLF) + (?![\x80-\xbf]) # prevent splitting multibyte characters + )/x', + "$1\r\n ", + $str + ); + + // remove single space after last CRLF + return \substr($str, 0, -1); + } + + /** + * Returns the value, in the format it should be encoded for JSON. + * + * This method must always return an array. + * + * @return array + */ + public function getJsonValue() + { + return $this->getParts(); + } + + /** + * Sets the JSON value, as it would appear in a jCard or jCal object. + * + * The value must always be an array. + */ + public function setJsonValue(array $value) + { + if (1 === count($value)) { + $this->setValue(reset($value)); + } else { + $this->setValue($value); + } + } + + /** + * This method returns an array, with the representation as it should be + * encoded in JSON. This is used to create jCard or jCal documents. + * + * @return array + */ + #[\ReturnTypeWillChange] + public function jsonSerialize() + { + $parameters = []; + + foreach ($this->parameters as $parameter) { + if ('VALUE' === $parameter->name) { + continue; + } + $parameters[strtolower($parameter->name)] = $parameter->jsonSerialize(); + } + // In jCard, we need to encode the property-group as a separate 'group' + // parameter. + if ($this->group) { + $parameters['group'] = $this->group; + } + + return array_merge( + [ + strtolower($this->name), + (object) $parameters, + strtolower($this->getValueType()), + ], + $this->getJsonValue() + ); + } + + /** + * Hydrate data from a XML subtree, as it would appear in a xCard or xCal + * object. + */ + public function setXmlValue(array $value) + { + $this->setJsonValue($value); + } + + /** + * This method serializes the data into XML. This is used to create xCard or + * xCal documents. + * + * @param Xml\Writer $writer XML writer + */ + public function xmlSerialize(Xml\Writer $writer): void + { + $parameters = []; + + foreach ($this->parameters as $parameter) { + if ('VALUE' === $parameter->name) { + continue; + } + + $parameters[] = $parameter; + } + + $writer->startElement(strtolower($this->name)); + + if (!empty($parameters)) { + $writer->startElement('parameters'); + + foreach ($parameters as $parameter) { + $writer->startElement(strtolower($parameter->name)); + $writer->write($parameter); + $writer->endElement(); + } + + $writer->endElement(); + } + + $this->xmlSerializeValue($writer); + $writer->endElement(); + } + + /** + * This method serializes only the value of a property. This is used to + * create xCard or xCal documents. + * + * @param Xml\Writer $writer XML writer + */ + protected function xmlSerializeValue(Xml\Writer $writer) + { + $valueType = strtolower($this->getValueType()); + + foreach ($this->getJsonValue() as $values) { + foreach ((array) $values as $value) { + $writer->writeElement($valueType, $value); + } + } + } + + /** + * Called when this object is being cast to a string. + * + * If the property only had a single value, you will get just that. In the + * case the property had multiple values, the contents will be escaped and + * combined with ,. + * + * @return string + */ + public function __toString() + { + return (string) $this->getValue(); + } + + /* ArrayAccess interface {{{ */ + + /** + * Checks if an array element exists. + * + * @param mixed $name + * + * @return bool + */ + #[\ReturnTypeWillChange] + public function offsetExists($name) + { + if (is_int($name)) { + return parent::offsetExists($name); + } + + $name = strtoupper($name); + + foreach ($this->parameters as $parameter) { + if ($parameter->name == $name) { + return true; + } + } + + return false; + } + + /** + * Returns a parameter. + * + * If the parameter does not exist, null is returned. + * + * @param string $name + * + * @return Node + */ + #[\ReturnTypeWillChange] + public function offsetGet($name) + { + if (is_int($name)) { + return parent::offsetGet($name); + } + $name = strtoupper($name); + + if (!isset($this->parameters[$name])) { + return; + } + + return $this->parameters[$name]; + } + + /** + * Creates a new parameter. + * + * @param string $name + * @param mixed $value + */ + #[\ReturnTypeWillChange] + public function offsetSet($name, $value) + { + if (is_int($name)) { + parent::offsetSet($name, $value); + // @codeCoverageIgnoreStart + // This will never be reached, because an exception is always + // thrown. + return; + // @codeCoverageIgnoreEnd + } + + $param = new Parameter($this->root, $name, $value); + $this->parameters[$param->name] = $param; + } + + /** + * Removes one or more parameters with the specified name. + * + * @param string $name + */ + #[\ReturnTypeWillChange] + public function offsetUnset($name) + { + if (is_int($name)) { + parent::offsetUnset($name); + // @codeCoverageIgnoreStart + // This will never be reached, because an exception is always + // thrown. + return; + // @codeCoverageIgnoreEnd + } + + unset($this->parameters[strtoupper($name)]); + } + + /* }}} */ + + /** + * This method is automatically called when the object is cloned. + * Specifically, this will ensure all child elements are also cloned. + */ + public function __clone() + { + foreach ($this->parameters as $key => $child) { + $this->parameters[$key] = clone $child; + $this->parameters[$key]->parent = $this; + } + } + + /** + * Validates the node for correctness. + * + * The following options are supported: + * - Node::REPAIR - If something is broken, and automatic repair may + * be attempted. + * + * An array is returned with warnings. + * + * Every item in the array has the following properties: + * * level - (number between 1 and 3 with severity information) + * * message - (human readable message) + * * node - (reference to the offending node) + * + * @param int $options + * + * @return array + */ + public function validate($options = 0) + { + $warnings = []; + + // Checking if our value is UTF-8 + if (!StringUtil::isUTF8($this->getRawMimeDirValue())) { + $oldValue = $this->getRawMimeDirValue(); + $level = 3; + if ($options & self::REPAIR) { + $newValue = StringUtil::convertToUTF8($oldValue); + if (true || StringUtil::isUTF8($newValue)) { + $this->setRawMimeDirValue($newValue); + $level = 1; + } + } + + if (preg_match('%([\x00-\x08\x0B-\x0C\x0E-\x1F\x7F])%', $oldValue, $matches)) { + $message = 'Property contained a control character (0x'.bin2hex($matches[1]).')'; + } else { + $message = 'Property is not valid UTF-8! '.$oldValue; + } + + $warnings[] = [ + 'level' => $level, + 'message' => $message, + 'node' => $this, + ]; + } + + // Checking if the propertyname does not contain any invalid bytes. + if (!preg_match('/^([A-Z0-9-]+)$/', $this->name)) { + $warnings[] = [ + 'level' => $options & self::REPAIR ? 1 : 3, + 'message' => 'The propertyname: '.$this->name.' contains invalid characters. Only A-Z, 0-9 and - are allowed', + 'node' => $this, + ]; + if ($options & self::REPAIR) { + // Uppercasing and converting underscores to dashes. + $this->name = strtoupper( + str_replace('_', '-', $this->name) + ); + // Removing every other invalid character + $this->name = preg_replace('/([^A-Z0-9-])/u', '', $this->name); + } + } + + if ($encoding = $this->offsetGet('ENCODING')) { + if (Document::VCARD40 === $this->root->getDocumentType()) { + $warnings[] = [ + 'level' => 3, + 'message' => 'ENCODING parameter is not valid in vCard 4.', + 'node' => $this, + ]; + } else { + $encoding = (string) $encoding; + + $allowedEncoding = []; + + switch ($this->root->getDocumentType()) { + case Document::ICALENDAR20: + $allowedEncoding = ['8BIT', 'BASE64']; + break; + case Document::VCARD21: + $allowedEncoding = ['QUOTED-PRINTABLE', 'BASE64', '8BIT']; + break; + case Document::VCARD30: + $allowedEncoding = ['B']; + //Repair vCard30 that use BASE64 encoding + if ($options & self::REPAIR) { + if ('BASE64' === strtoupper($encoding)) { + $encoding = 'B'; + $this['ENCODING'] = $encoding; + $warnings[] = [ + 'level' => 1, + 'message' => 'ENCODING=BASE64 has been transformed to ENCODING=B.', + 'node' => $this, + ]; + } + } + break; + } + if ($allowedEncoding && !in_array(strtoupper($encoding), $allowedEncoding)) { + $warnings[] = [ + 'level' => 3, + 'message' => 'ENCODING='.strtoupper($encoding).' is not valid for this document type.', + 'node' => $this, + ]; + } + } + } + + // Validating inner parameters + foreach ($this->parameters as $param) { + $warnings = array_merge($warnings, $param->validate($options)); + } + + return $warnings; + } + + /** + * Call this method on a document if you're done using it. + * + * It's intended to remove all circular references, so PHP can easily clean + * it up. + */ + public function destroy() + { + parent::destroy(); + foreach ($this->parameters as $param) { + $param->destroy(); + } + $this->parameters = []; + } +} diff --git a/3rdparty/sabre/vobject/lib/Property/Binary.php b/3rdparty/sabre/vobject/lib/Property/Binary.php new file mode 100644 index 00000000..1262dd05 --- /dev/null +++ b/3rdparty/sabre/vobject/lib/Property/Binary.php @@ -0,0 +1,109 @@ +value = $value[0]; + } else { + throw new \InvalidArgumentException('The argument must either be a string or an array with only one child'); + } + } else { + $this->value = $value; + } + } + + /** + * Sets a raw value coming from a mimedir (iCalendar/vCard) file. + * + * This has been 'unfolded', so only 1 line will be passed. Unescaping is + * not yet done, but parameters are not included. + * + * @param string $val + */ + public function setRawMimeDirValue($val) + { + $this->value = base64_decode($val); + } + + /** + * Returns a raw mime-dir representation of the value. + * + * @return string + */ + public function getRawMimeDirValue() + { + return base64_encode($this->value); + } + + /** + * Returns the type of value. + * + * This corresponds to the VALUE= parameter. Every property also has a + * 'default' valueType. + * + * @return string + */ + public function getValueType() + { + return 'BINARY'; + } + + /** + * Returns the value, in the format it should be encoded for json. + * + * This method must always return an array. + * + * @return array + */ + public function getJsonValue() + { + return [base64_encode($this->getValue())]; + } + + /** + * Sets the json value, as it would appear in a jCard or jCal object. + * + * The value must always be an array. + */ + public function setJsonValue(array $value) + { + $value = array_map('base64_decode', $value); + parent::setJsonValue($value); + } +} diff --git a/3rdparty/sabre/vobject/lib/Property/Boolean.php b/3rdparty/sabre/vobject/lib/Property/Boolean.php new file mode 100644 index 00000000..4bd6ffdf --- /dev/null +++ b/3rdparty/sabre/vobject/lib/Property/Boolean.php @@ -0,0 +1,72 @@ +setValue($val); + } + + /** + * Returns a raw mime-dir representation of the value. + * + * @return string + */ + public function getRawMimeDirValue() + { + return $this->value ? 'TRUE' : 'FALSE'; + } + + /** + * Returns the type of value. + * + * This corresponds to the VALUE= parameter. Every property also has a + * 'default' valueType. + * + * @return string + */ + public function getValueType() + { + return 'BOOLEAN'; + } + + /** + * Hydrate data from a XML subtree, as it would appear in a xCard or xCal + * object. + */ + public function setXmlValue(array $value) + { + $value = array_map( + function ($value) { + return 'true' === $value; + }, + $value + ); + parent::setXmlValue($value); + } +} diff --git a/3rdparty/sabre/vobject/lib/Property/FlatText.php b/3rdparty/sabre/vobject/lib/Property/FlatText.php new file mode 100644 index 00000000..d15cfe05 --- /dev/null +++ b/3rdparty/sabre/vobject/lib/Property/FlatText.php @@ -0,0 +1,46 @@ +setValue($val); + } +} diff --git a/3rdparty/sabre/vobject/lib/Property/FloatValue.php b/3rdparty/sabre/vobject/lib/Property/FloatValue.php new file mode 100644 index 00000000..e780ae6c --- /dev/null +++ b/3rdparty/sabre/vobject/lib/Property/FloatValue.php @@ -0,0 +1,124 @@ +delimiter, $val); + foreach ($val as &$item) { + $item = (float) $item; + } + $this->setParts($val); + } + + /** + * Returns a raw mime-dir representation of the value. + * + * @return string + */ + public function getRawMimeDirValue() + { + return implode( + $this->delimiter, + $this->getParts() + ); + } + + /** + * Returns the type of value. + * + * This corresponds to the VALUE= parameter. Every property also has a + * 'default' valueType. + * + * @return string + */ + public function getValueType() + { + return 'FLOAT'; + } + + /** + * Returns the value, in the format it should be encoded for JSON. + * + * This method must always return an array. + * + * @return array + */ + public function getJsonValue() + { + $val = array_map('floatval', $this->getParts()); + + // Special-casing the GEO property. + // + // See: + // http://tools.ietf.org/html/draft-ietf-jcardcal-jcal-04#section-3.4.1.2 + if ('GEO' === $this->name) { + return [$val]; + } + + return $val; + } + + /** + * Hydrate data from a XML subtree, as it would appear in a xCard or xCal + * object. + */ + public function setXmlValue(array $value) + { + $value = array_map('floatval', $value); + parent::setXmlValue($value); + } + + /** + * This method serializes only the value of a property. This is used to + * create xCard or xCal documents. + * + * @param Xml\Writer $writer XML writer + */ + protected function xmlSerializeValue(Xml\Writer $writer) + { + // Special-casing the GEO property. + // + // See: + // http://tools.ietf.org/html/rfc6321#section-3.4.1.2 + if ('GEO' === $this->name) { + $value = array_map('floatval', $this->getParts()); + + $writer->writeElement('latitude', $value[0]); + $writer->writeElement('longitude', $value[1]); + } else { + parent::xmlSerializeValue($writer); + } + } +} diff --git a/3rdparty/sabre/vobject/lib/Property/ICalendar/CalAddress.php b/3rdparty/sabre/vobject/lib/Property/ICalendar/CalAddress.php new file mode 100644 index 00000000..c90967d7 --- /dev/null +++ b/3rdparty/sabre/vobject/lib/Property/ICalendar/CalAddress.php @@ -0,0 +1,63 @@ +getValue(); + if (!strpos($input, ':')) { + return $input; + } + list($schema, $everythingElse) = explode(':', $input, 2); + $schema = strtolower($schema); + if ('mailto' === $schema) { + $everythingElse = strtolower($everythingElse); + } + + return $schema.':'.$everythingElse; + } +} diff --git a/3rdparty/sabre/vobject/lib/Property/ICalendar/Date.php b/3rdparty/sabre/vobject/lib/Property/ICalendar/Date.php new file mode 100644 index 00000000..d8e86d13 --- /dev/null +++ b/3rdparty/sabre/vobject/lib/Property/ICalendar/Date.php @@ -0,0 +1,18 @@ +setDateTimes($parts); + } else { + parent::setParts($parts); + } + } + + /** + * Updates the current value. + * + * This may be either a single, or multiple strings in an array. + * + * Instead of strings, you may also use DateTime here. + * + * @param string|array|DateTimeInterface $value + */ + public function setValue($value) + { + if (is_array($value) && isset($value[0]) && $value[0] instanceof DateTimeInterface) { + $this->setDateTimes($value); + } elseif ($value instanceof DateTimeInterface) { + $this->setDateTimes([$value]); + } else { + parent::setValue($value); + } + } + + /** + * Sets a raw value coming from a mimedir (iCalendar/vCard) file. + * + * This has been 'unfolded', so only 1 line will be passed. Unescaping is + * not yet done, but parameters are not included. + * + * @param string $val + */ + public function setRawMimeDirValue($val) + { + $this->setValue(explode($this->delimiter, $val)); + } + + /** + * Returns a raw mime-dir representation of the value. + * + * @return string + */ + public function getRawMimeDirValue() + { + return implode($this->delimiter, $this->getParts()); + } + + /** + * Returns true if this is a DATE-TIME value, false if it's a DATE. + * + * @return bool + */ + public function hasTime() + { + return 'DATE' !== strtoupper((string) $this['VALUE']); + } + + /** + * Returns true if this is a floating DATE or DATE-TIME. + * + * Note that DATE is always floating. + */ + public function isFloating() + { + return + !$this->hasTime() || + ( + !isset($this['TZID']) && + false === strpos($this->getValue(), 'Z') + ); + } + + /** + * Returns a date-time value. + * + * Note that if this property contained more than 1 date-time, only the + * first will be returned. To get an array with multiple values, call + * getDateTimes. + * + * If no timezone information is known, because it's either an all-day + * property or floating time, we will use the DateTimeZone argument to + * figure out the exact date. + * + * @param DateTimeZone $timeZone + * + * @return \DateTimeImmutable + */ + public function getDateTime(?DateTimeZone $timeZone = null) + { + $dt = $this->getDateTimes($timeZone); + if (!$dt) { + return; + } + + return $dt[0]; + } + + /** + * Returns multiple date-time values. + * + * If no timezone information is known, because it's either an all-day + * property or floating time, we will use the DateTimeZone argument to + * figure out the exact date. + * + * @param DateTimeZone $timeZone + * + * @return \DateTimeImmutable[] + * @return \DateTime[] + */ + public function getDateTimes(?DateTimeZone $timeZone = null) + { + // Does the property have a TZID? + $tzid = $this['TZID']; + + if ($tzid) { + $timeZone = TimeZoneUtil::getTimeZone((string) $tzid, $this->root); + } + + $dts = []; + foreach ($this->getParts() as $part) { + $dts[] = DateTimeParser::parse($part, $timeZone); + } + + return $dts; + } + + /** + * Sets the property as a DateTime object. + * + * @param bool isFloating If set to true, timezones will be ignored + */ + public function setDateTime(DateTimeInterface $dt, $isFloating = false) + { + $this->setDateTimes([$dt], $isFloating); + } + + /** + * Sets the property as multiple date-time objects. + * + * The first value will be used as a reference for the timezones, and all + * the other values will be adjusted for that timezone + * + * @param DateTimeInterface[] $dt + * @param bool isFloating If set to true, timezones will be ignored + */ + public function setDateTimes(array $dt, $isFloating = false) + { + $values = []; + + if ($this->hasTime()) { + $tz = null; + $isUtc = false; + + foreach ($dt as $d) { + if ($isFloating) { + $values[] = $d->format('Ymd\\THis'); + continue; + } + if (is_null($tz)) { + $tz = $d->getTimeZone(); + $isUtc = in_array($tz->getName(), ['UTC', 'GMT', 'Z', '+00:00']); + if (!$isUtc) { + $this->offsetSet('TZID', $tz->getName()); + } + } else { + $d = $d->setTimeZone($tz); + } + + if ($isUtc) { + $values[] = $d->format('Ymd\\THis\\Z'); + } else { + $values[] = $d->format('Ymd\\THis'); + } + } + if ($isUtc || $isFloating) { + $this->offsetUnset('TZID'); + } + } else { + foreach ($dt as $d) { + $values[] = $d->format('Ymd'); + } + $this->offsetUnset('TZID'); + } + + $this->value = $values; + } + + /** + * Returns the type of value. + * + * This corresponds to the VALUE= parameter. Every property also has a + * 'default' valueType. + * + * @return string + */ + public function getValueType() + { + return $this->hasTime() ? 'DATE-TIME' : 'DATE'; + } + + /** + * Returns the value, in the format it should be encoded for JSON. + * + * This method must always return an array. + * + * @return array + */ + public function getJsonValue() + { + $dts = $this->getDateTimes(); + $hasTime = $this->hasTime(); + $isFloating = $this->isFloating(); + + $tz = $dts[0]->getTimeZone(); + $isUtc = $isFloating ? false : in_array($tz->getName(), ['UTC', 'GMT', 'Z']); + + return array_map( + function (DateTimeInterface $dt) use ($hasTime, $isUtc) { + if ($hasTime) { + return $dt->format('Y-m-d\\TH:i:s').($isUtc ? 'Z' : ''); + } else { + return $dt->format('Y-m-d'); + } + }, + $dts + ); + } + + /** + * Sets the json value, as it would appear in a jCard or jCal object. + * + * The value must always be an array. + */ + public function setJsonValue(array $value) + { + // dates and times in jCal have one difference to dates and times in + // iCalendar. In jCal date-parts are separated by dashes, and + // time-parts are separated by colons. It makes sense to just remove + // those. + $this->setValue( + array_map( + function ($item) { + return strtr($item, [':' => '', '-' => '']); + }, + $value + ) + ); + } + + /** + * We need to intercept offsetSet, because it may be used to alter the + * VALUE from DATE-TIME to DATE or vice-versa. + * + * @param string $name + * @param mixed $value + */ + #[\ReturnTypeWillChange] + public function offsetSet($name, $value) + { + parent::offsetSet($name, $value); + if ('VALUE' !== strtoupper($name)) { + return; + } + + // This will ensure that dates are correctly encoded. + $this->setDateTimes($this->getDateTimes()); + } + + /** + * Validates the node for correctness. + * + * The following options are supported: + * Node::REPAIR - May attempt to automatically repair the problem. + * + * This method returns an array with detected problems. + * Every element has the following properties: + * + * * level - problem level. + * * message - A human-readable string describing the issue. + * * node - A reference to the problematic node. + * + * The level means: + * 1 - The issue was repaired (only happens if REPAIR was turned on) + * 2 - An inconsequential issue + * 3 - A severe issue. + * + * @param int $options + * + * @return array + */ + public function validate($options = 0) + { + $messages = parent::validate($options); + $valueType = $this->getValueType(); + $values = $this->getParts(); + foreach ($values as $value) { + try { + switch ($valueType) { + case 'DATE': + DateTimeParser::parseDate($value); + break; + case 'DATE-TIME': + DateTimeParser::parseDateTime($value); + break; + } + } catch (InvalidDataException $e) { + $messages[] = [ + 'level' => 3, + 'message' => 'The supplied value ('.$value.') is not a correct '.$valueType, + 'node' => $this, + ]; + break; + } + } + + return $messages; + } +} diff --git a/3rdparty/sabre/vobject/lib/Property/ICalendar/Duration.php b/3rdparty/sabre/vobject/lib/Property/ICalendar/Duration.php new file mode 100644 index 00000000..e18fe191 --- /dev/null +++ b/3rdparty/sabre/vobject/lib/Property/ICalendar/Duration.php @@ -0,0 +1,79 @@ +setValue(explode($this->delimiter, $val)); + } + + /** + * Returns a raw mime-dir representation of the value. + * + * @return string + */ + public function getRawMimeDirValue() + { + return implode($this->delimiter, $this->getParts()); + } + + /** + * Returns the type of value. + * + * This corresponds to the VALUE= parameter. Every property also has a + * 'default' valueType. + * + * @return string + */ + public function getValueType() + { + return 'DURATION'; + } + + /** + * Returns a DateInterval representation of the Duration property. + * + * If the property has more than one value, only the first is returned. + * + * @return \DateInterval + */ + public function getDateInterval() + { + $parts = $this->getParts(); + $value = $parts[0]; + + return DateTimeParser::parseDuration($value); + } +} diff --git a/3rdparty/sabre/vobject/lib/Property/ICalendar/Period.php b/3rdparty/sabre/vobject/lib/Property/ICalendar/Period.php new file mode 100644 index 00000000..ae8a7891 --- /dev/null +++ b/3rdparty/sabre/vobject/lib/Property/ICalendar/Period.php @@ -0,0 +1,135 @@ +setValue(explode($this->delimiter, $val)); + } + + /** + * Returns a raw mime-dir representation of the value. + * + * @return string + */ + public function getRawMimeDirValue() + { + return implode($this->delimiter, $this->getParts()); + } + + /** + * Returns the type of value. + * + * This corresponds to the VALUE= parameter. Every property also has a + * 'default' valueType. + * + * @return string + */ + public function getValueType() + { + return 'PERIOD'; + } + + /** + * Sets the json value, as it would appear in a jCard or jCal object. + * + * The value must always be an array. + */ + public function setJsonValue(array $value) + { + $value = array_map( + function ($item) { + return strtr(implode('/', $item), [':' => '', '-' => '']); + }, + $value + ); + parent::setJsonValue($value); + } + + /** + * Returns the value, in the format it should be encoded for json. + * + * This method must always return an array. + * + * @return array + */ + public function getJsonValue() + { + $return = []; + foreach ($this->getParts() as $item) { + list($start, $end) = explode('/', $item, 2); + + $start = DateTimeParser::parseDateTime($start); + + // This is a duration value. + if ('P' === $end[0]) { + $return[] = [ + $start->format('Y-m-d\\TH:i:s'), + $end, + ]; + } else { + $end = DateTimeParser::parseDateTime($end); + $return[] = [ + $start->format('Y-m-d\\TH:i:s'), + $end->format('Y-m-d\\TH:i:s'), + ]; + } + } + + return $return; + } + + /** + * This method serializes only the value of a property. This is used to + * create xCard or xCal documents. + * + * @param Xml\Writer $writer XML writer + */ + protected function xmlSerializeValue(Xml\Writer $writer) + { + $writer->startElement(strtolower($this->getValueType())); + $value = $this->getJsonValue(); + $writer->writeElement('start', $value[0][0]); + + if ('P' === $value[0][1][0]) { + $writer->writeElement('duration', $value[0][1]); + } else { + $writer->writeElement('end', $value[0][1]); + } + + $writer->endElement(); + } +} diff --git a/3rdparty/sabre/vobject/lib/Property/ICalendar/Recur.php b/3rdparty/sabre/vobject/lib/Property/ICalendar/Recur.php new file mode 100644 index 00000000..cd3d7a5e --- /dev/null +++ b/3rdparty/sabre/vobject/lib/Property/ICalendar/Recur.php @@ -0,0 +1,344 @@ +value array that is accessible using + * getParts, and may be set using setParts. + * + * @copyright Copyright (C) fruux GmbH (https://fruux.com/) + * @author Evert Pot (http://evertpot.com/) + * @license http://sabre.io/license/ Modified BSD License + */ +class Recur extends Property +{ + /** + * Updates the current value. + * + * This may be either a single, or multiple strings in an array. + * + * @param string|array $value + */ + public function setValue($value) + { + // If we're getting the data from json, we'll be receiving an object + if ($value instanceof \StdClass) { + $value = (array) $value; + } + + if (is_array($value)) { + $newVal = []; + foreach ($value as $k => $v) { + if (is_string($v)) { + $v = strtoupper($v); + + // The value had multiple sub-values + if (false !== strpos($v, ',')) { + $v = explode(',', $v); + } + if (0 === strcmp($k, 'until')) { + $v = strtr($v, [':' => '', '-' => '']); + } + } elseif (is_array($v)) { + $v = array_map('strtoupper', $v); + } + + $newVal[strtoupper($k)] = $v; + } + $this->value = $newVal; + } elseif (is_string($value)) { + $this->value = self::stringToArray($value); + } else { + throw new \InvalidArgumentException('You must either pass a string, or a key=>value array'); + } + } + + /** + * Returns the current value. + * + * This method will always return a singular value. If this was a + * multi-value object, some decision will be made first on how to represent + * it as a string. + * + * To get the correct multi-value version, use getParts. + * + * @return string + */ + public function getValue() + { + $out = []; + foreach ($this->value as $key => $value) { + $out[] = $key.'='.(is_array($value) ? implode(',', $value) : $value); + } + + return strtoupper(implode(';', $out)); + } + + /** + * Sets a multi-valued property. + */ + public function setParts(array $parts) + { + $this->setValue($parts); + } + + /** + * Returns a multi-valued property. + * + * This method always returns an array, if there was only a single value, + * it will still be wrapped in an array. + * + * @return array + */ + public function getParts() + { + return $this->value; + } + + /** + * Sets a raw value coming from a mimedir (iCalendar/vCard) file. + * + * This has been 'unfolded', so only 1 line will be passed. Unescaping is + * not yet done, but parameters are not included. + * + * @param string $val + */ + public function setRawMimeDirValue($val) + { + $this->setValue($val); + } + + /** + * Returns a raw mime-dir representation of the value. + * + * @return string + */ + public function getRawMimeDirValue() + { + return $this->getValue(); + } + + /** + * Returns the type of value. + * + * This corresponds to the VALUE= parameter. Every property also has a + * 'default' valueType. + * + * @return string + */ + public function getValueType() + { + return 'RECUR'; + } + + /** + * Returns the value, in the format it should be encoded for json. + * + * This method must always return an array. + * + * @return array + */ + public function getJsonValue() + { + $values = []; + foreach ($this->getParts() as $k => $v) { + if (0 === strcmp($k, 'UNTIL')) { + $date = new DateTime($this->root, null, $v); + $values[strtolower($k)] = $date->getJsonValue()[0]; + } elseif (0 === strcmp($k, 'COUNT')) { + $values[strtolower($k)] = intval($v); + } else { + $values[strtolower($k)] = $v; + } + } + + return [$values]; + } + + /** + * This method serializes only the value of a property. This is used to + * create xCard or xCal documents. + * + * @param Xml\Writer $writer XML writer + */ + protected function xmlSerializeValue(Xml\Writer $writer) + { + $valueType = strtolower($this->getValueType()); + + foreach ($this->getJsonValue() as $value) { + $writer->writeElement($valueType, $value); + } + } + + /** + * Parses an RRULE value string, and turns it into a struct-ish array. + * + * @param string $value + * + * @return array + */ + public static function stringToArray($value) + { + $value = strtoupper($value); + $newValue = []; + foreach (explode(';', $value) as $part) { + // Skipping empty parts. + if (empty($part)) { + continue; + } + + $parts = explode('=', $part); + + if (2 !== count($parts)) { + throw new InvalidDataException('The supplied iCalendar RRULE part is incorrect: '.$part); + } + + list($partName, $partValue) = $parts; + + // The value itself had multiple values.. + if (false !== strpos($partValue, ',')) { + $partValue = explode(',', $partValue); + } + $newValue[$partName] = $partValue; + } + + return $newValue; + } + + /** + * Validates the node for correctness. + * + * The following options are supported: + * Node::REPAIR - May attempt to automatically repair the problem. + * + * This method returns an array with detected problems. + * Every element has the following properties: + * + * * level - problem level. + * * message - A human-readable string describing the issue. + * * node - A reference to the problematic node. + * + * The level means: + * 1 - The issue was repaired (only happens if REPAIR was turned on) + * 2 - An inconsequential issue + * 3 - A severe issue. + * + * @param int $options + * + * @return array + */ + public function validate($options = 0) + { + $repair = ($options & self::REPAIR); + + $warnings = parent::validate($options); + $values = $this->getParts(); + + foreach ($values as $key => $value) { + if ('' === $value) { + $warnings[] = [ + 'level' => $repair ? 1 : 3, + 'message' => 'Invalid value for '.$key.' in '.$this->name, + 'node' => $this, + ]; + if ($repair) { + unset($values[$key]); + } + } elseif ('BYMONTH' == $key) { + $byMonth = (array) $value; + foreach ($byMonth as $i => $v) { + if (!is_numeric($v) || (int) $v < 1 || (int) $v > 12) { + $warnings[] = [ + 'level' => $repair ? 1 : 3, + 'message' => 'BYMONTH in RRULE must have value(s) between 1 and 12!', + 'node' => $this, + ]; + if ($repair) { + if (is_array($value)) { + unset($values[$key][$i]); + } else { + unset($values[$key]); + } + } + } + } + // if there is no valid entry left, remove the whole value + if (is_array($value) && empty($values[$key])) { + unset($values[$key]); + } + } elseif ('BYWEEKNO' == $key) { + $byWeekNo = (array) $value; + foreach ($byWeekNo as $i => $v) { + if (!is_numeric($v) || (int) $v < -53 || 0 == (int) $v || (int) $v > 53) { + $warnings[] = [ + 'level' => $repair ? 1 : 3, + 'message' => 'BYWEEKNO in RRULE must have value(s) from -53 to -1, or 1 to 53!', + 'node' => $this, + ]; + if ($repair) { + if (is_array($value)) { + unset($values[$key][$i]); + } else { + unset($values[$key]); + } + } + } + } + // if there is no valid entry left, remove the whole value + if (is_array($value) && empty($values[$key])) { + unset($values[$key]); + } + } elseif ('BYYEARDAY' == $key) { + $byYearDay = (array) $value; + foreach ($byYearDay as $i => $v) { + if (!is_numeric($v) || (int) $v < -366 || 0 == (int) $v || (int) $v > 366) { + $warnings[] = [ + 'level' => $repair ? 1 : 3, + 'message' => 'BYYEARDAY in RRULE must have value(s) from -366 to -1, or 1 to 366!', + 'node' => $this, + ]; + if ($repair) { + if (is_array($value)) { + unset($values[$key][$i]); + } else { + unset($values[$key]); + } + } + } + } + // if there is no valid entry left, remove the whole value + if (is_array($value) && empty($values[$key])) { + unset($values[$key]); + } + } + } + if (!isset($values['FREQ'])) { + $warnings[] = [ + 'level' => $repair ? 1 : 3, + 'message' => 'FREQ is required in '.$this->name, + 'node' => $this, + ]; + if ($repair) { + $this->parent->remove($this); + } + } + if ($repair) { + $this->setValue($values); + } + + return $warnings; + } +} diff --git a/3rdparty/sabre/vobject/lib/Property/IntegerValue.php b/3rdparty/sabre/vobject/lib/Property/IntegerValue.php new file mode 100644 index 00000000..3ae77521 --- /dev/null +++ b/3rdparty/sabre/vobject/lib/Property/IntegerValue.php @@ -0,0 +1,76 @@ +setValue((int) $val); + } + + /** + * Returns a raw mime-dir representation of the value. + * + * @return string + */ + public function getRawMimeDirValue() + { + return $this->value; + } + + /** + * Returns the type of value. + * + * This corresponds to the VALUE= parameter. Every property also has a + * 'default' valueType. + * + * @return string + */ + public function getValueType() + { + return 'INTEGER'; + } + + /** + * Returns the value, in the format it should be encoded for json. + * + * This method must always return an array. + * + * @return array + */ + public function getJsonValue() + { + return [(int) $this->getValue()]; + } + + /** + * Hydrate data from a XML subtree, as it would appear in a xCard or xCal + * object. + */ + public function setXmlValue(array $value) + { + $value = array_map('intval', $value); + parent::setXmlValue($value); + } +} diff --git a/3rdparty/sabre/vobject/lib/Property/Text.php b/3rdparty/sabre/vobject/lib/Property/Text.php new file mode 100644 index 00000000..16d2c07f --- /dev/null +++ b/3rdparty/sabre/vobject/lib/Property/Text.php @@ -0,0 +1,392 @@ + 5, + 'ADR' => 7, + ]; + + /** + * Creates the property. + * + * You can specify the parameters either in key=>value syntax, in which case + * parameters will automatically be created, or you can just pass a list of + * Parameter objects. + * + * @param Component $root The root document + * @param string $name + * @param string|array|null $value + * @param array $parameters List of parameters + * @param string $group The vcard property group + */ + public function __construct(Component $root, $name, $value = null, array $parameters = [], $group = null) + { + // There's two types of multi-valued text properties: + // 1. multivalue properties. + // 2. structured value properties + // + // The former is always separated by a comma, the latter by semi-colon. + if (in_array($name, $this->structuredValues)) { + $this->delimiter = ';'; + } + + parent::__construct($root, $name, $value, $parameters, $group); + } + + /** + * Sets a raw value coming from a mimedir (iCalendar/vCard) file. + * + * This has been 'unfolded', so only 1 line will be passed. Unescaping is + * not yet done, but parameters are not included. + * + * @param string $val + */ + public function setRawMimeDirValue($val) + { + $this->setValue(MimeDir::unescapeValue($val, $this->delimiter)); + } + + /** + * Sets the value as a quoted-printable encoded string. + * + * @param string $val + */ + public function setQuotedPrintableValue($val) + { + $val = quoted_printable_decode($val); + + // Quoted printable only appears in vCard 2.1, and the only character + // that may be escaped there is ;. So we are simply splitting on just + // that. + // + // We also don't have to unescape \\, so all we need to look for is a ; + // that's not preceded with a \. + $regex = '# (?setValue($matches); + } + + /** + * Returns a raw mime-dir representation of the value. + * + * @return string + */ + public function getRawMimeDirValue() + { + $val = $this->getParts(); + + if (isset($this->minimumPropertyValues[$this->name])) { + $val = array_pad($val, $this->minimumPropertyValues[$this->name], ''); + } + + foreach ($val as &$item) { + if (!is_array($item)) { + $item = [$item]; + } + + foreach ($item as &$subItem) { + if (!is_null($subItem)) { + $subItem = strtr( + $subItem, + [ + '\\' => '\\\\', + ';' => '\;', + ',' => '\,', + "\n" => '\n', + "\r" => '', + ] + ); + } + } + $item = implode(',', $item); + } + + return implode($this->delimiter, $val); + } + + /** + * Returns the value, in the format it should be encoded for json. + * + * This method must always return an array. + * + * @return array + */ + public function getJsonValue() + { + // Structured text values should always be returned as a single + // array-item. Multi-value text should be returned as multiple items in + // the top-array. + if (in_array($this->name, $this->structuredValues)) { + return [$this->getParts()]; + } + + return $this->getParts(); + } + + /** + * Returns the type of value. + * + * This corresponds to the VALUE= parameter. Every property also has a + * 'default' valueType. + * + * @return string + */ + public function getValueType() + { + return 'TEXT'; + } + + /** + * Turns the object back into a serialized blob. + * + * @return string + */ + public function serialize() + { + // We need to kick in a special type of encoding, if it's a 2.1 vcard. + if (Document::VCARD21 !== $this->root->getDocumentType()) { + return parent::serialize(); + } + + $val = $this->getParts(); + + if (isset($this->minimumPropertyValues[$this->name])) { + $val = \array_pad($val, $this->minimumPropertyValues[$this->name], ''); + } + + // Imploding multiple parts into a single value, and splitting the + // values with ;. + if (\count($val) > 1) { + foreach ($val as $k => $v) { + $val[$k] = \str_replace(';', '\;', $v); + } + $val = \implode(';', $val); + } else { + $val = $val[0]; + } + + $str = $this->name; + if ($this->group) { + $str = $this->group.'.'.$this->name; + } + foreach ($this->parameters as $param) { + if ('QUOTED-PRINTABLE' === $param->getValue()) { + continue; + } + $str .= ';'.$param->serialize(); + } + + // If the resulting value contains a \n, we must encode it as + // quoted-printable. + if (false !== \strpos($val, "\n")) { + $str .= ';ENCODING=QUOTED-PRINTABLE:'; + $lastLine = $str; + $out = null; + + // The PHP built-in quoted-printable-encode does not correctly + // encode newlines for us. Specifically, the \r\n sequence must in + // vcards be encoded as =0D=OA and we must insert soft-newlines + // every 75 bytes. + for ($ii = 0; $ii < \strlen($val); ++$ii) { + $ord = \ord($val[$ii]); + // These characters are encoded as themselves. + if ($ord >= 32 && $ord <= 126) { + $lastLine .= $val[$ii]; + } else { + $lastLine .= '='.\strtoupper(\bin2hex($val[$ii])); + } + if (\strlen($lastLine) >= 75) { + // Soft line break + $out .= $lastLine."=\r\n "; + $lastLine = null; + } + } + if (!\is_null($lastLine)) { + $out .= $lastLine."\r\n"; + } + + return $out; + } else { + $str .= ':'.$val; + + $str = \preg_replace( + '/( + (?:^.)? # 1 additional byte in first line because of missing single space (see next line) + .{1,74} # max 75 bytes per line (1 byte is used for a single space added after every CRLF) + (?![\x80-\xbf]) # prevent splitting multibyte characters + )/x', + "$1\r\n ", + $str + ); + + // remove single space after last CRLF + return \substr($str, 0, -1); + } + } + + /** + * This method serializes only the value of a property. This is used to + * create xCard or xCal documents. + * + * @param Xml\Writer $writer XML writer + */ + protected function xmlSerializeValue(Xml\Writer $writer) + { + $values = $this->getParts(); + + $map = function ($items) use ($values, $writer) { + foreach ($items as $i => $item) { + $writer->writeElement( + $item, + !empty($values[$i]) ? $values[$i] : null + ); + } + }; + + switch ($this->name) { + // Special-casing the REQUEST-STATUS property. + // + // See: + // http://tools.ietf.org/html/rfc6321#section-3.4.1.3 + case 'REQUEST-STATUS': + $writer->writeElement('code', $values[0]); + $writer->writeElement('description', $values[1]); + + if (isset($values[2])) { + $writer->writeElement('data', $values[2]); + } + break; + + case 'N': + $map([ + 'surname', + 'given', + 'additional', + 'prefix', + 'suffix', + ]); + break; + + case 'GENDER': + $map([ + 'sex', + 'text', + ]); + break; + + case 'ADR': + $map([ + 'pobox', + 'ext', + 'street', + 'locality', + 'region', + 'code', + 'country', + ]); + break; + + case 'CLIENTPIDMAP': + $map([ + 'sourceid', + 'uri', + ]); + break; + + default: + parent::xmlSerializeValue($writer); + } + } + + /** + * Validates the node for correctness. + * + * The following options are supported: + * - Node::REPAIR - If something is broken, and automatic repair may + * be attempted. + * + * An array is returned with warnings. + * + * Every item in the array has the following properties: + * * level - (number between 1 and 3 with severity information) + * * message - (human readable message) + * * node - (reference to the offending node) + * + * @param int $options + * + * @return array + */ + public function validate($options = 0) + { + $warnings = parent::validate($options); + + if (isset($this->minimumPropertyValues[$this->name])) { + $minimum = $this->minimumPropertyValues[$this->name]; + $parts = $this->getParts(); + if (count($parts) < $minimum) { + $warnings[] = [ + 'level' => $options & self::REPAIR ? 1 : 3, + 'message' => 'The '.$this->name.' property must have at least '.$minimum.' values. It only has '.count($parts), + 'node' => $this, + ]; + if ($options & self::REPAIR) { + $parts = array_pad($parts, $minimum, ''); + $this->setParts($parts); + } + } + } + + return $warnings; + } +} diff --git a/3rdparty/sabre/vobject/lib/Property/Time.php b/3rdparty/sabre/vobject/lib/Property/Time.php new file mode 100644 index 00000000..1b81609a --- /dev/null +++ b/3rdparty/sabre/vobject/lib/Property/Time.php @@ -0,0 +1,131 @@ +setValue(reset($value)); + } else { + $this->setValue($value); + } + } + + /** + * Returns the value, in the format it should be encoded for json. + * + * This method must always return an array. + * + * @return array + */ + public function getJsonValue() + { + $parts = DateTimeParser::parseVCardTime($this->getValue()); + $timeStr = ''; + + // Hour + if (!is_null($parts['hour'])) { + $timeStr .= $parts['hour']; + + if (!is_null($parts['minute'])) { + $timeStr .= ':'; + } + } else { + // We know either minute or second _must_ be set, so we insert a + // dash for an empty value. + $timeStr .= '-'; + } + + // Minute + if (!is_null($parts['minute'])) { + $timeStr .= $parts['minute']; + + if (!is_null($parts['second'])) { + $timeStr .= ':'; + } + } else { + if (isset($parts['second'])) { + // Dash for empty minute + $timeStr .= '-'; + } + } + + // Second + if (!is_null($parts['second'])) { + $timeStr .= $parts['second']; + } + + // Timezone + if (!is_null($parts['timezone'])) { + if ('Z' === $parts['timezone']) { + $timeStr .= 'Z'; + } else { + $timeStr .= + preg_replace('/([0-9]{2})([0-9]{2})$/', '$1:$2', $parts['timezone']); + } + } + + return [$timeStr]; + } + + /** + * Hydrate data from a XML subtree, as it would appear in a xCard or xCal + * object. + */ + public function setXmlValue(array $value) + { + $value = array_map( + function ($value) { + return str_replace(':', '', $value); + }, + $value + ); + parent::setXmlValue($value); + } +} diff --git a/3rdparty/sabre/vobject/lib/Property/Unknown.php b/3rdparty/sabre/vobject/lib/Property/Unknown.php new file mode 100644 index 00000000..6f404c28 --- /dev/null +++ b/3rdparty/sabre/vobject/lib/Property/Unknown.php @@ -0,0 +1,41 @@ +getRawMimeDirValue()]; + } + + /** + * Returns the type of value. + * + * This corresponds to the VALUE= parameter. Every property also has a + * 'default' valueType. + * + * @return string + */ + public function getValueType() + { + return 'UNKNOWN'; + } +} diff --git a/3rdparty/sabre/vobject/lib/Property/Uri.php b/3rdparty/sabre/vobject/lib/Property/Uri.php new file mode 100644 index 00000000..1ad1fb19 --- /dev/null +++ b/3rdparty/sabre/vobject/lib/Property/Uri.php @@ -0,0 +1,116 @@ +name, ['URL', 'PHOTO'])) { + // If we are encoding a URI value, and this URI value has no + // VALUE=URI parameter, we add it anyway. + // + // This is not required by any spec, but both Apple iCal and Apple + // AddressBook (at least in version 10.8) will trip over this if + // this is not set, and so it improves compatibility. + // + // See Issue #227 and #235 + $parameters['VALUE'] = new Parameter($this->root, 'VALUE', 'URI'); + } + + return $parameters; + } + + /** + * Sets a raw value coming from a mimedir (iCalendar/vCard) file. + * + * This has been 'unfolded', so only 1 line will be passed. Unescaping is + * not yet done, but parameters are not included. + * + * @param string $val + */ + public function setRawMimeDirValue($val) + { + // Normally we don't need to do any type of unescaping for these + // properties, however.. we've noticed that Google Contacts + // specifically escapes the colon (:) with a backslash. While I have + // no clue why they thought that was a good idea, I'm unescaping it + // anyway. + // + // Good thing backslashes are not allowed in urls. Makes it easy to + // assume that a backslash is always intended as an escape character. + if ('URL' === $this->name) { + $regex = '# (?: (\\\\ (?: \\\\ | : ) ) ) #x'; + $matches = preg_split($regex, $val, -1, PREG_SPLIT_DELIM_CAPTURE | PREG_SPLIT_NO_EMPTY); + $newVal = ''; + foreach ($matches as $match) { + switch ($match) { + case '\:': + $newVal .= ':'; + break; + default: + $newVal .= $match; + break; + } + } + $this->value = $newVal; + } else { + $this->value = strtr($val, ['\,' => ',']); + } + } + + /** + * Returns a raw mime-dir representation of the value. + * + * @return string + */ + public function getRawMimeDirValue() + { + if (is_array($this->value)) { + $value = $this->value[0]; + } else { + $value = $this->value; + } + + return strtr($value, [',' => '\,']); + } +} diff --git a/3rdparty/sabre/vobject/lib/Property/UtcOffset.php b/3rdparty/sabre/vobject/lib/Property/UtcOffset.php new file mode 100644 index 00000000..04b88447 --- /dev/null +++ b/3rdparty/sabre/vobject/lib/Property/UtcOffset.php @@ -0,0 +1,70 @@ +value = $dt->format('Ymd'); + } +} diff --git a/3rdparty/sabre/vobject/lib/Property/VCard/DateAndOrTime.php b/3rdparty/sabre/vobject/lib/Property/VCard/DateAndOrTime.php new file mode 100644 index 00000000..7bf79c48 --- /dev/null +++ b/3rdparty/sabre/vobject/lib/Property/VCard/DateAndOrTime.php @@ -0,0 +1,367 @@ + 1) { + throw new \InvalidArgumentException('Only one value allowed'); + } + if (isset($parts[0]) && $parts[0] instanceof DateTimeInterface) { + $this->setDateTime($parts[0]); + } else { + parent::setParts($parts); + } + } + + /** + * Updates the current value. + * + * This may be either a single, or multiple strings in an array. + * + * Instead of strings, you may also use DateTimeInterface here. + * + * @param string|array|DateTimeInterface $value + */ + public function setValue($value) + { + if ($value instanceof DateTimeInterface) { + $this->setDateTime($value); + } else { + parent::setValue($value); + } + } + + /** + * Sets the property as a DateTime object. + */ + public function setDateTime(DateTimeInterface $dt) + { + $tz = $dt->getTimeZone(); + $isUtc = in_array($tz->getName(), ['UTC', 'GMT', 'Z']); + + if ($isUtc) { + $value = $dt->format('Ymd\\THis\\Z'); + } else { + // Calculating the offset. + $value = $dt->format('Ymd\\THisO'); + } + + $this->value = $value; + } + + /** + * Returns a date-time value. + * + * Note that if this property contained more than 1 date-time, only the + * first will be returned. To get an array with multiple values, call + * getDateTimes. + * + * If no time was specified, we will always use midnight (in the default + * timezone) as the time. + * + * If parts of the date were omitted, such as the year, we will grab the + * current values for those. So at the time of writing, if the year was + * omitted, we would have filled in 2014. + * + * @return DateTimeImmutable + */ + public function getDateTime() + { + $now = new DateTime(); + + $tzFormat = 0 === $now->getTimezone()->getOffset($now) ? '\\Z' : 'O'; + $nowParts = DateTimeParser::parseVCardDateTime($now->format('Ymd\\This'.$tzFormat)); + + $dateParts = DateTimeParser::parseVCardDateTime($this->getValue()); + + // This sets all the missing parts to the current date/time. + // So if the year was missing for a birthday, we're making it 'this + // year'. + foreach ($dateParts as $k => $v) { + if (is_null($v)) { + $dateParts[$k] = $nowParts[$k]; + } + } + + return new DateTimeImmutable("$dateParts[year]-$dateParts[month]-$dateParts[date] $dateParts[hour]:$dateParts[minute]:$dateParts[second] $dateParts[timezone]"); + } + + /** + * Returns the value, in the format it should be encoded for json. + * + * This method must always return an array. + * + * @return array + */ + public function getJsonValue() + { + $parts = DateTimeParser::parseVCardDateTime($this->getValue()); + + $dateStr = ''; + + // Year + if (!is_null($parts['year'])) { + $dateStr .= $parts['year']; + + if (!is_null($parts['month'])) { + // If a year and a month is set, we need to insert a separator + // dash. + $dateStr .= '-'; + } + } else { + if (!is_null($parts['month']) || !is_null($parts['date'])) { + // Inserting two dashes + $dateStr .= '--'; + } + } + + // Month + if (!is_null($parts['month'])) { + $dateStr .= $parts['month']; + + if (isset($parts['date'])) { + // If month and date are set, we need the separator dash. + $dateStr .= '-'; + } + } elseif (isset($parts['date'])) { + // If the month is empty, and a date is set, we need a 'empty + // dash' + $dateStr .= '-'; + } + + // Date + if (!is_null($parts['date'])) { + $dateStr .= $parts['date']; + } + + // Early exit if we don't have a time string. + if (is_null($parts['hour']) && is_null($parts['minute']) && is_null($parts['second'])) { + return [$dateStr]; + } + + $dateStr .= 'T'; + + // Hour + if (!is_null($parts['hour'])) { + $dateStr .= $parts['hour']; + + if (!is_null($parts['minute'])) { + $dateStr .= ':'; + } + } else { + // We know either minute or second _must_ be set, so we insert a + // dash for an empty value. + $dateStr .= '-'; + } + + // Minute + if (!is_null($parts['minute'])) { + $dateStr .= $parts['minute']; + + if (!is_null($parts['second'])) { + $dateStr .= ':'; + } + } elseif (isset($parts['second'])) { + // Dash for empty minute + $dateStr .= '-'; + } + + // Second + if (!is_null($parts['second'])) { + $dateStr .= $parts['second']; + } + + // Timezone + if (!is_null($parts['timezone'])) { + $dateStr .= $parts['timezone']; + } + + return [$dateStr]; + } + + /** + * This method serializes only the value of a property. This is used to + * create xCard or xCal documents. + * + * @param Xml\Writer $writer XML writer + */ + protected function xmlSerializeValue(Xml\Writer $writer) + { + $valueType = strtolower($this->getValueType()); + $parts = DateTimeParser::parseVCardDateAndOrTime($this->getValue()); + $value = ''; + + // $d = defined + $d = function ($part) use ($parts) { + return !is_null($parts[$part]); + }; + + // $r = read + $r = function ($part) use ($parts) { + return $parts[$part]; + }; + + // From the Relax NG Schema. + // + // # 4.3.1 + // value-date = element date { + // xsd:string { pattern = "\d{8}|\d{4}-\d\d|--\d\d(\d\d)?|---\d\d" } + // } + if (($d('year') || $d('month') || $d('date')) + && (!$d('hour') && !$d('minute') && !$d('second') && !$d('timezone'))) { + if ($d('year') && $d('month') && $d('date')) { + $value .= $r('year').$r('month').$r('date'); + } elseif ($d('year') && $d('month') && !$d('date')) { + $value .= $r('year').'-'.$r('month'); + } elseif (!$d('year') && $d('month')) { + $value .= '--'.$r('month').$r('date'); + } elseif (!$d('year') && !$d('month') && $d('date')) { + $value .= '---'.$r('date'); + } + + // # 4.3.2 + // value-time = element time { + // xsd:string { pattern = "(\d\d(\d\d(\d\d)?)?|-\d\d(\d\d?)|--\d\d)" + // ~ "(Z|[+\-]\d\d(\d\d)?)?" } + // } + } elseif ((!$d('year') && !$d('month') && !$d('date')) + && ($d('hour') || $d('minute') || $d('second'))) { + if ($d('hour')) { + $value .= $r('hour').$r('minute').$r('second'); + } elseif ($d('minute')) { + $value .= '-'.$r('minute').$r('second'); + } elseif ($d('second')) { + $value .= '--'.$r('second'); + } + + $value .= $r('timezone'); + + // # 4.3.3 + // value-date-time = element date-time { + // xsd:string { pattern = "(\d{8}|--\d{4}|---\d\d)T\d\d(\d\d(\d\d)?)?" + // ~ "(Z|[+\-]\d\d(\d\d)?)?" } + // } + } elseif ($d('date') && $d('hour')) { + if ($d('year') && $d('month') && $d('date')) { + $value .= $r('year').$r('month').$r('date'); + } elseif (!$d('year') && $d('month') && $d('date')) { + $value .= '--'.$r('month').$r('date'); + } elseif (!$d('year') && !$d('month') && $d('date')) { + $value .= '---'.$r('date'); + } + + $value .= 'T'.$r('hour').$r('minute').$r('second'). + $r('timezone'); + } + + $writer->writeElement($valueType, $value); + } + + /** + * Sets a raw value coming from a mimedir (iCalendar/vCard) file. + * + * This has been 'unfolded', so only 1 line will be passed. Unescaping is + * not yet done, but parameters are not included. + * + * @param string $val + */ + public function setRawMimeDirValue($val) + { + $this->setValue($val); + } + + /** + * Returns a raw mime-dir representation of the value. + * + * @return string + */ + public function getRawMimeDirValue() + { + return implode($this->delimiter, $this->getParts()); + } + + /** + * Validates the node for correctness. + * + * The following options are supported: + * Node::REPAIR - May attempt to automatically repair the problem. + * + * This method returns an array with detected problems. + * Every element has the following properties: + * + * * level - problem level. + * * message - A human-readable string describing the issue. + * * node - A reference to the problematic node. + * + * The level means: + * 1 - The issue was repaired (only happens if REPAIR was turned on) + * 2 - An inconsequential issue + * 3 - A severe issue. + * + * @param int $options + * + * @return array + */ + public function validate($options = 0) + { + $messages = parent::validate($options); + $value = $this->getValue(); + + try { + DateTimeParser::parseVCardDateTime($value); + } catch (InvalidDataException $e) { + $messages[] = [ + 'level' => 3, + 'message' => 'The supplied value ('.$value.') is not a correct DATE-AND-OR-TIME property', + 'node' => $this, + ]; + } + + return $messages; + } +} diff --git a/3rdparty/sabre/vobject/lib/Property/VCard/DateTime.php b/3rdparty/sabre/vobject/lib/Property/VCard/DateTime.php new file mode 100644 index 00000000..49c1f355 --- /dev/null +++ b/3rdparty/sabre/vobject/lib/Property/VCard/DateTime.php @@ -0,0 +1,28 @@ +setValue($val); + } + + /** + * Returns a raw mime-dir representation of the value. + * + * @return string + */ + public function getRawMimeDirValue() + { + return $this->getValue(); + } + + /** + * Returns the type of value. + * + * This corresponds to the VALUE= parameter. Every property also has a + * 'default' valueType. + * + * @return string + */ + public function getValueType() + { + return 'LANGUAGE-TAG'; + } +} diff --git a/3rdparty/sabre/vobject/lib/Property/VCard/PhoneNumber.php b/3rdparty/sabre/vobject/lib/Property/VCard/PhoneNumber.php new file mode 100644 index 00000000..b714ffd0 --- /dev/null +++ b/3rdparty/sabre/vobject/lib/Property/VCard/PhoneNumber.php @@ -0,0 +1,30 @@ + + */ +class PhoneNumber extends Property\Text +{ + protected $structuredValues = []; + + /** + * Returns the type of value. + * + * This corresponds to the VALUE= parameter. Every property also has a + * 'default' valueType. + * + * @return string + */ + public function getValueType() + { + return 'PHONE-NUMBER'; + } +} diff --git a/3rdparty/sabre/vobject/lib/Property/VCard/TimeStamp.php b/3rdparty/sabre/vobject/lib/Property/VCard/TimeStamp.php new file mode 100644 index 00000000..da6ea3d4 --- /dev/null +++ b/3rdparty/sabre/vobject/lib/Property/VCard/TimeStamp.php @@ -0,0 +1,81 @@ +getValue()); + + $dateStr = + $parts['year'].'-'. + $parts['month'].'-'. + $parts['date'].'T'. + $parts['hour'].':'. + $parts['minute'].':'. + $parts['second']; + + // Timezone + if (!is_null($parts['timezone'])) { + $dateStr .= $parts['timezone']; + } + + return [$dateStr]; + } + + /** + * This method serializes only the value of a property. This is used to + * create xCard or xCal documents. + * + * @param Xml\Writer $writer XML writer + */ + protected function xmlSerializeValue(Xml\Writer $writer) + { + // xCard is the only XML and JSON format that has the same date and time + // format than vCard. + $valueType = strtolower($this->getValueType()); + $writer->writeElement($valueType, $this->getValue()); + } +} diff --git a/3rdparty/sabre/vobject/lib/Reader.php b/3rdparty/sabre/vobject/lib/Reader.php new file mode 100644 index 00000000..055d546a --- /dev/null +++ b/3rdparty/sabre/vobject/lib/Reader.php @@ -0,0 +1,95 @@ +setCharset($charset); + $result = $parser->parse($data, $options); + + return $result; + } + + /** + * Parses a jCard or jCal object, and returns the top component. + * + * The options argument is a bitfield. Pass any of the OPTIONS constant to + * alter the parsers' behaviour. + * + * You can either a string, a readable stream, or an array for its input. + * Specifying the array is useful if json_decode was already called on the + * input. + * + * @param string|resource|array $data + * @param int $options + * + * @return Document + */ + public static function readJson($data, $options = 0) + { + $parser = new Parser\Json(); + $result = $parser->parse($data, $options); + + return $result; + } + + /** + * Parses a xCard or xCal object, and returns the top component. + * + * The options argument is a bitfield. Pass any of the OPTIONS constant to + * alter the parsers' behaviour. + * + * You can either supply a string, or a readable stream for input. + * + * @param string|resource $data + * @param int $options + * + * @return Document + */ + public static function readXML($data, $options = 0) + { + $parser = new Parser\XML(); + $result = $parser->parse($data, $options); + + return $result; + } +} diff --git a/3rdparty/sabre/vobject/lib/Recur/EventIterator.php b/3rdparty/sabre/vobject/lib/Recur/EventIterator.php new file mode 100644 index 00000000..55d6e477 --- /dev/null +++ b/3rdparty/sabre/vobject/lib/Recur/EventIterator.php @@ -0,0 +1,497 @@ +timeZone = $timeZone; + + if (is_array($input)) { + $events = $input; + } elseif ($input instanceof VEvent) { + // Single instance mode. + $events = [$input]; + } else { + // Calendar + UID mode. + $uid = (string) $uid; + if (!$uid) { + throw new InvalidArgumentException('The UID argument is required when a VCALENDAR is passed to this constructor'); + } + if (!isset($input->VEVENT)) { + throw new InvalidArgumentException('No events found in this calendar'); + } + $events = $input->getByUID($uid); + } + + foreach ($events as $vevent) { + if (!isset($vevent->{'RECURRENCE-ID'})) { + $this->masterEvent = $vevent; + } else { + $this->exceptions[ + $vevent->{'RECURRENCE-ID'}->getDateTime($this->timeZone)->getTimeStamp() + ] = true; + $this->overriddenEvents[] = $vevent; + } + } + + if (!$this->masterEvent) { + // No base event was found. CalDAV does allow cases where only + // overridden instances are stored. + // + // In this particular case, we're just going to grab the first + // event and use that instead. This may not always give the + // desired result. + if (!count($this->overriddenEvents)) { + throw new InvalidArgumentException('This VCALENDAR did not have an event with UID: '.$uid); + } + $this->masterEvent = array_shift($this->overriddenEvents); + } + + $this->startDate = $this->masterEvent->DTSTART->getDateTime($this->timeZone); + $this->allDay = !$this->masterEvent->DTSTART->hasTime(); + + if (isset($this->masterEvent->EXDATE)) { + foreach ($this->masterEvent->EXDATE as $exDate) { + foreach ($exDate->getDateTimes($this->timeZone) as $dt) { + $this->exceptions[$dt->getTimeStamp()] = true; + } + } + } + + if (isset($this->masterEvent->DTEND)) { + $this->eventDuration = + $this->masterEvent->DTEND->getDateTime($this->timeZone)->getTimeStamp() - + $this->startDate->getTimeStamp(); + } elseif (isset($this->masterEvent->DURATION)) { + $duration = $this->masterEvent->DURATION->getDateInterval(); + $end = clone $this->startDate; + $end = $end->add($duration); + $this->eventDuration = $end->getTimeStamp() - $this->startDate->getTimeStamp(); + } elseif ($this->allDay) { + $this->eventDuration = 3600 * 24; + } else { + $this->eventDuration = 0; + } + + if (isset($this->masterEvent->RDATE)) { + $this->recurIterator = new RDateIterator( + $this->masterEvent->RDATE->getParts(), + $this->startDate + ); + } elseif (isset($this->masterEvent->RRULE)) { + $this->recurIterator = new RRuleIterator( + $this->masterEvent->RRULE->getParts(), + $this->startDate + ); + } else { + $this->recurIterator = new RRuleIterator( + [ + 'FREQ' => 'DAILY', + 'COUNT' => 1, + ], + $this->startDate + ); + } + + $this->rewind(); + if (!$this->valid()) { + throw new NoInstancesException('This recurrence rule does not generate any valid instances'); + } + } + + /** + * Returns the date for the current position of the iterator. + * + * @return DateTimeImmutable + */ + #[\ReturnTypeWillChange] + public function current() + { + if ($this->currentDate) { + return clone $this->currentDate; + } + } + + /** + * This method returns the start date for the current iteration of the + * event. + * + * @return DateTimeImmutable + */ + public function getDtStart() + { + if ($this->currentDate) { + return clone $this->currentDate; + } + } + + /** + * This method returns the end date for the current iteration of the + * event. + * + * @return DateTimeImmutable + */ + public function getDtEnd() + { + if (!$this->valid()) { + return; + } + if ($this->currentOverriddenEvent && $this->currentOverriddenEvent->DTEND) { + return $this->currentOverriddenEvent->DTEND->getDateTime($this->timeZone); + } else { + $end = clone $this->currentDate; + + return $end->modify('+'.$this->eventDuration.' seconds'); + } + } + + /** + * Returns a VEVENT for the current iterations of the event. + * + * This VEVENT will have a recurrence id, and its DTSTART and DTEND + * altered. + * + * @return VEvent + */ + public function getEventObject() + { + if ($this->currentOverriddenEvent) { + return $this->currentOverriddenEvent; + } + + $event = clone $this->masterEvent; + + // Ignoring the following block, because PHPUnit's code coverage + // ignores most of these lines, and this messes with our stats. + // + // @codeCoverageIgnoreStart + unset( + $event->RRULE, + $event->EXDATE, + $event->RDATE, + $event->EXRULE, + $event->{'RECURRENCE-ID'} + ); + // @codeCoverageIgnoreEnd + + $event->DTSTART->setDateTime($this->getDtStart(), $event->DTSTART->isFloating()); + if (isset($event->DTEND)) { + $event->DTEND->setDateTime($this->getDtEnd(), $event->DTEND->isFloating()); + } + $recurid = clone $event->DTSTART; + $recurid->name = 'RECURRENCE-ID'; + $event->add($recurid); + + return $event; + } + + /** + * Returns the current position of the iterator. + * + * This is for us simply a 0-based index. + * + * @return int + */ + #[\ReturnTypeWillChange] + public function key() + { + // The counter is always 1 ahead. + return $this->counter - 1; + } + + /** + * This is called after next, to see if the iterator is still at a valid + * position, or if it's at the end. + * + * @return bool + */ + #[\ReturnTypeWillChange] + public function valid() + { + if ($this->counter > Settings::$maxRecurrences && -1 !== Settings::$maxRecurrences) { + throw new MaxInstancesExceededException('Recurring events are only allowed to generate '.Settings::$maxRecurrences); + } + + return (bool) $this->currentDate; + } + + /** + * Sets the iterator back to the starting point. + * + * @return void + */ + #[\ReturnTypeWillChange] + public function rewind() + { + $this->recurIterator->rewind(); + // re-creating overridden event index. + $index = []; + foreach ($this->overriddenEvents as $key => $event) { + $stamp = $event->DTSTART->getDateTime($this->timeZone)->getTimeStamp(); + $index[$stamp][] = $key; + } + krsort($index); + $this->counter = 0; + $this->overriddenEventsIndex = $index; + $this->currentOverriddenEvent = null; + + $this->nextDate = null; + $this->currentDate = clone $this->startDate; + + $this->next(); + } + + /** + * Advances the iterator with one step. + * + * @return void + */ + #[\ReturnTypeWillChange] + public function next() + { + $this->currentOverriddenEvent = null; + ++$this->counter; + if ($this->nextDate) { + // We had a stored value. + $nextDate = $this->nextDate; + $this->nextDate = null; + } else { + // We need to ask rruleparser for the next date. + // We need to do this until we find a date that's not in the + // exception list. + do { + if (!$this->recurIterator->valid()) { + $nextDate = null; + break; + } + $nextDate = $this->recurIterator->current(); + $this->recurIterator->next(); + } while (isset($this->exceptions[$nextDate->getTimeStamp()])); + } + + // $nextDate now contains what rrule thinks is the next one, but an + // overridden event may cut ahead. + if ($this->overriddenEventsIndex) { + $offsets = end($this->overriddenEventsIndex); + $timestamp = key($this->overriddenEventsIndex); + $offset = end($offsets); + if (!$nextDate || $timestamp < $nextDate->getTimeStamp()) { + // Overridden event comes first. + $this->currentOverriddenEvent = $this->overriddenEvents[$offset]; + + // Putting the rrule next date aside. + $this->nextDate = $nextDate; + $this->currentDate = $this->currentOverriddenEvent->DTSTART->getDateTime($this->timeZone); + + // Ensuring that this item will only be used once. + array_pop($this->overriddenEventsIndex[$timestamp]); + if (!$this->overriddenEventsIndex[$timestamp]) { + array_pop($this->overriddenEventsIndex); + } + + // Exit point! + return; + } + } + + $this->currentDate = $nextDate; + } + + /** + * Quickly jump to a date in the future. + */ + public function fastForward(DateTimeInterface $dateTime) + { + while ($this->valid() && $this->getDtEnd() <= $dateTime) { + $this->next(); + } + } + + /** + * Returns true if this recurring event never ends. + * + * @return bool + */ + public function isInfinite() + { + return $this->recurIterator->isInfinite(); + } + + /** + * RRULE parser. + * + * @var RRuleIterator + */ + protected $recurIterator; + + /** + * The duration, in seconds, of the master event. + * + * We use this to calculate the DTEND for subsequent events. + */ + protected $eventDuration; + + /** + * A reference to the main (master) event. + * + * @var VEVENT + */ + protected $masterEvent; + + /** + * List of overridden events. + * + * @var array + */ + protected $overriddenEvents = []; + + /** + * Overridden event index. + * + * Key is timestamp, value is the list of indexes of the item in the $overriddenEvent + * property. + * + * @var array + */ + protected $overriddenEventsIndex; + + /** + * A list of recurrence-id's that are either part of EXDATE, or are + * overridden. + * + * @var array + */ + protected $exceptions = []; + + /** + * Internal event counter. + * + * @var int + */ + protected $counter; + + /** + * The very start of the iteration process. + * + * @var DateTimeImmutable + */ + protected $startDate; + + /** + * Where we are currently in the iteration process. + * + * @var DateTimeImmutable + */ + protected $currentDate; + + /** + * The next date from the rrule parser. + * + * Sometimes we need to temporary store the next date, because an + * overridden event came before. + * + * @var DateTimeImmutable + */ + protected $nextDate; + + /** + * The event that overwrites the current iteration. + * + * @var VEVENT + */ + protected $currentOverriddenEvent; +} diff --git a/3rdparty/sabre/vobject/lib/Recur/MaxInstancesExceededException.php b/3rdparty/sabre/vobject/lib/Recur/MaxInstancesExceededException.php new file mode 100644 index 00000000..cb083581 --- /dev/null +++ b/3rdparty/sabre/vobject/lib/Recur/MaxInstancesExceededException.php @@ -0,0 +1,17 @@ +startDate = $start; + $this->parseRDate($rrule); + $this->currentDate = clone $this->startDate; + } + + /* Implementation of the Iterator interface {{{ */ + + #[\ReturnTypeWillChange] + public function current() + { + if (!$this->valid()) { + return; + } + + return clone $this->currentDate; + } + + /** + * Returns the current item number. + * + * @return int + */ + #[\ReturnTypeWillChange] + public function key() + { + return $this->counter; + } + + /** + * Returns whether the current item is a valid item for the recurrence + * iterator. + * + * @return bool + */ + #[\ReturnTypeWillChange] + public function valid() + { + return $this->counter <= count($this->dates); + } + + /** + * Resets the iterator. + * + * @return void + */ + #[\ReturnTypeWillChange] + public function rewind() + { + $this->currentDate = clone $this->startDate; + $this->counter = 0; + } + + /** + * Goes on to the next iteration. + * + * @return void + */ + #[\ReturnTypeWillChange] + public function next() + { + ++$this->counter; + if (!$this->valid()) { + return; + } + + $this->currentDate = + DateTimeParser::parse( + $this->dates[$this->counter - 1], + $this->startDate->getTimezone() + ); + } + + /* End of Iterator implementation }}} */ + + /** + * Returns true if this recurring event never ends. + * + * @return bool + */ + public function isInfinite() + { + return false; + } + + /** + * This method allows you to quickly go to the next occurrence after the + * specified date. + */ + public function fastForward(DateTimeInterface $dt) + { + while ($this->valid() && $this->currentDate < $dt) { + $this->next(); + } + } + + /** + * The reference start date/time for the rrule. + * + * All calculations are based on this initial date. + * + * @var DateTimeInterface + */ + protected $startDate; + + /** + * The date of the current iteration. You can get this by calling + * ->current(). + * + * @var DateTimeInterface + */ + protected $currentDate; + + /** + * The current item in the list. + * + * You can get this number with the key() method. + * + * @var int + */ + protected $counter = 0; + + /* }}} */ + + /** + * This method receives a string from an RRULE property, and populates this + * class with all the values. + * + * @param string|array $rrule + */ + protected function parseRDate($rdate) + { + if (is_string($rdate)) { + $rdate = explode(',', $rdate); + } + + $this->dates = $rdate; + } + + /** + * Array with the RRULE dates. + * + * @var array + */ + protected $dates = []; +} diff --git a/3rdparty/sabre/vobject/lib/Recur/RRuleIterator.php b/3rdparty/sabre/vobject/lib/Recur/RRuleIterator.php new file mode 100644 index 00000000..ca53b63e --- /dev/null +++ b/3rdparty/sabre/vobject/lib/Recur/RRuleIterator.php @@ -0,0 +1,1079 @@ +startDate = $start; + $this->parseRRule($rrule); + $this->currentDate = clone $this->startDate; + } + + /* Implementation of the Iterator interface {{{ */ + + #[\ReturnTypeWillChange] + public function current() + { + if (!$this->valid()) { + return; + } + + return clone $this->currentDate; + } + + /** + * Returns the current item number. + * + * @return int + */ + #[\ReturnTypeWillChange] + public function key() + { + return $this->counter; + } + + /** + * Returns whether the current item is a valid item for the recurrence + * iterator. This will return false if we've gone beyond the UNTIL or COUNT + * statements. + * + * @return bool + */ + #[\ReturnTypeWillChange] + public function valid() + { + if (null === $this->currentDate) { + return false; + } + if (!is_null($this->count)) { + return $this->counter < $this->count; + } + + return is_null($this->until) || $this->currentDate <= $this->until; + } + + /** + * Resets the iterator. + * + * @return void + */ + #[\ReturnTypeWillChange] + public function rewind() + { + $this->currentDate = clone $this->startDate; + $this->counter = 0; + } + + /** + * Goes on to the next iteration. + * + * @return void + */ + #[\ReturnTypeWillChange] + public function next() + { + // Otherwise, we find the next event in the normal RRULE + // sequence. + switch ($this->frequency) { + case 'hourly': + $this->nextHourly(); + break; + + case 'daily': + $this->nextDaily(); + break; + + case 'weekly': + $this->nextWeekly(); + break; + + case 'monthly': + $this->nextMonthly(); + break; + + case 'yearly': + $this->nextYearly(); + break; + } + ++$this->counter; + } + + /* End of Iterator implementation }}} */ + + /** + * Returns true if this recurring event never ends. + * + * @return bool + */ + public function isInfinite() + { + return !$this->count && !$this->until; + } + + /** + * This method allows you to quickly go to the next occurrence after the + * specified date. + */ + public function fastForward(DateTimeInterface $dt) + { + while ($this->valid() && $this->currentDate < $dt) { + $this->next(); + } + } + + /** + * The reference start date/time for the rrule. + * + * All calculations are based on this initial date. + * + * @var DateTimeInterface + */ + protected $startDate; + + /** + * The date of the current iteration. You can get this by calling + * ->current(). + * + * @var DateTimeInterface + */ + protected $currentDate; + + /** + * The number of hours that the next occurrence of an event + * jumped forward, usually because summer time started and + * the requested time-of-day like 0230 did not exist on that + * day. And so the event was scheduled 1 hour later at 0330. + */ + protected $hourJump = 0; + + /** + * Frequency is one of: secondly, minutely, hourly, daily, weekly, monthly, + * yearly. + * + * @var string + */ + protected $frequency; + + /** + * The number of recurrences, or 'null' if infinitely recurring. + * + * @var int + */ + protected $count; + + /** + * The interval. + * + * If for example frequency is set to daily, interval = 2 would mean every + * 2 days. + * + * @var int + */ + protected $interval = 1; + + /** + * The last instance of this recurrence, inclusively. + * + * @var DateTimeInterface|null + */ + protected $until; + + /** + * Which seconds to recur. + * + * This is an array of integers (between 0 and 60) + * + * @var array + */ + protected $bySecond; + + /** + * Which minutes to recur. + * + * This is an array of integers (between 0 and 59) + * + * @var array + */ + protected $byMinute; + + /** + * Which hours to recur. + * + * This is an array of integers (between 0 and 23) + * + * @var array + */ + protected $byHour; + + /** + * The current item in the list. + * + * You can get this number with the key() method. + * + * @var int + */ + protected $counter = 0; + + /** + * Which weekdays to recur. + * + * This is an array of weekdays + * + * This may also be preceded by a positive or negative integer. If present, + * this indicates the nth occurrence of a specific day within the monthly or + * yearly rrule. For instance, -2TU indicates the second-last tuesday of + * the month, or year. + * + * @var array + */ + protected $byDay; + + /** + * Which days of the month to recur. + * + * This is an array of days of the months (1-31). The value can also be + * negative. -5 for instance means the 5th last day of the month. + * + * @var array + */ + protected $byMonthDay; + + /** + * Which days of the year to recur. + * + * This is an array with days of the year (1 to 366). The values can also + * be negative. For instance, -1 will always represent the last day of the + * year. (December 31st). + * + * @var array + */ + protected $byYearDay; + + /** + * Which week numbers to recur. + * + * This is an array of integers from 1 to 53. The values can also be + * negative. -1 will always refer to the last week of the year. + * + * @var array + */ + protected $byWeekNo; + + /** + * Which months to recur. + * + * This is an array of integers from 1 to 12. + * + * @var array + */ + protected $byMonth; + + /** + * Which items in an existing st to recur. + * + * These numbers work together with an existing by* rule. It specifies + * exactly which items of the existing by-rule to filter. + * + * Valid values are 1 to 366 and -1 to -366. As an example, this can be + * used to recur the last workday of the month. + * + * This would be done by setting frequency to 'monthly', byDay to + * 'MO,TU,WE,TH,FR' and bySetPos to -1. + * + * @var array + */ + protected $bySetPos; + + /** + * When the week starts. + * + * @var string + */ + protected $weekStart = 'MO'; + + /* Functions that advance the iterator {{{ */ + + /** + * Gets the original start time of the RRULE. + * + * The value is formatted as a string with 24-hour:minute:second + */ + protected function startTime(): string + { + return $this->startDate->format('H:i:s'); + } + + /** + * Advances currentDate by the interval. + * The time is set from the original startDate. + * If the recurrence is on a day when summer time started, then the + * time on that day may have jumped forward, for example, from 0230 to 0330. + * Using the original time means that the next recurrence will be calculated + * based on the original start time and the day/week/month/year interval. + * So the start time of the next occurrence can correctly revert to 0230. + */ + protected function advanceTheDate(string $interval): void + { + $this->currentDate = $this->currentDate->modify($interval.' '.$this->startTime()); + } + + /** + * Does the processing for adjusting the time of multi-hourly events when summer time starts. + */ + protected function adjustForTimeJumpsOfHourlyEvent(DateTimeInterface $previousEventDateTime): void + { + if (0 === $this->hourJump) { + // Remember if the clock time jumped forward on the next occurrence. + // That happens if the next event time is on a day when summer time starts + // and the event time is in the non-existent hour of the day. + // For example, an event that normally starts at 02:30 will + // have to start at 03:30 on that day. + // If the interval is just 1 hour, then there is no "jumping back" to do. + // The events that day will happen, for example, at 0030 0130 0330 0430 0530... + if ($this->interval > 1) { + $expectedHourOfNextDate = ((int) $previousEventDateTime->format('G') + $this->interval) % 24; + $actualHourOfNextDate = (int) $this->currentDate->format('G'); + $this->hourJump = $actualHourOfNextDate - $expectedHourOfNextDate; + } + } else { + // The hour "jumped" for the previous occurrence, to avoid the non-existent time. + // currentDate got set ahead by (usually) 1 hour on that day. + // Adjust it back for this next occurrence. + $this->currentDate = $this->currentDate->sub(new \DateInterval('PT'.$this->hourJump.'H')); + $this->hourJump = 0; + } + } + + /** + * Does the processing for advancing the iterator for hourly frequency. + */ + protected function nextHourly() + { + $previousEventDateTime = clone $this->currentDate; + $this->currentDate = $this->currentDate->modify('+'.$this->interval.' hours'); + $this->adjustForTimeJumpsOfHourlyEvent($previousEventDateTime); + } + + /** + * Does the processing for advancing the iterator for daily frequency. + */ + protected function nextDaily() + { + if (!$this->byHour && !$this->byDay) { + $this->advanceTheDate('+'.$this->interval.' days'); + + return; + } + + $recurrenceHours = []; + if (!empty($this->byHour)) { + $recurrenceHours = $this->getHours(); + } + + $recurrenceDays = []; + if (!empty($this->byDay)) { + $recurrenceDays = $this->getDays(); + } + + $recurrenceMonths = []; + if (!empty($this->byMonth)) { + $recurrenceMonths = $this->getMonths(); + } + + do { + if ($this->byHour) { + if ('23' == $this->currentDate->format('G')) { + // to obey the interval rule + $this->currentDate = $this->currentDate->modify('+'.($this->interval - 1).' days'); + } + + $this->currentDate = $this->currentDate->modify('+1 hours'); + } else { + $this->currentDate = $this->currentDate->modify('+'.$this->interval.' days'); + } + + // Current month of the year + $currentMonth = $this->currentDate->format('n'); + + // Current day of the week + $currentDay = $this->currentDate->format('w'); + + // Current hour of the day + $currentHour = $this->currentDate->format('G'); + + if ($this->currentDate->getTimestamp() > self::dateUpperLimit) { + $this->currentDate = null; + + return; + } + } while ( + ($this->byDay && !in_array($currentDay, $recurrenceDays)) || + ($this->byHour && !in_array($currentHour, $recurrenceHours)) || + ($this->byMonth && !in_array($currentMonth, $recurrenceMonths)) + ); + } + + /** + * Does the processing for advancing the iterator for weekly frequency. + */ + protected function nextWeekly() + { + if (!$this->byHour && !$this->byDay) { + $this->advanceTheDate('+'.$this->interval.' weeks'); + + return; + } + + $recurrenceHours = []; + if ($this->byHour) { + $recurrenceHours = $this->getHours(); + } + + $recurrenceDays = []; + if ($this->byDay) { + $recurrenceDays = $this->getDays(); + } + + // First day of the week: + $firstDay = $this->dayMap[$this->weekStart]; + + do { + if ($this->byHour) { + $this->currentDate = $this->currentDate->modify('+1 hours'); + } else { + $this->advanceTheDate('+1 days'); + } + + // Current day of the week + $currentDay = (int) $this->currentDate->format('w'); + + // Current hour of the day + $currentHour = (int) $this->currentDate->format('G'); + + // We need to roll over to the next week + if ($currentDay === $firstDay && (!$this->byHour || '0' == $currentHour)) { + $this->currentDate = $this->currentDate->modify('+'.($this->interval - 1).' weeks'); + + // We need to go to the first day of this week, but only if we + // are not already on this first day of this week. + if ($this->currentDate->format('w') != $firstDay) { + $this->currentDate = $this->currentDate->modify('last '.$this->dayNames[$this->dayMap[$this->weekStart]]); + } + } + + // We have a match + } while (($this->byDay && !in_array($currentDay, $recurrenceDays)) || ($this->byHour && !in_array($currentHour, $recurrenceHours))); + } + + /** + * Does the processing for advancing the iterator for monthly frequency. + */ + protected function nextMonthly() + { + $currentDayOfMonth = $this->currentDate->format('j'); + if (!$this->byMonthDay && !$this->byDay) { + // If the current day is higher than the 28th, rollover can + // occur to the next month. We Must skip these invalid + // entries. + if ($currentDayOfMonth < 29) { + $this->advanceTheDate('+'.$this->interval.' months'); + } else { + $increase = 0; + do { + ++$increase; + $tempDate = clone $this->currentDate; + $tempDate = $tempDate->modify('+ '.($this->interval * $increase).' months '.$this->startTime()); + } while ($tempDate->format('j') != $currentDayOfMonth); + $this->currentDate = $tempDate; + } + + return; + } + + $occurrence = -1; + while (true) { + $occurrences = $this->getMonthlyOccurrences(); + + foreach ($occurrences as $occurrence) { + // The first occurrence thats higher than the current + // day of the month wins. + if ($occurrence > $currentDayOfMonth) { + break 2; + } + } + + // If we made it all the way here, it means there were no + // valid occurrences, and we need to advance to the next + // month. + // + // This line does not currently work in hhvm. Temporary workaround + // follows: + // $this->currentDate->modify('first day of this month'); + $this->currentDate = new DateTimeImmutable($this->currentDate->format('Y-m-1 H:i:s'), $this->currentDate->getTimezone()); + // end of workaround + $this->currentDate = $this->currentDate->modify('+ '.$this->interval.' months'); + + // This goes to 0 because we need to start counting at the + // beginning. + $currentDayOfMonth = 0; + + // For some reason the "until" parameter was not being used here, + // that's why the workaround of the 10000 year bug was needed at all + // let's stop it before the "until" parameter date + if ($this->until && $this->currentDate->getTimestamp() >= $this->until->getTimestamp()) { + return; + } + + // To prevent running this forever (better: until we hit the max date of DateTimeImmutable) we simply + // stop at 9999-12-31. Looks like the year 10000 problem is not solved in php .... + if ($this->currentDate->getTimestamp() > self::dateUpperLimit) { + $this->currentDate = null; + + return; + } + } + + // Set the currentDate to the year and month that we are in, and the day of the month that we have selected. + // That day could be a day when summer time starts, and if the time of the event is, for example, 0230, + // then 0230 will not be a valid time on that day. So always apply the start time from the original startDate. + // The "modify" method will set the time forward to 0330, for example, if needed. + $this->currentDate = $this->currentDate->setDate( + (int) $this->currentDate->format('Y'), + (int) $this->currentDate->format('n'), + (int) $occurrence + )->modify($this->startTime()); + } + + /** + * Does the processing for advancing the iterator for yearly frequency. + */ + protected function nextYearly() + { + $currentMonth = $this->currentDate->format('n'); + $currentYear = $this->currentDate->format('Y'); + $currentDayOfMonth = $this->currentDate->format('j'); + + // No sub-rules, so we just advance by year + if (empty($this->byMonth)) { + // Unless it was a leap day! + if (2 == $currentMonth && 29 == $currentDayOfMonth) { + $counter = 0; + do { + ++$counter; + // Here we increase the year count by the interval, until + // we hit a date that's also in a leap year. + // + // We could just find the next interval that's dividable by + // 4, but that would ignore the rule that there's no leap + // year every year that's dividable by a 100, but not by + // 400. (1800, 1900, 2100). So we just rely on the datetime + // functions instead. + $nextDate = clone $this->currentDate; + $nextDate = $nextDate->modify('+ '.($this->interval * $counter).' years'); + } while (2 != $nextDate->format('n')); + + $this->currentDate = $nextDate; + + return; + } + + if (null !== $this->byWeekNo) { // byWeekNo is an array with values from -53 to -1, or 1 to 53 + $dayOffsets = []; + if ($this->byDay) { + foreach ($this->byDay as $byDay) { + $dayOffsets[] = $this->dayMap[$byDay]; + } + } else { // default is Monday + $dayOffsets[] = 1; + } + + $currentYear = $this->currentDate->format('Y'); + + while (true) { + $checkDates = []; + + // loop through all WeekNo and Days to check all the combinations + foreach ($this->byWeekNo as $byWeekNo) { + foreach ($dayOffsets as $dayOffset) { + $date = clone $this->currentDate; + $date = $date->setISODate($currentYear, $byWeekNo, $dayOffset); + + if ($date > $this->currentDate) { + $checkDates[] = $date; + } + } + } + + if (count($checkDates) > 0) { + $this->currentDate = min($checkDates); + + return; + } + + // if there is no date found, check the next year + $currentYear += $this->interval; + } + } + + if (null !== $this->byYearDay) { // byYearDay is an array with values from -366 to -1, or 1 to 366 + $dayOffsets = []; + if ($this->byDay) { + foreach ($this->byDay as $byDay) { + $dayOffsets[] = $this->dayMap[$byDay]; + } + } else { // default is Monday-Sunday + $dayOffsets = [1, 2, 3, 4, 5, 6, 7]; + } + + $currentYear = $this->currentDate->format('Y'); + + while (true) { + $checkDates = []; + + // loop through all YearDay and Days to check all the combinations + foreach ($this->byYearDay as $byYearDay) { + $date = clone $this->currentDate; + if ($byYearDay > 0) { + $date = $date->setDate($currentYear, 1, 1); + $date = $date->add(new \DateInterval('P'.($byYearDay - 1).'D')); + } else { + $date = $date->setDate($currentYear, 12, 31); + $date = $date->sub(new \DateInterval('P'.abs($byYearDay + 1).'D')); + } + + if ($date > $this->currentDate && in_array($date->format('N'), $dayOffsets)) { + $checkDates[] = $date; + } + } + + if (count($checkDates) > 0) { + $this->currentDate = min($checkDates); + + return; + } + + // if there is no date found, check the next year + $currentYear += $this->interval; + } + } + + // The easiest form + $this->advanceTheDate('+'.$this->interval.' years'); + + return; + } + + $currentMonth = $this->currentDate->format('n'); + $currentYear = $this->currentDate->format('Y'); + $currentDayOfMonth = $this->currentDate->format('j'); + + $advancedToNewMonth = false; + + // If we got a byDay or getMonthDay filter, we must first expand + // further. + if ($this->byDay || $this->byMonthDay) { + $occurrence = -1; + while (true) { + $occurrences = $this->getMonthlyOccurrences(); + + foreach ($occurrences as $occurrence) { + // The first occurrence that's higher than the current + // day of the month wins. + // If we advanced to the next month or year, the first + // occurrence is always correct. + if ($occurrence > $currentDayOfMonth || $advancedToNewMonth) { + // only consider byMonth matches, + // otherwise, we don't follow RRule correctly + if (in_array($currentMonth, $this->byMonth)) { + break 2; + } + } + } + + // If we made it here, it means we need to advance to + // the next month or year. + $currentDayOfMonth = 1; + $advancedToNewMonth = true; + do { + ++$currentMonth; + if ($currentMonth > 12) { + $currentYear += $this->interval; + $currentMonth = 1; + } + } while (!in_array($currentMonth, $this->byMonth)); + + $this->currentDate = $this->currentDate->setDate( + (int) $currentYear, + (int) $currentMonth, + (int) $currentDayOfMonth + ); + + // To prevent running this forever (better: until we hit the max date of DateTimeImmutable) we simply + // stop at 9999-12-31. Looks like the year 10000 problem is not solved in php .... + if ($this->currentDate->getTimestamp() > self::dateUpperLimit) { + $this->currentDate = null; + + return; + } + } + + // If we made it here, it means we got a valid occurrence + $this->currentDate = $this->currentDate->setDate( + (int) $currentYear, + (int) $currentMonth, + (int) $occurrence + )->modify($this->startTime()); + + return; + } else { + // These are the 'byMonth' rules, if there are no byDay or + // byMonthDay sub-rules. + do { + ++$currentMonth; + if ($currentMonth > 12) { + $currentYear += $this->interval; + $currentMonth = 1; + } + } while (!in_array($currentMonth, $this->byMonth)); + $this->currentDate = $this->currentDate->setDate( + (int) $currentYear, + (int) $currentMonth, + (int) $currentDayOfMonth + )->modify($this->startTime()); + + return; + } + } + + /* }}} */ + + /** + * This method receives a string from an RRULE property, and populates this + * class with all the values. + * + * @param string|array $rrule + */ + protected function parseRRule($rrule) + { + if (is_string($rrule)) { + $rrule = Property\ICalendar\Recur::stringToArray($rrule); + } + + foreach ($rrule as $key => $value) { + $key = strtoupper($key); + switch ($key) { + case 'FREQ': + $value = strtolower($value); + if (!in_array( + $value, + ['secondly', 'minutely', 'hourly', 'daily', 'weekly', 'monthly', 'yearly'] + )) { + throw new InvalidDataException('Unknown value for FREQ='.strtoupper($value)); + } + $this->frequency = $value; + break; + + case 'UNTIL': + $this->until = DateTimeParser::parse($value, $this->startDate->getTimezone()); + + // In some cases events are generated with an UNTIL= + // parameter before the actual start of the event. + // + // Not sure why this is happening. We assume that the + // intention was that the event only recurs once. + // + // So we are modifying the parameter so our code doesn't + // break. + if ($this->until < $this->startDate) { + $this->until = $this->startDate; + } + break; + + case 'INTERVAL': + case 'COUNT': + $val = (int) $value; + if ($val < 1) { + throw new InvalidDataException(strtoupper($key).' in RRULE must be a positive integer!'); + } + $key = strtolower($key); + $this->$key = $val; + break; + + case 'BYSECOND': + $this->bySecond = (array) $value; + break; + + case 'BYMINUTE': + $this->byMinute = (array) $value; + break; + + case 'BYHOUR': + $this->byHour = (array) $value; + break; + + case 'BYDAY': + $value = (array) $value; + foreach ($value as $part) { + if (!preg_match('#^ (-|\+)? ([1-5])? (MO|TU|WE|TH|FR|SA|SU) $# xi', $part)) { + throw new InvalidDataException('Invalid part in BYDAY clause: '.$part); + } + } + $this->byDay = $value; + break; + + case 'BYMONTHDAY': + $this->byMonthDay = (array) $value; + break; + + case 'BYYEARDAY': + $this->byYearDay = (array) $value; + foreach ($this->byYearDay as $byYearDay) { + if (!is_numeric($byYearDay) || (int) $byYearDay < -366 || 0 == (int) $byYearDay || (int) $byYearDay > 366) { + throw new InvalidDataException('BYYEARDAY in RRULE must have value(s) from 1 to 366, or -366 to -1!'); + } + } + break; + + case 'BYWEEKNO': + $this->byWeekNo = (array) $value; + foreach ($this->byWeekNo as $byWeekNo) { + if (!is_numeric($byWeekNo) || (int) $byWeekNo < -53 || 0 == (int) $byWeekNo || (int) $byWeekNo > 53) { + throw new InvalidDataException('BYWEEKNO in RRULE must have value(s) from 1 to 53, or -53 to -1!'); + } + } + break; + + case 'BYMONTH': + $this->byMonth = (array) $value; + foreach ($this->byMonth as $byMonth) { + if (!is_numeric($byMonth) || (int) $byMonth < 1 || (int) $byMonth > 12) { + throw new InvalidDataException('BYMONTH in RRULE must have value(s) between 1 and 12!'); + } + } + break; + + case 'BYSETPOS': + $this->bySetPos = (array) $value; + break; + + case 'WKST': + $this->weekStart = strtoupper($value); + break; + + default: + throw new InvalidDataException('Not supported: '.strtoupper($key)); + } + } + } + + /** + * Mappings between the day number and english day name. + * + * @var array + */ + protected $dayNames = [ + 0 => 'Sunday', + 1 => 'Monday', + 2 => 'Tuesday', + 3 => 'Wednesday', + 4 => 'Thursday', + 5 => 'Friday', + 6 => 'Saturday', + ]; + + /** + * Returns all the occurrences for a monthly frequency with a 'byDay' or + * 'byMonthDay' expansion for the current month. + * + * The returned list is an array of integers with the day of month (1-31). + * + * @return array + */ + protected function getMonthlyOccurrences() + { + $startDate = clone $this->currentDate; + + $byDayResults = []; + + // Our strategy is to simply go through the byDays, advance the date to + // that point and add it to the results. + if ($this->byDay) { + foreach ($this->byDay as $day) { + $dayName = $this->dayNames[$this->dayMap[substr($day, -2)]]; + + // Dayname will be something like 'wednesday'. Now we need to find + // all wednesdays in this month. + $dayHits = []; + + // workaround for missing 'first day of the month' support in hhvm + $checkDate = new \DateTime($startDate->format('Y-m-1')); + // workaround modify always advancing the date even if the current day is a $dayName in hhvm + if ($checkDate->format('l') !== $dayName) { + $checkDate = $checkDate->modify($dayName); + } + + do { + $dayHits[] = $checkDate->format('j'); + $checkDate = $checkDate->modify('next '.$dayName); + } while ($checkDate->format('n') === $startDate->format('n')); + + // So now we have 'all wednesdays' for month. It is however + // possible that the user only really wanted the 1st, 2nd or last + // wednesday. + if (strlen($day) > 2) { + $offset = (int) substr($day, 0, -2); + + if ($offset > 0) { + // It is possible that the day does not exist, such as a + // 5th or 6th wednesday of the month. + if (isset($dayHits[$offset - 1])) { + $byDayResults[] = $dayHits[$offset - 1]; + } + } else { + // if it was negative we count from the end of the array + // might not exist, fx. -5th tuesday + if (isset($dayHits[count($dayHits) + $offset])) { + $byDayResults[] = $dayHits[count($dayHits) + $offset]; + } + } + } else { + // There was no counter (first, second, last wednesdays), so we + // just need to add the all to the list). + $byDayResults = array_merge($byDayResults, $dayHits); + } + } + } + + $byMonthDayResults = []; + if ($this->byMonthDay) { + foreach ($this->byMonthDay as $monthDay) { + // Removing values that are out of range for this month + if ($monthDay > $startDate->format('t') || + $monthDay < 0 - $startDate->format('t')) { + continue; + } + if ($monthDay > 0) { + $byMonthDayResults[] = $monthDay; + } else { + // Negative values + $byMonthDayResults[] = $startDate->format('t') + 1 + $monthDay; + } + } + } + + // If there was just byDay or just byMonthDay, they just specify our + // (almost) final list. If both were provided, then byDay limits the + // list. + if ($this->byMonthDay && $this->byDay) { + $result = array_intersect($byMonthDayResults, $byDayResults); + } elseif ($this->byMonthDay) { + $result = $byMonthDayResults; + } else { + $result = $byDayResults; + } + $result = array_unique($result); + sort($result, SORT_NUMERIC); + + // The last thing that needs checking is the BYSETPOS. If it's set, it + // means only certain items in the set survive the filter. + if (!$this->bySetPos) { + return $result; + } + + $filteredResult = []; + foreach ($this->bySetPos as $setPos) { + if ($setPos < 0) { + $setPos = count($result) + ($setPos + 1); + } + if (isset($result[$setPos - 1])) { + $filteredResult[] = $result[$setPos - 1]; + } + } + + sort($filteredResult, SORT_NUMERIC); + + return $filteredResult; + } + + /** + * Simple mapping from iCalendar day names to day numbers. + * + * @var array + */ + protected $dayMap = [ + 'SU' => 0, + 'MO' => 1, + 'TU' => 2, + 'WE' => 3, + 'TH' => 4, + 'FR' => 5, + 'SA' => 6, + ]; + + protected function getHours() + { + $recurrenceHours = []; + foreach ($this->byHour as $byHour) { + $recurrenceHours[] = $byHour; + } + + return $recurrenceHours; + } + + protected function getDays() + { + $recurrenceDays = []; + foreach ($this->byDay as $byDay) { + // The day may be preceded with a positive (+n) or + // negative (-n) integer. However, this does not make + // sense in 'weekly' so we ignore it here. + $recurrenceDays[] = $this->dayMap[substr($byDay, -2)]; + } + + return $recurrenceDays; + } + + protected function getMonths() + { + $recurrenceMonths = []; + foreach ($this->byMonth as $byMonth) { + $recurrenceMonths[] = $byMonth; + } + + return $recurrenceMonths; + } +} diff --git a/3rdparty/sabre/vobject/lib/Settings.php b/3rdparty/sabre/vobject/lib/Settings.php new file mode 100644 index 00000000..b0bb80a8 --- /dev/null +++ b/3rdparty/sabre/vobject/lib/Settings.php @@ -0,0 +1,55 @@ +children() as $component) { + if (!$component instanceof VObject\Component) { + continue; + } + + // Get all timezones + if ('VTIMEZONE' === $component->name) { + $this->vtimezones[(string) $component->TZID] = $component; + continue; + } + + // Get component UID for recurring Events search + if (!$component->UID) { + $component->UID = sha1(microtime()).'-vobjectimport'; + } + $uid = (string) $component->UID; + + // Take care of recurring events + if (!array_key_exists($uid, $this->objects)) { + $this->objects[$uid] = new VCalendar(); + } + + $this->objects[$uid]->add(clone $component); + } + } + + /** + * Every time getNext() is called, a new object will be parsed, until we + * hit the end of the stream. + * + * When the end is reached, null will be returned. + * + * @return \Sabre\VObject\Component|null + */ + public function getNext() + { + if ($object = array_shift($this->objects)) { + // create our baseobject + $object->version = '2.0'; + $object->prodid = '-//Sabre//Sabre VObject '.VObject\Version::VERSION.'//EN'; + $object->calscale = 'GREGORIAN'; + + // add vtimezone information to obj (if we have it) + foreach ($this->vtimezones as $vtimezone) { + $object->add($vtimezone); + } + + return $object; + } else { + return; + } + } +} diff --git a/3rdparty/sabre/vobject/lib/Splitter/SplitterInterface.php b/3rdparty/sabre/vobject/lib/Splitter/SplitterInterface.php new file mode 100644 index 00000000..c845ac5f --- /dev/null +++ b/3rdparty/sabre/vobject/lib/Splitter/SplitterInterface.php @@ -0,0 +1,38 @@ +input = $input; + $this->parser = new MimeDir($input, $options); + } + + /** + * Every time getNext() is called, a new object will be parsed, until we + * hit the end of the stream. + * + * When the end is reached, null will be returned. + * + * @return \Sabre\VObject\Component|null + */ + public function getNext() + { + try { + $object = $this->parser->parse(); + + if (!$object instanceof VObject\Component\VCard) { + throw new VObject\ParseException('The supplied input contained non-VCARD data.'); + } + } catch (VObject\EofException $e) { + return; + } + + return $object; + } +} diff --git a/3rdparty/sabre/vobject/lib/StringUtil.php b/3rdparty/sabre/vobject/lib/StringUtil.php new file mode 100644 index 00000000..b04539e4 --- /dev/null +++ b/3rdparty/sabre/vobject/lib/StringUtil.php @@ -0,0 +1,50 @@ +addGuesser('lic', new GuessFromLicEntry()); + $this->addGuesser('msTzId', new GuessFromMsTzId()); + $this->addFinder('tzid', new FindFromTimezoneIdentifier()); + $this->addFinder('tzmap', new FindFromTimezoneMap()); + $this->addFinder('offset', new FindFromOffset()); + } + + private static function getInstance(): self + { + if (null === self::$instance) { + self::$instance = new self(); + } + + return self::$instance; + } + + private function addGuesser(string $key, TimezoneGuesser $guesser): void + { + $this->timezoneGuessers[$key] = $guesser; + } + + private function addFinder(string $key, TimezoneFinder $finder): void + { + $this->timezoneFinders[$key] = $finder; + } + + /** + * This method will try to find out the correct timezone for an iCalendar + * date-time value. + * + * You must pass the contents of the TZID parameter, as well as the full + * calendar. + * + * If the lookup fails, this method will return the default PHP timezone + * (as configured using date_default_timezone_set, or the date.timezone ini + * setting). + * + * Alternatively, if $failIfUncertain is set to true, it will throw an + * exception if we cannot accurately determine the timezone. + */ + private function findTimeZone(string $tzid, ?Component $vcalendar = null, bool $failIfUncertain = false): DateTimeZone + { + foreach ($this->timezoneFinders as $timezoneFinder) { + $timezone = $timezoneFinder->find($tzid, $failIfUncertain); + if (!$timezone instanceof DateTimeZone) { + continue; + } + + return $timezone; + } + + if ($vcalendar) { + // If that didn't work, we will scan VTIMEZONE objects + foreach ($vcalendar->select('VTIMEZONE') as $vtimezone) { + if ((string) $vtimezone->TZID === $tzid) { + foreach ($this->timezoneGuessers as $timezoneGuesser) { + $timezone = $timezoneGuesser->guess($vtimezone, $failIfUncertain); + if (!$timezone instanceof DateTimeZone) { + continue; + } + + return $timezone; + } + } + } + } + + if ($failIfUncertain) { + throw new InvalidArgumentException('We were unable to determine the correct PHP timezone for tzid: '.$tzid); + } + + // If we got all the way here, we default to whatever has been set as the PHP default timezone. + return new DateTimeZone(date_default_timezone_get()); + } + + public static function addTimezoneGuesser(string $key, TimezoneGuesser $guesser): void + { + self::getInstance()->addGuesser($key, $guesser); + } + + public static function addTimezoneFinder(string $key, TimezoneFinder $finder): void + { + self::getInstance()->addFinder($key, $finder); + } + + /** + * @param string $tzid + * @param false $failIfUncertain + * + * @return DateTimeZone + */ + public static function getTimeZone($tzid, ?Component $vcalendar = null, $failIfUncertain = false) + { + return self::getInstance()->findTimeZone($tzid, $vcalendar, $failIfUncertain); + } + + public static function clean(): void + { + self::$instance = null; + } + + // Keeping things for backwards compatibility + /** + * @var array|null + * + * @deprecated + */ + public static $map = null; + + /** + * List of microsoft exchange timezone ids. + * + * Source: http://msdn.microsoft.com/en-us/library/aa563018(loband).aspx + * + * @deprecated + */ + public static $microsoftExchangeMap = [ + 0 => 'UTC', + 31 => 'Africa/Casablanca', + // Insanely, id #2 is used for both Europe/Lisbon, and Europe/Sarajevo. + // I'm not even kidding.. We handle this special case in the + // getTimeZone method. + 2 => 'Europe/Lisbon', + 1 => 'Europe/London', + 4 => 'Europe/Berlin', + 6 => 'Europe/Prague', + 3 => 'Europe/Paris', + 69 => 'Africa/Luanda', // This was a best guess + 7 => 'Europe/Athens', + 5 => 'Europe/Bucharest', + 49 => 'Africa/Cairo', + 50 => 'Africa/Harare', + 59 => 'Europe/Helsinki', + 27 => 'Asia/Jerusalem', + 26 => 'Asia/Baghdad', + 74 => 'Asia/Kuwait', + 51 => 'Europe/Moscow', + 56 => 'Africa/Nairobi', + 25 => 'Asia/Tehran', + 24 => 'Asia/Muscat', // Best guess + 54 => 'Asia/Baku', + 48 => 'Asia/Kabul', + 58 => 'Asia/Yekaterinburg', + 47 => 'Asia/Karachi', + 23 => 'Asia/Calcutta', + 62 => 'Asia/Kathmandu', + 46 => 'Asia/Almaty', + 71 => 'Asia/Dhaka', + 66 => 'Asia/Colombo', + 61 => 'Asia/Rangoon', + 22 => 'Asia/Bangkok', + 64 => 'Asia/Krasnoyarsk', + 45 => 'Asia/Shanghai', + 63 => 'Asia/Irkutsk', + 21 => 'Asia/Singapore', + 73 => 'Australia/Perth', + 75 => 'Asia/Taipei', + 20 => 'Asia/Tokyo', + 72 => 'Asia/Seoul', + 70 => 'Asia/Yakutsk', + 19 => 'Australia/Adelaide', + 44 => 'Australia/Darwin', + 18 => 'Australia/Brisbane', + 76 => 'Australia/Sydney', + 43 => 'Pacific/Guam', + 42 => 'Australia/Hobart', + 68 => 'Asia/Vladivostok', + 41 => 'Asia/Magadan', + 17 => 'Pacific/Auckland', + 40 => 'Pacific/Fiji', + 67 => 'Pacific/Tongatapu', + 29 => 'Atlantic/Azores', + 53 => 'Atlantic/Cape_Verde', + 30 => 'America/Noronha', + 8 => 'America/Sao_Paulo', // Best guess + 32 => 'America/Argentina/Buenos_Aires', + 60 => 'America/Godthab', + 28 => 'America/St_Johns', + 9 => 'America/Halifax', + 33 => 'America/Caracas', + 65 => 'America/Santiago', + 35 => 'America/Bogota', + 10 => 'America/New_York', + 34 => 'America/Indiana/Indianapolis', + 55 => 'America/Guatemala', + 11 => 'America/Chicago', + 37 => 'America/Mexico_City', + 36 => 'America/Edmonton', + 38 => 'America/Phoenix', + 12 => 'America/Denver', // Best guess + 13 => 'America/Los_Angeles', // Best guess + 14 => 'America/Anchorage', + 15 => 'Pacific/Honolulu', + 16 => 'Pacific/Midway', + 39 => 'Pacific/Kwajalein', + ]; + + /** + * This method will load in all the tz mapping information, if it's not yet + * done. + * + * @deprecated + */ + public static function loadTzMaps() + { + if (!is_null(self::$map)) { + return; + } + + self::$map = array_merge( + include __DIR__.'/timezonedata/windowszones.php', + include __DIR__.'/timezonedata/lotuszones.php', + include __DIR__.'/timezonedata/exchangezones.php', + include __DIR__.'/timezonedata/php-workaround.php' + ); + } + + /** + * This method returns an array of timezone identifiers, that are supported + * by DateTimeZone(), but not returned by DateTimeZone::listIdentifiers(). + * + * We're not using DateTimeZone::listIdentifiers(DateTimeZone::ALL_WITH_BC) because: + * - It's not supported by some PHP versions as well as HHVM. + * - It also returns identifiers, that are invalid values for new DateTimeZone() on some PHP versions. + * (See timezonedata/php-bc.php and timezonedata php-workaround.php) + * + * @return array + * + * @deprecated + */ + public static function getIdentifiersBC() + { + return include __DIR__.'/timezonedata/php-bc.php'; + } +} diff --git a/3rdparty/sabre/vobject/lib/TimezoneGuesser/FindFromOffset.php b/3rdparty/sabre/vobject/lib/TimezoneGuesser/FindFromOffset.php new file mode 100644 index 00000000..990ac969 --- /dev/null +++ b/3rdparty/sabre/vobject/lib/TimezoneGuesser/FindFromOffset.php @@ -0,0 +1,31 @@ +getIdentifiersBC())) + ) { + return new DateTimeZone($tzid); + } + } catch (Exception $e) { + } + + return null; + } + + /** + * This method returns an array of timezone identifiers, that are supported + * by DateTimeZone(), but not returned by DateTimeZone::listIdentifiers(). + * + * We're not using DateTimeZone::listIdentifiers(DateTimeZone::ALL_WITH_BC) because: + * - It's not supported by some PHP versions as well as HHVM. + * - It also returns identifiers, that are invalid values for new DateTimeZone() on some PHP versions. + * (See timezonedata/php-bc.php and timezonedata php-workaround.php) + * + * @return array + */ + private function getIdentifiersBC() + { + return include __DIR__.'/../timezonedata/php-bc.php'; + } +} diff --git a/3rdparty/sabre/vobject/lib/TimezoneGuesser/FindFromTimezoneMap.php b/3rdparty/sabre/vobject/lib/TimezoneGuesser/FindFromTimezoneMap.php new file mode 100644 index 00000000..b52ba6a1 --- /dev/null +++ b/3rdparty/sabre/vobject/lib/TimezoneGuesser/FindFromTimezoneMap.php @@ -0,0 +1,78 @@ +hasTzInMap($tzid)) { + return new DateTimeZone($this->getTzFromMap($tzid)); + } + + // Some Microsoft products prefix the offset first, so let's strip that off + // and see if it is our tzid map. We don't want to check for this first just + // in case there are overrides in our tzid map. + foreach ($this->patterns as $pattern) { + if (!preg_match($pattern, $tzid, $matches)) { + continue; + } + $tzidAlternate = $matches[3]; + if ($this->hasTzInMap($tzidAlternate)) { + return new DateTimeZone($this->getTzFromMap($tzidAlternate)); + } + } + + return null; + } + + /** + * This method returns an array of timezone identifiers, that are supported + * by DateTimeZone(), but not returned by DateTimeZone::listIdentifiers(). + * + * We're not using DateTimeZone::listIdentifiers(DateTimeZone::ALL_WITH_BC) because: + * - It's not supported by some PHP versions as well as HHVM. + * - It also returns identifiers, that are invalid values for new DateTimeZone() on some PHP versions. + * (See timezonedata/php-bc.php and timezonedata php-workaround.php) + * + * @return array + */ + private function getTzMaps() + { + if ([] === $this->map) { + $this->map = array_merge( + include __DIR__.'/../timezonedata/windowszones.php', + include __DIR__.'/../timezonedata/lotuszones.php', + include __DIR__.'/../timezonedata/exchangezones.php', + include __DIR__.'/../timezonedata/php-workaround.php' + ); + } + + return $this->map; + } + + private function getTzFromMap(string $tzid): string + { + return $this->getTzMaps()[$tzid]; + } + + private function hasTzInMap(string $tzid): bool + { + return isset($this->getTzMaps()[$tzid]); + } +} diff --git a/3rdparty/sabre/vobject/lib/TimezoneGuesser/GuessFromLicEntry.php b/3rdparty/sabre/vobject/lib/TimezoneGuesser/GuessFromLicEntry.php new file mode 100644 index 00000000..f340a396 --- /dev/null +++ b/3rdparty/sabre/vobject/lib/TimezoneGuesser/GuessFromLicEntry.php @@ -0,0 +1,33 @@ +{'X-LIC-LOCATION'})) { + return null; + } + + $lic = (string) $vtimezone->{'X-LIC-LOCATION'}; + + // Libical generators may specify strings like + // "SystemV/EST5EDT". For those we must remove the + // SystemV part. + if ('SystemV/' === substr($lic, 0, 8)) { + $lic = substr($lic, 8); + } + + return TimeZoneUtil::getTimeZone($lic, null, $failIfUncertain); + } +} diff --git a/3rdparty/sabre/vobject/lib/TimezoneGuesser/GuessFromMsTzId.php b/3rdparty/sabre/vobject/lib/TimezoneGuesser/GuessFromMsTzId.php new file mode 100644 index 00000000..b11ce183 --- /dev/null +++ b/3rdparty/sabre/vobject/lib/TimezoneGuesser/GuessFromMsTzId.php @@ -0,0 +1,119 @@ + 'UTC', + 31 => 'Africa/Casablanca', + + // Insanely, id #2 is used for both Europe/Lisbon, and Europe/Sarajevo. + // I'm not even kidding.. We handle this special case in the + // getTimeZone method. + 2 => 'Europe/Lisbon', + 1 => 'Europe/London', + 4 => 'Europe/Berlin', + 6 => 'Europe/Prague', + 3 => 'Europe/Paris', + 69 => 'Africa/Luanda', // This was a best guess + 7 => 'Europe/Athens', + 5 => 'Europe/Bucharest', + 49 => 'Africa/Cairo', + 50 => 'Africa/Harare', + 59 => 'Europe/Helsinki', + 27 => 'Asia/Jerusalem', + 26 => 'Asia/Baghdad', + 74 => 'Asia/Kuwait', + 51 => 'Europe/Moscow', + 56 => 'Africa/Nairobi', + 25 => 'Asia/Tehran', + 24 => 'Asia/Muscat', // Best guess + 54 => 'Asia/Baku', + 48 => 'Asia/Kabul', + 58 => 'Asia/Yekaterinburg', + 47 => 'Asia/Karachi', + 23 => 'Asia/Calcutta', + 62 => 'Asia/Kathmandu', + 46 => 'Asia/Almaty', + 71 => 'Asia/Dhaka', + 66 => 'Asia/Colombo', + 61 => 'Asia/Rangoon', + 22 => 'Asia/Bangkok', + 64 => 'Asia/Krasnoyarsk', + 45 => 'Asia/Shanghai', + 63 => 'Asia/Irkutsk', + 21 => 'Asia/Singapore', + 73 => 'Australia/Perth', + 75 => 'Asia/Taipei', + 20 => 'Asia/Tokyo', + 72 => 'Asia/Seoul', + 70 => 'Asia/Yakutsk', + 19 => 'Australia/Adelaide', + 44 => 'Australia/Darwin', + 18 => 'Australia/Brisbane', + 76 => 'Australia/Sydney', + 43 => 'Pacific/Guam', + 42 => 'Australia/Hobart', + 68 => 'Asia/Vladivostok', + 41 => 'Asia/Magadan', + 17 => 'Pacific/Auckland', + 40 => 'Pacific/Fiji', + 67 => 'Pacific/Tongatapu', + 29 => 'Atlantic/Azores', + 53 => 'Atlantic/Cape_Verde', + 30 => 'America/Noronha', + 8 => 'America/Sao_Paulo', // Best guess + 32 => 'America/Argentina/Buenos_Aires', + 60 => 'America/Godthab', + 28 => 'America/St_Johns', + 9 => 'America/Halifax', + 33 => 'America/Caracas', + 65 => 'America/Santiago', + 35 => 'America/Bogota', + 10 => 'America/New_York', + 34 => 'America/Indiana/Indianapolis', + 55 => 'America/Guatemala', + 11 => 'America/Chicago', + 37 => 'America/Mexico_City', + 36 => 'America/Edmonton', + 38 => 'America/Phoenix', + 12 => 'America/Denver', // Best guess + 13 => 'America/Los_Angeles', // Best guess + 14 => 'America/Anchorage', + 15 => 'Pacific/Honolulu', + 16 => 'Pacific/Midway', + 39 => 'Pacific/Kwajalein', + ]; + + public function guess(VTimeZone $vtimezone, bool $throwIfUnsure = false): ?DateTimeZone + { + // Microsoft may add a magic number, which we also have an + // answer for. + if (!isset($vtimezone->{'X-MICROSOFT-CDO-TZID'})) { + return null; + } + $cdoId = (int) $vtimezone->{'X-MICROSOFT-CDO-TZID'}->getValue(); + + // 2 can mean both Europe/Lisbon and Europe/Sarajevo. + if (2 === $cdoId && false !== strpos((string) $vtimezone->TZID, 'Sarajevo')) { + return new DateTimeZone('Europe/Sarajevo'); + } + + if (isset(self::$microsoftExchangeMap[$cdoId])) { + return new DateTimeZone(self::$microsoftExchangeMap[$cdoId]); + } + + return null; + } +} diff --git a/3rdparty/sabre/vobject/lib/TimezoneGuesser/TimezoneFinder.php b/3rdparty/sabre/vobject/lib/TimezoneGuesser/TimezoneFinder.php new file mode 100644 index 00000000..5aa880a1 --- /dev/null +++ b/3rdparty/sabre/vobject/lib/TimezoneGuesser/TimezoneFinder.php @@ -0,0 +1,10 @@ +getDocumentType(); + if ($inputVersion === $targetVersion) { + return clone $input; + } + + if (!in_array($inputVersion, [Document::VCARD21, Document::VCARD30, Document::VCARD40])) { + throw new \InvalidArgumentException('Only vCard 2.1, 3.0 and 4.0 are supported for the input data'); + } + if (!in_array($targetVersion, [Document::VCARD30, Document::VCARD40])) { + throw new \InvalidArgumentException('You can only use vCard 3.0 or 4.0 for the target version'); + } + + $newVersion = Document::VCARD40 === $targetVersion ? '4.0' : '3.0'; + + $output = new Component\VCard([ + 'VERSION' => $newVersion, + ]); + + // We might have generated a default UID. Remove it! + unset($output->UID); + + foreach ($input->children() as $property) { + $this->convertProperty($input, $output, $property, $targetVersion); + } + + return $output; + } + + /** + * Handles conversion of a single property. + * + * @param int $targetVersion + */ + protected function convertProperty(Component\VCard $input, Component\VCard $output, Property $property, $targetVersion) + { + // Skipping these, those are automatically added. + if (in_array($property->name, ['VERSION', 'PRODID'])) { + return; + } + + $parameters = $property->parameters(); + $valueType = null; + if (isset($parameters['VALUE'])) { + $valueType = $parameters['VALUE']->getValue(); + unset($parameters['VALUE']); + } + if (!$valueType) { + $valueType = $property->getValueType(); + } + if (Document::VCARD30 !== $targetVersion && 'PHONE-NUMBER' === $valueType) { + $valueType = null; + } + $newProperty = $output->createProperty( + $property->name, + $property->getParts(), + [], // parameters will get added a bit later. + $valueType + ); + + if (Document::VCARD30 === $targetVersion) { + if ($property instanceof Property\Uri && in_array($property->name, ['PHOTO', 'LOGO', 'SOUND'])) { + $newProperty = $this->convertUriToBinary($output, $newProperty); + } elseif ($property instanceof Property\VCard\DateAndOrTime) { + // In vCard 4, the birth year may be optional. This is not the + // case for vCard 3. Apple has a workaround for this that + // allows applications that support Apple's extension still + // omit birthyears in vCard 3, but applications that do not + // support this, will just use a random birthyear. We're + // choosing 1604 for the birthyear, because that's what apple + // uses. + $parts = DateTimeParser::parseVCardDateTime($property->getValue()); + if (is_null($parts['year'])) { + $newValue = '1604-'.$parts['month'].'-'.$parts['date']; + $newProperty->setValue($newValue); + $newProperty['X-APPLE-OMIT-YEAR'] = '1604'; + } + + if ('ANNIVERSARY' == $newProperty->name) { + // Microsoft non-standard anniversary + $newProperty->name = 'X-ANNIVERSARY'; + + // We also need to add a new apple property for the same + // purpose. This apple property needs a 'label' in the same + // group, so we first need to find a groupname that doesn't + // exist yet. + $x = 1; + while ($output->select('ITEM'.$x.'.')) { + ++$x; + } + $output->add('ITEM'.$x.'.X-ABDATE', $newProperty->getValue(), ['VALUE' => 'DATE-AND-OR-TIME']); + $output->add('ITEM'.$x.'.X-ABLABEL', '_$!!$_'); + } + } elseif ('KIND' === $property->name) { + switch (strtolower($property->getValue())) { + case 'org': + // vCard 3.0 does not have an equivalent to KIND:ORG, + // but apple has an extension that means the same + // thing. + $newProperty = $output->createProperty('X-ABSHOWAS', 'COMPANY'); + break; + + case 'individual': + // Individual is implicit, so we skip it. + return; + + case 'group': + // OS X addressbook property + $newProperty = $output->createProperty('X-ADDRESSBOOKSERVER-KIND', 'GROUP'); + break; + } + } elseif ('MEMBER' === $property->name) { + $newProperty = $output->createProperty('X-ADDRESSBOOKSERVER-MEMBER', $property->getValue()); + } + } elseif (Document::VCARD40 === $targetVersion) { + // These properties were removed in vCard 4.0 + if (in_array($property->name, ['NAME', 'MAILER', 'LABEL', 'CLASS'])) { + return; + } + + if ($property instanceof Property\Binary) { + $newProperty = $this->convertBinaryToUri($output, $newProperty, $parameters); + } elseif ($property instanceof Property\VCard\DateAndOrTime && isset($parameters['X-APPLE-OMIT-YEAR'])) { + // If a property such as BDAY contained 'X-APPLE-OMIT-YEAR', + // then we're stripping the year from the vcard 4 value. + $parts = DateTimeParser::parseVCardDateTime($property->getValue()); + if ($parts['year'] === $property['X-APPLE-OMIT-YEAR']->getValue()) { + $newValue = '--'.$parts['month'].'-'.$parts['date']; + $newProperty->setValue($newValue); + } + + // Regardless if the year matched or not, we do need to strip + // X-APPLE-OMIT-YEAR. + unset($parameters['X-APPLE-OMIT-YEAR']); + } + switch ($property->name) { + case 'X-ABSHOWAS': + if ('COMPANY' === strtoupper($property->getValue())) { + $newProperty = $output->createProperty('KIND', 'ORG'); + } + break; + case 'X-ADDRESSBOOKSERVER-KIND': + if ('GROUP' === strtoupper($property->getValue())) { + $newProperty = $output->createProperty('KIND', 'GROUP'); + } + break; + case 'X-ADDRESSBOOKSERVER-MEMBER': + $newProperty = $output->createProperty('MEMBER', $property->getValue()); + break; + case 'X-ANNIVERSARY': + $newProperty->name = 'ANNIVERSARY'; + // If we already have an anniversary property with the same + // value, ignore. + foreach ($output->select('ANNIVERSARY') as $anniversary) { + if ($anniversary->getValue() === $newProperty->getValue()) { + return; + } + } + break; + case 'X-ABDATE': + // Find out what the label was, if it exists. + if (!$property->group) { + break; + } + $label = $input->{$property->group.'.X-ABLABEL'}; + + // We only support converting anniversaries. + if (!$label || '_$!!$_' !== $label->getValue()) { + break; + } + + // If we already have an anniversary property with the same + // value, ignore. + foreach ($output->select('ANNIVERSARY') as $anniversary) { + if ($anniversary->getValue() === $newProperty->getValue()) { + return; + } + } + $newProperty->name = 'ANNIVERSARY'; + break; + // Apple's per-property label system. + case 'X-ABLABEL': + if ('_$!!$_' === $newProperty->getValue()) { + // We can safely remove these, as they are converted to + // ANNIVERSARY properties. + return; + } + break; + } + } + + // set property group + $newProperty->group = $property->group; + + if (Document::VCARD40 === $targetVersion) { + $this->convertParameters40($newProperty, $parameters); + } else { + $this->convertParameters30($newProperty, $parameters); + } + + // Lastly, we need to see if there's a need for a VALUE parameter. + // + // We can do that by instantiating a empty property with that name, and + // seeing if the default valueType is identical to the current one. + $tempProperty = $output->createProperty($newProperty->name); + if ($tempProperty->getValueType() !== $newProperty->getValueType()) { + $newProperty['VALUE'] = $newProperty->getValueType(); + } + + $output->add($newProperty); + } + + /** + * Converts a BINARY property to a URI property. + * + * vCard 4.0 no longer supports BINARY properties. + * + * @param Property\Uri $property the input property + * @param $parameters list of parameters that will eventually be added to + * the new property + * + * @return Property\Uri + */ + protected function convertBinaryToUri(Component\VCard $output, Property\Binary $newProperty, array &$parameters) + { + $value = $newProperty->getValue(); + $newProperty = $output->createProperty( + $newProperty->name, + null, // no value + [], // no parameters yet + 'URI' // Forcing the BINARY type + ); + + $mimeType = 'application/octet-stream'; + + // See if we can find a better mimetype. + if (isset($parameters['TYPE'])) { + $newTypes = []; + foreach ($parameters['TYPE']->getParts() as $typePart) { + if (in_array( + strtoupper($typePart), + ['JPEG', 'PNG', 'GIF'] + )) { + $mimeType = 'image/'.strtolower($typePart); + } else { + $newTypes[] = $typePart; + } + } + + // If there were any parameters we're not converting to a + // mime-type, we need to keep them. + if ($newTypes) { + $parameters['TYPE']->setParts($newTypes); + } else { + unset($parameters['TYPE']); + } + } + + $newProperty->setValue('data:'.$mimeType.';base64,'.base64_encode($value)); + + return $newProperty; + } + + /** + * Converts a URI property to a BINARY property. + * + * In vCard 4.0 attachments are encoded as data: uri. Even though these may + * be valid in vCard 3.0 as well, we should convert those to BINARY if + * possible, to improve compatibility. + * + * @param Property\Uri $property the input property + * + * @return Property\Binary|null + */ + protected function convertUriToBinary(Component\VCard $output, Property\Uri $newProperty) + { + $value = $newProperty->getValue(); + + // Only converting data: uris + if ('data:' !== substr($value, 0, 5)) { + return $newProperty; + } + + $newProperty = $output->createProperty( + $newProperty->name, + null, // no value + [], // no parameters yet + 'BINARY' + ); + + $mimeType = substr($value, 5, strpos($value, ',') - 5); + if (strpos($mimeType, ';')) { + $mimeType = substr($mimeType, 0, strpos($mimeType, ';')); + $newProperty->setValue(base64_decode(substr($value, strpos($value, ',') + 1))); + } else { + $newProperty->setValue(substr($value, strpos($value, ',') + 1)); + } + unset($value); + + $newProperty['ENCODING'] = 'b'; + switch ($mimeType) { + case 'image/jpeg': + $newProperty['TYPE'] = 'JPEG'; + break; + case 'image/png': + $newProperty['TYPE'] = 'PNG'; + break; + case 'image/gif': + $newProperty['TYPE'] = 'GIF'; + break; + } + + return $newProperty; + } + + /** + * Adds parameters to a new property for vCard 4.0. + */ + protected function convertParameters40(Property $newProperty, array $parameters) + { + // Adding all parameters. + foreach ($parameters as $param) { + // vCard 2.1 allowed parameters with no name + if ($param->noName) { + $param->noName = false; + } + + switch ($param->name) { + // We need to see if there's any TYPE=PREF, because in vCard 4 + // that's now PREF=1. + case 'TYPE': + foreach ($param->getParts() as $paramPart) { + if ('PREF' === strtoupper($paramPart)) { + $newProperty->add('PREF', '1'); + } else { + $newProperty->add($param->name, $paramPart); + } + } + break; + // These no longer exist in vCard 4 + case 'ENCODING': + case 'CHARSET': + break; + + default: + $newProperty->add($param->name, $param->getParts()); + break; + } + } + } + + /** + * Adds parameters to a new property for vCard 3.0. + */ + protected function convertParameters30(Property $newProperty, array $parameters) + { + // Adding all parameters. + foreach ($parameters as $param) { + // vCard 2.1 allowed parameters with no name + if ($param->noName) { + $param->noName = false; + } + + switch ($param->name) { + case 'ENCODING': + // This value only existed in vCard 2.1, and should be + // removed for anything else. + if ('QUOTED-PRINTABLE' !== strtoupper($param->getValue())) { + $newProperty->add($param->name, $param->getParts()); + } + break; + + /* + * Converting PREF=1 to TYPE=PREF. + * + * Any other PREF numbers we'll drop. + */ + case 'PREF': + if ('1' == $param->getValue()) { + $newProperty->add('TYPE', 'PREF'); + } + break; + + default: + $newProperty->add($param->name, $param->getParts()); + break; + } + } + } +} diff --git a/3rdparty/sabre/vobject/lib/Version.php b/3rdparty/sabre/vobject/lib/Version.php new file mode 100644 index 00000000..060c69a3 --- /dev/null +++ b/3rdparty/sabre/vobject/lib/Version.php @@ -0,0 +1,18 @@ +serialize(); + } + + /** + * Serializes a jCal or jCard object. + * + * @param int $options + * + * @return string + */ + public static function writeJson(Component $component, $options = 0) + { + return json_encode($component, $options); + } + + /** + * Serializes a xCal or xCard object. + * + * @return string + */ + public static function writeXml(Component $component) + { + $writer = new Xml\Writer(); + $writer->openMemory(); + $writer->setIndent(true); + + $writer->startDocument('1.0', 'utf-8'); + + if ($component instanceof Component\VCalendar) { + $writer->startElement('icalendar'); + $writer->writeAttribute('xmlns', Parser\XML::XCAL_NAMESPACE); + } else { + $writer->startElement('vcards'); + $writer->writeAttribute('xmlns', Parser\XML::XCARD_NAMESPACE); + } + + $component->xmlSerialize($writer); + + $writer->endElement(); + + return $writer->outputMemory(); + } +} diff --git a/3rdparty/sabre/vobject/lib/timezonedata/exchangezones.php b/3rdparty/sabre/vobject/lib/timezonedata/exchangezones.php new file mode 100644 index 00000000..89bddc27 --- /dev/null +++ b/3rdparty/sabre/vobject/lib/timezonedata/exchangezones.php @@ -0,0 +1,94 @@ + 'UTC', + 'Casablanca, Monrovia' => 'Africa/Casablanca', + 'Greenwich Mean Time: Dublin, Edinburgh, Lisbon, London' => 'Europe/Lisbon', + 'Greenwich Mean Time; Dublin, Edinburgh, London' => 'Europe/London', + 'Amsterdam, Berlin, Bern, Rome, Stockholm, Vienna' => 'Europe/Berlin', + 'Belgrade, Pozsony, Budapest, Ljubljana, Prague' => 'Europe/Prague', + 'Brussels, Copenhagen, Madrid, Paris' => 'Europe/Paris', + 'Paris, Madrid, Brussels, Copenhagen' => 'Europe/Paris', + 'Prague, Central Europe' => 'Europe/Prague', + 'Sarajevo, Skopje, Sofija, Vilnius, Warsaw, Zagreb' => 'Europe/Sarajevo', + 'West Central Africa' => 'Africa/Luanda', // This was a best guess + 'Athens, Istanbul, Minsk' => 'Europe/Athens', + 'Bucharest' => 'Europe/Bucharest', + 'Cairo' => 'Africa/Cairo', + 'Harare, Pretoria' => 'Africa/Harare', + 'Helsinki, Riga, Tallinn' => 'Europe/Helsinki', + 'Israel, Jerusalem Standard Time' => 'Asia/Jerusalem', + 'Baghdad' => 'Asia/Baghdad', + 'Arab, Kuwait, Riyadh' => 'Asia/Kuwait', + 'Moscow, St. Petersburg, Volgograd' => 'Europe/Moscow', + 'East Africa, Nairobi' => 'Africa/Nairobi', + 'Tehran' => 'Asia/Tehran', + 'Abu Dhabi, Muscat' => 'Asia/Muscat', // Best guess + 'Baku, Tbilisi, Yerevan' => 'Asia/Baku', + 'Kabul' => 'Asia/Kabul', + 'Ekaterinburg' => 'Asia/Yekaterinburg', + 'Islamabad, Karachi, Tashkent' => 'Asia/Karachi', + 'Kolkata, Chennai, Mumbai, New Delhi, India Standard Time' => 'Asia/Calcutta', + 'Kathmandu, Nepal' => 'Asia/Kathmandu', + 'Almaty, Novosibirsk, North Central Asia' => 'Asia/Almaty', + 'Astana, Dhaka' => 'Asia/Dhaka', + 'Sri Jayawardenepura, Sri Lanka' => 'Asia/Colombo', + 'Rangoon' => 'Asia/Rangoon', + 'Bangkok, Hanoi, Jakarta' => 'Asia/Bangkok', + 'Krasnoyarsk' => 'Asia/Krasnoyarsk', + 'Beijing, Chongqing, Hong Kong SAR, Urumqi' => 'Asia/Shanghai', + 'Irkutsk, Ulaan Bataar' => 'Asia/Irkutsk', + 'Kuala Lumpur, Singapore' => 'Asia/Singapore', + 'Perth, Western Australia' => 'Australia/Perth', + 'Taipei' => 'Asia/Taipei', + 'Osaka, Sapporo, Tokyo' => 'Asia/Tokyo', + 'Seoul, Korea Standard time' => 'Asia/Seoul', + 'Yakutsk' => 'Asia/Yakutsk', + 'Adelaide, Central Australia' => 'Australia/Adelaide', + 'Darwin' => 'Australia/Darwin', + 'Brisbane, East Australia' => 'Australia/Brisbane', + 'Canberra, Melbourne, Sydney, Hobart (year 2000 only)' => 'Australia/Sydney', + 'Guam, Port Moresby' => 'Pacific/Guam', + 'Hobart, Tasmania' => 'Australia/Hobart', + 'Vladivostok' => 'Asia/Vladivostok', + 'Magadan, Solomon Is., New Caledonia' => 'Asia/Magadan', + 'Auckland, Wellington' => 'Pacific/Auckland', + 'Fiji Islands, Kamchatka, Marshall Is.' => 'Pacific/Fiji', + 'Nuku\'alofa, Tonga' => 'Pacific/Tongatapu', + 'Azores' => 'Atlantic/Azores', + 'Cape Verde Is.' => 'Atlantic/Cape_Verde', + 'Mid-Atlantic' => 'America/Noronha', + 'Brasilia' => 'America/Sao_Paulo', // Best guess + 'Buenos Aires' => 'America/Argentina/Buenos_Aires', + 'Greenland' => 'America/Godthab', + 'Newfoundland' => 'America/St_Johns', + 'Atlantic Time (Canada)' => 'America/Halifax', + 'Caracas, La Paz' => 'America/Caracas', + 'Santiago' => 'America/Santiago', + 'Bogota, Lima, Quito' => 'America/Bogota', + 'Eastern Time (US & Canada)' => 'America/New_York', + 'Indiana (East)' => 'America/Indiana/Indianapolis', + 'Central America' => 'America/Guatemala', + 'Central Time (US & Canada)' => 'America/Chicago', + 'Mexico City, Tegucigalpa' => 'America/Mexico_City', + 'Saskatchewan' => 'America/Edmonton', + 'Arizona' => 'America/Phoenix', + 'Mountain Time (US & Canada)' => 'America/Denver', // Best guess + 'Pacific Time (US & Canada)' => 'America/Los_Angeles', // Best guess + 'Pacific Time (US & Canada); Tijuana' => 'America/Los_Angeles', // Best guess + 'Alaska' => 'America/Anchorage', + 'Hawaii' => 'Pacific/Honolulu', + 'Midway Island, Samoa' => 'Pacific/Midway', + 'Eniwetok, Kwajalein, Dateline Time' => 'Pacific/Kwajalein', +]; diff --git a/3rdparty/sabre/vobject/lib/timezonedata/lotuszones.php b/3rdparty/sabre/vobject/lib/timezonedata/lotuszones.php new file mode 100644 index 00000000..4b50808f --- /dev/null +++ b/3rdparty/sabre/vobject/lib/timezonedata/lotuszones.php @@ -0,0 +1,101 @@ + 'Etc/GMT-12', + 'Samoa' => 'Pacific/Apia', + 'Hawaiian' => 'Pacific/Honolulu', + 'Alaskan' => 'America/Anchorage', + 'Pacific' => 'America/Los_Angeles', + 'Pacific Standard Time' => 'America/Los_Angeles', + 'Mexico Standard Time 2' => 'America/Chihuahua', + 'Mountain' => 'America/Denver', + // 'Mountain Standard Time' => 'America/Chihuahua', // conflict with windows timezones. + 'US Mountain' => 'America/Phoenix', + 'Canada Central' => 'America/Edmonton', + 'Central America' => 'America/Guatemala', + 'Central' => 'America/Chicago', + // 'Central Standard Time' => 'America/Mexico_City', // conflict with windows timezones. + 'Mexico' => 'America/Mexico_City', + 'Eastern' => 'America/New_York', + 'SA Pacific' => 'America/Bogota', + 'US Eastern' => 'America/Indiana/Indianapolis', + 'Venezuela' => 'America/Caracas', + 'Atlantic' => 'America/Halifax', + 'Central Brazilian' => 'America/Manaus', + 'Pacific SA' => 'America/Santiago', + 'SA Western' => 'America/La_Paz', + 'Newfoundland' => 'America/St_Johns', + 'Argentina' => 'America/Argentina/Buenos_Aires', + 'E. South America' => 'America/Belem', + 'Greenland' => 'America/Godthab', + 'Montevideo' => 'America/Montevideo', + 'SA Eastern' => 'America/Belem', + // 'Mid-Atlantic' => 'Etc/GMT-2', // conflict with windows timezones. + 'Azores' => 'Atlantic/Azores', + 'Cape Verde' => 'Atlantic/Cape_Verde', + 'Greenwich' => 'Atlantic/Reykjavik', // No I'm serious.. Greenwich is not GMT. + 'Morocco' => 'Africa/Casablanca', + 'Central Europe' => 'Europe/Prague', + 'Central European' => 'Europe/Sarajevo', + 'Romance' => 'Europe/Paris', + 'W. Central Africa' => 'Africa/Lagos', // Best guess + 'W. Europe' => 'Europe/Amsterdam', + 'E. Europe' => 'Europe/Minsk', + 'Egypt' => 'Africa/Cairo', + 'FLE' => 'Europe/Helsinki', + 'GTB' => 'Europe/Athens', + 'Israel' => 'Asia/Jerusalem', + 'Jordan' => 'Asia/Amman', + 'Middle East' => 'Asia/Beirut', + 'Namibia' => 'Africa/Windhoek', + 'South Africa' => 'Africa/Harare', + 'Arab' => 'Asia/Kuwait', + 'Arabic' => 'Asia/Baghdad', + 'E. Africa' => 'Africa/Nairobi', + 'Georgian' => 'Asia/Tbilisi', + 'Russian' => 'Europe/Moscow', + 'Iran' => 'Asia/Tehran', + 'Arabian' => 'Asia/Muscat', + 'Armenian' => 'Asia/Yerevan', + 'Azerbijan' => 'Asia/Baku', + 'Caucasus' => 'Asia/Yerevan', + 'Mauritius' => 'Indian/Mauritius', + 'Afghanistan' => 'Asia/Kabul', + 'Ekaterinburg' => 'Asia/Yekaterinburg', + 'Pakistan' => 'Asia/Karachi', + 'West Asia' => 'Asia/Tashkent', + 'India' => 'Asia/Calcutta', + 'Sri Lanka' => 'Asia/Colombo', + 'Nepal' => 'Asia/Kathmandu', + 'Central Asia' => 'Asia/Dhaka', + 'N. Central Asia' => 'Asia/Almaty', + 'Myanmar' => 'Asia/Rangoon', + 'North Asia' => 'Asia/Krasnoyarsk', + 'SE Asia' => 'Asia/Bangkok', + 'China' => 'Asia/Shanghai', + 'North Asia East' => 'Asia/Irkutsk', + 'Singapore' => 'Asia/Singapore', + 'Taipei' => 'Asia/Taipei', + 'W. Australia' => 'Australia/Perth', + 'Korea' => 'Asia/Seoul', + 'Tokyo' => 'Asia/Tokyo', + 'Yakutsk' => 'Asia/Yakutsk', + 'AUS Central' => 'Australia/Darwin', + 'Cen. Australia' => 'Australia/Adelaide', + 'AUS Eastern' => 'Australia/Sydney', + 'E. Australia' => 'Australia/Brisbane', + 'Tasmania' => 'Australia/Hobart', + 'Vladivostok' => 'Asia/Vladivostok', + 'West Pacific' => 'Pacific/Guam', + 'Central Pacific' => 'Asia/Magadan', + 'Fiji' => 'Pacific/Fiji', + 'New Zealand' => 'Pacific/Auckland', + 'Tonga' => 'Pacific/Tongatapu', +]; diff --git a/3rdparty/sabre/vobject/lib/timezonedata/php-bc.php b/3rdparty/sabre/vobject/lib/timezonedata/php-bc.php new file mode 100644 index 00000000..3116c686 --- /dev/null +++ b/3rdparty/sabre/vobject/lib/timezonedata/php-bc.php @@ -0,0 +1,152 @@ + 'America/Chicago', + 'Cuba' => 'America/Havana', + 'Egypt' => 'Africa/Cairo', + 'Eire' => 'Europe/Dublin', + 'EST5EDT' => 'America/New_York', + 'Factory' => 'UTC', + 'GB-Eire' => 'Europe/London', + 'GMT0' => 'UTC', + 'Greenwich' => 'UTC', + 'Hongkong' => 'Asia/Hong_Kong', + 'Iceland' => 'Atlantic/Reykjavik', + 'Iran' => 'Asia/Tehran', + 'Israel' => 'Asia/Jerusalem', + 'Jamaica' => 'America/Jamaica', + 'Japan' => 'Asia/Tokyo', + 'Kwajalein' => 'Pacific/Kwajalein', + 'Libya' => 'Africa/Tripoli', + 'MST7MDT' => 'America/Denver', + 'Navajo' => 'America/Denver', + 'NZ-CHAT' => 'Pacific/Chatham', + 'Poland' => 'Europe/Warsaw', + 'Portugal' => 'Europe/Lisbon', + 'PST8PDT' => 'America/Los_Angeles', + 'Singapore' => 'Asia/Singapore', + 'Turkey' => 'Europe/Istanbul', + 'Universal' => 'UTC', + 'W-SU' => 'Europe/Moscow', + 'Zulu' => 'UTC', +]; diff --git a/3rdparty/sabre/vobject/lib/timezonedata/windowszones.php b/3rdparty/sabre/vobject/lib/timezonedata/windowszones.php new file mode 100644 index 00000000..2049a95c --- /dev/null +++ b/3rdparty/sabre/vobject/lib/timezonedata/windowszones.php @@ -0,0 +1,152 @@ + 'Australia/Darwin', + 'AUS Eastern Standard Time' => 'Australia/Sydney', + 'Afghanistan Standard Time' => 'Asia/Kabul', + 'Alaskan Standard Time' => 'America/Anchorage', + 'Aleutian Standard Time' => 'America/Adak', + 'Altai Standard Time' => 'Asia/Barnaul', + 'Arab Standard Time' => 'Asia/Riyadh', + 'Arabian Standard Time' => 'Asia/Dubai', + 'Arabic Standard Time' => 'Asia/Baghdad', + 'Argentina Standard Time' => 'America/Buenos_Aires', + 'Astrakhan Standard Time' => 'Europe/Astrakhan', + 'Atlantic Standard Time' => 'America/Halifax', + 'Aus Central W. Standard Time' => 'Australia/Eucla', + 'Azerbaijan Standard Time' => 'Asia/Baku', + 'Azores Standard Time' => 'Atlantic/Azores', + 'Bahia Standard Time' => 'America/Bahia', + 'Bangladesh Standard Time' => 'Asia/Dhaka', + 'Belarus Standard Time' => 'Europe/Minsk', + 'Bougainville Standard Time' => 'Pacific/Bougainville', + 'Canada Central Standard Time' => 'America/Regina', + 'Cape Verde Standard Time' => 'Atlantic/Cape_Verde', + 'Caucasus Standard Time' => 'Asia/Yerevan', + 'Cen. Australia Standard Time' => 'Australia/Adelaide', + 'Central America Standard Time' => 'America/Guatemala', + 'Central Asia Standard Time' => 'Asia/Almaty', + 'Central Brazilian Standard Time' => 'America/Cuiaba', + 'Central Europe Standard Time' => 'Europe/Budapest', + 'Central European Standard Time' => 'Europe/Warsaw', + 'Central Pacific Standard Time' => 'Pacific/Guadalcanal', + 'Central Standard Time' => 'America/Chicago', + 'Central Standard Time (Mexico)' => 'America/Mexico_City', + 'Chatham Islands Standard Time' => 'Pacific/Chatham', + 'China Standard Time' => 'Asia/Shanghai', + 'Cuba Standard Time' => 'America/Havana', + 'Dateline Standard Time' => 'Etc/GMT+12', + 'E. Africa Standard Time' => 'Africa/Nairobi', + 'E. Australia Standard Time' => 'Australia/Brisbane', + 'E. Europe Standard Time' => 'Europe/Chisinau', + 'E. South America Standard Time' => 'America/Sao_Paulo', + 'Easter Island Standard Time' => 'Pacific/Easter', + 'Eastern Standard Time' => 'America/New_York', + 'Eastern Standard Time (Mexico)' => 'America/Cancun', + 'Egypt Standard Time' => 'Africa/Cairo', + 'Ekaterinburg Standard Time' => 'Asia/Yekaterinburg', + 'FLE Standard Time' => 'Europe/Kiev', + 'Fiji Standard Time' => 'Pacific/Fiji', + 'GMT Standard Time' => 'Europe/London', + 'GTB Standard Time' => 'Europe/Bucharest', + 'Georgian Standard Time' => 'Asia/Tbilisi', + 'Greenland Standard Time' => 'America/Godthab', + 'Greenwich Standard Time' => 'Atlantic/Reykjavik', + 'Haiti Standard Time' => 'America/Port-au-Prince', + 'Hawaiian Standard Time' => 'Pacific/Honolulu', + 'India Standard Time' => 'Asia/Calcutta', + 'Iran Standard Time' => 'Asia/Tehran', + 'Israel Standard Time' => 'Asia/Jerusalem', + 'Jordan Standard Time' => 'Asia/Amman', + 'Kaliningrad Standard Time' => 'Europe/Kaliningrad', + 'Korea Standard Time' => 'Asia/Seoul', + 'Libya Standard Time' => 'Africa/Tripoli', + 'Line Islands Standard Time' => 'Pacific/Kiritimati', + 'Lord Howe Standard Time' => 'Australia/Lord_Howe', + 'Magadan Standard Time' => 'Asia/Magadan', + 'Magallanes Standard Time' => 'America/Punta_Arenas', + 'Marquesas Standard Time' => 'Pacific/Marquesas', + 'Mauritius Standard Time' => 'Indian/Mauritius', + 'Middle East Standard Time' => 'Asia/Beirut', + 'Montevideo Standard Time' => 'America/Montevideo', + 'Morocco Standard Time' => 'Africa/Casablanca', + 'Mountain Standard Time' => 'America/Denver', + 'Mountain Standard Time (Mexico)' => 'America/Chihuahua', + 'Myanmar Standard Time' => 'Asia/Rangoon', + 'N. Central Asia Standard Time' => 'Asia/Novosibirsk', + 'Namibia Standard Time' => 'Africa/Windhoek', + 'Nepal Standard Time' => 'Asia/Katmandu', + 'New Zealand Standard Time' => 'Pacific/Auckland', + 'Newfoundland Standard Time' => 'America/St_Johns', + 'Norfolk Standard Time' => 'Pacific/Norfolk', + 'North Asia East Standard Time' => 'Asia/Irkutsk', + 'North Asia Standard Time' => 'Asia/Krasnoyarsk', + 'North Korea Standard Time' => 'Asia/Pyongyang', + 'Omsk Standard Time' => 'Asia/Omsk', + 'Pacific SA Standard Time' => 'America/Santiago', + 'Pacific Standard Time' => 'America/Los_Angeles', + 'Pacific Standard Time (Mexico)' => 'America/Tijuana', + 'Pakistan Standard Time' => 'Asia/Karachi', + 'Paraguay Standard Time' => 'America/Asuncion', + 'Qyzylorda Standard Time' => 'Asia/Qyzylorda', + 'Romance Standard Time' => 'Europe/Paris', + 'Russia Time Zone 10' => 'Asia/Srednekolymsk', + 'Russia Time Zone 11' => 'Asia/Kamchatka', + 'Russia Time Zone 3' => 'Europe/Samara', + 'Russian Standard Time' => 'Europe/Moscow', + 'SA Eastern Standard Time' => 'America/Cayenne', + 'SA Pacific Standard Time' => 'America/Bogota', + 'SA Western Standard Time' => 'America/La_Paz', + 'SE Asia Standard Time' => 'Asia/Bangkok', + 'Saint Pierre Standard Time' => 'America/Miquelon', + 'Sakhalin Standard Time' => 'Asia/Sakhalin', + 'Samoa Standard Time' => 'Pacific/Apia', + 'Sao Tome Standard Time' => 'Africa/Sao_Tome', + 'Saratov Standard Time' => 'Europe/Saratov', + 'Singapore Standard Time' => 'Asia/Singapore', + 'South Africa Standard Time' => 'Africa/Johannesburg', + 'Sri Lanka Standard Time' => 'Asia/Colombo', + 'Sudan Standard Time' => 'Africa/Khartoum', + 'Syria Standard Time' => 'Asia/Damascus', + 'Taipei Standard Time' => 'Asia/Taipei', + 'Tasmania Standard Time' => 'Australia/Hobart', + 'Tocantins Standard Time' => 'America/Araguaina', + 'Tokyo Standard Time' => 'Asia/Tokyo', + 'Tomsk Standard Time' => 'Asia/Tomsk', + 'Tonga Standard Time' => 'Pacific/Tongatapu', + 'Transbaikal Standard Time' => 'Asia/Chita', + 'Turkey Standard Time' => 'Europe/Istanbul', + 'Turks And Caicos Standard Time' => 'America/Grand_Turk', + 'US Eastern Standard Time' => 'America/Indianapolis', + 'US Mountain Standard Time' => 'America/Phoenix', + 'UTC' => 'Etc/GMT', + 'UTC+12' => 'Etc/GMT-12', + 'UTC+13' => 'Etc/GMT-13', + 'UTC-02' => 'Etc/GMT+2', + 'UTC-08' => 'Etc/GMT+8', + 'UTC-09' => 'Etc/GMT+9', + 'UTC-11' => 'Etc/GMT+11', + 'Ulaanbaatar Standard Time' => 'Asia/Ulaanbaatar', + 'Venezuela Standard Time' => 'America/Caracas', + 'Vladivostok Standard Time' => 'Asia/Vladivostok', + 'Volgograd Standard Time' => 'Europe/Volgograd', + 'W. Australia Standard Time' => 'Australia/Perth', + 'W. Central Africa Standard Time' => 'Africa/Lagos', + 'W. Europe Standard Time' => 'Europe/Berlin', + 'W. Mongolia Standard Time' => 'Asia/Hovd', + 'West Asia Standard Time' => 'Asia/Tashkent', + 'West Bank Standard Time' => 'Asia/Hebron', + 'West Pacific Standard Time' => 'Pacific/Port_Moresby', + 'Yakutsk Standard Time' => 'Asia/Yakutsk', + 'Yukon Standard Time' => 'America/Whitehorse', +]; diff --git a/3rdparty/sabre/vobject/resources/schema/xcal.rng b/3rdparty/sabre/vobject/resources/schema/xcal.rng new file mode 100644 index 00000000..4a51460e --- /dev/null +++ b/3rdparty/sabre/vobject/resources/schema/xcal.rng @@ -0,0 +1,1192 @@ +# RELAX NG Schema for iCalendar in XML +# Extract from RFC6321. +# Erratum 3042 applied. +# Erratum 3050 applied. +# Erratum 3314 applied. + +default namespace = "urn:ietf:params:xml:ns:icalendar-2.0" + +# 3.2 Property Parameters + +# 3.2.1 Alternate Text Representation + +altrepparam = element altrep { + value-uri +} + +# 3.2.2 Common Name + +cnparam = element cn { + value-text +} + +# 3.2.3 Calendar User Type + +cutypeparam = element cutype { + element text { + "INDIVIDUAL" | + "GROUP" | + "RESOURCE" | + "ROOM" | + "UNKNOWN" + } +} + +# 3.2.4 Delegators + +delfromparam = element delegated-from { + value-cal-address+ +} + +# 3.2.5 Delegatees + +deltoparam = element delegated-to { + value-cal-address+ +} + +# 3.2.6 Directory Entry Reference + +dirparam = element dir { + value-uri +} + +# 3.2.7 Inline Encoding + +encodingparam = element encoding { + element text { + "8BIT" | + "BASE64" + } +} + +# 3.2.8 Format Type + +fmttypeparam = element fmttype { + value-text +} + +# 3.2.9 Free/Busy Time Type + +fbtypeparam = element fbtype { + element text { + "FREE" | + "BUSY" | + "BUSY-UNAVAILABLE" | + "BUSY-TENTATIVE" + } +} + +# 3.2.10 Language + +languageparam = element language { + value-text +} + +# 3.2.11 Group or List Membership + +memberparam = element member { + value-cal-address+ +} + +# 3.2.12 Participation Status + +partstatparam = element partstat { + type-partstat-event | + type-partstat-todo | + type-partstat-jour +} + +type-partstat-event = ( + element text { + "NEEDS-ACTION" | + "ACCEPTED" | + "DECLINED" | + "TENTATIVE" | + "DELEGATED" + } +) + +type-partstat-todo = ( + element text { + "NEEDS-ACTION" | + "ACCEPTED" | + "DECLINED" | + "TENTATIVE" | + "DELEGATED" | + "COMPLETED" | + "IN-PROCESS" + } +) + +type-partstat-jour = ( + element text { + "NEEDS-ACTION" | + "ACCEPTED" | + "DECLINED" + } +) + +# 3.2.13 Recurrence Identifier Range + +rangeparam = element range { + element text { + "THISANDFUTURE" + } +} + +# 3.2.14 Alarm Trigger Relationship + +trigrelparam = element related { + element text { + "START" | + "END" + } +} + +# 3.2.15 Relationship Type + +reltypeparam = element reltype { + element text { + "PARENT" | + "CHILD" | + "SIBLING" + } +} + +# 3.2.16 Participation Role + +roleparam = element role { + element text { + "CHAIR" | + "REQ-PARTICIPANT" | + "OPT-PARTICIPANT" | + "NON-PARTICIPANT" + } +} + +# 3.2.17 RSVP Expectation + +rsvpparam = element rsvp { + value-boolean +} + +# 3.2.18 Sent By + +sentbyparam = element sent-by { + value-cal-address +} + +# 3.2.19 Time Zone Identifier + +tzidparam = element tzid { + value-text +} + +# 3.3 Property Value Data Types + +# 3.3.1 BINARY + +value-binary = element binary { + xsd:string +} + +# 3.3.2 BOOLEAN + +value-boolean = element boolean { + xsd:boolean +} + +# 3.3.3 CAL-ADDRESS + +value-cal-address = element cal-address { + xsd:anyURI +} + +# 3.3.4 DATE + +pattern-date = xsd:string { + pattern = "\d\d\d\d-\d\d-\d\d" +} + +value-date = element date { + pattern-date +} + +# 3.3.5 DATE-TIME + +pattern-date-time = xsd:string { + pattern = "\d\d\d\d-\d\d-\d\dT\d\d:\d\d:\d\dZ?" +} + +value-date-time = element date-time { + pattern-date-time +} + +# 3.3.6 DURATION + +pattern-duration = xsd:string { + pattern = "(+|-)?P(\d+W)|(\d+D)?" + ~ "(T(\d+H(\d+M)?(\d+S)?)|" + ~ "(\d+M(\d+S)?)|" + ~ "(\d+S))?" +} + +value-duration = element duration { + pattern-duration +} + +# 3.3.7 FLOAT + +value-float = element float { + xsd:float +} + +# 3.3.8 INTEGER + +value-integer = element integer { + xsd:integer +} + +# 3.3.9 PERIOD + +value-period = element period { + element start { + pattern-date-time + }, + ( + element end { + pattern-date-time + } | + element duration { + pattern-duration + } + ) +} + +# 3.3.10 RECUR + +value-recur = element recur { + type-freq, + (type-until | type-count)?, + element interval { + xsd:positiveInteger + }?, + type-bysecond*, + type-byminute*, + type-byhour*, + type-byday*, + type-bymonthday*, + type-byyearday*, + type-byweekno*, + type-bymonth*, + type-bysetpos*, + element wkst { type-weekday }? +} + +type-freq = element freq { + "SECONDLY" | + "MINUTELY" | + "HOURLY" | + "DAILY" | + "WEEKLY" | + "MONTHLY" | + "YEARLY" +} + +type-until = element until { + type-date | + type-date-time +} + +type-count = element count { + xsd:positiveInteger +} + +type-bysecond = element bysecond { + xsd:nonNegativeInteger +} + +type-byminute = element byminute { + xsd:nonNegativeInteger +} + +type-byhour = element byhour { + xsd:nonNegativeInteger +} + +type-weekday = ( + "SU" | + "MO" | + "TU" | + "WE" | + "TH" | + "FR" | + "SA" +) + +type-byday = element byday { + xsd:integer?, + type-weekday +} + +type-bymonthday = element bymonthday { + xsd:integer +} + +type-byyearday = element byyearday { + xsd:integer +} + +type-byweekno = element byweekno { + xsd:integer +} + +type-bymonth = element bymonth { + xsd:positiveInteger +} + +type-bysetpos = element bysetpos { + xsd:integer +} + +# 3.3.11 TEXT + +value-text = element text { + xsd:string +} + +# 3.3.12 TIME + +pattern-time = xsd:string { + pattern = "\d\d:\d\d:\d\dZ?" +} + +value-time = element time { + pattern-time +} + +# 3.3.13 URI + +value-uri = element uri { + xsd:anyURI +} + +# 3.3.14 UTC-OFFSET + +value-utc-offset = element utc-offset { + xsd:string { pattern = "(+|-)\d\d:\d\d(:\d\d)?" } +} + +# UNKNOWN + +value-unknown = element unknown { + xsd:string +} + +# 3.4 iCalendar Stream + +start = element icalendar { + vcalendar+ +} + +# 3.6 Calendar Components + +vcalendar = element vcalendar { + type-calprops, + type-component +} + +type-calprops = element properties { + property-prodid & + property-version & + property-calscale? & + property-method? +} + +type-component = element components { + ( + component-vevent | + component-vtodo | + component-vjournal | + component-vfreebusy | + component-vtimezone + )* +} + +# 3.6.1 Event Component + +component-vevent = element vevent { + type-eventprop, + element components { + component-valarm+ + }? +} + +type-eventprop = element properties { + property-dtstamp & + property-dtstart & + property-uid & + + property-class? & + property-created? & + property-description? & + property-geo? & + property-last-mod? & + property-location? & + property-organizer? & + property-priority? & + property-seq? & + property-status-event? & + property-summary? & + property-transp? & + property-url? & + property-recurid? & + + property-rrule? & + + (property-dtend | property-duration)? & + + property-attach* & + property-attendee* & + property-categories* & + property-comment* & + property-contact* & + property-exdate* & + property-rstatus* & + property-related* & + property-resources* & + property-rdate* +} + +# 3.6.2 To-do Component + +component-vtodo = element vtodo { + type-todoprop, + element components { + component-valarm+ + }? +} + +type-todoprop = element properties { + property-dtstamp & + property-uid & + + property-class? & + property-completed? & + property-created? & + property-description? & + property-geo? & + property-last-mod? & + property-location? & + property-organizer? & + property-percent? & + property-priority? & + property-recurid? & + property-seq? & + property-status-todo? & + property-summary? & + property-url? & + + property-rrule? & + + ( + (property-dtstart?, property-dtend? ) | + (property-dtstart, property-duration)? + ) & + + property-attach* & + property-attendee* & + property-categories* & + property-comment* & + property-contact* & + property-exdate* & + property-rstatus* & + property-related* & + property-resources* & + property-rdate* +} + +# 3.6.3 Journal Component + +component-vjournal = element vjournal { + type-jourprop +} + +type-jourprop = element properties { + property-dtstamp & + property-uid & + + property-class? & + property-created? & + property-dtstart? & + property-last-mod? & + property-organizer? & + property-recurid? & + property-seq? & + property-status-jour? & + property-summary? & + property-url? & + + property-rrule? & + + property-attach* & + property-attendee* & + property-categories* & + property-comment* & + property-contact* & + property-description? & + property-exdate* & + property-related* & + property-rdate* & + property-rstatus* +} + +# 3.6.4 Free/Busy Component + +component-vfreebusy = element vfreebusy { + type-fbprop +} + +type-fbprop = element properties { + property-dtstamp & + property-uid & + + property-contact? & + property-dtstart? & + property-dtend? & + property-duration? & + property-organizer? & + property-url? & + + property-attendee* & + property-comment* & + property-freebusy* & + property-rstatus* +} + +# 3.6.5 Time Zone Component + +component-vtimezone = element vtimezone { + element properties { + property-tzid & + + property-last-mod? & + property-tzurl? + }, + element components { + (component-standard | component-daylight) & + component-standard* & + component-daylight* + } +} + +component-standard = element standard { + type-tzprop +} + +component-daylight = element daylight { + type-tzprop +} + +type-tzprop = element properties { + property-dtstart & + property-tzoffsetto & + property-tzoffsetfrom & + + property-rrule? & + + property-comment* & + property-rdate* & + property-tzname* +} + +# 3.6.6 Alarm Component + +component-valarm = element valarm { + type-audioprop | type-dispprop | type-emailprop +} + +type-audioprop = element properties { + property-action & + + property-trigger & + + (property-duration, property-repeat)? & + + property-attach? +} + +type-emailprop = element properties { + property-action & + property-description & + property-trigger & + property-summary & + + property-attendee+ & + + (property-duration, property-repeat)? & + + property-attach* +} + +type-dispprop = element properties { + property-action & + property-description & + property-trigger & + + (property-duration, property-repeat)? +} + +# 3.7 Calendar Properties + +# 3.7.1 Calendar Scale + +property-calscale = element calscale { + + element parameters { empty }?, + + element text { "GREGORIAN" } +} + +# 3.7.2 Method + +property-method = element method { + + element parameters { empty }?, + + value-text +} + +# 3.7.3 Product Identifier + +property-prodid = element prodid { + + element parameters { empty }?, + + value-text +} + +# 3.7.4 Version + +property-version = element version { + + element parameters { empty }?, + + element text { "2.0" } +} + +# 3.8 Component Properties + +# 3.8.1 Descriptive Component Properties + +# 3.8.1.1 Attachment + +property-attach = element attach { + + element parameters { + fmttypeparam? & + encodingparam? + }?, + + value-uri | value-binary +} + +# 3.8.1.2 Categories + +property-categories = element categories { + + element parameters { + languageparam? & + }?, + + value-text+ +} + +# 3.8.1.3 Classification + +property-class = element class { + + element parameters { empty }?, + + element text { + "PUBLIC" | + "PRIVATE" | + "CONFIDENTIAL" + } +} + +# 3.8.1.4 Comment + +property-comment = element comment { + + element parameters { + altrepparam? & + languageparam? + }?, + + value-text +} + +# 3.8.1.5 Description + +property-description = element description { + + element parameters { + altrepparam? & + languageparam? + }?, + + value-text +} + +# 3.8.1.6 Geographic Position + +property-geo = element geo { + + element parameters { empty }?, + + element latitude { xsd:float }, + element longitude { xsd:float } +} + +# 3.8.1.7 Location + +property-location = element location { + + element parameters { + + altrepparam? & + languageparam? + }?, + + value-text +} + +# 3.8.1.8 Percent Complete + +property-percent = element percent-complete { + + element parameters { empty }?, + + value-integer +} + +# 3.8.1.9 Priority + +property-priority = element priority { + + element parameters { empty }?, + + value-integer +} + +# 3.8.1.10 Resources + +property-resources = element resources { + + element parameters { + altrepparam? & + languageparam? + }?, + + value-text+ +} + +# 3.8.1.11 Status + +property-status-event = element status { + + element parameters { empty }?, + + element text { + "TENTATIVE" | + "CONFIRMED" | + "CANCELLED" + } +} + +property-status-todo = element status { + + element parameters { empty }?, + + element text { + "NEEDS-ACTION" | + "COMPLETED" | + "IN-PROCESS" | + "CANCELLED" + } +} + +property-status-jour = element status { + + element parameters { empty }?, + + element text { + "DRAFT" | + "FINAL" | + "CANCELLED" + } +} + +# 3.8.1.12 Summary + +property-summary = element summary { + + element parameters { + altrepparam? & + languageparam? + }?, + + value-text +} + +# 3.8.2 Date and Time Component Properties + +# 3.8.2.1 Date/Time Completed + +property-completed = element completed { + + element parameters { empty }?, + + value-date-time +} + +# 3.8.2.2 Date/Time End + +property-dtend = element dtend { + + element parameters { + tzidparam? + }?, + + value-date-time | + value-date +} + +# 3.8.2.3 Date/Time Due + +property-due = element due { + + element parameters { + tzidparam? + }?, + + value-date-time | + value-date +} + +# 3.8.2.4 Date/Time Start + +property-dtstart = element dtstart { + + element parameters { + tzidparam? + }?, + + value-date-time | + value-date +} + +# 3.8.2.5 Duration + +property-duration = element duration { + + element parameters { empty }?, + + value-duration +} + +# 3.8.2.6 Free/Busy Time + +property-freebusy = element freebusy { + + element parameters { + fbtypeparam? + }?, + + + value-period+ +} + +# 3.8.2.7 Time Transparency + +property-transp = element transp { + + element parameters { empty }?, + + element text { + "OPAQUE" | + "TRANSPARENT" + } +} + +# 3.8.3 Time Zone Component Properties + +# 3.8.3.1 Time Zone Identifier + +property-tzid = element tzid { + + element parameters { empty }?, + + value-text +} + +# 3.8.3.2 Time Zone Name + +property-tzname = element tzname { + + element parameters { + languageparam? + }?, + + value-text +} + +# 3.8.3.3 Time Zone Offset From + +property-tzoffsetfrom = element tzoffsetfrom { + + element parameters { empty }?, + + value-utc-offset +} + +# 3.8.3.4 Time Zone Offset To + +property-tzoffsetto = element tzoffsetto { + + element parameters { empty }?, + + value-utc-offset +} + +# 3.8.3.5 Time Zone URL + +property-tzurl = element tzurl { + + element parameters { empty }?, + + value-uri +} + +# 3.8.4 Relationship Component Properties + +# 3.8.4.1 Attendee + +property-attendee = element attendee { + + element parameters { + cutypeparam? & + memberparam? & + roleparam? & + partstatparam? & + rsvpparam? & + deltoparam? & + delfromparam? & + sentbyparam? & + cnparam? & + dirparam? & + languageparam? + }?, + + value-cal-address +} + +# 3.8.4.2 Contact + +property-contact = element contact { + + element parameters { + altrepparam? & + languageparam? + }?, + + value-text +} + +# 3.8.4.3 Organizer + +property-organizer = element organizer { + + element parameters { + cnparam? & + dirparam? & + sentbyparam? & + languageparam? + }?, + + value-cal-address +} + +# 3.8.4.4 Recurrence ID + +property-recurid = element recurrence-id { + + element parameters { + tzidparam? & + rangeparam? + }?, + + value-date-time | + value-date +} + +# 3.8.4.5 Related-To + +property-related = element related-to { + + element parameters { + reltypeparam? + }?, + + value-text +} + +# 3.8.4.6 Uniform Resource Locator + +property-url = element url { + + element parameters { empty }?, + + value-uri +} + +# 3.8.4.7 Unique Identifier + +property-uid = element uid { + + element parameters { empty }?, + + value-text +} + +# 3.8.5 Recurrence Component Properties + +# 3.8.5.1 Exception Date/Times + +property-exdate = element exdate { + + element parameters { + tzidparam? + }?, + + value-date-time+ | + value-date+ +} + +# 3.8.5.2 Recurrence Date/Times + +property-rdate = element rdate { + + element parameters { + tzidparam? + }?, + + value-date-time+ | + value-date+ | + value-period+ +} + +# 3.8.5.3 Recurrence Rule + +property-rrule = element rrule { + + element parameters { empty }?, + + value-recur +} + +# 3.8.6 Alarm Component Properties + +# 3.8.6.1 Action + +property-action = element action { + + element parameters { empty }?, + + element text { + "AUDIO" | + "DISPLAY" | + "EMAIL" + } +} + +# 3.8.6.2 Repeat Count + +property-repeat = element repeat { + + element parameters { empty }?, + + value-integer +} + +# 3.8.6.3 Trigger + +property-trigger = element trigger { + + ( + element parameters { + trigrelparam? + }?, + + value-duration + ) | + ( + element parameters { empty }?, + + value-date-time + ) +} + +# 3.8.7 Change Management Component Properties + +# 3.8.7.1 Date/Time Created + +property-created = element created { + + element parameters { empty }?, + + value-date-time +} + +# 3.8.7.2 Date/Time Stamp + +property-dtstamp = element dtstamp { + + element parameters { empty }?, + + value-date-time +} + +# 3.8.7.3 Last Modified + +property-last-mod = element last-modified { + + element parameters { empty }?, + + value-date-time +} + +# 3.8.7.4 Sequence Number + +property-seq = element sequence { + + element parameters { empty }?, + + value-integer +} + +# 3.8.8 Miscellaneous Component Properties + +# 3.8.8.3 Request Status + +property-rstatus = element request-status { + + element parameters { + languageparam? + }?, + + element code { xsd:string }, + element description { xsd:string }, + element data { xsd:string }? +} diff --git a/3rdparty/sabre/vobject/resources/schema/xcard.rng b/3rdparty/sabre/vobject/resources/schema/xcard.rng new file mode 100644 index 00000000..c0b7cfb3 --- /dev/null +++ b/3rdparty/sabre/vobject/resources/schema/xcard.rng @@ -0,0 +1,388 @@ +# RELAX NG Schema for vCard in XML +# Extract from RFC6351. +# Erratum 2994 applied. +# Erratum 3047 applied. +# Erratum 3008 applied. +# Erratum 4247 applied. + +default namespace = "urn:ietf:params:xml:ns:vcard-4.0" + +### Section 3.3: vCard Format Specification +# +# 3.3 +iana-token = xsd:string { pattern = "[a-zA-Z0-9\-]+" } +x-name = xsd:string { pattern = "x-[a-zA-Z0-9\-]+" } + +### Section 4: Value types +# +# 4.1 +value-text = element text { text } +value-text-list = value-text+ + +# 4.2 +value-uri = element uri { xsd:anyURI } + +# 4.3.1 +value-date = element date { + xsd:string { pattern = "\d{8}|\d{4}-\d\d|--\d\d(\d\d)?|---\d\d" } + } + +# 4.3.2 +value-time = element time { + xsd:string { pattern = "(\d\d(\d\d(\d\d)?)?|-\d\d(\d\d)?|--\d\d)" + ~ "(Z|[+\-]\d\d(\d\d)?)?" } + } + +# 4.3.3 +value-date-time = element date-time { + xsd:string { pattern = "(\d{8}|--\d{4}|---\d\d)T\d\d(\d\d(\d\d)?)?" + ~ "(Z|[+\-]\d\d(\d\d)?)?" } + } + +# 4.3.4 +value-date-and-or-time = value-date | value-date-time | value-time + +# 4.3.5 +value-timestamp = element timestamp { + xsd:string { pattern = "\d{8}T\d{6}(Z|[+\-]\d\d(\d\d)?)?" } + } + +# 4.4 +value-boolean = element boolean { xsd:boolean } + +# 4.5 +value-integer = element integer { xsd:integer } + +# 4.6 +value-float = element float { xsd:float } + +# 4.7 +value-utc-offset = element utc-offset { + xsd:string { pattern = "[+\-]\d\d(\d\d)?" } + } + +# 4.8 +value-language-tag = element language-tag { + xsd:string { pattern = "([a-z]{2,3}((-[a-z]{3}){0,3})?|[a-z]{4,8})" + ~ "(-[a-z]{4})?(-([a-z]{2}|\d{3}))?" + ~ "(-([0-9a-z]{5,8}|\d[0-9a-z]{3}))*" + ~ "(-[0-9a-wyz](-[0-9a-z]{2,8})+)*" + ~ "(-x(-[0-9a-z]{1,8})+)?|x(-[0-9a-z]{1,8})+|" + ~ "[a-z]{1,3}(-[0-9a-z]{2,8}){1,2}" } + } + +### Section 5: Parameters +# +# 5.1 +param-language = element language { value-language-tag }? + +# 5.2 +param-pref = element pref { + element integer { + xsd:integer { minInclusive = "1" maxInclusive = "100" } + } + }? + +# 5.4 +param-altid = element altid { value-text }? + +# 5.5 +param-pid = element pid { + element text { xsd:string { pattern = "\d+(\.\d+)?" } }+ + }? + +# 5.6 +param-type = element type { element text { "work" | "home" }+ }? + +# 5.7 +param-mediatype = element mediatype { value-text }? + +# 5.8 +param-calscale = element calscale { element text { "gregorian" } }? + +# 5.9 +param-sort-as = element sort-as { value-text+ }? + +# 5.10 +param-geo = element geo { value-uri }? + +# 5.11 +param-tz = element tz { value-text | value-uri }? + +### Section 6: Properties +# +# 6.1.3 +property-source = element source { + element parameters { param-altid, param-pid, param-pref, + param-mediatype }?, + value-uri + } + +# 6.1.4 +property-kind = element kind { + element text { "individual" | "group" | "org" | "location" | + x-name | iana-token }* + } + +# 6.2.1 +property-fn = element fn { + element parameters { param-language, param-altid, param-pid, + param-pref, param-type }?, + value-text + } + +# 6.2.2 +property-n = element n { + element parameters { param-language, param-sort-as, param-altid }?, + element surname { text }+, + element given { text }+, + element additional { text }+, + element prefix { text }+, + element suffix { text }+ + } + +# 6.2.3 +property-nickname = element nickname { + element parameters { param-language, param-altid, param-pid, + param-pref, param-type }?, + value-text-list + } + +# 6.2.4 +property-photo = element photo { + element parameters { param-altid, param-pid, param-pref, param-type, + param-mediatype }?, + value-uri + } + +# 6.2.5 +property-bday = element bday { + element parameters { param-altid, param-calscale }?, + (value-date-and-or-time | value-text) + } + +# 6.2.6 +property-anniversary = element anniversary { + element parameters { param-altid, param-calscale }?, + (value-date-and-or-time | value-text) + } + +# 6.2.7 +property-gender = element gender { + element sex { "" | "M" | "F" | "O" | "N" | "U" }, + element identity { text }? + } + +# 6.3.1 +param-label = element label { value-text }? +property-adr = element adr { + element parameters { param-language, param-altid, param-pid, + param-pref, param-type, param-geo, param-tz, + param-label }?, + element pobox { text }+, + element ext { text }+, + element street { text }+, + element locality { text }+, + element region { text }+, + element code { text }+, + element country { text }+ + } + +# 6.4.1 +property-tel = element tel { + element parameters { + param-altid, + param-pid, + param-pref, + element type { + element text { "work" | "home" | "text" | "voice" + | "fax" | "cell" | "video" | "pager" + | "textphone" | x-name | iana-token }+ + }?, + param-mediatype + }?, + (value-text | value-uri) + } + +# 6.4.2 +property-email = element email { + element parameters { param-altid, param-pid, param-pref, + param-type }?, + value-text + } + +# 6.4.3 +property-impp = element impp { + element parameters { param-altid, param-pid, param-pref, + param-type, param-mediatype }?, + value-uri + } + +# 6.4.4 +property-lang = element lang { + element parameters { param-altid, param-pid, param-pref, + param-type }?, + value-language-tag + } + +# 6.5.1 +property-tz = element tz { + element parameters { param-altid, param-pid, param-pref, + param-type, param-mediatype }?, + (value-text | value-uri | value-utc-offset) + } + +# 6.5.2 +property-geo = element geo { + element parameters { param-altid, param-pid, param-pref, + param-type, param-mediatype }?, + value-uri + } + +# 6.6.1 +property-title = element title { + element parameters { param-language, param-altid, param-pid, + param-pref, param-type }?, + value-text + } + +# 6.6.2 +property-role = element role { + element parameters { param-language, param-altid, param-pid, + param-pref, param-type }?, + value-text + } + +# 6.6.3 +property-logo = element logo { + element parameters { param-language, param-altid, param-pid, + param-pref, param-type, param-mediatype }?, + value-uri + } + +# 6.6.4 +property-org = element org { + element parameters { param-language, param-altid, param-pid, + param-pref, param-type, param-sort-as }?, + value-text-list + } + +# 6.6.5 +property-member = element member { + element parameters { param-altid, param-pid, param-pref, + param-mediatype }?, + value-uri + } + +# 6.6.6 +property-related = element related { + element parameters { + param-altid, + param-pid, + param-pref, + element type { + element text { + "work" | "home" | "contact" | "acquaintance" | + "friend" | "met" | "co-worker" | "colleague" | "co-resident" | + "neighbor" | "child" | "parent" | "sibling" | "spouse" | + "kin" | "muse" | "crush" | "date" | "sweetheart" | "me" | + "agent" | "emergency" + }+ + }?, + param-mediatype + }?, + (value-uri | value-text) + } + +# 6.7.1 +property-categories = element categories { + element parameters { param-altid, param-pid, param-pref, + param-type }?, + value-text-list + } + +# 6.7.2 +property-note = element note { + element parameters { param-language, param-altid, param-pid, + param-pref, param-type }?, + value-text + } + +# 6.7.3 +property-prodid = element prodid { value-text } + +# 6.7.4 +property-rev = element rev { value-timestamp } + +# 6.7.5 +property-sound = element sound { + element parameters { param-language, param-altid, param-pid, + param-pref, param-type, param-mediatype }?, + value-uri + } + +# 6.7.6 +property-uid = element uid { value-uri } + +# 6.7.7 +property-clientpidmap = element clientpidmap { + element sourceid { xsd:positiveInteger }, + value-uri + } + +# 6.7.8 +property-url = element url { + element parameters { param-altid, param-pid, param-pref, + param-type, param-mediatype }?, + value-uri + } + +# 6.8.1 +property-key = element key { + element parameters { param-altid, param-pid, param-pref, + param-type, param-mediatype }?, + (value-uri | value-text) + } + +# 6.9.1 +property-fburl = element fburl { + element parameters { param-altid, param-pid, param-pref, + param-type, param-mediatype }?, + value-uri + } + +# 6.9.2 +property-caladruri = element caladruri { + element parameters { param-altid, param-pid, param-pref, + param-type, param-mediatype }?, + value-uri + } + +# 6.9.3 +property-caluri = element caluri { + element parameters { param-altid, param-pid, param-pref, + param-type, param-mediatype }?, + value-uri + } + +# Top-level grammar +property = property-adr | property-anniversary | property-bday + | property-caladruri | property-caluri | property-categories + | property-clientpidmap | property-email | property-fburl + | property-fn | property-geo | property-impp | property-key + | property-kind | property-lang | property-logo + | property-member | property-n | property-nickname + | property-note | property-org | property-photo + | property-prodid | property-related | property-rev + | property-role | property-gender | property-sound + | property-source | property-tel | property-title + | property-tz | property-uid | property-url +start = element vcards { + element vcard { + (property + | element group { + attribute name { text }, + property* + })+ + }+ + } diff --git a/3rdparty/sabre/xml/LICENSE b/3rdparty/sabre/xml/LICENSE new file mode 100644 index 00000000..c9faf409 --- /dev/null +++ b/3rdparty/sabre/xml/LICENSE @@ -0,0 +1,27 @@ +Copyright (C) 2009-2015 fruux GmbH (https://fruux.com/) + +All rights reserved. + +Redistribution and use in source and binary forms, with or without modification, +are permitted provided that the following conditions are met: + + * Redistributions of source code must retain the above copyright notice, + this list of conditions and the following disclaimer. + * Redistributions in binary form must reproduce the above copyright notice, + this list of conditions and the following disclaimer in the documentation + and/or other materials provided with the distribution. + * Neither the name Sabre nor the names of its contributors + may be used to endorse or promote products derived from this software + without specific prior written permission. + + THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" + AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE + IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE + ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE + LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR + CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF + SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS + INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN + CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) + ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE + POSSIBILITY OF SUCH DAMAGE. diff --git a/3rdparty/sabre/xml/lib/ContextStackTrait.php b/3rdparty/sabre/xml/lib/ContextStackTrait.php new file mode 100644 index 00000000..4e15bd41 --- /dev/null +++ b/3rdparty/sabre/xml/lib/ContextStackTrait.php @@ -0,0 +1,118 @@ +contextStack[] = [ + $this->elementMap, + $this->contextUri, + $this->namespaceMap, + $this->classMap, + ]; + } + + /** + * Restore the previous "context". + */ + public function popContext() + { + list( + $this->elementMap, + $this->contextUri, + $this->namespaceMap, + $this->classMap + ) = array_pop($this->contextStack); + } +} diff --git a/3rdparty/sabre/xml/lib/Deserializer/functions.php b/3rdparty/sabre/xml/lib/Deserializer/functions.php new file mode 100644 index 00000000..50818098 --- /dev/null +++ b/3rdparty/sabre/xml/lib/Deserializer/functions.php @@ -0,0 +1,360 @@ +value" array. + * + * For example, keyvalue will parse: + * + * + * + * value1 + * value2 + * + * + * + * Into: + * + * [ + * "{http://sabredav.org/ns}elem1" => "value1", + * "{http://sabredav.org/ns}elem2" => "value2", + * "{http://sabredav.org/ns}elem3" => null, + * ]; + * + * If you specify the 'namespace' argument, the deserializer will remove + * the namespaces of the keys that match that namespace. + * + * For example, if you call keyValue like this: + * + * keyValue($reader, 'http://sabredav.org/ns') + * + * it's output will instead be: + * + * [ + * "elem1" => "value1", + * "elem2" => "value2", + * "elem3" => null, + * ]; + * + * Attributes will be removed from the top-level elements. If elements with + * the same name appear twice in the list, only the last one will be kept. + */ +function keyValue(Reader $reader, ?string $namespace = null): array +{ + // If there's no children, we don't do anything. + if ($reader->isEmptyElement) { + $reader->next(); + + return []; + } + + if (!$reader->read()) { + $reader->next(); + + return []; + } + + if (Reader::END_ELEMENT === $reader->nodeType) { + $reader->next(); + + return []; + } + + $values = []; + + do { + if (Reader::ELEMENT === $reader->nodeType) { + if (null !== $namespace && $reader->namespaceURI === $namespace) { + $values[$reader->localName] = $reader->parseCurrentElement()['value']; + } else { + $clark = $reader->getClark(); + $values[$clark] = $reader->parseCurrentElement()['value']; + } + } else { + if (!$reader->read()) { + break; + } + } + } while (Reader::END_ELEMENT !== $reader->nodeType); + + $reader->read(); + + return $values; +} + +/** + * The 'enum' deserializer parses elements into a simple list + * without values or attributes. + * + * For example, Elements will parse: + * + * + * + * + * + * + * content + * + * + * + * Into: + * + * [ + * "{http://sabredav.org/ns}elem1", + * "{http://sabredav.org/ns}elem2", + * "{http://sabredav.org/ns}elem3", + * "{http://sabredav.org/ns}elem4", + * "{http://sabredav.org/ns}elem5", + * ]; + * + * This is useful for 'enum'-like structures. + * + * If the $namespace argument is specified, it will strip the namespace + * for all elements that match that. + * + * For example, + * + * enum($reader, 'http://sabredav.org/ns') + * + * would return: + * + * [ + * "elem1", + * "elem2", + * "elem3", + * "elem4", + * "elem5", + * ]; + * + * @return string[] + */ +function enum(Reader $reader, ?string $namespace = null): array +{ + // If there's no children, we don't do anything. + if ($reader->isEmptyElement) { + $reader->next(); + + return []; + } + if (!$reader->read()) { + $reader->next(); + + return []; + } + + if (Reader::END_ELEMENT === $reader->nodeType) { + $reader->next(); + + return []; + } + $currentDepth = $reader->depth; + + $values = []; + do { + if (Reader::ELEMENT !== $reader->nodeType) { + continue; + } + if (!is_null($namespace) && $namespace === $reader->namespaceURI) { + $values[] = $reader->localName; + } else { + $values[] = (string) $reader->getClark(); + } + } while ($reader->depth >= $currentDepth && $reader->next()); + + $reader->next(); + + return $values; +} + +/** + * The valueObject deserializer turns an xml element into a PHP object of + * a specific class. + * + * This is primarily used by the mapValueObject function from the Service + * class, but it can also easily be used for more specific situations. + * + * @return object + */ +function valueObject(Reader $reader, string $className, string $namespace) +{ + $valueObject = new $className(); + if ($reader->isEmptyElement) { + $reader->next(); + + return $valueObject; + } + + $defaultProperties = get_class_vars($className); + + $reader->read(); + do { + if (Reader::ELEMENT === $reader->nodeType && $reader->namespaceURI == $namespace) { + if (property_exists($valueObject, $reader->localName)) { + if (is_array($defaultProperties[$reader->localName])) { + $valueObject->{$reader->localName}[] = $reader->parseCurrentElement()['value']; + } else { + $valueObject->{$reader->localName} = $reader->parseCurrentElement()['value']; + } + } else { + // Ignore property + $reader->next(); + } + } elseif (Reader::ELEMENT === $reader->nodeType) { + // Skipping element from different namespace + $reader->next(); + } else { + if (Reader::END_ELEMENT !== $reader->nodeType && !$reader->read()) { + break; + } + } + } while (Reader::END_ELEMENT !== $reader->nodeType); + + $reader->read(); + + return $valueObject; +} + +/** + * This deserializer helps you deserialize xml structures that look like + * this:. + * + * + * ... + * ... + * ... + * + * + * Many XML documents use patterns like that, and this deserializer + * allow you to get all the 'items' as an array. + * + * In that previous example, you would register the deserializer as such: + * + * $reader->elementMap['{}collection'] = function($reader) { + * return repeatingElements($reader, '{}item'); + * } + * + * The repeatingElements deserializer simply returns everything as an array. + * + * $childElementName must either be a a clark-notation element name, or if no + * namespace is used, the bare element name. + */ +function repeatingElements(Reader $reader, string $childElementName): array +{ + if ('{' !== $childElementName[0]) { + $childElementName = '{}'.$childElementName; + } + $result = []; + + foreach ($reader->parseGetElements() as $element) { + if ($element['name'] === $childElementName) { + $result[] = $element['value']; + } + } + + return $result; +} + +/** + * This deserializer helps you to deserialize structures which contain mixed content like this:. + * + *

    some text and a inline tagand even more text

    + * + * The above example will return + * + * [ + * 'some text', + * [ + * 'name' => '{}extref', + * 'value' => 'and a inline tag', + * 'attributes' => [] + * ], + * 'and even more text' + * ] + * + * In strict XML documents you wont find this kind of markup but in html this is a quite common pattern. + */ +function mixedContent(Reader $reader): array +{ + // If there's no children, we don't do anything. + if ($reader->isEmptyElement) { + $reader->next(); + + return []; + } + + $previousDepth = $reader->depth; + + $content = []; + $reader->read(); + while (true) { + if (Reader::ELEMENT == $reader->nodeType) { + $content[] = $reader->parseCurrentElement(); + } elseif ($reader->depth >= $previousDepth && in_array($reader->nodeType, [Reader::TEXT, Reader::CDATA, Reader::WHITESPACE])) { + $content[] = $reader->value; + $reader->read(); + } elseif (Reader::END_ELEMENT == $reader->nodeType) { + // Ensuring we are moving the cursor after the end element. + $reader->read(); + break; + } else { + $reader->read(); + } + } + + return $content; +} + +/** + * The functionCaller deserializer turns an xml element into whatever your callable return. + * + * You can use, e.g., a named constructor (factory method) to create an object using + * this function. + */ +function functionCaller(Reader $reader, callable $func, string $namespace) +{ + if ($reader->isEmptyElement) { + $reader->next(); + + return null; + } + + $funcArgs = []; + $func = is_string($func) && false !== strpos($func, '::') ? explode('::', $func) : $func; + $ref = is_array($func) ? new \ReflectionMethod($func[0], $func[1]) : new \ReflectionFunction($func); + foreach ($ref->getParameters() as $parameter) { + $funcArgs[$parameter->getName()] = null; + } + + $reader->read(); + do { + if (Reader::ELEMENT === $reader->nodeType && $reader->namespaceURI == $namespace) { + if (array_key_exists($reader->localName, $funcArgs)) { + $funcArgs[$reader->localName] = $reader->parseCurrentElement()['value']; + } else { + // Ignore property + $reader->next(); + } + } else { + $reader->read(); + } + } while (Reader::END_ELEMENT !== $reader->nodeType); + $reader->read(); + + return $func(...array_values($funcArgs)); +} diff --git a/3rdparty/sabre/xml/lib/Element.php b/3rdparty/sabre/xml/lib/Element.php new file mode 100644 index 00000000..559eb54e --- /dev/null +++ b/3rdparty/sabre/xml/lib/Element.php @@ -0,0 +1,22 @@ +value = $value; + } + + /** + * The xmlSerialize method is called during xml writing. + * + * Use the $writer argument to write its own xml serialization. + * + * An important note: do _not_ create a parent element. Any element + * implementing XmlSerializable should only ever write what's considered + * its 'inner xml'. + * + * The parent of the current element is responsible for writing a + * containing element. + * + * This allows serializers to be re-used for different element names. + * + * If you are opening new elements, you must also close them again. + */ + public function xmlSerialize(Xml\Writer $writer) + { + $writer->write($this->value); + } + + /** + * The deserialize method is called during xml parsing. + * + * This method is called statically, this is because in theory this method + * may be used as a type of constructor, or factory method. + * + * Often you want to return an instance of the current class, but you are + * free to return other data as well. + * + * Important note 2: You are responsible for advancing the reader to the + * next element. Not doing anything will result in a never-ending loop. + * + * If you just want to skip parsing for this element altogether, you can + * just call $reader->next(); + * + * $reader->parseInnerTree() will parse the entire sub-tree, and advance to + * the next element. + */ + public static function xmlDeserialize(Xml\Reader $reader) + { + $subTree = $reader->parseInnerTree(); + + return $subTree; + } +} diff --git a/3rdparty/sabre/xml/lib/Element/Cdata.php b/3rdparty/sabre/xml/lib/Element/Cdata.php new file mode 100644 index 00000000..1367343f --- /dev/null +++ b/3rdparty/sabre/xml/lib/Element/Cdata.php @@ -0,0 +1,59 @@ +value = $value; + } + + /** + * The xmlSerialize method is called during xml writing. + * + * Use the $writer argument to write its own xml serialization. + * + * An important note: do _not_ create a parent element. Any element + * implementing XmlSerializable should only ever write what's considered + * its 'inner xml'. + * + * The parent of the current element is responsible for writing a + * containing element. + * + * This allows serializers to be re-used for different element names. + * + * If you are opening new elements, you must also close them again. + */ + public function xmlSerialize(Xml\Writer $writer) + { + $writer->writeCData($this->value); + } +} diff --git a/3rdparty/sabre/xml/lib/Element/Elements.php b/3rdparty/sabre/xml/lib/Element/Elements.php new file mode 100644 index 00000000..6915fd46 --- /dev/null +++ b/3rdparty/sabre/xml/lib/Element/Elements.php @@ -0,0 +1,98 @@ + + * + * + * + * + * content + * + * + * + * Into: + * + * [ + * "{http://sabredav.org/ns}elem1", + * "{http://sabredav.org/ns}elem2", + * "{http://sabredav.org/ns}elem3", + * "{http://sabredav.org/ns}elem4", + * "{http://sabredav.org/ns}elem5", + * ]; + * + * @copyright Copyright (C) 2009-2015 fruux GmbH (https://fruux.com/). + * @author Evert Pot (http://evertpot.com/) + * @license http://sabre.io/license/ Modified BSD License + */ +class Elements implements Xml\Element +{ + /** + * Value to serialize. + * + * @var array + */ + protected $value; + + /** + * Constructor. + */ + public function __construct(array $value = []) + { + $this->value = $value; + } + + /** + * The xmlSerialize method is called during xml writing. + * + * Use the $writer argument to write its own xml serialization. + * + * An important note: do _not_ create a parent element. Any element + * implementing XmlSerializable should only ever write what's considered + * its 'inner xml'. + * + * The parent of the current element is responsible for writing a + * containing element. + * + * This allows serializers to be re-used for different element names. + * + * If you are opening new elements, you must also close them again. + */ + public function xmlSerialize(Xml\Writer $writer) + { + Serializer\enum($writer, $this->value); + } + + /** + * The deserialize method is called during xml parsing. + * + * This method is called statically, this is because in theory this method + * may be used as a type of constructor, or factory method. + * + * Often you want to return an instance of the current class, but you are + * free to return other data as well. + * + * Important note 2: You are responsible for advancing the reader to the + * next element. Not doing anything will result in a never-ending loop. + * + * If you just want to skip parsing for this element altogether, you can + * just call $reader->next(); + * + * $reader->parseSubTree() will parse the entire sub-tree, and advance to + * the next element. + */ + public static function xmlDeserialize(Xml\Reader $reader) + { + return Deserializer\enum($reader); + } +} diff --git a/3rdparty/sabre/xml/lib/Element/KeyValue.php b/3rdparty/sabre/xml/lib/Element/KeyValue.php new file mode 100644 index 00000000..7d75a3ac --- /dev/null +++ b/3rdparty/sabre/xml/lib/Element/KeyValue.php @@ -0,0 +1,98 @@ +value struct. + * + * Attributes will be removed, and duplicate child elements are discarded. + * Complex values within the elements will be parsed by the 'standard' parser. + * + * For example, KeyValue will parse: + * + * + * + * value1 + * value2 + * + * + * + * Into: + * + * [ + * "{http://sabredav.org/ns}elem1" => "value1", + * "{http://sabredav.org/ns}elem2" => "value2", + * "{http://sabredav.org/ns}elem3" => null, + * ]; + * + * @copyright Copyright (C) 2009-2015 fruux GmbH (https://fruux.com/). + * @author Evert Pot (http://evertpot.com/) + * @license http://sabre.io/license/ Modified BSD License + */ +class KeyValue implements Xml\Element +{ + /** + * Value to serialize. + * + * @var array + */ + protected $value; + + /** + * Constructor. + */ + public function __construct(array $value = []) + { + $this->value = $value; + } + + /** + * The xmlSerialize method is called during xml writing. + * + * Use the $writer argument to write its own xml serialization. + * + * An important note: do _not_ create a parent element. Any element + * implementing XmlSerializable should only ever write what's considered + * its 'inner xml'. + * + * The parent of the current element is responsible for writing a + * containing element. + * + * This allows serializers to be re-used for different element names. + * + * If you are opening new elements, you must also close them again. + */ + public function xmlSerialize(Xml\Writer $writer) + { + $writer->write($this->value); + } + + /** + * The deserialize method is called during xml parsing. + * + * This method is called statically, this is because in theory this method + * may be used as a type of constructor, or factory method. + * + * Often you want to return an instance of the current class, but you are + * free to return other data as well. + * + * Important note 2: You are responsible for advancing the reader to the + * next element. Not doing anything will result in a never-ending loop. + * + * If you just want to skip parsing for this element altogether, you can + * just call $reader->next(); + * + * $reader->parseInnerTree() will parse the entire sub-tree, and advance to + * the next element. + */ + public static function xmlDeserialize(Xml\Reader $reader) + { + return Deserializer\keyValue($reader); + } +} diff --git a/3rdparty/sabre/xml/lib/Element/Uri.php b/3rdparty/sabre/xml/lib/Element/Uri.php new file mode 100644 index 00000000..65276380 --- /dev/null +++ b/3rdparty/sabre/xml/lib/Element/Uri.php @@ -0,0 +1,97 @@ +/foo/bar + * http://example.org/hi + * + * If the uri is relative, it will be automatically expanded to an absolute + * url during writing and reading, if the contextUri property is set on the + * reader and/or writer. + * + * @copyright Copyright (C) 2009-2015 fruux GmbH (https://fruux.com/). + * @author Evert Pot (http://evertpot.com/) + * @license http://sabre.io/license/ Modified BSD License + */ +class Uri implements Xml\Element +{ + /** + * Uri element value. + * + * @var string + */ + protected $value; + + /** + * Constructor. + * + * @param string $value + */ + public function __construct($value) + { + $this->value = $value; + } + + /** + * The xmlSerialize method is called during xml writing. + * + * Use the $writer argument to write its own xml serialization. + * + * An important note: do _not_ create a parent element. Any element + * implementing XmlSerializable should only ever write what's considered + * its 'inner xml'. + * + * The parent of the current element is responsible for writing a + * containing element. + * + * This allows serializers to be re-used for different element names. + * + * If you are opening new elements, you must also close them again. + */ + public function xmlSerialize(Xml\Writer $writer) + { + $writer->text( + \Sabre\Uri\resolve( + $writer->contextUri, + $this->value + ) + ); + } + + /** + * This method is called during xml parsing. + * + * This method is called statically, this is because in theory this method + * may be used as a type of constructor, or factory method. + * + * Often you want to return an instance of the current class, but you are + * free to return other data as well. + * + * Important note 2: You are responsible for advancing the reader to the + * next element. Not doing anything will result in a never-ending loop. + * + * If you just want to skip parsing for this element altogether, you can + * just call $reader->next(); + * + * $reader->parseSubTree() will parse the entire sub-tree, and advance to + * the next element. + */ + public static function xmlDeserialize(Xml\Reader $reader) + { + return new self( + \Sabre\Uri\resolve( + (string) $reader->contextUri, + $reader->readText() + ) + ); + } +} diff --git a/3rdparty/sabre/xml/lib/Element/XmlFragment.php b/3rdparty/sabre/xml/lib/Element/XmlFragment.php new file mode 100644 index 00000000..99d1f87f --- /dev/null +++ b/3rdparty/sabre/xml/lib/Element/XmlFragment.php @@ -0,0 +1,146 @@ +xml = $xml; + } + + /** + * Returns the inner XML document. + */ + public function getXml(): string + { + return $this->xml; + } + + /** + * The xmlSerialize method is called during xml writing. + * + * Use the $writer argument to write its own xml serialization. + * + * An important note: do _not_ create a parent element. Any element + * implementing XmlSerializable should only ever write what's considered + * its 'inner xml'. + * + * The parent of the current element is responsible for writing a + * containing element. + * + * This allows serializers to be re-used for different element names. + * + * If you are opening new elements, you must also close them again. + */ + public function xmlSerialize(Writer $writer) + { + $reader = new Reader(); + + // Wrapping the xml in a container, so root-less values can still be + // parsed. + $xml = << +{$this->getXml()} +XML; + + $reader->xml($xml); + + while ($reader->read()) { + if ($reader->depth < 1) { + // Skipping the root node. + continue; + } + + switch ($reader->nodeType) { + case Reader::ELEMENT: + $writer->startElement( + (string) $reader->getClark() + ); + $empty = $reader->isEmptyElement; + while ($reader->moveToNextAttribute()) { + switch ($reader->namespaceURI) { + case '': + $writer->writeAttribute($reader->localName, $reader->value); + break; + case 'http://www.w3.org/2000/xmlns/': + // Skip namespace declarations + break; + default: + $writer->writeAttribute((string) $reader->getClark(), $reader->value); + break; + } + } + if ($empty) { + $writer->endElement(); + } + break; + case Reader::CDATA: + case Reader::TEXT: + $writer->text( + $reader->value + ); + break; + case Reader::END_ELEMENT: + $writer->endElement(); + break; + } + } + } + + /** + * The deserialize method is called during xml parsing. + * + * This method is called statically, this is because in theory this method + * may be used as a type of constructor, or factory method. + * + * Often you want to return an instance of the current class, but you are + * free to return other data as well. + * + * You are responsible for advancing the reader to the next element. Not + * doing anything will result in a never-ending loop. + * + * If you just want to skip parsing for this element altogether, you can + * just call $reader->next(); + * + * $reader->parseInnerTree() will parse the entire sub-tree, and advance to + * the next element. + */ + public static function xmlDeserialize(Reader $reader) + { + $result = new self($reader->readInnerXml()); + $reader->next(); + + return $result; + } +} diff --git a/3rdparty/sabre/xml/lib/LibXMLException.php b/3rdparty/sabre/xml/lib/LibXMLException.php new file mode 100644 index 00000000..993f95fd --- /dev/null +++ b/3rdparty/sabre/xml/lib/LibXMLException.php @@ -0,0 +1,47 @@ +errors = $errors; + parent::__construct($errors[0]->message.' on line '.$errors[0]->line.', column '.$errors[0]->column, $code, $previousException); + } + + /** + * Returns the LibXML errors. + */ + public function getErrors(): array + { + return $this->errors; + } +} diff --git a/3rdparty/sabre/xml/lib/ParseException.php b/3rdparty/sabre/xml/lib/ParseException.php new file mode 100644 index 00000000..158cf011 --- /dev/null +++ b/3rdparty/sabre/xml/lib/ParseException.php @@ -0,0 +1,18 @@ +localName) { + return null; + } + + return '{'.$this->namespaceURI.'}'.$this->localName; + } + + /** + * Reads the entire document. + * + * This function returns an array with the following three elements: + * * name - The root element name. + * * value - The value for the root element. + * * attributes - An array of attributes. + * + * This function will also disable the standard libxml error handler (which + * usually just results in PHP errors), and throw exceptions instead. + */ + public function parse(): array + { + $previousEntityState = null; + $shouldCallLibxmlDisableEntityLoader = (\LIBXML_VERSION < 20900); + if ($shouldCallLibxmlDisableEntityLoader) { + $previousEntityState = libxml_disable_entity_loader(true); + } + $previousSetting = libxml_use_internal_errors(true); + + try { + while (self::ELEMENT !== $this->nodeType) { + if (!$this->read()) { + $errors = libxml_get_errors(); + libxml_clear_errors(); + if ($errors) { + throw new LibXMLException($errors); + } + } + } + $result = $this->parseCurrentElement(); + + // last line of defense in case errors did occur above + $errors = libxml_get_errors(); + libxml_clear_errors(); + if ($errors) { + throw new LibXMLException($errors); + } + } finally { + libxml_use_internal_errors($previousSetting); + if ($shouldCallLibxmlDisableEntityLoader) { + libxml_disable_entity_loader($previousEntityState); + } + } + + return $result; + } + + /** + * parseGetElements parses everything in the current sub-tree, + * and returns an array of elements. + * + * Each element has a 'name', 'value' and 'attributes' key. + * + * If the element didn't contain sub-elements, an empty array is always + * returned. If there was any text inside the element, it will be + * discarded. + * + * If the $elementMap argument is specified, the existing elementMap will + * be overridden while parsing the tree, and restored after this process. + */ + public function parseGetElements(?array $elementMap = null): array + { + $result = $this->parseInnerTree($elementMap); + if (!is_array($result)) { + return []; + } + + return $result; + } + + /** + * Parses all elements below the current element. + * + * This method will return a string if this was a text-node, or an array if + * there were sub-elements. + * + * If there's both text and sub-elements, the text will be discarded. + * + * If the $elementMap argument is specified, the existing elementMap will + * be overridden while parsing the tree, and restored after this process. + * + * @return array|string|null + */ + public function parseInnerTree(?array $elementMap = null) + { + $text = null; + $elements = []; + + if (self::ELEMENT === $this->nodeType && $this->isEmptyElement) { + // Easy! + $this->next(); + + return null; + } + + if (!is_null($elementMap)) { + $this->pushContext(); + $this->elementMap = $elementMap; + } + + try { + if (!$this->read()) { + $errors = libxml_get_errors(); + libxml_clear_errors(); + if ($errors) { + throw new LibXMLException($errors); + } + throw new ParseException('This should never happen (famous last words)'); + } + + $keepOnParsing = true; + + while ($keepOnParsing) { + if (!$this->isValid()) { + $errors = libxml_get_errors(); + + if ($errors) { + libxml_clear_errors(); + throw new LibXMLException($errors); + } + } + + switch ($this->nodeType) { + case self::ELEMENT: + $elements[] = $this->parseCurrentElement(); + break; + case self::TEXT: + case self::CDATA: + $text .= $this->value; + $this->read(); + break; + case self::END_ELEMENT: + // Ensuring we are moving the cursor after the end element. + $this->read(); + $keepOnParsing = false; + break; + case self::NONE: + throw new ParseException('We hit the end of the document prematurely. This likely means that some parser "eats" too many elements. Do not attempt to continue parsing.'); + default: + // Advance to the next element + $this->read(); + break; + } + } + } finally { + if (!is_null($elementMap)) { + $this->popContext(); + } + } + + return $elements ? $elements : $text; + } + + /** + * Reads all text below the current element, and returns this as a string. + */ + public function readText(): string + { + $result = ''; + $previousDepth = $this->depth; + + while ($this->read() && $this->depth != $previousDepth) { + if (in_array($this->nodeType, [\XMLReader::TEXT, \XMLReader::CDATA, \XMLReader::WHITESPACE])) { + $result .= $this->value; + } + } + + return $result; + } + + /** + * Parses the current XML element. + * + * This method returns arn array with 3 properties: + * * name - A clark-notation XML element name. + * * value - The parsed value. + * * attributes - A key-value list of attributes. + */ + public function parseCurrentElement(): array + { + $name = $this->getClark(); + + $attributes = []; + + if ($this->hasAttributes) { + $attributes = $this->parseAttributes(); + } + + $value = call_user_func( + $this->getDeserializerForElementName((string) $name), + $this + ); + + return [ + 'name' => $name, + 'value' => $value, + 'attributes' => $attributes, + ]; + } + + /** + * Grabs all the attributes from the current element, and returns them as a + * key-value array. + * + * If the attributes are part of the same namespace, they will simply be + * short keys. If they are defined on a different namespace, the attribute + * name will be returned in clark-notation. + */ + public function parseAttributes(): array + { + $attributes = []; + + while ($this->moveToNextAttribute()) { + if ($this->namespaceURI) { + // Ignoring 'xmlns', it doesn't make any sense. + if ('http://www.w3.org/2000/xmlns/' === $this->namespaceURI) { + continue; + } + + $name = $this->getClark(); + $attributes[$name] = $this->value; + } else { + $attributes[$this->localName] = $this->value; + } + } + $this->moveToElement(); + + return $attributes; + } + + /** + * Returns the function that should be used to parse the element identified + * by its clark-notation name. + */ + public function getDeserializerForElementName(string $name): callable + { + if (!array_key_exists($name, $this->elementMap)) { + if ('{}' == substr($name, 0, 2) && array_key_exists(substr($name, 2), $this->elementMap)) { + $name = substr($name, 2); + } else { + return ['Sabre\\Xml\\Element\\Base', 'xmlDeserialize']; + } + } + + $deserializer = $this->elementMap[$name]; + if (is_subclass_of($deserializer, 'Sabre\\Xml\\XmlDeserializable')) { + return [$deserializer, 'xmlDeserialize']; + } + + if (is_callable($deserializer)) { + return $deserializer; + } + + $type = gettype($deserializer); + if ('string' === $type) { + $type .= ' ('.$deserializer.')'; + } elseif ('object' === $type) { + $type .= ' ('.get_class($deserializer).')'; + } + throw new \LogicException('Could not use this type as a deserializer: '.$type.' for element: '.$name); + } +} diff --git a/3rdparty/sabre/xml/lib/Serializer/functions.php b/3rdparty/sabre/xml/lib/Serializer/functions.php new file mode 100644 index 00000000..23f22d4c --- /dev/null +++ b/3rdparty/sabre/xml/lib/Serializer/functions.php @@ -0,0 +1,207 @@ + + * + * + * content + * + * + * @param string[] $values + */ +function enum(Writer $writer, array $values) +{ + foreach ($values as $value) { + $writer->writeElement($value); + } +} + +/** + * The valueObject serializer turns a simple PHP object into a classname. + * + * Every public property will be encoded as an xml element with the same + * name, in the XML namespace as specified. + * + * Values that are set to null or an empty array are not serialized. To + * serialize empty properties, you must specify them as an empty string. + * + * @param object $valueObject + */ +function valueObject(Writer $writer, $valueObject, string $namespace) +{ + foreach (get_object_vars($valueObject) as $key => $val) { + if (is_array($val)) { + // If $val is an array, it has a special meaning. We need to + // generate one child element for each item in $val + foreach ($val as $child) { + $writer->writeElement('{'.$namespace.'}'.$key, $child); + } + } elseif (null !== $val) { + $writer->writeElement('{'.$namespace.'}'.$key, $val); + } + } +} + +/** + * This serializer helps you serialize xml structures that look like + * this:. + * + * + * ... + * ... + * ... + * + * + * In that previous example, this serializer just serializes the item element, + * and this could be called like this: + * + * repeatingElements($writer, $items, '{}item'); + */ +function repeatingElements(Writer $writer, array $items, string $childElementName) +{ + foreach ($items as $item) { + $writer->writeElement($childElementName, $item); + } +} + +/** + * This function is the 'default' serializer that is able to serialize most + * things, and delegates to other serializers if needed. + * + * The standardSerializer supports a wide-array of values. + * + * $value may be a string or integer, it will just write out the string as text. + * $value may be an instance of XmlSerializable or Element, in which case it + * calls it's xmlSerialize() method. + * $value may be a PHP callback/function/closure, in case we call the callback + * and give it the Writer as an argument. + * $value may be a an object, and if it's in the classMap we automatically call + * the correct serializer for it. + * $value may be null, in which case we do nothing. + * + * If $value is an array, the array must look like this: + * + * [ + * [ + * 'name' => '{namespaceUri}element-name', + * 'value' => '...', + * 'attributes' => [ 'attName' => 'attValue' ] + * ] + * [, + * 'name' => '{namespaceUri}element-name2', + * 'value' => '...', + * ] + * ] + * + * This would result in xml like: + * + * + * ... + * + * + * ... + * + * + * The value property may be any value standardSerializer supports, so you can + * nest data-structures this way. Both value and attributes are optional. + * + * Alternatively, you can also specify the array using this syntax: + * + * [ + * [ + * '{namespaceUri}element-name' => '...', + * '{namespaceUri}element-name2' => '...', + * ] + * ] + * + * This is excellent for simple key->value structures, and here you can also + * specify anything for the value. + * + * You can even mix the two array syntaxes. + * + * @param string|int|float|bool|array|object $value + */ +function standardSerializer(Writer $writer, $value) +{ + if (is_scalar($value)) { + // String, integer, float, boolean + $writer->text((string) $value); + } elseif ($value instanceof XmlSerializable) { + // XmlSerializable classes or Element classes. + $value->xmlSerialize($writer); + } elseif (is_object($value) && isset($writer->classMap[get_class($value)])) { + // It's an object which class appears in the classmap. + $writer->classMap[get_class($value)]($writer, $value); + } elseif (is_callable($value)) { + // A callback + $value($writer); + } elseif (is_array($value) && array_key_exists('name', $value)) { + // if the array had a 'name' element, we assume that this array + // describes a 'name' and optionally 'attributes' and 'value'. + + $name = $value['name']; + $attributes = isset($value['attributes']) ? $value['attributes'] : []; + $value = isset($value['value']) ? $value['value'] : null; + + $writer->startElement($name); + $writer->writeAttributes($attributes); + $writer->write($value); + $writer->endElement(); + } elseif (is_array($value)) { + foreach ($value as $name => $item) { + if (is_int($name)) { + // This item has a numeric index. We just loop through the + // array and throw it back in the writer. + standardSerializer($writer, $item); + } elseif (is_string($name) && is_array($item) && isset($item['attributes'])) { + // The key is used for a name, but $item has 'attributes' and + // possibly 'value' + $writer->startElement($name); + $writer->writeAttributes($item['attributes']); + if (isset($item['value'])) { + $writer->write($item['value']); + } + $writer->endElement(); + } elseif (is_string($name)) { + // This was a plain key-value array. + $writer->startElement($name); + $writer->write($item); + $writer->endElement(); + } else { + throw new \InvalidArgumentException('The writer does not know how to serialize arrays with keys of type: '.gettype($name)); + } + } + } elseif (is_object($value)) { + throw new \InvalidArgumentException('The writer cannot serialize objects of class: '.get_class($value)); + } elseif (!is_null($value)) { + throw new \InvalidArgumentException('The writer cannot serialize values of type: '.gettype($value)); + } +} diff --git a/3rdparty/sabre/xml/lib/Service.php b/3rdparty/sabre/xml/lib/Service.php new file mode 100644 index 00000000..6e522630 --- /dev/null +++ b/3rdparty/sabre/xml/lib/Service.php @@ -0,0 +1,326 @@ +elementMap = $this->elementMap; + + return $r; + } + + /** + * Returns a fresh xml writer. + */ + public function getWriter(): Writer + { + $w = new Writer(); + $w->namespaceMap = $this->namespaceMap; + $w->classMap = $this->classMap; + + return $w; + } + + /** + * Parses a document in full. + * + * Input may be specified as a string or readable stream resource. + * The returned value is the value of the root document. + * + * Specifying the $contextUri allows the parser to figure out what the URI + * of the document was. This allows relative URIs within the document to be + * expanded easily. + * + * The $rootElementName is specified by reference and will be populated + * with the root element name of the document. + * + * @param string|resource $input + * + * @return array|object|string + * + * @throws ParseException + */ + public function parse($input, ?string $contextUri = null, ?string &$rootElementName = null) + { + if (!is_string($input)) { + // Unfortunately the XMLReader doesn't support streams. When it + // does, we can optimize this. + if (is_resource($input)) { + $input = (string) stream_get_contents($input); + } else { + // Input is not a string and not a resource. + // Therefore, it has to be a closed resource. + // Effectively empty input has been passed in. + $input = ''; + } + } + + // If input is empty, then it's safe to throw an exception + if (empty($input)) { + throw new ParseException('The input element to parse is empty. Do not attempt to parse'); + } + + $r = $this->getReader(); + $r->contextUri = $contextUri; + $r->XML($input, null, $this->options); + + $result = $r->parse(); + $rootElementName = $result['name']; + + return $result['value']; + } + + /** + * Parses a document in full, and specify what the expected root element + * name is. + * + * This function works similar to parse, but the difference is that the + * user can specify what the expected name of the root element should be, + * in clark notation. + * + * This is useful in cases where you expected a specific document to be + * passed, and reduces the amount of if statements. + * + * It's also possible to pass an array of expected rootElements if your + * code may expect more than one document type. + * + * @param string|string[] $rootElementName + * @param string|resource $input + * + * @return array|object|string + * + * @throws ParseException + */ + public function expect($rootElementName, $input, ?string $contextUri = null) + { + if (!is_string($input)) { + // Unfortunately the XMLReader doesn't support streams. When it + // does, we can optimize this. + if (is_resource($input)) { + $input = (string) stream_get_contents($input); + } else { + // Input is not a string and not a resource. + // Therefore, it has to be a closed resource. + // Effectively empty input has been passed in. + $input = ''; + } + } + + // If input is empty, then it's safe to throw an exception + if (empty($input)) { + throw new ParseException('The input element to parse is empty. Do not attempt to parse'); + } + + $r = $this->getReader(); + $r->contextUri = $contextUri; + $r->XML($input, null, $this->options); + + $rootElementName = (array) $rootElementName; + + foreach ($rootElementName as &$rEl) { + if ('{' !== $rEl[0]) { + $rEl = '{}'.$rEl; + } + } + + $result = $r->parse(); + if (!in_array($result['name'], $rootElementName, true)) { + throw new ParseException('Expected '.implode(' or ', $rootElementName).' but received '.$result['name'].' as the root element'); + } + + return $result['value']; + } + + /** + * Generates an XML document in one go. + * + * The $rootElement must be specified in clark notation. + * The value must be a string, an array or an object implementing + * XmlSerializable. Basically, anything that's supported by the Writer + * object. + * + * $contextUri can be used to specify a sort of 'root' of the PHP application, + * in case the xml document is used as a http response. + * + * This allows an implementor to easily create URI's relative to the root + * of the domain. + * + * @param string|array|object|XmlSerializable $value + * + * @return string + */ + public function write(string $rootElementName, $value, ?string $contextUri = null) + { + $w = $this->getWriter(); + $w->openMemory(); + $w->contextUri = $contextUri; + $w->setIndent(true); + $w->startDocument(); + $w->writeElement($rootElementName, $value); + + return $w->outputMemory(); + } + + /** + * Map an XML element to a PHP class. + * + * Calling this function will automatically set up the Reader and Writer + * classes to turn a specific XML element to a PHP class. + * + * For example, given a class such as : + * + * class Author { + * public $firstName; + * public $lastName; + * } + * + * and an XML element such as: + * + * + * ... + * ... + * + * + * These can easily be mapped by calling: + * + * $service->mapValueObject('{http://example.org}author', 'Author'); + */ + public function mapValueObject(string $elementName, string $className) + { + list($namespace) = self::parseClarkNotation($elementName); + + $this->elementMap[$elementName] = function (Reader $reader) use ($className, $namespace) { + return \Sabre\Xml\Deserializer\valueObject($reader, $className, $namespace); + }; + $this->classMap[$className] = function (Writer $writer, $valueObject) use ($namespace) { + return \Sabre\Xml\Serializer\valueObject($writer, $valueObject, $namespace); + }; + $this->valueObjectMap[$className] = $elementName; + } + + /** + * Writes a value object. + * + * This function largely behaves similar to write(), except that it's + * intended specifically to serialize a Value Object into an XML document. + * + * The ValueObject must have been previously registered using + * mapValueObject(). + * + * @param object $object + * + * @throws \InvalidArgumentException + */ + public function writeValueObject($object, ?string $contextUri = null) + { + if (!isset($this->valueObjectMap[get_class($object)])) { + throw new \InvalidArgumentException('"'.get_class($object).'" is not a registered value object class. Register your class with mapValueObject.'); + } + + return $this->write( + $this->valueObjectMap[get_class($object)], + $object, + $contextUri + ); + } + + /** + * Parses a clark-notation string, and returns the namespace and element + * name components. + * + * If the string was invalid, it will throw an InvalidArgumentException. + * + * @throws \InvalidArgumentException + */ + public static function parseClarkNotation(string $str): array + { + static $cache = []; + + if (!isset($cache[$str])) { + if (!preg_match('/^{([^}]*)}(.*)$/', $str, $matches)) { + throw new \InvalidArgumentException('\''.$str.'\' is not a valid clark-notation formatted string'); + } + + $cache[$str] = [ + $matches[1], + $matches[2], + ]; + } + + return $cache[$str]; + } + + /** + * A list of classes and which XML elements they map to. + */ + protected $valueObjectMap = []; +} diff --git a/3rdparty/sabre/xml/lib/Version.php b/3rdparty/sabre/xml/lib/Version.php new file mode 100644 index 00000000..c2da842e --- /dev/null +++ b/3rdparty/sabre/xml/lib/Version.php @@ -0,0 +1,20 @@ + "..", + * "{namespace}name2" => "..", + * ] + * + * One element will be created for each key in this array. The values of + * this array support any format this method supports (this method is + * called recursively). + * + * Array format 2: + * + * [ + * [ + * "name" => "{namespace}name1" + * "value" => "..", + * "attributes" => [ + * "attr" => "attribute value", + * ] + * ], + * [ + * "name" => "{namespace}name1" + * "value" => "..", + * "attributes" => [ + * "attr" => "attribute value", + * ] + * ] + * ] + */ + public function write($value) + { + Serializer\standardSerializer($this, $value); + } + + /** + * Opens a new element. + * + * You can either just use a local elementname, or you can use clark- + * notation to start a new element. + * + * Example: + * + * $writer->startElement('{http://www.w3.org/2005/Atom}entry'); + * + * Would result in something like: + * + * + * + * Note: this function doesn't have the string typehint, because PHP's + * XMLWriter::startElement doesn't either. + * + * @param string $name + */ + public function startElement($name): bool + { + if ('{' === $name[0]) { + list($namespace, $localName) = + Service::parseClarkNotation($name); + + if (array_key_exists($namespace, $this->namespaceMap)) { + $result = $this->startElementNS( + '' === $this->namespaceMap[$namespace] ? null : $this->namespaceMap[$namespace], + $localName, + null + ); + } else { + // An empty namespace means it's the global namespace. This is + // allowed, but it mustn't get a prefix. + if ('' === $namespace || null === $namespace) { + $result = $this->startElement($localName); + $this->writeAttribute('xmlns', ''); + } else { + if (!isset($this->adhocNamespaces[$namespace])) { + $this->adhocNamespaces[$namespace] = 'x'.(count($this->adhocNamespaces) + 1); + } + $result = $this->startElementNS($this->adhocNamespaces[$namespace], $localName, $namespace); + } + } + } else { + $result = parent::startElement($name); + } + + if (!$this->namespacesWritten) { + foreach ($this->namespaceMap as $namespace => $prefix) { + $this->writeAttribute($prefix ? 'xmlns:'.$prefix : 'xmlns', $namespace); + } + $this->namespacesWritten = true; + } + + return $result; + } + + /** + * Write a full element tag and it's contents. + * + * This method automatically closes the element as well. + * + * The element name may be specified in clark-notation. + * + * Examples: + * + * $writer->writeElement('{http://www.w3.org/2005/Atom}author',null); + * becomes: + * + * + * $writer->writeElement('{http://www.w3.org/2005/Atom}author', [ + * '{http://www.w3.org/2005/Atom}name' => 'Evert Pot', + * ]); + * becomes: + * Evert Pot + * + * Note: this function doesn't have the string typehint, because PHP's + * XMLWriter::startElement doesn't either. + * + * @param array|string|object|null $content + */ + public function writeElement($name, $content = null): bool + { + $this->startElement($name); + if (!is_null($content)) { + $this->write($content); + } + $this->endElement(); + + return true; + } + + /** + * Writes a list of attributes. + * + * Attributes are specified as a key->value array. + * + * The key is an attribute name. If the key is a 'localName', the current + * xml namespace is assumed. If it's a 'clark notation key', this namespace + * will be used instead. + */ + public function writeAttributes(array $attributes) + { + foreach ($attributes as $name => $value) { + $this->writeAttribute($name, $value); + } + } + + /** + * Writes a new attribute. + * + * The name may be specified in clark-notation. + * + * Returns true when successful. + * + * Note: this function doesn't have typehints, because for some reason + * PHP's XMLWriter::writeAttribute doesn't either. + * + * @param string $name + * @param string $value + */ + public function writeAttribute($name, $value): bool + { + if ('{' !== $name[0]) { + return parent::writeAttribute($name, $value); + } + + list( + $namespace, + $localName + ) = Service::parseClarkNotation($name); + + if (array_key_exists($namespace, $this->namespaceMap)) { + // It's an attribute with a namespace we know + return $this->writeAttribute( + $this->namespaceMap[$namespace].':'.$localName, + $value + ); + } + + // We don't know the namespace, we must add it in-line + if (!isset($this->adhocNamespaces[$namespace])) { + $this->adhocNamespaces[$namespace] = 'x'.(count($this->adhocNamespaces) + 1); + } + + return $this->writeAttributeNS( + $this->adhocNamespaces[$namespace], + $localName, + $namespace, + $value + ); + } +} diff --git a/3rdparty/sabre/xml/lib/XmlDeserializable.php b/3rdparty/sabre/xml/lib/XmlDeserializable.php new file mode 100644 index 00000000..0a572033 --- /dev/null +++ b/3rdparty/sabre/xml/lib/XmlDeserializable.php @@ -0,0 +1,36 @@ +next(); + * + * $reader->parseInnerTree() will parse the entire sub-tree, and advance to + * the next element. + */ + public static function xmlDeserialize(Reader $reader); +} diff --git a/3rdparty/sabre/xml/lib/XmlSerializable.php b/3rdparty/sabre/xml/lib/XmlSerializable.php new file mode 100644 index 00000000..2affc33f --- /dev/null +++ b/3rdparty/sabre/xml/lib/XmlSerializable.php @@ -0,0 +1,34 @@ +majorType << 5 | $this->additionalInformation); + } + + public function getMajorType(): int + { + return $this->majorType; + } + + public function getAdditionalInformation(): int + { + return $this->additionalInformation; + } +} diff --git a/3rdparty/spomky-labs/cbor-php/src/ByteStringObject.php b/3rdparty/spomky-labs/cbor-php/src/ByteStringObject.php new file mode 100644 index 00000000..b7bb01fd --- /dev/null +++ b/3rdparty/spomky-labs/cbor-php/src/ByteStringObject.php @@ -0,0 +1,56 @@ +length = $length; + $this->value = $data; + } + + public function __toString(): string + { + $result = parent::__toString(); + if ($this->length !== null) { + $result .= $this->length; + } + + return $result . $this->value; + } + + public static function create(string $data): self + { + return new self($data); + } + + public function getValue(): string + { + return $this->value; + } + + public function getLength(): int + { + return mb_strlen($this->value, '8bit'); + } + + public function normalize(): string + { + return $this->value; + } +} diff --git a/3rdparty/spomky-labs/cbor-php/src/CBORObject.php b/3rdparty/spomky-labs/cbor-php/src/CBORObject.php new file mode 100644 index 00000000..2da9f8ae --- /dev/null +++ b/3rdparty/spomky-labs/cbor-php/src/CBORObject.php @@ -0,0 +1,94 @@ +tagObjectManager = $tagObjectManager ?? $this->generateTagManager(); + $this->otherTypeManager = $otherTypeManager ?? $this->generateOtherObjectManager(); + } + + public static function create( + ?TagManagerInterface $tagObjectManager = null, + ?OtherObjectManagerInterface $otherTypeManager = null + ): self { + return new self($tagObjectManager, $otherTypeManager); + } + + public function decode(Stream $stream): CBORObject + { + return $this->process($stream, false); + } + + private function process(Stream $stream, bool $breakable): CBORObject + { + $ib = ord($stream->read(1)); + $mt = $ib >> 5; + $ai = $ib & 0b00011111; + $val = null; + switch ($ai) { + case CBORObject::LENGTH_1_BYTE: //24 + case CBORObject::LENGTH_2_BYTES: //25 + case CBORObject::LENGTH_4_BYTES: //26 + case CBORObject::LENGTH_8_BYTES: //27 + $val = $stream->read(2 ** ($ai & 0b00000111)); + break; + case CBORObject::FUTURE_USE_1: //28 + case CBORObject::FUTURE_USE_2: //29 + case CBORObject::FUTURE_USE_3: //30 + throw new InvalidArgumentException(sprintf( + 'Cannot parse the data. Found invalid Additional Information "%s" (%d).', + str_pad(decbin($ai), 8, '0', STR_PAD_LEFT), + $ai + )); + case CBORObject::LENGTH_INDEFINITE: //31 + return $this->processInfinite($stream, $mt, $breakable); + } + + return $this->processFinite($stream, $mt, $ai, $val); + } + + private function processFinite(Stream $stream, int $mt, int $ai, ?string $val): CBORObject + { + switch ($mt) { + case CBORObject::MAJOR_TYPE_UNSIGNED_INTEGER: //0 + return UnsignedIntegerObject::createObjectForValue($ai, $val); + case CBORObject::MAJOR_TYPE_NEGATIVE_INTEGER: //1 + return NegativeIntegerObject::createObjectForValue($ai, $val); + case CBORObject::MAJOR_TYPE_BYTE_STRING: //2 + $length = $val === null ? $ai : Utils::binToInt($val); + + return ByteStringObject::create($stream->read($length)); + case CBORObject::MAJOR_TYPE_TEXT_STRING: //3 + $length = $val === null ? $ai : Utils::binToInt($val); + + return TextStringObject::create($stream->read($length)); + case CBORObject::MAJOR_TYPE_LIST: //4 + $object = ListObject::create(); + $nbItems = $val === null ? $ai : Utils::binToInt($val); + for ($i = 0; $i < $nbItems; ++$i) { + $object->add($this->process($stream, false)); + } + + return $object; + case CBORObject::MAJOR_TYPE_MAP: //5 + $object = MapObject::create(); + $nbItems = $val === null ? $ai : Utils::binToInt($val); + for ($i = 0; $i < $nbItems; ++$i) { + $object->add($this->process($stream, false), $this->process($stream, false)); + } + + return $object; + case CBORObject::MAJOR_TYPE_TAG: //6 + return $this->tagObjectManager->createObjectForValue($ai, $val, $this->process($stream, false)); + case CBORObject::MAJOR_TYPE_OTHER_TYPE: //7 + return $this->otherTypeManager->createObjectForValue($ai, $val); + default: + throw new RuntimeException(sprintf( + 'Unsupported major type "%s" (%d).', + str_pad(decbin($mt), 5, '0', STR_PAD_LEFT), + $mt + )); // Should never append + } + } + + private function processInfinite(Stream $stream, int $mt, bool $breakable): CBORObject + { + switch ($mt) { + case CBORObject::MAJOR_TYPE_BYTE_STRING: //2 + $object = IndefiniteLengthByteStringObject::create(); + while (! ($it = $this->process($stream, true)) instanceof BreakObject) { + if (! $it instanceof ByteStringObject) { + throw new RuntimeException( + 'Unable to parse the data. Infinite Byte String object can only get Byte String objects.' + ); + } + $object->add($it); + } + + return $object; + case CBORObject::MAJOR_TYPE_TEXT_STRING : //3 + $object = IndefiniteLengthTextStringObject::create(); + while (! ($it = $this->process($stream, true)) instanceof BreakObject) { + if (! $it instanceof TextStringObject) { + throw new RuntimeException( + 'Unable to parse the data. Infinite Text String object can only get Text String objects.' + ); + } + $object->add($it); + } + + return $object; + case CBORObject::MAJOR_TYPE_LIST : //4 + $object = IndefiniteLengthListObject::create(); + $it = $this->process($stream, true); + while (! $it instanceof BreakObject) { + $object->add($it); + $it = $this->process($stream, true); + } + + return $object; + case CBORObject::MAJOR_TYPE_MAP : //5 + $object = IndefiniteLengthMapObject::create(); + while (! ($it = $this->process($stream, true)) instanceof BreakObject) { + $object->add($it, $this->process($stream, false)); + } + + return $object; + case CBORObject::MAJOR_TYPE_OTHER_TYPE : //7 + if (! $breakable) { + throw new InvalidArgumentException('Cannot parse the data. No enclosing indefinite.'); + } + + return BreakObject::create(); + case CBORObject::MAJOR_TYPE_UNSIGNED_INTEGER : //0 + case CBORObject::MAJOR_TYPE_NEGATIVE_INTEGER : //1 + case CBORObject::MAJOR_TYPE_TAG : //6 + default : + throw new InvalidArgumentException(sprintf( + 'Cannot parse the data. Found infinite length for Major Type "%s" (%d).', + str_pad(decbin($mt), 5, '0', STR_PAD_LEFT), + $mt + )); + } + } + + private function generateTagManager(): TagManagerInterface + { + return TagManager::create() + ->add(DatetimeTag::class) + ->add(TimestampTag::class) + + ->add(UnsignedBigIntegerTag::class) + ->add(NegativeBigIntegerTag::class) + + ->add(DecimalFractionTag::class) + ->add(BigFloatTag::class) + + ->add(Base64UrlEncodingTag::class) + ->add(Base64EncodingTag::class) + ->add(Base16EncodingTag::class) + ->add(CBOREncodingTag::class) + + ->add(UriTag::class) + ->add(Base64UrlTag::class) + ->add(Base64Tag::class) + ->add(MimeTag::class) + + ->add(CBORTag::class) + ; + } + + private function generateOtherObjectManager(): OtherObjectManagerInterface + { + return OtherObjectManager::create() + ->add(BreakObject::class) + ->add(SimpleObject::class) + ->add(FalseObject::class) + ->add(TrueObject::class) + ->add(NullObject::class) + ->add(UndefinedObject::class) + ->add(HalfPrecisionFloatObject::class) + ->add(SinglePrecisionFloatObject::class) + ->add(DoublePrecisionFloatObject::class) + ; + } +} diff --git a/3rdparty/spomky-labs/cbor-php/src/DecoderInterface.php b/3rdparty/spomky-labs/cbor-php/src/DecoderInterface.php new file mode 100644 index 00000000..464eb8b2 --- /dev/null +++ b/3rdparty/spomky-labs/cbor-php/src/DecoderInterface.php @@ -0,0 +1,10 @@ +chunks as $chunk) { + $result .= $chunk->__toString(); + } + + return $result . "\xFF"; + } + + public static function create(): self + { + return new self(); + } + + public function add(ByteStringObject $chunk): self + { + $this->chunks[] = $chunk; + + return $this; + } + + public function append(string $chunk): self + { + $this->add(ByteStringObject::create($chunk)); + + return $this; + } + + public function getValue(): string + { + $result = ''; + foreach ($this->chunks as $chunk) { + $result .= $chunk->getValue(); + } + + return $result; + } + + public function getLength(): int + { + $length = 0; + foreach ($this->chunks as $chunk) { + $length += $chunk->getLength(); + } + + return $length; + } + + public function normalize(): string + { + $result = ''; + foreach ($this->chunks as $chunk) { + $result .= $chunk->normalize(); + } + + return $result; + } +} diff --git a/3rdparty/spomky-labs/cbor-php/src/IndefiniteLengthListObject.php b/3rdparty/spomky-labs/cbor-php/src/IndefiniteLengthListObject.php new file mode 100644 index 00000000..e1bef93e --- /dev/null +++ b/3rdparty/spomky-labs/cbor-php/src/IndefiniteLengthListObject.php @@ -0,0 +1,137 @@ + + * @phpstan-implements IteratorAggregate + * @final + */ +class IndefiniteLengthListObject extends AbstractCBORObject implements IteratorAggregate, Normalizable, ArrayAccess +{ + private const MAJOR_TYPE = self::MAJOR_TYPE_LIST; + + private const ADDITIONAL_INFORMATION = self::LENGTH_INDEFINITE; + + /** + * @var CBORObject[] + */ + private array $data = []; + + public function __construct() + { + parent::__construct(self::MAJOR_TYPE, self::ADDITIONAL_INFORMATION); + } + + public function __toString(): string + { + $result = parent::__toString(); + foreach ($this->data as $object) { + $result .= (string) $object; + } + + return $result . "\xFF"; + } + + public static function create(): self + { + return new self(); + } + + /** + * @return mixed[] + */ + public function normalize(): array + { + return array_map( + static fn (CBORObject $object) => $object instanceof Normalizable ? $object->normalize() : $object, + $this->data + ); + } + + public function add(CBORObject $item): self + { + $this->data[] = $item; + + return $this; + } + + public function has(int $index): bool + { + return array_key_exists($index, $this->data); + } + + public function remove(int $index): self + { + if (! $this->has($index)) { + return $this; + } + unset($this->data[$index]); + $this->data = array_values($this->data); + + return $this; + } + + public function get(int $index): CBORObject + { + if (! $this->has($index)) { + throw new InvalidArgumentException('Index not found.'); + } + + return $this->data[$index]; + } + + public function set(int $index, CBORObject $object): self + { + if (! $this->has($index)) { + throw new InvalidArgumentException('Index not found.'); + } + + $this->data[$index] = $object; + + return $this; + } + + /** + * @return Iterator + */ + public function getIterator(): Iterator + { + return new ArrayIterator($this->data); + } + + public function offsetExists($offset): bool + { + return $this->has($offset); + } + + public function offsetGet($offset): CBORObject + { + return $this->get($offset); + } + + public function offsetSet($offset, $value): void + { + if ($offset === null) { + $this->add($value); + + return; + } + + $this->set($offset, $value); + } + + public function offsetUnset($offset): void + { + $this->remove($offset); + } +} diff --git a/3rdparty/spomky-labs/cbor-php/src/IndefiniteLengthMapObject.php b/3rdparty/spomky-labs/cbor-php/src/IndefiniteLengthMapObject.php new file mode 100644 index 00000000..ece69f27 --- /dev/null +++ b/3rdparty/spomky-labs/cbor-php/src/IndefiniteLengthMapObject.php @@ -0,0 +1,149 @@ + + * @phpstan-implements IteratorAggregate + * @final + */ +class IndefiniteLengthMapObject extends AbstractCBORObject implements IteratorAggregate, Normalizable, ArrayAccess +{ + private const MAJOR_TYPE = self::MAJOR_TYPE_MAP; + + private const ADDITIONAL_INFORMATION = self::LENGTH_INDEFINITE; + + /** + * @var MapItem[] + */ + private array $data = []; + + public function __construct() + { + parent::__construct(self::MAJOR_TYPE, self::ADDITIONAL_INFORMATION); + } + + public function __toString(): string + { + $result = parent::__toString(); + foreach ($this->data as $object) { + $result .= (string) $object->getKey(); + $result .= (string) $object->getValue(); + } + + return $result . "\xFF"; + } + + public static function create(): self + { + return new self(); + } + + public function add(CBORObject $key, CBORObject $value): self + { + if (! $key instanceof Normalizable) { + throw new InvalidArgumentException('Invalid key. Shall be normalizable'); + } + $this->data[$key->normalize()] = MapItem::create($key, $value); + + return $this; + } + + public function has(int|string $key): bool + { + return array_key_exists($key, $this->data); + } + + public function remove(int|string $index): self + { + if (! $this->has($index)) { + return $this; + } + unset($this->data[$index]); + $this->data = array_values($this->data); + + return $this; + } + + public function get(int|string $index): CBORObject + { + if (! $this->has($index)) { + throw new InvalidArgumentException('Index not found.'); + } + + return $this->data[$index]->getValue(); + } + + public function set(MapItem $object): self + { + $key = $object->getKey(); + if (! $key instanceof Normalizable) { + throw new InvalidArgumentException('Invalid key. Shall be normalizable'); + } + + $this->data[$key->normalize()] = $object; + + return $this; + } + + /** + * @return Iterator + */ + public function getIterator(): Iterator + { + return new ArrayIterator($this->data); + } + + /** + * @return mixed[] + */ + public function normalize(): array + { + return array_reduce($this->data, static function (array $carry, MapItem $item): array { + $key = $item->getKey(); + if (! $key instanceof Normalizable) { + throw new InvalidArgumentException('Invalid key. Shall be normalizable'); + } + $valueObject = $item->getValue(); + $carry[$key->normalize()] = $valueObject instanceof Normalizable ? $valueObject->normalize() : $valueObject; + + return $carry; + }, []); + } + + public function offsetExists($offset): bool + { + return $this->has($offset); + } + + public function offsetGet($offset): CBORObject + { + return $this->get($offset); + } + + public function offsetSet($offset, $value): void + { + if (! $offset instanceof CBORObject) { + throw new InvalidArgumentException('Invalid key'); + } + if (! $value instanceof CBORObject) { + throw new InvalidArgumentException('Invalid value'); + } + + $this->set(MapItem::create($offset, $value)); + } + + public function offsetUnset($offset): void + { + $this->remove($offset); + } +} diff --git a/3rdparty/spomky-labs/cbor-php/src/IndefiniteLengthTextStringObject.php b/3rdparty/spomky-labs/cbor-php/src/IndefiniteLengthTextStringObject.php new file mode 100644 index 00000000..bc3a15da --- /dev/null +++ b/3rdparty/spomky-labs/cbor-php/src/IndefiniteLengthTextStringObject.php @@ -0,0 +1,84 @@ +data as $object) { + $result .= (string) $object; + } + + return $result . "\xFF"; + } + + public static function create(): self + { + return new self(); + } + + public function add(TextStringObject $chunk): self + { + $this->data[] = $chunk; + + return $this; + } + + public function append(string $chunk): self + { + $this->add(TextStringObject::create($chunk)); + + return $this; + } + + public function getValue(): string + { + $result = ''; + foreach ($this->data as $object) { + $result .= $object->getValue(); + } + + return $result; + } + + public function getLength(): int + { + $length = 0; + foreach ($this->data as $object) { + $length += $object->getLength(); + } + + return $length; + } + + public function normalize(): string + { + $result = ''; + foreach ($this->data as $object) { + $result .= $object->normalize(); + } + + return $result; + } +} diff --git a/3rdparty/spomky-labs/cbor-php/src/LengthCalculator.php b/3rdparty/spomky-labs/cbor-php/src/LengthCalculator.php new file mode 100644 index 00000000..ec8c678c --- /dev/null +++ b/3rdparty/spomky-labs/cbor-php/src/LengthCalculator.php @@ -0,0 +1,65 @@ + $data + * + * @return array{int, null|string} + */ + public static function getLengthOfArray(array $data): array + { + $length = count($data); + + return self::computeLength($length); + } + + /** + * @return array{int, null|string} + */ + private static function computeLength(int $length): array + { + return match (true) { + $length <= 23 => [$length, null], + $length <= 0xFF => [24, chr($length)], + $length <= 0xFFFF => [25, self::hex2bin(dechex($length))], + $length <= 0xFFFFFFFF => [26, self::hex2bin(dechex($length))], + BigInteger::of($length)->isLessThan(BigInteger::fromBase('FFFFFFFFFFFFFFFF', 16)) => [ + 27, + self::hex2bin(dechex($length)), + ], + default => [31, null], + }; + } + + private static function hex2bin(string $data): string + { + $data = str_pad($data, (int) (2 ** ceil(log(mb_strlen($data, '8bit'), 2))), '0', STR_PAD_LEFT); + $result = hex2bin($data); + if ($result === false) { + throw new InvalidArgumentException('Unable to convert the data'); + } + + return $result; + } +} diff --git a/3rdparty/spomky-labs/cbor-php/src/ListObject.php b/3rdparty/spomky-labs/cbor-php/src/ListObject.php new file mode 100644 index 00000000..4f8da726 --- /dev/null +++ b/3rdparty/spomky-labs/cbor-php/src/ListObject.php @@ -0,0 +1,162 @@ + + * @phpstan-implements IteratorAggregate + * @see \CBOR\Test\ListObjectTest + */ +class ListObject extends AbstractCBORObject implements Countable, IteratorAggregate, Normalizable, ArrayAccess +{ + private const MAJOR_TYPE = self::MAJOR_TYPE_LIST; + + /** + * @var CBORObject[] + */ + private array $data; + + private ?string $length = null; + + /** + * @param CBORObject[] $data + */ + public function __construct(array $data = []) + { + [$additionalInformation, $length] = LengthCalculator::getLengthOfArray($data); + array_map(static function ($item): void { + }, $data); + + parent::__construct(self::MAJOR_TYPE, $additionalInformation); + $this->data = array_values($data); + $this->length = $length; + } + + public function __toString(): string + { + $result = parent::__toString(); + if ($this->length !== null) { + $result .= $this->length; + } + foreach ($this->data as $object) { + $result .= (string) $object; + } + + return $result; + } + + /** + * @param CBORObject[] $data + */ + public static function create(array $data = []): self + { + return new self($data); + } + + public function add(CBORObject $object): self + { + $this->data[] = $object; + [$this->additionalInformation, $this->length] = LengthCalculator::getLengthOfArray($this->data); + + return $this; + } + + public function has(int $index): bool + { + return array_key_exists($index, $this->data); + } + + public function remove(int $index): self + { + if (! $this->has($index)) { + return $this; + } + unset($this->data[$index]); + $this->data = array_values($this->data); + [$this->additionalInformation, $this->length] = LengthCalculator::getLengthOfArray($this->data); + + return $this; + } + + public function get(int $index): CBORObject + { + if (! $this->has($index)) { + throw new InvalidArgumentException('Index not found.'); + } + + return $this->data[$index]; + } + + public function set(int $index, CBORObject $object): self + { + if (! $this->has($index)) { + throw new InvalidArgumentException('Index not found.'); + } + + $this->data[$index] = $object; + [$this->additionalInformation, $this->length] = LengthCalculator::getLengthOfArray($this->data); + + return $this; + } + + /** + * @return array + */ + public function normalize(): array + { + return array_map( + static fn (CBORObject $object) => $object instanceof Normalizable ? $object->normalize() : $object, + $this->data + ); + } + + public function count(): int + { + return count($this->data); + } + + /** + * @return Iterator + */ + public function getIterator(): Iterator + { + return new ArrayIterator($this->data); + } + + public function offsetExists($offset): bool + { + return $this->has($offset); + } + + public function offsetGet($offset): CBORObject + { + return $this->get($offset); + } + + public function offsetSet($offset, $value): void + { + if ($offset === null) { + $this->add($value); + + return; + } + + $this->set($offset, $value); + } + + public function offsetUnset($offset): void + { + $this->remove($offset); + } +} diff --git a/3rdparty/spomky-labs/cbor-php/src/MapItem.php b/3rdparty/spomky-labs/cbor-php/src/MapItem.php new file mode 100644 index 00000000..7cb4a25c --- /dev/null +++ b/3rdparty/spomky-labs/cbor-php/src/MapItem.php @@ -0,0 +1,29 @@ +key; + } + + public function getValue(): CBORObject + { + return $this->value; + } +} diff --git a/3rdparty/spomky-labs/cbor-php/src/MapObject.php b/3rdparty/spomky-labs/cbor-php/src/MapObject.php new file mode 100644 index 00000000..72a7431f --- /dev/null +++ b/3rdparty/spomky-labs/cbor-php/src/MapObject.php @@ -0,0 +1,180 @@ + + * @phpstan-implements IteratorAggregate + */ +final class MapObject extends AbstractCBORObject implements Countable, IteratorAggregate, Normalizable, ArrayAccess +{ + private const MAJOR_TYPE = self::MAJOR_TYPE_MAP; + + /** + * @var MapItem[] + */ + private array $data; + + private ?string $length = null; + + /** + * @param MapItem[] $data + */ + public function __construct(array $data = []) + { + [$additionalInformation, $length] = LengthCalculator::getLengthOfArray($data); + array_map(static function ($item): void { + if (! $item instanceof MapItem) { + throw new InvalidArgumentException('The list must contain only MapItem objects.'); + } + }, $data); + + parent::__construct(self::MAJOR_TYPE, $additionalInformation); + $this->data = $data; + $this->length = $length; + } + + public function __toString(): string + { + $result = parent::__toString(); + if ($this->length !== null) { + $result .= $this->length; + } + foreach ($this->data as $object) { + $result .= $object->getKey() + ->__toString() + ; + $result .= $object->getValue() + ->__toString() + ; + } + + return $result; + } + + /** + * @param MapItem[] $data + */ + public static function create(array $data = []): self + { + return new self($data); + } + + public function add(CBORObject $key, CBORObject $value): self + { + if (! $key instanceof Normalizable) { + throw new InvalidArgumentException('Invalid key. Shall be normalizable'); + } + $this->data[$key->normalize()] = MapItem::create($key, $value); + [$this->additionalInformation, $this->length] = LengthCalculator::getLengthOfArray($this->data); + + return $this; + } + + public function has(int|string $key): bool + { + return array_key_exists($key, $this->data); + } + + public function remove(int|string $index): self + { + if (! $this->has($index)) { + return $this; + } + unset($this->data[$index]); + $this->data = array_values($this->data); + [$this->additionalInformation, $this->length] = LengthCalculator::getLengthOfArray($this->data); + + return $this; + } + + public function get(int|string $index): CBORObject + { + if (! $this->has($index)) { + throw new InvalidArgumentException('Index not found.'); + } + + return $this->data[$index]->getValue(); + } + + public function set(MapItem $object): self + { + $key = $object->getKey(); + if (! $key instanceof Normalizable) { + throw new InvalidArgumentException('Invalid key. Shall be normalizable'); + } + + $this->data[$key->normalize()] = $object; + [$this->additionalInformation, $this->length] = LengthCalculator::getLengthOfArray($this->data); + + return $this; + } + + public function count(): int + { + return count($this->data); + } + + /** + * @return Iterator + */ + public function getIterator(): Iterator + { + return new ArrayIterator($this->data); + } + + /** + * @return array + */ + public function normalize(): array + { + return array_reduce($this->data, static function (array $carry, MapItem $item): array { + $key = $item->getKey(); + if (! $key instanceof Normalizable) { + throw new InvalidArgumentException('Invalid key. Shall be normalizable'); + } + $valueObject = $item->getValue(); + $carry[$key->normalize()] = $valueObject instanceof Normalizable ? $valueObject->normalize() : $valueObject; + + return $carry; + }, []); + } + + public function offsetExists($offset): bool + { + return $this->has($offset); + } + + public function offsetGet($offset): CBORObject + { + return $this->get($offset); + } + + public function offsetSet($offset, $value): void + { + if (! $offset instanceof CBORObject) { + throw new InvalidArgumentException('Invalid key'); + } + if (! $value instanceof CBORObject) { + throw new InvalidArgumentException('Invalid value'); + } + + $this->set(MapItem::create($offset, $value)); + } + + public function offsetUnset($offset): void + { + $this->remove($offset); + } +} diff --git a/3rdparty/spomky-labs/cbor-php/src/NegativeIntegerObject.php b/3rdparty/spomky-labs/cbor-php/src/NegativeIntegerObject.php new file mode 100644 index 00000000..93c0ee70 --- /dev/null +++ b/3rdparty/spomky-labs/cbor-php/src/NegativeIntegerObject.php @@ -0,0 +1,112 @@ +data !== null) { + $result .= $this->data; + } + + return $result; + } + + public static function createObjectForValue(int $additionalInformation, ?string $data): self + { + return new self($additionalInformation, $data); + } + + public static function create(int $value): self + { + return self::createFromString((string) $value); + } + + public static function createFromString(string $value): self + { + $integer = BigInteger::of($value); + + return self::createBigInteger($integer); + } + + public function getValue(): string + { + if ($this->data === null) { + return (string) (-1 - $this->additionalInformation); + } + + $result = Utils::binToBigInteger($this->data); + $minusOne = BigInteger::of(-1); + + return $minusOne->minus($result) + ->toBase(10) + ; + } + + public function normalize(): string + { + return $this->getValue(); + } + + private static function createBigInteger(BigInteger $integer): self + { + if ($integer->isGreaterThanOrEqualTo(BigInteger::zero())) { + throw new InvalidArgumentException('The value must be a negative integer.'); + } + + $minusOne = BigInteger::of(-1); + $computed_value = $minusOne->minus($integer); + + switch (true) { + case $computed_value->isLessThan(BigInteger::of(24)): + $ai = $computed_value->toInt(); + $data = null; + break; + case $computed_value->isLessThan(BigInteger::fromBase('FF', 16)): + $ai = 24; + $data = self::hex2bin(str_pad($computed_value->toBase(16), 2, '0', STR_PAD_LEFT)); + break; + case $computed_value->isLessThan(BigInteger::fromBase('FFFF', 16)): + $ai = 25; + $data = self::hex2bin(str_pad($computed_value->toBase(16), 4, '0', STR_PAD_LEFT)); + break; + case $computed_value->isLessThan(BigInteger::fromBase('FFFFFFFF', 16)): + $ai = 26; + $data = self::hex2bin(str_pad($computed_value->toBase(16), 8, '0', STR_PAD_LEFT)); + break; + default: + throw new InvalidArgumentException( + 'Out of range. Please use NegativeBigIntegerTag tag with ByteStringObject object instead.' + ); + } + + return new self($ai, $data); + } + + private static function hex2bin(string $data): string + { + $result = hex2bin($data); + if ($result === false) { + throw new InvalidArgumentException('Unable to convert the data'); + } + + return $result; + } +} diff --git a/3rdparty/spomky-labs/cbor-php/src/Normalizable.php b/3rdparty/spomky-labs/cbor-php/src/Normalizable.php new file mode 100644 index 00000000..7ff12bf9 --- /dev/null +++ b/3rdparty/spomky-labs/cbor-php/src/Normalizable.php @@ -0,0 +1,13 @@ +data !== null) { + $result .= $this->data; + } + + return $result; + } + + public function getContent(): ?string + { + return $this->data; + } +} diff --git a/3rdparty/spomky-labs/cbor-php/src/OtherObject/BreakObject.php b/3rdparty/spomky-labs/cbor-php/src/OtherObject/BreakObject.php new file mode 100644 index 00000000..f9e6f061 --- /dev/null +++ b/3rdparty/spomky-labs/cbor-php/src/OtherObject/BreakObject.php @@ -0,0 +1,30 @@ +getExponent(); + $mantissa = $this->getMantissa(); + $sign = $this->getSign(); + + if ($exponent === 0) { + $val = $mantissa * 2 ** (-(1022 + 52)); + } elseif ($exponent !== 0b11111111111) { + $val = ($mantissa + (1 << 52)) * 2 ** ($exponent - (1023 + 52)); + } else { + $val = $mantissa === 0 ? INF : NAN; + } + + return $sign * $val; + } + + public function getExponent(): int + { + $data = $this->data; + Utils::assertString($data, 'Invalid data'); + + return Utils::binToBigInteger($data)->shiftedRight(52)->and(Utils::hexToBigInteger('7ff'))->toInt(); + } + + public function getMantissa(): int + { + $data = $this->data; + Utils::assertString($data, 'Invalid data'); + + return Utils::binToBigInteger($data)->and(Utils::hexToBigInteger('fffffffffffff'))->toInt(); + } + + public function getSign(): int + { + $data = $this->data; + Utils::assertString($data, 'Invalid data'); + $sign = Utils::binToBigInteger($data)->shiftedRight(63); + + return $sign->isEqualTo(BigInteger::one()) ? -1 : 1; + } +} diff --git a/3rdparty/spomky-labs/cbor-php/src/OtherObject/FalseObject.php b/3rdparty/spomky-labs/cbor-php/src/OtherObject/FalseObject.php new file mode 100644 index 00000000..be5d8b9d --- /dev/null +++ b/3rdparty/spomky-labs/cbor-php/src/OtherObject/FalseObject.php @@ -0,0 +1,36 @@ +getExponent(); + $mantissa = $this->getMantissa(); + $sign = $this->getSign(); + + if ($exponent === 0) { + $val = $mantissa * 2 ** (-24); + } elseif ($exponent !== 0b11111) { + $val = ($mantissa + (1 << 10)) * 2 ** ($exponent - 25); + } else { + $val = $mantissa === 0 ? INF : NAN; + } + + return $sign * $val; + } + + public function getExponent(): int + { + $data = $this->data; + Utils::assertString($data, 'Invalid data'); + + return Utils::binToBigInteger($data)->shiftedRight(10)->and(Utils::hexToBigInteger('1f'))->toInt(); + } + + public function getMantissa(): int + { + $data = $this->data; + Utils::assertString($data, 'Invalid data'); + + return Utils::binToBigInteger($data)->and(Utils::hexToBigInteger('3ff'))->toInt(); + } + + public function getSign(): int + { + $data = $this->data; + Utils::assertString($data, 'Invalid data'); + $sign = Utils::binToBigInteger($data)->shiftedRight(15); + + return $sign->isEqualTo(BigInteger::one()) ? -1 : 1; + } +} diff --git a/3rdparty/spomky-labs/cbor-php/src/OtherObject/NullObject.php b/3rdparty/spomky-labs/cbor-php/src/OtherObject/NullObject.php new file mode 100644 index 00000000..6253a195 --- /dev/null +++ b/3rdparty/spomky-labs/cbor-php/src/OtherObject/NullObject.php @@ -0,0 +1,36 @@ +classes[$ai] = $class; + } + + return $this; + } + + public function getClassForValue(int $value): string + { + return array_key_exists($value, $this->classes) ? $this->classes[$value] : GenericObject::class; + } + + public function createObjectForValue(int $value, ?string $data): OtherObjectInterface + { + /** @var OtherObject $class */ + $class = $this->getClassForValue($value); + + return $class::createFromLoadedData($value, $data); + } +} diff --git a/3rdparty/spomky-labs/cbor-php/src/OtherObject/OtherObjectManagerInterface.php b/3rdparty/spomky-labs/cbor-php/src/OtherObject/OtherObjectManagerInterface.php new file mode 100644 index 00000000..9d6d7ea9 --- /dev/null +++ b/3rdparty/spomky-labs/cbor-php/src/OtherObject/OtherObjectManagerInterface.php @@ -0,0 +1,10 @@ += 0 && $value <= 19: + return new self($value, null); + case $value === 20: + return FalseObject::create(); + case $value === 21: + return TrueObject::create(); + case $value === 22: + return NullObject::create(); + case $value === 23: + return UndefinedObject::create(); + case $value <= 31: + throw new InvalidArgumentException('Invalid simple value. Shall be between 32 and 255.'); + case $value <= 255: + return new self(24, chr($value)); + default: + throw new InvalidArgumentException('The value is not a valid simple value.'); + } + } + + public static function createFromLoadedData(int $additionalInformation, ?string $data): Base + { + if ($additionalInformation === 24) { + if ($data === null) { + throw new InvalidArgumentException('Invalid simple value. Content data is missing.'); + } + if (mb_strlen($data, '8bit') !== 1) { + throw new InvalidArgumentException('Invalid simple value. Content data is too long.'); + } + if (ord($data) < 32) { + throw new InvalidArgumentException('Invalid simple value. Content data must be between 32 and 255.'); + } + } elseif ($additionalInformation < 20) { + if ($data !== null) { + throw new InvalidArgumentException('Invalid simple value. Content data should not be present.'); + } + } + + return new self($additionalInformation, $data); + } + + public function normalize(): int + { + if ($this->data === null) { + return $this->getAdditionalInformation(); + } + + return Utils::binToInt($this->data); + } +} diff --git a/3rdparty/spomky-labs/cbor-php/src/OtherObject/SinglePrecisionFloatObject.php b/3rdparty/spomky-labs/cbor-php/src/OtherObject/SinglePrecisionFloatObject.php new file mode 100644 index 00000000..d47cd309 --- /dev/null +++ b/3rdparty/spomky-labs/cbor-php/src/OtherObject/SinglePrecisionFloatObject.php @@ -0,0 +1,76 @@ +getExponent(); + $mantissa = $this->getMantissa(); + $sign = $this->getSign(); + + if ($exponent === 0) { + $val = $mantissa * 2 ** (-(126 + 23)); + } elseif ($exponent !== 0b11111111) { + $val = ($mantissa + (1 << 23)) * 2 ** ($exponent - (127 + 23)); + } else { + $val = $mantissa === 0 ? INF : NAN; + } + + return $sign * $val; + } + + public function getExponent(): int + { + $data = $this->data; + Utils::assertString($data, 'Invalid data'); + + return Utils::binToBigInteger($data)->shiftedRight(23)->and(Utils::hexToBigInteger('ff'))->toInt(); + } + + public function getMantissa(): int + { + $data = $this->data; + Utils::assertString($data, 'Invalid data'); + + return Utils::binToBigInteger($data)->and(Utils::hexToBigInteger('7fffff'))->toInt(); + } + + public function getSign(): int + { + $data = $this->data; + Utils::assertString($data, 'Invalid data'); + $sign = Utils::binToBigInteger($data)->shiftedRight(31); + + return $sign->isEqualTo(BigInteger::one()) ? -1 : 1; + } +} diff --git a/3rdparty/spomky-labs/cbor-php/src/OtherObject/TrueObject.php b/3rdparty/spomky-labs/cbor-php/src/OtherObject/TrueObject.php new file mode 100644 index 00000000..edd8d83c --- /dev/null +++ b/3rdparty/spomky-labs/cbor-php/src/OtherObject/TrueObject.php @@ -0,0 +1,36 @@ +resource = $resource; + } + + public static function create(string $data): self + { + return new self($data); + } + + public function read(int $length): string + { + if ($length === 0) { + return ''; + } + + $alreadyRead = 0; + $data = ''; + while ($alreadyRead < $length) { + $left = $length - $alreadyRead; + $sizeToRead = $left < 1024 && $left > 0 ? $left : 1024; + $newData = fread($this->resource, $sizeToRead); + $alreadyRead += $sizeToRead; + + if ($newData === false) { + throw new RuntimeException('Unable to read the memory'); + } + if (mb_strlen($newData, '8bit') < $sizeToRead) { + throw new InvalidArgumentException(sprintf( + 'Out of range. Expected: %d, read: %d.', + $length, + mb_strlen($data, '8bit') + )); + } + $data .= $newData; + } + + if (mb_strlen($data, '8bit') !== $length) { + throw new InvalidArgumentException(sprintf( + 'Out of range. Expected: %d, read: %d.', + $length, + mb_strlen($data, '8bit') + )); + } + + return $data; + } +} diff --git a/3rdparty/spomky-labs/cbor-php/src/Tag.php b/3rdparty/spomky-labs/cbor-php/src/Tag.php new file mode 100644 index 00000000..556c85dd --- /dev/null +++ b/3rdparty/spomky-labs/cbor-php/src/Tag.php @@ -0,0 +1,74 @@ +data !== null) { + $result .= $this->data; + } + + return $result . $this->object; + } + + public function getData(): ?string + { + return $this->data; + } + + public function getValue(): CBORObject + { + return $this->object; + } + + /** + * @return array{int, null|string} + */ + protected static function determineComponents(int $tag): array + { + switch (true) { + case $tag < 0: + throw new InvalidArgumentException('The value must be a positive integer.'); + case $tag < 24: + return [$tag, null]; + case $tag < 0xFF: + return [24, self::hex2bin(dechex($tag))]; + case $tag < 0xFFFF: + return [25, self::hex2bin(dechex($tag))]; + case $tag < 0xFFFFFFFF: + return [26, self::hex2bin(dechex($tag))]; + default: + throw new InvalidArgumentException( + 'Out of range. Please use PositiveBigIntegerTag tag with ByteStringObject object instead.' + ); + } + } + + private static function hex2bin(string $data): string + { + $result = hex2bin($data); + if ($result === false) { + throw new InvalidArgumentException('Unable to convert the data'); + } + + return $result; + } +} diff --git a/3rdparty/spomky-labs/cbor-php/src/Tag/Base16EncodingTag.php b/3rdparty/spomky-labs/cbor-php/src/Tag/Base16EncodingTag.php new file mode 100644 index 00000000..fb69f498 --- /dev/null +++ b/3rdparty/spomky-labs/cbor-php/src/Tag/Base16EncodingTag.php @@ -0,0 +1,28 @@ +get(0); + if (! $e instanceof UnsignedIntegerObject && ! $e instanceof NegativeIntegerObject) { + throw new InvalidArgumentException('The exponent must be a Signed Integer or an Unsigned Integer object.'); + } + $m = $object->get(1); + if (! $m instanceof UnsignedIntegerObject && ! $m instanceof NegativeIntegerObject && ! $m instanceof NegativeBigIntegerTag && ! $m instanceof UnsignedBigIntegerTag) { + throw new InvalidArgumentException( + 'The mantissa must be a Positive or Negative Signed Integer or an Unsigned Integer object.' + ); + } + + parent::__construct($additionalInformation, $data, $object); + } + + public static function getTagId(): int + { + return self::TAG_BIG_FLOAT; + } + + public static function createFromLoadedData(int $additionalInformation, ?string $data, CBORObject $object): Tag + { + return new self($additionalInformation, $data, $object); + } + + public static function create(CBORObject $object): Tag + { + [$ai, $data] = self::determineComponents(self::TAG_BIG_FLOAT); + + return new self($ai, $data, $object); + } + + public static function createFromExponentAndMantissa(CBORObject $e, CBORObject $m): Tag + { + $object = ListObject::create() + ->add($e) + ->add($m) + ; + + return self::create($object); + } + + public function normalize() + { + /** @var ListObject $object */ + $object = $this->object; + /** @var UnsignedIntegerObject|NegativeIntegerObject $e */ + $e = $object->get(0); + /** @var UnsignedIntegerObject|NegativeIntegerObject|NegativeBigIntegerTag|UnsignedBigIntegerTag $m */ + $m = $object->get(1); + + return rtrim(bcmul($m->normalize(), bcpow('2', $e->normalize(), 100), 100), '0'); + } +} diff --git a/3rdparty/spomky-labs/cbor-php/src/Tag/CBOREncodingTag.php b/3rdparty/spomky-labs/cbor-php/src/Tag/CBOREncodingTag.php new file mode 100644 index 00000000..1a6bf26e --- /dev/null +++ b/3rdparty/spomky-labs/cbor-php/src/Tag/CBOREncodingTag.php @@ -0,0 +1,40 @@ +object instanceof Normalizable ? $this->object->normalize() : $this->object; + } +} diff --git a/3rdparty/spomky-labs/cbor-php/src/Tag/DatetimeTag.php b/3rdparty/spomky-labs/cbor-php/src/Tag/DatetimeTag.php new file mode 100644 index 00000000..d1044ec2 --- /dev/null +++ b/3rdparty/spomky-labs/cbor-php/src/Tag/DatetimeTag.php @@ -0,0 +1,63 @@ +object; + $result = DateTimeImmutable::createFromFormat(DATE_RFC3339, $object->normalize()); + if ($result !== false) { + return $result; + } + + $formatted = DateTimeImmutable::createFromFormat('Y-m-d\TH:i:s.uP', $object->normalize()); + if ($formatted === false) { + throw new InvalidArgumentException('Invalid data. Cannot be converted into a datetime object'); + } + + return $formatted; + } +} diff --git a/3rdparty/spomky-labs/cbor-php/src/Tag/DecimalFractionTag.php b/3rdparty/spomky-labs/cbor-php/src/Tag/DecimalFractionTag.php new file mode 100644 index 00000000..9eafd253 --- /dev/null +++ b/3rdparty/spomky-labs/cbor-php/src/Tag/DecimalFractionTag.php @@ -0,0 +1,82 @@ +get(0); + if (! $e instanceof UnsignedIntegerObject && ! $e instanceof NegativeIntegerObject) { + throw new InvalidArgumentException('The exponent must be a Signed Integer or an Unsigned Integer object.'); + } + $m = $object->get(1); + if (! $m instanceof UnsignedIntegerObject && ! $m instanceof NegativeIntegerObject && ! $m instanceof NegativeBigIntegerTag && ! $m instanceof UnsignedBigIntegerTag) { + throw new InvalidArgumentException( + 'The mantissa must be a Positive or Negative Signed Integer or an Unsigned Integer object.' + ); + } + + parent::__construct($additionalInformation, $data, $object); + } + + public static function create(CBORObject $object): self + { + [$ai, $data] = self::determineComponents(self::TAG_DECIMAL_FRACTION); + + return new self($ai, $data, $object); + } + + public static function getTagId(): int + { + return self::TAG_DECIMAL_FRACTION; + } + + public static function createFromLoadedData(int $additionalInformation, ?string $data, CBORObject $object): Tag + { + return new self($additionalInformation, $data, $object); + } + + public static function createFromExponentAndMantissa(CBORObject $e, CBORObject $m): Tag + { + $object = ListObject::create() + ->add($e) + ->add($m) + ; + + return self::create($object); + } + + public function normalize() + { + /** @var ListObject $object */ + $object = $this->object; + /** @var UnsignedIntegerObject|NegativeIntegerObject $e */ + $e = $object->get(0); + /** @var UnsignedIntegerObject|NegativeIntegerObject|NegativeBigIntegerTag|UnsignedBigIntegerTag $m */ + $m = $object->get(1); + + return rtrim(bcmul($m->normalize(), bcpow('10', $e->normalize(), 100), 100), '0'); + } +} diff --git a/3rdparty/spomky-labs/cbor-php/src/Tag/GenericTag.php b/3rdparty/spomky-labs/cbor-php/src/Tag/GenericTag.php new file mode 100644 index 00000000..de780add --- /dev/null +++ b/3rdparty/spomky-labs/cbor-php/src/Tag/GenericTag.php @@ -0,0 +1,21 @@ +object; + + return $object->normalize(); + } +} diff --git a/3rdparty/spomky-labs/cbor-php/src/Tag/NegativeBigIntegerTag.php b/3rdparty/spomky-labs/cbor-php/src/Tag/NegativeBigIntegerTag.php new file mode 100644 index 00000000..e51c6eab --- /dev/null +++ b/3rdparty/spomky-labs/cbor-php/src/Tag/NegativeBigIntegerTag.php @@ -0,0 +1,54 @@ +object; + $integer = BigInteger::fromBase(bin2hex($object->getValue()), 16); + $minusOne = BigInteger::of(-1); + + return $minusOne->minus($integer) + ->toBase(10) + ; + } +} diff --git a/3rdparty/spomky-labs/cbor-php/src/Tag/TagInterface.php b/3rdparty/spomky-labs/cbor-php/src/Tag/TagInterface.php new file mode 100644 index 00000000..fbd24a71 --- /dev/null +++ b/3rdparty/spomky-labs/cbor-php/src/Tag/TagInterface.php @@ -0,0 +1,20 @@ +classes[$class::getTagId()] = $class; + + return $this; + } + + public function getClassForValue(int $value): string + { + return array_key_exists($value, $this->classes) ? $this->classes[$value] : GenericTag::class; + } + + public function createObjectForValue(int $additionalInformation, ?string $data, CBORObject $object): TagInterface + { + $value = $additionalInformation; + if ($additionalInformation >= 24) { + Utils::assertString($data, 'Invalid data'); + $value = Utils::binToInt($data); + } + /** @var Tag $class */ + $class = $this->getClassForValue($value); + + return $class::createFromLoadedData($additionalInformation, $data, $object); + } +} diff --git a/3rdparty/spomky-labs/cbor-php/src/Tag/TagManagerInterface.php b/3rdparty/spomky-labs/cbor-php/src/Tag/TagManagerInterface.php new file mode 100644 index 00000000..2e21d34c --- /dev/null +++ b/3rdparty/spomky-labs/cbor-php/src/Tag/TagManagerInterface.php @@ -0,0 +1,12 @@ +object; + + switch (true) { + case $object instanceof UnsignedIntegerObject: + case $object instanceof NegativeIntegerObject: + $formatted = DateTimeImmutable::createFromFormat('U', $object->normalize()); + + break; + case $object instanceof HalfPrecisionFloatObject: + case $object instanceof SinglePrecisionFloatObject: + case $object instanceof DoublePrecisionFloatObject: + $value = (string) $object->normalize(); + $parts = explode('.', $value); + if (isset($parts[1])) { + if (mb_strlen($parts[1], '8bit') > 6) { + $parts[1] = mb_substr($parts[1], 0, 6, '8bit'); + } else { + $parts[1] = str_pad($parts[1], 6, '0', STR_PAD_RIGHT); + } + } + $formatted = DateTimeImmutable::createFromFormat('U.u', implode('.', $parts)); + + break; + default: + throw new InvalidArgumentException('Unable to normalize the object'); + } + + if ($formatted === false) { + throw new InvalidArgumentException('Invalid data. Cannot be converted into a datetime object'); + } + + return $formatted; + } +} diff --git a/3rdparty/spomky-labs/cbor-php/src/Tag/UnsignedBigIntegerTag.php b/3rdparty/spomky-labs/cbor-php/src/Tag/UnsignedBigIntegerTag.php new file mode 100644 index 00000000..686dd6f9 --- /dev/null +++ b/3rdparty/spomky-labs/cbor-php/src/Tag/UnsignedBigIntegerTag.php @@ -0,0 +1,50 @@ +object; + + return Utils::hexToString($object->normalize()); + } +} diff --git a/3rdparty/spomky-labs/cbor-php/src/Tag/UriTag.php b/3rdparty/spomky-labs/cbor-php/src/Tag/UriTag.php new file mode 100644 index 00000000..eb20911a --- /dev/null +++ b/3rdparty/spomky-labs/cbor-php/src/Tag/UriTag.php @@ -0,0 +1,49 @@ +object; + + return $object->normalize(); + } +} diff --git a/3rdparty/spomky-labs/cbor-php/src/TextStringObject.php b/3rdparty/spomky-labs/cbor-php/src/TextStringObject.php new file mode 100644 index 00000000..a0ca87a5 --- /dev/null +++ b/3rdparty/spomky-labs/cbor-php/src/TextStringObject.php @@ -0,0 +1,56 @@ +data = $data; + $this->length = $length; + } + + public function __toString(): string + { + $result = parent::__toString(); + if ($this->length !== null) { + $result .= $this->length; + } + + return $result . $this->data; + } + + public static function create(string $data): self + { + return new self($data); + } + + public function getValue(): string + { + return $this->data; + } + + public function getLength(): int + { + return mb_strlen($this->data, 'utf8'); + } + + public function normalize(): string + { + return $this->data; + } +} diff --git a/3rdparty/spomky-labs/cbor-php/src/UnsignedIntegerObject.php b/3rdparty/spomky-labs/cbor-php/src/UnsignedIntegerObject.php new file mode 100644 index 00000000..34833344 --- /dev/null +++ b/3rdparty/spomky-labs/cbor-php/src/UnsignedIntegerObject.php @@ -0,0 +1,118 @@ +data !== null) { + $result .= $this->data; + } + + return $result; + } + + public static function createObjectForValue(int $additionalInformation, ?string $data): self + { + return new self($additionalInformation, $data); + } + + public static function create(int $value): self + { + return self::createFromString((string) $value); + } + + public static function createFromHex(string $value): self + { + $integer = BigInteger::fromBase($value, 16); + + return self::createBigInteger($integer); + } + + public static function createFromString(string $value): self + { + $integer = BigInteger::of($value); + + return self::createBigInteger($integer); + } + + public function getMajorType(): int + { + return self::MAJOR_TYPE; + } + + public function getValue(): string + { + if ($this->data === null) { + return (string) $this->additionalInformation; + } + + $integer = BigInteger::fromBase(bin2hex($this->data), 16); + + return $integer->toBase(10); + } + + public function normalize(): string + { + return $this->getValue(); + } + + private static function createBigInteger(BigInteger $integer): self + { + if ($integer->isLessThan(BigInteger::zero())) { + throw new InvalidArgumentException('The value must be a positive integer.'); + } + + switch (true) { + case $integer->isLessThan(BigInteger::of(24)): + $ai = $integer->toInt(); + $data = null; + break; + case $integer->isLessThan(BigInteger::fromBase('FF', 16)): + $ai = 24; + $data = self::hex2bin(str_pad($integer->toBase(16), 2, '0', STR_PAD_LEFT)); + break; + case $integer->isLessThan(BigInteger::fromBase('FFFF', 16)): + $ai = 25; + $data = self::hex2bin(str_pad($integer->toBase(16), 4, '0', STR_PAD_LEFT)); + break; + case $integer->isLessThan(BigInteger::fromBase('FFFFFFFF', 16)): + $ai = 26; + $data = self::hex2bin(str_pad($integer->toBase(16), 8, '0', STR_PAD_LEFT)); + break; + default: + throw new InvalidArgumentException( + 'Out of range. Please use PositiveBigIntegerTag tag with ByteStringObject object instead.' + ); + } + + return new self($ai, $data); + } + + private static function hex2bin(string $data): string + { + $result = hex2bin($data); + if ($result === false) { + throw new InvalidArgumentException('Unable to convert the data'); + } + + return $result; + } +} diff --git a/3rdparty/spomky-labs/cbor-php/src/Utils.php b/3rdparty/spomky-labs/cbor-php/src/Utils.php new file mode 100644 index 00000000..b75343f0 --- /dev/null +++ b/3rdparty/spomky-labs/cbor-php/src/Utils.php @@ -0,0 +1,60 @@ +toInt(); + } + + public static function binToBigInteger(string $value): BigInteger + { + return self::hexToBigInteger(bin2hex($value)); + } + + public static function hexToInt(string $value): int + { + return self::hexToBigInteger($value)->toInt(); + } + + public static function hexToBigInteger(string $value): BigInteger + { + return BigInteger::fromBase($value, 16); + } + + public static function hexToString(string $value): string + { + return BigInteger::fromBase(bin2hex($value), 16)->toBase(10); + } + + public static function decode(string $data): string + { + $decoded = base64_decode(strtr($data, '-_', '+/'), true); + if ($decoded === false) { + throw new InvalidArgumentException('Invalid data provided'); + } + + return $decoded; + } + + /** + * @param mixed|null $data + */ + public static function assertString($data, ?string $message = null): void + { + if (! is_string($data)) { + throw new InvalidArgumentException($message ?? ''); + } + } +} diff --git a/3rdparty/spomky-labs/pki-framework/LICENSE b/3rdparty/spomky-labs/pki-framework/LICENSE new file mode 100644 index 00000000..d6feca7c --- /dev/null +++ b/3rdparty/spomky-labs/pki-framework/LICENSE @@ -0,0 +1,22 @@ +MIT License + +Copyright (c) 2016-2019 Joni Eskelinen +Copyright (c) 2022 Spomky-Labs + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. diff --git a/3rdparty/spomky-labs/pki-framework/src/ASN1/Component/Identifier.php b/3rdparty/spomky-labs/pki-framework/src/ASN1/Component/Identifier.php new file mode 100644 index 00000000..c2f15492 --- /dev/null +++ b/3rdparty/spomky-labs/pki-framework/src/ASN1/Component/Identifier.php @@ -0,0 +1,278 @@ + + */ + private const MAP_CLASS_TO_NAME = [ + self::CLASS_UNIVERSAL => 'UNIVERSAL', + self::CLASS_APPLICATION => 'APPLICATION', + self::CLASS_CONTEXT_SPECIFIC => 'CONTEXT SPECIFIC', + self::CLASS_PRIVATE => 'PRIVATE', + ]; + + /** + * Type class. + */ + private int $_class; + + /** + * Primitive or Constructed. + */ + private readonly int $_pc; + + /** + * Content type tag. + */ + private BigInt $_tag; + + /** + * @param int $class Type class + * @param int $pc Primitive / Constructed + * @param BigInteger|int $tag Type tag number + */ + private function __construct(int $class, int $pc, BigInteger|int $tag) + { + $this->_class = 0b11 & $class; + $this->_pc = 0b1 & $pc; + $this->_tag = BigInt::create($tag); + } + + public static function create(int $class, int $pc, BigInteger|int $tag): self + { + return new self($class, $pc, $tag); + } + + /** + * Decode identifier component from DER data. + * + * @param string $data DER encoded data + * @param null|int $offset Reference to the variable that contains offset + * into the data where to start parsing. + * Variable is updated to the offset next to the + * parsed identifier. If null, start from offset 0. + */ + public static function fromDER(string $data, int &$offset = null): self + { + $idx = $offset ?? 0; + $datalen = mb_strlen($data, '8bit'); + if ($idx >= $datalen) { + throw new DecodeException('Invalid offset.'); + } + $byte = ord($data[$idx++]); + // bits 8 and 7 (class) + // 0 = universal, 1 = application, 2 = context-specific, 3 = private + $class = (0b11000000 & $byte) >> 6; + // bit 6 (0 = primitive / 1 = constructed) + $pc = (0b00100000 & $byte) >> 5; + // bits 5 to 1 (tag number) + $tag = (0b00011111 & $byte); + // long-form identifier + if ($tag === 0x1f) { + $tag = self::decodeLongFormTag($data, $idx); + } + if (isset($offset)) { + $offset = $idx; + } + return self::create($class, $pc, $tag); + } + + public function toDER(): string + { + $bytes = []; + $byte = $this->_class << 6 | $this->_pc << 5; + $tag = $this->_tag->getValue(); + if ($tag->isLessThan(0x1f)) { + $bytes[] = $byte | $tag->toInt(); + } // long-form identifier + else { + $bytes[] = $byte | 0x1f; + $octets = []; + for (; $tag->isGreaterThan(0); $tag = $tag->shiftedRight(7)) { + $octets[] = 0x80 | $tag->and(0x7f)->toInt(); + } + // last octet has bit 8 set to zero + $octets[0] &= 0x7f; + foreach (array_reverse($octets) as $octet) { + $bytes[] = $octet; + } + } + return pack('C*', ...$bytes); + } + + /** + * Get class of the type. + */ + public function typeClass(): int + { + return $this->_class; + } + + public function pc(): int + { + return $this->_pc; + } + + /** + * Get the tag number. + * + * @return string Base 10 integer string + */ + public function tag(): string + { + return $this->_tag->base10(); + } + + /** + * Get the tag as an integer. + */ + public function intTag(): int + { + return $this->_tag->toInt(); + } + + /** + * Check whether type is of an universal class. + */ + public function isUniversal(): bool + { + return $this->_class === self::CLASS_UNIVERSAL; + } + + /** + * Check whether type is of an application class. + */ + public function isApplication(): bool + { + return $this->_class === self::CLASS_APPLICATION; + } + + /** + * Check whether type is of a context specific class. + */ + public function isContextSpecific(): bool + { + return $this->_class === self::CLASS_CONTEXT_SPECIFIC; + } + + /** + * Check whether type is of a private class. + */ + public function isPrivate(): bool + { + return $this->_class === self::CLASS_PRIVATE; + } + + /** + * Check whether content is primitive type. + */ + public function isPrimitive(): bool + { + return $this->_pc === self::PRIMITIVE; + } + + /** + * Check hether content is constructed type. + */ + public function isConstructed(): bool + { + return $this->_pc === self::CONSTRUCTED; + } + + /** + * Get self with given type class. + * + * @param int $class One of `CLASS_*` enumerations + */ + public function withClass(int $class): self + { + $obj = clone $this; + $obj->_class = 0b11 & $class; + return $obj; + } + + /** + * Get self with given type tag. + * + * @param int $tag Tag number + */ + public function withTag(int $tag): self + { + $obj = clone $this; + $obj->_tag = BigInt::create($tag); + return $obj; + } + + /** + * Get human readable name of the type class. + */ + public static function classToName(int $class): string + { + if (! array_key_exists($class, self::MAP_CLASS_TO_NAME)) { + return "CLASS {$class}"; + } + return self::MAP_CLASS_TO_NAME[$class]; + } + + /** + * Parse long form tag. + * + * @param string $data DER data + * @param int $offset Reference to the variable containing offset to data + * + * @return BigInteger Tag number + */ + private static function decodeLongFormTag(string $data, int &$offset): BigInteger + { + $datalen = mb_strlen($data, '8bit'); + $tag = BigInteger::of(0); + while (true) { + if ($offset >= $datalen) { + throw new DecodeException('Unexpected end of data while decoding long form identifier.'); + } + $byte = ord($data[$offset++]); + $tag = $tag->shiftedLeft(7); + $tag = $tag->or(0x7f & $byte); + // last byte has bit 8 set to zero + if ((0x80 & $byte) === 0) { + break; + } + } + return $tag; + } +} diff --git a/3rdparty/spomky-labs/pki-framework/src/ASN1/Component/Length.php b/3rdparty/spomky-labs/pki-framework/src/ASN1/Component/Length.php new file mode 100644 index 00000000..feb5ab31 --- /dev/null +++ b/3rdparty/spomky-labs/pki-framework/src/ASN1/Component/Length.php @@ -0,0 +1,204 @@ +_length = BigInt::create($length); + } + + public static function create(BigInteger|int $length, bool $_indefinite = false): self + { + return new self($length, $_indefinite); + } + + /** + * Decode length component from DER data. + * + * @param string $data DER encoded data + * @param null|int $offset Reference to the variable that contains offset + * into the data where to start parsing. + * Variable is updated to the offset next to the + * parsed length component. If null, start from offset 0. + */ + public static function fromDER(string $data, int &$offset = null): self + { + $idx = $offset ?? 0; + $datalen = mb_strlen($data, '8bit'); + if ($idx >= $datalen) { + throw new DecodeException('Unexpected end of data while decoding length.'); + } + $indefinite = false; + $byte = ord($data[$idx++]); + // bits 7 to 1 + $length = (0x7f & $byte); + // long form + if ((0x80 & $byte) !== 0) { + if ($length === 0) { + $indefinite = true; + } else { + if ($idx + $length > $datalen) { + throw new DecodeException('Unexpected end of data while decoding long form length.'); + } + $length = self::decodeLongFormLength($length, $data, $idx); + } + } + if (isset($offset)) { + $offset = $idx; + } + return self::create($length, $indefinite); + } + + /** + * Decode length from DER. + * + * Throws an exception if length doesn't match with expected or if data doesn't contain enough bytes. + * + * Requirement of definite length is relaxed contrary to the specification (sect. 10.1). + * + * @param string $data DER data + * @param int $offset Reference to the offset variable + * @param null|int $expected Expected length, null to bypass checking + * @see self::fromDER + */ + public static function expectFromDER(string $data, int &$offset, int $expected = null): self + { + $idx = $offset; + $length = self::fromDER($data, $idx); + // if certain length was expected + if (isset($expected)) { + if ($length->isIndefinite()) { + throw new DecodeException(sprintf('Expected length %d, got indefinite.', $expected)); + } + if ($expected !== $length->intLength()) { + throw new DecodeException(sprintf('Expected length %d, got %d.', $expected, $length->intLength())); + } + } + // check that enough data is available + if (! $length->isIndefinite() + && mb_strlen($data, '8bit') < $idx + $length->intLength()) { + throw new DecodeException( + sprintf( + 'Length %d overflows data, %d bytes left.', + $length->intLength(), + mb_strlen($data, '8bit') - $idx + ) + ); + } + $offset = $idx; + return $length; + } + + public function toDER(): string + { + $bytes = []; + if ($this->_indefinite) { + $bytes[] = 0x80; + } else { + $num = $this->_length->getValue(); + // long form + if ($num->isGreaterThan(127)) { + $octets = []; + for (; $num->isGreaterThan(0); $num = $num->shiftedRight(8)) { + $octets[] = BigInteger::of(0xff)->and($num)->toInt(); + } + $count = count($octets); + // first octet must not be 0xff + if ($count >= 127) { + throw new DomainException('Too many length octets.'); + } + $bytes[] = 0x80 | $count; + foreach (array_reverse($octets) as $octet) { + $bytes[] = $octet; + } + } // short form + else { + $bytes[] = $num->toInt(); + } + } + return pack('C*', ...$bytes); + } + + /** + * Get the length. + * + * @return string Length as an integer string + */ + public function length(): string + { + if ($this->_indefinite) { + throw new LogicException('Length is indefinite.'); + } + return $this->_length->base10(); + } + + /** + * Get the length as an integer. + */ + public function intLength(): int + { + if ($this->_indefinite) { + throw new LogicException('Length is indefinite.'); + } + return $this->_length->toInt(); + } + + /** + * Whether length is indefinite. + */ + public function isIndefinite(): bool + { + return $this->_indefinite; + } + + /** + * Decode long form length. + * + * @param int $length Number of octets + * @param string $data Data + * @param int $offset reference to the variable containing offset to the data + */ + private static function decodeLongFormLength(int $length, string $data, int &$offset): BigInteger + { + // first octet must not be 0xff (spec 8.1.3.5c) + if ($length === 127) { + throw new DecodeException('Invalid number of length octets.'); + } + $num = BigInteger::of(0); + while (--$length >= 0) { + $byte = ord($data[$offset++]); + $num = $num->shiftedLeft(8) + ->or($byte); + } + + return $num; + } +} diff --git a/3rdparty/spomky-labs/pki-framework/src/ASN1/DERData.php b/3rdparty/spomky-labs/pki-framework/src/ASN1/DERData.php new file mode 100644 index 00000000..52708189 --- /dev/null +++ b/3rdparty/spomky-labs/pki-framework/src/ASN1/DERData.php @@ -0,0 +1,81 @@ +identifier = Identifier::fromDER($data, $this->contentOffset); + // check that length encoding is valid + Length::expectFromDER($data, $this->contentOffset); + $this->der = $data; + parent::__construct($this->identifier->intTag()); + } + + public static function create(string $data): self + { + return new self($data); + } + + public function typeClass(): int + { + return $this->identifier->typeClass(); + } + + public function isConstructed(): bool + { + return $this->identifier->isConstructed(); + } + + public function toDER(): string + { + return $this->der; + } + + protected function encodedAsDER(): string + { + // if there's no content payload + if (mb_strlen($this->der, '8bit') === $this->contentOffset) { + return ''; + } + return mb_substr($this->der, $this->contentOffset, null, '8bit'); + } + + protected static function decodeFromDER(Identifier $identifier, string $data, int &$offset): ElementBase + { + throw new BadMethodCallException(__METHOD__ . ' must be implemented in derived class.'); + } +} diff --git a/3rdparty/spomky-labs/pki-framework/src/ASN1/Element.php b/3rdparty/spomky-labs/pki-framework/src/ASN1/Element.php new file mode 100644 index 00000000..3d05aaa8 --- /dev/null +++ b/3rdparty/spomky-labs/pki-framework/src/ASN1/Element.php @@ -0,0 +1,475 @@ + + */ + private const MAP_TAG_TO_CLASS = [ + self::TYPE_EOC => EOC::class, + self::TYPE_BOOLEAN => Boolean::class, + self::TYPE_INTEGER => Integer::class, + self::TYPE_BIT_STRING => BitString::class, + self::TYPE_OCTET_STRING => OctetString::class, + self::TYPE_NULL => NullType::class, + self::TYPE_OBJECT_IDENTIFIER => ObjectIdentifier::class, + self::TYPE_OBJECT_DESCRIPTOR => ObjectDescriptor::class, + self::TYPE_REAL => Real::class, + self::TYPE_ENUMERATED => Enumerated::class, + self::TYPE_UTF8_STRING => UTF8String::class, + self::TYPE_RELATIVE_OID => RelativeOID::class, + self::TYPE_SEQUENCE => Sequence::class, + self::TYPE_SET => Set::class, + self::TYPE_NUMERIC_STRING => NumericString::class, + self::TYPE_PRINTABLE_STRING => PrintableString::class, + self::TYPE_T61_STRING => T61String::class, + self::TYPE_VIDEOTEX_STRING => VideotexString::class, + self::TYPE_IA5_STRING => IA5String::class, + self::TYPE_UTC_TIME => UTCTime::class, + self::TYPE_GENERALIZED_TIME => GeneralizedTime::class, + self::TYPE_GRAPHIC_STRING => GraphicString::class, + self::TYPE_VISIBLE_STRING => VisibleString::class, + self::TYPE_GENERAL_STRING => GeneralString::class, + self::TYPE_UNIVERSAL_STRING => UniversalString::class, + self::TYPE_CHARACTER_STRING => CharacterString::class, + self::TYPE_BMP_STRING => BMPString::class, + ]; + + /** + * Mapping from universal type tag to human-readable name. + * + * @internal + * + * @var array + */ + private const MAP_TYPE_TO_NAME = [ + self::TYPE_EOC => 'EOC', + self::TYPE_BOOLEAN => 'BOOLEAN', + self::TYPE_INTEGER => 'INTEGER', + self::TYPE_BIT_STRING => 'BIT STRING', + self::TYPE_OCTET_STRING => 'OCTET STRING', + self::TYPE_NULL => 'NULL', + self::TYPE_OBJECT_IDENTIFIER => 'OBJECT IDENTIFIER', + self::TYPE_OBJECT_DESCRIPTOR => 'ObjectDescriptor', + self::TYPE_EXTERNAL => 'EXTERNAL', + self::TYPE_REAL => 'REAL', + self::TYPE_ENUMERATED => 'ENUMERATED', + self::TYPE_EMBEDDED_PDV => 'EMBEDDED PDV', + self::TYPE_UTF8_STRING => 'UTF8String', + self::TYPE_RELATIVE_OID => 'RELATIVE-OID', + self::TYPE_SEQUENCE => 'SEQUENCE', + self::TYPE_SET => 'SET', + self::TYPE_NUMERIC_STRING => 'NumericString', + self::TYPE_PRINTABLE_STRING => 'PrintableString', + self::TYPE_T61_STRING => 'T61String', + self::TYPE_VIDEOTEX_STRING => 'VideotexString', + self::TYPE_IA5_STRING => 'IA5String', + self::TYPE_UTC_TIME => 'UTCTime', + self::TYPE_GENERALIZED_TIME => 'GeneralizedTime', + self::TYPE_GRAPHIC_STRING => 'GraphicString', + self::TYPE_VISIBLE_STRING => 'VisibleString', + self::TYPE_GENERAL_STRING => 'GeneralString', + self::TYPE_UNIVERSAL_STRING => 'UniversalString', + self::TYPE_CHARACTER_STRING => 'CHARACTER STRING', + self::TYPE_BMP_STRING => 'BMPString', + self::TYPE_STRING => 'Any String', + self::TYPE_TIME => 'Any Time', + self::TYPE_CONSTRUCTED_STRING => 'Constructed String', + ]; + + /** + * @param bool $indefiniteLength Whether type shall be encoded with indefinite length. + */ + protected function __construct( + protected readonly int $typeTag, + protected bool $indefiniteLength = false + ) { + } + + abstract public function typeClass(): int; + + abstract public function isConstructed(): bool; + + /** + * Decode element from DER data. + * + * @param string $data DER encoded data + * @param null|int $offset Reference to the variable that contains offset + * into the data where to start parsing. + * Variable is updated to the offset next to the + * parsed element. If null, start from offset 0. + */ + public static function fromDER(string $data, int &$offset = null): static + { + $idx = $offset ?? 0; + // decode identifier + $identifier = Identifier::fromDER($data, $idx); + // determine class that implements type specific decoding + $cls = self::determineImplClass($identifier); + // decode remaining element + $element = $cls::decodeFromDER($identifier, $data, $idx); + // if called in the context of a concrete class, check + // that decoded type matches the type of calling class + $called_class = static::class; + if ($called_class !== self::class) { + if (! $element instanceof $called_class) { + throw new UnexpectedValueException(sprintf('%s expected, got %s.', $called_class, $element::class)); + } + } + // update offset for the caller + if (isset($offset)) { + $offset = $idx; + } + return $element; + } + + public function toDER(): string + { + $identifier = Identifier::create( + $this->typeClass(), + $this->isConstructed() ? Identifier::CONSTRUCTED : Identifier::PRIMITIVE, + $this->typeTag + ); + $content = $this->encodedAsDER(); + if ($this->indefiniteLength) { + $length = Length::create(0, true); + $eoc = EOC::create(); + return $identifier->toDER() . $length->toDER() . $content . $eoc->toDER(); + } + $length = Length::create(mb_strlen($content, '8bit')); + return $identifier->toDER() . $length->toDER() . $content; + } + + public function tag(): int + { + return $this->typeTag; + } + + public function isType(int $tag): bool + { + // if element is context specific + if ($this->typeClass() === Identifier::CLASS_CONTEXT_SPECIFIC) { + return false; + } + // negative tags identify an abstract pseudotype + if ($tag < 0) { + return $this->isPseudoType($tag); + } + return $this->isConcreteType($tag); + } + + public function expectType(int $tag): ElementBase + { + if (! $this->isType($tag)) { + throw new UnexpectedValueException( + sprintf('%s expected, got %s.', self::tagToName($tag), $this->typeDescriptorString()) + ); + } + return $this; + } + + public function isTagged(): bool + { + return $this instanceof TaggedType; + } + + public function expectTagged(?int $tag = null): TaggedType + { + if (! $this->isTagged()) { + throw new UnexpectedValueException( + sprintf('Context specific element expected, got %s.', Identifier::classToName($this->typeClass())) + ); + } + if (isset($tag) && $this->tag() !== $tag) { + throw new UnexpectedValueException(sprintf('Tag %d expected, got %d.', $tag, $this->tag())); + } + return $this; + } + + /** + * Whether element has indefinite length. + */ + public function hasIndefiniteLength(): bool + { + return $this->indefiniteLength; + } + + /** + * Get self with indefinite length encoding set. + * + * @param bool $indefinite True for indefinite length, false for definite length + */ + public function withIndefiniteLength(bool $indefinite = true): self + { + $obj = clone $this; + $obj->indefiniteLength = $indefinite; + return $obj; + } + + final public function asElement(): self + { + return $this; + } + + /** + * Get element decorated with `UnspecifiedType` object. + */ + public function asUnspecified(): UnspecifiedType + { + return UnspecifiedType::create($this); + } + + /** + * Get human readable name for an universal tag. + */ + public static function tagToName(int $tag): string + { + if (! array_key_exists($tag, self::MAP_TYPE_TO_NAME)) { + return "TAG {$tag}"; + } + return self::MAP_TYPE_TO_NAME[$tag]; + } + + /** + * Get the content encoded in DER. + * + * Returns the DER encoded content without identifier and length header octets. + */ + abstract protected function encodedAsDER(): string; + + /** + * Decode type-specific element from DER. + * + * @param Identifier $identifier Pre-parsed identifier + * @param string $data DER data + * @param int $offset Offset in data to the next byte after identifier + */ + abstract protected static function decodeFromDER(Identifier $identifier, string $data, int &$offset): ElementBase; + + /** + * Determine the class that implements the type. + * + * @return string Class name + */ + protected static function determineImplClass(Identifier $identifier): string + { + switch ($identifier->typeClass()) { + case Identifier::CLASS_UNIVERSAL: + $cls = self::determineUniversalImplClass($identifier->intTag()); + // constructed strings may be present in BER + if ($identifier->isConstructed() + && is_subclass_of($cls, StringType::class)) { + $cls = ConstructedString::class; + } + return $cls; + case Identifier::CLASS_CONTEXT_SPECIFIC: + return ContextSpecificType::class; + case Identifier::CLASS_APPLICATION: + return ApplicationType::class; + case Identifier::CLASS_PRIVATE: + return PrivateType::class; + } + throw new UnexpectedValueException(sprintf( + '%s %d not implemented.', + Identifier::classToName($identifier->typeClass()), + $identifier->tag() + )); + } + + /** + * Determine the class that implements an universal type of the given tag. + * + * @return string Class name + */ + protected static function determineUniversalImplClass(int $tag): string + { + if (! array_key_exists($tag, self::MAP_TAG_TO_CLASS)) { + throw new UnexpectedValueException("Universal tag {$tag} not implemented."); + } + return self::MAP_TAG_TO_CLASS[$tag]; + } + + /** + * Get textual description of the type for debugging purposes. + */ + protected function typeDescriptorString(): string + { + if ($this->typeClass() === Identifier::CLASS_UNIVERSAL) { + return self::tagToName($this->typeTag); + } + return sprintf('%s TAG %d', Identifier::classToName($this->typeClass()), $this->typeTag); + } + + /** + * Check whether the element is a concrete type of given tag. + */ + private function isConcreteType(int $tag): bool + { + // if tag doesn't match + if ($this->tag() !== $tag) { + return false; + } + // if type is universal check that instance is of a correct class + if ($this->typeClass() === Identifier::CLASS_UNIVERSAL) { + $cls = self::determineUniversalImplClass($tag); + if (! $this instanceof $cls) { + return false; + } + } + return true; + } + + /** + * Check whether the element is a pseudotype. + */ + private function isPseudoType(int $tag): bool + { + return match ($tag) { + self::TYPE_STRING => $this instanceof StringType, + self::TYPE_TIME => $this instanceof TimeType, + self::TYPE_CONSTRUCTED_STRING => $this instanceof ConstructedString, + default => false, + }; + } +} diff --git a/3rdparty/spomky-labs/pki-framework/src/ASN1/Exception/DecodeException.php b/3rdparty/spomky-labs/pki-framework/src/ASN1/Exception/DecodeException.php new file mode 100644 index 00000000..400ff9bb --- /dev/null +++ b/3rdparty/spomky-labs/pki-framework/src/ASN1/Exception/DecodeException.php @@ -0,0 +1,14 @@ +validateString($string)) { + throw new InvalidArgumentException(sprintf('Not a valid %s string.', self::tagToName($this->typeTag))); + } + $this->string = $string; + } + + public function __toString(): string + { + return $this->string(); + } + + /** + * Get the string value. + */ + public function string(): string + { + return $this->string; + } + + protected function encodedAsDER(): string + { + return $this->string; + } + + /** + * Check whether string is valid for the concrete type. + */ + protected function validateString(string $string): bool + { + // Override in derived classes + return true; + } +} diff --git a/3rdparty/spomky-labs/pki-framework/src/ASN1/Type/BaseTime.php b/3rdparty/spomky-labs/pki-framework/src/ASN1/Type/BaseTime.php new file mode 100644 index 00000000..61b4e116 --- /dev/null +++ b/3rdparty/spomky-labs/pki-framework/src/ASN1/Type/BaseTime.php @@ -0,0 +1,59 @@ +string(); + } + + /** + * Initialize from datetime string. + * + * @see http://php.net/manual/en/datetime.formats.php + * + * @param string $time Time string + */ + abstract public static function fromString(string $time): static; + + /** + * Get the date and time. + */ + public function dateTime(): DateTimeImmutable + { + return $this->dateTime; + } + + /** + * Get the date and time as a type specific string. + */ + public function string(): string + { + return $this->encodedAsDER(); + } +} diff --git a/3rdparty/spomky-labs/pki-framework/src/ASN1/Type/Constructed/ConstructedString.php b/3rdparty/spomky-labs/pki-framework/src/ASN1/Type/Constructed/ConstructedString.php new file mode 100644 index 00000000..f03cac79 --- /dev/null +++ b/3rdparty/spomky-labs/pki-framework/src/ASN1/Type/Constructed/ConstructedString.php @@ -0,0 +1,156 @@ +string(); + } + + /** + * Create from a list of string type elements. + * + * All strings must have the same type. + */ + public static function create(StringType ...$elements): self + { + if (count($elements) === 0) { + throw new LogicException('No elements, unable to determine type tag.'); + } + $tag = $elements[0]->tag(); + + return self::createWithTag($tag, ...$elements); + } + + /** + * Create from strings with a given type tag. + * + * Does not perform any validation on types. + * + * @param int $tag Type tag for the constructed string element + * @param StringType ...$elements Any number of elements + */ + public static function createWithTag(int $tag, StringType ...$elements): self + { + foreach ($elements as $el) { + if ($el->tag() !== $tag) { + throw new LogicException('All elements in constructed string must have the same type.'); + } + } + + return new self($tag, ...$elements); + } + + /** + * Get a list of strings in this structure. + * + * @return string[] + */ + public function strings(): array + { + return array_map(static fn (Element $el): string => $el->string(), $this->elements); + } + + /** + * Get the contained strings concatenated together. + * + * NOTE: It's unclear how bit strings with unused bits should be concatenated. + */ + public function string(): string + { + return implode('', $this->strings()); + } + + protected static function decodeFromDER(Identifier $identifier, string $data, int &$offset): self + { + if (! $identifier->isConstructed()) { + throw new DecodeException('Structured element must have constructed bit set.'); + } + $idx = $offset; + $length = Length::expectFromDER($data, $idx); + if ($length->isIndefinite()) { + $type = self::decodeIndefiniteLength($identifier->intTag(), $data, $idx); + } else { + $type = self::decodeDefiniteLength($identifier->intTag(), $data, $idx, $length->intLength()); + } + $offset = $idx; + + return $type; + } + + /** + * Decode elements for a definite length. + * + * @param string $data DER data + * @param int $offset Offset to data + * @param int $length Number of bytes to decode + */ + protected static function decodeDefiniteLength(int $typeTag, string $data, int &$offset, int $length): self + { + $idx = $offset; + $end = $idx + $length; + $elements = []; + while ($idx < $end) { + $elements[] = Element::fromDER($data, $idx); + // check that element didn't overflow length + if ($idx > $end) { + throw new DecodeException("Structure's content overflows length."); + } + } + $offset = $idx; + // return instance by static late binding + return self::createWithTag($typeTag, ...$elements); + } + + /** + * Decode elements for an indefinite length. + * + * @param string $data DER data + * @param int $offset Offset to data + */ + protected static function decodeIndefiniteLength(int $typeTag, string $data, int &$offset): self + { + $idx = $offset; + $elements = []; + $end = mb_strlen($data, '8bit'); + while (true) { + if ($idx >= $end) { + throw new DecodeException('Unexpected end of data while decoding indefinite length structure.'); + } + $el = Element::fromDER($data, $idx); + if ($el->isType(self::TYPE_EOC)) { + break; + } + $elements[] = $el; + } + $offset = $idx; + $type = self::createWithTag($typeTag, ...$elements); + $type->indefiniteLength = true; + return $type; + } +} diff --git a/3rdparty/spomky-labs/pki-framework/src/ASN1/Type/Constructed/Sequence.php b/3rdparty/spomky-labs/pki-framework/src/ASN1/Type/Constructed/Sequence.php new file mode 100644 index 00000000..8072f90a --- /dev/null +++ b/3rdparty/spomky-labs/pki-framework/src/ASN1/Type/Constructed/Sequence.php @@ -0,0 +1,92 @@ +isConstructed()) { + throw new DecodeException('Structured element must have constructed bit set.'); + } + $idx = $offset; + $length = Length::expectFromDER($data, $idx); + if ($length->isIndefinite()) { + $type = self::decodeIndefiniteLength($data, $idx); + } else { + $type = self::decodeDefiniteLength($data, $idx, $length->intLength()); + } + $offset = $idx; + return $type; + } + + /** + * Decode elements for a definite length. + * + * @param string $data DER data + * @param int $offset Offset to data + * @param int $length Number of bytes to decode + */ + protected static function decodeDefiniteLength(string $data, int &$offset, int $length): self + { + $idx = $offset; + $end = $idx + $length; + $elements = []; + while ($idx < $end) { + $elements[] = Element::fromDER($data, $idx); + // check that element didn't overflow length + if ($idx > $end) { + throw new DecodeException("Structure's content overflows length."); + } + } + $offset = $idx; + // return instance by static late binding + return self::create(...$elements); + } + + /** + * Decode elements for an indefinite length. + * + * @param string $data DER data + * @param int $offset Offset to data + */ + protected static function decodeIndefiniteLength(string $data, int &$offset): self + { + $idx = $offset; + $elements = []; + $end = mb_strlen($data, '8bit'); + while (true) { + if ($idx >= $end) { + throw new DecodeException('Unexpected end of data while decoding indefinite length structure.'); + } + $el = Element::fromDER($data, $idx); + if ($el->isType(self::TYPE_EOC)) { + break; + } + $elements[] = $el; + } + $offset = $idx; + $type = self::create(...$elements); + $type->indefiniteLength = true; + return $type; + } +} diff --git a/3rdparty/spomky-labs/pki-framework/src/ASN1/Type/Constructed/Set.php b/3rdparty/spomky-labs/pki-framework/src/ASN1/Type/Constructed/Set.php new file mode 100644 index 00000000..dadce1b4 --- /dev/null +++ b/3rdparty/spomky-labs/pki-framework/src/ASN1/Type/Constructed/Set.php @@ -0,0 +1,135 @@ +elements, + function (Element $a, Element $b) { + if ($a->typeClass() !== $b->typeClass()) { + return $a->typeClass() < $b->typeClass() ? -1 : 1; + } + return $a->tag() <=> $b->tag(); + } + ); + return $obj; + } + + /** + * Sort by encoding ascending order. + * + * Used for DER encoding of *SET OF* type. + */ + public function sortedSetOf(): self + { + $obj = clone $this; + usort( + $obj->elements, + function (Element $a, Element $b) { + $a_der = $a->toDER(); + $b_der = $b->toDER(); + return strcmp($a_der, $b_der); + } + ); + return $obj; + } + + /** + * @return self + */ + protected static function decodeFromDER(Identifier $identifier, string $data, int &$offset): ElementBase + { + if (! $identifier->isConstructed()) { + throw new DecodeException('Structured element must have constructed bit set.'); + } + $idx = $offset; + $length = Length::expectFromDER($data, $idx); + if ($length->isIndefinite()) { + $type = self::decodeIndefiniteLength($data, $idx); + } else { + $type = self::decodeDefiniteLength($data, $idx, $length->intLength()); + } + $offset = $idx; + return $type; + } + + /** + * Decode elements for a definite length. + * + * @param string $data DER data + * @param int $offset Offset to data + * @param int $length Number of bytes to decode + */ + protected static function decodeDefiniteLength(string $data, int &$offset, int $length): ElementBase + { + $idx = $offset; + $end = $idx + $length; + $elements = []; + while ($idx < $end) { + $elements[] = Element::fromDER($data, $idx); + // check that element didn't overflow length + if ($idx > $end) { + throw new DecodeException("Structure's content overflows length."); + } + } + $offset = $idx; + // return instance by static late binding + return self::create(...$elements); + } + + /** + * Decode elements for an indefinite length. + * + * @param string $data DER data + * @param int $offset Offset to data + */ + protected static function decodeIndefiniteLength(string $data, int &$offset): ElementBase + { + $idx = $offset; + $elements = []; + $end = mb_strlen($data, '8bit'); + while (true) { + if ($idx >= $end) { + throw new DecodeException('Unexpected end of data while decoding indefinite length structure.'); + } + $el = Element::fromDER($data, $idx); + if ($el->isType(self::TYPE_EOC)) { + break; + } + $elements[] = $el; + } + $offset = $idx; + $type = self::create(...$elements); + $type->indefiniteLength = true; + return $type; + } +} diff --git a/3rdparty/spomky-labs/pki-framework/src/ASN1/Type/Primitive/BMPString.php b/3rdparty/spomky-labs/pki-framework/src/ASN1/Type/Primitive/BMPString.php new file mode 100644 index 00000000..2c2dccdc --- /dev/null +++ b/3rdparty/spomky-labs/pki-framework/src/ASN1/Type/Primitive/BMPString.php @@ -0,0 +1,35 @@ +string(), '8bit') * 8 - $this->unusedBits; + } + + /** + * Get the number of unused bits in the last octet of the string. + */ + public function unusedBits(): int + { + return $this->unusedBits; + } + + /** + * Test whether bit is set. + * + * @param int $idx Bit index. Most significant bit of the first octet is index 0. + */ + public function testBit(int $idx): bool + { + // octet index + $oi = (int) floor($idx / 8); + // if octet is outside range + if ($oi < 0 || $oi >= mb_strlen($this->string(), '8bit')) { + throw new OutOfBoundsException('Index is out of bounds.'); + } + // bit index + $bi = $idx % 8; + // if tested bit is last octet's unused bit + if ($oi === mb_strlen($this->string(), '8bit') - 1) { + if ($bi >= 8 - $this->unusedBits) { + throw new OutOfBoundsException('Index refers to an unused bit.'); + } + } + $byte = $this->string()[$oi]; + // index 0 is the most significant bit in byte + $mask = 0x01 << (7 - $bi); + return (ord($byte) & $mask) > 0; + } + + /** + * Get range of bits. + * + * @param int $start Index of first bit + * @param int $length Number of bits in range + * + * @return string Integer of $length bits + */ + public function range(int $start, int $length): string + { + if ($length === 0) { + return '0'; + } + if ($start + $length > $this->numBits()) { + throw new OutOfBoundsException('Not enough bits.'); + } + $bits = BigInteger::of(0); + $idx = $start; + $end = $start + $length; + while (true) { + $bit = $this->testBit($idx) ? 1 : 0; + $bits = $bits->or($bit); + if (++$idx >= $end) { + break; + } + $bits = $bits->shiftedLeft(1); + } + return $bits->toBase(10); + } + + /** + * Get a copy of the bit string with trailing zeroes removed. + */ + public function withoutTrailingZeroes(): self + { + // if bit string was empty + if ($this->string() === '') { + return self::create(''); + } + $bits = $this->string(); + // count number of empty trailing octets + $unused_octets = 0; + for ($idx = mb_strlen($bits, '8bit') - 1; $idx >= 0; --$idx, ++$unused_octets) { + if ($bits[$idx] !== "\x0") { + break; + } + } + // strip trailing octets + if ($unused_octets !== 0) { + $bits = mb_substr($bits, 0, -$unused_octets, '8bit'); + } + // if bit string was full of zeroes + if ($bits === '') { + return self::create(''); + } + // count number of trailing zeroes in the last octet + $unused_bits = 0; + $byte = ord($bits[mb_strlen($bits, '8bit') - 1]); + while (0 === ($byte & 0x01)) { + ++$unused_bits; + $byte >>= 1; + } + return self::create($bits, $unused_bits); + } + + protected function encodedAsDER(): string + { + $der = chr($this->unusedBits); + $der .= $this->string(); + if ($this->unusedBits !== 0) { + $octet = $der[mb_strlen($der, '8bit') - 1]; + // set unused bits to zero + $octet &= chr(0xff & ~((1 << $this->unusedBits) - 1)); + $der[mb_strlen($der, '8bit') - 1] = $octet; + } + return $der; + } + + protected static function decodeFromDER(Identifier $identifier, string $data, int &$offset): ElementBase + { + $idx = $offset; + $length = Length::expectFromDER($data, $idx); + if ($length->intLength() < 1) { + throw new DecodeException('Bit string length must be at least 1.'); + } + $unused_bits = ord($data[$idx++]); + if ($unused_bits > 7) { + throw new DecodeException('Unused bits in a bit string must be less than 8.'); + } + $str_len = $length->intLength() - 1; + if ($str_len !== 0) { + $str = mb_substr($data, $idx, $str_len, '8bit'); + if ($unused_bits !== 0) { + $mask = (1 << $unused_bits) - 1; + if (($mask & ord($str[mb_strlen($str, '8bit') - 1])) !== 0) { + throw new DecodeException('DER encoded bit string must have zero padding.'); + } + } + } else { + $str = ''; + } + $offset = $idx + $str_len; + return self::create($str, $unused_bits); + } +} diff --git a/3rdparty/spomky-labs/pki-framework/src/ASN1/Type/Primitive/Boolean.php b/3rdparty/spomky-labs/pki-framework/src/ASN1/Type/Primitive/Boolean.php new file mode 100644 index 00000000..aa2e9b47 --- /dev/null +++ b/3rdparty/spomky-labs/pki-framework/src/ASN1/Type/Primitive/Boolean.php @@ -0,0 +1,62 @@ +_bool; + } + + protected function encodedAsDER(): string + { + return $this->_bool ? chr(0xff) : chr(0); + } + + protected static function decodeFromDER(Identifier $identifier, string $data, int &$offset): ElementBase + { + $idx = $offset; + Length::expectFromDER($data, $idx, 1); + $byte = ord($data[$idx++]); + if ($byte !== 0) { + if ($byte !== 0xff) { + throw new DecodeException('DER encoded boolean true must have all bits set to 1.'); + } + } + $offset = $idx; + return self::create($byte !== 0); + } +} diff --git a/3rdparty/spomky-labs/pki-framework/src/ASN1/Type/Primitive/CharacterString.php b/3rdparty/spomky-labs/pki-framework/src/ASN1/Type/Primitive/CharacterString.php new file mode 100644 index 00000000..d49fb89f --- /dev/null +++ b/3rdparty/spomky-labs/pki-framework/src/ASN1/Type/Primitive/CharacterString.php @@ -0,0 +1,26 @@ +isPrimitive()) { + throw new DecodeException('EOC value must be primitive.'); + } + // EOC type has always zero length + Length::expectFromDER($data, $idx, 0); + $offset = $idx; + return self::create(); + } +} diff --git a/3rdparty/spomky-labs/pki-framework/src/ASN1/Type/Primitive/Enumerated.php b/3rdparty/spomky-labs/pki-framework/src/ASN1/Type/Primitive/Enumerated.php new file mode 100644 index 00000000..ebba75f7 --- /dev/null +++ b/3rdparty/spomky-labs/pki-framework/src/ASN1/Type/Primitive/Enumerated.php @@ -0,0 +1,34 @@ +intLength(); + $bytes = mb_substr($data, $idx, $length, '8bit'); + $idx += $length; + $num = BigInt::fromSignedOctets($bytes)->getValue(); + $offset = $idx; + // late static binding since enumerated extends integer type + return self::create($num); + } +} diff --git a/3rdparty/spomky-labs/pki-framework/src/ASN1/Type/Primitive/GeneralString.php b/3rdparty/spomky-labs/pki-framework/src/ASN1/Type/Primitive/GeneralString.php new file mode 100644 index 00000000..a3ae69a0 --- /dev/null +++ b/3rdparty/spomky-labs/pki-framework/src/ASN1/Type/Primitive/GeneralString.php @@ -0,0 +1,32 @@ +_formatted = null; + } + + public static function create(DateTimeImmutable $dt): self + { + return new self($dt); + } + + public static function fromString(string $time, ?string $tz = null): static + { + return new static(new DateTimeImmutable($time, self::createTimeZone($tz))); + } + + protected function encodedAsDER(): string + { + if (! isset($this->_formatted)) { + $dt = $this->dateTime->setTimezone(new DateTimeZone('UTC')); + $this->_formatted = $dt->format('YmdHis'); + // if fractions were used + $frac = $dt->format('u'); + if (intval($frac) !== 0) { + $frac = rtrim($frac, '0'); + $this->_formatted .= ".{$frac}"; + } + // timezone + $this->_formatted .= 'Z'; + } + return $this->_formatted; + } + + protected static function decodeFromDER(Identifier $identifier, string $data, int &$offset): ElementBase + { + $idx = $offset; + $length = Length::expectFromDER($data, $idx)->intLength(); + $str = mb_substr($data, $idx, $length, '8bit'); + $idx += $length; + if (preg_match(self::REGEX, $str, $match) !== 1) { + throw new DecodeException('Invalid GeneralizedTime format.'); + } + [, $year, $month, $day, $hour, $minute, $second] = $match; + // if fractions match, there's at least one digit + if (isset($match[7])) { + $frac = $match[7]; + // DER restricts trailing zeroes in fractional seconds component + if ($frac[mb_strlen($frac, '8bit') - 1] === '0') { + throw new DecodeException('Fractional seconds must omit trailing zeroes.'); + } + } else { + $frac = '0'; + } + $time = $year . $month . $day . $hour . $minute . $second . '.' . $frac . + self::TZ_UTC; + $dt = DateTimeImmutable::createFromFormat('!YmdHis.uT', $time, new DateTimeZone('UTC')); + if ($dt === false) { + throw new DecodeException('Failed to decode GeneralizedTime'); + } + $offset = $idx; + return self::create($dt); + } + + /** + * Create `DateTimeZone` object from string. + */ + private static function createTimeZone(?string $tz): DateTimeZone + { + try { + return new DateTimeZone($tz ?? 'UTC'); + } catch (Throwable $e) { + throw new UnexpectedValueException('Invalid timezone.', 0, $e); + } + } +} diff --git a/3rdparty/spomky-labs/pki-framework/src/ASN1/Type/Primitive/GraphicString.php b/3rdparty/spomky-labs/pki-framework/src/ASN1/Type/Primitive/GraphicString.php new file mode 100644 index 00000000..7fba4155 --- /dev/null +++ b/3rdparty/spomky-labs/pki-framework/src/ASN1/Type/Primitive/GraphicString.php @@ -0,0 +1,32 @@ +_number = BigInt::create($number); + } + + public static function create(BigInteger|int|string $number): static + { + return new static($number, self::TYPE_INTEGER); + } + + /** + * Get the number as a base 10. + * + * @return string Integer as a string + */ + public function number(): string + { + return $this->_number->base10(); + } + + public function getValue(): BigInteger + { + return $this->_number->getValue(); + } + + /** + * Get the number as an integer type. + */ + public function intNumber(): int + { + return $this->_number->toInt(); + } + + protected function encodedAsDER(): string + { + return $this->_number->signedOctets(); + } + + protected static function decodeFromDER(Identifier $identifier, string $data, int &$offset): ElementBase + { + $idx = $offset; + $length = Length::expectFromDER($data, $idx)->intLength(); + $bytes = mb_substr($data, $idx, $length, '8bit'); + $idx += $length; + $num = BigInt::fromSignedOctets($bytes)->getValue(); + $offset = $idx; + // late static binding since enumerated extends integer type + return static::create($num); + } + + /** + * Test that number is valid for this context. + */ + private static function validateNumber(mixed $num): bool + { + if (is_int($num)) { + return true; + } + if (is_string($num) && preg_match('/-?\d+/', $num) === 1) { + return true; + } + if ($num instanceof BigInteger) { + return true; + } + return false; + } +} diff --git a/3rdparty/spomky-labs/pki-framework/src/ASN1/Type/Primitive/NullType.php b/3rdparty/spomky-labs/pki-framework/src/ASN1/Type/Primitive/NullType.php new file mode 100644 index 00000000..66f1671e --- /dev/null +++ b/3rdparty/spomky-labs/pki-framework/src/ASN1/Type/Primitive/NullType.php @@ -0,0 +1,49 @@ +isPrimitive()) { + throw new DecodeException('Null value must be primitive.'); + } + // null type has always zero length + Length::expectFromDER($data, $idx, 0); + $offset = $idx; + return self::create(); + } +} diff --git a/3rdparty/spomky-labs/pki-framework/src/ASN1/Type/Primitive/Number.php b/3rdparty/spomky-labs/pki-framework/src/ASN1/Type/Primitive/Number.php new file mode 100644 index 00000000..bbf5ed2d --- /dev/null +++ b/3rdparty/spomky-labs/pki-framework/src/ASN1/Type/Primitive/Number.php @@ -0,0 +1,88 @@ +number = BigInt::create($number); + } + + abstract public static function create(BigInteger|int|string $number): self; + + /** + * Get the number as a base 10. + * + * @return string Integer as a string + */ + public function number(): string + { + return $this->number->base10(); + } + + public function getValue(): BigInteger + { + return $this->number->getValue(); + } + + /** + * Get the number as an integer type. + */ + public function intNumber(): int + { + return $this->number->toInt(); + } + + protected function encodedAsDER(): string + { + return $this->number->signedOctets(); + } + + /** + * Test that number is valid for this context. + */ + private static function validateNumber(mixed $num): bool + { + if (is_int($num)) { + return true; + } + if (is_string($num) && preg_match('/-?\d+/', $num) === 1) { + return true; + } + if ($num instanceof BigInteger) { + return true; + } + return false; + } +} diff --git a/3rdparty/spomky-labs/pki-framework/src/ASN1/Type/Primitive/NumericString.php b/3rdparty/spomky-labs/pki-framework/src/ASN1/Type/Primitive/NumericString.php new file mode 100644 index 00000000..d48d4cc7 --- /dev/null +++ b/3rdparty/spomky-labs/pki-framework/src/ASN1/Type/Primitive/NumericString.php @@ -0,0 +1,31 @@ +string(); + } +} diff --git a/3rdparty/spomky-labs/pki-framework/src/ASN1/Type/Primitive/ObjectIdentifier.php b/3rdparty/spomky-labs/pki-framework/src/ASN1/Type/Primitive/ObjectIdentifier.php new file mode 100644 index 00000000..207842a6 --- /dev/null +++ b/3rdparty/spomky-labs/pki-framework/src/ASN1/Type/Primitive/ObjectIdentifier.php @@ -0,0 +1,198 @@ +subids = self::explodeDottedOID($oid); + // if OID is non-empty + if (count($this->subids) > 0) { + // check that at least two nodes are set + if (count($this->subids) < 2) { + throw new UnexpectedValueException('OID must have at least two nodes.'); + } + // check that root arc is in 0..2 range + if ($this->subids[0]->isGreaterThan(2)) { + throw new UnexpectedValueException('Root arc must be in range of 0..2.'); + } + // if root arc is 0 or 1, second node must be in 0..39 range + if ($this->subids[0]->isLessThan(2) && $this->subids[1]->isGreaterThanOrEqualTo(40)) { + throw new UnexpectedValueException('Second node must be in 0..39 range for root arcs 0 and 1.'); + } + } + parent::__construct($typeTag ?? self::TYPE_OBJECT_IDENTIFIER); + } + + public static function create(string $oid, ?int $typeTag = null): self + { + return new self($oid, $typeTag); + } + + /** + * Get OID in dotted format. + */ + public function oid(): string + { + return $this->oid; + } + + protected function encodedAsDER(): string + { + $subids = $this->subids; + // encode first two subids to one according to spec section 8.19.4 + if (count($subids) >= 2) { + $num = $subids[0]->multipliedBy(40)->plus($subids[1]); + array_splice($subids, 0, 2, [$num]); + } + return self::encodeSubIDs(...$subids); + } + + protected static function decodeFromDER(Identifier $identifier, string $data, int &$offset): ElementBase + { + $idx = $offset; + $len = Length::expectFromDER($data, $idx)->intLength(); + $subids = self::decodeSubIDs(mb_substr($data, $idx, $len, '8bit')); + $idx += $len; + // decode first subidentifier according to spec section 8.19.4 + if (isset($subids[0])) { + if ($subids[0]->isLessThan(80)) { + [$x, $y] = $subids[0]->quotientAndRemainder(40); + } else { + $x = BigInteger::of(2); + $y = $subids[0]->minus(80); + } + array_splice($subids, 0, 1, [$x, $y]); + } + $offset = $idx; + return self::create(self::implodeSubIDs(...$subids)); + } + + /** + * Explode dotted OID to an array of sub ID's. + * + * @param string $oid OID in dotted format + * + * @return BigInteger[] Array of BigInteger numbers + */ + protected static function explodeDottedOID(string $oid): array + { + $subids = []; + if ($oid !== '') { + foreach (explode('.', $oid) as $subid) { + try { + $n = BigInteger::of($subid); + $subids[] = $n; + } catch (Throwable $e) { + throw new UnexpectedValueException(sprintf('"%s" is not a number.', $subid), 0, $e); + } + } + } + return $subids; + } + + /** + * Implode an array of sub IDs to dotted OID format. + */ + protected static function implodeSubIDs(BigInteger ...$subids): string + { + return implode('.', array_map(static fn ($num) => $num->toBase(10), $subids)); + } + + /** + * Encode sub ID's to DER. + */ + protected static function encodeSubIDs(BigInteger ...$subids): string + { + $data = ''; + foreach ($subids as $subid) { + // if number fits to one base 128 byte + if ($subid->isLessThan(128)) { + $data .= chr($subid->toInt()); + } else { // encode to multiple bytes + $bytes = []; + do { + array_unshift($bytes, 0x7f & $subid->toInt()); + $subid = $subid->shiftedRight(7); + } while ($subid->isGreaterThan(0)); + // all bytes except last must have bit 8 set to one + foreach (array_splice($bytes, 0, -1) as $byte) { + $data .= chr(0x80 | $byte); + } + $byte = reset($bytes); + if (! is_int($byte)) { + throw new RuntimeException('Encoding failed'); + } + $data .= chr($byte); + } + } + return $data; + } + + /** + * Decode sub ID's from DER data. + * + * @return BigInteger[] Array of BigInteger numbers + */ + protected static function decodeSubIDs(string $data): array + { + $subids = []; + $idx = 0; + $end = mb_strlen($data, '8bit'); + while ($idx < $end) { + $num = BigInteger::of(0); + while (true) { + if ($idx >= $end) { + throw new DecodeException('Unexpected end of data.'); + } + $byte = ord($data[$idx++]); + $num = $num->or($byte & 0x7f); + // bit 8 of the last octet is zero + if (0 === ($byte & 0x80)) { + break; + } + $num = $num->shiftedLeft(7); + } + $subids[] = $num; + } + return $subids; + } +} diff --git a/3rdparty/spomky-labs/pki-framework/src/ASN1/Type/Primitive/OctetString.php b/3rdparty/spomky-labs/pki-framework/src/ASN1/Type/Primitive/OctetString.php new file mode 100644 index 00000000..20c16b38 --- /dev/null +++ b/3rdparty/spomky-labs/pki-framework/src/ASN1/Type/Primitive/OctetString.php @@ -0,0 +1,26 @@ +[+\-])?' . // sign + '(?\d+)' . // integer + '$/'; + + /** + * Regex pattern to parse NR2 form number. + * + * @var string + */ + final public const NR2_REGEX = '/^\s*' . + '(?[+\-])?' . // sign + '(?(?:\d+[\.,]\d*)|(?:\d*[\.,]\d+))' . // decimal number + '$/'; + + /** + * Regex pattern to parse NR3 form number. + * + * @var string + */ + final public const NR3_REGEX = '/^\s*' . + '(?[+\-])?' . // mantissa sign + '(?(?:\d+[\.,]\d*)|(?:\d*[\.,]\d+))' . // mantissa + '[Ee](?[+\-])?' . // exponent sign + '(?\d+)' . // exponent + '$/'; + + /** + * Regex pattern to parse PHP exponent number format. + * + * @see http://php.net/manual/en/language.types.float.php + * + * @var string + */ + final public const PHP_EXPONENT_DNUM = '/^' . + '(?[+\-])?' . // sign + '(?' . + '\d+' . // LNUM + '|' . + '(?:\d*\.\d+|\d+\.\d*)' . // DNUM + ')[eE]' . + '(?[+\-])?(?\d+)' . // exponent + '$/'; + + /** + * Exponent when value is positive or negative infinite. + * + * @var int + */ + final public const INF_EXPONENT = 2047; + + /** + * Exponent bias for IEEE 754 double precision float. + * + * @var int + */ + final public const EXP_BIAS = -1023; + + /** + * Signed integer mantissa. + */ + private readonly BigInt $_mantissa; + + /** + * Signed integer exponent. + */ + private readonly BigInt $_exponent; + + /** + * Abstract value base. + * + * Must be 2 or 10. + */ + private readonly int $_base; + + /** + * Whether to encode strictly in DER. + */ + private bool $_strictDer; + + /** + * Number as a native float. + * + * @internal Lazily initialized + */ + private ?float $_float = null; + + /** + * @param BigInteger|int|string $mantissa Integer mantissa + * @param BigInteger|int|string $exponent Integer exponent + * @param int $base Base, 2 or 10 + */ + private function __construct(BigInteger|int|string $mantissa, BigInteger|int|string $exponent, int $base = 10) + { + if ($base !== 10 && $base !== 2) { + throw new UnexpectedValueException('Base must be 2 or 10.'); + } + parent::__construct(self::TYPE_REAL); + $this->_strictDer = true; + $this->_mantissa = BigInt::create($mantissa); + $this->_exponent = BigInt::create($exponent); + $this->_base = $base; + } + + public function __toString(): string + { + return sprintf('%g', $this->floatVal()); + } + + public static function create( + BigInteger|int|string $mantissa, + BigInteger|int|string $exponent, + int $base = 10 + ): self { + return new self($mantissa, $exponent, $base); + } + + /** + * Create base 2 real number from float. + */ + public static function fromFloat(float $number): self + { + if (is_infinite($number)) { + return self::fromInfinite($number); + } + if (is_nan($number)) { + throw new UnexpectedValueException('NaN values not supported.'); + } + [$m, $e] = self::parse754Double(pack('E', $number)); + return self::create($m, $e, 2); + } + + /** + * Create base 10 real number from string. + * + * @param string $number Real number in base-10 textual form + */ + public static function fromString(string $number): self + { + [$m, $e] = self::parseString($number); + return self::create($m, $e, 10); + } + + /** + * Get self with strict DER flag set or unset. + * + * @param bool $strict whether to encode strictly in DER + */ + public function withStrictDER(bool $strict): self + { + $obj = clone $this; + $obj->_strictDer = $strict; + return $obj; + } + + /** + * Get the mantissa. + */ + public function mantissa(): BigInt + { + return $this->_mantissa; + } + + /** + * Get the exponent. + */ + public function exponent(): BigInt + { + return $this->_exponent; + } + + /** + * Get the base. + */ + public function base(): int + { + return $this->_base; + } + + /** + * Get number as a float. + */ + public function floatVal(): float + { + if (! isset($this->_float)) { + $m = $this->_mantissa->toInt(); + $e = $this->_exponent->toInt(); + $this->_float = (float) ($m * $this->_base ** $e); + } + return $this->_float; + } + + /** + * Get number as a NR3 form string conforming to DER rules. + */ + public function nr3Val(): string + { + // convert to base 10 + if ($this->_base === 2) { + [$m, $e] = self::parseString(sprintf('%15E', $this->floatVal())); + } else { + $m = $this->_mantissa->getValue(); + $e = $this->_exponent->getValue(); + } + $zero = BigInteger::of(0); + $ten = BigInteger::of(10); + + // shift trailing zeroes from the mantissa to the exponent + // (X.690 07-2002, section 11.3.2.4) + while (! $m->isEqualTo($zero) && $m->mod($ten)->isEqualTo($zero)) { + $m = $m->dividedBy($ten); + $e = $e->plus(1); + } + // if exponent is zero, it must be prefixed with a "+" sign + // (X.690 07-2002, section 11.3.2.6) + if ($e->isEqualTo(0)) { + $es = '+'; + } else { + $es = $e->isLessThan(0) ? '-' : ''; + } + return sprintf('%s.E%s%s', $m->toBase(10), $es, $e->abs()->toBase(10)); + } + + protected function encodedAsDER(): string + { + $infExponent = BigInteger::of(self::INF_EXPONENT); + if ($this->_exponent->getValue()->isEqualTo($infExponent)) { + return $this->encodeSpecial(); + } + // if the real value is the value zero, there shall be no contents + // octets in the encoding. (X.690 07-2002, section 8.5.2) + if ($this->_mantissa->getValue()->toBase(10) === '0') { + return ''; + } + if ($this->_base === 10) { + return $this->encodeDecimal(); + } + return $this->encodeBinary(); + } + + /** + * Encode in binary format. + */ + protected function encodeBinary(): string + { + /** @var BigInteger $m */ + /** @var BigInteger $e */ + /** @var int $sign */ + [$base, $sign, $m, $e] = $this->prepareBinaryEncoding(); + $zero = BigInteger::of(0); + $byte = 0x80; + if ($sign < 0) { + $byte |= 0x40; + } + // normalization: mantissa must be 0 or odd + if ($base === 2) { + // while last bit is zero + while ($m->isGreaterThan(0) && $m->and(0x01)->isEqualTo($zero)) { + $m = $m->shiftedRight(1); + $e = $e->plus(1); + } + } elseif ($base === 8) { + $byte |= 0x10; + // while last 3 bits are zero + while ($m->isGreaterThan(0) && $m->and(0x07)->isEqualTo($zero)) { + $m = $m->shiftedRight(3); + $e = $e->plus(1); + } + } else { // base === 16 + $byte |= 0x20; + // while last 4 bits are zero + while ($m->isGreaterThan(0) && $m->and(0x0f)->isEqualTo($zero)) { + $m = $m->shiftedRight(4); + $e = $e->plus(1); + } + } + // scale factor + $scale = 0; + while ($m->isGreaterThan(0) && $m->and(0x01)->isEqualTo($zero)) { + $m = $m->shiftedRight(1); + ++$scale; + } + $byte |= ($scale & 0x03) << 2; + // encode exponent + $exp_bytes = (BigInt::create($e))->signedOctets(); + $exp_len = mb_strlen($exp_bytes, '8bit'); + if ($exp_len > 0xff) { + throw new RangeException('Exponent encoding is too long.'); + } + if ($exp_len <= 3) { + $byte |= ($exp_len - 1) & 0x03; + $bytes = chr($byte); + } else { + $byte |= 0x03; + $bytes = chr($byte) . chr($exp_len); + } + $bytes .= $exp_bytes; + // encode mantissa + $bytes .= (BigInt::create($m))->unsignedOctets(); + return $bytes; + } + + /** + * Encode in decimal format. + */ + protected function encodeDecimal(): string + { + // encode in NR3 decimal encoding + return chr(0x03) . $this->nr3Val(); + } + + /** + * Encode special value. + */ + protected function encodeSpecial(): string + { + return match ($this->_mantissa->toInt()) { + 1 => chr(0x40), + -1 => chr(0x41), + default => throw new LogicException('Invalid special value.'), + }; + } + + protected static function decodeFromDER(Identifier $identifier, string $data, int &$offset): ElementBase + { + $idx = $offset; + $length = Length::expectFromDER($data, $idx)->intLength(); + // if length is zero, value is zero (spec 8.5.2) + if ($length === 0) { + $obj = self::create(0, 0, 10); + } else { + $bytes = mb_substr($data, $idx, $length, '8bit'); + $byte = ord($bytes[0]); + if ((0x80 & $byte) !== 0) { // bit 8 = 1 + $obj = self::decodeBinaryEncoding($bytes); + } elseif ($byte >> 6 === 0x00) { // bit 8 = 0, bit 7 = 0 + $obj = self::decodeDecimalEncoding($bytes); + } else { // bit 8 = 0, bit 7 = 1 + $obj = self::decodeSpecialRealValue($bytes); + } + } + $offset = $idx + $length; + return $obj; + } + + /** + * Decode binary encoding. + */ + protected static function decodeBinaryEncoding(string $data): self + { + $byte = ord($data[0]); + // bit 7 is set if mantissa is negative + $neg = (bool) (0x40 & $byte); + $base = match (($byte >> 4) & 0x03) { + 0b00 => 2, + 0b01 => 8, + 0b10 => 16, + default => throw new DecodeException('Reserved REAL binary encoding base not supported.'), + }; + // scaling factor in bits 4 and 3 + $scale = ($byte >> 2) & 0x03; + $idx = 1; + // content length in bits 2 and 1 + $len = ($byte & 0x03) + 1; + // if both bits are set, the next octet encodes the length + if ($len > 3) { + if (mb_strlen($data, '8bit') < 2) { + throw new DecodeException('Unexpected end of data while decoding REAL exponent length.'); + } + $len = ord($data[1]); + $idx = 2; + } + if (mb_strlen($data, '8bit') < $idx + $len) { + throw new DecodeException('Unexpected end of data while decoding REAL exponent.'); + } + // decode exponent + $octets = mb_substr($data, $idx, $len, '8bit'); + $exp = BigInt::fromSignedOctets($octets)->getValue(); + if ($base === 8) { + $exp = $exp->multipliedBy(3); + } elseif ($base === 16) { + $exp = $exp->multipliedBy(4); + } + if (mb_strlen($data, '8bit') <= $idx + $len) { + throw new DecodeException('Unexpected end of data while decoding REAL mantissa.'); + } + // decode mantissa + $octets = mb_substr($data, $idx + $len, null, '8bit'); + $n = BigInt::fromUnsignedOctets($octets)->getValue(); + $n = $n->multipliedBy(2 ** $scale); + if ($neg) { + $n = $n->negated(); + } + return self::create($n, $exp, 2); + } + + /** + * Decode decimal encoding. + */ + protected static function decodeDecimalEncoding(string $data): self + { + $nr = ord($data[0]) & 0x3f; + if (! in_array($nr, [1, 2, 3], true)) { + throw new DecodeException('Unsupported decimal encoding form.'); + } + $str = mb_substr($data, 1, null, '8bit'); + return self::fromString($str); + } + + /** + * Decode special encoding. + */ + protected static function decodeSpecialRealValue(string $data): self + { + if (mb_strlen($data, '8bit') !== 1) { + throw new DecodeException('SpecialRealValue must have one content octet.'); + } + $byte = ord($data[0]); + if ($byte === 0x40) { // positive infinity + return self::fromInfinite(INF); + } + if ($byte === 0x41) { // negative infinity + return self::fromInfinite(-INF); + } + throw new DecodeException('Invalid SpecialRealValue encoding.'); + } + + /** + * Prepare value for binary encoding. + * + * @return array (int) base, (int) sign, (BigInteger) mantissa and (BigInteger) exponent + */ + protected function prepareBinaryEncoding(): array + { + $base = 2; + $m = $this->_mantissa->getValue(); + $ms = $m->getSign(); + $m = BigInteger::of($m->abs()); + $e = $this->_exponent->getValue(); + $es = $e->getSign(); + $e = BigInteger::of($e->abs()); + $zero = BigInteger::of(0); + $three = BigInteger::of(3); + $four = BigInteger::of(4); + // DER uses only base 2 binary encoding + if (! $this->_strictDer) { + if ($e->mod($four)->isEqualTo($zero)) { + $base = 16; + $e = $e->dividedBy(4); + } elseif ($e->mod($three)->isEqualTo($zero)) { + $base = 8; + $e = $e->dividedBy(3); + } + } + return [$base, $ms, $m, $e->multipliedBy($es)]; + } + + /** + * Initialize from INF or -INF. + */ + private static function fromInfinite(float $inf): self + { + return self::create($inf === -INF ? -1 : 1, self::INF_EXPONENT, 2); + } + + /** + * Parse IEEE 754 big endian formatted double precision float to base 2 mantissa and exponent. + * + * @param string $octets 64 bits + * + * @return BigInteger[] Tuple of mantissa and exponent + */ + private static function parse754Double(string $octets): array + { + $n = BigInteger::fromBytes($octets, false); + // sign bit + $neg = $n->testBit(63); + // 11 bits of biased exponent + $exponentMask = BigInteger::fromBase('7ff0000000000000', 16); + $exp = $n->and($exponentMask) + ->shiftedRight(52) + ->plus(self::EXP_BIAS); + + // 52 bits of mantissa + $mantissaMask = BigInteger::fromBase('fffffffffffff', 16); + $man = $n->and($mantissaMask); + // zero, ASN.1 doesn't differentiate -0 from +0 + $zero = BigInteger::of(0); + if ($exp->isEqualTo(self::EXP_BIAS) && $man->isEqualTo($zero)) { + return [BigInteger::of(0), BigInteger::of(0)]; + } + // denormalized value, shift binary point + if ($exp->isEqualTo(self::EXP_BIAS)) { + $exp = $exp->plus(1); + } // normalized value, insert implicit leading one before the binary point + else { + $man = $man->or(BigInteger::of(1)->shiftedLeft(52)); + } + + // find the last fraction bit that is set + $last = 0; + while (! $man->testBit($last) && $last !== 52) { + $last++; + } + + $bits_for_fraction = 52 - $last; + // adjust mantissa and exponent so that we have integer values + $man = $man->shiftedRight($last); + $exp = $exp->minus($bits_for_fraction); + // negate mantissa if number was negative + if ($neg) { + $man = $man->negated(); + } + return [$man, $exp]; + } + + /** + * Parse textual REAL number to base 10 mantissa and exponent. + * + * @param string $str Number + * + * @return BigInteger[] Tuple of mantissa and exponent + */ + private static function parseString(string $str): array + { + // PHP exponent format + if (preg_match(self::PHP_EXPONENT_DNUM, $str, $match) === 1) { + [$m, $e] = self::parsePHPExponentMatch($match); + } // NR3 format + elseif (preg_match(self::NR3_REGEX, $str, $match) === 1) { + [$m, $e] = self::parseNR3Match($match); + } // NR2 format + elseif (preg_match(self::NR2_REGEX, $str, $match) === 1) { + [$m, $e] = self::parseNR2Match($match); + } // NR1 format + elseif (preg_match(self::NR1_REGEX, $str, $match) === 1) { + [$m, $e] = self::parseNR1Match($match); + } // invalid number + else { + throw new UnexpectedValueException("{$str} could not be parsed to REAL."); + } + // normalize so that mantissa has no trailing zeroes + $zero = BigInteger::of(0); + $ten = BigInteger::of(10); + while (! $m->isEqualTo($zero) && $m->mod($ten)->isEqualTo($zero)) { + $m = $m->dividedBy($ten); + $e = $e->plus(1); + } + return [$m, $e]; + } + + /** + * Parse PHP form float to base 10 mantissa and exponent. + * + * @param array $match Regexp match + * + * @return BigInteger[] Tuple of mantissa and exponent + */ + private static function parsePHPExponentMatch(array $match): array + { + // mantissa sign + $ms = $match['ms'] === '-' ? -1 : 1; + $m_parts = explode('.', $match['m']); + // integer part of the mantissa + $int = ltrim($m_parts[0], '0'); + // exponent sign + $es = $match['es'] === '-' ? -1 : 1; + // signed exponent + $e = BigInteger::of($match['e'])->multipliedBy($es); + // if mantissa had fractional part + if (count($m_parts) === 2) { + $frac = rtrim($m_parts[1], '0'); + $e = $e->minus(mb_strlen($frac, '8bit')); + $int .= $frac; + } + $m = BigInteger::of($int)->multipliedBy($ms); + return [$m, $e]; + } + + /** + * Parse NR3 form number to base 10 mantissa and exponent. + * + * @param array $match Regexp match + * + * @return BigInteger[] Tuple of mantissa and exponent + */ + private static function parseNR3Match(array $match): array + { + // mantissa sign + $ms = $match['ms'] === '-' ? -1 : 1; + // explode mantissa to integer and fraction parts + [$int, $frac] = explode('.', str_replace(',', '.', $match['m'])); + $int = ltrim($int, '0'); + $frac = rtrim($frac, '0'); + // exponent sign + $es = $match['es'] === '-' ? -1 : 1; + // signed exponent + $e = BigInteger::of($match['e'])->multipliedBy($es); + // shift exponent by the number of base 10 fractions + $e = $e->minus(mb_strlen($frac, '8bit')); + // insert fractions to integer part and produce signed mantissa + $int .= $frac; + if ($int === '') { + $int = '0'; + } + $m = BigInteger::of($int)->multipliedBy($ms); + return [$m, $e]; + } + + /** + * Parse NR2 form number to base 10 mantissa and exponent. + * + * @param array $match Regexp match + * + * @return BigInteger[] Tuple of mantissa and exponent + */ + private static function parseNR2Match(array $match): array + { + $sign = $match['s'] === '-' ? -1 : 1; + // explode decimal number to integer and fraction parts + [$int, $frac] = explode('.', str_replace(',', '.', $match['d'])); + $int = ltrim($int, '0'); + $frac = rtrim($frac, '0'); + // shift exponent by the number of base 10 fractions + $e = BigInteger::of(0); + $e = $e->minus(mb_strlen($frac, '8bit')); + // insert fractions to integer part and produce signed mantissa + $int .= $frac; + if ($int === '') { + $int = '0'; + } + $m = BigInteger::of($int)->multipliedBy($sign); + return [$m, $e]; + } + + /** + * Parse NR1 form number to base 10 mantissa and exponent. + * + * @param array $match Regexp match + * + * @return BigInteger[] Tuple of mantissa and exponent + */ + private static function parseNR1Match(array $match): array + { + $sign = $match['s'] === '-' ? -1 : 1; + $int = ltrim($match['i'], '0'); + if ($int === '') { + $int = '0'; + } + $m = BigInteger::of($int)->multipliedBy($sign); + return [$m, BigInteger::of(0)]; + } +} diff --git a/3rdparty/spomky-labs/pki-framework/src/ASN1/Type/Primitive/RelativeOID.php b/3rdparty/spomky-labs/pki-framework/src/ASN1/Type/Primitive/RelativeOID.php new file mode 100644 index 00000000..f09ac6b8 --- /dev/null +++ b/3rdparty/spomky-labs/pki-framework/src/ASN1/Type/Primitive/RelativeOID.php @@ -0,0 +1,163 @@ +subids = self::explodeDottedOID($oid); + } + + public static function create(string $oid): self + { + return new self($oid); + } + + /** + * Get OID in dotted format. + */ + public function oid(): string + { + return $this->oid; + } + + protected function encodedAsDER(): string + { + return self::encodeSubIDs(...$this->subids); + } + + protected static function decodeFromDER(Identifier $identifier, string $data, int &$offset): ElementBase + { + $idx = $offset; + $len = Length::expectFromDER($data, $idx)->intLength(); + $subids = self::decodeSubIDs(mb_substr($data, $idx, $len, '8bit')); + $offset = $idx + $len; + return self::create(self::implodeSubIDs(...$subids)); + } + + /** + * Explode dotted OID to an array of sub ID's. + * + * @param string $oid OID in dotted format + * + * @return BigInteger[] Array of BigInteger numbers + */ + protected static function explodeDottedOID(string $oid): array + { + $subids = []; + if ($oid !== '') { + foreach (explode('.', $oid) as $subid) { + try { + $n = BigInteger::of($subid); + $subids[] = $n; + } catch (Throwable $e) { + throw new UnexpectedValueException(sprintf('"%s" is not a number.', $subid), 0, $e); + } + } + } + return $subids; + } + + /** + * Implode an array of sub IDs to dotted OID format. + */ + protected static function implodeSubIDs(BigInteger ...$subids): string + { + return implode('.', array_map(static fn ($num) => $num->toBase(10), $subids)); + } + + /** + * Encode sub ID's to DER. + */ + protected static function encodeSubIDs(BigInteger ...$subids): string + { + $data = ''; + foreach ($subids as $subid) { + // if number fits to one base 128 byte + if ($subid->isLessThan(128)) { + $data .= chr($subid->toInt()); + } else { // encode to multiple bytes + $bytes = []; + do { + array_unshift($bytes, 0x7f & $subid->toInt()); + $subid = $subid->shiftedRight(7); + } while ($subid->isGreaterThan(0)); + // all bytes except last must have bit 8 set to one + foreach (array_splice($bytes, 0, -1) as $byte) { + $data .= chr(0x80 | $byte); + } + $byte = reset($bytes); + if (! is_int($byte)) { + throw new RuntimeException('Encoding failed'); + } + $data .= chr($byte); + } + } + return $data; + } + + /** + * Decode sub ID's from DER data. + * + * @return BigInteger[] Array of BigInteger numbers + */ + protected static function decodeSubIDs(string $data): array + { + $subids = []; + $idx = 0; + $end = mb_strlen($data, '8bit'); + while ($idx < $end) { + $num = BigInteger::of(0); + while (true) { + if ($idx >= $end) { + throw new DecodeException('Unexpected end of data.'); + } + $byte = ord($data[$idx++]); + $num = $num->or($byte & 0x7f); + // bit 8 of the last octet is zero + if (0 === ($byte & 0x80)) { + break; + } + $num = $num->shiftedLeft(7); + } + $subids[] = $num; + } + return $subids; + } +} diff --git a/3rdparty/spomky-labs/pki-framework/src/ASN1/Type/Primitive/T61String.php b/3rdparty/spomky-labs/pki-framework/src/ASN1/Type/Primitive/T61String.php new file mode 100644 index 00000000..92ac2f29 --- /dev/null +++ b/3rdparty/spomky-labs/pki-framework/src/ASN1/Type/Primitive/T61String.php @@ -0,0 +1,33 @@ +dateTime->setTimezone(new DateTimeZone('UTC')); + return $dt->format('ymdHis\\Z'); + } + + protected static function decodeFromDER(Identifier $identifier, string $data, int &$offset): ElementBase + { + $idx = $offset; + $length = Length::expectFromDER($data, $idx)->intLength(); + $str = mb_substr($data, $idx, $length, '8bit'); + $idx += $length; + if (preg_match(self::REGEX, $str, $match) !== 1) { + throw new DecodeException('Invalid UTCTime format.'); + } + [, $year, $month, $day, $hour, $minute, $second] = $match; + $time = $year . $month . $day . $hour . $minute . $second . self::TZ_UTC; + $dt = DateTimeImmutable::createFromFormat('!ymdHisT', $time, new DateTimeZone('UTC')); + if ($dt === false) { + throw new DecodeException('Failed to decode UTCTime'); + } + $offset = $idx; + return self::create($dt); + } +} diff --git a/3rdparty/spomky-labs/pki-framework/src/ASN1/Type/Primitive/UTF8String.php b/3rdparty/spomky-labs/pki-framework/src/ASN1/Type/Primitive/UTF8String.php new file mode 100644 index 00000000..8d40315a --- /dev/null +++ b/3rdparty/spomky-labs/pki-framework/src/ASN1/Type/Primitive/UTF8String.php @@ -0,0 +1,33 @@ +isPrimitive()) { + throw new DecodeException('DER encoded string must be primitive.'); + } + $length = Length::expectFromDER($data, $idx)->intLength(); + $str = $length === 0 ? '' : mb_substr($data, $idx, $length, '8bit'); + $offset = $idx + $length; + try { + return static::create($str); + } catch (InvalidArgumentException $e) { + throw new DecodeException($e->getMessage(), 0, $e); + } + } +} diff --git a/3rdparty/spomky-labs/pki-framework/src/ASN1/Type/PrimitiveType.php b/3rdparty/spomky-labs/pki-framework/src/ASN1/Type/PrimitiveType.php new file mode 100644 index 00000000..4a3ac2fe --- /dev/null +++ b/3rdparty/spomky-labs/pki-framework/src/ASN1/Type/PrimitiveType.php @@ -0,0 +1,19 @@ +elements = array_map(static fn (ElementBase $el) => $el->asElement(), $elements); + } + + /** + * Clone magic method. + */ + public function __clone() + { + // clear cache-variables + $this->taggedMap = null; + $this->unspecifiedTypes = null; + } + + public function isConstructed(): bool + { + return true; + } + + /** + * Explode DER structure to DER encoded components that it contains. + * + * @return string[] + */ + public static function explodeDER(string $data): array + { + $offset = 0; + $identifier = Identifier::fromDER($data, $offset); + if (! $identifier->isConstructed()) { + throw new DecodeException('Element is not constructed.'); + } + $length = Length::expectFromDER($data, $offset); + if ($length->isIndefinite()) { + throw new DecodeException('Explode not implemented for indefinite length encoding.'); + } + $end = $offset + $length->intLength(); + $parts = []; + while ($offset < $end) { + // start of the element + $idx = $offset; + // skip identifier + Identifier::fromDER($data, $offset); + // decode element length + $length = Length::expectFromDER($data, $offset)->intLength(); + // extract der encoding of the element + $parts[] = mb_substr($data, $idx, $offset - $idx + $length, '8bit'); + // update offset over content + $offset += $length; + } + return $parts; + } + + /** + * Get self with an element at the given index replaced by another. + * + * @param int $idx Element index + * @param Element $el New element to insert into the structure + */ + public function withReplaced(int $idx, Element $el): self + { + if (! isset($this->elements[$idx])) { + throw new OutOfBoundsException("Structure doesn't have element at index {$idx}."); + } + $obj = clone $this; + $obj->elements[$idx] = $el; + return $obj; + } + + /** + * Get self with an element inserted before the given index. + * + * @param int $idx Element index + * @param Element $el New element to insert into the structure + */ + public function withInserted(int $idx, Element $el): self + { + if (count($this->elements) < $idx || $idx < 0) { + throw new OutOfBoundsException("Index {$idx} is out of bounds."); + } + $obj = clone $this; + array_splice($obj->elements, $idx, 0, [$el]); + return $obj; + } + + /** + * Get self with an element appended to the end. + * + * @param Element $el Element to insert into the structure + */ + public function withAppended(Element $el): self + { + $obj = clone $this; + array_push($obj->elements, $el); + return $obj; + } + + /** + * Get self with an element prepended in the beginning. + * + * @param Element $el Element to insert into the structure + */ + public function withPrepended(Element $el): self + { + $obj = clone $this; + array_unshift($obj->elements, $el); + return $obj; + } + + /** + * Get self with an element at the given index removed. + * + * @param int $idx Element index + */ + public function withoutElement(int $idx): self + { + if (! isset($this->elements[$idx])) { + throw new OutOfBoundsException("Structure doesn't have element at index {$idx}."); + } + $obj = clone $this; + array_splice($obj->elements, $idx, 1); + return $obj; + } + + /** + * Get elements in the structure. + * + * @return UnspecifiedType[] + */ + public function elements(): array + { + if (! isset($this->unspecifiedTypes)) { + $this->unspecifiedTypes = array_map( + static fn (Element $el) => UnspecifiedType::create($el), + $this->elements + ); + } + return $this->unspecifiedTypes; + } + + /** + * Check whether the structure has an element at the given index, optionally satisfying given tag expectation. + * + * @param int $idx Index 0..n + * @param null|int $expectedTag Optional type tag expectation + */ + public function has(int $idx, ?int $expectedTag = null): bool + { + if (! isset($this->elements[$idx])) { + return false; + } + if (isset($expectedTag)) { + if (! $this->elements[$idx]->isType($expectedTag)) { + return false; + } + } + return true; + } + + /** + * Get the element at the given index, optionally checking that the element has a given tag. + * + * @param int $idx Index 0..n + */ + public function at(int $idx): UnspecifiedType + { + if (! isset($this->elements[$idx])) { + throw new OutOfBoundsException("Structure doesn't have an element at index {$idx}."); + } + return UnspecifiedType::create($this->elements[$idx]); + } + + /** + * Check whether the structure contains a context specific element with a given tag. + * + * @param int $tag Tag number + */ + public function hasTagged(int $tag): bool + { + // lazily build lookup map + if (! isset($this->taggedMap)) { + $this->taggedMap = []; + foreach ($this->elements as $element) { + if ($element->isTagged()) { + $this->taggedMap[$element->tag()] = $element; + } + } + } + return isset($this->taggedMap[$tag]); + } + + /** + * Get a context specific element tagged with a given tag. + */ + public function getTagged(int $tag): TaggedType + { + if (! $this->hasTagged($tag)) { + throw new LogicException("No tagged element for tag {$tag}."); + } + return $this->taggedMap[$tag]; + } + + /** + * @see \Countable::count() + */ + public function count(): int + { + return count($this->elements); + } + + /** + * Get an iterator for the `UnspecifiedElement` objects. + * + * @see \IteratorAggregate::getIterator() + */ + public function getIterator(): ArrayIterator + { + return new ArrayIterator($this->elements()); + } + + protected function encodedAsDER(): string + { + $data = ''; + foreach ($this->elements as $element) { + $data .= $element->toDER(); + } + return $data; + } +} diff --git a/3rdparty/spomky-labs/pki-framework/src/ASN1/Type/Tagged/ApplicationType.php b/3rdparty/spomky-labs/pki-framework/src/ASN1/Type/Tagged/ApplicationType.php new file mode 100644 index 00000000..0505048b --- /dev/null +++ b/3rdparty/spomky-labs/pki-framework/src/ASN1/Type/Tagged/ApplicationType.php @@ -0,0 +1,12 @@ +intTag(), $indefinite_length); + } + + public static function create( + Identifier $_identifier, + string $_data, + int $_offset, + int $_valueOffset, + int $_valueLength, + bool $indefinite_length + ): static { + return new static($_identifier, $_data, $_offset, $_valueOffset, $_valueLength, $indefinite_length); + } + + public function typeClass(): int + { + return $this->_identifier->typeClass(); + } + + public function isConstructed(): bool + { + return $this->_identifier->isConstructed(); + } + + public function implicit(int $tag, int $class = Identifier::CLASS_UNIVERSAL): UnspecifiedType + { + $identifier = $this->_identifier->withClass($class) + ->withTag($tag); + $cls = self::determineImplClass($identifier); + $idx = $this->_offset; + /** @var ElementBase $element */ + $element = $cls::decodeFromDER($identifier, $this->_data, $idx); + return $element->asUnspecified(); + } + + public function explicit(): UnspecifiedType + { + $idx = $this->_valueOffset; + return Element::fromDER($this->_data, $idx)->asUnspecified(); + } + + protected static function decodeFromDER(Identifier $identifier, string $data, int &$offset): ElementBase + { + $idx = $offset; + $length = Length::expectFromDER($data, $idx); + // offset to inner value + $value_offset = $idx; + if ($length->isIndefinite()) { + if ($identifier->isPrimitive()) { + throw new DecodeException('Primitive type with indefinite length is not supported.'); + } + // EOC consists of two octets. + $value_length = $idx - $value_offset - 2; + } else { + $value_length = $length->intLength(); + $idx += $value_length; + } + // late static binding since ApplicationType and PrivateType extend this class + $type = static::create($identifier, $data, $offset, $value_offset, $value_length, $length->isIndefinite()); + $offset = $idx; + return $type; + } + + protected function encodedAsDER(): string + { + return mb_substr($this->_data, $this->_valueOffset, $this->_valueLength, '8bit'); + } +} diff --git a/3rdparty/spomky-labs/pki-framework/src/ASN1/Type/Tagged/ExplicitTagging.php b/3rdparty/spomky-labs/pki-framework/src/ASN1/Type/Tagged/ExplicitTagging.php new file mode 100644 index 00000000..bbbc26f3 --- /dev/null +++ b/3rdparty/spomky-labs/pki-framework/src/ASN1/Type/Tagged/ExplicitTagging.php @@ -0,0 +1,19 @@ +element->asUnspecified(); + } + + protected function encodedAsDER(): string + { + // get the full encoding of the wrapped element + return $this->element->toDER(); + } + + protected static function decodeFromDER(Identifier $identifier, string $data, int &$offset): ElementBase + { + throw new BadMethodCallException(__METHOD__ . ' must be implemented in derived class.'); + } +} diff --git a/3rdparty/spomky-labs/pki-framework/src/ASN1/Type/Tagged/ImplicitTagging.php b/3rdparty/spomky-labs/pki-framework/src/ASN1/Type/Tagged/ImplicitTagging.php new file mode 100644 index 00000000..7ad8372c --- /dev/null +++ b/3rdparty/spomky-labs/pki-framework/src/ASN1/Type/Tagged/ImplicitTagging.php @@ -0,0 +1,23 @@ +element->isConstructed(); + } + + public function implicit(int $tag, int $class = Identifier::CLASS_UNIVERSAL): UnspecifiedType + { + $this->element->expectType($tag); + if ($this->element->typeClass() !== $class) { + throw new UnexpectedValueException( + sprintf( + 'Type class %s expected, got %s.', + Identifier::classToName($class), + Identifier::classToName($this->element->typeClass()) + ) + ); + } + return $this->element->asUnspecified(); + } + + protected function encodedAsDER(): string + { + // get only the content of the wrapped element. + return $this->element->encodedAsDER(); + } + + protected static function decodeFromDER(Identifier $identifier, string $data, int &$offset): ElementBase + { + throw new BadMethodCallException(__METHOD__ . ' must be implemented in derived class.'); + } +} diff --git a/3rdparty/spomky-labs/pki-framework/src/ASN1/Type/Tagged/PrivateType.php b/3rdparty/spomky-labs/pki-framework/src/ASN1/Type/Tagged/PrivateType.php new file mode 100644 index 00000000..8d59ebb5 --- /dev/null +++ b/3rdparty/spomky-labs/pki-framework/src/ASN1/Type/Tagged/PrivateType.php @@ -0,0 +1,12 @@ +class; + } +} diff --git a/3rdparty/spomky-labs/pki-framework/src/ASN1/Type/TaggedType.php b/3rdparty/spomky-labs/pki-framework/src/ASN1/Type/TaggedType.php new file mode 100644 index 00000000..7ecf07fd --- /dev/null +++ b/3rdparty/spomky-labs/pki-framework/src/ASN1/Type/TaggedType.php @@ -0,0 +1,78 @@ +expectTagged($expectedTag); + } + return $el; + } + + /** + * Get the wrapped inner element employing explicit tagging. + * + * @param null|int $expectedTag Optional outer tag expectation + */ + public function asExplicit(?int $expectedTag = null): UnspecifiedType + { + return $this->expectExplicit($expectedTag) + ->explicit(); + } + + /** + * Check whether element supports implicit tagging. + * + * @param null|int $expectedTag Optional outer tag expectation + */ + public function expectImplicit(?int $expectedTag = null): ImplicitTagging + { + $el = $this; + if (! $el instanceof ImplicitTagging) { + throw new UnexpectedValueException("Element doesn't implement implicit tagging."); + } + if (isset($expectedTag)) { + $el->expectTagged($expectedTag); + } + return $el; + } + + /** + * Get the wrapped inner element employing implicit tagging. + * + * @param int $tag Type tag of the inner element + * @param null|int $expectedTag Optional outer tag expectation + * @param int $expectedClass Optional inner type class expectation + */ + public function asImplicit( + int $tag, + ?int $expectedTag = null, + int $expectedClass = Identifier::CLASS_UNIVERSAL + ): UnspecifiedType { + return $this->expectImplicit($expectedTag) + ->implicit($tag, $expectedClass); + } +} diff --git a/3rdparty/spomky-labs/pki-framework/src/ASN1/Type/TimeType.php b/3rdparty/spomky-labs/pki-framework/src/ASN1/Type/TimeType.php new file mode 100644 index 00000000..75e35287 --- /dev/null +++ b/3rdparty/spomky-labs/pki-framework/src/ASN1/Type/TimeType.php @@ -0,0 +1,18 @@ +asUnspecified(); + } + + /** + * Initialize from `ElementBase` interface. + */ + public static function fromElementBase(ElementBase $el): self + { + // if element is already wrapped + if ($el instanceof self) { + return $el; + } + return self::create($el->asElement()); + } + + /** + * Get the wrapped element as a context specific tagged type. + */ + public function asTagged(): TaggedType + { + if (! $this->element instanceof TaggedType) { + throw new UnexpectedValueException('Tagged element expected, got ' . $this->typeDescriptorString()); + } + return $this->element; + } + + /** + * Get the wrapped element as an application specific type. + */ + public function asApplication(): ApplicationType + { + if (! $this->element instanceof ApplicationType) { + throw new UnexpectedValueException('Application type expected, got ' . $this->typeDescriptorString()); + } + return $this->element; + } + + /** + * Get the wrapped element as a private tagged type. + */ + public function asPrivate(): PrivateType + { + if (! $this->element instanceof PrivateType) { + throw new UnexpectedValueException('Private type expected, got ' . $this->typeDescriptorString()); + } + return $this->element; + } + + /** + * Get the wrapped element as a boolean type. + */ + public function asBoolean(): Boolean + { + if (! $this->element instanceof Boolean) { + throw new UnexpectedValueException($this->generateExceptionMessage(Element::TYPE_BOOLEAN)); + } + return $this->element; + } + + /** + * Get the wrapped element as an integer type. + */ + public function asInteger(): Integer + { + if (! $this->element instanceof Integer) { + throw new UnexpectedValueException($this->generateExceptionMessage(Element::TYPE_INTEGER)); + } + return $this->element; + } + + /** + * Get the wrapped element as a bit string type. + */ + public function asBitString(): BitString + { + if (! $this->element instanceof BitString) { + throw new UnexpectedValueException($this->generateExceptionMessage(Element::TYPE_BIT_STRING)); + } + return $this->element; + } + + /** + * Get the wrapped element as an octet string type. + */ + public function asOctetString(): OctetString + { + if (! $this->element instanceof OctetString) { + throw new UnexpectedValueException($this->generateExceptionMessage(Element::TYPE_OCTET_STRING)); + } + return $this->element; + } + + /** + * Get the wrapped element as a null type. + */ + public function asNull(): NullType + { + if (! $this->element instanceof NullType) { + throw new UnexpectedValueException($this->generateExceptionMessage(Element::TYPE_NULL)); + } + return $this->element; + } + + /** + * Get the wrapped element as an object identifier type. + */ + public function asObjectIdentifier(): ObjectIdentifier + { + if (! $this->element instanceof ObjectIdentifier) { + throw new UnexpectedValueException($this->generateExceptionMessage(Element::TYPE_OBJECT_IDENTIFIER)); + } + return $this->element; + } + + /** + * Get the wrapped element as an object descriptor type. + */ + public function asObjectDescriptor(): ObjectDescriptor + { + if (! $this->element instanceof ObjectDescriptor) { + throw new UnexpectedValueException($this->generateExceptionMessage(Element::TYPE_OBJECT_DESCRIPTOR)); + } + return $this->element; + } + + /** + * Get the wrapped element as a real type. + */ + public function asReal(): Real + { + if (! $this->element instanceof Real) { + throw new UnexpectedValueException($this->generateExceptionMessage(Element::TYPE_REAL)); + } + return $this->element; + } + + /** + * Get the wrapped element as an enumerated type. + */ + public function asEnumerated(): Enumerated + { + if (! $this->element instanceof Enumerated) { + throw new UnexpectedValueException($this->generateExceptionMessage(Element::TYPE_ENUMERATED)); + } + return $this->element; + } + + /** + * Get the wrapped element as a UTF8 string type. + */ + public function asUTF8String(): UTF8String + { + if (! $this->element instanceof UTF8String) { + throw new UnexpectedValueException($this->generateExceptionMessage(Element::TYPE_UTF8_STRING)); + } + return $this->element; + } + + /** + * Get the wrapped element as a relative OID type. + */ + public function asRelativeOID(): RelativeOID + { + if (! $this->element instanceof RelativeOID) { + throw new UnexpectedValueException($this->generateExceptionMessage(Element::TYPE_RELATIVE_OID)); + } + return $this->element; + } + + /** + * Get the wrapped element as a sequence type. + */ + public function asSequence(): Sequence + { + if (! $this->element instanceof Sequence) { + throw new UnexpectedValueException($this->generateExceptionMessage(Element::TYPE_SEQUENCE)); + } + return $this->element; + } + + /** + * Get the wrapped element as a set type. + */ + public function asSet(): Set + { + if (! $this->element instanceof Set) { + throw new UnexpectedValueException($this->generateExceptionMessage(Element::TYPE_SET)); + } + return $this->element; + } + + /** + * Get the wrapped element as a numeric string type. + */ + public function asNumericString(): NumericString + { + if (! $this->element instanceof NumericString) { + throw new UnexpectedValueException($this->generateExceptionMessage(Element::TYPE_NUMERIC_STRING)); + } + return $this->element; + } + + /** + * Get the wrapped element as a printable string type. + */ + public function asPrintableString(): PrintableString + { + if (! $this->element instanceof PrintableString) { + throw new UnexpectedValueException($this->generateExceptionMessage(Element::TYPE_PRINTABLE_STRING)); + } + return $this->element; + } + + /** + * Get the wrapped element as a T61 string type. + */ + public function asT61String(): T61String + { + if (! $this->element instanceof T61String) { + throw new UnexpectedValueException($this->generateExceptionMessage(Element::TYPE_T61_STRING)); + } + return $this->element; + } + + /** + * Get the wrapped element as a videotex string type. + */ + public function asVideotexString(): VideotexString + { + if (! $this->element instanceof VideotexString) { + throw new UnexpectedValueException($this->generateExceptionMessage(Element::TYPE_VIDEOTEX_STRING)); + } + return $this->element; + } + + /** + * Get the wrapped element as a IA5 string type. + */ + public function asIA5String(): IA5String + { + if (! $this->element instanceof IA5String) { + throw new UnexpectedValueException($this->generateExceptionMessage(Element::TYPE_IA5_STRING)); + } + return $this->element; + } + + /** + * Get the wrapped element as an UTC time type. + */ + public function asUTCTime(): UTCTime + { + if (! $this->element instanceof UTCTime) { + throw new UnexpectedValueException($this->generateExceptionMessage(Element::TYPE_UTC_TIME)); + } + return $this->element; + } + + /** + * Get the wrapped element as a generalized time type. + */ + public function asGeneralizedTime(): GeneralizedTime + { + if (! $this->element instanceof GeneralizedTime) { + throw new UnexpectedValueException($this->generateExceptionMessage(Element::TYPE_GENERALIZED_TIME)); + } + return $this->element; + } + + /** + * Get the wrapped element as a graphic string type. + */ + public function asGraphicString(): GraphicString + { + if (! $this->element instanceof GraphicString) { + throw new UnexpectedValueException($this->generateExceptionMessage(Element::TYPE_GRAPHIC_STRING)); + } + return $this->element; + } + + /** + * Get the wrapped element as a visible string type. + */ + public function asVisibleString(): VisibleString + { + if (! $this->element instanceof VisibleString) { + throw new UnexpectedValueException($this->generateExceptionMessage(Element::TYPE_VISIBLE_STRING)); + } + return $this->element; + } + + /** + * Get the wrapped element as a general string type. + */ + public function asGeneralString(): GeneralString + { + if (! $this->element instanceof GeneralString) { + throw new UnexpectedValueException($this->generateExceptionMessage(Element::TYPE_GENERAL_STRING)); + } + return $this->element; + } + + /** + * Get the wrapped element as a universal string type. + */ + public function asUniversalString(): UniversalString + { + if (! $this->element instanceof UniversalString) { + throw new UnexpectedValueException($this->generateExceptionMessage(Element::TYPE_UNIVERSAL_STRING)); + } + return $this->element; + } + + /** + * Get the wrapped element as a character string type. + */ + public function asCharacterString(): CharacterString + { + if (! $this->element instanceof CharacterString) { + throw new UnexpectedValueException($this->generateExceptionMessage(Element::TYPE_CHARACTER_STRING)); + } + return $this->element; + } + + /** + * Get the wrapped element as a BMP string type. + */ + public function asBMPString(): BMPString + { + if (! $this->element instanceof BMPString) { + throw new UnexpectedValueException($this->generateExceptionMessage(Element::TYPE_BMP_STRING)); + } + return $this->element; + } + + /** + * Get the wrapped element as a constructed string type. + */ + public function asConstructedString(): ConstructedString + { + if (! $this->element instanceof ConstructedString) { + throw new UnexpectedValueException($this->generateExceptionMessage(Element::TYPE_CONSTRUCTED_STRING)); + } + return $this->element; + } + + /** + * Get the wrapped element as any string type. + */ + public function asString(): StringType + { + if (! $this->element instanceof StringType) { + throw new UnexpectedValueException($this->generateExceptionMessage(Element::TYPE_STRING)); + } + return $this->element; + } + + /** + * Get the wrapped element as any time type. + */ + public function asTime(): TimeType + { + if (! $this->element instanceof TimeType) { + throw new UnexpectedValueException($this->generateExceptionMessage(Element::TYPE_TIME)); + } + return $this->element; + } + + public function asElement(): Element + { + return $this->element; + } + + public function asUnspecified(): self + { + return $this; + } + + public function toDER(): string + { + return $this->element->toDER(); + } + + public function typeClass(): int + { + return $this->element->typeClass(); + } + + public function tag(): int + { + return $this->element->tag(); + } + + public function isConstructed(): bool + { + return $this->element->isConstructed(); + } + + public function isType(int $tag): bool + { + return $this->element->isType($tag); + } + + public function isTagged(): bool + { + return $this->element->isTagged(); + } + + /** + * {@inheritdoc} + * + * Consider using any of the `as*` accessor methods instead. + */ + public function expectType(int $tag): ElementBase + { + return $this->element->expectType($tag); + } + + /** + * {@inheritdoc} + * + * Consider using `asTagged()` method instead and chaining + * with `TaggedType::asExplicit()` or `TaggedType::asImplicit()`. + */ + public function expectTagged(?int $tag = null): TaggedType + { + return $this->element->expectTagged($tag); + } + + /** + * Generate message for exceptions thrown by `as*` methods. + * + * @param int $tag Type tag of the expected element + */ + private function generateExceptionMessage(int $tag): string + { + return sprintf('%s expected, got %s.', Element::tagToName($tag), $this->typeDescriptorString()); + } + + /** + * Get textual description of the wrapped element for debugging purposes. + */ + private function typeDescriptorString(): string + { + $type_cls = $this->element->typeClass(); + $tag = $this->element->tag(); + $str = $this->element->isConstructed() ? 'constructed ' : 'primitive '; + if ($type_cls === Identifier::CLASS_UNIVERSAL) { + $str .= Element::tagToName($tag); + } else { + $str .= Identifier::classToName($type_cls) . " TAG {$tag}"; + } + return $str; + } +} diff --git a/3rdparty/spomky-labs/pki-framework/src/ASN1/Util/BigInt.php b/3rdparty/spomky-labs/pki-framework/src/ASN1/Util/BigInt.php new file mode 100644 index 00000000..5657085a --- /dev/null +++ b/3rdparty/spomky-labs/pki-framework/src/ASN1/Util/BigInt.php @@ -0,0 +1,126 @@ +value = $num; + } + + public function __toString(): string + { + return $this->base10(); + } + + public static function create(BigInteger|int|string $num): self + { + return new self($num); + } + + /** + * Initialize from an arbitrary length of octets as an unsigned integer. + */ + public static function fromUnsignedOctets(string $octets): self + { + if (mb_strlen($octets, '8bit') === 0) { + throw new InvalidArgumentException('Empty octets.'); + } + return self::create(BigInteger::fromBytes($octets, false)); + } + + /** + * Initialize from an arbitrary length of octets as an signed integer having two's complement encoding. + */ + public static function fromSignedOctets(string $octets): self + { + if (mb_strlen($octets, '8bit') === 0) { + throw new InvalidArgumentException('Empty octets.'); + } + + return self::create(BigInteger::fromBytes($octets)); + } + + /** + * Get the number as a base 10 integer string. + */ + public function base10(): string + { + if ($this->number === null) { + $this->number = $this->value->toBase(10); + } + return $this->number; + } + + /** + * Get the number as an integer. + */ + public function toInt(): int + { + if (! isset($this->_intNum)) { + $this->_intNum = $this->value->toInt(); + } + return $this->_intNum; + } + + public function getValue(): BigInteger + { + return $this->value; + } + + /** + * Get the number as an unsigned integer encoded in binary. + */ + public function unsignedOctets(): string + { + return $this->value->toBytes(false); + } + + /** + * Get the number as a signed integer encoded in two's complement binary. + */ + public function signedOctets(): string + { + return $this->value->toBytes(); + } +} diff --git a/3rdparty/spomky-labs/pki-framework/src/ASN1/Util/Flags.php b/3rdparty/spomky-labs/pki-framework/src/ASN1/Util/Flags.php new file mode 100644 index 00000000..cc8cbace --- /dev/null +++ b/3rdparty/spomky-labs/pki-framework/src/ASN1/Util/Flags.php @@ -0,0 +1,148 @@ +_flags = ''; + return; + } + + // calculate number of unused bits in last octet + $last_octet_bits = $_width % 8; + $unused_bits = $last_octet_bits !== 0 ? 8 - $last_octet_bits : 0; + // mask bits outside bitfield width + $num = BigInteger::of($flags); + $mask = BigInteger::of(1)->shiftedLeft($_width)->minus(1); + $num = $num->and($mask); + + // shift towards MSB if needed + $data = $num->shiftedLeft($unused_bits) + ->toBytes(false); + $octets = unpack('C*', $data); + assert(is_array($octets), new RuntimeException('unpack() failed')); + $bits = count($octets) * 8; + // pad with zeroes + while ($bits < $_width) { + array_unshift($octets, 0); + $bits += 8; + } + $this->_flags = pack('C*', ...$octets); + } + + public static function create(BigInteger|int|string $flags, int $_width): self + { + return new self($flags, $_width); + } + + /** + * Initialize from `BitString`. + */ + public static function fromBitString(BitString $bs, int $width): self + { + $num_bits = $bs->numBits(); + $data = $bs->string(); + $num = $data === '' ? BigInteger::of(0) : BigInteger::fromBytes($bs->string(), false); + $num = $num->shiftedRight($bs->unusedBits()); + if ($num_bits < $width) { + $num = $num->shiftedLeft($width - $num_bits); + } + return self::create($num, $width); + } + + /** + * Check whether a bit at given index is set. + * + * Index 0 is the leftmost bit. + */ + public function test(int $idx): bool + { + if ($idx >= $this->_width) { + throw new OutOfBoundsException('Index is out of bounds.'); + } + // octet index + $oi = (int) floor($idx / 8); + $byte = $this->_flags[$oi]; + // bit index + $bi = $idx % 8; + // index 0 is the most significant bit in byte + $mask = 0x01 << (7 - $bi); + return (ord($byte) & $mask) > 0; + } + + /** + * Get flags as an octet string. + * + * Zeroes are appended to the last octet if width is not divisible by 8. + */ + public function string(): string + { + return $this->_flags; + } + + /** + * Get flags as a base 10 integer. + * + * @return string Integer as a string + */ + public function number(): string + { + $num = BigInteger::fromBytes($this->_flags, false); + $last_octet_bits = $this->_width % 8; + $unused_bits = $last_octet_bits !== 0 ? 8 - $last_octet_bits : 0; + $num = $num->shiftedRight($unused_bits); + return $num->toBase(10); + } + + /** + * Get flags as an integer. + */ + public function intNumber(): int + { + $num = BigInt::create($this->number()); + return $num->toInt(); + } + + /** + * Get flags as a `BitString` object. + * + * Unused bits are set accordingly. Trailing zeroes are not stripped. + */ + public function bitString(): BitString + { + $last_octet_bits = $this->_width % 8; + $unused_bits = $last_octet_bits !== 0 ? 8 - $last_octet_bits : 0; + return BitString::create($this->_flags, $unused_bits); + } +} diff --git a/3rdparty/spomky-labs/pki-framework/src/CryptoBridge/Crypto.php b/3rdparty/spomky-labs/pki-framework/src/CryptoBridge/Crypto.php new file mode 100644 index 00000000..c70e898e --- /dev/null +++ b/3rdparty/spomky-labs/pki-framework/src/CryptoBridge/Crypto.php @@ -0,0 +1,90 @@ + + */ + private const MAP_DIGEST_OID = [ + AlgorithmIdentifier::OID_MD4_WITH_RSA_ENCRYPTION => OPENSSL_ALGO_MD4, + AlgorithmIdentifier::OID_MD5_WITH_RSA_ENCRYPTION => OPENSSL_ALGO_MD5, + AlgorithmIdentifier::OID_SHA1_WITH_RSA_ENCRYPTION => OPENSSL_ALGO_SHA1, + AlgorithmIdentifier::OID_SHA224_WITH_RSA_ENCRYPTION => OPENSSL_ALGO_SHA224, + AlgorithmIdentifier::OID_SHA256_WITH_RSA_ENCRYPTION => OPENSSL_ALGO_SHA256, + AlgorithmIdentifier::OID_SHA384_WITH_RSA_ENCRYPTION => OPENSSL_ALGO_SHA384, + AlgorithmIdentifier::OID_SHA512_WITH_RSA_ENCRYPTION => OPENSSL_ALGO_SHA512, + AlgorithmIdentifier::OID_ECDSA_WITH_SHA1 => OPENSSL_ALGO_SHA1, + AlgorithmIdentifier::OID_ECDSA_WITH_SHA224 => OPENSSL_ALGO_SHA224, + AlgorithmIdentifier::OID_ECDSA_WITH_SHA256 => OPENSSL_ALGO_SHA256, + AlgorithmIdentifier::OID_ECDSA_WITH_SHA384 => OPENSSL_ALGO_SHA384, + AlgorithmIdentifier::OID_ECDSA_WITH_SHA512 => OPENSSL_ALGO_SHA512, + ]; + + /** + * Mapping from algorithm OID to OpenSSL cipher method name. + * + * @internal + * + * @var array + */ + private const MAP_CIPHER_OID = [ + AlgorithmIdentifier::OID_DES_CBC => 'des-cbc', + AlgorithmIdentifier::OID_DES_EDE3_CBC => 'des-ede3-cbc', + AlgorithmIdentifier::OID_AES_128_CBC => 'aes-128-cbc', + AlgorithmIdentifier::OID_AES_192_CBC => 'aes-192-cbc', + AlgorithmIdentifier::OID_AES_256_CBC => 'aes-256-cbc', + ]; + + public function sign( + string $data, + PrivateKeyInfo $privkey_info, + SignatureAlgorithmIdentifier $algo + ): Signature { + $this->_checkSignatureAlgoAndKey($algo, $privkey_info->algorithmIdentifier()); + $result = openssl_sign($data, $signature, (string) $privkey_info->toPEM(), $this->_algoToDigest($algo)); + if ($result === false) { + throw new RuntimeException('openssl_sign() failed: ' . $this->_getLastError()); + } + return Signature::fromSignatureData($signature, $algo); + } + + public function verify( + string $data, + Signature $signature, + PublicKeyInfo $pubkey_info, + SignatureAlgorithmIdentifier $algo + ): bool { + $this->_checkSignatureAlgoAndKey($algo, $pubkey_info->algorithmIdentifier()); + $result = openssl_verify( + $data, + $signature->bitString() + ->string(), + (string) $pubkey_info->toPEM(), + $this->_algoToDigest($algo) + ); + if ($result === -1) { + throw new RuntimeException('openssl_verify() failed: ' . $this->_getLastError()); + } + return $result === 1; + } + + public function encrypt(string $data, string $key, CipherAlgorithmIdentifier $algo): string + { + $this->_checkCipherKeySize($algo, $key); + $iv = $algo->initializationVector(); + $result = openssl_encrypt( + $data, + $this->_algoToCipher($algo), + $key, + OPENSSL_RAW_DATA | OPENSSL_ZERO_PADDING, + $iv + ); + if ($result === false) { + throw new RuntimeException('openssl_encrypt() failed: ' . $this->_getLastError()); + } + return $result; + } + + public function decrypt(string $data, string $key, CipherAlgorithmIdentifier $algo): string + { + $this->_checkCipherKeySize($algo, $key); + $iv = $algo->initializationVector(); + $result = openssl_decrypt( + $data, + $this->_algoToCipher($algo), + $key, + OPENSSL_RAW_DATA | OPENSSL_ZERO_PADDING, + $iv + ); + if ($result === false) { + throw new RuntimeException('openssl_decrypt() failed: ' . $this->_getLastError()); + } + return $result; + } + + /** + * Validate cipher algorithm key size. + */ + protected function _checkCipherKeySize(CipherAlgorithmIdentifier $algo, string $key): void + { + if ($algo instanceof BlockCipherAlgorithmIdentifier) { + if (mb_strlen($key, '8bit') !== $algo->keySize()) { + throw new UnexpectedValueException( + sprintf( + 'Key length for %s must be %d, %d given.', + $algo->name(), + $algo->keySize(), + mb_strlen($key, '8bit') + ) + ); + } + } + } + + /** + * Get last OpenSSL error message. + */ + protected function _getLastError(): ?string + { + // pump error message queue + $msg = null; + while (false !== ($err = openssl_error_string())) { + $msg = $err; + } + return $msg; + } + + /** + * Check that given signature algorithm supports key of given type. + * + * @param SignatureAlgorithmIdentifier $sig_algo Signature algorithm + * @param AlgorithmIdentifier $key_algo Key algorithm + */ + protected function _checkSignatureAlgoAndKey( + SignatureAlgorithmIdentifier $sig_algo, + AlgorithmIdentifier $key_algo + ): void { + if (! $sig_algo->supportsKeyAlgorithm($key_algo)) { + throw new UnexpectedValueException( + sprintf( + 'Signature algorithm %s does not support key algorithm %s.', + $sig_algo->name(), + $key_algo->name() + ) + ); + } + } + + /** + * Get OpenSSL digest method for given signature algorithm identifier. + */ + protected function _algoToDigest(SignatureAlgorithmIdentifier $algo): int + { + $oid = $algo->oid(); + if (! array_key_exists($oid, self::MAP_DIGEST_OID)) { + throw new UnexpectedValueException(sprintf('Digest method %s not supported.', $algo->name())); + } + return self::MAP_DIGEST_OID[$oid]; + } + + /** + * Get OpenSSL cipher method for given cipher algorithm identifier. + */ + protected function _algoToCipher(CipherAlgorithmIdentifier $algo): string + { + $oid = $algo->oid(); + if (array_key_exists($oid, self::MAP_CIPHER_OID)) { + return self::MAP_CIPHER_OID[$oid]; + } + if ($oid === AlgorithmIdentifier::OID_RC2_CBC) { + if (! $algo instanceof RC2CBCAlgorithmIdentifier) { + throw new UnexpectedValueException('Not an RC2-CBC algorithm.'); + } + return $this->_rc2AlgoToCipher($algo); + } + throw new UnexpectedValueException(sprintf('Cipher method %s not supported.', $algo->name())); + } + + /** + * Get OpenSSL cipher method for given RC2 algorithm identifier. + */ + protected function _rc2AlgoToCipher(RC2CBCAlgorithmIdentifier $algo): string + { + return match ($algo->effectiveKeyBits()) { + 128 => 'rc2-cbc', + 64 => 'rc2-64-cbc', + 40 => 'rc2-40-cbc', + default => throw new UnexpectedValueException($algo->effectiveKeyBits() . ' bit RC2 not supported.'), + }; + } +} diff --git a/3rdparty/spomky-labs/pki-framework/src/CryptoEncoding/PEM.php b/3rdparty/spomky-labs/pki-framework/src/CryptoEncoding/PEM.php new file mode 100644 index 00000000..3618a01c --- /dev/null +++ b/3rdparty/spomky-labs/pki-framework/src/CryptoEncoding/PEM.php @@ -0,0 +1,138 @@ +string(); + } + + public static function create(string $_type, string $_data): self + { + return new self($_type, $_data); + } + + /** + * Initialize from a PEM-formatted string. + */ + public static function fromString(string $str): self + { + if (preg_match(self::PEM_REGEX, $str, $match) !== 1) { + throw new UnexpectedValueException('Not a PEM formatted string.'); + } + $payload = preg_replace('/\s+/', '', $match[2]); + if (! is_string($payload)) { + throw new UnexpectedValueException('Failed to decode PEM data.'); + } + $data = base64_decode($payload, true); + if ($data === false) { + throw new UnexpectedValueException('Failed to decode PEM data.'); + } + return self::create($match[1], $data); + } + + /** + * Initialize from a file. + * + * @param string $filename Path to file + */ + public static function fromFile(string $filename): self + { + if (! is_readable($filename)) { + throw new RuntimeException("Failed to read {$filename}."); + } + $str = file_get_contents($filename); + if ($str === false) { + throw new RuntimeException("Failed to read {$filename}."); + } + return self::fromString($str); + } + + /** + * Get content type. + */ + public function type(): string + { + return $this->type; + } + + public function data(): string + { + return $this->data; + } + + /** + * Encode to PEM string. + */ + public function string(): string + { + return "-----BEGIN {$this->type}-----\n" . + trim(chunk_split(base64_encode($this->data), 64, "\n")) . "\n" . + "-----END {$this->type}-----"; + } +} diff --git a/3rdparty/spomky-labs/pki-framework/src/CryptoEncoding/PEMBundle.php b/3rdparty/spomky-labs/pki-framework/src/CryptoEncoding/PEMBundle.php new file mode 100644 index 00000000..3411004a --- /dev/null +++ b/3rdparty/spomky-labs/pki-framework/src/CryptoEncoding/PEMBundle.php @@ -0,0 +1,155 @@ +pems = $pems; + } + + public function __toString(): string + { + return $this->string(); + } + + public static function create(PEM ...$pems): self + { + return new self(...$pems); + } + + /** + * Initialize from a string. + */ + public static function fromString(string $str): self + { + $hasMatches = preg_match_all(PEM::PEM_REGEX, $str, $matches, PREG_SET_ORDER); + if ($hasMatches === false || $hasMatches === 0) { + throw new UnexpectedValueException('No PEM blocks.'); + } + $pems = array_map( + static function ($match) { + $payload = preg_replace('/\s+/', '', $match[2]); + if (! is_string($payload)) { + throw new UnexpectedValueException('Failed to decode PEM data.'); + } + $data = base64_decode($payload, true); + if ($data === false) { + throw new UnexpectedValueException('Failed to decode PEM data.'); + } + return PEM::create($match[1], $data); + }, + $matches + ); + return self::create(...$pems); + } + + /** + * Initialize from a file. + */ + public static function fromFile(string $filename): self + { + if (! is_readable($filename)) { + throw new RuntimeException("Failed to read {$filename}."); + } + $str = file_get_contents($filename); + if ($str === false) { + throw new RuntimeException("Failed to read {$filename}."); + } + return self::fromString($str); + } + + /** + * Get self with PEM objects appended. + */ + public function withPEMs(PEM ...$pems): self + { + $obj = clone $this; + $obj->pems = array_merge($obj->pems, $pems); + return $obj; + } + + /** + * Get all PEMs in a bundle. + * + * @return PEM[] + */ + public function all(): array + { + return $this->pems; + } + + /** + * Get the first PEM in a bundle. + */ + public function first(): PEM + { + if (count($this->pems) === 0) { + throw new LogicException('No PEMs.'); + } + return $this->pems[0]; + } + + /** + * Get the last PEM in a bundle. + */ + public function last(): PEM + { + if (count($this->pems) === 0) { + throw new LogicException('No PEMs.'); + } + return $this->pems[count($this->pems) - 1]; + } + + /** + * @see \Countable::count() + */ + public function count(): int + { + return count($this->pems); + } + + /** + * Get iterator for PEMs. + * + * @see \IteratorAggregate::getIterator() + */ + public function getIterator(): ArrayIterator + { + return new ArrayIterator($this->pems); + } + + /** + * Encode bundle to a string of contiguous PEM blocks. + */ + public function string(): string + { + return implode("\n", array_map(static fn (PEM $pem) => $pem->string(), $this->pems)); + } +} diff --git a/3rdparty/spomky-labs/pki-framework/src/CryptoTypes/AlgorithmIdentifier/AlgorithmIdentifier.php b/3rdparty/spomky-labs/pki-framework/src/CryptoTypes/AlgorithmIdentifier/AlgorithmIdentifier.php new file mode 100644 index 00000000..c78e174c --- /dev/null +++ b/3rdparty/spomky-labs/pki-framework/src/CryptoTypes/AlgorithmIdentifier/AlgorithmIdentifier.php @@ -0,0 +1,137 @@ +parse($seq); + } + + public function oid(): string + { + return $this->oid; + } + + public function toASN1(): Sequence + { + $elements = [ObjectIdentifier::create($this->oid)]; + $params = $this->paramsASN1(); + if (isset($params)) { + $elements[] = $params; + } + return Sequence::create(...$elements); + } + + /** + * Get algorithm identifier parameters as ASN.1. + * + * If type allows parameters to be omitted, return null. + */ + abstract protected function paramsASN1(): ?Element; +} diff --git a/3rdparty/spomky-labs/pki-framework/src/CryptoTypes/AlgorithmIdentifier/AlgorithmIdentifierFactory.php b/3rdparty/spomky-labs/pki-framework/src/CryptoTypes/AlgorithmIdentifier/AlgorithmIdentifierFactory.php new file mode 100644 index 00000000..175b2664 --- /dev/null +++ b/3rdparty/spomky-labs/pki-framework/src/CryptoTypes/AlgorithmIdentifier/AlgorithmIdentifierFactory.php @@ -0,0 +1,158 @@ + + */ + private const MAP_OID_TO_CLASS = [ + AlgorithmIdentifier::OID_RSA_ENCRYPTION => RSAEncryptionAlgorithmIdentifier::class, + AlgorithmIdentifier::OID_EC_PUBLIC_KEY => ECPublicKeyAlgorithmIdentifier::class, + AlgorithmIdentifier::OID_X25519 => X25519AlgorithmIdentifier::class, + AlgorithmIdentifier::OID_X448 => X448AlgorithmIdentifier::class, + AlgorithmIdentifier::OID_ED25519 => Ed25519AlgorithmIdentifier::class, + AlgorithmIdentifier::OID_ED448 => Ed448AlgorithmIdentifier::class, + AlgorithmIdentifier::OID_DES_CBC => DESCBCAlgorithmIdentifier::class, + AlgorithmIdentifier::OID_DES_EDE3_CBC => DESEDE3CBCAlgorithmIdentifier::class, + AlgorithmIdentifier::OID_RC2_CBC => RC2CBCAlgorithmIdentifier::class, + AlgorithmIdentifier::OID_AES_128_CBC => AES128CBCAlgorithmIdentifier::class, + AlgorithmIdentifier::OID_AES_192_CBC => AES192CBCAlgorithmIdentifier::class, + AlgorithmIdentifier::OID_AES_256_CBC => AES256CBCAlgorithmIdentifier::class, + AlgorithmIdentifier::OID_HMAC_WITH_SHA1 => HMACWithSHA1AlgorithmIdentifier::class, + AlgorithmIdentifier::OID_HMAC_WITH_SHA224 => HMACWithSHA224AlgorithmIdentifier::class, + AlgorithmIdentifier::OID_HMAC_WITH_SHA256 => HMACWithSHA256AlgorithmIdentifier::class, + AlgorithmIdentifier::OID_HMAC_WITH_SHA384 => HMACWithSHA384AlgorithmIdentifier::class, + AlgorithmIdentifier::OID_HMAC_WITH_SHA512 => HMACWithSHA512AlgorithmIdentifier::class, + AlgorithmIdentifier::OID_MD5 => MD5AlgorithmIdentifier::class, + AlgorithmIdentifier::OID_SHA1 => SHA1AlgorithmIdentifier::class, + AlgorithmIdentifier::OID_SHA224 => SHA224AlgorithmIdentifier::class, + AlgorithmIdentifier::OID_SHA256 => SHA256AlgorithmIdentifier::class, + AlgorithmIdentifier::OID_SHA384 => SHA384AlgorithmIdentifier::class, + AlgorithmIdentifier::OID_SHA512 => SHA512AlgorithmIdentifier::class, + AlgorithmIdentifier::OID_MD2_WITH_RSA_ENCRYPTION => MD2WithRSAEncryptionAlgorithmIdentifier::class, + AlgorithmIdentifier::OID_MD4_WITH_RSA_ENCRYPTION => MD4WithRSAEncryptionAlgorithmIdentifier::class, + AlgorithmIdentifier::OID_MD5_WITH_RSA_ENCRYPTION => MD5WithRSAEncryptionAlgorithmIdentifier::class, + AlgorithmIdentifier::OID_SHA1_WITH_RSA_ENCRYPTION => SHA1WithRSAEncryptionAlgorithmIdentifier::class, + AlgorithmIdentifier::OID_SHA224_WITH_RSA_ENCRYPTION => SHA224WithRSAEncryptionAlgorithmIdentifier::class, + AlgorithmIdentifier::OID_SHA256_WITH_RSA_ENCRYPTION => SHA256WithRSAEncryptionAlgorithmIdentifier::class, + AlgorithmIdentifier::OID_SHA384_WITH_RSA_ENCRYPTION => SHA384WithRSAEncryptionAlgorithmIdentifier::class, + AlgorithmIdentifier::OID_SHA512_WITH_RSA_ENCRYPTION => SHA512WithRSAEncryptionAlgorithmIdentifier::class, + AlgorithmIdentifier::OID_ECDSA_WITH_SHA1 => ECDSAWithSHA1AlgorithmIdentifier::class, + AlgorithmIdentifier::OID_ECDSA_WITH_SHA224 => ECDSAWithSHA224AlgorithmIdentifier::class, + AlgorithmIdentifier::OID_ECDSA_WITH_SHA256 => ECDSAWithSHA256AlgorithmIdentifier::class, + AlgorithmIdentifier::OID_ECDSA_WITH_SHA384 => ECDSAWithSHA384AlgorithmIdentifier::class, + AlgorithmIdentifier::OID_ECDSA_WITH_SHA512 => ECDSAWithSHA512AlgorithmIdentifier::class, + ]; + + /** + * Additional algorithm identifier providers. + * + * @var AlgorithmIdentifierProvider[] + */ + private readonly array $_additionalProviders; + + /** + * @param AlgorithmIdentifierProvider ...$providers Additional providers + */ + private function __construct(AlgorithmIdentifierProvider ...$providers) + { + $this->_additionalProviders = $providers; + } + + public static function create(AlgorithmIdentifierProvider ...$providers): self + { + return new self(...$providers); + } + + /** + * Get the name of a class that implements algorithm identifier for given OID. + * + * @param string $oid Object identifier in dotted format + * + * @return null|string Fully qualified class name or null if not supported + */ + public function getClass(string $oid): ?string + { + // if OID is provided by this factory + if (array_key_exists($oid, self::MAP_OID_TO_CLASS)) { + return self::MAP_OID_TO_CLASS[$oid]; + } + // try additional providers + foreach ($this->_additionalProviders as $provider) { + if ($provider->supportsOID($oid)) { + return $provider->getClassByOID($oid); + } + } + return null; + } + + /** + * Parse AlgorithmIdentifier from an ASN.1 sequence. + */ + public function parse(Sequence $seq): AlgorithmIdentifier + { + $oid = $seq->at(0) + ->asObjectIdentifier() + ->oid(); + $params = $seq->has(1) ? $seq->at(1) : null; + $cls = $this->getClass($oid); + if ($cls !== null) { + return $cls::fromASN1Params($params); + } + + return GenericAlgorithmIdentifier::create($oid, $params); + } +} diff --git a/3rdparty/spomky-labs/pki-framework/src/CryptoTypes/AlgorithmIdentifier/AlgorithmIdentifierProvider.php b/3rdparty/spomky-labs/pki-framework/src/CryptoTypes/AlgorithmIdentifier/AlgorithmIdentifierProvider.php new file mode 100644 index 00000000..846f0d9d --- /dev/null +++ b/3rdparty/spomky-labs/pki-framework/src/CryptoTypes/AlgorithmIdentifier/AlgorithmIdentifierProvider.php @@ -0,0 +1,31 @@ + + */ + final public const MAP_CURVE_TO_SIZE = [ + self::CURVE_PRIME192V1 => 192, + self::CURVE_PRIME192V2 => 192, + self::CURVE_PRIME192V3 => 192, + self::CURVE_PRIME239V1 => 239, + self::CURVE_PRIME239V2 => 239, + self::CURVE_PRIME239V3 => 239, + self::CURVE_PRIME256V1 => 256, + self::CURVE_SECP112R1 => 112, + self::CURVE_SECP112R2 => 112, + self::CURVE_SECP128R1 => 128, + self::CURVE_SECP128R2 => 128, + self::CURVE_SECP160K1 => 160, + self::CURVE_SECP160R1 => 160, + self::CURVE_SECP160R2 => 160, + self::CURVE_SECP192K1 => 192, + self::CURVE_SECP224K1 => 224, + self::CURVE_SECP224R1 => 224, + self::CURVE_SECP256K1 => 256, + self::CURVE_SECP384R1 => 384, + self::CURVE_SECP521R1 => 521, + ]; + + /** + * @param string $namedCurve Curve identifier + */ + private function __construct( + private readonly string $namedCurve + ) { + parent::__construct(self::OID_EC_PUBLIC_KEY); + } + + public static function create(string $namedCurve): self + { + return new self($namedCurve); + } + + public function name(): string + { + return 'ecPublicKey'; + } + + /** + * @return self + */ + public static function fromASN1Params(?UnspecifiedType $params = null): SpecificAlgorithmIdentifier + { + if (! isset($params)) { + throw new UnexpectedValueException('No parameters.'); + } + $named_curve = $params->asObjectIdentifier() + ->oid(); + return self::create($named_curve); + } + + /** + * Get OID of the named curve. + */ + public function namedCurve(): string + { + return $this->namedCurve; + } + + /** + * @return ObjectIdentifier + */ + protected function paramsASN1(): ?Element + { + return ObjectIdentifier::create($this->namedCurve); + } +} diff --git a/3rdparty/spomky-labs/pki-framework/src/CryptoTypes/AlgorithmIdentifier/Asymmetric/Ed25519AlgorithmIdentifier.php b/3rdparty/spomky-labs/pki-framework/src/CryptoTypes/AlgorithmIdentifier/Asymmetric/Ed25519AlgorithmIdentifier.php new file mode 100644 index 00000000..1ac5f807 --- /dev/null +++ b/3rdparty/spomky-labs/pki-framework/src/CryptoTypes/AlgorithmIdentifier/Asymmetric/Ed25519AlgorithmIdentifier.php @@ -0,0 +1,52 @@ +oid() === self::OID_ED25519; + } +} diff --git a/3rdparty/spomky-labs/pki-framework/src/CryptoTypes/AlgorithmIdentifier/Asymmetric/Ed448AlgorithmIdentifier.php b/3rdparty/spomky-labs/pki-framework/src/CryptoTypes/AlgorithmIdentifier/Asymmetric/Ed448AlgorithmIdentifier.php new file mode 100644 index 00000000..327fefbe --- /dev/null +++ b/3rdparty/spomky-labs/pki-framework/src/CryptoTypes/AlgorithmIdentifier/Asymmetric/Ed448AlgorithmIdentifier.php @@ -0,0 +1,52 @@ +oid() === self::OID_ED448; + } +} diff --git a/3rdparty/spomky-labs/pki-framework/src/CryptoTypes/AlgorithmIdentifier/Asymmetric/RFC8410EdAlgorithmIdentifier.php b/3rdparty/spomky-labs/pki-framework/src/CryptoTypes/AlgorithmIdentifier/Asymmetric/RFC8410EdAlgorithmIdentifier.php new file mode 100644 index 00000000..133f6b99 --- /dev/null +++ b/3rdparty/spomky-labs/pki-framework/src/CryptoTypes/AlgorithmIdentifier/Asymmetric/RFC8410EdAlgorithmIdentifier.php @@ -0,0 +1,39 @@ +asNull(); + return self::create(); + } + + /** + * @return NullType + */ + protected function paramsASN1(): ?Element + { + return NullType::create(); + } +} diff --git a/3rdparty/spomky-labs/pki-framework/src/CryptoTypes/AlgorithmIdentifier/Asymmetric/RSAPSSSSAEncryptionAlgorithmIdentifier.php b/3rdparty/spomky-labs/pki-framework/src/CryptoTypes/AlgorithmIdentifier/Asymmetric/RSAPSSSSAEncryptionAlgorithmIdentifier.php new file mode 100644 index 00000000..34aebd08 --- /dev/null +++ b/3rdparty/spomky-labs/pki-framework/src/CryptoTypes/AlgorithmIdentifier/Asymmetric/RSAPSSSSAEncryptionAlgorithmIdentifier.php @@ -0,0 +1,63 @@ +asNull(); + return self::create(); + } + + /** + * @return NullType + */ + protected function paramsASN1(): ?Element + { + return NullType::create(); + } +} diff --git a/3rdparty/spomky-labs/pki-framework/src/CryptoTypes/AlgorithmIdentifier/Asymmetric/X25519AlgorithmIdentifier.php b/3rdparty/spomky-labs/pki-framework/src/CryptoTypes/AlgorithmIdentifier/Asymmetric/X25519AlgorithmIdentifier.php new file mode 100644 index 00000000..7f362d7a --- /dev/null +++ b/3rdparty/spomky-labs/pki-framework/src/CryptoTypes/AlgorithmIdentifier/Asymmetric/X25519AlgorithmIdentifier.php @@ -0,0 +1,43 @@ +asOctetString() + ->string(); + return self::create($iv); + } + + public function name(): string + { + return 'aes128-CBC'; + } + + public function keySize(): int + { + return 16; + } +} diff --git a/3rdparty/spomky-labs/pki-framework/src/CryptoTypes/AlgorithmIdentifier/Cipher/AES192CBCAlgorithmIdentifier.php b/3rdparty/spomky-labs/pki-framework/src/CryptoTypes/AlgorithmIdentifier/Cipher/AES192CBCAlgorithmIdentifier.php new file mode 100644 index 00000000..27780936 --- /dev/null +++ b/3rdparty/spomky-labs/pki-framework/src/CryptoTypes/AlgorithmIdentifier/Cipher/AES192CBCAlgorithmIdentifier.php @@ -0,0 +1,55 @@ +asOctetString() + ->string(); + return self::create($iv); + } + + public function name(): string + { + return 'aes192-CBC'; + } + + public function keySize(): int + { + return 24; + } +} diff --git a/3rdparty/spomky-labs/pki-framework/src/CryptoTypes/AlgorithmIdentifier/Cipher/AES256CBCAlgorithmIdentifier.php b/3rdparty/spomky-labs/pki-framework/src/CryptoTypes/AlgorithmIdentifier/Cipher/AES256CBCAlgorithmIdentifier.php new file mode 100644 index 00000000..73e4e19f --- /dev/null +++ b/3rdparty/spomky-labs/pki-framework/src/CryptoTypes/AlgorithmIdentifier/Cipher/AES256CBCAlgorithmIdentifier.php @@ -0,0 +1,55 @@ +asOctetString() + ->string(); + return new static($iv); + } + + public function name(): string + { + return 'aes256-CBC'; + } + + public function keySize(): int + { + return 32; + } +} diff --git a/3rdparty/spomky-labs/pki-framework/src/CryptoTypes/AlgorithmIdentifier/Cipher/AESCBCAlgorithmIdentifier.php b/3rdparty/spomky-labs/pki-framework/src/CryptoTypes/AlgorithmIdentifier/Cipher/AESCBCAlgorithmIdentifier.php new file mode 100644 index 00000000..f1e9f5ad --- /dev/null +++ b/3rdparty/spomky-labs/pki-framework/src/CryptoTypes/AlgorithmIdentifier/Cipher/AESCBCAlgorithmIdentifier.php @@ -0,0 +1,42 @@ +initializationVector)) { + throw new LogicException('IV not set.'); + } + return OctetString::create($this->initializationVector); + } +} diff --git a/3rdparty/spomky-labs/pki-framework/src/CryptoTypes/AlgorithmIdentifier/Cipher/BlockCipherAlgorithmIdentifier.php b/3rdparty/spomky-labs/pki-framework/src/CryptoTypes/AlgorithmIdentifier/Cipher/BlockCipherAlgorithmIdentifier.php new file mode 100644 index 00000000..08be2a31 --- /dev/null +++ b/3rdparty/spomky-labs/pki-framework/src/CryptoTypes/AlgorithmIdentifier/Cipher/BlockCipherAlgorithmIdentifier.php @@ -0,0 +1,16 @@ +_checkIVSize($initializationVector); + parent::__construct($oid); + } + + /** + * Get key size in bytes. + */ + abstract public function keySize(): int; + + /** + * Get the initialization vector size in bytes. + */ + abstract public function ivSize(): int; + + /** + * Get initialization vector. + */ + public function initializationVector(): string + { + return $this->initializationVector; + } + + /** + * Get copy of the object with given initialization vector. + * + * @param string $iv Initialization vector or null to remove + */ + public function withInitializationVector(string $iv): self + { + $this->_checkIVSize($iv); + $obj = clone $this; + $obj->initializationVector = $iv; + return $obj; + } + + /** + * Check that initialization vector size is valid for the cipher. + */ + protected function _checkIVSize(string $iv): void + { + if (mb_strlen($iv, '8bit') !== $this->ivSize()) { + throw new UnexpectedValueException('Invalid IV size.'); + } + } +} diff --git a/3rdparty/spomky-labs/pki-framework/src/CryptoTypes/AlgorithmIdentifier/Cipher/DESCBCAlgorithmIdentifier.php b/3rdparty/spomky-labs/pki-framework/src/CryptoTypes/AlgorithmIdentifier/Cipher/DESCBCAlgorithmIdentifier.php new file mode 100644 index 00000000..efd7fbfe --- /dev/null +++ b/3rdparty/spomky-labs/pki-framework/src/CryptoTypes/AlgorithmIdentifier/Cipher/DESCBCAlgorithmIdentifier.php @@ -0,0 +1,86 @@ +_checkIVSize($iv); + parent::__construct(self::OID_DES_CBC, $iv); + } + + public static function create(?string $iv = null): self + { + return new self($iv); + } + + public function name(): string + { + return 'desCBC'; + } + + /** + * @return self + */ + public static function fromASN1Params(?UnspecifiedType $params = null): SpecificAlgorithmIdentifier + { + if (! isset($params)) { + throw new UnexpectedValueException('No parameters.'); + } + $iv = $params->asOctetString() + ->string(); + return self::create($iv); + } + + public function blockSize(): int + { + return 8; + } + + public function keySize(): int + { + return 8; + } + + public function ivSize(): int + { + return 8; + } + + /** + * @return OctetString + */ + protected function paramsASN1(): ?Element + { + if (! isset($this->initializationVector)) { + throw new LogicException('IV not set.'); + } + return OctetString::create($this->initializationVector); + } +} diff --git a/3rdparty/spomky-labs/pki-framework/src/CryptoTypes/AlgorithmIdentifier/Cipher/DESEDE3CBCAlgorithmIdentifier.php b/3rdparty/spomky-labs/pki-framework/src/CryptoTypes/AlgorithmIdentifier/Cipher/DESEDE3CBCAlgorithmIdentifier.php new file mode 100644 index 00000000..705475d7 --- /dev/null +++ b/3rdparty/spomky-labs/pki-framework/src/CryptoTypes/AlgorithmIdentifier/Cipher/DESEDE3CBCAlgorithmIdentifier.php @@ -0,0 +1,87 @@ +_checkIVSize($iv); + } + + public static function create(?string $iv = null): self + { + return new self($iv); + } + + public function name(): string + { + return 'des-EDE3-CBC'; + } + + /** + * @return self + */ + public static function fromASN1Params(?UnspecifiedType $params = null): SpecificAlgorithmIdentifier + { + if (! isset($params)) { + throw new UnexpectedValueException('No parameters.'); + } + $iv = $params->asOctetString() + ->string(); + return self::create($iv); + } + + public function blockSize(): int + { + return 8; + } + + public function keySize(): int + { + return 24; + } + + public function ivSize(): int + { + return 8; + } + + /** + * @return OctetString + */ + protected function paramsASN1(): ?Element + { + if (! isset($this->initializationVector)) { + throw new LogicException('IV not set.'); + } + return OctetString::create($this->initializationVector); + } +} diff --git a/3rdparty/spomky-labs/pki-framework/src/CryptoTypes/AlgorithmIdentifier/Cipher/RC2CBCAlgorithmIdentifier.php b/3rdparty/spomky-labs/pki-framework/src/CryptoTypes/AlgorithmIdentifier/Cipher/RC2CBCAlgorithmIdentifier.php new file mode 100644 index 00000000..d8d1043d --- /dev/null +++ b/3rdparty/spomky-labs/pki-framework/src/CryptoTypes/AlgorithmIdentifier/Cipher/RC2CBCAlgorithmIdentifier.php @@ -0,0 +1,200 @@ + ekb => version + */ + private const EKB_TABLE = [ + 0xbd, 0x56, 0xea, 0xf2, 0xa2, 0xf1, 0xac, 0x2a, + 0xb0, 0x93, 0xd1, 0x9c, 0x1b, 0x33, 0xfd, 0xd0, + 0x30, 0x04, 0xb6, 0xdc, 0x7d, 0xdf, 0x32, 0x4b, + 0xf7, 0xcb, 0x45, 0x9b, 0x31, 0xbb, 0x21, 0x5a, + 0x41, 0x9f, 0xe1, 0xd9, 0x4a, 0x4d, 0x9e, 0xda, + 0xa0, 0x68, 0x2c, 0xc3, 0x27, 0x5f, 0x80, 0x36, + 0x3e, 0xee, 0xfb, 0x95, 0x1a, 0xfe, 0xce, 0xa8, + 0x34, 0xa9, 0x13, 0xf0, 0xa6, 0x3f, 0xd8, 0x0c, + 0x78, 0x24, 0xaf, 0x23, 0x52, 0xc1, 0x67, 0x17, + 0xf5, 0x66, 0x90, 0xe7, 0xe8, 0x07, 0xb8, 0x60, + 0x48, 0xe6, 0x1e, 0x53, 0xf3, 0x92, 0xa4, 0x72, + 0x8c, 0x08, 0x15, 0x6e, 0x86, 0x00, 0x84, 0xfa, + 0xf4, 0x7f, 0x8a, 0x42, 0x19, 0xf6, 0xdb, 0xcd, + 0x14, 0x8d, 0x50, 0x12, 0xba, 0x3c, 0x06, 0x4e, + 0xec, 0xb3, 0x35, 0x11, 0xa1, 0x88, 0x8e, 0x2b, + 0x94, 0x99, 0xb7, 0x71, 0x74, 0xd3, 0xe4, 0xbf, + 0x3a, 0xde, 0x96, 0x0e, 0xbc, 0x0a, 0xed, 0x77, + 0xfc, 0x37, 0x6b, 0x03, 0x79, 0x89, 0x62, 0xc6, + 0xd7, 0xc0, 0xd2, 0x7c, 0x6a, 0x8b, 0x22, 0xa3, + 0x5b, 0x05, 0x5d, 0x02, 0x75, 0xd5, 0x61, 0xe3, + 0x18, 0x8f, 0x55, 0x51, 0xad, 0x1f, 0x0b, 0x5e, + 0x85, 0xe5, 0xc2, 0x57, 0x63, 0xca, 0x3d, 0x6c, + 0xb4, 0xc5, 0xcc, 0x70, 0xb2, 0x91, 0x59, 0x0d, + 0x47, 0x20, 0xc8, 0x4f, 0x58, 0xe0, 0x01, 0xe2, + 0x16, 0x38, 0xc4, 0x6f, 0x3b, 0x0f, 0x65, 0x46, + 0xbe, 0x7e, 0x2d, 0x7b, 0x82, 0xf9, 0x40, 0xb5, + 0x1d, 0x73, 0xf8, 0xeb, 0x26, 0xc7, 0x87, 0x97, + 0x25, 0x54, 0xb1, 0x28, 0xaa, 0x98, 0x9d, 0xa5, + 0x64, 0x6d, 0x7a, 0xd4, 0x10, 0x81, 0x44, 0xef, + 0x49, 0xd6, 0xae, 0x2e, 0xdd, 0x76, 0x5c, 0x2f, + 0xa7, 0x1c, 0xc9, 0x09, 0x69, 0x9a, 0x83, 0xcf, + 0x29, 0x39, 0xb9, 0xe9, 0x4c, 0xff, 0x43, 0xab, + ]; + + /** + * @param int $effectiveKeyBits Number of effective key bits + * @param null|string $iv Initialization vector + */ + private function __construct( + private readonly int $effectiveKeyBits, + ?string $iv + ) { + parent::__construct(self::OID_RC2_CBC, $iv); + $this->_checkIVSize($iv); + } + + public static function create(int $_effectiveKeyBits = 64, ?string $iv = null): self + { + return new self($_effectiveKeyBits, $iv); + } + + public function name(): string + { + return 'rc2-cbc'; + } + + /** + * @return self + */ + public static function fromASN1Params(?UnspecifiedType $params = null): SpecificAlgorithmIdentifier + { + if (! isset($params)) { + throw new UnexpectedValueException('No parameters.'); + } + $key_bits = 32; + // rfc2268 a choice containing only IV + if ($params->isType(Element::TYPE_OCTET_STRING)) { + $iv = $params->asOctetString() + ->string(); + } else { + $seq = $params->asSequence(); + $idx = 0; + // version is optional in rfc2898 + if ($seq->has($idx, Element::TYPE_INTEGER)) { + $version = $seq->at($idx++) + ->asInteger() + ->intNumber(); + $key_bits = self::_versionToEKB($version); + } + // IV is present in all variants + $iv = $seq->at($idx) + ->asOctetString() + ->string(); + } + return self::create($key_bits, $iv); + } + + /** + * Get number of effective key bits. + */ + public function effectiveKeyBits(): int + { + return $this->effectiveKeyBits; + } + + public function blockSize(): int + { + return 8; + } + + public function keySize(): int + { + return (int) round($this->effectiveKeyBits / 8); + } + + public function ivSize(): int + { + return 8; + } + + /** + * @return Sequence + */ + protected function paramsASN1(): ?Element + { + if ($this->effectiveKeyBits >= 256) { + $version = $this->effectiveKeyBits; + } else { + $version = self::EKB_TABLE[$this->effectiveKeyBits]; + } + if (! isset($this->initializationVector)) { + throw new LogicException('IV not set.'); + } + return Sequence::create(Integer::create($version), OctetString::create($this->initializationVector)); + } + + /** + * Translate version number to number of effective key bits. + */ + private static function _versionToEKB(int $version): int + { + static $lut; + if ($version > 255) { + return $version; + } + if (! isset($lut)) { + $lut = array_flip(self::EKB_TABLE); + } + return $lut[$version]; + } +} diff --git a/3rdparty/spomky-labs/pki-framework/src/CryptoTypes/AlgorithmIdentifier/Feature/AlgorithmIdentifierType.php b/3rdparty/spomky-labs/pki-framework/src/CryptoTypes/AlgorithmIdentifier/Feature/AlgorithmIdentifierType.php new file mode 100644 index 00000000..6a84b05c --- /dev/null +++ b/3rdparty/spomky-labs/pki-framework/src/CryptoTypes/AlgorithmIdentifier/Feature/AlgorithmIdentifierType.php @@ -0,0 +1,30 @@ +oid; + } + + public function parameters(): ?UnspecifiedType + { + return $this->params; + } + + protected function paramsASN1(): ?Element + { + return $this->params?->asElement(); + } +} diff --git a/3rdparty/spomky-labs/pki-framework/src/CryptoTypes/AlgorithmIdentifier/Hash/HMACWithSHA1AlgorithmIdentifier.php b/3rdparty/spomky-labs/pki-framework/src/CryptoTypes/AlgorithmIdentifier/Hash/HMACWithSHA1AlgorithmIdentifier.php new file mode 100644 index 00000000..4abf7901 --- /dev/null +++ b/3rdparty/spomky-labs/pki-framework/src/CryptoTypes/AlgorithmIdentifier/Hash/HMACWithSHA1AlgorithmIdentifier.php @@ -0,0 +1,60 @@ +asNull()); + } + + public function name(): string + { + return 'hmacWithSHA224'; + } +} diff --git a/3rdparty/spomky-labs/pki-framework/src/CryptoTypes/AlgorithmIdentifier/Hash/HMACWithSHA256AlgorithmIdentifier.php b/3rdparty/spomky-labs/pki-framework/src/CryptoTypes/AlgorithmIdentifier/Hash/HMACWithSHA256AlgorithmIdentifier.php new file mode 100644 index 00000000..bd794d43 --- /dev/null +++ b/3rdparty/spomky-labs/pki-framework/src/CryptoTypes/AlgorithmIdentifier/Hash/HMACWithSHA256AlgorithmIdentifier.php @@ -0,0 +1,40 @@ +asNull()); + } + + public function name(): string + { + return 'hmacWithSHA256'; + } +} diff --git a/3rdparty/spomky-labs/pki-framework/src/CryptoTypes/AlgorithmIdentifier/Hash/HMACWithSHA384AlgorithmIdentifier.php b/3rdparty/spomky-labs/pki-framework/src/CryptoTypes/AlgorithmIdentifier/Hash/HMACWithSHA384AlgorithmIdentifier.php new file mode 100644 index 00000000..7fe1dacb --- /dev/null +++ b/3rdparty/spomky-labs/pki-framework/src/CryptoTypes/AlgorithmIdentifier/Hash/HMACWithSHA384AlgorithmIdentifier.php @@ -0,0 +1,40 @@ +asNull()); + } + + public function name(): string + { + return 'hmacWithSHA384'; + } +} diff --git a/3rdparty/spomky-labs/pki-framework/src/CryptoTypes/AlgorithmIdentifier/Hash/HMACWithSHA512AlgorithmIdentifier.php b/3rdparty/spomky-labs/pki-framework/src/CryptoTypes/AlgorithmIdentifier/Hash/HMACWithSHA512AlgorithmIdentifier.php new file mode 100644 index 00000000..5d5ecdd6 --- /dev/null +++ b/3rdparty/spomky-labs/pki-framework/src/CryptoTypes/AlgorithmIdentifier/Hash/HMACWithSHA512AlgorithmIdentifier.php @@ -0,0 +1,40 @@ +asNull()); + } + + public function name(): string + { + return 'hmacWithSHA512'; + } +} diff --git a/3rdparty/spomky-labs/pki-framework/src/CryptoTypes/AlgorithmIdentifier/Hash/MD5AlgorithmIdentifier.php b/3rdparty/spomky-labs/pki-framework/src/CryptoTypes/AlgorithmIdentifier/Hash/MD5AlgorithmIdentifier.php new file mode 100644 index 00000000..94345cf5 --- /dev/null +++ b/3rdparty/spomky-labs/pki-framework/src/CryptoTypes/AlgorithmIdentifier/Hash/MD5AlgorithmIdentifier.php @@ -0,0 +1,74 @@ +params = NullType::create(); + } + + public static function create(): self + { + return new self(); + } + + public function name(): string + { + return 'md5'; + } + + public static function fromASN1Params(?UnspecifiedType $params = null): static + { + $obj = static::create(); + // if parameters field is present, it must be null type + if (isset($params)) { + $obj->params = $params->asNull(); + } + return $obj; + } + + /** + * @return null|NullType + */ + protected function paramsASN1(): ?Element + { + return $this->params; + } +} diff --git a/3rdparty/spomky-labs/pki-framework/src/CryptoTypes/AlgorithmIdentifier/Hash/RFC4231HMACAlgorithmIdentifier.php b/3rdparty/spomky-labs/pki-framework/src/CryptoTypes/AlgorithmIdentifier/Hash/RFC4231HMACAlgorithmIdentifier.php new file mode 100644 index 00000000..b341c6ea --- /dev/null +++ b/3rdparty/spomky-labs/pki-framework/src/CryptoTypes/AlgorithmIdentifier/Hash/RFC4231HMACAlgorithmIdentifier.php @@ -0,0 +1,37 @@ +params; + } +} diff --git a/3rdparty/spomky-labs/pki-framework/src/CryptoTypes/AlgorithmIdentifier/Hash/SHA1AlgorithmIdentifier.php b/3rdparty/spomky-labs/pki-framework/src/CryptoTypes/AlgorithmIdentifier/Hash/SHA1AlgorithmIdentifier.php new file mode 100644 index 00000000..2eec340f --- /dev/null +++ b/3rdparty/spomky-labs/pki-framework/src/CryptoTypes/AlgorithmIdentifier/Hash/SHA1AlgorithmIdentifier.php @@ -0,0 +1,70 @@ +params = null; + } + + public static function create(): self + { + return new self(); + } + + public function name(): string + { + return 'sha1'; + } + + public static function fromASN1Params(?UnspecifiedType $params = null): static + { + $obj = static::create(); + // if parameters field is present, it must be null type + if (isset($params)) { + $obj->params = $params->asNull(); + } + return $obj; + } + + /** + * @return null|NullType + */ + protected function paramsASN1(): ?Element + { + return $this->params; + } +} diff --git a/3rdparty/spomky-labs/pki-framework/src/CryptoTypes/AlgorithmIdentifier/Hash/SHA224AlgorithmIdentifier.php b/3rdparty/spomky-labs/pki-framework/src/CryptoTypes/AlgorithmIdentifier/Hash/SHA224AlgorithmIdentifier.php new file mode 100644 index 00000000..ba547c1d --- /dev/null +++ b/3rdparty/spomky-labs/pki-framework/src/CryptoTypes/AlgorithmIdentifier/Hash/SHA224AlgorithmIdentifier.php @@ -0,0 +1,47 @@ +_params = $params->asNull(); + } + return $obj; + } + + public function name(): string + { + return 'sha224'; + } +} diff --git a/3rdparty/spomky-labs/pki-framework/src/CryptoTypes/AlgorithmIdentifier/Hash/SHA256AlgorithmIdentifier.php b/3rdparty/spomky-labs/pki-framework/src/CryptoTypes/AlgorithmIdentifier/Hash/SHA256AlgorithmIdentifier.php new file mode 100644 index 00000000..638f45a9 --- /dev/null +++ b/3rdparty/spomky-labs/pki-framework/src/CryptoTypes/AlgorithmIdentifier/Hash/SHA256AlgorithmIdentifier.php @@ -0,0 +1,46 @@ +_params = $params->asNull(); + } + return $obj; + } + + public function name(): string + { + return 'sha256'; + } +} diff --git a/3rdparty/spomky-labs/pki-framework/src/CryptoTypes/AlgorithmIdentifier/Hash/SHA2AlgorithmIdentifier.php b/3rdparty/spomky-labs/pki-framework/src/CryptoTypes/AlgorithmIdentifier/Hash/SHA2AlgorithmIdentifier.php new file mode 100644 index 00000000..97921416 --- /dev/null +++ b/3rdparty/spomky-labs/pki-framework/src/CryptoTypes/AlgorithmIdentifier/Hash/SHA2AlgorithmIdentifier.php @@ -0,0 +1,39 @@ +_params; + } +} diff --git a/3rdparty/spomky-labs/pki-framework/src/CryptoTypes/AlgorithmIdentifier/Hash/SHA384AlgorithmIdentifier.php b/3rdparty/spomky-labs/pki-framework/src/CryptoTypes/AlgorithmIdentifier/Hash/SHA384AlgorithmIdentifier.php new file mode 100644 index 00000000..f9fbdbff --- /dev/null +++ b/3rdparty/spomky-labs/pki-framework/src/CryptoTypes/AlgorithmIdentifier/Hash/SHA384AlgorithmIdentifier.php @@ -0,0 +1,46 @@ +_params = $params->asNull(); + } + return $obj; + } + + public function name(): string + { + return 'sha384'; + } +} diff --git a/3rdparty/spomky-labs/pki-framework/src/CryptoTypes/AlgorithmIdentifier/Hash/SHA512AlgorithmIdentifier.php b/3rdparty/spomky-labs/pki-framework/src/CryptoTypes/AlgorithmIdentifier/Hash/SHA512AlgorithmIdentifier.php new file mode 100644 index 00000000..0294aa35 --- /dev/null +++ b/3rdparty/spomky-labs/pki-framework/src/CryptoTypes/AlgorithmIdentifier/Hash/SHA512AlgorithmIdentifier.php @@ -0,0 +1,46 @@ +_params = $params->asNull(); + } + return $obj; + } + + public function name(): string + { + return 'sha512'; + } +} diff --git a/3rdparty/spomky-labs/pki-framework/src/CryptoTypes/AlgorithmIdentifier/Signature/ECDSAWithSHA1AlgorithmIdentifier.php b/3rdparty/spomky-labs/pki-framework/src/CryptoTypes/AlgorithmIdentifier/Signature/ECDSAWithSHA1AlgorithmIdentifier.php new file mode 100644 index 00000000..fe462024 --- /dev/null +++ b/3rdparty/spomky-labs/pki-framework/src/CryptoTypes/AlgorithmIdentifier/Signature/ECDSAWithSHA1AlgorithmIdentifier.php @@ -0,0 +1,39 @@ +oid() === self::OID_EC_PUBLIC_KEY; + } + + protected function paramsASN1(): ?Element + { + return null; + } +} diff --git a/3rdparty/spomky-labs/pki-framework/src/CryptoTypes/AlgorithmIdentifier/Signature/MD2WithRSAEncryptionAlgorithmIdentifier.php b/3rdparty/spomky-labs/pki-framework/src/CryptoTypes/AlgorithmIdentifier/Signature/MD2WithRSAEncryptionAlgorithmIdentifier.php new file mode 100644 index 00000000..c9f216ec --- /dev/null +++ b/3rdparty/spomky-labs/pki-framework/src/CryptoTypes/AlgorithmIdentifier/Signature/MD2WithRSAEncryptionAlgorithmIdentifier.php @@ -0,0 +1,39 @@ +params; + } +} diff --git a/3rdparty/spomky-labs/pki-framework/src/CryptoTypes/AlgorithmIdentifier/Signature/RSASignatureAlgorithmIdentifier.php b/3rdparty/spomky-labs/pki-framework/src/CryptoTypes/AlgorithmIdentifier/Signature/RSASignatureAlgorithmIdentifier.php new file mode 100644 index 00000000..65f7600d --- /dev/null +++ b/3rdparty/spomky-labs/pki-framework/src/CryptoTypes/AlgorithmIdentifier/Signature/RSASignatureAlgorithmIdentifier.php @@ -0,0 +1,20 @@ +oid() === self::OID_RSA_ENCRYPTION; + } +} diff --git a/3rdparty/spomky-labs/pki-framework/src/CryptoTypes/AlgorithmIdentifier/Signature/SHA1WithRSAEncryptionAlgorithmIdentifier.php b/3rdparty/spomky-labs/pki-framework/src/CryptoTypes/AlgorithmIdentifier/Signature/SHA1WithRSAEncryptionAlgorithmIdentifier.php new file mode 100644 index 00000000..22a7b631 --- /dev/null +++ b/3rdparty/spomky-labs/pki-framework/src/CryptoTypes/AlgorithmIdentifier/Signature/SHA1WithRSAEncryptionAlgorithmIdentifier.php @@ -0,0 +1,40 @@ +asNull(); + return self::create(); + } + + public function name(): string + { + return 'sha1-with-rsa-signature'; + } +} diff --git a/3rdparty/spomky-labs/pki-framework/src/CryptoTypes/AlgorithmIdentifier/Signature/SHA224WithRSAEncryptionAlgorithmIdentifier.php b/3rdparty/spomky-labs/pki-framework/src/CryptoTypes/AlgorithmIdentifier/Signature/SHA224WithRSAEncryptionAlgorithmIdentifier.php new file mode 100644 index 00000000..15fa6507 --- /dev/null +++ b/3rdparty/spomky-labs/pki-framework/src/CryptoTypes/AlgorithmIdentifier/Signature/SHA224WithRSAEncryptionAlgorithmIdentifier.php @@ -0,0 +1,37 @@ +asElement()); + } + + public function name(): string + { + return 'sha224WithRSAEncryption'; + } +} diff --git a/3rdparty/spomky-labs/pki-framework/src/CryptoTypes/AlgorithmIdentifier/Signature/SHA256WithRSAEncryptionAlgorithmIdentifier.php b/3rdparty/spomky-labs/pki-framework/src/CryptoTypes/AlgorithmIdentifier/Signature/SHA256WithRSAEncryptionAlgorithmIdentifier.php new file mode 100644 index 00000000..be749d4c --- /dev/null +++ b/3rdparty/spomky-labs/pki-framework/src/CryptoTypes/AlgorithmIdentifier/Signature/SHA256WithRSAEncryptionAlgorithmIdentifier.php @@ -0,0 +1,37 @@ +asElement()); + } + + public function name(): string + { + return 'sha256WithRSAEncryption'; + } +} diff --git a/3rdparty/spomky-labs/pki-framework/src/CryptoTypes/AlgorithmIdentifier/Signature/SHA384WithRSAEncryptionAlgorithmIdentifier.php b/3rdparty/spomky-labs/pki-framework/src/CryptoTypes/AlgorithmIdentifier/Signature/SHA384WithRSAEncryptionAlgorithmIdentifier.php new file mode 100644 index 00000000..76407874 --- /dev/null +++ b/3rdparty/spomky-labs/pki-framework/src/CryptoTypes/AlgorithmIdentifier/Signature/SHA384WithRSAEncryptionAlgorithmIdentifier.php @@ -0,0 +1,37 @@ +asElement()); + } + + public function name(): string + { + return 'sha384WithRSAEncryption'; + } +} diff --git a/3rdparty/spomky-labs/pki-framework/src/CryptoTypes/AlgorithmIdentifier/Signature/SHA512WithRSAEncryptionAlgorithmIdentifier.php b/3rdparty/spomky-labs/pki-framework/src/CryptoTypes/AlgorithmIdentifier/Signature/SHA512WithRSAEncryptionAlgorithmIdentifier.php new file mode 100644 index 00000000..58a7f3fb --- /dev/null +++ b/3rdparty/spomky-labs/pki-framework/src/CryptoTypes/AlgorithmIdentifier/Signature/SHA512WithRSAEncryptionAlgorithmIdentifier.php @@ -0,0 +1,37 @@ +asElement()); + } + + public function name(): string + { + return 'sha512WithRSAEncryption'; + } +} diff --git a/3rdparty/spomky-labs/pki-framework/src/CryptoTypes/AlgorithmIdentifier/SpecificAlgorithmIdentifier.php b/3rdparty/spomky-labs/pki-framework/src/CryptoTypes/AlgorithmIdentifier/SpecificAlgorithmIdentifier.php new file mode 100644 index 00000000..18da7542 --- /dev/null +++ b/3rdparty/spomky-labs/pki-framework/src/CryptoTypes/AlgorithmIdentifier/SpecificAlgorithmIdentifier.php @@ -0,0 +1,20 @@ + $value->toAttribute(), $values)); + } + + // Nothing yet. Extended from base class for future extensions. +} diff --git a/3rdparty/spomky-labs/pki-framework/src/CryptoTypes/Asymmetric/EC/ECConversion.php b/3rdparty/spomky-labs/pki-framework/src/CryptoTypes/Asymmetric/EC/ECConversion.php new file mode 100644 index 00000000..05cd130b --- /dev/null +++ b/3rdparty/spomky-labs/pki-framework/src/CryptoTypes/Asymmetric/EC/ECConversion.php @@ -0,0 +1,109 @@ +string(); + if ($bs->unusedBits() !== 0) { + // @todo pad string + throw new RuntimeException('Unaligned bitstrings to supported'); + } + return OctetString::create($str); + } + + /** + * Perform Octet-String-to-Bit-String Conversion. + * + * Defined in SEC 1 section 2.3.2. + */ + public static function octetStringToBitString(OctetString $os): BitString + { + return BitString::create($os->string()); + } + + /** + * Perform Integer-to-Octet-String Conversion. + * + * Defined in SEC 1 section 2.3.7. + * + * @param null|int $mlen Optional desired output length + */ + public static function integerToOctetString(Integer $num, ?int $mlen = null): OctetString + { + $bigInteger = BigInteger::of($num->getValue()); + $str = $bigInteger->toBytes(false); + if ($mlen !== null) { + $len = mb_strlen($str, '8bit'); + if ($len > $mlen) { + throw new RangeException('Number is too large.'); + } + // pad with zeroes + if ($len < $mlen) { + $str = str_repeat("\0", $mlen - $len) . $str; + } + } + return OctetString::create($str); + } + + /** + * Perform Octet-String-to-Integer Conversion. + * + * Defined in SEC 1 section 2.3.8. + */ + public static function octetStringToInteger(OctetString $os): Integer + { + $num = BigInteger::fromBytes($os->string(), false); + + return Integer::create($num); + } + + /** + * Convert a base-10 number to octets. + * + * This is a convenicence method for integer <-> octet string conversion without the need for external ASN.1 + * dependencies. + * + * @param int|string $num Number in base-10 + * @param null|int $mlen Optional desired output length + */ + public static function numberToOctets(int|string $num, ?int $mlen = null): string + { + return self::integerToOctetString(Integer::create($num), $mlen)->string(); + } + + /** + * Convert octets to a base-10 number. + * + * This is a convenicence method for integer <-> octet string conversion without the need for external ASN.1 + * dependencies. + * + * @return string Number in base-10 + */ + public static function octetsToNumber(string $str): string + { + return self::octetStringToInteger(OctetString::create($str))->number(); + } +} diff --git a/3rdparty/spomky-labs/pki-framework/src/CryptoTypes/Asymmetric/EC/ECPrivateKey.php b/3rdparty/spomky-labs/pki-framework/src/CryptoTypes/Asymmetric/EC/ECPrivateKey.php new file mode 100644 index 00000000..cb9a8f02 --- /dev/null +++ b/3rdparty/spomky-labs/pki-framework/src/CryptoTypes/Asymmetric/EC/ECPrivateKey.php @@ -0,0 +1,190 @@ +at(0) + ->asInteger() + ->intNumber(); + if ($version !== 1) { + throw new UnexpectedValueException('Version must be 1.'); + } + $private_key = $seq->at(1) + ->asOctetString() + ->string(); + $named_curve = null; + if ($seq->hasTagged(0)) { + $params = $seq->getTagged(0) + ->asExplicit(); + $named_curve = $params->asObjectIdentifier() + ->oid(); + } + $public_key = null; + if ($seq->hasTagged(1)) { + $public_key = $seq->getTagged(1) + ->asExplicit() + ->asBitString() + ->string(); + } + return self::create($private_key, $named_curve, $public_key); + } + + /** + * Initialize from DER data. + */ + public static function fromDER(string $data): self + { + return self::fromASN1(UnspecifiedType::fromDER($data)->asSequence()); + } + + /** + * @see PrivateKey::fromPEM() + */ + public static function fromPEM(PEM $pem): self + { + $pk = parent::fromPEM($pem); + if (! ($pk instanceof self)) { + throw new UnexpectedValueException('Not an EC private key.'); + } + return $pk; + } + + /** + * Get the EC private key value. + * + * @return string Octets of the private key + */ + public function privateKeyOctets(): string + { + return $this->privateKey; + } + + /** + * Whether named curve is present. + */ + public function hasNamedCurve(): bool + { + return isset($this->namedCurve); + } + + /** + * Get named curve OID. + */ + public function namedCurve(): string + { + if (! $this->hasNamedCurve()) { + throw new LogicException('namedCurve not set.'); + } + return $this->namedCurve; + } + + /** + * Get self with named curve. + * + * @param null|string $named_curve Named curve OID + */ + public function withNamedCurve(?string $named_curve): self + { + $obj = clone $this; + $obj->namedCurve = $named_curve; + return $obj; + } + + public function algorithmIdentifier(): AlgorithmIdentifierType + { + return ECPublicKeyAlgorithmIdentifier::create($this->namedCurve()); + } + + /** + * Whether public key is present. + */ + public function hasPublicKey(): bool + { + return isset($this->publicKey); + } + + /** + * @return ECPublicKey + */ + public function publicKey(): PublicKey + { + if (! $this->hasPublicKey()) { + throw new LogicException('publicKey not set.'); + } + return ECPublicKey::create($this->publicKey, $this->namedCurve()); + } + + /** + * Generate ASN.1 structure. + */ + public function toASN1(): Sequence + { + $elements = [Integer::create(1), OctetString::create($this->privateKey)]; + if (isset($this->namedCurve)) { + $elements[] = ExplicitlyTaggedType::create(0, ObjectIdentifier::create($this->namedCurve)); + } + if (isset($this->publicKey)) { + $elements[] = ExplicitlyTaggedType::create(1, BitString::create($this->publicKey)); + } + return Sequence::create(...$elements); + } + + public function toDER(): string + { + return $this->toASN1() + ->toDER(); + } + + public function toPEM(): PEM + { + return PEM::create(PEM::TYPE_EC_PRIVATE_KEY, $this->toDER()); + } +} diff --git a/3rdparty/spomky-labs/pki-framework/src/CryptoTypes/Asymmetric/EC/ECPublicKey.php b/3rdparty/spomky-labs/pki-framework/src/CryptoTypes/Asymmetric/EC/ECPublicKey.php new file mode 100644 index 00000000..5ff88773 --- /dev/null +++ b/3rdparty/spomky-labs/pki-framework/src/CryptoTypes/Asymmetric/EC/ECPublicKey.php @@ -0,0 +1,209 @@ +ecPoint = $ecPoint; + } + + public static function create(string $ecPoint, ?string $namedCurve = null): self + { + return new self($ecPoint, $namedCurve); + } + + /** + * Initialize from curve point coordinates. + * + * @param int|string $x X coordinate as a base10 number + * @param int|string $y Y coordinate as a base10 number + * @param null|string $named_curve Named curve OID + * @param null|int $bits Size of *p* in bits + */ + public static function fromCoordinates( + int|string $x, + int|string $y, + ?string $named_curve = null, + ?int $bits = null + ): self { + // if bitsize is not explicitly set, check from supported curves + if (! isset($bits) && isset($named_curve)) { + $bits = self::_curveSize($named_curve); + } + $mlen = null; + if (isset($bits)) { + $mlen = (int) ceil($bits / 8); + } + $x_os = ECConversion::integerToOctetString(Integer::create($x), $mlen)->string(); + $y_os = ECConversion::integerToOctetString(Integer::create($y), $mlen)->string(); + $ec_point = "\x4{$x_os}{$y_os}"; + return self::create($ec_point, $named_curve); + } + + /** + * @see PublicKey::fromPEM() + */ + public static function fromPEM(PEM $pem): self + { + if ($pem->type() !== PEM::TYPE_PUBLIC_KEY) { + throw new UnexpectedValueException('Not a public key.'); + } + $pki = PublicKeyInfo::fromDER($pem->data()); + $algo = $pki->algorithmIdentifier(); + if ($algo->oid() !== AlgorithmIdentifier::OID_EC_PUBLIC_KEY + || ! ($algo instanceof ECPublicKeyAlgorithmIdentifier)) { + throw new UnexpectedValueException('Not an elliptic curve key.'); + } + // ECPoint is directly mapped into public key data + return self::create($pki->publicKeyData()->string(), $algo->namedCurve()); + } + + /** + * Get ECPoint value. + */ + public function ECPoint(): string + { + return $this->ecPoint; + } + + /** + * Get curve point coordinates. + * + * @return string[] Tuple of X and Y coordinates as base-10 numbers + */ + public function curvePoint(): array + { + return array_map(static fn ($str) => ECConversion::octetsToNumber($str), $this->curvePointOctets()); + } + + /** + * Get curve point coordinates in octet string representation. + * + * @return string[] tuple of X and Y field elements as a string + */ + public function curvePointOctets(): array + { + if ($this->isCompressed()) { + throw new RuntimeException('EC point compression not supported.'); + } + $str = mb_substr($this->ecPoint, 1, null, '8bit'); + $length = (int) floor(mb_strlen($str, '8bit') / 2); + if ($length < 1) { + throw new RuntimeException('Invalid EC point.'); + } + [$x, $y] = mb_str_split($str, $length, '8bit'); + return [$x, $y]; + } + + /** + * Whether ECPoint is in compressed form. + */ + public function isCompressed(): bool + { + $c = ord($this->ecPoint[0]); + return $c !== 4; + } + + /** + * Whether named curve is present. + */ + public function hasNamedCurve(): bool + { + return isset($this->namedCurve); + } + + /** + * Get named curve OID. + */ + public function namedCurve(): string + { + if (! $this->hasNamedCurve()) { + throw new LogicException('namedCurve not set.'); + } + return $this->namedCurve; + } + + public function algorithmIdentifier(): AlgorithmIdentifierType + { + return ECPublicKeyAlgorithmIdentifier::create($this->namedCurve()); + } + + /** + * Generate ASN.1 element. + */ + public function toASN1(): OctetString + { + return OctetString::create($this->ecPoint); + } + + public function toDER(): string + { + return $this->toASN1() + ->toDER(); + } + + /** + * @see https://tools.ietf.org/html/rfc5480#section-2.2 + */ + public function subjectPublicKey(): BitString + { + // ECPoint is directly mapped to subjectPublicKey + return BitString::create($this->ecPoint); + } + + /** + * Get the curve size *p* in bits. + * + * @param string $oid Curve OID + */ + private static function _curveSize(string $oid): ?int + { + if (! array_key_exists($oid, ECPublicKeyAlgorithmIdentifier::MAP_CURVE_TO_SIZE)) { + return null; + } + return ECPublicKeyAlgorithmIdentifier::MAP_CURVE_TO_SIZE[$oid]; + } +} diff --git a/3rdparty/spomky-labs/pki-framework/src/CryptoTypes/Asymmetric/OneAsymmetricKey.php b/3rdparty/spomky-labs/pki-framework/src/CryptoTypes/Asymmetric/OneAsymmetricKey.php new file mode 100644 index 00000000..f7f29bf0 --- /dev/null +++ b/3rdparty/spomky-labs/pki-framework/src/CryptoTypes/Asymmetric/OneAsymmetricKey.php @@ -0,0 +1,322 @@ +version = $version ?? self::VERSION_2; + } + + public static function create( + AlgorithmIdentifierType $algo, + string $privateKeyData, + ?OneAsymmetricKeyAttributes $attributes = null, + ?BitString $publicKeyData = null + ): self { + return new self($algo, $privateKeyData, $attributes, $publicKeyData); + } + + /** + * Get self with version number. + */ + public function withVersion(int $version): self + { + $obj = clone $this; + $obj->version = $version; + return $obj; + } + + /** + * Initialize from ASN.1. + */ + public static function fromASN1(Sequence $seq): static + { + $version = $seq->at(0) + ->asInteger() + ->intNumber(); + if (! in_array($version, [self::VERSION_1, self::VERSION_2], true)) { + throw new UnexpectedValueException("Version {$version} not supported."); + } + $algo = AlgorithmIdentifier::fromASN1($seq->at(1)->asSequence()); + $key = $seq->at(2) + ->asOctetString() + ->string(); + $attribs = null; + if ($seq->hasTagged(0)) { + $attribs = OneAsymmetricKeyAttributes::fromASN1($seq->getTagged(0) + ->asImplicit(Element::TYPE_SET)->asSet()); + } + $pubkey = null; + if ($seq->hasTagged(1)) { + $pubkey = $seq->getTagged(1) + ->asImplicit(Element::TYPE_BIT_STRING)->asBitString(); + } + return static::create($algo, $key, $attribs, $pubkey)->withVersion($version); + } + + /** + * Initialize from DER data. + */ + public static function fromDER(string $data): static + { + return static::fromASN1(UnspecifiedType::fromDER($data)->asSequence()); + } + + /** + * Initialize from a `PrivateKey`. + * + * Note that `OneAsymmetricKey` <-> `PrivateKey` conversions may not be bidirectional with all key types, since + * `OneAsymmetricKey` may include attributes as well the public key that are not conveyed in a specific `PrivateKey` + * object. + */ + public static function fromPrivateKey(PrivateKey $private_key): static + { + return static::create($private_key->algorithmIdentifier(), $private_key->privateKeyData()); + } + + /** + * Initialize from PEM. + */ + public static function fromPEM(PEM $pem): self + { + return match ($pem->type()) { + PEM::TYPE_PRIVATE_KEY => self::fromDER($pem->data()), + PEM::TYPE_RSA_PRIVATE_KEY => self::fromPrivateKey(RSAPrivateKey::fromDER($pem->data())), + PEM::TYPE_EC_PRIVATE_KEY => self::fromPrivateKey(ECPrivateKey::fromDER($pem->data())), + default => throw new UnexpectedValueException('Invalid PEM type.'), + }; + } + + /** + * Get version number. + */ + public function version(): int + { + return $this->version; + } + + /** + * Get algorithm identifier. + */ + public function algorithmIdentifier(): AlgorithmIdentifierType + { + return $this->algo; + } + + /** + * Get private key data. + */ + public function privateKeyData(): string + { + return $this->privateKeyData; + } + + /** + * Get private key. + */ + public function privateKey(): PrivateKey + { + $algo = $this->algorithmIdentifier(); + switch ($algo->oid()) { + // RSA + case AlgorithmIdentifier::OID_RSA_ENCRYPTION: + return RSAPrivateKey::fromDER($this->privateKeyData); + // RSASSA-PSS + case AlgorithmIdentifier::OID_RSASSA_PSS_ENCRYPTION: + return RSASSAPSSPrivateKey::fromDER($this->privateKeyData); + // elliptic curve + case AlgorithmIdentifier::OID_EC_PUBLIC_KEY: + $pk = ECPrivateKey::fromDER($this->privateKeyData); + // NOTE: OpenSSL strips named curve from ECPrivateKey structure + // when serializing into PrivateKeyInfo. However, RFC 5915 dictates + // that parameters (NamedCurve) must always be included. + // If private key doesn't encode named curve, assign from parameters. + if (! $pk->hasNamedCurve()) { + if (! $algo instanceof ECPublicKeyAlgorithmIdentifier) { + throw new UnexpectedValueException('Not an EC algorithm.'); + } + $pk = $pk->withNamedCurve($algo->namedCurve()); + } + return $pk; + // Ed25519 + case AlgorithmIdentifier::OID_ED25519: + $pubkey = $this->publicKeyData?->string(); + // RFC 8410 defines `CurvePrivateKey ::= OCTET STRING` that + // is encoded into private key data. So Ed25519 private key + // is doubly wrapped into octet string encodings. + return Ed25519PrivateKey::fromOctetString(OctetString::fromDER($this->privateKeyData), $pubkey) + ->withVersion($this->version) + ->withAttributes($this->attributes); + // X25519 + case AlgorithmIdentifier::OID_X25519: + $pubkey = $this->publicKeyData?->string(); + return X25519PrivateKey::fromOctetString(OctetString::fromDER($this->privateKeyData), $pubkey) + ->withVersion($this->version) + ->withAttributes($this->attributes); + // Ed448 + case AlgorithmIdentifier::OID_ED448: + $pubkey = $this->publicKeyData?->string(); + return Ed448PrivateKey::fromOctetString(OctetString::fromDER($this->privateKeyData), $pubkey) + ->withVersion($this->version) + ->withAttributes($this->attributes); + // X448 + case AlgorithmIdentifier::OID_X448: + $pubkey = $this->publicKeyData?->string(); + return X448PrivateKey::fromOctetString(OctetString::fromDER($this->privateKeyData), $pubkey) + ->withVersion($this->version) + ->withAttributes($this->attributes); + default: + throw new RuntimeException('Private key ' . $algo->name() . ' not supported.'); + } + } + + /** + * Get public key info corresponding to the private key. + */ + public function publicKeyInfo(): PublicKeyInfo + { + // if public key is explicitly defined + if ($this->hasPublicKeyData()) { + return PublicKeyInfo::create($this->algo, $this->publicKeyData); + } + // else derive from private key + return $this->privateKey() + ->publicKey() + ->publicKeyInfo(); + } + + /** + * Whether attributes are present. + */ + public function hasAttributes(): bool + { + return isset($this->attributes); + } + + public function attributes(): OneAsymmetricKeyAttributes + { + if (! $this->hasAttributes()) { + throw new LogicException('Attributes not set.'); + } + return $this->attributes; + } + + /** + * Whether explicit public key data is present. + */ + public function hasPublicKeyData(): bool + { + return isset($this->publicKeyData); + } + + /** + * Get the explicit public key data. + */ + public function publicKeyData(): BitString + { + if (! $this->hasPublicKeyData()) { + throw new LogicException('No explicit public key.'); + } + return $this->publicKeyData; + } + + /** + * Generate ASN.1 structure. + */ + public function toASN1(): Sequence + { + $elements = [ + Integer::create($this->version), + $this->algo->toASN1(), + OctetString::create($this->privateKeyData), + ]; + if ($this->attributes !== null) { + $elements[] = ImplicitlyTaggedType::create(0, $this->attributes->toASN1()); + } + if ($this->publicKeyData !== null) { + $elements[] = ImplicitlyTaggedType::create(1, $this->publicKeyData); + } + return Sequence::create(...$elements); + } + + /** + * Generate DER encoding. + */ + public function toDER(): string + { + return $this->toASN1() + ->toDER(); + } + + /** + * Generate PEM. + */ + public function toPEM(): PEM + { + return PEM::create(PEM::TYPE_PRIVATE_KEY, $this->toDER()); + } +} diff --git a/3rdparty/spomky-labs/pki-framework/src/CryptoTypes/Asymmetric/PrivateKey.php b/3rdparty/spomky-labs/pki-framework/src/CryptoTypes/Asymmetric/PrivateKey.php new file mode 100644 index 00000000..4208286a --- /dev/null +++ b/3rdparty/spomky-labs/pki-framework/src/CryptoTypes/Asymmetric/PrivateKey.php @@ -0,0 +1,71 @@ +toDER(); + } + + /** + * Get the private key as a PrivateKeyInfo type. + */ + public function privateKeyInfo(): PrivateKeyInfo + { + return PrivateKeyInfo::fromPrivateKey($this); + } + + /** + * Initialize private key from PEM. + * + * @return PrivateKey + */ + public static function fromPEM(PEM $pem) + { + return match ($pem->type()) { + PEM::TYPE_RSA_PRIVATE_KEY => RSAPrivateKey::fromDER($pem->data()), + PEM::TYPE_EC_PRIVATE_KEY => ECPrivateKey::fromDER($pem->data()), + PEM::TYPE_PRIVATE_KEY => PrivateKeyInfo::fromDER($pem->data())->privateKey(), + default => throw new UnexpectedValueException('PEM type ' . $pem->type() . ' is not a valid private key.'), + }; + } +} diff --git a/3rdparty/spomky-labs/pki-framework/src/CryptoTypes/Asymmetric/PrivateKeyInfo.php b/3rdparty/spomky-labs/pki-framework/src/CryptoTypes/Asymmetric/PrivateKeyInfo.php new file mode 100644 index 00000000..9ed65b14 --- /dev/null +++ b/3rdparty/spomky-labs/pki-framework/src/CryptoTypes/Asymmetric/PrivateKeyInfo.php @@ -0,0 +1,31 @@ +toDER()); + } + + /** + * Get the public key as a PublicKeyInfo type. + */ + public function publicKeyInfo(): PublicKeyInfo + { + return PublicKeyInfo::fromPublicKey($this); + } + + /** + * Initialize public key from PEM. + * + * @return PublicKey + */ + public static function fromPEM(PEM $pem) + { + return match ($pem->type()) { + PEM::TYPE_RSA_PUBLIC_KEY => RSAPublicKey::fromDER($pem->data()), + PEM::TYPE_PUBLIC_KEY => PublicKeyInfo::fromPEM($pem)->publicKey(), + default => throw new UnexpectedValueException('PEM type ' . $pem->type() . ' is not a valid public key.'), + }; + } +} diff --git a/3rdparty/spomky-labs/pki-framework/src/CryptoTypes/Asymmetric/PublicKeyInfo.php b/3rdparty/spomky-labs/pki-framework/src/CryptoTypes/Asymmetric/PublicKeyInfo.php new file mode 100644 index 00000000..ce92b2ed --- /dev/null +++ b/3rdparty/spomky-labs/pki-framework/src/CryptoTypes/Asymmetric/PublicKeyInfo.php @@ -0,0 +1,186 @@ +at(0)->asSequence()); + $key = $seq->at(1) + ->asBitString(); + return self::create($algo, $key); + } + + /** + * Initialize from a PublicKey. + */ + public static function fromPublicKey(PublicKey $key): self + { + return self::create($key->algorithmIdentifier(), $key->subjectPublicKey()); + } + + /** + * Initialize from PEM. + */ + public static function fromPEM(PEM $pem): self + { + return match ($pem->type()) { + PEM::TYPE_PUBLIC_KEY => self::fromDER($pem->data()), + PEM::TYPE_RSA_PUBLIC_KEY => RSAPublicKey::fromDER($pem->data())->publicKeyInfo(), + default => throw new UnexpectedValueException('Invalid PEM type.'), + }; + } + + /** + * Initialize from DER data. + */ + public static function fromDER(string $data): self + { + return self::fromASN1(UnspecifiedType::fromDER($data)->asSequence()); + } + + /** + * Get algorithm identifier. + */ + public function algorithmIdentifier(): AlgorithmIdentifierType + { + return $this->algo; + } + + /** + * Get public key data. + */ + public function publicKeyData(): BitString + { + return $this->publicKey; + } + + /** + * Get public key. + */ + public function publicKey(): PublicKey + { + $algo = $this->algorithmIdentifier(); + switch ($algo->oid()) { + // RSA + case AlgorithmIdentifier::OID_RSA_ENCRYPTION: + return RSAPublicKey::fromDER($this->publicKey->string()); + // Elliptic Curve + case AlgorithmIdentifier::OID_EC_PUBLIC_KEY: + if (! $algo instanceof ECPublicKeyAlgorithmIdentifier) { + throw new UnexpectedValueException('Not an EC algorithm.'); + } + // ECPoint is directly mapped into public key data + return ECPublicKey::create($this->publicKey->string(), $algo->namedCurve()); + // Ed25519 + case AlgorithmIdentifier::OID_ED25519: + return Ed25519PublicKey::create($this->publicKey->string()); + // X25519 + case AlgorithmIdentifier::OID_X25519: + return X25519PublicKey::create($this->publicKey->string()); + // Ed448 + case AlgorithmIdentifier::OID_ED448: + return Ed448PublicKey::create($this->publicKey->string()); + // X448 + case AlgorithmIdentifier::OID_X448: + return X448PublicKey::create($this->publicKey->string()); + } + throw new RuntimeException('Public key ' . $algo->name() . ' not supported.'); + } + + /** + * Get key identifier using method 1 as described by RFC 5280. + * + * @see https://tools.ietf.org/html/rfc5280#section-4.2.1.2 + * + * @return string 20 bytes (160 bits) long identifier + */ + public function keyIdentifier(): string + { + return sha1($this->publicKey->string(), true); + } + + /** + * Get key identifier using method 2 as described by RFC 5280. + * + * @see https://tools.ietf.org/html/rfc5280#section-4.2.1.2 + * + * @return string 8 bytes (64 bits) long identifier + */ + public function keyIdentifier64(): string + { + $id = mb_substr($this->keyIdentifier(), -8, null, '8bit'); + $c = (ord($id[0]) & 0x0f) | 0x40; + $id[0] = chr($c); + return $id; + } + + /** + * Generate ASN.1 structure. + */ + public function toASN1(): Sequence + { + return Sequence::create($this->algo->toASN1(), $this->publicKey); + } + + /** + * Generate DER encoding. + */ + public function toDER(): string + { + return $this->toASN1() + ->toDER(); + } + + /** + * Generate PEM. + */ + public function toPEM(): PEM + { + return PEM::create(PEM::TYPE_PUBLIC_KEY, $this->toDER()); + } +} diff --git a/3rdparty/spomky-labs/pki-framework/src/CryptoTypes/Asymmetric/RFC8410/Curve25519/Curve25519PrivateKey.php b/3rdparty/spomky-labs/pki-framework/src/CryptoTypes/Asymmetric/RFC8410/Curve25519/Curve25519PrivateKey.php new file mode 100644 index 00000000..606f6120 --- /dev/null +++ b/3rdparty/spomky-labs/pki-framework/src/CryptoTypes/Asymmetric/RFC8410/Curve25519/Curve25519PrivateKey.php @@ -0,0 +1,32 @@ +string(), $public_key); + } + + public function algorithmIdentifier(): AlgorithmIdentifierType + { + return Ed25519AlgorithmIdentifier::create(); + } + + public function publicKey(): PublicKey + { + if (! $this->hasPublicKey()) { + throw new LogicException('Public key not set.'); + } + return Ed25519PublicKey::create($this->_publicKeyData); + } +} diff --git a/3rdparty/spomky-labs/pki-framework/src/CryptoTypes/Asymmetric/RFC8410/Curve25519/Ed25519PublicKey.php b/3rdparty/spomky-labs/pki-framework/src/CryptoTypes/Asymmetric/RFC8410/Curve25519/Ed25519PublicKey.php new file mode 100644 index 00000000..41eb7685 --- /dev/null +++ b/3rdparty/spomky-labs/pki-framework/src/CryptoTypes/Asymmetric/RFC8410/Curve25519/Ed25519PublicKey.php @@ -0,0 +1,26 @@ +string(), $public_key); + } + + public function algorithmIdentifier(): AlgorithmIdentifierType + { + return X25519AlgorithmIdentifier::create(); + } + + public function publicKey(): PublicKey + { + if (! $this->hasPublicKey()) { + throw new LogicException('Public key not set.'); + } + return X25519PublicKey::create($this->_publicKeyData); + } +} diff --git a/3rdparty/spomky-labs/pki-framework/src/CryptoTypes/Asymmetric/RFC8410/Curve25519/X25519PublicKey.php b/3rdparty/spomky-labs/pki-framework/src/CryptoTypes/Asymmetric/RFC8410/Curve25519/X25519PublicKey.php new file mode 100644 index 00000000..95cad2a2 --- /dev/null +++ b/3rdparty/spomky-labs/pki-framework/src/CryptoTypes/Asymmetric/RFC8410/Curve25519/X25519PublicKey.php @@ -0,0 +1,26 @@ +string(), $public_key); + } + + public function algorithmIdentifier(): AlgorithmIdentifierType + { + return Ed448AlgorithmIdentifier::create(); + } + + public function publicKey(): PublicKey + { + if (! $this->hasPublicKey()) { + throw new LogicException('Public key not set.'); + } + return Ed448PublicKey::create($this->_publicKeyData); + } +} diff --git a/3rdparty/spomky-labs/pki-framework/src/CryptoTypes/Asymmetric/RFC8410/Curve448/Ed448PublicKey.php b/3rdparty/spomky-labs/pki-framework/src/CryptoTypes/Asymmetric/RFC8410/Curve448/Ed448PublicKey.php new file mode 100644 index 00000000..d106c4f2 --- /dev/null +++ b/3rdparty/spomky-labs/pki-framework/src/CryptoTypes/Asymmetric/RFC8410/Curve448/Ed448PublicKey.php @@ -0,0 +1,40 @@ +string(), $public_key); + } + + public function algorithmIdentifier(): AlgorithmIdentifierType + { + return X448AlgorithmIdentifier::create(); + } + + public function publicKey(): PublicKey + { + if (! $this->hasPublicKey()) { + throw new LogicException('Public key not set.'); + } + return X448PublicKey::create($this->_publicKeyData); + } +} diff --git a/3rdparty/spomky-labs/pki-framework/src/CryptoTypes/Asymmetric/RFC8410/Curve448/X448PublicKey.php b/3rdparty/spomky-labs/pki-framework/src/CryptoTypes/Asymmetric/RFC8410/Curve448/X448PublicKey.php new file mode 100644 index 00000000..6d668075 --- /dev/null +++ b/3rdparty/spomky-labs/pki-framework/src/CryptoTypes/Asymmetric/RFC8410/Curve448/X448PublicKey.php @@ -0,0 +1,40 @@ +_version = OneAsymmetricKey::VERSION_2; + $this->_attributes = null; + } + + /** + * Get self with version number. + */ + public function withVersion(int $version): self + { + $obj = clone $this; + $obj->_version = $version; + return $obj; + } + + /** + * Get self with attributes. + */ + public function withAttributes(?OneAsymmetricKeyAttributes $attribs): self + { + $obj = clone $this; + $obj->_attributes = $attribs; + return $obj; + } + + public function privateKeyData(): string + { + return $this->_privateKeyData; + } + + /** + * Whether public key is set. + */ + public function hasPublicKey(): bool + { + return isset($this->_publicKeyData); + } + + /** + * Generate ASN.1 structure. + */ + public function toASN1(): OctetString + { + return OctetString::create($this->_privateKeyData); + } + + public function toDER(): string + { + return $this->toASN1() + ->toDER(); + } + + public function toPEM(): PEM + { + $pub = $this->_publicKeyData === null ? null : + BitString::create($this->_publicKeyData); + + return OneAsymmetricKey::create($this->algorithmIdentifier(), $this->toDER(), $this->_attributes, $pub) + ->withVersion($this->_version) + ->toPEM(); + } +} diff --git a/3rdparty/spomky-labs/pki-framework/src/CryptoTypes/Asymmetric/RFC8410/RFC8410PublicKey.php b/3rdparty/spomky-labs/pki-framework/src/CryptoTypes/Asymmetric/RFC8410/RFC8410PublicKey.php new file mode 100644 index 00000000..ceec70b3 --- /dev/null +++ b/3rdparty/spomky-labs/pki-framework/src/CryptoTypes/Asymmetric/RFC8410/RFC8410PublicKey.php @@ -0,0 +1,37 @@ +publicKey); + } +} diff --git a/3rdparty/spomky-labs/pki-framework/src/CryptoTypes/Asymmetric/RSA/RSAPrivateKey.php b/3rdparty/spomky-labs/pki-framework/src/CryptoTypes/Asymmetric/RSA/RSAPrivateKey.php new file mode 100644 index 00000000..cd98166a --- /dev/null +++ b/3rdparty/spomky-labs/pki-framework/src/CryptoTypes/Asymmetric/RSA/RSAPrivateKey.php @@ -0,0 +1,226 @@ +at(0) + ->asInteger() + ->intNumber(); + if ($version !== 0) { + throw new UnexpectedValueException('Version must be 0.'); + } + // helper function get integer from given index + $get_int = static fn ($idx) => $seq->at($idx) + ->asInteger() + ->number(); + $n = $get_int(1); + $e = $get_int(2); + $d = $get_int(3); + $p = $get_int(4); + $q = $get_int(5); + $dp = $get_int(6); + $dq = $get_int(7); + $qi = $get_int(8); + return self::create($n, $e, $d, $p, $q, $dp, $dq, $qi); + } + + /** + * Initialize from DER data. + */ + public static function fromDER(string $data): self + { + return self::fromASN1(UnspecifiedType::fromDER($data)->asSequence()); + } + + /** + * @see PrivateKey::fromPEM() + */ + public static function fromPEM(PEM $pem): self + { + $pk = parent::fromPEM($pem); + if (! ($pk instanceof self)) { + throw new UnexpectedValueException('Not an RSA private key.'); + } + return $pk; + } + + /** + * Get modulus. + * + * @return string Base 10 integer + */ + public function modulus(): string + { + return $this->modulus; + } + + /** + * Get public exponent. + * + * @return string Base 10 integer + */ + public function publicExponent(): string + { + return $this->publicExponent; + } + + /** + * Get private exponent. + * + * @return string Base 10 integer + */ + public function privateExponent(): string + { + return $this->privateExponent; + } + + /** + * Get first prime factor. + * + * @return string Base 10 integer + */ + public function prime1(): string + { + return $this->prime1; + } + + /** + * Get second prime factor. + * + * @return string Base 10 integer + */ + public function prime2(): string + { + return $this->prime2; + } + + /** + * Get first factor exponent. + * + * @return string Base 10 integer + */ + public function exponent1(): string + { + return $this->exponent1; + } + + /** + * Get second factor exponent. + * + * @return string Base 10 integer + */ + public function exponent2(): string + { + return $this->exponent2; + } + + /** + * Get CRT coefficient of the second factor. + * + * @return string Base 10 integer + */ + public function coefficient(): string + { + return $this->coefficient; + } + + public function algorithmIdentifier(): AlgorithmIdentifierType + { + return RSAEncryptionAlgorithmIdentifier::create(); + } + + /** + * @return RSAPublicKey + */ + public function publicKey(): PublicKey + { + return RSAPublicKey::create($this->modulus, $this->publicExponent); + } + + /** + * Generate ASN.1 structure. + */ + public function toASN1(): Sequence + { + return Sequence::create( + Integer::create(0), + Integer::create($this->modulus), + Integer::create($this->publicExponent), + Integer::create($this->privateExponent), + Integer::create($this->prime1), + Integer::create($this->prime2), + Integer::create($this->exponent1), + Integer::create($this->exponent2), + Integer::create($this->coefficient) + ); + } + + public function toDER(): string + { + return $this->toASN1() + ->toDER(); + } + + public function toPEM(): PEM + { + return PEM::create(PEM::TYPE_RSA_PRIVATE_KEY, $this->toDER()); + } +} diff --git a/3rdparty/spomky-labs/pki-framework/src/CryptoTypes/Asymmetric/RSA/RSAPublicKey.php b/3rdparty/spomky-labs/pki-framework/src/CryptoTypes/Asymmetric/RSA/RSAPublicKey.php new file mode 100644 index 00000000..86f0c1ac --- /dev/null +++ b/3rdparty/spomky-labs/pki-framework/src/CryptoTypes/Asymmetric/RSA/RSAPublicKey.php @@ -0,0 +1,124 @@ +at(0) + ->asInteger() + ->number(); + $e = $seq->at(1) + ->asInteger() + ->number(); + return self::create($n, $e); + } + + /** + * Initialize from DER data. + */ + public static function fromDER(string $data): self + { + return self::fromASN1(UnspecifiedType::fromDER($data)->asSequence()); + } + + /** + * @see PublicKey::fromPEM() + */ + public static function fromPEM(PEM $pem): self + { + switch ($pem->type()) { + case PEM::TYPE_RSA_PUBLIC_KEY: + return self::fromDER($pem->data()); + case PEM::TYPE_PUBLIC_KEY: + $pki = PublicKeyInfo::fromDER($pem->data()); + if ($pki->algorithmIdentifier() + ->oid() !== + AlgorithmIdentifier::OID_RSA_ENCRYPTION) { + throw new UnexpectedValueException('Not an RSA public key.'); + } + return self::fromDER($pki->publicKeyData()->string()); + } + throw new UnexpectedValueException('Invalid PEM type ' . $pem->type()); + } + + /** + * Get modulus. + * + * @return string Base 10 integer + */ + public function modulus(): string + { + return $this->modulus; + } + + /** + * Get public exponent. + * + * @return string Base 10 integer + */ + public function publicExponent(): string + { + return $this->publicExponent; + } + + public function algorithmIdentifier(): AlgorithmIdentifierType + { + return RSAEncryptionAlgorithmIdentifier::create(); + } + + /** + * Generate ASN.1 structure. + */ + public function toASN1(): Sequence + { + return Sequence::create(Integer::create($this->modulus), Integer::create($this->publicExponent)); + } + + public function toDER(): string + { + return $this->toASN1() + ->toDER(); + } + + /** + * Generate PEM. + */ + public function toPEM(): PEM + { + return PEM::create(PEM::TYPE_RSA_PUBLIC_KEY, $this->toDER()); + } +} diff --git a/3rdparty/spomky-labs/pki-framework/src/CryptoTypes/Asymmetric/RSA/RSASSAPSSPrivateKey.php b/3rdparty/spomky-labs/pki-framework/src/CryptoTypes/Asymmetric/RSA/RSASSAPSSPrivateKey.php new file mode 100644 index 00000000..89703e85 --- /dev/null +++ b/3rdparty/spomky-labs/pki-framework/src/CryptoTypes/Asymmetric/RSA/RSASSAPSSPrivateKey.php @@ -0,0 +1,226 @@ +at(0) + ->asInteger() + ->intNumber(); + if ($version !== 0) { + throw new UnexpectedValueException('Version must be 0.'); + } + // helper function get integer from given index + $get_int = static fn ($idx) => $seq->at($idx) + ->asInteger() + ->number(); + $n = $get_int(1); + $e = $get_int(2); + $d = $get_int(3); + $p = $get_int(4); + $q = $get_int(5); + $dp = $get_int(6); + $dq = $get_int(7); + $qi = $get_int(8); + return self::create($n, $e, $d, $p, $q, $dp, $dq, $qi); + } + + /** + * Initialize from DER data. + */ + public static function fromDER(string $data): self + { + return self::fromASN1(UnspecifiedType::fromDER($data)->asSequence()); + } + + /** + * @see PrivateKey::fromPEM() + */ + public static function fromPEM(PEM $pem): self + { + $pk = parent::fromPEM($pem); + if (! ($pk instanceof self)) { + throw new UnexpectedValueException('Not an RSA private key.'); + } + return $pk; + } + + /** + * Get modulus. + * + * @return string Base 10 integer + */ + public function modulus(): string + { + return $this->modulus; + } + + /** + * Get public exponent. + * + * @return string Base 10 integer + */ + public function publicExponent(): string + { + return $this->publicExponent; + } + + /** + * Get private exponent. + * + * @return string Base 10 integer + */ + public function privateExponent(): string + { + return $this->privateExponent; + } + + /** + * Get first prime factor. + * + * @return string Base 10 integer + */ + public function prime1(): string + { + return $this->prime1; + } + + /** + * Get second prime factor. + * + * @return string Base 10 integer + */ + public function prime2(): string + { + return $this->prime2; + } + + /** + * Get first factor exponent. + * + * @return string Base 10 integer + */ + public function exponent1(): string + { + return $this->exponent1; + } + + /** + * Get second factor exponent. + * + * @return string Base 10 integer + */ + public function exponent2(): string + { + return $this->exponent2; + } + + /** + * Get CRT coefficient of the second factor. + * + * @return string Base 10 integer + */ + public function coefficient(): string + { + return $this->coefficient; + } + + public function algorithmIdentifier(): AlgorithmIdentifierType + { + return RSAPSSSSAEncryptionAlgorithmIdentifier::create(); + } + + /** + * @return RSAPublicKey + */ + public function publicKey(): PublicKey + { + return RSAPublicKey::create($this->modulus, $this->publicExponent); + } + + /** + * Generate ASN.1 structure. + */ + public function toASN1(): Sequence + { + return Sequence::create( + Integer::create(0), + Integer::create($this->modulus), + Integer::create($this->publicExponent), + Integer::create($this->privateExponent), + Integer::create($this->prime1), + Integer::create($this->prime2), + Integer::create($this->exponent1), + Integer::create($this->exponent2), + Integer::create($this->coefficient) + ); + } + + public function toDER(): string + { + return $this->toASN1() + ->toDER(); + } + + public function toPEM(): PEM + { + return PEM::create(PEM::TYPE_PRIVATE_KEY, $this->toDER()); + } +} diff --git a/3rdparty/spomky-labs/pki-framework/src/CryptoTypes/Signature/ECSignature.php b/3rdparty/spomky-labs/pki-framework/src/CryptoTypes/Signature/ECSignature.php new file mode 100644 index 00000000..d9b4d0dd --- /dev/null +++ b/3rdparty/spomky-labs/pki-framework/src/CryptoTypes/Signature/ECSignature.php @@ -0,0 +1,95 @@ +at(0) + ->asInteger() + ->number(); + $s = $seq->at(1) + ->asInteger() + ->number(); + return self::create($r, $s); + } + + /** + * Initialize from DER. + */ + public static function fromDER(string $data): self + { + return self::fromASN1(UnspecifiedType::fromDER($data)->asSequence()); + } + + /** + * Get the r-value. + * + * @return string Base 10 integer string + */ + public function r(): string + { + return $this->r; + } + + /** + * Get the s-value. + * + * @return string Base 10 integer string + */ + public function s(): string + { + return $this->s; + } + + /** + * Generate ASN.1 structure. + */ + public function toASN1(): Sequence + { + return Sequence::create(Integer::create($this->r), Integer::create($this->s)); + } + + /** + * Get DER encoding of the signature. + */ + public function toDER(): string + { + return $this->toASN1() + ->toDER(); + } + + public function bitString(): BitString + { + return BitString::create($this->toDER()); + } +} diff --git a/3rdparty/spomky-labs/pki-framework/src/CryptoTypes/Signature/Ed25519Signature.php b/3rdparty/spomky-labs/pki-framework/src/CryptoTypes/Signature/Ed25519Signature.php new file mode 100644 index 00000000..3a477668 --- /dev/null +++ b/3rdparty/spomky-labs/pki-framework/src/CryptoTypes/Signature/Ed25519Signature.php @@ -0,0 +1,42 @@ +signature = $signature; + } + + public static function create(string $signature): self + { + return new self($signature); + } + + public function bitString(): BitString + { + return BitString::create($this->signature); + } +} diff --git a/3rdparty/spomky-labs/pki-framework/src/CryptoTypes/Signature/Ed448Signature.php b/3rdparty/spomky-labs/pki-framework/src/CryptoTypes/Signature/Ed448Signature.php new file mode 100644 index 00000000..26324910 --- /dev/null +++ b/3rdparty/spomky-labs/pki-framework/src/CryptoTypes/Signature/Ed448Signature.php @@ -0,0 +1,42 @@ +signature = $signature; + } + + public static function create(string $signature): self + { + return new self($signature); + } + + public function bitString(): BitString + { + return BitString::create($this->signature); + } +} diff --git a/3rdparty/spomky-labs/pki-framework/src/CryptoTypes/Signature/GenericSignature.php b/3rdparty/spomky-labs/pki-framework/src/CryptoTypes/Signature/GenericSignature.php new file mode 100644 index 00000000..9487f6f1 --- /dev/null +++ b/3rdparty/spomky-labs/pki-framework/src/CryptoTypes/Signature/GenericSignature.php @@ -0,0 +1,42 @@ +signatureAlgorithm; + } + + public function bitString(): BitString + { + return $this->signature; + } +} diff --git a/3rdparty/spomky-labs/pki-framework/src/CryptoTypes/Signature/RSASignature.php b/3rdparty/spomky-labs/pki-framework/src/CryptoTypes/Signature/RSASignature.php new file mode 100644 index 00000000..61c08c59 --- /dev/null +++ b/3rdparty/spomky-labs/pki-framework/src/CryptoTypes/Signature/RSASignature.php @@ -0,0 +1,50 @@ +_signature = strval($signature); + return $obj; + } + + public function bitString(): BitString + { + return BitString::create($this->_signature); + } +} diff --git a/3rdparty/spomky-labs/pki-framework/src/CryptoTypes/Signature/Signature.php b/3rdparty/spomky-labs/pki-framework/src/CryptoTypes/Signature/Signature.php new file mode 100644 index 00000000..c164b8cc --- /dev/null +++ b/3rdparty/spomky-labs/pki-framework/src/CryptoTypes/Signature/Signature.php @@ -0,0 +1,46 @@ +oid() !== $type->oid()) { + throw new LogicException('Attribute OID mismatch.'); + } + } + $this->type = $type; + $this->values = $values; + } + + public static function create(AttributeType $type, AttributeValue ...$values): self + { + return new self($type, ...$values); + } + + /** + * Initialize from ASN.1. + */ + public static function fromASN1(Sequence $seq): self + { + $type = AttributeType::fromASN1($seq->at(0)->asObjectIdentifier()); + $values = array_map( + static fn (UnspecifiedType $el) => AttributeValue::fromASN1ByOID($type->oid(), $el), + $seq->at(1) + ->asSet() + ->elements() + ); + return self::create($type, ...$values); + } + + /** + * Convenience method to initialize from attribute values. + * + * @param AttributeValue ...$values One or more values + */ + public static function fromAttributeValues(AttributeValue ...$values): self + { + // we need at least one value to determine OID + if (count($values) === 0) { + throw new LogicException('No values.'); + } + $oid = reset($values) + ->oid(); + return self::create(AttributeType::create($oid), ...$values); + } + + /** + * Get first value of the attribute. + */ + public function first(): AttributeValue + { + if (count($this->values) === 0) { + throw new LogicException('Attribute contains no values.'); + } + return $this->values[0]; + } + + /** + * Get all values. + * + * @return AttributeValue[] + */ + public function values(): array + { + return $this->values; + } + + /** + * Generate ASN.1 structure. + */ + public function toASN1(): Sequence + { + $values = array_map(static fn (AttributeValue $value) => $value->toASN1(), $this->values); + $valueset = Set::create(...$values); + return Sequence::create($this->type->toASN1(), $valueset->sortedSetOf()); + } + + /** + * Cast attribute values to another AttributeValue class. + * + * This method is generally used to cast UnknownAttributeValue values to specific objects when class is declared + * outside this package. + * + * The new class must be derived from AttributeValue and have the same OID as current attribute values. + * + * @param string $cls AttributeValue class name + */ + public function castValues(string $cls): self + { + // check that target class derives from AttributeValue + if (! is_subclass_of($cls, AttributeValue::class)) { + throw new LogicException(sprintf('%s must be derived from %s.', $cls, AttributeValue::class)); + } + $values = array_map( + function (AttributeValue $value) use ($cls) { + /** @var AttributeValue $cls Class name as a string */ + $value = $cls::fromSelf($value); + if ($value->oid() !== $this->oid()) { + throw new LogicException('Attribute OID mismatch.'); + } + return $value; + }, + $this->values + ); + return self::fromAttributeValues(...$values); + } + + /** + * @see \Countable::count() + */ + public function count(): int + { + return count($this->values); + } + + /** + * @see \IteratorAggregate::getIterator() + */ + public function getIterator(): ArrayIterator + { + return new ArrayIterator($this->values); + } + + /** + * Get attribute type. + */ + public function type(): AttributeType + { + return $this->type; + } + + /** + * Get OID of the attribute. + */ + public function oid(): string + { + return $this->type->oid(); + } +} diff --git a/3rdparty/spomky-labs/pki-framework/src/X501/ASN1/AttributeType.php b/3rdparty/spomky-labs/pki-framework/src/X501/ASN1/AttributeType.php new file mode 100644 index 00000000..adde4278 --- /dev/null +++ b/3rdparty/spomky-labs/pki-framework/src/X501/ASN1/AttributeType.php @@ -0,0 +1,525 @@ + + */ + private const MAP_ATTR_TO_STR_TYPE = [ + self::OID_DN_QUALIFIER => Element::TYPE_PRINTABLE_STRING, + self::OID_COUNTRY_NAME => Element::TYPE_PRINTABLE_STRING, + self::OID_SERIAL_NUMBER => Element::TYPE_PRINTABLE_STRING, + ]; + + /** + * OID to attribute names mapping. + * + * First name is the primary name. If there's more than one name, others may be used as an alias. + * + * Generated using ldap-attribs.py. + * + * @internal + * + * @var array> + */ + private const MAP_OID_TO_NAME = [ + '0.9.2342.19200300.100.1.1' => ['uid', 'userid'], + '0.9.2342.19200300.100.1.2' => ['textEncodedORAddress'], + '0.9.2342.19200300.100.1.3' => ['mail', 'rfc822Mailbox'], + '0.9.2342.19200300.100.1.4' => ['info'], + '0.9.2342.19200300.100.1.5' => ['drink', 'favouriteDrink'], + '0.9.2342.19200300.100.1.6' => ['roomNumber'], + '0.9.2342.19200300.100.1.7' => ['photo'], + '0.9.2342.19200300.100.1.8' => ['userClass'], + '0.9.2342.19200300.100.1.9' => ['host'], + '0.9.2342.19200300.100.1.10' => ['manager'], + '0.9.2342.19200300.100.1.11' => ['documentIdentifier'], + '0.9.2342.19200300.100.1.12' => ['documentTitle'], + '0.9.2342.19200300.100.1.13' => ['documentVersion'], + '0.9.2342.19200300.100.1.14' => ['documentAuthor'], + '0.9.2342.19200300.100.1.15' => ['documentLocation'], + '0.9.2342.19200300.100.1.20' => ['homePhone', 'homeTelephoneNumber'], + '0.9.2342.19200300.100.1.21' => ['secretary'], + '0.9.2342.19200300.100.1.22' => ['otherMailbox'], + '0.9.2342.19200300.100.1.25' => ['dc', 'domainComponent'], + '0.9.2342.19200300.100.1.26' => ['aRecord'], + '0.9.2342.19200300.100.1.27' => ['mDRecord'], + '0.9.2342.19200300.100.1.28' => ['mXRecord'], + '0.9.2342.19200300.100.1.29' => ['nSRecord'], + '0.9.2342.19200300.100.1.30' => ['sOARecord'], + '0.9.2342.19200300.100.1.31' => ['cNAMERecord'], + '0.9.2342.19200300.100.1.37' => ['associatedDomain'], + '0.9.2342.19200300.100.1.38' => ['associatedName'], + '0.9.2342.19200300.100.1.39' => ['homePostalAddress'], + '0.9.2342.19200300.100.1.40' => ['personalTitle'], + '0.9.2342.19200300.100.1.41' => ['mobile', 'mobileTelephoneNumber'], + '0.9.2342.19200300.100.1.42' => ['pager', 'pagerTelephoneNumber'], + '0.9.2342.19200300.100.1.43' => ['co', 'friendlyCountryName'], + '0.9.2342.19200300.100.1.44' => ['uniqueIdentifier'], + '0.9.2342.19200300.100.1.45' => ['organizationalStatus'], + '0.9.2342.19200300.100.1.46' => ['janetMailbox'], + '0.9.2342.19200300.100.1.47' => ['mailPreferenceOption'], + '0.9.2342.19200300.100.1.48' => ['buildingName'], + '0.9.2342.19200300.100.1.49' => ['dSAQuality'], + '0.9.2342.19200300.100.1.50' => ['singleLevelQuality'], + '0.9.2342.19200300.100.1.51' => ['subtreeMinimumQuality'], + '0.9.2342.19200300.100.1.52' => ['subtreeMaximumQuality'], + '0.9.2342.19200300.100.1.53' => ['personalSignature'], + '0.9.2342.19200300.100.1.54' => ['dITRedirect'], + '0.9.2342.19200300.100.1.55' => ['audio'], + '0.9.2342.19200300.100.1.56' => ['documentPublisher'], + '0.9.2342.19200300.100.1.60' => ['jpegPhoto'], + '1.2.840.113549.1.9.1' => ['email', 'emailAddress', 'pkcs9email'], + '1.2.840.113556.1.2.102' => ['memberOf'], + '1.3.6.1.1.1.1.0' => ['uidNumber'], + '1.3.6.1.1.1.1.1' => ['gidNumber'], + '1.3.6.1.1.1.1.2' => ['gecos'], + '1.3.6.1.1.1.1.3' => ['homeDirectory'], + '1.3.6.1.1.1.1.4' => ['loginShell'], + '1.3.6.1.1.1.1.5' => ['shadowLastChange'], + '1.3.6.1.1.1.1.6' => ['shadowMin'], + '1.3.6.1.1.1.1.7' => ['shadowMax'], + '1.3.6.1.1.1.1.8' => ['shadowWarning'], + '1.3.6.1.1.1.1.9' => ['shadowInactive'], + '1.3.6.1.1.1.1.10' => ['shadowExpire'], + '1.3.6.1.1.1.1.11' => ['shadowFlag'], + '1.3.6.1.1.1.1.12' => ['memberUid'], + '1.3.6.1.1.1.1.13' => ['memberNisNetgroup'], + '1.3.6.1.1.1.1.14' => ['nisNetgroupTriple'], + '1.3.6.1.1.1.1.15' => ['ipServicePort'], + '1.3.6.1.1.1.1.16' => ['ipServiceProtocol'], + '1.3.6.1.1.1.1.17' => ['ipProtocolNumber'], + '1.3.6.1.1.1.1.18' => ['oncRpcNumber'], + '1.3.6.1.1.1.1.19' => ['ipHostNumber'], + '1.3.6.1.1.1.1.20' => ['ipNetworkNumber'], + '1.3.6.1.1.1.1.21' => ['ipNetmaskNumber'], + '1.3.6.1.1.1.1.22' => ['macAddress'], + '1.3.6.1.1.1.1.23' => ['bootParameter'], + '1.3.6.1.1.1.1.24' => ['bootFile'], + '1.3.6.1.1.1.1.26' => ['nisMapName'], + '1.3.6.1.1.1.1.27' => ['nisMapEntry'], + '1.3.6.1.1.4' => ['vendorName'], + '1.3.6.1.1.5' => ['vendorVersion'], + '1.3.6.1.1.16.4' => ['entryUUID'], + '1.3.6.1.1.20' => ['entryDN'], + '2.5.4.0' => ['objectClass'], + '2.5.4.1' => ['aliasedObjectName', 'aliasedEntryName'], + '2.5.4.2' => ['knowledgeInformation'], + '2.5.4.3' => ['cn', 'commonName'], + '2.5.4.4' => ['sn', 'surname'], + '2.5.4.5' => ['serialNumber'], + '2.5.4.6' => ['c', 'countryName'], + '2.5.4.7' => ['l', 'localityName'], + '2.5.4.8' => ['st', 'stateOrProvinceName'], + '2.5.4.9' => ['street', 'streetAddress'], + '2.5.4.10' => ['o', 'organizationName'], + '2.5.4.11' => ['ou', 'organizationalUnitName'], + '2.5.4.12' => ['title'], + '2.5.4.13' => ['description'], + '2.5.4.14' => ['searchGuide'], + '2.5.4.15' => ['businessCategory'], + '2.5.4.16' => ['postalAddress'], + '2.5.4.17' => ['postalCode'], + '2.5.4.18' => ['postOfficeBox'], + '2.5.4.19' => ['physicalDeliveryOfficeName'], + '2.5.4.20' => ['telephoneNumber'], + '2.5.4.21' => ['telexNumber'], + '2.5.4.22' => ['teletexTerminalIdentifier'], + '2.5.4.23' => ['facsimileTelephoneNumber', 'fax'], + '2.5.4.24' => ['x121Address'], + '2.5.4.25' => ['internationaliSDNNumber'], + '2.5.4.26' => ['registeredAddress'], + '2.5.4.27' => ['destinationIndicator'], + '2.5.4.28' => ['preferredDeliveryMethod'], + '2.5.4.29' => ['presentationAddress'], + '2.5.4.30' => ['supportedApplicationContext'], + '2.5.4.31' => ['member'], + '2.5.4.32' => ['owner'], + '2.5.4.33' => ['roleOccupant'], + '2.5.4.34' => ['seeAlso'], + '2.5.4.35' => ['userPassword'], + '2.5.4.36' => ['userCertificate'], + '2.5.4.37' => ['cACertificate'], + '2.5.4.38' => ['authorityRevocationList'], + '2.5.4.39' => ['certificateRevocationList'], + '2.5.4.40' => ['crossCertificatePair'], + '2.5.4.41' => ['name'], + '2.5.4.42' => ['givenName', 'gn'], + '2.5.4.43' => ['initials'], + '2.5.4.44' => ['generationQualifier'], + '2.5.4.45' => ['x500UniqueIdentifier'], + '2.5.4.46' => ['dnQualifier'], + '2.5.4.47' => ['enhancedSearchGuide'], + '2.5.4.48' => ['protocolInformation'], + '2.5.4.49' => ['distinguishedName'], + '2.5.4.50' => ['uniqueMember'], + '2.5.4.51' => ['houseIdentifier'], + '2.5.4.52' => ['supportedAlgorithms'], + '2.5.4.53' => ['deltaRevocationList'], + '2.5.4.54' => ['dmdName'], + '2.5.4.65' => ['pseudonym'], + '2.5.18.1' => ['createTimestamp'], + '2.5.18.2' => ['modifyTimestamp'], + '2.5.18.3' => ['creatorsName'], + '2.5.18.4' => ['modifiersName'], + '2.5.18.5' => ['administrativeRole'], + '2.5.18.6' => ['subtreeSpecification'], + '2.5.18.9' => ['hasSubordinates'], + '2.5.18.10' => ['subschemaSubentry'], + '2.5.21.1' => ['dITStructureRules'], + '2.5.21.2' => ['dITContentRules'], + '2.5.21.4' => ['matchingRules'], + '2.5.21.5' => ['attributeTypes'], + '2.5.21.6' => ['objectClasses'], + '2.5.21.7' => ['nameForms'], + '2.5.21.8' => ['matchingRuleUse'], + '2.5.21.9' => ['structuralObjectClass'], + '2.16.840.1.113730.3.1.1' => ['carLicense'], + '2.16.840.1.113730.3.1.2' => ['departmentNumber'], + '2.16.840.1.113730.3.1.3' => ['employeeNumber'], + '2.16.840.1.113730.3.1.4' => ['employeeType'], + '2.16.840.1.113730.3.1.34' => ['ref'], + '2.16.840.1.113730.3.1.39' => ['preferredLanguage'], + '2.16.840.1.113730.3.1.40' => ['userSMIMECertificate'], + '2.16.840.1.113730.3.1.216' => ['userPKCS12'], + '2.16.840.1.113730.3.1.241' => ['displayName'], + ]; + + /** + * @param string $_oid OID in dotted format + */ + private function __construct( + protected string $_oid + ) { + } + + public static function create(string $oid): self + { + return new self($oid); + } + + /** + * Initialize from ASN.1. + */ + public static function fromASN1(ObjectIdentifier $oi): self + { + return self::create($oi->oid()); + } + + /** + * Initialize from attribute name. + */ + public static function fromName(string $name): self + { + $oid = self::attrNameToOID($name); + return self::create($oid); + } + + /** + * Get OID of the attribute. + * + * @return string OID in dotted format + */ + public function oid(): string + { + return $this->_oid; + } + + /** + * Get name of the attribute. + */ + public function typeName(): string + { + if (array_key_exists($this->_oid, self::MAP_OID_TO_NAME)) { + return self::MAP_OID_TO_NAME[$this->_oid][0]; + } + return $this->_oid; + } + + /** + * Generate ASN.1 element. + */ + public function toASN1(): ObjectIdentifier + { + return ObjectIdentifier::create($this->_oid); + } + + /** + * Convert attribute name to OID. + * + * @param string $name Primary attribute name or an alias + * + * @return string OID in dotted format + */ + public static function attrNameToOID(string $name): string + { + // if already in OID form + if (preg_match('/^[0-9]+(?:\.[0-9]+)*$/', $name) === 1) { + return $name; + } + $map = self::_oidReverseMap(); + $k = mb_strtolower($name, '8bit'); + if (! isset($map[$k])) { + throw new OutOfBoundsException("No OID for {$name}."); + } + return $map[$k]; + } + + /** + * Get ASN.1 string for given attribute type. + * + * @param string $oid Attribute OID + * @param string $str String + */ + public static function asn1StringForType(string $oid, string $str): StringType + { + if (! array_key_exists($oid, self::MAP_ATTR_TO_STR_TYPE)) { + return UTF8String::create($str); + } + return PrintableString::create($str); + } + + /** + * Get name to OID lookup map. + * + * @return array + */ + private static function _oidReverseMap(): array + { + static $map; + if (! isset($map)) { + $map = []; + // for each attribute type + foreach (self::MAP_OID_TO_NAME as $oid => $names) { + // for primary name and aliases + foreach ($names as $name) { + $map[mb_strtolower($name, '8bit')] = $oid; + } + } + } + return $map; + } +} diff --git a/3rdparty/spomky-labs/pki-framework/src/X501/ASN1/AttributeTypeAndValue.php b/3rdparty/spomky-labs/pki-framework/src/X501/ASN1/AttributeTypeAndValue.php new file mode 100644 index 00000000..6bde15ba --- /dev/null +++ b/3rdparty/spomky-labs/pki-framework/src/X501/ASN1/AttributeTypeAndValue.php @@ -0,0 +1,115 @@ +toString(); + } + + public static function create(AttributeType $type, AttributeValue $value): self + { + return new self($type, $value); + } + + /** + * Initialize from ASN.1. + */ + public static function fromASN1(Sequence $seq): self + { + $type = AttributeType::fromASN1($seq->at(0)->asObjectIdentifier()); + $value = AttributeValue::fromASN1ByOID($type->oid(), $seq->at(1)); + return self::create($type, $value); + } + + /** + * Convenience method to initialize from attribute value. + * + * @param AttributeValue $value Attribute value + */ + public static function fromAttributeValue(AttributeValue $value): self + { + return self::create(AttributeType::create($value->oid()), $value); + } + + /** + * Get attribute value. + */ + public function value(): AttributeValue + { + return $this->value; + } + + /** + * Generate ASN.1 structure. + */ + public function toASN1(): Sequence + { + return Sequence::create($this->type->toASN1(), $this->value->toASN1()); + } + + /** + * Get attributeTypeAndValue string conforming to RFC 2253. + * + * @see https://tools.ietf.org/html/rfc2253#section-2.3 + */ + public function toString(): string + { + return $this->type->typeName() . '=' . $this->value->rfc2253String(); + } + + /** + * Check whether attribute is semantically equal to other. + * + * @param AttributeTypeAndValue $other Object to compare to + */ + public function equals(self $other): bool + { + // check that attribute types match + if ($this->oid() !== $other->oid()) { + return false; + } + $matcher = $this->value->equalityMatchingRule(); + + return $matcher->compare($this->value->stringValue(), $other->value->stringValue()) === true; + } + + /** + * Get attribute type. + */ + public function type(): AttributeType + { + return $this->type; + } + + /** + * Get OID of the attribute. + */ + public function oid(): string + { + return $this->type->oid(); + } +} diff --git a/3rdparty/spomky-labs/pki-framework/src/X501/ASN1/AttributeValue/AttributeValue.php b/3rdparty/spomky-labs/pki-framework/src/X501/ASN1/AttributeValue/AttributeValue.php new file mode 100644 index 00000000..9032fae8 --- /dev/null +++ b/3rdparty/spomky-labs/pki-framework/src/X501/ASN1/AttributeValue/AttributeValue.php @@ -0,0 +1,146 @@ + + */ + private const MAP_OID_TO_CLASS = [ + AttributeType::OID_COMMON_NAME => CommonNameValue::class, + AttributeType::OID_SURNAME => SurnameValue::class, + AttributeType::OID_SERIAL_NUMBER => SerialNumberValue::class, + AttributeType::OID_COUNTRY_NAME => CountryNameValue::class, + AttributeType::OID_LOCALITY_NAME => LocalityNameValue::class, + AttributeType::OID_STATE_OR_PROVINCE_NAME => StateOrProvinceNameValue::class, + AttributeType::OID_ORGANIZATION_NAME => OrganizationNameValue::class, + AttributeType::OID_ORGANIZATIONAL_UNIT_NAME => OrganizationalUnitNameValue::class, + AttributeType::OID_TITLE => TitleValue::class, + AttributeType::OID_DESCRIPTION => DescriptionValue::class, + AttributeType::OID_NAME => NameValue::class, + AttributeType::OID_GIVEN_NAME => GivenNameValue::class, + AttributeType::OID_PSEUDONYM => PseudonymValue::class, + ]; + + /** + * @param string $oid OID of the attribute type. + */ + protected function __construct( + protected string $oid + ) { + } + + /** + * Get attribute value as an UTF-8 encoded string. + */ + public function __toString(): string + { + return $this->_transcodedString(); + } + + /** + * Generate ASN.1 element. + */ + abstract public function toASN1(): Element; + + /** + * Get attribute value as a string. + */ + abstract public function stringValue(): string; + + /** + * Get matching rule for equality comparison. + */ + abstract public function equalityMatchingRule(): MatchingRule; + + /** + * Get attribute value as a string conforming to RFC 2253. + * + * @see https://tools.ietf.org/html/rfc2253#section-2.4 + */ + abstract public function rfc2253String(): string; + + /** + * Initialize from ASN.1. + */ + abstract public static function fromASN1(UnspecifiedType $el): self; + + /** + * Initialize from ASN.1 with given OID hint. + * + * @param string $oid Attribute's OID + */ + public static function fromASN1ByOID(string $oid, UnspecifiedType $el): self + { + if (! array_key_exists($oid, self::MAP_OID_TO_CLASS)) { + return new UnknownAttributeValue($oid, $el->asElement()); + } + $cls = self::MAP_OID_TO_CLASS[$oid]; + return $cls::fromASN1($el); + } + + /** + * Initialize from another AttributeValue. + * + * This method is generally used to cast UnknownAttributeValue to specific object when class is declared outside + * this package. + * + * @param self $obj Instance of AttributeValue + */ + public static function fromSelf(self $obj): self + { + return static::fromASN1($obj->toASN1()->asUnspecified()); + } + + /** + * Get attribute type's OID. + */ + public function oid(): string + { + return $this->oid; + } + + /** + * Get Attribute object with this as a single value. + */ + public function toAttribute(): Attribute + { + return Attribute::fromAttributeValues($this); + } + + /** + * Get AttributeTypeAndValue object with this as a value. + */ + public function toAttributeTypeAndValue(): AttributeTypeAndValue + { + return AttributeTypeAndValue::fromAttributeValue($this); + } + + /** + * Get attribute value as an UTF-8 string conforming to RFC 4518. + * + * @see https://tools.ietf.org/html/rfc4518#section-2.1 + */ + abstract protected function _transcodedString(): string; +} diff --git a/3rdparty/spomky-labs/pki-framework/src/X501/ASN1/AttributeValue/CommonNameValue.php b/3rdparty/spomky-labs/pki-framework/src/X501/ASN1/AttributeValue/CommonNameValue.php new file mode 100644 index 00000000..735ddf24 --- /dev/null +++ b/3rdparty/spomky-labs/pki-framework/src/X501/ASN1/AttributeValue/CommonNameValue.php @@ -0,0 +1,21 @@ +asPrintableString()->string()); + } +} diff --git a/3rdparty/spomky-labs/pki-framework/src/X501/ASN1/AttributeValue/DescriptionValue.php b/3rdparty/spomky-labs/pki-framework/src/X501/ASN1/AttributeValue/DescriptionValue.php new file mode 100644 index 00000000..b4ca4d67 --- /dev/null +++ b/3rdparty/spomky-labs/pki-framework/src/X501/ASN1/AttributeValue/DescriptionValue.php @@ -0,0 +1,21 @@ + + */ + private const MAP_TAG_TO_CLASS = [ + self::TELETEX => T61String::class, + self::PRINTABLE => PrintableString::class, + self::UNIVERSAL => UniversalString::class, + self::UTF8 => UTF8String::class, + self::BMP => BMPString::class, + ]; + + /** + * @param string $_string String value + * @param int $_stringTag Syntax choice + */ + final protected function __construct( + string $oid, + protected string $_string, + protected int $_stringTag + ) { + parent::__construct($oid); + } + + abstract public static function create(string $value, int $string_tag = self::UTF8): static; + + /** + * @return self + */ + public static function fromASN1(UnspecifiedType $el): AttributeValue + { + $tag = $el->tag(); + // validate tag + self::_tagToASN1Class($tag); + return static::create($el->asString()->string(), $tag); + } + + public function toASN1(): Element + { + $cls = self::_tagToASN1Class($this->_stringTag); + return $cls::create($this->_string); + } + + public function stringValue(): string + { + return $this->_string; + } + + public function equalityMatchingRule(): MatchingRule + { + return CaseIgnoreMatch::create($this->_stringTag); + } + + public function rfc2253String(): string + { + // TeletexString is encoded as binary + if ($this->_stringTag === self::TELETEX) { + return $this->_transcodedString(); + } + return DNParser::escapeString($this->_transcodedString()); + } + + protected function _transcodedString(): string + { + return TranscodeStep::create($this->_stringTag) + ->apply($this->_string) + ; + } + + /** + * Get ASN.1 class name for given DirectoryString type tag. + */ + private static function _tagToASN1Class(int $tag): string + { + if (! array_key_exists($tag, self::MAP_TAG_TO_CLASS)) { + throw new UnexpectedValueException( + sprintf('Type %s is not valid DirectoryString.', Element::tagToName($tag)) + ); + } + return self::MAP_TAG_TO_CLASS[$tag]; + } +} diff --git a/3rdparty/spomky-labs/pki-framework/src/X501/ASN1/AttributeValue/Feature/PrintableStringValue.php b/3rdparty/spomky-labs/pki-framework/src/X501/ASN1/AttributeValue/Feature/PrintableStringValue.php new file mode 100644 index 00000000..c8b31fe2 --- /dev/null +++ b/3rdparty/spomky-labs/pki-framework/src/X501/ASN1/AttributeValue/Feature/PrintableStringValue.php @@ -0,0 +1,55 @@ +_string); + } + + public function stringValue(): string + { + return $this->_string; + } + + public function equalityMatchingRule(): MatchingRule + { + // default to caseIgnoreMatch + return CaseIgnoreMatch::create(Element::TYPE_PRINTABLE_STRING); + } + + public function rfc2253String(): string + { + return DNParser::escapeString($this->_transcodedString()); + } + + protected function _transcodedString(): string + { + // PrintableString maps directly to UTF-8 + return $this->_string; + } +} diff --git a/3rdparty/spomky-labs/pki-framework/src/X501/ASN1/AttributeValue/GivenNameValue.php b/3rdparty/spomky-labs/pki-framework/src/X501/ASN1/AttributeValue/GivenNameValue.php new file mode 100644 index 00000000..a7641086 --- /dev/null +++ b/3rdparty/spomky-labs/pki-framework/src/X501/ASN1/AttributeValue/GivenNameValue.php @@ -0,0 +1,21 @@ +asPrintableString()->string()); + } +} diff --git a/3rdparty/spomky-labs/pki-framework/src/X501/ASN1/AttributeValue/StateOrProvinceNameValue.php b/3rdparty/spomky-labs/pki-framework/src/X501/ASN1/AttributeValue/StateOrProvinceNameValue.php new file mode 100644 index 00000000..41188cf2 --- /dev/null +++ b/3rdparty/spomky-labs/pki-framework/src/X501/ASN1/AttributeValue/StateOrProvinceNameValue.php @@ -0,0 +1,21 @@ +oid = $oid; + } + + public static function create(string $oid, Element $_element): self + { + return new self($oid, $_element); + } + + public function toASN1(): Element + { + return $this->_element; + } + + public function stringValue(): string + { + // if value is encoded as a string type + if ($this->_element->isType(Element::TYPE_STRING)) { + return $this->_element->asUnspecified() + ->asString() + ->string(); + } + // return DER encoding as a hexstring (see RFC2253 section 2.4) + return '#' . bin2hex($this->_element->toDER()); + } + + public function equalityMatchingRule(): MatchingRule + { + return new BinaryMatch(); + } + + public function rfc2253String(): string + { + $str = $this->_transcodedString(); + // if value has a string representation + if ($this->_element->isType(Element::TYPE_STRING)) { + $str = DNParser::escapeString($str); + } + return $str; + } + + public static function fromASN1(UnspecifiedType $el): AttributeValue + { + throw new BadMethodCallException('ASN.1 parsing must be implemented in a concrete class.'); + } + + protected function _transcodedString(): string + { + // if transcoding is defined for the value type + if (TranscodeStep::isTypeSupported($this->_element->tag())) { + $step = TranscodeStep::create($this->_element->tag()); + return $step->apply($this->stringValue()); + } + return $this->stringValue(); + } +} diff --git a/3rdparty/spomky-labs/pki-framework/src/X501/ASN1/Collection/AttributeCollection.php b/3rdparty/spomky-labs/pki-framework/src/X501/ASN1/Collection/AttributeCollection.php new file mode 100644 index 00000000..998767bb --- /dev/null +++ b/3rdparty/spomky-labs/pki-framework/src/X501/ASN1/Collection/AttributeCollection.php @@ -0,0 +1,187 @@ +_attributes = $attribs; + } + + public static function create(Attribute ...$attribs): static + { + return new static(...$attribs); + } + + /** + * Check whether attribute is present. + * + * @param string $name OID or attribute name + */ + public function has(string $name): bool + { + return $this->_findFirst($name) !== null; + } + + /** + * Get first attribute by OID or attribute name. + * + * @param string $name OID or attribute name + */ + public function firstOf(string $name): Attribute + { + $attr = $this->_findFirst($name); + if ($attr === null) { + throw new UnexpectedValueException("No {$name} attribute."); + } + return $attr; + } + + /** + * Get all attributes of given name. + * + * @param string $name OID or attribute name + * + * @return Attribute[] + */ + public function allOf(string $name): array + { + $oid = AttributeType::attrNameToOID($name); + return array_values(array_filter($this->_attributes, fn (Attribute $attr) => $attr->oid() === $oid)); + } + + /** + * Get all attributes. + * + * @return Attribute[] + */ + public function all(): array + { + return $this->_attributes; + } + + /** + * Get self with additional attributes added. + * + * @param Attribute ...$attribs List of attributes to add + */ + public function withAdditional(Attribute ...$attribs): self + { + $obj = clone $this; + foreach ($attribs as $attr) { + $obj->_attributes[] = $attr; + } + return $obj; + } + + /** + * Get self with single unique attribute added. + * + * All previous attributes of the same type are removed. + * + * @param Attribute $attr Attribute to add + */ + public function withUnique(Attribute $attr): static + { + $attribs = array_values(array_filter($this->_attributes, fn (Attribute $a) => $a->oid() !== $attr->oid())); + $attribs[] = $attr; + $obj = clone $this; + $obj->_attributes = $attribs; + return $obj; + } + + /** + * Get number of attributes. + * + * @see \Countable::count() + */ + public function count(): int + { + return count($this->_attributes); + } + + /** + * Get iterator for attributes. + * + * @return ArrayIterator|Attribute[] + * @see \IteratorAggregate::getIterator() + */ + public function getIterator(): ArrayIterator + { + return new ArrayIterator($this->_attributes); + } + + /** + * Find first attribute of given name or OID. + * + * @param string $name OID or attribute name + */ + protected function _findFirst(string $name): ?Attribute + { + $oid = AttributeType::attrNameToOID($name); + foreach ($this->_attributes as $attr) { + if ($attr->oid() === $oid) { + return $attr; + } + } + return null; + } + + /** + * Initialize from ASN.1 constructed element. + * + * @param Structure $struct ASN.1 structure + */ + protected static function _fromASN1Structure(Structure $struct): static + { + return static::create(...array_map( + static fn (UnspecifiedType $el) => static::_castAttributeValues( + Attribute::fromASN1($el->asSequence()) + ), + $struct->elements() + )); + } + + /** + * Cast Attribute's AttributeValues to implementation specific objects. + * + * Overridden in derived classes. + * + * @param Attribute $attribute Attribute to cast + */ + protected static function _castAttributeValues(Attribute $attribute): Attribute + { + // pass through by default + return $attribute; + } +} diff --git a/3rdparty/spomky-labs/pki-framework/src/X501/ASN1/Collection/SequenceOfAttributes.php b/3rdparty/spomky-labs/pki-framework/src/X501/ASN1/Collection/SequenceOfAttributes.php new file mode 100644 index 00000000..708e4cac --- /dev/null +++ b/3rdparty/spomky-labs/pki-framework/src/X501/ASN1/Collection/SequenceOfAttributes.php @@ -0,0 +1,46 @@ + $value->toAttribute(), $values)); + } + + /** + * Generate ASN.1 structure. + */ + public function toASN1(): Sequence + { + return Sequence::create(...array_map(static fn (Attribute $attr) => $attr->toASN1(), $this->_attributes)); + } +} diff --git a/3rdparty/spomky-labs/pki-framework/src/X501/ASN1/Collection/SetOfAttributes.php b/3rdparty/spomky-labs/pki-framework/src/X501/ASN1/Collection/SetOfAttributes.php new file mode 100644 index 00000000..501956cb --- /dev/null +++ b/3rdparty/spomky-labs/pki-framework/src/X501/ASN1/Collection/SetOfAttributes.php @@ -0,0 +1,47 @@ + $value->toAttribute(), $values)); + } + + /** + * Generate ASN.1 structure. + */ + public function toASN1(): Set + { + $set = Set::create(...array_map(static fn (Attribute $attr) => $attr->toASN1(), $this->_attributes)); + return $set->sortedSetOf(); + } +} diff --git a/3rdparty/spomky-labs/pki-framework/src/X501/ASN1/Name.php b/3rdparty/spomky-labs/pki-framework/src/X501/ASN1/Name.php new file mode 100644 index 00000000..b9163bfa --- /dev/null +++ b/3rdparty/spomky-labs/pki-framework/src/X501/ASN1/Name.php @@ -0,0 +1,193 @@ +rdns = $rdns; + } + + public function __toString(): string + { + return $this->toString(); + } + + public static function create(RDN ...$rdns): self + { + return new self(...$rdns); + } + + /** + * Initialize from ASN.1. + */ + public static function fromASN1(Sequence $seq): self + { + $rdns = array_map(static fn (UnspecifiedType $el) => RDN::fromASN1($el->asSet()), $seq->elements()); + return self::create(...$rdns); + } + + /** + * Initialize from distinguished name string. + * + * @see https://tools.ietf.org/html/rfc1779 + */ + public static function fromString(string $str): self + { + $rdns = []; + foreach (DNParser::parseString($str) as $nameComponent) { + $attribs = []; + foreach ($nameComponent as [$name, $val]) { + $type = AttributeType::fromName($name); + // hexstrings are parsed to ASN.1 elements + if ($val instanceof Element) { + $el = $val; + } else { + $el = AttributeType::asn1StringForType($type->oid(), $val); + } + $value = AttributeValue::fromASN1ByOID($type->oid(), $el->asUnspecified()); + $attribs[] = AttributeTypeAndValue::create($type, $value); + } + $rdns[] = RDN::create(...$attribs); + } + return self::create(...$rdns); + } + + /** + * Generate ASN.1 structure. + */ + public function toASN1(): Sequence + { + $elements = array_map(static fn (RDN $rdn) => $rdn->toASN1(), $this->rdns); + return Sequence::create(...$elements); + } + + /** + * Get distinguised name string conforming to RFC 2253. + * + * @see https://tools.ietf.org/html/rfc2253#section-2.1 + */ + public function toString(): string + { + $parts = array_map(static fn (RDN $rdn) => $rdn->toString(), array_reverse($this->rdns)); + return implode(',', $parts); + } + + /** + * Whether name is semantically equal to other. + * + * Comparison conforms to RFC 4518 string preparation algorithm. + * + * @see https://tools.ietf.org/html/rfc4518 + * + * @param Name $other Object to compare to + */ + public function equals(self $other): bool + { + // if RDN count doesn't match + if (count($this) !== count($other)) { + return false; + } + for ($i = count($this) - 1; $i >= 0; --$i) { + $rdn1 = $this->rdns[$i]; + $rdn2 = $other->rdns[$i]; + if (! $rdn1->equals($rdn2)) { + return false; + } + } + return true; + } + + /** + * Get all RDN objects. + * + * @return RDN[] + */ + public function all(): array + { + return $this->rdns; + } + + /** + * Get the first AttributeValue of given type. + * + * Relative name components shall be traversed in encoding order, which is reversed in regards to the string + * representation. Multi-valued RDN with multiple attributes of the requested type is ambiguous and shall throw an + * exception. + * + * @param string $name Attribute OID or name + */ + public function firstValueOf(string $name): AttributeValue + { + $oid = AttributeType::attrNameToOID($name); + foreach ($this->rdns as $rdn) { + $tvs = $rdn->allOf($oid); + if (count($tvs) > 1) { + throw new RangeException("RDN with multiple {$name} attributes."); + } + if (count($tvs) === 1) { + return $tvs[0]->value(); + } + } + throw new RangeException("Attribute {$name} not found."); + } + + /** + * @see \Countable::count() + */ + public function count(): int + { + return count($this->rdns); + } + + /** + * Get the number of attributes of given type. + * + * @param string $name Attribute OID or name + */ + public function countOfType(string $name): int + { + $oid = AttributeType::attrNameToOID($name); + return array_sum(array_map(static fn (RDN $rdn): int => count($rdn->allOf($oid)), $this->rdns)); + } + + /** + * @see \IteratorAggregate::getIterator() + */ + public function getIterator(): ArrayIterator + { + return new ArrayIterator($this->rdns); + } +} diff --git a/3rdparty/spomky-labs/pki-framework/src/X501/ASN1/RDN.php b/3rdparty/spomky-labs/pki-framework/src/X501/ASN1/RDN.php new file mode 100644 index 00000000..51ea3303 --- /dev/null +++ b/3rdparty/spomky-labs/pki-framework/src/X501/ASN1/RDN.php @@ -0,0 +1,167 @@ +_attribs = $attribs; + } + + public function __toString(): string + { + return $this->toString(); + } + + public static function create(AttributeTypeAndValue ...$attribs): self + { + return new self(...$attribs); + } + + /** + * Convenience method to initialize RDN from AttributeValue objects. + * + * @param AttributeValue ...$values One or more attributes + */ + public static function fromAttributeValues(AttributeValue ...$values): self + { + $attribs = array_map( + static fn (AttributeValue $value) => AttributeTypeAndValue::create(AttributeType::create( + $value->oid() + ), $value), + $values + ); + return self::create(...$attribs); + } + + /** + * Initialize from ASN.1. + */ + public static function fromASN1(Set $set): self + { + $attribs = array_map( + static fn (UnspecifiedType $el) => AttributeTypeAndValue::fromASN1($el->asSequence()), + $set->elements() + ); + return self::create(...$attribs); + } + + /** + * Generate ASN.1 structure. + */ + public function toASN1(): Set + { + $elements = array_map(static fn (AttributeTypeAndValue $tv) => $tv->toASN1(), $this->_attribs); + return Set::create(...$elements)->sortedSetOf(); + } + + /** + * Get name-component string conforming to RFC 2253. + * + * @see https://tools.ietf.org/html/rfc2253#section-2.2 + */ + public function toString(): string + { + $parts = array_map(static fn (AttributeTypeAndValue $tv) => $tv->toString(), $this->_attribs); + return implode('+', $parts); + } + + /** + * Check whether RDN is semantically equal to other. + * + * @param RDN $other Object to compare to + */ + public function equals(self $other): bool + { + // if attribute count doesn't match + if (count($this) !== count($other)) { + return false; + } + $attribs1 = $this->_attribs; + $attribs2 = $other->_attribs; + // if there's multiple attributes, sort using SET OF rules + if (count($attribs1) > 1) { + $attribs1 = self::fromASN1($this->toASN1())->_attribs; + $attribs2 = self::fromASN1($other->toASN1())->_attribs; + } + for ($i = count($attribs1) - 1; $i >= 0; --$i) { + $tv1 = $attribs1[$i]; + $tv2 = $attribs2[$i]; + if (! $tv1->equals($tv2)) { + return false; + } + } + return true; + } + + /** + * Get all AttributeTypeAndValue objects. + * + * @return AttributeTypeAndValue[] + */ + public function all(): array + { + return $this->_attribs; + } + + /** + * Get all AttributeTypeAndValue objects of the given attribute type. + * + * @param string $name Attribute OID or name + * + * @return AttributeTypeAndValue[] + */ + public function allOf(string $name): array + { + $oid = AttributeType::attrNameToOID($name); + $attribs = array_filter($this->_attribs, static fn (AttributeTypeAndValue $tv) => $tv->oid() === $oid); + return array_values($attribs); + } + + /** + * @see \Countable::count() + */ + public function count(): int + { + return count($this->_attribs); + } + + /** + * @see \IteratorAggregate::getIterator() + */ + public function getIterator(): ArrayIterator + { + return new ArrayIterator($this->_attribs); + } +} diff --git a/3rdparty/spomky-labs/pki-framework/src/X501/DN/DNParser.php b/3rdparty/spomky-labs/pki-framework/src/X501/DN/DNParser.php new file mode 100644 index 00000000..1e464125 --- /dev/null +++ b/3rdparty/spomky-labs/pki-framework/src/X501/DN/DNParser.php @@ -0,0 +1,363 @@ +#;'; + + /** + * DN string length. + */ + private readonly int $_len; + + /** + * @param string $_dn Distinguised name + */ + private function __construct( + private readonly string $_dn + ) { + $this->_len = mb_strlen($_dn, '8bit'); + } + + /** + * Parse distinguished name string to name-components. + * + * @return array> + */ + public static function parseString(string $dn): array + { + $parser = new self($dn); + return $parser->parse(); + } + + /** + * Escape a AttributeValue string conforming to RFC 2253. + * + * @see https://tools.ietf.org/html/rfc2253#section-2.4 + */ + public static function escapeString(string $str): string + { + // one of the characters ",", "+", """, "\", "<", ">" or ";" + $str = preg_replace('/([,\+"\\\<\>;])/u', '\\\\$1', $str); + // a space character occurring at the end of the string + $str = preg_replace('/( )$/u', '\\\\$1', (string) $str); + // a space or "#" character occurring at the beginning of the string + $str = preg_replace('/^([ #])/u', '\\\\$1', (string) $str); + // implementation specific special characters + $str = preg_replace_callback( + '/([\pC])/u', + function ($m) { + $octets = mb_str_split(bin2hex($m[1]), 2, '8bit'); + return implode('', array_map(static fn ($octet) => '\\' . mb_strtoupper($octet, '8bit'), $octets)); + }, + (string) $str + ); + return $str; + } + + /** + * Parse DN to name-components. + * + * @return array> + */ + private function parse(): array + { + $offset = 0; + $name = $this->_parseName($offset); + if ($offset < $this->_len) { + $remains = mb_substr($this->_dn, $offset, null, '8bit'); + throw new UnexpectedValueException(sprintf( + 'Parser finished before the end of string, remaining: %s', + $remains + )); + } + return $name; + } + + /** + * Parse 'name'. + * + * name-component *("," name-component) + * + * @return array> Array of name-components + */ + private function _parseName(int &$offset): array + { + $idx = $offset; + $names = []; + while ($idx < $this->_len) { + $names[] = $this->_parseNameComponent($idx); + if ($idx >= $this->_len) { + break; + } + $this->_skipWs($idx); + if ($this->_dn[$idx] !== ',' && $this->_dn[$idx] !== ';') { + break; + } + ++$idx; + $this->_skipWs($idx); + } + $offset = $idx; + return array_reverse($names); + } + + /** + * Parse 'name-component'. + * + * attributeTypeAndValue *("+" attributeTypeAndValue) + * + * @return array> Array of [type, value] tuples + */ + private function _parseNameComponent(int &$offset): array + { + $idx = $offset; + $tvpairs = []; + while ($idx < $this->_len) { + $tvpairs[] = $this->_parseAttrTypeAndValue($idx); + $this->_skipWs($idx); + if ($idx >= $this->_len || $this->_dn[$idx] !== '+') { + break; + } + ++$idx; + $this->_skipWs($idx); + } + $offset = $idx; + return $tvpairs; + } + + /** + * Parse 'attributeTypeAndValue'. + * + * attributeType "=" attributeValue + * + * @return array A tuple of [type, value]. Value may be either a string or + * an Element, if it's encoded as hexstring. + */ + private function _parseAttrTypeAndValue(int &$offset): array + { + $idx = $offset; + $type = $this->_parseAttrType($idx); + $this->_skipWs($idx); + if ($idx >= $this->_len || $this->_dn[$idx++] !== '=') { + throw new UnexpectedValueException('Invalid type and value pair.'); + } + $this->_skipWs($idx); + // hexstring + if ($idx < $this->_len && $this->_dn[$idx] === '#') { + ++$idx; + $data = $this->_parseAttrHexValue($idx); + try { + $value = Element::fromDER($data); + } catch (DecodeException $e) { + throw new UnexpectedValueException('Invalid DER encoding from hexstring.', 0, $e); + } + } else { + $value = $this->_parseAttrStringValue($idx); + } + $offset = $idx; + return [$type, $value]; + } + + /** + * Parse 'attributeType'. + * + * (ALPHA 1*keychar) / oid + */ + private function _parseAttrType(int &$offset): string + { + $idx = $offset; + // dotted OID + $type = $this->_regexMatch('/^(?:oid\.)?([0-9]+(?:\.[0-9]+)*)/i', $idx); + if ($type === null) { + // name + $type = $this->_regexMatch('/^[a-z][a-z0-9\-]*/i', $idx); + if ($type === null) { + throw new UnexpectedValueException('Invalid attribute type.'); + } + } + $offset = $idx; + return $type; + } + + /** + * Parse 'attributeValue' of string type. + */ + private function _parseAttrStringValue(int &$offset): string + { + $idx = $offset; + if ($idx >= $this->_len) { + return ''; + } + if ($this->_dn[$idx] === '"') { // quoted string + $val = $this->_parseQuotedAttrString($idx); + } else { // string + $val = $this->_parseAttrString($idx); + } + $offset = $idx; + return $val; + } + + /** + * Parse plain 'attributeValue' string. + */ + private function _parseAttrString(int &$offset): string + { + $idx = $offset; + $val = ''; + $wsidx = null; + while ($idx < $this->_len) { + $c = $this->_dn[$idx]; + // pair (escape sequence) + if ($c === '\\') { + ++$idx; + $val .= $this->_parsePairAfterSlash($idx); + $wsidx = null; + continue; + } + if ($c === '"') { + throw new UnexpectedValueException('Unexpected quotation.'); + } + if (mb_strpos(self::SPECIAL_CHARS, $c, 0, '8bit') !== false) { + break; + } + // keep track of the first consecutive whitespace + if ($c === ' ') { + if ($wsidx === null) { + $wsidx = $idx; + } + } else { + $wsidx = null; + } + // stringchar + $val .= $c; + ++$idx; + } + // if there was non-escaped whitespace in the end of the value + if ($wsidx !== null) { + $val = mb_substr($val, 0, -($idx - $wsidx), '8bit'); + } + $offset = $idx; + return $val; + } + + /** + * Parse quoted 'attributeValue' string. + * + * @param int $offset Offset to starting quote + */ + private function _parseQuotedAttrString(int &$offset): string + { + $idx = $offset + 1; + $val = ''; + while ($idx < $this->_len) { + $c = $this->_dn[$idx]; + if ($c === '\\') { // pair + ++$idx; + $val .= $this->_parsePairAfterSlash($idx); + continue; + } + if ($c === '"') { + ++$idx; + break; + } + $val .= $c; + ++$idx; + } + $offset = $idx; + return $val; + } + + /** + * Parse 'attributeValue' of binary type. + */ + private function _parseAttrHexValue(int &$offset): string + { + $idx = $offset; + $hexstr = $this->_regexMatch('/^(?:[0-9a-f]{2})+/i', $idx); + if ($hexstr === null) { + throw new UnexpectedValueException('Invalid hexstring.'); + } + $data = hex2bin($hexstr); + $offset = $idx; + return $data; + } + + /** + * Parse 'pair' after leading slash. + */ + private function _parsePairAfterSlash(int &$offset): string + { + $idx = $offset; + if ($idx >= $this->_len) { + throw new UnexpectedValueException('Unexpected end of escape sequence.'); + } + $c = $this->_dn[$idx++]; + // special | \ | " | SPACE + if (mb_strpos(self::SPECIAL_CHARS . '\\" ', $c, 0, '8bit') !== false) { + $val = $c; + } else { // hexpair + if ($idx >= $this->_len) { + throw new UnexpectedValueException('Unexpected end of hexpair.'); + } + $val = @hex2bin($c . $this->_dn[$idx++]); + if ($val === false) { + throw new UnexpectedValueException('Invalid hexpair.'); + } + } + $offset = $idx; + return $val; + } + + /** + * Match DN to pattern and extract the last capture group. + * + * Updates offset to fully matched pattern. + * + * @return null|string Null if pattern doesn't match + */ + private function _regexMatch(string $pattern, int &$offset): ?string + { + $idx = $offset; + if (preg_match($pattern, mb_substr($this->_dn, $idx, null, '8bit'), $match) !== 1) { + return null; + } + $idx += mb_strlen($match[0], '8bit'); + $offset = $idx; + return end($match); + } + + /** + * Skip consecutive spaces. + */ + private function _skipWs(int &$offset): void + { + $idx = $offset; + while ($idx < $this->_len) { + if ($this->_dn[$idx] !== ' ') { + break; + } + ++$idx; + } + $offset = $idx; + } +} diff --git a/3rdparty/spomky-labs/pki-framework/src/X501/MatchingRule/BinaryMatch.php b/3rdparty/spomky-labs/pki-framework/src/X501/MatchingRule/BinaryMatch.php new file mode 100644 index 00000000..0bb6ab31 --- /dev/null +++ b/3rdparty/spomky-labs/pki-framework/src/X501/MatchingRule/BinaryMatch.php @@ -0,0 +1,18 @@ +withCaseFolding(true) + ); + } + + public static function create(int $stringType): self + { + return new self($stringType); + } +} diff --git a/3rdparty/spomky-labs/pki-framework/src/X501/MatchingRule/MatchingRule.php b/3rdparty/spomky-labs/pki-framework/src/X501/MatchingRule/MatchingRule.php new file mode 100644 index 00000000..7bc5a0c2 --- /dev/null +++ b/3rdparty/spomky-labs/pki-framework/src/X501/MatchingRule/MatchingRule.php @@ -0,0 +1,24 @@ +preparer->prepare($assertion); + $value = $this->preparer->prepare($value); + return strcmp($assertion, $value) === 0; + } +} diff --git a/3rdparty/spomky-labs/pki-framework/src/X501/StringPrep/CheckBidiStep.php b/3rdparty/spomky-labs/pki-framework/src/X501/StringPrep/CheckBidiStep.php new file mode 100644 index 00000000..e6aa502b --- /dev/null +++ b/3rdparty/spomky-labs/pki-framework/src/X501/StringPrep/CheckBidiStep.php @@ -0,0 +1,22 @@ +fold) { + $string = mb_convert_case($string, MB_CASE_LOWER, 'UTF-8'); + } + return $string; + } +} diff --git a/3rdparty/spomky-labs/pki-framework/src/X501/StringPrep/NormalizeStep.php b/3rdparty/spomky-labs/pki-framework/src/X501/StringPrep/NormalizeStep.php new file mode 100644 index 00000000..f6413916 --- /dev/null +++ b/3rdparty/spomky-labs/pki-framework/src/X501/StringPrep/NormalizeStep.php @@ -0,0 +1,23 @@ + TranscodeStep::create($string_type), + self::STEP_MAP => MapStep::create(), + self::STEP_NORMALIZE => new NormalizeStep(), + self::STEP_PROHIBIT => new ProhibitStep(), + self::STEP_CHECK_BIDI => new CheckBidiStep(), + // @todo Vary by string type + self::STEP_INSIGNIFICANT_CHARS => new InsignificantNonSubstringSpaceStep(), + ]; + return new self($steps); + } + + /** + * Get self with case folding set. + * + * @param bool $fold True to apply case folding + */ + public function withCaseFolding(bool $fold): self + { + $obj = clone $this; + $obj->_steps[self::STEP_MAP] = MapStep::create($fold); + return $obj; + } + + /** + * Prepare string. + */ + public function prepare(string $string): string + { + foreach ($this->_steps as $step) { + $string = $step->apply($string); + } + return $string; + } +} diff --git a/3rdparty/spomky-labs/pki-framework/src/X501/StringPrep/TranscodeStep.php b/3rdparty/spomky-labs/pki-framework/src/X501/StringPrep/TranscodeStep.php new file mode 100644 index 00000000..ff58611b --- /dev/null +++ b/3rdparty/spomky-labs/pki-framework/src/X501/StringPrep/TranscodeStep.php @@ -0,0 +1,82 @@ + + */ + private const SUPPORTED_TYPES = [ + Element::TYPE_UTF8_STRING, + Element::TYPE_PRINTABLE_STRING, + Element::TYPE_BMP_STRING, + Element::TYPE_UNIVERSAL_STRING, + Element::TYPE_T61_STRING, + ]; + + /** + * @param int $_type ASN.1 type tag of the string + */ + private function __construct( + private readonly int $_type + ) { + } + + public static function create(int $_type): self + { + return new self($_type); + } + + /** + * Check whether transcoding from given ASN.1 type tag is supported. + * + * @param int $type ASN.1 type tag + */ + public static function isTypeSupported(int $type): bool + { + return in_array($type, self::SUPPORTED_TYPES, true); + } + + /** + * @param string $string String to prepare + * + * @return string UTF-8 encoded string + */ + public function apply(string $string): string + { + switch ($this->_type) { + // UTF-8 string as is + case Element::TYPE_UTF8_STRING: + // PrintableString maps directly to UTF-8 + case Element::TYPE_PRINTABLE_STRING: + return $string; + // UCS-2 to UTF-8 + case Element::TYPE_BMP_STRING: + return mb_convert_encoding($string, 'UTF-8', 'UCS-2BE'); + // UCS-4 to UTF-8 + case Element::TYPE_UNIVERSAL_STRING: + return mb_convert_encoding($string, 'UTF-8', 'UCS-4BE'); + // TeletexString mapping is a local matter. + // We take a shortcut here and encode it as a hexstring. + case Element::TYPE_T61_STRING: + $el = T61String::create($string); + return '#' . bin2hex($el->toDER()); + } + throw new LogicException(sprintf('Unsupported string type %s.', Element::tagToName($this->_type))); + } +} diff --git a/3rdparty/spomky-labs/pki-framework/src/X509/AttributeCertificate/AttCertIssuer.php b/3rdparty/spomky-labs/pki-framework/src/X509/AttributeCertificate/AttCertIssuer.php new file mode 100644 index 00000000..8dce6afc --- /dev/null +++ b/3rdparty/spomky-labs/pki-framework/src/X509/AttributeCertificate/AttCertIssuer.php @@ -0,0 +1,69 @@ +tbsCertificate()->subject()); + } + + /** + * Initialize from ASN.1. + * + * @param UnspecifiedType $el CHOICE + */ + public static function fromASN1(UnspecifiedType $el): self + { + if (! $el->isTagged()) { + throw new UnexpectedValueException('v1Form issuer not supported.'); + } + $tagged = $el->asTagged(); + return match ($tagged->tag()) { + 0 => V2Form::fromV2ASN1($tagged->asImplicit(Element::TYPE_SEQUENCE)->asSequence()), + default => throw new UnexpectedValueException('Unsupported issuer type.'), + }; + } +} diff --git a/3rdparty/spomky-labs/pki-framework/src/X509/AttributeCertificate/AttCertValidityPeriod.php b/3rdparty/spomky-labs/pki-framework/src/X509/AttributeCertificate/AttCertValidityPeriod.php new file mode 100644 index 00000000..88119bf8 --- /dev/null +++ b/3rdparty/spomky-labs/pki-framework/src/X509/AttributeCertificate/AttCertValidityPeriod.php @@ -0,0 +1,86 @@ +at(0) + ->asGeneralizedTime() + ->dateTime(); + $na = $seq->at(1) + ->asGeneralizedTime() + ->dateTime(); + return self::create($nb, $na); + } + + /** + * Initialize from date strings. + * + * @param null|string $nb_date Not before date + * @param null|string $na_date Not after date + * @param null|string $tz Timezone string + */ + public static function fromStrings(?string $nb_date, ?string $na_date, ?string $tz = null): self + { + $nb = self::createDateTime($nb_date, $tz); + $na = self::createDateTime($na_date, $tz); + return self::create($nb, $na); + } + + /** + * Get not before time. + */ + public function notBeforeTime(): DateTimeImmutable + { + return $this->notBeforeTime; + } + + /** + * Get not after time. + */ + public function notAfterTime(): DateTimeImmutable + { + return $this->notAfterTime; + } + + /** + * Generate ASN.1 structure. + */ + public function toASN1(): Sequence + { + return Sequence::create( + GeneralizedTime::create($this->notBeforeTime), + GeneralizedTime::create($this->notAfterTime) + ); + } +} diff --git a/3rdparty/spomky-labs/pki-framework/src/X509/AttributeCertificate/Attribute/AccessIdentityAttributeValue.php b/3rdparty/spomky-labs/pki-framework/src/X509/AttributeCertificate/Attribute/AccessIdentityAttributeValue.php new file mode 100644 index 00000000..e40e62ef --- /dev/null +++ b/3rdparty/spomky-labs/pki-framework/src/X509/AttributeCertificate/Attribute/AccessIdentityAttributeValue.php @@ -0,0 +1,43 @@ +asSequence(); + $service = GeneralName::fromASN1($seq->at(0)->asTagged()); + $ident = GeneralName::fromASN1($seq->at(1)->asTagged()); + $auth_info = null; + if ($seq->has(2, Element::TYPE_OCTET_STRING)) { + $auth_info = $seq->at(2) + ->asString() + ->string(); + } + return static::create($service, $ident, $auth_info); + } +} diff --git a/3rdparty/spomky-labs/pki-framework/src/X509/AttributeCertificate/Attribute/AuthenticationInfoAttributeValue.php b/3rdparty/spomky-labs/pki-framework/src/X509/AttributeCertificate/Attribute/AuthenticationInfoAttributeValue.php new file mode 100644 index 00000000..7baa8a20 --- /dev/null +++ b/3rdparty/spomky-labs/pki-framework/src/X509/AttributeCertificate/Attribute/AuthenticationInfoAttributeValue.php @@ -0,0 +1,43 @@ +asSequence(); + $service = GeneralName::fromASN1($seq->at(0)->asTagged()); + $ident = GeneralName::fromASN1($seq->at(1)->asTagged()); + $auth_info = null; + if ($seq->has(2, Element::TYPE_OCTET_STRING)) { + $auth_info = $seq->at(2) + ->asString() + ->string(); + } + return static::create($service, $ident, $auth_info); + } +} diff --git a/3rdparty/spomky-labs/pki-framework/src/X509/AttributeCertificate/Attribute/ChargingIdentityAttributeValue.php b/3rdparty/spomky-labs/pki-framework/src/X509/AttributeCertificate/Attribute/ChargingIdentityAttributeValue.php new file mode 100644 index 00000000..fe828f3d --- /dev/null +++ b/3rdparty/spomky-labs/pki-framework/src/X509/AttributeCertificate/Attribute/ChargingIdentityAttributeValue.php @@ -0,0 +1,20 @@ +_policyAuthority = null; + $this->_values = $values; + } + + abstract public static function create(IetfAttrValue ...$values): self; + + /** + * @return self + */ + public static function fromASN1(UnspecifiedType $el): AttributeValue + { + $seq = $el->asSequence(); + $authority = null; + $idx = 0; + if ($seq->hasTagged(0)) { + $authority = GeneralNames::fromASN1( + $seq->getTagged(0) + ->asImplicit(Element::TYPE_SEQUENCE) + ->asSequence() + ); + ++$idx; + } + $values = array_map( + static fn (UnspecifiedType $el) => IetfAttrValue::fromASN1($el), + $seq->at($idx) + ->asSequence() + ->elements() + ); + $obj = static::create(...$values); + $obj->_policyAuthority = $authority; + return $obj; + } + + /** + * Get self with policy authority. + */ + public function withPolicyAuthority(GeneralNames $names): self + { + $obj = clone $this; + $obj->_policyAuthority = $names; + return $obj; + } + + /** + * Check whether policy authority is present. + */ + public function hasPolicyAuthority(): bool + { + return isset($this->_policyAuthority); + } + + /** + * Get policy authority. + */ + public function policyAuthority(): GeneralNames + { + if (! $this->hasPolicyAuthority()) { + throw new LogicException('policyAuthority not set.'); + } + return $this->_policyAuthority; + } + + /** + * Get values. + * + * @return IetfAttrValue[] + */ + public function values(): array + { + return $this->_values; + } + + /** + * Get first value. + */ + public function first(): IetfAttrValue + { + if (count($this->_values) === 0) { + throw new LogicException('No values.'); + } + return $this->_values[0]; + } + + public function toASN1(): Element + { + $elements = []; + if (isset($this->_policyAuthority)) { + $elements[] = ImplicitlyTaggedType::create(0, $this->_policyAuthority->toASN1()); + } + $values = array_map(static fn (IetfAttrValue $val) => $val->toASN1(), $this->_values); + $elements[] = Sequence::create(...$values); + return Sequence::create(...$elements); + } + + public function stringValue(): string + { + return '#' . bin2hex($this->toASN1()->toDER()); + } + + public function equalityMatchingRule(): MatchingRule + { + return new BinaryMatch(); + } + + public function rfc2253String(): string + { + return $this->stringValue(); + } + + /** + * Get number of values. + * + * @see \Countable::count() + */ + public function count(): int + { + return count($this->_values); + } + + /** + * Get iterator for values. + * + * @see \IteratorAggregate::getIterator() + */ + public function getIterator(): ArrayIterator + { + return new ArrayIterator($this->_values); + } + + protected function _transcodedString(): string + { + return $this->stringValue(); + } +} diff --git a/3rdparty/spomky-labs/pki-framework/src/X509/AttributeCertificate/Attribute/IetfAttrValue.php b/3rdparty/spomky-labs/pki-framework/src/X509/AttributeCertificate/Attribute/IetfAttrValue.php new file mode 100644 index 00000000..7b7c1e1c --- /dev/null +++ b/3rdparty/spomky-labs/pki-framework/src/X509/AttributeCertificate/Attribute/IetfAttrValue.php @@ -0,0 +1,128 @@ +value; + } + + public static function create(string $value, int $type): self + { + return new self($value, $type); + } + + /** + * Initialize from ASN.1. + */ + public static function fromASN1(UnspecifiedType $el): self + { + return match ($el->tag()) { + Element::TYPE_OCTET_STRING, Element::TYPE_UTF8_STRING => self::create( + $el->asString() + ->string(), + $el->tag() + ), + Element::TYPE_OBJECT_IDENTIFIER => self::create($el->asObjectIdentifier()->oid(), $el->tag()), + default => throw new UnexpectedValueException('Type ' . Element::tagToName($el->tag()) . ' not supported.'), + }; + } + + /** + * Initialize from octet string. + */ + public static function fromOctets(string $octets): self + { + return self::create($octets, Element::TYPE_OCTET_STRING); + } + + /** + * Initialize from UTF-8 string. + */ + public static function fromString(string $str): self + { + return self::create($str, Element::TYPE_UTF8_STRING); + } + + /** + * Initialize from OID. + */ + public static function fromOID(string $oid): self + { + return self::create($oid, Element::TYPE_OBJECT_IDENTIFIER); + } + + /** + * Get type tag. + */ + public function type(): int + { + return $this->type; + } + + /** + * Whether value type is octets. + */ + public function isOctets(): bool + { + return $this->type === Element::TYPE_OCTET_STRING; + } + + /** + * Whether value type is OID. + */ + public function isOID(): bool + { + return $this->type === Element::TYPE_OBJECT_IDENTIFIER; + } + + /** + * Whether value type is string. + */ + public function isString(): bool + { + return $this->type === Element::TYPE_UTF8_STRING; + } + + public function value(): string + { + return $this->value; + } + + /** + * Generate ASN.1 structure. + */ + public function toASN1(): Element + { + return match ($this->type) { + Element::TYPE_OCTET_STRING => OctetString::create($this->value), + Element::TYPE_UTF8_STRING => UTF8String::create($this->value), + Element::TYPE_OBJECT_IDENTIFIER => ObjectIdentifier::create($this->value), + default => throw new LogicException('Type ' . Element::tagToName($this->type) . ' not supported.'), + }; + } +} diff --git a/3rdparty/spomky-labs/pki-framework/src/X509/AttributeCertificate/Attribute/RoleAttributeValue.php b/3rdparty/spomky-labs/pki-framework/src/X509/AttributeCertificate/Attribute/RoleAttributeValue.php new file mode 100644 index 00000000..6471fb08 --- /dev/null +++ b/3rdparty/spomky-labs/pki-framework/src/X509/AttributeCertificate/Attribute/RoleAttributeValue.php @@ -0,0 +1,129 @@ +asSequence(); + $authority = null; + if ($seq->hasTagged(0)) { + $authority = GeneralNames::fromASN1( + $seq->getTagged(0) + ->asImplicit(Element::TYPE_SEQUENCE) + ->asSequence() + ); + } + $name = GeneralName::fromASN1($seq->getTagged(1)->asExplicit()->asTagged()); + return self::create($name, $authority); + } + + /** + * Check whether issuing authority is present. + */ + public function hasRoleAuthority(): bool + { + return isset($this->roleAuthority); + } + + /** + * Get issuing authority. + */ + public function roleAuthority(): GeneralNames + { + if (! $this->hasRoleAuthority()) { + throw new LogicException('roleAuthority not set.'); + } + return $this->roleAuthority; + } + + /** + * Get role name. + */ + public function roleName(): GeneralName + { + return $this->roleName; + } + + public function toASN1(): Element + { + $elements = []; + if (isset($this->roleAuthority)) { + $elements[] = ImplicitlyTaggedType::create(0, $this->roleAuthority->toASN1()); + } + $elements[] = ExplicitlyTaggedType::create(1, $this->roleName->toASN1()); + return Sequence::create(...$elements); + } + + public function stringValue(): string + { + return '#' . bin2hex($this->toASN1()->toDER()); + } + + public function equalityMatchingRule(): MatchingRule + { + return new BinaryMatch(); + } + + public function rfc2253String(): string + { + return $this->stringValue(); + } + + protected function _transcodedString(): string + { + return $this->stringValue(); + } +} diff --git a/3rdparty/spomky-labs/pki-framework/src/X509/AttributeCertificate/Attribute/SvceAuthInfo.php b/3rdparty/spomky-labs/pki-framework/src/X509/AttributeCertificate/Attribute/SvceAuthInfo.php new file mode 100644 index 00000000..4e4d93cc --- /dev/null +++ b/3rdparty/spomky-labs/pki-framework/src/X509/AttributeCertificate/Attribute/SvceAuthInfo.php @@ -0,0 +1,95 @@ +service; + } + + public function ident(): GeneralName + { + return $this->ident; + } + + /** + * Check whether authentication info is present. + */ + public function hasAuthInfo(): bool + { + return isset($this->authInfo); + } + + /** + * Get authentication info. + */ + public function authInfo(): string + { + if (! $this->hasAuthInfo()) { + throw new LogicException('authInfo not set.'); + } + return $this->authInfo; + } + + public function toASN1(): Element + { + $elements = [$this->service->toASN1(), $this->ident->toASN1()]; + if (isset($this->authInfo)) { + $elements[] = OctetString::create($this->authInfo); + } + return Sequence::create(...$elements); + } + + public function stringValue(): string + { + return '#' . bin2hex($this->toASN1()->toDER()); + } + + public function equalityMatchingRule(): MatchingRule + { + return new BinaryMatch(); + } + + public function rfc2253String(): string + { + return $this->stringValue(); + } + + protected function _transcodedString(): string + { + return $this->stringValue(); + } +} diff --git a/3rdparty/spomky-labs/pki-framework/src/X509/AttributeCertificate/AttributeCertificate.php b/3rdparty/spomky-labs/pki-framework/src/X509/AttributeCertificate/AttributeCertificate.php new file mode 100644 index 00000000..80e70f2a --- /dev/null +++ b/3rdparty/spomky-labs/pki-framework/src/X509/AttributeCertificate/AttributeCertificate.php @@ -0,0 +1,175 @@ +toPEM() + ->string(); + } + + public static function create( + AttributeCertificateInfo $acInfo, + SignatureAlgorithmIdentifier $signatureAlgorithm, + Signature $signatureValue + ): self { + return new self($acInfo, $signatureAlgorithm, $signatureValue); + } + + /** + * Initialize from ASN.1. + */ + public static function fromASN1(Sequence $seq): self + { + $acinfo = AttributeCertificateInfo::fromASN1($seq->at(0)->asSequence()); + $algo = AlgorithmIdentifier::fromASN1($seq->at(1)->asSequence()); + if (! $algo instanceof SignatureAlgorithmIdentifier) { + throw new UnexpectedValueException('Unsupported signature algorithm ' . $algo->oid() . '.'); + } + $signature = Signature::fromSignatureData($seq->at(2)->asBitString()->string(), $algo); + return self::create($acinfo, $algo, $signature); + } + + /** + * Initialize from DER data. + */ + public static function fromDER(string $data): self + { + return self::fromASN1(UnspecifiedType::fromDER($data)->asSequence()); + } + + /** + * Initialize from PEM. + */ + public static function fromPEM(PEM $pem): self + { + if ($pem->type() !== PEM::TYPE_ATTRIBUTE_CERTIFICATE) { + throw new UnexpectedValueException('Invalid PEM type.'); + } + return self::fromDER($pem->data()); + } + + /** + * Get attribute certificate info. + */ + public function acinfo(): AttributeCertificateInfo + { + return $this->acInfo; + } + + /** + * Get signature algorithm identifier. + */ + public function signatureAlgorithm(): SignatureAlgorithmIdentifier + { + return $this->signatureAlgorithm; + } + + /** + * Get signature value. + */ + public function signatureValue(): Signature + { + return $this->signatureValue; + } + + /** + * Get ASN.1 structure. + */ + public function toASN1(): Sequence + { + return Sequence::create( + $this->acInfo->toASN1(), + $this->signatureAlgorithm->toASN1(), + $this->signatureValue->bitString() + ); + } + + /** + * Get attribute certificate as a DER. + */ + public function toDER(): string + { + return $this->toASN1() + ->toDER(); + } + + /** + * Get attribute certificate as a PEM. + */ + public function toPEM(): PEM + { + return PEM::create(PEM::TYPE_ATTRIBUTE_CERTIFICATE, $this->toDER()); + } + + /** + * Check whether attribute certificate is issued to the subject identified by given public key certificate. + * + * @param Certificate $cert Certificate + */ + public function isHeldBy(Certificate $cert): bool + { + if (! $this->acInfo->holder()->identifiesPKC($cert)) { + return false; + } + return true; + } + + /** + * Check whether attribute certificate is issued by given public key certificate. + * + * @param Certificate $cert Certificate + */ + public function isIssuedBy(Certificate $cert): bool + { + if (! $this->acInfo->issuer()->identifiesPKC($cert)) { + return false; + } + return true; + } + + /** + * Verify signature. + * + * @param PublicKeyInfo $pubkey_info Signer's public key + * @param null|Crypto $crypto Crypto engine, use default if not set + */ + public function verify(PublicKeyInfo $pubkey_info, ?Crypto $crypto = null): bool + { + $crypto ??= Crypto::getDefault(); + $data = $this->acInfo->toASN1() + ->toDER(); + return $crypto->verify($data, $this->signatureValue, $pubkey_info, $this->signatureAlgorithm); + } +} diff --git a/3rdparty/spomky-labs/pki-framework/src/X509/AttributeCertificate/AttributeCertificateInfo.php b/3rdparty/spomky-labs/pki-framework/src/X509/AttributeCertificate/AttributeCertificateInfo.php new file mode 100644 index 00000000..d17f1980 --- /dev/null +++ b/3rdparty/spomky-labs/pki-framework/src/X509/AttributeCertificate/AttributeCertificateInfo.php @@ -0,0 +1,366 @@ +version = self::VERSION_2; + $this->extensions = Extensions::create(); + } + + public static function create( + Holder $holder, + AttCertIssuer $issuer, + AttCertValidityPeriod $attrCertValidityPeriod, + Attributes $attributes + ): self { + return new self($holder, $issuer, $attrCertValidityPeriod, $attributes); + } + + /** + * Initialize from ASN.1. + */ + public static function fromASN1(Sequence $seq): self + { + $idx = 0; + $version = $seq->at($idx++) + ->asInteger() + ->intNumber(); + if ($version !== self::VERSION_2) { + throw new UnexpectedValueException('Version must be 2.'); + } + $holder = Holder::fromASN1($seq->at($idx++)->asSequence()); + $issuer = AttCertIssuer::fromASN1($seq->at($idx++)); + $signature = AlgorithmIdentifier::fromASN1($seq->at($idx++)->asSequence()); + if (! $signature instanceof SignatureAlgorithmIdentifier) { + throw new UnexpectedValueException('Unsupported signature algorithm ' . $signature->oid() . '.'); + } + $serial = $seq->at($idx++) + ->asInteger() + ->number(); + $validity = AttCertValidityPeriod::fromASN1($seq->at($idx++)->asSequence()); + $attribs = Attributes::fromASN1($seq->at($idx++)->asSequence()); + $obj = self::create($holder, $issuer, $validity, $attribs); + $obj->signature = $signature; + $obj->serialNumber = $serial; + if ($seq->has($idx, Element::TYPE_BIT_STRING)) { + $obj->issuerUniqueID = UniqueIdentifier::fromASN1($seq->at($idx++)->asBitString()); + } + if ($seq->has($idx, Element::TYPE_SEQUENCE)) { + $obj->extensions = Extensions::fromASN1($seq->at($idx++)->asSequence()); + } + return $obj; + } + + /** + * Get self with holder. + */ + public function withHolder(Holder $holder): self + { + $obj = clone $this; + $obj->holder = $holder; + return $obj; + } + + /** + * Get self with issuer. + */ + public function withIssuer(AttCertIssuer $issuer): self + { + $obj = clone $this; + $obj->issuer = $issuer; + return $obj; + } + + /** + * Get self with signature algorithm identifier. + */ + public function withSignature(SignatureAlgorithmIdentifier $algo): self + { + $obj = clone $this; + $obj->signature = $algo; + return $obj; + } + + /** + * Get self with serial number. + * + * @param int|string $serial Base 10 serial number + */ + public function withSerialNumber(int|string $serial): self + { + $obj = clone $this; + $obj->serialNumber = strval($serial); + return $obj; + } + + /** + * Get self with random positive serial number. + * + * @param int $size Number of random bytes + */ + public function withRandomSerialNumber(int $size): self + { + // ensure that first byte is always non-zero and having first bit unset + $num = BigInteger::of(random_int(1, 0x7f)); + for ($i = 1; $i < $size; ++$i) { + $num = $num->shiftedLeft(8); + $num = $num->plus(random_int(0, 0xff)); + } + return $this->withSerialNumber($num->toBase(10)); + } + + /** + * Get self with validity period. + */ + public function withValidity(AttCertValidityPeriod $validity): self + { + $obj = clone $this; + $obj->attrCertValidityPeriod = $validity; + return $obj; + } + + /** + * Get self with attributes. + */ + public function withAttributes(Attributes $attribs): self + { + $obj = clone $this; + $obj->attributes = $attribs; + return $obj; + } + + /** + * Get self with issuer unique identifier. + */ + public function withIssuerUniqueID(UniqueIdentifier $uid): self + { + $obj = clone $this; + $obj->issuerUniqueID = $uid; + return $obj; + } + + /** + * Get self with extensions. + */ + public function withExtensions(Extensions $extensions): self + { + $obj = clone $this; + $obj->extensions = $extensions; + return $obj; + } + + /** + * Get self with extensions added. + * + * @param Extension ...$exts One or more Extension objects + */ + public function withAdditionalExtensions(Extension ...$exts): self + { + $obj = clone $this; + $obj->extensions = $obj->extensions->withExtensions(...$exts); + return $obj; + } + + public function version(): int + { + return $this->version; + } + + /** + * Get AC holder. + */ + public function holder(): Holder + { + return $this->holder; + } + + /** + * Get AC issuer. + */ + public function issuer(): AttCertIssuer + { + return $this->issuer; + } + + /** + * Check whether signature is set. + */ + public function hasSignature(): bool + { + return $this->signature !== null; + } + + /** + * Get signature algorithm identifier. + */ + public function signature(): SignatureAlgorithmIdentifier + { + if (! $this->hasSignature()) { + throw new LogicException('signature not set.'); + } + return $this->signature; + } + + /** + * Check whether serial number is present. + */ + public function hasSerialNumber(): bool + { + return isset($this->serialNumber); + } + + /** + * Get AC serial number as a base 10 integer. + */ + public function serialNumber(): string + { + if (! $this->hasSerialNumber()) { + throw new LogicException('serialNumber not set.'); + } + return $this->serialNumber; + } + + /** + * Get validity period. + */ + public function validityPeriod(): AttCertValidityPeriod + { + return $this->attrCertValidityPeriod; + } + + public function attributes(): Attributes + { + return $this->attributes; + } + + /** + * Check whether issuer unique identifier is present. + */ + public function hasIssuerUniqueID(): bool + { + return isset($this->issuerUniqueID); + } + + /** + * Get issuer unique identifier. + */ + public function issuerUniqueID(): UniqueIdentifier + { + if (! $this->hasIssuerUniqueID()) { + throw new LogicException('issuerUniqueID not set.'); + } + return $this->issuerUniqueID; + } + + public function extensions(): Extensions + { + return $this->extensions; + } + + /** + * Get ASN.1 structure. + */ + public function toASN1(): Sequence + { + $elements = [Integer::create($this->version), $this->holder->toASN1(), + $this->issuer->toASN1(), $this->signature() + ->toASN1(), + Integer::create($this->serialNumber()), + $this->attrCertValidityPeriod->toASN1(), + $this->attributes->toASN1(), ]; + if (isset($this->issuerUniqueID)) { + $elements[] = $this->issuerUniqueID->toASN1(); + } + if (count($this->extensions) !== 0) { + $elements[] = $this->extensions->toASN1(); + } + return Sequence::create(...$elements); + } + + /** + * Create signed attribute certificate. + * + * @param SignatureAlgorithmIdentifier $algo Signature algorithm + * @param PrivateKeyInfo $privkey_info Private key + * @param null|Crypto $crypto Crypto engine, use default if not set + */ + public function sign( + SignatureAlgorithmIdentifier $algo, + PrivateKeyInfo $privkey_info, + ?Crypto $crypto = null + ): AttributeCertificate { + $crypto ??= Crypto::getDefault(); + $aci = clone $this; + if (! isset($aci->serialNumber)) { + $aci->serialNumber = '0'; + } + $aci->signature = $algo; + $data = $aci->toASN1() + ->toDER(); + $signature = $crypto->sign($data, $privkey_info, $algo); + return AttributeCertificate::create($aci, $algo, $signature); + } +} diff --git a/3rdparty/spomky-labs/pki-framework/src/X509/AttributeCertificate/Attributes.php b/3rdparty/spomky-labs/pki-framework/src/X509/AttributeCertificate/Attributes.php new file mode 100644 index 00000000..2696a462 --- /dev/null +++ b/3rdparty/spomky-labs/pki-framework/src/X509/AttributeCertificate/Attributes.php @@ -0,0 +1,151 @@ + + */ + private const MAP_OID_TO_CLASS = [ + AccessIdentityAttributeValue::OID => AccessIdentityAttributeValue::class, + AuthenticationInfoAttributeValue::OID => AuthenticationInfoAttributeValue::class, + ChargingIdentityAttributeValue::OID => ChargingIdentityAttributeValue::class, + GroupAttributeValue::OID => GroupAttributeValue::class, + AttributeType::OID_ROLE => RoleAttributeValue::class, + ]; + + /** + * Initialize from attribute values. + * + * @param AttributeValue ...$values List of attribute values + */ + public static function fromAttributeValues(AttributeValue ...$values): static + { + return static::create(...array_map(static fn (AttributeValue $value) => $value->toAttribute(), $values)); + } + + /** + * Check whether 'Access Identity' attribute is present. + */ + public function hasAccessIdentity(): bool + { + return $this->has(AccessIdentityAttributeValue::OID); + } + + /** + * Get the first 'Access Identity' attribute value. + */ + public function accessIdentity(): AccessIdentityAttributeValue + { + return $this->firstOf(AccessIdentityAttributeValue::OID)->first(); + } + + /** + * Check whether 'Service Authentication Information' attribute is present. + */ + public function hasAuthenticationInformation(): bool + { + return $this->has(AuthenticationInfoAttributeValue::OID); + } + + /** + * Get the first 'Service Authentication Information' attribute value. + */ + public function authenticationInformation(): AuthenticationInfoAttributeValue + { + return $this->firstOf(AuthenticationInfoAttributeValue::OID)->first(); + } + + /** + * Check whether 'Charging Identity' attribute is present. + */ + public function hasChargingIdentity(): bool + { + return $this->has(ChargingIdentityAttributeValue::OID); + } + + /** + * Get the first 'Charging Identity' attribute value. + */ + public function chargingIdentity(): ChargingIdentityAttributeValue + { + return $this->firstOf(ChargingIdentityAttributeValue::OID)->first(); + } + + /** + * Check whether 'Group' attribute is present. + */ + public function hasGroup(): bool + { + return $this->has(GroupAttributeValue::OID); + } + + /** + * Get the first 'Group' attribute value. + */ + public function group(): GroupAttributeValue + { + return $this->firstOf(GroupAttributeValue::OID)->first(); + } + + /** + * Check whether 'Role' attribute is present. + */ + public function hasRole(): bool + { + return $this->has(AttributeType::OID_ROLE); + } + + /** + * Get the first 'Role' attribute value. + */ + public function role(): RoleAttributeValue + { + return $this->firstOf(AttributeType::OID_ROLE)->first(); + } + + /** + * Get all 'Role' attribute values. + * + * @return RoleAttributeValue[] + */ + public function roles(): array + { + return array_merge( + [], + ...array_map(static fn (Attribute $attr) => $attr->values(), $this->allOf(AttributeType::OID_ROLE)) + ); + } + + protected static function _castAttributeValues(Attribute $attribute): Attribute + { + $oid = $attribute->oid(); + if (isset(self::MAP_OID_TO_CLASS[$oid])) { + return $attribute->castValues(self::MAP_OID_TO_CLASS[$oid]); + } + return $attribute; + } +} diff --git a/3rdparty/spomky-labs/pki-framework/src/X509/AttributeCertificate/Holder.php b/3rdparty/spomky-labs/pki-framework/src/X509/AttributeCertificate/Holder.php new file mode 100644 index 00000000..d647f9ad --- /dev/null +++ b/3rdparty/spomky-labs/pki-framework/src/X509/AttributeCertificate/Holder.php @@ -0,0 +1,242 @@ +hasTagged(0)) { + $cert_id = IssuerSerial::fromASN1( + $seq->getTagged(0) + ->asImplicit(Element::TYPE_SEQUENCE) + ->asSequence() + ); + } + if ($seq->hasTagged(1)) { + $entity_name = GeneralNames::fromASN1( + $seq->getTagged(1) + ->asImplicit(Element::TYPE_SEQUENCE) + ->asSequence() + ); + } + if ($seq->hasTagged(2)) { + $digest_info = ObjectDigestInfo::fromASN1( + $seq->getTagged(2) + ->asImplicit(Element::TYPE_SEQUENCE) + ->asSequence() + ); + } + return self::create($cert_id, $entity_name) + ->withObjectDigestInfo($digest_info); + } + + /** + * Get self with base certificate ID. + */ + public function withBaseCertificateID(IssuerSerial $issuer): self + { + $obj = clone $this; + $obj->baseCertificateID = $issuer; + return $obj; + } + + /** + * Get self with entity name. + */ + public function withEntityName(GeneralNames $names): self + { + $obj = clone $this; + $obj->entityName = $names; + return $obj; + } + + /** + * Get self with object digest info. + */ + public function withObjectDigestInfo(?ObjectDigestInfo $odi): self + { + $obj = clone $this; + $obj->objectDigestInfo = $odi; + return $obj; + } + + /** + * Check whether base certificate ID is present. + */ + public function hasBaseCertificateID(): bool + { + return isset($this->baseCertificateID); + } + + /** + * Get base certificate ID. + */ + public function baseCertificateID(): IssuerSerial + { + if (! $this->hasBaseCertificateID()) { + throw new LogicException('baseCertificateID not set.'); + } + return $this->baseCertificateID; + } + + /** + * Check whether entity name is present. + */ + public function hasEntityName(): bool + { + return isset($this->entityName); + } + + /** + * Get entity name. + */ + public function entityName(): GeneralNames + { + if (! $this->hasEntityName()) { + throw new LogicException('entityName not set.'); + } + return $this->entityName; + } + + /** + * Check whether object digest info is present. + */ + public function hasObjectDigestInfo(): bool + { + return isset($this->objectDigestInfo); + } + + /** + * Get object digest info. + */ + public function objectDigestInfo(): ObjectDigestInfo + { + if (! $this->hasObjectDigestInfo()) { + throw new LogicException('objectDigestInfo not set.'); + } + return $this->objectDigestInfo; + } + + /** + * Generate ASN.1 structure. + */ + public function toASN1(): Sequence + { + $elements = []; + if (isset($this->baseCertificateID)) { + $elements[] = ImplicitlyTaggedType::create(0, $this->baseCertificateID->toASN1()); + } + if (isset($this->entityName)) { + $elements[] = ImplicitlyTaggedType::create(1, $this->entityName->toASN1()); + } + if (isset($this->objectDigestInfo)) { + $elements[] = ImplicitlyTaggedType::create(2, $this->objectDigestInfo->toASN1()); + } + return Sequence::create(...$elements); + } + + /** + * Check whether Holder identifies given certificate. + */ + public function identifiesPKC(Certificate $cert): bool + { + // if neither baseCertificateID nor entityName are present + if ($this->baseCertificateID === null && $this->entityName === null) { + return false; + } + // if baseCertificateID is present, but doesn't match + if ($this->baseCertificateID !== null && ! $this->baseCertificateID->identifiesPKC($cert)) { + return false; + } + // if entityName is present, but doesn't match + if ($this->entityName !== null && ! $this->_checkEntityName($cert)) { + return false; + } + return true; + } + + /** + * Check whether entityName matches the given certificate. + */ + private function _checkEntityName(Certificate $cert): bool + { + $name = $this->entityName?->firstDN(); + if ($name !== null && $cert->tbsCertificate()->subject()->equals($name)) { + return true; + } + $exts = $cert->tbsCertificate() + ->extensions(); + if ($exts->hasSubjectAlternativeName()) { + $ext = $exts->subjectAlternativeName(); + if ($this->_checkEntityAlternativeNames($ext->names())) { + return true; + } + } + return false; + } + + /** + * Check whether any of the subject alternative names match entityName. + */ + private function _checkEntityAlternativeNames(GeneralNames $san): bool + { + // only directory names supported for now + $name = $this->entityName?->firstDN(); + if ($name === null) { + return false; + } + foreach ($san->allOf(GeneralName::TAG_DIRECTORY_NAME) as $dn) { + if ($dn instanceof DirectoryName && $dn->dn()->equals($name)) { + return true; + } + } + return false; + } +} diff --git a/3rdparty/spomky-labs/pki-framework/src/X509/AttributeCertificate/IssuerSerial.php b/3rdparty/spomky-labs/pki-framework/src/X509/AttributeCertificate/IssuerSerial.php new file mode 100644 index 00000000..d8c4af5d --- /dev/null +++ b/3rdparty/spomky-labs/pki-framework/src/X509/AttributeCertificate/IssuerSerial.php @@ -0,0 +1,141 @@ +at(0)->asSequence()); + $serial = $seq->at(1) + ->asInteger() + ->number(); + $uid = null; + if ($seq->has(2, Element::TYPE_BIT_STRING)) { + $uid = UniqueIdentifier::fromASN1($seq->at(2)->asBitString()); + } + return self::create($issuer, $serial, $uid); + } + + /** + * Initialize from a public key certificate. + */ + public static function fromPKC(Certificate $cert): self + { + $tbsCert = $cert->tbsCertificate(); + $issuer = GeneralNames::create(DirectoryName::create($tbsCert->issuer())); + $serial = $tbsCert->serialNumber(); + $uid = $tbsCert->hasIssuerUniqueID() ? $tbsCert->issuerUniqueID() : null; + return self::create($issuer, $serial, $uid); + } + + /** + * Get issuer name. + */ + public function issuer(): GeneralNames + { + return $this->issuer; + } + + /** + * Get serial number. + */ + public function serial(): string + { + return $this->serial; + } + + /** + * Check whether issuer unique identifier is present. + */ + public function hasIssuerUID(): bool + { + return isset($this->issuerUID); + } + + /** + * Get issuer unique identifier. + */ + public function issuerUID(): UniqueIdentifier + { + if (! $this->hasIssuerUID()) { + throw new LogicException('issuerUID not set.'); + } + return $this->issuerUID; + } + + /** + * Generate ASN.1 structure. + */ + public function toASN1(): Sequence + { + $elements = [$this->issuer->toASN1(), Integer::create($this->serial)]; + if (isset($this->issuerUID)) { + $elements[] = $this->issuerUID->toASN1(); + } + return Sequence::create(...$elements); + } + + /** + * Check whether this IssuerSerial identifies given certificate. + */ + public function identifiesPKC(Certificate $cert): bool + { + $tbs = $cert->tbsCertificate(); + if (! $tbs->issuer()->equals($this->issuer->firstDN())) { + return false; + } + if ($tbs->serialNumber() !== $this->serial) { + return false; + } + if ($this->issuerUID !== null && ! $this->_checkUniqueID($cert)) { + return false; + } + return true; + } + + /** + * Check whether issuerUID matches given certificate. + */ + private function _checkUniqueID(Certificate $cert): bool + { + if (! $cert->tbsCertificate()->hasIssuerUniqueID()) { + return false; + } + $uid = $cert->tbsCertificate() + ->issuerUniqueID() + ->string(); + return $this->issuerUID?->string() === $uid; + } +} diff --git a/3rdparty/spomky-labs/pki-framework/src/X509/AttributeCertificate/ObjectDigestInfo.php b/3rdparty/spomky-labs/pki-framework/src/X509/AttributeCertificate/ObjectDigestInfo.php new file mode 100644 index 00000000..78a52656 --- /dev/null +++ b/3rdparty/spomky-labs/pki-framework/src/X509/AttributeCertificate/ObjectDigestInfo.php @@ -0,0 +1,80 @@ +at($idx++) + ->asEnumerated() + ->intNumber(); + if ($seq->has($idx, Element::TYPE_OBJECT_IDENTIFIER)) { + $oid = $seq->at($idx++) + ->asObjectIdentifier() + ->oid(); + } + $algo = AlgorithmIdentifier::fromASN1($seq->at($idx++)->asSequence()); + $digest = $seq->at($idx) + ->asBitString(); + return self::create($type, $algo, $digest, $oid); + } + + /** + * Generate ASN.1 structure. + */ + public function toASN1(): Sequence + { + $elements = [Enumerated::create($this->digestedObjectType)]; + if (isset($this->otherObjectTypeID)) { + $elements[] = ObjectIdentifier::create($this->otherObjectTypeID); + } + $elements[] = $this->digestAlgorithm->toASN1(); + $elements[] = $this->objectDigest; + return Sequence::create(...$elements); + } +} diff --git a/3rdparty/spomky-labs/pki-framework/src/X509/AttributeCertificate/V2Form.php b/3rdparty/spomky-labs/pki-framework/src/X509/AttributeCertificate/V2Form.php new file mode 100644 index 00000000..ee5813f8 --- /dev/null +++ b/3rdparty/spomky-labs/pki-framework/src/X509/AttributeCertificate/V2Form.php @@ -0,0 +1,116 @@ +has(0, Element::TYPE_SEQUENCE)) { + $issuer = GeneralNames::fromASN1($seq->at(0)->asSequence()); + } + if ($seq->hasTagged(0)) { + $cert_id = IssuerSerial::fromASN1( + $seq->getTagged(0) + ->asImplicit(Element::TYPE_SEQUENCE) + ->asSequence() + ); + } + if ($seq->hasTagged(1)) { + $digest_info = ObjectDigestInfo::fromASN1( + $seq->getTagged(1) + ->asImplicit(Element::TYPE_SEQUENCE) + ->asSequence() + ); + } + return self::create($issuer, $cert_id, $digest_info); + } + + /** + * Check whether issuer name is set. + */ + public function hasIssuerName(): bool + { + return isset($this->issuerName); + } + + /** + * Get issuer name. + */ + public function issuerName(): GeneralNames + { + if (! $this->hasIssuerName()) { + throw new LogicException('issuerName not set.'); + } + return $this->issuerName; + } + + /** + * Get DN of the issuer. + * + * This is a convenience method conforming to RFC 5755, which states that Issuer must contain only one non-empty + * distinguished name. + */ + public function name(): Name + { + return $this->issuerName() + ->firstDN(); + } + + public function toASN1(): Element + { + $elements = []; + if (isset($this->issuerName)) { + $elements[] = $this->issuerName->toASN1(); + } + if (isset($this->baseCertificateID)) { + $elements[] = ImplicitlyTaggedType::create(0, $this->baseCertificateID->toASN1()); + } + if (isset($this->objectDigestInfo)) { + $elements[] = ImplicitlyTaggedType::create(1, $this->objectDigestInfo->toASN1()); + } + return ImplicitlyTaggedType::create(0, Sequence::create(...$elements)); + } + + public function identifiesPKC(Certificate $cert): bool + { + $name = $this->issuerName?->firstDN(); + return ! ($name === null || ! $cert->tbsCertificate()->subject()->equals($name)); + } +} diff --git a/3rdparty/spomky-labs/pki-framework/src/X509/AttributeCertificate/Validation/ACValidationConfig.php b/3rdparty/spomky-labs/pki-framework/src/X509/AttributeCertificate/Validation/ACValidationConfig.php new file mode 100644 index 00000000..4f994698 --- /dev/null +++ b/3rdparty/spomky-labs/pki-framework/src/X509/AttributeCertificate/Validation/ACValidationConfig.php @@ -0,0 +1,98 @@ +evalTime = new DateTimeImmutable(); + $this->targets = []; + } + + public static function create(CertificationPath $holderPath, CertificationPath $issuerPath): self + { + return new self($holderPath, $issuerPath); + } + + /** + * Get certification path of the AC's holder. + */ + public function holderPath(): CertificationPath + { + return $this->holderPath; + } + + /** + * Get certification path of the AC's issuer. + */ + public function issuerPath(): CertificationPath + { + return $this->issuerPath; + } + + /** + * Get self with given evaluation reference time. + */ + public function withEvaluationTime(DateTimeImmutable $dt): self + { + $obj = clone $this; + $obj->evalTime = $dt; + return $obj; + } + + /** + * Get the evaluation reference time. + */ + public function evaluationTime(): DateTimeImmutable + { + return $this->evalTime; + } + + /** + * Get self with permitted targets. + */ + public function withTargets(Target ...$targets): self + { + $obj = clone $this; + $obj->targets = $targets; + return $obj; + } + + /** + * Get array of permitted targets. + * + * @return Target[] + */ + public function targets(): array + { + return $this->targets; + } +} diff --git a/3rdparty/spomky-labs/pki-framework/src/X509/AttributeCertificate/Validation/ACValidator.php b/3rdparty/spomky-labs/pki-framework/src/X509/AttributeCertificate/Validation/ACValidator.php new file mode 100644 index 00000000..934427ce --- /dev/null +++ b/3rdparty/spomky-labs/pki-framework/src/X509/AttributeCertificate/Validation/ACValidator.php @@ -0,0 +1,185 @@ +crypto = $crypto ?? Crypto::getDefault(); + } + + public static function create( + AttributeCertificate $ac, + ACValidationConfig $config, + ?Crypto $crypto = null + ): self { + return new self($ac, $config, $crypto); + } + + /** + * Validate attribute certificate. + * + * @return AttributeCertificate Validated AC + */ + public function validate(): AttributeCertificate + { + $this->validateHolder(); + $issuer = $this->verifyIssuer(); + $this->validateIssuerProfile($issuer); + $this->validateTime(); + $this->validateTargeting(); + return $this->ac; + } + + /** + * Validate AC holder's certification. + * + * @return Certificate Certificate of the AC's holder + */ + private function validateHolder(): Certificate + { + $path = $this->config->holderPath(); + $config = PathValidationConfig::defaultConfig() + ->withMaxLength(count($path)) + ->withDateTime($this->config->evaluationTime()); + try { + $holder = $path->validate($config, $this->crypto) + ->certificate(); + } catch (PathValidationException $e) { + throw new ACValidationException("Failed to validate holder PKC's certification path.", 0, $e); + } + if (! $this->ac->isHeldBy($holder)) { + throw new ACValidationException("Name mismatch of AC's holder PKC."); + } + return $holder; + } + + /** + * Verify AC's signature and issuer's certification. + * + * @return Certificate Certificate of the AC's issuer + */ + private function verifyIssuer(): Certificate + { + $path = $this->config->issuerPath(); + $config = PathValidationConfig::defaultConfig() + ->withMaxLength(count($path)) + ->withDateTime($this->config->evaluationTime()); + try { + $issuer = $path->validate($config, $this->crypto) + ->certificate(); + } catch (PathValidationException $e) { + throw new ACValidationException("Failed to validate issuer PKC's certification path.", 0, $e); + } + if (! $this->ac->isIssuedBy($issuer)) { + throw new ACValidationException("Name mismatch of AC's issuer PKC."); + } + $pubkey_info = $issuer->tbsCertificate() + ->subjectPublicKeyInfo(); + if (! $this->ac->verify($pubkey_info, $this->crypto)) { + throw new ACValidationException('Failed to verify signature.'); + } + return $issuer; + } + + /** + * Validate AC issuer's profile. + * + * @see https://tools.ietf.org/html/rfc5755#section-4.5 + */ + private function validateIssuerProfile(Certificate $cert): void + { + $exts = $cert->tbsCertificate() + ->extensions(); + if ($exts->hasKeyUsage() && ! $exts->keyUsage()->isDigitalSignature()) { + throw new ACValidationException( + "Issuer PKC's Key Usage extension doesn't permit" . + ' verification of digital signatures.' + ); + } + if ($exts->hasBasicConstraints() && $exts->basicConstraints()->isCA()) { + throw new ACValidationException('Issuer PKC must not be a CA.'); + } + } + + /** + * Validate AC's validity period. + */ + private function validateTime(): void + { + $t = $this->config->evaluationTime(); + $validity = $this->ac->acinfo() + ->validityPeriod(); + if ($validity->notBeforeTime()->diff($t)->invert === 1) { + throw new ACValidationException('Validity period has not started.'); + } + if ($t->diff($validity->notAfterTime())->invert === 1) { + throw new ACValidationException('Attribute certificate has expired.'); + } + } + + /** + * Validate AC's target information. + */ + private function validateTargeting(): void + { + $exts = $this->ac->acinfo() + ->extensions(); + // if target information extension is not present + if (! $exts->has(Extension::OID_TARGET_INFORMATION)) { + return; + } + $ext = $exts->get(Extension::OID_TARGET_INFORMATION); + if ($ext instanceof TargetInformationExtension && + ! $this->_hasMatchingTarget($ext->targets())) { + throw new ACValidationException("Attribute certificate doesn't have a matching target."); + } + } + + /** + * Check whether validation configuration has matching targets. + * + * @param Targets $targets Set of eligible targets + */ + private function _hasMatchingTarget(Targets $targets): bool + { + foreach ($this->config->targets() as $target) { + if ($targets->hasTarget($target)) { + return true; + } + } + return false; + } +} diff --git a/3rdparty/spomky-labs/pki-framework/src/X509/AttributeCertificate/Validation/Exception/ACValidationException.php b/3rdparty/spomky-labs/pki-framework/src/X509/AttributeCertificate/Validation/Exception/ACValidationException.php new file mode 100644 index 00000000..bd33d064 --- /dev/null +++ b/3rdparty/spomky-labs/pki-framework/src/X509/AttributeCertificate/Validation/Exception/ACValidationException.php @@ -0,0 +1,11 @@ +toPEM() + ->string(); + } + + public static function create( + TBSCertificate $tbsCertificate, + SignatureAlgorithmIdentifier $signatureAlgorithm, + Signature $signatureValue + ): self { + return new self($tbsCertificate, $signatureAlgorithm, $signatureValue); + } + + /** + * Initialize from ASN.1. + */ + public static function fromASN1(Sequence $seq): self + { + $tbsCert = TBSCertificate::fromASN1($seq->at(0)->asSequence()); + $algo = AlgorithmIdentifier::fromASN1($seq->at(1)->asSequence()); + if (! $algo instanceof SignatureAlgorithmIdentifier) { + throw new UnexpectedValueException('Unsupported signature algorithm ' . $algo->oid() . '.'); + } + $signature = Signature::fromSignatureData($seq->at(2)->asBitString()->string(), $algo); + return self::create($tbsCert, $algo, $signature); + } + + /** + * Initialize from DER. + */ + public static function fromDER(string $data): self + { + return self::fromASN1(UnspecifiedType::fromDER($data)->asSequence()); + } + + /** + * Initialize from PEM. + */ + public static function fromPEM(PEM $pem): self + { + if ($pem->type() !== PEM::TYPE_CERTIFICATE) { + throw new UnexpectedValueException('Invalid PEM type.'); + } + return self::fromDER($pem->data()); + } + + /** + * Get certificate information. + */ + public function tbsCertificate(): TBSCertificate + { + return $this->tbsCertificate; + } + + /** + * Get signature algorithm. + */ + public function signatureAlgorithm(): SignatureAlgorithmIdentifier + { + return $this->signatureAlgorithm; + } + + /** + * Get signature value. + */ + public function signatureValue(): Signature + { + return $this->signatureValue; + } + + /** + * Check whether certificate is self-issued. + */ + public function isSelfIssued(): bool + { + return $this->tbsCertificate->subject() + ->equals($this->tbsCertificate->issuer()); + } + + /** + * Check whether certificate is semantically equal to another. + * + * @param Certificate $cert Certificate to compare to + */ + public function equals(self $cert): bool + { + return $this->_hasEqualSerialNumber($cert) && + $this->_hasEqualPublicKey($cert) && $this->_hasEqualSubject($cert); + } + + /** + * Generate ASN.1 structure. + */ + public function toASN1(): Sequence + { + return Sequence::create( + $this->tbsCertificate->toASN1(), + $this->signatureAlgorithm->toASN1(), + $this->signatureValue->bitString() + ); + } + + /** + * Get certificate as a DER. + */ + public function toDER(): string + { + return $this->toASN1() + ->toDER(); + } + + /** + * Get certificate as a PEM. + */ + public function toPEM(): PEM + { + return PEM::create(PEM::TYPE_CERTIFICATE, $this->toDER()); + } + + /** + * Verify certificate signature. + * + * @param PublicKeyInfo $pubkey_info Issuer's public key + * @param null|Crypto $crypto Crypto engine, use default if not set + * + * @return bool True if certificate signature is valid + */ + public function verify(PublicKeyInfo $pubkey_info, ?Crypto $crypto = null): bool + { + $crypto ??= Crypto::getDefault(); + $data = $this->tbsCertificate->toASN1() + ->toDER(); + return $crypto->verify($data, $this->signatureValue, $pubkey_info, $this->signatureAlgorithm); + } + + /** + * Check whether certificate has serial number equal to another. + */ + private function _hasEqualSerialNumber(self $cert): bool + { + $sn1 = $this->tbsCertificate->serialNumber(); + $sn2 = $cert->tbsCertificate->serialNumber(); + return $sn1 === $sn2; + } + + /** + * Check whether certificate has public key equal to another. + */ + private function _hasEqualPublicKey(self $cert): bool + { + $kid1 = $this->tbsCertificate->subjectPublicKeyInfo() + ->keyIdentifier(); + $kid2 = $cert->tbsCertificate->subjectPublicKeyInfo() + ->keyIdentifier(); + return $kid1 === $kid2; + } + + /** + * Check whether certificate has subject equal to another. + */ + private function _hasEqualSubject(self $cert): bool + { + $dn1 = $this->tbsCertificate->subject(); + $dn2 = $cert->tbsCertificate->subject(); + return $dn1->equals($dn2); + } +} diff --git a/3rdparty/spomky-labs/pki-framework/src/X509/Certificate/CertificateBundle.php b/3rdparty/spomky-labs/pki-framework/src/X509/Certificate/CertificateBundle.php new file mode 100644 index 00000000..9429cdc5 --- /dev/null +++ b/3rdparty/spomky-labs/pki-framework/src/X509/Certificate/CertificateBundle.php @@ -0,0 +1,202 @@ +certs = $certs; + } + + /** + * Reset internal cached variables on clone. + */ + public function __clone() + { + $this->keyIdMap = null; + } + + public static function create(Certificate ...$certs): self + { + return new self(...$certs); + } + + /** + * Initialize from PEMs. + * + * @param PEM ...$pems PEM objects + */ + public static function fromPEMs(PEM ...$pems): self + { + $certs = array_map(static fn ($pem) => Certificate::fromPEM($pem), $pems); + return self::create(...$certs); + } + + /** + * Initialize from PEM bundle. + */ + public static function fromPEMBundle(PEMBundle $pem_bundle): self + { + return self::fromPEMs(...$pem_bundle->all()); + } + + /** + * Get self with certificates added. + */ + public function withCertificates(Certificate ...$cert): self + { + $obj = clone $this; + $obj->certs = array_merge($obj->certs, $cert); + return $obj; + } + + /** + * Get self with certificates from PEMBundle added. + */ + public function withPEMBundle(PEMBundle $pem_bundle): self + { + $certs = $this->certs; + foreach ($pem_bundle as $pem) { + $certs[] = Certificate::fromPEM($pem); + } + return self::create(...$certs); + } + + /** + * Get self with single certificate from PEM added. + */ + public function withPEM(PEM $pem): self + { + $certs = $this->certs; + $certs[] = Certificate::fromPEM($pem); + return self::create(...$certs); + } + + /** + * Check whether bundle contains a given certificate. + */ + public function contains(Certificate $cert): bool + { + $id = self::_getCertKeyId($cert); + $map = $this->_getKeyIdMap(); + if (! isset($map[$id])) { + return false; + } + foreach ($map[$id] as $c) { + /** @var Certificate $c */ + if ($cert->equals($c)) { + return true; + } + } + return false; + } + + /** + * Get all certificates that have given subject key identifier. + * + * @return Certificate[] + */ + public function allBySubjectKeyIdentifier(string $id): array + { + $map = $this->_getKeyIdMap(); + if (! isset($map[$id])) { + return []; + } + return $map[$id]; + } + + /** + * Get all certificates in a bundle. + * + * @return Certificate[] + */ + public function all(): array + { + return $this->certs; + } + + /** + * @see \Countable::count() + */ + public function count(): int + { + return count($this->certs); + } + + /** + * Get iterator for certificates. + * + * @see \IteratorAggregate::getIterator() + */ + public function getIterator(): ArrayIterator + { + return new ArrayIterator($this->certs); + } + + /** + * Get certificate mapping by public key id. + * + * @return (Certificate[])[] + */ + private function _getKeyIdMap(): array + { + // lazily build mapping + if (! isset($this->keyIdMap)) { + $this->keyIdMap = []; + foreach ($this->certs as $cert) { + $id = self::_getCertKeyId($cert); + if (! isset($this->keyIdMap[$id])) { + $this->keyIdMap[$id] = []; + } + array_push($this->keyIdMap[$id], $cert); + } + } + return $this->keyIdMap; + } + + /** + * Get public key id for the certificate. + */ + private static function _getCertKeyId(Certificate $cert): string + { + $exts = $cert->tbsCertificate() + ->extensions(); + if ($exts->hasSubjectKeyIdentifier()) { + return $exts->subjectKeyIdentifier() + ->keyIdentifier(); + } + return $cert->tbsCertificate() + ->subjectPublicKeyInfo() + ->keyIdentifier(); + } +} diff --git a/3rdparty/spomky-labs/pki-framework/src/X509/Certificate/CertificateChain.php b/3rdparty/spomky-labs/pki-framework/src/X509/Certificate/CertificateChain.php new file mode 100644 index 00000000..d4ef84ce --- /dev/null +++ b/3rdparty/spomky-labs/pki-framework/src/X509/Certificate/CertificateChain.php @@ -0,0 +1,124 @@ +certs = $certs; + } + + public static function create(Certificate ...$certs): self + { + return new self(...$certs); + } + + /** + * Initialize from a list of PEMs. + */ + public static function fromPEMs(PEM ...$pems): self + { + $certs = array_map(static fn (PEM $pem) => Certificate::fromPEM($pem), $pems); + return self::create(...$certs); + } + + /** + * Initialize from a string containing multiple PEM blocks. + */ + public static function fromPEMString(string $str): self + { + $pems = PEMBundle::fromString($str)->all(); + return self::fromPEMs(...$pems); + } + + /** + * Get all certificates in a chain ordered from the end-entity certificate to the trust anchor. + * + * @return Certificate[] + */ + public function certificates(): array + { + return $this->certs; + } + + /** + * Get the end-entity certificate. + */ + public function endEntityCertificate(): Certificate + { + if (count($this->certs) === 0) { + throw new LogicException('No certificates.'); + } + return $this->certs[0]; + } + + /** + * Get the trust anchor certificate. + */ + public function trustAnchorCertificate(): Certificate + { + if (count($this->certs) === 0) { + throw new LogicException('No certificates.'); + } + return $this->certs[count($this->certs) - 1]; + } + + /** + * Convert certificate chain to certification path. + */ + public function certificationPath(): CertificationPath + { + return CertificationPath::fromCertificateChain($this); + } + + /** + * Convert certificate chain to string of PEM blocks. + */ + public function toPEMString(): string + { + return implode("\n", array_map(static fn (Certificate $cert) => $cert->toPEM()->string(), $this->certs)); + } + + /** + * @see \Countable::count() + */ + public function count(): int + { + return count($this->certs); + } + + /** + * Get iterator for certificates. + * + * @see \IteratorAggregate::getIterator() + */ + public function getIterator(): ArrayIterator + { + return new ArrayIterator($this->certs); + } +} diff --git a/3rdparty/spomky-labs/pki-framework/src/X509/Certificate/Extension/AAControlsExtension.php b/3rdparty/spomky-labs/pki-framework/src/X509/Certificate/Extension/AAControlsExtension.php new file mode 100644 index 00000000..d576e4ec --- /dev/null +++ b/3rdparty/spomky-labs/pki-framework/src/X509/Certificate/Extension/AAControlsExtension.php @@ -0,0 +1,184 @@ +pathLenConstraint); + } + + /** + * Get path length constraint. + */ + public function pathLen(): int + { + if (! $this->hasPathLen()) { + throw new LogicException('pathLen not set.'); + } + return $this->pathLenConstraint; + } + + /** + * Check whether permitted attributes are present. + */ + public function hasPermittedAttrs(): bool + { + return isset($this->permittedAttrs); + } + + /** + * Get OID's of permitted attributes. + * + * @return string[] + */ + public function permittedAttrs(): array + { + if (! $this->hasPermittedAttrs()) { + throw new LogicException('permittedAttrs not set.'); + } + return $this->permittedAttrs; + } + + /** + * Check whether excluded attributes are present. + */ + public function hasExcludedAttrs(): bool + { + return isset($this->excludedAttrs); + } + + /** + * Get OID's of excluded attributes. + * + * @return string[] + */ + public function excludedAttrs(): array + { + if (! $this->hasExcludedAttrs()) { + throw new LogicException('excludedAttrs not set.'); + } + return $this->excludedAttrs; + } + + /** + * Whether to permit attributes that are not explicitly specified in neither permitted nor excluded list. + */ + public function permitUnspecified(): bool + { + return $this->permitUnSpecified; + } + + protected static function fromDER(string $data, bool $critical): static + { + $seq = UnspecifiedType::fromDER($data)->asSequence(); + $path_len = null; + $permitted = null; + $excluded = null; + $permit_unspecified = true; + $idx = 0; + if ($seq->has($idx, Element::TYPE_INTEGER)) { + $path_len = $seq->at($idx++) + ->asInteger() + ->intNumber(); + } + if ($seq->hasTagged(0)) { + $attr_seq = $seq->getTagged(0) + ->asImplicit(Element::TYPE_SEQUENCE) + ->asSequence(); + $permitted = array_map( + static fn (UnspecifiedType $el) => $el->asObjectIdentifier() + ->oid(), + $attr_seq->elements() + ); + ++$idx; + } + if ($seq->hasTagged(1)) { + $attr_seq = $seq->getTagged(1) + ->asImplicit(Element::TYPE_SEQUENCE) + ->asSequence(); + $excluded = array_map( + static fn (UnspecifiedType $el) => $el->asObjectIdentifier() + ->oid(), + $attr_seq->elements() + ); + ++$idx; + } + if ($seq->has($idx, Element::TYPE_BOOLEAN)) { + $permit_unspecified = $seq->at($idx++) + ->asBoolean() + ->value(); + } + return self::create($critical, $path_len, $permitted, $excluded, $permit_unspecified); + } + + protected function valueASN1(): Element + { + $elements = []; + if (isset($this->pathLenConstraint)) { + $elements[] = Integer::create($this->pathLenConstraint); + } + if (isset($this->permittedAttrs)) { + $oids = array_map(static fn ($oid) => ObjectIdentifier::create($oid), $this->permittedAttrs); + $elements[] = ImplicitlyTaggedType::create(0, Sequence::create(...$oids)); + } + if (isset($this->excludedAttrs)) { + $oids = array_map(static fn ($oid) => ObjectIdentifier::create($oid), $this->excludedAttrs); + $elements[] = ImplicitlyTaggedType::create(1, Sequence::create(...$oids)); + } + if ($this->permitUnSpecified !== true) { + $elements[] = Boolean::create(false); + } + return Sequence::create(...$elements); + } +} diff --git a/3rdparty/spomky-labs/pki-framework/src/X509/Certificate/Extension/AccessDescription/AccessDescription.php b/3rdparty/spomky-labs/pki-framework/src/X509/Certificate/Extension/AccessDescription/AccessDescription.php new file mode 100644 index 00000000..99c01960 --- /dev/null +++ b/3rdparty/spomky-labs/pki-framework/src/X509/Certificate/Extension/AccessDescription/AccessDescription.php @@ -0,0 +1,57 @@ +accessMethod; + } + + /** + * Get the access location. + */ + public function accessLocation(): GeneralName + { + return $this->accessLocation; + } + + /** + * Generate ASN.1 structure. + */ + public function toASN1(): Sequence + { + return Sequence::create(ObjectIdentifier::create($this->accessMethod), $this->accessLocation->toASN1()); + } +} diff --git a/3rdparty/spomky-labs/pki-framework/src/X509/Certificate/Extension/AccessDescription/AuthorityAccessDescription.php b/3rdparty/spomky-labs/pki-framework/src/X509/Certificate/Extension/AccessDescription/AuthorityAccessDescription.php new file mode 100644 index 00000000..ac1fa157 --- /dev/null +++ b/3rdparty/spomky-labs/pki-framework/src/X509/Certificate/Extension/AccessDescription/AuthorityAccessDescription.php @@ -0,0 +1,52 @@ +at(0)->asObjectIdentifier()->oid(), GeneralName::fromASN1($seq->at(1)->asTagged())); + } + + /** + * Check whether access method is OSCP. + */ + public function isOSCPMethod(): bool + { + return $this->accessMethod === self::OID_METHOD_OSCP; + } + + /** + * Check whether access method is CA issuers. + */ + public function isCAIssuersMethod(): bool + { + return $this->accessMethod === self::OID_METHOD_CA_ISSUERS; + } +} diff --git a/3rdparty/spomky-labs/pki-framework/src/X509/Certificate/Extension/AccessDescription/SubjectAccessDescription.php b/3rdparty/spomky-labs/pki-framework/src/X509/Certificate/Extension/AccessDescription/SubjectAccessDescription.php new file mode 100644 index 00000000..43e3767d --- /dev/null +++ b/3rdparty/spomky-labs/pki-framework/src/X509/Certificate/Extension/AccessDescription/SubjectAccessDescription.php @@ -0,0 +1,52 @@ +at(0)->asObjectIdentifier()->oid(), GeneralName::fromASN1($seq->at(1)->asTagged())); + } + + /** + * Check whether access method is time stamping. + */ + public function isTimeStampingMethod(): bool + { + return $this->accessMethod === self::OID_METHOD_TIME_STAMPING; + } + + /** + * Check whether access method is CA repository. + */ + public function isCARepositoryMethod(): bool + { + return $this->accessMethod === self::OID_METHOD_CA_REPOSITORY; + } +} diff --git a/3rdparty/spomky-labs/pki-framework/src/X509/Certificate/Extension/AuthorityInformationAccessExtension.php b/3rdparty/spomky-labs/pki-framework/src/X509/Certificate/Extension/AuthorityInformationAccessExtension.php new file mode 100644 index 00000000..3fbcd886 --- /dev/null +++ b/3rdparty/spomky-labs/pki-framework/src/X509/Certificate/Extension/AuthorityInformationAccessExtension.php @@ -0,0 +1,87 @@ +accessDescriptions = $access; + } + + public static function create(bool $critical, AuthorityAccessDescription ...$access): self + { + return new self($critical, ...$access); + } + + /** + * Get the access descriptions. + * + * @return AuthorityAccessDescription[] + */ + public function accessDescriptions(): array + { + return $this->accessDescriptions; + } + + /** + * Get the number of access descriptions. + * + * @see \Countable::count() + */ + public function count(): int + { + return count($this->accessDescriptions); + } + + /** + * Get iterator for access descriptions. + * + * @return ArrayIterator List of AuthorityAccessDescription objects + * @see \IteratorAggregate::getIterator() + */ + public function getIterator(): ArrayIterator + { + return new ArrayIterator($this->accessDescriptions); + } + + protected static function fromDER(string $data, bool $critical): static + { + $access = array_map( + static fn (UnspecifiedType $el) => AuthorityAccessDescription::fromASN1($el->asSequence()), + UnspecifiedType::fromDER($data)->asSequence()->elements() + ); + return self::create($critical, ...$access); + } + + protected function valueASN1(): Element + { + $elements = array_map(static fn (AccessDescription $access) => $access->toASN1(), $this->accessDescriptions); + return Sequence::create(...$elements); + } +} diff --git a/3rdparty/spomky-labs/pki-framework/src/X509/Certificate/Extension/AuthorityKeyIdentifierExtension.php b/3rdparty/spomky-labs/pki-framework/src/X509/Certificate/Extension/AuthorityKeyIdentifierExtension.php new file mode 100644 index 00000000..9fa6dd6d --- /dev/null +++ b/3rdparty/spomky-labs/pki-framework/src/X509/Certificate/Extension/AuthorityKeyIdentifierExtension.php @@ -0,0 +1,163 @@ +keyIdentifier()); + } + + /** + * Whether key identifier is present. + */ + public function hasKeyIdentifier(): bool + { + return isset($this->keyIdentifier); + } + + /** + * Get key identifier. + */ + public function keyIdentifier(): string + { + if (! $this->hasKeyIdentifier()) { + throw new LogicException('keyIdentifier not set.'); + } + return $this->keyIdentifier; + } + + /** + * Whether issuer is present. + */ + public function hasIssuer(): bool + { + return isset($this->authorityCertIssuer); + } + + public function issuer(): GeneralNames + { + if (! $this->hasIssuer()) { + throw new LogicException('authorityCertIssuer not set.'); + } + return $this->authorityCertIssuer; + } + + /** + * Whether serial is present. + */ + public function hasSerial(): bool + { + return isset($this->authorityCertSerialNumber); + } + + /** + * Get serial number. + * + * @return string Base 10 integer string + */ + public function serial(): string + { + if (! $this->hasSerial()) { + throw new LogicException('authorityCertSerialNumber not set.'); + } + return $this->authorityCertSerialNumber; + } + + protected static function fromDER(string $data, bool $critical): static + { + $seq = UnspecifiedType::fromDER($data)->asSequence(); + $keyIdentifier = null; + $issuer = null; + $serial = null; + if ($seq->hasTagged(0)) { + $keyIdentifier = $seq->getTagged(0) + ->asImplicit(Element::TYPE_OCTET_STRING) + ->asOctetString() + ->string(); + } + if ($seq->hasTagged(1) || $seq->hasTagged(2)) { + if (! $seq->hasTagged(1) || ! $seq->hasTagged(2)) { + throw new UnexpectedValueException( + 'AuthorityKeyIdentifier must have both' . + ' authorityCertIssuer and authorityCertSerialNumber' . + ' present or both absent.' + ); + } + $issuer = GeneralNames::fromASN1($seq->getTagged(1)->asImplicit(Element::TYPE_SEQUENCE)->asSequence()); + $serial = $seq->getTagged(2) + ->asImplicit(Element::TYPE_INTEGER) + ->asInteger() + ->number(); + } + return self::create($critical, $keyIdentifier, $issuer, $serial); + } + + protected function valueASN1(): Element + { + $elements = []; + if (isset($this->keyIdentifier)) { + $elements[] = ImplicitlyTaggedType::create(0, OctetString::create($this->keyIdentifier)); + } + // if either issuer or serial is set, both must be set + if (isset($this->authorityCertIssuer) || + isset($this->authorityCertSerialNumber)) { + if (! isset($this->authorityCertIssuer, + $this->authorityCertSerialNumber)) { + throw new LogicException( + 'AuthorityKeyIdentifier must have both' . + ' authorityCertIssuer and authorityCertSerialNumber' . + ' present or both absent.' + ); + } + $elements[] = ImplicitlyTaggedType::create(1, $this->authorityCertIssuer->toASN1()); + $elements[] = ImplicitlyTaggedType::create(2, Integer::create($this->authorityCertSerialNumber)); + } + return Sequence::create(...$elements); + } +} diff --git a/3rdparty/spomky-labs/pki-framework/src/X509/Certificate/Extension/BasicConstraintsExtension.php b/3rdparty/spomky-labs/pki-framework/src/X509/Certificate/Extension/BasicConstraintsExtension.php new file mode 100644 index 00000000..1253c466 --- /dev/null +++ b/3rdparty/spomky-labs/pki-framework/src/X509/Certificate/Extension/BasicConstraintsExtension.php @@ -0,0 +1,95 @@ +ca; + } + + /** + * Whether path length is present. + */ + public function hasPathLen(): bool + { + return isset($this->pathLen); + } + + /** + * Get path length. + */ + public function pathLen(): int + { + if (! $this->hasPathLen()) { + throw new LogicException('pathLenConstraint not set.'); + } + return $this->pathLen; + } + + protected static function fromDER(string $data, bool $critical): static + { + $seq = UnspecifiedType::fromDER($data)->asSequence(); + $ca = false; + $path_len = null; + $idx = 0; + if ($seq->has($idx, Element::TYPE_BOOLEAN)) { + $ca = $seq->at($idx++) + ->asBoolean() + ->value(); + } + if ($seq->has($idx, Element::TYPE_INTEGER)) { + $path_len = $seq->at($idx) + ->asInteger() + ->intNumber(); + } + return self::create($critical, $ca, $path_len); + } + + protected function valueASN1(): Element + { + $elements = []; + if ($this->ca) { + $elements[] = Boolean::create(true); + } + if (isset($this->pathLen)) { + $elements[] = Integer::create($this->pathLen); + } + return Sequence::create(...$elements); + } +} diff --git a/3rdparty/spomky-labs/pki-framework/src/X509/Certificate/Extension/CRLDistributionPointsExtension.php b/3rdparty/spomky-labs/pki-framework/src/X509/Certificate/Extension/CRLDistributionPointsExtension.php new file mode 100644 index 00000000..77aee6dd --- /dev/null +++ b/3rdparty/spomky-labs/pki-framework/src/X509/Certificate/Extension/CRLDistributionPointsExtension.php @@ -0,0 +1,94 @@ +distributionPoints = $distributionPoints; + } + + public static function create(bool $critical, DistributionPoint ...$distribution_points): self + { + return new self(self::OID_CRL_DISTRIBUTION_POINTS, $critical, ...$distribution_points); + } + + /** + * Get distribution points. + * + * @return DistributionPoint[] + */ + public function distributionPoints(): array + { + return $this->distributionPoints; + } + + /** + * Get the number of distribution points. + * + * @see \Countable::count() + */ + public function count(): int + { + return count($this->distributionPoints); + } + + /** + * Get iterator for distribution points. + * + * @see \IteratorAggregate::getIterator() + */ + public function getIterator(): ArrayIterator + { + return new ArrayIterator($this->distributionPoints); + } + + protected static function fromDER(string $data, bool $critical): static + { + $dps = array_map( + static fn (UnspecifiedType $el) => DistributionPoint::fromASN1($el->asSequence()), + UnspecifiedType::fromDER($data)->asSequence()->elements() + ); + if (count($dps) === 0) { + throw new UnexpectedValueException('CRLDistributionPoints must have at least one DistributionPoint.'); + } + // late static bound, extended by Freshest CRL extension + return static::create($critical, ...$dps); + } + + protected function valueASN1(): Element + { + if (count($this->distributionPoints) === 0) { + throw new LogicException('No distribution points.'); + } + $elements = array_map(static fn (DistributionPoint $dp) => $dp->toASN1(), $this->distributionPoints); + return Sequence::create(...$elements); + } +} diff --git a/3rdparty/spomky-labs/pki-framework/src/X509/Certificate/Extension/CertificatePoliciesExtension.php b/3rdparty/spomky-labs/pki-framework/src/X509/Certificate/Extension/CertificatePoliciesExtension.php new file mode 100644 index 00000000..9112bc8e --- /dev/null +++ b/3rdparty/spomky-labs/pki-framework/src/X509/Certificate/Extension/CertificatePoliciesExtension.php @@ -0,0 +1,124 @@ +_policies = []; + foreach ($policies as $policy) { + $this->_policies[$policy->oid()] = $policy; + } + } + + public static function create(bool $critical, PolicyInformation ...$policies): self + { + return new self($critical, ...$policies); + } + + /** + * Check whether policy information by OID is present. + */ + public function has(string $oid): bool + { + return isset($this->_policies[$oid]); + } + + /** + * Get policy information by OID. + */ + public function get(string $oid): PolicyInformation + { + if (! $this->has($oid)) { + throw new LogicException("Not certificate policy by OID {$oid}."); + } + return $this->_policies[$oid]; + } + + /** + * Check whether anyPolicy is present. + */ + public function hasAnyPolicy(): bool + { + return $this->has(PolicyInformation::OID_ANY_POLICY); + } + + /** + * Get anyPolicy information. + */ + public function anyPolicy(): PolicyInformation + { + if (! $this->hasAnyPolicy()) { + throw new LogicException('No anyPolicy.'); + } + return $this->get(PolicyInformation::OID_ANY_POLICY); + } + + /** + * Get the number of policies. + * + * @see \Countable::count() + */ + public function count(): int + { + return count($this->_policies); + } + + /** + * Get iterator for policy information terms. + * + * @see \IteratorAggregate::getIterator() + */ + public function getIterator(): ArrayIterator + { + return new ArrayIterator($this->_policies); + } + + protected static function fromDER(string $data, bool $critical): static + { + $policies = array_map( + static fn (UnspecifiedType $el) => PolicyInformation::fromASN1($el->asSequence()), + UnspecifiedType::fromDER($data)->asSequence()->elements() + ); + if (count($policies) === 0) { + throw new UnexpectedValueException('certificatePolicies must contain at least one PolicyInformation.'); + } + return self::create($critical, ...$policies); + } + + protected function valueASN1(): Element + { + if (count($this->_policies) === 0) { + throw new LogicException('No policies.'); + } + $elements = array_map(static fn (PolicyInformation $pi) => $pi->toASN1(), array_values($this->_policies)); + return Sequence::create(...$elements); + } +} diff --git a/3rdparty/spomky-labs/pki-framework/src/X509/Certificate/Extension/CertificatePolicy/CPSQualifier.php b/3rdparty/spomky-labs/pki-framework/src/X509/Certificate/Extension/CertificatePolicy/CPSQualifier.php new file mode 100644 index 00000000..3cd94dd6 --- /dev/null +++ b/3rdparty/spomky-labs/pki-framework/src/X509/Certificate/Extension/CertificatePolicy/CPSQualifier.php @@ -0,0 +1,46 @@ +asString()->string()); + } + + public function uri(): string + { + return $this->uri; + } + + protected function qualifierASN1(): Element + { + return IA5String::create($this->uri); + } +} diff --git a/3rdparty/spomky-labs/pki-framework/src/X509/Certificate/Extension/CertificatePolicy/DisplayText.php b/3rdparty/spomky-labs/pki-framework/src/X509/Certificate/Extension/CertificatePolicy/DisplayText.php new file mode 100644 index 00000000..911a477f --- /dev/null +++ b/3rdparty/spomky-labs/pki-framework/src/X509/Certificate/Extension/CertificatePolicy/DisplayText.php @@ -0,0 +1,78 @@ +string(); + } + + public static function create(string $text, int $tag): self + { + return new self($text, $tag); + } + + /** + * Initialize from ASN.1. + */ + public static function fromASN1(StringType $el): self + { + return self::create($el->string(), $el->tag()); + } + + /** + * Initialize from a UTF-8 string. + */ + public static function fromString(string $str): self + { + return self::create($str, Element::TYPE_UTF8_STRING); + } + + /** + * Get the text. + */ + public function string(): string + { + return $this->text; + } + + /** + * Generate ASN.1 element. + */ + public function toASN1(): StringType + { + return match ($this->tag) { + Element::TYPE_IA5_STRING => IA5String::create($this->text), + Element::TYPE_VISIBLE_STRING => VisibleString::create($this->text), + Element::TYPE_BMP_STRING => BMPString::create($this->text), + Element::TYPE_UTF8_STRING => UTF8String::create($this->text), + default => throw new UnexpectedValueException('Type ' . Element::tagToName( + $this->tag + ) . ' not supported.'), + }; + } +} diff --git a/3rdparty/spomky-labs/pki-framework/src/X509/Certificate/Extension/CertificatePolicy/NoticeReference.php b/3rdparty/spomky-labs/pki-framework/src/X509/Certificate/Extension/CertificatePolicy/NoticeReference.php new file mode 100644 index 00000000..1a58962c --- /dev/null +++ b/3rdparty/spomky-labs/pki-framework/src/X509/Certificate/Extension/CertificatePolicy/NoticeReference.php @@ -0,0 +1,80 @@ +numbers = $numbers; + } + + public static function create(DisplayText $organization, int ...$numbers): self + { + return new self($organization, ...$numbers); + } + + /** + * Initialize from ASN.1. + */ + public static function fromASN1(Sequence $seq): self + { + $org = DisplayText::fromASN1($seq->at(0)->asString()); + $numbers = array_map( + static fn (UnspecifiedType $el) => $el->asInteger() + ->intNumber(), + $seq->at(1) + ->asSequence() + ->elements() + ); + return self::create($org, ...$numbers); + } + + /** + * Get reference organization. + */ + public function organization(): DisplayText + { + return $this->organization; + } + + /** + * Get reference numbers. + * + * @return int[] + */ + public function numbers(): array + { + return $this->numbers; + } + + /** + * Generate ASN.1 structure. + */ + public function toASN1(): Sequence + { + $org = $this->organization->toASN1(); + $nums = array_map(static fn ($number) => Integer::create($number), $this->numbers); + return Sequence::create($org, Sequence::create(...$nums)); + } +} diff --git a/3rdparty/spomky-labs/pki-framework/src/X509/Certificate/Extension/CertificatePolicy/PolicyInformation.php b/3rdparty/spomky-labs/pki-framework/src/X509/Certificate/Extension/CertificatePolicy/PolicyInformation.php new file mode 100644 index 00000000..0ca3d55a --- /dev/null +++ b/3rdparty/spomky-labs/pki-framework/src/X509/Certificate/Extension/CertificatePolicy/PolicyInformation.php @@ -0,0 +1,190 @@ +qualifiers = []; + foreach ($qualifiers as $qualifier) { + $this->qualifiers[$qualifier->oid()] = $qualifier; + } + } + + public static function create(string $oid, PolicyQualifierInfo ...$qualifiers): self + { + return new self($oid, ...$qualifiers); + } + + /** + * Initialize from ASN.1. + */ + public static function fromASN1(Sequence $seq): self + { + $oid = $seq->at(0) + ->asObjectIdentifier() + ->oid(); + $qualifiers = []; + if (count($seq) > 1) { + $qualifiers = array_map( + static fn (UnspecifiedType $el) => PolicyQualifierInfo::fromASN1($el->asSequence()), + $seq->at(1) + ->asSequence() + ->elements() + ); + } + return self::create($oid, ...$qualifiers); + } + + /** + * Get policy identifier. + */ + public function oid(): string + { + return $this->oid; + } + + /** + * Check whether this policy is anyPolicy. + */ + public function isAnyPolicy(): bool + { + return $this->oid === self::OID_ANY_POLICY; + } + + /** + * Get policy qualifiers. + * + * @return PolicyQualifierInfo[] + */ + public function qualifiers(): array + { + return array_values($this->qualifiers); + } + + /** + * Check whether qualifier is present. + */ + public function has(string $oid): bool + { + return isset($this->qualifiers[$oid]); + } + + /** + * Get qualifier by OID. + */ + public function get(string $oid): PolicyQualifierInfo + { + if (! $this->has($oid)) { + throw new LogicException("No {$oid} qualifier."); + } + return $this->qualifiers[$oid]; + } + + /** + * Check whether CPS qualifier is present. + */ + public function hasCPSQualifier(): bool + { + return $this->has(PolicyQualifierInfo::OID_CPS); + } + + /** + * Get CPS qualifier. + */ + public function CPSQualifier(): CPSQualifier + { + if (! $this->hasCPSQualifier()) { + throw new LogicException('CPS qualifier not set.'); + } + return $this->get(PolicyQualifierInfo::OID_CPS); + } + + /** + * Check whether user notice qualifier is present. + */ + public function hasUserNoticeQualifier(): bool + { + return $this->has(PolicyQualifierInfo::OID_UNOTICE); + } + + /** + * Get user notice qualifier. + */ + public function userNoticeQualifier(): UserNoticeQualifier + { + if (! $this->hasUserNoticeQualifier()) { + throw new LogicException('User notice qualifier not set.'); + } + return $this->get(PolicyQualifierInfo::OID_UNOTICE); + } + + /** + * Get ASN.1 structure. + */ + public function toASN1(): Sequence + { + $elements = [ObjectIdentifier::create($this->oid)]; + if (count($this->qualifiers) !== 0) { + $qualifiers = array_map( + static fn (PolicyQualifierInfo $pqi) => $pqi->toASN1(), + array_values($this->qualifiers) + ); + $elements[] = Sequence::create(...$qualifiers); + } + return Sequence::create(...$elements); + } + + /** + * Get number of qualifiers. + * + * @see \Countable::count() + */ + public function count(): int + { + return count($this->qualifiers); + } + + /** + * Get iterator for qualifiers. + * + * @see \IteratorAggregate::getIterator() + */ + public function getIterator(): ArrayIterator + { + return new ArrayIterator($this->qualifiers); + } +} diff --git a/3rdparty/spomky-labs/pki-framework/src/X509/Certificate/Extension/CertificatePolicy/PolicyQualifierInfo.php b/3rdparty/spomky-labs/pki-framework/src/X509/Certificate/Extension/CertificatePolicy/PolicyQualifierInfo.php new file mode 100644 index 00000000..240ad9db --- /dev/null +++ b/3rdparty/spomky-labs/pki-framework/src/X509/Certificate/Extension/CertificatePolicy/PolicyQualifierInfo.php @@ -0,0 +1,79 @@ +at(0) + ->asObjectIdentifier() + ->oid(); + return match ($oid) { + self::OID_CPS => CPSQualifier::fromQualifierASN1($seq->at(1)), + self::OID_UNOTICE => UserNoticeQualifier::fromQualifierASN1($seq->at(1)), + default => throw new UnexpectedValueException("Qualifier {$oid} not supported."), + }; + } + + /** + * Get qualifier identifier. + */ + public function oid(): string + { + return $this->oid; + } + + /** + * Generate ASN.1 structure. + */ + public function toASN1(): Sequence + { + return Sequence::create(ObjectIdentifier::create($this->oid), $this->qualifierASN1()); + } + + /** + * Generate ASN.1 for the 'qualifier' field. + */ + abstract protected function qualifierASN1(): Element; +} diff --git a/3rdparty/spomky-labs/pki-framework/src/X509/Certificate/Extension/CertificatePolicy/UserNoticeQualifier.php b/3rdparty/spomky-labs/pki-framework/src/X509/Certificate/Extension/CertificatePolicy/UserNoticeQualifier.php new file mode 100644 index 00000000..31240206 --- /dev/null +++ b/3rdparty/spomky-labs/pki-framework/src/X509/Certificate/Extension/CertificatePolicy/UserNoticeQualifier.php @@ -0,0 +1,98 @@ +asSequence(); + $ref = null; + $text = null; + $idx = 0; + if ($seq->has($idx, Element::TYPE_SEQUENCE)) { + $ref = NoticeReference::fromASN1($seq->at($idx++)->asSequence()); + } + if ($seq->has($idx, Element::TYPE_STRING)) { + $text = DisplayText::fromASN1($seq->at($idx)->asString()); + } + return self::create($text, $ref); + } + + /** + * Whether explicit text is present. + */ + public function hasExplicitText(): bool + { + return isset($this->text); + } + + /** + * Get explicit text. + */ + public function explicitText(): DisplayText + { + if (! $this->hasExplicitText()) { + throw new LogicException('explicitText not set.'); + } + return $this->text; + } + + /** + * Whether notice reference is present. + */ + public function hasNoticeRef(): bool + { + return isset($this->ref); + } + + /** + * Get notice reference. + */ + public function noticeRef(): NoticeReference + { + if (! $this->hasNoticeRef()) { + throw new LogicException('noticeRef not set.'); + } + return $this->ref; + } + + protected function qualifierASN1(): Element + { + $elements = []; + if (isset($this->ref)) { + $elements[] = $this->ref->toASN1(); + } + if (isset($this->text)) { + $elements[] = $this->text->toASN1(); + } + return Sequence::create(...$elements); + } +} diff --git a/3rdparty/spomky-labs/pki-framework/src/X509/Certificate/Extension/DistributionPoint/DistributionPoint.php b/3rdparty/spomky-labs/pki-framework/src/X509/Certificate/Extension/DistributionPoint/DistributionPoint.php new file mode 100644 index 00000000..d9475f98 --- /dev/null +++ b/3rdparty/spomky-labs/pki-framework/src/X509/Certificate/Extension/DistributionPoint/DistributionPoint.php @@ -0,0 +1,181 @@ +hasTagged(0)) { + // promoted to explicit tagging because underlying type is CHOICE + $name = DistributionPointName::fromTaggedType($seq->getTagged(0)->asExplicit()->asTagged()); + } + if ($seq->hasTagged(1)) { + $reasons = ReasonFlags::fromASN1( + $seq->getTagged(1) + ->asImplicit(Element::TYPE_BIT_STRING) + ->asBitString() + ); + } + if ($seq->hasTagged(2)) { + $issuer = GeneralNames::fromASN1( + $seq->getTagged(2) + ->asImplicit(Element::TYPE_SEQUENCE) + ->asSequence() + ); + } + return self::create($name, $reasons, $issuer); + } + + /** + * Check whether distribution point name is set. + */ + public function hasDistributionPointName(): bool + { + return isset($this->distributionPoint); + } + + /** + * Get distribution point name. + */ + public function distributionPointName(): DistributionPointName + { + if (! $this->hasDistributionPointName()) { + throw new LogicException('distributionPoint not set.'); + } + return $this->distributionPoint; + } + + /** + * Check whether distribution point name is set, and it's a full name. + */ + public function hasFullName(): bool + { + return $this->distributionPointName() + ->tag() === + DistributionPointName::TAG_FULL_NAME; + } + + /** + * Get full distribution point name. + */ + public function fullName(): FullName + { + if (! $this->distributionPoint instanceof FullName || ! $this->hasFullName()) { + throw new LogicException('fullName not set.'); + } + return $this->distributionPoint; + } + + /** + * Check whether distribution point name is set and it's a relative name. + */ + public function hasRelativeName(): bool + { + return $this->distributionPointName() + ->tag() === + DistributionPointName::TAG_RDN; + } + + /** + * Get relative distribution point name. + */ + public function relativeName(): RelativeName + { + if (! $this->distributionPoint instanceof RelativeName || ! $this->hasRelativeName()) { + throw new LogicException('nameRelativeToCRLIssuer not set.'); + } + return $this->distributionPoint; + } + + /** + * Check whether reasons flags is set. + */ + public function hasReasons(): bool + { + return isset($this->reasons); + } + + /** + * Get revocation reason flags. + */ + public function reasons(): ReasonFlags + { + if (! $this->hasReasons()) { + throw new LogicException('reasons not set.'); + } + return $this->reasons; + } + + /** + * Check whether cRLIssuer is set. + */ + public function hasCRLIssuer(): bool + { + return isset($this->issuer); + } + + /** + * Get CRL issuer. + */ + public function crlIssuer(): GeneralNames + { + if (! $this->hasCRLIssuer()) { + throw new LogicException('crlIssuer not set.'); + } + return $this->issuer; + } + + /** + * Generate ASN.1 structure. + */ + public function toASN1(): Sequence + { + $elements = []; + if (isset($this->distributionPoint)) { + $elements[] = ExplicitlyTaggedType::create(0, $this->distributionPoint->toASN1()); + } + if (isset($this->reasons)) { + $elements[] = ImplicitlyTaggedType::create(1, $this->reasons->toASN1()); + } + if (isset($this->issuer)) { + $elements[] = ImplicitlyTaggedType::create(2, $this->issuer->toASN1()); + } + return Sequence::create(...$elements); + } +} diff --git a/3rdparty/spomky-labs/pki-framework/src/X509/Certificate/Extension/DistributionPoint/DistributionPointName.php b/3rdparty/spomky-labs/pki-framework/src/X509/Certificate/Extension/DistributionPoint/DistributionPointName.php new file mode 100644 index 00000000..633333a9 --- /dev/null +++ b/3rdparty/spomky-labs/pki-framework/src/X509/Certificate/Extension/DistributionPoint/DistributionPointName.php @@ -0,0 +1,66 @@ +tag()) { + self::TAG_FULL_NAME => FullName::create(GeneralNames::fromASN1( + $el->asImplicit(Element::TYPE_SEQUENCE)->asSequence() + )), + self::TAG_RDN => RelativeName::create(RDN::fromASN1($el->asImplicit(Element::TYPE_SET)->asSet())), + default => throw new UnexpectedValueException( + 'DistributionPointName tag ' . $el->tag() . ' not supported.' + ), + }; + } + + /** + * Get type tag. + */ + public function tag(): int + { + return $this->tag; + } + + /** + * Generate ASN.1 structure. + */ + public function toASN1(): ImplicitlyTaggedType + { + return ImplicitlyTaggedType::create($this->tag, $this->_valueASN1()); + } + + /** + * Generate ASN.1 element. + */ + abstract protected function _valueASN1(): Element; +} diff --git a/3rdparty/spomky-labs/pki-framework/src/X509/Certificate/Extension/DistributionPoint/FullName.php b/3rdparty/spomky-labs/pki-framework/src/X509/Certificate/Extension/DistributionPoint/FullName.php new file mode 100644 index 00000000..a315737c --- /dev/null +++ b/3rdparty/spomky-labs/pki-framework/src/X509/Certificate/Extension/DistributionPoint/FullName.php @@ -0,0 +1,47 @@ +names; + } + + protected function _valueASN1(): Element + { + return $this->names->toASN1(); + } +} diff --git a/3rdparty/spomky-labs/pki-framework/src/X509/Certificate/Extension/DistributionPoint/ReasonFlags.php b/3rdparty/spomky-labs/pki-framework/src/X509/Certificate/Extension/DistributionPoint/ReasonFlags.php new file mode 100644 index 00000000..970b61c6 --- /dev/null +++ b/3rdparty/spomky-labs/pki-framework/src/X509/Certificate/Extension/DistributionPoint/ReasonFlags.php @@ -0,0 +1,133 @@ +intNumber()); + } + + /** + * Check whether keyCompromise flag is set. + */ + public function isKeyCompromise(): bool + { + return $this->flagSet(self::KEY_COMPROMISE); + } + + /** + * Check whether cACompromise flag is set. + */ + public function isCACompromise(): bool + { + return $this->flagSet(self::CA_COMPROMISE); + } + + /** + * Check whether affiliationChanged flag is set. + */ + public function isAffiliationChanged(): bool + { + return $this->flagSet(self::AFFILIATION_CHANGED); + } + + /** + * Check whether superseded flag is set. + */ + public function isSuperseded(): bool + { + return $this->flagSet(self::SUPERSEDED); + } + + /** + * Check whether cessationOfOperation flag is set. + */ + public function isCessationOfOperation(): bool + { + return $this->flagSet(self::CESSATION_OF_OPERATION); + } + + /** + * Check whether certificateHold flag is set. + */ + public function isCertificateHold(): bool + { + return $this->flagSet(self::CERTIFICATE_HOLD); + } + + /** + * Check whether privilegeWithdrawn flag is set. + */ + public function isPrivilegeWithdrawn(): bool + { + return $this->flagSet(self::PRIVILEGE_WITHDRAWN); + } + + /** + * Check whether aACompromise flag is set. + */ + public function isAACompromise(): bool + { + return $this->flagSet(self::AA_COMPROMISE); + } + + /** + * Generate ASN.1 element. + */ + public function toASN1(): BitString + { + $flags = Flags::create($this->flags, 9); + return $flags->bitString() + ->withoutTrailingZeroes(); + } + + /** + * Check whether given flag is set. + */ + private function flagSet(int $flag): bool + { + return (bool) ($this->flags & $flag); + } +} diff --git a/3rdparty/spomky-labs/pki-framework/src/X509/Certificate/Extension/DistributionPoint/RelativeName.php b/3rdparty/spomky-labs/pki-framework/src/X509/Certificate/Extension/DistributionPoint/RelativeName.php new file mode 100644 index 00000000..5f0abe67 --- /dev/null +++ b/3rdparty/spomky-labs/pki-framework/src/X509/Certificate/Extension/DistributionPoint/RelativeName.php @@ -0,0 +1,38 @@ +rdn; + } + + protected function _valueASN1(): Element + { + return $this->rdn->toASN1(); + } +} diff --git a/3rdparty/spomky-labs/pki-framework/src/X509/Certificate/Extension/ExtendedKeyUsageExtension.php b/3rdparty/spomky-labs/pki-framework/src/X509/Certificate/Extension/ExtendedKeyUsageExtension.php new file mode 100644 index 00000000..f77cd069 --- /dev/null +++ b/3rdparty/spomky-labs/pki-framework/src/X509/Certificate/Extension/ExtendedKeyUsageExtension.php @@ -0,0 +1,160 @@ +purposes = $purposes; + } + + public static function create(bool $critical, string ...$purposes): self + { + return new self($critical, ...$purposes); + } + + /** + * Whether purposes are present. + * + * If multiple purposes are checked, all must be present. + */ + public function has(string ...$oids): bool + { + foreach ($oids as $oid) { + if (! in_array($oid, $this->purposes, true)) { + return false; + } + } + return true; + } + + /** + * Get key usage purpose OID's. + * + * @return string[] + */ + public function purposes(): array + { + return $this->purposes; + } + + /** + * Get the number of purposes. + * + * @see \Countable::count() + */ + public function count(): int + { + return count($this->purposes); + } + + /** + * Get iterator for usage purposes. + * + * @see \IteratorAggregate::getIterator() + */ + public function getIterator(): ArrayIterator + { + return new ArrayIterator($this->purposes); + } + + protected static function fromDER(string $data, bool $critical): static + { + $purposes = array_map( + static fn (UnspecifiedType $el) => $el->asObjectIdentifier() + ->oid(), + UnspecifiedType::fromDER($data)->asSequence()->elements() + ); + return self::create($critical, ...$purposes); + } + + protected function valueASN1(): Element + { + $elements = array_map(static fn ($oid) => ObjectIdentifier::create($oid), $this->purposes); + return Sequence::create(...$elements); + } +} diff --git a/3rdparty/spomky-labs/pki-framework/src/X509/Certificate/Extension/Extension.php b/3rdparty/spomky-labs/pki-framework/src/X509/Certificate/Extension/Extension.php new file mode 100644 index 00000000..da4a80c0 --- /dev/null +++ b/3rdparty/spomky-labs/pki-framework/src/X509/Certificate/Extension/Extension.php @@ -0,0 +1,327 @@ + + */ + private const MAP_OID_TO_CLASS = [ + self::OID_AUTHORITY_KEY_IDENTIFIER => AuthorityKeyIdentifierExtension::class, + self::OID_SUBJECT_KEY_IDENTIFIER => SubjectKeyIdentifierExtension::class, + self::OID_KEY_USAGE => KeyUsageExtension::class, + self::OID_CERTIFICATE_POLICIES => CertificatePoliciesExtension::class, + self::OID_POLICY_MAPPINGS => PolicyMappingsExtension::class, + self::OID_SUBJECT_ALT_NAME => SubjectAlternativeNameExtension::class, + self::OID_ISSUER_ALT_NAME => IssuerAlternativeNameExtension::class, + self::OID_SUBJECT_DIRECTORY_ATTRIBUTES => SubjectDirectoryAttributesExtension::class, + self::OID_BASIC_CONSTRAINTS => BasicConstraintsExtension::class, + self::OID_NAME_CONSTRAINTS => NameConstraintsExtension::class, + self::OID_POLICY_CONSTRAINTS => PolicyConstraintsExtension::class, + self::OID_EXT_KEY_USAGE => ExtendedKeyUsageExtension::class, + self::OID_CRL_DISTRIBUTION_POINTS => CRLDistributionPointsExtension::class, + self::OID_INHIBIT_ANY_POLICY => InhibitAnyPolicyExtension::class, + self::OID_FRESHEST_CRL => FreshestCRLExtension::class, + self::OID_NO_REV_AVAIL => NoRevocationAvailableExtension::class, + self::OID_TARGET_INFORMATION => TargetInformationExtension::class, + self::OID_AUTHORITY_INFORMATION_ACCESS => AuthorityInformationAccessExtension::class, + self::OID_AA_CONTROLS => AAControlsExtension::class, + self::OID_SUBJECT_INFORMATION_ACCESS => SubjectInformationAccessExtension::class, + ]; + + /** + * Mapping from extensions ID to short name. + * + * @internal + * + * @var array + */ + private const MAP_OID_TO_NAME = [ + self::OID_AUTHORITY_KEY_IDENTIFIER => 'authorityKeyIdentifier', + self::OID_SUBJECT_KEY_IDENTIFIER => 'subjectKeyIdentifier', + self::OID_KEY_USAGE => 'keyUsage', + self::OID_PRIVATE_KEY_USAGE_PERIOD => 'privateKeyUsagePeriod', + self::OID_CERTIFICATE_POLICIES => 'certificatePolicies', + self::OID_POLICY_MAPPINGS => 'policyMappings', + self::OID_SUBJECT_ALT_NAME => 'subjectAltName', + self::OID_ISSUER_ALT_NAME => 'issuerAltName', + self::OID_SUBJECT_DIRECTORY_ATTRIBUTES => 'subjectDirectoryAttributes', + self::OID_BASIC_CONSTRAINTS => 'basicConstraints', + self::OID_NAME_CONSTRAINTS => 'nameConstraints', + self::OID_POLICY_CONSTRAINTS => 'policyConstraints', + self::OID_EXT_KEY_USAGE => 'extKeyUsage', + self::OID_CRL_DISTRIBUTION_POINTS => 'cRLDistributionPoints', + self::OID_INHIBIT_ANY_POLICY => 'inhibitAnyPolicy', + self::OID_FRESHEST_CRL => 'freshestCRL', + self::OID_NO_REV_AVAIL => 'noRevAvail', + self::OID_TARGET_INFORMATION => 'targetInformation', + self::OID_AUTHORITY_INFORMATION_ACCESS => 'authorityInfoAccess', + self::OID_AA_CONTROLS => 'aaControls', + self::OID_SUBJECT_INFORMATION_ACCESS => 'subjectInfoAccess', + self::OID_LOGOTYPE => 'logotype', + ]; + + /** + * @param string $oid Extension OID + * @param bool $critical Whether extension is critical + */ + protected function __construct( + private readonly string $oid, + private readonly bool $critical + ) { + } + + public function __toString(): string + { + return $this->extensionName(); + } + + /** + * Initialize from ASN.1. + */ + public static function fromASN1(Sequence $seq): self + { + $idx = 0; + $extnID = $seq->at($idx++) + ->asObjectIdentifier() + ->oid(); + $critical = false; + if ($seq->has($idx, Element::TYPE_BOOLEAN)) { + $critical = $seq->at($idx++) + ->asBoolean() + ->value(); + } + $data = $seq->at($idx) + ->asOctetString() + ->string(); + if (array_key_exists($extnID, self::MAP_OID_TO_CLASS)) { + $cls = self::MAP_OID_TO_CLASS[$extnID]; + return $cls::fromDER($data, $critical); + } + return UnknownExtension::fromRawString($extnID, $critical, $data); + } + + /** + * Get extension OID. + */ + public function oid(): string + { + return $this->oid; + } + + /** + * Check whether extension is critical. + */ + public function isCritical(): bool + { + return $this->critical; + } + + /** + * Generate ASN.1 structure. + */ + public function toASN1(): Sequence + { + $elements = [ObjectIdentifier::create($this->oid)]; + if ($this->critical) { + $elements[] = Boolean::create(true); + } + $elements[] = $this->extnValue(); + return Sequence::create(...$elements); + } + + /** + * Get short name of the extension. + */ + public function extensionName(): string + { + if (array_key_exists($this->oid, self::MAP_OID_TO_NAME)) { + return self::MAP_OID_TO_NAME[$this->oid]; + } + return $this->oid(); + } + + /** + * Get ASN.1 structure of the extension value. + */ + abstract protected function valueASN1(): Element; + + /** + * Parse extension value from DER. + * + * @param string $data DER data + * @param bool $critical Whether extension is critical + */ + abstract protected static function fromDER(string $data, bool $critical): static; + + /** + * Get the extnValue element. + */ + protected function extnValue(): OctetString + { + return OctetString::create($this->valueASN1()->toDER()); + } +} diff --git a/3rdparty/spomky-labs/pki-framework/src/X509/Certificate/Extension/FreshestCRLExtension.php b/3rdparty/spomky-labs/pki-framework/src/X509/Certificate/Extension/FreshestCRLExtension.php new file mode 100644 index 00000000..82ed6ec5 --- /dev/null +++ b/3rdparty/spomky-labs/pki-framework/src/X509/Certificate/Extension/FreshestCRLExtension.php @@ -0,0 +1,20 @@ +skipCerts; + } + + protected static function fromDER(string $data, bool $critical): static + { + return self::create($critical, UnspecifiedType::fromDER($data)->asInteger()->intNumber()); + } + + protected function valueASN1(): Element + { + return Integer::create($this->skipCerts); + } +} diff --git a/3rdparty/spomky-labs/pki-framework/src/X509/Certificate/Extension/IssuerAlternativeNameExtension.php b/3rdparty/spomky-labs/pki-framework/src/X509/Certificate/Extension/IssuerAlternativeNameExtension.php new file mode 100644 index 00000000..57478403 --- /dev/null +++ b/3rdparty/spomky-labs/pki-framework/src/X509/Certificate/Extension/IssuerAlternativeNameExtension.php @@ -0,0 +1,44 @@ +names; + } + + protected static function fromDER(string $data, bool $critical): static + { + return self::create($critical, GeneralNames::fromASN1(UnspecifiedType::fromDER($data)->asSequence())); + } + + protected function valueASN1(): Element + { + return $this->names->toASN1(); + } +} diff --git a/3rdparty/spomky-labs/pki-framework/src/X509/Certificate/Extension/KeyUsageExtension.php b/3rdparty/spomky-labs/pki-framework/src/X509/Certificate/Extension/KeyUsageExtension.php new file mode 100644 index 00000000..d6b0356b --- /dev/null +++ b/3rdparty/spomky-labs/pki-framework/src/X509/Certificate/Extension/KeyUsageExtension.php @@ -0,0 +1,142 @@ +_flagSet(self::DIGITAL_SIGNATURE); + } + + /** + * Check whether nonRepudiation/contentCommitment flag is set. + */ + public function isNonRepudiation(): bool + { + return $this->_flagSet(self::NON_REPUDIATION); + } + + /** + * Check whether keyEncipherment flag is set. + */ + public function isKeyEncipherment(): bool + { + return $this->_flagSet(self::KEY_ENCIPHERMENT); + } + + /** + * Check whether dataEncipherment flag is set. + */ + public function isDataEncipherment(): bool + { + return $this->_flagSet(self::DATA_ENCIPHERMENT); + } + + /** + * Check whether keyAgreement flag is set. + */ + public function isKeyAgreement(): bool + { + return $this->_flagSet(self::KEY_AGREEMENT); + } + + /** + * Check whether keyCertSign flag is set. + */ + public function isKeyCertSign(): bool + { + return $this->_flagSet(self::KEY_CERT_SIGN); + } + + /** + * Check whether cRLSign flag is set. + */ + public function isCRLSign(): bool + { + return $this->_flagSet(self::CRL_SIGN); + } + + /** + * Check whether encipherOnly flag is set. + */ + public function isEncipherOnly(): bool + { + return $this->_flagSet(self::ENCIPHER_ONLY); + } + + /** + * Check whether decipherOnly flag is set. + */ + public function isDecipherOnly(): bool + { + return $this->_flagSet(self::DECIPHER_ONLY); + } + + /** + * Check whether given flag is set. + */ + protected function _flagSet(int $flag): bool + { + return (bool) ($this->keyUsage & $flag); + } + + protected static function fromDER(string $data, bool $critical): static + { + return self::create( + $critical, + Flags::fromBitString(UnspecifiedType::fromDER($data)->asBitString(), 9)->intNumber() + ); + } + + protected function valueASN1(): Element + { + $flags = Flags::create($this->keyUsage, 9); + return $flags->bitString() + ->withoutTrailingZeroes(); + } +} diff --git a/3rdparty/spomky-labs/pki-framework/src/X509/Certificate/Extension/NameConstraints/GeneralSubtree.php b/3rdparty/spomky-labs/pki-framework/src/X509/Certificate/Extension/NameConstraints/GeneralSubtree.php new file mode 100644 index 00000000..e0117a48 --- /dev/null +++ b/3rdparty/spomky-labs/pki-framework/src/X509/Certificate/Extension/NameConstraints/GeneralSubtree.php @@ -0,0 +1,98 @@ +base; + } + + public function getMin(): int + { + return $this->min; + } + + public function getMax(): ?int + { + return $this->max; + } + + /** + * Initialize from ASN.1. + */ + public static function fromASN1(Sequence $seq): self + { + $base = GeneralName::fromASN1($seq->at(0)->asTagged()); + $min = 0; + $max = null; + // GeneralName is a CHOICE, which may be tagged as otherName [0] + // or rfc822Name [1]. As minimum and maximum are also implicitly tagged, + // we have to iterate the remaining elements instead of just checking + // for tagged types. + for ($i = 1; $i < count($seq); ++$i) { + $el = $seq->at($i) + ->expectTagged(); + switch ($el->tag()) { + case 0: + $min = $el->asImplicit(Element::TYPE_INTEGER) + ->asInteger() + ->intNumber(); + break; + case 1: + $max = $el->asImplicit(Element::TYPE_INTEGER) + ->asInteger() + ->intNumber(); + break; + } + } + return self::create($base, $min, $max); + } + + public function base(): GeneralName + { + return $this->base; + } + + /** + * Generate ASN.1 structure. + */ + public function toASN1(): Sequence + { + $elements = [$this->base->toASN1()]; + if (isset($this->min) && $this->min !== 0) { + $elements[] = ImplicitlyTaggedType::create(0, Integer::create($this->min)); + } + if (isset($this->max)) { + $elements[] = ImplicitlyTaggedType::create(1, Integer::create($this->max)); + } + return Sequence::create(...$elements); + } +} diff --git a/3rdparty/spomky-labs/pki-framework/src/X509/Certificate/Extension/NameConstraints/GeneralSubtrees.php b/3rdparty/spomky-labs/pki-framework/src/X509/Certificate/Extension/NameConstraints/GeneralSubtrees.php new file mode 100644 index 00000000..a26908b4 --- /dev/null +++ b/3rdparty/spomky-labs/pki-framework/src/X509/Certificate/Extension/NameConstraints/GeneralSubtrees.php @@ -0,0 +1,94 @@ +subtrees = $subtrees; + } + + public static function create(GeneralSubtree ...$subtrees): self + { + return new self(...$subtrees); + } + + /** + * Initialize from ASN.1. + */ + public static function fromASN1(Sequence $seq): self + { + $subtrees = array_map( + static fn (UnspecifiedType $el) => GeneralSubtree::fromASN1($el->asSequence()), + $seq->elements() + ); + if (count($subtrees) === 0) { + throw new UnexpectedValueException('GeneralSubtrees must contain at least one GeneralSubtree.'); + } + return self::create(...$subtrees); + } + + /** + * Get all subtrees. + * + * @return GeneralSubtree[] + */ + public function all(): array + { + return $this->subtrees; + } + + /** + * Generate ASN.1 structure. + */ + public function toASN1(): Sequence + { + if (count($this->subtrees) === 0) { + throw new LogicException('No subtrees.'); + } + $elements = array_map(static fn (GeneralSubtree $gs) => $gs->toASN1(), $this->subtrees); + return Sequence::create(...$elements); + } + + /** + * @see \Countable::count() + */ + public function count(): int + { + return count($this->subtrees); + } + + /** + * Get iterator for subtrees. + * + * @see \IteratorAggregate::getIterator() + */ + public function getIterator(): ArrayIterator + { + return new ArrayIterator($this->subtrees); + } +} diff --git a/3rdparty/spomky-labs/pki-framework/src/X509/Certificate/Extension/NameConstraintsExtension.php b/3rdparty/spomky-labs/pki-framework/src/X509/Certificate/Extension/NameConstraintsExtension.php new file mode 100644 index 00000000..279a4012 --- /dev/null +++ b/3rdparty/spomky-labs/pki-framework/src/X509/Certificate/Extension/NameConstraintsExtension.php @@ -0,0 +1,106 @@ +permitted); + } + + /** + * Get permitted subtrees. + */ + public function permittedSubtrees(): GeneralSubtrees + { + if (! $this->hasPermittedSubtrees()) { + throw new LogicException('No permitted subtrees.'); + } + return $this->permitted; + } + + /** + * Whether excluded subtrees are present. + */ + public function hasExcludedSubtrees(): bool + { + return isset($this->excluded); + } + + /** + * Get excluded subtrees. + */ + public function excludedSubtrees(): GeneralSubtrees + { + if (! $this->hasExcludedSubtrees()) { + throw new LogicException('No excluded subtrees.'); + } + return $this->excluded; + } + + protected static function fromDER(string $data, bool $critical): static + { + $seq = UnspecifiedType::fromDER($data)->asSequence(); + $permitted = null; + $excluded = null; + if ($seq->hasTagged(0)) { + $permitted = GeneralSubtrees::fromASN1( + $seq->getTagged(0) + ->asImplicit(Element::TYPE_SEQUENCE)->asSequence() + ); + } + if ($seq->hasTagged(1)) { + $excluded = GeneralSubtrees::fromASN1( + $seq->getTagged(1) + ->asImplicit(Element::TYPE_SEQUENCE)->asSequence() + ); + } + return self::create($critical, $permitted, $excluded); + } + + protected function valueASN1(): Element + { + $elements = []; + if (isset($this->permitted)) { + $elements[] = ImplicitlyTaggedType::create(0, $this->permitted->toASN1()); + } + if (isset($this->excluded)) { + $elements[] = ImplicitlyTaggedType::create(1, $this->excluded->toASN1()); + } + return Sequence::create(...$elements); + } +} diff --git a/3rdparty/spomky-labs/pki-framework/src/X509/Certificate/Extension/NoRevocationAvailableExtension.php b/3rdparty/spomky-labs/pki-framework/src/X509/Certificate/Extension/NoRevocationAvailableExtension.php new file mode 100644 index 00000000..aa3d0ad5 --- /dev/null +++ b/3rdparty/spomky-labs/pki-framework/src/X509/Certificate/Extension/NoRevocationAvailableExtension.php @@ -0,0 +1,37 @@ +requireExplicitPolicy); + } + + public function requireExplicitPolicy(): int + { + if (! $this->hasRequireExplicitPolicy()) { + throw new LogicException('requireExplicitPolicy not set.'); + } + return $this->requireExplicitPolicy; + } + + /** + * Whether inhibitPolicyMapping is present. + */ + public function hasInhibitPolicyMapping(): bool + { + return isset($this->inhibitPolicyMapping); + } + + public function inhibitPolicyMapping(): int + { + if (! $this->hasInhibitPolicyMapping()) { + throw new LogicException('inhibitPolicyMapping not set.'); + } + return $this->inhibitPolicyMapping; + } + + protected static function fromDER(string $data, bool $critical): static + { + $seq = UnspecifiedType::fromDER($data)->asSequence(); + $require_explicit_policy = null; + $inhibit_policy_mapping = null; + if ($seq->hasTagged(0)) { + $require_explicit_policy = $seq->getTagged(0) + ->asImplicit(Element::TYPE_INTEGER)->asInteger()->intNumber(); + } + if ($seq->hasTagged(1)) { + $inhibit_policy_mapping = $seq->getTagged(1) + ->asImplicit(Element::TYPE_INTEGER)->asInteger()->intNumber(); + } + return self::create($critical, $require_explicit_policy, $inhibit_policy_mapping); + } + + protected function valueASN1(): Element + { + $elements = []; + if (isset($this->requireExplicitPolicy)) { + $elements[] = ImplicitlyTaggedType::create(0, Integer::create($this->requireExplicitPolicy)); + } + if (isset($this->inhibitPolicyMapping)) { + $elements[] = ImplicitlyTaggedType::create(1, Integer::create($this->inhibitPolicyMapping)); + } + return Sequence::create(...$elements); + } +} diff --git a/3rdparty/spomky-labs/pki-framework/src/X509/Certificate/Extension/PolicyMappings/PolicyMapping.php b/3rdparty/spomky-labs/pki-framework/src/X509/Certificate/Extension/PolicyMappings/PolicyMapping.php new file mode 100644 index 00000000..71750399 --- /dev/null +++ b/3rdparty/spomky-labs/pki-framework/src/X509/Certificate/Extension/PolicyMappings/PolicyMapping.php @@ -0,0 +1,76 @@ +at(0) + ->asObjectIdentifier() + ->oid(); + $subject_policy = $seq->at(1) + ->asObjectIdentifier() + ->oid(); + return self::create($issuer_policy, $subject_policy); + } + + /** + * Get issuer domain policy. + * + * @return string OID in dotted format + */ + public function issuerDomainPolicy(): string + { + return $this->issuerDomainPolicy; + } + + /** + * Get subject domain policy. + * + * @return string OID in dotted format + */ + public function subjectDomainPolicy(): string + { + return $this->subjectDomainPolicy; + } + + /** + * Generate ASN.1 structure. + */ + public function toASN1(): Sequence + { + return Sequence::create( + ObjectIdentifier::create($this->issuerDomainPolicy), + ObjectIdentifier::create($this->subjectDomainPolicy) + ); + } +} diff --git a/3rdparty/spomky-labs/pki-framework/src/X509/Certificate/Extension/PolicyMappingsExtension.php b/3rdparty/spomky-labs/pki-framework/src/X509/Certificate/Extension/PolicyMappingsExtension.php new file mode 100644 index 00000000..ec142aa9 --- /dev/null +++ b/3rdparty/spomky-labs/pki-framework/src/X509/Certificate/Extension/PolicyMappingsExtension.php @@ -0,0 +1,165 @@ +mappings = $mappings; + } + + public static function create(bool $critical, PolicyMapping ...$mappings): self + { + return new self($critical, ...$mappings); + } + + /** + * Get all mappings. + * + * @return PolicyMapping[] + */ + public function mappings(): array + { + return $this->mappings; + } + + /** + * Get mappings flattened into a single array of arrays of subject domains keyed by issuer domain. + * + * Eg. if policy mappings contains multiple mappings with the same issuer domain policy, their corresponding subject + * domain policies are placed under the same key. + * + * @return (string[])[] + */ + public function flattenedMappings(): array + { + $mappings = []; + foreach ($this->mappings as $mapping) { + $idp = $mapping->issuerDomainPolicy(); + if (! isset($mappings[$idp])) { + $mappings[$idp] = []; + } + array_push($mappings[$idp], $mapping->subjectDomainPolicy()); + } + return $mappings; + } + + /** + * Get all subject domain policy OIDs that are mapped to given issuer domain policy OID. + * + * @param string $oid Issuer domain policy + * + * @return string[] List of OIDs in dotted format + */ + public function issuerMappings(string $oid): array + { + $oids = []; + foreach ($this->mappings as $mapping) { + if ($mapping->issuerDomainPolicy() === $oid) { + $oids[] = $mapping->subjectDomainPolicy(); + } + } + return $oids; + } + + /** + * Get all mapped issuer domain policy OIDs. + * + * @return string[] + */ + public function issuerDomainPolicies(): array + { + $idps = array_map(static fn (PolicyMapping $mapping) => $mapping->issuerDomainPolicy(), $this->mappings); + return array_values(array_unique($idps)); + } + + /** + * Check whether policy mappings have anyPolicy mapped. + * + * RFC 5280 section 4.2.1.5 states that "Policies MUST NOT be mapped either to or from the special value anyPolicy". + */ + public function hasAnyPolicyMapping(): bool + { + foreach ($this->mappings as $mapping) { + if ($mapping->issuerDomainPolicy() === PolicyInformation::OID_ANY_POLICY) { + return true; + } + if ($mapping->subjectDomainPolicy() === PolicyInformation::OID_ANY_POLICY) { + return true; + } + } + return false; + } + + /** + * Get the number of mappings. + * + * @see \Countable::count() + */ + public function count(): int + { + return count($this->mappings); + } + + /** + * Get iterator for policy mappings. + * + * @see \IteratorAggregate::getIterator() + */ + public function getIterator(): ArrayIterator + { + return new ArrayIterator($this->mappings); + } + + protected static function fromDER(string $data, bool $critical): static + { + $mappings = array_map( + static fn (UnspecifiedType $el) => PolicyMapping::fromASN1($el->asSequence()), + UnspecifiedType::fromDER($data)->asSequence()->elements() + ); + if (count($mappings) === 0) { + throw new UnexpectedValueException('PolicyMappings must have at least one mapping.'); + } + return self::create($critical, ...$mappings); + } + + protected function valueASN1(): Element + { + if (count($this->mappings) === 0) { + throw new LogicException('No mappings.'); + } + $elements = array_map(static fn (PolicyMapping $mapping) => $mapping->toASN1(), $this->mappings); + return Sequence::create(...$elements); + } +} diff --git a/3rdparty/spomky-labs/pki-framework/src/X509/Certificate/Extension/SubjectAlternativeNameExtension.php b/3rdparty/spomky-labs/pki-framework/src/X509/Certificate/Extension/SubjectAlternativeNameExtension.php new file mode 100644 index 00000000..7b411404 --- /dev/null +++ b/3rdparty/spomky-labs/pki-framework/src/X509/Certificate/Extension/SubjectAlternativeNameExtension.php @@ -0,0 +1,44 @@ +names; + } + + protected static function fromDER(string $data, bool $critical): static + { + return self::create($critical, GeneralNames::fromASN1(UnspecifiedType::fromDER($data)->asSequence())); + } + + protected function valueASN1(): Element + { + return $this->names->toASN1(); + } +} diff --git a/3rdparty/spomky-labs/pki-framework/src/X509/Certificate/Extension/SubjectDirectoryAttributesExtension.php b/3rdparty/spomky-labs/pki-framework/src/X509/Certificate/Extension/SubjectDirectoryAttributesExtension.php new file mode 100644 index 00000000..51f8f405 --- /dev/null +++ b/3rdparty/spomky-labs/pki-framework/src/X509/Certificate/Extension/SubjectDirectoryAttributesExtension.php @@ -0,0 +1,120 @@ +attributes = SequenceOfAttributes::create(...$attribs); + } + + public static function create(bool $critical, Attribute ...$attribs): self + { + return new self($critical, ...$attribs); + } + + /** + * Check whether attribute is present. + * + * @param string $name OID or attribute name + */ + public function has(string $name): bool + { + return $this->attributes->has($name); + } + + /** + * Get first attribute by OID or attribute name. + * + * @param string $name OID or attribute name + */ + public function firstOf(string $name): Attribute + { + return $this->attributes->firstOf($name); + } + + /** + * Get all attributes of given name. + * + * @param string $name OID or attribute name + * + * @return Attribute[] + */ + public function allOf(string $name): array + { + return $this->attributes->allOf($name); + } + + /** + * Get all attributes. + * + * @return Attribute[] + */ + public function all(): array + { + return $this->attributes->all(); + } + + /** + * Get number of attributes. + */ + public function count(): int + { + return count($this->attributes); + } + + /** + * Get iterator for attributes. + * + * @return ArrayIterator|Attribute[] + */ + public function getIterator(): ArrayIterator + { + return $this->attributes->getIterator(); + } + + protected static function fromDER(string $data, bool $critical): static + { + $attribs = SequenceOfAttributes::fromASN1(UnspecifiedType::fromDER($data)->asSequence()); + if (count($attribs) === 0) { + throw new UnexpectedValueException('SubjectDirectoryAttributes must have at least one Attribute.'); + } + return self::create($critical, ...$attribs->all()); + } + + protected function valueASN1(): Element + { + if (count($this->attributes) === 0) { + throw new LogicException('No attributes'); + } + return $this->attributes->toASN1(); + } +} diff --git a/3rdparty/spomky-labs/pki-framework/src/X509/Certificate/Extension/SubjectInformationAccessExtension.php b/3rdparty/spomky-labs/pki-framework/src/X509/Certificate/Extension/SubjectInformationAccessExtension.php new file mode 100644 index 00000000..a000bef3 --- /dev/null +++ b/3rdparty/spomky-labs/pki-framework/src/X509/Certificate/Extension/SubjectInformationAccessExtension.php @@ -0,0 +1,87 @@ +accessDescriptions = $accessDescriptions; + } + + public static function create(bool $critical, SubjectAccessDescription ...$accessDescriptions): self + { + return new self($critical, ...$accessDescriptions); + } + + /** + * Get the access descriptions. + * + * @return SubjectAccessDescription[] + */ + public function accessDescriptions(): array + { + return $this->accessDescriptions; + } + + /** + * Get the number of access descriptions. + * + * @see \Countable::count() + */ + public function count(): int + { + return count($this->accessDescriptions); + } + + /** + * Get iterator for access descriptions. + * + * @return ArrayIterator List of SubjectAccessDescription objects + * @see \IteratorAggregate::getIterator() + */ + public function getIterator(): ArrayIterator + { + return new ArrayIterator($this->accessDescriptions); + } + + protected static function fromDER(string $data, bool $critical): static + { + $access = array_map( + static fn (UnspecifiedType $el) => SubjectAccessDescription::fromASN1($el->asSequence()), + UnspecifiedType::fromDER($data)->asSequence()->elements() + ); + return self::create($critical, ...$access); + } + + protected function valueASN1(): Element + { + $elements = array_map(static fn (AccessDescription $access) => $access->toASN1(), $this->accessDescriptions); + return Sequence::create(...$elements); + } +} diff --git a/3rdparty/spomky-labs/pki-framework/src/X509/Certificate/Extension/SubjectKeyIdentifierExtension.php b/3rdparty/spomky-labs/pki-framework/src/X509/Certificate/Extension/SubjectKeyIdentifierExtension.php new file mode 100644 index 00000000..60235234 --- /dev/null +++ b/3rdparty/spomky-labs/pki-framework/src/X509/Certificate/Extension/SubjectKeyIdentifierExtension.php @@ -0,0 +1,47 @@ +keyIdentifier; + } + + protected static function fromDER(string $data, bool $critical): static + { + return self::create($critical, UnspecifiedType::fromDER($data)->asOctetString()->string()); + } + + protected function valueASN1(): Element + { + return OctetString::create($this->keyIdentifier); + } +} diff --git a/3rdparty/spomky-labs/pki-framework/src/X509/Certificate/Extension/Target/Target.php b/3rdparty/spomky-labs/pki-framework/src/X509/Certificate/Extension/Target/Target.php new file mode 100644 index 00000000..eb3f6af4 --- /dev/null +++ b/3rdparty/spomky-labs/pki-framework/src/X509/Certificate/Extension/Target/Target.php @@ -0,0 +1,79 @@ +tag()) { + self::TYPE_NAME => TargetName::fromChosenASN1($el->asExplicit()->asTagged()), + self::TYPE_GROUP => TargetGroup::fromChosenASN1($el->asExplicit()->asTagged()), + self::TYPE_CERT => throw new RuntimeException('targetCert not supported.'), + default => throw new UnexpectedValueException('Target type ' . $el->tag() . ' not supported.'), + }; + } + + /** + * Get type tag. + */ + public function type(): int + { + return $this->type; + } + + /** + * Check whether target is equal to another. + */ + public function equals(self $other): bool + { + if ($this->type !== $other->type) { + return false; + } + if ($this->toASN1()->toDER() !== $other->toASN1()->toDER()) { + return false; + } + return true; + } +} diff --git a/3rdparty/spomky-labs/pki-framework/src/X509/Certificate/Extension/Target/TargetGroup.php b/3rdparty/spomky-labs/pki-framework/src/X509/Certificate/Extension/Target/TargetGroup.php new file mode 100644 index 00000000..870203e1 --- /dev/null +++ b/3rdparty/spomky-labs/pki-framework/src/X509/Certificate/Extension/Target/TargetGroup.php @@ -0,0 +1,55 @@ +name->string(); + } + + /** + * Get group name. + */ + public function name(): GeneralName + { + return $this->name; + } + + public function toASN1(): Element + { + return ExplicitlyTaggedType::create($this->type, $this->name->toASN1()); + } +} diff --git a/3rdparty/spomky-labs/pki-framework/src/X509/Certificate/Extension/Target/TargetName.php b/3rdparty/spomky-labs/pki-framework/src/X509/Certificate/Extension/Target/TargetName.php new file mode 100644 index 00000000..ef9bfe4d --- /dev/null +++ b/3rdparty/spomky-labs/pki-framework/src/X509/Certificate/Extension/Target/TargetName.php @@ -0,0 +1,52 @@ +name->string(); + } + + public function name(): GeneralName + { + return $this->name; + } + + public function toASN1(): Element + { + return ExplicitlyTaggedType::create($this->type, $this->name->toASN1()); + } +} diff --git a/3rdparty/spomky-labs/pki-framework/src/X509/Certificate/Extension/Target/Targets.php b/3rdparty/spomky-labs/pki-framework/src/X509/Certificate/Extension/Target/Targets.php new file mode 100644 index 00000000..4961bb0b --- /dev/null +++ b/3rdparty/spomky-labs/pki-framework/src/X509/Certificate/Extension/Target/Targets.php @@ -0,0 +1,126 @@ +targets = $targets; + } + + public static function create(Target ...$targets): self + { + return new self(...$targets); + } + + /** + * Initialize from ASN.1. + */ + public static function fromASN1(Sequence $seq): self + { + $targets = array_map(static fn (UnspecifiedType $el) => Target::fromASN1($el->asTagged()), $seq->elements()); + return self::create(...$targets); + } + + /** + * Get all targets. + * + * @return Target[] + */ + public function all(): array + { + return $this->targets; + } + + /** + * Get all name targets. + * + * @return Target[] + */ + public function nameTargets(): array + { + return $this->allOfType(Target::TYPE_NAME); + } + + /** + * Get all group targets. + * + * @return Target[] + */ + public function groupTargets(): array + { + return $this->allOfType(Target::TYPE_GROUP); + } + + /** + * Check whether given target is present. + */ + public function hasTarget(Target $target): bool + { + foreach ($this->allOfType($target->type()) as $t) { + if ($target->equals($t)) { + return true; + } + } + return false; + } + + /** + * Generate ASN.1 structure. + */ + public function toASN1(): Sequence + { + $elements = array_map(static fn (Target $target) => $target->toASN1(), $this->targets); + return Sequence::create(...$elements); + } + + /** + * @see \Countable::count() + */ + public function count(): int + { + return count($this->targets); + } + + /** + * Get iterator for targets. + * + * @see \IteratorAggregate::getIterator() + */ + public function getIterator(): ArrayIterator + { + return new ArrayIterator($this->targets); + } + + /** + * Get all targets of given type. + * + * @return Target[] + */ + private function allOfType(int $type): array + { + return array_values(array_filter($this->targets, static fn (Target $target) => $target->type() === $type)); + } +} diff --git a/3rdparty/spomky-labs/pki-framework/src/X509/Certificate/Extension/TargetInformationExtension.php b/3rdparty/spomky-labs/pki-framework/src/X509/Certificate/Extension/TargetInformationExtension.php new file mode 100644 index 00000000..41f72be0 --- /dev/null +++ b/3rdparty/spomky-labs/pki-framework/src/X509/Certificate/Extension/TargetInformationExtension.php @@ -0,0 +1,137 @@ +targets = $targets; + } + + /** + * Reset internal state on clone. + */ + public function __clone() + { + $this->merged = null; + } + + public static function create(bool $critical, Targets ...$targets): self + { + return new self($critical, ...$targets); + } + + /** + * Initialize from one or more Target objects. + * + * Extension criticality shall be set to true as specified by RFC 5755. + */ + public static function fromTargets(Target ...$target): self + { + return self::create(true, Targets::create(...$target)); + } + + /** + * Get all targets. + */ + public function targets(): Targets + { + if ($this->merged === null) { + $a = []; + foreach ($this->targets as $targets) { + $a = array_merge($a, $targets->all()); + } + $this->merged = Targets::create(...$a); + } + return $this->merged; + } + + /** + * Get all name targets. + * + * @return Target[] + */ + public function names(): array + { + return $this->targets() + ->nameTargets(); + } + + /** + * Get all group targets. + * + * @return Target[] + */ + public function groups(): array + { + return $this->targets() + ->groupTargets(); + } + + /** + * @see \Countable::count() + */ + public function count(): int + { + return count($this->targets()); + } + + /** + * Get iterator for targets. + * + * @see \IteratorAggregate::getIterator() + */ + public function getIterator(): ArrayIterator + { + return new ArrayIterator($this->targets()->all()); + } + + protected static function fromDER(string $data, bool $critical): static + { + $targets = array_map( + static fn (UnspecifiedType $el) => Targets::fromASN1($el->asSequence()), + UnspecifiedType::fromDER($data)->asSequence()->elements() + ); + return self::create($critical, ...$targets); + } + + protected function valueASN1(): Element + { + $elements = array_map(static fn (Targets $targets) => $targets->toASN1(), $this->targets); + return Sequence::create(...$elements); + } +} diff --git a/3rdparty/spomky-labs/pki-framework/src/X509/Certificate/Extension/UnknownExtension.php b/3rdparty/spomky-labs/pki-framework/src/X509/Certificate/Extension/UnknownExtension.php new file mode 100644 index 00000000..64fea571 --- /dev/null +++ b/3rdparty/spomky-labs/pki-framework/src/X509/Certificate/Extension/UnknownExtension.php @@ -0,0 +1,61 @@ +toDER()); + } + + /** + * Create instance from a raw encoded extension value. + */ + public static function fromRawString(string $oid, bool $critical, string $data): self + { + return new self($oid, $critical, NullType::create(), $data); + } + + /** + * Get the encoded extension value. + */ + public function extensionValue(): string + { + return $this->data; + } + + protected function extnValue(): OctetString + { + return OctetString::create($this->data); + } + + protected function valueASN1(): Element + { + return $this->element; + } + + protected static function fromDER(string $data, bool $critical): static + { + throw new BadMethodCallException(__FUNCTION__ . ' must be implemented in derived class.'); + } +} diff --git a/3rdparty/spomky-labs/pki-framework/src/X509/Certificate/Extensions.php b/3rdparty/spomky-labs/pki-framework/src/X509/Certificate/Extensions.php new file mode 100644 index 00000000..692380ae --- /dev/null +++ b/3rdparty/spomky-labs/pki-framework/src/X509/Certificate/Extensions.php @@ -0,0 +1,344 @@ +extensions = []; + foreach ($extensions as $ext) { + $this->extensions[$ext->oid()] = $ext; + } + } + + public static function create(Extension ...$extensions): self + { + return new self(...$extensions); + } + + /** + * Initialize from ASN.1. + */ + public static function fromASN1(Sequence $seq): self + { + $extensions = array_map( + static fn (UnspecifiedType $el) => Extension::fromASN1($el->asSequence()), + $seq->elements() + ); + return self::create(...$extensions); + } + + /** + * Generate ASN.1 structure. + */ + public function toASN1(): Sequence + { + $elements = array_values(array_map(static fn ($ext) => $ext->toASN1(), $this->extensions)); + return Sequence::create(...$elements); + } + + /** + * Get self with extensions added. + * + * @param Extension ...$exts One or more extensions to add + */ + public function withExtensions(Extension ...$exts): self + { + $obj = clone $this; + foreach ($exts as $ext) { + $obj->extensions[$ext->oid()] = $ext; + } + return $obj; + } + + /** + * Check whether extension is present. + * + * @param string $oid Extensions OID + */ + public function has(string $oid): bool + { + return isset($this->extensions[$oid]); + } + + /** + * Get extension by OID. + */ + public function get(string $oid): Extension + { + if (! $this->has($oid)) { + throw new LogicException("No extension by OID {$oid}."); + } + return $this->extensions[$oid]; + } + + /** + * Check whether 'Authority Key Identifier' extension is present. + */ + public function hasAuthorityKeyIdentifier(): bool + { + return $this->has(Extension::OID_AUTHORITY_KEY_IDENTIFIER); + } + + /** + * Get 'Authority Key Identifier' extension. + */ + public function authorityKeyIdentifier(): AuthorityKeyIdentifierExtension + { + return $this->get(Extension::OID_AUTHORITY_KEY_IDENTIFIER); + } + + /** + * Check whether 'Subject Key Identifier' extension is present. + */ + public function hasSubjectKeyIdentifier(): bool + { + return $this->has(Extension::OID_SUBJECT_KEY_IDENTIFIER); + } + + /** + * Get 'Subject Key Identifier' extension. + */ + public function subjectKeyIdentifier(): SubjectKeyIdentifierExtension + { + return $this->get(Extension::OID_SUBJECT_KEY_IDENTIFIER); + } + + /** + * Check whether 'Key Usage' extension is present. + */ + public function hasKeyUsage(): bool + { + return $this->has(Extension::OID_KEY_USAGE); + } + + /** + * Get 'Key Usage' extension. + */ + public function keyUsage(): KeyUsageExtension + { + return $this->get(Extension::OID_KEY_USAGE); + } + + /** + * Check whether 'Certificate Policies' extension is present. + */ + public function hasCertificatePolicies(): bool + { + return $this->has(Extension::OID_CERTIFICATE_POLICIES); + } + + /** + * Get 'Certificate Policies' extension. + */ + public function certificatePolicies(): CertificatePoliciesExtension + { + return $this->get(Extension::OID_CERTIFICATE_POLICIES); + } + + /** + * Check whether 'Policy Mappings' extension is present. + */ + public function hasPolicyMappings(): bool + { + return $this->has(Extension::OID_POLICY_MAPPINGS); + } + + /** + * Get 'Policy Mappings' extension. + */ + public function policyMappings(): PolicyMappingsExtension + { + return $this->get(Extension::OID_POLICY_MAPPINGS); + } + + /** + * Check whether 'Subject Alternative Name' extension is present. + */ + public function hasSubjectAlternativeName(): bool + { + return $this->has(Extension::OID_SUBJECT_ALT_NAME); + } + + /** + * Get 'Subject Alternative Name' extension. + */ + public function subjectAlternativeName(): SubjectAlternativeNameExtension + { + return $this->get(Extension::OID_SUBJECT_ALT_NAME); + } + + /** + * Check whether 'Issuer Alternative Name' extension is present. + */ + public function hasIssuerAlternativeName(): bool + { + return $this->has(Extension::OID_ISSUER_ALT_NAME); + } + + /** + * Get 'Issuer Alternative Name' extension. + */ + public function issuerAlternativeName(): IssuerAlternativeNameExtension + { + return $this->get(Extension::OID_ISSUER_ALT_NAME); + } + + /** + * Check whether 'Basic Constraints' extension is present. + */ + public function hasBasicConstraints(): bool + { + return $this->has(Extension::OID_BASIC_CONSTRAINTS); + } + + /** + * Get 'Basic Constraints' extension. + */ + public function basicConstraints(): BasicConstraintsExtension + { + return $this->get(Extension::OID_BASIC_CONSTRAINTS); + } + + /** + * Check whether 'Name Constraints' extension is present. + */ + public function hasNameConstraints(): bool + { + return $this->has(Extension::OID_NAME_CONSTRAINTS); + } + + /** + * Get 'Name Constraints' extension. + */ + public function nameConstraints(): NameConstraintsExtension + { + return $this->get(Extension::OID_NAME_CONSTRAINTS); + } + + /** + * Check whether 'Policy Constraints' extension is present. + */ + public function hasPolicyConstraints(): bool + { + return $this->has(Extension::OID_POLICY_CONSTRAINTS); + } + + /** + * Get 'Policy Constraints' extension. + */ + public function policyConstraints(): PolicyConstraintsExtension + { + return $this->get(Extension::OID_POLICY_CONSTRAINTS); + } + + /** + * Check whether 'Extended Key Usage' extension is present. + */ + public function hasExtendedKeyUsage(): bool + { + return $this->has(Extension::OID_EXT_KEY_USAGE); + } + + /** + * Get 'Extended Key Usage' extension. + */ + public function extendedKeyUsage(): ExtendedKeyUsageExtension + { + return $this->get(Extension::OID_EXT_KEY_USAGE); + } + + /** + * Check whether 'CRL Distribution Points' extension is present. + */ + public function hasCRLDistributionPoints(): bool + { + return $this->has(Extension::OID_CRL_DISTRIBUTION_POINTS); + } + + /** + * Get 'CRL Distribution Points' extension. + */ + public function crlDistributionPoints(): CRLDistributionPointsExtension + { + return $this->get(Extension::OID_CRL_DISTRIBUTION_POINTS); + } + + /** + * Check whether 'Inhibit anyPolicy' extension is present. + */ + public function hasInhibitAnyPolicy(): bool + { + return $this->has(Extension::OID_INHIBIT_ANY_POLICY); + } + + /** + * Get 'Inhibit anyPolicy' extension. + */ + public function inhibitAnyPolicy(): InhibitAnyPolicyExtension + { + return $this->get(Extension::OID_INHIBIT_ANY_POLICY); + } + + /** + * @see \Countable::count() + */ + public function count(): int + { + return count($this->extensions); + } + + /** + * Get iterator for extensions. + * + * @see \IteratorAggregate::getIterator() + */ + public function getIterator(): Traversable + { + return new ArrayIterator($this->extensions); + } +} diff --git a/3rdparty/spomky-labs/pki-framework/src/X509/Certificate/TBSCertificate.php b/3rdparty/spomky-labs/pki-framework/src/X509/Certificate/TBSCertificate.php new file mode 100644 index 00000000..c917e369 --- /dev/null +++ b/3rdparty/spomky-labs/pki-framework/src/X509/Certificate/TBSCertificate.php @@ -0,0 +1,527 @@ +extensions = Extensions::create(); + } + + public static function create( + Name $subject, + PublicKeyInfo $subjectPublicKeyInfo, + Name $issuer, + Validity $validity + ): self { + return new self($subject, $subjectPublicKeyInfo, $issuer, $validity); + } + + /** + * Initialize from ASN.1. + */ + public static function fromASN1(Sequence $seq): self + { + $idx = 0; + if ($seq->hasTagged(0)) { + ++$idx; + $version = $seq->getTagged(0) + ->asExplicit() + ->asInteger() + ->intNumber(); + } else { + $version = self::VERSION_1; + } + $serial = $seq->at($idx++) + ->asInteger() + ->number(); + $algo = AlgorithmIdentifier::fromASN1($seq->at($idx++)->asSequence()); + if (! $algo instanceof SignatureAlgorithmIdentifier) { + throw new UnexpectedValueException('Unsupported signature algorithm ' . $algo->name() . '.'); + } + $issuer = Name::fromASN1($seq->at($idx++)->asSequence()); + $validity = Validity::fromASN1($seq->at($idx++)->asSequence()); + $subject = Name::fromASN1($seq->at($idx++)->asSequence()); + $pki = PublicKeyInfo::fromASN1($seq->at($idx++)->asSequence()); + $tbs_cert = self::create($subject, $pki, $issuer, $validity) + ->withVersion($version) + ->withSerialNumber($serial) + ->withSignature($algo) + ; + if ($seq->hasTagged(1)) { + $tbs_cert = $tbs_cert->withIssuerUniqueID(UniqueIdentifier::fromASN1( + $seq->getTagged(1) + ->asImplicit(Element::TYPE_BIT_STRING) + ->asBitString() + )); + } + if ($seq->hasTagged(2)) { + $tbs_cert = $tbs_cert->withSubjectUniqueID(UniqueIdentifier::fromASN1( + $seq->getTagged(2) + ->asImplicit(Element::TYPE_BIT_STRING) + ->asBitString() + )); + } + if ($seq->hasTagged(3)) { + $tbs_cert = $tbs_cert->withExtensions(Extensions::fromASN1($seq->getTagged(3)->asExplicit()->asSequence())); + } + return $tbs_cert; + } + + /** + * Initialize from certification request. + * + * Note that signature is not verified and must be done by the caller. + */ + public static function fromCSR(CertificationRequest $cr): self + { + $cri = $cr->certificationRequestInfo(); + $tbs_cert = self::create( + $cri->subject(), + $cri->subjectPKInfo(), + Name::create(), + Validity::fromStrings(null, null) + ); + // if CSR has Extension Request attribute + if ($cri->hasAttributes()) { + $attribs = $cri->attributes(); + if ($attribs->hasExtensionRequest()) { + $tbs_cert = $tbs_cert->withExtensions($attribs->extensionRequest()->extensions()); + } + } + // add Subject Key Identifier extension + return $tbs_cert->withAdditionalExtensions( + SubjectKeyIdentifierExtension::create(false, $cri->subjectPKInfo()->keyIdentifier()) + ); + } + + /** + * Get self with fields set from the issuer's certificate. + * + * Issuer shall be set to issuing certificate's subject. Authority key identifier extensions shall be added with a + * key identifier set to issuing certificate's public key identifier. + * + * @param Certificate $cert Issuing party's certificate + */ + public function withIssuerCertificate(Certificate $cert): self + { + $obj = clone $this; + // set issuer DN from cert's subject + $obj->issuer = $cert->tbsCertificate() + ->subject(); + // add authority key identifier extension + $key_id = $cert->tbsCertificate() + ->subjectPublicKeyInfo() + ->keyIdentifier(); + $obj->extensions = $obj->extensions->withExtensions(AuthorityKeyIdentifierExtension::create(false, $key_id)); + return $obj; + } + + /** + * Get self with given version. + * + * If version is not set, appropriate version is automatically determined during signing. + */ + public function withVersion(int $version): self + { + $obj = clone $this; + $obj->version = $version; + return $obj; + } + + /** + * Get self with given serial number. + * + * @param int|string $serial Base 10 number + */ + public function withSerialNumber(int|string $serial): self + { + $obj = clone $this; + $obj->serialNumber = strval($serial); + return $obj; + } + + /** + * Get self with random positive serial number. + * + * @param int $size Number of random bytes + */ + public function withRandomSerialNumber(int $size): self + { + // ensure that first byte is always non-zero and having first bit unset + $num = BigInteger::of(random_int(1, 0x7f)); + for ($i = 1; $i < $size; ++$i) { + $num = $num->shiftedLeft(8); + $num = $num->plus(random_int(0, 0xff)); + } + return $this->withSerialNumber($num->toBase(10)); + } + + /** + * Get self with given signature algorithm. + */ + public function withSignature(SignatureAlgorithmIdentifier $algo): self + { + $obj = clone $this; + $obj->signature = $algo; + return $obj; + } + + /** + * Get self with given issuer. + */ + public function withIssuer(Name $issuer): self + { + $obj = clone $this; + $obj->issuer = $issuer; + return $obj; + } + + /** + * Get self with given validity. + */ + public function withValidity(Validity $validity): self + { + $obj = clone $this; + $obj->validity = $validity; + return $obj; + } + + /** + * Get self with given subject. + */ + public function withSubject(Name $subject): self + { + $obj = clone $this; + $obj->subject = $subject; + return $obj; + } + + /** + * Get self with given subject public key info. + */ + public function withSubjectPublicKeyInfo(PublicKeyInfo $pub_key_info): self + { + $obj = clone $this; + $obj->subjectPublicKeyInfo = $pub_key_info; + return $obj; + } + + /** + * Get self with issuer unique ID. + */ + public function withIssuerUniqueID(UniqueIdentifier $id): self + { + $obj = clone $this; + $obj->issuerUniqueID = $id; + return $obj; + } + + /** + * Get self with subject unique ID. + */ + public function withSubjectUniqueID(UniqueIdentifier $id): self + { + $obj = clone $this; + $obj->subjectUniqueID = $id; + return $obj; + } + + /** + * Get self with given extensions. + */ + public function withExtensions(Extensions $extensions): self + { + $obj = clone $this; + $obj->extensions = $extensions; + return $obj; + } + + /** + * Get self with extensions added. + * + * @param Extension ...$exts One or more Extension objects + */ + public function withAdditionalExtensions(Extension ...$exts): self + { + $obj = clone $this; + $obj->extensions = $obj->extensions->withExtensions(...$exts); + return $obj; + } + + /** + * Check whether version is set. + */ + public function hasVersion(): bool + { + return isset($this->version); + } + + /** + * Get certificate version. + */ + public function version(): int + { + if (! $this->hasVersion()) { + throw new LogicException('version not set.'); + } + return $this->version; + } + + /** + * Check whether serial number is set. + */ + public function hasSerialNumber(): bool + { + return isset($this->serialNumber); + } + + /** + * Get serial number. + * + * @return string Base 10 integer + */ + public function serialNumber(): string + { + if (! $this->hasSerialNumber()) { + throw new LogicException('serialNumber not set.'); + } + return $this->serialNumber; + } + + /** + * Check whether signature algorithm is set. + */ + public function hasSignature(): bool + { + return isset($this->signature); + } + + /** + * Get signature algorithm. + */ + public function signature(): SignatureAlgorithmIdentifier + { + if (! $this->hasSignature()) { + throw new LogicException('signature not set.'); + } + return $this->signature; + } + + public function issuer(): Name + { + return $this->issuer; + } + + /** + * Get validity period. + */ + public function validity(): Validity + { + return $this->validity; + } + + public function subject(): Name + { + return $this->subject; + } + + /** + * Get subject public key. + */ + public function subjectPublicKeyInfo(): PublicKeyInfo + { + return $this->subjectPublicKeyInfo; + } + + /** + * Whether issuer unique identifier is present. + */ + public function hasIssuerUniqueID(): bool + { + return isset($this->issuerUniqueID); + } + + public function issuerUniqueID(): UniqueIdentifier + { + if (! $this->hasIssuerUniqueID()) { + throw new LogicException('issuerUniqueID not set.'); + } + return $this->issuerUniqueID; + } + + /** + * Whether subject unique identifier is present. + */ + public function hasSubjectUniqueID(): bool + { + return isset($this->subjectUniqueID); + } + + public function subjectUniqueID(): UniqueIdentifier + { + if (! $this->hasSubjectUniqueID()) { + throw new LogicException('subjectUniqueID not set.'); + } + return $this->subjectUniqueID; + } + + public function extensions(): Extensions + { + return $this->extensions; + } + + /** + * Generate ASN.1 structure. + */ + public function toASN1(): Sequence + { + $elements = []; + $version = $this->version(); + // if version is not default + if ($version !== self::VERSION_1) { + $elements[] = ExplicitlyTaggedType::create(0, Integer::create($version)); + } + $serial = $this->serialNumber(); + $signature = $this->signature(); + // add required elements + array_push( + $elements, + Integer::create($serial), + $signature->toASN1(), + $this->issuer->toASN1(), + $this->validity->toASN1(), + $this->subject->toASN1(), + $this->subjectPublicKeyInfo->toASN1() + ); + if (isset($this->issuerUniqueID)) { + $elements[] = ImplicitlyTaggedType::create(1, $this->issuerUniqueID->toASN1()); + } + if (isset($this->subjectUniqueID)) { + $elements[] = ImplicitlyTaggedType::create(2, $this->subjectUniqueID->toASN1()); + } + if (count($this->extensions) !== 0) { + $elements[] = ExplicitlyTaggedType::create(3, $this->extensions->toASN1()); + } + return Sequence::create(...$elements); + } + + /** + * Create signed certificate. + * + * @param SignatureAlgorithmIdentifier $algo Algorithm used for signing + * @param PrivateKeyInfo $privkey_info Private key used for signing + * @param null|Crypto $crypto Crypto engine, use default if not set + */ + public function sign( + SignatureAlgorithmIdentifier $algo, + PrivateKeyInfo $privkey_info, + ?Crypto $crypto = null + ): Certificate { + $crypto ??= Crypto::getDefault(); + $tbs_cert = clone $this; + if (! isset($tbs_cert->version)) { + $tbs_cert->version = $tbs_cert->_determineVersion(); + } + if (! isset($tbs_cert->serialNumber)) { + $tbs_cert->serialNumber = '0'; + } + $tbs_cert->signature = $algo; + $data = $tbs_cert->toASN1() + ->toDER(); + $signature = $crypto->sign($data, $privkey_info, $algo); + return Certificate::create($tbs_cert, $algo, $signature); + } + + /** + * Determine minimum version for the certificate. + */ + private function _determineVersion(): int + { + // if extensions are present + if (count($this->extensions) !== 0) { + return self::VERSION_3; + } + // if UniqueIdentifier is present + if (isset($this->issuerUniqueID) || isset($this->subjectUniqueID)) { + return self::VERSION_2; + } + return self::VERSION_1; + } +} diff --git a/3rdparty/spomky-labs/pki-framework/src/X509/Certificate/Time.php b/3rdparty/spomky-labs/pki-framework/src/X509/Certificate/Time.php new file mode 100644 index 00000000..cd6d0eef --- /dev/null +++ b/3rdparty/spomky-labs/pki-framework/src/X509/Certificate/Time.php @@ -0,0 +1,95 @@ +type = $type ?? self::determineType($dt); + } + + public static function create(DateTimeImmutable $dt): self + { + return new self($dt, null); + } + + /** + * Initialize from ASN.1. + */ + public static function fromASN1(TimeType $el): self + { + return self::create($el->dateTime()); + } + + /** + * Initialize from date string. + */ + public static function fromString(?string $time, ?string $tz = null): self + { + return self::create(self::createDateTime($time, $tz)); + } + + public function dateTime(): DateTimeImmutable + { + return $this->dt; + } + + /** + * Generate ASN.1. + */ + public function toASN1(): TimeType + { + $dt = $this->dt; + switch ($this->type) { + case Element::TYPE_UTC_TIME: + return UTCTime::create($dt); + case Element::TYPE_GENERALIZED_TIME: + // GeneralizedTime must not contain fractional seconds + // (rfc5280 4.1.2.5.2) + if ((int) $dt->format('u') !== 0) { + // remove fractional seconds (round down) + $dt = self::roundDownFractionalSeconds($dt); + } + return GeneralizedTime::create($dt); + } + throw new UnexpectedValueException('Time type ' . Element::tagToName($this->type) . ' not supported.'); + } + + /** + * Determine whether to use UTCTime or GeneralizedTime ASN.1 type. + * + * @return int Type tag + */ + protected static function determineType(DateTimeImmutable $dt): int + { + if ($dt->format('Y') >= 2050) { + return Element::TYPE_GENERALIZED_TIME; + } + return Element::TYPE_UTC_TIME; + } +} diff --git a/3rdparty/spomky-labs/pki-framework/src/X509/Certificate/UniqueIdentifier.php b/3rdparty/spomky-labs/pki-framework/src/X509/Certificate/UniqueIdentifier.php new file mode 100644 index 00000000..da6a7f8b --- /dev/null +++ b/3rdparty/spomky-labs/pki-framework/src/X509/Certificate/UniqueIdentifier.php @@ -0,0 +1,65 @@ +uid->string(); + } + + /** + * Get unique identifier as a bit string. + */ + public function bitString(): BitString + { + return $this->uid; + } + + /** + * Get ASN.1 element. + */ + public function toASN1(): BitString + { + return $this->uid; + } +} diff --git a/3rdparty/spomky-labs/pki-framework/src/X509/Certificate/Validity.php b/3rdparty/spomky-labs/pki-framework/src/X509/Certificate/Validity.php new file mode 100644 index 00000000..c58c39b9 --- /dev/null +++ b/3rdparty/spomky-labs/pki-framework/src/X509/Certificate/Validity.php @@ -0,0 +1,72 @@ +at(0)->asTime()); + $na = Time::fromASN1($seq->at(1)->asTime()); + return self::create($nb, $na); + } + + /** + * Initialize from date strings. + * + * @param null|string $nb_date Not before date + * @param null|string $na_date Not after date + * @param null|string $tz Timezone string + */ + public static function fromStrings(?string $nb_date, ?string $na_date, ?string $tz = null): self + { + return self::create(Time::fromString($nb_date, $tz), Time::fromString($na_date, $tz)); + } + + /** + * Get not before time. + */ + public function notBefore(): Time + { + return $this->notBefore; + } + + /** + * Get not after time. + */ + public function notAfter(): Time + { + return $this->notAfter; + } + + /** + * Generate ASN.1 structure. + */ + public function toASN1(): Sequence + { + return Sequence::create($this->notBefore->toASN1(), $this->notAfter->toASN1()); + } +} diff --git a/3rdparty/spomky-labs/pki-framework/src/X509/CertificationPath/CertificationPath.php b/3rdparty/spomky-labs/pki-framework/src/X509/CertificationPath/CertificationPath.php new file mode 100644 index 00000000..098af86b --- /dev/null +++ b/3rdparty/spomky-labs/pki-framework/src/X509/CertificationPath/CertificationPath.php @@ -0,0 +1,178 @@ +certificates = $certificates; + } + + public static function create(Certificate ...$certificates): self + { + return new self(...$certificates); + } + + /** + * Initialize from a certificate chain. + */ + public static function fromCertificateChain(CertificateChain $chain): self + { + return self::create(...array_reverse($chain->certificates(), false)); + } + + /** + * Build certification path to given target. + * + * @param Certificate $target Target end-entity certificate + * @param CertificateBundle $trust_anchors List of trust anchors + * @param null|CertificateBundle $intermediate Optional intermediate certificates + */ + public static function toTarget( + Certificate $target, + CertificateBundle $trust_anchors, + ?CertificateBundle $intermediate = null + ): self { + return CertificationPathBuilder::create($trust_anchors)->shortestPathToTarget($target, $intermediate); + } + + /** + * Build certification path from given trust anchor to target certificate, using intermediate certificates from + * given bundle. + * + * @param Certificate $trust_anchor Trust anchor certificate + * @param Certificate $target Target end-entity certificate + * @param null|CertificateBundle $intermediate Optional intermediate certificates + */ + public static function fromTrustAnchorToTarget( + Certificate $trust_anchor, + Certificate $target, + ?CertificateBundle $intermediate = null + ): self { + return self::toTarget($target, CertificateBundle::create($trust_anchor), $intermediate); + } + + /** + * Get certificates. + * + * @return Certificate[] + */ + public function certificates(): array + { + return $this->certificates; + } + + /** + * Get the trust anchor certificate from the path. + */ + public function trustAnchorCertificate(): Certificate + { + if (count($this->certificates) === 0) { + throw new LogicException('No certificates.'); + } + return $this->certificates[0]; + } + + /** + * Get the end-entity certificate from the path. + */ + public function endEntityCertificate(): Certificate + { + if (count($this->certificates) === 0) { + throw new LogicException('No certificates.'); + } + return $this->certificates[count($this->certificates) - 1]; + } + + /** + * Get certification path as a certificate chain. + */ + public function certificateChain(): CertificateChain + { + return CertificateChain::create(...array_reverse($this->certificates, false)); + } + + /** + * Check whether certification path starts with one ore more given certificates in parameter order. + * + * @param Certificate ...$certs Certificates + */ + public function startsWith(Certificate ...$certs): bool + { + $n = count($certs); + if ($n > count($this->certificates)) { + return false; + } + for ($i = 0; $i < $n; ++$i) { + if (! $certs[$i]->equals($this->certificates[$i])) { + return false; + } + } + return true; + } + + /** + * Validate certification path. + * + * @param null|Crypto $crypto Crypto engine, use default if not set + */ + public function validate(PathValidationConfig $config, ?Crypto $crypto = null): PathValidationResult + { + $crypto ??= Crypto::getDefault(); + return PathValidator::create($crypto, $config, ...$this->certificates)->validate(); + } + + /** + * @see \Countable::count() + */ + public function count(): int + { + return count($this->certificates); + } + + /** + * Get iterator for certificates. + * + * @see \IteratorAggregate::getIterator() + */ + public function getIterator(): ArrayIterator + { + return new ArrayIterator($this->certificates); + } +} diff --git a/3rdparty/spomky-labs/pki-framework/src/X509/CertificationPath/Exception/PathBuildingException.php b/3rdparty/spomky-labs/pki-framework/src/X509/CertificationPath/Exception/PathBuildingException.php new file mode 100644 index 00000000..a55536e6 --- /dev/null +++ b/3rdparty/spomky-labs/pki-framework/src/X509/CertificationPath/Exception/PathBuildingException.php @@ -0,0 +1,14 @@ +resolvePathsToTarget($target, $intermediate); + // map paths to CertificationPath objects + return array_map(static fn ($certs) => CertificationPath::create(...$certs), $paths); + } + + /** + * Get the shortest path to given target certificate from any trust anchor. + * + * @param Certificate $target Target certificate + * @param null|CertificateBundle $intermediate Optional intermediate certificates + */ + public function shortestPathToTarget( + Certificate $target, + ?CertificateBundle $intermediate = null + ): CertificationPath { + $paths = $this->allPathsToTarget($target, $intermediate); + if (count($paths) === 0) { + throw new PathBuildingException('No certification paths.'); + } + usort($paths, fn ($a, $b) => count($a) < count($b) ? -1 : 1); + return reset($paths); + } + + /** + * Find all issuers of the target certificate from a given bundle. + * + * @param Certificate $target Target certificate + * @param CertificateBundle $bundle Certificates to search + * + * @return Certificate[] + */ + private function findIssuers(Certificate $target, CertificateBundle $bundle): array + { + $issuers = []; + $issuer_name = $target->tbsCertificate() + ->issuer(); + $extensions = $target->tbsCertificate() + ->extensions(); + // find by authority key identifier + if ($extensions->hasAuthorityKeyIdentifier()) { + $ext = $extensions->authorityKeyIdentifier(); + if ($ext->hasKeyIdentifier()) { + foreach ($bundle->allBySubjectKeyIdentifier($ext->keyIdentifier()) as $issuer) { + // check that issuer name matches + if ($issuer->tbsCertificate()->subject()->equals($issuer_name)) { + $issuers[] = $issuer; + } + } + } + } + return $issuers; + } + + /** + * Resolve all possible certification paths from any trust anchor to the target certificate, using optional + * intermediate certificates. + * + * Helper method for allPathsToTarget to be called recursively. + * + * @return array> Array of arrays containing path certificates + * @todo Implement loop detection + */ + private function resolvePathsToTarget(Certificate $target, ?CertificateBundle $intermediate = null): array + { + // array of possible paths + $paths = []; + // signed by certificate in the trust list + foreach ($this->findIssuers($target, $this->trustList) as $issuer) { + // if target is self-signed, path consists of only + // the target certificate + if ($target->equals($issuer)) { + $paths[] = [$target]; + } else { + $paths[] = [$issuer, $target]; + } + } + if (isset($intermediate)) { + // signed by intermediate certificate + foreach ($this->findIssuers($target, $intermediate) as $issuer) { + // intermediate certificate must not be self-signed + if ($issuer->isSelfIssued()) { + continue; + } + // resolve paths to issuer + $subpaths = $this->resolvePathsToTarget($issuer, $intermediate); + foreach ($subpaths as $path) { + $paths[] = array_merge($path, [$target]); + } + } + } + + return $paths; + } +} diff --git a/3rdparty/spomky-labs/pki-framework/src/X509/CertificationPath/PathValidation/PathValidationConfig.php b/3rdparty/spomky-labs/pki-framework/src/X509/CertificationPath/PathValidation/PathValidationConfig.php new file mode 100644 index 00000000..3fbf04f6 --- /dev/null +++ b/3rdparty/spomky-labs/pki-framework/src/X509/CertificationPath/PathValidation/PathValidationConfig.php @@ -0,0 +1,210 @@ +policySet = [PolicyInformation::OID_ANY_POLICY]; + $this->policyMappingInhibit = false; + $this->explicitPolicy = false; + $this->anyPolicyInhibit = false; + } + + public static function create(DateTimeImmutable $dateTime, int $maxLength): self + { + return new self($dateTime, $maxLength); + } + + /** + * Get default configuration. + */ + public static function defaultConfig(): self + { + return self::create(new DateTimeImmutable(), 3); + } + + /** + * Get self with maximum path length. + */ + public function withMaxLength(int $length): self + { + $obj = clone $this; + $obj->maxLength = $length; + return $obj; + } + + /** + * Get self with reference date and time. + */ + public function withDateTime(DateTimeImmutable $dt): self + { + $obj = clone $this; + $obj->dateTime = $dt; + return $obj; + } + + /** + * Get self with trust anchor certificate. + */ + public function withTrustAnchor(Certificate $ca): self + { + $obj = clone $this; + $obj->trustAnchor = $ca; + return $obj; + } + + /** + * Get self with initial-policy-mapping-inhibit set. + */ + public function withPolicyMappingInhibit(bool $flag): self + { + $obj = clone $this; + $obj->policyMappingInhibit = $flag; + return $obj; + } + + /** + * Get self with initial-explicit-policy set. + */ + public function withExplicitPolicy(bool $flag): self + { + $obj = clone $this; + $obj->explicitPolicy = $flag; + return $obj; + } + + /** + * Get self with initial-any-policy-inhibit set. + */ + public function withAnyPolicyInhibit(bool $flag): self + { + $obj = clone $this; + $obj->anyPolicyInhibit = $flag; + return $obj; + } + + /** + * Get self with user-initial-policy-set set to policy OIDs. + * + * @param string ...$policies List of policy OIDs + */ + public function withPolicySet(string ...$policies): self + { + $obj = clone $this; + $obj->policySet = $policies; + return $obj; + } + + /** + * Get maximum certification path length. + */ + public function maxLength(): int + { + return $this->maxLength; + } + + /** + * Get reference date and time. + */ + public function dateTime(): DateTimeImmutable + { + return $this->dateTime; + } + + /** + * Get user-initial-policy-set. + * + * @return string[] Array of OID's + */ + public function policySet(): array + { + return $this->policySet; + } + + /** + * Check whether trust anchor certificate is set. + */ + public function hasTrustAnchor(): bool + { + return isset($this->trustAnchor); + } + + /** + * Get trust anchor certificate. + */ + public function trustAnchor(): Certificate + { + if (! $this->hasTrustAnchor()) { + throw new LogicException('No trust anchor.'); + } + return $this->trustAnchor; + } + + public function policyMappingInhibit(): bool + { + return $this->policyMappingInhibit; + } + + public function explicitPolicy(): bool + { + return $this->explicitPolicy; + } + + public function anyPolicyInhibit(): bool + { + return $this->anyPolicyInhibit; + } +} diff --git a/3rdparty/spomky-labs/pki-framework/src/X509/CertificationPath/PathValidation/PathValidationResult.php b/3rdparty/spomky-labs/pki-framework/src/X509/CertificationPath/PathValidation/PathValidationResult.php new file mode 100644 index 00000000..b80e89f5 --- /dev/null +++ b/3rdparty/spomky-labs/pki-framework/src/X509/CertificationPath/PathValidation/PathValidationResult.php @@ -0,0 +1,99 @@ +certificates = array_values($certificates); + } + + /** + * @param Certificate[] $certificates Certificates in a certification path + * @param null|PolicyTree $policyTree Valid policy tree + * @param PublicKeyInfo $publicKeyInfo Public key of the end-entity certificate + * @param AlgorithmIdentifierType $publicKeyAlgo Public key algorithm of the end-entity certificate + * @param null|Element $publicKeyParameters Algorithm parameters + */ + public static function create( + array $certificates, + ?PolicyTree $policyTree, + PublicKeyInfo $publicKeyInfo, + AlgorithmIdentifierType $publicKeyAlgo, + ?Element $publicKeyParameters = null + ): self { + return new self($certificates, $policyTree, $publicKeyInfo, $publicKeyAlgo, $publicKeyParameters); + } + + public function getPolicyTree(): ?PolicyTree + { + return $this->policyTree; + } + + public function getPublicKeyInfo(): PublicKeyInfo + { + return $this->publicKeyInfo; + } + + public function getPublicKeyAlgo(): AlgorithmIdentifierType + { + return $this->publicKeyAlgo; + } + + public function getPublicKeyParameters(): ?Element + { + return $this->publicKeyParameters; + } + + /** + * Get end-entity certificate. + */ + public function certificate(): Certificate + { + return $this->certificates[count($this->certificates) - 1]; + } + + /** + * Get certificate policies of the end-entity certificate. + * + * @return PolicyInformation[] + */ + public function policies(): array + { + if ($this->policyTree === null) { + return []; + } + return $this->policyTree->policiesAtDepth(count($this->certificates)); + } +} diff --git a/3rdparty/spomky-labs/pki-framework/src/X509/CertificationPath/PathValidation/PathValidator.php b/3rdparty/spomky-labs/pki-framework/src/X509/CertificationPath/PathValidation/PathValidator.php new file mode 100644 index 00000000..8a64596c --- /dev/null +++ b/3rdparty/spomky-labs/pki-framework/src/X509/CertificationPath/PathValidation/PathValidator.php @@ -0,0 +1,484 @@ +certificates = $certificates; + // if trust anchor is explicitly given in configuration + if ($config->hasTrustAnchor()) { + $this->trustAnchor = $config->trustAnchor(); + } else { + $this->trustAnchor = $certificates[0]; + } + } + + public static function create( + Crypto $crypto, + PathValidationConfig $config, + Certificate ...$certificates + ): self { + return new self($crypto, $config, ...$certificates); + } + + /** + * Validate certification path. + */ + public function validate(): PathValidationResult + { + $n = count($this->certificates); + $state = ValidatorState::initialize($this->config, $this->trustAnchor, $n); + foreach ($this->certificates as $i => $iValue) { + $state = $state->withIndex($i + 1); + $cert = $iValue; + // process certificate (section 6.1.3.) + $state = $this->processCertificate($state, $cert); + if (! $state->isFinal()) { + // prepare next certificate (section 6.1.4.) + $state = $this->prepareNext($state, $cert); + } + } + if (! isset($cert)) { + throw new LogicException('No certificates.'); + } + // wrap-up (section 6.1.5.) + $state = $this->wrapUp($state, $cert); + // return outputs + return $state->getResult($this->certificates); + } + + /** + * Apply basic certificate processing according to RFC 5280 section 6.1.3. + * + * @see https://tools.ietf.org/html/rfc5280#section-6.1.3 + */ + private function processCertificate(ValidatorState $state, Certificate $cert): ValidatorState + { + // (a.1) verify signature + $this->verifySignature($state, $cert); + // (a.2) check validity period + $this->checkValidity($cert); + // (a.3) check that certificate is not revoked + $this->checkRevocation(); + // (a.4) check issuer + $this->checkIssuer($state, $cert); + // (b)(c) if certificate is self-issued and it is not + // the final certificate in the path, skip this step + if (! ($cert->isSelfIssued() && ! $state->isFinal())) { + // (b) check permitted subtrees + $this->checkPermittedSubtrees($state); + // (c) check excluded subtrees + $this->checkExcludedSubtrees($state); + } + $extensions = $cert->tbsCertificate() + ->extensions(); + if ($extensions->hasCertificatePolicies()) { + // (d) process policy information + if ($state->hasValidPolicyTree()) { + $state = $state->validPolicyTree() + ->processPolicies($state, $cert); + } + } else { + // (e) certificate policies extension not present, + // set the valid_policy_tree to NULL + $state = $state->withoutValidPolicyTree(); + } + // (f) check that explicit_policy > 0 or valid_policy_tree is set + if (! ($state->explicitPolicy() > 0 || $state->hasValidPolicyTree())) { + throw new PathValidationException('No valid policies.'); + } + return $state; + } + + /** + * Apply preparation for the certificate i+1 according to rfc5280 section 6.1.4. + * + * @see https://tools.ietf.org/html/rfc5280#section-6.1.4 + */ + private function prepareNext(ValidatorState $state, Certificate $cert): ValidatorState + { + // (a)(b) if policy mappings extension is present + $state = $this->preparePolicyMappings($state, $cert); + // (c) assign working_issuer_name + $state = $state->withWorkingIssuerName($cert->tbsCertificate()->subject()); + // (d)(e)(f) + $state = $this->setPublicKeyState($state, $cert); + // (g) if name constraints extension is present + $state = $this->prepareNameConstraints($state, $cert); + // (h) if certificate is not self-issued + if (! $cert->isSelfIssued()) { + $state = $this->prepareNonSelfIssued($state); + } + // (i) if policy constraints extension is present + $state = $this->preparePolicyConstraints($state, $cert); + // (j) if inhibit any policy extension is present + $state = $this->prepareInhibitAnyPolicy($state, $cert); + // (k) check basic constraints + $this->processBasicContraints($cert); + // (l) verify max_path_length + $state = $this->verifyMaxPathLength($state, $cert); + // (m) check pathLenContraint + $state = $this->processPathLengthContraint($state, $cert); + // (n) check key usage + $this->checkKeyUsage($cert); + // (o) process relevant extensions + return $this->processExtensions($state); + } + + /** + * Apply wrap-up procedure according to RFC 5280 section 6.1.5. + * + * @see https://tools.ietf.org/html/rfc5280#section-6.1.5 + */ + private function wrapUp(ValidatorState $state, Certificate $cert): ValidatorState + { + $tbs_cert = $cert->tbsCertificate(); + $extensions = $tbs_cert->extensions(); + // (a) + if ($state->explicitPolicy() > 0) { + $state = $state->withExplicitPolicy($state->explicitPolicy() - 1); + } + // (b) + if ($extensions->hasPolicyConstraints()) { + $ext = $extensions->policyConstraints(); + if ($ext->hasRequireExplicitPolicy() && + $ext->requireExplicitPolicy() === 0) { + $state = $state->withExplicitPolicy(0); + } + } + // (c)(d)(e) + $state = $this->setPublicKeyState($state, $cert); + // (f) process relevant extensions + $state = $this->processExtensions($state); + // (g) intersection of valid_policy_tree and the initial-policy-set + $state = $this->calculatePolicyIntersection($state); + // check that explicit_policy > 0 or valid_policy_tree is set + if (! ($state->explicitPolicy() > 0 || $state->hasValidPolicyTree())) { + throw new PathValidationException('No valid policies.'); + } + // path validation succeeded + return $state; + } + + /** + * Update working_public_key, working_public_key_parameters and working_public_key_algorithm state variables from + * certificate. + */ + private function setPublicKeyState(ValidatorState $state, Certificate $cert): ValidatorState + { + $pk_info = $cert->tbsCertificate() + ->subjectPublicKeyInfo(); + // assign working_public_key + $state = $state->withWorkingPublicKey($pk_info); + // assign working_public_key_parameters + $params = ValidatorState::getAlgorithmParameters($pk_info->algorithmIdentifier()); + if ($params !== null) { + $state = $state->withWorkingPublicKeyParameters($params); + } else { + // if algorithms differ, set parameters to null + if ($pk_info->algorithmIdentifier()->oid() !== + $state->workingPublicKeyAlgorithm() + ->oid()) { + $state = $state->withWorkingPublicKeyParameters(null); + } + } + // assign working_public_key_algorithm + return $state->withWorkingPublicKeyAlgorithm($pk_info->algorithmIdentifier()); + } + + /** + * Verify certificate signature. + */ + private function verifySignature(ValidatorState $state, Certificate $cert): void + { + try { + $valid = $cert->verify($state->workingPublicKey(), $this->crypto); + } catch (RuntimeException $e) { + throw new PathValidationException('Failed to verify signature: ' . $e->getMessage(), 0, $e); + } + if (! $valid) { + throw new PathValidationException("Certificate signature doesn't match."); + } + } + + /** + * Check certificate validity. + */ + private function checkValidity(Certificate $cert): void + { + $refdt = $this->config->dateTime(); + $validity = $cert->tbsCertificate() + ->validity(); + if ($validity->notBefore()->dateTime()->diff($refdt)->invert !== 0) { + throw new PathValidationException('Certificate validity period has not started.'); + } + if ($refdt->diff($validity->notAfter()->dateTime())->invert !== 0) { + throw new PathValidationException('Certificate has expired.'); + } + } + + /** + * Check certificate revocation. + */ + private function checkRevocation(): void + { + // @todo Implement CRL handling + } + + /** + * Check certificate issuer. + */ + private function checkIssuer(ValidatorState $state, Certificate $cert): void + { + if (! $cert->tbsCertificate()->issuer()->equals($state->workingIssuerName())) { + throw new PathValidationException('Certification issuer mismatch.'); + } + } + + private function checkPermittedSubtrees(ValidatorState $state): void + { + // @todo Implement + $state->permittedSubtrees(); + } + + private function checkExcludedSubtrees(ValidatorState $state): void + { + // @todo Implement + $state->excludedSubtrees(); + } + + /** + * Apply policy mappings handling for the preparation step. + */ + private function preparePolicyMappings(ValidatorState $state, Certificate $cert): ValidatorState + { + $extensions = $cert->tbsCertificate() + ->extensions(); + if ($extensions->hasPolicyMappings()) { + // (a) verify that anyPolicy mapping is not used + if ($extensions->policyMappings()->hasAnyPolicyMapping()) { + throw new PathValidationException('anyPolicy mapping found.'); + } + // (b) process policy mappings + if ($state->hasValidPolicyTree()) { + $state = $state->validPolicyTree() + ->processMappings($state, $cert); + } + } + return $state; + } + + /** + * Apply name constraints handling for the preparation step. + */ + private function prepareNameConstraints(ValidatorState $state, Certificate $cert): ValidatorState + { + $extensions = $cert->tbsCertificate() + ->extensions(); + if ($extensions->hasNameConstraints()) { + $state = $this->processNameConstraints($state); + } + return $state; + } + + /** + * Apply preparation for a non-self-signed certificate. + */ + private function prepareNonSelfIssued(ValidatorState $state): ValidatorState + { + // (h.1) + if ($state->explicitPolicy() > 0) { + $state = $state->withExplicitPolicy($state->explicitPolicy() - 1); + } + // (h.2) + if ($state->policyMapping() > 0) { + $state = $state->withPolicyMapping($state->policyMapping() - 1); + } + // (h.3) + if ($state->inhibitAnyPolicy() > 0) { + $state = $state->withInhibitAnyPolicy($state->inhibitAnyPolicy() - 1); + } + return $state; + } + + /** + * Apply policy constraints handling for the preparation step. + */ + private function preparePolicyConstraints(ValidatorState $state, Certificate $cert): ValidatorState + { + $extensions = $cert->tbsCertificate() + ->extensions(); + if (! $extensions->hasPolicyConstraints()) { + return $state; + } + $ext = $extensions->policyConstraints(); + // (i.1) + if ($ext->hasRequireExplicitPolicy() && + $ext->requireExplicitPolicy() < $state->explicitPolicy()) { + $state = $state->withExplicitPolicy($ext->requireExplicitPolicy()); + } + // (i.2) + if ($ext->hasInhibitPolicyMapping() && + $ext->inhibitPolicyMapping() < $state->policyMapping()) { + $state = $state->withPolicyMapping($ext->inhibitPolicyMapping()); + } + return $state; + } + + /** + * Apply inhibit any-policy handling for the preparation step. + */ + private function prepareInhibitAnyPolicy(ValidatorState $state, Certificate $cert): ValidatorState + { + $extensions = $cert->tbsCertificate() + ->extensions(); + if ($extensions->hasInhibitAnyPolicy()) { + $ext = $extensions->inhibitAnyPolicy(); + if ($ext->skipCerts() < $state->inhibitAnyPolicy()) { + $state = $state->withInhibitAnyPolicy($ext->skipCerts()); + } + } + return $state; + } + + /** + * Verify maximum certification path length for the preparation step. + */ + private function verifyMaxPathLength(ValidatorState $state, Certificate $cert): ValidatorState + { + if (! $cert->isSelfIssued()) { + if ($state->maxPathLength() <= 0) { + throw new PathValidationException('Certification path length exceeded.'); + } + $state = $state->withMaxPathLength($state->maxPathLength() - 1); + } + return $state; + } + + /** + * Check key usage extension for the preparation step. + */ + private function checkKeyUsage(Certificate $cert): void + { + $extensions = $cert->tbsCertificate() + ->extensions(); + if ($extensions->hasKeyUsage()) { + $ext = $extensions->keyUsage(); + if (! $ext->isKeyCertSign()) { + throw new PathValidationException('keyCertSign usage not set.'); + } + } + } + + private function processNameConstraints(ValidatorState $state): ValidatorState + { + // @todo Implement + return $state; + } + + /** + * Process basic constraints extension. + */ + private function processBasicContraints(Certificate $cert): void + { + if ($cert->tbsCertificate()->version() === TBSCertificate::VERSION_3) { + $extensions = $cert->tbsCertificate() + ->extensions(); + if (! $extensions->hasBasicConstraints()) { + throw new PathValidationException('v3 certificate must have basicConstraints extension.'); + } + // verify that cA is set to TRUE + if (! $extensions->basicConstraints()->isCA()) { + throw new PathValidationException('Certificate is not a CA certificate.'); + } + } + } + + /** + * Process pathLenConstraint. + */ + private function processPathLengthContraint(ValidatorState $state, Certificate $cert): ValidatorState + { + $extensions = $cert->tbsCertificate() + ->extensions(); + if ($extensions->hasBasicConstraints()) { + $ext = $extensions->basicConstraints(); + if ($ext->hasPathLen()) { + if ($ext->pathLen() < $state->maxPathLength()) { + $state = $state->withMaxPathLength($ext->pathLen()); + } + } + } + return $state; + } + + private function processExtensions(ValidatorState $state): ValidatorState + { + // @todo Implement + return $state; + } + + private function calculatePolicyIntersection(ValidatorState $state): ValidatorState + { + // (i) If the valid_policy_tree is NULL, the intersection is NULL + if (! $state->hasValidPolicyTree()) { + return $state; + } + // (ii) If the valid_policy_tree is not NULL and + // the user-initial-policy-set is any-policy, the intersection + // is the entire valid_policy_tree + $initial_policies = $this->config->policySet(); + if (in_array(PolicyInformation::OID_ANY_POLICY, $initial_policies, true)) { + return $state; + } + // (iii) If the valid_policy_tree is not NULL and the + // user-initial-policy-set is not any-policy, calculate + // the intersection of the valid_policy_tree and the + // user-initial-policy-set as follows + return $state->validPolicyTree() + ->calculateIntersection($state, $initial_policies); + } +} diff --git a/3rdparty/spomky-labs/pki-framework/src/X509/CertificationPath/PathValidation/ValidatorState.php b/3rdparty/spomky-labs/pki-framework/src/X509/CertificationPath/PathValidation/ValidatorState.php new file mode 100644 index 00000000..7f83b1d7 --- /dev/null +++ b/3rdparty/spomky-labs/pki-framework/src/X509/CertificationPath/PathValidation/ValidatorState.php @@ -0,0 +1,379 @@ +_pathLength = $n; + $state->_index = 1; + $state->_validPolicyTree = PolicyTree::create(PolicyNode::anyPolicyNode()); + $state->_permittedSubtrees = null; + $state->_excludedSubtrees = null; + $state->_explicitPolicy = $config->explicitPolicy() ? 0 : $n + 1; + $state->_inhibitAnyPolicy = $config->anyPolicyInhibit() ? 0 : $n + 1; + $state->_policyMapping = $config->policyMappingInhibit() ? 0 : $n + 1; + $state->_workingPublicKeyAlgorithm = $trust_anchor->signatureAlgorithm(); + $tbsCert = $trust_anchor->tbsCertificate(); + $state->_workingPublicKey = $tbsCert->subjectPublicKeyInfo(); + $state->_workingPublicKeyParameters = self::getAlgorithmParameters( + $state->_workingPublicKey->algorithmIdentifier() + ); + $state->_workingIssuerName = $tbsCert->issuer(); + $state->_maxPathLength = $config->maxLength(); + return $state; + } + + /** + * Get self with current certification path index set. + */ + public function withIndex(int $index): self + { + $state = clone $this; + $state->_index = $index; + return $state; + } + + /** + * Get self with valid_policy_tree. + */ + public function withValidPolicyTree(PolicyTree $policy_tree): self + { + $state = clone $this; + $state->_validPolicyTree = $policy_tree; + return $state; + } + + /** + * Get self with valid_policy_tree set to null. + */ + public function withoutValidPolicyTree(): self + { + $state = clone $this; + $state->_validPolicyTree = null; + return $state; + } + + /** + * Get self with explicit_policy. + */ + public function withExplicitPolicy(int $num): self + { + $state = clone $this; + $state->_explicitPolicy = $num; + return $state; + } + + /** + * Get self with inhibit_anyPolicy. + */ + public function withInhibitAnyPolicy(int $num): self + { + $state = clone $this; + $state->_inhibitAnyPolicy = $num; + return $state; + } + + /** + * Get self with policy_mapping. + */ + public function withPolicyMapping(int $num): self + { + $state = clone $this; + $state->_policyMapping = $num; + return $state; + } + + /** + * Get self with working_public_key_algorithm. + */ + public function withWorkingPublicKeyAlgorithm(AlgorithmIdentifierType $algo): self + { + $state = clone $this; + $state->_workingPublicKeyAlgorithm = $algo; + return $state; + } + + /** + * Get self with working_public_key. + */ + public function withWorkingPublicKey(PublicKeyInfo $pubkey_info): self + { + $state = clone $this; + $state->_workingPublicKey = $pubkey_info; + return $state; + } + + /** + * Get self with working_public_key_parameters. + */ + public function withWorkingPublicKeyParameters(?Element $params = null): self + { + $state = clone $this; + $state->_workingPublicKeyParameters = $params; + return $state; + } + + /** + * Get self with working_issuer_name. + */ + public function withWorkingIssuerName(Name $issuer): self + { + $state = clone $this; + $state->_workingIssuerName = $issuer; + return $state; + } + + /** + * Get self with max_path_length. + */ + public function withMaxPathLength(int $length): self + { + $state = clone $this; + $state->_maxPathLength = $length; + return $state; + } + + /** + * Get the certification path length (n). + */ + public function pathLength(): int + { + return $this->_pathLength; + } + + /** + * Get the current index in certification path in the range of 1..n. + */ + public function index(): int + { + return $this->_index; + } + + /** + * Check whether valid_policy_tree is present. + */ + public function hasValidPolicyTree(): bool + { + return isset($this->_validPolicyTree); + } + + public function validPolicyTree(): PolicyTree + { + if (! $this->hasValidPolicyTree()) { + throw new LogicException('valid_policy_tree not set.'); + } + return $this->_validPolicyTree; + } + + public function permittedSubtrees(): mixed + { + return $this->_permittedSubtrees; + } + + public function excludedSubtrees(): mixed + { + return $this->_excludedSubtrees; + } + + public function explicitPolicy(): int + { + return $this->_explicitPolicy; + } + + public function inhibitAnyPolicy(): int + { + return $this->_inhibitAnyPolicy; + } + + public function policyMapping(): int + { + return $this->_policyMapping; + } + + public function workingPublicKeyAlgorithm(): AlgorithmIdentifierType + { + return $this->_workingPublicKeyAlgorithm; + } + + public function workingPublicKey(): PublicKeyInfo + { + return $this->_workingPublicKey; + } + + public function workingPublicKeyParameters(): ?Element + { + return $this->_workingPublicKeyParameters; + } + + public function workingIssuerName(): Name + { + return $this->_workingIssuerName; + } + + /** + * Get maximum certification path length. + */ + public function maxPathLength(): int + { + return $this->_maxPathLength; + } + + /** + * Check whether processing the final certificate of the certification path. + */ + public function isFinal(): bool + { + return $this->_index === $this->_pathLength; + } + + /** + * Get the path validation result. + * + * @param Certificate[] $certificates Certificates in a certification path + */ + public function getResult(array $certificates): PathValidationResult + { + return PathValidationResult::create( + $certificates, + $this->_validPolicyTree, + $this->_workingPublicKey, + $this->_workingPublicKeyAlgorithm, + $this->_workingPublicKeyParameters + ); + } + + /** + * Get ASN.1 parameters from algorithm identifier. + * + * @return null|Element ASN.1 element or null if parameters are omitted + */ + public static function getAlgorithmParameters(AlgorithmIdentifierType $algo): ?Element + { + $seq = $algo->toASN1(); + return $seq->has(1) ? $seq->at(1) + ->asElement() : null; + } +} diff --git a/3rdparty/spomky-labs/pki-framework/src/X509/CertificationPath/Policy/PolicyNode.php b/3rdparty/spomky-labs/pki-framework/src/X509/CertificationPath/Policy/PolicyNode.php new file mode 100644 index 00000000..f355276b --- /dev/null +++ b/3rdparty/spomky-labs/pki-framework/src/X509/CertificationPath/Policy/PolicyNode.php @@ -0,0 +1,242 @@ +children = []; + } + + /** + * @param PolicyQualifierInfo[] $qualifiers + * @param string[] $expectedPolicies + */ + public static function create(string $validPolicy, array $qualifiers, array $expectedPolicies): self + { + return new self($validPolicy, $qualifiers, $expectedPolicies); + } + + /** + * Create initial node for the policy tree. + */ + public static function anyPolicyNode(): self + { + return self::create(PolicyInformation::OID_ANY_POLICY, [], [PolicyInformation::OID_ANY_POLICY]); + } + + /** + * Get the valid policy OID. + */ + public function validPolicy(): string + { + return $this->validPolicy; + } + + /** + * Check whether node has anyPolicy as a valid policy. + */ + public function isAnyPolicy(): bool + { + return $this->validPolicy === PolicyInformation::OID_ANY_POLICY; + } + + /** + * Get the qualifier set. + * + * @return PolicyQualifierInfo[] + */ + public function qualifiers(): array + { + return $this->qualifiers; + } + + /** + * Check whether node has OID as an expected policy. + */ + public function hasExpectedPolicy(string $oid): bool + { + return in_array($oid, $this->expectedPolicies, true); + } + + /** + * Get the expected policy set. + * + * @return string[] + */ + public function expectedPolicies(): array + { + return $this->expectedPolicies; + } + + /** + * Set expected policies. + * + * @param string ...$oids Policy OIDs + */ + public function setExpectedPolicies(string ...$oids): void + { + $this->expectedPolicies = $oids; + } + + /** + * Check whether node has a child node with given valid policy OID. + */ + public function hasChildWithValidPolicy(string $oid): bool + { + foreach ($this->children as $node) { + if ($node->validPolicy() === $oid) { + return true; + } + } + return false; + } + + /** + * Add child node. + */ + public function addChild(self $node): self + { + $id = spl_object_hash($node); + $node->parent = $this; + $this->children[$id] = $node; + return $this; + } + + /** + * Get the child nodes. + * + * @return PolicyNode[] + */ + public function children(): array + { + return array_values($this->children); + } + + /** + * Remove this node from the tree. + * + * @return self The removed node + */ + public function remove(): self + { + if ($this->parent !== null) { + $id = spl_object_hash($this); + unset($this->parent->children[$id], $this->parent); + } + return $this; + } + + /** + * Check whether node has a parent. + */ + public function hasParent(): bool + { + return isset($this->parent); + } + + /** + * Get the parent node. + */ + public function parent(): ?self + { + return $this->parent; + } + + /** + * Get chain of parent nodes from this node's parent to the root node. + * + * @return PolicyNode[] + */ + public function parents(): array + { + if ($this->parent === null) { + return []; + } + $nodes = $this->parent->parents(); + $nodes[] = $this->parent; + return array_reverse($nodes); + } + + /** + * Walk tree from this node, applying a callback for each node. + * + * Nodes are traversed depth-first and callback shall be applied post-order. + */ + public function walkNodes(callable $fn): void + { + foreach ($this->children as $node) { + $node->walkNodes($fn); + } + $fn($this); + } + + /** + * Get the total number of nodes in a tree. + */ + public function nodeCount(): int + { + $c = 1; + foreach ($this->children as $child) { + $c += $child->nodeCount(); + } + return $c; + } + + /** + * Get the number of child nodes. + * + * @see \Countable::count() + */ + public function count(): int + { + return count($this->children); + } + + /** + * Get iterator for the child nodes. + * + * @see \IteratorAggregate::getIterator() + */ + public function getIterator(): ArrayIterator + { + return new ArrayIterator($this->children); + } +} diff --git a/3rdparty/spomky-labs/pki-framework/src/X509/CertificationPath/Policy/PolicyTree.php b/3rdparty/spomky-labs/pki-framework/src/X509/CertificationPath/Policy/PolicyTree.php new file mode 100644 index 00000000..56ee3550 --- /dev/null +++ b/3rdparty/spomky-labs/pki-framework/src/X509/CertificationPath/Policy/PolicyTree.php @@ -0,0 +1,389 @@ +tbsCertificate() + ->extensions() + ->certificatePolicies(); + $tree = clone $this; + // (d.1) for each policy P not equal to anyPolicy + foreach ($policies as $policy) { + /** @var PolicyInformation $policy */ + if ($policy->isAnyPolicy()) { + $tree->processAnyPolicy($policy, $cert, $state); + } else { + $tree->processPolicy($policy, $state); + } + } + // if whole tree is pruned + if ($tree->pruneTree($state->index() - 1) === 0) { + return $state->withoutValidPolicyTree(); + } + return $state->withValidPolicyTree($tree); + } + + /** + * Process policy mappings from the certificate. + */ + public function processMappings(ValidatorState $state, Certificate $cert): ValidatorState + { + $tree = clone $this; + if ($state->policyMapping() > 0) { + $tree->_applyMappings($cert, $state); + } elseif ($state->policyMapping() === 0) { + $tree->_deleteMappings($cert, $state); + } + // if whole tree is pruned + if ($tree->root === null) { + return $state->withoutValidPolicyTree(); + } + return $state->withValidPolicyTree($tree); + } + + /** + * Calculate policy intersection as specified in Wrap-Up Procedure 6.1.5.g. + * + * @param array $policies + */ + public function calculateIntersection(ValidatorState $state, array $policies): ValidatorState + { + $tree = clone $this; + $valid_policy_node_set = $tree->validPolicyNodeSet(); + // 2. If the valid_policy of any node in the valid_policy_node_set + // is not in the user-initial-policy-set and is not anyPolicy, + // delete this node and all its children. + $valid_policy_node_set = array_filter( + $valid_policy_node_set, + function (PolicyNode $node) use ($policies) { + if ($node->isAnyPolicy()) { + return true; + } + if (in_array($node->validPolicy(), $policies, true)) { + return true; + } + $node->remove(); + return false; + } + ); + // array of valid policy OIDs + $valid_policy_set = array_map(static fn (PolicyNode $node) => $node->validPolicy(), $valid_policy_node_set); + // 3. If the valid_policy_tree includes a node of depth n with + // the valid_policy anyPolicy and the user-initial-policy-set + // is not any-policy + foreach ($tree->nodesAtDepth($state->index()) as $node) { + if ($node->hasParent() && $node->isAnyPolicy()) { + // a. Set P-Q to the qualifier_set in the node of depth n + // with valid_policy anyPolicy. + $pq = $node->qualifiers(); + // b. For each P-OID in the user-initial-policy-set that is not + // the valid_policy of a node in the valid_policy_node_set, + // create a child node whose parent is the node of depth n-1 + // with the valid_policy anyPolicy. + $poids = array_diff($policies, $valid_policy_set); + foreach ($tree->nodesAtDepth($state->index() - 1) as $parent) { + if ($parent->isAnyPolicy()) { + // Set the values in the child node as follows: + // set the valid_policy to P-OID, set the qualifier_set + // to P-Q, and set the expected_policy_set to {P-OID}. + foreach ($poids as $poid) { + $parent->addChild(PolicyNode::create($poid, $pq, [$poid])); + } + break; + } + } + // c. Delete the node of depth n with the + // valid_policy anyPolicy. + $node->remove(); + } + } + // 4. If there is a node in the valid_policy_tree of depth n-1 or less + // without any child nodes, delete that node. Repeat this step until + // there are no nodes of depth n-1 or less without children. + if ($tree->pruneTree($state->index() - 1) === 0) { + return $state->withoutValidPolicyTree(); + } + return $state->withValidPolicyTree($tree); + } + + /** + * Get policies at given policy tree depth. + * + * @param int $i Depth in range 1..n + * + * @return PolicyInformation[] + */ + public function policiesAtDepth(int $i): array + { + $policies = []; + foreach ($this->nodesAtDepth($i) as $node) { + $policies[] = PolicyInformation::create($node->validPolicy(), ...$node->qualifiers()); + } + return $policies; + } + + /** + * Process single policy information. + */ + private function processPolicy(PolicyInformation $policy, ValidatorState $state): void + { + $p_oid = $policy->oid(); + $i = $state->index(); + $match_count = 0; + // (d.1.i) for each node of depth i-1 in the valid_policy_tree... + foreach ($this->nodesAtDepth($i - 1) as $node) { + // ...where P-OID is in the expected_policy_set + if ($node->hasExpectedPolicy($p_oid)) { + $node->addChild(PolicyNode::create($p_oid, $policy->qualifiers(), [$p_oid])); + ++$match_count; + } + } + // (d.1.ii) if there was no match in step (i)... + if ($match_count === 0) { + // ...and the valid_policy_tree includes a node of depth i-1 with + // the valid_policy anyPolicy + foreach ($this->nodesAtDepth($i - 1) as $node) { + if ($node->isAnyPolicy()) { + $node->addChild(PolicyNode::create($p_oid, $policy->qualifiers(), [$p_oid])); + } + } + } + } + + /** + * Process anyPolicy policy information. + */ + private function processAnyPolicy(PolicyInformation $policy, Certificate $cert, ValidatorState $state): void + { + $i = $state->index(); + // if (a) inhibit_anyPolicy is greater than 0 or + // (b) iinhibitAnyPolicy() > 0 || + ($i < $state->pathLength() && $cert->isSelfIssued()))) { + return; + } + // for each node in the valid_policy_tree of depth i-1 + foreach ($this->nodesAtDepth($i - 1) as $node) { + // for each value in the expected_policy_set + foreach ($node->expectedPolicies() as $p_oid) { + // that does not appear in a child node + if (! $node->hasChildWithValidPolicy($p_oid)) { + $node->addChild(PolicyNode::create($p_oid, $policy->qualifiers(), [$p_oid])); + } + } + } + } + + /** + * Apply policy mappings to the policy tree. + */ + private function _applyMappings(Certificate $cert, ValidatorState $state): void + { + $policy_mappings = $cert->tbsCertificate() + ->extensions() + ->policyMappings(); + // (6.1.4. b.1.) for each node in the valid_policy_tree of depth i... + foreach ($policy_mappings->flattenedMappings() as $idp => $sdps) { + $match_count = 0; + foreach ($this->nodesAtDepth($state->index()) as $node) { + // ...where ID-P is the valid_policy + if ($node->validPolicy() === $idp) { + // set expected_policy_set to the set of subjectDomainPolicy + // values that are specified as equivalent to ID-P by + // the policy mappings extension + $node->setExpectedPolicies(...$sdps); + ++$match_count; + } + } + // if no node of depth i in the valid_policy_tree has + // a valid_policy of ID-P... + if ($match_count === 0) { + $this->_applyAnyPolicyMapping($cert, $state, $idp, $sdps); + } + } + } + + /** + * Apply anyPolicy mapping to the policy tree as specified in 6.1.4 (b)(1). + * + * @param string $idp OID of the issuer domain policy + * @param array $sdps Array of subject domain policy OIDs + */ + private function _applyAnyPolicyMapping( + Certificate $cert, + ValidatorState $state, + string $idp, + array $sdps + ): void { + // (6.1.4. b.1.) ...but there is a node of depth i with + // a valid_policy of anyPolicy + foreach ($this->nodesAtDepth($state->index()) as $node) { + if ($node->isAnyPolicy()) { + // then generate a child node of the node of depth i-1 + // that has a valid_policy of anyPolicy as follows... + foreach ($this->nodesAtDepth($state->index() - 1) as $subnode) { + if ($subnode->isAnyPolicy()) { + // try to fetch qualifiers of anyPolicy certificate policy + try { + $qualifiers = $cert->tbsCertificate() + ->extensions() + ->certificatePolicies() + ->anyPolicy() + ->qualifiers(); + } catch (LogicException) { + // if there's no policies or no qualifiers + $qualifiers = []; + } + $subnode->addChild(PolicyNode::create($idp, $qualifiers, $sdps)); + // bail after first anyPolicy has been processed + break; + } + } + // bail after first anyPolicy has been processed + break; + } + } + } + + /** + * Delete nodes as specified in 6.1.4 (b)(2). + */ + private function _deleteMappings(Certificate $cert, ValidatorState $state): void + { + $idps = $cert->tbsCertificate() + ->extensions() + ->policyMappings() + ->issuerDomainPolicies(); + // delete each node of depth i in the valid_policy_tree + // where ID-P is the valid_policy + foreach ($this->nodesAtDepth($state->index()) as $node) { + if (in_array($node->validPolicy(), $idps, true)) { + $node->remove(); + } + } + $this->pruneTree($state->index() - 1); + } + + /** + * Prune tree starting from given depth. + * + * @return int The number of nodes left in a tree + */ + private function pruneTree(int $depth): int + { + if ($this->root === null) { + return 0; + } + for ($i = $depth; $i > 0; --$i) { + foreach ($this->nodesAtDepth($i) as $node) { + if (count($node) === 0) { + $node->remove(); + } + } + } + // if root has no children left + if (count($this->root) === 0) { + $this->root = null; + return 0; + } + return $this->root->nodeCount(); + } + + /** + * Get all nodes at given depth. + * + * @return PolicyNode[] + */ + private function nodesAtDepth(int $i): array + { + if ($this->root === null) { + return []; + } + $depth = 0; + $nodes = [$this->root]; + while ($depth < $i) { + $nodes = self::gatherChildren(...$nodes); + if (count($nodes) === 0) { + break; + } + ++$depth; + } + return $nodes; + } + + /** + * Get the valid policy node set as specified in spec 6.1.5.(g)(iii)1. + * + * @return PolicyNode[] + */ + private function validPolicyNodeSet(): array + { + // 1. Determine the set of policy nodes whose parent nodes have + // a valid_policy of anyPolicy. This is the valid_policy_node_set. + $set = []; + if ($this->root === null) { + return $set; + } + // for each node in a tree + $this->root->walkNodes( + function (PolicyNode $node) use (&$set) { + $parents = $node->parents(); + // node has parents + if (count($parents) !== 0) { + // check that each ancestor is an anyPolicy node + foreach ($parents as $ancestor) { + if (! $ancestor->isAnyPolicy()) { + return; + } + } + $set[] = $node; + } + } + ); + return $set; + } + + /** + * Gather all children of given nodes to a flattened array. + * + * @return PolicyNode[] + */ + private static function gatherChildren(PolicyNode ...$nodes): array + { + $children = []; + foreach ($nodes as $node) { + $children = array_merge($children, $node->children()); + } + return $children; + } +} diff --git a/3rdparty/spomky-labs/pki-framework/src/X509/CertificationRequest/Attribute/ExtensionRequestValue.php b/3rdparty/spomky-labs/pki-framework/src/X509/CertificationRequest/Attribute/ExtensionRequestValue.php new file mode 100644 index 00000000..1b9e6868 --- /dev/null +++ b/3rdparty/spomky-labs/pki-framework/src/X509/CertificationRequest/Attribute/ExtensionRequestValue.php @@ -0,0 +1,77 @@ +asSequence())); + } + + /** + * Get requested extensions. + */ + public function extensions(): Extensions + { + return $this->extensions; + } + + public function toASN1(): Element + { + return $this->extensions->toASN1(); + } + + public function stringValue(): string + { + return '#' . bin2hex($this->toASN1()->toDER()); + } + + public function equalityMatchingRule(): MatchingRule + { + return new BinaryMatch(); + } + + public function rfc2253String(): string + { + return $this->stringValue(); + } + + protected function _transcodedString(): string + { + return $this->stringValue(); + } +} diff --git a/3rdparty/spomky-labs/pki-framework/src/X509/CertificationRequest/Attributes.php b/3rdparty/spomky-labs/pki-framework/src/X509/CertificationRequest/Attributes.php new file mode 100644 index 00000000..9df71744 --- /dev/null +++ b/3rdparty/spomky-labs/pki-framework/src/X509/CertificationRequest/Attributes.php @@ -0,0 +1,68 @@ + + */ + private const MAP_OID_TO_CLASS = [ + ExtensionRequestValue::OID => ExtensionRequestValue::class, + ]; + + /** + * Initialize from attribute values. + * + * @param AttributeValue ...$values List of attribute values + */ + public static function fromAttributeValues(AttributeValue ...$values): static + { + return static::create(...array_map(static fn (AttributeValue $value) => $value->toAttribute(), $values)); + } + + /** + * Check whether extension request attribute is present. + */ + public function hasExtensionRequest(): bool + { + return $this->has(ExtensionRequestValue::OID); + } + + /** + * Get extension request attribute value. + */ + public function extensionRequest(): ExtensionRequestValue + { + if (! $this->hasExtensionRequest()) { + throw new LogicException('No extension request attribute.'); + } + return $this->firstOf(ExtensionRequestValue::OID)->first(); + } + + protected static function _castAttributeValues(Attribute $attribute): Attribute + { + $oid = $attribute->oid(); + if (isset(self::MAP_OID_TO_CLASS[$oid])) { + return $attribute->castValues(self::MAP_OID_TO_CLASS[$oid]); + } + return $attribute; + } +} diff --git a/3rdparty/spomky-labs/pki-framework/src/X509/CertificationRequest/CertificationRequest.php b/3rdparty/spomky-labs/pki-framework/src/X509/CertificationRequest/CertificationRequest.php new file mode 100644 index 00000000..300385e8 --- /dev/null +++ b/3rdparty/spomky-labs/pki-framework/src/X509/CertificationRequest/CertificationRequest.php @@ -0,0 +1,146 @@ +toPEM() + ->string(); + } + + public static function create( + CertificationRequestInfo $_certificationRequestInfo, + SignatureAlgorithmIdentifier $_signatureAlgorithm, + Signature $_signature + ): self { + return new self($_certificationRequestInfo, $_signatureAlgorithm, $_signature); + } + + /** + * Initialize from ASN.1. + */ + public static function fromASN1(Sequence $seq): self + { + $info = CertificationRequestInfo::fromASN1($seq->at(0)->asSequence()); + $algo = AlgorithmIdentifier::fromASN1($seq->at(1)->asSequence()); + if (! $algo instanceof SignatureAlgorithmIdentifier) { + throw new UnexpectedValueException('Unsupported signature algorithm ' . $algo->oid() . '.'); + } + $signature = Signature::fromSignatureData($seq->at(2)->asBitString()->string(), $algo); + return self::create($info, $algo, $signature); + } + + /** + * Initialize from DER. + */ + public static function fromDER(string $data): self + { + return self::fromASN1(UnspecifiedType::fromDER($data)->asSequence()); + } + + /** + * Initialize from PEM. + */ + public static function fromPEM(PEM $pem): self + { + if ($pem->type() !== PEM::TYPE_CERTIFICATE_REQUEST) { + throw new UnexpectedValueException('Invalid PEM type.'); + } + return self::fromDER($pem->data()); + } + + /** + * Get certification request info. + */ + public function certificationRequestInfo(): CertificationRequestInfo + { + return $this->certificationRequestInfo; + } + + /** + * Get signature algorithm. + */ + public function signatureAlgorithm(): SignatureAlgorithmIdentifier + { + return $this->signatureAlgorithm; + } + + public function signature(): Signature + { + return $this->signature; + } + + /** + * Generate ASN.1 structure. + */ + public function toASN1(): Sequence + { + return Sequence::create( + $this->certificationRequestInfo->toASN1(), + $this->signatureAlgorithm->toASN1(), + $this->signature->bitString() + ); + } + + /** + * Get certification request as a DER. + */ + public function toDER(): string + { + return $this->toASN1() + ->toDER(); + } + + /** + * Get certification request as a PEM. + */ + public function toPEM(): PEM + { + return PEM::create(PEM::TYPE_CERTIFICATE_REQUEST, $this->toDER()); + } + + /** + * Verify certification request signature. + * + * @param null|Crypto $crypto Crypto engine, use default if not set + * + * @return bool True if signature matches + */ + public function verify(?Crypto $crypto = null): bool + { + $crypto ??= Crypto::getDefault(); + $data = $this->certificationRequestInfo->toASN1() + ->toDER(); + $pk_info = $this->certificationRequestInfo->subjectPKInfo(); + return $crypto->verify($data, $this->signature, $pk_info, $this->signatureAlgorithm); + } +} diff --git a/3rdparty/spomky-labs/pki-framework/src/X509/CertificationRequest/CertificationRequestInfo.php b/3rdparty/spomky-labs/pki-framework/src/X509/CertificationRequest/CertificationRequestInfo.php new file mode 100644 index 00000000..b93584e2 --- /dev/null +++ b/3rdparty/spomky-labs/pki-framework/src/X509/CertificationRequest/CertificationRequestInfo.php @@ -0,0 +1,181 @@ +version = self::VERSION_1; + } + + public static function create(Name $subject, PublicKeyInfo $subjectPKInfo): self + { + return new self($subject, $subjectPKInfo); + } + + /** + * Initialize from ASN.1. + */ + public static function fromASN1(Sequence $seq): self + { + $version = $seq->at(0) + ->asInteger() + ->intNumber(); + if ($version !== self::VERSION_1) { + throw new UnexpectedValueException("Version {$version} not supported."); + } + $subject = Name::fromASN1($seq->at(1)->asSequence()); + $pkinfo = PublicKeyInfo::fromASN1($seq->at(2)->asSequence()); + $obj = self::create($subject, $pkinfo); + if ($seq->hasTagged(0)) { + $obj = $obj->withAttributes( + Attributes::fromASN1($seq->getTagged(0)->asImplicit(Element::TYPE_SET)->asSet()) + ); + } + + return $obj; + } + + public function version(): int + { + return $this->version; + } + + /** + * Get self with subject. + */ + public function withSubject(Name $subject): self + { + $obj = clone $this; + $obj->subject = $subject; + return $obj; + } + + public function subject(): Name + { + return $this->subject; + } + + /** + * Get subject public key info. + */ + public function subjectPKInfo(): PublicKeyInfo + { + return $this->subjectPKInfo; + } + + /** + * Whether certification request info has attributes. + */ + public function hasAttributes(): bool + { + return isset($this->attributes); + } + + public function attributes(): Attributes + { + if (! $this->hasAttributes()) { + throw new LogicException('No attributes.'); + } + return $this->attributes; + } + + /** + * Get instance of self with attributes. + */ + public function withAttributes(Attributes $attribs): self + { + $obj = clone $this; + $obj->attributes = $attribs; + return $obj; + } + + /** + * Get self with extension request attribute. + * + * @param Extensions $extensions Extensions to request + */ + public function withExtensionRequest(Extensions $extensions): self + { + $obj = clone $this; + if (! isset($obj->attributes)) { + $obj->attributes = Attributes::create(); + } + $obj->attributes = $obj->attributes->withUnique( + Attribute::fromAttributeValues(ExtensionRequestValue::create($extensions)) + ); + return $obj; + } + + /** + * Generate ASN.1 structure. + */ + public function toASN1(): Sequence + { + $elements = [Integer::create($this->version), $this->subject->toASN1(), $this->subjectPKInfo->toASN1()]; + if (isset($this->attributes)) { + $elements[] = ImplicitlyTaggedType::create(0, $this->attributes->toASN1()); + } + return Sequence::create(...$elements); + } + + /** + * Create signed CertificationRequest. + * + * @param SignatureAlgorithmIdentifier $algo Algorithm used for signing + * @param PrivateKeyInfo $privkey_info Private key used for signing + * @param null|Crypto $crypto Crypto engine, use default if not set + */ + public function sign( + SignatureAlgorithmIdentifier $algo, + PrivateKeyInfo $privkey_info, + ?Crypto $crypto = null + ): CertificationRequest { + $crypto ??= Crypto::getDefault(); + $data = $this->toASN1() + ->toDER(); + $signature = $crypto->sign($data, $privkey_info, $algo); + return CertificationRequest::create($this, $algo, $signature); + } +} diff --git a/3rdparty/spomky-labs/pki-framework/src/X509/Exception/X509ValidationException.php b/3rdparty/spomky-labs/pki-framework/src/X509/Exception/X509ValidationException.php new file mode 100644 index 00000000..83d49d88 --- /dev/null +++ b/3rdparty/spomky-labs/pki-framework/src/X509/Exception/X509ValidationException.php @@ -0,0 +1,11 @@ +format('Y-m-d H:i:s'), $dt->getTimezone()); + } + + /** + * Create DateTimeZone object from string. + */ + private static function createTimeZone(string $tz): DateTimeZone + { + try { + return new DateTimeZone($tz); + } catch (Exception $e) { + throw new UnexpectedValueException('Invalid timezone.', 0, $e); + } + } +} diff --git a/3rdparty/spomky-labs/pki-framework/src/X509/GeneralName/DNSName.php b/3rdparty/spomky-labs/pki-framework/src/X509/GeneralName/DNSName.php new file mode 100644 index 00000000..a71db11a --- /dev/null +++ b/3rdparty/spomky-labs/pki-framework/src/X509/GeneralName/DNSName.php @@ -0,0 +1,58 @@ +asIA5String()->string()); + } + + public function string(): string + { + return $this->name; + } + + /** + * Get DNS name. + */ + public function name(): string + { + return $this->name; + } + + protected function choiceASN1(): TaggedType + { + return ImplicitlyTaggedType::create($this->tag, IA5String::create($this->name)); + } +} diff --git a/3rdparty/spomky-labs/pki-framework/src/X509/GeneralName/DirectoryName.php b/3rdparty/spomky-labs/pki-framework/src/X509/GeneralName/DirectoryName.php new file mode 100644 index 00000000..cb1a4854 --- /dev/null +++ b/3rdparty/spomky-labs/pki-framework/src/X509/GeneralName/DirectoryName.php @@ -0,0 +1,65 @@ +asSequence())); + } + + /** + * Initialize from distinguished name string. + */ + public static function fromDNString(string $str): self + { + return self::create(Name::fromString($str)); + } + + public function string(): string + { + return $this->directoryName->toString(); + } + + /** + * Get directory name. + */ + public function dn(): Name + { + return $this->directoryName; + } + + protected function choiceASN1(): TaggedType + { + // Name type is itself a CHOICE, so explicit tagging must be + // employed to avoid ambiguities + return ExplicitlyTaggedType::create($this->tag, $this->directoryName->toASN1()); + } +} diff --git a/3rdparty/spomky-labs/pki-framework/src/X509/GeneralName/EDIPartyName.php b/3rdparty/spomky-labs/pki-framework/src/X509/GeneralName/EDIPartyName.php new file mode 100644 index 00000000..231c4d59 --- /dev/null +++ b/3rdparty/spomky-labs/pki-framework/src/X509/GeneralName/EDIPartyName.php @@ -0,0 +1,51 @@ +asSequence()); + } + + public function string(): string + { + return bin2hex($this->element->toDER()); + } + + protected function choiceASN1(): TaggedType + { + return ImplicitlyTaggedType::create($this->tag, $this->element); + } +} diff --git a/3rdparty/spomky-labs/pki-framework/src/X509/GeneralName/GeneralName.php b/3rdparty/spomky-labs/pki-framework/src/X509/GeneralName/GeneralName.php new file mode 100644 index 00000000..485f5957 --- /dev/null +++ b/3rdparty/spomky-labs/pki-framework/src/X509/GeneralName/GeneralName.php @@ -0,0 +1,119 @@ +string(); + } + + /** + * Get string value of the type. + */ + abstract public function string(): string; + + /** + * Initialize concrete object from the chosen ASN.1 element. + */ + abstract public static function fromChosenASN1(UnspecifiedType $el): self; + + /** + * Initialize from ASN.1. + */ + public static function fromASN1(TaggedType $el): self + { + return match ($el->tag()) { + self::TAG_OTHER_NAME => OtherName::fromChosenASN1($el->asImplicit(Element::TYPE_SEQUENCE)), + self::TAG_RFC822_NAME => RFC822Name::fromChosenASN1($el->asImplicit(Element::TYPE_IA5_STRING)), + self::TAG_DNS_NAME => DNSName::fromChosenASN1($el->asImplicit(Element::TYPE_IA5_STRING)), + self::TAG_X400_ADDRESS => X400Address::fromChosenASN1($el->asImplicit(Element::TYPE_SEQUENCE)), + self::TAG_DIRECTORY_NAME => DirectoryName::fromChosenASN1($el->asExplicit()), + self::TAG_EDI_PARTY_NAME => EDIPartyName::fromChosenASN1($el->asImplicit(Element::TYPE_SEQUENCE)), + self::TAG_URI => UniformResourceIdentifier::fromChosenASN1($el->asImplicit(Element::TYPE_IA5_STRING)), + self::TAG_IP_ADDRESS => IPAddress::fromChosenASN1($el->asImplicit(Element::TYPE_OCTET_STRING)), + self::TAG_REGISTERED_ID => RegisteredID::fromChosenASN1($el->asImplicit(Element::TYPE_OBJECT_IDENTIFIER)), + default => throw new UnexpectedValueException('GeneralName type ' . $el->tag() . ' not supported.'), + }; + } + + /** + * Get type tag. + */ + public function tag(): int + { + return $this->tag; + } + + /** + * Generate ASN.1 element. + */ + public function toASN1(): Element + { + return $this->choiceASN1(); + } + + /** + * Check whether GeneralName is equal to others. + * + * @param GeneralName $other GeneralName to compare to + * + * @return bool True if names are equal + */ + public function equals(self $other): bool + { + if ($this->tag !== $other->tag) { + return false; + } + if ($this->choiceASN1()->toDER() !== $other->choiceASN1()->toDER()) { + return false; + } + return true; + } + + /** + * Get ASN.1 value in GeneralName CHOICE context. + */ + abstract protected function choiceASN1(): TaggedType; +} diff --git a/3rdparty/spomky-labs/pki-framework/src/X509/GeneralName/GeneralNames.php b/3rdparty/spomky-labs/pki-framework/src/X509/GeneralName/GeneralNames.php new file mode 100644 index 00000000..1854f3d7 --- /dev/null +++ b/3rdparty/spomky-labs/pki-framework/src/X509/GeneralName/GeneralNames.php @@ -0,0 +1,174 @@ +_names = $names; + } + + public static function create(GeneralName ...$names): self + { + return new self(...$names); + } + + /** + * Initialize from ASN.1. + */ + public static function fromASN1(Sequence $seq): self + { + if (count($seq) === 0) { + throw new UnexpectedValueException('GeneralNames must have at least one GeneralName.'); + } + $names = array_map(static fn (UnspecifiedType $el) => GeneralName::fromASN1($el->asTagged()), $seq->elements()); + return self::create(...$names); + } + + /** + * Check whether GeneralNames contains a GeneralName of given type. + * + * @param int $tag One of `GeneralName::TAG_*` enumerations + */ + public function has(int $tag): bool + { + return $this->findFirst($tag) !== null; + } + + /** + * Get first GeneralName of given type. + * + * @param int $tag One of `GeneralName::TAG_*` enumerations + */ + public function firstOf(int $tag): GeneralName + { + $name = $this->findFirst($tag); + if ($name === null) { + throw new UnexpectedValueException("No GeneralName by tag {$tag}."); + } + return $name; + } + + /** + * Get all GeneralName objects of given type. + * + * @param int $tag One of `GeneralName::TAG_*` enumerations + * + * @return GeneralName[] + */ + public function allOf(int $tag): array + { + $names = array_filter($this->_names, fn (GeneralName $name) => $name->tag() === $tag); + return array_values($names); + } + + /** + * Get value of the first 'dNSName' type. + */ + public function firstDNS(): string + { + $gn = $this->firstOf(GeneralName::TAG_DNS_NAME); + if (! $gn instanceof DNSName) { + throw new RuntimeException(DNSName::class . ' expected, got ' . $gn::class); + } + return $gn->name(); + } + + /** + * Get value of the first 'directoryName' type. + */ + public function firstDN(): Name + { + $gn = $this->firstOf(GeneralName::TAG_DIRECTORY_NAME); + if (! $gn instanceof DirectoryName) { + throw new RuntimeException(DirectoryName::class . ' expected, got ' . $gn::class); + } + return $gn->dn(); + } + + /** + * Get value of the first 'uniformResourceIdentifier' type. + */ + public function firstURI(): string + { + $gn = $this->firstOf(GeneralName::TAG_URI); + if (! $gn instanceof UniformResourceIdentifier) { + throw new RuntimeException(UniformResourceIdentifier::class . ' expected, got ' . $gn::class); + } + return $gn->uri(); + } + + /** + * Generate ASN.1 structure. + */ + public function toASN1(): Sequence + { + if (count($this->_names) === 0) { + throw new LogicException('GeneralNames must have at least one GeneralName.'); + } + $elements = array_map(static fn (GeneralName $name) => $name->toASN1(), $this->_names); + return Sequence::create(...$elements); + } + + /** + * @see \Countable::count() + */ + public function count(): int + { + return count($this->_names); + } + + /** + * Get iterator for GeneralName objects. + * + * @see \IteratorAggregate::getIterator() + */ + public function getIterator(): ArrayIterator + { + return new ArrayIterator($this->_names); + } + + /** + * Find first GeneralName by given tag. + */ + private function findFirst(int $tag): ?GeneralName + { + foreach ($this->_names as $name) { + if ($name->tag() === $tag) { + return $name; + } + } + return null; + } +} diff --git a/3rdparty/spomky-labs/pki-framework/src/X509/GeneralName/IPAddress.php b/3rdparty/spomky-labs/pki-framework/src/X509/GeneralName/IPAddress.php new file mode 100644 index 00000000..38d44b5f --- /dev/null +++ b/3rdparty/spomky-labs/pki-framework/src/X509/GeneralName/IPAddress.php @@ -0,0 +1,86 @@ +asOctetString() + ->string(); + return match (mb_strlen($octets, '8bit')) { + 4, 8 => IPv4Address::fromOctets($octets), + 16, 32 => IPv6Address::fromOctets($octets), + default => throw new UnexpectedValueException('Invalid octet length for IP address.'), + }; + } + + public function string(): string + { + return $this->ip . (isset($this->mask) ? '/' . $this->mask : ''); + } + + /** + * Get IP address as a string. + */ + public function address(): string + { + return $this->ip; + } + + /** + * Check whether mask is present. + */ + public function hasMask(): bool + { + return isset($this->mask); + } + + /** + * Get subnet mask as a string. + */ + public function mask(): string + { + if (! $this->hasMask()) { + throw new LogicException('mask is not set.'); + } + return $this->mask; + } + + /** + * Get octet representation of the IP address. + */ + abstract protected function octets(): string; + + protected function choiceASN1(): TaggedType + { + return ImplicitlyTaggedType::create($this->tag, OctetString::create($this->octets())); + } +} diff --git a/3rdparty/spomky-labs/pki-framework/src/X509/GeneralName/IPv4Address.php b/3rdparty/spomky-labs/pki-framework/src/X509/GeneralName/IPv4Address.php new file mode 100644 index 00000000..a5563b8e --- /dev/null +++ b/3rdparty/spomky-labs/pki-framework/src/X509/GeneralName/IPv4Address.php @@ -0,0 +1,48 @@ +ip)); + if (isset($this->mask)) { + $bytes = array_merge($bytes, array_map('intval', explode('.', $this->mask))); + } + return pack('C*', ...$bytes); + } +} diff --git a/3rdparty/spomky-labs/pki-framework/src/X509/GeneralName/IPv6Address.php b/3rdparty/spomky-labs/pki-framework/src/X509/GeneralName/IPv6Address.php new file mode 100644 index 00000000..a79983a7 --- /dev/null +++ b/3rdparty/spomky-labs/pki-framework/src/X509/GeneralName/IPv6Address.php @@ -0,0 +1,59 @@ + sprintf('%04x', $word), $words); + return implode(':', $groups); + } + + protected function octets(): string + { + $words = array_map('hexdec', explode(':', $this->ip)); + if (isset($this->mask)) { + $words = array_merge($words, array_map('hexdec', explode(':', $this->mask))); + } + return pack('n*', ...$words); + } +} diff --git a/3rdparty/spomky-labs/pki-framework/src/X509/GeneralName/OtherName.php b/3rdparty/spomky-labs/pki-framework/src/X509/GeneralName/OtherName.php new file mode 100644 index 00000000..03f601ea --- /dev/null +++ b/3rdparty/spomky-labs/pki-framework/src/X509/GeneralName/OtherName.php @@ -0,0 +1,80 @@ +asSequence(); + $type_id = $seq->at(0) + ->asObjectIdentifier() + ->oid(); + $value = $seq->getTagged(0) + ->asExplicit() + ->asElement(); + return self::create($type_id, $value); + } + + public function string(): string + { + return $this->type . '/#' . bin2hex($this->element->toDER()); + } + + /** + * Get type OID. + */ + public function type(): string + { + return $this->type; + } + + /** + * Get value element. + */ + public function value(): Element + { + return $this->element; + } + + protected function choiceASN1(): TaggedType + { + return ImplicitlyTaggedType::create( + $this->tag, + Sequence::create(ObjectIdentifier::create($this->type), ExplicitlyTaggedType::create(0, $this->element)) + ); + } +} diff --git a/3rdparty/spomky-labs/pki-framework/src/X509/GeneralName/RFC822Name.php b/3rdparty/spomky-labs/pki-framework/src/X509/GeneralName/RFC822Name.php new file mode 100644 index 00000000..294d27b7 --- /dev/null +++ b/3rdparty/spomky-labs/pki-framework/src/X509/GeneralName/RFC822Name.php @@ -0,0 +1,52 @@ +asIA5String()->string()); + } + + public function string(): string + { + return $this->email; + } + + public function email(): string + { + return $this->email; + } + + protected function choiceASN1(): TaggedType + { + return ImplicitlyTaggedType::create($this->tag, IA5String::create($this->email)); + } +} diff --git a/3rdparty/spomky-labs/pki-framework/src/X509/GeneralName/RegisteredID.php b/3rdparty/spomky-labs/pki-framework/src/X509/GeneralName/RegisteredID.php new file mode 100644 index 00000000..a30e02a7 --- /dev/null +++ b/3rdparty/spomky-labs/pki-framework/src/X509/GeneralName/RegisteredID.php @@ -0,0 +1,60 @@ +asObjectIdentifier()->oid()); + } + + public function string(): string + { + return $this->oid; + } + + /** + * Get object identifier in dotted format. + * + * @return string OID + */ + public function oid(): string + { + return $this->oid; + } + + protected function choiceASN1(): TaggedType + { + return ImplicitlyTaggedType::create($this->tag, ObjectIdentifier::create($this->oid)); + } +} diff --git a/3rdparty/spomky-labs/pki-framework/src/X509/GeneralName/UniformResourceIdentifier.php b/3rdparty/spomky-labs/pki-framework/src/X509/GeneralName/UniformResourceIdentifier.php new file mode 100644 index 00000000..3b107a81 --- /dev/null +++ b/3rdparty/spomky-labs/pki-framework/src/X509/GeneralName/UniformResourceIdentifier.php @@ -0,0 +1,52 @@ +asIA5String()->string()); + } + + public function string(): string + { + return $this->uri; + } + + public function uri(): string + { + return $this->uri; + } + + protected function choiceASN1(): TaggedType + { + return ImplicitlyTaggedType::create($this->tag, IA5String::create($this->uri)); + } +} diff --git a/3rdparty/spomky-labs/pki-framework/src/X509/GeneralName/X400Address.php b/3rdparty/spomky-labs/pki-framework/src/X509/GeneralName/X400Address.php new file mode 100644 index 00000000..8b0476c2 --- /dev/null +++ b/3rdparty/spomky-labs/pki-framework/src/X509/GeneralName/X400Address.php @@ -0,0 +1,48 @@ +asSequence()); + } + + public function string(): string + { + return bin2hex($this->element->toDER()); + } + + protected function choiceASN1(): TaggedType + { + return ImplicitlyTaggedType::create($this->tag, $this->element); + } +} diff --git a/3rdparty/stecman/symfony-console-completion/LICENCE b/3rdparty/stecman/symfony-console-completion/LICENCE new file mode 100644 index 00000000..8f8e82c0 --- /dev/null +++ b/3rdparty/stecman/symfony-console-completion/LICENCE @@ -0,0 +1,21 @@ +The MIT License (MIT) + +Copyright (c) 2014 Stephen Holdaway + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. \ No newline at end of file diff --git a/3rdparty/stecman/symfony-console-completion/src/Completion.php b/3rdparty/stecman/symfony-console-completion/src/Completion.php new file mode 100644 index 00000000..f5adb45b --- /dev/null +++ b/3rdparty/stecman/symfony-console-completion/src/Completion.php @@ -0,0 +1,180 @@ +commandName = $commandName; + $this->targetName = $targetName; + $this->type = $type; + $this->completion = $completion; + } + + /** + * Return the stored completion, or the results returned from the completion callback + * + * @return array + */ + public function run() + { + if ($this->isCallable()) { + return call_user_func($this->completion); + } + + return $this->completion; + } + + /** + * Get type of input (option/argument) the completion should be run for + * + * @see CompletionInterface::ALL_TYPES + * @return string|null + */ + public function getType() + { + return $this->type; + } + + /** + * Set type of input (option/argument) the completion should be run for + * + * @see CompletionInterface::ALL_TYPES + * @param string|null $type + */ + public function setType($type) + { + $this->type = $type; + } + + /** + * Get the command name the completion should be run for + * + * @see CompletionInterface::ALL_COMMANDS + * @return string|null + */ + public function getCommandName() + { + return $this->commandName; + } + + /** + * Set the command name the completion should be run for + * + * @see CompletionInterface::ALL_COMMANDS + * @param string|null $commandName + */ + public function setCommandName($commandName) + { + $this->commandName = $commandName; + } + + /** + * Set the option/argument name the completion should be run for + * + * @see setType() + * @return string + */ + public function getTargetName() + { + return $this->targetName; + } + + /** + * Get the option/argument name the completion should be run for + * + * @see getType() + * @param string $targetName + */ + public function setTargetName($targetName) + { + $this->targetName = $targetName; + } + + /** + * Return the array or callback configured for for the Completion + * + * @return array|callable + */ + public function getCompletion() + { + return $this->completion; + } + + /** + * Set the array or callback to return/run when Completion is run + * + * @see run() + * @param array|callable $completion + */ + public function setCompletion($completion) + { + $this->completion = $completion; + } + + /** + * Check if the configured completion value is a callback function + * + * @return bool + */ + public function isCallable() + { + return is_callable($this->completion); + } +} diff --git a/3rdparty/stecman/symfony-console-completion/src/Completion/CompletionAwareInterface.php b/3rdparty/stecman/symfony-console-completion/src/Completion/CompletionAwareInterface.php new file mode 100644 index 00000000..20963cb8 --- /dev/null +++ b/3rdparty/stecman/symfony-console-completion/src/Completion/CompletionAwareInterface.php @@ -0,0 +1,27 @@ +commandName = $commandName; + $this->targetName = $targetName; + $this->type = $type; + } + + /** + * @inheritdoc + */ + public function getType() + { + return $this->type; + } + + /** + * @inheritdoc + */ + public function getCommandName() + { + return $this->commandName; + } + + /** + * @inheritdoc + */ + public function getTargetName() + { + return $this->targetName; + } + + /** + * Exit with a status code configured to defer completion to the shell + * + * @see \Stecman\Component\Symfony\Console\BashCompletion\HookFactory::$hooks + */ + public function run() + { + exit(self::PATH_COMPLETION_EXIT_CODE); + } +} diff --git a/3rdparty/stecman/symfony-console-completion/src/CompletionCommand.php b/3rdparty/stecman/symfony-console-completion/src/CompletionCommand.php new file mode 100644 index 00000000..ef0b3e48 --- /dev/null +++ b/3rdparty/stecman/symfony-console-completion/src/CompletionCommand.php @@ -0,0 +1,240 @@ +setName('_completion') + ->setDefinition($this->createDefinition()) + ->setDescription('BASH completion hook.') + ->setHelp(<<eval `[program] _completion -g`. + +Or for an alias: + + eval `[program] _completion -g -p [alias]`. + +END + ); + + // Hide this command from listing if supported + $this->setHidden(true); + } + + /** + * {@inheritdoc} + */ + public function getNativeDefinition(): InputDefinition + { + return $this->createDefinition(); + } + + /** + * Ignore user-defined global options + * + * Any global options defined by user-code are meaningless to this command. + * Options outside of the core defaults are ignored to avoid name and shortcut conflicts. + */ + public function mergeApplicationDefinition(bool $mergeArgs = true): void + { + // Get current application options + $appDefinition = $this->getApplication()->getDefinition(); + $originalOptions = $appDefinition->getOptions(); + + // Temporarily replace application options with a filtered list + $appDefinition->setOptions( + $this->filterApplicationOptions($originalOptions) + ); + + parent::mergeApplicationDefinition($mergeArgs); + + // Restore original application options + $appDefinition->setOptions($originalOptions); + } + + /** + * Reduce the passed list of options to the core defaults (if they exist) + * + * @param InputOption[] $appOptions + * @return InputOption[] + */ + protected function filterApplicationOptions(array $appOptions) + { + return array_filter($appOptions, function(InputOption $option) { + static $coreOptions = array( + 'help' => true, + 'quiet' => true, + 'verbose' => true, + 'version' => true, + 'ansi' => true, + 'no-ansi' => true, + 'no-interaction' => true, + ); + + return isset($coreOptions[$option->getName()]); + }); + } + + protected function execute(InputInterface $input, OutputInterface $output): int + { + $this->handler = new CompletionHandler($this->getApplication()); + $handler = $this->handler; + + if ($input->getOption('generate-hook')) { + global $argv; + $program = $argv[0]; + + $factory = new HookFactory(); + $alias = $input->getOption('program'); + $multiple = (bool)$input->getOption('multiple'); + + if (!$alias) { + $alias = basename($program); + } + + $hook = $factory->generateHook( + $input->getOption('shell-type') ?: $this->getShellType(), + $program, + $alias, + $multiple + ); + + $output->write($hook, true); + } else { + $handler->setContext(new EnvironmentCompletionContext()); + + // Get completion results + $results = $this->runCompletion(); + + // Escape results for the current shell + $shellType = $input->getOption('shell-type') ?: $this->getShellType(); + + foreach ($results as &$result) { + $result = $this->escapeForShell($result, $shellType); + } + + $output->write($results, true); + } + + return SymfonyCommand::SUCCESS; + } + + /** + * Escape each completion result for the specified shell + * + * @param string $result - Completion results that should appear in the shell + * @param string $shellType - Valid shell type from HookFactory + * @return string + */ + protected function escapeForShell($result, $shellType) + { + switch ($shellType) { + // BASH requires special escaping for multi-word and special character results + // This emulates registering completion with`-o filenames`, without side-effects like dir name slashes + case 'bash': + $context = $this->handler->getContext(); + $wordStart = substr($context->getRawCurrentWord(), 0, 1); + + if ($wordStart == "'") { + // If the current word is single-quoted, escape any single quotes in the result + $result = str_replace("'", "\\'", $result); + } else if ($wordStart == '"') { + // If the current word is double-quoted, escape any double quotes in the result + $result = str_replace('"', '\\"', $result); + } else { + // Otherwise assume the string is unquoted and word breaks should be escaped + $result = preg_replace('/([\s\'"\\\\])/', '\\\\$1', $result); + } + + // Escape output to prevent special characters being lost when passing results to compgen + return escapeshellarg($result); + + // No transformation by default + default: + return $result; + } + } + + /** + * Run the completion handler and return a filtered list of results + * + * @deprecated - This will be removed in 1.0.0 in favour of CompletionCommand::configureCompletion + * + * @return string[] + */ + protected function runCompletion() + { + $this->configureCompletion($this->handler); + return $this->handler->runCompletion(); + } + + /** + * Configure the CompletionHandler instance before it is run + * + * @param CompletionHandler $handler + */ + protected function configureCompletion(CompletionHandler $handler) + { + // Override this method to configure custom value completions + } + + /** + * Determine the shell type for use with HookFactory + * + * @return string + */ + protected function getShellType() + { + if (!getenv('SHELL')) { + throw new \RuntimeException('Could not read SHELL environment variable. Please specify your shell type using the --shell-type option.'); + } + + return basename(getenv('SHELL')); + } + + protected function createDefinition() + { + return new InputDefinition(array( + new InputOption( + 'generate-hook', + 'g', + InputOption::VALUE_NONE, + 'Generate BASH code that sets up completion for this application.' + ), + new InputOption( + 'program', + 'p', + InputOption::VALUE_REQUIRED, + "Program name that should trigger completion\n(defaults to the absolute application path)." + ), + new InputOption( + 'multiple', + 'm', + InputOption::VALUE_NONE, + "Generated hook can be used for multiple applications." + ), + new InputOption( + 'shell-type', + null, + InputOption::VALUE_OPTIONAL, + 'Set the shell type (zsh or bash). Otherwise this is determined automatically.' + ), + )); + } +} diff --git a/3rdparty/stecman/symfony-console-completion/src/CompletionContext.php b/3rdparty/stecman/symfony-console-completion/src/CompletionContext.php new file mode 100644 index 00000000..f09ab9b4 --- /dev/null +++ b/3rdparty/stecman/symfony-console-completion/src/CompletionContext.php @@ -0,0 +1,390 @@ +commandLine + * + * Bash equivalent: COMP_POINT + * + * @var int + */ + protected $charIndex = 0; + + /** + * An array of the individual words in the current command line. + * + * This is not set until $this->splitCommand() is called, when it is populated by + * $commandLine exploded by $wordBreaks + * + * Bash equivalent: COMP_WORDS + * + * @var string[]|null + */ + protected $words = null; + + /** + * Words from the currently command-line before quotes and escaping is processed + * + * This is indexed the same as $this->words, but in their raw input terms are in their input form, including + * quotes and escaping. + * + * @var string[]|null + */ + protected $rawWords = null; + + /** + * The index in $this->words containing the word at the current cursor position. + * + * This is not set until $this->splitCommand() is called. + * + * Bash equivalent: COMP_CWORD + * + * @var int|null + */ + protected $wordIndex = null; + + /** + * Characters that $this->commandLine should be split on to get a list of individual words + * + * Bash equivalent: COMP_WORDBREAKS + * + * @var string + */ + protected $wordBreaks = "= \t\n"; + + /** + * Set the whole contents of the command line as a string + * + * @param string $commandLine + */ + public function setCommandLine($commandLine) + { + $this->commandLine = $commandLine; + $this->reset(); + } + + /** + * Return the current command line verbatim as a string + * + * @return string + */ + public function getCommandLine() + { + return $this->commandLine; + } + + /** + * Return the word from the command line that the cursor is currently in + * + * Most of the time this will be a partial word. If the cursor has a space before it, + * this will return an empty string, indicating a new word. + * + * @return string + */ + public function getCurrentWord() + { + if (isset($this->words[$this->wordIndex])) { + return $this->words[$this->wordIndex]; + } + + return ''; + } + + /** + * Return the unprocessed string for the word under the cursor + * + * This preserves any quotes and escaping that are present in the input command line. + * + * @return string + */ + public function getRawCurrentWord() + { + if (isset($this->rawWords[$this->wordIndex])) { + return $this->rawWords[$this->wordIndex]; + } + + return ''; + } + + /** + * Return a word by index from the command line + * + * @see $words, $wordBreaks + * @param int $index + * @return string + */ + public function getWordAtIndex($index) + { + if (isset($this->words[$index])) { + return $this->words[$index]; + } + + return ''; + } + + /** + * Get the contents of the command line, exploded into words based on the configured word break characters + * + * @see $wordBreaks, setWordBreaks + * @return array + */ + public function getWords() + { + if ($this->words === null) { + $this->splitCommand(); + } + + return $this->words; + } + + /** + * Get the unprocessed/literal words from the command line + * + * This is indexed the same as getWords(), but preserves any quoting and escaping from the command line + * + * @return string[] + */ + public function getRawWords() + { + if ($this->rawWords === null) { + $this->splitCommand(); + } + + return $this->rawWords; + } + + /** + * Get the index of the word the cursor is currently in + * + * @see getWords, getCurrentWord + * @return int + */ + public function getWordIndex() + { + if ($this->wordIndex === null) { + $this->splitCommand(); + } + + return $this->wordIndex; + } + + /** + * Get the character index of the user's cursor on the command line + * + * This is in the context of the full command line string, so includes word break characters. + * Note that some shells can only provide an approximation for character index. Under ZSH for + * example, this will always be the character at the start of the current word. + * + * @return int + */ + public function getCharIndex() + { + return $this->charIndex; + } + + /** + * Set the cursor position as a character index relative to the start of the command line + * + * @param int $index + */ + public function setCharIndex($index) + { + $this->charIndex = $index; + $this->reset(); + } + + /** + * Set characters to use as split points when breaking the command line into words + * + * This defaults to a sane value based on BASH's word break characters and shouldn't + * need to be changed unless your completions contain the default word break characters. + * + * @deprecated This is becoming an internal setting that doesn't make sense to expose publicly. + * + * @see wordBreaks + * @param string $charList - a single string containing all of the characters to break words on + */ + public function setWordBreaks($charList) + { + // Drop quotes from break characters - strings are handled separately to word breaks now + $this->wordBreaks = str_replace(array('"', '\''), '', $charList);; + $this->reset(); + } + + /** + * Split the command line into words using the configured word break characters + * + * @return string[] + */ + protected function splitCommand() + { + $tokens = $this->tokenizeString($this->commandLine); + + foreach ($tokens as $token) { + if ($token['type'] != 'break') { + $this->words[] = $this->getTokenValue($token); + $this->rawWords[] = $token['value']; + } + + // Determine which word index the cursor is inside once we reach it's offset + if ($this->wordIndex === null && $this->charIndex <= $token['offsetEnd']) { + $this->wordIndex = count($this->words) - 1; + + if ($token['type'] == 'break') { + // Cursor is in the break-space after a word + // Push an empty word at the cursor to allow completion of new terms at the cursor, ignoring words ahead + $this->wordIndex++; + $this->words[] = ''; + $this->rawWords[] = ''; + continue; + } + + if ($this->charIndex < $token['offsetEnd']) { + // Cursor is inside the current word - truncate the word at the cursor to complete on + // This emulates BASH completion's behaviour with COMP_CWORD + + // Create a copy of the token with its value truncated + $truncatedToken = $token; + $relativeOffset = $this->charIndex - $token['offset']; + $truncatedToken['value'] = substr($token['value'], 0, $relativeOffset); + + // Replace the current word with the truncated value + $this->words[$this->wordIndex] = $this->getTokenValue($truncatedToken); + $this->rawWords[$this->wordIndex] = $truncatedToken['value']; + } + } + } + + // Cursor position is past the end of the command line string - consider it a new word + if ($this->wordIndex === null) { + $this->wordIndex = count($this->words); + $this->words[] = ''; + $this->rawWords[] = ''; + } + } + + /** + * Return a token's value with escaping and quotes removed + * + * @see self::tokenizeString() + * @param array $token + * @return string + */ + protected function getTokenValue($token) + { + $value = $token['value']; + + // Remove outer quote characters (or first quote if unclosed) + if ($token['type'] == 'quoted') { + $value = preg_replace('/^(?:[\'"])(.*?)(?:[\'"])?$/', '$1', $value); + } + + // Remove escape characters + $value = preg_replace('/\\\\(.)/', '$1', $value); + + return $value; + } + + /** + * Break a string into words, quoted strings and non-words (breaks) + * + * Returns an array of unmodified segments of $string with offset and type information. + * + * @param string $string + * @return array as [ [type => string, value => string, offset => int], ... ] + */ + protected function tokenizeString($string) + { + // Map capture groups to returned token type + $typeMap = array( + 'double_quote_string' => 'quoted', + 'single_quote_string' => 'quoted', + 'word' => 'word', + 'break' => 'break', + ); + + // Escape every word break character including whitespace + // preg_quote won't work here as it doesn't understand the ignore whitespace flag ("x") + $breaks = preg_replace('/(.)/', '\\\$1', $this->wordBreaks); + + $pattern = <<<"REGEX" + /(?: + (?P + "(\\\\.|[^\"\\\\])*(?:"|$) + ) | + (?P + '(\\\\.|[^'\\\\])*(?:'|$) + ) | + (?P + (?:\\\\.|[^$breaks])+ + ) | + (?P + [$breaks]+ + ) + )/x +REGEX; + + $tokens = array(); + + if (!preg_match_all($pattern, $string, $matches, PREG_OFFSET_CAPTURE | PREG_SET_ORDER)) { + return $tokens; + } + + foreach ($matches as $set) { + foreach ($set as $groupName => $match) { + + // Ignore integer indices preg_match outputs (duplicates of named groups) + if (is_integer($groupName)) { + continue; + } + + // Skip if the offset indicates this group didn't match + if ($match[1] === -1) { + continue; + } + + $tokens[] = array( + 'type' => $typeMap[$groupName], + 'value' => $match[0], + 'offset' => $match[1], + 'offsetEnd' => $match[1] + strlen($match[0]) + ); + + // Move to the next set (only one group should match per set) + continue; + } + } + + return $tokens; + } + + /** + * Reset the computed words so that $this->splitWords is forced to run again + */ + protected function reset() + { + $this->words = null; + $this->wordIndex = null; + } +} diff --git a/3rdparty/stecman/symfony-console-completion/src/CompletionHandler.php b/3rdparty/stecman/symfony-console-completion/src/CompletionHandler.php new file mode 100644 index 00000000..871838e2 --- /dev/null +++ b/3rdparty/stecman/symfony-console-completion/src/CompletionHandler.php @@ -0,0 +1,504 @@ +application = $application; + $this->context = $context; + + // Set up completions for commands that are built-into Application + $this->addHandler( + new Completion( + 'help', + 'command_name', + Completion::TYPE_ARGUMENT, + $this->getCommandNames() + ) + ); + + $this->addHandler( + new Completion( + 'list', + 'namespace', + Completion::TYPE_ARGUMENT, + $application->getNamespaces() + ) + ); + } + + public function setContext(CompletionContext $context) + { + $this->context = $context; + } + + /** + * @return CompletionContext + */ + public function getContext() + { + return $this->context; + } + + /** + * @param CompletionInterface[] $array + */ + public function addHandlers(array $array) + { + $this->helpers = array_merge($this->helpers, $array); + } + + /** + * @param CompletionInterface $helper + */ + public function addHandler(CompletionInterface $helper) + { + $this->helpers[] = $helper; + } + + /** + * Do the actual completion, returning an array of strings to provide to the parent shell's completion system + * + * @throws \RuntimeException + * @return string[] + */ + public function runCompletion() + { + if (!$this->context) { + throw new \RuntimeException('A CompletionContext must be set before requesting completion.'); + } + + // Set the command to query options and arugments from + $this->command = $this->detectCommand(); + + $process = array( + 'completeForOptionValues', + 'completeForOptionShortcuts', + 'completeForOptionShortcutValues', + 'completeForOptions', + 'completeForCommandName', + 'completeForCommandArguments' + ); + + foreach ($process as $methodName) { + $result = $this->{$methodName}(); + + if (false !== $result) { + // Return the result of the first completion mode that matches + return $this->filterResults((array) $result); + } + } + + return array(); + } + + /** + * Get an InputInterface representation of the completion context + * + * @deprecated Incorrectly uses the ArrayInput API and is no longer needed. + * This will be removed in the next major version. + * + * @return ArrayInput + */ + public function getInput() + { + // Filter the command line content to suit ArrayInput + $words = $this->context->getWords(); + array_shift($words); + $words = array_filter($words); + + return new ArrayInput($words); + } + + /** + * Attempt to complete the current word as a long-form option (--my-option) + * + * @return array|false + */ + protected function completeForOptions() + { + $word = $this->context->getCurrentWord(); + + if (substr($word, 0, 2) === '--') { + $options = array(); + + foreach ($this->getAllOptions() as $opt) { + $options[] = '--'.$opt->getName(); + } + + return $options; + } + + return false; + } + + /** + * Attempt to complete the current word as an option shortcut. + * + * If the shortcut exists it will be completed, but a list of possible shortcuts is never returned for completion. + * + * @return array|false + */ + protected function completeForOptionShortcuts() + { + $word = $this->context->getCurrentWord(); + + if (strpos($word, '-') === 0 && strlen($word) == 2) { + $definition = $this->command ? $this->command->getNativeDefinition() : $this->application->getDefinition(); + + if ($definition->hasShortcut(substr($word, 1))) { + return array($word); + } + } + + return false; + } + + /** + * Attempt to complete the current word as the value of an option shortcut + * + * @return array|false + */ + protected function completeForOptionShortcutValues() + { + $wordIndex = $this->context->getWordIndex(); + + if ($this->command && $wordIndex > 1) { + $left = $this->context->getWordAtIndex($wordIndex - 1); + + // Complete short options + if ($left[0] == '-' && strlen($left) == 2) { + $shortcut = substr($left, 1); + $def = $this->command->getNativeDefinition(); + + if (!$def->hasShortcut($shortcut)) { + return false; + } + + $opt = $def->getOptionForShortcut($shortcut); + if ($opt->isValueRequired() || $opt->isValueOptional()) { + return $this->completeOption($opt); + } + } + } + + return false; + } + + /** + * Attemp to complete the current word as the value of a long-form option + * + * @return array|false + */ + protected function completeForOptionValues() + { + $wordIndex = $this->context->getWordIndex(); + + if ($this->command && $wordIndex > 1) { + $left = $this->context->getWordAtIndex($wordIndex - 1); + + if (strpos($left, '--') === 0) { + $name = substr($left, 2); + $def = $this->command->getNativeDefinition(); + + if (!$def->hasOption($name)) { + return false; + } + + $opt = $def->getOption($name); + if ($opt->isValueRequired() || $opt->isValueOptional()) { + return $this->completeOption($opt); + } + } + } + + return false; + } + + /** + * Attempt to complete the current word as a command name + * + * @return array|false + */ + protected function completeForCommandName() + { + if (!$this->command || $this->context->getWordIndex() == $this->commandWordIndex) { + return $this->getCommandNames(); + } + + return false; + } + + /** + * Attempt to complete the current word as a command argument value + * + * @see Symfony\Component\Console\Input\InputArgument + * @return array|false + */ + protected function completeForCommandArguments() + { + if (!$this->command || strpos($this->context->getCurrentWord(), '-') === 0) { + return false; + } + + $definition = $this->command->getNativeDefinition(); + $argWords = $this->mapArgumentsToWords($definition->getArguments()); + $wordIndex = $this->context->getWordIndex(); + + if (isset($argWords[$wordIndex])) { + $name = $argWords[$wordIndex]; + } elseif (!empty($argWords) && $definition->getArgument(end($argWords))->isArray()) { + $name = end($argWords); + } else { + return false; + } + + if ($helper = $this->getCompletionHelper($name, Completion::TYPE_ARGUMENT)) { + return $helper->run(); + } + + if ($this->command instanceof CompletionAwareInterface) { + return $this->command->completeArgumentValues($name, $this->context); + } + + return false; + } + + /** + * Find a CompletionInterface that matches the current command, target name, and target type + * + * @param string $name + * @param string $type + * @return CompletionInterface|null + */ + protected function getCompletionHelper($name, $type) + { + foreach ($this->helpers as $helper) { + if ($helper->getType() != $type && $helper->getType() != CompletionInterface::ALL_TYPES) { + continue; + } + + if ($helper->getCommandName() == CompletionInterface::ALL_COMMANDS || $helper->getCommandName() == $this->command->getName()) { + if ($helper->getTargetName() == $name) { + return $helper; + } + } + } + + return null; + } + + /** + * Complete the value for the given option if a value completion is availble + * + * @param InputOption $option + * @return array|false + */ + protected function completeOption(InputOption $option) + { + if ($helper = $this->getCompletionHelper($option->getName(), Completion::TYPE_OPTION)) { + return $helper->run(); + } + + if ($this->command instanceof CompletionAwareInterface) { + return $this->command->completeOptionValues($option->getName(), $this->context); + } + + return false; + } + + /** + * Step through the command line to determine which word positions represent which argument values + * + * The word indexes of argument values are found by eliminating words that are known to not be arguments (options, + * option values, and command names). Any word that doesn't match for elimination is assumed to be an argument value, + * + * @param InputArgument[] $argumentDefinitions + * @return array as [argument name => word index on command line] + */ + protected function mapArgumentsToWords($argumentDefinitions) + { + $argumentPositions = array(); + $argumentNumber = 0; + $previousWord = null; + $argumentNames = array_keys($argumentDefinitions); + + // Build a list of option values to filter out + $optionsWithArgs = $this->getOptionWordsWithValues(); + + foreach ($this->context->getWords() as $wordIndex => $word) { + // Skip program name, command name, options, and option values + if ($wordIndex == 0 + || $wordIndex === $this->commandWordIndex + || ($word && '-' === $word[0]) + || in_array($previousWord, $optionsWithArgs)) { + $previousWord = $word; + continue; + } else { + $previousWord = $word; + } + + // If argument n exists, pair that argument's name with the current word + if (isset($argumentNames[$argumentNumber])) { + $argumentPositions[$wordIndex] = $argumentNames[$argumentNumber]; + } + + $argumentNumber++; + } + + return $argumentPositions; + } + + /** + * Build a list of option words/flags that will have a value after them + * Options are returned in the format they appear as on the command line. + * + * @return string[] - eg. ['--myoption', '-m', ... ] + */ + protected function getOptionWordsWithValues() + { + $strings = array(); + + foreach ($this->getAllOptions() as $option) { + if ($option->isValueRequired()) { + $strings[] = '--' . $option->getName(); + + if ($option->getShortcut()) { + $strings[] = '-' . $option->getShortcut(); + } + } + } + + return $strings; + } + + /** + * Filter out results that don't match the current word on the command line + * + * @param string[] $array + * @return string[] + */ + protected function filterResults(array $array) + { + $curWord = $this->context->getCurrentWord(); + + return array_filter($array, function($val) use ($curWord) { + return fnmatch($curWord.'*', $val); + }); + } + + /** + * Get the combined options of the application and entered command + * + * @return InputOption[] + */ + protected function getAllOptions() + { + if (!$this->command) { + return $this->application->getDefinition()->getOptions(); + } + + return array_merge( + $this->command->getNativeDefinition()->getOptions(), + $this->application->getDefinition()->getOptions() + ); + } + + /** + * Get command names available for completion + * + * Filters out hidden commands where supported. + * + * @return string[] + */ + protected function getCommandNames() + { + $commands = array(); + + foreach ($this->application->all() as $name => $command) { + if (!$command->isHidden()) { + $commands[] = $name; + } + } + + return $commands; + } + + /** + * Find the current command name in the command-line + * + * Note this only cares about flag-type options. Options with values cannot + * appear before a command name in Symfony Console application. + * + * @return Command|null + */ + private function detectCommand() + { + // Always skip the first word (program name) + $skipNext = true; + + foreach ($this->context->getWords() as $index => $word) { + + // Skip word if flagged + if ($skipNext) { + $skipNext = false; + continue; + } + + // Skip empty words and words that look like options + if (strlen($word) == 0 || $word[0] === '-') { + continue; + } + + // Return the first unambiguous match to argument-like words + try { + $cmd = $this->application->find($word); + $this->commandWordIndex = $index; + return $cmd; + } catch (\InvalidArgumentException $e) { + // Exception thrown, when multiple or no commands are found. + } + } + + // No command found + return null; + } +} diff --git a/3rdparty/stecman/symfony-console-completion/src/EnvironmentCompletionContext.php b/3rdparty/stecman/symfony-console-completion/src/EnvironmentCompletionContext.php new file mode 100644 index 00000000..04027ce1 --- /dev/null +++ b/3rdparty/stecman/symfony-console-completion/src/EnvironmentCompletionContext.php @@ -0,0 +1,46 @@ +commandLine = getenv('CMDLINE_CONTENTS'); + $this->charIndex = intval(getenv('CMDLINE_CURSOR_INDEX')); + + if ($this->commandLine === false) { + $message = 'Failed to configure from environment; Environment var CMDLINE_CONTENTS not set.'; + + if (getenv('COMP_LINE')) { + $message .= "\n\nYou appear to be attempting completion using an out-dated hook. If you've just updated," + . " you probably need to reinitialise the completion shell hook by reloading your shell" + . " profile or starting a new shell session. If you are using a hard-coded (rather than generated)" + . " hook, you will need to update that function with the new environment variable names." + . "\n\nSee here for details: https://github.com/stecman/symfony-console-completion/issues/31"; + } + + throw new \RuntimeException($message); + } + } + + /** + * Use the word break characters set by the parent shell. + * + * @throws \RuntimeException + */ + public function useWordBreaksFromEnvironment() + { + $breaks = getenv('CMDLINE_WORDBREAKS'); + + if (!$breaks) { + throw new \RuntimeException('Failed to read word breaks from environment; Environment var CMDLINE_WORDBREAKS not set'); + } + + $this->wordBreaks = $breaks; + } +} diff --git a/3rdparty/stecman/symfony-console-completion/src/HookFactory.php b/3rdparty/stecman/symfony-console-completion/src/HookFactory.php new file mode 100644 index 00000000..4ad763df --- /dev/null +++ b/3rdparty/stecman/symfony-console-completion/src/HookFactory.php @@ -0,0 +1,214 @@ + <<<'END' +# BASH completion for %%program_path%% +function %%function_name%% { + + # Copy BASH's completion variables to the ones the completion command expects + # These line up exactly as the library was originally designed for BASH + local CMDLINE_CONTENTS="$COMP_LINE"; + local CMDLINE_CURSOR_INDEX="$COMP_POINT"; + local CMDLINE_WORDBREAKS="$COMP_WORDBREAKS"; + + export CMDLINE_CONTENTS CMDLINE_CURSOR_INDEX CMDLINE_WORDBREAKS; + + local RESULT STATUS; + + # Force splitting by newline instead of default delimiters + local IFS=$'\n'; + + RESULT="$(%%completion_command%% &2 echo "Completion was not registered for %%program_name%%:"; + >&2 echo "The 'bash-completion' package is required but doesn't appear to be installed."; +fi; +END + + // ZSH Hook + , 'zsh' => <<<'END' +# ZSH completion for %%program_path%% +function %%function_name%% { + local -x CMDLINE_CONTENTS="$words"; + local -x CMDLINE_CURSOR_INDEX; + (( CMDLINE_CURSOR_INDEX = ${#${(j. .)words[1,CURRENT]}} )); + + local RESULT STATUS; + RESULT=("${(@f)$( %%completion_command%% )}"); + STATUS=$?; + + # Check if shell provided path completion is requested + # @see Completion\ShellPathCompletion + if [ $STATUS -eq 200 ]; then + _path_files; + return 0; + + # Bail out if PHP didn't exit cleanly + elif [ $STATUS -ne 0 ]; then + echo -e "$RESULT"; + return $?; + fi; + + compadd -- $RESULT; +}; + +compdef %%function_name%% "%%program_name%%"; +END + ); + + /** + * Return the names of shells that have hooks + * + * @return string[] + */ + public static function getShellTypes() + { + return array_keys(self::$hooks); + } + + /** + * Return a completion hook for the specified shell type + * + * @param string $type - a key from self::$hooks + * @param string $programPath + * @param ?string $programName + * @param bool $multiple + * + * @return string + */ + public function generateHook($type, $programPath, $programName = null, $multiple = false) + { + if (!isset(self::$hooks[$type])) { + throw new \RuntimeException(sprintf( + "Cannot generate hook for unknown shell type '%s'. Available hooks are: %s", + $type, + implode(', ', self::getShellTypes()) + )); + } + + // Use the program path if an alias/name is not given + $programName = $programName ?: $programPath; + + if ($multiple) { + $completionCommand = '$1 _completion'; + } else { + $completionCommand = $programPath . ' _completion'; + } + + // Pass shell type during completion so output can be encoded if the shell requires it + $completionCommand .= " --shell-type $type"; + + return str_replace( + array( + '%%function_name%%', + '%%program_name%%', + '%%program_path%%', + '%%completion_command%%', + ), + array( + $this->generateFunctionName($programPath, $programName), + $programName, + $programPath, + $completionCommand + ), + $this->stripComments(self::$hooks[$type]) + ); + } + + /** + * Generate a function name that is unlikely to conflict with other generated function names in the same shell + */ + protected function generateFunctionName($programPath, $programName) + { + return sprintf( + '_%s_%s_complete', + $this->sanitiseForFunctionName(basename($programName)), + substr(md5($programPath), 0, 16) + ); + } + + + /** + * Make a string safe for use as a shell function name + * + * @param string $name + * @return string + */ + protected function sanitiseForFunctionName($name) + { + $name = str_replace('-', '_', $name); + return preg_replace('/[^A-Za-z0-9_]+/', '', $name); + } + + /** + * Strip '#' style comments from a string + * + * BASH's eval doesn't work with comments as it removes line breaks, so comments have to be stripped out + * for this method of sourcing the hook to work. Eval seems to be the most reliable method of getting a + * hook into a shell, so while it would be nice to render comments, this stripping is required for now. + * + * @param string $script + * @return string + */ + protected function stripComments($script) + { + return preg_replace('/(^\s*\#.*$)/m', '', $script); + } +} diff --git a/3rdparty/symfony/console/Application.php b/3rdparty/symfony/console/Application.php new file mode 100644 index 00000000..dc710e8c --- /dev/null +++ b/3rdparty/symfony/console/Application.php @@ -0,0 +1,1331 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Component\Console; + +use Symfony\Component\Console\Command\Command; +use Symfony\Component\Console\Command\CompleteCommand; +use Symfony\Component\Console\Command\DumpCompletionCommand; +use Symfony\Component\Console\Command\HelpCommand; +use Symfony\Component\Console\Command\LazyCommand; +use Symfony\Component\Console\Command\ListCommand; +use Symfony\Component\Console\Command\SignalableCommandInterface; +use Symfony\Component\Console\CommandLoader\CommandLoaderInterface; +use Symfony\Component\Console\Completion\CompletionInput; +use Symfony\Component\Console\Completion\CompletionSuggestions; +use Symfony\Component\Console\Completion\Suggestion; +use Symfony\Component\Console\Event\ConsoleCommandEvent; +use Symfony\Component\Console\Event\ConsoleErrorEvent; +use Symfony\Component\Console\Event\ConsoleSignalEvent; +use Symfony\Component\Console\Event\ConsoleTerminateEvent; +use Symfony\Component\Console\Exception\CommandNotFoundException; +use Symfony\Component\Console\Exception\ExceptionInterface; +use Symfony\Component\Console\Exception\LogicException; +use Symfony\Component\Console\Exception\NamespaceNotFoundException; +use Symfony\Component\Console\Exception\RuntimeException; +use Symfony\Component\Console\Formatter\OutputFormatter; +use Symfony\Component\Console\Helper\DebugFormatterHelper; +use Symfony\Component\Console\Helper\DescriptorHelper; +use Symfony\Component\Console\Helper\FormatterHelper; +use Symfony\Component\Console\Helper\Helper; +use Symfony\Component\Console\Helper\HelperSet; +use Symfony\Component\Console\Helper\ProcessHelper; +use Symfony\Component\Console\Helper\QuestionHelper; +use Symfony\Component\Console\Input\ArgvInput; +use Symfony\Component\Console\Input\ArrayInput; +use Symfony\Component\Console\Input\InputArgument; +use Symfony\Component\Console\Input\InputAwareInterface; +use Symfony\Component\Console\Input\InputDefinition; +use Symfony\Component\Console\Input\InputInterface; +use Symfony\Component\Console\Input\InputOption; +use Symfony\Component\Console\Output\ConsoleOutput; +use Symfony\Component\Console\Output\ConsoleOutputInterface; +use Symfony\Component\Console\Output\OutputInterface; +use Symfony\Component\Console\SignalRegistry\SignalRegistry; +use Symfony\Component\Console\Style\SymfonyStyle; +use Symfony\Component\ErrorHandler\ErrorHandler; +use Symfony\Contracts\EventDispatcher\EventDispatcherInterface; +use Symfony\Contracts\Service\ResetInterface; + +/** + * An Application is the container for a collection of commands. + * + * It is the main entry point of a Console application. + * + * This class is optimized for a standard CLI environment. + * + * Usage: + * + * $app = new Application('myapp', '1.0 (stable)'); + * $app->add(new SimpleCommand()); + * $app->run(); + * + * @author Fabien Potencier + */ +class Application implements ResetInterface +{ + private array $commands = []; + private bool $wantHelps = false; + private ?Command $runningCommand = null; + private string $name; + private string $version; + private ?CommandLoaderInterface $commandLoader = null; + private bool $catchExceptions = true; + private bool $catchErrors = false; + private bool $autoExit = true; + private InputDefinition $definition; + private HelperSet $helperSet; + private ?EventDispatcherInterface $dispatcher = null; + private Terminal $terminal; + private string $defaultCommand; + private bool $singleCommand = false; + private bool $initialized = false; + private ?SignalRegistry $signalRegistry = null; + private array $signalsToDispatchEvent = []; + + public function __construct(string $name = 'UNKNOWN', string $version = 'UNKNOWN') + { + $this->name = $name; + $this->version = $version; + $this->terminal = new Terminal(); + $this->defaultCommand = 'list'; + if (\defined('SIGINT') && SignalRegistry::isSupported()) { + $this->signalRegistry = new SignalRegistry(); + $this->signalsToDispatchEvent = [\SIGINT, \SIGTERM, \SIGUSR1, \SIGUSR2]; + } + } + + /** + * @final + */ + public function setDispatcher(EventDispatcherInterface $dispatcher): void + { + $this->dispatcher = $dispatcher; + } + + /** + * @return void + */ + public function setCommandLoader(CommandLoaderInterface $commandLoader) + { + $this->commandLoader = $commandLoader; + } + + public function getSignalRegistry(): SignalRegistry + { + if (!$this->signalRegistry) { + throw new RuntimeException('Signals are not supported. Make sure that the "pcntl" extension is installed and that "pcntl_*" functions are not disabled by your php.ini\'s "disable_functions" directive.'); + } + + return $this->signalRegistry; + } + + /** + * @return void + */ + public function setSignalsToDispatchEvent(int ...$signalsToDispatchEvent) + { + $this->signalsToDispatchEvent = $signalsToDispatchEvent; + } + + /** + * Runs the current application. + * + * @return int 0 if everything went fine, or an error code + * + * @throws \Exception When running fails. Bypass this when {@link setCatchExceptions()}. + */ + public function run(?InputInterface $input = null, ?OutputInterface $output = null): int + { + if (\function_exists('putenv')) { + @putenv('LINES='.$this->terminal->getHeight()); + @putenv('COLUMNS='.$this->terminal->getWidth()); + } + + $input ??= new ArgvInput(); + $output ??= new ConsoleOutput(); + + $renderException = function (\Throwable $e) use ($output) { + if ($output instanceof ConsoleOutputInterface) { + $this->renderThrowable($e, $output->getErrorOutput()); + } else { + $this->renderThrowable($e, $output); + } + }; + if ($phpHandler = set_exception_handler($renderException)) { + restore_exception_handler(); + if (!\is_array($phpHandler) || !$phpHandler[0] instanceof ErrorHandler) { + $errorHandler = true; + } elseif ($errorHandler = $phpHandler[0]->setExceptionHandler($renderException)) { + $phpHandler[0]->setExceptionHandler($errorHandler); + } + } + + try { + $this->configureIO($input, $output); + + $exitCode = $this->doRun($input, $output); + } catch (\Throwable $e) { + if ($e instanceof \Exception && !$this->catchExceptions) { + throw $e; + } + if (!$e instanceof \Exception && !$this->catchErrors) { + throw $e; + } + + $renderException($e); + + $exitCode = $e->getCode(); + if (is_numeric($exitCode)) { + $exitCode = (int) $exitCode; + if ($exitCode <= 0) { + $exitCode = 1; + } + } else { + $exitCode = 1; + } + } finally { + // if the exception handler changed, keep it + // otherwise, unregister $renderException + if (!$phpHandler) { + if (set_exception_handler($renderException) === $renderException) { + restore_exception_handler(); + } + restore_exception_handler(); + } elseif (!$errorHandler) { + $finalHandler = $phpHandler[0]->setExceptionHandler(null); + if ($finalHandler !== $renderException) { + $phpHandler[0]->setExceptionHandler($finalHandler); + } + } + } + + if ($this->autoExit) { + if ($exitCode > 255) { + $exitCode = 255; + } + + exit($exitCode); + } + + return $exitCode; + } + + /** + * Runs the current application. + * + * @return int 0 if everything went fine, or an error code + */ + public function doRun(InputInterface $input, OutputInterface $output) + { + if (true === $input->hasParameterOption(['--version', '-V'], true)) { + $output->writeln($this->getLongVersion()); + + return 0; + } + + try { + // Makes ArgvInput::getFirstArgument() able to distinguish an option from an argument. + $input->bind($this->getDefinition()); + } catch (ExceptionInterface) { + // Errors must be ignored, full binding/validation happens later when the command is known. + } + + $name = $this->getCommandName($input); + if (true === $input->hasParameterOption(['--help', '-h'], true)) { + if (!$name) { + $name = 'help'; + $input = new ArrayInput(['command_name' => $this->defaultCommand]); + } else { + $this->wantHelps = true; + } + } + + if (!$name) { + $name = $this->defaultCommand; + $definition = $this->getDefinition(); + $definition->setArguments(array_merge( + $definition->getArguments(), + [ + 'command' => new InputArgument('command', InputArgument::OPTIONAL, $definition->getArgument('command')->getDescription(), $name), + ] + )); + } + + try { + $this->runningCommand = null; + // the command name MUST be the first element of the input + $command = $this->find($name); + } catch (\Throwable $e) { + if (($e instanceof CommandNotFoundException && !$e instanceof NamespaceNotFoundException) && 1 === \count($alternatives = $e->getAlternatives()) && $input->isInteractive()) { + $alternative = $alternatives[0]; + + $style = new SymfonyStyle($input, $output); + $output->writeln(''); + $formattedBlock = (new FormatterHelper())->formatBlock(sprintf('Command "%s" is not defined.', $name), 'error', true); + $output->writeln($formattedBlock); + if (!$style->confirm(sprintf('Do you want to run "%s" instead? ', $alternative), false)) { + if (null !== $this->dispatcher) { + $event = new ConsoleErrorEvent($input, $output, $e); + $this->dispatcher->dispatch($event, ConsoleEvents::ERROR); + + return $event->getExitCode(); + } + + return 1; + } + + $command = $this->find($alternative); + } else { + if (null !== $this->dispatcher) { + $event = new ConsoleErrorEvent($input, $output, $e); + $this->dispatcher->dispatch($event, ConsoleEvents::ERROR); + + if (0 === $event->getExitCode()) { + return 0; + } + + $e = $event->getError(); + } + + try { + if ($e instanceof CommandNotFoundException && $namespace = $this->findNamespace($name)) { + $helper = new DescriptorHelper(); + $helper->describe($output instanceof ConsoleOutputInterface ? $output->getErrorOutput() : $output, $this, [ + 'format' => 'txt', + 'raw_text' => false, + 'namespace' => $namespace, + 'short' => false, + ]); + + return isset($event) ? $event->getExitCode() : 1; + } + + throw $e; + } catch (NamespaceNotFoundException) { + throw $e; + } + } + } + + if ($command instanceof LazyCommand) { + $command = $command->getCommand(); + } + + $this->runningCommand = $command; + $exitCode = $this->doRunCommand($command, $input, $output); + $this->runningCommand = null; + + return $exitCode; + } + + /** + * @return void + */ + public function reset() + { + } + + /** + * @return void + */ + public function setHelperSet(HelperSet $helperSet) + { + $this->helperSet = $helperSet; + } + + /** + * Get the helper set associated with the command. + */ + public function getHelperSet(): HelperSet + { + return $this->helperSet ??= $this->getDefaultHelperSet(); + } + + /** + * @return void + */ + public function setDefinition(InputDefinition $definition) + { + $this->definition = $definition; + } + + /** + * Gets the InputDefinition related to this Application. + */ + public function getDefinition(): InputDefinition + { + $this->definition ??= $this->getDefaultInputDefinition(); + + if ($this->singleCommand) { + $inputDefinition = $this->definition; + $inputDefinition->setArguments(); + + return $inputDefinition; + } + + return $this->definition; + } + + /** + * Adds suggestions to $suggestions for the current completion input (e.g. option or argument). + */ + public function complete(CompletionInput $input, CompletionSuggestions $suggestions): void + { + if ( + CompletionInput::TYPE_ARGUMENT_VALUE === $input->getCompletionType() + && 'command' === $input->getCompletionName() + ) { + foreach ($this->all() as $name => $command) { + // skip hidden commands and aliased commands as they already get added below + if ($command->isHidden() || $command->getName() !== $name) { + continue; + } + $suggestions->suggestValue(new Suggestion($command->getName(), $command->getDescription())); + foreach ($command->getAliases() as $name) { + $suggestions->suggestValue(new Suggestion($name, $command->getDescription())); + } + } + + return; + } + + if (CompletionInput::TYPE_OPTION_NAME === $input->getCompletionType()) { + $suggestions->suggestOptions($this->getDefinition()->getOptions()); + + return; + } + } + + /** + * Gets the help message. + */ + public function getHelp(): string + { + return $this->getLongVersion(); + } + + /** + * Gets whether to catch exceptions or not during commands execution. + */ + public function areExceptionsCaught(): bool + { + return $this->catchExceptions; + } + + /** + * Sets whether to catch exceptions or not during commands execution. + * + * @return void + */ + public function setCatchExceptions(bool $boolean) + { + $this->catchExceptions = $boolean; + } + + /** + * Sets whether to catch errors or not during commands execution. + */ + public function setCatchErrors(bool $catchErrors = true): void + { + $this->catchErrors = $catchErrors; + } + + /** + * Gets whether to automatically exit after a command execution or not. + */ + public function isAutoExitEnabled(): bool + { + return $this->autoExit; + } + + /** + * Sets whether to automatically exit after a command execution or not. + * + * @return void + */ + public function setAutoExit(bool $boolean) + { + $this->autoExit = $boolean; + } + + /** + * Gets the name of the application. + */ + public function getName(): string + { + return $this->name; + } + + /** + * Sets the application name. + * + * @return void + */ + public function setName(string $name) + { + $this->name = $name; + } + + /** + * Gets the application version. + */ + public function getVersion(): string + { + return $this->version; + } + + /** + * Sets the application version. + * + * @return void + */ + public function setVersion(string $version) + { + $this->version = $version; + } + + /** + * Returns the long version of the application. + * + * @return string + */ + public function getLongVersion() + { + if ('UNKNOWN' !== $this->getName()) { + if ('UNKNOWN' !== $this->getVersion()) { + return sprintf('%s %s', $this->getName(), $this->getVersion()); + } + + return $this->getName(); + } + + return 'Console Tool'; + } + + /** + * Registers a new command. + */ + public function register(string $name): Command + { + return $this->add(new Command($name)); + } + + /** + * Adds an array of command objects. + * + * If a Command is not enabled it will not be added. + * + * @param Command[] $commands An array of commands + * + * @return void + */ + public function addCommands(array $commands) + { + foreach ($commands as $command) { + $this->add($command); + } + } + + /** + * Adds a command object. + * + * If a command with the same name already exists, it will be overridden. + * If the command is not enabled it will not be added. + * + * @return Command|null + */ + public function add(Command $command) + { + $this->init(); + + $command->setApplication($this); + + if (!$command->isEnabled()) { + $command->setApplication(null); + + return null; + } + + if (!$command instanceof LazyCommand) { + // Will throw if the command is not correctly initialized. + $command->getDefinition(); + } + + if (!$command->getName()) { + throw new LogicException(sprintf('The command defined in "%s" cannot have an empty name.', get_debug_type($command))); + } + + $this->commands[$command->getName()] = $command; + + foreach ($command->getAliases() as $alias) { + $this->commands[$alias] = $command; + } + + return $command; + } + + /** + * Returns a registered command by name or alias. + * + * @return Command + * + * @throws CommandNotFoundException When given command name does not exist + */ + public function get(string $name) + { + $this->init(); + + if (!$this->has($name)) { + throw new CommandNotFoundException(sprintf('The command "%s" does not exist.', $name)); + } + + // When the command has a different name than the one used at the command loader level + if (!isset($this->commands[$name])) { + throw new CommandNotFoundException(sprintf('The "%s" command cannot be found because it is registered under multiple names. Make sure you don\'t set a different name via constructor or "setName()".', $name)); + } + + $command = $this->commands[$name]; + + if ($this->wantHelps) { + $this->wantHelps = false; + + $helpCommand = $this->get('help'); + $helpCommand->setCommand($command); + + return $helpCommand; + } + + return $command; + } + + /** + * Returns true if the command exists, false otherwise. + */ + public function has(string $name): bool + { + $this->init(); + + return isset($this->commands[$name]) || ($this->commandLoader?->has($name) && $this->add($this->commandLoader->get($name))); + } + + /** + * Returns an array of all unique namespaces used by currently registered commands. + * + * It does not return the global namespace which always exists. + * + * @return string[] + */ + public function getNamespaces(): array + { + $namespaces = []; + foreach ($this->all() as $command) { + if ($command->isHidden()) { + continue; + } + + $namespaces[] = $this->extractAllNamespaces($command->getName()); + + foreach ($command->getAliases() as $alias) { + $namespaces[] = $this->extractAllNamespaces($alias); + } + } + + return array_values(array_unique(array_filter(array_merge([], ...$namespaces)))); + } + + /** + * Finds a registered namespace by a name or an abbreviation. + * + * @throws NamespaceNotFoundException When namespace is incorrect or ambiguous + */ + public function findNamespace(string $namespace): string + { + $allNamespaces = $this->getNamespaces(); + $expr = implode('[^:]*:', array_map('preg_quote', explode(':', $namespace))).'[^:]*'; + $namespaces = preg_grep('{^'.$expr.'}', $allNamespaces); + + if (empty($namespaces)) { + $message = sprintf('There are no commands defined in the "%s" namespace.', $namespace); + + if ($alternatives = $this->findAlternatives($namespace, $allNamespaces)) { + if (1 == \count($alternatives)) { + $message .= "\n\nDid you mean this?\n "; + } else { + $message .= "\n\nDid you mean one of these?\n "; + } + + $message .= implode("\n ", $alternatives); + } + + throw new NamespaceNotFoundException($message, $alternatives); + } + + $exact = \in_array($namespace, $namespaces, true); + if (\count($namespaces) > 1 && !$exact) { + throw new NamespaceNotFoundException(sprintf("The namespace \"%s\" is ambiguous.\nDid you mean one of these?\n%s.", $namespace, $this->getAbbreviationSuggestions(array_values($namespaces))), array_values($namespaces)); + } + + return $exact ? $namespace : reset($namespaces); + } + + /** + * Finds a command by name or alias. + * + * Contrary to get, this command tries to find the best + * match if you give it an abbreviation of a name or alias. + * + * @return Command + * + * @throws CommandNotFoundException When command name is incorrect or ambiguous + */ + public function find(string $name) + { + $this->init(); + + $aliases = []; + + foreach ($this->commands as $command) { + foreach ($command->getAliases() as $alias) { + if (!$this->has($alias)) { + $this->commands[$alias] = $command; + } + } + } + + if ($this->has($name)) { + return $this->get($name); + } + + $allCommands = $this->commandLoader ? array_merge($this->commandLoader->getNames(), array_keys($this->commands)) : array_keys($this->commands); + $expr = implode('[^:]*:', array_map('preg_quote', explode(':', $name))).'[^:]*'; + $commands = preg_grep('{^'.$expr.'}', $allCommands); + + if (empty($commands)) { + $commands = preg_grep('{^'.$expr.'}i', $allCommands); + } + + // if no commands matched or we just matched namespaces + if (empty($commands) || \count(preg_grep('{^'.$expr.'$}i', $commands)) < 1) { + if (false !== $pos = strrpos($name, ':')) { + // check if a namespace exists and contains commands + $this->findNamespace(substr($name, 0, $pos)); + } + + $message = sprintf('Command "%s" is not defined.', $name); + + if ($alternatives = $this->findAlternatives($name, $allCommands)) { + // remove hidden commands + $alternatives = array_filter($alternatives, fn ($name) => !$this->get($name)->isHidden()); + + if (1 == \count($alternatives)) { + $message .= "\n\nDid you mean this?\n "; + } else { + $message .= "\n\nDid you mean one of these?\n "; + } + $message .= implode("\n ", $alternatives); + } + + throw new CommandNotFoundException($message, array_values($alternatives)); + } + + // filter out aliases for commands which are already on the list + if (\count($commands) > 1) { + $commandList = $this->commandLoader ? array_merge(array_flip($this->commandLoader->getNames()), $this->commands) : $this->commands; + $commands = array_unique(array_filter($commands, function ($nameOrAlias) use (&$commandList, $commands, &$aliases) { + if (!$commandList[$nameOrAlias] instanceof Command) { + $commandList[$nameOrAlias] = $this->commandLoader->get($nameOrAlias); + } + + $commandName = $commandList[$nameOrAlias]->getName(); + + $aliases[$nameOrAlias] = $commandName; + + return $commandName === $nameOrAlias || !\in_array($commandName, $commands); + })); + } + + if (\count($commands) > 1) { + $usableWidth = $this->terminal->getWidth() - 10; + $abbrevs = array_values($commands); + $maxLen = 0; + foreach ($abbrevs as $abbrev) { + $maxLen = max(Helper::width($abbrev), $maxLen); + } + $abbrevs = array_map(function ($cmd) use ($commandList, $usableWidth, $maxLen, &$commands) { + if ($commandList[$cmd]->isHidden()) { + unset($commands[array_search($cmd, $commands)]); + + return false; + } + + $abbrev = str_pad($cmd, $maxLen, ' ').' '.$commandList[$cmd]->getDescription(); + + return Helper::width($abbrev) > $usableWidth ? Helper::substr($abbrev, 0, $usableWidth - 3).'...' : $abbrev; + }, array_values($commands)); + + if (\count($commands) > 1) { + $suggestions = $this->getAbbreviationSuggestions(array_filter($abbrevs)); + + throw new CommandNotFoundException(sprintf("Command \"%s\" is ambiguous.\nDid you mean one of these?\n%s.", $name, $suggestions), array_values($commands)); + } + } + + $command = $this->get(reset($commands)); + + if ($command->isHidden()) { + throw new CommandNotFoundException(sprintf('The command "%s" does not exist.', $name)); + } + + return $command; + } + + /** + * Gets the commands (registered in the given namespace if provided). + * + * The array keys are the full names and the values the command instances. + * + * @return Command[] + */ + public function all(?string $namespace = null) + { + $this->init(); + + if (null === $namespace) { + if (!$this->commandLoader) { + return $this->commands; + } + + $commands = $this->commands; + foreach ($this->commandLoader->getNames() as $name) { + if (!isset($commands[$name]) && $this->has($name)) { + $commands[$name] = $this->get($name); + } + } + + return $commands; + } + + $commands = []; + foreach ($this->commands as $name => $command) { + if ($namespace === $this->extractNamespace($name, substr_count($namespace, ':') + 1)) { + $commands[$name] = $command; + } + } + + if ($this->commandLoader) { + foreach ($this->commandLoader->getNames() as $name) { + if (!isset($commands[$name]) && $namespace === $this->extractNamespace($name, substr_count($namespace, ':') + 1) && $this->has($name)) { + $commands[$name] = $this->get($name); + } + } + } + + return $commands; + } + + /** + * Returns an array of possible abbreviations given a set of names. + * + * @return string[][] + */ + public static function getAbbreviations(array $names): array + { + $abbrevs = []; + foreach ($names as $name) { + for ($len = \strlen($name); $len > 0; --$len) { + $abbrev = substr($name, 0, $len); + $abbrevs[$abbrev][] = $name; + } + } + + return $abbrevs; + } + + public function renderThrowable(\Throwable $e, OutputInterface $output): void + { + $output->writeln('', OutputInterface::VERBOSITY_QUIET); + + $this->doRenderThrowable($e, $output); + + if (null !== $this->runningCommand) { + $output->writeln(sprintf('%s', OutputFormatter::escape(sprintf($this->runningCommand->getSynopsis(), $this->getName()))), OutputInterface::VERBOSITY_QUIET); + $output->writeln('', OutputInterface::VERBOSITY_QUIET); + } + } + + protected function doRenderThrowable(\Throwable $e, OutputInterface $output): void + { + do { + $message = trim($e->getMessage()); + if ('' === $message || OutputInterface::VERBOSITY_VERBOSE <= $output->getVerbosity()) { + $class = get_debug_type($e); + $title = sprintf(' [%s%s] ', $class, 0 !== ($code = $e->getCode()) ? ' ('.$code.')' : ''); + $len = Helper::width($title); + } else { + $len = 0; + } + + if (str_contains($message, "@anonymous\0")) { + $message = preg_replace_callback('/[a-zA-Z_\x7f-\xff][\\\\a-zA-Z0-9_\x7f-\xff]*+@anonymous\x00.*?\.php(?:0x?|:[0-9]++\$)?[0-9a-fA-F]++/', fn ($m) => class_exists($m[0], false) ? (get_parent_class($m[0]) ?: key(class_implements($m[0])) ?: 'class').'@anonymous' : $m[0], $message); + } + + $width = $this->terminal->getWidth() ? $this->terminal->getWidth() - 1 : \PHP_INT_MAX; + $lines = []; + foreach ('' !== $message ? preg_split('/\r?\n/', $message) : [] as $line) { + foreach ($this->splitStringByWidth($line, $width - 4) as $line) { + // pre-format lines to get the right string length + $lineLength = Helper::width($line) + 4; + $lines[] = [$line, $lineLength]; + + $len = max($lineLength, $len); + } + } + + $messages = []; + if (!$e instanceof ExceptionInterface || OutputInterface::VERBOSITY_VERBOSE <= $output->getVerbosity()) { + $messages[] = sprintf('%s', OutputFormatter::escape(sprintf('In %s line %s:', basename($e->getFile()) ?: 'n/a', $e->getLine() ?: 'n/a'))); + } + $messages[] = $emptyLine = sprintf('%s', str_repeat(' ', $len)); + if ('' === $message || OutputInterface::VERBOSITY_VERBOSE <= $output->getVerbosity()) { + $messages[] = sprintf('%s%s', $title, str_repeat(' ', max(0, $len - Helper::width($title)))); + } + foreach ($lines as $line) { + $messages[] = sprintf(' %s %s', OutputFormatter::escape($line[0]), str_repeat(' ', $len - $line[1])); + } + $messages[] = $emptyLine; + $messages[] = ''; + + $output->writeln($messages, OutputInterface::VERBOSITY_QUIET); + + if (OutputInterface::VERBOSITY_VERBOSE <= $output->getVerbosity()) { + $output->writeln('Exception trace:', OutputInterface::VERBOSITY_QUIET); + + // exception related properties + $trace = $e->getTrace(); + + array_unshift($trace, [ + 'function' => '', + 'file' => $e->getFile() ?: 'n/a', + 'line' => $e->getLine() ?: 'n/a', + 'args' => [], + ]); + + for ($i = 0, $count = \count($trace); $i < $count; ++$i) { + $class = $trace[$i]['class'] ?? ''; + $type = $trace[$i]['type'] ?? ''; + $function = $trace[$i]['function'] ?? ''; + $file = $trace[$i]['file'] ?? 'n/a'; + $line = $trace[$i]['line'] ?? 'n/a'; + + $output->writeln(sprintf(' %s%s at %s:%s', $class, $function ? $type.$function.'()' : '', $file, $line), OutputInterface::VERBOSITY_QUIET); + } + + $output->writeln('', OutputInterface::VERBOSITY_QUIET); + } + } while ($e = $e->getPrevious()); + } + + /** + * Configures the input and output instances based on the user arguments and options. + * + * @return void + */ + protected function configureIO(InputInterface $input, OutputInterface $output) + { + if (true === $input->hasParameterOption(['--ansi'], true)) { + $output->setDecorated(true); + } elseif (true === $input->hasParameterOption(['--no-ansi'], true)) { + $output->setDecorated(false); + } + + if (true === $input->hasParameterOption(['--no-interaction', '-n'], true)) { + $input->setInteractive(false); + } + + switch ($shellVerbosity = (int) getenv('SHELL_VERBOSITY')) { + case -1: + $output->setVerbosity(OutputInterface::VERBOSITY_QUIET); + break; + case 1: + $output->setVerbosity(OutputInterface::VERBOSITY_VERBOSE); + break; + case 2: + $output->setVerbosity(OutputInterface::VERBOSITY_VERY_VERBOSE); + break; + case 3: + $output->setVerbosity(OutputInterface::VERBOSITY_DEBUG); + break; + default: + $shellVerbosity = 0; + break; + } + + if (true === $input->hasParameterOption(['--quiet', '-q'], true)) { + $output->setVerbosity(OutputInterface::VERBOSITY_QUIET); + $shellVerbosity = -1; + } else { + if ($input->hasParameterOption('-vvv', true) || $input->hasParameterOption('--verbose=3', true) || 3 === $input->getParameterOption('--verbose', false, true)) { + $output->setVerbosity(OutputInterface::VERBOSITY_DEBUG); + $shellVerbosity = 3; + } elseif ($input->hasParameterOption('-vv', true) || $input->hasParameterOption('--verbose=2', true) || 2 === $input->getParameterOption('--verbose', false, true)) { + $output->setVerbosity(OutputInterface::VERBOSITY_VERY_VERBOSE); + $shellVerbosity = 2; + } elseif ($input->hasParameterOption('-v', true) || $input->hasParameterOption('--verbose=1', true) || $input->hasParameterOption('--verbose', true) || $input->getParameterOption('--verbose', false, true)) { + $output->setVerbosity(OutputInterface::VERBOSITY_VERBOSE); + $shellVerbosity = 1; + } + } + + if (-1 === $shellVerbosity) { + $input->setInteractive(false); + } + + if (\function_exists('putenv')) { + @putenv('SHELL_VERBOSITY='.$shellVerbosity); + } + $_ENV['SHELL_VERBOSITY'] = $shellVerbosity; + $_SERVER['SHELL_VERBOSITY'] = $shellVerbosity; + } + + /** + * Runs the current command. + * + * If an event dispatcher has been attached to the application, + * events are also dispatched during the life-cycle of the command. + * + * @return int 0 if everything went fine, or an error code + */ + protected function doRunCommand(Command $command, InputInterface $input, OutputInterface $output) + { + foreach ($command->getHelperSet() as $helper) { + if ($helper instanceof InputAwareInterface) { + $helper->setInput($input); + } + } + + $commandSignals = $command instanceof SignalableCommandInterface ? $command->getSubscribedSignals() : []; + if ($commandSignals || $this->dispatcher && $this->signalsToDispatchEvent) { + if (!$this->signalRegistry) { + throw new RuntimeException('Unable to subscribe to signal events. Make sure that the "pcntl" extension is installed and that "pcntl_*" functions are not disabled by your php.ini\'s "disable_functions" directive.'); + } + + if (Terminal::hasSttyAvailable()) { + $sttyMode = shell_exec('stty -g'); + + foreach ([\SIGINT, \SIGTERM] as $signal) { + $this->signalRegistry->register($signal, static fn () => shell_exec('stty '.$sttyMode)); + } + } + + if ($this->dispatcher) { + // We register application signals, so that we can dispatch the event + foreach ($this->signalsToDispatchEvent as $signal) { + $event = new ConsoleSignalEvent($command, $input, $output, $signal); + + $this->signalRegistry->register($signal, function ($signal) use ($event, $command, $commandSignals) { + $this->dispatcher->dispatch($event, ConsoleEvents::SIGNAL); + $exitCode = $event->getExitCode(); + + // If the command is signalable, we call the handleSignal() method + if (\in_array($signal, $commandSignals, true)) { + $exitCode = $command->handleSignal($signal, $exitCode); + // BC layer for Symfony <= 5 + if (null === $exitCode) { + trigger_deprecation('symfony/console', '6.3', 'Not returning an exit code from "%s::handleSignal()" is deprecated, return "false" to keep the command running or "0" to exit successfully.', get_debug_type($command)); + $exitCode = 0; + } + } + + if (false !== $exitCode) { + $event = new ConsoleTerminateEvent($command, $event->getInput(), $event->getOutput(), $exitCode, $signal); + $this->dispatcher->dispatch($event, ConsoleEvents::TERMINATE); + + exit($event->getExitCode()); + } + }); + } + + // then we register command signals, but not if already handled after the dispatcher + $commandSignals = array_diff($commandSignals, $this->signalsToDispatchEvent); + } + + foreach ($commandSignals as $signal) { + $this->signalRegistry->register($signal, function (int $signal) use ($command): void { + $exitCode = $command->handleSignal($signal); + // BC layer for Symfony <= 5 + if (null === $exitCode) { + trigger_deprecation('symfony/console', '6.3', 'Not returning an exit code from "%s::handleSignal()" is deprecated, return "false" to keep the command running or "0" to exit successfully.', get_debug_type($command)); + $exitCode = 0; + } + + if (false !== $exitCode) { + exit($exitCode); + } + }); + } + } + + if (null === $this->dispatcher) { + return $command->run($input, $output); + } + + // bind before the console.command event, so the listeners have access to input options/arguments + try { + $command->mergeApplicationDefinition(); + $input->bind($command->getDefinition()); + } catch (ExceptionInterface) { + // ignore invalid options/arguments for now, to allow the event listeners to customize the InputDefinition + } + + $event = new ConsoleCommandEvent($command, $input, $output); + $e = null; + + try { + $this->dispatcher->dispatch($event, ConsoleEvents::COMMAND); + + if ($event->commandShouldRun()) { + $exitCode = $command->run($input, $output); + } else { + $exitCode = ConsoleCommandEvent::RETURN_CODE_DISABLED; + } + } catch (\Throwable $e) { + $event = new ConsoleErrorEvent($input, $output, $e, $command); + $this->dispatcher->dispatch($event, ConsoleEvents::ERROR); + $e = $event->getError(); + + if (0 === $exitCode = $event->getExitCode()) { + $e = null; + } + } + + $event = new ConsoleTerminateEvent($command, $input, $output, $exitCode); + $this->dispatcher->dispatch($event, ConsoleEvents::TERMINATE); + + if (null !== $e) { + throw $e; + } + + return $event->getExitCode(); + } + + /** + * Gets the name of the command based on input. + */ + protected function getCommandName(InputInterface $input): ?string + { + return $this->singleCommand ? $this->defaultCommand : $input->getFirstArgument(); + } + + /** + * Gets the default input definition. + */ + protected function getDefaultInputDefinition(): InputDefinition + { + return new InputDefinition([ + new InputArgument('command', InputArgument::REQUIRED, 'The command to execute'), + new InputOption('--help', '-h', InputOption::VALUE_NONE, 'Display help for the given command. When no command is given display help for the '.$this->defaultCommand.' command'), + new InputOption('--quiet', '-q', InputOption::VALUE_NONE, 'Do not output any message'), + new InputOption('--verbose', '-v|vv|vvv', InputOption::VALUE_NONE, 'Increase the verbosity of messages: 1 for normal output, 2 for more verbose output and 3 for debug'), + new InputOption('--version', '-V', InputOption::VALUE_NONE, 'Display this application version'), + new InputOption('--ansi', '', InputOption::VALUE_NEGATABLE, 'Force (or disable --no-ansi) ANSI output', null), + new InputOption('--no-interaction', '-n', InputOption::VALUE_NONE, 'Do not ask any interactive question'), + ]); + } + + /** + * Gets the default commands that should always be available. + * + * @return Command[] + */ + protected function getDefaultCommands(): array + { + return [new HelpCommand(), new ListCommand(), new CompleteCommand(), new DumpCompletionCommand()]; + } + + /** + * Gets the default helper set with the helpers that should always be available. + */ + protected function getDefaultHelperSet(): HelperSet + { + return new HelperSet([ + new FormatterHelper(), + new DebugFormatterHelper(), + new ProcessHelper(), + new QuestionHelper(), + ]); + } + + /** + * Returns abbreviated suggestions in string format. + */ + private function getAbbreviationSuggestions(array $abbrevs): string + { + return ' '.implode("\n ", $abbrevs); + } + + /** + * Returns the namespace part of the command name. + * + * This method is not part of public API and should not be used directly. + */ + public function extractNamespace(string $name, ?int $limit = null): string + { + $parts = explode(':', $name, -1); + + return implode(':', null === $limit ? $parts : \array_slice($parts, 0, $limit)); + } + + /** + * Finds alternative of $name among $collection, + * if nothing is found in $collection, try in $abbrevs. + * + * @return string[] + */ + private function findAlternatives(string $name, iterable $collection): array + { + $threshold = 1e3; + $alternatives = []; + + $collectionParts = []; + foreach ($collection as $item) { + $collectionParts[$item] = explode(':', $item); + } + + foreach (explode(':', $name) as $i => $subname) { + foreach ($collectionParts as $collectionName => $parts) { + $exists = isset($alternatives[$collectionName]); + if (!isset($parts[$i]) && $exists) { + $alternatives[$collectionName] += $threshold; + continue; + } elseif (!isset($parts[$i])) { + continue; + } + + $lev = levenshtein($subname, $parts[$i]); + if ($lev <= \strlen($subname) / 3 || '' !== $subname && str_contains($parts[$i], $subname)) { + $alternatives[$collectionName] = $exists ? $alternatives[$collectionName] + $lev : $lev; + } elseif ($exists) { + $alternatives[$collectionName] += $threshold; + } + } + } + + foreach ($collection as $item) { + $lev = levenshtein($name, $item); + if ($lev <= \strlen($name) / 3 || str_contains($item, $name)) { + $alternatives[$item] = isset($alternatives[$item]) ? $alternatives[$item] - $lev : $lev; + } + } + + $alternatives = array_filter($alternatives, fn ($lev) => $lev < 2 * $threshold); + ksort($alternatives, \SORT_NATURAL | \SORT_FLAG_CASE); + + return array_keys($alternatives); + } + + /** + * Sets the default Command name. + * + * @return $this + */ + public function setDefaultCommand(string $commandName, bool $isSingleCommand = false): static + { + $this->defaultCommand = explode('|', ltrim($commandName, '|'))[0]; + + if ($isSingleCommand) { + // Ensure the command exist + $this->find($commandName); + + $this->singleCommand = true; + } + + return $this; + } + + /** + * @internal + */ + public function isSingleCommand(): bool + { + return $this->singleCommand; + } + + private function splitStringByWidth(string $string, int $width): array + { + // str_split is not suitable for multi-byte characters, we should use preg_split to get char array properly. + // additionally, array_slice() is not enough as some character has doubled width. + // we need a function to split string not by character count but by string width + if (false === $encoding = mb_detect_encoding($string, null, true)) { + return str_split($string, $width); + } + + $utf8String = mb_convert_encoding($string, 'utf8', $encoding); + $lines = []; + $line = ''; + + $offset = 0; + while (preg_match('/.{1,10000}/u', $utf8String, $m, 0, $offset)) { + $offset += \strlen($m[0]); + + foreach (preg_split('//u', $m[0]) as $char) { + // test if $char could be appended to current line + if (mb_strwidth($line.$char, 'utf8') <= $width) { + $line .= $char; + continue; + } + // if not, push current line to array and make new line + $lines[] = str_pad($line, $width); + $line = $char; + } + } + + $lines[] = \count($lines) ? str_pad($line, $width) : $line; + + mb_convert_variables($encoding, 'utf8', $lines); + + return $lines; + } + + /** + * Returns all namespaces of the command name. + * + * @return string[] + */ + private function extractAllNamespaces(string $name): array + { + // -1 as third argument is needed to skip the command short name when exploding + $parts = explode(':', $name, -1); + $namespaces = []; + + foreach ($parts as $part) { + if (\count($namespaces)) { + $namespaces[] = end($namespaces).':'.$part; + } else { + $namespaces[] = $part; + } + } + + return $namespaces; + } + + private function init(): void + { + if ($this->initialized) { + return; + } + $this->initialized = true; + + foreach ($this->getDefaultCommands() as $command) { + $this->add($command); + } + } +} diff --git a/3rdparty/symfony/console/Attribute/AsCommand.php b/3rdparty/symfony/console/Attribute/AsCommand.php new file mode 100644 index 00000000..b337f548 --- /dev/null +++ b/3rdparty/symfony/console/Attribute/AsCommand.php @@ -0,0 +1,39 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Component\Console\Attribute; + +/** + * Service tag to autoconfigure commands. + */ +#[\Attribute(\Attribute::TARGET_CLASS)] +class AsCommand +{ + public function __construct( + public string $name, + public ?string $description = null, + array $aliases = [], + bool $hidden = false, + ) { + if (!$hidden && !$aliases) { + return; + } + + $name = explode('|', $name); + $name = array_merge($name, $aliases); + + if ($hidden && '' !== $name[0]) { + array_unshift($name, ''); + } + + $this->name = implode('|', $name); + } +} diff --git a/3rdparty/symfony/console/CI/GithubActionReporter.php b/3rdparty/symfony/console/CI/GithubActionReporter.php new file mode 100644 index 00000000..2cae6fd8 --- /dev/null +++ b/3rdparty/symfony/console/CI/GithubActionReporter.php @@ -0,0 +1,99 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Component\Console\CI; + +use Symfony\Component\Console\Output\OutputInterface; + +/** + * Utility class for Github actions. + * + * @author Maxime Steinhausser + */ +class GithubActionReporter +{ + private OutputInterface $output; + + /** + * @see https://github.com/actions/toolkit/blob/5e5e1b7aacba68a53836a34db4a288c3c1c1585b/packages/core/src/command.ts#L80-L85 + */ + private const ESCAPED_DATA = [ + '%' => '%25', + "\r" => '%0D', + "\n" => '%0A', + ]; + + /** + * @see https://github.com/actions/toolkit/blob/5e5e1b7aacba68a53836a34db4a288c3c1c1585b/packages/core/src/command.ts#L87-L94 + */ + private const ESCAPED_PROPERTIES = [ + '%' => '%25', + "\r" => '%0D', + "\n" => '%0A', + ':' => '%3A', + ',' => '%2C', + ]; + + public function __construct(OutputInterface $output) + { + $this->output = $output; + } + + public static function isGithubActionEnvironment(): bool + { + return false !== getenv('GITHUB_ACTIONS'); + } + + /** + * Output an error using the Github annotations format. + * + * @see https://docs.github.com/en/free-pro-team@latest/actions/reference/workflow-commands-for-github-actions#setting-an-error-message + */ + public function error(string $message, ?string $file = null, ?int $line = null, ?int $col = null): void + { + $this->log('error', $message, $file, $line, $col); + } + + /** + * Output a warning using the Github annotations format. + * + * @see https://docs.github.com/en/free-pro-team@latest/actions/reference/workflow-commands-for-github-actions#setting-a-warning-message + */ + public function warning(string $message, ?string $file = null, ?int $line = null, ?int $col = null): void + { + $this->log('warning', $message, $file, $line, $col); + } + + /** + * Output a debug log using the Github annotations format. + * + * @see https://docs.github.com/en/free-pro-team@latest/actions/reference/workflow-commands-for-github-actions#setting-a-debug-message + */ + public function debug(string $message, ?string $file = null, ?int $line = null, ?int $col = null): void + { + $this->log('debug', $message, $file, $line, $col); + } + + private function log(string $type, string $message, ?string $file = null, ?int $line = null, ?int $col = null): void + { + // Some values must be encoded. + $message = strtr($message, self::ESCAPED_DATA); + + if (!$file) { + // No file provided, output the message solely: + $this->output->writeln(sprintf('::%s::%s', $type, $message)); + + return; + } + + $this->output->writeln(sprintf('::%s file=%s,line=%s,col=%s::%s', $type, strtr($file, self::ESCAPED_PROPERTIES), strtr($line ?? 1, self::ESCAPED_PROPERTIES), strtr($col ?? 0, self::ESCAPED_PROPERTIES), $message)); + } +} diff --git a/3rdparty/symfony/console/Color.php b/3rdparty/symfony/console/Color.php new file mode 100644 index 00000000..60ed046a --- /dev/null +++ b/3rdparty/symfony/console/Color.php @@ -0,0 +1,133 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Component\Console; + +use Symfony\Component\Console\Exception\InvalidArgumentException; + +/** + * @author Fabien Potencier + */ +final class Color +{ + private const COLORS = [ + 'black' => 0, + 'red' => 1, + 'green' => 2, + 'yellow' => 3, + 'blue' => 4, + 'magenta' => 5, + 'cyan' => 6, + 'white' => 7, + 'default' => 9, + ]; + + private const BRIGHT_COLORS = [ + 'gray' => 0, + 'bright-red' => 1, + 'bright-green' => 2, + 'bright-yellow' => 3, + 'bright-blue' => 4, + 'bright-magenta' => 5, + 'bright-cyan' => 6, + 'bright-white' => 7, + ]; + + private const AVAILABLE_OPTIONS = [ + 'bold' => ['set' => 1, 'unset' => 22], + 'underscore' => ['set' => 4, 'unset' => 24], + 'blink' => ['set' => 5, 'unset' => 25], + 'reverse' => ['set' => 7, 'unset' => 27], + 'conceal' => ['set' => 8, 'unset' => 28], + ]; + + private string $foreground; + private string $background; + private array $options = []; + + public function __construct(string $foreground = '', string $background = '', array $options = []) + { + $this->foreground = $this->parseColor($foreground); + $this->background = $this->parseColor($background, true); + + foreach ($options as $option) { + if (!isset(self::AVAILABLE_OPTIONS[$option])) { + throw new InvalidArgumentException(sprintf('Invalid option specified: "%s". Expected one of (%s).', $option, implode(', ', array_keys(self::AVAILABLE_OPTIONS)))); + } + + $this->options[$option] = self::AVAILABLE_OPTIONS[$option]; + } + } + + public function apply(string $text): string + { + return $this->set().$text.$this->unset(); + } + + public function set(): string + { + $setCodes = []; + if ('' !== $this->foreground) { + $setCodes[] = $this->foreground; + } + if ('' !== $this->background) { + $setCodes[] = $this->background; + } + foreach ($this->options as $option) { + $setCodes[] = $option['set']; + } + if (0 === \count($setCodes)) { + return ''; + } + + return sprintf("\033[%sm", implode(';', $setCodes)); + } + + public function unset(): string + { + $unsetCodes = []; + if ('' !== $this->foreground) { + $unsetCodes[] = 39; + } + if ('' !== $this->background) { + $unsetCodes[] = 49; + } + foreach ($this->options as $option) { + $unsetCodes[] = $option['unset']; + } + if (0 === \count($unsetCodes)) { + return ''; + } + + return sprintf("\033[%sm", implode(';', $unsetCodes)); + } + + private function parseColor(string $color, bool $background = false): string + { + if ('' === $color) { + return ''; + } + + if ('#' === $color[0]) { + return ($background ? '4' : '3').Terminal::getColorMode()->convertFromHexToAnsiColorCode($color); + } + + if (isset(self::COLORS[$color])) { + return ($background ? '4' : '3').self::COLORS[$color]; + } + + if (isset(self::BRIGHT_COLORS[$color])) { + return ($background ? '10' : '9').self::BRIGHT_COLORS[$color]; + } + + throw new InvalidArgumentException(sprintf('Invalid "%s" color; expected one of (%s).', $color, implode(', ', array_merge(array_keys(self::COLORS), array_keys(self::BRIGHT_COLORS))))); + } +} diff --git a/3rdparty/symfony/console/Command/Command.php b/3rdparty/symfony/console/Command/Command.php new file mode 100644 index 00000000..9f9cb2f5 --- /dev/null +++ b/3rdparty/symfony/console/Command/Command.php @@ -0,0 +1,725 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +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; +use Symfony\Component\Console\Completion\Suggestion; +use Symfony\Component\Console\Exception\ExceptionInterface; +use Symfony\Component\Console\Exception\InvalidArgumentException; +use Symfony\Component\Console\Exception\LogicException; +use Symfony\Component\Console\Helper\HelperInterface; +use Symfony\Component\Console\Helper\HelperSet; +use Symfony\Component\Console\Input\InputArgument; +use Symfony\Component\Console\Input\InputDefinition; +use Symfony\Component\Console\Input\InputInterface; +use Symfony\Component\Console\Input\InputOption; +use Symfony\Component\Console\Output\OutputInterface; + +/** + * Base class for all commands. + * + * @author Fabien Potencier + */ +class Command +{ + // see https://tldp.org/LDP/abs/html/exitcodes.html + public const SUCCESS = 0; + public const FAILURE = 1; + public const INVALID = 2; + + /** + * @var string|null The default command name + * + * @deprecated since Symfony 6.1, use the AsCommand attribute instead + */ + protected static $defaultName; + + /** + * @var string|null The default command description + * + * @deprecated since Symfony 6.1, use the AsCommand attribute instead + */ + protected static $defaultDescription; + + private ?Application $application = null; + private ?string $name = null; + private ?string $processTitle = null; + private array $aliases = []; + private InputDefinition $definition; + private bool $hidden = false; + private string $help = ''; + private string $description = ''; + private ?InputDefinition $fullDefinition = null; + private bool $ignoreValidationErrors = false; + private ?\Closure $code = null; + private array $synopsis = []; + private array $usages = []; + private ?HelperSet $helperSet = null; + + public static function getDefaultName(): ?string + { + $class = static::class; + + if ($attribute = (new \ReflectionClass($class))->getAttributes(AsCommand::class)) { + return $attribute[0]->newInstance()->name; + } + + $r = new \ReflectionProperty($class, 'defaultName'); + + if ($class !== $r->class || null === static::$defaultName) { + return null; + } + + trigger_deprecation('symfony/console', '6.1', 'Relying on the static property "$defaultName" for setting a command name is deprecated. Add the "%s" attribute to the "%s" class instead.', AsCommand::class, static::class); + + return static::$defaultName; + } + + public static function getDefaultDescription(): ?string + { + $class = static::class; + + if ($attribute = (new \ReflectionClass($class))->getAttributes(AsCommand::class)) { + return $attribute[0]->newInstance()->description; + } + + $r = new \ReflectionProperty($class, 'defaultDescription'); + + if ($class !== $r->class || null === static::$defaultDescription) { + return null; + } + + trigger_deprecation('symfony/console', '6.1', 'Relying on the static property "$defaultDescription" for setting a command description is deprecated. Add the "%s" attribute to the "%s" class instead.', AsCommand::class, static::class); + + return static::$defaultDescription; + } + + /** + * @param string|null $name The name of the command; passing null means it must be set in configure() + * + * @throws LogicException When the command name is empty + */ + public function __construct(?string $name = null) + { + $this->definition = new InputDefinition(); + + if (null === $name && null !== $name = static::getDefaultName()) { + $aliases = explode('|', $name); + + if ('' === $name = array_shift($aliases)) { + $this->setHidden(true); + $name = array_shift($aliases); + } + + $this->setAliases($aliases); + } + + if (null !== $name) { + $this->setName($name); + } + + if ('' === $this->description) { + $this->setDescription(static::getDefaultDescription() ?? ''); + } + + $this->configure(); + } + + /** + * Ignores validation errors. + * + * This is mainly useful for the help command. + * + * @return void + */ + public function ignoreValidationErrors() + { + $this->ignoreValidationErrors = true; + } + + /** + * @return void + */ + public function setApplication(?Application $application = null) + { + if (1 > \func_num_args()) { + trigger_deprecation('symfony/console', '6.2', 'Calling "%s()" without any arguments is deprecated, pass null explicitly instead.', __METHOD__); + } + $this->application = $application; + if ($application) { + $this->setHelperSet($application->getHelperSet()); + } else { + $this->helperSet = null; + } + + $this->fullDefinition = null; + } + + /** + * @return void + */ + public function setHelperSet(HelperSet $helperSet) + { + $this->helperSet = $helperSet; + } + + /** + * Gets the helper set. + */ + public function getHelperSet(): ?HelperSet + { + return $this->helperSet; + } + + /** + * Gets the application instance for this command. + */ + public function getApplication(): ?Application + { + return $this->application; + } + + /** + * Checks whether the command is enabled or not in the current environment. + * + * Override this to check for x or y and return false if the command cannot + * run properly under the current conditions. + * + * @return bool + */ + public function isEnabled() + { + return true; + } + + /** + * Configures the current command. + * + * @return void + */ + protected function configure() + { + } + + /** + * Executes the current command. + * + * This method is not abstract because you can use this class + * as a concrete class. In this case, instead of defining the + * execute() method, you set the code to execute by passing + * a Closure to the setCode() method. + * + * @return int 0 if everything went fine, or an exit code + * + * @throws LogicException When this abstract method is not implemented + * + * @see setCode() + */ + protected function execute(InputInterface $input, OutputInterface $output) + { + throw new LogicException('You must override the execute() method in the concrete command class.'); + } + + /** + * Interacts with the user. + * + * This method is executed before the InputDefinition is validated. + * This means that this is the only place where the command can + * interactively ask for values of missing required arguments. + * + * @return void + */ + protected function interact(InputInterface $input, OutputInterface $output) + { + } + + /** + * Initializes the command after the input has been bound and before the input + * is validated. + * + * This is mainly useful when a lot of commands extends one main command + * where some things need to be initialized based on the input arguments and options. + * + * @see InputInterface::bind() + * @see InputInterface::validate() + * + * @return void + */ + protected function initialize(InputInterface $input, OutputInterface $output) + { + } + + /** + * Runs the command. + * + * The code to execute is either defined directly with the + * setCode() method or by overriding the execute() method + * in a sub-class. + * + * @return int The command exit code + * + * @throws ExceptionInterface When input binding fails. Bypass this by calling {@link ignoreValidationErrors()}. + * + * @see setCode() + * @see execute() + */ + public function run(InputInterface $input, OutputInterface $output): int + { + // add the application arguments and options + $this->mergeApplicationDefinition(); + + // bind the input against the command specific arguments/options + try { + $input->bind($this->getDefinition()); + } catch (ExceptionInterface $e) { + if (!$this->ignoreValidationErrors) { + throw $e; + } + } + + $this->initialize($input, $output); + + if (null !== $this->processTitle) { + if (\function_exists('cli_set_process_title')) { + if (!@cli_set_process_title($this->processTitle)) { + if ('Darwin' === \PHP_OS) { + $output->writeln('Running "cli_set_process_title" as an unprivileged user is not supported on MacOS.', OutputInterface::VERBOSITY_VERY_VERBOSE); + } else { + cli_set_process_title($this->processTitle); + } + } + } elseif (\function_exists('setproctitle')) { + setproctitle($this->processTitle); + } elseif (OutputInterface::VERBOSITY_VERY_VERBOSE === $output->getVerbosity()) { + $output->writeln('Install the proctitle PECL to be able to change the process title.'); + } + } + + if ($input->isInteractive()) { + $this->interact($input, $output); + } + + // The command name argument is often omitted when a command is executed directly with its run() method. + // It would fail the validation if we didn't make sure the command argument is present, + // since it's required by the application. + if ($input->hasArgument('command') && null === $input->getArgument('command')) { + $input->setArgument('command', $this->getName()); + } + + $input->validate(); + + if ($this->code) { + $statusCode = ($this->code)($input, $output); + } else { + $statusCode = $this->execute($input, $output); + + if (!\is_int($statusCode)) { + throw new \TypeError(sprintf('Return value of "%s::execute()" must be of the type int, "%s" returned.', static::class, get_debug_type($statusCode))); + } + } + + return is_numeric($statusCode) ? (int) $statusCode : 0; + } + + /** + * Adds suggestions to $suggestions for the current completion input (e.g. option or argument). + */ + public function complete(CompletionInput $input, CompletionSuggestions $suggestions): void + { + $definition = $this->getDefinition(); + if (CompletionInput::TYPE_OPTION_VALUE === $input->getCompletionType() && $definition->hasOption($input->getCompletionName())) { + $definition->getOption($input->getCompletionName())->complete($input, $suggestions); + } elseif (CompletionInput::TYPE_ARGUMENT_VALUE === $input->getCompletionType() && $definition->hasArgument($input->getCompletionName())) { + $definition->getArgument($input->getCompletionName())->complete($input, $suggestions); + } + } + + /** + * Sets the code to execute when running this command. + * + * If this method is used, it overrides the code defined + * in the execute() method. + * + * @param callable $code A callable(InputInterface $input, OutputInterface $output) + * + * @return $this + * + * @throws InvalidArgumentException + * + * @see execute() + */ + public function setCode(callable $code): static + { + if ($code instanceof \Closure) { + $r = new \ReflectionFunction($code); + if (null === $r->getClosureThis()) { + set_error_handler(static function () {}); + try { + if ($c = \Closure::bind($code, $this)) { + $code = $c; + } + } finally { + restore_error_handler(); + } + } + } else { + $code = $code(...); + } + + $this->code = $code; + + return $this; + } + + /** + * Merges the application definition with the command definition. + * + * This method is not part of public API and should not be used directly. + * + * @param bool $mergeArgs Whether to merge or not the Application definition arguments to Command definition arguments + * + * @internal + */ + public function mergeApplicationDefinition(bool $mergeArgs = true): void + { + if (null === $this->application) { + return; + } + + $this->fullDefinition = new InputDefinition(); + $this->fullDefinition->setOptions($this->definition->getOptions()); + $this->fullDefinition->addOptions($this->application->getDefinition()->getOptions()); + + if ($mergeArgs) { + $this->fullDefinition->setArguments($this->application->getDefinition()->getArguments()); + $this->fullDefinition->addArguments($this->definition->getArguments()); + } else { + $this->fullDefinition->setArguments($this->definition->getArguments()); + } + } + + /** + * Sets an array of argument and option instances. + * + * @return $this + */ + public function setDefinition(array|InputDefinition $definition): static + { + if ($definition instanceof InputDefinition) { + $this->definition = $definition; + } else { + $this->definition->setDefinition($definition); + } + + $this->fullDefinition = null; + + return $this; + } + + /** + * Gets the InputDefinition attached to this Command. + */ + public function getDefinition(): InputDefinition + { + return $this->fullDefinition ?? $this->getNativeDefinition(); + } + + /** + * Gets the InputDefinition to be used to create representations of this Command. + * + * Can be overridden to provide the original command representation when it would otherwise + * be changed by merging with the application InputDefinition. + * + * This method is not part of public API and should not be used directly. + */ + public function getNativeDefinition(): InputDefinition + { + return $this->definition ?? throw new LogicException(sprintf('Command class "%s" is not correctly initialized. You probably forgot to call the parent constructor.', static::class)); + } + + /** + * Adds an argument. + * + * @param $mode The argument mode: InputArgument::REQUIRED or InputArgument::OPTIONAL + * @param $default The default value (for InputArgument::OPTIONAL mode only) + * @param array|\Closure(CompletionInput,CompletionSuggestions):list $suggestedValues The values used for input completion + * + * @return $this + * + * @throws InvalidArgumentException When argument mode is not valid + */ + public function addArgument(string $name, ?int $mode = null, string $description = '', mixed $default = null /* array|\Closure $suggestedValues = null */): static + { + $suggestedValues = 5 <= \func_num_args() ? func_get_arg(4) : []; + if (!\is_array($suggestedValues) && !$suggestedValues instanceof \Closure) { + throw new \TypeError(sprintf('Argument 5 passed to "%s()" must be array or \Closure, "%s" given.', __METHOD__, get_debug_type($suggestedValues))); + } + $this->definition->addArgument(new InputArgument($name, $mode, $description, $default, $suggestedValues)); + $this->fullDefinition?->addArgument(new InputArgument($name, $mode, $description, $default, $suggestedValues)); + + return $this; + } + + /** + * Adds an option. + * + * @param $shortcut The shortcuts, can be null, a string of shortcuts delimited by | or an array of shortcuts + * @param $mode The option mode: One of the InputOption::VALUE_* constants + * @param $default The default value (must be null for InputOption::VALUE_NONE) + * @param array|\Closure(CompletionInput,CompletionSuggestions):list $suggestedValues The values used for input completion + * + * @return $this + * + * @throws InvalidArgumentException If option mode is invalid or incompatible + */ + public function addOption(string $name, string|array|null $shortcut = null, ?int $mode = null, string $description = '', mixed $default = null /* array|\Closure $suggestedValues = [] */): static + { + $suggestedValues = 6 <= \func_num_args() ? func_get_arg(5) : []; + if (!\is_array($suggestedValues) && !$suggestedValues instanceof \Closure) { + throw new \TypeError(sprintf('Argument 5 passed to "%s()" must be array or \Closure, "%s" given.', __METHOD__, get_debug_type($suggestedValues))); + } + $this->definition->addOption(new InputOption($name, $shortcut, $mode, $description, $default, $suggestedValues)); + $this->fullDefinition?->addOption(new InputOption($name, $shortcut, $mode, $description, $default, $suggestedValues)); + + return $this; + } + + /** + * Sets the name of the command. + * + * This method can set both the namespace and the name if + * you separate them by a colon (:) + * + * $command->setName('foo:bar'); + * + * @return $this + * + * @throws InvalidArgumentException When the name is invalid + */ + public function setName(string $name): static + { + $this->validateName($name); + + $this->name = $name; + + return $this; + } + + /** + * Sets the process title of the command. + * + * This feature should be used only when creating a long process command, + * like a daemon. + * + * @return $this + */ + public function setProcessTitle(string $title): static + { + $this->processTitle = $title; + + return $this; + } + + /** + * Returns the command name. + */ + public function getName(): ?string + { + return $this->name; + } + + /** + * @param bool $hidden Whether or not the command should be hidden from the list of commands + * + * @return $this + */ + public function setHidden(bool $hidden = true): static + { + $this->hidden = $hidden; + + return $this; + } + + /** + * @return bool whether the command should be publicly shown or not + */ + public function isHidden(): bool + { + return $this->hidden; + } + + /** + * Sets the description for the command. + * + * @return $this + */ + public function setDescription(string $description): static + { + $this->description = $description; + + return $this; + } + + /** + * Returns the description for the command. + */ + public function getDescription(): string + { + return $this->description; + } + + /** + * Sets the help for the command. + * + * @return $this + */ + public function setHelp(string $help): static + { + $this->help = $help; + + return $this; + } + + /** + * Returns the help for the command. + */ + public function getHelp(): string + { + return $this->help; + } + + /** + * Returns the processed help for the command replacing the %command.name% and + * %command.full_name% patterns with the real values dynamically. + */ + public function getProcessedHelp(): string + { + $name = $this->name; + $isSingleCommand = $this->application?->isSingleCommand(); + + $placeholders = [ + '%command.name%', + '%command.full_name%', + ]; + $replacements = [ + $name, + $isSingleCommand ? $_SERVER['PHP_SELF'] : $_SERVER['PHP_SELF'].' '.$name, + ]; + + return str_replace($placeholders, $replacements, $this->getHelp() ?: $this->getDescription()); + } + + /** + * Sets the aliases for the command. + * + * @param string[] $aliases An array of aliases for the command + * + * @return $this + * + * @throws InvalidArgumentException When an alias is invalid + */ + public function setAliases(iterable $aliases): static + { + $list = []; + + foreach ($aliases as $alias) { + $this->validateName($alias); + $list[] = $alias; + } + + $this->aliases = \is_array($aliases) ? $aliases : $list; + + return $this; + } + + /** + * Returns the aliases for the command. + */ + public function getAliases(): array + { + return $this->aliases; + } + + /** + * Returns the synopsis for the command. + * + * @param bool $short Whether to show the short version of the synopsis (with options folded) or not + */ + public function getSynopsis(bool $short = false): string + { + $key = $short ? 'short' : 'long'; + + if (!isset($this->synopsis[$key])) { + $this->synopsis[$key] = trim(sprintf('%s %s', $this->name, $this->definition->getSynopsis($short))); + } + + return $this->synopsis[$key]; + } + + /** + * Add a command usage example, it'll be prefixed with the command name. + * + * @return $this + */ + public function addUsage(string $usage): static + { + if (!str_starts_with($usage, $this->name)) { + $usage = sprintf('%s %s', $this->name, $usage); + } + + $this->usages[] = $usage; + + return $this; + } + + /** + * Returns alternative usages of the command. + */ + public function getUsages(): array + { + return $this->usages; + } + + /** + * Gets a helper instance by name. + * + * @return HelperInterface + * + * @throws LogicException if no HelperSet is defined + * @throws InvalidArgumentException if the helper is not defined + */ + public function getHelper(string $name): mixed + { + if (null === $this->helperSet) { + throw new LogicException(sprintf('Cannot retrieve helper "%s" because there is no HelperSet defined. Did you forget to add your command to the application or to set the application on the command using the setApplication() method? You can also set the HelperSet directly using the setHelperSet() method.', $name)); + } + + return $this->helperSet->get($name); + } + + /** + * Validates a command name. + * + * It must be non-empty and parts can optionally be separated by ":". + * + * @throws InvalidArgumentException When the name is invalid + */ + private function validateName(string $name): void + { + if (!preg_match('/^[^\:]++(\:[^\:]++)*$/', $name)) { + throw new InvalidArgumentException(sprintf('Command name "%s" is invalid.', $name)); + } + } +} diff --git a/3rdparty/symfony/console/Command/CompleteCommand.php b/3rdparty/symfony/console/Command/CompleteCommand.php new file mode 100644 index 00000000..23be5577 --- /dev/null +++ b/3rdparty/symfony/console/Command/CompleteCommand.php @@ -0,0 +1,223 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Component\Console\Command; + +use Symfony\Component\Console\Attribute\AsCommand; +use Symfony\Component\Console\Completion\CompletionInput; +use Symfony\Component\Console\Completion\CompletionSuggestions; +use Symfony\Component\Console\Completion\Output\BashCompletionOutput; +use Symfony\Component\Console\Completion\Output\CompletionOutputInterface; +use Symfony\Component\Console\Completion\Output\FishCompletionOutput; +use Symfony\Component\Console\Completion\Output\ZshCompletionOutput; +use Symfony\Component\Console\Exception\CommandNotFoundException; +use Symfony\Component\Console\Exception\ExceptionInterface; +use Symfony\Component\Console\Input\InputInterface; +use Symfony\Component\Console\Input\InputOption; +use Symfony\Component\Console\Output\OutputInterface; + +/** + * Responsible for providing the values to the shell completion. + * + * @author Wouter de Jong + */ +#[AsCommand(name: '|_complete', description: 'Internal command to provide shell completion suggestions')] +final class CompleteCommand extends Command +{ + public const COMPLETION_API_VERSION = '1'; + + /** + * @deprecated since Symfony 6.1 + */ + protected static $defaultName = '|_complete'; + + /** + * @deprecated since Symfony 6.1 + */ + protected static $defaultDescription = 'Internal command to provide shell completion suggestions'; + + private array $completionOutputs; + + private bool $isDebug = false; + + /** + * @param array> $completionOutputs A list of additional completion outputs, with shell name as key and FQCN as value + */ + public function __construct(array $completionOutputs = []) + { + // must be set before the parent constructor, as the property value is used in configure() + $this->completionOutputs = $completionOutputs + [ + 'bash' => BashCompletionOutput::class, + 'fish' => FishCompletionOutput::class, + 'zsh' => ZshCompletionOutput::class, + ]; + + parent::__construct(); + } + + protected function configure(): void + { + $this + ->addOption('shell', 's', InputOption::VALUE_REQUIRED, 'The shell type ("'.implode('", "', array_keys($this->completionOutputs)).'")') + ->addOption('input', 'i', InputOption::VALUE_REQUIRED | InputOption::VALUE_IS_ARRAY, 'An array of input tokens (e.g. COMP_WORDS or argv)') + ->addOption('current', 'c', InputOption::VALUE_REQUIRED, 'The index of the "input" array that the cursor is in (e.g. COMP_CWORD)') + ->addOption('api-version', 'a', InputOption::VALUE_REQUIRED, 'The API version of the completion script') + ->addOption('symfony', 'S', InputOption::VALUE_REQUIRED, 'deprecated') + ; + } + + protected function initialize(InputInterface $input, OutputInterface $output): void + { + $this->isDebug = filter_var(getenv('SYMFONY_COMPLETION_DEBUG'), \FILTER_VALIDATE_BOOL); + } + + protected function execute(InputInterface $input, OutputInterface $output): int + { + try { + // "symfony" must be kept for compat with the shell scripts generated by Symfony Console 5.4 - 6.1 + $version = $input->getOption('symfony') ? '1' : $input->getOption('api-version'); + if ($version && version_compare($version, self::COMPLETION_API_VERSION, '<')) { + $message = sprintf('Completion script version is not supported ("%s" given, ">=%s" required).', $version, self::COMPLETION_API_VERSION); + $this->log($message); + + $output->writeln($message.' Install the Symfony completion script again by using the "completion" command.'); + + return 126; + } + + $shell = $input->getOption('shell'); + if (!$shell) { + throw new \RuntimeException('The "--shell" option must be set.'); + } + + if (!$completionOutput = $this->completionOutputs[$shell] ?? false) { + throw new \RuntimeException(sprintf('Shell completion is not supported for your shell: "%s" (supported: "%s").', $shell, implode('", "', array_keys($this->completionOutputs)))); + } + + $completionInput = $this->createCompletionInput($input); + $suggestions = new CompletionSuggestions(); + + $this->log([ + '', + ''.date('Y-m-d H:i:s').'', + 'Input: ("|" indicates the cursor position)', + ' '.(string) $completionInput, + 'Command:', + ' '.(string) implode(' ', $_SERVER['argv']), + 'Messages:', + ]); + + $command = $this->findCommand($completionInput, $output); + if (null === $command) { + $this->log(' No command found, completing using the Application class.'); + + $this->getApplication()->complete($completionInput, $suggestions); + } elseif ( + $completionInput->mustSuggestArgumentValuesFor('command') + && $command->getName() !== $completionInput->getCompletionValue() + && !\in_array($completionInput->getCompletionValue(), $command->getAliases(), true) + ) { + $this->log(' No command found, completing using the Application class.'); + + // expand shortcut names ("cache:cl") into their full name ("cache:clear") + $suggestions->suggestValues(array_filter(array_merge([$command->getName()], $command->getAliases()))); + } else { + $command->mergeApplicationDefinition(); + $completionInput->bind($command->getDefinition()); + + if (CompletionInput::TYPE_OPTION_NAME === $completionInput->getCompletionType()) { + $this->log(' Completing option names for the '.($command instanceof LazyCommand ? $command->getCommand() : $command)::class.' command.'); + + $suggestions->suggestOptions($command->getDefinition()->getOptions()); + } else { + $this->log([ + ' Completing using the '.($command instanceof LazyCommand ? $command->getCommand() : $command)::class.' class.', + ' Completing '.$completionInput->getCompletionType().' for '.$completionInput->getCompletionName().'', + ]); + if (null !== $compval = $completionInput->getCompletionValue()) { + $this->log(' Current value: '.$compval.''); + } + + $command->complete($completionInput, $suggestions); + } + } + + /** @var CompletionOutputInterface $completionOutput */ + $completionOutput = new $completionOutput(); + + $this->log('Suggestions:'); + if ($options = $suggestions->getOptionSuggestions()) { + $this->log(' --'.implode(' --', array_map(fn ($o) => $o->getName(), $options))); + } elseif ($values = $suggestions->getValueSuggestions()) { + $this->log(' '.implode(' ', $values)); + } else { + $this->log(' No suggestions were provided'); + } + + $completionOutput->write($suggestions, $output); + } catch (\Throwable $e) { + $this->log([ + 'Error!', + (string) $e, + ]); + + if ($output->isDebug()) { + throw $e; + } + + return 2; + } + + return 0; + } + + private function createCompletionInput(InputInterface $input): CompletionInput + { + $currentIndex = $input->getOption('current'); + if (!$currentIndex || !ctype_digit($currentIndex)) { + throw new \RuntimeException('The "--current" option must be set and it must be an integer.'); + } + + $completionInput = CompletionInput::fromTokens($input->getOption('input'), (int) $currentIndex); + + try { + $completionInput->bind($this->getApplication()->getDefinition()); + } catch (ExceptionInterface) { + } + + return $completionInput; + } + + private function findCommand(CompletionInput $completionInput, OutputInterface $output): ?Command + { + try { + $inputName = $completionInput->getFirstArgument(); + if (null === $inputName) { + return null; + } + + return $this->getApplication()->find($inputName); + } catch (CommandNotFoundException) { + } + + return null; + } + + private function log($messages): void + { + if (!$this->isDebug) { + return; + } + + $commandName = basename($_SERVER['argv'][0]); + file_put_contents(sys_get_temp_dir().'/sf_'.$commandName.'.log', implode(\PHP_EOL, (array) $messages).\PHP_EOL, \FILE_APPEND); + } +} diff --git a/3rdparty/symfony/console/Command/DumpCompletionCommand.php b/3rdparty/symfony/console/Command/DumpCompletionCommand.php new file mode 100644 index 00000000..51b613a1 --- /dev/null +++ b/3rdparty/symfony/console/Command/DumpCompletionCommand.php @@ -0,0 +1,161 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Component\Console\Command; + +use Symfony\Component\Console\Attribute\AsCommand; +use Symfony\Component\Console\Input\InputArgument; +use Symfony\Component\Console\Input\InputInterface; +use Symfony\Component\Console\Input\InputOption; +use Symfony\Component\Console\Output\ConsoleOutputInterface; +use Symfony\Component\Console\Output\OutputInterface; +use Symfony\Component\Process\Process; + +/** + * Dumps the completion script for the current shell. + * + * @author Wouter de Jong + */ +#[AsCommand(name: 'completion', description: 'Dump the shell completion script')] +final class DumpCompletionCommand extends Command +{ + /** + * @deprecated since Symfony 6.1 + */ + protected static $defaultName = 'completion'; + + /** + * @deprecated since Symfony 6.1 + */ + protected static $defaultDescription = 'Dump the shell completion script'; + + private array $supportedShells; + + protected function configure(): void + { + $fullCommand = $_SERVER['PHP_SELF']; + $commandName = basename($fullCommand); + $fullCommand = @realpath($fullCommand) ?: $fullCommand; + + $shell = $this->guessShell(); + [$rcFile, $completionFile] = match ($shell) { + 'fish' => ['~/.config/fish/config.fish', "/etc/fish/completions/$commandName.fish"], + 'zsh' => ['~/.zshrc', '$fpath[1]/_'.$commandName], + default => ['~/.bashrc', "/etc/bash_completion.d/$commandName"], + }; + + $supportedShells = implode(', ', $this->getSupportedShells()); + + $this + ->setHelp(<<%command.name% command dumps the shell completion script required +to use shell autocompletion (currently, {$supportedShells} completion are supported). + +Static installation +------------------- + +Dump the script to a global completion file and restart your shell: + + %command.full_name% {$shell} | sudo tee {$completionFile} + +Or dump the script to a local file and source it: + + %command.full_name% {$shell} > completion.sh + + # source the file whenever you use the project + source completion.sh + + # or add this line at the end of your "{$rcFile}" file: + source /path/to/completion.sh + +Dynamic installation +-------------------- + +Add this to the end of your shell configuration file (e.g. "{$rcFile}"): + + eval "$({$fullCommand} completion {$shell})" +EOH + ) + ->addArgument('shell', InputArgument::OPTIONAL, 'The shell type (e.g. "bash"), the value of the "$SHELL" env var will be used if this is not given', null, $this->getSupportedShells(...)) + ->addOption('debug', null, InputOption::VALUE_NONE, 'Tail the completion debug log') + ; + } + + protected function execute(InputInterface $input, OutputInterface $output): int + { + $commandName = basename($_SERVER['argv'][0]); + + if ($input->getOption('debug')) { + $this->tailDebugLog($commandName, $output); + + return 0; + } + + $shell = $input->getArgument('shell') ?? self::guessShell(); + $completionFile = __DIR__.'/../Resources/completion.'.$shell; + if (!file_exists($completionFile)) { + $supportedShells = $this->getSupportedShells(); + + if ($output instanceof ConsoleOutputInterface) { + $output = $output->getErrorOutput(); + } + if ($shell) { + $output->writeln(sprintf('Detected shell "%s", which is not supported by Symfony shell completion (supported shells: "%s").', $shell, implode('", "', $supportedShells))); + } else { + $output->writeln(sprintf('Shell not detected, Symfony shell completion only supports "%s").', implode('", "', $supportedShells))); + } + + return 2; + } + + $output->write(str_replace(['{{ COMMAND_NAME }}', '{{ VERSION }}'], [$commandName, CompleteCommand::COMPLETION_API_VERSION], file_get_contents($completionFile))); + + return 0; + } + + private static function guessShell(): string + { + return basename($_SERVER['SHELL'] ?? ''); + } + + private function tailDebugLog(string $commandName, OutputInterface $output): void + { + $debugFile = sys_get_temp_dir().'/sf_'.$commandName.'.log'; + if (!file_exists($debugFile)) { + touch($debugFile); + } + $process = new Process(['tail', '-f', $debugFile], null, null, null, 0); + $process->run(function (string $type, string $line) use ($output): void { + $output->write($line); + }); + } + + /** + * @return string[] + */ + private function getSupportedShells(): array + { + if (isset($this->supportedShells)) { + return $this->supportedShells; + } + + $shells = []; + + foreach (new \DirectoryIterator(__DIR__.'/../Resources/') as $file) { + if (str_starts_with($file->getBasename(), 'completion.') && $file->isFile()) { + $shells[] = $file->getExtension(); + } + } + sort($shells); + + return $this->supportedShells = $shells; + } +} diff --git a/3rdparty/symfony/console/Command/HelpCommand.php b/3rdparty/symfony/console/Command/HelpCommand.php new file mode 100644 index 00000000..e6447b05 --- /dev/null +++ b/3rdparty/symfony/console/Command/HelpCommand.php @@ -0,0 +1,82 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Component\Console\Command; + +use Symfony\Component\Console\Descriptor\ApplicationDescription; +use Symfony\Component\Console\Helper\DescriptorHelper; +use Symfony\Component\Console\Input\InputArgument; +use Symfony\Component\Console\Input\InputInterface; +use Symfony\Component\Console\Input\InputOption; +use Symfony\Component\Console\Output\OutputInterface; + +/** + * HelpCommand displays the help for a given command. + * + * @author Fabien Potencier + */ +class HelpCommand extends Command +{ + private Command $command; + + /** + * @return void + */ + protected function configure() + { + $this->ignoreValidationErrors(); + + $this + ->setName('help') + ->setDefinition([ + new InputArgument('command_name', InputArgument::OPTIONAL, 'The command name', 'help', fn () => array_keys((new ApplicationDescription($this->getApplication()))->getCommands())), + new InputOption('format', null, InputOption::VALUE_REQUIRED, 'The output format (txt, xml, json, or md)', 'txt', fn () => (new DescriptorHelper())->getFormats()), + new InputOption('raw', null, InputOption::VALUE_NONE, 'To output raw command help'), + ]) + ->setDescription('Display help for a command') + ->setHelp(<<<'EOF' +The %command.name% command displays help for a given command: + + %command.full_name% list + +You can also output the help in other formats by using the --format option: + + %command.full_name% --format=xml list + +To display the list of available commands, please use the list command. +EOF + ) + ; + } + + /** + * @return void + */ + public function setCommand(Command $command) + { + $this->command = $command; + } + + protected function execute(InputInterface $input, OutputInterface $output): int + { + $this->command ??= $this->getApplication()->find($input->getArgument('command_name')); + + $helper = new DescriptorHelper(); + $helper->describe($output, $this->command, [ + 'format' => $input->getOption('format'), + 'raw_text' => $input->getOption('raw'), + ]); + + unset($this->command); + + return 0; + } +} diff --git a/3rdparty/symfony/console/Command/LazyCommand.php b/3rdparty/symfony/console/Command/LazyCommand.php new file mode 100644 index 00000000..b94da666 --- /dev/null +++ b/3rdparty/symfony/console/Command/LazyCommand.php @@ -0,0 +1,207 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Component\Console\Command; + +use Symfony\Component\Console\Application; +use Symfony\Component\Console\Completion\CompletionInput; +use Symfony\Component\Console\Completion\CompletionSuggestions; +use Symfony\Component\Console\Completion\Suggestion; +use Symfony\Component\Console\Helper\HelperInterface; +use Symfony\Component\Console\Helper\HelperSet; +use Symfony\Component\Console\Input\InputDefinition; +use Symfony\Component\Console\Input\InputInterface; +use Symfony\Component\Console\Output\OutputInterface; + +/** + * @author Nicolas Grekas + */ +final class LazyCommand extends Command +{ + private \Closure|Command $command; + private ?bool $isEnabled; + + public function __construct(string $name, array $aliases, string $description, bool $isHidden, \Closure $commandFactory, ?bool $isEnabled = true) + { + $this->setName($name) + ->setAliases($aliases) + ->setHidden($isHidden) + ->setDescription($description); + + $this->command = $commandFactory; + $this->isEnabled = $isEnabled; + } + + public function ignoreValidationErrors(): void + { + $this->getCommand()->ignoreValidationErrors(); + } + + public function setApplication(?Application $application = null): void + { + if (1 > \func_num_args()) { + trigger_deprecation('symfony/console', '6.2', 'Calling "%s()" without any arguments is deprecated, pass null explicitly instead.', __METHOD__); + } + if ($this->command instanceof parent) { + $this->command->setApplication($application); + } + + parent::setApplication($application); + } + + public function setHelperSet(HelperSet $helperSet): void + { + if ($this->command instanceof parent) { + $this->command->setHelperSet($helperSet); + } + + parent::setHelperSet($helperSet); + } + + public function isEnabled(): bool + { + return $this->isEnabled ?? $this->getCommand()->isEnabled(); + } + + public function run(InputInterface $input, OutputInterface $output): int + { + return $this->getCommand()->run($input, $output); + } + + public function complete(CompletionInput $input, CompletionSuggestions $suggestions): void + { + $this->getCommand()->complete($input, $suggestions); + } + + public function setCode(callable $code): static + { + $this->getCommand()->setCode($code); + + return $this; + } + + /** + * @internal + */ + public function mergeApplicationDefinition(bool $mergeArgs = true): void + { + $this->getCommand()->mergeApplicationDefinition($mergeArgs); + } + + public function setDefinition(array|InputDefinition $definition): static + { + $this->getCommand()->setDefinition($definition); + + return $this; + } + + public function getDefinition(): InputDefinition + { + return $this->getCommand()->getDefinition(); + } + + public function getNativeDefinition(): InputDefinition + { + return $this->getCommand()->getNativeDefinition(); + } + + /** + * @param array|\Closure(CompletionInput,CompletionSuggestions):list $suggestedValues The values used for input completion + */ + public function addArgument(string $name, ?int $mode = null, string $description = '', mixed $default = null /* array|\Closure $suggestedValues = [] */): static + { + $suggestedValues = 5 <= \func_num_args() ? func_get_arg(4) : []; + $this->getCommand()->addArgument($name, $mode, $description, $default, $suggestedValues); + + return $this; + } + + /** + * @param array|\Closure(CompletionInput,CompletionSuggestions):list $suggestedValues The values used for input completion + */ + public function addOption(string $name, string|array|null $shortcut = null, ?int $mode = null, string $description = '', mixed $default = null /* array|\Closure $suggestedValues = [] */): static + { + $suggestedValues = 6 <= \func_num_args() ? func_get_arg(5) : []; + $this->getCommand()->addOption($name, $shortcut, $mode, $description, $default, $suggestedValues); + + return $this; + } + + public function setProcessTitle(string $title): static + { + $this->getCommand()->setProcessTitle($title); + + return $this; + } + + public function setHelp(string $help): static + { + $this->getCommand()->setHelp($help); + + return $this; + } + + public function getHelp(): string + { + return $this->getCommand()->getHelp(); + } + + public function getProcessedHelp(): string + { + return $this->getCommand()->getProcessedHelp(); + } + + public function getSynopsis(bool $short = false): string + { + return $this->getCommand()->getSynopsis($short); + } + + public function addUsage(string $usage): static + { + $this->getCommand()->addUsage($usage); + + return $this; + } + + public function getUsages(): array + { + return $this->getCommand()->getUsages(); + } + + public function getHelper(string $name): HelperInterface + { + return $this->getCommand()->getHelper($name); + } + + public function getCommand(): parent + { + if (!$this->command instanceof \Closure) { + return $this->command; + } + + $command = $this->command = ($this->command)(); + $command->setApplication($this->getApplication()); + + if (null !== $this->getHelperSet()) { + $command->setHelperSet($this->getHelperSet()); + } + + $command->setName($this->getName()) + ->setAliases($this->getAliases()) + ->setHidden($this->isHidden()) + ->setDescription($this->getDescription()); + + // Will throw if the command is not correctly initialized. + $command->getDefinition(); + + return $command; + } +} diff --git a/3rdparty/symfony/console/Command/ListCommand.php b/3rdparty/symfony/console/Command/ListCommand.php new file mode 100644 index 00000000..5850c3d7 --- /dev/null +++ b/3rdparty/symfony/console/Command/ListCommand.php @@ -0,0 +1,75 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Component\Console\Command; + +use Symfony\Component\Console\Descriptor\ApplicationDescription; +use Symfony\Component\Console\Helper\DescriptorHelper; +use Symfony\Component\Console\Input\InputArgument; +use Symfony\Component\Console\Input\InputInterface; +use Symfony\Component\Console\Input\InputOption; +use Symfony\Component\Console\Output\OutputInterface; + +/** + * ListCommand displays the list of all available commands for the application. + * + * @author Fabien Potencier + */ +class ListCommand extends Command +{ + /** + * @return void + */ + protected function configure() + { + $this + ->setName('list') + ->setDefinition([ + new InputArgument('namespace', InputArgument::OPTIONAL, 'The namespace name', null, fn () => array_keys((new ApplicationDescription($this->getApplication()))->getNamespaces())), + new InputOption('raw', null, InputOption::VALUE_NONE, 'To output raw command list'), + new InputOption('format', null, InputOption::VALUE_REQUIRED, 'The output format (txt, xml, json, or md)', 'txt', fn () => (new DescriptorHelper())->getFormats()), + new InputOption('short', null, InputOption::VALUE_NONE, 'To skip describing commands\' arguments'), + ]) + ->setDescription('List commands') + ->setHelp(<<<'EOF' +The %command.name% command lists all commands: + + %command.full_name% + +You can also display the commands for a specific namespace: + + %command.full_name% test + +You can also output the information in other formats by using the --format option: + + %command.full_name% --format=xml + +It's also possible to get raw list of commands (useful for embedding command runner): + + %command.full_name% --raw +EOF + ) + ; + } + + protected function execute(InputInterface $input, OutputInterface $output): int + { + $helper = new DescriptorHelper(); + $helper->describe($output, $this->getApplication(), [ + 'format' => $input->getOption('format'), + 'raw_text' => $input->getOption('raw'), + 'namespace' => $input->getArgument('namespace'), + 'short' => $input->getOption('short'), + ]); + + return 0; + } +} diff --git a/3rdparty/symfony/console/Command/LockableTrait.php b/3rdparty/symfony/console/Command/LockableTrait.php new file mode 100644 index 00000000..cd7548f0 --- /dev/null +++ b/3rdparty/symfony/console/Command/LockableTrait.php @@ -0,0 +1,68 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Component\Console\Command; + +use Symfony\Component\Console\Exception\LogicException; +use Symfony\Component\Lock\LockFactory; +use Symfony\Component\Lock\LockInterface; +use Symfony\Component\Lock\Store\FlockStore; +use Symfony\Component\Lock\Store\SemaphoreStore; + +/** + * Basic lock feature for commands. + * + * @author Geoffrey Brier + */ +trait LockableTrait +{ + private ?LockInterface $lock = null; + + /** + * Locks a command. + */ + private function lock(?string $name = null, bool $blocking = false): bool + { + if (!class_exists(SemaphoreStore::class)) { + throw new LogicException('To enable the locking feature you must install the symfony/lock component. Try running "composer require symfony/lock".'); + } + + if (null !== $this->lock) { + throw new LogicException('A lock is already in place.'); + } + + if (SemaphoreStore::isSupported()) { + $store = new SemaphoreStore(); + } else { + $store = new FlockStore(); + } + + $this->lock = (new LockFactory($store))->createLock($name ?: $this->getName()); + if (!$this->lock->acquire($blocking)) { + $this->lock = null; + + return false; + } + + return true; + } + + /** + * Releases the command lock if there is one. + */ + private function release(): void + { + if ($this->lock) { + $this->lock->release(); + $this->lock = null; + } + } +} diff --git a/3rdparty/symfony/console/Command/SignalableCommandInterface.php b/3rdparty/symfony/console/Command/SignalableCommandInterface.php new file mode 100644 index 00000000..f8eb8e52 --- /dev/null +++ b/3rdparty/symfony/console/Command/SignalableCommandInterface.php @@ -0,0 +1,34 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Component\Console\Command; + +/** + * Interface for command reacting to signal. + * + * @author Grégoire Pineau + */ +interface SignalableCommandInterface +{ + /** + * Returns the list of signals to subscribe. + */ + public function getSubscribedSignals(): array; + + /** + * The method will be called when the application is signaled. + * + * @param int|false $previousExitCode + * + * @return int|false The exit code to return or false to continue the normal execution + */ + public function handleSignal(int $signal, /* int|false $previousExitCode = 0 */); +} diff --git a/3rdparty/symfony/console/Command/TraceableCommand.php b/3rdparty/symfony/console/Command/TraceableCommand.php new file mode 100644 index 00000000..9ffb68da --- /dev/null +++ b/3rdparty/symfony/console/Command/TraceableCommand.php @@ -0,0 +1,356 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Component\Console\Command; + +use Symfony\Component\Console\Application; +use Symfony\Component\Console\Completion\CompletionInput; +use Symfony\Component\Console\Completion\CompletionSuggestions; +use Symfony\Component\Console\Helper\HelperInterface; +use Symfony\Component\Console\Helper\HelperSet; +use Symfony\Component\Console\Input\InputDefinition; +use Symfony\Component\Console\Input\InputInterface; +use Symfony\Component\Console\Output\ConsoleOutputInterface; +use Symfony\Component\Console\Output\OutputInterface; +use Symfony\Component\Stopwatch\Stopwatch; + +/** + * @internal + * + * @author Jules Pietri + */ +final class TraceableCommand extends Command implements SignalableCommandInterface +{ + public readonly Command $command; + public int $exitCode; + public ?int $interruptedBySignal = null; + public bool $ignoreValidation; + public bool $isInteractive = false; + public string $duration = 'n/a'; + public string $maxMemoryUsage = 'n/a'; + public InputInterface $input; + public OutputInterface $output; + /** @var array */ + public array $arguments; + /** @var array */ + public array $options; + /** @var array */ + public array $interactiveInputs = []; + public array $handledSignals = []; + + public function __construct( + Command $command, + private readonly Stopwatch $stopwatch, + ) { + if ($command instanceof LazyCommand) { + $command = $command->getCommand(); + } + + $this->command = $command; + + // prevent call to self::getDefaultDescription() + $this->setDescription($command->getDescription()); + + parent::__construct($command->getName()); + + // init below enables calling {@see parent::run()} + [$code, $processTitle, $ignoreValidationErrors] = \Closure::bind(function () { + return [$this->code, $this->processTitle, $this->ignoreValidationErrors]; + }, $command, Command::class)(); + + if (\is_callable($code)) { + $this->setCode($code); + } + + if ($processTitle) { + parent::setProcessTitle($processTitle); + } + + if ($ignoreValidationErrors) { + parent::ignoreValidationErrors(); + } + + $this->ignoreValidation = $ignoreValidationErrors; + } + + public function __call(string $name, array $arguments): mixed + { + return $this->command->{$name}(...$arguments); + } + + public function getSubscribedSignals(): array + { + return $this->command instanceof SignalableCommandInterface ? $this->command->getSubscribedSignals() : []; + } + + public function handleSignal(int $signal, int|false $previousExitCode = 0): int|false + { + if (!$this->command instanceof SignalableCommandInterface) { + return false; + } + + $event = $this->stopwatch->start($this->getName().'.handle_signal'); + + $exit = $this->command->handleSignal($signal, $previousExitCode); + + $event->stop(); + + if (!isset($this->handledSignals[$signal])) { + $this->handledSignals[$signal] = [ + 'handled' => 0, + 'duration' => 0, + 'memory' => 0, + ]; + } + + ++$this->handledSignals[$signal]['handled']; + $this->handledSignals[$signal]['duration'] += $event->getDuration(); + $this->handledSignals[$signal]['memory'] = max( + $this->handledSignals[$signal]['memory'], + $event->getMemory() >> 20 + ); + + return $exit; + } + + /** + * {@inheritdoc} + * + * Calling parent method is required to be used in {@see parent::run()}. + */ + public function ignoreValidationErrors(): void + { + $this->ignoreValidation = true; + $this->command->ignoreValidationErrors(); + + parent::ignoreValidationErrors(); + } + + public function setApplication(?Application $application = null): void + { + $this->command->setApplication($application); + } + + public function getApplication(): ?Application + { + return $this->command->getApplication(); + } + + public function setHelperSet(HelperSet $helperSet): void + { + $this->command->setHelperSet($helperSet); + } + + public function getHelperSet(): ?HelperSet + { + return $this->command->getHelperSet(); + } + + public function isEnabled(): bool + { + return $this->command->isEnabled(); + } + + public function complete(CompletionInput $input, CompletionSuggestions $suggestions): void + { + $this->command->complete($input, $suggestions); + } + + /** + * {@inheritdoc} + * + * Calling parent method is required to be used in {@see parent::run()}. + */ + public function setCode(callable $code): static + { + $this->command->setCode($code); + + return parent::setCode(function (InputInterface $input, OutputInterface $output) use ($code): int { + $event = $this->stopwatch->start($this->getName().'.code'); + + $this->exitCode = $code($input, $output); + + $event->stop(); + + return $this->exitCode; + }); + } + + /** + * @internal + */ + public function mergeApplicationDefinition(bool $mergeArgs = true): void + { + $this->command->mergeApplicationDefinition($mergeArgs); + } + + public function setDefinition(array|InputDefinition $definition): static + { + $this->command->setDefinition($definition); + + return $this; + } + + public function getDefinition(): InputDefinition + { + return $this->command->getDefinition(); + } + + public function getNativeDefinition(): InputDefinition + { + return $this->command->getNativeDefinition(); + } + + public function addArgument(string $name, ?int $mode = null, string $description = '', mixed $default = null, array|\Closure $suggestedValues = []): static + { + $this->command->addArgument($name, $mode, $description, $default, $suggestedValues); + + return $this; + } + + public function addOption(string $name, string|array|null $shortcut = null, ?int $mode = null, string $description = '', mixed $default = null, array|\Closure $suggestedValues = []): static + { + $this->command->addOption($name, $shortcut, $mode, $description, $default, $suggestedValues); + + return $this; + } + + /** + * {@inheritdoc} + * + * Calling parent method is required to be used in {@see parent::run()}. + */ + public function setProcessTitle(string $title): static + { + $this->command->setProcessTitle($title); + + return parent::setProcessTitle($title); + } + + public function setHelp(string $help): static + { + $this->command->setHelp($help); + + return $this; + } + + public function getHelp(): string + { + return $this->command->getHelp(); + } + + public function getProcessedHelp(): string + { + return $this->command->getProcessedHelp(); + } + + public function getSynopsis(bool $short = false): string + { + return $this->command->getSynopsis($short); + } + + public function addUsage(string $usage): static + { + $this->command->addUsage($usage); + + return $this; + } + + public function getUsages(): array + { + return $this->command->getUsages(); + } + + public function getHelper(string $name): HelperInterface + { + return $this->command->getHelper($name); + } + + public function run(InputInterface $input, OutputInterface $output): int + { + $this->input = $input; + $this->output = $output; + $this->arguments = $input->getArguments(); + $this->options = $input->getOptions(); + $event = $this->stopwatch->start($this->getName(), 'command'); + + try { + $this->exitCode = parent::run($input, $output); + } finally { + $event->stop(); + + if ($output instanceof ConsoleOutputInterface && $output->isDebug()) { + $output->getErrorOutput()->writeln((string) $event); + } + + $this->duration = $event->getDuration().' ms'; + $this->maxMemoryUsage = ($event->getMemory() >> 20).' MiB'; + + if ($this->isInteractive) { + $this->extractInteractiveInputs($input->getArguments(), $input->getOptions()); + } + } + + return $this->exitCode; + } + + protected function initialize(InputInterface $input, OutputInterface $output): void + { + $event = $this->stopwatch->start($this->getName().'.init', 'command'); + + $this->command->initialize($input, $output); + + $event->stop(); + } + + protected function interact(InputInterface $input, OutputInterface $output): void + { + if (!$this->isInteractive = Command::class !== (new \ReflectionMethod($this->command, 'interact'))->getDeclaringClass()->getName()) { + return; + } + + $event = $this->stopwatch->start($this->getName().'.interact', 'command'); + + $this->command->interact($input, $output); + + $event->stop(); + } + + protected function execute(InputInterface $input, OutputInterface $output): int + { + $event = $this->stopwatch->start($this->getName().'.execute', 'command'); + + $exitCode = $this->command->execute($input, $output); + + $event->stop(); + + return $exitCode; + } + + private function extractInteractiveInputs(array $arguments, array $options): void + { + foreach ($arguments as $argName => $argValue) { + if (\array_key_exists($argName, $this->arguments) && $this->arguments[$argName] === $argValue) { + continue; + } + + $this->interactiveInputs[$argName] = $argValue; + } + + foreach ($options as $optName => $optValue) { + if (\array_key_exists($optName, $this->options) && $this->options[$optName] === $optValue) { + continue; + } + + $this->interactiveInputs['--'.$optName] = $optValue; + } + } +} diff --git a/3rdparty/symfony/console/CommandLoader/CommandLoaderInterface.php b/3rdparty/symfony/console/CommandLoader/CommandLoaderInterface.php new file mode 100644 index 00000000..b6b637ce --- /dev/null +++ b/3rdparty/symfony/console/CommandLoader/CommandLoaderInterface.php @@ -0,0 +1,38 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Component\Console\CommandLoader; + +use Symfony\Component\Console\Command\Command; +use Symfony\Component\Console\Exception\CommandNotFoundException; + +/** + * @author Robin Chalas + */ +interface CommandLoaderInterface +{ + /** + * Loads a command. + * + * @throws CommandNotFoundException + */ + public function get(string $name): Command; + + /** + * Checks if a command exists. + */ + public function has(string $name): bool; + + /** + * @return string[] + */ + public function getNames(): array; +} diff --git a/3rdparty/symfony/console/CommandLoader/ContainerCommandLoader.php b/3rdparty/symfony/console/CommandLoader/ContainerCommandLoader.php new file mode 100644 index 00000000..bfa0ac46 --- /dev/null +++ b/3rdparty/symfony/console/CommandLoader/ContainerCommandLoader.php @@ -0,0 +1,55 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Component\Console\CommandLoader; + +use Psr\Container\ContainerInterface; +use Symfony\Component\Console\Command\Command; +use Symfony\Component\Console\Exception\CommandNotFoundException; + +/** + * Loads commands from a PSR-11 container. + * + * @author Robin Chalas + */ +class ContainerCommandLoader implements CommandLoaderInterface +{ + private ContainerInterface $container; + private array $commandMap; + + /** + * @param array $commandMap An array with command names as keys and service ids as values + */ + public function __construct(ContainerInterface $container, array $commandMap) + { + $this->container = $container; + $this->commandMap = $commandMap; + } + + public function get(string $name): Command + { + if (!$this->has($name)) { + throw new CommandNotFoundException(sprintf('Command "%s" does not exist.', $name)); + } + + return $this->container->get($this->commandMap[$name]); + } + + public function has(string $name): bool + { + return isset($this->commandMap[$name]) && $this->container->has($this->commandMap[$name]); + } + + public function getNames(): array + { + return array_keys($this->commandMap); + } +} diff --git a/3rdparty/symfony/console/CommandLoader/FactoryCommandLoader.php b/3rdparty/symfony/console/CommandLoader/FactoryCommandLoader.php new file mode 100644 index 00000000..9ced75ae --- /dev/null +++ b/3rdparty/symfony/console/CommandLoader/FactoryCommandLoader.php @@ -0,0 +1,54 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Component\Console\CommandLoader; + +use Symfony\Component\Console\Command\Command; +use Symfony\Component\Console\Exception\CommandNotFoundException; + +/** + * A simple command loader using factories to instantiate commands lazily. + * + * @author Maxime Steinhausser + */ +class FactoryCommandLoader implements CommandLoaderInterface +{ + private array $factories; + + /** + * @param callable[] $factories Indexed by command names + */ + public function __construct(array $factories) + { + $this->factories = $factories; + } + + public function has(string $name): bool + { + return isset($this->factories[$name]); + } + + public function get(string $name): Command + { + if (!isset($this->factories[$name])) { + throw new CommandNotFoundException(sprintf('Command "%s" does not exist.', $name)); + } + + $factory = $this->factories[$name]; + + return $factory(); + } + + public function getNames(): array + { + return array_keys($this->factories); + } +} diff --git a/3rdparty/symfony/console/Completion/CompletionInput.php b/3rdparty/symfony/console/Completion/CompletionInput.php new file mode 100644 index 00000000..79c2f659 --- /dev/null +++ b/3rdparty/symfony/console/Completion/CompletionInput.php @@ -0,0 +1,248 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Component\Console\Completion; + +use Symfony\Component\Console\Exception\RuntimeException; +use Symfony\Component\Console\Input\ArgvInput; +use Symfony\Component\Console\Input\InputDefinition; +use Symfony\Component\Console\Input\InputOption; + +/** + * An input specialized for shell completion. + * + * This input allows unfinished option names or values and exposes what kind of + * completion is expected. + * + * @author Wouter de Jong + */ +final class CompletionInput extends ArgvInput +{ + public const TYPE_ARGUMENT_VALUE = 'argument_value'; + public const TYPE_OPTION_VALUE = 'option_value'; + public const TYPE_OPTION_NAME = 'option_name'; + public const TYPE_NONE = 'none'; + + private array $tokens; + private int $currentIndex; + private string $completionType; + private ?string $completionName = null; + private string $completionValue = ''; + + /** + * Converts a terminal string into tokens. + * + * This is required for shell completions without COMP_WORDS support. + */ + public static function fromString(string $inputStr, int $currentIndex): self + { + preg_match_all('/(?<=^|\s)([\'"]?)(.+?)(?tokens = $tokens; + $input->currentIndex = $currentIndex; + + return $input; + } + + public function bind(InputDefinition $definition): void + { + parent::bind($definition); + + $relevantToken = $this->getRelevantToken(); + if ('-' === $relevantToken[0]) { + // the current token is an input option: complete either option name or option value + [$optionToken, $optionValue] = explode('=', $relevantToken, 2) + ['', '']; + + $option = $this->getOptionFromToken($optionToken); + if (null === $option && !$this->isCursorFree()) { + $this->completionType = self::TYPE_OPTION_NAME; + $this->completionValue = $relevantToken; + + return; + } + + if ($option?->acceptValue()) { + $this->completionType = self::TYPE_OPTION_VALUE; + $this->completionName = $option->getName(); + $this->completionValue = $optionValue ?: (!str_starts_with($optionToken, '--') ? substr($optionToken, 2) : ''); + + return; + } + } + + $previousToken = $this->tokens[$this->currentIndex - 1]; + if ('-' === $previousToken[0] && '' !== trim($previousToken, '-')) { + // check if previous option accepted a value + $previousOption = $this->getOptionFromToken($previousToken); + if ($previousOption?->acceptValue()) { + $this->completionType = self::TYPE_OPTION_VALUE; + $this->completionName = $previousOption->getName(); + $this->completionValue = $relevantToken; + + return; + } + } + + // complete argument value + $this->completionType = self::TYPE_ARGUMENT_VALUE; + + foreach ($this->definition->getArguments() as $argumentName => $argument) { + if (!isset($this->arguments[$argumentName])) { + break; + } + + $argumentValue = $this->arguments[$argumentName]; + $this->completionName = $argumentName; + if (\is_array($argumentValue)) { + $this->completionValue = $argumentValue ? $argumentValue[array_key_last($argumentValue)] : null; + } else { + $this->completionValue = $argumentValue; + } + } + + if ($this->currentIndex >= \count($this->tokens)) { + if (!isset($this->arguments[$argumentName]) || $this->definition->getArgument($argumentName)->isArray()) { + $this->completionName = $argumentName; + $this->completionValue = ''; + } else { + // we've reached the end + $this->completionType = self::TYPE_NONE; + $this->completionName = null; + $this->completionValue = ''; + } + } + } + + /** + * Returns the type of completion required. + * + * TYPE_ARGUMENT_VALUE when completing the value of an input argument + * TYPE_OPTION_VALUE when completing the value of an input option + * TYPE_OPTION_NAME when completing the name of an input option + * TYPE_NONE when nothing should be completed + * + * TYPE_OPTION_NAME and TYPE_NONE are already implemented by the Console component. + * + * @return self::TYPE_* + */ + public function getCompletionType(): string + { + return $this->completionType; + } + + /** + * The name of the input option or argument when completing a value. + * + * @return string|null returns null when completing an option name + */ + public function getCompletionName(): ?string + { + return $this->completionName; + } + + /** + * The value already typed by the user (or empty string). + */ + public function getCompletionValue(): string + { + return $this->completionValue; + } + + public function mustSuggestOptionValuesFor(string $optionName): bool + { + return self::TYPE_OPTION_VALUE === $this->getCompletionType() && $optionName === $this->getCompletionName(); + } + + public function mustSuggestArgumentValuesFor(string $argumentName): bool + { + return self::TYPE_ARGUMENT_VALUE === $this->getCompletionType() && $argumentName === $this->getCompletionName(); + } + + protected function parseToken(string $token, bool $parseOptions): bool + { + try { + return parent::parseToken($token, $parseOptions); + } catch (RuntimeException) { + // suppress errors, completed input is almost never valid + } + + return $parseOptions; + } + + private function getOptionFromToken(string $optionToken): ?InputOption + { + $optionName = ltrim($optionToken, '-'); + if (!$optionName) { + return null; + } + + if ('-' === ($optionToken[1] ?? ' ')) { + // long option name + return $this->definition->hasOption($optionName) ? $this->definition->getOption($optionName) : null; + } + + // short option name + return $this->definition->hasShortcut($optionName[0]) ? $this->definition->getOptionForShortcut($optionName[0]) : null; + } + + /** + * The token of the cursor, or the last token if the cursor is at the end of the input. + */ + private function getRelevantToken(): string + { + return $this->tokens[$this->isCursorFree() ? $this->currentIndex - 1 : $this->currentIndex]; + } + + /** + * Whether the cursor is "free" (i.e. at the end of the input preceded by a space). + */ + private function isCursorFree(): bool + { + $nrOfTokens = \count($this->tokens); + if ($this->currentIndex > $nrOfTokens) { + throw new \LogicException('Current index is invalid, it must be the number of input tokens or one more.'); + } + + return $this->currentIndex >= $nrOfTokens; + } + + public function __toString() + { + $str = ''; + foreach ($this->tokens as $i => $token) { + $str .= $token; + + if ($this->currentIndex === $i) { + $str .= '|'; + } + + $str .= ' '; + } + + if ($this->currentIndex > $i) { + $str .= '|'; + } + + return rtrim($str); + } +} diff --git a/3rdparty/symfony/console/Completion/CompletionSuggestions.php b/3rdparty/symfony/console/Completion/CompletionSuggestions.php new file mode 100644 index 00000000..549bbafb --- /dev/null +++ b/3rdparty/symfony/console/Completion/CompletionSuggestions.php @@ -0,0 +1,97 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Component\Console\Completion; + +use Symfony\Component\Console\Input\InputOption; + +/** + * Stores all completion suggestions for the current input. + * + * @author Wouter de Jong + */ +final class CompletionSuggestions +{ + private array $valueSuggestions = []; + private array $optionSuggestions = []; + + /** + * Add a suggested value for an input option or argument. + * + * @return $this + */ + public function suggestValue(string|Suggestion $value): static + { + $this->valueSuggestions[] = !$value instanceof Suggestion ? new Suggestion($value) : $value; + + return $this; + } + + /** + * Add multiple suggested values at once for an input option or argument. + * + * @param list $values + * + * @return $this + */ + public function suggestValues(array $values): static + { + foreach ($values as $value) { + $this->suggestValue($value); + } + + return $this; + } + + /** + * Add a suggestion for an input option name. + * + * @return $this + */ + public function suggestOption(InputOption $option): static + { + $this->optionSuggestions[] = $option; + + return $this; + } + + /** + * Add multiple suggestions for input option names at once. + * + * @param InputOption[] $options + * + * @return $this + */ + public function suggestOptions(array $options): static + { + foreach ($options as $option) { + $this->suggestOption($option); + } + + return $this; + } + + /** + * @return InputOption[] + */ + public function getOptionSuggestions(): array + { + return $this->optionSuggestions; + } + + /** + * @return Suggestion[] + */ + public function getValueSuggestions(): array + { + return $this->valueSuggestions; + } +} diff --git a/3rdparty/symfony/console/Completion/Output/BashCompletionOutput.php b/3rdparty/symfony/console/Completion/Output/BashCompletionOutput.php new file mode 100644 index 00000000..c6f76eb8 --- /dev/null +++ b/3rdparty/symfony/console/Completion/Output/BashCompletionOutput.php @@ -0,0 +1,33 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Component\Console\Completion\Output; + +use Symfony\Component\Console\Completion\CompletionSuggestions; +use Symfony\Component\Console\Output\OutputInterface; + +/** + * @author Wouter de Jong + */ +class BashCompletionOutput implements CompletionOutputInterface +{ + public function write(CompletionSuggestions $suggestions, OutputInterface $output): void + { + $values = $suggestions->getValueSuggestions(); + foreach ($suggestions->getOptionSuggestions() as $option) { + $values[] = '--'.$option->getName(); + if ($option->isNegatable()) { + $values[] = '--no-'.$option->getName(); + } + } + $output->writeln(implode("\n", $values)); + } +} diff --git a/3rdparty/symfony/console/Completion/Output/CompletionOutputInterface.php b/3rdparty/symfony/console/Completion/Output/CompletionOutputInterface.php new file mode 100644 index 00000000..659e5965 --- /dev/null +++ b/3rdparty/symfony/console/Completion/Output/CompletionOutputInterface.php @@ -0,0 +1,25 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Component\Console\Completion\Output; + +use Symfony\Component\Console\Completion\CompletionSuggestions; +use Symfony\Component\Console\Output\OutputInterface; + +/** + * Transforms the {@see CompletionSuggestions} object into output readable by the shell completion. + * + * @author Wouter de Jong + */ +interface CompletionOutputInterface +{ + public function write(CompletionSuggestions $suggestions, OutputInterface $output): void; +} diff --git a/3rdparty/symfony/console/Completion/Output/FishCompletionOutput.php b/3rdparty/symfony/console/Completion/Output/FishCompletionOutput.php new file mode 100644 index 00000000..d2c414e4 --- /dev/null +++ b/3rdparty/symfony/console/Completion/Output/FishCompletionOutput.php @@ -0,0 +1,33 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Component\Console\Completion\Output; + +use Symfony\Component\Console\Completion\CompletionSuggestions; +use Symfony\Component\Console\Output\OutputInterface; + +/** + * @author Guillaume Aveline + */ +class FishCompletionOutput implements CompletionOutputInterface +{ + public function write(CompletionSuggestions $suggestions, OutputInterface $output): void + { + $values = $suggestions->getValueSuggestions(); + foreach ($suggestions->getOptionSuggestions() as $option) { + $values[] = '--'.$option->getName(); + if ($option->isNegatable()) { + $values[] = '--no-'.$option->getName(); + } + } + $output->write(implode("\n", $values)); + } +} diff --git a/3rdparty/symfony/console/Completion/Output/ZshCompletionOutput.php b/3rdparty/symfony/console/Completion/Output/ZshCompletionOutput.php new file mode 100644 index 00000000..bb4ce70b --- /dev/null +++ b/3rdparty/symfony/console/Completion/Output/ZshCompletionOutput.php @@ -0,0 +1,36 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Component\Console\Completion\Output; + +use Symfony\Component\Console\Completion\CompletionSuggestions; +use Symfony\Component\Console\Output\OutputInterface; + +/** + * @author Jitendra A + */ +class ZshCompletionOutput implements CompletionOutputInterface +{ + public function write(CompletionSuggestions $suggestions, OutputInterface $output): void + { + $values = []; + foreach ($suggestions->getValueSuggestions() as $value) { + $values[] = $value->getValue().($value->getDescription() ? "\t".$value->getDescription() : ''); + } + foreach ($suggestions->getOptionSuggestions() as $option) { + $values[] = '--'.$option->getName().($option->getDescription() ? "\t".$option->getDescription() : ''); + if ($option->isNegatable()) { + $values[] = '--no-'.$option->getName().($option->getDescription() ? "\t".$option->getDescription() : ''); + } + } + $output->write(implode("\n", $values)."\n"); + } +} diff --git a/3rdparty/symfony/console/Completion/Suggestion.php b/3rdparty/symfony/console/Completion/Suggestion.php new file mode 100644 index 00000000..7392965a --- /dev/null +++ b/3rdparty/symfony/console/Completion/Suggestion.php @@ -0,0 +1,41 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Component\Console\Completion; + +/** + * Represents a single suggested value. + * + * @author Wouter de Jong + */ +class Suggestion implements \Stringable +{ + public function __construct( + private readonly string $value, + private readonly string $description = '' + ) { + } + + public function getValue(): string + { + return $this->value; + } + + public function getDescription(): string + { + return $this->description; + } + + public function __toString(): string + { + return $this->getValue(); + } +} diff --git a/3rdparty/symfony/console/ConsoleEvents.php b/3rdparty/symfony/console/ConsoleEvents.php new file mode 100644 index 00000000..6ae8f32b --- /dev/null +++ b/3rdparty/symfony/console/ConsoleEvents.php @@ -0,0 +1,72 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Component\Console; + +use Symfony\Component\Console\Event\ConsoleCommandEvent; +use Symfony\Component\Console\Event\ConsoleErrorEvent; +use Symfony\Component\Console\Event\ConsoleSignalEvent; +use Symfony\Component\Console\Event\ConsoleTerminateEvent; + +/** + * Contains all events dispatched by an Application. + * + * @author Francesco Levorato + */ +final class ConsoleEvents +{ + /** + * The COMMAND event allows you to attach listeners before any command is + * executed by the console. It also allows you to modify the command, input and output + * before they are handed to the command. + * + * @Event("Symfony\Component\Console\Event\ConsoleCommandEvent") + */ + public const COMMAND = 'console.command'; + + /** + * The SIGNAL event allows you to perform some actions + * after the command execution was interrupted. + * + * @Event("Symfony\Component\Console\Event\ConsoleSignalEvent") + */ + public const SIGNAL = 'console.signal'; + + /** + * The TERMINATE event allows you to attach listeners after a command is + * executed by the console. + * + * @Event("Symfony\Component\Console\Event\ConsoleTerminateEvent") + */ + public const TERMINATE = 'console.terminate'; + + /** + * The ERROR event occurs when an uncaught exception or error appears. + * + * This event allows you to deal with the exception/error or + * to modify the thrown exception. + * + * @Event("Symfony\Component\Console\Event\ConsoleErrorEvent") + */ + public const ERROR = 'console.error'; + + /** + * Event aliases. + * + * These aliases can be consumed by RegisterListenersPass. + */ + public const ALIASES = [ + ConsoleCommandEvent::class => self::COMMAND, + ConsoleErrorEvent::class => self::ERROR, + ConsoleSignalEvent::class => self::SIGNAL, + ConsoleTerminateEvent::class => self::TERMINATE, + ]; +} diff --git a/3rdparty/symfony/console/Cursor.php b/3rdparty/symfony/console/Cursor.php new file mode 100644 index 00000000..69fd3821 --- /dev/null +++ b/3rdparty/symfony/console/Cursor.php @@ -0,0 +1,204 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Component\Console; + +use Symfony\Component\Console\Output\OutputInterface; + +/** + * @author Pierre du Plessis + */ +final class Cursor +{ + private OutputInterface $output; + /** @var resource */ + private $input; + + /** + * @param resource|null $input + */ + public function __construct(OutputInterface $output, $input = null) + { + $this->output = $output; + $this->input = $input ?? (\defined('STDIN') ? \STDIN : fopen('php://input', 'r+')); + } + + /** + * @return $this + */ + public function moveUp(int $lines = 1): static + { + $this->output->write(sprintf("\x1b[%dA", $lines)); + + return $this; + } + + /** + * @return $this + */ + public function moveDown(int $lines = 1): static + { + $this->output->write(sprintf("\x1b[%dB", $lines)); + + return $this; + } + + /** + * @return $this + */ + public function moveRight(int $columns = 1): static + { + $this->output->write(sprintf("\x1b[%dC", $columns)); + + return $this; + } + + /** + * @return $this + */ + public function moveLeft(int $columns = 1): static + { + $this->output->write(sprintf("\x1b[%dD", $columns)); + + return $this; + } + + /** + * @return $this + */ + public function moveToColumn(int $column): static + { + $this->output->write(sprintf("\x1b[%dG", $column)); + + return $this; + } + + /** + * @return $this + */ + public function moveToPosition(int $column, int $row): static + { + $this->output->write(sprintf("\x1b[%d;%dH", $row + 1, $column)); + + return $this; + } + + /** + * @return $this + */ + public function savePosition(): static + { + $this->output->write("\x1b7"); + + return $this; + } + + /** + * @return $this + */ + public function restorePosition(): static + { + $this->output->write("\x1b8"); + + return $this; + } + + /** + * @return $this + */ + public function hide(): static + { + $this->output->write("\x1b[?25l"); + + return $this; + } + + /** + * @return $this + */ + public function show(): static + { + $this->output->write("\x1b[?25h\x1b[?0c"); + + return $this; + } + + /** + * Clears all the output from the current line. + * + * @return $this + */ + public function clearLine(): static + { + $this->output->write("\x1b[2K"); + + return $this; + } + + /** + * Clears all the output from the current line after the current position. + */ + public function clearLineAfter(): self + { + $this->output->write("\x1b[K"); + + return $this; + } + + /** + * Clears all the output from the cursors' current position to the end of the screen. + * + * @return $this + */ + public function clearOutput(): static + { + $this->output->write("\x1b[0J"); + + return $this; + } + + /** + * Clears the entire screen. + * + * @return $this + */ + public function clearScreen(): static + { + $this->output->write("\x1b[2J"); + + return $this; + } + + /** + * Returns the current cursor position as x,y coordinates. + */ + public function getCurrentPosition(): array + { + static $isTtySupported; + + if (!$isTtySupported ??= '/' === \DIRECTORY_SEPARATOR && stream_isatty(\STDOUT)) { + return [1, 1]; + } + + $sttyMode = shell_exec('stty -g'); + shell_exec('stty -icanon -echo'); + + @fwrite($this->input, "\033[6n"); + + $code = trim(fread($this->input, 1024)); + + shell_exec(sprintf('stty %s', $sttyMode)); + + sscanf($code, "\033[%d;%dR", $row, $col); + + return [$col, $row]; + } +} diff --git a/3rdparty/symfony/console/DataCollector/CommandDataCollector.php b/3rdparty/symfony/console/DataCollector/CommandDataCollector.php new file mode 100644 index 00000000..45138c7d --- /dev/null +++ b/3rdparty/symfony/console/DataCollector/CommandDataCollector.php @@ -0,0 +1,234 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Component\Console\DataCollector; + +use Symfony\Component\Console\Command\Command; +use Symfony\Component\Console\Debug\CliRequest; +use Symfony\Component\Console\Output\OutputInterface; +use Symfony\Component\Console\SignalRegistry\SignalMap; +use Symfony\Component\HttpFoundation\Request; +use Symfony\Component\HttpFoundation\Response; +use Symfony\Component\HttpKernel\DataCollector\DataCollector; +use Symfony\Component\VarDumper\Cloner\Data; + +/** + * @internal + * + * @author Jules Pietri + */ +final class CommandDataCollector extends DataCollector +{ + public function collect(Request $request, Response $response, ?\Throwable $exception = null): void + { + if (!$request instanceof CliRequest) { + return; + } + + $command = $request->command; + $application = $command->getApplication(); + + $this->data = [ + 'command' => $this->cloneVar($command->command), + 'exit_code' => $command->exitCode, + 'interrupted_by_signal' => $command->interruptedBySignal, + 'duration' => $command->duration, + 'max_memory_usage' => $command->maxMemoryUsage, + 'verbosity_level' => match ($command->output->getVerbosity()) { + OutputInterface::VERBOSITY_QUIET => 'quiet', + OutputInterface::VERBOSITY_NORMAL => 'normal', + OutputInterface::VERBOSITY_VERBOSE => 'verbose', + OutputInterface::VERBOSITY_VERY_VERBOSE => 'very verbose', + OutputInterface::VERBOSITY_DEBUG => 'debug', + }, + 'interactive' => $command->isInteractive, + 'validate_input' => !$command->ignoreValidation, + 'enabled' => $command->isEnabled(), + 'visible' => !$command->isHidden(), + 'input' => $this->cloneVar($command->input), + 'output' => $this->cloneVar($command->output), + 'interactive_inputs' => array_map($this->cloneVar(...), $command->interactiveInputs), + 'signalable' => $command->getSubscribedSignals(), + 'handled_signals' => $command->handledSignals, + 'helper_set' => array_map($this->cloneVar(...), iterator_to_array($command->getHelperSet())), + ]; + + $baseDefinition = $application->getDefinition(); + + foreach ($command->arguments as $argName => $argValue) { + if ($baseDefinition->hasArgument($argName)) { + $this->data['application_inputs'][$argName] = $this->cloneVar($argValue); + } else { + $this->data['arguments'][$argName] = $this->cloneVar($argValue); + } + } + + foreach ($command->options as $optName => $optValue) { + if ($baseDefinition->hasOption($optName)) { + $this->data['application_inputs']['--'.$optName] = $this->cloneVar($optValue); + } else { + $this->data['options'][$optName] = $this->cloneVar($optValue); + } + } + } + + public function getName(): string + { + return 'command'; + } + + /** + * @return array{ + * class?: class-string, + * executor?: string, + * file: string, + * line: int, + * } + */ + public function getCommand(): array + { + $class = $this->data['command']->getType(); + $r = new \ReflectionMethod($class, 'execute'); + + if (Command::class !== $r->getDeclaringClass()) { + return [ + 'executor' => $class.'::'.$r->name, + 'file' => $r->getFileName(), + 'line' => $r->getStartLine(), + ]; + } + + $r = new \ReflectionClass($class); + + return [ + 'class' => $class, + 'file' => $r->getFileName(), + 'line' => $r->getStartLine(), + ]; + } + + public function getInterruptedBySignal(): ?string + { + if (isset($this->data['interrupted_by_signal'])) { + return sprintf('%s (%d)', SignalMap::getSignalName($this->data['interrupted_by_signal']), $this->data['interrupted_by_signal']); + } + + return null; + } + + public function getDuration(): string + { + return $this->data['duration']; + } + + public function getMaxMemoryUsage(): string + { + return $this->data['max_memory_usage']; + } + + public function getVerbosityLevel(): string + { + return $this->data['verbosity_level']; + } + + public function getInteractive(): bool + { + return $this->data['interactive']; + } + + public function getValidateInput(): bool + { + return $this->data['validate_input']; + } + + public function getEnabled(): bool + { + return $this->data['enabled']; + } + + public function getVisible(): bool + { + return $this->data['visible']; + } + + public function getInput(): Data + { + return $this->data['input']; + } + + public function getOutput(): Data + { + return $this->data['output']; + } + + /** + * @return Data[] + */ + public function getArguments(): array + { + return $this->data['arguments'] ?? []; + } + + /** + * @return Data[] + */ + public function getOptions(): array + { + return $this->data['options'] ?? []; + } + + /** + * @return Data[] + */ + public function getApplicationInputs(): array + { + return $this->data['application_inputs'] ?? []; + } + + /** + * @return Data[] + */ + public function getInteractiveInputs(): array + { + return $this->data['interactive_inputs'] ?? []; + } + + public function getSignalable(): array + { + return array_map( + static fn (int $signal): string => sprintf('%s (%d)', SignalMap::getSignalName($signal), $signal), + $this->data['signalable'] + ); + } + + public function getHandledSignals(): array + { + $keys = array_map( + static fn (int $signal): string => sprintf('%s (%d)', SignalMap::getSignalName($signal), $signal), + array_keys($this->data['handled_signals']) + ); + + return array_combine($keys, array_values($this->data['handled_signals'])); + } + + /** + * @return Data[] + */ + public function getHelperSet(): array + { + return $this->data['helper_set'] ?? []; + } + + public function reset(): void + { + $this->data = []; + } +} diff --git a/3rdparty/symfony/console/Debug/CliRequest.php b/3rdparty/symfony/console/Debug/CliRequest.php new file mode 100644 index 00000000..b023db07 --- /dev/null +++ b/3rdparty/symfony/console/Debug/CliRequest.php @@ -0,0 +1,70 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Component\Console\Debug; + +use Symfony\Component\Console\Command\TraceableCommand; +use Symfony\Component\HttpFoundation\Request; +use Symfony\Component\HttpFoundation\Response; + +/** + * @internal + */ +final class CliRequest extends Request +{ + public function __construct( + public readonly TraceableCommand $command, + ) { + parent::__construct( + attributes: ['_controller' => \get_class($command->command), '_virtual_type' => 'command'], + server: $_SERVER, + ); + } + + // Methods below allow to populate a profile, thus enable search and filtering + public function getUri(): string + { + if ($this->server->has('SYMFONY_CLI_BINARY_NAME')) { + $binary = $this->server->get('SYMFONY_CLI_BINARY_NAME').' console'; + } else { + $binary = $this->server->get('argv')[0]; + } + + return $binary.' '.$this->command->input; + } + + public function getMethod(): string + { + return $this->command->isInteractive ? 'INTERACTIVE' : 'BATCH'; + } + + public function getResponse(): Response + { + return new class($this->command->exitCode) extends Response { + public function __construct(private readonly int $exitCode) + { + parent::__construct(); + } + + public function getStatusCode(): int + { + return $this->exitCode; + } + }; + } + + public function getClientIp(): string + { + $application = $this->command->getApplication(); + + return $application->getName().' '.$application->getVersion(); + } +} diff --git a/3rdparty/symfony/console/DependencyInjection/AddConsoleCommandPass.php b/3rdparty/symfony/console/DependencyInjection/AddConsoleCommandPass.php new file mode 100644 index 00000000..27705ddb --- /dev/null +++ b/3rdparty/symfony/console/DependencyInjection/AddConsoleCommandPass.php @@ -0,0 +1,134 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Component\Console\DependencyInjection; + +use Symfony\Component\Console\Command\Command; +use Symfony\Component\Console\Command\LazyCommand; +use Symfony\Component\Console\CommandLoader\ContainerCommandLoader; +use Symfony\Component\DependencyInjection\Argument\ServiceClosureArgument; +use Symfony\Component\DependencyInjection\Compiler\CompilerPassInterface; +use Symfony\Component\DependencyInjection\Compiler\ServiceLocatorTagPass; +use Symfony\Component\DependencyInjection\ContainerBuilder; +use Symfony\Component\DependencyInjection\Exception\InvalidArgumentException; +use Symfony\Component\DependencyInjection\Reference; +use Symfony\Component\DependencyInjection\TypedReference; + +/** + * Registers console commands. + * + * @author Grégoire Pineau + */ +class AddConsoleCommandPass implements CompilerPassInterface +{ + /** + * @return void + */ + public function process(ContainerBuilder $container) + { + $commandServices = $container->findTaggedServiceIds('console.command', true); + $lazyCommandMap = []; + $lazyCommandRefs = []; + $serviceIds = []; + + foreach ($commandServices as $id => $tags) { + $definition = $container->getDefinition($id); + $definition->addTag('container.no_preload'); + $class = $container->getParameterBag()->resolveValue($definition->getClass()); + + if (isset($tags[0]['command'])) { + $aliases = $tags[0]['command']; + } else { + if (!$r = $container->getReflectionClass($class)) { + throw new InvalidArgumentException(sprintf('Class "%s" used for service "%s" cannot be found.', $class, $id)); + } + if (!$r->isSubclassOf(Command::class)) { + throw new InvalidArgumentException(sprintf('The service "%s" tagged "%s" must be a subclass of "%s".', $id, 'console.command', Command::class)); + } + $aliases = str_replace('%', '%%', $class::getDefaultName() ?? ''); + } + + $aliases = explode('|', $aliases ?? ''); + $commandName = array_shift($aliases); + + if ($isHidden = '' === $commandName) { + $commandName = array_shift($aliases); + } + + if (null === $commandName) { + if (!$definition->isPublic() || $definition->isPrivate() || $definition->hasTag('container.private')) { + $commandId = 'console.command.public_alias.'.$id; + $container->setAlias($commandId, $id)->setPublic(true); + $id = $commandId; + } + $serviceIds[] = $id; + + continue; + } + + $description = $tags[0]['description'] ?? null; + + unset($tags[0]); + $lazyCommandMap[$commandName] = $id; + $lazyCommandRefs[$id] = new TypedReference($id, $class); + + foreach ($aliases as $alias) { + $lazyCommandMap[$alias] = $id; + } + + foreach ($tags as $tag) { + if (isset($tag['command'])) { + $aliases[] = $tag['command']; + $lazyCommandMap[$tag['command']] = $id; + } + + $description ??= $tag['description'] ?? null; + } + + $definition->addMethodCall('setName', [$commandName]); + + if ($aliases) { + $definition->addMethodCall('setAliases', [$aliases]); + } + + if ($isHidden) { + $definition->addMethodCall('setHidden', [true]); + } + + if (!$description) { + if (!$r = $container->getReflectionClass($class)) { + throw new InvalidArgumentException(sprintf('Class "%s" used for service "%s" cannot be found.', $class, $id)); + } + if (!$r->isSubclassOf(Command::class)) { + throw new InvalidArgumentException(sprintf('The service "%s" tagged "%s" must be a subclass of "%s".', $id, 'console.command', Command::class)); + } + $description = str_replace('%', '%%', $class::getDefaultDescription() ?? ''); + } + + if ($description) { + $definition->addMethodCall('setDescription', [$description]); + + $container->register('.'.$id.'.lazy', LazyCommand::class) + ->setArguments([$commandName, $aliases, $description, $isHidden, new ServiceClosureArgument($lazyCommandRefs[$id])]); + + $lazyCommandRefs[$id] = new Reference('.'.$id.'.lazy'); + } + } + + $container + ->register('console.command_loader', ContainerCommandLoader::class) + ->setPublic(true) + ->addTag('container.no_preload') + ->setArguments([ServiceLocatorTagPass::register($container, $lazyCommandRefs), $lazyCommandMap]); + + $container->setParameter('console.command.ids', $serviceIds); + } +} diff --git a/3rdparty/symfony/console/Descriptor/ApplicationDescription.php b/3rdparty/symfony/console/Descriptor/ApplicationDescription.php new file mode 100644 index 00000000..ef9e8a63 --- /dev/null +++ b/3rdparty/symfony/console/Descriptor/ApplicationDescription.php @@ -0,0 +1,139 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Component\Console\Descriptor; + +use Symfony\Component\Console\Application; +use Symfony\Component\Console\Command\Command; +use Symfony\Component\Console\Exception\CommandNotFoundException; + +/** + * @author Jean-François Simon + * + * @internal + */ +class ApplicationDescription +{ + public const GLOBAL_NAMESPACE = '_global'; + + private Application $application; + private ?string $namespace; + private bool $showHidden; + private array $namespaces; + + /** + * @var array + */ + private array $commands; + + /** + * @var array + */ + private array $aliases = []; + + public function __construct(Application $application, ?string $namespace = null, bool $showHidden = false) + { + $this->application = $application; + $this->namespace = $namespace; + $this->showHidden = $showHidden; + } + + public function getNamespaces(): array + { + if (!isset($this->namespaces)) { + $this->inspectApplication(); + } + + return $this->namespaces; + } + + /** + * @return Command[] + */ + public function getCommands(): array + { + if (!isset($this->commands)) { + $this->inspectApplication(); + } + + return $this->commands; + } + + /** + * @throws CommandNotFoundException + */ + public function getCommand(string $name): Command + { + if (!isset($this->commands[$name]) && !isset($this->aliases[$name])) { + throw new CommandNotFoundException(sprintf('Command "%s" does not exist.', $name)); + } + + return $this->commands[$name] ?? $this->aliases[$name]; + } + + private function inspectApplication(): void + { + $this->commands = []; + $this->namespaces = []; + + $all = $this->application->all($this->namespace ? $this->application->findNamespace($this->namespace) : null); + foreach ($this->sortCommands($all) as $namespace => $commands) { + $names = []; + + /** @var Command $command */ + foreach ($commands as $name => $command) { + if (!$command->getName() || (!$this->showHidden && $command->isHidden())) { + continue; + } + + if ($command->getName() === $name) { + $this->commands[$name] = $command; + } else { + $this->aliases[$name] = $command; + } + + $names[] = $name; + } + + $this->namespaces[$namespace] = ['id' => $namespace, 'commands' => $names]; + } + } + + private function sortCommands(array $commands): array + { + $namespacedCommands = []; + $globalCommands = []; + $sortedCommands = []; + foreach ($commands as $name => $command) { + $key = $this->application->extractNamespace($name, 1); + if (\in_array($key, ['', self::GLOBAL_NAMESPACE], true)) { + $globalCommands[$name] = $command; + } else { + $namespacedCommands[$key][$name] = $command; + } + } + + if ($globalCommands) { + ksort($globalCommands); + $sortedCommands[self::GLOBAL_NAMESPACE] = $globalCommands; + } + + if ($namespacedCommands) { + ksort($namespacedCommands, \SORT_STRING); + foreach ($namespacedCommands as $key => $commandsSet) { + ksort($commandsSet); + $sortedCommands[$key] = $commandsSet; + } + } + + return $sortedCommands; + } +} diff --git a/3rdparty/symfony/console/Descriptor/Descriptor.php b/3rdparty/symfony/console/Descriptor/Descriptor.php new file mode 100644 index 00000000..7b2509c6 --- /dev/null +++ b/3rdparty/symfony/console/Descriptor/Descriptor.php @@ -0,0 +1,74 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Component\Console\Descriptor; + +use Symfony\Component\Console\Application; +use Symfony\Component\Console\Command\Command; +use Symfony\Component\Console\Exception\InvalidArgumentException; +use Symfony\Component\Console\Input\InputArgument; +use Symfony\Component\Console\Input\InputDefinition; +use Symfony\Component\Console\Input\InputOption; +use Symfony\Component\Console\Output\OutputInterface; + +/** + * @author Jean-François Simon + * + * @internal + */ +abstract class Descriptor implements DescriptorInterface +{ + protected OutputInterface $output; + + public function describe(OutputInterface $output, object $object, array $options = []): void + { + $this->output = $output; + + match (true) { + $object instanceof InputArgument => $this->describeInputArgument($object, $options), + $object instanceof InputOption => $this->describeInputOption($object, $options), + $object instanceof InputDefinition => $this->describeInputDefinition($object, $options), + $object instanceof Command => $this->describeCommand($object, $options), + $object instanceof Application => $this->describeApplication($object, $options), + default => throw new InvalidArgumentException(sprintf('Object of type "%s" is not describable.', get_debug_type($object))), + }; + } + + protected function write(string $content, bool $decorated = false): void + { + $this->output->write($content, false, $decorated ? OutputInterface::OUTPUT_NORMAL : OutputInterface::OUTPUT_RAW); + } + + /** + * Describes an InputArgument instance. + */ + abstract protected function describeInputArgument(InputArgument $argument, array $options = []): void; + + /** + * Describes an InputOption instance. + */ + abstract protected function describeInputOption(InputOption $option, array $options = []): void; + + /** + * Describes an InputDefinition instance. + */ + abstract protected function describeInputDefinition(InputDefinition $definition, array $options = []): void; + + /** + * Describes a Command instance. + */ + abstract protected function describeCommand(Command $command, array $options = []): void; + + /** + * Describes an Application instance. + */ + abstract protected function describeApplication(Application $application, array $options = []): void; +} diff --git a/3rdparty/symfony/console/Descriptor/DescriptorInterface.php b/3rdparty/symfony/console/Descriptor/DescriptorInterface.php new file mode 100644 index 00000000..ab468a25 --- /dev/null +++ b/3rdparty/symfony/console/Descriptor/DescriptorInterface.php @@ -0,0 +1,27 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Component\Console\Descriptor; + +use Symfony\Component\Console\Output\OutputInterface; + +/** + * Descriptor interface. + * + * @author Jean-François Simon + */ +interface DescriptorInterface +{ + /** + * @return void + */ + public function describe(OutputInterface $output, object $object, array $options = []); +} diff --git a/3rdparty/symfony/console/Descriptor/JsonDescriptor.php b/3rdparty/symfony/console/Descriptor/JsonDescriptor.php new file mode 100644 index 00000000..95630370 --- /dev/null +++ b/3rdparty/symfony/console/Descriptor/JsonDescriptor.php @@ -0,0 +1,166 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Component\Console\Descriptor; + +use Symfony\Component\Console\Application; +use Symfony\Component\Console\Command\Command; +use Symfony\Component\Console\Input\InputArgument; +use Symfony\Component\Console\Input\InputDefinition; +use Symfony\Component\Console\Input\InputOption; + +/** + * JSON descriptor. + * + * @author Jean-François Simon + * + * @internal + */ +class JsonDescriptor extends Descriptor +{ + protected function describeInputArgument(InputArgument $argument, array $options = []): void + { + $this->writeData($this->getInputArgumentData($argument), $options); + } + + protected function describeInputOption(InputOption $option, array $options = []): void + { + $this->writeData($this->getInputOptionData($option), $options); + if ($option->isNegatable()) { + $this->writeData($this->getInputOptionData($option, true), $options); + } + } + + protected function describeInputDefinition(InputDefinition $definition, array $options = []): void + { + $this->writeData($this->getInputDefinitionData($definition), $options); + } + + protected function describeCommand(Command $command, array $options = []): void + { + $this->writeData($this->getCommandData($command, $options['short'] ?? false), $options); + } + + protected function describeApplication(Application $application, array $options = []): void + { + $describedNamespace = $options['namespace'] ?? null; + $description = new ApplicationDescription($application, $describedNamespace, true); + $commands = []; + + foreach ($description->getCommands() as $command) { + $commands[] = $this->getCommandData($command, $options['short'] ?? false); + } + + $data = []; + if ('UNKNOWN' !== $application->getName()) { + $data['application']['name'] = $application->getName(); + if ('UNKNOWN' !== $application->getVersion()) { + $data['application']['version'] = $application->getVersion(); + } + } + + $data['commands'] = $commands; + + if ($describedNamespace) { + $data['namespace'] = $describedNamespace; + } else { + $data['namespaces'] = array_values($description->getNamespaces()); + } + + $this->writeData($data, $options); + } + + /** + * Writes data as json. + */ + private function writeData(array $data, array $options): void + { + $flags = $options['json_encoding'] ?? 0; + + $this->write(json_encode($data, $flags)); + } + + private function getInputArgumentData(InputArgument $argument): array + { + return [ + 'name' => $argument->getName(), + 'is_required' => $argument->isRequired(), + 'is_array' => $argument->isArray(), + 'description' => preg_replace('/\s*[\r\n]\s*/', ' ', $argument->getDescription()), + 'default' => \INF === $argument->getDefault() ? 'INF' : $argument->getDefault(), + ]; + } + + private function getInputOptionData(InputOption $option, bool $negated = false): array + { + return $negated ? [ + 'name' => '--no-'.$option->getName(), + 'shortcut' => '', + 'accept_value' => false, + 'is_value_required' => false, + 'is_multiple' => false, + 'description' => 'Negate the "--'.$option->getName().'" option', + 'default' => false, + ] : [ + 'name' => '--'.$option->getName(), + 'shortcut' => $option->getShortcut() ? '-'.str_replace('|', '|-', $option->getShortcut()) : '', + 'accept_value' => $option->acceptValue(), + 'is_value_required' => $option->isValueRequired(), + 'is_multiple' => $option->isArray(), + 'description' => preg_replace('/\s*[\r\n]\s*/', ' ', $option->getDescription()), + 'default' => \INF === $option->getDefault() ? 'INF' : $option->getDefault(), + ]; + } + + private function getInputDefinitionData(InputDefinition $definition): array + { + $inputArguments = []; + foreach ($definition->getArguments() as $name => $argument) { + $inputArguments[$name] = $this->getInputArgumentData($argument); + } + + $inputOptions = []; + foreach ($definition->getOptions() as $name => $option) { + $inputOptions[$name] = $this->getInputOptionData($option); + if ($option->isNegatable()) { + $inputOptions['no-'.$name] = $this->getInputOptionData($option, true); + } + } + + return ['arguments' => $inputArguments, 'options' => $inputOptions]; + } + + private function getCommandData(Command $command, bool $short = false): array + { + $data = [ + 'name' => $command->getName(), + 'description' => $command->getDescription(), + ]; + + if ($short) { + $data += [ + 'usage' => $command->getAliases(), + ]; + } else { + $command->mergeApplicationDefinition(false); + + $data += [ + 'usage' => array_merge([$command->getSynopsis()], $command->getUsages(), $command->getAliases()), + 'help' => $command->getProcessedHelp(), + 'definition' => $this->getInputDefinitionData($command->getDefinition()), + ]; + } + + $data['hidden'] = $command->isHidden(); + + return $data; + } +} diff --git a/3rdparty/symfony/console/Descriptor/MarkdownDescriptor.php b/3rdparty/symfony/console/Descriptor/MarkdownDescriptor.php new file mode 100644 index 00000000..b3f16ee9 --- /dev/null +++ b/3rdparty/symfony/console/Descriptor/MarkdownDescriptor.php @@ -0,0 +1,173 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Component\Console\Descriptor; + +use Symfony\Component\Console\Application; +use Symfony\Component\Console\Command\Command; +use Symfony\Component\Console\Helper\Helper; +use Symfony\Component\Console\Input\InputArgument; +use Symfony\Component\Console\Input\InputDefinition; +use Symfony\Component\Console\Input\InputOption; +use Symfony\Component\Console\Output\OutputInterface; + +/** + * Markdown descriptor. + * + * @author Jean-François Simon + * + * @internal + */ +class MarkdownDescriptor extends Descriptor +{ + public function describe(OutputInterface $output, object $object, array $options = []): void + { + $decorated = $output->isDecorated(); + $output->setDecorated(false); + + parent::describe($output, $object, $options); + + $output->setDecorated($decorated); + } + + protected function write(string $content, bool $decorated = true): void + { + parent::write($content, $decorated); + } + + protected function describeInputArgument(InputArgument $argument, array $options = []): void + { + $this->write( + '#### `'.($argument->getName() ?: '')."`\n\n" + .($argument->getDescription() ? preg_replace('/\s*[\r\n]\s*/', "\n", $argument->getDescription())."\n\n" : '') + .'* Is required: '.($argument->isRequired() ? 'yes' : 'no')."\n" + .'* Is array: '.($argument->isArray() ? 'yes' : 'no')."\n" + .'* Default: `'.str_replace("\n", '', var_export($argument->getDefault(), true)).'`' + ); + } + + protected function describeInputOption(InputOption $option, array $options = []): void + { + $name = '--'.$option->getName(); + if ($option->isNegatable()) { + $name .= '|--no-'.$option->getName(); + } + if ($option->getShortcut()) { + $name .= '|-'.str_replace('|', '|-', $option->getShortcut()).''; + } + + $this->write( + '#### `'.$name.'`'."\n\n" + .($option->getDescription() ? preg_replace('/\s*[\r\n]\s*/', "\n", $option->getDescription())."\n\n" : '') + .'* Accept value: '.($option->acceptValue() ? 'yes' : 'no')."\n" + .'* Is value required: '.($option->isValueRequired() ? 'yes' : 'no')."\n" + .'* Is multiple: '.($option->isArray() ? 'yes' : 'no')."\n" + .'* Is negatable: '.($option->isNegatable() ? 'yes' : 'no')."\n" + .'* Default: `'.str_replace("\n", '', var_export($option->getDefault(), true)).'`' + ); + } + + protected function describeInputDefinition(InputDefinition $definition, array $options = []): void + { + if ($showArguments = \count($definition->getArguments()) > 0) { + $this->write('### Arguments'); + foreach ($definition->getArguments() as $argument) { + $this->write("\n\n"); + $this->describeInputArgument($argument); + } + } + + if (\count($definition->getOptions()) > 0) { + if ($showArguments) { + $this->write("\n\n"); + } + + $this->write('### Options'); + foreach ($definition->getOptions() as $option) { + $this->write("\n\n"); + $this->describeInputOption($option); + } + } + } + + protected function describeCommand(Command $command, array $options = []): void + { + if ($options['short'] ?? false) { + $this->write( + '`'.$command->getName()."`\n" + .str_repeat('-', Helper::width($command->getName()) + 2)."\n\n" + .($command->getDescription() ? $command->getDescription()."\n\n" : '') + .'### Usage'."\n\n" + .array_reduce($command->getAliases(), fn ($carry, $usage) => $carry.'* `'.$usage.'`'."\n") + ); + + return; + } + + $command->mergeApplicationDefinition(false); + + $this->write( + '`'.$command->getName()."`\n" + .str_repeat('-', Helper::width($command->getName()) + 2)."\n\n" + .($command->getDescription() ? $command->getDescription()."\n\n" : '') + .'### Usage'."\n\n" + .array_reduce(array_merge([$command->getSynopsis()], $command->getAliases(), $command->getUsages()), fn ($carry, $usage) => $carry.'* `'.$usage.'`'."\n") + ); + + if ($help = $command->getProcessedHelp()) { + $this->write("\n"); + $this->write($help); + } + + $definition = $command->getDefinition(); + if ($definition->getOptions() || $definition->getArguments()) { + $this->write("\n\n"); + $this->describeInputDefinition($definition); + } + } + + protected function describeApplication(Application $application, array $options = []): void + { + $describedNamespace = $options['namespace'] ?? null; + $description = new ApplicationDescription($application, $describedNamespace); + $title = $this->getApplicationTitle($application); + + $this->write($title."\n".str_repeat('=', Helper::width($title))); + + foreach ($description->getNamespaces() as $namespace) { + if (ApplicationDescription::GLOBAL_NAMESPACE !== $namespace['id']) { + $this->write("\n\n"); + $this->write('**'.$namespace['id'].':**'); + } + + $this->write("\n\n"); + $this->write(implode("\n", array_map(fn ($commandName) => sprintf('* [`%s`](#%s)', $commandName, str_replace(':', '', $description->getCommand($commandName)->getName())), $namespace['commands']))); + } + + foreach ($description->getCommands() as $command) { + $this->write("\n\n"); + $this->describeCommand($command, $options); + } + } + + private function getApplicationTitle(Application $application): string + { + if ('UNKNOWN' !== $application->getName()) { + if ('UNKNOWN' !== $application->getVersion()) { + return sprintf('%s %s', $application->getName(), $application->getVersion()); + } + + return $application->getName(); + } + + return 'Console Tool'; + } +} diff --git a/3rdparty/symfony/console/Descriptor/ReStructuredTextDescriptor.php b/3rdparty/symfony/console/Descriptor/ReStructuredTextDescriptor.php new file mode 100644 index 00000000..d4423fd3 --- /dev/null +++ b/3rdparty/symfony/console/Descriptor/ReStructuredTextDescriptor.php @@ -0,0 +1,272 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Component\Console\Descriptor; + +use Symfony\Component\Console\Application; +use Symfony\Component\Console\Command\Command; +use Symfony\Component\Console\Helper\Helper; +use Symfony\Component\Console\Input\InputArgument; +use Symfony\Component\Console\Input\InputDefinition; +use Symfony\Component\Console\Input\InputOption; +use Symfony\Component\Console\Output\OutputInterface; +use Symfony\Component\String\UnicodeString; + +class ReStructuredTextDescriptor extends Descriptor +{ + //

    + private string $partChar = '='; + //

    + private string $chapterChar = '-'; + //

    + private string $sectionChar = '~'; + //

    + private string $subsectionChar = '.'; + //

    + private string $subsubsectionChar = '^'; + //
    + private string $paragraphsChar = '"'; + + private array $visibleNamespaces = []; + + public function describe(OutputInterface $output, object $object, array $options = []): void + { + $decorated = $output->isDecorated(); + $output->setDecorated(false); + + parent::describe($output, $object, $options); + + $output->setDecorated($decorated); + } + + /** + * Override parent method to set $decorated = true. + */ + protected function write(string $content, bool $decorated = true): void + { + parent::write($content, $decorated); + } + + protected function describeInputArgument(InputArgument $argument, array $options = []): void + { + $this->write( + $argument->getName() ?: ''."\n".str_repeat($this->paragraphsChar, Helper::width($argument->getName()))."\n\n" + .($argument->getDescription() ? preg_replace('/\s*[\r\n]\s*/', "\n", $argument->getDescription())."\n\n" : '') + .'- **Is required**: '.($argument->isRequired() ? 'yes' : 'no')."\n" + .'- **Is array**: '.($argument->isArray() ? 'yes' : 'no')."\n" + .'- **Default**: ``'.str_replace("\n", '', var_export($argument->getDefault(), true)).'``' + ); + } + + protected function describeInputOption(InputOption $option, array $options = []): void + { + $name = '\-\-'.$option->getName(); + if ($option->isNegatable()) { + $name .= '|\-\-no-'.$option->getName(); + } + if ($option->getShortcut()) { + $name .= '|-'.str_replace('|', '|-', $option->getShortcut()); + } + + $optionDescription = $option->getDescription() ? preg_replace('/\s*[\r\n]\s*/', "\n\n", $option->getDescription())."\n\n" : ''; + $optionDescription = (new UnicodeString($optionDescription))->ascii(); + $this->write( + $name."\n".str_repeat($this->paragraphsChar, Helper::width($name))."\n\n" + .$optionDescription + .'- **Accept value**: '.($option->acceptValue() ? 'yes' : 'no')."\n" + .'- **Is value required**: '.($option->isValueRequired() ? 'yes' : 'no')."\n" + .'- **Is multiple**: '.($option->isArray() ? 'yes' : 'no')."\n" + .'- **Is negatable**: '.($option->isNegatable() ? 'yes' : 'no')."\n" + .'- **Default**: ``'.str_replace("\n", '', var_export($option->getDefault(), true)).'``'."\n" + ); + } + + protected function describeInputDefinition(InputDefinition $definition, array $options = []): void + { + if ($showArguments = ((bool) $definition->getArguments())) { + $this->write("Arguments\n".str_repeat($this->subsubsectionChar, 9))."\n\n"; + foreach ($definition->getArguments() as $argument) { + $this->write("\n\n"); + $this->describeInputArgument($argument); + } + } + + if ($nonDefaultOptions = $this->getNonDefaultOptions($definition)) { + if ($showArguments) { + $this->write("\n\n"); + } + + $this->write("Options\n".str_repeat($this->subsubsectionChar, 7)."\n\n"); + foreach ($nonDefaultOptions as $option) { + $this->describeInputOption($option); + $this->write("\n"); + } + } + } + + protected function describeCommand(Command $command, array $options = []): void + { + if ($options['short'] ?? false) { + $this->write( + '``'.$command->getName()."``\n" + .str_repeat($this->subsectionChar, Helper::width($command->getName()))."\n\n" + .($command->getDescription() ? $command->getDescription()."\n\n" : '') + ."Usage\n".str_repeat($this->paragraphsChar, 5)."\n\n" + .array_reduce($command->getAliases(), static fn ($carry, $usage) => $carry.'- ``'.$usage.'``'."\n") + ); + + return; + } + + $command->mergeApplicationDefinition(false); + + foreach ($command->getAliases() as $alias) { + $this->write('.. _'.$alias.":\n\n"); + } + $this->write( + $command->getName()."\n" + .str_repeat($this->subsectionChar, Helper::width($command->getName()))."\n\n" + .($command->getDescription() ? $command->getDescription()."\n\n" : '') + ."Usage\n".str_repeat($this->subsubsectionChar, 5)."\n\n" + .array_reduce(array_merge([$command->getSynopsis()], $command->getAliases(), $command->getUsages()), static fn ($carry, $usage) => $carry.'- ``'.$usage.'``'."\n") + ); + + if ($help = $command->getProcessedHelp()) { + $this->write("\n"); + $this->write($help); + } + + $definition = $command->getDefinition(); + if ($definition->getOptions() || $definition->getArguments()) { + $this->write("\n\n"); + $this->describeInputDefinition($definition); + } + } + + protected function describeApplication(Application $application, array $options = []): void + { + $description = new ApplicationDescription($application, $options['namespace'] ?? null); + $title = $this->getApplicationTitle($application); + + $this->write($title."\n".str_repeat($this->partChar, Helper::width($title))); + $this->createTableOfContents($description, $application); + $this->describeCommands($application, $options); + } + + private function getApplicationTitle(Application $application): string + { + if ('UNKNOWN' === $application->getName()) { + return 'Console Tool'; + } + if ('UNKNOWN' !== $application->getVersion()) { + return sprintf('%s %s', $application->getName(), $application->getVersion()); + } + + return $application->getName(); + } + + private function describeCommands($application, array $options): void + { + $title = 'Commands'; + $this->write("\n\n$title\n".str_repeat($this->chapterChar, Helper::width($title))."\n\n"); + foreach ($this->visibleNamespaces as $namespace) { + if ('_global' === $namespace) { + $commands = $application->all(''); + $this->write('Global'."\n".str_repeat($this->sectionChar, Helper::width('Global'))."\n\n"); + } else { + $commands = $application->all($namespace); + $this->write($namespace."\n".str_repeat($this->sectionChar, Helper::width($namespace))."\n\n"); + } + + foreach ($this->removeAliasesAndHiddenCommands($commands) as $command) { + $this->describeCommand($command, $options); + $this->write("\n\n"); + } + } + } + + private function createTableOfContents(ApplicationDescription $description, Application $application): void + { + $this->setVisibleNamespaces($description); + $chapterTitle = 'Table of Contents'; + $this->write("\n\n$chapterTitle\n".str_repeat($this->chapterChar, Helper::width($chapterTitle))."\n\n"); + foreach ($this->visibleNamespaces as $namespace) { + if ('_global' === $namespace) { + $commands = $application->all(''); + } else { + $commands = $application->all($namespace); + $this->write("\n\n"); + $this->write($namespace."\n".str_repeat($this->sectionChar, Helper::width($namespace))."\n\n"); + } + $commands = $this->removeAliasesAndHiddenCommands($commands); + + $this->write("\n\n"); + $this->write(implode("\n", array_map(static fn ($commandName) => sprintf('- `%s`_', $commandName), array_keys($commands)))); + } + } + + private function getNonDefaultOptions(InputDefinition $definition): array + { + $globalOptions = [ + 'help', + 'quiet', + 'verbose', + 'version', + 'ansi', + 'no-interaction', + ]; + $nonDefaultOptions = []; + foreach ($definition->getOptions() as $option) { + // Skip global options. + if (!\in_array($option->getName(), $globalOptions)) { + $nonDefaultOptions[] = $option; + } + } + + return $nonDefaultOptions; + } + + private function setVisibleNamespaces(ApplicationDescription $description): void + { + $commands = $description->getCommands(); + foreach ($description->getNamespaces() as $namespace) { + try { + $namespaceCommands = $namespace['commands']; + foreach ($namespaceCommands as $key => $commandName) { + if (!\array_key_exists($commandName, $commands)) { + // If the array key does not exist, then this is an alias. + unset($namespaceCommands[$key]); + } elseif ($commands[$commandName]->isHidden()) { + unset($namespaceCommands[$key]); + } + } + if (!$namespaceCommands) { + // If the namespace contained only aliases or hidden commands, skip the namespace. + continue; + } + } catch (\Exception) { + } + $this->visibleNamespaces[] = $namespace['id']; + } + } + + private function removeAliasesAndHiddenCommands(array $commands): array + { + foreach ($commands as $key => $command) { + if ($command->isHidden() || \in_array($key, $command->getAliases(), true)) { + unset($commands[$key]); + } + } + unset($commands['completion']); + + return $commands; + } +} diff --git a/3rdparty/symfony/console/Descriptor/TextDescriptor.php b/3rdparty/symfony/console/Descriptor/TextDescriptor.php new file mode 100644 index 00000000..d04d1023 --- /dev/null +++ b/3rdparty/symfony/console/Descriptor/TextDescriptor.php @@ -0,0 +1,317 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Component\Console\Descriptor; + +use Symfony\Component\Console\Application; +use Symfony\Component\Console\Command\Command; +use Symfony\Component\Console\Formatter\OutputFormatter; +use Symfony\Component\Console\Helper\Helper; +use Symfony\Component\Console\Input\InputArgument; +use Symfony\Component\Console\Input\InputDefinition; +use Symfony\Component\Console\Input\InputOption; + +/** + * Text descriptor. + * + * @author Jean-François Simon + * + * @internal + */ +class TextDescriptor extends Descriptor +{ + protected function describeInputArgument(InputArgument $argument, array $options = []): void + { + if (null !== $argument->getDefault() && (!\is_array($argument->getDefault()) || \count($argument->getDefault()))) { + $default = sprintf(' [default: %s]', $this->formatDefaultValue($argument->getDefault())); + } else { + $default = ''; + } + + $totalWidth = $options['total_width'] ?? Helper::width($argument->getName()); + $spacingWidth = $totalWidth - \strlen($argument->getName()); + + $this->writeText(sprintf(' %s %s%s%s', + $argument->getName(), + str_repeat(' ', $spacingWidth), + // + 4 = 2 spaces before , 2 spaces after + preg_replace('/\s*[\r\n]\s*/', "\n".str_repeat(' ', $totalWidth + 4), $argument->getDescription()), + $default + ), $options); + } + + protected function describeInputOption(InputOption $option, array $options = []): void + { + if ($option->acceptValue() && null !== $option->getDefault() && (!\is_array($option->getDefault()) || \count($option->getDefault()))) { + $default = sprintf(' [default: %s]', $this->formatDefaultValue($option->getDefault())); + } else { + $default = ''; + } + + $value = ''; + if ($option->acceptValue()) { + $value = '='.strtoupper($option->getName()); + + if ($option->isValueOptional()) { + $value = '['.$value.']'; + } + } + + $totalWidth = $options['total_width'] ?? $this->calculateTotalWidthForOptions([$option]); + $synopsis = sprintf('%s%s', + $option->getShortcut() ? sprintf('-%s, ', $option->getShortcut()) : ' ', + sprintf($option->isNegatable() ? '--%1$s|--no-%1$s' : '--%1$s%2$s', $option->getName(), $value) + ); + + $spacingWidth = $totalWidth - Helper::width($synopsis); + + $this->writeText(sprintf(' %s %s%s%s%s', + $synopsis, + str_repeat(' ', $spacingWidth), + // + 4 = 2 spaces before , 2 spaces after + preg_replace('/\s*[\r\n]\s*/', "\n".str_repeat(' ', $totalWidth + 4), $option->getDescription()), + $default, + $option->isArray() ? ' (multiple values allowed)' : '' + ), $options); + } + + protected function describeInputDefinition(InputDefinition $definition, array $options = []): void + { + $totalWidth = $this->calculateTotalWidthForOptions($definition->getOptions()); + foreach ($definition->getArguments() as $argument) { + $totalWidth = max($totalWidth, Helper::width($argument->getName())); + } + + if ($definition->getArguments()) { + $this->writeText('Arguments:', $options); + $this->writeText("\n"); + foreach ($definition->getArguments() as $argument) { + $this->describeInputArgument($argument, array_merge($options, ['total_width' => $totalWidth])); + $this->writeText("\n"); + } + } + + if ($definition->getArguments() && $definition->getOptions()) { + $this->writeText("\n"); + } + + if ($definition->getOptions()) { + $laterOptions = []; + + $this->writeText('Options:', $options); + foreach ($definition->getOptions() as $option) { + if (\strlen($option->getShortcut() ?? '') > 1) { + $laterOptions[] = $option; + continue; + } + $this->writeText("\n"); + $this->describeInputOption($option, array_merge($options, ['total_width' => $totalWidth])); + } + foreach ($laterOptions as $option) { + $this->writeText("\n"); + $this->describeInputOption($option, array_merge($options, ['total_width' => $totalWidth])); + } + } + } + + protected function describeCommand(Command $command, array $options = []): void + { + $command->mergeApplicationDefinition(false); + + if ($description = $command->getDescription()) { + $this->writeText('Description:', $options); + $this->writeText("\n"); + $this->writeText(' '.$description); + $this->writeText("\n\n"); + } + + $this->writeText('Usage:', $options); + foreach (array_merge([$command->getSynopsis(true)], $command->getAliases(), $command->getUsages()) as $usage) { + $this->writeText("\n"); + $this->writeText(' '.OutputFormatter::escape($usage), $options); + } + $this->writeText("\n"); + + $definition = $command->getDefinition(); + if ($definition->getOptions() || $definition->getArguments()) { + $this->writeText("\n"); + $this->describeInputDefinition($definition, $options); + $this->writeText("\n"); + } + + $help = $command->getProcessedHelp(); + if ($help && $help !== $description) { + $this->writeText("\n"); + $this->writeText('Help:', $options); + $this->writeText("\n"); + $this->writeText(' '.str_replace("\n", "\n ", $help), $options); + $this->writeText("\n"); + } + } + + protected function describeApplication(Application $application, array $options = []): void + { + $describedNamespace = $options['namespace'] ?? null; + $description = new ApplicationDescription($application, $describedNamespace); + + if (isset($options['raw_text']) && $options['raw_text']) { + $width = $this->getColumnWidth($description->getCommands()); + + foreach ($description->getCommands() as $command) { + $this->writeText(sprintf("%-{$width}s %s", $command->getName(), $command->getDescription()), $options); + $this->writeText("\n"); + } + } else { + if ('' != $help = $application->getHelp()) { + $this->writeText("$help\n\n", $options); + } + + $this->writeText("Usage:\n", $options); + $this->writeText(" command [options] [arguments]\n\n", $options); + + $this->describeInputDefinition(new InputDefinition($application->getDefinition()->getOptions()), $options); + + $this->writeText("\n"); + $this->writeText("\n"); + + $commands = $description->getCommands(); + $namespaces = $description->getNamespaces(); + if ($describedNamespace && $namespaces) { + // make sure all alias commands are included when describing a specific namespace + $describedNamespaceInfo = reset($namespaces); + foreach ($describedNamespaceInfo['commands'] as $name) { + $commands[$name] = $description->getCommand($name); + } + } + + // calculate max. width based on available commands per namespace + $width = $this->getColumnWidth(array_merge(...array_values(array_map(fn ($namespace) => array_intersect($namespace['commands'], array_keys($commands)), array_values($namespaces))))); + + if ($describedNamespace) { + $this->writeText(sprintf('Available commands for the "%s" namespace:', $describedNamespace), $options); + } else { + $this->writeText('Available commands:', $options); + } + + foreach ($namespaces as $namespace) { + $namespace['commands'] = array_filter($namespace['commands'], fn ($name) => isset($commands[$name])); + + if (!$namespace['commands']) { + continue; + } + + if (!$describedNamespace && ApplicationDescription::GLOBAL_NAMESPACE !== $namespace['id']) { + $this->writeText("\n"); + $this->writeText(' '.$namespace['id'].'', $options); + } + + foreach ($namespace['commands'] as $name) { + $this->writeText("\n"); + $spacingWidth = $width - Helper::width($name); + $command = $commands[$name]; + $commandAliases = $name === $command->getName() ? $this->getCommandAliasesText($command) : ''; + $this->writeText(sprintf(' %s%s%s', $name, str_repeat(' ', $spacingWidth), $commandAliases.$command->getDescription()), $options); + } + } + + $this->writeText("\n"); + } + } + + private function writeText(string $content, array $options = []): void + { + $this->write( + isset($options['raw_text']) && $options['raw_text'] ? strip_tags($content) : $content, + isset($options['raw_output']) ? !$options['raw_output'] : true + ); + } + + /** + * Formats command aliases to show them in the command description. + */ + private function getCommandAliasesText(Command $command): string + { + $text = ''; + $aliases = $command->getAliases(); + + if ($aliases) { + $text = '['.implode('|', $aliases).'] '; + } + + return $text; + } + + /** + * Formats input option/argument default value. + */ + private function formatDefaultValue(mixed $default): string + { + if (\INF === $default) { + return 'INF'; + } + + if (\is_string($default)) { + $default = OutputFormatter::escape($default); + } elseif (\is_array($default)) { + foreach ($default as $key => $value) { + if (\is_string($value)) { + $default[$key] = OutputFormatter::escape($value); + } + } + } + + return str_replace('\\\\', '\\', json_encode($default, \JSON_UNESCAPED_SLASHES | \JSON_UNESCAPED_UNICODE)); + } + + /** + * @param array $commands + */ + private function getColumnWidth(array $commands): int + { + $widths = []; + + foreach ($commands as $command) { + if ($command instanceof Command) { + $widths[] = Helper::width($command->getName()); + foreach ($command->getAliases() as $alias) { + $widths[] = Helper::width($alias); + } + } else { + $widths[] = Helper::width($command); + } + } + + return $widths ? max($widths) + 2 : 0; + } + + /** + * @param InputOption[] $options + */ + private function calculateTotalWidthForOptions(array $options): int + { + $totalWidth = 0; + foreach ($options as $option) { + // "-" + shortcut + ", --" + name + $nameLength = 1 + max(Helper::width($option->getShortcut()), 1) + 4 + Helper::width($option->getName()); + if ($option->isNegatable()) { + $nameLength += 6 + Helper::width($option->getName()); // |--no- + name + } elseif ($option->acceptValue()) { + $valueLength = 1 + Helper::width($option->getName()); // = + value + $valueLength += $option->isValueOptional() ? 2 : 0; // [ + ] + + $nameLength += $valueLength; + } + $totalWidth = max($totalWidth, $nameLength); + } + + return $totalWidth; + } +} diff --git a/3rdparty/symfony/console/Descriptor/XmlDescriptor.php b/3rdparty/symfony/console/Descriptor/XmlDescriptor.php new file mode 100644 index 00000000..866c7185 --- /dev/null +++ b/3rdparty/symfony/console/Descriptor/XmlDescriptor.php @@ -0,0 +1,232 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Component\Console\Descriptor; + +use Symfony\Component\Console\Application; +use Symfony\Component\Console\Command\Command; +use Symfony\Component\Console\Input\InputArgument; +use Symfony\Component\Console\Input\InputDefinition; +use Symfony\Component\Console\Input\InputOption; + +/** + * XML descriptor. + * + * @author Jean-François Simon + * + * @internal + */ +class XmlDescriptor extends Descriptor +{ + public function getInputDefinitionDocument(InputDefinition $definition): \DOMDocument + { + $dom = new \DOMDocument('1.0', 'UTF-8'); + $dom->appendChild($definitionXML = $dom->createElement('definition')); + + $definitionXML->appendChild($argumentsXML = $dom->createElement('arguments')); + foreach ($definition->getArguments() as $argument) { + $this->appendDocument($argumentsXML, $this->getInputArgumentDocument($argument)); + } + + $definitionXML->appendChild($optionsXML = $dom->createElement('options')); + foreach ($definition->getOptions() as $option) { + $this->appendDocument($optionsXML, $this->getInputOptionDocument($option)); + } + + return $dom; + } + + public function getCommandDocument(Command $command, bool $short = false): \DOMDocument + { + $dom = new \DOMDocument('1.0', 'UTF-8'); + $dom->appendChild($commandXML = $dom->createElement('command')); + + $commandXML->setAttribute('id', $command->getName()); + $commandXML->setAttribute('name', $command->getName()); + $commandXML->setAttribute('hidden', $command->isHidden() ? 1 : 0); + + $commandXML->appendChild($usagesXML = $dom->createElement('usages')); + + $commandXML->appendChild($descriptionXML = $dom->createElement('description')); + $descriptionXML->appendChild($dom->createTextNode(str_replace("\n", "\n ", $command->getDescription()))); + + if ($short) { + foreach ($command->getAliases() as $usage) { + $usagesXML->appendChild($dom->createElement('usage', $usage)); + } + } else { + $command->mergeApplicationDefinition(false); + + foreach (array_merge([$command->getSynopsis()], $command->getAliases(), $command->getUsages()) as $usage) { + $usagesXML->appendChild($dom->createElement('usage', $usage)); + } + + $commandXML->appendChild($helpXML = $dom->createElement('help')); + $helpXML->appendChild($dom->createTextNode(str_replace("\n", "\n ", $command->getProcessedHelp()))); + + $definitionXML = $this->getInputDefinitionDocument($command->getDefinition()); + $this->appendDocument($commandXML, $definitionXML->getElementsByTagName('definition')->item(0)); + } + + return $dom; + } + + public function getApplicationDocument(Application $application, ?string $namespace = null, bool $short = false): \DOMDocument + { + $dom = new \DOMDocument('1.0', 'UTF-8'); + $dom->appendChild($rootXml = $dom->createElement('symfony')); + + if ('UNKNOWN' !== $application->getName()) { + $rootXml->setAttribute('name', $application->getName()); + if ('UNKNOWN' !== $application->getVersion()) { + $rootXml->setAttribute('version', $application->getVersion()); + } + } + + $rootXml->appendChild($commandsXML = $dom->createElement('commands')); + + $description = new ApplicationDescription($application, $namespace, true); + + if ($namespace) { + $commandsXML->setAttribute('namespace', $namespace); + } + + foreach ($description->getCommands() as $command) { + $this->appendDocument($commandsXML, $this->getCommandDocument($command, $short)); + } + + if (!$namespace) { + $rootXml->appendChild($namespacesXML = $dom->createElement('namespaces')); + + foreach ($description->getNamespaces() as $namespaceDescription) { + $namespacesXML->appendChild($namespaceArrayXML = $dom->createElement('namespace')); + $namespaceArrayXML->setAttribute('id', $namespaceDescription['id']); + + foreach ($namespaceDescription['commands'] as $name) { + $namespaceArrayXML->appendChild($commandXML = $dom->createElement('command')); + $commandXML->appendChild($dom->createTextNode($name)); + } + } + } + + return $dom; + } + + protected function describeInputArgument(InputArgument $argument, array $options = []): void + { + $this->writeDocument($this->getInputArgumentDocument($argument)); + } + + protected function describeInputOption(InputOption $option, array $options = []): void + { + $this->writeDocument($this->getInputOptionDocument($option)); + } + + protected function describeInputDefinition(InputDefinition $definition, array $options = []): void + { + $this->writeDocument($this->getInputDefinitionDocument($definition)); + } + + protected function describeCommand(Command $command, array $options = []): void + { + $this->writeDocument($this->getCommandDocument($command, $options['short'] ?? false)); + } + + protected function describeApplication(Application $application, array $options = []): void + { + $this->writeDocument($this->getApplicationDocument($application, $options['namespace'] ?? null, $options['short'] ?? false)); + } + + /** + * Appends document children to parent node. + */ + private function appendDocument(\DOMNode $parentNode, \DOMNode $importedParent): void + { + foreach ($importedParent->childNodes as $childNode) { + $parentNode->appendChild($parentNode->ownerDocument->importNode($childNode, true)); + } + } + + /** + * Writes DOM document. + */ + private function writeDocument(\DOMDocument $dom): void + { + $dom->formatOutput = true; + $this->write($dom->saveXML()); + } + + private function getInputArgumentDocument(InputArgument $argument): \DOMDocument + { + $dom = new \DOMDocument('1.0', 'UTF-8'); + + $dom->appendChild($objectXML = $dom->createElement('argument')); + $objectXML->setAttribute('name', $argument->getName()); + $objectXML->setAttribute('is_required', $argument->isRequired() ? 1 : 0); + $objectXML->setAttribute('is_array', $argument->isArray() ? 1 : 0); + $objectXML->appendChild($descriptionXML = $dom->createElement('description')); + $descriptionXML->appendChild($dom->createTextNode($argument->getDescription())); + + $objectXML->appendChild($defaultsXML = $dom->createElement('defaults')); + $defaults = \is_array($argument->getDefault()) ? $argument->getDefault() : (\is_bool($argument->getDefault()) ? [var_export($argument->getDefault(), true)] : ($argument->getDefault() ? [$argument->getDefault()] : [])); + foreach ($defaults as $default) { + $defaultsXML->appendChild($defaultXML = $dom->createElement('default')); + $defaultXML->appendChild($dom->createTextNode($default)); + } + + return $dom; + } + + private function getInputOptionDocument(InputOption $option): \DOMDocument + { + $dom = new \DOMDocument('1.0', 'UTF-8'); + + $dom->appendChild($objectXML = $dom->createElement('option')); + $objectXML->setAttribute('name', '--'.$option->getName()); + $pos = strpos($option->getShortcut() ?? '', '|'); + if (false !== $pos) { + $objectXML->setAttribute('shortcut', '-'.substr($option->getShortcut(), 0, $pos)); + $objectXML->setAttribute('shortcuts', '-'.str_replace('|', '|-', $option->getShortcut())); + } else { + $objectXML->setAttribute('shortcut', $option->getShortcut() ? '-'.$option->getShortcut() : ''); + } + $objectXML->setAttribute('accept_value', $option->acceptValue() ? 1 : 0); + $objectXML->setAttribute('is_value_required', $option->isValueRequired() ? 1 : 0); + $objectXML->setAttribute('is_multiple', $option->isArray() ? 1 : 0); + $objectXML->appendChild($descriptionXML = $dom->createElement('description')); + $descriptionXML->appendChild($dom->createTextNode($option->getDescription())); + + if ($option->acceptValue()) { + $defaults = \is_array($option->getDefault()) ? $option->getDefault() : (\is_bool($option->getDefault()) ? [var_export($option->getDefault(), true)] : ($option->getDefault() ? [$option->getDefault()] : [])); + $objectXML->appendChild($defaultsXML = $dom->createElement('defaults')); + + if (!empty($defaults)) { + foreach ($defaults as $default) { + $defaultsXML->appendChild($defaultXML = $dom->createElement('default')); + $defaultXML->appendChild($dom->createTextNode($default)); + } + } + } + + if ($option->isNegatable()) { + $dom->appendChild($objectXML = $dom->createElement('option')); + $objectXML->setAttribute('name', '--no-'.$option->getName()); + $objectXML->setAttribute('shortcut', ''); + $objectXML->setAttribute('accept_value', 0); + $objectXML->setAttribute('is_value_required', 0); + $objectXML->setAttribute('is_multiple', 0); + $objectXML->appendChild($descriptionXML = $dom->createElement('description')); + $descriptionXML->appendChild($dom->createTextNode('Negate the "--'.$option->getName().'" option')); + } + + return $dom; + } +} diff --git a/3rdparty/symfony/console/Event/ConsoleCommandEvent.php b/3rdparty/symfony/console/Event/ConsoleCommandEvent.php new file mode 100644 index 00000000..0757a23f --- /dev/null +++ b/3rdparty/symfony/console/Event/ConsoleCommandEvent.php @@ -0,0 +1,54 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Component\Console\Event; + +/** + * Allows to do things before the command is executed, like skipping the command or executing code before the command is + * going to be executed. + * + * Changing the input arguments will have no effect. + * + * @author Fabien Potencier + */ +final class ConsoleCommandEvent extends ConsoleEvent +{ + /** + * The return code for skipped commands, this will also be passed into the terminate event. + */ + public const RETURN_CODE_DISABLED = 113; + + /** + * Indicates if the command should be run or skipped. + */ + private bool $commandShouldRun = true; + + /** + * Disables the command, so it won't be run. + */ + public function disableCommand(): bool + { + return $this->commandShouldRun = false; + } + + public function enableCommand(): bool + { + return $this->commandShouldRun = true; + } + + /** + * Returns true if the command is runnable, false otherwise. + */ + public function commandShouldRun(): bool + { + return $this->commandShouldRun; + } +} diff --git a/3rdparty/symfony/console/Event/ConsoleErrorEvent.php b/3rdparty/symfony/console/Event/ConsoleErrorEvent.php new file mode 100644 index 00000000..7be2ff83 --- /dev/null +++ b/3rdparty/symfony/console/Event/ConsoleErrorEvent.php @@ -0,0 +1,57 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Component\Console\Event; + +use Symfony\Component\Console\Command\Command; +use Symfony\Component\Console\Input\InputInterface; +use Symfony\Component\Console\Output\OutputInterface; + +/** + * Allows to handle throwables thrown while running a command. + * + * @author Wouter de Jong + */ +final class ConsoleErrorEvent extends ConsoleEvent +{ + private \Throwable $error; + private int $exitCode; + + public function __construct(InputInterface $input, OutputInterface $output, \Throwable $error, ?Command $command = null) + { + parent::__construct($command, $input, $output); + + $this->error = $error; + } + + public function getError(): \Throwable + { + return $this->error; + } + + public function setError(\Throwable $error): void + { + $this->error = $error; + } + + public function setExitCode(int $exitCode): void + { + $this->exitCode = $exitCode; + + $r = new \ReflectionProperty($this->error, 'code'); + $r->setValue($this->error, $this->exitCode); + } + + public function getExitCode(): int + { + return $this->exitCode ?? (\is_int($this->error->getCode()) && 0 !== $this->error->getCode() ? $this->error->getCode() : 1); + } +} diff --git a/3rdparty/symfony/console/Event/ConsoleEvent.php b/3rdparty/symfony/console/Event/ConsoleEvent.php new file mode 100644 index 00000000..6ba1615f --- /dev/null +++ b/3rdparty/symfony/console/Event/ConsoleEvent.php @@ -0,0 +1,61 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Component\Console\Event; + +use Symfony\Component\Console\Command\Command; +use Symfony\Component\Console\Input\InputInterface; +use Symfony\Component\Console\Output\OutputInterface; +use Symfony\Contracts\EventDispatcher\Event; + +/** + * Allows to inspect input and output of a command. + * + * @author Francesco Levorato + */ +class ConsoleEvent extends Event +{ + protected $command; + + private InputInterface $input; + private OutputInterface $output; + + public function __construct(?Command $command, InputInterface $input, OutputInterface $output) + { + $this->command = $command; + $this->input = $input; + $this->output = $output; + } + + /** + * Gets the command that is executed. + */ + public function getCommand(): ?Command + { + return $this->command; + } + + /** + * Gets the input instance. + */ + public function getInput(): InputInterface + { + return $this->input; + } + + /** + * Gets the output instance. + */ + public function getOutput(): OutputInterface + { + return $this->output; + } +} diff --git a/3rdparty/symfony/console/Event/ConsoleSignalEvent.php b/3rdparty/symfony/console/Event/ConsoleSignalEvent.php new file mode 100644 index 00000000..95af1f91 --- /dev/null +++ b/3rdparty/symfony/console/Event/ConsoleSignalEvent.php @@ -0,0 +1,56 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Component\Console\Event; + +use Symfony\Component\Console\Command\Command; +use Symfony\Component\Console\Input\InputInterface; +use Symfony\Component\Console\Output\OutputInterface; + +/** + * @author marie + */ +final class ConsoleSignalEvent extends ConsoleEvent +{ + private int $handlingSignal; + private int|false $exitCode; + + public function __construct(Command $command, InputInterface $input, OutputInterface $output, int $handlingSignal, int|false $exitCode = 0) + { + parent::__construct($command, $input, $output); + $this->handlingSignal = $handlingSignal; + $this->exitCode = $exitCode; + } + + public function getHandlingSignal(): int + { + return $this->handlingSignal; + } + + public function setExitCode(int $exitCode): void + { + if ($exitCode < 0 || $exitCode > 255) { + throw new \InvalidArgumentException('Exit code must be between 0 and 255.'); + } + + $this->exitCode = $exitCode; + } + + public function abortExit(): void + { + $this->exitCode = false; + } + + public function getExitCode(): int|false + { + return $this->exitCode; + } +} diff --git a/3rdparty/symfony/console/Event/ConsoleTerminateEvent.php b/3rdparty/symfony/console/Event/ConsoleTerminateEvent.php new file mode 100644 index 00000000..38f7253a --- /dev/null +++ b/3rdparty/symfony/console/Event/ConsoleTerminateEvent.php @@ -0,0 +1,50 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Component\Console\Event; + +use Symfony\Component\Console\Command\Command; +use Symfony\Component\Console\Input\InputInterface; +use Symfony\Component\Console\Output\OutputInterface; + +/** + * Allows to manipulate the exit code of a command after its execution. + * + * @author Francesco Levorato + * @author Jules Pietri + */ +final class ConsoleTerminateEvent extends ConsoleEvent +{ + public function __construct( + Command $command, + InputInterface $input, + OutputInterface $output, + private int $exitCode, + private readonly ?int $interruptingSignal = null, + ) { + parent::__construct($command, $input, $output); + } + + public function setExitCode(int $exitCode): void + { + $this->exitCode = $exitCode; + } + + public function getExitCode(): int + { + return $this->exitCode; + } + + public function getInterruptingSignal(): ?int + { + return $this->interruptingSignal; + } +} diff --git a/3rdparty/symfony/console/EventListener/ErrorListener.php b/3rdparty/symfony/console/EventListener/ErrorListener.php new file mode 100644 index 00000000..c9ec2443 --- /dev/null +++ b/3rdparty/symfony/console/EventListener/ErrorListener.php @@ -0,0 +1,101 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Component\Console\EventListener; + +use Psr\Log\LoggerInterface; +use Symfony\Component\Console\ConsoleEvents; +use Symfony\Component\Console\Event\ConsoleErrorEvent; +use Symfony\Component\Console\Event\ConsoleEvent; +use Symfony\Component\Console\Event\ConsoleTerminateEvent; +use Symfony\Component\EventDispatcher\EventSubscriberInterface; + +/** + * @author James Halsall + * @author Robin Chalas + */ +class ErrorListener implements EventSubscriberInterface +{ + private ?LoggerInterface $logger; + + public function __construct(?LoggerInterface $logger = null) + { + $this->logger = $logger; + } + + /** + * @return void + */ + public function onConsoleError(ConsoleErrorEvent $event) + { + if (null === $this->logger) { + return; + } + + $error = $event->getError(); + + if (!$inputString = $this->getInputString($event)) { + $this->logger->critical('An error occurred while using the console. Message: "{message}"', ['exception' => $error, 'message' => $error->getMessage()]); + + return; + } + + $this->logger->critical('Error thrown while running command "{command}". Message: "{message}"', ['exception' => $error, 'command' => $inputString, 'message' => $error->getMessage()]); + } + + /** + * @return void + */ + public function onConsoleTerminate(ConsoleTerminateEvent $event) + { + if (null === $this->logger) { + return; + } + + $exitCode = $event->getExitCode(); + + if (0 === $exitCode) { + return; + } + + if (!$inputString = $this->getInputString($event)) { + $this->logger->debug('The console exited with code "{code}"', ['code' => $exitCode]); + + return; + } + + $this->logger->debug('Command "{command}" exited with code "{code}"', ['command' => $inputString, 'code' => $exitCode]); + } + + public static function getSubscribedEvents(): array + { + return [ + ConsoleEvents::ERROR => ['onConsoleError', -128], + ConsoleEvents::TERMINATE => ['onConsoleTerminate', -128], + ]; + } + + private static function getInputString(ConsoleEvent $event): ?string + { + $commandName = $event->getCommand()?->getName(); + $input = $event->getInput(); + + if ($input instanceof \Stringable) { + if ($commandName) { + return str_replace(["'$commandName'", "\"$commandName\""], $commandName, (string) $input); + } + + return (string) $input; + } + + return $commandName; + } +} diff --git a/3rdparty/symfony/console/Exception/CommandNotFoundException.php b/3rdparty/symfony/console/Exception/CommandNotFoundException.php new file mode 100644 index 00000000..541b32b2 --- /dev/null +++ b/3rdparty/symfony/console/Exception/CommandNotFoundException.php @@ -0,0 +1,43 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Component\Console\Exception; + +/** + * Represents an incorrect command name typed in the console. + * + * @author Jérôme Tamarelle + */ +class CommandNotFoundException extends \InvalidArgumentException implements ExceptionInterface +{ + private array $alternatives; + + /** + * @param string $message Exception message to throw + * @param string[] $alternatives List of similar defined names + * @param int $code Exception code + * @param \Throwable|null $previous Previous exception used for the exception chaining + */ + public function __construct(string $message, array $alternatives = [], int $code = 0, ?\Throwable $previous = null) + { + parent::__construct($message, $code, $previous); + + $this->alternatives = $alternatives; + } + + /** + * @return string[] + */ + public function getAlternatives(): array + { + return $this->alternatives; + } +} diff --git a/3rdparty/symfony/console/Exception/ExceptionInterface.php b/3rdparty/symfony/console/Exception/ExceptionInterface.php new file mode 100644 index 00000000..1624e13d --- /dev/null +++ b/3rdparty/symfony/console/Exception/ExceptionInterface.php @@ -0,0 +1,21 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Component\Console\Exception; + +/** + * ExceptionInterface. + * + * @author Jérôme Tamarelle + */ +interface ExceptionInterface extends \Throwable +{ +} diff --git a/3rdparty/symfony/console/Exception/InvalidArgumentException.php b/3rdparty/symfony/console/Exception/InvalidArgumentException.php new file mode 100644 index 00000000..07cc0b61 --- /dev/null +++ b/3rdparty/symfony/console/Exception/InvalidArgumentException.php @@ -0,0 +1,19 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Component\Console\Exception; + +/** + * @author Jérôme Tamarelle + */ +class InvalidArgumentException extends \InvalidArgumentException implements ExceptionInterface +{ +} diff --git a/3rdparty/symfony/console/Exception/InvalidOptionException.php b/3rdparty/symfony/console/Exception/InvalidOptionException.php new file mode 100644 index 00000000..5cf62792 --- /dev/null +++ b/3rdparty/symfony/console/Exception/InvalidOptionException.php @@ -0,0 +1,21 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Component\Console\Exception; + +/** + * Represents an incorrect option name or value typed in the console. + * + * @author Jérôme Tamarelle + */ +class InvalidOptionException extends \InvalidArgumentException implements ExceptionInterface +{ +} diff --git a/3rdparty/symfony/console/Exception/LogicException.php b/3rdparty/symfony/console/Exception/LogicException.php new file mode 100644 index 00000000..fc37b8d8 --- /dev/null +++ b/3rdparty/symfony/console/Exception/LogicException.php @@ -0,0 +1,19 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Component\Console\Exception; + +/** + * @author Jérôme Tamarelle + */ +class LogicException extends \LogicException implements ExceptionInterface +{ +} diff --git a/3rdparty/symfony/console/Exception/MissingInputException.php b/3rdparty/symfony/console/Exception/MissingInputException.php new file mode 100644 index 00000000..04f02ade --- /dev/null +++ b/3rdparty/symfony/console/Exception/MissingInputException.php @@ -0,0 +1,21 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Component\Console\Exception; + +/** + * Represents failure to read input from stdin. + * + * @author Gabriel Ostrolucký + */ +class MissingInputException extends RuntimeException implements ExceptionInterface +{ +} diff --git a/3rdparty/symfony/console/Exception/NamespaceNotFoundException.php b/3rdparty/symfony/console/Exception/NamespaceNotFoundException.php new file mode 100644 index 00000000..dd16e450 --- /dev/null +++ b/3rdparty/symfony/console/Exception/NamespaceNotFoundException.php @@ -0,0 +1,21 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Component\Console\Exception; + +/** + * Represents an incorrect namespace typed in the console. + * + * @author Pierre du Plessis + */ +class NamespaceNotFoundException extends CommandNotFoundException +{ +} diff --git a/3rdparty/symfony/console/Exception/RunCommandFailedException.php b/3rdparty/symfony/console/Exception/RunCommandFailedException.php new file mode 100644 index 00000000..5d87ec94 --- /dev/null +++ b/3rdparty/symfony/console/Exception/RunCommandFailedException.php @@ -0,0 +1,29 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Component\Console\Exception; + +use Symfony\Component\Console\Messenger\RunCommandContext; + +/** + * @author Kevin Bond + */ +final class RunCommandFailedException extends RuntimeException +{ + public function __construct(\Throwable|string $exception, public readonly RunCommandContext $context) + { + parent::__construct( + $exception instanceof \Throwable ? $exception->getMessage() : $exception, + $exception instanceof \Throwable ? $exception->getCode() : 0, + $exception instanceof \Throwable ? $exception : null, + ); + } +} diff --git a/3rdparty/symfony/console/Exception/RuntimeException.php b/3rdparty/symfony/console/Exception/RuntimeException.php new file mode 100644 index 00000000..51d7d80a --- /dev/null +++ b/3rdparty/symfony/console/Exception/RuntimeException.php @@ -0,0 +1,19 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Component\Console\Exception; + +/** + * @author Jérôme Tamarelle + */ +class RuntimeException extends \RuntimeException implements ExceptionInterface +{ +} diff --git a/3rdparty/symfony/console/Formatter/NullOutputFormatter.php b/3rdparty/symfony/console/Formatter/NullOutputFormatter.php new file mode 100644 index 00000000..5c11c764 --- /dev/null +++ b/3rdparty/symfony/console/Formatter/NullOutputFormatter.php @@ -0,0 +1,51 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Component\Console\Formatter; + +/** + * @author Tien Xuan Vo + */ +final class NullOutputFormatter implements OutputFormatterInterface +{ + private NullOutputFormatterStyle $style; + + public function format(?string $message): ?string + { + return null; + } + + public function getStyle(string $name): OutputFormatterStyleInterface + { + // to comply with the interface we must return a OutputFormatterStyleInterface + return $this->style ??= new NullOutputFormatterStyle(); + } + + public function hasStyle(string $name): bool + { + return false; + } + + public function isDecorated(): bool + { + return false; + } + + public function setDecorated(bool $decorated): void + { + // do nothing + } + + public function setStyle(string $name, OutputFormatterStyleInterface $style): void + { + // do nothing + } +} diff --git a/3rdparty/symfony/console/Formatter/NullOutputFormatterStyle.php b/3rdparty/symfony/console/Formatter/NullOutputFormatterStyle.php new file mode 100644 index 00000000..ae23decb --- /dev/null +++ b/3rdparty/symfony/console/Formatter/NullOutputFormatterStyle.php @@ -0,0 +1,54 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Component\Console\Formatter; + +/** + * @author Tien Xuan Vo + */ +final class NullOutputFormatterStyle implements OutputFormatterStyleInterface +{ + public function apply(string $text): string + { + return $text; + } + + public function setBackground(?string $color = null): void + { + if (1 > \func_num_args()) { + trigger_deprecation('symfony/console', '6.2', 'Calling "%s()" without any arguments is deprecated, pass null explicitly instead.', __METHOD__); + } + // do nothing + } + + public function setForeground(?string $color = null): void + { + if (1 > \func_num_args()) { + trigger_deprecation('symfony/console', '6.2', 'Calling "%s()" without any arguments is deprecated, pass null explicitly instead.', __METHOD__); + } + // do nothing + } + + public function setOption(string $option): void + { + // do nothing + } + + public function setOptions(array $options): void + { + // do nothing + } + + public function unsetOption(string $option): void + { + // do nothing + } +} diff --git a/3rdparty/symfony/console/Formatter/OutputFormatter.php b/3rdparty/symfony/console/Formatter/OutputFormatter.php new file mode 100644 index 00000000..3e4897c3 --- /dev/null +++ b/3rdparty/symfony/console/Formatter/OutputFormatter.php @@ -0,0 +1,277 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Component\Console\Formatter; + +use Symfony\Component\Console\Exception\InvalidArgumentException; + +use function Symfony\Component\String\b; + +/** + * Formatter class for console output. + * + * @author Konstantin Kudryashov + * @author Roland Franssen + */ +class OutputFormatter implements WrappableOutputFormatterInterface +{ + private bool $decorated; + private array $styles = []; + private OutputFormatterStyleStack $styleStack; + + public function __clone() + { + $this->styleStack = clone $this->styleStack; + foreach ($this->styles as $key => $value) { + $this->styles[$key] = clone $value; + } + } + + /** + * Escapes "<" and ">" special chars in given text. + */ + public static function escape(string $text): string + { + $text = preg_replace('/([^\\\\]|^)([<>])/', '$1\\\\$2', $text); + + return self::escapeTrailingBackslash($text); + } + + /** + * Escapes trailing "\" in given text. + * + * @internal + */ + public static function escapeTrailingBackslash(string $text): string + { + if (str_ends_with($text, '\\')) { + $len = \strlen($text); + $text = rtrim($text, '\\'); + $text = str_replace("\0", '', $text); + $text .= str_repeat("\0", $len - \strlen($text)); + } + + return $text; + } + + /** + * Initializes console output formatter. + * + * @param OutputFormatterStyleInterface[] $styles Array of "name => FormatterStyle" instances + */ + public function __construct(bool $decorated = false, array $styles = []) + { + $this->decorated = $decorated; + + $this->setStyle('error', new OutputFormatterStyle('white', 'red')); + $this->setStyle('info', new OutputFormatterStyle('green')); + $this->setStyle('comment', new OutputFormatterStyle('yellow')); + $this->setStyle('question', new OutputFormatterStyle('black', 'cyan')); + + foreach ($styles as $name => $style) { + $this->setStyle($name, $style); + } + + $this->styleStack = new OutputFormatterStyleStack(); + } + + /** + * @return void + */ + public function setDecorated(bool $decorated) + { + $this->decorated = $decorated; + } + + public function isDecorated(): bool + { + return $this->decorated; + } + + /** + * @return void + */ + public function setStyle(string $name, OutputFormatterStyleInterface $style) + { + $this->styles[strtolower($name)] = $style; + } + + public function hasStyle(string $name): bool + { + return isset($this->styles[strtolower($name)]); + } + + public function getStyle(string $name): OutputFormatterStyleInterface + { + if (!$this->hasStyle($name)) { + throw new InvalidArgumentException(sprintf('Undefined style: "%s".', $name)); + } + + return $this->styles[strtolower($name)]; + } + + public function format(?string $message): ?string + { + return $this->formatAndWrap($message, 0); + } + + /** + * @return string + */ + public function formatAndWrap(?string $message, int $width) + { + if (null === $message) { + return ''; + } + + $offset = 0; + $output = ''; + $openTagRegex = '[a-z](?:[^\\\\<>]*+ | \\\\.)*'; + $closeTagRegex = '[a-z][^<>]*+'; + $currentLineLength = 0; + preg_match_all("#<(($openTagRegex) | /($closeTagRegex)?)>#ix", $message, $matches, \PREG_OFFSET_CAPTURE); + foreach ($matches[0] as $i => $match) { + $pos = $match[1]; + $text = $match[0]; + + if (0 != $pos && '\\' == $message[$pos - 1]) { + continue; + } + + // add the text up to the next tag + $output .= $this->applyCurrentStyle(substr($message, $offset, $pos - $offset), $output, $width, $currentLineLength); + $offset = $pos + \strlen($text); + + // opening tag? + if ($open = '/' !== $text[1]) { + $tag = $matches[1][$i][0]; + } else { + $tag = $matches[3][$i][0] ?? ''; + } + + if (!$open && !$tag) { + // + $this->styleStack->pop(); + } elseif (null === $style = $this->createStyleFromString($tag)) { + $output .= $this->applyCurrentStyle($text, $output, $width, $currentLineLength); + } elseif ($open) { + $this->styleStack->push($style); + } else { + $this->styleStack->pop($style); + } + } + + $output .= $this->applyCurrentStyle(substr($message, $offset), $output, $width, $currentLineLength); + + return strtr($output, ["\0" => '\\', '\\<' => '<', '\\>' => '>']); + } + + public function getStyleStack(): OutputFormatterStyleStack + { + return $this->styleStack; + } + + /** + * Tries to create new style instance from string. + */ + private function createStyleFromString(string $string): ?OutputFormatterStyleInterface + { + if (isset($this->styles[$string])) { + return $this->styles[$string]; + } + + if (!preg_match_all('/([^=]+)=([^;]+)(;|$)/', $string, $matches, \PREG_SET_ORDER)) { + return null; + } + + $style = new OutputFormatterStyle(); + foreach ($matches as $match) { + array_shift($match); + $match[0] = strtolower($match[0]); + + if ('fg' == $match[0]) { + $style->setForeground(strtolower($match[1])); + } elseif ('bg' == $match[0]) { + $style->setBackground(strtolower($match[1])); + } elseif ('href' === $match[0]) { + $url = preg_replace('{\\\\([<>])}', '$1', $match[1]); + $style->setHref($url); + } elseif ('options' === $match[0]) { + preg_match_all('([^,;]+)', strtolower($match[1]), $options); + $options = array_shift($options); + foreach ($options as $option) { + $style->setOption($option); + } + } else { + return null; + } + } + + return $style; + } + + /** + * Applies current style from stack to text, if must be applied. + */ + private function applyCurrentStyle(string $text, string $current, int $width, int &$currentLineLength): string + { + if ('' === $text) { + return ''; + } + + if (!$width) { + return $this->isDecorated() ? $this->styleStack->getCurrent()->apply($text) : $text; + } + + if (!$currentLineLength && '' !== $current) { + $text = ltrim($text); + } + + if ($currentLineLength) { + $prefix = substr($text, 0, $i = $width - $currentLineLength)."\n"; + $text = substr($text, $i); + } 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; + } + + $lines = explode("\n", $text); + + foreach ($lines as $line) { + $currentLineLength += \strlen($line); + if ($width <= $currentLineLength) { + $currentLineLength = 0; + } + } + + if ($this->isDecorated()) { + foreach ($lines as $i => $line) { + $lines[$i] = $this->styleStack->getCurrent()->apply($line); + } + } + + return implode("\n", $lines); + } + + private function addLineBreaks(string $text, int $width): string + { + $encoding = mb_detect_encoding($text, null, true) ?: 'UTF-8'; + + return b($text)->toCodePointString($encoding)->wordwrap($width, "\n", true)->toByteString($encoding); + } +} diff --git a/3rdparty/symfony/console/Formatter/OutputFormatterInterface.php b/3rdparty/symfony/console/Formatter/OutputFormatterInterface.php new file mode 100644 index 00000000..433cd419 --- /dev/null +++ b/3rdparty/symfony/console/Formatter/OutputFormatterInterface.php @@ -0,0 +1,56 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Component\Console\Formatter; + +/** + * Formatter interface for console output. + * + * @author Konstantin Kudryashov + */ +interface OutputFormatterInterface +{ + /** + * Sets the decorated flag. + * + * @return void + */ + public function setDecorated(bool $decorated); + + /** + * Whether the output will decorate messages. + */ + public function isDecorated(): bool; + + /** + * Sets a new style. + * + * @return void + */ + public function setStyle(string $name, OutputFormatterStyleInterface $style); + + /** + * Checks if output formatter has style with specified name. + */ + public function hasStyle(string $name): bool; + + /** + * Gets style options from style with specified name. + * + * @throws \InvalidArgumentException When style isn't defined + */ + public function getStyle(string $name): OutputFormatterStyleInterface; + + /** + * Formats a message according to the given styles. + */ + public function format(?string $message): ?string; +} diff --git a/3rdparty/symfony/console/Formatter/OutputFormatterStyle.php b/3rdparty/symfony/console/Formatter/OutputFormatterStyle.php new file mode 100644 index 00000000..21e7f5ab --- /dev/null +++ b/3rdparty/symfony/console/Formatter/OutputFormatterStyle.php @@ -0,0 +1,110 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Component\Console\Formatter; + +use Symfony\Component\Console\Color; + +/** + * Formatter style class for defining styles. + * + * @author Konstantin Kudryashov + */ +class OutputFormatterStyle implements OutputFormatterStyleInterface +{ + private Color $color; + private string $foreground; + private string $background; + private array $options; + private ?string $href = null; + private bool $handlesHrefGracefully; + + /** + * Initializes output formatter style. + * + * @param string|null $foreground The style foreground color name + * @param string|null $background The style background color name + */ + public function __construct(?string $foreground = null, ?string $background = null, array $options = []) + { + $this->color = new Color($this->foreground = $foreground ?: '', $this->background = $background ?: '', $this->options = $options); + } + + /** + * @return void + */ + public function setForeground(?string $color = null) + { + if (1 > \func_num_args()) { + trigger_deprecation('symfony/console', '6.2', 'Calling "%s()" without any arguments is deprecated, pass null explicitly instead.', __METHOD__); + } + $this->color = new Color($this->foreground = $color ?: '', $this->background, $this->options); + } + + /** + * @return void + */ + public function setBackground(?string $color = null) + { + if (1 > \func_num_args()) { + trigger_deprecation('symfony/console', '6.2', 'Calling "%s()" without any arguments is deprecated, pass null explicitly instead.', __METHOD__); + } + $this->color = new Color($this->foreground, $this->background = $color ?: '', $this->options); + } + + public function setHref(string $url): void + { + $this->href = $url; + } + + /** + * @return void + */ + public function setOption(string $option) + { + $this->options[] = $option; + $this->color = new Color($this->foreground, $this->background, $this->options); + } + + /** + * @return void + */ + public function unsetOption(string $option) + { + $pos = array_search($option, $this->options); + if (false !== $pos) { + unset($this->options[$pos]); + } + + $this->color = new Color($this->foreground, $this->background, $this->options); + } + + /** + * @return void + */ + public function setOptions(array $options) + { + $this->color = new Color($this->foreground, $this->background, $this->options = $options); + } + + public function apply(string $text): string + { + $this->handlesHrefGracefully ??= 'JetBrains-JediTerm' !== getenv('TERMINAL_EMULATOR') + && (!getenv('KONSOLE_VERSION') || (int) getenv('KONSOLE_VERSION') > 201100) + && !isset($_SERVER['IDEA_INITIAL_DIRECTORY']); + + if (null !== $this->href && $this->handlesHrefGracefully) { + $text = "\033]8;;$this->href\033\\$text\033]8;;\033\\"; + } + + return $this->color->apply($text); + } +} diff --git a/3rdparty/symfony/console/Formatter/OutputFormatterStyleInterface.php b/3rdparty/symfony/console/Formatter/OutputFormatterStyleInterface.php new file mode 100644 index 00000000..3b15098c --- /dev/null +++ b/3rdparty/symfony/console/Formatter/OutputFormatterStyleInterface.php @@ -0,0 +1,60 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Component\Console\Formatter; + +/** + * Formatter style interface for defining styles. + * + * @author Konstantin Kudryashov + */ +interface OutputFormatterStyleInterface +{ + /** + * Sets style foreground color. + * + * @return void + */ + public function setForeground(?string $color); + + /** + * Sets style background color. + * + * @return void + */ + public function setBackground(?string $color); + + /** + * Sets some specific style option. + * + * @return void + */ + public function setOption(string $option); + + /** + * Unsets some specific style option. + * + * @return void + */ + public function unsetOption(string $option); + + /** + * Sets multiple style options at once. + * + * @return void + */ + public function setOptions(array $options); + + /** + * Applies the style to a given text. + */ + public function apply(string $text): string; +} diff --git a/3rdparty/symfony/console/Formatter/OutputFormatterStyleStack.php b/3rdparty/symfony/console/Formatter/OutputFormatterStyleStack.php new file mode 100644 index 00000000..62d2ca0e --- /dev/null +++ b/3rdparty/symfony/console/Formatter/OutputFormatterStyleStack.php @@ -0,0 +1,107 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Component\Console\Formatter; + +use Symfony\Component\Console\Exception\InvalidArgumentException; +use Symfony\Contracts\Service\ResetInterface; + +/** + * @author Jean-François Simon + */ +class OutputFormatterStyleStack implements ResetInterface +{ + /** + * @var OutputFormatterStyleInterface[] + */ + private array $styles = []; + + private OutputFormatterStyleInterface $emptyStyle; + + public function __construct(?OutputFormatterStyleInterface $emptyStyle = null) + { + $this->emptyStyle = $emptyStyle ?? new OutputFormatterStyle(); + $this->reset(); + } + + /** + * Resets stack (ie. empty internal arrays). + * + * @return void + */ + public function reset() + { + $this->styles = []; + } + + /** + * Pushes a style in the stack. + * + * @return void + */ + public function push(OutputFormatterStyleInterface $style) + { + $this->styles[] = $style; + } + + /** + * Pops a style from the stack. + * + * @throws InvalidArgumentException When style tags incorrectly nested + */ + public function pop(?OutputFormatterStyleInterface $style = null): OutputFormatterStyleInterface + { + if (!$this->styles) { + return $this->emptyStyle; + } + + if (null === $style) { + return array_pop($this->styles); + } + + foreach (array_reverse($this->styles, true) as $index => $stackedStyle) { + if ($style->apply('') === $stackedStyle->apply('')) { + $this->styles = \array_slice($this->styles, 0, $index); + + return $stackedStyle; + } + } + + throw new InvalidArgumentException('Incorrectly nested style tag found.'); + } + + /** + * Computes current style with stacks top codes. + */ + public function getCurrent(): OutputFormatterStyleInterface + { + if (!$this->styles) { + return $this->emptyStyle; + } + + return $this->styles[\count($this->styles) - 1]; + } + + /** + * @return $this + */ + public function setEmptyStyle(OutputFormatterStyleInterface $emptyStyle): static + { + $this->emptyStyle = $emptyStyle; + + return $this; + } + + public function getEmptyStyle(): OutputFormatterStyleInterface + { + return $this->emptyStyle; + } +} diff --git a/3rdparty/symfony/console/Formatter/WrappableOutputFormatterInterface.php b/3rdparty/symfony/console/Formatter/WrappableOutputFormatterInterface.php new file mode 100644 index 00000000..746cd27e --- /dev/null +++ b/3rdparty/symfony/console/Formatter/WrappableOutputFormatterInterface.php @@ -0,0 +1,27 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Component\Console\Formatter; + +/** + * Formatter interface for console output that supports word wrapping. + * + * @author Roland Franssen + */ +interface WrappableOutputFormatterInterface extends OutputFormatterInterface +{ + /** + * Formats a message according to the given styles, wrapping at `$width` (0 means no wrapping). + * + * @return string + */ + public function formatAndWrap(?string $message, int $width); +} diff --git a/3rdparty/symfony/console/Helper/DebugFormatterHelper.php b/3rdparty/symfony/console/Helper/DebugFormatterHelper.php new file mode 100644 index 00000000..9ea7fb91 --- /dev/null +++ b/3rdparty/symfony/console/Helper/DebugFormatterHelper.php @@ -0,0 +1,98 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Component\Console\Helper; + +/** + * Helps outputting debug information when running an external program from a command. + * + * An external program can be a Process, an HTTP request, or anything else. + * + * @author Fabien Potencier + */ +class DebugFormatterHelper extends Helper +{ + private const COLORS = ['black', 'red', 'green', 'yellow', 'blue', 'magenta', 'cyan', 'white', 'default']; + private array $started = []; + private int $count = -1; + + /** + * Starts a debug formatting session. + */ + public function start(string $id, string $message, string $prefix = 'RUN'): string + { + $this->started[$id] = ['border' => ++$this->count % \count(self::COLORS)]; + + return sprintf("%s %s %s\n", $this->getBorder($id), $prefix, $message); + } + + /** + * Adds progress to a formatting session. + */ + public function progress(string $id, string $buffer, bool $error = false, string $prefix = 'OUT', string $errorPrefix = 'ERR'): string + { + $message = ''; + + if ($error) { + if (isset($this->started[$id]['out'])) { + $message .= "\n"; + unset($this->started[$id]['out']); + } + if (!isset($this->started[$id]['err'])) { + $message .= sprintf('%s %s ', $this->getBorder($id), $errorPrefix); + $this->started[$id]['err'] = true; + } + + $message .= str_replace("\n", sprintf("\n%s %s ", $this->getBorder($id), $errorPrefix), $buffer); + } else { + if (isset($this->started[$id]['err'])) { + $message .= "\n"; + unset($this->started[$id]['err']); + } + if (!isset($this->started[$id]['out'])) { + $message .= sprintf('%s %s ', $this->getBorder($id), $prefix); + $this->started[$id]['out'] = true; + } + + $message .= str_replace("\n", sprintf("\n%s %s ", $this->getBorder($id), $prefix), $buffer); + } + + return $message; + } + + /** + * Stops a formatting session. + */ + public function stop(string $id, string $message, bool $successful, string $prefix = 'RES'): string + { + $trailingEOL = isset($this->started[$id]['out']) || isset($this->started[$id]['err']) ? "\n" : ''; + + if ($successful) { + return sprintf("%s%s %s %s\n", $trailingEOL, $this->getBorder($id), $prefix, $message); + } + + $message = sprintf("%s%s %s %s\n", $trailingEOL, $this->getBorder($id), $prefix, $message); + + unset($this->started[$id]['out'], $this->started[$id]['err']); + + return $message; + } + + private function getBorder(string $id): string + { + return sprintf(' ', self::COLORS[$this->started[$id]['border']]); + } + + public function getName(): string + { + return 'debug_formatter'; + } +} diff --git a/3rdparty/symfony/console/Helper/DescriptorHelper.php b/3rdparty/symfony/console/Helper/DescriptorHelper.php new file mode 100644 index 00000000..eb32bce8 --- /dev/null +++ b/3rdparty/symfony/console/Helper/DescriptorHelper.php @@ -0,0 +1,93 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Component\Console\Helper; + +use Symfony\Component\Console\Descriptor\DescriptorInterface; +use Symfony\Component\Console\Descriptor\JsonDescriptor; +use Symfony\Component\Console\Descriptor\MarkdownDescriptor; +use Symfony\Component\Console\Descriptor\ReStructuredTextDescriptor; +use Symfony\Component\Console\Descriptor\TextDescriptor; +use Symfony\Component\Console\Descriptor\XmlDescriptor; +use Symfony\Component\Console\Exception\InvalidArgumentException; +use Symfony\Component\Console\Output\OutputInterface; + +/** + * This class adds helper method to describe objects in various formats. + * + * @author Jean-François Simon + */ +class DescriptorHelper extends Helper +{ + /** + * @var DescriptorInterface[] + */ + private array $descriptors = []; + + public function __construct() + { + $this + ->register('txt', new TextDescriptor()) + ->register('xml', new XmlDescriptor()) + ->register('json', new JsonDescriptor()) + ->register('md', new MarkdownDescriptor()) + ->register('rst', new ReStructuredTextDescriptor()) + ; + } + + /** + * Describes an object if supported. + * + * Available options are: + * * format: string, the output format name + * * raw_text: boolean, sets output type as raw + * + * @return void + * + * @throws InvalidArgumentException when the given format is not supported + */ + public function describe(OutputInterface $output, ?object $object, array $options = []) + { + $options = array_merge([ + 'raw_text' => false, + 'format' => 'txt', + ], $options); + + if (!isset($this->descriptors[$options['format']])) { + throw new InvalidArgumentException(sprintf('Unsupported format "%s".', $options['format'])); + } + + $descriptor = $this->descriptors[$options['format']]; + $descriptor->describe($output, $object, $options); + } + + /** + * Registers a descriptor. + * + * @return $this + */ + public function register(string $format, DescriptorInterface $descriptor): static + { + $this->descriptors[$format] = $descriptor; + + return $this; + } + + public function getName(): string + { + return 'descriptor'; + } + + public function getFormats(): array + { + return array_keys($this->descriptors); + } +} diff --git a/3rdparty/symfony/console/Helper/Dumper.php b/3rdparty/symfony/console/Helper/Dumper.php new file mode 100644 index 00000000..a3b8e395 --- /dev/null +++ b/3rdparty/symfony/console/Helper/Dumper.php @@ -0,0 +1,57 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Component\Console\Helper; + +use Symfony\Component\Console\Output\OutputInterface; +use Symfony\Component\VarDumper\Cloner\ClonerInterface; +use Symfony\Component\VarDumper\Cloner\VarCloner; +use Symfony\Component\VarDumper\Dumper\CliDumper; + +/** + * @author Roland Franssen + */ +final class Dumper +{ + private OutputInterface $output; + private ?CliDumper $dumper; + private ?ClonerInterface $cloner; + private \Closure $handler; + + public function __construct(OutputInterface $output, ?CliDumper $dumper = null, ?ClonerInterface $cloner = null) + { + $this->output = $output; + $this->dumper = $dumper; + $this->cloner = $cloner; + + if (class_exists(CliDumper::class)) { + $this->handler = function ($var): string { + $dumper = $this->dumper ??= new CliDumper(null, null, CliDumper::DUMP_LIGHT_ARRAY | CliDumper::DUMP_COMMA_SEPARATOR); + $dumper->setColors($this->output->isDecorated()); + + return rtrim($dumper->dump(($this->cloner ??= new VarCloner())->cloneVar($var)->withRefHandles(false), true)); + }; + } else { + $this->handler = fn ($var): string => match (true) { + null === $var => 'null', + true === $var => 'true', + false === $var => 'false', + \is_string($var) => '"'.$var.'"', + default => rtrim(print_r($var, true)), + }; + } + } + + public function __invoke(mixed $var): string + { + return ($this->handler)($var); + } +} diff --git a/3rdparty/symfony/console/Helper/FormatterHelper.php b/3rdparty/symfony/console/Helper/FormatterHelper.php new file mode 100644 index 00000000..279e4c79 --- /dev/null +++ b/3rdparty/symfony/console/Helper/FormatterHelper.php @@ -0,0 +1,81 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Component\Console\Helper; + +use Symfony\Component\Console\Formatter\OutputFormatter; + +/** + * The Formatter class provides helpers to format messages. + * + * @author Fabien Potencier + */ +class FormatterHelper extends Helper +{ + /** + * Formats a message within a section. + */ + public function formatSection(string $section, string $message, string $style = 'info'): string + { + return sprintf('<%s>[%s] %s', $style, $section, $style, $message); + } + + /** + * Formats a message as a block of text. + */ + public function formatBlock(string|array $messages, string $style, bool $large = false): string + { + if (!\is_array($messages)) { + $messages = [$messages]; + } + + $len = 0; + $lines = []; + foreach ($messages as $message) { + $message = OutputFormatter::escape($message); + $lines[] = sprintf($large ? ' %s ' : ' %s ', $message); + $len = max(self::width($message) + ($large ? 4 : 2), $len); + } + + $messages = $large ? [str_repeat(' ', $len)] : []; + for ($i = 0; isset($lines[$i]); ++$i) { + $messages[] = $lines[$i].str_repeat(' ', $len - self::width($lines[$i])); + } + if ($large) { + $messages[] = str_repeat(' ', $len); + } + + for ($i = 0; isset($messages[$i]); ++$i) { + $messages[$i] = sprintf('<%s>%s', $style, $messages[$i], $style); + } + + return implode("\n", $messages); + } + + /** + * Truncates a message to the given length. + */ + public function truncate(string $message, int $length, string $suffix = '...'): string + { + $computedLength = $length - self::width($suffix); + + if ($computedLength > self::width($message)) { + return $message; + } + + return self::substr($message, 0, $length).$suffix; + } + + public function getName(): string + { + return 'formatter'; + } +} diff --git a/3rdparty/symfony/console/Helper/Helper.php b/3rdparty/symfony/console/Helper/Helper.php new file mode 100644 index 00000000..05be6478 --- /dev/null +++ b/3rdparty/symfony/console/Helper/Helper.php @@ -0,0 +1,174 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Component\Console\Helper; + +use Symfony\Component\Console\Formatter\OutputFormatterInterface; +use Symfony\Component\String\UnicodeString; + +/** + * Helper is the base class for all helper classes. + * + * @author Fabien Potencier + */ +abstract class Helper implements HelperInterface +{ + protected $helperSet; + + /** + * @return void + */ + public function setHelperSet(?HelperSet $helperSet = null) + { + if (1 > \func_num_args()) { + trigger_deprecation('symfony/console', '6.2', 'Calling "%s()" without any arguments is deprecated, pass null explicitly instead.', __METHOD__); + } + $this->helperSet = $helperSet; + } + + public function getHelperSet(): ?HelperSet + { + return $this->helperSet; + } + + /** + * Returns the width of a string, using mb_strwidth if it is available. + * The width is how many characters positions the string will use. + */ + public static function width(?string $string): int + { + $string ??= ''; + + if (preg_match('//u', $string)) { + return (new UnicodeString($string))->width(false); + } + + if (false === $encoding = mb_detect_encoding($string, null, true)) { + return \strlen($string); + } + + return mb_strwidth($string, $encoding); + } + + /** + * Returns the length of a string, using mb_strlen if it is available. + * The length is related to how many bytes the string will use. + */ + public static function length(?string $string): int + { + $string ??= ''; + + if (preg_match('//u', $string)) { + return (new UnicodeString($string))->length(); + } + + if (false === $encoding = mb_detect_encoding($string, null, true)) { + return \strlen($string); + } + + return mb_strlen($string, $encoding); + } + + /** + * Returns the subset of a string, using mb_substr if it is available. + */ + public static function substr(?string $string, int $from, ?int $length = null): string + { + $string ??= ''; + + if (false === $encoding = mb_detect_encoding($string, null, true)) { + return substr($string, $from, $length); + } + + return mb_substr($string, $from, $length, $encoding); + } + + /** + * @return string + */ + public static function formatTime(int|float $secs, int $precision = 1) + { + $secs = (int) floor($secs); + + if (0 === $secs) { + return '< 1 sec'; + } + + static $timeFormats = [ + [1, '1 sec', 'secs'], + [60, '1 min', 'mins'], + [3600, '1 hr', 'hrs'], + [86400, '1 day', 'days'], + ]; + + $times = []; + foreach ($timeFormats as $index => $format) { + $seconds = isset($timeFormats[$index + 1]) ? $secs % $timeFormats[$index + 1][0] : $secs; + + if (isset($times[$index - $precision])) { + unset($times[$index - $precision]); + } + + if (0 === $seconds) { + continue; + } + + $unitCount = ($seconds / $format[0]); + $times[$index] = 1 === $unitCount ? $format[1] : $unitCount.' '.$format[2]; + + if ($secs === $seconds) { + break; + } + + $secs -= $seconds; + } + + return implode(', ', array_reverse($times)); + } + + /** + * @return string + */ + public static function formatMemory(int $memory) + { + if ($memory >= 1024 * 1024 * 1024) { + return sprintf('%.1f GiB', $memory / 1024 / 1024 / 1024); + } + + if ($memory >= 1024 * 1024) { + return sprintf('%.1f MiB', $memory / 1024 / 1024); + } + + if ($memory >= 1024) { + return sprintf('%d KiB', $memory / 1024); + } + + return sprintf('%d B', $memory); + } + + /** + * @return string + */ + public static function removeDecoration(OutputFormatterInterface $formatter, ?string $string) + { + $isDecorated = $formatter->isDecorated(); + $formatter->setDecorated(false); + // remove <...> formatting + $string = $formatter->format($string ?? ''); + // remove already formatted characters + $string = preg_replace("/\033\[[^m]*m/", '', $string ?? ''); + // remove terminal hyperlinks + $string = preg_replace('/\\033]8;[^;]*;[^\\033]*\\033\\\\/', '', $string ?? ''); + $formatter->setDecorated($isDecorated); + + return $string; + } +} diff --git a/3rdparty/symfony/console/Helper/HelperInterface.php b/3rdparty/symfony/console/Helper/HelperInterface.php new file mode 100644 index 00000000..ab626c93 --- /dev/null +++ b/3rdparty/symfony/console/Helper/HelperInterface.php @@ -0,0 +1,39 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Component\Console\Helper; + +/** + * HelperInterface is the interface all helpers must implement. + * + * @author Fabien Potencier + */ +interface HelperInterface +{ + /** + * Sets the helper set associated with this helper. + * + * @return void + */ + public function setHelperSet(?HelperSet $helperSet); + + /** + * Gets the helper set associated with this helper. + */ + public function getHelperSet(): ?HelperSet; + + /** + * Returns the canonical name of this helper. + * + * @return string + */ + public function getName(); +} diff --git a/3rdparty/symfony/console/Helper/HelperSet.php b/3rdparty/symfony/console/Helper/HelperSet.php new file mode 100644 index 00000000..f8c74ca2 --- /dev/null +++ b/3rdparty/symfony/console/Helper/HelperSet.php @@ -0,0 +1,77 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Component\Console\Helper; + +use Symfony\Component\Console\Exception\InvalidArgumentException; + +/** + * HelperSet represents a set of helpers to be used with a command. + * + * @author Fabien Potencier + * + * @implements \IteratorAggregate + */ +class HelperSet implements \IteratorAggregate +{ + /** @var array */ + private array $helpers = []; + + /** + * @param HelperInterface[] $helpers + */ + public function __construct(array $helpers = []) + { + foreach ($helpers as $alias => $helper) { + $this->set($helper, \is_int($alias) ? null : $alias); + } + } + + /** + * @return void + */ + public function set(HelperInterface $helper, ?string $alias = null) + { + $this->helpers[$helper->getName()] = $helper; + if (null !== $alias) { + $this->helpers[$alias] = $helper; + } + + $helper->setHelperSet($this); + } + + /** + * Returns true if the helper if defined. + */ + public function has(string $name): bool + { + return isset($this->helpers[$name]); + } + + /** + * Gets a helper value. + * + * @throws InvalidArgumentException if the helper is not defined + */ + public function get(string $name): HelperInterface + { + if (!$this->has($name)) { + throw new InvalidArgumentException(sprintf('The helper "%s" is not defined.', $name)); + } + + return $this->helpers[$name]; + } + + public function getIterator(): \Traversable + { + return new \ArrayIterator($this->helpers); + } +} diff --git a/3rdparty/symfony/console/Helper/InputAwareHelper.php b/3rdparty/symfony/console/Helper/InputAwareHelper.php new file mode 100644 index 00000000..6f822597 --- /dev/null +++ b/3rdparty/symfony/console/Helper/InputAwareHelper.php @@ -0,0 +1,33 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Component\Console\Helper; + +use Symfony\Component\Console\Input\InputAwareInterface; +use Symfony\Component\Console\Input\InputInterface; + +/** + * An implementation of InputAwareInterface for Helpers. + * + * @author Wouter J + */ +abstract class InputAwareHelper extends Helper implements InputAwareInterface +{ + protected $input; + + /** + * @return void + */ + public function setInput(InputInterface $input) + { + $this->input = $input; + } +} diff --git a/3rdparty/symfony/console/Helper/OutputWrapper.php b/3rdparty/symfony/console/Helper/OutputWrapper.php new file mode 100644 index 00000000..2ec819c7 --- /dev/null +++ b/3rdparty/symfony/console/Helper/OutputWrapper.php @@ -0,0 +1,76 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Component\Console\Helper; + +/** + * Simple output wrapper for "tagged outputs" instead of wordwrap(). This solution is based on a StackOverflow + * answer: https://stackoverflow.com/a/20434776/1476819 from user557597 (alias SLN). + * + * (?: + * # -- Words/Characters + * ( # (1 start) + * (?> # Atomic Group - Match words with valid breaks + * .{1,16} # 1-N characters + * # Followed by one of 4 prioritized, non-linebreak whitespace + * (?: # break types: + * (?<= [^\S\r\n] ) # 1. - Behind a non-linebreak whitespace + * [^\S\r\n]? # ( optionally accept an extra non-linebreak whitespace ) + * | (?= \r? \n ) # 2. - Ahead a linebreak + * | $ # 3. - EOS + * | [^\S\r\n] # 4. - Accept an extra non-linebreak whitespace + * ) + * ) # End atomic group + * | + * .{1,16} # No valid word breaks, just break on the N'th character + * ) # (1 end) + * (?: \r? \n )? # Optional linebreak after Words/Characters + * | + * # -- Or, Linebreak + * (?: \r? \n | $ ) # Stand alone linebreak or at EOS + * ) + * + * @author Krisztián Ferenczi + * + * @see https://stackoverflow.com/a/20434776/1476819 + */ +final class OutputWrapper +{ + private const TAG_OPEN_REGEX_SEGMENT = '[a-z](?:[^\\\\<>]*+ | \\\\.)*'; + private const TAG_CLOSE_REGEX_SEGMENT = '[a-z][^<>]*+'; + private const URL_PATTERN = 'https?://\S+'; + + public function __construct( + private bool $allowCutUrls = false + ) { + } + + public function wrap(string $text, int $width, string $break = "\n"): string + { + if (!$width) { + return $text; + } + + $tagPattern = sprintf('<(?:(?:%s)|/(?:%s)?)>', self::TAG_OPEN_REGEX_SEGMENT, self::TAG_CLOSE_REGEX_SEGMENT); + $limitPattern = "{1,$width}"; + $patternBlocks = [$tagPattern]; + if (!$this->allowCutUrls) { + $patternBlocks[] = self::URL_PATTERN; + } + $patternBlocks[] = '.'; + $blocks = implode('|', $patternBlocks); + $rowPattern = "(?:$blocks)$limitPattern"; + $pattern = sprintf('#(?:((?>(%1$s)((?<=[^\S\r\n])[^\S\r\n]?|(?=\r?\n)|$|[^\S\r\n]))|(%1$s))(?:\r?\n)?|(?:\r?\n|$))#imux', $rowPattern); + $output = rtrim(preg_replace($pattern, '\\1'.$break, $text), $break); + + return str_replace(' '.$break, $break, $output); + } +} diff --git a/3rdparty/symfony/console/Helper/ProcessHelper.php b/3rdparty/symfony/console/Helper/ProcessHelper.php new file mode 100644 index 00000000..3ef6f71f --- /dev/null +++ b/3rdparty/symfony/console/Helper/ProcessHelper.php @@ -0,0 +1,137 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Component\Console\Helper; + +use Symfony\Component\Console\Output\ConsoleOutputInterface; +use Symfony\Component\Console\Output\OutputInterface; +use Symfony\Component\Process\Exception\ProcessFailedException; +use Symfony\Component\Process\Process; + +/** + * The ProcessHelper class provides helpers to run external processes. + * + * @author Fabien Potencier + * + * @final + */ +class ProcessHelper extends Helper +{ + /** + * Runs an external process. + * + * @param array|Process $cmd An instance of Process or an array of the command and arguments + * @param callable|null $callback A PHP callback to run whenever there is some + * output available on STDOUT or STDERR + */ + public function run(OutputInterface $output, array|Process $cmd, ?string $error = null, ?callable $callback = null, int $verbosity = OutputInterface::VERBOSITY_VERY_VERBOSE): Process + { + if (!class_exists(Process::class)) { + throw new \LogicException('The ProcessHelper cannot be run as the Process component is not installed. Try running "compose require symfony/process".'); + } + + if ($output instanceof ConsoleOutputInterface) { + $output = $output->getErrorOutput(); + } + + $formatter = $this->getHelperSet()->get('debug_formatter'); + + if ($cmd instanceof Process) { + $cmd = [$cmd]; + } + + if (\is_string($cmd[0] ?? null)) { + $process = new Process($cmd); + $cmd = []; + } elseif (($cmd[0] ?? null) instanceof Process) { + $process = $cmd[0]; + unset($cmd[0]); + } else { + throw new \InvalidArgumentException(sprintf('Invalid command provided to "%s()": the command should be an array whose first element is either the path to the binary to run or a "Process" object.', __METHOD__)); + } + + if ($verbosity <= $output->getVerbosity()) { + $output->write($formatter->start(spl_object_hash($process), $this->escapeString($process->getCommandLine()))); + } + + if ($output->isDebug()) { + $callback = $this->wrapCallback($output, $process, $callback); + } + + $process->run($callback, $cmd); + + if ($verbosity <= $output->getVerbosity()) { + $message = $process->isSuccessful() ? 'Command ran successfully' : sprintf('%s Command did not run successfully', $process->getExitCode()); + $output->write($formatter->stop(spl_object_hash($process), $message, $process->isSuccessful())); + } + + if (!$process->isSuccessful() && null !== $error) { + $output->writeln(sprintf('%s', $this->escapeString($error))); + } + + return $process; + } + + /** + * Runs the process. + * + * This is identical to run() except that an exception is thrown if the process + * exits with a non-zero exit code. + * + * @param array|Process $cmd An instance of Process or a command to run + * @param callable|null $callback A PHP callback to run whenever there is some + * output available on STDOUT or STDERR + * + * @throws ProcessFailedException + * + * @see run() + */ + public function mustRun(OutputInterface $output, array|Process $cmd, ?string $error = null, ?callable $callback = null): Process + { + $process = $this->run($output, $cmd, $error, $callback); + + if (!$process->isSuccessful()) { + throw new ProcessFailedException($process); + } + + return $process; + } + + /** + * Wraps a Process callback to add debugging output. + */ + public function wrapCallback(OutputInterface $output, Process $process, ?callable $callback = null): callable + { + if ($output instanceof ConsoleOutputInterface) { + $output = $output->getErrorOutput(); + } + + $formatter = $this->getHelperSet()->get('debug_formatter'); + + return function ($type, $buffer) use ($output, $process, $callback, $formatter) { + $output->write($formatter->progress(spl_object_hash($process), $this->escapeString($buffer), Process::ERR === $type)); + + if (null !== $callback) { + $callback($type, $buffer); + } + }; + } + + private function escapeString(string $str): string + { + return str_replace('<', '\\<', $str); + } + + public function getName(): string + { + return 'process'; + } +} diff --git a/3rdparty/symfony/console/Helper/ProgressBar.php b/3rdparty/symfony/console/Helper/ProgressBar.php new file mode 100644 index 00000000..23157e3c --- /dev/null +++ b/3rdparty/symfony/console/Helper/ProgressBar.php @@ -0,0 +1,618 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Component\Console\Helper; + +use Symfony\Component\Console\Cursor; +use Symfony\Component\Console\Exception\LogicException; +use Symfony\Component\Console\Output\ConsoleOutputInterface; +use Symfony\Component\Console\Output\ConsoleSectionOutput; +use Symfony\Component\Console\Output\OutputInterface; +use Symfony\Component\Console\Terminal; + +/** + * The ProgressBar provides helpers to display progress output. + * + * @author Fabien Potencier + * @author Chris Jones + */ +final class ProgressBar +{ + public const FORMAT_VERBOSE = 'verbose'; + public const FORMAT_VERY_VERBOSE = 'very_verbose'; + public const FORMAT_DEBUG = 'debug'; + public const FORMAT_NORMAL = 'normal'; + + private const FORMAT_VERBOSE_NOMAX = 'verbose_nomax'; + private const FORMAT_VERY_VERBOSE_NOMAX = 'very_verbose_nomax'; + private const FORMAT_DEBUG_NOMAX = 'debug_nomax'; + private const FORMAT_NORMAL_NOMAX = 'normal_nomax'; + + private int $barWidth = 28; + private string $barChar; + private string $emptyBarChar = '-'; + private string $progressChar = '>'; + private ?string $format = null; + private ?string $internalFormat = null; + private ?int $redrawFreq = 1; + private int $writeCount = 0; + private float $lastWriteTime = 0; + private float $minSecondsBetweenRedraws = 0; + private float $maxSecondsBetweenRedraws = 1; + private OutputInterface $output; + private int $step = 0; + private int $startingStep = 0; + private ?int $max = null; + private int $startTime; + private int $stepWidth; + private float $percent = 0.0; + private array $messages = []; + private bool $overwrite = true; + private Terminal $terminal; + private ?string $previousMessage = null; + private Cursor $cursor; + private array $placeholders = []; + + private static array $formatters; + private static array $formats; + + /** + * @param int $max Maximum steps (0 if unknown) + */ + public function __construct(OutputInterface $output, int $max = 0, float $minSecondsBetweenRedraws = 1 / 25) + { + if ($output instanceof ConsoleOutputInterface) { + $output = $output->getErrorOutput(); + } + + $this->output = $output; + $this->setMaxSteps($max); + $this->terminal = new Terminal(); + + if (0 < $minSecondsBetweenRedraws) { + $this->redrawFreq = null; + $this->minSecondsBetweenRedraws = $minSecondsBetweenRedraws; + } + + if (!$this->output->isDecorated()) { + // disable overwrite when output does not support ANSI codes. + $this->overwrite = false; + + // set a reasonable redraw frequency so output isn't flooded + $this->redrawFreq = null; + } + + $this->startTime = time(); + $this->cursor = new Cursor($output); + } + + /** + * Sets a placeholder formatter for a given name, globally for all instances of ProgressBar. + * + * This method also allow you to override an existing placeholder. + * + * @param string $name The placeholder name (including the delimiter char like %) + * @param callable(ProgressBar):string $callable A PHP callable + */ + public static function setPlaceholderFormatterDefinition(string $name, callable $callable): void + { + self::$formatters ??= self::initPlaceholderFormatters(); + + self::$formatters[$name] = $callable; + } + + /** + * Gets the placeholder formatter for a given name. + * + * @param string $name The placeholder name (including the delimiter char like %) + */ + public static function getPlaceholderFormatterDefinition(string $name): ?callable + { + self::$formatters ??= self::initPlaceholderFormatters(); + + return self::$formatters[$name] ?? null; + } + + /** + * Sets a placeholder formatter for a given name, for this instance only. + * + * @param callable(ProgressBar):string $callable A PHP callable + */ + public function setPlaceholderFormatter(string $name, callable $callable): void + { + $this->placeholders[$name] = $callable; + } + + /** + * Gets the placeholder formatter for a given name. + * + * @param string $name The placeholder name (including the delimiter char like %) + */ + public function getPlaceholderFormatter(string $name): ?callable + { + return $this->placeholders[$name] ?? $this::getPlaceholderFormatterDefinition($name); + } + + /** + * Sets a format for a given name. + * + * This method also allow you to override an existing format. + * + * @param string $name The format name + * @param string $format A format string + */ + public static function setFormatDefinition(string $name, string $format): void + { + self::$formats ??= self::initFormats(); + + self::$formats[$name] = $format; + } + + /** + * Gets the format for a given name. + * + * @param string $name The format name + */ + public static function getFormatDefinition(string $name): ?string + { + self::$formats ??= self::initFormats(); + + return self::$formats[$name] ?? null; + } + + /** + * Associates a text with a named placeholder. + * + * The text is displayed when the progress bar is rendered but only + * when the corresponding placeholder is part of the custom format line + * (by wrapping the name with %). + * + * @param string $message The text to associate with the placeholder + * @param string $name The name of the placeholder + */ + public function setMessage(string $message, string $name = 'message'): void + { + $this->messages[$name] = $message; + } + + public function getMessage(string $name = 'message'): ?string + { + return $this->messages[$name] ?? null; + } + + public function getStartTime(): int + { + return $this->startTime; + } + + public function getMaxSteps(): int + { + return $this->max; + } + + public function getProgress(): int + { + return $this->step; + } + + private function getStepWidth(): int + { + return $this->stepWidth; + } + + public function getProgressPercent(): float + { + return $this->percent; + } + + public function getBarOffset(): float + { + return floor($this->max ? $this->percent * $this->barWidth : (null === $this->redrawFreq ? (int) (min(5, $this->barWidth / 15) * $this->writeCount) : $this->step) % $this->barWidth); + } + + public function getEstimated(): float + { + if (0 === $this->step || $this->step === $this->startingStep) { + return 0; + } + + return round((time() - $this->startTime) / ($this->step - $this->startingStep) * $this->max); + } + + public function getRemaining(): float + { + if (0 === $this->step || $this->step === $this->startingStep) { + return 0; + } + + return round((time() - $this->startTime) / ($this->step - $this->startingStep) * ($this->max - $this->step)); + } + + public function setBarWidth(int $size): void + { + $this->barWidth = max(1, $size); + } + + public function getBarWidth(): int + { + return $this->barWidth; + } + + public function setBarCharacter(string $char): void + { + $this->barChar = $char; + } + + public function getBarCharacter(): string + { + return $this->barChar ?? ($this->max ? '=' : $this->emptyBarChar); + } + + public function setEmptyBarCharacter(string $char): void + { + $this->emptyBarChar = $char; + } + + public function getEmptyBarCharacter(): string + { + return $this->emptyBarChar; + } + + public function setProgressCharacter(string $char): void + { + $this->progressChar = $char; + } + + public function getProgressCharacter(): string + { + return $this->progressChar; + } + + public function setFormat(string $format): void + { + $this->format = null; + $this->internalFormat = $format; + } + + /** + * Sets the redraw frequency. + * + * @param int|null $freq The frequency in steps + */ + public function setRedrawFrequency(?int $freq): void + { + $this->redrawFreq = null !== $freq ? max(1, $freq) : null; + } + + public function minSecondsBetweenRedraws(float $seconds): void + { + $this->minSecondsBetweenRedraws = $seconds; + } + + public function maxSecondsBetweenRedraws(float $seconds): void + { + $this->maxSecondsBetweenRedraws = $seconds; + } + + /** + * Returns an iterator that will automatically update the progress bar when iterated. + * + * @template TKey + * @template TValue + * + * @param iterable $iterable + * @param int|null $max Number of steps to complete the bar (0 if indeterminate), if null it will be inferred from $iterable + * + * @return iterable + */ + public function iterate(iterable $iterable, ?int $max = null): iterable + { + $this->start($max ?? (is_countable($iterable) ? \count($iterable) : 0)); + + foreach ($iterable as $key => $value) { + yield $key => $value; + + $this->advance(); + } + + $this->finish(); + } + + /** + * Starts the progress output. + * + * @param int|null $max Number of steps to complete the bar (0 if indeterminate), null to leave unchanged + * @param int $startAt The starting point of the bar (useful e.g. when resuming a previously started bar) + */ + public function start(?int $max = null, int $startAt = 0): void + { + $this->startTime = time(); + $this->step = $startAt; + $this->startingStep = $startAt; + + $startAt > 0 ? $this->setProgress($startAt) : $this->percent = 0.0; + + if (null !== $max) { + $this->setMaxSteps($max); + } + + $this->display(); + } + + /** + * Advances the progress output X steps. + * + * @param int $step Number of steps to advance + */ + public function advance(int $step = 1): void + { + $this->setProgress($this->step + $step); + } + + /** + * Sets whether to overwrite the progressbar, false for new line. + */ + public function setOverwrite(bool $overwrite): void + { + $this->overwrite = $overwrite; + } + + public function setProgress(int $step): void + { + if ($this->max && $step > $this->max) { + $this->max = $step; + } elseif ($step < 0) { + $step = 0; + } + + $redrawFreq = $this->redrawFreq ?? (($this->max ?: 10) / 10); + $prevPeriod = (int) ($this->step / $redrawFreq); + $currPeriod = (int) ($step / $redrawFreq); + $this->step = $step; + $this->percent = $this->max ? (float) $this->step / $this->max : 0; + $timeInterval = microtime(true) - $this->lastWriteTime; + + // Draw regardless of other limits + if ($this->max === $step) { + $this->display(); + + return; + } + + // Throttling + if ($timeInterval < $this->minSecondsBetweenRedraws) { + return; + } + + // Draw each step period, but not too late + if ($prevPeriod !== $currPeriod || $timeInterval >= $this->maxSecondsBetweenRedraws) { + $this->display(); + } + } + + public function setMaxSteps(int $max): void + { + $this->format = null; + $this->max = max(0, $max); + $this->stepWidth = $this->max ? Helper::width((string) $this->max) : 4; + } + + /** + * Finishes the progress output. + */ + public function finish(): void + { + if (!$this->max) { + $this->max = $this->step; + } + + if ($this->step === $this->max && !$this->overwrite) { + // prevent double 100% output + return; + } + + $this->setProgress($this->max); + } + + /** + * Outputs the current progress string. + */ + public function display(): void + { + if (OutputInterface::VERBOSITY_QUIET === $this->output->getVerbosity()) { + return; + } + + if (null === $this->format) { + $this->setRealFormat($this->internalFormat ?: $this->determineBestFormat()); + } + + $this->overwrite($this->buildLine()); + } + + /** + * Removes the progress bar from the current line. + * + * This is useful if you wish to write some output + * while a progress bar is running. + * Call display() to show the progress bar again. + */ + public function clear(): void + { + if (!$this->overwrite) { + return; + } + + if (null === $this->format) { + $this->setRealFormat($this->internalFormat ?: $this->determineBestFormat()); + } + + $this->overwrite(''); + } + + private function setRealFormat(string $format): void + { + // try to use the _nomax variant if available + if (!$this->max && null !== self::getFormatDefinition($format.'_nomax')) { + $this->format = self::getFormatDefinition($format.'_nomax'); + } elseif (null !== self::getFormatDefinition($format)) { + $this->format = self::getFormatDefinition($format); + } else { + $this->format = $format; + } + } + + /** + * Overwrites a previous message to the output. + */ + private function overwrite(string $message): void + { + if ($this->previousMessage === $message) { + return; + } + + $originalMessage = $message; + + if ($this->overwrite) { + if (null !== $this->previousMessage) { + if ($this->output instanceof ConsoleSectionOutput) { + $messageLines = explode("\n", $this->previousMessage); + $lineCount = \count($messageLines); + 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"); + for ($i = 0; $i < $lineCount; ++$i) { + $this->cursor->moveToColumn(1); + $this->cursor->clearLine(); + $this->cursor->moveUp(); + } + + $this->cursor->moveToColumn(1); + $this->cursor->clearLine(); + } + } + } elseif ($this->step > 0) { + $message = \PHP_EOL.$message; + } + + $this->previousMessage = $originalMessage; + $this->lastWriteTime = microtime(true); + + $this->output->write($message); + ++$this->writeCount; + } + + private function determineBestFormat(): string + { + return match ($this->output->getVerbosity()) { + // OutputInterface::VERBOSITY_QUIET: display is disabled anyway + OutputInterface::VERBOSITY_VERBOSE => $this->max ? self::FORMAT_VERBOSE : self::FORMAT_VERBOSE_NOMAX, + OutputInterface::VERBOSITY_VERY_VERBOSE => $this->max ? self::FORMAT_VERY_VERBOSE : self::FORMAT_VERY_VERBOSE_NOMAX, + OutputInterface::VERBOSITY_DEBUG => $this->max ? self::FORMAT_DEBUG : self::FORMAT_DEBUG_NOMAX, + default => $this->max ? self::FORMAT_NORMAL : self::FORMAT_NORMAL_NOMAX, + }; + } + + private static function initPlaceholderFormatters(): array + { + return [ + 'bar' => function (self $bar, OutputInterface $output) { + $completeBars = $bar->getBarOffset(); + $display = str_repeat($bar->getBarCharacter(), $completeBars); + if ($completeBars < $bar->getBarWidth()) { + $emptyBars = $bar->getBarWidth() - $completeBars - Helper::length(Helper::removeDecoration($output->getFormatter(), $bar->getProgressCharacter())); + $display .= $bar->getProgressCharacter().str_repeat($bar->getEmptyBarCharacter(), $emptyBars); + } + + return $display; + }, + 'elapsed' => fn (self $bar) => Helper::formatTime(time() - $bar->getStartTime(), 2), + 'remaining' => function (self $bar) { + if (!$bar->getMaxSteps()) { + throw new LogicException('Unable to display the remaining time if the maximum number of steps is not set.'); + } + + return Helper::formatTime($bar->getRemaining(), 2); + }, + 'estimated' => function (self $bar) { + if (!$bar->getMaxSteps()) { + throw new LogicException('Unable to display the estimated time if the maximum number of steps is not set.'); + } + + return Helper::formatTime($bar->getEstimated(), 2); + }, + 'memory' => fn (self $bar) => Helper::formatMemory(memory_get_usage(true)), + 'current' => fn (self $bar) => str_pad($bar->getProgress(), $bar->getStepWidth(), ' ', \STR_PAD_LEFT), + 'max' => fn (self $bar) => $bar->getMaxSteps(), + 'percent' => fn (self $bar) => floor($bar->getProgressPercent() * 100), + ]; + } + + private static function initFormats(): array + { + return [ + self::FORMAT_NORMAL => ' %current%/%max% [%bar%] %percent:3s%%', + self::FORMAT_NORMAL_NOMAX => ' %current% [%bar%]', + + self::FORMAT_VERBOSE => ' %current%/%max% [%bar%] %percent:3s%% %elapsed:6s%', + self::FORMAT_VERBOSE_NOMAX => ' %current% [%bar%] %elapsed:6s%', + + self::FORMAT_VERY_VERBOSE => ' %current%/%max% [%bar%] %percent:3s%% %elapsed:6s%/%estimated:-6s%', + self::FORMAT_VERY_VERBOSE_NOMAX => ' %current% [%bar%] %elapsed:6s%', + + self::FORMAT_DEBUG => ' %current%/%max% [%bar%] %percent:3s%% %elapsed:6s%/%estimated:-6s% %memory:6s%', + self::FORMAT_DEBUG_NOMAX => ' %current% [%bar%] %elapsed:6s% %memory:6s%', + ]; + } + + private function buildLine(): string + { + \assert(null !== $this->format); + + $regex = "{%([a-z\-_]+)(?:\:([^%]+))?%}i"; + $callback = function ($matches) { + if ($formatter = $this->getPlaceholderFormatter($matches[1])) { + $text = $formatter($this, $this->output); + } elseif (isset($this->messages[$matches[1]])) { + $text = $this->messages[$matches[1]]; + } else { + return $matches[0]; + } + + if (isset($matches[2])) { + $text = sprintf('%'.$matches[2], $text); + } + + return $text; + }; + $line = preg_replace_callback($regex, $callback, $this->format); + + // gets string length for each sub line with multiline format + $linesLength = array_map(fn ($subLine) => Helper::width(Helper::removeDecoration($this->output->getFormatter(), rtrim($subLine, "\r"))), explode("\n", $line)); + + $linesWidth = max($linesLength); + + $terminalWidth = $this->terminal->getWidth(); + if ($linesWidth <= $terminalWidth) { + return $line; + } + + $this->setBarWidth($this->barWidth - $linesWidth + $terminalWidth); + + return preg_replace_callback($regex, $callback, $this->format); + } +} diff --git a/3rdparty/symfony/console/Helper/ProgressIndicator.php b/3rdparty/symfony/console/Helper/ProgressIndicator.php new file mode 100644 index 00000000..92106caf --- /dev/null +++ b/3rdparty/symfony/console/Helper/ProgressIndicator.php @@ -0,0 +1,235 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Component\Console\Helper; + +use Symfony\Component\Console\Exception\InvalidArgumentException; +use Symfony\Component\Console\Exception\LogicException; +use Symfony\Component\Console\Output\OutputInterface; + +/** + * @author Kevin Bond + */ +class ProgressIndicator +{ + private const FORMATS = [ + 'normal' => ' %indicator% %message%', + 'normal_no_ansi' => ' %message%', + + 'verbose' => ' %indicator% %message% (%elapsed:6s%)', + 'verbose_no_ansi' => ' %message% (%elapsed:6s%)', + + 'very_verbose' => ' %indicator% %message% (%elapsed:6s%, %memory:6s%)', + 'very_verbose_no_ansi' => ' %message% (%elapsed:6s%, %memory:6s%)', + ]; + + private OutputInterface $output; + private int $startTime; + private ?string $format = null; + private ?string $message = null; + private array $indicatorValues; + private int $indicatorCurrent; + private int $indicatorChangeInterval; + private float $indicatorUpdateTime; + private bool $started = false; + + /** + * @var array + */ + private static array $formatters; + + /** + * @param int $indicatorChangeInterval Change interval in milliseconds + * @param array|null $indicatorValues Animated indicator characters + */ + public function __construct(OutputInterface $output, ?string $format = null, int $indicatorChangeInterval = 100, ?array $indicatorValues = null) + { + $this->output = $output; + + $format ??= $this->determineBestFormat(); + $indicatorValues ??= ['-', '\\', '|', '/']; + $indicatorValues = array_values($indicatorValues); + + if (2 > \count($indicatorValues)) { + throw new InvalidArgumentException('Must have at least 2 indicator value characters.'); + } + + $this->format = self::getFormatDefinition($format); + $this->indicatorChangeInterval = $indicatorChangeInterval; + $this->indicatorValues = $indicatorValues; + $this->startTime = time(); + } + + /** + * Sets the current indicator message. + * + * @return void + */ + public function setMessage(?string $message) + { + $this->message = $message; + + $this->display(); + } + + /** + * Starts the indicator output. + * + * @return void + */ + public function start(string $message) + { + if ($this->started) { + throw new LogicException('Progress indicator already started.'); + } + + $this->message = $message; + $this->started = true; + $this->startTime = time(); + $this->indicatorUpdateTime = $this->getCurrentTimeInMilliseconds() + $this->indicatorChangeInterval; + $this->indicatorCurrent = 0; + + $this->display(); + } + + /** + * Advances the indicator. + * + * @return void + */ + public function advance() + { + if (!$this->started) { + throw new LogicException('Progress indicator has not yet been started.'); + } + + if (!$this->output->isDecorated()) { + return; + } + + $currentTime = $this->getCurrentTimeInMilliseconds(); + + if ($currentTime < $this->indicatorUpdateTime) { + return; + } + + $this->indicatorUpdateTime = $currentTime + $this->indicatorChangeInterval; + ++$this->indicatorCurrent; + + $this->display(); + } + + /** + * Finish the indicator with message. + * + * @return void + */ + public function finish(string $message) + { + if (!$this->started) { + throw new LogicException('Progress indicator has not yet been started.'); + } + + $this->message = $message; + $this->display(); + $this->output->writeln(''); + $this->started = false; + } + + /** + * Gets the format for a given name. + */ + public static function getFormatDefinition(string $name): ?string + { + return self::FORMATS[$name] ?? null; + } + + /** + * Sets a placeholder formatter for a given name. + * + * This method also allow you to override an existing placeholder. + * + * @return void + */ + public static function setPlaceholderFormatterDefinition(string $name, callable $callable) + { + self::$formatters ??= self::initPlaceholderFormatters(); + + self::$formatters[$name] = $callable; + } + + /** + * Gets the placeholder formatter for a given name (including the delimiter char like %). + */ + public static function getPlaceholderFormatterDefinition(string $name): ?callable + { + self::$formatters ??= self::initPlaceholderFormatters(); + + return self::$formatters[$name] ?? null; + } + + private function display(): void + { + if (OutputInterface::VERBOSITY_QUIET === $this->output->getVerbosity()) { + return; + } + + $this->overwrite(preg_replace_callback("{%([a-z\-_]+)(?:\:([^%]+))?%}i", function ($matches) { + if ($formatter = self::getPlaceholderFormatterDefinition($matches[1])) { + return $formatter($this); + } + + return $matches[0]; + }, $this->format ?? '')); + } + + private function determineBestFormat(): string + { + return match ($this->output->getVerbosity()) { + // OutputInterface::VERBOSITY_QUIET: display is disabled anyway + OutputInterface::VERBOSITY_VERBOSE => $this->output->isDecorated() ? 'verbose' : 'verbose_no_ansi', + OutputInterface::VERBOSITY_VERY_VERBOSE, + OutputInterface::VERBOSITY_DEBUG => $this->output->isDecorated() ? 'very_verbose' : 'very_verbose_no_ansi', + default => $this->output->isDecorated() ? 'normal' : 'normal_no_ansi', + }; + } + + /** + * Overwrites a previous message to the output. + */ + private function overwrite(string $message): void + { + if ($this->output->isDecorated()) { + $this->output->write("\x0D\x1B[2K"); + $this->output->write($message); + } else { + $this->output->writeln($message); + } + } + + private function getCurrentTimeInMilliseconds(): float + { + return round(microtime(true) * 1000); + } + + /** + * @return array + */ + private static function initPlaceholderFormatters(): array + { + return [ + 'indicator' => fn (self $indicator) => $indicator->indicatorValues[$indicator->indicatorCurrent % \count($indicator->indicatorValues)], + 'message' => fn (self $indicator) => $indicator->message, + 'elapsed' => fn (self $indicator) => Helper::formatTime(time() - $indicator->startTime, 2), + 'memory' => fn () => Helper::formatMemory(memory_get_usage(true)), + ]; + } +} diff --git a/3rdparty/symfony/console/Helper/QuestionHelper.php b/3rdparty/symfony/console/Helper/QuestionHelper.php new file mode 100644 index 00000000..b40b1319 --- /dev/null +++ b/3rdparty/symfony/console/Helper/QuestionHelper.php @@ -0,0 +1,600 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Component\Console\Helper; + +use Symfony\Component\Console\Cursor; +use Symfony\Component\Console\Exception\MissingInputException; +use Symfony\Component\Console\Exception\RuntimeException; +use Symfony\Component\Console\Formatter\OutputFormatter; +use Symfony\Component\Console\Formatter\OutputFormatterStyle; +use Symfony\Component\Console\Input\InputInterface; +use Symfony\Component\Console\Input\StreamableInputInterface; +use Symfony\Component\Console\Output\ConsoleOutputInterface; +use Symfony\Component\Console\Output\ConsoleSectionOutput; +use Symfony\Component\Console\Output\OutputInterface; +use Symfony\Component\Console\Question\ChoiceQuestion; +use Symfony\Component\Console\Question\Question; +use Symfony\Component\Console\Terminal; + +use function Symfony\Component\String\s; + +/** + * The QuestionHelper class provides helpers to interact with the user. + * + * @author Fabien Potencier + */ +class QuestionHelper extends Helper +{ + /** + * @var resource|null + */ + private $inputStream; + + private static bool $stty = true; + private static bool $stdinIsInteractive; + + /** + * Asks a question to the user. + * + * @return mixed The user answer + * + * @throws RuntimeException If there is no data to read in the input stream + */ + public function ask(InputInterface $input, OutputInterface $output, Question $question): mixed + { + if ($output instanceof ConsoleOutputInterface) { + $output = $output->getErrorOutput(); + } + + if (!$input->isInteractive()) { + return $this->getDefaultAnswer($question); + } + + if ($input instanceof StreamableInputInterface && $stream = $input->getStream()) { + $this->inputStream = $stream; + } + + try { + if (!$question->getValidator()) { + return $this->doAsk($output, $question); + } + + $interviewer = fn () => $this->doAsk($output, $question); + + return $this->validateAttempts($interviewer, $output, $question); + } catch (MissingInputException $exception) { + $input->setInteractive(false); + + if (null === $fallbackOutput = $this->getDefaultAnswer($question)) { + throw $exception; + } + + return $fallbackOutput; + } + } + + public function getName(): string + { + return 'question'; + } + + /** + * Prevents usage of stty. + * + * @return void + */ + public static function disableStty() + { + self::$stty = false; + } + + /** + * Asks the question to the user. + * + * @throws RuntimeException In case the fallback is deactivated and the response cannot be hidden + */ + private function doAsk(OutputInterface $output, Question $question): mixed + { + $this->writePrompt($output, $question); + + $inputStream = $this->inputStream ?: \STDIN; + $autocomplete = $question->getAutocompleterCallback(); + + if (null === $autocomplete || !self::$stty || !Terminal::hasSttyAvailable()) { + $ret = false; + if ($question->isHidden()) { + try { + $hiddenResponse = $this->getHiddenResponse($output, $inputStream, $question->isTrimmable()); + $ret = $question->isTrimmable() ? trim($hiddenResponse) : $hiddenResponse; + } catch (RuntimeException $e) { + if (!$question->isHiddenFallback()) { + throw $e; + } + } + } + + if (false === $ret) { + $isBlocked = stream_get_meta_data($inputStream)['blocked'] ?? true; + + if (!$isBlocked) { + stream_set_blocking($inputStream, true); + } + + $ret = $this->readInput($inputStream, $question); + + if (!$isBlocked) { + stream_set_blocking($inputStream, false); + } + + if (false === $ret) { + throw new MissingInputException('Aborted.'); + } + if ($question->isTrimmable()) { + $ret = trim($ret); + } + } + } else { + $autocomplete = $this->autocomplete($output, $question, $inputStream, $autocomplete); + $ret = $question->isTrimmable() ? trim($autocomplete) : $autocomplete; + } + + if ($output instanceof ConsoleSectionOutput) { + $output->addContent(''); // add EOL to the question + $output->addContent($ret); + } + + $ret = \strlen($ret) > 0 ? $ret : $question->getDefault(); + + if ($normalizer = $question->getNormalizer()) { + return $normalizer($ret); + } + + return $ret; + } + + private function getDefaultAnswer(Question $question): mixed + { + $default = $question->getDefault(); + + if (null === $default) { + return $default; + } + + if ($validator = $question->getValidator()) { + return \call_user_func($validator, $default); + } elseif ($question instanceof ChoiceQuestion) { + $choices = $question->getChoices(); + + if (!$question->isMultiselect()) { + return $choices[$default] ?? $default; + } + + $default = explode(',', $default); + foreach ($default as $k => $v) { + $v = $question->isTrimmable() ? trim($v) : $v; + $default[$k] = $choices[$v] ?? $v; + } + } + + return $default; + } + + /** + * Outputs the question prompt. + * + * @return void + */ + protected function writePrompt(OutputInterface $output, Question $question) + { + $message = $question->getQuestion(); + + if ($question instanceof ChoiceQuestion) { + $output->writeln(array_merge([ + $question->getQuestion(), + ], $this->formatChoiceQuestionChoices($question, 'info'))); + + $message = $question->getPrompt(); + } + + $output->write($message); + } + + /** + * @return string[] + */ + protected function formatChoiceQuestionChoices(ChoiceQuestion $question, string $tag): array + { + $messages = []; + + $maxWidth = max(array_map([__CLASS__, 'width'], array_keys($choices = $question->getChoices()))); + + foreach ($choices as $key => $value) { + $padding = str_repeat(' ', $maxWidth - self::width($key)); + + $messages[] = sprintf(" [<$tag>%s$padding] %s", $key, $value); + } + + return $messages; + } + + /** + * Outputs an error message. + * + * @return void + */ + protected function writeError(OutputInterface $output, \Exception $error) + { + if (null !== $this->getHelperSet() && $this->getHelperSet()->has('formatter')) { + $message = $this->getHelperSet()->get('formatter')->formatBlock($error->getMessage(), 'error'); + } else { + $message = ''.$error->getMessage().''; + } + + $output->writeln($message); + } + + /** + * Autocompletes a question. + * + * @param resource $inputStream + */ + private function autocomplete(OutputInterface $output, Question $question, $inputStream, callable $autocomplete): string + { + $cursor = new Cursor($output, $inputStream); + + $fullChoice = ''; + $ret = ''; + + $i = 0; + $ofs = -1; + $matches = $autocomplete($ret); + $numMatches = \count($matches); + + $sttyMode = shell_exec('stty -g'); + $isStdin = 'php://stdin' === (stream_get_meta_data($inputStream)['uri'] ?? null); + $r = [$inputStream]; + $w = []; + + // Disable icanon (so we can fread each keypress) and echo (we'll do echoing here instead) + shell_exec('stty -icanon -echo'); + + // Add highlighted text style + $output->getFormatter()->setStyle('hl', new OutputFormatterStyle('black', 'white')); + + // Read a keypress + while (!feof($inputStream)) { + while ($isStdin && 0 === @stream_select($r, $w, $w, 0, 100)) { + // Give signal handlers a chance to run + $r = [$inputStream]; + } + $c = fread($inputStream, 1); + + // as opposed to fgets(), fread() returns an empty string when the stream content is empty, not false. + if (false === $c || ('' === $ret && '' === $c && null === $question->getDefault())) { + shell_exec('stty '.$sttyMode); + throw new MissingInputException('Aborted.'); + } elseif ("\177" === $c) { // Backspace Character + if (0 === $numMatches && 0 !== $i) { + --$i; + $cursor->moveLeft(s($fullChoice)->slice(-1)->width(false)); + + $fullChoice = self::substr($fullChoice, 0, $i); + } + + if (0 === $i) { + $ofs = -1; + $matches = $autocomplete($ret); + $numMatches = \count($matches); + } else { + $numMatches = 0; + } + + // Pop the last character off the end of our string + $ret = self::substr($ret, 0, $i); + } elseif ("\033" === $c) { + // Did we read an escape sequence? + $c .= fread($inputStream, 2); + + // A = Up Arrow. B = Down Arrow + if (isset($c[2]) && ('A' === $c[2] || 'B' === $c[2])) { + if ('A' === $c[2] && -1 === $ofs) { + $ofs = 0; + } + + if (0 === $numMatches) { + continue; + } + + $ofs += ('A' === $c[2]) ? -1 : 1; + $ofs = ($numMatches + $ofs) % $numMatches; + } + } elseif (\ord($c) < 32) { + if ("\t" === $c || "\n" === $c) { + if ($numMatches > 0 && -1 !== $ofs) { + $ret = (string) $matches[$ofs]; + // Echo out remaining chars for current match + $remainingCharacters = substr($ret, \strlen(trim($this->mostRecentlyEnteredValue($fullChoice)))); + $output->write($remainingCharacters); + $fullChoice .= $remainingCharacters; + $i = (false === $encoding = mb_detect_encoding($fullChoice, null, true)) ? \strlen($fullChoice) : mb_strlen($fullChoice, $encoding); + + $matches = array_filter( + $autocomplete($ret), + fn ($match) => '' === $ret || str_starts_with($match, $ret) + ); + $numMatches = \count($matches); + $ofs = -1; + } + + if ("\n" === $c) { + $output->write($c); + break; + } + + $numMatches = 0; + } + + continue; + } else { + if ("\x80" <= $c) { + $c .= fread($inputStream, ["\xC0" => 1, "\xD0" => 1, "\xE0" => 2, "\xF0" => 3][$c & "\xF0"]); + } + + $output->write($c); + $ret .= $c; + $fullChoice .= $c; + ++$i; + + $tempRet = $ret; + + if ($question instanceof ChoiceQuestion && $question->isMultiselect()) { + $tempRet = $this->mostRecentlyEnteredValue($fullChoice); + } + + $numMatches = 0; + $ofs = 0; + + foreach ($autocomplete($ret) as $value) { + // If typed characters match the beginning chunk of value (e.g. [AcmeDe]moBundle) + if (str_starts_with($value, $tempRet)) { + $matches[$numMatches++] = $value; + } + } + } + + $cursor->clearLineAfter(); + + if ($numMatches > 0 && -1 !== $ofs) { + $cursor->savePosition(); + // Write highlighted text, complete the partially entered response + $charactersEntered = \strlen(trim($this->mostRecentlyEnteredValue($fullChoice))); + $output->write(''.OutputFormatter::escapeTrailingBackslash(substr($matches[$ofs], $charactersEntered)).''); + $cursor->restorePosition(); + } + } + + // Reset stty so it behaves normally again + shell_exec('stty '.$sttyMode); + + return $fullChoice; + } + + private function mostRecentlyEnteredValue(string $entered): string + { + // Determine the most recent value that the user entered + if (!str_contains($entered, ',')) { + return $entered; + } + + $choices = explode(',', $entered); + if ('' !== $lastChoice = trim($choices[\count($choices) - 1])) { + return $lastChoice; + } + + return $entered; + } + + /** + * Gets a hidden response from user. + * + * @param resource $inputStream The handler resource + * @param bool $trimmable Is the answer trimmable + * + * @throws RuntimeException In case the fallback is deactivated and the response cannot be hidden + */ + private function getHiddenResponse(OutputInterface $output, $inputStream, bool $trimmable = true): string + { + if ('\\' === \DIRECTORY_SEPARATOR) { + $exe = __DIR__.'/../Resources/bin/hiddeninput.exe'; + + // handle code running from a phar + if (str_starts_with(__FILE__, 'phar:')) { + $tmpExe = sys_get_temp_dir().'/hiddeninput.exe'; + copy($exe, $tmpExe); + $exe = $tmpExe; + } + + $sExec = shell_exec('"'.$exe.'"'); + $value = $trimmable ? rtrim($sExec) : $sExec; + $output->writeln(''); + + if (isset($tmpExe)) { + unlink($tmpExe); + } + + return $value; + } + + if (self::$stty && Terminal::hasSttyAvailable()) { + $sttyMode = shell_exec('stty -g'); + shell_exec('stty -echo'); + } elseif ($this->isInteractiveInput($inputStream)) { + throw new RuntimeException('Unable to hide the response.'); + } + + $value = fgets($inputStream, 4096); + + if (4095 === \strlen($value)) { + $errOutput = $output instanceof ConsoleOutputInterface ? $output->getErrorOutput() : $output; + $errOutput->warning('The value was possibly truncated by your shell or terminal emulator'); + } + + if (self::$stty && Terminal::hasSttyAvailable()) { + shell_exec('stty '.$sttyMode); + } + + if (false === $value) { + throw new MissingInputException('Aborted.'); + } + if ($trimmable) { + $value = trim($value); + } + $output->writeln(''); + + return $value; + } + + /** + * Validates an attempt. + * + * @param callable $interviewer A callable that will ask for a question and return the result + * + * @throws \Exception In case the max number of attempts has been reached and no valid response has been given + */ + private function validateAttempts(callable $interviewer, OutputInterface $output, Question $question): mixed + { + $error = null; + $attempts = $question->getMaxAttempts(); + + while (null === $attempts || $attempts--) { + if (null !== $error) { + $this->writeError($output, $error); + } + + try { + return $question->getValidator()($interviewer()); + } catch (RuntimeException $e) { + throw $e; + } catch (\Exception $error) { + } + } + + throw $error; + } + + private function isInteractiveInput($inputStream): bool + { + if ('php://stdin' !== (stream_get_meta_data($inputStream)['uri'] ?? null)) { + return false; + } + + if (isset(self::$stdinIsInteractive)) { + return self::$stdinIsInteractive; + } + + return self::$stdinIsInteractive = @stream_isatty(fopen('php://stdin', 'r')); + } + + /** + * Reads one or more lines of input and returns what is read. + * + * @param resource $inputStream The handler resource + * @param Question $question The question being asked + */ + private function readInput($inputStream, Question $question): string|false + { + if (!$question->isMultiline()) { + $cp = $this->setIOCodepage(); + $ret = fgets($inputStream, 4096); + + return $this->resetIOCodepage($cp, $ret); + } + + $multiLineStreamReader = $this->cloneInputStream($inputStream); + if (null === $multiLineStreamReader) { + return false; + } + + $ret = ''; + $cp = $this->setIOCodepage(); + while (false !== ($char = fgetc($multiLineStreamReader))) { + if (\PHP_EOL === "{$ret}{$char}") { + break; + } + $ret .= $char; + } + + return $this->resetIOCodepage($cp, $ret); + } + + private function setIOCodepage(): int + { + if (\function_exists('sapi_windows_cp_set')) { + $cp = sapi_windows_cp_get(); + sapi_windows_cp_set(sapi_windows_cp_get('oem')); + + return $cp; + } + + return 0; + } + + /** + * Sets console I/O to the specified code page and converts the user input. + */ + private function resetIOCodepage(int $cp, string|false $input): string|false + { + if (0 !== $cp) { + sapi_windows_cp_set($cp); + + if (false !== $input && '' !== $input) { + $input = sapi_windows_cp_conv(sapi_windows_cp_get('oem'), $cp, $input); + } + } + + return $input; + } + + /** + * Clones an input stream in order to act on one instance of the same + * stream without affecting the other instance. + * + * @param resource $inputStream The handler resource + * + * @return resource|null The cloned resource, null in case it could not be cloned + */ + private function cloneInputStream($inputStream) + { + $streamMetaData = stream_get_meta_data($inputStream); + $seekable = $streamMetaData['seekable'] ?? false; + $mode = $streamMetaData['mode'] ?? 'rb'; + $uri = $streamMetaData['uri'] ?? null; + + if (null === $uri) { + return null; + } + + $cloneStream = fopen($uri, $mode); + + // For seekable and writable streams, add all the same data to the + // cloned stream and then seek to the same offset. + if (true === $seekable && !\in_array($mode, ['r', 'rb', 'rt'])) { + $offset = ftell($inputStream); + rewind($inputStream); + stream_copy_to_stream($inputStream, $cloneStream); + fseek($inputStream, $offset); + fseek($cloneStream, $offset); + } + + return $cloneStream; + } +} diff --git a/3rdparty/symfony/console/Helper/SymfonyQuestionHelper.php b/3rdparty/symfony/console/Helper/SymfonyQuestionHelper.php new file mode 100644 index 00000000..8ebc8437 --- /dev/null +++ b/3rdparty/symfony/console/Helper/SymfonyQuestionHelper.php @@ -0,0 +1,109 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Component\Console\Helper; + +use Symfony\Component\Console\Formatter\OutputFormatter; +use Symfony\Component\Console\Output\OutputInterface; +use Symfony\Component\Console\Question\ChoiceQuestion; +use Symfony\Component\Console\Question\ConfirmationQuestion; +use Symfony\Component\Console\Question\Question; +use Symfony\Component\Console\Style\SymfonyStyle; + +/** + * Symfony Style Guide compliant question helper. + * + * @author Kevin Bond + */ +class SymfonyQuestionHelper extends QuestionHelper +{ + /** + * @return void + */ + protected function writePrompt(OutputInterface $output, Question $question) + { + $text = OutputFormatter::escapeTrailingBackslash($question->getQuestion()); + $default = $question->getDefault(); + + if ($question->isMultiline()) { + $text .= sprintf(' (press %s to continue)', $this->getEofShortcut()); + } + + switch (true) { + case null === $default: + $text = sprintf(' %s:', $text); + + break; + + case $question instanceof ConfirmationQuestion: + $text = sprintf(' %s (yes/no) [%s]:', $text, $default ? 'yes' : 'no'); + + break; + + case $question instanceof ChoiceQuestion && $question->isMultiselect(): + $choices = $question->getChoices(); + $default = explode(',', $default); + + foreach ($default as $key => $value) { + $default[$key] = $choices[trim($value)]; + } + + $text = sprintf(' %s [%s]:', $text, OutputFormatter::escape(implode(', ', $default))); + + break; + + case $question instanceof ChoiceQuestion: + $choices = $question->getChoices(); + $text = sprintf(' %s [%s]:', $text, OutputFormatter::escape($choices[$default] ?? $default)); + + break; + + default: + $text = sprintf(' %s [%s]:', $text, OutputFormatter::escape($default)); + } + + $output->writeln($text); + + $prompt = ' > '; + + if ($question instanceof ChoiceQuestion) { + $output->writeln($this->formatChoiceQuestionChoices($question, 'comment')); + + $prompt = $question->getPrompt(); + } + + $output->write($prompt); + } + + /** + * @return void + */ + protected function writeError(OutputInterface $output, \Exception $error) + { + if ($output instanceof SymfonyStyle) { + $output->newLine(); + $output->error($error->getMessage()); + + return; + } + + parent::writeError($output, $error); + } + + private function getEofShortcut(): string + { + if ('Windows' === \PHP_OS_FAMILY) { + return 'Ctrl+Z then Enter'; + } + + return 'Ctrl+D'; + } +} diff --git a/3rdparty/symfony/console/Helper/Table.php b/3rdparty/symfony/console/Helper/Table.php new file mode 100644 index 00000000..1f026dc5 --- /dev/null +++ b/3rdparty/symfony/console/Helper/Table.php @@ -0,0 +1,930 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Component\Console\Helper; + +use Symfony\Component\Console\Exception\InvalidArgumentException; +use Symfony\Component\Console\Exception\RuntimeException; +use Symfony\Component\Console\Formatter\OutputFormatter; +use Symfony\Component\Console\Formatter\WrappableOutputFormatterInterface; +use Symfony\Component\Console\Output\ConsoleSectionOutput; +use Symfony\Component\Console\Output\OutputInterface; + +/** + * Provides helpers to display a table. + * + * @author Fabien Potencier + * @author Саша Стаменковић + * @author Abdellatif Ait boudad + * @author Max Grigorian + * @author Dany Maillard + */ +class Table +{ + private const SEPARATOR_TOP = 0; + private const SEPARATOR_TOP_BOTTOM = 1; + private const SEPARATOR_MID = 2; + private const SEPARATOR_BOTTOM = 3; + private const BORDER_OUTSIDE = 0; + private const BORDER_INSIDE = 1; + private const DISPLAY_ORIENTATION_DEFAULT = 'default'; + private const DISPLAY_ORIENTATION_HORIZONTAL = 'horizontal'; + private const DISPLAY_ORIENTATION_VERTICAL = 'vertical'; + + private ?string $headerTitle = null; + private ?string $footerTitle = null; + private array $headers = []; + private array $rows = []; + private array $effectiveColumnWidths = []; + private int $numberOfColumns; + private OutputInterface $output; + private TableStyle $style; + private array $columnStyles = []; + private array $columnWidths = []; + private array $columnMaxWidths = []; + private bool $rendered = false; + private string $displayOrientation = self::DISPLAY_ORIENTATION_DEFAULT; + + private static array $styles; + + public function __construct(OutputInterface $output) + { + $this->output = $output; + + self::$styles ??= self::initStyles(); + + $this->setStyle('default'); + } + + /** + * Sets a style definition. + * + * @return void + */ + public static function setStyleDefinition(string $name, TableStyle $style) + { + self::$styles ??= self::initStyles(); + + self::$styles[$name] = $style; + } + + /** + * Gets a style definition by name. + */ + public static function getStyleDefinition(string $name): TableStyle + { + self::$styles ??= self::initStyles(); + + return self::$styles[$name] ?? throw new InvalidArgumentException(sprintf('Style "%s" is not defined.', $name)); + } + + /** + * Sets table style. + * + * @return $this + */ + public function setStyle(TableStyle|string $name): static + { + $this->style = $this->resolveStyle($name); + + return $this; + } + + /** + * Gets the current table style. + */ + public function getStyle(): TableStyle + { + return $this->style; + } + + /** + * Sets table column style. + * + * @param TableStyle|string $name The style name or a TableStyle instance + * + * @return $this + */ + public function setColumnStyle(int $columnIndex, TableStyle|string $name): static + { + $this->columnStyles[$columnIndex] = $this->resolveStyle($name); + + return $this; + } + + /** + * Gets the current style for a column. + * + * If style was not set, it returns the global table style. + */ + public function getColumnStyle(int $columnIndex): TableStyle + { + return $this->columnStyles[$columnIndex] ?? $this->getStyle(); + } + + /** + * Sets the minimum width of a column. + * + * @return $this + */ + public function setColumnWidth(int $columnIndex, int $width): static + { + $this->columnWidths[$columnIndex] = $width; + + return $this; + } + + /** + * Sets the minimum width of all columns. + * + * @return $this + */ + public function setColumnWidths(array $widths): static + { + $this->columnWidths = []; + foreach ($widths as $index => $width) { + $this->setColumnWidth($index, $width); + } + + return $this; + } + + /** + * Sets the maximum width of a column. + * + * Any cell within this column which contents exceeds the specified width will be wrapped into multiple lines, while + * formatted strings are preserved. + * + * @return $this + */ + public function setColumnMaxWidth(int $columnIndex, int $width): static + { + if (!$this->output->getFormatter() instanceof WrappableOutputFormatterInterface) { + throw new \LogicException(sprintf('Setting a maximum column width is only supported when using a "%s" formatter, got "%s".', WrappableOutputFormatterInterface::class, get_debug_type($this->output->getFormatter()))); + } + + $this->columnMaxWidths[$columnIndex] = $width; + + return $this; + } + + /** + * @return $this + */ + public function setHeaders(array $headers): static + { + $headers = array_values($headers); + if ($headers && !\is_array($headers[0])) { + $headers = [$headers]; + } + + $this->headers = $headers; + + return $this; + } + + /** + * @return $this + */ + public function setRows(array $rows) + { + $this->rows = []; + + return $this->addRows($rows); + } + + /** + * @return $this + */ + public function addRows(array $rows): static + { + foreach ($rows as $row) { + $this->addRow($row); + } + + return $this; + } + + /** + * @return $this + */ + public function addRow(TableSeparator|array $row): static + { + if ($row instanceof TableSeparator) { + $this->rows[] = $row; + + return $this; + } + + $this->rows[] = array_values($row); + + return $this; + } + + /** + * Adds a row to the table, and re-renders the table. + * + * @return $this + */ + public function appendRow(TableSeparator|array $row): static + { + if (!$this->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()); + } + + $this->addRow($row); + $this->render(); + + return $this; + } + + /** + * @return $this + */ + public function setRow(int|string $column, array $row): static + { + $this->rows[$column] = $row; + + return $this; + } + + /** + * @return $this + */ + public function setHeaderTitle(?string $title): static + { + $this->headerTitle = $title; + + return $this; + } + + /** + * @return $this + */ + public function setFooterTitle(?string $title): static + { + $this->footerTitle = $title; + + return $this; + } + + /** + * @return $this + */ + public function setHorizontal(bool $horizontal = true): static + { + $this->displayOrientation = $horizontal ? self::DISPLAY_ORIENTATION_HORIZONTAL : self::DISPLAY_ORIENTATION_DEFAULT; + + return $this; + } + + /** + * @return $this + */ + public function setVertical(bool $vertical = true): static + { + $this->displayOrientation = $vertical ? self::DISPLAY_ORIENTATION_VERTICAL : self::DISPLAY_ORIENTATION_DEFAULT; + + return $this; + } + + /** + * Renders table to output. + * + * Example: + * + * +---------------+-----------------------+------------------+ + * | ISBN | Title | Author | + * +---------------+-----------------------+------------------+ + * | 99921-58-10-7 | Divine Comedy | Dante Alighieri | + * | 9971-5-0210-0 | A Tale of Two Cities | Charles Dickens | + * | 960-425-059-0 | The Lord of the Rings | J. R. R. Tolkien | + * +---------------+-----------------------+------------------+ + * + * @return void + */ + public function render() + { + $divider = new TableSeparator(); + $isCellWithColspan = static fn ($cell) => $cell instanceof TableCell && $cell->getColspan() >= 2; + + $horizontal = self::DISPLAY_ORIENTATION_HORIZONTAL === $this->displayOrientation; + $vertical = self::DISPLAY_ORIENTATION_VERTICAL === $this->displayOrientation; + + $rows = []; + if ($horizontal) { + foreach ($this->headers[0] ?? [] as $i => $header) { + $rows[$i] = [$header]; + foreach ($this->rows as $row) { + if ($row instanceof TableSeparator) { + continue; + } + if (isset($row[$i])) { + $rows[$i][] = $row[$i]; + } elseif ($isCellWithColspan($rows[$i][0])) { + // Noop, there is a "title" + } else { + $rows[$i][] = null; + } + } + } + } 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); + + foreach ($this->rows as $row) { + if ($row instanceof TableSeparator) { + continue; + } + + if ($rows) { + $rows[] = [$divider]; + } + + $containsColspan = false; + foreach ($row as $cell) { + if ($containsColspan = $isCellWithColspan($cell)) { + break; + } + } + + $headers = $this->headers[0] ?? []; + $maxRows = max(\count($headers), \count($row)); + for ($i = 0; $i < $maxRows; ++$i) { + $cell = (string) ($row[$i] ?? ''); + + $eol = str_contains($cell, "\r\n") ? "\r\n" : "\n"; + $parts = explode($eol, $cell); + foreach ($parts as $idx => $part) { + if ($headers && !$containsColspan) { + if (0 === $idx) { + $rows[] = [sprintf( + '%s%s: %s', + str_repeat(' ', $maxHeaderLength - Helper::width(Helper::removeDecoration($formatter, $headers[$i] ?? ''))), + $headers[$i] ?? '', + $part + )]; + } else { + $rows[] = [sprintf( + '%s %s', + str_pad('', $maxHeaderLength, ' ', \STR_PAD_LEFT), + $part + )]; + } + } elseif ('' !== $cell) { + $rows[] = [$part]; + } + } + } + } + } else { + $rows = array_merge($this->headers, [$divider], $this->rows); + } + + $this->calculateNumberOfColumns($rows); + + $rowGroups = $this->buildTableRows($rows); + $this->calculateColumnsWidth($rowGroups); + + $isHeader = !$horizontal; + $isFirstRow = $horizontal; + $hasTitle = (bool) $this->headerTitle; + + foreach ($rowGroups as $rowGroup) { + $isHeaderSeparatorRendered = false; + + foreach ($rowGroup as $row) { + if ($divider === $row) { + $isHeader = false; + $isFirstRow = true; + + continue; + } + + if ($row instanceof TableSeparator) { + $this->renderRowSeparator(); + + continue; + } + + if (!$row) { + continue; + } + + if ($isHeader && !$isHeaderSeparatorRendered) { + $this->renderRowSeparator( + self::SEPARATOR_TOP, + $hasTitle ? $this->headerTitle : null, + $hasTitle ? $this->style->getHeaderTitleFormat() : null + ); + $hasTitle = false; + $isHeaderSeparatorRendered = true; + } + + if ($isFirstRow) { + $this->renderRowSeparator( + $horizontal ? self::SEPARATOR_TOP : self::SEPARATOR_TOP_BOTTOM, + $hasTitle ? $this->headerTitle : null, + $hasTitle ? $this->style->getHeaderTitleFormat() : null + ); + $isFirstRow = false; + $hasTitle = false; + } + + if ($vertical) { + $isHeader = false; + $isFirstRow = false; + } + + if ($horizontal) { + $this->renderRow($row, $this->style->getCellRowFormat(), $this->style->getCellHeaderFormat()); + } else { + $this->renderRow($row, $isHeader ? $this->style->getCellHeaderFormat() : $this->style->getCellRowFormat()); + } + } + } + $this->renderRowSeparator(self::SEPARATOR_BOTTOM, $this->footerTitle, $this->style->getFooterTitleFormat()); + + $this->cleanup(); + $this->rendered = true; + } + + /** + * Renders horizontal header separator. + * + * Example: + * + * +-----+-----------+-------+ + */ + private function renderRowSeparator(int $type = self::SEPARATOR_MID, ?string $title = null, ?string $titleFormat = null): void + { + if (!$count = $this->numberOfColumns) { + return; + } + + $borders = $this->style->getBorderChars(); + if (!$borders[0] && !$borders[2] && !$this->style->getCrossingChar()) { + return; + } + + $crossings = $this->style->getCrossingChars(); + if (self::SEPARATOR_MID === $type) { + [$horizontal, $leftChar, $midChar, $rightChar] = [$borders[2], $crossings[8], $crossings[0], $crossings[4]]; + } elseif (self::SEPARATOR_TOP === $type) { + [$horizontal, $leftChar, $midChar, $rightChar] = [$borders[0], $crossings[1], $crossings[2], $crossings[3]]; + } elseif (self::SEPARATOR_TOP_BOTTOM === $type) { + [$horizontal, $leftChar, $midChar, $rightChar] = [$borders[0], $crossings[9], $crossings[10], $crossings[11]]; + } else { + [$horizontal, $leftChar, $midChar, $rightChar] = [$borders[0], $crossings[7], $crossings[6], $crossings[5]]; + } + + $markup = $leftChar; + for ($column = 0; $column < $count; ++$column) { + $markup .= str_repeat($horizontal, $this->effectiveColumnWidths[$column]); + $markup .= $column === $count - 1 ? $rightChar : $midChar; + } + + if (null !== $title) { + $titleLength = Helper::width(Helper::removeDecoration($formatter = $this->output->getFormatter(), $formattedTitle = sprintf($titleFormat, $title))); + $markupLength = Helper::width($markup); + if ($titleLength > $limit = $markupLength - 4) { + $titleLength = $limit; + $formatLength = Helper::width(Helper::removeDecoration($formatter, sprintf($titleFormat, ''))); + $formattedTitle = sprintf($titleFormat, Helper::substr($title, 0, $limit - $formatLength - 3).'...'); + } + + $titleStart = intdiv($markupLength - $titleLength, 2); + if (false === mb_detect_encoding($markup, null, true)) { + $markup = substr_replace($markup, $formattedTitle, $titleStart, $titleLength); + } else { + $markup = mb_substr($markup, 0, $titleStart).$formattedTitle.mb_substr($markup, $titleStart + $titleLength); + } + } + + $this->output->writeln(sprintf($this->style->getBorderFormat(), $markup)); + } + + /** + * Renders vertical column separator. + */ + private function renderColumnSeparator(int $type = self::BORDER_OUTSIDE): string + { + $borders = $this->style->getBorderChars(); + + return sprintf($this->style->getBorderFormat(), self::BORDER_OUTSIDE === $type ? $borders[1] : $borders[3]); + } + + /** + * Renders table row. + * + * Example: + * + * | 9971-5-0210-0 | A Tale of Two Cities | Charles Dickens | + */ + private function renderRow(array $row, string $cellFormat, ?string $firstCellFormat = null): void + { + $rowContent = $this->renderColumnSeparator(self::BORDER_OUTSIDE); + $columns = $this->getRowColumns($row); + $last = \count($columns) - 1; + foreach ($columns as $i => $column) { + if ($firstCellFormat && 0 === $i) { + $rowContent .= $this->renderCell($row, $column, $firstCellFormat); + } else { + $rowContent .= $this->renderCell($row, $column, $cellFormat); + } + $rowContent .= $this->renderColumnSeparator($last === $i ? self::BORDER_OUTSIDE : self::BORDER_INSIDE); + } + $this->output->writeln($rowContent); + } + + /** + * Renders table cell with padding. + */ + private function renderCell(array $row, int $column, string $cellFormat): string + { + $cell = $row[$column] ?? ''; + $width = $this->effectiveColumnWidths[$column]; + if ($cell instanceof TableCell && $cell->getColspan() > 1) { + // add the width of the following columns(numbers of colspan). + foreach (range($column + 1, $column + $cell->getColspan() - 1) as $nextColumn) { + $width += $this->getColumnSeparatorWidth() + $this->effectiveColumnWidths[$nextColumn]; + } + } + + // str_pad won't work properly with multi-byte strings, we need to fix the padding + if (false !== $encoding = mb_detect_encoding($cell, null, true)) { + $width += \strlen($cell) - mb_strwidth($cell, $encoding); + } + + $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); + 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(); + } + + return sprintf($cellFormat, str_pad($content, $width, $style->getPaddingChar(), $padType)); + } + + /** + * Calculate number of columns for this table. + */ + private function calculateNumberOfColumns(array $rows): void + { + $columns = [0]; + foreach ($rows as $row) { + if ($row instanceof TableSeparator) { + continue; + } + + $columns[] = $this->getNumberOfColumns($row); + } + + $this->numberOfColumns = max($columns); + } + + private function buildTableRows(array $rows): TableRows + { + /** @var WrappableOutputFormatterInterface $formatter */ + $formatter = $this->output->getFormatter(); + $unmergedRows = []; + for ($rowKey = 0; $rowKey < \count($rows); ++$rowKey) { + $rows = $this->fillNextRows($rows, $rowKey); + + // Remove any new line breaks and replace it with a new line + foreach ($rows[$rowKey] as $column => $cell) { + $colspan = $cell instanceof TableCell ? $cell->getColspan() : 1; + + if (isset($this->columnMaxWidths[$column]) && Helper::width(Helper::removeDecoration($formatter, $cell)) > $this->columnMaxWidths[$column]) { + $cell = $formatter->formatAndWrap($cell, $this->columnMaxWidths[$column] * $colspan); + } + if (!str_contains($cell ?? '', "\n")) { + continue; + } + $eol = str_contains($cell ?? '', "\r\n") ? "\r\n" : "\n"; + $escaped = implode($eol, array_map(OutputFormatter::escapeTrailingBackslash(...), explode($eol, $cell))); + $cell = $cell instanceof TableCell ? new TableCell($escaped, ['colspan' => $cell->getColspan()]) : $escaped; + $lines = explode($eol, str_replace($eol, ''.$eol, $cell)); + foreach ($lines as $lineKey => $line) { + if ($colspan > 1) { + $line = new TableCell($line, ['colspan' => $colspan]); + } + if (0 === $lineKey) { + $rows[$rowKey][$column] = $line; + } else { + if (!\array_key_exists($rowKey, $unmergedRows) || !\array_key_exists($lineKey, $unmergedRows[$rowKey])) { + $unmergedRows[$rowKey][$lineKey] = $this->copyRow($rows, $rowKey); + } + $unmergedRows[$rowKey][$lineKey][$column] = $line; + } + } + } + } + + return new TableRows(function () use ($rows, $unmergedRows): \Traversable { + foreach ($rows as $rowKey => $row) { + $rowGroup = [$row instanceof TableSeparator ? $row : $this->fillCells($row)]; + + if (isset($unmergedRows[$rowKey])) { + foreach ($unmergedRows[$rowKey] as $row) { + $rowGroup[] = $row instanceof TableSeparator ? $row : $this->fillCells($row); + } + } + yield $rowGroup; + } + }); + } + + 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. + * + * @throws InvalidArgumentException + */ + private function fillNextRows(array $rows, int $line): array + { + $unmergedRows = []; + foreach ($rows[$line] as $column => $cell) { + if (null !== $cell && !$cell instanceof TableCell && !\is_scalar($cell) && !$cell instanceof \Stringable) { + throw new InvalidArgumentException(sprintf('A cell must be a TableCell, a scalar or an object implementing "__toString()", "%s" given.', get_debug_type($cell))); + } + if ($cell instanceof TableCell && $cell->getRowspan() > 1) { + $nbLines = $cell->getRowspan() - 1; + $lines = [$cell]; + if (str_contains($cell, "\n")) { + $eol = str_contains($cell, "\r\n") ? "\r\n" : "\n"; + $lines = explode($eol, str_replace($eol, ''.$eol.'', $cell)); + $nbLines = \count($lines) > $nbLines ? substr_count($cell, $eol) : $nbLines; + + $rows[$line][$column] = new TableCell($lines[0], ['colspan' => $cell->getColspan(), 'style' => $cell->getStyle()]); + unset($lines[0]); + } + + // create a two dimensional array (rowspan x colspan) + $unmergedRows = array_replace_recursive(array_fill($line + 1, $nbLines, []), $unmergedRows); + foreach ($unmergedRows as $unmergedRowKey => $unmergedRow) { + $value = $lines[$unmergedRowKey - $line] ?? ''; + $unmergedRows[$unmergedRowKey][$column] = new TableCell($value, ['colspan' => $cell->getColspan(), 'style' => $cell->getStyle()]); + if ($nbLines === $unmergedRowKey - $line) { + break; + } + } + } + } + + foreach ($unmergedRows as $unmergedRowKey => $unmergedRow) { + // we need to know if $unmergedRow will be merged or inserted into $rows + if (isset($rows[$unmergedRowKey]) && \is_array($rows[$unmergedRowKey]) && ($this->getNumberOfColumns($rows[$unmergedRowKey]) + $this->getNumberOfColumns($unmergedRows[$unmergedRowKey]) <= $this->numberOfColumns)) { + foreach ($unmergedRow as $cellKey => $cell) { + // insert cell into row at cellKey position + array_splice($rows[$unmergedRowKey], $cellKey, 0, [$cell]); + } + } else { + $row = $this->copyRow($rows, $unmergedRowKey - 1); + foreach ($unmergedRow as $column => $cell) { + if (!empty($cell)) { + $row[$column] = $unmergedRow[$column]; + } + } + array_splice($rows, $unmergedRowKey, 0, [$row]); + } + } + + return $rows; + } + + /** + * fill cells for a row that contains colspan > 1. + */ + private function fillCells(iterable $row): iterable + { + $newRow = []; + + foreach ($row as $column => $cell) { + $newRow[] = $cell; + if ($cell instanceof TableCell && $cell->getColspan() > 1) { + foreach (range($column + 1, $column + $cell->getColspan() - 1) as $position) { + // insert empty value at column position + $newRow[] = ''; + } + } + } + + return $newRow ?: $row; + } + + private function copyRow(array $rows, int $line): array + { + $row = $rows[$line]; + foreach ($row as $cellKey => $cellValue) { + $row[$cellKey] = ''; + if ($cellValue instanceof TableCell) { + $row[$cellKey] = new TableCell('', ['colspan' => $cellValue->getColspan()]); + } + } + + return $row; + } + + /** + * Gets number of columns by row. + */ + private function getNumberOfColumns(array $row): int + { + $columns = \count($row); + foreach ($row as $column) { + $columns += $column instanceof TableCell ? ($column->getColspan() - 1) : 0; + } + + return $columns; + } + + /** + * Gets list of columns for the given row. + */ + private function getRowColumns(array $row): array + { + $columns = range(0, $this->numberOfColumns - 1); + foreach ($row as $cellKey => $cell) { + if ($cell instanceof TableCell && $cell->getColspan() > 1) { + // exclude grouped columns. + $columns = array_diff($columns, range($cellKey + 1, $cellKey + $cell->getColspan() - 1)); + } + } + + return $columns; + } + + /** + * Calculates columns widths. + */ + private function calculateColumnsWidth(iterable $groups): void + { + for ($column = 0; $column < $this->numberOfColumns; ++$column) { + $lengths = []; + foreach ($groups as $group) { + foreach ($group as $row) { + if ($row instanceof TableSeparator) { + continue; + } + + foreach ($row as $i => $cell) { + if ($cell instanceof TableCell) { + $textContent = Helper::removeDecoration($this->output->getFormatter(), $cell); + $textLength = Helper::width($textContent); + if ($textLength > 0) { + $contentColumns = mb_str_split($textContent, ceil($textLength / $cell->getColspan())); + foreach ($contentColumns as $position => $content) { + $row[$i + $position] = $content; + } + } + } + } + + $lengths[] = $this->getCellWidth($row, $column); + } + } + + $this->effectiveColumnWidths[$column] = max($lengths) + Helper::width($this->style->getCellRowContentFormat()) - 2; + } + } + + private function getColumnSeparatorWidth(): int + { + return Helper::width(sprintf($this->style->getBorderFormat(), $this->style->getBorderChars()[3])); + } + + private function getCellWidth(array $row, int $column): int + { + $cellWidth = 0; + + if (isset($row[$column])) { + $cell = $row[$column]; + $cellWidth = Helper::width(Helper::removeDecoration($this->output->getFormatter(), $cell)); + } + + $columnWidth = $this->columnWidths[$column] ?? 0; + $cellWidth = max($cellWidth, $columnWidth); + + return isset($this->columnMaxWidths[$column]) ? min($this->columnMaxWidths[$column], $cellWidth) : $cellWidth; + } + + /** + * Called after rendering to cleanup cache data. + */ + private function cleanup(): void + { + $this->effectiveColumnWidths = []; + unset($this->numberOfColumns); + } + + /** + * @return array + */ + private static function initStyles(): array + { + $borderless = new TableStyle(); + $borderless + ->setHorizontalBorderChars('=') + ->setVerticalBorderChars(' ') + ->setDefaultCrossingChar(' ') + ; + + $compact = new TableStyle(); + $compact + ->setHorizontalBorderChars('') + ->setVerticalBorderChars('') + ->setDefaultCrossingChar('') + ->setCellRowContentFormat('%s ') + ; + + $styleGuide = new TableStyle(); + $styleGuide + ->setHorizontalBorderChars('-') + ->setVerticalBorderChars(' ') + ->setDefaultCrossingChar(' ') + ->setCellHeaderFormat('%s') + ; + + $box = (new TableStyle()) + ->setHorizontalBorderChars('─') + ->setVerticalBorderChars('│') + ->setCrossingChars('┼', '┌', '┬', '┐', '┤', '┘', '┴', '└', '├') + ; + + $boxDouble = (new TableStyle()) + ->setHorizontalBorderChars('═', '─') + ->setVerticalBorderChars('║', '│') + ->setCrossingChars('┼', '╔', '╤', '╗', '╢', '╝', '╧', '╚', '╟', '╠', '╪', '╣') + ; + + return [ + 'default' => new TableStyle(), + 'borderless' => $borderless, + 'compact' => $compact, + 'symfony-style-guide' => $styleGuide, + 'box' => $box, + 'box-double' => $boxDouble, + ]; + } + + private function resolveStyle(TableStyle|string $name): TableStyle + { + if ($name instanceof TableStyle) { + return $name; + } + + return self::$styles[$name] ?? throw new InvalidArgumentException(sprintf('Style "%s" is not defined.', $name)); + } +} diff --git a/3rdparty/symfony/console/Helper/TableCell.php b/3rdparty/symfony/console/Helper/TableCell.php new file mode 100644 index 00000000..394b2bc9 --- /dev/null +++ b/3rdparty/symfony/console/Helper/TableCell.php @@ -0,0 +1,72 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Component\Console\Helper; + +use Symfony\Component\Console\Exception\InvalidArgumentException; + +/** + * @author Abdellatif Ait boudad + */ +class TableCell +{ + private string $value; + private array $options = [ + 'rowspan' => 1, + 'colspan' => 1, + 'style' => null, + ]; + + public function __construct(string $value = '', array $options = []) + { + $this->value = $value; + + // check option names + if ($diff = array_diff(array_keys($options), array_keys($this->options))) { + throw new InvalidArgumentException(sprintf('The TableCell does not support the following options: \'%s\'.', implode('\', \'', $diff))); + } + + if (isset($options['style']) && !$options['style'] instanceof TableCellStyle) { + throw new InvalidArgumentException('The style option must be an instance of "TableCellStyle".'); + } + + $this->options = array_merge($this->options, $options); + } + + /** + * Returns the cell value. + */ + public function __toString(): string + { + return $this->value; + } + + /** + * Gets number of colspan. + */ + public function getColspan(): int + { + return (int) $this->options['colspan']; + } + + /** + * Gets number of rowspan. + */ + public function getRowspan(): int + { + return (int) $this->options['rowspan']; + } + + public function getStyle(): ?TableCellStyle + { + return $this->options['style']; + } +} diff --git a/3rdparty/symfony/console/Helper/TableCellStyle.php b/3rdparty/symfony/console/Helper/TableCellStyle.php new file mode 100644 index 00000000..9419dcb4 --- /dev/null +++ b/3rdparty/symfony/console/Helper/TableCellStyle.php @@ -0,0 +1,84 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Component\Console\Helper; + +use Symfony\Component\Console\Exception\InvalidArgumentException; + +/** + * @author Yewhen Khoptynskyi + */ +class TableCellStyle +{ + public const DEFAULT_ALIGN = 'left'; + + private const TAG_OPTIONS = [ + 'fg', + 'bg', + 'options', + ]; + + private const ALIGN_MAP = [ + 'left' => \STR_PAD_RIGHT, + 'center' => \STR_PAD_BOTH, + 'right' => \STR_PAD_LEFT, + ]; + + private array $options = [ + 'fg' => 'default', + 'bg' => 'default', + 'options' => null, + 'align' => self::DEFAULT_ALIGN, + 'cellFormat' => null, + ]; + + public function __construct(array $options = []) + { + if ($diff = array_diff(array_keys($options), array_keys($this->options))) { + throw new InvalidArgumentException(sprintf('The TableCellStyle does not support the following options: \'%s\'.', implode('\', \'', $diff))); + } + + if (isset($options['align']) && !\array_key_exists($options['align'], self::ALIGN_MAP)) { + throw new InvalidArgumentException(sprintf('Wrong align value. Value must be following: \'%s\'.', implode('\', \'', array_keys(self::ALIGN_MAP)))); + } + + $this->options = array_merge($this->options, $options); + } + + public function getOptions(): array + { + return $this->options; + } + + /** + * Gets options we need for tag for example fg, bg. + * + * @return string[] + */ + public function getTagOptions(): array + { + return array_filter( + $this->getOptions(), + fn ($key) => \in_array($key, self::TAG_OPTIONS) && isset($this->options[$key]), + \ARRAY_FILTER_USE_KEY + ); + } + + public function getPadByAlign(): int + { + return self::ALIGN_MAP[$this->getOptions()['align']]; + } + + public function getCellFormat(): ?string + { + return $this->getOptions()['cellFormat']; + } +} diff --git a/3rdparty/symfony/console/Helper/TableRows.php b/3rdparty/symfony/console/Helper/TableRows.php new file mode 100644 index 00000000..97d07726 --- /dev/null +++ b/3rdparty/symfony/console/Helper/TableRows.php @@ -0,0 +1,30 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Component\Console\Helper; + +/** + * @internal + */ +class TableRows implements \IteratorAggregate +{ + private \Closure $generator; + + public function __construct(\Closure $generator) + { + $this->generator = $generator; + } + + public function getIterator(): \Traversable + { + return ($this->generator)(); + } +} diff --git a/3rdparty/symfony/console/Helper/TableSeparator.php b/3rdparty/symfony/console/Helper/TableSeparator.php new file mode 100644 index 00000000..e541c531 --- /dev/null +++ b/3rdparty/symfony/console/Helper/TableSeparator.php @@ -0,0 +1,25 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Component\Console\Helper; + +/** + * Marks a row as being a separator. + * + * @author Fabien Potencier + */ +class TableSeparator extends TableCell +{ + public function __construct(array $options = []) + { + parent::__construct('', $options); + } +} diff --git a/3rdparty/symfony/console/Helper/TableStyle.php b/3rdparty/symfony/console/Helper/TableStyle.php new file mode 100644 index 00000000..be956c10 --- /dev/null +++ b/3rdparty/symfony/console/Helper/TableStyle.php @@ -0,0 +1,362 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Component\Console\Helper; + +use Symfony\Component\Console\Exception\InvalidArgumentException; +use Symfony\Component\Console\Exception\LogicException; + +/** + * Defines the styles for a Table. + * + * @author Fabien Potencier + * @author Саша Стаменковић + * @author Dany Maillard + */ +class TableStyle +{ + private string $paddingChar = ' '; + private string $horizontalOutsideBorderChar = '-'; + private string $horizontalInsideBorderChar = '-'; + private string $verticalOutsideBorderChar = '|'; + private string $verticalInsideBorderChar = '|'; + private string $crossingChar = '+'; + private string $crossingTopRightChar = '+'; + private string $crossingTopMidChar = '+'; + private string $crossingTopLeftChar = '+'; + private string $crossingMidRightChar = '+'; + private string $crossingBottomRightChar = '+'; + private string $crossingBottomMidChar = '+'; + private string $crossingBottomLeftChar = '+'; + private string $crossingMidLeftChar = '+'; + private string $crossingTopLeftBottomChar = '+'; + private string $crossingTopMidBottomChar = '+'; + private string $crossingTopRightBottomChar = '+'; + private string $headerTitleFormat = ' %s '; + private string $footerTitleFormat = ' %s '; + private string $cellHeaderFormat = '%s'; + private string $cellRowFormat = '%s'; + private string $cellRowContentFormat = ' %s '; + private string $borderFormat = '%s'; + private int $padType = \STR_PAD_RIGHT; + + /** + * Sets padding character, used for cell padding. + * + * @return $this + */ + public function setPaddingChar(string $paddingChar): static + { + if (!$paddingChar) { + throw new LogicException('The padding char must not be empty.'); + } + + $this->paddingChar = $paddingChar; + + return $this; + } + + /** + * Gets padding character, used for cell padding. + */ + public function getPaddingChar(): string + { + return $this->paddingChar; + } + + /** + * Sets horizontal border characters. + * + * + * ╔═══════════════╤══════════════════════════╤══════════════════╗ + * 1 ISBN 2 Title │ Author ║ + * ╠═══════════════╪══════════════════════════╪══════════════════╣ + * ║ 99921-58-10-7 │ Divine Comedy │ Dante Alighieri ║ + * ║ 9971-5-0210-0 │ A Tale of Two Cities │ Charles Dickens ║ + * ║ 960-425-059-0 │ The Lord of the Rings │ J. R. R. Tolkien ║ + * ║ 80-902734-1-6 │ And Then There Were None │ Agatha Christie ║ + * ╚═══════════════╧══════════════════════════╧══════════════════╝ + * + * + * @return $this + */ + public function setHorizontalBorderChars(string $outside, ?string $inside = null): static + { + $this->horizontalOutsideBorderChar = $outside; + $this->horizontalInsideBorderChar = $inside ?? $outside; + + return $this; + } + + /** + * Sets vertical border characters. + * + * + * ╔═══════════════╤══════════════════════════╤══════════════════╗ + * ║ ISBN │ Title │ Author ║ + * ╠═══════1═══════╪══════════════════════════╪══════════════════╣ + * ║ 99921-58-10-7 │ Divine Comedy │ Dante Alighieri ║ + * ║ 9971-5-0210-0 │ A Tale of Two Cities │ Charles Dickens ║ + * ╟───────2───────┼──────────────────────────┼──────────────────╢ + * ║ 960-425-059-0 │ The Lord of the Rings │ J. R. R. Tolkien ║ + * ║ 80-902734-1-6 │ And Then There Were None │ Agatha Christie ║ + * ╚═══════════════╧══════════════════════════╧══════════════════╝ + * + * + * @return $this + */ + public function setVerticalBorderChars(string $outside, ?string $inside = null): static + { + $this->verticalOutsideBorderChar = $outside; + $this->verticalInsideBorderChar = $inside ?? $outside; + + return $this; + } + + /** + * Gets border characters. + * + * @internal + */ + public function getBorderChars(): array + { + return [ + $this->horizontalOutsideBorderChar, + $this->verticalOutsideBorderChar, + $this->horizontalInsideBorderChar, + $this->verticalInsideBorderChar, + ]; + } + + /** + * Sets crossing characters. + * + * Example: + * + * 1═══════════════2══════════════════════════2══════════════════3 + * ║ ISBN │ Title │ Author ║ + * 8'══════════════0'═════════════════════════0'═════════════════4' + * ║ 99921-58-10-7 │ Divine Comedy │ Dante Alighieri ║ + * ║ 9971-5-0210-0 │ A Tale of Two Cities │ Charles Dickens ║ + * 8───────────────0──────────────────────────0──────────────────4 + * ║ 960-425-059-0 │ The Lord of the Rings │ J. R. R. Tolkien ║ + * ║ 80-902734-1-6 │ And Then There Were None │ Agatha Christie ║ + * 7═══════════════6══════════════════════════6══════════════════5 + * + * + * @param string $cross Crossing char (see #0 of example) + * @param string $topLeft Top left char (see #1 of example) + * @param string $topMid Top mid char (see #2 of example) + * @param string $topRight Top right char (see #3 of example) + * @param string $midRight Mid right char (see #4 of example) + * @param string $bottomRight Bottom right char (see #5 of example) + * @param string $bottomMid Bottom mid char (see #6 of example) + * @param string $bottomLeft Bottom left char (see #7 of example) + * @param string $midLeft Mid left char (see #8 of example) + * @param string|null $topLeftBottom Top left bottom char (see #8' of example), equals to $midLeft if null + * @param string|null $topMidBottom Top mid bottom char (see #0' of example), equals to $cross if null + * @param string|null $topRightBottom Top right bottom char (see #4' of example), equals to $midRight if null + * + * @return $this + */ + public function setCrossingChars(string $cross, string $topLeft, string $topMid, string $topRight, string $midRight, string $bottomRight, string $bottomMid, string $bottomLeft, string $midLeft, ?string $topLeftBottom = null, ?string $topMidBottom = null, ?string $topRightBottom = null): static + { + $this->crossingChar = $cross; + $this->crossingTopLeftChar = $topLeft; + $this->crossingTopMidChar = $topMid; + $this->crossingTopRightChar = $topRight; + $this->crossingMidRightChar = $midRight; + $this->crossingBottomRightChar = $bottomRight; + $this->crossingBottomMidChar = $bottomMid; + $this->crossingBottomLeftChar = $bottomLeft; + $this->crossingMidLeftChar = $midLeft; + $this->crossingTopLeftBottomChar = $topLeftBottom ?? $midLeft; + $this->crossingTopMidBottomChar = $topMidBottom ?? $cross; + $this->crossingTopRightBottomChar = $topRightBottom ?? $midRight; + + return $this; + } + + /** + * Sets default crossing character used for each cross. + * + * @see {@link setCrossingChars()} for setting each crossing individually. + */ + public function setDefaultCrossingChar(string $char): self + { + return $this->setCrossingChars($char, $char, $char, $char, $char, $char, $char, $char, $char); + } + + /** + * Gets crossing character. + */ + public function getCrossingChar(): string + { + return $this->crossingChar; + } + + /** + * Gets crossing characters. + * + * @internal + */ + public function getCrossingChars(): array + { + return [ + $this->crossingChar, + $this->crossingTopLeftChar, + $this->crossingTopMidChar, + $this->crossingTopRightChar, + $this->crossingMidRightChar, + $this->crossingBottomRightChar, + $this->crossingBottomMidChar, + $this->crossingBottomLeftChar, + $this->crossingMidLeftChar, + $this->crossingTopLeftBottomChar, + $this->crossingTopMidBottomChar, + $this->crossingTopRightBottomChar, + ]; + } + + /** + * Sets header cell format. + * + * @return $this + */ + public function setCellHeaderFormat(string $cellHeaderFormat): static + { + $this->cellHeaderFormat = $cellHeaderFormat; + + return $this; + } + + /** + * Gets header cell format. + */ + public function getCellHeaderFormat(): string + { + return $this->cellHeaderFormat; + } + + /** + * Sets row cell format. + * + * @return $this + */ + public function setCellRowFormat(string $cellRowFormat): static + { + $this->cellRowFormat = $cellRowFormat; + + return $this; + } + + /** + * Gets row cell format. + */ + public function getCellRowFormat(): string + { + return $this->cellRowFormat; + } + + /** + * Sets row cell content format. + * + * @return $this + */ + public function setCellRowContentFormat(string $cellRowContentFormat): static + { + $this->cellRowContentFormat = $cellRowContentFormat; + + return $this; + } + + /** + * Gets row cell content format. + */ + public function getCellRowContentFormat(): string + { + return $this->cellRowContentFormat; + } + + /** + * Sets table border format. + * + * @return $this + */ + public function setBorderFormat(string $borderFormat): static + { + $this->borderFormat = $borderFormat; + + return $this; + } + + /** + * Gets table border format. + */ + public function getBorderFormat(): string + { + return $this->borderFormat; + } + + /** + * Sets cell padding type. + * + * @return $this + */ + public function setPadType(int $padType): static + { + if (!\in_array($padType, [\STR_PAD_LEFT, \STR_PAD_RIGHT, \STR_PAD_BOTH], true)) { + throw new InvalidArgumentException('Invalid padding type. Expected one of (STR_PAD_LEFT, STR_PAD_RIGHT, STR_PAD_BOTH).'); + } + + $this->padType = $padType; + + return $this; + } + + /** + * Gets cell padding type. + */ + public function getPadType(): int + { + return $this->padType; + } + + public function getHeaderTitleFormat(): string + { + return $this->headerTitleFormat; + } + + /** + * @return $this + */ + public function setHeaderTitleFormat(string $format): static + { + $this->headerTitleFormat = $format; + + return $this; + } + + public function getFooterTitleFormat(): string + { + return $this->footerTitleFormat; + } + + /** + * @return $this + */ + public function setFooterTitleFormat(string $format): static + { + $this->footerTitleFormat = $format; + + return $this; + } +} diff --git a/3rdparty/symfony/console/Input/ArgvInput.php b/3rdparty/symfony/console/Input/ArgvInput.php new file mode 100644 index 00000000..ab9f28c5 --- /dev/null +++ b/3rdparty/symfony/console/Input/ArgvInput.php @@ -0,0 +1,370 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Component\Console\Input; + +use Symfony\Component\Console\Exception\RuntimeException; + +/** + * ArgvInput represents an input coming from the CLI arguments. + * + * Usage: + * + * $input = new ArgvInput(); + * + * By default, the `$_SERVER['argv']` array is used for the input values. + * + * This can be overridden by explicitly passing the input values in the constructor: + * + * $input = new ArgvInput($_SERVER['argv']); + * + * If you pass it yourself, don't forget that the first element of the array + * is the name of the running application. + * + * When passing an argument to the constructor, be sure that it respects + * the same rules as the argv one. It's almost always better to use the + * `StringInput` when you want to provide your own input. + * + * @author Fabien Potencier + * + * @see http://www.gnu.org/software/libc/manual/html_node/Argument-Syntax.html + * @see http://www.opengroup.org/onlinepubs/009695399/basedefs/xbd_chap12.html#tag_12_02 + */ +class ArgvInput extends Input +{ + private array $tokens; + private array $parsed; + + public function __construct(?array $argv = null, ?InputDefinition $definition = null) + { + $argv ??= $_SERVER['argv'] ?? []; + + // strip the application name + array_shift($argv); + + $this->tokens = $argv; + + parent::__construct($definition); + } + + /** + * @return void + */ + protected function setTokens(array $tokens) + { + $this->tokens = $tokens; + } + + /** + * @return void + */ + protected function parse() + { + $parseOptions = true; + $this->parsed = $this->tokens; + while (null !== $token = array_shift($this->parsed)) { + $parseOptions = $this->parseToken($token, $parseOptions); + } + } + + protected function parseToken(string $token, bool $parseOptions): bool + { + if ($parseOptions && '' == $token) { + $this->parseArgument($token); + } elseif ($parseOptions && '--' == $token) { + return false; + } elseif ($parseOptions && str_starts_with($token, '--')) { + $this->parseLongOption($token); + } elseif ($parseOptions && '-' === $token[0] && '-' !== $token) { + $this->parseShortOption($token); + } else { + $this->parseArgument($token); + } + + return $parseOptions; + } + + /** + * Parses a short option. + */ + private function parseShortOption(string $token): void + { + $name = substr($token, 1); + + if (\strlen($name) > 1) { + if ($this->definition->hasShortcut($name[0]) && $this->definition->getOptionForShortcut($name[0])->acceptValue()) { + // an option with a value (with no space) + $this->addShortOption($name[0], substr($name, 1)); + } else { + $this->parseShortOptionSet($name); + } + } else { + $this->addShortOption($name, null); + } + } + + /** + * Parses a short option set. + * + * @throws RuntimeException When option given doesn't exist + */ + private function parseShortOptionSet(string $name): void + { + $len = \strlen($name); + for ($i = 0; $i < $len; ++$i) { + if (!$this->definition->hasShortcut($name[$i])) { + $encoding = mb_detect_encoding($name, null, true); + throw new RuntimeException(sprintf('The "-%s" option does not exist.', false === $encoding ? $name[$i] : mb_substr($name, $i, 1, $encoding))); + } + + $option = $this->definition->getOptionForShortcut($name[$i]); + if ($option->acceptValue()) { + $this->addLongOption($option->getName(), $i === $len - 1 ? null : substr($name, $i + 1)); + + break; + } else { + $this->addLongOption($option->getName(), null); + } + } + } + + /** + * Parses a long option. + */ + private function parseLongOption(string $token): void + { + $name = substr($token, 2); + + if (false !== $pos = strpos($name, '=')) { + if ('' === $value = substr($name, $pos + 1)) { + array_unshift($this->parsed, $value); + } + $this->addLongOption(substr($name, 0, $pos), $value); + } else { + $this->addLongOption($name, null); + } + } + + /** + * Parses an argument. + * + * @throws RuntimeException When too many arguments are given + */ + private function parseArgument(string $token): void + { + $c = \count($this->arguments); + + // if input is expecting another argument, add it + if ($this->definition->hasArgument($c)) { + $arg = $this->definition->getArgument($c); + $this->arguments[$arg->getName()] = $arg->isArray() ? [$token] : $token; + + // if last argument isArray(), append token to last argument + } elseif ($this->definition->hasArgument($c - 1) && $this->definition->getArgument($c - 1)->isArray()) { + $arg = $this->definition->getArgument($c - 1); + $this->arguments[$arg->getName()][] = $token; + + // unexpected argument + } else { + $all = $this->definition->getArguments(); + $symfonyCommandName = null; + if (($inputArgument = $all[$key = array_key_first($all)] ?? null) && 'command' === $inputArgument->getName()) { + $symfonyCommandName = $this->arguments['command'] ?? null; + unset($all[$key]); + } + + if (\count($all)) { + if ($symfonyCommandName) { + $message = sprintf('Too many arguments to "%s" command, expected arguments "%s".', $symfonyCommandName, implode('" "', array_keys($all))); + } else { + $message = sprintf('Too many arguments, expected arguments "%s".', implode('" "', array_keys($all))); + } + } elseif ($symfonyCommandName) { + $message = sprintf('No arguments expected for "%s" command, got "%s".', $symfonyCommandName, $token); + } else { + $message = sprintf('No arguments expected, got "%s".', $token); + } + + throw new RuntimeException($message); + } + } + + /** + * Adds a short option value. + * + * @throws RuntimeException When option given doesn't exist + */ + private function addShortOption(string $shortcut, mixed $value): void + { + if (!$this->definition->hasShortcut($shortcut)) { + throw new RuntimeException(sprintf('The "-%s" option does not exist.', $shortcut)); + } + + $this->addLongOption($this->definition->getOptionForShortcut($shortcut)->getName(), $value); + } + + /** + * Adds a long option value. + * + * @throws RuntimeException When option given doesn't exist + */ + private function addLongOption(string $name, mixed $value): void + { + if (!$this->definition->hasOption($name)) { + if (!$this->definition->hasNegation($name)) { + throw new RuntimeException(sprintf('The "--%s" option does not exist.', $name)); + } + + $optionName = $this->definition->negationToName($name); + if (null !== $value) { + throw new RuntimeException(sprintf('The "--%s" option does not accept a value.', $name)); + } + $this->options[$optionName] = false; + + return; + } + + $option = $this->definition->getOption($name); + + if (null !== $value && !$option->acceptValue()) { + throw new RuntimeException(sprintf('The "--%s" option does not accept a value.', $name)); + } + + if (\in_array($value, ['', null], true) && $option->acceptValue() && \count($this->parsed)) { + // if option accepts an optional or mandatory argument + // let's see if there is one provided + $next = array_shift($this->parsed); + if ((isset($next[0]) && '-' !== $next[0]) || \in_array($next, ['', null], true)) { + $value = $next; + } else { + array_unshift($this->parsed, $next); + } + } + + if (null === $value) { + if ($option->isValueRequired()) { + throw new RuntimeException(sprintf('The "--%s" option requires a value.', $name)); + } + + if (!$option->isArray() && !$option->isValueOptional()) { + $value = true; + } + } + + if ($option->isArray()) { + $this->options[$name][] = $value; + } else { + $this->options[$name] = $value; + } + } + + public function getFirstArgument(): ?string + { + $isOption = false; + foreach ($this->tokens as $i => $token) { + if ($token && '-' === $token[0]) { + if (str_contains($token, '=') || !isset($this->tokens[$i + 1])) { + continue; + } + + // If it's a long option, consider that everything after "--" is the option name. + // Otherwise, use the last char (if it's a short option set, only the last one can take a value with space separator) + $name = '-' === $token[1] ? substr($token, 2) : substr($token, -1); + if (!isset($this->options[$name]) && !$this->definition->hasShortcut($name)) { + // noop + } elseif ((isset($this->options[$name]) || isset($this->options[$name = $this->definition->shortcutToName($name)])) && $this->tokens[$i + 1] === $this->options[$name]) { + $isOption = true; + } + + continue; + } + + if ($isOption) { + $isOption = false; + continue; + } + + return $token; + } + + return null; + } + + public function hasParameterOption(string|array $values, bool $onlyParams = false): bool + { + $values = (array) $values; + + foreach ($this->tokens as $token) { + if ($onlyParams && '--' === $token) { + return false; + } + foreach ($values as $value) { + // Options with values: + // For long options, test for '--option=' at beginning + // For short options, test for '-o' at beginning + $leading = str_starts_with($value, '--') ? $value.'=' : $value; + if ($token === $value || '' !== $leading && str_starts_with($token, $leading)) { + return true; + } + } + } + + return false; + } + + public function getParameterOption(string|array $values, string|bool|int|float|array|null $default = false, bool $onlyParams = false): mixed + { + $values = (array) $values; + $tokens = $this->tokens; + + while (0 < \count($tokens)) { + $token = array_shift($tokens); + if ($onlyParams && '--' === $token) { + return $default; + } + + foreach ($values as $value) { + if ($token === $value) { + return array_shift($tokens); + } + // Options with values: + // For long options, test for '--option=' at beginning + // For short options, test for '-o' at beginning + $leading = str_starts_with($value, '--') ? $value.'=' : $value; + if ('' !== $leading && str_starts_with($token, $leading)) { + return substr($token, \strlen($leading)); + } + } + } + + return $default; + } + + /** + * Returns a stringified representation of the args passed to the command. + */ + public function __toString(): string + { + $tokens = array_map(function ($token) { + if (preg_match('{^(-[^=]+=)(.+)}', $token, $match)) { + return $match[1].$this->escapeToken($match[2]); + } + + if ($token && '-' !== $token[0]) { + return $this->escapeToken($token); + } + + return $token; + }, $this->tokens); + + return implode(' ', $tokens); + } +} diff --git a/3rdparty/symfony/console/Input/ArrayInput.php b/3rdparty/symfony/console/Input/ArrayInput.php new file mode 100644 index 00000000..c1bc914c --- /dev/null +++ b/3rdparty/symfony/console/Input/ArrayInput.php @@ -0,0 +1,196 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Component\Console\Input; + +use Symfony\Component\Console\Exception\InvalidArgumentException; +use Symfony\Component\Console\Exception\InvalidOptionException; + +/** + * ArrayInput represents an input provided as an array. + * + * Usage: + * + * $input = new ArrayInput(['command' => 'foo:bar', 'foo' => 'bar', '--bar' => 'foobar']); + * + * @author Fabien Potencier + */ +class ArrayInput extends Input +{ + private array $parameters; + + public function __construct(array $parameters, ?InputDefinition $definition = null) + { + $this->parameters = $parameters; + + parent::__construct($definition); + } + + public function getFirstArgument(): ?string + { + foreach ($this->parameters as $param => $value) { + if ($param && \is_string($param) && '-' === $param[0]) { + continue; + } + + return $value; + } + + return null; + } + + public function hasParameterOption(string|array $values, bool $onlyParams = false): bool + { + $values = (array) $values; + + foreach ($this->parameters as $k => $v) { + if (!\is_int($k)) { + $v = $k; + } + + if ($onlyParams && '--' === $v) { + return false; + } + + if (\in_array($v, $values)) { + return true; + } + } + + return false; + } + + public function getParameterOption(string|array $values, string|bool|int|float|array|null $default = false, bool $onlyParams = false): mixed + { + $values = (array) $values; + + foreach ($this->parameters as $k => $v) { + if ($onlyParams && ('--' === $k || (\is_int($k) && '--' === $v))) { + return $default; + } + + if (\is_int($k)) { + if (\in_array($v, $values)) { + return true; + } + } elseif (\in_array($k, $values)) { + return $v; + } + } + + return $default; + } + + /** + * Returns a stringified representation of the args passed to the command. + */ + public function __toString(): string + { + $params = []; + foreach ($this->parameters as $param => $val) { + if ($param && \is_string($param) && '-' === $param[0]) { + $glue = ('-' === $param[1]) ? '=' : ' '; + if (\is_array($val)) { + foreach ($val as $v) { + $params[] = $param.('' != $v ? $glue.$this->escapeToken($v) : ''); + } + } else { + $params[] = $param.('' != $val ? $glue.$this->escapeToken($val) : ''); + } + } else { + $params[] = \is_array($val) ? implode(' ', array_map($this->escapeToken(...), $val)) : $this->escapeToken($val); + } + } + + return implode(' ', $params); + } + + /** + * @return void + */ + protected function parse() + { + foreach ($this->parameters as $key => $value) { + if ('--' === $key) { + return; + } + if (str_starts_with($key, '--')) { + $this->addLongOption(substr($key, 2), $value); + } elseif (str_starts_with($key, '-')) { + $this->addShortOption(substr($key, 1), $value); + } else { + $this->addArgument($key, $value); + } + } + } + + /** + * Adds a short option value. + * + * @throws InvalidOptionException When option given doesn't exist + */ + private function addShortOption(string $shortcut, mixed $value): void + { + if (!$this->definition->hasShortcut($shortcut)) { + throw new InvalidOptionException(sprintf('The "-%s" option does not exist.', $shortcut)); + } + + $this->addLongOption($this->definition->getOptionForShortcut($shortcut)->getName(), $value); + } + + /** + * Adds a long option value. + * + * @throws InvalidOptionException When option given doesn't exist + * @throws InvalidOptionException When a required value is missing + */ + private function addLongOption(string $name, mixed $value): void + { + if (!$this->definition->hasOption($name)) { + if (!$this->definition->hasNegation($name)) { + throw new InvalidOptionException(sprintf('The "--%s" option does not exist.', $name)); + } + + $optionName = $this->definition->negationToName($name); + $this->options[$optionName] = false; + + return; + } + + $option = $this->definition->getOption($name); + + if (null === $value) { + if ($option->isValueRequired()) { + throw new InvalidOptionException(sprintf('The "--%s" option requires a value.', $name)); + } + + if (!$option->isValueOptional()) { + $value = true; + } + } + + $this->options[$name] = $value; + } + + /** + * Adds an argument value. + * + * @throws InvalidArgumentException When argument given doesn't exist + */ + private function addArgument(string|int $name, mixed $value): void + { + if (!$this->definition->hasArgument($name)) { + throw new InvalidArgumentException(sprintf('The "%s" argument does not exist.', $name)); + } + + $this->arguments[$name] = $value; + } +} diff --git a/3rdparty/symfony/console/Input/Input.php b/3rdparty/symfony/console/Input/Input.php new file mode 100644 index 00000000..1c21573b --- /dev/null +++ b/3rdparty/symfony/console/Input/Input.php @@ -0,0 +1,193 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Component\Console\Input; + +use Symfony\Component\Console\Exception\InvalidArgumentException; +use Symfony\Component\Console\Exception\RuntimeException; + +/** + * Input is the base class for all concrete Input classes. + * + * Three concrete classes are provided by default: + * + * * `ArgvInput`: The input comes from the CLI arguments (argv) + * * `StringInput`: The input is provided as a string + * * `ArrayInput`: The input is provided as an array + * + * @author Fabien Potencier + */ +abstract class Input implements InputInterface, StreamableInputInterface +{ + protected $definition; + /** @var resource */ + protected $stream; + protected $options = []; + protected $arguments = []; + protected $interactive = true; + + public function __construct(?InputDefinition $definition = null) + { + if (null === $definition) { + $this->definition = new InputDefinition(); + } else { + $this->bind($definition); + $this->validate(); + } + } + + /** + * @return void + */ + public function bind(InputDefinition $definition) + { + $this->arguments = []; + $this->options = []; + $this->definition = $definition; + + $this->parse(); + } + + /** + * Processes command line arguments. + * + * @return void + */ + abstract protected function parse(); + + /** + * @return void + */ + public function validate() + { + $definition = $this->definition; + $givenArguments = $this->arguments; + + $missingArguments = array_filter(array_keys($definition->getArguments()), fn ($argument) => !\array_key_exists($argument, $givenArguments) && $definition->getArgument($argument)->isRequired()); + + if (\count($missingArguments) > 0) { + throw new RuntimeException(sprintf('Not enough arguments (missing: "%s").', implode(', ', $missingArguments))); + } + } + + public function isInteractive(): bool + { + return $this->interactive; + } + + /** + * @return void + */ + public function setInteractive(bool $interactive) + { + $this->interactive = $interactive; + } + + public function getArguments(): array + { + return array_merge($this->definition->getArgumentDefaults(), $this->arguments); + } + + public function getArgument(string $name): mixed + { + if (!$this->definition->hasArgument($name)) { + throw new InvalidArgumentException(sprintf('The "%s" argument does not exist.', $name)); + } + + return $this->arguments[$name] ?? $this->definition->getArgument($name)->getDefault(); + } + + /** + * @return void + */ + public function setArgument(string $name, mixed $value) + { + if (!$this->definition->hasArgument($name)) { + throw new InvalidArgumentException(sprintf('The "%s" argument does not exist.', $name)); + } + + $this->arguments[$name] = $value; + } + + public function hasArgument(string $name): bool + { + return $this->definition->hasArgument($name); + } + + public function getOptions(): array + { + return array_merge($this->definition->getOptionDefaults(), $this->options); + } + + public function getOption(string $name): mixed + { + if ($this->definition->hasNegation($name)) { + if (null === $value = $this->getOption($this->definition->negationToName($name))) { + return $value; + } + + return !$value; + } + + if (!$this->definition->hasOption($name)) { + throw new InvalidArgumentException(sprintf('The "%s" option does not exist.', $name)); + } + + return \array_key_exists($name, $this->options) ? $this->options[$name] : $this->definition->getOption($name)->getDefault(); + } + + /** + * @return void + */ + public function setOption(string $name, mixed $value) + { + if ($this->definition->hasNegation($name)) { + $this->options[$this->definition->negationToName($name)] = !$value; + + return; + } elseif (!$this->definition->hasOption($name)) { + throw new InvalidArgumentException(sprintf('The "%s" option does not exist.', $name)); + } + + $this->options[$name] = $value; + } + + public function hasOption(string $name): bool + { + return $this->definition->hasOption($name) || $this->definition->hasNegation($name); + } + + /** + * Escapes a token through escapeshellarg if it contains unsafe chars. + */ + public function escapeToken(string $token): string + { + return preg_match('{^[\w-]+$}', $token) ? $token : escapeshellarg($token); + } + + /** + * @param resource $stream + * + * @return void + */ + public function setStream($stream) + { + $this->stream = $stream; + } + + /** + * @return resource + */ + public function getStream() + { + return $this->stream; + } +} diff --git a/3rdparty/symfony/console/Input/InputArgument.php b/3rdparty/symfony/console/Input/InputArgument.php new file mode 100644 index 00000000..4ef79feb --- /dev/null +++ b/3rdparty/symfony/console/Input/InputArgument.php @@ -0,0 +1,154 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Component\Console\Input; + +use Symfony\Component\Console\Command\Command; +use Symfony\Component\Console\Completion\CompletionInput; +use Symfony\Component\Console\Completion\CompletionSuggestions; +use Symfony\Component\Console\Completion\Suggestion; +use Symfony\Component\Console\Exception\InvalidArgumentException; +use Symfony\Component\Console\Exception\LogicException; + +/** + * Represents a command line argument. + * + * @author Fabien Potencier + */ +class InputArgument +{ + public const REQUIRED = 1; + public const OPTIONAL = 2; + public const IS_ARRAY = 4; + + private string $name; + private int $mode; + private string|int|bool|array|null|float $default; + private array|\Closure $suggestedValues; + private string $description; + + /** + * @param string $name The argument name + * @param int|null $mode The argument mode: a bit mask of self::REQUIRED, self::OPTIONAL and self::IS_ARRAY + * @param string $description A description text + * @param string|bool|int|float|array|null $default The default value (for self::OPTIONAL mode only) + * @param array|\Closure(CompletionInput,CompletionSuggestions):list $suggestedValues The values used for input completion + * + * @throws InvalidArgumentException When argument mode is not valid + */ + public function __construct(string $name, ?int $mode = null, string $description = '', string|bool|int|float|array|null $default = null, \Closure|array $suggestedValues = []) + { + if (null === $mode) { + $mode = self::OPTIONAL; + } elseif ($mode > 7 || $mode < 1) { + throw new InvalidArgumentException(sprintf('Argument mode "%s" is not valid.', $mode)); + } + + $this->name = $name; + $this->mode = $mode; + $this->description = $description; + $this->suggestedValues = $suggestedValues; + + $this->setDefault($default); + } + + /** + * Returns the argument name. + */ + public function getName(): string + { + return $this->name; + } + + /** + * Returns true if the argument is required. + * + * @return bool true if parameter mode is self::REQUIRED, false otherwise + */ + public function isRequired(): bool + { + return self::REQUIRED === (self::REQUIRED & $this->mode); + } + + /** + * Returns true if the argument can take multiple values. + * + * @return bool true if mode is self::IS_ARRAY, false otherwise + */ + public function isArray(): bool + { + return self::IS_ARRAY === (self::IS_ARRAY & $this->mode); + } + + /** + * Sets the default value. + * + * @return void + * + * @throws LogicException When incorrect default value is given + */ + public function setDefault(string|bool|int|float|array|null $default = null) + { + if (1 > \func_num_args()) { + trigger_deprecation('symfony/console', '6.2', 'Calling "%s()" without any arguments is deprecated, pass null explicitly instead.', __METHOD__); + } + if ($this->isRequired() && null !== $default) { + throw new LogicException('Cannot set a default value except for InputArgument::OPTIONAL mode.'); + } + + if ($this->isArray()) { + if (null === $default) { + $default = []; + } elseif (!\is_array($default)) { + throw new LogicException('A default value for an array argument must be an array.'); + } + } + + $this->default = $default; + } + + /** + * Returns the default value. + */ + public function getDefault(): string|bool|int|float|array|null + { + return $this->default; + } + + public function hasCompletion(): bool + { + return [] !== $this->suggestedValues; + } + + /** + * Adds suggestions to $suggestions for the current completion input. + * + * @see Command::complete() + */ + public function complete(CompletionInput $input, CompletionSuggestions $suggestions): void + { + $values = $this->suggestedValues; + if ($values instanceof \Closure && !\is_array($values = $values($input))) { + throw new LogicException(sprintf('Closure for argument "%s" must return an array. Got "%s".', $this->name, get_debug_type($values))); + } + if ($values) { + $suggestions->suggestValues($values); + } + } + + /** + * Returns the description text. + */ + public function getDescription(): string + { + return $this->description; + } +} diff --git a/3rdparty/symfony/console/Input/InputAwareInterface.php b/3rdparty/symfony/console/Input/InputAwareInterface.php new file mode 100644 index 00000000..0ad27b45 --- /dev/null +++ b/3rdparty/symfony/console/Input/InputAwareInterface.php @@ -0,0 +1,28 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Component\Console\Input; + +/** + * InputAwareInterface should be implemented by classes that depends on the + * Console Input. + * + * @author Wouter J + */ +interface InputAwareInterface +{ + /** + * Sets the Console Input. + * + * @return void + */ + public function setInput(InputInterface $input); +} diff --git a/3rdparty/symfony/console/Input/InputDefinition.php b/3rdparty/symfony/console/Input/InputDefinition.php new file mode 100644 index 00000000..b7162d77 --- /dev/null +++ b/3rdparty/symfony/console/Input/InputDefinition.php @@ -0,0 +1,416 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Component\Console\Input; + +use Symfony\Component\Console\Exception\InvalidArgumentException; +use Symfony\Component\Console\Exception\LogicException; + +/** + * A InputDefinition represents a set of valid command line arguments and options. + * + * Usage: + * + * $definition = new InputDefinition([ + * new InputArgument('name', InputArgument::REQUIRED), + * new InputOption('foo', 'f', InputOption::VALUE_REQUIRED), + * ]); + * + * @author Fabien Potencier + */ +class InputDefinition +{ + private array $arguments = []; + private int $requiredCount = 0; + private ?InputArgument $lastArrayArgument = null; + private ?InputArgument $lastOptionalArgument = null; + private array $options = []; + private array $negations = []; + private array $shortcuts = []; + + /** + * @param array $definition An array of InputArgument and InputOption instance + */ + public function __construct(array $definition = []) + { + $this->setDefinition($definition); + } + + /** + * Sets the definition of the input. + * + * @return void + */ + public function setDefinition(array $definition) + { + $arguments = []; + $options = []; + foreach ($definition as $item) { + if ($item instanceof InputOption) { + $options[] = $item; + } else { + $arguments[] = $item; + } + } + + $this->setArguments($arguments); + $this->setOptions($options); + } + + /** + * Sets the InputArgument objects. + * + * @param InputArgument[] $arguments An array of InputArgument objects + * + * @return void + */ + public function setArguments(array $arguments = []) + { + $this->arguments = []; + $this->requiredCount = 0; + $this->lastOptionalArgument = null; + $this->lastArrayArgument = null; + $this->addArguments($arguments); + } + + /** + * Adds an array of InputArgument objects. + * + * @param InputArgument[] $arguments An array of InputArgument objects + * + * @return void + */ + public function addArguments(?array $arguments = []) + { + if (null !== $arguments) { + foreach ($arguments as $argument) { + $this->addArgument($argument); + } + } + } + + /** + * @return void + * + * @throws LogicException When incorrect argument is given + */ + public function addArgument(InputArgument $argument) + { + if (isset($this->arguments[$argument->getName()])) { + throw new LogicException(sprintf('An argument with name "%s" already exists.', $argument->getName())); + } + + if (null !== $this->lastArrayArgument) { + throw new LogicException(sprintf('Cannot add a required argument "%s" after an array argument "%s".', $argument->getName(), $this->lastArrayArgument->getName())); + } + + if ($argument->isRequired() && null !== $this->lastOptionalArgument) { + throw new LogicException(sprintf('Cannot add a required argument "%s" after an optional one "%s".', $argument->getName(), $this->lastOptionalArgument->getName())); + } + + if ($argument->isArray()) { + $this->lastArrayArgument = $argument; + } + + if ($argument->isRequired()) { + ++$this->requiredCount; + } else { + $this->lastOptionalArgument = $argument; + } + + $this->arguments[$argument->getName()] = $argument; + } + + /** + * Returns an InputArgument by name or by position. + * + * @throws InvalidArgumentException When argument given doesn't exist + */ + public function getArgument(string|int $name): InputArgument + { + if (!$this->hasArgument($name)) { + throw new InvalidArgumentException(sprintf('The "%s" argument does not exist.', $name)); + } + + $arguments = \is_int($name) ? array_values($this->arguments) : $this->arguments; + + return $arguments[$name]; + } + + /** + * Returns true if an InputArgument object exists by name or position. + */ + public function hasArgument(string|int $name): bool + { + $arguments = \is_int($name) ? array_values($this->arguments) : $this->arguments; + + return isset($arguments[$name]); + } + + /** + * Gets the array of InputArgument objects. + * + * @return InputArgument[] + */ + public function getArguments(): array + { + return $this->arguments; + } + + /** + * Returns the number of InputArguments. + */ + public function getArgumentCount(): int + { + return null !== $this->lastArrayArgument ? \PHP_INT_MAX : \count($this->arguments); + } + + /** + * Returns the number of required InputArguments. + */ + public function getArgumentRequiredCount(): int + { + return $this->requiredCount; + } + + /** + * @return array + */ + public function getArgumentDefaults(): array + { + $values = []; + foreach ($this->arguments as $argument) { + $values[$argument->getName()] = $argument->getDefault(); + } + + return $values; + } + + /** + * Sets the InputOption objects. + * + * @param InputOption[] $options An array of InputOption objects + * + * @return void + */ + public function setOptions(array $options = []) + { + $this->options = []; + $this->shortcuts = []; + $this->negations = []; + $this->addOptions($options); + } + + /** + * Adds an array of InputOption objects. + * + * @param InputOption[] $options An array of InputOption objects + * + * @return void + */ + public function addOptions(array $options = []) + { + foreach ($options as $option) { + $this->addOption($option); + } + } + + /** + * @return void + * + * @throws LogicException When option given already exist + */ + public function addOption(InputOption $option) + { + if (isset($this->options[$option->getName()]) && !$option->equals($this->options[$option->getName()])) { + throw new LogicException(sprintf('An option named "%s" already exists.', $option->getName())); + } + if (isset($this->negations[$option->getName()])) { + throw new LogicException(sprintf('An option named "%s" already exists.', $option->getName())); + } + + if ($option->getShortcut()) { + foreach (explode('|', $option->getShortcut()) as $shortcut) { + if (isset($this->shortcuts[$shortcut]) && !$option->equals($this->options[$this->shortcuts[$shortcut]])) { + throw new LogicException(sprintf('An option with shortcut "%s" already exists.', $shortcut)); + } + } + } + + $this->options[$option->getName()] = $option; + if ($option->getShortcut()) { + foreach (explode('|', $option->getShortcut()) as $shortcut) { + $this->shortcuts[$shortcut] = $option->getName(); + } + } + + if ($option->isNegatable()) { + $negatedName = 'no-'.$option->getName(); + if (isset($this->options[$negatedName])) { + throw new LogicException(sprintf('An option named "%s" already exists.', $negatedName)); + } + $this->negations[$negatedName] = $option->getName(); + } + } + + /** + * Returns an InputOption by name. + * + * @throws InvalidArgumentException When option given doesn't exist + */ + public function getOption(string $name): InputOption + { + if (!$this->hasOption($name)) { + throw new InvalidArgumentException(sprintf('The "--%s" option does not exist.', $name)); + } + + return $this->options[$name]; + } + + /** + * Returns true if an InputOption object exists by name. + * + * This method can't be used to check if the user included the option when + * executing the command (use getOption() instead). + */ + public function hasOption(string $name): bool + { + return isset($this->options[$name]); + } + + /** + * Gets the array of InputOption objects. + * + * @return InputOption[] + */ + public function getOptions(): array + { + return $this->options; + } + + /** + * Returns true if an InputOption object exists by shortcut. + */ + public function hasShortcut(string $name): bool + { + return isset($this->shortcuts[$name]); + } + + /** + * Returns true if an InputOption object exists by negated name. + */ + public function hasNegation(string $name): bool + { + return isset($this->negations[$name]); + } + + /** + * Gets an InputOption by shortcut. + */ + public function getOptionForShortcut(string $shortcut): InputOption + { + return $this->getOption($this->shortcutToName($shortcut)); + } + + /** + * @return array + */ + public function getOptionDefaults(): array + { + $values = []; + foreach ($this->options as $option) { + $values[$option->getName()] = $option->getDefault(); + } + + return $values; + } + + /** + * Returns the InputOption name given a shortcut. + * + * @throws InvalidArgumentException When option given does not exist + * + * @internal + */ + public function shortcutToName(string $shortcut): string + { + if (!isset($this->shortcuts[$shortcut])) { + throw new InvalidArgumentException(sprintf('The "-%s" option does not exist.', $shortcut)); + } + + return $this->shortcuts[$shortcut]; + } + + /** + * Returns the InputOption name given a negation. + * + * @throws InvalidArgumentException When option given does not exist + * + * @internal + */ + public function negationToName(string $negation): string + { + if (!isset($this->negations[$negation])) { + throw new InvalidArgumentException(sprintf('The "--%s" option does not exist.', $negation)); + } + + return $this->negations[$negation]; + } + + /** + * Gets the synopsis. + */ + public function getSynopsis(bool $short = false): string + { + $elements = []; + + if ($short && $this->getOptions()) { + $elements[] = '[options]'; + } elseif (!$short) { + foreach ($this->getOptions() as $option) { + $value = ''; + if ($option->acceptValue()) { + $value = sprintf( + ' %s%s%s', + $option->isValueOptional() ? '[' : '', + strtoupper($option->getName()), + $option->isValueOptional() ? ']' : '' + ); + } + + $shortcut = $option->getShortcut() ? sprintf('-%s|', $option->getShortcut()) : ''; + $negation = $option->isNegatable() ? sprintf('|--no-%s', $option->getName()) : ''; + $elements[] = sprintf('[%s--%s%s%s]', $shortcut, $option->getName(), $value, $negation); + } + } + + if (\count($elements) && $this->getArguments()) { + $elements[] = '[--]'; + } + + $tail = ''; + foreach ($this->getArguments() as $argument) { + $element = '<'.$argument->getName().'>'; + if ($argument->isArray()) { + $element .= '...'; + } + + if (!$argument->isRequired()) { + $element = '['.$element; + $tail .= ']'; + } + + $elements[] = $element; + } + + return implode(' ', $elements).$tail; + } +} diff --git a/3rdparty/symfony/console/Input/InputInterface.php b/3rdparty/symfony/console/Input/InputInterface.php new file mode 100644 index 00000000..aaed5fd0 --- /dev/null +++ b/3rdparty/symfony/console/Input/InputInterface.php @@ -0,0 +1,150 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Component\Console\Input; + +use Symfony\Component\Console\Exception\InvalidArgumentException; +use Symfony\Component\Console\Exception\RuntimeException; + +/** + * InputInterface is the interface implemented by all input classes. + * + * @author Fabien Potencier + * + * @method string __toString() Returns a stringified representation of the args passed to the command. + * InputArguments MUST be escaped as well as the InputOption values passed to the command. + */ +interface InputInterface +{ + /** + * Returns the first argument from the raw parameters (not parsed). + */ + public function getFirstArgument(): ?string; + + /** + * Returns true if the raw parameters (not parsed) contain a value. + * + * This method is to be used to introspect the input parameters + * before they have been validated. It must be used carefully. + * Does not necessarily return the correct result for short options + * when multiple flags are combined in the same option. + * + * @param string|array $values The values to look for in the raw parameters (can be an array) + * @param bool $onlyParams Only check real parameters, skip those following an end of options (--) signal + */ + public function hasParameterOption(string|array $values, bool $onlyParams = false): bool; + + /** + * Returns the value of a raw option (not parsed). + * + * This method is to be used to introspect the input parameters + * before they have been validated. It must be used carefully. + * Does not necessarily return the correct result for short options + * when multiple flags are combined in the same option. + * + * @param string|array $values The value(s) to look for in the raw parameters (can be an array) + * @param string|bool|int|float|array|null $default The default value to return if no result is found + * @param bool $onlyParams Only check real parameters, skip those following an end of options (--) signal + * + * @return mixed + */ + public function getParameterOption(string|array $values, string|bool|int|float|array|null $default = false, bool $onlyParams = false); + + /** + * Binds the current Input instance with the given arguments and options. + * + * @return void + * + * @throws RuntimeException + */ + public function bind(InputDefinition $definition); + + /** + * Validates the input. + * + * @return void + * + * @throws RuntimeException When not enough arguments are given + */ + public function validate(); + + /** + * Returns all the given arguments merged with the default values. + * + * @return array + */ + public function getArguments(): array; + + /** + * Returns the argument value for a given argument name. + * + * @return mixed + * + * @throws InvalidArgumentException When argument given doesn't exist + */ + public function getArgument(string $name); + + /** + * Sets an argument value by name. + * + * @return void + * + * @throws InvalidArgumentException When argument given doesn't exist + */ + public function setArgument(string $name, mixed $value); + + /** + * Returns true if an InputArgument object exists by name or position. + */ + public function hasArgument(string $name): bool; + + /** + * Returns all the given options merged with the default values. + * + * @return array + */ + public function getOptions(): array; + + /** + * Returns the option value for a given option name. + * + * @return mixed + * + * @throws InvalidArgumentException When option given doesn't exist + */ + public function getOption(string $name); + + /** + * Sets an option value by name. + * + * @return void + * + * @throws InvalidArgumentException When option given doesn't exist + */ + public function setOption(string $name, mixed $value); + + /** + * Returns true if an InputOption object exists by name. + */ + public function hasOption(string $name): bool; + + /** + * Is this input means interactive? + */ + public function isInteractive(): bool; + + /** + * Sets the input interactivity. + * + * @return void + */ + public function setInteractive(bool $interactive); +} diff --git a/3rdparty/symfony/console/Input/InputOption.php b/3rdparty/symfony/console/Input/InputOption.php new file mode 100644 index 00000000..bb533801 --- /dev/null +++ b/3rdparty/symfony/console/Input/InputOption.php @@ -0,0 +1,255 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Component\Console\Input; + +use Symfony\Component\Console\Command\Command; +use Symfony\Component\Console\Completion\CompletionInput; +use Symfony\Component\Console\Completion\CompletionSuggestions; +use Symfony\Component\Console\Completion\Suggestion; +use Symfony\Component\Console\Exception\InvalidArgumentException; +use Symfony\Component\Console\Exception\LogicException; + +/** + * Represents a command line option. + * + * @author Fabien Potencier + */ +class InputOption +{ + /** + * Do not accept input for the option (e.g. --yell). This is the default behavior of options. + */ + public const VALUE_NONE = 1; + + /** + * A value must be passed when the option is used (e.g. --iterations=5 or -i5). + */ + public const VALUE_REQUIRED = 2; + + /** + * The option may or may not have a value (e.g. --yell or --yell=loud). + */ + public const VALUE_OPTIONAL = 4; + + /** + * The option accepts multiple values (e.g. --dir=/foo --dir=/bar). + */ + public const VALUE_IS_ARRAY = 8; + + /** + * The option may have either positive or negative value (e.g. --ansi or --no-ansi). + */ + public const VALUE_NEGATABLE = 16; + + private string $name; + private string|array|null $shortcut; + private int $mode; + private string|int|bool|array|null|float $default; + private array|\Closure $suggestedValues; + private string $description; + + /** + * @param string|array|null $shortcut The shortcuts, can be null, a string of shortcuts delimited by | or an array of shortcuts + * @param int|null $mode The option mode: One of the VALUE_* constants + * @param string|bool|int|float|array|null $default The default value (must be null for self::VALUE_NONE) + * @param array|\Closure(CompletionInput,CompletionSuggestions):list $suggestedValues The values used for input completion + * + * @throws InvalidArgumentException If option mode is invalid or incompatible + */ + public function __construct(string $name, string|array|null $shortcut = null, ?int $mode = null, string $description = '', string|bool|int|float|array|null $default = null, array|\Closure $suggestedValues = []) + { + if (str_starts_with($name, '--')) { + $name = substr($name, 2); + } + + if (empty($name)) { + throw new InvalidArgumentException('An option name cannot be empty.'); + } + + if ('' === $shortcut || [] === $shortcut || false === $shortcut) { + $shortcut = null; + } + + if (null !== $shortcut) { + if (\is_array($shortcut)) { + $shortcut = implode('|', $shortcut); + } + $shortcuts = preg_split('{(\|)-?}', ltrim($shortcut, '-')); + $shortcuts = array_filter($shortcuts, 'strlen'); + $shortcut = implode('|', $shortcuts); + + if ('' === $shortcut) { + throw new InvalidArgumentException('An option shortcut cannot be empty.'); + } + } + + if (null === $mode) { + $mode = self::VALUE_NONE; + } elseif ($mode >= (self::VALUE_NEGATABLE << 1) || $mode < 1) { + throw new InvalidArgumentException(sprintf('Option mode "%s" is not valid.', $mode)); + } + + $this->name = $name; + $this->shortcut = $shortcut; + $this->mode = $mode; + $this->description = $description; + $this->suggestedValues = $suggestedValues; + + if ($suggestedValues && !$this->acceptValue()) { + throw new LogicException('Cannot set suggested values if the option does not accept a value.'); + } + if ($this->isArray() && !$this->acceptValue()) { + throw new InvalidArgumentException('Impossible to have an option mode VALUE_IS_ARRAY if the option does not accept a value.'); + } + if ($this->isNegatable() && $this->acceptValue()) { + throw new InvalidArgumentException('Impossible to have an option mode VALUE_NEGATABLE if the option also accepts a value.'); + } + + $this->setDefault($default); + } + + /** + * Returns the option shortcut. + */ + public function getShortcut(): ?string + { + return $this->shortcut; + } + + /** + * Returns the option name. + */ + public function getName(): string + { + return $this->name; + } + + /** + * Returns true if the option accepts a value. + * + * @return bool true if value mode is not self::VALUE_NONE, false otherwise + */ + public function acceptValue(): bool + { + return $this->isValueRequired() || $this->isValueOptional(); + } + + /** + * Returns true if the option requires a value. + * + * @return bool true if value mode is self::VALUE_REQUIRED, false otherwise + */ + public function isValueRequired(): bool + { + return self::VALUE_REQUIRED === (self::VALUE_REQUIRED & $this->mode); + } + + /** + * Returns true if the option takes an optional value. + * + * @return bool true if value mode is self::VALUE_OPTIONAL, false otherwise + */ + public function isValueOptional(): bool + { + return self::VALUE_OPTIONAL === (self::VALUE_OPTIONAL & $this->mode); + } + + /** + * Returns true if the option can take multiple values. + * + * @return bool true if mode is self::VALUE_IS_ARRAY, false otherwise + */ + public function isArray(): bool + { + return self::VALUE_IS_ARRAY === (self::VALUE_IS_ARRAY & $this->mode); + } + + public function isNegatable(): bool + { + return self::VALUE_NEGATABLE === (self::VALUE_NEGATABLE & $this->mode); + } + + /** + * @return void + */ + public function setDefault(string|bool|int|float|array|null $default = null) + { + if (1 > \func_num_args()) { + trigger_deprecation('symfony/console', '6.2', 'Calling "%s()" without any arguments is deprecated, pass null explicitly instead.', __METHOD__); + } + if (self::VALUE_NONE === (self::VALUE_NONE & $this->mode) && null !== $default) { + throw new LogicException('Cannot set a default value when using InputOption::VALUE_NONE mode.'); + } + + if ($this->isArray()) { + if (null === $default) { + $default = []; + } elseif (!\is_array($default)) { + throw new LogicException('A default value for an array option must be an array.'); + } + } + + $this->default = $this->acceptValue() || $this->isNegatable() ? $default : false; + } + + /** + * Returns the default value. + */ + public function getDefault(): string|bool|int|float|array|null + { + return $this->default; + } + + /** + * Returns the description text. + */ + public function getDescription(): string + { + return $this->description; + } + + public function hasCompletion(): bool + { + return [] !== $this->suggestedValues; + } + + /** + * Adds suggestions to $suggestions for the current completion input. + * + * @see Command::complete() + */ + public function complete(CompletionInput $input, CompletionSuggestions $suggestions): void + { + $values = $this->suggestedValues; + if ($values instanceof \Closure && !\is_array($values = $values($input))) { + throw new LogicException(sprintf('Closure for option "%s" must return an array. Got "%s".', $this->name, get_debug_type($values))); + } + if ($values) { + $suggestions->suggestValues($values); + } + } + + /** + * Checks whether the given option equals this one. + */ + public function equals(self $option): bool + { + return $option->getName() === $this->getName() + && $option->getShortcut() === $this->getShortcut() + && $option->getDefault() === $this->getDefault() + && $option->isNegatable() === $this->isNegatable() + && $option->isArray() === $this->isArray() + && $option->isValueRequired() === $this->isValueRequired() + && $option->isValueOptional() === $this->isValueOptional() + ; + } +} diff --git a/3rdparty/symfony/console/Input/StreamableInputInterface.php b/3rdparty/symfony/console/Input/StreamableInputInterface.php new file mode 100644 index 00000000..4b95fcb1 --- /dev/null +++ b/3rdparty/symfony/console/Input/StreamableInputInterface.php @@ -0,0 +1,39 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Component\Console\Input; + +/** + * StreamableInputInterface is the interface implemented by all input classes + * that have an input stream. + * + * @author Robin Chalas + */ +interface StreamableInputInterface extends InputInterface +{ + /** + * Sets the input stream to read from when interacting with the user. + * + * This is mainly useful for testing purpose. + * + * @param resource $stream The input stream + * + * @return void + */ + public function setStream($stream); + + /** + * Returns the input stream. + * + * @return resource|null + */ + public function getStream(); +} diff --git a/3rdparty/symfony/console/Input/StringInput.php b/3rdparty/symfony/console/Input/StringInput.php new file mode 100644 index 00000000..82bd2144 --- /dev/null +++ b/3rdparty/symfony/console/Input/StringInput.php @@ -0,0 +1,87 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Component\Console\Input; + +use Symfony\Component\Console\Exception\InvalidArgumentException; + +/** + * StringInput represents an input provided as a string. + * + * Usage: + * + * $input = new StringInput('foo --bar="foobar"'); + * + * @author Fabien Potencier + */ +class StringInput extends ArgvInput +{ + /** + * @deprecated since Symfony 6.1 + */ + public const REGEX_STRING = '([^\s]+?)(?:\s|(?setTokens($this->tokenize($input)); + } + + /** + * Tokenizes a string. + * + * @throws InvalidArgumentException When unable to parse input (should never happen) + */ + private function tokenize(string $input): array + { + $tokens = []; + $length = \strlen($input); + $cursor = 0; + $token = null; + while ($cursor < $length) { + if ('\\' === $input[$cursor]) { + $token .= $input[++$cursor] ?? ''; + ++$cursor; + continue; + } + + if (preg_match('/\s+/A', $input, $match, 0, $cursor)) { + if (null !== $token) { + $tokens[] = $token; + $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))); + } 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)) { + $token .= $match[1]; + } else { + // should never happen + throw new InvalidArgumentException(sprintf('Unable to parse input near "... %s ...".', substr($input, $cursor, 10))); + } + + $cursor += \strlen($match[0]); + } + + if (null !== $token) { + $tokens[] = $token; + } + + return $tokens; + } +} diff --git a/3rdparty/symfony/console/LICENSE b/3rdparty/symfony/console/LICENSE new file mode 100644 index 00000000..0138f8f0 --- /dev/null +++ b/3rdparty/symfony/console/LICENSE @@ -0,0 +1,19 @@ +Copyright (c) 2004-present Fabien Potencier + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is furnished +to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +THE SOFTWARE. diff --git a/3rdparty/symfony/console/Logger/ConsoleLogger.php b/3rdparty/symfony/console/Logger/ConsoleLogger.php new file mode 100644 index 00000000..fddef50c --- /dev/null +++ b/3rdparty/symfony/console/Logger/ConsoleLogger.php @@ -0,0 +1,119 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Component\Console\Logger; + +use Psr\Log\AbstractLogger; +use Psr\Log\InvalidArgumentException; +use Psr\Log\LogLevel; +use Symfony\Component\Console\Output\ConsoleOutputInterface; +use Symfony\Component\Console\Output\OutputInterface; + +/** + * PSR-3 compliant console logger. + * + * @author Kévin Dunglas + * + * @see https://www.php-fig.org/psr/psr-3/ + */ +class ConsoleLogger extends AbstractLogger +{ + public const INFO = 'info'; + public const ERROR = 'error'; + + private OutputInterface $output; + private array $verbosityLevelMap = [ + LogLevel::EMERGENCY => OutputInterface::VERBOSITY_NORMAL, + LogLevel::ALERT => OutputInterface::VERBOSITY_NORMAL, + LogLevel::CRITICAL => OutputInterface::VERBOSITY_NORMAL, + LogLevel::ERROR => OutputInterface::VERBOSITY_NORMAL, + LogLevel::WARNING => OutputInterface::VERBOSITY_NORMAL, + LogLevel::NOTICE => OutputInterface::VERBOSITY_VERBOSE, + LogLevel::INFO => OutputInterface::VERBOSITY_VERY_VERBOSE, + LogLevel::DEBUG => OutputInterface::VERBOSITY_DEBUG, + ]; + private array $formatLevelMap = [ + LogLevel::EMERGENCY => self::ERROR, + LogLevel::ALERT => self::ERROR, + LogLevel::CRITICAL => self::ERROR, + LogLevel::ERROR => self::ERROR, + LogLevel::WARNING => self::INFO, + LogLevel::NOTICE => self::INFO, + LogLevel::INFO => self::INFO, + LogLevel::DEBUG => self::INFO, + ]; + private bool $errored = false; + + public function __construct(OutputInterface $output, array $verbosityLevelMap = [], array $formatLevelMap = []) + { + $this->output = $output; + $this->verbosityLevelMap = $verbosityLevelMap + $this->verbosityLevelMap; + $this->formatLevelMap = $formatLevelMap + $this->formatLevelMap; + } + + public function log($level, $message, array $context = []): void + { + if (!isset($this->verbosityLevelMap[$level])) { + throw new InvalidArgumentException(sprintf('The log level "%s" does not exist.', $level)); + } + + $output = $this->output; + + // Write to the error output if necessary and available + if (self::ERROR === $this->formatLevelMap[$level]) { + if ($this->output instanceof ConsoleOutputInterface) { + $output = $output->getErrorOutput(); + } + $this->errored = true; + } + + // the if condition check isn't necessary -- it's the same one that $output will do internally anyway. + // We only do it for efficiency here as the message formatting is relatively expensive. + if ($output->getVerbosity() >= $this->verbosityLevelMap[$level]) { + $output->writeln(sprintf('<%1$s>[%2$s] %3$s', $this->formatLevelMap[$level], $level, $this->interpolate($message, $context)), $this->verbosityLevelMap[$level]); + } + } + + /** + * Returns true when any messages have been logged at error levels. + */ + public function hasErrored(): bool + { + return $this->errored; + } + + /** + * Interpolates context values into the message placeholders. + * + * @author PHP Framework Interoperability Group + */ + private function interpolate(string $message, array $context): string + { + if (!str_contains($message, '{')) { + return $message; + } + + $replacements = []; + foreach ($context as $key => $val) { + if (null === $val || \is_scalar($val) || $val instanceof \Stringable) { + $replacements["{{$key}}"] = $val; + } elseif ($val instanceof \DateTimeInterface) { + $replacements["{{$key}}"] = $val->format(\DateTimeInterface::RFC3339); + } elseif (\is_object($val)) { + $replacements["{{$key}}"] = '[object '.$val::class.']'; + } else { + $replacements["{{$key}}"] = '['.\gettype($val).']'; + } + } + + return strtr($message, $replacements); + } +} diff --git a/3rdparty/symfony/console/Messenger/RunCommandContext.php b/3rdparty/symfony/console/Messenger/RunCommandContext.php new file mode 100644 index 00000000..2ee5415c --- /dev/null +++ b/3rdparty/symfony/console/Messenger/RunCommandContext.php @@ -0,0 +1,25 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Component\Console\Messenger; + +/** + * @author Kevin Bond + */ +final class RunCommandContext +{ + public function __construct( + public readonly RunCommandMessage $message, + public readonly int $exitCode, + public readonly string $output, + ) { + } +} diff --git a/3rdparty/symfony/console/Messenger/RunCommandMessage.php b/3rdparty/symfony/console/Messenger/RunCommandMessage.php new file mode 100644 index 00000000..b530c438 --- /dev/null +++ b/3rdparty/symfony/console/Messenger/RunCommandMessage.php @@ -0,0 +1,36 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Component\Console\Messenger; + +use Symfony\Component\Console\Exception\RunCommandFailedException; + +/** + * @author Kevin Bond + */ +class RunCommandMessage implements \Stringable +{ + /** + * @param bool $throwOnFailure If the command has a non-zero exit code, throw {@see RunCommandFailedException} + * @param bool $catchExceptions @see Application::setCatchExceptions() + */ + public function __construct( + public readonly string $input, + public readonly bool $throwOnFailure = true, + public readonly bool $catchExceptions = false, + ) { + } + + public function __toString(): string + { + return $this->input; + } +} diff --git a/3rdparty/symfony/console/Messenger/RunCommandMessageHandler.php b/3rdparty/symfony/console/Messenger/RunCommandMessageHandler.php new file mode 100644 index 00000000..14f9c176 --- /dev/null +++ b/3rdparty/symfony/console/Messenger/RunCommandMessageHandler.php @@ -0,0 +1,48 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Component\Console\Messenger; + +use Symfony\Component\Console\Application; +use Symfony\Component\Console\Command\Command; +use Symfony\Component\Console\Exception\RunCommandFailedException; +use Symfony\Component\Console\Input\StringInput; +use Symfony\Component\Console\Output\BufferedOutput; + +/** + * @author Kevin Bond + */ +final class RunCommandMessageHandler +{ + public function __construct(private readonly Application $application) + { + } + + public function __invoke(RunCommandMessage $message): RunCommandContext + { + $input = new StringInput($message->input); + $output = new BufferedOutput(); + + $this->application->setCatchExceptions($message->catchExceptions); + + try { + $exitCode = $this->application->run($input, $output); + } catch (\Throwable $e) { + throw new RunCommandFailedException($e, new RunCommandContext($message, Command::FAILURE, $output->fetch())); + } + + if ($message->throwOnFailure && Command::SUCCESS !== $exitCode) { + throw new RunCommandFailedException(sprintf('Command "%s" exited with code "%s".', $message->input, $exitCode), new RunCommandContext($message, $exitCode, $output->fetch())); + } + + return new RunCommandContext($message, $exitCode, $output->fetch()); + } +} diff --git a/3rdparty/symfony/console/Output/AnsiColorMode.php b/3rdparty/symfony/console/Output/AnsiColorMode.php new file mode 100644 index 00000000..5f9f744f --- /dev/null +++ b/3rdparty/symfony/console/Output/AnsiColorMode.php @@ -0,0 +1,106 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Component\Console\Output; + +use Symfony\Component\Console\Exception\InvalidArgumentException; + +/** + * @author Fabien Potencier + * @author Julien Boudry + */ +enum AnsiColorMode +{ + /* + * Classical 4-bit Ansi colors, including 8 classical colors and 8 bright color. Output syntax is "ESC[${foreGroundColorcode};${backGroundColorcode}m" + * Must be compatible with all terminals and it's the minimal version supported. + */ + case Ansi4; + + /* + * 8-bit Ansi colors (240 different colors + 16 duplicate color codes, ensuring backward compatibility). + * Output syntax is: "ESC[38;5;${foreGroundColorcode};48;5;${backGroundColorcode}m" + * Should be compatible with most terminals. + */ + case Ansi8; + + /* + * 24-bit Ansi colors (RGB). + * Output syntax is: "ESC[38;2;${foreGroundColorcodeRed};${foreGroundColorcodeGreen};${foreGroundColorcodeBlue};48;2;${backGroundColorcodeRed};${backGroundColorcodeGreen};${backGroundColorcodeBlue}m" + * May be compatible with many modern terminals. + */ + case Ansi24; + + /** + * Converts an RGB hexadecimal color to the corresponding Ansi code. + */ + public function convertFromHexToAnsiColorCode(string $hexColor): string + { + $hexColor = str_replace('#', '', $hexColor); + + if (3 === \strlen($hexColor)) { + $hexColor = $hexColor[0].$hexColor[0].$hexColor[1].$hexColor[1].$hexColor[2].$hexColor[2]; + } + + if (6 !== \strlen($hexColor)) { + throw new InvalidArgumentException(sprintf('Invalid "#%s" color.', $hexColor)); + } + + $color = hexdec($hexColor); + + $r = ($color >> 16) & 255; + $g = ($color >> 8) & 255; + $b = $color & 255; + + return match ($this) { + self::Ansi4 => (string) $this->convertFromRGB($r, $g, $b), + self::Ansi8 => '8;5;'.((string) $this->convertFromRGB($r, $g, $b)), + self::Ansi24 => sprintf('8;2;%d;%d;%d', $r, $g, $b) + }; + } + + private function convertFromRGB(int $r, int $g, int $b): int + { + return match ($this) { + self::Ansi4 => $this->degradeHexColorToAnsi4($r, $g, $b), + self::Ansi8 => $this->degradeHexColorToAnsi8($r, $g, $b), + default => throw new InvalidArgumentException("RGB cannot be converted to {$this->name}.") + }; + } + + private function degradeHexColorToAnsi4(int $r, int $g, int $b): int + { + return round($b / 255) << 2 | (round($g / 255) << 1) | round($r / 255); + } + + /** + * Inspired from https://github.com/ajalt/colormath/blob/e464e0da1b014976736cf97250063248fc77b8e7/colormath/src/commonMain/kotlin/com/github/ajalt/colormath/model/Ansi256.kt code (MIT license). + */ + private function degradeHexColorToAnsi8(int $r, int $g, int $b): int + { + if ($r === $g && $g === $b) { + if ($r < 8) { + return 16; + } + + if ($r > 248) { + return 231; + } + + return (int) round(($r - 8) / 247 * 24) + 232; + } else { + return 16 + + (36 * (int) round($r / 255 * 5)) + + (6 * (int) round($g / 255 * 5)) + + (int) round($b / 255 * 5); + } + } +} diff --git a/3rdparty/symfony/console/Output/BufferedOutput.php b/3rdparty/symfony/console/Output/BufferedOutput.php new file mode 100644 index 00000000..ef5099bf --- /dev/null +++ b/3rdparty/symfony/console/Output/BufferedOutput.php @@ -0,0 +1,43 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Component\Console\Output; + +/** + * @author Jean-François Simon + */ +class BufferedOutput extends Output +{ + private string $buffer = ''; + + /** + * Empties buffer and returns its content. + */ + public function fetch(): string + { + $content = $this->buffer; + $this->buffer = ''; + + return $content; + } + + /** + * @return void + */ + protected function doWrite(string $message, bool $newline) + { + $this->buffer .= $message; + + if ($newline) { + $this->buffer .= \PHP_EOL; + } + } +} diff --git a/3rdparty/symfony/console/Output/ConsoleOutput.php b/3rdparty/symfony/console/Output/ConsoleOutput.php new file mode 100644 index 00000000..5837e74a --- /dev/null +++ b/3rdparty/symfony/console/Output/ConsoleOutput.php @@ -0,0 +1,165 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Component\Console\Output; + +use Symfony\Component\Console\Formatter\OutputFormatterInterface; + +/** + * ConsoleOutput is the default class for all CLI output. It uses STDOUT and STDERR. + * + * This class is a convenient wrapper around `StreamOutput` for both STDOUT and STDERR. + * + * $output = new ConsoleOutput(); + * + * This is equivalent to: + * + * $output = new StreamOutput(fopen('php://stdout', 'w')); + * $stdErr = new StreamOutput(fopen('php://stderr', 'w')); + * + * @author Fabien Potencier + */ +class ConsoleOutput extends StreamOutput implements ConsoleOutputInterface +{ + private OutputInterface $stderr; + private array $consoleSectionOutputs = []; + + /** + * @param int $verbosity The verbosity level (one of the VERBOSITY constants in OutputInterface) + * @param bool|null $decorated Whether to decorate messages (null for auto-guessing) + * @param OutputFormatterInterface|null $formatter Output formatter instance (null to use default OutputFormatter) + */ + public function __construct(int $verbosity = self::VERBOSITY_NORMAL, ?bool $decorated = null, ?OutputFormatterInterface $formatter = null) + { + parent::__construct($this->openOutputStream(), $verbosity, $decorated, $formatter); + + if (null === $formatter) { + // for BC reasons, stdErr has it own Formatter only when user don't inject a specific formatter. + $this->stderr = new StreamOutput($this->openErrorStream(), $verbosity, $decorated); + + return; + } + + $actualDecorated = $this->isDecorated(); + $this->stderr = new StreamOutput($this->openErrorStream(), $verbosity, $decorated, $this->getFormatter()); + + if (null === $decorated) { + $this->setDecorated($actualDecorated && $this->stderr->isDecorated()); + } + } + + /** + * Creates a new output section. + */ + public function section(): ConsoleSectionOutput + { + return new ConsoleSectionOutput($this->getStream(), $this->consoleSectionOutputs, $this->getVerbosity(), $this->isDecorated(), $this->getFormatter()); + } + + /** + * @return void + */ + public function setDecorated(bool $decorated) + { + parent::setDecorated($decorated); + $this->stderr->setDecorated($decorated); + } + + /** + * @return void + */ + public function setFormatter(OutputFormatterInterface $formatter) + { + parent::setFormatter($formatter); + $this->stderr->setFormatter($formatter); + } + + /** + * @return void + */ + public function setVerbosity(int $level) + { + parent::setVerbosity($level); + $this->stderr->setVerbosity($level); + } + + public function getErrorOutput(): OutputInterface + { + return $this->stderr; + } + + /** + * @return void + */ + public function setErrorOutput(OutputInterface $error) + { + $this->stderr = $error; + } + + /** + * Returns true if current environment supports writing console output to + * STDOUT. + */ + protected function hasStdoutSupport(): bool + { + return false === $this->isRunningOS400(); + } + + /** + * Returns true if current environment supports writing console output to + * STDERR. + */ + protected function hasStderrSupport(): bool + { + return false === $this->isRunningOS400(); + } + + /** + * Checks if current executing environment is IBM iSeries (OS400), which + * doesn't properly convert character-encodings between ASCII to EBCDIC. + */ + private function isRunningOS400(): bool + { + $checks = [ + \function_exists('php_uname') ? php_uname('s') : '', + getenv('OSTYPE'), + \PHP_OS, + ]; + + return false !== stripos(implode(';', $checks), 'OS400'); + } + + /** + * @return resource + */ + private function openOutputStream() + { + if (!$this->hasStdoutSupport()) { + return fopen('php://output', 'w'); + } + + // Use STDOUT when possible to prevent from opening too many file descriptors + return \defined('STDOUT') ? \STDOUT : (@fopen('php://stdout', 'w') ?: fopen('php://output', 'w')); + } + + /** + * @return resource + */ + private function openErrorStream() + { + if (!$this->hasStderrSupport()) { + return fopen('php://output', 'w'); + } + + // Use STDERR when possible to prevent from opening too many file descriptors + return \defined('STDERR') ? \STDERR : (@fopen('php://stderr', 'w') ?: fopen('php://output', 'w')); + } +} diff --git a/3rdparty/symfony/console/Output/ConsoleOutputInterface.php b/3rdparty/symfony/console/Output/ConsoleOutputInterface.php new file mode 100644 index 00000000..9c0049c8 --- /dev/null +++ b/3rdparty/symfony/console/Output/ConsoleOutputInterface.php @@ -0,0 +1,33 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Component\Console\Output; + +/** + * ConsoleOutputInterface is the interface implemented by ConsoleOutput class. + * This adds information about stderr and section output stream. + * + * @author Dariusz Górecki + */ +interface ConsoleOutputInterface extends OutputInterface +{ + /** + * Gets the OutputInterface for errors. + */ + public function getErrorOutput(): OutputInterface; + + /** + * @return void + */ + public function setErrorOutput(OutputInterface $error); + + public function section(): ConsoleSectionOutput; +} diff --git a/3rdparty/symfony/console/Output/ConsoleSectionOutput.php b/3rdparty/symfony/console/Output/ConsoleSectionOutput.php new file mode 100644 index 00000000..f2d7933b --- /dev/null +++ b/3rdparty/symfony/console/Output/ConsoleSectionOutput.php @@ -0,0 +1,244 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Component\Console\Output; + +use Symfony\Component\Console\Formatter\OutputFormatterInterface; +use Symfony\Component\Console\Helper\Helper; +use Symfony\Component\Console\Terminal; + +/** + * @author Pierre du Plessis + * @author Gabriel Ostrolucký + */ +class ConsoleSectionOutput extends StreamOutput +{ + private array $content = []; + private int $lines = 0; + private array $sections; + private Terminal $terminal; + private int $maxHeight = 0; + + /** + * @param resource $stream + * @param ConsoleSectionOutput[] $sections + */ + public function __construct($stream, array &$sections, int $verbosity, bool $decorated, OutputFormatterInterface $formatter) + { + parent::__construct($stream, $verbosity, $decorated, $formatter); + array_unshift($sections, $this); + $this->sections = &$sections; + $this->terminal = new Terminal(); + } + + /** + * Defines a maximum number of lines for this section. + * + * When more lines are added, the section will automatically scroll to the + * end (i.e. remove the first lines to comply with the max height). + */ + public function setMaxHeight(int $maxHeight): void + { + // when changing max height, clear output of current section and redraw again with the new height + $previousMaxHeight = $this->maxHeight; + $this->maxHeight = $maxHeight; + $existingContent = $this->popStreamContentUntilCurrentSection($previousMaxHeight ? min($previousMaxHeight, $this->lines) : $this->lines); + + parent::doWrite($this->getVisibleContent(), false); + parent::doWrite($existingContent, false); + } + + /** + * Clears previous output for this section. + * + * @param int $lines Number of lines to clear. If null, then the entire output of this section is cleared + * + * @return void + */ + public function clear(?int $lines = null) + { + if (empty($this->content) || !$this->isDecorated()) { + return; + } + + if ($lines) { + array_splice($this->content, -$lines); + } else { + $lines = $this->lines; + $this->content = []; + } + + $this->lines -= $lines; + + parent::doWrite($this->popStreamContentUntilCurrentSection($this->maxHeight ? min($this->maxHeight, $lines) : $lines), false); + } + + /** + * Overwrites the previous output with a new message. + * + * @return void + */ + public function overwrite(string|iterable $message) + { + $this->clear(); + $this->writeln($message); + } + + public function getContent(): string + { + return implode('', $this->content); + } + + public function getVisibleContent(): string + { + if (0 === $this->maxHeight) { + return $this->getContent(); + } + + return implode('', \array_slice($this->content, -$this->maxHeight)); + } + + /** + * @internal + */ + public function addContent(string $input, bool $newline = true): int + { + $width = $this->terminal->getWidth(); + $lines = explode(\PHP_EOL, $input); + $linesAdded = 0; + $count = \count($lines) - 1; + foreach ($lines as $i => $lineContent) { + // re-add the line break (that has been removed in the above `explode()` for + // - every line that is not the last line + // - if $newline is required, also add it to the last line + if ($i < $count || $newline) { + $lineContent .= \PHP_EOL; + } + + // skip line if there is no text (or newline for that matter) + if ('' === $lineContent) { + continue; + } + + // For the first line, check if the previous line (last entry of `$this->content`) + // needs to be continued (i.e. does not end with a line break). + if (0 === $i + && (false !== $lastLine = end($this->content)) + && !str_ends_with($lastLine, \PHP_EOL) + ) { + // deduct the line count of the previous line + $this->lines -= (int) ceil($this->getDisplayLength($lastLine) / $width) ?: 1; + // concatenate previous and new line + $lineContent = $lastLine.$lineContent; + // replace last entry of `$this->content` with the new expanded line + array_splice($this->content, -1, 1, $lineContent); + } else { + // otherwise just add the new content + $this->content[] = $lineContent; + } + + $linesAdded += (int) ceil($this->getDisplayLength($lineContent) / $width) ?: 1; + } + + $this->lines += $linesAdded; + + return $linesAdded; + } + + /** + * @internal + */ + public function addNewLineOfInputSubmit(): void + { + $this->content[] = \PHP_EOL; + ++$this->lines; + } + + /** + * @return void + */ + protected function doWrite(string $message, bool $newline) + { + // Simulate newline behavior for consistent output formatting, avoiding extra logic + if (!$newline && str_ends_with($message, \PHP_EOL)) { + $message = substr($message, 0, -\strlen(\PHP_EOL)); + $newline = true; + } + + if (!$this->isDecorated()) { + parent::doWrite($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; + + $linesAdded = $this->addContent($message, $newline); + + 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; + } + + $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); + parent::doWrite($erasedContent, false); + } + + /** + * At initial stage, cursor is at the end of stream output. This method makes cursor crawl upwards until it hits + * current section. Then it erases content it crawled through. Optionally, it erases part of current section too. + */ + private function popStreamContentUntilCurrentSection(int $numberOfLinesToClearFromCurrentSection = 0): string + { + $numberOfLinesToClear = $numberOfLinesToClearFromCurrentSection; + $erasedContent = []; + + foreach ($this->sections as $section) { + if ($section === $this) { + break; + } + + $numberOfLinesToClear += $section->maxHeight ? min($section->lines, $section->maxHeight) : $section->lines; + if ('' !== $sectionContent = $section->getVisibleContent()) { + if (!str_ends_with($sectionContent, \PHP_EOL)) { + $sectionContent .= \PHP_EOL; + } + $erasedContent[] = $sectionContent; + } + } + + if ($numberOfLinesToClear > 0) { + // move cursor up n lines + parent::doWrite(sprintf("\x1b[%dA", $numberOfLinesToClear), false); + // erase to end of screen + parent::doWrite("\x1b[0J", false); + } + + return implode('', array_reverse($erasedContent)); + } + + private function getDisplayLength(string $text): int + { + return Helper::width(Helper::removeDecoration($this->getFormatter(), str_replace("\t", ' ', $text))); + } +} diff --git a/3rdparty/symfony/console/Output/NullOutput.php b/3rdparty/symfony/console/Output/NullOutput.php new file mode 100644 index 00000000..f3aa15b1 --- /dev/null +++ b/3rdparty/symfony/console/Output/NullOutput.php @@ -0,0 +1,104 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Component\Console\Output; + +use Symfony\Component\Console\Formatter\NullOutputFormatter; +use Symfony\Component\Console\Formatter\OutputFormatterInterface; + +/** + * NullOutput suppresses all output. + * + * $output = new NullOutput(); + * + * @author Fabien Potencier + * @author Tobias Schultze + */ +class NullOutput implements OutputInterface +{ + private NullOutputFormatter $formatter; + + /** + * @return void + */ + public function setFormatter(OutputFormatterInterface $formatter) + { + // do nothing + } + + public function getFormatter(): OutputFormatterInterface + { + // to comply with the interface we must return a OutputFormatterInterface + return $this->formatter ??= new NullOutputFormatter(); + } + + /** + * @return void + */ + public function setDecorated(bool $decorated) + { + // do nothing + } + + public function isDecorated(): bool + { + return false; + } + + /** + * @return void + */ + public function setVerbosity(int $level) + { + // do nothing + } + + public function getVerbosity(): int + { + return self::VERBOSITY_QUIET; + } + + public function isQuiet(): bool + { + return true; + } + + public function isVerbose(): bool + { + return false; + } + + public function isVeryVerbose(): bool + { + return false; + } + + public function isDebug(): bool + { + return false; + } + + /** + * @return void + */ + public function writeln(string|iterable $messages, int $options = self::OUTPUT_NORMAL) + { + // do nothing + } + + /** + * @return void + */ + public function write(string|iterable $messages, bool $newline = false, int $options = self::OUTPUT_NORMAL) + { + // do nothing + } +} diff --git a/3rdparty/symfony/console/Output/Output.php b/3rdparty/symfony/console/Output/Output.php new file mode 100644 index 00000000..00f481e0 --- /dev/null +++ b/3rdparty/symfony/console/Output/Output.php @@ -0,0 +1,155 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Component\Console\Output; + +use Symfony\Component\Console\Formatter\OutputFormatter; +use Symfony\Component\Console\Formatter\OutputFormatterInterface; + +/** + * Base class for output classes. + * + * There are five levels of verbosity: + * + * * normal: no option passed (normal output) + * * verbose: -v (more output) + * * very verbose: -vv (highly extended output) + * * debug: -vvv (all debug output) + * * quiet: -q (no output) + * + * @author Fabien Potencier + */ +abstract class Output implements OutputInterface +{ + private int $verbosity; + private OutputFormatterInterface $formatter; + + /** + * @param int|null $verbosity The verbosity level (one of the VERBOSITY constants in OutputInterface) + * @param bool $decorated Whether to decorate messages + * @param OutputFormatterInterface|null $formatter Output formatter instance (null to use default OutputFormatter) + */ + public function __construct(?int $verbosity = self::VERBOSITY_NORMAL, bool $decorated = false, ?OutputFormatterInterface $formatter = null) + { + $this->verbosity = $verbosity ?? self::VERBOSITY_NORMAL; + $this->formatter = $formatter ?? new OutputFormatter(); + $this->formatter->setDecorated($decorated); + } + + /** + * @return void + */ + public function setFormatter(OutputFormatterInterface $formatter) + { + $this->formatter = $formatter; + } + + public function getFormatter(): OutputFormatterInterface + { + return $this->formatter; + } + + /** + * @return void + */ + public function setDecorated(bool $decorated) + { + $this->formatter->setDecorated($decorated); + } + + public function isDecorated(): bool + { + return $this->formatter->isDecorated(); + } + + /** + * @return void + */ + public function setVerbosity(int $level) + { + $this->verbosity = $level; + } + + public function getVerbosity(): int + { + return $this->verbosity; + } + + public function isQuiet(): bool + { + return self::VERBOSITY_QUIET === $this->verbosity; + } + + public function isVerbose(): bool + { + return self::VERBOSITY_VERBOSE <= $this->verbosity; + } + + public function isVeryVerbose(): bool + { + return self::VERBOSITY_VERY_VERBOSE <= $this->verbosity; + } + + public function isDebug(): bool + { + return self::VERBOSITY_DEBUG <= $this->verbosity; + } + + /** + * @return void + */ + public function writeln(string|iterable $messages, int $options = self::OUTPUT_NORMAL) + { + $this->write($messages, true, $options); + } + + /** + * @return void + */ + public function write(string|iterable $messages, bool $newline = false, int $options = self::OUTPUT_NORMAL) + { + if (!is_iterable($messages)) { + $messages = [$messages]; + } + + $types = self::OUTPUT_NORMAL | self::OUTPUT_RAW | self::OUTPUT_PLAIN; + $type = $types & $options ?: self::OUTPUT_NORMAL; + + $verbosities = self::VERBOSITY_QUIET | self::VERBOSITY_NORMAL | self::VERBOSITY_VERBOSE | self::VERBOSITY_VERY_VERBOSE | self::VERBOSITY_DEBUG; + $verbosity = $verbosities & $options ?: self::VERBOSITY_NORMAL; + + if ($verbosity > $this->getVerbosity()) { + return; + } + + foreach ($messages as $message) { + switch ($type) { + case OutputInterface::OUTPUT_NORMAL: + $message = $this->formatter->format($message); + break; + case OutputInterface::OUTPUT_RAW: + break; + case OutputInterface::OUTPUT_PLAIN: + $message = strip_tags($this->formatter->format($message)); + break; + } + + $this->doWrite($message ?? '', $newline); + } + } + + /** + * Writes a message to the output. + * + * @return void + */ + abstract protected function doWrite(string $message, bool $newline); +} diff --git a/3rdparty/symfony/console/Output/OutputInterface.php b/3rdparty/symfony/console/Output/OutputInterface.php new file mode 100644 index 00000000..19a81790 --- /dev/null +++ b/3rdparty/symfony/console/Output/OutputInterface.php @@ -0,0 +1,111 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Component\Console\Output; + +use Symfony\Component\Console\Formatter\OutputFormatterInterface; + +/** + * OutputInterface is the interface implemented by all Output classes. + * + * @author Fabien Potencier + */ +interface OutputInterface +{ + public const VERBOSITY_QUIET = 16; + public const VERBOSITY_NORMAL = 32; + public const VERBOSITY_VERBOSE = 64; + public const VERBOSITY_VERY_VERBOSE = 128; + public const VERBOSITY_DEBUG = 256; + + public const OUTPUT_NORMAL = 1; + public const OUTPUT_RAW = 2; + public const OUTPUT_PLAIN = 4; + + /** + * Writes a message to the output. + * + * @param bool $newline Whether to add a newline + * @param int $options A bitmask of options (one of the OUTPUT or VERBOSITY constants), + * 0 is considered the same as self::OUTPUT_NORMAL | self::VERBOSITY_NORMAL + * + * @return void + */ + public function write(string|iterable $messages, bool $newline = false, int $options = 0); + + /** + * Writes a message to the output and adds a newline at the end. + * + * @param int $options A bitmask of options (one of the OUTPUT or VERBOSITY constants), + * 0 is considered the same as self::OUTPUT_NORMAL | self::VERBOSITY_NORMAL + * + * @return void + */ + public function writeln(string|iterable $messages, int $options = 0); + + /** + * Sets the verbosity of the output. + * + * @param self::VERBOSITY_* $level + * + * @return void + */ + public function setVerbosity(int $level); + + /** + * Gets the current verbosity of the output. + * + * @return self::VERBOSITY_* + */ + public function getVerbosity(): int; + + /** + * Returns whether verbosity is quiet (-q). + */ + public function isQuiet(): bool; + + /** + * Returns whether verbosity is verbose (-v). + */ + public function isVerbose(): bool; + + /** + * Returns whether verbosity is very verbose (-vv). + */ + public function isVeryVerbose(): bool; + + /** + * Returns whether verbosity is debug (-vvv). + */ + public function isDebug(): bool; + + /** + * Sets the decorated flag. + * + * @return void + */ + public function setDecorated(bool $decorated); + + /** + * Gets the decorated flag. + */ + public function isDecorated(): bool; + + /** + * @return void + */ + public function setFormatter(OutputFormatterInterface $formatter); + + /** + * Returns current output formatter instance. + */ + public function getFormatter(): OutputFormatterInterface; +} diff --git a/3rdparty/symfony/console/Output/StreamOutput.php b/3rdparty/symfony/console/Output/StreamOutput.php new file mode 100644 index 00000000..f51d0376 --- /dev/null +++ b/3rdparty/symfony/console/Output/StreamOutput.php @@ -0,0 +1,125 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Component\Console\Output; + +use Symfony\Component\Console\Exception\InvalidArgumentException; +use Symfony\Component\Console\Formatter\OutputFormatterInterface; + +/** + * StreamOutput writes the output to a given stream. + * + * Usage: + * + * $output = new StreamOutput(fopen('php://stdout', 'w')); + * + * As `StreamOutput` can use any stream, you can also use a file: + * + * $output = new StreamOutput(fopen('/path/to/output.log', 'a', false)); + * + * @author Fabien Potencier + */ +class StreamOutput extends Output +{ + /** @var resource */ + private $stream; + + /** + * @param resource $stream A stream resource + * @param int $verbosity The verbosity level (one of the VERBOSITY constants in OutputInterface) + * @param bool|null $decorated Whether to decorate messages (null for auto-guessing) + * @param OutputFormatterInterface|null $formatter Output formatter instance (null to use default OutputFormatter) + * + * @throws InvalidArgumentException When first argument is not a real stream + */ + public function __construct($stream, int $verbosity = self::VERBOSITY_NORMAL, ?bool $decorated = null, ?OutputFormatterInterface $formatter = null) + { + if (!\is_resource($stream) || 'stream' !== get_resource_type($stream)) { + throw new InvalidArgumentException('The StreamOutput class needs a stream as its first argument.'); + } + + $this->stream = $stream; + + $decorated ??= $this->hasColorSupport(); + + parent::__construct($verbosity, $decorated, $formatter); + } + + /** + * Gets the stream attached to this StreamOutput instance. + * + * @return resource + */ + public function getStream() + { + return $this->stream; + } + + /** + * @return void + */ + protected function doWrite(string $message, bool $newline) + { + if ($newline) { + $message .= \PHP_EOL; + } + + @fwrite($this->stream, $message); + + fflush($this->stream); + } + + /** + * Returns true if the stream supports colorization. + * + * Colorization is disabled if not supported by the stream: + * + * This is tricky on Windows, because Cygwin, Msys2 etc emulate pseudo + * terminals via named pipes, so we can only check the environment. + * + * Reference: Composer\XdebugHandler\Process::supportsColor + * https://github.com/composer/xdebug-handler + * + * @return bool true if the stream supports colorization, false otherwise + */ + protected function hasColorSupport(): bool + { + // Follow https://no-color.org/ + if ('' !== (($_SERVER['NO_COLOR'] ?? getenv('NO_COLOR'))[0] ?? '')) { + return false; + } + + // Detect msysgit/mingw and assume this is a tty because detection + // does not work correctly, see https://github.com/composer/composer/issues/9690 + if (!@stream_isatty($this->stream) && !\in_array(strtoupper((string) getenv('MSYSTEM')), ['MINGW32', 'MINGW64'], true)) { + return false; + } + + if ('\\' === \DIRECTORY_SEPARATOR && @sapi_windows_vt100_support($this->stream)) { + return true; + } + + if ('Hyper' === getenv('TERM_PROGRAM') + || false !== getenv('COLORTERM') + || false !== getenv('ANSICON') + || 'ON' === getenv('ConEmuANSI') + ) { + return true; + } + + if ('dumb' === $term = (string) getenv('TERM')) { + return false; + } + + // See https://github.com/chalk/supports-color/blob/d4f413efaf8da045c5ab440ed418ef02dbb28bf1/index.js#L157 + return preg_match('/^((screen|xterm|vt100|vt220|putty|rxvt|ansi|cygwin|linux).*)|(.*-256(color)?(-bce)?)$/', $term); + } +} diff --git a/3rdparty/symfony/console/Output/TrimmedBufferOutput.php b/3rdparty/symfony/console/Output/TrimmedBufferOutput.php new file mode 100644 index 00000000..23a2be8c --- /dev/null +++ b/3rdparty/symfony/console/Output/TrimmedBufferOutput.php @@ -0,0 +1,61 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Component\Console\Output; + +use Symfony\Component\Console\Exception\InvalidArgumentException; +use Symfony\Component\Console\Formatter\OutputFormatterInterface; + +/** + * A BufferedOutput that keeps only the last N chars. + * + * @author Jérémy Derussé + */ +class TrimmedBufferOutput extends Output +{ + private int $maxLength; + private string $buffer = ''; + + public function __construct(int $maxLength, ?int $verbosity = self::VERBOSITY_NORMAL, bool $decorated = false, ?OutputFormatterInterface $formatter = null) + { + if ($maxLength <= 0) { + throw new InvalidArgumentException(sprintf('"%s()" expects a strictly positive maxLength. Got %d.', __METHOD__, $maxLength)); + } + + parent::__construct($verbosity, $decorated, $formatter); + $this->maxLength = $maxLength; + } + + /** + * Empties buffer and returns its content. + */ + public function fetch(): string + { + $content = $this->buffer; + $this->buffer = ''; + + return $content; + } + + /** + * @return void + */ + protected function doWrite(string $message, bool $newline) + { + $this->buffer .= $message; + + if ($newline) { + $this->buffer .= \PHP_EOL; + } + + $this->buffer = substr($this->buffer, 0 - $this->maxLength); + } +} diff --git a/3rdparty/symfony/console/Question/ChoiceQuestion.php b/3rdparty/symfony/console/Question/ChoiceQuestion.php new file mode 100644 index 00000000..465f3184 --- /dev/null +++ b/3rdparty/symfony/console/Question/ChoiceQuestion.php @@ -0,0 +1,177 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Component\Console\Question; + +use Symfony\Component\Console\Exception\InvalidArgumentException; + +/** + * Represents a choice question. + * + * @author Fabien Potencier + */ +class ChoiceQuestion extends Question +{ + private array $choices; + private bool $multiselect = false; + private string $prompt = ' > '; + private string $errorMessage = 'Value "%s" is invalid'; + + /** + * @param string $question The question to ask to the user + * @param array $choices The list of available choices + * @param string|bool|int|float|null $default The default answer to return + */ + public function __construct(string $question, array $choices, string|bool|int|float|null $default = null) + { + if (!$choices) { + throw new \LogicException('Choice question must have at least 1 choice available.'); + } + + parent::__construct($question, $default); + + $this->choices = $choices; + $this->setValidator($this->getDefaultValidator()); + $this->setAutocompleterValues($choices); + } + + /** + * Returns available choices. + */ + public function getChoices(): array + { + return $this->choices; + } + + /** + * Sets multiselect option. + * + * When multiselect is set to true, multiple choices can be answered. + * + * @return $this + */ + public function setMultiselect(bool $multiselect): static + { + $this->multiselect = $multiselect; + $this->setValidator($this->getDefaultValidator()); + + return $this; + } + + /** + * Returns whether the choices are multiselect. + */ + public function isMultiselect(): bool + { + return $this->multiselect; + } + + /** + * Gets the prompt for choices. + */ + public function getPrompt(): string + { + return $this->prompt; + } + + /** + * Sets the prompt for choices. + * + * @return $this + */ + public function setPrompt(string $prompt): static + { + $this->prompt = $prompt; + + return $this; + } + + /** + * Sets the error message for invalid values. + * + * The error message has a string placeholder (%s) for the invalid value. + * + * @return $this + */ + public function setErrorMessage(string $errorMessage): static + { + $this->errorMessage = $errorMessage; + $this->setValidator($this->getDefaultValidator()); + + return $this; + } + + private function getDefaultValidator(): callable + { + $choices = $this->choices; + $errorMessage = $this->errorMessage; + $multiselect = $this->multiselect; + $isAssoc = $this->isAssoc($choices); + + return function ($selected) use ($choices, $errorMessage, $multiselect, $isAssoc) { + if ($multiselect) { + // Check for a separated comma values + if (!preg_match('/^[^,]+(?:,[^,]+)*$/', (string) $selected, $matches)) { + throw new InvalidArgumentException(sprintf($errorMessage, $selected)); + } + + $selectedChoices = explode(',', (string) $selected); + } else { + $selectedChoices = [$selected]; + } + + if ($this->isTrimmable()) { + foreach ($selectedChoices as $k => $v) { + $selectedChoices[$k] = trim((string) $v); + } + } + + $multiselectChoices = []; + foreach ($selectedChoices as $value) { + $results = []; + foreach ($choices as $key => $choice) { + if ($choice === $value) { + $results[] = $key; + } + } + + if (\count($results) > 1) { + throw new InvalidArgumentException(sprintf('The provided answer is ambiguous. Value should be one of "%s".', implode('" or "', $results))); + } + + $result = array_search($value, $choices); + + if (!$isAssoc) { + if (false !== $result) { + $result = $choices[$result]; + } elseif (isset($choices[$value])) { + $result = $choices[$value]; + } + } elseif (false === $result && isset($choices[$value])) { + $result = $value; + } + + if (false === $result) { + throw new InvalidArgumentException(sprintf($errorMessage, $value)); + } + + // For associative choices, consistently return the key as string: + $multiselectChoices[] = $isAssoc ? (string) $result : $result; + } + + if ($multiselect) { + return $multiselectChoices; + } + + return current($multiselectChoices); + }; + } +} diff --git a/3rdparty/symfony/console/Question/ConfirmationQuestion.php b/3rdparty/symfony/console/Question/ConfirmationQuestion.php new file mode 100644 index 00000000..40eab242 --- /dev/null +++ b/3rdparty/symfony/console/Question/ConfirmationQuestion.php @@ -0,0 +1,57 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Component\Console\Question; + +/** + * Represents a yes/no question. + * + * @author Fabien Potencier + */ +class ConfirmationQuestion extends Question +{ + private string $trueAnswerRegex; + + /** + * @param string $question The question to ask to the user + * @param bool $default The default answer to return, true or false + * @param string $trueAnswerRegex A regex to match the "yes" answer + */ + public function __construct(string $question, bool $default = true, string $trueAnswerRegex = '/^y/i') + { + parent::__construct($question, $default); + + $this->trueAnswerRegex = $trueAnswerRegex; + $this->setNormalizer($this->getDefaultNormalizer()); + } + + /** + * Returns the default answer normalizer. + */ + private function getDefaultNormalizer(): callable + { + $default = $this->getDefault(); + $regex = $this->trueAnswerRegex; + + return function ($answer) use ($default, $regex) { + if (\is_bool($answer)) { + return $answer; + } + + $answerIsTrue = (bool) preg_match($regex, $answer); + if (false === $default) { + return $answer && $answerIsTrue; + } + + return '' === $answer || $answerIsTrue; + }; + } +} diff --git a/3rdparty/symfony/console/Question/Question.php b/3rdparty/symfony/console/Question/Question.php new file mode 100644 index 00000000..94c688fa --- /dev/null +++ b/3rdparty/symfony/console/Question/Question.php @@ -0,0 +1,291 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Component\Console\Question; + +use Symfony\Component\Console\Exception\InvalidArgumentException; +use Symfony\Component\Console\Exception\LogicException; + +/** + * Represents a Question. + * + * @author Fabien Potencier + */ +class Question +{ + private string $question; + private ?int $attempts = null; + private bool $hidden = false; + private bool $hiddenFallback = true; + private ?\Closure $autocompleterCallback = null; + private ?\Closure $validator = null; + private string|int|bool|null|float $default; + private ?\Closure $normalizer = null; + private bool $trimmable = true; + private bool $multiline = false; + + /** + * @param string $question The question to ask to the user + * @param string|bool|int|float|null $default The default answer to return if the user enters nothing + */ + public function __construct(string $question, string|bool|int|float|null $default = null) + { + $this->question = $question; + $this->default = $default; + } + + /** + * Returns the question. + */ + public function getQuestion(): string + { + return $this->question; + } + + /** + * Returns the default answer. + */ + public function getDefault(): string|bool|int|float|null + { + return $this->default; + } + + /** + * Returns whether the user response accepts newline characters. + */ + public function isMultiline(): bool + { + return $this->multiline; + } + + /** + * Sets whether the user response should accept newline characters. + * + * @return $this + */ + public function setMultiline(bool $multiline): static + { + $this->multiline = $multiline; + + return $this; + } + + /** + * Returns whether the user response must be hidden. + */ + public function isHidden(): bool + { + return $this->hidden; + } + + /** + * Sets whether the user response must be hidden or not. + * + * @return $this + * + * @throws LogicException In case the autocompleter is also used + */ + public function setHidden(bool $hidden): static + { + if ($this->autocompleterCallback) { + throw new LogicException('A hidden question cannot use the autocompleter.'); + } + + $this->hidden = $hidden; + + return $this; + } + + /** + * In case the response cannot be hidden, whether to fallback on non-hidden question or not. + */ + public function isHiddenFallback(): bool + { + return $this->hiddenFallback; + } + + /** + * Sets whether to fallback on non-hidden question if the response cannot be hidden. + * + * @return $this + */ + public function setHiddenFallback(bool $fallback): static + { + $this->hiddenFallback = $fallback; + + return $this; + } + + /** + * Gets values for the autocompleter. + */ + public function getAutocompleterValues(): ?iterable + { + $callback = $this->getAutocompleterCallback(); + + return $callback ? $callback('') : null; + } + + /** + * Sets values for the autocompleter. + * + * @return $this + * + * @throws LogicException + */ + public function setAutocompleterValues(?iterable $values): static + { + if (\is_array($values)) { + $values = $this->isAssoc($values) ? array_merge(array_keys($values), array_values($values)) : array_values($values); + + $callback = static fn () => $values; + } elseif ($values instanceof \Traversable) { + $callback = static function () use ($values) { + static $valueCache; + + return $valueCache ??= iterator_to_array($values, false); + }; + } else { + $callback = null; + } + + return $this->setAutocompleterCallback($callback); + } + + /** + * Gets the callback function used for the autocompleter. + */ + public function getAutocompleterCallback(): ?callable + { + return $this->autocompleterCallback; + } + + /** + * Sets the callback function used for the autocompleter. + * + * The callback is passed the user input as argument and should return an iterable of corresponding suggestions. + * + * @return $this + */ + public function setAutocompleterCallback(?callable $callback = null): static + { + if (1 > \func_num_args()) { + trigger_deprecation('symfony/console', '6.2', 'Calling "%s()" without any arguments is deprecated, pass null explicitly instead.', __METHOD__); + } + if ($this->hidden && null !== $callback) { + throw new LogicException('A hidden question cannot use the autocompleter.'); + } + + $this->autocompleterCallback = null === $callback ? null : $callback(...); + + return $this; + } + + /** + * Sets a validator for the question. + * + * @return $this + */ + public function setValidator(?callable $validator = null): static + { + if (1 > \func_num_args()) { + trigger_deprecation('symfony/console', '6.2', 'Calling "%s()" without any arguments is deprecated, pass null explicitly instead.', __METHOD__); + } + $this->validator = null === $validator ? null : $validator(...); + + return $this; + } + + /** + * Gets the validator for the question. + */ + public function getValidator(): ?callable + { + return $this->validator; + } + + /** + * Sets the maximum number of attempts. + * + * Null means an unlimited number of attempts. + * + * @return $this + * + * @throws InvalidArgumentException in case the number of attempts is invalid + */ + public function setMaxAttempts(?int $attempts): static + { + if (null !== $attempts && $attempts < 1) { + throw new InvalidArgumentException('Maximum number of attempts must be a positive value.'); + } + + $this->attempts = $attempts; + + return $this; + } + + /** + * Gets the maximum number of attempts. + * + * Null means an unlimited number of attempts. + */ + public function getMaxAttempts(): ?int + { + return $this->attempts; + } + + /** + * Sets a normalizer for the response. + * + * The normalizer can be a callable (a string), a closure or a class implementing __invoke. + * + * @return $this + */ + public function setNormalizer(callable $normalizer): static + { + $this->normalizer = $normalizer(...); + + return $this; + } + + /** + * Gets the normalizer for the response. + * + * The normalizer can ba a callable (a string), a closure or a class implementing __invoke. + */ + public function getNormalizer(): ?callable + { + return $this->normalizer; + } + + /** + * @return bool + */ + protected function isAssoc(array $array) + { + return (bool) \count(array_filter(array_keys($array), 'is_string')); + } + + public function isTrimmable(): bool + { + return $this->trimmable; + } + + /** + * @return $this + */ + public function setTrimmable(bool $trimmable): static + { + $this->trimmable = $trimmable; + + return $this; + } +} diff --git a/3rdparty/symfony/console/Resources/completion.bash b/3rdparty/symfony/console/Resources/completion.bash new file mode 100644 index 00000000..64c6a338 --- /dev/null +++ b/3rdparty/symfony/console/Resources/completion.bash @@ -0,0 +1,94 @@ +# This file is part of the Symfony package. +# +# (c) Fabien Potencier +# +# For the full copyright and license information, please view +# https://symfony.com/doc/current/contributing/code/license.html + +_sf_{{ COMMAND_NAME }}() { + + # Use the default completion for shell redirect operators. + for w in '>' '>>' '&>' '<'; do + if [[ $w = "${COMP_WORDS[COMP_CWORD-1]}" ]]; then + compopt -o filenames + COMPREPLY=($(compgen -f -- "${COMP_WORDS[COMP_CWORD]}")) + return 0 + fi + done + + # Use newline as only separator to allow space in completion values + local IFS=$'\n' + local sf_cmd="${COMP_WORDS[0]}" + + # for an alias, get the real script behind it + sf_cmd_type=$(type -t $sf_cmd) + if [[ $sf_cmd_type == "alias" ]]; then + sf_cmd=$(alias $sf_cmd | sed -E "s/alias $sf_cmd='(.*)'/\1/") + elif [[ $sf_cmd_type == "file" ]]; then + sf_cmd=$(type -p $sf_cmd) + fi + + if [[ $sf_cmd_type != "function" && ! -x $sf_cmd ]]; then + return 1 + fi + + local cur prev words cword + _get_comp_words_by_ref -n := cur prev words cword + + local completecmd=("$sf_cmd" "_complete" "--no-interaction" "-sbash" "-c$cword" "-a{{ VERSION }}") + for w in ${words[@]}; do + w=$(printf -- '%b' "$w") + # remove quotes from typed values + quote="${w:0:1}" + if [ "$quote" == \' ]; then + w="${w%\'}" + w="${w#\'}" + elif [ "$quote" == \" ]; then + w="${w%\"}" + w="${w#\"}" + fi + # empty values are ignored + if [ ! -z "$w" ]; then + completecmd+=("-i$w") + fi + done + + local sfcomplete + if sfcomplete=$(${completecmd[@]} 2>&1); then + local quote suggestions + quote=${cur:0:1} + + # Use single quotes by default if suggestions contains backslash (FQCN) + if [ "$quote" == '' ] && [[ "$sfcomplete" =~ \\ ]]; then + quote=\' + fi + + if [ "$quote" == \' ]; then + # single quotes: no additional escaping (does not accept ' in values) + suggestions=$(for s in $sfcomplete; do printf $'%q%q%q\n' "$quote" "$s" "$quote"; done) + elif [ "$quote" == \" ]; then + # double quotes: double escaping for \ $ ` " + suggestions=$(for s in $sfcomplete; do + s=${s//\\/\\\\} + s=${s//\$/\\\$} + s=${s//\`/\\\`} + s=${s//\"/\\\"} + printf $'%q%q%q\n' "$quote" "$s" "$quote"; + done) + else + # no quotes: double escaping + suggestions=$(for s in $sfcomplete; do printf $'%q\n' $(printf '%q' "$s"); done) + fi + COMPREPLY=($(IFS=$'\n' compgen -W "$suggestions" -- $(printf -- "%q" "$cur"))) + __ltrim_colon_completions "$cur" + else + if [[ "$sfcomplete" != *"Command \"_complete\" is not defined."* ]]; then + >&2 echo + >&2 echo $sfcomplete + fi + + return 1 + fi +} + +complete -F _sf_{{ COMMAND_NAME }} {{ COMMAND_NAME }} diff --git a/3rdparty/symfony/console/Resources/completion.fish b/3rdparty/symfony/console/Resources/completion.fish new file mode 100644 index 00000000..1c34292a --- /dev/null +++ b/3rdparty/symfony/console/Resources/completion.fish @@ -0,0 +1,29 @@ +# This file is part of the Symfony package. +# +# (c) Fabien Potencier +# +# For the full copyright and license information, please view +# https://symfony.com/doc/current/contributing/code/license.html + +function _sf_{{ COMMAND_NAME }} + set sf_cmd (commandline -o) + set c (count (commandline -oc)) + + set completecmd "$sf_cmd[1]" "_complete" "--no-interaction" "-sfish" "-a{{ VERSION }}" + + for i in $sf_cmd + if [ $i != "" ] + set completecmd $completecmd "-i$i" + end + end + + set completecmd $completecmd "-c$c" + + set sfcomplete ($completecmd) + + for i in $sfcomplete + echo $i + end +end + +complete -c '{{ COMMAND_NAME }}' -a '(_sf_{{ COMMAND_NAME }})' -f diff --git a/3rdparty/symfony/console/Resources/completion.zsh b/3rdparty/symfony/console/Resources/completion.zsh new file mode 100644 index 00000000..ff76fe5f --- /dev/null +++ b/3rdparty/symfony/console/Resources/completion.zsh @@ -0,0 +1,82 @@ +#compdef {{ COMMAND_NAME }} + +# This file is part of the Symfony package. +# +# (c) Fabien Potencier +# +# For the full copyright and license information, please view +# https://symfony.com/doc/current/contributing/code/license.html + +# +# zsh completions for {{ COMMAND_NAME }} +# +# References: +# - https://github.com/spf13/cobra/blob/master/zsh_completions.go +# - https://github.com/symfony/symfony/blob/5.4/src/Symfony/Component/Console/Resources/completion.bash +# +_sf_{{ COMMAND_NAME }}() { + local lastParam flagPrefix requestComp out comp + local -a completions + + # The user could have moved the cursor backwards on the command-line. + # We need to trigger completion from the $CURRENT location, so we need + # to truncate the command-line ($words) up to the $CURRENT location. + # (We cannot use $CURSOR as its value does not work when a command is an alias.) + words=("${=words[1,CURRENT]}") lastParam=${words[-1]} + + # For zsh, when completing a flag with an = (e.g., {{ COMMAND_NAME }} -n=) + # completions must be prefixed with the flag + setopt local_options BASH_REMATCH + if [[ "${lastParam}" =~ '-.*=' ]]; then + # We are dealing with a flag with an = + flagPrefix="-P ${BASH_REMATCH}" + fi + + # Prepare the command to obtain completions + requestComp="${words[0]} ${words[1]} _complete --no-interaction -szsh -a{{ VERSION }} -c$((CURRENT-1))" i="" + for w in ${words[@]}; do + w=$(printf -- '%b' "$w") + # remove quotes from typed values + quote="${w:0:1}" + if [ "$quote" = \' ]; then + w="${w%\'}" + w="${w#\'}" + elif [ "$quote" = \" ]; then + w="${w%\"}" + w="${w#\"}" + fi + # empty values are ignored + if [ ! -z "$w" ]; then + i="${i}-i${w} " + fi + done + + # Ensure at least 1 input + if [ "${i}" = "" ]; then + requestComp="${requestComp} -i\" \"" + else + requestComp="${requestComp} ${i}" + fi + + # Use eval to handle any environment variables and such + out=$(eval ${requestComp} 2>/dev/null) + + while IFS='\n' read -r comp; do + if [ -n "$comp" ]; then + # If requested, completions are returned with a description. + # The description is preceded by a TAB character. + # For zsh's _describe, we need to use a : instead of a TAB. + # We first need to escape any : as part of the completion itself. + comp=${comp//:/\\:} + local tab=$(printf '\t') + comp=${comp//$tab/:} + completions+=${comp} + fi + done < <(printf "%s\n" "${out[@]}") + + # Let inbuilt _describe handle completions + eval _describe "completions" completions $flagPrefix + return $? +} + +compdef _sf_{{ COMMAND_NAME }} {{ COMMAND_NAME }} diff --git a/3rdparty/symfony/console/SignalRegistry/SignalMap.php b/3rdparty/symfony/console/SignalRegistry/SignalMap.php new file mode 100644 index 00000000..de419bda --- /dev/null +++ b/3rdparty/symfony/console/SignalRegistry/SignalMap.php @@ -0,0 +1,36 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Component\Console\SignalRegistry; + +/** + * @author Grégoire Pineau + */ +class SignalMap +{ + private static array $map; + + public static function getSignalName(int $signal): ?string + { + if (!\extension_loaded('pcntl')) { + return null; + } + + if (!isset(self::$map)) { + $r = new \ReflectionExtension('pcntl'); + $c = $r->getConstants(); + $map = array_filter($c, fn ($k) => str_starts_with($k, 'SIG') && !str_starts_with($k, 'SIG_'), \ARRAY_FILTER_USE_KEY); + self::$map = array_flip($map); + } + + return self::$map[$signal] ?? null; + } +} diff --git a/3rdparty/symfony/console/SignalRegistry/SignalRegistry.php b/3rdparty/symfony/console/SignalRegistry/SignalRegistry.php new file mode 100644 index 00000000..ef2e5f04 --- /dev/null +++ b/3rdparty/symfony/console/SignalRegistry/SignalRegistry.php @@ -0,0 +1,57 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Component\Console\SignalRegistry; + +final class SignalRegistry +{ + private array $signalHandlers = []; + + public function __construct() + { + if (\function_exists('pcntl_async_signals')) { + pcntl_async_signals(true); + } + } + + public function register(int $signal, callable $signalHandler): void + { + if (!isset($this->signalHandlers[$signal])) { + $previousCallback = pcntl_signal_get_handler($signal); + + if (\is_callable($previousCallback)) { + $this->signalHandlers[$signal][] = $previousCallback; + } + } + + $this->signalHandlers[$signal][] = $signalHandler; + + pcntl_signal($signal, $this->handle(...)); + } + + public static function isSupported(): bool + { + return \function_exists('pcntl_signal'); + } + + /** + * @internal + */ + public function handle(int $signal): void + { + $count = \count($this->signalHandlers[$signal]); + + foreach ($this->signalHandlers[$signal] as $i => $signalHandler) { + $hasNext = $i !== $count - 1; + $signalHandler($signal, $hasNext); + } + } +} diff --git a/3rdparty/symfony/console/SingleCommandApplication.php b/3rdparty/symfony/console/SingleCommandApplication.php new file mode 100644 index 00000000..ff1c1724 --- /dev/null +++ b/3rdparty/symfony/console/SingleCommandApplication.php @@ -0,0 +1,72 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Component\Console; + +use Symfony\Component\Console\Command\Command; +use Symfony\Component\Console\Input\InputInterface; +use Symfony\Component\Console\Output\OutputInterface; + +/** + * @author Grégoire Pineau + */ +class SingleCommandApplication extends Command +{ + private string $version = 'UNKNOWN'; + private bool $autoExit = true; + private bool $running = false; + + /** + * @return $this + */ + public function setVersion(string $version): static + { + $this->version = $version; + + return $this; + } + + /** + * @final + * + * @return $this + */ + public function setAutoExit(bool $autoExit): static + { + $this->autoExit = $autoExit; + + return $this; + } + + public function run(?InputInterface $input = null, ?OutputInterface $output = null): int + { + if ($this->running) { + return parent::run($input, $output); + } + + // We use the command name as the application name + $application = new Application($this->getName() ?: 'UNKNOWN', $this->version); + $application->setAutoExit($this->autoExit); + // Fix the usage of the command displayed with "--help" + $this->setName($_SERVER['argv'][0]); + $application->add($this); + $application->setDefaultCommand($this->getName(), true); + + $this->running = true; + try { + $ret = $application->run($input, $output); + } finally { + $this->running = false; + } + + return $ret ?? 1; + } +} diff --git a/3rdparty/symfony/console/Style/OutputStyle.php b/3rdparty/symfony/console/Style/OutputStyle.php new file mode 100644 index 00000000..ddfa8dec --- /dev/null +++ b/3rdparty/symfony/console/Style/OutputStyle.php @@ -0,0 +1,132 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Component\Console\Style; + +use Symfony\Component\Console\Formatter\OutputFormatterInterface; +use Symfony\Component\Console\Helper\ProgressBar; +use Symfony\Component\Console\Output\ConsoleOutputInterface; +use Symfony\Component\Console\Output\OutputInterface; + +/** + * Decorates output to add console style guide helpers. + * + * @author Kevin Bond + */ +abstract class OutputStyle implements OutputInterface, StyleInterface +{ + private OutputInterface $output; + + public function __construct(OutputInterface $output) + { + $this->output = $output; + } + + /** + * @return void + */ + public function newLine(int $count = 1) + { + $this->output->write(str_repeat(\PHP_EOL, $count)); + } + + public function createProgressBar(int $max = 0): ProgressBar + { + return new ProgressBar($this->output, $max); + } + + /** + * @return void + */ + public function write(string|iterable $messages, bool $newline = false, int $type = self::OUTPUT_NORMAL) + { + $this->output->write($messages, $newline, $type); + } + + /** + * @return void + */ + public function writeln(string|iterable $messages, int $type = self::OUTPUT_NORMAL) + { + $this->output->writeln($messages, $type); + } + + /** + * @return void + */ + public function setVerbosity(int $level) + { + $this->output->setVerbosity($level); + } + + public function getVerbosity(): int + { + return $this->output->getVerbosity(); + } + + /** + * @return void + */ + public function setDecorated(bool $decorated) + { + $this->output->setDecorated($decorated); + } + + public function isDecorated(): bool + { + return $this->output->isDecorated(); + } + + /** + * @return void + */ + public function setFormatter(OutputFormatterInterface $formatter) + { + $this->output->setFormatter($formatter); + } + + public function getFormatter(): OutputFormatterInterface + { + return $this->output->getFormatter(); + } + + public function isQuiet(): bool + { + return $this->output->isQuiet(); + } + + public function isVerbose(): bool + { + return $this->output->isVerbose(); + } + + public function isVeryVerbose(): bool + { + return $this->output->isVeryVerbose(); + } + + public function isDebug(): bool + { + return $this->output->isDebug(); + } + + /** + * @return OutputInterface + */ + protected function getErrorOutput() + { + if (!$this->output instanceof ConsoleOutputInterface) { + return $this->output; + } + + return $this->output->getErrorOutput(); + } +} diff --git a/3rdparty/symfony/console/Style/StyleInterface.php b/3rdparty/symfony/console/Style/StyleInterface.php new file mode 100644 index 00000000..6bced158 --- /dev/null +++ b/3rdparty/symfony/console/Style/StyleInterface.php @@ -0,0 +1,138 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Component\Console\Style; + +/** + * Output style helpers. + * + * @author Kevin Bond + */ +interface StyleInterface +{ + /** + * Formats a command title. + * + * @return void + */ + public function title(string $message); + + /** + * Formats a section title. + * + * @return void + */ + public function section(string $message); + + /** + * Formats a list. + * + * @return void + */ + public function listing(array $elements); + + /** + * Formats informational text. + * + * @return void + */ + public function text(string|array $message); + + /** + * Formats a success result bar. + * + * @return void + */ + public function success(string|array $message); + + /** + * Formats an error result bar. + * + * @return void + */ + public function error(string|array $message); + + /** + * Formats an warning result bar. + * + * @return void + */ + public function warning(string|array $message); + + /** + * Formats a note admonition. + * + * @return void + */ + public function note(string|array $message); + + /** + * Formats a caution admonition. + * + * @return void + */ + public function caution(string|array $message); + + /** + * Formats a table. + * + * @return void + */ + public function table(array $headers, array $rows); + + /** + * Asks a question. + */ + public function ask(string $question, ?string $default = null, ?callable $validator = null): mixed; + + /** + * Asks a question with the user input hidden. + */ + public function askHidden(string $question, ?callable $validator = null): mixed; + + /** + * Asks for confirmation. + */ + public function confirm(string $question, bool $default = true): bool; + + /** + * Asks a choice question. + */ + public function choice(string $question, array $choices, mixed $default = null): mixed; + + /** + * Add newline(s). + * + * @return void + */ + public function newLine(int $count = 1); + + /** + * Starts the progress output. + * + * @return void + */ + public function progressStart(int $max = 0); + + /** + * Advances the progress output X steps. + * + * @return void + */ + public function progressAdvance(int $step = 1); + + /** + * Finishes the progress output. + * + * @return void + */ + public function progressFinish(); +} diff --git a/3rdparty/symfony/console/Style/SymfonyStyle.php b/3rdparty/symfony/console/Style/SymfonyStyle.php new file mode 100644 index 00000000..03bda878 --- /dev/null +++ b/3rdparty/symfony/console/Style/SymfonyStyle.php @@ -0,0 +1,514 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Component\Console\Style; + +use Symfony\Component\Console\Exception\InvalidArgumentException; +use Symfony\Component\Console\Exception\RuntimeException; +use Symfony\Component\Console\Formatter\OutputFormatter; +use Symfony\Component\Console\Helper\Helper; +use Symfony\Component\Console\Helper\OutputWrapper; +use Symfony\Component\Console\Helper\ProgressBar; +use Symfony\Component\Console\Helper\SymfonyQuestionHelper; +use Symfony\Component\Console\Helper\Table; +use Symfony\Component\Console\Helper\TableCell; +use Symfony\Component\Console\Helper\TableSeparator; +use Symfony\Component\Console\Input\InputInterface; +use Symfony\Component\Console\Output\ConsoleOutputInterface; +use Symfony\Component\Console\Output\ConsoleSectionOutput; +use Symfony\Component\Console\Output\OutputInterface; +use Symfony\Component\Console\Output\TrimmedBufferOutput; +use Symfony\Component\Console\Question\ChoiceQuestion; +use Symfony\Component\Console\Question\ConfirmationQuestion; +use Symfony\Component\Console\Question\Question; +use Symfony\Component\Console\Terminal; + +/** + * Output decorator helpers for the Symfony Style Guide. + * + * @author Kevin Bond + */ +class SymfonyStyle extends OutputStyle +{ + public const MAX_LINE_LENGTH = 120; + + private InputInterface $input; + private OutputInterface $output; + private SymfonyQuestionHelper $questionHelper; + private ProgressBar $progressBar; + private int $lineLength; + private TrimmedBufferOutput $bufferedOutput; + + public function __construct(InputInterface $input, OutputInterface $output) + { + $this->input = $input; + $this->bufferedOutput = new TrimmedBufferOutput(\DIRECTORY_SEPARATOR === '\\' ? 4 : 2, $output->getVerbosity(), false, clone $output->getFormatter()); + // Windows cmd wraps lines as soon as the terminal width is reached, whether there are following chars or not. + $width = (new Terminal())->getWidth() ?: self::MAX_LINE_LENGTH; + $this->lineLength = min($width - (int) (\DIRECTORY_SEPARATOR === '\\'), self::MAX_LINE_LENGTH); + + parent::__construct($this->output = $output); + } + + /** + * Formats a message as a block of text. + * + * @return void + */ + public function block(string|array $messages, ?string $type = null, ?string $style = null, string $prefix = ' ', bool $padding = false, bool $escape = true) + { + $messages = \is_array($messages) ? array_values($messages) : [$messages]; + + $this->autoPrependBlock(); + $this->writeln($this->createBlock($messages, $type, $style, $prefix, $padding, $escape)); + $this->newLine(); + } + + /** + * @return void + */ + public function title(string $message) + { + $this->autoPrependBlock(); + $this->writeln([ + sprintf('%s', OutputFormatter::escapeTrailingBackslash($message)), + sprintf('%s', str_repeat('=', Helper::width(Helper::removeDecoration($this->getFormatter(), $message)))), + ]); + $this->newLine(); + } + + /** + * @return void + */ + public function section(string $message) + { + $this->autoPrependBlock(); + $this->writeln([ + sprintf('%s', OutputFormatter::escapeTrailingBackslash($message)), + sprintf('%s', str_repeat('-', Helper::width(Helper::removeDecoration($this->getFormatter(), $message)))), + ]); + $this->newLine(); + } + + /** + * @return void + */ + public function listing(array $elements) + { + $this->autoPrependText(); + $elements = array_map(fn ($element) => sprintf(' * %s', $element), $elements); + + $this->writeln($elements); + $this->newLine(); + } + + /** + * @return void + */ + public function text(string|array $message) + { + $this->autoPrependText(); + + $messages = \is_array($message) ? array_values($message) : [$message]; + foreach ($messages as $message) { + $this->writeln(sprintf(' %s', $message)); + } + } + + /** + * Formats a command comment. + * + * @return void + */ + public function comment(string|array $message) + { + $this->block($message, null, null, ' // ', false, false); + } + + /** + * @return void + */ + public function success(string|array $message) + { + $this->block($message, 'OK', 'fg=black;bg=green', ' ', true); + } + + /** + * @return void + */ + public function error(string|array $message) + { + $this->block($message, 'ERROR', 'fg=white;bg=red', ' ', true); + } + + /** + * @return void + */ + public function warning(string|array $message) + { + $this->block($message, 'WARNING', 'fg=black;bg=yellow', ' ', true); + } + + /** + * @return void + */ + public function note(string|array $message) + { + $this->block($message, 'NOTE', 'fg=yellow', ' ! '); + } + + /** + * Formats an info message. + * + * @return void + */ + public function info(string|array $message) + { + $this->block($message, 'INFO', 'fg=green', ' ', true); + } + + /** + * @return void + */ + public function caution(string|array $message) + { + $this->block($message, 'CAUTION', 'fg=white;bg=red', ' ! ', true); + } + + /** + * @return void + */ + public function table(array $headers, array $rows) + { + $this->createTable() + ->setHeaders($headers) + ->setRows($rows) + ->render() + ; + + $this->newLine(); + } + + /** + * Formats a horizontal table. + * + * @return void + */ + public function horizontalTable(array $headers, array $rows) + { + $this->createTable() + ->setHorizontal(true) + ->setHeaders($headers) + ->setRows($rows) + ->render() + ; + + $this->newLine(); + } + + /** + * Formats a list of key/value horizontally. + * + * Each row can be one of: + * * 'A title' + * * ['key' => 'value'] + * * new TableSeparator() + * + * @return void + */ + public function definitionList(string|array|TableSeparator ...$list) + { + $headers = []; + $row = []; + foreach ($list as $value) { + if ($value instanceof TableSeparator) { + $headers[] = $value; + $row[] = $value; + continue; + } + if (\is_string($value)) { + $headers[] = new TableCell($value, ['colspan' => 2]); + $row[] = null; + continue; + } + if (!\is_array($value)) { + throw new InvalidArgumentException('Value should be an array, string, or an instance of TableSeparator.'); + } + $headers[] = key($value); + $row[] = current($value); + } + + $this->horizontalTable($headers, [$row]); + } + + public function ask(string $question, ?string $default = null, ?callable $validator = null): mixed + { + $question = new Question($question, $default); + $question->setValidator($validator); + + return $this->askQuestion($question); + } + + public function askHidden(string $question, ?callable $validator = null): mixed + { + $question = new Question($question); + + $question->setHidden(true); + $question->setValidator($validator); + + return $this->askQuestion($question); + } + + public function confirm(string $question, bool $default = true): bool + { + return $this->askQuestion(new ConfirmationQuestion($question, $default)); + } + + public function choice(string $question, array $choices, mixed $default = null, bool $multiSelect = false): mixed + { + if (null !== $default) { + $values = array_flip($choices); + $default = $values[$default] ?? $default; + } + + $questionChoice = new ChoiceQuestion($question, $choices, $default); + $questionChoice->setMultiselect($multiSelect); + + return $this->askQuestion($questionChoice); + } + + /** + * @return void + */ + public function progressStart(int $max = 0) + { + $this->progressBar = $this->createProgressBar($max); + $this->progressBar->start(); + } + + /** + * @return void + */ + public function progressAdvance(int $step = 1) + { + $this->getProgressBar()->advance($step); + } + + /** + * @return void + */ + public function progressFinish() + { + $this->getProgressBar()->finish(); + $this->newLine(2); + unset($this->progressBar); + } + + public function createProgressBar(int $max = 0): ProgressBar + { + $progressBar = parent::createProgressBar($max); + + if ('\\' !== \DIRECTORY_SEPARATOR || 'Hyper' === getenv('TERM_PROGRAM')) { + $progressBar->setEmptyBarCharacter('░'); // light shade character \u2591 + $progressBar->setProgressCharacter(''); + $progressBar->setBarCharacter('▓'); // dark shade character \u2593 + } + + return $progressBar; + } + + /** + * @see ProgressBar::iterate() + * + * @template TKey + * @template TValue + * + * @param iterable $iterable + * @param int|null $max Number of steps to complete the bar (0 if indeterminate), if null it will be inferred from $iterable + * + * @return iterable + */ + public function progressIterate(iterable $iterable, ?int $max = null): iterable + { + yield from $this->createProgressBar()->iterate($iterable, $max); + + $this->newLine(2); + } + + public function askQuestion(Question $question): mixed + { + if ($this->input->isInteractive()) { + $this->autoPrependBlock(); + } + + $this->questionHelper ??= new SymfonyQuestionHelper(); + + $answer = $this->questionHelper->ask($this->input, $this, $question); + + if ($this->input->isInteractive()) { + if ($this->output instanceof ConsoleSectionOutput) { + // add the new line of the `return` to submit the input to ConsoleSectionOutput, because ConsoleSectionOutput is holding all it's lines. + // this is relevant when a `ConsoleSectionOutput::clear` is called. + $this->output->addNewLineOfInputSubmit(); + } + $this->newLine(); + $this->bufferedOutput->write("\n"); + } + + return $answer; + } + + /** + * @return void + */ + public function writeln(string|iterable $messages, int $type = self::OUTPUT_NORMAL) + { + if (!is_iterable($messages)) { + $messages = [$messages]; + } + + foreach ($messages as $message) { + parent::writeln($message, $type); + $this->writeBuffer($message, true, $type); + } + } + + /** + * @return void + */ + public function write(string|iterable $messages, bool $newline = false, int $type = self::OUTPUT_NORMAL) + { + if (!is_iterable($messages)) { + $messages = [$messages]; + } + + foreach ($messages as $message) { + parent::write($message, $newline, $type); + $this->writeBuffer($message, $newline, $type); + } + } + + /** + * @return void + */ + public function newLine(int $count = 1) + { + parent::newLine($count); + $this->bufferedOutput->write(str_repeat("\n", $count)); + } + + /** + * Returns a new instance which makes use of stderr if available. + */ + public function getErrorStyle(): self + { + return new self($this->input, $this->getErrorOutput()); + } + + public function createTable(): Table + { + $output = $this->output instanceof ConsoleOutputInterface ? $this->output->section() : $this->output; + $style = clone Table::getStyleDefinition('symfony-style-guide'); + $style->setCellHeaderFormat('%s'); + + return (new Table($output))->setStyle($style); + } + + private function getProgressBar(): ProgressBar + { + return $this->progressBar + ?? throw new RuntimeException('The ProgressBar is not started.'); + } + + private function autoPrependBlock(): void + { + $chars = substr(str_replace(\PHP_EOL, "\n", $this->bufferedOutput->fetch()), -2); + + if (!isset($chars[0])) { + $this->newLine(); // empty history, so we should start with a new line. + + return; + } + // Prepend new line for each non LF chars (This means no blank line was output before) + $this->newLine(2 - substr_count($chars, "\n")); + } + + private function autoPrependText(): void + { + $fetched = $this->bufferedOutput->fetch(); + // Prepend new line if last char isn't EOL: + if ($fetched && !str_ends_with($fetched, "\n")) { + $this->newLine(); + } + } + + private function writeBuffer(string $message, bool $newLine, int $type): void + { + // We need to know if the last chars are PHP_EOL + $this->bufferedOutput->write($message, $newLine, $type); + } + + private function createBlock(iterable $messages, ?string $type = null, ?string $style = null, string $prefix = ' ', bool $padding = false, bool $escape = false): array + { + $indentLength = 0; + $prefixLength = Helper::width(Helper::removeDecoration($this->getFormatter(), $prefix)); + $lines = []; + + if (null !== $type) { + $type = sprintf('[%s] ', $type); + $indentLength = Helper::width($type); + $lineIndentation = str_repeat(' ', $indentLength); + } + + // wrap and add newlines for each element + $outputWrapper = new OutputWrapper(); + foreach ($messages as $key => $message) { + if ($escape) { + $message = OutputFormatter::escape($message); + } + + $lines = array_merge( + $lines, + explode(\PHP_EOL, $outputWrapper->wrap( + $message, + $this->lineLength - $prefixLength - $indentLength, + \PHP_EOL + )) + ); + + if (\count($messages) > 1 && $key < \count($messages) - 1) { + $lines[] = ''; + } + } + + $firstLineIndex = 0; + if ($padding && $this->isDecorated()) { + $firstLineIndex = 1; + array_unshift($lines, ''); + $lines[] = ''; + } + + foreach ($lines as $i => &$line) { + if (null !== $type) { + $line = $firstLineIndex === $i ? $type.$line : $lineIndentation.$line; + } + + $line = $prefix.$line; + $line .= str_repeat(' ', max($this->lineLength - Helper::width(Helper::removeDecoration($this->getFormatter(), $line)), 0)); + + if ($style) { + $line = sprintf('<%s>%s', $style, $line); + } + } + + return $lines; + } +} diff --git a/3rdparty/symfony/console/Terminal.php b/3rdparty/symfony/console/Terminal.php new file mode 100644 index 00000000..f094aded --- /dev/null +++ b/3rdparty/symfony/console/Terminal.php @@ -0,0 +1,235 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Component\Console; + +use Symfony\Component\Console\Output\AnsiColorMode; + +class Terminal +{ + public const DEFAULT_COLOR_MODE = AnsiColorMode::Ansi4; + + private static ?AnsiColorMode $colorMode = null; + private static ?int $width = null; + private static ?int $height = null; + private static ?bool $stty = null; + + /** + * About Ansi color types: https://en.wikipedia.org/wiki/ANSI_escape_code#Colors + * For more information about true color support with terminals https://github.com/termstandard/colors/. + */ + public static function getColorMode(): AnsiColorMode + { + // Use Cache from previous run (or user forced mode) + if (null !== self::$colorMode) { + return self::$colorMode; + } + + // Try with $COLORTERM first + if (\is_string($colorterm = getenv('COLORTERM'))) { + $colorterm = strtolower($colorterm); + + if (str_contains($colorterm, 'truecolor')) { + self::setColorMode(AnsiColorMode::Ansi24); + + return self::$colorMode; + } + + if (str_contains($colorterm, '256color')) { + self::setColorMode(AnsiColorMode::Ansi8); + + return self::$colorMode; + } + } + + // Try with $TERM + if (\is_string($term = getenv('TERM'))) { + $term = strtolower($term); + + if (str_contains($term, 'truecolor')) { + self::setColorMode(AnsiColorMode::Ansi24); + + return self::$colorMode; + } + + if (str_contains($term, '256color')) { + self::setColorMode(AnsiColorMode::Ansi8); + + return self::$colorMode; + } + } + + self::setColorMode(self::DEFAULT_COLOR_MODE); + + return self::$colorMode; + } + + /** + * Force a terminal color mode rendering. + */ + public static function setColorMode(?AnsiColorMode $colorMode): void + { + self::$colorMode = $colorMode; + } + + /** + * Gets the terminal width. + */ + public function getWidth(): int + { + $width = getenv('COLUMNS'); + if (false !== $width) { + return (int) trim($width); + } + + if (null === self::$width) { + self::initDimensions(); + } + + return self::$width ?: 80; + } + + /** + * Gets the terminal height. + */ + public function getHeight(): int + { + $height = getenv('LINES'); + if (false !== $height) { + return (int) trim($height); + } + + if (null === self::$height) { + self::initDimensions(); + } + + return self::$height ?: 50; + } + + /** + * @internal + */ + public static function hasSttyAvailable(): bool + { + if (null !== self::$stty) { + return self::$stty; + } + + // skip check if shell_exec function is disabled + if (!\function_exists('shell_exec')) { + return false; + } + + return self::$stty = (bool) shell_exec('stty 2> '.('\\' === \DIRECTORY_SEPARATOR ? 'NUL' : '/dev/null')); + } + + private static function initDimensions(): void + { + if ('\\' === \DIRECTORY_SEPARATOR) { + $ansicon = getenv('ANSICON'); + if (false !== $ansicon && preg_match('/^(\d+)x(\d+)(?: \((\d+)x(\d+)\))?$/', trim($ansicon), $matches)) { + // extract [w, H] from "wxh (WxH)" + // or [w, h] from "wxh" + self::$width = (int) $matches[1]; + self::$height = isset($matches[4]) ? (int) $matches[4] : (int) $matches[2]; + } elseif (!self::hasVt100Support() && self::hasSttyAvailable()) { + // only use stty on Windows if the terminal does not support vt100 (e.g. Windows 7 + git-bash) + // testing for stty in a Windows 10 vt100-enabled console will implicitly disable vt100 support on STDOUT + self::initDimensionsUsingStty(); + } elseif (null !== $dimensions = self::getConsoleMode()) { + // extract [w, h] from "wxh" + self::$width = (int) $dimensions[0]; + self::$height = (int) $dimensions[1]; + } + } else { + self::initDimensionsUsingStty(); + } + } + + /** + * Returns whether STDOUT has vt100 support (some Windows 10+ configurations). + */ + private static function hasVt100Support(): bool + { + return \function_exists('sapi_windows_vt100_support') && sapi_windows_vt100_support(fopen('php://stdout', 'w')); + } + + /** + * Initializes dimensions using the output of an stty columns line. + */ + private static function initDimensionsUsingStty(): void + { + if ($sttyString = self::getSttyColumns()) { + if (preg_match('/rows.(\d+);.columns.(\d+);/is', $sttyString, $matches)) { + // extract [w, h] from "rows h; columns w;" + self::$width = (int) $matches[2]; + self::$height = (int) $matches[1]; + } elseif (preg_match('/;.(\d+).rows;.(\d+).columns/is', $sttyString, $matches)) { + // extract [w, h] from "; h rows; w columns" + self::$width = (int) $matches[2]; + self::$height = (int) $matches[1]; + } + } + } + + /** + * Runs and parses mode CON if it's available, suppressing any error output. + * + * @return int[]|null An array composed of the width and the height or null if it could not be parsed + */ + private static function getConsoleMode(): ?array + { + $info = self::readFromProcess('mode CON'); + + if (null === $info || !preg_match('/--------+\r?\n.+?(\d+)\r?\n.+?(\d+)\r?\n/', $info, $matches)) { + return null; + } + + return [(int) $matches[2], (int) $matches[1]]; + } + + /** + * Runs and parses stty -a if it's available, suppressing any error output. + */ + private static function getSttyColumns(): ?string + { + return self::readFromProcess(['stty', '-a']); + } + + private static function readFromProcess(string|array $command): ?string + { + if (!\function_exists('proc_open')) { + return null; + } + + $descriptorspec = [ + 1 => ['pipe', 'w'], + 2 => ['pipe', 'w'], + ]; + + $cp = \function_exists('sapi_windows_cp_set') ? sapi_windows_cp_get() : 0; + + if (!$process = @proc_open($command, $descriptorspec, $pipes, null, null, ['suppress_errors' => true])) { + return null; + } + + $info = stream_get_contents($pipes[1]); + fclose($pipes[1]); + fclose($pipes[2]); + proc_close($process); + + if ($cp) { + sapi_windows_cp_set($cp); + } + + return $info; + } +} diff --git a/3rdparty/symfony/console/Tester/ApplicationTester.php b/3rdparty/symfony/console/Tester/ApplicationTester.php new file mode 100644 index 00000000..58aee54d --- /dev/null +++ b/3rdparty/symfony/console/Tester/ApplicationTester.php @@ -0,0 +1,85 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Component\Console\Tester; + +use Symfony\Component\Console\Application; +use Symfony\Component\Console\Input\ArrayInput; + +/** + * Eases the testing of console applications. + * + * When testing an application, don't forget to disable the auto exit flag: + * + * $application = new Application(); + * $application->setAutoExit(false); + * + * @author Fabien Potencier + */ +class ApplicationTester +{ + use TesterTrait; + + private Application $application; + + public function __construct(Application $application) + { + $this->application = $application; + } + + /** + * Executes the application. + * + * Available options: + * + * * interactive: Sets the input interactive flag + * * decorated: Sets the output decorated flag + * * verbosity: Sets the output verbosity flag + * * capture_stderr_separately: Make output of stdOut and stdErr separately available + * + * @return int The command exit code + */ + public function run(array $input, array $options = []): int + { + $prevShellVerbosity = getenv('SHELL_VERBOSITY'); + + try { + $this->input = new ArrayInput($input); + if (isset($options['interactive'])) { + $this->input->setInteractive($options['interactive']); + } + + if ($this->inputs) { + $this->input->setStream(self::createStream($this->inputs)); + } + + $this->initOutput($options); + + return $this->statusCode = $this->application->run($this->input, $this->output); + } finally { + // SHELL_VERBOSITY is set by Application::configureIO so we need to unset/reset it + // to its previous value to avoid one test's verbosity to spread to the following tests + if (false === $prevShellVerbosity) { + if (\function_exists('putenv')) { + @putenv('SHELL_VERBOSITY'); + } + unset($_ENV['SHELL_VERBOSITY']); + unset($_SERVER['SHELL_VERBOSITY']); + } else { + if (\function_exists('putenv')) { + @putenv('SHELL_VERBOSITY='.$prevShellVerbosity); + } + $_ENV['SHELL_VERBOSITY'] = $prevShellVerbosity; + $_SERVER['SHELL_VERBOSITY'] = $prevShellVerbosity; + } + } + } +} diff --git a/3rdparty/symfony/console/Tester/CommandCompletionTester.php b/3rdparty/symfony/console/Tester/CommandCompletionTester.php new file mode 100644 index 00000000..a90fe52e --- /dev/null +++ b/3rdparty/symfony/console/Tester/CommandCompletionTester.php @@ -0,0 +1,56 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Component\Console\Tester; + +use Symfony\Component\Console\Command\Command; +use Symfony\Component\Console\Completion\CompletionInput; +use Symfony\Component\Console\Completion\CompletionSuggestions; + +/** + * Eases the testing of command completion. + * + * @author Jérôme Tamarelle + */ +class CommandCompletionTester +{ + private Command $command; + + public function __construct(Command $command) + { + $this->command = $command; + } + + /** + * Create completion suggestions from input tokens. + */ + public function complete(array $input): array + { + $currentIndex = \count($input); + if ('' === end($input)) { + array_pop($input); + } + array_unshift($input, $this->command->getName()); + + $completionInput = CompletionInput::fromTokens($input, $currentIndex); + $completionInput->bind($this->command->getDefinition()); + $suggestions = new CompletionSuggestions(); + + $this->command->complete($completionInput, $suggestions); + + $options = []; + foreach ($suggestions->getOptionSuggestions() as $option) { + $options[] = '--'.$option->getName(); + } + + return array_map('strval', array_merge($options, $suggestions->getValueSuggestions())); + } +} diff --git a/3rdparty/symfony/console/Tester/CommandTester.php b/3rdparty/symfony/console/Tester/CommandTester.php new file mode 100644 index 00000000..2ff813b7 --- /dev/null +++ b/3rdparty/symfony/console/Tester/CommandTester.php @@ -0,0 +1,76 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Component\Console\Tester; + +use Symfony\Component\Console\Command\Command; +use Symfony\Component\Console\Input\ArrayInput; + +/** + * Eases the testing of console commands. + * + * @author Fabien Potencier + * @author Robin Chalas + */ +class CommandTester +{ + use TesterTrait; + + private Command $command; + + public function __construct(Command $command) + { + $this->command = $command; + } + + /** + * Executes the command. + * + * Available execution options: + * + * * interactive: Sets the input interactive flag + * * decorated: Sets the output decorated flag + * * verbosity: Sets the output verbosity flag + * * capture_stderr_separately: Make output of stdOut and stdErr separately available + * + * @param array $input An array of command arguments and options + * @param array $options An array of execution options + * + * @return int The command exit code + */ + public function execute(array $input, array $options = []): int + { + // set the command name automatically if the application requires + // this argument and no command name was passed + if (!isset($input['command']) + && (null !== $application = $this->command->getApplication()) + && $application->getDefinition()->hasArgument('command') + ) { + $input = array_merge(['command' => $this->command->getName()], $input); + } + + $this->input = new ArrayInput($input); + // Use an in-memory input stream even if no inputs are set so that QuestionHelper::ask() does not rely on the blocking STDIN. + $this->input->setStream(self::createStream($this->inputs)); + + if (isset($options['interactive'])) { + $this->input->setInteractive($options['interactive']); + } + + if (!isset($options['decorated'])) { + $options['decorated'] = false; + } + + $this->initOutput($options); + + return $this->statusCode = $this->command->run($this->input, $this->output); + } +} diff --git a/3rdparty/symfony/console/Tester/Constraint/CommandIsSuccessful.php b/3rdparty/symfony/console/Tester/Constraint/CommandIsSuccessful.php new file mode 100644 index 00000000..09c6194b --- /dev/null +++ b/3rdparty/symfony/console/Tester/Constraint/CommandIsSuccessful.php @@ -0,0 +1,43 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Component\Console\Tester\Constraint; + +use PHPUnit\Framework\Constraint\Constraint; +use Symfony\Component\Console\Command\Command; + +final class CommandIsSuccessful extends Constraint +{ + public function toString(): string + { + return 'is successful'; + } + + protected function matches($other): bool + { + return Command::SUCCESS === $other; + } + + protected function failureDescription($other): string + { + return 'the command '.$this->toString(); + } + + protected function additionalFailureDescription($other): string + { + $mapping = [ + Command::FAILURE => 'Command failed.', + Command::INVALID => 'Command was invalid.', + ]; + + return $mapping[$other] ?? sprintf('Command returned exit status %d.', $other); + } +} diff --git a/3rdparty/symfony/console/Tester/TesterTrait.php b/3rdparty/symfony/console/Tester/TesterTrait.php new file mode 100644 index 00000000..1ab7a70a --- /dev/null +++ b/3rdparty/symfony/console/Tester/TesterTrait.php @@ -0,0 +1,178 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Component\Console\Tester; + +use PHPUnit\Framework\Assert; +use Symfony\Component\Console\Input\InputInterface; +use Symfony\Component\Console\Output\ConsoleOutput; +use Symfony\Component\Console\Output\OutputInterface; +use Symfony\Component\Console\Output\StreamOutput; +use Symfony\Component\Console\Tester\Constraint\CommandIsSuccessful; + +/** + * @author Amrouche Hamza + */ +trait TesterTrait +{ + private StreamOutput $output; + private array $inputs = []; + private bool $captureStreamsIndependently = false; + private InputInterface $input; + private int $statusCode; + + /** + * Gets the display returned by the last execution of the command or application. + * + * @throws \RuntimeException If it's called before the execute method + */ + public function getDisplay(bool $normalize = false): string + { + if (!isset($this->output)) { + throw new \RuntimeException('Output not initialized, did you execute the command before requesting the display?'); + } + + rewind($this->output->getStream()); + + $display = stream_get_contents($this->output->getStream()); + + if ($normalize) { + $display = str_replace(\PHP_EOL, "\n", $display); + } + + return $display; + } + + /** + * Gets the output written to STDERR by the application. + * + * @param bool $normalize Whether to normalize end of lines to \n or not + */ + public function getErrorOutput(bool $normalize = false): string + { + if (!$this->captureStreamsIndependently) { + throw new \LogicException('The error output is not available when the tester is run without "capture_stderr_separately" option set.'); + } + + rewind($this->output->getErrorOutput()->getStream()); + + $display = stream_get_contents($this->output->getErrorOutput()->getStream()); + + if ($normalize) { + $display = str_replace(\PHP_EOL, "\n", $display); + } + + return $display; + } + + /** + * Gets the input instance used by the last execution of the command or application. + */ + public function getInput(): InputInterface + { + return $this->input; + } + + /** + * Gets the output instance used by the last execution of the command or application. + */ + public function getOutput(): OutputInterface + { + return $this->output; + } + + /** + * Gets the status code returned by the last execution of the command or application. + * + * @throws \RuntimeException If it's called before the execute method + */ + public function getStatusCode(): int + { + return $this->statusCode ?? throw new \RuntimeException('Status code not initialized, did you execute the command before requesting the status code?'); + } + + public function assertCommandIsSuccessful(string $message = ''): void + { + Assert::assertThat($this->statusCode, new CommandIsSuccessful(), $message); + } + + /** + * Sets the user inputs. + * + * @param array $inputs An array of strings representing each input + * passed to the command input stream + * + * @return $this + */ + public function setInputs(array $inputs): static + { + $this->inputs = $inputs; + + return $this; + } + + /** + * Initializes the output property. + * + * Available options: + * + * * decorated: Sets the output decorated flag + * * verbosity: Sets the output verbosity flag + * * capture_stderr_separately: Make output of stdOut and stdErr separately available + */ + private function initOutput(array $options): void + { + $this->captureStreamsIndependently = $options['capture_stderr_separately'] ?? false; + if (!$this->captureStreamsIndependently) { + $this->output = new StreamOutput(fopen('php://memory', 'w', false)); + if (isset($options['decorated'])) { + $this->output->setDecorated($options['decorated']); + } + if (isset($options['verbosity'])) { + $this->output->setVerbosity($options['verbosity']); + } + } else { + $this->output = new ConsoleOutput( + $options['verbosity'] ?? ConsoleOutput::VERBOSITY_NORMAL, + $options['decorated'] ?? null + ); + + $errorOutput = new StreamOutput(fopen('php://memory', 'w', false)); + $errorOutput->setFormatter($this->output->getFormatter()); + $errorOutput->setVerbosity($this->output->getVerbosity()); + $errorOutput->setDecorated($this->output->isDecorated()); + + $reflectedOutput = new \ReflectionObject($this->output); + $strErrProperty = $reflectedOutput->getProperty('stderr'); + $strErrProperty->setValue($this->output, $errorOutput); + + $reflectedParent = $reflectedOutput->getParentClass(); + $streamProperty = $reflectedParent->getProperty('stream'); + $streamProperty->setValue($this->output, fopen('php://memory', 'w', false)); + } + } + + /** + * @return resource + */ + private static function createStream(array $inputs) + { + $stream = fopen('php://memory', 'r+', false); + + foreach ($inputs as $input) { + fwrite($stream, $input.\PHP_EOL); + } + + rewind($stream); + + return $stream; + } +} diff --git a/3rdparty/symfony/css-selector/CssSelectorConverter.php b/3rdparty/symfony/css-selector/CssSelectorConverter.php new file mode 100644 index 00000000..7120a295 --- /dev/null +++ b/3rdparty/symfony/css-selector/CssSelectorConverter.php @@ -0,0 +1,67 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Component\CssSelector; + +use Symfony\Component\CssSelector\Parser\Shortcut\ClassParser; +use Symfony\Component\CssSelector\Parser\Shortcut\ElementParser; +use Symfony\Component\CssSelector\Parser\Shortcut\EmptyStringParser; +use Symfony\Component\CssSelector\Parser\Shortcut\HashParser; +use Symfony\Component\CssSelector\XPath\Extension\HtmlExtension; +use Symfony\Component\CssSelector\XPath\Translator; + +/** + * CssSelectorConverter is the main entry point of the component and can convert CSS + * selectors to XPath expressions. + * + * @author Christophe Coevoet + */ +class CssSelectorConverter +{ + private Translator $translator; + private array $cache; + + private static array $xmlCache = []; + private static array $htmlCache = []; + + /** + * @param bool $html Whether HTML support should be enabled. Disable it for XML documents + */ + public function __construct(bool $html = true) + { + $this->translator = new Translator(); + + if ($html) { + $this->translator->registerExtension(new HtmlExtension($this->translator)); + $this->cache = &self::$htmlCache; + } else { + $this->cache = &self::$xmlCache; + } + + $this->translator + ->registerParserShortcut(new EmptyStringParser()) + ->registerParserShortcut(new ElementParser()) + ->registerParserShortcut(new ClassParser()) + ->registerParserShortcut(new HashParser()) + ; + } + + /** + * Translates a CSS expression to its XPath equivalent. + * + * Optionally, a prefix can be added to the resulting XPath + * expression with the $prefix parameter. + */ + public function toXPath(string $cssExpr, string $prefix = 'descendant-or-self::'): string + { + return $this->cache[$prefix][$cssExpr] ??= $this->translator->cssToXPath($cssExpr, $prefix); + } +} diff --git a/3rdparty/symfony/css-selector/Exception/ExceptionInterface.php b/3rdparty/symfony/css-selector/Exception/ExceptionInterface.php new file mode 100644 index 00000000..9e259006 --- /dev/null +++ b/3rdparty/symfony/css-selector/Exception/ExceptionInterface.php @@ -0,0 +1,24 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Component\CssSelector\Exception; + +/** + * Interface for exceptions. + * + * This component is a port of the Python cssselect library, + * which is copyright Ian Bicking, @see https://github.com/SimonSapin/cssselect. + * + * @author Jean-François Simon + */ +interface ExceptionInterface extends \Throwable +{ +} diff --git a/3rdparty/symfony/css-selector/Exception/ExpressionErrorException.php b/3rdparty/symfony/css-selector/Exception/ExpressionErrorException.php new file mode 100644 index 00000000..fd5deeab --- /dev/null +++ b/3rdparty/symfony/css-selector/Exception/ExpressionErrorException.php @@ -0,0 +1,24 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Component\CssSelector\Exception; + +/** + * ParseException is thrown when a CSS selector syntax is not valid. + * + * This component is a port of the Python cssselect library, + * which is copyright Ian Bicking, @see https://github.com/SimonSapin/cssselect. + * + * @author Jean-François Simon + */ +class ExpressionErrorException extends ParseException +{ +} diff --git a/3rdparty/symfony/css-selector/Exception/InternalErrorException.php b/3rdparty/symfony/css-selector/Exception/InternalErrorException.php new file mode 100644 index 00000000..e60e5ed0 --- /dev/null +++ b/3rdparty/symfony/css-selector/Exception/InternalErrorException.php @@ -0,0 +1,24 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Component\CssSelector\Exception; + +/** + * ParseException is thrown when a CSS selector syntax is not valid. + * + * This component is a port of the Python cssselect library, + * which is copyright Ian Bicking, @see https://github.com/SimonSapin/cssselect. + * + * @author Jean-François Simon + */ +class InternalErrorException extends ParseException +{ +} diff --git a/3rdparty/symfony/css-selector/Exception/ParseException.php b/3rdparty/symfony/css-selector/Exception/ParseException.php new file mode 100644 index 00000000..3b0b0ee8 --- /dev/null +++ b/3rdparty/symfony/css-selector/Exception/ParseException.php @@ -0,0 +1,24 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Component\CssSelector\Exception; + +/** + * ParseException is thrown when a CSS selector syntax is not valid. + * + * This component is a port of the Python cssselect library, + * which is copyright Ian Bicking, @see https://github.com/SimonSapin/cssselect. + * + * @author Fabien Potencier + */ +class ParseException extends \Exception implements ExceptionInterface +{ +} diff --git a/3rdparty/symfony/css-selector/Exception/SyntaxErrorException.php b/3rdparty/symfony/css-selector/Exception/SyntaxErrorException.php new file mode 100644 index 00000000..5a9d8078 --- /dev/null +++ b/3rdparty/symfony/css-selector/Exception/SyntaxErrorException.php @@ -0,0 +1,55 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Component\CssSelector\Exception; + +use Symfony\Component\CssSelector\Parser\Token; + +/** + * ParseException is thrown when a CSS selector syntax is not valid. + * + * This component is a port of the Python cssselect library, + * which is copyright Ian Bicking, @see https://github.com/SimonSapin/cssselect. + * + * @author Jean-François Simon + */ +class SyntaxErrorException extends ParseException +{ + public static function unexpectedToken(string $expectedValue, Token $foundToken): self + { + return new self(sprintf('Expected %s, but %s found.', $expectedValue, $foundToken)); + } + + public static function pseudoElementFound(string $pseudoElement, string $unexpectedLocation): self + { + return new self(sprintf('Unexpected pseudo-element "::%s" found %s.', $pseudoElement, $unexpectedLocation)); + } + + public static function unclosedString(int $position): self + { + return new self(sprintf('Unclosed/invalid string at %s.', $position)); + } + + public static function nestedNot(): self + { + return new self('Got nested ::not().'); + } + + public static function notAtTheStartOfASelector(string $pseudoElement): self + { + return new self(sprintf('Got immediate child pseudo-element ":%s" not at the start of a selector', $pseudoElement)); + } + + public static function stringAsFunctionArgument(): self + { + return new self('String not allowed as function argument.'); + } +} diff --git a/3rdparty/symfony/css-selector/LICENSE b/3rdparty/symfony/css-selector/LICENSE new file mode 100644 index 00000000..0138f8f0 --- /dev/null +++ b/3rdparty/symfony/css-selector/LICENSE @@ -0,0 +1,19 @@ +Copyright (c) 2004-present Fabien Potencier + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is furnished +to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +THE SOFTWARE. diff --git a/3rdparty/symfony/css-selector/Node/AbstractNode.php b/3rdparty/symfony/css-selector/Node/AbstractNode.php new file mode 100644 index 00000000..d99e80a8 --- /dev/null +++ b/3rdparty/symfony/css-selector/Node/AbstractNode.php @@ -0,0 +1,32 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Component\CssSelector\Node; + +/** + * Abstract base node class. + * + * This component is a port of the Python cssselect library, + * which is copyright Ian Bicking, @see https://github.com/SimonSapin/cssselect. + * + * @author Jean-François Simon + * + * @internal + */ +abstract class AbstractNode implements NodeInterface +{ + private string $nodeName; + + public function getNodeName(): string + { + return $this->nodeName ??= preg_replace('~.*\\\\([^\\\\]+)Node$~', '$1', static::class); + } +} diff --git a/3rdparty/symfony/css-selector/Node/AttributeNode.php b/3rdparty/symfony/css-selector/Node/AttributeNode.php new file mode 100644 index 00000000..ba4df310 --- /dev/null +++ b/3rdparty/symfony/css-selector/Node/AttributeNode.php @@ -0,0 +1,79 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Component\CssSelector\Node; + +/** + * Represents a "[| ]" node. + * + * This component is a port of the Python cssselect library, + * which is copyright Ian Bicking, @see https://github.com/SimonSapin/cssselect. + * + * @author Jean-François Simon + * + * @internal + */ +class AttributeNode extends AbstractNode +{ + private NodeInterface $selector; + private ?string $namespace; + private string $attribute; + private string $operator; + private ?string $value; + + public function __construct(NodeInterface $selector, ?string $namespace, string $attribute, string $operator, ?string $value) + { + $this->selector = $selector; + $this->namespace = $namespace; + $this->attribute = $attribute; + $this->operator = $operator; + $this->value = $value; + } + + public function getSelector(): NodeInterface + { + return $this->selector; + } + + public function getNamespace(): ?string + { + return $this->namespace; + } + + public function getAttribute(): string + { + return $this->attribute; + } + + public function getOperator(): string + { + return $this->operator; + } + + public function getValue(): ?string + { + return $this->value; + } + + public function getSpecificity(): Specificity + { + return $this->selector->getSpecificity()->plus(new Specificity(0, 1, 0)); + } + + public function __toString(): string + { + $attribute = $this->namespace ? $this->namespace.'|'.$this->attribute : $this->attribute; + + return 'exists' === $this->operator + ? sprintf('%s[%s[%s]]', $this->getNodeName(), $this->selector, $attribute) + : sprintf("%s[%s[%s %s '%s']]", $this->getNodeName(), $this->selector, $attribute, $this->operator, $this->value); + } +} diff --git a/3rdparty/symfony/css-selector/Node/ClassNode.php b/3rdparty/symfony/css-selector/Node/ClassNode.php new file mode 100644 index 00000000..71744587 --- /dev/null +++ b/3rdparty/symfony/css-selector/Node/ClassNode.php @@ -0,0 +1,54 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Component\CssSelector\Node; + +/** + * Represents a "." node. + * + * This component is a port of the Python cssselect library, + * which is copyright Ian Bicking, @see https://github.com/SimonSapin/cssselect. + * + * @author Jean-François Simon + * + * @internal + */ +class ClassNode extends AbstractNode +{ + private NodeInterface $selector; + private string $name; + + public function __construct(NodeInterface $selector, string $name) + { + $this->selector = $selector; + $this->name = $name; + } + + public function getSelector(): NodeInterface + { + return $this->selector; + } + + public function getName(): string + { + return $this->name; + } + + public function getSpecificity(): Specificity + { + return $this->selector->getSpecificity()->plus(new Specificity(0, 1, 0)); + } + + public function __toString(): string + { + return sprintf('%s[%s.%s]', $this->getNodeName(), $this->selector, $this->name); + } +} diff --git a/3rdparty/symfony/css-selector/Node/CombinedSelectorNode.php b/3rdparty/symfony/css-selector/Node/CombinedSelectorNode.php new file mode 100644 index 00000000..19fecb70 --- /dev/null +++ b/3rdparty/symfony/css-selector/Node/CombinedSelectorNode.php @@ -0,0 +1,63 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Component\CssSelector\Node; + +/** + * Represents a combined node. + * + * This component is a port of the Python cssselect library, + * which is copyright Ian Bicking, @see https://github.com/SimonSapin/cssselect. + * + * @author Jean-François Simon + * + * @internal + */ +class CombinedSelectorNode extends AbstractNode +{ + private NodeInterface $selector; + private string $combinator; + private NodeInterface $subSelector; + + public function __construct(NodeInterface $selector, string $combinator, NodeInterface $subSelector) + { + $this->selector = $selector; + $this->combinator = $combinator; + $this->subSelector = $subSelector; + } + + public function getSelector(): NodeInterface + { + return $this->selector; + } + + public function getCombinator(): string + { + return $this->combinator; + } + + public function getSubSelector(): NodeInterface + { + return $this->subSelector; + } + + public function getSpecificity(): Specificity + { + return $this->selector->getSpecificity()->plus($this->subSelector->getSpecificity()); + } + + public function __toString(): string + { + $combinator = ' ' === $this->combinator ? '' : $this->combinator; + + return sprintf('%s[%s %s %s]', $this->getNodeName(), $this->selector, $combinator, $this->subSelector); + } +} diff --git a/3rdparty/symfony/css-selector/Node/ElementNode.php b/3rdparty/symfony/css-selector/Node/ElementNode.php new file mode 100644 index 00000000..76d2078e --- /dev/null +++ b/3rdparty/symfony/css-selector/Node/ElementNode.php @@ -0,0 +1,56 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Component\CssSelector\Node; + +/** + * Represents a "|" node. + * + * This component is a port of the Python cssselect library, + * which is copyright Ian Bicking, @see https://github.com/SimonSapin/cssselect. + * + * @author Jean-François Simon + * + * @internal + */ +class ElementNode extends AbstractNode +{ + private ?string $namespace; + private ?string $element; + + public function __construct(?string $namespace = null, ?string $element = null) + { + $this->namespace = $namespace; + $this->element = $element; + } + + public function getNamespace(): ?string + { + return $this->namespace; + } + + public function getElement(): ?string + { + return $this->element; + } + + public function getSpecificity(): Specificity + { + return new Specificity(0, 0, $this->element ? 1 : 0); + } + + public function __toString(): string + { + $element = $this->element ?: '*'; + + return sprintf('%s[%s]', $this->getNodeName(), $this->namespace ? $this->namespace.'|'.$element : $element); + } +} diff --git a/3rdparty/symfony/css-selector/Node/FunctionNode.php b/3rdparty/symfony/css-selector/Node/FunctionNode.php new file mode 100644 index 00000000..938a82b1 --- /dev/null +++ b/3rdparty/symfony/css-selector/Node/FunctionNode.php @@ -0,0 +1,71 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Component\CssSelector\Node; + +use Symfony\Component\CssSelector\Parser\Token; + +/** + * Represents a ":()" node. + * + * This component is a port of the Python cssselect library, + * which is copyright Ian Bicking, @see https://github.com/SimonSapin/cssselect. + * + * @author Jean-François Simon + * + * @internal + */ +class FunctionNode extends AbstractNode +{ + private NodeInterface $selector; + private string $name; + private array $arguments; + + /** + * @param Token[] $arguments + */ + public function __construct(NodeInterface $selector, string $name, array $arguments = []) + { + $this->selector = $selector; + $this->name = strtolower($name); + $this->arguments = $arguments; + } + + public function getSelector(): NodeInterface + { + return $this->selector; + } + + public function getName(): string + { + return $this->name; + } + + /** + * @return Token[] + */ + public function getArguments(): array + { + return $this->arguments; + } + + public function getSpecificity(): Specificity + { + return $this->selector->getSpecificity()->plus(new Specificity(0, 1, 0)); + } + + public function __toString(): string + { + $arguments = implode(', ', array_map(fn (Token $token) => "'".$token->getValue()."'", $this->arguments)); + + return sprintf('%s[%s:%s(%s)]', $this->getNodeName(), $this->selector, $this->name, $arguments ? '['.$arguments.']' : ''); + } +} diff --git a/3rdparty/symfony/css-selector/Node/HashNode.php b/3rdparty/symfony/css-selector/Node/HashNode.php new file mode 100644 index 00000000..3af8e847 --- /dev/null +++ b/3rdparty/symfony/css-selector/Node/HashNode.php @@ -0,0 +1,54 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Component\CssSelector\Node; + +/** + * Represents a "#" node. + * + * This component is a port of the Python cssselect library, + * which is copyright Ian Bicking, @see https://github.com/SimonSapin/cssselect. + * + * @author Jean-François Simon + * + * @internal + */ +class HashNode extends AbstractNode +{ + private NodeInterface $selector; + private string $id; + + public function __construct(NodeInterface $selector, string $id) + { + $this->selector = $selector; + $this->id = $id; + } + + public function getSelector(): NodeInterface + { + return $this->selector; + } + + public function getId(): string + { + return $this->id; + } + + public function getSpecificity(): Specificity + { + return $this->selector->getSpecificity()->plus(new Specificity(1, 0, 0)); + } + + public function __toString(): string + { + return sprintf('%s[%s#%s]', $this->getNodeName(), $this->selector, $this->id); + } +} diff --git a/3rdparty/symfony/css-selector/Node/NegationNode.php b/3rdparty/symfony/css-selector/Node/NegationNode.php new file mode 100644 index 00000000..f8067583 --- /dev/null +++ b/3rdparty/symfony/css-selector/Node/NegationNode.php @@ -0,0 +1,54 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Component\CssSelector\Node; + +/** + * Represents a ":not()" node. + * + * This component is a port of the Python cssselect library, + * which is copyright Ian Bicking, @see https://github.com/SimonSapin/cssselect. + * + * @author Jean-François Simon + * + * @internal + */ +class NegationNode extends AbstractNode +{ + private NodeInterface $selector; + private NodeInterface $subSelector; + + public function __construct(NodeInterface $selector, NodeInterface $subSelector) + { + $this->selector = $selector; + $this->subSelector = $subSelector; + } + + public function getSelector(): NodeInterface + { + return $this->selector; + } + + public function getSubSelector(): NodeInterface + { + return $this->subSelector; + } + + public function getSpecificity(): Specificity + { + return $this->selector->getSpecificity()->plus($this->subSelector->getSpecificity()); + } + + public function __toString(): string + { + return sprintf('%s[%s:not(%s)]', $this->getNodeName(), $this->selector, $this->subSelector); + } +} diff --git a/3rdparty/symfony/css-selector/Node/NodeInterface.php b/3rdparty/symfony/css-selector/Node/NodeInterface.php new file mode 100644 index 00000000..7d541f9c --- /dev/null +++ b/3rdparty/symfony/css-selector/Node/NodeInterface.php @@ -0,0 +1,29 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Component\CssSelector\Node; + +/** + * Interface for nodes. + * + * This component is a port of the Python cssselect library, + * which is copyright Ian Bicking, @see https://github.com/SimonSapin/cssselect. + * + * @author Jean-François Simon + * + * @internal + */ +interface NodeInterface extends \Stringable +{ + public function getNodeName(): string; + + public function getSpecificity(): Specificity; +} diff --git a/3rdparty/symfony/css-selector/Node/PseudoNode.php b/3rdparty/symfony/css-selector/Node/PseudoNode.php new file mode 100644 index 00000000..c21cd6e9 --- /dev/null +++ b/3rdparty/symfony/css-selector/Node/PseudoNode.php @@ -0,0 +1,54 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Component\CssSelector\Node; + +/** + * Represents a ":" node. + * + * This component is a port of the Python cssselect library, + * which is copyright Ian Bicking, @see https://github.com/SimonSapin/cssselect. + * + * @author Jean-François Simon + * + * @internal + */ +class PseudoNode extends AbstractNode +{ + private NodeInterface $selector; + private string $identifier; + + public function __construct(NodeInterface $selector, string $identifier) + { + $this->selector = $selector; + $this->identifier = strtolower($identifier); + } + + public function getSelector(): NodeInterface + { + return $this->selector; + } + + public function getIdentifier(): string + { + return $this->identifier; + } + + public function getSpecificity(): Specificity + { + return $this->selector->getSpecificity()->plus(new Specificity(0, 1, 0)); + } + + public function __toString(): string + { + return sprintf('%s[%s:%s]', $this->getNodeName(), $this->selector, $this->identifier); + } +} diff --git a/3rdparty/symfony/css-selector/Node/SelectorNode.php b/3rdparty/symfony/css-selector/Node/SelectorNode.php new file mode 100644 index 00000000..2318b2bf --- /dev/null +++ b/3rdparty/symfony/css-selector/Node/SelectorNode.php @@ -0,0 +1,54 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Component\CssSelector\Node; + +/** + * Represents a "(::|:)" node. + * + * This component is a port of the Python cssselect library, + * which is copyright Ian Bicking, @see https://github.com/SimonSapin/cssselect. + * + * @author Jean-François Simon + * + * @internal + */ +class SelectorNode extends AbstractNode +{ + private NodeInterface $tree; + private ?string $pseudoElement; + + public function __construct(NodeInterface $tree, ?string $pseudoElement = null) + { + $this->tree = $tree; + $this->pseudoElement = $pseudoElement ? strtolower($pseudoElement) : null; + } + + public function getTree(): NodeInterface + { + return $this->tree; + } + + public function getPseudoElement(): ?string + { + return $this->pseudoElement; + } + + public function getSpecificity(): Specificity + { + return $this->tree->getSpecificity()->plus(new Specificity(0, 0, $this->pseudoElement ? 1 : 0)); + } + + public function __toString(): string + { + return sprintf('%s[%s%s]', $this->getNodeName(), $this->tree, $this->pseudoElement ? '::'.$this->pseudoElement : ''); + } +} diff --git a/3rdparty/symfony/css-selector/Node/Specificity.php b/3rdparty/symfony/css-selector/Node/Specificity.php new file mode 100644 index 00000000..bb8e5e34 --- /dev/null +++ b/3rdparty/symfony/css-selector/Node/Specificity.php @@ -0,0 +1,73 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Component\CssSelector\Node; + +/** + * Represents a node specificity. + * + * This component is a port of the Python cssselect library, + * which is copyright Ian Bicking, @see https://github.com/SimonSapin/cssselect. + * + * @see http://www.w3.org/TR/selectors/#specificity + * + * @author Jean-François Simon + * + * @internal + */ +class Specificity +{ + public const A_FACTOR = 100; + public const B_FACTOR = 10; + public const C_FACTOR = 1; + + private int $a; + private int $b; + private int $c; + + public function __construct(int $a, int $b, int $c) + { + $this->a = $a; + $this->b = $b; + $this->c = $c; + } + + public function plus(self $specificity): self + { + return new self($this->a + $specificity->a, $this->b + $specificity->b, $this->c + $specificity->c); + } + + public function getValue(): int + { + return $this->a * self::A_FACTOR + $this->b * self::B_FACTOR + $this->c * self::C_FACTOR; + } + + /** + * Returns -1 if the object specificity is lower than the argument, + * 0 if they are equal, and 1 if the argument is lower. + */ + public function compareTo(self $specificity): int + { + if ($this->a !== $specificity->a) { + return $this->a > $specificity->a ? 1 : -1; + } + + if ($this->b !== $specificity->b) { + return $this->b > $specificity->b ? 1 : -1; + } + + if ($this->c !== $specificity->c) { + return $this->c > $specificity->c ? 1 : -1; + } + + return 0; + } +} diff --git a/3rdparty/symfony/css-selector/Parser/Handler/CommentHandler.php b/3rdparty/symfony/css-selector/Parser/Handler/CommentHandler.php new file mode 100644 index 00000000..cc01d1e6 --- /dev/null +++ b/3rdparty/symfony/css-selector/Parser/Handler/CommentHandler.php @@ -0,0 +1,45 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Component\CssSelector\Parser\Handler; + +use Symfony\Component\CssSelector\Parser\Reader; +use Symfony\Component\CssSelector\Parser\TokenStream; + +/** + * CSS selector comment handler. + * + * This component is a port of the Python cssselect library, + * which is copyright Ian Bicking, @see https://github.com/SimonSapin/cssselect. + * + * @author Jean-François Simon + * + * @internal + */ +class CommentHandler implements HandlerInterface +{ + public function handle(Reader $reader, TokenStream $stream): bool + { + if ('/*' !== $reader->getSubstring(2)) { + return false; + } + + $offset = $reader->getOffset('*/'); + + if (false === $offset) { + $reader->moveToEnd(); + } else { + $reader->moveForward($offset + 2); + } + + return true; + } +} diff --git a/3rdparty/symfony/css-selector/Parser/Handler/HandlerInterface.php b/3rdparty/symfony/css-selector/Parser/Handler/HandlerInterface.php new file mode 100644 index 00000000..9ec714d5 --- /dev/null +++ b/3rdparty/symfony/css-selector/Parser/Handler/HandlerInterface.php @@ -0,0 +1,30 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Component\CssSelector\Parser\Handler; + +use Symfony\Component\CssSelector\Parser\Reader; +use Symfony\Component\CssSelector\Parser\TokenStream; + +/** + * CSS selector handler interface. + * + * This component is a port of the Python cssselect library, + * which is copyright Ian Bicking, @see https://github.com/SimonSapin/cssselect. + * + * @author Jean-François Simon + * + * @internal + */ +interface HandlerInterface +{ + public function handle(Reader $reader, TokenStream $stream): bool; +} diff --git a/3rdparty/symfony/css-selector/Parser/Handler/HashHandler.php b/3rdparty/symfony/css-selector/Parser/Handler/HashHandler.php new file mode 100644 index 00000000..b29042f5 --- /dev/null +++ b/3rdparty/symfony/css-selector/Parser/Handler/HashHandler.php @@ -0,0 +1,55 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Component\CssSelector\Parser\Handler; + +use Symfony\Component\CssSelector\Parser\Reader; +use Symfony\Component\CssSelector\Parser\Token; +use Symfony\Component\CssSelector\Parser\Tokenizer\TokenizerEscaping; +use Symfony\Component\CssSelector\Parser\Tokenizer\TokenizerPatterns; +use Symfony\Component\CssSelector\Parser\TokenStream; + +/** + * CSS selector comment handler. + * + * This component is a port of the Python cssselect library, + * which is copyright Ian Bicking, @see https://github.com/SimonSapin/cssselect. + * + * @author Jean-François Simon + * + * @internal + */ +class HashHandler implements HandlerInterface +{ + private TokenizerPatterns $patterns; + private TokenizerEscaping $escaping; + + public function __construct(TokenizerPatterns $patterns, TokenizerEscaping $escaping) + { + $this->patterns = $patterns; + $this->escaping = $escaping; + } + + public function handle(Reader $reader, TokenStream $stream): bool + { + $match = $reader->findPattern($this->patterns->getHashPattern()); + + if (!$match) { + return false; + } + + $value = $this->escaping->escapeUnicode($match[1]); + $stream->push(new Token(Token::TYPE_HASH, $value, $reader->getPosition())); + $reader->moveForward(\strlen($match[0])); + + return true; + } +} diff --git a/3rdparty/symfony/css-selector/Parser/Handler/IdentifierHandler.php b/3rdparty/symfony/css-selector/Parser/Handler/IdentifierHandler.php new file mode 100644 index 00000000..25c0761e --- /dev/null +++ b/3rdparty/symfony/css-selector/Parser/Handler/IdentifierHandler.php @@ -0,0 +1,55 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Component\CssSelector\Parser\Handler; + +use Symfony\Component\CssSelector\Parser\Reader; +use Symfony\Component\CssSelector\Parser\Token; +use Symfony\Component\CssSelector\Parser\Tokenizer\TokenizerEscaping; +use Symfony\Component\CssSelector\Parser\Tokenizer\TokenizerPatterns; +use Symfony\Component\CssSelector\Parser\TokenStream; + +/** + * CSS selector comment handler. + * + * This component is a port of the Python cssselect library, + * which is copyright Ian Bicking, @see https://github.com/SimonSapin/cssselect. + * + * @author Jean-François Simon + * + * @internal + */ +class IdentifierHandler implements HandlerInterface +{ + private TokenizerPatterns $patterns; + private TokenizerEscaping $escaping; + + public function __construct(TokenizerPatterns $patterns, TokenizerEscaping $escaping) + { + $this->patterns = $patterns; + $this->escaping = $escaping; + } + + public function handle(Reader $reader, TokenStream $stream): bool + { + $match = $reader->findPattern($this->patterns->getIdentifierPattern()); + + if (!$match) { + return false; + } + + $value = $this->escaping->escapeUnicode($match[0]); + $stream->push(new Token(Token::TYPE_IDENTIFIER, $value, $reader->getPosition())); + $reader->moveForward(\strlen($match[0])); + + return true; + } +} diff --git a/3rdparty/symfony/css-selector/Parser/Handler/NumberHandler.php b/3rdparty/symfony/css-selector/Parser/Handler/NumberHandler.php new file mode 100644 index 00000000..e3eb7afe --- /dev/null +++ b/3rdparty/symfony/css-selector/Parser/Handler/NumberHandler.php @@ -0,0 +1,51 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Component\CssSelector\Parser\Handler; + +use Symfony\Component\CssSelector\Parser\Reader; +use Symfony\Component\CssSelector\Parser\Token; +use Symfony\Component\CssSelector\Parser\Tokenizer\TokenizerPatterns; +use Symfony\Component\CssSelector\Parser\TokenStream; + +/** + * CSS selector comment handler. + * + * This component is a port of the Python cssselect library, + * which is copyright Ian Bicking, @see https://github.com/SimonSapin/cssselect. + * + * @author Jean-François Simon + * + * @internal + */ +class NumberHandler implements HandlerInterface +{ + private TokenizerPatterns $patterns; + + public function __construct(TokenizerPatterns $patterns) + { + $this->patterns = $patterns; + } + + public function handle(Reader $reader, TokenStream $stream): bool + { + $match = $reader->findPattern($this->patterns->getNumberPattern()); + + if (!$match) { + return false; + } + + $stream->push(new Token(Token::TYPE_NUMBER, $match[0], $reader->getPosition())); + $reader->moveForward(\strlen($match[0])); + + return true; + } +} diff --git a/3rdparty/symfony/css-selector/Parser/Handler/StringHandler.php b/3rdparty/symfony/css-selector/Parser/Handler/StringHandler.php new file mode 100644 index 00000000..5fd44df2 --- /dev/null +++ b/3rdparty/symfony/css-selector/Parser/Handler/StringHandler.php @@ -0,0 +1,74 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Component\CssSelector\Parser\Handler; + +use Symfony\Component\CssSelector\Exception\InternalErrorException; +use Symfony\Component\CssSelector\Exception\SyntaxErrorException; +use Symfony\Component\CssSelector\Parser\Reader; +use Symfony\Component\CssSelector\Parser\Token; +use Symfony\Component\CssSelector\Parser\Tokenizer\TokenizerEscaping; +use Symfony\Component\CssSelector\Parser\Tokenizer\TokenizerPatterns; +use Symfony\Component\CssSelector\Parser\TokenStream; + +/** + * CSS selector comment handler. + * + * This component is a port of the Python cssselect library, + * which is copyright Ian Bicking, @see https://github.com/SimonSapin/cssselect. + * + * @author Jean-François Simon + * + * @internal + */ +class StringHandler implements HandlerInterface +{ + private TokenizerPatterns $patterns; + private TokenizerEscaping $escaping; + + public function __construct(TokenizerPatterns $patterns, TokenizerEscaping $escaping) + { + $this->patterns = $patterns; + $this->escaping = $escaping; + } + + public function handle(Reader $reader, TokenStream $stream): bool + { + $quote = $reader->getSubstring(1); + + if (!\in_array($quote, ["'", '"'])) { + return false; + } + + $reader->moveForward(1); + $match = $reader->findPattern($this->patterns->getQuotedStringPattern($quote)); + + if (!$match) { + throw new InternalErrorException(sprintf('Should have found at least an empty match at %d.', $reader->getPosition())); + } + + // check unclosed strings + if (\strlen($match[0]) === $reader->getRemainingLength()) { + throw SyntaxErrorException::unclosedString($reader->getPosition() - 1); + } + + // check quotes pairs validity + if ($quote !== $reader->getSubstring(1, \strlen($match[0]))) { + throw SyntaxErrorException::unclosedString($reader->getPosition() - 1); + } + + $string = $this->escaping->escapeUnicodeAndNewLine($match[0]); + $stream->push(new Token(Token::TYPE_STRING, $string, $reader->getPosition())); + $reader->moveForward(\strlen($match[0]) + 1); + + return true; + } +} diff --git a/3rdparty/symfony/css-selector/Parser/Handler/WhitespaceHandler.php b/3rdparty/symfony/css-selector/Parser/Handler/WhitespaceHandler.php new file mode 100644 index 00000000..eb41c3f7 --- /dev/null +++ b/3rdparty/symfony/css-selector/Parser/Handler/WhitespaceHandler.php @@ -0,0 +1,43 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Component\CssSelector\Parser\Handler; + +use Symfony\Component\CssSelector\Parser\Reader; +use Symfony\Component\CssSelector\Parser\Token; +use Symfony\Component\CssSelector\Parser\TokenStream; + +/** + * CSS selector whitespace handler. + * + * This component is a port of the Python cssselect library, + * which is copyright Ian Bicking, @see https://github.com/SimonSapin/cssselect. + * + * @author Jean-François Simon + * + * @internal + */ +class WhitespaceHandler implements HandlerInterface +{ + public function handle(Reader $reader, TokenStream $stream): bool + { + $match = $reader->findPattern('~^[ \t\r\n\f]+~'); + + if (false === $match) { + return false; + } + + $stream->push(new Token(Token::TYPE_WHITESPACE, $match[0], $reader->getPosition())); + $reader->moveForward(\strlen($match[0])); + + return true; + } +} diff --git a/3rdparty/symfony/css-selector/Parser/Parser.php b/3rdparty/symfony/css-selector/Parser/Parser.php new file mode 100644 index 00000000..309c9b52 --- /dev/null +++ b/3rdparty/symfony/css-selector/Parser/Parser.php @@ -0,0 +1,359 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Component\CssSelector\Parser; + +use Symfony\Component\CssSelector\Exception\SyntaxErrorException; +use Symfony\Component\CssSelector\Node; +use Symfony\Component\CssSelector\Parser\Tokenizer\Tokenizer; + +/** + * CSS selector parser. + * + * This component is a port of the Python cssselect library, + * which is copyright Ian Bicking, @see https://github.com/scrapy/cssselect. + * + * @author Jean-François Simon + * + * @internal + */ +class Parser implements ParserInterface +{ + private Tokenizer $tokenizer; + + public function __construct(?Tokenizer $tokenizer = null) + { + $this->tokenizer = $tokenizer ?? new Tokenizer(); + } + + public function parse(string $source): array + { + $reader = new Reader($source); + $stream = $this->tokenizer->tokenize($reader); + + return $this->parseSelectorList($stream); + } + + /** + * Parses the arguments for ":nth-child()" and friends. + * + * @param Token[] $tokens + * + * @throws SyntaxErrorException + */ + public static function parseSeries(array $tokens): array + { + foreach ($tokens as $token) { + if ($token->isString()) { + throw SyntaxErrorException::stringAsFunctionArgument(); + } + } + + $joined = trim(implode('', array_map(fn (Token $token) => $token->getValue(), $tokens))); + + $int = function ($string) { + if (!is_numeric($string)) { + throw SyntaxErrorException::stringAsFunctionArgument(); + } + + return (int) $string; + }; + + switch (true) { + case 'odd' === $joined: + return [2, 1]; + case 'even' === $joined: + return [2, 0]; + case 'n' === $joined: + return [1, 0]; + case !str_contains($joined, 'n'): + return [0, $int($joined)]; + } + + $split = explode('n', $joined); + $first = $split[0] ?? null; + + return [ + $first ? ('-' === $first || '+' === $first ? $int($first.'1') : $int($first)) : 1, + isset($split[1]) && $split[1] ? $int($split[1]) : 0, + ]; + } + + private function parseSelectorList(TokenStream $stream): array + { + $stream->skipWhitespace(); + $selectors = []; + + while (true) { + $selectors[] = $this->parserSelectorNode($stream); + + if ($stream->getPeek()->isDelimiter([','])) { + $stream->getNext(); + $stream->skipWhitespace(); + } else { + break; + } + } + + return $selectors; + } + + private function parserSelectorNode(TokenStream $stream): Node\SelectorNode + { + [$result, $pseudoElement] = $this->parseSimpleSelector($stream); + + while (true) { + $stream->skipWhitespace(); + $peek = $stream->getPeek(); + + if ($peek->isFileEnd() || $peek->isDelimiter([','])) { + break; + } + + if (null !== $pseudoElement) { + throw SyntaxErrorException::pseudoElementFound($pseudoElement, 'not at the end of a selector'); + } + + if ($peek->isDelimiter(['+', '>', '~'])) { + $combinator = $stream->getNext()->getValue(); + $stream->skipWhitespace(); + } else { + $combinator = ' '; + } + + [$nextSelector, $pseudoElement] = $this->parseSimpleSelector($stream); + $result = new Node\CombinedSelectorNode($result, $combinator, $nextSelector); + } + + return new Node\SelectorNode($result, $pseudoElement); + } + + /** + * Parses next simple node (hash, class, pseudo, negation). + * + * @throws SyntaxErrorException + */ + private function parseSimpleSelector(TokenStream $stream, bool $insideNegation = false): array + { + $stream->skipWhitespace(); + + $selectorStart = \count($stream->getUsed()); + $result = $this->parseElementNode($stream); + $pseudoElement = null; + + while (true) { + $peek = $stream->getPeek(); + if ($peek->isWhitespace() + || $peek->isFileEnd() + || $peek->isDelimiter([',', '+', '>', '~']) + || ($insideNegation && $peek->isDelimiter([')'])) + ) { + break; + } + + if (null !== $pseudoElement) { + throw SyntaxErrorException::pseudoElementFound($pseudoElement, 'not at the end of a selector'); + } + + if ($peek->isHash()) { + $result = new Node\HashNode($result, $stream->getNext()->getValue()); + } elseif ($peek->isDelimiter(['.'])) { + $stream->getNext(); + $result = new Node\ClassNode($result, $stream->getNextIdentifier()); + } elseif ($peek->isDelimiter(['['])) { + $stream->getNext(); + $result = $this->parseAttributeNode($result, $stream); + } elseif ($peek->isDelimiter([':'])) { + $stream->getNext(); + + if ($stream->getPeek()->isDelimiter([':'])) { + $stream->getNext(); + $pseudoElement = $stream->getNextIdentifier(); + + continue; + } + + $identifier = $stream->getNextIdentifier(); + if (\in_array(strtolower($identifier), ['first-line', 'first-letter', 'before', 'after'])) { + // Special case: CSS 2.1 pseudo-elements can have a single ':'. + // Any new pseudo-element must have two. + $pseudoElement = $identifier; + + continue; + } + + if (!$stream->getPeek()->isDelimiter(['('])) { + $result = new Node\PseudoNode($result, $identifier); + if ('Pseudo[Element[*]:scope]' === $result->__toString()) { + $used = \count($stream->getUsed()); + if (!(2 === $used + || 3 === $used && $stream->getUsed()[0]->isWhiteSpace() + || $used >= 3 && $stream->getUsed()[$used - 3]->isDelimiter([',']) + || $used >= 4 + && $stream->getUsed()[$used - 3]->isWhiteSpace() + && $stream->getUsed()[$used - 4]->isDelimiter([',']) + )) { + throw SyntaxErrorException::notAtTheStartOfASelector('scope'); + } + } + continue; + } + + $stream->getNext(); + $stream->skipWhitespace(); + + if ('not' === strtolower($identifier)) { + if ($insideNegation) { + throw SyntaxErrorException::nestedNot(); + } + + [$argument, $argumentPseudoElement] = $this->parseSimpleSelector($stream, true); + $next = $stream->getNext(); + + if (null !== $argumentPseudoElement) { + throw SyntaxErrorException::pseudoElementFound($argumentPseudoElement, 'inside ::not()'); + } + + if (!$next->isDelimiter([')'])) { + throw SyntaxErrorException::unexpectedToken('")"', $next); + } + + $result = new Node\NegationNode($result, $argument); + } else { + $arguments = []; + $next = null; + + while (true) { + $stream->skipWhitespace(); + $next = $stream->getNext(); + + if ($next->isIdentifier() + || $next->isString() + || $next->isNumber() + || $next->isDelimiter(['+', '-']) + ) { + $arguments[] = $next; + } elseif ($next->isDelimiter([')'])) { + break; + } else { + throw SyntaxErrorException::unexpectedToken('an argument', $next); + } + } + + if (!$arguments) { + throw SyntaxErrorException::unexpectedToken('at least one argument', $next); + } + + $result = new Node\FunctionNode($result, $identifier, $arguments); + } + } else { + throw SyntaxErrorException::unexpectedToken('selector', $peek); + } + } + + if (\count($stream->getUsed()) === $selectorStart) { + throw SyntaxErrorException::unexpectedToken('selector', $stream->getPeek()); + } + + return [$result, $pseudoElement]; + } + + private function parseElementNode(TokenStream $stream): Node\ElementNode + { + $peek = $stream->getPeek(); + + if ($peek->isIdentifier() || $peek->isDelimiter(['*'])) { + if ($peek->isIdentifier()) { + $namespace = $stream->getNext()->getValue(); + } else { + $stream->getNext(); + $namespace = null; + } + + if ($stream->getPeek()->isDelimiter(['|'])) { + $stream->getNext(); + $element = $stream->getNextIdentifierOrStar(); + } else { + $element = $namespace; + $namespace = null; + } + } else { + $element = $namespace = null; + } + + return new Node\ElementNode($namespace, $element); + } + + private function parseAttributeNode(Node\NodeInterface $selector, TokenStream $stream): Node\AttributeNode + { + $stream->skipWhitespace(); + $attribute = $stream->getNextIdentifierOrStar(); + + if (null === $attribute && !$stream->getPeek()->isDelimiter(['|'])) { + throw SyntaxErrorException::unexpectedToken('"|"', $stream->getPeek()); + } + + if ($stream->getPeek()->isDelimiter(['|'])) { + $stream->getNext(); + + if ($stream->getPeek()->isDelimiter(['='])) { + $namespace = null; + $stream->getNext(); + $operator = '|='; + } else { + $namespace = $attribute; + $attribute = $stream->getNextIdentifier(); + $operator = null; + } + } else { + $namespace = $operator = null; + } + + if (null === $operator) { + $stream->skipWhitespace(); + $next = $stream->getNext(); + + if ($next->isDelimiter([']'])) { + return new Node\AttributeNode($selector, $namespace, $attribute, 'exists', null); + } elseif ($next->isDelimiter(['='])) { + $operator = '='; + } elseif ($next->isDelimiter(['^', '$', '*', '~', '|', '!']) + && $stream->getPeek()->isDelimiter(['=']) + ) { + $operator = $next->getValue().'='; + $stream->getNext(); + } else { + throw SyntaxErrorException::unexpectedToken('operator', $next); + } + } + + $stream->skipWhitespace(); + $value = $stream->getNext(); + + if ($value->isNumber()) { + // if the value is a number, it's casted into a string + $value = new Token(Token::TYPE_STRING, (string) $value->getValue(), $value->getPosition()); + } + + if (!($value->isIdentifier() || $value->isString())) { + throw SyntaxErrorException::unexpectedToken('string or identifier', $value); + } + + $stream->skipWhitespace(); + $next = $stream->getNext(); + + if (!$next->isDelimiter([']'])) { + throw SyntaxErrorException::unexpectedToken('"]"', $next); + } + + return new Node\AttributeNode($selector, $namespace, $attribute, $operator, $value->getValue()); + } +} diff --git a/3rdparty/symfony/css-selector/Parser/ParserInterface.php b/3rdparty/symfony/css-selector/Parser/ParserInterface.php new file mode 100644 index 00000000..51c3d935 --- /dev/null +++ b/3rdparty/symfony/css-selector/Parser/ParserInterface.php @@ -0,0 +1,34 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Component\CssSelector\Parser; + +use Symfony\Component\CssSelector\Node\SelectorNode; + +/** + * CSS selector parser interface. + * + * This component is a port of the Python cssselect library, + * which is copyright Ian Bicking, @see https://github.com/SimonSapin/cssselect. + * + * @author Jean-François Simon + * + * @internal + */ +interface ParserInterface +{ + /** + * Parses given selector source into an array of tokens. + * + * @return SelectorNode[] + */ + public function parse(string $source): array; +} diff --git a/3rdparty/symfony/css-selector/Parser/Reader.php b/3rdparty/symfony/css-selector/Parser/Reader.php new file mode 100644 index 00000000..7f6ae7a6 --- /dev/null +++ b/3rdparty/symfony/css-selector/Parser/Reader.php @@ -0,0 +1,86 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Component\CssSelector\Parser; + +/** + * CSS selector reader. + * + * This component is a port of the Python cssselect library, + * which is copyright Ian Bicking, @see https://github.com/SimonSapin/cssselect. + * + * @author Jean-François Simon + * + * @internal + */ +class Reader +{ + private string $source; + private int $length; + private int $position = 0; + + public function __construct(string $source) + { + $this->source = $source; + $this->length = \strlen($source); + } + + public function isEOF(): bool + { + return $this->position >= $this->length; + } + + public function getPosition(): int + { + return $this->position; + } + + public function getRemainingLength(): int + { + return $this->length - $this->position; + } + + public function getSubstring(int $length, int $offset = 0): string + { + return substr($this->source, $this->position + $offset, $length); + } + + /** + * @return int|false + */ + public function getOffset(string $string): int|bool + { + $position = strpos($this->source, $string, $this->position); + + return false === $position ? false : $position - $this->position; + } + + public function findPattern(string $pattern): array|false + { + $source = substr($this->source, $this->position); + + if (preg_match($pattern, $source, $matches)) { + return $matches; + } + + return false; + } + + public function moveForward(int $length): void + { + $this->position += $length; + } + + public function moveToEnd(): void + { + $this->position = $this->length; + } +} diff --git a/3rdparty/symfony/css-selector/Parser/Shortcut/ClassParser.php b/3rdparty/symfony/css-selector/Parser/Shortcut/ClassParser.php new file mode 100644 index 00000000..f0ce6118 --- /dev/null +++ b/3rdparty/symfony/css-selector/Parser/Shortcut/ClassParser.php @@ -0,0 +1,48 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Component\CssSelector\Parser\Shortcut; + +use Symfony\Component\CssSelector\Node\ClassNode; +use Symfony\Component\CssSelector\Node\ElementNode; +use Symfony\Component\CssSelector\Node\SelectorNode; +use Symfony\Component\CssSelector\Parser\ParserInterface; + +/** + * CSS selector class parser shortcut. + * + * This component is a port of the Python cssselect library, + * which is copyright Ian Bicking, @see https://github.com/SimonSapin/cssselect. + * + * @author Jean-François Simon + * + * @internal + */ +class ClassParser implements ParserInterface +{ + public function parse(string $source): array + { + // Matches an optional namespace, optional element, and required class + // $source = 'test|input.ab6bd_field'; + // $matches = array (size=4) + // 0 => string 'test|input.ab6bd_field' (length=22) + // 1 => string 'test' (length=4) + // 2 => string 'input' (length=5) + // 3 => string 'ab6bd_field' (length=11) + if (preg_match('/^(?:([a-z]++)\|)?+([\w-]++|\*)?+\.([\w-]++)$/i', trim($source), $matches)) { + return [ + new SelectorNode(new ClassNode(new ElementNode($matches[1] ?: null, $matches[2] ?: null), $matches[3])), + ]; + } + + return []; + } +} diff --git a/3rdparty/symfony/css-selector/Parser/Shortcut/ElementParser.php b/3rdparty/symfony/css-selector/Parser/Shortcut/ElementParser.php new file mode 100644 index 00000000..a448e4a8 --- /dev/null +++ b/3rdparty/symfony/css-selector/Parser/Shortcut/ElementParser.php @@ -0,0 +1,44 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Component\CssSelector\Parser\Shortcut; + +use Symfony\Component\CssSelector\Node\ElementNode; +use Symfony\Component\CssSelector\Node\SelectorNode; +use Symfony\Component\CssSelector\Parser\ParserInterface; + +/** + * CSS selector element parser shortcut. + * + * This component is a port of the Python cssselect library, + * which is copyright Ian Bicking, @see https://github.com/SimonSapin/cssselect. + * + * @author Jean-François Simon + * + * @internal + */ +class ElementParser implements ParserInterface +{ + public function parse(string $source): array + { + // Matches an optional namespace, required element or `*` + // $source = 'testns|testel'; + // $matches = array (size=3) + // 0 => string 'testns|testel' (length=13) + // 1 => string 'testns' (length=6) + // 2 => string 'testel' (length=6) + if (preg_match('/^(?:([a-z]++)\|)?([\w-]++|\*)$/i', trim($source), $matches)) { + return [new SelectorNode(new ElementNode($matches[1] ?: null, $matches[2]))]; + } + + return []; + } +} diff --git a/3rdparty/symfony/css-selector/Parser/Shortcut/EmptyStringParser.php b/3rdparty/symfony/css-selector/Parser/Shortcut/EmptyStringParser.php new file mode 100644 index 00000000..a6391912 --- /dev/null +++ b/3rdparty/symfony/css-selector/Parser/Shortcut/EmptyStringParser.php @@ -0,0 +1,43 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Component\CssSelector\Parser\Shortcut; + +use Symfony\Component\CssSelector\Node\ElementNode; +use Symfony\Component\CssSelector\Node\SelectorNode; +use Symfony\Component\CssSelector\Parser\ParserInterface; + +/** + * CSS selector class parser shortcut. + * + * This shortcut ensure compatibility with previous version. + * - The parser fails to parse an empty string. + * - In the previous version, an empty string matches each tags. + * + * This component is a port of the Python cssselect library, + * which is copyright Ian Bicking, @see https://github.com/SimonSapin/cssselect. + * + * @author Jean-François Simon + * + * @internal + */ +class EmptyStringParser implements ParserInterface +{ + public function parse(string $source): array + { + // Matches an empty string + if ('' == $source) { + return [new SelectorNode(new ElementNode(null, '*'))]; + } + + return []; + } +} diff --git a/3rdparty/symfony/css-selector/Parser/Shortcut/HashParser.php b/3rdparty/symfony/css-selector/Parser/Shortcut/HashParser.php new file mode 100644 index 00000000..6683126a --- /dev/null +++ b/3rdparty/symfony/css-selector/Parser/Shortcut/HashParser.php @@ -0,0 +1,48 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Component\CssSelector\Parser\Shortcut; + +use Symfony\Component\CssSelector\Node\ElementNode; +use Symfony\Component\CssSelector\Node\HashNode; +use Symfony\Component\CssSelector\Node\SelectorNode; +use Symfony\Component\CssSelector\Parser\ParserInterface; + +/** + * CSS selector hash parser shortcut. + * + * This component is a port of the Python cssselect library, + * which is copyright Ian Bicking, @see https://github.com/SimonSapin/cssselect. + * + * @author Jean-François Simon + * + * @internal + */ +class HashParser implements ParserInterface +{ + public function parse(string $source): array + { + // Matches an optional namespace, optional element, and required id + // $source = 'test|input#ab6bd_field'; + // $matches = array (size=4) + // 0 => string 'test|input#ab6bd_field' (length=22) + // 1 => string 'test' (length=4) + // 2 => string 'input' (length=5) + // 3 => string 'ab6bd_field' (length=11) + if (preg_match('/^(?:([a-z]++)\|)?+([\w-]++|\*)?+#([\w-]++)$/i', trim($source), $matches)) { + return [ + new SelectorNode(new HashNode(new ElementNode($matches[1] ?: null, $matches[2] ?: null), $matches[3])), + ]; + } + + return []; + } +} diff --git a/3rdparty/symfony/css-selector/Parser/Token.php b/3rdparty/symfony/css-selector/Parser/Token.php new file mode 100644 index 00000000..b50441a8 --- /dev/null +++ b/3rdparty/symfony/css-selector/Parser/Token.php @@ -0,0 +1,111 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Component\CssSelector\Parser; + +/** + * CSS selector token. + * + * This component is a port of the Python cssselect library, + * which is copyright Ian Bicking, @see https://github.com/SimonSapin/cssselect. + * + * @author Jean-François Simon + * + * @internal + */ +class Token +{ + public const TYPE_FILE_END = 'eof'; + public const TYPE_DELIMITER = 'delimiter'; + public const TYPE_WHITESPACE = 'whitespace'; + public const TYPE_IDENTIFIER = 'identifier'; + public const TYPE_HASH = 'hash'; + public const TYPE_NUMBER = 'number'; + public const TYPE_STRING = 'string'; + + private ?string $type; + private ?string $value; + private ?int $position; + + public function __construct(?string $type, ?string $value, ?int $position) + { + $this->type = $type; + $this->value = $value; + $this->position = $position; + } + + public function getType(): ?int + { + return $this->type; + } + + public function getValue(): ?string + { + return $this->value; + } + + public function getPosition(): ?int + { + return $this->position; + } + + public function isFileEnd(): bool + { + return self::TYPE_FILE_END === $this->type; + } + + public function isDelimiter(array $values = []): bool + { + if (self::TYPE_DELIMITER !== $this->type) { + return false; + } + + if (!$values) { + return true; + } + + return \in_array($this->value, $values); + } + + public function isWhitespace(): bool + { + return self::TYPE_WHITESPACE === $this->type; + } + + public function isIdentifier(): bool + { + return self::TYPE_IDENTIFIER === $this->type; + } + + public function isHash(): bool + { + return self::TYPE_HASH === $this->type; + } + + public function isNumber(): bool + { + return self::TYPE_NUMBER === $this->type; + } + + public function isString(): bool + { + return self::TYPE_STRING === $this->type; + } + + public function __toString(): string + { + if ($this->value) { + return sprintf('<%s "%s" at %s>', $this->type, $this->value, $this->position); + } + + return sprintf('<%s at %s>', $this->type, $this->position); + } +} diff --git a/3rdparty/symfony/css-selector/Parser/TokenStream.php b/3rdparty/symfony/css-selector/Parser/TokenStream.php new file mode 100644 index 00000000..8b72d5db --- /dev/null +++ b/3rdparty/symfony/css-selector/Parser/TokenStream.php @@ -0,0 +1,156 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Component\CssSelector\Parser; + +use Symfony\Component\CssSelector\Exception\InternalErrorException; +use Symfony\Component\CssSelector\Exception\SyntaxErrorException; + +/** + * CSS selector token stream. + * + * This component is a port of the Python cssselect library, + * which is copyright Ian Bicking, @see https://github.com/SimonSapin/cssselect. + * + * @author Jean-François Simon + * + * @internal + */ +class TokenStream +{ + /** + * @var Token[] + */ + private array $tokens = []; + + /** + * @var Token[] + */ + private array $used = []; + + private int $cursor = 0; + private ?Token $peeked; + private bool $peeking = false; + + /** + * Pushes a token. + * + * @return $this + */ + public function push(Token $token): static + { + $this->tokens[] = $token; + + return $this; + } + + /** + * Freezes stream. + * + * @return $this + */ + public function freeze(): static + { + return $this; + } + + /** + * Returns next token. + * + * @throws InternalErrorException If there is no more token + */ + public function getNext(): Token + { + if ($this->peeking) { + $this->peeking = false; + $this->used[] = $this->peeked; + + return $this->peeked; + } + + if (!isset($this->tokens[$this->cursor])) { + throw new InternalErrorException('Unexpected token stream end.'); + } + + return $this->tokens[$this->cursor++]; + } + + /** + * Returns peeked token. + */ + public function getPeek(): Token + { + if (!$this->peeking) { + $this->peeked = $this->getNext(); + $this->peeking = true; + } + + return $this->peeked; + } + + /** + * Returns used tokens. + * + * @return Token[] + */ + public function getUsed(): array + { + return $this->used; + } + + /** + * Returns next identifier token. + * + * @throws SyntaxErrorException If next token is not an identifier + */ + public function getNextIdentifier(): string + { + $next = $this->getNext(); + + if (!$next->isIdentifier()) { + throw SyntaxErrorException::unexpectedToken('identifier', $next); + } + + return $next->getValue(); + } + + /** + * Returns next identifier or null if star delimiter token is found. + * + * @throws SyntaxErrorException If next token is not an identifier or a star delimiter + */ + public function getNextIdentifierOrStar(): ?string + { + $next = $this->getNext(); + + if ($next->isIdentifier()) { + return $next->getValue(); + } + + if ($next->isDelimiter(['*'])) { + return null; + } + + throw SyntaxErrorException::unexpectedToken('identifier or "*"', $next); + } + + /** + * Skips next whitespace if any. + */ + public function skipWhitespace(): void + { + $peek = $this->getPeek(); + + if ($peek->isWhitespace()) { + $this->getNext(); + } + } +} diff --git a/3rdparty/symfony/css-selector/Parser/Tokenizer/Tokenizer.php b/3rdparty/symfony/css-selector/Parser/Tokenizer/Tokenizer.php new file mode 100644 index 00000000..35c96a48 --- /dev/null +++ b/3rdparty/symfony/css-selector/Parser/Tokenizer/Tokenizer.php @@ -0,0 +1,73 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Component\CssSelector\Parser\Tokenizer; + +use Symfony\Component\CssSelector\Parser\Handler; +use Symfony\Component\CssSelector\Parser\Reader; +use Symfony\Component\CssSelector\Parser\Token; +use Symfony\Component\CssSelector\Parser\TokenStream; + +/** + * CSS selector tokenizer. + * + * This component is a port of the Python cssselect library, + * which is copyright Ian Bicking, @see https://github.com/SimonSapin/cssselect. + * + * @author Jean-François Simon + * + * @internal + */ +class Tokenizer +{ + /** + * @var Handler\HandlerInterface[] + */ + private array $handlers; + + public function __construct() + { + $patterns = new TokenizerPatterns(); + $escaping = new TokenizerEscaping($patterns); + + $this->handlers = [ + new Handler\WhitespaceHandler(), + new Handler\IdentifierHandler($patterns, $escaping), + new Handler\HashHandler($patterns, $escaping), + new Handler\StringHandler($patterns, $escaping), + new Handler\NumberHandler($patterns), + new Handler\CommentHandler(), + ]; + } + + /** + * Tokenize selector source code. + */ + public function tokenize(Reader $reader): TokenStream + { + $stream = new TokenStream(); + + while (!$reader->isEOF()) { + foreach ($this->handlers as $handler) { + if ($handler->handle($reader, $stream)) { + continue 2; + } + } + + $stream->push(new Token(Token::TYPE_DELIMITER, $reader->getSubstring(1), $reader->getPosition())); + $reader->moveForward(1); + } + + return $stream + ->push(new Token(Token::TYPE_FILE_END, null, $reader->getPosition())) + ->freeze(); + } +} diff --git a/3rdparty/symfony/css-selector/Parser/Tokenizer/TokenizerEscaping.php b/3rdparty/symfony/css-selector/Parser/Tokenizer/TokenizerEscaping.php new file mode 100644 index 00000000..8c4b9f74 --- /dev/null +++ b/3rdparty/symfony/css-selector/Parser/Tokenizer/TokenizerEscaping.php @@ -0,0 +1,65 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Component\CssSelector\Parser\Tokenizer; + +/** + * CSS selector tokenizer escaping applier. + * + * This component is a port of the Python cssselect library, + * which is copyright Ian Bicking, @see https://github.com/SimonSapin/cssselect. + * + * @author Jean-François Simon + * + * @internal + */ +class TokenizerEscaping +{ + private TokenizerPatterns $patterns; + + public function __construct(TokenizerPatterns $patterns) + { + $this->patterns = $patterns; + } + + public function escapeUnicode(string $value): string + { + $value = $this->replaceUnicodeSequences($value); + + return preg_replace($this->patterns->getSimpleEscapePattern(), '$1', $value); + } + + public function escapeUnicodeAndNewLine(string $value): string + { + $value = preg_replace($this->patterns->getNewLineEscapePattern(), '', $value); + + return $this->escapeUnicode($value); + } + + private function replaceUnicodeSequences(string $value): string + { + return preg_replace_callback($this->patterns->getUnicodeEscapePattern(), function ($match) { + $c = hexdec($match[1]); + + if (0x80 > $c %= 0x200000) { + return \chr($c); + } + if (0x800 > $c) { + return \chr(0xC0 | $c >> 6).\chr(0x80 | $c & 0x3F); + } + if (0x10000 > $c) { + return \chr(0xE0 | $c >> 12).\chr(0x80 | $c >> 6 & 0x3F).\chr(0x80 | $c & 0x3F); + } + + return ''; + }, $value); + } +} diff --git a/3rdparty/symfony/css-selector/Parser/Tokenizer/TokenizerPatterns.php b/3rdparty/symfony/css-selector/Parser/Tokenizer/TokenizerPatterns.php new file mode 100644 index 00000000..3c77cf09 --- /dev/null +++ b/3rdparty/symfony/css-selector/Parser/Tokenizer/TokenizerPatterns.php @@ -0,0 +1,89 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Component\CssSelector\Parser\Tokenizer; + +/** + * CSS selector tokenizer patterns builder. + * + * This component is a port of the Python cssselect library, + * which is copyright Ian Bicking, @see https://github.com/SimonSapin/cssselect. + * + * @author Jean-François Simon + * + * @internal + */ +class TokenizerPatterns +{ + private string $unicodeEscapePattern; + private string $simpleEscapePattern; + private string $newLineEscapePattern; + private string $escapePattern; + private string $stringEscapePattern; + private string $nonAsciiPattern; + private string $nmCharPattern; + private string $nmStartPattern; + private string $identifierPattern; + private string $hashPattern; + private string $numberPattern; + private string $quotedStringPattern; + + public function __construct() + { + $this->unicodeEscapePattern = '\\\\([0-9a-f]{1,6})(?:\r\n|[ \n\r\t\f])?'; + $this->simpleEscapePattern = '\\\\(.)'; + $this->newLineEscapePattern = '\\\\(?:\n|\r\n|\r|\f)'; + $this->escapePattern = $this->unicodeEscapePattern.'|\\\\[^\n\r\f0-9a-f]'; + $this->stringEscapePattern = $this->newLineEscapePattern.'|'.$this->escapePattern; + $this->nonAsciiPattern = '[^\x00-\x7F]'; + $this->nmCharPattern = '[_a-z0-9-]|'.$this->escapePattern.'|'.$this->nonAsciiPattern; + $this->nmStartPattern = '[_a-z]|'.$this->escapePattern.'|'.$this->nonAsciiPattern; + $this->identifierPattern = '-?(?:'.$this->nmStartPattern.')(?:'.$this->nmCharPattern.')*'; + $this->hashPattern = '#((?:'.$this->nmCharPattern.')+)'; + $this->numberPattern = '[+-]?(?:[0-9]*\.[0-9]+|[0-9]+)'; + $this->quotedStringPattern = '([^\n\r\f\\\\%s]|'.$this->stringEscapePattern.')*'; + } + + public function getNewLineEscapePattern(): string + { + return '~'.$this->newLineEscapePattern.'~'; + } + + public function getSimpleEscapePattern(): string + { + return '~'.$this->simpleEscapePattern.'~'; + } + + public function getUnicodeEscapePattern(): string + { + return '~'.$this->unicodeEscapePattern.'~i'; + } + + public function getIdentifierPattern(): string + { + return '~^'.$this->identifierPattern.'~i'; + } + + public function getHashPattern(): string + { + return '~^'.$this->hashPattern.'~i'; + } + + public function getNumberPattern(): string + { + return '~^'.$this->numberPattern.'~'; + } + + public function getQuotedStringPattern(string $quote): string + { + return '~^'.sprintf($this->quotedStringPattern, $quote).'~i'; + } +} diff --git a/3rdparty/symfony/css-selector/XPath/Extension/AbstractExtension.php b/3rdparty/symfony/css-selector/XPath/Extension/AbstractExtension.php new file mode 100644 index 00000000..495f8829 --- /dev/null +++ b/3rdparty/symfony/css-selector/XPath/Extension/AbstractExtension.php @@ -0,0 +1,50 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Component\CssSelector\XPath\Extension; + +/** + * XPath expression translator abstract extension. + * + * This component is a port of the Python cssselect library, + * which is copyright Ian Bicking, @see https://github.com/SimonSapin/cssselect. + * + * @author Jean-François Simon + * + * @internal + */ +abstract class AbstractExtension implements ExtensionInterface +{ + public function getNodeTranslators(): array + { + return []; + } + + public function getCombinationTranslators(): array + { + return []; + } + + public function getFunctionTranslators(): array + { + return []; + } + + public function getPseudoClassTranslators(): array + { + return []; + } + + public function getAttributeMatchingTranslators(): array + { + return []; + } +} diff --git a/3rdparty/symfony/css-selector/XPath/Extension/AttributeMatchingExtension.php b/3rdparty/symfony/css-selector/XPath/Extension/AttributeMatchingExtension.php new file mode 100644 index 00000000..3c785e97 --- /dev/null +++ b/3rdparty/symfony/css-selector/XPath/Extension/AttributeMatchingExtension.php @@ -0,0 +1,113 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Component\CssSelector\XPath\Extension; + +use Symfony\Component\CssSelector\XPath\Translator; +use Symfony\Component\CssSelector\XPath\XPathExpr; + +/** + * XPath expression translator attribute extension. + * + * This component is a port of the Python cssselect library, + * which is copyright Ian Bicking, @see https://github.com/SimonSapin/cssselect. + * + * @author Jean-François Simon + * + * @internal + */ +class AttributeMatchingExtension extends AbstractExtension +{ + public function getAttributeMatchingTranslators(): array + { + return [ + 'exists' => $this->translateExists(...), + '=' => $this->translateEquals(...), + '~=' => $this->translateIncludes(...), + '|=' => $this->translateDashMatch(...), + '^=' => $this->translatePrefixMatch(...), + '$=' => $this->translateSuffixMatch(...), + '*=' => $this->translateSubstringMatch(...), + '!=' => $this->translateDifferent(...), + ]; + } + + public function translateExists(XPathExpr $xpath, string $attribute, ?string $value): XPathExpr + { + return $xpath->addCondition($attribute); + } + + public function translateEquals(XPathExpr $xpath, string $attribute, ?string $value): XPathExpr + { + return $xpath->addCondition(sprintf('%s = %s', $attribute, Translator::getXpathLiteral($value))); + } + + public function translateIncludes(XPathExpr $xpath, string $attribute, ?string $value): XPathExpr + { + return $xpath->addCondition($value ? sprintf( + '%1$s and contains(concat(\' \', normalize-space(%1$s), \' \'), %2$s)', + $attribute, + Translator::getXpathLiteral(' '.$value.' ') + ) : '0'); + } + + public function translateDashMatch(XPathExpr $xpath, string $attribute, ?string $value): XPathExpr + { + return $xpath->addCondition(sprintf( + '%1$s and (%1$s = %2$s or starts-with(%1$s, %3$s))', + $attribute, + Translator::getXpathLiteral($value), + Translator::getXpathLiteral($value.'-') + )); + } + + public function translatePrefixMatch(XPathExpr $xpath, string $attribute, ?string $value): XPathExpr + { + return $xpath->addCondition($value ? sprintf( + '%1$s and starts-with(%1$s, %2$s)', + $attribute, + Translator::getXpathLiteral($value) + ) : '0'); + } + + public function translateSuffixMatch(XPathExpr $xpath, string $attribute, ?string $value): XPathExpr + { + return $xpath->addCondition($value ? sprintf( + '%1$s and substring(%1$s, string-length(%1$s)-%2$s) = %3$s', + $attribute, + \strlen($value) - 1, + Translator::getXpathLiteral($value) + ) : '0'); + } + + public function translateSubstringMatch(XPathExpr $xpath, string $attribute, ?string $value): XPathExpr + { + return $xpath->addCondition($value ? sprintf( + '%1$s and contains(%1$s, %2$s)', + $attribute, + Translator::getXpathLiteral($value) + ) : '0'); + } + + public function translateDifferent(XPathExpr $xpath, string $attribute, ?string $value): XPathExpr + { + return $xpath->addCondition(sprintf( + $value ? 'not(%1$s) or %1$s != %2$s' : '%s != %s', + $attribute, + Translator::getXpathLiteral($value) + )); + } + + public function getName(): string + { + return 'attribute-matching'; + } +} diff --git a/3rdparty/symfony/css-selector/XPath/Extension/CombinationExtension.php b/3rdparty/symfony/css-selector/XPath/Extension/CombinationExtension.php new file mode 100644 index 00000000..f78d4888 --- /dev/null +++ b/3rdparty/symfony/css-selector/XPath/Extension/CombinationExtension.php @@ -0,0 +1,65 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Component\CssSelector\XPath\Extension; + +use Symfony\Component\CssSelector\XPath\XPathExpr; + +/** + * XPath expression translator combination extension. + * + * This component is a port of the Python cssselect library, + * which is copyright Ian Bicking, @see https://github.com/SimonSapin/cssselect. + * + * @author Jean-François Simon + * + * @internal + */ +class CombinationExtension extends AbstractExtension +{ + public function getCombinationTranslators(): array + { + return [ + ' ' => $this->translateDescendant(...), + '>' => $this->translateChild(...), + '+' => $this->translateDirectAdjacent(...), + '~' => $this->translateIndirectAdjacent(...), + ]; + } + + public function translateDescendant(XPathExpr $xpath, XPathExpr $combinedXpath): XPathExpr + { + return $xpath->join('/descendant-or-self::*/', $combinedXpath); + } + + public function translateChild(XPathExpr $xpath, XPathExpr $combinedXpath): XPathExpr + { + return $xpath->join('/', $combinedXpath); + } + + public function translateDirectAdjacent(XPathExpr $xpath, XPathExpr $combinedXpath): XPathExpr + { + return $xpath + ->join('/following-sibling::', $combinedXpath) + ->addNameTest() + ->addCondition('position() = 1'); + } + + public function translateIndirectAdjacent(XPathExpr $xpath, XPathExpr $combinedXpath): XPathExpr + { + return $xpath->join('/following-sibling::', $combinedXpath); + } + + public function getName(): string + { + return 'combination'; + } +} diff --git a/3rdparty/symfony/css-selector/XPath/Extension/ExtensionInterface.php b/3rdparty/symfony/css-selector/XPath/Extension/ExtensionInterface.php new file mode 100644 index 00000000..1a74b90a --- /dev/null +++ b/3rdparty/symfony/css-selector/XPath/Extension/ExtensionInterface.php @@ -0,0 +1,67 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Component\CssSelector\XPath\Extension; + +/** + * XPath expression translator extension interface. + * + * This component is a port of the Python cssselect library, + * which is copyright Ian Bicking, @see https://github.com/SimonSapin/cssselect. + * + * @author Jean-François Simon + * + * @internal + */ +interface ExtensionInterface +{ + /** + * Returns node translators. + * + * These callables will receive the node as first argument and the translator as second argument. + * + * @return callable[] + */ + public function getNodeTranslators(): array; + + /** + * Returns combination translators. + * + * @return callable[] + */ + public function getCombinationTranslators(): array; + + /** + * Returns function translators. + * + * @return callable[] + */ + public function getFunctionTranslators(): array; + + /** + * Returns pseudo-class translators. + * + * @return callable[] + */ + public function getPseudoClassTranslators(): array; + + /** + * Returns attribute operation translators. + * + * @return callable[] + */ + public function getAttributeMatchingTranslators(): array; + + /** + * Returns extension name. + */ + public function getName(): string; +} diff --git a/3rdparty/symfony/css-selector/XPath/Extension/FunctionExtension.php b/3rdparty/symfony/css-selector/XPath/Extension/FunctionExtension.php new file mode 100644 index 00000000..4b9d7bc2 --- /dev/null +++ b/3rdparty/symfony/css-selector/XPath/Extension/FunctionExtension.php @@ -0,0 +1,165 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Component\CssSelector\XPath\Extension; + +use Symfony\Component\CssSelector\Exception\ExpressionErrorException; +use Symfony\Component\CssSelector\Exception\SyntaxErrorException; +use Symfony\Component\CssSelector\Node\FunctionNode; +use Symfony\Component\CssSelector\Parser\Parser; +use Symfony\Component\CssSelector\XPath\Translator; +use Symfony\Component\CssSelector\XPath\XPathExpr; + +/** + * XPath expression translator function extension. + * + * This component is a port of the Python cssselect library, + * which is copyright Ian Bicking, @see https://github.com/SimonSapin/cssselect. + * + * @author Jean-François Simon + * + * @internal + */ +class FunctionExtension extends AbstractExtension +{ + public function getFunctionTranslators(): array + { + return [ + 'nth-child' => $this->translateNthChild(...), + 'nth-last-child' => $this->translateNthLastChild(...), + 'nth-of-type' => $this->translateNthOfType(...), + 'nth-last-of-type' => $this->translateNthLastOfType(...), + 'contains' => $this->translateContains(...), + 'lang' => $this->translateLang(...), + ]; + } + + /** + * @throws ExpressionErrorException + */ + public function translateNthChild(XPathExpr $xpath, FunctionNode $function, bool $last = false, bool $addNameTest = true): XPathExpr + { + try { + [$a, $b] = Parser::parseSeries($function->getArguments()); + } catch (SyntaxErrorException $e) { + throw new ExpressionErrorException(sprintf('Invalid series: "%s".', implode('", "', $function->getArguments())), 0, $e); + } + + $xpath->addStarPrefix(); + if ($addNameTest) { + $xpath->addNameTest(); + } + + if (0 === $a) { + return $xpath->addCondition('position() = '.($last ? 'last() - '.($b - 1) : $b)); + } + + if ($a < 0) { + if ($b < 1) { + return $xpath->addCondition('false()'); + } + + $sign = '<='; + } else { + $sign = '>='; + } + + $expr = 'position()'; + + if ($last) { + $expr = 'last() - '.$expr; + --$b; + } + + if (0 !== $b) { + $expr .= ' - '.$b; + } + + $conditions = [sprintf('%s %s 0', $expr, $sign)]; + + if (1 !== $a && -1 !== $a) { + $conditions[] = sprintf('(%s) mod %d = 0', $expr, $a); + } + + return $xpath->addCondition(implode(' and ', $conditions)); + + // todo: handle an+b, odd, even + // an+b means every-a, plus b, e.g., 2n+1 means odd + // 0n+b means b + // n+0 means a=1, i.e., all elements + // an means every a elements, i.e., 2n means even + // -n means -1n + // -1n+6 means elements 6 and previous + } + + public function translateNthLastChild(XPathExpr $xpath, FunctionNode $function): XPathExpr + { + return $this->translateNthChild($xpath, $function, true); + } + + public function translateNthOfType(XPathExpr $xpath, FunctionNode $function): XPathExpr + { + return $this->translateNthChild($xpath, $function, false, false); + } + + /** + * @throws ExpressionErrorException + */ + public function translateNthLastOfType(XPathExpr $xpath, FunctionNode $function): XPathExpr + { + if ('*' === $xpath->getElement()) { + throw new ExpressionErrorException('"*:nth-of-type()" is not implemented.'); + } + + return $this->translateNthChild($xpath, $function, true, false); + } + + /** + * @throws ExpressionErrorException + */ + public function translateContains(XPathExpr $xpath, FunctionNode $function): XPathExpr + { + $arguments = $function->getArguments(); + foreach ($arguments as $token) { + if (!($token->isString() || $token->isIdentifier())) { + throw new ExpressionErrorException('Expected a single string or identifier for :contains(), got '.implode(', ', $arguments)); + } + } + + return $xpath->addCondition(sprintf( + 'contains(string(.), %s)', + Translator::getXpathLiteral($arguments[0]->getValue()) + )); + } + + /** + * @throws ExpressionErrorException + */ + public function translateLang(XPathExpr $xpath, FunctionNode $function): XPathExpr + { + $arguments = $function->getArguments(); + foreach ($arguments as $token) { + if (!($token->isString() || $token->isIdentifier())) { + throw new ExpressionErrorException('Expected a single string or identifier for :lang(), got '.implode(', ', $arguments)); + } + } + + return $xpath->addCondition(sprintf( + 'lang(%s)', + Translator::getXpathLiteral($arguments[0]->getValue()) + )); + } + + public function getName(): string + { + return 'function'; + } +} diff --git a/3rdparty/symfony/css-selector/XPath/Extension/HtmlExtension.php b/3rdparty/symfony/css-selector/XPath/Extension/HtmlExtension.php new file mode 100644 index 00000000..4653add5 --- /dev/null +++ b/3rdparty/symfony/css-selector/XPath/Extension/HtmlExtension.php @@ -0,0 +1,178 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Component\CssSelector\XPath\Extension; + +use Symfony\Component\CssSelector\Exception\ExpressionErrorException; +use Symfony\Component\CssSelector\Node\FunctionNode; +use Symfony\Component\CssSelector\XPath\Translator; +use Symfony\Component\CssSelector\XPath\XPathExpr; + +/** + * XPath expression translator HTML extension. + * + * This component is a port of the Python cssselect library, + * which is copyright Ian Bicking, @see https://github.com/SimonSapin/cssselect. + * + * @author Jean-François Simon + * + * @internal + */ +class HtmlExtension extends AbstractExtension +{ + public function __construct(Translator $translator) + { + $translator + ->getExtension('node') + ->setFlag(NodeExtension::ELEMENT_NAME_IN_LOWER_CASE, true) + ->setFlag(NodeExtension::ATTRIBUTE_NAME_IN_LOWER_CASE, true); + } + + public function getPseudoClassTranslators(): array + { + return [ + 'checked' => $this->translateChecked(...), + 'link' => $this->translateLink(...), + 'disabled' => $this->translateDisabled(...), + 'enabled' => $this->translateEnabled(...), + 'selected' => $this->translateSelected(...), + 'invalid' => $this->translateInvalid(...), + 'hover' => $this->translateHover(...), + 'visited' => $this->translateVisited(...), + ]; + } + + public function getFunctionTranslators(): array + { + return [ + 'lang' => $this->translateLang(...), + ]; + } + + public function translateChecked(XPathExpr $xpath): XPathExpr + { + return $xpath->addCondition( + '(@checked ' + ."and (name(.) = 'input' or name(.) = 'command')" + ."and (@type = 'checkbox' or @type = 'radio'))" + ); + } + + public function translateLink(XPathExpr $xpath): XPathExpr + { + return $xpath->addCondition("@href and (name(.) = 'a' or name(.) = 'link' or name(.) = 'area')"); + } + + public function translateDisabled(XPathExpr $xpath): XPathExpr + { + return $xpath->addCondition( + '(' + .'@disabled and' + .'(' + ."(name(.) = 'input' and @type != 'hidden')" + ." or name(.) = 'button'" + ." or name(.) = 'select'" + ." or name(.) = 'textarea'" + ." or name(.) = 'command'" + ." or name(.) = 'fieldset'" + ." or name(.) = 'optgroup'" + ." or name(.) = 'option'" + .')' + .') or (' + ."(name(.) = 'input' and @type != 'hidden')" + ." or name(.) = 'button'" + ." or name(.) = 'select'" + ." or name(.) = 'textarea'" + .')' + .' and ancestor::fieldset[@disabled]' + ); + // todo: in the second half, add "and is not a descendant of that fieldset element's first legend element child, if any." + } + + public function translateEnabled(XPathExpr $xpath): XPathExpr + { + return $xpath->addCondition( + '(' + .'@href and (' + ."name(.) = 'a'" + ." or name(.) = 'link'" + ." or name(.) = 'area'" + .')' + .') or (' + .'(' + ."name(.) = 'command'" + ." or name(.) = 'fieldset'" + ." or name(.) = 'optgroup'" + .')' + .' and not(@disabled)' + .') or (' + .'(' + ."(name(.) = 'input' and @type != 'hidden')" + ." or name(.) = 'button'" + ." or name(.) = 'select'" + ." or name(.) = 'textarea'" + ." or name(.) = 'keygen'" + .')' + .' and not (@disabled or ancestor::fieldset[@disabled])' + .') or (' + ."name(.) = 'option' and not(" + .'@disabled or ancestor::optgroup[@disabled]' + .')' + .')' + ); + } + + /** + * @throws ExpressionErrorException + */ + public function translateLang(XPathExpr $xpath, FunctionNode $function): XPathExpr + { + $arguments = $function->getArguments(); + foreach ($arguments as $token) { + if (!($token->isString() || $token->isIdentifier())) { + throw new ExpressionErrorException('Expected a single string or identifier for :lang(), got '.implode(', ', $arguments)); + } + } + + return $xpath->addCondition(sprintf( + 'ancestor-or-self::*[@lang][1][starts-with(concat(' + ."translate(@%s, 'ABCDEFGHIJKLMNOPQRSTUVWXYZ', 'abcdefghijklmnopqrstuvwxyz'), '-')" + .', %s)]', + 'lang', + Translator::getXpathLiteral(strtolower($arguments[0]->getValue()).'-') + )); + } + + public function translateSelected(XPathExpr $xpath): XPathExpr + { + return $xpath->addCondition("(@selected and name(.) = 'option')"); + } + + public function translateInvalid(XPathExpr $xpath): XPathExpr + { + return $xpath->addCondition('0'); + } + + public function translateHover(XPathExpr $xpath): XPathExpr + { + return $xpath->addCondition('0'); + } + + public function translateVisited(XPathExpr $xpath): XPathExpr + { + return $xpath->addCondition('0'); + } + + public function getName(): string + { + return 'html'; + } +} diff --git a/3rdparty/symfony/css-selector/XPath/Extension/NodeExtension.php b/3rdparty/symfony/css-selector/XPath/Extension/NodeExtension.php new file mode 100644 index 00000000..49e894ad --- /dev/null +++ b/3rdparty/symfony/css-selector/XPath/Extension/NodeExtension.php @@ -0,0 +1,191 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Component\CssSelector\XPath\Extension; + +use Symfony\Component\CssSelector\Node; +use Symfony\Component\CssSelector\XPath\Translator; +use Symfony\Component\CssSelector\XPath\XPathExpr; + +/** + * XPath expression translator node extension. + * + * This component is a port of the Python cssselect library, + * which is copyright Ian Bicking, @see https://github.com/SimonSapin/cssselect. + * + * @author Jean-François Simon + * + * @internal + */ +class NodeExtension extends AbstractExtension +{ + public const ELEMENT_NAME_IN_LOWER_CASE = 1; + public const ATTRIBUTE_NAME_IN_LOWER_CASE = 2; + public const ATTRIBUTE_VALUE_IN_LOWER_CASE = 4; + + private int $flags; + + public function __construct(int $flags = 0) + { + $this->flags = $flags; + } + + /** + * @return $this + */ + public function setFlag(int $flag, bool $on): static + { + if ($on && !$this->hasFlag($flag)) { + $this->flags += $flag; + } + + if (!$on && $this->hasFlag($flag)) { + $this->flags -= $flag; + } + + return $this; + } + + public function hasFlag(int $flag): bool + { + return (bool) ($this->flags & $flag); + } + + public function getNodeTranslators(): array + { + return [ + 'Selector' => $this->translateSelector(...), + 'CombinedSelector' => $this->translateCombinedSelector(...), + 'Negation' => $this->translateNegation(...), + 'Function' => $this->translateFunction(...), + 'Pseudo' => $this->translatePseudo(...), + 'Attribute' => $this->translateAttribute(...), + 'Class' => $this->translateClass(...), + 'Hash' => $this->translateHash(...), + 'Element' => $this->translateElement(...), + ]; + } + + public function translateSelector(Node\SelectorNode $node, Translator $translator): XPathExpr + { + return $translator->nodeToXPath($node->getTree()); + } + + public function translateCombinedSelector(Node\CombinedSelectorNode $node, Translator $translator): XPathExpr + { + return $translator->addCombination($node->getCombinator(), $node->getSelector(), $node->getSubSelector()); + } + + public function translateNegation(Node\NegationNode $node, Translator $translator): XPathExpr + { + $xpath = $translator->nodeToXPath($node->getSelector()); + $subXpath = $translator->nodeToXPath($node->getSubSelector()); + $subXpath->addNameTest(); + + if ($subXpath->getCondition()) { + return $xpath->addCondition(sprintf('not(%s)', $subXpath->getCondition())); + } + + return $xpath->addCondition('0'); + } + + public function translateFunction(Node\FunctionNode $node, Translator $translator): XPathExpr + { + $xpath = $translator->nodeToXPath($node->getSelector()); + + return $translator->addFunction($xpath, $node); + } + + public function translatePseudo(Node\PseudoNode $node, Translator $translator): XPathExpr + { + $xpath = $translator->nodeToXPath($node->getSelector()); + + return $translator->addPseudoClass($xpath, $node->getIdentifier()); + } + + public function translateAttribute(Node\AttributeNode $node, Translator $translator): XPathExpr + { + $name = $node->getAttribute(); + $safe = $this->isSafeName($name); + + if ($this->hasFlag(self::ATTRIBUTE_NAME_IN_LOWER_CASE)) { + $name = strtolower($name); + } + + if ($node->getNamespace()) { + $name = sprintf('%s:%s', $node->getNamespace(), $name); + $safe = $safe && $this->isSafeName($node->getNamespace()); + } + + $attribute = $safe ? '@'.$name : sprintf('attribute::*[name() = %s]', Translator::getXpathLiteral($name)); + $value = $node->getValue(); + $xpath = $translator->nodeToXPath($node->getSelector()); + + if ($this->hasFlag(self::ATTRIBUTE_VALUE_IN_LOWER_CASE)) { + $value = strtolower($value); + } + + return $translator->addAttributeMatching($xpath, $node->getOperator(), $attribute, $value); + } + + public function translateClass(Node\ClassNode $node, Translator $translator): XPathExpr + { + $xpath = $translator->nodeToXPath($node->getSelector()); + + return $translator->addAttributeMatching($xpath, '~=', '@class', $node->getName()); + } + + public function translateHash(Node\HashNode $node, Translator $translator): XPathExpr + { + $xpath = $translator->nodeToXPath($node->getSelector()); + + return $translator->addAttributeMatching($xpath, '=', '@id', $node->getId()); + } + + public function translateElement(Node\ElementNode $node): XPathExpr + { + $element = $node->getElement(); + + if ($element && $this->hasFlag(self::ELEMENT_NAME_IN_LOWER_CASE)) { + $element = strtolower($element); + } + + if ($element) { + $safe = $this->isSafeName($element); + } else { + $element = '*'; + $safe = true; + } + + if ($node->getNamespace()) { + $element = sprintf('%s:%s', $node->getNamespace(), $element); + $safe = $safe && $this->isSafeName($node->getNamespace()); + } + + $xpath = new XPathExpr('', $element); + + if (!$safe) { + $xpath->addNameTest(); + } + + return $xpath; + } + + public function getName(): string + { + return 'node'; + } + + private function isSafeName(string $name): bool + { + return 0 < preg_match('~^[a-zA-Z_][a-zA-Z0-9_.-]*$~', $name); + } +} diff --git a/3rdparty/symfony/css-selector/XPath/Extension/PseudoClassExtension.php b/3rdparty/symfony/css-selector/XPath/Extension/PseudoClassExtension.php new file mode 100644 index 00000000..aada8329 --- /dev/null +++ b/3rdparty/symfony/css-selector/XPath/Extension/PseudoClassExtension.php @@ -0,0 +1,122 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Component\CssSelector\XPath\Extension; + +use Symfony\Component\CssSelector\Exception\ExpressionErrorException; +use Symfony\Component\CssSelector\XPath\XPathExpr; + +/** + * XPath expression translator pseudo-class extension. + * + * This component is a port of the Python cssselect library, + * which is copyright Ian Bicking, @see https://github.com/SimonSapin/cssselect. + * + * @author Jean-François Simon + * + * @internal + */ +class PseudoClassExtension extends AbstractExtension +{ + public function getPseudoClassTranslators(): array + { + return [ + 'root' => $this->translateRoot(...), + 'scope' => $this->translateScopePseudo(...), + 'first-child' => $this->translateFirstChild(...), + 'last-child' => $this->translateLastChild(...), + 'first-of-type' => $this->translateFirstOfType(...), + 'last-of-type' => $this->translateLastOfType(...), + 'only-child' => $this->translateOnlyChild(...), + 'only-of-type' => $this->translateOnlyOfType(...), + 'empty' => $this->translateEmpty(...), + ]; + } + + public function translateRoot(XPathExpr $xpath): XPathExpr + { + return $xpath->addCondition('not(parent::*)'); + } + + public function translateScopePseudo(XPathExpr $xpath): XPathExpr + { + return $xpath->addCondition('1'); + } + + public function translateFirstChild(XPathExpr $xpath): XPathExpr + { + return $xpath + ->addStarPrefix() + ->addNameTest() + ->addCondition('position() = 1'); + } + + public function translateLastChild(XPathExpr $xpath): XPathExpr + { + return $xpath + ->addStarPrefix() + ->addNameTest() + ->addCondition('position() = last()'); + } + + /** + * @throws ExpressionErrorException + */ + public function translateFirstOfType(XPathExpr $xpath): XPathExpr + { + if ('*' === $xpath->getElement()) { + throw new ExpressionErrorException('"*:first-of-type" is not implemented.'); + } + + return $xpath + ->addStarPrefix() + ->addCondition('position() = 1'); + } + + /** + * @throws ExpressionErrorException + */ + public function translateLastOfType(XPathExpr $xpath): XPathExpr + { + if ('*' === $xpath->getElement()) { + throw new ExpressionErrorException('"*:last-of-type" is not implemented.'); + } + + return $xpath + ->addStarPrefix() + ->addCondition('position() = last()'); + } + + public function translateOnlyChild(XPathExpr $xpath): XPathExpr + { + return $xpath + ->addStarPrefix() + ->addNameTest() + ->addCondition('last() = 1'); + } + + public function translateOnlyOfType(XPathExpr $xpath): XPathExpr + { + $element = $xpath->getElement(); + + return $xpath->addCondition(sprintf('count(preceding-sibling::%s)=0 and count(following-sibling::%s)=0', $element, $element)); + } + + public function translateEmpty(XPathExpr $xpath): XPathExpr + { + return $xpath->addCondition('not(*) and not(string-length())'); + } + + public function getName(): string + { + return 'pseudo-class'; + } +} diff --git a/3rdparty/symfony/css-selector/XPath/Translator.php b/3rdparty/symfony/css-selector/XPath/Translator.php new file mode 100644 index 00000000..9e66ce7d --- /dev/null +++ b/3rdparty/symfony/css-selector/XPath/Translator.php @@ -0,0 +1,224 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Component\CssSelector\XPath; + +use Symfony\Component\CssSelector\Exception\ExpressionErrorException; +use Symfony\Component\CssSelector\Node\FunctionNode; +use Symfony\Component\CssSelector\Node\NodeInterface; +use Symfony\Component\CssSelector\Node\SelectorNode; +use Symfony\Component\CssSelector\Parser\Parser; +use Symfony\Component\CssSelector\Parser\ParserInterface; + +/** + * XPath expression translator interface. + * + * This component is a port of the Python cssselect library, + * which is copyright Ian Bicking, @see https://github.com/SimonSapin/cssselect. + * + * @author Jean-François Simon + * + * @internal + */ +class Translator implements TranslatorInterface +{ + private ParserInterface $mainParser; + + /** + * @var ParserInterface[] + */ + private array $shortcutParsers = []; + + /** + * @var Extension\ExtensionInterface[] + */ + private array $extensions = []; + + private array $nodeTranslators = []; + private array $combinationTranslators = []; + private array $functionTranslators = []; + private array $pseudoClassTranslators = []; + private array $attributeMatchingTranslators = []; + + public function __construct(?ParserInterface $parser = null) + { + $this->mainParser = $parser ?? new Parser(); + + $this + ->registerExtension(new Extension\NodeExtension()) + ->registerExtension(new Extension\CombinationExtension()) + ->registerExtension(new Extension\FunctionExtension()) + ->registerExtension(new Extension\PseudoClassExtension()) + ->registerExtension(new Extension\AttributeMatchingExtension()) + ; + } + + public static function getXpathLiteral(string $element): string + { + if (!str_contains($element, "'")) { + return "'".$element."'"; + } + + if (!str_contains($element, '"')) { + return '"'.$element.'"'; + } + + $string = $element; + $parts = []; + while (true) { + if (false !== $pos = strpos($string, "'")) { + $parts[] = sprintf("'%s'", substr($string, 0, $pos)); + $parts[] = "\"'\""; + $string = substr($string, $pos + 1); + } else { + $parts[] = "'$string'"; + break; + } + } + + return sprintf('concat(%s)', implode(', ', $parts)); + } + + public function cssToXPath(string $cssExpr, string $prefix = 'descendant-or-self::'): string + { + $selectors = $this->parseSelectors($cssExpr); + + /** @var SelectorNode $selector */ + foreach ($selectors as $index => $selector) { + if (null !== $selector->getPseudoElement()) { + throw new ExpressionErrorException('Pseudo-elements are not supported.'); + } + + $selectors[$index] = $this->selectorToXPath($selector, $prefix); + } + + return implode(' | ', $selectors); + } + + public function selectorToXPath(SelectorNode $selector, string $prefix = 'descendant-or-self::'): string + { + return ($prefix ?: '').$this->nodeToXPath($selector); + } + + /** + * @return $this + */ + public function registerExtension(Extension\ExtensionInterface $extension): static + { + $this->extensions[$extension->getName()] = $extension; + + $this->nodeTranslators = array_merge($this->nodeTranslators, $extension->getNodeTranslators()); + $this->combinationTranslators = array_merge($this->combinationTranslators, $extension->getCombinationTranslators()); + $this->functionTranslators = array_merge($this->functionTranslators, $extension->getFunctionTranslators()); + $this->pseudoClassTranslators = array_merge($this->pseudoClassTranslators, $extension->getPseudoClassTranslators()); + $this->attributeMatchingTranslators = array_merge($this->attributeMatchingTranslators, $extension->getAttributeMatchingTranslators()); + + return $this; + } + + /** + * @throws ExpressionErrorException + */ + public function getExtension(string $name): Extension\ExtensionInterface + { + if (!isset($this->extensions[$name])) { + throw new ExpressionErrorException(sprintf('Extension "%s" not registered.', $name)); + } + + return $this->extensions[$name]; + } + + /** + * @return $this + */ + public function registerParserShortcut(ParserInterface $shortcut): static + { + $this->shortcutParsers[] = $shortcut; + + return $this; + } + + /** + * @throws ExpressionErrorException + */ + public function nodeToXPath(NodeInterface $node): XPathExpr + { + if (!isset($this->nodeTranslators[$node->getNodeName()])) { + throw new ExpressionErrorException(sprintf('Node "%s" not supported.', $node->getNodeName())); + } + + return $this->nodeTranslators[$node->getNodeName()]($node, $this); + } + + /** + * @throws ExpressionErrorException + */ + public function addCombination(string $combiner, NodeInterface $xpath, NodeInterface $combinedXpath): XPathExpr + { + if (!isset($this->combinationTranslators[$combiner])) { + throw new ExpressionErrorException(sprintf('Combiner "%s" not supported.', $combiner)); + } + + return $this->combinationTranslators[$combiner]($this->nodeToXPath($xpath), $this->nodeToXPath($combinedXpath)); + } + + /** + * @throws ExpressionErrorException + */ + public function addFunction(XPathExpr $xpath, FunctionNode $function): XPathExpr + { + if (!isset($this->functionTranslators[$function->getName()])) { + throw new ExpressionErrorException(sprintf('Function "%s" not supported.', $function->getName())); + } + + return $this->functionTranslators[$function->getName()]($xpath, $function); + } + + /** + * @throws ExpressionErrorException + */ + public function addPseudoClass(XPathExpr $xpath, string $pseudoClass): XPathExpr + { + if (!isset($this->pseudoClassTranslators[$pseudoClass])) { + throw new ExpressionErrorException(sprintf('Pseudo-class "%s" not supported.', $pseudoClass)); + } + + return $this->pseudoClassTranslators[$pseudoClass]($xpath); + } + + /** + * @throws ExpressionErrorException + */ + public function addAttributeMatching(XPathExpr $xpath, string $operator, string $attribute, ?string $value): XPathExpr + { + if (!isset($this->attributeMatchingTranslators[$operator])) { + throw new ExpressionErrorException(sprintf('Attribute matcher operator "%s" not supported.', $operator)); + } + + return $this->attributeMatchingTranslators[$operator]($xpath, $attribute, $value); + } + + /** + * @return SelectorNode[] + */ + private function parseSelectors(string $css): array + { + foreach ($this->shortcutParsers as $shortcut) { + $tokens = $shortcut->parse($css); + + if ($tokens) { + return $tokens; + } + } + + return $this->mainParser->parse($css); + } +} diff --git a/3rdparty/symfony/css-selector/XPath/TranslatorInterface.php b/3rdparty/symfony/css-selector/XPath/TranslatorInterface.php new file mode 100644 index 00000000..c19eefb9 --- /dev/null +++ b/3rdparty/symfony/css-selector/XPath/TranslatorInterface.php @@ -0,0 +1,37 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Component\CssSelector\XPath; + +use Symfony\Component\CssSelector\Node\SelectorNode; + +/** + * XPath expression translator interface. + * + * This component is a port of the Python cssselect library, + * which is copyright Ian Bicking, @see https://github.com/SimonSapin/cssselect. + * + * @author Jean-François Simon + * + * @internal + */ +interface TranslatorInterface +{ + /** + * Translates a CSS selector to an XPath expression. + */ + public function cssToXPath(string $cssExpr, string $prefix = 'descendant-or-self::'): string; + + /** + * Translates a parsed selector node to an XPath expression. + */ + public function selectorToXPath(SelectorNode $selector, string $prefix = 'descendant-or-self::'): string; +} diff --git a/3rdparty/symfony/css-selector/XPath/XPathExpr.php b/3rdparty/symfony/css-selector/XPath/XPathExpr.php new file mode 100644 index 00000000..a76e30be --- /dev/null +++ b/3rdparty/symfony/css-selector/XPath/XPathExpr.php @@ -0,0 +1,111 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Component\CssSelector\XPath; + +/** + * XPath expression translator interface. + * + * This component is a port of the Python cssselect library, + * which is copyright Ian Bicking, @see https://github.com/SimonSapin/cssselect. + * + * @author Jean-François Simon + * + * @internal + */ +class XPathExpr +{ + private string $path; + private string $element; + private string $condition; + + public function __construct(string $path = '', string $element = '*', string $condition = '', bool $starPrefix = false) + { + $this->path = $path; + $this->element = $element; + $this->condition = $condition; + + if ($starPrefix) { + $this->addStarPrefix(); + } + } + + public function getElement(): string + { + return $this->element; + } + + /** + * @return $this + */ + public function addCondition(string $condition): static + { + $this->condition = $this->condition ? sprintf('(%s) and (%s)', $this->condition, $condition) : $condition; + + return $this; + } + + public function getCondition(): string + { + return $this->condition; + } + + /** + * @return $this + */ + public function addNameTest(): static + { + if ('*' !== $this->element) { + $this->addCondition('name() = '.Translator::getXpathLiteral($this->element)); + $this->element = '*'; + } + + return $this; + } + + /** + * @return $this + */ + public function addStarPrefix(): static + { + $this->path .= '*/'; + + return $this; + } + + /** + * Joins another XPathExpr with a combiner. + * + * @return $this + */ + public function join(string $combiner, self $expr): static + { + $path = $this->__toString().$combiner; + + if ('*/' !== $expr->path) { + $path .= $expr->path; + } + + $this->path = $path; + $this->element = $expr->element; + $this->condition = $expr->condition; + + return $this; + } + + public function __toString(): string + { + $path = $this->path.$this->element; + $condition = null === $this->condition || '' === $this->condition ? '' : '['.$this->condition.']'; + + return $path.$condition; + } +} diff --git a/3rdparty/symfony/deprecation-contracts/LICENSE b/3rdparty/symfony/deprecation-contracts/LICENSE new file mode 100644 index 00000000..0ed3a246 --- /dev/null +++ b/3rdparty/symfony/deprecation-contracts/LICENSE @@ -0,0 +1,19 @@ +Copyright (c) 2020-present Fabien Potencier + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is furnished +to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +THE SOFTWARE. diff --git a/3rdparty/symfony/deprecation-contracts/function.php b/3rdparty/symfony/deprecation-contracts/function.php new file mode 100644 index 00000000..2d56512b --- /dev/null +++ b/3rdparty/symfony/deprecation-contracts/function.php @@ -0,0 +1,27 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +if (!function_exists('trigger_deprecation')) { + /** + * Triggers a silenced deprecation notice. + * + * @param string $package The name of the Composer package that is triggering the deprecation + * @param string $version The version of the package that introduced the deprecation + * @param string $message The message of the deprecation + * @param mixed ...$args Values to insert in the message using printf() formatting + * + * @author Nicolas Grekas + */ + function trigger_deprecation(string $package, string $version, string $message, mixed ...$args): void + { + @trigger_error(($package || $version ? "Since $package $version: " : '').($args ? vsprintf($message, $args) : $message), \E_USER_DEPRECATED); + } +} diff --git a/3rdparty/symfony/dom-crawler/AbstractUriElement.php b/3rdparty/symfony/dom-crawler/AbstractUriElement.php new file mode 100644 index 00000000..6d5846a8 --- /dev/null +++ b/3rdparty/symfony/dom-crawler/AbstractUriElement.php @@ -0,0 +1,123 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Component\DomCrawler; + +/** + * Any HTML element that can link to an URI. + * + * @author Fabien Potencier + */ +abstract class AbstractUriElement +{ + /** + * @var \DOMElement + */ + protected $node; + + /** + * @var string|null The method to use for the element + */ + protected $method; + + /** + * @var string The URI of the page where the element is embedded (or the base href) + */ + protected $currentUri; + + /** + * @param \DOMElement $node A \DOMElement instance + * @param string|null $currentUri The URI of the page where the link is embedded (or the base href) + * @param string|null $method The method to use for the link (GET by default) + * + * @throws \InvalidArgumentException if the node is not a link + */ + public function __construct(\DOMElement $node, ?string $currentUri = null, ?string $method = 'GET') + { + $this->setNode($node); + $this->method = $method ? strtoupper($method) : null; + $this->currentUri = $currentUri; + + $elementUriIsRelative = !parse_url(trim($this->getRawUri()), \PHP_URL_SCHEME); + $baseUriIsAbsolute = null !== $this->currentUri && \in_array(strtolower(substr($this->currentUri, 0, 4)), ['http', 'file']); + if ($elementUriIsRelative && !$baseUriIsAbsolute) { + throw new \InvalidArgumentException(sprintf('The URL of the element is relative, so you must define its base URI passing an absolute URL to the constructor of the "%s" class ("%s" was passed).', __CLASS__, $this->currentUri)); + } + } + + /** + * Gets the node associated with this link. + */ + public function getNode(): \DOMElement + { + return $this->node; + } + + /** + * Gets the method associated with this link. + */ + public function getMethod(): string + { + return $this->method ?? 'GET'; + } + + /** + * Gets the URI associated with this link. + */ + public function getUri(): string + { + return UriResolver::resolve($this->getRawUri(), $this->currentUri); + } + + /** + * Returns raw URI data. + */ + abstract protected function getRawUri(): string; + + /** + * Returns the canonicalized URI path (see RFC 3986, section 5.2.4). + * + * @param string $path URI path + */ + protected function canonicalizePath(string $path): string + { + if ('' === $path || '/' === $path) { + return $path; + } + + if (str_ends_with($path, '.')) { + $path .= '/'; + } + + $output = []; + + foreach (explode('/', $path) as $segment) { + if ('..' === $segment) { + array_pop($output); + } elseif ('.' !== $segment) { + $output[] = $segment; + } + } + + return implode('/', $output); + } + + /** + * Sets current \DOMElement instance. + * + * @param \DOMElement $node A \DOMElement instance + * + * @return void + * + * @throws \LogicException If given node is not an anchor + */ + abstract protected function setNode(\DOMElement $node); +} diff --git a/3rdparty/symfony/dom-crawler/Crawler.php b/3rdparty/symfony/dom-crawler/Crawler.php new file mode 100644 index 00000000..71e8528f --- /dev/null +++ b/3rdparty/symfony/dom-crawler/Crawler.php @@ -0,0 +1,1265 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Component\DomCrawler; + +use Masterminds\HTML5; +use Symfony\Component\CssSelector\CssSelectorConverter; + +/** + * Crawler eases navigation of a list of \DOMNode objects. + * + * @author Fabien Potencier + * + * @implements \IteratorAggregate + */ +class Crawler implements \Countable, \IteratorAggregate +{ + /** + * @var string|null + */ + protected $uri; + + /** + * The default namespace prefix to be used with XPath and CSS expressions. + */ + private string $defaultNamespacePrefix = 'default'; + + /** + * A map of manually registered namespaces. + * + * @var array + */ + private array $namespaces = []; + + /** + * A map of cached namespaces. + */ + private \ArrayObject $cachedNamespaces; + + private ?string $baseHref; + private ?\DOMDocument $document = null; + + /** + * @var list<\DOMNode> + */ + private array $nodes = []; + + /** + * Whether the Crawler contains HTML or XML content (used when converting CSS to XPath). + */ + private bool $isHtml = true; + + private ?HTML5 $html5Parser = null; + + /** + * @param \DOMNodeList|\DOMNode|\DOMNode[]|string|null $node A Node to use as the base for the crawling + */ + public function __construct(\DOMNodeList|\DOMNode|array|string|null $node = null, ?string $uri = null, ?string $baseHref = null, bool $useHtml5Parser = true) + { + $this->uri = $uri; + $this->baseHref = $baseHref ?: $uri; + $this->html5Parser = $useHtml5Parser ? new HTML5(['disable_html_ns' => true]) : null; + $this->cachedNamespaces = new \ArrayObject(); + + $this->add($node); + } + + /** + * Returns the current URI. + */ + public function getUri(): ?string + { + return $this->uri; + } + + /** + * Returns base href. + */ + public function getBaseHref(): ?string + { + return $this->baseHref; + } + + /** + * Removes all the nodes. + * + * @return void + */ + public function clear() + { + $this->nodes = []; + $this->document = null; + $this->cachedNamespaces = new \ArrayObject(); + } + + /** + * Adds a node to the current list of nodes. + * + * This method uses the appropriate specialized add*() method based + * on the type of the argument. + * + * @param \DOMNodeList|\DOMNode|\DOMNode[]|string|null $node A node + * + * @return void + * + * @throws \InvalidArgumentException when node is not the expected type + */ + public function add(\DOMNodeList|\DOMNode|array|string|null $node) + { + if ($node instanceof \DOMNodeList) { + $this->addNodeList($node); + } elseif ($node instanceof \DOMNode) { + $this->addNode($node); + } elseif (\is_array($node)) { + $this->addNodes($node); + } elseif (\is_string($node)) { + $this->addContent($node); + } elseif (null !== $node) { + throw new \InvalidArgumentException(sprintf('Expecting a DOMNodeList or DOMNode instance, an array, a string, or null, but got "%s".', get_debug_type($node))); + } + } + + /** + * Adds HTML/XML content. + * + * If the charset is not set via the content type, it is assumed to be UTF-8, + * or ISO-8859-1 as a fallback, which is the default charset defined by the + * HTTP 1.1 specification. + * + * @return void + */ + public function addContent(string $content, ?string $type = null) + { + if (empty($type)) { + $type = str_starts_with($content, 'convertToHtmlEntities('charset=', $m[2])) { + $charset = $m[2]; + } + + return $m[1].$charset; + }, $content, 1); + + if ('x' === $xmlMatches[1]) { + $this->addXmlContent($content, $charset); + } else { + $this->addHtmlContent($content, $charset); + } + } + + /** + * Adds an HTML content to the list of nodes. + * + * The libxml errors are disabled when the content is parsed. + * + * If you want to get parsing errors, be sure to enable + * internal errors via libxml_use_internal_errors(true) + * and then, get the errors via libxml_get_errors(). Be + * sure to clear errors with libxml_clear_errors() afterward. + * + * @return void + */ + public function addHtmlContent(string $content, string $charset = 'UTF-8') + { + $dom = $this->parseHtmlString($content, $charset); + $this->addDocument($dom); + + $base = $this->filterRelativeXPath('descendant-or-self::base')->extract(['href']); + + $baseHref = current($base); + if (\count($base) && !empty($baseHref)) { + if ($this->baseHref) { + $linkNode = $dom->createElement('a'); + $linkNode->setAttribute('href', $baseHref); + $link = new Link($linkNode, $this->baseHref); + $this->baseHref = $link->getUri(); + } else { + $this->baseHref = $baseHref; + } + } + } + + /** + * Adds an XML content to the list of nodes. + * + * The libxml errors are disabled when the content is parsed. + * + * If you want to get parsing errors, be sure to enable + * internal errors via libxml_use_internal_errors(true) + * and then, get the errors via libxml_get_errors(). Be + * sure to clear errors with libxml_clear_errors() afterward. + * + * @param int $options Bitwise OR of the libxml option constants + * LIBXML_PARSEHUGE is dangerous, see + * http://symfony.com/blog/security-release-symfony-2-0-17-released + * + * @return void + */ + public function addXmlContent(string $content, string $charset = 'UTF-8', int $options = \LIBXML_NONET) + { + // remove the default namespace if it's the only namespace to make XPath expressions simpler + if (!str_contains($content, 'xmlns:')) { + $content = str_replace('xmlns', 'ns', $content); + } + + $internalErrors = libxml_use_internal_errors(true); + + $dom = new \DOMDocument('1.0', $charset); + $dom->validateOnParse = true; + + if ('' !== trim($content)) { + @$dom->loadXML($content, $options); + } + + libxml_use_internal_errors($internalErrors); + + $this->addDocument($dom); + + $this->isHtml = false; + } + + /** + * Adds a \DOMDocument to the list of nodes. + * + * @param \DOMDocument $dom A \DOMDocument instance + * + * @return void + */ + public function addDocument(\DOMDocument $dom) + { + if ($dom->documentElement) { + $this->addNode($dom->documentElement); + } + } + + /** + * Adds a \DOMNodeList to the list of nodes. + * + * @param \DOMNodeList $nodes A \DOMNodeList instance + * + * @return void + */ + public function addNodeList(\DOMNodeList $nodes) + { + foreach ($nodes as $node) { + if ($node instanceof \DOMNode) { + $this->addNode($node); + } + } + } + + /** + * Adds an array of \DOMNode instances to the list of nodes. + * + * @param \DOMNode[] $nodes An array of \DOMNode instances + * + * @return void + */ + public function addNodes(array $nodes) + { + foreach ($nodes as $node) { + $this->add($node); + } + } + + /** + * Adds a \DOMNode instance to the list of nodes. + * + * @param \DOMNode $node A \DOMNode instance + * + * @return void + */ + public function addNode(\DOMNode $node) + { + if ($node instanceof \DOMDocument) { + $node = $node->documentElement; + } + + if (null !== $this->document && $this->document !== $node->ownerDocument) { + throw new \InvalidArgumentException('Attaching DOM nodes from multiple documents in the same crawler is forbidden.'); + } + + $this->document ??= $node->ownerDocument; + + // Don't add duplicate nodes in the Crawler + if (\in_array($node, $this->nodes, true)) { + return; + } + + $this->nodes[] = $node; + } + + /** + * Returns a node given its position in the node list. + */ + public function eq(int $position): static + { + if (isset($this->nodes[$position])) { + return $this->createSubCrawler($this->nodes[$position]); + } + + return $this->createSubCrawler(null); + } + + /** + * Calls an anonymous function on each node of the list. + * + * The anonymous function receives the position and the node wrapped + * in a Crawler instance as arguments. + * + * Example: + * + * $crawler->filter('h1')->each(function ($node, $i) { + * return $node->text(); + * }); + * + * @param \Closure $closure An anonymous function + * + * @return array An array of values returned by the anonymous function + */ + public function each(\Closure $closure): array + { + $data = []; + foreach ($this->nodes as $i => $node) { + $data[] = $closure($this->createSubCrawler($node), $i); + } + + return $data; + } + + /** + * Slices the list of nodes by $offset and $length. + */ + public function slice(int $offset = 0, ?int $length = null): static + { + return $this->createSubCrawler(\array_slice($this->nodes, $offset, $length)); + } + + /** + * Reduces the list of nodes by calling an anonymous function. + * + * To remove a node from the list, the anonymous function must return false. + * + * @param \Closure $closure An anonymous function + */ + public function reduce(\Closure $closure): static + { + $nodes = []; + foreach ($this->nodes as $i => $node) { + if (false !== $closure($this->createSubCrawler($node), $i)) { + $nodes[] = $node; + } + } + + return $this->createSubCrawler($nodes); + } + + /** + * Returns the first node of the current selection. + */ + public function first(): static + { + return $this->eq(0); + } + + /** + * Returns the last node of the current selection. + */ + public function last(): static + { + return $this->eq(\count($this->nodes) - 1); + } + + /** + * Returns the siblings nodes of the current selection. + * + * @throws \InvalidArgumentException When current node is empty + */ + public function siblings(): static + { + if (!$this->nodes) { + throw new \InvalidArgumentException('The current node list is empty.'); + } + + return $this->createSubCrawler($this->sibling($this->getNode(0)->parentNode->firstChild)); + } + + public function matches(string $selector): bool + { + if (!$this->nodes) { + return false; + } + + $converter = $this->createCssSelectorConverter(); + $xpath = $converter->toXPath($selector, 'self::'); + + return 0 !== $this->filterRelativeXPath($xpath)->count(); + } + + /** + * Return first parents (heading toward the document root) of the Element that matches the provided selector. + * + * @see https://developer.mozilla.org/en-US/docs/Web/API/Element/closest#Polyfill + * + * @throws \InvalidArgumentException When current node is empty + */ + public function closest(string $selector): ?self + { + if (!$this->nodes) { + throw new \InvalidArgumentException('The current node list is empty.'); + } + + $domNode = $this->getNode(0); + + while (null !== $domNode && \XML_ELEMENT_NODE === $domNode->nodeType) { + $node = $this->createSubCrawler($domNode); + if ($node->matches($selector)) { + return $node; + } + + $domNode = $node->getNode(0)->parentNode; + } + + return null; + } + + /** + * Returns the next siblings nodes of the current selection. + * + * @throws \InvalidArgumentException When current node is empty + */ + public function nextAll(): static + { + if (!$this->nodes) { + throw new \InvalidArgumentException('The current node list is empty.'); + } + + return $this->createSubCrawler($this->sibling($this->getNode(0))); + } + + /** + * Returns the previous sibling nodes of the current selection. + * + * @throws \InvalidArgumentException + */ + public function previousAll(): static + { + if (!$this->nodes) { + throw new \InvalidArgumentException('The current node list is empty.'); + } + + return $this->createSubCrawler($this->sibling($this->getNode(0), 'previousSibling')); + } + + /** + * Returns the ancestors of the current selection. + * + * @throws \InvalidArgumentException When the current node is empty + */ + public function ancestors(): static + { + if (!$this->nodes) { + throw new \InvalidArgumentException('The current node list is empty.'); + } + + $node = $this->getNode(0); + $nodes = []; + + while ($node = $node->parentNode) { + if (\XML_ELEMENT_NODE === $node->nodeType) { + $nodes[] = $node; + } + } + + return $this->createSubCrawler($nodes); + } + + /** + * Returns the children nodes of the current selection. + * + * @throws \InvalidArgumentException When current node is empty + * @throws \RuntimeException If the CssSelector Component is not available and $selector is provided + */ + public function children(?string $selector = null): static + { + if (!$this->nodes) { + throw new \InvalidArgumentException('The current node list is empty.'); + } + + if (null !== $selector) { + $converter = $this->createCssSelectorConverter(); + $xpath = $converter->toXPath($selector, 'child::'); + + return $this->filterRelativeXPath($xpath); + } + + $node = $this->getNode(0)->firstChild; + + return $this->createSubCrawler($node ? $this->sibling($node) : []); + } + + /** + * Returns the attribute value of the first node of the list. + * + * @param string|null $default When not null: the value to return when the node or attribute is empty + * + * @throws \InvalidArgumentException When current node is empty + */ + public function attr(string $attribute/* , string $default = null */): ?string + { + $default = \func_num_args() > 1 ? func_get_arg(1) : null; + if (!$this->nodes) { + if (null !== $default) { + return $default; + } + + throw new \InvalidArgumentException('The current node list is empty.'); + } + + $node = $this->getNode(0); + + return $node->hasAttribute($attribute) ? $node->getAttribute($attribute) : $default; + } + + /** + * Returns the node name of the first node of the list. + * + * @throws \InvalidArgumentException When current node is empty + */ + public function nodeName(): string + { + if (!$this->nodes) { + throw new \InvalidArgumentException('The current node list is empty.'); + } + + return $this->getNode(0)->nodeName; + } + + /** + * Returns the text of the first node of the list. + * + * Pass true as the second argument to normalize whitespaces. + * + * @param string|null $default When not null: the value to return when the current node is empty + * @param bool $normalizeWhitespace Whether whitespaces should be trimmed and normalized to single spaces + * + * @throws \InvalidArgumentException When current node is empty + */ + public function text(?string $default = null, bool $normalizeWhitespace = true): string + { + if (!$this->nodes) { + if (null !== $default) { + return $default; + } + + throw new \InvalidArgumentException('The current node list is empty.'); + } + + $text = $this->getNode(0)->nodeValue; + + if ($normalizeWhitespace) { + return $this->normalizeWhitespace($text); + } + + return $text; + } + + /** + * Returns only the inner text that is the direct descendent of the current node, excluding any child nodes. + * + * @param bool $normalizeWhitespace Whether whitespaces should be trimmed and normalized to single spaces + */ + public function innerText(/* bool $normalizeWhitespace = true */): string + { + $normalizeWhitespace = 1 <= \func_num_args() ? func_get_arg(0) : true; + + foreach ($this->getNode(0)->childNodes as $childNode) { + if (\XML_TEXT_NODE !== $childNode->nodeType && \XML_CDATA_SECTION_NODE !== $childNode->nodeType) { + continue; + } + if (!$normalizeWhitespace) { + return $childNode->nodeValue; + } + if ('' !== trim($childNode->nodeValue)) { + return $this->normalizeWhitespace($childNode->nodeValue); + } + } + + return ''; + } + + /** + * Returns the first node of the list as HTML. + * + * @param string|null $default When not null: the value to return when the current node is empty + * + * @throws \InvalidArgumentException When current node is empty + */ + public function html(?string $default = null): string + { + if (!$this->nodes) { + if (null !== $default) { + return $default; + } + + throw new \InvalidArgumentException('The current node list is empty.'); + } + + $node = $this->getNode(0); + $owner = $node->ownerDocument; + + if ($this->html5Parser && '' === $owner->saveXML($owner->childNodes[0])) { + $owner = $this->html5Parser; + } + + $html = ''; + foreach ($node->childNodes as $child) { + $html .= $owner->saveHTML($child); + } + + return $html; + } + + public function outerHtml(): string + { + if (!\count($this)) { + throw new \InvalidArgumentException('The current node list is empty.'); + } + + $node = $this->getNode(0); + $owner = $node->ownerDocument; + + if ($this->html5Parser && '' === $owner->saveXML($owner->childNodes[0])) { + $owner = $this->html5Parser; + } + + return $owner->saveHTML($node); + } + + /** + * Evaluates an XPath expression. + * + * Since an XPath expression might evaluate to either a simple type or a \DOMNodeList, + * this method will return either an array of simple types or a new Crawler instance. + */ + public function evaluate(string $xpath): array|self + { + if (null === $this->document) { + throw new \LogicException('Cannot evaluate the expression on an uninitialized crawler.'); + } + + $data = []; + $domxpath = $this->createDOMXPath($this->document, $this->findNamespacePrefixes($xpath)); + + foreach ($this->nodes as $node) { + $data[] = $domxpath->evaluate($xpath, $node); + } + + if (isset($data[0]) && $data[0] instanceof \DOMNodeList) { + return $this->createSubCrawler($data); + } + + return $data; + } + + /** + * Extracts information from the list of nodes. + * + * You can extract attributes or/and the node value (_text). + * + * Example: + * + * $crawler->filter('h1 a')->extract(['_text', 'href']); + */ + public function extract(array $attributes): array + { + $count = \count($attributes); + + $data = []; + foreach ($this->nodes as $node) { + $elements = []; + foreach ($attributes as $attribute) { + if ('_text' === $attribute) { + $elements[] = $node->nodeValue; + } elseif ('_name' === $attribute) { + $elements[] = $node->nodeName; + } else { + $elements[] = $node->getAttribute($attribute); + } + } + + $data[] = 1 === $count ? $elements[0] : $elements; + } + + return $data; + } + + /** + * Filters the list of nodes with an XPath expression. + * + * The XPath expression is evaluated in the context of the crawler, which + * is considered as a fake parent of the elements inside it. + * This means that a child selector "div" or "./div" will match only + * the div elements of the current crawler, not their children. + */ + public function filterXPath(string $xpath): static + { + $xpath = $this->relativize($xpath); + + // If we dropped all expressions in the XPath while preparing it, there would be no match + if ('' === $xpath) { + return $this->createSubCrawler(null); + } + + return $this->filterRelativeXPath($xpath); + } + + /** + * Filters the list of nodes with a CSS selector. + * + * This method only works if you have installed the CssSelector Symfony Component. + * + * @throws \LogicException if the CssSelector Component is not available + */ + public function filter(string $selector): static + { + $converter = $this->createCssSelectorConverter(); + + // The CssSelector already prefixes the selector with descendant-or-self:: + return $this->filterRelativeXPath($converter->toXPath($selector)); + } + + /** + * Selects links by name or alt value for clickable images. + */ + public function selectLink(string $value): static + { + return $this->filterRelativeXPath( + sprintf('descendant-or-self::a[contains(concat(\' \', normalize-space(string(.)), \' \'), %1$s) or ./img[contains(concat(\' \', normalize-space(string(@alt)), \' \'), %1$s)]]', static::xpathLiteral(' '.$value.' ')) + ); + } + + /** + * Selects images by alt value. + */ + public function selectImage(string $value): static + { + $xpath = sprintf('descendant-or-self::img[contains(normalize-space(string(@alt)), %s)]', static::xpathLiteral($value)); + + return $this->filterRelativeXPath($xpath); + } + + /** + * Selects a button by its text content, id, value, name or alt attribute. + */ + public function selectButton(string $value): static + { + return $this->filterRelativeXPath( + sprintf('descendant-or-self::input[((contains(%1$s, "submit") or contains(%1$s, "button")) and contains(concat(\' \', normalize-space(string(@value)), \' \'), %2$s)) or (contains(%1$s, "image") and contains(concat(\' \', normalize-space(string(@alt)), \' \'), %2$s)) or @id=%3$s or @name=%3$s] | descendant-or-self::button[contains(concat(\' \', normalize-space(string(.)), \' \'), %2$s) or contains(concat(\' \', normalize-space(string(@value)), \' \'), %2$s) or @id=%3$s or @name=%3$s]', 'translate(@type, "ABCDEFGHIJKLMNOPQRSTUVWXYZ", "abcdefghijklmnopqrstuvwxyz")', static::xpathLiteral(' '.$value.' '), static::xpathLiteral($value)) + ); + } + + /** + * Returns a Link object for the first node in the list. + * + * @throws \InvalidArgumentException If the current node list is empty or the selected node is not instance of DOMElement + */ + public function link(string $method = 'get'): Link + { + if (!$this->nodes) { + throw new \InvalidArgumentException('The current node list is empty.'); + } + + $node = $this->getNode(0); + + if (!$node instanceof \DOMElement) { + throw new \InvalidArgumentException(sprintf('The selected node should be instance of DOMElement, got "%s".', get_debug_type($node))); + } + + return new Link($node, $this->baseHref, $method); + } + + /** + * Returns an array of Link objects for the nodes in the list. + * + * @return Link[] + * + * @throws \InvalidArgumentException If the current node list contains non-DOMElement instances + */ + public function links(): array + { + $links = []; + foreach ($this->nodes as $node) { + if (!$node instanceof \DOMElement) { + throw new \InvalidArgumentException(sprintf('The current node list should contain only DOMElement instances, "%s" found.', get_debug_type($node))); + } + + $links[] = new Link($node, $this->baseHref, 'get'); + } + + return $links; + } + + /** + * Returns an Image object for the first node in the list. + * + * @throws \InvalidArgumentException If the current node list is empty + */ + public function image(): Image + { + if (!\count($this)) { + throw new \InvalidArgumentException('The current node list is empty.'); + } + + $node = $this->getNode(0); + + if (!$node instanceof \DOMElement) { + throw new \InvalidArgumentException(sprintf('The selected node should be instance of DOMElement, got "%s".', get_debug_type($node))); + } + + return new Image($node, $this->baseHref); + } + + /** + * Returns an array of Image objects for the nodes in the list. + * + * @return Image[] + */ + public function images(): array + { + $images = []; + foreach ($this as $node) { + if (!$node instanceof \DOMElement) { + throw new \InvalidArgumentException(sprintf('The current node list should contain only DOMElement instances, "%s" found.', get_debug_type($node))); + } + + $images[] = new Image($node, $this->baseHref); + } + + return $images; + } + + /** + * Returns a Form object for the first node in the list. + * + * @throws \InvalidArgumentException If the current node list is empty or the selected node is not instance of DOMElement + */ + public function form(?array $values = null, ?string $method = null): Form + { + if (!$this->nodes) { + throw new \InvalidArgumentException('The current node list is empty.'); + } + + $node = $this->getNode(0); + + if (!$node instanceof \DOMElement) { + throw new \InvalidArgumentException(sprintf('The selected node should be instance of DOMElement, got "%s".', get_debug_type($node))); + } + + $form = new Form($node, $this->uri, $method, $this->baseHref); + + if (null !== $values) { + $form->setValues($values); + } + + return $form; + } + + /** + * Overloads a default namespace prefix to be used with XPath and CSS expressions. + * + * @return void + */ + public function setDefaultNamespacePrefix(string $prefix) + { + $this->defaultNamespacePrefix = $prefix; + } + + /** + * @return void + */ + public function registerNamespace(string $prefix, string $namespace) + { + $this->namespaces[$prefix] = $namespace; + } + + /** + * Converts string for XPath expressions. + * + * Escaped characters are: quotes (") and apostrophe ('). + * + * Examples: + * + * echo Crawler::xpathLiteral('foo " bar'); + * //prints 'foo " bar' + * + * echo Crawler::xpathLiteral("foo ' bar"); + * //prints "foo ' bar" + * + * echo Crawler::xpathLiteral('a\'b"c'); + * //prints concat('a', "'", 'b"c') + */ + public static function xpathLiteral(string $s): string + { + if (!str_contains($s, "'")) { + return sprintf("'%s'", $s); + } + + if (!str_contains($s, '"')) { + return sprintf('"%s"', $s); + } + + $string = $s; + $parts = []; + while (true) { + if (false !== $pos = strpos($string, "'")) { + $parts[] = sprintf("'%s'", substr($string, 0, $pos)); + $parts[] = "\"'\""; + $string = substr($string, $pos + 1); + } else { + $parts[] = "'$string'"; + break; + } + } + + return sprintf('concat(%s)', implode(', ', $parts)); + } + + /** + * Filters the list of nodes with an XPath expression. + * + * The XPath expression should already be processed to apply it in the context of each node. + */ + private function filterRelativeXPath(string $xpath): static + { + $crawler = $this->createSubCrawler(null); + if (null === $this->document) { + return $crawler; + } + + $domxpath = $this->createDOMXPath($this->document, $this->findNamespacePrefixes($xpath)); + + foreach ($this->nodes as $node) { + $crawler->add($domxpath->query($xpath, $node)); + } + + return $crawler; + } + + /** + * Make the XPath relative to the current context. + * + * The returned XPath will match elements matching the XPath inside the current crawler + * when running in the context of a node of the crawler. + */ + private function relativize(string $xpath): string + { + $expressions = []; + + // An expression which will never match to replace expressions which cannot match in the crawler + // We cannot drop + $nonMatchingExpression = 'a[name() = "b"]'; + + $xpathLen = \strlen($xpath); + $openedBrackets = 0; + $startPosition = strspn($xpath, " \t\n\r\0\x0B"); + + for ($i = $startPosition; $i <= $xpathLen; ++$i) { + $i += strcspn($xpath, '"\'[]|', $i); + + if ($i < $xpathLen) { + switch ($xpath[$i]) { + case '"': + case "'": + if (false === $i = strpos($xpath, $xpath[$i], $i + 1)) { + return $xpath; // The XPath expression is invalid + } + continue 2; + case '[': + ++$openedBrackets; + continue 2; + case ']': + --$openedBrackets; + continue 2; + } + } + if ($openedBrackets) { + continue; + } + + if ($startPosition < $xpathLen && '(' === $xpath[$startPosition]) { + // If the union is inside some braces, we need to preserve the opening braces and apply + // the change only inside it. + $j = 1 + strspn($xpath, "( \t\n\r\0\x0B", $startPosition + 1); + $parenthesis = substr($xpath, $startPosition, $j); + $startPosition += $j; + } else { + $parenthesis = ''; + } + $expression = rtrim(substr($xpath, $startPosition, $i - $startPosition)); + + if (str_starts_with($expression, 'self::*/')) { + $expression = './'.substr($expression, 8); + } + + // add prefix before absolute element selector + if ('' === $expression) { + $expression = $nonMatchingExpression; + } elseif (str_starts_with($expression, '//')) { + $expression = 'descendant-or-self::'.substr($expression, 2); + } elseif (str_starts_with($expression, './/')) { + $expression = 'descendant-or-self::'.substr($expression, 3); + } elseif (str_starts_with($expression, './')) { + $expression = 'self::'.substr($expression, 2); + } elseif (str_starts_with($expression, 'child::')) { + $expression = 'self::'.substr($expression, 7); + } elseif ('/' === $expression[0] || '.' === $expression[0] || str_starts_with($expression, 'self::')) { + $expression = $nonMatchingExpression; + } elseif (str_starts_with($expression, 'descendant::')) { + $expression = 'descendant-or-self::'.substr($expression, 12); + } elseif (preg_match('/^(ancestor|ancestor-or-self|attribute|following|following-sibling|namespace|parent|preceding|preceding-sibling)::/', $expression)) { + // the fake root has no parent, preceding or following nodes and also no attributes (even no namespace attributes) + $expression = $nonMatchingExpression; + } elseif (!str_starts_with($expression, 'descendant-or-self::')) { + $expression = 'self::'.$expression; + } + $expressions[] = $parenthesis.$expression; + + if ($i === $xpathLen) { + return implode(' | ', $expressions); + } + + $i += strspn($xpath, " \t\n\r\0\x0B", $i + 1); + $startPosition = $i + 1; + } + + return $xpath; // The XPath expression is invalid + } + + public function getNode(int $position): ?\DOMNode + { + return $this->nodes[$position] ?? null; + } + + public function count(): int + { + return \count($this->nodes); + } + + /** + * @return \ArrayIterator + */ + public function getIterator(): \ArrayIterator + { + return new \ArrayIterator($this->nodes); + } + + protected function sibling(\DOMNode $node, string $siblingDir = 'nextSibling'): array + { + $nodes = []; + + $currentNode = $this->getNode(0); + do { + if ($node !== $currentNode && \XML_ELEMENT_NODE === $node->nodeType) { + $nodes[] = $node; + } + } while ($node = $node->$siblingDir); + + return $nodes; + } + + private function parseHtml5(string $htmlContent, string $charset = 'UTF-8'): \DOMDocument + { + if (!$this->supportsEncoding($charset)) { + $htmlContent = $this->convertToHtmlEntities($htmlContent, $charset); + $charset = 'UTF-8'; + } + + return $this->html5Parser->parse($htmlContent, ['encoding' => $charset]); + } + + private function supportsEncoding(string $encoding): bool + { + try { + return '' === @mb_convert_encoding('', $encoding, 'UTF-8'); + } catch (\Throwable $e) { + return false; + } + } + + private function parseXhtml(string $htmlContent, string $charset = 'UTF-8'): \DOMDocument + { + if ('UTF-8' === $charset && preg_match('//u', $htmlContent)) { + $htmlContent = ''.$htmlContent; + } else { + $htmlContent = $this->convertToHtmlEntities($htmlContent, $charset); + } + + $internalErrors = libxml_use_internal_errors(true); + + $dom = new \DOMDocument('1.0', $charset); + $dom->validateOnParse = true; + + if ('' !== trim($htmlContent)) { + @$dom->loadHTML($htmlContent); + } + + libxml_use_internal_errors($internalErrors); + + return $dom; + } + + /** + * Converts charset to HTML-entities to ensure valid parsing. + */ + private function convertToHtmlEntities(string $htmlContent, string $charset = 'UTF-8'): string + { + set_error_handler(static fn () => throw new \Exception()); + + try { + return mb_encode_numericentity($htmlContent, [0x80, 0x10FFFF, 0, 0x1FFFFF], $charset); + } catch (\Exception|\ValueError) { + try { + $htmlContent = iconv($charset, 'UTF-8', $htmlContent); + $htmlContent = mb_encode_numericentity($htmlContent, [0x80, 0x10FFFF, 0, 0x1FFFFF], 'UTF-8'); + } catch (\Exception|\ValueError) { + } + + return $htmlContent; + } finally { + restore_error_handler(); + } + } + + /** + * @throws \InvalidArgumentException + */ + private function createDOMXPath(\DOMDocument $document, array $prefixes = []): \DOMXPath + { + $domxpath = new \DOMXPath($document); + + foreach ($prefixes as $prefix) { + $namespace = $this->discoverNamespace($domxpath, $prefix); + if (null !== $namespace) { + $domxpath->registerNamespace($prefix, $namespace); + } + } + + return $domxpath; + } + + /** + * @throws \InvalidArgumentException + */ + private function discoverNamespace(\DOMXPath $domxpath, string $prefix): ?string + { + if (\array_key_exists($prefix, $this->namespaces)) { + return $this->namespaces[$prefix]; + } + + if ($this->cachedNamespaces->offsetExists($prefix)) { + return $this->cachedNamespaces[$prefix]; + } + + // ask for one namespace, otherwise we'd get a collection with an item for each node + $namespaces = $domxpath->query(sprintf('(//namespace::*[name()="%s"])[last()]', $this->defaultNamespacePrefix === $prefix ? '' : $prefix)); + + return $this->cachedNamespaces[$prefix] = ($node = $namespaces->item(0)) ? $node->nodeValue : null; + } + + private function findNamespacePrefixes(string $xpath): array + { + if (preg_match_all('/(?P[a-z_][a-z_0-9\-\.]*+):[^"\/:]/i', $xpath, $matches)) { + return array_unique($matches['prefix']); + } + + return []; + } + + /** + * Creates a crawler for some subnodes. + * + * @param \DOMNodeList|\DOMNode|\DOMNode[]|string|null $nodes + */ + private function createSubCrawler(\DOMNodeList|\DOMNode|array|string|null $nodes): static + { + $crawler = new static($nodes, $this->uri, $this->baseHref); + $crawler->isHtml = $this->isHtml; + $crawler->document = $this->document; + $crawler->namespaces = $this->namespaces; + $crawler->cachedNamespaces = $this->cachedNamespaces; + $crawler->html5Parser = $this->html5Parser; + + return $crawler; + } + + /** + * @throws \LogicException If the CssSelector Component is not available + */ + private function createCssSelectorConverter(): CssSelectorConverter + { + if (!class_exists(CssSelectorConverter::class)) { + throw new \LogicException('To filter with a CSS selector, install the CssSelector component ("composer require symfony/css-selector"). Or use filterXpath instead.'); + } + + return new CssSelectorConverter($this->isHtml); + } + + /** + * Parse string into DOMDocument object using HTML5 parser if the content is HTML5 and the library is available. + * Use libxml parser otherwise. + */ + private function parseHtmlString(string $content, string $charset): \DOMDocument + { + if ($this->canParseHtml5String($content)) { + return $this->parseHtml5($content, $charset); + } + + return $this->parseXhtml($content, $charset); + } + + private function canParseHtml5String(string $content): bool + { + if (!$this->html5Parser) { + return false; + } + + if (false === ($pos = stripos($content, ''))) { + return false; + } + + $header = substr($content, 0, $pos); + + return '' === $header || $this->isValidHtml5Heading($header); + } + + private function isValidHtml5Heading(string $heading): bool + { + return 1 === preg_match('/^\x{FEFF}?\s*(\s*)*$/u', $heading); + } + + private function normalizeWhitespace(string $string): string + { + return trim(preg_replace("/(?:[ \n\r\t\x0C]{2,}+|[\n\r\t\x0C])/", ' ', $string), " \n\r\t\x0C"); + } +} diff --git a/3rdparty/symfony/dom-crawler/Field/ChoiceFormField.php b/3rdparty/symfony/dom-crawler/Field/ChoiceFormField.php new file mode 100644 index 00000000..7688b6d7 --- /dev/null +++ b/3rdparty/symfony/dom-crawler/Field/ChoiceFormField.php @@ -0,0 +1,309 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Component\DomCrawler\Field; + +/** + * ChoiceFormField represents a choice form field. + * + * It is constructed from an HTML select tag, or an HTML checkbox, or radio inputs. + * + * @author Fabien Potencier + */ +class ChoiceFormField extends FormField +{ + private string $type; + private bool $multiple; + private array $options; + private bool $validationDisabled = false; + + /** + * Returns true if the field should be included in the submitted values. + * + * @return bool true if the field should be included in the submitted values, false otherwise + */ + public function hasValue(): bool + { + // don't send a value for unchecked checkboxes + if (\in_array($this->type, ['checkbox', 'radio']) && null === $this->value) { + return false; + } + + return true; + } + + /** + * Check if the current selected option is disabled. + */ + public function isDisabled(): bool + { + if ('checkbox' === $this->type) { + return parent::isDisabled(); + } + + if (parent::isDisabled() && 'select' === $this->type) { + return true; + } + + foreach ($this->options as $option) { + if ($option['value'] == $this->value && $option['disabled']) { + return true; + } + } + + return false; + } + + /** + * Sets the value of the field. + * + * @return void + */ + public function select(string|array|bool $value) + { + $this->setValue($value); + } + + /** + * Ticks a checkbox. + * + * @return void + * + * @throws \LogicException When the type provided is not correct + */ + public function tick() + { + if ('checkbox' !== $this->type) { + throw new \LogicException(sprintf('You cannot tick "%s" as it is not a checkbox (%s).', $this->name, $this->type)); + } + + $this->setValue(true); + } + + /** + * Unticks a checkbox. + * + * @return void + * + * @throws \LogicException When the type provided is not correct + */ + public function untick() + { + if ('checkbox' !== $this->type) { + throw new \LogicException(sprintf('You cannot untick "%s" as it is not a checkbox (%s).', $this->name, $this->type)); + } + + $this->setValue(false); + } + + /** + * Sets the value of the field. + * + * @return void + * + * @throws \InvalidArgumentException When value type provided is not correct + */ + public function setValue(string|array|bool|null $value) + { + if ('checkbox' === $this->type && false === $value) { + // uncheck + $this->value = null; + } elseif ('checkbox' === $this->type && true === $value) { + // check + $this->value = $this->options[0]['value']; + } else { + if (\is_array($value)) { + if (!$this->multiple) { + throw new \InvalidArgumentException(sprintf('The value for "%s" cannot be an array.', $this->name)); + } + + foreach ($value as $v) { + if (!$this->containsOption($v, $this->options)) { + throw new \InvalidArgumentException(sprintf('Input "%s" cannot take "%s" as a value (possible values: "%s").', $this->name, $v, implode('", "', $this->availableOptionValues()))); + } + } + } elseif (!$this->containsOption($value, $this->options)) { + throw new \InvalidArgumentException(sprintf('Input "%s" cannot take "%s" as a value (possible values: "%s").', $this->name, $value, implode('", "', $this->availableOptionValues()))); + } + + if ($this->multiple) { + $value = (array) $value; + } + + if (\is_array($value)) { + $this->value = $value; + } else { + parent::setValue($value); + } + } + } + + /** + * Adds a choice to the current ones. + * + * @throws \LogicException When choice provided is not multiple nor radio + * + * @internal + */ + public function addChoice(\DOMElement $node): void + { + if (!$this->multiple && 'radio' !== $this->type) { + throw new \LogicException(sprintf('Unable to add a choice for "%s" as it is not multiple or is not a radio button.', $this->name)); + } + + $option = $this->buildOptionValue($node); + $this->options[] = $option; + + if ($node->hasAttribute('checked')) { + $this->value = $option['value']; + } + } + + /** + * Returns the type of the choice field (radio, select, or checkbox). + */ + public function getType(): string + { + return $this->type; + } + + /** + * Returns true if the field accepts multiple values. + */ + public function isMultiple(): bool + { + return $this->multiple; + } + + /** + * Initializes the form field. + * + * @return void + * + * @throws \LogicException When node type is incorrect + */ + protected function initialize() + { + if ('input' !== $this->node->nodeName && 'select' !== $this->node->nodeName) { + throw new \LogicException(sprintf('A ChoiceFormField can only be created from an input or select tag (%s given).', $this->node->nodeName)); + } + + if ('input' === $this->node->nodeName && 'checkbox' !== strtolower($this->node->getAttribute('type')) && 'radio' !== strtolower($this->node->getAttribute('type'))) { + throw new \LogicException(sprintf('A ChoiceFormField can only be created from an input tag with a type of checkbox or radio (given type is "%s").', $this->node->getAttribute('type'))); + } + + $this->value = null; + $this->options = []; + $this->multiple = false; + + if ('input' == $this->node->nodeName) { + $this->type = strtolower($this->node->getAttribute('type')); + $optionValue = $this->buildOptionValue($this->node); + $this->options[] = $optionValue; + + if ($this->node->hasAttribute('checked')) { + $this->value = $optionValue['value']; + } + } else { + $this->type = 'select'; + if ($this->node->hasAttribute('multiple')) { + $this->multiple = true; + $this->value = []; + $this->name = str_replace('[]', '', $this->name); + } + + $found = false; + foreach ($this->xpath->query('descendant::option', $this->node) as $option) { + $optionValue = $this->buildOptionValue($option); + $this->options[] = $optionValue; + + if ($option->hasAttribute('selected')) { + $found = true; + if ($this->multiple) { + $this->value[] = $optionValue['value']; + } else { + $this->value = $optionValue['value']; + } + } + } + + // if no option is selected and if it is a simple select box, take the first option as the value + if (!$found && !$this->multiple && $this->options) { + $this->value = $this->options[0]['value']; + } + } + } + + /** + * Returns option value with associated disabled flag. + */ + private function buildOptionValue(\DOMElement $node): array + { + $option = []; + + $defaultDefaultValue = 'select' === $this->node->nodeName ? '' : 'on'; + $defaultValue = (isset($node->nodeValue) && !empty($node->nodeValue)) ? $node->nodeValue : $defaultDefaultValue; + $option['value'] = $node->hasAttribute('value') ? $node->getAttribute('value') : $defaultValue; + $option['disabled'] = $node->hasAttribute('disabled'); + + return $option; + } + + /** + * Checks whether given value is in the existing options. + * + * @internal + */ + public function containsOption(string $optionValue, array $options): bool + { + if ($this->validationDisabled) { + return true; + } + + foreach ($options as $option) { + if ($option['value'] == $optionValue) { + return true; + } + } + + return false; + } + + /** + * Returns list of available field options. + * + * @internal + */ + public function availableOptionValues(): array + { + $values = []; + + foreach ($this->options as $option) { + $values[] = $option['value']; + } + + return $values; + } + + /** + * Disables the internal validation of the field. + * + * @internal + * + * @return $this + */ + public function disableValidation(): static + { + $this->validationDisabled = true; + + return $this; + } +} diff --git a/3rdparty/symfony/dom-crawler/Field/FileFormField.php b/3rdparty/symfony/dom-crawler/Field/FileFormField.php new file mode 100644 index 00000000..4ebe766f --- /dev/null +++ b/3rdparty/symfony/dom-crawler/Field/FileFormField.php @@ -0,0 +1,112 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Component\DomCrawler\Field; + +/** + * FileFormField represents a file form field (an HTML file input tag). + * + * @author Fabien Potencier + */ +class FileFormField extends FormField +{ + /** + * Sets the PHP error code associated with the field. + * + * @param int $error The error code (one of UPLOAD_ERR_INI_SIZE, UPLOAD_ERR_FORM_SIZE, UPLOAD_ERR_PARTIAL, UPLOAD_ERR_NO_FILE, UPLOAD_ERR_NO_TMP_DIR, UPLOAD_ERR_CANT_WRITE, or UPLOAD_ERR_EXTENSION) + * + * @return void + * + * @throws \InvalidArgumentException When error code doesn't exist + */ + public function setErrorCode(int $error) + { + $codes = [\UPLOAD_ERR_INI_SIZE, \UPLOAD_ERR_FORM_SIZE, \UPLOAD_ERR_PARTIAL, \UPLOAD_ERR_NO_FILE, \UPLOAD_ERR_NO_TMP_DIR, \UPLOAD_ERR_CANT_WRITE, \UPLOAD_ERR_EXTENSION]; + if (!\in_array($error, $codes)) { + throw new \InvalidArgumentException(sprintf('The error code "%s" is not valid.', $error)); + } + + $this->value = ['name' => '', 'type' => '', 'tmp_name' => '', 'error' => $error, 'size' => 0]; + } + + /** + * Sets the value of the field. + * + * @return void + */ + public function upload(?string $value) + { + $this->setValue($value); + } + + /** + * Sets the value of the field. + * + * @return void + */ + public function setValue(?string $value) + { + if (null !== $value && is_readable($value)) { + $error = \UPLOAD_ERR_OK; + $size = filesize($value); + $info = pathinfo($value); + $name = $info['basename']; + + // copy to a tmp location + $tmp = sys_get_temp_dir().'/'.strtr(substr(base64_encode(hash('sha256', uniqid(mt_rand(), true), true)), 0, 7), '/', '_'); + if (\array_key_exists('extension', $info)) { + $tmp .= '.'.$info['extension']; + } + if (is_file($tmp)) { + unlink($tmp); + } + copy($value, $tmp); + $value = $tmp; + } else { + $error = \UPLOAD_ERR_NO_FILE; + $size = 0; + $name = ''; + $value = ''; + } + + $this->value = ['name' => $name, 'type' => '', 'tmp_name' => $value, 'error' => $error, 'size' => $size]; + } + + /** + * Sets path to the file as string for simulating HTTP request. + * + * @return void + */ + public function setFilePath(string $path) + { + parent::setValue($path); + } + + /** + * Initializes the form field. + * + * @return void + * + * @throws \LogicException When node type is incorrect + */ + protected function initialize() + { + if ('input' !== $this->node->nodeName) { + throw new \LogicException(sprintf('A FileFormField can only be created from an input tag (%s given).', $this->node->nodeName)); + } + + if ('file' !== strtolower($this->node->getAttribute('type'))) { + throw new \LogicException(sprintf('A FileFormField can only be created from an input tag with a type of file (given type is "%s").', $this->node->getAttribute('type'))); + } + + $this->setValue(null); + } +} diff --git a/3rdparty/symfony/dom-crawler/Field/FormField.php b/3rdparty/symfony/dom-crawler/Field/FormField.php new file mode 100644 index 00000000..b97d54dd --- /dev/null +++ b/3rdparty/symfony/dom-crawler/Field/FormField.php @@ -0,0 +1,125 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Component\DomCrawler\Field; + +/** + * FormField is the abstract class for all form fields. + * + * @author Fabien Potencier + */ +abstract class FormField +{ + /** + * @var \DOMElement + */ + protected $node; + /** + * @var string + */ + protected $name; + /** + * @var string + */ + protected $value; + /** + * @var \DOMDocument + */ + protected $document; + /** + * @var \DOMXPath + */ + protected $xpath; + /** + * @var bool + */ + protected $disabled; + + /** + * @param \DOMElement $node The node associated with this field + */ + public function __construct(\DOMElement $node) + { + $this->node = $node; + $this->name = $node->getAttribute('name'); + $this->xpath = new \DOMXPath($node->ownerDocument); + + $this->initialize(); + } + + /** + * Returns the label tag associated to the field or null if none. + */ + public function getLabel(): ?\DOMElement + { + $xpath = new \DOMXPath($this->node->ownerDocument); + + if ($this->node->hasAttribute('id')) { + $labels = $xpath->query(sprintf('descendant::label[@for="%s"]', $this->node->getAttribute('id'))); + if ($labels->length > 0) { + return $labels->item(0); + } + } + + $labels = $xpath->query('ancestor::label[1]', $this->node); + + return $labels->length > 0 ? $labels->item(0) : null; + } + + /** + * Returns the name of the field. + */ + public function getName(): string + { + return $this->name; + } + + /** + * Gets the value of the field. + */ + public function getValue(): string|array|null + { + return $this->value; + } + + /** + * Sets the value of the field. + * + * @return void + */ + public function setValue(?string $value) + { + $this->value = $value ?? ''; + } + + /** + * Returns true if the field should be included in the submitted values. + */ + public function hasValue(): bool + { + return true; + } + + /** + * Check if the current field is disabled. + */ + public function isDisabled(): bool + { + return $this->node->hasAttribute('disabled'); + } + + /** + * Initializes the form field. + * + * @return void + */ + abstract protected function initialize(); +} diff --git a/3rdparty/symfony/dom-crawler/Field/InputFormField.php b/3rdparty/symfony/dom-crawler/Field/InputFormField.php new file mode 100644 index 00000000..19d77352 --- /dev/null +++ b/3rdparty/symfony/dom-crawler/Field/InputFormField.php @@ -0,0 +1,48 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Component\DomCrawler\Field; + +/** + * InputFormField represents an input form field (an HTML input tag). + * + * For inputs with type of file, checkbox, or radio, there are other more + * specialized classes (cf. FileFormField and ChoiceFormField). + * + * @author Fabien Potencier + */ +class InputFormField extends FormField +{ + /** + * Initializes the form field. + * + * @return void + * + * @throws \LogicException When node type is incorrect + */ + protected function initialize() + { + if ('input' !== $this->node->nodeName && 'button' !== $this->node->nodeName) { + throw new \LogicException(sprintf('An InputFormField can only be created from an input or button tag (%s given).', $this->node->nodeName)); + } + + $type = strtolower($this->node->getAttribute('type')); + if ('checkbox' === $type) { + throw new \LogicException('Checkboxes should be instances of ChoiceFormField.'); + } + + if ('file' === $type) { + throw new \LogicException('File inputs should be instances of FileFormField.'); + } + + $this->value = $this->node->getAttribute('value'); + } +} diff --git a/3rdparty/symfony/dom-crawler/Field/TextareaFormField.php b/3rdparty/symfony/dom-crawler/Field/TextareaFormField.php new file mode 100644 index 00000000..5168c522 --- /dev/null +++ b/3rdparty/symfony/dom-crawler/Field/TextareaFormField.php @@ -0,0 +1,39 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Component\DomCrawler\Field; + +/** + * TextareaFormField represents a textarea form field (an HTML textarea tag). + * + * @author Fabien Potencier + */ +class TextareaFormField extends FormField +{ + /** + * Initializes the form field. + * + * @return void + * + * @throws \LogicException When node type is incorrect + */ + protected function initialize() + { + if ('textarea' !== $this->node->nodeName) { + throw new \LogicException(sprintf('A TextareaFormField can only be created from a textarea tag (%s given).', $this->node->nodeName)); + } + + $this->value = ''; + foreach ($this->node->childNodes as $node) { + $this->value .= $node->wholeText; + } + } +} diff --git a/3rdparty/symfony/dom-crawler/Form.php b/3rdparty/symfony/dom-crawler/Form.php new file mode 100644 index 00000000..9a7c19c1 --- /dev/null +++ b/3rdparty/symfony/dom-crawler/Form.php @@ -0,0 +1,469 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Component\DomCrawler; + +use Symfony\Component\DomCrawler\Field\ChoiceFormField; +use Symfony\Component\DomCrawler\Field\FormField; + +/** + * Form represents an HTML form. + * + * @author Fabien Potencier + */ +class Form extends Link implements \ArrayAccess +{ + private \DOMElement $button; + private FormFieldRegistry $fields; + private ?string $baseHref; + + /** + * @param \DOMElement $node A \DOMElement instance + * @param string|null $currentUri The URI of the page where the form is embedded + * @param string|null $method The method to use for the link (if null, it defaults to the method defined by the form) + * @param string|null $baseHref The URI of the used for relative links, but not for empty action + * + * @throws \LogicException if the node is not a button inside a form tag + */ + public function __construct(\DOMElement $node, ?string $currentUri = null, ?string $method = null, ?string $baseHref = null) + { + parent::__construct($node, $currentUri, $method); + $this->baseHref = $baseHref; + + $this->initialize(); + } + + /** + * Gets the form node associated with this form. + */ + public function getFormNode(): \DOMElement + { + return $this->node; + } + + /** + * Sets the value of the fields. + * + * @param array $values An array of field values + * + * @return $this + */ + public function setValues(array $values): static + { + foreach ($values as $name => $value) { + $this->fields->set($name, $value); + } + + return $this; + } + + /** + * Gets the field values. + * + * The returned array does not include file fields (@see getFiles). + */ + public function getValues(): array + { + $values = []; + foreach ($this->fields->all() as $name => $field) { + if ($field->isDisabled()) { + continue; + } + + if (!$field instanceof Field\FileFormField && $field->hasValue()) { + $values[$name] = $field->getValue(); + } + } + + return $values; + } + + /** + * Gets the file field values. + */ + public function getFiles(): array + { + if (!\in_array($this->getMethod(), ['POST', 'PUT', 'DELETE', 'PATCH'])) { + return []; + } + + $files = []; + + foreach ($this->fields->all() as $name => $field) { + if ($field->isDisabled()) { + continue; + } + + if ($field instanceof Field\FileFormField) { + $files[$name] = $field->getValue(); + } + } + + return $files; + } + + /** + * Gets the field values as PHP. + * + * This method converts fields with the array notation + * (like foo[bar] to arrays) like PHP does. + */ + public function getPhpValues(): array + { + $values = []; + foreach ($this->getValues() as $name => $value) { + $qs = http_build_query([$name => $value], '', '&'); + if (!empty($qs)) { + parse_str($qs, $expandedValue); + $varName = substr($name, 0, \strlen(key($expandedValue))); + $values[] = [$varName => current($expandedValue)]; + } + } + + return array_replace_recursive([], ...$values); + } + + /** + * Gets the file field values as PHP. + * + * This method converts fields with the array notation + * (like foo[bar] to arrays) like PHP does. + * The returned array is consistent with the array for field values + * (@see getPhpValues), rather than uploaded files found in $_FILES. + * For a compound file field foo[bar] it will create foo[bar][name], + * instead of foo[name][bar] which would be found in $_FILES. + */ + public function getPhpFiles(): array + { + $values = []; + foreach ($this->getFiles() as $name => $value) { + $qs = http_build_query([$name => $value], '', '&'); + if (!empty($qs)) { + parse_str($qs, $expandedValue); + $varName = substr($name, 0, \strlen(key($expandedValue))); + + array_walk_recursive( + $expandedValue, + function (&$value, $key) { + if (ctype_digit($value) && ('size' === $key || 'error' === $key)) { + $value = (int) $value; + } + } + ); + + reset($expandedValue); + + $values[] = [$varName => current($expandedValue)]; + } + } + + return array_replace_recursive([], ...$values); + } + + /** + * Gets the URI of the form. + * + * The returned URI is not the same as the form "action" attribute. + * This method merges the value if the method is GET to mimics + * browser behavior. + */ + public function getUri(): string + { + $uri = parent::getUri(); + + if (!\in_array($this->getMethod(), ['POST', 'PUT', 'DELETE', 'PATCH'])) { + $currentParameters = []; + if ($query = parse_url($uri, \PHP_URL_QUERY)) { + parse_str($query, $currentParameters); + } + + $queryString = http_build_query(array_merge($currentParameters, $this->getValues()), '', '&'); + + $pos = strpos($uri, '?'); + $base = false === $pos ? $uri : substr($uri, 0, $pos); + $uri = rtrim($base.'?'.$queryString, '?'); + } + + return $uri; + } + + protected function getRawUri(): string + { + // If the form was created from a button rather than the form node, check for HTML5 action overrides + if ($this->button !== $this->node && $this->button->getAttribute('formaction')) { + return $this->button->getAttribute('formaction'); + } + + return $this->node->getAttribute('action'); + } + + /** + * Gets the form method. + * + * If no method is defined in the form, GET is returned. + */ + public function getMethod(): string + { + if (null !== $this->method) { + return $this->method; + } + + // If the form was created from a button rather than the form node, check for HTML5 method override + if ($this->button !== $this->node && $this->button->getAttribute('formmethod')) { + return strtoupper($this->button->getAttribute('formmethod')); + } + + return $this->node->getAttribute('method') ? strtoupper($this->node->getAttribute('method')) : 'GET'; + } + + /** + * Gets the form name. + * + * If no name is defined on the form, an empty string is returned. + */ + public function getName(): string + { + return $this->node->getAttribute('name'); + } + + /** + * Returns true if the named field exists. + */ + public function has(string $name): bool + { + return $this->fields->has($name); + } + + /** + * Removes a field from the form. + * + * @return void + */ + public function remove(string $name) + { + $this->fields->remove($name); + } + + /** + * Gets a named field. + * + * @return FormField|FormField[]|FormField[][] + * + * @throws \InvalidArgumentException When field is not present in this form + */ + public function get(string $name): FormField|array + { + return $this->fields->get($name); + } + + /** + * Sets a named field. + * + * @return void + */ + public function set(FormField $field) + { + $this->fields->add($field); + } + + /** + * Gets all fields. + * + * @return FormField[] + */ + public function all(): array + { + return $this->fields->all(); + } + + /** + * Returns true if the named field exists. + * + * @param string $name The field name + */ + public function offsetExists(mixed $name): bool + { + return $this->has($name); + } + + /** + * Gets the value of a field. + * + * @param string $name The field name + * + * @return FormField|FormField[]|FormField[][] + * + * @throws \InvalidArgumentException if the field does not exist + */ + public function offsetGet(mixed $name): FormField|array + { + return $this->fields->get($name); + } + + /** + * Sets the value of a field. + * + * @param string $name The field name + * @param string|array $value The value of the field + * + * @throws \InvalidArgumentException if the field does not exist + */ + public function offsetSet(mixed $name, mixed $value): void + { + $this->fields->set($name, $value); + } + + /** + * Removes a field from the form. + * + * @param string $name The field name + */ + public function offsetUnset(mixed $name): void + { + $this->fields->remove($name); + } + + /** + * Disables validation. + * + * @return $this + */ + public function disableValidation(): static + { + foreach ($this->fields->all() as $field) { + if ($field instanceof Field\ChoiceFormField) { + $field->disableValidation(); + } + } + + return $this; + } + + /** + * Sets the node for the form. + * + * Expects a 'submit' button \DOMElement and finds the corresponding form element, or the form element itself. + * + * @return void + * + * @throws \LogicException If given node is not a button or input or does not have a form ancestor + */ + protected function setNode(\DOMElement $node) + { + $this->button = $node; + if ('button' === $node->nodeName || ('input' === $node->nodeName && \in_array(strtolower($node->getAttribute('type')), ['submit', 'button', 'image']))) { + if ($node->hasAttribute('form')) { + // if the node has the HTML5-compliant 'form' attribute, use it + $formId = $node->getAttribute('form'); + $form = $node->ownerDocument->getElementById($formId); + if (null === $form) { + throw new \LogicException(sprintf('The selected node has an invalid form attribute (%s).', $formId)); + } + $this->node = $form; + + return; + } + // we loop until we find a form ancestor + do { + if (null === $node = $node->parentNode) { + throw new \LogicException('The selected node does not have a form ancestor.'); + } + } while ('form' !== $node->nodeName); + } elseif ('form' !== $node->nodeName) { + throw new \LogicException(sprintf('Unable to submit on a "%s" tag.', $node->nodeName)); + } + + $this->node = $node; + } + + /** + * Adds form elements related to this form. + * + * Creates an internal copy of the submitted 'button' element and + * the form node or the entire document depending on whether we need + * to find non-descendant elements through HTML5 'form' attribute. + */ + private function initialize(): void + { + $this->fields = new FormFieldRegistry(); + + $xpath = new \DOMXPath($this->node->ownerDocument); + + // add submitted button if it has a valid name + if ('form' !== $this->button->nodeName && $this->button->hasAttribute('name') && $this->button->getAttribute('name')) { + if ('input' == $this->button->nodeName && 'image' == strtolower($this->button->getAttribute('type'))) { + $name = $this->button->getAttribute('name'); + $this->button->setAttribute('value', '0'); + + // temporarily change the name of the input node for the x coordinate + $this->button->setAttribute('name', $name.'.x'); + $this->set(new Field\InputFormField($this->button)); + + // temporarily change the name of the input node for the y coordinate + $this->button->setAttribute('name', $name.'.y'); + $this->set(new Field\InputFormField($this->button)); + + // restore the original name of the input node + $this->button->setAttribute('name', $name); + } else { + $this->set(new Field\InputFormField($this->button)); + } + } + + // find form elements corresponding to the current form + if ($this->node->hasAttribute('id')) { + // corresponding elements are either descendants or have a matching HTML5 form attribute + $formId = Crawler::xpathLiteral($this->node->getAttribute('id')); + + $fieldNodes = $xpath->query(sprintf('( descendant::input[@form=%s] | descendant::button[@form=%1$s] | descendant::textarea[@form=%1$s] | descendant::select[@form=%1$s] | //form[@id=%1$s]//input[not(@form)] | //form[@id=%1$s]//button[not(@form)] | //form[@id=%1$s]//textarea[not(@form)] | //form[@id=%1$s]//select[not(@form)] )[( not(ancestor::template) or ancestor::turbo-stream )]', $formId)); + foreach ($fieldNodes as $node) { + $this->addField($node); + } + } else { + // do the xpath query with $this->node as the context node, to only find descendant elements + // however, descendant elements with form attribute are not part of this form + $fieldNodes = $xpath->query('( descendant::input[not(@form)] | descendant::button[not(@form)] | descendant::textarea[not(@form)] | descendant::select[not(@form)] )[( not(ancestor::template) or ancestor::turbo-stream )]', $this->node); + foreach ($fieldNodes as $node) { + $this->addField($node); + } + } + + if ($this->baseHref && '' !== $this->node->getAttribute('action')) { + $this->currentUri = $this->baseHref; + } + } + + private function addField(\DOMElement $node): void + { + if (!$node->hasAttribute('name') || !$node->getAttribute('name')) { + return; + } + + $nodeName = $node->nodeName; + if ('select' == $nodeName || 'input' == $nodeName && 'checkbox' == strtolower($node->getAttribute('type'))) { + $this->set(new Field\ChoiceFormField($node)); + } elseif ('input' == $nodeName && 'radio' == strtolower($node->getAttribute('type'))) { + // there may be other fields with the same name that are no choice + // fields already registered (see https://github.com/symfony/symfony/issues/11689) + if ($this->has($node->getAttribute('name')) && $this->get($node->getAttribute('name')) instanceof ChoiceFormField) { + $this->get($node->getAttribute('name'))->addChoice($node); + } else { + $this->set(new Field\ChoiceFormField($node)); + } + } elseif ('input' == $nodeName && 'file' == strtolower($node->getAttribute('type'))) { + $this->set(new Field\FileFormField($node)); + } elseif ('input' == $nodeName && !\in_array(strtolower($node->getAttribute('type')), ['submit', 'button', 'image'])) { + $this->set(new Field\InputFormField($node)); + } elseif ('textarea' == $nodeName) { + $this->set(new Field\TextareaFormField($node)); + } + } +} diff --git a/3rdparty/symfony/dom-crawler/FormFieldRegistry.php b/3rdparty/symfony/dom-crawler/FormFieldRegistry.php new file mode 100644 index 00000000..9e157165 --- /dev/null +++ b/3rdparty/symfony/dom-crawler/FormFieldRegistry.php @@ -0,0 +1,175 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Component\DomCrawler; + +use Symfony\Component\DomCrawler\Field\FormField; + +/** + * This is an internal class that must not be used directly. + * + * @internal + */ +class FormFieldRegistry +{ + private array $fields = []; + private string $base = ''; + + /** + * Adds a field to the registry. + */ + public function add(FormField $field): void + { + $segments = $this->getSegments($field->getName()); + + $target = &$this->fields; + while ($segments) { + if (!\is_array($target)) { + $target = []; + } + $path = array_shift($segments); + if ('' === $path) { + $target = &$target[]; + } else { + $target = &$target[$path]; + } + } + $target = $field; + } + + /** + * Removes a field based on the fully qualified name and its children from the registry. + */ + public function remove(string $name): void + { + $segments = $this->getSegments($name); + $target = &$this->fields; + while (\count($segments) > 1) { + $path = array_shift($segments); + if (!\is_array($target) || !\array_key_exists($path, $target)) { + return; + } + $target = &$target[$path]; + } + unset($target[array_shift($segments)]); + } + + /** + * Returns the value of the field based on the fully qualified name and its children. + * + * @return FormField|FormField[]|FormField[][] + * + * @throws \InvalidArgumentException if the field does not exist + */ + public function &get(string $name): FormField|array + { + $segments = $this->getSegments($name); + $target = &$this->fields; + while ($segments) { + $path = array_shift($segments); + if (!\is_array($target) || !\array_key_exists($path, $target)) { + throw new \InvalidArgumentException(sprintf('Unreachable field "%s".', $path)); + } + $target = &$target[$path]; + } + + return $target; + } + + /** + * Tests whether the form has the given field based on the fully qualified name. + */ + public function has(string $name): bool + { + try { + $this->get($name); + + return true; + } catch (\InvalidArgumentException) { + return false; + } + } + + /** + * Set the value of a field based on the fully qualified name and its children. + * + * @throws \InvalidArgumentException if the field does not exist + */ + public function set(string $name, mixed $value): void + { + $target = &$this->get($name); + if ((!\is_array($value) && $target instanceof Field\FormField) || $target instanceof Field\ChoiceFormField) { + $target->setValue($value); + } elseif (\is_array($value)) { + $registry = new static(); + $registry->base = $name; + $registry->fields = $value; + foreach ($registry->all() as $k => $v) { + $this->set($k, $v); + } + } else { + throw new \InvalidArgumentException(sprintf('Cannot set value on a compound field "%s".', $name)); + } + } + + /** + * Returns the list of field with their value. + * + * @return FormField[] The list of fields as [string] Fully qualified name => (mixed) value) + */ + public function all(): array + { + return $this->walk($this->fields, $this->base); + } + + /** + * Transforms a PHP array in a list of fully qualified name / value. + */ + private function walk(array $array, ?string $base = '', array &$output = []): array + { + foreach ($array as $k => $v) { + $path = empty($base) ? $k : sprintf('%s[%s]', $base, $k); + if (\is_array($v)) { + $this->walk($v, $path, $output); + } else { + $output[$path] = $v; + } + } + + return $output; + } + + /** + * Splits a field name into segments as a web browser would do. + * + * getSegments('base[foo][3][]') = ['base', 'foo, '3', '']; + * + * @return string[] + */ + private function getSegments(string $name): array + { + if (preg_match('/^(?P[^[]+)(?P(\[.*)|$)/', $name, $m)) { + $segments = [$m['base']]; + while (!empty($m['extra'])) { + $extra = $m['extra']; + if (preg_match('/^\[(?P.*?)\](?P.*)$/', $extra, $m)) { + $segments[] = $m['segment']; + } else { + $segments[] = $extra; + } + } + + return $segments; + } + + return [$name]; + } +} diff --git a/3rdparty/symfony/dom-crawler/Image.php b/3rdparty/symfony/dom-crawler/Image.php new file mode 100644 index 00000000..34c8fda6 --- /dev/null +++ b/3rdparty/symfony/dom-crawler/Image.php @@ -0,0 +1,40 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Component\DomCrawler; + +/** + * Image represents an HTML image (an HTML img tag). + */ +class Image extends AbstractUriElement +{ + public function __construct(\DOMElement $node, ?string $currentUri = null) + { + parent::__construct($node, $currentUri, 'GET'); + } + + protected function getRawUri(): string + { + return $this->node->getAttribute('src'); + } + + /** + * @return void + */ + protected function setNode(\DOMElement $node) + { + if ('img' !== $node->nodeName) { + throw new \LogicException(sprintf('Unable to visualize a "%s" tag.', $node->nodeName)); + } + + $this->node = $node; + } +} diff --git a/3rdparty/symfony/dom-crawler/LICENSE b/3rdparty/symfony/dom-crawler/LICENSE new file mode 100644 index 00000000..0138f8f0 --- /dev/null +++ b/3rdparty/symfony/dom-crawler/LICENSE @@ -0,0 +1,19 @@ +Copyright (c) 2004-present Fabien Potencier + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is furnished +to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +THE SOFTWARE. diff --git a/3rdparty/symfony/dom-crawler/Link.php b/3rdparty/symfony/dom-crawler/Link.php new file mode 100644 index 00000000..681a2f7a --- /dev/null +++ b/3rdparty/symfony/dom-crawler/Link.php @@ -0,0 +1,37 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Component\DomCrawler; + +/** + * Link represents an HTML link (an HTML a, area or link tag). + * + * @author Fabien Potencier + */ +class Link extends AbstractUriElement +{ + protected function getRawUri(): string + { + return $this->node->getAttribute('href'); + } + + /** + * @return void + */ + protected function setNode(\DOMElement $node) + { + if ('a' !== $node->nodeName && 'area' !== $node->nodeName && 'link' !== $node->nodeName) { + throw new \LogicException(sprintf('Unable to navigate from a "%s" tag.', $node->nodeName)); + } + + $this->node = $node; + } +} diff --git a/3rdparty/symfony/dom-crawler/UriResolver.php b/3rdparty/symfony/dom-crawler/UriResolver.php new file mode 100644 index 00000000..398cb7bc --- /dev/null +++ b/3rdparty/symfony/dom-crawler/UriResolver.php @@ -0,0 +1,136 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Component\DomCrawler; + +/** + * The UriResolver class takes an URI (relative, absolute, fragment, etc.) + * and turns it into an absolute URI against another given base URI. + * + * @author Fabien Potencier + * @author Grégoire Pineau + */ +class UriResolver +{ + /** + * Resolves a URI according to a base URI. + * + * For example if $uri=/foo/bar and $baseUri=https://symfony.com it will + * return https://symfony.com/foo/bar + * + * If the $uri is not absolute you must pass an absolute $baseUri + */ + public static function resolve(string $uri, ?string $baseUri): string + { + $uri = trim($uri); + + // absolute URL? + if (null !== parse_url(\strlen($uri) !== strcspn($uri, '?#') ? $uri : $uri.'#', \PHP_URL_SCHEME)) { + return $uri; + } + + if (null === $baseUri) { + throw new \InvalidArgumentException('The URI is relative, so you must define its base URI passing an absolute URL.'); + } + + // empty URI + if (!$uri) { + return $baseUri; + } + + // an anchor + if ('#' === $uri[0]) { + return self::cleanupAnchor($baseUri).$uri; + } + + $baseUriCleaned = self::cleanupUri($baseUri); + + if ('?' === $uri[0]) { + return $baseUriCleaned.$uri; + } + + // absolute URL with relative schema + if (str_starts_with($uri, '//')) { + return preg_replace('#^([^/]*)//.*$#', '$1', $baseUriCleaned).$uri; + } + + $baseUriCleaned = preg_replace('#^(.*?//[^/]*)(?:\/.*)?$#', '$1', $baseUriCleaned); + + // absolute path + if ('/' === $uri[0]) { + return $baseUriCleaned.$uri; + } + + // relative path + $path = parse_url(substr($baseUri, \strlen($baseUriCleaned)), \PHP_URL_PATH) ?? ''; + $path = self::canonicalizePath(substr($path, 0, strrpos($path, '/')).'/'.$uri); + + return $baseUriCleaned.('' === $path || '/' !== $path[0] ? '/' : '').$path; + } + + /** + * Returns the canonicalized URI path (see RFC 3986, section 5.2.4). + */ + private static function canonicalizePath(string $path): string + { + if ('' === $path || '/' === $path) { + return $path; + } + + if (str_ends_with($path, '.')) { + $path .= '/'; + } + + $output = []; + + foreach (explode('/', $path) as $segment) { + if ('..' === $segment) { + array_pop($output); + } elseif ('.' !== $segment) { + $output[] = $segment; + } + } + + return implode('/', $output); + } + + /** + * Removes the query string and the anchor from the given uri. + */ + private static function cleanupUri(string $uri): string + { + return self::cleanupQuery(self::cleanupAnchor($uri)); + } + + /** + * Removes the query string from the uri. + */ + private static function cleanupQuery(string $uri): string + { + if (false !== $pos = strpos($uri, '?')) { + return substr($uri, 0, $pos); + } + + return $uri; + } + + /** + * Removes the anchor from the uri. + */ + private static function cleanupAnchor(string $uri): string + { + if (false !== $pos = strpos($uri, '#')) { + return substr($uri, 0, $pos); + } + + return $uri; + } +} diff --git a/3rdparty/symfony/event-dispatcher-contracts/Event.php b/3rdparty/symfony/event-dispatcher-contracts/Event.php new file mode 100644 index 00000000..2e7f9989 --- /dev/null +++ b/3rdparty/symfony/event-dispatcher-contracts/Event.php @@ -0,0 +1,51 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Contracts\EventDispatcher; + +use Psr\EventDispatcher\StoppableEventInterface; + +/** + * Event is the base class for classes containing event data. + * + * This class contains no event data. It is used by events that do not pass + * state information to an event handler when an event is raised. + * + * You can call the method stopPropagation() to abort the execution of + * further listeners in your event listener. + * + * @author Guilherme Blanco + * @author Jonathan Wage + * @author Roman Borschel + * @author Bernhard Schussek + * @author Nicolas Grekas + */ +class Event implements StoppableEventInterface +{ + private bool $propagationStopped = false; + + public function isPropagationStopped(): bool + { + return $this->propagationStopped; + } + + /** + * Stops the propagation of the event to further event listeners. + * + * If multiple event listeners are connected to the same event, no + * further event listener will be triggered once any trigger calls + * stopPropagation(). + */ + public function stopPropagation(): void + { + $this->propagationStopped = true; + } +} diff --git a/3rdparty/symfony/event-dispatcher-contracts/EventDispatcherInterface.php b/3rdparty/symfony/event-dispatcher-contracts/EventDispatcherInterface.php new file mode 100644 index 00000000..2d7840d3 --- /dev/null +++ b/3rdparty/symfony/event-dispatcher-contracts/EventDispatcherInterface.php @@ -0,0 +1,33 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Contracts\EventDispatcher; + +use Psr\EventDispatcher\EventDispatcherInterface as PsrEventDispatcherInterface; + +/** + * Allows providing hooks on domain-specific lifecycles by dispatching events. + */ +interface EventDispatcherInterface extends PsrEventDispatcherInterface +{ + /** + * Dispatches an event to all registered listeners. + * + * @template T of object + * + * @param T $event The event to pass to the event handlers/listeners + * @param string|null $eventName The name of the event to dispatch. If not supplied, + * the class of $event should be used instead. + * + * @return T The passed $event MUST be returned + */ + public function dispatch(object $event, ?string $eventName = null): object; +} diff --git a/3rdparty/symfony/event-dispatcher-contracts/LICENSE b/3rdparty/symfony/event-dispatcher-contracts/LICENSE new file mode 100644 index 00000000..7536caea --- /dev/null +++ b/3rdparty/symfony/event-dispatcher-contracts/LICENSE @@ -0,0 +1,19 @@ +Copyright (c) 2018-present Fabien Potencier + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is furnished +to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +THE SOFTWARE. diff --git a/3rdparty/symfony/event-dispatcher/Attribute/AsEventListener.php b/3rdparty/symfony/event-dispatcher/Attribute/AsEventListener.php new file mode 100644 index 00000000..bb931b82 --- /dev/null +++ b/3rdparty/symfony/event-dispatcher/Attribute/AsEventListener.php @@ -0,0 +1,29 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Component\EventDispatcher\Attribute; + +/** + * Service tag to autoconfigure event listeners. + * + * @author Alexander M. Turek + */ +#[\Attribute(\Attribute::TARGET_CLASS | \Attribute::TARGET_METHOD | \Attribute::IS_REPEATABLE)] +class AsEventListener +{ + public function __construct( + public ?string $event = null, + public ?string $method = null, + public int $priority = 0, + public ?string $dispatcher = null, + ) { + } +} diff --git a/3rdparty/symfony/event-dispatcher/Debug/TraceableEventDispatcher.php b/3rdparty/symfony/event-dispatcher/Debug/TraceableEventDispatcher.php new file mode 100644 index 00000000..5ba83dad --- /dev/null +++ b/3rdparty/symfony/event-dispatcher/Debug/TraceableEventDispatcher.php @@ -0,0 +1,375 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Component\EventDispatcher\Debug; + +use Psr\EventDispatcher\StoppableEventInterface; +use Psr\Log\LoggerInterface; +use Symfony\Component\EventDispatcher\EventDispatcher; +use Symfony\Component\EventDispatcher\EventDispatcherInterface; +use Symfony\Component\EventDispatcher\EventSubscriberInterface; +use Symfony\Component\HttpFoundation\Request; +use Symfony\Component\HttpFoundation\RequestStack; +use Symfony\Component\Stopwatch\Stopwatch; +use Symfony\Contracts\Service\ResetInterface; + +/** + * Collects some data about event listeners. + * + * This event dispatcher delegates the dispatching to another one. + * + * @author Fabien Potencier + */ +class TraceableEventDispatcher implements EventDispatcherInterface, ResetInterface +{ + protected $logger; + protected $stopwatch; + + /** + * @var \SplObjectStorage|null + */ + private ?\SplObjectStorage $callStack = null; + private EventDispatcherInterface $dispatcher; + private array $wrappedListeners = []; + private array $orphanedEvents = []; + private ?RequestStack $requestStack; + private string $currentRequestHash = ''; + + public function __construct(EventDispatcherInterface $dispatcher, Stopwatch $stopwatch, ?LoggerInterface $logger = null, ?RequestStack $requestStack = null) + { + $this->dispatcher = $dispatcher; + $this->stopwatch = $stopwatch; + $this->logger = $logger; + $this->requestStack = $requestStack; + } + + /** + * @return void + */ + public function addListener(string $eventName, callable|array $listener, int $priority = 0) + { + $this->dispatcher->addListener($eventName, $listener, $priority); + } + + /** + * @return void + */ + public function addSubscriber(EventSubscriberInterface $subscriber) + { + $this->dispatcher->addSubscriber($subscriber); + } + + /** + * @return void + */ + public function removeListener(string $eventName, callable|array $listener) + { + if (isset($this->wrappedListeners[$eventName])) { + foreach ($this->wrappedListeners[$eventName] as $index => $wrappedListener) { + if ($wrappedListener->getWrappedListener() === $listener || ($listener instanceof \Closure && $wrappedListener->getWrappedListener() == $listener)) { + $listener = $wrappedListener; + unset($this->wrappedListeners[$eventName][$index]); + break; + } + } + } + + $this->dispatcher->removeListener($eventName, $listener); + } + + /** + * @return void + */ + public function removeSubscriber(EventSubscriberInterface $subscriber) + { + $this->dispatcher->removeSubscriber($subscriber); + } + + public function getListeners(?string $eventName = null): array + { + return $this->dispatcher->getListeners($eventName); + } + + public function getListenerPriority(string $eventName, callable|array $listener): ?int + { + // we might have wrapped listeners for the event (if called while dispatching) + // in that case get the priority by wrapper + if (isset($this->wrappedListeners[$eventName])) { + foreach ($this->wrappedListeners[$eventName] as $wrappedListener) { + if ($wrappedListener->getWrappedListener() === $listener || ($listener instanceof \Closure && $wrappedListener->getWrappedListener() == $listener)) { + return $this->dispatcher->getListenerPriority($eventName, $wrappedListener); + } + } + } + + return $this->dispatcher->getListenerPriority($eventName, $listener); + } + + public function hasListeners(?string $eventName = null): bool + { + return $this->dispatcher->hasListeners($eventName); + } + + public function dispatch(object $event, ?string $eventName = null): object + { + $eventName ??= $event::class; + + $this->callStack ??= new \SplObjectStorage(); + + $currentRequestHash = $this->currentRequestHash = $this->requestStack && ($request = $this->requestStack->getCurrentRequest()) ? spl_object_hash($request) : ''; + + if (null !== $this->logger && $event instanceof StoppableEventInterface && $event->isPropagationStopped()) { + $this->logger->debug(sprintf('The "%s" event is already stopped. No listeners have been called.', $eventName)); + } + + $this->preProcess($eventName); + try { + $this->beforeDispatch($eventName, $event); + try { + $e = $this->stopwatch->start($eventName, 'section'); + try { + $this->dispatcher->dispatch($event, $eventName); + } finally { + if ($e->isStarted()) { + $e->stop(); + } + } + } finally { + $this->afterDispatch($eventName, $event); + } + } finally { + $this->currentRequestHash = $currentRequestHash; + $this->postProcess($eventName); + } + + return $event; + } + + public function getCalledListeners(?Request $request = null): array + { + if (null === $this->callStack) { + return []; + } + + $hash = $request ? spl_object_hash($request) : null; + $called = []; + foreach ($this->callStack as $listener) { + [$eventName, $requestHash] = $this->callStack->getInfo(); + if (null === $hash || $hash === $requestHash) { + $called[] = $listener->getInfo($eventName); + } + } + + return $called; + } + + public function getNotCalledListeners(?Request $request = null): array + { + try { + $allListeners = $this->dispatcher instanceof EventDispatcher ? $this->getListenersWithPriority() : $this->getListenersWithoutPriority(); + } catch (\Exception $e) { + $this->logger?->info('An exception was thrown while getting the uncalled listeners.', ['exception' => $e]); + + // unable to retrieve the uncalled listeners + return []; + } + + $hash = $request ? spl_object_hash($request) : null; + $calledListeners = []; + + if (null !== $this->callStack) { + foreach ($this->callStack as $calledListener) { + [, $requestHash] = $this->callStack->getInfo(); + + if (null === $hash || $hash === $requestHash) { + $calledListeners[] = $calledListener->getWrappedListener(); + } + } + } + + $notCalled = []; + + foreach ($allListeners as $eventName => $listeners) { + foreach ($listeners as [$listener, $priority]) { + if (!\in_array($listener, $calledListeners, true)) { + if (!$listener instanceof WrappedListener) { + $listener = new WrappedListener($listener, null, $this->stopwatch, $this, $priority); + } + $notCalled[] = $listener->getInfo($eventName); + } + } + } + + uasort($notCalled, $this->sortNotCalledListeners(...)); + + return $notCalled; + } + + public function getOrphanedEvents(?Request $request = null): array + { + if ($request) { + return $this->orphanedEvents[spl_object_hash($request)] ?? []; + } + + if (!$this->orphanedEvents) { + return []; + } + + return array_merge(...array_values($this->orphanedEvents)); + } + + /** + * @return void + */ + public function reset() + { + $this->callStack = null; + $this->orphanedEvents = []; + $this->currentRequestHash = ''; + } + + /** + * Proxies all method calls to the original event dispatcher. + * + * @param string $method The method name + * @param array $arguments The method arguments + */ + public function __call(string $method, array $arguments): mixed + { + return $this->dispatcher->{$method}(...$arguments); + } + + /** + * Called before dispatching the event. + * + * @return void + */ + protected function beforeDispatch(string $eventName, object $event) + { + } + + /** + * Called after dispatching the event. + * + * @return void + */ + protected function afterDispatch(string $eventName, object $event) + { + } + + private function preProcess(string $eventName): void + { + if (!$this->dispatcher->hasListeners($eventName)) { + $this->orphanedEvents[$this->currentRequestHash][] = $eventName; + + return; + } + + foreach ($this->dispatcher->getListeners($eventName) as $listener) { + $priority = $this->getListenerPriority($eventName, $listener); + $wrappedListener = new WrappedListener($listener instanceof WrappedListener ? $listener->getWrappedListener() : $listener, null, $this->stopwatch, $this); + $this->wrappedListeners[$eventName][] = $wrappedListener; + $this->dispatcher->removeListener($eventName, $listener); + $this->dispatcher->addListener($eventName, $wrappedListener, $priority); + $this->callStack->attach($wrappedListener, [$eventName, $this->currentRequestHash]); + } + } + + private function postProcess(string $eventName): void + { + unset($this->wrappedListeners[$eventName]); + $skipped = false; + foreach ($this->dispatcher->getListeners($eventName) as $listener) { + if (!$listener instanceof WrappedListener) { // #12845: a new listener was added during dispatch. + continue; + } + // Unwrap listener + $priority = $this->getListenerPriority($eventName, $listener); + $this->dispatcher->removeListener($eventName, $listener); + $this->dispatcher->addListener($eventName, $listener->getWrappedListener(), $priority); + + if (null !== $this->logger) { + $context = ['event' => $eventName, 'listener' => $listener->getPretty()]; + } + + if ($listener->wasCalled()) { + $this->logger?->debug('Notified event "{event}" to listener "{listener}".', $context); + } else { + $this->callStack->detach($listener); + } + + if (null !== $this->logger && $skipped) { + $this->logger->debug('Listener "{listener}" was not called for event "{event}".', $context); + } + + if ($listener->stoppedPropagation()) { + $this->logger?->debug('Listener "{listener}" stopped propagation of the event "{event}".', $context); + + $skipped = true; + } + } + } + + private function sortNotCalledListeners(array $a, array $b): int + { + if (0 !== $cmp = strcmp($a['event'], $b['event'])) { + return $cmp; + } + + if (\is_int($a['priority']) && !\is_int($b['priority'])) { + return 1; + } + + if (!\is_int($a['priority']) && \is_int($b['priority'])) { + return -1; + } + + if ($a['priority'] === $b['priority']) { + return 0; + } + + if ($a['priority'] > $b['priority']) { + return -1; + } + + return 1; + } + + private function getListenersWithPriority(): array + { + $result = []; + + $allListeners = new \ReflectionProperty(EventDispatcher::class, 'listeners'); + + foreach ($allListeners->getValue($this->dispatcher) as $eventName => $listenersByPriority) { + foreach ($listenersByPriority as $priority => $listeners) { + foreach ($listeners as $listener) { + $result[$eventName][] = [$listener, $priority]; + } + } + } + + return $result; + } + + private function getListenersWithoutPriority(): array + { + $result = []; + + foreach ($this->getListeners() as $eventName => $listeners) { + foreach ($listeners as $listener) { + $result[$eventName][] = [$listener, null]; + } + } + + return $result; + } +} diff --git a/3rdparty/symfony/event-dispatcher/Debug/WrappedListener.php b/3rdparty/symfony/event-dispatcher/Debug/WrappedListener.php new file mode 100644 index 00000000..59f7c136 --- /dev/null +++ b/3rdparty/symfony/event-dispatcher/Debug/WrappedListener.php @@ -0,0 +1,144 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Component\EventDispatcher\Debug; + +use Psr\EventDispatcher\StoppableEventInterface; +use Symfony\Component\EventDispatcher\EventDispatcherInterface; +use Symfony\Component\Stopwatch\Stopwatch; +use Symfony\Component\VarDumper\Caster\ClassStub; + +/** + * @author Fabien Potencier + */ +final class WrappedListener +{ + private string|array|object $listener; + private ?\Closure $optimizedListener; + private string $name; + private bool $called = false; + private bool $stoppedPropagation = false; + private Stopwatch $stopwatch; + private ?EventDispatcherInterface $dispatcher; + private string $pretty; + private string $callableRef; + private ClassStub|string $stub; + private ?int $priority = null; + private static bool $hasClassStub; + + public function __construct(callable|array $listener, ?string $name, Stopwatch $stopwatch, ?EventDispatcherInterface $dispatcher = null, ?int $priority = null) + { + $this->listener = $listener; + $this->optimizedListener = $listener instanceof \Closure ? $listener : (\is_callable($listener) ? $listener(...) : null); + $this->stopwatch = $stopwatch; + $this->dispatcher = $dispatcher; + $this->priority = $priority; + + if (\is_array($listener)) { + [$this->name, $this->callableRef] = $this->parseListener($listener); + $this->pretty = $this->name.'::'.$listener[1]; + $this->callableRef .= '::'.$listener[1]; + } elseif ($listener instanceof \Closure) { + $r = new \ReflectionFunction($listener); + if (str_contains($r->name, '{closure')) { + $this->pretty = $this->name = 'closure'; + } elseif ($class = \PHP_VERSION_ID >= 80111 ? $r->getClosureCalledClass() : $r->getClosureScopeClass()) { + $this->name = $class->name; + $this->pretty = $this->name.'::'.$r->name; + } else { + $this->pretty = $this->name = $r->name; + } + } elseif (\is_string($listener)) { + $this->pretty = $this->name = $listener; + } else { + $this->name = get_debug_type($listener); + $this->pretty = $this->name.'::__invoke'; + $this->callableRef = $listener::class.'::__invoke'; + } + + if (null !== $name) { + $this->name = $name; + } + + self::$hasClassStub ??= class_exists(ClassStub::class); + } + + public function getWrappedListener(): callable|array + { + return $this->listener; + } + + public function wasCalled(): bool + { + return $this->called; + } + + public function stoppedPropagation(): bool + { + return $this->stoppedPropagation; + } + + public function getPretty(): string + { + return $this->pretty; + } + + public function getInfo(string $eventName): array + { + $this->stub ??= self::$hasClassStub ? new ClassStub($this->pretty.'()', $this->callableRef ?? $this->listener) : $this->pretty.'()'; + + return [ + 'event' => $eventName, + 'priority' => $this->priority ??= $this->dispatcher?->getListenerPriority($eventName, $this->listener), + 'pretty' => $this->pretty, + 'stub' => $this->stub, + ]; + } + + public function __invoke(object $event, string $eventName, EventDispatcherInterface $dispatcher): void + { + $dispatcher = $this->dispatcher ?: $dispatcher; + + $this->called = true; + $this->priority ??= $dispatcher->getListenerPriority($eventName, $this->listener); + + $e = $this->stopwatch->start($this->name, 'event_listener'); + + try { + ($this->optimizedListener ?? $this->listener)($event, $eventName, $dispatcher); + } finally { + if ($e->isStarted()) { + $e->stop(); + } + } + + if ($event instanceof StoppableEventInterface && $event->isPropagationStopped()) { + $this->stoppedPropagation = true; + } + } + + private function parseListener(array $listener): array + { + if ($listener[0] instanceof \Closure) { + foreach ((new \ReflectionFunction($listener[0]))->getAttributes(\Closure::class) as $attribute) { + if ($name = $attribute->getArguments()['name'] ?? false) { + return [$name, $attribute->getArguments()['class'] ?? $name]; + } + } + } + + if (\is_object($listener[0])) { + return [get_debug_type($listener[0]), $listener[0]::class]; + } + + return [$listener[0], $listener[0]]; + } +} diff --git a/3rdparty/symfony/event-dispatcher/DependencyInjection/AddEventAliasesPass.php b/3rdparty/symfony/event-dispatcher/DependencyInjection/AddEventAliasesPass.php new file mode 100644 index 00000000..13b4336a --- /dev/null +++ b/3rdparty/symfony/event-dispatcher/DependencyInjection/AddEventAliasesPass.php @@ -0,0 +1,40 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Component\EventDispatcher\DependencyInjection; + +use Symfony\Component\DependencyInjection\Compiler\CompilerPassInterface; +use Symfony\Component\DependencyInjection\ContainerBuilder; + +/** + * This pass allows bundles to extend the list of event aliases. + * + * @author Alexander M. Turek + */ +class AddEventAliasesPass implements CompilerPassInterface +{ + private array $eventAliases; + + public function __construct(array $eventAliases) + { + $this->eventAliases = $eventAliases; + } + + public function process(ContainerBuilder $container): void + { + $eventAliases = $container->hasParameter('event_dispatcher.event_aliases') ? $container->getParameter('event_dispatcher.event_aliases') : []; + + $container->setParameter( + 'event_dispatcher.event_aliases', + array_merge($eventAliases, $this->eventAliases) + ); + } +} diff --git a/3rdparty/symfony/event-dispatcher/DependencyInjection/RegisterListenersPass.php b/3rdparty/symfony/event-dispatcher/DependencyInjection/RegisterListenersPass.php new file mode 100644 index 00000000..866f4e64 --- /dev/null +++ b/3rdparty/symfony/event-dispatcher/DependencyInjection/RegisterListenersPass.php @@ -0,0 +1,216 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Component\EventDispatcher\DependencyInjection; + +use Symfony\Component\DependencyInjection\Argument\ServiceClosureArgument; +use Symfony\Component\DependencyInjection\Compiler\CompilerPassInterface; +use Symfony\Component\DependencyInjection\ContainerBuilder; +use Symfony\Component\DependencyInjection\Exception\InvalidArgumentException; +use Symfony\Component\DependencyInjection\Reference; +use Symfony\Component\EventDispatcher\EventDispatcher; +use Symfony\Component\EventDispatcher\EventSubscriberInterface; +use Symfony\Contracts\EventDispatcher\Event; + +/** + * Compiler pass to register tagged services for an event dispatcher. + */ +class RegisterListenersPass implements CompilerPassInterface +{ + private array $hotPathEvents = []; + private array $noPreloadEvents = []; + + /** + * @return $this + */ + public function setHotPathEvents(array $hotPathEvents): static + { + $this->hotPathEvents = array_flip($hotPathEvents); + + return $this; + } + + /** + * @return $this + */ + public function setNoPreloadEvents(array $noPreloadEvents): static + { + $this->noPreloadEvents = array_flip($noPreloadEvents); + + return $this; + } + + /** + * @return void + */ + public function process(ContainerBuilder $container) + { + if (!$container->hasDefinition('event_dispatcher') && !$container->hasAlias('event_dispatcher')) { + return; + } + + $aliases = []; + + if ($container->hasParameter('event_dispatcher.event_aliases')) { + $aliases = $container->getParameter('event_dispatcher.event_aliases'); + } + + $globalDispatcherDefinition = $container->findDefinition('event_dispatcher'); + + foreach ($container->findTaggedServiceIds('kernel.event_listener', true) as $id => $events) { + $noPreload = 0; + + foreach ($events as $event) { + $priority = $event['priority'] ?? 0; + + if (!isset($event['event'])) { + if ($container->getDefinition($id)->hasTag('kernel.event_subscriber')) { + continue; + } + + $event['method'] ??= '__invoke'; + $event['event'] = $this->getEventFromTypeDeclaration($container, $id, $event['method']); + } + + $event['event'] = $aliases[$event['event']] ?? $event['event']; + + if (!isset($event['method'])) { + $event['method'] = 'on'.preg_replace_callback([ + '/(?<=\b|_)[a-z]/i', + '/[^a-z0-9]/i', + ], fn ($matches) => strtoupper($matches[0]), $event['event']); + $event['method'] = preg_replace('/[^a-z0-9]/i', '', $event['method']); + + if (null !== ($class = $container->getDefinition($id)->getClass()) && ($r = $container->getReflectionClass($class, false)) && !$r->hasMethod($event['method'])) { + if (!$r->hasMethod('__invoke')) { + throw new InvalidArgumentException(sprintf('None of the "%s" or "__invoke" methods exist for the service "%s". Please define the "method" attribute on "kernel.event_listener" tags.', $event['method'], $id)); + } + + $event['method'] = '__invoke'; + } + } + + $dispatcherDefinition = $globalDispatcherDefinition; + if (isset($event['dispatcher'])) { + $dispatcherDefinition = $container->findDefinition($event['dispatcher']); + } + + $dispatcherDefinition->addMethodCall('addListener', [$event['event'], [new ServiceClosureArgument(new Reference($id)), $event['method']], $priority]); + + if (isset($this->hotPathEvents[$event['event']])) { + $container->getDefinition($id)->addTag('container.hot_path'); + } elseif (isset($this->noPreloadEvents[$event['event']])) { + ++$noPreload; + } + } + + if ($noPreload && \count($events) === $noPreload) { + $container->getDefinition($id)->addTag('container.no_preload'); + } + } + + $extractingDispatcher = new ExtractingEventDispatcher(); + + foreach ($container->findTaggedServiceIds('kernel.event_subscriber', true) as $id => $tags) { + $def = $container->getDefinition($id); + + // We must assume that the class value has been correctly filled, even if the service is created by a factory + $class = $def->getClass(); + + if (!$r = $container->getReflectionClass($class)) { + throw new InvalidArgumentException(sprintf('Class "%s" used for service "%s" cannot be found.', $class, $id)); + } + if (!$r->isSubclassOf(EventSubscriberInterface::class)) { + throw new InvalidArgumentException(sprintf('Service "%s" must implement interface "%s".', $id, EventSubscriberInterface::class)); + } + $class = $r->name; + + $dispatcherDefinitions = []; + foreach ($tags as $attributes) { + if (!isset($attributes['dispatcher']) || isset($dispatcherDefinitions[$attributes['dispatcher']])) { + continue; + } + + $dispatcherDefinitions[$attributes['dispatcher']] = $container->findDefinition($attributes['dispatcher']); + } + + if (!$dispatcherDefinitions) { + $dispatcherDefinitions = [$globalDispatcherDefinition]; + } + + $noPreload = 0; + ExtractingEventDispatcher::$aliases = $aliases; + ExtractingEventDispatcher::$subscriber = $class; + $extractingDispatcher->addSubscriber($extractingDispatcher); + foreach ($extractingDispatcher->listeners as $args) { + $args[1] = [new ServiceClosureArgument(new Reference($id)), $args[1]]; + foreach ($dispatcherDefinitions as $dispatcherDefinition) { + $dispatcherDefinition->addMethodCall('addListener', $args); + } + + if (isset($this->hotPathEvents[$args[0]])) { + $container->getDefinition($id)->addTag('container.hot_path'); + } elseif (isset($this->noPreloadEvents[$args[0]])) { + ++$noPreload; + } + } + if ($noPreload && \count($extractingDispatcher->listeners) === $noPreload) { + $container->getDefinition($id)->addTag('container.no_preload'); + } + $extractingDispatcher->listeners = []; + ExtractingEventDispatcher::$aliases = []; + } + } + + private function getEventFromTypeDeclaration(ContainerBuilder $container, string $id, string $method): string + { + if ( + null === ($class = $container->getDefinition($id)->getClass()) + || !($r = $container->getReflectionClass($class, false)) + || !$r->hasMethod($method) + || 1 > ($m = $r->getMethod($method))->getNumberOfParameters() + || !($type = $m->getParameters()[0]->getType()) instanceof \ReflectionNamedType + || $type->isBuiltin() + || Event::class === ($name = $type->getName()) + ) { + throw new InvalidArgumentException(sprintf('Service "%s" must define the "event" attribute on "kernel.event_listener" tags.', $id)); + } + + return $name; + } +} + +/** + * @internal + */ +class ExtractingEventDispatcher extends EventDispatcher implements EventSubscriberInterface +{ + public array $listeners = []; + + public static array $aliases = []; + public static string $subscriber; + + public function addListener(string $eventName, callable|array $listener, int $priority = 0): void + { + $this->listeners[] = [$eventName, $listener[1], $priority]; + } + + public static function getSubscribedEvents(): array + { + $events = []; + + foreach ([self::$subscriber, 'getSubscribedEvents']() as $eventName => $params) { + $events[self::$aliases[$eventName] ?? $eventName] = $params; + } + + return $events; + } +} diff --git a/3rdparty/symfony/event-dispatcher/EventDispatcher.php b/3rdparty/symfony/event-dispatcher/EventDispatcher.php new file mode 100644 index 00000000..60529892 --- /dev/null +++ b/3rdparty/symfony/event-dispatcher/EventDispatcher.php @@ -0,0 +1,270 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Component\EventDispatcher; + +use Psr\EventDispatcher\StoppableEventInterface; +use Symfony\Component\EventDispatcher\Debug\WrappedListener; + +/** + * The EventDispatcherInterface is the central point of Symfony's event listener system. + * + * Listeners are registered on the manager and events are dispatched through the + * manager. + * + * @author Guilherme Blanco + * @author Jonathan Wage + * @author Roman Borschel + * @author Bernhard Schussek + * @author Fabien Potencier + * @author Jordi Boggiano + * @author Jordan Alliot + * @author Nicolas Grekas + */ +class EventDispatcher implements EventDispatcherInterface +{ + private array $listeners = []; + private array $sorted = []; + private array $optimized; + + public function __construct() + { + if (__CLASS__ === static::class) { + $this->optimized = []; + } + } + + public function dispatch(object $event, ?string $eventName = null): object + { + $eventName ??= $event::class; + + if (isset($this->optimized)) { + $listeners = $this->optimized[$eventName] ?? (empty($this->listeners[$eventName]) ? [] : $this->optimizeListeners($eventName)); + } else { + $listeners = $this->getListeners($eventName); + } + + if ($listeners) { + $this->callListeners($listeners, $eventName, $event); + } + + return $event; + } + + public function getListeners(?string $eventName = null): array + { + if (null !== $eventName) { + if (empty($this->listeners[$eventName])) { + return []; + } + + if (!isset($this->sorted[$eventName])) { + $this->sortListeners($eventName); + } + + return $this->sorted[$eventName]; + } + + foreach ($this->listeners as $eventName => $eventListeners) { + if (!isset($this->sorted[$eventName])) { + $this->sortListeners($eventName); + } + } + + return array_filter($this->sorted); + } + + public function getListenerPriority(string $eventName, callable|array $listener): ?int + { + if (empty($this->listeners[$eventName])) { + return null; + } + + if (\is_array($listener) && isset($listener[0]) && $listener[0] instanceof \Closure && 2 >= \count($listener)) { + $listener[0] = $listener[0](); + $listener[1] ??= '__invoke'; + } + + foreach ($this->listeners[$eventName] as $priority => &$listeners) { + foreach ($listeners as &$v) { + if ($v !== $listener && \is_array($v) && isset($v[0]) && $v[0] instanceof \Closure && 2 >= \count($v)) { + $v[0] = $v[0](); + $v[1] ??= '__invoke'; + } + if ($v === $listener || ($listener instanceof \Closure && $v == $listener)) { + return $priority; + } + } + } + + return null; + } + + public function hasListeners(?string $eventName = null): bool + { + if (null !== $eventName) { + return !empty($this->listeners[$eventName]); + } + + foreach ($this->listeners as $eventListeners) { + if ($eventListeners) { + return true; + } + } + + return false; + } + + /** + * @return void + */ + public function addListener(string $eventName, callable|array $listener, int $priority = 0) + { + $this->listeners[$eventName][$priority][] = $listener; + unset($this->sorted[$eventName], $this->optimized[$eventName]); + } + + /** + * @return void + */ + public function removeListener(string $eventName, callable|array $listener) + { + if (empty($this->listeners[$eventName])) { + return; + } + + if (\is_array($listener) && isset($listener[0]) && $listener[0] instanceof \Closure && 2 >= \count($listener)) { + $listener[0] = $listener[0](); + $listener[1] ??= '__invoke'; + } + + foreach ($this->listeners[$eventName] as $priority => &$listeners) { + foreach ($listeners as $k => &$v) { + if ($v !== $listener && \is_array($v) && isset($v[0]) && $v[0] instanceof \Closure && 2 >= \count($v)) { + $v[0] = $v[0](); + $v[1] ??= '__invoke'; + } + if ($v === $listener || ($listener instanceof \Closure && $v == $listener)) { + unset($listeners[$k], $this->sorted[$eventName], $this->optimized[$eventName]); + } + } + + if (!$listeners) { + unset($this->listeners[$eventName][$priority]); + } + } + } + + /** + * @return void + */ + public function addSubscriber(EventSubscriberInterface $subscriber) + { + foreach ($subscriber->getSubscribedEvents() as $eventName => $params) { + if (\is_string($params)) { + $this->addListener($eventName, [$subscriber, $params]); + } elseif (\is_string($params[0])) { + $this->addListener($eventName, [$subscriber, $params[0]], $params[1] ?? 0); + } else { + foreach ($params as $listener) { + $this->addListener($eventName, [$subscriber, $listener[0]], $listener[1] ?? 0); + } + } + } + } + + /** + * @return void + */ + public function removeSubscriber(EventSubscriberInterface $subscriber) + { + foreach ($subscriber->getSubscribedEvents() as $eventName => $params) { + if (\is_array($params) && \is_array($params[0])) { + foreach ($params as $listener) { + $this->removeListener($eventName, [$subscriber, $listener[0]]); + } + } else { + $this->removeListener($eventName, [$subscriber, \is_string($params) ? $params : $params[0]]); + } + } + } + + /** + * Triggers the listeners of an event. + * + * This method can be overridden to add functionality that is executed + * for each listener. + * + * @param callable[] $listeners The event listeners + * @param string $eventName The name of the event to dispatch + * @param object $event The event object to pass to the event handlers/listeners + * + * @return void + */ + protected function callListeners(iterable $listeners, string $eventName, object $event) + { + $stoppable = $event instanceof StoppableEventInterface; + + foreach ($listeners as $listener) { + if ($stoppable && $event->isPropagationStopped()) { + break; + } + $listener($event, $eventName, $this); + } + } + + /** + * Sorts the internal list of listeners for the given event by priority. + */ + private function sortListeners(string $eventName): void + { + krsort($this->listeners[$eventName]); + $this->sorted[$eventName] = []; + + foreach ($this->listeners[$eventName] as &$listeners) { + foreach ($listeners as &$listener) { + if (\is_array($listener) && isset($listener[0]) && $listener[0] instanceof \Closure && 2 >= \count($listener)) { + $listener[0] = $listener[0](); + $listener[1] ??= '__invoke'; + } + $this->sorted[$eventName][] = $listener; + } + } + } + + /** + * Optimizes the internal list of listeners for the given event by priority. + */ + private function optimizeListeners(string $eventName): array + { + krsort($this->listeners[$eventName]); + $this->optimized[$eventName] = []; + + foreach ($this->listeners[$eventName] as &$listeners) { + foreach ($listeners as &$listener) { + $closure = &$this->optimized[$eventName][]; + if (\is_array($listener) && isset($listener[0]) && $listener[0] instanceof \Closure && 2 >= \count($listener)) { + $closure = static function (...$args) use (&$listener, &$closure) { + if ($listener[0] instanceof \Closure) { + $listener[0] = $listener[0](); + $listener[1] ??= '__invoke'; + } + ($closure = $listener(...))(...$args); + }; + } else { + $closure = $listener instanceof WrappedListener ? $listener : $listener(...); + } + } + } + + return $this->optimized[$eventName]; + } +} diff --git a/3rdparty/symfony/event-dispatcher/EventDispatcherInterface.php b/3rdparty/symfony/event-dispatcher/EventDispatcherInterface.php new file mode 100644 index 00000000..e95a7b11 --- /dev/null +++ b/3rdparty/symfony/event-dispatcher/EventDispatcherInterface.php @@ -0,0 +1,75 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Component\EventDispatcher; + +use Symfony\Contracts\EventDispatcher\EventDispatcherInterface as ContractsEventDispatcherInterface; + +/** + * The EventDispatcherInterface is the central point of Symfony's event listener system. + * Listeners are registered on the manager and events are dispatched through the + * manager. + * + * @author Bernhard Schussek + */ +interface EventDispatcherInterface extends ContractsEventDispatcherInterface +{ + /** + * Adds an event listener that listens on the specified events. + * + * @param int $priority The higher this value, the earlier an event + * listener will be triggered in the chain (defaults to 0) + * + * @return void + */ + public function addListener(string $eventName, callable $listener, int $priority = 0); + + /** + * Adds an event subscriber. + * + * The subscriber is asked for all the events it is + * interested in and added as a listener for these events. + * + * @return void + */ + public function addSubscriber(EventSubscriberInterface $subscriber); + + /** + * Removes an event listener from the specified events. + * + * @return void + */ + public function removeListener(string $eventName, callable $listener); + + /** + * @return void + */ + public function removeSubscriber(EventSubscriberInterface $subscriber); + + /** + * Gets the listeners of a specific event or all listeners sorted by descending priority. + * + * @return array + */ + public function getListeners(?string $eventName = null): array; + + /** + * Gets the listener priority for a specific event. + * + * Returns null if the event or the listener does not exist. + */ + public function getListenerPriority(string $eventName, callable $listener): ?int; + + /** + * Checks whether an event has any registered listeners. + */ + public function hasListeners(?string $eventName = null): bool; +} diff --git a/3rdparty/symfony/event-dispatcher/EventSubscriberInterface.php b/3rdparty/symfony/event-dispatcher/EventSubscriberInterface.php new file mode 100644 index 00000000..2085e428 --- /dev/null +++ b/3rdparty/symfony/event-dispatcher/EventSubscriberInterface.php @@ -0,0 +1,49 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Component\EventDispatcher; + +/** + * An EventSubscriber knows itself what events it is interested in. + * If an EventSubscriber is added to an EventDispatcherInterface, the manager invokes + * {@link getSubscribedEvents} and registers the subscriber as a listener for all + * returned events. + * + * @author Guilherme Blanco + * @author Jonathan Wage + * @author Roman Borschel + * @author Bernhard Schussek + */ +interface EventSubscriberInterface +{ + /** + * Returns an array of event names this subscriber wants to listen to. + * + * The array keys are event names and the value can be: + * + * * The method name to call (priority defaults to 0) + * * An array composed of the method name to call and the priority + * * An array of arrays composed of the method names to call and respective + * priorities, or 0 if unset + * + * For instance: + * + * * ['eventName' => 'methodName'] + * * ['eventName' => ['methodName', $priority]] + * * ['eventName' => [['methodName1', $priority], ['methodName2']]] + * + * The code must not depend on runtime state as it will only be called at compile time. + * All logic depending on runtime state must be put into the individual methods handling the events. + * + * @return array> + */ + public static function getSubscribedEvents(); +} diff --git a/3rdparty/symfony/event-dispatcher/GenericEvent.php b/3rdparty/symfony/event-dispatcher/GenericEvent.php new file mode 100644 index 00000000..0ccbbd81 --- /dev/null +++ b/3rdparty/symfony/event-dispatcher/GenericEvent.php @@ -0,0 +1,158 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Component\EventDispatcher; + +use Symfony\Contracts\EventDispatcher\Event; + +/** + * Event encapsulation class. + * + * Encapsulates events thus decoupling the observer from the subject they encapsulate. + * + * @author Drak + * + * @implements \ArrayAccess + * @implements \IteratorAggregate + */ +class GenericEvent extends Event implements \ArrayAccess, \IteratorAggregate +{ + protected $subject; + protected $arguments; + + /** + * Encapsulate an event with $subject and $arguments. + * + * @param mixed $subject The subject of the event, usually an object or a callable + * @param array $arguments Arguments to store in the event + */ + public function __construct(mixed $subject = null, array $arguments = []) + { + $this->subject = $subject; + $this->arguments = $arguments; + } + + /** + * Getter for subject property. + */ + public function getSubject(): mixed + { + return $this->subject; + } + + /** + * Get argument by key. + * + * @throws \InvalidArgumentException if key is not found + */ + public function getArgument(string $key): mixed + { + if ($this->hasArgument($key)) { + return $this->arguments[$key]; + } + + throw new \InvalidArgumentException(sprintf('Argument "%s" not found.', $key)); + } + + /** + * Add argument to event. + * + * @return $this + */ + public function setArgument(string $key, mixed $value): static + { + $this->arguments[$key] = $value; + + return $this; + } + + /** + * Getter for all arguments. + */ + public function getArguments(): array + { + return $this->arguments; + } + + /** + * Set args property. + * + * @return $this + */ + public function setArguments(array $args = []): static + { + $this->arguments = $args; + + return $this; + } + + /** + * Has argument. + */ + public function hasArgument(string $key): bool + { + return \array_key_exists($key, $this->arguments); + } + + /** + * ArrayAccess for argument getter. + * + * @param string $key Array key + * + * @throws \InvalidArgumentException if key does not exist in $this->args + */ + public function offsetGet(mixed $key): mixed + { + return $this->getArgument($key); + } + + /** + * ArrayAccess for argument setter. + * + * @param string $key Array key to set + */ + public function offsetSet(mixed $key, mixed $value): void + { + $this->setArgument($key, $value); + } + + /** + * ArrayAccess for unset argument. + * + * @param string $key Array key + */ + public function offsetUnset(mixed $key): void + { + if ($this->hasArgument($key)) { + unset($this->arguments[$key]); + } + } + + /** + * ArrayAccess has argument. + * + * @param string $key Array key + */ + public function offsetExists(mixed $key): bool + { + return $this->hasArgument($key); + } + + /** + * IteratorAggregate for iterating over the object like an array. + * + * @return \ArrayIterator + */ + public function getIterator(): \ArrayIterator + { + return new \ArrayIterator($this->arguments); + } +} diff --git a/3rdparty/symfony/event-dispatcher/ImmutableEventDispatcher.php b/3rdparty/symfony/event-dispatcher/ImmutableEventDispatcher.php new file mode 100644 index 00000000..301a805c --- /dev/null +++ b/3rdparty/symfony/event-dispatcher/ImmutableEventDispatcher.php @@ -0,0 +1,79 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Component\EventDispatcher; + +/** + * A read-only proxy for an event dispatcher. + * + * @author Bernhard Schussek + */ +class ImmutableEventDispatcher implements EventDispatcherInterface +{ + private EventDispatcherInterface $dispatcher; + + public function __construct(EventDispatcherInterface $dispatcher) + { + $this->dispatcher = $dispatcher; + } + + public function dispatch(object $event, ?string $eventName = null): object + { + return $this->dispatcher->dispatch($event, $eventName); + } + + /** + * @return never + */ + public function addListener(string $eventName, callable|array $listener, int $priority = 0) + { + throw new \BadMethodCallException('Unmodifiable event dispatchers must not be modified.'); + } + + /** + * @return never + */ + public function addSubscriber(EventSubscriberInterface $subscriber) + { + throw new \BadMethodCallException('Unmodifiable event dispatchers must not be modified.'); + } + + /** + * @return never + */ + public function removeListener(string $eventName, callable|array $listener) + { + throw new \BadMethodCallException('Unmodifiable event dispatchers must not be modified.'); + } + + /** + * @return never + */ + public function removeSubscriber(EventSubscriberInterface $subscriber) + { + throw new \BadMethodCallException('Unmodifiable event dispatchers must not be modified.'); + } + + public function getListeners(?string $eventName = null): array + { + return $this->dispatcher->getListeners($eventName); + } + + public function getListenerPriority(string $eventName, callable|array $listener): ?int + { + return $this->dispatcher->getListenerPriority($eventName, $listener); + } + + public function hasListeners(?string $eventName = null): bool + { + return $this->dispatcher->hasListeners($eventName); + } +} diff --git a/3rdparty/symfony/event-dispatcher/LICENSE b/3rdparty/symfony/event-dispatcher/LICENSE new file mode 100644 index 00000000..0138f8f0 --- /dev/null +++ b/3rdparty/symfony/event-dispatcher/LICENSE @@ -0,0 +1,19 @@ +Copyright (c) 2004-present Fabien Potencier + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is furnished +to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +THE SOFTWARE. diff --git a/3rdparty/symfony/http-foundation/AcceptHeader.php b/3rdparty/symfony/http-foundation/AcceptHeader.php new file mode 100644 index 00000000..853c000e --- /dev/null +++ b/3rdparty/symfony/http-foundation/AcceptHeader.php @@ -0,0 +1,150 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Component\HttpFoundation; + +// Help opcache.preload discover always-needed symbols +class_exists(AcceptHeaderItem::class); + +/** + * Represents an Accept-* header. + * + * An accept header is compound with a list of items, + * sorted by descending quality. + * + * @author Jean-François Simon + */ +class AcceptHeader +{ + /** + * @var AcceptHeaderItem[] + */ + private array $items = []; + + private bool $sorted = true; + + /** + * @param AcceptHeaderItem[] $items + */ + public function __construct(array $items) + { + foreach ($items as $item) { + $this->add($item); + } + } + + /** + * Builds an AcceptHeader instance from a string. + */ + public static function fromString(?string $headerValue): self + { + $parts = HeaderUtils::split($headerValue ?? '', ',;='); + + return new self(array_map(function ($subParts) { + static $index = 0; + $part = array_shift($subParts); + $attributes = HeaderUtils::combine($subParts); + + $item = new AcceptHeaderItem($part[0], $attributes); + $item->setIndex($index++); + + return $item; + }, $parts)); + } + + /** + * Returns header value's string representation. + */ + public function __toString(): string + { + return implode(',', $this->items); + } + + /** + * Tests if header has given value. + */ + public function has(string $value): bool + { + return isset($this->items[$value]); + } + + /** + * Returns given value's item, if exists. + */ + public function get(string $value): ?AcceptHeaderItem + { + return $this->items[$value] ?? $this->items[explode('/', $value)[0].'/*'] ?? $this->items['*/*'] ?? $this->items['*'] ?? null; + } + + /** + * Adds an item. + * + * @return $this + */ + public function add(AcceptHeaderItem $item): static + { + $this->items[$item->getValue()] = $item; + $this->sorted = false; + + return $this; + } + + /** + * Returns all items. + * + * @return AcceptHeaderItem[] + */ + public function all(): array + { + $this->sort(); + + return $this->items; + } + + /** + * Filters items on their value using given regex. + */ + public function filter(string $pattern): self + { + return new self(array_filter($this->items, fn (AcceptHeaderItem $item) => preg_match($pattern, $item->getValue()))); + } + + /** + * Returns first item. + */ + public function first(): ?AcceptHeaderItem + { + $this->sort(); + + return $this->items ? reset($this->items) : null; + } + + /** + * Sorts items by descending quality. + */ + private function sort(): void + { + if (!$this->sorted) { + uasort($this->items, function (AcceptHeaderItem $a, AcceptHeaderItem $b) { + $qA = $a->getQuality(); + $qB = $b->getQuality(); + + if ($qA === $qB) { + return $a->getIndex() > $b->getIndex() ? 1 : -1; + } + + return $qA > $qB ? -1 : 1; + }); + + $this->sorted = true; + } + } +} diff --git a/3rdparty/symfony/http-foundation/AcceptHeaderItem.php b/3rdparty/symfony/http-foundation/AcceptHeaderItem.php new file mode 100644 index 00000000..35ecd4ea --- /dev/null +++ b/3rdparty/symfony/http-foundation/AcceptHeaderItem.php @@ -0,0 +1,159 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Component\HttpFoundation; + +/** + * Represents an Accept-* header item. + * + * @author Jean-François Simon + */ +class AcceptHeaderItem +{ + private string $value; + private float $quality = 1.0; + private int $index = 0; + private array $attributes = []; + + public function __construct(string $value, array $attributes = []) + { + $this->value = $value; + foreach ($attributes as $name => $value) { + $this->setAttribute($name, $value); + } + } + + /** + * Builds an AcceptHeaderInstance instance from a string. + */ + public static function fromString(?string $itemValue): self + { + $parts = HeaderUtils::split($itemValue ?? '', ';='); + + $part = array_shift($parts); + $attributes = HeaderUtils::combine($parts); + + return new self($part[0], $attributes); + } + + /** + * Returns header value's string representation. + */ + public function __toString(): string + { + $string = $this->value.($this->quality < 1 ? ';q='.$this->quality : ''); + if (\count($this->attributes) > 0) { + $string .= '; '.HeaderUtils::toString($this->attributes, ';'); + } + + return $string; + } + + /** + * Set the item value. + * + * @return $this + */ + public function setValue(string $value): static + { + $this->value = $value; + + return $this; + } + + /** + * Returns the item value. + */ + public function getValue(): string + { + return $this->value; + } + + /** + * Set the item quality. + * + * @return $this + */ + public function setQuality(float $quality): static + { + $this->quality = $quality; + + return $this; + } + + /** + * Returns the item quality. + */ + public function getQuality(): float + { + return $this->quality; + } + + /** + * Set the item index. + * + * @return $this + */ + public function setIndex(int $index): static + { + $this->index = $index; + + return $this; + } + + /** + * Returns the item index. + */ + public function getIndex(): int + { + return $this->index; + } + + /** + * Tests if an attribute exists. + */ + public function hasAttribute(string $name): bool + { + return isset($this->attributes[$name]); + } + + /** + * Returns an attribute by its name. + */ + public function getAttribute(string $name, mixed $default = null): mixed + { + return $this->attributes[$name] ?? $default; + } + + /** + * Returns all attributes. + */ + public function getAttributes(): array + { + return $this->attributes; + } + + /** + * Set an attribute. + * + * @return $this + */ + public function setAttribute(string $name, string $value): static + { + if ('q' === $name) { + $this->quality = (float) $value; + } else { + $this->attributes[$name] = $value; + } + + return $this; + } +} diff --git a/3rdparty/symfony/http-foundation/BinaryFileResponse.php b/3rdparty/symfony/http-foundation/BinaryFileResponse.php new file mode 100644 index 00000000..9cd86879 --- /dev/null +++ b/3rdparty/symfony/http-foundation/BinaryFileResponse.php @@ -0,0 +1,385 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Component\HttpFoundation; + +use Symfony\Component\HttpFoundation\File\Exception\FileException; +use Symfony\Component\HttpFoundation\File\File; + +/** + * BinaryFileResponse represents an HTTP response delivering a file. + * + * @author Niklas Fiekas + * @author stealth35 + * @author Igor Wiedler + * @author Jordan Alliot + * @author Sergey Linnik + */ +class BinaryFileResponse extends Response +{ + protected static $trustXSendfileTypeHeader = false; + + /** + * @var File + */ + protected $file; + protected $offset = 0; + protected $maxlen = -1; + protected $deleteFileAfterSend = false; + protected $chunkSize = 16 * 1024; + + /** + * @param \SplFileInfo|string $file The file to stream + * @param int $status The response status code (200 "OK" by default) + * @param array $headers An array of response headers + * @param bool $public Files are public by default + * @param string|null $contentDisposition The type of Content-Disposition to set automatically with the filename + * @param bool $autoEtag Whether the ETag header should be automatically set + * @param bool $autoLastModified Whether the Last-Modified header should be automatically set + */ + public function __construct(\SplFileInfo|string $file, int $status = 200, array $headers = [], bool $public = true, ?string $contentDisposition = null, bool $autoEtag = false, bool $autoLastModified = true) + { + parent::__construct(null, $status, $headers); + + $this->setFile($file, $contentDisposition, $autoEtag, $autoLastModified); + + if ($public) { + $this->setPublic(); + } + } + + /** + * Sets the file to stream. + * + * @return $this + * + * @throws FileException + */ + public function setFile(\SplFileInfo|string $file, ?string $contentDisposition = null, bool $autoEtag = false, bool $autoLastModified = true): static + { + if (!$file instanceof File) { + if ($file instanceof \SplFileInfo) { + $file = new File($file->getPathname()); + } else { + $file = new File((string) $file); + } + } + + if (!$file->isReadable()) { + throw new FileException('File must be readable.'); + } + + $this->file = $file; + + if ($autoEtag) { + $this->setAutoEtag(); + } + + if ($autoLastModified) { + $this->setAutoLastModified(); + } + + if ($contentDisposition) { + $this->setContentDisposition($contentDisposition); + } + + return $this; + } + + /** + * Gets the file. + */ + public function getFile(): File + { + return $this->file; + } + + /** + * Sets the response stream chunk size. + * + * @return $this + */ + public function setChunkSize(int $chunkSize): static + { + if ($chunkSize < 1 || $chunkSize > \PHP_INT_MAX) { + throw new \LogicException('The chunk size of a BinaryFileResponse cannot be less than 1 or greater than PHP_INT_MAX.'); + } + + $this->chunkSize = $chunkSize; + + return $this; + } + + /** + * Automatically sets the Last-Modified header according the file modification date. + * + * @return $this + */ + public function setAutoLastModified(): static + { + $this->setLastModified(\DateTimeImmutable::createFromFormat('U', $this->file->getMTime())); + + return $this; + } + + /** + * Automatically sets the ETag header according to the checksum of the file. + * + * @return $this + */ + public function setAutoEtag(): static + { + $this->setEtag(base64_encode(hash_file('sha256', $this->file->getPathname(), true))); + + return $this; + } + + /** + * Sets the Content-Disposition header with the given filename. + * + * @param string $disposition ResponseHeaderBag::DISPOSITION_INLINE or ResponseHeaderBag::DISPOSITION_ATTACHMENT + * @param string $filename Optionally use this UTF-8 encoded filename instead of the real name of the file + * @param string $filenameFallback A fallback filename, containing only ASCII characters. Defaults to an automatically encoded filename + * + * @return $this + */ + public function setContentDisposition(string $disposition, string $filename = '', string $filenameFallback = ''): static + { + if ('' === $filename) { + $filename = $this->file->getFilename(); + } + + if ('' === $filenameFallback && (!preg_match('/^[\x20-\x7e]*$/', $filename) || str_contains($filename, '%'))) { + $encoding = mb_detect_encoding($filename, null, true) ?: '8bit'; + + for ($i = 0, $filenameLength = mb_strlen($filename, $encoding); $i < $filenameLength; ++$i) { + $char = mb_substr($filename, $i, 1, $encoding); + + if ('%' === $char || \ord($char[0]) < 32 || \ord($char[0]) > 126) { + $filenameFallback .= '_'; + } else { + $filenameFallback .= $char; + } + } + } + + $dispositionHeader = $this->headers->makeDisposition($disposition, $filename, $filenameFallback); + $this->headers->set('Content-Disposition', $dispositionHeader); + + return $this; + } + + public function prepare(Request $request): static + { + if ($this->isInformational() || $this->isEmpty()) { + parent::prepare($request); + + $this->maxlen = 0; + + return $this; + } + + if (!$this->headers->has('Content-Type')) { + $this->headers->set('Content-Type', $this->file->getMimeType() ?: 'application/octet-stream'); + } + + parent::prepare($request); + + $this->offset = 0; + $this->maxlen = -1; + + if (false === $fileSize = $this->file->getSize()) { + return $this; + } + $this->headers->remove('Transfer-Encoding'); + $this->headers->set('Content-Length', $fileSize); + + if (!$this->headers->has('Accept-Ranges')) { + // Only accept ranges on safe HTTP methods + $this->headers->set('Accept-Ranges', $request->isMethodSafe() ? 'bytes' : 'none'); + } + + if (self::$trustXSendfileTypeHeader && $request->headers->has('X-Sendfile-Type')) { + // Use X-Sendfile, do not send any content. + $type = $request->headers->get('X-Sendfile-Type'); + $path = $this->file->getRealPath(); + // Fall back to scheme://path for stream wrapped locations. + if (false === $path) { + $path = $this->file->getPathname(); + } + if ('x-accel-redirect' === strtolower($type)) { + // Do X-Accel-Mapping substitutions. + // @link https://github.com/rack/rack/blob/main/lib/rack/sendfile.rb + // @link https://mattbrictson.com/blog/accelerated-rails-downloads + if (!$request->headers->has('X-Accel-Mapping')) { + throw new \LogicException('The "X-Accel-Mapping" header must be set when "X-Sendfile-Type" is set to "X-Accel-Redirect".'); + } + $parts = HeaderUtils::split($request->headers->get('X-Accel-Mapping'), ',='); + foreach ($parts as $part) { + [$pathPrefix, $location] = $part; + if (str_starts_with($path, $pathPrefix)) { + $path = $location.substr($path, \strlen($pathPrefix)); + // Only set X-Accel-Redirect header if a valid URI can be produced + // as nginx does not serve arbitrary file paths. + $this->headers->set($type, rawurlencode($path)); + $this->maxlen = 0; + break; + } + } + } else { + $this->headers->set($type, $path); + $this->maxlen = 0; + } + } elseif ($request->headers->has('Range') && $request->isMethod('GET')) { + // Process the range headers. + if (!$request->headers->has('If-Range') || $this->hasValidIfRangeHeader($request->headers->get('If-Range'))) { + $range = $request->headers->get('Range'); + + if (str_starts_with($range, 'bytes=')) { + [$start, $end] = explode('-', substr($range, 6), 2) + [1 => 0]; + + $end = ('' === $end) ? $fileSize - 1 : (int) $end; + + if ('' === $start) { + $start = $fileSize - $end; + $end = $fileSize - 1; + } else { + $start = (int) $start; + } + + if ($start <= $end) { + $end = min($end, $fileSize - 1); + if ($start < 0 || $start > $end) { + $this->setStatusCode(416); + $this->headers->set('Content-Range', \sprintf('bytes */%s', $fileSize)); + } elseif ($end - $start < $fileSize - 1) { + $this->maxlen = $end < $fileSize ? $end - $start + 1 : -1; + $this->offset = $start; + + $this->setStatusCode(206); + $this->headers->set('Content-Range', \sprintf('bytes %s-%s/%s', $start, $end, $fileSize)); + $this->headers->set('Content-Length', $end - $start + 1); + } + } + } + } + } + + if ($request->isMethod('HEAD')) { + $this->maxlen = 0; + } + + return $this; + } + + private function hasValidIfRangeHeader(?string $header): bool + { + if ($this->getEtag() === $header) { + return true; + } + + if (null === $lastModified = $this->getLastModified()) { + return false; + } + + return $lastModified->format('D, d M Y H:i:s').' GMT' === $header; + } + + public function sendContent(): static + { + try { + if (!$this->isSuccessful()) { + return $this; + } + + if (0 === $this->maxlen) { + return $this; + } + + $out = fopen('php://output', 'w'); + $file = fopen($this->file->getPathname(), 'r'); + + ignore_user_abort(true); + + if (0 !== $this->offset) { + fseek($file, $this->offset); + } + + $length = $this->maxlen; + while ($length && !feof($file)) { + $read = $length > $this->chunkSize || 0 > $length ? $this->chunkSize : $length; + + if (false === $data = fread($file, $read)) { + break; + } + while ('' !== $data) { + $read = fwrite($out, $data); + if (false === $read || connection_aborted()) { + break 2; + } + if (0 < $length) { + $length -= $read; + } + $data = substr($data, $read); + } + } + + fclose($out); + fclose($file); + } finally { + if ($this->deleteFileAfterSend && is_file($this->file->getPathname())) { + unlink($this->file->getPathname()); + } + } + + return $this; + } + + /** + * @throws \LogicException when the content is not null + */ + public function setContent(?string $content): static + { + if (null !== $content) { + throw new \LogicException('The content cannot be set on a BinaryFileResponse instance.'); + } + + return $this; + } + + public function getContent(): string|false + { + return false; + } + + /** + * Trust X-Sendfile-Type header. + * + * @return void + */ + public static function trustXSendfileTypeHeader() + { + self::$trustXSendfileTypeHeader = true; + } + + /** + * If this is set to true, the file will be unlinked after the request is sent + * Note: If the X-Sendfile header is used, the deleteFileAfterSend setting will not be used. + * + * @return $this + */ + public function deleteFileAfterSend(bool $shouldDelete = true): static + { + $this->deleteFileAfterSend = $shouldDelete; + + return $this; + } +} diff --git a/3rdparty/symfony/http-foundation/ChainRequestMatcher.php b/3rdparty/symfony/http-foundation/ChainRequestMatcher.php new file mode 100644 index 00000000..29486fc8 --- /dev/null +++ b/3rdparty/symfony/http-foundation/ChainRequestMatcher.php @@ -0,0 +1,38 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Component\HttpFoundation; + +/** + * ChainRequestMatcher verifies that all checks match against a Request instance. + * + * @author Fabien Potencier + */ +class ChainRequestMatcher implements RequestMatcherInterface +{ + /** + * @param iterable $matchers + */ + public function __construct(private iterable $matchers) + { + } + + public function matches(Request $request): bool + { + foreach ($this->matchers as $matcher) { + if (!$matcher->matches($request)) { + return false; + } + } + + return true; + } +} diff --git a/3rdparty/symfony/http-foundation/Cookie.php b/3rdparty/symfony/http-foundation/Cookie.php new file mode 100644 index 00000000..05c6c62d --- /dev/null +++ b/3rdparty/symfony/http-foundation/Cookie.php @@ -0,0 +1,412 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Component\HttpFoundation; + +/** + * Represents a cookie. + * + * @author Johannes M. Schmitt + */ +class Cookie +{ + public const SAMESITE_NONE = 'none'; + public const SAMESITE_LAX = 'lax'; + public const SAMESITE_STRICT = 'strict'; + + protected $name; + protected $value; + protected $domain; + protected $expire; + protected $path; + protected $secure; + protected $httpOnly; + + private bool $raw; + private ?string $sameSite = null; + private bool $partitioned = false; + private bool $secureDefault = false; + + private const RESERVED_CHARS_LIST = "=,; \t\r\n\v\f"; + private const RESERVED_CHARS_FROM = ['=', ',', ';', ' ', "\t", "\r", "\n", "\v", "\f"]; + private const RESERVED_CHARS_TO = ['%3D', '%2C', '%3B', '%20', '%09', '%0D', '%0A', '%0B', '%0C']; + + /** + * Creates cookie from raw header string. + */ + public static function fromString(string $cookie, bool $decode = false): static + { + $data = [ + 'expires' => 0, + 'path' => '/', + 'domain' => null, + 'secure' => false, + 'httponly' => false, + 'raw' => !$decode, + 'samesite' => null, + 'partitioned' => false, + ]; + + $parts = HeaderUtils::split($cookie, ';='); + $part = array_shift($parts); + + $name = $decode ? urldecode($part[0]) : $part[0]; + $value = isset($part[1]) ? ($decode ? urldecode($part[1]) : $part[1]) : null; + + $data = HeaderUtils::combine($parts) + $data; + $data['expires'] = self::expiresTimestamp($data['expires']); + + if (isset($data['max-age']) && ($data['max-age'] > 0 || $data['expires'] > time())) { + $data['expires'] = time() + (int) $data['max-age']; + } + + return new static($name, $value, $data['expires'], $data['path'], $data['domain'], $data['secure'], $data['httponly'], $data['raw'], $data['samesite'], $data['partitioned']); + } + + /** + * @see self::__construct + * + * @param self::SAMESITE_*|''|null $sameSite + * @param bool $partitioned + */ + public static function create(string $name, ?string $value = null, int|string|\DateTimeInterface $expire = 0, ?string $path = '/', ?string $domain = null, ?bool $secure = null, bool $httpOnly = true, bool $raw = false, ?string $sameSite = self::SAMESITE_LAX /* , bool $partitioned = false */): self + { + $partitioned = 9 < \func_num_args() ? func_get_arg(9) : false; + + return new self($name, $value, $expire, $path, $domain, $secure, $httpOnly, $raw, $sameSite, $partitioned); + } + + /** + * @param string $name The name of the cookie + * @param string|null $value The value of the cookie + * @param int|string|\DateTimeInterface $expire The time the cookie expires + * @param string|null $path The path on the server in which the cookie will be available on + * @param string|null $domain The domain that the cookie is available to + * @param bool|null $secure Whether the client should send back the cookie only over HTTPS or null to auto-enable this when the request is already using HTTPS + * @param bool $httpOnly Whether the cookie will be made accessible only through the HTTP protocol + * @param bool $raw Whether the cookie value should be sent with no url encoding + * @param self::SAMESITE_*|''|null $sameSite Whether the cookie will be available for cross-site requests + * + * @throws \InvalidArgumentException + */ + public function __construct(string $name, ?string $value = null, int|string|\DateTimeInterface $expire = 0, ?string $path = '/', ?string $domain = null, ?bool $secure = null, bool $httpOnly = true, bool $raw = false, ?string $sameSite = self::SAMESITE_LAX, bool $partitioned = false) + { + // from PHP source code + if ($raw && false !== strpbrk($name, self::RESERVED_CHARS_LIST)) { + throw new \InvalidArgumentException(\sprintf('The cookie name "%s" contains invalid characters.', $name)); + } + + if (empty($name)) { + throw new \InvalidArgumentException('The cookie name cannot be empty.'); + } + + $this->name = $name; + $this->value = $value; + $this->domain = $domain; + $this->expire = self::expiresTimestamp($expire); + $this->path = empty($path) ? '/' : $path; + $this->secure = $secure; + $this->httpOnly = $httpOnly; + $this->raw = $raw; + $this->sameSite = $this->withSameSite($sameSite)->sameSite; + $this->partitioned = $partitioned; + } + + /** + * Creates a cookie copy with a new value. + */ + public function withValue(?string $value): static + { + $cookie = clone $this; + $cookie->value = $value; + + return $cookie; + } + + /** + * Creates a cookie copy with a new domain that the cookie is available to. + */ + public function withDomain(?string $domain): static + { + $cookie = clone $this; + $cookie->domain = $domain; + + return $cookie; + } + + /** + * Creates a cookie copy with a new time the cookie expires. + */ + public function withExpires(int|string|\DateTimeInterface $expire = 0): static + { + $cookie = clone $this; + $cookie->expire = self::expiresTimestamp($expire); + + return $cookie; + } + + /** + * Converts expires formats to a unix timestamp. + */ + private static function expiresTimestamp(int|string|\DateTimeInterface $expire = 0): int + { + // convert expiration time to a Unix timestamp + if ($expire instanceof \DateTimeInterface) { + $expire = $expire->format('U'); + } elseif (!is_numeric($expire)) { + $expire = strtotime($expire); + + if (false === $expire) { + throw new \InvalidArgumentException('The cookie expiration time is not valid.'); + } + } + + return 0 < $expire ? (int) $expire : 0; + } + + /** + * Creates a cookie copy with a new path on the server in which the cookie will be available on. + */ + public function withPath(string $path): static + { + $cookie = clone $this; + $cookie->path = '' === $path ? '/' : $path; + + return $cookie; + } + + /** + * Creates a cookie copy that only be transmitted over a secure HTTPS connection from the client. + */ + public function withSecure(bool $secure = true): static + { + $cookie = clone $this; + $cookie->secure = $secure; + + return $cookie; + } + + /** + * Creates a cookie copy that be accessible only through the HTTP protocol. + */ + public function withHttpOnly(bool $httpOnly = true): static + { + $cookie = clone $this; + $cookie->httpOnly = $httpOnly; + + return $cookie; + } + + /** + * Creates a cookie copy that uses no url encoding. + */ + public function withRaw(bool $raw = true): static + { + if ($raw && false !== strpbrk($this->name, self::RESERVED_CHARS_LIST)) { + throw new \InvalidArgumentException(\sprintf('The cookie name "%s" contains invalid characters.', $this->name)); + } + + $cookie = clone $this; + $cookie->raw = $raw; + + return $cookie; + } + + /** + * Creates a cookie copy with SameSite attribute. + * + * @param self::SAMESITE_*|''|null $sameSite + */ + public function withSameSite(?string $sameSite): static + { + if ('' === $sameSite) { + $sameSite = null; + } elseif (null !== $sameSite) { + $sameSite = strtolower($sameSite); + } + + if (!\in_array($sameSite, [self::SAMESITE_LAX, self::SAMESITE_STRICT, self::SAMESITE_NONE, null], true)) { + throw new \InvalidArgumentException('The "sameSite" parameter value is not valid.'); + } + + $cookie = clone $this; + $cookie->sameSite = $sameSite; + + return $cookie; + } + + /** + * Creates a cookie copy that is tied to the top-level site in cross-site context. + */ + public function withPartitioned(bool $partitioned = true): static + { + $cookie = clone $this; + $cookie->partitioned = $partitioned; + + return $cookie; + } + + /** + * Returns the cookie as a string. + */ + public function __toString(): string + { + if ($this->isRaw()) { + $str = $this->getName(); + } else { + $str = str_replace(self::RESERVED_CHARS_FROM, self::RESERVED_CHARS_TO, $this->getName()); + } + + $str .= '='; + + if ('' === (string) $this->getValue()) { + $str .= 'deleted; expires='.gmdate('D, d M Y H:i:s T', time() - 31536001).'; Max-Age=0'; + } else { + $str .= $this->isRaw() ? $this->getValue() : rawurlencode($this->getValue()); + + if (0 !== $this->getExpiresTime()) { + $str .= '; expires='.gmdate('D, d M Y H:i:s T', $this->getExpiresTime()).'; Max-Age='.$this->getMaxAge(); + } + } + + if ($this->getPath()) { + $str .= '; path='.$this->getPath(); + } + + if ($this->getDomain()) { + $str .= '; domain='.$this->getDomain(); + } + + if ($this->isSecure()) { + $str .= '; secure'; + } + + if ($this->isHttpOnly()) { + $str .= '; httponly'; + } + + if (null !== $this->getSameSite()) { + $str .= '; samesite='.$this->getSameSite(); + } + + if ($this->isPartitioned()) { + $str .= '; partitioned'; + } + + return $str; + } + + /** + * Gets the name of the cookie. + */ + public function getName(): string + { + return $this->name; + } + + /** + * Gets the value of the cookie. + */ + public function getValue(): ?string + { + return $this->value; + } + + /** + * Gets the domain that the cookie is available to. + */ + public function getDomain(): ?string + { + return $this->domain; + } + + /** + * Gets the time the cookie expires. + */ + public function getExpiresTime(): int + { + return $this->expire; + } + + /** + * Gets the max-age attribute. + */ + public function getMaxAge(): int + { + $maxAge = $this->expire - time(); + + return 0 >= $maxAge ? 0 : $maxAge; + } + + /** + * Gets the path on the server in which the cookie will be available on. + */ + public function getPath(): string + { + return $this->path; + } + + /** + * Checks whether the cookie should only be transmitted over a secure HTTPS connection from the client. + */ + public function isSecure(): bool + { + return $this->secure ?? $this->secureDefault; + } + + /** + * Checks whether the cookie will be made accessible only through the HTTP protocol. + */ + public function isHttpOnly(): bool + { + return $this->httpOnly; + } + + /** + * Whether this cookie is about to be cleared. + */ + public function isCleared(): bool + { + return 0 !== $this->expire && $this->expire < time(); + } + + /** + * Checks if the cookie value should be sent with no url encoding. + */ + public function isRaw(): bool + { + return $this->raw; + } + + /** + * Checks whether the cookie should be tied to the top-level site in cross-site context. + */ + public function isPartitioned(): bool + { + return $this->partitioned; + } + + /** + * @return self::SAMESITE_*|null + */ + public function getSameSite(): ?string + { + return $this->sameSite; + } + + /** + * @param bool $default The default value of the "secure" flag when it is set to null + */ + public function setSecureDefault(bool $default): void + { + $this->secureDefault = $default; + } +} diff --git a/3rdparty/symfony/http-foundation/Exception/BadRequestException.php b/3rdparty/symfony/http-foundation/Exception/BadRequestException.php new file mode 100644 index 00000000..505e1cfd --- /dev/null +++ b/3rdparty/symfony/http-foundation/Exception/BadRequestException.php @@ -0,0 +1,19 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Component\HttpFoundation\Exception; + +/** + * Raised when a user sends a malformed request. + */ +class BadRequestException extends UnexpectedValueException implements RequestExceptionInterface +{ +} diff --git a/3rdparty/symfony/http-foundation/Exception/ConflictingHeadersException.php b/3rdparty/symfony/http-foundation/Exception/ConflictingHeadersException.php new file mode 100644 index 00000000..77aa0e1e --- /dev/null +++ b/3rdparty/symfony/http-foundation/Exception/ConflictingHeadersException.php @@ -0,0 +1,21 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Component\HttpFoundation\Exception; + +/** + * The HTTP request contains headers with conflicting information. + * + * @author Magnus Nordlander + */ +class ConflictingHeadersException extends UnexpectedValueException implements RequestExceptionInterface +{ +} diff --git a/3rdparty/symfony/http-foundation/Exception/JsonException.php b/3rdparty/symfony/http-foundation/Exception/JsonException.php new file mode 100644 index 00000000..6d1e0aec --- /dev/null +++ b/3rdparty/symfony/http-foundation/Exception/JsonException.php @@ -0,0 +1,21 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Component\HttpFoundation\Exception; + +/** + * Thrown by Request::toArray() when the content cannot be JSON-decoded. + * + * @author Tobias Nyholm + */ +final class JsonException extends UnexpectedValueException implements RequestExceptionInterface +{ +} diff --git a/3rdparty/symfony/http-foundation/Exception/RequestExceptionInterface.php b/3rdparty/symfony/http-foundation/Exception/RequestExceptionInterface.php new file mode 100644 index 00000000..478d0dc7 --- /dev/null +++ b/3rdparty/symfony/http-foundation/Exception/RequestExceptionInterface.php @@ -0,0 +1,21 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Component\HttpFoundation\Exception; + +/** + * Interface for Request exceptions. + * + * Exceptions implementing this interface should trigger an HTTP 400 response in the application code. + */ +interface RequestExceptionInterface +{ +} diff --git a/3rdparty/symfony/http-foundation/Exception/SessionNotFoundException.php b/3rdparty/symfony/http-foundation/Exception/SessionNotFoundException.php new file mode 100644 index 00000000..80a21bf1 --- /dev/null +++ b/3rdparty/symfony/http-foundation/Exception/SessionNotFoundException.php @@ -0,0 +1,27 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Component\HttpFoundation\Exception; + +/** + * Raised when a session does not exist. This happens in the following cases: + * - the session is not enabled + * - attempt to read a session outside a request context (ie. cli script). + * + * @author Jérémy Derussé + */ +class SessionNotFoundException extends \LogicException implements RequestExceptionInterface +{ + public function __construct(string $message = 'There is currently no session available.', int $code = 0, ?\Throwable $previous = null) + { + parent::__construct($message, $code, $previous); + } +} diff --git a/3rdparty/symfony/http-foundation/Exception/SuspiciousOperationException.php b/3rdparty/symfony/http-foundation/Exception/SuspiciousOperationException.php new file mode 100644 index 00000000..4818ef2c --- /dev/null +++ b/3rdparty/symfony/http-foundation/Exception/SuspiciousOperationException.php @@ -0,0 +1,20 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Component\HttpFoundation\Exception; + +/** + * Raised when a user has performed an operation that should be considered + * suspicious from a security perspective. + */ +class SuspiciousOperationException extends UnexpectedValueException implements RequestExceptionInterface +{ +} diff --git a/3rdparty/symfony/http-foundation/Exception/UnexpectedValueException.php b/3rdparty/symfony/http-foundation/Exception/UnexpectedValueException.php new file mode 100644 index 00000000..c3e6c9d6 --- /dev/null +++ b/3rdparty/symfony/http-foundation/Exception/UnexpectedValueException.php @@ -0,0 +1,16 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Component\HttpFoundation\Exception; + +class UnexpectedValueException extends \UnexpectedValueException +{ +} diff --git a/3rdparty/symfony/http-foundation/ExpressionRequestMatcher.php b/3rdparty/symfony/http-foundation/ExpressionRequestMatcher.php new file mode 100644 index 00000000..fe65e920 --- /dev/null +++ b/3rdparty/symfony/http-foundation/ExpressionRequestMatcher.php @@ -0,0 +1,56 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Component\HttpFoundation; + +use Symfony\Component\ExpressionLanguage\Expression; +use Symfony\Component\ExpressionLanguage\ExpressionLanguage; +use Symfony\Component\HttpFoundation\RequestMatcher\ExpressionRequestMatcher as NewExpressionRequestMatcher; + +trigger_deprecation('symfony/http-foundation', '6.2', 'The "%s" class is deprecated, use "%s" instead.', ExpressionRequestMatcher::class, NewExpressionRequestMatcher::class); + +/** + * ExpressionRequestMatcher uses an expression to match a Request. + * + * @author Fabien Potencier + * + * @deprecated since Symfony 6.2, use "Symfony\Component\HttpFoundation\RequestMatcher\ExpressionRequestMatcher" instead + */ +class ExpressionRequestMatcher extends RequestMatcher +{ + private ExpressionLanguage $language; + private Expression|string $expression; + + /** + * @return void + */ + public function setExpression(ExpressionLanguage $language, Expression|string $expression) + { + $this->language = $language; + $this->expression = $expression; + } + + public function matches(Request $request): bool + { + if (!isset($this->language)) { + throw new \LogicException('Unable to match the request as the expression language is not available. Try running "composer require symfony/expression-language".'); + } + + return $this->language->evaluate($this->expression, [ + 'request' => $request, + 'method' => $request->getMethod(), + 'path' => rawurldecode($request->getPathInfo()), + 'host' => $request->getHost(), + 'ip' => $request->getClientIp(), + 'attributes' => $request->attributes->all(), + ]) && parent::matches($request); + } +} diff --git a/3rdparty/symfony/http-foundation/File/Exception/AccessDeniedException.php b/3rdparty/symfony/http-foundation/File/Exception/AccessDeniedException.php new file mode 100644 index 00000000..79ab0fce --- /dev/null +++ b/3rdparty/symfony/http-foundation/File/Exception/AccessDeniedException.php @@ -0,0 +1,25 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Component\HttpFoundation\File\Exception; + +/** + * Thrown when the access on a file was denied. + * + * @author Bernhard Schussek + */ +class AccessDeniedException extends FileException +{ + public function __construct(string $path) + { + parent::__construct(\sprintf('The file %s could not be accessed', $path)); + } +} diff --git a/3rdparty/symfony/http-foundation/File/Exception/CannotWriteFileException.php b/3rdparty/symfony/http-foundation/File/Exception/CannotWriteFileException.php new file mode 100644 index 00000000..c49f53a6 --- /dev/null +++ b/3rdparty/symfony/http-foundation/File/Exception/CannotWriteFileException.php @@ -0,0 +1,21 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Component\HttpFoundation\File\Exception; + +/** + * Thrown when an UPLOAD_ERR_CANT_WRITE error occurred with UploadedFile. + * + * @author Florent Mata + */ +class CannotWriteFileException extends FileException +{ +} diff --git a/3rdparty/symfony/http-foundation/File/Exception/ExtensionFileException.php b/3rdparty/symfony/http-foundation/File/Exception/ExtensionFileException.php new file mode 100644 index 00000000..ed83499c --- /dev/null +++ b/3rdparty/symfony/http-foundation/File/Exception/ExtensionFileException.php @@ -0,0 +1,21 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Component\HttpFoundation\File\Exception; + +/** + * Thrown when an UPLOAD_ERR_EXTENSION error occurred with UploadedFile. + * + * @author Florent Mata + */ +class ExtensionFileException extends FileException +{ +} diff --git a/3rdparty/symfony/http-foundation/File/Exception/FileException.php b/3rdparty/symfony/http-foundation/File/Exception/FileException.php new file mode 100644 index 00000000..fad5133e --- /dev/null +++ b/3rdparty/symfony/http-foundation/File/Exception/FileException.php @@ -0,0 +1,21 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Component\HttpFoundation\File\Exception; + +/** + * Thrown when an error occurred in the component File. + * + * @author Bernhard Schussek + */ +class FileException extends \RuntimeException +{ +} diff --git a/3rdparty/symfony/http-foundation/File/Exception/FileNotFoundException.php b/3rdparty/symfony/http-foundation/File/Exception/FileNotFoundException.php new file mode 100644 index 00000000..3a5eb039 --- /dev/null +++ b/3rdparty/symfony/http-foundation/File/Exception/FileNotFoundException.php @@ -0,0 +1,25 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Component\HttpFoundation\File\Exception; + +/** + * Thrown when a file was not found. + * + * @author Bernhard Schussek + */ +class FileNotFoundException extends FileException +{ + public function __construct(string $path) + { + parent::__construct(\sprintf('The file "%s" does not exist', $path)); + } +} diff --git a/3rdparty/symfony/http-foundation/File/Exception/FormSizeFileException.php b/3rdparty/symfony/http-foundation/File/Exception/FormSizeFileException.php new file mode 100644 index 00000000..8741be08 --- /dev/null +++ b/3rdparty/symfony/http-foundation/File/Exception/FormSizeFileException.php @@ -0,0 +1,21 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Component\HttpFoundation\File\Exception; + +/** + * Thrown when an UPLOAD_ERR_FORM_SIZE error occurred with UploadedFile. + * + * @author Florent Mata + */ +class FormSizeFileException extends FileException +{ +} diff --git a/3rdparty/symfony/http-foundation/File/Exception/IniSizeFileException.php b/3rdparty/symfony/http-foundation/File/Exception/IniSizeFileException.php new file mode 100644 index 00000000..c8fde610 --- /dev/null +++ b/3rdparty/symfony/http-foundation/File/Exception/IniSizeFileException.php @@ -0,0 +1,21 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Component\HttpFoundation\File\Exception; + +/** + * Thrown when an UPLOAD_ERR_INI_SIZE error occurred with UploadedFile. + * + * @author Florent Mata + */ +class IniSizeFileException extends FileException +{ +} diff --git a/3rdparty/symfony/http-foundation/File/Exception/NoFileException.php b/3rdparty/symfony/http-foundation/File/Exception/NoFileException.php new file mode 100644 index 00000000..4b48cc77 --- /dev/null +++ b/3rdparty/symfony/http-foundation/File/Exception/NoFileException.php @@ -0,0 +1,21 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Component\HttpFoundation\File\Exception; + +/** + * Thrown when an UPLOAD_ERR_NO_FILE error occurred with UploadedFile. + * + * @author Florent Mata + */ +class NoFileException extends FileException +{ +} diff --git a/3rdparty/symfony/http-foundation/File/Exception/NoTmpDirFileException.php b/3rdparty/symfony/http-foundation/File/Exception/NoTmpDirFileException.php new file mode 100644 index 00000000..bdead2d9 --- /dev/null +++ b/3rdparty/symfony/http-foundation/File/Exception/NoTmpDirFileException.php @@ -0,0 +1,21 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Component\HttpFoundation\File\Exception; + +/** + * Thrown when an UPLOAD_ERR_NO_TMP_DIR error occurred with UploadedFile. + * + * @author Florent Mata + */ +class NoTmpDirFileException extends FileException +{ +} diff --git a/3rdparty/symfony/http-foundation/File/Exception/PartialFileException.php b/3rdparty/symfony/http-foundation/File/Exception/PartialFileException.php new file mode 100644 index 00000000..4641efb5 --- /dev/null +++ b/3rdparty/symfony/http-foundation/File/Exception/PartialFileException.php @@ -0,0 +1,21 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Component\HttpFoundation\File\Exception; + +/** + * Thrown when an UPLOAD_ERR_PARTIAL error occurred with UploadedFile. + * + * @author Florent Mata + */ +class PartialFileException extends FileException +{ +} diff --git a/3rdparty/symfony/http-foundation/File/Exception/UnexpectedTypeException.php b/3rdparty/symfony/http-foundation/File/Exception/UnexpectedTypeException.php new file mode 100644 index 00000000..09b1c7e1 --- /dev/null +++ b/3rdparty/symfony/http-foundation/File/Exception/UnexpectedTypeException.php @@ -0,0 +1,20 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Component\HttpFoundation\File\Exception; + +class UnexpectedTypeException extends FileException +{ + public function __construct(mixed $value, string $expectedType) + { + parent::__construct(\sprintf('Expected argument of type %s, %s given', $expectedType, get_debug_type($value))); + } +} diff --git a/3rdparty/symfony/http-foundation/File/Exception/UploadException.php b/3rdparty/symfony/http-foundation/File/Exception/UploadException.php new file mode 100644 index 00000000..7074e765 --- /dev/null +++ b/3rdparty/symfony/http-foundation/File/Exception/UploadException.php @@ -0,0 +1,21 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Component\HttpFoundation\File\Exception; + +/** + * Thrown when an error occurred during file upload. + * + * @author Bernhard Schussek + */ +class UploadException extends FileException +{ +} diff --git a/3rdparty/symfony/http-foundation/File/File.php b/3rdparty/symfony/http-foundation/File/File.php new file mode 100644 index 00000000..c369ecbf --- /dev/null +++ b/3rdparty/symfony/http-foundation/File/File.php @@ -0,0 +1,141 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Component\HttpFoundation\File; + +use Symfony\Component\HttpFoundation\File\Exception\FileException; +use Symfony\Component\HttpFoundation\File\Exception\FileNotFoundException; +use Symfony\Component\Mime\MimeTypes; + +/** + * A file in the file system. + * + * @author Bernhard Schussek + */ +class File extends \SplFileInfo +{ + /** + * Constructs a new file from the given path. + * + * @param string $path The path to the file + * @param bool $checkPath Whether to check the path or not + * + * @throws FileNotFoundException If the given path is not a file + */ + public function __construct(string $path, bool $checkPath = true) + { + if ($checkPath && !is_file($path)) { + throw new FileNotFoundException($path); + } + + parent::__construct($path); + } + + /** + * Returns the extension based on the mime type. + * + * If the mime type is unknown, returns null. + * + * This method uses the mime type as guessed by getMimeType() + * to guess the file extension. + * + * @see MimeTypes + * @see getMimeType() + */ + public function guessExtension(): ?string + { + if (!class_exists(MimeTypes::class)) { + throw new \LogicException('You cannot guess the extension as the Mime component is not installed. Try running "composer require symfony/mime".'); + } + + return MimeTypes::getDefault()->getExtensions($this->getMimeType())[0] ?? null; + } + + /** + * Returns the mime type of the file. + * + * The mime type is guessed using a MimeTypeGuesserInterface instance, + * which uses finfo_file() then the "file" system binary, + * depending on which of those are available. + * + * @see MimeTypes + */ + public function getMimeType(): ?string + { + if (!class_exists(MimeTypes::class)) { + throw new \LogicException('You cannot guess the mime type as the Mime component is not installed. Try running "composer require symfony/mime".'); + } + + return MimeTypes::getDefault()->guessMimeType($this->getPathname()); + } + + /** + * Moves the file to a new location. + * + * @throws FileException if the target file could not be created + */ + public function move(string $directory, ?string $name = null): self + { + $target = $this->getTargetFile($directory, $name); + + set_error_handler(function ($type, $msg) use (&$error) { $error = $msg; }); + try { + $renamed = rename($this->getPathname(), $target); + } finally { + restore_error_handler(); + } + if (!$renamed) { + throw new FileException(\sprintf('Could not move the file "%s" to "%s" (%s).', $this->getPathname(), $target, strip_tags($error))); + } + + @chmod($target, 0666 & ~umask()); + + return $target; + } + + public function getContent(): string + { + $content = file_get_contents($this->getPathname()); + + if (false === $content) { + throw new FileException(\sprintf('Could not get the content of the file "%s".', $this->getPathname())); + } + + return $content; + } + + protected function getTargetFile(string $directory, ?string $name = null): self + { + if (!is_dir($directory)) { + if (false === @mkdir($directory, 0777, true) && !is_dir($directory)) { + throw new FileException(\sprintf('Unable to create the "%s" directory.', $directory)); + } + } elseif (!is_writable($directory)) { + throw new FileException(\sprintf('Unable to write in the "%s" directory.', $directory)); + } + + $target = rtrim($directory, '/\\').\DIRECTORY_SEPARATOR.(null === $name ? $this->getBasename() : $this->getName($name)); + + return new self($target, false); + } + + /** + * Returns locale independent base name of the given path. + */ + protected function getName(string $name): string + { + $originalName = str_replace('\\', '/', $name); + $pos = strrpos($originalName, '/'); + $originalName = false === $pos ? $originalName : substr($originalName, $pos + 1); + + return $originalName; + } +} diff --git a/3rdparty/symfony/http-foundation/File/Stream.php b/3rdparty/symfony/http-foundation/File/Stream.php new file mode 100644 index 00000000..2c156b2e --- /dev/null +++ b/3rdparty/symfony/http-foundation/File/Stream.php @@ -0,0 +1,25 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Component\HttpFoundation\File; + +/** + * A PHP stream of unknown size. + * + * @author Nicolas Grekas + */ +class Stream extends File +{ + public function getSize(): int|false + { + return false; + } +} diff --git a/3rdparty/symfony/http-foundation/File/UploadedFile.php b/3rdparty/symfony/http-foundation/File/UploadedFile.php new file mode 100644 index 00000000..85aab287 --- /dev/null +++ b/3rdparty/symfony/http-foundation/File/UploadedFile.php @@ -0,0 +1,269 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Component\HttpFoundation\File; + +use Symfony\Component\HttpFoundation\File\Exception\CannotWriteFileException; +use Symfony\Component\HttpFoundation\File\Exception\ExtensionFileException; +use Symfony\Component\HttpFoundation\File\Exception\FileException; +use Symfony\Component\HttpFoundation\File\Exception\FileNotFoundException; +use Symfony\Component\HttpFoundation\File\Exception\FormSizeFileException; +use Symfony\Component\HttpFoundation\File\Exception\IniSizeFileException; +use Symfony\Component\HttpFoundation\File\Exception\NoFileException; +use Symfony\Component\HttpFoundation\File\Exception\NoTmpDirFileException; +use Symfony\Component\HttpFoundation\File\Exception\PartialFileException; +use Symfony\Component\Mime\MimeTypes; + +/** + * A file uploaded through a form. + * + * @author Bernhard Schussek + * @author Florian Eckerstorfer + * @author Fabien Potencier + */ +class UploadedFile extends File +{ + private bool $test; + private string $originalName; + private string $mimeType; + private int $error; + + /** + * Accepts the information of the uploaded file as provided by the PHP global $_FILES. + * + * The file object is only created when the uploaded file is valid (i.e. when the + * isValid() method returns true). Otherwise the only methods that could be called + * on an UploadedFile instance are: + * + * * getClientOriginalName, + * * getClientMimeType, + * * isValid, + * * getError. + * + * Calling any other method on an non-valid instance will cause an unpredictable result. + * + * @param string $path The full temporary path to the file + * @param string $originalName The original file name of the uploaded file + * @param string|null $mimeType The type of the file as provided by PHP; null defaults to application/octet-stream + * @param int|null $error The error constant of the upload (one of PHP's UPLOAD_ERR_XXX constants); null defaults to UPLOAD_ERR_OK + * @param bool $test Whether the test mode is active + * Local files are used in test mode hence the code should not enforce HTTP uploads + * + * @throws FileException If file_uploads is disabled + * @throws FileNotFoundException If the file does not exist + */ + public function __construct(string $path, string $originalName, ?string $mimeType = null, ?int $error = null, bool $test = false) + { + $this->originalName = $this->getName($originalName); + $this->mimeType = $mimeType ?: 'application/octet-stream'; + $this->error = $error ?: \UPLOAD_ERR_OK; + $this->test = $test; + + parent::__construct($path, \UPLOAD_ERR_OK === $this->error); + } + + /** + * Returns the original file name. + * + * It is extracted from the request from which the file has been uploaded. + * This should not be considered as a safe value to use for a file name on your servers. + */ + public function getClientOriginalName(): string + { + return $this->originalName; + } + + /** + * Returns the original file extension. + * + * It is extracted from the original file name that was uploaded. + * This should not be considered as a safe value to use for a file name on your servers. + */ + public function getClientOriginalExtension(): string + { + return pathinfo($this->originalName, \PATHINFO_EXTENSION); + } + + /** + * Returns the file mime type. + * + * The client mime type is extracted from the request from which the file + * was uploaded, so it should not be considered as a safe value. + * + * For a trusted mime type, use getMimeType() instead (which guesses the mime + * type based on the file content). + * + * @see getMimeType() + */ + public function getClientMimeType(): string + { + return $this->mimeType; + } + + /** + * Returns the extension based on the client mime type. + * + * If the mime type is unknown, returns null. + * + * This method uses the mime type as guessed by getClientMimeType() + * to guess the file extension. As such, the extension returned + * by this method cannot be trusted. + * + * For a trusted extension, use guessExtension() instead (which guesses + * the extension based on the guessed mime type for the file). + * + * @see guessExtension() + * @see getClientMimeType() + */ + public function guessClientExtension(): ?string + { + if (!class_exists(MimeTypes::class)) { + throw new \LogicException('You cannot guess the extension as the Mime component is not installed. Try running "composer require symfony/mime".'); + } + + return MimeTypes::getDefault()->getExtensions($this->getClientMimeType())[0] ?? null; + } + + /** + * Returns the upload error. + * + * If the upload was successful, the constant UPLOAD_ERR_OK is returned. + * Otherwise one of the other UPLOAD_ERR_XXX constants is returned. + */ + public function getError(): int + { + return $this->error; + } + + /** + * Returns whether the file has been uploaded with HTTP and no error occurred. + */ + public function isValid(): bool + { + $isOk = \UPLOAD_ERR_OK === $this->error; + + return $this->test ? $isOk : $isOk && is_uploaded_file($this->getPathname()); + } + + /** + * Moves the file to a new location. + * + * @throws FileException if, for any reason, the file could not have been moved + */ + public function move(string $directory, ?string $name = null): File + { + if ($this->isValid()) { + if ($this->test) { + return parent::move($directory, $name); + } + + $target = $this->getTargetFile($directory, $name); + + set_error_handler(function ($type, $msg) use (&$error) { $error = $msg; }); + try { + $moved = move_uploaded_file($this->getPathname(), $target); + } finally { + restore_error_handler(); + } + if (!$moved) { + throw new FileException(\sprintf('Could not move the file "%s" to "%s" (%s).', $this->getPathname(), $target, strip_tags($error))); + } + + @chmod($target, 0666 & ~umask()); + + return $target; + } + + switch ($this->error) { + case \UPLOAD_ERR_INI_SIZE: + throw new IniSizeFileException($this->getErrorMessage()); + case \UPLOAD_ERR_FORM_SIZE: + throw new FormSizeFileException($this->getErrorMessage()); + case \UPLOAD_ERR_PARTIAL: + throw new PartialFileException($this->getErrorMessage()); + case \UPLOAD_ERR_NO_FILE: + throw new NoFileException($this->getErrorMessage()); + case \UPLOAD_ERR_CANT_WRITE: + throw new CannotWriteFileException($this->getErrorMessage()); + case \UPLOAD_ERR_NO_TMP_DIR: + throw new NoTmpDirFileException($this->getErrorMessage()); + case \UPLOAD_ERR_EXTENSION: + throw new ExtensionFileException($this->getErrorMessage()); + } + + throw new FileException($this->getErrorMessage()); + } + + /** + * Returns the maximum size of an uploaded file as configured in php.ini. + * + * @return int|float The maximum size of an uploaded file in bytes (returns float if size > PHP_INT_MAX) + */ + public static function getMaxFilesize(): int|float + { + $sizePostMax = self::parseFilesize(\ini_get('post_max_size')); + $sizeUploadMax = self::parseFilesize(\ini_get('upload_max_filesize')); + + return min($sizePostMax ?: \PHP_INT_MAX, $sizeUploadMax ?: \PHP_INT_MAX); + } + + private static function parseFilesize(string $size): int|float + { + if ('' === $size) { + return 0; + } + + $size = strtolower($size); + + $max = ltrim($size, '+'); + if (str_starts_with($max, '0x')) { + $max = \intval($max, 16); + } elseif (str_starts_with($max, '0')) { + $max = \intval($max, 8); + } else { + $max = (int) $max; + } + + switch (substr($size, -1)) { + case 't': $max *= 1024; + // no break + case 'g': $max *= 1024; + // no break + case 'm': $max *= 1024; + // no break + case 'k': $max *= 1024; + } + + return $max; + } + + /** + * Returns an informative upload error message. + */ + public function getErrorMessage(): string + { + static $errors = [ + \UPLOAD_ERR_INI_SIZE => 'The file "%s" exceeds your upload_max_filesize ini directive (limit is %d KiB).', + \UPLOAD_ERR_FORM_SIZE => 'The file "%s" exceeds the upload limit defined in your form.', + \UPLOAD_ERR_PARTIAL => 'The file "%s" was only partially uploaded.', + \UPLOAD_ERR_NO_FILE => 'No file was uploaded.', + \UPLOAD_ERR_CANT_WRITE => 'The file "%s" could not be written on disk.', + \UPLOAD_ERR_NO_TMP_DIR => 'File could not be uploaded: missing temporary directory.', + \UPLOAD_ERR_EXTENSION => 'File upload was stopped by a PHP extension.', + ]; + + $errorCode = $this->error; + $maxFilesize = \UPLOAD_ERR_INI_SIZE === $errorCode ? self::getMaxFilesize() / 1024 : 0; + $message = $errors[$errorCode] ?? 'The file "%s" was not uploaded due to an unknown error.'; + + return \sprintf($message, $this->getClientOriginalName(), $maxFilesize); + } +} diff --git a/3rdparty/symfony/http-foundation/FileBag.php b/3rdparty/symfony/http-foundation/FileBag.php new file mode 100644 index 00000000..b74a02e2 --- /dev/null +++ b/3rdparty/symfony/http-foundation/FileBag.php @@ -0,0 +1,136 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Component\HttpFoundation; + +use Symfony\Component\HttpFoundation\File\UploadedFile; + +/** + * FileBag is a container for uploaded files. + * + * @author Fabien Potencier + * @author Bulat Shakirzyanov + */ +class FileBag extends ParameterBag +{ + private const FILE_KEYS = ['error', 'name', 'size', 'tmp_name', 'type']; + + /** + * @param array|UploadedFile[] $parameters An array of HTTP files + */ + public function __construct(array $parameters = []) + { + $this->replace($parameters); + } + + /** + * @return void + */ + public function replace(array $files = []) + { + $this->parameters = []; + $this->add($files); + } + + /** + * @return void + */ + public function set(string $key, mixed $value) + { + if (!\is_array($value) && !$value instanceof UploadedFile) { + throw new \InvalidArgumentException('An uploaded file must be an array or an instance of UploadedFile.'); + } + + parent::set($key, $this->convertFileInformation($value)); + } + + /** + * @return void + */ + public function add(array $files = []) + { + foreach ($files as $key => $file) { + $this->set($key, $file); + } + } + + /** + * Converts uploaded files to UploadedFile instances. + * + * @return UploadedFile[]|UploadedFile|null + */ + protected function convertFileInformation(array|UploadedFile $file): array|UploadedFile|null + { + if ($file instanceof UploadedFile) { + return $file; + } + + $file = $this->fixPhpFilesArray($file); + $keys = array_keys($file); + sort($keys); + + if (self::FILE_KEYS == $keys) { + if (\UPLOAD_ERR_NO_FILE == $file['error']) { + $file = null; + } else { + $file = new UploadedFile($file['tmp_name'], $file['name'], $file['type'], $file['error'], false); + } + } else { + $file = array_map(fn ($v) => $v instanceof UploadedFile || \is_array($v) ? $this->convertFileInformation($v) : $v, $file); + if (array_keys($keys) === $keys) { + $file = array_filter($file); + } + } + + return $file; + } + + /** + * Fixes a malformed PHP $_FILES array. + * + * PHP has a bug that the format of the $_FILES array differs, depending on + * whether the uploaded file fields had normal field names or array-like + * field names ("normal" vs. "parent[child]"). + * + * This method fixes the array to look like the "normal" $_FILES array. + * + * It's safe to pass an already converted array, in which case this method + * just returns the original array unmodified. + */ + protected function fixPhpFilesArray(array $data): array + { + // Remove extra key added by PHP 8.1. + unset($data['full_path']); + $keys = array_keys($data); + sort($keys); + + if (self::FILE_KEYS != $keys || !isset($data['name']) || !\is_array($data['name'])) { + return $data; + } + + $files = $data; + foreach (self::FILE_KEYS as $k) { + unset($files[$k]); + } + + foreach ($data['name'] as $key => $name) { + $files[$key] = $this->fixPhpFilesArray([ + 'error' => $data['error'][$key], + 'name' => $name, + 'type' => $data['type'][$key], + 'tmp_name' => $data['tmp_name'][$key], + 'size' => $data['size'][$key], + ]); + } + + return $files; + } +} diff --git a/3rdparty/symfony/http-foundation/HeaderBag.php b/3rdparty/symfony/http-foundation/HeaderBag.php new file mode 100644 index 00000000..e8072add --- /dev/null +++ b/3rdparty/symfony/http-foundation/HeaderBag.php @@ -0,0 +1,290 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Component\HttpFoundation; + +/** + * HeaderBag is a container for HTTP headers. + * + * @author Fabien Potencier + * + * @implements \IteratorAggregate> + */ +class HeaderBag implements \IteratorAggregate, \Countable, \Stringable +{ + protected const UPPER = '_ABCDEFGHIJKLMNOPQRSTUVWXYZ'; + protected const LOWER = '-abcdefghijklmnopqrstuvwxyz'; + + /** + * @var array> + */ + protected $headers = []; + protected $cacheControl = []; + + public function __construct(array $headers = []) + { + foreach ($headers as $key => $values) { + $this->set($key, $values); + } + } + + /** + * Returns the headers as a string. + */ + public function __toString(): string + { + if (!$headers = $this->all()) { + return ''; + } + + ksort($headers); + $max = max(array_map('strlen', array_keys($headers))) + 1; + $content = ''; + foreach ($headers as $name => $values) { + $name = ucwords($name, '-'); + foreach ($values as $value) { + $content .= \sprintf("%-{$max}s %s\r\n", $name.':', $value); + } + } + + return $content; + } + + /** + * Returns the headers. + * + * @param string|null $key The name of the headers to return or null to get them all + * + * @return ($key is null ? array> : list) + */ + public function all(?string $key = null): array + { + if (null !== $key) { + return $this->headers[strtr($key, self::UPPER, self::LOWER)] ?? []; + } + + return $this->headers; + } + + /** + * Returns the parameter keys. + * + * @return string[] + */ + public function keys(): array + { + return array_keys($this->all()); + } + + /** + * Replaces the current HTTP headers by a new set. + * + * @return void + */ + public function replace(array $headers = []) + { + $this->headers = []; + $this->add($headers); + } + + /** + * Adds new headers the current HTTP headers set. + * + * @return void + */ + public function add(array $headers) + { + foreach ($headers as $key => $values) { + $this->set($key, $values); + } + } + + /** + * Returns the first header by name or the default one. + */ + public function get(string $key, ?string $default = null): ?string + { + $headers = $this->all($key); + + if (!$headers) { + return $default; + } + + if (null === $headers[0]) { + return null; + } + + return (string) $headers[0]; + } + + /** + * Sets a header by name. + * + * @param string|string[]|null $values The value or an array of values + * @param bool $replace Whether to replace the actual value or not (true by default) + * + * @return void + */ + public function set(string $key, string|array|null $values, bool $replace = true) + { + $key = strtr($key, self::UPPER, self::LOWER); + + if (\is_array($values)) { + $values = array_values($values); + + if (true === $replace || !isset($this->headers[$key])) { + $this->headers[$key] = $values; + } else { + $this->headers[$key] = array_merge($this->headers[$key], $values); + } + } else { + if (true === $replace || !isset($this->headers[$key])) { + $this->headers[$key] = [$values]; + } else { + $this->headers[$key][] = $values; + } + } + + if ('cache-control' === $key) { + $this->cacheControl = $this->parseCacheControl(implode(', ', $this->headers[$key])); + } + } + + /** + * Returns true if the HTTP header is defined. + */ + public function has(string $key): bool + { + return \array_key_exists(strtr($key, self::UPPER, self::LOWER), $this->all()); + } + + /** + * Returns true if the given HTTP header contains the given value. + */ + public function contains(string $key, string $value): bool + { + return \in_array($value, $this->all($key)); + } + + /** + * Removes a header. + * + * @return void + */ + public function remove(string $key) + { + $key = strtr($key, self::UPPER, self::LOWER); + + unset($this->headers[$key]); + + if ('cache-control' === $key) { + $this->cacheControl = []; + } + } + + /** + * Returns the HTTP header value converted to a date. + * + * @return \DateTimeImmutable|null + * + * @throws \RuntimeException When the HTTP header is not parseable + */ + public function getDate(string $key, ?\DateTimeInterface $default = null): ?\DateTimeInterface + { + if (null === $value = $this->get($key)) { + return null !== $default ? \DateTimeImmutable::createFromInterface($default) : null; + } + + if (false === $date = \DateTimeImmutable::createFromFormat(\DATE_RFC2822, $value)) { + throw new \RuntimeException(\sprintf('The "%s" HTTP header is not parseable (%s).', $key, $value)); + } + + return $date; + } + + /** + * Adds a custom Cache-Control directive. + * + * @return void + */ + public function addCacheControlDirective(string $key, bool|string $value = true) + { + $this->cacheControl[$key] = $value; + + $this->set('Cache-Control', $this->getCacheControlHeader()); + } + + /** + * Returns true if the Cache-Control directive is defined. + */ + public function hasCacheControlDirective(string $key): bool + { + return \array_key_exists($key, $this->cacheControl); + } + + /** + * Returns a Cache-Control directive value by name. + */ + public function getCacheControlDirective(string $key): bool|string|null + { + return $this->cacheControl[$key] ?? null; + } + + /** + * Removes a Cache-Control directive. + * + * @return void + */ + public function removeCacheControlDirective(string $key) + { + unset($this->cacheControl[$key]); + + $this->set('Cache-Control', $this->getCacheControlHeader()); + } + + /** + * Returns an iterator for headers. + * + * @return \ArrayIterator> + */ + public function getIterator(): \ArrayIterator + { + return new \ArrayIterator($this->headers); + } + + /** + * Returns the number of headers. + */ + public function count(): int + { + return \count($this->headers); + } + + /** + * @return string + */ + protected function getCacheControlHeader() + { + ksort($this->cacheControl); + + return HeaderUtils::toString($this->cacheControl, ','); + } + + /** + * Parses a Cache-Control HTTP header. + */ + protected function parseCacheControl(string $header): array + { + $parts = HeaderUtils::split($header, ',='); + + return HeaderUtils::combine($parts); + } +} diff --git a/3rdparty/symfony/http-foundation/HeaderUtils.php b/3rdparty/symfony/http-foundation/HeaderUtils.php new file mode 100644 index 00000000..ad47f220 --- /dev/null +++ b/3rdparty/symfony/http-foundation/HeaderUtils.php @@ -0,0 +1,298 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Component\HttpFoundation; + +/** + * HTTP header utility functions. + * + * @author Christian Schmidt + */ +class HeaderUtils +{ + public const DISPOSITION_ATTACHMENT = 'attachment'; + public const DISPOSITION_INLINE = 'inline'; + + /** + * This class should not be instantiated. + */ + private function __construct() + { + } + + /** + * Splits an HTTP header by one or more separators. + * + * Example: + * + * HeaderUtils::split('da, en-gb;q=0.8', ',;') + * // => ['da'], ['en-gb', 'q=0.8']] + * + * @param string $separators List of characters to split on, ordered by + * precedence, e.g. ',', ';=', or ',;=' + * + * @return array Nested array with as many levels as there are characters in + * $separators + */ + public static function split(string $header, string $separators): array + { + if ('' === $separators) { + throw new \InvalidArgumentException('At least one separator must be specified.'); + } + + $quotedSeparators = preg_quote($separators, '/'); + + preg_match_all(' + / + (?!\s) + (?: + # quoted-string + "(?:[^"\\\\]|\\\\.)*(?:"|\\\\|$) + | + # token + [^"'.$quotedSeparators.']+ + )+ + (?['.$quotedSeparators.']) + \s* + /x', trim($header), $matches, \PREG_SET_ORDER); + + return self::groupParts($matches, $separators); + } + + /** + * Combines an array of arrays into one associative array. + * + * Each of the nested arrays should have one or two elements. The first + * value will be used as the keys in the associative array, and the second + * will be used as the values, or true if the nested array only contains one + * element. Array keys are lowercased. + * + * Example: + * + * HeaderUtils::combine([['foo', 'abc'], ['bar']]) + * // => ['foo' => 'abc', 'bar' => true] + */ + public static function combine(array $parts): array + { + $assoc = []; + foreach ($parts as $part) { + $name = strtolower($part[0]); + $value = $part[1] ?? true; + $assoc[$name] = $value; + } + + return $assoc; + } + + /** + * Joins an associative array into a string for use in an HTTP header. + * + * The key and value of each entry are joined with '=', and all entries + * are joined with the specified separator and an additional space (for + * readability). Values are quoted if necessary. + * + * Example: + * + * HeaderUtils::toString(['foo' => 'abc', 'bar' => true, 'baz' => 'a b c'], ',') + * // => 'foo=abc, bar, baz="a b c"' + */ + public static function toString(array $assoc, string $separator): string + { + $parts = []; + foreach ($assoc as $name => $value) { + if (true === $value) { + $parts[] = $name; + } else { + $parts[] = $name.'='.self::quote($value); + } + } + + return implode($separator.' ', $parts); + } + + /** + * Encodes a string as a quoted string, if necessary. + * + * If a string contains characters not allowed by the "token" construct in + * the HTTP specification, it is backslash-escaped and enclosed in quotes + * to match the "quoted-string" construct. + */ + public static function quote(string $s): string + { + if (preg_match('/^[a-z0-9!#$%&\'*.^_`|~-]+$/i', $s)) { + return $s; + } + + return '"'.addcslashes($s, '"\\"').'"'; + } + + /** + * Decodes a quoted string. + * + * If passed an unquoted string that matches the "token" construct (as + * defined in the HTTP specification), it is passed through verbatim. + */ + public static function unquote(string $s): string + { + return preg_replace('/\\\\(.)|"/', '$1', $s); + } + + /** + * Generates an HTTP Content-Disposition field-value. + * + * @param string $disposition One of "inline" or "attachment" + * @param string $filename A unicode string + * @param string $filenameFallback A string containing only ASCII characters that + * is semantically equivalent to $filename. If the filename is already ASCII, + * it can be omitted, or just copied from $filename + * + * @throws \InvalidArgumentException + * + * @see RFC 6266 + */ + public static function makeDisposition(string $disposition, string $filename, string $filenameFallback = ''): string + { + if (!\in_array($disposition, [self::DISPOSITION_ATTACHMENT, self::DISPOSITION_INLINE])) { + throw new \InvalidArgumentException(\sprintf('The disposition must be either "%s" or "%s".', self::DISPOSITION_ATTACHMENT, self::DISPOSITION_INLINE)); + } + + if ('' === $filenameFallback) { + $filenameFallback = $filename; + } + + // filenameFallback is not ASCII. + if (!preg_match('/^[\x20-\x7e]*$/', $filenameFallback)) { + throw new \InvalidArgumentException('The filename fallback must only contain ASCII characters.'); + } + + // percent characters aren't safe in fallback. + if (str_contains($filenameFallback, '%')) { + throw new \InvalidArgumentException('The filename fallback cannot contain the "%" character.'); + } + + // path separators aren't allowed in either. + if (str_contains($filename, '/') || str_contains($filename, '\\') || str_contains($filenameFallback, '/') || str_contains($filenameFallback, '\\')) { + throw new \InvalidArgumentException('The filename and the fallback cannot contain the "/" and "\\" characters.'); + } + + $params = ['filename' => $filenameFallback]; + if ($filename !== $filenameFallback) { + $params['filename*'] = "utf-8''".rawurlencode($filename); + } + + return $disposition.'; '.self::toString($params, ';'); + } + + /** + * Like parse_str(), but preserves dots in variable names. + */ + public static function parseQuery(string $query, bool $ignoreBrackets = false, string $separator = '&'): array + { + $q = []; + + foreach (explode($separator, $query) as $v) { + if (false !== $i = strpos($v, "\0")) { + $v = substr($v, 0, $i); + } + + if (false === $i = strpos($v, '=')) { + $k = urldecode($v); + $v = ''; + } else { + $k = urldecode(substr($v, 0, $i)); + $v = substr($v, $i); + } + + if (false !== $i = strpos($k, "\0")) { + $k = substr($k, 0, $i); + } + + $k = ltrim($k, ' '); + + if ($ignoreBrackets) { + $q[$k][] = urldecode(substr($v, 1)); + + continue; + } + + if (false === $i = strpos($k, '[')) { + $q[] = bin2hex($k).$v; + } else { + $q[] = bin2hex(substr($k, 0, $i)).rawurlencode(substr($k, $i)).$v; + } + } + + if ($ignoreBrackets) { + return $q; + } + + parse_str(implode('&', $q), $q); + + $query = []; + + foreach ($q as $k => $v) { + if (false !== $i = strpos($k, '_')) { + $query[substr_replace($k, hex2bin(substr($k, 0, $i)).'[', 0, 1 + $i)] = $v; + } else { + $query[hex2bin($k)] = $v; + } + } + + return $query; + } + + private static function groupParts(array $matches, string $separators, bool $first = true): array + { + $separator = $separators[0]; + $separators = substr($separators, 1) ?: ''; + $i = 0; + + if ('' === $separators && !$first) { + $parts = ['']; + + foreach ($matches as $match) { + if (!$i && isset($match['separator'])) { + $i = 1; + $parts[1] = ''; + } else { + $parts[$i] .= self::unquote($match[0]); + } + } + + return $parts; + } + + $parts = []; + $partMatches = []; + + foreach ($matches as $match) { + if (($match['separator'] ?? null) === $separator) { + ++$i; + } else { + $partMatches[$i][] = $match; + } + } + + foreach ($partMatches as $matches) { + if ('' === $separators && '' !== $unquoted = self::unquote($matches[0][0])) { + $parts[] = $unquoted; + } elseif ($groupedParts = self::groupParts($matches, $separators, false)) { + $parts[] = $groupedParts; + } + } + + return $parts; + } +} diff --git a/3rdparty/symfony/http-foundation/InputBag.php b/3rdparty/symfony/http-foundation/InputBag.php new file mode 100644 index 00000000..08b92757 --- /dev/null +++ b/3rdparty/symfony/http-foundation/InputBag.php @@ -0,0 +1,140 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Component\HttpFoundation; + +use Symfony\Component\HttpFoundation\Exception\BadRequestException; +use Symfony\Component\HttpFoundation\Exception\UnexpectedValueException; + +/** + * InputBag is a container for user input values such as $_GET, $_POST, $_REQUEST, and $_COOKIE. + * + * @author Saif Eddin Gmati + */ +final class InputBag extends ParameterBag +{ + /** + * Returns a scalar input value by name. + * + * @param string|int|float|bool|null $default The default value if the input key does not exist + */ + public function get(string $key, mixed $default = null): string|int|float|bool|null + { + if (null !== $default && !\is_scalar($default) && !$default instanceof \Stringable) { + throw new \InvalidArgumentException(\sprintf('Expected a scalar value as a 2nd argument to "%s()", "%s" given.', __METHOD__, get_debug_type($default))); + } + + $value = parent::get($key, $this); + + if (null !== $value && $this !== $value && !\is_scalar($value) && !$value instanceof \Stringable) { + throw new BadRequestException(\sprintf('Input value "%s" contains a non-scalar value.', $key)); + } + + return $this === $value ? $default : $value; + } + + /** + * Replaces the current input values by a new set. + */ + public function replace(array $inputs = []): void + { + $this->parameters = []; + $this->add($inputs); + } + + /** + * Adds input values. + */ + public function add(array $inputs = []): void + { + foreach ($inputs as $input => $value) { + $this->set($input, $value); + } + } + + /** + * Sets an input by name. + * + * @param string|int|float|bool|array|null $value + */ + public function set(string $key, mixed $value): void + { + if (null !== $value && !\is_scalar($value) && !\is_array($value) && !$value instanceof \Stringable) { + throw new \InvalidArgumentException(\sprintf('Expected a scalar, or an array as a 2nd argument to "%s()", "%s" given.', __METHOD__, get_debug_type($value))); + } + + $this->parameters[$key] = $value; + } + + /** + * Returns the parameter value converted to an enum. + * + * @template T of \BackedEnum + * + * @param class-string $class + * @param ?T $default + * + * @return ?T + */ + public function getEnum(string $key, string $class, ?\BackedEnum $default = null): ?\BackedEnum + { + try { + return parent::getEnum($key, $class, $default); + } catch (UnexpectedValueException $e) { + throw new BadRequestException($e->getMessage(), $e->getCode(), $e); + } + } + + /** + * Returns the parameter value converted to string. + */ + public function getString(string $key, string $default = ''): string + { + // Shortcuts the parent method because the validation on scalar is already done in get(). + return (string) $this->get($key, $default); + } + + public function filter(string $key, mixed $default = null, int $filter = \FILTER_DEFAULT, mixed $options = []): mixed + { + $value = $this->has($key) ? $this->all()[$key] : $default; + + // Always turn $options into an array - this allows filter_var option shortcuts. + if (!\is_array($options) && $options) { + $options = ['flags' => $options]; + } + + if (\is_array($value) && !(($options['flags'] ?? 0) & (\FILTER_REQUIRE_ARRAY | \FILTER_FORCE_ARRAY))) { + throw new BadRequestException(\sprintf('Input value "%s" contains an array, but "FILTER_REQUIRE_ARRAY" or "FILTER_FORCE_ARRAY" flags were not set.', $key)); + } + + if ((\FILTER_CALLBACK & $filter) && !(($options['options'] ?? null) instanceof \Closure)) { + throw new \InvalidArgumentException(\sprintf('A Closure must be passed to "%s()" when FILTER_CALLBACK is used, "%s" given.', __METHOD__, get_debug_type($options['options'] ?? null))); + } + + $options['flags'] ??= 0; + $nullOnFailure = $options['flags'] & \FILTER_NULL_ON_FAILURE; + $options['flags'] |= \FILTER_NULL_ON_FAILURE; + + $value = filter_var($value, $filter, $options); + + if (null !== $value || $nullOnFailure) { + return $value; + } + + $method = debug_backtrace(\DEBUG_BACKTRACE_IGNORE_ARGS | \DEBUG_BACKTRACE_PROVIDE_OBJECT, 2)[1]; + $method = ($method['object'] ?? null) === $this ? $method['function'] : 'filter'; + $hint = 'filter' === $method ? 'pass' : 'use method "filter()" with'; + + trigger_deprecation('symfony/http-foundation', '6.3', 'Ignoring invalid values when using "%s::%s(\'%s\')" is deprecated and will throw a "%s" in 7.0; '.$hint.' flag "FILTER_NULL_ON_FAILURE" to keep ignoring them.', $this::class, $method, $key, BadRequestException::class); + + return false; + } +} diff --git a/3rdparty/symfony/http-foundation/IpUtils.php b/3rdparty/symfony/http-foundation/IpUtils.php new file mode 100644 index 00000000..8b52d2a9 --- /dev/null +++ b/3rdparty/symfony/http-foundation/IpUtils.php @@ -0,0 +1,251 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Component\HttpFoundation; + +/** + * Http utility functions. + * + * @author Fabien Potencier + */ +class IpUtils +{ + public const PRIVATE_SUBNETS = [ + '127.0.0.0/8', // RFC1700 (Loopback) + '10.0.0.0/8', // RFC1918 + '192.168.0.0/16', // RFC1918 + '172.16.0.0/12', // RFC1918 + '169.254.0.0/16', // RFC3927 + '0.0.0.0/8', // RFC5735 + '240.0.0.0/4', // RFC1112 + '::1/128', // Loopback + 'fc00::/7', // Unique Local Address + 'fe80::/10', // Link Local Address + '::ffff:0:0/96', // IPv4 translations + '::/128', // Unspecified address + ]; + + private static array $checkedIps = []; + + /** + * This class should not be instantiated. + */ + private function __construct() + { + } + + /** + * Checks if an IPv4 or IPv6 address is contained in the list of given IPs or subnets. + * + * @param string|array $ips List of IPs or subnets (can be a string if only a single one) + */ + public static function checkIp(string $requestIp, string|array $ips): bool + { + if (!\is_array($ips)) { + $ips = [$ips]; + } + + $method = substr_count($requestIp, ':') > 1 ? 'checkIp6' : 'checkIp4'; + + foreach ($ips as $ip) { + if (self::$method($requestIp, $ip)) { + return true; + } + } + + return false; + } + + /** + * Compares two IPv4 addresses. + * In case a subnet is given, it checks if it contains the request IP. + * + * @param string $ip IPv4 address or subnet in CIDR notation + * + * @return bool Whether the request IP matches the IP, or whether the request IP is within the CIDR subnet + */ + public static function checkIp4(string $requestIp, string $ip): bool + { + $cacheKey = $requestIp.'-'.$ip.'-v4'; + if (null !== $cacheValue = self::getCacheResult($cacheKey)) { + return $cacheValue; + } + + if (!filter_var($requestIp, \FILTER_VALIDATE_IP, \FILTER_FLAG_IPV4)) { + return self::setCacheResult($cacheKey, false); + } + + if (str_contains($ip, '/')) { + [$address, $netmask] = explode('/', $ip, 2); + + if ('0' === $netmask) { + return self::setCacheResult($cacheKey, false !== filter_var($address, \FILTER_VALIDATE_IP, \FILTER_FLAG_IPV4)); + } + + if ($netmask < 0 || $netmask > 32) { + return self::setCacheResult($cacheKey, false); + } + } else { + $address = $ip; + $netmask = 32; + } + + if (false === ip2long($address)) { + return self::setCacheResult($cacheKey, false); + } + + return self::setCacheResult($cacheKey, 0 === substr_compare(\sprintf('%032b', ip2long($requestIp)), \sprintf('%032b', ip2long($address)), 0, $netmask)); + } + + /** + * Compares two IPv6 addresses. + * In case a subnet is given, it checks if it contains the request IP. + * + * @author David Soria Parra + * + * @see https://github.com/dsp/v6tools + * + * @param string $ip IPv6 address or subnet in CIDR notation + * + * @throws \RuntimeException When IPV6 support is not enabled + */ + public static function checkIp6(string $requestIp, string $ip): bool + { + $cacheKey = $requestIp.'-'.$ip.'-v6'; + if (null !== $cacheValue = self::getCacheResult($cacheKey)) { + return $cacheValue; + } + + if (!((\extension_loaded('sockets') && \defined('AF_INET6')) || @inet_pton('::1'))) { + throw new \RuntimeException('Unable to check Ipv6. Check that PHP was not compiled with option "disable-ipv6".'); + } + + // Check to see if we were given a IP4 $requestIp or $ip by mistake + if (!filter_var($requestIp, \FILTER_VALIDATE_IP, \FILTER_FLAG_IPV6)) { + return self::setCacheResult($cacheKey, false); + } + + if (str_contains($ip, '/')) { + [$address, $netmask] = explode('/', $ip, 2); + + if (!filter_var($address, \FILTER_VALIDATE_IP, \FILTER_FLAG_IPV6)) { + return self::setCacheResult($cacheKey, false); + } + + if ('0' === $netmask) { + return (bool) unpack('n*', @inet_pton($address)); + } + + if ($netmask < 1 || $netmask > 128) { + return self::setCacheResult($cacheKey, false); + } + } else { + if (!filter_var($ip, \FILTER_VALIDATE_IP, \FILTER_FLAG_IPV6)) { + return self::setCacheResult($cacheKey, false); + } + + $address = $ip; + $netmask = 128; + } + + $bytesAddr = unpack('n*', @inet_pton($address)); + $bytesTest = unpack('n*', @inet_pton($requestIp)); + + if (!$bytesAddr || !$bytesTest) { + return self::setCacheResult($cacheKey, false); + } + + for ($i = 1, $ceil = ceil($netmask / 16); $i <= $ceil; ++$i) { + $left = $netmask - 16 * ($i - 1); + $left = ($left <= 16) ? $left : 16; + $mask = ~(0xFFFF >> $left) & 0xFFFF; + if (($bytesAddr[$i] & $mask) != ($bytesTest[$i] & $mask)) { + return self::setCacheResult($cacheKey, false); + } + } + + return self::setCacheResult($cacheKey, true); + } + + /** + * Anonymizes an IP/IPv6. + * + * Removes the last byte for v4 and the last 8 bytes for v6 IPs + */ + public static function anonymize(string $ip): string + { + /* + * If the IP contains a % symbol, then it is a local-link address with scoping according to RFC 4007 + * In that case, we only care about the part before the % symbol, as the following functions, can only work with + * the IP address itself. As the scope can leak information (containing interface name), we do not want to + * include it in our anonymized IP data. + */ + if (str_contains($ip, '%')) { + $ip = substr($ip, 0, strpos($ip, '%')); + } + + $wrappedIPv6 = false; + if (str_starts_with($ip, '[') && str_ends_with($ip, ']')) { + $wrappedIPv6 = true; + $ip = substr($ip, 1, -1); + } + + $packedAddress = inet_pton($ip); + if (4 === \strlen($packedAddress)) { + $mask = '255.255.255.0'; + } elseif ($ip === inet_ntop($packedAddress & inet_pton('::ffff:ffff:ffff'))) { + $mask = '::ffff:ffff:ff00'; + } elseif ($ip === inet_ntop($packedAddress & inet_pton('::ffff:ffff'))) { + $mask = '::ffff:ff00'; + } else { + $mask = 'ffff:ffff:ffff:ffff:0000:0000:0000:0000'; + } + $ip = inet_ntop($packedAddress & inet_pton($mask)); + + if ($wrappedIPv6) { + $ip = '['.$ip.']'; + } + + return $ip; + } + + /** + * Checks if an IPv4 or IPv6 address is contained in the list of private IP subnets. + */ + public static function isPrivateIp(string $requestIp): bool + { + return self::checkIp($requestIp, self::PRIVATE_SUBNETS); + } + + private static function getCacheResult(string $cacheKey): ?bool + { + if (isset(self::$checkedIps[$cacheKey])) { + // Move the item last in cache (LRU) + $value = self::$checkedIps[$cacheKey]; + unset(self::$checkedIps[$cacheKey]); + self::$checkedIps[$cacheKey] = $value; + + return self::$checkedIps[$cacheKey]; + } + + return null; + } + + private static function setCacheResult(string $cacheKey, bool $result): bool + { + if (1000 < \count(self::$checkedIps)) { + // stop memory leak if there are many keys + self::$checkedIps = \array_slice(self::$checkedIps, 500, null, true); + } + + return self::$checkedIps[$cacheKey] = $result; + } +} diff --git a/3rdparty/symfony/http-foundation/JsonResponse.php b/3rdparty/symfony/http-foundation/JsonResponse.php new file mode 100644 index 00000000..616bccfe --- /dev/null +++ b/3rdparty/symfony/http-foundation/JsonResponse.php @@ -0,0 +1,190 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Component\HttpFoundation; + +/** + * Response represents an HTTP response in JSON format. + * + * Note that this class does not force the returned JSON content to be an + * object. It is however recommended that you do return an object as it + * protects yourself against XSSI and JSON-JavaScript Hijacking. + * + * @see https://github.com/OWASP/CheatSheetSeries/blob/master/cheatsheets/AJAX_Security_Cheat_Sheet.md#always-return-json-with-an-object-on-the-outside + * + * @author Igor Wiedler + */ +class JsonResponse extends Response +{ + protected $data; + protected $callback; + + // Encode <, >, ', &, and " characters in the JSON, making it also safe to be embedded into HTML. + // 15 === JSON_HEX_TAG | JSON_HEX_APOS | JSON_HEX_AMP | JSON_HEX_QUOT + public const DEFAULT_ENCODING_OPTIONS = 15; + + protected $encodingOptions = self::DEFAULT_ENCODING_OPTIONS; + + /** + * @param bool $json If the data is already a JSON string + */ + public function __construct(mixed $data = null, int $status = 200, array $headers = [], bool $json = false) + { + parent::__construct('', $status, $headers); + + if ($json && !\is_string($data) && !is_numeric($data) && !\is_callable([$data, '__toString'])) { + throw new \TypeError(\sprintf('"%s": If $json is set to true, argument $data must be a string or object implementing __toString(), "%s" given.', __METHOD__, get_debug_type($data))); + } + + $data ??= new \ArrayObject(); + + $json ? $this->setJson($data) : $this->setData($data); + } + + /** + * Factory method for chainability. + * + * Example: + * + * return JsonResponse::fromJsonString('{"key": "value"}') + * ->setSharedMaxAge(300); + * + * @param string $data The JSON response string + * @param int $status The response status code (200 "OK" by default) + * @param array $headers An array of response headers + */ + public static function fromJsonString(string $data, int $status = 200, array $headers = []): static + { + return new static($data, $status, $headers, true); + } + + /** + * Sets the JSONP callback. + * + * @param string|null $callback The JSONP callback or null to use none + * + * @return $this + * + * @throws \InvalidArgumentException When the callback name is not valid + */ + public function setCallback(?string $callback = null): static + { + if (1 > \func_num_args()) { + trigger_deprecation('symfony/http-foundation', '6.2', 'Calling "%s()" without any arguments is deprecated, pass null explicitly instead.', __METHOD__); + } + if (null !== $callback) { + // partially taken from https://geekality.net/2011/08/03/valid-javascript-identifier/ + // partially taken from https://github.com/willdurand/JsonpCallbackValidator + // JsonpCallbackValidator is released under the MIT License. See https://github.com/willdurand/JsonpCallbackValidator/blob/v1.1.0/LICENSE for details. + // (c) William Durand + $pattern = '/^[$_\p{L}][$_\p{L}\p{Mn}\p{Mc}\p{Nd}\p{Pc}\x{200C}\x{200D}]*(?:\[(?:"(?:\\\.|[^"\\\])*"|\'(?:\\\.|[^\'\\\])*\'|\d+)\])*?$/u'; + $reserved = [ + 'break', 'do', 'instanceof', 'typeof', 'case', 'else', 'new', 'var', 'catch', 'finally', 'return', 'void', 'continue', 'for', 'switch', 'while', + 'debugger', 'function', 'this', 'with', 'default', 'if', 'throw', 'delete', 'in', 'try', 'class', 'enum', 'extends', 'super', 'const', 'export', + 'import', 'implements', 'let', 'private', 'public', 'yield', 'interface', 'package', 'protected', 'static', 'null', 'true', 'false', + ]; + $parts = explode('.', $callback); + foreach ($parts as $part) { + if (!preg_match($pattern, $part) || \in_array($part, $reserved, true)) { + throw new \InvalidArgumentException('The callback name is not valid.'); + } + } + } + + $this->callback = $callback; + + return $this->update(); + } + + /** + * Sets a raw string containing a JSON document to be sent. + * + * @return $this + */ + public function setJson(string $json): static + { + $this->data = $json; + + return $this->update(); + } + + /** + * Sets the data to be sent as JSON. + * + * @return $this + * + * @throws \InvalidArgumentException + */ + public function setData(mixed $data = []): static + { + try { + $data = json_encode($data, $this->encodingOptions); + } catch (\Exception $e) { + if ('Exception' === $e::class && str_starts_with($e->getMessage(), 'Failed calling ')) { + throw $e->getPrevious() ?: $e; + } + throw $e; + } + + if (\JSON_THROW_ON_ERROR & $this->encodingOptions) { + return $this->setJson($data); + } + + if (\JSON_ERROR_NONE !== json_last_error()) { + throw new \InvalidArgumentException(json_last_error_msg()); + } + + return $this->setJson($data); + } + + /** + * Returns options used while encoding data to JSON. + */ + public function getEncodingOptions(): int + { + return $this->encodingOptions; + } + + /** + * Sets options used while encoding data to JSON. + * + * @return $this + */ + public function setEncodingOptions(int $encodingOptions): static + { + $this->encodingOptions = $encodingOptions; + + return $this->setData(json_decode($this->data)); + } + + /** + * Updates the content and headers according to the JSON data and callback. + * + * @return $this + */ + protected function update(): static + { + if (null !== $this->callback) { + // Not using application/javascript for compatibility reasons with older browsers. + $this->headers->set('Content-Type', 'text/javascript'); + + return $this->setContent(\sprintf('/**/%s(%s);', $this->callback, $this->data)); + } + + // Only set the header when there is none or when it equals 'text/javascript' (from a previous update with callback) + // in order to not overwrite a custom definition. + if (!$this->headers->has('Content-Type') || 'text/javascript' === $this->headers->get('Content-Type')) { + $this->headers->set('Content-Type', 'application/json'); + } + + return $this->setContent($this->data); + } +} diff --git a/3rdparty/symfony/http-foundation/LICENSE b/3rdparty/symfony/http-foundation/LICENSE new file mode 100644 index 00000000..0138f8f0 --- /dev/null +++ b/3rdparty/symfony/http-foundation/LICENSE @@ -0,0 +1,19 @@ +Copyright (c) 2004-present Fabien Potencier + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is furnished +to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +THE SOFTWARE. diff --git a/3rdparty/symfony/http-foundation/ParameterBag.php b/3rdparty/symfony/http-foundation/ParameterBag.php new file mode 100644 index 00000000..2bd8cb15 --- /dev/null +++ b/3rdparty/symfony/http-foundation/ParameterBag.php @@ -0,0 +1,258 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Component\HttpFoundation; + +use Symfony\Component\HttpFoundation\Exception\BadRequestException; +use Symfony\Component\HttpFoundation\Exception\UnexpectedValueException; + +/** + * ParameterBag is a container for key/value pairs. + * + * @author Fabien Potencier + * + * @implements \IteratorAggregate + */ +class ParameterBag implements \IteratorAggregate, \Countable +{ + /** + * Parameter storage. + */ + protected $parameters; + + public function __construct(array $parameters = []) + { + $this->parameters = $parameters; + } + + /** + * Returns the parameters. + * + * @param string|null $key The name of the parameter to return or null to get them all + */ + public function all(?string $key = null): array + { + if (null === $key) { + return $this->parameters; + } + + if (!\is_array($value = $this->parameters[$key] ?? [])) { + throw new BadRequestException(\sprintf('Unexpected value for parameter "%s": expecting "array", got "%s".', $key, get_debug_type($value))); + } + + return $value; + } + + /** + * Returns the parameter keys. + */ + public function keys(): array + { + return array_keys($this->parameters); + } + + /** + * Replaces the current parameters by a new set. + * + * @return void + */ + public function replace(array $parameters = []) + { + $this->parameters = $parameters; + } + + /** + * Adds parameters. + * + * @return void + */ + public function add(array $parameters = []) + { + $this->parameters = array_replace($this->parameters, $parameters); + } + + public function get(string $key, mixed $default = null): mixed + { + return \array_key_exists($key, $this->parameters) ? $this->parameters[$key] : $default; + } + + /** + * @return void + */ + public function set(string $key, mixed $value) + { + $this->parameters[$key] = $value; + } + + /** + * Returns true if the parameter is defined. + */ + public function has(string $key): bool + { + return \array_key_exists($key, $this->parameters); + } + + /** + * Removes a parameter. + * + * @return void + */ + public function remove(string $key) + { + unset($this->parameters[$key]); + } + + /** + * Returns the alphabetic characters of the parameter value. + */ + public function getAlpha(string $key, string $default = ''): string + { + return preg_replace('/[^[:alpha:]]/', '', $this->getString($key, $default)); + } + + /** + * Returns the alphabetic characters and digits of the parameter value. + */ + public function getAlnum(string $key, string $default = ''): string + { + return preg_replace('/[^[:alnum:]]/', '', $this->getString($key, $default)); + } + + /** + * Returns the digits of the parameter value. + */ + public function getDigits(string $key, string $default = ''): string + { + return preg_replace('/[^[:digit:]]/', '', $this->getString($key, $default)); + } + + /** + * Returns the parameter as string. + */ + public function getString(string $key, string $default = ''): string + { + $value = $this->get($key, $default); + if (!\is_scalar($value) && !$value instanceof \Stringable) { + throw new UnexpectedValueException(\sprintf('Parameter value "%s" cannot be converted to "string".', $key)); + } + + return (string) $value; + } + + /** + * Returns the parameter value converted to integer. + */ + public function getInt(string $key, int $default = 0): int + { + // In 7.0 remove the fallback to 0, in case of failure an exception will be thrown + return $this->filter($key, $default, \FILTER_VALIDATE_INT, ['flags' => \FILTER_REQUIRE_SCALAR]) ?: 0; + } + + /** + * Returns the parameter value converted to boolean. + */ + public function getBoolean(string $key, bool $default = false): bool + { + return $this->filter($key, $default, \FILTER_VALIDATE_BOOL, ['flags' => \FILTER_REQUIRE_SCALAR]); + } + + /** + * Returns the parameter value converted to an enum. + * + * @template T of \BackedEnum + * + * @param class-string $class + * @param ?T $default + * + * @return ?T + */ + public function getEnum(string $key, string $class, ?\BackedEnum $default = null): ?\BackedEnum + { + $value = $this->get($key); + + if (null === $value) { + return $default; + } + + try { + return $class::from($value); + } catch (\ValueError|\TypeError $e) { + throw new UnexpectedValueException(\sprintf('Parameter "%s" cannot be converted to enum: %s.', $key, $e->getMessage()), $e->getCode(), $e); + } + } + + /** + * Filter key. + * + * @param int $filter FILTER_* constant + * @param int|array{flags?: int, options?: array} $options Flags from FILTER_* constants + * + * @see https://php.net/filter-var + */ + public function filter(string $key, mixed $default = null, int $filter = \FILTER_DEFAULT, mixed $options = []): mixed + { + $value = $this->get($key, $default); + + // Always turn $options into an array - this allows filter_var option shortcuts. + if (!\is_array($options) && $options) { + $options = ['flags' => $options]; + } + + // Add a convenience check for arrays. + if (\is_array($value) && !isset($options['flags'])) { + $options['flags'] = \FILTER_REQUIRE_ARRAY; + } + + if (\is_object($value) && !$value instanceof \Stringable) { + throw new UnexpectedValueException(\sprintf('Parameter value "%s" cannot be filtered.', $key)); + } + + if ((\FILTER_CALLBACK & $filter) && !(($options['options'] ?? null) instanceof \Closure)) { + throw new \InvalidArgumentException(\sprintf('A Closure must be passed to "%s()" when FILTER_CALLBACK is used, "%s" given.', __METHOD__, get_debug_type($options['options'] ?? null))); + } + + $options['flags'] ??= 0; + $nullOnFailure = $options['flags'] & \FILTER_NULL_ON_FAILURE; + $options['flags'] |= \FILTER_NULL_ON_FAILURE; + + $value = filter_var($value, $filter, $options); + + if (null !== $value || $nullOnFailure) { + return $value; + } + + $method = debug_backtrace(\DEBUG_BACKTRACE_IGNORE_ARGS | \DEBUG_BACKTRACE_PROVIDE_OBJECT, 2)[1]; + $method = ($method['object'] ?? null) === $this ? $method['function'] : 'filter'; + $hint = 'filter' === $method ? 'pass' : 'use method "filter()" with'; + + trigger_deprecation('symfony/http-foundation', '6.3', 'Ignoring invalid values when using "%s::%s(\'%s\')" is deprecated and will throw an "%s" in 7.0; '.$hint.' flag "FILTER_NULL_ON_FAILURE" to keep ignoring them.', $this::class, $method, $key, UnexpectedValueException::class); + + return false; + } + + /** + * Returns an iterator for parameters. + * + * @return \ArrayIterator + */ + public function getIterator(): \ArrayIterator + { + return new \ArrayIterator($this->parameters); + } + + /** + * Returns the number of parameters. + */ + public function count(): int + { + return \count($this->parameters); + } +} diff --git a/3rdparty/symfony/http-foundation/RateLimiter/AbstractRequestRateLimiter.php b/3rdparty/symfony/http-foundation/RateLimiter/AbstractRequestRateLimiter.php new file mode 100644 index 00000000..550090f9 --- /dev/null +++ b/3rdparty/symfony/http-foundation/RateLimiter/AbstractRequestRateLimiter.php @@ -0,0 +1,81 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Component\HttpFoundation\RateLimiter; + +use Symfony\Component\HttpFoundation\Request; +use Symfony\Component\RateLimiter\LimiterInterface; +use Symfony\Component\RateLimiter\Policy\NoLimiter; +use Symfony\Component\RateLimiter\RateLimit; + +/** + * An implementation of PeekableRequestRateLimiterInterface that + * fits most use-cases. + * + * @author Wouter de Jong + */ +abstract class AbstractRequestRateLimiter implements PeekableRequestRateLimiterInterface +{ + public function consume(Request $request): RateLimit + { + return $this->doConsume($request, 1); + } + + public function peek(Request $request): RateLimit + { + return $this->doConsume($request, 0); + } + + private function doConsume(Request $request, int $tokens): RateLimit + { + $limiters = $this->getLimiters($request); + if (0 === \count($limiters)) { + $limiters = [new NoLimiter()]; + } + + $minimalRateLimit = null; + foreach ($limiters as $limiter) { + $rateLimit = $limiter->consume($tokens); + + $minimalRateLimit = $minimalRateLimit ? self::getMinimalRateLimit($minimalRateLimit, $rateLimit) : $rateLimit; + } + + return $minimalRateLimit; + } + + public function reset(Request $request): void + { + foreach ($this->getLimiters($request) as $limiter) { + $limiter->reset(); + } + } + + /** + * @return LimiterInterface[] a set of limiters using keys extracted from the request + */ + abstract protected function getLimiters(Request $request): array; + + private static function getMinimalRateLimit(RateLimit $first, RateLimit $second): RateLimit + { + if ($first->isAccepted() !== $second->isAccepted()) { + return $first->isAccepted() ? $second : $first; + } + + $firstRemainingTokens = $first->getRemainingTokens(); + $secondRemainingTokens = $second->getRemainingTokens(); + + if ($firstRemainingTokens === $secondRemainingTokens) { + return $first->getRetryAfter() < $second->getRetryAfter() ? $second : $first; + } + + return $firstRemainingTokens > $secondRemainingTokens ? $second : $first; + } +} diff --git a/3rdparty/symfony/http-foundation/RateLimiter/PeekableRequestRateLimiterInterface.php b/3rdparty/symfony/http-foundation/RateLimiter/PeekableRequestRateLimiterInterface.php new file mode 100644 index 00000000..63471af2 --- /dev/null +++ b/3rdparty/symfony/http-foundation/RateLimiter/PeekableRequestRateLimiterInterface.php @@ -0,0 +1,35 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Component\HttpFoundation\RateLimiter; + +use Symfony\Component\HttpFoundation\Request; +use Symfony\Component\RateLimiter\RateLimit; + +/** + * A request limiter which allows peeking ahead. + * + * This is valuable to reduce the cache backend load in scenarios + * like a login when we only want to consume a token on login failure, + * and where the majority of requests will be successful and thus not + * need to consume a token. + * + * This way we can peek ahead before allowing the request through, and + * only consume if the request failed (1 backend op). This is compared + * to always consuming and then resetting the limit if the request + * is successful (2 backend ops). + * + * @author Jordi Boggiano + */ +interface PeekableRequestRateLimiterInterface extends RequestRateLimiterInterface +{ + public function peek(Request $request): RateLimit; +} diff --git a/3rdparty/symfony/http-foundation/RateLimiter/RequestRateLimiterInterface.php b/3rdparty/symfony/http-foundation/RateLimiter/RequestRateLimiterInterface.php new file mode 100644 index 00000000..4c87a40a --- /dev/null +++ b/3rdparty/symfony/http-foundation/RateLimiter/RequestRateLimiterInterface.php @@ -0,0 +1,30 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Component\HttpFoundation\RateLimiter; + +use Symfony\Component\HttpFoundation\Request; +use Symfony\Component\RateLimiter\RateLimit; + +/** + * A special type of limiter that deals with requests. + * + * This allows to limit on different types of information + * from the requests. + * + * @author Wouter de Jong + */ +interface RequestRateLimiterInterface +{ + public function consume(Request $request): RateLimit; + + public function reset(Request $request): void; +} diff --git a/3rdparty/symfony/http-foundation/RedirectResponse.php b/3rdparty/symfony/http-foundation/RedirectResponse.php new file mode 100644 index 00000000..220dcf61 --- /dev/null +++ b/3rdparty/symfony/http-foundation/RedirectResponse.php @@ -0,0 +1,92 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Component\HttpFoundation; + +/** + * RedirectResponse represents an HTTP response doing a redirect. + * + * @author Fabien Potencier + */ +class RedirectResponse extends Response +{ + protected $targetUrl; + + /** + * Creates a redirect response so that it conforms to the rules defined for a redirect status code. + * + * @param string $url The URL to redirect to. The URL should be a full URL, with schema etc., + * but practically every browser redirects on paths only as well + * @param int $status The HTTP status code (302 "Found" by default) + * @param array $headers The headers (Location is always set to the given URL) + * + * @throws \InvalidArgumentException + * + * @see https://tools.ietf.org/html/rfc2616#section-10.3 + */ + public function __construct(string $url, int $status = 302, array $headers = []) + { + parent::__construct('', $status, $headers); + + $this->setTargetUrl($url); + + if (!$this->isRedirect()) { + throw new \InvalidArgumentException(\sprintf('The HTTP status code is not a redirect ("%s" given).', $status)); + } + + if (301 == $status && !\array_key_exists('cache-control', array_change_key_case($headers, \CASE_LOWER))) { + $this->headers->remove('cache-control'); + } + } + + /** + * Returns the target URL. + */ + public function getTargetUrl(): string + { + return $this->targetUrl; + } + + /** + * Sets the redirect target of this response. + * + * @return $this + * + * @throws \InvalidArgumentException + */ + public function setTargetUrl(string $url): static + { + if ('' === $url) { + throw new \InvalidArgumentException('Cannot redirect to an empty URL.'); + } + + $this->targetUrl = $url; + + $this->setContent( + \sprintf(' + + + + + + Redirecting to %1$s + + + Redirecting to %1$s. + +', htmlspecialchars($url, \ENT_QUOTES, 'UTF-8'))); + + $this->headers->set('Location', $url); + $this->headers->set('Content-Type', 'text/html; charset=utf-8'); + + return $this; + } +} diff --git a/3rdparty/symfony/http-foundation/Request.php b/3rdparty/symfony/http-foundation/Request.php new file mode 100644 index 00000000..a66312c8 --- /dev/null +++ b/3rdparty/symfony/http-foundation/Request.php @@ -0,0 +1,2163 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Component\HttpFoundation; + +use Symfony\Component\HttpFoundation\Exception\BadRequestException; +use Symfony\Component\HttpFoundation\Exception\ConflictingHeadersException; +use Symfony\Component\HttpFoundation\Exception\JsonException; +use Symfony\Component\HttpFoundation\Exception\SessionNotFoundException; +use Symfony\Component\HttpFoundation\Exception\SuspiciousOperationException; +use Symfony\Component\HttpFoundation\Session\SessionInterface; + +// Help opcache.preload discover always-needed symbols +class_exists(AcceptHeader::class); +class_exists(FileBag::class); +class_exists(HeaderBag::class); +class_exists(HeaderUtils::class); +class_exists(InputBag::class); +class_exists(ParameterBag::class); +class_exists(ServerBag::class); + +/** + * Request represents an HTTP request. + * + * The methods dealing with URL accept / return a raw path (% encoded): + * * getBasePath + * * getBaseUrl + * * getPathInfo + * * getRequestUri + * * getUri + * * getUriForPath + * + * @author Fabien Potencier + */ +class Request +{ + public const HEADER_FORWARDED = 0b000001; // When using RFC 7239 + public const HEADER_X_FORWARDED_FOR = 0b000010; + public const HEADER_X_FORWARDED_HOST = 0b000100; + public const HEADER_X_FORWARDED_PROTO = 0b001000; + public const HEADER_X_FORWARDED_PORT = 0b010000; + public const HEADER_X_FORWARDED_PREFIX = 0b100000; + + public const HEADER_X_FORWARDED_AWS_ELB = 0b0011010; // AWS ELB doesn't send X-Forwarded-Host + public const HEADER_X_FORWARDED_TRAEFIK = 0b0111110; // All "X-Forwarded-*" headers sent by Traefik reverse proxy + + public const METHOD_HEAD = 'HEAD'; + public const METHOD_GET = 'GET'; + public const METHOD_POST = 'POST'; + public const METHOD_PUT = 'PUT'; + public const METHOD_PATCH = 'PATCH'; + public const METHOD_DELETE = 'DELETE'; + public const METHOD_PURGE = 'PURGE'; + public const METHOD_OPTIONS = 'OPTIONS'; + public const METHOD_TRACE = 'TRACE'; + public const METHOD_CONNECT = 'CONNECT'; + + /** + * @var string[] + */ + protected static $trustedProxies = []; + + /** + * @var string[] + */ + protected static $trustedHostPatterns = []; + + /** + * @var string[] + */ + protected static $trustedHosts = []; + + protected static $httpMethodParameterOverride = false; + + /** + * Custom parameters. + * + * @var ParameterBag + */ + public $attributes; + + /** + * Request body parameters ($_POST). + * + * @see getPayload() for portability between content types + * + * @var InputBag + */ + public $request; + + /** + * Query string parameters ($_GET). + * + * @var InputBag + */ + public $query; + + /** + * Server and execution environment parameters ($_SERVER). + * + * @var ServerBag + */ + public $server; + + /** + * Uploaded files ($_FILES). + * + * @var FileBag + */ + public $files; + + /** + * Cookies ($_COOKIE). + * + * @var InputBag + */ + public $cookies; + + /** + * Headers (taken from the $_SERVER). + * + * @var HeaderBag + */ + public $headers; + + /** + * @var string|resource|false|null + */ + protected $content; + + /** + * @var string[]|null + */ + protected $languages; + + /** + * @var string[]|null + */ + protected $charsets; + + /** + * @var string[]|null + */ + protected $encodings; + + /** + * @var string[]|null + */ + protected $acceptableContentTypes; + + /** + * @var string|null + */ + protected $pathInfo; + + /** + * @var string|null + */ + protected $requestUri; + + /** + * @var string|null + */ + protected $baseUrl; + + /** + * @var string|null + */ + protected $basePath; + + /** + * @var string|null + */ + protected $method; + + /** + * @var string|null + */ + protected $format; + + /** + * @var SessionInterface|callable():SessionInterface|null + */ + protected $session; + + /** + * @var string|null + */ + protected $locale; + + /** + * @var string + */ + protected $defaultLocale = 'en'; + + /** + * @var array|null + */ + protected static $formats; + + protected static $requestFactory; + + private ?string $preferredFormat = null; + private bool $isHostValid = true; + private bool $isForwardedValid = true; + private bool $isSafeContentPreferred; + + private array $trustedValuesCache = []; + + private static int $trustedHeaderSet = -1; + + private const FORWARDED_PARAMS = [ + self::HEADER_X_FORWARDED_FOR => 'for', + self::HEADER_X_FORWARDED_HOST => 'host', + self::HEADER_X_FORWARDED_PROTO => 'proto', + self::HEADER_X_FORWARDED_PORT => 'host', + ]; + + /** + * Names for headers that can be trusted when + * using trusted proxies. + * + * The FORWARDED header is the standard as of rfc7239. + * + * The other headers are non-standard, but widely used + * by popular reverse proxies (like Apache mod_proxy or Amazon EC2). + */ + private const TRUSTED_HEADERS = [ + self::HEADER_FORWARDED => 'FORWARDED', + self::HEADER_X_FORWARDED_FOR => 'X_FORWARDED_FOR', + self::HEADER_X_FORWARDED_HOST => 'X_FORWARDED_HOST', + self::HEADER_X_FORWARDED_PROTO => 'X_FORWARDED_PROTO', + self::HEADER_X_FORWARDED_PORT => 'X_FORWARDED_PORT', + self::HEADER_X_FORWARDED_PREFIX => 'X_FORWARDED_PREFIX', + ]; + + /** @var bool */ + private $isIisRewrite = false; + + /** + * @param array $query The GET parameters + * @param array $request The POST parameters + * @param array $attributes The request attributes (parameters parsed from the PATH_INFO, ...) + * @param array $cookies The COOKIE parameters + * @param array $files The FILES parameters + * @param array $server The SERVER parameters + * @param string|resource|null $content The raw body data + */ + public function __construct(array $query = [], array $request = [], array $attributes = [], array $cookies = [], array $files = [], array $server = [], $content = null) + { + $this->initialize($query, $request, $attributes, $cookies, $files, $server, $content); + } + + /** + * Sets the parameters for this request. + * + * This method also re-initializes all properties. + * + * @param array $query The GET parameters + * @param array $request The POST parameters + * @param array $attributes The request attributes (parameters parsed from the PATH_INFO, ...) + * @param array $cookies The COOKIE parameters + * @param array $files The FILES parameters + * @param array $server The SERVER parameters + * @param string|resource|null $content The raw body data + * + * @return void + */ + public function initialize(array $query = [], array $request = [], array $attributes = [], array $cookies = [], array $files = [], array $server = [], $content = null) + { + $this->request = new InputBag($request); + $this->query = new InputBag($query); + $this->attributes = new ParameterBag($attributes); + $this->cookies = new InputBag($cookies); + $this->files = new FileBag($files); + $this->server = new ServerBag($server); + $this->headers = new HeaderBag($this->server->getHeaders()); + + $this->content = $content; + $this->languages = null; + $this->charsets = null; + $this->encodings = null; + $this->acceptableContentTypes = null; + $this->pathInfo = null; + $this->requestUri = null; + $this->baseUrl = null; + $this->basePath = null; + $this->method = null; + $this->format = null; + } + + /** + * Creates a new request with values from PHP's super globals. + */ + public static function createFromGlobals(): static + { + $request = self::createRequestFromFactory($_GET, $_POST, [], $_COOKIE, $_FILES, $_SERVER); + + if (str_starts_with($request->headers->get('CONTENT_TYPE', ''), 'application/x-www-form-urlencoded') + && \in_array(strtoupper($request->server->get('REQUEST_METHOD', 'GET')), ['PUT', 'DELETE', 'PATCH']) + ) { + parse_str($request->getContent(), $data); + $request->request = new InputBag($data); + } + + return $request; + } + + /** + * Creates a Request based on a given URI and configuration. + * + * The information contained in the URI always take precedence + * over the other information (server and parameters). + * + * @param string $uri The URI + * @param string $method The HTTP method + * @param array $parameters The query (GET) or request (POST) parameters + * @param array $cookies The request cookies ($_COOKIE) + * @param array $files The request files ($_FILES) + * @param array $server The server parameters ($_SERVER) + * @param string|resource|null $content The raw body data + * + * @throws BadRequestException When the URI is invalid + */ + public static function create(string $uri, string $method = 'GET', array $parameters = [], array $cookies = [], array $files = [], array $server = [], $content = null): static + { + $server = array_replace([ + 'SERVER_NAME' => 'localhost', + 'SERVER_PORT' => 80, + 'HTTP_HOST' => 'localhost', + 'HTTP_USER_AGENT' => 'Symfony', + 'HTTP_ACCEPT' => 'text/html,application/xhtml+xml,application/xml;q=0.9,*/*;q=0.8', + 'HTTP_ACCEPT_LANGUAGE' => 'en-us,en;q=0.5', + 'HTTP_ACCEPT_CHARSET' => 'ISO-8859-1,utf-8;q=0.7,*;q=0.7', + 'REMOTE_ADDR' => '127.0.0.1', + 'SCRIPT_NAME' => '', + 'SCRIPT_FILENAME' => '', + 'SERVER_PROTOCOL' => 'HTTP/1.1', + 'REQUEST_TIME' => time(), + 'REQUEST_TIME_FLOAT' => microtime(true), + ], $server); + + $server['PATH_INFO'] = ''; + $server['REQUEST_METHOD'] = strtoupper($method); + + if (($i = strcspn($uri, ':/?#')) && ':' === ($uri[$i] ?? null) && (strspn($uri, 'abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789+-.') !== $i || strcspn($uri, 'abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ'))) { + throw new BadRequestException('Invalid URI: Scheme is malformed.'); + } + if (false === $components = parse_url(\strlen($uri) !== strcspn($uri, '?#') ? $uri : $uri.'#')) { + throw new BadRequestException('Invalid URI.'); + } + + $part = ($components['user'] ?? '').':'.($components['pass'] ?? ''); + + if (':' !== $part && \strlen($part) !== strcspn($part, '[]')) { + throw new BadRequestException('Invalid URI: Userinfo is malformed.'); + } + if (($part = $components['host'] ?? '') && !self::isHostValid($part)) { + throw new BadRequestException('Invalid URI: Host is malformed.'); + } + if (false !== ($i = strpos($uri, '\\')) && $i < strcspn($uri, '?#')) { + throw new BadRequestException('Invalid URI: A URI cannot contain a backslash.'); + } + if (\strlen($uri) !== strcspn($uri, "\r\n\t")) { + throw new BadRequestException('Invalid URI: A URI cannot contain CR/LF/TAB characters.'); + } + if ('' !== $uri && (\ord($uri[0]) <= 32 || \ord($uri[-1]) <= 32)) { + throw new BadRequestException('Invalid URI: A URI must not start nor end with ASCII control characters or spaces.'); + } + + if (isset($components['host'])) { + $server['SERVER_NAME'] = $components['host']; + $server['HTTP_HOST'] = $components['host']; + } + + if (isset($components['scheme'])) { + if ('https' === $components['scheme']) { + $server['HTTPS'] = 'on'; + $server['SERVER_PORT'] = 443; + } else { + unset($server['HTTPS']); + $server['SERVER_PORT'] = 80; + } + } + + if (isset($components['port'])) { + $server['SERVER_PORT'] = $components['port']; + $server['HTTP_HOST'] .= ':'.$components['port']; + } + + if (isset($components['user'])) { + $server['PHP_AUTH_USER'] = $components['user']; + } + + if (isset($components['pass'])) { + $server['PHP_AUTH_PW'] = $components['pass']; + } + + if (!isset($components['path'])) { + $components['path'] = '/'; + } + + switch (strtoupper($method)) { + case 'POST': + case 'PUT': + case 'DELETE': + if (!isset($server['CONTENT_TYPE'])) { + $server['CONTENT_TYPE'] = 'application/x-www-form-urlencoded'; + } + // no break + case 'PATCH': + $request = $parameters; + $query = []; + break; + default: + $request = []; + $query = $parameters; + break; + } + + $queryString = ''; + if (isset($components['query'])) { + parse_str(html_entity_decode($components['query']), $qs); + + if ($query) { + $query = array_replace($qs, $query); + $queryString = http_build_query($query, '', '&'); + } else { + $query = $qs; + $queryString = $components['query']; + } + } elseif ($query) { + $queryString = http_build_query($query, '', '&'); + } + + $server['REQUEST_URI'] = $components['path'].('' !== $queryString ? '?'.$queryString : ''); + $server['QUERY_STRING'] = $queryString; + + return self::createRequestFromFactory($query, $request, [], $cookies, $files, $server, $content); + } + + /** + * Sets a callable able to create a Request instance. + * + * This is mainly useful when you need to override the Request class + * to keep BC with an existing system. It should not be used for any + * other purpose. + * + * @return void + */ + public static function setFactory(?callable $callable) + { + self::$requestFactory = $callable; + } + + /** + * Clones a request and overrides some of its parameters. + * + * @param array|null $query The GET parameters + * @param array|null $request The POST parameters + * @param array|null $attributes The request attributes (parameters parsed from the PATH_INFO, ...) + * @param array|null $cookies The COOKIE parameters + * @param array|null $files The FILES parameters + * @param array|null $server The SERVER parameters + */ + public function duplicate(?array $query = null, ?array $request = null, ?array $attributes = null, ?array $cookies = null, ?array $files = null, ?array $server = null): static + { + $dup = clone $this; + if (null !== $query) { + $dup->query = new InputBag($query); + } + if (null !== $request) { + $dup->request = new InputBag($request); + } + if (null !== $attributes) { + $dup->attributes = new ParameterBag($attributes); + } + if (null !== $cookies) { + $dup->cookies = new InputBag($cookies); + } + if (null !== $files) { + $dup->files = new FileBag($files); + } + if (null !== $server) { + $dup->server = new ServerBag($server); + $dup->headers = new HeaderBag($dup->server->getHeaders()); + } + $dup->languages = null; + $dup->charsets = null; + $dup->encodings = null; + $dup->acceptableContentTypes = null; + $dup->pathInfo = null; + $dup->requestUri = null; + $dup->baseUrl = null; + $dup->basePath = null; + $dup->method = null; + $dup->format = null; + + if (!$dup->get('_format') && $this->get('_format')) { + $dup->attributes->set('_format', $this->get('_format')); + } + + if (!$dup->getRequestFormat(null)) { + $dup->setRequestFormat($this->getRequestFormat(null)); + } + + return $dup; + } + + /** + * Clones the current request. + * + * Note that the session is not cloned as duplicated requests + * are most of the time sub-requests of the main one. + */ + public function __clone() + { + $this->query = clone $this->query; + $this->request = clone $this->request; + $this->attributes = clone $this->attributes; + $this->cookies = clone $this->cookies; + $this->files = clone $this->files; + $this->server = clone $this->server; + $this->headers = clone $this->headers; + } + + public function __toString(): string + { + $content = $this->getContent(); + + $cookieHeader = ''; + $cookies = []; + + foreach ($this->cookies as $k => $v) { + $cookies[] = \is_array($v) ? http_build_query([$k => $v], '', '; ', \PHP_QUERY_RFC3986) : "$k=$v"; + } + + if ($cookies) { + $cookieHeader = 'Cookie: '.implode('; ', $cookies)."\r\n"; + } + + return + \sprintf('%s %s %s', $this->getMethod(), $this->getRequestUri(), $this->server->get('SERVER_PROTOCOL'))."\r\n". + $this->headers. + $cookieHeader."\r\n". + $content; + } + + /** + * Overrides the PHP global variables according to this request instance. + * + * It overrides $_GET, $_POST, $_REQUEST, $_SERVER, $_COOKIE. + * $_FILES is never overridden, see rfc1867 + * + * @return void + */ + public function overrideGlobals() + { + $this->server->set('QUERY_STRING', static::normalizeQueryString(http_build_query($this->query->all(), '', '&'))); + + $_GET = $this->query->all(); + $_POST = $this->request->all(); + $_SERVER = $this->server->all(); + $_COOKIE = $this->cookies->all(); + + foreach ($this->headers->all() as $key => $value) { + $key = strtoupper(str_replace('-', '_', $key)); + if (\in_array($key, ['CONTENT_TYPE', 'CONTENT_LENGTH', 'CONTENT_MD5'], true)) { + $_SERVER[$key] = implode(', ', $value); + } else { + $_SERVER['HTTP_'.$key] = implode(', ', $value); + } + } + + $request = ['g' => $_GET, 'p' => $_POST, 'c' => $_COOKIE]; + + $requestOrder = \ini_get('request_order') ?: \ini_get('variables_order'); + $requestOrder = preg_replace('#[^cgp]#', '', strtolower($requestOrder)) ?: 'gp'; + + $_REQUEST = [[]]; + + foreach (str_split($requestOrder) as $order) { + $_REQUEST[] = $request[$order]; + } + + $_REQUEST = array_merge(...$_REQUEST); + } + + /** + * Sets a list of trusted proxies. + * + * You should only list the reverse proxies that you manage directly. + * + * @param array $proxies A list of trusted proxies, the string 'REMOTE_ADDR' will be replaced with $_SERVER['REMOTE_ADDR'] + * @param int $trustedHeaderSet A bit field of Request::HEADER_*, to set which headers to trust from your proxies + * + * @return void + */ + public static function setTrustedProxies(array $proxies, int $trustedHeaderSet) + { + self::$trustedProxies = array_reduce($proxies, function ($proxies, $proxy) { + if ('REMOTE_ADDR' !== $proxy) { + $proxies[] = $proxy; + } elseif (isset($_SERVER['REMOTE_ADDR'])) { + $proxies[] = $_SERVER['REMOTE_ADDR']; + } + + return $proxies; + }, []); + self::$trustedHeaderSet = $trustedHeaderSet; + } + + /** + * Gets the list of trusted proxies. + * + * @return string[] + */ + public static function getTrustedProxies(): array + { + return self::$trustedProxies; + } + + /** + * Gets the set of trusted headers from trusted proxies. + * + * @return int A bit field of Request::HEADER_* that defines which headers are trusted from your proxies + */ + public static function getTrustedHeaderSet(): int + { + return self::$trustedHeaderSet; + } + + /** + * Sets a list of trusted host patterns. + * + * You should only list the hosts you manage using regexs. + * + * @param array $hostPatterns A list of trusted host patterns + * + * @return void + */ + public static function setTrustedHosts(array $hostPatterns) + { + self::$trustedHostPatterns = array_map(fn ($hostPattern) => \sprintf('{%s}i', $hostPattern), $hostPatterns); + // we need to reset trusted hosts on trusted host patterns change + self::$trustedHosts = []; + } + + /** + * Gets the list of trusted host patterns. + * + * @return string[] + */ + public static function getTrustedHosts(): array + { + return self::$trustedHostPatterns; + } + + /** + * Normalizes a query string. + * + * It builds a normalized query string, where keys/value pairs are alphabetized, + * have consistent escaping and unneeded delimiters are removed. + */ + public static function normalizeQueryString(?string $qs): string + { + if ('' === ($qs ?? '')) { + return ''; + } + + $qs = HeaderUtils::parseQuery($qs); + ksort($qs); + + return http_build_query($qs, '', '&', \PHP_QUERY_RFC3986); + } + + /** + * Enables support for the _method request parameter to determine the intended HTTP method. + * + * Be warned that enabling this feature might lead to CSRF issues in your code. + * Check that you are using CSRF tokens when required. + * If the HTTP method parameter override is enabled, an html-form with method "POST" can be altered + * and used to send a "PUT" or "DELETE" request via the _method request parameter. + * If these methods are not protected against CSRF, this presents a possible vulnerability. + * + * The HTTP method can only be overridden when the real HTTP method is POST. + * + * @return void + */ + public static function enableHttpMethodParameterOverride() + { + self::$httpMethodParameterOverride = true; + } + + /** + * Checks whether support for the _method request parameter is enabled. + */ + public static function getHttpMethodParameterOverride(): bool + { + return self::$httpMethodParameterOverride; + } + + /** + * Gets a "parameter" value from any bag. + * + * This method is mainly useful for libraries that want to provide some flexibility. If you don't need the + * flexibility in controllers, it is better to explicitly get request parameters from the appropriate + * public property instead (attributes, query, request). + * + * Order of precedence: PATH (routing placeholders or custom attributes), GET, POST + * + * @internal use explicit input sources instead + */ + public function get(string $key, mixed $default = null): mixed + { + if ($this !== $result = $this->attributes->get($key, $this)) { + return $result; + } + + if ($this->query->has($key)) { + return $this->query->all()[$key]; + } + + if ($this->request->has($key)) { + return $this->request->all()[$key]; + } + + return $default; + } + + /** + * Gets the Session. + * + * @throws SessionNotFoundException When session is not set properly + */ + public function getSession(): SessionInterface + { + $session = $this->session; + if (!$session instanceof SessionInterface && null !== $session) { + $this->setSession($session = $session()); + } + + if (null === $session) { + throw new SessionNotFoundException('Session has not been set.'); + } + + return $session; + } + + /** + * Whether the request contains a Session which was started in one of the + * previous requests. + */ + public function hasPreviousSession(): bool + { + // the check for $this->session avoids malicious users trying to fake a session cookie with proper name + return $this->hasSession() && $this->cookies->has($this->getSession()->getName()); + } + + /** + * Whether the request contains a Session object. + * + * This method does not give any information about the state of the session object, + * like whether the session is started or not. It is just a way to check if this Request + * is associated with a Session instance. + * + * @param bool $skipIfUninitialized When true, ignores factories injected by `setSessionFactory` + */ + public function hasSession(bool $skipIfUninitialized = false): bool + { + return null !== $this->session && (!$skipIfUninitialized || $this->session instanceof SessionInterface); + } + + /** + * @return void + */ + public function setSession(SessionInterface $session) + { + $this->session = $session; + } + + /** + * @internal + * + * @param callable(): SessionInterface $factory + */ + public function setSessionFactory(callable $factory): void + { + $this->session = $factory(...); + } + + /** + * Returns the client IP addresses. + * + * In the returned array the most trusted IP address is first, and the + * least trusted one last. The "real" client IP address is the last one, + * but this is also the least trusted one. Trusted proxies are stripped. + * + * Use this method carefully; you should use getClientIp() instead. + * + * @see getClientIp() + */ + public function getClientIps(): array + { + $ip = $this->server->get('REMOTE_ADDR'); + + if (!$this->isFromTrustedProxy()) { + return [$ip]; + } + + return $this->getTrustedValues(self::HEADER_X_FORWARDED_FOR, $ip) ?: [$ip]; + } + + /** + * Returns the client IP address. + * + * This method can read the client IP address from the "X-Forwarded-For" header + * when trusted proxies were set via "setTrustedProxies()". The "X-Forwarded-For" + * header value is a comma+space separated list of IP addresses, the left-most + * being the original client, and each successive proxy that passed the request + * adding the IP address where it received the request from. + * + * If your reverse proxy uses a different header name than "X-Forwarded-For", + * ("Client-Ip" for instance), configure it via the $trustedHeaderSet + * argument of the Request::setTrustedProxies() method instead. + * + * @see getClientIps() + * @see https://wikipedia.org/wiki/X-Forwarded-For + */ + public function getClientIp(): ?string + { + $ipAddresses = $this->getClientIps(); + + return $ipAddresses[0]; + } + + /** + * Returns current script name. + */ + public function getScriptName(): string + { + return $this->server->get('SCRIPT_NAME', $this->server->get('ORIG_SCRIPT_NAME', '')); + } + + /** + * Returns the path being requested relative to the executed script. + * + * The path info always starts with a /. + * + * Suppose this request is instantiated from /mysite on localhost: + * + * * http://localhost/mysite returns an empty string + * * http://localhost/mysite/about returns '/about' + * * http://localhost/mysite/enco%20ded returns '/enco%20ded' + * * http://localhost/mysite/about?var=1 returns '/about' + * + * @return string The raw path (i.e. not urldecoded) + */ + public function getPathInfo(): string + { + return $this->pathInfo ??= $this->preparePathInfo(); + } + + /** + * Returns the root path from which this request is executed. + * + * Suppose that an index.php file instantiates this request object: + * + * * http://localhost/index.php returns an empty string + * * http://localhost/index.php/page returns an empty string + * * http://localhost/web/index.php returns '/web' + * * http://localhost/we%20b/index.php returns '/we%20b' + * + * @return string The raw path (i.e. not urldecoded) + */ + public function getBasePath(): string + { + return $this->basePath ??= $this->prepareBasePath(); + } + + /** + * Returns the root URL from which this request is executed. + * + * The base URL never ends with a /. + * + * This is similar to getBasePath(), except that it also includes the + * script filename (e.g. index.php) if one exists. + * + * @return string The raw URL (i.e. not urldecoded) + */ + public function getBaseUrl(): string + { + $trustedPrefix = ''; + + // the proxy prefix must be prepended to any prefix being needed at the webserver level + if ($this->isFromTrustedProxy() && $trustedPrefixValues = $this->getTrustedValues(self::HEADER_X_FORWARDED_PREFIX)) { + $trustedPrefix = rtrim($trustedPrefixValues[0], '/'); + } + + return $trustedPrefix.$this->getBaseUrlReal(); + } + + /** + * Returns the real base URL received by the webserver from which this request is executed. + * The URL does not include trusted reverse proxy prefix. + * + * @return string The raw URL (i.e. not urldecoded) + */ + private function getBaseUrlReal(): string + { + return $this->baseUrl ??= $this->prepareBaseUrl(); + } + + /** + * Gets the request's scheme. + */ + public function getScheme(): string + { + return $this->isSecure() ? 'https' : 'http'; + } + + /** + * Returns the port on which the request is made. + * + * This method can read the client port from the "X-Forwarded-Port" header + * when trusted proxies were set via "setTrustedProxies()". + * + * The "X-Forwarded-Port" header must contain the client port. + * + * @return int|string|null Can be a string if fetched from the server bag + */ + public function getPort(): int|string|null + { + if ($this->isFromTrustedProxy() && $host = $this->getTrustedValues(self::HEADER_X_FORWARDED_PORT)) { + $host = $host[0]; + } elseif ($this->isFromTrustedProxy() && $host = $this->getTrustedValues(self::HEADER_X_FORWARDED_HOST)) { + $host = $host[0]; + } elseif (!$host = $this->headers->get('HOST')) { + return $this->server->get('SERVER_PORT'); + } + + if ('[' === $host[0]) { + $pos = strpos($host, ':', strrpos($host, ']')); + } else { + $pos = strrpos($host, ':'); + } + + if (false !== $pos && $port = substr($host, $pos + 1)) { + return (int) $port; + } + + return 'https' === $this->getScheme() ? 443 : 80; + } + + /** + * Returns the user. + */ + public function getUser(): ?string + { + return $this->headers->get('PHP_AUTH_USER'); + } + + /** + * Returns the password. + */ + public function getPassword(): ?string + { + return $this->headers->get('PHP_AUTH_PW'); + } + + /** + * Gets the user info. + * + * @return string|null A user name if any and, optionally, scheme-specific information about how to gain authorization to access the server + */ + public function getUserInfo(): ?string + { + $userinfo = $this->getUser(); + + $pass = $this->getPassword(); + if ('' != $pass) { + $userinfo .= ":$pass"; + } + + return $userinfo; + } + + /** + * Returns the HTTP host being requested. + * + * The port name will be appended to the host if it's non-standard. + */ + public function getHttpHost(): string + { + $scheme = $this->getScheme(); + $port = $this->getPort(); + + if (('http' === $scheme && 80 == $port) || ('https' === $scheme && 443 == $port)) { + return $this->getHost(); + } + + return $this->getHost().':'.$port; + } + + /** + * Returns the requested URI (path and query string). + * + * @return string The raw URI (i.e. not URI decoded) + */ + public function getRequestUri(): string + { + return $this->requestUri ??= $this->prepareRequestUri(); + } + + /** + * Gets the scheme and HTTP host. + * + * If the URL was called with basic authentication, the user + * and the password are not added to the generated string. + */ + public function getSchemeAndHttpHost(): string + { + return $this->getScheme().'://'.$this->getHttpHost(); + } + + /** + * Generates a normalized URI (URL) for the Request. + * + * @see getQueryString() + */ + public function getUri(): string + { + if (null !== $qs = $this->getQueryString()) { + $qs = '?'.$qs; + } + + return $this->getSchemeAndHttpHost().$this->getBaseUrl().$this->getPathInfo().$qs; + } + + /** + * Generates a normalized URI for the given path. + * + * @param string $path A path to use instead of the current one + */ + public function getUriForPath(string $path): string + { + return $this->getSchemeAndHttpHost().$this->getBaseUrl().$path; + } + + /** + * Returns the path as relative reference from the current Request path. + * + * Only the URIs path component (no schema, host etc.) is relevant and must be given. + * Both paths must be absolute and not contain relative parts. + * Relative URLs from one resource to another are useful when generating self-contained downloadable document archives. + * Furthermore, they can be used to reduce the link size in documents. + * + * Example target paths, given a base path of "/a/b/c/d": + * - "/a/b/c/d" -> "" + * - "/a/b/c/" -> "./" + * - "/a/b/" -> "../" + * - "/a/b/c/other" -> "other" + * - "/a/x/y" -> "../../x/y" + */ + public function getRelativeUriForPath(string $path): string + { + // be sure that we are dealing with an absolute path + if (!isset($path[0]) || '/' !== $path[0]) { + return $path; + } + + if ($path === $basePath = $this->getPathInfo()) { + return ''; + } + + $sourceDirs = explode('/', isset($basePath[0]) && '/' === $basePath[0] ? substr($basePath, 1) : $basePath); + $targetDirs = explode('/', substr($path, 1)); + array_pop($sourceDirs); + $targetFile = array_pop($targetDirs); + + foreach ($sourceDirs as $i => $dir) { + if (isset($targetDirs[$i]) && $dir === $targetDirs[$i]) { + unset($sourceDirs[$i], $targetDirs[$i]); + } else { + break; + } + } + + $targetDirs[] = $targetFile; + $path = str_repeat('../', \count($sourceDirs)).implode('/', $targetDirs); + + // A reference to the same base directory or an empty subdirectory must be prefixed with "./". + // This also applies to a segment with a colon character (e.g., "file:colon") that cannot be used + // as the first segment of a relative-path reference, as it would be mistaken for a scheme name + // (see https://tools.ietf.org/html/rfc3986#section-4.2). + return !isset($path[0]) || '/' === $path[0] + || false !== ($colonPos = strpos($path, ':')) && ($colonPos < ($slashPos = strpos($path, '/')) || false === $slashPos) + ? "./$path" : $path; + } + + /** + * Generates the normalized query string for the Request. + * + * It builds a normalized query string, where keys/value pairs are alphabetized + * and have consistent escaping. + */ + public function getQueryString(): ?string + { + $qs = static::normalizeQueryString($this->server->get('QUERY_STRING')); + + return '' === $qs ? null : $qs; + } + + /** + * Checks whether the request is secure or not. + * + * This method can read the client protocol from the "X-Forwarded-Proto" header + * when trusted proxies were set via "setTrustedProxies()". + * + * The "X-Forwarded-Proto" header must contain the protocol: "https" or "http". + */ + public function isSecure(): bool + { + if ($this->isFromTrustedProxy() && $proto = $this->getTrustedValues(self::HEADER_X_FORWARDED_PROTO)) { + return \in_array(strtolower($proto[0]), ['https', 'on', 'ssl', '1'], true); + } + + $https = $this->server->get('HTTPS'); + + return !empty($https) && 'off' !== strtolower($https); + } + + /** + * Returns the host name. + * + * This method can read the client host name from the "X-Forwarded-Host" header + * when trusted proxies were set via "setTrustedProxies()". + * + * The "X-Forwarded-Host" header must contain the client host name. + * + * @throws SuspiciousOperationException when the host name is invalid or not trusted + */ + public function getHost(): string + { + if ($this->isFromTrustedProxy() && $host = $this->getTrustedValues(self::HEADER_X_FORWARDED_HOST)) { + $host = $host[0]; + } elseif (!$host = $this->headers->get('HOST')) { + if (!$host = $this->server->get('SERVER_NAME')) { + $host = $this->server->get('SERVER_ADDR', ''); + } + } + + // trim and remove port number from host + // host is lowercase as per RFC 952/2181 + $host = strtolower(preg_replace('/:\d+$/', '', trim($host))); + + // the host can come from the user (HTTP_HOST and depending on the configuration, SERVER_NAME too can come from the user) + if ($host && !self::isHostValid($host)) { + if (!$this->isHostValid) { + return ''; + } + $this->isHostValid = false; + + throw new SuspiciousOperationException(\sprintf('Invalid Host "%s".', $host)); + } + + if (\count(self::$trustedHostPatterns) > 0) { + // to avoid host header injection attacks, you should provide a list of trusted host patterns + + if (\in_array($host, self::$trustedHosts)) { + return $host; + } + + foreach (self::$trustedHostPatterns as $pattern) { + if (preg_match($pattern, $host)) { + self::$trustedHosts[] = $host; + + return $host; + } + } + + if (!$this->isHostValid) { + return ''; + } + $this->isHostValid = false; + + throw new SuspiciousOperationException(\sprintf('Untrusted Host "%s".', $host)); + } + + return $host; + } + + /** + * Sets the request method. + * + * @return void + */ + public function setMethod(string $method) + { + $this->method = null; + $this->server->set('REQUEST_METHOD', $method); + } + + /** + * Gets the request "intended" method. + * + * If the X-HTTP-Method-Override header is set, and if the method is a POST, + * then it is used to determine the "real" intended HTTP method. + * + * The _method request parameter can also be used to determine the HTTP method, + * but only if enableHttpMethodParameterOverride() has been called. + * + * The method is always an uppercased string. + * + * @see getRealMethod() + */ + public function getMethod(): string + { + if (null !== $this->method) { + return $this->method; + } + + $this->method = strtoupper($this->server->get('REQUEST_METHOD', 'GET')); + + if ('POST' !== $this->method) { + return $this->method; + } + + $method = $this->headers->get('X-HTTP-METHOD-OVERRIDE'); + + if (!$method && self::$httpMethodParameterOverride) { + $method = $this->request->get('_method', $this->query->get('_method', 'POST')); + } + + if (!\is_string($method)) { + return $this->method; + } + + $method = strtoupper($method); + + if (\in_array($method, ['GET', 'HEAD', 'POST', 'PUT', 'DELETE', 'CONNECT', 'OPTIONS', 'PATCH', 'PURGE', 'TRACE'], true)) { + return $this->method = $method; + } + + if (!preg_match('/^[A-Z]++$/D', $method)) { + throw new SuspiciousOperationException('Invalid HTTP method override.'); + } + + return $this->method = $method; + } + + /** + * Gets the "real" request method. + * + * @see getMethod() + */ + public function getRealMethod(): string + { + return strtoupper($this->server->get('REQUEST_METHOD', 'GET')); + } + + /** + * Gets the mime type associated with the format. + */ + public function getMimeType(string $format): ?string + { + if (null === static::$formats) { + static::initializeFormats(); + } + + return isset(static::$formats[$format]) ? static::$formats[$format][0] : null; + } + + /** + * Gets the mime types associated with the format. + * + * @return string[] + */ + public static function getMimeTypes(string $format): array + { + if (null === static::$formats) { + static::initializeFormats(); + } + + return static::$formats[$format] ?? []; + } + + /** + * Gets the format associated with the mime type. + */ + public function getFormat(?string $mimeType): ?string + { + $canonicalMimeType = null; + if ($mimeType && false !== $pos = strpos($mimeType, ';')) { + $canonicalMimeType = trim(substr($mimeType, 0, $pos)); + } + + if (null === static::$formats) { + static::initializeFormats(); + } + + $exactFormat = null; + $canonicalFormat = null; + + foreach (static::$formats as $format => $mimeTypes) { + if (\in_array($mimeType, $mimeTypes, true)) { + $exactFormat = $format; + } + if (null !== $canonicalMimeType && \in_array($canonicalMimeType, $mimeTypes, true)) { + $canonicalFormat = $format; + } + } + + if ($format = $exactFormat ?? $canonicalFormat) { + return $format; + } + + return null; + } + + /** + * Associates a format with mime types. + * + * @param string|string[] $mimeTypes The associated mime types (the preferred one must be the first as it will be used as the content type) + * + * @return void + */ + public function setFormat(?string $format, string|array $mimeTypes) + { + if (null === static::$formats) { + static::initializeFormats(); + } + + static::$formats[$format ?? ''] = (array) $mimeTypes; + } + + /** + * Gets the request format. + * + * Here is the process to determine the format: + * + * * format defined by the user (with setRequestFormat()) + * * _format request attribute + * * $default + * + * @see getPreferredFormat + */ + public function getRequestFormat(?string $default = 'html'): ?string + { + $this->format ??= $this->attributes->get('_format'); + + return $this->format ?? $default; + } + + /** + * Sets the request format. + * + * @return void + */ + public function setRequestFormat(?string $format) + { + $this->format = $format; + } + + /** + * Gets the usual name of the format associated with the request's media type (provided in the Content-Type header). + * + * @deprecated since Symfony 6.2, use getContentTypeFormat() instead + */ + public function getContentType(): ?string + { + trigger_deprecation('symfony/http-foundation', '6.2', 'The "%s()" method is deprecated, use "getContentTypeFormat()" instead.', __METHOD__); + + return $this->getContentTypeFormat(); + } + + /** + * Gets the usual name of the format associated with the request's media type (provided in the Content-Type header). + * + * @see Request::$formats + */ + public function getContentTypeFormat(): ?string + { + return $this->getFormat($this->headers->get('CONTENT_TYPE', '')); + } + + /** + * Sets the default locale. + * + * @return void + */ + public function setDefaultLocale(string $locale) + { + $this->defaultLocale = $locale; + + if (null === $this->locale) { + $this->setPhpDefaultLocale($locale); + } + } + + /** + * Get the default locale. + */ + public function getDefaultLocale(): string + { + return $this->defaultLocale; + } + + /** + * Sets the locale. + * + * @return void + */ + public function setLocale(string $locale) + { + $this->setPhpDefaultLocale($this->locale = $locale); + } + + /** + * Get the locale. + */ + public function getLocale(): string + { + return $this->locale ?? $this->defaultLocale; + } + + /** + * Checks if the request method is of specified type. + * + * @param string $method Uppercase request method (GET, POST etc) + */ + public function isMethod(string $method): bool + { + return $this->getMethod() === strtoupper($method); + } + + /** + * Checks whether or not the method is safe. + * + * @see https://tools.ietf.org/html/rfc7231#section-4.2.1 + */ + public function isMethodSafe(): bool + { + return \in_array($this->getMethod(), ['GET', 'HEAD', 'OPTIONS', 'TRACE']); + } + + /** + * Checks whether or not the method is idempotent. + */ + public function isMethodIdempotent(): bool + { + return \in_array($this->getMethod(), ['HEAD', 'GET', 'PUT', 'DELETE', 'TRACE', 'OPTIONS', 'PURGE']); + } + + /** + * Checks whether the method is cacheable or not. + * + * @see https://tools.ietf.org/html/rfc7231#section-4.2.3 + */ + public function isMethodCacheable(): bool + { + return \in_array($this->getMethod(), ['GET', 'HEAD']); + } + + /** + * Returns the protocol version. + * + * If the application is behind a proxy, the protocol version used in the + * requests between the client and the proxy and between the proxy and the + * server might be different. This returns the former (from the "Via" header) + * if the proxy is trusted (see "setTrustedProxies()"), otherwise it returns + * the latter (from the "SERVER_PROTOCOL" server parameter). + */ + public function getProtocolVersion(): ?string + { + if ($this->isFromTrustedProxy()) { + preg_match('~^(HTTP/)?([1-9]\.[0-9])\b~', $this->headers->get('Via') ?? '', $matches); + + if ($matches) { + return 'HTTP/'.$matches[2]; + } + } + + return $this->server->get('SERVER_PROTOCOL'); + } + + /** + * Returns the request body content. + * + * @param bool $asResource If true, a resource will be returned + * + * @return string|resource + * + * @psalm-return ($asResource is true ? resource : string) + */ + public function getContent(bool $asResource = false) + { + $currentContentIsResource = \is_resource($this->content); + + if (true === $asResource) { + if ($currentContentIsResource) { + rewind($this->content); + + return $this->content; + } + + // Content passed in parameter (test) + if (\is_string($this->content)) { + $resource = fopen('php://temp', 'r+'); + fwrite($resource, $this->content); + rewind($resource); + + return $resource; + } + + $this->content = false; + + return fopen('php://input', 'r'); + } + + if ($currentContentIsResource) { + rewind($this->content); + + return stream_get_contents($this->content); + } + + if (null === $this->content || false === $this->content) { + $this->content = file_get_contents('php://input'); + } + + return $this->content; + } + + /** + * Gets the decoded form or json request body. + * + * @throws JsonException When the body cannot be decoded to an array + */ + public function getPayload(): InputBag + { + if ($this->request->count()) { + return clone $this->request; + } + + if ('' === $content = $this->getContent()) { + return new InputBag([]); + } + + try { + $content = json_decode($content, true, 512, \JSON_BIGINT_AS_STRING | \JSON_THROW_ON_ERROR); + } catch (\JsonException $e) { + throw new JsonException('Could not decode request body.', $e->getCode(), $e); + } + + if (!\is_array($content)) { + throw new JsonException(\sprintf('JSON content was expected to decode to an array, "%s" returned.', get_debug_type($content))); + } + + return new InputBag($content); + } + + /** + * Gets the request body decoded as array, typically from a JSON payload. + * + * @see getPayload() for portability between content types + * + * @throws JsonException When the body cannot be decoded to an array + */ + public function toArray(): array + { + if ('' === $content = $this->getContent()) { + throw new JsonException('Request body is empty.'); + } + + try { + $content = json_decode($content, true, 512, \JSON_BIGINT_AS_STRING | \JSON_THROW_ON_ERROR); + } catch (\JsonException $e) { + throw new JsonException('Could not decode request body.', $e->getCode(), $e); + } + + if (!\is_array($content)) { + throw new JsonException(\sprintf('JSON content was expected to decode to an array, "%s" returned.', get_debug_type($content))); + } + + return $content; + } + + /** + * Gets the Etags. + */ + public function getETags(): array + { + return preg_split('/\s*,\s*/', $this->headers->get('If-None-Match', ''), -1, \PREG_SPLIT_NO_EMPTY); + } + + public function isNoCache(): bool + { + return $this->headers->hasCacheControlDirective('no-cache') || 'no-cache' == $this->headers->get('Pragma'); + } + + /** + * Gets the preferred format for the response by inspecting, in the following order: + * * the request format set using setRequestFormat; + * * the values of the Accept HTTP header. + * + * Note that if you use this method, you should send the "Vary: Accept" header + * in the response to prevent any issues with intermediary HTTP caches. + */ + public function getPreferredFormat(?string $default = 'html'): ?string + { + if ($this->preferredFormat ??= $this->getRequestFormat(null)) { + return $this->preferredFormat; + } + + foreach ($this->getAcceptableContentTypes() as $mimeType) { + if ($this->preferredFormat = $this->getFormat($mimeType)) { + return $this->preferredFormat; + } + } + + return $default; + } + + /** + * Returns the preferred language. + * + * @param string[] $locales An array of ordered available locales + */ + public function getPreferredLanguage(?array $locales = null): ?string + { + $preferredLanguages = $this->getLanguages(); + + if (empty($locales)) { + return $preferredLanguages[0] ?? null; + } + + if (!$preferredLanguages) { + return $locales[0]; + } + + $extendedPreferredLanguages = []; + foreach ($preferredLanguages as $language) { + $extendedPreferredLanguages[] = $language; + if (false !== $position = strpos($language, '_')) { + $superLanguage = substr($language, 0, $position); + if (!\in_array($superLanguage, $preferredLanguages)) { + $extendedPreferredLanguages[] = $superLanguage; + } + } + } + + $preferredLanguages = array_values(array_intersect($extendedPreferredLanguages, $locales)); + + return $preferredLanguages[0] ?? $locales[0]; + } + + /** + * Gets a list of languages acceptable by the client browser ordered in the user browser preferences. + * + * @return string[] + */ + public function getLanguages(): array + { + if (null !== $this->languages) { + return $this->languages; + } + + $languages = AcceptHeader::fromString($this->headers->get('Accept-Language'))->all(); + $this->languages = []; + foreach ($languages as $acceptHeaderItem) { + $lang = $acceptHeaderItem->getValue(); + if (str_contains($lang, '-')) { + $codes = explode('-', $lang); + if ('i' === $codes[0]) { + // Language not listed in ISO 639 that are not variants + // of any listed language, which can be registered with the + // i-prefix, such as i-cherokee + if (\count($codes) > 1) { + $lang = $codes[1]; + } + } else { + for ($i = 0, $max = \count($codes); $i < $max; ++$i) { + if (0 === $i) { + $lang = strtolower($codes[0]); + } else { + $lang .= '_'.strtoupper($codes[$i]); + } + } + } + } + + $this->languages[] = $lang; + } + + return $this->languages; + } + + /** + * Gets a list of charsets acceptable by the client browser in preferable order. + * + * @return string[] + */ + public function getCharsets(): array + { + return $this->charsets ??= array_map('strval', array_keys(AcceptHeader::fromString($this->headers->get('Accept-Charset'))->all())); + } + + /** + * Gets a list of encodings acceptable by the client browser in preferable order. + * + * @return string[] + */ + public function getEncodings(): array + { + return $this->encodings ??= array_map('strval', array_keys(AcceptHeader::fromString($this->headers->get('Accept-Encoding'))->all())); + } + + /** + * Gets a list of content types acceptable by the client browser in preferable order. + * + * @return string[] + */ + public function getAcceptableContentTypes(): array + { + return $this->acceptableContentTypes ??= array_map('strval', array_keys(AcceptHeader::fromString($this->headers->get('Accept'))->all())); + } + + /** + * Returns true if the request is an XMLHttpRequest. + * + * It works if your JavaScript library sets an X-Requested-With HTTP header. + * It is known to work with common JavaScript frameworks: + * + * @see https://wikipedia.org/wiki/List_of_Ajax_frameworks#JavaScript + */ + public function isXmlHttpRequest(): bool + { + return 'XMLHttpRequest' == $this->headers->get('X-Requested-With'); + } + + /** + * Checks whether the client browser prefers safe content or not according to RFC8674. + * + * @see https://tools.ietf.org/html/rfc8674 + */ + public function preferSafeContent(): bool + { + if (isset($this->isSafeContentPreferred)) { + return $this->isSafeContentPreferred; + } + + if (!$this->isSecure()) { + // see https://tools.ietf.org/html/rfc8674#section-3 + return $this->isSafeContentPreferred = false; + } + + return $this->isSafeContentPreferred = AcceptHeader::fromString($this->headers->get('Prefer'))->has('safe'); + } + + /* + * The following methods are derived from code of the Zend Framework (1.10dev - 2010-01-24) + * + * Code subject to the new BSD license (https://framework.zend.com/license). + * + * Copyright (c) 2005-2010 Zend Technologies USA Inc. (https://www.zend.com/) + */ + + /** + * @return string + */ + protected function prepareRequestUri() + { + $requestUri = ''; + + if ($this->isIisRewrite() && '' != $this->server->get('UNENCODED_URL')) { + // IIS7 with URL Rewrite: make sure we get the unencoded URL (double slash problem) + $requestUri = $this->server->get('UNENCODED_URL'); + $this->server->remove('UNENCODED_URL'); + } elseif ($this->server->has('REQUEST_URI')) { + $requestUri = $this->server->get('REQUEST_URI'); + + if ('' !== $requestUri && '/' === $requestUri[0]) { + // To only use path and query remove the fragment. + if (false !== $pos = strpos($requestUri, '#')) { + $requestUri = substr($requestUri, 0, $pos); + } + } else { + // HTTP proxy reqs setup request URI with scheme and host [and port] + the URL path, + // only use URL path. + $uriComponents = parse_url($requestUri); + + if (isset($uriComponents['path'])) { + $requestUri = $uriComponents['path']; + } + + if (isset($uriComponents['query'])) { + $requestUri .= '?'.$uriComponents['query']; + } + } + } elseif ($this->server->has('ORIG_PATH_INFO')) { + // IIS 5.0, PHP as CGI + $requestUri = $this->server->get('ORIG_PATH_INFO'); + if ('' != $this->server->get('QUERY_STRING')) { + $requestUri .= '?'.$this->server->get('QUERY_STRING'); + } + $this->server->remove('ORIG_PATH_INFO'); + } + + // normalize the request URI to ease creating sub-requests from this request + $this->server->set('REQUEST_URI', $requestUri); + + return $requestUri; + } + + /** + * Prepares the base URL. + */ + protected function prepareBaseUrl(): string + { + $filename = basename($this->server->get('SCRIPT_FILENAME', '')); + + if (basename($this->server->get('SCRIPT_NAME', '')) === $filename) { + $baseUrl = $this->server->get('SCRIPT_NAME'); + } elseif (basename($this->server->get('PHP_SELF', '')) === $filename) { + $baseUrl = $this->server->get('PHP_SELF'); + } elseif (basename($this->server->get('ORIG_SCRIPT_NAME', '')) === $filename) { + $baseUrl = $this->server->get('ORIG_SCRIPT_NAME'); // 1and1 shared hosting compatibility + } else { + // Backtrack up the script_filename to find the portion matching + // php_self + $path = $this->server->get('PHP_SELF', ''); + $file = $this->server->get('SCRIPT_FILENAME', ''); + $segs = explode('/', trim($file, '/')); + $segs = array_reverse($segs); + $index = 0; + $last = \count($segs); + $baseUrl = ''; + do { + $seg = $segs[$index]; + $baseUrl = '/'.$seg.$baseUrl; + ++$index; + } while ($last > $index && (false !== $pos = strpos($path, $baseUrl)) && 0 != $pos); + } + + // Does the baseUrl have anything in common with the request_uri? + $requestUri = $this->getRequestUri(); + if ('' !== $requestUri && '/' !== $requestUri[0]) { + $requestUri = '/'.$requestUri; + } + + if ($baseUrl && null !== $prefix = $this->getUrlencodedPrefix($requestUri, $baseUrl)) { + // full $baseUrl matches + return $prefix; + } + + if ($baseUrl && null !== $prefix = $this->getUrlencodedPrefix($requestUri, rtrim(\dirname($baseUrl), '/'.\DIRECTORY_SEPARATOR).'/')) { + // directory portion of $baseUrl matches + return rtrim($prefix, '/'.\DIRECTORY_SEPARATOR); + } + + $truncatedRequestUri = $requestUri; + if (false !== $pos = strpos($requestUri, '?')) { + $truncatedRequestUri = substr($requestUri, 0, $pos); + } + + $basename = basename($baseUrl ?? ''); + if (empty($basename) || !strpos(rawurldecode($truncatedRequestUri), $basename)) { + // no match whatsoever; set it blank + return ''; + } + + // If using mod_rewrite or ISAPI_Rewrite strip the script filename + // out of baseUrl. $pos !== 0 makes sure it is not matching a value + // from PATH_INFO or QUERY_STRING + if (\strlen($requestUri) >= \strlen($baseUrl) && (false !== $pos = strpos($requestUri, $baseUrl)) && 0 !== $pos) { + $baseUrl = substr($requestUri, 0, $pos + \strlen($baseUrl)); + } + + return rtrim($baseUrl, '/'.\DIRECTORY_SEPARATOR); + } + + /** + * Prepares the base path. + */ + protected function prepareBasePath(): string + { + $baseUrl = $this->getBaseUrl(); + if (empty($baseUrl)) { + return ''; + } + + $filename = basename($this->server->get('SCRIPT_FILENAME')); + if (basename($baseUrl) === $filename) { + $basePath = \dirname($baseUrl); + } else { + $basePath = $baseUrl; + } + + if ('\\' === \DIRECTORY_SEPARATOR) { + $basePath = str_replace('\\', '/', $basePath); + } + + return rtrim($basePath, '/'); + } + + /** + * Prepares the path info. + */ + protected function preparePathInfo(): string + { + if (null === ($requestUri = $this->getRequestUri())) { + return '/'; + } + + // Remove the query string from REQUEST_URI + if (false !== $pos = strpos($requestUri, '?')) { + $requestUri = substr($requestUri, 0, $pos); + } + if ('' !== $requestUri && '/' !== $requestUri[0]) { + $requestUri = '/'.$requestUri; + } + + if (null === ($baseUrl = $this->getBaseUrlReal())) { + return $requestUri; + } + + $pathInfo = substr($requestUri, \strlen($baseUrl)); + if (false === $pathInfo || '' === $pathInfo || '/' !== $pathInfo[0]) { + return '/'.$pathInfo; + } + + return $pathInfo; + } + + /** + * Initializes HTTP request formats. + * + * @return void + */ + protected static function initializeFormats() + { + static::$formats = [ + 'html' => ['text/html', 'application/xhtml+xml'], + 'txt' => ['text/plain'], + 'js' => ['application/javascript', 'application/x-javascript', 'text/javascript'], + 'css' => ['text/css'], + 'json' => ['application/json', 'application/x-json'], + 'jsonld' => ['application/ld+json'], + 'xml' => ['text/xml', 'application/xml', 'application/x-xml'], + 'rdf' => ['application/rdf+xml'], + 'atom' => ['application/atom+xml'], + 'rss' => ['application/rss+xml'], + 'form' => ['application/x-www-form-urlencoded', 'multipart/form-data'], + ]; + } + + private function setPhpDefaultLocale(string $locale): void + { + // if either the class Locale doesn't exist, or an exception is thrown when + // setting the default locale, the intl module is not installed, and + // the call can be ignored: + try { + if (class_exists(\Locale::class, false)) { + \Locale::setDefault($locale); + } + } catch (\Exception) { + } + } + + /** + * Returns the prefix as encoded in the string when the string starts with + * the given prefix, null otherwise. + */ + private function getUrlencodedPrefix(string $string, string $prefix): ?string + { + if ($this->isIisRewrite()) { + // ISS with UrlRewriteModule might report SCRIPT_NAME/PHP_SELF with wrong case + // see https://github.com/php/php-src/issues/11981 + if (0 !== stripos(rawurldecode($string), $prefix)) { + return null; + } + } elseif (!str_starts_with(rawurldecode($string), $prefix)) { + return null; + } + + $len = \strlen($prefix); + + if (preg_match(\sprintf('#^(%%[[:xdigit:]]{2}|.){%d}#', $len), $string, $match)) { + return $match[0]; + } + + return null; + } + + private static function createRequestFromFactory(array $query = [], array $request = [], array $attributes = [], array $cookies = [], array $files = [], array $server = [], $content = null): static + { + if (self::$requestFactory) { + $request = (self::$requestFactory)($query, $request, $attributes, $cookies, $files, $server, $content); + + if (!$request instanceof self) { + throw new \LogicException('The Request factory must return an instance of Symfony\Component\HttpFoundation\Request.'); + } + + return $request; + } + + return new static($query, $request, $attributes, $cookies, $files, $server, $content); + } + + /** + * Indicates whether this request originated from a trusted proxy. + * + * This can be useful to determine whether or not to trust the + * contents of a proxy-specific header. + */ + public function isFromTrustedProxy(): bool + { + return self::$trustedProxies && IpUtils::checkIp($this->server->get('REMOTE_ADDR', ''), self::$trustedProxies); + } + + /** + * This method is rather heavy because it splits and merges headers, and it's called by many other methods such as + * getPort(), isSecure(), getHost(), getClientIps(), getBaseUrl() etc. Thus, we try to cache the results for + * best performance. + */ + private function getTrustedValues(int $type, ?string $ip = null): array + { + $cacheKey = $type."\0".((self::$trustedHeaderSet & $type) ? $this->headers->get(self::TRUSTED_HEADERS[$type]) : ''); + $cacheKey .= "\0".$ip."\0".$this->headers->get(self::TRUSTED_HEADERS[self::HEADER_FORWARDED]); + + if (isset($this->trustedValuesCache[$cacheKey])) { + return $this->trustedValuesCache[$cacheKey]; + } + + $clientValues = []; + $forwardedValues = []; + + if ((self::$trustedHeaderSet & $type) && $this->headers->has(self::TRUSTED_HEADERS[$type])) { + foreach (explode(',', $this->headers->get(self::TRUSTED_HEADERS[$type])) as $v) { + $clientValues[] = (self::HEADER_X_FORWARDED_PORT === $type ? '0.0.0.0:' : '').trim($v); + } + } + + if ((self::$trustedHeaderSet & self::HEADER_FORWARDED) && (isset(self::FORWARDED_PARAMS[$type])) && $this->headers->has(self::TRUSTED_HEADERS[self::HEADER_FORWARDED])) { + $forwarded = $this->headers->get(self::TRUSTED_HEADERS[self::HEADER_FORWARDED]); + $parts = HeaderUtils::split($forwarded, ',;='); + $param = self::FORWARDED_PARAMS[$type]; + foreach ($parts as $subParts) { + if (null === $v = HeaderUtils::combine($subParts)[$param] ?? null) { + continue; + } + if (self::HEADER_X_FORWARDED_PORT === $type) { + if (str_ends_with($v, ']') || false === $v = strrchr($v, ':')) { + $v = $this->isSecure() ? ':443' : ':80'; + } + $v = '0.0.0.0'.$v; + } + $forwardedValues[] = $v; + } + } + + if (null !== $ip) { + $clientValues = $this->normalizeAndFilterClientIps($clientValues, $ip); + $forwardedValues = $this->normalizeAndFilterClientIps($forwardedValues, $ip); + } + + if ($forwardedValues === $clientValues || !$clientValues) { + return $this->trustedValuesCache[$cacheKey] = $forwardedValues; + } + + if (!$forwardedValues) { + return $this->trustedValuesCache[$cacheKey] = $clientValues; + } + + if (!$this->isForwardedValid) { + return $this->trustedValuesCache[$cacheKey] = null !== $ip ? ['0.0.0.0', $ip] : []; + } + $this->isForwardedValid = false; + + throw new ConflictingHeadersException(\sprintf('The request has both a trusted "%s" header and a trusted "%s" header, conflicting with each other. You should either configure your proxy to remove one of them, or configure your project to distrust the offending one.', self::TRUSTED_HEADERS[self::HEADER_FORWARDED], self::TRUSTED_HEADERS[$type])); + } + + private function normalizeAndFilterClientIps(array $clientIps, string $ip): array + { + if (!$clientIps) { + return []; + } + $clientIps[] = $ip; // Complete the IP chain with the IP the request actually came from + $firstTrustedIp = null; + + foreach ($clientIps as $key => $clientIp) { + if (strpos($clientIp, '.')) { + // Strip :port from IPv4 addresses. This is allowed in Forwarded + // and may occur in X-Forwarded-For. + $i = strpos($clientIp, ':'); + if ($i) { + $clientIps[$key] = $clientIp = substr($clientIp, 0, $i); + } + } elseif (str_starts_with($clientIp, '[')) { + // Strip brackets and :port from IPv6 addresses. + $i = strpos($clientIp, ']', 1); + $clientIps[$key] = $clientIp = substr($clientIp, 1, $i - 1); + } + + if (!filter_var($clientIp, \FILTER_VALIDATE_IP)) { + unset($clientIps[$key]); + + continue; + } + + if (IpUtils::checkIp($clientIp, self::$trustedProxies)) { + unset($clientIps[$key]); + + // Fallback to this when the client IP falls into the range of trusted proxies + $firstTrustedIp ??= $clientIp; + } + } + + // Now the IP chain contains only untrusted proxies and the client IP + return $clientIps ? array_reverse($clientIps) : [$firstTrustedIp]; + } + + /** + * Is this IIS with UrlRewriteModule? + * + * This method consumes, caches and removed the IIS_WasUrlRewritten env var, + * so we don't inherit it to sub-requests. + */ + private function isIisRewrite(): bool + { + if (1 === $this->server->getInt('IIS_WasUrlRewritten')) { + $this->isIisRewrite = true; + $this->server->remove('IIS_WasUrlRewritten'); + } + + return $this->isIisRewrite; + } + + /** + * See https://url.spec.whatwg.org/. + */ + private static function isHostValid(string $host): bool + { + if ('[' === $host[0]) { + return ']' === $host[-1] && filter_var(substr($host, 1, -1), \FILTER_VALIDATE_IP, \FILTER_FLAG_IPV6); + } + + if (preg_match('/\.[0-9]++\.?$/D', $host)) { + return null !== filter_var($host, \FILTER_VALIDATE_IP, \FILTER_FLAG_IPV4 | \FILTER_NULL_ON_FAILURE); + } + + // use preg_replace() instead of preg_match() to prevent DoS attacks with long host names + return '' === preg_replace('/[-a-zA-Z0-9_]++\.?/', '', $host); + } +} diff --git a/3rdparty/symfony/http-foundation/RequestMatcher.php b/3rdparty/symfony/http-foundation/RequestMatcher.php new file mode 100644 index 00000000..b3ca3715 --- /dev/null +++ b/3rdparty/symfony/http-foundation/RequestMatcher.php @@ -0,0 +1,200 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Component\HttpFoundation; + +trigger_deprecation('symfony/http-foundation', '6.2', 'The "%s" class is deprecated, use "%s" instead.', RequestMatcher::class, ChainRequestMatcher::class); + +/** + * RequestMatcher compares a pre-defined set of checks against a Request instance. + * + * @author Fabien Potencier + * + * @deprecated since Symfony 6.2, use ChainRequestMatcher instead + */ +class RequestMatcher implements RequestMatcherInterface +{ + private ?string $path = null; + private ?string $host = null; + private ?int $port = null; + + /** + * @var string[] + */ + private array $methods = []; + + /** + * @var string[] + */ + private array $ips = []; + + /** + * @var string[] + */ + private array $attributes = []; + + /** + * @var string[] + */ + private array $schemes = []; + + /** + * @param string|string[]|null $methods + * @param string|string[]|null $ips + * @param string|string[]|null $schemes + */ + public function __construct(?string $path = null, ?string $host = null, string|array|null $methods = null, string|array|null $ips = null, array $attributes = [], string|array|null $schemes = null, ?int $port = null) + { + $this->matchPath($path); + $this->matchHost($host); + $this->matchMethod($methods); + $this->matchIps($ips); + $this->matchScheme($schemes); + $this->matchPort($port); + + foreach ($attributes as $k => $v) { + $this->matchAttribute($k, $v); + } + } + + /** + * Adds a check for the HTTP scheme. + * + * @param string|string[]|null $scheme An HTTP scheme or an array of HTTP schemes + * + * @return void + */ + public function matchScheme(string|array|null $scheme) + { + $this->schemes = null !== $scheme ? array_map('strtolower', (array) $scheme) : []; + } + + /** + * Adds a check for the URL host name. + * + * @return void + */ + public function matchHost(?string $regexp) + { + $this->host = $regexp; + } + + /** + * Adds a check for the URL port. + * + * @param int|null $port The port number to connect to + * + * @return void + */ + public function matchPort(?int $port) + { + $this->port = $port; + } + + /** + * Adds a check for the URL path info. + * + * @return void + */ + public function matchPath(?string $regexp) + { + $this->path = $regexp; + } + + /** + * Adds a check for the client IP. + * + * @param string $ip A specific IP address or a range specified using IP/netmask like 192.168.1.0/24 + * + * @return void + */ + public function matchIp(string $ip) + { + $this->matchIps($ip); + } + + /** + * Adds a check for the client IP. + * + * @param string|string[]|null $ips A specific IP address or a range specified using IP/netmask like 192.168.1.0/24 + * + * @return void + */ + public function matchIps(string|array|null $ips) + { + $ips = null !== $ips ? (array) $ips : []; + + $this->ips = array_reduce($ips, static fn (array $ips, string $ip) => array_merge($ips, preg_split('/\s*,\s*/', $ip)), []); + } + + /** + * Adds a check for the HTTP method. + * + * @param string|string[]|null $method An HTTP method or an array of HTTP methods + * + * @return void + */ + public function matchMethod(string|array|null $method) + { + $this->methods = null !== $method ? array_map('strtoupper', (array) $method) : []; + } + + /** + * Adds a check for request attribute. + * + * @return void + */ + public function matchAttribute(string $key, string $regexp) + { + $this->attributes[$key] = $regexp; + } + + public function matches(Request $request): bool + { + if ($this->schemes && !\in_array($request->getScheme(), $this->schemes, true)) { + return false; + } + + if ($this->methods && !\in_array($request->getMethod(), $this->methods, true)) { + return false; + } + + foreach ($this->attributes as $key => $pattern) { + $requestAttribute = $request->attributes->get($key); + if (!\is_string($requestAttribute)) { + return false; + } + if (!preg_match('{'.$pattern.'}', $requestAttribute)) { + return false; + } + } + + if (null !== $this->path && !preg_match('{'.$this->path.'}', rawurldecode($request->getPathInfo()))) { + return false; + } + + if (null !== $this->host && !preg_match('{'.$this->host.'}i', $request->getHost())) { + return false; + } + + if (null !== $this->port && 0 < $this->port && $request->getPort() !== $this->port) { + return false; + } + + if (IpUtils::checkIp($request->getClientIp() ?? '', $this->ips)) { + return true; + } + + // Note to future implementors: add additional checks above the + // foreach above or else your check might not be run! + return 0 === \count($this->ips); + } +} diff --git a/3rdparty/symfony/http-foundation/RequestMatcher/AttributesRequestMatcher.php b/3rdparty/symfony/http-foundation/RequestMatcher/AttributesRequestMatcher.php new file mode 100644 index 00000000..09d6f49d --- /dev/null +++ b/3rdparty/symfony/http-foundation/RequestMatcher/AttributesRequestMatcher.php @@ -0,0 +1,45 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Component\HttpFoundation\RequestMatcher; + +use Symfony\Component\HttpFoundation\Request; +use Symfony\Component\HttpFoundation\RequestMatcherInterface; + +/** + * Checks the Request attributes matches all regular expressions. + * + * @author Fabien Potencier + */ +class AttributesRequestMatcher implements RequestMatcherInterface +{ + /** + * @param array $regexps + */ + public function __construct(private array $regexps) + { + } + + public function matches(Request $request): bool + { + foreach ($this->regexps as $key => $regexp) { + $attribute = $request->attributes->get($key); + if (!\is_string($attribute)) { + return false; + } + if (!preg_match('{'.$regexp.'}', $attribute)) { + return false; + } + } + + return true; + } +} diff --git a/3rdparty/symfony/http-foundation/RequestMatcher/ExpressionRequestMatcher.php b/3rdparty/symfony/http-foundation/RequestMatcher/ExpressionRequestMatcher.php new file mode 100644 index 00000000..935853f1 --- /dev/null +++ b/3rdparty/symfony/http-foundation/RequestMatcher/ExpressionRequestMatcher.php @@ -0,0 +1,43 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Component\HttpFoundation\RequestMatcher; + +use Symfony\Component\ExpressionLanguage\Expression; +use Symfony\Component\ExpressionLanguage\ExpressionLanguage; +use Symfony\Component\HttpFoundation\Request; +use Symfony\Component\HttpFoundation\RequestMatcherInterface; + +/** + * ExpressionRequestMatcher uses an expression to match a Request. + * + * @author Fabien Potencier + */ +class ExpressionRequestMatcher implements RequestMatcherInterface +{ + public function __construct( + private ExpressionLanguage $language, + private Expression|string $expression, + ) { + } + + public function matches(Request $request): bool + { + return $this->language->evaluate($this->expression, [ + 'request' => $request, + 'method' => $request->getMethod(), + 'path' => rawurldecode($request->getPathInfo()), + 'host' => $request->getHost(), + 'ip' => $request->getClientIp(), + 'attributes' => $request->attributes->all(), + ]); + } +} diff --git a/3rdparty/symfony/http-foundation/RequestMatcher/HostRequestMatcher.php b/3rdparty/symfony/http-foundation/RequestMatcher/HostRequestMatcher.php new file mode 100644 index 00000000..2836759c --- /dev/null +++ b/3rdparty/symfony/http-foundation/RequestMatcher/HostRequestMatcher.php @@ -0,0 +1,32 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Component\HttpFoundation\RequestMatcher; + +use Symfony\Component\HttpFoundation\Request; +use Symfony\Component\HttpFoundation\RequestMatcherInterface; + +/** + * Checks the Request URL host name matches a regular expression. + * + * @author Fabien Potencier + */ +class HostRequestMatcher implements RequestMatcherInterface +{ + public function __construct(private string $regexp) + { + } + + public function matches(Request $request): bool + { + return preg_match('{'.$this->regexp.'}i', $request->getHost()); + } +} diff --git a/3rdparty/symfony/http-foundation/RequestMatcher/IpsRequestMatcher.php b/3rdparty/symfony/http-foundation/RequestMatcher/IpsRequestMatcher.php new file mode 100644 index 00000000..333612e2 --- /dev/null +++ b/3rdparty/symfony/http-foundation/RequestMatcher/IpsRequestMatcher.php @@ -0,0 +1,44 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Component\HttpFoundation\RequestMatcher; + +use Symfony\Component\HttpFoundation\IpUtils; +use Symfony\Component\HttpFoundation\Request; +use Symfony\Component\HttpFoundation\RequestMatcherInterface; + +/** + * Checks the client IP of a Request. + * + * @author Fabien Potencier + */ +class IpsRequestMatcher implements RequestMatcherInterface +{ + private array $ips; + + /** + * @param string[]|string $ips A specific IP address or a range specified using IP/netmask like 192.168.1.0/24 + * Strings can contain a comma-delimited list of IPs/ranges + */ + public function __construct(array|string $ips) + { + $this->ips = array_reduce((array) $ips, static fn (array $ips, string $ip) => array_merge($ips, preg_split('/\s*,\s*/', $ip)), []); + } + + public function matches(Request $request): bool + { + if (!$this->ips) { + return true; + } + + return IpUtils::checkIp($request->getClientIp() ?? '', $this->ips); + } +} diff --git a/3rdparty/symfony/http-foundation/RequestMatcher/IsJsonRequestMatcher.php b/3rdparty/symfony/http-foundation/RequestMatcher/IsJsonRequestMatcher.php new file mode 100644 index 00000000..875f992b --- /dev/null +++ b/3rdparty/symfony/http-foundation/RequestMatcher/IsJsonRequestMatcher.php @@ -0,0 +1,28 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Component\HttpFoundation\RequestMatcher; + +use Symfony\Component\HttpFoundation\Request; +use Symfony\Component\HttpFoundation\RequestMatcherInterface; + +/** + * Checks the Request content is valid JSON. + * + * @author Fabien Potencier + */ +class IsJsonRequestMatcher implements RequestMatcherInterface +{ + public function matches(Request $request): bool + { + return json_validate($request->getContent()); + } +} diff --git a/3rdparty/symfony/http-foundation/RequestMatcher/MethodRequestMatcher.php b/3rdparty/symfony/http-foundation/RequestMatcher/MethodRequestMatcher.php new file mode 100644 index 00000000..b37f6e3c --- /dev/null +++ b/3rdparty/symfony/http-foundation/RequestMatcher/MethodRequestMatcher.php @@ -0,0 +1,46 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Component\HttpFoundation\RequestMatcher; + +use Symfony\Component\HttpFoundation\Request; +use Symfony\Component\HttpFoundation\RequestMatcherInterface; + +/** + * Checks the HTTP method of a Request. + * + * @author Fabien Potencier + */ +class MethodRequestMatcher implements RequestMatcherInterface +{ + /** + * @var string[] + */ + private array $methods = []; + + /** + * @param string[]|string $methods An HTTP method or an array of HTTP methods + * Strings can contain a comma-delimited list of methods + */ + public function __construct(array|string $methods) + { + $this->methods = array_reduce(array_map('strtoupper', (array) $methods), static fn (array $methods, string $method) => array_merge($methods, preg_split('/\s*,\s*/', $method)), []); + } + + public function matches(Request $request): bool + { + if (!$this->methods) { + return true; + } + + return \in_array($request->getMethod(), $this->methods, true); + } +} diff --git a/3rdparty/symfony/http-foundation/RequestMatcher/PathRequestMatcher.php b/3rdparty/symfony/http-foundation/RequestMatcher/PathRequestMatcher.php new file mode 100644 index 00000000..c7c7a02c --- /dev/null +++ b/3rdparty/symfony/http-foundation/RequestMatcher/PathRequestMatcher.php @@ -0,0 +1,32 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Component\HttpFoundation\RequestMatcher; + +use Symfony\Component\HttpFoundation\Request; +use Symfony\Component\HttpFoundation\RequestMatcherInterface; + +/** + * Checks the Request URL path info matches a regular expression. + * + * @author Fabien Potencier + */ +class PathRequestMatcher implements RequestMatcherInterface +{ + public function __construct(private string $regexp) + { + } + + public function matches(Request $request): bool + { + return preg_match('{'.$this->regexp.'}', rawurldecode($request->getPathInfo())); + } +} diff --git a/3rdparty/symfony/http-foundation/RequestMatcher/PortRequestMatcher.php b/3rdparty/symfony/http-foundation/RequestMatcher/PortRequestMatcher.php new file mode 100644 index 00000000..5a01ce95 --- /dev/null +++ b/3rdparty/symfony/http-foundation/RequestMatcher/PortRequestMatcher.php @@ -0,0 +1,32 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Component\HttpFoundation\RequestMatcher; + +use Symfony\Component\HttpFoundation\Request; +use Symfony\Component\HttpFoundation\RequestMatcherInterface; + +/** + * Checks the HTTP port of a Request. + * + * @author Fabien Potencier + */ +class PortRequestMatcher implements RequestMatcherInterface +{ + public function __construct(private int $port) + { + } + + public function matches(Request $request): bool + { + return $request->getPort() === $this->port; + } +} diff --git a/3rdparty/symfony/http-foundation/RequestMatcher/SchemeRequestMatcher.php b/3rdparty/symfony/http-foundation/RequestMatcher/SchemeRequestMatcher.php new file mode 100644 index 00000000..9c9cd58b --- /dev/null +++ b/3rdparty/symfony/http-foundation/RequestMatcher/SchemeRequestMatcher.php @@ -0,0 +1,46 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Component\HttpFoundation\RequestMatcher; + +use Symfony\Component\HttpFoundation\Request; +use Symfony\Component\HttpFoundation\RequestMatcherInterface; + +/** + * Checks the HTTP scheme of a Request. + * + * @author Fabien Potencier + */ +class SchemeRequestMatcher implements RequestMatcherInterface +{ + /** + * @var string[] + */ + private array $schemes; + + /** + * @param string[]|string $schemes A scheme or a list of schemes + * Strings can contain a comma-delimited list of schemes + */ + public function __construct(array|string $schemes) + { + $this->schemes = array_reduce(array_map('strtolower', (array) $schemes), static fn (array $schemes, string $scheme) => array_merge($schemes, preg_split('/\s*,\s*/', $scheme)), []); + } + + public function matches(Request $request): bool + { + if (!$this->schemes) { + return true; + } + + return \in_array($request->getScheme(), $this->schemes, true); + } +} diff --git a/3rdparty/symfony/http-foundation/RequestMatcherInterface.php b/3rdparty/symfony/http-foundation/RequestMatcherInterface.php new file mode 100644 index 00000000..6dcc3e0f --- /dev/null +++ b/3rdparty/symfony/http-foundation/RequestMatcherInterface.php @@ -0,0 +1,25 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Component\HttpFoundation; + +/** + * RequestMatcherInterface is an interface for strategies to match a Request. + * + * @author Fabien Potencier + */ +interface RequestMatcherInterface +{ + /** + * Decides whether the rule(s) implemented by the strategy matches the supplied request. + */ + public function matches(Request $request): bool; +} diff --git a/3rdparty/symfony/http-foundation/RequestStack.php b/3rdparty/symfony/http-foundation/RequestStack.php new file mode 100644 index 00000000..ca61eef2 --- /dev/null +++ b/3rdparty/symfony/http-foundation/RequestStack.php @@ -0,0 +1,116 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Component\HttpFoundation; + +use Symfony\Component\HttpFoundation\Exception\SessionNotFoundException; +use Symfony\Component\HttpFoundation\Session\SessionInterface; + +/** + * Request stack that controls the lifecycle of requests. + * + * @author Benjamin Eberlei + */ +class RequestStack +{ + /** + * @var Request[] + */ + private array $requests = []; + + /** + * Pushes a Request on the stack. + * + * This method should generally not be called directly as the stack + * management should be taken care of by the application itself. + * + * @return void + */ + public function push(Request $request) + { + $this->requests[] = $request; + } + + /** + * Pops the current request from the stack. + * + * This operation lets the current request go out of scope. + * + * This method should generally not be called directly as the stack + * management should be taken care of by the application itself. + */ + public function pop(): ?Request + { + if (!$this->requests) { + return null; + } + + return array_pop($this->requests); + } + + public function getCurrentRequest(): ?Request + { + return end($this->requests) ?: null; + } + + /** + * Gets the main request. + * + * Be warned that making your code aware of the main request + * might make it un-compatible with other features of your framework + * like ESI support. + */ + public function getMainRequest(): ?Request + { + if (!$this->requests) { + return null; + } + + return $this->requests[0]; + } + + /** + * Returns the parent request of the current. + * + * Be warned that making your code aware of the parent request + * might make it un-compatible with other features of your framework + * like ESI support. + * + * If current Request is the main request, it returns null. + */ + public function getParentRequest(): ?Request + { + $pos = \count($this->requests) - 2; + + return $this->requests[$pos] ?? null; + } + + /** + * Gets the current session. + * + * @throws SessionNotFoundException + */ + public function getSession(): SessionInterface + { + if ((null !== $request = end($this->requests) ?: null) && $request->hasSession()) { + return $request->getSession(); + } + + throw new SessionNotFoundException(); + } + + public function resetRequestFormats(): void + { + static $resetRequestFormats; + $resetRequestFormats ??= \Closure::bind(static fn () => self::$formats = null, null, Request::class); + $resetRequestFormats(); + } +} diff --git a/3rdparty/symfony/http-foundation/Response.php b/3rdparty/symfony/http-foundation/Response.php new file mode 100644 index 00000000..e476e29d --- /dev/null +++ b/3rdparty/symfony/http-foundation/Response.php @@ -0,0 +1,1353 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Component\HttpFoundation; + +// Help opcache.preload discover always-needed symbols +class_exists(ResponseHeaderBag::class); + +/** + * Response represents an HTTP response. + * + * @author Fabien Potencier + */ +class Response +{ + public const HTTP_CONTINUE = 100; + public const HTTP_SWITCHING_PROTOCOLS = 101; + public const HTTP_PROCESSING = 102; // RFC2518 + public const HTTP_EARLY_HINTS = 103; // RFC8297 + public const HTTP_OK = 200; + public const HTTP_CREATED = 201; + public const HTTP_ACCEPTED = 202; + public const HTTP_NON_AUTHORITATIVE_INFORMATION = 203; + public const HTTP_NO_CONTENT = 204; + public const HTTP_RESET_CONTENT = 205; + public const HTTP_PARTIAL_CONTENT = 206; + public const HTTP_MULTI_STATUS = 207; // RFC4918 + public const HTTP_ALREADY_REPORTED = 208; // RFC5842 + public const HTTP_IM_USED = 226; // RFC3229 + public const HTTP_MULTIPLE_CHOICES = 300; + public const HTTP_MOVED_PERMANENTLY = 301; + public const HTTP_FOUND = 302; + public const HTTP_SEE_OTHER = 303; + public const HTTP_NOT_MODIFIED = 304; + public const HTTP_USE_PROXY = 305; + public const HTTP_RESERVED = 306; + public const HTTP_TEMPORARY_REDIRECT = 307; + public const HTTP_PERMANENTLY_REDIRECT = 308; // RFC7238 + public const HTTP_BAD_REQUEST = 400; + public const HTTP_UNAUTHORIZED = 401; + public const HTTP_PAYMENT_REQUIRED = 402; + public const HTTP_FORBIDDEN = 403; + public const HTTP_NOT_FOUND = 404; + public const HTTP_METHOD_NOT_ALLOWED = 405; + public const HTTP_NOT_ACCEPTABLE = 406; + public const HTTP_PROXY_AUTHENTICATION_REQUIRED = 407; + public const HTTP_REQUEST_TIMEOUT = 408; + public const HTTP_CONFLICT = 409; + public const HTTP_GONE = 410; + public const HTTP_LENGTH_REQUIRED = 411; + public const HTTP_PRECONDITION_FAILED = 412; + public const HTTP_REQUEST_ENTITY_TOO_LARGE = 413; + public const HTTP_REQUEST_URI_TOO_LONG = 414; + public const HTTP_UNSUPPORTED_MEDIA_TYPE = 415; + public const HTTP_REQUESTED_RANGE_NOT_SATISFIABLE = 416; + public const HTTP_EXPECTATION_FAILED = 417; + public const HTTP_I_AM_A_TEAPOT = 418; // RFC2324 + public const HTTP_MISDIRECTED_REQUEST = 421; // RFC7540 + public const HTTP_UNPROCESSABLE_ENTITY = 422; // RFC4918 + public const HTTP_LOCKED = 423; // RFC4918 + public const HTTP_FAILED_DEPENDENCY = 424; // RFC4918 + public const HTTP_TOO_EARLY = 425; // RFC-ietf-httpbis-replay-04 + public const HTTP_UPGRADE_REQUIRED = 426; // RFC2817 + public const HTTP_PRECONDITION_REQUIRED = 428; // RFC6585 + public const HTTP_TOO_MANY_REQUESTS = 429; // RFC6585 + public const HTTP_REQUEST_HEADER_FIELDS_TOO_LARGE = 431; // RFC6585 + public const HTTP_UNAVAILABLE_FOR_LEGAL_REASONS = 451; // RFC7725 + public const HTTP_INTERNAL_SERVER_ERROR = 500; + public const HTTP_NOT_IMPLEMENTED = 501; + public const HTTP_BAD_GATEWAY = 502; + public const HTTP_SERVICE_UNAVAILABLE = 503; + public const HTTP_GATEWAY_TIMEOUT = 504; + public const HTTP_VERSION_NOT_SUPPORTED = 505; + public const HTTP_VARIANT_ALSO_NEGOTIATES_EXPERIMENTAL = 506; // RFC2295 + public const HTTP_INSUFFICIENT_STORAGE = 507; // RFC4918 + public const HTTP_LOOP_DETECTED = 508; // RFC5842 + public const HTTP_NOT_EXTENDED = 510; // RFC2774 + public const HTTP_NETWORK_AUTHENTICATION_REQUIRED = 511; // RFC6585 + + /** + * @see https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Cache-Control + */ + private const HTTP_RESPONSE_CACHE_CONTROL_DIRECTIVES = [ + 'must_revalidate' => false, + 'no_cache' => false, + 'no_store' => false, + 'no_transform' => false, + 'public' => false, + 'private' => false, + 'proxy_revalidate' => false, + 'max_age' => true, + 's_maxage' => true, + 'stale_if_error' => true, // RFC5861 + 'stale_while_revalidate' => true, // RFC5861 + 'immutable' => false, + 'last_modified' => true, + 'etag' => true, + ]; + + /** + * @var ResponseHeaderBag + */ + public $headers; + + /** + * @var string + */ + protected $content; + + /** + * @var string + */ + protected $version; + + /** + * @var int + */ + protected $statusCode; + + /** + * @var string + */ + protected $statusText; + + /** + * @var string + */ + protected $charset; + + /** + * Status codes translation table. + * + * The list of codes is complete according to the + * {@link https://www.iana.org/assignments/http-status-codes/http-status-codes.xhtml Hypertext Transfer Protocol (HTTP) Status Code Registry} + * (last updated 2021-10-01). + * + * Unless otherwise noted, the status code is defined in RFC2616. + * + * @var array + */ + public static $statusTexts = [ + 100 => 'Continue', + 101 => 'Switching Protocols', + 102 => 'Processing', // RFC2518 + 103 => 'Early Hints', + 200 => 'OK', + 201 => 'Created', + 202 => 'Accepted', + 203 => 'Non-Authoritative Information', + 204 => 'No Content', + 205 => 'Reset Content', + 206 => 'Partial Content', + 207 => 'Multi-Status', // RFC4918 + 208 => 'Already Reported', // RFC5842 + 226 => 'IM Used', // RFC3229 + 300 => 'Multiple Choices', + 301 => 'Moved Permanently', + 302 => 'Found', + 303 => 'See Other', + 304 => 'Not Modified', + 305 => 'Use Proxy', + 307 => 'Temporary Redirect', + 308 => 'Permanent Redirect', // RFC7238 + 400 => 'Bad Request', + 401 => 'Unauthorized', + 402 => 'Payment Required', + 403 => 'Forbidden', + 404 => 'Not Found', + 405 => 'Method Not Allowed', + 406 => 'Not Acceptable', + 407 => 'Proxy Authentication Required', + 408 => 'Request Timeout', + 409 => 'Conflict', + 410 => 'Gone', + 411 => 'Length Required', + 412 => 'Precondition Failed', + 413 => 'Content Too Large', // RFC-ietf-httpbis-semantics + 414 => 'URI Too Long', + 415 => 'Unsupported Media Type', + 416 => 'Range Not Satisfiable', + 417 => 'Expectation Failed', + 418 => 'I\'m a teapot', // RFC2324 + 421 => 'Misdirected Request', // RFC7540 + 422 => 'Unprocessable Content', // RFC-ietf-httpbis-semantics + 423 => 'Locked', // RFC4918 + 424 => 'Failed Dependency', // RFC4918 + 425 => 'Too Early', // RFC-ietf-httpbis-replay-04 + 426 => 'Upgrade Required', // RFC2817 + 428 => 'Precondition Required', // RFC6585 + 429 => 'Too Many Requests', // RFC6585 + 431 => 'Request Header Fields Too Large', // RFC6585 + 451 => 'Unavailable For Legal Reasons', // RFC7725 + 500 => 'Internal Server Error', + 501 => 'Not Implemented', + 502 => 'Bad Gateway', + 503 => 'Service Unavailable', + 504 => 'Gateway Timeout', + 505 => 'HTTP Version Not Supported', + 506 => 'Variant Also Negotiates', // RFC2295 + 507 => 'Insufficient Storage', // RFC4918 + 508 => 'Loop Detected', // RFC5842 + 510 => 'Not Extended', // RFC2774 + 511 => 'Network Authentication Required', // RFC6585 + ]; + + /** + * Tracks headers already sent in informational responses. + */ + private array $sentHeaders; + + /** + * @param int $status The HTTP status code (200 "OK" by default) + * + * @throws \InvalidArgumentException When the HTTP status code is not valid + */ + public function __construct(?string $content = '', int $status = 200, array $headers = []) + { + $this->headers = new ResponseHeaderBag($headers); + $this->setContent($content); + $this->setStatusCode($status); + $this->setProtocolVersion('1.0'); + } + + /** + * Returns the Response as an HTTP string. + * + * The string representation of the Response is the same as the + * one that will be sent to the client only if the prepare() method + * has been called before. + * + * @see prepare() + */ + public function __toString(): string + { + return + \sprintf('HTTP/%s %s %s', $this->version, $this->statusCode, $this->statusText)."\r\n". + $this->headers."\r\n". + $this->getContent(); + } + + /** + * Clones the current Response instance. + */ + public function __clone() + { + $this->headers = clone $this->headers; + } + + /** + * Prepares the Response before it is sent to the client. + * + * This method tweaks the Response to ensure that it is + * compliant with RFC 2616. Most of the changes are based on + * the Request that is "associated" with this Response. + * + * @return $this + */ + public function prepare(Request $request): static + { + $headers = $this->headers; + + if ($this->isInformational() || $this->isEmpty()) { + $this->setContent(null); + $headers->remove('Content-Type'); + $headers->remove('Content-Length'); + // prevent PHP from sending the Content-Type header based on default_mimetype + ini_set('default_mimetype', ''); + } else { + // Content-type based on the Request + if (!$headers->has('Content-Type')) { + $format = $request->getRequestFormat(null); + if (null !== $format && $mimeType = $request->getMimeType($format)) { + $headers->set('Content-Type', $mimeType); + } + } + + // Fix Content-Type + $charset = $this->charset ?: 'UTF-8'; + if (!$headers->has('Content-Type')) { + $headers->set('Content-Type', 'text/html; charset='.$charset); + } elseif (0 === stripos($headers->get('Content-Type') ?? '', 'text/') && false === stripos($headers->get('Content-Type') ?? '', 'charset')) { + // add the charset + $headers->set('Content-Type', $headers->get('Content-Type').'; charset='.$charset); + } + + // Fix Content-Length + if ($headers->has('Transfer-Encoding')) { + $headers->remove('Content-Length'); + } + + if ($request->isMethod('HEAD')) { + // cf. RFC2616 14.13 + $length = $headers->get('Content-Length'); + $this->setContent(null); + if ($length) { + $headers->set('Content-Length', $length); + } + } + } + + // Fix protocol + if ('HTTP/1.0' != $request->server->get('SERVER_PROTOCOL')) { + $this->setProtocolVersion('1.1'); + } + + // Check if we need to send extra expire info headers + if ('1.0' == $this->getProtocolVersion() && str_contains($headers->get('Cache-Control', ''), 'no-cache')) { + $headers->set('pragma', 'no-cache'); + $headers->set('expires', -1); + } + + $this->ensureIEOverSSLCompatibility($request); + + if ($request->isSecure()) { + foreach ($headers->getCookies() as $cookie) { + $cookie->setSecureDefault(true); + } + } + + return $this; + } + + /** + * Sends HTTP headers. + * + * @param positive-int|null $statusCode The status code to use, override the statusCode property if set and not null + * + * @return $this + */ + public function sendHeaders(/* int $statusCode = null */): static + { + // headers have already been sent by the developer + if (headers_sent()) { + return $this; + } + + $statusCode = \func_num_args() > 0 ? func_get_arg(0) : null; + $informationalResponse = $statusCode >= 100 && $statusCode < 200; + if ($informationalResponse && !\function_exists('headers_send')) { + // skip informational responses if not supported by the SAPI + return $this; + } + + // headers + foreach ($this->headers->allPreserveCaseWithoutCookies() as $name => $values) { + $newValues = $values; + $replace = false; + + // As recommended by RFC 8297, PHP automatically copies headers from previous 103 responses, we need to deal with that if headers changed + $previousValues = $this->sentHeaders[$name] ?? null; + if ($previousValues === $values) { + // Header already sent in a previous response, it will be automatically copied in this response by PHP + continue; + } + + $replace = 0 === strcasecmp($name, 'Content-Type'); + + if (null !== $previousValues && array_diff($previousValues, $values)) { + header_remove($name); + $previousValues = null; + } + + $newValues = null === $previousValues ? $values : array_diff($values, $previousValues); + + foreach ($newValues as $value) { + header($name.': '.$value, $replace, $this->statusCode); + } + + if ($informationalResponse) { + $this->sentHeaders[$name] = $values; + } + } + + // cookies + foreach ($this->headers->getCookies() as $cookie) { + header('Set-Cookie: '.$cookie, false, $this->statusCode); + } + + if ($informationalResponse) { + headers_send($statusCode); + + return $this; + } + + $statusCode ??= $this->statusCode; + + // status + header(\sprintf('HTTP/%s %s %s', $this->version, $statusCode, $this->statusText), true, $statusCode); + + return $this; + } + + /** + * Sends content for the current web response. + * + * @return $this + */ + public function sendContent(): static + { + echo $this->content; + + return $this; + } + + /** + * Sends HTTP headers and content. + * + * @param bool $flush Whether output buffers should be flushed + * + * @return $this + */ + public function send(/* bool $flush = true */): static + { + $this->sendHeaders(); + $this->sendContent(); + + $flush = 1 <= \func_num_args() ? func_get_arg(0) : true; + if (!$flush) { + return $this; + } + + if (\function_exists('fastcgi_finish_request')) { + fastcgi_finish_request(); + } elseif (\function_exists('litespeed_finish_request')) { + litespeed_finish_request(); + } elseif (!\in_array(\PHP_SAPI, ['cli', 'phpdbg', 'embed'], true)) { + static::closeOutputBuffers(0, true); + flush(); + } + + return $this; + } + + /** + * Sets the response content. + * + * @return $this + */ + public function setContent(?string $content): static + { + $this->content = $content ?? ''; + + return $this; + } + + /** + * Gets the current response content. + */ + public function getContent(): string|false + { + return $this->content; + } + + /** + * Sets the HTTP protocol version (1.0 or 1.1). + * + * @return $this + * + * @final + */ + public function setProtocolVersion(string $version): static + { + $this->version = $version; + + return $this; + } + + /** + * Gets the HTTP protocol version. + * + * @final + */ + public function getProtocolVersion(): string + { + return $this->version; + } + + /** + * Sets the response status code. + * + * If the status text is null it will be automatically populated for the known + * status codes and left empty otherwise. + * + * @return $this + * + * @throws \InvalidArgumentException When the HTTP status code is not valid + * + * @final + */ + public function setStatusCode(int $code, ?string $text = null): static + { + $this->statusCode = $code; + if ($this->isInvalid()) { + throw new \InvalidArgumentException(\sprintf('The HTTP status code "%s" is not valid.', $code)); + } + + if (null === $text) { + $this->statusText = self::$statusTexts[$code] ?? 'unknown status'; + + return $this; + } + + $this->statusText = $text; + + return $this; + } + + /** + * Retrieves the status code for the current web response. + * + * @final + */ + public function getStatusCode(): int + { + return $this->statusCode; + } + + /** + * Sets the response charset. + * + * @return $this + * + * @final + */ + public function setCharset(string $charset): static + { + $this->charset = $charset; + + return $this; + } + + /** + * Retrieves the response charset. + * + * @final + */ + public function getCharset(): ?string + { + return $this->charset; + } + + /** + * Returns true if the response may safely be kept in a shared (surrogate) cache. + * + * Responses marked "private" with an explicit Cache-Control directive are + * considered uncacheable. + * + * Responses with neither a freshness lifetime (Expires, max-age) nor cache + * validator (Last-Modified, ETag) are considered uncacheable because there is + * no way to tell when or how to remove them from the cache. + * + * Note that RFC 7231 and RFC 7234 possibly allow for a more permissive implementation, + * for example "status codes that are defined as cacheable by default [...] + * can be reused by a cache with heuristic expiration unless otherwise indicated" + * (https://tools.ietf.org/html/rfc7231#section-6.1) + * + * @final + */ + public function isCacheable(): bool + { + if (!\in_array($this->statusCode, [200, 203, 300, 301, 302, 404, 410])) { + return false; + } + + if ($this->headers->hasCacheControlDirective('no-store') || $this->headers->getCacheControlDirective('private')) { + return false; + } + + return $this->isValidateable() || $this->isFresh(); + } + + /** + * Returns true if the response is "fresh". + * + * Fresh responses may be served from cache without any interaction with the + * origin. A response is considered fresh when it includes a Cache-Control/max-age + * indicator or Expires header and the calculated age is less than the freshness lifetime. + * + * @final + */ + public function isFresh(): bool + { + return $this->getTtl() > 0; + } + + /** + * Returns true if the response includes headers that can be used to validate + * the response with the origin server using a conditional GET request. + * + * @final + */ + public function isValidateable(): bool + { + return $this->headers->has('Last-Modified') || $this->headers->has('ETag'); + } + + /** + * Marks the response as "private". + * + * It makes the response ineligible for serving other clients. + * + * @return $this + * + * @final + */ + public function setPrivate(): static + { + $this->headers->removeCacheControlDirective('public'); + $this->headers->addCacheControlDirective('private'); + + return $this; + } + + /** + * Marks the response as "public". + * + * It makes the response eligible for serving other clients. + * + * @return $this + * + * @final + */ + public function setPublic(): static + { + $this->headers->addCacheControlDirective('public'); + $this->headers->removeCacheControlDirective('private'); + + return $this; + } + + /** + * Marks the response as "immutable". + * + * @return $this + * + * @final + */ + public function setImmutable(bool $immutable = true): static + { + if ($immutable) { + $this->headers->addCacheControlDirective('immutable'); + } else { + $this->headers->removeCacheControlDirective('immutable'); + } + + return $this; + } + + /** + * Returns true if the response is marked as "immutable". + * + * @final + */ + public function isImmutable(): bool + { + return $this->headers->hasCacheControlDirective('immutable'); + } + + /** + * Returns true if the response must be revalidated by shared caches once it has become stale. + * + * This method indicates that the response must not be served stale by a + * cache in any circumstance without first revalidating with the origin. + * When present, the TTL of the response should not be overridden to be + * greater than the value provided by the origin. + * + * @final + */ + public function mustRevalidate(): bool + { + return $this->headers->hasCacheControlDirective('must-revalidate') || $this->headers->hasCacheControlDirective('proxy-revalidate'); + } + + /** + * Returns the Date header as a DateTime instance. + * + * @throws \RuntimeException When the header is not parseable + * + * @final + */ + public function getDate(): ?\DateTimeImmutable + { + return $this->headers->getDate('Date'); + } + + /** + * Sets the Date header. + * + * @return $this + * + * @final + */ + public function setDate(\DateTimeInterface $date): static + { + $date = \DateTimeImmutable::createFromInterface($date); + $date = $date->setTimezone(new \DateTimeZone('UTC')); + $this->headers->set('Date', $date->format('D, d M Y H:i:s').' GMT'); + + return $this; + } + + /** + * Returns the age of the response in seconds. + * + * @final + */ + public function getAge(): int + { + if (null !== $age = $this->headers->get('Age')) { + return (int) $age; + } + + return max(time() - (int) $this->getDate()->format('U'), 0); + } + + /** + * Marks the response stale by setting the Age header to be equal to the maximum age of the response. + * + * @return $this + */ + public function expire(): static + { + if ($this->isFresh()) { + $this->headers->set('Age', $this->getMaxAge()); + $this->headers->remove('Expires'); + } + + return $this; + } + + /** + * Returns the value of the Expires header as a DateTime instance. + * + * @final + */ + public function getExpires(): ?\DateTimeImmutable + { + try { + return $this->headers->getDate('Expires'); + } catch (\RuntimeException) { + // according to RFC 2616 invalid date formats (e.g. "0" and "-1") must be treated as in the past + return \DateTimeImmutable::createFromFormat('U', time() - 172800); + } + } + + /** + * Sets the Expires HTTP header with a DateTime instance. + * + * Passing null as value will remove the header. + * + * @return $this + * + * @final + */ + public function setExpires(?\DateTimeInterface $date = null): static + { + if (1 > \func_num_args()) { + trigger_deprecation('symfony/http-foundation', '6.2', 'Calling "%s()" without any arguments is deprecated, pass null explicitly instead.', __METHOD__); + } + if (null === $date) { + $this->headers->remove('Expires'); + + return $this; + } + + $date = \DateTimeImmutable::createFromInterface($date); + $date = $date->setTimezone(new \DateTimeZone('UTC')); + $this->headers->set('Expires', $date->format('D, d M Y H:i:s').' GMT'); + + return $this; + } + + /** + * Returns the number of seconds after the time specified in the response's Date + * header when the response should no longer be considered fresh. + * + * First, it checks for a s-maxage directive, then a max-age directive, and then it falls + * back on an expires header. It returns null when no maximum age can be established. + * + * @final + */ + public function getMaxAge(): ?int + { + if ($this->headers->hasCacheControlDirective('s-maxage')) { + return (int) $this->headers->getCacheControlDirective('s-maxage'); + } + + if ($this->headers->hasCacheControlDirective('max-age')) { + return (int) $this->headers->getCacheControlDirective('max-age'); + } + + if (null !== $expires = $this->getExpires()) { + $maxAge = (int) $expires->format('U') - (int) $this->getDate()->format('U'); + + return max($maxAge, 0); + } + + return null; + } + + /** + * Sets the number of seconds after which the response should no longer be considered fresh. + * + * This methods sets the Cache-Control max-age directive. + * + * @return $this + * + * @final + */ + public function setMaxAge(int $value): static + { + $this->headers->addCacheControlDirective('max-age', $value); + + return $this; + } + + /** + * Sets the number of seconds after which the response should no longer be returned by shared caches when backend is down. + * + * This method sets the Cache-Control stale-if-error directive. + * + * @return $this + * + * @final + */ + public function setStaleIfError(int $value): static + { + $this->headers->addCacheControlDirective('stale-if-error', $value); + + return $this; + } + + /** + * Sets the number of seconds after which the response should no longer return stale content by shared caches. + * + * This method sets the Cache-Control stale-while-revalidate directive. + * + * @return $this + * + * @final + */ + public function setStaleWhileRevalidate(int $value): static + { + $this->headers->addCacheControlDirective('stale-while-revalidate', $value); + + return $this; + } + + /** + * Sets the number of seconds after which the response should no longer be considered fresh by shared caches. + * + * This methods sets the Cache-Control s-maxage directive. + * + * @return $this + * + * @final + */ + public function setSharedMaxAge(int $value): static + { + $this->setPublic(); + $this->headers->addCacheControlDirective('s-maxage', $value); + + return $this; + } + + /** + * Returns the response's time-to-live in seconds. + * + * It returns null when no freshness information is present in the response. + * + * When the response's TTL is 0, the response may not be served from cache without first + * revalidating with the origin. + * + * @final + */ + public function getTtl(): ?int + { + $maxAge = $this->getMaxAge(); + + return null !== $maxAge ? max($maxAge - $this->getAge(), 0) : null; + } + + /** + * Sets the response's time-to-live for shared caches in seconds. + * + * This method adjusts the Cache-Control/s-maxage directive. + * + * @return $this + * + * @final + */ + public function setTtl(int $seconds): static + { + $this->setSharedMaxAge($this->getAge() + $seconds); + + return $this; + } + + /** + * Sets the response's time-to-live for private/client caches in seconds. + * + * This method adjusts the Cache-Control/max-age directive. + * + * @return $this + * + * @final + */ + public function setClientTtl(int $seconds): static + { + $this->setMaxAge($this->getAge() + $seconds); + + return $this; + } + + /** + * Returns the Last-Modified HTTP header as a DateTime instance. + * + * @throws \RuntimeException When the HTTP header is not parseable + * + * @final + */ + public function getLastModified(): ?\DateTimeImmutable + { + return $this->headers->getDate('Last-Modified'); + } + + /** + * Sets the Last-Modified HTTP header with a DateTime instance. + * + * Passing null as value will remove the header. + * + * @return $this + * + * @final + */ + public function setLastModified(?\DateTimeInterface $date = null): static + { + if (1 > \func_num_args()) { + trigger_deprecation('symfony/http-foundation', '6.2', 'Calling "%s()" without any arguments is deprecated, pass null explicitly instead.', __METHOD__); + } + if (null === $date) { + $this->headers->remove('Last-Modified'); + + return $this; + } + + $date = \DateTimeImmutable::createFromInterface($date); + $date = $date->setTimezone(new \DateTimeZone('UTC')); + $this->headers->set('Last-Modified', $date->format('D, d M Y H:i:s').' GMT'); + + return $this; + } + + /** + * Returns the literal value of the ETag HTTP header. + * + * @final + */ + public function getEtag(): ?string + { + return $this->headers->get('ETag'); + } + + /** + * Sets the ETag value. + * + * @param string|null $etag The ETag unique identifier or null to remove the header + * @param bool $weak Whether you want a weak ETag or not + * + * @return $this + * + * @final + */ + public function setEtag(?string $etag = null, bool $weak = false): static + { + if (1 > \func_num_args()) { + trigger_deprecation('symfony/http-foundation', '6.2', 'Calling "%s()" without any arguments is deprecated, pass null explicitly instead.', __METHOD__); + } + if (null === $etag) { + $this->headers->remove('Etag'); + } else { + if (!str_starts_with($etag, '"')) { + $etag = '"'.$etag.'"'; + } + + $this->headers->set('ETag', (true === $weak ? 'W/' : '').$etag); + } + + return $this; + } + + /** + * Sets the response's cache headers (validation and/or expiration). + * + * Available options are: must_revalidate, no_cache, no_store, no_transform, public, private, proxy_revalidate, max_age, s_maxage, immutable, last_modified and etag. + * + * @return $this + * + * @throws \InvalidArgumentException + * + * @final + */ + public function setCache(array $options): static + { + if ($diff = array_diff(array_keys($options), array_keys(self::HTTP_RESPONSE_CACHE_CONTROL_DIRECTIVES))) { + throw new \InvalidArgumentException(\sprintf('Response does not support the following options: "%s".', implode('", "', $diff))); + } + + if (isset($options['etag'])) { + $this->setEtag($options['etag']); + } + + if (isset($options['last_modified'])) { + $this->setLastModified($options['last_modified']); + } + + if (isset($options['max_age'])) { + $this->setMaxAge($options['max_age']); + } + + if (isset($options['s_maxage'])) { + $this->setSharedMaxAge($options['s_maxage']); + } + + if (isset($options['stale_while_revalidate'])) { + $this->setStaleWhileRevalidate($options['stale_while_revalidate']); + } + + if (isset($options['stale_if_error'])) { + $this->setStaleIfError($options['stale_if_error']); + } + + foreach (self::HTTP_RESPONSE_CACHE_CONTROL_DIRECTIVES as $directive => $hasValue) { + if (!$hasValue && isset($options[$directive])) { + if ($options[$directive]) { + $this->headers->addCacheControlDirective(str_replace('_', '-', $directive)); + } else { + $this->headers->removeCacheControlDirective(str_replace('_', '-', $directive)); + } + } + } + + if (isset($options['public'])) { + if ($options['public']) { + $this->setPublic(); + } else { + $this->setPrivate(); + } + } + + if (isset($options['private'])) { + if ($options['private']) { + $this->setPrivate(); + } else { + $this->setPublic(); + } + } + + return $this; + } + + /** + * Modifies the response so that it conforms to the rules defined for a 304 status code. + * + * This sets the status, removes the body, and discards any headers + * that MUST NOT be included in 304 responses. + * + * @return $this + * + * @see https://tools.ietf.org/html/rfc2616#section-10.3.5 + * + * @final + */ + public function setNotModified(): static + { + $this->setStatusCode(304); + $this->setContent(null); + + // remove headers that MUST NOT be included with 304 Not Modified responses + foreach (['Allow', 'Content-Encoding', 'Content-Language', 'Content-Length', 'Content-MD5', 'Content-Type', 'Last-Modified'] as $header) { + $this->headers->remove($header); + } + + return $this; + } + + /** + * Returns true if the response includes a Vary header. + * + * @final + */ + public function hasVary(): bool + { + return null !== $this->headers->get('Vary'); + } + + /** + * Returns an array of header names given in the Vary header. + * + * @final + */ + public function getVary(): array + { + if (!$vary = $this->headers->all('Vary')) { + return []; + } + + $ret = []; + foreach ($vary as $item) { + $ret[] = preg_split('/[\s,]+/', $item); + } + + return array_merge([], ...$ret); + } + + /** + * Sets the Vary header. + * + * @param bool $replace Whether to replace the actual value or not (true by default) + * + * @return $this + * + * @final + */ + public function setVary(string|array $headers, bool $replace = true): static + { + $this->headers->set('Vary', $headers, $replace); + + return $this; + } + + /** + * Determines if the Response validators (ETag, Last-Modified) match + * a conditional value specified in the Request. + * + * If the Response is not modified, it sets the status code to 304 and + * removes the actual content by calling the setNotModified() method. + * + * @final + */ + public function isNotModified(Request $request): bool + { + if (!$request->isMethodCacheable()) { + return false; + } + + $notModified = false; + $lastModified = $this->headers->get('Last-Modified'); + $modifiedSince = $request->headers->get('If-Modified-Since'); + + if (($ifNoneMatchEtags = $request->getETags()) && (null !== $etag = $this->getEtag())) { + if (0 == strncmp($etag, 'W/', 2)) { + $etag = substr($etag, 2); + } + + // Use weak comparison as per https://tools.ietf.org/html/rfc7232#section-3.2. + foreach ($ifNoneMatchEtags as $ifNoneMatchEtag) { + if (0 == strncmp($ifNoneMatchEtag, 'W/', 2)) { + $ifNoneMatchEtag = substr($ifNoneMatchEtag, 2); + } + + if ($ifNoneMatchEtag === $etag || '*' === $ifNoneMatchEtag) { + $notModified = true; + break; + } + } + } + // Only do If-Modified-Since date comparison when If-None-Match is not present as per https://tools.ietf.org/html/rfc7232#section-3.3. + elseif ($modifiedSince && $lastModified) { + $notModified = strtotime($modifiedSince) >= strtotime($lastModified); + } + + if ($notModified) { + $this->setNotModified(); + } + + return $notModified; + } + + /** + * Is response invalid? + * + * @see https://www.w3.org/Protocols/rfc2616/rfc2616-sec10.html + * + * @final + */ + public function isInvalid(): bool + { + return $this->statusCode < 100 || $this->statusCode >= 600; + } + + /** + * Is response informative? + * + * @final + */ + public function isInformational(): bool + { + return $this->statusCode >= 100 && $this->statusCode < 200; + } + + /** + * Is response successful? + * + * @final + */ + public function isSuccessful(): bool + { + return $this->statusCode >= 200 && $this->statusCode < 300; + } + + /** + * Is the response a redirect? + * + * @final + */ + public function isRedirection(): bool + { + return $this->statusCode >= 300 && $this->statusCode < 400; + } + + /** + * Is there a client error? + * + * @final + */ + public function isClientError(): bool + { + return $this->statusCode >= 400 && $this->statusCode < 500; + } + + /** + * Was there a server side error? + * + * @final + */ + public function isServerError(): bool + { + return $this->statusCode >= 500 && $this->statusCode < 600; + } + + /** + * Is the response OK? + * + * @final + */ + public function isOk(): bool + { + return 200 === $this->statusCode; + } + + /** + * Is the response forbidden? + * + * @final + */ + public function isForbidden(): bool + { + return 403 === $this->statusCode; + } + + /** + * Is the response a not found error? + * + * @final + */ + public function isNotFound(): bool + { + return 404 === $this->statusCode; + } + + /** + * Is the response a redirect of some form? + * + * @final + */ + public function isRedirect(?string $location = null): bool + { + return \in_array($this->statusCode, [201, 301, 302, 303, 307, 308]) && (null === $location ?: $location == $this->headers->get('Location')); + } + + /** + * Is the response empty? + * + * @final + */ + public function isEmpty(): bool + { + return \in_array($this->statusCode, [204, 304]); + } + + /** + * Cleans or flushes output buffers up to target level. + * + * Resulting level can be greater than target level if a non-removable buffer has been encountered. + * + * @final + */ + public static function closeOutputBuffers(int $targetLevel, bool $flush): void + { + $status = ob_get_status(true); + $level = \count($status); + $flags = \PHP_OUTPUT_HANDLER_REMOVABLE | ($flush ? \PHP_OUTPUT_HANDLER_FLUSHABLE : \PHP_OUTPUT_HANDLER_CLEANABLE); + + while ($level-- > $targetLevel && ($s = $status[$level]) && (!isset($s['del']) ? !isset($s['flags']) || ($s['flags'] & $flags) === $flags : $s['del'])) { + if ($flush) { + ob_end_flush(); + } else { + ob_end_clean(); + } + } + } + + /** + * Marks a response as safe according to RFC8674. + * + * @see https://tools.ietf.org/html/rfc8674 + */ + public function setContentSafe(bool $safe = true): void + { + if ($safe) { + $this->headers->set('Preference-Applied', 'safe'); + } elseif ('safe' === $this->headers->get('Preference-Applied')) { + $this->headers->remove('Preference-Applied'); + } + + $this->setVary('Prefer', false); + } + + /** + * Checks if we need to remove Cache-Control for SSL encrypted downloads when using IE < 9. + * + * @see http://support.microsoft.com/kb/323308 + * + * @final + */ + protected function ensureIEOverSSLCompatibility(Request $request): void + { + if (false !== stripos($this->headers->get('Content-Disposition') ?? '', 'attachment') && 1 == preg_match('/MSIE (.*?);/i', $request->server->get('HTTP_USER_AGENT') ?? '', $match) && true === $request->isSecure()) { + if ((int) preg_replace('/(MSIE )(.*?);/', '$2', $match[0]) < 9) { + $this->headers->remove('Cache-Control'); + } + } + } +} diff --git a/3rdparty/symfony/http-foundation/ResponseHeaderBag.php b/3rdparty/symfony/http-foundation/ResponseHeaderBag.php new file mode 100644 index 00000000..562f75c9 --- /dev/null +++ b/3rdparty/symfony/http-foundation/ResponseHeaderBag.php @@ -0,0 +1,292 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Component\HttpFoundation; + +/** + * ResponseHeaderBag is a container for Response HTTP headers. + * + * @author Fabien Potencier + */ +class ResponseHeaderBag extends HeaderBag +{ + public const COOKIES_FLAT = 'flat'; + public const COOKIES_ARRAY = 'array'; + + public const DISPOSITION_ATTACHMENT = 'attachment'; + public const DISPOSITION_INLINE = 'inline'; + + protected $computedCacheControl = []; + protected $cookies = []; + protected $headerNames = []; + + public function __construct(array $headers = []) + { + parent::__construct($headers); + + if (!isset($this->headers['cache-control'])) { + $this->set('Cache-Control', ''); + } + + /* RFC2616 - 14.18 says all Responses need to have a Date */ + if (!isset($this->headers['date'])) { + $this->initDate(); + } + } + + /** + * Returns the headers, with original capitalizations. + */ + public function allPreserveCase(): array + { + $headers = []; + foreach ($this->all() as $name => $value) { + $headers[$this->headerNames[$name] ?? $name] = $value; + } + + return $headers; + } + + /** + * @return array + */ + public function allPreserveCaseWithoutCookies() + { + $headers = $this->allPreserveCase(); + if (isset($this->headerNames['set-cookie'])) { + unset($headers[$this->headerNames['set-cookie']]); + } + + return $headers; + } + + /** + * @return void + */ + public function replace(array $headers = []) + { + $this->headerNames = []; + + parent::replace($headers); + + if (!isset($this->headers['cache-control'])) { + $this->set('Cache-Control', ''); + } + + if (!isset($this->headers['date'])) { + $this->initDate(); + } + } + + public function all(?string $key = null): array + { + $headers = parent::all(); + + if (null !== $key) { + $key = strtr($key, self::UPPER, self::LOWER); + + return 'set-cookie' !== $key ? $headers[$key] ?? [] : array_map('strval', $this->getCookies()); + } + + foreach ($this->getCookies() as $cookie) { + $headers['set-cookie'][] = (string) $cookie; + } + + return $headers; + } + + /** + * @return void + */ + public function set(string $key, string|array|null $values, bool $replace = true) + { + $uniqueKey = strtr($key, self::UPPER, self::LOWER); + + if ('set-cookie' === $uniqueKey) { + if ($replace) { + $this->cookies = []; + } + foreach ((array) $values as $cookie) { + $this->setCookie(Cookie::fromString($cookie)); + } + $this->headerNames[$uniqueKey] = $key; + + return; + } + + $this->headerNames[$uniqueKey] = $key; + + parent::set($key, $values, $replace); + + // ensure the cache-control header has sensible defaults + if (\in_array($uniqueKey, ['cache-control', 'etag', 'last-modified', 'expires'], true) && '' !== $computed = $this->computeCacheControlValue()) { + $this->headers['cache-control'] = [$computed]; + $this->headerNames['cache-control'] = 'Cache-Control'; + $this->computedCacheControl = $this->parseCacheControl($computed); + } + } + + /** + * @return void + */ + public function remove(string $key) + { + $uniqueKey = strtr($key, self::UPPER, self::LOWER); + unset($this->headerNames[$uniqueKey]); + + if ('set-cookie' === $uniqueKey) { + $this->cookies = []; + + return; + } + + parent::remove($key); + + if ('cache-control' === $uniqueKey) { + $this->computedCacheControl = []; + } + + if ('date' === $uniqueKey) { + $this->initDate(); + } + } + + public function hasCacheControlDirective(string $key): bool + { + return \array_key_exists($key, $this->computedCacheControl); + } + + public function getCacheControlDirective(string $key): bool|string|null + { + return $this->computedCacheControl[$key] ?? null; + } + + /** + * @return void + */ + public function setCookie(Cookie $cookie) + { + $this->cookies[$cookie->getDomain() ?? ''][$cookie->getPath()][$cookie->getName()] = $cookie; + $this->headerNames['set-cookie'] = 'Set-Cookie'; + } + + /** + * Removes a cookie from the array, but does not unset it in the browser. + * + * @return void + */ + public function removeCookie(string $name, ?string $path = '/', ?string $domain = null) + { + $path ??= '/'; + + unset($this->cookies[$domain ?? ''][$path][$name]); + + if (empty($this->cookies[$domain ?? ''][$path])) { + unset($this->cookies[$domain ?? ''][$path]); + + if (empty($this->cookies[$domain ?? ''])) { + unset($this->cookies[$domain ?? '']); + } + } + + if (empty($this->cookies)) { + unset($this->headerNames['set-cookie']); + } + } + + /** + * Returns an array with all cookies. + * + * @return Cookie[] + * + * @throws \InvalidArgumentException When the $format is invalid + */ + public function getCookies(string $format = self::COOKIES_FLAT): array + { + if (!\in_array($format, [self::COOKIES_FLAT, self::COOKIES_ARRAY])) { + throw new \InvalidArgumentException(\sprintf('Format "%s" invalid (%s).', $format, implode(', ', [self::COOKIES_FLAT, self::COOKIES_ARRAY]))); + } + + if (self::COOKIES_ARRAY === $format) { + return $this->cookies; + } + + $flattenedCookies = []; + foreach ($this->cookies as $path) { + foreach ($path as $cookies) { + foreach ($cookies as $cookie) { + $flattenedCookies[] = $cookie; + } + } + } + + return $flattenedCookies; + } + + /** + * Clears a cookie in the browser. + * + * @param bool $partitioned + * + * @return void + */ + public function clearCookie(string $name, ?string $path = '/', ?string $domain = null, bool $secure = false, bool $httpOnly = true, ?string $sameSite = null /* , bool $partitioned = false */) + { + $partitioned = 6 < \func_num_args() ? func_get_arg(6) : false; + + $this->setCookie(new Cookie($name, null, 1, $path, $domain, $secure, $httpOnly, false, $sameSite, $partitioned)); + } + + /** + * @see HeaderUtils::makeDisposition() + * + * @return string + */ + public function makeDisposition(string $disposition, string $filename, string $filenameFallback = '') + { + return HeaderUtils::makeDisposition($disposition, $filename, $filenameFallback); + } + + /** + * Returns the calculated value of the cache-control header. + * + * This considers several other headers and calculates or modifies the + * cache-control header to a sensible, conservative value. + */ + protected function computeCacheControlValue(): string + { + if (!$this->cacheControl) { + if ($this->has('Last-Modified') || $this->has('Expires')) { + return 'private, must-revalidate'; // allows for heuristic expiration (RFC 7234 Section 4.2.2) in the case of "Last-Modified" + } + + // conservative by default + return 'no-cache, private'; + } + + $header = $this->getCacheControlHeader(); + if (isset($this->cacheControl['public']) || isset($this->cacheControl['private'])) { + return $header; + } + + // public if s-maxage is defined, private otherwise + if (!isset($this->cacheControl['s-maxage'])) { + return $header.', private'; + } + + return $header; + } + + private function initDate(): void + { + $this->set('Date', gmdate('D, d M Y H:i:s').' GMT'); + } +} diff --git a/3rdparty/symfony/http-foundation/ServerBag.php b/3rdparty/symfony/http-foundation/ServerBag.php new file mode 100644 index 00000000..09fc3866 --- /dev/null +++ b/3rdparty/symfony/http-foundation/ServerBag.php @@ -0,0 +1,97 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Component\HttpFoundation; + +/** + * ServerBag is a container for HTTP headers from the $_SERVER variable. + * + * @author Fabien Potencier + * @author Bulat Shakirzyanov + * @author Robert Kiss + */ +class ServerBag extends ParameterBag +{ + /** + * Gets the HTTP headers. + */ + public function getHeaders(): array + { + $headers = []; + foreach ($this->parameters as $key => $value) { + if (str_starts_with($key, 'HTTP_')) { + $headers[substr($key, 5)] = $value; + } elseif (\in_array($key, ['CONTENT_TYPE', 'CONTENT_LENGTH', 'CONTENT_MD5'], true) && '' !== $value) { + $headers[$key] = $value; + } + } + + if (isset($this->parameters['PHP_AUTH_USER'])) { + $headers['PHP_AUTH_USER'] = $this->parameters['PHP_AUTH_USER']; + $headers['PHP_AUTH_PW'] = $this->parameters['PHP_AUTH_PW'] ?? ''; + } else { + /* + * php-cgi under Apache does not pass HTTP Basic user/pass to PHP by default + * For this workaround to work, add these lines to your .htaccess file: + * RewriteCond %{HTTP:Authorization} .+ + * RewriteRule ^ - [E=HTTP_AUTHORIZATION:%0] + * + * A sample .htaccess file: + * RewriteEngine On + * RewriteCond %{HTTP:Authorization} .+ + * RewriteRule ^ - [E=HTTP_AUTHORIZATION:%0] + * RewriteCond %{REQUEST_FILENAME} !-f + * RewriteRule ^(.*)$ index.php [QSA,L] + */ + + $authorizationHeader = null; + if (isset($this->parameters['HTTP_AUTHORIZATION'])) { + $authorizationHeader = $this->parameters['HTTP_AUTHORIZATION']; + } elseif (isset($this->parameters['REDIRECT_HTTP_AUTHORIZATION'])) { + $authorizationHeader = $this->parameters['REDIRECT_HTTP_AUTHORIZATION']; + } + + if (null !== $authorizationHeader) { + if (0 === stripos($authorizationHeader, 'basic ')) { + // Decode AUTHORIZATION header into PHP_AUTH_USER and PHP_AUTH_PW when authorization header is basic + $exploded = explode(':', base64_decode(substr($authorizationHeader, 6)), 2); + if (2 == \count($exploded)) { + [$headers['PHP_AUTH_USER'], $headers['PHP_AUTH_PW']] = $exploded; + } + } elseif (empty($this->parameters['PHP_AUTH_DIGEST']) && (0 === stripos($authorizationHeader, 'digest '))) { + // In some circumstances PHP_AUTH_DIGEST needs to be set + $headers['PHP_AUTH_DIGEST'] = $authorizationHeader; + $this->parameters['PHP_AUTH_DIGEST'] = $authorizationHeader; + } elseif (0 === stripos($authorizationHeader, 'bearer ')) { + /* + * XXX: Since there is no PHP_AUTH_BEARER in PHP predefined variables, + * I'll just set $headers['AUTHORIZATION'] here. + * https://php.net/reserved.variables.server + */ + $headers['AUTHORIZATION'] = $authorizationHeader; + } + } + } + + if (isset($headers['AUTHORIZATION'])) { + return $headers; + } + + // PHP_AUTH_USER/PHP_AUTH_PW + if (isset($headers['PHP_AUTH_USER'])) { + $headers['AUTHORIZATION'] = 'Basic '.base64_encode($headers['PHP_AUTH_USER'].':'.($headers['PHP_AUTH_PW'] ?? '')); + } elseif (isset($headers['PHP_AUTH_DIGEST'])) { + $headers['AUTHORIZATION'] = $headers['PHP_AUTH_DIGEST']; + } + + return $headers; + } +} diff --git a/3rdparty/symfony/http-foundation/Session/Attribute/AttributeBag.php b/3rdparty/symfony/http-foundation/Session/Attribute/AttributeBag.php new file mode 100644 index 00000000..ad5a6590 --- /dev/null +++ b/3rdparty/symfony/http-foundation/Session/Attribute/AttributeBag.php @@ -0,0 +1,130 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Component\HttpFoundation\Session\Attribute; + +/** + * This class relates to session attribute storage. + * + * @implements \IteratorAggregate + */ +class AttributeBag implements AttributeBagInterface, \IteratorAggregate, \Countable +{ + private string $name = 'attributes'; + private string $storageKey; + + protected $attributes = []; + + /** + * @param string $storageKey The key used to store attributes in the session + */ + public function __construct(string $storageKey = '_sf2_attributes') + { + $this->storageKey = $storageKey; + } + + public function getName(): string + { + return $this->name; + } + + /** + * @return void + */ + public function setName(string $name) + { + $this->name = $name; + } + + /** + * @return void + */ + public function initialize(array &$attributes) + { + $this->attributes = &$attributes; + } + + public function getStorageKey(): string + { + return $this->storageKey; + } + + public function has(string $name): bool + { + return \array_key_exists($name, $this->attributes); + } + + public function get(string $name, mixed $default = null): mixed + { + return \array_key_exists($name, $this->attributes) ? $this->attributes[$name] : $default; + } + + /** + * @return void + */ + public function set(string $name, mixed $value) + { + $this->attributes[$name] = $value; + } + + public function all(): array + { + return $this->attributes; + } + + /** + * @return void + */ + public function replace(array $attributes) + { + $this->attributes = []; + foreach ($attributes as $key => $value) { + $this->set($key, $value); + } + } + + public function remove(string $name): mixed + { + $retval = null; + if (\array_key_exists($name, $this->attributes)) { + $retval = $this->attributes[$name]; + unset($this->attributes[$name]); + } + + return $retval; + } + + public function clear(): mixed + { + $return = $this->attributes; + $this->attributes = []; + + return $return; + } + + /** + * Returns an iterator for attributes. + * + * @return \ArrayIterator + */ + public function getIterator(): \ArrayIterator + { + return new \ArrayIterator($this->attributes); + } + + /** + * Returns the number of attributes. + */ + public function count(): int + { + return \count($this->attributes); + } +} diff --git a/3rdparty/symfony/http-foundation/Session/Attribute/AttributeBagInterface.php b/3rdparty/symfony/http-foundation/Session/Attribute/AttributeBagInterface.php new file mode 100644 index 00000000..e8cd0b5a --- /dev/null +++ b/3rdparty/symfony/http-foundation/Session/Attribute/AttributeBagInterface.php @@ -0,0 +1,58 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Component\HttpFoundation\Session\Attribute; + +use Symfony\Component\HttpFoundation\Session\SessionBagInterface; + +/** + * Attributes store. + * + * @author Drak + */ +interface AttributeBagInterface extends SessionBagInterface +{ + /** + * Checks if an attribute is defined. + */ + public function has(string $name): bool; + + /** + * Returns an attribute. + */ + public function get(string $name, mixed $default = null): mixed; + + /** + * Sets an attribute. + * + * @return void + */ + public function set(string $name, mixed $value); + + /** + * Returns attributes. + * + * @return array + */ + public function all(): array; + + /** + * @return void + */ + public function replace(array $attributes); + + /** + * Removes an attribute. + * + * @return mixed The removed value or null when it does not exist + */ + public function remove(string $name): mixed; +} diff --git a/3rdparty/symfony/http-foundation/Session/Flash/AutoExpireFlashBag.php b/3rdparty/symfony/http-foundation/Session/Flash/AutoExpireFlashBag.php new file mode 100644 index 00000000..80bbeda0 --- /dev/null +++ b/3rdparty/symfony/http-foundation/Session/Flash/AutoExpireFlashBag.php @@ -0,0 +1,137 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Component\HttpFoundation\Session\Flash; + +/** + * AutoExpireFlashBag flash message container. + * + * @author Drak + */ +class AutoExpireFlashBag implements FlashBagInterface +{ + private string $name = 'flashes'; + private array $flashes = ['display' => [], 'new' => []]; + private string $storageKey; + + /** + * @param string $storageKey The key used to store flashes in the session + */ + public function __construct(string $storageKey = '_symfony_flashes') + { + $this->storageKey = $storageKey; + } + + public function getName(): string + { + return $this->name; + } + + /** + * @return void + */ + public function setName(string $name) + { + $this->name = $name; + } + + /** + * @return void + */ + public function initialize(array &$flashes) + { + $this->flashes = &$flashes; + + // The logic: messages from the last request will be stored in new, so we move them to previous + // This request we will show what is in 'display'. What is placed into 'new' this time round will + // be moved to display next time round. + $this->flashes['display'] = \array_key_exists('new', $this->flashes) ? $this->flashes['new'] : []; + $this->flashes['new'] = []; + } + + /** + * @return void + */ + public function add(string $type, mixed $message) + { + $this->flashes['new'][$type][] = $message; + } + + public function peek(string $type, array $default = []): array + { + return $this->has($type) ? $this->flashes['display'][$type] : $default; + } + + public function peekAll(): array + { + return \array_key_exists('display', $this->flashes) ? $this->flashes['display'] : []; + } + + public function get(string $type, array $default = []): array + { + $return = $default; + + if (!$this->has($type)) { + return $return; + } + + if (isset($this->flashes['display'][$type])) { + $return = $this->flashes['display'][$type]; + unset($this->flashes['display'][$type]); + } + + return $return; + } + + public function all(): array + { + $return = $this->flashes['display']; + $this->flashes['display'] = []; + + return $return; + } + + /** + * @return void + */ + public function setAll(array $messages) + { + $this->flashes['new'] = $messages; + } + + /** + * @return void + */ + public function set(string $type, string|array $messages) + { + $this->flashes['new'][$type] = (array) $messages; + } + + public function has(string $type): bool + { + return \array_key_exists($type, $this->flashes['display']) && $this->flashes['display'][$type]; + } + + public function keys(): array + { + return array_keys($this->flashes['display']); + } + + public function getStorageKey(): string + { + return $this->storageKey; + } + + public function clear(): mixed + { + return $this->all(); + } +} diff --git a/3rdparty/symfony/http-foundation/Session/Flash/FlashBag.php b/3rdparty/symfony/http-foundation/Session/Flash/FlashBag.php new file mode 100644 index 00000000..659d59d1 --- /dev/null +++ b/3rdparty/symfony/http-foundation/Session/Flash/FlashBag.php @@ -0,0 +1,128 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Component\HttpFoundation\Session\Flash; + +/** + * FlashBag flash message container. + * + * @author Drak + */ +class FlashBag implements FlashBagInterface +{ + private string $name = 'flashes'; + private array $flashes = []; + private string $storageKey; + + /** + * @param string $storageKey The key used to store flashes in the session + */ + public function __construct(string $storageKey = '_symfony_flashes') + { + $this->storageKey = $storageKey; + } + + public function getName(): string + { + return $this->name; + } + + /** + * @return void + */ + public function setName(string $name) + { + $this->name = $name; + } + + /** + * @return void + */ + public function initialize(array &$flashes) + { + $this->flashes = &$flashes; + } + + /** + * @return void + */ + public function add(string $type, mixed $message) + { + $this->flashes[$type][] = $message; + } + + public function peek(string $type, array $default = []): array + { + return $this->has($type) ? $this->flashes[$type] : $default; + } + + public function peekAll(): array + { + return $this->flashes; + } + + public function get(string $type, array $default = []): array + { + if (!$this->has($type)) { + return $default; + } + + $return = $this->flashes[$type]; + + unset($this->flashes[$type]); + + return $return; + } + + public function all(): array + { + $return = $this->peekAll(); + $this->flashes = []; + + return $return; + } + + /** + * @return void + */ + public function set(string $type, string|array $messages) + { + $this->flashes[$type] = (array) $messages; + } + + /** + * @return void + */ + public function setAll(array $messages) + { + $this->flashes = $messages; + } + + public function has(string $type): bool + { + return \array_key_exists($type, $this->flashes) && $this->flashes[$type]; + } + + public function keys(): array + { + return array_keys($this->flashes); + } + + public function getStorageKey(): string + { + return $this->storageKey; + } + + public function clear(): mixed + { + return $this->all(); + } +} diff --git a/3rdparty/symfony/http-foundation/Session/Flash/FlashBagInterface.php b/3rdparty/symfony/http-foundation/Session/Flash/FlashBagInterface.php new file mode 100644 index 00000000..bbcf7f8b --- /dev/null +++ b/3rdparty/symfony/http-foundation/Session/Flash/FlashBagInterface.php @@ -0,0 +1,78 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Component\HttpFoundation\Session\Flash; + +use Symfony\Component\HttpFoundation\Session\SessionBagInterface; + +/** + * FlashBagInterface. + * + * @author Drak + */ +interface FlashBagInterface extends SessionBagInterface +{ + /** + * Adds a flash message for the given type. + * + * @return void + */ + public function add(string $type, mixed $message); + + /** + * Registers one or more messages for a given type. + * + * @return void + */ + public function set(string $type, string|array $messages); + + /** + * Gets flash messages for a given type. + * + * @param string $type Message category type + * @param array $default Default value if $type does not exist + */ + public function peek(string $type, array $default = []): array; + + /** + * Gets all flash messages. + */ + public function peekAll(): array; + + /** + * Gets and clears flash from the stack. + * + * @param array $default Default value if $type does not exist + */ + public function get(string $type, array $default = []): array; + + /** + * Gets and clears flashes from the stack. + */ + public function all(): array; + + /** + * Sets all flash messages. + * + * @return void + */ + public function setAll(array $messages); + + /** + * Has flash messages for a given type? + */ + public function has(string $type): bool; + + /** + * Returns a list of all defined types. + */ + public function keys(): array; +} diff --git a/3rdparty/symfony/http-foundation/Session/FlashBagAwareSessionInterface.php b/3rdparty/symfony/http-foundation/Session/FlashBagAwareSessionInterface.php new file mode 100644 index 00000000..90151d38 --- /dev/null +++ b/3rdparty/symfony/http-foundation/Session/FlashBagAwareSessionInterface.php @@ -0,0 +1,22 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Component\HttpFoundation\Session; + +use Symfony\Component\HttpFoundation\Session\Flash\FlashBagInterface; + +/** + * Interface for session with a flashbag. + */ +interface FlashBagAwareSessionInterface extends SessionInterface +{ + public function getFlashBag(): FlashBagInterface; +} diff --git a/3rdparty/symfony/http-foundation/Session/Session.php b/3rdparty/symfony/http-foundation/Session/Session.php new file mode 100644 index 00000000..5b6db175 --- /dev/null +++ b/3rdparty/symfony/http-foundation/Session/Session.php @@ -0,0 +1,244 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Component\HttpFoundation\Session; + +use Symfony\Component\HttpFoundation\Session\Attribute\AttributeBag; +use Symfony\Component\HttpFoundation\Session\Attribute\AttributeBagInterface; +use Symfony\Component\HttpFoundation\Session\Flash\FlashBag; +use Symfony\Component\HttpFoundation\Session\Flash\FlashBagInterface; +use Symfony\Component\HttpFoundation\Session\Storage\MetadataBag; +use Symfony\Component\HttpFoundation\Session\Storage\NativeSessionStorage; +use Symfony\Component\HttpFoundation\Session\Storage\SessionStorageInterface; + +// Help opcache.preload discover always-needed symbols +class_exists(AttributeBag::class); +class_exists(FlashBag::class); +class_exists(SessionBagProxy::class); + +/** + * @author Fabien Potencier + * @author Drak + * + * @implements \IteratorAggregate + */ +class Session implements FlashBagAwareSessionInterface, \IteratorAggregate, \Countable +{ + protected $storage; + + private string $flashName; + private string $attributeName; + private array $data = []; + private int $usageIndex = 0; + private ?\Closure $usageReporter; + + public function __construct(?SessionStorageInterface $storage = null, ?AttributeBagInterface $attributes = null, ?FlashBagInterface $flashes = null, ?callable $usageReporter = null) + { + $this->storage = $storage ?? new NativeSessionStorage(); + $this->usageReporter = null === $usageReporter ? null : $usageReporter(...); + + $attributes ??= new AttributeBag(); + $this->attributeName = $attributes->getName(); + $this->registerBag($attributes); + + $flashes ??= new FlashBag(); + $this->flashName = $flashes->getName(); + $this->registerBag($flashes); + } + + public function start(): bool + { + return $this->storage->start(); + } + + public function has(string $name): bool + { + return $this->getAttributeBag()->has($name); + } + + public function get(string $name, mixed $default = null): mixed + { + return $this->getAttributeBag()->get($name, $default); + } + + /** + * @return void + */ + public function set(string $name, mixed $value) + { + $this->getAttributeBag()->set($name, $value); + } + + public function all(): array + { + return $this->getAttributeBag()->all(); + } + + /** + * @return void + */ + public function replace(array $attributes) + { + $this->getAttributeBag()->replace($attributes); + } + + public function remove(string $name): mixed + { + return $this->getAttributeBag()->remove($name); + } + + /** + * @return void + */ + public function clear() + { + $this->getAttributeBag()->clear(); + } + + public function isStarted(): bool + { + return $this->storage->isStarted(); + } + + /** + * Returns an iterator for attributes. + * + * @return \ArrayIterator + */ + public function getIterator(): \ArrayIterator + { + return new \ArrayIterator($this->getAttributeBag()->all()); + } + + /** + * Returns the number of attributes. + */ + public function count(): int + { + return \count($this->getAttributeBag()->all()); + } + + public function &getUsageIndex(): int + { + return $this->usageIndex; + } + + /** + * @internal + */ + public function isEmpty(): bool + { + if ($this->isStarted()) { + ++$this->usageIndex; + if ($this->usageReporter && 0 <= $this->usageIndex) { + ($this->usageReporter)(); + } + } + foreach ($this->data as &$data) { + if (!empty($data)) { + return false; + } + } + + return true; + } + + public function invalidate(?int $lifetime = null): bool + { + $this->storage->clear(); + + return $this->migrate(true, $lifetime); + } + + public function migrate(bool $destroy = false, ?int $lifetime = null): bool + { + return $this->storage->regenerate($destroy, $lifetime); + } + + /** + * @return void + */ + public function save() + { + $this->storage->save(); + } + + public function getId(): string + { + return $this->storage->getId(); + } + + /** + * @return void + */ + public function setId(string $id) + { + if ($this->storage->getId() !== $id) { + $this->storage->setId($id); + } + } + + public function getName(): string + { + return $this->storage->getName(); + } + + /** + * @return void + */ + public function setName(string $name) + { + $this->storage->setName($name); + } + + public function getMetadataBag(): MetadataBag + { + ++$this->usageIndex; + if ($this->usageReporter && 0 <= $this->usageIndex) { + ($this->usageReporter)(); + } + + return $this->storage->getMetadataBag(); + } + + /** + * @return void + */ + public function registerBag(SessionBagInterface $bag) + { + $this->storage->registerBag(new SessionBagProxy($bag, $this->data, $this->usageIndex, $this->usageReporter)); + } + + public function getBag(string $name): SessionBagInterface + { + $bag = $this->storage->getBag($name); + + return method_exists($bag, 'getBag') ? $bag->getBag() : $bag; + } + + /** + * Gets the flashbag interface. + */ + public function getFlashBag(): FlashBagInterface + { + return $this->getBag($this->flashName); + } + + /** + * Gets the attributebag interface. + * + * Note that this method was added to help with IDE autocompletion. + */ + private function getAttributeBag(): AttributeBagInterface + { + return $this->getBag($this->attributeName); + } +} diff --git a/3rdparty/symfony/http-foundation/Session/SessionBagInterface.php b/3rdparty/symfony/http-foundation/Session/SessionBagInterface.php new file mode 100644 index 00000000..e1c25055 --- /dev/null +++ b/3rdparty/symfony/http-foundation/Session/SessionBagInterface.php @@ -0,0 +1,44 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Component\HttpFoundation\Session; + +/** + * Session Bag store. + * + * @author Drak + */ +interface SessionBagInterface +{ + /** + * Gets this bag's name. + */ + public function getName(): string; + + /** + * Initializes the Bag. + * + * @return void + */ + public function initialize(array &$array); + + /** + * Gets the storage key for this bag. + */ + public function getStorageKey(): string; + + /** + * Clears out data from bag. + * + * @return mixed Whatever data was contained + */ + public function clear(): mixed; +} diff --git a/3rdparty/symfony/http-foundation/Session/SessionBagProxy.php b/3rdparty/symfony/http-foundation/Session/SessionBagProxy.php new file mode 100644 index 00000000..e759d94d --- /dev/null +++ b/3rdparty/symfony/http-foundation/Session/SessionBagProxy.php @@ -0,0 +1,83 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Component\HttpFoundation\Session; + +/** + * @author Nicolas Grekas + * + * @internal + */ +final class SessionBagProxy implements SessionBagInterface +{ + private SessionBagInterface $bag; + private array $data; + private ?int $usageIndex; + private ?\Closure $usageReporter; + + public function __construct(SessionBagInterface $bag, array &$data, ?int &$usageIndex, ?callable $usageReporter) + { + $this->bag = $bag; + $this->data = &$data; + $this->usageIndex = &$usageIndex; + $this->usageReporter = null === $usageReporter ? null : $usageReporter(...); + } + + public function getBag(): SessionBagInterface + { + ++$this->usageIndex; + if ($this->usageReporter && 0 <= $this->usageIndex) { + ($this->usageReporter)(); + } + + return $this->bag; + } + + public function isEmpty(): bool + { + if (!isset($this->data[$this->bag->getStorageKey()])) { + return true; + } + ++$this->usageIndex; + if ($this->usageReporter && 0 <= $this->usageIndex) { + ($this->usageReporter)(); + } + + return empty($this->data[$this->bag->getStorageKey()]); + } + + public function getName(): string + { + return $this->bag->getName(); + } + + public function initialize(array &$array): void + { + ++$this->usageIndex; + if ($this->usageReporter && 0 <= $this->usageIndex) { + ($this->usageReporter)(); + } + + $this->data[$this->bag->getStorageKey()] = &$array; + + $this->bag->initialize($array); + } + + public function getStorageKey(): string + { + return $this->bag->getStorageKey(); + } + + public function clear(): mixed + { + return $this->bag->clear(); + } +} diff --git a/3rdparty/symfony/http-foundation/Session/SessionFactory.php b/3rdparty/symfony/http-foundation/Session/SessionFactory.php new file mode 100644 index 00000000..c06ed4b7 --- /dev/null +++ b/3rdparty/symfony/http-foundation/Session/SessionFactory.php @@ -0,0 +1,40 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Component\HttpFoundation\Session; + +use Symfony\Component\HttpFoundation\RequestStack; +use Symfony\Component\HttpFoundation\Session\Storage\SessionStorageFactoryInterface; + +// Help opcache.preload discover always-needed symbols +class_exists(Session::class); + +/** + * @author Jérémy Derussé + */ +class SessionFactory implements SessionFactoryInterface +{ + private RequestStack $requestStack; + private SessionStorageFactoryInterface $storageFactory; + private ?\Closure $usageReporter; + + public function __construct(RequestStack $requestStack, SessionStorageFactoryInterface $storageFactory, ?callable $usageReporter = null) + { + $this->requestStack = $requestStack; + $this->storageFactory = $storageFactory; + $this->usageReporter = null === $usageReporter ? null : $usageReporter(...); + } + + public function createSession(): SessionInterface + { + return new Session($this->storageFactory->createStorage($this->requestStack->getMainRequest()), null, null, $this->usageReporter); + } +} diff --git a/3rdparty/symfony/http-foundation/Session/SessionFactoryInterface.php b/3rdparty/symfony/http-foundation/Session/SessionFactoryInterface.php new file mode 100644 index 00000000..b24fdc49 --- /dev/null +++ b/3rdparty/symfony/http-foundation/Session/SessionFactoryInterface.php @@ -0,0 +1,20 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Component\HttpFoundation\Session; + +/** + * @author Kevin Bond + */ +interface SessionFactoryInterface +{ + public function createSession(): SessionInterface; +} diff --git a/3rdparty/symfony/http-foundation/Session/SessionInterface.php b/3rdparty/symfony/http-foundation/Session/SessionInterface.php new file mode 100644 index 00000000..07785a6f --- /dev/null +++ b/3rdparty/symfony/http-foundation/Session/SessionInterface.php @@ -0,0 +1,154 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Component\HttpFoundation\Session; + +use Symfony\Component\HttpFoundation\Session\Storage\MetadataBag; + +/** + * Interface for the session. + * + * @author Drak + */ +interface SessionInterface +{ + /** + * Starts the session storage. + * + * @throws \RuntimeException if session fails to start + */ + public function start(): bool; + + /** + * Returns the session ID. + */ + public function getId(): string; + + /** + * Sets the session ID. + * + * @return void + */ + public function setId(string $id); + + /** + * Returns the session name. + */ + public function getName(): string; + + /** + * Sets the session name. + * + * @return void + */ + public function setName(string $name); + + /** + * Invalidates the current session. + * + * Clears all session attributes and flashes and regenerates the + * session and deletes the old session from persistence. + * + * @param int|null $lifetime Sets the cookie lifetime for the session cookie. A null value + * will leave the system settings unchanged, 0 sets the cookie + * to expire with browser session. Time is in seconds, and is + * not a Unix timestamp. + */ + public function invalidate(?int $lifetime = null): bool; + + /** + * Migrates the current session to a new session id while maintaining all + * session attributes. + * + * @param bool $destroy Whether to delete the old session or leave it to garbage collection + * @param int|null $lifetime Sets the cookie lifetime for the session cookie. A null value + * will leave the system settings unchanged, 0 sets the cookie + * to expire with browser session. Time is in seconds, and is + * not a Unix timestamp. + */ + public function migrate(bool $destroy = false, ?int $lifetime = null): bool; + + /** + * Force the session to be saved and closed. + * + * This method is generally not required for real sessions as + * the session will be automatically saved at the end of + * code execution. + * + * @return void + */ + public function save(); + + /** + * Checks if an attribute is defined. + */ + public function has(string $name): bool; + + /** + * Returns an attribute. + */ + public function get(string $name, mixed $default = null): mixed; + + /** + * Sets an attribute. + * + * @return void + */ + public function set(string $name, mixed $value); + + /** + * Returns attributes. + */ + public function all(): array; + + /** + * Sets attributes. + * + * @return void + */ + public function replace(array $attributes); + + /** + * Removes an attribute. + * + * @return mixed The removed value or null when it does not exist + */ + public function remove(string $name): mixed; + + /** + * Clears all attributes. + * + * @return void + */ + public function clear(); + + /** + * Checks if the session was started. + */ + public function isStarted(): bool; + + /** + * Registers a SessionBagInterface with the session. + * + * @return void + */ + public function registerBag(SessionBagInterface $bag); + + /** + * Gets a bag instance by name. + */ + public function getBag(string $name): SessionBagInterface; + + /** + * Gets session meta. + */ + public function getMetadataBag(): MetadataBag; +} diff --git a/3rdparty/symfony/http-foundation/Session/SessionUtils.php b/3rdparty/symfony/http-foundation/Session/SessionUtils.php new file mode 100644 index 00000000..57aa565f --- /dev/null +++ b/3rdparty/symfony/http-foundation/Session/SessionUtils.php @@ -0,0 +1,59 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Component\HttpFoundation\Session; + +/** + * Session utility functions. + * + * @author Nicolas Grekas + * @author Rémon van de Kamp + * + * @internal + */ +final class SessionUtils +{ + /** + * Finds the session header amongst the headers that are to be sent, removes it, and returns + * it so the caller can process it further. + */ + public static function popSessionCookie(string $sessionName, #[\SensitiveParameter] string $sessionId): ?string + { + $sessionCookie = null; + $sessionCookiePrefix = \sprintf(' %s=', urlencode($sessionName)); + $sessionCookieWithId = \sprintf('%s%s;', $sessionCookiePrefix, urlencode($sessionId)); + $otherCookies = []; + foreach (headers_list() as $h) { + if (0 !== stripos($h, 'Set-Cookie:')) { + continue; + } + if (11 === strpos($h, $sessionCookiePrefix, 11)) { + $sessionCookie = $h; + + if (11 !== strpos($h, $sessionCookieWithId, 11)) { + $otherCookies[] = $h; + } + } else { + $otherCookies[] = $h; + } + } + if (null === $sessionCookie) { + return null; + } + + header_remove('Set-Cookie'); + foreach ($otherCookies as $h) { + header($h, false); + } + + return $sessionCookie; + } +} diff --git a/3rdparty/symfony/http-foundation/Session/Storage/Handler/AbstractSessionHandler.php b/3rdparty/symfony/http-foundation/Session/Storage/Handler/AbstractSessionHandler.php new file mode 100644 index 00000000..fd856237 --- /dev/null +++ b/3rdparty/symfony/http-foundation/Session/Storage/Handler/AbstractSessionHandler.php @@ -0,0 +1,111 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Component\HttpFoundation\Session\Storage\Handler; + +use Symfony\Component\HttpFoundation\Session\SessionUtils; + +/** + * This abstract session handler provides a generic implementation + * of the PHP 7.0 SessionUpdateTimestampHandlerInterface, + * enabling strict and lazy session handling. + * + * @author Nicolas Grekas + */ +abstract class AbstractSessionHandler implements \SessionHandlerInterface, \SessionUpdateTimestampHandlerInterface +{ + private string $sessionName; + private string $prefetchId; + private string $prefetchData; + private ?string $newSessionId = null; + private string $igbinaryEmptyData; + + public function open(string $savePath, string $sessionName): bool + { + $this->sessionName = $sessionName; + if (!headers_sent() && !\ini_get('session.cache_limiter') && '0' !== \ini_get('session.cache_limiter')) { + header(\sprintf('Cache-Control: max-age=%d, private, must-revalidate', 60 * (int) \ini_get('session.cache_expire'))); + } + + return true; + } + + abstract protected function doRead(#[\SensitiveParameter] string $sessionId): string; + + abstract protected function doWrite(#[\SensitiveParameter] string $sessionId, string $data): bool; + + abstract protected function doDestroy(#[\SensitiveParameter] string $sessionId): bool; + + public function validateId(#[\SensitiveParameter] string $sessionId): bool + { + $this->prefetchData = $this->read($sessionId); + $this->prefetchId = $sessionId; + + return '' !== $this->prefetchData; + } + + public function read(#[\SensitiveParameter] string $sessionId): string + { + if (isset($this->prefetchId)) { + $prefetchId = $this->prefetchId; + $prefetchData = $this->prefetchData; + unset($this->prefetchId, $this->prefetchData); + + if ($prefetchId === $sessionId || '' === $prefetchData) { + $this->newSessionId = '' === $prefetchData ? $sessionId : null; + + return $prefetchData; + } + } + + $data = $this->doRead($sessionId); + $this->newSessionId = '' === $data ? $sessionId : null; + + return $data; + } + + public function write(#[\SensitiveParameter] string $sessionId, string $data): bool + { + // see https://github.com/igbinary/igbinary/issues/146 + $this->igbinaryEmptyData ??= \function_exists('igbinary_serialize') ? igbinary_serialize([]) : ''; + if ('' === $data || $this->igbinaryEmptyData === $data) { + return $this->destroy($sessionId); + } + $this->newSessionId = null; + + return $this->doWrite($sessionId, $data); + } + + public function destroy(#[\SensitiveParameter] string $sessionId): bool + { + if (!headers_sent() && filter_var(\ini_get('session.use_cookies'), \FILTER_VALIDATE_BOOL)) { + if (!isset($this->sessionName)) { + throw new \LogicException(\sprintf('Session name cannot be empty, did you forget to call "parent::open()" in "%s"?.', static::class)); + } + $cookie = SessionUtils::popSessionCookie($this->sessionName, $sessionId); + + /* + * We send an invalidation Set-Cookie header (zero lifetime) + * when either the session was started or a cookie with + * the session name was sent by the client (in which case + * we know it's invalid as a valid session cookie would've + * started the session). + */ + if (null === $cookie || isset($_COOKIE[$this->sessionName])) { + $params = session_get_cookie_params(); + unset($params['lifetime']); + setcookie($this->sessionName, '', $params); + } + } + + return $this->newSessionId === $sessionId || $this->doDestroy($sessionId); + } +} diff --git a/3rdparty/symfony/http-foundation/Session/Storage/Handler/IdentityMarshaller.php b/3rdparty/symfony/http-foundation/Session/Storage/Handler/IdentityMarshaller.php new file mode 100644 index 00000000..70ac7624 --- /dev/null +++ b/3rdparty/symfony/http-foundation/Session/Storage/Handler/IdentityMarshaller.php @@ -0,0 +1,36 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Component\HttpFoundation\Session\Storage\Handler; + +use Symfony\Component\Cache\Marshaller\MarshallerInterface; + +/** + * @author Ahmed TAILOULOUTE + */ +class IdentityMarshaller implements MarshallerInterface +{ + public function marshall(array $values, ?array &$failed): array + { + foreach ($values as $key => $value) { + if (!\is_string($value)) { + throw new \LogicException(\sprintf('%s accepts only string as data.', __METHOD__)); + } + } + + return $values; + } + + public function unmarshall(string $value): string + { + return $value; + } +} diff --git a/3rdparty/symfony/http-foundation/Session/Storage/Handler/MarshallingSessionHandler.php b/3rdparty/symfony/http-foundation/Session/Storage/Handler/MarshallingSessionHandler.php new file mode 100644 index 00000000..1567f543 --- /dev/null +++ b/3rdparty/symfony/http-foundation/Session/Storage/Handler/MarshallingSessionHandler.php @@ -0,0 +1,76 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Component\HttpFoundation\Session\Storage\Handler; + +use Symfony\Component\Cache\Marshaller\MarshallerInterface; + +/** + * @author Ahmed TAILOULOUTE + */ +class MarshallingSessionHandler implements \SessionHandlerInterface, \SessionUpdateTimestampHandlerInterface +{ + private AbstractSessionHandler $handler; + private MarshallerInterface $marshaller; + + public function __construct(AbstractSessionHandler $handler, MarshallerInterface $marshaller) + { + $this->handler = $handler; + $this->marshaller = $marshaller; + } + + public function open(string $savePath, string $name): bool + { + return $this->handler->open($savePath, $name); + } + + public function close(): bool + { + return $this->handler->close(); + } + + public function destroy(#[\SensitiveParameter] string $sessionId): bool + { + return $this->handler->destroy($sessionId); + } + + public function gc(int $maxlifetime): int|false + { + return $this->handler->gc($maxlifetime); + } + + public function read(#[\SensitiveParameter] string $sessionId): string + { + return $this->marshaller->unmarshall($this->handler->read($sessionId)); + } + + public function write(#[\SensitiveParameter] string $sessionId, string $data): bool + { + $failed = []; + $marshalledData = $this->marshaller->marshall(['data' => $data], $failed); + + if (isset($failed['data'])) { + return false; + } + + return $this->handler->write($sessionId, $marshalledData['data']); + } + + public function validateId(#[\SensitiveParameter] string $sessionId): bool + { + return $this->handler->validateId($sessionId); + } + + public function updateTimestamp(#[\SensitiveParameter] string $sessionId, string $data): bool + { + return $this->handler->updateTimestamp($sessionId, $data); + } +} diff --git a/3rdparty/symfony/http-foundation/Session/Storage/Handler/MemcachedSessionHandler.php b/3rdparty/symfony/http-foundation/Session/Storage/Handler/MemcachedSessionHandler.php new file mode 100644 index 00000000..9647f42b --- /dev/null +++ b/3rdparty/symfony/http-foundation/Session/Storage/Handler/MemcachedSessionHandler.php @@ -0,0 +1,112 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Component\HttpFoundation\Session\Storage\Handler; + +/** + * Memcached based session storage handler based on the Memcached class + * provided by the PHP memcached extension. + * + * @see https://php.net/memcached + * + * @author Drak + */ +class MemcachedSessionHandler extends AbstractSessionHandler +{ + private \Memcached $memcached; + + /** + * Time to live in seconds. + */ + private int|\Closure|null $ttl; + + /** + * Key prefix for shared environments. + */ + private string $prefix; + + /** + * Constructor. + * + * List of available options: + * * prefix: The prefix to use for the memcached keys in order to avoid collision + * * ttl: The time to live in seconds. + * + * @throws \InvalidArgumentException When unsupported options are passed + */ + public function __construct(\Memcached $memcached, array $options = []) + { + $this->memcached = $memcached; + + if ($diff = array_diff(array_keys($options), ['prefix', 'expiretime', 'ttl'])) { + throw new \InvalidArgumentException(\sprintf('The following options are not supported "%s".', implode(', ', $diff))); + } + + $this->ttl = $options['expiretime'] ?? $options['ttl'] ?? null; + $this->prefix = $options['prefix'] ?? 'sf2s'; + } + + public function close(): bool + { + return $this->memcached->quit(); + } + + protected function doRead(#[\SensitiveParameter] string $sessionId): string + { + return $this->memcached->get($this->prefix.$sessionId) ?: ''; + } + + public function updateTimestamp(#[\SensitiveParameter] string $sessionId, string $data): bool + { + $this->memcached->touch($this->prefix.$sessionId, $this->getCompatibleTtl()); + + return true; + } + + protected function doWrite(#[\SensitiveParameter] string $sessionId, string $data): bool + { + return $this->memcached->set($this->prefix.$sessionId, $data, $this->getCompatibleTtl()); + } + + private function getCompatibleTtl(): int + { + $ttl = ($this->ttl instanceof \Closure ? ($this->ttl)() : $this->ttl) ?? \ini_get('session.gc_maxlifetime'); + + // If the relative TTL that is used exceeds 30 days, memcached will treat the value as Unix time. + // We have to convert it to an absolute Unix time at this point, to make sure the TTL is correct. + if ($ttl > 60 * 60 * 24 * 30) { + $ttl += time(); + } + + return $ttl; + } + + protected function doDestroy(#[\SensitiveParameter] string $sessionId): bool + { + $result = $this->memcached->delete($this->prefix.$sessionId); + + return $result || \Memcached::RES_NOTFOUND == $this->memcached->getResultCode(); + } + + public function gc(int $maxlifetime): int|false + { + // not required here because memcached will auto expire the records anyhow. + return 0; + } + + /** + * Return a Memcached instance. + */ + protected function getMemcached(): \Memcached + { + return $this->memcached; + } +} diff --git a/3rdparty/symfony/http-foundation/Session/Storage/Handler/MigratingSessionHandler.php b/3rdparty/symfony/http-foundation/Session/Storage/Handler/MigratingSessionHandler.php new file mode 100644 index 00000000..8ed6a7b3 --- /dev/null +++ b/3rdparty/symfony/http-foundation/Session/Storage/Handler/MigratingSessionHandler.php @@ -0,0 +1,100 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Component\HttpFoundation\Session\Storage\Handler; + +/** + * Migrating session handler for migrating from one handler to another. It reads + * from the current handler and writes both the current and new ones. + * + * It ignores errors from the new handler. + * + * @author Ross Motley + * @author Oliver Radwell + */ +class MigratingSessionHandler implements \SessionHandlerInterface, \SessionUpdateTimestampHandlerInterface +{ + private \SessionHandlerInterface&\SessionUpdateTimestampHandlerInterface $currentHandler; + private \SessionHandlerInterface&\SessionUpdateTimestampHandlerInterface $writeOnlyHandler; + + public function __construct(\SessionHandlerInterface $currentHandler, \SessionHandlerInterface $writeOnlyHandler) + { + if (!$currentHandler instanceof \SessionUpdateTimestampHandlerInterface) { + $currentHandler = new StrictSessionHandler($currentHandler); + } + if (!$writeOnlyHandler instanceof \SessionUpdateTimestampHandlerInterface) { + $writeOnlyHandler = new StrictSessionHandler($writeOnlyHandler); + } + + $this->currentHandler = $currentHandler; + $this->writeOnlyHandler = $writeOnlyHandler; + } + + public function close(): bool + { + $result = $this->currentHandler->close(); + $this->writeOnlyHandler->close(); + + return $result; + } + + public function destroy(#[\SensitiveParameter] string $sessionId): bool + { + $result = $this->currentHandler->destroy($sessionId); + $this->writeOnlyHandler->destroy($sessionId); + + return $result; + } + + public function gc(int $maxlifetime): int|false + { + $result = $this->currentHandler->gc($maxlifetime); + $this->writeOnlyHandler->gc($maxlifetime); + + return $result; + } + + public function open(string $savePath, string $sessionName): bool + { + $result = $this->currentHandler->open($savePath, $sessionName); + $this->writeOnlyHandler->open($savePath, $sessionName); + + return $result; + } + + public function read(#[\SensitiveParameter] string $sessionId): string + { + // No reading from new handler until switch-over + return $this->currentHandler->read($sessionId); + } + + public function write(#[\SensitiveParameter] string $sessionId, string $sessionData): bool + { + $result = $this->currentHandler->write($sessionId, $sessionData); + $this->writeOnlyHandler->write($sessionId, $sessionData); + + return $result; + } + + public function validateId(#[\SensitiveParameter] string $sessionId): bool + { + // No reading from new handler until switch-over + return $this->currentHandler->validateId($sessionId); + } + + public function updateTimestamp(#[\SensitiveParameter] string $sessionId, string $sessionData): bool + { + $result = $this->currentHandler->updateTimestamp($sessionId, $sessionData); + $this->writeOnlyHandler->updateTimestamp($sessionId, $sessionData); + + return $result; + } +} diff --git a/3rdparty/symfony/http-foundation/Session/Storage/Handler/MongoDbSessionHandler.php b/3rdparty/symfony/http-foundation/Session/Storage/Handler/MongoDbSessionHandler.php new file mode 100644 index 00000000..d5586030 --- /dev/null +++ b/3rdparty/symfony/http-foundation/Session/Storage/Handler/MongoDbSessionHandler.php @@ -0,0 +1,186 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Component\HttpFoundation\Session\Storage\Handler; + +use MongoDB\BSON\Binary; +use MongoDB\BSON\UTCDateTime; +use MongoDB\Client; +use MongoDB\Driver\BulkWrite; +use MongoDB\Driver\Manager; +use MongoDB\Driver\Query; + +/** + * Session handler using the MongoDB driver extension. + * + * @author Markus Bachmann + * @author Jérôme Tamarelle + * + * @see https://php.net/mongodb + */ +class MongoDbSessionHandler extends AbstractSessionHandler +{ + private Manager $manager; + private string $namespace; + private array $options; + private int|\Closure|null $ttl; + + /** + * Constructor. + * + * List of available options: + * * database: The name of the database [required] + * * collection: The name of the collection [required] + * * id_field: The field name for storing the session id [default: _id] + * * data_field: The field name for storing the session data [default: data] + * * time_field: The field name for storing the timestamp [default: time] + * * expiry_field: The field name for storing the expiry-timestamp [default: expires_at] + * * ttl: The time to live in seconds. + * + * It is strongly recommended to put an index on the `expiry_field` for + * garbage-collection. Alternatively it's possible to automatically expire + * the sessions in the database as described below: + * + * A TTL collections can be used on MongoDB 2.2+ to cleanup expired sessions + * automatically. Such an index can for example look like this: + * + * db..createIndex( + * { "": 1 }, + * { "expireAfterSeconds": 0 } + * ) + * + * More details on: https://docs.mongodb.org/manual/tutorial/expire-data/ + * + * If you use such an index, you can drop `gc_probability` to 0 since + * no garbage-collection is required. + * + * @throws \InvalidArgumentException When "database" or "collection" not provided + */ + public function __construct(Client|Manager $mongo, array $options) + { + if (!isset($options['database']) || !isset($options['collection'])) { + throw new \InvalidArgumentException('You must provide the "database" and "collection" option for MongoDBSessionHandler.'); + } + + if ($mongo instanceof Client) { + $mongo = $mongo->getManager(); + } + + $this->manager = $mongo; + $this->namespace = $options['database'].'.'.$options['collection']; + + $this->options = array_merge([ + 'id_field' => '_id', + 'data_field' => 'data', + 'time_field' => 'time', + 'expiry_field' => 'expires_at', + ], $options); + $this->ttl = $this->options['ttl'] ?? null; + } + + public function close(): bool + { + return true; + } + + protected function doDestroy(#[\SensitiveParameter] string $sessionId): bool + { + $write = new BulkWrite(); + $write->delete( + [$this->options['id_field'] => $sessionId], + ['limit' => 1] + ); + + $this->manager->executeBulkWrite($this->namespace, $write); + + return true; + } + + public function gc(int $maxlifetime): int|false + { + $write = new BulkWrite(); + $write->delete( + [$this->options['expiry_field'] => ['$lt' => $this->getUTCDateTime()]], + ); + $result = $this->manager->executeBulkWrite($this->namespace, $write); + + return $result->getDeletedCount() ?? false; + } + + protected function doWrite(#[\SensitiveParameter] string $sessionId, string $data): bool + { + $ttl = ($this->ttl instanceof \Closure ? ($this->ttl)() : $this->ttl) ?? \ini_get('session.gc_maxlifetime'); + $expiry = $this->getUTCDateTime($ttl); + + $fields = [ + $this->options['time_field'] => $this->getUTCDateTime(), + $this->options['expiry_field'] => $expiry, + $this->options['data_field'] => new Binary($data, Binary::TYPE_GENERIC), + ]; + + $write = new BulkWrite(); + $write->update( + [$this->options['id_field'] => $sessionId], + ['$set' => $fields], + ['upsert' => true] + ); + + $this->manager->executeBulkWrite($this->namespace, $write); + + return true; + } + + public function updateTimestamp(#[\SensitiveParameter] string $sessionId, string $data): bool + { + $ttl = ($this->ttl instanceof \Closure ? ($this->ttl)() : $this->ttl) ?? \ini_get('session.gc_maxlifetime'); + $expiry = $this->getUTCDateTime($ttl); + + $write = new BulkWrite(); + $write->update( + [$this->options['id_field'] => $sessionId], + ['$set' => [ + $this->options['time_field'] => $this->getUTCDateTime(), + $this->options['expiry_field'] => $expiry, + ]], + ['multi' => false], + ); + + $this->manager->executeBulkWrite($this->namespace, $write); + + return true; + } + + protected function doRead(#[\SensitiveParameter] string $sessionId): string + { + $cursor = $this->manager->executeQuery($this->namespace, new Query([ + $this->options['id_field'] => $sessionId, + $this->options['expiry_field'] => ['$gte' => $this->getUTCDateTime()], + ], [ + 'projection' => [ + '_id' => false, + $this->options['data_field'] => true, + ], + 'limit' => 1, + ])); + + foreach ($cursor as $document) { + return (string) $document->{$this->options['data_field']} ?? ''; + } + + // Not found + return ''; + } + + private function getUTCDateTime(int $additionalSeconds = 0): UTCDateTime + { + return new UTCDateTime((time() + $additionalSeconds) * 1000); + } +} diff --git a/3rdparty/symfony/http-foundation/Session/Storage/Handler/NativeFileSessionHandler.php b/3rdparty/symfony/http-foundation/Session/Storage/Handler/NativeFileSessionHandler.php new file mode 100644 index 00000000..284cd869 --- /dev/null +++ b/3rdparty/symfony/http-foundation/Session/Storage/Handler/NativeFileSessionHandler.php @@ -0,0 +1,55 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Component\HttpFoundation\Session\Storage\Handler; + +/** + * Native session handler using PHP's built in file storage. + * + * @author Drak + */ +class NativeFileSessionHandler extends \SessionHandler +{ + /** + * @param string|null $savePath Path of directory to save session files + * Default null will leave setting as defined by PHP. + * '/path', 'N;/path', or 'N;octal-mode;/path + * + * @see https://php.net/session.configuration#ini.session.save-path for further details. + * + * @throws \InvalidArgumentException On invalid $savePath + * @throws \RuntimeException When failing to create the save directory + */ + public function __construct(?string $savePath = null) + { + $baseDir = $savePath ??= \ini_get('session.save_path'); + + if ($count = substr_count($savePath, ';')) { + if ($count > 2) { + throw new \InvalidArgumentException(\sprintf('Invalid argument $savePath \'%s\'.', $savePath)); + } + + // characters after last ';' are the path + $baseDir = ltrim(strrchr($savePath, ';'), ';'); + } + + if ($baseDir && !is_dir($baseDir) && !@mkdir($baseDir, 0777, true) && !is_dir($baseDir)) { + throw new \RuntimeException(\sprintf('Session Storage was not able to create directory "%s".', $baseDir)); + } + + if ($savePath !== \ini_get('session.save_path')) { + ini_set('session.save_path', $savePath); + } + if ('files' !== \ini_get('session.save_handler')) { + ini_set('session.save_handler', 'files'); + } + } +} diff --git a/3rdparty/symfony/http-foundation/Session/Storage/Handler/NullSessionHandler.php b/3rdparty/symfony/http-foundation/Session/Storage/Handler/NullSessionHandler.php new file mode 100644 index 00000000..a77185e2 --- /dev/null +++ b/3rdparty/symfony/http-foundation/Session/Storage/Handler/NullSessionHandler.php @@ -0,0 +1,55 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Component\HttpFoundation\Session\Storage\Handler; + +/** + * Can be used in unit testing or in a situations where persisted sessions are not desired. + * + * @author Drak + */ +class NullSessionHandler extends AbstractSessionHandler +{ + public function close(): bool + { + return true; + } + + public function validateId(#[\SensitiveParameter] string $sessionId): bool + { + return true; + } + + protected function doRead(#[\SensitiveParameter] string $sessionId): string + { + return ''; + } + + public function updateTimestamp(#[\SensitiveParameter] string $sessionId, string $data): bool + { + return true; + } + + protected function doWrite(#[\SensitiveParameter] string $sessionId, string $data): bool + { + return true; + } + + protected function doDestroy(#[\SensitiveParameter] string $sessionId): bool + { + return true; + } + + public function gc(int $maxlifetime): int|false + { + return 0; + } +} diff --git a/3rdparty/symfony/http-foundation/Session/Storage/Handler/PdoSessionHandler.php b/3rdparty/symfony/http-foundation/Session/Storage/Handler/PdoSessionHandler.php new file mode 100644 index 00000000..48c219a7 --- /dev/null +++ b/3rdparty/symfony/http-foundation/Session/Storage/Handler/PdoSessionHandler.php @@ -0,0 +1,901 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Component\HttpFoundation\Session\Storage\Handler; + +use Doctrine\DBAL\Schema\Schema; +use Doctrine\DBAL\Types\Types; + +/** + * Session handler using a PDO connection to read and write data. + * + * It works with MySQL, PostgreSQL, Oracle, SQL Server and SQLite and implements + * different locking strategies to handle concurrent access to the same session. + * Locking is necessary to prevent loss of data due to race conditions and to keep + * the session data consistent between read() and write(). With locking, requests + * for the same session will wait until the other one finished writing. For this + * reason it's best practice to close a session as early as possible to improve + * concurrency. PHPs internal files session handler also implements locking. + * + * Attention: Since SQLite does not support row level locks but locks the whole database, + * it means only one session can be accessed at a time. Even different sessions would wait + * for another to finish. So saving session in SQLite should only be considered for + * development or prototypes. + * + * Session data is a binary string that can contain non-printable characters like the null byte. + * For this reason it must be saved in a binary column in the database like BLOB in MySQL. + * Saving it in a character column could corrupt the data. You can use createTable() + * to initialize a correctly defined table. + * + * @see https://php.net/sessionhandlerinterface + * + * @author Fabien Potencier + * @author Michael Williams + * @author Tobias Schultze + */ +class PdoSessionHandler extends AbstractSessionHandler +{ + /** + * No locking is done. This means sessions are prone to loss of data due to + * race conditions of concurrent requests to the same session. The last session + * write will win in this case. It might be useful when you implement your own + * logic to deal with this like an optimistic approach. + */ + public const LOCK_NONE = 0; + + /** + * Creates an application-level lock on a session. The disadvantage is that the + * lock is not enforced by the database and thus other, unaware parts of the + * application could still concurrently modify the session. The advantage is it + * does not require a transaction. + * This mode is not available for SQLite and not yet implemented for oci and sqlsrv. + */ + public const LOCK_ADVISORY = 1; + + /** + * Issues a real row lock. Since it uses a transaction between opening and + * closing a session, you have to be careful when you use same database connection + * that you also use for your application logic. This mode is the default because + * it's the only reliable solution across DBMSs. + */ + public const LOCK_TRANSACTIONAL = 2; + + private \PDO $pdo; + + /** + * DSN string or null for session.save_path or false when lazy connection disabled. + */ + private string|false|null $dsn = false; + + private string $driver; + private string $table = 'sessions'; + private string $idCol = 'sess_id'; + private string $dataCol = 'sess_data'; + private string $lifetimeCol = 'sess_lifetime'; + private string $timeCol = 'sess_time'; + + /** + * Time to live in seconds. + */ + private int|\Closure|null $ttl; + + /** + * Username when lazy-connect. + */ + private ?string $username = null; + + /** + * Password when lazy-connect. + */ + private ?string $password = null; + + /** + * Connection options when lazy-connect. + */ + private array $connectionOptions = []; + + /** + * The strategy for locking, see constants. + */ + private int $lockMode = self::LOCK_TRANSACTIONAL; + + /** + * It's an array to support multiple reads before closing which is manual, non-standard usage. + * + * @var \PDOStatement[] An array of statements to release advisory locks + */ + private array $unlockStatements = []; + + /** + * True when the current session exists but expired according to session.gc_maxlifetime. + */ + private bool $sessionExpired = false; + + /** + * Whether a transaction is active. + */ + private bool $inTransaction = false; + + /** + * Whether gc() has been called. + */ + private bool $gcCalled = false; + + /** + * You can either pass an existing database connection as PDO instance or + * pass a DSN string that will be used to lazy-connect to the database + * when the session is actually used. Furthermore it's possible to pass null + * which will then use the session.save_path ini setting as PDO DSN parameter. + * + * List of available options: + * * db_table: The name of the table [default: sessions] + * * db_id_col: The column where to store the session id [default: sess_id] + * * db_data_col: The column where to store the session data [default: sess_data] + * * db_lifetime_col: The column where to store the lifetime [default: sess_lifetime] + * * db_time_col: The column where to store the timestamp [default: sess_time] + * * db_username: The username when lazy-connect [default: ''] + * * db_password: The password when lazy-connect [default: ''] + * * db_connection_options: An array of driver-specific connection options [default: []] + * * lock_mode: The strategy for locking, see constants [default: LOCK_TRANSACTIONAL] + * * ttl: The time to live in seconds. + * + * @param \PDO|string|null $pdoOrDsn A \PDO instance or DSN string or URL string or null + * + * @throws \InvalidArgumentException When PDO error mode is not PDO::ERRMODE_EXCEPTION + */ + public function __construct(#[\SensitiveParameter] \PDO|string|null $pdoOrDsn = null, #[\SensitiveParameter] array $options = []) + { + if ($pdoOrDsn instanceof \PDO) { + if (\PDO::ERRMODE_EXCEPTION !== $pdoOrDsn->getAttribute(\PDO::ATTR_ERRMODE)) { + throw new \InvalidArgumentException(\sprintf('"%s" requires PDO error mode attribute be set to throw Exceptions (i.e. $pdo->setAttribute(PDO::ATTR_ERRMODE, PDO::ERRMODE_EXCEPTION)).', __CLASS__)); + } + + $this->pdo = $pdoOrDsn; + $this->driver = $this->pdo->getAttribute(\PDO::ATTR_DRIVER_NAME); + } elseif (\is_string($pdoOrDsn) && str_contains($pdoOrDsn, '://')) { + $this->dsn = $this->buildDsnFromUrl($pdoOrDsn); + } else { + $this->dsn = $pdoOrDsn; + } + + $this->table = $options['db_table'] ?? $this->table; + $this->idCol = $options['db_id_col'] ?? $this->idCol; + $this->dataCol = $options['db_data_col'] ?? $this->dataCol; + $this->lifetimeCol = $options['db_lifetime_col'] ?? $this->lifetimeCol; + $this->timeCol = $options['db_time_col'] ?? $this->timeCol; + $this->username = $options['db_username'] ?? $this->username; + $this->password = $options['db_password'] ?? $this->password; + $this->connectionOptions = $options['db_connection_options'] ?? $this->connectionOptions; + $this->lockMode = $options['lock_mode'] ?? $this->lockMode; + $this->ttl = $options['ttl'] ?? null; + } + + /** + * Adds the Table to the Schema if it doesn't exist. + */ + public function configureSchema(Schema $schema, ?\Closure $isSameDatabase = null): void + { + if ($schema->hasTable($this->table) || ($isSameDatabase && !$isSameDatabase($this->getConnection()->exec(...)))) { + return; + } + + $table = $schema->createTable($this->table); + switch ($this->driver) { + case 'mysql': + $table->addColumn($this->idCol, Types::BINARY)->setLength(128)->setNotnull(true); + $table->addColumn($this->dataCol, Types::BLOB)->setNotnull(true); + $table->addColumn($this->lifetimeCol, Types::INTEGER)->setUnsigned(true)->setNotnull(true); + $table->addColumn($this->timeCol, Types::INTEGER)->setUnsigned(true)->setNotnull(true); + $table->addOption('collate', 'utf8mb4_bin'); + $table->addOption('engine', 'InnoDB'); + break; + case 'sqlite': + $table->addColumn($this->idCol, Types::TEXT)->setNotnull(true); + $table->addColumn($this->dataCol, Types::BLOB)->setNotnull(true); + $table->addColumn($this->lifetimeCol, Types::INTEGER)->setNotnull(true); + $table->addColumn($this->timeCol, Types::INTEGER)->setNotnull(true); + break; + case 'pgsql': + $table->addColumn($this->idCol, Types::STRING)->setLength(128)->setNotnull(true); + $table->addColumn($this->dataCol, Types::BINARY)->setNotnull(true); + $table->addColumn($this->lifetimeCol, Types::INTEGER)->setNotnull(true); + $table->addColumn($this->timeCol, Types::INTEGER)->setNotnull(true); + break; + case 'oci': + $table->addColumn($this->idCol, Types::STRING)->setLength(128)->setNotnull(true); + $table->addColumn($this->dataCol, Types::BLOB)->setNotnull(true); + $table->addColumn($this->lifetimeCol, Types::INTEGER)->setNotnull(true); + $table->addColumn($this->timeCol, Types::INTEGER)->setNotnull(true); + break; + case 'sqlsrv': + $table->addColumn($this->idCol, Types::STRING)->setLength(128)->setNotnull(true); + $table->addColumn($this->dataCol, Types::BLOB)->setNotnull(true); + $table->addColumn($this->lifetimeCol, Types::INTEGER)->setUnsigned(true)->setNotnull(true); + $table->addColumn($this->timeCol, Types::INTEGER)->setUnsigned(true)->setNotnull(true); + break; + default: + throw new \DomainException(\sprintf('Creating the session table is currently not implemented for PDO driver "%s".', $this->driver)); + } + $table->setPrimaryKey([$this->idCol]); + $table->addIndex([$this->lifetimeCol], $this->lifetimeCol.'_idx'); + } + + /** + * Creates the table to store sessions which can be called once for setup. + * + * Session ID is saved in a column of maximum length 128 because that is enough even + * for a 512 bit configured session.hash_function like Whirlpool. Session data is + * saved in a BLOB. One could also use a shorter inlined varbinary column + * if one was sure the data fits into it. + * + * @return void + * + * @throws \PDOException When the table already exists + * @throws \DomainException When an unsupported PDO driver is used + */ + public function createTable() + { + // connect if we are not yet + $this->getConnection(); + + $sql = match ($this->driver) { + // We use varbinary for the ID column because it prevents unwanted conversions: + // - character set conversions between server and client + // - trailing space removal + // - case-insensitivity + // - language processing like é == e + 'mysql' => "CREATE TABLE $this->table ($this->idCol VARBINARY(128) NOT NULL PRIMARY KEY, $this->dataCol BLOB NOT NULL, $this->lifetimeCol INTEGER UNSIGNED NOT NULL, $this->timeCol INTEGER UNSIGNED NOT NULL) COLLATE utf8mb4_bin, ENGINE = InnoDB", + 'sqlite' => "CREATE TABLE $this->table ($this->idCol TEXT NOT NULL PRIMARY KEY, $this->dataCol BLOB NOT NULL, $this->lifetimeCol INTEGER NOT NULL, $this->timeCol INTEGER NOT NULL)", + 'pgsql' => "CREATE TABLE $this->table ($this->idCol VARCHAR(128) NOT NULL PRIMARY KEY, $this->dataCol BYTEA NOT NULL, $this->lifetimeCol INTEGER NOT NULL, $this->timeCol INTEGER NOT NULL)", + 'oci' => "CREATE TABLE $this->table ($this->idCol VARCHAR2(128) NOT NULL PRIMARY KEY, $this->dataCol BLOB NOT NULL, $this->lifetimeCol INTEGER NOT NULL, $this->timeCol INTEGER NOT NULL)", + 'sqlsrv' => "CREATE TABLE $this->table ($this->idCol VARCHAR(128) NOT NULL PRIMARY KEY, $this->dataCol VARBINARY(MAX) NOT NULL, $this->lifetimeCol INTEGER NOT NULL, $this->timeCol INTEGER NOT NULL)", + default => throw new \DomainException(\sprintf('Creating the session table is currently not implemented for PDO driver "%s".', $this->driver)), + }; + + try { + $this->pdo->exec($sql); + $this->pdo->exec("CREATE INDEX {$this->lifetimeCol}_idx ON $this->table ($this->lifetimeCol)"); + } catch (\PDOException $e) { + $this->rollback(); + + throw $e; + } + } + + /** + * Returns true when the current session exists but expired according to session.gc_maxlifetime. + * + * Can be used to distinguish between a new session and one that expired due to inactivity. + */ + public function isSessionExpired(): bool + { + return $this->sessionExpired; + } + + public function open(string $savePath, string $sessionName): bool + { + $this->sessionExpired = false; + + if (!isset($this->pdo)) { + $this->connect($this->dsn ?: $savePath); + } + + return parent::open($savePath, $sessionName); + } + + public function read(#[\SensitiveParameter] string $sessionId): string + { + try { + return parent::read($sessionId); + } catch (\PDOException $e) { + $this->rollback(); + + throw $e; + } + } + + public function gc(int $maxlifetime): int|false + { + // We delay gc() to close() so that it is executed outside the transactional and blocking read-write process. + // This way, pruning expired sessions does not block them from being started while the current session is used. + $this->gcCalled = true; + + return 0; + } + + protected function doDestroy(#[\SensitiveParameter] string $sessionId): bool + { + // delete the record associated with this id + $sql = "DELETE FROM $this->table WHERE $this->idCol = :id"; + + try { + $stmt = $this->pdo->prepare($sql); + $stmt->bindParam(':id', $sessionId, \PDO::PARAM_STR); + $stmt->execute(); + } catch (\PDOException $e) { + $this->rollback(); + + throw $e; + } + + return true; + } + + protected function doWrite(#[\SensitiveParameter] string $sessionId, string $data): bool + { + $maxlifetime = (int) (($this->ttl instanceof \Closure ? ($this->ttl)() : $this->ttl) ?? \ini_get('session.gc_maxlifetime')); + + try { + // We use a single MERGE SQL query when supported by the database. + $mergeStmt = $this->getMergeStatement($sessionId, $data, $maxlifetime); + if (null !== $mergeStmt) { + $mergeStmt->execute(); + + return true; + } + + $updateStmt = $this->getUpdateStatement($sessionId, $data, $maxlifetime); + $updateStmt->execute(); + + // When MERGE is not supported, like in Postgres < 9.5, we have to use this approach that can result in + // duplicate key errors when the same session is written simultaneously (given the LOCK_NONE behavior). + // We can just catch such an error and re-execute the update. This is similar to a serializable + // transaction with retry logic on serialization failures but without the overhead and without possible + // false positives due to longer gap locking. + if (!$updateStmt->rowCount()) { + try { + $insertStmt = $this->getInsertStatement($sessionId, $data, $maxlifetime); + $insertStmt->execute(); + } catch (\PDOException $e) { + // Handle integrity violation SQLSTATE 23000 (or a subclass like 23505 in Postgres) for duplicate keys + if (str_starts_with($e->getCode(), '23')) { + $updateStmt->execute(); + } else { + throw $e; + } + } + } + } catch (\PDOException $e) { + $this->rollback(); + + throw $e; + } + + return true; + } + + public function updateTimestamp(#[\SensitiveParameter] string $sessionId, string $data): bool + { + $expiry = time() + (int) (($this->ttl instanceof \Closure ? ($this->ttl)() : $this->ttl) ?? \ini_get('session.gc_maxlifetime')); + + try { + $updateStmt = $this->pdo->prepare( + "UPDATE $this->table SET $this->lifetimeCol = :expiry, $this->timeCol = :time WHERE $this->idCol = :id" + ); + $updateStmt->bindValue(':id', $sessionId, \PDO::PARAM_STR); + $updateStmt->bindValue(':expiry', $expiry, \PDO::PARAM_INT); + $updateStmt->bindValue(':time', time(), \PDO::PARAM_INT); + $updateStmt->execute(); + } catch (\PDOException $e) { + $this->rollback(); + + throw $e; + } + + return true; + } + + public function close(): bool + { + $this->commit(); + + while ($unlockStmt = array_shift($this->unlockStatements)) { + $unlockStmt->execute(); + } + + if ($this->gcCalled) { + $this->gcCalled = false; + + // delete the session records that have expired + $sql = "DELETE FROM $this->table WHERE $this->lifetimeCol < :time"; + $stmt = $this->pdo->prepare($sql); + $stmt->bindValue(':time', time(), \PDO::PARAM_INT); + $stmt->execute(); + } + + if (false !== $this->dsn) { + unset($this->pdo, $this->driver); // only close lazy-connection + } + + return true; + } + + /** + * Lazy-connects to the database. + */ + private function connect(#[\SensitiveParameter] string $dsn): void + { + $this->pdo = new \PDO($dsn, $this->username, $this->password, $this->connectionOptions); + $this->pdo->setAttribute(\PDO::ATTR_ERRMODE, \PDO::ERRMODE_EXCEPTION); + $this->driver = $this->pdo->getAttribute(\PDO::ATTR_DRIVER_NAME); + } + + /** + * Builds a PDO DSN from a URL-like connection string. + * + * @todo implement missing support for oci DSN (which look totally different from other PDO ones) + */ + private function buildDsnFromUrl(#[\SensitiveParameter] string $dsnOrUrl): string + { + // (pdo_)?sqlite3?:///... => (pdo_)?sqlite3?://localhost/... or else the URL will be invalid + $url = preg_replace('#^((?:pdo_)?sqlite3?):///#', '$1://localhost/', $dsnOrUrl); + + $params = parse_url($url); + + if (false === $params) { + return $dsnOrUrl; // If the URL is not valid, let's assume it might be a DSN already. + } + + $params = array_map('rawurldecode', $params); + + // Override the default username and password. Values passed through options will still win over these in the constructor. + if (isset($params['user'])) { + $this->username = $params['user']; + } + + if (isset($params['pass'])) { + $this->password = $params['pass']; + } + + if (!isset($params['scheme'])) { + throw new \InvalidArgumentException('URLs without scheme are not supported to configure the PdoSessionHandler.'); + } + + $driverAliasMap = [ + 'mssql' => 'sqlsrv', + 'mysql2' => 'mysql', // Amazon RDS, for some weird reason + 'postgres' => 'pgsql', + 'postgresql' => 'pgsql', + 'sqlite3' => 'sqlite', + ]; + + $driver = $driverAliasMap[$params['scheme']] ?? $params['scheme']; + + // Doctrine DBAL supports passing its internal pdo_* driver names directly too (allowing both dashes and underscores). This allows supporting the same here. + if (str_starts_with($driver, 'pdo_') || str_starts_with($driver, 'pdo-')) { + $driver = substr($driver, 4); + } + + $dsn = null; + switch ($driver) { + case 'mysql': + $dsn = 'mysql:'; + if ('' !== ($params['query'] ?? '')) { + $queryParams = []; + parse_str($params['query'], $queryParams); + if ('' !== ($queryParams['charset'] ?? '')) { + $dsn .= 'charset='.$queryParams['charset'].';'; + } + + if ('' !== ($queryParams['unix_socket'] ?? '')) { + $dsn .= 'unix_socket='.$queryParams['unix_socket'].';'; + + if (isset($params['path'])) { + $dbName = substr($params['path'], 1); // Remove the leading slash + $dsn .= 'dbname='.$dbName.';'; + } + + return $dsn; + } + } + // If "unix_socket" is not in the query, we continue with the same process as pgsql + // no break + case 'pgsql': + $dsn ??= 'pgsql:'; + + if (isset($params['host']) && '' !== $params['host']) { + $dsn .= 'host='.$params['host'].';'; + } + + if (isset($params['port']) && '' !== $params['port']) { + $dsn .= 'port='.$params['port'].';'; + } + + if (isset($params['path'])) { + $dbName = substr($params['path'], 1); // Remove the leading slash + $dsn .= 'dbname='.$dbName.';'; + } + + return $dsn; + + case 'sqlite': + return 'sqlite:'.substr($params['path'], 1); + + case 'sqlsrv': + $dsn = 'sqlsrv:server='; + + if (isset($params['host'])) { + $dsn .= $params['host']; + } + + if (isset($params['port']) && '' !== $params['port']) { + $dsn .= ','.$params['port']; + } + + if (isset($params['path'])) { + $dbName = substr($params['path'], 1); // Remove the leading slash + $dsn .= ';Database='.$dbName; + } + + return $dsn; + + default: + throw new \InvalidArgumentException(\sprintf('The scheme "%s" is not supported by the PdoSessionHandler URL configuration. Pass a PDO DSN directly.', $params['scheme'])); + } + } + + /** + * Helper method to begin a transaction. + * + * Since SQLite does not support row level locks, we have to acquire a reserved lock + * on the database immediately. Because of https://bugs.php.net/42766 we have to create + * such a transaction manually which also means we cannot use PDO::commit or + * PDO::rollback or PDO::inTransaction for SQLite. + * + * Also MySQLs default isolation, REPEATABLE READ, causes deadlock for different sessions + * due to https://percona.com/blog/2013/12/12/one-more-innodb-gap-lock-to-avoid/ . + * So we change it to READ COMMITTED. + */ + private function beginTransaction(): void + { + if (!$this->inTransaction) { + if ('sqlite' === $this->driver) { + $this->pdo->exec('BEGIN IMMEDIATE TRANSACTION'); + } else { + if ('mysql' === $this->driver) { + $this->pdo->exec('SET TRANSACTION ISOLATION LEVEL READ COMMITTED'); + } + $this->pdo->beginTransaction(); + } + $this->inTransaction = true; + } + } + + /** + * Helper method to commit a transaction. + */ + private function commit(): void + { + if ($this->inTransaction) { + try { + // commit read-write transaction which also releases the lock + if ('sqlite' === $this->driver) { + $this->pdo->exec('COMMIT'); + } else { + $this->pdo->commit(); + } + $this->inTransaction = false; + } catch (\PDOException $e) { + $this->rollback(); + + throw $e; + } + } + } + + /** + * Helper method to rollback a transaction. + */ + private function rollback(): void + { + // We only need to rollback if we are in a transaction. Otherwise the resulting + // error would hide the real problem why rollback was called. We might not be + // in a transaction when not using the transactional locking behavior or when + // two callbacks (e.g. destroy and write) are invoked that both fail. + if ($this->inTransaction) { + if ('sqlite' === $this->driver) { + $this->pdo->exec('ROLLBACK'); + } else { + $this->pdo->rollBack(); + } + $this->inTransaction = false; + } + } + + /** + * Reads the session data in respect to the different locking strategies. + * + * We need to make sure we do not return session data that is already considered garbage according + * to the session.gc_maxlifetime setting because gc() is called after read() and only sometimes. + */ + protected function doRead(#[\SensitiveParameter] string $sessionId): string + { + if (self::LOCK_ADVISORY === $this->lockMode) { + $this->unlockStatements[] = $this->doAdvisoryLock($sessionId); + } + + $selectSql = $this->getSelectSql(); + $selectStmt = $this->pdo->prepare($selectSql); + $selectStmt->bindParam(':id', $sessionId, \PDO::PARAM_STR); + $insertStmt = null; + + while (true) { + $selectStmt->execute(); + $sessionRows = $selectStmt->fetchAll(\PDO::FETCH_NUM); + + if ($sessionRows) { + $expiry = (int) $sessionRows[0][1]; + + if ($expiry < time()) { + $this->sessionExpired = true; + + return ''; + } + + return \is_resource($sessionRows[0][0]) ? stream_get_contents($sessionRows[0][0]) : $sessionRows[0][0]; + } + + if (null !== $insertStmt) { + $this->rollback(); + throw new \RuntimeException('Failed to read session: INSERT reported a duplicate id but next SELECT did not return any data.'); + } + + if (!filter_var(\ini_get('session.use_strict_mode'), \FILTER_VALIDATE_BOOL) && self::LOCK_TRANSACTIONAL === $this->lockMode && 'sqlite' !== $this->driver) { + // In strict mode, session fixation is not possible: new sessions always start with a unique + // random id, so that concurrency is not possible and this code path can be skipped. + // Exclusive-reading of non-existent rows does not block, so we need to do an insert to block + // until other connections to the session are committed. + try { + $insertStmt = $this->getInsertStatement($sessionId, '', 0); + $insertStmt->execute(); + } catch (\PDOException $e) { + // Catch duplicate key error because other connection created the session already. + // It would only not be the case when the other connection destroyed the session. + if (str_starts_with($e->getCode(), '23')) { + // Retrieve finished session data written by concurrent connection by restarting the loop. + // We have to start a new transaction as a failed query will mark the current transaction as + // aborted in PostgreSQL and disallow further queries within it. + $this->rollback(); + $this->beginTransaction(); + continue; + } + + throw $e; + } + } + + return ''; + } + } + + /** + * Executes an application-level lock on the database. + * + * @return \PDOStatement The statement that needs to be executed later to release the lock + * + * @throws \DomainException When an unsupported PDO driver is used + * + * @todo implement missing advisory locks + * - for oci using DBMS_LOCK.REQUEST + * - for sqlsrv using sp_getapplock with LockOwner = Session + */ + private function doAdvisoryLock(#[\SensitiveParameter] string $sessionId): \PDOStatement + { + switch ($this->driver) { + case 'mysql': + // MySQL 5.7.5 and later enforces a maximum length on lock names of 64 characters. Previously, no limit was enforced. + $lockId = substr($sessionId, 0, 64); + // should we handle the return value? 0 on timeout, null on error + // we use a timeout of 50 seconds which is also the default for innodb_lock_wait_timeout + $stmt = $this->pdo->prepare('SELECT GET_LOCK(:key, 50)'); + $stmt->bindValue(':key', $lockId, \PDO::PARAM_STR); + $stmt->execute(); + + $releaseStmt = $this->pdo->prepare('DO RELEASE_LOCK(:key)'); + $releaseStmt->bindValue(':key', $lockId, \PDO::PARAM_STR); + + return $releaseStmt; + case 'pgsql': + // Obtaining an exclusive session level advisory lock requires an integer key. + // When session.sid_bits_per_character > 4, the session id can contain non-hex-characters. + // So we cannot just use hexdec(). + if (4 === \PHP_INT_SIZE) { + $sessionInt1 = $this->convertStringToInt($sessionId); + $sessionInt2 = $this->convertStringToInt(substr($sessionId, 4, 4)); + + $stmt = $this->pdo->prepare('SELECT pg_advisory_lock(:key1, :key2)'); + $stmt->bindValue(':key1', $sessionInt1, \PDO::PARAM_INT); + $stmt->bindValue(':key2', $sessionInt2, \PDO::PARAM_INT); + $stmt->execute(); + + $releaseStmt = $this->pdo->prepare('SELECT pg_advisory_unlock(:key1, :key2)'); + $releaseStmt->bindValue(':key1', $sessionInt1, \PDO::PARAM_INT); + $releaseStmt->bindValue(':key2', $sessionInt2, \PDO::PARAM_INT); + } else { + $sessionBigInt = $this->convertStringToInt($sessionId); + + $stmt = $this->pdo->prepare('SELECT pg_advisory_lock(:key)'); + $stmt->bindValue(':key', $sessionBigInt, \PDO::PARAM_INT); + $stmt->execute(); + + $releaseStmt = $this->pdo->prepare('SELECT pg_advisory_unlock(:key)'); + $releaseStmt->bindValue(':key', $sessionBigInt, \PDO::PARAM_INT); + } + + return $releaseStmt; + case 'sqlite': + throw new \DomainException('SQLite does not support advisory locks.'); + default: + throw new \DomainException(\sprintf('Advisory locks are currently not implemented for PDO driver "%s".', $this->driver)); + } + } + + /** + * Encodes the first 4 (when PHP_INT_SIZE == 4) or 8 characters of the string as an integer. + * + * Keep in mind, PHP integers are signed. + */ + private function convertStringToInt(string $string): int + { + if (4 === \PHP_INT_SIZE) { + return (\ord($string[3]) << 24) + (\ord($string[2]) << 16) + (\ord($string[1]) << 8) + \ord($string[0]); + } + + $int1 = (\ord($string[7]) << 24) + (\ord($string[6]) << 16) + (\ord($string[5]) << 8) + \ord($string[4]); + $int2 = (\ord($string[3]) << 24) + (\ord($string[2]) << 16) + (\ord($string[1]) << 8) + \ord($string[0]); + + return $int2 + ($int1 << 32); + } + + /** + * Return a locking or nonlocking SQL query to read session information. + * + * @throws \DomainException When an unsupported PDO driver is used + */ + private function getSelectSql(): string + { + if (self::LOCK_TRANSACTIONAL === $this->lockMode) { + $this->beginTransaction(); + + switch ($this->driver) { + case 'mysql': + case 'oci': + case 'pgsql': + return "SELECT $this->dataCol, $this->lifetimeCol FROM $this->table WHERE $this->idCol = :id FOR UPDATE"; + case 'sqlsrv': + return "SELECT $this->dataCol, $this->lifetimeCol FROM $this->table WITH (UPDLOCK, ROWLOCK) WHERE $this->idCol = :id"; + case 'sqlite': + // we already locked when starting transaction + break; + default: + throw new \DomainException(\sprintf('Transactional locks are currently not implemented for PDO driver "%s".', $this->driver)); + } + } + + return "SELECT $this->dataCol, $this->lifetimeCol FROM $this->table WHERE $this->idCol = :id"; + } + + /** + * Returns an insert statement supported by the database for writing session data. + */ + private function getInsertStatement(#[\SensitiveParameter] string $sessionId, string $sessionData, int $maxlifetime): \PDOStatement + { + switch ($this->driver) { + case 'oci': + $data = fopen('php://memory', 'r+'); + fwrite($data, $sessionData); + rewind($data); + $sql = "INSERT INTO $this->table ($this->idCol, $this->dataCol, $this->lifetimeCol, $this->timeCol) VALUES (:id, EMPTY_BLOB(), :expiry, :time) RETURNING $this->dataCol into :data"; + break; + default: + $data = $sessionData; + $sql = "INSERT INTO $this->table ($this->idCol, $this->dataCol, $this->lifetimeCol, $this->timeCol) VALUES (:id, :data, :expiry, :time)"; + break; + } + + $stmt = $this->pdo->prepare($sql); + $stmt->bindParam(':id', $sessionId, \PDO::PARAM_STR); + $stmt->bindParam(':data', $data, \PDO::PARAM_LOB); + $stmt->bindValue(':expiry', time() + $maxlifetime, \PDO::PARAM_INT); + $stmt->bindValue(':time', time(), \PDO::PARAM_INT); + + return $stmt; + } + + /** + * Returns an update statement supported by the database for writing session data. + */ + private function getUpdateStatement(#[\SensitiveParameter] string $sessionId, string $sessionData, int $maxlifetime): \PDOStatement + { + switch ($this->driver) { + case 'oci': + $data = fopen('php://memory', 'r+'); + fwrite($data, $sessionData); + rewind($data); + $sql = "UPDATE $this->table SET $this->dataCol = EMPTY_BLOB(), $this->lifetimeCol = :expiry, $this->timeCol = :time WHERE $this->idCol = :id RETURNING $this->dataCol into :data"; + break; + default: + $data = $sessionData; + $sql = "UPDATE $this->table SET $this->dataCol = :data, $this->lifetimeCol = :expiry, $this->timeCol = :time WHERE $this->idCol = :id"; + break; + } + + $stmt = $this->pdo->prepare($sql); + $stmt->bindParam(':id', $sessionId, \PDO::PARAM_STR); + $stmt->bindParam(':data', $data, \PDO::PARAM_LOB); + $stmt->bindValue(':expiry', time() + $maxlifetime, \PDO::PARAM_INT); + $stmt->bindValue(':time', time(), \PDO::PARAM_INT); + + return $stmt; + } + + /** + * Returns a merge/upsert (i.e. insert or update) statement when supported by the database for writing session data. + */ + private function getMergeStatement(#[\SensitiveParameter] string $sessionId, string $data, int $maxlifetime): ?\PDOStatement + { + switch (true) { + case 'mysql' === $this->driver: + $mergeSql = "INSERT INTO $this->table ($this->idCol, $this->dataCol, $this->lifetimeCol, $this->timeCol) VALUES (:id, :data, :expiry, :time) ". + "ON DUPLICATE KEY UPDATE $this->dataCol = VALUES($this->dataCol), $this->lifetimeCol = VALUES($this->lifetimeCol), $this->timeCol = VALUES($this->timeCol)"; + break; + case 'sqlsrv' === $this->driver && version_compare($this->pdo->getAttribute(\PDO::ATTR_SERVER_VERSION), '10', '>='): + // MERGE is only available since SQL Server 2008 and must be terminated by semicolon + // It also requires HOLDLOCK according to https://weblogs.sqlteam.com/dang/2009/01/31/upsert-race-condition-with-merge/ + $mergeSql = "MERGE INTO $this->table WITH (HOLDLOCK) USING (SELECT 1 AS dummy) AS src ON ($this->idCol = ?) ". + "WHEN NOT MATCHED THEN INSERT ($this->idCol, $this->dataCol, $this->lifetimeCol, $this->timeCol) VALUES (?, ?, ?, ?) ". + "WHEN MATCHED THEN UPDATE SET $this->dataCol = ?, $this->lifetimeCol = ?, $this->timeCol = ?;"; + break; + case 'sqlite' === $this->driver: + $mergeSql = "INSERT OR REPLACE INTO $this->table ($this->idCol, $this->dataCol, $this->lifetimeCol, $this->timeCol) VALUES (:id, :data, :expiry, :time)"; + break; + case 'pgsql' === $this->driver && version_compare($this->pdo->getAttribute(\PDO::ATTR_SERVER_VERSION), '9.5', '>='): + $mergeSql = "INSERT INTO $this->table ($this->idCol, $this->dataCol, $this->lifetimeCol, $this->timeCol) VALUES (:id, :data, :expiry, :time) ". + "ON CONFLICT ($this->idCol) DO UPDATE SET ($this->dataCol, $this->lifetimeCol, $this->timeCol) = (EXCLUDED.$this->dataCol, EXCLUDED.$this->lifetimeCol, EXCLUDED.$this->timeCol)"; + break; + default: + // MERGE is not supported with LOBs: https://oracle.com/technetwork/articles/fuecks-lobs-095315.html + return null; + } + + $mergeStmt = $this->pdo->prepare($mergeSql); + + if ('sqlsrv' === $this->driver) { + $mergeStmt->bindParam(1, $sessionId, \PDO::PARAM_STR); + $mergeStmt->bindParam(2, $sessionId, \PDO::PARAM_STR); + $mergeStmt->bindParam(3, $data, \PDO::PARAM_LOB); + $mergeStmt->bindValue(4, time() + $maxlifetime, \PDO::PARAM_INT); + $mergeStmt->bindValue(5, time(), \PDO::PARAM_INT); + $mergeStmt->bindParam(6, $data, \PDO::PARAM_LOB); + $mergeStmt->bindValue(7, time() + $maxlifetime, \PDO::PARAM_INT); + $mergeStmt->bindValue(8, time(), \PDO::PARAM_INT); + } else { + $mergeStmt->bindParam(':id', $sessionId, \PDO::PARAM_STR); + $mergeStmt->bindParam(':data', $data, \PDO::PARAM_LOB); + $mergeStmt->bindValue(':expiry', time() + $maxlifetime, \PDO::PARAM_INT); + $mergeStmt->bindValue(':time', time(), \PDO::PARAM_INT); + } + + return $mergeStmt; + } + + /** + * Return a PDO instance. + */ + protected function getConnection(): \PDO + { + if (!isset($this->pdo)) { + $this->connect($this->dsn ?: \ini_get('session.save_path')); + } + + return $this->pdo; + } +} diff --git a/3rdparty/symfony/http-foundation/Session/Storage/Handler/RedisSessionHandler.php b/3rdparty/symfony/http-foundation/Session/Storage/Handler/RedisSessionHandler.php new file mode 100644 index 00000000..78cd4e7c --- /dev/null +++ b/3rdparty/symfony/http-foundation/Session/Storage/Handler/RedisSessionHandler.php @@ -0,0 +1,103 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Component\HttpFoundation\Session\Storage\Handler; + +use Predis\Response\ErrorInterface; +use Relay\Relay; + +/** + * Redis based session storage handler based on the Redis class + * provided by the PHP redis extension. + * + * @author Dalibor Karlović + */ +class RedisSessionHandler extends AbstractSessionHandler +{ + /** + * Key prefix for shared environments. + */ + private string $prefix; + + /** + * Time to live in seconds. + */ + private int|\Closure|null $ttl; + + /** + * List of available options: + * * prefix: The prefix to use for the keys in order to avoid collision on the Redis server + * * ttl: The time to live in seconds. + * + * @throws \InvalidArgumentException When unsupported client or options are passed + */ + public function __construct( + private \Redis|Relay|\RedisArray|\RedisCluster|\Predis\ClientInterface $redis, + array $options = [], + ) { + if ($diff = array_diff(array_keys($options), ['prefix', 'ttl'])) { + throw new \InvalidArgumentException(\sprintf('The following options are not supported "%s".', implode(', ', $diff))); + } + + $this->prefix = $options['prefix'] ?? 'sf_s'; + $this->ttl = $options['ttl'] ?? null; + } + + protected function doRead(#[\SensitiveParameter] string $sessionId): string + { + return $this->redis->get($this->prefix.$sessionId) ?: ''; + } + + protected function doWrite(#[\SensitiveParameter] string $sessionId, string $data): bool + { + $ttl = ($this->ttl instanceof \Closure ? ($this->ttl)() : $this->ttl) ?? \ini_get('session.gc_maxlifetime'); + $result = $this->redis->setEx($this->prefix.$sessionId, (int) $ttl, $data); + + return $result && !$result instanceof ErrorInterface; + } + + protected function doDestroy(#[\SensitiveParameter] string $sessionId): bool + { + static $unlink = true; + + if ($unlink) { + try { + $unlink = false !== $this->redis->unlink($this->prefix.$sessionId); + } catch (\Throwable) { + $unlink = false; + } + } + + if (!$unlink) { + $this->redis->del($this->prefix.$sessionId); + } + + return true; + } + + #[\ReturnTypeWillChange] + public function close(): bool + { + return true; + } + + public function gc(int $maxlifetime): int|false + { + return 0; + } + + public function updateTimestamp(#[\SensitiveParameter] string $sessionId, string $data): bool + { + $ttl = ($this->ttl instanceof \Closure ? ($this->ttl)() : $this->ttl) ?? \ini_get('session.gc_maxlifetime'); + + return $this->redis->expire($this->prefix.$sessionId, (int) $ttl); + } +} diff --git a/3rdparty/symfony/http-foundation/Session/Storage/Handler/SessionHandlerFactory.php b/3rdparty/symfony/http-foundation/Session/Storage/Handler/SessionHandlerFactory.php new file mode 100644 index 00000000..43a9eb84 --- /dev/null +++ b/3rdparty/symfony/http-foundation/Session/Storage/Handler/SessionHandlerFactory.php @@ -0,0 +1,99 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Component\HttpFoundation\Session\Storage\Handler; + +use Doctrine\DBAL\Configuration; +use Doctrine\DBAL\DriverManager; +use Doctrine\DBAL\Schema\DefaultSchemaManagerFactory; +use Doctrine\DBAL\Tools\DsnParser; +use Relay\Relay; +use Symfony\Component\Cache\Adapter\AbstractAdapter; + +/** + * @author Nicolas Grekas + */ +class SessionHandlerFactory +{ + public static function createHandler(object|string $connection, array $options = []): AbstractSessionHandler + { + if ($query = \is_string($connection) ? parse_url($connection) : false) { + parse_str($query['query'] ?? '', $query); + + if (($options['ttl'] ?? null) instanceof \Closure) { + $query['ttl'] = $options['ttl']; + } + } + $options = ($query ?: []) + $options; + + switch (true) { + case $connection instanceof \Redis: + case $connection instanceof Relay: + case $connection instanceof \RedisArray: + case $connection instanceof \RedisCluster: + case $connection instanceof \Predis\ClientInterface: + return new RedisSessionHandler($connection); + + case $connection instanceof \Memcached: + return new MemcachedSessionHandler($connection); + + case $connection instanceof \PDO: + return new PdoSessionHandler($connection); + + case !\is_string($connection): + throw new \InvalidArgumentException(\sprintf('Unsupported Connection: "%s".', get_debug_type($connection))); + case str_starts_with($connection, 'file://'): + $savePath = substr($connection, 7); + + return new StrictSessionHandler(new NativeFileSessionHandler('' === $savePath ? null : $savePath)); + + case str_starts_with($connection, 'redis:'): + case str_starts_with($connection, 'rediss:'): + case str_starts_with($connection, 'memcached:'): + if (!class_exists(AbstractAdapter::class)) { + throw new \InvalidArgumentException('Unsupported Redis or Memcached DSN. Try running "composer require symfony/cache".'); + } + $handlerClass = str_starts_with($connection, 'memcached:') ? MemcachedSessionHandler::class : RedisSessionHandler::class; + $connection = AbstractAdapter::createConnection($connection, ['lazy' => true]); + + return new $handlerClass($connection, array_intersect_key($options, ['prefix' => 1, 'ttl' => 1])); + + case str_starts_with($connection, 'pdo_oci://'): + if (!class_exists(DriverManager::class)) { + throw new \InvalidArgumentException('Unsupported PDO OCI DSN. Try running "composer require doctrine/dbal".'); + } + $connection[3] = '-'; + $params = class_exists(DsnParser::class) ? (new DsnParser())->parse($connection) : ['url' => $connection]; + $config = new Configuration(); + if (class_exists(DefaultSchemaManagerFactory::class)) { + $config->setSchemaManagerFactory(new DefaultSchemaManagerFactory()); + } + + $connection = DriverManager::getConnection($params, $config); + // The condition should be removed once support for DBAL <3.3 is dropped + $connection = method_exists($connection, 'getNativeConnection') ? $connection->getNativeConnection() : $connection->getWrappedConnection(); + // no break; + + case str_starts_with($connection, 'mssql://'): + case str_starts_with($connection, 'mysql://'): + case str_starts_with($connection, 'mysql2://'): + case str_starts_with($connection, 'pgsql://'): + case str_starts_with($connection, 'postgres://'): + case str_starts_with($connection, 'postgresql://'): + case str_starts_with($connection, 'sqlsrv://'): + case str_starts_with($connection, 'sqlite://'): + case str_starts_with($connection, 'sqlite3://'): + return new PdoSessionHandler($connection, $options); + } + + throw new \InvalidArgumentException(\sprintf('Unsupported Connection: "%s".', $connection)); + } +} diff --git a/3rdparty/symfony/http-foundation/Session/Storage/Handler/StrictSessionHandler.php b/3rdparty/symfony/http-foundation/Session/Storage/Handler/StrictSessionHandler.php new file mode 100644 index 00000000..38afc125 --- /dev/null +++ b/3rdparty/symfony/http-foundation/Session/Storage/Handler/StrictSessionHandler.php @@ -0,0 +1,89 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Component\HttpFoundation\Session\Storage\Handler; + +/** + * Adds basic `SessionUpdateTimestampHandlerInterface` behaviors to another `SessionHandlerInterface`. + * + * @author Nicolas Grekas + */ +class StrictSessionHandler extends AbstractSessionHandler +{ + private \SessionHandlerInterface $handler; + private bool $doDestroy; + + public function __construct(\SessionHandlerInterface $handler) + { + if ($handler instanceof \SessionUpdateTimestampHandlerInterface) { + throw new \LogicException(\sprintf('"%s" is already an instance of "SessionUpdateTimestampHandlerInterface", you cannot wrap it with "%s".', get_debug_type($handler), self::class)); + } + + $this->handler = $handler; + } + + /** + * Returns true if this handler wraps an internal PHP session save handler using \SessionHandler. + * + * @internal + */ + public function isWrapper(): bool + { + return $this->handler instanceof \SessionHandler; + } + + public function open(string $savePath, string $sessionName): bool + { + parent::open($savePath, $sessionName); + + return $this->handler->open($savePath, $sessionName); + } + + protected function doRead(#[\SensitiveParameter] string $sessionId): string + { + return $this->handler->read($sessionId); + } + + public function updateTimestamp(#[\SensitiveParameter] string $sessionId, string $data): bool + { + return $this->write($sessionId, $data); + } + + protected function doWrite(#[\SensitiveParameter] string $sessionId, string $data): bool + { + return $this->handler->write($sessionId, $data); + } + + public function destroy(#[\SensitiveParameter] string $sessionId): bool + { + $this->doDestroy = true; + $destroyed = parent::destroy($sessionId); + + return $this->doDestroy ? $this->doDestroy($sessionId) : $destroyed; + } + + protected function doDestroy(#[\SensitiveParameter] string $sessionId): bool + { + $this->doDestroy = false; + + return $this->handler->destroy($sessionId); + } + + public function close(): bool + { + return $this->handler->close(); + } + + public function gc(int $maxlifetime): int|false + { + return $this->handler->gc($maxlifetime); + } +} diff --git a/3rdparty/symfony/http-foundation/Session/Storage/MetadataBag.php b/3rdparty/symfony/http-foundation/Session/Storage/MetadataBag.php new file mode 100644 index 00000000..5bb4cfbc --- /dev/null +++ b/3rdparty/symfony/http-foundation/Session/Storage/MetadataBag.php @@ -0,0 +1,148 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Component\HttpFoundation\Session\Storage; + +use Symfony\Component\HttpFoundation\Session\SessionBagInterface; + +/** + * Metadata container. + * + * Adds metadata to the session. + * + * @author Drak + */ +class MetadataBag implements SessionBagInterface +{ + public const CREATED = 'c'; + public const UPDATED = 'u'; + public const LIFETIME = 'l'; + + private string $name = '__metadata'; + private string $storageKey; + + /** + * @var array + */ + protected $meta = [self::CREATED => 0, self::UPDATED => 0, self::LIFETIME => 0]; + + /** + * Unix timestamp. + */ + private int $lastUsed; + + private int $updateThreshold; + + /** + * @param string $storageKey The key used to store bag in the session + * @param int $updateThreshold The time to wait between two UPDATED updates + */ + public function __construct(string $storageKey = '_sf2_meta', int $updateThreshold = 0) + { + $this->storageKey = $storageKey; + $this->updateThreshold = $updateThreshold; + } + + /** + * @return void + */ + public function initialize(array &$array) + { + $this->meta = &$array; + + if (isset($array[self::CREATED])) { + $this->lastUsed = $this->meta[self::UPDATED]; + + $timeStamp = time(); + if ($timeStamp - $array[self::UPDATED] >= $this->updateThreshold) { + $this->meta[self::UPDATED] = $timeStamp; + } + } else { + $this->stampCreated(); + } + } + + /** + * Gets the lifetime that the session cookie was set with. + */ + public function getLifetime(): int + { + return $this->meta[self::LIFETIME]; + } + + /** + * Stamps a new session's metadata. + * + * @param int|null $lifetime Sets the cookie lifetime for the session cookie. A null value + * will leave the system settings unchanged, 0 sets the cookie + * to expire with browser session. Time is in seconds, and is + * not a Unix timestamp. + * + * @return void + */ + public function stampNew(?int $lifetime = null) + { + $this->stampCreated($lifetime); + } + + public function getStorageKey(): string + { + return $this->storageKey; + } + + /** + * Gets the created timestamp metadata. + * + * @return int Unix timestamp + */ + public function getCreated(): int + { + return $this->meta[self::CREATED]; + } + + /** + * Gets the last used metadata. + * + * @return int Unix timestamp + */ + public function getLastUsed(): int + { + return $this->lastUsed; + } + + public function clear(): mixed + { + // nothing to do + return null; + } + + public function getName(): string + { + return $this->name; + } + + /** + * Sets name. + * + * @return void + */ + public function setName(string $name) + { + $this->name = $name; + } + + private function stampCreated(?int $lifetime = null): void + { + $timeStamp = time(); + $this->meta[self::CREATED] = $this->meta[self::UPDATED] = $this->lastUsed = $timeStamp; + $this->meta[self::LIFETIME] = $lifetime ?? (int) \ini_get('session.cookie_lifetime'); + } +} diff --git a/3rdparty/symfony/http-foundation/Session/Storage/MockArraySessionStorage.php b/3rdparty/symfony/http-foundation/Session/Storage/MockArraySessionStorage.php new file mode 100644 index 00000000..65ab34f9 --- /dev/null +++ b/3rdparty/symfony/http-foundation/Session/Storage/MockArraySessionStorage.php @@ -0,0 +1,238 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Component\HttpFoundation\Session\Storage; + +use Symfony\Component\HttpFoundation\Session\SessionBagInterface; + +/** + * MockArraySessionStorage mocks the session for unit tests. + * + * No PHP session is actually started since a session can be initialized + * and shutdown only once per PHP execution cycle. + * + * When doing functional testing, you should use MockFileSessionStorage instead. + * + * @author Fabien Potencier + * @author Bulat Shakirzyanov + * @author Drak + */ +class MockArraySessionStorage implements SessionStorageInterface +{ + /** + * @var string + */ + protected $id = ''; + + /** + * @var string + */ + protected $name; + + /** + * @var bool + */ + protected $started = false; + + /** + * @var bool + */ + protected $closed = false; + + /** + * @var array + */ + protected $data = []; + + /** + * @var MetadataBag + */ + protected $metadataBag; + + /** + * @var array|SessionBagInterface[] + */ + protected $bags = []; + + public function __construct(string $name = 'MOCKSESSID', ?MetadataBag $metaBag = null) + { + $this->name = $name; + $this->setMetadataBag($metaBag); + } + + /** + * @return void + */ + public function setSessionData(array $array) + { + $this->data = $array; + } + + public function start(): bool + { + if ($this->started) { + return true; + } + + if (empty($this->id)) { + $this->id = $this->generateId(); + } + + $this->loadSession(); + + return true; + } + + public function regenerate(bool $destroy = false, ?int $lifetime = null): bool + { + if (!$this->started) { + $this->start(); + } + + $this->metadataBag->stampNew($lifetime); + $this->id = $this->generateId(); + + return true; + } + + public function getId(): string + { + return $this->id; + } + + /** + * @return void + */ + public function setId(string $id) + { + if ($this->started) { + throw new \LogicException('Cannot set session ID after the session has started.'); + } + + $this->id = $id; + } + + public function getName(): string + { + return $this->name; + } + + /** + * @return void + */ + public function setName(string $name) + { + $this->name = $name; + } + + /** + * @return void + */ + public function save() + { + if (!$this->started || $this->closed) { + throw new \RuntimeException('Trying to save a session that was not started yet or was already closed.'); + } + // nothing to do since we don't persist the session data + $this->closed = false; + $this->started = false; + } + + /** + * @return void + */ + public function clear() + { + // clear out the bags + foreach ($this->bags as $bag) { + $bag->clear(); + } + + // clear out the session + $this->data = []; + + // reconnect the bags to the session + $this->loadSession(); + } + + /** + * @return void + */ + public function registerBag(SessionBagInterface $bag) + { + $this->bags[$bag->getName()] = $bag; + } + + public function getBag(string $name): SessionBagInterface + { + if (!isset($this->bags[$name])) { + throw new \InvalidArgumentException(\sprintf('The SessionBagInterface "%s" is not registered.', $name)); + } + + if (!$this->started) { + $this->start(); + } + + return $this->bags[$name]; + } + + public function isStarted(): bool + { + return $this->started; + } + + /** + * @return void + */ + public function setMetadataBag(?MetadataBag $bag = null) + { + if (1 > \func_num_args()) { + trigger_deprecation('symfony/http-foundation', '6.2', 'Calling "%s()" without any arguments is deprecated, pass null explicitly instead.', __METHOD__); + } + $this->metadataBag = $bag ?? new MetadataBag(); + } + + /** + * Gets the MetadataBag. + */ + public function getMetadataBag(): MetadataBag + { + return $this->metadataBag; + } + + /** + * Generates a session ID. + * + * This doesn't need to be particularly cryptographically secure since this is just + * a mock. + */ + protected function generateId(): string + { + return bin2hex(random_bytes(16)); + } + + /** + * @return void + */ + protected function loadSession() + { + $bags = array_merge($this->bags, [$this->metadataBag]); + + foreach ($bags as $bag) { + $key = $bag->getStorageKey(); + $this->data[$key] ??= []; + $bag->initialize($this->data[$key]); + } + + $this->started = true; + $this->closed = false; + } +} diff --git a/3rdparty/symfony/http-foundation/Session/Storage/MockFileSessionStorage.php b/3rdparty/symfony/http-foundation/Session/Storage/MockFileSessionStorage.php new file mode 100644 index 00000000..84c2c436 --- /dev/null +++ b/3rdparty/symfony/http-foundation/Session/Storage/MockFileSessionStorage.php @@ -0,0 +1,152 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Component\HttpFoundation\Session\Storage; + +/** + * MockFileSessionStorage is used to mock sessions for + * functional testing where you may need to persist session data + * across separate PHP processes. + * + * No PHP session is actually started since a session can be initialized + * and shutdown only once per PHP execution cycle and this class does + * not pollute any session related globals, including session_*() functions + * or session.* PHP ini directives. + * + * @author Drak + */ +class MockFileSessionStorage extends MockArraySessionStorage +{ + private string $savePath; + + /** + * @param string|null $savePath Path of directory to save session files + */ + public function __construct(?string $savePath = null, string $name = 'MOCKSESSID', ?MetadataBag $metaBag = null) + { + $savePath ??= sys_get_temp_dir(); + + if (!is_dir($savePath) && !@mkdir($savePath, 0777, true) && !is_dir($savePath)) { + throw new \RuntimeException(\sprintf('Session Storage was not able to create directory "%s".', $savePath)); + } + + $this->savePath = $savePath; + + parent::__construct($name, $metaBag); + } + + public function start(): bool + { + if ($this->started) { + return true; + } + + if (!$this->id) { + $this->id = $this->generateId(); + } + + $this->read(); + + $this->started = true; + + return true; + } + + public function regenerate(bool $destroy = false, ?int $lifetime = null): bool + { + if (!$this->started) { + $this->start(); + } + + if ($destroy) { + $this->destroy(); + } + + return parent::regenerate($destroy, $lifetime); + } + + /** + * @return void + */ + public function save() + { + if (!$this->started) { + throw new \RuntimeException('Trying to save a session that was not started yet or was already closed.'); + } + + $data = $this->data; + + foreach ($this->bags as $bag) { + if (empty($data[$key = $bag->getStorageKey()])) { + unset($data[$key]); + } + } + if ([$key = $this->metadataBag->getStorageKey()] === array_keys($data)) { + unset($data[$key]); + } + + try { + if ($data) { + $path = $this->getFilePath(); + $tmp = $path.bin2hex(random_bytes(6)); + file_put_contents($tmp, serialize($data)); + rename($tmp, $path); + } else { + $this->destroy(); + } + } finally { + $this->data = $data; + } + + // this is needed when the session object is re-used across multiple requests + // in functional tests. + $this->started = false; + } + + /** + * Deletes a session from persistent storage. + * Deliberately leaves session data in memory intact. + */ + private function destroy(): void + { + set_error_handler(static function () {}); + try { + unlink($this->getFilePath()); + } finally { + restore_error_handler(); + } + } + + /** + * Calculate path to file. + */ + private function getFilePath(): string + { + return $this->savePath.'/'.$this->id.'.mocksess'; + } + + /** + * Reads session from storage and loads session. + */ + private function read(): void + { + set_error_handler(static function () {}); + try { + $data = file_get_contents($this->getFilePath()); + } finally { + restore_error_handler(); + } + + $this->data = $data ? unserialize($data) : []; + + $this->loadSession(); + } +} diff --git a/3rdparty/symfony/http-foundation/Session/Storage/MockFileSessionStorageFactory.php b/3rdparty/symfony/http-foundation/Session/Storage/MockFileSessionStorageFactory.php new file mode 100644 index 00000000..6727cf14 --- /dev/null +++ b/3rdparty/symfony/http-foundation/Session/Storage/MockFileSessionStorageFactory.php @@ -0,0 +1,42 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Component\HttpFoundation\Session\Storage; + +use Symfony\Component\HttpFoundation\Request; + +// Help opcache.preload discover always-needed symbols +class_exists(MockFileSessionStorage::class); + +/** + * @author Jérémy Derussé + */ +class MockFileSessionStorageFactory implements SessionStorageFactoryInterface +{ + private ?string $savePath; + private string $name; + private ?MetadataBag $metaBag; + + /** + * @see MockFileSessionStorage constructor. + */ + public function __construct(?string $savePath = null, string $name = 'MOCKSESSID', ?MetadataBag $metaBag = null) + { + $this->savePath = $savePath; + $this->name = $name; + $this->metaBag = $metaBag; + } + + public function createStorage(?Request $request): SessionStorageInterface + { + return new MockFileSessionStorage($this->savePath, $this->name, $this->metaBag); + } +} diff --git a/3rdparty/symfony/http-foundation/Session/Storage/NativeSessionStorage.php b/3rdparty/symfony/http-foundation/Session/Storage/NativeSessionStorage.php new file mode 100644 index 00000000..c8801cc2 --- /dev/null +++ b/3rdparty/symfony/http-foundation/Session/Storage/NativeSessionStorage.php @@ -0,0 +1,449 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Component\HttpFoundation\Session\Storage; + +use Symfony\Component\HttpFoundation\Session\SessionBagInterface; +use Symfony\Component\HttpFoundation\Session\Storage\Handler\StrictSessionHandler; +use Symfony\Component\HttpFoundation\Session\Storage\Proxy\AbstractProxy; +use Symfony\Component\HttpFoundation\Session\Storage\Proxy\SessionHandlerProxy; + +// Help opcache.preload discover always-needed symbols +class_exists(MetadataBag::class); +class_exists(StrictSessionHandler::class); +class_exists(SessionHandlerProxy::class); + +/** + * This provides a base class for session attribute storage. + * + * @author Drak + */ +class NativeSessionStorage implements SessionStorageInterface +{ + /** + * @var SessionBagInterface[] + */ + protected $bags = []; + + /** + * @var bool + */ + protected $started = false; + + /** + * @var bool + */ + protected $closed = false; + + /** + * @var AbstractProxy|\SessionHandlerInterface + */ + protected $saveHandler; + + /** + * @var MetadataBag + */ + protected $metadataBag; + + /** + * Depending on how you want the storage driver to behave you probably + * want to override this constructor entirely. + * + * List of options for $options array with their defaults. + * + * @see https://php.net/session.configuration for options + * but we omit 'session.' from the beginning of the keys for convenience. + * + * ("auto_start", is not supported as it tells PHP to start a session before + * PHP starts to execute user-land code. Setting during runtime has no effect). + * + * cache_limiter, "" (use "0" to prevent headers from being sent entirely). + * cache_expire, "0" + * cookie_domain, "" + * cookie_httponly, "" + * cookie_lifetime, "0" + * cookie_path, "/" + * cookie_secure, "" + * cookie_samesite, null + * gc_divisor, "100" + * gc_maxlifetime, "1440" + * gc_probability, "1" + * lazy_write, "1" + * name, "PHPSESSID" + * referer_check, "" + * serialize_handler, "php" + * use_strict_mode, "1" + * use_cookies, "1" + * use_only_cookies, "1" + * use_trans_sid, "0" + * sid_length, "32" + * sid_bits_per_character, "5" + * trans_sid_hosts, $_SERVER['HTTP_HOST'] + * trans_sid_tags, "a=href,area=href,frame=src,form=" + */ + public function __construct(array $options = [], AbstractProxy|\SessionHandlerInterface|null $handler = null, ?MetadataBag $metaBag = null) + { + if (!\extension_loaded('session')) { + throw new \LogicException('PHP extension "session" is required.'); + } + + $options += [ + 'cache_limiter' => '', + 'cache_expire' => 0, + 'use_cookies' => 1, + 'lazy_write' => 1, + 'use_strict_mode' => 1, + ]; + + session_register_shutdown(); + + $this->setMetadataBag($metaBag); + $this->setOptions($options); + $this->setSaveHandler($handler); + } + + /** + * Gets the save handler instance. + */ + public function getSaveHandler(): AbstractProxy|\SessionHandlerInterface + { + return $this->saveHandler; + } + + public function start(): bool + { + if ($this->started) { + return true; + } + + if (\PHP_SESSION_ACTIVE === session_status()) { + throw new \RuntimeException('Failed to start the session: already started by PHP.'); + } + + if (filter_var(\ini_get('session.use_cookies'), \FILTER_VALIDATE_BOOL) && headers_sent($file, $line)) { + throw new \RuntimeException(\sprintf('Failed to start the session because headers have already been sent by "%s" at line %d.', $file, $line)); + } + + $sessionId = $_COOKIE[session_name()] ?? null; + /* + * Explanation of the session ID regular expression: `/^[a-zA-Z0-9,-]{22,250}$/`. + * + * ---------- Part 1 + * + * The part `[a-zA-Z0-9,-]` is related to the PHP ini directive `session.sid_bits_per_character` defined as 6. + * See https://php.net/session.configuration#ini.session.sid-bits-per-character + * Allowed values are integers such as: + * - 4 for range `a-f0-9` + * - 5 for range `a-v0-9` + * - 6 for range `a-zA-Z0-9,-` + * + * ---------- Part 2 + * + * The part `{22,250}` is related to the PHP ini directive `session.sid_length`. + * See https://php.net/session.configuration#ini.session.sid-length + * Allowed values are integers between 22 and 256, but we use 250 for the max. + * + * Where does the 250 come from? + * - The length of Windows and Linux filenames is limited to 255 bytes. Then the max must not exceed 255. + * - The session filename prefix is `sess_`, a 5 bytes string. Then the max must not exceed 255 - 5 = 250. + * + * ---------- Conclusion + * + * The parts 1 and 2 prevent the warning below: + * `PHP Warning: SessionHandler::read(): Session ID is too long or contains illegal characters. Only the A-Z, a-z, 0-9, "-", and "," characters are allowed.` + * + * The part 2 prevents the warning below: + * `PHP Warning: SessionHandler::read(): open(filepath, O_RDWR) failed: No such file or directory (2).` + */ + if ($sessionId && $this->saveHandler instanceof AbstractProxy && 'files' === $this->saveHandler->getSaveHandlerName() && !preg_match('/^[a-zA-Z0-9,-]{22,250}$/', $sessionId)) { + // the session ID in the header is invalid, create a new one + session_id(session_create_id()); + } + + // ok to try and start the session + if (!session_start()) { + throw new \RuntimeException('Failed to start the session.'); + } + + $this->loadSession(); + + return true; + } + + public function getId(): string + { + return $this->saveHandler->getId(); + } + + /** + * @return void + */ + public function setId(string $id) + { + $this->saveHandler->setId($id); + } + + public function getName(): string + { + return $this->saveHandler->getName(); + } + + /** + * @return void + */ + public function setName(string $name) + { + $this->saveHandler->setName($name); + } + + public function regenerate(bool $destroy = false, ?int $lifetime = null): bool + { + // Cannot regenerate the session ID for non-active sessions. + if (\PHP_SESSION_ACTIVE !== session_status()) { + return false; + } + + if (headers_sent()) { + return false; + } + + if (null !== $lifetime && $lifetime != \ini_get('session.cookie_lifetime')) { + $this->save(); + ini_set('session.cookie_lifetime', $lifetime); + $this->start(); + } + + if ($destroy) { + $this->metadataBag->stampNew(); + } + + return session_regenerate_id($destroy); + } + + /** + * @return void + */ + public function save() + { + // Store a copy so we can restore the bags in case the session was not left empty + $session = $_SESSION; + + foreach ($this->bags as $bag) { + if (empty($_SESSION[$key = $bag->getStorageKey()])) { + unset($_SESSION[$key]); + } + } + if ($_SESSION && [$key = $this->metadataBag->getStorageKey()] === array_keys($_SESSION)) { + unset($_SESSION[$key]); + } + + // Register error handler to add information about the current save handler + $previousHandler = set_error_handler(function ($type, $msg, $file, $line) use (&$previousHandler) { + if (\E_WARNING === $type && str_starts_with($msg, 'session_write_close():')) { + $handler = $this->saveHandler instanceof SessionHandlerProxy ? $this->saveHandler->getHandler() : $this->saveHandler; + $msg = \sprintf('session_write_close(): Failed to write session data with "%s" handler', $handler::class); + } + + return $previousHandler ? $previousHandler($type, $msg, $file, $line) : false; + }); + + try { + session_write_close(); + } finally { + restore_error_handler(); + + // Restore only if not empty + if ($_SESSION) { + $_SESSION = $session; + } + } + + $this->closed = true; + $this->started = false; + } + + /** + * @return void + */ + public function clear() + { + // clear out the bags + foreach ($this->bags as $bag) { + $bag->clear(); + } + + // clear out the session + $_SESSION = []; + + // reconnect the bags to the session + $this->loadSession(); + } + + /** + * @return void + */ + public function registerBag(SessionBagInterface $bag) + { + if ($this->started) { + throw new \LogicException('Cannot register a bag when the session is already started.'); + } + + $this->bags[$bag->getName()] = $bag; + } + + public function getBag(string $name): SessionBagInterface + { + if (!isset($this->bags[$name])) { + throw new \InvalidArgumentException(\sprintf('The SessionBagInterface "%s" is not registered.', $name)); + } + + if (!$this->started && $this->saveHandler->isActive()) { + $this->loadSession(); + } elseif (!$this->started) { + $this->start(); + } + + return $this->bags[$name]; + } + + /** + * @return void + */ + public function setMetadataBag(?MetadataBag $metaBag = null) + { + if (1 > \func_num_args()) { + trigger_deprecation('symfony/http-foundation', '6.2', 'Calling "%s()" without any arguments is deprecated, pass null explicitly instead.', __METHOD__); + } + $this->metadataBag = $metaBag ?? new MetadataBag(); + } + + /** + * Gets the MetadataBag. + */ + public function getMetadataBag(): MetadataBag + { + return $this->metadataBag; + } + + public function isStarted(): bool + { + return $this->started; + } + + /** + * Sets session.* ini variables. + * + * For convenience we omit 'session.' from the beginning of the keys. + * Explicitly ignores other ini keys. + * + * @param array $options Session ini directives [key => value] + * + * @see https://php.net/session.configuration + * + * @return void + */ + public function setOptions(array $options) + { + if (headers_sent() || \PHP_SESSION_ACTIVE === session_status()) { + return; + } + + $validOptions = array_flip([ + 'cache_expire', 'cache_limiter', 'cookie_domain', 'cookie_httponly', + 'cookie_lifetime', 'cookie_path', 'cookie_secure', 'cookie_samesite', + 'gc_divisor', 'gc_maxlifetime', 'gc_probability', + 'lazy_write', 'name', 'referer_check', + 'serialize_handler', 'use_strict_mode', 'use_cookies', + 'use_only_cookies', 'use_trans_sid', + 'sid_length', 'sid_bits_per_character', 'trans_sid_hosts', 'trans_sid_tags', + ]); + + foreach ($options as $key => $value) { + if (isset($validOptions[$key])) { + if ('cookie_secure' === $key && 'auto' === $value) { + continue; + } + ini_set('session.'.$key, $value); + } + } + } + + /** + * Registers session save handler as a PHP session handler. + * + * To use internal PHP session save handlers, override this method using ini_set with + * session.save_handler and session.save_path e.g. + * + * ini_set('session.save_handler', 'files'); + * ini_set('session.save_path', '/tmp'); + * + * or pass in a \SessionHandler instance which configures session.save_handler in the + * constructor, for a template see NativeFileSessionHandler. + * + * @see https://php.net/session-set-save-handler + * @see https://php.net/sessionhandlerinterface + * @see https://php.net/sessionhandler + * + * @return void + * + * @throws \InvalidArgumentException + */ + public function setSaveHandler(AbstractProxy|\SessionHandlerInterface|null $saveHandler = null) + { + if (1 > \func_num_args()) { + trigger_deprecation('symfony/http-foundation', '6.2', 'Calling "%s()" without any arguments is deprecated, pass null explicitly instead.', __METHOD__); + } + + // Wrap $saveHandler in proxy and prevent double wrapping of proxy + if (!$saveHandler instanceof AbstractProxy && $saveHandler instanceof \SessionHandlerInterface) { + $saveHandler = new SessionHandlerProxy($saveHandler); + } elseif (!$saveHandler instanceof AbstractProxy) { + $saveHandler = new SessionHandlerProxy(new StrictSessionHandler(new \SessionHandler())); + } + $this->saveHandler = $saveHandler; + + if (headers_sent() || \PHP_SESSION_ACTIVE === session_status()) { + return; + } + + if ($this->saveHandler instanceof SessionHandlerProxy) { + session_set_save_handler($this->saveHandler, false); + } + } + + /** + * Load the session with attributes. + * + * After starting the session, PHP retrieves the session from whatever handlers + * are set to (either PHP's internal, or a custom save handler set with session_set_save_handler()). + * PHP takes the return value from the read() handler, unserializes it + * and populates $_SESSION with the result automatically. + * + * @return void + */ + protected function loadSession(?array &$session = null) + { + if (null === $session) { + $session = &$_SESSION; + } + + $bags = array_merge($this->bags, [$this->metadataBag]); + + foreach ($bags as $bag) { + $key = $bag->getStorageKey(); + $session[$key] = isset($session[$key]) && \is_array($session[$key]) ? $session[$key] : []; + $bag->initialize($session[$key]); + } + + $this->started = true; + $this->closed = false; + } +} diff --git a/3rdparty/symfony/http-foundation/Session/Storage/NativeSessionStorageFactory.php b/3rdparty/symfony/http-foundation/Session/Storage/NativeSessionStorageFactory.php new file mode 100644 index 00000000..6463a4c1 --- /dev/null +++ b/3rdparty/symfony/http-foundation/Session/Storage/NativeSessionStorageFactory.php @@ -0,0 +1,50 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Component\HttpFoundation\Session\Storage; + +use Symfony\Component\HttpFoundation\Request; +use Symfony\Component\HttpFoundation\Session\Storage\Proxy\AbstractProxy; + +// Help opcache.preload discover always-needed symbols +class_exists(NativeSessionStorage::class); + +/** + * @author Jérémy Derussé + */ +class NativeSessionStorageFactory implements SessionStorageFactoryInterface +{ + private array $options; + private AbstractProxy|\SessionHandlerInterface|null $handler; + private ?MetadataBag $metaBag; + private bool $secure; + + /** + * @see NativeSessionStorage constructor. + */ + public function __construct(array $options = [], AbstractProxy|\SessionHandlerInterface|null $handler = null, ?MetadataBag $metaBag = null, bool $secure = false) + { + $this->options = $options; + $this->handler = $handler; + $this->metaBag = $metaBag; + $this->secure = $secure; + } + + public function createStorage(?Request $request): SessionStorageInterface + { + $storage = new NativeSessionStorage($this->options, $this->handler, $this->metaBag); + if ($this->secure && $request?->isSecure()) { + $storage->setOptions(['cookie_secure' => true]); + } + + return $storage; + } +} diff --git a/3rdparty/symfony/http-foundation/Session/Storage/PhpBridgeSessionStorage.php b/3rdparty/symfony/http-foundation/Session/Storage/PhpBridgeSessionStorage.php new file mode 100644 index 00000000..4fb26d2a --- /dev/null +++ b/3rdparty/symfony/http-foundation/Session/Storage/PhpBridgeSessionStorage.php @@ -0,0 +1,58 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Component\HttpFoundation\Session\Storage; + +use Symfony\Component\HttpFoundation\Session\Storage\Proxy\AbstractProxy; + +/** + * Allows session to be started by PHP and managed by Symfony. + * + * @author Drak + */ +class PhpBridgeSessionStorage extends NativeSessionStorage +{ + public function __construct(AbstractProxy|\SessionHandlerInterface|null $handler = null, ?MetadataBag $metaBag = null) + { + if (!\extension_loaded('session')) { + throw new \LogicException('PHP extension "session" is required.'); + } + + $this->setMetadataBag($metaBag); + $this->setSaveHandler($handler); + } + + public function start(): bool + { + if ($this->started) { + return true; + } + + $this->loadSession(); + + return true; + } + + /** + * @return void + */ + public function clear() + { + // clear out the bags and nothing else that may be set + // since the purpose of this driver is to share a handler + foreach ($this->bags as $bag) { + $bag->clear(); + } + + // reconnect the bags to the session + $this->loadSession(); + } +} diff --git a/3rdparty/symfony/http-foundation/Session/Storage/PhpBridgeSessionStorageFactory.php b/3rdparty/symfony/http-foundation/Session/Storage/PhpBridgeSessionStorageFactory.php new file mode 100644 index 00000000..aa4f800d --- /dev/null +++ b/3rdparty/symfony/http-foundation/Session/Storage/PhpBridgeSessionStorageFactory.php @@ -0,0 +1,45 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Component\HttpFoundation\Session\Storage; + +use Symfony\Component\HttpFoundation\Request; +use Symfony\Component\HttpFoundation\Session\Storage\Proxy\AbstractProxy; + +// Help opcache.preload discover always-needed symbols +class_exists(PhpBridgeSessionStorage::class); + +/** + * @author Jérémy Derussé + */ +class PhpBridgeSessionStorageFactory implements SessionStorageFactoryInterface +{ + private AbstractProxy|\SessionHandlerInterface|null $handler; + private ?MetadataBag $metaBag; + private bool $secure; + + public function __construct(AbstractProxy|\SessionHandlerInterface|null $handler = null, ?MetadataBag $metaBag = null, bool $secure = false) + { + $this->handler = $handler; + $this->metaBag = $metaBag; + $this->secure = $secure; + } + + public function createStorage(?Request $request): SessionStorageInterface + { + $storage = new PhpBridgeSessionStorage($this->handler, $this->metaBag); + if ($this->secure && $request?->isSecure()) { + $storage->setOptions(['cookie_secure' => true]); + } + + return $storage; + } +} diff --git a/3rdparty/symfony/http-foundation/Session/Storage/Proxy/AbstractProxy.php b/3rdparty/symfony/http-foundation/Session/Storage/Proxy/AbstractProxy.php new file mode 100644 index 00000000..2fcd06b1 --- /dev/null +++ b/3rdparty/symfony/http-foundation/Session/Storage/Proxy/AbstractProxy.php @@ -0,0 +1,110 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Component\HttpFoundation\Session\Storage\Proxy; + +/** + * @author Drak + */ +abstract class AbstractProxy +{ + /** + * Flag if handler wraps an internal PHP session handler (using \SessionHandler). + * + * @var bool + */ + protected $wrapper = false; + + /** + * @var string + */ + protected $saveHandlerName; + + /** + * Gets the session.save_handler name. + */ + public function getSaveHandlerName(): ?string + { + return $this->saveHandlerName; + } + + /** + * Is this proxy handler and instance of \SessionHandlerInterface. + */ + public function isSessionHandlerInterface(): bool + { + return $this instanceof \SessionHandlerInterface; + } + + /** + * Returns true if this handler wraps an internal PHP session save handler using \SessionHandler. + */ + public function isWrapper(): bool + { + return $this->wrapper; + } + + /** + * Has a session started? + */ + public function isActive(): bool + { + return \PHP_SESSION_ACTIVE === session_status(); + } + + /** + * Gets the session ID. + */ + public function getId(): string + { + return session_id(); + } + + /** + * Sets the session ID. + * + * @return void + * + * @throws \LogicException + */ + public function setId(string $id) + { + if ($this->isActive()) { + throw new \LogicException('Cannot change the ID of an active session.'); + } + + session_id($id); + } + + /** + * Gets the session name. + */ + public function getName(): string + { + return session_name(); + } + + /** + * Sets the session name. + * + * @return void + * + * @throws \LogicException + */ + public function setName(string $name) + { + if ($this->isActive()) { + throw new \LogicException('Cannot change the name of an active session.'); + } + + session_name($name); + } +} diff --git a/3rdparty/symfony/http-foundation/Session/Storage/Proxy/SessionHandlerProxy.php b/3rdparty/symfony/http-foundation/Session/Storage/Proxy/SessionHandlerProxy.php new file mode 100644 index 00000000..7bf3f9ff --- /dev/null +++ b/3rdparty/symfony/http-foundation/Session/Storage/Proxy/SessionHandlerProxy.php @@ -0,0 +1,76 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Component\HttpFoundation\Session\Storage\Proxy; + +use Symfony\Component\HttpFoundation\Session\Storage\Handler\StrictSessionHandler; + +/** + * @author Drak + */ +class SessionHandlerProxy extends AbstractProxy implements \SessionHandlerInterface, \SessionUpdateTimestampHandlerInterface +{ + protected $handler; + + public function __construct(\SessionHandlerInterface $handler) + { + $this->handler = $handler; + $this->wrapper = $handler instanceof \SessionHandler; + $this->saveHandlerName = $this->wrapper || ($handler instanceof StrictSessionHandler && $handler->isWrapper()) ? \ini_get('session.save_handler') : 'user'; + } + + public function getHandler(): \SessionHandlerInterface + { + return $this->handler; + } + + // \SessionHandlerInterface + + public function open(string $savePath, string $sessionName): bool + { + return $this->handler->open($savePath, $sessionName); + } + + public function close(): bool + { + return $this->handler->close(); + } + + public function read(#[\SensitiveParameter] string $sessionId): string|false + { + return $this->handler->read($sessionId); + } + + public function write(#[\SensitiveParameter] string $sessionId, string $data): bool + { + return $this->handler->write($sessionId, $data); + } + + public function destroy(#[\SensitiveParameter] string $sessionId): bool + { + return $this->handler->destroy($sessionId); + } + + public function gc(int $maxlifetime): int|false + { + return $this->handler->gc($maxlifetime); + } + + public function validateId(#[\SensitiveParameter] string $sessionId): bool + { + return !$this->handler instanceof \SessionUpdateTimestampHandlerInterface || $this->handler->validateId($sessionId); + } + + public function updateTimestamp(#[\SensitiveParameter] string $sessionId, string $data): bool + { + return $this->handler instanceof \SessionUpdateTimestampHandlerInterface ? $this->handler->updateTimestamp($sessionId, $data) : $this->write($sessionId, $data); + } +} diff --git a/3rdparty/symfony/http-foundation/Session/Storage/SessionStorageFactoryInterface.php b/3rdparty/symfony/http-foundation/Session/Storage/SessionStorageFactoryInterface.php new file mode 100644 index 00000000..d03f0da4 --- /dev/null +++ b/3rdparty/symfony/http-foundation/Session/Storage/SessionStorageFactoryInterface.php @@ -0,0 +1,25 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Component\HttpFoundation\Session\Storage; + +use Symfony\Component\HttpFoundation\Request; + +/** + * @author Jérémy Derussé + */ +interface SessionStorageFactoryInterface +{ + /** + * Creates a new instance of SessionStorageInterface. + */ + public function createStorage(?Request $request): SessionStorageInterface; +} diff --git a/3rdparty/symfony/http-foundation/Session/Storage/SessionStorageInterface.php b/3rdparty/symfony/http-foundation/Session/Storage/SessionStorageInterface.php new file mode 100644 index 00000000..7865135b --- /dev/null +++ b/3rdparty/symfony/http-foundation/Session/Storage/SessionStorageInterface.php @@ -0,0 +1,126 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Component\HttpFoundation\Session\Storage; + +use Symfony\Component\HttpFoundation\Session\SessionBagInterface; + +/** + * StorageInterface. + * + * @author Fabien Potencier + * @author Drak + */ +interface SessionStorageInterface +{ + /** + * Starts the session. + * + * @throws \RuntimeException if something goes wrong starting the session + */ + public function start(): bool; + + /** + * Checks if the session is started. + */ + public function isStarted(): bool; + + /** + * Returns the session ID. + */ + public function getId(): string; + + /** + * Sets the session ID. + * + * @return void + */ + public function setId(string $id); + + /** + * Returns the session name. + */ + public function getName(): string; + + /** + * Sets the session name. + * + * @return void + */ + public function setName(string $name); + + /** + * Regenerates id that represents this storage. + * + * This method must invoke session_regenerate_id($destroy) unless + * this interface is used for a storage object designed for unit + * or functional testing where a real PHP session would interfere + * with testing. + * + * Note regenerate+destroy should not clear the session data in memory + * only delete the session data from persistent storage. + * + * Care: When regenerating the session ID no locking is involved in PHP's + * session design. See https://bugs.php.net/61470 for a discussion. + * So you must make sure the regenerated session is saved BEFORE sending the + * headers with the new ID. Symfony's HttpKernel offers a listener for this. + * See Symfony\Component\HttpKernel\EventListener\SaveSessionListener. + * Otherwise session data could get lost again for concurrent requests with the + * new ID. One result could be that you get logged out after just logging in. + * + * @param bool $destroy Destroy session when regenerating? + * @param int|null $lifetime Sets the cookie lifetime for the session cookie. A null value + * will leave the system settings unchanged, 0 sets the cookie + * to expire with browser session. Time is in seconds, and is + * not a Unix timestamp. + * + * @throws \RuntimeException If an error occurs while regenerating this storage + */ + public function regenerate(bool $destroy = false, ?int $lifetime = null): bool; + + /** + * Force the session to be saved and closed. + * + * This method must invoke session_write_close() unless this interface is + * used for a storage object design for unit or functional testing where + * a real PHP session would interfere with testing, in which case + * it should actually persist the session data if required. + * + * @return void + * + * @throws \RuntimeException if the session is saved without being started, or if the session + * is already closed + */ + public function save(); + + /** + * Clear all session data in memory. + * + * @return void + */ + public function clear(); + + /** + * Gets a SessionBagInterface by name. + * + * @throws \InvalidArgumentException If the bag does not exist + */ + public function getBag(string $name): SessionBagInterface; + + /** + * Registers a SessionBagInterface for use. + * + * @return void + */ + public function registerBag(SessionBagInterface $bag); + + public function getMetadataBag(): MetadataBag; +} diff --git a/3rdparty/symfony/http-foundation/StreamedJsonResponse.php b/3rdparty/symfony/http-foundation/StreamedJsonResponse.php new file mode 100644 index 00000000..5b20ce91 --- /dev/null +++ b/3rdparty/symfony/http-foundation/StreamedJsonResponse.php @@ -0,0 +1,162 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Component\HttpFoundation; + +/** + * StreamedJsonResponse represents a streamed HTTP response for JSON. + * + * A StreamedJsonResponse uses a structure and generics to create an + * efficient resource-saving JSON response. + * + * It is recommended to use flush() function after a specific number of items to directly stream the data. + * + * @see flush() + * + * @author Alexander Schranz + * + * Example usage: + * + * function loadArticles(): \Generator + * // some streamed loading + * yield ['title' => 'Article 1']; + * yield ['title' => 'Article 2']; + * yield ['title' => 'Article 3']; + * // recommended to use flush() after every specific number of items + * }), + * + * $response = new StreamedJsonResponse( + * // json structure with generators in which will be streamed + * [ + * '_embedded' => [ + * 'articles' => loadArticles(), // any generator which you want to stream as list of data + * ], + * ], + * ); + */ +class StreamedJsonResponse extends StreamedResponse +{ + private const PLACEHOLDER = '__symfony_json__'; + + /** + * @param mixed[] $data JSON Data containing PHP generators which will be streamed as list of data or a Generator + * @param int $status The HTTP status code (200 "OK" by default) + * @param array $headers An array of HTTP headers + * @param int $encodingOptions Flags for the json_encode() function + */ + public function __construct( + private readonly iterable $data, + int $status = 200, + array $headers = [], + private int $encodingOptions = JsonResponse::DEFAULT_ENCODING_OPTIONS, + ) { + parent::__construct($this->stream(...), $status, $headers); + + if (!$this->headers->get('Content-Type')) { + $this->headers->set('Content-Type', 'application/json'); + } + } + + private function stream(): void + { + $jsonEncodingOptions = \JSON_THROW_ON_ERROR | $this->encodingOptions; + $keyEncodingOptions = $jsonEncodingOptions & ~\JSON_NUMERIC_CHECK; + + $this->streamData($this->data, $jsonEncodingOptions, $keyEncodingOptions); + } + + private function streamData(mixed $data, int $jsonEncodingOptions, int $keyEncodingOptions): void + { + if (\is_array($data)) { + $this->streamArray($data, $jsonEncodingOptions, $keyEncodingOptions); + + return; + } + + if (is_iterable($data) && !$data instanceof \JsonSerializable) { + $this->streamIterable($data, $jsonEncodingOptions, $keyEncodingOptions); + + return; + } + + echo json_encode($data, $jsonEncodingOptions); + } + + private function streamArray(array $data, int $jsonEncodingOptions, int $keyEncodingOptions): void + { + $generators = []; + + array_walk_recursive($data, function (&$item, $key) use (&$generators) { + if (self::PLACEHOLDER === $key) { + // if the placeholder is already in the structure it should be replaced with a new one that explode + // works like expected for the structure + $generators[] = $key; + } + + // generators should be used but for better DX all kind of Traversable and objects are supported + if (\is_object($item)) { + $generators[] = $item; + $item = self::PLACEHOLDER; + } elseif (self::PLACEHOLDER === $item) { + // if the placeholder is already in the structure it should be replaced with a new one that explode + // works like expected for the structure + $generators[] = $item; + } + }); + + $jsonParts = explode('"'.self::PLACEHOLDER.'"', json_encode($data, $jsonEncodingOptions)); + + foreach ($generators as $index => $generator) { + // send first and between parts of the structure + echo $jsonParts[$index]; + + $this->streamData($generator, $jsonEncodingOptions, $keyEncodingOptions); + } + + // send last part of the structure + echo $jsonParts[array_key_last($jsonParts)]; + } + + private function streamIterable(iterable $iterable, int $jsonEncodingOptions, int $keyEncodingOptions): void + { + $isFirstItem = true; + $startTag = '['; + + foreach ($iterable as $key => $item) { + if ($isFirstItem) { + $isFirstItem = false; + // depending on the first elements key the generator is detected as a list or map + // we can not check for a whole list or map because that would hurt the performance + // of the streamed response which is the main goal of this response class + if (0 !== $key) { + $startTag = '{'; + } + + echo $startTag; + } else { + // if not first element of the generic, a separator is required between the elements + echo ','; + } + + if ('{' === $startTag) { + echo json_encode((string) $key, $keyEncodingOptions).':'; + } + + $this->streamData($item, $jsonEncodingOptions, $keyEncodingOptions); + } + + if ($isFirstItem) { // indicates that the generator was empty + echo '['; + } + + echo '[' === $startTag ? ']' : '}'; + } +} diff --git a/3rdparty/symfony/http-foundation/StreamedResponse.php b/3rdparty/symfony/http-foundation/StreamedResponse.php new file mode 100644 index 00000000..0ab88e09 --- /dev/null +++ b/3rdparty/symfony/http-foundation/StreamedResponse.php @@ -0,0 +1,131 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Component\HttpFoundation; + +/** + * StreamedResponse represents a streamed HTTP response. + * + * A StreamedResponse uses a callback for its content. + * + * The callback should use the standard PHP functions like echo + * to stream the response back to the client. The flush() function + * can also be used if needed. + * + * @see flush() + * + * @author Fabien Potencier + */ +class StreamedResponse extends Response +{ + protected $callback; + protected $streamed; + private bool $headersSent; + + /** + * @param int $status The HTTP status code (200 "OK" by default) + */ + public function __construct(?callable $callback = null, int $status = 200, array $headers = []) + { + parent::__construct(null, $status, $headers); + + if (null !== $callback) { + $this->setCallback($callback); + } + $this->streamed = false; + $this->headersSent = false; + } + + /** + * Sets the PHP callback associated with this Response. + * + * @return $this + */ + public function setCallback(callable $callback): static + { + $this->callback = $callback(...); + + return $this; + } + + public function getCallback(): ?\Closure + { + if (!isset($this->callback)) { + return null; + } + + return ($this->callback)(...); + } + + /** + * This method only sends the headers once. + * + * @param positive-int|null $statusCode The status code to use, override the statusCode property if set and not null + * + * @return $this + */ + public function sendHeaders(/* int $statusCode = null */): static + { + if ($this->headersSent) { + return $this; + } + + $statusCode = \func_num_args() > 0 ? func_get_arg(0) : null; + if ($statusCode < 100 || $statusCode >= 200) { + $this->headersSent = true; + } + + return parent::sendHeaders($statusCode); + } + + /** + * This method only sends the content once. + * + * @return $this + */ + public function sendContent(): static + { + if ($this->streamed) { + return $this; + } + + $this->streamed = true; + + if (!isset($this->callback)) { + throw new \LogicException('The Response callback must be set.'); + } + + ($this->callback)(); + + return $this; + } + + /** + * @return $this + * + * @throws \LogicException when the content is not null + */ + public function setContent(?string $content): static + { + if (null !== $content) { + throw new \LogicException('The content cannot be set on a StreamedResponse instance.'); + } + + $this->streamed = true; + + return $this; + } + + public function getContent(): string|false + { + return false; + } +} diff --git a/3rdparty/symfony/http-foundation/UriSigner.php b/3rdparty/symfony/http-foundation/UriSigner.php new file mode 100644 index 00000000..b0498772 --- /dev/null +++ b/3rdparty/symfony/http-foundation/UriSigner.php @@ -0,0 +1,112 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Component\HttpFoundation; + +/** + * @author Fabien Potencier + */ +class UriSigner +{ + private string $secret; + private string $parameter; + + /** + * @param string $parameter Query string parameter to use + */ + public function __construct(#[\SensitiveParameter] string $secret, string $parameter = '_hash') + { + if (!$secret) { + throw new \InvalidArgumentException('A non-empty secret is required.'); + } + + $this->secret = $secret; + $this->parameter = $parameter; + } + + /** + * Signs a URI. + * + * The given URI is signed by adding the query string parameter + * which value depends on the URI and the secret. + */ + public function sign(string $uri): string + { + $url = parse_url($uri); + $params = []; + + if (isset($url['query'])) { + parse_str($url['query'], $params); + } + + $uri = $this->buildUrl($url, $params); + $params[$this->parameter] = $this->computeHash($uri); + + return $this->buildUrl($url, $params); + } + + /** + * Checks that a URI contains the correct hash. + */ + public function check(string $uri): bool + { + $url = parse_url($uri); + $params = []; + + if (isset($url['query'])) { + parse_str($url['query'], $params); + } + + if (empty($params[$this->parameter])) { + return false; + } + + $hash = $params[$this->parameter]; + unset($params[$this->parameter]); + + return hash_equals($this->computeHash($this->buildUrl($url, $params)), $hash); + } + + public function checkRequest(Request $request): bool + { + $qs = ($qs = $request->server->get('QUERY_STRING')) ? '?'.$qs : ''; + + // we cannot use $request->getUri() here as we want to work with the original URI (no query string reordering) + return $this->check($request->getSchemeAndHttpHost().$request->getBaseUrl().$request->getPathInfo().$qs); + } + + private function computeHash(string $uri): string + { + return base64_encode(hash_hmac('sha256', $uri, $this->secret, true)); + } + + private function buildUrl(array $url, array $params = []): string + { + ksort($params, \SORT_STRING); + $url['query'] = http_build_query($params, '', '&'); + + $scheme = isset($url['scheme']) ? $url['scheme'].'://' : ''; + $host = $url['host'] ?? ''; + $port = isset($url['port']) ? ':'.$url['port'] : ''; + $user = $url['user'] ?? ''; + $pass = isset($url['pass']) ? ':'.$url['pass'] : ''; + $pass = ($user || $pass) ? "$pass@" : ''; + $path = $url['path'] ?? ''; + $query = $url['query'] ? '?'.$url['query'] : ''; + $fragment = isset($url['fragment']) ? '#'.$url['fragment'] : ''; + + return $scheme.$user.$pass.$host.$port.$path.$query.$fragment; + } +} + +if (!class_exists(\Symfony\Component\HttpKernel\UriSigner::class, false)) { + class_alias(UriSigner::class, \Symfony\Component\HttpKernel\UriSigner::class); +} diff --git a/3rdparty/symfony/http-foundation/UrlHelper.php b/3rdparty/symfony/http-foundation/UrlHelper.php new file mode 100644 index 00000000..f971cf66 --- /dev/null +++ b/3rdparty/symfony/http-foundation/UrlHelper.php @@ -0,0 +1,108 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Component\HttpFoundation; + +use Symfony\Component\Routing\RequestContext; +use Symfony\Component\Routing\RequestContextAwareInterface; + +/** + * A helper service for manipulating URLs within and outside the request scope. + * + * @author Valentin Udaltsov + */ +final class UrlHelper +{ + public function __construct( + private RequestStack $requestStack, + private RequestContextAwareInterface|RequestContext|null $requestContext = null, + ) { + } + + public function getAbsoluteUrl(string $path): string + { + if (str_contains($path, '://') || str_starts_with($path, '//')) { + return $path; + } + + if (null === $request = $this->requestStack->getMainRequest()) { + return $this->getAbsoluteUrlFromContext($path); + } + + if ('#' === $path[0]) { + $path = $request->getRequestUri().$path; + } elseif ('?' === $path[0]) { + $path = $request->getPathInfo().$path; + } + + if (!$path || '/' !== $path[0]) { + $prefix = $request->getPathInfo(); + $last = \strlen($prefix) - 1; + if ($last !== $pos = strrpos($prefix, '/')) { + $prefix = substr($prefix, 0, $pos).'/'; + } + + return $request->getUriForPath($prefix.$path); + } + + return $request->getSchemeAndHttpHost().$path; + } + + public function getRelativePath(string $path): string + { + if (str_contains($path, '://') || str_starts_with($path, '//')) { + return $path; + } + + if (null === $request = $this->requestStack->getMainRequest()) { + return $path; + } + + return $request->getRelativeUriForPath($path); + } + + private function getAbsoluteUrlFromContext(string $path): string + { + if (null === $context = $this->requestContext) { + return $path; + } + + if ($context instanceof RequestContextAwareInterface) { + $context = $context->getContext(); + } + + if ('' === $host = $context->getHost()) { + return $path; + } + + $scheme = $context->getScheme(); + $port = ''; + + if ('http' === $scheme && 80 !== $context->getHttpPort()) { + $port = ':'.$context->getHttpPort(); + } elseif ('https' === $scheme && 443 !== $context->getHttpsPort()) { + $port = ':'.$context->getHttpsPort(); + } + + if ('#' === $path[0]) { + $queryString = $context->getQueryString(); + $path = $context->getPathInfo().($queryString ? '?'.$queryString : '').$path; + } elseif ('?' === $path[0]) { + $path = $context->getPathInfo().$path; + } + + if ('/' !== $path[0]) { + $path = rtrim($context->getBaseUrl(), '/').'/'.$path; + } + + return $scheme.'://'.$host.$port.$path; + } +} diff --git a/3rdparty/symfony/mailer/Command/MailerTestCommand.php b/3rdparty/symfony/mailer/Command/MailerTestCommand.php new file mode 100644 index 00000000..bfc2779e --- /dev/null +++ b/3rdparty/symfony/mailer/Command/MailerTestCommand.php @@ -0,0 +1,75 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Component\Mailer\Command; + +use Symfony\Component\Console\Attribute\AsCommand; +use Symfony\Component\Console\Command\Command; +use Symfony\Component\Console\Input\InputArgument; +use Symfony\Component\Console\Input\InputInterface; +use Symfony\Component\Console\Input\InputOption; +use Symfony\Component\Console\Output\OutputInterface; +use Symfony\Component\Mailer\Transport\TransportInterface; +use Symfony\Component\Mime\Email; + +/** + * A console command to test Mailer transports. + */ +#[AsCommand(name: 'mailer:test', description: 'Test Mailer transports by sending an email')] +final class MailerTestCommand extends Command +{ + public function __construct(private TransportInterface $transport) + { + $this->transport = $transport; + + parent::__construct(); + } + + protected function configure(): void + { + $this + ->addArgument('to', InputArgument::REQUIRED, 'The recipient of the message') + ->addOption('from', null, InputOption::VALUE_OPTIONAL, 'The sender of the message', 'from@example.org') + ->addOption('subject', null, InputOption::VALUE_OPTIONAL, 'The subject of the message', 'Testing transport') + ->addOption('body', null, InputOption::VALUE_OPTIONAL, 'The body of the message', 'Testing body') + ->addOption('transport', null, InputOption::VALUE_OPTIONAL, 'The transport to be used') + ->setHelp(<<<'EOF' +The %command.name% command tests a Mailer transport by sending a simple email message: + +php %command.full_name% to@example.com + +You can also specify a specific transport: + + php %command.full_name% to@example.com --transport=transport_name + +Note that this command bypasses the Messenger bus if configured. + +EOF + ); + } + + protected function execute(InputInterface $input, OutputInterface $output): int + { + $message = (new Email()) + ->to($input->getArgument('to')) + ->from($input->getOption('from')) + ->subject($input->getOption('subject')) + ->text($input->getOption('body')) + ; + if ($transport = $input->getOption('transport')) { + $message->getHeaders()->addTextHeader('X-Transport', $transport); + } + + $this->transport->send($message); + + return 0; + } +} diff --git a/3rdparty/symfony/mailer/DataCollector/MessageDataCollector.php b/3rdparty/symfony/mailer/DataCollector/MessageDataCollector.php new file mode 100644 index 00000000..5c9ac354 --- /dev/null +++ b/3rdparty/symfony/mailer/DataCollector/MessageDataCollector.php @@ -0,0 +1,59 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Component\Mailer\DataCollector; + +use Symfony\Component\HttpFoundation\Request; +use Symfony\Component\HttpFoundation\Response; +use Symfony\Component\HttpKernel\DataCollector\DataCollector; +use Symfony\Component\Mailer\Event\MessageEvents; +use Symfony\Component\Mailer\EventListener\MessageLoggerListener; + +/** + * @author Fabien Potencier + */ +final class MessageDataCollector extends DataCollector +{ + private MessageEvents $events; + + public function __construct(MessageLoggerListener $logger) + { + $this->events = $logger->getEvents(); + } + + public function collect(Request $request, Response $response, ?\Throwable $exception = null): void + { + $this->data['events'] = $this->events; + } + + public function getEvents(): MessageEvents + { + return $this->data['events']; + } + + /** + * @internal + */ + public function base64Encode(string $data): string + { + return base64_encode($data); + } + + public function reset(): void + { + $this->data = []; + } + + public function getName(): string + { + return 'mailer'; + } +} diff --git a/3rdparty/symfony/mailer/DelayedEnvelope.php b/3rdparty/symfony/mailer/DelayedEnvelope.php new file mode 100644 index 00000000..7458db2f --- /dev/null +++ b/3rdparty/symfony/mailer/DelayedEnvelope.php @@ -0,0 +1,98 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Component\Mailer; + +use Symfony\Component\Mailer\Exception\LogicException; +use Symfony\Component\Mime\Address; +use Symfony\Component\Mime\Header\Headers; +use Symfony\Component\Mime\Message; + +/** + * @author Fabien Potencier + * + * @internal + */ +final class DelayedEnvelope extends Envelope +{ + private bool $senderSet = false; + private bool $recipientsSet = false; + private Message $message; + + public function __construct(Message $message) + { + $this->message = $message; + } + + public function setSender(Address $sender): void + { + parent::setSender($sender); + + $this->senderSet = true; + } + + public function getSender(): Address + { + if (!$this->senderSet) { + parent::setSender(self::getSenderFromHeaders($this->message->getHeaders())); + } + + return parent::getSender(); + } + + public function setRecipients(array $recipients): void + { + parent::setRecipients($recipients); + + $this->recipientsSet = (bool) parent::getRecipients(); + } + + /** + * @return Address[] + */ + public function getRecipients(): array + { + if ($this->recipientsSet) { + return parent::getRecipients(); + } + + return self::getRecipientsFromHeaders($this->message->getHeaders()); + } + + private static function getRecipientsFromHeaders(Headers $headers): array + { + $recipients = []; + foreach (['to', 'cc', 'bcc'] as $name) { + foreach ($headers->all($name) as $header) { + foreach ($header->getAddresses() as $address) { + $recipients[] = $address; + } + } + } + + return $recipients; + } + + private static function getSenderFromHeaders(Headers $headers): Address + { + if ($sender = $headers->get('Sender')) { + return $sender->getAddress(); + } + if ($return = $headers->get('Return-Path')) { + return $return->getAddress(); + } + if ($from = $headers->get('From')) { + return $from->getAddresses()[0]; + } + + throw new LogicException('Unable to determine the sender of the message.'); + } +} diff --git a/3rdparty/symfony/mailer/Envelope.php b/3rdparty/symfony/mailer/Envelope.php new file mode 100644 index 00000000..0a4af2ed --- /dev/null +++ b/3rdparty/symfony/mailer/Envelope.php @@ -0,0 +1,88 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Component\Mailer; + +use Symfony\Component\Mailer\Exception\InvalidArgumentException; +use Symfony\Component\Mailer\Exception\LogicException; +use Symfony\Component\Mime\Address; +use Symfony\Component\Mime\RawMessage; + +/** + * @author Fabien Potencier + */ +class Envelope +{ + private Address $sender; + private array $recipients = []; + + /** + * @param Address[] $recipients + */ + public function __construct(Address $sender, array $recipients) + { + $this->setSender($sender); + $this->setRecipients($recipients); + } + + public static function create(RawMessage $message): self + { + if (RawMessage::class === $message::class) { + throw new LogicException('Cannot send a RawMessage instance without an explicit Envelope.'); + } + + return new DelayedEnvelope($message); + } + + public function setSender(Address $sender): void + { + // to ensure deliverability of bounce emails independent of UTF-8 capabilities of SMTP servers + if (!preg_match('/^[^@\x80-\xFF]++@/', $sender->getAddress())) { + throw new InvalidArgumentException(sprintf('Invalid sender "%s": non-ASCII characters not supported in local-part of email.', $sender->getAddress())); + } + $this->sender = $sender; + } + + /** + * @return Address Returns a "mailbox" as specified by RFC 2822 + * Must be converted to an "addr-spec" when used as a "MAIL FROM" value in SMTP (use getAddress()) + */ + public function getSender(): Address + { + return $this->sender; + } + + /** + * @param Address[] $recipients + */ + public function setRecipients(array $recipients): void + { + if (!$recipients) { + throw new InvalidArgumentException('An envelope must have at least one recipient.'); + } + + $this->recipients = []; + foreach ($recipients as $recipient) { + if (!$recipient instanceof Address) { + throw new InvalidArgumentException(sprintf('A recipient must be an instance of "%s" (got "%s").', Address::class, get_debug_type($recipient))); + } + $this->recipients[] = new Address($recipient->getAddress()); + } + } + + /** + * @return Address[] + */ + public function getRecipients(): array + { + return $this->recipients; + } +} diff --git a/3rdparty/symfony/mailer/Event/FailedMessageEvent.php b/3rdparty/symfony/mailer/Event/FailedMessageEvent.php new file mode 100644 index 00000000..4808c659 --- /dev/null +++ b/3rdparty/symfony/mailer/Event/FailedMessageEvent.php @@ -0,0 +1,37 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Component\Mailer\Event; + +use Symfony\Component\Mime\RawMessage; +use Symfony\Contracts\EventDispatcher\Event; + +/** + * @author Fabien Potencier + */ +final class FailedMessageEvent extends Event +{ + public function __construct( + private RawMessage $message, + private \Throwable $error, + ) { + } + + public function getMessage(): RawMessage + { + return $this->message; + } + + public function getError(): \Throwable + { + return $this->error; + } +} diff --git a/3rdparty/symfony/mailer/Event/MessageEvent.php b/3rdparty/symfony/mailer/Event/MessageEvent.php new file mode 100644 index 00000000..f00fdd52 --- /dev/null +++ b/3rdparty/symfony/mailer/Event/MessageEvent.php @@ -0,0 +1,105 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Component\Mailer\Event; + +use Symfony\Component\Mailer\Envelope; +use Symfony\Component\Mailer\Exception\LogicException; +use Symfony\Component\Messenger\Stamp\StampInterface; +use Symfony\Component\Mime\RawMessage; +use Symfony\Contracts\EventDispatcher\Event; + +/** + * Allows the transformation of a Message and the Envelope before the email is sent. + * + * @author Fabien Potencier + */ +final class MessageEvent extends Event +{ + private RawMessage $message; + private Envelope $envelope; + private string $transport; + private bool $queued; + private bool $rejected = false; + + /** @var StampInterface[] */ + private array $stamps = []; + + public function __construct(RawMessage $message, Envelope $envelope, string $transport, bool $queued = false) + { + $this->message = $message; + $this->envelope = $envelope; + $this->transport = $transport; + $this->queued = $queued; + } + + public function getMessage(): RawMessage + { + return $this->message; + } + + public function setMessage(RawMessage $message): void + { + $this->message = $message; + } + + public function getEnvelope(): Envelope + { + return $this->envelope; + } + + public function setEnvelope(Envelope $envelope): void + { + $this->envelope = $envelope; + } + + public function getTransport(): string + { + return $this->transport; + } + + public function isQueued(): bool + { + return $this->queued; + } + + public function isRejected(): bool + { + return $this->rejected; + } + + public function reject(): void + { + $this->rejected = true; + $this->stopPropagation(); + } + + public function addStamp(StampInterface $stamp): void + { + if (!$this->queued) { + throw new LogicException(sprintf('Cannot call "%s()" on a message that is not meant to be queued.', __METHOD__)); + } + + $this->stamps[] = $stamp; + } + + /** + * @return StampInterface[] + */ + public function getStamps(): array + { + if (!$this->queued) { + throw new LogicException(sprintf('Cannot call "%s()" on a message that is not meant to be queued.', __METHOD__)); + } + + return $this->stamps; + } +} diff --git a/3rdparty/symfony/mailer/Event/MessageEvents.php b/3rdparty/symfony/mailer/Event/MessageEvents.php new file mode 100644 index 00000000..2b438e38 --- /dev/null +++ b/3rdparty/symfony/mailer/Event/MessageEvents.php @@ -0,0 +1,74 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Component\Mailer\Event; + +use Symfony\Component\Mime\RawMessage; + +/** + * @author Fabien Potencier + */ +class MessageEvents +{ + /** + * @var MessageEvent[] + */ + private array $events = []; + + /** + * @var array + */ + private array $transports = []; + + public function add(MessageEvent $event): void + { + $this->events[] = $event; + $this->transports[$event->getTransport()] = true; + } + + public function getTransports(): array + { + return array_keys($this->transports); + } + + /** + * @return MessageEvent[] + */ + public function getEvents(?string $name = null): array + { + if (null === $name) { + return $this->events; + } + + $events = []; + foreach ($this->events as $event) { + if ($name === $event->getTransport()) { + $events[] = $event; + } + } + + return $events; + } + + /** + * @return RawMessage[] + */ + public function getMessages(?string $name = null): array + { + $events = $this->getEvents($name); + $messages = []; + foreach ($events as $event) { + $messages[] = $event->getMessage(); + } + + return $messages; + } +} diff --git a/3rdparty/symfony/mailer/Event/SentMessageEvent.php b/3rdparty/symfony/mailer/Event/SentMessageEvent.php new file mode 100644 index 00000000..a412a9f4 --- /dev/null +++ b/3rdparty/symfony/mailer/Event/SentMessageEvent.php @@ -0,0 +1,30 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Component\Mailer\Event; + +use Symfony\Component\Mailer\SentMessage; +use Symfony\Contracts\EventDispatcher\Event; + +/** + * @author Fabien Potencier + */ +final class SentMessageEvent extends Event +{ + public function __construct(private SentMessage $message) + { + } + + public function getMessage(): SentMessage + { + return $this->message; + } +} diff --git a/3rdparty/symfony/mailer/EventListener/EnvelopeListener.php b/3rdparty/symfony/mailer/EventListener/EnvelopeListener.php new file mode 100644 index 00000000..db9dd723 --- /dev/null +++ b/3rdparty/symfony/mailer/EventListener/EnvelopeListener.php @@ -0,0 +1,71 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Component\Mailer\EventListener; + +use Symfony\Component\EventDispatcher\EventSubscriberInterface; +use Symfony\Component\Mailer\Event\MessageEvent; +use Symfony\Component\Mime\Address; +use Symfony\Component\Mime\Message; + +/** + * Manipulates the Envelope of a Message. + * + * @author Fabien Potencier + */ +class EnvelopeListener implements EventSubscriberInterface +{ + private ?Address $sender = null; + + /** + * @var Address[]|null + */ + private ?array $recipients = null; + + /** + * @param array $recipients + */ + public function __construct(Address|string|null $sender = null, ?array $recipients = null) + { + if (null !== $sender) { + $this->sender = Address::create($sender); + } + if (null !== $recipients) { + $this->recipients = Address::createArray($recipients); + } + } + + public function onMessage(MessageEvent $event): void + { + if ($this->sender) { + $event->getEnvelope()->setSender($this->sender); + + $message = $event->getMessage(); + if ($message instanceof Message) { + if (!$message->getHeaders()->has('Sender') && !$message->getHeaders()->has('From')) { + $message->getHeaders()->addMailboxHeader('Sender', $this->sender); + } + } + } + + if ($this->recipients) { + $event->getEnvelope()->setRecipients($this->recipients); + } + } + + public static function getSubscribedEvents(): array + { + return [ + // should be the last one to allow header changes by other listeners first + MessageEvent::class => ['onMessage', -255], + ]; + } +} diff --git a/3rdparty/symfony/mailer/EventListener/MessageListener.php b/3rdparty/symfony/mailer/EventListener/MessageListener.php new file mode 100644 index 00000000..ec822d9c --- /dev/null +++ b/3rdparty/symfony/mailer/EventListener/MessageListener.php @@ -0,0 +1,134 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Component\Mailer\EventListener; + +use Symfony\Component\EventDispatcher\EventSubscriberInterface; +use Symfony\Component\Mailer\Event\MessageEvent; +use Symfony\Component\Mailer\Exception\InvalidArgumentException; +use Symfony\Component\Mailer\Exception\RuntimeException; +use Symfony\Component\Mime\BodyRendererInterface; +use Symfony\Component\Mime\Header\Headers; +use Symfony\Component\Mime\Header\MailboxListHeader; +use Symfony\Component\Mime\Message; + +/** + * Manipulates the headers and the body of a Message. + * + * @author Fabien Potencier + */ +class MessageListener implements EventSubscriberInterface +{ + public const HEADER_SET_IF_EMPTY = 1; + public const HEADER_ADD = 2; + public const HEADER_REPLACE = 3; + public const DEFAULT_RULES = [ + 'from' => self::HEADER_SET_IF_EMPTY, + 'return-path' => self::HEADER_SET_IF_EMPTY, + 'reply-to' => self::HEADER_ADD, + 'to' => self::HEADER_SET_IF_EMPTY, + 'cc' => self::HEADER_ADD, + 'bcc' => self::HEADER_ADD, + ]; + + private ?Headers $headers; + private array $headerRules = []; + private ?BodyRendererInterface $renderer; + + public function __construct(?Headers $headers = null, ?BodyRendererInterface $renderer = null, array $headerRules = self::DEFAULT_RULES) + { + $this->headers = $headers; + $this->renderer = $renderer; + foreach ($headerRules as $headerName => $rule) { + $this->addHeaderRule($headerName, $rule); + } + } + + public function addHeaderRule(string $headerName, int $rule): void + { + if ($rule < 1 || $rule > 3) { + throw new InvalidArgumentException(sprintf('The "%d" rule is not supported.', $rule)); + } + + $this->headerRules[strtolower($headerName)] = $rule; + } + + public function onMessage(MessageEvent $event): void + { + $message = $event->getMessage(); + if (!$message instanceof Message) { + return; + } + + $this->setHeaders($message); + $this->renderMessage($message); + } + + private function setHeaders(Message $message): void + { + if (!$this->headers) { + return; + } + + $headers = $message->getHeaders(); + foreach ($this->headers->all() as $name => $header) { + if (!$headers->has($name)) { + $headers->add($header); + + continue; + } + + switch ($this->headerRules[$name] ?? self::HEADER_SET_IF_EMPTY) { + case self::HEADER_SET_IF_EMPTY: + break; + + case self::HEADER_REPLACE: + $headers->remove($name); + $headers->add($header); + + break; + + case self::HEADER_ADD: + if (!Headers::isUniqueHeader($name)) { + $headers->add($header); + + break; + } + + $h = $headers->get($name); + if (!$h instanceof MailboxListHeader) { + throw new RuntimeException(sprintf('Unable to set header "%s".', $name)); + } + + Headers::checkHeaderClass($header); + foreach ($header->getAddresses() as $address) { + $h->addAddress($address); + } + } + } + } + + private function renderMessage(Message $message): void + { + if (!$this->renderer) { + return; + } + + $this->renderer->render($message); + } + + public static function getSubscribedEvents(): array + { + return [ + MessageEvent::class => 'onMessage', + ]; + } +} diff --git a/3rdparty/symfony/mailer/EventListener/MessageLoggerListener.php b/3rdparty/symfony/mailer/EventListener/MessageLoggerListener.php new file mode 100644 index 00000000..6c5605fa --- /dev/null +++ b/3rdparty/symfony/mailer/EventListener/MessageLoggerListener.php @@ -0,0 +1,57 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Component\Mailer\EventListener; + +use Symfony\Component\EventDispatcher\EventSubscriberInterface; +use Symfony\Component\Mailer\Event\MessageEvent; +use Symfony\Component\Mailer\Event\MessageEvents; +use Symfony\Contracts\Service\ResetInterface; + +/** + * Logs Messages. + * + * @author Fabien Potencier + */ +class MessageLoggerListener implements EventSubscriberInterface, ResetInterface +{ + private MessageEvents $events; + + public function __construct() + { + $this->events = new MessageEvents(); + } + + /** + * @return void + */ + public function reset() + { + $this->events = new MessageEvents(); + } + + public function onMessage(MessageEvent $event): void + { + $this->events->add($event); + } + + public function getEvents(): MessageEvents + { + return $this->events; + } + + public static function getSubscribedEvents(): array + { + return [ + MessageEvent::class => ['onMessage', -255], + ]; + } +} diff --git a/3rdparty/symfony/mailer/EventListener/MessengerTransportListener.php b/3rdparty/symfony/mailer/EventListener/MessengerTransportListener.php new file mode 100644 index 00000000..8cf8cc80 --- /dev/null +++ b/3rdparty/symfony/mailer/EventListener/MessengerTransportListener.php @@ -0,0 +1,49 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Component\Mailer\EventListener; + +use Symfony\Component\EventDispatcher\EventSubscriberInterface; +use Symfony\Component\Mailer\Event\MessageEvent; +use Symfony\Component\Messenger\Stamp\TransportNamesStamp; +use Symfony\Component\Mime\Message; + +/** + * Allows messages to be sent to specific Messenger transports via the "X-Bus-Transport" MIME header. + * + * @author Fabien Potencier + */ +final class MessengerTransportListener implements EventSubscriberInterface +{ + public function onMessage(MessageEvent $event): void + { + if (!$event->isQueued()) { + return; + } + + $message = $event->getMessage(); + if (!$message instanceof Message || !$message->getHeaders()->has('X-Bus-Transport')) { + return; + } + + $names = $message->getHeaders()->get('X-Bus-Transport')->getBody(); + $names = array_map('trim', explode(',', $names)); + $event->addStamp(new TransportNamesStamp($names)); + $message->getHeaders()->remove('X-Bus-Transport'); + } + + public static function getSubscribedEvents(): array + { + return [ + MessageEvent::class => 'onMessage', + ]; + } +} diff --git a/3rdparty/symfony/mailer/Exception/ExceptionInterface.php b/3rdparty/symfony/mailer/Exception/ExceptionInterface.php new file mode 100644 index 00000000..2f0f3a6f --- /dev/null +++ b/3rdparty/symfony/mailer/Exception/ExceptionInterface.php @@ -0,0 +1,21 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Component\Mailer\Exception; + +/** + * Exception interface for all exceptions thrown by the component. + * + * @author Fabien Potencier + */ +interface ExceptionInterface extends \Throwable +{ +} diff --git a/3rdparty/symfony/mailer/Exception/HttpTransportException.php b/3rdparty/symfony/mailer/Exception/HttpTransportException.php new file mode 100644 index 00000000..ad10910f --- /dev/null +++ b/3rdparty/symfony/mailer/Exception/HttpTransportException.php @@ -0,0 +1,34 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Component\Mailer\Exception; + +use Symfony\Contracts\HttpClient\ResponseInterface; + +/** + * @author Fabien Potencier + */ +class HttpTransportException extends TransportException +{ + private ResponseInterface $response; + + public function __construct(string $message, ResponseInterface $response, int $code = 0, ?\Throwable $previous = null) + { + parent::__construct($message, $code, $previous); + + $this->response = $response; + } + + public function getResponse(): ResponseInterface + { + return $this->response; + } +} diff --git a/3rdparty/symfony/mailer/Exception/IncompleteDsnException.php b/3rdparty/symfony/mailer/Exception/IncompleteDsnException.php new file mode 100644 index 00000000..f2618b65 --- /dev/null +++ b/3rdparty/symfony/mailer/Exception/IncompleteDsnException.php @@ -0,0 +1,19 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Component\Mailer\Exception; + +/** + * @author Konstantin Myakshin + */ +class IncompleteDsnException extends InvalidArgumentException +{ +} diff --git a/3rdparty/symfony/mailer/Exception/InvalidArgumentException.php b/3rdparty/symfony/mailer/Exception/InvalidArgumentException.php new file mode 100644 index 00000000..ba533345 --- /dev/null +++ b/3rdparty/symfony/mailer/Exception/InvalidArgumentException.php @@ -0,0 +1,19 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Component\Mailer\Exception; + +/** + * @author Fabien Potencier + */ +class InvalidArgumentException extends \InvalidArgumentException implements ExceptionInterface +{ +} diff --git a/3rdparty/symfony/mailer/Exception/LogicException.php b/3rdparty/symfony/mailer/Exception/LogicException.php new file mode 100644 index 00000000..487c0a34 --- /dev/null +++ b/3rdparty/symfony/mailer/Exception/LogicException.php @@ -0,0 +1,19 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Component\Mailer\Exception; + +/** + * @author Fabien Potencier + */ +class LogicException extends \LogicException implements ExceptionInterface +{ +} diff --git a/3rdparty/symfony/mailer/Exception/RuntimeException.php b/3rdparty/symfony/mailer/Exception/RuntimeException.php new file mode 100644 index 00000000..44b79cc6 --- /dev/null +++ b/3rdparty/symfony/mailer/Exception/RuntimeException.php @@ -0,0 +1,19 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Component\Mailer\Exception; + +/** + * @author Fabien Potencier + */ +class RuntimeException extends \RuntimeException implements ExceptionInterface +{ +} diff --git a/3rdparty/symfony/mailer/Exception/TransportException.php b/3rdparty/symfony/mailer/Exception/TransportException.php new file mode 100644 index 00000000..ea538d6f --- /dev/null +++ b/3rdparty/symfony/mailer/Exception/TransportException.php @@ -0,0 +1,30 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Component\Mailer\Exception; + +/** + * @author Fabien Potencier + */ +class TransportException extends RuntimeException implements TransportExceptionInterface +{ + private string $debug = ''; + + public function getDebug(): string + { + return $this->debug; + } + + public function appendDebug(string $debug): void + { + $this->debug .= $debug; + } +} diff --git a/3rdparty/symfony/mailer/Exception/TransportExceptionInterface.php b/3rdparty/symfony/mailer/Exception/TransportExceptionInterface.php new file mode 100644 index 00000000..4318f5ce --- /dev/null +++ b/3rdparty/symfony/mailer/Exception/TransportExceptionInterface.php @@ -0,0 +1,22 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Component\Mailer\Exception; + +/** + * @author Fabien Potencier + */ +interface TransportExceptionInterface extends ExceptionInterface +{ + public function getDebug(): string; + + public function appendDebug(string $debug): void; +} diff --git a/3rdparty/symfony/mailer/Exception/UnexpectedResponseException.php b/3rdparty/symfony/mailer/Exception/UnexpectedResponseException.php new file mode 100644 index 00000000..e779df12 --- /dev/null +++ b/3rdparty/symfony/mailer/Exception/UnexpectedResponseException.php @@ -0,0 +1,16 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Component\Mailer\Exception; + +class UnexpectedResponseException extends TransportException +{ +} diff --git a/3rdparty/symfony/mailer/Exception/UnsupportedSchemeException.php b/3rdparty/symfony/mailer/Exception/UnsupportedSchemeException.php new file mode 100644 index 00000000..257fd47d --- /dev/null +++ b/3rdparty/symfony/mailer/Exception/UnsupportedSchemeException.php @@ -0,0 +1,101 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Component\Mailer\Exception; + +use Symfony\Component\Mailer\Bridge; +use Symfony\Component\Mailer\Transport\Dsn; + +/** + * @author Konstantin Myakshin + */ +class UnsupportedSchemeException extends LogicException +{ + private const SCHEME_TO_PACKAGE_MAP = [ + 'brevo' => [ + 'class' => Bridge\Brevo\Transport\BrevoTransportFactory::class, + 'package' => 'symfony/brevo-mailer', + ], + 'gmail' => [ + 'class' => Bridge\Google\Transport\GmailTransportFactory::class, + 'package' => 'symfony/google-mailer', + ], + 'infobip' => [ + 'class' => Bridge\Infobip\Transport\InfobipTransportFactory::class, + 'package' => 'symfony/infobip-mailer', + ], + 'mailersend' => [ + 'class' => Bridge\MailerSend\Transport\MailerSendTransportFactory::class, + 'package' => 'symfony/mailersend-mailer', + ], + 'mailgun' => [ + 'class' => Bridge\Mailgun\Transport\MailgunTransportFactory::class, + 'package' => 'symfony/mailgun-mailer', + ], + 'mailjet' => [ + 'class' => Bridge\Mailjet\Transport\MailjetTransportFactory::class, + 'package' => 'symfony/mailjet-mailer', + ], + 'mailpace' => [ + 'class' => Bridge\MailPace\Transport\MailPaceTransportFactory::class, + 'package' => 'symfony/mail-pace-mailer', + ], + 'mandrill' => [ + 'class' => Bridge\Mailchimp\Transport\MandrillTransportFactory::class, + 'package' => 'symfony/mailchimp-mailer', + ], + 'ohmysmtp' => [ + 'class' => Bridge\OhMySmtp\Transport\OhMySmtpTransportFactory::class, + 'package' => 'symfony/oh-my-smtp-mailer', + ], + 'postmark' => [ + 'class' => Bridge\Postmark\Transport\PostmarkTransportFactory::class, + 'package' => 'symfony/postmark-mailer', + ], + 'scaleway' => [ + 'class' => Bridge\Scaleway\Transport\ScalewayTransportFactory::class, + 'package' => 'symfony/scaleway-mailer', + ], + 'sendgrid' => [ + 'class' => Bridge\Sendgrid\Transport\SendgridTransportFactory::class, + 'package' => 'symfony/sendgrid-mailer', + ], + 'sendinblue' => [ + 'class' => Bridge\Sendinblue\Transport\SendinblueTransportFactory::class, + 'package' => 'symfony/sendinblue-mailer', + ], + 'ses' => [ + 'class' => Bridge\Amazon\Transport\SesTransportFactory::class, + 'package' => 'symfony/amazon-mailer', + ], + ]; + + public function __construct(Dsn $dsn, ?string $name = null, array $supported = []) + { + $provider = $dsn->getScheme(); + if (false !== $pos = strpos($provider, '+')) { + $provider = substr($provider, 0, $pos); + } + $package = self::SCHEME_TO_PACKAGE_MAP[$provider] ?? null; + if ($package && !class_exists($package['class'])) { + parent::__construct(sprintf('Unable to send emails via "%s" as the bridge is not installed. Try running "composer require %s".', $provider, $package['package'])); + + return; + } + + $message = sprintf('The "%s" scheme is not supported', $dsn->getScheme()); + if ($name && $supported) { + $message .= sprintf('; supported schemes for mailer "%s" are: "%s"', $name, implode('", "', $supported)); + } + + parent::__construct($message.'.'); + } +} diff --git a/3rdparty/symfony/mailer/Header/MetadataHeader.php b/3rdparty/symfony/mailer/Header/MetadataHeader.php new file mode 100644 index 00000000..d6ee5440 --- /dev/null +++ b/3rdparty/symfony/mailer/Header/MetadataHeader.php @@ -0,0 +1,34 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Component\Mailer\Header; + +use Symfony\Component\Mime\Header\UnstructuredHeader; + +/** + * @author Kevin Bond + */ +final class MetadataHeader extends UnstructuredHeader +{ + private string $key; + + public function __construct(string $key, string $value) + { + $this->key = $key; + + parent::__construct('X-Metadata-'.$key, $value); + } + + public function getKey(): string + { + return $this->key; + } +} diff --git a/3rdparty/symfony/mailer/Header/TagHeader.php b/3rdparty/symfony/mailer/Header/TagHeader.php new file mode 100644 index 00000000..7115caeb --- /dev/null +++ b/3rdparty/symfony/mailer/Header/TagHeader.php @@ -0,0 +1,25 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Component\Mailer\Header; + +use Symfony\Component\Mime\Header\UnstructuredHeader; + +/** + * @author Kevin Bond + */ +final class TagHeader extends UnstructuredHeader +{ + public function __construct(string $value) + { + parent::__construct('X-Tag', $value); + } +} diff --git a/3rdparty/symfony/mailer/LICENSE b/3rdparty/symfony/mailer/LICENSE new file mode 100644 index 00000000..f37c76b5 --- /dev/null +++ b/3rdparty/symfony/mailer/LICENSE @@ -0,0 +1,19 @@ +Copyright (c) 2019-present Fabien Potencier + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is furnished +to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +THE SOFTWARE. diff --git a/3rdparty/symfony/mailer/Mailer.php b/3rdparty/symfony/mailer/Mailer.php new file mode 100644 index 00000000..6df99091 --- /dev/null +++ b/3rdparty/symfony/mailer/Mailer.php @@ -0,0 +1,76 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Component\Mailer; + +use Psr\EventDispatcher\EventDispatcherInterface; +use Symfony\Component\Mailer\Event\MessageEvent; +use Symfony\Component\Mailer\Exception\TransportExceptionInterface; +use Symfony\Component\Mailer\Messenger\SendEmailMessage; +use Symfony\Component\Mailer\Transport\TransportInterface; +use Symfony\Component\Messenger\Exception\HandlerFailedException; +use Symfony\Component\Messenger\MessageBusInterface; +use Symfony\Component\Mime\RawMessage; + +/** + * @author Fabien Potencier + */ +final class Mailer implements MailerInterface +{ + private TransportInterface $transport; + private ?MessageBusInterface $bus; + private ?EventDispatcherInterface $dispatcher; + + public function __construct(TransportInterface $transport, ?MessageBusInterface $bus = null, ?EventDispatcherInterface $dispatcher = null) + { + $this->transport = $transport; + $this->bus = $bus; + $this->dispatcher = $dispatcher; + } + + public function send(RawMessage $message, ?Envelope $envelope = null): void + { + if (null === $this->bus) { + $this->transport->send($message, $envelope); + + return; + } + + $stamps = []; + if (null !== $this->dispatcher) { + // The dispatched event here has `queued` set to `true`; the goal is NOT to render the message, but to let + // listeners do something before a message is sent to the queue. + // We are using a cloned message as we still want to dispatch the **original** message, not the one modified by listeners. + // That's because the listeners will run again when the email is sent via Messenger by the transport (see `AbstractTransport`). + // Listeners should act depending on the `$queued` argument of the `MessageEvent` instance. + $clonedMessage = clone $message; + $clonedEnvelope = null !== $envelope ? clone $envelope : Envelope::create($clonedMessage); + $event = new MessageEvent($clonedMessage, $clonedEnvelope, (string) $this->transport, true); + $this->dispatcher->dispatch($event); + $stamps = $event->getStamps(); + + if ($event->isRejected()) { + return; + } + } + + try { + $this->bus->dispatch(new SendEmailMessage($message, $envelope), $stamps); + } catch (HandlerFailedException $e) { + foreach ($e->getWrappedExceptions() as $nested) { + if ($nested instanceof TransportExceptionInterface) { + throw $nested; + } + } + throw $e; + } + } +} diff --git a/3rdparty/symfony/mailer/MailerInterface.php b/3rdparty/symfony/mailer/MailerInterface.php new file mode 100644 index 00000000..ebac4b53 --- /dev/null +++ b/3rdparty/symfony/mailer/MailerInterface.php @@ -0,0 +1,30 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Component\Mailer; + +use Symfony\Component\Mailer\Exception\TransportExceptionInterface; +use Symfony\Component\Mime\RawMessage; + +/** + * Interface for mailers able to send emails synchronously and/or asynchronously. + * + * Implementations must support synchronous and asynchronous sending. + * + * @author Fabien Potencier + */ +interface MailerInterface +{ + /** + * @throws TransportExceptionInterface + */ + public function send(RawMessage $message, ?Envelope $envelope = null): void; +} diff --git a/3rdparty/symfony/mailer/Messenger/MessageHandler.php b/3rdparty/symfony/mailer/Messenger/MessageHandler.php new file mode 100644 index 00000000..f8fb14fc --- /dev/null +++ b/3rdparty/symfony/mailer/Messenger/MessageHandler.php @@ -0,0 +1,33 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Component\Mailer\Messenger; + +use Symfony\Component\Mailer\SentMessage; +use Symfony\Component\Mailer\Transport\TransportInterface; + +/** + * @author Fabien Potencier + */ +class MessageHandler +{ + private TransportInterface $transport; + + public function __construct(TransportInterface $transport) + { + $this->transport = $transport; + } + + public function __invoke(SendEmailMessage $message): ?SentMessage + { + return $this->transport->send($message->getMessage(), $message->getEnvelope()); + } +} diff --git a/3rdparty/symfony/mailer/Messenger/SendEmailMessage.php b/3rdparty/symfony/mailer/Messenger/SendEmailMessage.php new file mode 100644 index 00000000..18bd5062 --- /dev/null +++ b/3rdparty/symfony/mailer/Messenger/SendEmailMessage.php @@ -0,0 +1,40 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Component\Mailer\Messenger; + +use Symfony\Component\Mailer\Envelope; +use Symfony\Component\Mime\RawMessage; + +/** + * @author Fabien Potencier + */ +class SendEmailMessage +{ + private RawMessage $message; + private ?Envelope $envelope; + + public function __construct(RawMessage $message, ?Envelope $envelope = null) + { + $this->message = $message; + $this->envelope = $envelope; + } + + public function getMessage(): RawMessage + { + return $this->message; + } + + public function getEnvelope(): ?Envelope + { + return $this->envelope; + } +} diff --git a/3rdparty/symfony/mailer/SentMessage.php b/3rdparty/symfony/mailer/SentMessage.php new file mode 100644 index 00000000..be847118 --- /dev/null +++ b/3rdparty/symfony/mailer/SentMessage.php @@ -0,0 +1,95 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Component\Mailer; + +use Symfony\Component\Mime\Message; +use Symfony\Component\Mime\RawMessage; + +/** + * @author Fabien Potencier + */ +class SentMessage +{ + private RawMessage $original; + private RawMessage $raw; + private Envelope $envelope; + private string $messageId; + private string $debug = ''; + + /** + * @internal + */ + public function __construct(RawMessage $message, Envelope $envelope) + { + $message->ensureValidity(); + + $this->original = $message; + $this->envelope = $envelope; + + if ($message instanceof Message) { + $message = clone $message; + $headers = $message->getHeaders(); + if (!$headers->has('Message-ID')) { + $headers->addIdHeader('Message-ID', $message->generateMessageId()); + } + $this->messageId = $headers->get('Message-ID')->getId(); + $this->raw = new RawMessage($message->toIterable()); + } else { + $this->raw = $message; + } + } + + public function getMessage(): RawMessage + { + return $this->raw; + } + + public function getOriginalMessage(): RawMessage + { + return $this->original; + } + + public function getEnvelope(): Envelope + { + return $this->envelope; + } + + public function setMessageId(string $id): void + { + $this->messageId = $id; + } + + public function getMessageId(): string + { + return $this->messageId; + } + + public function getDebug(): string + { + return $this->debug; + } + + public function appendDebug(string $debug): void + { + $this->debug .= $debug; + } + + public function toString(): string + { + return $this->raw->toString(); + } + + public function toIterable(): iterable + { + return $this->raw->toIterable(); + } +} diff --git a/3rdparty/symfony/mailer/Transport.php b/3rdparty/symfony/mailer/Transport.php new file mode 100644 index 00000000..599f4fbc --- /dev/null +++ b/3rdparty/symfony/mailer/Transport.php @@ -0,0 +1,188 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Component\Mailer; + +use Psr\EventDispatcher\EventDispatcherInterface; +use Psr\Log\LoggerInterface; +use Symfony\Component\Mailer\Bridge\Amazon\Transport\SesTransportFactory; +use Symfony\Component\Mailer\Bridge\Brevo\Transport\BrevoTransportFactory; +use Symfony\Component\Mailer\Bridge\Google\Transport\GmailTransportFactory; +use Symfony\Component\Mailer\Bridge\Infobip\Transport\InfobipTransportFactory; +use Symfony\Component\Mailer\Bridge\Mailchimp\Transport\MandrillTransportFactory; +use Symfony\Component\Mailer\Bridge\MailerSend\Transport\MailerSendTransportFactory; +use Symfony\Component\Mailer\Bridge\Mailgun\Transport\MailgunTransportFactory; +use Symfony\Component\Mailer\Bridge\Mailjet\Transport\MailjetTransportFactory; +use Symfony\Component\Mailer\Bridge\MailPace\Transport\MailPaceTransportFactory; +use Symfony\Component\Mailer\Bridge\OhMySmtp\Transport\OhMySmtpTransportFactory; +use Symfony\Component\Mailer\Bridge\Postmark\Transport\PostmarkTransportFactory; +use Symfony\Component\Mailer\Bridge\Scaleway\Transport\ScalewayTransportFactory; +use Symfony\Component\Mailer\Bridge\Sendgrid\Transport\SendgridTransportFactory; +use Symfony\Component\Mailer\Bridge\Sendinblue\Transport\SendinblueTransportFactory; +use Symfony\Component\Mailer\Exception\InvalidArgumentException; +use Symfony\Component\Mailer\Exception\UnsupportedSchemeException; +use Symfony\Component\Mailer\Transport\Dsn; +use Symfony\Component\Mailer\Transport\FailoverTransport; +use Symfony\Component\Mailer\Transport\NativeTransportFactory; +use Symfony\Component\Mailer\Transport\NullTransportFactory; +use Symfony\Component\Mailer\Transport\RoundRobinTransport; +use Symfony\Component\Mailer\Transport\SendmailTransportFactory; +use Symfony\Component\Mailer\Transport\Smtp\EsmtpTransportFactory; +use Symfony\Component\Mailer\Transport\TransportFactoryInterface; +use Symfony\Component\Mailer\Transport\TransportInterface; +use Symfony\Component\Mailer\Transport\Transports; +use Symfony\Contracts\HttpClient\HttpClientInterface; + +/** + * @author Fabien Potencier + * @author Konstantin Myakshin + */ +final class Transport +{ + private const FACTORY_CLASSES = [ + BrevoTransportFactory::class, + GmailTransportFactory::class, + InfobipTransportFactory::class, + MailerSendTransportFactory::class, + MailgunTransportFactory::class, + MailjetTransportFactory::class, + MailPaceTransportFactory::class, + MandrillTransportFactory::class, + OhMySmtpTransportFactory::class, + PostmarkTransportFactory::class, + ScalewayTransportFactory::class, + SendgridTransportFactory::class, + SendinblueTransportFactory::class, + SesTransportFactory::class, + ]; + + private iterable $factories; + + public static function fromDsn(#[\SensitiveParameter] string $dsn, ?EventDispatcherInterface $dispatcher = null, ?HttpClientInterface $client = null, ?LoggerInterface $logger = null): TransportInterface + { + $factory = new self(iterator_to_array(self::getDefaultFactories($dispatcher, $client, $logger))); + + return $factory->fromString($dsn); + } + + public static function fromDsns(#[\SensitiveParameter] array $dsns, ?EventDispatcherInterface $dispatcher = null, ?HttpClientInterface $client = null, ?LoggerInterface $logger = null): TransportInterface + { + $factory = new self(iterator_to_array(self::getDefaultFactories($dispatcher, $client, $logger))); + + return $factory->fromStrings($dsns); + } + + /** + * @param TransportFactoryInterface[] $factories + */ + public function __construct(iterable $factories) + { + $this->factories = $factories; + } + + public function fromStrings(#[\SensitiveParameter] array $dsns): Transports + { + $transports = []; + foreach ($dsns as $name => $dsn) { + $transports[$name] = $this->fromString($dsn); + } + + return new Transports($transports); + } + + public function fromString(#[\SensitiveParameter] string $dsn): TransportInterface + { + [$transport, $offset] = $this->parseDsn($dsn); + if ($offset !== \strlen($dsn)) { + throw new InvalidArgumentException('The mailer DSN has some garbage at the end.'); + } + + return $transport; + } + + private function parseDsn(#[\SensitiveParameter] string $dsn, int $offset = 0): array + { + static $keywords = [ + 'failover' => FailoverTransport::class, + 'roundrobin' => RoundRobinTransport::class, + ]; + + while (true) { + foreach ($keywords as $name => $class) { + $name .= '('; + if ($name === substr($dsn, $offset, \strlen($name))) { + $offset += \strlen($name) - 1; + preg_match('{\(([^()]|(?R))*\)}A', $dsn, $matches, 0, $offset); + if (!isset($matches[0])) { + continue; + } + + ++$offset; + $args = []; + while (true) { + [$arg, $offset] = $this->parseDsn($dsn, $offset); + $args[] = $arg; + if (\strlen($dsn) === $offset) { + break; + } + ++$offset; + if (')' === $dsn[$offset - 1]) { + break; + } + } + + return [new $class($args), $offset]; + } + } + + if (preg_match('{(\w+)\(}A', $dsn, $matches, 0, $offset)) { + throw new InvalidArgumentException(sprintf('The "%s" keyword is not valid (valid ones are "%s"), ', $matches[1], implode('", "', array_keys($keywords)))); + } + + if ($pos = strcspn($dsn, ' )', $offset)) { + return [$this->fromDsnObject(Dsn::fromString(substr($dsn, $offset, $pos))), $offset + $pos]; + } + + return [$this->fromDsnObject(Dsn::fromString(substr($dsn, $offset))), \strlen($dsn)]; + } + } + + public function fromDsnObject(Dsn $dsn): TransportInterface + { + foreach ($this->factories as $factory) { + if ($factory->supports($dsn)) { + return $factory->create($dsn); + } + } + + throw new UnsupportedSchemeException($dsn); + } + + /** + * @return \Traversable + */ + public static function getDefaultFactories(?EventDispatcherInterface $dispatcher = null, ?HttpClientInterface $client = null, ?LoggerInterface $logger = null): \Traversable + { + foreach (self::FACTORY_CLASSES as $factoryClass) { + if (class_exists($factoryClass)) { + yield new $factoryClass($dispatcher, $client, $logger); + } + } + + yield new NullTransportFactory($dispatcher, $client, $logger); + + yield new SendmailTransportFactory($dispatcher, $client, $logger); + + yield new EsmtpTransportFactory($dispatcher, $client, $logger); + + yield new NativeTransportFactory($dispatcher, $client, $logger); + } +} diff --git a/3rdparty/symfony/mailer/Transport/AbstractApiTransport.php b/3rdparty/symfony/mailer/Transport/AbstractApiTransport.php new file mode 100644 index 00000000..040745ee --- /dev/null +++ b/3rdparty/symfony/mailer/Transport/AbstractApiTransport.php @@ -0,0 +1,47 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Component\Mailer\Transport; + +use Symfony\Component\Mailer\Envelope; +use Symfony\Component\Mailer\Exception\RuntimeException; +use Symfony\Component\Mailer\SentMessage; +use Symfony\Component\Mime\Address; +use Symfony\Component\Mime\Email; +use Symfony\Component\Mime\MessageConverter; +use Symfony\Contracts\HttpClient\ResponseInterface; + +/** + * @author Fabien Potencier + */ +abstract class AbstractApiTransport extends AbstractHttpTransport +{ + abstract protected function doSendApi(SentMessage $sentMessage, Email $email, Envelope $envelope): ResponseInterface; + + protected function doSendHttp(SentMessage $message): ResponseInterface + { + try { + $email = MessageConverter::toEmail($message->getOriginalMessage()); + } catch (\Exception $e) { + throw new RuntimeException(sprintf('Unable to send message with the "%s" transport: ', __CLASS__).$e->getMessage(), 0, $e); + } + + return $this->doSendApi($message, $email, $message->getEnvelope()); + } + + /** + * @return Address[] + */ + protected function getRecipients(Email $email, Envelope $envelope): array + { + return array_filter($envelope->getRecipients(), fn (Address $address) => false === \in_array($address, array_merge($email->getCc(), $email->getBcc()), true)); + } +} diff --git a/3rdparty/symfony/mailer/Transport/AbstractHttpTransport.php b/3rdparty/symfony/mailer/Transport/AbstractHttpTransport.php new file mode 100644 index 00000000..96b7ec00 --- /dev/null +++ b/3rdparty/symfony/mailer/Transport/AbstractHttpTransport.php @@ -0,0 +1,78 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Component\Mailer\Transport; + +use Psr\EventDispatcher\EventDispatcherInterface; +use Psr\Log\LoggerInterface; +use Symfony\Component\HttpClient\HttpClient; +use Symfony\Component\Mailer\Exception\HttpTransportException; +use Symfony\Component\Mailer\SentMessage; +use Symfony\Contracts\HttpClient\HttpClientInterface; +use Symfony\Contracts\HttpClient\ResponseInterface; + +/** + * @author Victor Bocharsky + */ +abstract class AbstractHttpTransport extends AbstractTransport +{ + protected $host; + protected $port; + protected $client; + + public function __construct(?HttpClientInterface $client = null, ?EventDispatcherInterface $dispatcher = null, ?LoggerInterface $logger = null) + { + $this->client = $client; + if (null === $client) { + if (!class_exists(HttpClient::class)) { + throw new \LogicException(sprintf('You cannot use "%s" as the HttpClient component is not installed. Try running "composer require symfony/http-client".', __CLASS__)); + } + + $this->client = HttpClient::create(); + } + + parent::__construct($dispatcher, $logger); + } + + /** + * @return $this + */ + public function setHost(?string $host): static + { + $this->host = $host; + + return $this; + } + + /** + * @return $this + */ + public function setPort(?int $port): static + { + $this->port = $port; + + return $this; + } + + abstract protected function doSendHttp(SentMessage $message): ResponseInterface; + + protected function doSend(SentMessage $message): void + { + try { + $response = $this->doSendHttp($message); + $message->appendDebug($response->getInfo('debug') ?? ''); + } catch (HttpTransportException $e) { + $e->appendDebug($e->getResponse()->getInfo('debug') ?? ''); + + throw $e; + } + } +} diff --git a/3rdparty/symfony/mailer/Transport/AbstractTransport.php b/3rdparty/symfony/mailer/Transport/AbstractTransport.php new file mode 100644 index 00000000..9a1cd685 --- /dev/null +++ b/3rdparty/symfony/mailer/Transport/AbstractTransport.php @@ -0,0 +1,136 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Component\Mailer\Transport; + +use Psr\EventDispatcher\EventDispatcherInterface; +use Psr\Log\LoggerInterface; +use Psr\Log\NullLogger; +use Symfony\Bridge\Twig\Mime\TemplatedEmail; +use Symfony\Component\Mailer\Envelope; +use Symfony\Component\Mailer\Event\FailedMessageEvent; +use Symfony\Component\Mailer\Event\MessageEvent; +use Symfony\Component\Mailer\Event\SentMessageEvent; +use Symfony\Component\Mailer\Exception\LogicException; +use Symfony\Component\Mailer\SentMessage; +use Symfony\Component\Mime\Address; +use Symfony\Component\Mime\BodyRendererInterface; +use Symfony\Component\Mime\RawMessage; + +/** + * @author Fabien Potencier + */ +abstract class AbstractTransport implements TransportInterface +{ + private ?EventDispatcherInterface $dispatcher; + private LoggerInterface $logger; + private float $rate = 0; + private float $lastSent = 0; + + public function __construct(?EventDispatcherInterface $dispatcher = null, ?LoggerInterface $logger = null) + { + $this->dispatcher = $dispatcher; + $this->logger = $logger ?? new NullLogger(); + } + + /** + * Sets the maximum number of messages to send per second (0 to disable). + * + * @return $this + */ + public function setMaxPerSecond(float $rate): static + { + if (0 >= $rate) { + $rate = 0; + } + + $this->rate = $rate; + $this->lastSent = 0; + + return $this; + } + + public function send(RawMessage $message, ?Envelope $envelope = null): ?SentMessage + { + $message = clone $message; + $envelope = null !== $envelope ? clone $envelope : Envelope::create($message); + + try { + if (!$this->dispatcher) { + $sentMessage = new SentMessage($message, $envelope); + $this->doSend($sentMessage); + + return $sentMessage; + } + + $event = new MessageEvent($message, $envelope, (string) $this); + $this->dispatcher->dispatch($event); + if ($event->isRejected()) { + return null; + } + + $envelope = $event->getEnvelope(); + $message = $event->getMessage(); + + if ($message instanceof TemplatedEmail && !$message->isRendered()) { + throw new LogicException(sprintf('You must configure a "%s" when a "%s" instance has a text or HTML template set.', BodyRendererInterface::class, get_debug_type($message))); + } + + $sentMessage = new SentMessage($message, $envelope); + + try { + $this->doSend($sentMessage); + } catch (\Throwable $error) { + $this->dispatcher->dispatch(new FailedMessageEvent($message, $error)); + $this->checkThrottling(); + + throw $error; + } + + $this->dispatcher->dispatch(new SentMessageEvent($sentMessage)); + + return $sentMessage; + } finally { + $this->checkThrottling(); + } + } + + abstract protected function doSend(SentMessage $message): void; + + /** + * @param Address[] $addresses + * + * @return string[] + */ + protected function stringifyAddresses(array $addresses): array + { + return array_map(fn (Address $a) => $a->toString(), $addresses); + } + + protected function getLogger(): LoggerInterface + { + return $this->logger; + } + + private function checkThrottling(): void + { + if (0 == $this->rate) { + return; + } + + $sleep = (1 / $this->rate) - (microtime(true) - $this->lastSent); + if (0 < $sleep) { + $this->logger->debug(sprintf('Email transport "%s" sleeps for %.2f seconds', __CLASS__, $sleep)); + usleep((int) ($sleep * 1000000)); + } + $this->lastSent = microtime(true); + } +} diff --git a/3rdparty/symfony/mailer/Transport/AbstractTransportFactory.php b/3rdparty/symfony/mailer/Transport/AbstractTransportFactory.php new file mode 100644 index 00000000..96e90f34 --- /dev/null +++ b/3rdparty/symfony/mailer/Transport/AbstractTransportFactory.php @@ -0,0 +1,51 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Component\Mailer\Transport; + +use Psr\EventDispatcher\EventDispatcherInterface; +use Psr\Log\LoggerInterface; +use Symfony\Component\Mailer\Exception\IncompleteDsnException; +use Symfony\Contracts\HttpClient\HttpClientInterface; + +/** + * @author Konstantin Myakshin + */ +abstract class AbstractTransportFactory implements TransportFactoryInterface +{ + protected $dispatcher; + protected $client; + protected $logger; + + public function __construct(?EventDispatcherInterface $dispatcher = null, ?HttpClientInterface $client = null, ?LoggerInterface $logger = null) + { + $this->dispatcher = $dispatcher; + $this->client = $client; + $this->logger = $logger; + } + + public function supports(Dsn $dsn): bool + { + return \in_array($dsn->getScheme(), $this->getSupportedSchemes()); + } + + abstract protected function getSupportedSchemes(): array; + + protected function getUser(Dsn $dsn): string + { + return $dsn->getUser() ?? throw new IncompleteDsnException('User is not set.'); + } + + protected function getPassword(Dsn $dsn): string + { + return $dsn->getPassword() ?? throw new IncompleteDsnException('Password is not set.'); + } +} diff --git a/3rdparty/symfony/mailer/Transport/Dsn.php b/3rdparty/symfony/mailer/Transport/Dsn.php new file mode 100644 index 00000000..0cff6aad --- /dev/null +++ b/3rdparty/symfony/mailer/Transport/Dsn.php @@ -0,0 +1,89 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Component\Mailer\Transport; + +use Symfony\Component\Mailer\Exception\InvalidArgumentException; + +/** + * @author Konstantin Myakshin + */ +final class Dsn +{ + private string $scheme; + private string $host; + private ?string $user; + private ?string $password; + private ?int $port; + private array $options; + + public function __construct(string $scheme, string $host, ?string $user = null, #[\SensitiveParameter] ?string $password = null, ?int $port = null, array $options = []) + { + $this->scheme = $scheme; + $this->host = $host; + $this->user = $user; + $this->password = $password; + $this->port = $port; + $this->options = $options; + } + + public static function fromString(#[\SensitiveParameter] string $dsn): self + { + if (false === $params = parse_url($dsn)) { + throw new InvalidArgumentException('The mailer DSN is invalid.'); + } + + if (!isset($params['scheme'])) { + throw new InvalidArgumentException('The mailer DSN must contain a scheme.'); + } + + if (!isset($params['host'])) { + throw new InvalidArgumentException('The mailer DSN must contain a host (use "default" by default).'); + } + + $user = '' !== ($params['user'] ?? '') ? rawurldecode($params['user']) : null; + $password = '' !== ($params['pass'] ?? '') ? rawurldecode($params['pass']) : null; + $port = $params['port'] ?? null; + parse_str($params['query'] ?? '', $query); + + return new self($params['scheme'], $params['host'], $user, $password, $port, $query); + } + + public function getScheme(): string + { + return $this->scheme; + } + + public function getHost(): string + { + return $this->host; + } + + public function getUser(): ?string + { + return $this->user; + } + + public function getPassword(): ?string + { + return $this->password; + } + + public function getPort(?int $default = null): ?int + { + return $this->port ?? $default; + } + + public function getOption(string $key, mixed $default = null): mixed + { + return $this->options[$key] ?? $default; + } +} diff --git a/3rdparty/symfony/mailer/Transport/FailoverTransport.php b/3rdparty/symfony/mailer/Transport/FailoverTransport.php new file mode 100644 index 00000000..d6a74773 --- /dev/null +++ b/3rdparty/symfony/mailer/Transport/FailoverTransport.php @@ -0,0 +1,41 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Component\Mailer\Transport; + +/** + * Uses several Transports using a failover algorithm. + * + * @author Fabien Potencier + */ +class FailoverTransport extends RoundRobinTransport +{ + private ?TransportInterface $currentTransport = null; + + protected function getNextTransport(): ?TransportInterface + { + if (null === $this->currentTransport || $this->isTransportDead($this->currentTransport)) { + $this->currentTransport = parent::getNextTransport(); + } + + return $this->currentTransport; + } + + protected function getInitialCursor(): int + { + return 0; + } + + protected function getNameSymbol(): string + { + return 'failover'; + } +} diff --git a/3rdparty/symfony/mailer/Transport/NativeTransportFactory.php b/3rdparty/symfony/mailer/Transport/NativeTransportFactory.php new file mode 100644 index 00000000..8afa53cc --- /dev/null +++ b/3rdparty/symfony/mailer/Transport/NativeTransportFactory.php @@ -0,0 +1,63 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Component\Mailer\Transport; + +use Symfony\Component\Mailer\Exception\TransportException; +use Symfony\Component\Mailer\Exception\UnsupportedSchemeException; +use Symfony\Component\Mailer\Transport\Smtp\SmtpTransport; +use Symfony\Component\Mailer\Transport\Smtp\Stream\SocketStream; + +/** + * Factory that configures a transport (sendmail or SMTP) based on php.ini settings. + * + * @author Laurent VOULLEMIER + */ +final class NativeTransportFactory extends AbstractTransportFactory +{ + public function create(Dsn $dsn): TransportInterface + { + if (!\in_array($dsn->getScheme(), $this->getSupportedSchemes(), true)) { + throw new UnsupportedSchemeException($dsn, 'native', $this->getSupportedSchemes()); + } + + if ($sendMailPath = ini_get('sendmail_path')) { + return new SendmailTransport($sendMailPath, $this->dispatcher, $this->logger); + } + + if ('\\' !== \DIRECTORY_SEPARATOR) { + throw new TransportException('sendmail_path is not configured in php.ini.'); + } + + // Only for windows hosts; at this point non-windows + // host have already thrown an exception or returned a transport + $host = ini_get('SMTP'); + $port = (int) ini_get('smtp_port'); + + if (!$host || !$port) { + throw new TransportException('smtp or smtp_port is not configured in php.ini.'); + } + + $socketStream = new SocketStream(); + $socketStream->setHost($host); + $socketStream->setPort($port); + if (465 !== $port) { + $socketStream->disableTls(); + } + + return new SmtpTransport($socketStream, $this->dispatcher, $this->logger); + } + + protected function getSupportedSchemes(): array + { + return ['native']; + } +} diff --git a/3rdparty/symfony/mailer/Transport/NullTransport.php b/3rdparty/symfony/mailer/Transport/NullTransport.php new file mode 100644 index 00000000..92fb82a4 --- /dev/null +++ b/3rdparty/symfony/mailer/Transport/NullTransport.php @@ -0,0 +1,31 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Component\Mailer\Transport; + +use Symfony\Component\Mailer\SentMessage; + +/** + * Pretends messages have been sent, but just ignores them. + * + * @author Fabien Potencier + */ +final class NullTransport extends AbstractTransport +{ + protected function doSend(SentMessage $message): void + { + } + + public function __toString(): string + { + return 'null://'; + } +} diff --git a/3rdparty/symfony/mailer/Transport/NullTransportFactory.php b/3rdparty/symfony/mailer/Transport/NullTransportFactory.php new file mode 100644 index 00000000..4c45f39e --- /dev/null +++ b/3rdparty/symfony/mailer/Transport/NullTransportFactory.php @@ -0,0 +1,34 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Component\Mailer\Transport; + +use Symfony\Component\Mailer\Exception\UnsupportedSchemeException; + +/** + * @author Konstantin Myakshin + */ +final class NullTransportFactory extends AbstractTransportFactory +{ + public function create(Dsn $dsn): TransportInterface + { + if ('null' === $dsn->getScheme()) { + return new NullTransport($this->dispatcher, $this->logger); + } + + throw new UnsupportedSchemeException($dsn, 'null', $this->getSupportedSchemes()); + } + + protected function getSupportedSchemes(): array + { + return ['null']; + } +} diff --git a/3rdparty/symfony/mailer/Transport/RoundRobinTransport.php b/3rdparty/symfony/mailer/Transport/RoundRobinTransport.php new file mode 100644 index 00000000..ac9709bf --- /dev/null +++ b/3rdparty/symfony/mailer/Transport/RoundRobinTransport.php @@ -0,0 +1,125 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Component\Mailer\Transport; + +use Symfony\Component\Mailer\Envelope; +use Symfony\Component\Mailer\Exception\TransportException; +use Symfony\Component\Mailer\Exception\TransportExceptionInterface; +use Symfony\Component\Mailer\SentMessage; +use Symfony\Component\Mime\RawMessage; + +/** + * Uses several Transports using a round robin algorithm. + * + * @author Fabien Potencier + */ +class RoundRobinTransport implements TransportInterface +{ + /** + * @var \SplObjectStorage + */ + private \SplObjectStorage $deadTransports; + private array $transports = []; + private int $retryPeriod; + private int $cursor = -1; + + /** + * @param TransportInterface[] $transports + */ + public function __construct(array $transports, int $retryPeriod = 60) + { + if (!$transports) { + throw new TransportException(sprintf('"%s" must have at least one transport configured.', static::class)); + } + + $this->transports = $transports; + $this->deadTransports = new \SplObjectStorage(); + $this->retryPeriod = $retryPeriod; + } + + public function send(RawMessage $message, ?Envelope $envelope = null): ?SentMessage + { + $exception = null; + + while ($transport = $this->getNextTransport()) { + try { + return $transport->send($message, $envelope); + } catch (TransportExceptionInterface $e) { + $exception ??= new TransportException('All transports failed.'); + $exception->appendDebug(sprintf("Transport \"%s\": %s\n", $transport, $e->getDebug())); + $this->deadTransports[$transport] = microtime(true); + } + } + + throw $exception ?? new TransportException('No transports found.'); + } + + public function __toString(): string + { + return $this->getNameSymbol().'('.implode(' ', array_map('strval', $this->transports)).')'; + } + + /** + * Rotates the transport list around and returns the first instance. + */ + protected function getNextTransport(): ?TransportInterface + { + if (-1 === $this->cursor) { + $this->cursor = $this->getInitialCursor(); + } + + $cursor = $this->cursor; + while (true) { + $transport = $this->transports[$cursor]; + + if (!$this->isTransportDead($transport)) { + break; + } + + if ((microtime(true) - $this->deadTransports[$transport]) > $this->retryPeriod) { + $this->deadTransports->detach($transport); + + break; + } + + if ($this->cursor === $cursor = $this->moveCursor($cursor)) { + return null; + } + } + + $this->cursor = $this->moveCursor($cursor); + + return $transport; + } + + protected function isTransportDead(TransportInterface $transport): bool + { + return $this->deadTransports->contains($transport); + } + + protected function getInitialCursor(): int + { + // the cursor initial value is randomized so that + // when are not in a daemon, we are still rotating the transports + return mt_rand(0, \count($this->transports) - 1); + } + + protected function getNameSymbol(): string + { + return 'roundrobin'; + } + + private function moveCursor(int $cursor): int + { + return ++$cursor >= \count($this->transports) ? 0 : $cursor; + } +} diff --git a/3rdparty/symfony/mailer/Transport/SendmailTransport.php b/3rdparty/symfony/mailer/Transport/SendmailTransport.php new file mode 100644 index 00000000..3add460e --- /dev/null +++ b/3rdparty/symfony/mailer/Transport/SendmailTransport.php @@ -0,0 +1,124 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Component\Mailer\Transport; + +use Psr\EventDispatcher\EventDispatcherInterface; +use Psr\Log\LoggerInterface; +use Symfony\Component\Mailer\Envelope; +use Symfony\Component\Mailer\SentMessage; +use Symfony\Component\Mailer\Transport\Smtp\SmtpTransport; +use Symfony\Component\Mailer\Transport\Smtp\Stream\AbstractStream; +use Symfony\Component\Mailer\Transport\Smtp\Stream\ProcessStream; +use Symfony\Component\Mime\RawMessage; + +/** + * SendmailTransport for sending mail through a Sendmail/Postfix (etc..) binary. + * + * Transport can be instantiated through SendmailTransportFactory or NativeTransportFactory: + * + * - SendmailTransportFactory to use most common sendmail path and recommended options + * - NativeTransportFactory when configuration is set via php.ini + * + * @author Fabien Potencier + * @author Chris Corbyn + */ +class SendmailTransport extends AbstractTransport +{ + private string $command = '/usr/sbin/sendmail -bs'; + private ProcessStream $stream; + private ?SmtpTransport $transport = null; + + /** + * Constructor. + * + * Supported modes are -bs and -t, with any additional flags desired. + * + * The recommended mode is "-bs" since it is interactive and failure notifications are hence possible. + * Note that the -t mode does not support error reporting and does not support Bcc properly (the Bcc headers are not removed). + * + * If using -t mode, you are strongly advised to include -oi or -i in the flags (like /usr/sbin/sendmail -oi -t) + * + * -f flag will be appended automatically if one is not present. + */ + public function __construct(?string $command = null, ?EventDispatcherInterface $dispatcher = null, ?LoggerInterface $logger = null) + { + parent::__construct($dispatcher, $logger); + + if (null !== $command) { + if (!str_contains($command, ' -bs') && !str_contains($command, ' -t')) { + throw new \InvalidArgumentException(sprintf('Unsupported sendmail command flags "%s"; must be one of "-bs" or "-t" but can include additional flags.', $command)); + } + + $this->command = $command; + } + + $this->stream = new ProcessStream(); + if (str_contains($this->command, ' -bs')) { + $this->stream->setCommand($this->command); + $this->stream->setInteractive(true); + $this->transport = new SmtpTransport($this->stream, $dispatcher, $logger); + } + } + + public function send(RawMessage $message, ?Envelope $envelope = null): ?SentMessage + { + if ($this->transport) { + return $this->transport->send($message, $envelope); + } + + return parent::send($message, $envelope); + } + + public function __toString(): string + { + if ($this->transport) { + return (string) $this->transport; + } + + return 'smtp://sendmail'; + } + + protected function doSend(SentMessage $message): void + { + $this->getLogger()->debug(sprintf('Email transport "%s" starting', __CLASS__)); + + $command = $this->command; + + if ($recipients = $message->getEnvelope()->getRecipients()) { + $command = str_replace(' -t', '', $command); + } + + if (!str_contains($command, ' -f')) { + $command .= ' -f'.escapeshellarg($message->getEnvelope()->getSender()->getEncodedAddress()); + } + + $chunks = AbstractStream::replace("\r\n", "\n", $message->toIterable()); + + if (!str_contains($command, ' -i') && !str_contains($command, ' -oi')) { + $chunks = AbstractStream::replace("\n.", "\n..", $chunks); + } + + foreach ($recipients as $recipient) { + $command .= ' '.escapeshellarg($recipient->getEncodedAddress()); + } + + $this->stream->setCommand($command); + $this->stream->initialize(); + foreach ($chunks as $chunk) { + $this->stream->write($chunk); + } + $this->stream->flush(); + $this->stream->terminate(); + + $this->getLogger()->debug(sprintf('Email transport "%s" stopped', __CLASS__)); + } +} diff --git a/3rdparty/symfony/mailer/Transport/SendmailTransportFactory.php b/3rdparty/symfony/mailer/Transport/SendmailTransportFactory.php new file mode 100644 index 00000000..6d977e74 --- /dev/null +++ b/3rdparty/symfony/mailer/Transport/SendmailTransportFactory.php @@ -0,0 +1,34 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Component\Mailer\Transport; + +use Symfony\Component\Mailer\Exception\UnsupportedSchemeException; + +/** + * @author Konstantin Myakshin + */ +final class SendmailTransportFactory extends AbstractTransportFactory +{ + public function create(Dsn $dsn): TransportInterface + { + if ('sendmail+smtp' === $dsn->getScheme() || 'sendmail' === $dsn->getScheme()) { + return new SendmailTransport($dsn->getOption('command'), $this->dispatcher, $this->logger); + } + + throw new UnsupportedSchemeException($dsn, 'sendmail', $this->getSupportedSchemes()); + } + + protected function getSupportedSchemes(): array + { + return ['sendmail', 'sendmail+smtp']; + } +} diff --git a/3rdparty/symfony/mailer/Transport/Smtp/Auth/AuthenticatorInterface.php b/3rdparty/symfony/mailer/Transport/Smtp/Auth/AuthenticatorInterface.php new file mode 100644 index 00000000..98ea2d44 --- /dev/null +++ b/3rdparty/symfony/mailer/Transport/Smtp/Auth/AuthenticatorInterface.php @@ -0,0 +1,35 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Component\Mailer\Transport\Smtp\Auth; + +use Symfony\Component\Mailer\Exception\TransportExceptionInterface; +use Symfony\Component\Mailer\Transport\Smtp\EsmtpTransport; + +/** + * An Authentication mechanism. + * + * @author Chris Corbyn + */ +interface AuthenticatorInterface +{ + /** + * Tries to authenticate the user. + * + * @throws TransportExceptionInterface + */ + public function authenticate(EsmtpTransport $client): void; + + /** + * Gets the name of the AUTH mechanism this Authenticator handles. + */ + public function getAuthKeyword(): string; +} diff --git a/3rdparty/symfony/mailer/Transport/Smtp/Auth/CramMd5Authenticator.php b/3rdparty/symfony/mailer/Transport/Smtp/Auth/CramMd5Authenticator.php new file mode 100644 index 00000000..79cddc46 --- /dev/null +++ b/3rdparty/symfony/mailer/Transport/Smtp/Auth/CramMd5Authenticator.php @@ -0,0 +1,65 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Component\Mailer\Transport\Smtp\Auth; + +use Symfony\Component\Mailer\Exception\InvalidArgumentException; +use Symfony\Component\Mailer\Transport\Smtp\EsmtpTransport; + +/** + * Handles CRAM-MD5 authentication. + * + * @author Chris Corbyn + */ +class CramMd5Authenticator implements AuthenticatorInterface +{ + public function getAuthKeyword(): string + { + return 'CRAM-MD5'; + } + + /** + * @see https://www.ietf.org/rfc/rfc4954.txt + */ + public function authenticate(EsmtpTransport $client): void + { + $challenge = $client->executeCommand("AUTH CRAM-MD5\r\n", [334]); + $challenge = base64_decode(substr($challenge, 4)); + $message = base64_encode($client->getUsername().' '.$this->getResponse($client->getPassword(), $challenge)); + $client->executeCommand(sprintf("%s\r\n", $message), [235]); + } + + /** + * Generates a CRAM-MD5 response from a server challenge. + */ + private function getResponse(#[\SensitiveParameter] string $secret, string $challenge): string + { + if (!$secret) { + throw new InvalidArgumentException('A non-empty secret is required.'); + } + + if (\strlen($secret) > 64) { + $secret = pack('H32', md5($secret)); + } + + if (\strlen($secret) < 64) { + $secret = str_pad($secret, 64, \chr(0)); + } + + $kipad = substr($secret, 0, 64) ^ str_repeat(\chr(0x36), 64); + $kopad = substr($secret, 0, 64) ^ str_repeat(\chr(0x5C), 64); + + $inner = pack('H32', md5($kipad.$challenge)); + $digest = md5($kopad.$inner); + + return $digest; + } +} diff --git a/3rdparty/symfony/mailer/Transport/Smtp/Auth/LoginAuthenticator.php b/3rdparty/symfony/mailer/Transport/Smtp/Auth/LoginAuthenticator.php new file mode 100644 index 00000000..e0b7d577 --- /dev/null +++ b/3rdparty/symfony/mailer/Transport/Smtp/Auth/LoginAuthenticator.php @@ -0,0 +1,37 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Component\Mailer\Transport\Smtp\Auth; + +use Symfony\Component\Mailer\Transport\Smtp\EsmtpTransport; + +/** + * Handles LOGIN authentication. + * + * @author Chris Corbyn + */ +class LoginAuthenticator implements AuthenticatorInterface +{ + public function getAuthKeyword(): string + { + return 'LOGIN'; + } + + /** + * @see https://www.ietf.org/rfc/rfc4954.txt + */ + public function authenticate(EsmtpTransport $client): void + { + $client->executeCommand("AUTH LOGIN\r\n", [334]); + $client->executeCommand(sprintf("%s\r\n", base64_encode($client->getUsername())), [334]); + $client->executeCommand(sprintf("%s\r\n", base64_encode($client->getPassword())), [235]); + } +} diff --git a/3rdparty/symfony/mailer/Transport/Smtp/Auth/PlainAuthenticator.php b/3rdparty/symfony/mailer/Transport/Smtp/Auth/PlainAuthenticator.php new file mode 100644 index 00000000..6b680b51 --- /dev/null +++ b/3rdparty/symfony/mailer/Transport/Smtp/Auth/PlainAuthenticator.php @@ -0,0 +1,35 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Component\Mailer\Transport\Smtp\Auth; + +use Symfony\Component\Mailer\Transport\Smtp\EsmtpTransport; + +/** + * Handles PLAIN authentication. + * + * @author Chris Corbyn + */ +class PlainAuthenticator implements AuthenticatorInterface +{ + public function getAuthKeyword(): string + { + return 'PLAIN'; + } + + /** + * @see https://www.ietf.org/rfc/rfc4954.txt + */ + public function authenticate(EsmtpTransport $client): void + { + $client->executeCommand(sprintf("AUTH PLAIN %s\r\n", base64_encode($client->getUsername().\chr(0).$client->getUsername().\chr(0).$client->getPassword())), [235]); + } +} diff --git a/3rdparty/symfony/mailer/Transport/Smtp/Auth/XOAuth2Authenticator.php b/3rdparty/symfony/mailer/Transport/Smtp/Auth/XOAuth2Authenticator.php new file mode 100644 index 00000000..c3aaa490 --- /dev/null +++ b/3rdparty/symfony/mailer/Transport/Smtp/Auth/XOAuth2Authenticator.php @@ -0,0 +1,37 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Component\Mailer\Transport\Smtp\Auth; + +use Symfony\Component\Mailer\Transport\Smtp\EsmtpTransport; + +/** + * Handles XOAUTH2 authentication. + * + * @author xu.li + * + * @see https://developers.google.com/google-apps/gmail/xoauth2_protocol + */ +class XOAuth2Authenticator implements AuthenticatorInterface +{ + public function getAuthKeyword(): string + { + return 'XOAUTH2'; + } + + /** + * @see https://developers.google.com/google-apps/gmail/xoauth2_protocol#the_sasl_xoauth2_mechanism + */ + public function authenticate(EsmtpTransport $client): void + { + $client->executeCommand('AUTH XOAUTH2 '.base64_encode('user='.$client->getUsername()."\1auth=Bearer ".$client->getPassword()."\1\1")."\r\n", [235]); + } +} diff --git a/3rdparty/symfony/mailer/Transport/Smtp/EsmtpTransport.php b/3rdparty/symfony/mailer/Transport/Smtp/EsmtpTransport.php new file mode 100644 index 00000000..f5edfe11 --- /dev/null +++ b/3rdparty/symfony/mailer/Transport/Smtp/EsmtpTransport.php @@ -0,0 +1,228 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Component\Mailer\Transport\Smtp; + +use Psr\EventDispatcher\EventDispatcherInterface; +use Psr\Log\LoggerInterface; +use Symfony\Component\Mailer\Exception\TransportException; +use Symfony\Component\Mailer\Exception\TransportExceptionInterface; +use Symfony\Component\Mailer\Exception\UnexpectedResponseException; +use Symfony\Component\Mailer\Transport\Smtp\Auth\AuthenticatorInterface; +use Symfony\Component\Mailer\Transport\Smtp\Stream\AbstractStream; +use Symfony\Component\Mailer\Transport\Smtp\Stream\SocketStream; + +/** + * Sends Emails over SMTP with ESMTP support. + * + * @author Fabien Potencier + * @author Chris Corbyn + */ +class EsmtpTransport extends SmtpTransport +{ + private array $authenticators = []; + private string $username = ''; + private string $password = ''; + private array $capabilities; + + public function __construct(string $host = 'localhost', int $port = 0, ?bool $tls = null, ?EventDispatcherInterface $dispatcher = null, ?LoggerInterface $logger = null, ?AbstractStream $stream = null, ?array $authenticators = null) + { + parent::__construct($stream, $dispatcher, $logger); + + if (null === $authenticators) { + // fallback to default authenticators + // order is important here (roughly most secure and popular first) + $authenticators = [ + new Auth\CramMd5Authenticator(), + new Auth\LoginAuthenticator(), + new Auth\PlainAuthenticator(), + new Auth\XOAuth2Authenticator(), + ]; + } + $this->setAuthenticators($authenticators); + + /** @var SocketStream $stream */ + $stream = $this->getStream(); + + if (null === $tls) { + if (465 === $port) { + $tls = true; + } else { + $tls = \defined('OPENSSL_VERSION_NUMBER') && 0 === $port && 'localhost' !== $host; + } + } + if (!$tls) { + $stream->disableTls(); + } + if (0 === $port) { + $port = $tls ? 465 : 25; + } + + $stream->setHost($host); + $stream->setPort($port); + } + + /** + * @return $this + */ + public function setUsername(string $username): static + { + $this->username = $username; + + return $this; + } + + public function getUsername(): string + { + return $this->username; + } + + /** + * @return $this + */ + public function setPassword(#[\SensitiveParameter] string $password): static + { + $this->password = $password; + + return $this; + } + + public function getPassword(): string + { + return $this->password; + } + + public function setAuthenticators(array $authenticators): void + { + $this->authenticators = []; + foreach ($authenticators as $authenticator) { + $this->addAuthenticator($authenticator); + } + } + + public function addAuthenticator(AuthenticatorInterface $authenticator): void + { + $this->authenticators[] = $authenticator; + } + + public function executeCommand(string $command, array $codes): string + { + return [250] === $codes && str_starts_with($command, 'HELO ') ? $this->doEhloCommand() : parent::executeCommand($command, $codes); + } + + final protected function getCapabilities(): array + { + return $this->capabilities; + } + + private function doEhloCommand(): string + { + try { + $response = $this->executeCommand(sprintf("EHLO %s\r\n", $this->getLocalDomain()), [250]); + } catch (TransportExceptionInterface $e) { + try { + return parent::executeCommand(sprintf("HELO %s\r\n", $this->getLocalDomain()), [250]); + } catch (TransportExceptionInterface $ex) { + if (!$ex->getCode()) { + throw $e; + } + + throw $ex; + } + } + + $this->capabilities = $this->parseCapabilities($response); + + /** @var SocketStream $stream */ + $stream = $this->getStream(); + // WARNING: !$stream->isTLS() is right, 100% sure :) + // if you think that the ! should be removed, read the code again + // if doing so "fixes" your issue then it probably means your SMTP server behaves incorrectly or is wrongly configured + if (!$stream->isTLS() && \defined('OPENSSL_VERSION_NUMBER') && \array_key_exists('STARTTLS', $this->capabilities)) { + $this->executeCommand("STARTTLS\r\n", [220]); + + if (!$stream->startTLS()) { + throw new TransportException('Unable to connect with STARTTLS.'); + } + + $response = $this->executeCommand(sprintf("EHLO %s\r\n", $this->getLocalDomain()), [250]); + $this->capabilities = $this->parseCapabilities($response); + } + + if (\array_key_exists('AUTH', $this->capabilities)) { + $this->handleAuth($this->capabilities['AUTH']); + } + + return $response; + } + + private function parseCapabilities(string $ehloResponse): array + { + $capabilities = []; + $lines = explode("\r\n", trim($ehloResponse)); + array_shift($lines); + foreach ($lines as $line) { + if (preg_match('/^[0-9]{3}[ -]([A-Z0-9-]+)((?:[ =].*)?)$/Di', $line, $matches)) { + $value = strtoupper(ltrim($matches[2], ' =')); + $capabilities[strtoupper($matches[1])] = $value ? explode(' ', $value) : []; + } + } + + return $capabilities; + } + + private function handleAuth(array $modes): void + { + if (!$this->username) { + return; + } + + $code = null; + $authNames = []; + $errors = []; + $modes = array_map('strtolower', $modes); + foreach ($this->authenticators as $authenticator) { + if (!\in_array(strtolower($authenticator->getAuthKeyword()), $modes, true)) { + continue; + } + + $code = null; + $authNames[] = $authenticator->getAuthKeyword(); + try { + $authenticator->authenticate($this); + + return; + } catch (UnexpectedResponseException $e) { + $code = $e->getCode(); + + try { + $this->executeCommand("RSET\r\n", [250]); + } catch (TransportExceptionInterface) { + // ignore this exception as it probably means that the server error was final + } + + // keep the error message, but tries the other authenticators + $errors[$authenticator->getAuthKeyword()] = $e->getMessage(); + } + } + + if (!$authNames) { + throw new TransportException(sprintf('Failed to find an authenticator supported by the SMTP server, which currently supports: "%s".', implode('", "', $modes)), $code ?: 504); + } + + $message = sprintf('Failed to authenticate on SMTP server with username "%s" using the following authenticators: "%s".', $this->username, implode('", "', $authNames)); + foreach ($errors as $name => $error) { + $message .= sprintf(' Authenticator "%s" returned "%s".', $name, $error); + } + + throw new TransportException($message, $code ?: 535); + } +} diff --git a/3rdparty/symfony/mailer/Transport/Smtp/EsmtpTransportFactory.php b/3rdparty/symfony/mailer/Transport/Smtp/EsmtpTransportFactory.php new file mode 100644 index 00000000..a15d1224 --- /dev/null +++ b/3rdparty/symfony/mailer/Transport/Smtp/EsmtpTransportFactory.php @@ -0,0 +1,78 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Component\Mailer\Transport\Smtp; + +use Symfony\Component\Mailer\Transport\AbstractTransportFactory; +use Symfony\Component\Mailer\Transport\Dsn; +use Symfony\Component\Mailer\Transport\Smtp\Stream\SocketStream; +use Symfony\Component\Mailer\Transport\TransportInterface; + +/** + * @author Konstantin Myakshin + */ +final class EsmtpTransportFactory extends AbstractTransportFactory +{ + public function create(Dsn $dsn): TransportInterface + { + $tls = 'smtps' === $dsn->getScheme() ? true : null; + $port = $dsn->getPort(0); + $host = $dsn->getHost(); + + $transport = new EsmtpTransport($host, $port, $tls, $this->dispatcher, $this->logger); + + /** @var SocketStream $stream */ + $stream = $transport->getStream(); + $streamOptions = $stream->getStreamOptions(); + + if ('' !== $dsn->getOption('verify_peer') && !filter_var($dsn->getOption('verify_peer', true), \FILTER_VALIDATE_BOOL)) { + $streamOptions['ssl']['verify_peer'] = false; + $streamOptions['ssl']['verify_peer_name'] = false; + } + + if (null !== $peerFingerprint = $dsn->getOption('peer_fingerprint')) { + $streamOptions['ssl']['peer_fingerprint'] = $peerFingerprint; + } + + $stream->setStreamOptions($streamOptions); + + if ($user = $dsn->getUser()) { + $transport->setUsername($user); + } + + if ($password = $dsn->getPassword()) { + $transport->setPassword($password); + } + + if (null !== ($localDomain = $dsn->getOption('local_domain'))) { + $transport->setLocalDomain($localDomain); + } + + if (null !== ($maxPerSecond = $dsn->getOption('max_per_second'))) { + $transport->setMaxPerSecond((float) $maxPerSecond); + } + + if (null !== ($restartThreshold = $dsn->getOption('restart_threshold'))) { + $transport->setRestartThreshold((int) $restartThreshold, (int) $dsn->getOption('restart_threshold_sleep', 0)); + } + + if (null !== ($pingThreshold = $dsn->getOption('ping_threshold'))) { + $transport->setPingThreshold((int) $pingThreshold); + } + + return $transport; + } + + protected function getSupportedSchemes(): array + { + return ['smtp', 'smtps']; + } +} diff --git a/3rdparty/symfony/mailer/Transport/Smtp/SmtpTransport.php b/3rdparty/symfony/mailer/Transport/Smtp/SmtpTransport.php new file mode 100644 index 00000000..0de38fb2 --- /dev/null +++ b/3rdparty/symfony/mailer/Transport/Smtp/SmtpTransport.php @@ -0,0 +1,392 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Component\Mailer\Transport\Smtp; + +use Psr\EventDispatcher\EventDispatcherInterface; +use Psr\Log\LoggerInterface; +use Symfony\Component\Mailer\Envelope; +use Symfony\Component\Mailer\Exception\LogicException; +use Symfony\Component\Mailer\Exception\TransportException; +use Symfony\Component\Mailer\Exception\TransportExceptionInterface; +use Symfony\Component\Mailer\Exception\UnexpectedResponseException; +use Symfony\Component\Mailer\SentMessage; +use Symfony\Component\Mailer\Transport\AbstractTransport; +use Symfony\Component\Mailer\Transport\Smtp\Stream\AbstractStream; +use Symfony\Component\Mailer\Transport\Smtp\Stream\SocketStream; +use Symfony\Component\Mime\RawMessage; + +/** + * Sends emails over SMTP. + * + * @author Fabien Potencier + * @author Chris Corbyn + */ +class SmtpTransport extends AbstractTransport +{ + private bool $started = false; + private int $restartThreshold = 100; + private int $restartThresholdSleep = 0; + private int $restartCounter = 0; + private int $pingThreshold = 100; + private float $lastMessageTime = 0; + private AbstractStream $stream; + private string $domain = '[127.0.0.1]'; + + public function __construct(?AbstractStream $stream = null, ?EventDispatcherInterface $dispatcher = null, ?LoggerInterface $logger = null) + { + parent::__construct($dispatcher, $logger); + + $this->stream = $stream ?? new SocketStream(); + } + + public function getStream(): AbstractStream + { + return $this->stream; + } + + /** + * Sets the maximum number of messages to send before re-starting the transport. + * + * By default, the threshold is set to 100 (and no sleep at restart). + * + * @param int $threshold The maximum number of messages (0 to disable) + * @param int $sleep The number of seconds to sleep between stopping and re-starting the transport + * + * @return $this + */ + public function setRestartThreshold(int $threshold, int $sleep = 0): static + { + $this->restartThreshold = $threshold; + $this->restartThresholdSleep = $sleep; + + return $this; + } + + /** + * Sets the minimum number of seconds required between two messages, before the server is pinged. + * If the transport wants to send a message and the time since the last message exceeds the specified threshold, + * the transport will ping the server first (NOOP command) to check if the connection is still alive. + * Otherwise the message will be sent without pinging the server first. + * + * Do not set the threshold too low, as the SMTP server may drop the connection if there are too many + * non-mail commands (like pinging the server with NOOP). + * + * By default, the threshold is set to 100 seconds. + * + * @param int $seconds The minimum number of seconds between two messages required to ping the server + * + * @return $this + */ + public function setPingThreshold(int $seconds): static + { + $this->pingThreshold = $seconds; + + return $this; + } + + /** + * Sets the name of the local domain that will be used in HELO. + * + * This should be a fully-qualified domain name and should be truly the domain + * you're using. + * + * If your server does not have a domain name, use the IP address. This will + * automatically be wrapped in square brackets as described in RFC 5321, + * section 4.1.3. + * + * @return $this + */ + public function setLocalDomain(string $domain): static + { + if ('' !== $domain && '[' !== $domain[0]) { + if (filter_var($domain, \FILTER_VALIDATE_IP, \FILTER_FLAG_IPV4)) { + $domain = '['.$domain.']'; + } elseif (filter_var($domain, \FILTER_VALIDATE_IP, \FILTER_FLAG_IPV6)) { + $domain = '[IPv6:'.$domain.']'; + } + } + + $this->domain = $domain; + + return $this; + } + + /** + * Gets the name of the domain that will be used in HELO. + * + * If an IP address was specified, this will be returned wrapped in square + * brackets as described in RFC 5321, section 4.1.3. + */ + public function getLocalDomain(): string + { + return $this->domain; + } + + public function send(RawMessage $message, ?Envelope $envelope = null): ?SentMessage + { + try { + $message = parent::send($message, $envelope); + } catch (TransportExceptionInterface $e) { + if ($this->started) { + try { + $this->executeCommand("RSET\r\n", [250]); + } catch (TransportExceptionInterface) { + // ignore this exception as it probably means that the server error was final + } + } + + throw $e; + } + + $this->checkRestartThreshold(); + + return $message; + } + + protected function parseMessageId(string $mtaResult): string + { + $regexps = [ + '/250 Ok (?P[0-9a-f-]+)\r?$/mis', + '/250 Ok:? queued as (?P[A-Z0-9]+)\r?$/mis', + ]; + $matches = []; + foreach ($regexps as $regexp) { + if (preg_match($regexp, $mtaResult, $matches)) { + return $matches['id']; + } + } + + return ''; + } + + public function __toString(): string + { + if ($this->stream instanceof SocketStream) { + $name = sprintf('smtp%s://%s', ($tls = $this->stream->isTLS()) ? 's' : '', $this->stream->getHost()); + $port = $this->stream->getPort(); + if (!(25 === $port || ($tls && 465 === $port))) { + $name .= ':'.$port; + } + + return $name; + } + + return 'smtp://sendmail'; + } + + /** + * Runs a command against the stream, expecting the given response codes. + * + * @param int[] $codes + * + * @throws TransportException when an invalid response if received + */ + public function executeCommand(string $command, array $codes): string + { + $this->stream->write($command); + $response = $this->getFullResponse(); + $this->assertResponseCode($response, $codes); + + return $response; + } + + protected function doSend(SentMessage $message): void + { + if (microtime(true) - $this->lastMessageTime > $this->pingThreshold) { + $this->ping(); + } + + if (!$this->started) { + $this->start(); + } + + try { + $envelope = $message->getEnvelope(); + $this->doMailFromCommand($envelope->getSender()->getEncodedAddress()); + foreach ($envelope->getRecipients() as $recipient) { + $this->doRcptToCommand($recipient->getEncodedAddress()); + } + + $this->executeCommand("DATA\r\n", [354]); + try { + foreach (AbstractStream::replace("\r\n.", "\r\n..", $message->toIterable()) as $chunk) { + $this->stream->write($chunk, false); + } + $this->stream->flush(); + } catch (TransportExceptionInterface $e) { + throw $e; + } catch (\Exception $e) { + $this->stream->terminate(); + $this->started = false; + $this->getLogger()->debug(sprintf('Email transport "%s" stopped', __CLASS__)); + throw $e; + } + $mtaResult = $this->executeCommand("\r\n.\r\n", [250]); + $message->appendDebug($this->stream->getDebug()); + $this->lastMessageTime = microtime(true); + + if ($mtaResult && $messageId = $this->parseMessageId($mtaResult)) { + $message->setMessageId($messageId); + } + } catch (TransportExceptionInterface $e) { + $e->appendDebug($this->stream->getDebug()); + $this->lastMessageTime = 0; + throw $e; + } + } + + /** + * @internal since version 6.1, to be made private in 7.0 + * + * @final since version 6.1, to be made private in 7.0 + */ + protected function doHeloCommand(): void + { + $this->executeCommand(sprintf("HELO %s\r\n", $this->domain), [250]); + } + + private function doMailFromCommand(string $address): void + { + $this->executeCommand(sprintf("MAIL FROM:<%s>\r\n", $address), [250]); + } + + private function doRcptToCommand(string $address): void + { + $this->executeCommand(sprintf("RCPT TO:<%s>\r\n", $address), [250, 251, 252]); + } + + public function start(): void + { + if ($this->started) { + return; + } + + $this->getLogger()->debug(sprintf('Email transport "%s" starting', __CLASS__)); + + $this->stream->initialize(); + $this->assertResponseCode($this->getFullResponse(), [220]); + $this->doHeloCommand(); + $this->started = true; + $this->lastMessageTime = 0; + + $this->getLogger()->debug(sprintf('Email transport "%s" started', __CLASS__)); + } + + /** + * Manually disconnect from the SMTP server. + * + * In most cases this is not necessary since the disconnect happens automatically on termination. + * In cases of long-running scripts, this might however make sense to avoid keeping an open + * connection to the SMTP server in between sending emails. + */ + public function stop(): void + { + if (!$this->started) { + return; + } + + $this->getLogger()->debug(sprintf('Email transport "%s" stopping', __CLASS__)); + + try { + $this->executeCommand("QUIT\r\n", [221]); + } catch (TransportExceptionInterface) { + } finally { + $this->stream->terminate(); + $this->started = false; + $this->getLogger()->debug(sprintf('Email transport "%s" stopped', __CLASS__)); + } + } + + private function ping(): void + { + if (!$this->started) { + return; + } + + try { + $this->executeCommand("NOOP\r\n", [250]); + } catch (TransportExceptionInterface) { + $this->stop(); + } + } + + /** + * @throws TransportException if a response code is incorrect + */ + private function assertResponseCode(string $response, array $codes): void + { + if (!$codes) { + throw new LogicException('You must set the expected response code.'); + } + + [$code] = sscanf($response, '%3d'); + $valid = \in_array($code, $codes); + + if (!$valid || !$response) { + $codeStr = $code ? sprintf('code "%s"', $code) : 'empty code'; + $responseStr = $response ? sprintf(', with message "%s"', trim($response)) : ''; + + throw new UnexpectedResponseException(sprintf('Expected response code "%s" but got ', implode('/', $codes)).$codeStr.$responseStr.'.', $code ?: 0); + } + } + + private function getFullResponse(): string + { + $response = ''; + do { + $line = $this->stream->readLine(); + $response .= $line; + } while ($line && isset($line[3]) && ' ' !== $line[3]); + + return $response; + } + + private function checkRestartThreshold(): void + { + // when using sendmail via non-interactive mode, the transport is never "started" + if (!$this->started) { + return; + } + + ++$this->restartCounter; + if ($this->restartCounter < $this->restartThreshold) { + return; + } + + $this->stop(); + if (0 < $sleep = $this->restartThresholdSleep) { + $this->getLogger()->debug(sprintf('Email transport "%s" sleeps for %d seconds after stopping', __CLASS__, $sleep)); + + sleep($sleep); + } + $this->start(); + $this->restartCounter = 0; + } + + public function __sleep(): array + { + throw new \BadMethodCallException('Cannot serialize '.__CLASS__); + } + + /** + * @return void + */ + public function __wakeup() + { + throw new \BadMethodCallException('Cannot unserialize '.__CLASS__); + } + + public function __destruct() + { + $this->stop(); + } +} diff --git a/3rdparty/symfony/mailer/Transport/Smtp/Stream/AbstractStream.php b/3rdparty/symfony/mailer/Transport/Smtp/Stream/AbstractStream.php new file mode 100644 index 00000000..498dc560 --- /dev/null +++ b/3rdparty/symfony/mailer/Transport/Smtp/Stream/AbstractStream.php @@ -0,0 +1,145 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Component\Mailer\Transport\Smtp\Stream; + +use Symfony\Component\Mailer\Exception\TransportException; + +/** + * A stream supporting remote sockets and local processes. + * + * @author Fabien Potencier + * @author Nicolas Grekas + * @author Chris Corbyn + * + * @internal + */ +abstract class AbstractStream +{ + /** @var resource|null */ + protected $stream; + /** @var resource|null */ + protected $in; + /** @var resource|null */ + protected $out; + protected $err; + + private string $debug = ''; + + public function write(string $bytes, bool $debug = true): void + { + if ($debug) { + foreach (explode("\n", trim($bytes)) as $line) { + $this->debug .= sprintf("> %s\n", $line); + } + } + + $bytesToWrite = \strlen($bytes); + $totalBytesWritten = 0; + while ($totalBytesWritten < $bytesToWrite) { + $bytesWritten = @fwrite($this->in, substr($bytes, $totalBytesWritten)); + if (false === $bytesWritten || 0 === $bytesWritten) { + throw new TransportException('Unable to write bytes on the wire.'); + } + + $totalBytesWritten += $bytesWritten; + } + } + + /** + * Flushes the contents of the stream (empty it) and set the internal pointer to the beginning. + */ + public function flush(): void + { + fflush($this->in); + } + + /** + * Performs any initialization needed. + */ + abstract public function initialize(): void; + + public function terminate(): void + { + $this->stream = $this->err = $this->out = $this->in = null; + } + + public function readLine(): string + { + if (feof($this->out)) { + return ''; + } + + $line = @fgets($this->out); + if ('' === $line || false === $line) { + $metas = stream_get_meta_data($this->out); + if ($metas['timed_out']) { + throw new TransportException(sprintf('Connection to "%s" timed out.', $this->getReadConnectionDescription())); + } + if ($metas['eof']) { + throw new TransportException(sprintf('Connection to "%s" has been closed unexpectedly.', $this->getReadConnectionDescription())); + } + if (false === $line) { + throw new TransportException(sprintf('Unable to read from connection to "%s": ', $this->getReadConnectionDescription()).error_get_last()['message']); + } + } + + $this->debug .= sprintf('< %s', $line); + + return $line; + } + + public function getDebug(): string + { + $debug = $this->debug; + $this->debug = ''; + + return $debug; + } + + public static function replace(string $from, string $to, iterable $chunks): \Generator + { + if ('' === $from) { + yield from $chunks; + + return; + } + + $carry = ''; + $fromLen = \strlen($from); + + foreach ($chunks as $chunk) { + if ('' === $chunk = $carry.$chunk) { + continue; + } + + if (str_contains($chunk, $from)) { + $chunk = explode($from, $chunk); + $carry = array_pop($chunk); + + yield implode($to, $chunk).$to; + } else { + $carry = $chunk; + } + + if (\strlen($carry) > $fromLen) { + yield substr($carry, 0, -$fromLen); + $carry = substr($carry, -$fromLen); + } + } + + if ('' !== $carry) { + yield $carry; + } + } + + abstract protected function getReadConnectionDescription(): string; +} diff --git a/3rdparty/symfony/mailer/Transport/Smtp/Stream/ProcessStream.php b/3rdparty/symfony/mailer/Transport/Smtp/Stream/ProcessStream.php new file mode 100644 index 00000000..e6351470 --- /dev/null +++ b/3rdparty/symfony/mailer/Transport/Smtp/Stream/ProcessStream.php @@ -0,0 +1,81 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Component\Mailer\Transport\Smtp\Stream; + +use Symfony\Component\Mailer\Exception\TransportException; + +/** + * A stream supporting local processes. + * + * @author Fabien Potencier + * @author Chris Corbyn + * + * @internal + */ +final class ProcessStream extends AbstractStream +{ + private string $command; + private bool $interactive = false; + + public function setCommand(string $command): void + { + $this->command = $command; + } + + public function setInteractive(bool $interactive): void + { + $this->interactive = $interactive; + } + + public function initialize(): void + { + $descriptorSpec = [ + 0 => ['pipe', 'r'], + 1 => ['pipe', 'w'], + 2 => ['pipe', '\\' === \DIRECTORY_SEPARATOR ? 'a' : 'w'], + ]; + $pipes = []; + $this->stream = proc_open($this->command, $descriptorSpec, $pipes); + stream_set_blocking($pipes[2], false); + if ($err = stream_get_contents($pipes[2])) { + throw new TransportException('Process could not be started: '.$err); + } + $this->in = &$pipes[0]; + $this->out = &$pipes[1]; + $this->err = &$pipes[2]; + } + + public function terminate(): void + { + if (null !== $this->stream) { + fclose($this->in); + $out = stream_get_contents($this->out); + fclose($this->out); + $err = stream_get_contents($this->err); + fclose($this->err); + if (0 !== $exitCode = proc_close($this->stream)) { + $errorMessage = 'Process failed with exit code '.$exitCode.': '.$out.$err; + } + } + + parent::terminate(); + + if (!$this->interactive && isset($errorMessage)) { + throw new TransportException($errorMessage); + } + } + + protected function getReadConnectionDescription(): string + { + return 'process '.$this->command; + } +} diff --git a/3rdparty/symfony/mailer/Transport/Smtp/Stream/SocketStream.php b/3rdparty/symfony/mailer/Transport/Smtp/Stream/SocketStream.php new file mode 100644 index 00000000..49e6ea49 --- /dev/null +++ b/3rdparty/symfony/mailer/Transport/Smtp/Stream/SocketStream.php @@ -0,0 +1,193 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Component\Mailer\Transport\Smtp\Stream; + +use Symfony\Component\Mailer\Exception\TransportException; + +/** + * A stream supporting remote sockets. + * + * @author Fabien Potencier + * @author Chris Corbyn + * + * @internal + */ +final class SocketStream extends AbstractStream +{ + private string $url; + private string $host = 'localhost'; + private int $port = 465; + private float $timeout; + private bool $tls = true; + private ?string $sourceIp = null; + private array $streamContextOptions = []; + + /** + * @return $this + */ + public function setTimeout(float $timeout): static + { + $this->timeout = $timeout; + + return $this; + } + + public function getTimeout(): float + { + return $this->timeout ?? (float) \ini_get('default_socket_timeout'); + } + + /** + * Literal IPv6 addresses should be wrapped in square brackets. + * + * @return $this + */ + public function setHost(string $host): static + { + $this->host = $host; + + return $this; + } + + public function getHost(): string + { + return $this->host; + } + + /** + * @return $this + */ + public function setPort(int $port): static + { + $this->port = $port; + + return $this; + } + + public function getPort(): int + { + return $this->port; + } + + /** + * Sets the TLS/SSL on the socket (disables STARTTLS). + * + * @return $this + */ + public function disableTls(): static + { + $this->tls = false; + + return $this; + } + + public function isTLS(): bool + { + return $this->tls; + } + + /** + * @return $this + */ + public function setStreamOptions(array $options): static + { + $this->streamContextOptions = $options; + + return $this; + } + + public function getStreamOptions(): array + { + return $this->streamContextOptions; + } + + /** + * Sets the source IP. + * + * IPv6 addresses should be wrapped in square brackets. + * + * @return $this + */ + public function setSourceIp(string $ip): static + { + $this->sourceIp = $ip; + + return $this; + } + + /** + * Returns the IP used to connect to the destination. + */ + public function getSourceIp(): ?string + { + return $this->sourceIp; + } + + public function initialize(): void + { + $this->url = $this->host.':'.$this->port; + if ($this->tls) { + $this->url = 'ssl://'.$this->url; + } + $options = []; + if ($this->sourceIp) { + $options['socket']['bindto'] = $this->sourceIp.':0'; + } + if ($this->streamContextOptions) { + $options = array_merge($options, $this->streamContextOptions); + } + // do it unconditionally as it will be used by STARTTLS as well if supported + $options['ssl']['crypto_method'] ??= \STREAM_CRYPTO_METHOD_TLS_CLIENT | \STREAM_CRYPTO_METHOD_TLSv1_2_CLIENT | \STREAM_CRYPTO_METHOD_TLSv1_1_CLIENT; + $streamContext = stream_context_create($options); + + $timeout = $this->getTimeout(); + set_error_handler(function ($type, $msg) { + throw new TransportException(sprintf('Connection could not be established with host "%s": ', $this->url).$msg); + }); + try { + $this->stream = stream_socket_client($this->url, $errno, $errstr, $timeout, \STREAM_CLIENT_CONNECT, $streamContext); + } finally { + restore_error_handler(); + } + + stream_set_blocking($this->stream, true); + stream_set_timeout($this->stream, (int) $timeout, (int) (($timeout - (int) $timeout) * 1000000)); + $this->in = &$this->stream; + $this->out = &$this->stream; + } + + public function startTLS(): bool + { + set_error_handler(function ($type, $msg) { + throw new TransportException('Unable to connect with STARTTLS: '.$msg); + }); + try { + return stream_socket_enable_crypto($this->stream, true); + } finally { + restore_error_handler(); + } + } + + public function terminate(): void + { + if (null !== $this->stream) { + fclose($this->stream); + } + + parent::terminate(); + } + + protected function getReadConnectionDescription(): string + { + return $this->url; + } +} diff --git a/3rdparty/symfony/mailer/Transport/TransportFactoryInterface.php b/3rdparty/symfony/mailer/Transport/TransportFactoryInterface.php new file mode 100644 index 00000000..9785ae81 --- /dev/null +++ b/3rdparty/symfony/mailer/Transport/TransportFactoryInterface.php @@ -0,0 +1,29 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Component\Mailer\Transport; + +use Symfony\Component\Mailer\Exception\IncompleteDsnException; +use Symfony\Component\Mailer\Exception\UnsupportedSchemeException; + +/** + * @author Konstantin Myakshin + */ +interface TransportFactoryInterface +{ + /** + * @throws UnsupportedSchemeException + * @throws IncompleteDsnException + */ + public function create(Dsn $dsn): TransportInterface; + + public function supports(Dsn $dsn): bool; +} diff --git a/3rdparty/symfony/mailer/Transport/TransportInterface.php b/3rdparty/symfony/mailer/Transport/TransportInterface.php new file mode 100644 index 00000000..01570cab --- /dev/null +++ b/3rdparty/symfony/mailer/Transport/TransportInterface.php @@ -0,0 +1,33 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Component\Mailer\Transport; + +use Symfony\Component\Mailer\Envelope; +use Symfony\Component\Mailer\Exception\TransportExceptionInterface; +use Symfony\Component\Mailer\SentMessage; +use Symfony\Component\Mime\RawMessage; + +/** + * Interface for all mailer transports. + * + * When sending emails, you should prefer MailerInterface implementations + * as they allow asynchronous sending. + * + * @author Fabien Potencier + */ +interface TransportInterface extends \Stringable +{ + /** + * @throws TransportExceptionInterface + */ + public function send(RawMessage $message, ?Envelope $envelope = null): ?SentMessage; +} diff --git a/3rdparty/symfony/mailer/Transport/Transports.php b/3rdparty/symfony/mailer/Transport/Transports.php new file mode 100644 index 00000000..f02b9bc9 --- /dev/null +++ b/3rdparty/symfony/mailer/Transport/Transports.php @@ -0,0 +1,75 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Component\Mailer\Transport; + +use Symfony\Component\Mailer\Envelope; +use Symfony\Component\Mailer\Exception\InvalidArgumentException; +use Symfony\Component\Mailer\Exception\LogicException; +use Symfony\Component\Mailer\SentMessage; +use Symfony\Component\Mime\Message; +use Symfony\Component\Mime\RawMessage; + +/** + * @author Fabien Potencier + */ +final class Transports implements TransportInterface +{ + /** + * @var array + */ + private array $transports = []; + private TransportInterface $default; + + /** + * @param iterable $transports + */ + public function __construct(iterable $transports) + { + foreach ($transports as $name => $transport) { + $this->default ??= $transport; + $this->transports[$name] = $transport; + } + + if (!$this->transports) { + throw new LogicException(sprintf('"%s" must have at least one transport configured.', __CLASS__)); + } + } + + public function send(RawMessage $message, ?Envelope $envelope = null): ?SentMessage + { + /** @var Message $message */ + if (RawMessage::class === $message::class || !$message->getHeaders()->has('X-Transport')) { + return $this->default->send($message, $envelope); + } + + $headers = $message->getHeaders(); + $transport = $headers->get('X-Transport')->getBody(); + $headers->remove('X-Transport'); + + if (!isset($this->transports[$transport])) { + throw new InvalidArgumentException(sprintf('The "%s" transport does not exist (available transports: "%s").', $transport, implode('", "', array_keys($this->transports)))); + } + + try { + return $this->transports[$transport]->send($message, $envelope); + } catch (\Throwable $e) { + $headers->addTextHeader('X-Transport', $transport); + + throw $e; + } + } + + public function __toString(): string + { + return '['.implode(',', array_keys($this->transports)).']'; + } +} diff --git a/3rdparty/symfony/mime/Address.php b/3rdparty/symfony/mime/Address.php new file mode 100644 index 00000000..ce57f77e --- /dev/null +++ b/3rdparty/symfony/mime/Address.php @@ -0,0 +1,120 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Component\Mime; + +use Egulias\EmailValidator\EmailValidator; +use Egulias\EmailValidator\Validation\MessageIDValidation; +use Egulias\EmailValidator\Validation\RFCValidation; +use Symfony\Component\Mime\Encoder\IdnAddressEncoder; +use Symfony\Component\Mime\Exception\InvalidArgumentException; +use Symfony\Component\Mime\Exception\LogicException; +use Symfony\Component\Mime\Exception\RfcComplianceException; + +/** + * @author Fabien Potencier + */ +final class Address +{ + /** + * A regex that matches a structure like 'Name '. + * It matches anything between the first < and last > as email address. + * This allows to use a single string to construct an Address, which can be convenient to use in + * config, and allows to have more readable config. + * This does not try to cover all edge cases for address. + */ + private const FROM_STRING_PATTERN = '~(?[^<]*)<(?.*)>[^>]*~'; + + private static EmailValidator $validator; + private static IdnAddressEncoder $encoder; + + private string $address; + private string $name; + + public function __construct(string $address, string $name = '') + { + if (!class_exists(EmailValidator::class)) { + throw new LogicException(sprintf('The "%s" class cannot be used as it needs "%s". Try running "composer require egulias/email-validator".', __CLASS__, EmailValidator::class)); + } + + self::$validator ??= new EmailValidator(); + + $this->address = trim($address); + $this->name = trim(str_replace(["\n", "\r"], '', $name)); + + if (!self::$validator->isValid($this->address, class_exists(MessageIDValidation::class) ? new MessageIDValidation() : new RFCValidation())) { + throw new RfcComplianceException(sprintf('Email "%s" does not comply with addr-spec of RFC 2822.', $address)); + } + } + + public function getAddress(): string + { + return $this->address; + } + + public function getName(): string + { + return $this->name; + } + + public function getEncodedAddress(): string + { + self::$encoder ??= new IdnAddressEncoder(); + + return self::$encoder->encodeString($this->address); + } + + public function toString(): string + { + return ($n = $this->getEncodedName()) ? $n.' <'.$this->getEncodedAddress().'>' : $this->getEncodedAddress(); + } + + public function getEncodedName(): string + { + if ('' === $this->getName()) { + return ''; + } + + return sprintf('"%s"', preg_replace('/"/u', '\"', $this->getName())); + } + + public static function create(self|string $address): self + { + if ($address instanceof self) { + return $address; + } + + if (!str_contains($address, '<')) { + return new self($address); + } + + if (!preg_match(self::FROM_STRING_PATTERN, $address, $matches)) { + throw new InvalidArgumentException(sprintf('Could not parse "%s" to a "%s" instance.', $address, self::class)); + } + + return new self($matches['addrSpec'], trim($matches['displayName'], ' \'"')); + } + + /** + * @param array $addresses + * + * @return Address[] + */ + public static function createArray(array $addresses): array + { + $addrs = []; + foreach ($addresses as $address) { + $addrs[] = self::create($address); + } + + return $addrs; + } +} diff --git a/3rdparty/symfony/mime/BodyRendererInterface.php b/3rdparty/symfony/mime/BodyRendererInterface.php new file mode 100644 index 00000000..d6921726 --- /dev/null +++ b/3rdparty/symfony/mime/BodyRendererInterface.php @@ -0,0 +1,20 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Component\Mime; + +/** + * @author Fabien Potencier + */ +interface BodyRendererInterface +{ + public function render(Message $message): void; +} diff --git a/3rdparty/symfony/mime/CharacterStream.php b/3rdparty/symfony/mime/CharacterStream.php new file mode 100644 index 00000000..21d7bc5f --- /dev/null +++ b/3rdparty/symfony/mime/CharacterStream.php @@ -0,0 +1,211 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Component\Mime; + +/** + * @author Fabien Potencier + * @author Xavier De Cock + * + * @internal + */ +final class CharacterStream +{ + /** Pre-computed for optimization */ + private const UTF8_LENGTH_MAP = [ + "\x00" => 1, "\x01" => 1, "\x02" => 1, "\x03" => 1, "\x04" => 1, "\x05" => 1, "\x06" => 1, "\x07" => 1, + "\x08" => 1, "\x09" => 1, "\x0a" => 1, "\x0b" => 1, "\x0c" => 1, "\x0d" => 1, "\x0e" => 1, "\x0f" => 1, + "\x10" => 1, "\x11" => 1, "\x12" => 1, "\x13" => 1, "\x14" => 1, "\x15" => 1, "\x16" => 1, "\x17" => 1, + "\x18" => 1, "\x19" => 1, "\x1a" => 1, "\x1b" => 1, "\x1c" => 1, "\x1d" => 1, "\x1e" => 1, "\x1f" => 1, + "\x20" => 1, "\x21" => 1, "\x22" => 1, "\x23" => 1, "\x24" => 1, "\x25" => 1, "\x26" => 1, "\x27" => 1, + "\x28" => 1, "\x29" => 1, "\x2a" => 1, "\x2b" => 1, "\x2c" => 1, "\x2d" => 1, "\x2e" => 1, "\x2f" => 1, + "\x30" => 1, "\x31" => 1, "\x32" => 1, "\x33" => 1, "\x34" => 1, "\x35" => 1, "\x36" => 1, "\x37" => 1, + "\x38" => 1, "\x39" => 1, "\x3a" => 1, "\x3b" => 1, "\x3c" => 1, "\x3d" => 1, "\x3e" => 1, "\x3f" => 1, + "\x40" => 1, "\x41" => 1, "\x42" => 1, "\x43" => 1, "\x44" => 1, "\x45" => 1, "\x46" => 1, "\x47" => 1, + "\x48" => 1, "\x49" => 1, "\x4a" => 1, "\x4b" => 1, "\x4c" => 1, "\x4d" => 1, "\x4e" => 1, "\x4f" => 1, + "\x50" => 1, "\x51" => 1, "\x52" => 1, "\x53" => 1, "\x54" => 1, "\x55" => 1, "\x56" => 1, "\x57" => 1, + "\x58" => 1, "\x59" => 1, "\x5a" => 1, "\x5b" => 1, "\x5c" => 1, "\x5d" => 1, "\x5e" => 1, "\x5f" => 1, + "\x60" => 1, "\x61" => 1, "\x62" => 1, "\x63" => 1, "\x64" => 1, "\x65" => 1, "\x66" => 1, "\x67" => 1, + "\x68" => 1, "\x69" => 1, "\x6a" => 1, "\x6b" => 1, "\x6c" => 1, "\x6d" => 1, "\x6e" => 1, "\x6f" => 1, + "\x70" => 1, "\x71" => 1, "\x72" => 1, "\x73" => 1, "\x74" => 1, "\x75" => 1, "\x76" => 1, "\x77" => 1, + "\x78" => 1, "\x79" => 1, "\x7a" => 1, "\x7b" => 1, "\x7c" => 1, "\x7d" => 1, "\x7e" => 1, "\x7f" => 1, + "\x80" => 0, "\x81" => 0, "\x82" => 0, "\x83" => 0, "\x84" => 0, "\x85" => 0, "\x86" => 0, "\x87" => 0, + "\x88" => 0, "\x89" => 0, "\x8a" => 0, "\x8b" => 0, "\x8c" => 0, "\x8d" => 0, "\x8e" => 0, "\x8f" => 0, + "\x90" => 0, "\x91" => 0, "\x92" => 0, "\x93" => 0, "\x94" => 0, "\x95" => 0, "\x96" => 0, "\x97" => 0, + "\x98" => 0, "\x99" => 0, "\x9a" => 0, "\x9b" => 0, "\x9c" => 0, "\x9d" => 0, "\x9e" => 0, "\x9f" => 0, + "\xa0" => 0, "\xa1" => 0, "\xa2" => 0, "\xa3" => 0, "\xa4" => 0, "\xa5" => 0, "\xa6" => 0, "\xa7" => 0, + "\xa8" => 0, "\xa9" => 0, "\xaa" => 0, "\xab" => 0, "\xac" => 0, "\xad" => 0, "\xae" => 0, "\xaf" => 0, + "\xb0" => 0, "\xb1" => 0, "\xb2" => 0, "\xb3" => 0, "\xb4" => 0, "\xb5" => 0, "\xb6" => 0, "\xb7" => 0, + "\xb8" => 0, "\xb9" => 0, "\xba" => 0, "\xbb" => 0, "\xbc" => 0, "\xbd" => 0, "\xbe" => 0, "\xbf" => 0, + "\xc0" => 2, "\xc1" => 2, "\xc2" => 2, "\xc3" => 2, "\xc4" => 2, "\xc5" => 2, "\xc6" => 2, "\xc7" => 2, + "\xc8" => 2, "\xc9" => 2, "\xca" => 2, "\xcb" => 2, "\xcc" => 2, "\xcd" => 2, "\xce" => 2, "\xcf" => 2, + "\xd0" => 2, "\xd1" => 2, "\xd2" => 2, "\xd3" => 2, "\xd4" => 2, "\xd5" => 2, "\xd6" => 2, "\xd7" => 2, + "\xd8" => 2, "\xd9" => 2, "\xda" => 2, "\xdb" => 2, "\xdc" => 2, "\xdd" => 2, "\xde" => 2, "\xdf" => 2, + "\xe0" => 3, "\xe1" => 3, "\xe2" => 3, "\xe3" => 3, "\xe4" => 3, "\xe5" => 3, "\xe6" => 3, "\xe7" => 3, + "\xe8" => 3, "\xe9" => 3, "\xea" => 3, "\xeb" => 3, "\xec" => 3, "\xed" => 3, "\xee" => 3, "\xef" => 3, + "\xf0" => 4, "\xf1" => 4, "\xf2" => 4, "\xf3" => 4, "\xf4" => 4, "\xf5" => 4, "\xf6" => 4, "\xf7" => 4, + "\xf8" => 5, "\xf9" => 5, "\xfa" => 5, "\xfb" => 5, "\xfc" => 6, "\xfd" => 6, "\xfe" => 0, "\xff" => 0, + ]; + + private string $data = ''; + private int $dataSize = 0; + private array $map = []; + private int $charCount = 0; + private int $currentPos = 0; + private int $fixedWidth = 0; + + /** + * @param resource|string $input + */ + public function __construct($input, ?string $charset = 'utf-8') + { + $charset = strtolower(trim($charset)) ?: 'utf-8'; + if ('utf-8' === $charset || 'utf8' === $charset) { + $this->fixedWidth = 0; + $this->map = ['p' => [], 'i' => []]; + } else { + $this->fixedWidth = match ($charset) { + // 16 bits + 'ucs2', + 'ucs-2', + 'utf16', + 'utf-16' => 2, + // 32 bits + 'ucs4', + 'ucs-4', + 'utf32', + 'utf-32' => 4, + // 7-8 bit charsets: (us-)?ascii, (iso|iec)-?8859-?[0-9]+, windows-?125[0-9], cp-?[0-9]+, ansi, macintosh, + // koi-?7, koi-?8-?.+, mik, (cork|t1), v?iscii + // and fallback + default => 1, + }; + } + if (\is_resource($input)) { + $blocks = 16372; + while (false !== $read = fread($input, $blocks)) { + $this->write($read); + } + } else { + $this->write($input); + } + } + + public function read(int $length): ?string + { + if ($this->currentPos >= $this->charCount) { + return null; + } + $length = ($this->currentPos + $length > $this->charCount) ? $this->charCount - $this->currentPos : $length; + if ($this->fixedWidth > 0) { + $len = $length * $this->fixedWidth; + $ret = substr($this->data, $this->currentPos * $this->fixedWidth, $len); + $this->currentPos += $length; + } else { + $end = $this->currentPos + $length; + $end = $end > $this->charCount ? $this->charCount : $end; + $ret = ''; + $start = 0; + if ($this->currentPos > 0) { + $start = $this->map['p'][$this->currentPos - 1]; + } + $to = $start; + for (; $this->currentPos < $end; ++$this->currentPos) { + if (isset($this->map['i'][$this->currentPos])) { + $ret .= substr($this->data, $start, $to - $start).'?'; + $start = $this->map['p'][$this->currentPos]; + } else { + $to = $this->map['p'][$this->currentPos]; + } + } + $ret .= substr($this->data, $start, $to - $start); + } + + return $ret; + } + + public function readBytes(int $length): ?array + { + if (null !== $read = $this->read($length)) { + return array_map('ord', str_split($read, 1)); + } + + return null; + } + + public function setPointer(int $charOffset): void + { + if ($this->charCount < $charOffset) { + $charOffset = $this->charCount; + } + $this->currentPos = $charOffset; + } + + public function write(string $chars): void + { + $ignored = ''; + $this->data .= $chars; + if ($this->fixedWidth > 0) { + $strlen = \strlen($chars); + $ignoredL = $strlen % $this->fixedWidth; + $ignored = $ignoredL ? substr($chars, -$ignoredL) : ''; + $this->charCount += ($strlen - $ignoredL) / $this->fixedWidth; + } else { + $this->charCount += $this->getUtf8CharPositions($chars, $this->dataSize, $ignored); + } + $this->dataSize = \strlen($this->data) - \strlen($ignored); + } + + private function getUtf8CharPositions(string $string, int $startOffset, string &$ignoredChars): int + { + $strlen = \strlen($string); + $charPos = \count($this->map['p']); + $foundChars = 0; + $invalid = false; + for ($i = 0; $i < $strlen; ++$i) { + $char = $string[$i]; + $size = self::UTF8_LENGTH_MAP[$char]; + if (0 == $size) { + /* char is invalid, we must wait for a resync */ + $invalid = true; + continue; + } + + if ($invalid) { + /* We mark the chars as invalid and start a new char */ + $this->map['p'][$charPos + $foundChars] = $startOffset + $i; + $this->map['i'][$charPos + $foundChars] = true; + ++$foundChars; + $invalid = false; + } + if (($i + $size) > $strlen) { + $ignoredChars = substr($string, $i); + break; + } + for ($j = 1; $j < $size; ++$j) { + $char = $string[$i + $j]; + if ($char > "\x7F" && $char < "\xC0") { + // Valid - continue parsing + } else { + /* char is invalid, we must wait for a resync */ + $invalid = true; + continue 2; + } + } + /* Ok we got a complete char here */ + $this->map['p'][$charPos + $foundChars] = $startOffset + $i + $size; + $i += $j - 1; + ++$foundChars; + } + + return $foundChars; + } +} diff --git a/3rdparty/symfony/mime/Crypto/DkimOptions.php b/3rdparty/symfony/mime/Crypto/DkimOptions.php new file mode 100644 index 00000000..cee4e7cb --- /dev/null +++ b/3rdparty/symfony/mime/Crypto/DkimOptions.php @@ -0,0 +1,97 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Component\Mime\Crypto; + +/** + * A helper providing autocompletion for available DkimSigner options. + * + * @author Fabien Potencier + */ +final class DkimOptions +{ + private array $options = []; + + public function toArray(): array + { + return $this->options; + } + + /** + * @return $this + */ + public function algorithm(string $algo): static + { + $this->options['algorithm'] = $algo; + + return $this; + } + + /** + * @return $this + */ + public function signatureExpirationDelay(int $show): static + { + $this->options['signature_expiration_delay'] = $show; + + return $this; + } + + /** + * @return $this + */ + public function bodyMaxLength(int $max): static + { + $this->options['body_max_length'] = $max; + + return $this; + } + + /** + * @return $this + */ + public function bodyShowLength(bool $show): static + { + $this->options['body_show_length'] = $show; + + return $this; + } + + /** + * @return $this + */ + public function headerCanon(string $canon): static + { + $this->options['header_canon'] = $canon; + + return $this; + } + + /** + * @return $this + */ + public function bodyCanon(string $canon): static + { + $this->options['body_canon'] = $canon; + + return $this; + } + + /** + * @return $this + */ + public function headersToIgnore(array $headers): static + { + $this->options['headers_to_ignore'] = $headers; + + return $this; + } +} diff --git a/3rdparty/symfony/mime/Crypto/DkimSigner.php b/3rdparty/symfony/mime/Crypto/DkimSigner.php new file mode 100644 index 00000000..1d2005e5 --- /dev/null +++ b/3rdparty/symfony/mime/Crypto/DkimSigner.php @@ -0,0 +1,217 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Component\Mime\Crypto; + +use Symfony\Component\Mime\Exception\InvalidArgumentException; +use Symfony\Component\Mime\Exception\RuntimeException; +use Symfony\Component\Mime\Header\UnstructuredHeader; +use Symfony\Component\Mime\Message; +use Symfony\Component\Mime\Part\AbstractPart; + +/** + * @author Fabien Potencier + * + * RFC 6376 and 8301 + */ +final class DkimSigner +{ + public const CANON_SIMPLE = 'simple'; + public const CANON_RELAXED = 'relaxed'; + + public const ALGO_SHA256 = 'rsa-sha256'; + public const ALGO_ED25519 = 'ed25519-sha256'; // RFC 8463 + + private \OpenSSLAsymmetricKey $key; + private string $domainName; + private string $selector; + private array $defaultOptions; + + /** + * @param string $pk The private key as a string or the path to the file containing the private key, should be prefixed with file:// (in PEM format) + * @param string $passphrase A passphrase of the private key (if any) + */ + public function __construct(string $pk, string $domainName, string $selector, array $defaultOptions = [], string $passphrase = '') + { + if (!\extension_loaded('openssl')) { + throw new \LogicException('PHP extension "openssl" is required to use DKIM.'); + } + $this->key = openssl_pkey_get_private($pk, $passphrase) ?: throw new InvalidArgumentException('Unable to load DKIM private key: '.openssl_error_string()); + $this->domainName = $domainName; + $this->selector = $selector; + $this->defaultOptions = $defaultOptions + [ + 'algorithm' => self::ALGO_SHA256, + 'signature_expiration_delay' => 0, + 'body_max_length' => \PHP_INT_MAX, + 'body_show_length' => false, + 'header_canon' => self::CANON_RELAXED, + 'body_canon' => self::CANON_RELAXED, + 'headers_to_ignore' => [], + ]; + } + + public function sign(Message $message, array $options = []): Message + { + $options += $this->defaultOptions; + if (!\in_array($options['algorithm'], [self::ALGO_SHA256, self::ALGO_ED25519], true)) { + throw new InvalidArgumentException(sprintf('Invalid DKIM signing algorithm "%s".', $options['algorithm'])); + } + $headersToIgnore['return-path'] = true; + $headersToIgnore['x-transport'] = true; + foreach ($options['headers_to_ignore'] as $name) { + $headersToIgnore[strtolower($name)] = true; + } + unset($headersToIgnore['from']); + $signedHeaderNames = []; + $headerCanonData = ''; + $headers = $message->getPreparedHeaders(); + foreach ($headers->getNames() as $name) { + foreach ($headers->all($name) as $header) { + if (isset($headersToIgnore[strtolower($header->getName())])) { + continue; + } + + if ('' !== $header->getBodyAsString()) { + $headerCanonData .= $this->canonicalizeHeader($header->toString(), $options['header_canon']); + $signedHeaderNames[] = $header->getName(); + } + } + } + + [$bodyHash, $bodyLength] = $this->hashBody($message->getBody(), $options['body_canon'], $options['body_max_length']); + + $params = [ + 'v' => '1', + 'q' => 'dns/txt', + 'a' => $options['algorithm'], + 'bh' => base64_encode($bodyHash), + 'd' => $this->domainName, + 'h' => implode(': ', $signedHeaderNames), + 'i' => '@'.$this->domainName, + 's' => $this->selector, + 't' => time(), + 'c' => $options['header_canon'].'/'.$options['body_canon'], + ]; + + if ($options['body_show_length']) { + $params['l'] = $bodyLength; + } + if ($options['signature_expiration_delay']) { + $params['x'] = $params['t'] + $options['signature_expiration_delay']; + } + $value = ''; + foreach ($params as $k => $v) { + $value .= $k.'='.$v.'; '; + } + $value = trim($value); + $header = new UnstructuredHeader('DKIM-Signature', $value); + $headerCanonData .= rtrim($this->canonicalizeHeader($header->toString()."\r\n b=", $options['header_canon'])); + if (self::ALGO_SHA256 === $options['algorithm']) { + if (!openssl_sign($headerCanonData, $signature, $this->key, \OPENSSL_ALGO_SHA256)) { + throw new RuntimeException('Unable to sign DKIM hash: '.openssl_error_string()); + } + } else { + throw new \RuntimeException(sprintf('The "%s" DKIM signing algorithm is not supported yet.', self::ALGO_ED25519)); + } + $header->setValue($value.' b='.trim(chunk_split(base64_encode($signature), 73, ' '))); + $headers->add($header); + + return new Message($headers, $message->getBody()); + } + + private function canonicalizeHeader(string $header, string $headerCanon): string + { + if (self::CANON_RELAXED !== $headerCanon) { + return $header."\r\n"; + } + + $exploded = explode(':', $header, 2); + $name = strtolower(trim($exploded[0])); + $value = str_replace("\r\n", '', $exploded[1]); + $value = trim(preg_replace("/[ \t][ \t]+/", ' ', $value)); + + return $name.':'.$value."\r\n"; + } + + private function hashBody(AbstractPart $body, string $bodyCanon, int $maxLength): array + { + $hash = hash_init('sha256'); + $relaxed = self::CANON_RELAXED === $bodyCanon; + $currentLine = ''; + $emptyCounter = 0; + $isSpaceSequence = false; + $length = 0; + foreach ($body->bodyToIterable() as $chunk) { + $canon = ''; + for ($i = 0, $len = \strlen($chunk); $i < $len; ++$i) { + switch ($chunk[$i]) { + case "\r": + break; + case "\n": + // previous char is always \r + if ($relaxed) { + $isSpaceSequence = false; + } + if ('' === $currentLine) { + ++$emptyCounter; + } else { + $currentLine = ''; + $canon .= "\r\n"; + } + break; + case ' ': + case "\t": + if ($relaxed) { + $isSpaceSequence = true; + break; + } + // no break + default: + if ($emptyCounter > 0) { + $canon .= str_repeat("\r\n", $emptyCounter); + $emptyCounter = 0; + } + if ($isSpaceSequence) { + $currentLine .= ' '; + $canon .= ' '; + $isSpaceSequence = false; + } + $currentLine .= $chunk[$i]; + $canon .= $chunk[$i]; + } + } + + if ($length + \strlen($canon) >= $maxLength) { + $canon = substr($canon, 0, $maxLength - $length); + $length += \strlen($canon); + hash_update($hash, $canon); + + break; + } + + $length += \strlen($canon); + hash_update($hash, $canon); + } + + // Add trailing Line return if last line is non empty + if ('' !== $currentLine) { + hash_update($hash, "\r\n"); + $length += \strlen("\r\n"); + } + + if (!$relaxed && 0 === $length) { + hash_update($hash, "\r\n"); + $length = 2; + } + + return [hash_final($hash, true), $length]; + } +} diff --git a/3rdparty/symfony/mime/Crypto/SMime.php b/3rdparty/symfony/mime/Crypto/SMime.php new file mode 100644 index 00000000..cba95f21 --- /dev/null +++ b/3rdparty/symfony/mime/Crypto/SMime.php @@ -0,0 +1,111 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Component\Mime\Crypto; + +use Symfony\Component\Mime\Exception\RuntimeException; +use Symfony\Component\Mime\Part\SMimePart; + +/** + * @author Sebastiaan Stok + * + * @internal + */ +abstract class SMime +{ + protected function normalizeFilePath(string $path): string + { + if (!file_exists($path)) { + throw new RuntimeException(sprintf('File does not exist: "%s".', $path)); + } + + return 'file://'.str_replace('\\', '/', realpath($path)); + } + + protected function iteratorToFile(iterable $iterator, $stream): void + { + foreach ($iterator as $chunk) { + fwrite($stream, $chunk); + } + } + + protected function convertMessageToSMimePart($stream, string $type, string $subtype): SMimePart + { + rewind($stream); + + $headers = ''; + + while (!feof($stream)) { + $buffer = fread($stream, 78); + $headers .= $buffer; + + // Detect ending of header list + if (preg_match('/(\r\n\r\n|\n\n)/', $headers, $match)) { + $headersPosEnd = strpos($headers, $headerBodySeparator = $match[0]); + + break; + } + } + + $headers = $this->getMessageHeaders(trim(substr($headers, 0, $headersPosEnd))); + + fseek($stream, $headersPosEnd + \strlen($headerBodySeparator)); + + return new SMimePart($this->getStreamIterator($stream), $type, $subtype, $this->getParametersFromHeader($headers['content-type'])); + } + + protected function getStreamIterator($stream): iterable + { + while (!feof($stream)) { + yield str_replace("\n", "\r\n", str_replace("\r\n", "\n", fread($stream, 16372))); + } + } + + private function getMessageHeaders(string $headerData): array + { + $headers = []; + $headerLines = explode("\r\n", str_replace("\n", "\r\n", str_replace("\r\n", "\n", $headerData))); + $currentHeaderName = ''; + + // Transform header lines into an associative array + foreach ($headerLines as $headerLine) { + // Empty lines between headers indicate a new mime-entity + if ('' === $headerLine) { + break; + } + + // Handle headers that span multiple lines + if (!str_contains($headerLine, ':')) { + $headers[$currentHeaderName] .= ' '.trim($headerLine); + continue; + } + + $header = explode(':', $headerLine, 2); + $currentHeaderName = strtolower($header[0]); + $headers[$currentHeaderName] = trim($header[1]); + } + + return $headers; + } + + private function getParametersFromHeader(string $header): array + { + $params = []; + + preg_match_all('/(?P[a-z-0-9]+)=(?P"[^"]+"|(?:[^\s;]+|$))(?:\s+;)?/i', $header, $matches); + + foreach ($matches['value'] as $pos => $paramValue) { + $params[$matches['name'][$pos]] = trim($paramValue, '"'); + } + + return $params; + } +} diff --git a/3rdparty/symfony/mime/Crypto/SMimeEncrypter.php b/3rdparty/symfony/mime/Crypto/SMimeEncrypter.php new file mode 100644 index 00000000..c7c05452 --- /dev/null +++ b/3rdparty/symfony/mime/Crypto/SMimeEncrypter.php @@ -0,0 +1,63 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Component\Mime\Crypto; + +use Symfony\Component\Mime\Exception\RuntimeException; +use Symfony\Component\Mime\Message; + +/** + * @author Sebastiaan Stok + */ +final class SMimeEncrypter extends SMime +{ + private string|array $certs; + private int $cipher; + + /** + * @param string|string[] $certificate The path (or array of paths) of the file(s) containing the X.509 certificate(s) + * @param int|null $cipher A set of algorithms used to encrypt the message. Must be one of these PHP constants: https://www.php.net/manual/en/openssl.ciphers.php + */ + public function __construct(string|array $certificate, ?int $cipher = null) + { + if (!\extension_loaded('openssl')) { + throw new \LogicException('PHP extension "openssl" is required to use SMime.'); + } + + if (\is_array($certificate)) { + $this->certs = array_map($this->normalizeFilePath(...), $certificate); + } else { + $this->certs = $this->normalizeFilePath($certificate); + } + + $this->cipher = $cipher ?? \OPENSSL_CIPHER_AES_256_CBC; + } + + public function encrypt(Message $message): Message + { + $bufferFile = tmpfile(); + $outputFile = tmpfile(); + + $this->iteratorToFile($message->toIterable(), $bufferFile); + + if (!@openssl_pkcs7_encrypt(stream_get_meta_data($bufferFile)['uri'], stream_get_meta_data($outputFile)['uri'], $this->certs, [], 0, $this->cipher)) { + throw new RuntimeException(sprintf('Failed to encrypt S/Mime message. Error: "%s".', openssl_error_string())); + } + + $mimePart = $this->convertMessageToSMimePart($outputFile, 'application', 'pkcs7-mime'); + $mimePart->getHeaders() + ->addTextHeader('Content-Transfer-Encoding', 'base64') + ->addParameterizedHeader('Content-Disposition', 'attachment', ['name' => 'smime.p7m']) + ; + + return new Message($message->getHeaders(), $mimePart); + } +} diff --git a/3rdparty/symfony/mime/Crypto/SMimeSigner.php b/3rdparty/symfony/mime/Crypto/SMimeSigner.php new file mode 100644 index 00000000..eaa423d8 --- /dev/null +++ b/3rdparty/symfony/mime/Crypto/SMimeSigner.php @@ -0,0 +1,65 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Component\Mime\Crypto; + +use Symfony\Component\Mime\Exception\RuntimeException; +use Symfony\Component\Mime\Message; + +/** + * @author Sebastiaan Stok + */ +final class SMimeSigner extends SMime +{ + private string $signCertificate; + private string|array $signPrivateKey; + private int $signOptions; + private ?string $extraCerts; + + /** + * @param string $certificate The path of the file containing the signing certificate (in PEM format) + * @param string $privateKey The path of the file containing the private key (in PEM format) + * @param string|null $privateKeyPassphrase A passphrase of the private key (if any) + * @param string|null $extraCerts The path of the file containing intermediate certificates (in PEM format) needed by the signing certificate + * @param int|null $signOptions Bitwise operator options for openssl_pkcs7_sign() (@see https://secure.php.net/manual/en/openssl.pkcs7.flags.php) + */ + public function __construct(string $certificate, string $privateKey, ?string $privateKeyPassphrase = null, ?string $extraCerts = null, ?int $signOptions = null) + { + if (!\extension_loaded('openssl')) { + throw new \LogicException('PHP extension "openssl" is required to use SMime.'); + } + + $this->signCertificate = $this->normalizeFilePath($certificate); + + if (null !== $privateKeyPassphrase) { + $this->signPrivateKey = [$this->normalizeFilePath($privateKey), $privateKeyPassphrase]; + } else { + $this->signPrivateKey = $this->normalizeFilePath($privateKey); + } + + $this->signOptions = $signOptions ?? \PKCS7_DETACHED; + $this->extraCerts = $extraCerts ? realpath($extraCerts) : null; + } + + public function sign(Message $message): Message + { + $bufferFile = tmpfile(); + $outputFile = tmpfile(); + + $this->iteratorToFile($message->getBody()->toIterable(), $bufferFile); + + if (!@openssl_pkcs7_sign(stream_get_meta_data($bufferFile)['uri'], stream_get_meta_data($outputFile)['uri'], $this->signCertificate, $this->signPrivateKey, [], $this->signOptions, $this->extraCerts)) { + throw new RuntimeException(sprintf('Failed to sign S/Mime message. Error: "%s".', openssl_error_string())); + } + + return new Message($message->getHeaders(), $this->convertMessageToSMimePart($outputFile, 'multipart', 'signed')); + } +} diff --git a/3rdparty/symfony/mime/DependencyInjection/AddMimeTypeGuesserPass.php b/3rdparty/symfony/mime/DependencyInjection/AddMimeTypeGuesserPass.php new file mode 100644 index 00000000..70fa7863 --- /dev/null +++ b/3rdparty/symfony/mime/DependencyInjection/AddMimeTypeGuesserPass.php @@ -0,0 +1,37 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Component\Mime\DependencyInjection; + +use Symfony\Component\DependencyInjection\Compiler\CompilerPassInterface; +use Symfony\Component\DependencyInjection\ContainerBuilder; +use Symfony\Component\DependencyInjection\Reference; + +/** + * Registers custom mime types guessers. + * + * @author Fabien Potencier + */ +class AddMimeTypeGuesserPass implements CompilerPassInterface +{ + /** + * @return void + */ + public function process(ContainerBuilder $container) + { + if ($container->has('mime_types')) { + $definition = $container->findDefinition('mime_types'); + foreach ($container->findTaggedServiceIds('mime.mime_type_guesser', true) as $id => $attributes) { + $definition->addMethodCall('registerGuesser', [new Reference($id)]); + } + } + } +} diff --git a/3rdparty/symfony/mime/DraftEmail.php b/3rdparty/symfony/mime/DraftEmail.php new file mode 100644 index 00000000..82ce7636 --- /dev/null +++ b/3rdparty/symfony/mime/DraftEmail.php @@ -0,0 +1,45 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Component\Mime; + +use Symfony\Component\Mime\Header\Headers; +use Symfony\Component\Mime\Part\AbstractPart; + +/** + * @author Kevin Bond + */ +class DraftEmail extends Email +{ + public function __construct(?Headers $headers = null, ?AbstractPart $body = null) + { + parent::__construct($headers, $body); + + $this->getHeaders()->addTextHeader('X-Unsent', '1'); + } + + /** + * Override default behavior as draft emails do not require From/Sender/Date/Message-ID headers. + * These are added by the client that actually sends the email. + */ + public function getPreparedHeaders(): Headers + { + $headers = clone $this->getHeaders(); + + if (!$headers->has('MIME-Version')) { + $headers->addTextHeader('MIME-Version', '1.0'); + } + + $headers->remove('Bcc'); + + return $headers; + } +} diff --git a/3rdparty/symfony/mime/Email.php b/3rdparty/symfony/mime/Email.php new file mode 100644 index 00000000..346618cf --- /dev/null +++ b/3rdparty/symfony/mime/Email.php @@ -0,0 +1,591 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Component\Mime; + +use Symfony\Component\Mime\Exception\LogicException; +use Symfony\Component\Mime\Part\AbstractPart; +use Symfony\Component\Mime\Part\DataPart; +use Symfony\Component\Mime\Part\File; +use Symfony\Component\Mime\Part\Multipart\AlternativePart; +use Symfony\Component\Mime\Part\Multipart\MixedPart; +use Symfony\Component\Mime\Part\Multipart\RelatedPart; +use Symfony\Component\Mime\Part\TextPart; + +/** + * @author Fabien Potencier + */ +class Email extends Message +{ + public const PRIORITY_HIGHEST = 1; + public const PRIORITY_HIGH = 2; + public const PRIORITY_NORMAL = 3; + public const PRIORITY_LOW = 4; + public const PRIORITY_LOWEST = 5; + + private const PRIORITY_MAP = [ + self::PRIORITY_HIGHEST => 'Highest', + self::PRIORITY_HIGH => 'High', + self::PRIORITY_NORMAL => 'Normal', + self::PRIORITY_LOW => 'Low', + self::PRIORITY_LOWEST => 'Lowest', + ]; + + /** + * @var resource|string|null + */ + private $text; + + private ?string $textCharset = null; + + /** + * @var resource|string|null + */ + private $html; + + private ?string $htmlCharset = null; + private array $attachments = []; + private ?AbstractPart $cachedBody = null; // Used to avoid wrong body hash in DKIM signatures with multiple parts (e.g. HTML + TEXT) due to multiple boundaries. + + /** + * @return $this + */ + public function subject(string $subject): static + { + return $this->setHeaderBody('Text', 'Subject', $subject); + } + + public function getSubject(): ?string + { + return $this->getHeaders()->getHeaderBody('Subject'); + } + + /** + * @return $this + */ + public function date(\DateTimeInterface $dateTime): static + { + return $this->setHeaderBody('Date', 'Date', $dateTime); + } + + public function getDate(): ?\DateTimeImmutable + { + return $this->getHeaders()->getHeaderBody('Date'); + } + + /** + * @return $this + */ + public function returnPath(Address|string $address): static + { + return $this->setHeaderBody('Path', 'Return-Path', Address::create($address)); + } + + public function getReturnPath(): ?Address + { + return $this->getHeaders()->getHeaderBody('Return-Path'); + } + + /** + * @return $this + */ + public function sender(Address|string $address): static + { + return $this->setHeaderBody('Mailbox', 'Sender', Address::create($address)); + } + + public function getSender(): ?Address + { + return $this->getHeaders()->getHeaderBody('Sender'); + } + + /** + * @return $this + */ + public function addFrom(Address|string ...$addresses): static + { + return $this->addListAddressHeaderBody('From', $addresses); + } + + /** + * @return $this + */ + public function from(Address|string ...$addresses): static + { + if (!$addresses) { + throw new LogicException('"from()" must be called with at least one address.'); + } + + return $this->setListAddressHeaderBody('From', $addresses); + } + + /** + * @return Address[] + */ + public function getFrom(): array + { + return $this->getHeaders()->getHeaderBody('From') ?: []; + } + + /** + * @return $this + */ + public function addReplyTo(Address|string ...$addresses): static + { + return $this->addListAddressHeaderBody('Reply-To', $addresses); + } + + /** + * @return $this + */ + public function replyTo(Address|string ...$addresses): static + { + return $this->setListAddressHeaderBody('Reply-To', $addresses); + } + + /** + * @return Address[] + */ + public function getReplyTo(): array + { + return $this->getHeaders()->getHeaderBody('Reply-To') ?: []; + } + + /** + * @return $this + */ + public function addTo(Address|string ...$addresses): static + { + return $this->addListAddressHeaderBody('To', $addresses); + } + + /** + * @return $this + */ + public function to(Address|string ...$addresses): static + { + return $this->setListAddressHeaderBody('To', $addresses); + } + + /** + * @return Address[] + */ + public function getTo(): array + { + return $this->getHeaders()->getHeaderBody('To') ?: []; + } + + /** + * @return $this + */ + public function addCc(Address|string ...$addresses): static + { + return $this->addListAddressHeaderBody('Cc', $addresses); + } + + /** + * @return $this + */ + public function cc(Address|string ...$addresses): static + { + return $this->setListAddressHeaderBody('Cc', $addresses); + } + + /** + * @return Address[] + */ + public function getCc(): array + { + return $this->getHeaders()->getHeaderBody('Cc') ?: []; + } + + /** + * @return $this + */ + public function addBcc(Address|string ...$addresses): static + { + return $this->addListAddressHeaderBody('Bcc', $addresses); + } + + /** + * @return $this + */ + public function bcc(Address|string ...$addresses): static + { + return $this->setListAddressHeaderBody('Bcc', $addresses); + } + + /** + * @return Address[] + */ + public function getBcc(): array + { + return $this->getHeaders()->getHeaderBody('Bcc') ?: []; + } + + /** + * Sets the priority of this message. + * + * The value is an integer where 1 is the highest priority and 5 is the lowest. + * + * @return $this + */ + public function priority(int $priority): static + { + if ($priority > 5) { + $priority = 5; + } elseif ($priority < 1) { + $priority = 1; + } + + return $this->setHeaderBody('Text', 'X-Priority', sprintf('%d (%s)', $priority, self::PRIORITY_MAP[$priority])); + } + + /** + * Get the priority of this message. + * + * The returned value is an integer where 1 is the highest priority and 5 + * is the lowest. + */ + public function getPriority(): int + { + [$priority] = sscanf($this->getHeaders()->getHeaderBody('X-Priority') ?? '', '%[1-5]'); + + return $priority ?? 3; + } + + /** + * @param resource|string|null $body + * + * @return $this + */ + public function text($body, string $charset = 'utf-8'): static + { + if (null !== $body && !\is_string($body) && !\is_resource($body)) { + throw new \TypeError(sprintf('The body must be a string, a resource or null (got "%s").', get_debug_type($body))); + } + + $this->cachedBody = null; + $this->text = $body; + $this->textCharset = $charset; + + return $this; + } + + /** + * @return resource|string|null + */ + public function getTextBody() + { + return $this->text; + } + + public function getTextCharset(): ?string + { + return $this->textCharset; + } + + /** + * @param resource|string|null $body + * + * @return $this + */ + public function html($body, string $charset = 'utf-8'): static + { + if (null !== $body && !\is_string($body) && !\is_resource($body)) { + throw new \TypeError(sprintf('The body must be a string, a resource or null (got "%s").', get_debug_type($body))); + } + + $this->cachedBody = null; + $this->html = $body; + $this->htmlCharset = $charset; + + return $this; + } + + /** + * @return resource|string|null + */ + public function getHtmlBody() + { + return $this->html; + } + + public function getHtmlCharset(): ?string + { + return $this->htmlCharset; + } + + /** + * @param resource|string $body + * + * @return $this + */ + public function attach($body, ?string $name = null, ?string $contentType = null): static + { + return $this->addPart(new DataPart($body, $name, $contentType)); + } + + /** + * @return $this + */ + public function attachFromPath(string $path, ?string $name = null, ?string $contentType = null): static + { + return $this->addPart(new DataPart(new File($path), $name, $contentType)); + } + + /** + * @param resource|string $body + * + * @return $this + */ + public function embed($body, ?string $name = null, ?string $contentType = null): static + { + return $this->addPart((new DataPart($body, $name, $contentType))->asInline()); + } + + /** + * @return $this + */ + public function embedFromPath(string $path, ?string $name = null, ?string $contentType = null): static + { + return $this->addPart((new DataPart(new File($path), $name, $contentType))->asInline()); + } + + /** + * @return $this + * + * @deprecated since Symfony 6.2, use addPart() instead + */ + public function attachPart(DataPart $part): static + { + @trigger_deprecation('symfony/mime', '6.2', 'The "%s()" method is deprecated, use "addPart()" instead.', __METHOD__); + + return $this->addPart($part); + } + + /** + * @return $this + */ + public function addPart(DataPart $part): static + { + $this->cachedBody = null; + $this->attachments[] = $part; + + return $this; + } + + /** + * @return DataPart[] + */ + public function getAttachments(): array + { + return $this->attachments; + } + + public function getBody(): AbstractPart + { + if (null !== $body = parent::getBody()) { + return $body; + } + + return $this->generateBody(); + } + + /** + * @return void + */ + public function ensureValidity() + { + $this->ensureBodyValid(); + + if ('1' === $this->getHeaders()->getHeaderBody('X-Unsent')) { + throw new LogicException('Cannot send messages marked as "draft".'); + } + + parent::ensureValidity(); + } + + private function ensureBodyValid(): void + { + if (null === $this->text && null === $this->html && !$this->attachments) { + throw new LogicException('A message must have a text or an HTML part or attachments.'); + } + } + + /** + * Generates an AbstractPart based on the raw body of a message. + * + * The most "complex" part generated by this method is when there is text and HTML bodies + * with related images for the HTML part and some attachments: + * + * multipart/mixed + * | + * |------------> multipart/related + * | | + * | |------------> multipart/alternative + * | | | + * | | ------------> text/plain (with content) + * | | | + * | | ------------> text/html (with content) + * | | + * | ------------> image/png (with content) + * | + * ------------> application/pdf (with content) + */ + private function generateBody(): AbstractPart + { + if (null !== $this->cachedBody) { + return $this->cachedBody; + } + + $this->ensureBodyValid(); + + [$htmlPart, $otherParts, $relatedParts] = $this->prepareParts(); + + $part = null === $this->text ? null : new TextPart($this->text, $this->textCharset); + if (null !== $htmlPart) { + if (null !== $part) { + $part = new AlternativePart($part, $htmlPart); + } else { + $part = $htmlPart; + } + } + + if ($relatedParts) { + $part = new RelatedPart($part, ...$relatedParts); + } + + if ($otherParts) { + if ($part) { + $part = new MixedPart($part, ...$otherParts); + } else { + $part = new MixedPart(...$otherParts); + } + } + + return $this->cachedBody = $part; + } + + private function prepareParts(): ?array + { + $names = []; + $htmlPart = null; + $html = $this->html; + if (null !== $html) { + $htmlPart = new TextPart($html, $this->htmlCharset, 'html'); + $html = $htmlPart->getBody(); + + $regexes = [ + ']*src\s*=\s*(?:([\'"])cid:(.+?)\\1|cid:([^>\s]+))', + '<\w+\s+[^>]*background\s*=\s*(?:([\'"])cid:(.+?)\\1|cid:([^>\s]+))', + ]; + $tmpMatches = []; + foreach ($regexes as $regex) { + preg_match_all('/'.$regex.'/i', $html, $tmpMatches); + $names = array_merge($names, $tmpMatches[2], $tmpMatches[3]); + } + $names = array_filter(array_unique($names)); + } + + $otherParts = $relatedParts = []; + foreach ($this->attachments as $part) { + foreach ($names as $name) { + if ($name !== $part->getName() && (!$part->hasContentId() || $name !== $part->getContentId())) { + continue; + } + if (isset($relatedParts[$name])) { + continue 2; + } + + if ($name !== $part->getContentId()) { + $html = str_replace('cid:'.$name, 'cid:'.$part->getContentId(), $html, $count); + } + $relatedParts[$name] = $part; + $part->setName($part->getContentId())->asInline(); + + continue 2; + } + + $otherParts[] = $part; + } + if (null !== $htmlPart) { + $htmlPart = new TextPart($html, $this->htmlCharset, 'html'); + } + + return [$htmlPart, $otherParts, array_values($relatedParts)]; + } + + /** + * @return $this + */ + private function setHeaderBody(string $type, string $name, $body): static + { + $this->getHeaders()->setHeaderBody($type, $name, $body); + + return $this; + } + + /** + * @return $this + */ + private function addListAddressHeaderBody(string $name, array $addresses): static + { + if (!$header = $this->getHeaders()->get($name)) { + return $this->setListAddressHeaderBody($name, $addresses); + } + $header->addAddresses(Address::createArray($addresses)); + + return $this; + } + + /** + * @return $this + */ + private function setListAddressHeaderBody(string $name, array $addresses): static + { + $addresses = Address::createArray($addresses); + $headers = $this->getHeaders(); + if ($header = $headers->get($name)) { + $header->setAddresses($addresses); + } else { + $headers->addMailboxListHeader($name, $addresses); + } + + return $this; + } + + /** + * @internal + */ + public function __serialize(): array + { + if (\is_resource($this->text)) { + $this->text = (new TextPart($this->text))->getBody(); + } + + if (\is_resource($this->html)) { + $this->html = (new TextPart($this->html))->getBody(); + } + + return [$this->text, $this->textCharset, $this->html, $this->htmlCharset, $this->attachments, parent::__serialize()]; + } + + /** + * @internal + */ + public function __unserialize(array $data): void + { + [$this->text, $this->textCharset, $this->html, $this->htmlCharset, $this->attachments, $parentData] = $data; + + parent::__unserialize($parentData); + } +} diff --git a/3rdparty/symfony/mime/Encoder/AddressEncoderInterface.php b/3rdparty/symfony/mime/Encoder/AddressEncoderInterface.php new file mode 100644 index 00000000..de477d88 --- /dev/null +++ b/3rdparty/symfony/mime/Encoder/AddressEncoderInterface.php @@ -0,0 +1,28 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Component\Mime\Encoder; + +use Symfony\Component\Mime\Exception\AddressEncoderException; + +/** + * @author Christian Schmidt + */ +interface AddressEncoderInterface +{ + /** + * Encodes an email address. + * + * @throws AddressEncoderException if the email cannot be represented in + * the encoding implemented by this class + */ + public function encodeString(string $address): string; +} diff --git a/3rdparty/symfony/mime/Encoder/Base64ContentEncoder.php b/3rdparty/symfony/mime/Encoder/Base64ContentEncoder.php new file mode 100644 index 00000000..440868af --- /dev/null +++ b/3rdparty/symfony/mime/Encoder/Base64ContentEncoder.php @@ -0,0 +1,45 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Component\Mime\Encoder; + +use Symfony\Component\Mime\Exception\RuntimeException; + +/** + * @author Fabien Potencier + */ +final class Base64ContentEncoder extends Base64Encoder implements ContentEncoderInterface +{ + public function encodeByteStream($stream, int $maxLineLength = 0): iterable + { + if (!\is_resource($stream)) { + throw new \TypeError(sprintf('Method "%s" takes a stream as a first argument.', __METHOD__)); + } + + $filter = stream_filter_append($stream, 'convert.base64-encode', \STREAM_FILTER_READ, [ + 'line-length' => 0 >= $maxLineLength || 76 < $maxLineLength ? 76 : $maxLineLength, + 'line-break-chars' => "\r\n", + ]); + if (!\is_resource($filter)) { + throw new RuntimeException('Unable to set the base64 content encoder to the filter.'); + } + + while (!feof($stream)) { + yield fread($stream, 16372); + } + stream_filter_remove($filter); + } + + public function getName(): string + { + return 'base64'; + } +} diff --git a/3rdparty/symfony/mime/Encoder/Base64Encoder.php b/3rdparty/symfony/mime/Encoder/Base64Encoder.php new file mode 100644 index 00000000..71064785 --- /dev/null +++ b/3rdparty/symfony/mime/Encoder/Base64Encoder.php @@ -0,0 +1,41 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Component\Mime\Encoder; + +/** + * @author Chris Corbyn + */ +class Base64Encoder implements EncoderInterface +{ + /** + * Takes an unencoded string and produces a Base64 encoded string from it. + * + * Base64 encoded strings have a maximum line length of 76 characters. + * If the first line needs to be shorter, indicate the difference with + * $firstLineOffset. + */ + public function encodeString(string $string, ?string $charset = 'utf-8', int $firstLineOffset = 0, int $maxLineLength = 0): string + { + if (0 >= $maxLineLength || 76 < $maxLineLength) { + $maxLineLength = 76; + } + + $encodedString = base64_encode($string); + $firstLine = ''; + if (0 !== $firstLineOffset) { + $firstLine = substr($encodedString, 0, $maxLineLength - $firstLineOffset)."\r\n"; + $encodedString = substr($encodedString, $maxLineLength - $firstLineOffset); + } + + return $firstLine.trim(chunk_split($encodedString, $maxLineLength, "\r\n")); + } +} diff --git a/3rdparty/symfony/mime/Encoder/Base64MimeHeaderEncoder.php b/3rdparty/symfony/mime/Encoder/Base64MimeHeaderEncoder.php new file mode 100644 index 00000000..5c06f6d9 --- /dev/null +++ b/3rdparty/symfony/mime/Encoder/Base64MimeHeaderEncoder.php @@ -0,0 +1,43 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Component\Mime\Encoder; + +/** + * @author Chris Corbyn + */ +final class Base64MimeHeaderEncoder extends Base64Encoder implements MimeHeaderEncoderInterface +{ + public function getName(): string + { + return 'B'; + } + + /** + * Takes an unencoded string and produces a Base64 encoded string from it. + * + * If the charset is iso-2022-jp, it uses mb_encode_mimeheader instead of + * default encodeString, otherwise pass to the parent method. + */ + public function encodeString(string $string, ?string $charset = 'utf-8', int $firstLineOffset = 0, int $maxLineLength = 0): string + { + if ('iso-2022-jp' === strtolower($charset)) { + $old = mb_internal_encoding(); + mb_internal_encoding('utf-8'); + $newstring = mb_encode_mimeheader($string, 'iso-2022-jp', $this->getName(), "\r\n"); + mb_internal_encoding($old); + + return $newstring; + } + + return parent::encodeString($string, $charset, $firstLineOffset, $maxLineLength); + } +} diff --git a/3rdparty/symfony/mime/Encoder/ContentEncoderInterface.php b/3rdparty/symfony/mime/Encoder/ContentEncoderInterface.php new file mode 100644 index 00000000..a45ad04c --- /dev/null +++ b/3rdparty/symfony/mime/Encoder/ContentEncoderInterface.php @@ -0,0 +1,30 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Component\Mime\Encoder; + +/** + * @author Chris Corbyn + */ +interface ContentEncoderInterface extends EncoderInterface +{ + /** + * Encodes the stream to a Generator. + * + * @param resource $stream + */ + public function encodeByteStream($stream, int $maxLineLength = 0): iterable; + + /** + * Gets the MIME name of this content encoding scheme. + */ + public function getName(): string; +} diff --git a/3rdparty/symfony/mime/Encoder/EightBitContentEncoder.php b/3rdparty/symfony/mime/Encoder/EightBitContentEncoder.php new file mode 100644 index 00000000..82831209 --- /dev/null +++ b/3rdparty/symfony/mime/Encoder/EightBitContentEncoder.php @@ -0,0 +1,35 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Component\Mime\Encoder; + +/** + * @author Fabien Potencier + */ +final class EightBitContentEncoder implements ContentEncoderInterface +{ + public function encodeByteStream($stream, int $maxLineLength = 0): iterable + { + while (!feof($stream)) { + yield fread($stream, 16372); + } + } + + public function getName(): string + { + return '8bit'; + } + + public function encodeString(string $string, ?string $charset = 'utf-8', int $firstLineOffset = 0, int $maxLineLength = 0): string + { + return $string; + } +} diff --git a/3rdparty/symfony/mime/Encoder/EncoderInterface.php b/3rdparty/symfony/mime/Encoder/EncoderInterface.php new file mode 100644 index 00000000..bbf6d488 --- /dev/null +++ b/3rdparty/symfony/mime/Encoder/EncoderInterface.php @@ -0,0 +1,26 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Component\Mime\Encoder; + +/** + * @author Chris Corbyn + */ +interface EncoderInterface +{ + /** + * Encode a given string to produce an encoded string. + * + * @param int $firstLineOffset if first line needs to be shorter + * @param int $maxLineLength - 0 indicates the default length for this encoding + */ + public function encodeString(string $string, ?string $charset = 'utf-8', int $firstLineOffset = 0, int $maxLineLength = 0): string; +} diff --git a/3rdparty/symfony/mime/Encoder/IdnAddressEncoder.php b/3rdparty/symfony/mime/Encoder/IdnAddressEncoder.php new file mode 100644 index 00000000..b56e7e39 --- /dev/null +++ b/3rdparty/symfony/mime/Encoder/IdnAddressEncoder.php @@ -0,0 +1,44 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Component\Mime\Encoder; + +/** + * An IDN email address encoder. + * + * Encodes the domain part of an address using IDN. This is compatible will all + * SMTP servers. + * + * Note: It leaves the local part as is. In case there are non-ASCII characters + * in the local part then it depends on the SMTP Server if this is supported. + * + * @author Christian Schmidt + */ +final class IdnAddressEncoder implements AddressEncoderInterface +{ + /** + * Encodes the domain part of an address using IDN. + */ + public function encodeString(string $address): string + { + $i = strrpos($address, '@'); + if (false !== $i) { + $local = substr($address, 0, $i); + $domain = substr($address, $i + 1); + + if (preg_match('/[^\x00-\x7F]/', $domain)) { + $address = sprintf('%s@%s', $local, idn_to_ascii($domain, \IDNA_DEFAULT | \IDNA_USE_STD3_RULES | \IDNA_CHECK_BIDI | \IDNA_CHECK_CONTEXTJ | \IDNA_NONTRANSITIONAL_TO_ASCII, \INTL_IDNA_VARIANT_UTS46)); + } + } + + return $address; + } +} diff --git a/3rdparty/symfony/mime/Encoder/MimeHeaderEncoderInterface.php b/3rdparty/symfony/mime/Encoder/MimeHeaderEncoderInterface.php new file mode 100644 index 00000000..fff2c782 --- /dev/null +++ b/3rdparty/symfony/mime/Encoder/MimeHeaderEncoderInterface.php @@ -0,0 +1,23 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Component\Mime\Encoder; + +/** + * @author Chris Corbyn + */ +interface MimeHeaderEncoderInterface +{ + /** + * Get the MIME name of this content encoding scheme. + */ + public function getName(): string; +} diff --git a/3rdparty/symfony/mime/Encoder/QpContentEncoder.php b/3rdparty/symfony/mime/Encoder/QpContentEncoder.php new file mode 100644 index 00000000..6f420fff --- /dev/null +++ b/3rdparty/symfony/mime/Encoder/QpContentEncoder.php @@ -0,0 +1,55 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Component\Mime\Encoder; + +/** + * @author Lars Strojny + */ +final class QpContentEncoder implements ContentEncoderInterface +{ + public function encodeByteStream($stream, int $maxLineLength = 0): iterable + { + if (!\is_resource($stream)) { + throw new \TypeError(sprintf('Method "%s" takes a stream as a first argument.', __METHOD__)); + } + + // we don't use PHP stream filters here as the content should be small enough + yield $this->encodeString(stream_get_contents($stream), 'utf-8', 0, $maxLineLength); + } + + public function getName(): string + { + return 'quoted-printable'; + } + + public function encodeString(string $string, ?string $charset = 'utf-8', int $firstLineOffset = 0, int $maxLineLength = 0): string + { + return $this->standardize(quoted_printable_encode($string)); + } + + /** + * Make sure CRLF is correct and HT/SPACE are in valid places. + */ + private function standardize(string $string): string + { + // transform CR or LF to CRLF + $string = preg_replace('~=0D(?!=0A)|(? substr_replace($string, '=09', -1), + 0x20 => substr_replace($string, '=20', -1), + default => $string, + }; + } +} diff --git a/3rdparty/symfony/mime/Encoder/QpEncoder.php b/3rdparty/symfony/mime/Encoder/QpEncoder.php new file mode 100644 index 00000000..160dde32 --- /dev/null +++ b/3rdparty/symfony/mime/Encoder/QpEncoder.php @@ -0,0 +1,192 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Component\Mime\Encoder; + +use Symfony\Component\Mime\CharacterStream; + +/** + * @author Chris Corbyn + */ +class QpEncoder implements EncoderInterface +{ + /** + * Pre-computed QP for HUGE optimization. + */ + private const QP_MAP = [ + 0 => '=00', 1 => '=01', 2 => '=02', 3 => '=03', 4 => '=04', + 5 => '=05', 6 => '=06', 7 => '=07', 8 => '=08', 9 => '=09', + 10 => '=0A', 11 => '=0B', 12 => '=0C', 13 => '=0D', 14 => '=0E', + 15 => '=0F', 16 => '=10', 17 => '=11', 18 => '=12', 19 => '=13', + 20 => '=14', 21 => '=15', 22 => '=16', 23 => '=17', 24 => '=18', + 25 => '=19', 26 => '=1A', 27 => '=1B', 28 => '=1C', 29 => '=1D', + 30 => '=1E', 31 => '=1F', 32 => '=20', 33 => '=21', 34 => '=22', + 35 => '=23', 36 => '=24', 37 => '=25', 38 => '=26', 39 => '=27', + 40 => '=28', 41 => '=29', 42 => '=2A', 43 => '=2B', 44 => '=2C', + 45 => '=2D', 46 => '=2E', 47 => '=2F', 48 => '=30', 49 => '=31', + 50 => '=32', 51 => '=33', 52 => '=34', 53 => '=35', 54 => '=36', + 55 => '=37', 56 => '=38', 57 => '=39', 58 => '=3A', 59 => '=3B', + 60 => '=3C', 61 => '=3D', 62 => '=3E', 63 => '=3F', 64 => '=40', + 65 => '=41', 66 => '=42', 67 => '=43', 68 => '=44', 69 => '=45', + 70 => '=46', 71 => '=47', 72 => '=48', 73 => '=49', 74 => '=4A', + 75 => '=4B', 76 => '=4C', 77 => '=4D', 78 => '=4E', 79 => '=4F', + 80 => '=50', 81 => '=51', 82 => '=52', 83 => '=53', 84 => '=54', + 85 => '=55', 86 => '=56', 87 => '=57', 88 => '=58', 89 => '=59', + 90 => '=5A', 91 => '=5B', 92 => '=5C', 93 => '=5D', 94 => '=5E', + 95 => '=5F', 96 => '=60', 97 => '=61', 98 => '=62', 99 => '=63', + 100 => '=64', 101 => '=65', 102 => '=66', 103 => '=67', 104 => '=68', + 105 => '=69', 106 => '=6A', 107 => '=6B', 108 => '=6C', 109 => '=6D', + 110 => '=6E', 111 => '=6F', 112 => '=70', 113 => '=71', 114 => '=72', + 115 => '=73', 116 => '=74', 117 => '=75', 118 => '=76', 119 => '=77', + 120 => '=78', 121 => '=79', 122 => '=7A', 123 => '=7B', 124 => '=7C', + 125 => '=7D', 126 => '=7E', 127 => '=7F', 128 => '=80', 129 => '=81', + 130 => '=82', 131 => '=83', 132 => '=84', 133 => '=85', 134 => '=86', + 135 => '=87', 136 => '=88', 137 => '=89', 138 => '=8A', 139 => '=8B', + 140 => '=8C', 141 => '=8D', 142 => '=8E', 143 => '=8F', 144 => '=90', + 145 => '=91', 146 => '=92', 147 => '=93', 148 => '=94', 149 => '=95', + 150 => '=96', 151 => '=97', 152 => '=98', 153 => '=99', 154 => '=9A', + 155 => '=9B', 156 => '=9C', 157 => '=9D', 158 => '=9E', 159 => '=9F', + 160 => '=A0', 161 => '=A1', 162 => '=A2', 163 => '=A3', 164 => '=A4', + 165 => '=A5', 166 => '=A6', 167 => '=A7', 168 => '=A8', 169 => '=A9', + 170 => '=AA', 171 => '=AB', 172 => '=AC', 173 => '=AD', 174 => '=AE', + 175 => '=AF', 176 => '=B0', 177 => '=B1', 178 => '=B2', 179 => '=B3', + 180 => '=B4', 181 => '=B5', 182 => '=B6', 183 => '=B7', 184 => '=B8', + 185 => '=B9', 186 => '=BA', 187 => '=BB', 188 => '=BC', 189 => '=BD', + 190 => '=BE', 191 => '=BF', 192 => '=C0', 193 => '=C1', 194 => '=C2', + 195 => '=C3', 196 => '=C4', 197 => '=C5', 198 => '=C6', 199 => '=C7', + 200 => '=C8', 201 => '=C9', 202 => '=CA', 203 => '=CB', 204 => '=CC', + 205 => '=CD', 206 => '=CE', 207 => '=CF', 208 => '=D0', 209 => '=D1', + 210 => '=D2', 211 => '=D3', 212 => '=D4', 213 => '=D5', 214 => '=D6', + 215 => '=D7', 216 => '=D8', 217 => '=D9', 218 => '=DA', 219 => '=DB', + 220 => '=DC', 221 => '=DD', 222 => '=DE', 223 => '=DF', 224 => '=E0', + 225 => '=E1', 226 => '=E2', 227 => '=E3', 228 => '=E4', 229 => '=E5', + 230 => '=E6', 231 => '=E7', 232 => '=E8', 233 => '=E9', 234 => '=EA', + 235 => '=EB', 236 => '=EC', 237 => '=ED', 238 => '=EE', 239 => '=EF', + 240 => '=F0', 241 => '=F1', 242 => '=F2', 243 => '=F3', 244 => '=F4', + 245 => '=F5', 246 => '=F6', 247 => '=F7', 248 => '=F8', 249 => '=F9', + 250 => '=FA', 251 => '=FB', 252 => '=FC', 253 => '=FD', 254 => '=FE', + 255 => '=FF', + ]; + + private static array $safeMapShare = []; + + /** + * A map of non-encoded ascii characters. + * + * @var string[] + * + * @internal + */ + protected array $safeMap = []; + + public function __construct() + { + $id = static::class; + if (!isset(self::$safeMapShare[$id])) { + $this->initSafeMap(); + self::$safeMapShare[$id] = $this->safeMap; + } else { + $this->safeMap = self::$safeMapShare[$id]; + } + } + + protected function initSafeMap(): void + { + foreach (array_merge([0x09, 0x20], range(0x21, 0x3C), range(0x3E, 0x7E)) as $byte) { + $this->safeMap[$byte] = \chr($byte); + } + } + + /** + * Takes an unencoded string and produces a QP encoded string from it. + * + * QP encoded strings have a maximum line length of 76 characters. + * If the first line needs to be shorter, indicate the difference with + * $firstLineOffset. + */ + public function encodeString(string $string, ?string $charset = 'utf-8', int $firstLineOffset = 0, int $maxLineLength = 0): string + { + if ($maxLineLength > 76 || $maxLineLength <= 0) { + $maxLineLength = 76; + } + + $thisLineLength = $maxLineLength - $firstLineOffset; + + $lines = []; + $lNo = 0; + $lines[$lNo] = ''; + $currentLine = &$lines[$lNo++]; + $size = $lineLen = 0; + $charStream = new CharacterStream($string, $charset); + + // Fetching more than 4 chars at one is slower, as is fetching fewer bytes + // Conveniently 4 chars is the UTF-8 safe number since UTF-8 has up to 6 + // bytes per char and (6 * 4 * 3 = 72 chars per line) * =NN is 3 bytes + while (null !== $bytes = $charStream->readBytes(4)) { + $enc = $this->encodeByteSequence($bytes, $size); + + $i = strpos($enc, '=0D=0A'); + $newLineLength = $lineLen + (false === $i ? $size : $i); + + if ($currentLine && $newLineLength >= $thisLineLength) { + $lines[$lNo] = ''; + $currentLine = &$lines[$lNo++]; + $thisLineLength = $maxLineLength; + $lineLen = 0; + } + + $currentLine .= $enc; + + if (false === $i) { + $lineLen += $size; + } else { + // 6 is the length of '=0D=0A'. + $lineLen = $size - strrpos($enc, '=0D=0A') - 6; + } + } + + return $this->standardize(implode("=\r\n", $lines)); + } + + /** + * Encode the given byte array into a verbatim QP form. + */ + private function encodeByteSequence(array $bytes, int &$size): string + { + $ret = ''; + $size = 0; + foreach ($bytes as $b) { + if (isset($this->safeMap[$b])) { + $ret .= $this->safeMap[$b]; + ++$size; + } else { + $ret .= self::QP_MAP[$b]; + $size += 3; + } + } + + return $ret; + } + + /** + * Make sure CRLF is correct and HT/SPACE are in valid places. + */ + private function standardize(string $string): string + { + $string = str_replace(["\t=0D=0A", ' =0D=0A', '=0D=0A'], ["=09\r\n", "=20\r\n", "\r\n"], $string); + + return match ($end = \ord(substr($string, -1))) { + 0x09, + 0x20 => substr_replace($string, self::QP_MAP[$end], -1), + default => $string, + }; + } +} diff --git a/3rdparty/symfony/mime/Encoder/QpMimeHeaderEncoder.php b/3rdparty/symfony/mime/Encoder/QpMimeHeaderEncoder.php new file mode 100644 index 00000000..d1d38375 --- /dev/null +++ b/3rdparty/symfony/mime/Encoder/QpMimeHeaderEncoder.php @@ -0,0 +1,40 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Component\Mime\Encoder; + +/** + * @author Chris Corbyn + */ +final class QpMimeHeaderEncoder extends QpEncoder implements MimeHeaderEncoderInterface +{ + protected function initSafeMap(): void + { + foreach (array_merge( + range(0x61, 0x7A), range(0x41, 0x5A), + range(0x30, 0x39), [0x20, 0x21, 0x2A, 0x2B, 0x2D, 0x2F] + ) as $byte) { + $this->safeMap[$byte] = \chr($byte); + } + } + + public function getName(): string + { + return 'Q'; + } + + public function encodeString(string $string, ?string $charset = 'utf-8', int $firstLineOffset = 0, int $maxLineLength = 0): string + { + return str_replace([' ', '=20', "=\r\n"], ['_', '_', "\r\n"], + parent::encodeString($string, $charset, $firstLineOffset, $maxLineLength) + ); + } +} diff --git a/3rdparty/symfony/mime/Encoder/Rfc2231Encoder.php b/3rdparty/symfony/mime/Encoder/Rfc2231Encoder.php new file mode 100644 index 00000000..4d93dc64 --- /dev/null +++ b/3rdparty/symfony/mime/Encoder/Rfc2231Encoder.php @@ -0,0 +1,50 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Component\Mime\Encoder; + +use Symfony\Component\Mime\CharacterStream; + +/** + * @author Chris Corbyn + */ +final class Rfc2231Encoder implements EncoderInterface +{ + /** + * Takes an unencoded string and produces a string encoded according to RFC 2231 from it. + */ + public function encodeString(string $string, ?string $charset = 'utf-8', int $firstLineOffset = 0, int $maxLineLength = 0): string + { + $lines = []; + $lineCount = 0; + $lines[] = ''; + $currentLine = &$lines[$lineCount++]; + + if (0 >= $maxLineLength) { + $maxLineLength = 75; + } + + $charStream = new CharacterStream($string, $charset); + $thisLineLength = $maxLineLength - $firstLineOffset; + + while (null !== $char = $charStream->read(4)) { + $encodedChar = rawurlencode($char); + if ('' !== $currentLine && \strlen($currentLine.$encodedChar) > $thisLineLength) { + $lines[] = ''; + $currentLine = &$lines[$lineCount++]; + $thisLineLength = $maxLineLength; + } + $currentLine .= $encodedChar; + } + + return implode("\r\n", $lines); + } +} diff --git a/3rdparty/symfony/mime/Exception/AddressEncoderException.php b/3rdparty/symfony/mime/Exception/AddressEncoderException.php new file mode 100644 index 00000000..51ee2e06 --- /dev/null +++ b/3rdparty/symfony/mime/Exception/AddressEncoderException.php @@ -0,0 +1,19 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Component\Mime\Exception; + +/** + * @author Fabien Potencier + */ +class AddressEncoderException extends RfcComplianceException +{ +} diff --git a/3rdparty/symfony/mime/Exception/ExceptionInterface.php b/3rdparty/symfony/mime/Exception/ExceptionInterface.php new file mode 100644 index 00000000..11933900 --- /dev/null +++ b/3rdparty/symfony/mime/Exception/ExceptionInterface.php @@ -0,0 +1,19 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Component\Mime\Exception; + +/** + * @author Fabien Potencier + */ +interface ExceptionInterface extends \Throwable +{ +} diff --git a/3rdparty/symfony/mime/Exception/InvalidArgumentException.php b/3rdparty/symfony/mime/Exception/InvalidArgumentException.php new file mode 100644 index 00000000..e89ebae2 --- /dev/null +++ b/3rdparty/symfony/mime/Exception/InvalidArgumentException.php @@ -0,0 +1,19 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Component\Mime\Exception; + +/** + * @author Fabien Potencier + */ +class InvalidArgumentException extends \InvalidArgumentException implements ExceptionInterface +{ +} diff --git a/3rdparty/symfony/mime/Exception/LogicException.php b/3rdparty/symfony/mime/Exception/LogicException.php new file mode 100644 index 00000000..0508ee73 --- /dev/null +++ b/3rdparty/symfony/mime/Exception/LogicException.php @@ -0,0 +1,19 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Component\Mime\Exception; + +/** + * @author Fabien Potencier + */ +class LogicException extends \LogicException implements ExceptionInterface +{ +} diff --git a/3rdparty/symfony/mime/Exception/RfcComplianceException.php b/3rdparty/symfony/mime/Exception/RfcComplianceException.php new file mode 100644 index 00000000..26e4a509 --- /dev/null +++ b/3rdparty/symfony/mime/Exception/RfcComplianceException.php @@ -0,0 +1,19 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Component\Mime\Exception; + +/** + * @author Fabien Potencier + */ +class RfcComplianceException extends \InvalidArgumentException implements ExceptionInterface +{ +} diff --git a/3rdparty/symfony/mime/Exception/RuntimeException.php b/3rdparty/symfony/mime/Exception/RuntimeException.php new file mode 100644 index 00000000..fb018b00 --- /dev/null +++ b/3rdparty/symfony/mime/Exception/RuntimeException.php @@ -0,0 +1,19 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Component\Mime\Exception; + +/** + * @author Fabien Potencier + */ +class RuntimeException extends \RuntimeException implements ExceptionInterface +{ +} diff --git a/3rdparty/symfony/mime/FileBinaryMimeTypeGuesser.php b/3rdparty/symfony/mime/FileBinaryMimeTypeGuesser.php new file mode 100644 index 00000000..83e2950c --- /dev/null +++ b/3rdparty/symfony/mime/FileBinaryMimeTypeGuesser.php @@ -0,0 +1,87 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Component\Mime; + +use Symfony\Component\Mime\Exception\InvalidArgumentException; +use Symfony\Component\Mime\Exception\LogicException; + +/** + * Guesses the MIME type with the binary "file" (only available on *nix). + * + * @author Bernhard Schussek + */ +class FileBinaryMimeTypeGuesser implements MimeTypeGuesserInterface +{ + private string $cmd; + + /** + * The $cmd pattern must contain a "%s" string that will be replaced + * with the file name to guess. + * + * The command output must start with the MIME type of the file. + * + * @param string $cmd The command to run to get the MIME type of a file + */ + public function __construct(string $cmd = 'file -b --mime -- %s 2>/dev/null') + { + $this->cmd = $cmd; + } + + public function isGuesserSupported(): bool + { + static $supported = null; + + if (null !== $supported) { + return $supported; + } + + if ('\\' === \DIRECTORY_SEPARATOR || !\function_exists('passthru') || !\function_exists('escapeshellarg')) { + return $supported = false; + } + + ob_start(); + passthru('command -v file', $exitStatus); + $binPath = trim(ob_get_clean()); + + return $supported = 0 === $exitStatus && '' !== $binPath; + } + + public function guessMimeType(string $path): ?string + { + if (!is_file($path) || !is_readable($path)) { + throw new InvalidArgumentException(sprintf('The "%s" file does not exist or is not readable.', $path)); + } + + if (!$this->isGuesserSupported()) { + throw new LogicException(sprintf('The "%s" guesser is not supported.', __CLASS__)); + } + + ob_start(); + + // need to use --mime instead of -i. see #6641 + passthru(sprintf($this->cmd, escapeshellarg((str_starts_with($path, '-') ? './' : '').$path)), $return); + if ($return > 0) { + ob_end_clean(); + + return null; + } + + $type = trim(ob_get_clean()); + + if (!preg_match('#^([a-z0-9\-]+/[a-z0-9\-\+\.]+)#i', $type, $match)) { + // it's not a type, but an error message + return null; + } + + return $match[1]; + } +} diff --git a/3rdparty/symfony/mime/FileinfoMimeTypeGuesser.php b/3rdparty/symfony/mime/FileinfoMimeTypeGuesser.php new file mode 100644 index 00000000..776124f8 --- /dev/null +++ b/3rdparty/symfony/mime/FileinfoMimeTypeGuesser.php @@ -0,0 +1,63 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Component\Mime; + +use Symfony\Component\Mime\Exception\InvalidArgumentException; +use Symfony\Component\Mime\Exception\LogicException; + +/** + * Guesses the MIME type using the PECL extension FileInfo. + * + * @author Bernhard Schussek + */ +class FileinfoMimeTypeGuesser implements MimeTypeGuesserInterface +{ + private ?string $magicFile; + + /** + * @param string|null $magicFile A magic file to use with the finfo instance + * + * @see http://www.php.net/manual/en/function.finfo-open.php + */ + public function __construct(?string $magicFile = null) + { + $this->magicFile = $magicFile; + } + + public function isGuesserSupported(): bool + { + return \function_exists('finfo_open'); + } + + public function guessMimeType(string $path): ?string + { + if (!is_file($path) || !is_readable($path)) { + throw new InvalidArgumentException(sprintf('The "%s" file does not exist or is not readable.', $path)); + } + + if (!$this->isGuesserSupported()) { + throw new LogicException(sprintf('The "%s" guesser is not supported.', __CLASS__)); + } + + if (false === $finfo = new \finfo(\FILEINFO_MIME_TYPE, $this->magicFile)) { + return null; + } + $mimeType = $finfo->file($path); + + if ($mimeType && 0 === (\strlen($mimeType) % 2)) { + $mimeStart = substr($mimeType, 0, \strlen($mimeType) >> 1); + $mimeType = $mimeStart.$mimeStart === $mimeType ? $mimeStart : $mimeType; + } + + return $mimeType; + } +} diff --git a/3rdparty/symfony/mime/Header/AbstractHeader.php b/3rdparty/symfony/mime/Header/AbstractHeader.php new file mode 100644 index 00000000..9994ec61 --- /dev/null +++ b/3rdparty/symfony/mime/Header/AbstractHeader.php @@ -0,0 +1,288 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Component\Mime\Header; + +use Symfony\Component\Mime\Encoder\QpMimeHeaderEncoder; + +/** + * An abstract base MIME Header. + * + * @author Chris Corbyn + */ +abstract class AbstractHeader implements HeaderInterface +{ + public const PHRASE_PATTERN = '(?:(?:(?:(?:(?:(?:(?:[ \t]*(?:\r\n))?[ \t])?(\((?:(?:(?:[ \t]*(?:\r\n))?[ \t])|(?:(?:[\x01-\x08\x0B\x0C\x0E-\x19\x7F]|[\x21-\x27\x2A-\x5B\x5D-\x7E])|(?:\\[\x00-\x08\x0B\x0C\x0E-\x7F])|(?1)))*(?:(?:[ \t]*(?:\r\n))?[ \t])?\)))*(?:(?:(?:(?:[ \t]*(?:\r\n))?[ \t])?(\((?:(?:(?:[ \t]*(?:\r\n))?[ \t])|(?:(?:[\x01-\x08\x0B\x0C\x0E-\x19\x7F]|[\x21-\x27\x2A-\x5B\x5D-\x7E])|(?:\\[\x00-\x08\x0B\x0C\x0E-\x7F])|(?1)))*(?:(?:[ \t]*(?:\r\n))?[ \t])?\)))|(?:(?:[ \t]*(?:\r\n))?[ \t])))?[a-zA-Z0-9!#\$%&\'\*\+\-\/=\?\^_`\{\}\|~]+(?:(?:(?:(?:[ \t]*(?:\r\n))?[ \t])?(\((?:(?:(?:[ \t]*(?:\r\n))?[ \t])|(?:(?:[\x01-\x08\x0B\x0C\x0E-\x19\x7F]|[\x21-\x27\x2A-\x5B\x5D-\x7E])|(?:\\[\x00-\x08\x0B\x0C\x0E-\x7F])|(?1)))*(?:(?:[ \t]*(?:\r\n))?[ \t])?\)))*(?:(?:(?:(?:[ \t]*(?:\r\n))?[ \t])?(\((?:(?:(?:[ \t]*(?:\r\n))?[ \t])|(?:(?:[\x01-\x08\x0B\x0C\x0E-\x19\x7F]|[\x21-\x27\x2A-\x5B\x5D-\x7E])|(?:\\[\x00-\x08\x0B\x0C\x0E-\x7F])|(?1)))*(?:(?:[ \t]*(?:\r\n))?[ \t])?\)))|(?:(?:[ \t]*(?:\r\n))?[ \t])))?)|(?:(?:(?:(?:(?:[ \t]*(?:\r\n))?[ \t])?(\((?:(?:(?:[ \t]*(?:\r\n))?[ \t])|(?:(?:[\x01-\x08\x0B\x0C\x0E-\x19\x7F]|[\x21-\x27\x2A-\x5B\x5D-\x7E])|(?:\\[\x00-\x08\x0B\x0C\x0E-\x7F])|(?1)))*(?:(?:[ \t]*(?:\r\n))?[ \t])?\)))*(?:(?:(?:(?:[ \t]*(?:\r\n))?[ \t])?(\((?:(?:(?:[ \t]*(?:\r\n))?[ \t])|(?:(?:[\x01-\x08\x0B\x0C\x0E-\x19\x7F]|[\x21-\x27\x2A-\x5B\x5D-\x7E])|(?:\\[\x00-\x08\x0B\x0C\x0E-\x7F])|(?1)))*(?:(?:[ \t]*(?:\r\n))?[ \t])?\)))|(?:(?:[ \t]*(?:\r\n))?[ \t])))?"((?:(?:[ \t]*(?:\r\n))?[ \t])?(?:(?:[\x01-\x08\x0B\x0C\x0E-\x19\x7F]|[\x21\x23-\x5B\x5D-\x7E])|(?:\\[\x00-\x08\x0B\x0C\x0E-\x7F])))*(?:(?:[ \t]*(?:\r\n))?[ \t])?"(?:(?:(?:(?:[ \t]*(?:\r\n))?[ \t])?(\((?:(?:(?:[ \t]*(?:\r\n))?[ \t])|(?:(?:[\x01-\x08\x0B\x0C\x0E-\x19\x7F]|[\x21-\x27\x2A-\x5B\x5D-\x7E])|(?:\\[\x00-\x08\x0B\x0C\x0E-\x7F])|(?1)))*(?:(?:[ \t]*(?:\r\n))?[ \t])?\)))*(?:(?:(?:(?:[ \t]*(?:\r\n))?[ \t])?(\((?:(?:(?:[ \t]*(?:\r\n))?[ \t])|(?:(?:[\x01-\x08\x0B\x0C\x0E-\x19\x7F]|[\x21-\x27\x2A-\x5B\x5D-\x7E])|(?:\\[\x00-\x08\x0B\x0C\x0E-\x7F])|(?1)))*(?:(?:[ \t]*(?:\r\n))?[ \t])?\)))|(?:(?:[ \t]*(?:\r\n))?[ \t])))?))+?)'; + + private static QpMimeHeaderEncoder $encoder; + + private string $name; + private int $lineLength = 76; + private ?string $lang = null; + private string $charset = 'utf-8'; + + public function __construct(string $name) + { + $this->name = $name; + } + + /** + * @return void + */ + public function setCharset(string $charset) + { + $this->charset = $charset; + } + + public function getCharset(): ?string + { + return $this->charset; + } + + /** + * Set the language used in this Header. + * + * For example, for US English, 'en-us'. + * + * @return void + */ + public function setLanguage(string $lang) + { + $this->lang = $lang; + } + + public function getLanguage(): ?string + { + return $this->lang; + } + + public function getName(): string + { + return $this->name; + } + + /** + * @return void + */ + public function setMaxLineLength(int $lineLength) + { + $this->lineLength = $lineLength; + } + + public function getMaxLineLength(): int + { + return $this->lineLength; + } + + public function toString(): string + { + return $this->tokensToString($this->toTokens()); + } + + /** + * Produces a compliant, formatted RFC 2822 'phrase' based on the string given. + * + * @param string $string as displayed + * @param bool $shorten the first line to make remove for header name + */ + protected function createPhrase(HeaderInterface $header, string $string, string $charset, bool $shorten = false): string + { + // Treat token as exactly what was given + $phraseStr = $string; + + // If it's not valid + if (!preg_match('/^'.self::PHRASE_PATTERN.'$/D', $phraseStr)) { + // .. but it is just ascii text, try escaping some characters + // and make it a quoted-string + if (preg_match('/^[\x00-\x08\x0B\x0C\x0E-\x7F]*$/D', $phraseStr)) { + foreach (['\\', '"'] as $char) { + $phraseStr = str_replace($char, '\\'.$char, $phraseStr); + } + $phraseStr = '"'.$phraseStr.'"'; + } else { + // ... otherwise it needs encoding + // Determine space remaining on line if first line + if ($shorten) { + $usedLength = \strlen($header->getName().': '); + } else { + $usedLength = 0; + } + $phraseStr = $this->encodeWords($header, $string, $usedLength); + } + } elseif (str_contains($phraseStr, '(')) { + foreach (['\\', '"'] as $char) { + $phraseStr = str_replace($char, '\\'.$char, $phraseStr); + } + $phraseStr = '"'.$phraseStr.'"'; + } + + return $phraseStr; + } + + /** + * Encode needed word tokens within a string of input. + */ + protected function encodeWords(HeaderInterface $header, string $input, int $usedLength = -1): string + { + $value = ''; + $tokens = $this->getEncodableWordTokens($input); + foreach ($tokens as $token) { + // See RFC 2822, Sect 2.2 (really 2.2 ??) + if ($this->tokenNeedsEncoding($token)) { + // Don't encode starting WSP + $firstChar = substr($token, 0, 1); + switch ($firstChar) { + case ' ': + case "\t": + $value .= $firstChar; + $token = substr($token, 1); + } + + if (-1 == $usedLength) { + $usedLength = \strlen($header->getName().': ') + \strlen($value); + } + $value .= $this->getTokenAsEncodedWord($token, $usedLength); + } else { + $value .= $token; + } + } + + return $value; + } + + protected function tokenNeedsEncoding(string $token): bool + { + return (bool) preg_match('~[\x00-\x08\x10-\x19\x7F-\xFF\r\n]~', $token); + } + + /** + * Splits a string into tokens in blocks of words which can be encoded quickly. + * + * @return string[] + */ + protected function getEncodableWordTokens(string $string): array + { + $tokens = []; + $encodedToken = ''; + // Split at all whitespace boundaries + foreach (preg_split('~(?=[\t ])~', $string) as $token) { + if ($this->tokenNeedsEncoding($token)) { + $encodedToken .= $token; + } else { + if ('' !== $encodedToken) { + $tokens[] = $encodedToken; + $encodedToken = ''; + } + $tokens[] = $token; + } + } + if ('' !== $encodedToken) { + $tokens[] = $encodedToken; + } + + return $tokens; + } + + /** + * Get a token as an encoded word for safe insertion into headers. + */ + protected function getTokenAsEncodedWord(string $token, int $firstLineOffset = 0): string + { + self::$encoder ??= new QpMimeHeaderEncoder(); + + // Adjust $firstLineOffset to account for space needed for syntax + $charsetDecl = $this->charset; + if (null !== $this->lang) { + $charsetDecl .= '*'.$this->lang; + } + $encodingWrapperLength = \strlen('=?'.$charsetDecl.'?'.self::$encoder->getName().'??='); + + if ($firstLineOffset >= 75) { + // Does this logic need to be here? + $firstLineOffset = 0; + } + + $encodedTextLines = explode("\r\n", + self::$encoder->encodeString($token, $this->charset, $firstLineOffset, 75 - $encodingWrapperLength) + ); + + if ('iso-2022-jp' !== strtolower($this->charset)) { + // special encoding for iso-2022-jp using mb_encode_mimeheader + foreach ($encodedTextLines as $lineNum => $line) { + $encodedTextLines[$lineNum] = '=?'.$charsetDecl.'?'.self::$encoder->getName().'?'.$line.'?='; + } + } + + return implode("\r\n ", $encodedTextLines); + } + + /** + * Generates tokens from the given string which include CRLF as individual tokens. + * + * @return string[] + */ + protected function generateTokenLines(string $token): array + { + return preg_split('~(\r\n)~', $token, -1, \PREG_SPLIT_DELIM_CAPTURE); + } + + /** + * Generate a list of all tokens in the final header. + */ + protected function toTokens(?string $string = null): array + { + $string ??= $this->getBodyAsString(); + + $tokens = []; + // Generate atoms; split at all invisible boundaries followed by WSP + foreach (preg_split('~(?=[ \t])~', $string) as $token) { + $newTokens = $this->generateTokenLines($token); + foreach ($newTokens as $newToken) { + $tokens[] = $newToken; + } + } + + return $tokens; + } + + /** + * Takes an array of tokens which appear in the header and turns them into + * an RFC 2822 compliant string, adding FWSP where needed. + * + * @param string[] $tokens + */ + private function tokensToString(array $tokens): string + { + $lineCount = 0; + $headerLines = []; + $headerLines[] = $this->name.': '; + $currentLine = &$headerLines[$lineCount++]; + + // Build all tokens back into compliant header + foreach ($tokens as $i => $token) { + // Line longer than specified maximum or token was just a new line + if (("\r\n" === $token) + || ($i > 0 && \strlen($currentLine.$token) > $this->lineLength) + && '' !== $currentLine) { + $headerLines[] = ''; + $currentLine = &$headerLines[$lineCount++]; + } + + // Append token to the line + if ("\r\n" !== $token) { + $currentLine .= $token; + } + } + + // Implode with FWS (RFC 2822, 2.2.3) + return implode("\r\n", $headerLines); + } +} diff --git a/3rdparty/symfony/mime/Header/DateHeader.php b/3rdparty/symfony/mime/Header/DateHeader.php new file mode 100644 index 00000000..2b361802 --- /dev/null +++ b/3rdparty/symfony/mime/Header/DateHeader.php @@ -0,0 +1,62 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Component\Mime\Header; + +/** + * A Date MIME Header. + * + * @author Chris Corbyn + */ +final class DateHeader extends AbstractHeader +{ + private \DateTimeImmutable $dateTime; + + public function __construct(string $name, \DateTimeInterface $date) + { + parent::__construct($name); + + $this->setDateTime($date); + } + + /** + * @param \DateTimeInterface $body + */ + public function setBody(mixed $body): void + { + $this->setDateTime($body); + } + + public function getBody(): \DateTimeImmutable + { + return $this->getDateTime(); + } + + public function getDateTime(): \DateTimeImmutable + { + return $this->dateTime; + } + + /** + * Set the date-time of the Date in this Header. + * + * If a DateTime instance is provided, it is converted to DateTimeImmutable. + */ + public function setDateTime(\DateTimeInterface $dateTime): void + { + $this->dateTime = \DateTimeImmutable::createFromInterface($dateTime); + } + + public function getBodyAsString(): string + { + return $this->dateTime->format(\DateTimeInterface::RFC2822); + } +} diff --git a/3rdparty/symfony/mime/Header/HeaderInterface.php b/3rdparty/symfony/mime/Header/HeaderInterface.php new file mode 100644 index 00000000..5bc4162c --- /dev/null +++ b/3rdparty/symfony/mime/Header/HeaderInterface.php @@ -0,0 +1,72 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Component\Mime\Header; + +/** + * A MIME Header. + * + * @author Chris Corbyn + */ +interface HeaderInterface +{ + /** + * Sets the body. + * + * The type depends on the Header concrete class. + * + * @return void + */ + public function setBody(mixed $body); + + /** + * Gets the body. + * + * The return type depends on the Header concrete class. + */ + public function getBody(): mixed; + + /** + * @return void + */ + public function setCharset(string $charset); + + public function getCharset(): ?string; + + /** + * @return void + */ + public function setLanguage(string $lang); + + public function getLanguage(): ?string; + + public function getName(): string; + + /** + * @return void + */ + public function setMaxLineLength(int $lineLength); + + public function getMaxLineLength(): int; + + /** + * Gets this Header rendered as a compliant string. + */ + public function toString(): string; + + /** + * Gets the header's body, prepared for folding into a final header value. + * + * This is not necessarily RFC 2822 compliant since folding white space is + * not added at this stage (see {@link toString()} for that). + */ + public function getBodyAsString(): string; +} diff --git a/3rdparty/symfony/mime/Header/Headers.php b/3rdparty/symfony/mime/Header/Headers.php new file mode 100644 index 00000000..164f4ac3 --- /dev/null +++ b/3rdparty/symfony/mime/Header/Headers.php @@ -0,0 +1,316 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Component\Mime\Header; + +use Symfony\Component\Mime\Address; +use Symfony\Component\Mime\Exception\LogicException; + +/** + * A collection of headers. + * + * @author Fabien Potencier + */ +final class Headers +{ + private const UNIQUE_HEADERS = [ + 'date', 'from', 'sender', 'reply-to', 'to', 'cc', 'bcc', + 'message-id', 'in-reply-to', 'references', 'subject', + ]; + private const HEADER_CLASS_MAP = [ + 'date' => DateHeader::class, + 'from' => MailboxListHeader::class, + 'sender' => MailboxHeader::class, + 'reply-to' => MailboxListHeader::class, + 'to' => MailboxListHeader::class, + 'cc' => MailboxListHeader::class, + 'bcc' => MailboxListHeader::class, + 'message-id' => IdentificationHeader::class, + 'in-reply-to' => [UnstructuredHeader::class, IdentificationHeader::class], // `In-Reply-To` and `References` are less strict than RFC 2822 (3.6.4) to allow users entering the original email's ... + 'references' => [UnstructuredHeader::class, IdentificationHeader::class], // ... `Message-ID`, even if that is no valid `msg-id` + 'return-path' => PathHeader::class, + ]; + + /** + * @var HeaderInterface[][] + */ + private array $headers = []; + private int $lineLength = 76; + + public function __construct(HeaderInterface ...$headers) + { + foreach ($headers as $header) { + $this->add($header); + } + } + + public function __clone() + { + foreach ($this->headers as $name => $collection) { + foreach ($collection as $i => $header) { + $this->headers[$name][$i] = clone $header; + } + } + } + + public function setMaxLineLength(int $lineLength): void + { + $this->lineLength = $lineLength; + foreach ($this->all() as $header) { + $header->setMaxLineLength($lineLength); + } + } + + public function getMaxLineLength(): int + { + return $this->lineLength; + } + + /** + * @param array $addresses + * + * @return $this + */ + public function addMailboxListHeader(string $name, array $addresses): static + { + return $this->add(new MailboxListHeader($name, Address::createArray($addresses))); + } + + /** + * @return $this + */ + public function addMailboxHeader(string $name, Address|string $address): static + { + return $this->add(new MailboxHeader($name, Address::create($address))); + } + + /** + * @return $this + */ + public function addIdHeader(string $name, string|array $ids): static + { + return $this->add(new IdentificationHeader($name, $ids)); + } + + /** + * @return $this + */ + public function addPathHeader(string $name, Address|string $path): static + { + return $this->add(new PathHeader($name, $path instanceof Address ? $path : new Address($path))); + } + + /** + * @return $this + */ + public function addDateHeader(string $name, \DateTimeInterface $dateTime): static + { + return $this->add(new DateHeader($name, $dateTime)); + } + + /** + * @return $this + */ + public function addTextHeader(string $name, string $value): static + { + return $this->add(new UnstructuredHeader($name, $value)); + } + + /** + * @return $this + */ + public function addParameterizedHeader(string $name, string $value, array $params = []): static + { + return $this->add(new ParameterizedHeader($name, $value, $params)); + } + + /** + * @return $this + */ + public function addHeader(string $name, mixed $argument, array $more = []): static + { + $headerClass = self::HEADER_CLASS_MAP[strtolower($name)] ?? UnstructuredHeader::class; + if (\is_array($headerClass)) { + $headerClass = $headerClass[0]; + } + $parts = explode('\\', $headerClass); + $method = 'add'.ucfirst(array_pop($parts)); + if ('addUnstructuredHeader' === $method) { + $method = 'addTextHeader'; + } elseif ('addIdentificationHeader' === $method) { + $method = 'addIdHeader'; + } elseif ('addMailboxListHeader' === $method && !\is_array($argument)) { + $argument = [$argument]; + } + + return $this->$method($name, $argument, $more); + } + + public function has(string $name): bool + { + return isset($this->headers[strtolower($name)]); + } + + /** + * @return $this + */ + public function add(HeaderInterface $header): static + { + self::checkHeaderClass($header); + + $header->setMaxLineLength($this->lineLength); + $name = strtolower($header->getName()); + + if (\in_array($name, self::UNIQUE_HEADERS, true) && isset($this->headers[$name]) && \count($this->headers[$name]) > 0) { + throw new LogicException(sprintf('Impossible to set header "%s" as it\'s already defined and must be unique.', $header->getName())); + } + + $this->headers[$name][] = $header; + + return $this; + } + + public function get(string $name): ?HeaderInterface + { + $name = strtolower($name); + if (!isset($this->headers[$name])) { + return null; + } + + $values = array_values($this->headers[$name]); + + return array_shift($values); + } + + public function all(?string $name = null): iterable + { + if (null === $name) { + foreach ($this->headers as $name => $collection) { + foreach ($collection as $header) { + yield $name => $header; + } + } + } elseif (isset($this->headers[strtolower($name)])) { + foreach ($this->headers[strtolower($name)] as $header) { + yield $header; + } + } + } + + public function getNames(): array + { + return array_keys($this->headers); + } + + public function remove(string $name): void + { + unset($this->headers[strtolower($name)]); + } + + public static function isUniqueHeader(string $name): bool + { + return \in_array(strtolower($name), self::UNIQUE_HEADERS, true); + } + + /** + * @throws LogicException if the header name and class are not compatible + */ + public static function checkHeaderClass(HeaderInterface $header): void + { + $name = strtolower($header->getName()); + $headerClasses = self::HEADER_CLASS_MAP[$name] ?? []; + if (!\is_array($headerClasses)) { + $headerClasses = [$headerClasses]; + } + + if (!$headerClasses) { + return; + } + + foreach ($headerClasses as $c) { + if ($header instanceof $c) { + return; + } + } + + throw new LogicException(sprintf('The "%s" header must be an instance of "%s" (got "%s").', $header->getName(), implode('" or "', $headerClasses), get_debug_type($header))); + } + + public function toString(): string + { + $string = ''; + foreach ($this->toArray() as $str) { + $string .= $str."\r\n"; + } + + return $string; + } + + public function toArray(): array + { + $arr = []; + foreach ($this->all() as $header) { + if ('' !== $header->getBodyAsString()) { + $arr[] = $header->toString(); + } + } + + return $arr; + } + + public function getHeaderBody(string $name): mixed + { + return $this->has($name) ? $this->get($name)->getBody() : null; + } + + /** + * @internal + */ + public function setHeaderBody(string $type, string $name, mixed $body): void + { + if ($this->has($name)) { + $this->get($name)->setBody($body); + } else { + $this->{'add'.$type.'Header'}($name, $body); + } + } + + public function getHeaderParameter(string $name, string $parameter): ?string + { + if (!$this->has($name)) { + return null; + } + + $header = $this->get($name); + if (!$header instanceof ParameterizedHeader) { + throw new LogicException(sprintf('Unable to get parameter "%s" on header "%s" as the header is not of class "%s".', $parameter, $name, ParameterizedHeader::class)); + } + + return $header->getParameter($parameter); + } + + /** + * @internal + */ + public function setHeaderParameter(string $name, string $parameter, ?string $value): void + { + if (!$this->has($name)) { + throw new LogicException(sprintf('Unable to set parameter "%s" on header "%s" as the header is not defined.', $parameter, $name)); + } + + $header = $this->get($name); + if (!$header instanceof ParameterizedHeader) { + throw new LogicException(sprintf('Unable to set parameter "%s" on header "%s" as the header is not of class "%s".', $parameter, $name, ParameterizedHeader::class)); + } + + $header->setParameter($parameter, $value); + } +} diff --git a/3rdparty/symfony/mime/Header/IdentificationHeader.php b/3rdparty/symfony/mime/Header/IdentificationHeader.php new file mode 100644 index 00000000..14e18bf2 --- /dev/null +++ b/3rdparty/symfony/mime/Header/IdentificationHeader.php @@ -0,0 +1,107 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Component\Mime\Header; + +use Symfony\Component\Mime\Address; +use Symfony\Component\Mime\Exception\RfcComplianceException; + +/** + * An ID MIME Header for something like Message-ID or Content-ID (one or more addresses). + * + * @author Chris Corbyn + */ +final class IdentificationHeader extends AbstractHeader +{ + private array $ids = []; + private array $idsAsAddresses = []; + + public function __construct(string $name, string|array $ids) + { + parent::__construct($name); + + $this->setId($ids); + } + + /** + * @param string|string[] $body a string ID or an array of IDs + * + * @throws RfcComplianceException + */ + public function setBody(mixed $body): void + { + $this->setId($body); + } + + public function getBody(): array + { + return $this->getIds(); + } + + /** + * Set the ID used in the value of this header. + * + * @param string|string[] $id + * + * @throws RfcComplianceException + */ + public function setId(string|array $id): void + { + $this->setIds(\is_array($id) ? $id : [$id]); + } + + /** + * Get the ID used in the value of this Header. + * + * If multiple IDs are set only the first is returned. + */ + public function getId(): ?string + { + return $this->ids[0] ?? null; + } + + /** + * Set a collection of IDs to use in the value of this Header. + * + * @param string[] $ids + * + * @throws RfcComplianceException + */ + public function setIds(array $ids): void + { + $this->ids = []; + $this->idsAsAddresses = []; + foreach ($ids as $id) { + $this->idsAsAddresses[] = new Address($id); + $this->ids[] = $id; + } + } + + /** + * Get the list of IDs used in this Header. + * + * @return string[] + */ + public function getIds(): array + { + return $this->ids; + } + + public function getBodyAsString(): string + { + $addrs = []; + foreach ($this->idsAsAddresses as $address) { + $addrs[] = '<'.$address->toString().'>'; + } + + return implode(' ', $addrs); + } +} diff --git a/3rdparty/symfony/mime/Header/MailboxHeader.php b/3rdparty/symfony/mime/Header/MailboxHeader.php new file mode 100644 index 00000000..8ba964b5 --- /dev/null +++ b/3rdparty/symfony/mime/Header/MailboxHeader.php @@ -0,0 +1,85 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Component\Mime\Header; + +use Symfony\Component\Mime\Address; +use Symfony\Component\Mime\Exception\RfcComplianceException; + +/** + * A Mailbox MIME Header for something like Sender (one named address). + * + * @author Fabien Potencier + */ +final class MailboxHeader extends AbstractHeader +{ + private Address $address; + + public function __construct(string $name, Address $address) + { + parent::__construct($name); + + $this->setAddress($address); + } + + /** + * @param Address $body + * + * @throws RfcComplianceException + */ + public function setBody(mixed $body): void + { + $this->setAddress($body); + } + + /** + * @throws RfcComplianceException + */ + public function getBody(): Address + { + return $this->getAddress(); + } + + /** + * @throws RfcComplianceException + */ + public function setAddress(Address $address): void + { + $this->address = $address; + } + + public function getAddress(): Address + { + return $this->address; + } + + public function getBodyAsString(): string + { + $str = $this->address->getEncodedAddress(); + if ($name = $this->address->getName()) { + $str = $this->createPhrase($this, $name, $this->getCharset(), true).' <'.$str.'>'; + } + + return $str; + } + + /** + * Redefine the encoding requirements for an address. + * + * All "specials" must be encoded as the full header value will not be quoted + * + * @see RFC 2822 3.2.1 + */ + protected function tokenNeedsEncoding(string $token): bool + { + return preg_match('/[()<>\[\]:;@\,."]/', $token) || parent::tokenNeedsEncoding($token); + } +} diff --git a/3rdparty/symfony/mime/Header/MailboxListHeader.php b/3rdparty/symfony/mime/Header/MailboxListHeader.php new file mode 100644 index 00000000..8d902fb7 --- /dev/null +++ b/3rdparty/symfony/mime/Header/MailboxListHeader.php @@ -0,0 +1,136 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Component\Mime\Header; + +use Symfony\Component\Mime\Address; +use Symfony\Component\Mime\Exception\RfcComplianceException; + +/** + * A Mailbox list MIME Header for something like From, To, Cc, and Bcc (one or more named addresses). + * + * @author Chris Corbyn + */ +final class MailboxListHeader extends AbstractHeader +{ + private array $addresses = []; + + /** + * @param Address[] $addresses + */ + public function __construct(string $name, array $addresses) + { + parent::__construct($name); + + $this->setAddresses($addresses); + } + + /** + * @param Address[] $body + * + * @throws RfcComplianceException + */ + public function setBody(mixed $body): void + { + $this->setAddresses($body); + } + + /** + * @return Address[] + * + * @throws RfcComplianceException + */ + public function getBody(): array + { + return $this->getAddresses(); + } + + /** + * Sets a list of addresses to be shown in this Header. + * + * @param Address[] $addresses + * + * @throws RfcComplianceException + */ + public function setAddresses(array $addresses): void + { + $this->addresses = []; + $this->addAddresses($addresses); + } + + /** + * Sets a list of addresses to be shown in this Header. + * + * @param Address[] $addresses + * + * @throws RfcComplianceException + */ + public function addAddresses(array $addresses): void + { + foreach ($addresses as $address) { + $this->addAddress($address); + } + } + + /** + * @throws RfcComplianceException + */ + public function addAddress(Address $address): void + { + $this->addresses[] = $address; + } + + /** + * @return Address[] + */ + public function getAddresses(): array + { + return $this->addresses; + } + + /** + * Gets the full mailbox list of this Header as an array of valid RFC 2822 strings. + * + * @return string[] + * + * @throws RfcComplianceException + */ + public function getAddressStrings(): array + { + $strings = []; + foreach ($this->addresses as $address) { + $str = $address->getEncodedAddress(); + if ($name = $address->getName()) { + $str = $this->createPhrase($this, $name, $this->getCharset(), !$strings).' <'.$str.'>'; + } + $strings[] = $str; + } + + return $strings; + } + + public function getBodyAsString(): string + { + return implode(', ', $this->getAddressStrings()); + } + + /** + * Redefine the encoding requirements for addresses. + * + * All "specials" must be encoded as the full header value will not be quoted + * + * @see RFC 2822 3.2.1 + */ + protected function tokenNeedsEncoding(string $token): bool + { + return preg_match('/[()<>\[\]:;@\,."]/', $token) || parent::tokenNeedsEncoding($token); + } +} diff --git a/3rdparty/symfony/mime/Header/ParameterizedHeader.php b/3rdparty/symfony/mime/Header/ParameterizedHeader.php new file mode 100644 index 00000000..5ef4f212 --- /dev/null +++ b/3rdparty/symfony/mime/Header/ParameterizedHeader.php @@ -0,0 +1,191 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Component\Mime\Header; + +use Symfony\Component\Mime\Encoder\Rfc2231Encoder; + +/** + * @author Chris Corbyn + */ +final class ParameterizedHeader extends UnstructuredHeader +{ + /** + * RFC 2231's definition of a token. + * + * @var string + */ + public const TOKEN_REGEX = '(?:[\x21\x23-\x27\x2A\x2B\x2D\x2E\x30-\x39\x41-\x5A\x5E-\x7E]+)'; + + private ?Rfc2231Encoder $encoder = null; + private array $parameters = []; + + public function __construct(string $name, string $value, array $parameters = []) + { + parent::__construct($name, $value); + + foreach ($parameters as $k => $v) { + $this->setParameter($k, $v); + } + + if ('content-type' !== strtolower($name)) { + $this->encoder = new Rfc2231Encoder(); + } + } + + public function setParameter(string $parameter, ?string $value): void + { + $this->setParameters(array_merge($this->getParameters(), [$parameter => $value])); + } + + public function getParameter(string $parameter): string + { + return $this->getParameters()[$parameter] ?? ''; + } + + /** + * @param string[] $parameters + */ + public function setParameters(array $parameters): void + { + $this->parameters = $parameters; + } + + /** + * @return string[] + */ + public function getParameters(): array + { + return $this->parameters; + } + + public function getBodyAsString(): string + { + $body = parent::getBodyAsString(); + foreach ($this->parameters as $name => $value) { + if (null !== $value) { + $body .= '; '.$this->createParameter($name, $value); + } + } + + return $body; + } + + /** + * Generate a list of all tokens in the final header. + * + * This doesn't need to be overridden in theory, but it is for implementation + * reasons to prevent potential breakage of attributes. + */ + protected function toTokens(?string $string = null): array + { + $tokens = parent::toTokens(parent::getBodyAsString()); + + // Try creating any parameters + foreach ($this->parameters as $name => $value) { + if (null !== $value) { + // Add the semi-colon separator + $tokens[\count($tokens) - 1] .= ';'; + $tokens = array_merge($tokens, $this->generateTokenLines(' '.$this->createParameter($name, $value))); + } + } + + return $tokens; + } + + /** + * Render an RFC 2047 compliant header parameter from the $name and $value. + */ + private function createParameter(string $name, string $value): string + { + $origValue = $value; + + $encoded = false; + // Allow room for parameter name, indices, "=" and DQUOTEs + $maxValueLength = $this->getMaxLineLength() - \strlen($name.'=*N"";') - 1; + $firstLineOffset = 0; + + // If it's not already a valid parameter value... + if (!preg_match('/^'.self::TOKEN_REGEX.'$/D', $value)) { + // TODO: text, or something else?? + // ... and it's not ascii + if (!preg_match('/^[\x00-\x08\x0B\x0C\x0E-\x7F]*$/D', $value)) { + $encoded = true; + // Allow space for the indices, charset and language + $maxValueLength = $this->getMaxLineLength() - \strlen($name.'*N*="";') - 1; + $firstLineOffset = \strlen($this->getCharset()."'".$this->getLanguage()."'"); + } + + if (\in_array($name, ['name', 'filename'], true) && 'form-data' === $this->getValue() && 'content-disposition' === strtolower($this->getName()) && preg_match('//u', $value)) { + // WHATWG HTML living standard 4.10.21.8 2 specifies: + // For field names and filenames for file fields, the result of the + // encoding in the previous bullet point must be escaped by replacing + // any 0x0A (LF) bytes with the byte sequence `%0A`, 0x0D (CR) with `%0D` + // and 0x22 (") with `%22`. + // The user agent must not perform any other escapes. + $value = str_replace(['"', "\r", "\n"], ['%22', '%0D', '%0A'], $value); + + if (\strlen($value) <= $maxValueLength) { + return $name.'="'.$value.'"'; + } + + $value = $origValue; + } + } + + // Encode if we need to + if ($encoded || \strlen($value) > $maxValueLength) { + if (null !== $this->encoder) { + $value = $this->encoder->encodeString($origValue, $this->getCharset(), $firstLineOffset, $maxValueLength); + } else { + // We have to go against RFC 2183/2231 in some areas for interoperability + $value = $this->getTokenAsEncodedWord($origValue); + $encoded = false; + } + } + + $valueLines = $this->encoder ? explode("\r\n", $value) : [$value]; + + // Need to add indices + if (\count($valueLines) > 1) { + $paramLines = []; + foreach ($valueLines as $i => $line) { + $paramLines[] = $name.'*'.$i.$this->getEndOfParameterValue($line, true, 0 === $i); + } + + return implode(";\r\n ", $paramLines); + } else { + return $name.$this->getEndOfParameterValue($valueLines[0], $encoded, true); + } + } + + /** + * Returns the parameter value from the "=" and beyond. + * + * @param string $value to append + */ + private function getEndOfParameterValue(string $value, bool $encoded = false, bool $firstLine = false): string + { + $forceHttpQuoting = 'form-data' === $this->getValue() && 'content-disposition' === strtolower($this->getName()); + if ($forceHttpQuoting || !preg_match('/^'.self::TOKEN_REGEX.'$/D', $value)) { + $value = '"'.$value.'"'; + } + $prepend = '='; + if ($encoded) { + $prepend = '*='; + if ($firstLine) { + $prepend = '*='.$this->getCharset()."'".$this->getLanguage()."'"; + } + } + + return $prepend.$value; + } +} diff --git a/3rdparty/symfony/mime/Header/PathHeader.php b/3rdparty/symfony/mime/Header/PathHeader.php new file mode 100644 index 00000000..63eb30af --- /dev/null +++ b/3rdparty/symfony/mime/Header/PathHeader.php @@ -0,0 +1,62 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Component\Mime\Header; + +use Symfony\Component\Mime\Address; +use Symfony\Component\Mime\Exception\RfcComplianceException; + +/** + * A Path Header, such a Return-Path (one address). + * + * @author Chris Corbyn + */ +final class PathHeader extends AbstractHeader +{ + private Address $address; + + public function __construct(string $name, Address $address) + { + parent::__construct($name); + + $this->setAddress($address); + } + + /** + * @param Address $body + * + * @throws RfcComplianceException + */ + public function setBody(mixed $body): void + { + $this->setAddress($body); + } + + public function getBody(): Address + { + return $this->getAddress(); + } + + public function setAddress(Address $address): void + { + $this->address = $address; + } + + public function getAddress(): Address + { + return $this->address; + } + + public function getBodyAsString(): string + { + return '<'.$this->address->toString().'>'; + } +} diff --git a/3rdparty/symfony/mime/Header/UnstructuredHeader.php b/3rdparty/symfony/mime/Header/UnstructuredHeader.php new file mode 100644 index 00000000..61c06d8f --- /dev/null +++ b/3rdparty/symfony/mime/Header/UnstructuredHeader.php @@ -0,0 +1,70 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Component\Mime\Header; + +/** + * A Simple MIME Header. + * + * @author Chris Corbyn + */ +class UnstructuredHeader extends AbstractHeader +{ + private string $value; + + public function __construct(string $name, string $value) + { + parent::__construct($name); + + $this->setValue($value); + } + + /** + * @param string $body + * + * @return void + */ + public function setBody(mixed $body) + { + $this->setValue($body); + } + + public function getBody(): string + { + return $this->getValue(); + } + + /** + * Get the (unencoded) value of this header. + */ + public function getValue(): string + { + return $this->value; + } + + /** + * Set the (unencoded) value of this header. + * + * @return void + */ + public function setValue(string $value) + { + $this->value = $value; + } + + /** + * Get the value of this header prepared for rendering. + */ + public function getBodyAsString(): string + { + return $this->encodeWords($this, $this->value); + } +} diff --git a/3rdparty/symfony/mime/HtmlToTextConverter/DefaultHtmlToTextConverter.php b/3rdparty/symfony/mime/HtmlToTextConverter/DefaultHtmlToTextConverter.php new file mode 100644 index 00000000..2aaf8e6c --- /dev/null +++ b/3rdparty/symfony/mime/HtmlToTextConverter/DefaultHtmlToTextConverter.php @@ -0,0 +1,23 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Component\Mime\HtmlToTextConverter; + +/** + * @author Fabien Potencier + */ +class DefaultHtmlToTextConverter implements HtmlToTextConverterInterface +{ + public function convert(string $html, string $charset): string + { + return strip_tags(preg_replace('{<(head|style)\b.*?}is', '', $html)); + } +} diff --git a/3rdparty/symfony/mime/HtmlToTextConverter/HtmlToTextConverterInterface.php b/3rdparty/symfony/mime/HtmlToTextConverter/HtmlToTextConverterInterface.php new file mode 100644 index 00000000..696f37cc --- /dev/null +++ b/3rdparty/symfony/mime/HtmlToTextConverter/HtmlToTextConverterInterface.php @@ -0,0 +1,25 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Component\Mime\HtmlToTextConverter; + +/** + * @author Fabien Potencier + */ +interface HtmlToTextConverterInterface +{ + /** + * Converts an HTML representation of a Message to a text representation. + * + * The output must use the same charset as the HTML one. + */ + public function convert(string $html, string $charset): string; +} diff --git a/3rdparty/symfony/mime/HtmlToTextConverter/LeagueHtmlToMarkdownConverter.php b/3rdparty/symfony/mime/HtmlToTextConverter/LeagueHtmlToMarkdownConverter.php new file mode 100644 index 00000000..253a7b19 --- /dev/null +++ b/3rdparty/symfony/mime/HtmlToTextConverter/LeagueHtmlToMarkdownConverter.php @@ -0,0 +1,35 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Component\Mime\HtmlToTextConverter; + +use League\HTMLToMarkdown\HtmlConverter; +use League\HTMLToMarkdown\HtmlConverterInterface; + +/** + * @author Fabien Potencier + */ +class LeagueHtmlToMarkdownConverter implements HtmlToTextConverterInterface +{ + public function __construct( + private HtmlConverterInterface $converter = new HtmlConverter([ + 'hard_break' => true, + 'strip_tags' => true, + 'remove_nodes' => 'head style', + ]), + ) { + } + + public function convert(string $html, string $charset): string + { + return $this->converter->convert($html); + } +} diff --git a/3rdparty/symfony/mime/LICENSE b/3rdparty/symfony/mime/LICENSE new file mode 100644 index 00000000..4dd83ce0 --- /dev/null +++ b/3rdparty/symfony/mime/LICENSE @@ -0,0 +1,19 @@ +Copyright (c) 2010-present Fabien Potencier + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is furnished +to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +THE SOFTWARE. diff --git a/3rdparty/symfony/mime/Message.php b/3rdparty/symfony/mime/Message.php new file mode 100644 index 00000000..fc8940eb --- /dev/null +++ b/3rdparty/symfony/mime/Message.php @@ -0,0 +1,169 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Component\Mime; + +use Symfony\Component\Mime\Exception\LogicException; +use Symfony\Component\Mime\Header\Headers; +use Symfony\Component\Mime\Part\AbstractPart; +use Symfony\Component\Mime\Part\TextPart; + +/** + * @author Fabien Potencier + */ +class Message extends RawMessage +{ + private Headers $headers; + private ?AbstractPart $body; + + public function __construct(?Headers $headers = null, ?AbstractPart $body = null) + { + $this->headers = $headers ? clone $headers : new Headers(); + $this->body = $body; + } + + public function __clone() + { + $this->headers = clone $this->headers; + + if (null !== $this->body) { + $this->body = clone $this->body; + } + } + + /** + * @return $this + */ + public function setBody(?AbstractPart $body = null): static + { + if (1 > \func_num_args()) { + trigger_deprecation('symfony/mime', '6.2', 'Calling "%s()" without any arguments is deprecated, pass null explicitly instead.', __METHOD__); + } + $this->body = $body; + + return $this; + } + + public function getBody(): ?AbstractPart + { + return $this->body; + } + + /** + * @return $this + */ + public function setHeaders(Headers $headers): static + { + $this->headers = $headers; + + return $this; + } + + public function getHeaders(): Headers + { + return $this->headers; + } + + public function getPreparedHeaders(): Headers + { + $headers = clone $this->headers; + + if (!$headers->has('From')) { + if (!$headers->has('Sender')) { + throw new LogicException('An email must have a "From" or a "Sender" header.'); + } + $headers->addMailboxListHeader('From', [$headers->get('Sender')->getAddress()]); + } + + if (!$headers->has('MIME-Version')) { + $headers->addTextHeader('MIME-Version', '1.0'); + } + + if (!$headers->has('Date')) { + $headers->addDateHeader('Date', new \DateTimeImmutable()); + } + + // determine the "real" sender + if (!$headers->has('Sender') && \count($froms = $headers->get('From')->getAddresses()) > 1) { + $headers->addMailboxHeader('Sender', $froms[0]); + } + + if (!$headers->has('Message-ID')) { + $headers->addIdHeader('Message-ID', $this->generateMessageId()); + } + + // remove the Bcc field which should NOT be part of the sent message + $headers->remove('Bcc'); + + return $headers; + } + + public function toString(): string + { + if (null === $body = $this->getBody()) { + $body = new TextPart(''); + } + + return $this->getPreparedHeaders()->toString().$body->toString(); + } + + public function toIterable(): iterable + { + if (null === $body = $this->getBody()) { + $body = new TextPart(''); + } + + yield $this->getPreparedHeaders()->toString(); + yield from $body->toIterable(); + } + + /** + * @return void + */ + public function ensureValidity() + { + if (!$this->headers->get('To')?->getBody() && !$this->headers->get('Cc')?->getBody() && !$this->headers->get('Bcc')?->getBody()) { + throw new LogicException('An email must have a "To", "Cc", or "Bcc" header.'); + } + + if (!$this->headers->get('From')?->getBody() && !$this->headers->get('Sender')?->getBody()) { + throw new LogicException('An email must have a "From" or a "Sender" header.'); + } + + parent::ensureValidity(); + } + + public function generateMessageId(): string + { + if ($this->headers->has('Sender')) { + $sender = $this->headers->get('Sender')->getAddress(); + } elseif ($this->headers->has('From')) { + if (!$froms = $this->headers->get('From')->getAddresses()) { + throw new LogicException('A "From" header must have at least one email address.'); + } + $sender = $froms[0]; + } else { + throw new LogicException('An email must have a "From" or a "Sender" header.'); + } + + return bin2hex(random_bytes(16)).strstr($sender->getAddress(), '@'); + } + + public function __serialize(): array + { + return [$this->headers, $this->body]; + } + + public function __unserialize(array $data): void + { + [$this->headers, $this->body] = $data; + } +} diff --git a/3rdparty/symfony/mime/MessageConverter.php b/3rdparty/symfony/mime/MessageConverter.php new file mode 100644 index 00000000..bdce921a --- /dev/null +++ b/3rdparty/symfony/mime/MessageConverter.php @@ -0,0 +1,122 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Component\Mime; + +use Symfony\Component\Mime\Exception\RuntimeException; +use Symfony\Component\Mime\Part\DataPart; +use Symfony\Component\Mime\Part\Multipart\AlternativePart; +use Symfony\Component\Mime\Part\Multipart\MixedPart; +use Symfony\Component\Mime\Part\Multipart\RelatedPart; +use Symfony\Component\Mime\Part\TextPart; + +/** + * @author Fabien Potencier + */ +final class MessageConverter +{ + /** + * @throws RuntimeException when unable to convert the message to an email + */ + public static function toEmail(Message $message): Email + { + if ($message instanceof Email) { + return $message; + } + + // try to convert to a "simple" Email instance + $body = $message->getBody(); + if ($body instanceof TextPart) { + return self::createEmailFromTextPart($message, $body); + } + + if ($body instanceof AlternativePart) { + return self::createEmailFromAlternativePart($message, $body); + } + + if ($body instanceof RelatedPart) { + return self::createEmailFromRelatedPart($message, $body); + } + + if ($body instanceof MixedPart) { + $parts = $body->getParts(); + if ($parts[0] instanceof RelatedPart) { + $email = self::createEmailFromRelatedPart($message, $parts[0]); + } elseif ($parts[0] instanceof AlternativePart) { + $email = self::createEmailFromAlternativePart($message, $parts[0]); + } elseif ($parts[0] instanceof TextPart) { + $email = self::createEmailFromTextPart($message, $parts[0]); + } else { + throw new RuntimeException(sprintf('Unable to create an Email from an instance of "%s" as the body is too complex.', get_debug_type($message))); + } + + return self::addParts($email, \array_slice($parts, 1)); + } + + throw new RuntimeException(sprintf('Unable to create an Email from an instance of "%s" as the body is too complex.', get_debug_type($message))); + } + + private static function createEmailFromTextPart(Message $message, TextPart $part): Email + { + if ('text' === $part->getMediaType() && 'plain' === $part->getMediaSubtype()) { + return (new Email(clone $message->getHeaders()))->text($part->getBody(), $part->getPreparedHeaders()->getHeaderParameter('Content-Type', 'charset') ?: 'utf-8'); + } + if ('text' === $part->getMediaType() && 'html' === $part->getMediaSubtype()) { + return (new Email(clone $message->getHeaders()))->html($part->getBody(), $part->getPreparedHeaders()->getHeaderParameter('Content-Type', 'charset') ?: 'utf-8'); + } + + throw new RuntimeException(sprintf('Unable to create an Email from an instance of "%s" as the body is too complex.', get_debug_type($message))); + } + + private static function createEmailFromAlternativePart(Message $message, AlternativePart $part): Email + { + $parts = $part->getParts(); + if ( + 2 === \count($parts) + && $parts[0] instanceof TextPart && 'text' === $parts[0]->getMediaType() && 'plain' === $parts[0]->getMediaSubtype() + && $parts[1] instanceof TextPart && 'text' === $parts[1]->getMediaType() && 'html' === $parts[1]->getMediaSubtype() + ) { + return (new Email(clone $message->getHeaders())) + ->text($parts[0]->getBody(), $parts[0]->getPreparedHeaders()->getHeaderParameter('Content-Type', 'charset') ?: 'utf-8') + ->html($parts[1]->getBody(), $parts[1]->getPreparedHeaders()->getHeaderParameter('Content-Type', 'charset') ?: 'utf-8') + ; + } + + throw new RuntimeException(sprintf('Unable to create an Email from an instance of "%s" as the body is too complex.', get_debug_type($message))); + } + + private static function createEmailFromRelatedPart(Message $message, RelatedPart $part): Email + { + $parts = $part->getParts(); + if ($parts[0] instanceof AlternativePart) { + $email = self::createEmailFromAlternativePart($message, $parts[0]); + } elseif ($parts[0] instanceof TextPart) { + $email = self::createEmailFromTextPart($message, $parts[0]); + } else { + throw new RuntimeException(sprintf('Unable to create an Email from an instance of "%s" as the body is too complex.', get_debug_type($message))); + } + + return self::addParts($email, \array_slice($parts, 1)); + } + + private static function addParts(Email $email, array $parts): Email + { + foreach ($parts as $part) { + if (!$part instanceof DataPart) { + throw new RuntimeException(sprintf('Unable to create an Email from an instance of "%s" as the body is too complex.', get_debug_type($email))); + } + + $email->addPart($part); + } + + return $email; + } +} diff --git a/3rdparty/symfony/mime/MimeTypeGuesserInterface.php b/3rdparty/symfony/mime/MimeTypeGuesserInterface.php new file mode 100644 index 00000000..30ee3b64 --- /dev/null +++ b/3rdparty/symfony/mime/MimeTypeGuesserInterface.php @@ -0,0 +1,33 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Component\Mime; + +/** + * Guesses the MIME type of a file. + * + * @author Fabien Potencier + */ +interface MimeTypeGuesserInterface +{ + /** + * Returns true if this guesser is supported. + */ + public function isGuesserSupported(): bool; + + /** + * Guesses the MIME type of the file with the given path. + * + * @throws \LogicException If the guesser is not supported + * @throws \InvalidArgumentException If the file does not exist or is not readable + */ + public function guessMimeType(string $path): ?string; +} diff --git a/3rdparty/symfony/mime/MimeTypes.php b/3rdparty/symfony/mime/MimeTypes.php new file mode 100644 index 00000000..19628b0b --- /dev/null +++ b/3rdparty/symfony/mime/MimeTypes.php @@ -0,0 +1,3655 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Component\Mime; + +use Symfony\Component\Mime\Exception\LogicException; + +/** + * Manages MIME types and file extensions. + * + * For MIME type guessing, you can register custom guessers + * by calling the registerGuesser() method. + * Custom guessers are always called before any default ones: + * + * $guesser = new MimeTypes(); + * $guesser->registerGuesser(new MyCustomMimeTypeGuesser()); + * + * If you want to change the order of the default guessers, just re-register your + * preferred one as a custom one. The last registered guesser is preferred over + * previously registered ones. + * + * Re-registering a built-in guesser also allows you to configure it: + * + * $guesser = new MimeTypes(); + * $guesser->registerGuesser(new FileinfoMimeTypeGuesser('/path/to/magic/file')); + * + * @author Fabien Potencier + */ +final class MimeTypes implements MimeTypesInterface +{ + private array $extensions = []; + private array $mimeTypes = []; + + /** + * @var MimeTypeGuesserInterface[] + */ + private array $guessers = []; + private static MimeTypes $default; + + public function __construct(array $map = []) + { + foreach ($map as $mimeType => $extensions) { + $this->extensions[$mimeType] = $extensions; + + foreach ($extensions as $extension) { + $this->mimeTypes[$extension][] = $mimeType; + } + } + $this->registerGuesser(new FileBinaryMimeTypeGuesser()); + $this->registerGuesser(new FileinfoMimeTypeGuesser()); + } + + public static function setDefault(self $default): void + { + self::$default = $default; + } + + public static function getDefault(): self + { + return self::$default ??= new self(); + } + + /** + * Registers a MIME type guesser. + * + * The last registered guesser has precedence over the other ones. + */ + public function registerGuesser(MimeTypeGuesserInterface $guesser): void + { + array_unshift($this->guessers, $guesser); + } + + public function getExtensions(string $mimeType): array + { + if ($this->extensions) { + $extensions = $this->extensions[$mimeType] ?? $this->extensions[$lcMimeType = strtolower($mimeType)] ?? null; + } + + return $extensions ?? self::MAP[$mimeType] ?? self::MAP[$lcMimeType ?? strtolower($mimeType)] ?? []; + } + + public function getMimeTypes(string $ext): array + { + if ($this->mimeTypes) { + $mimeTypes = $this->mimeTypes[$ext] ?? $this->mimeTypes[$lcExt = strtolower($ext)] ?? null; + } + + return $mimeTypes ?? self::REVERSE_MAP[$ext] ?? self::REVERSE_MAP[$lcExt ?? strtolower($ext)] ?? []; + } + + public function isGuesserSupported(): bool + { + foreach ($this->guessers as $guesser) { + if ($guesser->isGuesserSupported()) { + return true; + } + } + + return false; + } + + /** + * The file is passed to each registered MIME type guesser in reverse order + * of their registration (last registered is queried first). Once a guesser + * returns a value that is not null, this method terminates and returns the + * value. + */ + public function guessMimeType(string $path): ?string + { + foreach ($this->guessers as $guesser) { + if (!$guesser->isGuesserSupported()) { + continue; + } + + if (null !== $mimeType = $guesser->guessMimeType($path)) { + return $mimeType; + } + } + + if (!$this->isGuesserSupported()) { + throw new LogicException('Unable to guess the MIME type as no guessers are available (have you enabled the php_fileinfo extension?).'); + } + + return null; + } + + /** + * A map of MIME types and their default extensions. + * + * Updated from upstream on 2023-10-14. + * + * @see Resources/bin/update_mime_types.php + */ + private const MAP = [ + 'application/acrobat' => ['pdf'], + 'application/andrew-inset' => ['ez'], + 'application/annodex' => ['anx'], + 'application/applixware' => ['aw'], + 'application/atom+xml' => ['atom'], + 'application/atomcat+xml' => ['atomcat'], + 'application/atomdeleted+xml' => ['atomdeleted'], + 'application/atomsvc+xml' => ['atomsvc'], + 'application/atsc-dwd+xml' => ['dwd'], + 'application/atsc-held+xml' => ['held'], + 'application/atsc-rsat+xml' => ['rsat'], + 'application/bat' => ['bat'], + 'application/bdoc' => ['bdoc'], + 'application/bzip2' => ['bz2', 'bz'], + 'application/calendar+xml' => ['xcs'], + 'application/cbor' => ['cbor'], + 'application/ccxml+xml' => ['ccxml'], + 'application/cdfx+xml' => ['cdfx'], + 'application/cdmi-capability' => ['cdmia'], + 'application/cdmi-container' => ['cdmic'], + 'application/cdmi-domain' => ['cdmid'], + 'application/cdmi-object' => ['cdmio'], + 'application/cdmi-queue' => ['cdmiq'], + 'application/cdr' => ['cdr'], + 'application/coreldraw' => ['cdr'], + 'application/cpl+xml' => ['cpl'], + 'application/csv' => ['csv'], + 'application/cu-seeme' => ['cu'], + 'application/dash+xml' => ['mpd'], + 'application/dash-patch+xml' => ['mpp'], + 'application/davmount+xml' => ['davmount'], + 'application/dbase' => ['dbf'], + 'application/dbf' => ['dbf'], + 'application/dicom' => ['dcm'], + 'application/docbook+xml' => ['dbk', 'docbook'], + 'application/dssc+der' => ['dssc'], + 'application/dssc+xml' => ['xdssc'], + 'application/ecmascript' => ['ecma', 'es'], + 'application/emf' => ['emf'], + 'application/emma+xml' => ['emma'], + 'application/emotionml+xml' => ['emotionml'], + 'application/epub+zip' => ['epub'], + 'application/exi' => ['exi'], + 'application/express' => ['exp'], + 'application/fdt+xml' => ['fdt'], + 'application/fits' => ['fits', 'fit', 'fts'], + 'application/font-tdpfr' => ['pfr'], + 'application/font-woff' => ['woff'], + 'application/futuresplash' => ['swf', 'spl'], + 'application/geo+json' => ['geojson', 'geo.json'], + 'application/gml+xml' => ['gml'], + 'application/gnunet-directory' => ['gnd'], + 'application/gpx' => ['gpx'], + 'application/gpx+xml' => ['gpx'], + 'application/gxf' => ['gxf'], + 'application/gzip' => ['gz'], + 'application/hjson' => ['hjson'], + 'application/hyperstudio' => ['stk'], + 'application/ico' => ['ico'], + 'application/ics' => ['vcs', 'ics'], + 'application/illustrator' => ['ai'], + 'application/inkml+xml' => ['ink', 'inkml'], + 'application/ipfix' => ['ipfix'], + 'application/its+xml' => ['its'], + 'application/java' => ['class'], + 'application/java-archive' => ['jar', 'war', 'ear'], + 'application/java-byte-code' => ['class'], + 'application/java-serialized-object' => ['ser'], + 'application/java-vm' => ['class'], + 'application/javascript' => ['js', 'mjs', 'jsm'], + 'application/jrd+json' => ['jrd'], + 'application/json' => ['json', 'map'], + 'application/json-patch+json' => ['json-patch'], + 'application/json5' => ['json5'], + 'application/jsonml+json' => ['jsonml'], + 'application/ld+json' => ['jsonld'], + 'application/lgr+xml' => ['lgr'], + 'application/lost+xml' => ['lostxml'], + 'application/lotus123' => ['123', 'wk1', 'wk3', 'wk4', 'wks'], + 'application/m3u' => ['m3u', 'm3u8', 'vlc'], + 'application/mac-binhex40' => ['hqx'], + 'application/mac-compactpro' => ['cpt'], + 'application/mads+xml' => ['mads'], + 'application/manifest+json' => ['webmanifest'], + 'application/marc' => ['mrc'], + 'application/marcxml+xml' => ['mrcx'], + 'application/mathematica' => ['ma', 'nb', 'mb'], + 'application/mathml+xml' => ['mathml', 'mml'], + 'application/mbox' => ['mbox'], + 'application/mdb' => ['mdb'], + 'application/media-policy-dataset+xml' => ['mpf'], + 'application/mediaservercontrol+xml' => ['mscml'], + 'application/metalink+xml' => ['metalink'], + 'application/metalink4+xml' => ['meta4'], + 'application/mets+xml' => ['mets'], + 'application/mmt-aei+xml' => ['maei'], + 'application/mmt-usd+xml' => ['musd'], + 'application/mods+xml' => ['mods'], + 'application/mp21' => ['m21', 'mp21'], + 'application/mp4' => ['mp4s', 'm4p'], + 'application/mrb-consumer+xml' => ['xdf'], + 'application/mrb-publish+xml' => ['xdf'], + 'application/ms-tnef' => ['tnef', 'tnf'], + 'application/msaccess' => ['mdb'], + 'application/msexcel' => ['xls', 'xlc', 'xll', 'xlm', 'xlw', 'xla', 'xlt', 'xld'], + 'application/mspowerpoint' => ['ppz', 'ppt', 'pps', 'pot'], + 'application/msword' => ['doc', 'dot'], + 'application/msword-template' => ['dot'], + 'application/mxf' => ['mxf'], + 'application/n-quads' => ['nq'], + 'application/n-triples' => ['nt'], + 'application/nappdf' => ['pdf'], + 'application/node' => ['cjs'], + 'application/octet-stream' => ['bin', 'dms', 'lrf', 'mar', 'so', 'dist', 'distz', 'pkg', 'bpk', 'dump', 'elc', 'deploy', 'exe', 'dll', 'deb', 'dmg', 'iso', 'img', 'msi', 'msp', 'msm', 'buffer'], + 'application/oda' => ['oda'], + 'application/oebps-package+xml' => ['opf'], + 'application/ogg' => ['ogx'], + 'application/omdoc+xml' => ['omdoc'], + 'application/onenote' => ['onetoc', 'onetoc2', 'onetmp', 'onepkg'], + 'application/ovf' => ['ova'], + 'application/owl+xml' => ['owx'], + 'application/oxps' => ['oxps'], + 'application/p2p-overlay+xml' => ['relo'], + 'application/patch-ops-error+xml' => ['xer'], + 'application/pcap' => ['pcap', 'cap', 'dmp'], + 'application/pdf' => ['pdf'], + 'application/pgp' => ['pgp', 'gpg', 'asc'], + 'application/pgp-encrypted' => ['pgp', 'gpg', 'asc'], + 'application/pgp-keys' => ['asc', 'skr', 'pkr', 'pgp', 'gpg', 'key'], + 'application/pgp-signature' => ['asc', 'sig', 'pgp', 'gpg'], + 'application/photoshop' => ['psd'], + 'application/pics-rules' => ['prf'], + 'application/pkcs10' => ['p10'], + 'application/pkcs12' => ['p12', 'pfx'], + 'application/pkcs7-mime' => ['p7m', 'p7c'], + 'application/pkcs7-signature' => ['p7s'], + 'application/pkcs8' => ['p8'], + 'application/pkcs8-encrypted' => ['p8e'], + 'application/pkix-attr-cert' => ['ac'], + 'application/pkix-cert' => ['cer'], + 'application/pkix-crl' => ['crl'], + 'application/pkix-pkipath' => ['pkipath'], + 'application/pkixcmp' => ['pki'], + 'application/pls' => ['pls'], + 'application/pls+xml' => ['pls'], + 'application/postscript' => ['ai', 'eps', 'ps'], + 'application/powerpoint' => ['ppz', 'ppt', 'pps', 'pot'], + 'application/provenance+xml' => ['provx'], + 'application/prs.cww' => ['cww'], + 'application/pskc+xml' => ['pskcxml'], + 'application/ram' => ['ram'], + 'application/raml+yaml' => ['raml'], + 'application/rdf+xml' => ['rdf', 'owl', 'rdfs'], + 'application/reginfo+xml' => ['rif'], + 'application/relax-ng-compact-syntax' => ['rnc'], + 'application/resource-lists+xml' => ['rl'], + 'application/resource-lists-diff+xml' => ['rld'], + 'application/rls-services+xml' => ['rs'], + 'application/route-apd+xml' => ['rapd'], + 'application/route-s-tsid+xml' => ['sls'], + 'application/route-usd+xml' => ['rusd'], + 'application/rpki-ghostbusters' => ['gbr'], + 'application/rpki-manifest' => ['mft'], + 'application/rpki-roa' => ['roa'], + 'application/rsd+xml' => ['rsd'], + 'application/rss+xml' => ['rss'], + 'application/rtf' => ['rtf'], + 'application/sbml+xml' => ['sbml'], + 'application/schema+json' => ['json'], + 'application/scvp-cv-request' => ['scq'], + 'application/scvp-cv-response' => ['scs'], + 'application/scvp-vp-request' => ['spq'], + 'application/scvp-vp-response' => ['spp'], + 'application/sdp' => ['sdp'], + 'application/senml+xml' => ['senmlx'], + 'application/sensml+xml' => ['sensmlx'], + 'application/set-payment-initiation' => ['setpay'], + 'application/set-registration-initiation' => ['setreg'], + 'application/shf+xml' => ['shf'], + 'application/sieve' => ['siv', 'sieve'], + 'application/smil' => ['smil', 'smi', 'sml', 'kino'], + 'application/smil+xml' => ['smi', 'smil', 'sml', 'kino'], + 'application/sparql-query' => ['rq', 'qs'], + 'application/sparql-results+xml' => ['srx'], + 'application/sql' => ['sql'], + 'application/srgs' => ['gram'], + 'application/srgs+xml' => ['grxml'], + 'application/sru+xml' => ['sru'], + 'application/ssdl+xml' => ['ssdl'], + 'application/ssml+xml' => ['ssml'], + 'application/stuffit' => ['sit', 'hqx'], + 'application/swid+xml' => ['swidtag'], + 'application/tei+xml' => ['tei', 'teicorpus'], + 'application/tga' => ['tga', 'icb', 'tpic', 'vda', 'vst'], + 'application/thraud+xml' => ['tfi'], + 'application/timestamped-data' => ['tsd'], + 'application/toml' => ['toml'], + 'application/trig' => ['trig'], + 'application/ttml+xml' => ['ttml'], + 'application/ubjson' => ['ubj'], + 'application/urc-ressheet+xml' => ['rsheet'], + 'application/urc-targetdesc+xml' => ['td'], + 'application/vnd.1000minds.decision-model+xml' => ['1km'], + 'application/vnd.3gpp.pic-bw-large' => ['plb'], + 'application/vnd.3gpp.pic-bw-small' => ['psb'], + 'application/vnd.3gpp.pic-bw-var' => ['pvb'], + 'application/vnd.3gpp2.tcap' => ['tcap'], + 'application/vnd.3m.post-it-notes' => ['pwn'], + 'application/vnd.accpac.simply.aso' => ['aso'], + 'application/vnd.accpac.simply.imp' => ['imp'], + 'application/vnd.acucobol' => ['acu'], + 'application/vnd.acucorp' => ['atc', 'acutc'], + 'application/vnd.adobe.air-application-installer-package+zip' => ['air'], + 'application/vnd.adobe.flash.movie' => ['swf', 'spl'], + 'application/vnd.adobe.formscentral.fcdt' => ['fcdt'], + 'application/vnd.adobe.fxp' => ['fxp', 'fxpl'], + 'application/vnd.adobe.illustrator' => ['ai'], + 'application/vnd.adobe.xdp+xml' => ['xdp'], + 'application/vnd.adobe.xfdf' => ['xfdf'], + 'application/vnd.age' => ['age'], + 'application/vnd.ahead.space' => ['ahead'], + 'application/vnd.airzip.filesecure.azf' => ['azf'], + 'application/vnd.airzip.filesecure.azs' => ['azs'], + 'application/vnd.amazon.ebook' => ['azw'], + 'application/vnd.amazon.mobi8-ebook' => ['azw3', 'kfx'], + 'application/vnd.americandynamics.acc' => ['acc'], + 'application/vnd.amiga.ami' => ['ami'], + 'application/vnd.android.package-archive' => ['apk'], + 'application/vnd.anser-web-certificate-issue-initiation' => ['cii'], + 'application/vnd.anser-web-funds-transfer-initiation' => ['fti'], + 'application/vnd.antix.game-component' => ['atx'], + 'application/vnd.appimage' => ['appimage'], + 'application/vnd.apple.installer+xml' => ['mpkg'], + 'application/vnd.apple.keynote' => ['key', 'keynote'], + 'application/vnd.apple.mpegurl' => ['m3u8', 'm3u'], + 'application/vnd.apple.numbers' => ['numbers'], + 'application/vnd.apple.pages' => ['pages'], + 'application/vnd.apple.pkpass' => ['pkpass'], + 'application/vnd.aristanetworks.swi' => ['swi'], + 'application/vnd.astraea-software.iota' => ['iota'], + 'application/vnd.audiograph' => ['aep'], + 'application/vnd.balsamiq.bmml+xml' => ['bmml'], + 'application/vnd.blueice.multipass' => ['mpm'], + 'application/vnd.bmi' => ['bmi'], + 'application/vnd.businessobjects' => ['rep'], + 'application/vnd.chemdraw+xml' => ['cdxml'], + 'application/vnd.chess-pgn' => ['pgn'], + 'application/vnd.chipnuts.karaoke-mmd' => ['mmd'], + 'application/vnd.cinderella' => ['cdy'], + 'application/vnd.citationstyles.style+xml' => ['csl'], + 'application/vnd.claymore' => ['cla'], + 'application/vnd.cloanto.rp9' => ['rp9'], + 'application/vnd.clonk.c4group' => ['c4g', 'c4d', 'c4f', 'c4p', 'c4u'], + 'application/vnd.cluetrust.cartomobile-config' => ['c11amc'], + 'application/vnd.cluetrust.cartomobile-config-pkg' => ['c11amz'], + 'application/vnd.coffeescript' => ['coffee'], + 'application/vnd.comicbook+zip' => ['cbz'], + 'application/vnd.comicbook-rar' => ['cbr'], + 'application/vnd.commonspace' => ['csp'], + 'application/vnd.contact.cmsg' => ['cdbcmsg'], + 'application/vnd.corel-draw' => ['cdr'], + 'application/vnd.cosmocaller' => ['cmc'], + 'application/vnd.crick.clicker' => ['clkx'], + 'application/vnd.crick.clicker.keyboard' => ['clkk'], + 'application/vnd.crick.clicker.palette' => ['clkp'], + 'application/vnd.crick.clicker.template' => ['clkt'], + 'application/vnd.crick.clicker.wordbank' => ['clkw'], + 'application/vnd.criticaltools.wbs+xml' => ['wbs'], + 'application/vnd.ctc-posml' => ['pml'], + 'application/vnd.cups-ppd' => ['ppd'], + 'application/vnd.curl.car' => ['car'], + 'application/vnd.curl.pcurl' => ['pcurl'], + 'application/vnd.dart' => ['dart'], + 'application/vnd.data-vision.rdz' => ['rdz'], + 'application/vnd.dbf' => ['dbf'], + 'application/vnd.debian.binary-package' => ['deb', 'udeb'], + 'application/vnd.dece.data' => ['uvf', 'uvvf', 'uvd', 'uvvd'], + 'application/vnd.dece.ttml+xml' => ['uvt', 'uvvt'], + 'application/vnd.dece.unspecified' => ['uvx', 'uvvx'], + 'application/vnd.dece.zip' => ['uvz', 'uvvz'], + 'application/vnd.denovo.fcselayout-link' => ['fe_launch'], + 'application/vnd.dna' => ['dna'], + 'application/vnd.dolby.mlp' => ['mlp'], + 'application/vnd.dpgraph' => ['dpg'], + 'application/vnd.dreamfactory' => ['dfac'], + 'application/vnd.ds-keypoint' => ['kpxx'], + 'application/vnd.dvb.ait' => ['ait'], + 'application/vnd.dvb.service' => ['svc'], + 'application/vnd.dynageo' => ['geo'], + 'application/vnd.ecowin.chart' => ['mag'], + 'application/vnd.efi.img' => ['raw-disk-image', 'img'], + 'application/vnd.efi.iso' => ['iso', 'iso9660'], + 'application/vnd.emusic-emusic_package' => ['emp'], + 'application/vnd.enliven' => ['nml'], + 'application/vnd.epson.esf' => ['esf'], + 'application/vnd.epson.msf' => ['msf'], + 'application/vnd.epson.quickanime' => ['qam'], + 'application/vnd.epson.salt' => ['slt'], + 'application/vnd.epson.ssf' => ['ssf'], + 'application/vnd.eszigno3+xml' => ['es3', 'et3'], + 'application/vnd.etsi.asic-e+zip' => ['asice'], + 'application/vnd.ezpix-album' => ['ez2'], + 'application/vnd.ezpix-package' => ['ez3'], + 'application/vnd.fdf' => ['fdf'], + 'application/vnd.fdsn.mseed' => ['mseed'], + 'application/vnd.fdsn.seed' => ['seed', 'dataless'], + 'application/vnd.flatpak' => ['flatpak', 'xdgapp'], + 'application/vnd.flatpak.ref' => ['flatpakref'], + 'application/vnd.flatpak.repo' => ['flatpakrepo'], + 'application/vnd.flographit' => ['gph'], + 'application/vnd.fluxtime.clip' => ['ftc'], + 'application/vnd.framemaker' => ['fm', 'frame', 'maker', 'book'], + 'application/vnd.frogans.fnc' => ['fnc'], + 'application/vnd.frogans.ltf' => ['ltf'], + 'application/vnd.fsc.weblaunch' => ['fsc'], + 'application/vnd.fujitsu.oasys' => ['oas'], + 'application/vnd.fujitsu.oasys2' => ['oa2'], + 'application/vnd.fujitsu.oasys3' => ['oa3'], + 'application/vnd.fujitsu.oasysgp' => ['fg5'], + 'application/vnd.fujitsu.oasysprs' => ['bh2'], + 'application/vnd.fujixerox.ddd' => ['ddd'], + 'application/vnd.fujixerox.docuworks' => ['xdw'], + 'application/vnd.fujixerox.docuworks.binder' => ['xbd'], + 'application/vnd.fuzzysheet' => ['fzs'], + 'application/vnd.genomatix.tuxedo' => ['txd'], + 'application/vnd.geo+json' => ['geojson', 'geo.json'], + 'application/vnd.geogebra.file' => ['ggb'], + 'application/vnd.geogebra.tool' => ['ggt'], + 'application/vnd.geometry-explorer' => ['gex', 'gre'], + 'application/vnd.geonext' => ['gxt'], + 'application/vnd.geoplan' => ['g2w'], + 'application/vnd.geospace' => ['g3w'], + 'application/vnd.gerber' => ['gbr'], + 'application/vnd.gmx' => ['gmx'], + 'application/vnd.google-apps.document' => ['gdoc'], + 'application/vnd.google-apps.presentation' => ['gslides'], + 'application/vnd.google-apps.spreadsheet' => ['gsheet'], + 'application/vnd.google-earth.kml+xml' => ['kml'], + 'application/vnd.google-earth.kmz' => ['kmz'], + 'application/vnd.grafeq' => ['gqf', 'gqs'], + 'application/vnd.groove-account' => ['gac'], + 'application/vnd.groove-help' => ['ghf'], + 'application/vnd.groove-identity-message' => ['gim'], + 'application/vnd.groove-injector' => ['grv'], + 'application/vnd.groove-tool-message' => ['gtm'], + 'application/vnd.groove-tool-template' => ['tpl'], + 'application/vnd.groove-vcard' => ['vcg'], + 'application/vnd.haansoft-hwp' => ['hwp'], + 'application/vnd.haansoft-hwt' => ['hwt'], + 'application/vnd.hal+xml' => ['hal'], + 'application/vnd.handheld-entertainment+xml' => ['zmm'], + 'application/vnd.hbci' => ['hbci'], + 'application/vnd.hhe.lesson-player' => ['les'], + 'application/vnd.hp-hpgl' => ['hpgl'], + 'application/vnd.hp-hpid' => ['hpid'], + 'application/vnd.hp-hps' => ['hps'], + 'application/vnd.hp-jlyt' => ['jlt'], + 'application/vnd.hp-pcl' => ['pcl'], + 'application/vnd.hp-pclxl' => ['pclxl'], + 'application/vnd.hydrostatix.sof-data' => ['sfd-hdstx'], + 'application/vnd.ibm.minipay' => ['mpy'], + 'application/vnd.ibm.modcap' => ['afp', 'listafp', 'list3820'], + 'application/vnd.ibm.rights-management' => ['irm'], + 'application/vnd.ibm.secure-container' => ['sc'], + 'application/vnd.iccprofile' => ['icc', 'icm'], + 'application/vnd.igloader' => ['igl'], + 'application/vnd.immervision-ivp' => ['ivp'], + 'application/vnd.immervision-ivu' => ['ivu'], + 'application/vnd.insors.igm' => ['igm'], + 'application/vnd.intercon.formnet' => ['xpw', 'xpx'], + 'application/vnd.intergeo' => ['i2g'], + 'application/vnd.intu.qbo' => ['qbo'], + 'application/vnd.intu.qfx' => ['qfx'], + 'application/vnd.ipunplugged.rcprofile' => ['rcprofile'], + 'application/vnd.irepository.package+xml' => ['irp'], + 'application/vnd.is-xpr' => ['xpr'], + 'application/vnd.isac.fcs' => ['fcs'], + 'application/vnd.jam' => ['jam'], + 'application/vnd.jcp.javame.midlet-rms' => ['rms'], + 'application/vnd.jisp' => ['jisp'], + 'application/vnd.joost.joda-archive' => ['joda'], + 'application/vnd.kahootz' => ['ktz', 'ktr'], + 'application/vnd.kde.karbon' => ['karbon'], + 'application/vnd.kde.kchart' => ['chrt'], + 'application/vnd.kde.kformula' => ['kfo'], + 'application/vnd.kde.kivio' => ['flw'], + 'application/vnd.kde.kontour' => ['kon'], + 'application/vnd.kde.kpresenter' => ['kpr', 'kpt'], + 'application/vnd.kde.kspread' => ['ksp'], + 'application/vnd.kde.kword' => ['kwd', 'kwt'], + 'application/vnd.kenameaapp' => ['htke'], + 'application/vnd.kidspiration' => ['kia'], + 'application/vnd.kinar' => ['kne', 'knp'], + 'application/vnd.koan' => ['skp', 'skd', 'skt', 'skm'], + 'application/vnd.kodak-descriptor' => ['sse'], + 'application/vnd.las.las+xml' => ['lasxml'], + 'application/vnd.llamagraphics.life-balance.desktop' => ['lbd'], + 'application/vnd.llamagraphics.life-balance.exchange+xml' => ['lbe'], + 'application/vnd.lotus-1-2-3' => ['123', 'wk1', 'wk3', 'wk4', 'wks'], + 'application/vnd.lotus-approach' => ['apr'], + 'application/vnd.lotus-freelance' => ['pre'], + 'application/vnd.lotus-notes' => ['nsf'], + 'application/vnd.lotus-organizer' => ['org'], + 'application/vnd.lotus-screencam' => ['scm'], + 'application/vnd.lotus-wordpro' => ['lwp'], + 'application/vnd.macports.portpkg' => ['portpkg'], + 'application/vnd.mapbox-vector-tile' => ['mvt'], + 'application/vnd.mcd' => ['mcd'], + 'application/vnd.medcalcdata' => ['mc1'], + 'application/vnd.mediastation.cdkey' => ['cdkey'], + 'application/vnd.mfer' => ['mwf'], + 'application/vnd.mfmp' => ['mfm'], + 'application/vnd.micrografx.flo' => ['flo'], + 'application/vnd.micrografx.igx' => ['igx'], + 'application/vnd.mif' => ['mif'], + 'application/vnd.mobius.daf' => ['daf'], + 'application/vnd.mobius.dis' => ['dis'], + 'application/vnd.mobius.mbk' => ['mbk'], + 'application/vnd.mobius.mqy' => ['mqy'], + 'application/vnd.mobius.msl' => ['msl'], + 'application/vnd.mobius.plc' => ['plc'], + 'application/vnd.mobius.txf' => ['txf'], + 'application/vnd.mophun.application' => ['mpn'], + 'application/vnd.mophun.certificate' => ['mpc'], + 'application/vnd.mozilla.xul+xml' => ['xul'], + 'application/vnd.ms-3mfdocument' => ['3mf'], + 'application/vnd.ms-access' => ['mdb'], + 'application/vnd.ms-artgalry' => ['cil'], + 'application/vnd.ms-asf' => ['asf'], + 'application/vnd.ms-cab-compressed' => ['cab'], + 'application/vnd.ms-excel' => ['xls', 'xlm', 'xla', 'xlc', 'xlt', 'xlw', 'xll', 'xld'], + 'application/vnd.ms-excel.addin.macroenabled.12' => ['xlam'], + 'application/vnd.ms-excel.sheet.binary.macroenabled.12' => ['xlsb'], + 'application/vnd.ms-excel.sheet.macroenabled.12' => ['xlsm'], + 'application/vnd.ms-excel.template.macroenabled.12' => ['xltm'], + 'application/vnd.ms-fontobject' => ['eot'], + 'application/vnd.ms-htmlhelp' => ['chm'], + 'application/vnd.ms-ims' => ['ims'], + 'application/vnd.ms-lrm' => ['lrm'], + 'application/vnd.ms-officetheme' => ['thmx'], + 'application/vnd.ms-outlook' => ['msg'], + 'application/vnd.ms-pki.seccat' => ['cat'], + 'application/vnd.ms-pki.stl' => ['stl'], + 'application/vnd.ms-powerpoint' => ['ppt', 'pps', 'pot', 'ppz'], + 'application/vnd.ms-powerpoint.addin.macroenabled.12' => ['ppam'], + 'application/vnd.ms-powerpoint.presentation.macroenabled.12' => ['pptm'], + 'application/vnd.ms-powerpoint.slide.macroenabled.12' => ['sldm'], + 'application/vnd.ms-powerpoint.slideshow.macroenabled.12' => ['ppsm'], + 'application/vnd.ms-powerpoint.template.macroenabled.12' => ['potm'], + 'application/vnd.ms-project' => ['mpp', 'mpt'], + 'application/vnd.ms-publisher' => ['pub'], + 'application/vnd.ms-tnef' => ['tnef', 'tnf'], + 'application/vnd.ms-visio.drawing.macroenabled.main+xml' => ['vsdm'], + 'application/vnd.ms-visio.drawing.main+xml' => ['vsdx'], + 'application/vnd.ms-visio.stencil.macroenabled.main+xml' => ['vssm'], + 'application/vnd.ms-visio.stencil.main+xml' => ['vssx'], + 'application/vnd.ms-visio.template.macroenabled.main+xml' => ['vstm'], + 'application/vnd.ms-visio.template.main+xml' => ['vstx'], + 'application/vnd.ms-word' => ['doc'], + 'application/vnd.ms-word.document.macroenabled.12' => ['docm'], + 'application/vnd.ms-word.template.macroenabled.12' => ['dotm'], + 'application/vnd.ms-works' => ['wps', 'wks', 'wcm', 'wdb', 'xlr'], + 'application/vnd.ms-wpl' => ['wpl'], + 'application/vnd.ms-xpsdocument' => ['xps'], + 'application/vnd.msaccess' => ['mdb'], + 'application/vnd.mseq' => ['mseq'], + 'application/vnd.musician' => ['mus'], + 'application/vnd.muvee.style' => ['msty'], + 'application/vnd.mynfc' => ['taglet'], + 'application/vnd.neurolanguage.nlu' => ['nlu'], + 'application/vnd.nintendo.snes.rom' => ['sfc', 'smc'], + 'application/vnd.nitf' => ['ntf', 'nitf'], + 'application/vnd.noblenet-directory' => ['nnd'], + 'application/vnd.noblenet-sealer' => ['nns'], + 'application/vnd.noblenet-web' => ['nnw'], + 'application/vnd.nokia.n-gage.ac+xml' => ['ac'], + 'application/vnd.nokia.n-gage.data' => ['ngdat'], + 'application/vnd.nokia.n-gage.symbian.install' => ['n-gage'], + 'application/vnd.nokia.radio-preset' => ['rpst'], + 'application/vnd.nokia.radio-presets' => ['rpss'], + 'application/vnd.novadigm.edm' => ['edm'], + 'application/vnd.novadigm.edx' => ['edx'], + 'application/vnd.novadigm.ext' => ['ext'], + 'application/vnd.oasis.docbook+xml' => ['dbk', 'docbook'], + 'application/vnd.oasis.opendocument.chart' => ['odc'], + 'application/vnd.oasis.opendocument.chart-template' => ['otc'], + 'application/vnd.oasis.opendocument.database' => ['odb'], + 'application/vnd.oasis.opendocument.formula' => ['odf'], + 'application/vnd.oasis.opendocument.formula-template' => ['odft', 'otf'], + 'application/vnd.oasis.opendocument.graphics' => ['odg'], + 'application/vnd.oasis.opendocument.graphics-flat-xml' => ['fodg'], + 'application/vnd.oasis.opendocument.graphics-template' => ['otg'], + 'application/vnd.oasis.opendocument.image' => ['odi'], + 'application/vnd.oasis.opendocument.image-template' => ['oti'], + 'application/vnd.oasis.opendocument.presentation' => ['odp'], + 'application/vnd.oasis.opendocument.presentation-flat-xml' => ['fodp'], + 'application/vnd.oasis.opendocument.presentation-template' => ['otp'], + 'application/vnd.oasis.opendocument.spreadsheet' => ['ods'], + 'application/vnd.oasis.opendocument.spreadsheet-flat-xml' => ['fods'], + 'application/vnd.oasis.opendocument.spreadsheet-template' => ['ots'], + 'application/vnd.oasis.opendocument.text' => ['odt'], + 'application/vnd.oasis.opendocument.text-flat-xml' => ['fodt'], + 'application/vnd.oasis.opendocument.text-master' => ['odm'], + 'application/vnd.oasis.opendocument.text-template' => ['ott'], + 'application/vnd.oasis.opendocument.text-web' => ['oth'], + 'application/vnd.olpc-sugar' => ['xo'], + 'application/vnd.oma.dd2+xml' => ['dd2'], + 'application/vnd.openblox.game+xml' => ['obgx'], + 'application/vnd.openofficeorg.extension' => ['oxt'], + 'application/vnd.openstreetmap.data+xml' => ['osm'], + 'application/vnd.openxmlformats-officedocument.presentationml.presentation' => ['pptx'], + 'application/vnd.openxmlformats-officedocument.presentationml.slide' => ['sldx'], + 'application/vnd.openxmlformats-officedocument.presentationml.slideshow' => ['ppsx'], + 'application/vnd.openxmlformats-officedocument.presentationml.template' => ['potx'], + 'application/vnd.openxmlformats-officedocument.spreadsheetml.sheet' => ['xlsx'], + 'application/vnd.openxmlformats-officedocument.spreadsheetml.template' => ['xltx'], + 'application/vnd.openxmlformats-officedocument.wordprocessingml.document' => ['docx'], + 'application/vnd.openxmlformats-officedocument.wordprocessingml.template' => ['dotx'], + 'application/vnd.osgeo.mapguide.package' => ['mgp'], + 'application/vnd.osgi.dp' => ['dp'], + 'application/vnd.osgi.subsystem' => ['esa'], + 'application/vnd.palm' => ['pdb', 'pqa', 'oprc', 'prc'], + 'application/vnd.pawaafile' => ['paw'], + 'application/vnd.pg.format' => ['str'], + 'application/vnd.pg.osasli' => ['ei6'], + 'application/vnd.picsel' => ['efif'], + 'application/vnd.pmi.widget' => ['wg'], + 'application/vnd.pocketlearn' => ['plf'], + 'application/vnd.powerbuilder6' => ['pbd'], + 'application/vnd.previewsystems.box' => ['box'], + 'application/vnd.proteus.magazine' => ['mgz'], + 'application/vnd.publishare-delta-tree' => ['qps'], + 'application/vnd.pvi.ptid1' => ['ptid'], + 'application/vnd.quark.quarkxpress' => ['qxd', 'qxt', 'qwd', 'qwt', 'qxl', 'qxb'], + 'application/vnd.rar' => ['rar'], + 'application/vnd.realvnc.bed' => ['bed'], + 'application/vnd.recordare.musicxml' => ['mxl'], + 'application/vnd.recordare.musicxml+xml' => ['musicxml'], + 'application/vnd.rig.cryptonote' => ['cryptonote'], + 'application/vnd.rim.cod' => ['cod'], + 'application/vnd.rn-realmedia' => ['rm', 'rmj', 'rmm', 'rms', 'rmx', 'rmvb'], + 'application/vnd.rn-realmedia-vbr' => ['rmvb', 'rm', 'rmj', 'rmm', 'rms', 'rmx'], + 'application/vnd.route66.link66+xml' => ['link66'], + 'application/vnd.sailingtracker.track' => ['st'], + 'application/vnd.sdp' => ['sdp'], + 'application/vnd.seemail' => ['see'], + 'application/vnd.sema' => ['sema'], + 'application/vnd.semd' => ['semd'], + 'application/vnd.semf' => ['semf'], + 'application/vnd.shana.informed.formdata' => ['ifm'], + 'application/vnd.shana.informed.formtemplate' => ['itp'], + 'application/vnd.shana.informed.interchange' => ['iif'], + 'application/vnd.shana.informed.package' => ['ipk'], + 'application/vnd.simtech-mindmapper' => ['twd', 'twds'], + 'application/vnd.smaf' => ['mmf', 'smaf'], + 'application/vnd.smart.teacher' => ['teacher'], + 'application/vnd.snap' => ['snap'], + 'application/vnd.software602.filler.form+xml' => ['fo'], + 'application/vnd.solent.sdkm+xml' => ['sdkm', 'sdkd'], + 'application/vnd.spotfire.dxp' => ['dxp'], + 'application/vnd.spotfire.sfs' => ['sfs'], + 'application/vnd.sqlite3' => ['sqlite3'], + 'application/vnd.squashfs' => ['sqsh'], + 'application/vnd.stardivision.calc' => ['sdc'], + 'application/vnd.stardivision.chart' => ['sds'], + 'application/vnd.stardivision.draw' => ['sda'], + 'application/vnd.stardivision.impress' => ['sdd', 'sdp'], + 'application/vnd.stardivision.mail' => ['smd'], + 'application/vnd.stardivision.math' => ['smf'], + 'application/vnd.stardivision.writer' => ['sdw', 'vor', 'sgl'], + 'application/vnd.stardivision.writer-global' => ['sgl', 'sdw', 'vor'], + 'application/vnd.stepmania.package' => ['smzip'], + 'application/vnd.stepmania.stepchart' => ['sm'], + 'application/vnd.sun.wadl+xml' => ['wadl'], + 'application/vnd.sun.xml.base' => ['odb'], + 'application/vnd.sun.xml.calc' => ['sxc'], + 'application/vnd.sun.xml.calc.template' => ['stc'], + 'application/vnd.sun.xml.draw' => ['sxd'], + 'application/vnd.sun.xml.draw.template' => ['std'], + 'application/vnd.sun.xml.impress' => ['sxi'], + 'application/vnd.sun.xml.impress.template' => ['sti'], + 'application/vnd.sun.xml.math' => ['sxm'], + 'application/vnd.sun.xml.writer' => ['sxw'], + 'application/vnd.sun.xml.writer.global' => ['sxg'], + 'application/vnd.sun.xml.writer.template' => ['stw'], + 'application/vnd.sus-calendar' => ['sus', 'susp'], + 'application/vnd.svd' => ['svd'], + 'application/vnd.symbian.install' => ['sis', 'sisx'], + 'application/vnd.syncml+xml' => ['xsm'], + 'application/vnd.syncml.dm+wbxml' => ['bdm'], + 'application/vnd.syncml.dm+xml' => ['xdm'], + 'application/vnd.syncml.dmddf+xml' => ['ddf'], + 'application/vnd.tao.intent-module-archive' => ['tao'], + 'application/vnd.tcpdump.pcap' => ['pcap', 'cap', 'dmp'], + 'application/vnd.tmobile-livetv' => ['tmo'], + 'application/vnd.trid.tpt' => ['tpt'], + 'application/vnd.triscape.mxs' => ['mxs'], + 'application/vnd.trueapp' => ['tra'], + 'application/vnd.truedoc' => ['pfr'], + 'application/vnd.ufdl' => ['ufd', 'ufdl'], + 'application/vnd.uiq.theme' => ['utz'], + 'application/vnd.umajin' => ['umj'], + 'application/vnd.unity' => ['unityweb'], + 'application/vnd.uoml+xml' => ['uoml'], + 'application/vnd.vcx' => ['vcx'], + 'application/vnd.visio' => ['vsd', 'vst', 'vss', 'vsw'], + 'application/vnd.visionary' => ['vis'], + 'application/vnd.vsf' => ['vsf'], + 'application/vnd.wap.wbxml' => ['wbxml'], + 'application/vnd.wap.wmlc' => ['wmlc'], + 'application/vnd.wap.wmlscriptc' => ['wmlsc'], + 'application/vnd.webturbo' => ['wtb'], + 'application/vnd.wolfram.player' => ['nbp'], + 'application/vnd.wordperfect' => ['wpd', 'wp', 'wp4', 'wp5', 'wp6', 'wpp'], + 'application/vnd.wqd' => ['wqd'], + 'application/vnd.wt.stf' => ['stf'], + 'application/vnd.xara' => ['xar'], + 'application/vnd.xdgapp' => ['flatpak', 'xdgapp'], + 'application/vnd.xfdl' => ['xfdl'], + 'application/vnd.yamaha.hv-dic' => ['hvd'], + 'application/vnd.yamaha.hv-script' => ['hvs'], + 'application/vnd.yamaha.hv-voice' => ['hvp'], + 'application/vnd.yamaha.openscoreformat' => ['osf'], + 'application/vnd.yamaha.openscoreformat.osfpvg+xml' => ['osfpvg'], + 'application/vnd.yamaha.smaf-audio' => ['saf'], + 'application/vnd.yamaha.smaf-phrase' => ['spf'], + 'application/vnd.yellowriver-custom-menu' => ['cmp'], + 'application/vnd.youtube.yt' => ['yt'], + 'application/vnd.zul' => ['zir', 'zirz'], + 'application/vnd.zzazz.deck+xml' => ['zaz'], + 'application/voicexml+xml' => ['vxml'], + 'application/wasm' => ['wasm'], + 'application/watcherinfo+xml' => ['wif'], + 'application/widget' => ['wgt'], + 'application/winhlp' => ['hlp'], + 'application/wk1' => ['123', 'wk1', 'wk3', 'wk4', 'wks'], + 'application/wmf' => ['wmf'], + 'application/wordperfect' => ['wp', 'wp4', 'wp5', 'wp6', 'wpd', 'wpp'], + 'application/wsdl+xml' => ['wsdl'], + 'application/wspolicy+xml' => ['wspolicy'], + 'application/wwf' => ['wwf'], + 'application/x-123' => ['123', 'wk1', 'wk3', 'wk4', 'wks'], + 'application/x-7z-compressed' => ['7z', '7z.001'], + 'application/x-abiword' => ['abw', 'abw.CRASHED', 'abw.gz', 'zabw'], + 'application/x-ace' => ['ace'], + 'application/x-ace-compressed' => ['ace'], + 'application/x-alz' => ['alz'], + 'application/x-amiga-disk-format' => ['adf'], + 'application/x-amipro' => ['sam'], + 'application/x-annodex' => ['anx'], + 'application/x-aportisdoc' => ['pdb', 'pdc'], + 'application/x-apple-diskimage' => ['dmg'], + 'application/x-apple-systemprofiler+xml' => ['spx'], + 'application/x-appleworks-document' => ['cwk'], + 'application/x-applix-spreadsheet' => ['as'], + 'application/x-applix-word' => ['aw'], + 'application/x-archive' => ['a', 'ar'], + 'application/x-arj' => ['arj'], + 'application/x-asar' => ['asar'], + 'application/x-asp' => ['asp'], + 'application/x-atari-2600-rom' => ['a26'], + 'application/x-atari-7800-rom' => ['a78'], + 'application/x-atari-lynx-rom' => ['lnx'], + 'application/x-authorware-bin' => ['aab', 'x32', 'u32', 'vox'], + 'application/x-authorware-map' => ['aam'], + 'application/x-authorware-seg' => ['aas'], + 'application/x-awk' => ['awk'], + 'application/x-bat' => ['bat'], + 'application/x-bcpio' => ['bcpio'], + 'application/x-bdoc' => ['bdoc'], + 'application/x-bittorrent' => ['torrent'], + 'application/x-blender' => ['blend', 'BLEND', 'blender'], + 'application/x-blorb' => ['blb', 'blorb'], + 'application/x-bps-patch' => ['bps'], + 'application/x-bsdiff' => ['bsdiff'], + 'application/x-bz2' => ['bz2'], + 'application/x-bzdvi' => ['dvi.bz2'], + 'application/x-bzip' => ['bz'], + 'application/x-bzip-compressed-tar' => ['tar.bz', 'tbz', 'tbz2', 'tb2'], + 'application/x-bzip2' => ['bz2', 'boz'], + 'application/x-bzip2-compressed-tar' => ['tar.bz2', 'tbz2', 'tb2'], + 'application/x-bzip3' => ['bz3'], + 'application/x-bzip3-compressed-tar' => ['tar.bz3', 'tbz3'], + 'application/x-bzpdf' => ['pdf.bz2'], + 'application/x-bzpostscript' => ['ps.bz2'], + 'application/x-cb7' => ['cb7'], + 'application/x-cbr' => ['cbr', 'cba', 'cbt', 'cbz', 'cb7'], + 'application/x-cbt' => ['cbt'], + 'application/x-cbz' => ['cbz'], + 'application/x-ccmx' => ['ccmx'], + 'application/x-cd-image' => ['iso', 'iso9660'], + 'application/x-cdlink' => ['vcd'], + 'application/x-cdr' => ['cdr'], + 'application/x-cdrdao-toc' => ['toc'], + 'application/x-cfs-compressed' => ['cfs'], + 'application/x-chat' => ['chat'], + 'application/x-chess-pgn' => ['pgn'], + 'application/x-chm' => ['chm'], + 'application/x-chrome-extension' => ['crx'], + 'application/x-cisco-vpn-settings' => ['pcf'], + 'application/x-cocoa' => ['cco'], + 'application/x-compress' => ['Z'], + 'application/x-compressed-iso' => ['cso'], + 'application/x-compressed-tar' => ['tar.gz', 'tgz'], + 'application/x-conference' => ['nsc'], + 'application/x-coreldraw' => ['cdr'], + 'application/x-cpio' => ['cpio'], + 'application/x-cpio-compressed' => ['cpio.gz'], + 'application/x-csh' => ['csh'], + 'application/x-cue' => ['cue'], + 'application/x-dar' => ['dar'], + 'application/x-dbase' => ['dbf'], + 'application/x-dbf' => ['dbf'], + 'application/x-dc-rom' => ['dc'], + 'application/x-deb' => ['deb', 'udeb'], + 'application/x-debian-package' => ['deb', 'udeb'], + 'application/x-designer' => ['ui'], + 'application/x-desktop' => ['desktop', 'kdelnk'], + 'application/x-dgc-compressed' => ['dgc'], + 'application/x-dia-diagram' => ['dia'], + 'application/x-dia-shape' => ['shape'], + 'application/x-director' => ['dir', 'dcr', 'dxr', 'cst', 'cct', 'cxt', 'w3d', 'fgd', 'swa'], + 'application/x-discjuggler-cd-image' => ['cdi'], + 'application/x-docbook+xml' => ['dbk', 'docbook'], + 'application/x-doom' => ['wad'], + 'application/x-doom-wad' => ['wad'], + 'application/x-dreamcast-rom' => ['iso'], + 'application/x-dtbncx+xml' => ['ncx'], + 'application/x-dtbook+xml' => ['dtb'], + 'application/x-dtbresource+xml' => ['res'], + 'application/x-dvi' => ['dvi'], + 'application/x-e-theme' => ['etheme'], + 'application/x-egon' => ['egon'], + 'application/x-emf' => ['emf'], + 'application/x-envoy' => ['evy'], + 'application/x-eris-link+cbor' => ['eris'], + 'application/x-eva' => ['eva'], + 'application/x-excellon' => ['drl'], + 'application/x-fd-file' => ['fd', 'qd'], + 'application/x-fds-disk' => ['fds'], + 'application/x-fictionbook' => ['fb2'], + 'application/x-fictionbook+xml' => ['fb2'], + 'application/x-fishscript' => ['fish'], + 'application/x-flash-video' => ['flv'], + 'application/x-fluid' => ['fl'], + 'application/x-font-afm' => ['afm'], + 'application/x-font-bdf' => ['bdf'], + 'application/x-font-ghostscript' => ['gsf'], + 'application/x-font-linux-psf' => ['psf'], + 'application/x-font-otf' => ['otf'], + 'application/x-font-pcf' => ['pcf', 'pcf.Z', 'pcf.gz'], + 'application/x-font-snf' => ['snf'], + 'application/x-font-speedo' => ['spd'], + 'application/x-font-truetype' => ['ttf'], + 'application/x-font-ttf' => ['ttf'], + 'application/x-font-ttx' => ['ttx'], + 'application/x-font-type1' => ['pfa', 'pfb', 'pfm', 'afm', 'gsf'], + 'application/x-font-woff' => ['woff'], + 'application/x-frame' => ['fm'], + 'application/x-freearc' => ['arc'], + 'application/x-futuresplash' => ['spl'], + 'application/x-gameboy-color-rom' => ['gbc', 'cgb'], + 'application/x-gameboy-rom' => ['gb', 'sgb'], + 'application/x-gamecube-iso-image' => ['iso'], + 'application/x-gamecube-rom' => ['iso'], + 'application/x-gamegear-rom' => ['gg'], + 'application/x-gba-rom' => ['gba', 'agb'], + 'application/x-gca-compressed' => ['gca'], + 'application/x-gd-rom-cue' => ['gdi'], + 'application/x-gdscript' => ['gd'], + 'application/x-gedcom' => ['ged', 'gedcom'], + 'application/x-genesis-32x-rom' => ['32x', 'mdx'], + 'application/x-genesis-rom' => ['gen', 'smd', 'sgd'], + 'application/x-gerber' => ['gbr'], + 'application/x-gerber-job' => ['gbrjob'], + 'application/x-gettext' => ['po'], + 'application/x-gettext-translation' => ['gmo', 'mo'], + 'application/x-glade' => ['glade'], + 'application/x-glulx' => ['ulx'], + 'application/x-gnome-app-info' => ['desktop', 'kdelnk'], + 'application/x-gnucash' => ['gnucash', 'gnc', 'xac'], + 'application/x-gnumeric' => ['gnumeric'], + 'application/x-gnuplot' => ['gp', 'gplt', 'gnuplot'], + 'application/x-go-sgf' => ['sgf'], + 'application/x-godot-resource' => ['res', 'tres'], + 'application/x-godot-scene' => ['scn', 'tscn', 'escn'], + 'application/x-godot-shader' => ['gdshader'], + 'application/x-gpx' => ['gpx'], + 'application/x-gpx+xml' => ['gpx'], + 'application/x-gramps-xml' => ['gramps'], + 'application/x-graphite' => ['gra'], + 'application/x-gtar' => ['gtar', 'tar', 'gem'], + 'application/x-gtk-builder' => ['ui'], + 'application/x-gz-font-linux-psf' => ['psf.gz'], + 'application/x-gzdvi' => ['dvi.gz'], + 'application/x-gzip' => ['gz'], + 'application/x-gzpdf' => ['pdf.gz'], + 'application/x-gzpostscript' => ['ps.gz'], + 'application/x-hdf' => ['hdf', 'hdf4', 'h4', 'hdf5', 'h5'], + 'application/x-hfe-file' => ['hfe'], + 'application/x-hfe-floppy-image' => ['hfe'], + 'application/x-httpd-php' => ['php'], + 'application/x-hwp' => ['hwp'], + 'application/x-hwt' => ['hwt'], + 'application/x-ica' => ['ica'], + 'application/x-install-instructions' => ['install'], + 'application/x-ips-patch' => ['ips'], + 'application/x-ipynb+json' => ['ipynb'], + 'application/x-iso9660-appimage' => ['appimage'], + 'application/x-iso9660-image' => ['iso', 'iso9660'], + 'application/x-it87' => ['it87'], + 'application/x-iwork-keynote-sffkey' => ['key'], + 'application/x-iwork-numbers-sffnumbers' => ['numbers'], + 'application/x-iwork-pages-sffpages' => ['pages'], + 'application/x-jar' => ['jar'], + 'application/x-java' => ['class'], + 'application/x-java-archive' => ['jar'], + 'application/x-java-archive-diff' => ['jardiff'], + 'application/x-java-class' => ['class'], + 'application/x-java-jce-keystore' => ['jceks'], + 'application/x-java-jnlp-file' => ['jnlp'], + 'application/x-java-keystore' => ['jks', 'ks'], + 'application/x-java-pack200' => ['pack'], + 'application/x-java-vm' => ['class'], + 'application/x-javascript' => ['js', 'jsm', 'mjs'], + 'application/x-jbuilder-project' => ['jpr', 'jpx'], + 'application/x-karbon' => ['karbon'], + 'application/x-kchart' => ['chrt'], + 'application/x-keepass2' => ['kdbx'], + 'application/x-kexi-connectiondata' => ['kexic'], + 'application/x-kexiproject-shortcut' => ['kexis'], + 'application/x-kexiproject-sqlite' => ['kexi'], + 'application/x-kexiproject-sqlite2' => ['kexi'], + 'application/x-kexiproject-sqlite3' => ['kexi'], + 'application/x-kformula' => ['kfo'], + 'application/x-killustrator' => ['kil'], + 'application/x-kivio' => ['flw'], + 'application/x-kontour' => ['kon'], + 'application/x-kpovmodeler' => ['kpm'], + 'application/x-kpresenter' => ['kpr', 'kpt'], + 'application/x-krita' => ['kra', 'krz'], + 'application/x-kspread' => ['ksp'], + 'application/x-kugar' => ['kud'], + 'application/x-kword' => ['kwd', 'kwt'], + 'application/x-latex' => ['latex'], + 'application/x-lha' => ['lha', 'lzh'], + 'application/x-lhz' => ['lhz'], + 'application/x-linguist' => ['ts'], + 'application/x-lmdb' => ['mdb', 'lmdb'], + 'application/x-lotus123' => ['123', 'wk1', 'wk3', 'wk4', 'wks'], + 'application/x-lrzip' => ['lrz'], + 'application/x-lrzip-compressed-tar' => ['tar.lrz', 'tlrz'], + 'application/x-lua-bytecode' => ['luac'], + 'application/x-lyx' => ['lyx'], + 'application/x-lz4' => ['lz4'], + 'application/x-lz4-compressed-tar' => ['tar.lz4'], + 'application/x-lzh-compressed' => ['lzh', 'lha'], + 'application/x-lzip' => ['lz'], + 'application/x-lzip-compressed-tar' => ['tar.lz'], + 'application/x-lzma' => ['lzma'], + 'application/x-lzma-compressed-tar' => ['tar.lzma', 'tlz'], + 'application/x-lzop' => ['lzo'], + 'application/x-lzpdf' => ['pdf.lz'], + 'application/x-m4' => ['m4'], + 'application/x-magicpoint' => ['mgp'], + 'application/x-makeself' => ['run'], + 'application/x-mame-chd' => ['chd'], + 'application/x-markaby' => ['mab'], + 'application/x-mathematica' => ['nb'], + 'application/x-mdb' => ['mdb'], + 'application/x-mie' => ['mie'], + 'application/x-mif' => ['mif'], + 'application/x-mimearchive' => ['mhtml', 'mht'], + 'application/x-mobi8-ebook' => ['azw3', 'kfx'], + 'application/x-mobipocket-ebook' => ['prc', 'mobi'], + 'application/x-modrinth-modpack+zip' => ['mrpack'], + 'application/x-ms-application' => ['application'], + 'application/x-ms-asx' => ['asx', 'wax', 'wvx', 'wmx'], + 'application/x-ms-dos-executable' => ['exe'], + 'application/x-ms-pdb' => ['pdb'], + 'application/x-ms-shortcut' => ['lnk'], + 'application/x-ms-wim' => ['wim', 'swm'], + 'application/x-ms-wmd' => ['wmd'], + 'application/x-ms-wmz' => ['wmz'], + 'application/x-ms-xbap' => ['xbap'], + 'application/x-msaccess' => ['mdb'], + 'application/x-msbinder' => ['obd'], + 'application/x-mscardfile' => ['crd'], + 'application/x-msclip' => ['clp'], + 'application/x-msdos-program' => ['exe'], + 'application/x-msdownload' => ['exe', 'dll', 'com', 'bat', 'msi'], + 'application/x-msexcel' => ['xls', 'xlc', 'xll', 'xlm', 'xlw', 'xla', 'xlt', 'xld'], + 'application/x-msi' => ['msi'], + 'application/x-msmediaview' => ['mvb', 'm13', 'm14'], + 'application/x-msmetafile' => ['wmf', 'wmz', 'emf', 'emz'], + 'application/x-msmoney' => ['mny'], + 'application/x-mspowerpoint' => ['ppz', 'ppt', 'pps', 'pot'], + 'application/x-mspublisher' => ['pub'], + 'application/x-msschedule' => ['scd'], + 'application/x-msterminal' => ['trm'], + 'application/x-mswinurl' => ['url'], + 'application/x-msword' => ['doc'], + 'application/x-mswrite' => ['wri'], + 'application/x-msx-rom' => ['msx'], + 'application/x-n64-rom' => ['n64', 'z64', 'v64'], + 'application/x-navi-animation' => ['ani'], + 'application/x-neo-geo-pocket-color-rom' => ['ngc'], + 'application/x-neo-geo-pocket-rom' => ['ngp'], + 'application/x-nes-rom' => ['nes', 'nez', 'unf', 'unif'], + 'application/x-netcdf' => ['nc', 'cdf'], + 'application/x-netshow-channel' => ['nsc'], + 'application/x-nintendo-3ds-executable' => ['3dsx'], + 'application/x-nintendo-3ds-rom' => ['3ds', 'cci'], + 'application/x-nintendo-ds-rom' => ['nds'], + 'application/x-ns-proxy-autoconfig' => ['pac'], + 'application/x-nuscript' => ['nu'], + 'application/x-nzb' => ['nzb'], + 'application/x-object' => ['o', 'mod'], + 'application/x-ogg' => ['ogx'], + 'application/x-oleo' => ['oleo'], + 'application/x-openvpn-profile' => ['openvpn', 'ovpn'], + 'application/x-openzim' => ['zim'], + 'application/x-pagemaker' => ['p65', 'pm', 'pm6', 'pmd'], + 'application/x-pak' => ['pak'], + 'application/x-palm-database' => ['prc', 'pdb', 'pqa', 'oprc'], + 'application/x-par2' => ['PAR2', 'par2'], + 'application/x-partial-download' => ['wkdownload', 'crdownload', 'part'], + 'application/x-pc-engine-rom' => ['pce'], + 'application/x-pcap' => ['pcap', 'cap', 'dmp'], + 'application/x-pdf' => ['pdf'], + 'application/x-perl' => ['pl', 'pm', 'PL', 'al', 'perl', 'pod', 't'], + 'application/x-photoshop' => ['psd'], + 'application/x-php' => ['php', 'php3', 'php4', 'php5', 'phps'], + 'application/x-pilot' => ['prc', 'pdb'], + 'application/x-pkcs12' => ['p12', 'pfx'], + 'application/x-pkcs7-certificates' => ['p7b', 'spc'], + 'application/x-pkcs7-certreqresp' => ['p7r'], + 'application/x-planperfect' => ['pln'], + 'application/x-pocket-word' => ['psw'], + 'application/x-pw' => ['pw'], + 'application/x-pyspread-bz-spreadsheet' => ['pys'], + 'application/x-pyspread-spreadsheet' => ['pysu'], + 'application/x-python-bytecode' => ['pyc', 'pyo'], + 'application/x-qed-disk' => ['qed'], + 'application/x-qemu-disk' => ['qcow2', 'qcow'], + 'application/x-qpress' => ['qp'], + 'application/x-qtiplot' => ['qti', 'qti.gz'], + 'application/x-quattropro' => ['wb1', 'wb2', 'wb3'], + 'application/x-quicktime-media-link' => ['qtl'], + 'application/x-quicktimeplayer' => ['qtl'], + 'application/x-qw' => ['qif'], + 'application/x-rar' => ['rar'], + 'application/x-rar-compressed' => ['rar'], + 'application/x-raw-disk-image' => ['raw-disk-image', 'img'], + 'application/x-raw-disk-image-xz-compressed' => ['raw-disk-image.xz', 'img.xz'], + 'application/x-raw-floppy-disk-image' => ['fd', 'qd'], + 'application/x-redhat-package-manager' => ['rpm'], + 'application/x-reject' => ['rej'], + 'application/x-research-info-systems' => ['ris'], + 'application/x-rnc' => ['rnc'], + 'application/x-rpm' => ['rpm'], + 'application/x-ruby' => ['rb'], + 'application/x-sami' => ['smi', 'sami'], + 'application/x-sap-file' => ['sap'], + 'application/x-saturn-rom' => ['iso'], + 'application/x-sdp' => ['sdp'], + 'application/x-sea' => ['sea'], + 'application/x-sega-cd-rom' => ['iso'], + 'application/x-sega-pico-rom' => ['iso'], + 'application/x-sg1000-rom' => ['sg'], + 'application/x-sh' => ['sh'], + 'application/x-shar' => ['shar'], + 'application/x-shared-library-la' => ['la'], + 'application/x-sharedlib' => ['so'], + 'application/x-shellscript' => ['sh'], + 'application/x-shockwave-flash' => ['swf', 'spl'], + 'application/x-shorten' => ['shn'], + 'application/x-siag' => ['siag'], + 'application/x-silverlight-app' => ['xap'], + 'application/x-sit' => ['sit'], + 'application/x-sitx' => ['sitx'], + 'application/x-smaf' => ['mmf', 'smaf'], + 'application/x-sms-rom' => ['sms'], + 'application/x-snes-rom' => ['sfc', 'smc'], + 'application/x-source-rpm' => ['src.rpm', 'spm'], + 'application/x-spss-por' => ['por'], + 'application/x-spss-sav' => ['sav', 'zsav'], + 'application/x-spss-savefile' => ['sav', 'zsav'], + 'application/x-sql' => ['sql'], + 'application/x-sqlite2' => ['sqlite2'], + 'application/x-sqlite3' => ['sqlite3'], + 'application/x-srt' => ['srt'], + 'application/x-stuffit' => ['sit'], + 'application/x-stuffitx' => ['sitx'], + 'application/x-subrip' => ['srt'], + 'application/x-sv4cpio' => ['sv4cpio'], + 'application/x-sv4crc' => ['sv4crc'], + 'application/x-t3vm-image' => ['t3'], + 'application/x-t602' => ['602'], + 'application/x-tads' => ['gam'], + 'application/x-tar' => ['tar', 'gtar', 'gem'], + 'application/x-targa' => ['tga', 'icb', 'tpic', 'vda', 'vst'], + 'application/x-tarz' => ['tar.Z', 'taz'], + 'application/x-tcl' => ['tcl', 'tk'], + 'application/x-tex' => ['tex', 'ltx', 'sty', 'cls', 'dtx', 'ins', 'latex'], + 'application/x-tex-gf' => ['gf'], + 'application/x-tex-pk' => ['pk'], + 'application/x-tex-tfm' => ['tfm'], + 'application/x-texinfo' => ['texinfo', 'texi'], + 'application/x-tga' => ['tga', 'icb', 'tpic', 'vda', 'vst'], + 'application/x-tgif' => ['obj'], + 'application/x-theme' => ['theme'], + 'application/x-thomson-cartridge-memo7' => ['m7'], + 'application/x-thomson-cassette' => ['k7'], + 'application/x-thomson-sap-image' => ['sap'], + 'application/x-tiled-tmx' => ['tmx'], + 'application/x-tiled-tsx' => ['tsx'], + 'application/x-trash' => ['bak', 'old', 'sik'], + 'application/x-trig' => ['trig'], + 'application/x-troff' => ['tr', 'roff', 't'], + 'application/x-troff-man' => ['man'], + 'application/x-tzo' => ['tar.lzo', 'tzo'], + 'application/x-ufraw' => ['ufraw'], + 'application/x-ustar' => ['ustar'], + 'application/x-vdi-disk' => ['vdi'], + 'application/x-vhd-disk' => ['vhd', 'vpc'], + 'application/x-vhdx-disk' => ['vhdx'], + 'application/x-virtual-boy-rom' => ['vb'], + 'application/x-virtualbox-hdd' => ['hdd'], + 'application/x-virtualbox-ova' => ['ova'], + 'application/x-virtualbox-ovf' => ['ovf'], + 'application/x-virtualbox-vbox' => ['vbox'], + 'application/x-virtualbox-vbox-extpack' => ['vbox-extpack'], + 'application/x-virtualbox-vdi' => ['vdi'], + 'application/x-virtualbox-vhd' => ['vhd', 'vpc'], + 'application/x-virtualbox-vhdx' => ['vhdx'], + 'application/x-virtualbox-vmdk' => ['vmdk'], + 'application/x-vmdk-disk' => ['vmdk'], + 'application/x-vnd.kde.kexi' => ['kexi'], + 'application/x-wais-source' => ['src'], + 'application/x-wbfs' => ['iso'], + 'application/x-web-app-manifest+json' => ['webapp'], + 'application/x-wia' => ['iso'], + 'application/x-wii-iso-image' => ['iso'], + 'application/x-wii-rom' => ['iso'], + 'application/x-wii-wad' => ['wad'], + 'application/x-windows-themepack' => ['themepack'], + 'application/x-wmf' => ['wmf'], + 'application/x-wonderswan-color-rom' => ['wsc'], + 'application/x-wonderswan-rom' => ['ws'], + 'application/x-wordperfect' => ['wp', 'wp4', 'wp5', 'wp6', 'wpd', 'wpp'], + 'application/x-wpg' => ['wpg'], + 'application/x-wwf' => ['wwf'], + 'application/x-x509-ca-cert' => ['der', 'crt', 'pem', 'cert'], + 'application/x-xar' => ['xar', 'pkg'], + 'application/x-xbel' => ['xbel'], + 'application/x-xfig' => ['fig'], + 'application/x-xliff' => ['xlf', 'xliff'], + 'application/x-xliff+xml' => ['xlf'], + 'application/x-xpinstall' => ['xpi'], + 'application/x-xspf+xml' => ['xspf'], + 'application/x-xz' => ['xz'], + 'application/x-xz-compressed-tar' => ['tar.xz', 'txz'], + 'application/x-xzpdf' => ['pdf.xz'], + 'application/x-yaml' => ['yaml', 'yml'], + 'application/x-zip' => ['zip', 'zipx'], + 'application/x-zip-compressed' => ['zip', 'zipx'], + 'application/x-zip-compressed-fb2' => ['fb2.zip'], + 'application/x-zmachine' => ['z1', 'z2', 'z3', 'z4', 'z5', 'z6', 'z7', 'z8'], + 'application/x-zoo' => ['zoo'], + 'application/x-zpaq' => ['zpaq'], + 'application/x-zstd-compressed-tar' => ['tar.zst', 'tzst'], + 'application/xaml+xml' => ['xaml'], + 'application/xcap-att+xml' => ['xav'], + 'application/xcap-caps+xml' => ['xca'], + 'application/xcap-diff+xml' => ['xdf'], + 'application/xcap-el+xml' => ['xel'], + 'application/xcap-error+xml' => ['xer'], + 'application/xcap-ns+xml' => ['xns'], + 'application/xenc+xml' => ['xenc'], + 'application/xhtml+xml' => ['xhtml', 'xht', 'html', 'htm'], + 'application/xliff+xml' => ['xlf', 'xliff'], + 'application/xml' => ['xml', 'xsl', 'xsd', 'rng', 'xbl'], + 'application/xml-dtd' => ['dtd'], + 'application/xml-external-parsed-entity' => ['ent'], + 'application/xop+xml' => ['xop'], + 'application/xproc+xml' => ['xpl'], + 'application/xps' => ['xps'], + 'application/xslt+xml' => ['xsl', 'xslt'], + 'application/xspf+xml' => ['xspf'], + 'application/xv+xml' => ['mxml', 'xhvml', 'xvml', 'xvm'], + 'application/yaml' => ['yaml', 'yml'], + 'application/yang' => ['yang'], + 'application/yin+xml' => ['yin'], + 'application/zip' => ['zip', 'zipx'], + 'application/zlib' => ['zz'], + 'application/zstd' => ['zst'], + 'audio/3gpp' => ['3gpp', '3gp', '3ga'], + 'audio/3gpp-encrypted' => ['3gp', '3gpp', '3ga'], + 'audio/3gpp2' => ['3g2', '3gp2', '3gpp2'], + 'audio/aac' => ['aac', 'adts', 'ass'], + 'audio/ac3' => ['ac3'], + 'audio/adpcm' => ['adp'], + 'audio/amr' => ['amr'], + 'audio/amr-encrypted' => ['amr'], + 'audio/amr-wb' => ['awb'], + 'audio/amr-wb-encrypted' => ['awb'], + 'audio/annodex' => ['axa'], + 'audio/basic' => ['au', 'snd'], + 'audio/dff' => ['dff'], + 'audio/dsd' => ['dsf'], + 'audio/dsf' => ['dsf'], + 'audio/flac' => ['flac'], + 'audio/imelody' => ['imy', 'ime'], + 'audio/m3u' => ['m3u', 'm3u8', 'vlc'], + 'audio/m4a' => ['m4a', 'f4a'], + 'audio/midi' => ['mid', 'midi', 'kar', 'rmi'], + 'audio/mobile-xmf' => ['mxmf'], + 'audio/mp2' => ['mp2'], + 'audio/mp3' => ['mp3', 'mpga'], + 'audio/mp4' => ['m4a', 'mp4a', 'f4a'], + 'audio/mpeg' => ['mp3', 'mpga', 'mp2', 'mp2a', 'm2a', 'm3a'], + 'audio/mpegurl' => ['m3u', 'm3u8', 'vlc'], + 'audio/ogg' => ['ogg', 'oga', 'spx', 'opus'], + 'audio/prs.sid' => ['sid', 'psid'], + 'audio/s3m' => ['s3m'], + 'audio/scpls' => ['pls'], + 'audio/silk' => ['sil'], + 'audio/tta' => ['tta'], + 'audio/usac' => ['loas', 'xhe'], + 'audio/vnd.audible' => ['aa', 'aax'], + 'audio/vnd.audible.aax' => ['aax'], + 'audio/vnd.audible.aaxc' => ['aaxc'], + 'audio/vnd.dece.audio' => ['uva', 'uvva'], + 'audio/vnd.digital-winds' => ['eol'], + 'audio/vnd.dra' => ['dra'], + 'audio/vnd.dts' => ['dts'], + 'audio/vnd.dts.hd' => ['dtshd'], + 'audio/vnd.lucent.voice' => ['lvp'], + 'audio/vnd.m-realaudio' => ['ra', 'rax'], + 'audio/vnd.ms-playready.media.pya' => ['pya'], + 'audio/vnd.nokia.mobile-xmf' => ['mxmf'], + 'audio/vnd.nuera.ecelp4800' => ['ecelp4800'], + 'audio/vnd.nuera.ecelp7470' => ['ecelp7470'], + 'audio/vnd.nuera.ecelp9600' => ['ecelp9600'], + 'audio/vnd.rip' => ['rip'], + 'audio/vnd.rn-realaudio' => ['ra', 'rax'], + 'audio/vnd.wave' => ['wav'], + 'audio/vorbis' => ['oga', 'ogg'], + 'audio/wav' => ['wav'], + 'audio/wave' => ['wav'], + 'audio/webm' => ['weba'], + 'audio/wma' => ['wma'], + 'audio/x-aac' => ['aac', 'adts', 'ass'], + 'audio/x-aifc' => ['aifc', 'aiffc'], + 'audio/x-aiff' => ['aif', 'aiff', 'aifc'], + 'audio/x-aiffc' => ['aifc', 'aiffc'], + 'audio/x-amzxml' => ['amz'], + 'audio/x-annodex' => ['axa'], + 'audio/x-ape' => ['ape'], + 'audio/x-caf' => ['caf'], + 'audio/x-dff' => ['dff'], + 'audio/x-dsd' => ['dsf'], + 'audio/x-dsf' => ['dsf'], + 'audio/x-dts' => ['dts'], + 'audio/x-dtshd' => ['dtshd'], + 'audio/x-flac' => ['flac'], + 'audio/x-flac+ogg' => ['oga', 'ogg'], + 'audio/x-gsm' => ['gsm'], + 'audio/x-hx-aac-adts' => ['aac', 'adts', 'ass'], + 'audio/x-imelody' => ['imy', 'ime'], + 'audio/x-iriver-pla' => ['pla'], + 'audio/x-it' => ['it'], + 'audio/x-m3u' => ['m3u', 'm3u8', 'vlc'], + 'audio/x-m4a' => ['m4a', 'f4a'], + 'audio/x-m4b' => ['m4b', 'f4b'], + 'audio/x-m4r' => ['m4r'], + 'audio/x-matroska' => ['mka'], + 'audio/x-midi' => ['mid', 'midi', 'kar'], + 'audio/x-minipsf' => ['minipsf'], + 'audio/x-mo3' => ['mo3'], + 'audio/x-mod' => ['mod', 'ult', 'uni', 'm15', 'mtm', '669', 'med'], + 'audio/x-mp2' => ['mp2'], + 'audio/x-mp3' => ['mp3', 'mpga'], + 'audio/x-mp3-playlist' => ['m3u', 'm3u8', 'vlc'], + 'audio/x-mpeg' => ['mp3', 'mpga'], + 'audio/x-mpegurl' => ['m3u', 'm3u8', 'vlc'], + 'audio/x-mpg' => ['mp3', 'mpga'], + 'audio/x-ms-asx' => ['asx', 'wax', 'wvx', 'wmx'], + 'audio/x-ms-wax' => ['wax'], + 'audio/x-ms-wma' => ['wma'], + 'audio/x-ms-wmv' => ['wmv'], + 'audio/x-musepack' => ['mpc', 'mpp', 'mp+'], + 'audio/x-ogg' => ['oga', 'ogg', 'opus'], + 'audio/x-oggflac' => ['oga', 'ogg'], + 'audio/x-opus+ogg' => ['opus'], + 'audio/x-pn-audibleaudio' => ['aa', 'aax'], + 'audio/x-pn-realaudio' => ['ram', 'ra', 'rax'], + 'audio/x-pn-realaudio-plugin' => ['rmp'], + 'audio/x-psf' => ['psf'], + 'audio/x-psflib' => ['psflib'], + 'audio/x-realaudio' => ['ra'], + 'audio/x-rn-3gpp-amr' => ['3gp', '3gpp', '3ga'], + 'audio/x-rn-3gpp-amr-encrypted' => ['3gp', '3gpp', '3ga'], + 'audio/x-rn-3gpp-amr-wb' => ['3gp', '3gpp', '3ga'], + 'audio/x-rn-3gpp-amr-wb-encrypted' => ['3gp', '3gpp', '3ga'], + 'audio/x-s3m' => ['s3m'], + 'audio/x-scpls' => ['pls'], + 'audio/x-shorten' => ['shn'], + 'audio/x-speex' => ['spx'], + 'audio/x-speex+ogg' => ['oga', 'ogg', 'spx'], + 'audio/x-stm' => ['stm'], + 'audio/x-tak' => ['tak'], + 'audio/x-tta' => ['tta'], + 'audio/x-voc' => ['voc'], + 'audio/x-vorbis' => ['oga', 'ogg'], + 'audio/x-vorbis+ogg' => ['oga', 'ogg'], + 'audio/x-wav' => ['wav'], + 'audio/x-wavpack' => ['wv', 'wvp'], + 'audio/x-wavpack-correction' => ['wvc'], + 'audio/x-xi' => ['xi'], + 'audio/x-xm' => ['xm'], + 'audio/x-xmf' => ['xmf'], + 'audio/xm' => ['xm'], + 'audio/xmf' => ['xmf'], + 'chemical/x-cdx' => ['cdx'], + 'chemical/x-cif' => ['cif'], + 'chemical/x-cmdf' => ['cmdf'], + 'chemical/x-cml' => ['cml'], + 'chemical/x-csml' => ['csml'], + 'chemical/x-xyz' => ['xyz'], + 'flv-application/octet-stream' => ['flv'], + 'font/collection' => ['ttc'], + 'font/otf' => ['otf'], + 'font/ttf' => ['ttf'], + 'font/woff' => ['woff'], + 'font/woff2' => ['woff2'], + 'image/aces' => ['exr'], + 'image/apng' => ['apng', 'png'], + 'image/astc' => ['astc'], + 'image/avci' => ['avci'], + 'image/avcs' => ['avcs'], + 'image/avif' => ['avif', 'avifs'], + 'image/avif-sequence' => ['avif', 'avifs'], + 'image/bmp' => ['bmp', 'dib'], + 'image/cdr' => ['cdr'], + 'image/cgm' => ['cgm'], + 'image/dicom-rle' => ['drle'], + 'image/emf' => ['emf'], + 'image/fax-g3' => ['g3'], + 'image/fits' => ['fits', 'fit', 'fts'], + 'image/g3fax' => ['g3'], + 'image/gif' => ['gif'], + 'image/heic' => ['heic', 'heif', 'hif'], + 'image/heic-sequence' => ['heics', 'heic', 'heif', 'hif'], + 'image/heif' => ['heif', 'heic', 'hif'], + 'image/heif-sequence' => ['heifs', 'heic', 'heif', 'hif'], + 'image/hej2k' => ['hej2'], + 'image/hsj2' => ['hsj2'], + 'image/ico' => ['ico'], + 'image/icon' => ['ico'], + 'image/ief' => ['ief'], + 'image/jls' => ['jls'], + 'image/jp2' => ['jp2', 'jpg2'], + 'image/jpeg' => ['jpg', 'jpeg', 'jpe'], + 'image/jpeg2000' => ['jp2', 'jpg2'], + 'image/jpeg2000-image' => ['jp2', 'jpg2'], + 'image/jph' => ['jph'], + 'image/jphc' => ['jhc'], + 'image/jpm' => ['jpm', 'jpgm'], + 'image/jpx' => ['jpx', 'jpf'], + 'image/jxl' => ['jxl'], + 'image/jxr' => ['jxr', 'hdp', 'wdp'], + 'image/jxra' => ['jxra'], + 'image/jxrs' => ['jxrs'], + 'image/jxs' => ['jxs'], + 'image/jxsc' => ['jxsc'], + 'image/jxsi' => ['jxsi'], + 'image/jxss' => ['jxss'], + 'image/ktx' => ['ktx'], + 'image/ktx2' => ['ktx2'], + 'image/openraster' => ['ora'], + 'image/pdf' => ['pdf'], + 'image/photoshop' => ['psd'], + 'image/pjpeg' => ['jpg', 'jpeg', 'jpe'], + 'image/png' => ['png'], + 'image/prs.btif' => ['btif'], + 'image/prs.pti' => ['pti'], + 'image/psd' => ['psd'], + 'image/qoi' => ['qoi'], + 'image/rle' => ['rle'], + 'image/sgi' => ['sgi'], + 'image/svg' => ['svg'], + 'image/svg+xml' => ['svg', 'svgz'], + 'image/svg+xml-compressed' => ['svgz', 'svg.gz'], + 'image/t38' => ['t38'], + 'image/targa' => ['tga', 'icb', 'tpic', 'vda', 'vst'], + 'image/tga' => ['tga', 'icb', 'tpic', 'vda', 'vst'], + 'image/tiff' => ['tif', 'tiff'], + 'image/tiff-fx' => ['tfx'], + 'image/vnd.adobe.photoshop' => ['psd'], + 'image/vnd.airzip.accelerator.azv' => ['azv'], + 'image/vnd.dece.graphic' => ['uvi', 'uvvi', 'uvg', 'uvvg'], + 'image/vnd.djvu' => ['djvu', 'djv'], + 'image/vnd.djvu+multipage' => ['djvu', 'djv'], + 'image/vnd.dvb.subtitle' => ['sub'], + 'image/vnd.dwg' => ['dwg'], + 'image/vnd.dxf' => ['dxf'], + 'image/vnd.fastbidsheet' => ['fbs'], + 'image/vnd.fpx' => ['fpx'], + 'image/vnd.fst' => ['fst'], + 'image/vnd.fujixerox.edmics-mmr' => ['mmr'], + 'image/vnd.fujixerox.edmics-rlc' => ['rlc'], + 'image/vnd.microsoft.icon' => ['ico'], + 'image/vnd.mozilla.apng' => ['apng', 'png'], + 'image/vnd.ms-dds' => ['dds'], + 'image/vnd.ms-modi' => ['mdi'], + 'image/vnd.ms-photo' => ['wdp', 'jxr', 'hdp'], + 'image/vnd.net-fpx' => ['npx'], + 'image/vnd.pco.b16' => ['b16'], + 'image/vnd.rn-realpix' => ['rp'], + 'image/vnd.tencent.tap' => ['tap'], + 'image/vnd.valve.source.texture' => ['vtf'], + 'image/vnd.wap.wbmp' => ['wbmp'], + 'image/vnd.xiff' => ['xif'], + 'image/vnd.zbrush.pcx' => ['pcx'], + 'image/webp' => ['webp'], + 'image/wmf' => ['wmf'], + 'image/x-3ds' => ['3ds'], + 'image/x-adobe-dng' => ['dng'], + 'image/x-applix-graphics' => ['ag'], + 'image/x-bmp' => ['bmp', 'dib'], + 'image/x-bzeps' => ['eps.bz2', 'epsi.bz2', 'epsf.bz2'], + 'image/x-canon-cr2' => ['cr2'], + 'image/x-canon-cr3' => ['cr3'], + 'image/x-canon-crw' => ['crw'], + 'image/x-cdr' => ['cdr'], + 'image/x-cmu-raster' => ['ras'], + 'image/x-cmx' => ['cmx'], + 'image/x-compressed-xcf' => ['xcf.gz', 'xcf.bz2'], + 'image/x-dds' => ['dds'], + 'image/x-djvu' => ['djvu', 'djv'], + 'image/x-emf' => ['emf'], + 'image/x-eps' => ['eps', 'epsi', 'epsf'], + 'image/x-exr' => ['exr'], + 'image/x-fits' => ['fits', 'fit', 'fts'], + 'image/x-freehand' => ['fh', 'fhc', 'fh4', 'fh5', 'fh7'], + 'image/x-fuji-raf' => ['raf'], + 'image/x-gimp-gbr' => ['gbr'], + 'image/x-gimp-gih' => ['gih'], + 'image/x-gimp-pat' => ['pat'], + 'image/x-gzeps' => ['eps.gz', 'epsi.gz', 'epsf.gz'], + 'image/x-icb' => ['tga', 'icb', 'tpic', 'vda', 'vst'], + 'image/x-icns' => ['icns'], + 'image/x-ico' => ['ico'], + 'image/x-icon' => ['ico'], + 'image/x-iff' => ['iff', 'ilbm', 'lbm'], + 'image/x-ilbm' => ['iff', 'ilbm', 'lbm'], + 'image/x-jng' => ['jng'], + 'image/x-jp2-codestream' => ['j2c', 'j2k', 'jpc'], + 'image/x-jpeg2000-image' => ['jp2', 'jpg2'], + 'image/x-kodak-dcr' => ['dcr'], + 'image/x-kodak-k25' => ['k25'], + 'image/x-kodak-kdc' => ['kdc'], + 'image/x-lwo' => ['lwo', 'lwob'], + 'image/x-lws' => ['lws'], + 'image/x-macpaint' => ['pntg'], + 'image/x-minolta-mrw' => ['mrw'], + 'image/x-mrsid-image' => ['sid'], + 'image/x-ms-bmp' => ['bmp', 'dib'], + 'image/x-msod' => ['msod'], + 'image/x-nikon-nef' => ['nef'], + 'image/x-nikon-nrw' => ['nrw'], + 'image/x-olympus-orf' => ['orf'], + 'image/x-panasonic-raw' => ['raw'], + 'image/x-panasonic-raw2' => ['rw2'], + 'image/x-panasonic-rw' => ['raw'], + 'image/x-panasonic-rw2' => ['rw2'], + 'image/x-pcx' => ['pcx'], + 'image/x-pentax-pef' => ['pef'], + 'image/x-photo-cd' => ['pcd'], + 'image/x-photoshop' => ['psd'], + 'image/x-pict' => ['pic', 'pct', 'pict', 'pict1', 'pict2'], + 'image/x-portable-anymap' => ['pnm'], + 'image/x-portable-bitmap' => ['pbm'], + 'image/x-portable-graymap' => ['pgm'], + 'image/x-portable-pixmap' => ['ppm'], + 'image/x-psd' => ['psd'], + 'image/x-quicktime' => ['qtif', 'qif'], + 'image/x-rgb' => ['rgb'], + 'image/x-sgi' => ['sgi'], + 'image/x-sigma-x3f' => ['x3f'], + 'image/x-skencil' => ['sk', 'sk1'], + 'image/x-sony-arw' => ['arw'], + 'image/x-sony-sr2' => ['sr2'], + 'image/x-sony-srf' => ['srf'], + 'image/x-sun-raster' => ['sun'], + 'image/x-targa' => ['tga', 'icb', 'tpic', 'vda', 'vst'], + 'image/x-tga' => ['tga', 'icb', 'tpic', 'vda', 'vst'], + 'image/x-win-bitmap' => ['cur'], + 'image/x-win-metafile' => ['wmf'], + 'image/x-wmf' => ['wmf'], + 'image/x-xbitmap' => ['xbm'], + 'image/x-xcf' => ['xcf'], + 'image/x-xfig' => ['fig'], + 'image/x-xpixmap' => ['xpm'], + 'image/x-xpm' => ['xpm'], + 'image/x-xwindowdump' => ['xwd'], + 'image/x.djvu' => ['djvu', 'djv'], + 'message/disposition-notification' => ['disposition-notification'], + 'message/global' => ['u8msg'], + 'message/global-delivery-status' => ['u8dsn'], + 'message/global-disposition-notification' => ['u8mdn'], + 'message/global-headers' => ['u8hdr'], + 'message/rfc822' => ['eml', 'mime'], + 'message/vnd.wfa.wsc' => ['wsc'], + 'model/3mf' => ['3mf'], + 'model/gltf+json' => ['gltf'], + 'model/gltf-binary' => ['glb'], + 'model/iges' => ['igs', 'iges'], + 'model/mesh' => ['msh', 'mesh', 'silo'], + 'model/mtl' => ['mtl'], + 'model/obj' => ['obj'], + 'model/step+xml' => ['stpx'], + 'model/step+zip' => ['stpz'], + 'model/step-xml+zip' => ['stpxz'], + 'model/stl' => ['stl'], + 'model/vnd.collada+xml' => ['dae'], + 'model/vnd.dwf' => ['dwf'], + 'model/vnd.gdl' => ['gdl'], + 'model/vnd.gtw' => ['gtw'], + 'model/vnd.mts' => ['mts'], + 'model/vnd.opengex' => ['ogex'], + 'model/vnd.parasolid.transmit.binary' => ['x_b'], + 'model/vnd.parasolid.transmit.text' => ['x_t'], + 'model/vnd.sap.vds' => ['vds'], + 'model/vnd.usdz+zip' => ['usdz'], + 'model/vnd.valve.source.compiled-map' => ['bsp'], + 'model/vnd.vtu' => ['vtu'], + 'model/vrml' => ['wrl', 'vrml', 'vrm'], + 'model/x.stl-ascii' => ['stl'], + 'model/x.stl-binary' => ['stl'], + 'model/x3d+binary' => ['x3db', 'x3dbz'], + 'model/x3d+fastinfoset' => ['x3db'], + 'model/x3d+vrml' => ['x3dv', 'x3dvz'], + 'model/x3d+xml' => ['x3d', 'x3dz'], + 'model/x3d-vrml' => ['x3dv'], + 'text/cache-manifest' => ['appcache', 'manifest'], + 'text/calendar' => ['ics', 'ifb', 'vcs'], + 'text/coffeescript' => ['coffee', 'litcoffee'], + 'text/crystal' => ['cr'], + 'text/css' => ['css'], + 'text/csv' => ['csv'], + 'text/csv-schema' => ['csvs'], + 'text/directory' => ['vcard', 'vcf', 'vct', 'gcrd'], + 'text/ecmascript' => ['es'], + 'text/gedcom' => ['ged', 'gedcom'], + 'text/google-video-pointer' => ['gvp'], + 'text/html' => ['html', 'htm', 'shtml'], + 'text/ico' => ['ico'], + 'text/jade' => ['jade'], + 'text/javascript' => ['js', 'jsm', 'mjs'], + 'text/jsx' => ['jsx'], + 'text/julia' => ['jl'], + 'text/less' => ['less'], + 'text/markdown' => ['md', 'markdown', 'mkd'], + 'text/mathml' => ['mml'], + 'text/mdx' => ['mdx'], + 'text/n3' => ['n3'], + 'text/org' => ['org'], + 'text/plain' => ['txt', 'text', 'conf', 'def', 'list', 'log', 'in', 'ini', 'asc'], + 'text/prs.lines.tag' => ['dsc'], + 'text/rdf' => ['rdf', 'rdfs', 'owl'], + 'text/richtext' => ['rtx'], + 'text/rss' => ['rss'], + 'text/rtf' => ['rtf'], + 'text/rust' => ['rs'], + 'text/sgml' => ['sgml', 'sgm'], + 'text/shex' => ['shex'], + 'text/slim' => ['slim', 'slm'], + 'text/spdx' => ['spdx'], + 'text/spreadsheet' => ['sylk', 'slk'], + 'text/stylus' => ['stylus', 'styl'], + 'text/tab-separated-values' => ['tsv'], + 'text/tcl' => ['tcl', 'tk'], + 'text/troff' => ['t', 'tr', 'roff', 'man', 'me', 'ms'], + 'text/turtle' => ['ttl'], + 'text/uri-list' => ['uri', 'uris', 'urls'], + 'text/vbs' => ['vbs'], + 'text/vbscript' => ['vbs'], + 'text/vcard' => ['vcard', 'vcf', 'vct', 'gcrd'], + 'text/vnd.curl' => ['curl'], + 'text/vnd.curl.dcurl' => ['dcurl'], + 'text/vnd.curl.mcurl' => ['mcurl'], + 'text/vnd.curl.scurl' => ['scurl'], + 'text/vnd.dvb.subtitle' => ['sub'], + 'text/vnd.familysearch.gedcom' => ['ged', 'gedcom'], + 'text/vnd.fly' => ['fly'], + 'text/vnd.fmi.flexstor' => ['flx'], + 'text/vnd.graphviz' => ['gv', 'dot'], + 'text/vnd.in3d.3dml' => ['3dml'], + 'text/vnd.in3d.spot' => ['spot'], + 'text/vnd.qt.linguist' => ['ts'], + 'text/vnd.rn-realtext' => ['rt'], + 'text/vnd.senx.warpscript' => ['mc2'], + 'text/vnd.sun.j2me.app-descriptor' => ['jad'], + 'text/vnd.trolltech.linguist' => ['ts'], + 'text/vnd.wap.wml' => ['wml'], + 'text/vnd.wap.wmlscript' => ['wmls'], + 'text/vtt' => ['vtt'], + 'text/x-adasrc' => ['adb', 'ads'], + 'text/x-asm' => ['s', 'asm'], + 'text/x-bibtex' => ['bib'], + 'text/x-blueprint' => ['blp'], + 'text/x-c' => ['c', 'cc', 'cxx', 'cpp', 'h', 'hh', 'dic'], + 'text/x-c++hdr' => ['hh', 'hp', 'hpp', 'h++', 'hxx'], + 'text/x-c++src' => ['cpp', 'cxx', 'cc', 'C', 'c++'], + 'text/x-chdr' => ['h'], + 'text/x-cmake' => ['cmake'], + 'text/x-cobol' => ['cbl', 'cob'], + 'text/x-comma-separated-values' => ['csv'], + 'text/x-common-lisp' => ['asd', 'fasl', 'lisp', 'ros'], + 'text/x-component' => ['htc'], + 'text/x-crystal' => ['cr'], + 'text/x-csharp' => ['cs'], + 'text/x-csrc' => ['c'], + 'text/x-csv' => ['csv'], + 'text/x-dart' => ['dart'], + 'text/x-dbus-service' => ['service'], + 'text/x-dcl' => ['dcl'], + 'text/x-devicetree-binary' => ['dtb'], + 'text/x-devicetree-source' => ['dts', 'dtsi'], + 'text/x-diff' => ['diff', 'patch'], + 'text/x-dsl' => ['dsl'], + 'text/x-dsrc' => ['d', 'di'], + 'text/x-dtd' => ['dtd'], + 'text/x-eiffel' => ['e', 'eif'], + 'text/x-elixir' => ['ex', 'exs'], + 'text/x-emacs-lisp' => ['el'], + 'text/x-erlang' => ['erl'], + 'text/x-fish' => ['fish'], + 'text/x-fortran' => ['f', 'for', 'f77', 'f90', 'f95'], + 'text/x-gcode-gx' => ['gx'], + 'text/x-genie' => ['gs'], + 'text/x-gettext-translation' => ['po'], + 'text/x-gettext-translation-template' => ['pot'], + 'text/x-gherkin' => ['feature'], + 'text/x-go' => ['go'], + 'text/x-google-video-pointer' => ['gvp'], + 'text/x-gradle' => ['gradle'], + 'text/x-groovy' => ['groovy', 'gvy', 'gy', 'gsh'], + 'text/x-handlebars-template' => ['hbs'], + 'text/x-haskell' => ['hs'], + 'text/x-idl' => ['idl'], + 'text/x-imelody' => ['imy', 'ime'], + 'text/x-iptables' => ['iptables'], + 'text/x-java' => ['java'], + 'text/x-java-source' => ['java'], + 'text/x-kaitai-struct' => ['ksy'], + 'text/x-kotlin' => ['kt'], + 'text/x-ldif' => ['ldif'], + 'text/x-lilypond' => ['ly'], + 'text/x-literate-haskell' => ['lhs'], + 'text/x-log' => ['log'], + 'text/x-lua' => ['lua'], + 'text/x-lyx' => ['lyx'], + 'text/x-makefile' => ['mk', 'mak'], + 'text/x-markdown' => ['md', 'mkd', 'markdown'], + 'text/x-matlab' => ['m'], + 'text/x-microdvd' => ['sub'], + 'text/x-moc' => ['moc'], + 'text/x-modelica' => ['mo'], + 'text/x-mof' => ['mof'], + 'text/x-mpl2' => ['mpl'], + 'text/x-mpsub' => ['sub'], + 'text/x-mrml' => ['mrml', 'mrl'], + 'text/x-ms-regedit' => ['reg'], + 'text/x-mup' => ['mup', 'not'], + 'text/x-nfo' => ['nfo'], + 'text/x-nim' => ['nim'], + 'text/x-nimscript' => ['nims', 'nimble'], + 'text/x-nu' => ['nu'], + 'text/x-objc++src' => ['mm'], + 'text/x-objcsrc' => ['m'], + 'text/x-ocaml' => ['ml', 'mli'], + 'text/x-ocl' => ['ocl'], + 'text/x-octave' => ['m'], + 'text/x-ooc' => ['ooc'], + 'text/x-opencl-src' => ['cl'], + 'text/x-opml' => ['opml'], + 'text/x-opml+xml' => ['opml'], + 'text/x-org' => ['org'], + 'text/x-pascal' => ['p', 'pas'], + 'text/x-patch' => ['diff', 'patch'], + 'text/x-perl' => ['pl', 'PL', 'pm', 'al', 'perl', 'pod', 't'], + 'text/x-po' => ['po'], + 'text/x-pot' => ['pot'], + 'text/x-processing' => ['pde'], + 'text/x-python' => ['py', 'pyx', 'wsgi'], + 'text/x-python3' => ['py', 'py3', 'py3x', 'pyi'], + 'text/x-qml' => ['qml', 'qmltypes', 'qmlproject'], + 'text/x-reject' => ['rej'], + 'text/x-rpm-spec' => ['spec'], + 'text/x-rst' => ['rst'], + 'text/x-sagemath' => ['sage'], + 'text/x-sass' => ['sass'], + 'text/x-scala' => ['scala', 'sc'], + 'text/x-scheme' => ['scm', 'ss'], + 'text/x-scss' => ['scss'], + 'text/x-setext' => ['etx'], + 'text/x-sfv' => ['sfv'], + 'text/x-sh' => ['sh'], + 'text/x-sql' => ['sql'], + 'text/x-ssa' => ['ssa', 'ass'], + 'text/x-subviewer' => ['sub'], + 'text/x-suse-ymp' => ['ymp'], + 'text/x-svhdr' => ['svh'], + 'text/x-svsrc' => ['sv'], + 'text/x-systemd-unit' => ['automount', 'device', 'mount', 'path', 'scope', 'service', 'slice', 'socket', 'swap', 'target', 'timer'], + 'text/x-tcl' => ['tcl', 'tk'], + 'text/x-tex' => ['tex', 'ltx', 'sty', 'cls', 'dtx', 'ins', 'latex'], + 'text/x-texinfo' => ['texi', 'texinfo'], + 'text/x-troff' => ['tr', 'roff', 't'], + 'text/x-troff-me' => ['me'], + 'text/x-troff-mm' => ['mm'], + 'text/x-troff-ms' => ['ms'], + 'text/x-twig' => ['twig'], + 'text/x-txt2tags' => ['t2t'], + 'text/x-typst' => ['typ'], + 'text/x-uil' => ['uil'], + 'text/x-uuencode' => ['uu', 'uue'], + 'text/x-vala' => ['vala', 'vapi'], + 'text/x-vcalendar' => ['vcs', 'ics'], + 'text/x-vcard' => ['vcf', 'vcard', 'vct', 'gcrd'], + 'text/x-verilog' => ['v'], + 'text/x-vhdl' => ['vhd', 'vhdl'], + 'text/x-xmi' => ['xmi'], + 'text/x-xslfo' => ['fo', 'xslfo'], + 'text/x-yaml' => ['yaml', 'yml'], + 'text/x.gcode' => ['gcode'], + 'text/xml' => ['xml', 'xbl', 'xsd', 'rng'], + 'text/xml-external-parsed-entity' => ['ent'], + 'text/yaml' => ['yaml', 'yml'], + 'video/3gp' => ['3gp', '3gpp', '3ga'], + 'video/3gpp' => ['3gp', '3gpp', '3ga'], + 'video/3gpp-encrypted' => ['3gp', '3gpp', '3ga'], + 'video/3gpp2' => ['3g2', '3gp2', '3gpp2'], + 'video/annodex' => ['axv'], + 'video/avi' => ['avi', 'avf', 'divx'], + 'video/divx' => ['avi', 'avf', 'divx'], + 'video/dv' => ['dv'], + 'video/fli' => ['fli', 'flc'], + 'video/flv' => ['flv'], + 'video/h261' => ['h261'], + 'video/h263' => ['h263'], + 'video/h264' => ['h264'], + 'video/iso.segment' => ['m4s'], + 'video/jpeg' => ['jpgv'], + 'video/jpm' => ['jpm', 'jpgm'], + 'video/mj2' => ['mj2', 'mjp2'], + 'video/mp2t' => ['ts', 'm2t', 'm2ts', 'mts', 'cpi', 'clpi', 'mpl', 'mpls', 'bdm', 'bdmv'], + 'video/mp4' => ['mp4', 'mp4v', 'mpg4', 'm4v', 'f4v', 'lrv'], + 'video/mp4v-es' => ['mp4', 'm4v', 'f4v', 'lrv'], + 'video/mpeg' => ['mpeg', 'mpg', 'mpe', 'm1v', 'm2v', 'mp2', 'vob'], + 'video/mpeg-system' => ['mpeg', 'mpg', 'mp2', 'mpe', 'vob'], + 'video/msvideo' => ['avi', 'avf', 'divx'], + 'video/ogg' => ['ogv', 'ogg'], + 'video/quicktime' => ['mov', 'qt', 'moov', 'qtvr'], + 'video/vivo' => ['viv', 'vivo'], + 'video/vnd.avi' => ['avi', 'avf', 'divx'], + 'video/vnd.dece.hd' => ['uvh', 'uvvh'], + 'video/vnd.dece.mobile' => ['uvm', 'uvvm'], + 'video/vnd.dece.pd' => ['uvp', 'uvvp'], + 'video/vnd.dece.sd' => ['uvs', 'uvvs'], + 'video/vnd.dece.video' => ['uvv', 'uvvv'], + 'video/vnd.divx' => ['avi', 'avf', 'divx'], + 'video/vnd.dvb.file' => ['dvb'], + 'video/vnd.fvt' => ['fvt'], + 'video/vnd.mpegurl' => ['mxu', 'm4u', 'm1u'], + 'video/vnd.ms-playready.media.pyv' => ['pyv'], + 'video/vnd.radgamettools.bink' => ['bik', 'bk2'], + 'video/vnd.radgamettools.smacker' => ['smk'], + 'video/vnd.rn-realvideo' => ['rv', 'rvx'], + 'video/vnd.uvvu.mp4' => ['uvu', 'uvvu'], + 'video/vnd.vivo' => ['viv', 'vivo'], + 'video/vnd.youtube.yt' => ['yt'], + 'video/webm' => ['webm'], + 'video/x-anim' => ['anim1', 'anim2', 'anim3', 'anim4', 'anim5', 'anim6', 'anim7', 'anim8', 'anim9', 'animj'], + 'video/x-annodex' => ['axv'], + 'video/x-avi' => ['avi', 'avf', 'divx'], + 'video/x-f4v' => ['f4v'], + 'video/x-fli' => ['fli', 'flc'], + 'video/x-flic' => ['fli', 'flc'], + 'video/x-flv' => ['flv'], + 'video/x-javafx' => ['fxm'], + 'video/x-m4v' => ['m4v', 'mp4', 'f4v', 'lrv'], + 'video/x-matroska' => ['mkv', 'mk3d', 'mks'], + 'video/x-matroska-3d' => ['mk3d'], + 'video/x-mjpeg' => ['mjpeg', 'mjpg'], + 'video/x-mng' => ['mng'], + 'video/x-mpeg' => ['mpeg', 'mpg', 'mp2', 'mpe', 'vob'], + 'video/x-mpeg-system' => ['mpeg', 'mpg', 'mp2', 'mpe', 'vob'], + 'video/x-mpeg2' => ['mpeg', 'mpg', 'mp2', 'mpe', 'vob'], + 'video/x-mpegurl' => ['m1u', 'm4u', 'mxu'], + 'video/x-ms-asf' => ['asf', 'asx'], + 'video/x-ms-asf-plugin' => ['asf'], + 'video/x-ms-vob' => ['vob'], + 'video/x-ms-wax' => ['asx', 'wax', 'wvx', 'wmx'], + 'video/x-ms-wm' => ['wm', 'asf'], + 'video/x-ms-wmv' => ['wmv'], + 'video/x-ms-wmx' => ['wmx', 'asx', 'wax', 'wvx'], + 'video/x-ms-wvx' => ['wvx', 'asx', 'wax', 'wmx'], + 'video/x-msvideo' => ['avi', 'avf', 'divx'], + 'video/x-nsv' => ['nsv'], + 'video/x-ogg' => ['ogv', 'ogg'], + 'video/x-ogm' => ['ogm'], + 'video/x-ogm+ogg' => ['ogm'], + 'video/x-real-video' => ['rv', 'rvx'], + 'video/x-sgi-movie' => ['movie'], + 'video/x-smv' => ['smv'], + 'video/x-theora' => ['ogg'], + 'video/x-theora+ogg' => ['ogg'], + 'x-conference/x-cooltalk' => ['ice'], + 'x-epoc/x-sisx-app' => ['sisx'], + 'zz-application/zz-winassoc-123' => ['123', 'wk1', 'wk3', 'wk4', 'wks'], + 'zz-application/zz-winassoc-cab' => ['cab'], + 'zz-application/zz-winassoc-cdr' => ['cdr'], + 'zz-application/zz-winassoc-doc' => ['doc'], + 'zz-application/zz-winassoc-hlp' => ['hlp'], + 'zz-application/zz-winassoc-mdb' => ['mdb'], + 'zz-application/zz-winassoc-uu' => ['uue'], + 'zz-application/zz-winassoc-xls' => ['xls', 'xlc', 'xll', 'xlm', 'xlw', 'xla', 'xlt', 'xld'], + ]; + + private const REVERSE_MAP = [ + '123' => ['application/lotus123', 'application/vnd.lotus-1-2-3', 'application/wk1', 'application/x-123', 'application/x-lotus123', 'zz-application/zz-winassoc-123'], + '1km' => ['application/vnd.1000minds.decision-model+xml'], + '32x' => ['application/x-genesis-32x-rom'], + '3dml' => ['text/vnd.in3d.3dml'], + '3ds' => ['application/x-nintendo-3ds-rom', 'image/x-3ds'], + '3dsx' => ['application/x-nintendo-3ds-executable'], + '3g2' => ['audio/3gpp2', 'video/3gpp2'], + '3ga' => ['audio/3gpp', 'audio/3gpp-encrypted', 'audio/x-rn-3gpp-amr', 'audio/x-rn-3gpp-amr-encrypted', 'audio/x-rn-3gpp-amr-wb', 'audio/x-rn-3gpp-amr-wb-encrypted', 'video/3gp', 'video/3gpp', 'video/3gpp-encrypted'], + '3gp' => ['audio/3gpp', 'audio/3gpp-encrypted', 'audio/x-rn-3gpp-amr', 'audio/x-rn-3gpp-amr-encrypted', 'audio/x-rn-3gpp-amr-wb', 'audio/x-rn-3gpp-amr-wb-encrypted', 'video/3gp', 'video/3gpp', 'video/3gpp-encrypted'], + '3gp2' => ['audio/3gpp2', 'video/3gpp2'], + '3gpp' => ['audio/3gpp', 'audio/3gpp-encrypted', 'audio/x-rn-3gpp-amr', 'audio/x-rn-3gpp-amr-encrypted', 'audio/x-rn-3gpp-amr-wb', 'audio/x-rn-3gpp-amr-wb-encrypted', 'video/3gp', 'video/3gpp', 'video/3gpp-encrypted'], + '3gpp2' => ['audio/3gpp2', 'video/3gpp2'], + '3mf' => ['application/vnd.ms-3mfdocument', 'model/3mf'], + '602' => ['application/x-t602'], + '669' => ['audio/x-mod'], + '7z' => ['application/x-7z-compressed'], + '7z.001' => ['application/x-7z-compressed'], + 'BLEND' => ['application/x-blender'], + 'C' => ['text/x-c++src'], + 'PAR2' => ['application/x-par2'], + 'PL' => ['application/x-perl', 'text/x-perl'], + 'Z' => ['application/x-compress'], + 'a' => ['application/x-archive'], + 'a26' => ['application/x-atari-2600-rom'], + 'a78' => ['application/x-atari-7800-rom'], + 'aa' => ['audio/vnd.audible', 'audio/x-pn-audibleaudio'], + 'aab' => ['application/x-authorware-bin'], + 'aac' => ['audio/aac', 'audio/x-aac', 'audio/x-hx-aac-adts'], + 'aam' => ['application/x-authorware-map'], + 'aas' => ['application/x-authorware-seg'], + 'aax' => ['audio/vnd.audible', 'audio/vnd.audible.aax', 'audio/x-pn-audibleaudio'], + 'aaxc' => ['audio/vnd.audible.aaxc'], + 'abw' => ['application/x-abiword'], + 'abw.CRASHED' => ['application/x-abiword'], + 'abw.gz' => ['application/x-abiword'], + 'ac' => ['application/pkix-attr-cert', 'application/vnd.nokia.n-gage.ac+xml'], + 'ac3' => ['audio/ac3'], + 'acc' => ['application/vnd.americandynamics.acc'], + 'ace' => ['application/x-ace', 'application/x-ace-compressed'], + 'acu' => ['application/vnd.acucobol'], + 'acutc' => ['application/vnd.acucorp'], + 'adb' => ['text/x-adasrc'], + 'adf' => ['application/x-amiga-disk-format'], + 'adp' => ['audio/adpcm'], + 'ads' => ['text/x-adasrc'], + 'adts' => ['audio/aac', 'audio/x-aac', 'audio/x-hx-aac-adts'], + 'aep' => ['application/vnd.audiograph'], + 'afm' => ['application/x-font-afm', 'application/x-font-type1'], + 'afp' => ['application/vnd.ibm.modcap'], + 'ag' => ['image/x-applix-graphics'], + 'agb' => ['application/x-gba-rom'], + 'age' => ['application/vnd.age'], + 'ahead' => ['application/vnd.ahead.space'], + 'ai' => ['application/illustrator', 'application/postscript', 'application/vnd.adobe.illustrator'], + 'aif' => ['audio/x-aiff'], + 'aifc' => ['audio/x-aifc', 'audio/x-aiff', 'audio/x-aiffc'], + 'aiff' => ['audio/x-aiff'], + 'aiffc' => ['audio/x-aifc', 'audio/x-aiffc'], + 'air' => ['application/vnd.adobe.air-application-installer-package+zip'], + 'ait' => ['application/vnd.dvb.ait'], + 'al' => ['application/x-perl', 'text/x-perl'], + 'alz' => ['application/x-alz'], + 'ami' => ['application/vnd.amiga.ami'], + 'amr' => ['audio/amr', 'audio/amr-encrypted'], + 'amz' => ['audio/x-amzxml'], + 'ani' => ['application/x-navi-animation'], + 'anim1' => ['video/x-anim'], + 'anim2' => ['video/x-anim'], + 'anim3' => ['video/x-anim'], + 'anim4' => ['video/x-anim'], + 'anim5' => ['video/x-anim'], + 'anim6' => ['video/x-anim'], + 'anim7' => ['video/x-anim'], + 'anim8' => ['video/x-anim'], + 'anim9' => ['video/x-anim'], + 'animj' => ['video/x-anim'], + 'anx' => ['application/annodex', 'application/x-annodex'], + 'ape' => ['audio/x-ape'], + 'apk' => ['application/vnd.android.package-archive'], + 'apng' => ['image/apng', 'image/vnd.mozilla.apng'], + 'appcache' => ['text/cache-manifest'], + 'appimage' => ['application/vnd.appimage', 'application/x-iso9660-appimage'], + 'application' => ['application/x-ms-application'], + 'apr' => ['application/vnd.lotus-approach'], + 'ar' => ['application/x-archive'], + 'arc' => ['application/x-freearc'], + 'arj' => ['application/x-arj'], + 'arw' => ['image/x-sony-arw'], + 'as' => ['application/x-applix-spreadsheet'], + 'asar' => ['application/x-asar'], + 'asc' => ['application/pgp', 'application/pgp-encrypted', 'application/pgp-keys', 'application/pgp-signature', 'text/plain'], + 'asd' => ['text/x-common-lisp'], + 'asf' => ['application/vnd.ms-asf', 'video/x-ms-asf', 'video/x-ms-asf-plugin', 'video/x-ms-wm'], + 'asice' => ['application/vnd.etsi.asic-e+zip'], + 'asm' => ['text/x-asm'], + 'aso' => ['application/vnd.accpac.simply.aso'], + 'asp' => ['application/x-asp'], + 'ass' => ['audio/aac', 'audio/x-aac', 'audio/x-hx-aac-adts', 'text/x-ssa'], + 'astc' => ['image/astc'], + 'asx' => ['application/x-ms-asx', 'audio/x-ms-asx', 'video/x-ms-asf', 'video/x-ms-wax', 'video/x-ms-wmx', 'video/x-ms-wvx'], + 'atc' => ['application/vnd.acucorp'], + 'atom' => ['application/atom+xml'], + 'atomcat' => ['application/atomcat+xml'], + 'atomdeleted' => ['application/atomdeleted+xml'], + 'atomsvc' => ['application/atomsvc+xml'], + 'atx' => ['application/vnd.antix.game-component'], + 'au' => ['audio/basic'], + 'automount' => ['text/x-systemd-unit'], + 'avci' => ['image/avci'], + 'avcs' => ['image/avcs'], + 'avf' => ['video/avi', 'video/divx', 'video/msvideo', 'video/vnd.avi', 'video/vnd.divx', 'video/x-avi', 'video/x-msvideo'], + 'avi' => ['video/avi', 'video/divx', 'video/msvideo', 'video/vnd.avi', 'video/vnd.divx', 'video/x-avi', 'video/x-msvideo'], + 'avif' => ['image/avif', 'image/avif-sequence'], + 'avifs' => ['image/avif', 'image/avif-sequence'], + 'aw' => ['application/applixware', 'application/x-applix-word'], + 'awb' => ['audio/amr-wb', 'audio/amr-wb-encrypted'], + 'awk' => ['application/x-awk'], + 'axa' => ['audio/annodex', 'audio/x-annodex'], + 'axv' => ['video/annodex', 'video/x-annodex'], + 'azf' => ['application/vnd.airzip.filesecure.azf'], + 'azs' => ['application/vnd.airzip.filesecure.azs'], + 'azv' => ['image/vnd.airzip.accelerator.azv'], + 'azw' => ['application/vnd.amazon.ebook'], + 'azw3' => ['application/vnd.amazon.mobi8-ebook', 'application/x-mobi8-ebook'], + 'b16' => ['image/vnd.pco.b16'], + 'bak' => ['application/x-trash'], + 'bat' => ['application/bat', 'application/x-bat', 'application/x-msdownload'], + 'bcpio' => ['application/x-bcpio'], + 'bdf' => ['application/x-font-bdf'], + 'bdm' => ['application/vnd.syncml.dm+wbxml', 'video/mp2t'], + 'bdmv' => ['video/mp2t'], + 'bdoc' => ['application/bdoc', 'application/x-bdoc'], + 'bed' => ['application/vnd.realvnc.bed'], + 'bh2' => ['application/vnd.fujitsu.oasysprs'], + 'bib' => ['text/x-bibtex'], + 'bik' => ['video/vnd.radgamettools.bink'], + 'bin' => ['application/octet-stream'], + 'bk2' => ['video/vnd.radgamettools.bink'], + 'blb' => ['application/x-blorb'], + 'blend' => ['application/x-blender'], + 'blender' => ['application/x-blender'], + 'blorb' => ['application/x-blorb'], + 'blp' => ['text/x-blueprint'], + 'bmi' => ['application/vnd.bmi'], + 'bmml' => ['application/vnd.balsamiq.bmml+xml'], + 'bmp' => ['image/bmp', 'image/x-bmp', 'image/x-ms-bmp'], + 'book' => ['application/vnd.framemaker'], + 'box' => ['application/vnd.previewsystems.box'], + 'boz' => ['application/x-bzip2'], + 'bps' => ['application/x-bps-patch'], + 'bsdiff' => ['application/x-bsdiff'], + 'bsp' => ['model/vnd.valve.source.compiled-map'], + 'btif' => ['image/prs.btif'], + 'bz' => ['application/bzip2', 'application/x-bzip'], + 'bz2' => ['application/x-bz2', 'application/bzip2', 'application/x-bzip2'], + 'bz3' => ['application/x-bzip3'], + 'c' => ['text/x-c', 'text/x-csrc'], + 'c++' => ['text/x-c++src'], + 'c11amc' => ['application/vnd.cluetrust.cartomobile-config'], + 'c11amz' => ['application/vnd.cluetrust.cartomobile-config-pkg'], + 'c4d' => ['application/vnd.clonk.c4group'], + 'c4f' => ['application/vnd.clonk.c4group'], + 'c4g' => ['application/vnd.clonk.c4group'], + 'c4p' => ['application/vnd.clonk.c4group'], + 'c4u' => ['application/vnd.clonk.c4group'], + 'cab' => ['application/vnd.ms-cab-compressed', 'zz-application/zz-winassoc-cab'], + 'caf' => ['audio/x-caf'], + 'cap' => ['application/pcap', 'application/vnd.tcpdump.pcap', 'application/x-pcap'], + 'car' => ['application/vnd.curl.car'], + 'cat' => ['application/vnd.ms-pki.seccat'], + 'cb7' => ['application/x-cb7', 'application/x-cbr'], + 'cba' => ['application/x-cbr'], + 'cbl' => ['text/x-cobol'], + 'cbor' => ['application/cbor'], + 'cbr' => ['application/vnd.comicbook-rar', 'application/x-cbr'], + 'cbt' => ['application/x-cbr', 'application/x-cbt'], + 'cbz' => ['application/vnd.comicbook+zip', 'application/x-cbr', 'application/x-cbz'], + 'cc' => ['text/x-c', 'text/x-c++src'], + 'cci' => ['application/x-nintendo-3ds-rom'], + 'ccmx' => ['application/x-ccmx'], + 'cco' => ['application/x-cocoa'], + 'cct' => ['application/x-director'], + 'ccxml' => ['application/ccxml+xml'], + 'cdbcmsg' => ['application/vnd.contact.cmsg'], + 'cdf' => ['application/x-netcdf'], + 'cdfx' => ['application/cdfx+xml'], + 'cdi' => ['application/x-discjuggler-cd-image'], + 'cdkey' => ['application/vnd.mediastation.cdkey'], + 'cdmia' => ['application/cdmi-capability'], + 'cdmic' => ['application/cdmi-container'], + 'cdmid' => ['application/cdmi-domain'], + 'cdmio' => ['application/cdmi-object'], + 'cdmiq' => ['application/cdmi-queue'], + 'cdr' => ['application/cdr', 'application/coreldraw', 'application/vnd.corel-draw', 'application/x-cdr', 'application/x-coreldraw', 'image/cdr', 'image/x-cdr', 'zz-application/zz-winassoc-cdr'], + 'cdx' => ['chemical/x-cdx'], + 'cdxml' => ['application/vnd.chemdraw+xml'], + 'cdy' => ['application/vnd.cinderella'], + 'cer' => ['application/pkix-cert'], + 'cert' => ['application/x-x509-ca-cert'], + 'cfs' => ['application/x-cfs-compressed'], + 'cgb' => ['application/x-gameboy-color-rom'], + 'cgm' => ['image/cgm'], + 'chat' => ['application/x-chat'], + 'chd' => ['application/x-mame-chd'], + 'chm' => ['application/vnd.ms-htmlhelp', 'application/x-chm'], + 'chrt' => ['application/vnd.kde.kchart', 'application/x-kchart'], + 'cif' => ['chemical/x-cif'], + 'cii' => ['application/vnd.anser-web-certificate-issue-initiation'], + 'cil' => ['application/vnd.ms-artgalry'], + 'cjs' => ['application/node'], + 'cl' => ['text/x-opencl-src'], + 'cla' => ['application/vnd.claymore'], + 'class' => ['application/java', 'application/java-byte-code', 'application/java-vm', 'application/x-java', 'application/x-java-class', 'application/x-java-vm'], + 'clkk' => ['application/vnd.crick.clicker.keyboard'], + 'clkp' => ['application/vnd.crick.clicker.palette'], + 'clkt' => ['application/vnd.crick.clicker.template'], + 'clkw' => ['application/vnd.crick.clicker.wordbank'], + 'clkx' => ['application/vnd.crick.clicker'], + 'clp' => ['application/x-msclip'], + 'clpi' => ['video/mp2t'], + 'cls' => ['application/x-tex', 'text/x-tex'], + 'cmake' => ['text/x-cmake'], + 'cmc' => ['application/vnd.cosmocaller'], + 'cmdf' => ['chemical/x-cmdf'], + 'cml' => ['chemical/x-cml'], + 'cmp' => ['application/vnd.yellowriver-custom-menu'], + 'cmx' => ['image/x-cmx'], + 'cob' => ['text/x-cobol'], + 'cod' => ['application/vnd.rim.cod'], + 'coffee' => ['application/vnd.coffeescript', 'text/coffeescript'], + 'com' => ['application/x-msdownload'], + 'conf' => ['text/plain'], + 'cpi' => ['video/mp2t'], + 'cpio' => ['application/x-cpio'], + 'cpio.gz' => ['application/x-cpio-compressed'], + 'cpl' => ['application/cpl+xml'], + 'cpp' => ['text/x-c', 'text/x-c++src'], + 'cpt' => ['application/mac-compactpro'], + 'cr' => ['text/crystal', 'text/x-crystal'], + 'cr2' => ['image/x-canon-cr2'], + 'cr3' => ['image/x-canon-cr3'], + 'crd' => ['application/x-mscardfile'], + 'crdownload' => ['application/x-partial-download'], + 'crl' => ['application/pkix-crl'], + 'crt' => ['application/x-x509-ca-cert'], + 'crw' => ['image/x-canon-crw'], + 'crx' => ['application/x-chrome-extension'], + 'cryptonote' => ['application/vnd.rig.cryptonote'], + 'cs' => ['text/x-csharp'], + 'csh' => ['application/x-csh'], + 'csl' => ['application/vnd.citationstyles.style+xml'], + 'csml' => ['chemical/x-csml'], + 'cso' => ['application/x-compressed-iso'], + 'csp' => ['application/vnd.commonspace'], + 'css' => ['text/css'], + 'cst' => ['application/x-director'], + 'csv' => ['text/csv', 'application/csv', 'text/x-comma-separated-values', 'text/x-csv'], + 'csvs' => ['text/csv-schema'], + 'cu' => ['application/cu-seeme'], + 'cue' => ['application/x-cue'], + 'cur' => ['image/x-win-bitmap'], + 'curl' => ['text/vnd.curl'], + 'cwk' => ['application/x-appleworks-document'], + 'cww' => ['application/prs.cww'], + 'cxt' => ['application/x-director'], + 'cxx' => ['text/x-c', 'text/x-c++src'], + 'd' => ['text/x-dsrc'], + 'dae' => ['model/vnd.collada+xml'], + 'daf' => ['application/vnd.mobius.daf'], + 'dar' => ['application/x-dar'], + 'dart' => ['application/vnd.dart', 'text/x-dart'], + 'dataless' => ['application/vnd.fdsn.seed'], + 'davmount' => ['application/davmount+xml'], + 'dbf' => ['application/dbase', 'application/dbf', 'application/vnd.dbf', 'application/x-dbase', 'application/x-dbf'], + 'dbk' => ['application/docbook+xml', 'application/vnd.oasis.docbook+xml', 'application/x-docbook+xml'], + 'dc' => ['application/x-dc-rom'], + 'dcl' => ['text/x-dcl'], + 'dcm' => ['application/dicom'], + 'dcr' => ['application/x-director', 'image/x-kodak-dcr'], + 'dcurl' => ['text/vnd.curl.dcurl'], + 'dd2' => ['application/vnd.oma.dd2+xml'], + 'ddd' => ['application/vnd.fujixerox.ddd'], + 'ddf' => ['application/vnd.syncml.dmddf+xml'], + 'dds' => ['image/vnd.ms-dds', 'image/x-dds'], + 'deb' => ['application/vnd.debian.binary-package', 'application/x-deb', 'application/x-debian-package'], + 'def' => ['text/plain'], + 'der' => ['application/x-x509-ca-cert'], + 'desktop' => ['application/x-desktop', 'application/x-gnome-app-info'], + 'device' => ['text/x-systemd-unit'], + 'dfac' => ['application/vnd.dreamfactory'], + 'dff' => ['audio/dff', 'audio/x-dff'], + 'dgc' => ['application/x-dgc-compressed'], + 'di' => ['text/x-dsrc'], + 'dia' => ['application/x-dia-diagram'], + 'dib' => ['image/bmp', 'image/x-bmp', 'image/x-ms-bmp'], + 'dic' => ['text/x-c'], + 'diff' => ['text/x-diff', 'text/x-patch'], + 'dir' => ['application/x-director'], + 'dis' => ['application/vnd.mobius.dis'], + 'disposition-notification' => ['message/disposition-notification'], + 'divx' => ['video/avi', 'video/divx', 'video/msvideo', 'video/vnd.avi', 'video/vnd.divx', 'video/x-avi', 'video/x-msvideo'], + 'djv' => ['image/vnd.djvu', 'image/vnd.djvu+multipage', 'image/x-djvu', 'image/x.djvu'], + 'djvu' => ['image/vnd.djvu', 'image/vnd.djvu+multipage', 'image/x-djvu', 'image/x.djvu'], + 'dll' => ['application/x-msdownload'], + 'dmg' => ['application/x-apple-diskimage'], + 'dmp' => ['application/pcap', 'application/vnd.tcpdump.pcap', 'application/x-pcap'], + 'dna' => ['application/vnd.dna'], + 'dng' => ['image/x-adobe-dng'], + 'doc' => ['application/msword', 'application/vnd.ms-word', 'application/x-msword', 'zz-application/zz-winassoc-doc'], + 'docbook' => ['application/docbook+xml', 'application/vnd.oasis.docbook+xml', 'application/x-docbook+xml'], + 'docm' => ['application/vnd.ms-word.document.macroenabled.12'], + 'docx' => ['application/vnd.openxmlformats-officedocument.wordprocessingml.document'], + 'dot' => ['application/msword', 'application/msword-template', 'text/vnd.graphviz'], + 'dotm' => ['application/vnd.ms-word.template.macroenabled.12'], + 'dotx' => ['application/vnd.openxmlformats-officedocument.wordprocessingml.template'], + 'dp' => ['application/vnd.osgi.dp'], + 'dpg' => ['application/vnd.dpgraph'], + 'dra' => ['audio/vnd.dra'], + 'drl' => ['application/x-excellon'], + 'drle' => ['image/dicom-rle'], + 'dsc' => ['text/prs.lines.tag'], + 'dsf' => ['audio/dsd', 'audio/dsf', 'audio/x-dsd', 'audio/x-dsf'], + 'dsl' => ['text/x-dsl'], + 'dssc' => ['application/dssc+der'], + 'dtb' => ['application/x-dtbook+xml', 'text/x-devicetree-binary'], + 'dtd' => ['application/xml-dtd', 'text/x-dtd'], + 'dts' => ['audio/vnd.dts', 'audio/x-dts', 'text/x-devicetree-source'], + 'dtshd' => ['audio/vnd.dts.hd', 'audio/x-dtshd'], + 'dtsi' => ['text/x-devicetree-source'], + 'dtx' => ['application/x-tex', 'text/x-tex'], + 'dv' => ['video/dv'], + 'dvb' => ['video/vnd.dvb.file'], + 'dvi' => ['application/x-dvi'], + 'dvi.bz2' => ['application/x-bzdvi'], + 'dvi.gz' => ['application/x-gzdvi'], + 'dwd' => ['application/atsc-dwd+xml'], + 'dwf' => ['model/vnd.dwf'], + 'dwg' => ['image/vnd.dwg'], + 'dxf' => ['image/vnd.dxf'], + 'dxp' => ['application/vnd.spotfire.dxp'], + 'dxr' => ['application/x-director'], + 'e' => ['text/x-eiffel'], + 'ear' => ['application/java-archive'], + 'ecelp4800' => ['audio/vnd.nuera.ecelp4800'], + 'ecelp7470' => ['audio/vnd.nuera.ecelp7470'], + 'ecelp9600' => ['audio/vnd.nuera.ecelp9600'], + 'ecma' => ['application/ecmascript'], + 'edm' => ['application/vnd.novadigm.edm'], + 'edx' => ['application/vnd.novadigm.edx'], + 'efif' => ['application/vnd.picsel'], + 'egon' => ['application/x-egon'], + 'ei6' => ['application/vnd.pg.osasli'], + 'eif' => ['text/x-eiffel'], + 'el' => ['text/x-emacs-lisp'], + 'emf' => ['application/emf', 'application/x-emf', 'application/x-msmetafile', 'image/emf', 'image/x-emf'], + 'eml' => ['message/rfc822'], + 'emma' => ['application/emma+xml'], + 'emotionml' => ['application/emotionml+xml'], + 'emp' => ['application/vnd.emusic-emusic_package'], + 'emz' => ['application/x-msmetafile'], + 'ent' => ['application/xml-external-parsed-entity', 'text/xml-external-parsed-entity'], + 'eol' => ['audio/vnd.digital-winds'], + 'eot' => ['application/vnd.ms-fontobject'], + 'eps' => ['application/postscript', 'image/x-eps'], + 'eps.bz2' => ['image/x-bzeps'], + 'eps.gz' => ['image/x-gzeps'], + 'epsf' => ['image/x-eps'], + 'epsf.bz2' => ['image/x-bzeps'], + 'epsf.gz' => ['image/x-gzeps'], + 'epsi' => ['image/x-eps'], + 'epsi.bz2' => ['image/x-bzeps'], + 'epsi.gz' => ['image/x-gzeps'], + 'epub' => ['application/epub+zip'], + 'eris' => ['application/x-eris-link+cbor'], + 'erl' => ['text/x-erlang'], + 'es' => ['application/ecmascript', 'text/ecmascript'], + 'es3' => ['application/vnd.eszigno3+xml'], + 'esa' => ['application/vnd.osgi.subsystem'], + 'escn' => ['application/x-godot-scene'], + 'esf' => ['application/vnd.epson.esf'], + 'et3' => ['application/vnd.eszigno3+xml'], + 'etheme' => ['application/x-e-theme'], + 'etx' => ['text/x-setext'], + 'eva' => ['application/x-eva'], + 'evy' => ['application/x-envoy'], + 'ex' => ['text/x-elixir'], + 'exe' => ['application/x-ms-dos-executable', 'application/x-msdos-program', 'application/x-msdownload'], + 'exi' => ['application/exi'], + 'exp' => ['application/express'], + 'exr' => ['image/aces', 'image/x-exr'], + 'exs' => ['text/x-elixir'], + 'ext' => ['application/vnd.novadigm.ext'], + 'ez' => ['application/andrew-inset'], + 'ez2' => ['application/vnd.ezpix-album'], + 'ez3' => ['application/vnd.ezpix-package'], + 'f' => ['text/x-fortran'], + 'f4a' => ['audio/m4a', 'audio/mp4', 'audio/x-m4a'], + 'f4b' => ['audio/x-m4b'], + 'f4v' => ['video/mp4', 'video/mp4v-es', 'video/x-f4v', 'video/x-m4v'], + 'f77' => ['text/x-fortran'], + 'f90' => ['text/x-fortran'], + 'f95' => ['text/x-fortran'], + 'fasl' => ['text/x-common-lisp'], + 'fb2' => ['application/x-fictionbook', 'application/x-fictionbook+xml'], + 'fb2.zip' => ['application/x-zip-compressed-fb2'], + 'fbs' => ['image/vnd.fastbidsheet'], + 'fcdt' => ['application/vnd.adobe.formscentral.fcdt'], + 'fcs' => ['application/vnd.isac.fcs'], + 'fd' => ['application/x-fd-file', 'application/x-raw-floppy-disk-image'], + 'fdf' => ['application/vnd.fdf'], + 'fds' => ['application/x-fds-disk'], + 'fdt' => ['application/fdt+xml'], + 'fe_launch' => ['application/vnd.denovo.fcselayout-link'], + 'feature' => ['text/x-gherkin'], + 'fg5' => ['application/vnd.fujitsu.oasysgp'], + 'fgd' => ['application/x-director'], + 'fh' => ['image/x-freehand'], + 'fh4' => ['image/x-freehand'], + 'fh5' => ['image/x-freehand'], + 'fh7' => ['image/x-freehand'], + 'fhc' => ['image/x-freehand'], + 'fig' => ['application/x-xfig', 'image/x-xfig'], + 'fish' => ['application/x-fishscript', 'text/x-fish'], + 'fit' => ['application/fits', 'image/fits', 'image/x-fits'], + 'fits' => ['application/fits', 'image/fits', 'image/x-fits'], + 'fl' => ['application/x-fluid'], + 'flac' => ['audio/flac', 'audio/x-flac'], + 'flatpak' => ['application/vnd.flatpak', 'application/vnd.xdgapp'], + 'flatpakref' => ['application/vnd.flatpak.ref'], + 'flatpakrepo' => ['application/vnd.flatpak.repo'], + 'flc' => ['video/fli', 'video/x-fli', 'video/x-flic'], + 'fli' => ['video/fli', 'video/x-fli', 'video/x-flic'], + 'flo' => ['application/vnd.micrografx.flo'], + 'flv' => ['video/x-flv', 'application/x-flash-video', 'flv-application/octet-stream', 'video/flv'], + 'flw' => ['application/vnd.kde.kivio', 'application/x-kivio'], + 'flx' => ['text/vnd.fmi.flexstor'], + 'fly' => ['text/vnd.fly'], + 'fm' => ['application/vnd.framemaker', 'application/x-frame'], + 'fnc' => ['application/vnd.frogans.fnc'], + 'fo' => ['application/vnd.software602.filler.form+xml', 'text/x-xslfo'], + 'fodg' => ['application/vnd.oasis.opendocument.graphics-flat-xml'], + 'fodp' => ['application/vnd.oasis.opendocument.presentation-flat-xml'], + 'fods' => ['application/vnd.oasis.opendocument.spreadsheet-flat-xml'], + 'fodt' => ['application/vnd.oasis.opendocument.text-flat-xml'], + 'for' => ['text/x-fortran'], + 'fpx' => ['image/vnd.fpx'], + 'frame' => ['application/vnd.framemaker'], + 'fsc' => ['application/vnd.fsc.weblaunch'], + 'fst' => ['image/vnd.fst'], + 'ftc' => ['application/vnd.fluxtime.clip'], + 'fti' => ['application/vnd.anser-web-funds-transfer-initiation'], + 'fts' => ['application/fits', 'image/fits', 'image/x-fits'], + 'fvt' => ['video/vnd.fvt'], + 'fxm' => ['video/x-javafx'], + 'fxp' => ['application/vnd.adobe.fxp'], + 'fxpl' => ['application/vnd.adobe.fxp'], + 'fzs' => ['application/vnd.fuzzysheet'], + 'g2w' => ['application/vnd.geoplan'], + 'g3' => ['image/fax-g3', 'image/g3fax'], + 'g3w' => ['application/vnd.geospace'], + 'gac' => ['application/vnd.groove-account'], + 'gam' => ['application/x-tads'], + 'gb' => ['application/x-gameboy-rom'], + 'gba' => ['application/x-gba-rom'], + 'gbc' => ['application/x-gameboy-color-rom'], + 'gbr' => ['application/rpki-ghostbusters', 'application/vnd.gerber', 'application/x-gerber', 'image/x-gimp-gbr'], + 'gbrjob' => ['application/x-gerber-job'], + 'gca' => ['application/x-gca-compressed'], + 'gcode' => ['text/x.gcode'], + 'gcrd' => ['text/directory', 'text/vcard', 'text/x-vcard'], + 'gd' => ['application/x-gdscript'], + 'gdi' => ['application/x-gd-rom-cue'], + 'gdl' => ['model/vnd.gdl'], + 'gdoc' => ['application/vnd.google-apps.document'], + 'gdshader' => ['application/x-godot-shader'], + 'ged' => ['application/x-gedcom', 'text/gedcom', 'text/vnd.familysearch.gedcom'], + 'gedcom' => ['application/x-gedcom', 'text/gedcom', 'text/vnd.familysearch.gedcom'], + 'gem' => ['application/x-gtar', 'application/x-tar'], + 'gen' => ['application/x-genesis-rom'], + 'geo' => ['application/vnd.dynageo'], + 'geo.json' => ['application/geo+json', 'application/vnd.geo+json'], + 'geojson' => ['application/geo+json', 'application/vnd.geo+json'], + 'gex' => ['application/vnd.geometry-explorer'], + 'gf' => ['application/x-tex-gf'], + 'gg' => ['application/x-gamegear-rom'], + 'ggb' => ['application/vnd.geogebra.file'], + 'ggt' => ['application/vnd.geogebra.tool'], + 'ghf' => ['application/vnd.groove-help'], + 'gif' => ['image/gif'], + 'gih' => ['image/x-gimp-gih'], + 'gim' => ['application/vnd.groove-identity-message'], + 'glade' => ['application/x-glade'], + 'glb' => ['model/gltf-binary'], + 'gltf' => ['model/gltf+json'], + 'gml' => ['application/gml+xml'], + 'gmo' => ['application/x-gettext-translation'], + 'gmx' => ['application/vnd.gmx'], + 'gnc' => ['application/x-gnucash'], + 'gnd' => ['application/gnunet-directory'], + 'gnucash' => ['application/x-gnucash'], + 'gnumeric' => ['application/x-gnumeric'], + 'gnuplot' => ['application/x-gnuplot'], + 'go' => ['text/x-go'], + 'gp' => ['application/x-gnuplot'], + 'gpg' => ['application/pgp', 'application/pgp-encrypted', 'application/pgp-keys', 'application/pgp-signature'], + 'gph' => ['application/vnd.flographit'], + 'gplt' => ['application/x-gnuplot'], + 'gpx' => ['application/gpx', 'application/gpx+xml', 'application/x-gpx', 'application/x-gpx+xml'], + 'gqf' => ['application/vnd.grafeq'], + 'gqs' => ['application/vnd.grafeq'], + 'gra' => ['application/x-graphite'], + 'gradle' => ['text/x-gradle'], + 'gram' => ['application/srgs'], + 'gramps' => ['application/x-gramps-xml'], + 'gre' => ['application/vnd.geometry-explorer'], + 'groovy' => ['text/x-groovy'], + 'grv' => ['application/vnd.groove-injector'], + 'grxml' => ['application/srgs+xml'], + 'gs' => ['text/x-genie'], + 'gsf' => ['application/x-font-ghostscript', 'application/x-font-type1'], + 'gsh' => ['text/x-groovy'], + 'gsheet' => ['application/vnd.google-apps.spreadsheet'], + 'gslides' => ['application/vnd.google-apps.presentation'], + 'gsm' => ['audio/x-gsm'], + 'gtar' => ['application/x-gtar', 'application/x-tar'], + 'gtm' => ['application/vnd.groove-tool-message'], + 'gtw' => ['model/vnd.gtw'], + 'gv' => ['text/vnd.graphviz'], + 'gvp' => ['text/google-video-pointer', 'text/x-google-video-pointer'], + 'gvy' => ['text/x-groovy'], + 'gx' => ['text/x-gcode-gx'], + 'gxf' => ['application/gxf'], + 'gxt' => ['application/vnd.geonext'], + 'gy' => ['text/x-groovy'], + 'gz' => ['application/x-gzip', 'application/gzip'], + 'h' => ['text/x-c', 'text/x-chdr'], + 'h++' => ['text/x-c++hdr'], + 'h261' => ['video/h261'], + 'h263' => ['video/h263'], + 'h264' => ['video/h264'], + 'h4' => ['application/x-hdf'], + 'h5' => ['application/x-hdf'], + 'hal' => ['application/vnd.hal+xml'], + 'hbci' => ['application/vnd.hbci'], + 'hbs' => ['text/x-handlebars-template'], + 'hdd' => ['application/x-virtualbox-hdd'], + 'hdf' => ['application/x-hdf'], + 'hdf4' => ['application/x-hdf'], + 'hdf5' => ['application/x-hdf'], + 'hdp' => ['image/jxr', 'image/vnd.ms-photo'], + 'heic' => ['image/heic', 'image/heic-sequence', 'image/heif', 'image/heif-sequence'], + 'heics' => ['image/heic-sequence'], + 'heif' => ['image/heic', 'image/heic-sequence', 'image/heif', 'image/heif-sequence'], + 'heifs' => ['image/heif-sequence'], + 'hej2' => ['image/hej2k'], + 'held' => ['application/atsc-held+xml'], + 'hfe' => ['application/x-hfe-file', 'application/x-hfe-floppy-image'], + 'hh' => ['text/x-c', 'text/x-c++hdr'], + 'hif' => ['image/heic', 'image/heic-sequence', 'image/heif', 'image/heif-sequence'], + 'hjson' => ['application/hjson'], + 'hlp' => ['application/winhlp', 'zz-application/zz-winassoc-hlp'], + 'hp' => ['text/x-c++hdr'], + 'hpgl' => ['application/vnd.hp-hpgl'], + 'hpid' => ['application/vnd.hp-hpid'], + 'hpp' => ['text/x-c++hdr'], + 'hps' => ['application/vnd.hp-hps'], + 'hqx' => ['application/stuffit', 'application/mac-binhex40'], + 'hs' => ['text/x-haskell'], + 'hsj2' => ['image/hsj2'], + 'htc' => ['text/x-component'], + 'htke' => ['application/vnd.kenameaapp'], + 'htm' => ['text/html', 'application/xhtml+xml'], + 'html' => ['text/html', 'application/xhtml+xml'], + 'hvd' => ['application/vnd.yamaha.hv-dic'], + 'hvp' => ['application/vnd.yamaha.hv-voice'], + 'hvs' => ['application/vnd.yamaha.hv-script'], + 'hwp' => ['application/vnd.haansoft-hwp', 'application/x-hwp'], + 'hwt' => ['application/vnd.haansoft-hwt', 'application/x-hwt'], + 'hxx' => ['text/x-c++hdr'], + 'i2g' => ['application/vnd.intergeo'], + 'ica' => ['application/x-ica'], + 'icb' => ['application/tga', 'application/x-targa', 'application/x-tga', 'image/targa', 'image/tga', 'image/x-icb', 'image/x-targa', 'image/x-tga'], + 'icc' => ['application/vnd.iccprofile'], + 'ice' => ['x-conference/x-cooltalk'], + 'icm' => ['application/vnd.iccprofile'], + 'icns' => ['image/x-icns'], + 'ico' => ['application/ico', 'image/ico', 'image/icon', 'image/vnd.microsoft.icon', 'image/x-ico', 'image/x-icon', 'text/ico'], + 'ics' => ['application/ics', 'text/calendar', 'text/x-vcalendar'], + 'idl' => ['text/x-idl'], + 'ief' => ['image/ief'], + 'ifb' => ['text/calendar'], + 'iff' => ['image/x-iff', 'image/x-ilbm'], + 'ifm' => ['application/vnd.shana.informed.formdata'], + 'iges' => ['model/iges'], + 'igl' => ['application/vnd.igloader'], + 'igm' => ['application/vnd.insors.igm'], + 'igs' => ['model/iges'], + 'igx' => ['application/vnd.micrografx.igx'], + 'iif' => ['application/vnd.shana.informed.interchange'], + 'ilbm' => ['image/x-iff', 'image/x-ilbm'], + 'ime' => ['audio/imelody', 'audio/x-imelody', 'text/x-imelody'], + 'img' => ['application/vnd.efi.img', 'application/x-raw-disk-image'], + 'img.xz' => ['application/x-raw-disk-image-xz-compressed'], + 'imp' => ['application/vnd.accpac.simply.imp'], + 'ims' => ['application/vnd.ms-ims'], + 'imy' => ['audio/imelody', 'audio/x-imelody', 'text/x-imelody'], + 'in' => ['text/plain'], + 'ini' => ['text/plain'], + 'ink' => ['application/inkml+xml'], + 'inkml' => ['application/inkml+xml'], + 'ins' => ['application/x-tex', 'text/x-tex'], + 'install' => ['application/x-install-instructions'], + 'iota' => ['application/vnd.astraea-software.iota'], + 'ipfix' => ['application/ipfix'], + 'ipk' => ['application/vnd.shana.informed.package'], + 'ips' => ['application/x-ips-patch'], + 'iptables' => ['text/x-iptables'], + 'ipynb' => ['application/x-ipynb+json'], + 'irm' => ['application/vnd.ibm.rights-management'], + 'irp' => ['application/vnd.irepository.package+xml'], + 'iso' => ['application/vnd.efi.iso', 'application/x-cd-image', 'application/x-dreamcast-rom', 'application/x-gamecube-iso-image', 'application/x-gamecube-rom', 'application/x-iso9660-image', 'application/x-saturn-rom', 'application/x-sega-cd-rom', 'application/x-sega-pico-rom', 'application/x-wbfs', 'application/x-wia', 'application/x-wii-iso-image', 'application/x-wii-rom'], + 'iso9660' => ['application/vnd.efi.iso', 'application/x-cd-image', 'application/x-iso9660-image'], + 'it' => ['audio/x-it'], + 'it87' => ['application/x-it87'], + 'itp' => ['application/vnd.shana.informed.formtemplate'], + 'its' => ['application/its+xml'], + 'ivp' => ['application/vnd.immervision-ivp'], + 'ivu' => ['application/vnd.immervision-ivu'], + 'j2c' => ['image/x-jp2-codestream'], + 'j2k' => ['image/x-jp2-codestream'], + 'jad' => ['text/vnd.sun.j2me.app-descriptor'], + 'jade' => ['text/jade'], + 'jam' => ['application/vnd.jam'], + 'jar' => ['application/x-java-archive', 'application/java-archive', 'application/x-jar'], + 'jardiff' => ['application/x-java-archive-diff'], + 'java' => ['text/x-java', 'text/x-java-source'], + 'jceks' => ['application/x-java-jce-keystore'], + 'jhc' => ['image/jphc'], + 'jisp' => ['application/vnd.jisp'], + 'jks' => ['application/x-java-keystore'], + 'jl' => ['text/julia'], + 'jls' => ['image/jls'], + 'jlt' => ['application/vnd.hp-jlyt'], + 'jng' => ['image/x-jng'], + 'jnlp' => ['application/x-java-jnlp-file'], + 'joda' => ['application/vnd.joost.joda-archive'], + 'jp2' => ['image/jp2', 'image/jpeg2000', 'image/jpeg2000-image', 'image/x-jpeg2000-image'], + 'jpc' => ['image/x-jp2-codestream'], + 'jpe' => ['image/jpeg', 'image/pjpeg'], + 'jpeg' => ['image/jpeg', 'image/pjpeg'], + 'jpf' => ['image/jpx'], + 'jpg' => ['image/jpeg', 'image/pjpeg'], + 'jpg2' => ['image/jp2', 'image/jpeg2000', 'image/jpeg2000-image', 'image/x-jpeg2000-image'], + 'jpgm' => ['image/jpm', 'video/jpm'], + 'jpgv' => ['video/jpeg'], + 'jph' => ['image/jph'], + 'jpm' => ['image/jpm', 'video/jpm'], + 'jpr' => ['application/x-jbuilder-project'], + 'jpx' => ['application/x-jbuilder-project', 'image/jpx'], + 'jrd' => ['application/jrd+json'], + 'js' => ['text/javascript', 'application/javascript', 'application/x-javascript'], + 'jsm' => ['application/javascript', 'application/x-javascript', 'text/javascript'], + 'json' => ['application/json', 'application/schema+json'], + 'json-patch' => ['application/json-patch+json'], + 'json5' => ['application/json5'], + 'jsonld' => ['application/ld+json'], + 'jsonml' => ['application/jsonml+json'], + 'jsx' => ['text/jsx'], + 'jxl' => ['image/jxl'], + 'jxr' => ['image/jxr', 'image/vnd.ms-photo'], + 'jxra' => ['image/jxra'], + 'jxrs' => ['image/jxrs'], + 'jxs' => ['image/jxs'], + 'jxsc' => ['image/jxsc'], + 'jxsi' => ['image/jxsi'], + 'jxss' => ['image/jxss'], + 'k25' => ['image/x-kodak-k25'], + 'k7' => ['application/x-thomson-cassette'], + 'kar' => ['audio/midi', 'audio/x-midi'], + 'karbon' => ['application/vnd.kde.karbon', 'application/x-karbon'], + 'kdbx' => ['application/x-keepass2'], + 'kdc' => ['image/x-kodak-kdc'], + 'kdelnk' => ['application/x-desktop', 'application/x-gnome-app-info'], + 'kexi' => ['application/x-kexiproject-sqlite', 'application/x-kexiproject-sqlite2', 'application/x-kexiproject-sqlite3', 'application/x-vnd.kde.kexi'], + 'kexic' => ['application/x-kexi-connectiondata'], + 'kexis' => ['application/x-kexiproject-shortcut'], + 'key' => ['application/vnd.apple.keynote', 'application/pgp-keys', 'application/x-iwork-keynote-sffkey'], + 'keynote' => ['application/vnd.apple.keynote'], + 'kfo' => ['application/vnd.kde.kformula', 'application/x-kformula'], + 'kfx' => ['application/vnd.amazon.mobi8-ebook', 'application/x-mobi8-ebook'], + 'kia' => ['application/vnd.kidspiration'], + 'kil' => ['application/x-killustrator'], + 'kino' => ['application/smil', 'application/smil+xml'], + 'kml' => ['application/vnd.google-earth.kml+xml'], + 'kmz' => ['application/vnd.google-earth.kmz'], + 'kne' => ['application/vnd.kinar'], + 'knp' => ['application/vnd.kinar'], + 'kon' => ['application/vnd.kde.kontour', 'application/x-kontour'], + 'kpm' => ['application/x-kpovmodeler'], + 'kpr' => ['application/vnd.kde.kpresenter', 'application/x-kpresenter'], + 'kpt' => ['application/vnd.kde.kpresenter', 'application/x-kpresenter'], + 'kpxx' => ['application/vnd.ds-keypoint'], + 'kra' => ['application/x-krita'], + 'krz' => ['application/x-krita'], + 'ks' => ['application/x-java-keystore'], + 'ksp' => ['application/vnd.kde.kspread', 'application/x-kspread'], + 'ksy' => ['text/x-kaitai-struct'], + 'kt' => ['text/x-kotlin'], + 'ktr' => ['application/vnd.kahootz'], + 'ktx' => ['image/ktx'], + 'ktx2' => ['image/ktx2'], + 'ktz' => ['application/vnd.kahootz'], + 'kud' => ['application/x-kugar'], + 'kwd' => ['application/vnd.kde.kword', 'application/x-kword'], + 'kwt' => ['application/vnd.kde.kword', 'application/x-kword'], + 'la' => ['application/x-shared-library-la'], + 'lasxml' => ['application/vnd.las.las+xml'], + 'latex' => ['application/x-latex', 'application/x-tex', 'text/x-tex'], + 'lbd' => ['application/vnd.llamagraphics.life-balance.desktop'], + 'lbe' => ['application/vnd.llamagraphics.life-balance.exchange+xml'], + 'lbm' => ['image/x-iff', 'image/x-ilbm'], + 'ldif' => ['text/x-ldif'], + 'les' => ['application/vnd.hhe.lesson-player'], + 'less' => ['text/less'], + 'lgr' => ['application/lgr+xml'], + 'lha' => ['application/x-lha', 'application/x-lzh-compressed'], + 'lhs' => ['text/x-literate-haskell'], + 'lhz' => ['application/x-lhz'], + 'link66' => ['application/vnd.route66.link66+xml'], + 'lisp' => ['text/x-common-lisp'], + 'list' => ['text/plain'], + 'list3820' => ['application/vnd.ibm.modcap'], + 'listafp' => ['application/vnd.ibm.modcap'], + 'litcoffee' => ['text/coffeescript'], + 'lmdb' => ['application/x-lmdb'], + 'lnk' => ['application/x-ms-shortcut'], + 'lnx' => ['application/x-atari-lynx-rom'], + 'loas' => ['audio/usac'], + 'log' => ['text/plain', 'text/x-log'], + 'lostxml' => ['application/lost+xml'], + 'lrm' => ['application/vnd.ms-lrm'], + 'lrv' => ['video/mp4', 'video/mp4v-es', 'video/x-m4v'], + 'lrz' => ['application/x-lrzip'], + 'ltf' => ['application/vnd.frogans.ltf'], + 'ltx' => ['application/x-tex', 'text/x-tex'], + 'lua' => ['text/x-lua'], + 'luac' => ['application/x-lua-bytecode'], + 'lvp' => ['audio/vnd.lucent.voice'], + 'lwo' => ['image/x-lwo'], + 'lwob' => ['image/x-lwo'], + 'lwp' => ['application/vnd.lotus-wordpro'], + 'lws' => ['image/x-lws'], + 'ly' => ['text/x-lilypond'], + 'lyx' => ['application/x-lyx', 'text/x-lyx'], + 'lz' => ['application/x-lzip'], + 'lz4' => ['application/x-lz4'], + 'lzh' => ['application/x-lha', 'application/x-lzh-compressed'], + 'lzma' => ['application/x-lzma'], + 'lzo' => ['application/x-lzop'], + 'm' => ['text/x-matlab', 'text/x-objcsrc', 'text/x-octave'], + 'm13' => ['application/x-msmediaview'], + 'm14' => ['application/x-msmediaview'], + 'm15' => ['audio/x-mod'], + 'm1u' => ['video/vnd.mpegurl', 'video/x-mpegurl'], + 'm1v' => ['video/mpeg'], + 'm21' => ['application/mp21'], + 'm2a' => ['audio/mpeg'], + 'm2t' => ['video/mp2t'], + 'm2ts' => ['video/mp2t'], + 'm2v' => ['video/mpeg'], + 'm3a' => ['audio/mpeg'], + 'm3u' => ['audio/x-mpegurl', 'application/m3u', 'application/vnd.apple.mpegurl', 'audio/m3u', 'audio/mpegurl', 'audio/x-m3u', 'audio/x-mp3-playlist'], + 'm3u8' => ['application/m3u', 'application/vnd.apple.mpegurl', 'audio/m3u', 'audio/mpegurl', 'audio/x-m3u', 'audio/x-mp3-playlist', 'audio/x-mpegurl'], + 'm4' => ['application/x-m4'], + 'm4a' => ['audio/mp4', 'audio/m4a', 'audio/x-m4a'], + 'm4b' => ['audio/x-m4b'], + 'm4p' => ['application/mp4'], + 'm4r' => ['audio/x-m4r'], + 'm4s' => ['video/iso.segment'], + 'm4u' => ['video/vnd.mpegurl', 'video/x-mpegurl'], + 'm4v' => ['video/mp4', 'video/mp4v-es', 'video/x-m4v'], + 'm7' => ['application/x-thomson-cartridge-memo7'], + 'ma' => ['application/mathematica'], + 'mab' => ['application/x-markaby'], + 'mads' => ['application/mads+xml'], + 'maei' => ['application/mmt-aei+xml'], + 'mag' => ['application/vnd.ecowin.chart'], + 'mak' => ['text/x-makefile'], + 'maker' => ['application/vnd.framemaker'], + 'man' => ['application/x-troff-man', 'text/troff'], + 'manifest' => ['text/cache-manifest'], + 'map' => ['application/json'], + 'markdown' => ['text/markdown', 'text/x-markdown'], + 'mathml' => ['application/mathml+xml'], + 'mb' => ['application/mathematica'], + 'mbk' => ['application/vnd.mobius.mbk'], + 'mbox' => ['application/mbox'], + 'mc1' => ['application/vnd.medcalcdata'], + 'mc2' => ['text/vnd.senx.warpscript'], + 'mcd' => ['application/vnd.mcd'], + 'mcurl' => ['text/vnd.curl.mcurl'], + 'md' => ['text/markdown', 'text/x-markdown'], + 'mdb' => ['application/x-msaccess', 'application/mdb', 'application/msaccess', 'application/vnd.ms-access', 'application/vnd.msaccess', 'application/x-lmdb', 'application/x-mdb', 'zz-application/zz-winassoc-mdb'], + 'mdi' => ['image/vnd.ms-modi'], + 'mdx' => ['application/x-genesis-32x-rom', 'text/mdx'], + 'me' => ['text/troff', 'text/x-troff-me'], + 'med' => ['audio/x-mod'], + 'mesh' => ['model/mesh'], + 'meta4' => ['application/metalink4+xml'], + 'metalink' => ['application/metalink+xml'], + 'mets' => ['application/mets+xml'], + 'mfm' => ['application/vnd.mfmp'], + 'mft' => ['application/rpki-manifest'], + 'mgp' => ['application/vnd.osgeo.mapguide.package', 'application/x-magicpoint'], + 'mgz' => ['application/vnd.proteus.magazine'], + 'mht' => ['application/x-mimearchive'], + 'mhtml' => ['application/x-mimearchive'], + 'mid' => ['audio/midi', 'audio/x-midi'], + 'midi' => ['audio/midi', 'audio/x-midi'], + 'mie' => ['application/x-mie'], + 'mif' => ['application/vnd.mif', 'application/x-mif'], + 'mime' => ['message/rfc822'], + 'minipsf' => ['audio/x-minipsf'], + 'mj2' => ['video/mj2'], + 'mjp2' => ['video/mj2'], + 'mjpeg' => ['video/x-mjpeg'], + 'mjpg' => ['video/x-mjpeg'], + 'mjs' => ['application/javascript', 'application/x-javascript', 'text/javascript'], + 'mk' => ['text/x-makefile'], + 'mk3d' => ['video/x-matroska', 'video/x-matroska-3d'], + 'mka' => ['audio/x-matroska'], + 'mkd' => ['text/markdown', 'text/x-markdown'], + 'mks' => ['video/x-matroska'], + 'mkv' => ['video/x-matroska'], + 'ml' => ['text/x-ocaml'], + 'mli' => ['text/x-ocaml'], + 'mlp' => ['application/vnd.dolby.mlp'], + 'mm' => ['text/x-objc++src', 'text/x-troff-mm'], + 'mmd' => ['application/vnd.chipnuts.karaoke-mmd'], + 'mmf' => ['application/vnd.smaf', 'application/x-smaf'], + 'mml' => ['application/mathml+xml', 'text/mathml'], + 'mmr' => ['image/vnd.fujixerox.edmics-mmr'], + 'mng' => ['video/x-mng'], + 'mny' => ['application/x-msmoney'], + 'mo' => ['application/x-gettext-translation', 'text/x-modelica'], + 'mo3' => ['audio/x-mo3'], + 'mobi' => ['application/x-mobipocket-ebook'], + 'moc' => ['text/x-moc'], + 'mod' => ['application/x-object', 'audio/x-mod'], + 'mods' => ['application/mods+xml'], + 'mof' => ['text/x-mof'], + 'moov' => ['video/quicktime'], + 'mount' => ['text/x-systemd-unit'], + 'mov' => ['video/quicktime'], + 'movie' => ['video/x-sgi-movie'], + 'mp+' => ['audio/x-musepack'], + 'mp2' => ['audio/mp2', 'audio/mpeg', 'audio/x-mp2', 'video/mpeg', 'video/mpeg-system', 'video/x-mpeg', 'video/x-mpeg-system', 'video/x-mpeg2'], + 'mp21' => ['application/mp21'], + 'mp2a' => ['audio/mpeg'], + 'mp3' => ['audio/mpeg', 'audio/mp3', 'audio/x-mp3', 'audio/x-mpeg', 'audio/x-mpg'], + 'mp4' => ['video/mp4', 'video/mp4v-es', 'video/x-m4v'], + 'mp4a' => ['audio/mp4'], + 'mp4s' => ['application/mp4'], + 'mp4v' => ['video/mp4'], + 'mpc' => ['application/vnd.mophun.certificate', 'audio/x-musepack'], + 'mpd' => ['application/dash+xml'], + 'mpe' => ['video/mpeg', 'video/mpeg-system', 'video/x-mpeg', 'video/x-mpeg-system', 'video/x-mpeg2'], + 'mpeg' => ['video/mpeg', 'video/mpeg-system', 'video/x-mpeg', 'video/x-mpeg-system', 'video/x-mpeg2'], + 'mpf' => ['application/media-policy-dataset+xml'], + 'mpg' => ['video/mpeg', 'video/mpeg-system', 'video/x-mpeg', 'video/x-mpeg-system', 'video/x-mpeg2'], + 'mpg4' => ['video/mp4'], + 'mpga' => ['audio/mp3', 'audio/mpeg', 'audio/x-mp3', 'audio/x-mpeg', 'audio/x-mpg'], + 'mpkg' => ['application/vnd.apple.installer+xml'], + 'mpl' => ['text/x-mpl2', 'video/mp2t'], + 'mpls' => ['video/mp2t'], + 'mpm' => ['application/vnd.blueice.multipass'], + 'mpn' => ['application/vnd.mophun.application'], + 'mpp' => ['application/dash-patch+xml', 'application/vnd.ms-project', 'audio/x-musepack'], + 'mpt' => ['application/vnd.ms-project'], + 'mpy' => ['application/vnd.ibm.minipay'], + 'mqy' => ['application/vnd.mobius.mqy'], + 'mrc' => ['application/marc'], + 'mrcx' => ['application/marcxml+xml'], + 'mrl' => ['text/x-mrml'], + 'mrml' => ['text/x-mrml'], + 'mrpack' => ['application/x-modrinth-modpack+zip'], + 'mrw' => ['image/x-minolta-mrw'], + 'ms' => ['text/troff', 'text/x-troff-ms'], + 'mscml' => ['application/mediaservercontrol+xml'], + 'mseed' => ['application/vnd.fdsn.mseed'], + 'mseq' => ['application/vnd.mseq'], + 'msf' => ['application/vnd.epson.msf'], + 'msg' => ['application/vnd.ms-outlook'], + 'msh' => ['model/mesh'], + 'msi' => ['application/x-msdownload', 'application/x-msi'], + 'msl' => ['application/vnd.mobius.msl'], + 'msod' => ['image/x-msod'], + 'msty' => ['application/vnd.muvee.style'], + 'msx' => ['application/x-msx-rom'], + 'mtl' => ['model/mtl'], + 'mtm' => ['audio/x-mod'], + 'mts' => ['model/vnd.mts', 'video/mp2t'], + 'mup' => ['text/x-mup'], + 'mus' => ['application/vnd.musician'], + 'musd' => ['application/mmt-usd+xml'], + 'musicxml' => ['application/vnd.recordare.musicxml+xml'], + 'mvb' => ['application/x-msmediaview'], + 'mvt' => ['application/vnd.mapbox-vector-tile'], + 'mwf' => ['application/vnd.mfer'], + 'mxf' => ['application/mxf'], + 'mxl' => ['application/vnd.recordare.musicxml'], + 'mxmf' => ['audio/mobile-xmf', 'audio/vnd.nokia.mobile-xmf'], + 'mxml' => ['application/xv+xml'], + 'mxs' => ['application/vnd.triscape.mxs'], + 'mxu' => ['video/vnd.mpegurl', 'video/x-mpegurl'], + 'n-gage' => ['application/vnd.nokia.n-gage.symbian.install'], + 'n3' => ['text/n3'], + 'n64' => ['application/x-n64-rom'], + 'nb' => ['application/mathematica', 'application/x-mathematica'], + 'nbp' => ['application/vnd.wolfram.player'], + 'nc' => ['application/x-netcdf'], + 'ncx' => ['application/x-dtbncx+xml'], + 'nds' => ['application/x-nintendo-ds-rom'], + 'nef' => ['image/x-nikon-nef'], + 'nes' => ['application/x-nes-rom'], + 'nez' => ['application/x-nes-rom'], + 'nfo' => ['text/x-nfo'], + 'ngc' => ['application/x-neo-geo-pocket-color-rom'], + 'ngdat' => ['application/vnd.nokia.n-gage.data'], + 'ngp' => ['application/x-neo-geo-pocket-rom'], + 'nim' => ['text/x-nim'], + 'nimble' => ['text/x-nimscript'], + 'nims' => ['text/x-nimscript'], + 'nitf' => ['application/vnd.nitf'], + 'nlu' => ['application/vnd.neurolanguage.nlu'], + 'nml' => ['application/vnd.enliven'], + 'nnd' => ['application/vnd.noblenet-directory'], + 'nns' => ['application/vnd.noblenet-sealer'], + 'nnw' => ['application/vnd.noblenet-web'], + 'not' => ['text/x-mup'], + 'npx' => ['image/vnd.net-fpx'], + 'nq' => ['application/n-quads'], + 'nrw' => ['image/x-nikon-nrw'], + 'nsc' => ['application/x-conference', 'application/x-netshow-channel'], + 'nsf' => ['application/vnd.lotus-notes'], + 'nsv' => ['video/x-nsv'], + 'nt' => ['application/n-triples'], + 'ntf' => ['application/vnd.nitf'], + 'nu' => ['application/x-nuscript', 'text/x-nu'], + 'numbers' => ['application/vnd.apple.numbers', 'application/x-iwork-numbers-sffnumbers'], + 'nzb' => ['application/x-nzb'], + 'o' => ['application/x-object'], + 'oa2' => ['application/vnd.fujitsu.oasys2'], + 'oa3' => ['application/vnd.fujitsu.oasys3'], + 'oas' => ['application/vnd.fujitsu.oasys'], + 'obd' => ['application/x-msbinder'], + 'obgx' => ['application/vnd.openblox.game+xml'], + 'obj' => ['application/x-tgif', 'model/obj'], + 'ocl' => ['text/x-ocl'], + 'oda' => ['application/oda'], + 'odb' => ['application/vnd.oasis.opendocument.database', 'application/vnd.sun.xml.base'], + 'odc' => ['application/vnd.oasis.opendocument.chart'], + 'odf' => ['application/vnd.oasis.opendocument.formula'], + 'odft' => ['application/vnd.oasis.opendocument.formula-template'], + 'odg' => ['application/vnd.oasis.opendocument.graphics'], + 'odi' => ['application/vnd.oasis.opendocument.image'], + 'odm' => ['application/vnd.oasis.opendocument.text-master'], + 'odp' => ['application/vnd.oasis.opendocument.presentation'], + 'ods' => ['application/vnd.oasis.opendocument.spreadsheet'], + 'odt' => ['application/vnd.oasis.opendocument.text'], + 'oga' => ['audio/ogg', 'audio/vorbis', 'audio/x-flac+ogg', 'audio/x-ogg', 'audio/x-oggflac', 'audio/x-speex+ogg', 'audio/x-vorbis', 'audio/x-vorbis+ogg'], + 'ogex' => ['model/vnd.opengex'], + 'ogg' => ['audio/ogg', 'audio/vorbis', 'audio/x-flac+ogg', 'audio/x-ogg', 'audio/x-oggflac', 'audio/x-speex+ogg', 'audio/x-vorbis', 'audio/x-vorbis+ogg', 'video/ogg', 'video/x-ogg', 'video/x-theora', 'video/x-theora+ogg'], + 'ogm' => ['video/x-ogm', 'video/x-ogm+ogg'], + 'ogv' => ['video/ogg', 'video/x-ogg'], + 'ogx' => ['application/ogg', 'application/x-ogg'], + 'old' => ['application/x-trash'], + 'oleo' => ['application/x-oleo'], + 'omdoc' => ['application/omdoc+xml'], + 'onepkg' => ['application/onenote'], + 'onetmp' => ['application/onenote'], + 'onetoc' => ['application/onenote'], + 'onetoc2' => ['application/onenote'], + 'ooc' => ['text/x-ooc'], + 'openvpn' => ['application/x-openvpn-profile'], + 'opf' => ['application/oebps-package+xml'], + 'opml' => ['text/x-opml', 'text/x-opml+xml'], + 'oprc' => ['application/vnd.palm', 'application/x-palm-database'], + 'opus' => ['audio/ogg', 'audio/x-ogg', 'audio/x-opus+ogg'], + 'ora' => ['image/openraster'], + 'orf' => ['image/x-olympus-orf'], + 'org' => ['application/vnd.lotus-organizer', 'text/org', 'text/x-org'], + 'osf' => ['application/vnd.yamaha.openscoreformat'], + 'osfpvg' => ['application/vnd.yamaha.openscoreformat.osfpvg+xml'], + 'osm' => ['application/vnd.openstreetmap.data+xml'], + 'otc' => ['application/vnd.oasis.opendocument.chart-template'], + 'otf' => ['application/vnd.oasis.opendocument.formula-template', 'application/x-font-otf', 'font/otf'], + 'otg' => ['application/vnd.oasis.opendocument.graphics-template'], + 'oth' => ['application/vnd.oasis.opendocument.text-web'], + 'oti' => ['application/vnd.oasis.opendocument.image-template'], + 'otp' => ['application/vnd.oasis.opendocument.presentation-template'], + 'ots' => ['application/vnd.oasis.opendocument.spreadsheet-template'], + 'ott' => ['application/vnd.oasis.opendocument.text-template'], + 'ova' => ['application/ovf', 'application/x-virtualbox-ova'], + 'ovf' => ['application/x-virtualbox-ovf'], + 'ovpn' => ['application/x-openvpn-profile'], + 'owl' => ['application/rdf+xml', 'text/rdf'], + 'owx' => ['application/owl+xml'], + 'oxps' => ['application/oxps'], + 'oxt' => ['application/vnd.openofficeorg.extension'], + 'p' => ['text/x-pascal'], + 'p10' => ['application/pkcs10'], + 'p12' => ['application/pkcs12', 'application/x-pkcs12'], + 'p65' => ['application/x-pagemaker'], + 'p7b' => ['application/x-pkcs7-certificates'], + 'p7c' => ['application/pkcs7-mime'], + 'p7m' => ['application/pkcs7-mime'], + 'p7r' => ['application/x-pkcs7-certreqresp'], + 'p7s' => ['application/pkcs7-signature'], + 'p8' => ['application/pkcs8'], + 'p8e' => ['application/pkcs8-encrypted'], + 'pac' => ['application/x-ns-proxy-autoconfig'], + 'pack' => ['application/x-java-pack200'], + 'pages' => ['application/vnd.apple.pages', 'application/x-iwork-pages-sffpages'], + 'pak' => ['application/x-pak'], + 'par2' => ['application/x-par2'], + 'part' => ['application/x-partial-download'], + 'pas' => ['text/x-pascal'], + 'pat' => ['image/x-gimp-pat'], + 'patch' => ['text/x-diff', 'text/x-patch'], + 'path' => ['text/x-systemd-unit'], + 'paw' => ['application/vnd.pawaafile'], + 'pbd' => ['application/vnd.powerbuilder6'], + 'pbm' => ['image/x-portable-bitmap'], + 'pcap' => ['application/pcap', 'application/vnd.tcpdump.pcap', 'application/x-pcap'], + 'pcd' => ['image/x-photo-cd'], + 'pce' => ['application/x-pc-engine-rom'], + 'pcf' => ['application/x-cisco-vpn-settings', 'application/x-font-pcf'], + 'pcf.Z' => ['application/x-font-pcf'], + 'pcf.gz' => ['application/x-font-pcf'], + 'pcl' => ['application/vnd.hp-pcl'], + 'pclxl' => ['application/vnd.hp-pclxl'], + 'pct' => ['image/x-pict'], + 'pcurl' => ['application/vnd.curl.pcurl'], + 'pcx' => ['image/vnd.zbrush.pcx', 'image/x-pcx'], + 'pdb' => ['application/vnd.palm', 'application/x-aportisdoc', 'application/x-ms-pdb', 'application/x-palm-database', 'application/x-pilot'], + 'pdc' => ['application/x-aportisdoc'], + 'pde' => ['text/x-processing'], + 'pdf' => ['application/pdf', 'application/acrobat', 'application/nappdf', 'application/x-pdf', 'image/pdf'], + 'pdf.bz2' => ['application/x-bzpdf'], + 'pdf.gz' => ['application/x-gzpdf'], + 'pdf.lz' => ['application/x-lzpdf'], + 'pdf.xz' => ['application/x-xzpdf'], + 'pef' => ['image/x-pentax-pef'], + 'pem' => ['application/x-x509-ca-cert'], + 'perl' => ['application/x-perl', 'text/x-perl'], + 'pfa' => ['application/x-font-type1'], + 'pfb' => ['application/x-font-type1'], + 'pfm' => ['application/x-font-type1'], + 'pfr' => ['application/font-tdpfr', 'application/vnd.truedoc'], + 'pfx' => ['application/pkcs12', 'application/x-pkcs12'], + 'pgm' => ['image/x-portable-graymap'], + 'pgn' => ['application/vnd.chess-pgn', 'application/x-chess-pgn'], + 'pgp' => ['application/pgp', 'application/pgp-encrypted', 'application/pgp-keys', 'application/pgp-signature'], + 'php' => ['application/x-php', 'application/x-httpd-php'], + 'php3' => ['application/x-php'], + 'php4' => ['application/x-php'], + 'php5' => ['application/x-php'], + 'phps' => ['application/x-php'], + 'pic' => ['image/x-pict'], + 'pict' => ['image/x-pict'], + 'pict1' => ['image/x-pict'], + 'pict2' => ['image/x-pict'], + 'pk' => ['application/x-tex-pk'], + 'pkg' => ['application/x-xar'], + 'pki' => ['application/pkixcmp'], + 'pkipath' => ['application/pkix-pkipath'], + 'pkpass' => ['application/vnd.apple.pkpass'], + 'pkr' => ['application/pgp-keys'], + 'pl' => ['application/x-perl', 'text/x-perl'], + 'pla' => ['audio/x-iriver-pla'], + 'plb' => ['application/vnd.3gpp.pic-bw-large'], + 'plc' => ['application/vnd.mobius.plc'], + 'plf' => ['application/vnd.pocketlearn'], + 'pln' => ['application/x-planperfect'], + 'pls' => ['application/pls', 'application/pls+xml', 'audio/scpls', 'audio/x-scpls'], + 'pm' => ['application/x-pagemaker', 'application/x-perl', 'text/x-perl'], + 'pm6' => ['application/x-pagemaker'], + 'pmd' => ['application/x-pagemaker'], + 'pml' => ['application/vnd.ctc-posml'], + 'png' => ['image/png', 'image/apng', 'image/vnd.mozilla.apng'], + 'pnm' => ['image/x-portable-anymap'], + 'pntg' => ['image/x-macpaint'], + 'po' => ['application/x-gettext', 'text/x-gettext-translation', 'text/x-po'], + 'pod' => ['application/x-perl', 'text/x-perl'], + 'por' => ['application/x-spss-por'], + 'portpkg' => ['application/vnd.macports.portpkg'], + 'pot' => ['application/mspowerpoint', 'application/powerpoint', 'application/vnd.ms-powerpoint', 'application/x-mspowerpoint', 'text/x-gettext-translation-template', 'text/x-pot'], + 'potm' => ['application/vnd.ms-powerpoint.template.macroenabled.12'], + 'potx' => ['application/vnd.openxmlformats-officedocument.presentationml.template'], + 'ppam' => ['application/vnd.ms-powerpoint.addin.macroenabled.12'], + 'ppd' => ['application/vnd.cups-ppd'], + 'ppm' => ['image/x-portable-pixmap'], + 'pps' => ['application/mspowerpoint', 'application/powerpoint', 'application/vnd.ms-powerpoint', 'application/x-mspowerpoint'], + 'ppsm' => ['application/vnd.ms-powerpoint.slideshow.macroenabled.12'], + 'ppsx' => ['application/vnd.openxmlformats-officedocument.presentationml.slideshow'], + 'ppt' => ['application/vnd.ms-powerpoint', 'application/mspowerpoint', 'application/powerpoint', 'application/x-mspowerpoint'], + 'pptm' => ['application/vnd.ms-powerpoint.presentation.macroenabled.12'], + 'pptx' => ['application/vnd.openxmlformats-officedocument.presentationml.presentation'], + 'ppz' => ['application/mspowerpoint', 'application/powerpoint', 'application/vnd.ms-powerpoint', 'application/x-mspowerpoint'], + 'pqa' => ['application/vnd.palm', 'application/x-palm-database'], + 'prc' => ['application/vnd.palm', 'application/x-mobipocket-ebook', 'application/x-palm-database', 'application/x-pilot'], + 'pre' => ['application/vnd.lotus-freelance'], + 'prf' => ['application/pics-rules'], + 'provx' => ['application/provenance+xml'], + 'ps' => ['application/postscript'], + 'ps.bz2' => ['application/x-bzpostscript'], + 'ps.gz' => ['application/x-gzpostscript'], + 'psb' => ['application/vnd.3gpp.pic-bw-small'], + 'psd' => ['application/photoshop', 'application/x-photoshop', 'image/photoshop', 'image/psd', 'image/vnd.adobe.photoshop', 'image/x-photoshop', 'image/x-psd'], + 'psf' => ['application/x-font-linux-psf', 'audio/x-psf'], + 'psf.gz' => ['application/x-gz-font-linux-psf'], + 'psflib' => ['audio/x-psflib'], + 'psid' => ['audio/prs.sid'], + 'pskcxml' => ['application/pskc+xml'], + 'psw' => ['application/x-pocket-word'], + 'pti' => ['image/prs.pti'], + 'ptid' => ['application/vnd.pvi.ptid1'], + 'pub' => ['application/vnd.ms-publisher', 'application/x-mspublisher'], + 'pvb' => ['application/vnd.3gpp.pic-bw-var'], + 'pw' => ['application/x-pw'], + 'pwn' => ['application/vnd.3m.post-it-notes'], + 'py' => ['text/x-python', 'text/x-python3'], + 'py3' => ['text/x-python3'], + 'py3x' => ['text/x-python3'], + 'pya' => ['audio/vnd.ms-playready.media.pya'], + 'pyc' => ['application/x-python-bytecode'], + 'pyi' => ['text/x-python3'], + 'pyo' => ['application/x-python-bytecode'], + 'pys' => ['application/x-pyspread-bz-spreadsheet'], + 'pysu' => ['application/x-pyspread-spreadsheet'], + 'pyv' => ['video/vnd.ms-playready.media.pyv'], + 'pyx' => ['text/x-python'], + 'qam' => ['application/vnd.epson.quickanime'], + 'qbo' => ['application/vnd.intu.qbo'], + 'qcow' => ['application/x-qemu-disk'], + 'qcow2' => ['application/x-qemu-disk'], + 'qd' => ['application/x-fd-file', 'application/x-raw-floppy-disk-image'], + 'qed' => ['application/x-qed-disk'], + 'qfx' => ['application/vnd.intu.qfx'], + 'qif' => ['application/x-qw', 'image/x-quicktime'], + 'qml' => ['text/x-qml'], + 'qmlproject' => ['text/x-qml'], + 'qmltypes' => ['text/x-qml'], + 'qoi' => ['image/qoi'], + 'qp' => ['application/x-qpress'], + 'qps' => ['application/vnd.publishare-delta-tree'], + 'qs' => ['application/sparql-query'], + 'qt' => ['video/quicktime'], + 'qti' => ['application/x-qtiplot'], + 'qti.gz' => ['application/x-qtiplot'], + 'qtif' => ['image/x-quicktime'], + 'qtl' => ['application/x-quicktime-media-link', 'application/x-quicktimeplayer'], + 'qtvr' => ['video/quicktime'], + 'qwd' => ['application/vnd.quark.quarkxpress'], + 'qwt' => ['application/vnd.quark.quarkxpress'], + 'qxb' => ['application/vnd.quark.quarkxpress'], + 'qxd' => ['application/vnd.quark.quarkxpress'], + 'qxl' => ['application/vnd.quark.quarkxpress'], + 'qxt' => ['application/vnd.quark.quarkxpress'], + 'ra' => ['audio/vnd.m-realaudio', 'audio/vnd.rn-realaudio', 'audio/x-pn-realaudio', 'audio/x-realaudio'], + 'raf' => ['image/x-fuji-raf'], + 'ram' => ['application/ram', 'audio/x-pn-realaudio'], + 'raml' => ['application/raml+yaml'], + 'rapd' => ['application/route-apd+xml'], + 'rar' => ['application/x-rar-compressed', 'application/vnd.rar', 'application/x-rar'], + 'ras' => ['image/x-cmu-raster'], + 'raw' => ['image/x-panasonic-raw', 'image/x-panasonic-rw'], + 'raw-disk-image' => ['application/vnd.efi.img', 'application/x-raw-disk-image'], + 'raw-disk-image.xz' => ['application/x-raw-disk-image-xz-compressed'], + 'rax' => ['audio/vnd.m-realaudio', 'audio/vnd.rn-realaudio', 'audio/x-pn-realaudio'], + 'rb' => ['application/x-ruby'], + 'rcprofile' => ['application/vnd.ipunplugged.rcprofile'], + 'rdf' => ['application/rdf+xml', 'text/rdf'], + 'rdfs' => ['application/rdf+xml', 'text/rdf'], + 'rdz' => ['application/vnd.data-vision.rdz'], + 'reg' => ['text/x-ms-regedit'], + 'rej' => ['application/x-reject', 'text/x-reject'], + 'relo' => ['application/p2p-overlay+xml'], + 'rep' => ['application/vnd.businessobjects'], + 'res' => ['application/x-dtbresource+xml', 'application/x-godot-resource'], + 'rgb' => ['image/x-rgb'], + 'rif' => ['application/reginfo+xml'], + 'rip' => ['audio/vnd.rip'], + 'ris' => ['application/x-research-info-systems'], + 'rl' => ['application/resource-lists+xml'], + 'rlc' => ['image/vnd.fujixerox.edmics-rlc'], + 'rld' => ['application/resource-lists-diff+xml'], + 'rle' => ['image/rle'], + 'rm' => ['application/vnd.rn-realmedia', 'application/vnd.rn-realmedia-vbr'], + 'rmi' => ['audio/midi'], + 'rmj' => ['application/vnd.rn-realmedia', 'application/vnd.rn-realmedia-vbr'], + 'rmm' => ['application/vnd.rn-realmedia', 'application/vnd.rn-realmedia-vbr'], + 'rmp' => ['audio/x-pn-realaudio-plugin'], + 'rms' => ['application/vnd.jcp.javame.midlet-rms', 'application/vnd.rn-realmedia', 'application/vnd.rn-realmedia-vbr'], + 'rmvb' => ['application/vnd.rn-realmedia', 'application/vnd.rn-realmedia-vbr'], + 'rmx' => ['application/vnd.rn-realmedia', 'application/vnd.rn-realmedia-vbr'], + 'rnc' => ['application/relax-ng-compact-syntax', 'application/x-rnc'], + 'rng' => ['application/xml', 'text/xml'], + 'roa' => ['application/rpki-roa'], + 'roff' => ['application/x-troff', 'text/troff', 'text/x-troff'], + 'ros' => ['text/x-common-lisp'], + 'rp' => ['image/vnd.rn-realpix'], + 'rp9' => ['application/vnd.cloanto.rp9'], + 'rpm' => ['application/x-redhat-package-manager', 'application/x-rpm'], + 'rpss' => ['application/vnd.nokia.radio-presets'], + 'rpst' => ['application/vnd.nokia.radio-preset'], + 'rq' => ['application/sparql-query'], + 'rs' => ['application/rls-services+xml', 'text/rust'], + 'rsat' => ['application/atsc-rsat+xml'], + 'rsd' => ['application/rsd+xml'], + 'rsheet' => ['application/urc-ressheet+xml'], + 'rss' => ['application/rss+xml', 'text/rss'], + 'rst' => ['text/x-rst'], + 'rt' => ['text/vnd.rn-realtext'], + 'rtf' => ['application/rtf', 'text/rtf'], + 'rtx' => ['text/richtext'], + 'run' => ['application/x-makeself'], + 'rusd' => ['application/route-usd+xml'], + 'rv' => ['video/vnd.rn-realvideo', 'video/x-real-video'], + 'rvx' => ['video/vnd.rn-realvideo', 'video/x-real-video'], + 'rw2' => ['image/x-panasonic-raw2', 'image/x-panasonic-rw2'], + 's' => ['text/x-asm'], + 's3m' => ['audio/s3m', 'audio/x-s3m'], + 'saf' => ['application/vnd.yamaha.smaf-audio'], + 'sage' => ['text/x-sagemath'], + 'sam' => ['application/x-amipro'], + 'sami' => ['application/x-sami'], + 'sap' => ['application/x-sap-file', 'application/x-thomson-sap-image'], + 'sass' => ['text/x-sass'], + 'sav' => ['application/x-spss-sav', 'application/x-spss-savefile'], + 'sbml' => ['application/sbml+xml'], + 'sc' => ['application/vnd.ibm.secure-container', 'text/x-scala'], + 'scala' => ['text/x-scala'], + 'scd' => ['application/x-msschedule'], + 'scm' => ['application/vnd.lotus-screencam', 'text/x-scheme'], + 'scn' => ['application/x-godot-scene'], + 'scope' => ['text/x-systemd-unit'], + 'scq' => ['application/scvp-cv-request'], + 'scs' => ['application/scvp-cv-response'], + 'scss' => ['text/x-scss'], + 'scurl' => ['text/vnd.curl.scurl'], + 'sda' => ['application/vnd.stardivision.draw'], + 'sdc' => ['application/vnd.stardivision.calc'], + 'sdd' => ['application/vnd.stardivision.impress'], + 'sdkd' => ['application/vnd.solent.sdkm+xml'], + 'sdkm' => ['application/vnd.solent.sdkm+xml'], + 'sdp' => ['application/sdp', 'application/vnd.sdp', 'application/vnd.stardivision.impress', 'application/x-sdp'], + 'sds' => ['application/vnd.stardivision.chart'], + 'sdw' => ['application/vnd.stardivision.writer', 'application/vnd.stardivision.writer-global'], + 'sea' => ['application/x-sea'], + 'see' => ['application/vnd.seemail'], + 'seed' => ['application/vnd.fdsn.seed'], + 'sema' => ['application/vnd.sema'], + 'semd' => ['application/vnd.semd'], + 'semf' => ['application/vnd.semf'], + 'senmlx' => ['application/senml+xml'], + 'sensmlx' => ['application/sensml+xml'], + 'ser' => ['application/java-serialized-object'], + 'service' => ['text/x-dbus-service', 'text/x-systemd-unit'], + 'setpay' => ['application/set-payment-initiation'], + 'setreg' => ['application/set-registration-initiation'], + 'sfc' => ['application/vnd.nintendo.snes.rom', 'application/x-snes-rom'], + 'sfd-hdstx' => ['application/vnd.hydrostatix.sof-data'], + 'sfs' => ['application/vnd.spotfire.sfs'], + 'sfv' => ['text/x-sfv'], + 'sg' => ['application/x-sg1000-rom'], + 'sgb' => ['application/x-gameboy-rom'], + 'sgd' => ['application/x-genesis-rom'], + 'sgf' => ['application/x-go-sgf'], + 'sgi' => ['image/sgi', 'image/x-sgi'], + 'sgl' => ['application/vnd.stardivision.writer', 'application/vnd.stardivision.writer-global'], + 'sgm' => ['text/sgml'], + 'sgml' => ['text/sgml'], + 'sh' => ['application/x-sh', 'application/x-shellscript', 'text/x-sh'], + 'shape' => ['application/x-dia-shape'], + 'shar' => ['application/x-shar'], + 'shex' => ['text/shex'], + 'shf' => ['application/shf+xml'], + 'shn' => ['application/x-shorten', 'audio/x-shorten'], + 'shtml' => ['text/html'], + 'siag' => ['application/x-siag'], + 'sid' => ['audio/prs.sid', 'image/x-mrsid-image'], + 'sieve' => ['application/sieve'], + 'sig' => ['application/pgp-signature'], + 'sik' => ['application/x-trash'], + 'sil' => ['audio/silk'], + 'silo' => ['model/mesh'], + 'sis' => ['application/vnd.symbian.install'], + 'sisx' => ['application/vnd.symbian.install', 'x-epoc/x-sisx-app'], + 'sit' => ['application/x-stuffit', 'application/stuffit', 'application/x-sit'], + 'sitx' => ['application/x-sitx', 'application/x-stuffitx'], + 'siv' => ['application/sieve'], + 'sk' => ['image/x-skencil'], + 'sk1' => ['image/x-skencil'], + 'skd' => ['application/vnd.koan'], + 'skm' => ['application/vnd.koan'], + 'skp' => ['application/vnd.koan'], + 'skr' => ['application/pgp-keys'], + 'skt' => ['application/vnd.koan'], + 'sldm' => ['application/vnd.ms-powerpoint.slide.macroenabled.12'], + 'sldx' => ['application/vnd.openxmlformats-officedocument.presentationml.slide'], + 'slice' => ['text/x-systemd-unit'], + 'slim' => ['text/slim'], + 'slk' => ['text/spreadsheet'], + 'slm' => ['text/slim'], + 'sls' => ['application/route-s-tsid+xml'], + 'slt' => ['application/vnd.epson.salt'], + 'sm' => ['application/vnd.stepmania.stepchart'], + 'smaf' => ['application/vnd.smaf', 'application/x-smaf'], + 'smc' => ['application/vnd.nintendo.snes.rom', 'application/x-snes-rom'], + 'smd' => ['application/vnd.stardivision.mail', 'application/x-genesis-rom'], + 'smf' => ['application/vnd.stardivision.math'], + 'smi' => ['application/smil', 'application/smil+xml', 'application/x-sami'], + 'smil' => ['application/smil', 'application/smil+xml'], + 'smk' => ['video/vnd.radgamettools.smacker'], + 'sml' => ['application/smil', 'application/smil+xml'], + 'sms' => ['application/x-sms-rom'], + 'smv' => ['video/x-smv'], + 'smzip' => ['application/vnd.stepmania.package'], + 'snap' => ['application/vnd.snap'], + 'snd' => ['audio/basic'], + 'snf' => ['application/x-font-snf'], + 'so' => ['application/x-sharedlib'], + 'socket' => ['text/x-systemd-unit'], + 'spc' => ['application/x-pkcs7-certificates'], + 'spd' => ['application/x-font-speedo'], + 'spdx' => ['text/spdx'], + 'spec' => ['text/x-rpm-spec'], + 'spf' => ['application/vnd.yamaha.smaf-phrase'], + 'spl' => ['application/futuresplash', 'application/vnd.adobe.flash.movie', 'application/x-futuresplash', 'application/x-shockwave-flash'], + 'spm' => ['application/x-source-rpm'], + 'spot' => ['text/vnd.in3d.spot'], + 'spp' => ['application/scvp-vp-response'], + 'spq' => ['application/scvp-vp-request'], + 'spx' => ['application/x-apple-systemprofiler+xml', 'audio/ogg', 'audio/x-speex', 'audio/x-speex+ogg'], + 'sql' => ['application/sql', 'application/x-sql', 'text/x-sql'], + 'sqlite2' => ['application/x-sqlite2'], + 'sqlite3' => ['application/vnd.sqlite3', 'application/x-sqlite3'], + 'sqsh' => ['application/vnd.squashfs'], + 'sr2' => ['image/x-sony-sr2'], + 'src' => ['application/x-wais-source'], + 'src.rpm' => ['application/x-source-rpm'], + 'srf' => ['image/x-sony-srf'], + 'srt' => ['application/x-srt', 'application/x-subrip'], + 'sru' => ['application/sru+xml'], + 'srx' => ['application/sparql-results+xml'], + 'ss' => ['text/x-scheme'], + 'ssa' => ['text/x-ssa'], + 'ssdl' => ['application/ssdl+xml'], + 'sse' => ['application/vnd.kodak-descriptor'], + 'ssf' => ['application/vnd.epson.ssf'], + 'ssml' => ['application/ssml+xml'], + 'st' => ['application/vnd.sailingtracker.track'], + 'stc' => ['application/vnd.sun.xml.calc.template'], + 'std' => ['application/vnd.sun.xml.draw.template'], + 'stf' => ['application/vnd.wt.stf'], + 'sti' => ['application/vnd.sun.xml.impress.template'], + 'stk' => ['application/hyperstudio'], + 'stl' => ['application/vnd.ms-pki.stl', 'model/stl', 'model/x.stl-ascii', 'model/x.stl-binary'], + 'stm' => ['audio/x-stm'], + 'stpx' => ['model/step+xml'], + 'stpxz' => ['model/step-xml+zip'], + 'stpz' => ['model/step+zip'], + 'str' => ['application/vnd.pg.format'], + 'stw' => ['application/vnd.sun.xml.writer.template'], + 'sty' => ['application/x-tex', 'text/x-tex'], + 'styl' => ['text/stylus'], + 'stylus' => ['text/stylus'], + 'sub' => ['image/vnd.dvb.subtitle', 'text/vnd.dvb.subtitle', 'text/x-microdvd', 'text/x-mpsub', 'text/x-subviewer'], + 'sun' => ['image/x-sun-raster'], + 'sus' => ['application/vnd.sus-calendar'], + 'susp' => ['application/vnd.sus-calendar'], + 'sv' => ['text/x-svsrc'], + 'sv4cpio' => ['application/x-sv4cpio'], + 'sv4crc' => ['application/x-sv4crc'], + 'svc' => ['application/vnd.dvb.service'], + 'svd' => ['application/vnd.svd'], + 'svg' => ['image/svg+xml', 'image/svg'], + 'svg.gz' => ['image/svg+xml-compressed'], + 'svgz' => ['image/svg+xml', 'image/svg+xml-compressed'], + 'svh' => ['text/x-svhdr'], + 'swa' => ['application/x-director'], + 'swap' => ['text/x-systemd-unit'], + 'swf' => ['application/futuresplash', 'application/vnd.adobe.flash.movie', 'application/x-shockwave-flash'], + 'swi' => ['application/vnd.aristanetworks.swi'], + 'swidtag' => ['application/swid+xml'], + 'swm' => ['application/x-ms-wim'], + 'sxc' => ['application/vnd.sun.xml.calc'], + 'sxd' => ['application/vnd.sun.xml.draw'], + 'sxg' => ['application/vnd.sun.xml.writer.global'], + 'sxi' => ['application/vnd.sun.xml.impress'], + 'sxm' => ['application/vnd.sun.xml.math'], + 'sxw' => ['application/vnd.sun.xml.writer'], + 'sylk' => ['text/spreadsheet'], + 't' => ['application/x-perl', 'application/x-troff', 'text/troff', 'text/x-perl', 'text/x-troff'], + 't2t' => ['text/x-txt2tags'], + 't3' => ['application/x-t3vm-image'], + 't38' => ['image/t38'], + 'taglet' => ['application/vnd.mynfc'], + 'tak' => ['audio/x-tak'], + 'tao' => ['application/vnd.tao.intent-module-archive'], + 'tap' => ['image/vnd.tencent.tap'], + 'tar' => ['application/x-tar', 'application/x-gtar'], + 'tar.Z' => ['application/x-tarz'], + 'tar.bz' => ['application/x-bzip-compressed-tar'], + 'tar.bz2' => ['application/x-bzip2-compressed-tar'], + 'tar.bz3' => ['application/x-bzip3-compressed-tar'], + 'tar.gz' => ['application/x-compressed-tar'], + 'tar.lrz' => ['application/x-lrzip-compressed-tar'], + 'tar.lz' => ['application/x-lzip-compressed-tar'], + 'tar.lz4' => ['application/x-lz4-compressed-tar'], + 'tar.lzma' => ['application/x-lzma-compressed-tar'], + 'tar.lzo' => ['application/x-tzo'], + 'tar.xz' => ['application/x-xz-compressed-tar'], + 'tar.zst' => ['application/x-zstd-compressed-tar'], + 'target' => ['text/x-systemd-unit'], + 'taz' => ['application/x-tarz'], + 'tb2' => ['application/x-bzip2-compressed-tar', 'application/x-bzip-compressed-tar'], + 'tbz' => ['application/x-bzip-compressed-tar'], + 'tbz2' => ['application/x-bzip2-compressed-tar', 'application/x-bzip-compressed-tar'], + 'tbz3' => ['application/x-bzip3-compressed-tar'], + 'tcap' => ['application/vnd.3gpp2.tcap'], + 'tcl' => ['application/x-tcl', 'text/tcl', 'text/x-tcl'], + 'td' => ['application/urc-targetdesc+xml'], + 'teacher' => ['application/vnd.smart.teacher'], + 'tei' => ['application/tei+xml'], + 'teicorpus' => ['application/tei+xml'], + 'tex' => ['application/x-tex', 'text/x-tex'], + 'texi' => ['application/x-texinfo', 'text/x-texinfo'], + 'texinfo' => ['application/x-texinfo', 'text/x-texinfo'], + 'text' => ['text/plain'], + 'tfi' => ['application/thraud+xml'], + 'tfm' => ['application/x-tex-tfm'], + 'tfx' => ['image/tiff-fx'], + 'tga' => ['application/tga', 'application/x-targa', 'application/x-tga', 'image/targa', 'image/tga', 'image/x-icb', 'image/x-targa', 'image/x-tga'], + 'tgz' => ['application/x-compressed-tar'], + 'theme' => ['application/x-theme'], + 'themepack' => ['application/x-windows-themepack'], + 'thmx' => ['application/vnd.ms-officetheme'], + 'tif' => ['image/tiff'], + 'tiff' => ['image/tiff'], + 'timer' => ['text/x-systemd-unit'], + 'tk' => ['application/x-tcl', 'text/tcl', 'text/x-tcl'], + 'tlrz' => ['application/x-lrzip-compressed-tar'], + 'tlz' => ['application/x-lzma-compressed-tar'], + 'tmo' => ['application/vnd.tmobile-livetv'], + 'tmx' => ['application/x-tiled-tmx'], + 'tnef' => ['application/ms-tnef', 'application/vnd.ms-tnef'], + 'tnf' => ['application/ms-tnef', 'application/vnd.ms-tnef'], + 'toc' => ['application/x-cdrdao-toc'], + 'toml' => ['application/toml'], + 'torrent' => ['application/x-bittorrent'], + 'tpic' => ['application/tga', 'application/x-targa', 'application/x-tga', 'image/targa', 'image/tga', 'image/x-icb', 'image/x-targa', 'image/x-tga'], + 'tpl' => ['application/vnd.groove-tool-template'], + 'tpt' => ['application/vnd.trid.tpt'], + 'tr' => ['application/x-troff', 'text/troff', 'text/x-troff'], + 'tra' => ['application/vnd.trueapp'], + 'tres' => ['application/x-godot-resource'], + 'trig' => ['application/trig', 'application/x-trig'], + 'trm' => ['application/x-msterminal'], + 'ts' => ['application/x-linguist', 'text/vnd.qt.linguist', 'text/vnd.trolltech.linguist', 'video/mp2t'], + 'tscn' => ['application/x-godot-scene'], + 'tsd' => ['application/timestamped-data'], + 'tsv' => ['text/tab-separated-values'], + 'tsx' => ['application/x-tiled-tsx'], + 'tta' => ['audio/tta', 'audio/x-tta'], + 'ttc' => ['font/collection'], + 'ttf' => ['application/x-font-truetype', 'application/x-font-ttf', 'font/ttf'], + 'ttl' => ['text/turtle'], + 'ttml' => ['application/ttml+xml'], + 'ttx' => ['application/x-font-ttx'], + 'twd' => ['application/vnd.simtech-mindmapper'], + 'twds' => ['application/vnd.simtech-mindmapper'], + 'twig' => ['text/x-twig'], + 'txd' => ['application/vnd.genomatix.tuxedo'], + 'txf' => ['application/vnd.mobius.txf'], + 'txt' => ['text/plain'], + 'txz' => ['application/x-xz-compressed-tar'], + 'typ' => ['text/x-typst'], + 'tzo' => ['application/x-tzo'], + 'tzst' => ['application/x-zstd-compressed-tar'], + 'u32' => ['application/x-authorware-bin'], + 'u8dsn' => ['message/global-delivery-status'], + 'u8hdr' => ['message/global-headers'], + 'u8mdn' => ['message/global-disposition-notification'], + 'u8msg' => ['message/global'], + 'ubj' => ['application/ubjson'], + 'udeb' => ['application/vnd.debian.binary-package', 'application/x-deb', 'application/x-debian-package'], + 'ufd' => ['application/vnd.ufdl'], + 'ufdl' => ['application/vnd.ufdl'], + 'ufraw' => ['application/x-ufraw'], + 'ui' => ['application/x-designer', 'application/x-gtk-builder'], + 'uil' => ['text/x-uil'], + 'ult' => ['audio/x-mod'], + 'ulx' => ['application/x-glulx'], + 'umj' => ['application/vnd.umajin'], + 'unf' => ['application/x-nes-rom'], + 'uni' => ['audio/x-mod'], + 'unif' => ['application/x-nes-rom'], + 'unityweb' => ['application/vnd.unity'], + 'uoml' => ['application/vnd.uoml+xml'], + 'uri' => ['text/uri-list'], + 'uris' => ['text/uri-list'], + 'url' => ['application/x-mswinurl'], + 'urls' => ['text/uri-list'], + 'usdz' => ['model/vnd.usdz+zip'], + 'ustar' => ['application/x-ustar'], + 'utz' => ['application/vnd.uiq.theme'], + 'uu' => ['text/x-uuencode'], + 'uue' => ['text/x-uuencode', 'zz-application/zz-winassoc-uu'], + 'uva' => ['audio/vnd.dece.audio'], + 'uvd' => ['application/vnd.dece.data'], + 'uvf' => ['application/vnd.dece.data'], + 'uvg' => ['image/vnd.dece.graphic'], + 'uvh' => ['video/vnd.dece.hd'], + 'uvi' => ['image/vnd.dece.graphic'], + 'uvm' => ['video/vnd.dece.mobile'], + 'uvp' => ['video/vnd.dece.pd'], + 'uvs' => ['video/vnd.dece.sd'], + 'uvt' => ['application/vnd.dece.ttml+xml'], + 'uvu' => ['video/vnd.uvvu.mp4'], + 'uvv' => ['video/vnd.dece.video'], + 'uvva' => ['audio/vnd.dece.audio'], + 'uvvd' => ['application/vnd.dece.data'], + 'uvvf' => ['application/vnd.dece.data'], + 'uvvg' => ['image/vnd.dece.graphic'], + 'uvvh' => ['video/vnd.dece.hd'], + 'uvvi' => ['image/vnd.dece.graphic'], + 'uvvm' => ['video/vnd.dece.mobile'], + 'uvvp' => ['video/vnd.dece.pd'], + 'uvvs' => ['video/vnd.dece.sd'], + 'uvvt' => ['application/vnd.dece.ttml+xml'], + 'uvvu' => ['video/vnd.uvvu.mp4'], + 'uvvv' => ['video/vnd.dece.video'], + 'uvvx' => ['application/vnd.dece.unspecified'], + 'uvvz' => ['application/vnd.dece.zip'], + 'uvx' => ['application/vnd.dece.unspecified'], + 'uvz' => ['application/vnd.dece.zip'], + 'v' => ['text/x-verilog'], + 'v64' => ['application/x-n64-rom'], + 'vala' => ['text/x-vala'], + 'vapi' => ['text/x-vala'], + 'vb' => ['application/x-virtual-boy-rom'], + 'vbox' => ['application/x-virtualbox-vbox'], + 'vbox-extpack' => ['application/x-virtualbox-vbox-extpack'], + 'vbs' => ['text/vbs', 'text/vbscript'], + 'vcard' => ['text/directory', 'text/vcard', 'text/x-vcard'], + 'vcd' => ['application/x-cdlink'], + 'vcf' => ['text/x-vcard', 'text/directory', 'text/vcard'], + 'vcg' => ['application/vnd.groove-vcard'], + 'vcs' => ['application/ics', 'text/calendar', 'text/x-vcalendar'], + 'vct' => ['text/directory', 'text/vcard', 'text/x-vcard'], + 'vcx' => ['application/vnd.vcx'], + 'vda' => ['application/tga', 'application/x-targa', 'application/x-tga', 'image/targa', 'image/tga', 'image/x-icb', 'image/x-targa', 'image/x-tga'], + 'vdi' => ['application/x-vdi-disk', 'application/x-virtualbox-vdi'], + 'vds' => ['model/vnd.sap.vds'], + 'vhd' => ['application/x-vhd-disk', 'application/x-virtualbox-vhd', 'text/x-vhdl'], + 'vhdl' => ['text/x-vhdl'], + 'vhdx' => ['application/x-vhdx-disk', 'application/x-virtualbox-vhdx'], + 'vis' => ['application/vnd.visionary'], + 'viv' => ['video/vivo', 'video/vnd.vivo'], + 'vivo' => ['video/vivo', 'video/vnd.vivo'], + 'vlc' => ['application/m3u', 'audio/m3u', 'audio/mpegurl', 'audio/x-m3u', 'audio/x-mp3-playlist', 'audio/x-mpegurl'], + 'vmdk' => ['application/x-virtualbox-vmdk', 'application/x-vmdk-disk'], + 'vob' => ['video/mpeg', 'video/mpeg-system', 'video/x-mpeg', 'video/x-mpeg-system', 'video/x-mpeg2', 'video/x-ms-vob'], + 'voc' => ['audio/x-voc'], + 'vor' => ['application/vnd.stardivision.writer', 'application/vnd.stardivision.writer-global'], + 'vox' => ['application/x-authorware-bin'], + 'vpc' => ['application/x-vhd-disk', 'application/x-virtualbox-vhd'], + 'vrm' => ['model/vrml'], + 'vrml' => ['model/vrml'], + 'vsd' => ['application/vnd.visio'], + 'vsdm' => ['application/vnd.ms-visio.drawing.macroenabled.main+xml'], + 'vsdx' => ['application/vnd.ms-visio.drawing.main+xml'], + 'vsf' => ['application/vnd.vsf'], + 'vss' => ['application/vnd.visio'], + 'vssm' => ['application/vnd.ms-visio.stencil.macroenabled.main+xml'], + 'vssx' => ['application/vnd.ms-visio.stencil.main+xml'], + 'vst' => ['application/tga', 'application/vnd.visio', 'application/x-targa', 'application/x-tga', 'image/targa', 'image/tga', 'image/x-icb', 'image/x-targa', 'image/x-tga'], + 'vstm' => ['application/vnd.ms-visio.template.macroenabled.main+xml'], + 'vstx' => ['application/vnd.ms-visio.template.main+xml'], + 'vsw' => ['application/vnd.visio'], + 'vtf' => ['image/vnd.valve.source.texture'], + 'vtt' => ['text/vtt'], + 'vtu' => ['model/vnd.vtu'], + 'vxml' => ['application/voicexml+xml'], + 'w3d' => ['application/x-director'], + 'wad' => ['application/x-doom', 'application/x-doom-wad', 'application/x-wii-wad'], + 'wadl' => ['application/vnd.sun.wadl+xml'], + 'war' => ['application/java-archive'], + 'wasm' => ['application/wasm'], + 'wav' => ['audio/wav', 'audio/vnd.wave', 'audio/wave', 'audio/x-wav'], + 'wax' => ['application/x-ms-asx', 'audio/x-ms-asx', 'audio/x-ms-wax', 'video/x-ms-wax', 'video/x-ms-wmx', 'video/x-ms-wvx'], + 'wb1' => ['application/x-quattropro'], + 'wb2' => ['application/x-quattropro'], + 'wb3' => ['application/x-quattropro'], + 'wbmp' => ['image/vnd.wap.wbmp'], + 'wbs' => ['application/vnd.criticaltools.wbs+xml'], + 'wbxml' => ['application/vnd.wap.wbxml'], + 'wcm' => ['application/vnd.ms-works'], + 'wdb' => ['application/vnd.ms-works'], + 'wdp' => ['image/jxr', 'image/vnd.ms-photo'], + 'weba' => ['audio/webm'], + 'webapp' => ['application/x-web-app-manifest+json'], + 'webm' => ['video/webm'], + 'webmanifest' => ['application/manifest+json'], + 'webp' => ['image/webp'], + 'wg' => ['application/vnd.pmi.widget'], + 'wgt' => ['application/widget'], + 'wif' => ['application/watcherinfo+xml'], + 'wim' => ['application/x-ms-wim'], + 'wk1' => ['application/lotus123', 'application/vnd.lotus-1-2-3', 'application/wk1', 'application/x-123', 'application/x-lotus123', 'zz-application/zz-winassoc-123'], + 'wk3' => ['application/lotus123', 'application/vnd.lotus-1-2-3', 'application/wk1', 'application/x-123', 'application/x-lotus123', 'zz-application/zz-winassoc-123'], + 'wk4' => ['application/lotus123', 'application/vnd.lotus-1-2-3', 'application/wk1', 'application/x-123', 'application/x-lotus123', 'zz-application/zz-winassoc-123'], + 'wkdownload' => ['application/x-partial-download'], + 'wks' => ['application/lotus123', 'application/vnd.lotus-1-2-3', 'application/vnd.ms-works', 'application/wk1', 'application/x-123', 'application/x-lotus123', 'zz-application/zz-winassoc-123'], + 'wm' => ['video/x-ms-wm'], + 'wma' => ['audio/x-ms-wma', 'audio/wma'], + 'wmd' => ['application/x-ms-wmd'], + 'wmf' => ['application/wmf', 'application/x-msmetafile', 'application/x-wmf', 'image/wmf', 'image/x-win-metafile', 'image/x-wmf'], + 'wml' => ['text/vnd.wap.wml'], + 'wmlc' => ['application/vnd.wap.wmlc'], + 'wmls' => ['text/vnd.wap.wmlscript'], + 'wmlsc' => ['application/vnd.wap.wmlscriptc'], + 'wmv' => ['audio/x-ms-wmv', 'video/x-ms-wmv'], + 'wmx' => ['application/x-ms-asx', 'audio/x-ms-asx', 'video/x-ms-wax', 'video/x-ms-wmx', 'video/x-ms-wvx'], + 'wmz' => ['application/x-ms-wmz', 'application/x-msmetafile'], + 'woff' => ['application/font-woff', 'application/x-font-woff', 'font/woff'], + 'woff2' => ['font/woff2'], + 'wp' => ['application/vnd.wordperfect', 'application/wordperfect', 'application/x-wordperfect'], + 'wp4' => ['application/vnd.wordperfect', 'application/wordperfect', 'application/x-wordperfect'], + 'wp5' => ['application/vnd.wordperfect', 'application/wordperfect', 'application/x-wordperfect'], + 'wp6' => ['application/vnd.wordperfect', 'application/wordperfect', 'application/x-wordperfect'], + 'wpd' => ['application/vnd.wordperfect', 'application/wordperfect', 'application/x-wordperfect'], + 'wpg' => ['application/x-wpg'], + 'wpl' => ['application/vnd.ms-wpl'], + 'wpp' => ['application/vnd.wordperfect', 'application/wordperfect', 'application/x-wordperfect'], + 'wps' => ['application/vnd.ms-works'], + 'wqd' => ['application/vnd.wqd'], + 'wri' => ['application/x-mswrite'], + 'wrl' => ['model/vrml'], + 'ws' => ['application/x-wonderswan-rom'], + 'wsc' => ['application/x-wonderswan-color-rom', 'message/vnd.wfa.wsc'], + 'wsdl' => ['application/wsdl+xml'], + 'wsgi' => ['text/x-python'], + 'wspolicy' => ['application/wspolicy+xml'], + 'wtb' => ['application/vnd.webturbo'], + 'wv' => ['audio/x-wavpack'], + 'wvc' => ['audio/x-wavpack-correction'], + 'wvp' => ['audio/x-wavpack'], + 'wvx' => ['application/x-ms-asx', 'audio/x-ms-asx', 'video/x-ms-wax', 'video/x-ms-wmx', 'video/x-ms-wvx'], + 'wwf' => ['application/wwf', 'application/x-wwf'], + 'x32' => ['application/x-authorware-bin'], + 'x3d' => ['model/x3d+xml'], + 'x3db' => ['model/x3d+binary', 'model/x3d+fastinfoset'], + 'x3dbz' => ['model/x3d+binary'], + 'x3dv' => ['model/x3d+vrml', 'model/x3d-vrml'], + 'x3dvz' => ['model/x3d+vrml'], + 'x3dz' => ['model/x3d+xml'], + 'x3f' => ['image/x-sigma-x3f'], + 'x_b' => ['model/vnd.parasolid.transmit.binary'], + 'x_t' => ['model/vnd.parasolid.transmit.text'], + 'xac' => ['application/x-gnucash'], + 'xaml' => ['application/xaml+xml'], + 'xap' => ['application/x-silverlight-app'], + 'xar' => ['application/vnd.xara', 'application/x-xar'], + 'xav' => ['application/xcap-att+xml'], + 'xbap' => ['application/x-ms-xbap'], + 'xbd' => ['application/vnd.fujixerox.docuworks.binder'], + 'xbel' => ['application/x-xbel'], + 'xbl' => ['application/xml', 'text/xml'], + 'xbm' => ['image/x-xbitmap'], + 'xca' => ['application/xcap-caps+xml'], + 'xcf' => ['image/x-xcf'], + 'xcf.bz2' => ['image/x-compressed-xcf'], + 'xcf.gz' => ['image/x-compressed-xcf'], + 'xcs' => ['application/calendar+xml'], + 'xdf' => ['application/mrb-consumer+xml', 'application/mrb-publish+xml', 'application/xcap-diff+xml'], + 'xdgapp' => ['application/vnd.flatpak', 'application/vnd.xdgapp'], + 'xdm' => ['application/vnd.syncml.dm+xml'], + 'xdp' => ['application/vnd.adobe.xdp+xml'], + 'xdssc' => ['application/dssc+xml'], + 'xdw' => ['application/vnd.fujixerox.docuworks'], + 'xel' => ['application/xcap-el+xml'], + 'xenc' => ['application/xenc+xml'], + 'xer' => ['application/patch-ops-error+xml', 'application/xcap-error+xml'], + 'xfdf' => ['application/vnd.adobe.xfdf'], + 'xfdl' => ['application/vnd.xfdl'], + 'xhe' => ['audio/usac'], + 'xht' => ['application/xhtml+xml'], + 'xhtml' => ['application/xhtml+xml'], + 'xhvml' => ['application/xv+xml'], + 'xi' => ['audio/x-xi'], + 'xif' => ['image/vnd.xiff'], + 'xla' => ['application/msexcel', 'application/vnd.ms-excel', 'application/x-msexcel', 'zz-application/zz-winassoc-xls'], + 'xlam' => ['application/vnd.ms-excel.addin.macroenabled.12'], + 'xlc' => ['application/msexcel', 'application/vnd.ms-excel', 'application/x-msexcel', 'zz-application/zz-winassoc-xls'], + 'xld' => ['application/msexcel', 'application/vnd.ms-excel', 'application/x-msexcel', 'zz-application/zz-winassoc-xls'], + 'xlf' => ['application/x-xliff', 'application/x-xliff+xml', 'application/xliff+xml'], + 'xliff' => ['application/x-xliff', 'application/xliff+xml'], + 'xll' => ['application/msexcel', 'application/vnd.ms-excel', 'application/x-msexcel', 'zz-application/zz-winassoc-xls'], + 'xlm' => ['application/msexcel', 'application/vnd.ms-excel', 'application/x-msexcel', 'zz-application/zz-winassoc-xls'], + 'xlr' => ['application/vnd.ms-works'], + 'xls' => ['application/vnd.ms-excel', 'application/msexcel', 'application/x-msexcel', 'zz-application/zz-winassoc-xls'], + 'xlsb' => ['application/vnd.ms-excel.sheet.binary.macroenabled.12'], + 'xlsm' => ['application/vnd.ms-excel.sheet.macroenabled.12'], + 'xlsx' => ['application/vnd.openxmlformats-officedocument.spreadsheetml.sheet'], + 'xlt' => ['application/msexcel', 'application/vnd.ms-excel', 'application/x-msexcel', 'zz-application/zz-winassoc-xls'], + 'xltm' => ['application/vnd.ms-excel.template.macroenabled.12'], + 'xltx' => ['application/vnd.openxmlformats-officedocument.spreadsheetml.template'], + 'xlw' => ['application/msexcel', 'application/vnd.ms-excel', 'application/x-msexcel', 'zz-application/zz-winassoc-xls'], + 'xm' => ['audio/x-xm', 'audio/xm'], + 'xmf' => ['audio/x-xmf', 'audio/xmf'], + 'xmi' => ['text/x-xmi'], + 'xml' => ['application/xml', 'text/xml'], + 'xns' => ['application/xcap-ns+xml'], + 'xo' => ['application/vnd.olpc-sugar'], + 'xop' => ['application/xop+xml'], + 'xpi' => ['application/x-xpinstall'], + 'xpl' => ['application/xproc+xml'], + 'xpm' => ['image/x-xpixmap', 'image/x-xpm'], + 'xpr' => ['application/vnd.is-xpr'], + 'xps' => ['application/vnd.ms-xpsdocument', 'application/xps'], + 'xpw' => ['application/vnd.intercon.formnet'], + 'xpx' => ['application/vnd.intercon.formnet'], + 'xsd' => ['application/xml', 'text/xml'], + 'xsl' => ['application/xml', 'application/xslt+xml'], + 'xslfo' => ['text/x-xslfo'], + 'xslt' => ['application/xslt+xml'], + 'xsm' => ['application/vnd.syncml+xml'], + 'xspf' => ['application/x-xspf+xml', 'application/xspf+xml'], + 'xul' => ['application/vnd.mozilla.xul+xml'], + 'xvm' => ['application/xv+xml'], + 'xvml' => ['application/xv+xml'], + 'xwd' => ['image/x-xwindowdump'], + 'xyz' => ['chemical/x-xyz'], + 'xz' => ['application/x-xz'], + 'yaml' => ['application/yaml', 'application/x-yaml', 'text/x-yaml', 'text/yaml'], + 'yang' => ['application/yang'], + 'yin' => ['application/yin+xml'], + 'yml' => ['application/yaml', 'application/x-yaml', 'text/x-yaml', 'text/yaml'], + 'ymp' => ['text/x-suse-ymp'], + 'yt' => ['application/vnd.youtube.yt', 'video/vnd.youtube.yt'], + 'z1' => ['application/x-zmachine'], + 'z2' => ['application/x-zmachine'], + 'z3' => ['application/x-zmachine'], + 'z4' => ['application/x-zmachine'], + 'z5' => ['application/x-zmachine'], + 'z6' => ['application/x-zmachine'], + 'z64' => ['application/x-n64-rom'], + 'z7' => ['application/x-zmachine'], + 'z8' => ['application/x-zmachine'], + 'zabw' => ['application/x-abiword'], + 'zaz' => ['application/vnd.zzazz.deck+xml'], + 'zim' => ['application/x-openzim'], + 'zip' => ['application/zip', 'application/x-zip', 'application/x-zip-compressed'], + 'zipx' => ['application/x-zip', 'application/x-zip-compressed', 'application/zip'], + 'zir' => ['application/vnd.zul'], + 'zirz' => ['application/vnd.zul'], + 'zmm' => ['application/vnd.handheld-entertainment+xml'], + 'zoo' => ['application/x-zoo'], + 'zpaq' => ['application/x-zpaq'], + 'zsav' => ['application/x-spss-sav', 'application/x-spss-savefile'], + 'zst' => ['application/zstd'], + 'zz' => ['application/zlib'], + ]; +} diff --git a/3rdparty/symfony/mime/MimeTypesInterface.php b/3rdparty/symfony/mime/MimeTypesInterface.php new file mode 100644 index 00000000..17d45ad2 --- /dev/null +++ b/3rdparty/symfony/mime/MimeTypesInterface.php @@ -0,0 +1,32 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Component\Mime; + +/** + * @author Fabien Potencier + */ +interface MimeTypesInterface extends MimeTypeGuesserInterface +{ + /** + * Gets the extensions for the given MIME type in decreasing order of preference. + * + * @return string[] + */ + public function getExtensions(string $mimeType): array; + + /** + * Gets the MIME types for the given extension in decreasing order of preference. + * + * @return string[] + */ + public function getMimeTypes(string $ext): array; +} diff --git a/3rdparty/symfony/mime/Part/AbstractMultipartPart.php b/3rdparty/symfony/mime/Part/AbstractMultipartPart.php new file mode 100644 index 00000000..1da0ddf3 --- /dev/null +++ b/3rdparty/symfony/mime/Part/AbstractMultipartPart.php @@ -0,0 +1,95 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Component\Mime\Part; + +use Symfony\Component\Mime\Header\Headers; + +/** + * @author Fabien Potencier + */ +abstract class AbstractMultipartPart extends AbstractPart +{ + private ?string $boundary = null; + private array $parts = []; + + public function __construct(AbstractPart ...$parts) + { + parent::__construct(); + + foreach ($parts as $part) { + $this->parts[] = $part; + } + } + + /** + * @return AbstractPart[] + */ + public function getParts(): array + { + return $this->parts; + } + + public function getMediaType(): string + { + return 'multipart'; + } + + public function getPreparedHeaders(): Headers + { + $headers = parent::getPreparedHeaders(); + $headers->setHeaderParameter('Content-Type', 'boundary', $this->getBoundary()); + + return $headers; + } + + public function bodyToString(): string + { + $parts = $this->getParts(); + $string = ''; + foreach ($parts as $part) { + $string .= '--'.$this->getBoundary()."\r\n".$part->toString()."\r\n"; + } + $string .= '--'.$this->getBoundary()."--\r\n"; + + return $string; + } + + public function bodyToIterable(): iterable + { + $parts = $this->getParts(); + foreach ($parts as $part) { + yield '--'.$this->getBoundary()."\r\n"; + yield from $part->toIterable(); + yield "\r\n"; + } + yield '--'.$this->getBoundary()."--\r\n"; + } + + public function asDebugString(): string + { + $str = parent::asDebugString(); + foreach ($this->getParts() as $part) { + $lines = explode("\n", $part->asDebugString()); + $str .= "\n └ ".array_shift($lines); + foreach ($lines as $line) { + $str .= "\n |".$line; + } + } + + return $str; + } + + private function getBoundary(): string + { + return $this->boundary ??= strtr(base64_encode(random_bytes(6)), '+/', '-_'); + } +} diff --git a/3rdparty/symfony/mime/Part/AbstractPart.php b/3rdparty/symfony/mime/Part/AbstractPart.php new file mode 100644 index 00000000..130106d6 --- /dev/null +++ b/3rdparty/symfony/mime/Part/AbstractPart.php @@ -0,0 +1,65 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Component\Mime\Part; + +use Symfony\Component\Mime\Header\Headers; + +/** + * @author Fabien Potencier + */ +abstract class AbstractPart +{ + private Headers $headers; + + public function __construct() + { + $this->headers = new Headers(); + } + + public function getHeaders(): Headers + { + return $this->headers; + } + + public function getPreparedHeaders(): Headers + { + $headers = clone $this->headers; + $headers->setHeaderBody('Parameterized', 'Content-Type', $this->getMediaType().'/'.$this->getMediaSubtype()); + + return $headers; + } + + public function toString(): string + { + return $this->getPreparedHeaders()->toString()."\r\n".$this->bodyToString(); + } + + public function toIterable(): iterable + { + yield $this->getPreparedHeaders()->toString(); + yield "\r\n"; + yield from $this->bodyToIterable(); + } + + public function asDebugString(): string + { + return $this->getMediaType().'/'.$this->getMediaSubtype(); + } + + abstract public function bodyToString(): string; + + abstract public function bodyToIterable(): iterable; + + abstract public function getMediaType(): string; + + abstract public function getMediaSubtype(): string; +} diff --git a/3rdparty/symfony/mime/Part/DataPart.php b/3rdparty/symfony/mime/Part/DataPart.php new file mode 100644 index 00000000..9d2f3be4 --- /dev/null +++ b/3rdparty/symfony/mime/Part/DataPart.php @@ -0,0 +1,168 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Component\Mime\Part; + +use Symfony\Component\Mime\Exception\InvalidArgumentException; +use Symfony\Component\Mime\Header\Headers; + +/** + * @author Fabien Potencier + */ +class DataPart extends TextPart +{ + /** @internal */ + protected array $_parent; + + private ?string $filename = null; + private string $mediaType; + private ?string $cid = null; + + /** + * @param resource|string|File $body Use a File instance to defer loading the file until rendering + */ + public function __construct($body, ?string $filename = null, ?string $contentType = null, ?string $encoding = null) + { + if ($body instanceof File && !$filename) { + $filename = $body->getFilename(); + } + + $contentType ??= $body instanceof File ? $body->getContentType() : 'application/octet-stream'; + [$this->mediaType, $subtype] = explode('/', $contentType); + + parent::__construct($body, null, $subtype, $encoding); + + if (null !== $filename) { + $this->filename = $filename; + $this->setName($filename); + } + $this->setDisposition('attachment'); + } + + public static function fromPath(string $path, ?string $name = null, ?string $contentType = null): self + { + return new self(new File($path), $name, $contentType); + } + + /** + * @return $this + */ + public function asInline(): static + { + return $this->setDisposition('inline'); + } + + /** + * @return $this + */ + public function setContentId(string $cid): static + { + if (!str_contains($cid, '@')) { + throw new InvalidArgumentException(sprintf('Invalid cid "%s".', $cid)); + } + + $this->cid = $cid; + + return $this; + } + + public function getContentId(): string + { + return $this->cid ?: $this->cid = $this->generateContentId(); + } + + public function hasContentId(): bool + { + return null !== $this->cid; + } + + public function getMediaType(): string + { + return $this->mediaType; + } + + public function getPreparedHeaders(): Headers + { + $headers = parent::getPreparedHeaders(); + + if (null !== $this->cid) { + $headers->setHeaderBody('Id', 'Content-ID', $this->cid); + } + + if (null !== $this->filename) { + $headers->setHeaderParameter('Content-Disposition', 'filename', $this->filename); + } + + return $headers; + } + + public function asDebugString(): string + { + $str = parent::asDebugString(); + if (null !== $this->filename) { + $str .= ' filename: '.$this->filename; + } + + return $str; + } + + public function getFilename(): ?string + { + return $this->filename; + } + + public function getContentType(): string + { + return implode('/', [$this->getMediaType(), $this->getMediaSubtype()]); + } + + private function generateContentId(): string + { + return bin2hex(random_bytes(16)).'@symfony'; + } + + public function __sleep(): array + { + // converts the body to a string + parent::__sleep(); + + $this->_parent = []; + foreach (['body', 'charset', 'subtype', 'disposition', 'name', 'encoding'] as $name) { + $r = new \ReflectionProperty(TextPart::class, $name); + $this->_parent[$name] = $r->getValue($this); + } + $this->_headers = $this->getHeaders(); + + return ['_headers', '_parent', 'filename', 'mediaType']; + } + + /** + * @return void + */ + public function __wakeup() + { + $r = new \ReflectionProperty(AbstractPart::class, 'headers'); + $r->setValue($this, $this->_headers); + unset($this->_headers); + + if (!\is_array($this->_parent)) { + throw new \BadMethodCallException('Cannot unserialize '.__CLASS__); + } + foreach (['body', 'charset', 'subtype', 'disposition', 'name', 'encoding'] as $name) { + if (null !== $this->_parent[$name] && !\is_string($this->_parent[$name]) && !$this->_parent[$name] instanceof File) { + throw new \BadMethodCallException('Cannot unserialize '.__CLASS__); + } + $r = new \ReflectionProperty(TextPart::class, $name); + $r->setValue($this, $this->_parent[$name]); + } + unset($this->_parent); + } +} diff --git a/3rdparty/symfony/mime/Part/File.php b/3rdparty/symfony/mime/Part/File.php new file mode 100644 index 00000000..cd05a3de --- /dev/null +++ b/3rdparty/symfony/mime/Part/File.php @@ -0,0 +1,51 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Component\Mime\Part; + +use Symfony\Component\Mime\MimeTypes; + +/** + * @author Fabien Potencier + */ +class File +{ + private static MimeTypes $mimeTypes; + + public function __construct( + private string $path, + private ?string $filename = null, + ) { + } + + public function getPath(): string + { + return $this->path; + } + + public function getContentType(): string + { + $ext = strtolower(pathinfo($this->path, \PATHINFO_EXTENSION)); + self::$mimeTypes ??= new MimeTypes(); + + return self::$mimeTypes->getMimeTypes($ext)[0] ?? 'application/octet-stream'; + } + + public function getSize(): int + { + return filesize($this->path); + } + + public function getFilename(): string + { + return $this->filename ??= basename($this->getPath()); + } +} diff --git a/3rdparty/symfony/mime/Part/MessagePart.php b/3rdparty/symfony/mime/Part/MessagePart.php new file mode 100644 index 00000000..9d30544a --- /dev/null +++ b/3rdparty/symfony/mime/Part/MessagePart.php @@ -0,0 +1,72 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Component\Mime\Part; + +use Symfony\Component\Mime\Message; +use Symfony\Component\Mime\RawMessage; + +/** + * @final + * + * @author Fabien Potencier + */ +class MessagePart extends DataPart +{ + private RawMessage $message; + + public function __construct(RawMessage $message) + { + if ($message instanceof Message) { + $name = $message->getHeaders()->getHeaderBody('Subject').'.eml'; + } else { + $name = 'email.eml'; + } + parent::__construct('', $name); + + $this->message = $message; + } + + public function getMediaType(): string + { + return 'message'; + } + + public function getMediaSubtype(): string + { + return 'rfc822'; + } + + public function getBody(): string + { + return $this->message->toString(); + } + + public function bodyToString(): string + { + return $this->getBody(); + } + + public function bodyToIterable(): iterable + { + return $this->message->toIterable(); + } + + public function __sleep(): array + { + return ['message']; + } + + public function __wakeup(): void + { + $this->__construct($this->message); + } +} diff --git a/3rdparty/symfony/mime/Part/Multipart/AlternativePart.php b/3rdparty/symfony/mime/Part/Multipart/AlternativePart.php new file mode 100644 index 00000000..fd754234 --- /dev/null +++ b/3rdparty/symfony/mime/Part/Multipart/AlternativePart.php @@ -0,0 +1,25 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Component\Mime\Part\Multipart; + +use Symfony\Component\Mime\Part\AbstractMultipartPart; + +/** + * @author Fabien Potencier + */ +final class AlternativePart extends AbstractMultipartPart +{ + public function getMediaSubtype(): string + { + return 'alternative'; + } +} diff --git a/3rdparty/symfony/mime/Part/Multipart/DigestPart.php b/3rdparty/symfony/mime/Part/Multipart/DigestPart.php new file mode 100644 index 00000000..27537f15 --- /dev/null +++ b/3rdparty/symfony/mime/Part/Multipart/DigestPart.php @@ -0,0 +1,31 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Component\Mime\Part\Multipart; + +use Symfony\Component\Mime\Part\AbstractMultipartPart; +use Symfony\Component\Mime\Part\MessagePart; + +/** + * @author Fabien Potencier + */ +final class DigestPart extends AbstractMultipartPart +{ + public function __construct(MessagePart ...$parts) + { + parent::__construct(...$parts); + } + + public function getMediaSubtype(): string + { + return 'digest'; + } +} diff --git a/3rdparty/symfony/mime/Part/Multipart/FormDataPart.php b/3rdparty/symfony/mime/Part/Multipart/FormDataPart.php new file mode 100644 index 00000000..904c86d6 --- /dev/null +++ b/3rdparty/symfony/mime/Part/Multipart/FormDataPart.php @@ -0,0 +1,108 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Component\Mime\Part\Multipart; + +use Symfony\Component\Mime\Exception\InvalidArgumentException; +use Symfony\Component\Mime\Part\AbstractMultipartPart; +use Symfony\Component\Mime\Part\DataPart; +use Symfony\Component\Mime\Part\TextPart; + +/** + * Implements RFC 7578. + * + * @author Fabien Potencier + */ +final class FormDataPart extends AbstractMultipartPart +{ + private array $fields = []; + + /** + * @param array $fields + */ + public function __construct(array $fields = []) + { + parent::__construct(); + + $this->fields = $fields; + + // HTTP does not support \r\n in header values + $this->getHeaders()->setMaxLineLength(\PHP_INT_MAX); + } + + public function getMediaSubtype(): string + { + return 'form-data'; + } + + public function getParts(): array + { + return $this->prepareFields($this->fields); + } + + private function prepareFields(array $fields): array + { + $values = []; + + $prepare = function ($item, $key, $root = null) use (&$values, &$prepare) { + if (null === $root && \is_int($key) && \is_array($item)) { + if (1 !== \count($item)) { + throw new InvalidArgumentException(sprintf('Form field values with integer keys can only have one array element, the key being the field name and the value being the field value, %d provided.', \count($item))); + } + + $key = key($item); + $item = $item[$key]; + } + + $fieldName = null !== $root ? sprintf('%s[%s]', $root, $key) : $key; + + if (\is_array($item)) { + array_walk($item, $prepare, $fieldName); + + return; + } + + if (!\is_string($item) && !$item instanceof TextPart) { + throw new InvalidArgumentException(sprintf('The value of the form field "%s" can only be a string, an array, or an instance of TextPart, "%s" given.', $fieldName, get_debug_type($item))); + } + + $values[] = $this->preparePart($fieldName, $item); + }; + + array_walk($fields, $prepare); + + return $values; + } + + private function preparePart(string $name, string|TextPart $value): TextPart + { + if (\is_string($value)) { + return $this->configurePart($name, new TextPart($value, 'utf-8', 'plain', '8bit')); + } + + return $this->configurePart($name, $value); + } + + private function configurePart(string $name, TextPart $part): TextPart + { + static $r; + + $r ??= new \ReflectionProperty(TextPart::class, 'encoding'); + + $part->setDisposition('form-data'); + $part->setName($name); + // HTTP does not support \r\n in header values + $part->getHeaders()->setMaxLineLength(\PHP_INT_MAX); + $r->setValue($part, '8bit'); + + return $part; + } +} diff --git a/3rdparty/symfony/mime/Part/Multipart/MixedPart.php b/3rdparty/symfony/mime/Part/Multipart/MixedPart.php new file mode 100644 index 00000000..c8d7028c --- /dev/null +++ b/3rdparty/symfony/mime/Part/Multipart/MixedPart.php @@ -0,0 +1,25 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Component\Mime\Part\Multipart; + +use Symfony\Component\Mime\Part\AbstractMultipartPart; + +/** + * @author Fabien Potencier + */ +final class MixedPart extends AbstractMultipartPart +{ + public function getMediaSubtype(): string + { + return 'mixed'; + } +} diff --git a/3rdparty/symfony/mime/Part/Multipart/RelatedPart.php b/3rdparty/symfony/mime/Part/Multipart/RelatedPart.php new file mode 100644 index 00000000..a0d6a1c2 --- /dev/null +++ b/3rdparty/symfony/mime/Part/Multipart/RelatedPart.php @@ -0,0 +1,55 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Component\Mime\Part\Multipart; + +use Symfony\Component\Mime\Part\AbstractMultipartPart; +use Symfony\Component\Mime\Part\AbstractPart; + +/** + * @author Fabien Potencier + */ +final class RelatedPart extends AbstractMultipartPart +{ + private AbstractPart $mainPart; + + public function __construct(AbstractPart $mainPart, AbstractPart $part, AbstractPart ...$parts) + { + $this->mainPart = $mainPart; + $this->prepareParts($part, ...$parts); + + parent::__construct($part, ...$parts); + } + + public function getParts(): array + { + return array_merge([$this->mainPart], parent::getParts()); + } + + public function getMediaSubtype(): string + { + return 'related'; + } + + private function generateContentId(): string + { + return bin2hex(random_bytes(16)).'@symfony'; + } + + private function prepareParts(AbstractPart ...$parts): void + { + foreach ($parts as $part) { + if (!$part->getHeaders()->has('Content-ID')) { + $part->getHeaders()->setHeaderBody('Id', 'Content-ID', $this->generateContentId()); + } + } + } +} diff --git a/3rdparty/symfony/mime/Part/SMimePart.php b/3rdparty/symfony/mime/Part/SMimePart.php new file mode 100644 index 00000000..57b9766b --- /dev/null +++ b/3rdparty/symfony/mime/Part/SMimePart.php @@ -0,0 +1,111 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Component\Mime\Part; + +use Symfony\Component\Mime\Header\Headers; + +/** + * @author Sebastiaan Stok + */ +class SMimePart extends AbstractPart +{ + /** @internal */ + protected Headers $_headers; + + private iterable|string $body; + private string $type; + private string $subtype; + private array $parameters; + + public function __construct(iterable|string $body, string $type, string $subtype, array $parameters) + { + parent::__construct(); + + $this->body = $body; + $this->type = $type; + $this->subtype = $subtype; + $this->parameters = $parameters; + } + + public function getMediaType(): string + { + return $this->type; + } + + public function getMediaSubtype(): string + { + return $this->subtype; + } + + public function bodyToString(): string + { + if (\is_string($this->body)) { + return $this->body; + } + + $body = ''; + foreach ($this->body as $chunk) { + $body .= $chunk; + } + $this->body = $body; + + return $body; + } + + public function bodyToIterable(): iterable + { + if (\is_string($this->body)) { + yield $this->body; + + return; + } + + $body = ''; + foreach ($this->body as $chunk) { + $body .= $chunk; + yield $chunk; + } + $this->body = $body; + } + + public function getPreparedHeaders(): Headers + { + $headers = clone parent::getHeaders(); + + $headers->setHeaderBody('Parameterized', 'Content-Type', $this->getMediaType().'/'.$this->getMediaSubtype()); + + foreach ($this->parameters as $name => $value) { + $headers->setHeaderParameter('Content-Type', $name, $value); + } + + return $headers; + } + + public function __sleep(): array + { + // convert iterables to strings for serialization + if (is_iterable($this->body)) { + $this->body = $this->bodyToString(); + } + + $this->_headers = $this->getHeaders(); + + return ['_headers', 'body', 'type', 'subtype', 'parameters']; + } + + public function __wakeup(): void + { + $r = new \ReflectionProperty(AbstractPart::class, 'headers'); + $r->setValue($this, $this->_headers); + unset($this->_headers); + } +} diff --git a/3rdparty/symfony/mime/Part/TextPart.php b/3rdparty/symfony/mime/Part/TextPart.php new file mode 100644 index 00000000..2a8dd585 --- /dev/null +++ b/3rdparty/symfony/mime/Part/TextPart.php @@ -0,0 +1,248 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Component\Mime\Part; + +use Symfony\Component\Mime\Encoder\Base64ContentEncoder; +use Symfony\Component\Mime\Encoder\ContentEncoderInterface; +use Symfony\Component\Mime\Encoder\EightBitContentEncoder; +use Symfony\Component\Mime\Encoder\QpContentEncoder; +use Symfony\Component\Mime\Exception\InvalidArgumentException; +use Symfony\Component\Mime\Header\Headers; + +/** + * @author Fabien Potencier + */ +class TextPart extends AbstractPart +{ + /** @internal */ + protected Headers $_headers; + + private static array $encoders = []; + + /** @var resource|string|File */ + private $body; + private ?string $charset; + private string $subtype; + private ?string $disposition = null; + private ?string $name = null; + private string $encoding; + private ?bool $seekable = null; + + /** + * @param resource|string|File $body Use a File instance to defer loading the file until rendering + */ + public function __construct($body, ?string $charset = 'utf-8', string $subtype = 'plain', ?string $encoding = null) + { + parent::__construct(); + + if (!\is_string($body) && !\is_resource($body) && !$body instanceof File) { + throw new \TypeError(sprintf('The body of "%s" must be a string, a resource, or an instance of "%s" (got "%s").', self::class, File::class, get_debug_type($body))); + } + + if ($body instanceof File) { + $path = $body->getPath(); + if ((is_file($path) && !is_readable($path)) || is_dir($path)) { + throw new InvalidArgumentException(sprintf('Path "%s" is not readable.', $path)); + } + } + + $this->body = $body; + $this->charset = $charset; + $this->subtype = $subtype; + $this->seekable = \is_resource($body) ? stream_get_meta_data($body)['seekable'] && 0 === fseek($body, 0, \SEEK_CUR) : null; + + if (null === $encoding) { + $this->encoding = $this->chooseEncoding(); + } else { + if ('quoted-printable' !== $encoding && 'base64' !== $encoding && '8bit' !== $encoding) { + throw new InvalidArgumentException(sprintf('The encoding must be one of "quoted-printable", "base64", or "8bit" ("%s" given).', $encoding)); + } + $this->encoding = $encoding; + } + } + + public function getMediaType(): string + { + return 'text'; + } + + public function getMediaSubtype(): string + { + return $this->subtype; + } + + /** + * @param string $disposition one of attachment, inline, or form-data + * + * @return $this + */ + public function setDisposition(string $disposition): static + { + $this->disposition = $disposition; + + return $this; + } + + /** + * @return ?string null or one of attachment, inline, or form-data + */ + public function getDisposition(): ?string + { + return $this->disposition; + } + + /** + * Sets the name of the file (used by FormDataPart). + * + * @return $this + */ + public function setName(string $name): static + { + $this->name = $name; + + return $this; + } + + /** + * Gets the name of the file. + */ + public function getName(): ?string + { + return $this->name; + } + + public function getBody(): string + { + if ($this->body instanceof File) { + if (false === $ret = @file_get_contents($this->body->getPath())) { + throw new InvalidArgumentException(error_get_last()['message']); + } + + return $ret; + } + + if (null === $this->seekable) { + return $this->body; + } + + if ($this->seekable) { + rewind($this->body); + } + + return stream_get_contents($this->body) ?: ''; + } + + public function bodyToString(): string + { + return $this->getEncoder()->encodeString($this->getBody(), $this->charset); + } + + public function bodyToIterable(): iterable + { + if ($this->body instanceof File) { + $path = $this->body->getPath(); + if (false === $handle = @fopen($path, 'r', false)) { + throw new InvalidArgumentException(sprintf('Unable to open path "%s".', $path)); + } + + yield from $this->getEncoder()->encodeByteStream($handle); + } elseif (null !== $this->seekable) { + if ($this->seekable) { + rewind($this->body); + } + yield from $this->getEncoder()->encodeByteStream($this->body); + } else { + yield $this->getEncoder()->encodeString($this->body); + } + } + + public function getPreparedHeaders(): Headers + { + $headers = parent::getPreparedHeaders(); + + $headers->setHeaderBody('Parameterized', 'Content-Type', $this->getMediaType().'/'.$this->getMediaSubtype()); + if ($this->charset) { + $headers->setHeaderParameter('Content-Type', 'charset', $this->charset); + } + if ($this->name && 'form-data' !== $this->disposition) { + $headers->setHeaderParameter('Content-Type', 'name', $this->name); + } + $headers->setHeaderBody('Text', 'Content-Transfer-Encoding', $this->encoding); + + if (!$headers->has('Content-Disposition') && null !== $this->disposition) { + $headers->setHeaderBody('Parameterized', 'Content-Disposition', $this->disposition); + if ($this->name) { + $headers->setHeaderParameter('Content-Disposition', 'name', $this->name); + } + } + + return $headers; + } + + public function asDebugString(): string + { + $str = parent::asDebugString(); + if (null !== $this->charset) { + $str .= ' charset: '.$this->charset; + } + if (null !== $this->disposition) { + $str .= ' disposition: '.$this->disposition; + } + + return $str; + } + + private function getEncoder(): ContentEncoderInterface + { + if ('8bit' === $this->encoding) { + return self::$encoders[$this->encoding] ??= new EightBitContentEncoder(); + } + + if ('quoted-printable' === $this->encoding) { + return self::$encoders[$this->encoding] ??= new QpContentEncoder(); + } + + return self::$encoders[$this->encoding] ??= new Base64ContentEncoder(); + } + + private function chooseEncoding(): string + { + if (null === $this->charset) { + return 'base64'; + } + + return 'quoted-printable'; + } + + public function __sleep(): array + { + // convert resources to strings for serialization + if (null !== $this->seekable) { + $this->body = $this->getBody(); + $this->seekable = null; + } + + $this->_headers = $this->getHeaders(); + + return ['_headers', 'body', 'charset', 'subtype', 'disposition', 'name', 'encoding']; + } + + /** + * @return void + */ + public function __wakeup() + { + $r = new \ReflectionProperty(AbstractPart::class, 'headers'); + $r->setValue($this, $this->_headers); + unset($this->_headers); + } +} diff --git a/3rdparty/symfony/mime/RawMessage.php b/3rdparty/symfony/mime/RawMessage.php new file mode 100644 index 00000000..2b1b52cd --- /dev/null +++ b/3rdparty/symfony/mime/RawMessage.php @@ -0,0 +1,115 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Component\Mime; + +use Symfony\Component\Mime\Exception\LogicException; + +/** + * @author Fabien Potencier + */ +class RawMessage +{ + /** @var iterable|string|resource */ + private $message; + private bool $isGeneratorClosed; + + /** + * @param iterable|string|resource $message + */ + public function __construct(mixed $message) + { + $this->message = $message; + } + + public function __destruct() + { + if (\is_resource($this->message)) { + fclose($this->message); + } + } + + public function toString(): string + { + if (\is_string($this->message)) { + return $this->message; + } + + if (\is_resource($this->message)) { + return stream_get_contents($this->message, -1, 0); + } + + $message = ''; + foreach ($this->message as $chunk) { + $message .= $chunk; + } + + return $this->message = $message; + } + + public function toIterable(): iterable + { + if ($this->isGeneratorClosed ?? false) { + trigger_deprecation('symfony/mime', '6.4', 'Sending an email with a closed generator is deprecated and will throw in 7.0.'); + // throw new LogicException('Unable to send the email as its generator is already closed.'); + } + + if (\is_string($this->message)) { + yield $this->message; + + return; + } + + if (\is_resource($this->message)) { + rewind($this->message); + while ($line = fgets($this->message)) { + yield $line; + } + + return; + } + + if ($this->message instanceof \Generator) { + $message = fopen('php://temp', 'w+'); + foreach ($this->message as $chunk) { + fwrite($message, $chunk); + yield $chunk; + } + $this->isGeneratorClosed = !$this->message->valid(); + $this->message = $message; + + return; + } + + foreach ($this->message as $chunk) { + yield $chunk; + } + } + + /** + * @return void + * + * @throws LogicException if the message is not valid + */ + public function ensureValidity() + { + } + + public function __serialize(): array + { + return [$this->toString()]; + } + + public function __unserialize(array $data): void + { + [$this->message] = $data; + } +} diff --git a/3rdparty/symfony/polyfill-intl-grapheme/Grapheme.php b/3rdparty/symfony/polyfill-intl-grapheme/Grapheme.php new file mode 100644 index 00000000..5373f168 --- /dev/null +++ b/3rdparty/symfony/polyfill-intl-grapheme/Grapheme.php @@ -0,0 +1,247 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Polyfill\Intl\Grapheme; + +\define('SYMFONY_GRAPHEME_CLUSTER_RX', ((float) \PCRE_VERSION < 10 ? (float) \PCRE_VERSION >= 8.32 : (float) \PCRE_VERSION >= 10.39) ? '\X' : Grapheme::GRAPHEME_CLUSTER_RX); + +/** + * Partial intl implementation in pure PHP. + * + * Implemented: + * - grapheme_extract - Extract a sequence of grapheme clusters from a text buffer, which must be encoded in UTF-8 + * - grapheme_stripos - Find position (in grapheme units) of first occurrence of a case-insensitive string + * - grapheme_stristr - Returns part of haystack string from the first occurrence of case-insensitive needle to the end of haystack + * - grapheme_strlen - Get string length in grapheme units + * - grapheme_strpos - Find position (in grapheme units) of first occurrence of a string + * - grapheme_strripos - Find position (in grapheme units) of last occurrence of a case-insensitive string + * - grapheme_strrpos - Find position (in grapheme units) of last occurrence of a string + * - grapheme_strstr - Returns part of haystack string from the first occurrence of needle to the end of haystack + * - grapheme_substr - Return part of a string + * + * @author Nicolas Grekas + * + * @internal + */ +final class Grapheme +{ + // (CRLF|([ZWNJ-ZWJ]|T+|L*(LV?V+|LV|LVT)T*|L+|[^Control])[Extend]*|[Control]) + // This regular expression is a work around for http://bugs.exim.org/1279 + public const GRAPHEME_CLUSTER_RX = '(?:\r\n|(?:[ -~\x{200C}\x{200D}]|[ᆨ-ᇹ]+|[ᄀ-ᅟ]*(?:[가개갸걔거게겨계고과괘괴교구궈궤귀규그긔기까깨꺄꺠꺼께껴꼐꼬꽈꽤꾀꾜꾸꿔꿰뀌뀨끄끠끼나내냐냬너네녀녜노놔놰뇌뇨누눠눼뉘뉴느늬니다대댜댸더데뎌뎨도돠돼되됴두둬뒈뒤듀드듸디따때땨떄떠떼뗘뗴또똬뙈뙤뚀뚜뚸뛔뛰뜌뜨띄띠라래랴럐러레려례로롸뢔뢰료루뤄뤠뤼류르릐리마매먀먜머메며몌모뫄뫠뫼묘무뭐뭬뮈뮤므믜미바배뱌뱨버베벼볘보봐봬뵈뵤부붜붸뷔뷰브븨비빠빼뺘뺴뻐뻬뼈뼤뽀뽜뽸뾔뾰뿌뿨쀄쀠쀼쁘쁴삐사새샤섀서세셔셰소솨쇄쇠쇼수숴쉐쉬슈스싀시싸쌔쌰썌써쎄쎠쎼쏘쏴쐐쐬쑈쑤쒀쒜쒸쓔쓰씌씨아애야얘어에여예오와왜외요우워웨위유으의이자재쟈쟤저제져졔조좌좨죄죠주줘줴쥐쥬즈즤지짜째쨔쨰쩌쩨쪄쪠쪼쫘쫴쬐쬬쭈쭤쮀쮜쮸쯔쯰찌차채챠챼처체쳐쳬초촤쵀최쵸추춰췌취츄츠츼치카캐캬컈커케켜켸코콰쾌쾨쿄쿠쿼퀘퀴큐크킈키타태탸턔터테텨톄토톼퇘퇴툐투퉈퉤튀튜트틔티파패퍄퍠퍼페펴폐포퐈퐤푀표푸풔풰퓌퓨프픠피하해햐햬허헤혀혜호화홰회효후훠훼휘휴흐희히]?[ᅠ-ᆢ]+|[가-힣])[ᆨ-ᇹ]*|[ᄀ-ᅟ]+|[^\p{Cc}\p{Cf}\p{Zl}\p{Zp}])[\p{Mn}\p{Me}\x{09BE}\x{09D7}\x{0B3E}\x{0B57}\x{0BBE}\x{0BD7}\x{0CC2}\x{0CD5}\x{0CD6}\x{0D3E}\x{0D57}\x{0DCF}\x{0DDF}\x{200C}\x{200D}\x{1D165}\x{1D16E}-\x{1D172}]*|[\p{Cc}\p{Cf}\p{Zl}\p{Zp}])'; + + private const CASE_FOLD = [ + ['µ', 'ſ', "\xCD\x85", 'ς', "\xCF\x90", "\xCF\x91", "\xCF\x95", "\xCF\x96", "\xCF\xB0", "\xCF\xB1", "\xCF\xB5", "\xE1\xBA\x9B", "\xE1\xBE\xBE"], + ['μ', 's', 'ι', 'σ', 'β', 'θ', 'φ', 'π', 'κ', 'ρ', 'ε', "\xE1\xB9\xA1", 'ι'], + ]; + + public static function grapheme_extract($s, $size, $type = \GRAPHEME_EXTR_COUNT, $start = 0, &$next = 0) + { + if (0 > $start) { + $start = \strlen($s) + $start; + } + + if (!\is_scalar($s)) { + $hasError = false; + set_error_handler(function () use (&$hasError) { $hasError = true; }); + $next = substr($s, $start); + restore_error_handler(); + if ($hasError) { + substr($s, $start); + $s = ''; + } else { + $s = $next; + } + } else { + $s = substr($s, $start); + } + $size = (int) $size; + $type = (int) $type; + $start = (int) $start; + + if (\GRAPHEME_EXTR_COUNT !== $type && \GRAPHEME_EXTR_MAXBYTES !== $type && \GRAPHEME_EXTR_MAXCHARS !== $type) { + if (80000 > \PHP_VERSION_ID) { + return false; + } + + throw new \ValueError('grapheme_extract(): Argument #3 ($type) must be one of GRAPHEME_EXTR_COUNT, GRAPHEME_EXTR_MAXBYTES, or GRAPHEME_EXTR_MAXCHARS'); + } + + if (!isset($s[0]) || 0 > $size || 0 > $start) { + return false; + } + if (0 === $size) { + return ''; + } + + $next = $start; + + $s = preg_split('/('.SYMFONY_GRAPHEME_CLUSTER_RX.')/u', "\r\n".$s, $size + 1, \PREG_SPLIT_NO_EMPTY | \PREG_SPLIT_DELIM_CAPTURE); + + if (!isset($s[1])) { + return false; + } + + $i = 1; + $ret = ''; + + do { + if (\GRAPHEME_EXTR_COUNT === $type) { + --$size; + } elseif (\GRAPHEME_EXTR_MAXBYTES === $type) { + $size -= \strlen($s[$i]); + } else { + $size -= iconv_strlen($s[$i], 'UTF-8//IGNORE'); + } + + if ($size >= 0) { + $ret .= $s[$i]; + } + } while (isset($s[++$i]) && $size > 0); + + $next += \strlen($ret); + + return $ret; + } + + public static function grapheme_strlen($s) + { + preg_replace('/'.SYMFONY_GRAPHEME_CLUSTER_RX.'/u', '', $s, -1, $len); + + return 0 === $len && '' !== $s ? null : $len; + } + + public static function grapheme_substr($s, $start, $len = null) + { + if (null === $len) { + $len = 2147483647; + } + + preg_match_all('/'.SYMFONY_GRAPHEME_CLUSTER_RX.'/u', $s, $s); + + $slen = \count($s[0]); + $start = (int) $start; + + if (0 > $start) { + $start += $slen; + } + if (0 > $start) { + if (\PHP_VERSION_ID < 80000) { + return false; + } + + $start = 0; + } + if ($start >= $slen) { + return \PHP_VERSION_ID >= 80000 ? '' : false; + } + + $rem = $slen - $start; + + if (0 > $len) { + $len += $rem; + } + if (0 === $len) { + return ''; + } + if (0 > $len) { + return \PHP_VERSION_ID >= 80000 ? '' : false; + } + if ($len > $rem) { + $len = $rem; + } + + return implode('', \array_slice($s[0], $start, $len)); + } + + public static function grapheme_strpos($s, $needle, $offset = 0) + { + return self::grapheme_position($s, $needle, $offset, 0); + } + + public static function grapheme_stripos($s, $needle, $offset = 0) + { + return self::grapheme_position($s, $needle, $offset, 1); + } + + public static function grapheme_strrpos($s, $needle, $offset = 0) + { + return self::grapheme_position($s, $needle, $offset, 2); + } + + public static function grapheme_strripos($s, $needle, $offset = 0) + { + return self::grapheme_position($s, $needle, $offset, 3); + } + + public static function grapheme_stristr($s, $needle, $beforeNeedle = false) + { + return mb_stristr($s, $needle, $beforeNeedle, 'UTF-8'); + } + + public static function grapheme_strstr($s, $needle, $beforeNeedle = false) + { + return mb_strstr($s, $needle, $beforeNeedle, 'UTF-8'); + } + + private static function grapheme_position($s, $needle, $offset, $mode) + { + $needle = (string) $needle; + if (80000 > \PHP_VERSION_ID && !preg_match('/./us', $needle)) { + return false; + } + $s = (string) $s; + if (!preg_match('/./us', $s)) { + return false; + } + if ($offset > 0) { + $s = self::grapheme_substr($s, $offset); + } elseif ($offset < 0) { + if (2 > $mode) { + $offset += self::grapheme_strlen($s); + $s = self::grapheme_substr($s, $offset); + if (0 > $offset) { + $offset = 0; + } + } elseif (0 > $offset += self::grapheme_strlen($needle)) { + $s = self::grapheme_substr($s, 0, $offset); + $offset = 0; + } else { + $offset = 0; + } + } + + // As UTF-8 is self-synchronizing, and we have ensured the strings are valid UTF-8, + // we can use normal binary string functions here. For case-insensitive searches, + // case fold the strings first. + $caseInsensitive = $mode & 1; + $reverse = $mode & 2; + if ($caseInsensitive) { + // Use the same case folding mode as mbstring does for mb_stripos(). + // Stick to SIMPLE case folding to avoid changing the length of the string, which + // might result in offsets being shifted. + $mode = \defined('MB_CASE_FOLD_SIMPLE') ? \MB_CASE_FOLD_SIMPLE : \MB_CASE_LOWER; + $s = mb_convert_case($s, $mode, 'UTF-8'); + $needle = mb_convert_case($needle, $mode, 'UTF-8'); + + if (!\defined('MB_CASE_FOLD_SIMPLE')) { + $s = str_replace(self::CASE_FOLD[0], self::CASE_FOLD[1], $s); + $needle = str_replace(self::CASE_FOLD[0], self::CASE_FOLD[1], $needle); + } + } + if ($reverse) { + $needlePos = strrpos($s, $needle); + } else { + $needlePos = strpos($s, $needle); + } + + return false !== $needlePos ? self::grapheme_strlen(substr($s, 0, $needlePos)) + $offset : false; + } +} diff --git a/3rdparty/symfony/polyfill-intl-grapheme/LICENSE b/3rdparty/symfony/polyfill-intl-grapheme/LICENSE new file mode 100644 index 00000000..6e3afce6 --- /dev/null +++ b/3rdparty/symfony/polyfill-intl-grapheme/LICENSE @@ -0,0 +1,19 @@ +Copyright (c) 2015-present Fabien Potencier + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is furnished +to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +THE SOFTWARE. diff --git a/3rdparty/symfony/polyfill-intl-grapheme/bootstrap.php b/3rdparty/symfony/polyfill-intl-grapheme/bootstrap.php new file mode 100644 index 00000000..a9ea03c7 --- /dev/null +++ b/3rdparty/symfony/polyfill-intl-grapheme/bootstrap.php @@ -0,0 +1,58 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +use Symfony\Polyfill\Intl\Grapheme as p; + +if (extension_loaded('intl')) { + return; +} + +if (\PHP_VERSION_ID >= 80000) { + return require __DIR__.'/bootstrap80.php'; +} + +if (!defined('GRAPHEME_EXTR_COUNT')) { + define('GRAPHEME_EXTR_COUNT', 0); +} +if (!defined('GRAPHEME_EXTR_MAXBYTES')) { + define('GRAPHEME_EXTR_MAXBYTES', 1); +} +if (!defined('GRAPHEME_EXTR_MAXCHARS')) { + define('GRAPHEME_EXTR_MAXCHARS', 2); +} + +if (!function_exists('grapheme_extract')) { + function grapheme_extract($haystack, $size, $type = 0, $start = 0, &$next = 0) { return p\Grapheme::grapheme_extract($haystack, $size, $type, $start, $next); } +} +if (!function_exists('grapheme_stripos')) { + function grapheme_stripos($haystack, $needle, $offset = 0) { return p\Grapheme::grapheme_stripos($haystack, $needle, $offset); } +} +if (!function_exists('grapheme_stristr')) { + function grapheme_stristr($haystack, $needle, $beforeNeedle = false) { return p\Grapheme::grapheme_stristr($haystack, $needle, $beforeNeedle); } +} +if (!function_exists('grapheme_strlen')) { + function grapheme_strlen($input) { return p\Grapheme::grapheme_strlen($input); } +} +if (!function_exists('grapheme_strpos')) { + function grapheme_strpos($haystack, $needle, $offset = 0) { return p\Grapheme::grapheme_strpos($haystack, $needle, $offset); } +} +if (!function_exists('grapheme_strripos')) { + function grapheme_strripos($haystack, $needle, $offset = 0) { return p\Grapheme::grapheme_strripos($haystack, $needle, $offset); } +} +if (!function_exists('grapheme_strrpos')) { + function grapheme_strrpos($haystack, $needle, $offset = 0) { return p\Grapheme::grapheme_strrpos($haystack, $needle, $offset); } +} +if (!function_exists('grapheme_strstr')) { + function grapheme_strstr($haystack, $needle, $beforeNeedle = false) { return p\Grapheme::grapheme_strstr($haystack, $needle, $beforeNeedle); } +} +if (!function_exists('grapheme_substr')) { + function grapheme_substr($string, $offset, $length = null) { return p\Grapheme::grapheme_substr($string, $offset, $length); } +} diff --git a/3rdparty/symfony/polyfill-intl-grapheme/bootstrap80.php b/3rdparty/symfony/polyfill-intl-grapheme/bootstrap80.php new file mode 100644 index 00000000..b8c07867 --- /dev/null +++ b/3rdparty/symfony/polyfill-intl-grapheme/bootstrap80.php @@ -0,0 +1,50 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +use Symfony\Polyfill\Intl\Grapheme as p; + +if (!defined('GRAPHEME_EXTR_COUNT')) { + define('GRAPHEME_EXTR_COUNT', 0); +} +if (!defined('GRAPHEME_EXTR_MAXBYTES')) { + define('GRAPHEME_EXTR_MAXBYTES', 1); +} +if (!defined('GRAPHEME_EXTR_MAXCHARS')) { + define('GRAPHEME_EXTR_MAXCHARS', 2); +} + +if (!function_exists('grapheme_extract')) { + function grapheme_extract(?string $haystack, ?int $size, ?int $type = GRAPHEME_EXTR_COUNT, ?int $offset = 0, &$next = null): string|false { return p\Grapheme::grapheme_extract((string) $haystack, (int) $size, (int) $type, (int) $offset, $next); } +} +if (!function_exists('grapheme_stripos')) { + function grapheme_stripos(?string $haystack, ?string $needle, ?int $offset = 0): int|false { return p\Grapheme::grapheme_stripos((string) $haystack, (string) $needle, (int) $offset); } +} +if (!function_exists('grapheme_stristr')) { + function grapheme_stristr(?string $haystack, ?string $needle, ?bool $beforeNeedle = false): string|false { return p\Grapheme::grapheme_stristr((string) $haystack, (string) $needle, (bool) $beforeNeedle); } +} +if (!function_exists('grapheme_strlen')) { + function grapheme_strlen(?string $string): int|false|null { return p\Grapheme::grapheme_strlen((string) $string); } +} +if (!function_exists('grapheme_strpos')) { + function grapheme_strpos(?string $haystack, ?string $needle, ?int $offset = 0): int|false { return p\Grapheme::grapheme_strpos((string) $haystack, (string) $needle, (int) $offset); } +} +if (!function_exists('grapheme_strripos')) { + function grapheme_strripos(?string $haystack, ?string $needle, ?int $offset = 0): int|false { return p\Grapheme::grapheme_strripos((string) $haystack, (string) $needle, (int) $offset); } +} +if (!function_exists('grapheme_strrpos')) { + function grapheme_strrpos(?string $haystack, ?string $needle, ?int $offset = 0): int|false { return p\Grapheme::grapheme_strrpos((string) $haystack, (string) $needle, (int) $offset); } +} +if (!function_exists('grapheme_strstr')) { + function grapheme_strstr(?string $haystack, ?string $needle, ?bool $beforeNeedle = false): string|false { return p\Grapheme::grapheme_strstr((string) $haystack, (string) $needle, (bool) $beforeNeedle); } +} +if (!function_exists('grapheme_substr')) { + function grapheme_substr(?string $string, ?int $offset, ?int $length = null): string|false { return p\Grapheme::grapheme_substr((string) $string, (int) $offset, $length); } +} diff --git a/3rdparty/symfony/polyfill-intl-idn/Idn.php b/3rdparty/symfony/polyfill-intl-idn/Idn.php new file mode 100644 index 00000000..448f74ce --- /dev/null +++ b/3rdparty/symfony/polyfill-intl-idn/Idn.php @@ -0,0 +1,941 @@ + and Trevor Rowbotham + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Polyfill\Intl\Idn; + +use Symfony\Polyfill\Intl\Idn\Resources\unidata\DisallowedRanges; +use Symfony\Polyfill\Intl\Idn\Resources\unidata\Regex; + +/** + * @see https://www.unicode.org/reports/tr46/ + * + * @internal + */ +final class Idn +{ + public const ERROR_EMPTY_LABEL = 1; + public const ERROR_LABEL_TOO_LONG = 2; + public const ERROR_DOMAIN_NAME_TOO_LONG = 4; + public const ERROR_LEADING_HYPHEN = 8; + public const ERROR_TRAILING_HYPHEN = 0x10; + public const ERROR_HYPHEN_3_4 = 0x20; + public const ERROR_LEADING_COMBINING_MARK = 0x40; + public const ERROR_DISALLOWED = 0x80; + public const ERROR_PUNYCODE = 0x100; + public const ERROR_LABEL_HAS_DOT = 0x200; + public const ERROR_INVALID_ACE_LABEL = 0x400; + public const ERROR_BIDI = 0x800; + public const ERROR_CONTEXTJ = 0x1000; + public const ERROR_CONTEXTO_PUNCTUATION = 0x2000; + public const ERROR_CONTEXTO_DIGITS = 0x4000; + + public const INTL_IDNA_VARIANT_2003 = 0; + public const INTL_IDNA_VARIANT_UTS46 = 1; + + public const IDNA_DEFAULT = 0; + public const IDNA_ALLOW_UNASSIGNED = 1; + public const IDNA_USE_STD3_RULES = 2; + public const IDNA_CHECK_BIDI = 4; + public const IDNA_CHECK_CONTEXTJ = 8; + public const IDNA_NONTRANSITIONAL_TO_ASCII = 16; + public const IDNA_NONTRANSITIONAL_TO_UNICODE = 32; + + public const MAX_DOMAIN_SIZE = 253; + public const MAX_LABEL_SIZE = 63; + + public const BASE = 36; + public const TMIN = 1; + public const TMAX = 26; + public const SKEW = 38; + public const DAMP = 700; + public const INITIAL_BIAS = 72; + public const INITIAL_N = 128; + public const DELIMITER = '-'; + public const MAX_INT = 2147483647; + + /** + * Contains the numeric value of a basic code point (for use in representing integers) in the + * range 0 to BASE-1, or -1 if b is does not represent a value. + * + * @var array + */ + private static $basicToDigit = [ + -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, + -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, + + -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, + 26, 27, 28, 29, 30, 31, 32, 33, 34, 35, -1, -1, -1, -1, -1, -1, + + -1, 0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, + 15, 16, 17, 18, 19, 20, 21, 22, 23, 24, 25, -1, -1, -1, -1, -1, + + -1, 0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, + 15, 16, 17, 18, 19, 20, 21, 22, 23, 24, 25, -1, -1, -1, -1, -1, + + -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, + -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, + + -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, + -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, + + -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, + -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, + + -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, + -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, + ]; + + /** + * @var array + */ + private static $virama; + + /** + * @var array + */ + private static $mapped; + + /** + * @var array + */ + private static $ignored; + + /** + * @var array + */ + private static $deviation; + + /** + * @var array + */ + private static $disallowed; + + /** + * @var array + */ + private static $disallowed_STD3_mapped; + + /** + * @var array + */ + private static $disallowed_STD3_valid; + + /** + * @var bool + */ + private static $mappingTableLoaded = false; + + /** + * @see https://www.unicode.org/reports/tr46/#ToASCII + * + * @param string $domainName + * @param int $options + * @param int $variant + * @param array $idna_info + * + * @return string|false + */ + public static function idn_to_ascii($domainName, $options = self::IDNA_DEFAULT, $variant = self::INTL_IDNA_VARIANT_UTS46, &$idna_info = []) + { + if (\PHP_VERSION_ID > 80400 && '' === $domainName) { + throw new \ValueError('idn_to_ascii(): Argument #1 ($domain) cannot be empty'); + } + + if (self::INTL_IDNA_VARIANT_2003 === $variant) { + @trigger_error('idn_to_ascii(): INTL_IDNA_VARIANT_2003 is deprecated', \E_USER_DEPRECATED); + } + + $options = [ + 'CheckHyphens' => true, + 'CheckBidi' => self::INTL_IDNA_VARIANT_2003 === $variant || 0 !== ($options & self::IDNA_CHECK_BIDI), + 'CheckJoiners' => self::INTL_IDNA_VARIANT_UTS46 === $variant && 0 !== ($options & self::IDNA_CHECK_CONTEXTJ), + 'UseSTD3ASCIIRules' => 0 !== ($options & self::IDNA_USE_STD3_RULES), + 'Transitional_Processing' => self::INTL_IDNA_VARIANT_2003 === $variant || 0 === ($options & self::IDNA_NONTRANSITIONAL_TO_ASCII), + 'VerifyDnsLength' => true, + ]; + $info = new Info(); + $labels = self::process((string) $domainName, $options, $info); + + foreach ($labels as $i => $label) { + // Only convert labels to punycode that contain non-ASCII code points + if (1 === preg_match('/[^\x00-\x7F]/', $label)) { + try { + $label = 'xn--'.self::punycodeEncode($label); + } catch (\Exception $e) { + $info->errors |= self::ERROR_PUNYCODE; + } + + $labels[$i] = $label; + } + } + + if ($options['VerifyDnsLength']) { + self::validateDomainAndLabelLength($labels, $info); + } + + $idna_info = [ + 'result' => implode('.', $labels), + 'isTransitionalDifferent' => $info->transitionalDifferent, + 'errors' => $info->errors, + ]; + + return 0 === $info->errors ? $idna_info['result'] : false; + } + + /** + * @see https://www.unicode.org/reports/tr46/#ToUnicode + * + * @param string $domainName + * @param int $options + * @param int $variant + * @param array $idna_info + * + * @return string|false + */ + public static function idn_to_utf8($domainName, $options = self::IDNA_DEFAULT, $variant = self::INTL_IDNA_VARIANT_UTS46, &$idna_info = []) + { + if (\PHP_VERSION_ID > 80400 && '' === $domainName) { + throw new \ValueError('idn_to_utf8(): Argument #1 ($domain) cannot be empty'); + } + + if (self::INTL_IDNA_VARIANT_2003 === $variant) { + @trigger_error('idn_to_utf8(): INTL_IDNA_VARIANT_2003 is deprecated', \E_USER_DEPRECATED); + } + + $info = new Info(); + $labels = self::process((string) $domainName, [ + 'CheckHyphens' => true, + 'CheckBidi' => self::INTL_IDNA_VARIANT_2003 === $variant || 0 !== ($options & self::IDNA_CHECK_BIDI), + 'CheckJoiners' => self::INTL_IDNA_VARIANT_UTS46 === $variant && 0 !== ($options & self::IDNA_CHECK_CONTEXTJ), + 'UseSTD3ASCIIRules' => 0 !== ($options & self::IDNA_USE_STD3_RULES), + 'Transitional_Processing' => self::INTL_IDNA_VARIANT_2003 === $variant || 0 === ($options & self::IDNA_NONTRANSITIONAL_TO_UNICODE), + ], $info); + $idna_info = [ + 'result' => implode('.', $labels), + 'isTransitionalDifferent' => $info->transitionalDifferent, + 'errors' => $info->errors, + ]; + + return 0 === $info->errors ? $idna_info['result'] : false; + } + + /** + * @param string $label + * + * @return bool + */ + private static function isValidContextJ(array $codePoints, $label) + { + if (!isset(self::$virama)) { + self::$virama = require __DIR__.\DIRECTORY_SEPARATOR.'Resources'.\DIRECTORY_SEPARATOR.'unidata'.\DIRECTORY_SEPARATOR.'virama.php'; + } + + $offset = 0; + + foreach ($codePoints as $i => $codePoint) { + if (0x200C !== $codePoint && 0x200D !== $codePoint) { + continue; + } + + if (!isset($codePoints[$i - 1])) { + return false; + } + + // If Canonical_Combining_Class(Before(cp)) .eq. Virama Then True; + if (isset(self::$virama[$codePoints[$i - 1]])) { + continue; + } + + // If RegExpMatch((Joining_Type:{L,D})(Joining_Type:T)*\u200C(Joining_Type:T)*(Joining_Type:{R,D})) Then + // True; + // Generated RegExp = ([Joining_Type:{L,D}][Joining_Type:T]*\u200C[Joining_Type:T]*)[Joining_Type:{R,D}] + if (0x200C === $codePoint && 1 === preg_match(Regex::ZWNJ, $label, $matches, \PREG_OFFSET_CAPTURE, $offset)) { + $offset += \strlen($matches[1][0]); + + continue; + } + + return false; + } + + return true; + } + + /** + * @see https://www.unicode.org/reports/tr46/#ProcessingStepMap + * + * @param string $input + * @param array $options + * + * @return string + */ + private static function mapCodePoints($input, array $options, Info $info) + { + $str = ''; + $useSTD3ASCIIRules = $options['UseSTD3ASCIIRules']; + $transitional = $options['Transitional_Processing']; + + foreach (self::utf8Decode($input) as $codePoint) { + $data = self::lookupCodePointStatus($codePoint, $useSTD3ASCIIRules); + + switch ($data['status']) { + case 'disallowed': + case 'valid': + $str .= mb_chr($codePoint, 'utf-8'); + + break; + + case 'ignored': + // Do nothing. + break; + + case 'mapped': + $str .= $transitional && 0x1E9E === $codePoint ? 'ss' : $data['mapping']; + + break; + + case 'deviation': + $info->transitionalDifferent = true; + $str .= ($transitional ? $data['mapping'] : mb_chr($codePoint, 'utf-8')); + + break; + } + } + + return $str; + } + + /** + * @see https://www.unicode.org/reports/tr46/#Processing + * + * @param string $domain + * @param array $options + * + * @return array + */ + private static function process($domain, array $options, Info $info) + { + // If VerifyDnsLength is not set, we are doing ToUnicode otherwise we are doing ToASCII and + // we need to respect the VerifyDnsLength option. + $checkForEmptyLabels = !isset($options['VerifyDnsLength']) || $options['VerifyDnsLength']; + + if ($checkForEmptyLabels && '' === $domain) { + $info->errors |= self::ERROR_EMPTY_LABEL; + + return [$domain]; + } + + // Step 1. Map each code point in the domain name string + $domain = self::mapCodePoints($domain, $options, $info); + + // Step 2. Normalize the domain name string to Unicode Normalization Form C. + if (!\Normalizer::isNormalized($domain, \Normalizer::FORM_C)) { + $domain = \Normalizer::normalize($domain, \Normalizer::FORM_C); + } + + // Step 3. Break the string into labels at U+002E (.) FULL STOP. + $labels = explode('.', $domain); + $lastLabelIndex = \count($labels) - 1; + + // Step 4. Convert and validate each label in the domain name string. + foreach ($labels as $i => $label) { + $validationOptions = $options; + + if ('xn--' === substr($label, 0, 4)) { + // Step 4.1. If the label contains any non-ASCII code point (i.e., a code point greater than U+007F), + // record that there was an error, and continue with the next label. + if (preg_match('/[^\x00-\x7F]/', $label)) { + $info->errors |= self::ERROR_PUNYCODE; + + continue; + } + + // Step 4.2. Attempt to convert the rest of the label to Unicode according to Punycode [RFC3492]. If + // that conversion fails, record that there was an error, and continue + // with the next label. Otherwise replace the original label in the string by the results of the + // conversion. + try { + $label = self::punycodeDecode(substr($label, 4)); + } catch (\Exception $e) { + $info->errors |= self::ERROR_PUNYCODE; + + continue; + } + + $validationOptions['Transitional_Processing'] = false; + $labels[$i] = $label; + } + + self::validateLabel($label, $info, $validationOptions, $i > 0 && $i === $lastLabelIndex); + } + + if ($info->bidiDomain && !$info->validBidiDomain) { + $info->errors |= self::ERROR_BIDI; + } + + // Any input domain name string that does not record an error has been successfully + // processed according to this specification. Conversely, if an input domain_name string + // causes an error, then the processing of the input domain_name string fails. Determining + // what to do with error input is up to the caller, and not in the scope of this document. + return $labels; + } + + /** + * @see https://tools.ietf.org/html/rfc5893#section-2 + * + * @param string $label + */ + private static function validateBidiLabel($label, Info $info) + { + if (1 === preg_match(Regex::RTL_LABEL, $label)) { + $info->bidiDomain = true; + + // Step 1. The first character must be a character with Bidi property L, R, or AL. + // If it has the R or AL property, it is an RTL label + if (1 !== preg_match(Regex::BIDI_STEP_1_RTL, $label)) { + $info->validBidiDomain = false; + + return; + } + + // Step 2. In an RTL label, only characters with the Bidi properties R, AL, AN, EN, ES, + // CS, ET, ON, BN, or NSM are allowed. + if (1 === preg_match(Regex::BIDI_STEP_2, $label)) { + $info->validBidiDomain = false; + + return; + } + + // Step 3. In an RTL label, the end of the label must be a character with Bidi property + // R, AL, EN, or AN, followed by zero or more characters with Bidi property NSM. + if (1 !== preg_match(Regex::BIDI_STEP_3, $label)) { + $info->validBidiDomain = false; + + return; + } + + // Step 4. In an RTL label, if an EN is present, no AN may be present, and vice versa. + if (1 === preg_match(Regex::BIDI_STEP_4_AN, $label) && 1 === preg_match(Regex::BIDI_STEP_4_EN, $label)) { + $info->validBidiDomain = false; + + return; + } + + return; + } + + // We are a LTR label + // Step 1. The first character must be a character with Bidi property L, R, or AL. + // If it has the L property, it is an LTR label. + if (1 !== preg_match(Regex::BIDI_STEP_1_LTR, $label)) { + $info->validBidiDomain = false; + + return; + } + + // Step 5. In an LTR label, only characters with the Bidi properties L, EN, + // ES, CS, ET, ON, BN, or NSM are allowed. + if (1 === preg_match(Regex::BIDI_STEP_5, $label)) { + $info->validBidiDomain = false; + + return; + } + + // Step 6.In an LTR label, the end of the label must be a character with Bidi property L or + // EN, followed by zero or more characters with Bidi property NSM. + if (1 !== preg_match(Regex::BIDI_STEP_6, $label)) { + $info->validBidiDomain = false; + + return; + } + } + + /** + * @param array $labels + */ + private static function validateDomainAndLabelLength(array $labels, Info $info) + { + $maxDomainSize = self::MAX_DOMAIN_SIZE; + $length = \count($labels); + + // Number of "." delimiters. + $domainLength = $length - 1; + + // If the last label is empty and it is not the first label, then it is the root label. + // Increase the max size by 1, making it 254, to account for the root label's "." + // delimiter. This also means we don't need to check the last label's length for being too + // long. + if ($length > 1 && '' === $labels[$length - 1]) { + ++$maxDomainSize; + --$length; + } + + for ($i = 0; $i < $length; ++$i) { + $bytes = \strlen($labels[$i]); + $domainLength += $bytes; + + if ($bytes > self::MAX_LABEL_SIZE) { + $info->errors |= self::ERROR_LABEL_TOO_LONG; + } + } + + if ($domainLength > $maxDomainSize) { + $info->errors |= self::ERROR_DOMAIN_NAME_TOO_LONG; + } + } + + /** + * @see https://www.unicode.org/reports/tr46/#Validity_Criteria + * + * @param string $label + * @param array $options + * @param bool $canBeEmpty + */ + private static function validateLabel($label, Info $info, array $options, $canBeEmpty) + { + if ('' === $label) { + if (!$canBeEmpty && (!isset($options['VerifyDnsLength']) || $options['VerifyDnsLength'])) { + $info->errors |= self::ERROR_EMPTY_LABEL; + } + + return; + } + + // Step 1. The label must be in Unicode Normalization Form C. + if (!\Normalizer::isNormalized($label, \Normalizer::FORM_C)) { + $info->errors |= self::ERROR_INVALID_ACE_LABEL; + } + + $codePoints = self::utf8Decode($label); + + if ($options['CheckHyphens']) { + // Step 2. If CheckHyphens, the label must not contain a U+002D HYPHEN-MINUS character + // in both the thrid and fourth positions. + if (isset($codePoints[2], $codePoints[3]) && 0x002D === $codePoints[2] && 0x002D === $codePoints[3]) { + $info->errors |= self::ERROR_HYPHEN_3_4; + } + + // Step 3. If CheckHyphens, the label must neither begin nor end with a U+002D + // HYPHEN-MINUS character. + if ('-' === substr($label, 0, 1)) { + $info->errors |= self::ERROR_LEADING_HYPHEN; + } + + if ('-' === substr($label, -1, 1)) { + $info->errors |= self::ERROR_TRAILING_HYPHEN; + } + } elseif ('xn--' === substr($label, 0, 4)) { + $info->errors |= self::ERROR_PUNYCODE; + } + + // Step 4. The label must not contain a U+002E (.) FULL STOP. + if (false !== strpos($label, '.')) { + $info->errors |= self::ERROR_LABEL_HAS_DOT; + } + + // Step 5. The label must not begin with a combining mark, that is: General_Category=Mark. + if (1 === preg_match(Regex::COMBINING_MARK, $label)) { + $info->errors |= self::ERROR_LEADING_COMBINING_MARK; + } + + // Step 6. Each code point in the label must only have certain status values according to + // Section 5, IDNA Mapping Table: + $transitional = $options['Transitional_Processing']; + $useSTD3ASCIIRules = $options['UseSTD3ASCIIRules']; + + foreach ($codePoints as $codePoint) { + $data = self::lookupCodePointStatus($codePoint, $useSTD3ASCIIRules); + $status = $data['status']; + + if ('valid' === $status || (!$transitional && 'deviation' === $status)) { + continue; + } + + $info->errors |= self::ERROR_DISALLOWED; + + break; + } + + // Step 7. If CheckJoiners, the label must satisify the ContextJ rules from Appendix A, in + // The Unicode Code Points and Internationalized Domain Names for Applications (IDNA) + // [IDNA2008]. + if ($options['CheckJoiners'] && !self::isValidContextJ($codePoints, $label)) { + $info->errors |= self::ERROR_CONTEXTJ; + } + + // Step 8. If CheckBidi, and if the domain name is a Bidi domain name, then the label must + // satisfy all six of the numbered conditions in [IDNA2008] RFC 5893, Section 2. + if ($options['CheckBidi'] && (!$info->bidiDomain || $info->validBidiDomain)) { + self::validateBidiLabel($label, $info); + } + } + + /** + * @see https://tools.ietf.org/html/rfc3492#section-6.2 + * + * @param string $input + * + * @return string + */ + private static function punycodeDecode($input) + { + $n = self::INITIAL_N; + $out = 0; + $i = 0; + $bias = self::INITIAL_BIAS; + $lastDelimIndex = strrpos($input, self::DELIMITER); + $b = false === $lastDelimIndex ? 0 : $lastDelimIndex; + $inputLength = \strlen($input); + $output = []; + $bytes = array_map('ord', str_split($input)); + + for ($j = 0; $j < $b; ++$j) { + if ($bytes[$j] > 0x7F) { + throw new \Exception('Invalid input'); + } + + $output[$out++] = $input[$j]; + } + + if ($b > 0) { + ++$b; + } + + for ($in = $b; $in < $inputLength; ++$out) { + $oldi = $i; + $w = 1; + + for ($k = self::BASE; /* no condition */; $k += self::BASE) { + if ($in >= $inputLength) { + throw new \Exception('Invalid input'); + } + + $digit = self::$basicToDigit[$bytes[$in++] & 0xFF]; + + if ($digit < 0) { + throw new \Exception('Invalid input'); + } + + if ($digit > intdiv(self::MAX_INT - $i, $w)) { + throw new \Exception('Integer overflow'); + } + + $i += $digit * $w; + + if ($k <= $bias) { + $t = self::TMIN; + } elseif ($k >= $bias + self::TMAX) { + $t = self::TMAX; + } else { + $t = $k - $bias; + } + + if ($digit < $t) { + break; + } + + $baseMinusT = self::BASE - $t; + + if ($w > intdiv(self::MAX_INT, $baseMinusT)) { + throw new \Exception('Integer overflow'); + } + + $w *= $baseMinusT; + } + + $outPlusOne = $out + 1; + $bias = self::adaptBias($i - $oldi, $outPlusOne, 0 === $oldi); + + if (intdiv($i, $outPlusOne) > self::MAX_INT - $n) { + throw new \Exception('Integer overflow'); + } + + $n += intdiv($i, $outPlusOne); + $i %= $outPlusOne; + array_splice($output, $i++, 0, [mb_chr($n, 'utf-8')]); + } + + return implode('', $output); + } + + /** + * @see https://tools.ietf.org/html/rfc3492#section-6.3 + * + * @param string $input + * + * @return string + */ + private static function punycodeEncode($input) + { + $n = self::INITIAL_N; + $delta = 0; + $out = 0; + $bias = self::INITIAL_BIAS; + $inputLength = 0; + $output = ''; + $iter = self::utf8Decode($input); + + foreach ($iter as $codePoint) { + ++$inputLength; + + if ($codePoint < 0x80) { + $output .= \chr($codePoint); + ++$out; + } + } + + $h = $out; + $b = $out; + + if ($b > 0) { + $output .= self::DELIMITER; + ++$out; + } + + while ($h < $inputLength) { + $m = self::MAX_INT; + + foreach ($iter as $codePoint) { + if ($codePoint >= $n && $codePoint < $m) { + $m = $codePoint; + } + } + + if ($m - $n > intdiv(self::MAX_INT - $delta, $h + 1)) { + throw new \Exception('Integer overflow'); + } + + $delta += ($m - $n) * ($h + 1); + $n = $m; + + foreach ($iter as $codePoint) { + if ($codePoint < $n && 0 === ++$delta) { + throw new \Exception('Integer overflow'); + } + + if ($codePoint === $n) { + $q = $delta; + + for ($k = self::BASE; /* no condition */; $k += self::BASE) { + if ($k <= $bias) { + $t = self::TMIN; + } elseif ($k >= $bias + self::TMAX) { + $t = self::TMAX; + } else { + $t = $k - $bias; + } + + if ($q < $t) { + break; + } + + $qMinusT = $q - $t; + $baseMinusT = self::BASE - $t; + $output .= self::encodeDigit($t + $qMinusT % $baseMinusT, false); + ++$out; + $q = intdiv($qMinusT, $baseMinusT); + } + + $output .= self::encodeDigit($q, false); + ++$out; + $bias = self::adaptBias($delta, $h + 1, $h === $b); + $delta = 0; + ++$h; + } + } + + ++$delta; + ++$n; + } + + return $output; + } + + /** + * @see https://tools.ietf.org/html/rfc3492#section-6.1 + * + * @param int $delta + * @param int $numPoints + * @param bool $firstTime + * + * @return int + */ + private static function adaptBias($delta, $numPoints, $firstTime) + { + // xxx >> 1 is a faster way of doing intdiv(xxx, 2) + $delta = $firstTime ? intdiv($delta, self::DAMP) : $delta >> 1; + $delta += intdiv($delta, $numPoints); + $k = 0; + + while ($delta > ((self::BASE - self::TMIN) * self::TMAX) >> 1) { + $delta = intdiv($delta, self::BASE - self::TMIN); + $k += self::BASE; + } + + return $k + intdiv((self::BASE - self::TMIN + 1) * $delta, $delta + self::SKEW); + } + + /** + * @param int $d + * @param bool $flag + * + * @return string + */ + private static function encodeDigit($d, $flag) + { + return \chr($d + 22 + 75 * ($d < 26 ? 1 : 0) - (($flag ? 1 : 0) << 5)); + } + + /** + * Takes a UTF-8 encoded string and converts it into a series of integer code points. Any + * invalid byte sequences will be replaced by a U+FFFD replacement code point. + * + * @see https://encoding.spec.whatwg.org/#utf-8-decoder + * + * @param string $input + * + * @return array + */ + private static function utf8Decode($input) + { + $bytesSeen = 0; + $bytesNeeded = 0; + $lowerBoundary = 0x80; + $upperBoundary = 0xBF; + $codePoint = 0; + $codePoints = []; + $length = \strlen($input); + + for ($i = 0; $i < $length; ++$i) { + $byte = \ord($input[$i]); + + if (0 === $bytesNeeded) { + if ($byte >= 0x00 && $byte <= 0x7F) { + $codePoints[] = $byte; + + continue; + } + + if ($byte >= 0xC2 && $byte <= 0xDF) { + $bytesNeeded = 1; + $codePoint = $byte & 0x1F; + } elseif ($byte >= 0xE0 && $byte <= 0xEF) { + if (0xE0 === $byte) { + $lowerBoundary = 0xA0; + } elseif (0xED === $byte) { + $upperBoundary = 0x9F; + } + + $bytesNeeded = 2; + $codePoint = $byte & 0xF; + } elseif ($byte >= 0xF0 && $byte <= 0xF4) { + if (0xF0 === $byte) { + $lowerBoundary = 0x90; + } elseif (0xF4 === $byte) { + $upperBoundary = 0x8F; + } + + $bytesNeeded = 3; + $codePoint = $byte & 0x7; + } else { + $codePoints[] = 0xFFFD; + } + + continue; + } + + if ($byte < $lowerBoundary || $byte > $upperBoundary) { + $codePoint = 0; + $bytesNeeded = 0; + $bytesSeen = 0; + $lowerBoundary = 0x80; + $upperBoundary = 0xBF; + --$i; + $codePoints[] = 0xFFFD; + + continue; + } + + $lowerBoundary = 0x80; + $upperBoundary = 0xBF; + $codePoint = ($codePoint << 6) | ($byte & 0x3F); + + if (++$bytesSeen !== $bytesNeeded) { + continue; + } + + $codePoints[] = $codePoint; + $codePoint = 0; + $bytesNeeded = 0; + $bytesSeen = 0; + } + + // String unexpectedly ended, so append a U+FFFD code point. + if (0 !== $bytesNeeded) { + $codePoints[] = 0xFFFD; + } + + return $codePoints; + } + + /** + * @param int $codePoint + * @param bool $useSTD3ASCIIRules + * + * @return array{status: string, mapping?: string} + */ + private static function lookupCodePointStatus($codePoint, $useSTD3ASCIIRules) + { + if (!self::$mappingTableLoaded) { + self::$mappingTableLoaded = true; + self::$mapped = require __DIR__.'/Resources/unidata/mapped.php'; + self::$ignored = require __DIR__.'/Resources/unidata/ignored.php'; + self::$deviation = require __DIR__.'/Resources/unidata/deviation.php'; + self::$disallowed = require __DIR__.'/Resources/unidata/disallowed.php'; + self::$disallowed_STD3_mapped = require __DIR__.'/Resources/unidata/disallowed_STD3_mapped.php'; + self::$disallowed_STD3_valid = require __DIR__.'/Resources/unidata/disallowed_STD3_valid.php'; + } + + if (isset(self::$mapped[$codePoint])) { + return ['status' => 'mapped', 'mapping' => self::$mapped[$codePoint]]; + } + + if (isset(self::$ignored[$codePoint])) { + return ['status' => 'ignored']; + } + + if (isset(self::$deviation[$codePoint])) { + return ['status' => 'deviation', 'mapping' => self::$deviation[$codePoint]]; + } + + if (isset(self::$disallowed[$codePoint]) || DisallowedRanges::inRange($codePoint)) { + return ['status' => 'disallowed']; + } + + $isDisallowedMapped = isset(self::$disallowed_STD3_mapped[$codePoint]); + + if ($isDisallowedMapped || isset(self::$disallowed_STD3_valid[$codePoint])) { + $status = 'disallowed'; + + if (!$useSTD3ASCIIRules) { + $status = $isDisallowedMapped ? 'mapped' : 'valid'; + } + + if ($isDisallowedMapped) { + return ['status' => $status, 'mapping' => self::$disallowed_STD3_mapped[$codePoint]]; + } + + return ['status' => $status]; + } + + return ['status' => 'valid']; + } +} diff --git a/3rdparty/symfony/polyfill-intl-idn/Info.php b/3rdparty/symfony/polyfill-intl-idn/Info.php new file mode 100644 index 00000000..25c3582b --- /dev/null +++ b/3rdparty/symfony/polyfill-intl-idn/Info.php @@ -0,0 +1,23 @@ + and Trevor Rowbotham + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Polyfill\Intl\Idn; + +/** + * @internal + */ +class Info +{ + public $bidiDomain = false; + public $errors = 0; + public $validBidiDomain = true; + public $transitionalDifferent = false; +} diff --git a/3rdparty/symfony/polyfill-intl-idn/LICENSE b/3rdparty/symfony/polyfill-intl-idn/LICENSE new file mode 100644 index 00000000..fd0a0626 --- /dev/null +++ b/3rdparty/symfony/polyfill-intl-idn/LICENSE @@ -0,0 +1,19 @@ +Copyright (c) 2018-present Fabien Potencier and Trevor Rowbotham + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is furnished +to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +THE SOFTWARE. diff --git a/3rdparty/symfony/polyfill-intl-idn/Resources/unidata/DisallowedRanges.php b/3rdparty/symfony/polyfill-intl-idn/Resources/unidata/DisallowedRanges.php new file mode 100644 index 00000000..d285acd1 --- /dev/null +++ b/3rdparty/symfony/polyfill-intl-idn/Resources/unidata/DisallowedRanges.php @@ -0,0 +1,384 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Polyfill\Intl\Idn\Resources\unidata; + +/** + * @internal + */ +final class DisallowedRanges +{ + /** + * @param int $codePoint + * + * @return bool + */ + public static function inRange($codePoint) + { + if ($codePoint >= 128 && $codePoint <= 159) { + return true; + } + + if ($codePoint >= 2155 && $codePoint <= 2207) { + return true; + } + + if ($codePoint >= 3676 && $codePoint <= 3712) { + return true; + } + + if ($codePoint >= 3808 && $codePoint <= 3839) { + return true; + } + + if ($codePoint >= 4059 && $codePoint <= 4095) { + return true; + } + + if ($codePoint >= 4256 && $codePoint <= 4293) { + return true; + } + + if ($codePoint >= 6849 && $codePoint <= 6911) { + return true; + } + + if ($codePoint >= 11859 && $codePoint <= 11903) { + return true; + } + + if ($codePoint >= 42955 && $codePoint <= 42996) { + return true; + } + + if ($codePoint >= 55296 && $codePoint <= 57343) { + return true; + } + + if ($codePoint >= 57344 && $codePoint <= 63743) { + return true; + } + + if ($codePoint >= 64218 && $codePoint <= 64255) { + return true; + } + + if ($codePoint >= 64976 && $codePoint <= 65007) { + return true; + } + + if ($codePoint >= 65630 && $codePoint <= 65663) { + return true; + } + + if ($codePoint >= 65953 && $codePoint <= 65999) { + return true; + } + + if ($codePoint >= 66046 && $codePoint <= 66175) { + return true; + } + + if ($codePoint >= 66518 && $codePoint <= 66559) { + return true; + } + + if ($codePoint >= 66928 && $codePoint <= 67071) { + return true; + } + + if ($codePoint >= 67432 && $codePoint <= 67583) { + return true; + } + + if ($codePoint >= 67760 && $codePoint <= 67807) { + return true; + } + + if ($codePoint >= 67904 && $codePoint <= 67967) { + return true; + } + + if ($codePoint >= 68256 && $codePoint <= 68287) { + return true; + } + + if ($codePoint >= 68528 && $codePoint <= 68607) { + return true; + } + + if ($codePoint >= 68681 && $codePoint <= 68735) { + return true; + } + + if ($codePoint >= 68922 && $codePoint <= 69215) { + return true; + } + + if ($codePoint >= 69298 && $codePoint <= 69375) { + return true; + } + + if ($codePoint >= 69466 && $codePoint <= 69551) { + return true; + } + + if ($codePoint >= 70207 && $codePoint <= 70271) { + return true; + } + + if ($codePoint >= 70517 && $codePoint <= 70655) { + return true; + } + + if ($codePoint >= 70874 && $codePoint <= 71039) { + return true; + } + + if ($codePoint >= 71134 && $codePoint <= 71167) { + return true; + } + + if ($codePoint >= 71370 && $codePoint <= 71423) { + return true; + } + + if ($codePoint >= 71488 && $codePoint <= 71679) { + return true; + } + + if ($codePoint >= 71740 && $codePoint <= 71839) { + return true; + } + + if ($codePoint >= 72026 && $codePoint <= 72095) { + return true; + } + + if ($codePoint >= 72441 && $codePoint <= 72703) { + return true; + } + + if ($codePoint >= 72887 && $codePoint <= 72959) { + return true; + } + + if ($codePoint >= 73130 && $codePoint <= 73439) { + return true; + } + + if ($codePoint >= 73465 && $codePoint <= 73647) { + return true; + } + + if ($codePoint >= 74650 && $codePoint <= 74751) { + return true; + } + + if ($codePoint >= 75076 && $codePoint <= 77823) { + return true; + } + + if ($codePoint >= 78905 && $codePoint <= 82943) { + return true; + } + + if ($codePoint >= 83527 && $codePoint <= 92159) { + return true; + } + + if ($codePoint >= 92784 && $codePoint <= 92879) { + return true; + } + + if ($codePoint >= 93072 && $codePoint <= 93759) { + return true; + } + + if ($codePoint >= 93851 && $codePoint <= 93951) { + return true; + } + + if ($codePoint >= 94112 && $codePoint <= 94175) { + return true; + } + + if ($codePoint >= 101590 && $codePoint <= 101631) { + return true; + } + + if ($codePoint >= 101641 && $codePoint <= 110591) { + return true; + } + + if ($codePoint >= 110879 && $codePoint <= 110927) { + return true; + } + + if ($codePoint >= 111356 && $codePoint <= 113663) { + return true; + } + + if ($codePoint >= 113828 && $codePoint <= 118783) { + return true; + } + + if ($codePoint >= 119366 && $codePoint <= 119519) { + return true; + } + + if ($codePoint >= 119673 && $codePoint <= 119807) { + return true; + } + + if ($codePoint >= 121520 && $codePoint <= 122879) { + return true; + } + + if ($codePoint >= 122923 && $codePoint <= 123135) { + return true; + } + + if ($codePoint >= 123216 && $codePoint <= 123583) { + return true; + } + + if ($codePoint >= 123648 && $codePoint <= 124927) { + return true; + } + + if ($codePoint >= 125143 && $codePoint <= 125183) { + return true; + } + + if ($codePoint >= 125280 && $codePoint <= 126064) { + return true; + } + + if ($codePoint >= 126133 && $codePoint <= 126208) { + return true; + } + + if ($codePoint >= 126270 && $codePoint <= 126463) { + return true; + } + + if ($codePoint >= 126652 && $codePoint <= 126703) { + return true; + } + + if ($codePoint >= 126706 && $codePoint <= 126975) { + return true; + } + + if ($codePoint >= 127406 && $codePoint <= 127461) { + return true; + } + + if ($codePoint >= 127590 && $codePoint <= 127743) { + return true; + } + + if ($codePoint >= 129202 && $codePoint <= 129279) { + return true; + } + + if ($codePoint >= 129751 && $codePoint <= 129791) { + return true; + } + + if ($codePoint >= 129995 && $codePoint <= 130031) { + return true; + } + + if ($codePoint >= 130042 && $codePoint <= 131069) { + return true; + } + + if ($codePoint >= 173790 && $codePoint <= 173823) { + return true; + } + + if ($codePoint >= 191457 && $codePoint <= 194559) { + return true; + } + + if ($codePoint >= 195102 && $codePoint <= 196605) { + return true; + } + + if ($codePoint >= 201547 && $codePoint <= 262141) { + return true; + } + + if ($codePoint >= 262144 && $codePoint <= 327677) { + return true; + } + + if ($codePoint >= 327680 && $codePoint <= 393213) { + return true; + } + + if ($codePoint >= 393216 && $codePoint <= 458749) { + return true; + } + + if ($codePoint >= 458752 && $codePoint <= 524285) { + return true; + } + + if ($codePoint >= 524288 && $codePoint <= 589821) { + return true; + } + + if ($codePoint >= 589824 && $codePoint <= 655357) { + return true; + } + + if ($codePoint >= 655360 && $codePoint <= 720893) { + return true; + } + + if ($codePoint >= 720896 && $codePoint <= 786429) { + return true; + } + + if ($codePoint >= 786432 && $codePoint <= 851965) { + return true; + } + + if ($codePoint >= 851968 && $codePoint <= 917501) { + return true; + } + + if ($codePoint >= 917536 && $codePoint <= 917631) { + return true; + } + + if ($codePoint >= 917632 && $codePoint <= 917759) { + return true; + } + + if ($codePoint >= 918000 && $codePoint <= 983037) { + return true; + } + + if ($codePoint >= 983040 && $codePoint <= 1048573) { + return true; + } + + if ($codePoint >= 1048576 && $codePoint <= 1114109) { + return true; + } + + return false; + } +} diff --git a/3rdparty/symfony/polyfill-intl-idn/Resources/unidata/Regex.php b/3rdparty/symfony/polyfill-intl-idn/Resources/unidata/Regex.php new file mode 100644 index 00000000..3c6af0c1 --- /dev/null +++ b/3rdparty/symfony/polyfill-intl-idn/Resources/unidata/Regex.php @@ -0,0 +1,33 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Polyfill\Intl\Idn\Resources\unidata; + +/** + * @internal + */ +final class Regex +{ + const COMBINING_MARK = '/^[\x{0300}-\x{036F}\x{0483}-\x{0487}\x{0488}-\x{0489}\x{0591}-\x{05BD}\x{05BF}\x{05C1}-\x{05C2}\x{05C4}-\x{05C5}\x{05C7}\x{0610}-\x{061A}\x{064B}-\x{065F}\x{0670}\x{06D6}-\x{06DC}\x{06DF}-\x{06E4}\x{06E7}-\x{06E8}\x{06EA}-\x{06ED}\x{0711}\x{0730}-\x{074A}\x{07A6}-\x{07B0}\x{07EB}-\x{07F3}\x{07FD}\x{0816}-\x{0819}\x{081B}-\x{0823}\x{0825}-\x{0827}\x{0829}-\x{082D}\x{0859}-\x{085B}\x{08D3}-\x{08E1}\x{08E3}-\x{0902}\x{0903}\x{093A}\x{093B}\x{093C}\x{093E}-\x{0940}\x{0941}-\x{0948}\x{0949}-\x{094C}\x{094D}\x{094E}-\x{094F}\x{0951}-\x{0957}\x{0962}-\x{0963}\x{0981}\x{0982}-\x{0983}\x{09BC}\x{09BE}-\x{09C0}\x{09C1}-\x{09C4}\x{09C7}-\x{09C8}\x{09CB}-\x{09CC}\x{09CD}\x{09D7}\x{09E2}-\x{09E3}\x{09FE}\x{0A01}-\x{0A02}\x{0A03}\x{0A3C}\x{0A3E}-\x{0A40}\x{0A41}-\x{0A42}\x{0A47}-\x{0A48}\x{0A4B}-\x{0A4D}\x{0A51}\x{0A70}-\x{0A71}\x{0A75}\x{0A81}-\x{0A82}\x{0A83}\x{0ABC}\x{0ABE}-\x{0AC0}\x{0AC1}-\x{0AC5}\x{0AC7}-\x{0AC8}\x{0AC9}\x{0ACB}-\x{0ACC}\x{0ACD}\x{0AE2}-\x{0AE3}\x{0AFA}-\x{0AFF}\x{0B01}\x{0B02}-\x{0B03}\x{0B3C}\x{0B3E}\x{0B3F}\x{0B40}\x{0B41}-\x{0B44}\x{0B47}-\x{0B48}\x{0B4B}-\x{0B4C}\x{0B4D}\x{0B55}-\x{0B56}\x{0B57}\x{0B62}-\x{0B63}\x{0B82}\x{0BBE}-\x{0BBF}\x{0BC0}\x{0BC1}-\x{0BC2}\x{0BC6}-\x{0BC8}\x{0BCA}-\x{0BCC}\x{0BCD}\x{0BD7}\x{0C00}\x{0C01}-\x{0C03}\x{0C04}\x{0C3E}-\x{0C40}\x{0C41}-\x{0C44}\x{0C46}-\x{0C48}\x{0C4A}-\x{0C4D}\x{0C55}-\x{0C56}\x{0C62}-\x{0C63}\x{0C81}\x{0C82}-\x{0C83}\x{0CBC}\x{0CBE}\x{0CBF}\x{0CC0}-\x{0CC4}\x{0CC6}\x{0CC7}-\x{0CC8}\x{0CCA}-\x{0CCB}\x{0CCC}-\x{0CCD}\x{0CD5}-\x{0CD6}\x{0CE2}-\x{0CE3}\x{0D00}-\x{0D01}\x{0D02}-\x{0D03}\x{0D3B}-\x{0D3C}\x{0D3E}-\x{0D40}\x{0D41}-\x{0D44}\x{0D46}-\x{0D48}\x{0D4A}-\x{0D4C}\x{0D4D}\x{0D57}\x{0D62}-\x{0D63}\x{0D81}\x{0D82}-\x{0D83}\x{0DCA}\x{0DCF}-\x{0DD1}\x{0DD2}-\x{0DD4}\x{0DD6}\x{0DD8}-\x{0DDF}\x{0DF2}-\x{0DF3}\x{0E31}\x{0E34}-\x{0E3A}\x{0E47}-\x{0E4E}\x{0EB1}\x{0EB4}-\x{0EBC}\x{0EC8}-\x{0ECD}\x{0F18}-\x{0F19}\x{0F35}\x{0F37}\x{0F39}\x{0F3E}-\x{0F3F}\x{0F71}-\x{0F7E}\x{0F7F}\x{0F80}-\x{0F84}\x{0F86}-\x{0F87}\x{0F8D}-\x{0F97}\x{0F99}-\x{0FBC}\x{0FC6}\x{102B}-\x{102C}\x{102D}-\x{1030}\x{1031}\x{1032}-\x{1037}\x{1038}\x{1039}-\x{103A}\x{103B}-\x{103C}\x{103D}-\x{103E}\x{1056}-\x{1057}\x{1058}-\x{1059}\x{105E}-\x{1060}\x{1062}-\x{1064}\x{1067}-\x{106D}\x{1071}-\x{1074}\x{1082}\x{1083}-\x{1084}\x{1085}-\x{1086}\x{1087}-\x{108C}\x{108D}\x{108F}\x{109A}-\x{109C}\x{109D}\x{135D}-\x{135F}\x{1712}-\x{1714}\x{1732}-\x{1734}\x{1752}-\x{1753}\x{1772}-\x{1773}\x{17B4}-\x{17B5}\x{17B6}\x{17B7}-\x{17BD}\x{17BE}-\x{17C5}\x{17C6}\x{17C7}-\x{17C8}\x{17C9}-\x{17D3}\x{17DD}\x{180B}-\x{180D}\x{1885}-\x{1886}\x{18A9}\x{1920}-\x{1922}\x{1923}-\x{1926}\x{1927}-\x{1928}\x{1929}-\x{192B}\x{1930}-\x{1931}\x{1932}\x{1933}-\x{1938}\x{1939}-\x{193B}\x{1A17}-\x{1A18}\x{1A19}-\x{1A1A}\x{1A1B}\x{1A55}\x{1A56}\x{1A57}\x{1A58}-\x{1A5E}\x{1A60}\x{1A61}\x{1A62}\x{1A63}-\x{1A64}\x{1A65}-\x{1A6C}\x{1A6D}-\x{1A72}\x{1A73}-\x{1A7C}\x{1A7F}\x{1AB0}-\x{1ABD}\x{1ABE}\x{1ABF}-\x{1AC0}\x{1B00}-\x{1B03}\x{1B04}\x{1B34}\x{1B35}\x{1B36}-\x{1B3A}\x{1B3B}\x{1B3C}\x{1B3D}-\x{1B41}\x{1B42}\x{1B43}-\x{1B44}\x{1B6B}-\x{1B73}\x{1B80}-\x{1B81}\x{1B82}\x{1BA1}\x{1BA2}-\x{1BA5}\x{1BA6}-\x{1BA7}\x{1BA8}-\x{1BA9}\x{1BAA}\x{1BAB}-\x{1BAD}\x{1BE6}\x{1BE7}\x{1BE8}-\x{1BE9}\x{1BEA}-\x{1BEC}\x{1BED}\x{1BEE}\x{1BEF}-\x{1BF1}\x{1BF2}-\x{1BF3}\x{1C24}-\x{1C2B}\x{1C2C}-\x{1C33}\x{1C34}-\x{1C35}\x{1C36}-\x{1C37}\x{1CD0}-\x{1CD2}\x{1CD4}-\x{1CE0}\x{1CE1}\x{1CE2}-\x{1CE8}\x{1CED}\x{1CF4}\x{1CF7}\x{1CF8}-\x{1CF9}\x{1DC0}-\x{1DF9}\x{1DFB}-\x{1DFF}\x{20D0}-\x{20DC}\x{20DD}-\x{20E0}\x{20E1}\x{20E2}-\x{20E4}\x{20E5}-\x{20F0}\x{2CEF}-\x{2CF1}\x{2D7F}\x{2DE0}-\x{2DFF}\x{302A}-\x{302D}\x{302E}-\x{302F}\x{3099}-\x{309A}\x{A66F}\x{A670}-\x{A672}\x{A674}-\x{A67D}\x{A69E}-\x{A69F}\x{A6F0}-\x{A6F1}\x{A802}\x{A806}\x{A80B}\x{A823}-\x{A824}\x{A825}-\x{A826}\x{A827}\x{A82C}\x{A880}-\x{A881}\x{A8B4}-\x{A8C3}\x{A8C4}-\x{A8C5}\x{A8E0}-\x{A8F1}\x{A8FF}\x{A926}-\x{A92D}\x{A947}-\x{A951}\x{A952}-\x{A953}\x{A980}-\x{A982}\x{A983}\x{A9B3}\x{A9B4}-\x{A9B5}\x{A9B6}-\x{A9B9}\x{A9BA}-\x{A9BB}\x{A9BC}-\x{A9BD}\x{A9BE}-\x{A9C0}\x{A9E5}\x{AA29}-\x{AA2E}\x{AA2F}-\x{AA30}\x{AA31}-\x{AA32}\x{AA33}-\x{AA34}\x{AA35}-\x{AA36}\x{AA43}\x{AA4C}\x{AA4D}\x{AA7B}\x{AA7C}\x{AA7D}\x{AAB0}\x{AAB2}-\x{AAB4}\x{AAB7}-\x{AAB8}\x{AABE}-\x{AABF}\x{AAC1}\x{AAEB}\x{AAEC}-\x{AAED}\x{AAEE}-\x{AAEF}\x{AAF5}\x{AAF6}\x{ABE3}-\x{ABE4}\x{ABE5}\x{ABE6}-\x{ABE7}\x{ABE8}\x{ABE9}-\x{ABEA}\x{ABEC}\x{ABED}\x{FB1E}\x{FE00}-\x{FE0F}\x{FE20}-\x{FE2F}\x{101FD}\x{102E0}\x{10376}-\x{1037A}\x{10A01}-\x{10A03}\x{10A05}-\x{10A06}\x{10A0C}-\x{10A0F}\x{10A38}-\x{10A3A}\x{10A3F}\x{10AE5}-\x{10AE6}\x{10D24}-\x{10D27}\x{10EAB}-\x{10EAC}\x{10F46}-\x{10F50}\x{11000}\x{11001}\x{11002}\x{11038}-\x{11046}\x{1107F}-\x{11081}\x{11082}\x{110B0}-\x{110B2}\x{110B3}-\x{110B6}\x{110B7}-\x{110B8}\x{110B9}-\x{110BA}\x{11100}-\x{11102}\x{11127}-\x{1112B}\x{1112C}\x{1112D}-\x{11134}\x{11145}-\x{11146}\x{11173}\x{11180}-\x{11181}\x{11182}\x{111B3}-\x{111B5}\x{111B6}-\x{111BE}\x{111BF}-\x{111C0}\x{111C9}-\x{111CC}\x{111CE}\x{111CF}\x{1122C}-\x{1122E}\x{1122F}-\x{11231}\x{11232}-\x{11233}\x{11234}\x{11235}\x{11236}-\x{11237}\x{1123E}\x{112DF}\x{112E0}-\x{112E2}\x{112E3}-\x{112EA}\x{11300}-\x{11301}\x{11302}-\x{11303}\x{1133B}-\x{1133C}\x{1133E}-\x{1133F}\x{11340}\x{11341}-\x{11344}\x{11347}-\x{11348}\x{1134B}-\x{1134D}\x{11357}\x{11362}-\x{11363}\x{11366}-\x{1136C}\x{11370}-\x{11374}\x{11435}-\x{11437}\x{11438}-\x{1143F}\x{11440}-\x{11441}\x{11442}-\x{11444}\x{11445}\x{11446}\x{1145E}\x{114B0}-\x{114B2}\x{114B3}-\x{114B8}\x{114B9}\x{114BA}\x{114BB}-\x{114BE}\x{114BF}-\x{114C0}\x{114C1}\x{114C2}-\x{114C3}\x{115AF}-\x{115B1}\x{115B2}-\x{115B5}\x{115B8}-\x{115BB}\x{115BC}-\x{115BD}\x{115BE}\x{115BF}-\x{115C0}\x{115DC}-\x{115DD}\x{11630}-\x{11632}\x{11633}-\x{1163A}\x{1163B}-\x{1163C}\x{1163D}\x{1163E}\x{1163F}-\x{11640}\x{116AB}\x{116AC}\x{116AD}\x{116AE}-\x{116AF}\x{116B0}-\x{116B5}\x{116B6}\x{116B7}\x{1171D}-\x{1171F}\x{11720}-\x{11721}\x{11722}-\x{11725}\x{11726}\x{11727}-\x{1172B}\x{1182C}-\x{1182E}\x{1182F}-\x{11837}\x{11838}\x{11839}-\x{1183A}\x{11930}-\x{11935}\x{11937}-\x{11938}\x{1193B}-\x{1193C}\x{1193D}\x{1193E}\x{11940}\x{11942}\x{11943}\x{119D1}-\x{119D3}\x{119D4}-\x{119D7}\x{119DA}-\x{119DB}\x{119DC}-\x{119DF}\x{119E0}\x{119E4}\x{11A01}-\x{11A0A}\x{11A33}-\x{11A38}\x{11A39}\x{11A3B}-\x{11A3E}\x{11A47}\x{11A51}-\x{11A56}\x{11A57}-\x{11A58}\x{11A59}-\x{11A5B}\x{11A8A}-\x{11A96}\x{11A97}\x{11A98}-\x{11A99}\x{11C2F}\x{11C30}-\x{11C36}\x{11C38}-\x{11C3D}\x{11C3E}\x{11C3F}\x{11C92}-\x{11CA7}\x{11CA9}\x{11CAA}-\x{11CB0}\x{11CB1}\x{11CB2}-\x{11CB3}\x{11CB4}\x{11CB5}-\x{11CB6}\x{11D31}-\x{11D36}\x{11D3A}\x{11D3C}-\x{11D3D}\x{11D3F}-\x{11D45}\x{11D47}\x{11D8A}-\x{11D8E}\x{11D90}-\x{11D91}\x{11D93}-\x{11D94}\x{11D95}\x{11D96}\x{11D97}\x{11EF3}-\x{11EF4}\x{11EF5}-\x{11EF6}\x{16AF0}-\x{16AF4}\x{16B30}-\x{16B36}\x{16F4F}\x{16F51}-\x{16F87}\x{16F8F}-\x{16F92}\x{16FE4}\x{16FF0}-\x{16FF1}\x{1BC9D}-\x{1BC9E}\x{1D165}-\x{1D166}\x{1D167}-\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{1E000}-\x{1E006}\x{1E008}-\x{1E018}\x{1E01B}-\x{1E021}\x{1E023}-\x{1E024}\x{1E026}-\x{1E02A}\x{1E130}-\x{1E136}\x{1E2EC}-\x{1E2EF}\x{1E8D0}-\x{1E8D6}\x{1E944}-\x{1E94A}\x{E0100}-\x{E01EF}]/u'; + + const RTL_LABEL = '/[\x{0590}\x{05BE}\x{05C0}\x{05C3}\x{05C6}\x{05C8}-\x{05CF}\x{05D0}-\x{05EA}\x{05EB}-\x{05EE}\x{05EF}-\x{05F2}\x{05F3}-\x{05F4}\x{05F5}-\x{05FF}\x{0600}-\x{0605}\x{0608}\x{060B}\x{060D}\x{061B}\x{061C}\x{061D}\x{061E}-\x{061F}\x{0620}-\x{063F}\x{0640}\x{0641}-\x{064A}\x{0660}-\x{0669}\x{066B}-\x{066C}\x{066D}\x{066E}-\x{066F}\x{0671}-\x{06D3}\x{06D4}\x{06D5}\x{06DD}\x{06E5}-\x{06E6}\x{06EE}-\x{06EF}\x{06FA}-\x{06FC}\x{06FD}-\x{06FE}\x{06FF}\x{0700}-\x{070D}\x{070E}\x{070F}\x{0710}\x{0712}-\x{072F}\x{074B}-\x{074C}\x{074D}-\x{07A5}\x{07B1}\x{07B2}-\x{07BF}\x{07C0}-\x{07C9}\x{07CA}-\x{07EA}\x{07F4}-\x{07F5}\x{07FA}\x{07FB}-\x{07FC}\x{07FE}-\x{07FF}\x{0800}-\x{0815}\x{081A}\x{0824}\x{0828}\x{082E}-\x{082F}\x{0830}-\x{083E}\x{083F}\x{0840}-\x{0858}\x{085C}-\x{085D}\x{085E}\x{085F}\x{0860}-\x{086A}\x{086B}-\x{086F}\x{0870}-\x{089F}\x{08A0}-\x{08B4}\x{08B5}\x{08B6}-\x{08C7}\x{08C8}-\x{08D2}\x{08E2}\x{200F}\x{FB1D}\x{FB1F}-\x{FB28}\x{FB2A}-\x{FB36}\x{FB37}\x{FB38}-\x{FB3C}\x{FB3D}\x{FB3E}\x{FB3F}\x{FB40}-\x{FB41}\x{FB42}\x{FB43}-\x{FB44}\x{FB45}\x{FB46}-\x{FB4F}\x{FB50}-\x{FBB1}\x{FBB2}-\x{FBC1}\x{FBC2}-\x{FBD2}\x{FBD3}-\x{FD3D}\x{FD40}-\x{FD4F}\x{FD50}-\x{FD8F}\x{FD90}-\x{FD91}\x{FD92}-\x{FDC7}\x{FDC8}-\x{FDCF}\x{FDF0}-\x{FDFB}\x{FDFC}\x{FDFE}-\x{FDFF}\x{FE70}-\x{FE74}\x{FE75}\x{FE76}-\x{FEFC}\x{FEFD}-\x{FEFE}\x{10800}-\x{10805}\x{10806}-\x{10807}\x{10808}\x{10809}\x{1080A}-\x{10835}\x{10836}\x{10837}-\x{10838}\x{10839}-\x{1083B}\x{1083C}\x{1083D}-\x{1083E}\x{1083F}-\x{10855}\x{10856}\x{10857}\x{10858}-\x{1085F}\x{10860}-\x{10876}\x{10877}-\x{10878}\x{10879}-\x{1087F}\x{10880}-\x{1089E}\x{1089F}-\x{108A6}\x{108A7}-\x{108AF}\x{108B0}-\x{108DF}\x{108E0}-\x{108F2}\x{108F3}\x{108F4}-\x{108F5}\x{108F6}-\x{108FA}\x{108FB}-\x{108FF}\x{10900}-\x{10915}\x{10916}-\x{1091B}\x{1091C}-\x{1091E}\x{10920}-\x{10939}\x{1093A}-\x{1093E}\x{1093F}\x{10940}-\x{1097F}\x{10980}-\x{109B7}\x{109B8}-\x{109BB}\x{109BC}-\x{109BD}\x{109BE}-\x{109BF}\x{109C0}-\x{109CF}\x{109D0}-\x{109D1}\x{109D2}-\x{109FF}\x{10A00}\x{10A04}\x{10A07}-\x{10A0B}\x{10A10}-\x{10A13}\x{10A14}\x{10A15}-\x{10A17}\x{10A18}\x{10A19}-\x{10A35}\x{10A36}-\x{10A37}\x{10A3B}-\x{10A3E}\x{10A40}-\x{10A48}\x{10A49}-\x{10A4F}\x{10A50}-\x{10A58}\x{10A59}-\x{10A5F}\x{10A60}-\x{10A7C}\x{10A7D}-\x{10A7E}\x{10A7F}\x{10A80}-\x{10A9C}\x{10A9D}-\x{10A9F}\x{10AA0}-\x{10ABF}\x{10AC0}-\x{10AC7}\x{10AC8}\x{10AC9}-\x{10AE4}\x{10AE7}-\x{10AEA}\x{10AEB}-\x{10AEF}\x{10AF0}-\x{10AF6}\x{10AF7}-\x{10AFF}\x{10B00}-\x{10B35}\x{10B36}-\x{10B38}\x{10B40}-\x{10B55}\x{10B56}-\x{10B57}\x{10B58}-\x{10B5F}\x{10B60}-\x{10B72}\x{10B73}-\x{10B77}\x{10B78}-\x{10B7F}\x{10B80}-\x{10B91}\x{10B92}-\x{10B98}\x{10B99}-\x{10B9C}\x{10B9D}-\x{10BA8}\x{10BA9}-\x{10BAF}\x{10BB0}-\x{10BFF}\x{10C00}-\x{10C48}\x{10C49}-\x{10C7F}\x{10C80}-\x{10CB2}\x{10CB3}-\x{10CBF}\x{10CC0}-\x{10CF2}\x{10CF3}-\x{10CF9}\x{10CFA}-\x{10CFF}\x{10D00}-\x{10D23}\x{10D28}-\x{10D2F}\x{10D30}-\x{10D39}\x{10D3A}-\x{10D3F}\x{10D40}-\x{10E5F}\x{10E60}-\x{10E7E}\x{10E7F}\x{10E80}-\x{10EA9}\x{10EAA}\x{10EAD}\x{10EAE}-\x{10EAF}\x{10EB0}-\x{10EB1}\x{10EB2}-\x{10EFF}\x{10F00}-\x{10F1C}\x{10F1D}-\x{10F26}\x{10F27}\x{10F28}-\x{10F2F}\x{10F30}-\x{10F45}\x{10F51}-\x{10F54}\x{10F55}-\x{10F59}\x{10F5A}-\x{10F6F}\x{10F70}-\x{10FAF}\x{10FB0}-\x{10FC4}\x{10FC5}-\x{10FCB}\x{10FCC}-\x{10FDF}\x{10FE0}-\x{10FF6}\x{10FF7}-\x{10FFF}\x{1E800}-\x{1E8C4}\x{1E8C5}-\x{1E8C6}\x{1E8C7}-\x{1E8CF}\x{1E8D7}-\x{1E8FF}\x{1E900}-\x{1E943}\x{1E94B}\x{1E94C}-\x{1E94F}\x{1E950}-\x{1E959}\x{1E95A}-\x{1E95D}\x{1E95E}-\x{1E95F}\x{1E960}-\x{1EC6F}\x{1EC70}\x{1EC71}-\x{1ECAB}\x{1ECAC}\x{1ECAD}-\x{1ECAF}\x{1ECB0}\x{1ECB1}-\x{1ECB4}\x{1ECB5}-\x{1ECBF}\x{1ECC0}-\x{1ECFF}\x{1ED00}\x{1ED01}-\x{1ED2D}\x{1ED2E}\x{1ED2F}-\x{1ED3D}\x{1ED3E}-\x{1ED4F}\x{1ED50}-\x{1EDFF}\x{1EE00}-\x{1EE03}\x{1EE04}\x{1EE05}-\x{1EE1F}\x{1EE20}\x{1EE21}-\x{1EE22}\x{1EE23}\x{1EE24}\x{1EE25}-\x{1EE26}\x{1EE27}\x{1EE28}\x{1EE29}-\x{1EE32}\x{1EE33}\x{1EE34}-\x{1EE37}\x{1EE38}\x{1EE39}\x{1EE3A}\x{1EE3B}\x{1EE3C}-\x{1EE41}\x{1EE42}\x{1EE43}-\x{1EE46}\x{1EE47}\x{1EE48}\x{1EE49}\x{1EE4A}\x{1EE4B}\x{1EE4C}\x{1EE4D}-\x{1EE4F}\x{1EE50}\x{1EE51}-\x{1EE52}\x{1EE53}\x{1EE54}\x{1EE55}-\x{1EE56}\x{1EE57}\x{1EE58}\x{1EE59}\x{1EE5A}\x{1EE5B}\x{1EE5C}\x{1EE5D}\x{1EE5E}\x{1EE5F}\x{1EE60}\x{1EE61}-\x{1EE62}\x{1EE63}\x{1EE64}\x{1EE65}-\x{1EE66}\x{1EE67}-\x{1EE6A}\x{1EE6B}\x{1EE6C}-\x{1EE72}\x{1EE73}\x{1EE74}-\x{1EE77}\x{1EE78}\x{1EE79}-\x{1EE7C}\x{1EE7D}\x{1EE7E}\x{1EE7F}\x{1EE80}-\x{1EE89}\x{1EE8A}\x{1EE8B}-\x{1EE9B}\x{1EE9C}-\x{1EEA0}\x{1EEA1}-\x{1EEA3}\x{1EEA4}\x{1EEA5}-\x{1EEA9}\x{1EEAA}\x{1EEAB}-\x{1EEBB}\x{1EEBC}-\x{1EEEF}\x{1EEF2}-\x{1EEFF}\x{1EF00}-\x{1EFFF}]/u'; + + const BIDI_STEP_1_LTR = '/^[^\x{0000}-\x{0008}\x{0009}\x{000A}\x{000B}\x{000C}\x{000D}\x{000E}-\x{001B}\x{001C}-\x{001E}\x{001F}\x{0020}\x{0021}-\x{0022}\x{0023}\x{0024}\x{0025}\x{0026}-\x{0027}\x{0028}\x{0029}\x{002A}\x{002B}\x{002C}\x{002D}\x{002E}-\x{002F}\x{0030}-\x{0039}\x{003A}\x{003B}\x{003C}-\x{003E}\x{003F}-\x{0040}\x{005B}\x{005C}\x{005D}\x{005E}\x{005F}\x{0060}\x{007B}\x{007C}\x{007D}\x{007E}\x{007F}-\x{0084}\x{0085}\x{0086}-\x{009F}\x{00A0}\x{00A1}\x{00A2}-\x{00A5}\x{00A6}\x{00A7}\x{00A8}\x{00A9}\x{00AB}\x{00AC}\x{00AD}\x{00AE}\x{00AF}\x{00B0}\x{00B1}\x{00B2}-\x{00B3}\x{00B4}\x{00B6}-\x{00B7}\x{00B8}\x{00B9}\x{00BB}\x{00BC}-\x{00BE}\x{00BF}\x{00D7}\x{00F7}\x{02B9}-\x{02BA}\x{02C2}-\x{02C5}\x{02C6}-\x{02CF}\x{02D2}-\x{02DF}\x{02E5}-\x{02EB}\x{02EC}\x{02ED}\x{02EF}-\x{02FF}\x{0300}-\x{036F}\x{0374}\x{0375}\x{037E}\x{0384}-\x{0385}\x{0387}\x{03F6}\x{0483}-\x{0487}\x{0488}-\x{0489}\x{058A}\x{058D}-\x{058E}\x{058F}\x{0590}\x{0591}-\x{05BD}\x{05BE}\x{05BF}\x{05C0}\x{05C1}-\x{05C2}\x{05C3}\x{05C4}-\x{05C5}\x{05C6}\x{05C7}\x{05C8}-\x{05CF}\x{05D0}-\x{05EA}\x{05EB}-\x{05EE}\x{05EF}-\x{05F2}\x{05F3}-\x{05F4}\x{05F5}-\x{05FF}\x{0600}-\x{0605}\x{0606}-\x{0607}\x{0608}\x{0609}-\x{060A}\x{060B}\x{060C}\x{060D}\x{060E}-\x{060F}\x{0610}-\x{061A}\x{061B}\x{061C}\x{061D}\x{061E}-\x{061F}\x{0620}-\x{063F}\x{0640}\x{0641}-\x{064A}\x{064B}-\x{065F}\x{0660}-\x{0669}\x{066A}\x{066B}-\x{066C}\x{066D}\x{066E}-\x{066F}\x{0670}\x{0671}-\x{06D3}\x{06D4}\x{06D5}\x{06D6}-\x{06DC}\x{06DD}\x{06DE}\x{06DF}-\x{06E4}\x{06E5}-\x{06E6}\x{06E7}-\x{06E8}\x{06E9}\x{06EA}-\x{06ED}\x{06EE}-\x{06EF}\x{06F0}-\x{06F9}\x{06FA}-\x{06FC}\x{06FD}-\x{06FE}\x{06FF}\x{0700}-\x{070D}\x{070E}\x{070F}\x{0710}\x{0711}\x{0712}-\x{072F}\x{0730}-\x{074A}\x{074B}-\x{074C}\x{074D}-\x{07A5}\x{07A6}-\x{07B0}\x{07B1}\x{07B2}-\x{07BF}\x{07C0}-\x{07C9}\x{07CA}-\x{07EA}\x{07EB}-\x{07F3}\x{07F4}-\x{07F5}\x{07F6}\x{07F7}-\x{07F9}\x{07FA}\x{07FB}-\x{07FC}\x{07FD}\x{07FE}-\x{07FF}\x{0800}-\x{0815}\x{0816}-\x{0819}\x{081A}\x{081B}-\x{0823}\x{0824}\x{0825}-\x{0827}\x{0828}\x{0829}-\x{082D}\x{082E}-\x{082F}\x{0830}-\x{083E}\x{083F}\x{0840}-\x{0858}\x{0859}-\x{085B}\x{085C}-\x{085D}\x{085E}\x{085F}\x{0860}-\x{086A}\x{086B}-\x{086F}\x{0870}-\x{089F}\x{08A0}-\x{08B4}\x{08B5}\x{08B6}-\x{08C7}\x{08C8}-\x{08D2}\x{08D3}-\x{08E1}\x{08E2}\x{08E3}-\x{0902}\x{093A}\x{093C}\x{0941}-\x{0948}\x{094D}\x{0951}-\x{0957}\x{0962}-\x{0963}\x{0981}\x{09BC}\x{09C1}-\x{09C4}\x{09CD}\x{09E2}-\x{09E3}\x{09F2}-\x{09F3}\x{09FB}\x{09FE}\x{0A01}-\x{0A02}\x{0A3C}\x{0A41}-\x{0A42}\x{0A47}-\x{0A48}\x{0A4B}-\x{0A4D}\x{0A51}\x{0A70}-\x{0A71}\x{0A75}\x{0A81}-\x{0A82}\x{0ABC}\x{0AC1}-\x{0AC5}\x{0AC7}-\x{0AC8}\x{0ACD}\x{0AE2}-\x{0AE3}\x{0AF1}\x{0AFA}-\x{0AFF}\x{0B01}\x{0B3C}\x{0B3F}\x{0B41}-\x{0B44}\x{0B4D}\x{0B55}-\x{0B56}\x{0B62}-\x{0B63}\x{0B82}\x{0BC0}\x{0BCD}\x{0BF3}-\x{0BF8}\x{0BF9}\x{0BFA}\x{0C00}\x{0C04}\x{0C3E}-\x{0C40}\x{0C46}-\x{0C48}\x{0C4A}-\x{0C4D}\x{0C55}-\x{0C56}\x{0C62}-\x{0C63}\x{0C78}-\x{0C7E}\x{0C81}\x{0CBC}\x{0CCC}-\x{0CCD}\x{0CE2}-\x{0CE3}\x{0D00}-\x{0D01}\x{0D3B}-\x{0D3C}\x{0D41}-\x{0D44}\x{0D4D}\x{0D62}-\x{0D63}\x{0D81}\x{0DCA}\x{0DD2}-\x{0DD4}\x{0DD6}\x{0E31}\x{0E34}-\x{0E3A}\x{0E3F}\x{0E47}-\x{0E4E}\x{0EB1}\x{0EB4}-\x{0EBC}\x{0EC8}-\x{0ECD}\x{0F18}-\x{0F19}\x{0F35}\x{0F37}\x{0F39}\x{0F3A}\x{0F3B}\x{0F3C}\x{0F3D}\x{0F71}-\x{0F7E}\x{0F80}-\x{0F84}\x{0F86}-\x{0F87}\x{0F8D}-\x{0F97}\x{0F99}-\x{0FBC}\x{0FC6}\x{102D}-\x{1030}\x{1032}-\x{1037}\x{1039}-\x{103A}\x{103D}-\x{103E}\x{1058}-\x{1059}\x{105E}-\x{1060}\x{1071}-\x{1074}\x{1082}\x{1085}-\x{1086}\x{108D}\x{109D}\x{135D}-\x{135F}\x{1390}-\x{1399}\x{1400}\x{1680}\x{169B}\x{169C}\x{1712}-\x{1714}\x{1732}-\x{1734}\x{1752}-\x{1753}\x{1772}-\x{1773}\x{17B4}-\x{17B5}\x{17B7}-\x{17BD}\x{17C6}\x{17C9}-\x{17D3}\x{17DB}\x{17DD}\x{17F0}-\x{17F9}\x{1800}-\x{1805}\x{1806}\x{1807}-\x{180A}\x{180B}-\x{180D}\x{180E}\x{1885}-\x{1886}\x{18A9}\x{1920}-\x{1922}\x{1927}-\x{1928}\x{1932}\x{1939}-\x{193B}\x{1940}\x{1944}-\x{1945}\x{19DE}-\x{19FF}\x{1A17}-\x{1A18}\x{1A1B}\x{1A56}\x{1A58}-\x{1A5E}\x{1A60}\x{1A62}\x{1A65}-\x{1A6C}\x{1A73}-\x{1A7C}\x{1A7F}\x{1AB0}-\x{1ABD}\x{1ABE}\x{1ABF}-\x{1AC0}\x{1B00}-\x{1B03}\x{1B34}\x{1B36}-\x{1B3A}\x{1B3C}\x{1B42}\x{1B6B}-\x{1B73}\x{1B80}-\x{1B81}\x{1BA2}-\x{1BA5}\x{1BA8}-\x{1BA9}\x{1BAB}-\x{1BAD}\x{1BE6}\x{1BE8}-\x{1BE9}\x{1BED}\x{1BEF}-\x{1BF1}\x{1C2C}-\x{1C33}\x{1C36}-\x{1C37}\x{1CD0}-\x{1CD2}\x{1CD4}-\x{1CE0}\x{1CE2}-\x{1CE8}\x{1CED}\x{1CF4}\x{1CF8}-\x{1CF9}\x{1DC0}-\x{1DF9}\x{1DFB}-\x{1DFF}\x{1FBD}\x{1FBF}-\x{1FC1}\x{1FCD}-\x{1FCF}\x{1FDD}-\x{1FDF}\x{1FED}-\x{1FEF}\x{1FFD}-\x{1FFE}\x{2000}-\x{200A}\x{200B}-\x{200D}\x{200F}\x{2010}-\x{2015}\x{2016}-\x{2017}\x{2018}\x{2019}\x{201A}\x{201B}-\x{201C}\x{201D}\x{201E}\x{201F}\x{2020}-\x{2027}\x{2028}\x{2029}\x{202A}\x{202B}\x{202C}\x{202D}\x{202E}\x{202F}\x{2030}-\x{2034}\x{2035}-\x{2038}\x{2039}\x{203A}\x{203B}-\x{203E}\x{203F}-\x{2040}\x{2041}-\x{2043}\x{2044}\x{2045}\x{2046}\x{2047}-\x{2051}\x{2052}\x{2053}\x{2054}\x{2055}-\x{205E}\x{205F}\x{2060}-\x{2064}\x{2065}\x{2066}\x{2067}\x{2068}\x{2069}\x{206A}-\x{206F}\x{2070}\x{2074}-\x{2079}\x{207A}-\x{207B}\x{207C}\x{207D}\x{207E}\x{2080}-\x{2089}\x{208A}-\x{208B}\x{208C}\x{208D}\x{208E}\x{20A0}-\x{20BF}\x{20C0}-\x{20CF}\x{20D0}-\x{20DC}\x{20DD}-\x{20E0}\x{20E1}\x{20E2}-\x{20E4}\x{20E5}-\x{20F0}\x{2100}-\x{2101}\x{2103}-\x{2106}\x{2108}-\x{2109}\x{2114}\x{2116}-\x{2117}\x{2118}\x{211E}-\x{2123}\x{2125}\x{2127}\x{2129}\x{212E}\x{213A}-\x{213B}\x{2140}-\x{2144}\x{214A}\x{214B}\x{214C}-\x{214D}\x{2150}-\x{215F}\x{2189}\x{218A}-\x{218B}\x{2190}-\x{2194}\x{2195}-\x{2199}\x{219A}-\x{219B}\x{219C}-\x{219F}\x{21A0}\x{21A1}-\x{21A2}\x{21A3}\x{21A4}-\x{21A5}\x{21A6}\x{21A7}-\x{21AD}\x{21AE}\x{21AF}-\x{21CD}\x{21CE}-\x{21CF}\x{21D0}-\x{21D1}\x{21D2}\x{21D3}\x{21D4}\x{21D5}-\x{21F3}\x{21F4}-\x{2211}\x{2212}\x{2213}\x{2214}-\x{22FF}\x{2300}-\x{2307}\x{2308}\x{2309}\x{230A}\x{230B}\x{230C}-\x{231F}\x{2320}-\x{2321}\x{2322}-\x{2328}\x{2329}\x{232A}\x{232B}-\x{2335}\x{237B}\x{237C}\x{237D}-\x{2394}\x{2396}-\x{239A}\x{239B}-\x{23B3}\x{23B4}-\x{23DB}\x{23DC}-\x{23E1}\x{23E2}-\x{2426}\x{2440}-\x{244A}\x{2460}-\x{2487}\x{2488}-\x{249B}\x{24EA}-\x{24FF}\x{2500}-\x{25B6}\x{25B7}\x{25B8}-\x{25C0}\x{25C1}\x{25C2}-\x{25F7}\x{25F8}-\x{25FF}\x{2600}-\x{266E}\x{266F}\x{2670}-\x{26AB}\x{26AD}-\x{2767}\x{2768}\x{2769}\x{276A}\x{276B}\x{276C}\x{276D}\x{276E}\x{276F}\x{2770}\x{2771}\x{2772}\x{2773}\x{2774}\x{2775}\x{2776}-\x{2793}\x{2794}-\x{27BF}\x{27C0}-\x{27C4}\x{27C5}\x{27C6}\x{27C7}-\x{27E5}\x{27E6}\x{27E7}\x{27E8}\x{27E9}\x{27EA}\x{27EB}\x{27EC}\x{27ED}\x{27EE}\x{27EF}\x{27F0}-\x{27FF}\x{2900}-\x{2982}\x{2983}\x{2984}\x{2985}\x{2986}\x{2987}\x{2988}\x{2989}\x{298A}\x{298B}\x{298C}\x{298D}\x{298E}\x{298F}\x{2990}\x{2991}\x{2992}\x{2993}\x{2994}\x{2995}\x{2996}\x{2997}\x{2998}\x{2999}-\x{29D7}\x{29D8}\x{29D9}\x{29DA}\x{29DB}\x{29DC}-\x{29FB}\x{29FC}\x{29FD}\x{29FE}-\x{2AFF}\x{2B00}-\x{2B2F}\x{2B30}-\x{2B44}\x{2B45}-\x{2B46}\x{2B47}-\x{2B4C}\x{2B4D}-\x{2B73}\x{2B76}-\x{2B95}\x{2B97}-\x{2BFF}\x{2CE5}-\x{2CEA}\x{2CEF}-\x{2CF1}\x{2CF9}-\x{2CFC}\x{2CFD}\x{2CFE}-\x{2CFF}\x{2D7F}\x{2DE0}-\x{2DFF}\x{2E00}-\x{2E01}\x{2E02}\x{2E03}\x{2E04}\x{2E05}\x{2E06}-\x{2E08}\x{2E09}\x{2E0A}\x{2E0B}\x{2E0C}\x{2E0D}\x{2E0E}-\x{2E16}\x{2E17}\x{2E18}-\x{2E19}\x{2E1A}\x{2E1B}\x{2E1C}\x{2E1D}\x{2E1E}-\x{2E1F}\x{2E20}\x{2E21}\x{2E22}\x{2E23}\x{2E24}\x{2E25}\x{2E26}\x{2E27}\x{2E28}\x{2E29}\x{2E2A}-\x{2E2E}\x{2E2F}\x{2E30}-\x{2E39}\x{2E3A}-\x{2E3B}\x{2E3C}-\x{2E3F}\x{2E40}\x{2E41}\x{2E42}\x{2E43}-\x{2E4F}\x{2E50}-\x{2E51}\x{2E52}\x{2E80}-\x{2E99}\x{2E9B}-\x{2EF3}\x{2F00}-\x{2FD5}\x{2FF0}-\x{2FFB}\x{3000}\x{3001}-\x{3003}\x{3004}\x{3008}\x{3009}\x{300A}\x{300B}\x{300C}\x{300D}\x{300E}\x{300F}\x{3010}\x{3011}\x{3012}-\x{3013}\x{3014}\x{3015}\x{3016}\x{3017}\x{3018}\x{3019}\x{301A}\x{301B}\x{301C}\x{301D}\x{301E}-\x{301F}\x{3020}\x{302A}-\x{302D}\x{3030}\x{3036}-\x{3037}\x{303D}\x{303E}-\x{303F}\x{3099}-\x{309A}\x{309B}-\x{309C}\x{30A0}\x{30FB}\x{31C0}-\x{31E3}\x{321D}-\x{321E}\x{3250}\x{3251}-\x{325F}\x{327C}-\x{327E}\x{32B1}-\x{32BF}\x{32CC}-\x{32CF}\x{3377}-\x{337A}\x{33DE}-\x{33DF}\x{33FF}\x{4DC0}-\x{4DFF}\x{A490}-\x{A4C6}\x{A60D}-\x{A60F}\x{A66F}\x{A670}-\x{A672}\x{A673}\x{A674}-\x{A67D}\x{A67E}\x{A67F}\x{A69E}-\x{A69F}\x{A6F0}-\x{A6F1}\x{A700}-\x{A716}\x{A717}-\x{A71F}\x{A720}-\x{A721}\x{A788}\x{A802}\x{A806}\x{A80B}\x{A825}-\x{A826}\x{A828}-\x{A82B}\x{A82C}\x{A838}\x{A839}\x{A874}-\x{A877}\x{A8C4}-\x{A8C5}\x{A8E0}-\x{A8F1}\x{A8FF}\x{A926}-\x{A92D}\x{A947}-\x{A951}\x{A980}-\x{A982}\x{A9B3}\x{A9B6}-\x{A9B9}\x{A9BC}-\x{A9BD}\x{A9E5}\x{AA29}-\x{AA2E}\x{AA31}-\x{AA32}\x{AA35}-\x{AA36}\x{AA43}\x{AA4C}\x{AA7C}\x{AAB0}\x{AAB2}-\x{AAB4}\x{AAB7}-\x{AAB8}\x{AABE}-\x{AABF}\x{AAC1}\x{AAEC}-\x{AAED}\x{AAF6}\x{AB6A}-\x{AB6B}\x{ABE5}\x{ABE8}\x{ABED}\x{FB1D}\x{FB1E}\x{FB1F}-\x{FB28}\x{FB29}\x{FB2A}-\x{FB36}\x{FB37}\x{FB38}-\x{FB3C}\x{FB3D}\x{FB3E}\x{FB3F}\x{FB40}-\x{FB41}\x{FB42}\x{FB43}-\x{FB44}\x{FB45}\x{FB46}-\x{FB4F}\x{FB50}-\x{FBB1}\x{FBB2}-\x{FBC1}\x{FBC2}-\x{FBD2}\x{FBD3}-\x{FD3D}\x{FD3E}\x{FD3F}\x{FD40}-\x{FD4F}\x{FD50}-\x{FD8F}\x{FD90}-\x{FD91}\x{FD92}-\x{FDC7}\x{FDC8}-\x{FDCF}\x{FDD0}-\x{FDEF}\x{FDF0}-\x{FDFB}\x{FDFC}\x{FDFD}\x{FDFE}-\x{FDFF}\x{FE00}-\x{FE0F}\x{FE10}-\x{FE16}\x{FE17}\x{FE18}\x{FE19}\x{FE20}-\x{FE2F}\x{FE30}\x{FE31}-\x{FE32}\x{FE33}-\x{FE34}\x{FE35}\x{FE36}\x{FE37}\x{FE38}\x{FE39}\x{FE3A}\x{FE3B}\x{FE3C}\x{FE3D}\x{FE3E}\x{FE3F}\x{FE40}\x{FE41}\x{FE42}\x{FE43}\x{FE44}\x{FE45}-\x{FE46}\x{FE47}\x{FE48}\x{FE49}-\x{FE4C}\x{FE4D}-\x{FE4F}\x{FE50}\x{FE51}\x{FE52}\x{FE54}\x{FE55}\x{FE56}-\x{FE57}\x{FE58}\x{FE59}\x{FE5A}\x{FE5B}\x{FE5C}\x{FE5D}\x{FE5E}\x{FE5F}\x{FE60}-\x{FE61}\x{FE62}\x{FE63}\x{FE64}-\x{FE66}\x{FE68}\x{FE69}\x{FE6A}\x{FE6B}\x{FE70}-\x{FE74}\x{FE75}\x{FE76}-\x{FEFC}\x{FEFD}-\x{FEFE}\x{FEFF}\x{FF01}-\x{FF02}\x{FF03}\x{FF04}\x{FF05}\x{FF06}-\x{FF07}\x{FF08}\x{FF09}\x{FF0A}\x{FF0B}\x{FF0C}\x{FF0D}\x{FF0E}-\x{FF0F}\x{FF10}-\x{FF19}\x{FF1A}\x{FF1B}\x{FF1C}-\x{FF1E}\x{FF1F}-\x{FF20}\x{FF3B}\x{FF3C}\x{FF3D}\x{FF3E}\x{FF3F}\x{FF40}\x{FF5B}\x{FF5C}\x{FF5D}\x{FF5E}\x{FF5F}\x{FF60}\x{FF61}\x{FF62}\x{FF63}\x{FF64}-\x{FF65}\x{FFE0}-\x{FFE1}\x{FFE2}\x{FFE3}\x{FFE4}\x{FFE5}-\x{FFE6}\x{FFE8}\x{FFE9}-\x{FFEC}\x{FFED}-\x{FFEE}\x{FFF0}-\x{FFF8}\x{FFF9}-\x{FFFB}\x{FFFC}-\x{FFFD}\x{FFFE}-\x{FFFF}\x{10101}\x{10140}-\x{10174}\x{10175}-\x{10178}\x{10179}-\x{10189}\x{1018A}-\x{1018B}\x{1018C}\x{10190}-\x{1019C}\x{101A0}\x{101FD}\x{102E0}\x{102E1}-\x{102FB}\x{10376}-\x{1037A}\x{10800}-\x{10805}\x{10806}-\x{10807}\x{10808}\x{10809}\x{1080A}-\x{10835}\x{10836}\x{10837}-\x{10838}\x{10839}-\x{1083B}\x{1083C}\x{1083D}-\x{1083E}\x{1083F}-\x{10855}\x{10856}\x{10857}\x{10858}-\x{1085F}\x{10860}-\x{10876}\x{10877}-\x{10878}\x{10879}-\x{1087F}\x{10880}-\x{1089E}\x{1089F}-\x{108A6}\x{108A7}-\x{108AF}\x{108B0}-\x{108DF}\x{108E0}-\x{108F2}\x{108F3}\x{108F4}-\x{108F5}\x{108F6}-\x{108FA}\x{108FB}-\x{108FF}\x{10900}-\x{10915}\x{10916}-\x{1091B}\x{1091C}-\x{1091E}\x{1091F}\x{10920}-\x{10939}\x{1093A}-\x{1093E}\x{1093F}\x{10940}-\x{1097F}\x{10980}-\x{109B7}\x{109B8}-\x{109BB}\x{109BC}-\x{109BD}\x{109BE}-\x{109BF}\x{109C0}-\x{109CF}\x{109D0}-\x{109D1}\x{109D2}-\x{109FF}\x{10A00}\x{10A01}-\x{10A03}\x{10A04}\x{10A05}-\x{10A06}\x{10A07}-\x{10A0B}\x{10A0C}-\x{10A0F}\x{10A10}-\x{10A13}\x{10A14}\x{10A15}-\x{10A17}\x{10A18}\x{10A19}-\x{10A35}\x{10A36}-\x{10A37}\x{10A38}-\x{10A3A}\x{10A3B}-\x{10A3E}\x{10A3F}\x{10A40}-\x{10A48}\x{10A49}-\x{10A4F}\x{10A50}-\x{10A58}\x{10A59}-\x{10A5F}\x{10A60}-\x{10A7C}\x{10A7D}-\x{10A7E}\x{10A7F}\x{10A80}-\x{10A9C}\x{10A9D}-\x{10A9F}\x{10AA0}-\x{10ABF}\x{10AC0}-\x{10AC7}\x{10AC8}\x{10AC9}-\x{10AE4}\x{10AE5}-\x{10AE6}\x{10AE7}-\x{10AEA}\x{10AEB}-\x{10AEF}\x{10AF0}-\x{10AF6}\x{10AF7}-\x{10AFF}\x{10B00}-\x{10B35}\x{10B36}-\x{10B38}\x{10B39}-\x{10B3F}\x{10B40}-\x{10B55}\x{10B56}-\x{10B57}\x{10B58}-\x{10B5F}\x{10B60}-\x{10B72}\x{10B73}-\x{10B77}\x{10B78}-\x{10B7F}\x{10B80}-\x{10B91}\x{10B92}-\x{10B98}\x{10B99}-\x{10B9C}\x{10B9D}-\x{10BA8}\x{10BA9}-\x{10BAF}\x{10BB0}-\x{10BFF}\x{10C00}-\x{10C48}\x{10C49}-\x{10C7F}\x{10C80}-\x{10CB2}\x{10CB3}-\x{10CBF}\x{10CC0}-\x{10CF2}\x{10CF3}-\x{10CF9}\x{10CFA}-\x{10CFF}\x{10D00}-\x{10D23}\x{10D24}-\x{10D27}\x{10D28}-\x{10D2F}\x{10D30}-\x{10D39}\x{10D3A}-\x{10D3F}\x{10D40}-\x{10E5F}\x{10E60}-\x{10E7E}\x{10E7F}\x{10E80}-\x{10EA9}\x{10EAA}\x{10EAB}-\x{10EAC}\x{10EAD}\x{10EAE}-\x{10EAF}\x{10EB0}-\x{10EB1}\x{10EB2}-\x{10EFF}\x{10F00}-\x{10F1C}\x{10F1D}-\x{10F26}\x{10F27}\x{10F28}-\x{10F2F}\x{10F30}-\x{10F45}\x{10F46}-\x{10F50}\x{10F51}-\x{10F54}\x{10F55}-\x{10F59}\x{10F5A}-\x{10F6F}\x{10F70}-\x{10FAF}\x{10FB0}-\x{10FC4}\x{10FC5}-\x{10FCB}\x{10FCC}-\x{10FDF}\x{10FE0}-\x{10FF6}\x{10FF7}-\x{10FFF}\x{11001}\x{11038}-\x{11046}\x{11052}-\x{11065}\x{1107F}-\x{11081}\x{110B3}-\x{110B6}\x{110B9}-\x{110BA}\x{11100}-\x{11102}\x{11127}-\x{1112B}\x{1112D}-\x{11134}\x{11173}\x{11180}-\x{11181}\x{111B6}-\x{111BE}\x{111C9}-\x{111CC}\x{111CF}\x{1122F}-\x{11231}\x{11234}\x{11236}-\x{11237}\x{1123E}\x{112DF}\x{112E3}-\x{112EA}\x{11300}-\x{11301}\x{1133B}-\x{1133C}\x{11340}\x{11366}-\x{1136C}\x{11370}-\x{11374}\x{11438}-\x{1143F}\x{11442}-\x{11444}\x{11446}\x{1145E}\x{114B3}-\x{114B8}\x{114BA}\x{114BF}-\x{114C0}\x{114C2}-\x{114C3}\x{115B2}-\x{115B5}\x{115BC}-\x{115BD}\x{115BF}-\x{115C0}\x{115DC}-\x{115DD}\x{11633}-\x{1163A}\x{1163D}\x{1163F}-\x{11640}\x{11660}-\x{1166C}\x{116AB}\x{116AD}\x{116B0}-\x{116B5}\x{116B7}\x{1171D}-\x{1171F}\x{11722}-\x{11725}\x{11727}-\x{1172B}\x{1182F}-\x{11837}\x{11839}-\x{1183A}\x{1193B}-\x{1193C}\x{1193E}\x{11943}\x{119D4}-\x{119D7}\x{119DA}-\x{119DB}\x{119E0}\x{11A01}-\x{11A06}\x{11A09}-\x{11A0A}\x{11A33}-\x{11A38}\x{11A3B}-\x{11A3E}\x{11A47}\x{11A51}-\x{11A56}\x{11A59}-\x{11A5B}\x{11A8A}-\x{11A96}\x{11A98}-\x{11A99}\x{11C30}-\x{11C36}\x{11C38}-\x{11C3D}\x{11C92}-\x{11CA7}\x{11CAA}-\x{11CB0}\x{11CB2}-\x{11CB3}\x{11CB5}-\x{11CB6}\x{11D31}-\x{11D36}\x{11D3A}\x{11D3C}-\x{11D3D}\x{11D3F}-\x{11D45}\x{11D47}\x{11D90}-\x{11D91}\x{11D95}\x{11D97}\x{11EF3}-\x{11EF4}\x{11FD5}-\x{11FDC}\x{11FDD}-\x{11FE0}\x{11FE1}-\x{11FF1}\x{16AF0}-\x{16AF4}\x{16B30}-\x{16B36}\x{16F4F}\x{16F8F}-\x{16F92}\x{16FE2}\x{16FE4}\x{1BC9D}-\x{1BC9E}\x{1BCA0}-\x{1BCA3}\x{1D167}-\x{1D169}\x{1D173}-\x{1D17A}\x{1D17B}-\x{1D182}\x{1D185}-\x{1D18B}\x{1D1AA}-\x{1D1AD}\x{1D200}-\x{1D241}\x{1D242}-\x{1D244}\x{1D245}\x{1D300}-\x{1D356}\x{1D6DB}\x{1D715}\x{1D74F}\x{1D789}\x{1D7C3}\x{1D7CE}-\x{1D7FF}\x{1DA00}-\x{1DA36}\x{1DA3B}-\x{1DA6C}\x{1DA75}\x{1DA84}\x{1DA9B}-\x{1DA9F}\x{1DAA1}-\x{1DAAF}\x{1E000}-\x{1E006}\x{1E008}-\x{1E018}\x{1E01B}-\x{1E021}\x{1E023}-\x{1E024}\x{1E026}-\x{1E02A}\x{1E130}-\x{1E136}\x{1E2EC}-\x{1E2EF}\x{1E2FF}\x{1E800}-\x{1E8C4}\x{1E8C5}-\x{1E8C6}\x{1E8C7}-\x{1E8CF}\x{1E8D0}-\x{1E8D6}\x{1E8D7}-\x{1E8FF}\x{1E900}-\x{1E943}\x{1E944}-\x{1E94A}\x{1E94B}\x{1E94C}-\x{1E94F}\x{1E950}-\x{1E959}\x{1E95A}-\x{1E95D}\x{1E95E}-\x{1E95F}\x{1E960}-\x{1EC6F}\x{1EC70}\x{1EC71}-\x{1ECAB}\x{1ECAC}\x{1ECAD}-\x{1ECAF}\x{1ECB0}\x{1ECB1}-\x{1ECB4}\x{1ECB5}-\x{1ECBF}\x{1ECC0}-\x{1ECFF}\x{1ED00}\x{1ED01}-\x{1ED2D}\x{1ED2E}\x{1ED2F}-\x{1ED3D}\x{1ED3E}-\x{1ED4F}\x{1ED50}-\x{1EDFF}\x{1EE00}-\x{1EE03}\x{1EE04}\x{1EE05}-\x{1EE1F}\x{1EE20}\x{1EE21}-\x{1EE22}\x{1EE23}\x{1EE24}\x{1EE25}-\x{1EE26}\x{1EE27}\x{1EE28}\x{1EE29}-\x{1EE32}\x{1EE33}\x{1EE34}-\x{1EE37}\x{1EE38}\x{1EE39}\x{1EE3A}\x{1EE3B}\x{1EE3C}-\x{1EE41}\x{1EE42}\x{1EE43}-\x{1EE46}\x{1EE47}\x{1EE48}\x{1EE49}\x{1EE4A}\x{1EE4B}\x{1EE4C}\x{1EE4D}-\x{1EE4F}\x{1EE50}\x{1EE51}-\x{1EE52}\x{1EE53}\x{1EE54}\x{1EE55}-\x{1EE56}\x{1EE57}\x{1EE58}\x{1EE59}\x{1EE5A}\x{1EE5B}\x{1EE5C}\x{1EE5D}\x{1EE5E}\x{1EE5F}\x{1EE60}\x{1EE61}-\x{1EE62}\x{1EE63}\x{1EE64}\x{1EE65}-\x{1EE66}\x{1EE67}-\x{1EE6A}\x{1EE6B}\x{1EE6C}-\x{1EE72}\x{1EE73}\x{1EE74}-\x{1EE77}\x{1EE78}\x{1EE79}-\x{1EE7C}\x{1EE7D}\x{1EE7E}\x{1EE7F}\x{1EE80}-\x{1EE89}\x{1EE8A}\x{1EE8B}-\x{1EE9B}\x{1EE9C}-\x{1EEA0}\x{1EEA1}-\x{1EEA3}\x{1EEA4}\x{1EEA5}-\x{1EEA9}\x{1EEAA}\x{1EEAB}-\x{1EEBB}\x{1EEBC}-\x{1EEEF}\x{1EEF0}-\x{1EEF1}\x{1EEF2}-\x{1EEFF}\x{1EF00}-\x{1EFFF}\x{1F000}-\x{1F02B}\x{1F030}-\x{1F093}\x{1F0A0}-\x{1F0AE}\x{1F0B1}-\x{1F0BF}\x{1F0C1}-\x{1F0CF}\x{1F0D1}-\x{1F0F5}\x{1F100}-\x{1F10A}\x{1F10B}-\x{1F10C}\x{1F10D}-\x{1F10F}\x{1F12F}\x{1F16A}-\x{1F16F}\x{1F1AD}\x{1F260}-\x{1F265}\x{1F300}-\x{1F3FA}\x{1F3FB}-\x{1F3FF}\x{1F400}-\x{1F6D7}\x{1F6E0}-\x{1F6EC}\x{1F6F0}-\x{1F6FC}\x{1F700}-\x{1F773}\x{1F780}-\x{1F7D8}\x{1F7E0}-\x{1F7EB}\x{1F800}-\x{1F80B}\x{1F810}-\x{1F847}\x{1F850}-\x{1F859}\x{1F860}-\x{1F887}\x{1F890}-\x{1F8AD}\x{1F8B0}-\x{1F8B1}\x{1F900}-\x{1F978}\x{1F97A}-\x{1F9CB}\x{1F9CD}-\x{1FA53}\x{1FA60}-\x{1FA6D}\x{1FA70}-\x{1FA74}\x{1FA78}-\x{1FA7A}\x{1FA80}-\x{1FA86}\x{1FA90}-\x{1FAA8}\x{1FAB0}-\x{1FAB6}\x{1FAC0}-\x{1FAC2}\x{1FAD0}-\x{1FAD6}\x{1FB00}-\x{1FB92}\x{1FB94}-\x{1FBCA}\x{1FBF0}-\x{1FBF9}\x{1FFFE}-\x{1FFFF}\x{2FFFE}-\x{2FFFF}\x{3FFFE}-\x{3FFFF}\x{4FFFE}-\x{4FFFF}\x{5FFFE}-\x{5FFFF}\x{6FFFE}-\x{6FFFF}\x{7FFFE}-\x{7FFFF}\x{8FFFE}-\x{8FFFF}\x{9FFFE}-\x{9FFFF}\x{AFFFE}-\x{AFFFF}\x{BFFFE}-\x{BFFFF}\x{CFFFE}-\x{CFFFF}\x{DFFFE}-\x{E0000}\x{E0001}\x{E0002}-\x{E001F}\x{E0020}-\x{E007F}\x{E0080}-\x{E00FF}\x{E0100}-\x{E01EF}\x{E01F0}-\x{E0FFF}\x{EFFFE}-\x{EFFFF}\x{FFFFE}-\x{FFFFF}\x{10FFFE}-\x{10FFFF}]/u'; + const BIDI_STEP_1_RTL = '/^[\x{0590}\x{05BE}\x{05C0}\x{05C3}\x{05C6}\x{05C8}-\x{05CF}\x{05D0}-\x{05EA}\x{05EB}-\x{05EE}\x{05EF}-\x{05F2}\x{05F3}-\x{05F4}\x{05F5}-\x{05FF}\x{0608}\x{060B}\x{060D}\x{061B}\x{061C}\x{061D}\x{061E}-\x{061F}\x{0620}-\x{063F}\x{0640}\x{0641}-\x{064A}\x{066D}\x{066E}-\x{066F}\x{0671}-\x{06D3}\x{06D4}\x{06D5}\x{06E5}-\x{06E6}\x{06EE}-\x{06EF}\x{06FA}-\x{06FC}\x{06FD}-\x{06FE}\x{06FF}\x{0700}-\x{070D}\x{070E}\x{070F}\x{0710}\x{0712}-\x{072F}\x{074B}-\x{074C}\x{074D}-\x{07A5}\x{07B1}\x{07B2}-\x{07BF}\x{07C0}-\x{07C9}\x{07CA}-\x{07EA}\x{07F4}-\x{07F5}\x{07FA}\x{07FB}-\x{07FC}\x{07FE}-\x{07FF}\x{0800}-\x{0815}\x{081A}\x{0824}\x{0828}\x{082E}-\x{082F}\x{0830}-\x{083E}\x{083F}\x{0840}-\x{0858}\x{085C}-\x{085D}\x{085E}\x{085F}\x{0860}-\x{086A}\x{086B}-\x{086F}\x{0870}-\x{089F}\x{08A0}-\x{08B4}\x{08B5}\x{08B6}-\x{08C7}\x{08C8}-\x{08D2}\x{200F}\x{FB1D}\x{FB1F}-\x{FB28}\x{FB2A}-\x{FB36}\x{FB37}\x{FB38}-\x{FB3C}\x{FB3D}\x{FB3E}\x{FB3F}\x{FB40}-\x{FB41}\x{FB42}\x{FB43}-\x{FB44}\x{FB45}\x{FB46}-\x{FB4F}\x{FB50}-\x{FBB1}\x{FBB2}-\x{FBC1}\x{FBC2}-\x{FBD2}\x{FBD3}-\x{FD3D}\x{FD40}-\x{FD4F}\x{FD50}-\x{FD8F}\x{FD90}-\x{FD91}\x{FD92}-\x{FDC7}\x{FDC8}-\x{FDCF}\x{FDF0}-\x{FDFB}\x{FDFC}\x{FDFE}-\x{FDFF}\x{FE70}-\x{FE74}\x{FE75}\x{FE76}-\x{FEFC}\x{FEFD}-\x{FEFE}\x{10800}-\x{10805}\x{10806}-\x{10807}\x{10808}\x{10809}\x{1080A}-\x{10835}\x{10836}\x{10837}-\x{10838}\x{10839}-\x{1083B}\x{1083C}\x{1083D}-\x{1083E}\x{1083F}-\x{10855}\x{10856}\x{10857}\x{10858}-\x{1085F}\x{10860}-\x{10876}\x{10877}-\x{10878}\x{10879}-\x{1087F}\x{10880}-\x{1089E}\x{1089F}-\x{108A6}\x{108A7}-\x{108AF}\x{108B0}-\x{108DF}\x{108E0}-\x{108F2}\x{108F3}\x{108F4}-\x{108F5}\x{108F6}-\x{108FA}\x{108FB}-\x{108FF}\x{10900}-\x{10915}\x{10916}-\x{1091B}\x{1091C}-\x{1091E}\x{10920}-\x{10939}\x{1093A}-\x{1093E}\x{1093F}\x{10940}-\x{1097F}\x{10980}-\x{109B7}\x{109B8}-\x{109BB}\x{109BC}-\x{109BD}\x{109BE}-\x{109BF}\x{109C0}-\x{109CF}\x{109D0}-\x{109D1}\x{109D2}-\x{109FF}\x{10A00}\x{10A04}\x{10A07}-\x{10A0B}\x{10A10}-\x{10A13}\x{10A14}\x{10A15}-\x{10A17}\x{10A18}\x{10A19}-\x{10A35}\x{10A36}-\x{10A37}\x{10A3B}-\x{10A3E}\x{10A40}-\x{10A48}\x{10A49}-\x{10A4F}\x{10A50}-\x{10A58}\x{10A59}-\x{10A5F}\x{10A60}-\x{10A7C}\x{10A7D}-\x{10A7E}\x{10A7F}\x{10A80}-\x{10A9C}\x{10A9D}-\x{10A9F}\x{10AA0}-\x{10ABF}\x{10AC0}-\x{10AC7}\x{10AC8}\x{10AC9}-\x{10AE4}\x{10AE7}-\x{10AEA}\x{10AEB}-\x{10AEF}\x{10AF0}-\x{10AF6}\x{10AF7}-\x{10AFF}\x{10B00}-\x{10B35}\x{10B36}-\x{10B38}\x{10B40}-\x{10B55}\x{10B56}-\x{10B57}\x{10B58}-\x{10B5F}\x{10B60}-\x{10B72}\x{10B73}-\x{10B77}\x{10B78}-\x{10B7F}\x{10B80}-\x{10B91}\x{10B92}-\x{10B98}\x{10B99}-\x{10B9C}\x{10B9D}-\x{10BA8}\x{10BA9}-\x{10BAF}\x{10BB0}-\x{10BFF}\x{10C00}-\x{10C48}\x{10C49}-\x{10C7F}\x{10C80}-\x{10CB2}\x{10CB3}-\x{10CBF}\x{10CC0}-\x{10CF2}\x{10CF3}-\x{10CF9}\x{10CFA}-\x{10CFF}\x{10D00}-\x{10D23}\x{10D28}-\x{10D2F}\x{10D3A}-\x{10D3F}\x{10D40}-\x{10E5F}\x{10E7F}\x{10E80}-\x{10EA9}\x{10EAA}\x{10EAD}\x{10EAE}-\x{10EAF}\x{10EB0}-\x{10EB1}\x{10EB2}-\x{10EFF}\x{10F00}-\x{10F1C}\x{10F1D}-\x{10F26}\x{10F27}\x{10F28}-\x{10F2F}\x{10F30}-\x{10F45}\x{10F51}-\x{10F54}\x{10F55}-\x{10F59}\x{10F5A}-\x{10F6F}\x{10F70}-\x{10FAF}\x{10FB0}-\x{10FC4}\x{10FC5}-\x{10FCB}\x{10FCC}-\x{10FDF}\x{10FE0}-\x{10FF6}\x{10FF7}-\x{10FFF}\x{1E800}-\x{1E8C4}\x{1E8C5}-\x{1E8C6}\x{1E8C7}-\x{1E8CF}\x{1E8D7}-\x{1E8FF}\x{1E900}-\x{1E943}\x{1E94B}\x{1E94C}-\x{1E94F}\x{1E950}-\x{1E959}\x{1E95A}-\x{1E95D}\x{1E95E}-\x{1E95F}\x{1E960}-\x{1EC6F}\x{1EC70}\x{1EC71}-\x{1ECAB}\x{1ECAC}\x{1ECAD}-\x{1ECAF}\x{1ECB0}\x{1ECB1}-\x{1ECB4}\x{1ECB5}-\x{1ECBF}\x{1ECC0}-\x{1ECFF}\x{1ED00}\x{1ED01}-\x{1ED2D}\x{1ED2E}\x{1ED2F}-\x{1ED3D}\x{1ED3E}-\x{1ED4F}\x{1ED50}-\x{1EDFF}\x{1EE00}-\x{1EE03}\x{1EE04}\x{1EE05}-\x{1EE1F}\x{1EE20}\x{1EE21}-\x{1EE22}\x{1EE23}\x{1EE24}\x{1EE25}-\x{1EE26}\x{1EE27}\x{1EE28}\x{1EE29}-\x{1EE32}\x{1EE33}\x{1EE34}-\x{1EE37}\x{1EE38}\x{1EE39}\x{1EE3A}\x{1EE3B}\x{1EE3C}-\x{1EE41}\x{1EE42}\x{1EE43}-\x{1EE46}\x{1EE47}\x{1EE48}\x{1EE49}\x{1EE4A}\x{1EE4B}\x{1EE4C}\x{1EE4D}-\x{1EE4F}\x{1EE50}\x{1EE51}-\x{1EE52}\x{1EE53}\x{1EE54}\x{1EE55}-\x{1EE56}\x{1EE57}\x{1EE58}\x{1EE59}\x{1EE5A}\x{1EE5B}\x{1EE5C}\x{1EE5D}\x{1EE5E}\x{1EE5F}\x{1EE60}\x{1EE61}-\x{1EE62}\x{1EE63}\x{1EE64}\x{1EE65}-\x{1EE66}\x{1EE67}-\x{1EE6A}\x{1EE6B}\x{1EE6C}-\x{1EE72}\x{1EE73}\x{1EE74}-\x{1EE77}\x{1EE78}\x{1EE79}-\x{1EE7C}\x{1EE7D}\x{1EE7E}\x{1EE7F}\x{1EE80}-\x{1EE89}\x{1EE8A}\x{1EE8B}-\x{1EE9B}\x{1EE9C}-\x{1EEA0}\x{1EEA1}-\x{1EEA3}\x{1EEA4}\x{1EEA5}-\x{1EEA9}\x{1EEAA}\x{1EEAB}-\x{1EEBB}\x{1EEBC}-\x{1EEEF}\x{1EEF2}-\x{1EEFF}\x{1EF00}-\x{1EFFF}]/u'; + const BIDI_STEP_2 = '/[^\x{0000}-\x{0008}\x{000E}-\x{001B}\x{0021}-\x{0022}\x{0023}\x{0024}\x{0025}\x{0026}-\x{0027}\x{0028}\x{0029}\x{002A}\x{002B}\x{002C}\x{002D}\x{002E}-\x{002F}\x{0030}-\x{0039}\x{003A}\x{003B}\x{003C}-\x{003E}\x{003F}-\x{0040}\x{005B}\x{005C}\x{005D}\x{005E}\x{005F}\x{0060}\x{007B}\x{007C}\x{007D}\x{007E}\x{007F}-\x{0084}\x{0086}-\x{009F}\x{00A0}\x{00A1}\x{00A2}-\x{00A5}\x{00A6}\x{00A7}\x{00A8}\x{00A9}\x{00AB}\x{00AC}\x{00AD}\x{00AE}\x{00AF}\x{00B0}\x{00B1}\x{00B2}-\x{00B3}\x{00B4}\x{00B6}-\x{00B7}\x{00B8}\x{00B9}\x{00BB}\x{00BC}-\x{00BE}\x{00BF}\x{00D7}\x{00F7}\x{02B9}-\x{02BA}\x{02C2}-\x{02C5}\x{02C6}-\x{02CF}\x{02D2}-\x{02DF}\x{02E5}-\x{02EB}\x{02EC}\x{02ED}\x{02EF}-\x{02FF}\x{0300}-\x{036F}\x{0374}\x{0375}\x{037E}\x{0384}-\x{0385}\x{0387}\x{03F6}\x{0483}-\x{0487}\x{0488}-\x{0489}\x{058A}\x{058D}-\x{058E}\x{058F}\x{0590}\x{0591}-\x{05BD}\x{05BE}\x{05BF}\x{05C0}\x{05C1}-\x{05C2}\x{05C3}\x{05C4}-\x{05C5}\x{05C6}\x{05C7}\x{05C8}-\x{05CF}\x{05D0}-\x{05EA}\x{05EB}-\x{05EE}\x{05EF}-\x{05F2}\x{05F3}-\x{05F4}\x{05F5}-\x{05FF}\x{0600}-\x{0605}\x{0606}-\x{0607}\x{0608}\x{0609}-\x{060A}\x{060B}\x{060C}\x{060D}\x{060E}-\x{060F}\x{0610}-\x{061A}\x{061B}\x{061C}\x{061D}\x{061E}-\x{061F}\x{0620}-\x{063F}\x{0640}\x{0641}-\x{064A}\x{064B}-\x{065F}\x{0660}-\x{0669}\x{066A}\x{066B}-\x{066C}\x{066D}\x{066E}-\x{066F}\x{0670}\x{0671}-\x{06D3}\x{06D4}\x{06D5}\x{06D6}-\x{06DC}\x{06DD}\x{06DE}\x{06DF}-\x{06E4}\x{06E5}-\x{06E6}\x{06E7}-\x{06E8}\x{06E9}\x{06EA}-\x{06ED}\x{06EE}-\x{06EF}\x{06F0}-\x{06F9}\x{06FA}-\x{06FC}\x{06FD}-\x{06FE}\x{06FF}\x{0700}-\x{070D}\x{070E}\x{070F}\x{0710}\x{0711}\x{0712}-\x{072F}\x{0730}-\x{074A}\x{074B}-\x{074C}\x{074D}-\x{07A5}\x{07A6}-\x{07B0}\x{07B1}\x{07B2}-\x{07BF}\x{07C0}-\x{07C9}\x{07CA}-\x{07EA}\x{07EB}-\x{07F3}\x{07F4}-\x{07F5}\x{07F6}\x{07F7}-\x{07F9}\x{07FA}\x{07FB}-\x{07FC}\x{07FD}\x{07FE}-\x{07FF}\x{0800}-\x{0815}\x{0816}-\x{0819}\x{081A}\x{081B}-\x{0823}\x{0824}\x{0825}-\x{0827}\x{0828}\x{0829}-\x{082D}\x{082E}-\x{082F}\x{0830}-\x{083E}\x{083F}\x{0840}-\x{0858}\x{0859}-\x{085B}\x{085C}-\x{085D}\x{085E}\x{085F}\x{0860}-\x{086A}\x{086B}-\x{086F}\x{0870}-\x{089F}\x{08A0}-\x{08B4}\x{08B5}\x{08B6}-\x{08C7}\x{08C8}-\x{08D2}\x{08D3}-\x{08E1}\x{08E2}\x{08E3}-\x{0902}\x{093A}\x{093C}\x{0941}-\x{0948}\x{094D}\x{0951}-\x{0957}\x{0962}-\x{0963}\x{0981}\x{09BC}\x{09C1}-\x{09C4}\x{09CD}\x{09E2}-\x{09E3}\x{09F2}-\x{09F3}\x{09FB}\x{09FE}\x{0A01}-\x{0A02}\x{0A3C}\x{0A41}-\x{0A42}\x{0A47}-\x{0A48}\x{0A4B}-\x{0A4D}\x{0A51}\x{0A70}-\x{0A71}\x{0A75}\x{0A81}-\x{0A82}\x{0ABC}\x{0AC1}-\x{0AC5}\x{0AC7}-\x{0AC8}\x{0ACD}\x{0AE2}-\x{0AE3}\x{0AF1}\x{0AFA}-\x{0AFF}\x{0B01}\x{0B3C}\x{0B3F}\x{0B41}-\x{0B44}\x{0B4D}\x{0B55}-\x{0B56}\x{0B62}-\x{0B63}\x{0B82}\x{0BC0}\x{0BCD}\x{0BF3}-\x{0BF8}\x{0BF9}\x{0BFA}\x{0C00}\x{0C04}\x{0C3E}-\x{0C40}\x{0C46}-\x{0C48}\x{0C4A}-\x{0C4D}\x{0C55}-\x{0C56}\x{0C62}-\x{0C63}\x{0C78}-\x{0C7E}\x{0C81}\x{0CBC}\x{0CCC}-\x{0CCD}\x{0CE2}-\x{0CE3}\x{0D00}-\x{0D01}\x{0D3B}-\x{0D3C}\x{0D41}-\x{0D44}\x{0D4D}\x{0D62}-\x{0D63}\x{0D81}\x{0DCA}\x{0DD2}-\x{0DD4}\x{0DD6}\x{0E31}\x{0E34}-\x{0E3A}\x{0E3F}\x{0E47}-\x{0E4E}\x{0EB1}\x{0EB4}-\x{0EBC}\x{0EC8}-\x{0ECD}\x{0F18}-\x{0F19}\x{0F35}\x{0F37}\x{0F39}\x{0F3A}\x{0F3B}\x{0F3C}\x{0F3D}\x{0F71}-\x{0F7E}\x{0F80}-\x{0F84}\x{0F86}-\x{0F87}\x{0F8D}-\x{0F97}\x{0F99}-\x{0FBC}\x{0FC6}\x{102D}-\x{1030}\x{1032}-\x{1037}\x{1039}-\x{103A}\x{103D}-\x{103E}\x{1058}-\x{1059}\x{105E}-\x{1060}\x{1071}-\x{1074}\x{1082}\x{1085}-\x{1086}\x{108D}\x{109D}\x{135D}-\x{135F}\x{1390}-\x{1399}\x{1400}\x{169B}\x{169C}\x{1712}-\x{1714}\x{1732}-\x{1734}\x{1752}-\x{1753}\x{1772}-\x{1773}\x{17B4}-\x{17B5}\x{17B7}-\x{17BD}\x{17C6}\x{17C9}-\x{17D3}\x{17DB}\x{17DD}\x{17F0}-\x{17F9}\x{1800}-\x{1805}\x{1806}\x{1807}-\x{180A}\x{180B}-\x{180D}\x{180E}\x{1885}-\x{1886}\x{18A9}\x{1920}-\x{1922}\x{1927}-\x{1928}\x{1932}\x{1939}-\x{193B}\x{1940}\x{1944}-\x{1945}\x{19DE}-\x{19FF}\x{1A17}-\x{1A18}\x{1A1B}\x{1A56}\x{1A58}-\x{1A5E}\x{1A60}\x{1A62}\x{1A65}-\x{1A6C}\x{1A73}-\x{1A7C}\x{1A7F}\x{1AB0}-\x{1ABD}\x{1ABE}\x{1ABF}-\x{1AC0}\x{1B00}-\x{1B03}\x{1B34}\x{1B36}-\x{1B3A}\x{1B3C}\x{1B42}\x{1B6B}-\x{1B73}\x{1B80}-\x{1B81}\x{1BA2}-\x{1BA5}\x{1BA8}-\x{1BA9}\x{1BAB}-\x{1BAD}\x{1BE6}\x{1BE8}-\x{1BE9}\x{1BED}\x{1BEF}-\x{1BF1}\x{1C2C}-\x{1C33}\x{1C36}-\x{1C37}\x{1CD0}-\x{1CD2}\x{1CD4}-\x{1CE0}\x{1CE2}-\x{1CE8}\x{1CED}\x{1CF4}\x{1CF8}-\x{1CF9}\x{1DC0}-\x{1DF9}\x{1DFB}-\x{1DFF}\x{1FBD}\x{1FBF}-\x{1FC1}\x{1FCD}-\x{1FCF}\x{1FDD}-\x{1FDF}\x{1FED}-\x{1FEF}\x{1FFD}-\x{1FFE}\x{200B}-\x{200D}\x{200F}\x{2010}-\x{2015}\x{2016}-\x{2017}\x{2018}\x{2019}\x{201A}\x{201B}-\x{201C}\x{201D}\x{201E}\x{201F}\x{2020}-\x{2027}\x{202F}\x{2030}-\x{2034}\x{2035}-\x{2038}\x{2039}\x{203A}\x{203B}-\x{203E}\x{203F}-\x{2040}\x{2041}-\x{2043}\x{2044}\x{2045}\x{2046}\x{2047}-\x{2051}\x{2052}\x{2053}\x{2054}\x{2055}-\x{205E}\x{2060}-\x{2064}\x{2065}\x{206A}-\x{206F}\x{2070}\x{2074}-\x{2079}\x{207A}-\x{207B}\x{207C}\x{207D}\x{207E}\x{2080}-\x{2089}\x{208A}-\x{208B}\x{208C}\x{208D}\x{208E}\x{20A0}-\x{20BF}\x{20C0}-\x{20CF}\x{20D0}-\x{20DC}\x{20DD}-\x{20E0}\x{20E1}\x{20E2}-\x{20E4}\x{20E5}-\x{20F0}\x{2100}-\x{2101}\x{2103}-\x{2106}\x{2108}-\x{2109}\x{2114}\x{2116}-\x{2117}\x{2118}\x{211E}-\x{2123}\x{2125}\x{2127}\x{2129}\x{212E}\x{213A}-\x{213B}\x{2140}-\x{2144}\x{214A}\x{214B}\x{214C}-\x{214D}\x{2150}-\x{215F}\x{2189}\x{218A}-\x{218B}\x{2190}-\x{2194}\x{2195}-\x{2199}\x{219A}-\x{219B}\x{219C}-\x{219F}\x{21A0}\x{21A1}-\x{21A2}\x{21A3}\x{21A4}-\x{21A5}\x{21A6}\x{21A7}-\x{21AD}\x{21AE}\x{21AF}-\x{21CD}\x{21CE}-\x{21CF}\x{21D0}-\x{21D1}\x{21D2}\x{21D3}\x{21D4}\x{21D5}-\x{21F3}\x{21F4}-\x{2211}\x{2212}\x{2213}\x{2214}-\x{22FF}\x{2300}-\x{2307}\x{2308}\x{2309}\x{230A}\x{230B}\x{230C}-\x{231F}\x{2320}-\x{2321}\x{2322}-\x{2328}\x{2329}\x{232A}\x{232B}-\x{2335}\x{237B}\x{237C}\x{237D}-\x{2394}\x{2396}-\x{239A}\x{239B}-\x{23B3}\x{23B4}-\x{23DB}\x{23DC}-\x{23E1}\x{23E2}-\x{2426}\x{2440}-\x{244A}\x{2460}-\x{2487}\x{2488}-\x{249B}\x{24EA}-\x{24FF}\x{2500}-\x{25B6}\x{25B7}\x{25B8}-\x{25C0}\x{25C1}\x{25C2}-\x{25F7}\x{25F8}-\x{25FF}\x{2600}-\x{266E}\x{266F}\x{2670}-\x{26AB}\x{26AD}-\x{2767}\x{2768}\x{2769}\x{276A}\x{276B}\x{276C}\x{276D}\x{276E}\x{276F}\x{2770}\x{2771}\x{2772}\x{2773}\x{2774}\x{2775}\x{2776}-\x{2793}\x{2794}-\x{27BF}\x{27C0}-\x{27C4}\x{27C5}\x{27C6}\x{27C7}-\x{27E5}\x{27E6}\x{27E7}\x{27E8}\x{27E9}\x{27EA}\x{27EB}\x{27EC}\x{27ED}\x{27EE}\x{27EF}\x{27F0}-\x{27FF}\x{2900}-\x{2982}\x{2983}\x{2984}\x{2985}\x{2986}\x{2987}\x{2988}\x{2989}\x{298A}\x{298B}\x{298C}\x{298D}\x{298E}\x{298F}\x{2990}\x{2991}\x{2992}\x{2993}\x{2994}\x{2995}\x{2996}\x{2997}\x{2998}\x{2999}-\x{29D7}\x{29D8}\x{29D9}\x{29DA}\x{29DB}\x{29DC}-\x{29FB}\x{29FC}\x{29FD}\x{29FE}-\x{2AFF}\x{2B00}-\x{2B2F}\x{2B30}-\x{2B44}\x{2B45}-\x{2B46}\x{2B47}-\x{2B4C}\x{2B4D}-\x{2B73}\x{2B76}-\x{2B95}\x{2B97}-\x{2BFF}\x{2CE5}-\x{2CEA}\x{2CEF}-\x{2CF1}\x{2CF9}-\x{2CFC}\x{2CFD}\x{2CFE}-\x{2CFF}\x{2D7F}\x{2DE0}-\x{2DFF}\x{2E00}-\x{2E01}\x{2E02}\x{2E03}\x{2E04}\x{2E05}\x{2E06}-\x{2E08}\x{2E09}\x{2E0A}\x{2E0B}\x{2E0C}\x{2E0D}\x{2E0E}-\x{2E16}\x{2E17}\x{2E18}-\x{2E19}\x{2E1A}\x{2E1B}\x{2E1C}\x{2E1D}\x{2E1E}-\x{2E1F}\x{2E20}\x{2E21}\x{2E22}\x{2E23}\x{2E24}\x{2E25}\x{2E26}\x{2E27}\x{2E28}\x{2E29}\x{2E2A}-\x{2E2E}\x{2E2F}\x{2E30}-\x{2E39}\x{2E3A}-\x{2E3B}\x{2E3C}-\x{2E3F}\x{2E40}\x{2E41}\x{2E42}\x{2E43}-\x{2E4F}\x{2E50}-\x{2E51}\x{2E52}\x{2E80}-\x{2E99}\x{2E9B}-\x{2EF3}\x{2F00}-\x{2FD5}\x{2FF0}-\x{2FFB}\x{3001}-\x{3003}\x{3004}\x{3008}\x{3009}\x{300A}\x{300B}\x{300C}\x{300D}\x{300E}\x{300F}\x{3010}\x{3011}\x{3012}-\x{3013}\x{3014}\x{3015}\x{3016}\x{3017}\x{3018}\x{3019}\x{301A}\x{301B}\x{301C}\x{301D}\x{301E}-\x{301F}\x{3020}\x{302A}-\x{302D}\x{3030}\x{3036}-\x{3037}\x{303D}\x{303E}-\x{303F}\x{3099}-\x{309A}\x{309B}-\x{309C}\x{30A0}\x{30FB}\x{31C0}-\x{31E3}\x{321D}-\x{321E}\x{3250}\x{3251}-\x{325F}\x{327C}-\x{327E}\x{32B1}-\x{32BF}\x{32CC}-\x{32CF}\x{3377}-\x{337A}\x{33DE}-\x{33DF}\x{33FF}\x{4DC0}-\x{4DFF}\x{A490}-\x{A4C6}\x{A60D}-\x{A60F}\x{A66F}\x{A670}-\x{A672}\x{A673}\x{A674}-\x{A67D}\x{A67E}\x{A67F}\x{A69E}-\x{A69F}\x{A6F0}-\x{A6F1}\x{A700}-\x{A716}\x{A717}-\x{A71F}\x{A720}-\x{A721}\x{A788}\x{A802}\x{A806}\x{A80B}\x{A825}-\x{A826}\x{A828}-\x{A82B}\x{A82C}\x{A838}\x{A839}\x{A874}-\x{A877}\x{A8C4}-\x{A8C5}\x{A8E0}-\x{A8F1}\x{A8FF}\x{A926}-\x{A92D}\x{A947}-\x{A951}\x{A980}-\x{A982}\x{A9B3}\x{A9B6}-\x{A9B9}\x{A9BC}-\x{A9BD}\x{A9E5}\x{AA29}-\x{AA2E}\x{AA31}-\x{AA32}\x{AA35}-\x{AA36}\x{AA43}\x{AA4C}\x{AA7C}\x{AAB0}\x{AAB2}-\x{AAB4}\x{AAB7}-\x{AAB8}\x{AABE}-\x{AABF}\x{AAC1}\x{AAEC}-\x{AAED}\x{AAF6}\x{AB6A}-\x{AB6B}\x{ABE5}\x{ABE8}\x{ABED}\x{FB1D}\x{FB1E}\x{FB1F}-\x{FB28}\x{FB29}\x{FB2A}-\x{FB36}\x{FB37}\x{FB38}-\x{FB3C}\x{FB3D}\x{FB3E}\x{FB3F}\x{FB40}-\x{FB41}\x{FB42}\x{FB43}-\x{FB44}\x{FB45}\x{FB46}-\x{FB4F}\x{FB50}-\x{FBB1}\x{FBB2}-\x{FBC1}\x{FBC2}-\x{FBD2}\x{FBD3}-\x{FD3D}\x{FD3E}\x{FD3F}\x{FD40}-\x{FD4F}\x{FD50}-\x{FD8F}\x{FD90}-\x{FD91}\x{FD92}-\x{FDC7}\x{FDC8}-\x{FDCF}\x{FDD0}-\x{FDEF}\x{FDF0}-\x{FDFB}\x{FDFC}\x{FDFD}\x{FDFE}-\x{FDFF}\x{FE00}-\x{FE0F}\x{FE10}-\x{FE16}\x{FE17}\x{FE18}\x{FE19}\x{FE20}-\x{FE2F}\x{FE30}\x{FE31}-\x{FE32}\x{FE33}-\x{FE34}\x{FE35}\x{FE36}\x{FE37}\x{FE38}\x{FE39}\x{FE3A}\x{FE3B}\x{FE3C}\x{FE3D}\x{FE3E}\x{FE3F}\x{FE40}\x{FE41}\x{FE42}\x{FE43}\x{FE44}\x{FE45}-\x{FE46}\x{FE47}\x{FE48}\x{FE49}-\x{FE4C}\x{FE4D}-\x{FE4F}\x{FE50}\x{FE51}\x{FE52}\x{FE54}\x{FE55}\x{FE56}-\x{FE57}\x{FE58}\x{FE59}\x{FE5A}\x{FE5B}\x{FE5C}\x{FE5D}\x{FE5E}\x{FE5F}\x{FE60}-\x{FE61}\x{FE62}\x{FE63}\x{FE64}-\x{FE66}\x{FE68}\x{FE69}\x{FE6A}\x{FE6B}\x{FE70}-\x{FE74}\x{FE75}\x{FE76}-\x{FEFC}\x{FEFD}-\x{FEFE}\x{FEFF}\x{FF01}-\x{FF02}\x{FF03}\x{FF04}\x{FF05}\x{FF06}-\x{FF07}\x{FF08}\x{FF09}\x{FF0A}\x{FF0B}\x{FF0C}\x{FF0D}\x{FF0E}-\x{FF0F}\x{FF10}-\x{FF19}\x{FF1A}\x{FF1B}\x{FF1C}-\x{FF1E}\x{FF1F}-\x{FF20}\x{FF3B}\x{FF3C}\x{FF3D}\x{FF3E}\x{FF3F}\x{FF40}\x{FF5B}\x{FF5C}\x{FF5D}\x{FF5E}\x{FF5F}\x{FF60}\x{FF61}\x{FF62}\x{FF63}\x{FF64}-\x{FF65}\x{FFE0}-\x{FFE1}\x{FFE2}\x{FFE3}\x{FFE4}\x{FFE5}-\x{FFE6}\x{FFE8}\x{FFE9}-\x{FFEC}\x{FFED}-\x{FFEE}\x{FFF0}-\x{FFF8}\x{FFF9}-\x{FFFB}\x{FFFC}-\x{FFFD}\x{FFFE}-\x{FFFF}\x{10101}\x{10140}-\x{10174}\x{10175}-\x{10178}\x{10179}-\x{10189}\x{1018A}-\x{1018B}\x{1018C}\x{10190}-\x{1019C}\x{101A0}\x{101FD}\x{102E0}\x{102E1}-\x{102FB}\x{10376}-\x{1037A}\x{10800}-\x{10805}\x{10806}-\x{10807}\x{10808}\x{10809}\x{1080A}-\x{10835}\x{10836}\x{10837}-\x{10838}\x{10839}-\x{1083B}\x{1083C}\x{1083D}-\x{1083E}\x{1083F}-\x{10855}\x{10856}\x{10857}\x{10858}-\x{1085F}\x{10860}-\x{10876}\x{10877}-\x{10878}\x{10879}-\x{1087F}\x{10880}-\x{1089E}\x{1089F}-\x{108A6}\x{108A7}-\x{108AF}\x{108B0}-\x{108DF}\x{108E0}-\x{108F2}\x{108F3}\x{108F4}-\x{108F5}\x{108F6}-\x{108FA}\x{108FB}-\x{108FF}\x{10900}-\x{10915}\x{10916}-\x{1091B}\x{1091C}-\x{1091E}\x{1091F}\x{10920}-\x{10939}\x{1093A}-\x{1093E}\x{1093F}\x{10940}-\x{1097F}\x{10980}-\x{109B7}\x{109B8}-\x{109BB}\x{109BC}-\x{109BD}\x{109BE}-\x{109BF}\x{109C0}-\x{109CF}\x{109D0}-\x{109D1}\x{109D2}-\x{109FF}\x{10A00}\x{10A01}-\x{10A03}\x{10A04}\x{10A05}-\x{10A06}\x{10A07}-\x{10A0B}\x{10A0C}-\x{10A0F}\x{10A10}-\x{10A13}\x{10A14}\x{10A15}-\x{10A17}\x{10A18}\x{10A19}-\x{10A35}\x{10A36}-\x{10A37}\x{10A38}-\x{10A3A}\x{10A3B}-\x{10A3E}\x{10A3F}\x{10A40}-\x{10A48}\x{10A49}-\x{10A4F}\x{10A50}-\x{10A58}\x{10A59}-\x{10A5F}\x{10A60}-\x{10A7C}\x{10A7D}-\x{10A7E}\x{10A7F}\x{10A80}-\x{10A9C}\x{10A9D}-\x{10A9F}\x{10AA0}-\x{10ABF}\x{10AC0}-\x{10AC7}\x{10AC8}\x{10AC9}-\x{10AE4}\x{10AE5}-\x{10AE6}\x{10AE7}-\x{10AEA}\x{10AEB}-\x{10AEF}\x{10AF0}-\x{10AF6}\x{10AF7}-\x{10AFF}\x{10B00}-\x{10B35}\x{10B36}-\x{10B38}\x{10B39}-\x{10B3F}\x{10B40}-\x{10B55}\x{10B56}-\x{10B57}\x{10B58}-\x{10B5F}\x{10B60}-\x{10B72}\x{10B73}-\x{10B77}\x{10B78}-\x{10B7F}\x{10B80}-\x{10B91}\x{10B92}-\x{10B98}\x{10B99}-\x{10B9C}\x{10B9D}-\x{10BA8}\x{10BA9}-\x{10BAF}\x{10BB0}-\x{10BFF}\x{10C00}-\x{10C48}\x{10C49}-\x{10C7F}\x{10C80}-\x{10CB2}\x{10CB3}-\x{10CBF}\x{10CC0}-\x{10CF2}\x{10CF3}-\x{10CF9}\x{10CFA}-\x{10CFF}\x{10D00}-\x{10D23}\x{10D24}-\x{10D27}\x{10D28}-\x{10D2F}\x{10D30}-\x{10D39}\x{10D3A}-\x{10D3F}\x{10D40}-\x{10E5F}\x{10E60}-\x{10E7E}\x{10E7F}\x{10E80}-\x{10EA9}\x{10EAA}\x{10EAB}-\x{10EAC}\x{10EAD}\x{10EAE}-\x{10EAF}\x{10EB0}-\x{10EB1}\x{10EB2}-\x{10EFF}\x{10F00}-\x{10F1C}\x{10F1D}-\x{10F26}\x{10F27}\x{10F28}-\x{10F2F}\x{10F30}-\x{10F45}\x{10F46}-\x{10F50}\x{10F51}-\x{10F54}\x{10F55}-\x{10F59}\x{10F5A}-\x{10F6F}\x{10F70}-\x{10FAF}\x{10FB0}-\x{10FC4}\x{10FC5}-\x{10FCB}\x{10FCC}-\x{10FDF}\x{10FE0}-\x{10FF6}\x{10FF7}-\x{10FFF}\x{11001}\x{11038}-\x{11046}\x{11052}-\x{11065}\x{1107F}-\x{11081}\x{110B3}-\x{110B6}\x{110B9}-\x{110BA}\x{11100}-\x{11102}\x{11127}-\x{1112B}\x{1112D}-\x{11134}\x{11173}\x{11180}-\x{11181}\x{111B6}-\x{111BE}\x{111C9}-\x{111CC}\x{111CF}\x{1122F}-\x{11231}\x{11234}\x{11236}-\x{11237}\x{1123E}\x{112DF}\x{112E3}-\x{112EA}\x{11300}-\x{11301}\x{1133B}-\x{1133C}\x{11340}\x{11366}-\x{1136C}\x{11370}-\x{11374}\x{11438}-\x{1143F}\x{11442}-\x{11444}\x{11446}\x{1145E}\x{114B3}-\x{114B8}\x{114BA}\x{114BF}-\x{114C0}\x{114C2}-\x{114C3}\x{115B2}-\x{115B5}\x{115BC}-\x{115BD}\x{115BF}-\x{115C0}\x{115DC}-\x{115DD}\x{11633}-\x{1163A}\x{1163D}\x{1163F}-\x{11640}\x{11660}-\x{1166C}\x{116AB}\x{116AD}\x{116B0}-\x{116B5}\x{116B7}\x{1171D}-\x{1171F}\x{11722}-\x{11725}\x{11727}-\x{1172B}\x{1182F}-\x{11837}\x{11839}-\x{1183A}\x{1193B}-\x{1193C}\x{1193E}\x{11943}\x{119D4}-\x{119D7}\x{119DA}-\x{119DB}\x{119E0}\x{11A01}-\x{11A06}\x{11A09}-\x{11A0A}\x{11A33}-\x{11A38}\x{11A3B}-\x{11A3E}\x{11A47}\x{11A51}-\x{11A56}\x{11A59}-\x{11A5B}\x{11A8A}-\x{11A96}\x{11A98}-\x{11A99}\x{11C30}-\x{11C36}\x{11C38}-\x{11C3D}\x{11C92}-\x{11CA7}\x{11CAA}-\x{11CB0}\x{11CB2}-\x{11CB3}\x{11CB5}-\x{11CB6}\x{11D31}-\x{11D36}\x{11D3A}\x{11D3C}-\x{11D3D}\x{11D3F}-\x{11D45}\x{11D47}\x{11D90}-\x{11D91}\x{11D95}\x{11D97}\x{11EF3}-\x{11EF4}\x{11FD5}-\x{11FDC}\x{11FDD}-\x{11FE0}\x{11FE1}-\x{11FF1}\x{16AF0}-\x{16AF4}\x{16B30}-\x{16B36}\x{16F4F}\x{16F8F}-\x{16F92}\x{16FE2}\x{16FE4}\x{1BC9D}-\x{1BC9E}\x{1BCA0}-\x{1BCA3}\x{1D167}-\x{1D169}\x{1D173}-\x{1D17A}\x{1D17B}-\x{1D182}\x{1D185}-\x{1D18B}\x{1D1AA}-\x{1D1AD}\x{1D200}-\x{1D241}\x{1D242}-\x{1D244}\x{1D245}\x{1D300}-\x{1D356}\x{1D6DB}\x{1D715}\x{1D74F}\x{1D789}\x{1D7C3}\x{1D7CE}-\x{1D7FF}\x{1DA00}-\x{1DA36}\x{1DA3B}-\x{1DA6C}\x{1DA75}\x{1DA84}\x{1DA9B}-\x{1DA9F}\x{1DAA1}-\x{1DAAF}\x{1E000}-\x{1E006}\x{1E008}-\x{1E018}\x{1E01B}-\x{1E021}\x{1E023}-\x{1E024}\x{1E026}-\x{1E02A}\x{1E130}-\x{1E136}\x{1E2EC}-\x{1E2EF}\x{1E2FF}\x{1E800}-\x{1E8C4}\x{1E8C5}-\x{1E8C6}\x{1E8C7}-\x{1E8CF}\x{1E8D0}-\x{1E8D6}\x{1E8D7}-\x{1E8FF}\x{1E900}-\x{1E943}\x{1E944}-\x{1E94A}\x{1E94B}\x{1E94C}-\x{1E94F}\x{1E950}-\x{1E959}\x{1E95A}-\x{1E95D}\x{1E95E}-\x{1E95F}\x{1E960}-\x{1EC6F}\x{1EC70}\x{1EC71}-\x{1ECAB}\x{1ECAC}\x{1ECAD}-\x{1ECAF}\x{1ECB0}\x{1ECB1}-\x{1ECB4}\x{1ECB5}-\x{1ECBF}\x{1ECC0}-\x{1ECFF}\x{1ED00}\x{1ED01}-\x{1ED2D}\x{1ED2E}\x{1ED2F}-\x{1ED3D}\x{1ED3E}-\x{1ED4F}\x{1ED50}-\x{1EDFF}\x{1EE00}-\x{1EE03}\x{1EE04}\x{1EE05}-\x{1EE1F}\x{1EE20}\x{1EE21}-\x{1EE22}\x{1EE23}\x{1EE24}\x{1EE25}-\x{1EE26}\x{1EE27}\x{1EE28}\x{1EE29}-\x{1EE32}\x{1EE33}\x{1EE34}-\x{1EE37}\x{1EE38}\x{1EE39}\x{1EE3A}\x{1EE3B}\x{1EE3C}-\x{1EE41}\x{1EE42}\x{1EE43}-\x{1EE46}\x{1EE47}\x{1EE48}\x{1EE49}\x{1EE4A}\x{1EE4B}\x{1EE4C}\x{1EE4D}-\x{1EE4F}\x{1EE50}\x{1EE51}-\x{1EE52}\x{1EE53}\x{1EE54}\x{1EE55}-\x{1EE56}\x{1EE57}\x{1EE58}\x{1EE59}\x{1EE5A}\x{1EE5B}\x{1EE5C}\x{1EE5D}\x{1EE5E}\x{1EE5F}\x{1EE60}\x{1EE61}-\x{1EE62}\x{1EE63}\x{1EE64}\x{1EE65}-\x{1EE66}\x{1EE67}-\x{1EE6A}\x{1EE6B}\x{1EE6C}-\x{1EE72}\x{1EE73}\x{1EE74}-\x{1EE77}\x{1EE78}\x{1EE79}-\x{1EE7C}\x{1EE7D}\x{1EE7E}\x{1EE7F}\x{1EE80}-\x{1EE89}\x{1EE8A}\x{1EE8B}-\x{1EE9B}\x{1EE9C}-\x{1EEA0}\x{1EEA1}-\x{1EEA3}\x{1EEA4}\x{1EEA5}-\x{1EEA9}\x{1EEAA}\x{1EEAB}-\x{1EEBB}\x{1EEBC}-\x{1EEEF}\x{1EEF0}-\x{1EEF1}\x{1EEF2}-\x{1EEFF}\x{1EF00}-\x{1EFFF}\x{1F000}-\x{1F02B}\x{1F030}-\x{1F093}\x{1F0A0}-\x{1F0AE}\x{1F0B1}-\x{1F0BF}\x{1F0C1}-\x{1F0CF}\x{1F0D1}-\x{1F0F5}\x{1F100}-\x{1F10A}\x{1F10B}-\x{1F10C}\x{1F10D}-\x{1F10F}\x{1F12F}\x{1F16A}-\x{1F16F}\x{1F1AD}\x{1F260}-\x{1F265}\x{1F300}-\x{1F3FA}\x{1F3FB}-\x{1F3FF}\x{1F400}-\x{1F6D7}\x{1F6E0}-\x{1F6EC}\x{1F6F0}-\x{1F6FC}\x{1F700}-\x{1F773}\x{1F780}-\x{1F7D8}\x{1F7E0}-\x{1F7EB}\x{1F800}-\x{1F80B}\x{1F810}-\x{1F847}\x{1F850}-\x{1F859}\x{1F860}-\x{1F887}\x{1F890}-\x{1F8AD}\x{1F8B0}-\x{1F8B1}\x{1F900}-\x{1F978}\x{1F97A}-\x{1F9CB}\x{1F9CD}-\x{1FA53}\x{1FA60}-\x{1FA6D}\x{1FA70}-\x{1FA74}\x{1FA78}-\x{1FA7A}\x{1FA80}-\x{1FA86}\x{1FA90}-\x{1FAA8}\x{1FAB0}-\x{1FAB6}\x{1FAC0}-\x{1FAC2}\x{1FAD0}-\x{1FAD6}\x{1FB00}-\x{1FB92}\x{1FB94}-\x{1FBCA}\x{1FBF0}-\x{1FBF9}\x{1FFFE}-\x{1FFFF}\x{2FFFE}-\x{2FFFF}\x{3FFFE}-\x{3FFFF}\x{4FFFE}-\x{4FFFF}\x{5FFFE}-\x{5FFFF}\x{6FFFE}-\x{6FFFF}\x{7FFFE}-\x{7FFFF}\x{8FFFE}-\x{8FFFF}\x{9FFFE}-\x{9FFFF}\x{AFFFE}-\x{AFFFF}\x{BFFFE}-\x{BFFFF}\x{CFFFE}-\x{CFFFF}\x{DFFFE}-\x{E0000}\x{E0001}\x{E0002}-\x{E001F}\x{E0020}-\x{E007F}\x{E0080}-\x{E00FF}\x{E0100}-\x{E01EF}\x{E01F0}-\x{E0FFF}\x{EFFFE}-\x{EFFFF}\x{FFFFE}-\x{FFFFF}\x{10FFFE}-\x{10FFFF}]/u'; + const BIDI_STEP_3 = '/[\x{0030}-\x{0039}\x{00B2}-\x{00B3}\x{00B9}\x{0590}\x{05BE}\x{05C0}\x{05C3}\x{05C6}\x{05C8}-\x{05CF}\x{05D0}-\x{05EA}\x{05EB}-\x{05EE}\x{05EF}-\x{05F2}\x{05F3}-\x{05F4}\x{05F5}-\x{05FF}\x{0600}-\x{0605}\x{0608}\x{060B}\x{060D}\x{061B}\x{061C}\x{061D}\x{061E}-\x{061F}\x{0620}-\x{063F}\x{0640}\x{0641}-\x{064A}\x{0660}-\x{0669}\x{066B}-\x{066C}\x{066D}\x{066E}-\x{066F}\x{0671}-\x{06D3}\x{06D4}\x{06D5}\x{06DD}\x{06E5}-\x{06E6}\x{06EE}-\x{06EF}\x{06F0}-\x{06F9}\x{06FA}-\x{06FC}\x{06FD}-\x{06FE}\x{06FF}\x{0700}-\x{070D}\x{070E}\x{070F}\x{0710}\x{0712}-\x{072F}\x{074B}-\x{074C}\x{074D}-\x{07A5}\x{07B1}\x{07B2}-\x{07BF}\x{07C0}-\x{07C9}\x{07CA}-\x{07EA}\x{07F4}-\x{07F5}\x{07FA}\x{07FB}-\x{07FC}\x{07FE}-\x{07FF}\x{0800}-\x{0815}\x{081A}\x{0824}\x{0828}\x{082E}-\x{082F}\x{0830}-\x{083E}\x{083F}\x{0840}-\x{0858}\x{085C}-\x{085D}\x{085E}\x{085F}\x{0860}-\x{086A}\x{086B}-\x{086F}\x{0870}-\x{089F}\x{08A0}-\x{08B4}\x{08B5}\x{08B6}-\x{08C7}\x{08C8}-\x{08D2}\x{08E2}\x{200F}\x{2070}\x{2074}-\x{2079}\x{2080}-\x{2089}\x{2488}-\x{249B}\x{FB1D}\x{FB1F}-\x{FB28}\x{FB2A}-\x{FB36}\x{FB37}\x{FB38}-\x{FB3C}\x{FB3D}\x{FB3E}\x{FB3F}\x{FB40}-\x{FB41}\x{FB42}\x{FB43}-\x{FB44}\x{FB45}\x{FB46}-\x{FB4F}\x{FB50}-\x{FBB1}\x{FBB2}-\x{FBC1}\x{FBC2}-\x{FBD2}\x{FBD3}-\x{FD3D}\x{FD40}-\x{FD4F}\x{FD50}-\x{FD8F}\x{FD90}-\x{FD91}\x{FD92}-\x{FDC7}\x{FDC8}-\x{FDCF}\x{FDF0}-\x{FDFB}\x{FDFC}\x{FDFE}-\x{FDFF}\x{FE70}-\x{FE74}\x{FE75}\x{FE76}-\x{FEFC}\x{FEFD}-\x{FEFE}\x{FF10}-\x{FF19}\x{102E1}-\x{102FB}\x{10800}-\x{10805}\x{10806}-\x{10807}\x{10808}\x{10809}\x{1080A}-\x{10835}\x{10836}\x{10837}-\x{10838}\x{10839}-\x{1083B}\x{1083C}\x{1083D}-\x{1083E}\x{1083F}-\x{10855}\x{10856}\x{10857}\x{10858}-\x{1085F}\x{10860}-\x{10876}\x{10877}-\x{10878}\x{10879}-\x{1087F}\x{10880}-\x{1089E}\x{1089F}-\x{108A6}\x{108A7}-\x{108AF}\x{108B0}-\x{108DF}\x{108E0}-\x{108F2}\x{108F3}\x{108F4}-\x{108F5}\x{108F6}-\x{108FA}\x{108FB}-\x{108FF}\x{10900}-\x{10915}\x{10916}-\x{1091B}\x{1091C}-\x{1091E}\x{10920}-\x{10939}\x{1093A}-\x{1093E}\x{1093F}\x{10940}-\x{1097F}\x{10980}-\x{109B7}\x{109B8}-\x{109BB}\x{109BC}-\x{109BD}\x{109BE}-\x{109BF}\x{109C0}-\x{109CF}\x{109D0}-\x{109D1}\x{109D2}-\x{109FF}\x{10A00}\x{10A04}\x{10A07}-\x{10A0B}\x{10A10}-\x{10A13}\x{10A14}\x{10A15}-\x{10A17}\x{10A18}\x{10A19}-\x{10A35}\x{10A36}-\x{10A37}\x{10A3B}-\x{10A3E}\x{10A40}-\x{10A48}\x{10A49}-\x{10A4F}\x{10A50}-\x{10A58}\x{10A59}-\x{10A5F}\x{10A60}-\x{10A7C}\x{10A7D}-\x{10A7E}\x{10A7F}\x{10A80}-\x{10A9C}\x{10A9D}-\x{10A9F}\x{10AA0}-\x{10ABF}\x{10AC0}-\x{10AC7}\x{10AC8}\x{10AC9}-\x{10AE4}\x{10AE7}-\x{10AEA}\x{10AEB}-\x{10AEF}\x{10AF0}-\x{10AF6}\x{10AF7}-\x{10AFF}\x{10B00}-\x{10B35}\x{10B36}-\x{10B38}\x{10B40}-\x{10B55}\x{10B56}-\x{10B57}\x{10B58}-\x{10B5F}\x{10B60}-\x{10B72}\x{10B73}-\x{10B77}\x{10B78}-\x{10B7F}\x{10B80}-\x{10B91}\x{10B92}-\x{10B98}\x{10B99}-\x{10B9C}\x{10B9D}-\x{10BA8}\x{10BA9}-\x{10BAF}\x{10BB0}-\x{10BFF}\x{10C00}-\x{10C48}\x{10C49}-\x{10C7F}\x{10C80}-\x{10CB2}\x{10CB3}-\x{10CBF}\x{10CC0}-\x{10CF2}\x{10CF3}-\x{10CF9}\x{10CFA}-\x{10CFF}\x{10D00}-\x{10D23}\x{10D28}-\x{10D2F}\x{10D30}-\x{10D39}\x{10D3A}-\x{10D3F}\x{10D40}-\x{10E5F}\x{10E60}-\x{10E7E}\x{10E7F}\x{10E80}-\x{10EA9}\x{10EAA}\x{10EAD}\x{10EAE}-\x{10EAF}\x{10EB0}-\x{10EB1}\x{10EB2}-\x{10EFF}\x{10F00}-\x{10F1C}\x{10F1D}-\x{10F26}\x{10F27}\x{10F28}-\x{10F2F}\x{10F30}-\x{10F45}\x{10F51}-\x{10F54}\x{10F55}-\x{10F59}\x{10F5A}-\x{10F6F}\x{10F70}-\x{10FAF}\x{10FB0}-\x{10FC4}\x{10FC5}-\x{10FCB}\x{10FCC}-\x{10FDF}\x{10FE0}-\x{10FF6}\x{10FF7}-\x{10FFF}\x{1D7CE}-\x{1D7FF}\x{1E800}-\x{1E8C4}\x{1E8C5}-\x{1E8C6}\x{1E8C7}-\x{1E8CF}\x{1E8D7}-\x{1E8FF}\x{1E900}-\x{1E943}\x{1E94B}\x{1E94C}-\x{1E94F}\x{1E950}-\x{1E959}\x{1E95A}-\x{1E95D}\x{1E95E}-\x{1E95F}\x{1E960}-\x{1EC6F}\x{1EC70}\x{1EC71}-\x{1ECAB}\x{1ECAC}\x{1ECAD}-\x{1ECAF}\x{1ECB0}\x{1ECB1}-\x{1ECB4}\x{1ECB5}-\x{1ECBF}\x{1ECC0}-\x{1ECFF}\x{1ED00}\x{1ED01}-\x{1ED2D}\x{1ED2E}\x{1ED2F}-\x{1ED3D}\x{1ED3E}-\x{1ED4F}\x{1ED50}-\x{1EDFF}\x{1EE00}-\x{1EE03}\x{1EE04}\x{1EE05}-\x{1EE1F}\x{1EE20}\x{1EE21}-\x{1EE22}\x{1EE23}\x{1EE24}\x{1EE25}-\x{1EE26}\x{1EE27}\x{1EE28}\x{1EE29}-\x{1EE32}\x{1EE33}\x{1EE34}-\x{1EE37}\x{1EE38}\x{1EE39}\x{1EE3A}\x{1EE3B}\x{1EE3C}-\x{1EE41}\x{1EE42}\x{1EE43}-\x{1EE46}\x{1EE47}\x{1EE48}\x{1EE49}\x{1EE4A}\x{1EE4B}\x{1EE4C}\x{1EE4D}-\x{1EE4F}\x{1EE50}\x{1EE51}-\x{1EE52}\x{1EE53}\x{1EE54}\x{1EE55}-\x{1EE56}\x{1EE57}\x{1EE58}\x{1EE59}\x{1EE5A}\x{1EE5B}\x{1EE5C}\x{1EE5D}\x{1EE5E}\x{1EE5F}\x{1EE60}\x{1EE61}-\x{1EE62}\x{1EE63}\x{1EE64}\x{1EE65}-\x{1EE66}\x{1EE67}-\x{1EE6A}\x{1EE6B}\x{1EE6C}-\x{1EE72}\x{1EE73}\x{1EE74}-\x{1EE77}\x{1EE78}\x{1EE79}-\x{1EE7C}\x{1EE7D}\x{1EE7E}\x{1EE7F}\x{1EE80}-\x{1EE89}\x{1EE8A}\x{1EE8B}-\x{1EE9B}\x{1EE9C}-\x{1EEA0}\x{1EEA1}-\x{1EEA3}\x{1EEA4}\x{1EEA5}-\x{1EEA9}\x{1EEAA}\x{1EEAB}-\x{1EEBB}\x{1EEBC}-\x{1EEEF}\x{1EEF2}-\x{1EEFF}\x{1EF00}-\x{1EFFF}\x{1F100}-\x{1F10A}\x{1FBF0}-\x{1FBF9}][\x{0300}-\x{036F}\x{0483}-\x{0487}\x{0488}-\x{0489}\x{0591}-\x{05BD}\x{05BF}\x{05C1}-\x{05C2}\x{05C4}-\x{05C5}\x{05C7}\x{0610}-\x{061A}\x{064B}-\x{065F}\x{0670}\x{06D6}-\x{06DC}\x{06DF}-\x{06E4}\x{06E7}-\x{06E8}\x{06EA}-\x{06ED}\x{0711}\x{0730}-\x{074A}\x{07A6}-\x{07B0}\x{07EB}-\x{07F3}\x{07FD}\x{0816}-\x{0819}\x{081B}-\x{0823}\x{0825}-\x{0827}\x{0829}-\x{082D}\x{0859}-\x{085B}\x{08D3}-\x{08E1}\x{08E3}-\x{0902}\x{093A}\x{093C}\x{0941}-\x{0948}\x{094D}\x{0951}-\x{0957}\x{0962}-\x{0963}\x{0981}\x{09BC}\x{09C1}-\x{09C4}\x{09CD}\x{09E2}-\x{09E3}\x{09FE}\x{0A01}-\x{0A02}\x{0A3C}\x{0A41}-\x{0A42}\x{0A47}-\x{0A48}\x{0A4B}-\x{0A4D}\x{0A51}\x{0A70}-\x{0A71}\x{0A75}\x{0A81}-\x{0A82}\x{0ABC}\x{0AC1}-\x{0AC5}\x{0AC7}-\x{0AC8}\x{0ACD}\x{0AE2}-\x{0AE3}\x{0AFA}-\x{0AFF}\x{0B01}\x{0B3C}\x{0B3F}\x{0B41}-\x{0B44}\x{0B4D}\x{0B55}-\x{0B56}\x{0B62}-\x{0B63}\x{0B82}\x{0BC0}\x{0BCD}\x{0C00}\x{0C04}\x{0C3E}-\x{0C40}\x{0C46}-\x{0C48}\x{0C4A}-\x{0C4D}\x{0C55}-\x{0C56}\x{0C62}-\x{0C63}\x{0C81}\x{0CBC}\x{0CCC}-\x{0CCD}\x{0CE2}-\x{0CE3}\x{0D00}-\x{0D01}\x{0D3B}-\x{0D3C}\x{0D41}-\x{0D44}\x{0D4D}\x{0D62}-\x{0D63}\x{0D81}\x{0DCA}\x{0DD2}-\x{0DD4}\x{0DD6}\x{0E31}\x{0E34}-\x{0E3A}\x{0E47}-\x{0E4E}\x{0EB1}\x{0EB4}-\x{0EBC}\x{0EC8}-\x{0ECD}\x{0F18}-\x{0F19}\x{0F35}\x{0F37}\x{0F39}\x{0F71}-\x{0F7E}\x{0F80}-\x{0F84}\x{0F86}-\x{0F87}\x{0F8D}-\x{0F97}\x{0F99}-\x{0FBC}\x{0FC6}\x{102D}-\x{1030}\x{1032}-\x{1037}\x{1039}-\x{103A}\x{103D}-\x{103E}\x{1058}-\x{1059}\x{105E}-\x{1060}\x{1071}-\x{1074}\x{1082}\x{1085}-\x{1086}\x{108D}\x{109D}\x{135D}-\x{135F}\x{1712}-\x{1714}\x{1732}-\x{1734}\x{1752}-\x{1753}\x{1772}-\x{1773}\x{17B4}-\x{17B5}\x{17B7}-\x{17BD}\x{17C6}\x{17C9}-\x{17D3}\x{17DD}\x{180B}-\x{180D}\x{1885}-\x{1886}\x{18A9}\x{1920}-\x{1922}\x{1927}-\x{1928}\x{1932}\x{1939}-\x{193B}\x{1A17}-\x{1A18}\x{1A1B}\x{1A56}\x{1A58}-\x{1A5E}\x{1A60}\x{1A62}\x{1A65}-\x{1A6C}\x{1A73}-\x{1A7C}\x{1A7F}\x{1AB0}-\x{1ABD}\x{1ABE}\x{1ABF}-\x{1AC0}\x{1B00}-\x{1B03}\x{1B34}\x{1B36}-\x{1B3A}\x{1B3C}\x{1B42}\x{1B6B}-\x{1B73}\x{1B80}-\x{1B81}\x{1BA2}-\x{1BA5}\x{1BA8}-\x{1BA9}\x{1BAB}-\x{1BAD}\x{1BE6}\x{1BE8}-\x{1BE9}\x{1BED}\x{1BEF}-\x{1BF1}\x{1C2C}-\x{1C33}\x{1C36}-\x{1C37}\x{1CD0}-\x{1CD2}\x{1CD4}-\x{1CE0}\x{1CE2}-\x{1CE8}\x{1CED}\x{1CF4}\x{1CF8}-\x{1CF9}\x{1DC0}-\x{1DF9}\x{1DFB}-\x{1DFF}\x{20D0}-\x{20DC}\x{20DD}-\x{20E0}\x{20E1}\x{20E2}-\x{20E4}\x{20E5}-\x{20F0}\x{2CEF}-\x{2CF1}\x{2D7F}\x{2DE0}-\x{2DFF}\x{302A}-\x{302D}\x{3099}-\x{309A}\x{A66F}\x{A670}-\x{A672}\x{A674}-\x{A67D}\x{A69E}-\x{A69F}\x{A6F0}-\x{A6F1}\x{A802}\x{A806}\x{A80B}\x{A825}-\x{A826}\x{A82C}\x{A8C4}-\x{A8C5}\x{A8E0}-\x{A8F1}\x{A8FF}\x{A926}-\x{A92D}\x{A947}-\x{A951}\x{A980}-\x{A982}\x{A9B3}\x{A9B6}-\x{A9B9}\x{A9BC}-\x{A9BD}\x{A9E5}\x{AA29}-\x{AA2E}\x{AA31}-\x{AA32}\x{AA35}-\x{AA36}\x{AA43}\x{AA4C}\x{AA7C}\x{AAB0}\x{AAB2}-\x{AAB4}\x{AAB7}-\x{AAB8}\x{AABE}-\x{AABF}\x{AAC1}\x{AAEC}-\x{AAED}\x{AAF6}\x{ABE5}\x{ABE8}\x{ABED}\x{FB1E}\x{FE00}-\x{FE0F}\x{FE20}-\x{FE2F}\x{101FD}\x{102E0}\x{10376}-\x{1037A}\x{10A01}-\x{10A03}\x{10A05}-\x{10A06}\x{10A0C}-\x{10A0F}\x{10A38}-\x{10A3A}\x{10A3F}\x{10AE5}-\x{10AE6}\x{10D24}-\x{10D27}\x{10EAB}-\x{10EAC}\x{10F46}-\x{10F50}\x{11001}\x{11038}-\x{11046}\x{1107F}-\x{11081}\x{110B3}-\x{110B6}\x{110B9}-\x{110BA}\x{11100}-\x{11102}\x{11127}-\x{1112B}\x{1112D}-\x{11134}\x{11173}\x{11180}-\x{11181}\x{111B6}-\x{111BE}\x{111C9}-\x{111CC}\x{111CF}\x{1122F}-\x{11231}\x{11234}\x{11236}-\x{11237}\x{1123E}\x{112DF}\x{112E3}-\x{112EA}\x{11300}-\x{11301}\x{1133B}-\x{1133C}\x{11340}\x{11366}-\x{1136C}\x{11370}-\x{11374}\x{11438}-\x{1143F}\x{11442}-\x{11444}\x{11446}\x{1145E}\x{114B3}-\x{114B8}\x{114BA}\x{114BF}-\x{114C0}\x{114C2}-\x{114C3}\x{115B2}-\x{115B5}\x{115BC}-\x{115BD}\x{115BF}-\x{115C0}\x{115DC}-\x{115DD}\x{11633}-\x{1163A}\x{1163D}\x{1163F}-\x{11640}\x{116AB}\x{116AD}\x{116B0}-\x{116B5}\x{116B7}\x{1171D}-\x{1171F}\x{11722}-\x{11725}\x{11727}-\x{1172B}\x{1182F}-\x{11837}\x{11839}-\x{1183A}\x{1193B}-\x{1193C}\x{1193E}\x{11943}\x{119D4}-\x{119D7}\x{119DA}-\x{119DB}\x{119E0}\x{11A01}-\x{11A06}\x{11A09}-\x{11A0A}\x{11A33}-\x{11A38}\x{11A3B}-\x{11A3E}\x{11A47}\x{11A51}-\x{11A56}\x{11A59}-\x{11A5B}\x{11A8A}-\x{11A96}\x{11A98}-\x{11A99}\x{11C30}-\x{11C36}\x{11C38}-\x{11C3D}\x{11C92}-\x{11CA7}\x{11CAA}-\x{11CB0}\x{11CB2}-\x{11CB3}\x{11CB5}-\x{11CB6}\x{11D31}-\x{11D36}\x{11D3A}\x{11D3C}-\x{11D3D}\x{11D3F}-\x{11D45}\x{11D47}\x{11D90}-\x{11D91}\x{11D95}\x{11D97}\x{11EF3}-\x{11EF4}\x{16AF0}-\x{16AF4}\x{16B30}-\x{16B36}\x{16F4F}\x{16F8F}-\x{16F92}\x{16FE4}\x{1BC9D}-\x{1BC9E}\x{1D167}-\x{1D169}\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{1E000}-\x{1E006}\x{1E008}-\x{1E018}\x{1E01B}-\x{1E021}\x{1E023}-\x{1E024}\x{1E026}-\x{1E02A}\x{1E130}-\x{1E136}\x{1E2EC}-\x{1E2EF}\x{1E8D0}-\x{1E8D6}\x{1E944}-\x{1E94A}\x{E0100}-\x{E01EF}]*$/u'; + const BIDI_STEP_4_AN = '/[\x{0600}-\x{0605}\x{0660}-\x{0669}\x{066B}-\x{066C}\x{06DD}\x{08E2}\x{10D30}-\x{10D39}\x{10E60}-\x{10E7E}]/u'; + const BIDI_STEP_4_EN = '/[\x{0030}-\x{0039}\x{00B2}-\x{00B3}\x{00B9}\x{06F0}-\x{06F9}\x{2070}\x{2074}-\x{2079}\x{2080}-\x{2089}\x{2488}-\x{249B}\x{FF10}-\x{FF19}\x{102E1}-\x{102FB}\x{1D7CE}-\x{1D7FF}\x{1F100}-\x{1F10A}\x{1FBF0}-\x{1FBF9}]/u'; + const BIDI_STEP_5 = '/[\x{0009}\x{000A}\x{000B}\x{000C}\x{000D}\x{001C}-\x{001E}\x{001F}\x{0020}\x{0085}\x{0590}\x{05BE}\x{05C0}\x{05C3}\x{05C6}\x{05C8}-\x{05CF}\x{05D0}-\x{05EA}\x{05EB}-\x{05EE}\x{05EF}-\x{05F2}\x{05F3}-\x{05F4}\x{05F5}-\x{05FF}\x{0600}-\x{0605}\x{0608}\x{060B}\x{060D}\x{061B}\x{061C}\x{061D}\x{061E}-\x{061F}\x{0620}-\x{063F}\x{0640}\x{0641}-\x{064A}\x{0660}-\x{0669}\x{066B}-\x{066C}\x{066D}\x{066E}-\x{066F}\x{0671}-\x{06D3}\x{06D4}\x{06D5}\x{06DD}\x{06E5}-\x{06E6}\x{06EE}-\x{06EF}\x{06FA}-\x{06FC}\x{06FD}-\x{06FE}\x{06FF}\x{0700}-\x{070D}\x{070E}\x{070F}\x{0710}\x{0712}-\x{072F}\x{074B}-\x{074C}\x{074D}-\x{07A5}\x{07B1}\x{07B2}-\x{07BF}\x{07C0}-\x{07C9}\x{07CA}-\x{07EA}\x{07F4}-\x{07F5}\x{07FA}\x{07FB}-\x{07FC}\x{07FE}-\x{07FF}\x{0800}-\x{0815}\x{081A}\x{0824}\x{0828}\x{082E}-\x{082F}\x{0830}-\x{083E}\x{083F}\x{0840}-\x{0858}\x{085C}-\x{085D}\x{085E}\x{085F}\x{0860}-\x{086A}\x{086B}-\x{086F}\x{0870}-\x{089F}\x{08A0}-\x{08B4}\x{08B5}\x{08B6}-\x{08C7}\x{08C8}-\x{08D2}\x{08E2}\x{1680}\x{2000}-\x{200A}\x{200F}\x{2028}\x{2029}\x{202A}\x{202B}\x{202C}\x{202D}\x{202E}\x{205F}\x{2066}\x{2067}\x{2068}\x{2069}\x{3000}\x{FB1D}\x{FB1F}-\x{FB28}\x{FB2A}-\x{FB36}\x{FB37}\x{FB38}-\x{FB3C}\x{FB3D}\x{FB3E}\x{FB3F}\x{FB40}-\x{FB41}\x{FB42}\x{FB43}-\x{FB44}\x{FB45}\x{FB46}-\x{FB4F}\x{FB50}-\x{FBB1}\x{FBB2}-\x{FBC1}\x{FBC2}-\x{FBD2}\x{FBD3}-\x{FD3D}\x{FD40}-\x{FD4F}\x{FD50}-\x{FD8F}\x{FD90}-\x{FD91}\x{FD92}-\x{FDC7}\x{FDC8}-\x{FDCF}\x{FDF0}-\x{FDFB}\x{FDFC}\x{FDFE}-\x{FDFF}\x{FE70}-\x{FE74}\x{FE75}\x{FE76}-\x{FEFC}\x{FEFD}-\x{FEFE}\x{10800}-\x{10805}\x{10806}-\x{10807}\x{10808}\x{10809}\x{1080A}-\x{10835}\x{10836}\x{10837}-\x{10838}\x{10839}-\x{1083B}\x{1083C}\x{1083D}-\x{1083E}\x{1083F}-\x{10855}\x{10856}\x{10857}\x{10858}-\x{1085F}\x{10860}-\x{10876}\x{10877}-\x{10878}\x{10879}-\x{1087F}\x{10880}-\x{1089E}\x{1089F}-\x{108A6}\x{108A7}-\x{108AF}\x{108B0}-\x{108DF}\x{108E0}-\x{108F2}\x{108F3}\x{108F4}-\x{108F5}\x{108F6}-\x{108FA}\x{108FB}-\x{108FF}\x{10900}-\x{10915}\x{10916}-\x{1091B}\x{1091C}-\x{1091E}\x{10920}-\x{10939}\x{1093A}-\x{1093E}\x{1093F}\x{10940}-\x{1097F}\x{10980}-\x{109B7}\x{109B8}-\x{109BB}\x{109BC}-\x{109BD}\x{109BE}-\x{109BF}\x{109C0}-\x{109CF}\x{109D0}-\x{109D1}\x{109D2}-\x{109FF}\x{10A00}\x{10A04}\x{10A07}-\x{10A0B}\x{10A10}-\x{10A13}\x{10A14}\x{10A15}-\x{10A17}\x{10A18}\x{10A19}-\x{10A35}\x{10A36}-\x{10A37}\x{10A3B}-\x{10A3E}\x{10A40}-\x{10A48}\x{10A49}-\x{10A4F}\x{10A50}-\x{10A58}\x{10A59}-\x{10A5F}\x{10A60}-\x{10A7C}\x{10A7D}-\x{10A7E}\x{10A7F}\x{10A80}-\x{10A9C}\x{10A9D}-\x{10A9F}\x{10AA0}-\x{10ABF}\x{10AC0}-\x{10AC7}\x{10AC8}\x{10AC9}-\x{10AE4}\x{10AE7}-\x{10AEA}\x{10AEB}-\x{10AEF}\x{10AF0}-\x{10AF6}\x{10AF7}-\x{10AFF}\x{10B00}-\x{10B35}\x{10B36}-\x{10B38}\x{10B40}-\x{10B55}\x{10B56}-\x{10B57}\x{10B58}-\x{10B5F}\x{10B60}-\x{10B72}\x{10B73}-\x{10B77}\x{10B78}-\x{10B7F}\x{10B80}-\x{10B91}\x{10B92}-\x{10B98}\x{10B99}-\x{10B9C}\x{10B9D}-\x{10BA8}\x{10BA9}-\x{10BAF}\x{10BB0}-\x{10BFF}\x{10C00}-\x{10C48}\x{10C49}-\x{10C7F}\x{10C80}-\x{10CB2}\x{10CB3}-\x{10CBF}\x{10CC0}-\x{10CF2}\x{10CF3}-\x{10CF9}\x{10CFA}-\x{10CFF}\x{10D00}-\x{10D23}\x{10D28}-\x{10D2F}\x{10D30}-\x{10D39}\x{10D3A}-\x{10D3F}\x{10D40}-\x{10E5F}\x{10E60}-\x{10E7E}\x{10E7F}\x{10E80}-\x{10EA9}\x{10EAA}\x{10EAD}\x{10EAE}-\x{10EAF}\x{10EB0}-\x{10EB1}\x{10EB2}-\x{10EFF}\x{10F00}-\x{10F1C}\x{10F1D}-\x{10F26}\x{10F27}\x{10F28}-\x{10F2F}\x{10F30}-\x{10F45}\x{10F51}-\x{10F54}\x{10F55}-\x{10F59}\x{10F5A}-\x{10F6F}\x{10F70}-\x{10FAF}\x{10FB0}-\x{10FC4}\x{10FC5}-\x{10FCB}\x{10FCC}-\x{10FDF}\x{10FE0}-\x{10FF6}\x{10FF7}-\x{10FFF}\x{1E800}-\x{1E8C4}\x{1E8C5}-\x{1E8C6}\x{1E8C7}-\x{1E8CF}\x{1E8D7}-\x{1E8FF}\x{1E900}-\x{1E943}\x{1E94B}\x{1E94C}-\x{1E94F}\x{1E950}-\x{1E959}\x{1E95A}-\x{1E95D}\x{1E95E}-\x{1E95F}\x{1E960}-\x{1EC6F}\x{1EC70}\x{1EC71}-\x{1ECAB}\x{1ECAC}\x{1ECAD}-\x{1ECAF}\x{1ECB0}\x{1ECB1}-\x{1ECB4}\x{1ECB5}-\x{1ECBF}\x{1ECC0}-\x{1ECFF}\x{1ED00}\x{1ED01}-\x{1ED2D}\x{1ED2E}\x{1ED2F}-\x{1ED3D}\x{1ED3E}-\x{1ED4F}\x{1ED50}-\x{1EDFF}\x{1EE00}-\x{1EE03}\x{1EE04}\x{1EE05}-\x{1EE1F}\x{1EE20}\x{1EE21}-\x{1EE22}\x{1EE23}\x{1EE24}\x{1EE25}-\x{1EE26}\x{1EE27}\x{1EE28}\x{1EE29}-\x{1EE32}\x{1EE33}\x{1EE34}-\x{1EE37}\x{1EE38}\x{1EE39}\x{1EE3A}\x{1EE3B}\x{1EE3C}-\x{1EE41}\x{1EE42}\x{1EE43}-\x{1EE46}\x{1EE47}\x{1EE48}\x{1EE49}\x{1EE4A}\x{1EE4B}\x{1EE4C}\x{1EE4D}-\x{1EE4F}\x{1EE50}\x{1EE51}-\x{1EE52}\x{1EE53}\x{1EE54}\x{1EE55}-\x{1EE56}\x{1EE57}\x{1EE58}\x{1EE59}\x{1EE5A}\x{1EE5B}\x{1EE5C}\x{1EE5D}\x{1EE5E}\x{1EE5F}\x{1EE60}\x{1EE61}-\x{1EE62}\x{1EE63}\x{1EE64}\x{1EE65}-\x{1EE66}\x{1EE67}-\x{1EE6A}\x{1EE6B}\x{1EE6C}-\x{1EE72}\x{1EE73}\x{1EE74}-\x{1EE77}\x{1EE78}\x{1EE79}-\x{1EE7C}\x{1EE7D}\x{1EE7E}\x{1EE7F}\x{1EE80}-\x{1EE89}\x{1EE8A}\x{1EE8B}-\x{1EE9B}\x{1EE9C}-\x{1EEA0}\x{1EEA1}-\x{1EEA3}\x{1EEA4}\x{1EEA5}-\x{1EEA9}\x{1EEAA}\x{1EEAB}-\x{1EEBB}\x{1EEBC}-\x{1EEEF}\x{1EEF2}-\x{1EEFF}\x{1EF00}-\x{1EFFF}]/u'; + const BIDI_STEP_6 = '/[^\x{0000}-\x{0008}\x{0009}\x{000A}\x{000B}\x{000C}\x{000D}\x{000E}-\x{001B}\x{001C}-\x{001E}\x{001F}\x{0020}\x{0021}-\x{0022}\x{0023}\x{0024}\x{0025}\x{0026}-\x{0027}\x{0028}\x{0029}\x{002A}\x{002B}\x{002C}\x{002D}\x{002E}-\x{002F}\x{003A}\x{003B}\x{003C}-\x{003E}\x{003F}-\x{0040}\x{005B}\x{005C}\x{005D}\x{005E}\x{005F}\x{0060}\x{007B}\x{007C}\x{007D}\x{007E}\x{007F}-\x{0084}\x{0085}\x{0086}-\x{009F}\x{00A0}\x{00A1}\x{00A2}-\x{00A5}\x{00A6}\x{00A7}\x{00A8}\x{00A9}\x{00AB}\x{00AC}\x{00AD}\x{00AE}\x{00AF}\x{00B0}\x{00B1}\x{00B4}\x{00B6}-\x{00B7}\x{00B8}\x{00BB}\x{00BC}-\x{00BE}\x{00BF}\x{00D7}\x{00F7}\x{02B9}-\x{02BA}\x{02C2}-\x{02C5}\x{02C6}-\x{02CF}\x{02D2}-\x{02DF}\x{02E5}-\x{02EB}\x{02EC}\x{02ED}\x{02EF}-\x{02FF}\x{0300}-\x{036F}\x{0374}\x{0375}\x{037E}\x{0384}-\x{0385}\x{0387}\x{03F6}\x{0483}-\x{0487}\x{0488}-\x{0489}\x{058A}\x{058D}-\x{058E}\x{058F}\x{0590}\x{0591}-\x{05BD}\x{05BE}\x{05BF}\x{05C0}\x{05C1}-\x{05C2}\x{05C3}\x{05C4}-\x{05C5}\x{05C6}\x{05C7}\x{05C8}-\x{05CF}\x{05D0}-\x{05EA}\x{05EB}-\x{05EE}\x{05EF}-\x{05F2}\x{05F3}-\x{05F4}\x{05F5}-\x{05FF}\x{0600}-\x{0605}\x{0606}-\x{0607}\x{0608}\x{0609}-\x{060A}\x{060B}\x{060C}\x{060D}\x{060E}-\x{060F}\x{0610}-\x{061A}\x{061B}\x{061C}\x{061D}\x{061E}-\x{061F}\x{0620}-\x{063F}\x{0640}\x{0641}-\x{064A}\x{064B}-\x{065F}\x{0660}-\x{0669}\x{066A}\x{066B}-\x{066C}\x{066D}\x{066E}-\x{066F}\x{0670}\x{0671}-\x{06D3}\x{06D4}\x{06D5}\x{06D6}-\x{06DC}\x{06DD}\x{06DE}\x{06DF}-\x{06E4}\x{06E5}-\x{06E6}\x{06E7}-\x{06E8}\x{06E9}\x{06EA}-\x{06ED}\x{06EE}-\x{06EF}\x{06FA}-\x{06FC}\x{06FD}-\x{06FE}\x{06FF}\x{0700}-\x{070D}\x{070E}\x{070F}\x{0710}\x{0711}\x{0712}-\x{072F}\x{0730}-\x{074A}\x{074B}-\x{074C}\x{074D}-\x{07A5}\x{07A6}-\x{07B0}\x{07B1}\x{07B2}-\x{07BF}\x{07C0}-\x{07C9}\x{07CA}-\x{07EA}\x{07EB}-\x{07F3}\x{07F4}-\x{07F5}\x{07F6}\x{07F7}-\x{07F9}\x{07FA}\x{07FB}-\x{07FC}\x{07FD}\x{07FE}-\x{07FF}\x{0800}-\x{0815}\x{0816}-\x{0819}\x{081A}\x{081B}-\x{0823}\x{0824}\x{0825}-\x{0827}\x{0828}\x{0829}-\x{082D}\x{082E}-\x{082F}\x{0830}-\x{083E}\x{083F}\x{0840}-\x{0858}\x{0859}-\x{085B}\x{085C}-\x{085D}\x{085E}\x{085F}\x{0860}-\x{086A}\x{086B}-\x{086F}\x{0870}-\x{089F}\x{08A0}-\x{08B4}\x{08B5}\x{08B6}-\x{08C7}\x{08C8}-\x{08D2}\x{08D3}-\x{08E1}\x{08E2}\x{08E3}-\x{0902}\x{093A}\x{093C}\x{0941}-\x{0948}\x{094D}\x{0951}-\x{0957}\x{0962}-\x{0963}\x{0981}\x{09BC}\x{09C1}-\x{09C4}\x{09CD}\x{09E2}-\x{09E3}\x{09F2}-\x{09F3}\x{09FB}\x{09FE}\x{0A01}-\x{0A02}\x{0A3C}\x{0A41}-\x{0A42}\x{0A47}-\x{0A48}\x{0A4B}-\x{0A4D}\x{0A51}\x{0A70}-\x{0A71}\x{0A75}\x{0A81}-\x{0A82}\x{0ABC}\x{0AC1}-\x{0AC5}\x{0AC7}-\x{0AC8}\x{0ACD}\x{0AE2}-\x{0AE3}\x{0AF1}\x{0AFA}-\x{0AFF}\x{0B01}\x{0B3C}\x{0B3F}\x{0B41}-\x{0B44}\x{0B4D}\x{0B55}-\x{0B56}\x{0B62}-\x{0B63}\x{0B82}\x{0BC0}\x{0BCD}\x{0BF3}-\x{0BF8}\x{0BF9}\x{0BFA}\x{0C00}\x{0C04}\x{0C3E}-\x{0C40}\x{0C46}-\x{0C48}\x{0C4A}-\x{0C4D}\x{0C55}-\x{0C56}\x{0C62}-\x{0C63}\x{0C78}-\x{0C7E}\x{0C81}\x{0CBC}\x{0CCC}-\x{0CCD}\x{0CE2}-\x{0CE3}\x{0D00}-\x{0D01}\x{0D3B}-\x{0D3C}\x{0D41}-\x{0D44}\x{0D4D}\x{0D62}-\x{0D63}\x{0D81}\x{0DCA}\x{0DD2}-\x{0DD4}\x{0DD6}\x{0E31}\x{0E34}-\x{0E3A}\x{0E3F}\x{0E47}-\x{0E4E}\x{0EB1}\x{0EB4}-\x{0EBC}\x{0EC8}-\x{0ECD}\x{0F18}-\x{0F19}\x{0F35}\x{0F37}\x{0F39}\x{0F3A}\x{0F3B}\x{0F3C}\x{0F3D}\x{0F71}-\x{0F7E}\x{0F80}-\x{0F84}\x{0F86}-\x{0F87}\x{0F8D}-\x{0F97}\x{0F99}-\x{0FBC}\x{0FC6}\x{102D}-\x{1030}\x{1032}-\x{1037}\x{1039}-\x{103A}\x{103D}-\x{103E}\x{1058}-\x{1059}\x{105E}-\x{1060}\x{1071}-\x{1074}\x{1082}\x{1085}-\x{1086}\x{108D}\x{109D}\x{135D}-\x{135F}\x{1390}-\x{1399}\x{1400}\x{1680}\x{169B}\x{169C}\x{1712}-\x{1714}\x{1732}-\x{1734}\x{1752}-\x{1753}\x{1772}-\x{1773}\x{17B4}-\x{17B5}\x{17B7}-\x{17BD}\x{17C6}\x{17C9}-\x{17D3}\x{17DB}\x{17DD}\x{17F0}-\x{17F9}\x{1800}-\x{1805}\x{1806}\x{1807}-\x{180A}\x{180B}-\x{180D}\x{180E}\x{1885}-\x{1886}\x{18A9}\x{1920}-\x{1922}\x{1927}-\x{1928}\x{1932}\x{1939}-\x{193B}\x{1940}\x{1944}-\x{1945}\x{19DE}-\x{19FF}\x{1A17}-\x{1A18}\x{1A1B}\x{1A56}\x{1A58}-\x{1A5E}\x{1A60}\x{1A62}\x{1A65}-\x{1A6C}\x{1A73}-\x{1A7C}\x{1A7F}\x{1AB0}-\x{1ABD}\x{1ABE}\x{1ABF}-\x{1AC0}\x{1B00}-\x{1B03}\x{1B34}\x{1B36}-\x{1B3A}\x{1B3C}\x{1B42}\x{1B6B}-\x{1B73}\x{1B80}-\x{1B81}\x{1BA2}-\x{1BA5}\x{1BA8}-\x{1BA9}\x{1BAB}-\x{1BAD}\x{1BE6}\x{1BE8}-\x{1BE9}\x{1BED}\x{1BEF}-\x{1BF1}\x{1C2C}-\x{1C33}\x{1C36}-\x{1C37}\x{1CD0}-\x{1CD2}\x{1CD4}-\x{1CE0}\x{1CE2}-\x{1CE8}\x{1CED}\x{1CF4}\x{1CF8}-\x{1CF9}\x{1DC0}-\x{1DF9}\x{1DFB}-\x{1DFF}\x{1FBD}\x{1FBF}-\x{1FC1}\x{1FCD}-\x{1FCF}\x{1FDD}-\x{1FDF}\x{1FED}-\x{1FEF}\x{1FFD}-\x{1FFE}\x{2000}-\x{200A}\x{200B}-\x{200D}\x{200F}\x{2010}-\x{2015}\x{2016}-\x{2017}\x{2018}\x{2019}\x{201A}\x{201B}-\x{201C}\x{201D}\x{201E}\x{201F}\x{2020}-\x{2027}\x{2028}\x{2029}\x{202A}\x{202B}\x{202C}\x{202D}\x{202E}\x{202F}\x{2030}-\x{2034}\x{2035}-\x{2038}\x{2039}\x{203A}\x{203B}-\x{203E}\x{203F}-\x{2040}\x{2041}-\x{2043}\x{2044}\x{2045}\x{2046}\x{2047}-\x{2051}\x{2052}\x{2053}\x{2054}\x{2055}-\x{205E}\x{205F}\x{2060}-\x{2064}\x{2065}\x{2066}\x{2067}\x{2068}\x{2069}\x{206A}-\x{206F}\x{207A}-\x{207B}\x{207C}\x{207D}\x{207E}\x{208A}-\x{208B}\x{208C}\x{208D}\x{208E}\x{20A0}-\x{20BF}\x{20C0}-\x{20CF}\x{20D0}-\x{20DC}\x{20DD}-\x{20E0}\x{20E1}\x{20E2}-\x{20E4}\x{20E5}-\x{20F0}\x{2100}-\x{2101}\x{2103}-\x{2106}\x{2108}-\x{2109}\x{2114}\x{2116}-\x{2117}\x{2118}\x{211E}-\x{2123}\x{2125}\x{2127}\x{2129}\x{212E}\x{213A}-\x{213B}\x{2140}-\x{2144}\x{214A}\x{214B}\x{214C}-\x{214D}\x{2150}-\x{215F}\x{2189}\x{218A}-\x{218B}\x{2190}-\x{2194}\x{2195}-\x{2199}\x{219A}-\x{219B}\x{219C}-\x{219F}\x{21A0}\x{21A1}-\x{21A2}\x{21A3}\x{21A4}-\x{21A5}\x{21A6}\x{21A7}-\x{21AD}\x{21AE}\x{21AF}-\x{21CD}\x{21CE}-\x{21CF}\x{21D0}-\x{21D1}\x{21D2}\x{21D3}\x{21D4}\x{21D5}-\x{21F3}\x{21F4}-\x{2211}\x{2212}\x{2213}\x{2214}-\x{22FF}\x{2300}-\x{2307}\x{2308}\x{2309}\x{230A}\x{230B}\x{230C}-\x{231F}\x{2320}-\x{2321}\x{2322}-\x{2328}\x{2329}\x{232A}\x{232B}-\x{2335}\x{237B}\x{237C}\x{237D}-\x{2394}\x{2396}-\x{239A}\x{239B}-\x{23B3}\x{23B4}-\x{23DB}\x{23DC}-\x{23E1}\x{23E2}-\x{2426}\x{2440}-\x{244A}\x{2460}-\x{2487}\x{24EA}-\x{24FF}\x{2500}-\x{25B6}\x{25B7}\x{25B8}-\x{25C0}\x{25C1}\x{25C2}-\x{25F7}\x{25F8}-\x{25FF}\x{2600}-\x{266E}\x{266F}\x{2670}-\x{26AB}\x{26AD}-\x{2767}\x{2768}\x{2769}\x{276A}\x{276B}\x{276C}\x{276D}\x{276E}\x{276F}\x{2770}\x{2771}\x{2772}\x{2773}\x{2774}\x{2775}\x{2776}-\x{2793}\x{2794}-\x{27BF}\x{27C0}-\x{27C4}\x{27C5}\x{27C6}\x{27C7}-\x{27E5}\x{27E6}\x{27E7}\x{27E8}\x{27E9}\x{27EA}\x{27EB}\x{27EC}\x{27ED}\x{27EE}\x{27EF}\x{27F0}-\x{27FF}\x{2900}-\x{2982}\x{2983}\x{2984}\x{2985}\x{2986}\x{2987}\x{2988}\x{2989}\x{298A}\x{298B}\x{298C}\x{298D}\x{298E}\x{298F}\x{2990}\x{2991}\x{2992}\x{2993}\x{2994}\x{2995}\x{2996}\x{2997}\x{2998}\x{2999}-\x{29D7}\x{29D8}\x{29D9}\x{29DA}\x{29DB}\x{29DC}-\x{29FB}\x{29FC}\x{29FD}\x{29FE}-\x{2AFF}\x{2B00}-\x{2B2F}\x{2B30}-\x{2B44}\x{2B45}-\x{2B46}\x{2B47}-\x{2B4C}\x{2B4D}-\x{2B73}\x{2B76}-\x{2B95}\x{2B97}-\x{2BFF}\x{2CE5}-\x{2CEA}\x{2CEF}-\x{2CF1}\x{2CF9}-\x{2CFC}\x{2CFD}\x{2CFE}-\x{2CFF}\x{2D7F}\x{2DE0}-\x{2DFF}\x{2E00}-\x{2E01}\x{2E02}\x{2E03}\x{2E04}\x{2E05}\x{2E06}-\x{2E08}\x{2E09}\x{2E0A}\x{2E0B}\x{2E0C}\x{2E0D}\x{2E0E}-\x{2E16}\x{2E17}\x{2E18}-\x{2E19}\x{2E1A}\x{2E1B}\x{2E1C}\x{2E1D}\x{2E1E}-\x{2E1F}\x{2E20}\x{2E21}\x{2E22}\x{2E23}\x{2E24}\x{2E25}\x{2E26}\x{2E27}\x{2E28}\x{2E29}\x{2E2A}-\x{2E2E}\x{2E2F}\x{2E30}-\x{2E39}\x{2E3A}-\x{2E3B}\x{2E3C}-\x{2E3F}\x{2E40}\x{2E41}\x{2E42}\x{2E43}-\x{2E4F}\x{2E50}-\x{2E51}\x{2E52}\x{2E80}-\x{2E99}\x{2E9B}-\x{2EF3}\x{2F00}-\x{2FD5}\x{2FF0}-\x{2FFB}\x{3000}\x{3001}-\x{3003}\x{3004}\x{3008}\x{3009}\x{300A}\x{300B}\x{300C}\x{300D}\x{300E}\x{300F}\x{3010}\x{3011}\x{3012}-\x{3013}\x{3014}\x{3015}\x{3016}\x{3017}\x{3018}\x{3019}\x{301A}\x{301B}\x{301C}\x{301D}\x{301E}-\x{301F}\x{3020}\x{302A}-\x{302D}\x{3030}\x{3036}-\x{3037}\x{303D}\x{303E}-\x{303F}\x{3099}-\x{309A}\x{309B}-\x{309C}\x{30A0}\x{30FB}\x{31C0}-\x{31E3}\x{321D}-\x{321E}\x{3250}\x{3251}-\x{325F}\x{327C}-\x{327E}\x{32B1}-\x{32BF}\x{32CC}-\x{32CF}\x{3377}-\x{337A}\x{33DE}-\x{33DF}\x{33FF}\x{4DC0}-\x{4DFF}\x{A490}-\x{A4C6}\x{A60D}-\x{A60F}\x{A66F}\x{A670}-\x{A672}\x{A673}\x{A674}-\x{A67D}\x{A67E}\x{A67F}\x{A69E}-\x{A69F}\x{A6F0}-\x{A6F1}\x{A700}-\x{A716}\x{A717}-\x{A71F}\x{A720}-\x{A721}\x{A788}\x{A802}\x{A806}\x{A80B}\x{A825}-\x{A826}\x{A828}-\x{A82B}\x{A82C}\x{A838}\x{A839}\x{A874}-\x{A877}\x{A8C4}-\x{A8C5}\x{A8E0}-\x{A8F1}\x{A8FF}\x{A926}-\x{A92D}\x{A947}-\x{A951}\x{A980}-\x{A982}\x{A9B3}\x{A9B6}-\x{A9B9}\x{A9BC}-\x{A9BD}\x{A9E5}\x{AA29}-\x{AA2E}\x{AA31}-\x{AA32}\x{AA35}-\x{AA36}\x{AA43}\x{AA4C}\x{AA7C}\x{AAB0}\x{AAB2}-\x{AAB4}\x{AAB7}-\x{AAB8}\x{AABE}-\x{AABF}\x{AAC1}\x{AAEC}-\x{AAED}\x{AAF6}\x{AB6A}-\x{AB6B}\x{ABE5}\x{ABE8}\x{ABED}\x{FB1D}\x{FB1E}\x{FB1F}-\x{FB28}\x{FB29}\x{FB2A}-\x{FB36}\x{FB37}\x{FB38}-\x{FB3C}\x{FB3D}\x{FB3E}\x{FB3F}\x{FB40}-\x{FB41}\x{FB42}\x{FB43}-\x{FB44}\x{FB45}\x{FB46}-\x{FB4F}\x{FB50}-\x{FBB1}\x{FBB2}-\x{FBC1}\x{FBC2}-\x{FBD2}\x{FBD3}-\x{FD3D}\x{FD3E}\x{FD3F}\x{FD40}-\x{FD4F}\x{FD50}-\x{FD8F}\x{FD90}-\x{FD91}\x{FD92}-\x{FDC7}\x{FDC8}-\x{FDCF}\x{FDD0}-\x{FDEF}\x{FDF0}-\x{FDFB}\x{FDFC}\x{FDFD}\x{FDFE}-\x{FDFF}\x{FE00}-\x{FE0F}\x{FE10}-\x{FE16}\x{FE17}\x{FE18}\x{FE19}\x{FE20}-\x{FE2F}\x{FE30}\x{FE31}-\x{FE32}\x{FE33}-\x{FE34}\x{FE35}\x{FE36}\x{FE37}\x{FE38}\x{FE39}\x{FE3A}\x{FE3B}\x{FE3C}\x{FE3D}\x{FE3E}\x{FE3F}\x{FE40}\x{FE41}\x{FE42}\x{FE43}\x{FE44}\x{FE45}-\x{FE46}\x{FE47}\x{FE48}\x{FE49}-\x{FE4C}\x{FE4D}-\x{FE4F}\x{FE50}\x{FE51}\x{FE52}\x{FE54}\x{FE55}\x{FE56}-\x{FE57}\x{FE58}\x{FE59}\x{FE5A}\x{FE5B}\x{FE5C}\x{FE5D}\x{FE5E}\x{FE5F}\x{FE60}-\x{FE61}\x{FE62}\x{FE63}\x{FE64}-\x{FE66}\x{FE68}\x{FE69}\x{FE6A}\x{FE6B}\x{FE70}-\x{FE74}\x{FE75}\x{FE76}-\x{FEFC}\x{FEFD}-\x{FEFE}\x{FEFF}\x{FF01}-\x{FF02}\x{FF03}\x{FF04}\x{FF05}\x{FF06}-\x{FF07}\x{FF08}\x{FF09}\x{FF0A}\x{FF0B}\x{FF0C}\x{FF0D}\x{FF0E}-\x{FF0F}\x{FF1A}\x{FF1B}\x{FF1C}-\x{FF1E}\x{FF1F}-\x{FF20}\x{FF3B}\x{FF3C}\x{FF3D}\x{FF3E}\x{FF3F}\x{FF40}\x{FF5B}\x{FF5C}\x{FF5D}\x{FF5E}\x{FF5F}\x{FF60}\x{FF61}\x{FF62}\x{FF63}\x{FF64}-\x{FF65}\x{FFE0}-\x{FFE1}\x{FFE2}\x{FFE3}\x{FFE4}\x{FFE5}-\x{FFE6}\x{FFE8}\x{FFE9}-\x{FFEC}\x{FFED}-\x{FFEE}\x{FFF0}-\x{FFF8}\x{FFF9}-\x{FFFB}\x{FFFC}-\x{FFFD}\x{FFFE}-\x{FFFF}\x{10101}\x{10140}-\x{10174}\x{10175}-\x{10178}\x{10179}-\x{10189}\x{1018A}-\x{1018B}\x{1018C}\x{10190}-\x{1019C}\x{101A0}\x{101FD}\x{102E0}\x{10376}-\x{1037A}\x{10800}-\x{10805}\x{10806}-\x{10807}\x{10808}\x{10809}\x{1080A}-\x{10835}\x{10836}\x{10837}-\x{10838}\x{10839}-\x{1083B}\x{1083C}\x{1083D}-\x{1083E}\x{1083F}-\x{10855}\x{10856}\x{10857}\x{10858}-\x{1085F}\x{10860}-\x{10876}\x{10877}-\x{10878}\x{10879}-\x{1087F}\x{10880}-\x{1089E}\x{1089F}-\x{108A6}\x{108A7}-\x{108AF}\x{108B0}-\x{108DF}\x{108E0}-\x{108F2}\x{108F3}\x{108F4}-\x{108F5}\x{108F6}-\x{108FA}\x{108FB}-\x{108FF}\x{10900}-\x{10915}\x{10916}-\x{1091B}\x{1091C}-\x{1091E}\x{1091F}\x{10920}-\x{10939}\x{1093A}-\x{1093E}\x{1093F}\x{10940}-\x{1097F}\x{10980}-\x{109B7}\x{109B8}-\x{109BB}\x{109BC}-\x{109BD}\x{109BE}-\x{109BF}\x{109C0}-\x{109CF}\x{109D0}-\x{109D1}\x{109D2}-\x{109FF}\x{10A00}\x{10A01}-\x{10A03}\x{10A04}\x{10A05}-\x{10A06}\x{10A07}-\x{10A0B}\x{10A0C}-\x{10A0F}\x{10A10}-\x{10A13}\x{10A14}\x{10A15}-\x{10A17}\x{10A18}\x{10A19}-\x{10A35}\x{10A36}-\x{10A37}\x{10A38}-\x{10A3A}\x{10A3B}-\x{10A3E}\x{10A3F}\x{10A40}-\x{10A48}\x{10A49}-\x{10A4F}\x{10A50}-\x{10A58}\x{10A59}-\x{10A5F}\x{10A60}-\x{10A7C}\x{10A7D}-\x{10A7E}\x{10A7F}\x{10A80}-\x{10A9C}\x{10A9D}-\x{10A9F}\x{10AA0}-\x{10ABF}\x{10AC0}-\x{10AC7}\x{10AC8}\x{10AC9}-\x{10AE4}\x{10AE5}-\x{10AE6}\x{10AE7}-\x{10AEA}\x{10AEB}-\x{10AEF}\x{10AF0}-\x{10AF6}\x{10AF7}-\x{10AFF}\x{10B00}-\x{10B35}\x{10B36}-\x{10B38}\x{10B39}-\x{10B3F}\x{10B40}-\x{10B55}\x{10B56}-\x{10B57}\x{10B58}-\x{10B5F}\x{10B60}-\x{10B72}\x{10B73}-\x{10B77}\x{10B78}-\x{10B7F}\x{10B80}-\x{10B91}\x{10B92}-\x{10B98}\x{10B99}-\x{10B9C}\x{10B9D}-\x{10BA8}\x{10BA9}-\x{10BAF}\x{10BB0}-\x{10BFF}\x{10C00}-\x{10C48}\x{10C49}-\x{10C7F}\x{10C80}-\x{10CB2}\x{10CB3}-\x{10CBF}\x{10CC0}-\x{10CF2}\x{10CF3}-\x{10CF9}\x{10CFA}-\x{10CFF}\x{10D00}-\x{10D23}\x{10D24}-\x{10D27}\x{10D28}-\x{10D2F}\x{10D30}-\x{10D39}\x{10D3A}-\x{10D3F}\x{10D40}-\x{10E5F}\x{10E60}-\x{10E7E}\x{10E7F}\x{10E80}-\x{10EA9}\x{10EAA}\x{10EAB}-\x{10EAC}\x{10EAD}\x{10EAE}-\x{10EAF}\x{10EB0}-\x{10EB1}\x{10EB2}-\x{10EFF}\x{10F00}-\x{10F1C}\x{10F1D}-\x{10F26}\x{10F27}\x{10F28}-\x{10F2F}\x{10F30}-\x{10F45}\x{10F46}-\x{10F50}\x{10F51}-\x{10F54}\x{10F55}-\x{10F59}\x{10F5A}-\x{10F6F}\x{10F70}-\x{10FAF}\x{10FB0}-\x{10FC4}\x{10FC5}-\x{10FCB}\x{10FCC}-\x{10FDF}\x{10FE0}-\x{10FF6}\x{10FF7}-\x{10FFF}\x{11001}\x{11038}-\x{11046}\x{11052}-\x{11065}\x{1107F}-\x{11081}\x{110B3}-\x{110B6}\x{110B9}-\x{110BA}\x{11100}-\x{11102}\x{11127}-\x{1112B}\x{1112D}-\x{11134}\x{11173}\x{11180}-\x{11181}\x{111B6}-\x{111BE}\x{111C9}-\x{111CC}\x{111CF}\x{1122F}-\x{11231}\x{11234}\x{11236}-\x{11237}\x{1123E}\x{112DF}\x{112E3}-\x{112EA}\x{11300}-\x{11301}\x{1133B}-\x{1133C}\x{11340}\x{11366}-\x{1136C}\x{11370}-\x{11374}\x{11438}-\x{1143F}\x{11442}-\x{11444}\x{11446}\x{1145E}\x{114B3}-\x{114B8}\x{114BA}\x{114BF}-\x{114C0}\x{114C2}-\x{114C3}\x{115B2}-\x{115B5}\x{115BC}-\x{115BD}\x{115BF}-\x{115C0}\x{115DC}-\x{115DD}\x{11633}-\x{1163A}\x{1163D}\x{1163F}-\x{11640}\x{11660}-\x{1166C}\x{116AB}\x{116AD}\x{116B0}-\x{116B5}\x{116B7}\x{1171D}-\x{1171F}\x{11722}-\x{11725}\x{11727}-\x{1172B}\x{1182F}-\x{11837}\x{11839}-\x{1183A}\x{1193B}-\x{1193C}\x{1193E}\x{11943}\x{119D4}-\x{119D7}\x{119DA}-\x{119DB}\x{119E0}\x{11A01}-\x{11A06}\x{11A09}-\x{11A0A}\x{11A33}-\x{11A38}\x{11A3B}-\x{11A3E}\x{11A47}\x{11A51}-\x{11A56}\x{11A59}-\x{11A5B}\x{11A8A}-\x{11A96}\x{11A98}-\x{11A99}\x{11C30}-\x{11C36}\x{11C38}-\x{11C3D}\x{11C92}-\x{11CA7}\x{11CAA}-\x{11CB0}\x{11CB2}-\x{11CB3}\x{11CB5}-\x{11CB6}\x{11D31}-\x{11D36}\x{11D3A}\x{11D3C}-\x{11D3D}\x{11D3F}-\x{11D45}\x{11D47}\x{11D90}-\x{11D91}\x{11D95}\x{11D97}\x{11EF3}-\x{11EF4}\x{11FD5}-\x{11FDC}\x{11FDD}-\x{11FE0}\x{11FE1}-\x{11FF1}\x{16AF0}-\x{16AF4}\x{16B30}-\x{16B36}\x{16F4F}\x{16F8F}-\x{16F92}\x{16FE2}\x{16FE4}\x{1BC9D}-\x{1BC9E}\x{1BCA0}-\x{1BCA3}\x{1D167}-\x{1D169}\x{1D173}-\x{1D17A}\x{1D17B}-\x{1D182}\x{1D185}-\x{1D18B}\x{1D1AA}-\x{1D1AD}\x{1D200}-\x{1D241}\x{1D242}-\x{1D244}\x{1D245}\x{1D300}-\x{1D356}\x{1D6DB}\x{1D715}\x{1D74F}\x{1D789}\x{1D7C3}\x{1DA00}-\x{1DA36}\x{1DA3B}-\x{1DA6C}\x{1DA75}\x{1DA84}\x{1DA9B}-\x{1DA9F}\x{1DAA1}-\x{1DAAF}\x{1E000}-\x{1E006}\x{1E008}-\x{1E018}\x{1E01B}-\x{1E021}\x{1E023}-\x{1E024}\x{1E026}-\x{1E02A}\x{1E130}-\x{1E136}\x{1E2EC}-\x{1E2EF}\x{1E2FF}\x{1E800}-\x{1E8C4}\x{1E8C5}-\x{1E8C6}\x{1E8C7}-\x{1E8CF}\x{1E8D0}-\x{1E8D6}\x{1E8D7}-\x{1E8FF}\x{1E900}-\x{1E943}\x{1E944}-\x{1E94A}\x{1E94B}\x{1E94C}-\x{1E94F}\x{1E950}-\x{1E959}\x{1E95A}-\x{1E95D}\x{1E95E}-\x{1E95F}\x{1E960}-\x{1EC6F}\x{1EC70}\x{1EC71}-\x{1ECAB}\x{1ECAC}\x{1ECAD}-\x{1ECAF}\x{1ECB0}\x{1ECB1}-\x{1ECB4}\x{1ECB5}-\x{1ECBF}\x{1ECC0}-\x{1ECFF}\x{1ED00}\x{1ED01}-\x{1ED2D}\x{1ED2E}\x{1ED2F}-\x{1ED3D}\x{1ED3E}-\x{1ED4F}\x{1ED50}-\x{1EDFF}\x{1EE00}-\x{1EE03}\x{1EE04}\x{1EE05}-\x{1EE1F}\x{1EE20}\x{1EE21}-\x{1EE22}\x{1EE23}\x{1EE24}\x{1EE25}-\x{1EE26}\x{1EE27}\x{1EE28}\x{1EE29}-\x{1EE32}\x{1EE33}\x{1EE34}-\x{1EE37}\x{1EE38}\x{1EE39}\x{1EE3A}\x{1EE3B}\x{1EE3C}-\x{1EE41}\x{1EE42}\x{1EE43}-\x{1EE46}\x{1EE47}\x{1EE48}\x{1EE49}\x{1EE4A}\x{1EE4B}\x{1EE4C}\x{1EE4D}-\x{1EE4F}\x{1EE50}\x{1EE51}-\x{1EE52}\x{1EE53}\x{1EE54}\x{1EE55}-\x{1EE56}\x{1EE57}\x{1EE58}\x{1EE59}\x{1EE5A}\x{1EE5B}\x{1EE5C}\x{1EE5D}\x{1EE5E}\x{1EE5F}\x{1EE60}\x{1EE61}-\x{1EE62}\x{1EE63}\x{1EE64}\x{1EE65}-\x{1EE66}\x{1EE67}-\x{1EE6A}\x{1EE6B}\x{1EE6C}-\x{1EE72}\x{1EE73}\x{1EE74}-\x{1EE77}\x{1EE78}\x{1EE79}-\x{1EE7C}\x{1EE7D}\x{1EE7E}\x{1EE7F}\x{1EE80}-\x{1EE89}\x{1EE8A}\x{1EE8B}-\x{1EE9B}\x{1EE9C}-\x{1EEA0}\x{1EEA1}-\x{1EEA3}\x{1EEA4}\x{1EEA5}-\x{1EEA9}\x{1EEAA}\x{1EEAB}-\x{1EEBB}\x{1EEBC}-\x{1EEEF}\x{1EEF0}-\x{1EEF1}\x{1EEF2}-\x{1EEFF}\x{1EF00}-\x{1EFFF}\x{1F000}-\x{1F02B}\x{1F030}-\x{1F093}\x{1F0A0}-\x{1F0AE}\x{1F0B1}-\x{1F0BF}\x{1F0C1}-\x{1F0CF}\x{1F0D1}-\x{1F0F5}\x{1F10B}-\x{1F10C}\x{1F10D}-\x{1F10F}\x{1F12F}\x{1F16A}-\x{1F16F}\x{1F1AD}\x{1F260}-\x{1F265}\x{1F300}-\x{1F3FA}\x{1F3FB}-\x{1F3FF}\x{1F400}-\x{1F6D7}\x{1F6E0}-\x{1F6EC}\x{1F6F0}-\x{1F6FC}\x{1F700}-\x{1F773}\x{1F780}-\x{1F7D8}\x{1F7E0}-\x{1F7EB}\x{1F800}-\x{1F80B}\x{1F810}-\x{1F847}\x{1F850}-\x{1F859}\x{1F860}-\x{1F887}\x{1F890}-\x{1F8AD}\x{1F8B0}-\x{1F8B1}\x{1F900}-\x{1F978}\x{1F97A}-\x{1F9CB}\x{1F9CD}-\x{1FA53}\x{1FA60}-\x{1FA6D}\x{1FA70}-\x{1FA74}\x{1FA78}-\x{1FA7A}\x{1FA80}-\x{1FA86}\x{1FA90}-\x{1FAA8}\x{1FAB0}-\x{1FAB6}\x{1FAC0}-\x{1FAC2}\x{1FAD0}-\x{1FAD6}\x{1FB00}-\x{1FB92}\x{1FB94}-\x{1FBCA}\x{1FFFE}-\x{1FFFF}\x{2FFFE}-\x{2FFFF}\x{3FFFE}-\x{3FFFF}\x{4FFFE}-\x{4FFFF}\x{5FFFE}-\x{5FFFF}\x{6FFFE}-\x{6FFFF}\x{7FFFE}-\x{7FFFF}\x{8FFFE}-\x{8FFFF}\x{9FFFE}-\x{9FFFF}\x{AFFFE}-\x{AFFFF}\x{BFFFE}-\x{BFFFF}\x{CFFFE}-\x{CFFFF}\x{DFFFE}-\x{E0000}\x{E0001}\x{E0002}-\x{E001F}\x{E0020}-\x{E007F}\x{E0080}-\x{E00FF}\x{E0100}-\x{E01EF}\x{E01F0}-\x{E0FFF}\x{EFFFE}-\x{EFFFF}\x{FFFFE}-\x{FFFFF}\x{10FFFE}-\x{10FFFF}][\x{0300}-\x{036F}\x{0483}-\x{0487}\x{0488}-\x{0489}\x{0591}-\x{05BD}\x{05BF}\x{05C1}-\x{05C2}\x{05C4}-\x{05C5}\x{05C7}\x{0610}-\x{061A}\x{064B}-\x{065F}\x{0670}\x{06D6}-\x{06DC}\x{06DF}-\x{06E4}\x{06E7}-\x{06E8}\x{06EA}-\x{06ED}\x{0711}\x{0730}-\x{074A}\x{07A6}-\x{07B0}\x{07EB}-\x{07F3}\x{07FD}\x{0816}-\x{0819}\x{081B}-\x{0823}\x{0825}-\x{0827}\x{0829}-\x{082D}\x{0859}-\x{085B}\x{08D3}-\x{08E1}\x{08E3}-\x{0902}\x{093A}\x{093C}\x{0941}-\x{0948}\x{094D}\x{0951}-\x{0957}\x{0962}-\x{0963}\x{0981}\x{09BC}\x{09C1}-\x{09C4}\x{09CD}\x{09E2}-\x{09E3}\x{09FE}\x{0A01}-\x{0A02}\x{0A3C}\x{0A41}-\x{0A42}\x{0A47}-\x{0A48}\x{0A4B}-\x{0A4D}\x{0A51}\x{0A70}-\x{0A71}\x{0A75}\x{0A81}-\x{0A82}\x{0ABC}\x{0AC1}-\x{0AC5}\x{0AC7}-\x{0AC8}\x{0ACD}\x{0AE2}-\x{0AE3}\x{0AFA}-\x{0AFF}\x{0B01}\x{0B3C}\x{0B3F}\x{0B41}-\x{0B44}\x{0B4D}\x{0B55}-\x{0B56}\x{0B62}-\x{0B63}\x{0B82}\x{0BC0}\x{0BCD}\x{0C00}\x{0C04}\x{0C3E}-\x{0C40}\x{0C46}-\x{0C48}\x{0C4A}-\x{0C4D}\x{0C55}-\x{0C56}\x{0C62}-\x{0C63}\x{0C81}\x{0CBC}\x{0CCC}-\x{0CCD}\x{0CE2}-\x{0CE3}\x{0D00}-\x{0D01}\x{0D3B}-\x{0D3C}\x{0D41}-\x{0D44}\x{0D4D}\x{0D62}-\x{0D63}\x{0D81}\x{0DCA}\x{0DD2}-\x{0DD4}\x{0DD6}\x{0E31}\x{0E34}-\x{0E3A}\x{0E47}-\x{0E4E}\x{0EB1}\x{0EB4}-\x{0EBC}\x{0EC8}-\x{0ECD}\x{0F18}-\x{0F19}\x{0F35}\x{0F37}\x{0F39}\x{0F71}-\x{0F7E}\x{0F80}-\x{0F84}\x{0F86}-\x{0F87}\x{0F8D}-\x{0F97}\x{0F99}-\x{0FBC}\x{0FC6}\x{102D}-\x{1030}\x{1032}-\x{1037}\x{1039}-\x{103A}\x{103D}-\x{103E}\x{1058}-\x{1059}\x{105E}-\x{1060}\x{1071}-\x{1074}\x{1082}\x{1085}-\x{1086}\x{108D}\x{109D}\x{135D}-\x{135F}\x{1712}-\x{1714}\x{1732}-\x{1734}\x{1752}-\x{1753}\x{1772}-\x{1773}\x{17B4}-\x{17B5}\x{17B7}-\x{17BD}\x{17C6}\x{17C9}-\x{17D3}\x{17DD}\x{180B}-\x{180D}\x{1885}-\x{1886}\x{18A9}\x{1920}-\x{1922}\x{1927}-\x{1928}\x{1932}\x{1939}-\x{193B}\x{1A17}-\x{1A18}\x{1A1B}\x{1A56}\x{1A58}-\x{1A5E}\x{1A60}\x{1A62}\x{1A65}-\x{1A6C}\x{1A73}-\x{1A7C}\x{1A7F}\x{1AB0}-\x{1ABD}\x{1ABE}\x{1ABF}-\x{1AC0}\x{1B00}-\x{1B03}\x{1B34}\x{1B36}-\x{1B3A}\x{1B3C}\x{1B42}\x{1B6B}-\x{1B73}\x{1B80}-\x{1B81}\x{1BA2}-\x{1BA5}\x{1BA8}-\x{1BA9}\x{1BAB}-\x{1BAD}\x{1BE6}\x{1BE8}-\x{1BE9}\x{1BED}\x{1BEF}-\x{1BF1}\x{1C2C}-\x{1C33}\x{1C36}-\x{1C37}\x{1CD0}-\x{1CD2}\x{1CD4}-\x{1CE0}\x{1CE2}-\x{1CE8}\x{1CED}\x{1CF4}\x{1CF8}-\x{1CF9}\x{1DC0}-\x{1DF9}\x{1DFB}-\x{1DFF}\x{20D0}-\x{20DC}\x{20DD}-\x{20E0}\x{20E1}\x{20E2}-\x{20E4}\x{20E5}-\x{20F0}\x{2CEF}-\x{2CF1}\x{2D7F}\x{2DE0}-\x{2DFF}\x{302A}-\x{302D}\x{3099}-\x{309A}\x{A66F}\x{A670}-\x{A672}\x{A674}-\x{A67D}\x{A69E}-\x{A69F}\x{A6F0}-\x{A6F1}\x{A802}\x{A806}\x{A80B}\x{A825}-\x{A826}\x{A82C}\x{A8C4}-\x{A8C5}\x{A8E0}-\x{A8F1}\x{A8FF}\x{A926}-\x{A92D}\x{A947}-\x{A951}\x{A980}-\x{A982}\x{A9B3}\x{A9B6}-\x{A9B9}\x{A9BC}-\x{A9BD}\x{A9E5}\x{AA29}-\x{AA2E}\x{AA31}-\x{AA32}\x{AA35}-\x{AA36}\x{AA43}\x{AA4C}\x{AA7C}\x{AAB0}\x{AAB2}-\x{AAB4}\x{AAB7}-\x{AAB8}\x{AABE}-\x{AABF}\x{AAC1}\x{AAEC}-\x{AAED}\x{AAF6}\x{ABE5}\x{ABE8}\x{ABED}\x{FB1E}\x{FE00}-\x{FE0F}\x{FE20}-\x{FE2F}\x{101FD}\x{102E0}\x{10376}-\x{1037A}\x{10A01}-\x{10A03}\x{10A05}-\x{10A06}\x{10A0C}-\x{10A0F}\x{10A38}-\x{10A3A}\x{10A3F}\x{10AE5}-\x{10AE6}\x{10D24}-\x{10D27}\x{10EAB}-\x{10EAC}\x{10F46}-\x{10F50}\x{11001}\x{11038}-\x{11046}\x{1107F}-\x{11081}\x{110B3}-\x{110B6}\x{110B9}-\x{110BA}\x{11100}-\x{11102}\x{11127}-\x{1112B}\x{1112D}-\x{11134}\x{11173}\x{11180}-\x{11181}\x{111B6}-\x{111BE}\x{111C9}-\x{111CC}\x{111CF}\x{1122F}-\x{11231}\x{11234}\x{11236}-\x{11237}\x{1123E}\x{112DF}\x{112E3}-\x{112EA}\x{11300}-\x{11301}\x{1133B}-\x{1133C}\x{11340}\x{11366}-\x{1136C}\x{11370}-\x{11374}\x{11438}-\x{1143F}\x{11442}-\x{11444}\x{11446}\x{1145E}\x{114B3}-\x{114B8}\x{114BA}\x{114BF}-\x{114C0}\x{114C2}-\x{114C3}\x{115B2}-\x{115B5}\x{115BC}-\x{115BD}\x{115BF}-\x{115C0}\x{115DC}-\x{115DD}\x{11633}-\x{1163A}\x{1163D}\x{1163F}-\x{11640}\x{116AB}\x{116AD}\x{116B0}-\x{116B5}\x{116B7}\x{1171D}-\x{1171F}\x{11722}-\x{11725}\x{11727}-\x{1172B}\x{1182F}-\x{11837}\x{11839}-\x{1183A}\x{1193B}-\x{1193C}\x{1193E}\x{11943}\x{119D4}-\x{119D7}\x{119DA}-\x{119DB}\x{119E0}\x{11A01}-\x{11A06}\x{11A09}-\x{11A0A}\x{11A33}-\x{11A38}\x{11A3B}-\x{11A3E}\x{11A47}\x{11A51}-\x{11A56}\x{11A59}-\x{11A5B}\x{11A8A}-\x{11A96}\x{11A98}-\x{11A99}\x{11C30}-\x{11C36}\x{11C38}-\x{11C3D}\x{11C92}-\x{11CA7}\x{11CAA}-\x{11CB0}\x{11CB2}-\x{11CB3}\x{11CB5}-\x{11CB6}\x{11D31}-\x{11D36}\x{11D3A}\x{11D3C}-\x{11D3D}\x{11D3F}-\x{11D45}\x{11D47}\x{11D90}-\x{11D91}\x{11D95}\x{11D97}\x{11EF3}-\x{11EF4}\x{16AF0}-\x{16AF4}\x{16B30}-\x{16B36}\x{16F4F}\x{16F8F}-\x{16F92}\x{16FE4}\x{1BC9D}-\x{1BC9E}\x{1D167}-\x{1D169}\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{1E000}-\x{1E006}\x{1E008}-\x{1E018}\x{1E01B}-\x{1E021}\x{1E023}-\x{1E024}\x{1E026}-\x{1E02A}\x{1E130}-\x{1E136}\x{1E2EC}-\x{1E2EF}\x{1E8D0}-\x{1E8D6}\x{1E944}-\x{1E94A}\x{E0100}-\x{E01EF}]*$/u'; + + const ZWNJ = '/([\x{A872}\x{10ACD}\x{10AD7}\x{10D00}\x{10FCB}\x{0620}\x{0626}\x{0628}\x{062A}-\x{062E}\x{0633}-\x{063F}\x{0641}-\x{0647}\x{0649}-\x{064A}\x{066E}-\x{066F}\x{0678}-\x{0687}\x{069A}-\x{06BF}\x{06C1}-\x{06C2}\x{06CC}\x{06CE}\x{06D0}-\x{06D1}\x{06FA}-\x{06FC}\x{06FF}\x{0712}-\x{0714}\x{071A}-\x{071D}\x{071F}-\x{0727}\x{0729}\x{072B}\x{072D}-\x{072E}\x{074E}-\x{0758}\x{075C}-\x{076A}\x{076D}-\x{0770}\x{0772}\x{0775}-\x{0777}\x{077A}-\x{077F}\x{07CA}-\x{07EA}\x{0841}-\x{0845}\x{0848}\x{084A}-\x{0853}\x{0855}\x{0860}\x{0862}-\x{0865}\x{0868}\x{08A0}-\x{08A9}\x{08AF}-\x{08B0}\x{08B3}-\x{08B4}\x{08B6}-\x{08B8}\x{08BA}-\x{08C7}\x{1807}\x{1820}-\x{1842}\x{1843}\x{1844}-\x{1878}\x{1887}-\x{18A8}\x{18AA}\x{A840}-\x{A871}\x{10AC0}-\x{10AC4}\x{10AD3}-\x{10AD6}\x{10AD8}-\x{10ADC}\x{10ADE}-\x{10AE0}\x{10AEB}-\x{10AEE}\x{10B80}\x{10B82}\x{10B86}-\x{10B88}\x{10B8A}-\x{10B8B}\x{10B8D}\x{10B90}\x{10BAD}-\x{10BAE}\x{10D01}-\x{10D21}\x{10D23}\x{10F30}-\x{10F32}\x{10F34}-\x{10F44}\x{10F51}-\x{10F53}\x{10FB0}\x{10FB2}-\x{10FB3}\x{10FB8}\x{10FBB}-\x{10FBC}\x{10FBE}-\x{10FBF}\x{10FC1}\x{10FC4}\x{10FCA}\x{1E900}-\x{1E943}][\x{00AD}\x{0300}-\x{036F}\x{0483}-\x{0487}\x{0488}-\x{0489}\x{0591}-\x{05BD}\x{05BF}\x{05C1}-\x{05C2}\x{05C4}-\x{05C5}\x{05C7}\x{0610}-\x{061A}\x{061C}\x{064B}-\x{065F}\x{0670}\x{06D6}-\x{06DC}\x{06DF}-\x{06E4}\x{06E7}-\x{06E8}\x{06EA}-\x{06ED}\x{070F}\x{0711}\x{0730}-\x{074A}\x{07A6}-\x{07B0}\x{07EB}-\x{07F3}\x{07FD}\x{0816}-\x{0819}\x{081B}-\x{0823}\x{0825}-\x{0827}\x{0829}-\x{082D}\x{0859}-\x{085B}\x{08D3}-\x{08E1}\x{08E3}-\x{0902}\x{093A}\x{093C}\x{0941}-\x{0948}\x{094D}\x{0951}-\x{0957}\x{0962}-\x{0963}\x{0981}\x{09BC}\x{09C1}-\x{09C4}\x{09CD}\x{09E2}-\x{09E3}\x{09FE}\x{0A01}-\x{0A02}\x{0A3C}\x{0A41}-\x{0A42}\x{0A47}-\x{0A48}\x{0A4B}-\x{0A4D}\x{0A51}\x{0A70}-\x{0A71}\x{0A75}\x{0A81}-\x{0A82}\x{0ABC}\x{0AC1}-\x{0AC5}\x{0AC7}-\x{0AC8}\x{0ACD}\x{0AE2}-\x{0AE3}\x{0AFA}-\x{0AFF}\x{0B01}\x{0B3C}\x{0B3F}\x{0B41}-\x{0B44}\x{0B4D}\x{0B55}-\x{0B56}\x{0B62}-\x{0B63}\x{0B82}\x{0BC0}\x{0BCD}\x{0C00}\x{0C04}\x{0C3E}-\x{0C40}\x{0C46}-\x{0C48}\x{0C4A}-\x{0C4D}\x{0C55}-\x{0C56}\x{0C62}-\x{0C63}\x{0C81}\x{0CBC}\x{0CBF}\x{0CC6}\x{0CCC}-\x{0CCD}\x{0CE2}-\x{0CE3}\x{0D00}-\x{0D01}\x{0D3B}-\x{0D3C}\x{0D41}-\x{0D44}\x{0D4D}\x{0D62}-\x{0D63}\x{0D81}\x{0DCA}\x{0DD2}-\x{0DD4}\x{0DD6}\x{0E31}\x{0E34}-\x{0E3A}\x{0E47}-\x{0E4E}\x{0EB1}\x{0EB4}-\x{0EBC}\x{0EC8}-\x{0ECD}\x{0F18}-\x{0F19}\x{0F35}\x{0F37}\x{0F39}\x{0F71}-\x{0F7E}\x{0F80}-\x{0F84}\x{0F86}-\x{0F87}\x{0F8D}-\x{0F97}\x{0F99}-\x{0FBC}\x{0FC6}\x{102D}-\x{1030}\x{1032}-\x{1037}\x{1039}-\x{103A}\x{103D}-\x{103E}\x{1058}-\x{1059}\x{105E}-\x{1060}\x{1071}-\x{1074}\x{1082}\x{1085}-\x{1086}\x{108D}\x{109D}\x{135D}-\x{135F}\x{1712}-\x{1714}\x{1732}-\x{1734}\x{1752}-\x{1753}\x{1772}-\x{1773}\x{17B4}-\x{17B5}\x{17B7}-\x{17BD}\x{17C6}\x{17C9}-\x{17D3}\x{17DD}\x{180B}-\x{180D}\x{1885}-\x{1886}\x{18A9}\x{1920}-\x{1922}\x{1927}-\x{1928}\x{1932}\x{1939}-\x{193B}\x{1A17}-\x{1A18}\x{1A1B}\x{1A56}\x{1A58}-\x{1A5E}\x{1A60}\x{1A62}\x{1A65}-\x{1A6C}\x{1A73}-\x{1A7C}\x{1A7F}\x{1AB0}-\x{1ABD}\x{1ABE}\x{1ABF}-\x{1AC0}\x{1B00}-\x{1B03}\x{1B34}\x{1B36}-\x{1B3A}\x{1B3C}\x{1B42}\x{1B6B}-\x{1B73}\x{1B80}-\x{1B81}\x{1BA2}-\x{1BA5}\x{1BA8}-\x{1BA9}\x{1BAB}-\x{1BAD}\x{1BE6}\x{1BE8}-\x{1BE9}\x{1BED}\x{1BEF}-\x{1BF1}\x{1C2C}-\x{1C33}\x{1C36}-\x{1C37}\x{1CD0}-\x{1CD2}\x{1CD4}-\x{1CE0}\x{1CE2}-\x{1CE8}\x{1CED}\x{1CF4}\x{1CF8}-\x{1CF9}\x{1DC0}-\x{1DF9}\x{1DFB}-\x{1DFF}\x{200B}\x{200E}-\x{200F}\x{202A}-\x{202E}\x{2060}-\x{2064}\x{206A}-\x{206F}\x{20D0}-\x{20DC}\x{20DD}-\x{20E0}\x{20E1}\x{20E2}-\x{20E4}\x{20E5}-\x{20F0}\x{2CEF}-\x{2CF1}\x{2D7F}\x{2DE0}-\x{2DFF}\x{302A}-\x{302D}\x{3099}-\x{309A}\x{A66F}\x{A670}-\x{A672}\x{A674}-\x{A67D}\x{A69E}-\x{A69F}\x{A6F0}-\x{A6F1}\x{A802}\x{A806}\x{A80B}\x{A825}-\x{A826}\x{A82C}\x{A8C4}-\x{A8C5}\x{A8E0}-\x{A8F1}\x{A8FF}\x{A926}-\x{A92D}\x{A947}-\x{A951}\x{A980}-\x{A982}\x{A9B3}\x{A9B6}-\x{A9B9}\x{A9BC}-\x{A9BD}\x{A9E5}\x{AA29}-\x{AA2E}\x{AA31}-\x{AA32}\x{AA35}-\x{AA36}\x{AA43}\x{AA4C}\x{AA7C}\x{AAB0}\x{AAB2}-\x{AAB4}\x{AAB7}-\x{AAB8}\x{AABE}-\x{AABF}\x{AAC1}\x{AAEC}-\x{AAED}\x{AAF6}\x{ABE5}\x{ABE8}\x{ABED}\x{FB1E}\x{FE00}-\x{FE0F}\x{FE20}-\x{FE2F}\x{FEFF}\x{FFF9}-\x{FFFB}\x{101FD}\x{102E0}\x{10376}-\x{1037A}\x{10A01}-\x{10A03}\x{10A05}-\x{10A06}\x{10A0C}-\x{10A0F}\x{10A38}-\x{10A3A}\x{10A3F}\x{10AE5}-\x{10AE6}\x{10D24}-\x{10D27}\x{10EAB}-\x{10EAC}\x{10F46}-\x{10F50}\x{11001}\x{11038}-\x{11046}\x{1107F}-\x{11081}\x{110B3}-\x{110B6}\x{110B9}-\x{110BA}\x{11100}-\x{11102}\x{11127}-\x{1112B}\x{1112D}-\x{11134}\x{11173}\x{11180}-\x{11181}\x{111B6}-\x{111BE}\x{111C9}-\x{111CC}\x{111CF}\x{1122F}-\x{11231}\x{11234}\x{11236}-\x{11237}\x{1123E}\x{112DF}\x{112E3}-\x{112EA}\x{11300}-\x{11301}\x{1133B}-\x{1133C}\x{11340}\x{11366}-\x{1136C}\x{11370}-\x{11374}\x{11438}-\x{1143F}\x{11442}-\x{11444}\x{11446}\x{1145E}\x{114B3}-\x{114B8}\x{114BA}\x{114BF}-\x{114C0}\x{114C2}-\x{114C3}\x{115B2}-\x{115B5}\x{115BC}-\x{115BD}\x{115BF}-\x{115C0}\x{115DC}-\x{115DD}\x{11633}-\x{1163A}\x{1163D}\x{1163F}-\x{11640}\x{116AB}\x{116AD}\x{116B0}-\x{116B5}\x{116B7}\x{1171D}-\x{1171F}\x{11722}-\x{11725}\x{11727}-\x{1172B}\x{1182F}-\x{11837}\x{11839}-\x{1183A}\x{1193B}-\x{1193C}\x{1193E}\x{11943}\x{119D4}-\x{119D7}\x{119DA}-\x{119DB}\x{119E0}\x{11A01}-\x{11A0A}\x{11A33}-\x{11A38}\x{11A3B}-\x{11A3E}\x{11A47}\x{11A51}-\x{11A56}\x{11A59}-\x{11A5B}\x{11A8A}-\x{11A96}\x{11A98}-\x{11A99}\x{11C30}-\x{11C36}\x{11C38}-\x{11C3D}\x{11C3F}\x{11C92}-\x{11CA7}\x{11CAA}-\x{11CB0}\x{11CB2}-\x{11CB3}\x{11CB5}-\x{11CB6}\x{11D31}-\x{11D36}\x{11D3A}\x{11D3C}-\x{11D3D}\x{11D3F}-\x{11D45}\x{11D47}\x{11D90}-\x{11D91}\x{11D95}\x{11D97}\x{11EF3}-\x{11EF4}\x{13430}-\x{13438}\x{16AF0}-\x{16AF4}\x{16B30}-\x{16B36}\x{16F4F}\x{16F8F}-\x{16F92}\x{16FE4}\x{1BC9D}-\x{1BC9E}\x{1BCA0}-\x{1BCA3}\x{1D167}-\x{1D169}\x{1D173}-\x{1D17A}\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{1E000}-\x{1E006}\x{1E008}-\x{1E018}\x{1E01B}-\x{1E021}\x{1E023}-\x{1E024}\x{1E026}-\x{1E02A}\x{1E130}-\x{1E136}\x{1E2EC}-\x{1E2EF}\x{1E8D0}-\x{1E8D6}\x{1E944}-\x{1E94A}\x{1E94B}\x{E0001}\x{E0020}-\x{E007F}\x{E0100}-\x{E01EF}]*\x{200C}[\x{00AD}\x{0300}-\x{036F}\x{0483}-\x{0487}\x{0488}-\x{0489}\x{0591}-\x{05BD}\x{05BF}\x{05C1}-\x{05C2}\x{05C4}-\x{05C5}\x{05C7}\x{0610}-\x{061A}\x{061C}\x{064B}-\x{065F}\x{0670}\x{06D6}-\x{06DC}\x{06DF}-\x{06E4}\x{06E7}-\x{06E8}\x{06EA}-\x{06ED}\x{070F}\x{0711}\x{0730}-\x{074A}\x{07A6}-\x{07B0}\x{07EB}-\x{07F3}\x{07FD}\x{0816}-\x{0819}\x{081B}-\x{0823}\x{0825}-\x{0827}\x{0829}-\x{082D}\x{0859}-\x{085B}\x{08D3}-\x{08E1}\x{08E3}-\x{0902}\x{093A}\x{093C}\x{0941}-\x{0948}\x{094D}\x{0951}-\x{0957}\x{0962}-\x{0963}\x{0981}\x{09BC}\x{09C1}-\x{09C4}\x{09CD}\x{09E2}-\x{09E3}\x{09FE}\x{0A01}-\x{0A02}\x{0A3C}\x{0A41}-\x{0A42}\x{0A47}-\x{0A48}\x{0A4B}-\x{0A4D}\x{0A51}\x{0A70}-\x{0A71}\x{0A75}\x{0A81}-\x{0A82}\x{0ABC}\x{0AC1}-\x{0AC5}\x{0AC7}-\x{0AC8}\x{0ACD}\x{0AE2}-\x{0AE3}\x{0AFA}-\x{0AFF}\x{0B01}\x{0B3C}\x{0B3F}\x{0B41}-\x{0B44}\x{0B4D}\x{0B55}-\x{0B56}\x{0B62}-\x{0B63}\x{0B82}\x{0BC0}\x{0BCD}\x{0C00}\x{0C04}\x{0C3E}-\x{0C40}\x{0C46}-\x{0C48}\x{0C4A}-\x{0C4D}\x{0C55}-\x{0C56}\x{0C62}-\x{0C63}\x{0C81}\x{0CBC}\x{0CBF}\x{0CC6}\x{0CCC}-\x{0CCD}\x{0CE2}-\x{0CE3}\x{0D00}-\x{0D01}\x{0D3B}-\x{0D3C}\x{0D41}-\x{0D44}\x{0D4D}\x{0D62}-\x{0D63}\x{0D81}\x{0DCA}\x{0DD2}-\x{0DD4}\x{0DD6}\x{0E31}\x{0E34}-\x{0E3A}\x{0E47}-\x{0E4E}\x{0EB1}\x{0EB4}-\x{0EBC}\x{0EC8}-\x{0ECD}\x{0F18}-\x{0F19}\x{0F35}\x{0F37}\x{0F39}\x{0F71}-\x{0F7E}\x{0F80}-\x{0F84}\x{0F86}-\x{0F87}\x{0F8D}-\x{0F97}\x{0F99}-\x{0FBC}\x{0FC6}\x{102D}-\x{1030}\x{1032}-\x{1037}\x{1039}-\x{103A}\x{103D}-\x{103E}\x{1058}-\x{1059}\x{105E}-\x{1060}\x{1071}-\x{1074}\x{1082}\x{1085}-\x{1086}\x{108D}\x{109D}\x{135D}-\x{135F}\x{1712}-\x{1714}\x{1732}-\x{1734}\x{1752}-\x{1753}\x{1772}-\x{1773}\x{17B4}-\x{17B5}\x{17B7}-\x{17BD}\x{17C6}\x{17C9}-\x{17D3}\x{17DD}\x{180B}-\x{180D}\x{1885}-\x{1886}\x{18A9}\x{1920}-\x{1922}\x{1927}-\x{1928}\x{1932}\x{1939}-\x{193B}\x{1A17}-\x{1A18}\x{1A1B}\x{1A56}\x{1A58}-\x{1A5E}\x{1A60}\x{1A62}\x{1A65}-\x{1A6C}\x{1A73}-\x{1A7C}\x{1A7F}\x{1AB0}-\x{1ABD}\x{1ABE}\x{1ABF}-\x{1AC0}\x{1B00}-\x{1B03}\x{1B34}\x{1B36}-\x{1B3A}\x{1B3C}\x{1B42}\x{1B6B}-\x{1B73}\x{1B80}-\x{1B81}\x{1BA2}-\x{1BA5}\x{1BA8}-\x{1BA9}\x{1BAB}-\x{1BAD}\x{1BE6}\x{1BE8}-\x{1BE9}\x{1BED}\x{1BEF}-\x{1BF1}\x{1C2C}-\x{1C33}\x{1C36}-\x{1C37}\x{1CD0}-\x{1CD2}\x{1CD4}-\x{1CE0}\x{1CE2}-\x{1CE8}\x{1CED}\x{1CF4}\x{1CF8}-\x{1CF9}\x{1DC0}-\x{1DF9}\x{1DFB}-\x{1DFF}\x{200B}\x{200E}-\x{200F}\x{202A}-\x{202E}\x{2060}-\x{2064}\x{206A}-\x{206F}\x{20D0}-\x{20DC}\x{20DD}-\x{20E0}\x{20E1}\x{20E2}-\x{20E4}\x{20E5}-\x{20F0}\x{2CEF}-\x{2CF1}\x{2D7F}\x{2DE0}-\x{2DFF}\x{302A}-\x{302D}\x{3099}-\x{309A}\x{A66F}\x{A670}-\x{A672}\x{A674}-\x{A67D}\x{A69E}-\x{A69F}\x{A6F0}-\x{A6F1}\x{A802}\x{A806}\x{A80B}\x{A825}-\x{A826}\x{A82C}\x{A8C4}-\x{A8C5}\x{A8E0}-\x{A8F1}\x{A8FF}\x{A926}-\x{A92D}\x{A947}-\x{A951}\x{A980}-\x{A982}\x{A9B3}\x{A9B6}-\x{A9B9}\x{A9BC}-\x{A9BD}\x{A9E5}\x{AA29}-\x{AA2E}\x{AA31}-\x{AA32}\x{AA35}-\x{AA36}\x{AA43}\x{AA4C}\x{AA7C}\x{AAB0}\x{AAB2}-\x{AAB4}\x{AAB7}-\x{AAB8}\x{AABE}-\x{AABF}\x{AAC1}\x{AAEC}-\x{AAED}\x{AAF6}\x{ABE5}\x{ABE8}\x{ABED}\x{FB1E}\x{FE00}-\x{FE0F}\x{FE20}-\x{FE2F}\x{FEFF}\x{FFF9}-\x{FFFB}\x{101FD}\x{102E0}\x{10376}-\x{1037A}\x{10A01}-\x{10A03}\x{10A05}-\x{10A06}\x{10A0C}-\x{10A0F}\x{10A38}-\x{10A3A}\x{10A3F}\x{10AE5}-\x{10AE6}\x{10D24}-\x{10D27}\x{10EAB}-\x{10EAC}\x{10F46}-\x{10F50}\x{11001}\x{11038}-\x{11046}\x{1107F}-\x{11081}\x{110B3}-\x{110B6}\x{110B9}-\x{110BA}\x{11100}-\x{11102}\x{11127}-\x{1112B}\x{1112D}-\x{11134}\x{11173}\x{11180}-\x{11181}\x{111B6}-\x{111BE}\x{111C9}-\x{111CC}\x{111CF}\x{1122F}-\x{11231}\x{11234}\x{11236}-\x{11237}\x{1123E}\x{112DF}\x{112E3}-\x{112EA}\x{11300}-\x{11301}\x{1133B}-\x{1133C}\x{11340}\x{11366}-\x{1136C}\x{11370}-\x{11374}\x{11438}-\x{1143F}\x{11442}-\x{11444}\x{11446}\x{1145E}\x{114B3}-\x{114B8}\x{114BA}\x{114BF}-\x{114C0}\x{114C2}-\x{114C3}\x{115B2}-\x{115B5}\x{115BC}-\x{115BD}\x{115BF}-\x{115C0}\x{115DC}-\x{115DD}\x{11633}-\x{1163A}\x{1163D}\x{1163F}-\x{11640}\x{116AB}\x{116AD}\x{116B0}-\x{116B5}\x{116B7}\x{1171D}-\x{1171F}\x{11722}-\x{11725}\x{11727}-\x{1172B}\x{1182F}-\x{11837}\x{11839}-\x{1183A}\x{1193B}-\x{1193C}\x{1193E}\x{11943}\x{119D4}-\x{119D7}\x{119DA}-\x{119DB}\x{119E0}\x{11A01}-\x{11A0A}\x{11A33}-\x{11A38}\x{11A3B}-\x{11A3E}\x{11A47}\x{11A51}-\x{11A56}\x{11A59}-\x{11A5B}\x{11A8A}-\x{11A96}\x{11A98}-\x{11A99}\x{11C30}-\x{11C36}\x{11C38}-\x{11C3D}\x{11C3F}\x{11C92}-\x{11CA7}\x{11CAA}-\x{11CB0}\x{11CB2}-\x{11CB3}\x{11CB5}-\x{11CB6}\x{11D31}-\x{11D36}\x{11D3A}\x{11D3C}-\x{11D3D}\x{11D3F}-\x{11D45}\x{11D47}\x{11D90}-\x{11D91}\x{11D95}\x{11D97}\x{11EF3}-\x{11EF4}\x{13430}-\x{13438}\x{16AF0}-\x{16AF4}\x{16B30}-\x{16B36}\x{16F4F}\x{16F8F}-\x{16F92}\x{16FE4}\x{1BC9D}-\x{1BC9E}\x{1BCA0}-\x{1BCA3}\x{1D167}-\x{1D169}\x{1D173}-\x{1D17A}\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{1E000}-\x{1E006}\x{1E008}-\x{1E018}\x{1E01B}-\x{1E021}\x{1E023}-\x{1E024}\x{1E026}-\x{1E02A}\x{1E130}-\x{1E136}\x{1E2EC}-\x{1E2EF}\x{1E8D0}-\x{1E8D6}\x{1E944}-\x{1E94A}\x{1E94B}\x{E0001}\x{E0020}-\x{E007F}\x{E0100}-\x{E01EF}]*)[\x{0622}-\x{0625}\x{0627}\x{0629}\x{062F}-\x{0632}\x{0648}\x{0671}-\x{0673}\x{0675}-\x{0677}\x{0688}-\x{0699}\x{06C0}\x{06C3}-\x{06CB}\x{06CD}\x{06CF}\x{06D2}-\x{06D3}\x{06D5}\x{06EE}-\x{06EF}\x{0710}\x{0715}-\x{0719}\x{071E}\x{0728}\x{072A}\x{072C}\x{072F}\x{074D}\x{0759}-\x{075B}\x{076B}-\x{076C}\x{0771}\x{0773}-\x{0774}\x{0778}-\x{0779}\x{0840}\x{0846}-\x{0847}\x{0849}\x{0854}\x{0856}-\x{0858}\x{0867}\x{0869}-\x{086A}\x{08AA}-\x{08AC}\x{08AE}\x{08B1}-\x{08B2}\x{08B9}\x{10AC5}\x{10AC7}\x{10AC9}-\x{10ACA}\x{10ACE}-\x{10AD2}\x{10ADD}\x{10AE1}\x{10AE4}\x{10AEF}\x{10B81}\x{10B83}-\x{10B85}\x{10B89}\x{10B8C}\x{10B8E}-\x{10B8F}\x{10B91}\x{10BA9}-\x{10BAC}\x{10D22}\x{10F33}\x{10F54}\x{10FB4}-\x{10FB6}\x{10FB9}-\x{10FBA}\x{10FBD}\x{10FC2}-\x{10FC3}\x{10FC9}\x{0620}\x{0626}\x{0628}\x{062A}-\x{062E}\x{0633}-\x{063F}\x{0641}-\x{0647}\x{0649}-\x{064A}\x{066E}-\x{066F}\x{0678}-\x{0687}\x{069A}-\x{06BF}\x{06C1}-\x{06C2}\x{06CC}\x{06CE}\x{06D0}-\x{06D1}\x{06FA}-\x{06FC}\x{06FF}\x{0712}-\x{0714}\x{071A}-\x{071D}\x{071F}-\x{0727}\x{0729}\x{072B}\x{072D}-\x{072E}\x{074E}-\x{0758}\x{075C}-\x{076A}\x{076D}-\x{0770}\x{0772}\x{0775}-\x{0777}\x{077A}-\x{077F}\x{07CA}-\x{07EA}\x{0841}-\x{0845}\x{0848}\x{084A}-\x{0853}\x{0855}\x{0860}\x{0862}-\x{0865}\x{0868}\x{08A0}-\x{08A9}\x{08AF}-\x{08B0}\x{08B3}-\x{08B4}\x{08B6}-\x{08B8}\x{08BA}-\x{08C7}\x{1807}\x{1820}-\x{1842}\x{1843}\x{1844}-\x{1878}\x{1887}-\x{18A8}\x{18AA}\x{A840}-\x{A871}\x{10AC0}-\x{10AC4}\x{10AD3}-\x{10AD6}\x{10AD8}-\x{10ADC}\x{10ADE}-\x{10AE0}\x{10AEB}-\x{10AEE}\x{10B80}\x{10B82}\x{10B86}-\x{10B88}\x{10B8A}-\x{10B8B}\x{10B8D}\x{10B90}\x{10BAD}-\x{10BAE}\x{10D01}-\x{10D21}\x{10D23}\x{10F30}-\x{10F32}\x{10F34}-\x{10F44}\x{10F51}-\x{10F53}\x{10FB0}\x{10FB2}-\x{10FB3}\x{10FB8}\x{10FBB}-\x{10FBC}\x{10FBE}-\x{10FBF}\x{10FC1}\x{10FC4}\x{10FCA}\x{1E900}-\x{1E943}]/u'; +} diff --git a/3rdparty/symfony/polyfill-intl-idn/Resources/unidata/deviation.php b/3rdparty/symfony/polyfill-intl-idn/Resources/unidata/deviation.php new file mode 100644 index 00000000..0bbd3356 --- /dev/null +++ b/3rdparty/symfony/polyfill-intl-idn/Resources/unidata/deviation.php @@ -0,0 +1,8 @@ + 'ss', + 962 => 'σ', + 8204 => '', + 8205 => '', +); diff --git a/3rdparty/symfony/polyfill-intl-idn/Resources/unidata/disallowed.php b/3rdparty/symfony/polyfill-intl-idn/Resources/unidata/disallowed.php new file mode 100644 index 00000000..25a5f564 --- /dev/null +++ b/3rdparty/symfony/polyfill-intl-idn/Resources/unidata/disallowed.php @@ -0,0 +1,2638 @@ + true, + 889 => true, + 896 => true, + 897 => true, + 898 => true, + 899 => true, + 907 => true, + 909 => true, + 930 => true, + 1216 => true, + 1328 => true, + 1367 => true, + 1368 => true, + 1419 => true, + 1420 => true, + 1424 => true, + 1480 => true, + 1481 => true, + 1482 => true, + 1483 => true, + 1484 => true, + 1485 => true, + 1486 => true, + 1487 => true, + 1515 => true, + 1516 => true, + 1517 => true, + 1518 => true, + 1525 => true, + 1526 => true, + 1527 => true, + 1528 => true, + 1529 => true, + 1530 => true, + 1531 => true, + 1532 => true, + 1533 => true, + 1534 => true, + 1535 => true, + 1536 => true, + 1537 => true, + 1538 => true, + 1539 => true, + 1540 => true, + 1541 => true, + 1564 => true, + 1565 => true, + 1757 => true, + 1806 => true, + 1807 => true, + 1867 => true, + 1868 => true, + 1970 => true, + 1971 => true, + 1972 => true, + 1973 => true, + 1974 => true, + 1975 => true, + 1976 => true, + 1977 => true, + 1978 => true, + 1979 => true, + 1980 => true, + 1981 => true, + 1982 => true, + 1983 => true, + 2043 => true, + 2044 => true, + 2094 => true, + 2095 => true, + 2111 => true, + 2140 => true, + 2141 => true, + 2143 => true, + 2229 => true, + 2248 => true, + 2249 => true, + 2250 => true, + 2251 => true, + 2252 => true, + 2253 => true, + 2254 => true, + 2255 => true, + 2256 => true, + 2257 => true, + 2258 => true, + 2274 => true, + 2436 => true, + 2445 => true, + 2446 => true, + 2449 => true, + 2450 => true, + 2473 => true, + 2481 => true, + 2483 => true, + 2484 => true, + 2485 => true, + 2490 => true, + 2491 => true, + 2501 => true, + 2502 => true, + 2505 => true, + 2506 => true, + 2511 => true, + 2512 => true, + 2513 => true, + 2514 => true, + 2515 => true, + 2516 => true, + 2517 => true, + 2518 => true, + 2520 => true, + 2521 => true, + 2522 => true, + 2523 => true, + 2526 => true, + 2532 => true, + 2533 => true, + 2559 => true, + 2560 => true, + 2564 => true, + 2571 => true, + 2572 => true, + 2573 => true, + 2574 => true, + 2577 => true, + 2578 => true, + 2601 => true, + 2609 => true, + 2612 => true, + 2615 => true, + 2618 => true, + 2619 => true, + 2621 => true, + 2627 => true, + 2628 => true, + 2629 => true, + 2630 => true, + 2633 => true, + 2634 => true, + 2638 => true, + 2639 => true, + 2640 => true, + 2642 => true, + 2643 => true, + 2644 => true, + 2645 => true, + 2646 => true, + 2647 => true, + 2648 => true, + 2653 => true, + 2655 => true, + 2656 => true, + 2657 => true, + 2658 => true, + 2659 => true, + 2660 => true, + 2661 => true, + 2679 => true, + 2680 => true, + 2681 => true, + 2682 => true, + 2683 => true, + 2684 => true, + 2685 => true, + 2686 => true, + 2687 => true, + 2688 => true, + 2692 => true, + 2702 => true, + 2706 => true, + 2729 => true, + 2737 => true, + 2740 => true, + 2746 => true, + 2747 => true, + 2758 => true, + 2762 => true, + 2766 => true, + 2767 => true, + 2769 => true, + 2770 => true, + 2771 => true, + 2772 => true, + 2773 => true, + 2774 => true, + 2775 => true, + 2776 => true, + 2777 => true, + 2778 => true, + 2779 => true, + 2780 => true, + 2781 => true, + 2782 => true, + 2783 => true, + 2788 => true, + 2789 => true, + 2802 => true, + 2803 => true, + 2804 => true, + 2805 => true, + 2806 => true, + 2807 => true, + 2808 => true, + 2816 => true, + 2820 => true, + 2829 => true, + 2830 => true, + 2833 => true, + 2834 => true, + 2857 => true, + 2865 => true, + 2868 => true, + 2874 => true, + 2875 => true, + 2885 => true, + 2886 => true, + 2889 => true, + 2890 => true, + 2894 => true, + 2895 => true, + 2896 => true, + 2897 => true, + 2898 => true, + 2899 => true, + 2900 => true, + 2904 => true, + 2905 => true, + 2906 => true, + 2907 => true, + 2910 => true, + 2916 => true, + 2917 => true, + 2936 => true, + 2937 => true, + 2938 => true, + 2939 => true, + 2940 => true, + 2941 => true, + 2942 => true, + 2943 => true, + 2944 => true, + 2945 => true, + 2948 => true, + 2955 => true, + 2956 => true, + 2957 => true, + 2961 => true, + 2966 => true, + 2967 => true, + 2968 => true, + 2971 => true, + 2973 => true, + 2976 => true, + 2977 => true, + 2978 => true, + 2981 => true, + 2982 => true, + 2983 => true, + 2987 => true, + 2988 => true, + 2989 => true, + 3002 => true, + 3003 => true, + 3004 => true, + 3005 => true, + 3011 => true, + 3012 => true, + 3013 => true, + 3017 => true, + 3022 => true, + 3023 => true, + 3025 => true, + 3026 => true, + 3027 => true, + 3028 => true, + 3029 => true, + 3030 => true, + 3032 => true, + 3033 => true, + 3034 => true, + 3035 => true, + 3036 => true, + 3037 => true, + 3038 => true, + 3039 => true, + 3040 => true, + 3041 => true, + 3042 => true, + 3043 => true, + 3044 => true, + 3045 => true, + 3067 => true, + 3068 => true, + 3069 => true, + 3070 => true, + 3071 => true, + 3085 => true, + 3089 => true, + 3113 => true, + 3130 => true, + 3131 => true, + 3132 => true, + 3141 => true, + 3145 => true, + 3150 => true, + 3151 => true, + 3152 => true, + 3153 => true, + 3154 => true, + 3155 => true, + 3156 => true, + 3159 => true, + 3163 => true, + 3164 => true, + 3165 => true, + 3166 => true, + 3167 => true, + 3172 => true, + 3173 => true, + 3184 => true, + 3185 => true, + 3186 => true, + 3187 => true, + 3188 => true, + 3189 => true, + 3190 => true, + 3213 => true, + 3217 => true, + 3241 => true, + 3252 => true, + 3258 => true, + 3259 => true, + 3269 => true, + 3273 => true, + 3278 => true, + 3279 => true, + 3280 => true, + 3281 => true, + 3282 => true, + 3283 => true, + 3284 => true, + 3287 => true, + 3288 => true, + 3289 => true, + 3290 => true, + 3291 => true, + 3292 => true, + 3293 => true, + 3295 => true, + 3300 => true, + 3301 => true, + 3312 => true, + 3315 => true, + 3316 => true, + 3317 => true, + 3318 => true, + 3319 => true, + 3320 => true, + 3321 => true, + 3322 => true, + 3323 => true, + 3324 => true, + 3325 => true, + 3326 => true, + 3327 => true, + 3341 => true, + 3345 => true, + 3397 => true, + 3401 => true, + 3408 => true, + 3409 => true, + 3410 => true, + 3411 => true, + 3428 => true, + 3429 => true, + 3456 => true, + 3460 => true, + 3479 => true, + 3480 => true, + 3481 => true, + 3506 => true, + 3516 => true, + 3518 => true, + 3519 => true, + 3527 => true, + 3528 => true, + 3529 => true, + 3531 => true, + 3532 => true, + 3533 => true, + 3534 => true, + 3541 => true, + 3543 => true, + 3552 => true, + 3553 => true, + 3554 => true, + 3555 => true, + 3556 => true, + 3557 => true, + 3568 => true, + 3569 => true, + 3573 => true, + 3574 => true, + 3575 => true, + 3576 => true, + 3577 => true, + 3578 => true, + 3579 => true, + 3580 => true, + 3581 => true, + 3582 => true, + 3583 => true, + 3584 => true, + 3643 => true, + 3644 => true, + 3645 => true, + 3646 => true, + 3715 => true, + 3717 => true, + 3723 => true, + 3748 => true, + 3750 => true, + 3774 => true, + 3775 => true, + 3781 => true, + 3783 => true, + 3790 => true, + 3791 => true, + 3802 => true, + 3803 => true, + 3912 => true, + 3949 => true, + 3950 => true, + 3951 => true, + 3952 => true, + 3992 => true, + 4029 => true, + 4045 => true, + 4294 => true, + 4296 => true, + 4297 => true, + 4298 => true, + 4299 => true, + 4300 => true, + 4302 => true, + 4303 => true, + 4447 => true, + 4448 => true, + 4681 => true, + 4686 => true, + 4687 => true, + 4695 => true, + 4697 => true, + 4702 => true, + 4703 => true, + 4745 => true, + 4750 => true, + 4751 => true, + 4785 => true, + 4790 => true, + 4791 => true, + 4799 => true, + 4801 => true, + 4806 => true, + 4807 => true, + 4823 => true, + 4881 => true, + 4886 => true, + 4887 => true, + 4955 => true, + 4956 => true, + 4989 => true, + 4990 => true, + 4991 => true, + 5018 => true, + 5019 => true, + 5020 => true, + 5021 => true, + 5022 => true, + 5023 => true, + 5110 => true, + 5111 => true, + 5118 => true, + 5119 => true, + 5760 => true, + 5789 => true, + 5790 => true, + 5791 => true, + 5881 => true, + 5882 => true, + 5883 => true, + 5884 => true, + 5885 => true, + 5886 => true, + 5887 => true, + 5901 => true, + 5909 => true, + 5910 => true, + 5911 => true, + 5912 => true, + 5913 => true, + 5914 => true, + 5915 => true, + 5916 => true, + 5917 => true, + 5918 => true, + 5919 => true, + 5943 => true, + 5944 => true, + 5945 => true, + 5946 => true, + 5947 => true, + 5948 => true, + 5949 => true, + 5950 => true, + 5951 => true, + 5972 => true, + 5973 => true, + 5974 => true, + 5975 => true, + 5976 => true, + 5977 => true, + 5978 => true, + 5979 => true, + 5980 => true, + 5981 => true, + 5982 => true, + 5983 => true, + 5997 => true, + 6001 => true, + 6004 => true, + 6005 => true, + 6006 => true, + 6007 => true, + 6008 => true, + 6009 => true, + 6010 => true, + 6011 => true, + 6012 => true, + 6013 => true, + 6014 => true, + 6015 => true, + 6068 => true, + 6069 => true, + 6110 => true, + 6111 => true, + 6122 => true, + 6123 => true, + 6124 => true, + 6125 => true, + 6126 => true, + 6127 => true, + 6138 => true, + 6139 => true, + 6140 => true, + 6141 => true, + 6142 => true, + 6143 => true, + 6150 => true, + 6158 => true, + 6159 => true, + 6170 => true, + 6171 => true, + 6172 => true, + 6173 => true, + 6174 => true, + 6175 => true, + 6265 => true, + 6266 => true, + 6267 => true, + 6268 => true, + 6269 => true, + 6270 => true, + 6271 => true, + 6315 => true, + 6316 => true, + 6317 => true, + 6318 => true, + 6319 => true, + 6390 => true, + 6391 => true, + 6392 => true, + 6393 => true, + 6394 => true, + 6395 => true, + 6396 => true, + 6397 => true, + 6398 => true, + 6399 => true, + 6431 => true, + 6444 => true, + 6445 => true, + 6446 => true, + 6447 => true, + 6460 => true, + 6461 => true, + 6462 => true, + 6463 => true, + 6465 => true, + 6466 => true, + 6467 => true, + 6510 => true, + 6511 => true, + 6517 => true, + 6518 => true, + 6519 => true, + 6520 => true, + 6521 => true, + 6522 => true, + 6523 => true, + 6524 => true, + 6525 => true, + 6526 => true, + 6527 => true, + 6572 => true, + 6573 => true, + 6574 => true, + 6575 => true, + 6602 => true, + 6603 => true, + 6604 => true, + 6605 => true, + 6606 => true, + 6607 => true, + 6619 => true, + 6620 => true, + 6621 => true, + 6684 => true, + 6685 => true, + 6751 => true, + 6781 => true, + 6782 => true, + 6794 => true, + 6795 => true, + 6796 => true, + 6797 => true, + 6798 => true, + 6799 => true, + 6810 => true, + 6811 => true, + 6812 => true, + 6813 => true, + 6814 => true, + 6815 => true, + 6830 => true, + 6831 => true, + 6988 => true, + 6989 => true, + 6990 => true, + 6991 => true, + 7037 => true, + 7038 => true, + 7039 => true, + 7156 => true, + 7157 => true, + 7158 => true, + 7159 => true, + 7160 => true, + 7161 => true, + 7162 => true, + 7163 => true, + 7224 => true, + 7225 => true, + 7226 => true, + 7242 => true, + 7243 => true, + 7244 => true, + 7305 => true, + 7306 => true, + 7307 => true, + 7308 => true, + 7309 => true, + 7310 => true, + 7311 => true, + 7355 => true, + 7356 => true, + 7368 => true, + 7369 => true, + 7370 => true, + 7371 => true, + 7372 => true, + 7373 => true, + 7374 => true, + 7375 => true, + 7419 => true, + 7420 => true, + 7421 => true, + 7422 => true, + 7423 => true, + 7674 => true, + 7958 => true, + 7959 => true, + 7966 => true, + 7967 => true, + 8006 => true, + 8007 => true, + 8014 => true, + 8015 => true, + 8024 => true, + 8026 => true, + 8028 => true, + 8030 => true, + 8062 => true, + 8063 => true, + 8117 => true, + 8133 => true, + 8148 => true, + 8149 => true, + 8156 => true, + 8176 => true, + 8177 => true, + 8181 => true, + 8191 => true, + 8206 => true, + 8207 => true, + 8228 => true, + 8229 => true, + 8230 => true, + 8232 => true, + 8233 => true, + 8234 => true, + 8235 => true, + 8236 => true, + 8237 => true, + 8238 => true, + 8289 => true, + 8290 => true, + 8291 => true, + 8293 => true, + 8294 => true, + 8295 => true, + 8296 => true, + 8297 => true, + 8298 => true, + 8299 => true, + 8300 => true, + 8301 => true, + 8302 => true, + 8303 => true, + 8306 => true, + 8307 => true, + 8335 => true, + 8349 => true, + 8350 => true, + 8351 => true, + 8384 => true, + 8385 => true, + 8386 => true, + 8387 => true, + 8388 => true, + 8389 => true, + 8390 => true, + 8391 => true, + 8392 => true, + 8393 => true, + 8394 => true, + 8395 => true, + 8396 => true, + 8397 => true, + 8398 => true, + 8399 => true, + 8433 => true, + 8434 => true, + 8435 => true, + 8436 => true, + 8437 => true, + 8438 => true, + 8439 => true, + 8440 => true, + 8441 => true, + 8442 => true, + 8443 => true, + 8444 => true, + 8445 => true, + 8446 => true, + 8447 => true, + 8498 => true, + 8579 => true, + 8588 => true, + 8589 => true, + 8590 => true, + 8591 => true, + 9255 => true, + 9256 => true, + 9257 => true, + 9258 => true, + 9259 => true, + 9260 => true, + 9261 => true, + 9262 => true, + 9263 => true, + 9264 => true, + 9265 => true, + 9266 => true, + 9267 => true, + 9268 => true, + 9269 => true, + 9270 => true, + 9271 => true, + 9272 => true, + 9273 => true, + 9274 => true, + 9275 => true, + 9276 => true, + 9277 => true, + 9278 => true, + 9279 => true, + 9291 => true, + 9292 => true, + 9293 => true, + 9294 => true, + 9295 => true, + 9296 => true, + 9297 => true, + 9298 => true, + 9299 => true, + 9300 => true, + 9301 => true, + 9302 => true, + 9303 => true, + 9304 => true, + 9305 => true, + 9306 => true, + 9307 => true, + 9308 => true, + 9309 => true, + 9310 => true, + 9311 => true, + 9352 => true, + 9353 => true, + 9354 => true, + 9355 => true, + 9356 => true, + 9357 => true, + 9358 => true, + 9359 => true, + 9360 => true, + 9361 => true, + 9362 => true, + 9363 => true, + 9364 => true, + 9365 => true, + 9366 => true, + 9367 => true, + 9368 => true, + 9369 => true, + 9370 => true, + 9371 => true, + 11124 => true, + 11125 => true, + 11158 => true, + 11311 => true, + 11359 => true, + 11508 => true, + 11509 => true, + 11510 => true, + 11511 => true, + 11512 => true, + 11558 => true, + 11560 => true, + 11561 => true, + 11562 => true, + 11563 => true, + 11564 => true, + 11566 => true, + 11567 => true, + 11624 => true, + 11625 => true, + 11626 => true, + 11627 => true, + 11628 => true, + 11629 => true, + 11630 => true, + 11633 => true, + 11634 => true, + 11635 => true, + 11636 => true, + 11637 => true, + 11638 => true, + 11639 => true, + 11640 => true, + 11641 => true, + 11642 => true, + 11643 => true, + 11644 => true, + 11645 => true, + 11646 => true, + 11671 => true, + 11672 => true, + 11673 => true, + 11674 => true, + 11675 => true, + 11676 => true, + 11677 => true, + 11678 => true, + 11679 => true, + 11687 => true, + 11695 => true, + 11703 => true, + 11711 => true, + 11719 => true, + 11727 => true, + 11735 => true, + 11743 => true, + 11930 => true, + 12020 => true, + 12021 => true, + 12022 => true, + 12023 => true, + 12024 => true, + 12025 => true, + 12026 => true, + 12027 => true, + 12028 => true, + 12029 => true, + 12030 => true, + 12031 => true, + 12246 => true, + 12247 => true, + 12248 => true, + 12249 => true, + 12250 => true, + 12251 => true, + 12252 => true, + 12253 => true, + 12254 => true, + 12255 => true, + 12256 => true, + 12257 => true, + 12258 => true, + 12259 => true, + 12260 => true, + 12261 => true, + 12262 => true, + 12263 => true, + 12264 => true, + 12265 => true, + 12266 => true, + 12267 => true, + 12268 => true, + 12269 => true, + 12270 => true, + 12271 => true, + 12272 => true, + 12273 => true, + 12274 => true, + 12275 => true, + 12276 => true, + 12277 => true, + 12278 => true, + 12279 => true, + 12280 => true, + 12281 => true, + 12282 => true, + 12283 => true, + 12284 => true, + 12285 => true, + 12286 => true, + 12287 => true, + 12352 => true, + 12439 => true, + 12440 => true, + 12544 => true, + 12545 => true, + 12546 => true, + 12547 => true, + 12548 => true, + 12592 => true, + 12644 => true, + 12687 => true, + 12772 => true, + 12773 => true, + 12774 => true, + 12775 => true, + 12776 => true, + 12777 => true, + 12778 => true, + 12779 => true, + 12780 => true, + 12781 => true, + 12782 => true, + 12783 => true, + 12831 => true, + 13250 => true, + 13255 => true, + 13272 => true, + 40957 => true, + 40958 => true, + 40959 => true, + 42125 => true, + 42126 => true, + 42127 => true, + 42183 => true, + 42184 => true, + 42185 => true, + 42186 => true, + 42187 => true, + 42188 => true, + 42189 => true, + 42190 => true, + 42191 => true, + 42540 => true, + 42541 => true, + 42542 => true, + 42543 => true, + 42544 => true, + 42545 => true, + 42546 => true, + 42547 => true, + 42548 => true, + 42549 => true, + 42550 => true, + 42551 => true, + 42552 => true, + 42553 => true, + 42554 => true, + 42555 => true, + 42556 => true, + 42557 => true, + 42558 => true, + 42559 => true, + 42744 => true, + 42745 => true, + 42746 => true, + 42747 => true, + 42748 => true, + 42749 => true, + 42750 => true, + 42751 => true, + 42944 => true, + 42945 => true, + 43053 => true, + 43054 => true, + 43055 => true, + 43066 => true, + 43067 => true, + 43068 => true, + 43069 => true, + 43070 => true, + 43071 => true, + 43128 => true, + 43129 => true, + 43130 => true, + 43131 => true, + 43132 => true, + 43133 => true, + 43134 => true, + 43135 => true, + 43206 => true, + 43207 => true, + 43208 => true, + 43209 => true, + 43210 => true, + 43211 => true, + 43212 => true, + 43213 => true, + 43226 => true, + 43227 => true, + 43228 => true, + 43229 => true, + 43230 => true, + 43231 => true, + 43348 => true, + 43349 => true, + 43350 => true, + 43351 => true, + 43352 => true, + 43353 => true, + 43354 => true, + 43355 => true, + 43356 => true, + 43357 => true, + 43358 => true, + 43389 => true, + 43390 => true, + 43391 => true, + 43470 => true, + 43482 => true, + 43483 => true, + 43484 => true, + 43485 => true, + 43519 => true, + 43575 => true, + 43576 => true, + 43577 => true, + 43578 => true, + 43579 => true, + 43580 => true, + 43581 => true, + 43582 => true, + 43583 => true, + 43598 => true, + 43599 => true, + 43610 => true, + 43611 => true, + 43715 => true, + 43716 => true, + 43717 => true, + 43718 => true, + 43719 => true, + 43720 => true, + 43721 => true, + 43722 => true, + 43723 => true, + 43724 => true, + 43725 => true, + 43726 => true, + 43727 => true, + 43728 => true, + 43729 => true, + 43730 => true, + 43731 => true, + 43732 => true, + 43733 => true, + 43734 => true, + 43735 => true, + 43736 => true, + 43737 => true, + 43738 => true, + 43767 => true, + 43768 => true, + 43769 => true, + 43770 => true, + 43771 => true, + 43772 => true, + 43773 => true, + 43774 => true, + 43775 => true, + 43776 => true, + 43783 => true, + 43784 => true, + 43791 => true, + 43792 => true, + 43799 => true, + 43800 => true, + 43801 => true, + 43802 => true, + 43803 => true, + 43804 => true, + 43805 => true, + 43806 => true, + 43807 => true, + 43815 => true, + 43823 => true, + 43884 => true, + 43885 => true, + 43886 => true, + 43887 => true, + 44014 => true, + 44015 => true, + 44026 => true, + 44027 => true, + 44028 => true, + 44029 => true, + 44030 => true, + 44031 => true, + 55204 => true, + 55205 => true, + 55206 => true, + 55207 => true, + 55208 => true, + 55209 => true, + 55210 => true, + 55211 => true, + 55212 => true, + 55213 => true, + 55214 => true, + 55215 => true, + 55239 => true, + 55240 => true, + 55241 => true, + 55242 => true, + 55292 => true, + 55293 => true, + 55294 => true, + 55295 => true, + 64110 => true, + 64111 => true, + 64263 => true, + 64264 => true, + 64265 => true, + 64266 => true, + 64267 => true, + 64268 => true, + 64269 => true, + 64270 => true, + 64271 => true, + 64272 => true, + 64273 => true, + 64274 => true, + 64280 => true, + 64281 => true, + 64282 => true, + 64283 => true, + 64284 => true, + 64311 => true, + 64317 => true, + 64319 => true, + 64322 => true, + 64325 => true, + 64450 => true, + 64451 => true, + 64452 => true, + 64453 => true, + 64454 => true, + 64455 => true, + 64456 => true, + 64457 => true, + 64458 => true, + 64459 => true, + 64460 => true, + 64461 => true, + 64462 => true, + 64463 => true, + 64464 => true, + 64465 => true, + 64466 => true, + 64832 => true, + 64833 => true, + 64834 => true, + 64835 => true, + 64836 => true, + 64837 => true, + 64838 => true, + 64839 => true, + 64840 => true, + 64841 => true, + 64842 => true, + 64843 => true, + 64844 => true, + 64845 => true, + 64846 => true, + 64847 => true, + 64912 => true, + 64913 => true, + 64968 => true, + 64969 => true, + 64970 => true, + 64971 => true, + 64972 => true, + 64973 => true, + 64974 => true, + 64975 => true, + 65022 => true, + 65023 => true, + 65042 => true, + 65049 => true, + 65050 => true, + 65051 => true, + 65052 => true, + 65053 => true, + 65054 => true, + 65055 => true, + 65072 => true, + 65106 => true, + 65107 => true, + 65127 => true, + 65132 => true, + 65133 => true, + 65134 => true, + 65135 => true, + 65141 => true, + 65277 => true, + 65278 => true, + 65280 => true, + 65440 => true, + 65471 => true, + 65472 => true, + 65473 => true, + 65480 => true, + 65481 => true, + 65488 => true, + 65489 => true, + 65496 => true, + 65497 => true, + 65501 => true, + 65502 => true, + 65503 => true, + 65511 => true, + 65519 => true, + 65520 => true, + 65521 => true, + 65522 => true, + 65523 => true, + 65524 => true, + 65525 => true, + 65526 => true, + 65527 => true, + 65528 => true, + 65529 => true, + 65530 => true, + 65531 => true, + 65532 => true, + 65533 => true, + 65534 => true, + 65535 => true, + 65548 => true, + 65575 => true, + 65595 => true, + 65598 => true, + 65614 => true, + 65615 => true, + 65787 => true, + 65788 => true, + 65789 => true, + 65790 => true, + 65791 => true, + 65795 => true, + 65796 => true, + 65797 => true, + 65798 => true, + 65844 => true, + 65845 => true, + 65846 => true, + 65935 => true, + 65949 => true, + 65950 => true, + 65951 => true, + 66205 => true, + 66206 => true, + 66207 => true, + 66257 => true, + 66258 => true, + 66259 => true, + 66260 => true, + 66261 => true, + 66262 => true, + 66263 => true, + 66264 => true, + 66265 => true, + 66266 => true, + 66267 => true, + 66268 => true, + 66269 => true, + 66270 => true, + 66271 => true, + 66300 => true, + 66301 => true, + 66302 => true, + 66303 => true, + 66340 => true, + 66341 => true, + 66342 => true, + 66343 => true, + 66344 => true, + 66345 => true, + 66346 => true, + 66347 => true, + 66348 => true, + 66379 => true, + 66380 => true, + 66381 => true, + 66382 => true, + 66383 => true, + 66427 => true, + 66428 => true, + 66429 => true, + 66430 => true, + 66431 => true, + 66462 => true, + 66500 => true, + 66501 => true, + 66502 => true, + 66503 => true, + 66718 => true, + 66719 => true, + 66730 => true, + 66731 => true, + 66732 => true, + 66733 => true, + 66734 => true, + 66735 => true, + 66772 => true, + 66773 => true, + 66774 => true, + 66775 => true, + 66812 => true, + 66813 => true, + 66814 => true, + 66815 => true, + 66856 => true, + 66857 => true, + 66858 => true, + 66859 => true, + 66860 => true, + 66861 => true, + 66862 => true, + 66863 => true, + 66916 => true, + 66917 => true, + 66918 => true, + 66919 => true, + 66920 => true, + 66921 => true, + 66922 => true, + 66923 => true, + 66924 => true, + 66925 => true, + 66926 => true, + 67383 => true, + 67384 => true, + 67385 => true, + 67386 => true, + 67387 => true, + 67388 => true, + 67389 => true, + 67390 => true, + 67391 => true, + 67414 => true, + 67415 => true, + 67416 => true, + 67417 => true, + 67418 => true, + 67419 => true, + 67420 => true, + 67421 => true, + 67422 => true, + 67423 => true, + 67590 => true, + 67591 => true, + 67593 => true, + 67638 => true, + 67641 => true, + 67642 => true, + 67643 => true, + 67645 => true, + 67646 => true, + 67670 => true, + 67743 => true, + 67744 => true, + 67745 => true, + 67746 => true, + 67747 => true, + 67748 => true, + 67749 => true, + 67750 => true, + 67827 => true, + 67830 => true, + 67831 => true, + 67832 => true, + 67833 => true, + 67834 => true, + 67868 => true, + 67869 => true, + 67870 => true, + 67898 => true, + 67899 => true, + 67900 => true, + 67901 => true, + 67902 => true, + 68024 => true, + 68025 => true, + 68026 => true, + 68027 => true, + 68048 => true, + 68049 => true, + 68100 => true, + 68103 => true, + 68104 => true, + 68105 => true, + 68106 => true, + 68107 => true, + 68116 => true, + 68120 => true, + 68150 => true, + 68151 => true, + 68155 => true, + 68156 => true, + 68157 => true, + 68158 => true, + 68169 => true, + 68170 => true, + 68171 => true, + 68172 => true, + 68173 => true, + 68174 => true, + 68175 => true, + 68185 => true, + 68186 => true, + 68187 => true, + 68188 => true, + 68189 => true, + 68190 => true, + 68191 => true, + 68327 => true, + 68328 => true, + 68329 => true, + 68330 => true, + 68343 => true, + 68344 => true, + 68345 => true, + 68346 => true, + 68347 => true, + 68348 => true, + 68349 => true, + 68350 => true, + 68351 => true, + 68406 => true, + 68407 => true, + 68408 => true, + 68438 => true, + 68439 => true, + 68467 => true, + 68468 => true, + 68469 => true, + 68470 => true, + 68471 => true, + 68498 => true, + 68499 => true, + 68500 => true, + 68501 => true, + 68502 => true, + 68503 => true, + 68504 => true, + 68509 => true, + 68510 => true, + 68511 => true, + 68512 => true, + 68513 => true, + 68514 => true, + 68515 => true, + 68516 => true, + 68517 => true, + 68518 => true, + 68519 => true, + 68520 => true, + 68787 => true, + 68788 => true, + 68789 => true, + 68790 => true, + 68791 => true, + 68792 => true, + 68793 => true, + 68794 => true, + 68795 => true, + 68796 => true, + 68797 => true, + 68798 => true, + 68799 => true, + 68851 => true, + 68852 => true, + 68853 => true, + 68854 => true, + 68855 => true, + 68856 => true, + 68857 => true, + 68904 => true, + 68905 => true, + 68906 => true, + 68907 => true, + 68908 => true, + 68909 => true, + 68910 => true, + 68911 => true, + 69247 => true, + 69290 => true, + 69294 => true, + 69295 => true, + 69416 => true, + 69417 => true, + 69418 => true, + 69419 => true, + 69420 => true, + 69421 => true, + 69422 => true, + 69423 => true, + 69580 => true, + 69581 => true, + 69582 => true, + 69583 => true, + 69584 => true, + 69585 => true, + 69586 => true, + 69587 => true, + 69588 => true, + 69589 => true, + 69590 => true, + 69591 => true, + 69592 => true, + 69593 => true, + 69594 => true, + 69595 => true, + 69596 => true, + 69597 => true, + 69598 => true, + 69599 => true, + 69623 => true, + 69624 => true, + 69625 => true, + 69626 => true, + 69627 => true, + 69628 => true, + 69629 => true, + 69630 => true, + 69631 => true, + 69710 => true, + 69711 => true, + 69712 => true, + 69713 => true, + 69744 => true, + 69745 => true, + 69746 => true, + 69747 => true, + 69748 => true, + 69749 => true, + 69750 => true, + 69751 => true, + 69752 => true, + 69753 => true, + 69754 => true, + 69755 => true, + 69756 => true, + 69757 => true, + 69758 => true, + 69821 => true, + 69826 => true, + 69827 => true, + 69828 => true, + 69829 => true, + 69830 => true, + 69831 => true, + 69832 => true, + 69833 => true, + 69834 => true, + 69835 => true, + 69836 => true, + 69837 => true, + 69838 => true, + 69839 => true, + 69865 => true, + 69866 => true, + 69867 => true, + 69868 => true, + 69869 => true, + 69870 => true, + 69871 => true, + 69882 => true, + 69883 => true, + 69884 => true, + 69885 => true, + 69886 => true, + 69887 => true, + 69941 => true, + 69960 => true, + 69961 => true, + 69962 => true, + 69963 => true, + 69964 => true, + 69965 => true, + 69966 => true, + 69967 => true, + 70007 => true, + 70008 => true, + 70009 => true, + 70010 => true, + 70011 => true, + 70012 => true, + 70013 => true, + 70014 => true, + 70015 => true, + 70112 => true, + 70133 => true, + 70134 => true, + 70135 => true, + 70136 => true, + 70137 => true, + 70138 => true, + 70139 => true, + 70140 => true, + 70141 => true, + 70142 => true, + 70143 => true, + 70162 => true, + 70279 => true, + 70281 => true, + 70286 => true, + 70302 => true, + 70314 => true, + 70315 => true, + 70316 => true, + 70317 => true, + 70318 => true, + 70319 => true, + 70379 => true, + 70380 => true, + 70381 => true, + 70382 => true, + 70383 => true, + 70394 => true, + 70395 => true, + 70396 => true, + 70397 => true, + 70398 => true, + 70399 => true, + 70404 => true, + 70413 => true, + 70414 => true, + 70417 => true, + 70418 => true, + 70441 => true, + 70449 => true, + 70452 => true, + 70458 => true, + 70469 => true, + 70470 => true, + 70473 => true, + 70474 => true, + 70478 => true, + 70479 => true, + 70481 => true, + 70482 => true, + 70483 => true, + 70484 => true, + 70485 => true, + 70486 => true, + 70488 => true, + 70489 => true, + 70490 => true, + 70491 => true, + 70492 => true, + 70500 => true, + 70501 => true, + 70509 => true, + 70510 => true, + 70511 => true, + 70748 => true, + 70754 => true, + 70755 => true, + 70756 => true, + 70757 => true, + 70758 => true, + 70759 => true, + 70760 => true, + 70761 => true, + 70762 => true, + 70763 => true, + 70764 => true, + 70765 => true, + 70766 => true, + 70767 => true, + 70768 => true, + 70769 => true, + 70770 => true, + 70771 => true, + 70772 => true, + 70773 => true, + 70774 => true, + 70775 => true, + 70776 => true, + 70777 => true, + 70778 => true, + 70779 => true, + 70780 => true, + 70781 => true, + 70782 => true, + 70783 => true, + 70856 => true, + 70857 => true, + 70858 => true, + 70859 => true, + 70860 => true, + 70861 => true, + 70862 => true, + 70863 => true, + 71094 => true, + 71095 => true, + 71237 => true, + 71238 => true, + 71239 => true, + 71240 => true, + 71241 => true, + 71242 => true, + 71243 => true, + 71244 => true, + 71245 => true, + 71246 => true, + 71247 => true, + 71258 => true, + 71259 => true, + 71260 => true, + 71261 => true, + 71262 => true, + 71263 => true, + 71277 => true, + 71278 => true, + 71279 => true, + 71280 => true, + 71281 => true, + 71282 => true, + 71283 => true, + 71284 => true, + 71285 => true, + 71286 => true, + 71287 => true, + 71288 => true, + 71289 => true, + 71290 => true, + 71291 => true, + 71292 => true, + 71293 => true, + 71294 => true, + 71295 => true, + 71353 => true, + 71354 => true, + 71355 => true, + 71356 => true, + 71357 => true, + 71358 => true, + 71359 => true, + 71451 => true, + 71452 => true, + 71468 => true, + 71469 => true, + 71470 => true, + 71471 => true, + 71923 => true, + 71924 => true, + 71925 => true, + 71926 => true, + 71927 => true, + 71928 => true, + 71929 => true, + 71930 => true, + 71931 => true, + 71932 => true, + 71933 => true, + 71934 => true, + 71943 => true, + 71944 => true, + 71946 => true, + 71947 => true, + 71956 => true, + 71959 => true, + 71990 => true, + 71993 => true, + 71994 => true, + 72007 => true, + 72008 => true, + 72009 => true, + 72010 => true, + 72011 => true, + 72012 => true, + 72013 => true, + 72014 => true, + 72015 => true, + 72104 => true, + 72105 => true, + 72152 => true, + 72153 => true, + 72165 => true, + 72166 => true, + 72167 => true, + 72168 => true, + 72169 => true, + 72170 => true, + 72171 => true, + 72172 => true, + 72173 => true, + 72174 => true, + 72175 => true, + 72176 => true, + 72177 => true, + 72178 => true, + 72179 => true, + 72180 => true, + 72181 => true, + 72182 => true, + 72183 => true, + 72184 => true, + 72185 => true, + 72186 => true, + 72187 => true, + 72188 => true, + 72189 => true, + 72190 => true, + 72191 => true, + 72264 => true, + 72265 => true, + 72266 => true, + 72267 => true, + 72268 => true, + 72269 => true, + 72270 => true, + 72271 => true, + 72355 => true, + 72356 => true, + 72357 => true, + 72358 => true, + 72359 => true, + 72360 => true, + 72361 => true, + 72362 => true, + 72363 => true, + 72364 => true, + 72365 => true, + 72366 => true, + 72367 => true, + 72368 => true, + 72369 => true, + 72370 => true, + 72371 => true, + 72372 => true, + 72373 => true, + 72374 => true, + 72375 => true, + 72376 => true, + 72377 => true, + 72378 => true, + 72379 => true, + 72380 => true, + 72381 => true, + 72382 => true, + 72383 => true, + 72713 => true, + 72759 => true, + 72774 => true, + 72775 => true, + 72776 => true, + 72777 => true, + 72778 => true, + 72779 => true, + 72780 => true, + 72781 => true, + 72782 => true, + 72783 => true, + 72813 => true, + 72814 => true, + 72815 => true, + 72848 => true, + 72849 => true, + 72872 => true, + 72967 => true, + 72970 => true, + 73015 => true, + 73016 => true, + 73017 => true, + 73019 => true, + 73022 => true, + 73032 => true, + 73033 => true, + 73034 => true, + 73035 => true, + 73036 => true, + 73037 => true, + 73038 => true, + 73039 => true, + 73050 => true, + 73051 => true, + 73052 => true, + 73053 => true, + 73054 => true, + 73055 => true, + 73062 => true, + 73065 => true, + 73103 => true, + 73106 => true, + 73113 => true, + 73114 => true, + 73115 => true, + 73116 => true, + 73117 => true, + 73118 => true, + 73119 => true, + 73649 => true, + 73650 => true, + 73651 => true, + 73652 => true, + 73653 => true, + 73654 => true, + 73655 => true, + 73656 => true, + 73657 => true, + 73658 => true, + 73659 => true, + 73660 => true, + 73661 => true, + 73662 => true, + 73663 => true, + 73714 => true, + 73715 => true, + 73716 => true, + 73717 => true, + 73718 => true, + 73719 => true, + 73720 => true, + 73721 => true, + 73722 => true, + 73723 => true, + 73724 => true, + 73725 => true, + 73726 => true, + 74863 => true, + 74869 => true, + 74870 => true, + 74871 => true, + 74872 => true, + 74873 => true, + 74874 => true, + 74875 => true, + 74876 => true, + 74877 => true, + 74878 => true, + 74879 => true, + 78895 => true, + 78896 => true, + 78897 => true, + 78898 => true, + 78899 => true, + 78900 => true, + 78901 => true, + 78902 => true, + 78903 => true, + 78904 => true, + 92729 => true, + 92730 => true, + 92731 => true, + 92732 => true, + 92733 => true, + 92734 => true, + 92735 => true, + 92767 => true, + 92778 => true, + 92779 => true, + 92780 => true, + 92781 => true, + 92910 => true, + 92911 => true, + 92918 => true, + 92919 => true, + 92920 => true, + 92921 => true, + 92922 => true, + 92923 => true, + 92924 => true, + 92925 => true, + 92926 => true, + 92927 => true, + 92998 => true, + 92999 => true, + 93000 => true, + 93001 => true, + 93002 => true, + 93003 => true, + 93004 => true, + 93005 => true, + 93006 => true, + 93007 => true, + 93018 => true, + 93026 => true, + 93048 => true, + 93049 => true, + 93050 => true, + 93051 => true, + 93052 => true, + 94027 => true, + 94028 => true, + 94029 => true, + 94030 => true, + 94088 => true, + 94089 => true, + 94090 => true, + 94091 => true, + 94092 => true, + 94093 => true, + 94094 => true, + 94181 => true, + 94182 => true, + 94183 => true, + 94184 => true, + 94185 => true, + 94186 => true, + 94187 => true, + 94188 => true, + 94189 => true, + 94190 => true, + 94191 => true, + 94194 => true, + 94195 => true, + 94196 => true, + 94197 => true, + 94198 => true, + 94199 => true, + 94200 => true, + 94201 => true, + 94202 => true, + 94203 => true, + 94204 => true, + 94205 => true, + 94206 => true, + 94207 => true, + 100344 => true, + 100345 => true, + 100346 => true, + 100347 => true, + 100348 => true, + 100349 => true, + 100350 => true, + 100351 => true, + 110931 => true, + 110932 => true, + 110933 => true, + 110934 => true, + 110935 => true, + 110936 => true, + 110937 => true, + 110938 => true, + 110939 => true, + 110940 => true, + 110941 => true, + 110942 => true, + 110943 => true, + 110944 => true, + 110945 => true, + 110946 => true, + 110947 => true, + 110952 => true, + 110953 => true, + 110954 => true, + 110955 => true, + 110956 => true, + 110957 => true, + 110958 => true, + 110959 => true, + 113771 => true, + 113772 => true, + 113773 => true, + 113774 => true, + 113775 => true, + 113789 => true, + 113790 => true, + 113791 => true, + 113801 => true, + 113802 => true, + 113803 => true, + 113804 => true, + 113805 => true, + 113806 => true, + 113807 => true, + 113818 => true, + 113819 => true, + 119030 => true, + 119031 => true, + 119032 => true, + 119033 => true, + 119034 => true, + 119035 => true, + 119036 => true, + 119037 => true, + 119038 => true, + 119039 => true, + 119079 => true, + 119080 => true, + 119155 => true, + 119156 => true, + 119157 => true, + 119158 => true, + 119159 => true, + 119160 => true, + 119161 => true, + 119162 => true, + 119273 => true, + 119274 => true, + 119275 => true, + 119276 => true, + 119277 => true, + 119278 => true, + 119279 => true, + 119280 => true, + 119281 => true, + 119282 => true, + 119283 => true, + 119284 => true, + 119285 => true, + 119286 => true, + 119287 => true, + 119288 => true, + 119289 => true, + 119290 => true, + 119291 => true, + 119292 => true, + 119293 => true, + 119294 => true, + 119295 => true, + 119540 => true, + 119541 => true, + 119542 => true, + 119543 => true, + 119544 => true, + 119545 => true, + 119546 => true, + 119547 => true, + 119548 => true, + 119549 => true, + 119550 => true, + 119551 => true, + 119639 => true, + 119640 => true, + 119641 => true, + 119642 => true, + 119643 => true, + 119644 => true, + 119645 => true, + 119646 => true, + 119647 => true, + 119893 => true, + 119965 => true, + 119968 => true, + 119969 => true, + 119971 => true, + 119972 => true, + 119975 => true, + 119976 => true, + 119981 => true, + 119994 => true, + 119996 => true, + 120004 => true, + 120070 => true, + 120075 => true, + 120076 => true, + 120085 => true, + 120093 => true, + 120122 => true, + 120127 => true, + 120133 => true, + 120135 => true, + 120136 => true, + 120137 => true, + 120145 => true, + 120486 => true, + 120487 => true, + 120780 => true, + 120781 => true, + 121484 => true, + 121485 => true, + 121486 => true, + 121487 => true, + 121488 => true, + 121489 => true, + 121490 => true, + 121491 => true, + 121492 => true, + 121493 => true, + 121494 => true, + 121495 => true, + 121496 => true, + 121497 => true, + 121498 => true, + 121504 => true, + 122887 => true, + 122905 => true, + 122906 => true, + 122914 => true, + 122917 => true, + 123181 => true, + 123182 => true, + 123183 => true, + 123198 => true, + 123199 => true, + 123210 => true, + 123211 => true, + 123212 => true, + 123213 => true, + 123642 => true, + 123643 => true, + 123644 => true, + 123645 => true, + 123646 => true, + 125125 => true, + 125126 => true, + 125260 => true, + 125261 => true, + 125262 => true, + 125263 => true, + 125274 => true, + 125275 => true, + 125276 => true, + 125277 => true, + 126468 => true, + 126496 => true, + 126499 => true, + 126501 => true, + 126502 => true, + 126504 => true, + 126515 => true, + 126520 => true, + 126522 => true, + 126524 => true, + 126525 => true, + 126526 => true, + 126527 => true, + 126528 => true, + 126529 => true, + 126531 => true, + 126532 => true, + 126533 => true, + 126534 => true, + 126536 => true, + 126538 => true, + 126540 => true, + 126544 => true, + 126547 => true, + 126549 => true, + 126550 => true, + 126552 => true, + 126554 => true, + 126556 => true, + 126558 => true, + 126560 => true, + 126563 => true, + 126565 => true, + 126566 => true, + 126571 => true, + 126579 => true, + 126584 => true, + 126589 => true, + 126591 => true, + 126602 => true, + 126620 => true, + 126621 => true, + 126622 => true, + 126623 => true, + 126624 => true, + 126628 => true, + 126634 => true, + 127020 => true, + 127021 => true, + 127022 => true, + 127023 => true, + 127124 => true, + 127125 => true, + 127126 => true, + 127127 => true, + 127128 => true, + 127129 => true, + 127130 => true, + 127131 => true, + 127132 => true, + 127133 => true, + 127134 => true, + 127135 => true, + 127151 => true, + 127152 => true, + 127168 => true, + 127184 => true, + 127222 => true, + 127223 => true, + 127224 => true, + 127225 => true, + 127226 => true, + 127227 => true, + 127228 => true, + 127229 => true, + 127230 => true, + 127231 => true, + 127232 => true, + 127491 => true, + 127492 => true, + 127493 => true, + 127494 => true, + 127495 => true, + 127496 => true, + 127497 => true, + 127498 => true, + 127499 => true, + 127500 => true, + 127501 => true, + 127502 => true, + 127503 => true, + 127548 => true, + 127549 => true, + 127550 => true, + 127551 => true, + 127561 => true, + 127562 => true, + 127563 => true, + 127564 => true, + 127565 => true, + 127566 => true, + 127567 => true, + 127570 => true, + 127571 => true, + 127572 => true, + 127573 => true, + 127574 => true, + 127575 => true, + 127576 => true, + 127577 => true, + 127578 => true, + 127579 => true, + 127580 => true, + 127581 => true, + 127582 => true, + 127583 => true, + 128728 => true, + 128729 => true, + 128730 => true, + 128731 => true, + 128732 => true, + 128733 => true, + 128734 => true, + 128735 => true, + 128749 => true, + 128750 => true, + 128751 => true, + 128765 => true, + 128766 => true, + 128767 => true, + 128884 => true, + 128885 => true, + 128886 => true, + 128887 => true, + 128888 => true, + 128889 => true, + 128890 => true, + 128891 => true, + 128892 => true, + 128893 => true, + 128894 => true, + 128895 => true, + 128985 => true, + 128986 => true, + 128987 => true, + 128988 => true, + 128989 => true, + 128990 => true, + 128991 => true, + 129004 => true, + 129005 => true, + 129006 => true, + 129007 => true, + 129008 => true, + 129009 => true, + 129010 => true, + 129011 => true, + 129012 => true, + 129013 => true, + 129014 => true, + 129015 => true, + 129016 => true, + 129017 => true, + 129018 => true, + 129019 => true, + 129020 => true, + 129021 => true, + 129022 => true, + 129023 => true, + 129036 => true, + 129037 => true, + 129038 => true, + 129039 => true, + 129096 => true, + 129097 => true, + 129098 => true, + 129099 => true, + 129100 => true, + 129101 => true, + 129102 => true, + 129103 => true, + 129114 => true, + 129115 => true, + 129116 => true, + 129117 => true, + 129118 => true, + 129119 => true, + 129160 => true, + 129161 => true, + 129162 => true, + 129163 => true, + 129164 => true, + 129165 => true, + 129166 => true, + 129167 => true, + 129198 => true, + 129199 => true, + 129401 => true, + 129484 => true, + 129620 => true, + 129621 => true, + 129622 => true, + 129623 => true, + 129624 => true, + 129625 => true, + 129626 => true, + 129627 => true, + 129628 => true, + 129629 => true, + 129630 => true, + 129631 => true, + 129646 => true, + 129647 => true, + 129653 => true, + 129654 => true, + 129655 => true, + 129659 => true, + 129660 => true, + 129661 => true, + 129662 => true, + 129663 => true, + 129671 => true, + 129672 => true, + 129673 => true, + 129674 => true, + 129675 => true, + 129676 => true, + 129677 => true, + 129678 => true, + 129679 => true, + 129705 => true, + 129706 => true, + 129707 => true, + 129708 => true, + 129709 => true, + 129710 => true, + 129711 => true, + 129719 => true, + 129720 => true, + 129721 => true, + 129722 => true, + 129723 => true, + 129724 => true, + 129725 => true, + 129726 => true, + 129727 => true, + 129731 => true, + 129732 => true, + 129733 => true, + 129734 => true, + 129735 => true, + 129736 => true, + 129737 => true, + 129738 => true, + 129739 => true, + 129740 => true, + 129741 => true, + 129742 => true, + 129743 => true, + 129939 => true, + 131070 => true, + 131071 => true, + 177973 => true, + 177974 => true, + 177975 => true, + 177976 => true, + 177977 => true, + 177978 => true, + 177979 => true, + 177980 => true, + 177981 => true, + 177982 => true, + 177983 => true, + 178206 => true, + 178207 => true, + 183970 => true, + 183971 => true, + 183972 => true, + 183973 => true, + 183974 => true, + 183975 => true, + 183976 => true, + 183977 => true, + 183978 => true, + 183979 => true, + 183980 => true, + 183981 => true, + 183982 => true, + 183983 => true, + 194664 => true, + 194676 => true, + 194847 => true, + 194911 => true, + 195007 => true, + 196606 => true, + 196607 => true, + 262142 => true, + 262143 => true, + 327678 => true, + 327679 => true, + 393214 => true, + 393215 => true, + 458750 => true, + 458751 => true, + 524286 => true, + 524287 => true, + 589822 => true, + 589823 => true, + 655358 => true, + 655359 => true, + 720894 => true, + 720895 => true, + 786430 => true, + 786431 => true, + 851966 => true, + 851967 => true, + 917502 => true, + 917503 => true, + 917504 => true, + 917505 => true, + 917506 => true, + 917507 => true, + 917508 => true, + 917509 => true, + 917510 => true, + 917511 => true, + 917512 => true, + 917513 => true, + 917514 => true, + 917515 => true, + 917516 => true, + 917517 => true, + 917518 => true, + 917519 => true, + 917520 => true, + 917521 => true, + 917522 => true, + 917523 => true, + 917524 => true, + 917525 => true, + 917526 => true, + 917527 => true, + 917528 => true, + 917529 => true, + 917530 => true, + 917531 => true, + 917532 => true, + 917533 => true, + 917534 => true, + 917535 => true, + 983038 => true, + 983039 => true, + 1048574 => true, + 1048575 => true, + 1114110 => true, + 1114111 => true, +); diff --git a/3rdparty/symfony/polyfill-intl-idn/Resources/unidata/disallowed_STD3_mapped.php b/3rdparty/symfony/polyfill-intl-idn/Resources/unidata/disallowed_STD3_mapped.php new file mode 100644 index 00000000..54f21cc0 --- /dev/null +++ b/3rdparty/symfony/polyfill-intl-idn/Resources/unidata/disallowed_STD3_mapped.php @@ -0,0 +1,308 @@ + ' ', + 168 => ' ̈', + 175 => ' ̄', + 180 => ' ́', + 184 => ' ̧', + 728 => ' ̆', + 729 => ' ̇', + 730 => ' ̊', + 731 => ' ̨', + 732 => ' ̃', + 733 => ' ̋', + 890 => ' ι', + 894 => ';', + 900 => ' ́', + 901 => ' ̈́', + 8125 => ' ̓', + 8127 => ' ̓', + 8128 => ' ͂', + 8129 => ' ̈͂', + 8141 => ' ̓̀', + 8142 => ' ̓́', + 8143 => ' ̓͂', + 8157 => ' ̔̀', + 8158 => ' ̔́', + 8159 => ' ̔͂', + 8173 => ' ̈̀', + 8174 => ' ̈́', + 8175 => '`', + 8189 => ' ́', + 8190 => ' ̔', + 8192 => ' ', + 8193 => ' ', + 8194 => ' ', + 8195 => ' ', + 8196 => ' ', + 8197 => ' ', + 8198 => ' ', + 8199 => ' ', + 8200 => ' ', + 8201 => ' ', + 8202 => ' ', + 8215 => ' ̳', + 8239 => ' ', + 8252 => '!!', + 8254 => ' ̅', + 8263 => '??', + 8264 => '?!', + 8265 => '!?', + 8287 => ' ', + 8314 => '+', + 8316 => '=', + 8317 => '(', + 8318 => ')', + 8330 => '+', + 8332 => '=', + 8333 => '(', + 8334 => ')', + 8448 => 'a/c', + 8449 => 'a/s', + 8453 => 'c/o', + 8454 => 'c/u', + 9332 => '(1)', + 9333 => '(2)', + 9334 => '(3)', + 9335 => '(4)', + 9336 => '(5)', + 9337 => '(6)', + 9338 => '(7)', + 9339 => '(8)', + 9340 => '(9)', + 9341 => '(10)', + 9342 => '(11)', + 9343 => '(12)', + 9344 => '(13)', + 9345 => '(14)', + 9346 => '(15)', + 9347 => '(16)', + 9348 => '(17)', + 9349 => '(18)', + 9350 => '(19)', + 9351 => '(20)', + 9372 => '(a)', + 9373 => '(b)', + 9374 => '(c)', + 9375 => '(d)', + 9376 => '(e)', + 9377 => '(f)', + 9378 => '(g)', + 9379 => '(h)', + 9380 => '(i)', + 9381 => '(j)', + 9382 => '(k)', + 9383 => '(l)', + 9384 => '(m)', + 9385 => '(n)', + 9386 => '(o)', + 9387 => '(p)', + 9388 => '(q)', + 9389 => '(r)', + 9390 => '(s)', + 9391 => '(t)', + 9392 => '(u)', + 9393 => '(v)', + 9394 => '(w)', + 9395 => '(x)', + 9396 => '(y)', + 9397 => '(z)', + 10868 => '::=', + 10869 => '==', + 10870 => '===', + 12288 => ' ', + 12443 => ' ゙', + 12444 => ' ゚', + 12800 => '(ᄀ)', + 12801 => '(ᄂ)', + 12802 => '(ᄃ)', + 12803 => '(ᄅ)', + 12804 => '(ᄆ)', + 12805 => '(ᄇ)', + 12806 => '(ᄉ)', + 12807 => '(ᄋ)', + 12808 => '(ᄌ)', + 12809 => '(ᄎ)', + 12810 => '(ᄏ)', + 12811 => '(ᄐ)', + 12812 => '(ᄑ)', + 12813 => '(ᄒ)', + 12814 => '(가)', + 12815 => '(나)', + 12816 => '(다)', + 12817 => '(라)', + 12818 => '(마)', + 12819 => '(바)', + 12820 => '(사)', + 12821 => '(아)', + 12822 => '(자)', + 12823 => '(차)', + 12824 => '(카)', + 12825 => '(타)', + 12826 => '(파)', + 12827 => '(하)', + 12828 => '(주)', + 12829 => '(오전)', + 12830 => '(오후)', + 12832 => '(一)', + 12833 => '(二)', + 12834 => '(三)', + 12835 => '(四)', + 12836 => '(五)', + 12837 => '(六)', + 12838 => '(七)', + 12839 => '(八)', + 12840 => '(九)', + 12841 => '(十)', + 12842 => '(月)', + 12843 => '(火)', + 12844 => '(水)', + 12845 => '(木)', + 12846 => '(金)', + 12847 => '(土)', + 12848 => '(日)', + 12849 => '(株)', + 12850 => '(有)', + 12851 => '(社)', + 12852 => '(名)', + 12853 => '(特)', + 12854 => '(財)', + 12855 => '(祝)', + 12856 => '(労)', + 12857 => '(代)', + 12858 => '(呼)', + 12859 => '(学)', + 12860 => '(監)', + 12861 => '(企)', + 12862 => '(資)', + 12863 => '(協)', + 12864 => '(祭)', + 12865 => '(休)', + 12866 => '(自)', + 12867 => '(至)', + 64297 => '+', + 64606 => ' ٌّ', + 64607 => ' ٍّ', + 64608 => ' َّ', + 64609 => ' ُّ', + 64610 => ' ِّ', + 64611 => ' ّٰ', + 65018 => 'صلى الله عليه وسلم', + 65019 => 'جل جلاله', + 65040 => ',', + 65043 => ':', + 65044 => ';', + 65045 => '!', + 65046 => '?', + 65075 => '_', + 65076 => '_', + 65077 => '(', + 65078 => ')', + 65079 => '{', + 65080 => '}', + 65095 => '[', + 65096 => ']', + 65097 => ' ̅', + 65098 => ' ̅', + 65099 => ' ̅', + 65100 => ' ̅', + 65101 => '_', + 65102 => '_', + 65103 => '_', + 65104 => ',', + 65108 => ';', + 65109 => ':', + 65110 => '?', + 65111 => '!', + 65113 => '(', + 65114 => ')', + 65115 => '{', + 65116 => '}', + 65119 => '#', + 65120 => '&', + 65121 => '*', + 65122 => '+', + 65124 => '<', + 65125 => '>', + 65126 => '=', + 65128 => '\\', + 65129 => '$', + 65130 => '%', + 65131 => '@', + 65136 => ' ً', + 65138 => ' ٌ', + 65140 => ' ٍ', + 65142 => ' َ', + 65144 => ' ُ', + 65146 => ' ِ', + 65148 => ' ّ', + 65150 => ' ْ', + 65281 => '!', + 65282 => '"', + 65283 => '#', + 65284 => '$', + 65285 => '%', + 65286 => '&', + 65287 => '\'', + 65288 => '(', + 65289 => ')', + 65290 => '*', + 65291 => '+', + 65292 => ',', + 65295 => '/', + 65306 => ':', + 65307 => ';', + 65308 => '<', + 65309 => '=', + 65310 => '>', + 65311 => '?', + 65312 => '@', + 65339 => '[', + 65340 => '\\', + 65341 => ']', + 65342 => '^', + 65343 => '_', + 65344 => '`', + 65371 => '{', + 65372 => '|', + 65373 => '}', + 65374 => '~', + 65507 => ' ̄', + 127233 => '0,', + 127234 => '1,', + 127235 => '2,', + 127236 => '3,', + 127237 => '4,', + 127238 => '5,', + 127239 => '6,', + 127240 => '7,', + 127241 => '8,', + 127242 => '9,', + 127248 => '(a)', + 127249 => '(b)', + 127250 => '(c)', + 127251 => '(d)', + 127252 => '(e)', + 127253 => '(f)', + 127254 => '(g)', + 127255 => '(h)', + 127256 => '(i)', + 127257 => '(j)', + 127258 => '(k)', + 127259 => '(l)', + 127260 => '(m)', + 127261 => '(n)', + 127262 => '(o)', + 127263 => '(p)', + 127264 => '(q)', + 127265 => '(r)', + 127266 => '(s)', + 127267 => '(t)', + 127268 => '(u)', + 127269 => '(v)', + 127270 => '(w)', + 127271 => '(x)', + 127272 => '(y)', + 127273 => '(z)', +); diff --git a/3rdparty/symfony/polyfill-intl-idn/Resources/unidata/disallowed_STD3_valid.php b/3rdparty/symfony/polyfill-intl-idn/Resources/unidata/disallowed_STD3_valid.php new file mode 100644 index 00000000..223396ec --- /dev/null +++ b/3rdparty/symfony/polyfill-intl-idn/Resources/unidata/disallowed_STD3_valid.php @@ -0,0 +1,71 @@ + true, + 1 => true, + 2 => true, + 3 => true, + 4 => true, + 5 => true, + 6 => true, + 7 => true, + 8 => true, + 9 => true, + 10 => true, + 11 => true, + 12 => true, + 13 => true, + 14 => true, + 15 => true, + 16 => true, + 17 => true, + 18 => true, + 19 => true, + 20 => true, + 21 => true, + 22 => true, + 23 => true, + 24 => true, + 25 => true, + 26 => true, + 27 => true, + 28 => true, + 29 => true, + 30 => true, + 31 => true, + 32 => true, + 33 => true, + 34 => true, + 35 => true, + 36 => true, + 37 => true, + 38 => true, + 39 => true, + 40 => true, + 41 => true, + 42 => true, + 43 => true, + 44 => true, + 47 => true, + 58 => true, + 59 => true, + 60 => true, + 61 => true, + 62 => true, + 63 => true, + 64 => true, + 91 => true, + 92 => true, + 93 => true, + 94 => true, + 95 => true, + 96 => true, + 123 => true, + 124 => true, + 125 => true, + 126 => true, + 127 => true, + 8800 => true, + 8814 => true, + 8815 => true, +); diff --git a/3rdparty/symfony/polyfill-intl-idn/Resources/unidata/ignored.php b/3rdparty/symfony/polyfill-intl-idn/Resources/unidata/ignored.php new file mode 100644 index 00000000..b3778441 --- /dev/null +++ b/3rdparty/symfony/polyfill-intl-idn/Resources/unidata/ignored.php @@ -0,0 +1,273 @@ + true, + 847 => true, + 6155 => true, + 6156 => true, + 6157 => true, + 8203 => true, + 8288 => true, + 8292 => true, + 65024 => true, + 65025 => true, + 65026 => true, + 65027 => true, + 65028 => true, + 65029 => true, + 65030 => true, + 65031 => true, + 65032 => true, + 65033 => true, + 65034 => true, + 65035 => true, + 65036 => true, + 65037 => true, + 65038 => true, + 65039 => true, + 65279 => true, + 113824 => true, + 113825 => true, + 113826 => true, + 113827 => true, + 917760 => true, + 917761 => true, + 917762 => true, + 917763 => true, + 917764 => true, + 917765 => true, + 917766 => true, + 917767 => true, + 917768 => true, + 917769 => true, + 917770 => true, + 917771 => true, + 917772 => true, + 917773 => true, + 917774 => true, + 917775 => true, + 917776 => true, + 917777 => true, + 917778 => true, + 917779 => true, + 917780 => true, + 917781 => true, + 917782 => true, + 917783 => true, + 917784 => true, + 917785 => true, + 917786 => true, + 917787 => true, + 917788 => true, + 917789 => true, + 917790 => true, + 917791 => true, + 917792 => true, + 917793 => true, + 917794 => true, + 917795 => true, + 917796 => true, + 917797 => true, + 917798 => true, + 917799 => true, + 917800 => true, + 917801 => true, + 917802 => true, + 917803 => true, + 917804 => true, + 917805 => true, + 917806 => true, + 917807 => true, + 917808 => true, + 917809 => true, + 917810 => true, + 917811 => true, + 917812 => true, + 917813 => true, + 917814 => true, + 917815 => true, + 917816 => true, + 917817 => true, + 917818 => true, + 917819 => true, + 917820 => true, + 917821 => true, + 917822 => true, + 917823 => true, + 917824 => true, + 917825 => true, + 917826 => true, + 917827 => true, + 917828 => true, + 917829 => true, + 917830 => true, + 917831 => true, + 917832 => true, + 917833 => true, + 917834 => true, + 917835 => true, + 917836 => true, + 917837 => true, + 917838 => true, + 917839 => true, + 917840 => true, + 917841 => true, + 917842 => true, + 917843 => true, + 917844 => true, + 917845 => true, + 917846 => true, + 917847 => true, + 917848 => true, + 917849 => true, + 917850 => true, + 917851 => true, + 917852 => true, + 917853 => true, + 917854 => true, + 917855 => true, + 917856 => true, + 917857 => true, + 917858 => true, + 917859 => true, + 917860 => true, + 917861 => true, + 917862 => true, + 917863 => true, + 917864 => true, + 917865 => true, + 917866 => true, + 917867 => true, + 917868 => true, + 917869 => true, + 917870 => true, + 917871 => true, + 917872 => true, + 917873 => true, + 917874 => true, + 917875 => true, + 917876 => true, + 917877 => true, + 917878 => true, + 917879 => true, + 917880 => true, + 917881 => true, + 917882 => true, + 917883 => true, + 917884 => true, + 917885 => true, + 917886 => true, + 917887 => true, + 917888 => true, + 917889 => true, + 917890 => true, + 917891 => true, + 917892 => true, + 917893 => true, + 917894 => true, + 917895 => true, + 917896 => true, + 917897 => true, + 917898 => true, + 917899 => true, + 917900 => true, + 917901 => true, + 917902 => true, + 917903 => true, + 917904 => true, + 917905 => true, + 917906 => true, + 917907 => true, + 917908 => true, + 917909 => true, + 917910 => true, + 917911 => true, + 917912 => true, + 917913 => true, + 917914 => true, + 917915 => true, + 917916 => true, + 917917 => true, + 917918 => true, + 917919 => true, + 917920 => true, + 917921 => true, + 917922 => true, + 917923 => true, + 917924 => true, + 917925 => true, + 917926 => true, + 917927 => true, + 917928 => true, + 917929 => true, + 917930 => true, + 917931 => true, + 917932 => true, + 917933 => true, + 917934 => true, + 917935 => true, + 917936 => true, + 917937 => true, + 917938 => true, + 917939 => true, + 917940 => true, + 917941 => true, + 917942 => true, + 917943 => true, + 917944 => true, + 917945 => true, + 917946 => true, + 917947 => true, + 917948 => true, + 917949 => true, + 917950 => true, + 917951 => true, + 917952 => true, + 917953 => true, + 917954 => true, + 917955 => true, + 917956 => true, + 917957 => true, + 917958 => true, + 917959 => true, + 917960 => true, + 917961 => true, + 917962 => true, + 917963 => true, + 917964 => true, + 917965 => true, + 917966 => true, + 917967 => true, + 917968 => true, + 917969 => true, + 917970 => true, + 917971 => true, + 917972 => true, + 917973 => true, + 917974 => true, + 917975 => true, + 917976 => true, + 917977 => true, + 917978 => true, + 917979 => true, + 917980 => true, + 917981 => true, + 917982 => true, + 917983 => true, + 917984 => true, + 917985 => true, + 917986 => true, + 917987 => true, + 917988 => true, + 917989 => true, + 917990 => true, + 917991 => true, + 917992 => true, + 917993 => true, + 917994 => true, + 917995 => true, + 917996 => true, + 917997 => true, + 917998 => true, + 917999 => true, +); diff --git a/3rdparty/symfony/polyfill-intl-idn/Resources/unidata/mapped.php b/3rdparty/symfony/polyfill-intl-idn/Resources/unidata/mapped.php new file mode 100644 index 00000000..9b85fe9d --- /dev/null +++ b/3rdparty/symfony/polyfill-intl-idn/Resources/unidata/mapped.php @@ -0,0 +1,5778 @@ + 'a', + 66 => 'b', + 67 => 'c', + 68 => 'd', + 69 => 'e', + 70 => 'f', + 71 => 'g', + 72 => 'h', + 73 => 'i', + 74 => 'j', + 75 => 'k', + 76 => 'l', + 77 => 'm', + 78 => 'n', + 79 => 'o', + 80 => 'p', + 81 => 'q', + 82 => 'r', + 83 => 's', + 84 => 't', + 85 => 'u', + 86 => 'v', + 87 => 'w', + 88 => 'x', + 89 => 'y', + 90 => 'z', + 170 => 'a', + 178 => '2', + 179 => '3', + 181 => 'μ', + 185 => '1', + 186 => 'o', + 188 => '1⁄4', + 189 => '1⁄2', + 190 => '3⁄4', + 192 => 'à', + 193 => 'á', + 194 => 'â', + 195 => 'ã', + 196 => 'ä', + 197 => 'å', + 198 => 'æ', + 199 => 'ç', + 200 => 'è', + 201 => 'é', + 202 => 'ê', + 203 => 'ë', + 204 => 'ì', + 205 => 'í', + 206 => 'î', + 207 => 'ï', + 208 => 'ð', + 209 => 'ñ', + 210 => 'ò', + 211 => 'ó', + 212 => 'ô', + 213 => 'õ', + 214 => 'ö', + 216 => 'ø', + 217 => 'ù', + 218 => 'ú', + 219 => 'û', + 220 => 'ü', + 221 => 'ý', + 222 => 'þ', + 256 => 'ā', + 258 => 'ă', + 260 => 'ą', + 262 => 'ć', + 264 => 'ĉ', + 266 => 'ċ', + 268 => 'č', + 270 => 'ď', + 272 => 'đ', + 274 => 'ē', + 276 => 'ĕ', + 278 => 'ė', + 280 => 'ę', + 282 => 'ě', + 284 => 'ĝ', + 286 => 'ğ', + 288 => 'ġ', + 290 => 'ģ', + 292 => 'ĥ', + 294 => 'ħ', + 296 => 'ĩ', + 298 => 'ī', + 300 => 'ĭ', + 302 => 'į', + 304 => 'i̇', + 306 => 'ij', + 307 => 'ij', + 308 => 'ĵ', + 310 => 'ķ', + 313 => 'ĺ', + 315 => 'ļ', + 317 => 'ľ', + 319 => 'l·', + 320 => 'l·', + 321 => 'ł', + 323 => 'ń', + 325 => 'ņ', + 327 => 'ň', + 329 => 'ʼn', + 330 => 'ŋ', + 332 => 'ō', + 334 => 'ŏ', + 336 => 'ő', + 338 => 'œ', + 340 => 'ŕ', + 342 => 'ŗ', + 344 => 'ř', + 346 => 'ś', + 348 => 'ŝ', + 350 => 'ş', + 352 => 'š', + 354 => 'ţ', + 356 => 'ť', + 358 => 'ŧ', + 360 => 'ũ', + 362 => 'ū', + 364 => 'ŭ', + 366 => 'ů', + 368 => 'ű', + 370 => 'ų', + 372 => 'ŵ', + 374 => 'ŷ', + 376 => 'ÿ', + 377 => 'ź', + 379 => 'ż', + 381 => 'ž', + 383 => 's', + 385 => 'ɓ', + 386 => 'ƃ', + 388 => 'ƅ', + 390 => 'ɔ', + 391 => 'ƈ', + 393 => 'ɖ', + 394 => 'ɗ', + 395 => 'ƌ', + 398 => 'ǝ', + 399 => 'ə', + 400 => 'ɛ', + 401 => 'ƒ', + 403 => 'ɠ', + 404 => 'ɣ', + 406 => 'ɩ', + 407 => 'ɨ', + 408 => 'ƙ', + 412 => 'ɯ', + 413 => 'ɲ', + 415 => 'ɵ', + 416 => 'ơ', + 418 => 'ƣ', + 420 => 'ƥ', + 422 => 'ʀ', + 423 => 'ƨ', + 425 => 'ʃ', + 428 => 'ƭ', + 430 => 'ʈ', + 431 => 'ư', + 433 => 'ʊ', + 434 => 'ʋ', + 435 => 'ƴ', + 437 => 'ƶ', + 439 => 'ʒ', + 440 => 'ƹ', + 444 => 'ƽ', + 452 => 'dž', + 453 => 'dž', + 454 => 'dž', + 455 => 'lj', + 456 => 'lj', + 457 => 'lj', + 458 => 'nj', + 459 => 'nj', + 460 => 'nj', + 461 => 'ǎ', + 463 => 'ǐ', + 465 => 'ǒ', + 467 => 'ǔ', + 469 => 'ǖ', + 471 => 'ǘ', + 473 => 'ǚ', + 475 => 'ǜ', + 478 => 'ǟ', + 480 => 'ǡ', + 482 => 'ǣ', + 484 => 'ǥ', + 486 => 'ǧ', + 488 => 'ǩ', + 490 => 'ǫ', + 492 => 'ǭ', + 494 => 'ǯ', + 497 => 'dz', + 498 => 'dz', + 499 => 'dz', + 500 => 'ǵ', + 502 => 'ƕ', + 503 => 'ƿ', + 504 => 'ǹ', + 506 => 'ǻ', + 508 => 'ǽ', + 510 => 'ǿ', + 512 => 'ȁ', + 514 => 'ȃ', + 516 => 'ȅ', + 518 => 'ȇ', + 520 => 'ȉ', + 522 => 'ȋ', + 524 => 'ȍ', + 526 => 'ȏ', + 528 => 'ȑ', + 530 => 'ȓ', + 532 => 'ȕ', + 534 => 'ȗ', + 536 => 'ș', + 538 => 'ț', + 540 => 'ȝ', + 542 => 'ȟ', + 544 => 'ƞ', + 546 => 'ȣ', + 548 => 'ȥ', + 550 => 'ȧ', + 552 => 'ȩ', + 554 => 'ȫ', + 556 => 'ȭ', + 558 => 'ȯ', + 560 => 'ȱ', + 562 => 'ȳ', + 570 => 'ⱥ', + 571 => 'ȼ', + 573 => 'ƚ', + 574 => 'ⱦ', + 577 => 'ɂ', + 579 => 'ƀ', + 580 => 'ʉ', + 581 => 'ʌ', + 582 => 'ɇ', + 584 => 'ɉ', + 586 => 'ɋ', + 588 => 'ɍ', + 590 => 'ɏ', + 688 => 'h', + 689 => 'ɦ', + 690 => 'j', + 691 => 'r', + 692 => 'ɹ', + 693 => 'ɻ', + 694 => 'ʁ', + 695 => 'w', + 696 => 'y', + 736 => 'ɣ', + 737 => 'l', + 738 => 's', + 739 => 'x', + 740 => 'ʕ', + 832 => '̀', + 833 => '́', + 835 => '̓', + 836 => '̈́', + 837 => 'ι', + 880 => 'ͱ', + 882 => 'ͳ', + 884 => 'ʹ', + 886 => 'ͷ', + 895 => 'ϳ', + 902 => 'ά', + 903 => '·', + 904 => 'έ', + 905 => 'ή', + 906 => 'ί', + 908 => 'ό', + 910 => 'ύ', + 911 => 'ώ', + 913 => 'α', + 914 => 'β', + 915 => 'γ', + 916 => 'δ', + 917 => 'ε', + 918 => 'ζ', + 919 => 'η', + 920 => 'θ', + 921 => 'ι', + 922 => 'κ', + 923 => 'λ', + 924 => 'μ', + 925 => 'ν', + 926 => 'ξ', + 927 => 'ο', + 928 => 'π', + 929 => 'ρ', + 931 => 'σ', + 932 => 'τ', + 933 => 'υ', + 934 => 'φ', + 935 => 'χ', + 936 => 'ψ', + 937 => 'ω', + 938 => 'ϊ', + 939 => 'ϋ', + 975 => 'ϗ', + 976 => 'β', + 977 => 'θ', + 978 => 'υ', + 979 => 'ύ', + 980 => 'ϋ', + 981 => 'φ', + 982 => 'π', + 984 => 'ϙ', + 986 => 'ϛ', + 988 => 'ϝ', + 990 => 'ϟ', + 992 => 'ϡ', + 994 => 'ϣ', + 996 => 'ϥ', + 998 => 'ϧ', + 1000 => 'ϩ', + 1002 => 'ϫ', + 1004 => 'ϭ', + 1006 => 'ϯ', + 1008 => 'κ', + 1009 => 'ρ', + 1010 => 'σ', + 1012 => 'θ', + 1013 => 'ε', + 1015 => 'ϸ', + 1017 => 'σ', + 1018 => 'ϻ', + 1021 => 'ͻ', + 1022 => 'ͼ', + 1023 => 'ͽ', + 1024 => 'ѐ', + 1025 => 'ё', + 1026 => 'ђ', + 1027 => 'ѓ', + 1028 => 'є', + 1029 => 'ѕ', + 1030 => 'і', + 1031 => 'ї', + 1032 => 'ј', + 1033 => 'љ', + 1034 => 'њ', + 1035 => 'ћ', + 1036 => 'ќ', + 1037 => 'ѝ', + 1038 => 'ў', + 1039 => 'џ', + 1040 => 'а', + 1041 => 'б', + 1042 => 'в', + 1043 => 'г', + 1044 => 'д', + 1045 => 'е', + 1046 => 'ж', + 1047 => 'з', + 1048 => 'и', + 1049 => 'й', + 1050 => 'к', + 1051 => 'л', + 1052 => 'м', + 1053 => 'н', + 1054 => 'о', + 1055 => 'п', + 1056 => 'р', + 1057 => 'с', + 1058 => 'т', + 1059 => 'у', + 1060 => 'ф', + 1061 => 'х', + 1062 => 'ц', + 1063 => 'ч', + 1064 => 'ш', + 1065 => 'щ', + 1066 => 'ъ', + 1067 => 'ы', + 1068 => 'ь', + 1069 => 'э', + 1070 => 'ю', + 1071 => 'я', + 1120 => 'ѡ', + 1122 => 'ѣ', + 1124 => 'ѥ', + 1126 => 'ѧ', + 1128 => 'ѩ', + 1130 => 'ѫ', + 1132 => 'ѭ', + 1134 => 'ѯ', + 1136 => 'ѱ', + 1138 => 'ѳ', + 1140 => 'ѵ', + 1142 => 'ѷ', + 1144 => 'ѹ', + 1146 => 'ѻ', + 1148 => 'ѽ', + 1150 => 'ѿ', + 1152 => 'ҁ', + 1162 => 'ҋ', + 1164 => 'ҍ', + 1166 => 'ҏ', + 1168 => 'ґ', + 1170 => 'ғ', + 1172 => 'ҕ', + 1174 => 'җ', + 1176 => 'ҙ', + 1178 => 'қ', + 1180 => 'ҝ', + 1182 => 'ҟ', + 1184 => 'ҡ', + 1186 => 'ң', + 1188 => 'ҥ', + 1190 => 'ҧ', + 1192 => 'ҩ', + 1194 => 'ҫ', + 1196 => 'ҭ', + 1198 => 'ү', + 1200 => 'ұ', + 1202 => 'ҳ', + 1204 => 'ҵ', + 1206 => 'ҷ', + 1208 => 'ҹ', + 1210 => 'һ', + 1212 => 'ҽ', + 1214 => 'ҿ', + 1217 => 'ӂ', + 1219 => 'ӄ', + 1221 => 'ӆ', + 1223 => 'ӈ', + 1225 => 'ӊ', + 1227 => 'ӌ', + 1229 => 'ӎ', + 1232 => 'ӑ', + 1234 => 'ӓ', + 1236 => 'ӕ', + 1238 => 'ӗ', + 1240 => 'ә', + 1242 => 'ӛ', + 1244 => 'ӝ', + 1246 => 'ӟ', + 1248 => 'ӡ', + 1250 => 'ӣ', + 1252 => 'ӥ', + 1254 => 'ӧ', + 1256 => 'ө', + 1258 => 'ӫ', + 1260 => 'ӭ', + 1262 => 'ӯ', + 1264 => 'ӱ', + 1266 => 'ӳ', + 1268 => 'ӵ', + 1270 => 'ӷ', + 1272 => 'ӹ', + 1274 => 'ӻ', + 1276 => 'ӽ', + 1278 => 'ӿ', + 1280 => 'ԁ', + 1282 => 'ԃ', + 1284 => 'ԅ', + 1286 => 'ԇ', + 1288 => 'ԉ', + 1290 => 'ԋ', + 1292 => 'ԍ', + 1294 => 'ԏ', + 1296 => 'ԑ', + 1298 => 'ԓ', + 1300 => 'ԕ', + 1302 => 'ԗ', + 1304 => 'ԙ', + 1306 => 'ԛ', + 1308 => 'ԝ', + 1310 => 'ԟ', + 1312 => 'ԡ', + 1314 => 'ԣ', + 1316 => 'ԥ', + 1318 => 'ԧ', + 1320 => 'ԩ', + 1322 => 'ԫ', + 1324 => 'ԭ', + 1326 => 'ԯ', + 1329 => 'ա', + 1330 => 'բ', + 1331 => 'գ', + 1332 => 'դ', + 1333 => 'ե', + 1334 => 'զ', + 1335 => 'է', + 1336 => 'ը', + 1337 => 'թ', + 1338 => 'ժ', + 1339 => 'ի', + 1340 => 'լ', + 1341 => 'խ', + 1342 => 'ծ', + 1343 => 'կ', + 1344 => 'հ', + 1345 => 'ձ', + 1346 => 'ղ', + 1347 => 'ճ', + 1348 => 'մ', + 1349 => 'յ', + 1350 => 'ն', + 1351 => 'շ', + 1352 => 'ո', + 1353 => 'չ', + 1354 => 'պ', + 1355 => 'ջ', + 1356 => 'ռ', + 1357 => 'ս', + 1358 => 'վ', + 1359 => 'տ', + 1360 => 'ր', + 1361 => 'ց', + 1362 => 'ւ', + 1363 => 'փ', + 1364 => 'ք', + 1365 => 'օ', + 1366 => 'ֆ', + 1415 => 'եւ', + 1653 => 'اٴ', + 1654 => 'وٴ', + 1655 => 'ۇٴ', + 1656 => 'يٴ', + 2392 => 'क़', + 2393 => 'ख़', + 2394 => 'ग़', + 2395 => 'ज़', + 2396 => 'ड़', + 2397 => 'ढ़', + 2398 => 'फ़', + 2399 => 'य़', + 2524 => 'ড়', + 2525 => 'ঢ়', + 2527 => 'য়', + 2611 => 'ਲ਼', + 2614 => 'ਸ਼', + 2649 => 'ਖ਼', + 2650 => 'ਗ਼', + 2651 => 'ਜ਼', + 2654 => 'ਫ਼', + 2908 => 'ଡ଼', + 2909 => 'ଢ଼', + 3635 => 'ํา', + 3763 => 'ໍາ', + 3804 => 'ຫນ', + 3805 => 'ຫມ', + 3852 => '་', + 3907 => 'གྷ', + 3917 => 'ཌྷ', + 3922 => 'དྷ', + 3927 => 'བྷ', + 3932 => 'ཛྷ', + 3945 => 'ཀྵ', + 3955 => 'ཱི', + 3957 => 'ཱུ', + 3958 => 'ྲྀ', + 3959 => 'ྲཱྀ', + 3960 => 'ླྀ', + 3961 => 'ླཱྀ', + 3969 => 'ཱྀ', + 3987 => 'ྒྷ', + 3997 => 'ྜྷ', + 4002 => 'ྡྷ', + 4007 => 'ྦྷ', + 4012 => 'ྫྷ', + 4025 => 'ྐྵ', + 4295 => 'ⴧ', + 4301 => 'ⴭ', + 4348 => 'ნ', + 5112 => 'Ᏸ', + 5113 => 'Ᏹ', + 5114 => 'Ᏺ', + 5115 => 'Ᏻ', + 5116 => 'Ᏼ', + 5117 => 'Ᏽ', + 7296 => 'в', + 7297 => 'д', + 7298 => 'о', + 7299 => 'с', + 7300 => 'т', + 7301 => 'т', + 7302 => 'ъ', + 7303 => 'ѣ', + 7304 => 'ꙋ', + 7312 => 'ა', + 7313 => 'ბ', + 7314 => 'გ', + 7315 => 'დ', + 7316 => 'ე', + 7317 => 'ვ', + 7318 => 'ზ', + 7319 => 'თ', + 7320 => 'ი', + 7321 => 'კ', + 7322 => 'ლ', + 7323 => 'მ', + 7324 => 'ნ', + 7325 => 'ო', + 7326 => 'პ', + 7327 => 'ჟ', + 7328 => 'რ', + 7329 => 'ს', + 7330 => 'ტ', + 7331 => 'უ', + 7332 => 'ფ', + 7333 => 'ქ', + 7334 => 'ღ', + 7335 => 'ყ', + 7336 => 'შ', + 7337 => 'ჩ', + 7338 => 'ც', + 7339 => 'ძ', + 7340 => 'წ', + 7341 => 'ჭ', + 7342 => 'ხ', + 7343 => 'ჯ', + 7344 => 'ჰ', + 7345 => 'ჱ', + 7346 => 'ჲ', + 7347 => 'ჳ', + 7348 => 'ჴ', + 7349 => 'ჵ', + 7350 => 'ჶ', + 7351 => 'ჷ', + 7352 => 'ჸ', + 7353 => 'ჹ', + 7354 => 'ჺ', + 7357 => 'ჽ', + 7358 => 'ჾ', + 7359 => 'ჿ', + 7468 => 'a', + 7469 => 'æ', + 7470 => 'b', + 7472 => 'd', + 7473 => 'e', + 7474 => 'ǝ', + 7475 => 'g', + 7476 => 'h', + 7477 => 'i', + 7478 => 'j', + 7479 => 'k', + 7480 => 'l', + 7481 => 'm', + 7482 => 'n', + 7484 => 'o', + 7485 => 'ȣ', + 7486 => 'p', + 7487 => 'r', + 7488 => 't', + 7489 => 'u', + 7490 => 'w', + 7491 => 'a', + 7492 => 'ɐ', + 7493 => 'ɑ', + 7494 => 'ᴂ', + 7495 => 'b', + 7496 => 'd', + 7497 => 'e', + 7498 => 'ə', + 7499 => 'ɛ', + 7500 => 'ɜ', + 7501 => 'g', + 7503 => 'k', + 7504 => 'm', + 7505 => 'ŋ', + 7506 => 'o', + 7507 => 'ɔ', + 7508 => 'ᴖ', + 7509 => 'ᴗ', + 7510 => 'p', + 7511 => 't', + 7512 => 'u', + 7513 => 'ᴝ', + 7514 => 'ɯ', + 7515 => 'v', + 7516 => 'ᴥ', + 7517 => 'β', + 7518 => 'γ', + 7519 => 'δ', + 7520 => 'φ', + 7521 => 'χ', + 7522 => 'i', + 7523 => 'r', + 7524 => 'u', + 7525 => 'v', + 7526 => 'β', + 7527 => 'γ', + 7528 => 'ρ', + 7529 => 'φ', + 7530 => 'χ', + 7544 => 'н', + 7579 => 'ɒ', + 7580 => 'c', + 7581 => 'ɕ', + 7582 => 'ð', + 7583 => 'ɜ', + 7584 => 'f', + 7585 => 'ɟ', + 7586 => 'ɡ', + 7587 => 'ɥ', + 7588 => 'ɨ', + 7589 => 'ɩ', + 7590 => 'ɪ', + 7591 => 'ᵻ', + 7592 => 'ʝ', + 7593 => 'ɭ', + 7594 => 'ᶅ', + 7595 => 'ʟ', + 7596 => 'ɱ', + 7597 => 'ɰ', + 7598 => 'ɲ', + 7599 => 'ɳ', + 7600 => 'ɴ', + 7601 => 'ɵ', + 7602 => 'ɸ', + 7603 => 'ʂ', + 7604 => 'ʃ', + 7605 => 'ƫ', + 7606 => 'ʉ', + 7607 => 'ʊ', + 7608 => 'ᴜ', + 7609 => 'ʋ', + 7610 => 'ʌ', + 7611 => 'z', + 7612 => 'ʐ', + 7613 => 'ʑ', + 7614 => 'ʒ', + 7615 => 'θ', + 7680 => 'ḁ', + 7682 => 'ḃ', + 7684 => 'ḅ', + 7686 => 'ḇ', + 7688 => 'ḉ', + 7690 => 'ḋ', + 7692 => 'ḍ', + 7694 => 'ḏ', + 7696 => 'ḑ', + 7698 => 'ḓ', + 7700 => 'ḕ', + 7702 => 'ḗ', + 7704 => 'ḙ', + 7706 => 'ḛ', + 7708 => 'ḝ', + 7710 => 'ḟ', + 7712 => 'ḡ', + 7714 => 'ḣ', + 7716 => 'ḥ', + 7718 => 'ḧ', + 7720 => 'ḩ', + 7722 => 'ḫ', + 7724 => 'ḭ', + 7726 => 'ḯ', + 7728 => 'ḱ', + 7730 => 'ḳ', + 7732 => 'ḵ', + 7734 => 'ḷ', + 7736 => 'ḹ', + 7738 => 'ḻ', + 7740 => 'ḽ', + 7742 => 'ḿ', + 7744 => 'ṁ', + 7746 => 'ṃ', + 7748 => 'ṅ', + 7750 => 'ṇ', + 7752 => 'ṉ', + 7754 => 'ṋ', + 7756 => 'ṍ', + 7758 => 'ṏ', + 7760 => 'ṑ', + 7762 => 'ṓ', + 7764 => 'ṕ', + 7766 => 'ṗ', + 7768 => 'ṙ', + 7770 => 'ṛ', + 7772 => 'ṝ', + 7774 => 'ṟ', + 7776 => 'ṡ', + 7778 => 'ṣ', + 7780 => 'ṥ', + 7782 => 'ṧ', + 7784 => 'ṩ', + 7786 => 'ṫ', + 7788 => 'ṭ', + 7790 => 'ṯ', + 7792 => 'ṱ', + 7794 => 'ṳ', + 7796 => 'ṵ', + 7798 => 'ṷ', + 7800 => 'ṹ', + 7802 => 'ṻ', + 7804 => 'ṽ', + 7806 => 'ṿ', + 7808 => 'ẁ', + 7810 => 'ẃ', + 7812 => 'ẅ', + 7814 => 'ẇ', + 7816 => 'ẉ', + 7818 => 'ẋ', + 7820 => 'ẍ', + 7822 => 'ẏ', + 7824 => 'ẑ', + 7826 => 'ẓ', + 7828 => 'ẕ', + 7834 => 'aʾ', + 7835 => 'ṡ', + 7838 => 'ss', + 7840 => 'ạ', + 7842 => 'ả', + 7844 => 'ấ', + 7846 => 'ầ', + 7848 => 'ẩ', + 7850 => 'ẫ', + 7852 => 'ậ', + 7854 => 'ắ', + 7856 => 'ằ', + 7858 => 'ẳ', + 7860 => 'ẵ', + 7862 => 'ặ', + 7864 => 'ẹ', + 7866 => 'ẻ', + 7868 => 'ẽ', + 7870 => 'ế', + 7872 => 'ề', + 7874 => 'ể', + 7876 => 'ễ', + 7878 => 'ệ', + 7880 => 'ỉ', + 7882 => 'ị', + 7884 => 'ọ', + 7886 => 'ỏ', + 7888 => 'ố', + 7890 => 'ồ', + 7892 => 'ổ', + 7894 => 'ỗ', + 7896 => 'ộ', + 7898 => 'ớ', + 7900 => 'ờ', + 7902 => 'ở', + 7904 => 'ỡ', + 7906 => 'ợ', + 7908 => 'ụ', + 7910 => 'ủ', + 7912 => 'ứ', + 7914 => 'ừ', + 7916 => 'ử', + 7918 => 'ữ', + 7920 => 'ự', + 7922 => 'ỳ', + 7924 => 'ỵ', + 7926 => 'ỷ', + 7928 => 'ỹ', + 7930 => 'ỻ', + 7932 => 'ỽ', + 7934 => 'ỿ', + 7944 => 'ἀ', + 7945 => 'ἁ', + 7946 => 'ἂ', + 7947 => 'ἃ', + 7948 => 'ἄ', + 7949 => 'ἅ', + 7950 => 'ἆ', + 7951 => 'ἇ', + 7960 => 'ἐ', + 7961 => 'ἑ', + 7962 => 'ἒ', + 7963 => 'ἓ', + 7964 => 'ἔ', + 7965 => 'ἕ', + 7976 => 'ἠ', + 7977 => 'ἡ', + 7978 => 'ἢ', + 7979 => 'ἣ', + 7980 => 'ἤ', + 7981 => 'ἥ', + 7982 => 'ἦ', + 7983 => 'ἧ', + 7992 => 'ἰ', + 7993 => 'ἱ', + 7994 => 'ἲ', + 7995 => 'ἳ', + 7996 => 'ἴ', + 7997 => 'ἵ', + 7998 => 'ἶ', + 7999 => 'ἷ', + 8008 => 'ὀ', + 8009 => 'ὁ', + 8010 => 'ὂ', + 8011 => 'ὃ', + 8012 => 'ὄ', + 8013 => 'ὅ', + 8025 => 'ὑ', + 8027 => 'ὓ', + 8029 => 'ὕ', + 8031 => 'ὗ', + 8040 => 'ὠ', + 8041 => 'ὡ', + 8042 => 'ὢ', + 8043 => 'ὣ', + 8044 => 'ὤ', + 8045 => 'ὥ', + 8046 => 'ὦ', + 8047 => 'ὧ', + 8049 => 'ά', + 8051 => 'έ', + 8053 => 'ή', + 8055 => 'ί', + 8057 => 'ό', + 8059 => 'ύ', + 8061 => 'ώ', + 8064 => 'ἀι', + 8065 => 'ἁι', + 8066 => 'ἂι', + 8067 => 'ἃι', + 8068 => 'ἄι', + 8069 => 'ἅι', + 8070 => 'ἆι', + 8071 => 'ἇι', + 8072 => 'ἀι', + 8073 => 'ἁι', + 8074 => 'ἂι', + 8075 => 'ἃι', + 8076 => 'ἄι', + 8077 => 'ἅι', + 8078 => 'ἆι', + 8079 => 'ἇι', + 8080 => 'ἠι', + 8081 => 'ἡι', + 8082 => 'ἢι', + 8083 => 'ἣι', + 8084 => 'ἤι', + 8085 => 'ἥι', + 8086 => 'ἦι', + 8087 => 'ἧι', + 8088 => 'ἠι', + 8089 => 'ἡι', + 8090 => 'ἢι', + 8091 => 'ἣι', + 8092 => 'ἤι', + 8093 => 'ἥι', + 8094 => 'ἦι', + 8095 => 'ἧι', + 8096 => 'ὠι', + 8097 => 'ὡι', + 8098 => 'ὢι', + 8099 => 'ὣι', + 8100 => 'ὤι', + 8101 => 'ὥι', + 8102 => 'ὦι', + 8103 => 'ὧι', + 8104 => 'ὠι', + 8105 => 'ὡι', + 8106 => 'ὢι', + 8107 => 'ὣι', + 8108 => 'ὤι', + 8109 => 'ὥι', + 8110 => 'ὦι', + 8111 => 'ὧι', + 8114 => 'ὰι', + 8115 => 'αι', + 8116 => 'άι', + 8119 => 'ᾶι', + 8120 => 'ᾰ', + 8121 => 'ᾱ', + 8122 => 'ὰ', + 8123 => 'ά', + 8124 => 'αι', + 8126 => 'ι', + 8130 => 'ὴι', + 8131 => 'ηι', + 8132 => 'ήι', + 8135 => 'ῆι', + 8136 => 'ὲ', + 8137 => 'έ', + 8138 => 'ὴ', + 8139 => 'ή', + 8140 => 'ηι', + 8147 => 'ΐ', + 8152 => 'ῐ', + 8153 => 'ῑ', + 8154 => 'ὶ', + 8155 => 'ί', + 8163 => 'ΰ', + 8168 => 'ῠ', + 8169 => 'ῡ', + 8170 => 'ὺ', + 8171 => 'ύ', + 8172 => 'ῥ', + 8178 => 'ὼι', + 8179 => 'ωι', + 8180 => 'ώι', + 8183 => 'ῶι', + 8184 => 'ὸ', + 8185 => 'ό', + 8186 => 'ὼ', + 8187 => 'ώ', + 8188 => 'ωι', + 8209 => '‐', + 8243 => '′′', + 8244 => '′′′', + 8246 => '‵‵', + 8247 => '‵‵‵', + 8279 => '′′′′', + 8304 => '0', + 8305 => 'i', + 8308 => '4', + 8309 => '5', + 8310 => '6', + 8311 => '7', + 8312 => '8', + 8313 => '9', + 8315 => '−', + 8319 => 'n', + 8320 => '0', + 8321 => '1', + 8322 => '2', + 8323 => '3', + 8324 => '4', + 8325 => '5', + 8326 => '6', + 8327 => '7', + 8328 => '8', + 8329 => '9', + 8331 => '−', + 8336 => 'a', + 8337 => 'e', + 8338 => 'o', + 8339 => 'x', + 8340 => 'ə', + 8341 => 'h', + 8342 => 'k', + 8343 => 'l', + 8344 => 'm', + 8345 => 'n', + 8346 => 'p', + 8347 => 's', + 8348 => 't', + 8360 => 'rs', + 8450 => 'c', + 8451 => '°c', + 8455 => 'ɛ', + 8457 => '°f', + 8458 => 'g', + 8459 => 'h', + 8460 => 'h', + 8461 => 'h', + 8462 => 'h', + 8463 => 'ħ', + 8464 => 'i', + 8465 => 'i', + 8466 => 'l', + 8467 => 'l', + 8469 => 'n', + 8470 => 'no', + 8473 => 'p', + 8474 => 'q', + 8475 => 'r', + 8476 => 'r', + 8477 => 'r', + 8480 => 'sm', + 8481 => 'tel', + 8482 => 'tm', + 8484 => 'z', + 8486 => 'ω', + 8488 => 'z', + 8490 => 'k', + 8491 => 'å', + 8492 => 'b', + 8493 => 'c', + 8495 => 'e', + 8496 => 'e', + 8497 => 'f', + 8499 => 'm', + 8500 => 'o', + 8501 => 'א', + 8502 => 'ב', + 8503 => 'ג', + 8504 => 'ד', + 8505 => 'i', + 8507 => 'fax', + 8508 => 'π', + 8509 => 'γ', + 8510 => 'γ', + 8511 => 'π', + 8512 => '∑', + 8517 => 'd', + 8518 => 'd', + 8519 => 'e', + 8520 => 'i', + 8521 => 'j', + 8528 => '1⁄7', + 8529 => '1⁄9', + 8530 => '1⁄10', + 8531 => '1⁄3', + 8532 => '2⁄3', + 8533 => '1⁄5', + 8534 => '2⁄5', + 8535 => '3⁄5', + 8536 => '4⁄5', + 8537 => '1⁄6', + 8538 => '5⁄6', + 8539 => '1⁄8', + 8540 => '3⁄8', + 8541 => '5⁄8', + 8542 => '7⁄8', + 8543 => '1⁄', + 8544 => 'i', + 8545 => 'ii', + 8546 => 'iii', + 8547 => 'iv', + 8548 => 'v', + 8549 => 'vi', + 8550 => 'vii', + 8551 => 'viii', + 8552 => 'ix', + 8553 => 'x', + 8554 => 'xi', + 8555 => 'xii', + 8556 => 'l', + 8557 => 'c', + 8558 => 'd', + 8559 => 'm', + 8560 => 'i', + 8561 => 'ii', + 8562 => 'iii', + 8563 => 'iv', + 8564 => 'v', + 8565 => 'vi', + 8566 => 'vii', + 8567 => 'viii', + 8568 => 'ix', + 8569 => 'x', + 8570 => 'xi', + 8571 => 'xii', + 8572 => 'l', + 8573 => 'c', + 8574 => 'd', + 8575 => 'm', + 8585 => '0⁄3', + 8748 => '∫∫', + 8749 => '∫∫∫', + 8751 => '∮∮', + 8752 => '∮∮∮', + 9001 => '〈', + 9002 => '〉', + 9312 => '1', + 9313 => '2', + 9314 => '3', + 9315 => '4', + 9316 => '5', + 9317 => '6', + 9318 => '7', + 9319 => '8', + 9320 => '9', + 9321 => '10', + 9322 => '11', + 9323 => '12', + 9324 => '13', + 9325 => '14', + 9326 => '15', + 9327 => '16', + 9328 => '17', + 9329 => '18', + 9330 => '19', + 9331 => '20', + 9398 => 'a', + 9399 => 'b', + 9400 => 'c', + 9401 => 'd', + 9402 => 'e', + 9403 => 'f', + 9404 => 'g', + 9405 => 'h', + 9406 => 'i', + 9407 => 'j', + 9408 => 'k', + 9409 => 'l', + 9410 => 'm', + 9411 => 'n', + 9412 => 'o', + 9413 => 'p', + 9414 => 'q', + 9415 => 'r', + 9416 => 's', + 9417 => 't', + 9418 => 'u', + 9419 => 'v', + 9420 => 'w', + 9421 => 'x', + 9422 => 'y', + 9423 => 'z', + 9424 => 'a', + 9425 => 'b', + 9426 => 'c', + 9427 => 'd', + 9428 => 'e', + 9429 => 'f', + 9430 => 'g', + 9431 => 'h', + 9432 => 'i', + 9433 => 'j', + 9434 => 'k', + 9435 => 'l', + 9436 => 'm', + 9437 => 'n', + 9438 => 'o', + 9439 => 'p', + 9440 => 'q', + 9441 => 'r', + 9442 => 's', + 9443 => 't', + 9444 => 'u', + 9445 => 'v', + 9446 => 'w', + 9447 => 'x', + 9448 => 'y', + 9449 => 'z', + 9450 => '0', + 10764 => '∫∫∫∫', + 10972 => '⫝̸', + 11264 => 'ⰰ', + 11265 => 'ⰱ', + 11266 => 'ⰲ', + 11267 => 'ⰳ', + 11268 => 'ⰴ', + 11269 => 'ⰵ', + 11270 => 'ⰶ', + 11271 => 'ⰷ', + 11272 => 'ⰸ', + 11273 => 'ⰹ', + 11274 => 'ⰺ', + 11275 => 'ⰻ', + 11276 => 'ⰼ', + 11277 => 'ⰽ', + 11278 => 'ⰾ', + 11279 => 'ⰿ', + 11280 => 'ⱀ', + 11281 => 'ⱁ', + 11282 => 'ⱂ', + 11283 => 'ⱃ', + 11284 => 'ⱄ', + 11285 => 'ⱅ', + 11286 => 'ⱆ', + 11287 => 'ⱇ', + 11288 => 'ⱈ', + 11289 => 'ⱉ', + 11290 => 'ⱊ', + 11291 => 'ⱋ', + 11292 => 'ⱌ', + 11293 => 'ⱍ', + 11294 => 'ⱎ', + 11295 => 'ⱏ', + 11296 => 'ⱐ', + 11297 => 'ⱑ', + 11298 => 'ⱒ', + 11299 => 'ⱓ', + 11300 => 'ⱔ', + 11301 => 'ⱕ', + 11302 => 'ⱖ', + 11303 => 'ⱗ', + 11304 => 'ⱘ', + 11305 => 'ⱙ', + 11306 => 'ⱚ', + 11307 => 'ⱛ', + 11308 => 'ⱜ', + 11309 => 'ⱝ', + 11310 => 'ⱞ', + 11360 => 'ⱡ', + 11362 => 'ɫ', + 11363 => 'ᵽ', + 11364 => 'ɽ', + 11367 => 'ⱨ', + 11369 => 'ⱪ', + 11371 => 'ⱬ', + 11373 => 'ɑ', + 11374 => 'ɱ', + 11375 => 'ɐ', + 11376 => 'ɒ', + 11378 => 'ⱳ', + 11381 => 'ⱶ', + 11388 => 'j', + 11389 => 'v', + 11390 => 'ȿ', + 11391 => 'ɀ', + 11392 => 'ⲁ', + 11394 => 'ⲃ', + 11396 => 'ⲅ', + 11398 => 'ⲇ', + 11400 => 'ⲉ', + 11402 => 'ⲋ', + 11404 => 'ⲍ', + 11406 => 'ⲏ', + 11408 => 'ⲑ', + 11410 => 'ⲓ', + 11412 => 'ⲕ', + 11414 => 'ⲗ', + 11416 => 'ⲙ', + 11418 => 'ⲛ', + 11420 => 'ⲝ', + 11422 => 'ⲟ', + 11424 => 'ⲡ', + 11426 => 'ⲣ', + 11428 => 'ⲥ', + 11430 => 'ⲧ', + 11432 => 'ⲩ', + 11434 => 'ⲫ', + 11436 => 'ⲭ', + 11438 => 'ⲯ', + 11440 => 'ⲱ', + 11442 => 'ⲳ', + 11444 => 'ⲵ', + 11446 => 'ⲷ', + 11448 => 'ⲹ', + 11450 => 'ⲻ', + 11452 => 'ⲽ', + 11454 => 'ⲿ', + 11456 => 'ⳁ', + 11458 => 'ⳃ', + 11460 => 'ⳅ', + 11462 => 'ⳇ', + 11464 => 'ⳉ', + 11466 => 'ⳋ', + 11468 => 'ⳍ', + 11470 => 'ⳏ', + 11472 => 'ⳑ', + 11474 => 'ⳓ', + 11476 => 'ⳕ', + 11478 => 'ⳗ', + 11480 => 'ⳙ', + 11482 => 'ⳛ', + 11484 => 'ⳝ', + 11486 => 'ⳟ', + 11488 => 'ⳡ', + 11490 => 'ⳣ', + 11499 => 'ⳬ', + 11501 => 'ⳮ', + 11506 => 'ⳳ', + 11631 => 'ⵡ', + 11935 => '母', + 12019 => '龟', + 12032 => '一', + 12033 => '丨', + 12034 => '丶', + 12035 => '丿', + 12036 => '乙', + 12037 => '亅', + 12038 => '二', + 12039 => '亠', + 12040 => '人', + 12041 => '儿', + 12042 => '入', + 12043 => '八', + 12044 => '冂', + 12045 => '冖', + 12046 => '冫', + 12047 => '几', + 12048 => '凵', + 12049 => '刀', + 12050 => '力', + 12051 => '勹', + 12052 => '匕', + 12053 => '匚', + 12054 => '匸', + 12055 => '十', + 12056 => '卜', + 12057 => '卩', + 12058 => '厂', + 12059 => '厶', + 12060 => '又', + 12061 => '口', + 12062 => '囗', + 12063 => '土', + 12064 => '士', + 12065 => '夂', + 12066 => '夊', + 12067 => '夕', + 12068 => '大', + 12069 => '女', + 12070 => '子', + 12071 => '宀', + 12072 => '寸', + 12073 => '小', + 12074 => '尢', + 12075 => '尸', + 12076 => '屮', + 12077 => '山', + 12078 => '巛', + 12079 => '工', + 12080 => '己', + 12081 => '巾', + 12082 => '干', + 12083 => '幺', + 12084 => '广', + 12085 => '廴', + 12086 => '廾', + 12087 => '弋', + 12088 => '弓', + 12089 => '彐', + 12090 => '彡', + 12091 => '彳', + 12092 => '心', + 12093 => '戈', + 12094 => '戶', + 12095 => '手', + 12096 => '支', + 12097 => '攴', + 12098 => '文', + 12099 => '斗', + 12100 => '斤', + 12101 => '方', + 12102 => '无', + 12103 => '日', + 12104 => '曰', + 12105 => '月', + 12106 => '木', + 12107 => '欠', + 12108 => '止', + 12109 => '歹', + 12110 => '殳', + 12111 => '毋', + 12112 => '比', + 12113 => '毛', + 12114 => '氏', + 12115 => '气', + 12116 => '水', + 12117 => '火', + 12118 => '爪', + 12119 => '父', + 12120 => '爻', + 12121 => '爿', + 12122 => '片', + 12123 => '牙', + 12124 => '牛', + 12125 => '犬', + 12126 => '玄', + 12127 => '玉', + 12128 => '瓜', + 12129 => '瓦', + 12130 => '甘', + 12131 => '生', + 12132 => '用', + 12133 => '田', + 12134 => '疋', + 12135 => '疒', + 12136 => '癶', + 12137 => '白', + 12138 => '皮', + 12139 => '皿', + 12140 => '目', + 12141 => '矛', + 12142 => '矢', + 12143 => '石', + 12144 => '示', + 12145 => '禸', + 12146 => '禾', + 12147 => '穴', + 12148 => '立', + 12149 => '竹', + 12150 => '米', + 12151 => '糸', + 12152 => '缶', + 12153 => '网', + 12154 => '羊', + 12155 => '羽', + 12156 => '老', + 12157 => '而', + 12158 => '耒', + 12159 => '耳', + 12160 => '聿', + 12161 => '肉', + 12162 => '臣', + 12163 => '自', + 12164 => '至', + 12165 => '臼', + 12166 => '舌', + 12167 => '舛', + 12168 => '舟', + 12169 => '艮', + 12170 => '色', + 12171 => '艸', + 12172 => '虍', + 12173 => '虫', + 12174 => '血', + 12175 => '行', + 12176 => '衣', + 12177 => '襾', + 12178 => '見', + 12179 => '角', + 12180 => '言', + 12181 => '谷', + 12182 => '豆', + 12183 => '豕', + 12184 => '豸', + 12185 => '貝', + 12186 => '赤', + 12187 => '走', + 12188 => '足', + 12189 => '身', + 12190 => '車', + 12191 => '辛', + 12192 => '辰', + 12193 => '辵', + 12194 => '邑', + 12195 => '酉', + 12196 => '釆', + 12197 => '里', + 12198 => '金', + 12199 => '長', + 12200 => '門', + 12201 => '阜', + 12202 => '隶', + 12203 => '隹', + 12204 => '雨', + 12205 => '靑', + 12206 => '非', + 12207 => '面', + 12208 => '革', + 12209 => '韋', + 12210 => '韭', + 12211 => '音', + 12212 => '頁', + 12213 => '風', + 12214 => '飛', + 12215 => '食', + 12216 => '首', + 12217 => '香', + 12218 => '馬', + 12219 => '骨', + 12220 => '高', + 12221 => '髟', + 12222 => '鬥', + 12223 => '鬯', + 12224 => '鬲', + 12225 => '鬼', + 12226 => '魚', + 12227 => '鳥', + 12228 => '鹵', + 12229 => '鹿', + 12230 => '麥', + 12231 => '麻', + 12232 => '黃', + 12233 => '黍', + 12234 => '黑', + 12235 => '黹', + 12236 => '黽', + 12237 => '鼎', + 12238 => '鼓', + 12239 => '鼠', + 12240 => '鼻', + 12241 => '齊', + 12242 => '齒', + 12243 => '龍', + 12244 => '龜', + 12245 => '龠', + 12290 => '.', + 12342 => '〒', + 12344 => '十', + 12345 => '卄', + 12346 => '卅', + 12447 => 'より', + 12543 => 'コト', + 12593 => 'ᄀ', + 12594 => 'ᄁ', + 12595 => 'ᆪ', + 12596 => 'ᄂ', + 12597 => 'ᆬ', + 12598 => 'ᆭ', + 12599 => 'ᄃ', + 12600 => 'ᄄ', + 12601 => 'ᄅ', + 12602 => 'ᆰ', + 12603 => 'ᆱ', + 12604 => 'ᆲ', + 12605 => 'ᆳ', + 12606 => 'ᆴ', + 12607 => 'ᆵ', + 12608 => 'ᄚ', + 12609 => 'ᄆ', + 12610 => 'ᄇ', + 12611 => 'ᄈ', + 12612 => 'ᄡ', + 12613 => 'ᄉ', + 12614 => 'ᄊ', + 12615 => 'ᄋ', + 12616 => 'ᄌ', + 12617 => 'ᄍ', + 12618 => 'ᄎ', + 12619 => 'ᄏ', + 12620 => 'ᄐ', + 12621 => 'ᄑ', + 12622 => 'ᄒ', + 12623 => 'ᅡ', + 12624 => 'ᅢ', + 12625 => 'ᅣ', + 12626 => 'ᅤ', + 12627 => 'ᅥ', + 12628 => 'ᅦ', + 12629 => 'ᅧ', + 12630 => 'ᅨ', + 12631 => 'ᅩ', + 12632 => 'ᅪ', + 12633 => 'ᅫ', + 12634 => 'ᅬ', + 12635 => 'ᅭ', + 12636 => 'ᅮ', + 12637 => 'ᅯ', + 12638 => 'ᅰ', + 12639 => 'ᅱ', + 12640 => 'ᅲ', + 12641 => 'ᅳ', + 12642 => 'ᅴ', + 12643 => 'ᅵ', + 12645 => 'ᄔ', + 12646 => 'ᄕ', + 12647 => 'ᇇ', + 12648 => 'ᇈ', + 12649 => 'ᇌ', + 12650 => 'ᇎ', + 12651 => 'ᇓ', + 12652 => 'ᇗ', + 12653 => 'ᇙ', + 12654 => 'ᄜ', + 12655 => 'ᇝ', + 12656 => 'ᇟ', + 12657 => 'ᄝ', + 12658 => 'ᄞ', + 12659 => 'ᄠ', + 12660 => 'ᄢ', + 12661 => 'ᄣ', + 12662 => 'ᄧ', + 12663 => 'ᄩ', + 12664 => 'ᄫ', + 12665 => 'ᄬ', + 12666 => 'ᄭ', + 12667 => 'ᄮ', + 12668 => 'ᄯ', + 12669 => 'ᄲ', + 12670 => 'ᄶ', + 12671 => 'ᅀ', + 12672 => 'ᅇ', + 12673 => 'ᅌ', + 12674 => 'ᇱ', + 12675 => 'ᇲ', + 12676 => 'ᅗ', + 12677 => 'ᅘ', + 12678 => 'ᅙ', + 12679 => 'ᆄ', + 12680 => 'ᆅ', + 12681 => 'ᆈ', + 12682 => 'ᆑ', + 12683 => 'ᆒ', + 12684 => 'ᆔ', + 12685 => 'ᆞ', + 12686 => 'ᆡ', + 12690 => '一', + 12691 => '二', + 12692 => '三', + 12693 => '四', + 12694 => '上', + 12695 => '中', + 12696 => '下', + 12697 => '甲', + 12698 => '乙', + 12699 => '丙', + 12700 => '丁', + 12701 => '天', + 12702 => '地', + 12703 => '人', + 12868 => '問', + 12869 => '幼', + 12870 => '文', + 12871 => '箏', + 12880 => 'pte', + 12881 => '21', + 12882 => '22', + 12883 => '23', + 12884 => '24', + 12885 => '25', + 12886 => '26', + 12887 => '27', + 12888 => '28', + 12889 => '29', + 12890 => '30', + 12891 => '31', + 12892 => '32', + 12893 => '33', + 12894 => '34', + 12895 => '35', + 12896 => 'ᄀ', + 12897 => 'ᄂ', + 12898 => 'ᄃ', + 12899 => 'ᄅ', + 12900 => 'ᄆ', + 12901 => 'ᄇ', + 12902 => 'ᄉ', + 12903 => 'ᄋ', + 12904 => 'ᄌ', + 12905 => 'ᄎ', + 12906 => 'ᄏ', + 12907 => 'ᄐ', + 12908 => 'ᄑ', + 12909 => 'ᄒ', + 12910 => '가', + 12911 => '나', + 12912 => '다', + 12913 => '라', + 12914 => '마', + 12915 => '바', + 12916 => '사', + 12917 => '아', + 12918 => '자', + 12919 => '차', + 12920 => '카', + 12921 => '타', + 12922 => '파', + 12923 => '하', + 12924 => '참고', + 12925 => '주의', + 12926 => '우', + 12928 => '一', + 12929 => '二', + 12930 => '三', + 12931 => '四', + 12932 => '五', + 12933 => '六', + 12934 => '七', + 12935 => '八', + 12936 => '九', + 12937 => '十', + 12938 => '月', + 12939 => '火', + 12940 => '水', + 12941 => '木', + 12942 => '金', + 12943 => '土', + 12944 => '日', + 12945 => '株', + 12946 => '有', + 12947 => '社', + 12948 => '名', + 12949 => '特', + 12950 => '財', + 12951 => '祝', + 12952 => '労', + 12953 => '秘', + 12954 => '男', + 12955 => '女', + 12956 => '適', + 12957 => '優', + 12958 => '印', + 12959 => '注', + 12960 => '項', + 12961 => '休', + 12962 => '写', + 12963 => '正', + 12964 => '上', + 12965 => '中', + 12966 => '下', + 12967 => '左', + 12968 => '右', + 12969 => '医', + 12970 => '宗', + 12971 => '学', + 12972 => '監', + 12973 => '企', + 12974 => '資', + 12975 => '協', + 12976 => '夜', + 12977 => '36', + 12978 => '37', + 12979 => '38', + 12980 => '39', + 12981 => '40', + 12982 => '41', + 12983 => '42', + 12984 => '43', + 12985 => '44', + 12986 => '45', + 12987 => '46', + 12988 => '47', + 12989 => '48', + 12990 => '49', + 12991 => '50', + 12992 => '1月', + 12993 => '2月', + 12994 => '3月', + 12995 => '4月', + 12996 => '5月', + 12997 => '6月', + 12998 => '7月', + 12999 => '8月', + 13000 => '9月', + 13001 => '10月', + 13002 => '11月', + 13003 => '12月', + 13004 => 'hg', + 13005 => 'erg', + 13006 => 'ev', + 13007 => 'ltd', + 13008 => 'ア', + 13009 => 'イ', + 13010 => 'ウ', + 13011 => 'エ', + 13012 => 'オ', + 13013 => 'カ', + 13014 => 'キ', + 13015 => 'ク', + 13016 => 'ケ', + 13017 => 'コ', + 13018 => 'サ', + 13019 => 'シ', + 13020 => 'ス', + 13021 => 'セ', + 13022 => 'ソ', + 13023 => 'タ', + 13024 => 'チ', + 13025 => 'ツ', + 13026 => 'テ', + 13027 => 'ト', + 13028 => 'ナ', + 13029 => 'ニ', + 13030 => 'ヌ', + 13031 => 'ネ', + 13032 => 'ノ', + 13033 => 'ハ', + 13034 => 'ヒ', + 13035 => 'フ', + 13036 => 'ヘ', + 13037 => 'ホ', + 13038 => 'マ', + 13039 => 'ミ', + 13040 => 'ム', + 13041 => 'メ', + 13042 => 'モ', + 13043 => 'ヤ', + 13044 => 'ユ', + 13045 => 'ヨ', + 13046 => 'ラ', + 13047 => 'リ', + 13048 => 'ル', + 13049 => 'レ', + 13050 => 'ロ', + 13051 => 'ワ', + 13052 => 'ヰ', + 13053 => 'ヱ', + 13054 => 'ヲ', + 13055 => '令和', + 13056 => 'アパート', + 13057 => 'アルファ', + 13058 => 'アンペア', + 13059 => 'アール', + 13060 => 'イニング', + 13061 => 'インチ', + 13062 => 'ウォン', + 13063 => 'エスクード', + 13064 => 'エーカー', + 13065 => 'オンス', + 13066 => 'オーム', + 13067 => 'カイリ', + 13068 => 'カラット', + 13069 => 'カロリー', + 13070 => 'ガロン', + 13071 => 'ガンマ', + 13072 => 'ギガ', + 13073 => 'ギニー', + 13074 => 'キュリー', + 13075 => 'ギルダー', + 13076 => 'キロ', + 13077 => 'キログラム', + 13078 => 'キロメートル', + 13079 => 'キロワット', + 13080 => 'グラム', + 13081 => 'グラムトン', + 13082 => 'クルゼイロ', + 13083 => 'クローネ', + 13084 => 'ケース', + 13085 => 'コルナ', + 13086 => 'コーポ', + 13087 => 'サイクル', + 13088 => 'サンチーム', + 13089 => 'シリング', + 13090 => 'センチ', + 13091 => 'セント', + 13092 => 'ダース', + 13093 => 'デシ', + 13094 => 'ドル', + 13095 => 'トン', + 13096 => 'ナノ', + 13097 => 'ノット', + 13098 => 'ハイツ', + 13099 => 'パーセント', + 13100 => 'パーツ', + 13101 => 'バーレル', + 13102 => 'ピアストル', + 13103 => 'ピクル', + 13104 => 'ピコ', + 13105 => 'ビル', + 13106 => 'ファラッド', + 13107 => 'フィート', + 13108 => 'ブッシェル', + 13109 => 'フラン', + 13110 => 'ヘクタール', + 13111 => 'ペソ', + 13112 => 'ペニヒ', + 13113 => 'ヘルツ', + 13114 => 'ペンス', + 13115 => 'ページ', + 13116 => 'ベータ', + 13117 => 'ポイント', + 13118 => 'ボルト', + 13119 => 'ホン', + 13120 => 'ポンド', + 13121 => 'ホール', + 13122 => 'ホーン', + 13123 => 'マイクロ', + 13124 => 'マイル', + 13125 => 'マッハ', + 13126 => 'マルク', + 13127 => 'マンション', + 13128 => 'ミクロン', + 13129 => 'ミリ', + 13130 => 'ミリバール', + 13131 => 'メガ', + 13132 => 'メガトン', + 13133 => 'メートル', + 13134 => 'ヤード', + 13135 => 'ヤール', + 13136 => 'ユアン', + 13137 => 'リットル', + 13138 => 'リラ', + 13139 => 'ルピー', + 13140 => 'ルーブル', + 13141 => 'レム', + 13142 => 'レントゲン', + 13143 => 'ワット', + 13144 => '0点', + 13145 => '1点', + 13146 => '2点', + 13147 => '3点', + 13148 => '4点', + 13149 => '5点', + 13150 => '6点', + 13151 => '7点', + 13152 => '8点', + 13153 => '9点', + 13154 => '10点', + 13155 => '11点', + 13156 => '12点', + 13157 => '13点', + 13158 => '14点', + 13159 => '15点', + 13160 => '16点', + 13161 => '17点', + 13162 => '18点', + 13163 => '19点', + 13164 => '20点', + 13165 => '21点', + 13166 => '22点', + 13167 => '23点', + 13168 => '24点', + 13169 => 'hpa', + 13170 => 'da', + 13171 => 'au', + 13172 => 'bar', + 13173 => 'ov', + 13174 => 'pc', + 13175 => 'dm', + 13176 => 'dm2', + 13177 => 'dm3', + 13178 => 'iu', + 13179 => '平成', + 13180 => '昭和', + 13181 => '大正', + 13182 => '明治', + 13183 => '株式会社', + 13184 => 'pa', + 13185 => 'na', + 13186 => 'μa', + 13187 => 'ma', + 13188 => 'ka', + 13189 => 'kb', + 13190 => 'mb', + 13191 => 'gb', + 13192 => 'cal', + 13193 => 'kcal', + 13194 => 'pf', + 13195 => 'nf', + 13196 => 'μf', + 13197 => 'μg', + 13198 => 'mg', + 13199 => 'kg', + 13200 => 'hz', + 13201 => 'khz', + 13202 => 'mhz', + 13203 => 'ghz', + 13204 => 'thz', + 13205 => 'μl', + 13206 => 'ml', + 13207 => 'dl', + 13208 => 'kl', + 13209 => 'fm', + 13210 => 'nm', + 13211 => 'μm', + 13212 => 'mm', + 13213 => 'cm', + 13214 => 'km', + 13215 => 'mm2', + 13216 => 'cm2', + 13217 => 'm2', + 13218 => 'km2', + 13219 => 'mm3', + 13220 => 'cm3', + 13221 => 'm3', + 13222 => 'km3', + 13223 => 'm∕s', + 13224 => 'm∕s2', + 13225 => 'pa', + 13226 => 'kpa', + 13227 => 'mpa', + 13228 => 'gpa', + 13229 => 'rad', + 13230 => 'rad∕s', + 13231 => 'rad∕s2', + 13232 => 'ps', + 13233 => 'ns', + 13234 => 'μs', + 13235 => 'ms', + 13236 => 'pv', + 13237 => 'nv', + 13238 => 'μv', + 13239 => 'mv', + 13240 => 'kv', + 13241 => 'mv', + 13242 => 'pw', + 13243 => 'nw', + 13244 => 'μw', + 13245 => 'mw', + 13246 => 'kw', + 13247 => 'mw', + 13248 => 'kω', + 13249 => 'mω', + 13251 => 'bq', + 13252 => 'cc', + 13253 => 'cd', + 13254 => 'c∕kg', + 13256 => 'db', + 13257 => 'gy', + 13258 => 'ha', + 13259 => 'hp', + 13260 => 'in', + 13261 => 'kk', + 13262 => 'km', + 13263 => 'kt', + 13264 => 'lm', + 13265 => 'ln', + 13266 => 'log', + 13267 => 'lx', + 13268 => 'mb', + 13269 => 'mil', + 13270 => 'mol', + 13271 => 'ph', + 13273 => 'ppm', + 13274 => 'pr', + 13275 => 'sr', + 13276 => 'sv', + 13277 => 'wb', + 13278 => 'v∕m', + 13279 => 'a∕m', + 13280 => '1日', + 13281 => '2日', + 13282 => '3日', + 13283 => '4日', + 13284 => '5日', + 13285 => '6日', + 13286 => '7日', + 13287 => '8日', + 13288 => '9日', + 13289 => '10日', + 13290 => '11日', + 13291 => '12日', + 13292 => '13日', + 13293 => '14日', + 13294 => '15日', + 13295 => '16日', + 13296 => '17日', + 13297 => '18日', + 13298 => '19日', + 13299 => '20日', + 13300 => '21日', + 13301 => '22日', + 13302 => '23日', + 13303 => '24日', + 13304 => '25日', + 13305 => '26日', + 13306 => '27日', + 13307 => '28日', + 13308 => '29日', + 13309 => '30日', + 13310 => '31日', + 13311 => 'gal', + 42560 => 'ꙁ', + 42562 => 'ꙃ', + 42564 => 'ꙅ', + 42566 => 'ꙇ', + 42568 => 'ꙉ', + 42570 => 'ꙋ', + 42572 => 'ꙍ', + 42574 => 'ꙏ', + 42576 => 'ꙑ', + 42578 => 'ꙓ', + 42580 => 'ꙕ', + 42582 => 'ꙗ', + 42584 => 'ꙙ', + 42586 => 'ꙛ', + 42588 => 'ꙝ', + 42590 => 'ꙟ', + 42592 => 'ꙡ', + 42594 => 'ꙣ', + 42596 => 'ꙥ', + 42598 => 'ꙧ', + 42600 => 'ꙩ', + 42602 => 'ꙫ', + 42604 => 'ꙭ', + 42624 => 'ꚁ', + 42626 => 'ꚃ', + 42628 => 'ꚅ', + 42630 => 'ꚇ', + 42632 => 'ꚉ', + 42634 => 'ꚋ', + 42636 => 'ꚍ', + 42638 => 'ꚏ', + 42640 => 'ꚑ', + 42642 => 'ꚓ', + 42644 => 'ꚕ', + 42646 => 'ꚗ', + 42648 => 'ꚙ', + 42650 => 'ꚛ', + 42652 => 'ъ', + 42653 => 'ь', + 42786 => 'ꜣ', + 42788 => 'ꜥ', + 42790 => 'ꜧ', + 42792 => 'ꜩ', + 42794 => 'ꜫ', + 42796 => 'ꜭ', + 42798 => 'ꜯ', + 42802 => 'ꜳ', + 42804 => 'ꜵ', + 42806 => 'ꜷ', + 42808 => 'ꜹ', + 42810 => 'ꜻ', + 42812 => 'ꜽ', + 42814 => 'ꜿ', + 42816 => 'ꝁ', + 42818 => 'ꝃ', + 42820 => 'ꝅ', + 42822 => 'ꝇ', + 42824 => 'ꝉ', + 42826 => 'ꝋ', + 42828 => 'ꝍ', + 42830 => 'ꝏ', + 42832 => 'ꝑ', + 42834 => 'ꝓ', + 42836 => 'ꝕ', + 42838 => 'ꝗ', + 42840 => 'ꝙ', + 42842 => 'ꝛ', + 42844 => 'ꝝ', + 42846 => 'ꝟ', + 42848 => 'ꝡ', + 42850 => 'ꝣ', + 42852 => 'ꝥ', + 42854 => 'ꝧ', + 42856 => 'ꝩ', + 42858 => 'ꝫ', + 42860 => 'ꝭ', + 42862 => 'ꝯ', + 42864 => 'ꝯ', + 42873 => 'ꝺ', + 42875 => 'ꝼ', + 42877 => 'ᵹ', + 42878 => 'ꝿ', + 42880 => 'ꞁ', + 42882 => 'ꞃ', + 42884 => 'ꞅ', + 42886 => 'ꞇ', + 42891 => 'ꞌ', + 42893 => 'ɥ', + 42896 => 'ꞑ', + 42898 => 'ꞓ', + 42902 => 'ꞗ', + 42904 => 'ꞙ', + 42906 => 'ꞛ', + 42908 => 'ꞝ', + 42910 => 'ꞟ', + 42912 => 'ꞡ', + 42914 => 'ꞣ', + 42916 => 'ꞥ', + 42918 => 'ꞧ', + 42920 => 'ꞩ', + 42922 => 'ɦ', + 42923 => 'ɜ', + 42924 => 'ɡ', + 42925 => 'ɬ', + 42926 => 'ɪ', + 42928 => 'ʞ', + 42929 => 'ʇ', + 42930 => 'ʝ', + 42931 => 'ꭓ', + 42932 => 'ꞵ', + 42934 => 'ꞷ', + 42936 => 'ꞹ', + 42938 => 'ꞻ', + 42940 => 'ꞽ', + 42942 => 'ꞿ', + 42946 => 'ꟃ', + 42948 => 'ꞔ', + 42949 => 'ʂ', + 42950 => 'ᶎ', + 42951 => 'ꟈ', + 42953 => 'ꟊ', + 42997 => 'ꟶ', + 43000 => 'ħ', + 43001 => 'œ', + 43868 => 'ꜧ', + 43869 => 'ꬷ', + 43870 => 'ɫ', + 43871 => 'ꭒ', + 43881 => 'ʍ', + 43888 => 'Ꭰ', + 43889 => 'Ꭱ', + 43890 => 'Ꭲ', + 43891 => 'Ꭳ', + 43892 => 'Ꭴ', + 43893 => 'Ꭵ', + 43894 => 'Ꭶ', + 43895 => 'Ꭷ', + 43896 => 'Ꭸ', + 43897 => 'Ꭹ', + 43898 => 'Ꭺ', + 43899 => 'Ꭻ', + 43900 => 'Ꭼ', + 43901 => 'Ꭽ', + 43902 => 'Ꭾ', + 43903 => 'Ꭿ', + 43904 => 'Ꮀ', + 43905 => 'Ꮁ', + 43906 => 'Ꮂ', + 43907 => 'Ꮃ', + 43908 => 'Ꮄ', + 43909 => 'Ꮅ', + 43910 => 'Ꮆ', + 43911 => 'Ꮇ', + 43912 => 'Ꮈ', + 43913 => 'Ꮉ', + 43914 => 'Ꮊ', + 43915 => 'Ꮋ', + 43916 => 'Ꮌ', + 43917 => 'Ꮍ', + 43918 => 'Ꮎ', + 43919 => 'Ꮏ', + 43920 => 'Ꮐ', + 43921 => 'Ꮑ', + 43922 => 'Ꮒ', + 43923 => 'Ꮓ', + 43924 => 'Ꮔ', + 43925 => 'Ꮕ', + 43926 => 'Ꮖ', + 43927 => 'Ꮗ', + 43928 => 'Ꮘ', + 43929 => 'Ꮙ', + 43930 => 'Ꮚ', + 43931 => 'Ꮛ', + 43932 => 'Ꮜ', + 43933 => 'Ꮝ', + 43934 => 'Ꮞ', + 43935 => 'Ꮟ', + 43936 => 'Ꮠ', + 43937 => 'Ꮡ', + 43938 => 'Ꮢ', + 43939 => 'Ꮣ', + 43940 => 'Ꮤ', + 43941 => 'Ꮥ', + 43942 => 'Ꮦ', + 43943 => 'Ꮧ', + 43944 => 'Ꮨ', + 43945 => 'Ꮩ', + 43946 => 'Ꮪ', + 43947 => 'Ꮫ', + 43948 => 'Ꮬ', + 43949 => 'Ꮭ', + 43950 => 'Ꮮ', + 43951 => 'Ꮯ', + 43952 => 'Ꮰ', + 43953 => 'Ꮱ', + 43954 => 'Ꮲ', + 43955 => 'Ꮳ', + 43956 => 'Ꮴ', + 43957 => 'Ꮵ', + 43958 => 'Ꮶ', + 43959 => 'Ꮷ', + 43960 => 'Ꮸ', + 43961 => 'Ꮹ', + 43962 => 'Ꮺ', + 43963 => 'Ꮻ', + 43964 => 'Ꮼ', + 43965 => 'Ꮽ', + 43966 => 'Ꮾ', + 43967 => 'Ꮿ', + 63744 => '豈', + 63745 => '更', + 63746 => '車', + 63747 => '賈', + 63748 => '滑', + 63749 => '串', + 63750 => '句', + 63751 => '龜', + 63752 => '龜', + 63753 => '契', + 63754 => '金', + 63755 => '喇', + 63756 => '奈', + 63757 => '懶', + 63758 => '癩', + 63759 => '羅', + 63760 => '蘿', + 63761 => '螺', + 63762 => '裸', + 63763 => '邏', + 63764 => '樂', + 63765 => '洛', + 63766 => '烙', + 63767 => '珞', + 63768 => '落', + 63769 => '酪', + 63770 => '駱', + 63771 => '亂', + 63772 => '卵', + 63773 => '欄', + 63774 => '爛', + 63775 => '蘭', + 63776 => '鸞', + 63777 => '嵐', + 63778 => '濫', + 63779 => '藍', + 63780 => '襤', + 63781 => '拉', + 63782 => '臘', + 63783 => '蠟', + 63784 => '廊', + 63785 => '朗', + 63786 => '浪', + 63787 => '狼', + 63788 => '郎', + 63789 => '來', + 63790 => '冷', + 63791 => '勞', + 63792 => '擄', + 63793 => '櫓', + 63794 => '爐', + 63795 => '盧', + 63796 => '老', + 63797 => '蘆', + 63798 => '虜', + 63799 => '路', + 63800 => '露', + 63801 => '魯', + 63802 => '鷺', + 63803 => '碌', + 63804 => '祿', + 63805 => '綠', + 63806 => '菉', + 63807 => '錄', + 63808 => '鹿', + 63809 => '論', + 63810 => '壟', + 63811 => '弄', + 63812 => '籠', + 63813 => '聾', + 63814 => '牢', + 63815 => '磊', + 63816 => '賂', + 63817 => '雷', + 63818 => '壘', + 63819 => '屢', + 63820 => '樓', + 63821 => '淚', + 63822 => '漏', + 63823 => '累', + 63824 => '縷', + 63825 => '陋', + 63826 => '勒', + 63827 => '肋', + 63828 => '凜', + 63829 => '凌', + 63830 => '稜', + 63831 => '綾', + 63832 => '菱', + 63833 => '陵', + 63834 => '讀', + 63835 => '拏', + 63836 => '樂', + 63837 => '諾', + 63838 => '丹', + 63839 => '寧', + 63840 => '怒', + 63841 => '率', + 63842 => '異', + 63843 => '北', + 63844 => '磻', + 63845 => '便', + 63846 => '復', + 63847 => '不', + 63848 => '泌', + 63849 => '數', + 63850 => '索', + 63851 => '參', + 63852 => '塞', + 63853 => '省', + 63854 => '葉', + 63855 => '說', + 63856 => '殺', + 63857 => '辰', + 63858 => '沈', + 63859 => '拾', + 63860 => '若', + 63861 => '掠', + 63862 => '略', + 63863 => '亮', + 63864 => '兩', + 63865 => '凉', + 63866 => '梁', + 63867 => '糧', + 63868 => '良', + 63869 => '諒', + 63870 => '量', + 63871 => '勵', + 63872 => '呂', + 63873 => '女', + 63874 => '廬', + 63875 => '旅', + 63876 => '濾', + 63877 => '礪', + 63878 => '閭', + 63879 => '驪', + 63880 => '麗', + 63881 => '黎', + 63882 => '力', + 63883 => '曆', + 63884 => '歷', + 63885 => '轢', + 63886 => '年', + 63887 => '憐', + 63888 => '戀', + 63889 => '撚', + 63890 => '漣', + 63891 => '煉', + 63892 => '璉', + 63893 => '秊', + 63894 => '練', + 63895 => '聯', + 63896 => '輦', + 63897 => '蓮', + 63898 => '連', + 63899 => '鍊', + 63900 => '列', + 63901 => '劣', + 63902 => '咽', + 63903 => '烈', + 63904 => '裂', + 63905 => '說', + 63906 => '廉', + 63907 => '念', + 63908 => '捻', + 63909 => '殮', + 63910 => '簾', + 63911 => '獵', + 63912 => '令', + 63913 => '囹', + 63914 => '寧', + 63915 => '嶺', + 63916 => '怜', + 63917 => '玲', + 63918 => '瑩', + 63919 => '羚', + 63920 => '聆', + 63921 => '鈴', + 63922 => '零', + 63923 => '靈', + 63924 => '領', + 63925 => '例', + 63926 => '禮', + 63927 => '醴', + 63928 => '隸', + 63929 => '惡', + 63930 => '了', + 63931 => '僚', + 63932 => '寮', + 63933 => '尿', + 63934 => '料', + 63935 => '樂', + 63936 => '燎', + 63937 => '療', + 63938 => '蓼', + 63939 => '遼', + 63940 => '龍', + 63941 => '暈', + 63942 => '阮', + 63943 => '劉', + 63944 => '杻', + 63945 => '柳', + 63946 => '流', + 63947 => '溜', + 63948 => '琉', + 63949 => '留', + 63950 => '硫', + 63951 => '紐', + 63952 => '類', + 63953 => '六', + 63954 => '戮', + 63955 => '陸', + 63956 => '倫', + 63957 => '崙', + 63958 => '淪', + 63959 => '輪', + 63960 => '律', + 63961 => '慄', + 63962 => '栗', + 63963 => '率', + 63964 => '隆', + 63965 => '利', + 63966 => '吏', + 63967 => '履', + 63968 => '易', + 63969 => '李', + 63970 => '梨', + 63971 => '泥', + 63972 => '理', + 63973 => '痢', + 63974 => '罹', + 63975 => '裏', + 63976 => '裡', + 63977 => '里', + 63978 => '離', + 63979 => '匿', + 63980 => '溺', + 63981 => '吝', + 63982 => '燐', + 63983 => '璘', + 63984 => '藺', + 63985 => '隣', + 63986 => '鱗', + 63987 => '麟', + 63988 => '林', + 63989 => '淋', + 63990 => '臨', + 63991 => '立', + 63992 => '笠', + 63993 => '粒', + 63994 => '狀', + 63995 => '炙', + 63996 => '識', + 63997 => '什', + 63998 => '茶', + 63999 => '刺', + 64000 => '切', + 64001 => '度', + 64002 => '拓', + 64003 => '糖', + 64004 => '宅', + 64005 => '洞', + 64006 => '暴', + 64007 => '輻', + 64008 => '行', + 64009 => '降', + 64010 => '見', + 64011 => '廓', + 64012 => '兀', + 64013 => '嗀', + 64016 => '塚', + 64018 => '晴', + 64021 => '凞', + 64022 => '猪', + 64023 => '益', + 64024 => '礼', + 64025 => '神', + 64026 => '祥', + 64027 => '福', + 64028 => '靖', + 64029 => '精', + 64030 => '羽', + 64032 => '蘒', + 64034 => '諸', + 64037 => '逸', + 64038 => '都', + 64042 => '飯', + 64043 => '飼', + 64044 => '館', + 64045 => '鶴', + 64046 => '郞', + 64047 => '隷', + 64048 => '侮', + 64049 => '僧', + 64050 => '免', + 64051 => '勉', + 64052 => '勤', + 64053 => '卑', + 64054 => '喝', + 64055 => '嘆', + 64056 => '器', + 64057 => '塀', + 64058 => '墨', + 64059 => '層', + 64060 => '屮', + 64061 => '悔', + 64062 => '慨', + 64063 => '憎', + 64064 => '懲', + 64065 => '敏', + 64066 => '既', + 64067 => '暑', + 64068 => '梅', + 64069 => '海', + 64070 => '渚', + 64071 => '漢', + 64072 => '煮', + 64073 => '爫', + 64074 => '琢', + 64075 => '碑', + 64076 => '社', + 64077 => '祉', + 64078 => '祈', + 64079 => '祐', + 64080 => '祖', + 64081 => '祝', + 64082 => '禍', + 64083 => '禎', + 64084 => '穀', + 64085 => '突', + 64086 => '節', + 64087 => '練', + 64088 => '縉', + 64089 => '繁', + 64090 => '署', + 64091 => '者', + 64092 => '臭', + 64093 => '艹', + 64094 => '艹', + 64095 => '著', + 64096 => '褐', + 64097 => '視', + 64098 => '謁', + 64099 => '謹', + 64100 => '賓', + 64101 => '贈', + 64102 => '辶', + 64103 => '逸', + 64104 => '難', + 64105 => '響', + 64106 => '頻', + 64107 => '恵', + 64108 => '𤋮', + 64109 => '舘', + 64112 => '並', + 64113 => '况', + 64114 => '全', + 64115 => '侀', + 64116 => '充', + 64117 => '冀', + 64118 => '勇', + 64119 => '勺', + 64120 => '喝', + 64121 => '啕', + 64122 => '喙', + 64123 => '嗢', + 64124 => '塚', + 64125 => '墳', + 64126 => '奄', + 64127 => '奔', + 64128 => '婢', + 64129 => '嬨', + 64130 => '廒', + 64131 => '廙', + 64132 => '彩', + 64133 => '徭', + 64134 => '惘', + 64135 => '慎', + 64136 => '愈', + 64137 => '憎', + 64138 => '慠', + 64139 => '懲', + 64140 => '戴', + 64141 => '揄', + 64142 => '搜', + 64143 => '摒', + 64144 => '敖', + 64145 => '晴', + 64146 => '朗', + 64147 => '望', + 64148 => '杖', + 64149 => '歹', + 64150 => '殺', + 64151 => '流', + 64152 => '滛', + 64153 => '滋', + 64154 => '漢', + 64155 => '瀞', + 64156 => '煮', + 64157 => '瞧', + 64158 => '爵', + 64159 => '犯', + 64160 => '猪', + 64161 => '瑱', + 64162 => '甆', + 64163 => '画', + 64164 => '瘝', + 64165 => '瘟', + 64166 => '益', + 64167 => '盛', + 64168 => '直', + 64169 => '睊', + 64170 => '着', + 64171 => '磌', + 64172 => '窱', + 64173 => '節', + 64174 => '类', + 64175 => '絛', + 64176 => '練', + 64177 => '缾', + 64178 => '者', + 64179 => '荒', + 64180 => '華', + 64181 => '蝹', + 64182 => '襁', + 64183 => '覆', + 64184 => '視', + 64185 => '調', + 64186 => '諸', + 64187 => '請', + 64188 => '謁', + 64189 => '諾', + 64190 => '諭', + 64191 => '謹', + 64192 => '變', + 64193 => '贈', + 64194 => '輸', + 64195 => '遲', + 64196 => '醙', + 64197 => '鉶', + 64198 => '陼', + 64199 => '難', + 64200 => '靖', + 64201 => '韛', + 64202 => '響', + 64203 => '頋', + 64204 => '頻', + 64205 => '鬒', + 64206 => '龜', + 64207 => '𢡊', + 64208 => '𢡄', + 64209 => '𣏕', + 64210 => '㮝', + 64211 => '䀘', + 64212 => '䀹', + 64213 => '𥉉', + 64214 => '𥳐', + 64215 => '𧻓', + 64216 => '齃', + 64217 => '龎', + 64256 => 'ff', + 64257 => 'fi', + 64258 => 'fl', + 64259 => 'ffi', + 64260 => 'ffl', + 64261 => 'st', + 64262 => 'st', + 64275 => 'մն', + 64276 => 'մե', + 64277 => 'մի', + 64278 => 'վն', + 64279 => 'մխ', + 64285 => 'יִ', + 64287 => 'ײַ', + 64288 => 'ע', + 64289 => 'א', + 64290 => 'ד', + 64291 => 'ה', + 64292 => 'כ', + 64293 => 'ל', + 64294 => 'ם', + 64295 => 'ר', + 64296 => 'ת', + 64298 => 'שׁ', + 64299 => 'שׂ', + 64300 => 'שּׁ', + 64301 => 'שּׂ', + 64302 => 'אַ', + 64303 => 'אָ', + 64304 => 'אּ', + 64305 => 'בּ', + 64306 => 'גּ', + 64307 => 'דּ', + 64308 => 'הּ', + 64309 => 'וּ', + 64310 => 'זּ', + 64312 => 'טּ', + 64313 => 'יּ', + 64314 => 'ךּ', + 64315 => 'כּ', + 64316 => 'לּ', + 64318 => 'מּ', + 64320 => 'נּ', + 64321 => 'סּ', + 64323 => 'ףּ', + 64324 => 'פּ', + 64326 => 'צּ', + 64327 => 'קּ', + 64328 => 'רּ', + 64329 => 'שּ', + 64330 => 'תּ', + 64331 => 'וֹ', + 64332 => 'בֿ', + 64333 => 'כֿ', + 64334 => 'פֿ', + 64335 => 'אל', + 64336 => 'ٱ', + 64337 => 'ٱ', + 64338 => 'ٻ', + 64339 => 'ٻ', + 64340 => 'ٻ', + 64341 => 'ٻ', + 64342 => 'پ', + 64343 => 'پ', + 64344 => 'پ', + 64345 => 'پ', + 64346 => 'ڀ', + 64347 => 'ڀ', + 64348 => 'ڀ', + 64349 => 'ڀ', + 64350 => 'ٺ', + 64351 => 'ٺ', + 64352 => 'ٺ', + 64353 => 'ٺ', + 64354 => 'ٿ', + 64355 => 'ٿ', + 64356 => 'ٿ', + 64357 => 'ٿ', + 64358 => 'ٹ', + 64359 => 'ٹ', + 64360 => 'ٹ', + 64361 => 'ٹ', + 64362 => 'ڤ', + 64363 => 'ڤ', + 64364 => 'ڤ', + 64365 => 'ڤ', + 64366 => 'ڦ', + 64367 => 'ڦ', + 64368 => 'ڦ', + 64369 => 'ڦ', + 64370 => 'ڄ', + 64371 => 'ڄ', + 64372 => 'ڄ', + 64373 => 'ڄ', + 64374 => 'ڃ', + 64375 => 'ڃ', + 64376 => 'ڃ', + 64377 => 'ڃ', + 64378 => 'چ', + 64379 => 'چ', + 64380 => 'چ', + 64381 => 'چ', + 64382 => 'ڇ', + 64383 => 'ڇ', + 64384 => 'ڇ', + 64385 => 'ڇ', + 64386 => 'ڍ', + 64387 => 'ڍ', + 64388 => 'ڌ', + 64389 => 'ڌ', + 64390 => 'ڎ', + 64391 => 'ڎ', + 64392 => 'ڈ', + 64393 => 'ڈ', + 64394 => 'ژ', + 64395 => 'ژ', + 64396 => 'ڑ', + 64397 => 'ڑ', + 64398 => 'ک', + 64399 => 'ک', + 64400 => 'ک', + 64401 => 'ک', + 64402 => 'گ', + 64403 => 'گ', + 64404 => 'گ', + 64405 => 'گ', + 64406 => 'ڳ', + 64407 => 'ڳ', + 64408 => 'ڳ', + 64409 => 'ڳ', + 64410 => 'ڱ', + 64411 => 'ڱ', + 64412 => 'ڱ', + 64413 => 'ڱ', + 64414 => 'ں', + 64415 => 'ں', + 64416 => 'ڻ', + 64417 => 'ڻ', + 64418 => 'ڻ', + 64419 => 'ڻ', + 64420 => 'ۀ', + 64421 => 'ۀ', + 64422 => 'ہ', + 64423 => 'ہ', + 64424 => 'ہ', + 64425 => 'ہ', + 64426 => 'ھ', + 64427 => 'ھ', + 64428 => 'ھ', + 64429 => 'ھ', + 64430 => 'ے', + 64431 => 'ے', + 64432 => 'ۓ', + 64433 => 'ۓ', + 64467 => 'ڭ', + 64468 => 'ڭ', + 64469 => 'ڭ', + 64470 => 'ڭ', + 64471 => 'ۇ', + 64472 => 'ۇ', + 64473 => 'ۆ', + 64474 => 'ۆ', + 64475 => 'ۈ', + 64476 => 'ۈ', + 64477 => 'ۇٴ', + 64478 => 'ۋ', + 64479 => 'ۋ', + 64480 => 'ۅ', + 64481 => 'ۅ', + 64482 => 'ۉ', + 64483 => 'ۉ', + 64484 => 'ې', + 64485 => 'ې', + 64486 => 'ې', + 64487 => 'ې', + 64488 => 'ى', + 64489 => 'ى', + 64490 => 'ئا', + 64491 => 'ئا', + 64492 => 'ئە', + 64493 => 'ئە', + 64494 => 'ئو', + 64495 => 'ئو', + 64496 => 'ئۇ', + 64497 => 'ئۇ', + 64498 => 'ئۆ', + 64499 => 'ئۆ', + 64500 => 'ئۈ', + 64501 => 'ئۈ', + 64502 => 'ئې', + 64503 => 'ئې', + 64504 => 'ئې', + 64505 => 'ئى', + 64506 => 'ئى', + 64507 => 'ئى', + 64508 => 'ی', + 64509 => 'ی', + 64510 => 'ی', + 64511 => 'ی', + 64512 => 'ئج', + 64513 => 'ئح', + 64514 => 'ئم', + 64515 => 'ئى', + 64516 => 'ئي', + 64517 => 'بج', + 64518 => 'بح', + 64519 => 'بخ', + 64520 => 'بم', + 64521 => 'بى', + 64522 => 'بي', + 64523 => 'تج', + 64524 => 'تح', + 64525 => 'تخ', + 64526 => 'تم', + 64527 => 'تى', + 64528 => 'تي', + 64529 => 'ثج', + 64530 => 'ثم', + 64531 => 'ثى', + 64532 => 'ثي', + 64533 => 'جح', + 64534 => 'جم', + 64535 => 'حج', + 64536 => 'حم', + 64537 => 'خج', + 64538 => 'خح', + 64539 => 'خم', + 64540 => 'سج', + 64541 => 'سح', + 64542 => 'سخ', + 64543 => 'سم', + 64544 => 'صح', + 64545 => 'صم', + 64546 => 'ضج', + 64547 => 'ضح', + 64548 => 'ضخ', + 64549 => 'ضم', + 64550 => 'طح', + 64551 => 'طم', + 64552 => 'ظم', + 64553 => 'عج', + 64554 => 'عم', + 64555 => 'غج', + 64556 => 'غم', + 64557 => 'فج', + 64558 => 'فح', + 64559 => 'فخ', + 64560 => 'فم', + 64561 => 'فى', + 64562 => 'في', + 64563 => 'قح', + 64564 => 'قم', + 64565 => 'قى', + 64566 => 'قي', + 64567 => 'كا', + 64568 => 'كج', + 64569 => 'كح', + 64570 => 'كخ', + 64571 => 'كل', + 64572 => 'كم', + 64573 => 'كى', + 64574 => 'كي', + 64575 => 'لج', + 64576 => 'لح', + 64577 => 'لخ', + 64578 => 'لم', + 64579 => 'لى', + 64580 => 'لي', + 64581 => 'مج', + 64582 => 'مح', + 64583 => 'مخ', + 64584 => 'مم', + 64585 => 'مى', + 64586 => 'مي', + 64587 => 'نج', + 64588 => 'نح', + 64589 => 'نخ', + 64590 => 'نم', + 64591 => 'نى', + 64592 => 'ني', + 64593 => 'هج', + 64594 => 'هم', + 64595 => 'هى', + 64596 => 'هي', + 64597 => 'يج', + 64598 => 'يح', + 64599 => 'يخ', + 64600 => 'يم', + 64601 => 'يى', + 64602 => 'يي', + 64603 => 'ذٰ', + 64604 => 'رٰ', + 64605 => 'ىٰ', + 64612 => 'ئر', + 64613 => 'ئز', + 64614 => 'ئم', + 64615 => 'ئن', + 64616 => 'ئى', + 64617 => 'ئي', + 64618 => 'بر', + 64619 => 'بز', + 64620 => 'بم', + 64621 => 'بن', + 64622 => 'بى', + 64623 => 'بي', + 64624 => 'تر', + 64625 => 'تز', + 64626 => 'تم', + 64627 => 'تن', + 64628 => 'تى', + 64629 => 'تي', + 64630 => 'ثر', + 64631 => 'ثز', + 64632 => 'ثم', + 64633 => 'ثن', + 64634 => 'ثى', + 64635 => 'ثي', + 64636 => 'فى', + 64637 => 'في', + 64638 => 'قى', + 64639 => 'قي', + 64640 => 'كا', + 64641 => 'كل', + 64642 => 'كم', + 64643 => 'كى', + 64644 => 'كي', + 64645 => 'لم', + 64646 => 'لى', + 64647 => 'لي', + 64648 => 'ما', + 64649 => 'مم', + 64650 => 'نر', + 64651 => 'نز', + 64652 => 'نم', + 64653 => 'نن', + 64654 => 'نى', + 64655 => 'ني', + 64656 => 'ىٰ', + 64657 => 'ير', + 64658 => 'يز', + 64659 => 'يم', + 64660 => 'ين', + 64661 => 'يى', + 64662 => 'يي', + 64663 => 'ئج', + 64664 => 'ئح', + 64665 => 'ئخ', + 64666 => 'ئم', + 64667 => 'ئه', + 64668 => 'بج', + 64669 => 'بح', + 64670 => 'بخ', + 64671 => 'بم', + 64672 => 'به', + 64673 => 'تج', + 64674 => 'تح', + 64675 => 'تخ', + 64676 => 'تم', + 64677 => 'ته', + 64678 => 'ثم', + 64679 => 'جح', + 64680 => 'جم', + 64681 => 'حج', + 64682 => 'حم', + 64683 => 'خج', + 64684 => 'خم', + 64685 => 'سج', + 64686 => 'سح', + 64687 => 'سخ', + 64688 => 'سم', + 64689 => 'صح', + 64690 => 'صخ', + 64691 => 'صم', + 64692 => 'ضج', + 64693 => 'ضح', + 64694 => 'ضخ', + 64695 => 'ضم', + 64696 => 'طح', + 64697 => 'ظم', + 64698 => 'عج', + 64699 => 'عم', + 64700 => 'غج', + 64701 => 'غم', + 64702 => 'فج', + 64703 => 'فح', + 64704 => 'فخ', + 64705 => 'فم', + 64706 => 'قح', + 64707 => 'قم', + 64708 => 'كج', + 64709 => 'كح', + 64710 => 'كخ', + 64711 => 'كل', + 64712 => 'كم', + 64713 => 'لج', + 64714 => 'لح', + 64715 => 'لخ', + 64716 => 'لم', + 64717 => 'له', + 64718 => 'مج', + 64719 => 'مح', + 64720 => 'مخ', + 64721 => 'مم', + 64722 => 'نج', + 64723 => 'نح', + 64724 => 'نخ', + 64725 => 'نم', + 64726 => 'نه', + 64727 => 'هج', + 64728 => 'هم', + 64729 => 'هٰ', + 64730 => 'يج', + 64731 => 'يح', + 64732 => 'يخ', + 64733 => 'يم', + 64734 => 'يه', + 64735 => 'ئم', + 64736 => 'ئه', + 64737 => 'بم', + 64738 => 'به', + 64739 => 'تم', + 64740 => 'ته', + 64741 => 'ثم', + 64742 => 'ثه', + 64743 => 'سم', + 64744 => 'سه', + 64745 => 'شم', + 64746 => 'شه', + 64747 => 'كل', + 64748 => 'كم', + 64749 => 'لم', + 64750 => 'نم', + 64751 => 'نه', + 64752 => 'يم', + 64753 => 'يه', + 64754 => 'ـَّ', + 64755 => 'ـُّ', + 64756 => 'ـِّ', + 64757 => 'طى', + 64758 => 'طي', + 64759 => 'عى', + 64760 => 'عي', + 64761 => 'غى', + 64762 => 'غي', + 64763 => 'سى', + 64764 => 'سي', + 64765 => 'شى', + 64766 => 'شي', + 64767 => 'حى', + 64768 => 'حي', + 64769 => 'جى', + 64770 => 'جي', + 64771 => 'خى', + 64772 => 'خي', + 64773 => 'صى', + 64774 => 'صي', + 64775 => 'ضى', + 64776 => 'ضي', + 64777 => 'شج', + 64778 => 'شح', + 64779 => 'شخ', + 64780 => 'شم', + 64781 => 'شر', + 64782 => 'سر', + 64783 => 'صر', + 64784 => 'ضر', + 64785 => 'طى', + 64786 => 'طي', + 64787 => 'عى', + 64788 => 'عي', + 64789 => 'غى', + 64790 => 'غي', + 64791 => 'سى', + 64792 => 'سي', + 64793 => 'شى', + 64794 => 'شي', + 64795 => 'حى', + 64796 => 'حي', + 64797 => 'جى', + 64798 => 'جي', + 64799 => 'خى', + 64800 => 'خي', + 64801 => 'صى', + 64802 => 'صي', + 64803 => 'ضى', + 64804 => 'ضي', + 64805 => 'شج', + 64806 => 'شح', + 64807 => 'شخ', + 64808 => 'شم', + 64809 => 'شر', + 64810 => 'سر', + 64811 => 'صر', + 64812 => 'ضر', + 64813 => 'شج', + 64814 => 'شح', + 64815 => 'شخ', + 64816 => 'شم', + 64817 => 'سه', + 64818 => 'شه', + 64819 => 'طم', + 64820 => 'سج', + 64821 => 'سح', + 64822 => 'سخ', + 64823 => 'شج', + 64824 => 'شح', + 64825 => 'شخ', + 64826 => 'طم', + 64827 => 'ظم', + 64828 => 'اً', + 64829 => 'اً', + 64848 => 'تجم', + 64849 => 'تحج', + 64850 => 'تحج', + 64851 => 'تحم', + 64852 => 'تخم', + 64853 => 'تمج', + 64854 => 'تمح', + 64855 => 'تمخ', + 64856 => 'جمح', + 64857 => 'جمح', + 64858 => 'حمي', + 64859 => 'حمى', + 64860 => 'سحج', + 64861 => 'سجح', + 64862 => 'سجى', + 64863 => 'سمح', + 64864 => 'سمح', + 64865 => 'سمج', + 64866 => 'سمم', + 64867 => 'سمم', + 64868 => 'صحح', + 64869 => 'صحح', + 64870 => 'صمم', + 64871 => 'شحم', + 64872 => 'شحم', + 64873 => 'شجي', + 64874 => 'شمخ', + 64875 => 'شمخ', + 64876 => 'شمم', + 64877 => 'شمم', + 64878 => 'ضحى', + 64879 => 'ضخم', + 64880 => 'ضخم', + 64881 => 'طمح', + 64882 => 'طمح', + 64883 => 'طمم', + 64884 => 'طمي', + 64885 => 'عجم', + 64886 => 'عمم', + 64887 => 'عمم', + 64888 => 'عمى', + 64889 => 'غمم', + 64890 => 'غمي', + 64891 => 'غمى', + 64892 => 'فخم', + 64893 => 'فخم', + 64894 => 'قمح', + 64895 => 'قمم', + 64896 => 'لحم', + 64897 => 'لحي', + 64898 => 'لحى', + 64899 => 'لجج', + 64900 => 'لجج', + 64901 => 'لخم', + 64902 => 'لخم', + 64903 => 'لمح', + 64904 => 'لمح', + 64905 => 'محج', + 64906 => 'محم', + 64907 => 'محي', + 64908 => 'مجح', + 64909 => 'مجم', + 64910 => 'مخج', + 64911 => 'مخم', + 64914 => 'مجخ', + 64915 => 'همج', + 64916 => 'همم', + 64917 => 'نحم', + 64918 => 'نحى', + 64919 => 'نجم', + 64920 => 'نجم', + 64921 => 'نجى', + 64922 => 'نمي', + 64923 => 'نمى', + 64924 => 'يمم', + 64925 => 'يمم', + 64926 => 'بخي', + 64927 => 'تجي', + 64928 => 'تجى', + 64929 => 'تخي', + 64930 => 'تخى', + 64931 => 'تمي', + 64932 => 'تمى', + 64933 => 'جمي', + 64934 => 'جحى', + 64935 => 'جمى', + 64936 => 'سخى', + 64937 => 'صحي', + 64938 => 'شحي', + 64939 => 'ضحي', + 64940 => 'لجي', + 64941 => 'لمي', + 64942 => 'يحي', + 64943 => 'يجي', + 64944 => 'يمي', + 64945 => 'ممي', + 64946 => 'قمي', + 64947 => 'نحي', + 64948 => 'قمح', + 64949 => 'لحم', + 64950 => 'عمي', + 64951 => 'كمي', + 64952 => 'نجح', + 64953 => 'مخي', + 64954 => 'لجم', + 64955 => 'كمم', + 64956 => 'لجم', + 64957 => 'نجح', + 64958 => 'جحي', + 64959 => 'حجي', + 64960 => 'مجي', + 64961 => 'فمي', + 64962 => 'بحي', + 64963 => 'كمم', + 64964 => 'عجم', + 64965 => 'صمم', + 64966 => 'سخي', + 64967 => 'نجي', + 65008 => 'صلے', + 65009 => 'قلے', + 65010 => 'الله', + 65011 => 'اكبر', + 65012 => 'محمد', + 65013 => 'صلعم', + 65014 => 'رسول', + 65015 => 'عليه', + 65016 => 'وسلم', + 65017 => 'صلى', + 65020 => 'ریال', + 65041 => '、', + 65047 => '〖', + 65048 => '〗', + 65073 => '—', + 65074 => '–', + 65081 => '〔', + 65082 => '〕', + 65083 => '【', + 65084 => '】', + 65085 => '《', + 65086 => '》', + 65087 => '〈', + 65088 => '〉', + 65089 => '「', + 65090 => '」', + 65091 => '『', + 65092 => '』', + 65105 => '、', + 65112 => '—', + 65117 => '〔', + 65118 => '〕', + 65123 => '-', + 65137 => 'ـً', + 65143 => 'ـَ', + 65145 => 'ـُ', + 65147 => 'ـِ', + 65149 => 'ـّ', + 65151 => 'ـْ', + 65152 => 'ء', + 65153 => 'آ', + 65154 => 'آ', + 65155 => 'أ', + 65156 => 'أ', + 65157 => 'ؤ', + 65158 => 'ؤ', + 65159 => 'إ', + 65160 => 'إ', + 65161 => 'ئ', + 65162 => 'ئ', + 65163 => 'ئ', + 65164 => 'ئ', + 65165 => 'ا', + 65166 => 'ا', + 65167 => 'ب', + 65168 => 'ب', + 65169 => 'ب', + 65170 => 'ب', + 65171 => 'ة', + 65172 => 'ة', + 65173 => 'ت', + 65174 => 'ت', + 65175 => 'ت', + 65176 => 'ت', + 65177 => 'ث', + 65178 => 'ث', + 65179 => 'ث', + 65180 => 'ث', + 65181 => 'ج', + 65182 => 'ج', + 65183 => 'ج', + 65184 => 'ج', + 65185 => 'ح', + 65186 => 'ح', + 65187 => 'ح', + 65188 => 'ح', + 65189 => 'خ', + 65190 => 'خ', + 65191 => 'خ', + 65192 => 'خ', + 65193 => 'د', + 65194 => 'د', + 65195 => 'ذ', + 65196 => 'ذ', + 65197 => 'ر', + 65198 => 'ر', + 65199 => 'ز', + 65200 => 'ز', + 65201 => 'س', + 65202 => 'س', + 65203 => 'س', + 65204 => 'س', + 65205 => 'ش', + 65206 => 'ش', + 65207 => 'ش', + 65208 => 'ش', + 65209 => 'ص', + 65210 => 'ص', + 65211 => 'ص', + 65212 => 'ص', + 65213 => 'ض', + 65214 => 'ض', + 65215 => 'ض', + 65216 => 'ض', + 65217 => 'ط', + 65218 => 'ط', + 65219 => 'ط', + 65220 => 'ط', + 65221 => 'ظ', + 65222 => 'ظ', + 65223 => 'ظ', + 65224 => 'ظ', + 65225 => 'ع', + 65226 => 'ع', + 65227 => 'ع', + 65228 => 'ع', + 65229 => 'غ', + 65230 => 'غ', + 65231 => 'غ', + 65232 => 'غ', + 65233 => 'ف', + 65234 => 'ف', + 65235 => 'ف', + 65236 => 'ف', + 65237 => 'ق', + 65238 => 'ق', + 65239 => 'ق', + 65240 => 'ق', + 65241 => 'ك', + 65242 => 'ك', + 65243 => 'ك', + 65244 => 'ك', + 65245 => 'ل', + 65246 => 'ل', + 65247 => 'ل', + 65248 => 'ل', + 65249 => 'م', + 65250 => 'م', + 65251 => 'م', + 65252 => 'م', + 65253 => 'ن', + 65254 => 'ن', + 65255 => 'ن', + 65256 => 'ن', + 65257 => 'ه', + 65258 => 'ه', + 65259 => 'ه', + 65260 => 'ه', + 65261 => 'و', + 65262 => 'و', + 65263 => 'ى', + 65264 => 'ى', + 65265 => 'ي', + 65266 => 'ي', + 65267 => 'ي', + 65268 => 'ي', + 65269 => 'لآ', + 65270 => 'لآ', + 65271 => 'لأ', + 65272 => 'لأ', + 65273 => 'لإ', + 65274 => 'لإ', + 65275 => 'لا', + 65276 => 'لا', + 65293 => '-', + 65294 => '.', + 65296 => '0', + 65297 => '1', + 65298 => '2', + 65299 => '3', + 65300 => '4', + 65301 => '5', + 65302 => '6', + 65303 => '7', + 65304 => '8', + 65305 => '9', + 65313 => 'a', + 65314 => 'b', + 65315 => 'c', + 65316 => 'd', + 65317 => 'e', + 65318 => 'f', + 65319 => 'g', + 65320 => 'h', + 65321 => 'i', + 65322 => 'j', + 65323 => 'k', + 65324 => 'l', + 65325 => 'm', + 65326 => 'n', + 65327 => 'o', + 65328 => 'p', + 65329 => 'q', + 65330 => 'r', + 65331 => 's', + 65332 => 't', + 65333 => 'u', + 65334 => 'v', + 65335 => 'w', + 65336 => 'x', + 65337 => 'y', + 65338 => 'z', + 65345 => 'a', + 65346 => 'b', + 65347 => 'c', + 65348 => 'd', + 65349 => 'e', + 65350 => 'f', + 65351 => 'g', + 65352 => 'h', + 65353 => 'i', + 65354 => 'j', + 65355 => 'k', + 65356 => 'l', + 65357 => 'm', + 65358 => 'n', + 65359 => 'o', + 65360 => 'p', + 65361 => 'q', + 65362 => 'r', + 65363 => 's', + 65364 => 't', + 65365 => 'u', + 65366 => 'v', + 65367 => 'w', + 65368 => 'x', + 65369 => 'y', + 65370 => 'z', + 65375 => '⦅', + 65376 => '⦆', + 65377 => '.', + 65378 => '「', + 65379 => '」', + 65380 => '、', + 65381 => '・', + 65382 => 'ヲ', + 65383 => 'ァ', + 65384 => 'ィ', + 65385 => 'ゥ', + 65386 => 'ェ', + 65387 => 'ォ', + 65388 => 'ャ', + 65389 => 'ュ', + 65390 => 'ョ', + 65391 => 'ッ', + 65392 => 'ー', + 65393 => 'ア', + 65394 => 'イ', + 65395 => 'ウ', + 65396 => 'エ', + 65397 => 'オ', + 65398 => 'カ', + 65399 => 'キ', + 65400 => 'ク', + 65401 => 'ケ', + 65402 => 'コ', + 65403 => 'サ', + 65404 => 'シ', + 65405 => 'ス', + 65406 => 'セ', + 65407 => 'ソ', + 65408 => 'タ', + 65409 => 'チ', + 65410 => 'ツ', + 65411 => 'テ', + 65412 => 'ト', + 65413 => 'ナ', + 65414 => 'ニ', + 65415 => 'ヌ', + 65416 => 'ネ', + 65417 => 'ノ', + 65418 => 'ハ', + 65419 => 'ヒ', + 65420 => 'フ', + 65421 => 'ヘ', + 65422 => 'ホ', + 65423 => 'マ', + 65424 => 'ミ', + 65425 => 'ム', + 65426 => 'メ', + 65427 => 'モ', + 65428 => 'ヤ', + 65429 => 'ユ', + 65430 => 'ヨ', + 65431 => 'ラ', + 65432 => 'リ', + 65433 => 'ル', + 65434 => 'レ', + 65435 => 'ロ', + 65436 => 'ワ', + 65437 => 'ン', + 65438 => '゙', + 65439 => '゚', + 65441 => 'ᄀ', + 65442 => 'ᄁ', + 65443 => 'ᆪ', + 65444 => 'ᄂ', + 65445 => 'ᆬ', + 65446 => 'ᆭ', + 65447 => 'ᄃ', + 65448 => 'ᄄ', + 65449 => 'ᄅ', + 65450 => 'ᆰ', + 65451 => 'ᆱ', + 65452 => 'ᆲ', + 65453 => 'ᆳ', + 65454 => 'ᆴ', + 65455 => 'ᆵ', + 65456 => 'ᄚ', + 65457 => 'ᄆ', + 65458 => 'ᄇ', + 65459 => 'ᄈ', + 65460 => 'ᄡ', + 65461 => 'ᄉ', + 65462 => 'ᄊ', + 65463 => 'ᄋ', + 65464 => 'ᄌ', + 65465 => 'ᄍ', + 65466 => 'ᄎ', + 65467 => 'ᄏ', + 65468 => 'ᄐ', + 65469 => 'ᄑ', + 65470 => 'ᄒ', + 65474 => 'ᅡ', + 65475 => 'ᅢ', + 65476 => 'ᅣ', + 65477 => 'ᅤ', + 65478 => 'ᅥ', + 65479 => 'ᅦ', + 65482 => 'ᅧ', + 65483 => 'ᅨ', + 65484 => 'ᅩ', + 65485 => 'ᅪ', + 65486 => 'ᅫ', + 65487 => 'ᅬ', + 65490 => 'ᅭ', + 65491 => 'ᅮ', + 65492 => 'ᅯ', + 65493 => 'ᅰ', + 65494 => 'ᅱ', + 65495 => 'ᅲ', + 65498 => 'ᅳ', + 65499 => 'ᅴ', + 65500 => 'ᅵ', + 65504 => '¢', + 65505 => '£', + 65506 => '¬', + 65508 => '¦', + 65509 => '¥', + 65510 => '₩', + 65512 => '│', + 65513 => '←', + 65514 => '↑', + 65515 => '→', + 65516 => '↓', + 65517 => '■', + 65518 => '○', + 66560 => '𐐨', + 66561 => '𐐩', + 66562 => '𐐪', + 66563 => '𐐫', + 66564 => '𐐬', + 66565 => '𐐭', + 66566 => '𐐮', + 66567 => '𐐯', + 66568 => '𐐰', + 66569 => '𐐱', + 66570 => '𐐲', + 66571 => '𐐳', + 66572 => '𐐴', + 66573 => '𐐵', + 66574 => '𐐶', + 66575 => '𐐷', + 66576 => '𐐸', + 66577 => '𐐹', + 66578 => '𐐺', + 66579 => '𐐻', + 66580 => '𐐼', + 66581 => '𐐽', + 66582 => '𐐾', + 66583 => '𐐿', + 66584 => '𐑀', + 66585 => '𐑁', + 66586 => '𐑂', + 66587 => '𐑃', + 66588 => '𐑄', + 66589 => '𐑅', + 66590 => '𐑆', + 66591 => '𐑇', + 66592 => '𐑈', + 66593 => '𐑉', + 66594 => '𐑊', + 66595 => '𐑋', + 66596 => '𐑌', + 66597 => '𐑍', + 66598 => '𐑎', + 66599 => '𐑏', + 66736 => '𐓘', + 66737 => '𐓙', + 66738 => '𐓚', + 66739 => '𐓛', + 66740 => '𐓜', + 66741 => '𐓝', + 66742 => '𐓞', + 66743 => '𐓟', + 66744 => '𐓠', + 66745 => '𐓡', + 66746 => '𐓢', + 66747 => '𐓣', + 66748 => '𐓤', + 66749 => '𐓥', + 66750 => '𐓦', + 66751 => '𐓧', + 66752 => '𐓨', + 66753 => '𐓩', + 66754 => '𐓪', + 66755 => '𐓫', + 66756 => '𐓬', + 66757 => '𐓭', + 66758 => '𐓮', + 66759 => '𐓯', + 66760 => '𐓰', + 66761 => '𐓱', + 66762 => '𐓲', + 66763 => '𐓳', + 66764 => '𐓴', + 66765 => '𐓵', + 66766 => '𐓶', + 66767 => '𐓷', + 66768 => '𐓸', + 66769 => '𐓹', + 66770 => '𐓺', + 66771 => '𐓻', + 68736 => '𐳀', + 68737 => '𐳁', + 68738 => '𐳂', + 68739 => '𐳃', + 68740 => '𐳄', + 68741 => '𐳅', + 68742 => '𐳆', + 68743 => '𐳇', + 68744 => '𐳈', + 68745 => '𐳉', + 68746 => '𐳊', + 68747 => '𐳋', + 68748 => '𐳌', + 68749 => '𐳍', + 68750 => '𐳎', + 68751 => '𐳏', + 68752 => '𐳐', + 68753 => '𐳑', + 68754 => '𐳒', + 68755 => '𐳓', + 68756 => '𐳔', + 68757 => '𐳕', + 68758 => '𐳖', + 68759 => '𐳗', + 68760 => '𐳘', + 68761 => '𐳙', + 68762 => '𐳚', + 68763 => '𐳛', + 68764 => '𐳜', + 68765 => '𐳝', + 68766 => '𐳞', + 68767 => '𐳟', + 68768 => '𐳠', + 68769 => '𐳡', + 68770 => '𐳢', + 68771 => '𐳣', + 68772 => '𐳤', + 68773 => '𐳥', + 68774 => '𐳦', + 68775 => '𐳧', + 68776 => '𐳨', + 68777 => '𐳩', + 68778 => '𐳪', + 68779 => '𐳫', + 68780 => '𐳬', + 68781 => '𐳭', + 68782 => '𐳮', + 68783 => '𐳯', + 68784 => '𐳰', + 68785 => '𐳱', + 68786 => '𐳲', + 71840 => '𑣀', + 71841 => '𑣁', + 71842 => '𑣂', + 71843 => '𑣃', + 71844 => '𑣄', + 71845 => '𑣅', + 71846 => '𑣆', + 71847 => '𑣇', + 71848 => '𑣈', + 71849 => '𑣉', + 71850 => '𑣊', + 71851 => '𑣋', + 71852 => '𑣌', + 71853 => '𑣍', + 71854 => '𑣎', + 71855 => '𑣏', + 71856 => '𑣐', + 71857 => '𑣑', + 71858 => '𑣒', + 71859 => '𑣓', + 71860 => '𑣔', + 71861 => '𑣕', + 71862 => '𑣖', + 71863 => '𑣗', + 71864 => '𑣘', + 71865 => '𑣙', + 71866 => '𑣚', + 71867 => '𑣛', + 71868 => '𑣜', + 71869 => '𑣝', + 71870 => '𑣞', + 71871 => '𑣟', + 93760 => '𖹠', + 93761 => '𖹡', + 93762 => '𖹢', + 93763 => '𖹣', + 93764 => '𖹤', + 93765 => '𖹥', + 93766 => '𖹦', + 93767 => '𖹧', + 93768 => '𖹨', + 93769 => '𖹩', + 93770 => '𖹪', + 93771 => '𖹫', + 93772 => '𖹬', + 93773 => '𖹭', + 93774 => '𖹮', + 93775 => '𖹯', + 93776 => '𖹰', + 93777 => '𖹱', + 93778 => '𖹲', + 93779 => '𖹳', + 93780 => '𖹴', + 93781 => '𖹵', + 93782 => '𖹶', + 93783 => '𖹷', + 93784 => '𖹸', + 93785 => '𖹹', + 93786 => '𖹺', + 93787 => '𖹻', + 93788 => '𖹼', + 93789 => '𖹽', + 93790 => '𖹾', + 93791 => '𖹿', + 119134 => '𝅗𝅥', + 119135 => '𝅘𝅥', + 119136 => '𝅘𝅥𝅮', + 119137 => '𝅘𝅥𝅯', + 119138 => '𝅘𝅥𝅰', + 119139 => '𝅘𝅥𝅱', + 119140 => '𝅘𝅥𝅲', + 119227 => '𝆹𝅥', + 119228 => '𝆺𝅥', + 119229 => '𝆹𝅥𝅮', + 119230 => '𝆺𝅥𝅮', + 119231 => '𝆹𝅥𝅯', + 119232 => '𝆺𝅥𝅯', + 119808 => 'a', + 119809 => 'b', + 119810 => 'c', + 119811 => 'd', + 119812 => 'e', + 119813 => 'f', + 119814 => 'g', + 119815 => 'h', + 119816 => 'i', + 119817 => 'j', + 119818 => 'k', + 119819 => 'l', + 119820 => 'm', + 119821 => 'n', + 119822 => 'o', + 119823 => 'p', + 119824 => 'q', + 119825 => 'r', + 119826 => 's', + 119827 => 't', + 119828 => 'u', + 119829 => 'v', + 119830 => 'w', + 119831 => 'x', + 119832 => 'y', + 119833 => 'z', + 119834 => 'a', + 119835 => 'b', + 119836 => 'c', + 119837 => 'd', + 119838 => 'e', + 119839 => 'f', + 119840 => 'g', + 119841 => 'h', + 119842 => 'i', + 119843 => 'j', + 119844 => 'k', + 119845 => 'l', + 119846 => 'm', + 119847 => 'n', + 119848 => 'o', + 119849 => 'p', + 119850 => 'q', + 119851 => 'r', + 119852 => 's', + 119853 => 't', + 119854 => 'u', + 119855 => 'v', + 119856 => 'w', + 119857 => 'x', + 119858 => 'y', + 119859 => 'z', + 119860 => 'a', + 119861 => 'b', + 119862 => 'c', + 119863 => 'd', + 119864 => 'e', + 119865 => 'f', + 119866 => 'g', + 119867 => 'h', + 119868 => 'i', + 119869 => 'j', + 119870 => 'k', + 119871 => 'l', + 119872 => 'm', + 119873 => 'n', + 119874 => 'o', + 119875 => 'p', + 119876 => 'q', + 119877 => 'r', + 119878 => 's', + 119879 => 't', + 119880 => 'u', + 119881 => 'v', + 119882 => 'w', + 119883 => 'x', + 119884 => 'y', + 119885 => 'z', + 119886 => 'a', + 119887 => 'b', + 119888 => 'c', + 119889 => 'd', + 119890 => 'e', + 119891 => 'f', + 119892 => 'g', + 119894 => 'i', + 119895 => 'j', + 119896 => 'k', + 119897 => 'l', + 119898 => 'm', + 119899 => 'n', + 119900 => 'o', + 119901 => 'p', + 119902 => 'q', + 119903 => 'r', + 119904 => 's', + 119905 => 't', + 119906 => 'u', + 119907 => 'v', + 119908 => 'w', + 119909 => 'x', + 119910 => 'y', + 119911 => 'z', + 119912 => 'a', + 119913 => 'b', + 119914 => 'c', + 119915 => 'd', + 119916 => 'e', + 119917 => 'f', + 119918 => 'g', + 119919 => 'h', + 119920 => 'i', + 119921 => 'j', + 119922 => 'k', + 119923 => 'l', + 119924 => 'm', + 119925 => 'n', + 119926 => 'o', + 119927 => 'p', + 119928 => 'q', + 119929 => 'r', + 119930 => 's', + 119931 => 't', + 119932 => 'u', + 119933 => 'v', + 119934 => 'w', + 119935 => 'x', + 119936 => 'y', + 119937 => 'z', + 119938 => 'a', + 119939 => 'b', + 119940 => 'c', + 119941 => 'd', + 119942 => 'e', + 119943 => 'f', + 119944 => 'g', + 119945 => 'h', + 119946 => 'i', + 119947 => 'j', + 119948 => 'k', + 119949 => 'l', + 119950 => 'm', + 119951 => 'n', + 119952 => 'o', + 119953 => 'p', + 119954 => 'q', + 119955 => 'r', + 119956 => 's', + 119957 => 't', + 119958 => 'u', + 119959 => 'v', + 119960 => 'w', + 119961 => 'x', + 119962 => 'y', + 119963 => 'z', + 119964 => 'a', + 119966 => 'c', + 119967 => 'd', + 119970 => 'g', + 119973 => 'j', + 119974 => 'k', + 119977 => 'n', + 119978 => 'o', + 119979 => 'p', + 119980 => 'q', + 119982 => 's', + 119983 => 't', + 119984 => 'u', + 119985 => 'v', + 119986 => 'w', + 119987 => 'x', + 119988 => 'y', + 119989 => 'z', + 119990 => 'a', + 119991 => 'b', + 119992 => 'c', + 119993 => 'd', + 119995 => 'f', + 119997 => 'h', + 119998 => 'i', + 119999 => 'j', + 120000 => 'k', + 120001 => 'l', + 120002 => 'm', + 120003 => 'n', + 120005 => 'p', + 120006 => 'q', + 120007 => 'r', + 120008 => 's', + 120009 => 't', + 120010 => 'u', + 120011 => 'v', + 120012 => 'w', + 120013 => 'x', + 120014 => 'y', + 120015 => 'z', + 120016 => 'a', + 120017 => 'b', + 120018 => 'c', + 120019 => 'd', + 120020 => 'e', + 120021 => 'f', + 120022 => 'g', + 120023 => 'h', + 120024 => 'i', + 120025 => 'j', + 120026 => 'k', + 120027 => 'l', + 120028 => 'm', + 120029 => 'n', + 120030 => 'o', + 120031 => 'p', + 120032 => 'q', + 120033 => 'r', + 120034 => 's', + 120035 => 't', + 120036 => 'u', + 120037 => 'v', + 120038 => 'w', + 120039 => 'x', + 120040 => 'y', + 120041 => 'z', + 120042 => 'a', + 120043 => 'b', + 120044 => 'c', + 120045 => 'd', + 120046 => 'e', + 120047 => 'f', + 120048 => 'g', + 120049 => 'h', + 120050 => 'i', + 120051 => 'j', + 120052 => 'k', + 120053 => 'l', + 120054 => 'm', + 120055 => 'n', + 120056 => 'o', + 120057 => 'p', + 120058 => 'q', + 120059 => 'r', + 120060 => 's', + 120061 => 't', + 120062 => 'u', + 120063 => 'v', + 120064 => 'w', + 120065 => 'x', + 120066 => 'y', + 120067 => 'z', + 120068 => 'a', + 120069 => 'b', + 120071 => 'd', + 120072 => 'e', + 120073 => 'f', + 120074 => 'g', + 120077 => 'j', + 120078 => 'k', + 120079 => 'l', + 120080 => 'm', + 120081 => 'n', + 120082 => 'o', + 120083 => 'p', + 120084 => 'q', + 120086 => 's', + 120087 => 't', + 120088 => 'u', + 120089 => 'v', + 120090 => 'w', + 120091 => 'x', + 120092 => 'y', + 120094 => 'a', + 120095 => 'b', + 120096 => 'c', + 120097 => 'd', + 120098 => 'e', + 120099 => 'f', + 120100 => 'g', + 120101 => 'h', + 120102 => 'i', + 120103 => 'j', + 120104 => 'k', + 120105 => 'l', + 120106 => 'm', + 120107 => 'n', + 120108 => 'o', + 120109 => 'p', + 120110 => 'q', + 120111 => 'r', + 120112 => 's', + 120113 => 't', + 120114 => 'u', + 120115 => 'v', + 120116 => 'w', + 120117 => 'x', + 120118 => 'y', + 120119 => 'z', + 120120 => 'a', + 120121 => 'b', + 120123 => 'd', + 120124 => 'e', + 120125 => 'f', + 120126 => 'g', + 120128 => 'i', + 120129 => 'j', + 120130 => 'k', + 120131 => 'l', + 120132 => 'm', + 120134 => 'o', + 120138 => 's', + 120139 => 't', + 120140 => 'u', + 120141 => 'v', + 120142 => 'w', + 120143 => 'x', + 120144 => 'y', + 120146 => 'a', + 120147 => 'b', + 120148 => 'c', + 120149 => 'd', + 120150 => 'e', + 120151 => 'f', + 120152 => 'g', + 120153 => 'h', + 120154 => 'i', + 120155 => 'j', + 120156 => 'k', + 120157 => 'l', + 120158 => 'm', + 120159 => 'n', + 120160 => 'o', + 120161 => 'p', + 120162 => 'q', + 120163 => 'r', + 120164 => 's', + 120165 => 't', + 120166 => 'u', + 120167 => 'v', + 120168 => 'w', + 120169 => 'x', + 120170 => 'y', + 120171 => 'z', + 120172 => 'a', + 120173 => 'b', + 120174 => 'c', + 120175 => 'd', + 120176 => 'e', + 120177 => 'f', + 120178 => 'g', + 120179 => 'h', + 120180 => 'i', + 120181 => 'j', + 120182 => 'k', + 120183 => 'l', + 120184 => 'm', + 120185 => 'n', + 120186 => 'o', + 120187 => 'p', + 120188 => 'q', + 120189 => 'r', + 120190 => 's', + 120191 => 't', + 120192 => 'u', + 120193 => 'v', + 120194 => 'w', + 120195 => 'x', + 120196 => 'y', + 120197 => 'z', + 120198 => 'a', + 120199 => 'b', + 120200 => 'c', + 120201 => 'd', + 120202 => 'e', + 120203 => 'f', + 120204 => 'g', + 120205 => 'h', + 120206 => 'i', + 120207 => 'j', + 120208 => 'k', + 120209 => 'l', + 120210 => 'm', + 120211 => 'n', + 120212 => 'o', + 120213 => 'p', + 120214 => 'q', + 120215 => 'r', + 120216 => 's', + 120217 => 't', + 120218 => 'u', + 120219 => 'v', + 120220 => 'w', + 120221 => 'x', + 120222 => 'y', + 120223 => 'z', + 120224 => 'a', + 120225 => 'b', + 120226 => 'c', + 120227 => 'd', + 120228 => 'e', + 120229 => 'f', + 120230 => 'g', + 120231 => 'h', + 120232 => 'i', + 120233 => 'j', + 120234 => 'k', + 120235 => 'l', + 120236 => 'm', + 120237 => 'n', + 120238 => 'o', + 120239 => 'p', + 120240 => 'q', + 120241 => 'r', + 120242 => 's', + 120243 => 't', + 120244 => 'u', + 120245 => 'v', + 120246 => 'w', + 120247 => 'x', + 120248 => 'y', + 120249 => 'z', + 120250 => 'a', + 120251 => 'b', + 120252 => 'c', + 120253 => 'd', + 120254 => 'e', + 120255 => 'f', + 120256 => 'g', + 120257 => 'h', + 120258 => 'i', + 120259 => 'j', + 120260 => 'k', + 120261 => 'l', + 120262 => 'm', + 120263 => 'n', + 120264 => 'o', + 120265 => 'p', + 120266 => 'q', + 120267 => 'r', + 120268 => 's', + 120269 => 't', + 120270 => 'u', + 120271 => 'v', + 120272 => 'w', + 120273 => 'x', + 120274 => 'y', + 120275 => 'z', + 120276 => 'a', + 120277 => 'b', + 120278 => 'c', + 120279 => 'd', + 120280 => 'e', + 120281 => 'f', + 120282 => 'g', + 120283 => 'h', + 120284 => 'i', + 120285 => 'j', + 120286 => 'k', + 120287 => 'l', + 120288 => 'm', + 120289 => 'n', + 120290 => 'o', + 120291 => 'p', + 120292 => 'q', + 120293 => 'r', + 120294 => 's', + 120295 => 't', + 120296 => 'u', + 120297 => 'v', + 120298 => 'w', + 120299 => 'x', + 120300 => 'y', + 120301 => 'z', + 120302 => 'a', + 120303 => 'b', + 120304 => 'c', + 120305 => 'd', + 120306 => 'e', + 120307 => 'f', + 120308 => 'g', + 120309 => 'h', + 120310 => 'i', + 120311 => 'j', + 120312 => 'k', + 120313 => 'l', + 120314 => 'm', + 120315 => 'n', + 120316 => 'o', + 120317 => 'p', + 120318 => 'q', + 120319 => 'r', + 120320 => 's', + 120321 => 't', + 120322 => 'u', + 120323 => 'v', + 120324 => 'w', + 120325 => 'x', + 120326 => 'y', + 120327 => 'z', + 120328 => 'a', + 120329 => 'b', + 120330 => 'c', + 120331 => 'd', + 120332 => 'e', + 120333 => 'f', + 120334 => 'g', + 120335 => 'h', + 120336 => 'i', + 120337 => 'j', + 120338 => 'k', + 120339 => 'l', + 120340 => 'm', + 120341 => 'n', + 120342 => 'o', + 120343 => 'p', + 120344 => 'q', + 120345 => 'r', + 120346 => 's', + 120347 => 't', + 120348 => 'u', + 120349 => 'v', + 120350 => 'w', + 120351 => 'x', + 120352 => 'y', + 120353 => 'z', + 120354 => 'a', + 120355 => 'b', + 120356 => 'c', + 120357 => 'd', + 120358 => 'e', + 120359 => 'f', + 120360 => 'g', + 120361 => 'h', + 120362 => 'i', + 120363 => 'j', + 120364 => 'k', + 120365 => 'l', + 120366 => 'm', + 120367 => 'n', + 120368 => 'o', + 120369 => 'p', + 120370 => 'q', + 120371 => 'r', + 120372 => 's', + 120373 => 't', + 120374 => 'u', + 120375 => 'v', + 120376 => 'w', + 120377 => 'x', + 120378 => 'y', + 120379 => 'z', + 120380 => 'a', + 120381 => 'b', + 120382 => 'c', + 120383 => 'd', + 120384 => 'e', + 120385 => 'f', + 120386 => 'g', + 120387 => 'h', + 120388 => 'i', + 120389 => 'j', + 120390 => 'k', + 120391 => 'l', + 120392 => 'm', + 120393 => 'n', + 120394 => 'o', + 120395 => 'p', + 120396 => 'q', + 120397 => 'r', + 120398 => 's', + 120399 => 't', + 120400 => 'u', + 120401 => 'v', + 120402 => 'w', + 120403 => 'x', + 120404 => 'y', + 120405 => 'z', + 120406 => 'a', + 120407 => 'b', + 120408 => 'c', + 120409 => 'd', + 120410 => 'e', + 120411 => 'f', + 120412 => 'g', + 120413 => 'h', + 120414 => 'i', + 120415 => 'j', + 120416 => 'k', + 120417 => 'l', + 120418 => 'm', + 120419 => 'n', + 120420 => 'o', + 120421 => 'p', + 120422 => 'q', + 120423 => 'r', + 120424 => 's', + 120425 => 't', + 120426 => 'u', + 120427 => 'v', + 120428 => 'w', + 120429 => 'x', + 120430 => 'y', + 120431 => 'z', + 120432 => 'a', + 120433 => 'b', + 120434 => 'c', + 120435 => 'd', + 120436 => 'e', + 120437 => 'f', + 120438 => 'g', + 120439 => 'h', + 120440 => 'i', + 120441 => 'j', + 120442 => 'k', + 120443 => 'l', + 120444 => 'm', + 120445 => 'n', + 120446 => 'o', + 120447 => 'p', + 120448 => 'q', + 120449 => 'r', + 120450 => 's', + 120451 => 't', + 120452 => 'u', + 120453 => 'v', + 120454 => 'w', + 120455 => 'x', + 120456 => 'y', + 120457 => 'z', + 120458 => 'a', + 120459 => 'b', + 120460 => 'c', + 120461 => 'd', + 120462 => 'e', + 120463 => 'f', + 120464 => 'g', + 120465 => 'h', + 120466 => 'i', + 120467 => 'j', + 120468 => 'k', + 120469 => 'l', + 120470 => 'm', + 120471 => 'n', + 120472 => 'o', + 120473 => 'p', + 120474 => 'q', + 120475 => 'r', + 120476 => 's', + 120477 => 't', + 120478 => 'u', + 120479 => 'v', + 120480 => 'w', + 120481 => 'x', + 120482 => 'y', + 120483 => 'z', + 120484 => 'ı', + 120485 => 'ȷ', + 120488 => 'α', + 120489 => 'β', + 120490 => 'γ', + 120491 => 'δ', + 120492 => 'ε', + 120493 => 'ζ', + 120494 => 'η', + 120495 => 'θ', + 120496 => 'ι', + 120497 => 'κ', + 120498 => 'λ', + 120499 => 'μ', + 120500 => 'ν', + 120501 => 'ξ', + 120502 => 'ο', + 120503 => 'π', + 120504 => 'ρ', + 120505 => 'θ', + 120506 => 'σ', + 120507 => 'τ', + 120508 => 'υ', + 120509 => 'φ', + 120510 => 'χ', + 120511 => 'ψ', + 120512 => 'ω', + 120513 => '∇', + 120514 => 'α', + 120515 => 'β', + 120516 => 'γ', + 120517 => 'δ', + 120518 => 'ε', + 120519 => 'ζ', + 120520 => 'η', + 120521 => 'θ', + 120522 => 'ι', + 120523 => 'κ', + 120524 => 'λ', + 120525 => 'μ', + 120526 => 'ν', + 120527 => 'ξ', + 120528 => 'ο', + 120529 => 'π', + 120530 => 'ρ', + 120531 => 'σ', + 120532 => 'σ', + 120533 => 'τ', + 120534 => 'υ', + 120535 => 'φ', + 120536 => 'χ', + 120537 => 'ψ', + 120538 => 'ω', + 120539 => '∂', + 120540 => 'ε', + 120541 => 'θ', + 120542 => 'κ', + 120543 => 'φ', + 120544 => 'ρ', + 120545 => 'π', + 120546 => 'α', + 120547 => 'β', + 120548 => 'γ', + 120549 => 'δ', + 120550 => 'ε', + 120551 => 'ζ', + 120552 => 'η', + 120553 => 'θ', + 120554 => 'ι', + 120555 => 'κ', + 120556 => 'λ', + 120557 => 'μ', + 120558 => 'ν', + 120559 => 'ξ', + 120560 => 'ο', + 120561 => 'π', + 120562 => 'ρ', + 120563 => 'θ', + 120564 => 'σ', + 120565 => 'τ', + 120566 => 'υ', + 120567 => 'φ', + 120568 => 'χ', + 120569 => 'ψ', + 120570 => 'ω', + 120571 => '∇', + 120572 => 'α', + 120573 => 'β', + 120574 => 'γ', + 120575 => 'δ', + 120576 => 'ε', + 120577 => 'ζ', + 120578 => 'η', + 120579 => 'θ', + 120580 => 'ι', + 120581 => 'κ', + 120582 => 'λ', + 120583 => 'μ', + 120584 => 'ν', + 120585 => 'ξ', + 120586 => 'ο', + 120587 => 'π', + 120588 => 'ρ', + 120589 => 'σ', + 120590 => 'σ', + 120591 => 'τ', + 120592 => 'υ', + 120593 => 'φ', + 120594 => 'χ', + 120595 => 'ψ', + 120596 => 'ω', + 120597 => '∂', + 120598 => 'ε', + 120599 => 'θ', + 120600 => 'κ', + 120601 => 'φ', + 120602 => 'ρ', + 120603 => 'π', + 120604 => 'α', + 120605 => 'β', + 120606 => 'γ', + 120607 => 'δ', + 120608 => 'ε', + 120609 => 'ζ', + 120610 => 'η', + 120611 => 'θ', + 120612 => 'ι', + 120613 => 'κ', + 120614 => 'λ', + 120615 => 'μ', + 120616 => 'ν', + 120617 => 'ξ', + 120618 => 'ο', + 120619 => 'π', + 120620 => 'ρ', + 120621 => 'θ', + 120622 => 'σ', + 120623 => 'τ', + 120624 => 'υ', + 120625 => 'φ', + 120626 => 'χ', + 120627 => 'ψ', + 120628 => 'ω', + 120629 => '∇', + 120630 => 'α', + 120631 => 'β', + 120632 => 'γ', + 120633 => 'δ', + 120634 => 'ε', + 120635 => 'ζ', + 120636 => 'η', + 120637 => 'θ', + 120638 => 'ι', + 120639 => 'κ', + 120640 => 'λ', + 120641 => 'μ', + 120642 => 'ν', + 120643 => 'ξ', + 120644 => 'ο', + 120645 => 'π', + 120646 => 'ρ', + 120647 => 'σ', + 120648 => 'σ', + 120649 => 'τ', + 120650 => 'υ', + 120651 => 'φ', + 120652 => 'χ', + 120653 => 'ψ', + 120654 => 'ω', + 120655 => '∂', + 120656 => 'ε', + 120657 => 'θ', + 120658 => 'κ', + 120659 => 'φ', + 120660 => 'ρ', + 120661 => 'π', + 120662 => 'α', + 120663 => 'β', + 120664 => 'γ', + 120665 => 'δ', + 120666 => 'ε', + 120667 => 'ζ', + 120668 => 'η', + 120669 => 'θ', + 120670 => 'ι', + 120671 => 'κ', + 120672 => 'λ', + 120673 => 'μ', + 120674 => 'ν', + 120675 => 'ξ', + 120676 => 'ο', + 120677 => 'π', + 120678 => 'ρ', + 120679 => 'θ', + 120680 => 'σ', + 120681 => 'τ', + 120682 => 'υ', + 120683 => 'φ', + 120684 => 'χ', + 120685 => 'ψ', + 120686 => 'ω', + 120687 => '∇', + 120688 => 'α', + 120689 => 'β', + 120690 => 'γ', + 120691 => 'δ', + 120692 => 'ε', + 120693 => 'ζ', + 120694 => 'η', + 120695 => 'θ', + 120696 => 'ι', + 120697 => 'κ', + 120698 => 'λ', + 120699 => 'μ', + 120700 => 'ν', + 120701 => 'ξ', + 120702 => 'ο', + 120703 => 'π', + 120704 => 'ρ', + 120705 => 'σ', + 120706 => 'σ', + 120707 => 'τ', + 120708 => 'υ', + 120709 => 'φ', + 120710 => 'χ', + 120711 => 'ψ', + 120712 => 'ω', + 120713 => '∂', + 120714 => 'ε', + 120715 => 'θ', + 120716 => 'κ', + 120717 => 'φ', + 120718 => 'ρ', + 120719 => 'π', + 120720 => 'α', + 120721 => 'β', + 120722 => 'γ', + 120723 => 'δ', + 120724 => 'ε', + 120725 => 'ζ', + 120726 => 'η', + 120727 => 'θ', + 120728 => 'ι', + 120729 => 'κ', + 120730 => 'λ', + 120731 => 'μ', + 120732 => 'ν', + 120733 => 'ξ', + 120734 => 'ο', + 120735 => 'π', + 120736 => 'ρ', + 120737 => 'θ', + 120738 => 'σ', + 120739 => 'τ', + 120740 => 'υ', + 120741 => 'φ', + 120742 => 'χ', + 120743 => 'ψ', + 120744 => 'ω', + 120745 => '∇', + 120746 => 'α', + 120747 => 'β', + 120748 => 'γ', + 120749 => 'δ', + 120750 => 'ε', + 120751 => 'ζ', + 120752 => 'η', + 120753 => 'θ', + 120754 => 'ι', + 120755 => 'κ', + 120756 => 'λ', + 120757 => 'μ', + 120758 => 'ν', + 120759 => 'ξ', + 120760 => 'ο', + 120761 => 'π', + 120762 => 'ρ', + 120763 => 'σ', + 120764 => 'σ', + 120765 => 'τ', + 120766 => 'υ', + 120767 => 'φ', + 120768 => 'χ', + 120769 => 'ψ', + 120770 => 'ω', + 120771 => '∂', + 120772 => 'ε', + 120773 => 'θ', + 120774 => 'κ', + 120775 => 'φ', + 120776 => 'ρ', + 120777 => 'π', + 120778 => 'ϝ', + 120779 => 'ϝ', + 120782 => '0', + 120783 => '1', + 120784 => '2', + 120785 => '3', + 120786 => '4', + 120787 => '5', + 120788 => '6', + 120789 => '7', + 120790 => '8', + 120791 => '9', + 120792 => '0', + 120793 => '1', + 120794 => '2', + 120795 => '3', + 120796 => '4', + 120797 => '5', + 120798 => '6', + 120799 => '7', + 120800 => '8', + 120801 => '9', + 120802 => '0', + 120803 => '1', + 120804 => '2', + 120805 => '3', + 120806 => '4', + 120807 => '5', + 120808 => '6', + 120809 => '7', + 120810 => '8', + 120811 => '9', + 120812 => '0', + 120813 => '1', + 120814 => '2', + 120815 => '3', + 120816 => '4', + 120817 => '5', + 120818 => '6', + 120819 => '7', + 120820 => '8', + 120821 => '9', + 120822 => '0', + 120823 => '1', + 120824 => '2', + 120825 => '3', + 120826 => '4', + 120827 => '5', + 120828 => '6', + 120829 => '7', + 120830 => '8', + 120831 => '9', + 125184 => '𞤢', + 125185 => '𞤣', + 125186 => '𞤤', + 125187 => '𞤥', + 125188 => '𞤦', + 125189 => '𞤧', + 125190 => '𞤨', + 125191 => '𞤩', + 125192 => '𞤪', + 125193 => '𞤫', + 125194 => '𞤬', + 125195 => '𞤭', + 125196 => '𞤮', + 125197 => '𞤯', + 125198 => '𞤰', + 125199 => '𞤱', + 125200 => '𞤲', + 125201 => '𞤳', + 125202 => '𞤴', + 125203 => '𞤵', + 125204 => '𞤶', + 125205 => '𞤷', + 125206 => '𞤸', + 125207 => '𞤹', + 125208 => '𞤺', + 125209 => '𞤻', + 125210 => '𞤼', + 125211 => '𞤽', + 125212 => '𞤾', + 125213 => '𞤿', + 125214 => '𞥀', + 125215 => '𞥁', + 125216 => '𞥂', + 125217 => '𞥃', + 126464 => 'ا', + 126465 => 'ب', + 126466 => 'ج', + 126467 => 'د', + 126469 => 'و', + 126470 => 'ز', + 126471 => 'ح', + 126472 => 'ط', + 126473 => 'ي', + 126474 => 'ك', + 126475 => 'ل', + 126476 => 'م', + 126477 => 'ن', + 126478 => 'س', + 126479 => 'ع', + 126480 => 'ف', + 126481 => 'ص', + 126482 => 'ق', + 126483 => 'ر', + 126484 => 'ش', + 126485 => 'ت', + 126486 => 'ث', + 126487 => 'خ', + 126488 => 'ذ', + 126489 => 'ض', + 126490 => 'ظ', + 126491 => 'غ', + 126492 => 'ٮ', + 126493 => 'ں', + 126494 => 'ڡ', + 126495 => 'ٯ', + 126497 => 'ب', + 126498 => 'ج', + 126500 => 'ه', + 126503 => 'ح', + 126505 => 'ي', + 126506 => 'ك', + 126507 => 'ل', + 126508 => 'م', + 126509 => 'ن', + 126510 => 'س', + 126511 => 'ع', + 126512 => 'ف', + 126513 => 'ص', + 126514 => 'ق', + 126516 => 'ش', + 126517 => 'ت', + 126518 => 'ث', + 126519 => 'خ', + 126521 => 'ض', + 126523 => 'غ', + 126530 => 'ج', + 126535 => 'ح', + 126537 => 'ي', + 126539 => 'ل', + 126541 => 'ن', + 126542 => 'س', + 126543 => 'ع', + 126545 => 'ص', + 126546 => 'ق', + 126548 => 'ش', + 126551 => 'خ', + 126553 => 'ض', + 126555 => 'غ', + 126557 => 'ں', + 126559 => 'ٯ', + 126561 => 'ب', + 126562 => 'ج', + 126564 => 'ه', + 126567 => 'ح', + 126568 => 'ط', + 126569 => 'ي', + 126570 => 'ك', + 126572 => 'م', + 126573 => 'ن', + 126574 => 'س', + 126575 => 'ع', + 126576 => 'ف', + 126577 => 'ص', + 126578 => 'ق', + 126580 => 'ش', + 126581 => 'ت', + 126582 => 'ث', + 126583 => 'خ', + 126585 => 'ض', + 126586 => 'ظ', + 126587 => 'غ', + 126588 => 'ٮ', + 126590 => 'ڡ', + 126592 => 'ا', + 126593 => 'ب', + 126594 => 'ج', + 126595 => 'د', + 126596 => 'ه', + 126597 => 'و', + 126598 => 'ز', + 126599 => 'ح', + 126600 => 'ط', + 126601 => 'ي', + 126603 => 'ل', + 126604 => 'م', + 126605 => 'ن', + 126606 => 'س', + 126607 => 'ع', + 126608 => 'ف', + 126609 => 'ص', + 126610 => 'ق', + 126611 => 'ر', + 126612 => 'ش', + 126613 => 'ت', + 126614 => 'ث', + 126615 => 'خ', + 126616 => 'ذ', + 126617 => 'ض', + 126618 => 'ظ', + 126619 => 'غ', + 126625 => 'ب', + 126626 => 'ج', + 126627 => 'د', + 126629 => 'و', + 126630 => 'ز', + 126631 => 'ح', + 126632 => 'ط', + 126633 => 'ي', + 126635 => 'ل', + 126636 => 'م', + 126637 => 'ن', + 126638 => 'س', + 126639 => 'ع', + 126640 => 'ف', + 126641 => 'ص', + 126642 => 'ق', + 126643 => 'ر', + 126644 => 'ش', + 126645 => 'ت', + 126646 => 'ث', + 126647 => 'خ', + 126648 => 'ذ', + 126649 => 'ض', + 126650 => 'ظ', + 126651 => 'غ', + 127274 => '〔s〕', + 127275 => 'c', + 127276 => 'r', + 127277 => 'cd', + 127278 => 'wz', + 127280 => 'a', + 127281 => 'b', + 127282 => 'c', + 127283 => 'd', + 127284 => 'e', + 127285 => 'f', + 127286 => 'g', + 127287 => 'h', + 127288 => 'i', + 127289 => 'j', + 127290 => 'k', + 127291 => 'l', + 127292 => 'm', + 127293 => 'n', + 127294 => 'o', + 127295 => 'p', + 127296 => 'q', + 127297 => 'r', + 127298 => 's', + 127299 => 't', + 127300 => 'u', + 127301 => 'v', + 127302 => 'w', + 127303 => 'x', + 127304 => 'y', + 127305 => 'z', + 127306 => 'hv', + 127307 => 'mv', + 127308 => 'sd', + 127309 => 'ss', + 127310 => 'ppv', + 127311 => 'wc', + 127338 => 'mc', + 127339 => 'md', + 127340 => 'mr', + 127376 => 'dj', + 127488 => 'ほか', + 127489 => 'ココ', + 127490 => 'サ', + 127504 => '手', + 127505 => '字', + 127506 => '双', + 127507 => 'デ', + 127508 => '二', + 127509 => '多', + 127510 => '解', + 127511 => '天', + 127512 => '交', + 127513 => '映', + 127514 => '無', + 127515 => '料', + 127516 => '前', + 127517 => '後', + 127518 => '再', + 127519 => '新', + 127520 => '初', + 127521 => '終', + 127522 => '生', + 127523 => '販', + 127524 => '声', + 127525 => '吹', + 127526 => '演', + 127527 => '投', + 127528 => '捕', + 127529 => '一', + 127530 => '三', + 127531 => '遊', + 127532 => '左', + 127533 => '中', + 127534 => '右', + 127535 => '指', + 127536 => '走', + 127537 => '打', + 127538 => '禁', + 127539 => '空', + 127540 => '合', + 127541 => '満', + 127542 => '有', + 127543 => '月', + 127544 => '申', + 127545 => '割', + 127546 => '営', + 127547 => '配', + 127552 => '〔本〕', + 127553 => '〔三〕', + 127554 => '〔二〕', + 127555 => '〔安〕', + 127556 => '〔点〕', + 127557 => '〔打〕', + 127558 => '〔盗〕', + 127559 => '〔勝〕', + 127560 => '〔敗〕', + 127568 => '得', + 127569 => '可', + 130032 => '0', + 130033 => '1', + 130034 => '2', + 130035 => '3', + 130036 => '4', + 130037 => '5', + 130038 => '6', + 130039 => '7', + 130040 => '8', + 130041 => '9', + 194560 => '丽', + 194561 => '丸', + 194562 => '乁', + 194563 => '𠄢', + 194564 => '你', + 194565 => '侮', + 194566 => '侻', + 194567 => '倂', + 194568 => '偺', + 194569 => '備', + 194570 => '僧', + 194571 => '像', + 194572 => '㒞', + 194573 => '𠘺', + 194574 => '免', + 194575 => '兔', + 194576 => '兤', + 194577 => '具', + 194578 => '𠔜', + 194579 => '㒹', + 194580 => '內', + 194581 => '再', + 194582 => '𠕋', + 194583 => '冗', + 194584 => '冤', + 194585 => '仌', + 194586 => '冬', + 194587 => '况', + 194588 => '𩇟', + 194589 => '凵', + 194590 => '刃', + 194591 => '㓟', + 194592 => '刻', + 194593 => '剆', + 194594 => '割', + 194595 => '剷', + 194596 => '㔕', + 194597 => '勇', + 194598 => '勉', + 194599 => '勤', + 194600 => '勺', + 194601 => '包', + 194602 => '匆', + 194603 => '北', + 194604 => '卉', + 194605 => '卑', + 194606 => '博', + 194607 => '即', + 194608 => '卽', + 194609 => '卿', + 194610 => '卿', + 194611 => '卿', + 194612 => '𠨬', + 194613 => '灰', + 194614 => '及', + 194615 => '叟', + 194616 => '𠭣', + 194617 => '叫', + 194618 => '叱', + 194619 => '吆', + 194620 => '咞', + 194621 => '吸', + 194622 => '呈', + 194623 => '周', + 194624 => '咢', + 194625 => '哶', + 194626 => '唐', + 194627 => '啓', + 194628 => '啣', + 194629 => '善', + 194630 => '善', + 194631 => '喙', + 194632 => '喫', + 194633 => '喳', + 194634 => '嗂', + 194635 => '圖', + 194636 => '嘆', + 194637 => '圗', + 194638 => '噑', + 194639 => '噴', + 194640 => '切', + 194641 => '壮', + 194642 => '城', + 194643 => '埴', + 194644 => '堍', + 194645 => '型', + 194646 => '堲', + 194647 => '報', + 194648 => '墬', + 194649 => '𡓤', + 194650 => '売', + 194651 => '壷', + 194652 => '夆', + 194653 => '多', + 194654 => '夢', + 194655 => '奢', + 194656 => '𡚨', + 194657 => '𡛪', + 194658 => '姬', + 194659 => '娛', + 194660 => '娧', + 194661 => '姘', + 194662 => '婦', + 194663 => '㛮', + 194665 => '嬈', + 194666 => '嬾', + 194667 => '嬾', + 194668 => '𡧈', + 194669 => '寃', + 194670 => '寘', + 194671 => '寧', + 194672 => '寳', + 194673 => '𡬘', + 194674 => '寿', + 194675 => '将', + 194677 => '尢', + 194678 => '㞁', + 194679 => '屠', + 194680 => '屮', + 194681 => '峀', + 194682 => '岍', + 194683 => '𡷤', + 194684 => '嵃', + 194685 => '𡷦', + 194686 => '嵮', + 194687 => '嵫', + 194688 => '嵼', + 194689 => '巡', + 194690 => '巢', + 194691 => '㠯', + 194692 => '巽', + 194693 => '帨', + 194694 => '帽', + 194695 => '幩', + 194696 => '㡢', + 194697 => '𢆃', + 194698 => '㡼', + 194699 => '庰', + 194700 => '庳', + 194701 => '庶', + 194702 => '廊', + 194703 => '𪎒', + 194704 => '廾', + 194705 => '𢌱', + 194706 => '𢌱', + 194707 => '舁', + 194708 => '弢', + 194709 => '弢', + 194710 => '㣇', + 194711 => '𣊸', + 194712 => '𦇚', + 194713 => '形', + 194714 => '彫', + 194715 => '㣣', + 194716 => '徚', + 194717 => '忍', + 194718 => '志', + 194719 => '忹', + 194720 => '悁', + 194721 => '㤺', + 194722 => '㤜', + 194723 => '悔', + 194724 => '𢛔', + 194725 => '惇', + 194726 => '慈', + 194727 => '慌', + 194728 => '慎', + 194729 => '慌', + 194730 => '慺', + 194731 => '憎', + 194732 => '憲', + 194733 => '憤', + 194734 => '憯', + 194735 => '懞', + 194736 => '懲', + 194737 => '懶', + 194738 => '成', + 194739 => '戛', + 194740 => '扝', + 194741 => '抱', + 194742 => '拔', + 194743 => '捐', + 194744 => '𢬌', + 194745 => '挽', + 194746 => '拼', + 194747 => '捨', + 194748 => '掃', + 194749 => '揤', + 194750 => '𢯱', + 194751 => '搢', + 194752 => '揅', + 194753 => '掩', + 194754 => '㨮', + 194755 => '摩', + 194756 => '摾', + 194757 => '撝', + 194758 => '摷', + 194759 => '㩬', + 194760 => '敏', + 194761 => '敬', + 194762 => '𣀊', + 194763 => '旣', + 194764 => '書', + 194765 => '晉', + 194766 => '㬙', + 194767 => '暑', + 194768 => '㬈', + 194769 => '㫤', + 194770 => '冒', + 194771 => '冕', + 194772 => '最', + 194773 => '暜', + 194774 => '肭', + 194775 => '䏙', + 194776 => '朗', + 194777 => '望', + 194778 => '朡', + 194779 => '杞', + 194780 => '杓', + 194781 => '𣏃', + 194782 => '㭉', + 194783 => '柺', + 194784 => '枅', + 194785 => '桒', + 194786 => '梅', + 194787 => '𣑭', + 194788 => '梎', + 194789 => '栟', + 194790 => '椔', + 194791 => '㮝', + 194792 => '楂', + 194793 => '榣', + 194794 => '槪', + 194795 => '檨', + 194796 => '𣚣', + 194797 => '櫛', + 194798 => '㰘', + 194799 => '次', + 194800 => '𣢧', + 194801 => '歔', + 194802 => '㱎', + 194803 => '歲', + 194804 => '殟', + 194805 => '殺', + 194806 => '殻', + 194807 => '𣪍', + 194808 => '𡴋', + 194809 => '𣫺', + 194810 => '汎', + 194811 => '𣲼', + 194812 => '沿', + 194813 => '泍', + 194814 => '汧', + 194815 => '洖', + 194816 => '派', + 194817 => '海', + 194818 => '流', + 194819 => '浩', + 194820 => '浸', + 194821 => '涅', + 194822 => '𣴞', + 194823 => '洴', + 194824 => '港', + 194825 => '湮', + 194826 => '㴳', + 194827 => '滋', + 194828 => '滇', + 194829 => '𣻑', + 194830 => '淹', + 194831 => '潮', + 194832 => '𣽞', + 194833 => '𣾎', + 194834 => '濆', + 194835 => '瀹', + 194836 => '瀞', + 194837 => '瀛', + 194838 => '㶖', + 194839 => '灊', + 194840 => '災', + 194841 => '灷', + 194842 => '炭', + 194843 => '𠔥', + 194844 => '煅', + 194845 => '𤉣', + 194846 => '熜', + 194848 => '爨', + 194849 => '爵', + 194850 => '牐', + 194851 => '𤘈', + 194852 => '犀', + 194853 => '犕', + 194854 => '𤜵', + 194855 => '𤠔', + 194856 => '獺', + 194857 => '王', + 194858 => '㺬', + 194859 => '玥', + 194860 => '㺸', + 194861 => '㺸', + 194862 => '瑇', + 194863 => '瑜', + 194864 => '瑱', + 194865 => '璅', + 194866 => '瓊', + 194867 => '㼛', + 194868 => '甤', + 194869 => '𤰶', + 194870 => '甾', + 194871 => '𤲒', + 194872 => '異', + 194873 => '𢆟', + 194874 => '瘐', + 194875 => '𤾡', + 194876 => '𤾸', + 194877 => '𥁄', + 194878 => '㿼', + 194879 => '䀈', + 194880 => '直', + 194881 => '𥃳', + 194882 => '𥃲', + 194883 => '𥄙', + 194884 => '𥄳', + 194885 => '眞', + 194886 => '真', + 194887 => '真', + 194888 => '睊', + 194889 => '䀹', + 194890 => '瞋', + 194891 => '䁆', + 194892 => '䂖', + 194893 => '𥐝', + 194894 => '硎', + 194895 => '碌', + 194896 => '磌', + 194897 => '䃣', + 194898 => '𥘦', + 194899 => '祖', + 194900 => '𥚚', + 194901 => '𥛅', + 194902 => '福', + 194903 => '秫', + 194904 => '䄯', + 194905 => '穀', + 194906 => '穊', + 194907 => '穏', + 194908 => '𥥼', + 194909 => '𥪧', + 194910 => '𥪧', + 194912 => '䈂', + 194913 => '𥮫', + 194914 => '篆', + 194915 => '築', + 194916 => '䈧', + 194917 => '𥲀', + 194918 => '糒', + 194919 => '䊠', + 194920 => '糨', + 194921 => '糣', + 194922 => '紀', + 194923 => '𥾆', + 194924 => '絣', + 194925 => '䌁', + 194926 => '緇', + 194927 => '縂', + 194928 => '繅', + 194929 => '䌴', + 194930 => '𦈨', + 194931 => '𦉇', + 194932 => '䍙', + 194933 => '𦋙', + 194934 => '罺', + 194935 => '𦌾', + 194936 => '羕', + 194937 => '翺', + 194938 => '者', + 194939 => '𦓚', + 194940 => '𦔣', + 194941 => '聠', + 194942 => '𦖨', + 194943 => '聰', + 194944 => '𣍟', + 194945 => '䏕', + 194946 => '育', + 194947 => '脃', + 194948 => '䐋', + 194949 => '脾', + 194950 => '媵', + 194951 => '𦞧', + 194952 => '𦞵', + 194953 => '𣎓', + 194954 => '𣎜', + 194955 => '舁', + 194956 => '舄', + 194957 => '辞', + 194958 => '䑫', + 194959 => '芑', + 194960 => '芋', + 194961 => '芝', + 194962 => '劳', + 194963 => '花', + 194964 => '芳', + 194965 => '芽', + 194966 => '苦', + 194967 => '𦬼', + 194968 => '若', + 194969 => '茝', + 194970 => '荣', + 194971 => '莭', + 194972 => '茣', + 194973 => '莽', + 194974 => '菧', + 194975 => '著', + 194976 => '荓', + 194977 => '菊', + 194978 => '菌', + 194979 => '菜', + 194980 => '𦰶', + 194981 => '𦵫', + 194982 => '𦳕', + 194983 => '䔫', + 194984 => '蓱', + 194985 => '蓳', + 194986 => '蔖', + 194987 => '𧏊', + 194988 => '蕤', + 194989 => '𦼬', + 194990 => '䕝', + 194991 => '䕡', + 194992 => '𦾱', + 194993 => '𧃒', + 194994 => '䕫', + 194995 => '虐', + 194996 => '虜', + 194997 => '虧', + 194998 => '虩', + 194999 => '蚩', + 195000 => '蚈', + 195001 => '蜎', + 195002 => '蛢', + 195003 => '蝹', + 195004 => '蜨', + 195005 => '蝫', + 195006 => '螆', + 195008 => '蟡', + 195009 => '蠁', + 195010 => '䗹', + 195011 => '衠', + 195012 => '衣', + 195013 => '𧙧', + 195014 => '裗', + 195015 => '裞', + 195016 => '䘵', + 195017 => '裺', + 195018 => '㒻', + 195019 => '𧢮', + 195020 => '𧥦', + 195021 => '䚾', + 195022 => '䛇', + 195023 => '誠', + 195024 => '諭', + 195025 => '變', + 195026 => '豕', + 195027 => '𧲨', + 195028 => '貫', + 195029 => '賁', + 195030 => '贛', + 195031 => '起', + 195032 => '𧼯', + 195033 => '𠠄', + 195034 => '跋', + 195035 => '趼', + 195036 => '跰', + 195037 => '𠣞', + 195038 => '軔', + 195039 => '輸', + 195040 => '𨗒', + 195041 => '𨗭', + 195042 => '邔', + 195043 => '郱', + 195044 => '鄑', + 195045 => '𨜮', + 195046 => '鄛', + 195047 => '鈸', + 195048 => '鋗', + 195049 => '鋘', + 195050 => '鉼', + 195051 => '鏹', + 195052 => '鐕', + 195053 => '𨯺', + 195054 => '開', + 195055 => '䦕', + 195056 => '閷', + 195057 => '𨵷', + 195058 => '䧦', + 195059 => '雃', + 195060 => '嶲', + 195061 => '霣', + 195062 => '𩅅', + 195063 => '𩈚', + 195064 => '䩮', + 195065 => '䩶', + 195066 => '韠', + 195067 => '𩐊', + 195068 => '䪲', + 195069 => '𩒖', + 195070 => '頋', + 195071 => '頋', + 195072 => '頩', + 195073 => '𩖶', + 195074 => '飢', + 195075 => '䬳', + 195076 => '餩', + 195077 => '馧', + 195078 => '駂', + 195079 => '駾', + 195080 => '䯎', + 195081 => '𩬰', + 195082 => '鬒', + 195083 => '鱀', + 195084 => '鳽', + 195085 => '䳎', + 195086 => '䳭', + 195087 => '鵧', + 195088 => '𪃎', + 195089 => '䳸', + 195090 => '𪄅', + 195091 => '𪈎', + 195092 => '𪊑', + 195093 => '麻', + 195094 => '䵖', + 195095 => '黹', + 195096 => '黾', + 195097 => '鼅', + 195098 => '鼏', + 195099 => '鼖', + 195100 => '鼻', + 195101 => '𪘀', +); diff --git a/3rdparty/symfony/polyfill-intl-idn/Resources/unidata/virama.php b/3rdparty/symfony/polyfill-intl-idn/Resources/unidata/virama.php new file mode 100644 index 00000000..1958e37e --- /dev/null +++ b/3rdparty/symfony/polyfill-intl-idn/Resources/unidata/virama.php @@ -0,0 +1,65 @@ + 9, + 2509 => 9, + 2637 => 9, + 2765 => 9, + 2893 => 9, + 3021 => 9, + 3149 => 9, + 3277 => 9, + 3387 => 9, + 3388 => 9, + 3405 => 9, + 3530 => 9, + 3642 => 9, + 3770 => 9, + 3972 => 9, + 4153 => 9, + 4154 => 9, + 5908 => 9, + 5940 => 9, + 6098 => 9, + 6752 => 9, + 6980 => 9, + 7082 => 9, + 7083 => 9, + 7154 => 9, + 7155 => 9, + 11647 => 9, + 43014 => 9, + 43052 => 9, + 43204 => 9, + 43347 => 9, + 43456 => 9, + 43766 => 9, + 44013 => 9, + 68159 => 9, + 69702 => 9, + 69759 => 9, + 69817 => 9, + 69939 => 9, + 69940 => 9, + 70080 => 9, + 70197 => 9, + 70378 => 9, + 70477 => 9, + 70722 => 9, + 70850 => 9, + 71103 => 9, + 71231 => 9, + 71350 => 9, + 71467 => 9, + 71737 => 9, + 71997 => 9, + 71998 => 9, + 72160 => 9, + 72244 => 9, + 72263 => 9, + 72345 => 9, + 72767 => 9, + 73028 => 9, + 73029 => 9, + 73111 => 9, +); diff --git a/3rdparty/symfony/polyfill-intl-idn/bootstrap.php b/3rdparty/symfony/polyfill-intl-idn/bootstrap.php new file mode 100644 index 00000000..57c78356 --- /dev/null +++ b/3rdparty/symfony/polyfill-intl-idn/bootstrap.php @@ -0,0 +1,145 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +use Symfony\Polyfill\Intl\Idn as p; + +if (extension_loaded('intl')) { + return; +} + +if (\PHP_VERSION_ID >= 80000) { + return require __DIR__.'/bootstrap80.php'; +} + +if (!defined('U_IDNA_PROHIBITED_ERROR')) { + define('U_IDNA_PROHIBITED_ERROR', 66560); +} +if (!defined('U_IDNA_ERROR_START')) { + define('U_IDNA_ERROR_START', 66560); +} +if (!defined('U_IDNA_UNASSIGNED_ERROR')) { + define('U_IDNA_UNASSIGNED_ERROR', 66561); +} +if (!defined('U_IDNA_CHECK_BIDI_ERROR')) { + define('U_IDNA_CHECK_BIDI_ERROR', 66562); +} +if (!defined('U_IDNA_STD3_ASCII_RULES_ERROR')) { + define('U_IDNA_STD3_ASCII_RULES_ERROR', 66563); +} +if (!defined('U_IDNA_ACE_PREFIX_ERROR')) { + define('U_IDNA_ACE_PREFIX_ERROR', 66564); +} +if (!defined('U_IDNA_VERIFICATION_ERROR')) { + define('U_IDNA_VERIFICATION_ERROR', 66565); +} +if (!defined('U_IDNA_LABEL_TOO_LONG_ERROR')) { + define('U_IDNA_LABEL_TOO_LONG_ERROR', 66566); +} +if (!defined('U_IDNA_ZERO_LENGTH_LABEL_ERROR')) { + define('U_IDNA_ZERO_LENGTH_LABEL_ERROR', 66567); +} +if (!defined('U_IDNA_DOMAIN_NAME_TOO_LONG_ERROR')) { + define('U_IDNA_DOMAIN_NAME_TOO_LONG_ERROR', 66568); +} +if (!defined('U_IDNA_ERROR_LIMIT')) { + define('U_IDNA_ERROR_LIMIT', 66569); +} +if (!defined('U_STRINGPREP_PROHIBITED_ERROR')) { + define('U_STRINGPREP_PROHIBITED_ERROR', 66560); +} +if (!defined('U_STRINGPREP_UNASSIGNED_ERROR')) { + define('U_STRINGPREP_UNASSIGNED_ERROR', 66561); +} +if (!defined('U_STRINGPREP_CHECK_BIDI_ERROR')) { + define('U_STRINGPREP_CHECK_BIDI_ERROR', 66562); +} +if (!defined('IDNA_DEFAULT')) { + define('IDNA_DEFAULT', 0); +} +if (!defined('IDNA_ALLOW_UNASSIGNED')) { + define('IDNA_ALLOW_UNASSIGNED', 1); +} +if (!defined('IDNA_USE_STD3_RULES')) { + define('IDNA_USE_STD3_RULES', 2); +} +if (!defined('IDNA_CHECK_BIDI')) { + define('IDNA_CHECK_BIDI', 4); +} +if (!defined('IDNA_CHECK_CONTEXTJ')) { + define('IDNA_CHECK_CONTEXTJ', 8); +} +if (!defined('IDNA_NONTRANSITIONAL_TO_ASCII')) { + define('IDNA_NONTRANSITIONAL_TO_ASCII', 16); +} +if (!defined('IDNA_NONTRANSITIONAL_TO_UNICODE')) { + define('IDNA_NONTRANSITIONAL_TO_UNICODE', 32); +} +if (!defined('INTL_IDNA_VARIANT_2003')) { + define('INTL_IDNA_VARIANT_2003', 0); +} +if (!defined('INTL_IDNA_VARIANT_UTS46')) { + define('INTL_IDNA_VARIANT_UTS46', 1); +} +if (!defined('IDNA_ERROR_EMPTY_LABEL')) { + define('IDNA_ERROR_EMPTY_LABEL', 1); +} +if (!defined('IDNA_ERROR_LABEL_TOO_LONG')) { + define('IDNA_ERROR_LABEL_TOO_LONG', 2); +} +if (!defined('IDNA_ERROR_DOMAIN_NAME_TOO_LONG')) { + define('IDNA_ERROR_DOMAIN_NAME_TOO_LONG', 4); +} +if (!defined('IDNA_ERROR_LEADING_HYPHEN')) { + define('IDNA_ERROR_LEADING_HYPHEN', 8); +} +if (!defined('IDNA_ERROR_TRAILING_HYPHEN')) { + define('IDNA_ERROR_TRAILING_HYPHEN', 16); +} +if (!defined('IDNA_ERROR_HYPHEN_3_4')) { + define('IDNA_ERROR_HYPHEN_3_4', 32); +} +if (!defined('IDNA_ERROR_LEADING_COMBINING_MARK')) { + define('IDNA_ERROR_LEADING_COMBINING_MARK', 64); +} +if (!defined('IDNA_ERROR_DISALLOWED')) { + define('IDNA_ERROR_DISALLOWED', 128); +} +if (!defined('IDNA_ERROR_PUNYCODE')) { + define('IDNA_ERROR_PUNYCODE', 256); +} +if (!defined('IDNA_ERROR_LABEL_HAS_DOT')) { + define('IDNA_ERROR_LABEL_HAS_DOT', 512); +} +if (!defined('IDNA_ERROR_INVALID_ACE_LABEL')) { + define('IDNA_ERROR_INVALID_ACE_LABEL', 1024); +} +if (!defined('IDNA_ERROR_BIDI')) { + define('IDNA_ERROR_BIDI', 2048); +} +if (!defined('IDNA_ERROR_CONTEXTJ')) { + define('IDNA_ERROR_CONTEXTJ', 4096); +} + +if (\PHP_VERSION_ID < 70400) { + if (!function_exists('idn_to_ascii')) { + function idn_to_ascii($domain, $flags = 0, $variant = \INTL_IDNA_VARIANT_2003, &$idna_info = null) { return p\Idn::idn_to_ascii($domain, $flags, $variant, $idna_info); } + } + if (!function_exists('idn_to_utf8')) { + function idn_to_utf8($domain, $flags = 0, $variant = \INTL_IDNA_VARIANT_2003, &$idna_info = null) { return p\Idn::idn_to_utf8($domain, $flags, $variant, $idna_info); } + } +} else { + if (!function_exists('idn_to_ascii')) { + function idn_to_ascii($domain, $flags = 0, $variant = \INTL_IDNA_VARIANT_UTS46, &$idna_info = null) { return p\Idn::idn_to_ascii($domain, $flags, $variant, $idna_info); } + } + if (!function_exists('idn_to_utf8')) { + function idn_to_utf8($domain, $flags = 0, $variant = \INTL_IDNA_VARIANT_UTS46, &$idna_info = null) { return p\Idn::idn_to_utf8($domain, $flags, $variant, $idna_info); } + } +} diff --git a/3rdparty/symfony/polyfill-intl-idn/bootstrap80.php b/3rdparty/symfony/polyfill-intl-idn/bootstrap80.php new file mode 100644 index 00000000..a62c2d69 --- /dev/null +++ b/3rdparty/symfony/polyfill-intl-idn/bootstrap80.php @@ -0,0 +1,125 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +use Symfony\Polyfill\Intl\Idn as p; + +if (!defined('U_IDNA_PROHIBITED_ERROR')) { + define('U_IDNA_PROHIBITED_ERROR', 66560); +} +if (!defined('U_IDNA_ERROR_START')) { + define('U_IDNA_ERROR_START', 66560); +} +if (!defined('U_IDNA_UNASSIGNED_ERROR')) { + define('U_IDNA_UNASSIGNED_ERROR', 66561); +} +if (!defined('U_IDNA_CHECK_BIDI_ERROR')) { + define('U_IDNA_CHECK_BIDI_ERROR', 66562); +} +if (!defined('U_IDNA_STD3_ASCII_RULES_ERROR')) { + define('U_IDNA_STD3_ASCII_RULES_ERROR', 66563); +} +if (!defined('U_IDNA_ACE_PREFIX_ERROR')) { + define('U_IDNA_ACE_PREFIX_ERROR', 66564); +} +if (!defined('U_IDNA_VERIFICATION_ERROR')) { + define('U_IDNA_VERIFICATION_ERROR', 66565); +} +if (!defined('U_IDNA_LABEL_TOO_LONG_ERROR')) { + define('U_IDNA_LABEL_TOO_LONG_ERROR', 66566); +} +if (!defined('U_IDNA_ZERO_LENGTH_LABEL_ERROR')) { + define('U_IDNA_ZERO_LENGTH_LABEL_ERROR', 66567); +} +if (!defined('U_IDNA_DOMAIN_NAME_TOO_LONG_ERROR')) { + define('U_IDNA_DOMAIN_NAME_TOO_LONG_ERROR', 66568); +} +if (!defined('U_IDNA_ERROR_LIMIT')) { + define('U_IDNA_ERROR_LIMIT', 66569); +} +if (!defined('U_STRINGPREP_PROHIBITED_ERROR')) { + define('U_STRINGPREP_PROHIBITED_ERROR', 66560); +} +if (!defined('U_STRINGPREP_UNASSIGNED_ERROR')) { + define('U_STRINGPREP_UNASSIGNED_ERROR', 66561); +} +if (!defined('U_STRINGPREP_CHECK_BIDI_ERROR')) { + define('U_STRINGPREP_CHECK_BIDI_ERROR', 66562); +} +if (!defined('IDNA_DEFAULT')) { + define('IDNA_DEFAULT', 0); +} +if (!defined('IDNA_ALLOW_UNASSIGNED')) { + define('IDNA_ALLOW_UNASSIGNED', 1); +} +if (!defined('IDNA_USE_STD3_RULES')) { + define('IDNA_USE_STD3_RULES', 2); +} +if (!defined('IDNA_CHECK_BIDI')) { + define('IDNA_CHECK_BIDI', 4); +} +if (!defined('IDNA_CHECK_CONTEXTJ')) { + define('IDNA_CHECK_CONTEXTJ', 8); +} +if (!defined('IDNA_NONTRANSITIONAL_TO_ASCII')) { + define('IDNA_NONTRANSITIONAL_TO_ASCII', 16); +} +if (!defined('IDNA_NONTRANSITIONAL_TO_UNICODE')) { + define('IDNA_NONTRANSITIONAL_TO_UNICODE', 32); +} +if (!defined('INTL_IDNA_VARIANT_UTS46')) { + define('INTL_IDNA_VARIANT_UTS46', 1); +} +if (!defined('IDNA_ERROR_EMPTY_LABEL')) { + define('IDNA_ERROR_EMPTY_LABEL', 1); +} +if (!defined('IDNA_ERROR_LABEL_TOO_LONG')) { + define('IDNA_ERROR_LABEL_TOO_LONG', 2); +} +if (!defined('IDNA_ERROR_DOMAIN_NAME_TOO_LONG')) { + define('IDNA_ERROR_DOMAIN_NAME_TOO_LONG', 4); +} +if (!defined('IDNA_ERROR_LEADING_HYPHEN')) { + define('IDNA_ERROR_LEADING_HYPHEN', 8); +} +if (!defined('IDNA_ERROR_TRAILING_HYPHEN')) { + define('IDNA_ERROR_TRAILING_HYPHEN', 16); +} +if (!defined('IDNA_ERROR_HYPHEN_3_4')) { + define('IDNA_ERROR_HYPHEN_3_4', 32); +} +if (!defined('IDNA_ERROR_LEADING_COMBINING_MARK')) { + define('IDNA_ERROR_LEADING_COMBINING_MARK', 64); +} +if (!defined('IDNA_ERROR_DISALLOWED')) { + define('IDNA_ERROR_DISALLOWED', 128); +} +if (!defined('IDNA_ERROR_PUNYCODE')) { + define('IDNA_ERROR_PUNYCODE', 256); +} +if (!defined('IDNA_ERROR_LABEL_HAS_DOT')) { + define('IDNA_ERROR_LABEL_HAS_DOT', 512); +} +if (!defined('IDNA_ERROR_INVALID_ACE_LABEL')) { + define('IDNA_ERROR_INVALID_ACE_LABEL', 1024); +} +if (!defined('IDNA_ERROR_BIDI')) { + define('IDNA_ERROR_BIDI', 2048); +} +if (!defined('IDNA_ERROR_CONTEXTJ')) { + define('IDNA_ERROR_CONTEXTJ', 4096); +} + +if (!function_exists('idn_to_ascii')) { + function idn_to_ascii(?string $domain, ?int $flags = IDNA_DEFAULT, ?int $variant = INTL_IDNA_VARIANT_UTS46, &$idna_info = null): string|false { return p\Idn::idn_to_ascii((string) $domain, (int) $flags, (int) $variant, $idna_info); } +} +if (!function_exists('idn_to_utf8')) { + function idn_to_utf8(?string $domain, ?int $flags = IDNA_DEFAULT, ?int $variant = INTL_IDNA_VARIANT_UTS46, &$idna_info = null): string|false { return p\Idn::idn_to_utf8((string) $domain, (int) $flags, (int) $variant, $idna_info); } +} diff --git a/3rdparty/symfony/polyfill-intl-normalizer/LICENSE b/3rdparty/symfony/polyfill-intl-normalizer/LICENSE new file mode 100644 index 00000000..6e3afce6 --- /dev/null +++ b/3rdparty/symfony/polyfill-intl-normalizer/LICENSE @@ -0,0 +1,19 @@ +Copyright (c) 2015-present Fabien Potencier + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is furnished +to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +THE SOFTWARE. diff --git a/3rdparty/symfony/polyfill-intl-normalizer/Normalizer.php b/3rdparty/symfony/polyfill-intl-normalizer/Normalizer.php new file mode 100644 index 00000000..81704ab3 --- /dev/null +++ b/3rdparty/symfony/polyfill-intl-normalizer/Normalizer.php @@ -0,0 +1,310 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Polyfill\Intl\Normalizer; + +/** + * Normalizer is a PHP fallback implementation of the Normalizer class provided by the intl extension. + * + * It has been validated with Unicode 6.3 Normalization Conformance Test. + * See http://www.unicode.org/reports/tr15/ for detailed info about Unicode normalizations. + * + * @author Nicolas Grekas + * + * @internal + */ +class Normalizer +{ + public const FORM_D = \Normalizer::FORM_D; + public const FORM_KD = \Normalizer::FORM_KD; + public const FORM_C = \Normalizer::FORM_C; + public const FORM_KC = \Normalizer::FORM_KC; + public const NFD = \Normalizer::NFD; + public const NFKD = \Normalizer::NFKD; + public const NFC = \Normalizer::NFC; + public const NFKC = \Normalizer::NFKC; + + private static $C; + private static $D; + private static $KD; + private static $cC; + private static $ulenMask = ["\xC0" => 2, "\xD0" => 2, "\xE0" => 3, "\xF0" => 4]; + private static $ASCII = "\x20\x65\x69\x61\x73\x6E\x74\x72\x6F\x6C\x75\x64\x5D\x5B\x63\x6D\x70\x27\x0A\x67\x7C\x68\x76\x2E\x66\x62\x2C\x3A\x3D\x2D\x71\x31\x30\x43\x32\x2A\x79\x78\x29\x28\x4C\x39\x41\x53\x2F\x50\x22\x45\x6A\x4D\x49\x6B\x33\x3E\x35\x54\x3C\x44\x34\x7D\x42\x7B\x38\x46\x77\x52\x36\x37\x55\x47\x4E\x3B\x4A\x7A\x56\x23\x48\x4F\x57\x5F\x26\x21\x4B\x3F\x58\x51\x25\x59\x5C\x09\x5A\x2B\x7E\x5E\x24\x40\x60\x7F\x00\x01\x02\x03\x04\x05\x06\x07\x08\x0B\x0C\x0D\x0E\x0F\x10\x11\x12\x13\x14\x15\x16\x17\x18\x19\x1A\x1B\x1C\x1D\x1E\x1F"; + + public static function isNormalized(string $s, int $form = self::FORM_C) + { + if (!\in_array($form, [self::NFD, self::NFKD, self::NFC, self::NFKC])) { + return false; + } + if (!isset($s[strspn($s, self::$ASCII)])) { + return true; + } + if (self::NFC == $form && preg_match('//u', $s) && !preg_match('/[^\x00-\x{2FF}]/u', $s)) { + return true; + } + + return self::normalize($s, $form) === $s; + } + + public static function normalize(string $s, int $form = self::FORM_C) + { + if (!preg_match('//u', $s)) { + return false; + } + + switch ($form) { + case self::NFC: $C = true; $K = false; break; + case self::NFD: $C = false; $K = false; break; + case self::NFKC: $C = true; $K = true; break; + case self::NFKD: $C = false; $K = true; break; + default: + if (\defined('Normalizer::NONE') && \Normalizer::NONE == $form) { + return $s; + } + + if (80000 > \PHP_VERSION_ID) { + return false; + } + + throw new \ValueError('normalizer_normalize(): Argument #2 ($form) must be a a valid normalization form'); + } + + if ('' === $s) { + return ''; + } + + if ($K && null === self::$KD) { + self::$KD = self::getData('compatibilityDecomposition'); + } + + if (null === self::$D) { + self::$D = self::getData('canonicalDecomposition'); + self::$cC = self::getData('combiningClass'); + } + + if (null !== $mbEncoding = (2 /* MB_OVERLOAD_STRING */ & (int) \ini_get('mbstring.func_overload')) ? mb_internal_encoding() : null) { + mb_internal_encoding('8bit'); + } + + $r = self::decompose($s, $K); + + if ($C) { + if (null === self::$C) { + self::$C = self::getData('canonicalComposition'); + } + + $r = self::recompose($r); + } + if (null !== $mbEncoding) { + mb_internal_encoding($mbEncoding); + } + + return $r; + } + + private static function recompose($s) + { + $ASCII = self::$ASCII; + $compMap = self::$C; + $combClass = self::$cC; + $ulenMask = self::$ulenMask; + + $result = $tail = ''; + + $i = $s[0] < "\x80" ? 1 : $ulenMask[$s[0] & "\xF0"]; + $len = \strlen($s); + + $lastUchr = substr($s, 0, $i); + $lastUcls = isset($combClass[$lastUchr]) ? 256 : 0; + + while ($i < $len) { + if ($s[$i] < "\x80") { + // ASCII chars + + if ($tail) { + $lastUchr .= $tail; + $tail = ''; + } + + if ($j = strspn($s, $ASCII, $i + 1)) { + $lastUchr .= substr($s, $i, $j); + $i += $j; + } + + $result .= $lastUchr; + $lastUchr = $s[$i]; + $lastUcls = 0; + ++$i; + continue; + } + + $ulen = $ulenMask[$s[$i] & "\xF0"]; + $uchr = substr($s, $i, $ulen); + + if ($lastUchr < "\xE1\x84\x80" || "\xE1\x84\x92" < $lastUchr + || $uchr < "\xE1\x85\xA1" || "\xE1\x85\xB5" < $uchr + || $lastUcls) { + // Table lookup and combining chars composition + + $ucls = $combClass[$uchr] ?? 0; + + if (isset($compMap[$lastUchr.$uchr]) && (!$lastUcls || $lastUcls < $ucls)) { + $lastUchr = $compMap[$lastUchr.$uchr]; + } elseif ($lastUcls = $ucls) { + $tail .= $uchr; + } else { + if ($tail) { + $lastUchr .= $tail; + $tail = ''; + } + + $result .= $lastUchr; + $lastUchr = $uchr; + } + } else { + // Hangul chars + + $L = \ord($lastUchr[2]) - 0x80; + $V = \ord($uchr[2]) - 0xA1; + $T = 0; + + $uchr = substr($s, $i + $ulen, 3); + + if ("\xE1\x86\xA7" <= $uchr && $uchr <= "\xE1\x87\x82") { + $T = \ord($uchr[2]) - 0xA7; + 0 > $T && $T += 0x40; + $ulen += 3; + } + + $L = 0xAC00 + ($L * 21 + $V) * 28 + $T; + $lastUchr = \chr(0xE0 | $L >> 12).\chr(0x80 | $L >> 6 & 0x3F).\chr(0x80 | $L & 0x3F); + } + + $i += $ulen; + } + + return $result.$lastUchr.$tail; + } + + private static function decompose($s, $c) + { + $result = ''; + + $ASCII = self::$ASCII; + $decompMap = self::$D; + $combClass = self::$cC; + $ulenMask = self::$ulenMask; + if ($c) { + $compatMap = self::$KD; + } + + $c = []; + $i = 0; + $len = \strlen($s); + + while ($i < $len) { + if ($s[$i] < "\x80") { + // ASCII chars + + if ($c) { + ksort($c); + $result .= implode('', $c); + $c = []; + } + + $j = 1 + strspn($s, $ASCII, $i + 1); + $result .= substr($s, $i, $j); + $i += $j; + continue; + } + + $ulen = $ulenMask[$s[$i] & "\xF0"]; + $uchr = substr($s, $i, $ulen); + $i += $ulen; + + if ($uchr < "\xEA\xB0\x80" || "\xED\x9E\xA3" < $uchr) { + // Table lookup + + if ($uchr !== $j = $compatMap[$uchr] ?? ($decompMap[$uchr] ?? $uchr)) { + $uchr = $j; + + $j = \strlen($uchr); + $ulen = $uchr[0] < "\x80" ? 1 : $ulenMask[$uchr[0] & "\xF0"]; + + if ($ulen != $j) { + // Put trailing chars in $s + + $j -= $ulen; + $i -= $j; + + if (0 > $i) { + $s = str_repeat(' ', -$i).$s; + $len -= $i; + $i = 0; + } + + while ($j--) { + $s[$i + $j] = $uchr[$ulen + $j]; + } + + $uchr = substr($uchr, 0, $ulen); + } + } + if (isset($combClass[$uchr])) { + // Combining chars, for sorting + + if (!isset($c[$combClass[$uchr]])) { + $c[$combClass[$uchr]] = ''; + } + $c[$combClass[$uchr]] .= $uchr; + continue; + } + } else { + // Hangul chars + + $uchr = unpack('C*', $uchr); + $j = (($uchr[1] - 224) << 12) + (($uchr[2] - 128) << 6) + $uchr[3] - 0xAC80; + + $uchr = "\xE1\x84".\chr(0x80 + (int) ($j / 588)) + ."\xE1\x85".\chr(0xA1 + (int) (($j % 588) / 28)); + + if ($j %= 28) { + $uchr .= $j < 25 + ? ("\xE1\x86".\chr(0xA7 + $j)) + : ("\xE1\x87".\chr(0x67 + $j)); + } + } + if ($c) { + ksort($c); + $result .= implode('', $c); + $c = []; + } + + $result .= $uchr; + } + + if ($c) { + ksort($c); + $result .= implode('', $c); + } + + return $result; + } + + private static function getData($file) + { + if (file_exists($file = __DIR__.'/Resources/unidata/'.$file.'.php')) { + return require $file; + } + + return false; + } +} diff --git a/3rdparty/symfony/polyfill-intl-normalizer/Resources/stubs/Normalizer.php b/3rdparty/symfony/polyfill-intl-normalizer/Resources/stubs/Normalizer.php new file mode 100644 index 00000000..0fdfc890 --- /dev/null +++ b/3rdparty/symfony/polyfill-intl-normalizer/Resources/stubs/Normalizer.php @@ -0,0 +1,17 @@ + 'À', + 'Á' => 'Á', + 'Â' => 'Â', + 'Ã' => 'Ã', + 'Ä' => 'Ä', + 'Å' => 'Å', + 'Ç' => 'Ç', + 'È' => 'È', + 'É' => 'É', + 'Ê' => 'Ê', + 'Ë' => 'Ë', + 'Ì' => 'Ì', + 'Í' => 'Í', + 'Î' => 'Î', + 'Ï' => 'Ï', + 'Ñ' => 'Ñ', + 'Ò' => 'Ò', + 'Ó' => 'Ó', + 'Ô' => 'Ô', + 'Õ' => 'Õ', + 'Ö' => 'Ö', + 'Ù' => 'Ù', + 'Ú' => 'Ú', + 'Û' => 'Û', + 'Ü' => 'Ü', + 'Ý' => 'Ý', + 'à' => 'à', + 'á' => 'á', + 'â' => 'â', + 'ã' => 'ã', + 'ä' => 'ä', + 'å' => 'å', + 'ç' => 'ç', + 'è' => 'è', + 'é' => 'é', + 'ê' => 'ê', + 'ë' => 'ë', + 'ì' => 'ì', + 'í' => 'í', + 'î' => 'î', + 'ï' => 'ï', + 'ñ' => 'ñ', + 'ò' => 'ò', + 'ó' => 'ó', + 'ô' => 'ô', + 'õ' => 'õ', + 'ö' => 'ö', + 'ù' => 'ù', + 'ú' => 'ú', + 'û' => 'û', + 'ü' => 'ü', + 'ý' => 'ý', + 'ÿ' => 'ÿ', + 'Ā' => 'Ā', + 'ā' => 'ā', + 'Ă' => 'Ă', + 'ă' => 'ă', + 'Ą' => 'Ą', + 'ą' => 'ą', + 'Ć' => 'Ć', + 'ć' => 'ć', + 'Ĉ' => 'Ĉ', + 'ĉ' => 'ĉ', + 'Ċ' => 'Ċ', + 'ċ' => 'ċ', + 'Č' => 'Č', + 'č' => 'č', + 'Ď' => 'Ď', + 'ď' => 'ď', + 'Ē' => 'Ē', + 'ē' => 'ē', + 'Ĕ' => 'Ĕ', + 'ĕ' => 'ĕ', + 'Ė' => 'Ė', + 'ė' => 'ė', + 'Ę' => 'Ę', + 'ę' => 'ę', + 'Ě' => 'Ě', + 'ě' => 'ě', + 'Ĝ' => 'Ĝ', + 'ĝ' => 'ĝ', + 'Ğ' => 'Ğ', + 'ğ' => 'ğ', + 'Ġ' => 'Ġ', + 'ġ' => 'ġ', + 'Ģ' => 'Ģ', + 'ģ' => 'ģ', + 'Ĥ' => 'Ĥ', + 'ĥ' => 'ĥ', + 'Ĩ' => 'Ĩ', + 'ĩ' => 'ĩ', + 'Ī' => 'Ī', + 'ī' => 'ī', + 'Ĭ' => 'Ĭ', + 'ĭ' => 'ĭ', + 'Į' => 'Į', + 'į' => 'į', + 'İ' => 'İ', + 'Ĵ' => 'Ĵ', + 'ĵ' => 'ĵ', + 'Ķ' => 'Ķ', + 'ķ' => 'ķ', + 'Ĺ' => 'Ĺ', + 'ĺ' => 'ĺ', + 'Ļ' => 'Ļ', + 'ļ' => 'ļ', + 'Ľ' => 'Ľ', + 'ľ' => 'ľ', + 'Ń' => 'Ń', + 'ń' => 'ń', + 'Ņ' => 'Ņ', + 'ņ' => 'ņ', + 'Ň' => 'Ň', + 'ň' => 'ň', + 'Ō' => 'Ō', + 'ō' => 'ō', + 'Ŏ' => 'Ŏ', + 'ŏ' => 'ŏ', + 'Ő' => 'Ő', + 'ő' => 'ő', + 'Ŕ' => 'Ŕ', + 'ŕ' => 'ŕ', + 'Ŗ' => 'Ŗ', + 'ŗ' => 'ŗ', + 'Ř' => 'Ř', + 'ř' => 'ř', + 'Ś' => 'Ś', + 'ś' => 'ś', + 'Ŝ' => 'Ŝ', + 'ŝ' => 'ŝ', + 'Ş' => 'Ş', + 'ş' => 'ş', + 'Š' => 'Š', + 'š' => 'š', + 'Ţ' => 'Ţ', + 'ţ' => 'ţ', + 'Ť' => 'Ť', + 'ť' => 'ť', + 'Ũ' => 'Ũ', + 'ũ' => 'ũ', + 'Ū' => 'Ū', + 'ū' => 'ū', + 'Ŭ' => 'Ŭ', + 'ŭ' => 'ŭ', + 'Ů' => 'Ů', + 'ů' => 'ů', + 'Ű' => 'Ű', + 'ű' => 'ű', + 'Ų' => 'Ų', + 'ų' => 'ų', + 'Ŵ' => 'Ŵ', + 'ŵ' => 'ŵ', + 'Ŷ' => 'Ŷ', + 'ŷ' => 'ŷ', + 'Ÿ' => 'Ÿ', + 'Ź' => 'Ź', + 'ź' => 'ź', + 'Ż' => 'Ż', + 'ż' => 'ż', + 'Ž' => 'Ž', + 'ž' => 'ž', + 'Ơ' => 'Ơ', + 'ơ' => 'ơ', + 'Ư' => 'Ư', + 'ư' => 'ư', + 'Ǎ' => 'Ǎ', + 'ǎ' => 'ǎ', + 'Ǐ' => 'Ǐ', + 'ǐ' => 'ǐ', + 'Ǒ' => 'Ǒ', + 'ǒ' => 'ǒ', + 'Ǔ' => 'Ǔ', + 'ǔ' => 'ǔ', + 'Ǖ' => 'Ǖ', + 'ǖ' => 'ǖ', + 'Ǘ' => 'Ǘ', + 'ǘ' => 'ǘ', + 'Ǚ' => 'Ǚ', + 'ǚ' => 'ǚ', + 'Ǜ' => 'Ǜ', + 'ǜ' => 'ǜ', + 'Ǟ' => 'Ǟ', + 'ǟ' => 'ǟ', + 'Ǡ' => 'Ǡ', + 'ǡ' => 'ǡ', + 'Ǣ' => 'Ǣ', + 'ǣ' => 'ǣ', + 'Ǧ' => 'Ǧ', + 'ǧ' => 'ǧ', + 'Ǩ' => 'Ǩ', + 'ǩ' => 'ǩ', + 'Ǫ' => 'Ǫ', + 'ǫ' => 'ǫ', + 'Ǭ' => 'Ǭ', + 'ǭ' => 'ǭ', + 'Ǯ' => 'Ǯ', + 'ǯ' => 'ǯ', + 'ǰ' => 'ǰ', + 'Ǵ' => 'Ǵ', + 'ǵ' => 'ǵ', + 'Ǹ' => 'Ǹ', + 'ǹ' => 'ǹ', + 'Ǻ' => 'Ǻ', + 'ǻ' => 'ǻ', + 'Ǽ' => 'Ǽ', + 'ǽ' => 'ǽ', + 'Ǿ' => 'Ǿ', + 'ǿ' => 'ǿ', + 'Ȁ' => 'Ȁ', + 'ȁ' => 'ȁ', + 'Ȃ' => 'Ȃ', + 'ȃ' => 'ȃ', + 'Ȅ' => 'Ȅ', + 'ȅ' => 'ȅ', + 'Ȇ' => 'Ȇ', + 'ȇ' => 'ȇ', + 'Ȉ' => 'Ȉ', + 'ȉ' => 'ȉ', + 'Ȋ' => 'Ȋ', + 'ȋ' => 'ȋ', + 'Ȍ' => 'Ȍ', + 'ȍ' => 'ȍ', + 'Ȏ' => 'Ȏ', + 'ȏ' => 'ȏ', + 'Ȑ' => 'Ȑ', + 'ȑ' => 'ȑ', + 'Ȓ' => 'Ȓ', + 'ȓ' => 'ȓ', + 'Ȕ' => 'Ȕ', + 'ȕ' => 'ȕ', + 'Ȗ' => 'Ȗ', + 'ȗ' => 'ȗ', + 'Ș' => 'Ș', + 'ș' => 'ș', + 'Ț' => 'Ț', + 'ț' => 'ț', + 'Ȟ' => 'Ȟ', + 'ȟ' => 'ȟ', + 'Ȧ' => 'Ȧ', + 'ȧ' => 'ȧ', + 'Ȩ' => 'Ȩ', + 'ȩ' => 'ȩ', + 'Ȫ' => 'Ȫ', + 'ȫ' => 'ȫ', + 'Ȭ' => 'Ȭ', + 'ȭ' => 'ȭ', + 'Ȯ' => 'Ȯ', + 'ȯ' => 'ȯ', + 'Ȱ' => 'Ȱ', + 'ȱ' => 'ȱ', + 'Ȳ' => 'Ȳ', + 'ȳ' => 'ȳ', + '΅' => '΅', + 'Ά' => 'Ά', + 'Έ' => 'Έ', + 'Ή' => 'Ή', + 'Ί' => 'Ί', + 'Ό' => 'Ό', + 'Ύ' => 'Ύ', + 'Ώ' => 'Ώ', + 'ΐ' => 'ΐ', + 'Ϊ' => 'Ϊ', + 'Ϋ' => 'Ϋ', + 'ά' => 'ά', + 'έ' => 'έ', + 'ή' => 'ή', + 'ί' => 'ί', + 'ΰ' => 'ΰ', + 'ϊ' => 'ϊ', + 'ϋ' => 'ϋ', + 'ό' => 'ό', + 'ύ' => 'ύ', + 'ώ' => 'ώ', + 'ϓ' => 'ϓ', + 'ϔ' => 'ϔ', + 'Ѐ' => 'Ѐ', + 'Ё' => 'Ё', + 'Ѓ' => 'Ѓ', + 'Ї' => 'Ї', + 'Ќ' => 'Ќ', + 'Ѝ' => 'Ѝ', + 'Ў' => 'Ў', + 'Й' => 'Й', + 'й' => 'й', + 'ѐ' => 'ѐ', + 'ё' => 'ё', + 'ѓ' => 'ѓ', + 'ї' => 'ї', + 'ќ' => 'ќ', + 'ѝ' => 'ѝ', + 'ў' => 'ў', + 'Ѷ' => 'Ѷ', + 'ѷ' => 'ѷ', + 'Ӂ' => 'Ӂ', + 'ӂ' => 'ӂ', + 'Ӑ' => 'Ӑ', + 'ӑ' => 'ӑ', + 'Ӓ' => 'Ӓ', + 'ӓ' => 'ӓ', + 'Ӗ' => 'Ӗ', + 'ӗ' => 'ӗ', + 'Ӛ' => 'Ӛ', + 'ӛ' => 'ӛ', + 'Ӝ' => 'Ӝ', + 'ӝ' => 'ӝ', + 'Ӟ' => 'Ӟ', + 'ӟ' => 'ӟ', + 'Ӣ' => 'Ӣ', + 'ӣ' => 'ӣ', + 'Ӥ' => 'Ӥ', + 'ӥ' => 'ӥ', + 'Ӧ' => 'Ӧ', + 'ӧ' => 'ӧ', + 'Ӫ' => 'Ӫ', + 'ӫ' => 'ӫ', + 'Ӭ' => 'Ӭ', + 'ӭ' => 'ӭ', + 'Ӯ' => 'Ӯ', + 'ӯ' => 'ӯ', + 'Ӱ' => 'Ӱ', + 'ӱ' => 'ӱ', + 'Ӳ' => 'Ӳ', + 'ӳ' => 'ӳ', + 'Ӵ' => 'Ӵ', + 'ӵ' => 'ӵ', + 'Ӹ' => 'Ӹ', + 'ӹ' => 'ӹ', + 'آ' => 'آ', + 'أ' => 'أ', + 'ؤ' => 'ؤ', + 'إ' => 'إ', + 'ئ' => 'ئ', + 'ۀ' => 'ۀ', + 'ۂ' => 'ۂ', + 'ۓ' => 'ۓ', + 'ऩ' => 'ऩ', + 'ऱ' => 'ऱ', + 'ऴ' => 'ऴ', + 'ো' => 'ো', + 'ৌ' => 'ৌ', + 'ୈ' => 'ୈ', + 'ୋ' => 'ୋ', + 'ୌ' => 'ୌ', + 'ஔ' => 'ஔ', + 'ொ' => 'ொ', + 'ோ' => 'ோ', + 'ௌ' => 'ௌ', + 'ై' => 'ై', + 'ೀ' => 'ೀ', + 'ೇ' => 'ೇ', + 'ೈ' => 'ೈ', + 'ೊ' => 'ೊ', + 'ೋ' => 'ೋ', + 'ൊ' => 'ൊ', + 'ോ' => 'ോ', + 'ൌ' => 'ൌ', + 'ේ' => 'ේ', + 'ො' => 'ො', + 'ෝ' => 'ෝ', + 'ෞ' => 'ෞ', + 'ဦ' => 'ဦ', + 'ᬆ' => 'ᬆ', + 'ᬈ' => 'ᬈ', + 'ᬊ' => 'ᬊ', + 'ᬌ' => 'ᬌ', + 'ᬎ' => 'ᬎ', + 'ᬒ' => 'ᬒ', + 'ᬻ' => 'ᬻ', + 'ᬽ' => 'ᬽ', + 'ᭀ' => 'ᭀ', + 'ᭁ' => 'ᭁ', + 'ᭃ' => 'ᭃ', + 'Ḁ' => 'Ḁ', + 'ḁ' => 'ḁ', + 'Ḃ' => 'Ḃ', + 'ḃ' => 'ḃ', + 'Ḅ' => 'Ḅ', + 'ḅ' => 'ḅ', + 'Ḇ' => 'Ḇ', + 'ḇ' => 'ḇ', + 'Ḉ' => 'Ḉ', + 'ḉ' => 'ḉ', + 'Ḋ' => 'Ḋ', + 'ḋ' => 'ḋ', + 'Ḍ' => 'Ḍ', + 'ḍ' => 'ḍ', + 'Ḏ' => 'Ḏ', + 'ḏ' => 'ḏ', + 'Ḑ' => 'Ḑ', + 'ḑ' => 'ḑ', + 'Ḓ' => 'Ḓ', + 'ḓ' => 'ḓ', + 'Ḕ' => 'Ḕ', + 'ḕ' => 'ḕ', + 'Ḗ' => 'Ḗ', + 'ḗ' => 'ḗ', + 'Ḙ' => 'Ḙ', + 'ḙ' => 'ḙ', + 'Ḛ' => 'Ḛ', + 'ḛ' => 'ḛ', + 'Ḝ' => 'Ḝ', + 'ḝ' => 'ḝ', + 'Ḟ' => 'Ḟ', + 'ḟ' => 'ḟ', + 'Ḡ' => 'Ḡ', + 'ḡ' => 'ḡ', + 'Ḣ' => 'Ḣ', + 'ḣ' => 'ḣ', + 'Ḥ' => 'Ḥ', + 'ḥ' => 'ḥ', + 'Ḧ' => 'Ḧ', + 'ḧ' => 'ḧ', + 'Ḩ' => 'Ḩ', + 'ḩ' => 'ḩ', + 'Ḫ' => 'Ḫ', + 'ḫ' => 'ḫ', + 'Ḭ' => 'Ḭ', + 'ḭ' => 'ḭ', + 'Ḯ' => 'Ḯ', + 'ḯ' => 'ḯ', + 'Ḱ' => 'Ḱ', + 'ḱ' => 'ḱ', + 'Ḳ' => 'Ḳ', + 'ḳ' => 'ḳ', + 'Ḵ' => 'Ḵ', + 'ḵ' => 'ḵ', + 'Ḷ' => 'Ḷ', + 'ḷ' => 'ḷ', + 'Ḹ' => 'Ḹ', + 'ḹ' => 'ḹ', + 'Ḻ' => 'Ḻ', + 'ḻ' => 'ḻ', + 'Ḽ' => 'Ḽ', + 'ḽ' => 'ḽ', + 'Ḿ' => 'Ḿ', + 'ḿ' => 'ḿ', + 'Ṁ' => 'Ṁ', + 'ṁ' => 'ṁ', + 'Ṃ' => 'Ṃ', + 'ṃ' => 'ṃ', + 'Ṅ' => 'Ṅ', + 'ṅ' => 'ṅ', + 'Ṇ' => 'Ṇ', + 'ṇ' => 'ṇ', + 'Ṉ' => 'Ṉ', + 'ṉ' => 'ṉ', + 'Ṋ' => 'Ṋ', + 'ṋ' => 'ṋ', + 'Ṍ' => 'Ṍ', + 'ṍ' => 'ṍ', + 'Ṏ' => 'Ṏ', + 'ṏ' => 'ṏ', + 'Ṑ' => 'Ṑ', + 'ṑ' => 'ṑ', + 'Ṓ' => 'Ṓ', + 'ṓ' => 'ṓ', + 'Ṕ' => 'Ṕ', + 'ṕ' => 'ṕ', + 'Ṗ' => 'Ṗ', + 'ṗ' => 'ṗ', + 'Ṙ' => 'Ṙ', + 'ṙ' => 'ṙ', + 'Ṛ' => 'Ṛ', + 'ṛ' => 'ṛ', + 'Ṝ' => 'Ṝ', + 'ṝ' => 'ṝ', + 'Ṟ' => 'Ṟ', + 'ṟ' => 'ṟ', + 'Ṡ' => 'Ṡ', + 'ṡ' => 'ṡ', + 'Ṣ' => 'Ṣ', + 'ṣ' => 'ṣ', + 'Ṥ' => 'Ṥ', + 'ṥ' => 'ṥ', + 'Ṧ' => 'Ṧ', + 'ṧ' => 'ṧ', + 'Ṩ' => 'Ṩ', + 'ṩ' => 'ṩ', + 'Ṫ' => 'Ṫ', + 'ṫ' => 'ṫ', + 'Ṭ' => 'Ṭ', + 'ṭ' => 'ṭ', + 'Ṯ' => 'Ṯ', + 'ṯ' => 'ṯ', + 'Ṱ' => 'Ṱ', + 'ṱ' => 'ṱ', + 'Ṳ' => 'Ṳ', + 'ṳ' => 'ṳ', + 'Ṵ' => 'Ṵ', + 'ṵ' => 'ṵ', + 'Ṷ' => 'Ṷ', + 'ṷ' => 'ṷ', + 'Ṹ' => 'Ṹ', + 'ṹ' => 'ṹ', + 'Ṻ' => 'Ṻ', + 'ṻ' => 'ṻ', + 'Ṽ' => 'Ṽ', + 'ṽ' => 'ṽ', + 'Ṿ' => 'Ṿ', + 'ṿ' => 'ṿ', + 'Ẁ' => 'Ẁ', + 'ẁ' => 'ẁ', + 'Ẃ' => 'Ẃ', + 'ẃ' => 'ẃ', + 'Ẅ' => 'Ẅ', + 'ẅ' => 'ẅ', + 'Ẇ' => 'Ẇ', + 'ẇ' => 'ẇ', + 'Ẉ' => 'Ẉ', + 'ẉ' => 'ẉ', + 'Ẋ' => 'Ẋ', + 'ẋ' => 'ẋ', + 'Ẍ' => 'Ẍ', + 'ẍ' => 'ẍ', + 'Ẏ' => 'Ẏ', + 'ẏ' => 'ẏ', + 'Ẑ' => 'Ẑ', + 'ẑ' => 'ẑ', + 'Ẓ' => 'Ẓ', + 'ẓ' => 'ẓ', + 'Ẕ' => 'Ẕ', + 'ẕ' => 'ẕ', + 'ẖ' => 'ẖ', + 'ẗ' => 'ẗ', + 'ẘ' => 'ẘ', + 'ẙ' => 'ẙ', + 'ẛ' => 'ẛ', + 'Ạ' => 'Ạ', + 'ạ' => 'ạ', + 'Ả' => 'Ả', + 'ả' => 'ả', + 'Ấ' => 'Ấ', + 'ấ' => 'ấ', + 'Ầ' => 'Ầ', + 'ầ' => 'ầ', + 'Ẩ' => 'Ẩ', + 'ẩ' => 'ẩ', + 'Ẫ' => 'Ẫ', + 'ẫ' => 'ẫ', + 'Ậ' => 'Ậ', + 'ậ' => 'ậ', + 'Ắ' => 'Ắ', + 'ắ' => 'ắ', + 'Ằ' => 'Ằ', + 'ằ' => 'ằ', + 'Ẳ' => 'Ẳ', + 'ẳ' => 'ẳ', + 'Ẵ' => 'Ẵ', + 'ẵ' => 'ẵ', + 'Ặ' => 'Ặ', + 'ặ' => 'ặ', + 'Ẹ' => 'Ẹ', + 'ẹ' => 'ẹ', + 'Ẻ' => 'Ẻ', + 'ẻ' => 'ẻ', + 'Ẽ' => 'Ẽ', + 'ẽ' => 'ẽ', + 'Ế' => 'Ế', + 'ế' => 'ế', + 'Ề' => 'Ề', + 'ề' => 'ề', + 'Ể' => 'Ể', + 'ể' => 'ể', + 'Ễ' => 'Ễ', + 'ễ' => 'ễ', + 'Ệ' => 'Ệ', + 'ệ' => 'ệ', + 'Ỉ' => 'Ỉ', + 'ỉ' => 'ỉ', + 'Ị' => 'Ị', + 'ị' => 'ị', + 'Ọ' => 'Ọ', + 'ọ' => 'ọ', + 'Ỏ' => 'Ỏ', + 'ỏ' => 'ỏ', + 'Ố' => 'Ố', + 'ố' => 'ố', + 'Ồ' => 'Ồ', + 'ồ' => 'ồ', + 'Ổ' => 'Ổ', + 'ổ' => 'ổ', + 'Ỗ' => 'Ỗ', + 'ỗ' => 'ỗ', + 'Ộ' => 'Ộ', + 'ộ' => 'ộ', + 'Ớ' => 'Ớ', + 'ớ' => 'ớ', + 'Ờ' => 'Ờ', + 'ờ' => 'ờ', + 'Ở' => 'Ở', + 'ở' => 'ở', + 'Ỡ' => 'Ỡ', + 'ỡ' => 'ỡ', + 'Ợ' => 'Ợ', + 'ợ' => 'ợ', + 'Ụ' => 'Ụ', + 'ụ' => 'ụ', + 'Ủ' => 'Ủ', + 'ủ' => 'ủ', + 'Ứ' => 'Ứ', + 'ứ' => 'ứ', + 'Ừ' => 'Ừ', + 'ừ' => 'ừ', + 'Ử' => 'Ử', + 'ử' => 'ử', + 'Ữ' => 'Ữ', + 'ữ' => 'ữ', + 'Ự' => 'Ự', + 'ự' => 'ự', + 'Ỳ' => 'Ỳ', + 'ỳ' => 'ỳ', + 'Ỵ' => 'Ỵ', + 'ỵ' => 'ỵ', + 'Ỷ' => 'Ỷ', + 'ỷ' => 'ỷ', + 'Ỹ' => 'Ỹ', + 'ỹ' => 'ỹ', + 'ἀ' => 'ἀ', + 'ἁ' => 'ἁ', + 'ἂ' => 'ἂ', + 'ἃ' => 'ἃ', + 'ἄ' => 'ἄ', + 'ἅ' => 'ἅ', + 'ἆ' => 'ἆ', + 'ἇ' => 'ἇ', + 'Ἀ' => 'Ἀ', + 'Ἁ' => 'Ἁ', + 'Ἂ' => 'Ἂ', + 'Ἃ' => 'Ἃ', + 'Ἄ' => 'Ἄ', + 'Ἅ' => 'Ἅ', + 'Ἆ' => 'Ἆ', + 'Ἇ' => 'Ἇ', + 'ἐ' => 'ἐ', + 'ἑ' => 'ἑ', + 'ἒ' => 'ἒ', + 'ἓ' => 'ἓ', + 'ἔ' => 'ἔ', + 'ἕ' => 'ἕ', + 'Ἐ' => 'Ἐ', + 'Ἑ' => 'Ἑ', + 'Ἒ' => 'Ἒ', + 'Ἓ' => 'Ἓ', + 'Ἔ' => 'Ἔ', + 'Ἕ' => 'Ἕ', + 'ἠ' => 'ἠ', + 'ἡ' => 'ἡ', + 'ἢ' => 'ἢ', + 'ἣ' => 'ἣ', + 'ἤ' => 'ἤ', + 'ἥ' => 'ἥ', + 'ἦ' => 'ἦ', + 'ἧ' => 'ἧ', + 'Ἠ' => 'Ἠ', + 'Ἡ' => 'Ἡ', + 'Ἢ' => 'Ἢ', + 'Ἣ' => 'Ἣ', + 'Ἤ' => 'Ἤ', + 'Ἥ' => 'Ἥ', + 'Ἦ' => 'Ἦ', + 'Ἧ' => 'Ἧ', + 'ἰ' => 'ἰ', + 'ἱ' => 'ἱ', + 'ἲ' => 'ἲ', + 'ἳ' => 'ἳ', + 'ἴ' => 'ἴ', + 'ἵ' => 'ἵ', + 'ἶ' => 'ἶ', + 'ἷ' => 'ἷ', + 'Ἰ' => 'Ἰ', + 'Ἱ' => 'Ἱ', + 'Ἲ' => 'Ἲ', + 'Ἳ' => 'Ἳ', + 'Ἴ' => 'Ἴ', + 'Ἵ' => 'Ἵ', + 'Ἶ' => 'Ἶ', + 'Ἷ' => 'Ἷ', + 'ὀ' => 'ὀ', + 'ὁ' => 'ὁ', + 'ὂ' => 'ὂ', + 'ὃ' => 'ὃ', + 'ὄ' => 'ὄ', + 'ὅ' => 'ὅ', + 'Ὀ' => 'Ὀ', + 'Ὁ' => 'Ὁ', + 'Ὂ' => 'Ὂ', + 'Ὃ' => 'Ὃ', + 'Ὄ' => 'Ὄ', + 'Ὅ' => 'Ὅ', + 'ὐ' => 'ὐ', + 'ὑ' => 'ὑ', + 'ὒ' => 'ὒ', + 'ὓ' => 'ὓ', + 'ὔ' => 'ὔ', + 'ὕ' => 'ὕ', + 'ὖ' => 'ὖ', + 'ὗ' => 'ὗ', + 'Ὑ' => 'Ὑ', + 'Ὓ' => 'Ὓ', + 'Ὕ' => 'Ὕ', + 'Ὗ' => 'Ὗ', + 'ὠ' => 'ὠ', + 'ὡ' => 'ὡ', + 'ὢ' => 'ὢ', + 'ὣ' => 'ὣ', + 'ὤ' => 'ὤ', + 'ὥ' => 'ὥ', + 'ὦ' => 'ὦ', + 'ὧ' => 'ὧ', + 'Ὠ' => 'Ὠ', + 'Ὡ' => 'Ὡ', + 'Ὢ' => 'Ὢ', + 'Ὣ' => 'Ὣ', + 'Ὤ' => 'Ὤ', + 'Ὥ' => 'Ὥ', + 'Ὦ' => 'Ὦ', + 'Ὧ' => 'Ὧ', + 'ὰ' => 'ὰ', + 'ὲ' => 'ὲ', + 'ὴ' => 'ὴ', + 'ὶ' => 'ὶ', + 'ὸ' => 'ὸ', + 'ὺ' => 'ὺ', + 'ὼ' => 'ὼ', + 'ᾀ' => 'ᾀ', + 'ᾁ' => 'ᾁ', + 'ᾂ' => 'ᾂ', + 'ᾃ' => 'ᾃ', + 'ᾄ' => 'ᾄ', + 'ᾅ' => 'ᾅ', + 'ᾆ' => 'ᾆ', + 'ᾇ' => 'ᾇ', + 'ᾈ' => 'ᾈ', + 'ᾉ' => 'ᾉ', + 'ᾊ' => 'ᾊ', + 'ᾋ' => 'ᾋ', + 'ᾌ' => 'ᾌ', + 'ᾍ' => 'ᾍ', + 'ᾎ' => 'ᾎ', + 'ᾏ' => 'ᾏ', + 'ᾐ' => 'ᾐ', + 'ᾑ' => 'ᾑ', + 'ᾒ' => 'ᾒ', + 'ᾓ' => 'ᾓ', + 'ᾔ' => 'ᾔ', + 'ᾕ' => 'ᾕ', + 'ᾖ' => 'ᾖ', + 'ᾗ' => 'ᾗ', + 'ᾘ' => 'ᾘ', + 'ᾙ' => 'ᾙ', + 'ᾚ' => 'ᾚ', + 'ᾛ' => 'ᾛ', + 'ᾜ' => 'ᾜ', + 'ᾝ' => 'ᾝ', + 'ᾞ' => 'ᾞ', + 'ᾟ' => 'ᾟ', + 'ᾠ' => 'ᾠ', + 'ᾡ' => 'ᾡ', + 'ᾢ' => 'ᾢ', + 'ᾣ' => 'ᾣ', + 'ᾤ' => 'ᾤ', + 'ᾥ' => 'ᾥ', + 'ᾦ' => 'ᾦ', + 'ᾧ' => 'ᾧ', + 'ᾨ' => 'ᾨ', + 'ᾩ' => 'ᾩ', + 'ᾪ' => 'ᾪ', + 'ᾫ' => 'ᾫ', + 'ᾬ' => 'ᾬ', + 'ᾭ' => 'ᾭ', + 'ᾮ' => 'ᾮ', + 'ᾯ' => 'ᾯ', + 'ᾰ' => 'ᾰ', + 'ᾱ' => 'ᾱ', + 'ᾲ' => 'ᾲ', + 'ᾳ' => 'ᾳ', + 'ᾴ' => 'ᾴ', + 'ᾶ' => 'ᾶ', + 'ᾷ' => 'ᾷ', + 'Ᾰ' => 'Ᾰ', + 'Ᾱ' => 'Ᾱ', + 'Ὰ' => 'Ὰ', + 'ᾼ' => 'ᾼ', + '῁' => '῁', + 'ῂ' => 'ῂ', + 'ῃ' => 'ῃ', + 'ῄ' => 'ῄ', + 'ῆ' => 'ῆ', + 'ῇ' => 'ῇ', + 'Ὲ' => 'Ὲ', + 'Ὴ' => 'Ὴ', + 'ῌ' => 'ῌ', + '῍' => '῍', + '῎' => '῎', + '῏' => '῏', + 'ῐ' => 'ῐ', + 'ῑ' => 'ῑ', + 'ῒ' => 'ῒ', + 'ῖ' => 'ῖ', + 'ῗ' => 'ῗ', + 'Ῐ' => 'Ῐ', + 'Ῑ' => 'Ῑ', + 'Ὶ' => 'Ὶ', + '῝' => '῝', + '῞' => '῞', + '῟' => '῟', + 'ῠ' => 'ῠ', + 'ῡ' => 'ῡ', + 'ῢ' => 'ῢ', + 'ῤ' => 'ῤ', + 'ῥ' => 'ῥ', + 'ῦ' => 'ῦ', + 'ῧ' => 'ῧ', + 'Ῠ' => 'Ῠ', + 'Ῡ' => 'Ῡ', + 'Ὺ' => 'Ὺ', + 'Ῥ' => 'Ῥ', + '῭' => '῭', + 'ῲ' => 'ῲ', + 'ῳ' => 'ῳ', + 'ῴ' => 'ῴ', + 'ῶ' => 'ῶ', + 'ῷ' => 'ῷ', + 'Ὸ' => 'Ὸ', + 'Ὼ' => 'Ὼ', + 'ῼ' => 'ῼ', + '↚' => '↚', + '↛' => '↛', + '↮' => '↮', + '⇍' => '⇍', + '⇎' => '⇎', + '⇏' => '⇏', + '∄' => '∄', + '∉' => '∉', + '∌' => '∌', + '∤' => '∤', + '∦' => '∦', + '≁' => '≁', + '≄' => '≄', + '≇' => '≇', + '≉' => '≉', + '≠' => '≠', + '≢' => '≢', + '≭' => '≭', + '≮' => '≮', + '≯' => '≯', + '≰' => '≰', + '≱' => '≱', + '≴' => '≴', + '≵' => '≵', + '≸' => '≸', + '≹' => '≹', + '⊀' => '⊀', + '⊁' => '⊁', + '⊄' => '⊄', + '⊅' => '⊅', + '⊈' => '⊈', + '⊉' => '⊉', + '⊬' => '⊬', + '⊭' => '⊭', + '⊮' => '⊮', + '⊯' => '⊯', + '⋠' => '⋠', + '⋡' => '⋡', + '⋢' => '⋢', + '⋣' => '⋣', + '⋪' => '⋪', + '⋫' => '⋫', + '⋬' => '⋬', + '⋭' => '⋭', + 'が' => 'が', + 'ぎ' => 'ぎ', + 'ぐ' => 'ぐ', + 'げ' => 'げ', + 'ご' => 'ご', + 'ざ' => 'ざ', + 'じ' => 'じ', + 'ず' => 'ず', + 'ぜ' => 'ぜ', + 'ぞ' => 'ぞ', + 'だ' => 'だ', + 'ぢ' => 'ぢ', + 'づ' => 'づ', + 'で' => 'で', + 'ど' => 'ど', + 'ば' => 'ば', + 'ぱ' => 'ぱ', + 'び' => 'び', + 'ぴ' => 'ぴ', + 'ぶ' => 'ぶ', + 'ぷ' => 'ぷ', + 'べ' => 'べ', + 'ぺ' => 'ぺ', + 'ぼ' => 'ぼ', + 'ぽ' => 'ぽ', + 'ゔ' => 'ゔ', + 'ゞ' => 'ゞ', + 'ガ' => 'ガ', + 'ギ' => 'ギ', + 'グ' => 'グ', + 'ゲ' => 'ゲ', + 'ゴ' => 'ゴ', + 'ザ' => 'ザ', + 'ジ' => 'ジ', + 'ズ' => 'ズ', + 'ゼ' => 'ゼ', + 'ゾ' => 'ゾ', + 'ダ' => 'ダ', + 'ヂ' => 'ヂ', + 'ヅ' => 'ヅ', + 'デ' => 'デ', + 'ド' => 'ド', + 'バ' => 'バ', + 'パ' => 'パ', + 'ビ' => 'ビ', + 'ピ' => 'ピ', + 'ブ' => 'ブ', + 'プ' => 'プ', + 'ベ' => 'ベ', + 'ペ' => 'ペ', + 'ボ' => 'ボ', + 'ポ' => 'ポ', + 'ヴ' => 'ヴ', + 'ヷ' => 'ヷ', + 'ヸ' => 'ヸ', + 'ヹ' => 'ヹ', + 'ヺ' => 'ヺ', + 'ヾ' => 'ヾ', + '𑂚' => '𑂚', + '𑂜' => '𑂜', + '𑂫' => '𑂫', + '𑄮' => '𑄮', + '𑄯' => '𑄯', + '𑍋' => '𑍋', + '𑍌' => '𑍌', + '𑒻' => '𑒻', + '𑒼' => '𑒼', + '𑒾' => '𑒾', + '𑖺' => '𑖺', + '𑖻' => '𑖻', + '𑤸' => '𑤸', +); diff --git a/3rdparty/symfony/polyfill-intl-normalizer/Resources/unidata/canonicalDecomposition.php b/3rdparty/symfony/polyfill-intl-normalizer/Resources/unidata/canonicalDecomposition.php new file mode 100644 index 00000000..5a3e8e09 --- /dev/null +++ b/3rdparty/symfony/polyfill-intl-normalizer/Resources/unidata/canonicalDecomposition.php @@ -0,0 +1,2065 @@ + 'À', + 'Á' => 'Á', + 'Â' => 'Â', + 'Ã' => 'Ã', + 'Ä' => 'Ä', + 'Å' => 'Å', + 'Ç' => 'Ç', + 'È' => 'È', + 'É' => 'É', + 'Ê' => 'Ê', + 'Ë' => 'Ë', + 'Ì' => 'Ì', + 'Í' => 'Í', + 'Î' => 'Î', + 'Ï' => 'Ï', + 'Ñ' => 'Ñ', + 'Ò' => 'Ò', + 'Ó' => 'Ó', + 'Ô' => 'Ô', + 'Õ' => 'Õ', + 'Ö' => 'Ö', + 'Ù' => 'Ù', + 'Ú' => 'Ú', + 'Û' => 'Û', + 'Ü' => 'Ü', + 'Ý' => 'Ý', + 'à' => 'à', + 'á' => 'á', + 'â' => 'â', + 'ã' => 'ã', + 'ä' => 'ä', + 'å' => 'å', + 'ç' => 'ç', + 'è' => 'è', + 'é' => 'é', + 'ê' => 'ê', + 'ë' => 'ë', + 'ì' => 'ì', + 'í' => 'í', + 'î' => 'î', + 'ï' => 'ï', + 'ñ' => 'ñ', + 'ò' => 'ò', + 'ó' => 'ó', + 'ô' => 'ô', + 'õ' => 'õ', + 'ö' => 'ö', + 'ù' => 'ù', + 'ú' => 'ú', + 'û' => 'û', + 'ü' => 'ü', + 'ý' => 'ý', + 'ÿ' => 'ÿ', + 'Ā' => 'Ā', + 'ā' => 'ā', + 'Ă' => 'Ă', + 'ă' => 'ă', + 'Ą' => 'Ą', + 'ą' => 'ą', + 'Ć' => 'Ć', + 'ć' => 'ć', + 'Ĉ' => 'Ĉ', + 'ĉ' => 'ĉ', + 'Ċ' => 'Ċ', + 'ċ' => 'ċ', + 'Č' => 'Č', + 'č' => 'č', + 'Ď' => 'Ď', + 'ď' => 'ď', + 'Ē' => 'Ē', + 'ē' => 'ē', + 'Ĕ' => 'Ĕ', + 'ĕ' => 'ĕ', + 'Ė' => 'Ė', + 'ė' => 'ė', + 'Ę' => 'Ę', + 'ę' => 'ę', + 'Ě' => 'Ě', + 'ě' => 'ě', + 'Ĝ' => 'Ĝ', + 'ĝ' => 'ĝ', + 'Ğ' => 'Ğ', + 'ğ' => 'ğ', + 'Ġ' => 'Ġ', + 'ġ' => 'ġ', + 'Ģ' => 'Ģ', + 'ģ' => 'ģ', + 'Ĥ' => 'Ĥ', + 'ĥ' => 'ĥ', + 'Ĩ' => 'Ĩ', + 'ĩ' => 'ĩ', + 'Ī' => 'Ī', + 'ī' => 'ī', + 'Ĭ' => 'Ĭ', + 'ĭ' => 'ĭ', + 'Į' => 'Į', + 'į' => 'į', + 'İ' => 'İ', + 'Ĵ' => 'Ĵ', + 'ĵ' => 'ĵ', + 'Ķ' => 'Ķ', + 'ķ' => 'ķ', + 'Ĺ' => 'Ĺ', + 'ĺ' => 'ĺ', + 'Ļ' => 'Ļ', + 'ļ' => 'ļ', + 'Ľ' => 'Ľ', + 'ľ' => 'ľ', + 'Ń' => 'Ń', + 'ń' => 'ń', + 'Ņ' => 'Ņ', + 'ņ' => 'ņ', + 'Ň' => 'Ň', + 'ň' => 'ň', + 'Ō' => 'Ō', + 'ō' => 'ō', + 'Ŏ' => 'Ŏ', + 'ŏ' => 'ŏ', + 'Ő' => 'Ő', + 'ő' => 'ő', + 'Ŕ' => 'Ŕ', + 'ŕ' => 'ŕ', + 'Ŗ' => 'Ŗ', + 'ŗ' => 'ŗ', + 'Ř' => 'Ř', + 'ř' => 'ř', + 'Ś' => 'Ś', + 'ś' => 'ś', + 'Ŝ' => 'Ŝ', + 'ŝ' => 'ŝ', + 'Ş' => 'Ş', + 'ş' => 'ş', + 'Š' => 'Š', + 'š' => 'š', + 'Ţ' => 'Ţ', + 'ţ' => 'ţ', + 'Ť' => 'Ť', + 'ť' => 'ť', + 'Ũ' => 'Ũ', + 'ũ' => 'ũ', + 'Ū' => 'Ū', + 'ū' => 'ū', + 'Ŭ' => 'Ŭ', + 'ŭ' => 'ŭ', + 'Ů' => 'Ů', + 'ů' => 'ů', + 'Ű' => 'Ű', + 'ű' => 'ű', + 'Ų' => 'Ų', + 'ų' => 'ų', + 'Ŵ' => 'Ŵ', + 'ŵ' => 'ŵ', + 'Ŷ' => 'Ŷ', + 'ŷ' => 'ŷ', + 'Ÿ' => 'Ÿ', + 'Ź' => 'Ź', + 'ź' => 'ź', + 'Ż' => 'Ż', + 'ż' => 'ż', + 'Ž' => 'Ž', + 'ž' => 'ž', + 'Ơ' => 'Ơ', + 'ơ' => 'ơ', + 'Ư' => 'Ư', + 'ư' => 'ư', + 'Ǎ' => 'Ǎ', + 'ǎ' => 'ǎ', + 'Ǐ' => 'Ǐ', + 'ǐ' => 'ǐ', + 'Ǒ' => 'Ǒ', + 'ǒ' => 'ǒ', + 'Ǔ' => 'Ǔ', + 'ǔ' => 'ǔ', + 'Ǖ' => 'Ǖ', + 'ǖ' => 'ǖ', + 'Ǘ' => 'Ǘ', + 'ǘ' => 'ǘ', + 'Ǚ' => 'Ǚ', + 'ǚ' => 'ǚ', + 'Ǜ' => 'Ǜ', + 'ǜ' => 'ǜ', + 'Ǟ' => 'Ǟ', + 'ǟ' => 'ǟ', + 'Ǡ' => 'Ǡ', + 'ǡ' => 'ǡ', + 'Ǣ' => 'Ǣ', + 'ǣ' => 'ǣ', + 'Ǧ' => 'Ǧ', + 'ǧ' => 'ǧ', + 'Ǩ' => 'Ǩ', + 'ǩ' => 'ǩ', + 'Ǫ' => 'Ǫ', + 'ǫ' => 'ǫ', + 'Ǭ' => 'Ǭ', + 'ǭ' => 'ǭ', + 'Ǯ' => 'Ǯ', + 'ǯ' => 'ǯ', + 'ǰ' => 'ǰ', + 'Ǵ' => 'Ǵ', + 'ǵ' => 'ǵ', + 'Ǹ' => 'Ǹ', + 'ǹ' => 'ǹ', + 'Ǻ' => 'Ǻ', + 'ǻ' => 'ǻ', + 'Ǽ' => 'Ǽ', + 'ǽ' => 'ǽ', + 'Ǿ' => 'Ǿ', + 'ǿ' => 'ǿ', + 'Ȁ' => 'Ȁ', + 'ȁ' => 'ȁ', + 'Ȃ' => 'Ȃ', + 'ȃ' => 'ȃ', + 'Ȅ' => 'Ȅ', + 'ȅ' => 'ȅ', + 'Ȇ' => 'Ȇ', + 'ȇ' => 'ȇ', + 'Ȉ' => 'Ȉ', + 'ȉ' => 'ȉ', + 'Ȋ' => 'Ȋ', + 'ȋ' => 'ȋ', + 'Ȍ' => 'Ȍ', + 'ȍ' => 'ȍ', + 'Ȏ' => 'Ȏ', + 'ȏ' => 'ȏ', + 'Ȑ' => 'Ȑ', + 'ȑ' => 'ȑ', + 'Ȓ' => 'Ȓ', + 'ȓ' => 'ȓ', + 'Ȕ' => 'Ȕ', + 'ȕ' => 'ȕ', + 'Ȗ' => 'Ȗ', + 'ȗ' => 'ȗ', + 'Ș' => 'Ș', + 'ș' => 'ș', + 'Ț' => 'Ț', + 'ț' => 'ț', + 'Ȟ' => 'Ȟ', + 'ȟ' => 'ȟ', + 'Ȧ' => 'Ȧ', + 'ȧ' => 'ȧ', + 'Ȩ' => 'Ȩ', + 'ȩ' => 'ȩ', + 'Ȫ' => 'Ȫ', + 'ȫ' => 'ȫ', + 'Ȭ' => 'Ȭ', + 'ȭ' => 'ȭ', + 'Ȯ' => 'Ȯ', + 'ȯ' => 'ȯ', + 'Ȱ' => 'Ȱ', + 'ȱ' => 'ȱ', + 'Ȳ' => 'Ȳ', + 'ȳ' => 'ȳ', + '̀' => '̀', + '́' => '́', + '̓' => '̓', + '̈́' => '̈́', + 'ʹ' => 'ʹ', + ';' => ';', + '΅' => '΅', + 'Ά' => 'Ά', + '·' => '·', + 'Έ' => 'Έ', + 'Ή' => 'Ή', + 'Ί' => 'Ί', + 'Ό' => 'Ό', + 'Ύ' => 'Ύ', + 'Ώ' => 'Ώ', + 'ΐ' => 'ΐ', + 'Ϊ' => 'Ϊ', + 'Ϋ' => 'Ϋ', + 'ά' => 'ά', + 'έ' => 'έ', + 'ή' => 'ή', + 'ί' => 'ί', + 'ΰ' => 'ΰ', + 'ϊ' => 'ϊ', + 'ϋ' => 'ϋ', + 'ό' => 'ό', + 'ύ' => 'ύ', + 'ώ' => 'ώ', + 'ϓ' => 'ϓ', + 'ϔ' => 'ϔ', + 'Ѐ' => 'Ѐ', + 'Ё' => 'Ё', + 'Ѓ' => 'Ѓ', + 'Ї' => 'Ї', + 'Ќ' => 'Ќ', + 'Ѝ' => 'Ѝ', + 'Ў' => 'Ў', + 'Й' => 'Й', + 'й' => 'й', + 'ѐ' => 'ѐ', + 'ё' => 'ё', + 'ѓ' => 'ѓ', + 'ї' => 'ї', + 'ќ' => 'ќ', + 'ѝ' => 'ѝ', + 'ў' => 'ў', + 'Ѷ' => 'Ѷ', + 'ѷ' => 'ѷ', + 'Ӂ' => 'Ӂ', + 'ӂ' => 'ӂ', + 'Ӑ' => 'Ӑ', + 'ӑ' => 'ӑ', + 'Ӓ' => 'Ӓ', + 'ӓ' => 'ӓ', + 'Ӗ' => 'Ӗ', + 'ӗ' => 'ӗ', + 'Ӛ' => 'Ӛ', + 'ӛ' => 'ӛ', + 'Ӝ' => 'Ӝ', + 'ӝ' => 'ӝ', + 'Ӟ' => 'Ӟ', + 'ӟ' => 'ӟ', + 'Ӣ' => 'Ӣ', + 'ӣ' => 'ӣ', + 'Ӥ' => 'Ӥ', + 'ӥ' => 'ӥ', + 'Ӧ' => 'Ӧ', + 'ӧ' => 'ӧ', + 'Ӫ' => 'Ӫ', + 'ӫ' => 'ӫ', + 'Ӭ' => 'Ӭ', + 'ӭ' => 'ӭ', + 'Ӯ' => 'Ӯ', + 'ӯ' => 'ӯ', + 'Ӱ' => 'Ӱ', + 'ӱ' => 'ӱ', + 'Ӳ' => 'Ӳ', + 'ӳ' => 'ӳ', + 'Ӵ' => 'Ӵ', + 'ӵ' => 'ӵ', + 'Ӹ' => 'Ӹ', + 'ӹ' => 'ӹ', + 'آ' => 'آ', + 'أ' => 'أ', + 'ؤ' => 'ؤ', + 'إ' => 'إ', + 'ئ' => 'ئ', + 'ۀ' => 'ۀ', + 'ۂ' => 'ۂ', + 'ۓ' => 'ۓ', + 'ऩ' => 'ऩ', + 'ऱ' => 'ऱ', + 'ऴ' => 'ऴ', + 'क़' => 'क़', + 'ख़' => 'ख़', + 'ग़' => 'ग़', + 'ज़' => 'ज़', + 'ड़' => 'ड़', + 'ढ़' => 'ढ़', + 'फ़' => 'फ़', + 'य़' => 'य़', + 'ো' => 'ো', + 'ৌ' => 'ৌ', + 'ড়' => 'ড়', + 'ঢ়' => 'ঢ়', + 'য়' => 'য়', + 'ਲ਼' => 'ਲ਼', + 'ਸ਼' => 'ਸ਼', + 'ਖ਼' => 'ਖ਼', + 'ਗ਼' => 'ਗ਼', + 'ਜ਼' => 'ਜ਼', + 'ਫ਼' => 'ਫ਼', + 'ୈ' => 'ୈ', + 'ୋ' => 'ୋ', + 'ୌ' => 'ୌ', + 'ଡ଼' => 'ଡ଼', + 'ଢ଼' => 'ଢ଼', + 'ஔ' => 'ஔ', + 'ொ' => 'ொ', + 'ோ' => 'ோ', + 'ௌ' => 'ௌ', + 'ై' => 'ై', + 'ೀ' => 'ೀ', + 'ೇ' => 'ೇ', + 'ೈ' => 'ೈ', + 'ೊ' => 'ೊ', + 'ೋ' => 'ೋ', + 'ൊ' => 'ൊ', + 'ോ' => 'ോ', + 'ൌ' => 'ൌ', + 'ේ' => 'ේ', + 'ො' => 'ො', + 'ෝ' => 'ෝ', + 'ෞ' => 'ෞ', + 'གྷ' => 'གྷ', + 'ཌྷ' => 'ཌྷ', + 'དྷ' => 'དྷ', + 'བྷ' => 'བྷ', + 'ཛྷ' => 'ཛྷ', + 'ཀྵ' => 'ཀྵ', + 'ཱི' => 'ཱི', + 'ཱུ' => 'ཱུ', + 'ྲྀ' => 'ྲྀ', + 'ླྀ' => 'ླྀ', + 'ཱྀ' => 'ཱྀ', + 'ྒྷ' => 'ྒྷ', + 'ྜྷ' => 'ྜྷ', + 'ྡྷ' => 'ྡྷ', + 'ྦྷ' => 'ྦྷ', + 'ྫྷ' => 'ྫྷ', + 'ྐྵ' => 'ྐྵ', + 'ဦ' => 'ဦ', + 'ᬆ' => 'ᬆ', + 'ᬈ' => 'ᬈ', + 'ᬊ' => 'ᬊ', + 'ᬌ' => 'ᬌ', + 'ᬎ' => 'ᬎ', + 'ᬒ' => 'ᬒ', + 'ᬻ' => 'ᬻ', + 'ᬽ' => 'ᬽ', + 'ᭀ' => 'ᭀ', + 'ᭁ' => 'ᭁ', + 'ᭃ' => 'ᭃ', + 'Ḁ' => 'Ḁ', + 'ḁ' => 'ḁ', + 'Ḃ' => 'Ḃ', + 'ḃ' => 'ḃ', + 'Ḅ' => 'Ḅ', + 'ḅ' => 'ḅ', + 'Ḇ' => 'Ḇ', + 'ḇ' => 'ḇ', + 'Ḉ' => 'Ḉ', + 'ḉ' => 'ḉ', + 'Ḋ' => 'Ḋ', + 'ḋ' => 'ḋ', + 'Ḍ' => 'Ḍ', + 'ḍ' => 'ḍ', + 'Ḏ' => 'Ḏ', + 'ḏ' => 'ḏ', + 'Ḑ' => 'Ḑ', + 'ḑ' => 'ḑ', + 'Ḓ' => 'Ḓ', + 'ḓ' => 'ḓ', + 'Ḕ' => 'Ḕ', + 'ḕ' => 'ḕ', + 'Ḗ' => 'Ḗ', + 'ḗ' => 'ḗ', + 'Ḙ' => 'Ḙ', + 'ḙ' => 'ḙ', + 'Ḛ' => 'Ḛ', + 'ḛ' => 'ḛ', + 'Ḝ' => 'Ḝ', + 'ḝ' => 'ḝ', + 'Ḟ' => 'Ḟ', + 'ḟ' => 'ḟ', + 'Ḡ' => 'Ḡ', + 'ḡ' => 'ḡ', + 'Ḣ' => 'Ḣ', + 'ḣ' => 'ḣ', + 'Ḥ' => 'Ḥ', + 'ḥ' => 'ḥ', + 'Ḧ' => 'Ḧ', + 'ḧ' => 'ḧ', + 'Ḩ' => 'Ḩ', + 'ḩ' => 'ḩ', + 'Ḫ' => 'Ḫ', + 'ḫ' => 'ḫ', + 'Ḭ' => 'Ḭ', + 'ḭ' => 'ḭ', + 'Ḯ' => 'Ḯ', + 'ḯ' => 'ḯ', + 'Ḱ' => 'Ḱ', + 'ḱ' => 'ḱ', + 'Ḳ' => 'Ḳ', + 'ḳ' => 'ḳ', + 'Ḵ' => 'Ḵ', + 'ḵ' => 'ḵ', + 'Ḷ' => 'Ḷ', + 'ḷ' => 'ḷ', + 'Ḹ' => 'Ḹ', + 'ḹ' => 'ḹ', + 'Ḻ' => 'Ḻ', + 'ḻ' => 'ḻ', + 'Ḽ' => 'Ḽ', + 'ḽ' => 'ḽ', + 'Ḿ' => 'Ḿ', + 'ḿ' => 'ḿ', + 'Ṁ' => 'Ṁ', + 'ṁ' => 'ṁ', + 'Ṃ' => 'Ṃ', + 'ṃ' => 'ṃ', + 'Ṅ' => 'Ṅ', + 'ṅ' => 'ṅ', + 'Ṇ' => 'Ṇ', + 'ṇ' => 'ṇ', + 'Ṉ' => 'Ṉ', + 'ṉ' => 'ṉ', + 'Ṋ' => 'Ṋ', + 'ṋ' => 'ṋ', + 'Ṍ' => 'Ṍ', + 'ṍ' => 'ṍ', + 'Ṏ' => 'Ṏ', + 'ṏ' => 'ṏ', + 'Ṑ' => 'Ṑ', + 'ṑ' => 'ṑ', + 'Ṓ' => 'Ṓ', + 'ṓ' => 'ṓ', + 'Ṕ' => 'Ṕ', + 'ṕ' => 'ṕ', + 'Ṗ' => 'Ṗ', + 'ṗ' => 'ṗ', + 'Ṙ' => 'Ṙ', + 'ṙ' => 'ṙ', + 'Ṛ' => 'Ṛ', + 'ṛ' => 'ṛ', + 'Ṝ' => 'Ṝ', + 'ṝ' => 'ṝ', + 'Ṟ' => 'Ṟ', + 'ṟ' => 'ṟ', + 'Ṡ' => 'Ṡ', + 'ṡ' => 'ṡ', + 'Ṣ' => 'Ṣ', + 'ṣ' => 'ṣ', + 'Ṥ' => 'Ṥ', + 'ṥ' => 'ṥ', + 'Ṧ' => 'Ṧ', + 'ṧ' => 'ṧ', + 'Ṩ' => 'Ṩ', + 'ṩ' => 'ṩ', + 'Ṫ' => 'Ṫ', + 'ṫ' => 'ṫ', + 'Ṭ' => 'Ṭ', + 'ṭ' => 'ṭ', + 'Ṯ' => 'Ṯ', + 'ṯ' => 'ṯ', + 'Ṱ' => 'Ṱ', + 'ṱ' => 'ṱ', + 'Ṳ' => 'Ṳ', + 'ṳ' => 'ṳ', + 'Ṵ' => 'Ṵ', + 'ṵ' => 'ṵ', + 'Ṷ' => 'Ṷ', + 'ṷ' => 'ṷ', + 'Ṹ' => 'Ṹ', + 'ṹ' => 'ṹ', + 'Ṻ' => 'Ṻ', + 'ṻ' => 'ṻ', + 'Ṽ' => 'Ṽ', + 'ṽ' => 'ṽ', + 'Ṿ' => 'Ṿ', + 'ṿ' => 'ṿ', + 'Ẁ' => 'Ẁ', + 'ẁ' => 'ẁ', + 'Ẃ' => 'Ẃ', + 'ẃ' => 'ẃ', + 'Ẅ' => 'Ẅ', + 'ẅ' => 'ẅ', + 'Ẇ' => 'Ẇ', + 'ẇ' => 'ẇ', + 'Ẉ' => 'Ẉ', + 'ẉ' => 'ẉ', + 'Ẋ' => 'Ẋ', + 'ẋ' => 'ẋ', + 'Ẍ' => 'Ẍ', + 'ẍ' => 'ẍ', + 'Ẏ' => 'Ẏ', + 'ẏ' => 'ẏ', + 'Ẑ' => 'Ẑ', + 'ẑ' => 'ẑ', + 'Ẓ' => 'Ẓ', + 'ẓ' => 'ẓ', + 'Ẕ' => 'Ẕ', + 'ẕ' => 'ẕ', + 'ẖ' => 'ẖ', + 'ẗ' => 'ẗ', + 'ẘ' => 'ẘ', + 'ẙ' => 'ẙ', + 'ẛ' => 'ẛ', + 'Ạ' => 'Ạ', + 'ạ' => 'ạ', + 'Ả' => 'Ả', + 'ả' => 'ả', + 'Ấ' => 'Ấ', + 'ấ' => 'ấ', + 'Ầ' => 'Ầ', + 'ầ' => 'ầ', + 'Ẩ' => 'Ẩ', + 'ẩ' => 'ẩ', + 'Ẫ' => 'Ẫ', + 'ẫ' => 'ẫ', + 'Ậ' => 'Ậ', + 'ậ' => 'ậ', + 'Ắ' => 'Ắ', + 'ắ' => 'ắ', + 'Ằ' => 'Ằ', + 'ằ' => 'ằ', + 'Ẳ' => 'Ẳ', + 'ẳ' => 'ẳ', + 'Ẵ' => 'Ẵ', + 'ẵ' => 'ẵ', + 'Ặ' => 'Ặ', + 'ặ' => 'ặ', + 'Ẹ' => 'Ẹ', + 'ẹ' => 'ẹ', + 'Ẻ' => 'Ẻ', + 'ẻ' => 'ẻ', + 'Ẽ' => 'Ẽ', + 'ẽ' => 'ẽ', + 'Ế' => 'Ế', + 'ế' => 'ế', + 'Ề' => 'Ề', + 'ề' => 'ề', + 'Ể' => 'Ể', + 'ể' => 'ể', + 'Ễ' => 'Ễ', + 'ễ' => 'ễ', + 'Ệ' => 'Ệ', + 'ệ' => 'ệ', + 'Ỉ' => 'Ỉ', + 'ỉ' => 'ỉ', + 'Ị' => 'Ị', + 'ị' => 'ị', + 'Ọ' => 'Ọ', + 'ọ' => 'ọ', + 'Ỏ' => 'Ỏ', + 'ỏ' => 'ỏ', + 'Ố' => 'Ố', + 'ố' => 'ố', + 'Ồ' => 'Ồ', + 'ồ' => 'ồ', + 'Ổ' => 'Ổ', + 'ổ' => 'ổ', + 'Ỗ' => 'Ỗ', + 'ỗ' => 'ỗ', + 'Ộ' => 'Ộ', + 'ộ' => 'ộ', + 'Ớ' => 'Ớ', + 'ớ' => 'ớ', + 'Ờ' => 'Ờ', + 'ờ' => 'ờ', + 'Ở' => 'Ở', + 'ở' => 'ở', + 'Ỡ' => 'Ỡ', + 'ỡ' => 'ỡ', + 'Ợ' => 'Ợ', + 'ợ' => 'ợ', + 'Ụ' => 'Ụ', + 'ụ' => 'ụ', + 'Ủ' => 'Ủ', + 'ủ' => 'ủ', + 'Ứ' => 'Ứ', + 'ứ' => 'ứ', + 'Ừ' => 'Ừ', + 'ừ' => 'ừ', + 'Ử' => 'Ử', + 'ử' => 'ử', + 'Ữ' => 'Ữ', + 'ữ' => 'ữ', + 'Ự' => 'Ự', + 'ự' => 'ự', + 'Ỳ' => 'Ỳ', + 'ỳ' => 'ỳ', + 'Ỵ' => 'Ỵ', + 'ỵ' => 'ỵ', + 'Ỷ' => 'Ỷ', + 'ỷ' => 'ỷ', + 'Ỹ' => 'Ỹ', + 'ỹ' => 'ỹ', + 'ἀ' => 'ἀ', + 'ἁ' => 'ἁ', + 'ἂ' => 'ἂ', + 'ἃ' => 'ἃ', + 'ἄ' => 'ἄ', + 'ἅ' => 'ἅ', + 'ἆ' => 'ἆ', + 'ἇ' => 'ἇ', + 'Ἀ' => 'Ἀ', + 'Ἁ' => 'Ἁ', + 'Ἂ' => 'Ἂ', + 'Ἃ' => 'Ἃ', + 'Ἄ' => 'Ἄ', + 'Ἅ' => 'Ἅ', + 'Ἆ' => 'Ἆ', + 'Ἇ' => 'Ἇ', + 'ἐ' => 'ἐ', + 'ἑ' => 'ἑ', + 'ἒ' => 'ἒ', + 'ἓ' => 'ἓ', + 'ἔ' => 'ἔ', + 'ἕ' => 'ἕ', + 'Ἐ' => 'Ἐ', + 'Ἑ' => 'Ἑ', + 'Ἒ' => 'Ἒ', + 'Ἓ' => 'Ἓ', + 'Ἔ' => 'Ἔ', + 'Ἕ' => 'Ἕ', + 'ἠ' => 'ἠ', + 'ἡ' => 'ἡ', + 'ἢ' => 'ἢ', + 'ἣ' => 'ἣ', + 'ἤ' => 'ἤ', + 'ἥ' => 'ἥ', + 'ἦ' => 'ἦ', + 'ἧ' => 'ἧ', + 'Ἠ' => 'Ἠ', + 'Ἡ' => 'Ἡ', + 'Ἢ' => 'Ἢ', + 'Ἣ' => 'Ἣ', + 'Ἤ' => 'Ἤ', + 'Ἥ' => 'Ἥ', + 'Ἦ' => 'Ἦ', + 'Ἧ' => 'Ἧ', + 'ἰ' => 'ἰ', + 'ἱ' => 'ἱ', + 'ἲ' => 'ἲ', + 'ἳ' => 'ἳ', + 'ἴ' => 'ἴ', + 'ἵ' => 'ἵ', + 'ἶ' => 'ἶ', + 'ἷ' => 'ἷ', + 'Ἰ' => 'Ἰ', + 'Ἱ' => 'Ἱ', + 'Ἲ' => 'Ἲ', + 'Ἳ' => 'Ἳ', + 'Ἴ' => 'Ἴ', + 'Ἵ' => 'Ἵ', + 'Ἶ' => 'Ἶ', + 'Ἷ' => 'Ἷ', + 'ὀ' => 'ὀ', + 'ὁ' => 'ὁ', + 'ὂ' => 'ὂ', + 'ὃ' => 'ὃ', + 'ὄ' => 'ὄ', + 'ὅ' => 'ὅ', + 'Ὀ' => 'Ὀ', + 'Ὁ' => 'Ὁ', + 'Ὂ' => 'Ὂ', + 'Ὃ' => 'Ὃ', + 'Ὄ' => 'Ὄ', + 'Ὅ' => 'Ὅ', + 'ὐ' => 'ὐ', + 'ὑ' => 'ὑ', + 'ὒ' => 'ὒ', + 'ὓ' => 'ὓ', + 'ὔ' => 'ὔ', + 'ὕ' => 'ὕ', + 'ὖ' => 'ὖ', + 'ὗ' => 'ὗ', + 'Ὑ' => 'Ὑ', + 'Ὓ' => 'Ὓ', + 'Ὕ' => 'Ὕ', + 'Ὗ' => 'Ὗ', + 'ὠ' => 'ὠ', + 'ὡ' => 'ὡ', + 'ὢ' => 'ὢ', + 'ὣ' => 'ὣ', + 'ὤ' => 'ὤ', + 'ὥ' => 'ὥ', + 'ὦ' => 'ὦ', + 'ὧ' => 'ὧ', + 'Ὠ' => 'Ὠ', + 'Ὡ' => 'Ὡ', + 'Ὢ' => 'Ὢ', + 'Ὣ' => 'Ὣ', + 'Ὤ' => 'Ὤ', + 'Ὥ' => 'Ὥ', + 'Ὦ' => 'Ὦ', + 'Ὧ' => 'Ὧ', + 'ὰ' => 'ὰ', + 'ά' => 'ά', + 'ὲ' => 'ὲ', + 'έ' => 'έ', + 'ὴ' => 'ὴ', + 'ή' => 'ή', + 'ὶ' => 'ὶ', + 'ί' => 'ί', + 'ὸ' => 'ὸ', + 'ό' => 'ό', + 'ὺ' => 'ὺ', + 'ύ' => 'ύ', + 'ὼ' => 'ὼ', + 'ώ' => 'ώ', + 'ᾀ' => 'ᾀ', + 'ᾁ' => 'ᾁ', + 'ᾂ' => 'ᾂ', + 'ᾃ' => 'ᾃ', + 'ᾄ' => 'ᾄ', + 'ᾅ' => 'ᾅ', + 'ᾆ' => 'ᾆ', + 'ᾇ' => 'ᾇ', + 'ᾈ' => 'ᾈ', + 'ᾉ' => 'ᾉ', + 'ᾊ' => 'ᾊ', + 'ᾋ' => 'ᾋ', + 'ᾌ' => 'ᾌ', + 'ᾍ' => 'ᾍ', + 'ᾎ' => 'ᾎ', + 'ᾏ' => 'ᾏ', + 'ᾐ' => 'ᾐ', + 'ᾑ' => 'ᾑ', + 'ᾒ' => 'ᾒ', + 'ᾓ' => 'ᾓ', + 'ᾔ' => 'ᾔ', + 'ᾕ' => 'ᾕ', + 'ᾖ' => 'ᾖ', + 'ᾗ' => 'ᾗ', + 'ᾘ' => 'ᾘ', + 'ᾙ' => 'ᾙ', + 'ᾚ' => 'ᾚ', + 'ᾛ' => 'ᾛ', + 'ᾜ' => 'ᾜ', + 'ᾝ' => 'ᾝ', + 'ᾞ' => 'ᾞ', + 'ᾟ' => 'ᾟ', + 'ᾠ' => 'ᾠ', + 'ᾡ' => 'ᾡ', + 'ᾢ' => 'ᾢ', + 'ᾣ' => 'ᾣ', + 'ᾤ' => 'ᾤ', + 'ᾥ' => 'ᾥ', + 'ᾦ' => 'ᾦ', + 'ᾧ' => 'ᾧ', + 'ᾨ' => 'ᾨ', + 'ᾩ' => 'ᾩ', + 'ᾪ' => 'ᾪ', + 'ᾫ' => 'ᾫ', + 'ᾬ' => 'ᾬ', + 'ᾭ' => 'ᾭ', + 'ᾮ' => 'ᾮ', + 'ᾯ' => 'ᾯ', + 'ᾰ' => 'ᾰ', + 'ᾱ' => 'ᾱ', + 'ᾲ' => 'ᾲ', + 'ᾳ' => 'ᾳ', + 'ᾴ' => 'ᾴ', + 'ᾶ' => 'ᾶ', + 'ᾷ' => 'ᾷ', + 'Ᾰ' => 'Ᾰ', + 'Ᾱ' => 'Ᾱ', + 'Ὰ' => 'Ὰ', + 'Ά' => 'Ά', + 'ᾼ' => 'ᾼ', + 'ι' => 'ι', + '῁' => '῁', + 'ῂ' => 'ῂ', + 'ῃ' => 'ῃ', + 'ῄ' => 'ῄ', + 'ῆ' => 'ῆ', + 'ῇ' => 'ῇ', + 'Ὲ' => 'Ὲ', + 'Έ' => 'Έ', + 'Ὴ' => 'Ὴ', + 'Ή' => 'Ή', + 'ῌ' => 'ῌ', + '῍' => '῍', + '῎' => '῎', + '῏' => '῏', + 'ῐ' => 'ῐ', + 'ῑ' => 'ῑ', + 'ῒ' => 'ῒ', + 'ΐ' => 'ΐ', + 'ῖ' => 'ῖ', + 'ῗ' => 'ῗ', + 'Ῐ' => 'Ῐ', + 'Ῑ' => 'Ῑ', + 'Ὶ' => 'Ὶ', + 'Ί' => 'Ί', + '῝' => '῝', + '῞' => '῞', + '῟' => '῟', + 'ῠ' => 'ῠ', + 'ῡ' => 'ῡ', + 'ῢ' => 'ῢ', + 'ΰ' => 'ΰ', + 'ῤ' => 'ῤ', + 'ῥ' => 'ῥ', + 'ῦ' => 'ῦ', + 'ῧ' => 'ῧ', + 'Ῠ' => 'Ῠ', + 'Ῡ' => 'Ῡ', + 'Ὺ' => 'Ὺ', + 'Ύ' => 'Ύ', + 'Ῥ' => 'Ῥ', + '῭' => '῭', + '΅' => '΅', + '`' => '`', + 'ῲ' => 'ῲ', + 'ῳ' => 'ῳ', + 'ῴ' => 'ῴ', + 'ῶ' => 'ῶ', + 'ῷ' => 'ῷ', + 'Ὸ' => 'Ὸ', + 'Ό' => 'Ό', + 'Ὼ' => 'Ὼ', + 'Ώ' => 'Ώ', + 'ῼ' => 'ῼ', + '´' => '´', + ' ' => ' ', + ' ' => ' ', + 'Ω' => 'Ω', + 'K' => 'K', + 'Å' => 'Å', + '↚' => '↚', + '↛' => '↛', + '↮' => '↮', + '⇍' => '⇍', + '⇎' => '⇎', + '⇏' => '⇏', + '∄' => '∄', + '∉' => '∉', + '∌' => '∌', + '∤' => '∤', + '∦' => '∦', + '≁' => '≁', + '≄' => '≄', + '≇' => '≇', + '≉' => '≉', + '≠' => '≠', + '≢' => '≢', + '≭' => '≭', + '≮' => '≮', + '≯' => '≯', + '≰' => '≰', + '≱' => '≱', + '≴' => '≴', + '≵' => '≵', + '≸' => '≸', + '≹' => '≹', + '⊀' => '⊀', + '⊁' => '⊁', + '⊄' => '⊄', + '⊅' => '⊅', + '⊈' => '⊈', + '⊉' => '⊉', + '⊬' => '⊬', + '⊭' => '⊭', + '⊮' => '⊮', + '⊯' => '⊯', + '⋠' => '⋠', + '⋡' => '⋡', + '⋢' => '⋢', + '⋣' => '⋣', + '⋪' => '⋪', + '⋫' => '⋫', + '⋬' => '⋬', + '⋭' => '⋭', + '〈' => '〈', + '〉' => '〉', + '⫝̸' => '⫝̸', + 'が' => 'が', + 'ぎ' => 'ぎ', + 'ぐ' => 'ぐ', + 'げ' => 'げ', + 'ご' => 'ご', + 'ざ' => 'ざ', + 'じ' => 'じ', + 'ず' => 'ず', + 'ぜ' => 'ぜ', + 'ぞ' => 'ぞ', + 'だ' => 'だ', + 'ぢ' => 'ぢ', + 'づ' => 'づ', + 'で' => 'で', + 'ど' => 'ど', + 'ば' => 'ば', + 'ぱ' => 'ぱ', + 'び' => 'び', + 'ぴ' => 'ぴ', + 'ぶ' => 'ぶ', + 'ぷ' => 'ぷ', + 'べ' => 'べ', + 'ぺ' => 'ぺ', + 'ぼ' => 'ぼ', + 'ぽ' => 'ぽ', + 'ゔ' => 'ゔ', + 'ゞ' => 'ゞ', + 'ガ' => 'ガ', + 'ギ' => 'ギ', + 'グ' => 'グ', + 'ゲ' => 'ゲ', + 'ゴ' => 'ゴ', + 'ザ' => 'ザ', + 'ジ' => 'ジ', + 'ズ' => 'ズ', + 'ゼ' => 'ゼ', + 'ゾ' => 'ゾ', + 'ダ' => 'ダ', + 'ヂ' => 'ヂ', + 'ヅ' => 'ヅ', + 'デ' => 'デ', + 'ド' => 'ド', + 'バ' => 'バ', + 'パ' => 'パ', + 'ビ' => 'ビ', + 'ピ' => 'ピ', + 'ブ' => 'ブ', + 'プ' => 'プ', + 'ベ' => 'ベ', + 'ペ' => 'ペ', + 'ボ' => 'ボ', + 'ポ' => 'ポ', + 'ヴ' => 'ヴ', + 'ヷ' => 'ヷ', + 'ヸ' => 'ヸ', + 'ヹ' => 'ヹ', + 'ヺ' => 'ヺ', + 'ヾ' => 'ヾ', + '豈' => '豈', + '更' => '更', + '車' => '車', + '賈' => '賈', + '滑' => '滑', + '串' => '串', + '句' => '句', + '龜' => '龜', + '龜' => '龜', + '契' => '契', + '金' => '金', + '喇' => '喇', + '奈' => '奈', + '懶' => '懶', + '癩' => '癩', + '羅' => '羅', + '蘿' => '蘿', + '螺' => '螺', + '裸' => '裸', + '邏' => '邏', + '樂' => '樂', + '洛' => '洛', + '烙' => '烙', + '珞' => '珞', + '落' => '落', + '酪' => '酪', + '駱' => '駱', + '亂' => '亂', + '卵' => '卵', + '欄' => '欄', + '爛' => '爛', + '蘭' => '蘭', + '鸞' => '鸞', + '嵐' => '嵐', + '濫' => '濫', + '藍' => '藍', + '襤' => '襤', + '拉' => '拉', + '臘' => '臘', + '蠟' => '蠟', + '廊' => '廊', + '朗' => '朗', + '浪' => '浪', + '狼' => '狼', + '郎' => '郎', + '來' => '來', + '冷' => '冷', + '勞' => '勞', + '擄' => '擄', + '櫓' => '櫓', + '爐' => '爐', + '盧' => '盧', + '老' => '老', + '蘆' => '蘆', + '虜' => '虜', + '路' => '路', + '露' => '露', + '魯' => '魯', + '鷺' => '鷺', + '碌' => '碌', + '祿' => '祿', + '綠' => '綠', + '菉' => '菉', + '錄' => '錄', + '鹿' => '鹿', + '論' => '論', + '壟' => '壟', + '弄' => '弄', + '籠' => '籠', + '聾' => '聾', + '牢' => '牢', + '磊' => '磊', + '賂' => '賂', + '雷' => '雷', + '壘' => '壘', + '屢' => '屢', + '樓' => '樓', + '淚' => '淚', + '漏' => '漏', + '累' => '累', + '縷' => '縷', + '陋' => '陋', + '勒' => '勒', + '肋' => '肋', + '凜' => '凜', + '凌' => '凌', + '稜' => '稜', + '綾' => '綾', + '菱' => '菱', + '陵' => '陵', + '讀' => '讀', + '拏' => '拏', + '樂' => '樂', + '諾' => '諾', + '丹' => '丹', + '寧' => '寧', + '怒' => '怒', + '率' => '率', + '異' => '異', + '北' => '北', + '磻' => '磻', + '便' => '便', + '復' => '復', + '不' => '不', + '泌' => '泌', + '數' => '數', + '索' => '索', + '參' => '參', + '塞' => '塞', + '省' => '省', + '葉' => '葉', + '說' => '說', + '殺' => '殺', + '辰' => '辰', + '沈' => '沈', + '拾' => '拾', + '若' => '若', + '掠' => '掠', + '略' => '略', + '亮' => '亮', + '兩' => '兩', + '凉' => '凉', + '梁' => '梁', + '糧' => '糧', + '良' => '良', + '諒' => '諒', + '量' => '量', + '勵' => '勵', + '呂' => '呂', + '女' => '女', + '廬' => '廬', + '旅' => '旅', + '濾' => '濾', + '礪' => '礪', + '閭' => '閭', + '驪' => '驪', + '麗' => '麗', + '黎' => '黎', + '力' => '力', + '曆' => '曆', + '歷' => '歷', + '轢' => '轢', + '年' => '年', + '憐' => '憐', + '戀' => '戀', + '撚' => '撚', + '漣' => '漣', + '煉' => '煉', + '璉' => '璉', + '秊' => '秊', + '練' => '練', + '聯' => '聯', + '輦' => '輦', + '蓮' => '蓮', + '連' => '連', + '鍊' => '鍊', + '列' => '列', + '劣' => '劣', + '咽' => '咽', + '烈' => '烈', + '裂' => '裂', + '說' => '說', + '廉' => '廉', + '念' => '念', + '捻' => '捻', + '殮' => '殮', + '簾' => '簾', + '獵' => '獵', + '令' => '令', + '囹' => '囹', + '寧' => '寧', + '嶺' => '嶺', + '怜' => '怜', + '玲' => '玲', + '瑩' => '瑩', + '羚' => '羚', + '聆' => '聆', + '鈴' => '鈴', + '零' => '零', + '靈' => '靈', + '領' => '領', + '例' => '例', + '禮' => '禮', + '醴' => '醴', + '隸' => '隸', + '惡' => '惡', + '了' => '了', + '僚' => '僚', + '寮' => '寮', + '尿' => '尿', + '料' => '料', + '樂' => '樂', + '燎' => '燎', + '療' => '療', + '蓼' => '蓼', + '遼' => '遼', + '龍' => '龍', + '暈' => '暈', + '阮' => '阮', + '劉' => '劉', + '杻' => '杻', + '柳' => '柳', + '流' => '流', + '溜' => '溜', + '琉' => '琉', + '留' => '留', + '硫' => '硫', + '紐' => '紐', + '類' => '類', + '六' => '六', + '戮' => '戮', + '陸' => '陸', + '倫' => '倫', + '崙' => '崙', + '淪' => '淪', + '輪' => '輪', + '律' => '律', + '慄' => '慄', + '栗' => '栗', + '率' => '率', + '隆' => '隆', + '利' => '利', + '吏' => '吏', + '履' => '履', + '易' => '易', + '李' => '李', + '梨' => '梨', + '泥' => '泥', + '理' => '理', + '痢' => '痢', + '罹' => '罹', + '裏' => '裏', + '裡' => '裡', + '里' => '里', + '離' => '離', + '匿' => '匿', + '溺' => '溺', + '吝' => '吝', + '燐' => '燐', + '璘' => '璘', + '藺' => '藺', + '隣' => '隣', + '鱗' => '鱗', + '麟' => '麟', + '林' => '林', + '淋' => '淋', + '臨' => '臨', + '立' => '立', + '笠' => '笠', + '粒' => '粒', + '狀' => '狀', + '炙' => '炙', + '識' => '識', + '什' => '什', + '茶' => '茶', + '刺' => '刺', + '切' => '切', + '度' => '度', + '拓' => '拓', + '糖' => '糖', + '宅' => '宅', + '洞' => '洞', + '暴' => '暴', + '輻' => '輻', + '行' => '行', + '降' => '降', + '見' => '見', + '廓' => '廓', + '兀' => '兀', + '嗀' => '嗀', + '塚' => '塚', + '晴' => '晴', + '凞' => '凞', + '猪' => '猪', + '益' => '益', + '礼' => '礼', + '神' => '神', + '祥' => '祥', + '福' => '福', + '靖' => '靖', + '精' => '精', + '羽' => '羽', + '蘒' => '蘒', + '諸' => '諸', + '逸' => '逸', + '都' => '都', + '飯' => '飯', + '飼' => '飼', + '館' => '館', + '鶴' => '鶴', + '郞' => '郞', + '隷' => '隷', + '侮' => '侮', + '僧' => '僧', + '免' => '免', + '勉' => '勉', + '勤' => '勤', + '卑' => '卑', + '喝' => '喝', + '嘆' => '嘆', + '器' => '器', + '塀' => '塀', + '墨' => '墨', + '層' => '層', + '屮' => '屮', + '悔' => '悔', + '慨' => '慨', + '憎' => '憎', + '懲' => '懲', + '敏' => '敏', + '既' => '既', + '暑' => '暑', + '梅' => '梅', + '海' => '海', + '渚' => '渚', + '漢' => '漢', + '煮' => '煮', + '爫' => '爫', + '琢' => '琢', + '碑' => '碑', + '社' => '社', + '祉' => '祉', + '祈' => '祈', + '祐' => '祐', + '祖' => '祖', + '祝' => '祝', + '禍' => '禍', + '禎' => '禎', + '穀' => '穀', + '突' => '突', + '節' => '節', + '練' => '練', + '縉' => '縉', + '繁' => '繁', + '署' => '署', + '者' => '者', + '臭' => '臭', + '艹' => '艹', + '艹' => '艹', + '著' => '著', + '褐' => '褐', + '視' => '視', + '謁' => '謁', + '謹' => '謹', + '賓' => '賓', + '贈' => '贈', + '辶' => '辶', + '逸' => '逸', + '難' => '難', + '響' => '響', + '頻' => '頻', + '恵' => '恵', + '𤋮' => '𤋮', + '舘' => '舘', + '並' => '並', + '况' => '况', + '全' => '全', + '侀' => '侀', + '充' => '充', + '冀' => '冀', + '勇' => '勇', + '勺' => '勺', + '喝' => '喝', + '啕' => '啕', + '喙' => '喙', + '嗢' => '嗢', + '塚' => '塚', + '墳' => '墳', + '奄' => '奄', + '奔' => '奔', + '婢' => '婢', + '嬨' => '嬨', + '廒' => '廒', + '廙' => '廙', + '彩' => '彩', + '徭' => '徭', + '惘' => '惘', + '慎' => '慎', + '愈' => '愈', + '憎' => '憎', + '慠' => '慠', + '懲' => '懲', + '戴' => '戴', + '揄' => '揄', + '搜' => '搜', + '摒' => '摒', + '敖' => '敖', + '晴' => '晴', + '朗' => '朗', + '望' => '望', + '杖' => '杖', + '歹' => '歹', + '殺' => '殺', + '流' => '流', + '滛' => '滛', + '滋' => '滋', + '漢' => '漢', + '瀞' => '瀞', + '煮' => '煮', + '瞧' => '瞧', + '爵' => '爵', + '犯' => '犯', + '猪' => '猪', + '瑱' => '瑱', + '甆' => '甆', + '画' => '画', + '瘝' => '瘝', + '瘟' => '瘟', + '益' => '益', + '盛' => '盛', + '直' => '直', + '睊' => '睊', + '着' => '着', + '磌' => '磌', + '窱' => '窱', + '節' => '節', + '类' => '类', + '絛' => '絛', + '練' => '練', + '缾' => '缾', + '者' => '者', + '荒' => '荒', + '華' => '華', + '蝹' => '蝹', + '襁' => '襁', + '覆' => '覆', + '視' => '視', + '調' => '調', + '諸' => '諸', + '請' => '請', + '謁' => '謁', + '諾' => '諾', + '諭' => '諭', + '謹' => '謹', + '變' => '變', + '贈' => '贈', + '輸' => '輸', + '遲' => '遲', + '醙' => '醙', + '鉶' => '鉶', + '陼' => '陼', + '難' => '難', + '靖' => '靖', + '韛' => '韛', + '響' => '響', + '頋' => '頋', + '頻' => '頻', + '鬒' => '鬒', + '龜' => '龜', + '𢡊' => '𢡊', + '𢡄' => '𢡄', + '𣏕' => '𣏕', + '㮝' => '㮝', + '䀘' => '䀘', + '䀹' => '䀹', + '𥉉' => '𥉉', + '𥳐' => '𥳐', + '𧻓' => '𧻓', + '齃' => '齃', + '龎' => '龎', + 'יִ' => 'יִ', + 'ײַ' => 'ײַ', + 'שׁ' => 'שׁ', + 'שׂ' => 'שׂ', + 'שּׁ' => 'שּׁ', + 'שּׂ' => 'שּׂ', + 'אַ' => 'אַ', + 'אָ' => 'אָ', + 'אּ' => 'אּ', + 'בּ' => 'בּ', + 'גּ' => 'גּ', + 'דּ' => 'דּ', + 'הּ' => 'הּ', + 'וּ' => 'וּ', + 'זּ' => 'זּ', + 'טּ' => 'טּ', + 'יּ' => 'יּ', + 'ךּ' => 'ךּ', + 'כּ' => 'כּ', + 'לּ' => 'לּ', + 'מּ' => 'מּ', + 'נּ' => 'נּ', + 'סּ' => 'סּ', + 'ףּ' => 'ףּ', + 'פּ' => 'פּ', + 'צּ' => 'צּ', + 'קּ' => 'קּ', + 'רּ' => 'רּ', + 'שּ' => 'שּ', + 'תּ' => 'תּ', + 'וֹ' => 'וֹ', + 'בֿ' => 'בֿ', + 'כֿ' => 'כֿ', + 'פֿ' => 'פֿ', + '𑂚' => '𑂚', + '𑂜' => '𑂜', + '𑂫' => '𑂫', + '𑄮' => '𑄮', + '𑄯' => '𑄯', + '𑍋' => '𑍋', + '𑍌' => '𑍌', + '𑒻' => '𑒻', + '𑒼' => '𑒼', + '𑒾' => '𑒾', + '𑖺' => '𑖺', + '𑖻' => '𑖻', + '𑤸' => '𑤸', + '𝅗𝅥' => '𝅗𝅥', + '𝅘𝅥' => '𝅘𝅥', + '𝅘𝅥𝅮' => '𝅘𝅥𝅮', + '𝅘𝅥𝅯' => '𝅘𝅥𝅯', + '𝅘𝅥𝅰' => '𝅘𝅥𝅰', + '𝅘𝅥𝅱' => '𝅘𝅥𝅱', + '𝅘𝅥𝅲' => '𝅘𝅥𝅲', + '𝆹𝅥' => '𝆹𝅥', + '𝆺𝅥' => '𝆺𝅥', + '𝆹𝅥𝅮' => '𝆹𝅥𝅮', + '𝆺𝅥𝅮' => '𝆺𝅥𝅮', + '𝆹𝅥𝅯' => '𝆹𝅥𝅯', + '𝆺𝅥𝅯' => '𝆺𝅥𝅯', + '丽' => '丽', + '丸' => '丸', + '乁' => '乁', + '𠄢' => '𠄢', + '你' => '你', + '侮' => '侮', + '侻' => '侻', + '倂' => '倂', + '偺' => '偺', + '備' => '備', + '僧' => '僧', + '像' => '像', + '㒞' => '㒞', + '𠘺' => '𠘺', + '免' => '免', + '兔' => '兔', + '兤' => '兤', + '具' => '具', + '𠔜' => '𠔜', + '㒹' => '㒹', + '內' => '內', + '再' => '再', + '𠕋' => '𠕋', + '冗' => '冗', + '冤' => '冤', + '仌' => '仌', + '冬' => '冬', + '况' => '况', + '𩇟' => '𩇟', + '凵' => '凵', + '刃' => '刃', + '㓟' => '㓟', + '刻' => '刻', + '剆' => '剆', + '割' => '割', + '剷' => '剷', + '㔕' => '㔕', + '勇' => '勇', + '勉' => '勉', + '勤' => '勤', + '勺' => '勺', + '包' => '包', + '匆' => '匆', + '北' => '北', + '卉' => '卉', + '卑' => '卑', + '博' => '博', + '即' => '即', + '卽' => '卽', + '卿' => '卿', + '卿' => '卿', + '卿' => '卿', + '𠨬' => '𠨬', + '灰' => '灰', + '及' => '及', + '叟' => '叟', + '𠭣' => '𠭣', + '叫' => '叫', + '叱' => '叱', + '吆' => '吆', + '咞' => '咞', + '吸' => '吸', + '呈' => '呈', + '周' => '周', + '咢' => '咢', + '哶' => '哶', + '唐' => '唐', + '啓' => '啓', + '啣' => '啣', + '善' => '善', + '善' => '善', + '喙' => '喙', + '喫' => '喫', + '喳' => '喳', + '嗂' => '嗂', + '圖' => '圖', + '嘆' => '嘆', + '圗' => '圗', + '噑' => '噑', + '噴' => '噴', + '切' => '切', + '壮' => '壮', + '城' => '城', + '埴' => '埴', + '堍' => '堍', + '型' => '型', + '堲' => '堲', + '報' => '報', + '墬' => '墬', + '𡓤' => '𡓤', + '売' => '売', + '壷' => '壷', + '夆' => '夆', + '多' => '多', + '夢' => '夢', + '奢' => '奢', + '𡚨' => '𡚨', + '𡛪' => '𡛪', + '姬' => '姬', + '娛' => '娛', + '娧' => '娧', + '姘' => '姘', + '婦' => '婦', + '㛮' => '㛮', + '㛼' => '㛼', + '嬈' => '嬈', + '嬾' => '嬾', + '嬾' => '嬾', + '𡧈' => '𡧈', + '寃' => '寃', + '寘' => '寘', + '寧' => '寧', + '寳' => '寳', + '𡬘' => '𡬘', + '寿' => '寿', + '将' => '将', + '当' => '当', + '尢' => '尢', + '㞁' => '㞁', + '屠' => '屠', + '屮' => '屮', + '峀' => '峀', + '岍' => '岍', + '𡷤' => '𡷤', + '嵃' => '嵃', + '𡷦' => '𡷦', + '嵮' => '嵮', + '嵫' => '嵫', + '嵼' => '嵼', + '巡' => '巡', + '巢' => '巢', + '㠯' => '㠯', + '巽' => '巽', + '帨' => '帨', + '帽' => '帽', + '幩' => '幩', + '㡢' => '㡢', + '𢆃' => '𢆃', + '㡼' => '㡼', + '庰' => '庰', + '庳' => '庳', + '庶' => '庶', + '廊' => '廊', + '𪎒' => '𪎒', + '廾' => '廾', + '𢌱' => '𢌱', + '𢌱' => '𢌱', + '舁' => '舁', + '弢' => '弢', + '弢' => '弢', + '㣇' => '㣇', + '𣊸' => '𣊸', + '𦇚' => '𦇚', + '形' => '形', + '彫' => '彫', + '㣣' => '㣣', + '徚' => '徚', + '忍' => '忍', + '志' => '志', + '忹' => '忹', + '悁' => '悁', + '㤺' => '㤺', + '㤜' => '㤜', + '悔' => '悔', + '𢛔' => '𢛔', + '惇' => '惇', + '慈' => '慈', + '慌' => '慌', + '慎' => '慎', + '慌' => '慌', + '慺' => '慺', + '憎' => '憎', + '憲' => '憲', + '憤' => '憤', + '憯' => '憯', + '懞' => '懞', + '懲' => '懲', + '懶' => '懶', + '成' => '成', + '戛' => '戛', + '扝' => '扝', + '抱' => '抱', + '拔' => '拔', + '捐' => '捐', + '𢬌' => '𢬌', + '挽' => '挽', + '拼' => '拼', + '捨' => '捨', + '掃' => '掃', + '揤' => '揤', + '𢯱' => '𢯱', + '搢' => '搢', + '揅' => '揅', + '掩' => '掩', + '㨮' => '㨮', + '摩' => '摩', + '摾' => '摾', + '撝' => '撝', + '摷' => '摷', + '㩬' => '㩬', + '敏' => '敏', + '敬' => '敬', + '𣀊' => '𣀊', + '旣' => '旣', + '書' => '書', + '晉' => '晉', + '㬙' => '㬙', + '暑' => '暑', + '㬈' => '㬈', + '㫤' => '㫤', + '冒' => '冒', + '冕' => '冕', + '最' => '最', + '暜' => '暜', + '肭' => '肭', + '䏙' => '䏙', + '朗' => '朗', + '望' => '望', + '朡' => '朡', + '杞' => '杞', + '杓' => '杓', + '𣏃' => '𣏃', + '㭉' => '㭉', + '柺' => '柺', + '枅' => '枅', + '桒' => '桒', + '梅' => '梅', + '𣑭' => '𣑭', + '梎' => '梎', + '栟' => '栟', + '椔' => '椔', + '㮝' => '㮝', + '楂' => '楂', + '榣' => '榣', + '槪' => '槪', + '檨' => '檨', + '𣚣' => '𣚣', + '櫛' => '櫛', + '㰘' => '㰘', + '次' => '次', + '𣢧' => '𣢧', + '歔' => '歔', + '㱎' => '㱎', + '歲' => '歲', + '殟' => '殟', + '殺' => '殺', + '殻' => '殻', + '𣪍' => '𣪍', + '𡴋' => '𡴋', + '𣫺' => '𣫺', + '汎' => '汎', + '𣲼' => '𣲼', + '沿' => '沿', + '泍' => '泍', + '汧' => '汧', + '洖' => '洖', + '派' => '派', + '海' => '海', + '流' => '流', + '浩' => '浩', + '浸' => '浸', + '涅' => '涅', + '𣴞' => '𣴞', + '洴' => '洴', + '港' => '港', + '湮' => '湮', + '㴳' => '㴳', + '滋' => '滋', + '滇' => '滇', + '𣻑' => '𣻑', + '淹' => '淹', + '潮' => '潮', + '𣽞' => '𣽞', + '𣾎' => '𣾎', + '濆' => '濆', + '瀹' => '瀹', + '瀞' => '瀞', + '瀛' => '瀛', + '㶖' => '㶖', + '灊' => '灊', + '災' => '災', + '灷' => '灷', + '炭' => '炭', + '𠔥' => '𠔥', + '煅' => '煅', + '𤉣' => '𤉣', + '熜' => '熜', + '𤎫' => '𤎫', + '爨' => '爨', + '爵' => '爵', + '牐' => '牐', + '𤘈' => '𤘈', + '犀' => '犀', + '犕' => '犕', + '𤜵' => '𤜵', + '𤠔' => '𤠔', + '獺' => '獺', + '王' => '王', + '㺬' => '㺬', + '玥' => '玥', + '㺸' => '㺸', + '㺸' => '㺸', + '瑇' => '瑇', + '瑜' => '瑜', + '瑱' => '瑱', + '璅' => '璅', + '瓊' => '瓊', + '㼛' => '㼛', + '甤' => '甤', + '𤰶' => '𤰶', + '甾' => '甾', + '𤲒' => '𤲒', + '異' => '異', + '𢆟' => '𢆟', + '瘐' => '瘐', + '𤾡' => '𤾡', + '𤾸' => '𤾸', + '𥁄' => '𥁄', + '㿼' => '㿼', + '䀈' => '䀈', + '直' => '直', + '𥃳' => '𥃳', + '𥃲' => '𥃲', + '𥄙' => '𥄙', + '𥄳' => '𥄳', + '眞' => '眞', + '真' => '真', + '真' => '真', + '睊' => '睊', + '䀹' => '䀹', + '瞋' => '瞋', + '䁆' => '䁆', + '䂖' => '䂖', + '𥐝' => '𥐝', + '硎' => '硎', + '碌' => '碌', + '磌' => '磌', + '䃣' => '䃣', + '𥘦' => '𥘦', + '祖' => '祖', + '𥚚' => '𥚚', + '𥛅' => '𥛅', + '福' => '福', + '秫' => '秫', + '䄯' => '䄯', + '穀' => '穀', + '穊' => '穊', + '穏' => '穏', + '𥥼' => '𥥼', + '𥪧' => '𥪧', + '𥪧' => '𥪧', + '竮' => '竮', + '䈂' => '䈂', + '𥮫' => '𥮫', + '篆' => '篆', + '築' => '築', + '䈧' => '䈧', + '𥲀' => '𥲀', + '糒' => '糒', + '䊠' => '䊠', + '糨' => '糨', + '糣' => '糣', + '紀' => '紀', + '𥾆' => '𥾆', + '絣' => '絣', + '䌁' => '䌁', + '緇' => '緇', + '縂' => '縂', + '繅' => '繅', + '䌴' => '䌴', + '𦈨' => '𦈨', + '𦉇' => '𦉇', + '䍙' => '䍙', + '𦋙' => '𦋙', + '罺' => '罺', + '𦌾' => '𦌾', + '羕' => '羕', + '翺' => '翺', + '者' => '者', + '𦓚' => '𦓚', + '𦔣' => '𦔣', + '聠' => '聠', + '𦖨' => '𦖨', + '聰' => '聰', + '𣍟' => '𣍟', + '䏕' => '䏕', + '育' => '育', + '脃' => '脃', + '䐋' => '䐋', + '脾' => '脾', + '媵' => '媵', + '𦞧' => '𦞧', + '𦞵' => '𦞵', + '𣎓' => '𣎓', + '𣎜' => '𣎜', + '舁' => '舁', + '舄' => '舄', + '辞' => '辞', + '䑫' => '䑫', + '芑' => '芑', + '芋' => '芋', + '芝' => '芝', + '劳' => '劳', + '花' => '花', + '芳' => '芳', + '芽' => '芽', + '苦' => '苦', + '𦬼' => '𦬼', + '若' => '若', + '茝' => '茝', + '荣' => '荣', + '莭' => '莭', + '茣' => '茣', + '莽' => '莽', + '菧' => '菧', + '著' => '著', + '荓' => '荓', + '菊' => '菊', + '菌' => '菌', + '菜' => '菜', + '𦰶' => '𦰶', + '𦵫' => '𦵫', + '𦳕' => '𦳕', + '䔫' => '䔫', + '蓱' => '蓱', + '蓳' => '蓳', + '蔖' => '蔖', + '𧏊' => '𧏊', + '蕤' => '蕤', + '𦼬' => '𦼬', + '䕝' => '䕝', + '䕡' => '䕡', + '𦾱' => '𦾱', + '𧃒' => '𧃒', + '䕫' => '䕫', + '虐' => '虐', + '虜' => '虜', + '虧' => '虧', + '虩' => '虩', + '蚩' => '蚩', + '蚈' => '蚈', + '蜎' => '蜎', + '蛢' => '蛢', + '蝹' => '蝹', + '蜨' => '蜨', + '蝫' => '蝫', + '螆' => '螆', + '䗗' => '䗗', + '蟡' => '蟡', + '蠁' => '蠁', + '䗹' => '䗹', + '衠' => '衠', + '衣' => '衣', + '𧙧' => '𧙧', + '裗' => '裗', + '裞' => '裞', + '䘵' => '䘵', + '裺' => '裺', + '㒻' => '㒻', + '𧢮' => '𧢮', + '𧥦' => '𧥦', + '䚾' => '䚾', + '䛇' => '䛇', + '誠' => '誠', + '諭' => '諭', + '變' => '變', + '豕' => '豕', + '𧲨' => '𧲨', + '貫' => '貫', + '賁' => '賁', + '贛' => '贛', + '起' => '起', + '𧼯' => '𧼯', + '𠠄' => '𠠄', + '跋' => '跋', + '趼' => '趼', + '跰' => '跰', + '𠣞' => '𠣞', + '軔' => '軔', + '輸' => '輸', + '𨗒' => '𨗒', + '𨗭' => '𨗭', + '邔' => '邔', + '郱' => '郱', + '鄑' => '鄑', + '𨜮' => '𨜮', + '鄛' => '鄛', + '鈸' => '鈸', + '鋗' => '鋗', + '鋘' => '鋘', + '鉼' => '鉼', + '鏹' => '鏹', + '鐕' => '鐕', + '𨯺' => '𨯺', + '開' => '開', + '䦕' => '䦕', + '閷' => '閷', + '𨵷' => '𨵷', + '䧦' => '䧦', + '雃' => '雃', + '嶲' => '嶲', + '霣' => '霣', + '𩅅' => '𩅅', + '𩈚' => '𩈚', + '䩮' => '䩮', + '䩶' => '䩶', + '韠' => '韠', + '𩐊' => '𩐊', + '䪲' => '䪲', + '𩒖' => '𩒖', + '頋' => '頋', + '頋' => '頋', + '頩' => '頩', + '𩖶' => '𩖶', + '飢' => '飢', + '䬳' => '䬳', + '餩' => '餩', + '馧' => '馧', + '駂' => '駂', + '駾' => '駾', + '䯎' => '䯎', + '𩬰' => '𩬰', + '鬒' => '鬒', + '鱀' => '鱀', + '鳽' => '鳽', + '䳎' => '䳎', + '䳭' => '䳭', + '鵧' => '鵧', + '𪃎' => '𪃎', + '䳸' => '䳸', + '𪄅' => '𪄅', + '𪈎' => '𪈎', + '𪊑' => '𪊑', + '麻' => '麻', + '䵖' => '䵖', + '黹' => '黹', + '黾' => '黾', + '鼅' => '鼅', + '鼏' => '鼏', + '鼖' => '鼖', + '鼻' => '鼻', + '𪘀' => '𪘀', +); diff --git a/3rdparty/symfony/polyfill-intl-normalizer/Resources/unidata/combiningClass.php b/3rdparty/symfony/polyfill-intl-normalizer/Resources/unidata/combiningClass.php new file mode 100644 index 00000000..ec90f36e --- /dev/null +++ b/3rdparty/symfony/polyfill-intl-normalizer/Resources/unidata/combiningClass.php @@ -0,0 +1,876 @@ + 230, + '́' => 230, + '̂' => 230, + '̃' => 230, + '̄' => 230, + '̅' => 230, + '̆' => 230, + '̇' => 230, + '̈' => 230, + '̉' => 230, + '̊' => 230, + '̋' => 230, + '̌' => 230, + '̍' => 230, + '̎' => 230, + '̏' => 230, + '̐' => 230, + '̑' => 230, + '̒' => 230, + '̓' => 230, + '̔' => 230, + '̕' => 232, + '̖' => 220, + '̗' => 220, + '̘' => 220, + '̙' => 220, + '̚' => 232, + '̛' => 216, + '̜' => 220, + '̝' => 220, + '̞' => 220, + '̟' => 220, + '̠' => 220, + '̡' => 202, + '̢' => 202, + '̣' => 220, + '̤' => 220, + '̥' => 220, + '̦' => 220, + '̧' => 202, + '̨' => 202, + '̩' => 220, + '̪' => 220, + '̫' => 220, + '̬' => 220, + '̭' => 220, + '̮' => 220, + '̯' => 220, + '̰' => 220, + '̱' => 220, + '̲' => 220, + '̳' => 220, + '̴' => 1, + '̵' => 1, + '̶' => 1, + '̷' => 1, + '̸' => 1, + '̹' => 220, + '̺' => 220, + '̻' => 220, + '̼' => 220, + '̽' => 230, + '̾' => 230, + '̿' => 230, + '̀' => 230, + '́' => 230, + '͂' => 230, + '̓' => 230, + '̈́' => 230, + 'ͅ' => 240, + '͆' => 230, + '͇' => 220, + '͈' => 220, + '͉' => 220, + '͊' => 230, + '͋' => 230, + '͌' => 230, + '͍' => 220, + '͎' => 220, + '͐' => 230, + '͑' => 230, + '͒' => 230, + '͓' => 220, + '͔' => 220, + '͕' => 220, + '͖' => 220, + '͗' => 230, + '͘' => 232, + '͙' => 220, + '͚' => 220, + '͛' => 230, + '͜' => 233, + '͝' => 234, + '͞' => 234, + '͟' => 233, + '͠' => 234, + '͡' => 234, + '͢' => 233, + 'ͣ' => 230, + 'ͤ' => 230, + 'ͥ' => 230, + 'ͦ' => 230, + 'ͧ' => 230, + 'ͨ' => 230, + 'ͩ' => 230, + 'ͪ' => 230, + 'ͫ' => 230, + 'ͬ' => 230, + 'ͭ' => 230, + 'ͮ' => 230, + 'ͯ' => 230, + '҃' => 230, + '҄' => 230, + '҅' => 230, + '҆' => 230, + '҇' => 230, + '֑' => 220, + '֒' => 230, + '֓' => 230, + '֔' => 230, + '֕' => 230, + '֖' => 220, + '֗' => 230, + '֘' => 230, + '֙' => 230, + '֚' => 222, + '֛' => 220, + '֜' => 230, + '֝' => 230, + '֞' => 230, + '֟' => 230, + '֠' => 230, + '֡' => 230, + '֢' => 220, + '֣' => 220, + '֤' => 220, + '֥' => 220, + '֦' => 220, + '֧' => 220, + '֨' => 230, + '֩' => 230, + '֪' => 220, + '֫' => 230, + '֬' => 230, + '֭' => 222, + '֮' => 228, + '֯' => 230, + 'ְ' => 10, + 'ֱ' => 11, + 'ֲ' => 12, + 'ֳ' => 13, + 'ִ' => 14, + 'ֵ' => 15, + 'ֶ' => 16, + 'ַ' => 17, + 'ָ' => 18, + 'ֹ' => 19, + 'ֺ' => 19, + 'ֻ' => 20, + 'ּ' => 21, + 'ֽ' => 22, + 'ֿ' => 23, + 'ׁ' => 24, + 'ׂ' => 25, + 'ׄ' => 230, + 'ׅ' => 220, + 'ׇ' => 18, + 'ؐ' => 230, + 'ؑ' => 230, + 'ؒ' => 230, + 'ؓ' => 230, + 'ؔ' => 230, + 'ؕ' => 230, + 'ؖ' => 230, + 'ؗ' => 230, + 'ؘ' => 30, + 'ؙ' => 31, + 'ؚ' => 32, + 'ً' => 27, + 'ٌ' => 28, + 'ٍ' => 29, + 'َ' => 30, + 'ُ' => 31, + 'ِ' => 32, + 'ّ' => 33, + 'ْ' => 34, + 'ٓ' => 230, + 'ٔ' => 230, + 'ٕ' => 220, + 'ٖ' => 220, + 'ٗ' => 230, + '٘' => 230, + 'ٙ' => 230, + 'ٚ' => 230, + 'ٛ' => 230, + 'ٜ' => 220, + 'ٝ' => 230, + 'ٞ' => 230, + 'ٟ' => 220, + 'ٰ' => 35, + 'ۖ' => 230, + 'ۗ' => 230, + 'ۘ' => 230, + 'ۙ' => 230, + 'ۚ' => 230, + 'ۛ' => 230, + 'ۜ' => 230, + '۟' => 230, + '۠' => 230, + 'ۡ' => 230, + 'ۢ' => 230, + 'ۣ' => 220, + 'ۤ' => 230, + 'ۧ' => 230, + 'ۨ' => 230, + '۪' => 220, + '۫' => 230, + '۬' => 230, + 'ۭ' => 220, + 'ܑ' => 36, + 'ܰ' => 230, + 'ܱ' => 220, + 'ܲ' => 230, + 'ܳ' => 230, + 'ܴ' => 220, + 'ܵ' => 230, + 'ܶ' => 230, + 'ܷ' => 220, + 'ܸ' => 220, + 'ܹ' => 220, + 'ܺ' => 230, + 'ܻ' => 220, + 'ܼ' => 220, + 'ܽ' => 230, + 'ܾ' => 220, + 'ܿ' => 230, + '݀' => 230, + '݁' => 230, + '݂' => 220, + '݃' => 230, + '݄' => 220, + '݅' => 230, + '݆' => 220, + '݇' => 230, + '݈' => 220, + '݉' => 230, + '݊' => 230, + '߫' => 230, + '߬' => 230, + '߭' => 230, + '߮' => 230, + '߯' => 230, + '߰' => 230, + '߱' => 230, + '߲' => 220, + '߳' => 230, + '߽' => 220, + 'ࠖ' => 230, + 'ࠗ' => 230, + '࠘' => 230, + '࠙' => 230, + 'ࠛ' => 230, + 'ࠜ' => 230, + 'ࠝ' => 230, + 'ࠞ' => 230, + 'ࠟ' => 230, + 'ࠠ' => 230, + 'ࠡ' => 230, + 'ࠢ' => 230, + 'ࠣ' => 230, + 'ࠥ' => 230, + 'ࠦ' => 230, + 'ࠧ' => 230, + 'ࠩ' => 230, + 'ࠪ' => 230, + 'ࠫ' => 230, + 'ࠬ' => 230, + '࠭' => 230, + '࡙' => 220, + '࡚' => 220, + '࡛' => 220, + '࣓' => 220, + 'ࣔ' => 230, + 'ࣕ' => 230, + 'ࣖ' => 230, + 'ࣗ' => 230, + 'ࣘ' => 230, + 'ࣙ' => 230, + 'ࣚ' => 230, + 'ࣛ' => 230, + 'ࣜ' => 230, + 'ࣝ' => 230, + 'ࣞ' => 230, + 'ࣟ' => 230, + '࣠' => 230, + '࣡' => 230, + 'ࣣ' => 220, + 'ࣤ' => 230, + 'ࣥ' => 230, + 'ࣦ' => 220, + 'ࣧ' => 230, + 'ࣨ' => 230, + 'ࣩ' => 220, + '࣪' => 230, + '࣫' => 230, + '࣬' => 230, + '࣭' => 220, + '࣮' => 220, + '࣯' => 220, + 'ࣰ' => 27, + 'ࣱ' => 28, + 'ࣲ' => 29, + 'ࣳ' => 230, + 'ࣴ' => 230, + 'ࣵ' => 230, + 'ࣶ' => 220, + 'ࣷ' => 230, + 'ࣸ' => 230, + 'ࣹ' => 220, + 'ࣺ' => 220, + 'ࣻ' => 230, + 'ࣼ' => 230, + 'ࣽ' => 230, + 'ࣾ' => 230, + 'ࣿ' => 230, + '़' => 7, + '्' => 9, + '॑' => 230, + '॒' => 220, + '॓' => 230, + '॔' => 230, + '়' => 7, + '্' => 9, + '৾' => 230, + '਼' => 7, + '੍' => 9, + '઼' => 7, + '્' => 9, + '଼' => 7, + '୍' => 9, + '்' => 9, + '్' => 9, + 'ౕ' => 84, + 'ౖ' => 91, + '಼' => 7, + '್' => 9, + '഻' => 9, + '഼' => 9, + '്' => 9, + '්' => 9, + 'ุ' => 103, + 'ู' => 103, + 'ฺ' => 9, + '่' => 107, + '้' => 107, + '๊' => 107, + '๋' => 107, + 'ຸ' => 118, + 'ູ' => 118, + '຺' => 9, + '່' => 122, + '້' => 122, + '໊' => 122, + '໋' => 122, + '༘' => 220, + '༙' => 220, + '༵' => 220, + '༷' => 220, + '༹' => 216, + 'ཱ' => 129, + 'ི' => 130, + 'ུ' => 132, + 'ེ' => 130, + 'ཻ' => 130, + 'ོ' => 130, + 'ཽ' => 130, + 'ྀ' => 130, + 'ྂ' => 230, + 'ྃ' => 230, + '྄' => 9, + '྆' => 230, + '྇' => 230, + '࿆' => 220, + '့' => 7, + '္' => 9, + '်' => 9, + 'ႍ' => 220, + '፝' => 230, + '፞' => 230, + '፟' => 230, + '᜔' => 9, + '᜴' => 9, + '្' => 9, + '៝' => 230, + 'ᢩ' => 228, + '᤹' => 222, + '᤺' => 230, + '᤻' => 220, + 'ᨗ' => 230, + 'ᨘ' => 220, + '᩠' => 9, + '᩵' => 230, + '᩶' => 230, + '᩷' => 230, + '᩸' => 230, + '᩹' => 230, + '᩺' => 230, + '᩻' => 230, + '᩼' => 230, + '᩿' => 220, + '᪰' => 230, + '᪱' => 230, + '᪲' => 230, + '᪳' => 230, + '᪴' => 230, + '᪵' => 220, + '᪶' => 220, + '᪷' => 220, + '᪸' => 220, + '᪹' => 220, + '᪺' => 220, + '᪻' => 230, + '᪼' => 230, + '᪽' => 220, + 'ᪿ' => 220, + 'ᫀ' => 220, + '᬴' => 7, + '᭄' => 9, + '᭫' => 230, + '᭬' => 220, + '᭭' => 230, + '᭮' => 230, + '᭯' => 230, + '᭰' => 230, + '᭱' => 230, + '᭲' => 230, + '᭳' => 230, + '᮪' => 9, + '᮫' => 9, + '᯦' => 7, + '᯲' => 9, + '᯳' => 9, + '᰷' => 7, + '᳐' => 230, + '᳑' => 230, + '᳒' => 230, + '᳔' => 1, + '᳕' => 220, + '᳖' => 220, + '᳗' => 220, + '᳘' => 220, + '᳙' => 220, + '᳚' => 230, + '᳛' => 230, + '᳜' => 220, + '᳝' => 220, + '᳞' => 220, + '᳟' => 220, + '᳠' => 230, + '᳢' => 1, + '᳣' => 1, + '᳤' => 1, + '᳥' => 1, + '᳦' => 1, + '᳧' => 1, + '᳨' => 1, + '᳭' => 220, + '᳴' => 230, + '᳸' => 230, + '᳹' => 230, + '᷀' => 230, + '᷁' => 230, + '᷂' => 220, + '᷃' => 230, + '᷄' => 230, + '᷅' => 230, + '᷆' => 230, + '᷇' => 230, + '᷈' => 230, + '᷉' => 230, + '᷊' => 220, + '᷋' => 230, + '᷌' => 230, + '᷍' => 234, + '᷎' => 214, + '᷏' => 220, + '᷐' => 202, + '᷑' => 230, + '᷒' => 230, + 'ᷓ' => 230, + 'ᷔ' => 230, + 'ᷕ' => 230, + 'ᷖ' => 230, + 'ᷗ' => 230, + 'ᷘ' => 230, + 'ᷙ' => 230, + 'ᷚ' => 230, + 'ᷛ' => 230, + 'ᷜ' => 230, + 'ᷝ' => 230, + 'ᷞ' => 230, + 'ᷟ' => 230, + 'ᷠ' => 230, + 'ᷡ' => 230, + 'ᷢ' => 230, + 'ᷣ' => 230, + 'ᷤ' => 230, + 'ᷥ' => 230, + 'ᷦ' => 230, + 'ᷧ' => 230, + 'ᷨ' => 230, + 'ᷩ' => 230, + 'ᷪ' => 230, + 'ᷫ' => 230, + 'ᷬ' => 230, + 'ᷭ' => 230, + 'ᷮ' => 230, + 'ᷯ' => 230, + 'ᷰ' => 230, + 'ᷱ' => 230, + 'ᷲ' => 230, + 'ᷳ' => 230, + 'ᷴ' => 230, + '᷵' => 230, + '᷶' => 232, + '᷷' => 228, + '᷸' => 228, + '᷹' => 220, + '᷻' => 230, + '᷼' => 233, + '᷽' => 220, + '᷾' => 230, + '᷿' => 220, + '⃐' => 230, + '⃑' => 230, + '⃒' => 1, + '⃓' => 1, + '⃔' => 230, + '⃕' => 230, + '⃖' => 230, + '⃗' => 230, + '⃘' => 1, + '⃙' => 1, + '⃚' => 1, + '⃛' => 230, + '⃜' => 230, + '⃡' => 230, + '⃥' => 1, + '⃦' => 1, + '⃧' => 230, + '⃨' => 220, + '⃩' => 230, + '⃪' => 1, + '⃫' => 1, + '⃬' => 220, + '⃭' => 220, + '⃮' => 220, + '⃯' => 220, + '⃰' => 230, + '⳯' => 230, + '⳰' => 230, + '⳱' => 230, + '⵿' => 9, + 'ⷠ' => 230, + 'ⷡ' => 230, + 'ⷢ' => 230, + 'ⷣ' => 230, + 'ⷤ' => 230, + 'ⷥ' => 230, + 'ⷦ' => 230, + 'ⷧ' => 230, + 'ⷨ' => 230, + 'ⷩ' => 230, + 'ⷪ' => 230, + 'ⷫ' => 230, + 'ⷬ' => 230, + 'ⷭ' => 230, + 'ⷮ' => 230, + 'ⷯ' => 230, + 'ⷰ' => 230, + 'ⷱ' => 230, + 'ⷲ' => 230, + 'ⷳ' => 230, + 'ⷴ' => 230, + 'ⷵ' => 230, + 'ⷶ' => 230, + 'ⷷ' => 230, + 'ⷸ' => 230, + 'ⷹ' => 230, + 'ⷺ' => 230, + 'ⷻ' => 230, + 'ⷼ' => 230, + 'ⷽ' => 230, + 'ⷾ' => 230, + 'ⷿ' => 230, + '〪' => 218, + '〫' => 228, + '〬' => 232, + '〭' => 222, + '〮' => 224, + '〯' => 224, + '゙' => 8, + '゚' => 8, + '꙯' => 230, + 'ꙴ' => 230, + 'ꙵ' => 230, + 'ꙶ' => 230, + 'ꙷ' => 230, + 'ꙸ' => 230, + 'ꙹ' => 230, + 'ꙺ' => 230, + 'ꙻ' => 230, + '꙼' => 230, + '꙽' => 230, + 'ꚞ' => 230, + 'ꚟ' => 230, + '꛰' => 230, + '꛱' => 230, + '꠆' => 9, + '꠬' => 9, + '꣄' => 9, + '꣠' => 230, + '꣡' => 230, + '꣢' => 230, + '꣣' => 230, + '꣤' => 230, + '꣥' => 230, + '꣦' => 230, + '꣧' => 230, + '꣨' => 230, + '꣩' => 230, + '꣪' => 230, + '꣫' => 230, + '꣬' => 230, + '꣭' => 230, + '꣮' => 230, + '꣯' => 230, + '꣰' => 230, + '꣱' => 230, + '꤫' => 220, + '꤬' => 220, + '꤭' => 220, + '꥓' => 9, + '꦳' => 7, + '꧀' => 9, + 'ꪰ' => 230, + 'ꪲ' => 230, + 'ꪳ' => 230, + 'ꪴ' => 220, + 'ꪷ' => 230, + 'ꪸ' => 230, + 'ꪾ' => 230, + '꪿' => 230, + '꫁' => 230, + '꫶' => 9, + '꯭' => 9, + 'ﬞ' => 26, + '︠' => 230, + '︡' => 230, + '︢' => 230, + '︣' => 230, + '︤' => 230, + '︥' => 230, + '︦' => 230, + '︧' => 220, + '︨' => 220, + '︩' => 220, + '︪' => 220, + '︫' => 220, + '︬' => 220, + '︭' => 220, + '︮' => 230, + '︯' => 230, + '𐇽' => 220, + '𐋠' => 220, + '𐍶' => 230, + '𐍷' => 230, + '𐍸' => 230, + '𐍹' => 230, + '𐍺' => 230, + '𐨍' => 220, + '𐨏' => 230, + '𐨸' => 230, + '𐨹' => 1, + '𐨺' => 220, + '𐨿' => 9, + '𐫥' => 230, + '𐫦' => 220, + '𐴤' => 230, + '𐴥' => 230, + '𐴦' => 230, + '𐴧' => 230, + '𐺫' => 230, + '𐺬' => 230, + '𐽆' => 220, + '𐽇' => 220, + '𐽈' => 230, + '𐽉' => 230, + '𐽊' => 230, + '𐽋' => 220, + '𐽌' => 230, + '𐽍' => 220, + '𐽎' => 220, + '𐽏' => 220, + '𐽐' => 220, + '𑁆' => 9, + '𑁿' => 9, + '𑂹' => 9, + '𑂺' => 7, + '𑄀' => 230, + '𑄁' => 230, + '𑄂' => 230, + '𑄳' => 9, + '𑄴' => 9, + '𑅳' => 7, + '𑇀' => 9, + '𑇊' => 7, + '𑈵' => 9, + '𑈶' => 7, + '𑋩' => 7, + '𑋪' => 9, + '𑌻' => 7, + '𑌼' => 7, + '𑍍' => 9, + '𑍦' => 230, + '𑍧' => 230, + '𑍨' => 230, + '𑍩' => 230, + '𑍪' => 230, + '𑍫' => 230, + '𑍬' => 230, + '𑍰' => 230, + '𑍱' => 230, + '𑍲' => 230, + '𑍳' => 230, + '𑍴' => 230, + '𑑂' => 9, + '𑑆' => 7, + '𑑞' => 230, + '𑓂' => 9, + '𑓃' => 7, + '𑖿' => 9, + '𑗀' => 7, + '𑘿' => 9, + '𑚶' => 9, + '𑚷' => 7, + '𑜫' => 9, + '𑠹' => 9, + '𑠺' => 7, + '𑤽' => 9, + '𑤾' => 9, + '𑥃' => 7, + '𑧠' => 9, + '𑨴' => 9, + '𑩇' => 9, + '𑪙' => 9, + '𑰿' => 9, + '𑵂' => 7, + '𑵄' => 9, + '𑵅' => 9, + '𑶗' => 9, + '𖫰' => 1, + '𖫱' => 1, + '𖫲' => 1, + '𖫳' => 1, + '𖫴' => 1, + '𖬰' => 230, + '𖬱' => 230, + '𖬲' => 230, + '𖬳' => 230, + '𖬴' => 230, + '𖬵' => 230, + '𖬶' => 230, + '𖿰' => 6, + '𖿱' => 6, + '𛲞' => 1, + '𝅥' => 216, + '𝅦' => 216, + '𝅧' => 1, + '𝅨' => 1, + '𝅩' => 1, + '𝅭' => 226, + '𝅮' => 216, + '𝅯' => 216, + '𝅰' => 216, + '𝅱' => 216, + '𝅲' => 216, + '𝅻' => 220, + '𝅼' => 220, + '𝅽' => 220, + '𝅾' => 220, + '𝅿' => 220, + '𝆀' => 220, + '𝆁' => 220, + '𝆂' => 220, + '𝆅' => 230, + '𝆆' => 230, + '𝆇' => 230, + '𝆈' => 230, + '𝆉' => 230, + '𝆊' => 220, + '𝆋' => 220, + '𝆪' => 230, + '𝆫' => 230, + '𝆬' => 230, + '𝆭' => 230, + '𝉂' => 230, + '𝉃' => 230, + '𝉄' => 230, + '𞀀' => 230, + '𞀁' => 230, + '𞀂' => 230, + '𞀃' => 230, + '𞀄' => 230, + '𞀅' => 230, + '𞀆' => 230, + '𞀈' => 230, + '𞀉' => 230, + '𞀊' => 230, + '𞀋' => 230, + '𞀌' => 230, + '𞀍' => 230, + '𞀎' => 230, + '𞀏' => 230, + '𞀐' => 230, + '𞀑' => 230, + '𞀒' => 230, + '𞀓' => 230, + '𞀔' => 230, + '𞀕' => 230, + '𞀖' => 230, + '𞀗' => 230, + '𞀘' => 230, + '𞀛' => 230, + '𞀜' => 230, + '𞀝' => 230, + '𞀞' => 230, + '𞀟' => 230, + '𞀠' => 230, + '𞀡' => 230, + '𞀣' => 230, + '𞀤' => 230, + '𞀦' => 230, + '𞀧' => 230, + '𞀨' => 230, + '𞀩' => 230, + '𞀪' => 230, + '𞄰' => 230, + '𞄱' => 230, + '𞄲' => 230, + '𞄳' => 230, + '𞄴' => 230, + '𞄵' => 230, + '𞄶' => 230, + '𞋬' => 230, + '𞋭' => 230, + '𞋮' => 230, + '𞋯' => 230, + '𞣐' => 220, + '𞣑' => 220, + '𞣒' => 220, + '𞣓' => 220, + '𞣔' => 220, + '𞣕' => 220, + '𞣖' => 220, + '𞥄' => 230, + '𞥅' => 230, + '𞥆' => 230, + '𞥇' => 230, + '𞥈' => 230, + '𞥉' => 230, + '𞥊' => 7, +); diff --git a/3rdparty/symfony/polyfill-intl-normalizer/Resources/unidata/compatibilityDecomposition.php b/3rdparty/symfony/polyfill-intl-normalizer/Resources/unidata/compatibilityDecomposition.php new file mode 100644 index 00000000..15749028 --- /dev/null +++ b/3rdparty/symfony/polyfill-intl-normalizer/Resources/unidata/compatibilityDecomposition.php @@ -0,0 +1,3695 @@ + ' ', + '¨' => ' ̈', + 'ª' => 'a', + '¯' => ' ̄', + '²' => '2', + '³' => '3', + '´' => ' ́', + 'µ' => 'μ', + '¸' => ' ̧', + '¹' => '1', + 'º' => 'o', + '¼' => '1⁄4', + '½' => '1⁄2', + '¾' => '3⁄4', + 'IJ' => 'IJ', + 'ij' => 'ij', + 'Ŀ' => 'L·', + 'ŀ' => 'l·', + 'ʼn' => 'ʼn', + 'ſ' => 's', + 'DŽ' => 'DŽ', + 'Dž' => 'Dž', + 'dž' => 'dž', + 'LJ' => 'LJ', + 'Lj' => 'Lj', + 'lj' => 'lj', + 'NJ' => 'NJ', + 'Nj' => 'Nj', + 'nj' => 'nj', + 'DZ' => 'DZ', + 'Dz' => 'Dz', + 'dz' => 'dz', + 'ʰ' => 'h', + 'ʱ' => 'ɦ', + 'ʲ' => 'j', + 'ʳ' => 'r', + 'ʴ' => 'ɹ', + 'ʵ' => 'ɻ', + 'ʶ' => 'ʁ', + 'ʷ' => 'w', + 'ʸ' => 'y', + '˘' => ' ̆', + '˙' => ' ̇', + '˚' => ' ̊', + '˛' => ' ̨', + '˜' => ' ̃', + '˝' => ' ̋', + 'ˠ' => 'ɣ', + 'ˡ' => 'l', + 'ˢ' => 's', + 'ˣ' => 'x', + 'ˤ' => 'ʕ', + 'ͺ' => ' ͅ', + '΄' => ' ́', + '΅' => ' ̈́', + 'ϐ' => 'β', + 'ϑ' => 'θ', + 'ϒ' => 'Υ', + 'ϓ' => 'Ύ', + 'ϔ' => 'Ϋ', + 'ϕ' => 'φ', + 'ϖ' => 'π', + 'ϰ' => 'κ', + 'ϱ' => 'ρ', + 'ϲ' => 'ς', + 'ϴ' => 'Θ', + 'ϵ' => 'ε', + 'Ϲ' => 'Σ', + 'և' => 'եւ', + 'ٵ' => 'اٴ', + 'ٶ' => 'وٴ', + 'ٷ' => 'ۇٴ', + 'ٸ' => 'يٴ', + 'ำ' => 'ํา', + 'ຳ' => 'ໍາ', + 'ໜ' => 'ຫນ', + 'ໝ' => 'ຫມ', + '༌' => '་', + 'ཷ' => 'ྲཱྀ', + 'ཹ' => 'ླཱྀ', + 'ჼ' => 'ნ', + 'ᴬ' => 'A', + 'ᴭ' => 'Æ', + 'ᴮ' => 'B', + 'ᴰ' => 'D', + 'ᴱ' => 'E', + 'ᴲ' => 'Ǝ', + 'ᴳ' => 'G', + 'ᴴ' => 'H', + 'ᴵ' => 'I', + 'ᴶ' => 'J', + 'ᴷ' => 'K', + 'ᴸ' => 'L', + 'ᴹ' => 'M', + 'ᴺ' => 'N', + 'ᴼ' => 'O', + 'ᴽ' => 'Ȣ', + 'ᴾ' => 'P', + 'ᴿ' => 'R', + 'ᵀ' => 'T', + 'ᵁ' => 'U', + 'ᵂ' => 'W', + 'ᵃ' => 'a', + 'ᵄ' => 'ɐ', + 'ᵅ' => 'ɑ', + 'ᵆ' => 'ᴂ', + 'ᵇ' => 'b', + 'ᵈ' => 'd', + 'ᵉ' => 'e', + 'ᵊ' => 'ə', + 'ᵋ' => 'ɛ', + 'ᵌ' => 'ɜ', + 'ᵍ' => 'g', + 'ᵏ' => 'k', + 'ᵐ' => 'm', + 'ᵑ' => 'ŋ', + 'ᵒ' => 'o', + 'ᵓ' => 'ɔ', + 'ᵔ' => 'ᴖ', + 'ᵕ' => 'ᴗ', + 'ᵖ' => 'p', + 'ᵗ' => 't', + 'ᵘ' => 'u', + 'ᵙ' => 'ᴝ', + 'ᵚ' => 'ɯ', + 'ᵛ' => 'v', + 'ᵜ' => 'ᴥ', + 'ᵝ' => 'β', + 'ᵞ' => 'γ', + 'ᵟ' => 'δ', + 'ᵠ' => 'φ', + 'ᵡ' => 'χ', + 'ᵢ' => 'i', + 'ᵣ' => 'r', + 'ᵤ' => 'u', + 'ᵥ' => 'v', + 'ᵦ' => 'β', + 'ᵧ' => 'γ', + 'ᵨ' => 'ρ', + 'ᵩ' => 'φ', + 'ᵪ' => 'χ', + 'ᵸ' => 'н', + 'ᶛ' => 'ɒ', + 'ᶜ' => 'c', + 'ᶝ' => 'ɕ', + 'ᶞ' => 'ð', + 'ᶟ' => 'ɜ', + 'ᶠ' => 'f', + 'ᶡ' => 'ɟ', + 'ᶢ' => 'ɡ', + 'ᶣ' => 'ɥ', + 'ᶤ' => 'ɨ', + 'ᶥ' => 'ɩ', + 'ᶦ' => 'ɪ', + 'ᶧ' => 'ᵻ', + 'ᶨ' => 'ʝ', + 'ᶩ' => 'ɭ', + 'ᶪ' => 'ᶅ', + 'ᶫ' => 'ʟ', + 'ᶬ' => 'ɱ', + 'ᶭ' => 'ɰ', + 'ᶮ' => 'ɲ', + 'ᶯ' => 'ɳ', + 'ᶰ' => 'ɴ', + 'ᶱ' => 'ɵ', + 'ᶲ' => 'ɸ', + 'ᶳ' => 'ʂ', + 'ᶴ' => 'ʃ', + 'ᶵ' => 'ƫ', + 'ᶶ' => 'ʉ', + 'ᶷ' => 'ʊ', + 'ᶸ' => 'ᴜ', + 'ᶹ' => 'ʋ', + 'ᶺ' => 'ʌ', + 'ᶻ' => 'z', + 'ᶼ' => 'ʐ', + 'ᶽ' => 'ʑ', + 'ᶾ' => 'ʒ', + 'ᶿ' => 'θ', + 'ẚ' => 'aʾ', + 'ẛ' => 'ṡ', + '᾽' => ' ̓', + '᾿' => ' ̓', + '῀' => ' ͂', + '῁' => ' ̈͂', + '῍' => ' ̓̀', + '῎' => ' ̓́', + '῏' => ' ̓͂', + '῝' => ' ̔̀', + '῞' => ' ̔́', + '῟' => ' ̔͂', + '῭' => ' ̈̀', + '΅' => ' ̈́', + '´' => ' ́', + '῾' => ' ̔', + ' ' => ' ', + ' ' => ' ', + ' ' => ' ', + ' ' => ' ', + ' ' => ' ', + ' ' => ' ', + ' ' => ' ', + ' ' => ' ', + ' ' => ' ', + ' ' => ' ', + ' ' => ' ', + '‑' => '‐', + '‗' => ' ̳', + '․' => '.', + '‥' => '..', + '…' => '...', + ' ' => ' ', + '″' => '′′', + '‴' => '′′′', + '‶' => '‵‵', + '‷' => '‵‵‵', + '‼' => '!!', + '‾' => ' ̅', + '⁇' => '??', + '⁈' => '?!', + '⁉' => '!?', + '⁗' => '′′′′', + ' ' => ' ', + '⁰' => '0', + 'ⁱ' => 'i', + '⁴' => '4', + '⁵' => '5', + '⁶' => '6', + '⁷' => '7', + '⁸' => '8', + '⁹' => '9', + '⁺' => '+', + '⁻' => '−', + '⁼' => '=', + '⁽' => '(', + '⁾' => ')', + 'ⁿ' => 'n', + '₀' => '0', + '₁' => '1', + '₂' => '2', + '₃' => '3', + '₄' => '4', + '₅' => '5', + '₆' => '6', + '₇' => '7', + '₈' => '8', + '₉' => '9', + '₊' => '+', + '₋' => '−', + '₌' => '=', + '₍' => '(', + '₎' => ')', + 'ₐ' => 'a', + 'ₑ' => 'e', + 'ₒ' => 'o', + 'ₓ' => 'x', + 'ₔ' => 'ə', + 'ₕ' => 'h', + 'ₖ' => 'k', + 'ₗ' => 'l', + 'ₘ' => 'm', + 'ₙ' => 'n', + 'ₚ' => 'p', + 'ₛ' => 's', + 'ₜ' => 't', + '₨' => 'Rs', + '℀' => 'a/c', + '℁' => 'a/s', + 'ℂ' => 'C', + '℃' => '°C', + '℅' => 'c/o', + '℆' => 'c/u', + 'ℇ' => 'Ɛ', + '℉' => '°F', + 'ℊ' => 'g', + 'ℋ' => 'H', + 'ℌ' => 'H', + 'ℍ' => 'H', + 'ℎ' => 'h', + 'ℏ' => 'ħ', + 'ℐ' => 'I', + 'ℑ' => 'I', + 'ℒ' => 'L', + 'ℓ' => 'l', + 'ℕ' => 'N', + '№' => 'No', + 'ℙ' => 'P', + 'ℚ' => 'Q', + 'ℛ' => 'R', + 'ℜ' => 'R', + 'ℝ' => 'R', + '℠' => 'SM', + '℡' => 'TEL', + '™' => 'TM', + 'ℤ' => 'Z', + 'ℨ' => 'Z', + 'ℬ' => 'B', + 'ℭ' => 'C', + 'ℯ' => 'e', + 'ℰ' => 'E', + 'ℱ' => 'F', + 'ℳ' => 'M', + 'ℴ' => 'o', + 'ℵ' => 'א', + 'ℶ' => 'ב', + 'ℷ' => 'ג', + 'ℸ' => 'ד', + 'ℹ' => 'i', + '℻' => 'FAX', + 'ℼ' => 'π', + 'ℽ' => 'γ', + 'ℾ' => 'Γ', + 'ℿ' => 'Π', + '⅀' => '∑', + 'ⅅ' => 'D', + 'ⅆ' => 'd', + 'ⅇ' => 'e', + 'ⅈ' => 'i', + 'ⅉ' => 'j', + '⅐' => '1⁄7', + '⅑' => '1⁄9', + '⅒' => '1⁄10', + '⅓' => '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⁄', + 'Ⅰ' => 'I', + 'Ⅱ' => 'II', + 'Ⅲ' => 'III', + 'Ⅳ' => 'IV', + 'Ⅴ' => 'V', + 'Ⅵ' => 'VI', + 'Ⅶ' => 'VII', + 'Ⅷ' => 'VIII', + 'Ⅸ' => 'IX', + 'Ⅹ' => 'X', + 'Ⅺ' => 'XI', + 'Ⅻ' => 'XII', + 'Ⅼ' => 'L', + 'Ⅽ' => 'C', + 'Ⅾ' => 'D', + 'Ⅿ' => 'M', + 'ⅰ' => 'i', + 'ⅱ' => 'ii', + 'ⅲ' => 'iii', + 'ⅳ' => 'iv', + 'ⅴ' => 'v', + 'ⅵ' => 'vi', + 'ⅶ' => 'vii', + 'ⅷ' => 'viii', + 'ⅸ' => 'ix', + 'ⅹ' => 'x', + 'ⅺ' => 'xi', + 'ⅻ' => 'xii', + 'ⅼ' => 'l', + 'ⅽ' => 'c', + 'ⅾ' => 'd', + 'ⅿ' => 'm', + '↉' => '0⁄3', + '∬' => '∫∫', + '∭' => '∫∫∫', + '∯' => '∮∮', + '∰' => '∮∮∮', + '①' => '1', + '②' => '2', + '③' => '3', + '④' => '4', + '⑤' => '5', + '⑥' => '6', + '⑦' => '7', + '⑧' => '8', + '⑨' => '9', + '⑩' => '10', + '⑪' => '11', + '⑫' => '12', + '⑬' => '13', + '⑭' => '14', + '⑮' => '15', + '⑯' => '16', + '⑰' => '17', + '⑱' => '18', + '⑲' => '19', + '⑳' => '20', + '⑴' => '(1)', + '⑵' => '(2)', + '⑶' => '(3)', + '⑷' => '(4)', + '⑸' => '(5)', + '⑹' => '(6)', + '⑺' => '(7)', + '⑻' => '(8)', + '⑼' => '(9)', + '⑽' => '(10)', + '⑾' => '(11)', + '⑿' => '(12)', + '⒀' => '(13)', + '⒁' => '(14)', + '⒂' => '(15)', + '⒃' => '(16)', + '⒄' => '(17)', + '⒅' => '(18)', + '⒆' => '(19)', + '⒇' => '(20)', + '⒈' => '1.', + '⒉' => '2.', + '⒊' => '3.', + '⒋' => '4.', + '⒌' => '5.', + '⒍' => '6.', + '⒎' => '7.', + '⒏' => '8.', + '⒐' => '9.', + '⒑' => '10.', + '⒒' => '11.', + '⒓' => '12.', + '⒔' => '13.', + '⒕' => '14.', + '⒖' => '15.', + '⒗' => '16.', + '⒘' => '17.', + '⒙' => '18.', + '⒚' => '19.', + '⒛' => '20.', + '⒜' => '(a)', + '⒝' => '(b)', + '⒞' => '(c)', + '⒟' => '(d)', + '⒠' => '(e)', + '⒡' => '(f)', + '⒢' => '(g)', + '⒣' => '(h)', + '⒤' => '(i)', + '⒥' => '(j)', + '⒦' => '(k)', + '⒧' => '(l)', + '⒨' => '(m)', + '⒩' => '(n)', + '⒪' => '(o)', + '⒫' => '(p)', + '⒬' => '(q)', + '⒭' => '(r)', + '⒮' => '(s)', + '⒯' => '(t)', + '⒰' => '(u)', + '⒱' => '(v)', + '⒲' => '(w)', + '⒳' => '(x)', + '⒴' => '(y)', + '⒵' => '(z)', + 'Ⓐ' => 'A', + 'Ⓑ' => 'B', + 'Ⓒ' => 'C', + 'Ⓓ' => 'D', + 'Ⓔ' => 'E', + 'Ⓕ' => 'F', + 'Ⓖ' => 'G', + 'Ⓗ' => 'H', + 'Ⓘ' => 'I', + 'Ⓙ' => 'J', + 'Ⓚ' => 'K', + 'Ⓛ' => 'L', + 'Ⓜ' => 'M', + 'Ⓝ' => 'N', + 'Ⓞ' => 'O', + 'Ⓟ' => 'P', + 'Ⓠ' => 'Q', + 'Ⓡ' => 'R', + 'Ⓢ' => 'S', + 'Ⓣ' => 'T', + 'Ⓤ' => 'U', + 'Ⓥ' => 'V', + 'Ⓦ' => 'W', + 'Ⓧ' => 'X', + 'Ⓨ' => 'Y', + 'Ⓩ' => 'Z', + 'ⓐ' => 'a', + 'ⓑ' => 'b', + 'ⓒ' => 'c', + 'ⓓ' => 'd', + 'ⓔ' => 'e', + 'ⓕ' => 'f', + 'ⓖ' => 'g', + 'ⓗ' => 'h', + 'ⓘ' => 'i', + 'ⓙ' => 'j', + 'ⓚ' => 'k', + 'ⓛ' => 'l', + 'ⓜ' => 'm', + 'ⓝ' => 'n', + 'ⓞ' => 'o', + 'ⓟ' => 'p', + 'ⓠ' => 'q', + 'ⓡ' => 'r', + 'ⓢ' => 's', + 'ⓣ' => 't', + 'ⓤ' => 'u', + 'ⓥ' => 'v', + 'ⓦ' => 'w', + 'ⓧ' => 'x', + 'ⓨ' => 'y', + 'ⓩ' => 'z', + '⓪' => '0', + '⨌' => '∫∫∫∫', + '⩴' => '::=', + '⩵' => '==', + '⩶' => '===', + 'ⱼ' => 'j', + 'ⱽ' => 'V', + 'ⵯ' => 'ⵡ', + '⺟' => '母', + '⻳' => '龟', + '⼀' => '一', + '⼁' => '丨', + '⼂' => '丶', + '⼃' => '丿', + '⼄' => '乙', + '⼅' => '亅', + '⼆' => '二', + '⼇' => '亠', + '⼈' => '人', + '⼉' => '儿', + '⼊' => '入', + '⼋' => '八', + '⼌' => '冂', + '⼍' => '冖', + '⼎' => '冫', + '⼏' => '几', + '⼐' => '凵', + '⼑' => '刀', + '⼒' => '力', + '⼓' => '勹', + '⼔' => '匕', + '⼕' => '匚', + '⼖' => '匸', + '⼗' => '十', + '⼘' => '卜', + '⼙' => '卩', + '⼚' => '厂', + '⼛' => '厶', + '⼜' => '又', + '⼝' => '口', + '⼞' => '囗', + '⼟' => '土', + '⼠' => '士', + '⼡' => '夂', + '⼢' => '夊', + '⼣' => '夕', + '⼤' => '大', + '⼥' => '女', + '⼦' => '子', + '⼧' => '宀', + '⼨' => '寸', + '⼩' => '小', + '⼪' => '尢', + '⼫' => '尸', + '⼬' => '屮', + '⼭' => '山', + '⼮' => '巛', + '⼯' => '工', + '⼰' => '己', + '⼱' => '巾', + '⼲' => '干', + '⼳' => '幺', + '⼴' => '广', + '⼵' => '廴', + '⼶' => '廾', + '⼷' => '弋', + '⼸' => '弓', + '⼹' => '彐', + '⼺' => '彡', + '⼻' => '彳', + '⼼' => '心', + '⼽' => '戈', + '⼾' => '戶', + '⼿' => '手', + '⽀' => '支', + '⽁' => '攴', + '⽂' => '文', + '⽃' => '斗', + '⽄' => '斤', + '⽅' => '方', + '⽆' => '无', + '⽇' => '日', + '⽈' => '曰', + '⽉' => '月', + '⽊' => '木', + '⽋' => '欠', + '⽌' => '止', + '⽍' => '歹', + '⽎' => '殳', + '⽏' => '毋', + '⽐' => '比', + '⽑' => '毛', + '⽒' => '氏', + '⽓' => '气', + '⽔' => '水', + '⽕' => '火', + '⽖' => '爪', + '⽗' => '父', + '⽘' => '爻', + '⽙' => '爿', + '⽚' => '片', + '⽛' => '牙', + '⽜' => '牛', + '⽝' => '犬', + '⽞' => '玄', + '⽟' => '玉', + '⽠' => '瓜', + '⽡' => '瓦', + '⽢' => '甘', + '⽣' => '生', + '⽤' => '用', + '⽥' => '田', + '⽦' => '疋', + '⽧' => '疒', + '⽨' => '癶', + '⽩' => '白', + '⽪' => '皮', + '⽫' => '皿', + '⽬' => '目', + '⽭' => '矛', + '⽮' => '矢', + '⽯' => '石', + '⽰' => '示', + '⽱' => '禸', + '⽲' => '禾', + '⽳' => '穴', + '⽴' => '立', + '⽵' => '竹', + '⽶' => '米', + '⽷' => '糸', + '⽸' => '缶', + '⽹' => '网', + '⽺' => '羊', + '⽻' => '羽', + '⽼' => '老', + '⽽' => '而', + '⽾' => '耒', + '⽿' => '耳', + '⾀' => '聿', + '⾁' => '肉', + '⾂' => '臣', + '⾃' => '自', + '⾄' => '至', + '⾅' => '臼', + '⾆' => '舌', + '⾇' => '舛', + '⾈' => '舟', + '⾉' => '艮', + '⾊' => '色', + '⾋' => '艸', + '⾌' => '虍', + '⾍' => '虫', + '⾎' => '血', + '⾏' => '行', + '⾐' => '衣', + '⾑' => '襾', + '⾒' => '見', + '⾓' => '角', + '⾔' => '言', + '⾕' => '谷', + '⾖' => '豆', + '⾗' => '豕', + '⾘' => '豸', + '⾙' => '貝', + '⾚' => '赤', + '⾛' => '走', + '⾜' => '足', + '⾝' => '身', + '⾞' => '車', + '⾟' => '辛', + '⾠' => '辰', + '⾡' => '辵', + '⾢' => '邑', + '⾣' => '酉', + '⾤' => '釆', + '⾥' => '里', + '⾦' => '金', + '⾧' => '長', + '⾨' => '門', + '⾩' => '阜', + '⾪' => '隶', + '⾫' => '隹', + '⾬' => '雨', + '⾭' => '靑', + '⾮' => '非', + '⾯' => '面', + '⾰' => '革', + '⾱' => '韋', + '⾲' => '韭', + '⾳' => '音', + '⾴' => '頁', + '⾵' => '風', + '⾶' => '飛', + '⾷' => '食', + '⾸' => '首', + '⾹' => '香', + '⾺' => '馬', + '⾻' => '骨', + '⾼' => '高', + '⾽' => '髟', + '⾾' => '鬥', + '⾿' => '鬯', + '⿀' => '鬲', + '⿁' => '鬼', + '⿂' => '魚', + '⿃' => '鳥', + '⿄' => '鹵', + '⿅' => '鹿', + '⿆' => '麥', + '⿇' => '麻', + '⿈' => '黃', + '⿉' => '黍', + '⿊' => '黑', + '⿋' => '黹', + '⿌' => '黽', + '⿍' => '鼎', + '⿎' => '鼓', + '⿏' => '鼠', + '⿐' => '鼻', + '⿑' => '齊', + '⿒' => '齒', + '⿓' => '龍', + '⿔' => '龜', + '⿕' => '龠', + ' ' => ' ', + '〶' => '〒', + '〸' => '十', + '〹' => '卄', + '〺' => '卅', + '゛' => ' ゙', + '゜' => ' ゚', + 'ゟ' => 'より', + 'ヿ' => 'コト', + 'ㄱ' => 'ᄀ', + 'ㄲ' => 'ᄁ', + 'ㄳ' => 'ᆪ', + 'ㄴ' => 'ᄂ', + 'ㄵ' => 'ᆬ', + 'ㄶ' => 'ᆭ', + 'ㄷ' => 'ᄃ', + 'ㄸ' => 'ᄄ', + 'ㄹ' => 'ᄅ', + 'ㄺ' => 'ᆰ', + 'ㄻ' => 'ᆱ', + 'ㄼ' => 'ᆲ', + 'ㄽ' => 'ᆳ', + 'ㄾ' => 'ᆴ', + 'ㄿ' => 'ᆵ', + 'ㅀ' => 'ᄚ', + 'ㅁ' => 'ᄆ', + 'ㅂ' => 'ᄇ', + 'ㅃ' => 'ᄈ', + 'ㅄ' => 'ᄡ', + 'ㅅ' => 'ᄉ', + 'ㅆ' => 'ᄊ', + 'ㅇ' => 'ᄋ', + 'ㅈ' => 'ᄌ', + 'ㅉ' => 'ᄍ', + 'ㅊ' => 'ᄎ', + 'ㅋ' => 'ᄏ', + 'ㅌ' => 'ᄐ', + 'ㅍ' => 'ᄑ', + 'ㅎ' => 'ᄒ', + 'ㅏ' => 'ᅡ', + 'ㅐ' => 'ᅢ', + 'ㅑ' => 'ᅣ', + 'ㅒ' => 'ᅤ', + 'ㅓ' => 'ᅥ', + 'ㅔ' => 'ᅦ', + 'ㅕ' => 'ᅧ', + 'ㅖ' => 'ᅨ', + 'ㅗ' => 'ᅩ', + 'ㅘ' => 'ᅪ', + 'ㅙ' => 'ᅫ', + 'ㅚ' => 'ᅬ', + 'ㅛ' => 'ᅭ', + 'ㅜ' => 'ᅮ', + 'ㅝ' => 'ᅯ', + 'ㅞ' => 'ᅰ', + 'ㅟ' => 'ᅱ', + 'ㅠ' => 'ᅲ', + 'ㅡ' => 'ᅳ', + 'ㅢ' => 'ᅴ', + 'ㅣ' => 'ᅵ', + 'ㅤ' => 'ᅠ', + 'ㅥ' => 'ᄔ', + 'ㅦ' => 'ᄕ', + 'ㅧ' => 'ᇇ', + 'ㅨ' => 'ᇈ', + 'ㅩ' => 'ᇌ', + 'ㅪ' => 'ᇎ', + 'ㅫ' => 'ᇓ', + 'ㅬ' => 'ᇗ', + 'ㅭ' => 'ᇙ', + 'ㅮ' => 'ᄜ', + 'ㅯ' => 'ᇝ', + 'ㅰ' => 'ᇟ', + 'ㅱ' => 'ᄝ', + 'ㅲ' => 'ᄞ', + 'ㅳ' => 'ᄠ', + 'ㅴ' => 'ᄢ', + 'ㅵ' => 'ᄣ', + 'ㅶ' => 'ᄧ', + 'ㅷ' => 'ᄩ', + 'ㅸ' => 'ᄫ', + 'ㅹ' => 'ᄬ', + 'ㅺ' => 'ᄭ', + 'ㅻ' => 'ᄮ', + 'ㅼ' => 'ᄯ', + 'ㅽ' => 'ᄲ', + 'ㅾ' => 'ᄶ', + 'ㅿ' => 'ᅀ', + 'ㆀ' => 'ᅇ', + 'ㆁ' => 'ᅌ', + 'ㆂ' => 'ᇱ', + 'ㆃ' => 'ᇲ', + 'ㆄ' => 'ᅗ', + 'ㆅ' => 'ᅘ', + 'ㆆ' => 'ᅙ', + 'ㆇ' => 'ᆄ', + 'ㆈ' => 'ᆅ', + 'ㆉ' => 'ᆈ', + 'ㆊ' => 'ᆑ', + 'ㆋ' => 'ᆒ', + 'ㆌ' => 'ᆔ', + 'ㆍ' => 'ᆞ', + 'ㆎ' => 'ᆡ', + '㆒' => '一', + '㆓' => '二', + '㆔' => '三', + '㆕' => '四', + '㆖' => '上', + '㆗' => '中', + '㆘' => '下', + '㆙' => '甲', + '㆚' => '乙', + '㆛' => '丙', + '㆜' => '丁', + '㆝' => '天', + '㆞' => '地', + '㆟' => '人', + '㈀' => '(ᄀ)', + '㈁' => '(ᄂ)', + '㈂' => '(ᄃ)', + '㈃' => '(ᄅ)', + '㈄' => '(ᄆ)', + '㈅' => '(ᄇ)', + '㈆' => '(ᄉ)', + '㈇' => '(ᄋ)', + '㈈' => '(ᄌ)', + '㈉' => '(ᄎ)', + '㈊' => '(ᄏ)', + '㈋' => '(ᄐ)', + '㈌' => '(ᄑ)', + '㈍' => '(ᄒ)', + '㈎' => '(가)', + '㈏' => '(나)', + '㈐' => '(다)', + '㈑' => '(라)', + '㈒' => '(마)', + '㈓' => '(바)', + '㈔' => '(사)', + '㈕' => '(아)', + '㈖' => '(자)', + '㈗' => '(차)', + '㈘' => '(카)', + '㈙' => '(타)', + '㈚' => '(파)', + '㈛' => '(하)', + '㈜' => '(주)', + '㈝' => '(오전)', + '㈞' => '(오후)', + '㈠' => '(一)', + '㈡' => '(二)', + '㈢' => '(三)', + '㈣' => '(四)', + '㈤' => '(五)', + '㈥' => '(六)', + '㈦' => '(七)', + '㈧' => '(八)', + '㈨' => '(九)', + '㈩' => '(十)', + '㈪' => '(月)', + '㈫' => '(火)', + '㈬' => '(水)', + '㈭' => '(木)', + '㈮' => '(金)', + '㈯' => '(土)', + '㈰' => '(日)', + '㈱' => '(株)', + '㈲' => '(有)', + '㈳' => '(社)', + '㈴' => '(名)', + '㈵' => '(特)', + '㈶' => '(財)', + '㈷' => '(祝)', + '㈸' => '(労)', + '㈹' => '(代)', + '㈺' => '(呼)', + '㈻' => '(学)', + '㈼' => '(監)', + '㈽' => '(企)', + '㈾' => '(資)', + '㈿' => '(協)', + '㉀' => '(祭)', + '㉁' => '(休)', + '㉂' => '(自)', + '㉃' => '(至)', + '㉄' => '問', + '㉅' => '幼', + '㉆' => '文', + '㉇' => '箏', + '㉐' => 'PTE', + '㉑' => '21', + '㉒' => '22', + '㉓' => '23', + '㉔' => '24', + '㉕' => '25', + '㉖' => '26', + '㉗' => '27', + '㉘' => '28', + '㉙' => '29', + '㉚' => '30', + '㉛' => '31', + '㉜' => '32', + '㉝' => '33', + '㉞' => '34', + '㉟' => '35', + '㉠' => 'ᄀ', + '㉡' => 'ᄂ', + '㉢' => 'ᄃ', + '㉣' => 'ᄅ', + '㉤' => 'ᄆ', + '㉥' => 'ᄇ', + '㉦' => 'ᄉ', + '㉧' => 'ᄋ', + '㉨' => 'ᄌ', + '㉩' => 'ᄎ', + '㉪' => 'ᄏ', + '㉫' => 'ᄐ', + '㉬' => 'ᄑ', + '㉭' => 'ᄒ', + '㉮' => '가', + '㉯' => '나', + '㉰' => '다', + '㉱' => '라', + '㉲' => '마', + '㉳' => '바', + '㉴' => '사', + '㉵' => '아', + '㉶' => '자', + '㉷' => '차', + '㉸' => '카', + '㉹' => '타', + '㉺' => '파', + '㉻' => '하', + '㉼' => '참고', + '㉽' => '주의', + '㉾' => '우', + '㊀' => '一', + '㊁' => '二', + '㊂' => '三', + '㊃' => '四', + '㊄' => '五', + '㊅' => '六', + '㊆' => '七', + '㊇' => '八', + '㊈' => '九', + '㊉' => '十', + '㊊' => '月', + '㊋' => '火', + '㊌' => '水', + '㊍' => '木', + '㊎' => '金', + '㊏' => '土', + '㊐' => '日', + '㊑' => '株', + '㊒' => '有', + '㊓' => '社', + '㊔' => '名', + '㊕' => '特', + '㊖' => '財', + '㊗' => '祝', + '㊘' => '労', + '㊙' => '秘', + '㊚' => '男', + '㊛' => '女', + '㊜' => '適', + '㊝' => '優', + '㊞' => '印', + '㊟' => '注', + '㊠' => '項', + '㊡' => '休', + '㊢' => '写', + '㊣' => '正', + '㊤' => '上', + '㊥' => '中', + '㊦' => '下', + '㊧' => '左', + '㊨' => '右', + '㊩' => '医', + '㊪' => '宗', + '㊫' => '学', + '㊬' => '監', + '㊭' => '企', + '㊮' => '資', + '㊯' => '協', + '㊰' => '夜', + '㊱' => '36', + '㊲' => '37', + '㊳' => '38', + '㊴' => '39', + '㊵' => '40', + '㊶' => '41', + '㊷' => '42', + '㊸' => '43', + '㊹' => '44', + '㊺' => '45', + '㊻' => '46', + '㊼' => '47', + '㊽' => '48', + '㊾' => '49', + '㊿' => '50', + '㋀' => '1月', + '㋁' => '2月', + '㋂' => '3月', + '㋃' => '4月', + '㋄' => '5月', + '㋅' => '6月', + '㋆' => '7月', + '㋇' => '8月', + '㋈' => '9月', + '㋉' => '10月', + '㋊' => '11月', + '㋋' => '12月', + '㋌' => 'Hg', + '㋍' => 'erg', + '㋎' => 'eV', + '㋏' => 'LTD', + '㋐' => 'ア', + '㋑' => 'イ', + '㋒' => 'ウ', + '㋓' => 'エ', + '㋔' => 'オ', + '㋕' => 'カ', + '㋖' => 'キ', + '㋗' => 'ク', + '㋘' => 'ケ', + '㋙' => 'コ', + '㋚' => 'サ', + '㋛' => 'シ', + '㋜' => 'ス', + '㋝' => 'セ', + '㋞' => 'ソ', + '㋟' => 'タ', + '㋠' => 'チ', + '㋡' => 'ツ', + '㋢' => 'テ', + '㋣' => 'ト', + '㋤' => 'ナ', + '㋥' => 'ニ', + '㋦' => 'ヌ', + '㋧' => 'ネ', + '㋨' => 'ノ', + '㋩' => 'ハ', + '㋪' => 'ヒ', + '㋫' => 'フ', + '㋬' => 'ヘ', + '㋭' => 'ホ', + '㋮' => 'マ', + '㋯' => 'ミ', + '㋰' => 'ム', + '㋱' => 'メ', + '㋲' => 'モ', + '㋳' => 'ヤ', + '㋴' => 'ユ', + '㋵' => 'ヨ', + '㋶' => 'ラ', + '㋷' => 'リ', + '㋸' => 'ル', + '㋹' => 'レ', + '㋺' => 'ロ', + '㋻' => 'ワ', + '㋼' => 'ヰ', + '㋽' => 'ヱ', + '㋾' => 'ヲ', + '㋿' => '令和', + '㌀' => 'アパート', + '㌁' => 'アルファ', + '㌂' => 'アンペア', + '㌃' => 'アール', + '㌄' => 'イニング', + '㌅' => 'インチ', + '㌆' => 'ウォン', + '㌇' => 'エスクード', + '㌈' => 'エーカー', + '㌉' => 'オンス', + '㌊' => 'オーム', + '㌋' => 'カイリ', + '㌌' => 'カラット', + '㌍' => 'カロリー', + '㌎' => 'ガロン', + '㌏' => 'ガンマ', + '㌐' => 'ギガ', + '㌑' => 'ギニー', + '㌒' => 'キュリー', + '㌓' => 'ギルダー', + '㌔' => 'キロ', + '㌕' => 'キログラム', + '㌖' => 'キロメートル', + '㌗' => 'キロワット', + '㌘' => 'グラム', + '㌙' => 'グラムトン', + '㌚' => 'クルゼイロ', + '㌛' => 'クローネ', + '㌜' => 'ケース', + '㌝' => 'コルナ', + '㌞' => 'コーポ', + '㌟' => 'サイクル', + '㌠' => 'サンチーム', + '㌡' => 'シリング', + '㌢' => 'センチ', + '㌣' => 'セント', + '㌤' => 'ダース', + '㌥' => 'デシ', + '㌦' => 'ドル', + '㌧' => 'トン', + '㌨' => 'ナノ', + '㌩' => 'ノット', + '㌪' => 'ハイツ', + '㌫' => 'パーセント', + '㌬' => 'パーツ', + '㌭' => 'バーレル', + '㌮' => 'ピアストル', + '㌯' => 'ピクル', + '㌰' => 'ピコ', + '㌱' => 'ビル', + '㌲' => 'ファラッド', + '㌳' => 'フィート', + '㌴' => 'ブッシェル', + '㌵' => 'フラン', + '㌶' => 'ヘクタール', + '㌷' => 'ペソ', + '㌸' => 'ペニヒ', + '㌹' => 'ヘルツ', + '㌺' => 'ペンス', + '㌻' => 'ページ', + '㌼' => 'ベータ', + '㌽' => 'ポイント', + '㌾' => 'ボルト', + '㌿' => 'ホン', + '㍀' => 'ポンド', + '㍁' => 'ホール', + '㍂' => 'ホーン', + '㍃' => 'マイクロ', + '㍄' => 'マイル', + '㍅' => 'マッハ', + '㍆' => 'マルク', + '㍇' => 'マンション', + '㍈' => 'ミクロン', + '㍉' => 'ミリ', + '㍊' => 'ミリバール', + '㍋' => 'メガ', + '㍌' => 'メガトン', + '㍍' => 'メートル', + '㍎' => 'ヤード', + '㍏' => 'ヤール', + '㍐' => 'ユアン', + '㍑' => 'リットル', + '㍒' => 'リラ', + '㍓' => 'ルピー', + '㍔' => 'ルーブル', + '㍕' => 'レム', + '㍖' => 'レントゲン', + '㍗' => 'ワット', + '㍘' => '0点', + '㍙' => '1点', + '㍚' => '2点', + '㍛' => '3点', + '㍜' => '4点', + '㍝' => '5点', + '㍞' => '6点', + '㍟' => '7点', + '㍠' => '8点', + '㍡' => '9点', + '㍢' => '10点', + '㍣' => '11点', + '㍤' => '12点', + '㍥' => '13点', + '㍦' => '14点', + '㍧' => '15点', + '㍨' => '16点', + '㍩' => '17点', + '㍪' => '18点', + '㍫' => '19点', + '㍬' => '20点', + '㍭' => '21点', + '㍮' => '22点', + '㍯' => '23点', + '㍰' => '24点', + '㍱' => 'hPa', + '㍲' => 'da', + '㍳' => 'AU', + '㍴' => 'bar', + '㍵' => 'oV', + '㍶' => 'pc', + '㍷' => 'dm', + '㍸' => 'dm2', + '㍹' => 'dm3', + '㍺' => 'IU', + '㍻' => '平成', + '㍼' => '昭和', + '㍽' => '大正', + '㍾' => '明治', + '㍿' => '株式会社', + '㎀' => 'pA', + '㎁' => 'nA', + '㎂' => 'μA', + '㎃' => 'mA', + '㎄' => 'kA', + '㎅' => 'KB', + '㎆' => 'MB', + '㎇' => 'GB', + '㎈' => 'cal', + '㎉' => 'kcal', + '㎊' => 'pF', + '㎋' => 'nF', + '㎌' => 'μF', + '㎍' => 'μg', + '㎎' => 'mg', + '㎏' => 'kg', + '㎐' => 'Hz', + '㎑' => 'kHz', + '㎒' => 'MHz', + '㎓' => 'GHz', + '㎔' => 'THz', + '㎕' => 'μl', + '㎖' => 'ml', + '㎗' => 'dl', + '㎘' => 'kl', + '㎙' => 'fm', + '㎚' => 'nm', + '㎛' => 'μm', + '㎜' => 'mm', + '㎝' => 'cm', + '㎞' => 'km', + '㎟' => 'mm2', + '㎠' => 'cm2', + '㎡' => 'm2', + '㎢' => 'km2', + '㎣' => 'mm3', + '㎤' => 'cm3', + '㎥' => 'm3', + '㎦' => 'km3', + '㎧' => 'm∕s', + '㎨' => 'm∕s2', + '㎩' => 'Pa', + '㎪' => 'kPa', + '㎫' => 'MPa', + '㎬' => 'GPa', + '㎭' => 'rad', + '㎮' => 'rad∕s', + '㎯' => 'rad∕s2', + '㎰' => 'ps', + '㎱' => 'ns', + '㎲' => 'μs', + '㎳' => 'ms', + '㎴' => 'pV', + '㎵' => 'nV', + '㎶' => 'μV', + '㎷' => 'mV', + '㎸' => 'kV', + '㎹' => 'MV', + '㎺' => 'pW', + '㎻' => 'nW', + '㎼' => 'μW', + '㎽' => 'mW', + '㎾' => 'kW', + '㎿' => 'MW', + '㏀' => 'kΩ', + '㏁' => 'MΩ', + '㏂' => 'a.m.', + '㏃' => 'Bq', + '㏄' => 'cc', + '㏅' => 'cd', + '㏆' => 'C∕kg', + '㏇' => 'Co.', + '㏈' => 'dB', + '㏉' => 'Gy', + '㏊' => 'ha', + '㏋' => 'HP', + '㏌' => 'in', + '㏍' => 'KK', + '㏎' => 'KM', + '㏏' => 'kt', + '㏐' => 'lm', + '㏑' => 'ln', + '㏒' => 'log', + '㏓' => 'lx', + '㏔' => 'mb', + '㏕' => 'mil', + '㏖' => 'mol', + '㏗' => 'PH', + '㏘' => 'p.m.', + '㏙' => 'PPM', + '㏚' => 'PR', + '㏛' => 'sr', + '㏜' => 'Sv', + '㏝' => 'Wb', + '㏞' => 'V∕m', + '㏟' => 'A∕m', + '㏠' => '1日', + '㏡' => '2日', + '㏢' => '3日', + '㏣' => '4日', + '㏤' => '5日', + '㏥' => '6日', + '㏦' => '7日', + '㏧' => '8日', + '㏨' => '9日', + '㏩' => '10日', + '㏪' => '11日', + '㏫' => '12日', + '㏬' => '13日', + '㏭' => '14日', + '㏮' => '15日', + '㏯' => '16日', + '㏰' => '17日', + '㏱' => '18日', + '㏲' => '19日', + '㏳' => '20日', + '㏴' => '21日', + '㏵' => '22日', + '㏶' => '23日', + '㏷' => '24日', + '㏸' => '25日', + '㏹' => '26日', + '㏺' => '27日', + '㏻' => '28日', + '㏼' => '29日', + '㏽' => '30日', + '㏾' => '31日', + '㏿' => 'gal', + 'ꚜ' => 'ъ', + 'ꚝ' => 'ь', + 'ꝰ' => 'ꝯ', + 'ꟸ' => 'Ħ', + 'ꟹ' => 'œ', + 'ꭜ' => 'ꜧ', + 'ꭝ' => 'ꬷ', + 'ꭞ' => 'ɫ', + 'ꭟ' => 'ꭒ', + 'ꭩ' => 'ʍ', + 'ff' => 'ff', + 'fi' => 'fi', + 'fl' => 'fl', + 'ffi' => 'ffi', + 'ffl' => 'ffl', + 'ſt' => 'st', + 'st' => 'st', + 'ﬓ' => 'մն', + 'ﬔ' => 'մե', + 'ﬕ' => 'մի', + 'ﬖ' => 'վն', + 'ﬗ' => 'մխ', + 'ﬠ' => 'ע', + 'ﬡ' => 'א', + 'ﬢ' => 'ד', + 'ﬣ' => 'ה', + 'ﬤ' => 'כ', + 'ﬥ' => 'ל', + 'ﬦ' => 'ם', + 'ﬧ' => 'ר', + 'ﬨ' => 'ת', + '﬩' => '+', + 'ﭏ' => 'אל', + 'ﭐ' => 'ٱ', + 'ﭑ' => 'ٱ', + 'ﭒ' => 'ٻ', + 'ﭓ' => 'ٻ', + 'ﭔ' => 'ٻ', + 'ﭕ' => 'ٻ', + 'ﭖ' => 'پ', + 'ﭗ' => 'پ', + 'ﭘ' => 'پ', + 'ﭙ' => 'پ', + 'ﭚ' => 'ڀ', + 'ﭛ' => 'ڀ', + 'ﭜ' => 'ڀ', + 'ﭝ' => 'ڀ', + 'ﭞ' => 'ٺ', + 'ﭟ' => 'ٺ', + 'ﭠ' => 'ٺ', + 'ﭡ' => 'ٺ', + 'ﭢ' => 'ٿ', + 'ﭣ' => 'ٿ', + 'ﭤ' => 'ٿ', + 'ﭥ' => 'ٿ', + 'ﭦ' => 'ٹ', + 'ﭧ' => 'ٹ', + 'ﭨ' => 'ٹ', + 'ﭩ' => 'ٹ', + 'ﭪ' => 'ڤ', + 'ﭫ' => 'ڤ', + 'ﭬ' => 'ڤ', + 'ﭭ' => 'ڤ', + 'ﭮ' => 'ڦ', + 'ﭯ' => 'ڦ', + 'ﭰ' => 'ڦ', + 'ﭱ' => 'ڦ', + 'ﭲ' => 'ڄ', + 'ﭳ' => 'ڄ', + 'ﭴ' => 'ڄ', + 'ﭵ' => 'ڄ', + 'ﭶ' => 'ڃ', + 'ﭷ' => 'ڃ', + 'ﭸ' => 'ڃ', + 'ﭹ' => 'ڃ', + 'ﭺ' => 'چ', + 'ﭻ' => 'چ', + 'ﭼ' => 'چ', + 'ﭽ' => 'چ', + 'ﭾ' => 'ڇ', + 'ﭿ' => 'ڇ', + 'ﮀ' => 'ڇ', + 'ﮁ' => 'ڇ', + 'ﮂ' => 'ڍ', + 'ﮃ' => 'ڍ', + 'ﮄ' => 'ڌ', + 'ﮅ' => 'ڌ', + 'ﮆ' => 'ڎ', + 'ﮇ' => 'ڎ', + 'ﮈ' => 'ڈ', + 'ﮉ' => 'ڈ', + 'ﮊ' => 'ژ', + 'ﮋ' => 'ژ', + 'ﮌ' => 'ڑ', + 'ﮍ' => 'ڑ', + 'ﮎ' => 'ک', + 'ﮏ' => 'ک', + 'ﮐ' => 'ک', + 'ﮑ' => 'ک', + 'ﮒ' => 'گ', + 'ﮓ' => 'گ', + 'ﮔ' => 'گ', + 'ﮕ' => 'گ', + 'ﮖ' => 'ڳ', + 'ﮗ' => 'ڳ', + 'ﮘ' => 'ڳ', + 'ﮙ' => 'ڳ', + 'ﮚ' => 'ڱ', + 'ﮛ' => 'ڱ', + 'ﮜ' => 'ڱ', + 'ﮝ' => 'ڱ', + 'ﮞ' => 'ں', + 'ﮟ' => 'ں', + 'ﮠ' => 'ڻ', + 'ﮡ' => 'ڻ', + 'ﮢ' => 'ڻ', + 'ﮣ' => 'ڻ', + 'ﮤ' => 'ۀ', + 'ﮥ' => 'ۀ', + 'ﮦ' => 'ہ', + 'ﮧ' => 'ہ', + 'ﮨ' => 'ہ', + 'ﮩ' => 'ہ', + 'ﮪ' => 'ھ', + 'ﮫ' => 'ھ', + 'ﮬ' => 'ھ', + 'ﮭ' => 'ھ', + 'ﮮ' => 'ے', + 'ﮯ' => 'ے', + 'ﮰ' => 'ۓ', + 'ﮱ' => 'ۓ', + 'ﯓ' => 'ڭ', + 'ﯔ' => 'ڭ', + 'ﯕ' => 'ڭ', + 'ﯖ' => 'ڭ', + 'ﯗ' => 'ۇ', + 'ﯘ' => 'ۇ', + 'ﯙ' => 'ۆ', + 'ﯚ' => 'ۆ', + 'ﯛ' => 'ۈ', + 'ﯜ' => 'ۈ', + 'ﯝ' => 'ۇٴ', + 'ﯞ' => 'ۋ', + 'ﯟ' => 'ۋ', + 'ﯠ' => 'ۅ', + 'ﯡ' => 'ۅ', + 'ﯢ' => 'ۉ', + 'ﯣ' => 'ۉ', + 'ﯤ' => 'ې', + 'ﯥ' => 'ې', + 'ﯦ' => 'ې', + 'ﯧ' => 'ې', + 'ﯨ' => 'ى', + 'ﯩ' => 'ى', + 'ﯪ' => 'ئا', + 'ﯫ' => 'ئا', + 'ﯬ' => 'ئە', + 'ﯭ' => 'ئە', + 'ﯮ' => 'ئو', + 'ﯯ' => 'ئو', + 'ﯰ' => 'ئۇ', + 'ﯱ' => 'ئۇ', + 'ﯲ' => 'ئۆ', + 'ﯳ' => 'ئۆ', + 'ﯴ' => 'ئۈ', + 'ﯵ' => 'ئۈ', + 'ﯶ' => 'ئې', + 'ﯷ' => 'ئې', + 'ﯸ' => 'ئې', + 'ﯹ' => 'ئى', + 'ﯺ' => 'ئى', + 'ﯻ' => 'ئى', + 'ﯼ' => 'ی', + 'ﯽ' => 'ی', + 'ﯾ' => 'ی', + 'ﯿ' => 'ی', + 'ﰀ' => 'ئج', + 'ﰁ' => 'ئح', + 'ﰂ' => 'ئم', + 'ﰃ' => 'ئى', + 'ﰄ' => 'ئي', + 'ﰅ' => 'بج', + 'ﰆ' => 'بح', + 'ﰇ' => 'بخ', + 'ﰈ' => 'بم', + 'ﰉ' => 'بى', + 'ﰊ' => 'بي', + 'ﰋ' => 'تج', + 'ﰌ' => 'تح', + 'ﰍ' => 'تخ', + 'ﰎ' => 'تم', + 'ﰏ' => 'تى', + 'ﰐ' => 'تي', + 'ﰑ' => 'ثج', + 'ﰒ' => 'ثم', + 'ﰓ' => 'ثى', + 'ﰔ' => 'ثي', + 'ﰕ' => 'جح', + 'ﰖ' => 'جم', + 'ﰗ' => 'حج', + 'ﰘ' => 'حم', + 'ﰙ' => 'خج', + 'ﰚ' => 'خح', + 'ﰛ' => 'خم', + 'ﰜ' => 'سج', + 'ﰝ' => 'سح', + 'ﰞ' => 'سخ', + 'ﰟ' => 'سم', + 'ﰠ' => 'صح', + 'ﰡ' => 'صم', + 'ﰢ' => 'ضج', + 'ﰣ' => 'ضح', + 'ﰤ' => 'ضخ', + 'ﰥ' => 'ضم', + 'ﰦ' => 'طح', + 'ﰧ' => 'طم', + 'ﰨ' => 'ظم', + 'ﰩ' => 'عج', + 'ﰪ' => 'عم', + 'ﰫ' => 'غج', + 'ﰬ' => 'غم', + 'ﰭ' => 'فج', + 'ﰮ' => 'فح', + 'ﰯ' => 'فخ', + 'ﰰ' => 'فم', + 'ﰱ' => 'فى', + 'ﰲ' => 'في', + 'ﰳ' => 'قح', + 'ﰴ' => 'قم', + 'ﰵ' => 'قى', + 'ﰶ' => 'قي', + 'ﰷ' => 'كا', + 'ﰸ' => 'كج', + 'ﰹ' => 'كح', + 'ﰺ' => 'كخ', + 'ﰻ' => 'كل', + 'ﰼ' => 'كم', + 'ﰽ' => 'كى', + 'ﰾ' => 'كي', + 'ﰿ' => 'لج', + 'ﱀ' => 'لح', + 'ﱁ' => 'لخ', + 'ﱂ' => 'لم', + 'ﱃ' => 'لى', + 'ﱄ' => 'لي', + 'ﱅ' => 'مج', + 'ﱆ' => 'مح', + 'ﱇ' => 'مخ', + 'ﱈ' => 'مم', + 'ﱉ' => 'مى', + 'ﱊ' => 'مي', + 'ﱋ' => 'نج', + 'ﱌ' => 'نح', + 'ﱍ' => 'نخ', + 'ﱎ' => 'نم', + 'ﱏ' => 'نى', + 'ﱐ' => 'ني', + 'ﱑ' => 'هج', + 'ﱒ' => 'هم', + 'ﱓ' => 'هى', + 'ﱔ' => 'هي', + 'ﱕ' => 'يج', + 'ﱖ' => 'يح', + 'ﱗ' => 'يخ', + 'ﱘ' => 'يم', + 'ﱙ' => 'يى', + 'ﱚ' => 'يي', + 'ﱛ' => 'ذٰ', + 'ﱜ' => 'رٰ', + 'ﱝ' => 'ىٰ', + 'ﱞ' => ' ٌّ', + 'ﱟ' => ' ٍّ', + 'ﱠ' => ' َّ', + 'ﱡ' => ' ُّ', + 'ﱢ' => ' ِّ', + 'ﱣ' => ' ّٰ', + 'ﱤ' => 'ئر', + 'ﱥ' => 'ئز', + 'ﱦ' => 'ئم', + 'ﱧ' => 'ئن', + 'ﱨ' => 'ئى', + 'ﱩ' => 'ئي', + 'ﱪ' => 'بر', + 'ﱫ' => 'بز', + 'ﱬ' => 'بم', + 'ﱭ' => 'بن', + 'ﱮ' => 'بى', + 'ﱯ' => 'بي', + 'ﱰ' => 'تر', + 'ﱱ' => 'تز', + 'ﱲ' => 'تم', + 'ﱳ' => 'تن', + 'ﱴ' => 'تى', + 'ﱵ' => 'تي', + 'ﱶ' => 'ثر', + 'ﱷ' => 'ثز', + 'ﱸ' => 'ثم', + 'ﱹ' => 'ثن', + 'ﱺ' => 'ثى', + 'ﱻ' => 'ثي', + 'ﱼ' => 'فى', + 'ﱽ' => 'في', + 'ﱾ' => 'قى', + 'ﱿ' => 'قي', + 'ﲀ' => 'كا', + 'ﲁ' => 'كل', + 'ﲂ' => 'كم', + 'ﲃ' => 'كى', + 'ﲄ' => 'كي', + 'ﲅ' => 'لم', + 'ﲆ' => 'لى', + 'ﲇ' => 'لي', + 'ﲈ' => 'ما', + 'ﲉ' => 'مم', + 'ﲊ' => 'نر', + 'ﲋ' => 'نز', + 'ﲌ' => 'نم', + 'ﲍ' => 'نن', + 'ﲎ' => 'نى', + 'ﲏ' => 'ني', + 'ﲐ' => 'ىٰ', + 'ﲑ' => 'ير', + 'ﲒ' => 'يز', + 'ﲓ' => 'يم', + 'ﲔ' => 'ين', + 'ﲕ' => 'يى', + 'ﲖ' => 'يي', + 'ﲗ' => 'ئج', + 'ﲘ' => 'ئح', + 'ﲙ' => 'ئخ', + 'ﲚ' => 'ئم', + 'ﲛ' => 'ئه', + 'ﲜ' => 'بج', + 'ﲝ' => 'بح', + 'ﲞ' => 'بخ', + 'ﲟ' => 'بم', + 'ﲠ' => 'به', + 'ﲡ' => 'تج', + 'ﲢ' => 'تح', + 'ﲣ' => 'تخ', + 'ﲤ' => 'تم', + 'ﲥ' => 'ته', + 'ﲦ' => 'ثم', + 'ﲧ' => 'جح', + 'ﲨ' => 'جم', + 'ﲩ' => 'حج', + 'ﲪ' => 'حم', + 'ﲫ' => 'خج', + 'ﲬ' => 'خم', + 'ﲭ' => 'سج', + 'ﲮ' => 'سح', + 'ﲯ' => 'سخ', + 'ﲰ' => 'سم', + 'ﲱ' => 'صح', + 'ﲲ' => 'صخ', + 'ﲳ' => 'صم', + 'ﲴ' => 'ضج', + 'ﲵ' => 'ضح', + 'ﲶ' => 'ضخ', + 'ﲷ' => 'ضم', + 'ﲸ' => 'طح', + 'ﲹ' => 'ظم', + 'ﲺ' => 'عج', + 'ﲻ' => 'عم', + 'ﲼ' => 'غج', + 'ﲽ' => 'غم', + 'ﲾ' => 'فج', + 'ﲿ' => 'فح', + 'ﳀ' => 'فخ', + 'ﳁ' => 'فم', + 'ﳂ' => 'قح', + 'ﳃ' => 'قم', + 'ﳄ' => 'كج', + 'ﳅ' => 'كح', + 'ﳆ' => 'كخ', + 'ﳇ' => 'كل', + 'ﳈ' => 'كم', + 'ﳉ' => 'لج', + 'ﳊ' => 'لح', + 'ﳋ' => 'لخ', + 'ﳌ' => 'لم', + 'ﳍ' => 'له', + 'ﳎ' => 'مج', + 'ﳏ' => 'مح', + 'ﳐ' => 'مخ', + 'ﳑ' => 'مم', + 'ﳒ' => 'نج', + 'ﳓ' => 'نح', + 'ﳔ' => 'نخ', + 'ﳕ' => 'نم', + 'ﳖ' => 'نه', + 'ﳗ' => 'هج', + 'ﳘ' => 'هم', + 'ﳙ' => 'هٰ', + 'ﳚ' => 'يج', + 'ﳛ' => 'يح', + 'ﳜ' => 'يخ', + 'ﳝ' => 'يم', + 'ﳞ' => 'يه', + 'ﳟ' => 'ئم', + 'ﳠ' => 'ئه', + 'ﳡ' => 'بم', + 'ﳢ' => 'به', + 'ﳣ' => 'تم', + 'ﳤ' => 'ته', + 'ﳥ' => 'ثم', + 'ﳦ' => 'ثه', + 'ﳧ' => 'سم', + 'ﳨ' => 'سه', + 'ﳩ' => 'شم', + 'ﳪ' => 'شه', + 'ﳫ' => 'كل', + 'ﳬ' => 'كم', + 'ﳭ' => 'لم', + 'ﳮ' => 'نم', + 'ﳯ' => 'نه', + 'ﳰ' => 'يم', + 'ﳱ' => 'يه', + 'ﳲ' => 'ـَّ', + 'ﳳ' => 'ـُّ', + 'ﳴ' => 'ـِّ', + 'ﳵ' => 'طى', + 'ﳶ' => 'طي', + 'ﳷ' => 'عى', + 'ﳸ' => 'عي', + 'ﳹ' => 'غى', + 'ﳺ' => 'غي', + 'ﳻ' => 'سى', + 'ﳼ' => 'سي', + 'ﳽ' => 'شى', + 'ﳾ' => 'شي', + 'ﳿ' => 'حى', + 'ﴀ' => 'حي', + 'ﴁ' => 'جى', + 'ﴂ' => 'جي', + 'ﴃ' => 'خى', + 'ﴄ' => 'خي', + 'ﴅ' => 'صى', + 'ﴆ' => 'صي', + 'ﴇ' => 'ضى', + 'ﴈ' => 'ضي', + 'ﴉ' => 'شج', + 'ﴊ' => 'شح', + 'ﴋ' => 'شخ', + 'ﴌ' => 'شم', + 'ﴍ' => 'شر', + 'ﴎ' => 'سر', + 'ﴏ' => 'صر', + 'ﴐ' => 'ضر', + 'ﴑ' => 'طى', + 'ﴒ' => 'طي', + 'ﴓ' => 'عى', + 'ﴔ' => 'عي', + 'ﴕ' => 'غى', + 'ﴖ' => 'غي', + 'ﴗ' => 'سى', + 'ﴘ' => 'سي', + 'ﴙ' => 'شى', + 'ﴚ' => 'شي', + 'ﴛ' => 'حى', + 'ﴜ' => 'حي', + 'ﴝ' => 'جى', + 'ﴞ' => 'جي', + 'ﴟ' => 'خى', + 'ﴠ' => 'خي', + 'ﴡ' => 'صى', + 'ﴢ' => 'صي', + 'ﴣ' => 'ضى', + 'ﴤ' => 'ضي', + 'ﴥ' => 'شج', + 'ﴦ' => 'شح', + 'ﴧ' => 'شخ', + 'ﴨ' => 'شم', + 'ﴩ' => 'شر', + 'ﴪ' => 'سر', + 'ﴫ' => 'صر', + 'ﴬ' => 'ضر', + 'ﴭ' => 'شج', + 'ﴮ' => 'شح', + 'ﴯ' => 'شخ', + 'ﴰ' => 'شم', + 'ﴱ' => 'سه', + 'ﴲ' => 'شه', + 'ﴳ' => 'طم', + 'ﴴ' => 'سج', + 'ﴵ' => 'سح', + 'ﴶ' => 'سخ', + 'ﴷ' => 'شج', + 'ﴸ' => 'شح', + 'ﴹ' => 'شخ', + 'ﴺ' => 'طم', + 'ﴻ' => 'ظم', + 'ﴼ' => 'اً', + 'ﴽ' => 'اً', + 'ﵐ' => 'تجم', + 'ﵑ' => 'تحج', + 'ﵒ' => 'تحج', + 'ﵓ' => 'تحم', + 'ﵔ' => 'تخم', + 'ﵕ' => 'تمج', + 'ﵖ' => 'تمح', + 'ﵗ' => 'تمخ', + 'ﵘ' => 'جمح', + 'ﵙ' => 'جمح', + 'ﵚ' => 'حمي', + 'ﵛ' => 'حمى', + 'ﵜ' => 'سحج', + 'ﵝ' => 'سجح', + 'ﵞ' => 'سجى', + 'ﵟ' => 'سمح', + 'ﵠ' => 'سمح', + 'ﵡ' => 'سمج', + 'ﵢ' => 'سمم', + 'ﵣ' => 'سمم', + 'ﵤ' => 'صحح', + 'ﵥ' => 'صحح', + 'ﵦ' => 'صمم', + 'ﵧ' => 'شحم', + 'ﵨ' => 'شحم', + 'ﵩ' => 'شجي', + 'ﵪ' => 'شمخ', + 'ﵫ' => 'شمخ', + 'ﵬ' => 'شمم', + 'ﵭ' => 'شمم', + 'ﵮ' => 'ضحى', + 'ﵯ' => 'ضخم', + 'ﵰ' => 'ضخم', + 'ﵱ' => 'طمح', + 'ﵲ' => 'طمح', + 'ﵳ' => 'طمم', + 'ﵴ' => 'طمي', + 'ﵵ' => 'عجم', + 'ﵶ' => 'عمم', + 'ﵷ' => 'عمم', + 'ﵸ' => 'عمى', + 'ﵹ' => 'غمم', + 'ﵺ' => 'غمي', + 'ﵻ' => 'غمى', + 'ﵼ' => 'فخم', + 'ﵽ' => 'فخم', + 'ﵾ' => 'قمح', + 'ﵿ' => 'قمم', + 'ﶀ' => 'لحم', + 'ﶁ' => 'لحي', + 'ﶂ' => 'لحى', + 'ﶃ' => 'لجج', + 'ﶄ' => 'لجج', + 'ﶅ' => 'لخم', + 'ﶆ' => 'لخم', + 'ﶇ' => 'لمح', + 'ﶈ' => 'لمح', + 'ﶉ' => 'محج', + 'ﶊ' => 'محم', + 'ﶋ' => 'محي', + 'ﶌ' => 'مجح', + 'ﶍ' => 'مجم', + 'ﶎ' => 'مخج', + 'ﶏ' => 'مخم', + 'ﶒ' => 'مجخ', + 'ﶓ' => 'همج', + 'ﶔ' => 'همم', + 'ﶕ' => 'نحم', + 'ﶖ' => 'نحى', + 'ﶗ' => 'نجم', + 'ﶘ' => 'نجم', + 'ﶙ' => 'نجى', + 'ﶚ' => 'نمي', + 'ﶛ' => 'نمى', + 'ﶜ' => 'يمم', + 'ﶝ' => 'يمم', + 'ﶞ' => 'بخي', + 'ﶟ' => 'تجي', + 'ﶠ' => 'تجى', + 'ﶡ' => 'تخي', + 'ﶢ' => 'تخى', + 'ﶣ' => 'تمي', + 'ﶤ' => 'تمى', + 'ﶥ' => 'جمي', + 'ﶦ' => 'جحى', + 'ﶧ' => 'جمى', + 'ﶨ' => 'سخى', + 'ﶩ' => 'صحي', + 'ﶪ' => 'شحي', + 'ﶫ' => 'ضحي', + 'ﶬ' => 'لجي', + 'ﶭ' => 'لمي', + 'ﶮ' => 'يحي', + 'ﶯ' => 'يجي', + 'ﶰ' => 'يمي', + 'ﶱ' => 'ممي', + 'ﶲ' => 'قمي', + 'ﶳ' => 'نحي', + 'ﶴ' => 'قمح', + 'ﶵ' => 'لحم', + 'ﶶ' => 'عمي', + 'ﶷ' => 'كمي', + 'ﶸ' => 'نجح', + 'ﶹ' => 'مخي', + 'ﶺ' => 'لجم', + 'ﶻ' => 'كمم', + 'ﶼ' => 'لجم', + 'ﶽ' => 'نجح', + 'ﶾ' => 'جحي', + 'ﶿ' => 'حجي', + 'ﷀ' => 'مجي', + 'ﷁ' => 'فمي', + 'ﷂ' => 'بحي', + 'ﷃ' => 'كمم', + 'ﷄ' => 'عجم', + 'ﷅ' => 'صمم', + 'ﷆ' => 'سخي', + 'ﷇ' => 'نجي', + 'ﷰ' => 'صلے', + 'ﷱ' => 'قلے', + 'ﷲ' => 'الله', + 'ﷳ' => 'اكبر', + 'ﷴ' => 'محمد', + 'ﷵ' => 'صلعم', + 'ﷶ' => 'رسول', + 'ﷷ' => 'عليه', + 'ﷸ' => 'وسلم', + 'ﷹ' => 'صلى', + 'ﷺ' => 'صلى الله عليه وسلم', + 'ﷻ' => 'جل جلاله', + '﷼' => 'ریال', + '︐' => ',', + '︑' => '、', + '︒' => '。', + '︓' => ':', + '︔' => ';', + '︕' => '!', + '︖' => '?', + '︗' => '〖', + '︘' => '〗', + '︙' => '...', + '︰' => '..', + '︱' => '—', + '︲' => '–', + '︳' => '_', + '︴' => '_', + '︵' => '(', + '︶' => ')', + '︷' => '{', + '︸' => '}', + '︹' => '〔', + '︺' => '〕', + '︻' => '【', + '︼' => '】', + '︽' => '《', + '︾' => '》', + '︿' => '〈', + '﹀' => '〉', + '﹁' => '「', + '﹂' => '」', + '﹃' => '『', + '﹄' => '』', + '﹇' => '[', + '﹈' => ']', + '﹉' => ' ̅', + '﹊' => ' ̅', + '﹋' => ' ̅', + '﹌' => ' ̅', + '﹍' => '_', + '﹎' => '_', + '﹏' => '_', + '﹐' => ',', + '﹑' => '、', + '﹒' => '.', + '﹔' => ';', + '﹕' => ':', + '﹖' => '?', + '﹗' => '!', + '﹘' => '—', + '﹙' => '(', + '﹚' => ')', + '﹛' => '{', + '﹜' => '}', + '﹝' => '〔', + '﹞' => '〕', + '﹟' => '#', + '﹠' => '&', + '﹡' => '*', + '﹢' => '+', + '﹣' => '-', + '﹤' => '<', + '﹥' => '>', + '﹦' => '=', + '﹨' => '\\', + '﹩' => '$', + '﹪' => '%', + '﹫' => '@', + 'ﹰ' => ' ً', + 'ﹱ' => 'ـً', + 'ﹲ' => ' ٌ', + 'ﹴ' => ' ٍ', + 'ﹶ' => ' َ', + 'ﹷ' => 'ـَ', + 'ﹸ' => ' ُ', + 'ﹹ' => 'ـُ', + 'ﹺ' => ' ِ', + 'ﹻ' => 'ـِ', + 'ﹼ' => ' ّ', + 'ﹽ' => 'ـّ', + 'ﹾ' => ' ْ', + 'ﹿ' => 'ـْ', + 'ﺀ' => 'ء', + 'ﺁ' => 'آ', + 'ﺂ' => 'آ', + 'ﺃ' => 'أ', + 'ﺄ' => 'أ', + 'ﺅ' => 'ؤ', + 'ﺆ' => 'ؤ', + 'ﺇ' => 'إ', + 'ﺈ' => 'إ', + 'ﺉ' => 'ئ', + 'ﺊ' => 'ئ', + 'ﺋ' => 'ئ', + 'ﺌ' => 'ئ', + 'ﺍ' => 'ا', + 'ﺎ' => 'ا', + 'ﺏ' => 'ب', + 'ﺐ' => 'ب', + 'ﺑ' => 'ب', + 'ﺒ' => 'ب', + 'ﺓ' => 'ة', + 'ﺔ' => 'ة', + 'ﺕ' => 'ت', + 'ﺖ' => 'ت', + 'ﺗ' => 'ت', + 'ﺘ' => 'ت', + 'ﺙ' => 'ث', + 'ﺚ' => 'ث', + 'ﺛ' => 'ث', + 'ﺜ' => 'ث', + 'ﺝ' => 'ج', + 'ﺞ' => 'ج', + 'ﺟ' => 'ج', + 'ﺠ' => 'ج', + 'ﺡ' => 'ح', + 'ﺢ' => 'ح', + 'ﺣ' => 'ح', + 'ﺤ' => 'ح', + 'ﺥ' => 'خ', + 'ﺦ' => 'خ', + 'ﺧ' => 'خ', + 'ﺨ' => 'خ', + 'ﺩ' => 'د', + 'ﺪ' => 'د', + 'ﺫ' => 'ذ', + 'ﺬ' => 'ذ', + 'ﺭ' => 'ر', + 'ﺮ' => 'ر', + 'ﺯ' => 'ز', + 'ﺰ' => 'ز', + 'ﺱ' => 'س', + 'ﺲ' => 'س', + 'ﺳ' => 'س', + 'ﺴ' => 'س', + 'ﺵ' => 'ش', + 'ﺶ' => 'ش', + 'ﺷ' => 'ش', + 'ﺸ' => 'ش', + 'ﺹ' => 'ص', + 'ﺺ' => 'ص', + 'ﺻ' => 'ص', + 'ﺼ' => 'ص', + 'ﺽ' => 'ض', + 'ﺾ' => 'ض', + 'ﺿ' => 'ض', + 'ﻀ' => 'ض', + 'ﻁ' => 'ط', + 'ﻂ' => 'ط', + 'ﻃ' => 'ط', + 'ﻄ' => 'ط', + 'ﻅ' => 'ظ', + 'ﻆ' => 'ظ', + 'ﻇ' => 'ظ', + 'ﻈ' => 'ظ', + 'ﻉ' => 'ع', + 'ﻊ' => 'ع', + 'ﻋ' => 'ع', + 'ﻌ' => 'ع', + 'ﻍ' => 'غ', + 'ﻎ' => 'غ', + 'ﻏ' => 'غ', + 'ﻐ' => 'غ', + 'ﻑ' => 'ف', + 'ﻒ' => 'ف', + 'ﻓ' => 'ف', + 'ﻔ' => 'ف', + 'ﻕ' => 'ق', + 'ﻖ' => 'ق', + 'ﻗ' => 'ق', + 'ﻘ' => 'ق', + 'ﻙ' => 'ك', + 'ﻚ' => 'ك', + 'ﻛ' => 'ك', + 'ﻜ' => 'ك', + 'ﻝ' => 'ل', + 'ﻞ' => 'ل', + 'ﻟ' => 'ل', + 'ﻠ' => 'ل', + 'ﻡ' => 'م', + 'ﻢ' => 'م', + 'ﻣ' => 'م', + 'ﻤ' => 'م', + 'ﻥ' => 'ن', + 'ﻦ' => 'ن', + 'ﻧ' => 'ن', + 'ﻨ' => 'ن', + 'ﻩ' => 'ه', + 'ﻪ' => 'ه', + 'ﻫ' => 'ه', + 'ﻬ' => 'ه', + 'ﻭ' => 'و', + 'ﻮ' => 'و', + 'ﻯ' => 'ى', + 'ﻰ' => 'ى', + 'ﻱ' => 'ي', + 'ﻲ' => 'ي', + 'ﻳ' => 'ي', + 'ﻴ' => 'ي', + 'ﻵ' => 'لآ', + 'ﻶ' => 'لآ', + 'ﻷ' => 'لأ', + 'ﻸ' => 'لأ', + 'ﻹ' => 'لإ', + 'ﻺ' => 'لإ', + 'ﻻ' => 'لا', + 'ﻼ' => 'لا', + '!' => '!', + '"' => '"', + '#' => '#', + '$' => '$', + '%' => '%', + '&' => '&', + ''' => '\'', + '(' => '(', + ')' => ')', + '*' => '*', + '+' => '+', + ',' => ',', + '-' => '-', + '.' => '.', + '/' => '/', + '0' => '0', + '1' => '1', + '2' => '2', + '3' => '3', + '4' => '4', + '5' => '5', + '6' => '6', + '7' => '7', + '8' => '8', + '9' => '9', + ':' => ':', + ';' => ';', + '<' => '<', + '=' => '=', + '>' => '>', + '?' => '?', + '@' => '@', + 'A' => 'A', + 'B' => 'B', + 'C' => 'C', + 'D' => 'D', + 'E' => 'E', + 'F' => 'F', + 'G' => 'G', + 'H' => 'H', + 'I' => 'I', + 'J' => 'J', + 'K' => 'K', + 'L' => 'L', + 'M' => 'M', + 'N' => 'N', + 'O' => 'O', + 'P' => 'P', + 'Q' => 'Q', + 'R' => 'R', + 'S' => 'S', + 'T' => 'T', + 'U' => 'U', + 'V' => 'V', + 'W' => 'W', + 'X' => 'X', + 'Y' => 'Y', + 'Z' => 'Z', + '[' => '[', + '\' => '\\', + ']' => ']', + '^' => '^', + '_' => '_', + '`' => '`', + 'a' => 'a', + 'b' => 'b', + 'c' => 'c', + 'd' => 'd', + 'e' => 'e', + 'f' => 'f', + 'g' => 'g', + 'h' => 'h', + 'i' => 'i', + 'j' => 'j', + 'k' => 'k', + 'l' => 'l', + 'm' => 'm', + 'n' => 'n', + 'o' => 'o', + 'p' => 'p', + 'q' => 'q', + 'r' => 'r', + 's' => 's', + 't' => 't', + 'u' => 'u', + 'v' => 'v', + 'w' => 'w', + 'x' => 'x', + 'y' => 'y', + 'z' => 'z', + '{' => '{', + '|' => '|', + '}' => '}', + '~' => '~', + '⦅' => '⦅', + '⦆' => '⦆', + '。' => '。', + '「' => '「', + '」' => '」', + '、' => '、', + '・' => '・', + 'ヲ' => 'ヲ', + 'ァ' => 'ァ', + 'ィ' => 'ィ', + 'ゥ' => 'ゥ', + 'ェ' => 'ェ', + 'ォ' => 'ォ', + 'ャ' => 'ャ', + 'ュ' => 'ュ', + 'ョ' => 'ョ', + 'ッ' => 'ッ', + 'ー' => 'ー', + 'ア' => 'ア', + 'イ' => 'イ', + 'ウ' => 'ウ', + 'エ' => 'エ', + 'オ' => 'オ', + 'カ' => 'カ', + 'キ' => 'キ', + 'ク' => 'ク', + 'ケ' => 'ケ', + 'コ' => 'コ', + 'サ' => 'サ', + 'シ' => 'シ', + 'ス' => 'ス', + 'セ' => 'セ', + 'ソ' => 'ソ', + 'タ' => 'タ', + 'チ' => 'チ', + 'ツ' => 'ツ', + 'テ' => 'テ', + 'ト' => 'ト', + 'ナ' => 'ナ', + 'ニ' => 'ニ', + 'ヌ' => 'ヌ', + 'ネ' => 'ネ', + 'ノ' => 'ノ', + 'ハ' => 'ハ', + 'ヒ' => 'ヒ', + 'フ' => 'フ', + 'ヘ' => 'ヘ', + 'ホ' => 'ホ', + 'マ' => 'マ', + 'ミ' => 'ミ', + 'ム' => 'ム', + 'メ' => 'メ', + 'モ' => 'モ', + 'ヤ' => 'ヤ', + 'ユ' => 'ユ', + 'ヨ' => 'ヨ', + 'ラ' => 'ラ', + 'リ' => 'リ', + 'ル' => 'ル', + 'レ' => 'レ', + 'ロ' => 'ロ', + 'ワ' => 'ワ', + 'ン' => 'ン', + '゙' => '゙', + '゚' => '゚', + 'ᅠ' => 'ᅠ', + 'ᄀ' => 'ᄀ', + 'ᄁ' => 'ᄁ', + 'ᆪ' => 'ᆪ', + 'ᄂ' => 'ᄂ', + 'ᆬ' => 'ᆬ', + 'ᆭ' => 'ᆭ', + 'ᄃ' => 'ᄃ', + 'ᄄ' => 'ᄄ', + 'ᄅ' => 'ᄅ', + 'ᆰ' => 'ᆰ', + 'ᆱ' => 'ᆱ', + 'ᆲ' => 'ᆲ', + 'ᆳ' => 'ᆳ', + 'ᆴ' => 'ᆴ', + 'ᆵ' => 'ᆵ', + 'ᄚ' => 'ᄚ', + 'ᄆ' => 'ᄆ', + 'ᄇ' => 'ᄇ', + 'ᄈ' => 'ᄈ', + 'ᄡ' => 'ᄡ', + 'ᄉ' => 'ᄉ', + 'ᄊ' => 'ᄊ', + 'ᄋ' => 'ᄋ', + 'ᄌ' => 'ᄌ', + 'ᄍ' => 'ᄍ', + 'ᄎ' => 'ᄎ', + 'ᄏ' => 'ᄏ', + 'ᄐ' => 'ᄐ', + 'ᄑ' => 'ᄑ', + 'ᄒ' => 'ᄒ', + 'ᅡ' => 'ᅡ', + 'ᅢ' => 'ᅢ', + 'ᅣ' => 'ᅣ', + 'ᅤ' => 'ᅤ', + 'ᅥ' => 'ᅥ', + 'ᅦ' => 'ᅦ', + 'ᅧ' => 'ᅧ', + 'ᅨ' => 'ᅨ', + 'ᅩ' => 'ᅩ', + 'ᅪ' => 'ᅪ', + 'ᅫ' => 'ᅫ', + 'ᅬ' => 'ᅬ', + 'ᅭ' => 'ᅭ', + 'ᅮ' => 'ᅮ', + 'ᅯ' => 'ᅯ', + 'ᅰ' => 'ᅰ', + 'ᅱ' => 'ᅱ', + 'ᅲ' => 'ᅲ', + 'ᅳ' => 'ᅳ', + 'ᅴ' => 'ᅴ', + 'ᅵ' => 'ᅵ', + '¢' => '¢', + '£' => '£', + '¬' => '¬', + ' ̄' => ' ̄', + '¦' => '¦', + '¥' => '¥', + '₩' => '₩', + '│' => '│', + '←' => '←', + '↑' => '↑', + '→' => '→', + '↓' => '↓', + '■' => '■', + '○' => '○', + '𝐀' => 'A', + '𝐁' => 'B', + '𝐂' => 'C', + '𝐃' => 'D', + '𝐄' => 'E', + '𝐅' => 'F', + '𝐆' => 'G', + '𝐇' => 'H', + '𝐈' => 'I', + '𝐉' => 'J', + '𝐊' => 'K', + '𝐋' => 'L', + '𝐌' => 'M', + '𝐍' => 'N', + '𝐎' => 'O', + '𝐏' => 'P', + '𝐐' => 'Q', + '𝐑' => 'R', + '𝐒' => 'S', + '𝐓' => 'T', + '𝐔' => 'U', + '𝐕' => 'V', + '𝐖' => 'W', + '𝐗' => 'X', + '𝐘' => 'Y', + '𝐙' => 'Z', + '𝐚' => 'a', + '𝐛' => 'b', + '𝐜' => 'c', + '𝐝' => 'd', + '𝐞' => 'e', + '𝐟' => 'f', + '𝐠' => 'g', + '𝐡' => 'h', + '𝐢' => 'i', + '𝐣' => 'j', + '𝐤' => 'k', + '𝐥' => 'l', + '𝐦' => 'm', + '𝐧' => 'n', + '𝐨' => 'o', + '𝐩' => 'p', + '𝐪' => 'q', + '𝐫' => 'r', + '𝐬' => 's', + '𝐭' => 't', + '𝐮' => 'u', + '𝐯' => 'v', + '𝐰' => 'w', + '𝐱' => 'x', + '𝐲' => 'y', + '𝐳' => 'z', + '𝐴' => 'A', + '𝐵' => 'B', + '𝐶' => 'C', + '𝐷' => 'D', + '𝐸' => 'E', + '𝐹' => 'F', + '𝐺' => 'G', + '𝐻' => 'H', + '𝐼' => 'I', + '𝐽' => 'J', + '𝐾' => 'K', + '𝐿' => 'L', + '𝑀' => 'M', + '𝑁' => 'N', + '𝑂' => 'O', + '𝑃' => 'P', + '𝑄' => 'Q', + '𝑅' => 'R', + '𝑆' => 'S', + '𝑇' => 'T', + '𝑈' => 'U', + '𝑉' => 'V', + '𝑊' => 'W', + '𝑋' => 'X', + '𝑌' => 'Y', + '𝑍' => 'Z', + '𝑎' => 'a', + '𝑏' => 'b', + '𝑐' => 'c', + '𝑑' => 'd', + '𝑒' => 'e', + '𝑓' => 'f', + '𝑔' => 'g', + '𝑖' => 'i', + '𝑗' => 'j', + '𝑘' => 'k', + '𝑙' => 'l', + '𝑚' => 'm', + '𝑛' => 'n', + '𝑜' => 'o', + '𝑝' => 'p', + '𝑞' => 'q', + '𝑟' => 'r', + '𝑠' => 's', + '𝑡' => 't', + '𝑢' => 'u', + '𝑣' => 'v', + '𝑤' => 'w', + '𝑥' => 'x', + '𝑦' => 'y', + '𝑧' => 'z', + '𝑨' => 'A', + '𝑩' => 'B', + '𝑪' => 'C', + '𝑫' => 'D', + '𝑬' => 'E', + '𝑭' => 'F', + '𝑮' => 'G', + '𝑯' => 'H', + '𝑰' => 'I', + '𝑱' => 'J', + '𝑲' => 'K', + '𝑳' => 'L', + '𝑴' => 'M', + '𝑵' => 'N', + '𝑶' => 'O', + '𝑷' => 'P', + '𝑸' => 'Q', + '𝑹' => 'R', + '𝑺' => 'S', + '𝑻' => 'T', + '𝑼' => 'U', + '𝑽' => 'V', + '𝑾' => 'W', + '𝑿' => 'X', + '𝒀' => 'Y', + '𝒁' => 'Z', + '𝒂' => 'a', + '𝒃' => 'b', + '𝒄' => 'c', + '𝒅' => 'd', + '𝒆' => 'e', + '𝒇' => 'f', + '𝒈' => 'g', + '𝒉' => 'h', + '𝒊' => 'i', + '𝒋' => 'j', + '𝒌' => 'k', + '𝒍' => 'l', + '𝒎' => 'm', + '𝒏' => 'n', + '𝒐' => 'o', + '𝒑' => 'p', + '𝒒' => 'q', + '𝒓' => 'r', + '𝒔' => 's', + '𝒕' => 't', + '𝒖' => 'u', + '𝒗' => 'v', + '𝒘' => 'w', + '𝒙' => 'x', + '𝒚' => 'y', + '𝒛' => 'z', + '𝒜' => 'A', + '𝒞' => 'C', + '𝒟' => 'D', + '𝒢' => 'G', + '𝒥' => 'J', + '𝒦' => 'K', + '𝒩' => 'N', + '𝒪' => 'O', + '𝒫' => 'P', + '𝒬' => 'Q', + '𝒮' => 'S', + '𝒯' => 'T', + '𝒰' => 'U', + '𝒱' => 'V', + '𝒲' => 'W', + '𝒳' => 'X', + '𝒴' => 'Y', + '𝒵' => 'Z', + '𝒶' => 'a', + '𝒷' => 'b', + '𝒸' => 'c', + '𝒹' => 'd', + '𝒻' => 'f', + '𝒽' => 'h', + '𝒾' => 'i', + '𝒿' => 'j', + '𝓀' => 'k', + '𝓁' => 'l', + '𝓂' => 'm', + '𝓃' => 'n', + '𝓅' => 'p', + '𝓆' => 'q', + '𝓇' => 'r', + '𝓈' => 's', + '𝓉' => 't', + '𝓊' => 'u', + '𝓋' => 'v', + '𝓌' => 'w', + '𝓍' => 'x', + '𝓎' => 'y', + '𝓏' => 'z', + '𝓐' => 'A', + '𝓑' => 'B', + '𝓒' => 'C', + '𝓓' => 'D', + '𝓔' => 'E', + '𝓕' => 'F', + '𝓖' => 'G', + '𝓗' => 'H', + '𝓘' => 'I', + '𝓙' => 'J', + '𝓚' => 'K', + '𝓛' => 'L', + '𝓜' => 'M', + '𝓝' => 'N', + '𝓞' => 'O', + '𝓟' => 'P', + '𝓠' => 'Q', + '𝓡' => 'R', + '𝓢' => 'S', + '𝓣' => 'T', + '𝓤' => 'U', + '𝓥' => 'V', + '𝓦' => 'W', + '𝓧' => 'X', + '𝓨' => 'Y', + '𝓩' => 'Z', + '𝓪' => 'a', + '𝓫' => 'b', + '𝓬' => 'c', + '𝓭' => 'd', + '𝓮' => 'e', + '𝓯' => 'f', + '𝓰' => 'g', + '𝓱' => 'h', + '𝓲' => 'i', + '𝓳' => 'j', + '𝓴' => 'k', + '𝓵' => 'l', + '𝓶' => 'm', + '𝓷' => 'n', + '𝓸' => 'o', + '𝓹' => 'p', + '𝓺' => 'q', + '𝓻' => 'r', + '𝓼' => 's', + '𝓽' => 't', + '𝓾' => 'u', + '𝓿' => 'v', + '𝔀' => 'w', + '𝔁' => 'x', + '𝔂' => 'y', + '𝔃' => 'z', + '𝔄' => 'A', + '𝔅' => 'B', + '𝔇' => 'D', + '𝔈' => 'E', + '𝔉' => 'F', + '𝔊' => 'G', + '𝔍' => 'J', + '𝔎' => 'K', + '𝔏' => 'L', + '𝔐' => 'M', + '𝔑' => 'N', + '𝔒' => 'O', + '𝔓' => 'P', + '𝔔' => 'Q', + '𝔖' => 'S', + '𝔗' => 'T', + '𝔘' => 'U', + '𝔙' => 'V', + '𝔚' => 'W', + '𝔛' => 'X', + '𝔜' => 'Y', + '𝔞' => 'a', + '𝔟' => 'b', + '𝔠' => 'c', + '𝔡' => 'd', + '𝔢' => 'e', + '𝔣' => 'f', + '𝔤' => 'g', + '𝔥' => 'h', + '𝔦' => 'i', + '𝔧' => 'j', + '𝔨' => 'k', + '𝔩' => 'l', + '𝔪' => 'm', + '𝔫' => 'n', + '𝔬' => 'o', + '𝔭' => 'p', + '𝔮' => 'q', + '𝔯' => 'r', + '𝔰' => 's', + '𝔱' => 't', + '𝔲' => 'u', + '𝔳' => 'v', + '𝔴' => 'w', + '𝔵' => 'x', + '𝔶' => 'y', + '𝔷' => 'z', + '𝔸' => 'A', + '𝔹' => 'B', + '𝔻' => 'D', + '𝔼' => 'E', + '𝔽' => 'F', + '𝔾' => 'G', + '𝕀' => 'I', + '𝕁' => 'J', + '𝕂' => 'K', + '𝕃' => 'L', + '𝕄' => 'M', + '𝕆' => 'O', + '𝕊' => 'S', + '𝕋' => 'T', + '𝕌' => 'U', + '𝕍' => 'V', + '𝕎' => 'W', + '𝕏' => 'X', + '𝕐' => 'Y', + '𝕒' => 'a', + '𝕓' => 'b', + '𝕔' => 'c', + '𝕕' => 'd', + '𝕖' => 'e', + '𝕗' => 'f', + '𝕘' => 'g', + '𝕙' => 'h', + '𝕚' => 'i', + '𝕛' => 'j', + '𝕜' => 'k', + '𝕝' => 'l', + '𝕞' => 'm', + '𝕟' => 'n', + '𝕠' => 'o', + '𝕡' => 'p', + '𝕢' => 'q', + '𝕣' => 'r', + '𝕤' => 's', + '𝕥' => 't', + '𝕦' => 'u', + '𝕧' => 'v', + '𝕨' => 'w', + '𝕩' => 'x', + '𝕪' => 'y', + '𝕫' => 'z', + '𝕬' => 'A', + '𝕭' => 'B', + '𝕮' => 'C', + '𝕯' => 'D', + '𝕰' => 'E', + '𝕱' => 'F', + '𝕲' => 'G', + '𝕳' => 'H', + '𝕴' => 'I', + '𝕵' => 'J', + '𝕶' => 'K', + '𝕷' => 'L', + '𝕸' => 'M', + '𝕹' => 'N', + '𝕺' => 'O', + '𝕻' => 'P', + '𝕼' => 'Q', + '𝕽' => 'R', + '𝕾' => 'S', + '𝕿' => 'T', + '𝖀' => 'U', + '𝖁' => 'V', + '𝖂' => 'W', + '𝖃' => 'X', + '𝖄' => 'Y', + '𝖅' => 'Z', + '𝖆' => 'a', + '𝖇' => 'b', + '𝖈' => 'c', + '𝖉' => 'd', + '𝖊' => 'e', + '𝖋' => 'f', + '𝖌' => 'g', + '𝖍' => 'h', + '𝖎' => 'i', + '𝖏' => 'j', + '𝖐' => 'k', + '𝖑' => 'l', + '𝖒' => 'm', + '𝖓' => 'n', + '𝖔' => 'o', + '𝖕' => 'p', + '𝖖' => 'q', + '𝖗' => 'r', + '𝖘' => 's', + '𝖙' => 't', + '𝖚' => 'u', + '𝖛' => 'v', + '𝖜' => 'w', + '𝖝' => 'x', + '𝖞' => 'y', + '𝖟' => 'z', + '𝖠' => 'A', + '𝖡' => 'B', + '𝖢' => 'C', + '𝖣' => 'D', + '𝖤' => 'E', + '𝖥' => 'F', + '𝖦' => 'G', + '𝖧' => 'H', + '𝖨' => 'I', + '𝖩' => 'J', + '𝖪' => 'K', + '𝖫' => 'L', + '𝖬' => 'M', + '𝖭' => 'N', + '𝖮' => 'O', + '𝖯' => 'P', + '𝖰' => 'Q', + '𝖱' => 'R', + '𝖲' => 'S', + '𝖳' => 'T', + '𝖴' => 'U', + '𝖵' => 'V', + '𝖶' => 'W', + '𝖷' => 'X', + '𝖸' => 'Y', + '𝖹' => 'Z', + '𝖺' => 'a', + '𝖻' => 'b', + '𝖼' => 'c', + '𝖽' => 'd', + '𝖾' => 'e', + '𝖿' => 'f', + '𝗀' => 'g', + '𝗁' => 'h', + '𝗂' => 'i', + '𝗃' => 'j', + '𝗄' => 'k', + '𝗅' => 'l', + '𝗆' => 'm', + '𝗇' => 'n', + '𝗈' => 'o', + '𝗉' => 'p', + '𝗊' => 'q', + '𝗋' => 'r', + '𝗌' => 's', + '𝗍' => 't', + '𝗎' => 'u', + '𝗏' => 'v', + '𝗐' => 'w', + '𝗑' => 'x', + '𝗒' => 'y', + '𝗓' => 'z', + '𝗔' => 'A', + '𝗕' => 'B', + '𝗖' => 'C', + '𝗗' => 'D', + '𝗘' => 'E', + '𝗙' => 'F', + '𝗚' => 'G', + '𝗛' => 'H', + '𝗜' => 'I', + '𝗝' => 'J', + '𝗞' => 'K', + '𝗟' => 'L', + '𝗠' => 'M', + '𝗡' => 'N', + '𝗢' => 'O', + '𝗣' => 'P', + '𝗤' => 'Q', + '𝗥' => 'R', + '𝗦' => 'S', + '𝗧' => 'T', + '𝗨' => 'U', + '𝗩' => 'V', + '𝗪' => 'W', + '𝗫' => 'X', + '𝗬' => 'Y', + '𝗭' => 'Z', + '𝗮' => 'a', + '𝗯' => 'b', + '𝗰' => 'c', + '𝗱' => 'd', + '𝗲' => 'e', + '𝗳' => 'f', + '𝗴' => 'g', + '𝗵' => 'h', + '𝗶' => 'i', + '𝗷' => 'j', + '𝗸' => 'k', + '𝗹' => 'l', + '𝗺' => 'm', + '𝗻' => 'n', + '𝗼' => 'o', + '𝗽' => 'p', + '𝗾' => 'q', + '𝗿' => 'r', + '𝘀' => 's', + '𝘁' => 't', + '𝘂' => 'u', + '𝘃' => 'v', + '𝘄' => 'w', + '𝘅' => 'x', + '𝘆' => 'y', + '𝘇' => 'z', + '𝘈' => 'A', + '𝘉' => 'B', + '𝘊' => 'C', + '𝘋' => 'D', + '𝘌' => 'E', + '𝘍' => 'F', + '𝘎' => 'G', + '𝘏' => 'H', + '𝘐' => 'I', + '𝘑' => 'J', + '𝘒' => 'K', + '𝘓' => 'L', + '𝘔' => 'M', + '𝘕' => 'N', + '𝘖' => 'O', + '𝘗' => 'P', + '𝘘' => 'Q', + '𝘙' => 'R', + '𝘚' => 'S', + '𝘛' => 'T', + '𝘜' => 'U', + '𝘝' => 'V', + '𝘞' => 'W', + '𝘟' => 'X', + '𝘠' => 'Y', + '𝘡' => 'Z', + '𝘢' => 'a', + '𝘣' => 'b', + '𝘤' => 'c', + '𝘥' => 'd', + '𝘦' => 'e', + '𝘧' => 'f', + '𝘨' => 'g', + '𝘩' => 'h', + '𝘪' => 'i', + '𝘫' => 'j', + '𝘬' => 'k', + '𝘭' => 'l', + '𝘮' => 'm', + '𝘯' => 'n', + '𝘰' => 'o', + '𝘱' => 'p', + '𝘲' => 'q', + '𝘳' => 'r', + '𝘴' => 's', + '𝘵' => 't', + '𝘶' => 'u', + '𝘷' => 'v', + '𝘸' => 'w', + '𝘹' => 'x', + '𝘺' => 'y', + '𝘻' => 'z', + '𝘼' => 'A', + '𝘽' => 'B', + '𝘾' => 'C', + '𝘿' => 'D', + '𝙀' => 'E', + '𝙁' => 'F', + '𝙂' => 'G', + '𝙃' => 'H', + '𝙄' => 'I', + '𝙅' => 'J', + '𝙆' => 'K', + '𝙇' => 'L', + '𝙈' => 'M', + '𝙉' => 'N', + '𝙊' => 'O', + '𝙋' => 'P', + '𝙌' => 'Q', + '𝙍' => 'R', + '𝙎' => 'S', + '𝙏' => 'T', + '𝙐' => 'U', + '𝙑' => 'V', + '𝙒' => 'W', + '𝙓' => 'X', + '𝙔' => 'Y', + '𝙕' => 'Z', + '𝙖' => 'a', + '𝙗' => 'b', + '𝙘' => 'c', + '𝙙' => 'd', + '𝙚' => 'e', + '𝙛' => 'f', + '𝙜' => 'g', + '𝙝' => 'h', + '𝙞' => 'i', + '𝙟' => 'j', + '𝙠' => 'k', + '𝙡' => 'l', + '𝙢' => 'm', + '𝙣' => 'n', + '𝙤' => 'o', + '𝙥' => 'p', + '𝙦' => 'q', + '𝙧' => 'r', + '𝙨' => 's', + '𝙩' => 't', + '𝙪' => 'u', + '𝙫' => 'v', + '𝙬' => 'w', + '𝙭' => 'x', + '𝙮' => 'y', + '𝙯' => 'z', + '𝙰' => 'A', + '𝙱' => 'B', + '𝙲' => 'C', + '𝙳' => 'D', + '𝙴' => 'E', + '𝙵' => 'F', + '𝙶' => 'G', + '𝙷' => 'H', + '𝙸' => 'I', + '𝙹' => 'J', + '𝙺' => 'K', + '𝙻' => 'L', + '𝙼' => 'M', + '𝙽' => 'N', + '𝙾' => 'O', + '𝙿' => 'P', + '𝚀' => 'Q', + '𝚁' => 'R', + '𝚂' => 'S', + '𝚃' => 'T', + '𝚄' => 'U', + '𝚅' => 'V', + '𝚆' => 'W', + '𝚇' => 'X', + '𝚈' => 'Y', + '𝚉' => 'Z', + '𝚊' => 'a', + '𝚋' => 'b', + '𝚌' => 'c', + '𝚍' => 'd', + '𝚎' => 'e', + '𝚏' => 'f', + '𝚐' => 'g', + '𝚑' => 'h', + '𝚒' => 'i', + '𝚓' => 'j', + '𝚔' => 'k', + '𝚕' => 'l', + '𝚖' => 'm', + '𝚗' => 'n', + '𝚘' => 'o', + '𝚙' => 'p', + '𝚚' => 'q', + '𝚛' => 'r', + '𝚜' => 's', + '𝚝' => 't', + '𝚞' => 'u', + '𝚟' => 'v', + '𝚠' => 'w', + '𝚡' => 'x', + '𝚢' => 'y', + '𝚣' => 'z', + '𝚤' => 'ı', + '𝚥' => 'ȷ', + '𝚨' => 'Α', + '𝚩' => 'Β', + '𝚪' => 'Γ', + '𝚫' => 'Δ', + '𝚬' => 'Ε', + '𝚭' => 'Ζ', + '𝚮' => 'Η', + '𝚯' => 'Θ', + '𝚰' => 'Ι', + '𝚱' => 'Κ', + '𝚲' => 'Λ', + '𝚳' => 'Μ', + '𝚴' => 'Ν', + '𝚵' => 'Ξ', + '𝚶' => 'Ο', + '𝚷' => 'Π', + '𝚸' => 'Ρ', + '𝚹' => 'Θ', + '𝚺' => 'Σ', + '𝚻' => 'Τ', + '𝚼' => 'Υ', + '𝚽' => 'Φ', + '𝚾' => 'Χ', + '𝚿' => 'Ψ', + '𝛀' => 'Ω', + '𝛁' => '∇', + '𝛂' => 'α', + '𝛃' => 'β', + '𝛄' => 'γ', + '𝛅' => 'δ', + '𝛆' => 'ε', + '𝛇' => 'ζ', + '𝛈' => 'η', + '𝛉' => 'θ', + '𝛊' => 'ι', + '𝛋' => 'κ', + '𝛌' => 'λ', + '𝛍' => 'μ', + '𝛎' => 'ν', + '𝛏' => 'ξ', + '𝛐' => 'ο', + '𝛑' => 'π', + '𝛒' => 'ρ', + '𝛓' => 'ς', + '𝛔' => 'σ', + '𝛕' => 'τ', + '𝛖' => 'υ', + '𝛗' => 'φ', + '𝛘' => 'χ', + '𝛙' => 'ψ', + '𝛚' => 'ω', + '𝛛' => '∂', + '𝛜' => 'ε', + '𝛝' => 'θ', + '𝛞' => 'κ', + '𝛟' => 'φ', + '𝛠' => 'ρ', + '𝛡' => 'π', + '𝛢' => 'Α', + '𝛣' => 'Β', + '𝛤' => 'Γ', + '𝛥' => 'Δ', + '𝛦' => 'Ε', + '𝛧' => 'Ζ', + '𝛨' => 'Η', + '𝛩' => 'Θ', + '𝛪' => 'Ι', + '𝛫' => 'Κ', + '𝛬' => 'Λ', + '𝛭' => 'Μ', + '𝛮' => 'Ν', + '𝛯' => 'Ξ', + '𝛰' => 'Ο', + '𝛱' => 'Π', + '𝛲' => 'Ρ', + '𝛳' => 'Θ', + '𝛴' => 'Σ', + '𝛵' => 'Τ', + '𝛶' => 'Υ', + '𝛷' => 'Φ', + '𝛸' => 'Χ', + '𝛹' => 'Ψ', + '𝛺' => 'Ω', + '𝛻' => '∇', + '𝛼' => 'α', + '𝛽' => 'β', + '𝛾' => 'γ', + '𝛿' => 'δ', + '𝜀' => 'ε', + '𝜁' => 'ζ', + '𝜂' => 'η', + '𝜃' => 'θ', + '𝜄' => 'ι', + '𝜅' => 'κ', + '𝜆' => 'λ', + '𝜇' => 'μ', + '𝜈' => 'ν', + '𝜉' => 'ξ', + '𝜊' => 'ο', + '𝜋' => 'π', + '𝜌' => 'ρ', + '𝜍' => 'ς', + '𝜎' => 'σ', + '𝜏' => 'τ', + '𝜐' => 'υ', + '𝜑' => 'φ', + '𝜒' => 'χ', + '𝜓' => 'ψ', + '𝜔' => 'ω', + '𝜕' => '∂', + '𝜖' => 'ε', + '𝜗' => 'θ', + '𝜘' => 'κ', + '𝜙' => 'φ', + '𝜚' => 'ρ', + '𝜛' => 'π', + '𝜜' => 'Α', + '𝜝' => 'Β', + '𝜞' => 'Γ', + '𝜟' => 'Δ', + '𝜠' => 'Ε', + '𝜡' => 'Ζ', + '𝜢' => 'Η', + '𝜣' => 'Θ', + '𝜤' => 'Ι', + '𝜥' => 'Κ', + '𝜦' => 'Λ', + '𝜧' => 'Μ', + '𝜨' => 'Ν', + '𝜩' => 'Ξ', + '𝜪' => 'Ο', + '𝜫' => 'Π', + '𝜬' => 'Ρ', + '𝜭' => 'Θ', + '𝜮' => 'Σ', + '𝜯' => 'Τ', + '𝜰' => 'Υ', + '𝜱' => 'Φ', + '𝜲' => 'Χ', + '𝜳' => 'Ψ', + '𝜴' => 'Ω', + '𝜵' => '∇', + '𝜶' => 'α', + '𝜷' => 'β', + '𝜸' => 'γ', + '𝜹' => 'δ', + '𝜺' => 'ε', + '𝜻' => 'ζ', + '𝜼' => 'η', + '𝜽' => 'θ', + '𝜾' => 'ι', + '𝜿' => 'κ', + '𝝀' => 'λ', + '𝝁' => 'μ', + '𝝂' => 'ν', + '𝝃' => 'ξ', + '𝝄' => 'ο', + '𝝅' => 'π', + '𝝆' => 'ρ', + '𝝇' => 'ς', + '𝝈' => 'σ', + '𝝉' => 'τ', + '𝝊' => 'υ', + '𝝋' => 'φ', + '𝝌' => 'χ', + '𝝍' => 'ψ', + '𝝎' => 'ω', + '𝝏' => '∂', + '𝝐' => 'ε', + '𝝑' => 'θ', + '𝝒' => 'κ', + '𝝓' => 'φ', + '𝝔' => 'ρ', + '𝝕' => 'π', + '𝝖' => 'Α', + '𝝗' => 'Β', + '𝝘' => 'Γ', + '𝝙' => 'Δ', + '𝝚' => 'Ε', + '𝝛' => 'Ζ', + '𝝜' => 'Η', + '𝝝' => 'Θ', + '𝝞' => 'Ι', + '𝝟' => 'Κ', + '𝝠' => 'Λ', + '𝝡' => 'Μ', + '𝝢' => 'Ν', + '𝝣' => 'Ξ', + '𝝤' => 'Ο', + '𝝥' => 'Π', + '𝝦' => 'Ρ', + '𝝧' => 'Θ', + '𝝨' => 'Σ', + '𝝩' => 'Τ', + '𝝪' => 'Υ', + '𝝫' => 'Φ', + '𝝬' => 'Χ', + '𝝭' => 'Ψ', + '𝝮' => 'Ω', + '𝝯' => '∇', + '𝝰' => 'α', + '𝝱' => 'β', + '𝝲' => 'γ', + '𝝳' => 'δ', + '𝝴' => 'ε', + '𝝵' => 'ζ', + '𝝶' => 'η', + '𝝷' => 'θ', + '𝝸' => 'ι', + '𝝹' => 'κ', + '𝝺' => 'λ', + '𝝻' => 'μ', + '𝝼' => 'ν', + '𝝽' => 'ξ', + '𝝾' => 'ο', + '𝝿' => 'π', + '𝞀' => 'ρ', + '𝞁' => 'ς', + '𝞂' => 'σ', + '𝞃' => 'τ', + '𝞄' => 'υ', + '𝞅' => 'φ', + '𝞆' => 'χ', + '𝞇' => 'ψ', + '𝞈' => 'ω', + '𝞉' => '∂', + '𝞊' => 'ε', + '𝞋' => 'θ', + '𝞌' => 'κ', + '𝞍' => 'φ', + '𝞎' => 'ρ', + '𝞏' => 'π', + '𝞐' => 'Α', + '𝞑' => 'Β', + '𝞒' => 'Γ', + '𝞓' => 'Δ', + '𝞔' => 'Ε', + '𝞕' => 'Ζ', + '𝞖' => 'Η', + '𝞗' => 'Θ', + '𝞘' => 'Ι', + '𝞙' => 'Κ', + '𝞚' => 'Λ', + '𝞛' => 'Μ', + '𝞜' => 'Ν', + '𝞝' => 'Ξ', + '𝞞' => 'Ο', + '𝞟' => 'Π', + '𝞠' => 'Ρ', + '𝞡' => 'Θ', + '𝞢' => 'Σ', + '𝞣' => 'Τ', + '𝞤' => 'Υ', + '𝞥' => 'Φ', + '𝞦' => 'Χ', + '𝞧' => 'Ψ', + '𝞨' => 'Ω', + '𝞩' => '∇', + '𝞪' => 'α', + '𝞫' => 'β', + '𝞬' => 'γ', + '𝞭' => 'δ', + '𝞮' => 'ε', + '𝞯' => 'ζ', + '𝞰' => 'η', + '𝞱' => 'θ', + '𝞲' => 'ι', + '𝞳' => 'κ', + '𝞴' => 'λ', + '𝞵' => 'μ', + '𝞶' => 'ν', + '𝞷' => 'ξ', + '𝞸' => 'ο', + '𝞹' => 'π', + '𝞺' => 'ρ', + '𝞻' => 'ς', + '𝞼' => 'σ', + '𝞽' => 'τ', + '𝞾' => 'υ', + '𝞿' => 'φ', + '𝟀' => 'χ', + '𝟁' => 'ψ', + '𝟂' => 'ω', + '𝟃' => '∂', + '𝟄' => 'ε', + '𝟅' => 'θ', + '𝟆' => 'κ', + '𝟇' => 'φ', + '𝟈' => 'ρ', + '𝟉' => 'π', + '𝟊' => 'Ϝ', + '𝟋' => 'ϝ', + '𝟎' => '0', + '𝟏' => '1', + '𝟐' => '2', + '𝟑' => '3', + '𝟒' => '4', + '𝟓' => '5', + '𝟔' => '6', + '𝟕' => '7', + '𝟖' => '8', + '𝟗' => '9', + '𝟘' => '0', + '𝟙' => '1', + '𝟚' => '2', + '𝟛' => '3', + '𝟜' => '4', + '𝟝' => '5', + '𝟞' => '6', + '𝟟' => '7', + '𝟠' => '8', + '𝟡' => '9', + '𝟢' => '0', + '𝟣' => '1', + '𝟤' => '2', + '𝟥' => '3', + '𝟦' => '4', + '𝟧' => '5', + '𝟨' => '6', + '𝟩' => '7', + '𝟪' => '8', + '𝟫' => '9', + '𝟬' => '0', + '𝟭' => '1', + '𝟮' => '2', + '𝟯' => '3', + '𝟰' => '4', + '𝟱' => '5', + '𝟲' => '6', + '𝟳' => '7', + '𝟴' => '8', + '𝟵' => '9', + '𝟶' => '0', + '𝟷' => '1', + '𝟸' => '2', + '𝟹' => '3', + '𝟺' => '4', + '𝟻' => '5', + '𝟼' => '6', + '𝟽' => '7', + '𝟾' => '8', + '𝟿' => '9', + '𞸀' => 'ا', + '𞸁' => 'ب', + '𞸂' => 'ج', + '𞸃' => 'د', + '𞸅' => 'و', + '𞸆' => 'ز', + '𞸇' => 'ح', + '𞸈' => 'ط', + '𞸉' => 'ي', + '𞸊' => 'ك', + '𞸋' => 'ل', + '𞸌' => 'م', + '𞸍' => 'ن', + '𞸎' => 'س', + '𞸏' => 'ع', + '𞸐' => 'ف', + '𞸑' => 'ص', + '𞸒' => 'ق', + '𞸓' => 'ر', + '𞸔' => 'ش', + '𞸕' => 'ت', + '𞸖' => 'ث', + '𞸗' => 'خ', + '𞸘' => 'ذ', + '𞸙' => 'ض', + '𞸚' => 'ظ', + '𞸛' => 'غ', + '𞸜' => 'ٮ', + '𞸝' => 'ں', + '𞸞' => 'ڡ', + '𞸟' => 'ٯ', + '𞸡' => 'ب', + '𞸢' => 'ج', + '𞸤' => 'ه', + '𞸧' => 'ح', + '𞸩' => 'ي', + '𞸪' => 'ك', + '𞸫' => 'ل', + '𞸬' => 'م', + '𞸭' => 'ن', + '𞸮' => 'س', + '𞸯' => 'ع', + '𞸰' => 'ف', + '𞸱' => 'ص', + '𞸲' => 'ق', + '𞸴' => 'ش', + '𞸵' => 'ت', + '𞸶' => 'ث', + '𞸷' => 'خ', + '𞸹' => 'ض', + '𞸻' => 'غ', + '𞹂' => 'ج', + '𞹇' => 'ح', + '𞹉' => 'ي', + '𞹋' => 'ل', + '𞹍' => 'ن', + '𞹎' => 'س', + '𞹏' => 'ع', + '𞹑' => 'ص', + '𞹒' => 'ق', + '𞹔' => 'ش', + '𞹗' => 'خ', + '𞹙' => 'ض', + '𞹛' => 'غ', + '𞹝' => 'ں', + '𞹟' => 'ٯ', + '𞹡' => 'ب', + '𞹢' => 'ج', + '𞹤' => 'ه', + '𞹧' => 'ح', + '𞹨' => 'ط', + '𞹩' => 'ي', + '𞹪' => 'ك', + '𞹬' => 'م', + '𞹭' => 'ن', + '𞹮' => 'س', + '𞹯' => 'ع', + '𞹰' => 'ف', + '𞹱' => 'ص', + '𞹲' => 'ق', + '𞹴' => 'ش', + '𞹵' => 'ت', + '𞹶' => 'ث', + '𞹷' => 'خ', + '𞹹' => 'ض', + '𞹺' => 'ظ', + '𞹻' => 'غ', + '𞹼' => 'ٮ', + '𞹾' => 'ڡ', + '𞺀' => 'ا', + '𞺁' => 'ب', + '𞺂' => 'ج', + '𞺃' => 'د', + '𞺄' => 'ه', + '𞺅' => 'و', + '𞺆' => 'ز', + '𞺇' => 'ح', + '𞺈' => 'ط', + '𞺉' => 'ي', + '𞺋' => 'ل', + '𞺌' => 'م', + '𞺍' => 'ن', + '𞺎' => 'س', + '𞺏' => 'ع', + '𞺐' => 'ف', + '𞺑' => 'ص', + '𞺒' => 'ق', + '𞺓' => 'ر', + '𞺔' => 'ش', + '𞺕' => 'ت', + '𞺖' => 'ث', + '𞺗' => 'خ', + '𞺘' => 'ذ', + '𞺙' => 'ض', + '𞺚' => 'ظ', + '𞺛' => 'غ', + '𞺡' => 'ب', + '𞺢' => 'ج', + '𞺣' => 'د', + '𞺥' => 'و', + '𞺦' => 'ز', + '𞺧' => 'ح', + '𞺨' => 'ط', + '𞺩' => 'ي', + '𞺫' => 'ل', + '𞺬' => 'م', + '𞺭' => 'ن', + '𞺮' => 'س', + '𞺯' => 'ع', + '𞺰' => 'ف', + '𞺱' => 'ص', + '𞺲' => 'ق', + '𞺳' => 'ر', + '𞺴' => 'ش', + '𞺵' => 'ت', + '𞺶' => 'ث', + '𞺷' => 'خ', + '𞺸' => 'ذ', + '𞺹' => 'ض', + '𞺺' => 'ظ', + '𞺻' => 'غ', + '🄀' => '0.', + '🄁' => '0,', + '🄂' => '1,', + '🄃' => '2,', + '🄄' => '3,', + '🄅' => '4,', + '🄆' => '5,', + '🄇' => '6,', + '🄈' => '7,', + '🄉' => '8,', + '🄊' => '9,', + '🄐' => '(A)', + '🄑' => '(B)', + '🄒' => '(C)', + '🄓' => '(D)', + '🄔' => '(E)', + '🄕' => '(F)', + '🄖' => '(G)', + '🄗' => '(H)', + '🄘' => '(I)', + '🄙' => '(J)', + '🄚' => '(K)', + '🄛' => '(L)', + '🄜' => '(M)', + '🄝' => '(N)', + '🄞' => '(O)', + '🄟' => '(P)', + '🄠' => '(Q)', + '🄡' => '(R)', + '🄢' => '(S)', + '🄣' => '(T)', + '🄤' => '(U)', + '🄥' => '(V)', + '🄦' => '(W)', + '🄧' => '(X)', + '🄨' => '(Y)', + '🄩' => '(Z)', + '🄪' => '〔S〕', + '🄫' => 'C', + '🄬' => 'R', + '🄭' => 'CD', + '🄮' => 'WZ', + '🄰' => 'A', + '🄱' => 'B', + '🄲' => 'C', + '🄳' => 'D', + '🄴' => 'E', + '🄵' => 'F', + '🄶' => 'G', + '🄷' => 'H', + '🄸' => 'I', + '🄹' => 'J', + '🄺' => 'K', + '🄻' => 'L', + '🄼' => 'M', + '🄽' => 'N', + '🄾' => 'O', + '🄿' => 'P', + '🅀' => 'Q', + '🅁' => 'R', + '🅂' => 'S', + '🅃' => 'T', + '🅄' => 'U', + '🅅' => 'V', + '🅆' => 'W', + '🅇' => 'X', + '🅈' => 'Y', + '🅉' => 'Z', + '🅊' => 'HV', + '🅋' => 'MV', + '🅌' => 'SD', + '🅍' => 'SS', + '🅎' => 'PPV', + '🅏' => 'WC', + '🅪' => 'MC', + '🅫' => 'MD', + '🅬' => 'MR', + '🆐' => 'DJ', + '🈀' => 'ほか', + '🈁' => 'ココ', + '🈂' => 'サ', + '🈐' => '手', + '🈑' => '字', + '🈒' => '双', + '🈓' => 'デ', + '🈔' => '二', + '🈕' => '多', + '🈖' => '解', + '🈗' => '天', + '🈘' => '交', + '🈙' => '映', + '🈚' => '無', + '🈛' => '料', + '🈜' => '前', + '🈝' => '後', + '🈞' => '再', + '🈟' => '新', + '🈠' => '初', + '🈡' => '終', + '🈢' => '生', + '🈣' => '販', + '🈤' => '声', + '🈥' => '吹', + '🈦' => '演', + '🈧' => '投', + '🈨' => '捕', + '🈩' => '一', + '🈪' => '三', + '🈫' => '遊', + '🈬' => '左', + '🈭' => '中', + '🈮' => '右', + '🈯' => '指', + '🈰' => '走', + '🈱' => '打', + '🈲' => '禁', + '🈳' => '空', + '🈴' => '合', + '🈵' => '満', + '🈶' => '有', + '🈷' => '月', + '🈸' => '申', + '🈹' => '割', + '🈺' => '営', + '🈻' => '配', + '🉀' => '〔本〕', + '🉁' => '〔三〕', + '🉂' => '〔二〕', + '🉃' => '〔安〕', + '🉄' => '〔点〕', + '🉅' => '〔打〕', + '🉆' => '〔盗〕', + '🉇' => '〔勝〕', + '🉈' => '〔敗〕', + '🉐' => '得', + '🉑' => '可', + '🯰' => '0', + '🯱' => '1', + '🯲' => '2', + '🯳' => '3', + '🯴' => '4', + '🯵' => '5', + '🯶' => '6', + '🯷' => '7', + '🯸' => '8', + '🯹' => '9', +); diff --git a/3rdparty/symfony/polyfill-intl-normalizer/bootstrap.php b/3rdparty/symfony/polyfill-intl-normalizer/bootstrap.php new file mode 100644 index 00000000..3608e5c0 --- /dev/null +++ b/3rdparty/symfony/polyfill-intl-normalizer/bootstrap.php @@ -0,0 +1,23 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +use Symfony\Polyfill\Intl\Normalizer as p; + +if (\PHP_VERSION_ID >= 80000) { + return require __DIR__.'/bootstrap80.php'; +} + +if (!function_exists('normalizer_is_normalized')) { + function normalizer_is_normalized($string, $form = p\Normalizer::FORM_C) { return p\Normalizer::isNormalized($string, $form); } +} +if (!function_exists('normalizer_normalize')) { + function normalizer_normalize($string, $form = p\Normalizer::FORM_C) { return p\Normalizer::normalize($string, $form); } +} diff --git a/3rdparty/symfony/polyfill-intl-normalizer/bootstrap80.php b/3rdparty/symfony/polyfill-intl-normalizer/bootstrap80.php new file mode 100644 index 00000000..e36d1a94 --- /dev/null +++ b/3rdparty/symfony/polyfill-intl-normalizer/bootstrap80.php @@ -0,0 +1,19 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +use Symfony\Polyfill\Intl\Normalizer as p; + +if (!function_exists('normalizer_is_normalized')) { + function normalizer_is_normalized(?string $string, ?int $form = p\Normalizer::FORM_C): bool { return p\Normalizer::isNormalized((string) $string, (int) $form); } +} +if (!function_exists('normalizer_normalize')) { + function normalizer_normalize(?string $string, ?int $form = p\Normalizer::FORM_C): string|false { return p\Normalizer::normalize((string) $string, (int) $form); } +} diff --git a/3rdparty/symfony/polyfill-php82/LICENSE b/3rdparty/symfony/polyfill-php82/LICENSE new file mode 100644 index 00000000..733c826e --- /dev/null +++ b/3rdparty/symfony/polyfill-php82/LICENSE @@ -0,0 +1,19 @@ +Copyright (c) 2022-present Fabien Potencier + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is furnished +to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +THE SOFTWARE. diff --git a/3rdparty/symfony/polyfill-php82/NoDynamicProperties.php b/3rdparty/symfony/polyfill-php82/NoDynamicProperties.php new file mode 100644 index 00000000..450deff4 --- /dev/null +++ b/3rdparty/symfony/polyfill-php82/NoDynamicProperties.php @@ -0,0 +1,23 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Polyfill\Php82; + +/** + * @internal + */ +trait NoDynamicProperties +{ + public function __set(string $name, $value): void + { + throw new \Error('Cannot create dynamic property '.self::class.'::$'.$name); + } +} diff --git a/3rdparty/symfony/polyfill-php82/Php82.php b/3rdparty/symfony/polyfill-php82/Php82.php new file mode 100644 index 00000000..91da117f --- /dev/null +++ b/3rdparty/symfony/polyfill-php82/Php82.php @@ -0,0 +1,394 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Polyfill\Php82; + +/** + * @author Alexander M. Turek + * @author Greg Roach + * + * @internal + */ +class Php82 +{ + /** + * Determines if a string matches the ODBC quoting rules. + * + * A valid quoted string begins with a '{', ends with a '}', and has no '}' + * inside of the string that aren't repeated (as to be escaped). + * + * These rules are what .NET also follows. + * + * @see https://github.com/php/php-src/blob/838f6bffff6363a204a2597cbfbaad1d7ee3f2b6/main/php_odbc_utils.c#L31-L57 + */ + public static function odbc_connection_string_is_quoted(string $str): bool + { + if ('' === $str || '{' !== $str[0]) { + return false; + } + + /* Check for } that aren't doubled up or at the end of the string */ + $length = \strlen($str) - 1; + for ($i = 0; $i < $length; ++$i) { + if ('}' !== $str[$i]) { + continue; + } + + if ('}' !== $str[++$i]) { + return $i === $length; + } + } + + return true; + } + + /** + * Determines if a value for a connection string should be quoted. + * + * The ODBC specification mentions: + * "Because of connection string and initialization file grammar, keywords and + * attribute values that contain the characters []{}(),;?*=!@ not enclosed + * with braces should be avoided." + * + * Note that it assumes that the string is *not* already quoted. You should + * check beforehand. + * + * @see https://github.com/php/php-src/blob/838f6bffff6363a204a2597cbfbaad1d7ee3f2b6/main/php_odbc_utils.c#L59-L73 + */ + public static function odbc_connection_string_should_quote(string $str): bool + { + return false !== strpbrk($str, '[]{}(),;?*=!@'); + } + + public static function odbc_connection_string_quote(string $str): string + { + return '{'.str_replace('}', '}}', $str).'}'; + } + + /** + * Implementation closely based on the original C code - including the GOTOs + * and pointer-style string access. + * + * @see https://github.com/php/php-src/blob/master/Zend/zend_ini.c + */ + public static function ini_parse_quantity(string $value): int + { + // Avoid dependency on ctype_space() + $ctype_space = " \t\v\r\n\f"; + + $str = 0; + $str_end = \strlen($value); + $digits = $str; + $overflow = false; + + /* Ignore leading whitespace, but keep it for error messages. */ + while ($digits < $str_end && false !== strpos($ctype_space, $value[$digits])) { + ++$digits; + } + + /* Ignore trailing whitespace, but keep it for error messages. */ + while ($digits < $str_end && false !== strpos($ctype_space, $value[$str_end - 1])) { + --$str_end; + } + + if ($digits === $str_end) { + return 0; + } + + $is_negative = false; + + if ('+' === $value[$digits]) { + ++$digits; + } elseif ('-' === $value[$digits]) { + $is_negative = true; + ++$digits; + } + + if ($value[$digits] < '0' || $value[$digits] > 9) { + $message = sprintf( + 'Invalid quantity "%s": no valid leading digits, interpreting as "0" for backwards compatibility', + self::escapeString($value) + ); + + trigger_error($message, \E_USER_WARNING); + + return 0; + } + + $base = 10; + $allowed_digits = '0123456789'; + + if ('0' === $value[$digits] && ($digits + 1 === $str_end || false === strpos($allowed_digits, $value[$digits + 1]))) { + if ($digits + 1 === $str_end) { + return 0; + } + + switch ($value[$digits + 1]) { + case 'g': + case 'G': + case 'm': + case 'M': + case 'k': + case 'K': + goto evaluation; + case 'x': + case 'X': + $base = 16; + $allowed_digits = '0123456789abcdefABCDEF'; + break; + case 'o': + case 'O': + $base = 8; + $allowed_digits = '01234567'; + break; + case 'b': + case 'B': + $base = 2; + $allowed_digits = '01'; + break; + default: + $message = sprintf( + 'Invalid prefix "0%s", interpreting as "0" for backwards compatibility', + $value[$digits + 1] + ); + trigger_error($message, \E_USER_WARNING); + + return 0; + } + + $digits += 2; + if ($digits === $str_end) { + $message = sprintf( + 'Invalid quantity "%s": no digits after base prefix, interpreting as "0" for backwards compatibility', + self::escapeString($value) + ); + trigger_error($message, \E_USER_WARNING); + + return 0; + } + + $digits_consumed = $digits; + /* Ignore leading whitespace. */ + while ($digits_consumed < $str_end && false !== strpos($ctype_space, $value[$digits_consumed])) { + ++$digits_consumed; + } + if ($digits_consumed !== $str_end && ('+' === $value[$digits_consumed] || '-' === $value[$digits_consumed])) { + ++$digits_consumed; + } + + if ('0' === $value[$digits_consumed]) { + /* Value is just 0 */ + if ($digits_consumed + 1 === $str_end) { + goto evaluation; + } + switch ($value[$digits_consumed + 1]) { + case 'x': + case 'X': + case 'o': + case 'O': + case 'b': + case 'B': + $digits_consumed += 2; + break; + } + } + + if ($digits !== $digits_consumed) { + $message = sprintf( + 'Invalid quantity "%s": no digits after base prefix, interpreting as "0" for backwards compatibility', + self::escapeString($value) + ); + trigger_error($message, \E_USER_WARNING); + + return 0; + } + } + + evaluation: + + if (10 === $base && '0' === $value[$digits]) { + $base = 8; + $allowed_digits = '01234567'; + } + + while ($digits < $str_end && ' ' === $value[$digits]) { + ++$digits; + } + + if ($digits < $str_end && '+' === $value[$digits]) { + ++$digits; + } elseif ($digits < $str_end && '-' === $value[$digits]) { + $is_negative = true; + $overflow = true; + ++$digits; + } + + $digits_end = $digits; + + while ($digits_end < $str_end && false !== strpos($allowed_digits, $value[$digits_end])) { + ++$digits_end; + } + + $retval = base_convert(substr($value, $digits, $digits_end - $digits), $base, 10); + + if ($is_negative && '0' === $retval) { + $is_negative = false; + $overflow = false; + } + + // Check for overflow - remember that -PHP_INT_MIN = 1 + PHP_INT_MAX + if ($is_negative) { + $signed_max = strtr((string) \PHP_INT_MIN, ['-' => '']); + } else { + $signed_max = (string) \PHP_INT_MAX; + } + + $max_length = max(\strlen($retval), \strlen($signed_max)); + + $tmp1 = str_pad($retval, $max_length, '0', \STR_PAD_LEFT); + $tmp2 = str_pad($signed_max, $max_length, '0', \STR_PAD_LEFT); + + if ($tmp1 > $tmp2) { + $retval = -1; + $overflow = true; + } elseif ($is_negative) { + $retval = '-'.$retval; + } + + $retval = (int) $retval; + + if ($digits_end === $digits) { + $message = sprintf( + 'Invalid quantity "%s": no valid leading digits, interpreting as "0" for backwards compatibility', + self::escapeString($value) + ); + trigger_error($message, \E_USER_WARNING); + + return 0; + } + + /* Allow for whitespace between integer portion and any suffix character */ + while ($digits_end < $str_end && false !== strpos($ctype_space, $value[$digits_end])) { + ++$digits_end; + } + + /* No exponent suffix. */ + if ($digits_end === $str_end) { + goto end; + } + + switch ($value[$str_end - 1]) { + case 'g': + case 'G': + $shift = 30; + break; + case 'm': + case 'M': + $shift = 20; + break; + case 'k': + case 'K': + $shift = 10; + break; + default: + /* Unknown suffix */ + $invalid = self::escapeString($value); + $interpreted = self::escapeString(substr($value, $str, $digits_end - $str)); + $chr = self::escapeString($value[$str_end - 1]); + + $message = sprintf( + 'Invalid quantity "%s": unknown multiplier "%s", interpreting as "%s" for backwards compatibility', + $invalid, + $chr, + $interpreted + ); + + trigger_error($message, \E_USER_WARNING); + + return $retval; + } + + $factor = 1 << $shift; + + if (!$overflow) { + if ($retval > 0) { + $overflow = $retval > \PHP_INT_MAX / $factor; + } else { + $overflow = $retval < \PHP_INT_MIN / $factor; + } + } + + if (\is_float($retval * $factor)) { + $overflow = true; + $retval <<= $shift; + } else { + $retval *= $factor; + } + + if ($digits_end !== $str_end - 1) { + /* More than one character in suffix */ + $message = sprintf( + 'Invalid quantity "%s", interpreting as "%s%s" for backwards compatibility', + self::escapeString($value), + self::escapeString(substr($value, $str, $digits_end - $str)), + self::escapeString($value[$str_end - 1]) + ); + trigger_error($message, \E_USER_WARNING); + + return $retval; + } + + end: + + if ($overflow) { + /* Not specifying the resulting value here because the caller may make + * additional conversions. Not specifying the allowed range + * because the caller may do narrower range checks. */ + $message = sprintf( + 'Invalid quantity "%s": value is out of range, using overflow result for backwards compatibility', + self::escapeString($value) + ); + trigger_error($message, \E_USER_WARNING); + } + + return $retval; + } + + /** + * Escape the string to avoid null bytes and to make non-printable chars visible. + */ + private static function escapeString(string $string): string + { + $escaped = ''; + + for ($n = 0, $len = \strlen($string); $n < $len; ++$n) { + $c = \ord($string[$n]); + + if ($c < 32 || '\\' === $string[$n] || $c > 126) { + switch ($string[$n]) { + case "\n": $escaped .= '\\n'; break; + case "\r": $escaped .= '\\r'; break; + case "\t": $escaped .= '\\t'; break; + case "\f": $escaped .= '\\f'; break; + case "\v": $escaped .= '\\v'; break; + case '\\': $escaped .= '\\\\'; break; + case "\x1B": $escaped .= '\\e'; break; + default: + $escaped .= '\\x'.strtoupper(sprintf('%02x', $c)); + } + } else { + $escaped .= $string[$n]; + } + } + + return $escaped; + } +} diff --git a/3rdparty/symfony/polyfill-php82/Random/Engine/Secure.php b/3rdparty/symfony/polyfill-php82/Random/Engine/Secure.php new file mode 100644 index 00000000..5565386c --- /dev/null +++ b/3rdparty/symfony/polyfill-php82/Random/Engine/Secure.php @@ -0,0 +1,50 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Polyfill\Php82\Random\Engine; + +use Random\RandomException; +use Symfony\Polyfill\Php82\NoDynamicProperties; + +/** + * @author Tim Düsterhus + * @author Anton Smirnov + * + * @internal + */ +class Secure +{ + use NoDynamicProperties; + + public function generate(): string + { + try { + return random_bytes(\PHP_INT_SIZE); + } catch (\Exception $e) { + throw new RandomException($e->getMessage(), $e->getCode(), $e->getPrevious()); + } + } + + public function __sleep(): array + { + throw new \Exception("Serialization of 'Random\Engine\Secure' is not allowed"); + } + + public function __wakeup(): void + { + throw new \Exception("Unserialization of 'Random\Engine\Secure' is not allowed"); + } + + public function __clone() + { + throw new \Error('Trying to clone an uncloneable object of class Random\Engine\Secure'); + } +} diff --git a/3rdparty/symfony/polyfill-php82/Resources/stubs/AllowDynamicProperties.php b/3rdparty/symfony/polyfill-php82/Resources/stubs/AllowDynamicProperties.php new file mode 100644 index 00000000..d216e0ad --- /dev/null +++ b/3rdparty/symfony/polyfill-php82/Resources/stubs/AllowDynamicProperties.php @@ -0,0 +1,20 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +if (\PHP_VERSION_ID < 80200) { + #[Attribute(Attribute::TARGET_CLASS)] + final class AllowDynamicProperties + { + public function __construct() + { + } + } +} diff --git a/3rdparty/symfony/polyfill-php82/Resources/stubs/Random/BrokenRandomEngineError.php b/3rdparty/symfony/polyfill-php82/Resources/stubs/Random/BrokenRandomEngineError.php new file mode 100644 index 00000000..971ed570 --- /dev/null +++ b/3rdparty/symfony/polyfill-php82/Resources/stubs/Random/BrokenRandomEngineError.php @@ -0,0 +1,18 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Random; + +if (\PHP_VERSION_ID < 80200) { + class BrokenRandomEngineError extends RandomError + { + } +} diff --git a/3rdparty/symfony/polyfill-php82/Resources/stubs/Random/CryptoSafeEngine.php b/3rdparty/symfony/polyfill-php82/Resources/stubs/Random/CryptoSafeEngine.php new file mode 100644 index 00000000..fb32496f --- /dev/null +++ b/3rdparty/symfony/polyfill-php82/Resources/stubs/Random/CryptoSafeEngine.php @@ -0,0 +1,18 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Random; + +if (\PHP_VERSION_ID < 80200) { + interface CryptoSafeEngine extends Engine + { + } +} diff --git a/3rdparty/symfony/polyfill-php82/Resources/stubs/Random/Engine.php b/3rdparty/symfony/polyfill-php82/Resources/stubs/Random/Engine.php new file mode 100644 index 00000000..4fc78c8f --- /dev/null +++ b/3rdparty/symfony/polyfill-php82/Resources/stubs/Random/Engine.php @@ -0,0 +1,19 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Random; + +if (\PHP_VERSION_ID < 80200) { + interface Engine + { + public function generate(): string; + } +} diff --git a/3rdparty/symfony/polyfill-php82/Resources/stubs/Random/Engine/Secure.php b/3rdparty/symfony/polyfill-php82/Resources/stubs/Random/Engine/Secure.php new file mode 100644 index 00000000..e779b544 --- /dev/null +++ b/3rdparty/symfony/polyfill-php82/Resources/stubs/Random/Engine/Secure.php @@ -0,0 +1,20 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Random\Engine; + +use Symfony\Polyfill\Php82 as p; + +if (\PHP_VERSION_ID < 80200) { + final class Secure extends p\Random\Engine\Secure implements \Random\CryptoSafeEngine + { + } +} diff --git a/3rdparty/symfony/polyfill-php82/Resources/stubs/Random/RandomError.php b/3rdparty/symfony/polyfill-php82/Resources/stubs/Random/RandomError.php new file mode 100644 index 00000000..bf5e89e0 --- /dev/null +++ b/3rdparty/symfony/polyfill-php82/Resources/stubs/Random/RandomError.php @@ -0,0 +1,21 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Random; + +use Symfony\Polyfill\Php82\NoDynamicProperties; + +if (\PHP_VERSION_ID < 80200) { + class RandomError extends \Error + { + use NoDynamicProperties; + } +} diff --git a/3rdparty/symfony/polyfill-php82/Resources/stubs/Random/RandomException.php b/3rdparty/symfony/polyfill-php82/Resources/stubs/Random/RandomException.php new file mode 100644 index 00000000..3b9aae14 --- /dev/null +++ b/3rdparty/symfony/polyfill-php82/Resources/stubs/Random/RandomException.php @@ -0,0 +1,21 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Random; + +use Symfony\Polyfill\Php82\NoDynamicProperties; + +if (\PHP_VERSION_ID < 80200) { + class RandomException extends \Exception + { + use NoDynamicProperties; + } +} diff --git a/3rdparty/symfony/polyfill-php82/Resources/stubs/SensitiveParameter.php b/3rdparty/symfony/polyfill-php82/Resources/stubs/SensitiveParameter.php new file mode 100644 index 00000000..aea4dfbd --- /dev/null +++ b/3rdparty/symfony/polyfill-php82/Resources/stubs/SensitiveParameter.php @@ -0,0 +1,20 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +if (\PHP_VERSION_ID < 80200) { + #[Attribute(Attribute::TARGET_PARAMETER)] + final class SensitiveParameter + { + public function __construct() + { + } + } +} diff --git a/3rdparty/symfony/polyfill-php82/Resources/stubs/SensitiveParameterValue.php b/3rdparty/symfony/polyfill-php82/Resources/stubs/SensitiveParameterValue.php new file mode 100644 index 00000000..8349170b --- /dev/null +++ b/3rdparty/symfony/polyfill-php82/Resources/stubs/SensitiveParameterValue.php @@ -0,0 +1,16 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +if (\PHP_VERSION_ID < 80200) { + final class SensitiveParameterValue extends Symfony\Polyfill\Php82\SensitiveParameterValue + { + } +} diff --git a/3rdparty/symfony/polyfill-php82/SensitiveParameterValue.php b/3rdparty/symfony/polyfill-php82/SensitiveParameterValue.php new file mode 100644 index 00000000..944c0a65 --- /dev/null +++ b/3rdparty/symfony/polyfill-php82/SensitiveParameterValue.php @@ -0,0 +1,47 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Polyfill\Php82; + +/** + * @author Tim Düsterhus + * + * @internal + */ +class SensitiveParameterValue +{ + private $value; + + public function __construct($value) + { + $this->value = $value; + } + + public function getValue() + { + return $this->value; + } + + public function __debugInfo(): array + { + return []; + } + + public function __sleep(): array + { + throw new \Exception("Serialization of 'SensitiveParameterValue' is not allowed"); + } + + public function __wakeup(): void + { + throw new \Exception("Unserialization of 'SensitiveParameterValue' is not allowed"); + } +} diff --git a/3rdparty/symfony/polyfill-php82/bootstrap.php b/3rdparty/symfony/polyfill-php82/bootstrap.php new file mode 100644 index 00000000..399504dc --- /dev/null +++ b/3rdparty/symfony/polyfill-php82/bootstrap.php @@ -0,0 +1,34 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +use Symfony\Polyfill\Php82 as p; + +if (\PHP_VERSION_ID >= 80200) { + return; +} + +if (extension_loaded('odbc')) { + if (!function_exists('odbc_connection_string_is_quoted')) { + function odbc_connection_string_is_quoted(string $str): bool { return p\Php82::odbc_connection_string_is_quoted($str); } + } + + if (!function_exists('odbc_connection_string_should_quote')) { + function odbc_connection_string_should_quote(string $str): bool { return p\Php82::odbc_connection_string_should_quote($str); } + } + + if (!function_exists('odbc_connection_string_quote')) { + function odbc_connection_string_quote(string $str): string { return p\Php82::odbc_connection_string_quote($str); } + } +} + +if (!function_exists('ini_parse_quantity')) { + function ini_parse_quantity(string $shorthand): int { return p\Php82::ini_parse_quantity($shorthand); } +} diff --git a/3rdparty/symfony/polyfill-php83/LICENSE b/3rdparty/symfony/polyfill-php83/LICENSE new file mode 100644 index 00000000..733c826e --- /dev/null +++ b/3rdparty/symfony/polyfill-php83/LICENSE @@ -0,0 +1,19 @@ +Copyright (c) 2022-present Fabien Potencier + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is furnished +to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +THE SOFTWARE. diff --git a/3rdparty/symfony/polyfill-php83/Php83.php b/3rdparty/symfony/polyfill-php83/Php83.php new file mode 100644 index 00000000..3d94b6c3 --- /dev/null +++ b/3rdparty/symfony/polyfill-php83/Php83.php @@ -0,0 +1,197 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Polyfill\Php83; + +/** + * @author Ion Bazan + * @author Pierre Ambroise + * + * @internal + */ +final class Php83 +{ + private const JSON_MAX_DEPTH = 0x7FFFFFFF; // see https://www.php.net/manual/en/function.json-decode.php + + public static function json_validate(string $json, int $depth = 512, int $flags = 0): bool + { + if (0 !== $flags && \defined('JSON_INVALID_UTF8_IGNORE') && \JSON_INVALID_UTF8_IGNORE !== $flags) { + throw new \ValueError('json_validate(): Argument #3 ($flags) must be a valid flag (allowed flags: JSON_INVALID_UTF8_IGNORE)'); + } + + if ($depth <= 0) { + throw new \ValueError('json_validate(): Argument #2 ($depth) must be greater than 0'); + } + + if ($depth > self::JSON_MAX_DEPTH) { + throw new \ValueError(sprintf('json_validate(): Argument #2 ($depth) must be less than %d', self::JSON_MAX_DEPTH)); + } + + json_decode($json, null, $depth, $flags); + + return \JSON_ERROR_NONE === json_last_error(); + } + + public static function mb_str_pad(string $string, int $length, string $pad_string = ' ', int $pad_type = \STR_PAD_RIGHT, ?string $encoding = null): string + { + if (!\in_array($pad_type, [\STR_PAD_RIGHT, \STR_PAD_LEFT, \STR_PAD_BOTH], true)) { + throw new \ValueError('mb_str_pad(): Argument #4 ($pad_type) must be STR_PAD_LEFT, STR_PAD_RIGHT, or STR_PAD_BOTH'); + } + + if (null === $encoding) { + $encoding = mb_internal_encoding(); + } + + try { + $validEncoding = @mb_check_encoding('', $encoding); + } catch (\ValueError $e) { + throw new \ValueError(sprintf('mb_str_pad(): Argument #5 ($encoding) must be a valid encoding, "%s" given', $encoding)); + } + + // BC for PHP 7.3 and lower + if (!$validEncoding) { + throw new \ValueError(sprintf('mb_str_pad(): Argument #5 ($encoding) must be a valid encoding, "%s" given', $encoding)); + } + + if (mb_strlen($pad_string, $encoding) <= 0) { + throw new \ValueError('mb_str_pad(): Argument #3 ($pad_string) must be a non-empty string'); + } + + $paddingRequired = $length - mb_strlen($string, $encoding); + + if ($paddingRequired < 1) { + return $string; + } + + switch ($pad_type) { + case \STR_PAD_LEFT: + return mb_substr(str_repeat($pad_string, $paddingRequired), 0, $paddingRequired, $encoding).$string; + case \STR_PAD_RIGHT: + return $string.mb_substr(str_repeat($pad_string, $paddingRequired), 0, $paddingRequired, $encoding); + default: + $leftPaddingLength = floor($paddingRequired / 2); + $rightPaddingLength = $paddingRequired - $leftPaddingLength; + + return mb_substr(str_repeat($pad_string, $leftPaddingLength), 0, $leftPaddingLength, $encoding).$string.mb_substr(str_repeat($pad_string, $rightPaddingLength), 0, $rightPaddingLength, $encoding); + } + } + + public static function str_increment(string $string): string + { + if ('' === $string) { + throw new \ValueError('str_increment(): Argument #1 ($string) cannot be empty'); + } + + if (!preg_match('/^[a-zA-Z0-9]+$/', $string)) { + throw new \ValueError('str_increment(): Argument #1 ($string) must be composed only of alphanumeric ASCII characters'); + } + + if (is_numeric($string)) { + $offset = stripos($string, 'e'); + if (false !== $offset) { + $char = $string[$offset]; + ++$char; + $string[$offset] = $char; + ++$string; + + switch ($string[$offset]) { + case 'f': + $string[$offset] = 'e'; + break; + case 'F': + $string[$offset] = 'E'; + break; + case 'g': + $string[$offset] = 'f'; + break; + case 'G': + $string[$offset] = 'F'; + break; + } + + return $string; + } + } + + return ++$string; + } + + public static function str_decrement(string $string): string + { + if ('' === $string) { + throw new \ValueError('str_decrement(): Argument #1 ($string) cannot be empty'); + } + + if (!preg_match('/^[a-zA-Z0-9]+$/', $string)) { + throw new \ValueError('str_decrement(): Argument #1 ($string) must be composed only of alphanumeric ASCII characters'); + } + + if (preg_match('/\A(?:0[aA0]?|[aA])\z/', $string)) { + throw new \ValueError(sprintf('str_decrement(): Argument #1 ($string) "%s" is out of decrement range', $string)); + } + + if (!\in_array(substr($string, -1), ['A', 'a', '0'], true)) { + return implode('', \array_slice(str_split($string), 0, -1)).\chr(\ord(substr($string, -1)) - 1); + } + + $carry = ''; + $decremented = ''; + + for ($i = \strlen($string) - 1; $i >= 0; --$i) { + $char = $string[$i]; + + switch ($char) { + case 'A': + if ('' !== $carry) { + $decremented = $carry.$decremented; + $carry = ''; + } + $carry = 'Z'; + + break; + case 'a': + if ('' !== $carry) { + $decremented = $carry.$decremented; + $carry = ''; + } + $carry = 'z'; + + break; + case '0': + if ('' !== $carry) { + $decremented = $carry.$decremented; + $carry = ''; + } + $carry = '9'; + + break; + case '1': + if ('' !== $carry) { + $decremented = $carry.$decremented; + $carry = ''; + } + + break; + default: + if ('' !== $carry) { + $decremented = $carry.$decremented; + $carry = ''; + } + + if (!\in_array($char, ['A', 'a', '0'], true)) { + $decremented = \chr(\ord($char) - 1).$decremented; + } + } + } + + return $decremented; + } +} diff --git a/3rdparty/symfony/polyfill-php83/Resources/stubs/DateError.php b/3rdparty/symfony/polyfill-php83/Resources/stubs/DateError.php new file mode 100644 index 00000000..6e7ed8c8 --- /dev/null +++ b/3rdparty/symfony/polyfill-php83/Resources/stubs/DateError.php @@ -0,0 +1,16 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +if (\PHP_VERSION_ID < 80300) { + class DateError extends Error + { + } +} diff --git a/3rdparty/symfony/polyfill-php83/Resources/stubs/DateException.php b/3rdparty/symfony/polyfill-php83/Resources/stubs/DateException.php new file mode 100644 index 00000000..041710af --- /dev/null +++ b/3rdparty/symfony/polyfill-php83/Resources/stubs/DateException.php @@ -0,0 +1,16 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +if (\PHP_VERSION_ID < 80300) { + class DateException extends Exception + { + } +} diff --git a/3rdparty/symfony/polyfill-php83/Resources/stubs/DateInvalidOperationException.php b/3rdparty/symfony/polyfill-php83/Resources/stubs/DateInvalidOperationException.php new file mode 100644 index 00000000..e2e9dfc9 --- /dev/null +++ b/3rdparty/symfony/polyfill-php83/Resources/stubs/DateInvalidOperationException.php @@ -0,0 +1,16 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +if (\PHP_VERSION_ID < 80300) { + class DateInvalidOperationException extends DateException + { + } +} diff --git a/3rdparty/symfony/polyfill-php83/Resources/stubs/DateInvalidTimeZoneException.php b/3rdparty/symfony/polyfill-php83/Resources/stubs/DateInvalidTimeZoneException.php new file mode 100644 index 00000000..75bcd267 --- /dev/null +++ b/3rdparty/symfony/polyfill-php83/Resources/stubs/DateInvalidTimeZoneException.php @@ -0,0 +1,16 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +if (\PHP_VERSION_ID < 80300) { + class DateInvalidTimeZoneException extends DateException + { + } +} diff --git a/3rdparty/symfony/polyfill-php83/Resources/stubs/DateMalformedIntervalStringException.php b/3rdparty/symfony/polyfill-php83/Resources/stubs/DateMalformedIntervalStringException.php new file mode 100644 index 00000000..af91b8e4 --- /dev/null +++ b/3rdparty/symfony/polyfill-php83/Resources/stubs/DateMalformedIntervalStringException.php @@ -0,0 +1,16 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +if (\PHP_VERSION_ID < 80300) { + class DateMalformedIntervalStringException extends DateException + { + } +} diff --git a/3rdparty/symfony/polyfill-php83/Resources/stubs/DateMalformedPeriodStringException.php b/3rdparty/symfony/polyfill-php83/Resources/stubs/DateMalformedPeriodStringException.php new file mode 100644 index 00000000..9b6d2764 --- /dev/null +++ b/3rdparty/symfony/polyfill-php83/Resources/stubs/DateMalformedPeriodStringException.php @@ -0,0 +1,16 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +if (\PHP_VERSION_ID < 80300) { + class DateMalformedPeriodStringException extends DateException + { + } +} diff --git a/3rdparty/symfony/polyfill-php83/Resources/stubs/DateMalformedStringException.php b/3rdparty/symfony/polyfill-php83/Resources/stubs/DateMalformedStringException.php new file mode 100644 index 00000000..7ad04849 --- /dev/null +++ b/3rdparty/symfony/polyfill-php83/Resources/stubs/DateMalformedStringException.php @@ -0,0 +1,16 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +if (\PHP_VERSION_ID < 80300) { + class DateMalformedStringException extends DateException + { + } +} diff --git a/3rdparty/symfony/polyfill-php83/Resources/stubs/DateObjectError.php b/3rdparty/symfony/polyfill-php83/Resources/stubs/DateObjectError.php new file mode 100644 index 00000000..11f0edc6 --- /dev/null +++ b/3rdparty/symfony/polyfill-php83/Resources/stubs/DateObjectError.php @@ -0,0 +1,16 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +if (\PHP_VERSION_ID < 80300) { + class DateObjectError extends DateError + { + } +} diff --git a/3rdparty/symfony/polyfill-php83/Resources/stubs/DateRangeError.php b/3rdparty/symfony/polyfill-php83/Resources/stubs/DateRangeError.php new file mode 100644 index 00000000..98e67036 --- /dev/null +++ b/3rdparty/symfony/polyfill-php83/Resources/stubs/DateRangeError.php @@ -0,0 +1,16 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +if (\PHP_VERSION_ID < 80300) { + class DateRangeError extends DateError + { + } +} diff --git a/3rdparty/symfony/polyfill-php83/Resources/stubs/Override.php b/3rdparty/symfony/polyfill-php83/Resources/stubs/Override.php new file mode 100644 index 00000000..d3e6b3e1 --- /dev/null +++ b/3rdparty/symfony/polyfill-php83/Resources/stubs/Override.php @@ -0,0 +1,20 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +if (\PHP_VERSION_ID < 80300) { + #[Attribute(Attribute::TARGET_METHOD)] + final class Override + { + public function __construct() + { + } + } +} diff --git a/3rdparty/symfony/polyfill-php83/Resources/stubs/SQLite3Exception.php b/3rdparty/symfony/polyfill-php83/Resources/stubs/SQLite3Exception.php new file mode 100644 index 00000000..ecb7c98e --- /dev/null +++ b/3rdparty/symfony/polyfill-php83/Resources/stubs/SQLite3Exception.php @@ -0,0 +1,16 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +if (\PHP_VERSION_ID < 80300) { + class SQLite3Exception extends Exception + { + } +} diff --git a/3rdparty/symfony/polyfill-php83/bootstrap.php b/3rdparty/symfony/polyfill-php83/bootstrap.php new file mode 100644 index 00000000..a92799cb --- /dev/null +++ b/3rdparty/symfony/polyfill-php83/bootstrap.php @@ -0,0 +1,50 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +use Symfony\Polyfill\Php83 as p; + +if (\PHP_VERSION_ID >= 80300) { + return; +} + +if (!function_exists('json_validate')) { + function json_validate(string $json, int $depth = 512, int $flags = 0): bool { return p\Php83::json_validate($json, $depth, $flags); } +} + +if (extension_loaded('mbstring')) { + if (!function_exists('mb_str_pad')) { + function mb_str_pad(string $string, int $length, string $pad_string = ' ', int $pad_type = STR_PAD_RIGHT, ?string $encoding = null): string { return p\Php83::mb_str_pad($string, $length, $pad_string, $pad_type, $encoding); } + } +} + +if (!function_exists('stream_context_set_options')) { + function stream_context_set_options($context, array $options): bool { return stream_context_set_option($context, $options); } +} + +if (!function_exists('str_increment')) { + function str_increment(string $string): string { return p\Php83::str_increment($string); } +} + +if (!function_exists('str_decrement')) { + function str_decrement(string $string): string { return p\Php83::str_decrement($string); } +} + +if (\PHP_VERSION_ID >= 80100) { + return require __DIR__.'/bootstrap81.php'; +} + +if (!function_exists('ldap_exop_sync') && function_exists('ldap_exop')) { + function ldap_exop_sync($ldap, string $request_oid, ?string $request_data = null, ?array $controls = null, &$response_data = null, &$response_oid = null): bool { return ldap_exop($ldap, $request_oid, $request_data, $controls, $response_data, $response_oid); } +} + +if (!function_exists('ldap_connect_wallet') && function_exists('ldap_connect')) { + function ldap_connect_wallet(?string $uri, string $wallet, string $password, int $auth_mode = \GSLC_SSL_NO_AUTH) { return ldap_connect($uri, $wallet, $password, $auth_mode); } +} diff --git a/3rdparty/symfony/polyfill-php83/bootstrap81.php b/3rdparty/symfony/polyfill-php83/bootstrap81.php new file mode 100644 index 00000000..68395b43 --- /dev/null +++ b/3rdparty/symfony/polyfill-php83/bootstrap81.php @@ -0,0 +1,22 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +if (\PHP_VERSION_ID >= 80300) { + return; +} + +if (!function_exists('ldap_exop_sync') && function_exists('ldap_exop')) { + function ldap_exop_sync(\LDAP\Connection $ldap, string $request_oid, ?string $request_data = null, ?array $controls = null, &$response_data = null, &$response_oid = null): bool { return ldap_exop($ldap, $request_oid, $request_data, $controls, $response_data, $response_oid); } +} + +if (!function_exists('ldap_connect_wallet') && function_exists('ldap_connect')) { + function ldap_connect_wallet(?string $uri, string $wallet, #[\SensitiveParameter] string $password, int $auth_mode = \GSLC_SSL_NO_AUTH): \LDAP\Connection|false { return ldap_connect($uri, $wallet, $password, $auth_mode); } +} diff --git a/3rdparty/symfony/polyfill-php84/LICENSE b/3rdparty/symfony/polyfill-php84/LICENSE new file mode 100644 index 00000000..e374a5c8 --- /dev/null +++ b/3rdparty/symfony/polyfill-php84/LICENSE @@ -0,0 +1,19 @@ +Copyright (c) 2024-present Fabien Potencier + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is furnished +to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +THE SOFTWARE. diff --git a/3rdparty/symfony/polyfill-php84/Php84.php b/3rdparty/symfony/polyfill-php84/Php84.php new file mode 100644 index 00000000..17668e6b --- /dev/null +++ b/3rdparty/symfony/polyfill-php84/Php84.php @@ -0,0 +1,177 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Polyfill\Php84; + +/** + * @author Ayesh Karunaratne + * @author Pierre Ambroise + * + * @internal + */ +final class Php84 +{ + public static function mb_ucfirst(string $string, ?string $encoding = null): string + { + if (null === $encoding) { + $encoding = mb_internal_encoding(); + } + + try { + $validEncoding = @mb_check_encoding('', $encoding); + } catch (\ValueError $e) { + throw new \ValueError(sprintf('mb_ucfirst(): Argument #2 ($encoding) must be a valid encoding, "%s" given', $encoding)); + } + + // BC for PHP 7.3 and lower + if (!$validEncoding) { + throw new \ValueError(sprintf('mb_ucfirst(): Argument #2 ($encoding) must be a valid encoding, "%s" given', $encoding)); + } + + $firstChar = mb_substr($string, 0, 1, $encoding); + $firstChar = mb_convert_case($firstChar, \MB_CASE_TITLE, $encoding); + + return $firstChar.mb_substr($string, 1, null, $encoding); + } + + public static function mb_lcfirst(string $string, ?string $encoding = null): string + { + if (null === $encoding) { + $encoding = mb_internal_encoding(); + } + + try { + $validEncoding = @mb_check_encoding('', $encoding); + } catch (\ValueError $e) { + throw new \ValueError(sprintf('mb_lcfirst(): Argument #2 ($encoding) must be a valid encoding, "%s" given', $encoding)); + } + + // BC for PHP 7.3 and lower + if (!$validEncoding) { + throw new \ValueError(sprintf('mb_lcfirst(): Argument #2 ($encoding) must be a valid encoding, "%s" given', $encoding)); + } + + $firstChar = mb_substr($string, 0, 1, $encoding); + $firstChar = mb_convert_case($firstChar, \MB_CASE_LOWER, $encoding); + + return $firstChar.mb_substr($string, 1, null, $encoding); + } + + public static function array_find(array $array, callable $callback) + { + foreach ($array as $key => $value) { + if ($callback($value, $key)) { + return $value; + } + } + + return null; + } + + public static function array_find_key(array $array, callable $callback) + { + foreach ($array as $key => $value) { + if ($callback($value, $key)) { + return $key; + } + } + + return null; + } + + public static function array_any(array $array, callable $callback): bool + { + foreach ($array as $key => $value) { + if ($callback($value, $key)) { + return true; + } + } + + return false; + } + + public static function array_all(array $array, callable $callback): bool + { + foreach ($array as $key => $value) { + if (!$callback($value, $key)) { + return false; + } + } + + return true; + } + + public static function fpow(float $num, float $exponent): float + { + return $num ** $exponent; + } + + public static function mb_trim(string $string, ?string $characters = null, ?string $encoding = null): string + { + return self::mb_internal_trim('{^[%s]+|[%1$s]+$}Du', $string, $characters, $encoding, __FUNCTION__); + } + + public static function mb_ltrim(string $string, ?string $characters = null, ?string $encoding = null): string + { + return self::mb_internal_trim('{^[%s]+}Du', $string, $characters, $encoding, __FUNCTION__); + } + + public static function mb_rtrim(string $string, ?string $characters = null, ?string $encoding = null): string + { + return self::mb_internal_trim('{[%s]+$}Du', $string, $characters, $encoding, __FUNCTION__); + } + + private static function mb_internal_trim(string $regex, string $string, ?string $characters, ?string $encoding, string $function): string + { + if (null === $encoding) { + $encoding = mb_internal_encoding(); + } + + try { + $validEncoding = @mb_check_encoding('', $encoding); + } catch (\ValueError $e) { + throw new \ValueError(sprintf('%s(): Argument #3 ($encoding) must be a valid encoding, "%s" given', $function, $encoding)); + } + + // BC for PHP 7.3 and lower + if (!$validEncoding) { + throw new \ValueError(sprintf('%s(): Argument #3 ($encoding) must be a valid encoding, "%s" given', $function, $encoding)); + } + + if ('' === $characters) { + return null === $encoding ? $string : mb_convert_encoding($string, $encoding); + } + + if ('UTF-8' === $encoding || \in_array(strtolower($encoding), ['utf-8', 'utf8'], true)) { + $encoding = 'UTF-8'; + } + + $string = mb_convert_encoding($string, 'UTF-8', $encoding); + + if (null !== $characters) { + $characters = mb_convert_encoding($characters, 'UTF-8', $encoding); + } + + if (null === $characters) { + $characters = "\\0 \f\n\r\t\v\u{00A0}\u{1680}\u{2000}\u{2001}\u{2002}\u{2003}\u{2004}\u{2005}\u{2006}\u{2007}\u{2008}\u{2009}\u{200A}\u{2028}\u{2029}\u{202F}\u{205F}\u{3000}\u{0085}\u{180E}"; + } else { + $characters = preg_quote($characters); + } + + $string = preg_replace(sprintf($regex, $characters), '', $string); + + if ('UTF-8' === $encoding) { + return $string; + } + + return mb_convert_encoding($string, $encoding, 'UTF-8'); + } +} diff --git a/3rdparty/symfony/polyfill-php84/Resources/stubs/Deprecated.php b/3rdparty/symfony/polyfill-php84/Resources/stubs/Deprecated.php new file mode 100644 index 00000000..f3e6a4f8 --- /dev/null +++ b/3rdparty/symfony/polyfill-php84/Resources/stubs/Deprecated.php @@ -0,0 +1,25 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +if (\PHP_VERSION_ID < 80400) { + #[Attribute(Attribute::TARGET_METHOD | Attribute::TARGET_FUNCTION | Attribute::TARGET_CLASS_CONSTANT)] + final class Deprecated + { + public readonly ?string $message; + public readonly ?string $since; + + public function __construct(?string $message = null, ?string $since = null) + { + $this->message = $message; + $this->since = $since; + } + } +} diff --git a/3rdparty/symfony/polyfill-php84/bootstrap.php b/3rdparty/symfony/polyfill-php84/bootstrap.php new file mode 100644 index 00000000..2a972072 --- /dev/null +++ b/3rdparty/symfony/polyfill-php84/bootstrap.php @@ -0,0 +1,68 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +use Symfony\Polyfill\Php84 as p; + +if (\PHP_VERSION_ID >= 80400) { + return; +} + +if (defined('CURL_VERSION_HTTP3') || PHP_VERSION_ID < 80200 && function_exists('curl_version') && curl_version()['version'] >= 0x074200) { // libcurl >= 7.66.0 + if (!defined('CURL_HTTP_VERSION_3')) { + define('CURL_HTTP_VERSION_3', 30); + } + + if (!defined('CURL_HTTP_VERSION_3ONLY') && defined('CURLOPT_SSH_HOST_PUBLIC_KEY_SHA256')) { // libcurl >= 7.80.0 (7.88 would be better but is slow to check) + define('CURL_HTTP_VERSION_3ONLY', 31); + } +} + +if (!function_exists('array_find')) { + function array_find(array $array, callable $callback) { return p\Php84::array_find($array, $callback); } +} + +if (!function_exists('array_find_key')) { + function array_find_key(array $array, callable $callback) { return p\Php84::array_find_key($array, $callback); } +} + +if (!function_exists('array_any')) { + function array_any(array $array, callable $callback): bool { return p\Php84::array_any($array, $callback); } +} + +if (!function_exists('array_all')) { + function array_all(array $array, callable $callback): bool { return p\Php84::array_all($array, $callback); } +} + +if (!function_exists('fpow')) { + function fpow(float $num, float $exponent): float { return p\Php84::fpow($num, $exponent); } +} + +if (extension_loaded('mbstring')) { + if (!function_exists('mb_ucfirst')) { + function mb_ucfirst(string $string, ?string $encoding = null): string { return p\Php84::mb_ucfirst($string, $encoding); } + } + + if (!function_exists('mb_lcfirst')) { + function mb_lcfirst(string $string, ?string $encoding = null): string { return p\Php84::mb_lcfirst($string, $encoding); } + } + + if (!function_exists('mb_trim')) { + function mb_trim(string $string, ?string $characters = null, ?string $encoding = null): string { return p\Php84::mb_trim($string, $characters, $encoding); } + } + + if (!function_exists('mb_ltrim')) { + function mb_ltrim(string $string, ?string $characters = null, ?string $encoding = null): string { return p\Php84::mb_ltrim($string, $characters, $encoding); } + } + + if (!function_exists('mb_rtrim')) { + function mb_rtrim(string $string, ?string $characters = null, ?string $encoding = null): string { return p\Php84::mb_rtrim($string, $characters, $encoding); } + } +} diff --git a/3rdparty/symfony/polyfill-uuid/LICENSE b/3rdparty/symfony/polyfill-uuid/LICENSE new file mode 100644 index 00000000..7536caea --- /dev/null +++ b/3rdparty/symfony/polyfill-uuid/LICENSE @@ -0,0 +1,19 @@ +Copyright (c) 2018-present Fabien Potencier + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is furnished +to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +THE SOFTWARE. diff --git a/3rdparty/symfony/polyfill-uuid/Uuid.php b/3rdparty/symfony/polyfill-uuid/Uuid.php new file mode 100644 index 00000000..584095b1 --- /dev/null +++ b/3rdparty/symfony/polyfill-uuid/Uuid.php @@ -0,0 +1,531 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Polyfill\Uuid; + +/** + * @internal + * + * @author Grégoire Pineau + */ +final class Uuid +{ + public const UUID_VARIANT_NCS = 0; + public const UUID_VARIANT_DCE = 1; + public const UUID_VARIANT_MICROSOFT = 2; + public const UUID_VARIANT_OTHER = 3; + public const UUID_TYPE_DEFAULT = 0; + public const UUID_TYPE_TIME = 1; + public const UUID_TYPE_MD5 = 3; + public const UUID_TYPE_DCE = 4; // Deprecated alias + public const UUID_TYPE_NAME = 1; // Deprecated alias + public const UUID_TYPE_RANDOM = 4; + public const UUID_TYPE_SHA1 = 5; + public const UUID_TYPE_NULL = -1; + public const UUID_TYPE_INVALID = -42; + + // https://tools.ietf.org/html/rfc4122#section-4.1.4 + // 0x01b21dd213814000 is the number of 100-ns intervals between the + // UUID epoch 1582-10-15 00:00:00 and the Unix epoch 1970-01-01 00:00:00. + public const TIME_OFFSET_INT = 0x01B21DD213814000; + public const TIME_OFFSET_BIN = "\x01\xb2\x1d\xd2\x13\x81\x40\x00"; + public const TIME_OFFSET_COM = "\xfe\x4d\xe2\x2d\xec\x7e\xc0\x00"; + + public static function uuid_create($uuid_type = \UUID_TYPE_DEFAULT) + { + if (!is_numeric($uuid_type) && null !== $uuid_type) { + trigger_error(sprintf('uuid_create() expects parameter 1 to be int, %s given', \gettype($uuid_type)), \E_USER_WARNING); + + return null; + } + + switch ((int) $uuid_type) { + case self::UUID_TYPE_NAME: + case self::UUID_TYPE_TIME: + return self::uuid_generate_time(); + case self::UUID_TYPE_DCE: + case self::UUID_TYPE_RANDOM: + case self::UUID_TYPE_DEFAULT: + return self::uuid_generate_random(); + default: + trigger_error(sprintf("Unknown/invalid UUID type '%d' requested, using default type instead", $uuid_type), \E_USER_WARNING); + + return self::uuid_generate_random(); + } + } + + public static function uuid_generate_md5($uuid_ns, $name) + { + if (!\is_string($uuid_ns = self::toString($uuid_ns))) { + trigger_error(sprintf('uuid_generate_md5() expects parameter 1 to be string, %s given', \gettype($uuid_ns)), \E_USER_WARNING); + + return null; + } + + if (!\is_string($name = self::toString($name))) { + trigger_error(sprintf('uuid_generate_md5() expects parameter 2 to be string, %s given', \gettype($name)), \E_USER_WARNING); + + return null; + } + + if (!self::isValid($uuid_ns)) { + if (80000 > \PHP_VERSION_ID) { + return false; + } + + throw new \ValueError('uuid_generate_md5(): Argument #1 ($uuid_ns) UUID expected'); + } + + $hash = md5(hex2bin(str_replace('-', '', $uuid_ns)).$name); + + return sprintf('%08s-%04s-3%03s-%04x-%012s', + // 32 bits for "time_low" + substr($hash, 0, 8), + // 16 bits for "time_mid" + substr($hash, 8, 4), + // 16 bits for "time_hi_and_version", + // four most significant bits holds version number 3 + substr($hash, 13, 3), + // 16 bits: + // * 8 bits for "clk_seq_hi_res", + // * 8 bits for "clk_seq_low", + hexdec(substr($hash, 16, 4)) & 0x3FFF | 0x8000, + // 48 bits for "node" + substr($hash, 20, 12) + ); + } + + public static function uuid_generate_sha1($uuid_ns, $name) + { + if (!\is_string($uuid_ns = self::toString($uuid_ns))) { + trigger_error(sprintf('uuid_generate_sha1() expects parameter 1 to be string, %s given', \gettype($uuid_ns)), \E_USER_WARNING); + + return null; + } + + if (!\is_string($name = self::toString($name))) { + trigger_error(sprintf('uuid_generate_sha1() expects parameter 2 to be string, %s given', \gettype($name)), \E_USER_WARNING); + + return null; + } + + if (!self::isValid($uuid_ns)) { + if (80000 > \PHP_VERSION_ID) { + return false; + } + + throw new \ValueError('uuid_generate_sha1(): Argument #1 ($uuid_ns) UUID expected'); + } + + $hash = sha1(hex2bin(str_replace('-', '', $uuid_ns)).$name); + + return sprintf('%08s-%04s-5%03s-%04x-%012s', + // 32 bits for "time_low" + substr($hash, 0, 8), + // 16 bits for "time_mid" + substr($hash, 8, 4), + // 16 bits for "time_hi_and_version", + // four most significant bits holds version number 5 + substr($hash, 13, 3), + // 16 bits: + // * 8 bits for "clk_seq_hi_res", + // * 8 bits for "clk_seq_low", + // WARNING: On old libuuid version, there is a bug. 0x0fff is used instead of 0x3fff + // See https://github.com/karelzak/util-linux/commit/d6ddf07d31dfdc894eb8e7e6842aa856342c526e + hexdec(substr($hash, 16, 4)) & 0x3FFF | 0x8000, + // 48 bits for "node" + substr($hash, 20, 12) + ); + } + + public static function uuid_is_valid($uuid) + { + if (!\is_string($uuid = self::toString($uuid))) { + trigger_error(sprintf('uuid_is_valid() expects parameter 1 to be string, %s given', \gettype($uuid)), \E_USER_WARNING); + + return null; + } + + return self::isValid($uuid); + } + + public static function uuid_compare($uuid1, $uuid2) + { + if (!\is_string($uuid1 = self::toString($uuid1))) { + trigger_error(sprintf('uuid_compare() expects parameter 1 to be string, %s given', \gettype($uuid1)), \E_USER_WARNING); + + return null; + } + + if (!\is_string($uuid2 = self::toString($uuid2))) { + trigger_error(sprintf('uuid_compare() expects parameter 2 to be string, %s given', \gettype($uuid2)), \E_USER_WARNING); + + return null; + } + + if (!self::isValid($uuid1)) { + if (80000 > \PHP_VERSION_ID) { + return false; + } + + throw new \ValueError('uuid_compare(): Argument #1 ($uuid1) UUID expected'); + } + + if (!self::isValid($uuid2)) { + if (80000 > \PHP_VERSION_ID) { + return false; + } + + throw new \ValueError('uuid_compare(): Argument #2 ($uuid2) UUID expected'); + } + + return strcasecmp($uuid1, $uuid2); + } + + public static function uuid_is_null($uuid) + { + if (!\is_string($uuid = self::toString($uuid))) { + trigger_error(sprintf('uuid_is_null() expects parameter 1 to be string, %s given', \gettype($uuid)), \E_USER_WARNING); + + return null; + } + if (80000 <= \PHP_VERSION_ID && !self::isValid($uuid)) { + throw new \ValueError('uuid_is_null(): Argument #1 ($uuid) UUID expected'); + } + + return '00000000-0000-0000-0000-000000000000' === $uuid; + } + + public static function uuid_type($uuid) + { + if (!\is_string($uuid = self::toString($uuid))) { + trigger_error(sprintf('uuid_type() expects parameter 1 to be string, %s given', \gettype($uuid)), \E_USER_WARNING); + + return null; + } + + if ('00000000-0000-0000-0000-000000000000' === $uuid) { + return self::UUID_TYPE_NULL; + } + + if (null === $parsed = self::parse($uuid)) { + if (80000 > \PHP_VERSION_ID) { + return false; + } + + throw new \ValueError('uuid_type(): Argument #1 ($uuid) UUID expected'); + } + + return $parsed['version']; + } + + public static function uuid_variant($uuid) + { + if (!\is_string($uuid = self::toString($uuid))) { + trigger_error(sprintf('uuid_variant() expects parameter 1 to be string, %s given', \gettype($uuid)), \E_USER_WARNING); + + return null; + } + + if ('00000000-0000-0000-0000-000000000000' === $uuid) { + return self::UUID_TYPE_NULL; + } + + if (null === $parsed = self::parse($uuid)) { + if (80000 > \PHP_VERSION_ID) { + return false; + } + + throw new \ValueError('uuid_variant(): Argument #1 ($uuid) UUID expected'); + } + + if (($parsed['clock_seq'] & 0x8000) === 0) { + return self::UUID_VARIANT_NCS; + } + if (($parsed['clock_seq'] & 0x4000) === 0) { + return self::UUID_VARIANT_DCE; + } + if (($parsed['clock_seq'] & 0x2000) === 0) { + return self::UUID_VARIANT_MICROSOFT; + } + + return self::UUID_VARIANT_OTHER; + } + + public static function uuid_time($uuid) + { + if (!\is_string($uuid = self::toString($uuid))) { + trigger_error(sprintf('uuid_time() expects parameter 1 to be string, %s given', \gettype($uuid)), \E_USER_WARNING); + + return null; + } + + $parsed = self::parse($uuid); + + if (self::UUID_TYPE_TIME !== ($parsed['version'] ?? null)) { + if (80000 > \PHP_VERSION_ID) { + return false; + } + + throw new \ValueError('uuid_time(): Argument #1 ($uuid) UUID DCE TIME expected'); + } + + if (\PHP_INT_SIZE >= 8) { + return intdiv(hexdec($parsed['time']) - self::TIME_OFFSET_INT, 10000000); + } + + $time = str_pad(hex2bin($parsed['time']), 8, "\0", \STR_PAD_LEFT); + $time = self::binaryAdd($time, self::TIME_OFFSET_COM); + $time[0] = $time[0] & "\x7F"; + + return (int) substr(self::toDecimal($time), 0, -7); + } + + public static function uuid_mac($uuid) + { + if (!\is_string($uuid = self::toString($uuid))) { + trigger_error(sprintf('uuid_mac() expects parameter 1 to be string, %s given', \gettype($uuid)), \E_USER_WARNING); + + return null; + } + + $parsed = self::parse($uuid); + + if (self::UUID_TYPE_TIME !== ($parsed['version'] ?? null)) { + if (80000 > \PHP_VERSION_ID) { + return false; + } + + throw new \ValueError('uuid_mac(): Argument #1 ($uuid) UUID DCE TIME expected'); + } + + return strtr($parsed['node'], 'ABCDEF', 'abcdef'); + } + + public static function uuid_parse($uuid) + { + if (!\is_string($uuid = self::toString($uuid))) { + trigger_error(sprintf('uuid_parse() expects parameter 1 to be string, %s given', \gettype($uuid)), \E_USER_WARNING); + + return null; + } + + if (!self::isValid($uuid)) { + if (80000 > \PHP_VERSION_ID) { + return false; + } + + throw new \ValueError('uuid_parse(): Argument #1 ($uuid) UUID expected'); + } + + return hex2bin(str_replace('-', '', $uuid)); + } + + public static function uuid_unparse($bytes) + { + if (!\is_string($bytes = self::toString($bytes))) { + trigger_error(sprintf('uuid_unparse() expects parameter 1 to be string, %s given', \gettype($bytes)), \E_USER_WARNING); + + return null; + } + + if (16 !== \strlen($bytes)) { + if (80000 > \PHP_VERSION_ID) { + return false; + } + + throw new \ValueError('uuid_unparse(): Argument #1 ($uuid) UUID expected'); + } + + $uuid = bin2hex($bytes); + $uuid = substr_replace($uuid, '-', 8, 0); + $uuid = substr_replace($uuid, '-', 13, 0); + $uuid = substr_replace($uuid, '-', 18, 0); + + return substr_replace($uuid, '-', 23, 0); + } + + private static function uuid_generate_random() + { + $uuid = bin2hex(random_bytes(16)); + + return sprintf('%08s-%04s-4%03s-%04x-%012s', + // 32 bits for "time_low" + substr($uuid, 0, 8), + // 16 bits for "time_mid" + substr($uuid, 8, 4), + // 16 bits for "time_hi_and_version", + // four most significant bits holds version number 4 + substr($uuid, 13, 3), + // 16 bits: + // * 8 bits for "clk_seq_hi_res", + // * 8 bits for "clk_seq_low", + // two most significant bits holds zero and one for variant DCE1.1 + hexdec(substr($uuid, 16, 4)) & 0x3FFF | 0x8000, + // 48 bits for "node" + substr($uuid, 20, 12) + ); + } + + /** + * @see http://tools.ietf.org/html/rfc4122#section-4.2.2 + */ + private static function uuid_generate_time() + { + $time = microtime(false); + $time = substr($time, 11).substr($time, 2, 7); + + if (\PHP_INT_SIZE >= 8) { + $time = str_pad(dechex($time + self::TIME_OFFSET_INT), 16, '0', \STR_PAD_LEFT); + } else { + $time = str_pad(self::toBinary($time), 8, "\0", \STR_PAD_LEFT); + $time = self::binaryAdd($time, self::TIME_OFFSET_BIN); + $time = bin2hex($time); + } + + // https://tools.ietf.org/html/rfc4122#section-4.1.5 + // We are using a random data for the sake of simplicity: since we are + // not able to get a super precise timeOfDay as a unique sequence + $clockSeq = random_int(0, 0x3FFF); + + static $node; + if (null === $node) { + if (\function_exists('apcu_fetch')) { + $node = apcu_fetch('__symfony_uuid_node'); + if (false === $node) { + $node = sprintf('%06x%06x', + random_int(0, 0xFFFFFF) | 0x010000, + random_int(0, 0xFFFFFF) + ); + apcu_store('__symfony_uuid_node', $node); + } + } else { + $node = sprintf('%06x%06x', + random_int(0, 0xFFFFFF) | 0x010000, + random_int(0, 0xFFFFFF) + ); + } + } + + return sprintf('%08s-%04s-1%03s-%04x-%012s', + // 32 bits for "time_low" + substr($time, -8), + + // 16 bits for "time_mid" + substr($time, -12, 4), + + // 16 bits for "time_hi_and_version", + // four most significant bits holds version number 1 + substr($time, -15, 3), + + // 16 bits: + // * 8 bits for "clk_seq_hi_res", + // * 8 bits for "clk_seq_low", + // two most significant bits holds zero and one for variant DCE1.1 + $clockSeq | 0x8000, + + // 48 bits for "node" + $node + ); + } + + private static function isValid($uuid) + { + return (bool) preg_match('{^[0-9a-f]{8}(?:-[0-9a-f]{4}){3}-[0-9a-f]{12}$}Di', $uuid); + } + + private static function parse($uuid) + { + if (!preg_match('{^(?[0-9a-f]{8})-(?[0-9a-f]{4})-(?[0-9a-f])(?[0-9a-f]{3})-(?[0-9a-f]{4})-(?[0-9a-f]{12})$}Di', $uuid, $matches)) { + return null; + } + + return [ + 'time' => '0'.$matches['time_hi'].$matches['time_mid'].$matches['time_low'], + 'version' => hexdec($matches['version']), + 'clock_seq' => hexdec($matches['clock_seq']), + 'node' => $matches['node'], + ]; + } + + private static function toString($v) + { + if (\is_string($v) || null === $v || (\is_object($v) ? method_exists($v, '__toString') : \is_scalar($v))) { + return (string) $v; + } + + return $v; + } + + private static function toBinary($digits) + { + $bytes = ''; + $count = \strlen($digits); + + while ($count) { + $quotient = []; + $remainder = 0; + + for ($i = 0; $i !== $count; ++$i) { + $carry = $digits[$i] + $remainder * 10; + $digit = $carry >> 8; + $remainder = $carry & 0xFF; + + if ($digit || $quotient) { + $quotient[] = $digit; + } + } + + $bytes = \chr($remainder).$bytes; + $count = \count($digits = $quotient); + } + + return $bytes; + } + + private static function toDecimal($bytes) + { + $digits = ''; + $bytes = array_values(unpack('C*', $bytes)); + + while ($count = \count($bytes)) { + $quotient = []; + $remainder = 0; + + for ($i = 0; $i !== $count; ++$i) { + $carry = $bytes[$i] + ($remainder << 8); + $digit = (int) ($carry / 10); + $remainder = $carry % 10; + + if ($digit || $quotient) { + $quotient[] = $digit; + } + } + + $digits = $remainder.$digits; + $bytes = $quotient; + } + + return $digits; + } + + private static function binaryAdd($a, $b) + { + $sum = 0; + for ($i = 7; 0 <= $i; --$i) { + $sum += \ord($a[$i]) + \ord($b[$i]); + $a[$i] = \chr($sum & 0xFF); + $sum >>= 8; + } + + return $a; + } +} diff --git a/3rdparty/symfony/polyfill-uuid/bootstrap.php b/3rdparty/symfony/polyfill-uuid/bootstrap.php new file mode 100644 index 00000000..6d8545b3 --- /dev/null +++ b/3rdparty/symfony/polyfill-uuid/bootstrap.php @@ -0,0 +1,97 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +use Symfony\Polyfill\Uuid as p; + +if (extension_loaded('uuid')) { + return; +} + +if (\PHP_VERSION_ID >= 80000) { + return require __DIR__.'/bootstrap80.php'; +} + +if (!defined('UUID_VARIANT_NCS')) { + define('UUID_VARIANT_NCS', 0); +} +if (!defined('UUID_VARIANT_DCE')) { + define('UUID_VARIANT_DCE', 1); +} +if (!defined('UUID_VARIANT_MICROSOFT')) { + define('UUID_VARIANT_MICROSOFT', 2); +} +if (!defined('UUID_VARIANT_OTHER')) { + define('UUID_VARIANT_OTHER', 3); +} +if (!defined('UUID_TYPE_DEFAULT')) { + define('UUID_TYPE_DEFAULT', 0); +} +if (!defined('UUID_TYPE_TIME')) { + define('UUID_TYPE_TIME', 1); +} +if (!defined('UUID_TYPE_MD5')) { + define('UUID_TYPE_MD5', 3); +} +if (!defined('UUID_TYPE_DCE')) { + define('UUID_TYPE_DCE', 4); // Deprecated alias +} +if (!defined('UUID_TYPE_NAME')) { + define('UUID_TYPE_NAME', 1); // Deprecated alias +} +if (!defined('UUID_TYPE_RANDOM')) { + define('UUID_TYPE_RANDOM', 4); +} +if (!defined('UUID_TYPE_SHA1')) { + define('UUID_TYPE_SHA1', 5); +} +if (!defined('UUID_TYPE_NULL')) { + define('UUID_TYPE_NULL', -1); +} +if (!defined('UUID_TYPE_INVALID')) { + define('UUID_TYPE_INVALID', -42); +} + +if (!function_exists('uuid_create')) { + function uuid_create($uuid_type = \UUID_TYPE_DEFAULT) { return p\Uuid::uuid_create($uuid_type); } +} +if (!function_exists('uuid_generate_md5')) { + function uuid_generate_md5($uuid_ns, $name) { return p\Uuid::uuid_generate_md5($uuid_ns, $name); } +} +if (!function_exists('uuid_generate_sha1')) { + function uuid_generate_sha1($uuid_ns, $name) { return p\Uuid::uuid_generate_sha1($uuid_ns, $name); } +} +if (!function_exists('uuid_is_valid')) { + function uuid_is_valid($uuid) { return p\Uuid::uuid_is_valid($uuid); } +} +if (!function_exists('uuid_compare')) { + function uuid_compare($uuid1, $uuid2) { return p\Uuid::uuid_compare($uuid1, $uuid2); } +} +if (!function_exists('uuid_is_null')) { + function uuid_is_null($uuid) { return p\Uuid::uuid_is_null($uuid); } +} +if (!function_exists('uuid_type')) { + function uuid_type($uuid) { return p\Uuid::uuid_type($uuid); } +} +if (!function_exists('uuid_variant')) { + function uuid_variant($uuid) { return p\Uuid::uuid_variant($uuid); } +} +if (!function_exists('uuid_time')) { + function uuid_time($uuid) { return p\Uuid::uuid_time($uuid); } +} +if (!function_exists('uuid_mac')) { + function uuid_mac($uuid) { return p\Uuid::uuid_mac($uuid); } +} +if (!function_exists('uuid_parse')) { + function uuid_parse($uuid) { return p\Uuid::uuid_parse($uuid); } +} +if (!function_exists('uuid_unparse')) { + function uuid_unparse($uuid) { return p\Uuid::uuid_unparse($uuid); } +} diff --git a/3rdparty/symfony/polyfill-uuid/bootstrap80.php b/3rdparty/symfony/polyfill-uuid/bootstrap80.php new file mode 100644 index 00000000..d6c592fe --- /dev/null +++ b/3rdparty/symfony/polyfill-uuid/bootstrap80.php @@ -0,0 +1,89 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +use Symfony\Polyfill\Uuid as p; + +if (!defined('UUID_VARIANT_NCS')) { + define('UUID_VARIANT_NCS', 0); +} +if (!defined('UUID_VARIANT_DCE')) { + define('UUID_VARIANT_DCE', 1); +} +if (!defined('UUID_VARIANT_MICROSOFT')) { + define('UUID_VARIANT_MICROSOFT', 2); +} +if (!defined('UUID_VARIANT_OTHER')) { + define('UUID_VARIANT_OTHER', 3); +} +if (!defined('UUID_TYPE_DEFAULT')) { + define('UUID_TYPE_DEFAULT', 0); +} +if (!defined('UUID_TYPE_TIME')) { + define('UUID_TYPE_TIME', 1); +} +if (!defined('UUID_TYPE_MD5')) { + define('UUID_TYPE_MD5', 3); +} +if (!defined('UUID_TYPE_DCE')) { + define('UUID_TYPE_DCE', 4); // Deprecated alias +} +if (!defined('UUID_TYPE_NAME')) { + define('UUID_TYPE_NAME', 1); // Deprecated alias +} +if (!defined('UUID_TYPE_RANDOM')) { + define('UUID_TYPE_RANDOM', 4); +} +if (!defined('UUID_TYPE_SHA1')) { + define('UUID_TYPE_SHA1', 5); +} +if (!defined('UUID_TYPE_NULL')) { + define('UUID_TYPE_NULL', -1); +} +if (!defined('UUID_TYPE_INVALID')) { + define('UUID_TYPE_INVALID', -42); +} + +if (!function_exists('uuid_create')) { + function uuid_create(?int $uuid_type = \UUID_TYPE_DEFAULT): string { return p\Uuid::uuid_create((int) $uuid_type); } +} +if (!function_exists('uuid_generate_md5')) { + function uuid_generate_md5(?string $uuid_ns, ?string $name): string { return p\Uuid::uuid_generate_md5((string) $uuid_ns, (string) $name); } +} +if (!function_exists('uuid_generate_sha1')) { + function uuid_generate_sha1(?string $uuid_ns, ?string $name): string { return p\Uuid::uuid_generate_sha1((string) $uuid_ns, (string) $name); } +} +if (!function_exists('uuid_is_valid')) { + function uuid_is_valid(?string $uuid): bool { return p\Uuid::uuid_is_valid((string) $uuid); } +} +if (!function_exists('uuid_compare')) { + function uuid_compare(?string $uuid1, ?string $uuid2): int { return p\Uuid::uuid_compare((string) $uuid1, (string) $uuid2); } +} +if (!function_exists('uuid_is_null')) { + function uuid_is_null(?string $uuid): bool { return p\Uuid::uuid_is_null((string) $uuid); } +} +if (!function_exists('uuid_type')) { + function uuid_type(?string $uuid): int { return p\Uuid::uuid_type((string) $uuid); } +} +if (!function_exists('uuid_variant')) { + function uuid_variant(?string $uuid): int { return p\Uuid::uuid_variant((string) $uuid); } +} +if (!function_exists('uuid_time')) { + function uuid_time(?string $uuid): int { return p\Uuid::uuid_time((string) $uuid); } +} +if (!function_exists('uuid_mac')) { + function uuid_mac(?string $uuid): string { return p\Uuid::uuid_mac((string) $uuid); } +} +if (!function_exists('uuid_parse')) { + function uuid_parse(?string $uuid): string { return p\Uuid::uuid_parse((string) $uuid); } +} +if (!function_exists('uuid_unparse')) { + function uuid_unparse(?string $uuid): string { return p\Uuid::uuid_unparse((string) $uuid); } +} diff --git a/3rdparty/symfony/process/Exception/ExceptionInterface.php b/3rdparty/symfony/process/Exception/ExceptionInterface.php new file mode 100644 index 00000000..bd4a6040 --- /dev/null +++ b/3rdparty/symfony/process/Exception/ExceptionInterface.php @@ -0,0 +1,21 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Component\Process\Exception; + +/** + * Marker Interface for the Process Component. + * + * @author Johannes M. Schmitt + */ +interface ExceptionInterface extends \Throwable +{ +} diff --git a/3rdparty/symfony/process/Exception/InvalidArgumentException.php b/3rdparty/symfony/process/Exception/InvalidArgumentException.php new file mode 100644 index 00000000..926ee211 --- /dev/null +++ b/3rdparty/symfony/process/Exception/InvalidArgumentException.php @@ -0,0 +1,21 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Component\Process\Exception; + +/** + * InvalidArgumentException for the Process Component. + * + * @author Romain Neutron + */ +class InvalidArgumentException extends \InvalidArgumentException implements ExceptionInterface +{ +} diff --git a/3rdparty/symfony/process/Exception/LogicException.php b/3rdparty/symfony/process/Exception/LogicException.php new file mode 100644 index 00000000..be3d490d --- /dev/null +++ b/3rdparty/symfony/process/Exception/LogicException.php @@ -0,0 +1,21 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Component\Process\Exception; + +/** + * LogicException for the Process Component. + * + * @author Romain Neutron + */ +class LogicException extends \LogicException implements ExceptionInterface +{ +} diff --git a/3rdparty/symfony/process/Exception/ProcessFailedException.php b/3rdparty/symfony/process/Exception/ProcessFailedException.php new file mode 100644 index 00000000..19b40570 --- /dev/null +++ b/3rdparty/symfony/process/Exception/ProcessFailedException.php @@ -0,0 +1,57 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Component\Process\Exception; + +use Symfony\Component\Process\Process; + +/** + * Exception for failed processes. + * + * @author Johannes M. Schmitt + */ +class ProcessFailedException extends RuntimeException +{ + private Process $process; + + public function __construct(Process $process) + { + if ($process->isSuccessful()) { + throw new InvalidArgumentException('Expected a failed process, but the given process was successful.'); + } + + $error = sprintf('The command "%s" failed.'."\n\nExit Code: %s(%s)\n\nWorking directory: %s", + $process->getCommandLine(), + $process->getExitCode(), + $process->getExitCodeText(), + $process->getWorkingDirectory() + ); + + if (!$process->isOutputDisabled()) { + $error .= sprintf("\n\nOutput:\n================\n%s\n\nError Output:\n================\n%s", + $process->getOutput(), + $process->getErrorOutput() + ); + } + + parent::__construct($error); + + $this->process = $process; + } + + /** + * @return Process + */ + public function getProcess() + { + return $this->process; + } +} diff --git a/3rdparty/symfony/process/Exception/ProcessSignaledException.php b/3rdparty/symfony/process/Exception/ProcessSignaledException.php new file mode 100644 index 00000000..0fed8ac3 --- /dev/null +++ b/3rdparty/symfony/process/Exception/ProcessSignaledException.php @@ -0,0 +1,41 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Component\Process\Exception; + +use Symfony\Component\Process\Process; + +/** + * Exception that is thrown when a process has been signaled. + * + * @author Sullivan Senechal + */ +final class ProcessSignaledException extends RuntimeException +{ + private Process $process; + + public function __construct(Process $process) + { + $this->process = $process; + + parent::__construct(sprintf('The process has been signaled with signal "%s".', $process->getTermSignal())); + } + + public function getProcess(): Process + { + return $this->process; + } + + public function getSignal(): int + { + return $this->getProcess()->getTermSignal(); + } +} diff --git a/3rdparty/symfony/process/Exception/ProcessTimedOutException.php b/3rdparty/symfony/process/Exception/ProcessTimedOutException.php new file mode 100644 index 00000000..1cecdae7 --- /dev/null +++ b/3rdparty/symfony/process/Exception/ProcessTimedOutException.php @@ -0,0 +1,73 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Component\Process\Exception; + +use Symfony\Component\Process\Process; + +/** + * Exception that is thrown when a process times out. + * + * @author Johannes M. Schmitt + */ +class ProcessTimedOutException extends RuntimeException +{ + public const TYPE_GENERAL = 1; + public const TYPE_IDLE = 2; + + private Process $process; + private int $timeoutType; + + public function __construct(Process $process, int $timeoutType) + { + $this->process = $process; + $this->timeoutType = $timeoutType; + + parent::__construct(sprintf( + 'The process "%s" exceeded the timeout of %s seconds.', + $process->getCommandLine(), + $this->getExceededTimeout() + )); + } + + /** + * @return Process + */ + public function getProcess() + { + return $this->process; + } + + /** + * @return bool + */ + public function isGeneralTimeout() + { + return self::TYPE_GENERAL === $this->timeoutType; + } + + /** + * @return bool + */ + public function isIdleTimeout() + { + return self::TYPE_IDLE === $this->timeoutType; + } + + public function getExceededTimeout(): ?float + { + return match ($this->timeoutType) { + self::TYPE_GENERAL => $this->process->getTimeout(), + self::TYPE_IDLE => $this->process->getIdleTimeout(), + default => throw new \LogicException(sprintf('Unknown timeout type "%d".', $this->timeoutType)), + }; + } +} diff --git a/3rdparty/symfony/process/Exception/RunProcessFailedException.php b/3rdparty/symfony/process/Exception/RunProcessFailedException.php new file mode 100644 index 00000000..e7219d35 --- /dev/null +++ b/3rdparty/symfony/process/Exception/RunProcessFailedException.php @@ -0,0 +1,25 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Component\Process\Exception; + +use Symfony\Component\Process\Messenger\RunProcessContext; + +/** + * @author Kevin Bond + */ +final class RunProcessFailedException extends RuntimeException +{ + public function __construct(ProcessFailedException $exception, public readonly RunProcessContext $context) + { + parent::__construct($exception->getMessage(), $exception->getCode()); + } +} diff --git a/3rdparty/symfony/process/Exception/RuntimeException.php b/3rdparty/symfony/process/Exception/RuntimeException.php new file mode 100644 index 00000000..adead253 --- /dev/null +++ b/3rdparty/symfony/process/Exception/RuntimeException.php @@ -0,0 +1,21 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Component\Process\Exception; + +/** + * RuntimeException for the Process Component. + * + * @author Johannes M. Schmitt + */ +class RuntimeException extends \RuntimeException implements ExceptionInterface +{ +} diff --git a/3rdparty/symfony/process/ExecutableFinder.php b/3rdparty/symfony/process/ExecutableFinder.php new file mode 100644 index 00000000..1838d54b --- /dev/null +++ b/3rdparty/symfony/process/ExecutableFinder.php @@ -0,0 +1,105 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Component\Process; + +/** + * Generic executable finder. + * + * @author Fabien Potencier + * @author Johannes M. Schmitt + */ +class ExecutableFinder +{ + private const CMD_BUILTINS = [ + 'assoc', 'break', 'call', 'cd', 'chdir', 'cls', 'color', 'copy', 'date', + 'del', 'dir', 'echo', 'endlocal', 'erase', 'exit', 'for', 'ftype', 'goto', + 'help', 'if', 'label', 'md', 'mkdir', 'mklink', 'move', 'path', 'pause', + 'popd', 'prompt', 'pushd', 'rd', 'rem', 'ren', 'rename', 'rmdir', 'set', + 'setlocal', 'shift', 'start', 'time', 'title', 'type', 'ver', 'vol', + ]; + + private array $suffixes = []; + + /** + * Replaces default suffixes of executable. + * + * @return void + */ + public function setSuffixes(array $suffixes) + { + $this->suffixes = $suffixes; + } + + /** + * Adds new possible suffix to check for executable. + * + * @return void + */ + public function addSuffix(string $suffix) + { + $this->suffixes[] = $suffix; + } + + /** + * Finds an executable by name. + * + * @param string $name The executable name (without the extension) + * @param string|null $default The default to return if no executable is found + * @param array $extraDirs Additional dirs to check into + */ + public function find(string $name, ?string $default = null, array $extraDirs = []): ?string + { + // windows built-in commands that are present in cmd.exe should not be resolved using PATH as they do not exist as exes + if ('\\' === \DIRECTORY_SEPARATOR && \in_array(strtolower($name), self::CMD_BUILTINS, true)) { + return $name; + } + + $dirs = array_merge( + explode(\PATH_SEPARATOR, getenv('PATH') ?: getenv('Path')), + $extraDirs + ); + + $suffixes = []; + if ('\\' === \DIRECTORY_SEPARATOR) { + $pathExt = getenv('PATHEXT'); + $suffixes = $this->suffixes; + $suffixes = array_merge($suffixes, $pathExt ? explode(\PATH_SEPARATOR, $pathExt) : ['.exe', '.bat', '.cmd', '.com']); + } + $suffixes = '' !== pathinfo($name, PATHINFO_EXTENSION) ? array_merge([''], $suffixes) : array_merge($suffixes, ['']); + foreach ($suffixes as $suffix) { + foreach ($dirs as $dir) { + if ('' === $dir) { + $dir = '.'; + } + if (@is_file($file = $dir.\DIRECTORY_SEPARATOR.$name.$suffix) && ('\\' === \DIRECTORY_SEPARATOR || @is_executable($file))) { + return $file; + } + + if (!@is_dir($dir) && basename($dir) === $name.$suffix && @is_executable($dir)) { + return $dir; + } + } + } + + if ('\\' === \DIRECTORY_SEPARATOR || !\function_exists('exec') || \strlen($name) !== strcspn($name, '/'.\DIRECTORY_SEPARATOR)) { + return $default; + } + + $execResult = exec('command -v -- '.escapeshellarg($name)); + + if (($executablePath = substr($execResult, 0, strpos($execResult, \PHP_EOL) ?: null)) && @is_executable($executablePath)) { + return $executablePath; + } + + return $default; + } +} diff --git a/3rdparty/symfony/process/InputStream.php b/3rdparty/symfony/process/InputStream.php new file mode 100644 index 00000000..931217c8 --- /dev/null +++ b/3rdparty/symfony/process/InputStream.php @@ -0,0 +1,99 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Component\Process; + +use Symfony\Component\Process\Exception\RuntimeException; + +/** + * Provides a way to continuously write to the input of a Process until the InputStream is closed. + * + * @author Nicolas Grekas + * + * @implements \IteratorAggregate + */ +class InputStream implements \IteratorAggregate +{ + private ?\Closure $onEmpty = null; + private array $input = []; + private bool $open = true; + + /** + * Sets a callback that is called when the write buffer becomes empty. + * + * @return void + */ + public function onEmpty(?callable $onEmpty = null) + { + $this->onEmpty = null !== $onEmpty ? $onEmpty(...) : null; + } + + /** + * Appends an input to the write buffer. + * + * @param resource|string|int|float|bool|\Traversable|null $input The input to append as scalar, + * stream resource or \Traversable + * + * @return void + */ + public function write(mixed $input) + { + if (null === $input) { + return; + } + if ($this->isClosed()) { + throw new RuntimeException(sprintf('"%s" is closed.', static::class)); + } + $this->input[] = ProcessUtils::validateInput(__METHOD__, $input); + } + + /** + * Closes the write buffer. + * + * @return void + */ + public function close() + { + $this->open = false; + } + + /** + * Tells whether the write buffer is closed or not. + * + * @return bool + */ + public function isClosed() + { + return !$this->open; + } + + public function getIterator(): \Traversable + { + $this->open = true; + + while ($this->open || $this->input) { + if (!$this->input) { + yield ''; + continue; + } + $current = array_shift($this->input); + + if ($current instanceof \Iterator) { + yield from $current; + } else { + yield $current; + } + if (!$this->input && $this->open && null !== $onEmpty = $this->onEmpty) { + $this->write($onEmpty($this)); + } + } + } +} diff --git a/3rdparty/symfony/process/LICENSE b/3rdparty/symfony/process/LICENSE new file mode 100644 index 00000000..0138f8f0 --- /dev/null +++ b/3rdparty/symfony/process/LICENSE @@ -0,0 +1,19 @@ +Copyright (c) 2004-present Fabien Potencier + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is furnished +to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +THE SOFTWARE. diff --git a/3rdparty/symfony/process/Messenger/RunProcessContext.php b/3rdparty/symfony/process/Messenger/RunProcessContext.php new file mode 100644 index 00000000..b5ade072 --- /dev/null +++ b/3rdparty/symfony/process/Messenger/RunProcessContext.php @@ -0,0 +1,33 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Component\Process\Messenger; + +use Symfony\Component\Process\Process; + +/** + * @author Kevin Bond + */ +final class RunProcessContext +{ + public readonly ?int $exitCode; + public readonly ?string $output; + public readonly ?string $errorOutput; + + public function __construct( + public readonly RunProcessMessage $message, + Process $process, + ) { + $this->exitCode = $process->getExitCode(); + $this->output = $process->isOutputDisabled() ? null : $process->getOutput(); + $this->errorOutput = $process->isOutputDisabled() ? null : $process->getErrorOutput(); + } +} diff --git a/3rdparty/symfony/process/Messenger/RunProcessMessage.php b/3rdparty/symfony/process/Messenger/RunProcessMessage.php new file mode 100644 index 00000000..b2c33fe3 --- /dev/null +++ b/3rdparty/symfony/process/Messenger/RunProcessMessage.php @@ -0,0 +1,32 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Component\Process\Messenger; + +/** + * @author Kevin Bond + */ +class RunProcessMessage implements \Stringable +{ + public function __construct( + public readonly array $command, + public readonly ?string $cwd = null, + public readonly ?array $env = null, + public readonly mixed $input = null, + public readonly ?float $timeout = 60.0, + ) { + } + + public function __toString(): string + { + return implode(' ', $this->command); + } +} diff --git a/3rdparty/symfony/process/Messenger/RunProcessMessageHandler.php b/3rdparty/symfony/process/Messenger/RunProcessMessageHandler.php new file mode 100644 index 00000000..41c1934c --- /dev/null +++ b/3rdparty/symfony/process/Messenger/RunProcessMessageHandler.php @@ -0,0 +1,33 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Component\Process\Messenger; + +use Symfony\Component\Process\Exception\ProcessFailedException; +use Symfony\Component\Process\Exception\RunProcessFailedException; +use Symfony\Component\Process\Process; + +/** + * @author Kevin Bond + */ +final class RunProcessMessageHandler +{ + public function __invoke(RunProcessMessage $message): RunProcessContext + { + $process = new Process($message->command, $message->cwd, $message->env, $message->input, $message->timeout); + + try { + return new RunProcessContext($message, $process->mustRun()); + } catch (ProcessFailedException $e) { + throw new RunProcessFailedException($e, new RunProcessContext($message, $e->getProcess())); + } + } +} diff --git a/3rdparty/symfony/process/PhpExecutableFinder.php b/3rdparty/symfony/process/PhpExecutableFinder.php new file mode 100644 index 00000000..e24ca008 --- /dev/null +++ b/3rdparty/symfony/process/PhpExecutableFinder.php @@ -0,0 +1,92 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Component\Process; + +/** + * An executable finder specifically designed for the PHP executable. + * + * @author Fabien Potencier + * @author Johannes M. Schmitt + */ +class PhpExecutableFinder +{ + private ExecutableFinder $executableFinder; + + public function __construct() + { + $this->executableFinder = new ExecutableFinder(); + } + + /** + * Finds The PHP executable. + */ + public function find(bool $includeArgs = true): string|false + { + if ($php = getenv('PHP_BINARY')) { + if (!is_executable($php) && !$php = $this->executableFinder->find($php)) { + return false; + } + + if (@is_dir($php)) { + return false; + } + + return $php; + } + + $args = $this->findArguments(); + $args = $includeArgs && $args ? ' '.implode(' ', $args) : ''; + + // PHP_BINARY return the current sapi executable + if (\PHP_BINARY && \in_array(\PHP_SAPI, ['cli', 'cli-server', 'phpdbg'], true)) { + return \PHP_BINARY.$args; + } + + if ($php = getenv('PHP_PATH')) { + if (!@is_executable($php) || @is_dir($php)) { + return false; + } + + return $php; + } + + if ($php = getenv('PHP_PEAR_PHP_BIN')) { + if (@is_executable($php) && !@is_dir($php)) { + return $php; + } + } + + if (@is_executable($php = \PHP_BINDIR.('\\' === \DIRECTORY_SEPARATOR ? '\\php.exe' : '/php')) && !@is_dir($php)) { + return $php; + } + + $dirs = [\PHP_BINDIR]; + if ('\\' === \DIRECTORY_SEPARATOR) { + $dirs[] = 'C:\xampp\php\\'; + } + + return $this->executableFinder->find('php', false, $dirs); + } + + /** + * Finds the PHP executable arguments. + */ + public function findArguments(): array + { + $arguments = []; + if ('phpdbg' === \PHP_SAPI) { + $arguments[] = '-qrr'; + } + + return $arguments; + } +} diff --git a/3rdparty/symfony/process/PhpProcess.php b/3rdparty/symfony/process/PhpProcess.php new file mode 100644 index 00000000..6e2ab59f --- /dev/null +++ b/3rdparty/symfony/process/PhpProcess.php @@ -0,0 +1,69 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Component\Process; + +use Symfony\Component\Process\Exception\LogicException; +use Symfony\Component\Process\Exception\RuntimeException; + +/** + * PhpProcess runs a PHP script in an independent process. + * + * $p = new PhpProcess(''); + * $p->run(); + * print $p->getOutput()."\n"; + * + * @author Fabien Potencier + */ +class PhpProcess extends Process +{ + /** + * @param string $script The PHP script to run (as a string) + * @param string|null $cwd The working directory or null to use the working dir of the current PHP process + * @param array|null $env The environment variables or null to use the same environment as the current PHP process + * @param int $timeout The timeout in seconds + * @param array|null $php Path to the PHP binary to use with any additional arguments + */ + public function __construct(string $script, ?string $cwd = null, ?array $env = null, int $timeout = 60, ?array $php = null) + { + if (null === $php) { + $executableFinder = new PhpExecutableFinder(); + $php = $executableFinder->find(false); + $php = false === $php ? null : array_merge([$php], $executableFinder->findArguments()); + } + if ('phpdbg' === \PHP_SAPI) { + $file = tempnam(sys_get_temp_dir(), 'dbg'); + file_put_contents($file, $script); + register_shutdown_function('unlink', $file); + $php[] = $file; + $script = null; + } + + parent::__construct($php, $cwd, $env, $script, $timeout); + } + + public static function fromShellCommandline(string $command, ?string $cwd = null, ?array $env = null, mixed $input = null, ?float $timeout = 60): static + { + throw new LogicException(sprintf('The "%s()" method cannot be called when using "%s".', __METHOD__, self::class)); + } + + /** + * @return void + */ + public function start(?callable $callback = null, array $env = []) + { + if (null === $this->getCommandLine()) { + throw new RuntimeException('Unable to find the PHP executable.'); + } + + parent::start($callback, $env); + } +} diff --git a/3rdparty/symfony/process/PhpSubprocess.php b/3rdparty/symfony/process/PhpSubprocess.php new file mode 100644 index 00000000..04fd8ea8 --- /dev/null +++ b/3rdparty/symfony/process/PhpSubprocess.php @@ -0,0 +1,164 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Component\Process; + +use Symfony\Component\Process\Exception\LogicException; +use Symfony\Component\Process\Exception\RuntimeException; + +/** + * PhpSubprocess runs a PHP command as a subprocess while keeping the original php.ini settings. + * + * For this, it generates a temporary php.ini file taking over all the current settings and disables + * loading additional .ini files. Basically, your command gets prefixed using "php -n -c /tmp/temp.ini". + * + * Given your php.ini contains "memory_limit=-1" and you have a "MemoryTest.php" with the following content: + * + * run(); + * print $p->getOutput()."\n"; + * + * This will output "string(2) "-1", because the process is started with the default php.ini settings. + * + * $p = new PhpSubprocess(['MemoryTest.php'], null, null, 60, ['php', '-d', 'memory_limit=256M']); + * $p->run(); + * print $p->getOutput()."\n"; + * + * This will output "string(4) "256M"", because the process is started with the temporarily created php.ini settings. + * + * @author Yanick Witschi + * @author Partially copied and heavily inspired from composer/xdebug-handler by John Stevenson + */ +class PhpSubprocess extends Process +{ + /** + * @param array $command The command to run and its arguments listed as separate entries. They will automatically + * get prefixed with the PHP binary + * @param string|null $cwd The working directory or null to use the working dir of the current PHP process + * @param array|null $env The environment variables or null to use the same environment as the current PHP process + * @param int $timeout The timeout in seconds + * @param array|null $php Path to the PHP binary to use with any additional arguments + */ + public function __construct(array $command, ?string $cwd = null, ?array $env = null, int $timeout = 60, ?array $php = null) + { + if (null === $php) { + $executableFinder = new PhpExecutableFinder(); + $php = $executableFinder->find(false); + $php = false === $php ? null : array_merge([$php], $executableFinder->findArguments()); + } + + if (null === $php) { + throw new RuntimeException('Unable to find PHP binary.'); + } + + $tmpIni = $this->writeTmpIni($this->getAllIniFiles(), sys_get_temp_dir()); + + $php = array_merge($php, ['-n', '-c', $tmpIni]); + register_shutdown_function('unlink', $tmpIni); + + $command = array_merge($php, $command); + + parent::__construct($command, $cwd, $env, null, $timeout); + } + + public static function fromShellCommandline(string $command, ?string $cwd = null, ?array $env = null, mixed $input = null, ?float $timeout = 60): static + { + throw new LogicException(sprintf('The "%s()" method cannot be called when using "%s".', __METHOD__, self::class)); + } + + public function start(?callable $callback = null, array $env = []): void + { + if (null === $this->getCommandLine()) { + throw new RuntimeException('Unable to find the PHP executable.'); + } + + parent::start($callback, $env); + } + + private function writeTmpIni(array $iniFiles, string $tmpDir): string + { + if (false === $tmpfile = @tempnam($tmpDir, '')) { + throw new RuntimeException('Unable to create temporary ini file.'); + } + + // $iniFiles has at least one item and it may be empty + if ('' === $iniFiles[0]) { + array_shift($iniFiles); + } + + $content = ''; + + foreach ($iniFiles as $file) { + // Check for inaccessible ini files + if (($data = @file_get_contents($file)) === false) { + throw new RuntimeException('Unable to read ini: '.$file); + } + // Check and remove directives after HOST and PATH sections + if (preg_match('/^\s*\[(?:PATH|HOST)\s*=/mi', $data, $matches, \PREG_OFFSET_CAPTURE)) { + $data = substr($data, 0, $matches[0][1]); + } + + $content .= $data."\n"; + } + + // Merge loaded settings into our ini content, if it is valid + $config = parse_ini_string($content); + $loaded = ini_get_all(null, false); + + if (false === $config || false === $loaded) { + throw new RuntimeException('Unable to parse ini data.'); + } + + $content .= $this->mergeLoadedConfig($loaded, $config); + + // Work-around for https://bugs.php.net/bug.php?id=75932 + $content .= "opcache.enable_cli=0\n"; + + if (false === @file_put_contents($tmpfile, $content)) { + throw new RuntimeException('Unable to write temporary ini file.'); + } + + return $tmpfile; + } + + private function mergeLoadedConfig(array $loadedConfig, array $iniConfig): string + { + $content = ''; + + foreach ($loadedConfig as $name => $value) { + if (!\is_string($value)) { + continue; + } + + if (!isset($iniConfig[$name]) || $iniConfig[$name] !== $value) { + // Double-quote escape each value + $content .= $name.'="'.addcslashes($value, '\\"')."\"\n"; + } + } + + return $content; + } + + private function getAllIniFiles(): array + { + $paths = [(string) php_ini_loaded_file()]; + + if (false !== $scanned = php_ini_scanned_files()) { + $paths = array_merge($paths, array_map('trim', explode(',', $scanned))); + } + + return $paths; + } +} diff --git a/3rdparty/symfony/process/Pipes/AbstractPipes.php b/3rdparty/symfony/process/Pipes/AbstractPipes.php new file mode 100644 index 00000000..cbbb7277 --- /dev/null +++ b/3rdparty/symfony/process/Pipes/AbstractPipes.php @@ -0,0 +1,176 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Component\Process\Pipes; + +use Symfony\Component\Process\Exception\InvalidArgumentException; + +/** + * @author Romain Neutron + * + * @internal + */ +abstract class AbstractPipes implements PipesInterface +{ + public array $pipes = []; + + private string $inputBuffer = ''; + /** @var resource|string|\Iterator */ + private $input; + private bool $blocked = true; + private ?string $lastError = null; + + /** + * @param resource|string|\Iterator $input + */ + public function __construct($input) + { + if (\is_resource($input) || $input instanceof \Iterator) { + $this->input = $input; + } else { + $this->inputBuffer = (string) $input; + } + } + + public function close(): void + { + foreach ($this->pipes as $pipe) { + if (\is_resource($pipe)) { + fclose($pipe); + } + } + $this->pipes = []; + } + + /** + * Returns true if a system call has been interrupted. + */ + protected function hasSystemCallBeenInterrupted(): bool + { + $lastError = $this->lastError; + $this->lastError = null; + + // stream_select returns false when the `select` system call is interrupted by an incoming signal + return null !== $lastError && false !== stripos($lastError, 'interrupted system call'); + } + + /** + * Unblocks streams. + */ + protected function unblock(): void + { + if (!$this->blocked) { + return; + } + + foreach ($this->pipes as $pipe) { + stream_set_blocking($pipe, 0); + } + if (\is_resource($this->input)) { + stream_set_blocking($this->input, 0); + } + + $this->blocked = false; + } + + /** + * Writes input to stdin. + * + * @throws InvalidArgumentException When an input iterator yields a non supported value + */ + protected function write(): ?array + { + if (!isset($this->pipes[0])) { + return null; + } + $input = $this->input; + + if ($input instanceof \Iterator) { + if (!$input->valid()) { + $input = null; + } elseif (\is_resource($input = $input->current())) { + stream_set_blocking($input, 0); + } elseif (!isset($this->inputBuffer[0])) { + if (!\is_string($input)) { + if (!\is_scalar($input)) { + throw new InvalidArgumentException(sprintf('"%s" yielded a value of type "%s", but only scalars and stream resources are supported.', get_debug_type($this->input), get_debug_type($input))); + } + $input = (string) $input; + } + $this->inputBuffer = $input; + $this->input->next(); + $input = null; + } else { + $input = null; + } + } + + $r = $e = []; + $w = [$this->pipes[0]]; + + // let's have a look if something changed in streams + if (false === @stream_select($r, $w, $e, 0, 0)) { + return null; + } + + foreach ($w as $stdin) { + if (isset($this->inputBuffer[0])) { + $written = fwrite($stdin, $this->inputBuffer); + $this->inputBuffer = substr($this->inputBuffer, $written); + if (isset($this->inputBuffer[0])) { + return [$this->pipes[0]]; + } + } + + if ($input) { + while (true) { + $data = fread($input, self::CHUNK_SIZE); + if (!isset($data[0])) { + break; + } + $written = fwrite($stdin, $data); + $data = substr($data, $written); + if (isset($data[0])) { + $this->inputBuffer = $data; + + return [$this->pipes[0]]; + } + } + if (feof($input)) { + if ($this->input instanceof \Iterator) { + $this->input->next(); + } else { + $this->input = null; + } + } + } + } + + // no input to read on resource, buffer is empty + if (!isset($this->inputBuffer[0]) && !($this->input instanceof \Iterator ? $this->input->valid() : $this->input)) { + $this->input = null; + fclose($this->pipes[0]); + unset($this->pipes[0]); + } elseif (!$w) { + return [$this->pipes[0]]; + } + + return null; + } + + /** + * @internal + */ + public function handleError(int $type, string $msg): void + { + $this->lastError = $msg; + } +} diff --git a/3rdparty/symfony/process/Pipes/PipesInterface.php b/3rdparty/symfony/process/Pipes/PipesInterface.php new file mode 100644 index 00000000..967f8de7 --- /dev/null +++ b/3rdparty/symfony/process/Pipes/PipesInterface.php @@ -0,0 +1,61 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Component\Process\Pipes; + +/** + * PipesInterface manages descriptors and pipes for the use of proc_open. + * + * @author Romain Neutron + * + * @internal + */ +interface PipesInterface +{ + public const CHUNK_SIZE = 16384; + + /** + * Returns an array of descriptors for the use of proc_open. + */ + public function getDescriptors(): array; + + /** + * Returns an array of filenames indexed by their related stream in case these pipes use temporary files. + * + * @return string[] + */ + public function getFiles(): array; + + /** + * Reads data in file handles and pipes. + * + * @param bool $blocking Whether to use blocking calls or not + * @param bool $close Whether to close pipes if they've reached EOF + * + * @return string[] An array of read data indexed by their fd + */ + public function readAndWrite(bool $blocking, bool $close = false): array; + + /** + * Returns if the current state has open file handles or pipes. + */ + public function areOpen(): bool; + + /** + * Returns if pipes are able to read output. + */ + public function haveReadSupport(): bool; + + /** + * Closes file handles and pipes. + */ + public function close(): void; +} diff --git a/3rdparty/symfony/process/Pipes/UnixPipes.php b/3rdparty/symfony/process/Pipes/UnixPipes.php new file mode 100644 index 00000000..7bd0db0e --- /dev/null +++ b/3rdparty/symfony/process/Pipes/UnixPipes.php @@ -0,0 +1,148 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Component\Process\Pipes; + +use Symfony\Component\Process\Process; + +/** + * UnixPipes implementation uses unix pipes as handles. + * + * @author Romain Neutron + * + * @internal + */ +class UnixPipes extends AbstractPipes +{ + private ?bool $ttyMode; + private bool $ptyMode; + private bool $haveReadSupport; + + public function __construct(?bool $ttyMode, bool $ptyMode, mixed $input, bool $haveReadSupport) + { + $this->ttyMode = $ttyMode; + $this->ptyMode = $ptyMode; + $this->haveReadSupport = $haveReadSupport; + + parent::__construct($input); + } + + public function __sleep(): array + { + throw new \BadMethodCallException('Cannot serialize '.__CLASS__); + } + + public function __wakeup(): void + { + throw new \BadMethodCallException('Cannot unserialize '.__CLASS__); + } + + public function __destruct() + { + $this->close(); + } + + public function getDescriptors(): array + { + if (!$this->haveReadSupport) { + $nullstream = fopen('/dev/null', 'c'); + + return [ + ['pipe', 'r'], + $nullstream, + $nullstream, + ]; + } + + if ($this->ttyMode) { + return [ + ['file', '/dev/tty', 'r'], + ['file', '/dev/tty', 'w'], + ['file', '/dev/tty', 'w'], + ]; + } + + if ($this->ptyMode && Process::isPtySupported()) { + return [ + ['pty'], + ['pty'], + ['pty'], + ]; + } + + return [ + ['pipe', 'r'], + ['pipe', 'w'], // stdout + ['pipe', 'w'], // stderr + ]; + } + + public function getFiles(): array + { + return []; + } + + public function readAndWrite(bool $blocking, bool $close = false): array + { + $this->unblock(); + $w = $this->write(); + + $read = $e = []; + $r = $this->pipes; + unset($r[0]); + + // let's have a look if something changed in streams + set_error_handler($this->handleError(...)); + if (($r || $w) && false === stream_select($r, $w, $e, 0, $blocking ? Process::TIMEOUT_PRECISION * 1E6 : 0)) { + restore_error_handler(); + // if a system call has been interrupted, forget about it, let's try again + // otherwise, an error occurred, let's reset pipes + if (!$this->hasSystemCallBeenInterrupted()) { + $this->pipes = []; + } + + return $read; + } + restore_error_handler(); + + foreach ($r as $pipe) { + // prior PHP 5.4 the array passed to stream_select is modified and + // lose key association, we have to find back the key + $read[$type = array_search($pipe, $this->pipes, true)] = ''; + + do { + $data = @fread($pipe, self::CHUNK_SIZE); + $read[$type] .= $data; + } while (isset($data[0]) && ($close || isset($data[self::CHUNK_SIZE - 1]))); + + if (!isset($read[$type][0])) { + unset($read[$type]); + } + + if ($close && feof($pipe)) { + fclose($pipe); + unset($this->pipes[$type]); + } + } + + return $read; + } + + public function haveReadSupport(): bool + { + return $this->haveReadSupport; + } + + public function areOpen(): bool + { + return (bool) $this->pipes; + } +} diff --git a/3rdparty/symfony/process/Pipes/WindowsPipes.php b/3rdparty/symfony/process/Pipes/WindowsPipes.php new file mode 100644 index 00000000..8033442a --- /dev/null +++ b/3rdparty/symfony/process/Pipes/WindowsPipes.php @@ -0,0 +1,186 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Component\Process\Pipes; + +use Symfony\Component\Process\Exception\RuntimeException; +use Symfony\Component\Process\Process; + +/** + * WindowsPipes implementation uses temporary files as handles. + * + * @see https://bugs.php.net/51800 + * @see https://bugs.php.net/65650 + * + * @author Romain Neutron + * + * @internal + */ +class WindowsPipes extends AbstractPipes +{ + private array $files = []; + private array $fileHandles = []; + private array $lockHandles = []; + private array $readBytes = [ + Process::STDOUT => 0, + Process::STDERR => 0, + ]; + private bool $haveReadSupport; + + public function __construct(mixed $input, bool $haveReadSupport) + { + $this->haveReadSupport = $haveReadSupport; + + if ($this->haveReadSupport) { + // Fix for PHP bug #51800: reading from STDOUT pipe hangs forever on Windows if the output is too big. + // Workaround for this problem is to use temporary files instead of pipes on Windows platform. + // + // @see https://bugs.php.net/51800 + $pipes = [ + Process::STDOUT => Process::OUT, + Process::STDERR => Process::ERR, + ]; + $tmpDir = sys_get_temp_dir(); + $lastError = 'unknown reason'; + set_error_handler(function ($type, $msg) use (&$lastError) { $lastError = $msg; }); + for ($i = 0;; ++$i) { + foreach ($pipes as $pipe => $name) { + $file = sprintf('%s\\sf_proc_%02X.%s', $tmpDir, $i, $name); + + if (!$h = fopen($file.'.lock', 'w')) { + if (file_exists($file.'.lock')) { + continue 2; + } + restore_error_handler(); + throw new RuntimeException('A temporary file could not be opened to write the process output: '.$lastError); + } + if (!flock($h, \LOCK_EX | \LOCK_NB)) { + continue 2; + } + if (isset($this->lockHandles[$pipe])) { + flock($this->lockHandles[$pipe], \LOCK_UN); + fclose($this->lockHandles[$pipe]); + } + $this->lockHandles[$pipe] = $h; + + if (!($h = fopen($file, 'w')) || !fclose($h) || !$h = fopen($file, 'r')) { + flock($this->lockHandles[$pipe], \LOCK_UN); + fclose($this->lockHandles[$pipe]); + unset($this->lockHandles[$pipe]); + continue 2; + } + $this->fileHandles[$pipe] = $h; + $this->files[$pipe] = $file; + } + break; + } + restore_error_handler(); + } + + parent::__construct($input); + } + + public function __sleep(): array + { + throw new \BadMethodCallException('Cannot serialize '.__CLASS__); + } + + public function __wakeup(): void + { + throw new \BadMethodCallException('Cannot unserialize '.__CLASS__); + } + + public function __destruct() + { + $this->close(); + } + + public function getDescriptors(): array + { + if (!$this->haveReadSupport) { + $nullstream = fopen('NUL', 'c'); + + return [ + ['pipe', 'r'], + $nullstream, + $nullstream, + ]; + } + + // We're not using pipe on Windows platform as it hangs (https://bugs.php.net/51800) + // We're not using file handles as it can produce corrupted output https://bugs.php.net/65650 + // So we redirect output within the commandline and pass the nul device to the process + return [ + ['pipe', 'r'], + ['file', 'NUL', 'w'], + ['file', 'NUL', 'w'], + ]; + } + + public function getFiles(): array + { + return $this->files; + } + + public function readAndWrite(bool $blocking, bool $close = false): array + { + $this->unblock(); + $w = $this->write(); + $read = $r = $e = []; + + if ($blocking) { + if ($w) { + @stream_select($r, $w, $e, 0, Process::TIMEOUT_PRECISION * 1E6); + } elseif ($this->fileHandles) { + usleep((int) (Process::TIMEOUT_PRECISION * 1E6)); + } + } + foreach ($this->fileHandles as $type => $fileHandle) { + $data = stream_get_contents($fileHandle, -1, $this->readBytes[$type]); + + if (isset($data[0])) { + $this->readBytes[$type] += \strlen($data); + $read[$type] = $data; + } + if ($close) { + ftruncate($fileHandle, 0); + fclose($fileHandle); + flock($this->lockHandles[$type], \LOCK_UN); + fclose($this->lockHandles[$type]); + unset($this->fileHandles[$type], $this->lockHandles[$type]); + } + } + + return $read; + } + + public function haveReadSupport(): bool + { + return $this->haveReadSupport; + } + + public function areOpen(): bool + { + return $this->pipes && $this->fileHandles; + } + + public function close(): void + { + parent::close(); + foreach ($this->fileHandles as $type => $handle) { + ftruncate($handle, 0); + fclose($handle); + flock($this->lockHandles[$type], \LOCK_UN); + fclose($this->lockHandles[$type]); + } + $this->fileHandles = $this->lockHandles = []; + } +} diff --git a/3rdparty/symfony/process/Process.php b/3rdparty/symfony/process/Process.php new file mode 100644 index 00000000..280a732d --- /dev/null +++ b/3rdparty/symfony/process/Process.php @@ -0,0 +1,1618 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Component\Process; + +use Symfony\Component\Process\Exception\InvalidArgumentException; +use Symfony\Component\Process\Exception\LogicException; +use Symfony\Component\Process\Exception\ProcessFailedException; +use Symfony\Component\Process\Exception\ProcessSignaledException; +use Symfony\Component\Process\Exception\ProcessTimedOutException; +use Symfony\Component\Process\Exception\RuntimeException; +use Symfony\Component\Process\Pipes\UnixPipes; +use Symfony\Component\Process\Pipes\WindowsPipes; + +/** + * Process is a thin wrapper around proc_* functions to easily + * start independent PHP processes. + * + * @author Fabien Potencier + * @author Romain Neutron + * + * @implements \IteratorAggregate + */ +class Process implements \IteratorAggregate +{ + public const ERR = 'err'; + public const OUT = 'out'; + + public const STATUS_READY = 'ready'; + public const STATUS_STARTED = 'started'; + public const STATUS_TERMINATED = 'terminated'; + + public const STDIN = 0; + public const STDOUT = 1; + public const STDERR = 2; + + // Timeout Precision in seconds. + public const TIMEOUT_PRECISION = 0.2; + + public const ITER_NON_BLOCKING = 1; // By default, iterating over outputs is a blocking call, use this flag to make it non-blocking + public const ITER_KEEP_OUTPUT = 2; // By default, outputs are cleared while iterating, use this flag to keep them in memory + public const ITER_SKIP_OUT = 4; // Use this flag to skip STDOUT while iterating + public const ITER_SKIP_ERR = 8; // Use this flag to skip STDERR while iterating + + private ?\Closure $callback = null; + private array|string $commandline; + private ?string $cwd; + private array $env = []; + /** @var resource|string|\Iterator|null */ + private $input; + private ?float $starttime = null; + private ?float $lastOutputTime = null; + private ?float $timeout = null; + private ?float $idleTimeout = null; + private ?int $exitcode = null; + private array $fallbackStatus = []; + private array $processInformation; + private bool $outputDisabled = false; + /** @var resource */ + private $stdout; + /** @var resource */ + private $stderr; + /** @var resource|null */ + private $process; + private string $status = self::STATUS_READY; + private int $incrementalOutputOffset = 0; + private int $incrementalErrorOutputOffset = 0; + private bool $tty = false; + private bool $pty; + private array $options = ['suppress_errors' => true, 'bypass_shell' => true]; + + private WindowsPipes|UnixPipes $processPipes; + + private ?int $latestSignal = null; + private ?int $cachedExitCode = null; + + private static ?bool $sigchild = null; + + /** + * Exit codes translation table. + * + * User-defined errors must use exit codes in the 64-113 range. + */ + public static $exitCodes = [ + 0 => 'OK', + 1 => 'General error', + 2 => 'Misuse of shell builtins', + + 126 => 'Invoked command cannot execute', + 127 => 'Command not found', + 128 => 'Invalid exit argument', + + // signals + 129 => 'Hangup', + 130 => 'Interrupt', + 131 => 'Quit and dump core', + 132 => 'Illegal instruction', + 133 => 'Trace/breakpoint trap', + 134 => 'Process aborted', + 135 => 'Bus error: "access to undefined portion of memory object"', + 136 => 'Floating point exception: "erroneous arithmetic operation"', + 137 => 'Kill (terminate immediately)', + 138 => 'User-defined 1', + 139 => 'Segmentation violation', + 140 => 'User-defined 2', + 141 => 'Write to pipe with no one reading', + 142 => 'Signal raised by alarm', + 143 => 'Termination (request to terminate)', + // 144 - not defined + 145 => 'Child process terminated, stopped (or continued*)', + 146 => 'Continue if stopped', + 147 => 'Stop executing temporarily', + 148 => 'Terminal stop signal', + 149 => 'Background process attempting to read from tty ("in")', + 150 => 'Background process attempting to write to tty ("out")', + 151 => 'Urgent data available on socket', + 152 => 'CPU time limit exceeded', + 153 => 'File size limit exceeded', + 154 => 'Signal raised by timer counting virtual time: "virtual timer expired"', + 155 => 'Profiling timer expired', + // 156 - not defined + 157 => 'Pollable event', + // 158 - not defined + 159 => 'Bad syscall', + ]; + + /** + * @param array $command The command to run and its arguments listed as separate entries + * @param string|null $cwd The working directory or null to use the working dir of the current PHP process + * @param array|null $env The environment variables or null to use the same environment as the current PHP process + * @param mixed $input The input as stream resource, scalar or \Traversable, or null for no input + * @param int|float|null $timeout The timeout in seconds or null to disable + * + * @throws LogicException When proc_open is not installed + */ + public function __construct(array $command, ?string $cwd = null, ?array $env = null, mixed $input = null, ?float $timeout = 60) + { + if (!\function_exists('proc_open')) { + throw new LogicException('The Process class relies on proc_open, which is not available on your PHP installation.'); + } + + $this->commandline = $command; + $this->cwd = $cwd; + + // on Windows, if the cwd changed via chdir(), proc_open defaults to the dir where PHP was started + // on Gnu/Linux, PHP builds with --enable-maintainer-zts are also affected + // @see : https://bugs.php.net/51800 + // @see : https://bugs.php.net/50524 + if (null === $this->cwd && (\defined('ZEND_THREAD_SAFE') || '\\' === \DIRECTORY_SEPARATOR)) { + $this->cwd = getcwd(); + } + if (null !== $env) { + $this->setEnv($env); + } + + $this->setInput($input); + $this->setTimeout($timeout); + $this->pty = false; + } + + /** + * Creates a Process instance as a command-line to be run in a shell wrapper. + * + * Command-lines are parsed by the shell of your OS (/bin/sh on Unix-like, cmd.exe on Windows.) + * This allows using e.g. pipes or conditional execution. In this mode, signals are sent to the + * shell wrapper and not to your commands. + * + * In order to inject dynamic values into command-lines, we strongly recommend using placeholders. + * This will save escaping values, which is not portable nor secure anyway: + * + * $process = Process::fromShellCommandline('my_command "${:MY_VAR}"'); + * $process->run(null, ['MY_VAR' => $theValue]); + * + * @param string $command The command line to pass to the shell of the OS + * @param string|null $cwd The working directory or null to use the working dir of the current PHP process + * @param array|null $env The environment variables or null to use the same environment as the current PHP process + * @param mixed $input The input as stream resource, scalar or \Traversable, or null for no input + * @param int|float|null $timeout The timeout in seconds or null to disable + * + * @throws LogicException When proc_open is not installed + */ + public static function fromShellCommandline(string $command, ?string $cwd = null, ?array $env = null, mixed $input = null, ?float $timeout = 60): static + { + $process = new static([], $cwd, $env, $input, $timeout); + $process->commandline = $command; + + return $process; + } + + public function __sleep(): array + { + throw new \BadMethodCallException('Cannot serialize '.__CLASS__); + } + + /** + * @return void + */ + public function __wakeup() + { + throw new \BadMethodCallException('Cannot unserialize '.__CLASS__); + } + + public function __destruct() + { + if ($this->options['create_new_console'] ?? false) { + $this->processPipes->close(); + } else { + $this->stop(0); + } + } + + public function __clone() + { + $this->resetProcessData(); + } + + /** + * Runs the process. + * + * The callback receives the type of output (out or err) and + * some bytes from the output in real-time. It allows to have feedback + * from the independent process during execution. + * + * The STDOUT and STDERR are also available after the process is finished + * via the getOutput() and getErrorOutput() methods. + * + * @param callable|null $callback A PHP callback to run whenever there is some + * output available on STDOUT or STDERR + * + * @return int The exit status code + * + * @throws RuntimeException When process can't be launched + * @throws RuntimeException When process is already running + * @throws ProcessTimedOutException When process timed out + * @throws ProcessSignaledException When process stopped after receiving signal + * @throws LogicException In case a callback is provided and output has been disabled + * + * @final + */ + public function run(?callable $callback = null, array $env = []): int + { + $this->start($callback, $env); + + return $this->wait(); + } + + /** + * Runs the process. + * + * This is identical to run() except that an exception is thrown if the process + * exits with a non-zero exit code. + * + * @return $this + * + * @throws ProcessFailedException if the process didn't terminate successfully + * + * @final + */ + public function mustRun(?callable $callback = null, array $env = []): static + { + if (0 !== $this->run($callback, $env)) { + throw new ProcessFailedException($this); + } + + return $this; + } + + /** + * Starts the process and returns after writing the input to STDIN. + * + * This method blocks until all STDIN data is sent to the process then it + * returns while the process runs in the background. + * + * The termination of the process can be awaited with wait(). + * + * The callback receives the type of output (out or err) and some bytes from + * the output in real-time while writing the standard input to the process. + * It allows to have feedback from the independent process during execution. + * + * @param callable|null $callback A PHP callback to run whenever there is some + * output available on STDOUT or STDERR + * + * @return void + * + * @throws RuntimeException When process can't be launched + * @throws RuntimeException When process is already running + * @throws LogicException In case a callback is provided and output has been disabled + */ + public function start(?callable $callback = null, array $env = []) + { + if ($this->isRunning()) { + throw new RuntimeException('Process is already running.'); + } + + $this->resetProcessData(); + $this->starttime = $this->lastOutputTime = microtime(true); + $this->callback = $this->buildCallback($callback); + $descriptors = $this->getDescriptors(null !== $callback); + + if ($this->env) { + $env += '\\' === \DIRECTORY_SEPARATOR ? array_diff_ukey($this->env, $env, 'strcasecmp') : $this->env; + } + + $env += '\\' === \DIRECTORY_SEPARATOR ? array_diff_ukey($this->getDefaultEnv(), $env, 'strcasecmp') : $this->getDefaultEnv(); + + if (\is_array($commandline = $this->commandline)) { + $commandline = implode(' ', array_map($this->escapeArgument(...), $commandline)); + + if ('\\' !== \DIRECTORY_SEPARATOR) { + // exec is mandatory to deal with sending a signal to the process + $commandline = 'exec '.$commandline; + } + } else { + $commandline = $this->replacePlaceholders($commandline, $env); + } + + if ('\\' === \DIRECTORY_SEPARATOR) { + $commandline = $this->prepareWindowsCommandLine($commandline, $env); + } elseif ($this->isSigchildEnabled()) { + // last exit code is output on the fourth pipe and caught to work around --enable-sigchild + $descriptors[3] = ['pipe', 'w']; + + // See https://unix.stackexchange.com/questions/71205/background-process-pipe-input + $commandline = '{ ('.$commandline.') <&3 3<&- 3>/dev/null & } 3<&0;'; + $commandline .= 'pid=$!; echo $pid >&3; wait $pid 2>/dev/null; code=$?; echo $code >&3; exit $code'; + } + + $envPairs = []; + foreach ($env as $k => $v) { + if (false !== $v && false === \in_array($k, ['argc', 'argv', 'ARGC', 'ARGV'], true)) { + $envPairs[] = $k.'='.$v; + } + } + + if (!is_dir($this->cwd)) { + throw new RuntimeException(sprintf('The provided cwd "%s" does not exist.', $this->cwd)); + } + + $process = @proc_open($commandline, $descriptors, $this->processPipes->pipes, $this->cwd, $envPairs, $this->options); + + if (!$process) { + throw new RuntimeException('Unable to launch a new process.'); + } + $this->process = $process; + $this->status = self::STATUS_STARTED; + + if (isset($descriptors[3])) { + $this->fallbackStatus['pid'] = (int) fgets($this->processPipes->pipes[3]); + } + + if ($this->tty) { + return; + } + + $this->updateStatus(false); + $this->checkTimeout(); + } + + /** + * Restarts the process. + * + * Be warned that the process is cloned before being started. + * + * @param callable|null $callback A PHP callback to run whenever there is some + * output available on STDOUT or STDERR + * + * @throws RuntimeException When process can't be launched + * @throws RuntimeException When process is already running + * + * @see start() + * + * @final + */ + public function restart(?callable $callback = null, array $env = []): static + { + if ($this->isRunning()) { + throw new RuntimeException('Process is already running.'); + } + + $process = clone $this; + $process->start($callback, $env); + + return $process; + } + + /** + * Waits for the process to terminate. + * + * The callback receives the type of output (out or err) and some bytes + * from the output in real-time while writing the standard input to the process. + * It allows to have feedback from the independent process during execution. + * + * @param callable|null $callback A valid PHP callback + * + * @return int The exitcode of the process + * + * @throws ProcessTimedOutException When process timed out + * @throws ProcessSignaledException When process stopped after receiving signal + * @throws LogicException When process is not yet started + */ + public function wait(?callable $callback = null): int + { + $this->requireProcessIsStarted(__FUNCTION__); + + $this->updateStatus(false); + + if (null !== $callback) { + if (!$this->processPipes->haveReadSupport()) { + $this->stop(0); + throw new LogicException('Pass the callback to the "Process::start" method or call enableOutput to use a callback with "Process::wait".'); + } + $this->callback = $this->buildCallback($callback); + } + + do { + $this->checkTimeout(); + $running = $this->isRunning() && ('\\' === \DIRECTORY_SEPARATOR || $this->processPipes->areOpen()); + $this->readPipes($running, '\\' !== \DIRECTORY_SEPARATOR || !$running); + } while ($running); + + while ($this->isRunning()) { + $this->checkTimeout(); + usleep(1000); + } + + if ($this->processInformation['signaled'] && $this->processInformation['termsig'] !== $this->latestSignal) { + throw new ProcessSignaledException($this); + } + + return $this->exitcode; + } + + /** + * Waits until the callback returns true. + * + * The callback receives the type of output (out or err) and some bytes + * from the output in real-time while writing the standard input to the process. + * It allows to have feedback from the independent process during execution. + * + * @throws RuntimeException When process timed out + * @throws LogicException When process is not yet started + * @throws ProcessTimedOutException In case the timeout was reached + */ + public function waitUntil(callable $callback): bool + { + $this->requireProcessIsStarted(__FUNCTION__); + $this->updateStatus(false); + + if (!$this->processPipes->haveReadSupport()) { + $this->stop(0); + throw new LogicException('Pass the callback to the "Process::start" method or call enableOutput to use a callback with "Process::waitUntil".'); + } + $callback = $this->buildCallback($callback); + + $ready = false; + while (true) { + $this->checkTimeout(); + $running = '\\' === \DIRECTORY_SEPARATOR ? $this->isRunning() : $this->processPipes->areOpen(); + $output = $this->processPipes->readAndWrite($running, '\\' !== \DIRECTORY_SEPARATOR || !$running); + + foreach ($output as $type => $data) { + if (3 !== $type) { + $ready = $callback(self::STDOUT === $type ? self::OUT : self::ERR, $data) || $ready; + } elseif (!isset($this->fallbackStatus['signaled'])) { + $this->fallbackStatus['exitcode'] = (int) $data; + } + } + if ($ready) { + return true; + } + if (!$running) { + return false; + } + + usleep(1000); + } + } + + /** + * Returns the Pid (process identifier), if applicable. + * + * @return int|null The process id if running, null otherwise + */ + public function getPid(): ?int + { + return $this->isRunning() ? $this->processInformation['pid'] : null; + } + + /** + * Sends a POSIX signal to the process. + * + * @param int $signal A valid POSIX signal (see https://php.net/pcntl.constants) + * + * @return $this + * + * @throws LogicException In case the process is not running + * @throws RuntimeException In case --enable-sigchild is activated and the process can't be killed + * @throws RuntimeException In case of failure + */ + public function signal(int $signal): static + { + $this->doSignal($signal, true); + + return $this; + } + + /** + * Disables fetching output and error output from the underlying process. + * + * @return $this + * + * @throws RuntimeException In case the process is already running + * @throws LogicException if an idle timeout is set + */ + public function disableOutput(): static + { + if ($this->isRunning()) { + throw new RuntimeException('Disabling output while the process is running is not possible.'); + } + if (null !== $this->idleTimeout) { + throw new LogicException('Output cannot be disabled while an idle timeout is set.'); + } + + $this->outputDisabled = true; + + return $this; + } + + /** + * Enables fetching output and error output from the underlying process. + * + * @return $this + * + * @throws RuntimeException In case the process is already running + */ + public function enableOutput(): static + { + if ($this->isRunning()) { + throw new RuntimeException('Enabling output while the process is running is not possible.'); + } + + $this->outputDisabled = false; + + return $this; + } + + /** + * Returns true in case the output is disabled, false otherwise. + */ + public function isOutputDisabled(): bool + { + return $this->outputDisabled; + } + + /** + * Returns the current output of the process (STDOUT). + * + * @throws LogicException in case the output has been disabled + * @throws LogicException In case the process is not started + */ + public function getOutput(): string + { + $this->readPipesForOutput(__FUNCTION__); + + if (false === $ret = stream_get_contents($this->stdout, -1, 0)) { + return ''; + } + + return $ret; + } + + /** + * Returns the output incrementally. + * + * In comparison with the getOutput method which always return the whole + * output, this one returns the new output since the last call. + * + * @throws LogicException in case the output has been disabled + * @throws LogicException In case the process is not started + */ + public function getIncrementalOutput(): string + { + $this->readPipesForOutput(__FUNCTION__); + + $latest = stream_get_contents($this->stdout, -1, $this->incrementalOutputOffset); + $this->incrementalOutputOffset = ftell($this->stdout); + + if (false === $latest) { + return ''; + } + + return $latest; + } + + /** + * Returns an iterator to the output of the process, with the output type as keys (Process::OUT/ERR). + * + * @param int $flags A bit field of Process::ITER_* flags + * + * @return \Generator + * + * @throws LogicException in case the output has been disabled + * @throws LogicException In case the process is not started + */ + public function getIterator(int $flags = 0): \Generator + { + $this->readPipesForOutput(__FUNCTION__, false); + + $clearOutput = !(self::ITER_KEEP_OUTPUT & $flags); + $blocking = !(self::ITER_NON_BLOCKING & $flags); + $yieldOut = !(self::ITER_SKIP_OUT & $flags); + $yieldErr = !(self::ITER_SKIP_ERR & $flags); + + while (null !== $this->callback || ($yieldOut && !feof($this->stdout)) || ($yieldErr && !feof($this->stderr))) { + if ($yieldOut) { + $out = stream_get_contents($this->stdout, -1, $this->incrementalOutputOffset); + + if (isset($out[0])) { + if ($clearOutput) { + $this->clearOutput(); + } else { + $this->incrementalOutputOffset = ftell($this->stdout); + } + + yield self::OUT => $out; + } + } + + if ($yieldErr) { + $err = stream_get_contents($this->stderr, -1, $this->incrementalErrorOutputOffset); + + if (isset($err[0])) { + if ($clearOutput) { + $this->clearErrorOutput(); + } else { + $this->incrementalErrorOutputOffset = ftell($this->stderr); + } + + yield self::ERR => $err; + } + } + + if (!$blocking && !isset($out[0]) && !isset($err[0])) { + yield self::OUT => ''; + } + + $this->checkTimeout(); + $this->readPipesForOutput(__FUNCTION__, $blocking); + } + } + + /** + * Clears the process output. + * + * @return $this + */ + public function clearOutput(): static + { + ftruncate($this->stdout, 0); + fseek($this->stdout, 0); + $this->incrementalOutputOffset = 0; + + return $this; + } + + /** + * Returns the current error output of the process (STDERR). + * + * @throws LogicException in case the output has been disabled + * @throws LogicException In case the process is not started + */ + public function getErrorOutput(): string + { + $this->readPipesForOutput(__FUNCTION__); + + if (false === $ret = stream_get_contents($this->stderr, -1, 0)) { + return ''; + } + + return $ret; + } + + /** + * Returns the errorOutput incrementally. + * + * In comparison with the getErrorOutput method which always return the + * whole error output, this one returns the new error output since the last + * call. + * + * @throws LogicException in case the output has been disabled + * @throws LogicException In case the process is not started + */ + public function getIncrementalErrorOutput(): string + { + $this->readPipesForOutput(__FUNCTION__); + + $latest = stream_get_contents($this->stderr, -1, $this->incrementalErrorOutputOffset); + $this->incrementalErrorOutputOffset = ftell($this->stderr); + + if (false === $latest) { + return ''; + } + + return $latest; + } + + /** + * Clears the process output. + * + * @return $this + */ + public function clearErrorOutput(): static + { + ftruncate($this->stderr, 0); + fseek($this->stderr, 0); + $this->incrementalErrorOutputOffset = 0; + + return $this; + } + + /** + * Returns the exit code returned by the process. + * + * @return int|null The exit status code, null if the Process is not terminated + */ + public function getExitCode(): ?int + { + $this->updateStatus(false); + + return $this->exitcode; + } + + /** + * Returns a string representation for the exit code returned by the process. + * + * This method relies on the Unix exit code status standardization + * and might not be relevant for other operating systems. + * + * @return string|null A string representation for the exit status code, null if the Process is not terminated + * + * @see http://tldp.org/LDP/abs/html/exitcodes.html + * @see http://en.wikipedia.org/wiki/Unix_signal + */ + public function getExitCodeText(): ?string + { + if (null === $exitcode = $this->getExitCode()) { + return null; + } + + return self::$exitCodes[$exitcode] ?? 'Unknown error'; + } + + /** + * Checks if the process ended successfully. + */ + public function isSuccessful(): bool + { + return 0 === $this->getExitCode(); + } + + /** + * Returns true if the child process has been terminated by an uncaught signal. + * + * It always returns false on Windows. + * + * @throws LogicException In case the process is not terminated + */ + public function hasBeenSignaled(): bool + { + $this->requireProcessIsTerminated(__FUNCTION__); + + return $this->processInformation['signaled']; + } + + /** + * Returns the number of the signal that caused the child process to terminate its execution. + * + * It is only meaningful if hasBeenSignaled() returns true. + * + * @throws RuntimeException In case --enable-sigchild is activated + * @throws LogicException In case the process is not terminated + */ + public function getTermSignal(): int + { + $this->requireProcessIsTerminated(__FUNCTION__); + + if ($this->isSigchildEnabled() && -1 === $this->processInformation['termsig']) { + throw new RuntimeException('This PHP has been compiled with --enable-sigchild. Term signal cannot be retrieved.'); + } + + return $this->processInformation['termsig']; + } + + /** + * Returns true if the child process has been stopped by a signal. + * + * It always returns false on Windows. + * + * @throws LogicException In case the process is not terminated + */ + public function hasBeenStopped(): bool + { + $this->requireProcessIsTerminated(__FUNCTION__); + + return $this->processInformation['stopped']; + } + + /** + * Returns the number of the signal that caused the child process to stop its execution. + * + * It is only meaningful if hasBeenStopped() returns true. + * + * @throws LogicException In case the process is not terminated + */ + public function getStopSignal(): int + { + $this->requireProcessIsTerminated(__FUNCTION__); + + return $this->processInformation['stopsig']; + } + + /** + * Checks if the process is currently running. + */ + public function isRunning(): bool + { + if (self::STATUS_STARTED !== $this->status) { + return false; + } + + $this->updateStatus(false); + + return $this->processInformation['running']; + } + + /** + * Checks if the process has been started with no regard to the current state. + */ + public function isStarted(): bool + { + return self::STATUS_READY != $this->status; + } + + /** + * Checks if the process is terminated. + */ + public function isTerminated(): bool + { + $this->updateStatus(false); + + return self::STATUS_TERMINATED == $this->status; + } + + /** + * Gets the process status. + * + * The status is one of: ready, started, terminated. + */ + public function getStatus(): string + { + $this->updateStatus(false); + + return $this->status; + } + + /** + * Stops the process. + * + * @param int|float $timeout The timeout in seconds + * @param int|null $signal A POSIX signal to send in case the process has not stop at timeout, default is SIGKILL (9) + * + * @return int|null The exit-code of the process or null if it's not running + */ + public function stop(float $timeout = 10, ?int $signal = null): ?int + { + $timeoutMicro = microtime(true) + $timeout; + if ($this->isRunning()) { + // given SIGTERM may not be defined and that "proc_terminate" uses the constant value and not the constant itself, we use the same here + $this->doSignal(15, false); + do { + usleep(1000); + } while ($this->isRunning() && microtime(true) < $timeoutMicro); + + if ($this->isRunning()) { + // Avoid exception here: process is supposed to be running, but it might have stopped just + // after this line. In any case, let's silently discard the error, we cannot do anything. + $this->doSignal($signal ?: 9, false); + } + } + + if ($this->isRunning()) { + if (isset($this->fallbackStatus['pid'])) { + unset($this->fallbackStatus['pid']); + + return $this->stop(0, $signal); + } + $this->close(); + } + + return $this->exitcode; + } + + /** + * Adds a line to the STDOUT stream. + * + * @internal + */ + public function addOutput(string $line): void + { + $this->lastOutputTime = microtime(true); + + fseek($this->stdout, 0, \SEEK_END); + fwrite($this->stdout, $line); + fseek($this->stdout, $this->incrementalOutputOffset); + } + + /** + * Adds a line to the STDERR stream. + * + * @internal + */ + public function addErrorOutput(string $line): void + { + $this->lastOutputTime = microtime(true); + + fseek($this->stderr, 0, \SEEK_END); + fwrite($this->stderr, $line); + fseek($this->stderr, $this->incrementalErrorOutputOffset); + } + + /** + * Gets the last output time in seconds. + */ + public function getLastOutputTime(): ?float + { + return $this->lastOutputTime; + } + + /** + * Gets the command line to be executed. + */ + public function getCommandLine(): string + { + return \is_array($this->commandline) ? implode(' ', array_map($this->escapeArgument(...), $this->commandline)) : $this->commandline; + } + + /** + * Gets the process timeout in seconds (max. runtime). + */ + public function getTimeout(): ?float + { + return $this->timeout; + } + + /** + * Gets the process idle timeout in seconds (max. time since last output). + */ + public function getIdleTimeout(): ?float + { + return $this->idleTimeout; + } + + /** + * Sets the process timeout (max. runtime) in seconds. + * + * To disable the timeout, set this value to null. + * + * @return $this + * + * @throws InvalidArgumentException if the timeout is negative + */ + public function setTimeout(?float $timeout): static + { + $this->timeout = $this->validateTimeout($timeout); + + return $this; + } + + /** + * Sets the process idle timeout (max. time since last output) in seconds. + * + * To disable the timeout, set this value to null. + * + * @return $this + * + * @throws LogicException if the output is disabled + * @throws InvalidArgumentException if the timeout is negative + */ + public function setIdleTimeout(?float $timeout): static + { + if (null !== $timeout && $this->outputDisabled) { + throw new LogicException('Idle timeout cannot be set while the output is disabled.'); + } + + $this->idleTimeout = $this->validateTimeout($timeout); + + return $this; + } + + /** + * Enables or disables the TTY mode. + * + * @return $this + * + * @throws RuntimeException In case the TTY mode is not supported + */ + public function setTty(bool $tty): static + { + if ('\\' === \DIRECTORY_SEPARATOR && $tty) { + throw new RuntimeException('TTY mode is not supported on Windows platform.'); + } + + if ($tty && !self::isTtySupported()) { + throw new RuntimeException('TTY mode requires /dev/tty to be read/writable.'); + } + + $this->tty = $tty; + + return $this; + } + + /** + * Checks if the TTY mode is enabled. + */ + public function isTty(): bool + { + return $this->tty; + } + + /** + * Sets PTY mode. + * + * @return $this + */ + public function setPty(bool $bool): static + { + $this->pty = $bool; + + return $this; + } + + /** + * Returns PTY state. + */ + public function isPty(): bool + { + return $this->pty; + } + + /** + * Gets the working directory. + */ + public function getWorkingDirectory(): ?string + { + if (null === $this->cwd) { + // getcwd() will return false if any one of the parent directories does not have + // the readable or search mode set, even if the current directory does + return getcwd() ?: null; + } + + return $this->cwd; + } + + /** + * Sets the current working directory. + * + * @return $this + */ + public function setWorkingDirectory(string $cwd): static + { + $this->cwd = $cwd; + + return $this; + } + + /** + * Gets the environment variables. + */ + public function getEnv(): array + { + return $this->env; + } + + /** + * Sets the environment variables. + * + * @param array $env The new environment variables + * + * @return $this + */ + public function setEnv(array $env): static + { + $this->env = $env; + + return $this; + } + + /** + * Gets the Process input. + * + * @return resource|string|\Iterator|null + */ + public function getInput() + { + return $this->input; + } + + /** + * Sets the input. + * + * This content will be passed to the underlying process standard input. + * + * @param string|resource|\Traversable|self|null $input The content + * + * @return $this + * + * @throws LogicException In case the process is running + */ + public function setInput(mixed $input): static + { + if ($this->isRunning()) { + throw new LogicException('Input cannot be set while the process is running.'); + } + + $this->input = ProcessUtils::validateInput(__METHOD__, $input); + + return $this; + } + + /** + * Performs a check between the timeout definition and the time the process started. + * + * In case you run a background process (with the start method), you should + * trigger this method regularly to ensure the process timeout + * + * @return void + * + * @throws ProcessTimedOutException In case the timeout was reached + */ + public function checkTimeout() + { + if (self::STATUS_STARTED !== $this->status) { + return; + } + + if (null !== $this->timeout && $this->timeout < microtime(true) - $this->starttime) { + $this->stop(0); + + throw new ProcessTimedOutException($this, ProcessTimedOutException::TYPE_GENERAL); + } + + if (null !== $this->idleTimeout && $this->idleTimeout < microtime(true) - $this->lastOutputTime) { + $this->stop(0); + + throw new ProcessTimedOutException($this, ProcessTimedOutException::TYPE_IDLE); + } + } + + /** + * @throws LogicException in case process is not started + */ + public function getStartTime(): float + { + if (!$this->isStarted()) { + throw new LogicException('Start time is only available after process start.'); + } + + return $this->starttime; + } + + /** + * Defines options to pass to the underlying proc_open(). + * + * @see https://php.net/proc_open for the options supported by PHP. + * + * Enabling the "create_new_console" option allows a subprocess to continue + * to run after the main process exited, on both Windows and *nix + * + * @return void + */ + public function setOptions(array $options) + { + if ($this->isRunning()) { + throw new RuntimeException('Setting options while the process is running is not possible.'); + } + + $defaultOptions = $this->options; + $existingOptions = ['blocking_pipes', 'create_process_group', 'create_new_console']; + + foreach ($options as $key => $value) { + if (!\in_array($key, $existingOptions)) { + $this->options = $defaultOptions; + throw new LogicException(sprintf('Invalid option "%s" passed to "%s()". Supported options are "%s".', $key, __METHOD__, implode('", "', $existingOptions))); + } + $this->options[$key] = $value; + } + } + + /** + * Returns whether TTY is supported on the current operating system. + */ + public static function isTtySupported(): bool + { + static $isTtySupported; + + return $isTtySupported ??= ('/' === \DIRECTORY_SEPARATOR && stream_isatty(\STDOUT) && @is_writable('/dev/tty')); + } + + /** + * Returns whether PTY is supported on the current operating system. + */ + public static function isPtySupported(): bool + { + static $result; + + if (null !== $result) { + return $result; + } + + if ('\\' === \DIRECTORY_SEPARATOR) { + return $result = false; + } + + return $result = (bool) @proc_open('echo 1 >/dev/null', [['pty'], ['pty'], ['pty']], $pipes); + } + + /** + * Creates the descriptors needed by the proc_open. + */ + private function getDescriptors(bool $hasCallback): array + { + if ($this->input instanceof \Iterator) { + $this->input->rewind(); + } + if ('\\' === \DIRECTORY_SEPARATOR) { + $this->processPipes = new WindowsPipes($this->input, !$this->outputDisabled || $hasCallback); + } else { + $this->processPipes = new UnixPipes($this->isTty(), $this->isPty(), $this->input, !$this->outputDisabled || $hasCallback); + } + + return $this->processPipes->getDescriptors(); + } + + /** + * Builds up the callback used by wait(). + * + * The callbacks adds all occurred output to the specific buffer and calls + * the user callback (if present) with the received output. + * + * @param callable|null $callback The user defined PHP callback + */ + protected function buildCallback(?callable $callback = null): \Closure + { + if ($this->outputDisabled) { + return fn ($type, $data): bool => null !== $callback && $callback($type, $data); + } + + $out = self::OUT; + + return function ($type, $data) use ($callback, $out): bool { + if ($out == $type) { + $this->addOutput($data); + } else { + $this->addErrorOutput($data); + } + + return null !== $callback && $callback($type, $data); + }; + } + + /** + * Updates the status of the process, reads pipes. + * + * @param bool $blocking Whether to use a blocking read call + * + * @return void + */ + protected function updateStatus(bool $blocking) + { + if (self::STATUS_STARTED !== $this->status) { + return; + } + + $this->processInformation = proc_get_status($this->process); + $running = $this->processInformation['running']; + + // In PHP < 8.3, "proc_get_status" only returns the correct exit status on the first call. + // Subsequent calls return -1 as the process is discarded. This workaround caches the first + // retrieved exit status for consistent results in later calls, mimicking PHP 8.3 behavior. + if (\PHP_VERSION_ID < 80300) { + if (!isset($this->cachedExitCode) && !$running && -1 !== $this->processInformation['exitcode']) { + $this->cachedExitCode = $this->processInformation['exitcode']; + } + + if (isset($this->cachedExitCode) && !$running && -1 === $this->processInformation['exitcode']) { + $this->processInformation['exitcode'] = $this->cachedExitCode; + } + } + + $this->readPipes($running && $blocking, '\\' !== \DIRECTORY_SEPARATOR || !$running); + + if ($this->fallbackStatus && $this->isSigchildEnabled()) { + $this->processInformation = $this->fallbackStatus + $this->processInformation; + } + + if (!$running) { + $this->close(); + } + } + + /** + * Returns whether PHP has been compiled with the '--enable-sigchild' option or not. + */ + protected function isSigchildEnabled(): bool + { + if (null !== self::$sigchild) { + return self::$sigchild; + } + + if (!\function_exists('phpinfo')) { + return self::$sigchild = false; + } + + ob_start(); + phpinfo(\INFO_GENERAL); + + return self::$sigchild = str_contains(ob_get_clean(), '--enable-sigchild'); + } + + /** + * Reads pipes for the freshest output. + * + * @param string $caller The name of the method that needs fresh outputs + * @param bool $blocking Whether to use blocking calls or not + * + * @throws LogicException in case output has been disabled or process is not started + */ + private function readPipesForOutput(string $caller, bool $blocking = false): void + { + if ($this->outputDisabled) { + throw new LogicException('Output has been disabled.'); + } + + $this->requireProcessIsStarted($caller); + + $this->updateStatus($blocking); + } + + /** + * Validates and returns the filtered timeout. + * + * @throws InvalidArgumentException if the given timeout is a negative number + */ + private function validateTimeout(?float $timeout): ?float + { + $timeout = (float) $timeout; + + if (0.0 === $timeout) { + $timeout = null; + } elseif ($timeout < 0) { + throw new InvalidArgumentException('The timeout value must be a valid positive integer or float number.'); + } + + return $timeout; + } + + /** + * Reads pipes, executes callback. + * + * @param bool $blocking Whether to use blocking calls or not + * @param bool $close Whether to close file handles or not + */ + private function readPipes(bool $blocking, bool $close): void + { + $result = $this->processPipes->readAndWrite($blocking, $close); + + $callback = $this->callback; + foreach ($result as $type => $data) { + if (3 !== $type) { + $callback(self::STDOUT === $type ? self::OUT : self::ERR, $data); + } elseif (!isset($this->fallbackStatus['signaled'])) { + $this->fallbackStatus['exitcode'] = (int) $data; + } + } + } + + /** + * Closes process resource, closes file handles, sets the exitcode. + * + * @return int The exitcode + */ + private function close(): int + { + $this->processPipes->close(); + if ($this->process) { + proc_close($this->process); + $this->process = null; + } + $this->exitcode = $this->processInformation['exitcode']; + $this->status = self::STATUS_TERMINATED; + + if (-1 === $this->exitcode) { + if ($this->processInformation['signaled'] && 0 < $this->processInformation['termsig']) { + // if process has been signaled, no exitcode but a valid termsig, apply Unix convention + $this->exitcode = 128 + $this->processInformation['termsig']; + } elseif ($this->isSigchildEnabled()) { + $this->processInformation['signaled'] = true; + $this->processInformation['termsig'] = -1; + } + } + + // Free memory from self-reference callback created by buildCallback + // Doing so in other contexts like __destruct or by garbage collector is ineffective + // Now pipes are closed, so the callback is no longer necessary + $this->callback = null; + + return $this->exitcode; + } + + /** + * Resets data related to the latest run of the process. + */ + private function resetProcessData(): void + { + $this->starttime = null; + $this->callback = null; + $this->exitcode = null; + $this->fallbackStatus = []; + $this->processInformation = []; + $this->stdout = fopen('php://temp/maxmemory:'.(1024 * 1024), 'w+'); + $this->stderr = fopen('php://temp/maxmemory:'.(1024 * 1024), 'w+'); + $this->process = null; + $this->latestSignal = null; + $this->status = self::STATUS_READY; + $this->incrementalOutputOffset = 0; + $this->incrementalErrorOutputOffset = 0; + } + + /** + * Sends a POSIX signal to the process. + * + * @param int $signal A valid POSIX signal (see https://php.net/pcntl.constants) + * @param bool $throwException Whether to throw exception in case signal failed + * + * @throws LogicException In case the process is not running + * @throws RuntimeException In case --enable-sigchild is activated and the process can't be killed + * @throws RuntimeException In case of failure + */ + private function doSignal(int $signal, bool $throwException): bool + { + if (null === $pid = $this->getPid()) { + if ($throwException) { + throw new LogicException('Cannot send signal on a non running process.'); + } + + return false; + } + + if ('\\' === \DIRECTORY_SEPARATOR) { + exec(sprintf('taskkill /F /T /PID %d 2>&1', $pid), $output, $exitCode); + if ($exitCode && $this->isRunning()) { + if ($throwException) { + throw new RuntimeException(sprintf('Unable to kill the process (%s).', implode(' ', $output))); + } + + return false; + } + } else { + if (!$this->isSigchildEnabled()) { + $ok = @proc_terminate($this->process, $signal); + } elseif (\function_exists('posix_kill')) { + $ok = @posix_kill($pid, $signal); + } elseif ($ok = proc_open(sprintf('kill -%d %d', $signal, $pid), [2 => ['pipe', 'w']], $pipes)) { + $ok = false === fgets($pipes[2]); + } + if (!$ok) { + if ($throwException) { + throw new RuntimeException(sprintf('Error while sending signal "%s".', $signal)); + } + + return false; + } + } + + $this->latestSignal = $signal; + $this->fallbackStatus['signaled'] = true; + $this->fallbackStatus['exitcode'] = -1; + $this->fallbackStatus['termsig'] = $this->latestSignal; + + return true; + } + + private function prepareWindowsCommandLine(string $cmd, array &$env): string + { + $uid = uniqid('', true); + $cmd = preg_replace_callback( + '/"(?:( + [^"%!^]*+ + (?: + (?: !LF! | "(?:\^[%!^])?+" ) + [^"%!^]*+ + )++ + ) | [^"]*+ )"/x', + function ($m) use (&$env, $uid) { + static $varCount = 0; + static $varCache = []; + if (!isset($m[1])) { + return $m[0]; + } + if (isset($varCache[$m[0]])) { + return $varCache[$m[0]]; + } + if (str_contains($value = $m[1], "\0")) { + $value = str_replace("\0", '?', $value); + } + if (false === strpbrk($value, "\"%!\n")) { + return '"'.$value.'"'; + } + + $value = str_replace(['!LF!', '"^!"', '"^%"', '"^^"', '""'], ["\n", '!', '%', '^', '"'], $value); + $value = '"'.preg_replace('/(\\\\*)"/', '$1$1\\"', $value).'"'; + $var = $uid.++$varCount; + + $env[$var] = $value; + + return $varCache[$m[0]] = '!'.$var.'!'; + }, + $cmd + ); + + static $comSpec; + + if (!$comSpec && $comSpec = (new ExecutableFinder())->find('cmd.exe')) { + // Escape according to CommandLineToArgvW rules + $comSpec = '"'.preg_replace('{(\\\\*+)"}', '$1$1\"', $comSpec) .'"'; + } + + $cmd = ($comSpec ?? 'cmd').' /V:ON /E:ON /D /C ('.str_replace("\n", ' ', $cmd).')'; + foreach ($this->processPipes->getFiles() as $offset => $filename) { + $cmd .= ' '.$offset.'>"'.$filename.'"'; + } + + return $cmd; + } + + /** + * Ensures the process is running or terminated, throws a LogicException if the process has a not started. + * + * @throws LogicException if the process has not run + */ + private function requireProcessIsStarted(string $functionName): void + { + if (!$this->isStarted()) { + throw new LogicException(sprintf('Process must be started before calling "%s()".', $functionName)); + } + } + + /** + * Ensures the process is terminated, throws a LogicException if the process has a status different than "terminated". + * + * @throws LogicException if the process is not yet terminated + */ + private function requireProcessIsTerminated(string $functionName): void + { + if (!$this->isTerminated()) { + throw new LogicException(sprintf('Process must be terminated before calling "%s()".', $functionName)); + } + } + + /** + * Escapes a string to be used as a shell argument. + */ + private function escapeArgument(?string $argument): string + { + if ('' === $argument || null === $argument) { + return '""'; + } + if ('\\' !== \DIRECTORY_SEPARATOR) { + return "'".str_replace("'", "'\\''", $argument)."'"; + } + if (str_contains($argument, "\0")) { + $argument = str_replace("\0", '?', $argument); + } + if (!preg_match('/[()%!^"<>&|\s]/', $argument)) { + return $argument; + } + $argument = preg_replace('/(\\\\+)$/', '$1$1', $argument); + + return '"'.str_replace(['"', '^', '%', '!', "\n"], ['""', '"^^"', '"^%"', '"^!"', '!LF!'], $argument).'"'; + } + + private function replacePlaceholders(string $commandline, array $env): string + { + return preg_replace_callback('/"\$\{:([_a-zA-Z]++[_a-zA-Z0-9]*+)\}"/', function ($matches) use ($commandline, $env) { + if (!isset($env[$matches[1]]) || false === $env[$matches[1]]) { + throw new InvalidArgumentException(sprintf('Command line is missing a value for parameter "%s": ', $matches[1]).$commandline); + } + + return $this->escapeArgument($env[$matches[1]]); + }, $commandline); + } + + private function getDefaultEnv(): array + { + $env = getenv(); + $env = ('\\' === \DIRECTORY_SEPARATOR ? array_intersect_ukey($env, $_SERVER, 'strcasecmp') : array_intersect_key($env, $_SERVER)) ?: $env; + + return $_ENV + ('\\' === \DIRECTORY_SEPARATOR ? array_diff_ukey($env, $_ENV, 'strcasecmp') : $env); + } +} diff --git a/3rdparty/symfony/process/ProcessUtils.php b/3rdparty/symfony/process/ProcessUtils.php new file mode 100644 index 00000000..092c5ccf --- /dev/null +++ b/3rdparty/symfony/process/ProcessUtils.php @@ -0,0 +1,64 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Component\Process; + +use Symfony\Component\Process\Exception\InvalidArgumentException; + +/** + * ProcessUtils is a bunch of utility methods. + * + * This class contains static methods only and is not meant to be instantiated. + * + * @author Martin Hasoň + */ +class ProcessUtils +{ + /** + * This class should not be instantiated. + */ + private function __construct() + { + } + + /** + * Validates and normalizes a Process input. + * + * @param string $caller The name of method call that validates the input + * @param mixed $input The input to validate + * + * @throws InvalidArgumentException In case the input is not valid + */ + public static function validateInput(string $caller, mixed $input): mixed + { + if (null !== $input) { + if (\is_resource($input)) { + return $input; + } + if (\is_scalar($input)) { + return (string) $input; + } + if ($input instanceof Process) { + return $input->getIterator($input::ITER_SKIP_ERR); + } + if ($input instanceof \Iterator) { + return $input; + } + if ($input instanceof \Traversable) { + return new \IteratorIterator($input); + } + + throw new InvalidArgumentException(sprintf('"%s" only accepts strings, Traversable objects or stream resources.', $caller)); + } + + return $input; + } +} diff --git a/3rdparty/symfony/routing/Alias.php b/3rdparty/symfony/routing/Alias.php new file mode 100644 index 00000000..7627f12c --- /dev/null +++ b/3rdparty/symfony/routing/Alias.php @@ -0,0 +1,93 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Component\Routing; + +use Symfony\Component\Routing\Exception\InvalidArgumentException; + +class Alias +{ + private string $id; + private array $deprecation = []; + + public function __construct(string $id) + { + $this->id = $id; + } + + public function withId(string $id): static + { + $new = clone $this; + + $new->id = $id; + + return $new; + } + + /** + * Returns the target name of this alias. + * + * @return string The target name + */ + public function getId(): string + { + return $this->id; + } + + /** + * Whether this alias is deprecated, that means it should not be referenced anymore. + * + * @param string $package The name of the composer package that is triggering the deprecation + * @param string $version The version of the package that introduced the deprecation + * @param string $message The deprecation message to use + * + * @return $this + * + * @throws InvalidArgumentException when the message template is invalid + */ + public function setDeprecated(string $package, string $version, string $message): static + { + if ('' !== $message) { + if (preg_match('#[\r\n]|\*/#', $message)) { + throw new InvalidArgumentException('Invalid characters found in deprecation template.'); + } + + if (!str_contains($message, '%alias_id%')) { + throw new InvalidArgumentException('The deprecation template must contain the "%alias_id%" placeholder.'); + } + } + + $this->deprecation = [ + 'package' => $package, + 'version' => $version, + 'message' => $message ?: 'The "%alias_id%" route alias is deprecated. You should stop using it, as it will be removed in the future.', + ]; + + return $this; + } + + public function isDeprecated(): bool + { + return (bool) $this->deprecation; + } + + /** + * @param string $name Route name relying on this alias + */ + public function getDeprecation(string $name): array + { + return [ + 'package' => $this->deprecation['package'], + 'version' => $this->deprecation['version'], + 'message' => str_replace('%alias_id%', $name, $this->deprecation['message']), + ]; + } +} diff --git a/3rdparty/symfony/routing/Annotation/Route.php b/3rdparty/symfony/routing/Annotation/Route.php new file mode 100644 index 00000000..dda3bdad --- /dev/null +++ b/3rdparty/symfony/routing/Annotation/Route.php @@ -0,0 +1,23 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Component\Routing\Annotation; + +// do not deprecate in 6.4/7.0, to make it easier for the ecosystem to support 6.4, 7.4 and 8.0 simultaneously + +class_exists(\Symfony\Component\Routing\Attribute\Route::class); + +if (false) { + #[\Attribute(\Attribute::IS_REPEATABLE | \Attribute::TARGET_CLASS | \Attribute::TARGET_METHOD)] + class Route extends \Symfony\Component\Routing\Attribute\Route + { + } +} diff --git a/3rdparty/symfony/routing/Attribute/Route.php b/3rdparty/symfony/routing/Attribute/Route.php new file mode 100644 index 00000000..a1d86fe6 --- /dev/null +++ b/3rdparty/symfony/routing/Attribute/Route.php @@ -0,0 +1,259 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Component\Routing\Attribute; + +/** + * Annotation class for @Route(). + * + * @Annotation + * @NamedArgumentConstructor + * @Target({"CLASS", "METHOD"}) + * + * @author Fabien Potencier + * @author Alexander M. Turek + */ +#[\Attribute(\Attribute::IS_REPEATABLE | \Attribute::TARGET_CLASS | \Attribute::TARGET_METHOD)] +class Route +{ + private ?string $path = null; + private array $localizedPaths = []; + private array $methods; + private array $schemes; + + /** + * @param array $requirements + * @param string[]|string $methods + * @param string[]|string $schemes + */ + public function __construct( + string|array|null $path = null, + private ?string $name = null, + private array $requirements = [], + private array $options = [], + private array $defaults = [], + private ?string $host = null, + array|string $methods = [], + array|string $schemes = [], + private ?string $condition = null, + private ?int $priority = null, + ?string $locale = null, + ?string $format = null, + ?bool $utf8 = null, + ?bool $stateless = null, + private ?string $env = null + ) { + if (\is_array($path)) { + $this->localizedPaths = $path; + } else { + $this->path = $path; + } + $this->setMethods($methods); + $this->setSchemes($schemes); + + if (null !== $locale) { + $this->defaults['_locale'] = $locale; + } + + if (null !== $format) { + $this->defaults['_format'] = $format; + } + + if (null !== $utf8) { + $this->options['utf8'] = $utf8; + } + + if (null !== $stateless) { + $this->defaults['_stateless'] = $stateless; + } + } + + /** + * @return void + */ + public function setPath(string $path) + { + $this->path = $path; + } + + /** + * @return string|null + */ + public function getPath() + { + return $this->path; + } + + /** + * @return void + */ + public function setLocalizedPaths(array $localizedPaths) + { + $this->localizedPaths = $localizedPaths; + } + + public function getLocalizedPaths(): array + { + return $this->localizedPaths; + } + + /** + * @return void + */ + public function setHost(string $pattern) + { + $this->host = $pattern; + } + + /** + * @return string|null + */ + public function getHost() + { + return $this->host; + } + + /** + * @return void + */ + public function setName(string $name) + { + $this->name = $name; + } + + /** + * @return string|null + */ + public function getName() + { + return $this->name; + } + + /** + * @return void + */ + public function setRequirements(array $requirements) + { + $this->requirements = $requirements; + } + + /** + * @return array + */ + public function getRequirements() + { + return $this->requirements; + } + + /** + * @return void + */ + public function setOptions(array $options) + { + $this->options = $options; + } + + /** + * @return array + */ + public function getOptions() + { + return $this->options; + } + + /** + * @return void + */ + public function setDefaults(array $defaults) + { + $this->defaults = $defaults; + } + + /** + * @return array + */ + public function getDefaults() + { + return $this->defaults; + } + + /** + * @return void + */ + public function setSchemes(array|string $schemes) + { + $this->schemes = (array) $schemes; + } + + /** + * @return array + */ + public function getSchemes() + { + return $this->schemes; + } + + /** + * @return void + */ + public function setMethods(array|string $methods) + { + $this->methods = (array) $methods; + } + + /** + * @return array + */ + public function getMethods() + { + return $this->methods; + } + + /** + * @return void + */ + public function setCondition(?string $condition) + { + $this->condition = $condition; + } + + /** + * @return string|null + */ + public function getCondition() + { + return $this->condition; + } + + public function setPriority(int $priority): void + { + $this->priority = $priority; + } + + public function getPriority(): ?int + { + return $this->priority; + } + + public function setEnv(?string $env): void + { + $this->env = $env; + } + + public function getEnv(): ?string + { + return $this->env; + } +} + +if (!class_exists(\Symfony\Component\Routing\Annotation\Route::class, false)) { + class_alias(Route::class, \Symfony\Component\Routing\Annotation\Route::class); +} diff --git a/3rdparty/symfony/routing/CompiledRoute.php b/3rdparty/symfony/routing/CompiledRoute.php new file mode 100644 index 00000000..03215e36 --- /dev/null +++ b/3rdparty/symfony/routing/CompiledRoute.php @@ -0,0 +1,157 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Component\Routing; + +/** + * CompiledRoutes are returned by the RouteCompiler class. + * + * @author Fabien Potencier + */ +class CompiledRoute implements \Serializable +{ + private array $variables; + private array $tokens; + private string $staticPrefix; + private string $regex; + private array $pathVariables; + private array $hostVariables; + private ?string $hostRegex; + private array $hostTokens; + + /** + * @param string $staticPrefix The static prefix of the compiled route + * @param string $regex The regular expression to use to match this route + * @param array $tokens An array of tokens to use to generate URL for this route + * @param array $pathVariables An array of path variables + * @param string|null $hostRegex Host regex + * @param array $hostTokens Host tokens + * @param array $hostVariables An array of host variables + * @param array $variables An array of variables (variables defined in the path and in the host patterns) + */ + public function __construct(string $staticPrefix, string $regex, array $tokens, array $pathVariables, ?string $hostRegex = null, array $hostTokens = [], array $hostVariables = [], array $variables = []) + { + $this->staticPrefix = $staticPrefix; + $this->regex = $regex; + $this->tokens = $tokens; + $this->pathVariables = $pathVariables; + $this->hostRegex = $hostRegex; + $this->hostTokens = $hostTokens; + $this->hostVariables = $hostVariables; + $this->variables = $variables; + } + + public function __serialize(): array + { + return [ + 'vars' => $this->variables, + 'path_prefix' => $this->staticPrefix, + 'path_regex' => $this->regex, + 'path_tokens' => $this->tokens, + 'path_vars' => $this->pathVariables, + 'host_regex' => $this->hostRegex, + 'host_tokens' => $this->hostTokens, + 'host_vars' => $this->hostVariables, + ]; + } + + /** + * @internal + */ + final public function serialize(): string + { + throw new \BadMethodCallException('Cannot serialize '.__CLASS__); + } + + public function __unserialize(array $data): void + { + $this->variables = $data['vars']; + $this->staticPrefix = $data['path_prefix']; + $this->regex = $data['path_regex']; + $this->tokens = $data['path_tokens']; + $this->pathVariables = $data['path_vars']; + $this->hostRegex = $data['host_regex']; + $this->hostTokens = $data['host_tokens']; + $this->hostVariables = $data['host_vars']; + } + + /** + * @internal + */ + final public function unserialize(string $serialized): void + { + $this->__unserialize(unserialize($serialized, ['allowed_classes' => false])); + } + + /** + * Returns the static prefix. + */ + public function getStaticPrefix(): string + { + return $this->staticPrefix; + } + + /** + * Returns the regex. + */ + public function getRegex(): string + { + return $this->regex; + } + + /** + * Returns the host regex. + */ + public function getHostRegex(): ?string + { + return $this->hostRegex; + } + + /** + * Returns the tokens. + */ + public function getTokens(): array + { + return $this->tokens; + } + + /** + * Returns the host tokens. + */ + public function getHostTokens(): array + { + return $this->hostTokens; + } + + /** + * Returns the variables. + */ + public function getVariables(): array + { + return $this->variables; + } + + /** + * Returns the path variables. + */ + public function getPathVariables(): array + { + return $this->pathVariables; + } + + /** + * Returns the host variables. + */ + public function getHostVariables(): array + { + return $this->hostVariables; + } +} diff --git a/3rdparty/symfony/routing/DependencyInjection/AddExpressionLanguageProvidersPass.php b/3rdparty/symfony/routing/DependencyInjection/AddExpressionLanguageProvidersPass.php new file mode 100644 index 00000000..619fa67f --- /dev/null +++ b/3rdparty/symfony/routing/DependencyInjection/AddExpressionLanguageProvidersPass.php @@ -0,0 +1,36 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Component\Routing\DependencyInjection; + +use Symfony\Component\DependencyInjection\Compiler\CompilerPassInterface; +use Symfony\Component\DependencyInjection\ContainerBuilder; +use Symfony\Component\DependencyInjection\Reference; + +/** + * Registers the expression language providers. + * + * @author Fabien Potencier + */ +class AddExpressionLanguageProvidersPass implements CompilerPassInterface +{ + public function process(ContainerBuilder $container): void + { + if (!$container->has('router.default')) { + return; + } + + $definition = $container->findDefinition('router.default'); + foreach ($container->findTaggedServiceIds('routing.expression_language_provider', true) as $id => $attributes) { + $definition->addMethodCall('addExpressionLanguageProvider', [new Reference($id)]); + } + } +} diff --git a/3rdparty/symfony/routing/DependencyInjection/RoutingResolverPass.php b/3rdparty/symfony/routing/DependencyInjection/RoutingResolverPass.php new file mode 100644 index 00000000..edbecc1f --- /dev/null +++ b/3rdparty/symfony/routing/DependencyInjection/RoutingResolverPass.php @@ -0,0 +1,43 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Component\Routing\DependencyInjection; + +use Symfony\Component\DependencyInjection\Compiler\CompilerPassInterface; +use Symfony\Component\DependencyInjection\Compiler\PriorityTaggedServiceTrait; +use Symfony\Component\DependencyInjection\ContainerBuilder; +use Symfony\Component\DependencyInjection\Reference; + +/** + * Adds tagged routing.loader services to routing.resolver service. + * + * @author Fabien Potencier + */ +class RoutingResolverPass implements CompilerPassInterface +{ + use PriorityTaggedServiceTrait; + + /** + * @return void + */ + public function process(ContainerBuilder $container) + { + if (false === $container->hasDefinition('routing.resolver')) { + return; + } + + $definition = $container->getDefinition('routing.resolver'); + + foreach ($this->findAndSortTaggedServices('routing.loader', $container) as $id) { + $definition->addMethodCall('addLoader', [new Reference($id)]); + } + } +} diff --git a/3rdparty/symfony/routing/Exception/ExceptionInterface.php b/3rdparty/symfony/routing/Exception/ExceptionInterface.php new file mode 100644 index 00000000..22e72b16 --- /dev/null +++ b/3rdparty/symfony/routing/Exception/ExceptionInterface.php @@ -0,0 +1,21 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Component\Routing\Exception; + +/** + * ExceptionInterface. + * + * @author Alexandre Salomé + */ +interface ExceptionInterface extends \Throwable +{ +} diff --git a/3rdparty/symfony/routing/Exception/InvalidArgumentException.php b/3rdparty/symfony/routing/Exception/InvalidArgumentException.php new file mode 100644 index 00000000..950b9b15 --- /dev/null +++ b/3rdparty/symfony/routing/Exception/InvalidArgumentException.php @@ -0,0 +1,16 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Component\Routing\Exception; + +class InvalidArgumentException extends \InvalidArgumentException implements ExceptionInterface +{ +} diff --git a/3rdparty/symfony/routing/Exception/InvalidParameterException.php b/3rdparty/symfony/routing/Exception/InvalidParameterException.php new file mode 100644 index 00000000..94d841f4 --- /dev/null +++ b/3rdparty/symfony/routing/Exception/InvalidParameterException.php @@ -0,0 +1,21 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Component\Routing\Exception; + +/** + * Exception thrown when a parameter is not valid. + * + * @author Alexandre Salomé + */ +class InvalidParameterException extends \InvalidArgumentException implements ExceptionInterface +{ +} diff --git a/3rdparty/symfony/routing/Exception/MethodNotAllowedException.php b/3rdparty/symfony/routing/Exception/MethodNotAllowedException.php new file mode 100644 index 00000000..c96ae9b1 --- /dev/null +++ b/3rdparty/symfony/routing/Exception/MethodNotAllowedException.php @@ -0,0 +1,44 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Component\Routing\Exception; + +/** + * The resource was found but the request method is not allowed. + * + * This exception should trigger an HTTP 405 response in your application code. + * + * @author Kris Wallsmith + */ +class MethodNotAllowedException extends \RuntimeException implements ExceptionInterface +{ + protected $allowedMethods = []; + + /** + * @param string[] $allowedMethods + */ + public function __construct(array $allowedMethods, string $message = '', int $code = 0, ?\Throwable $previous = null) + { + $this->allowedMethods = array_map('strtoupper', $allowedMethods); + + parent::__construct($message, $code, $previous); + } + + /** + * Gets the allowed HTTP methods. + * + * @return string[] + */ + public function getAllowedMethods(): array + { + return $this->allowedMethods; + } +} diff --git a/3rdparty/symfony/routing/Exception/MissingMandatoryParametersException.php b/3rdparty/symfony/routing/Exception/MissingMandatoryParametersException.php new file mode 100644 index 00000000..72d063ab --- /dev/null +++ b/3rdparty/symfony/routing/Exception/MissingMandatoryParametersException.php @@ -0,0 +1,57 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Component\Routing\Exception; + +/** + * Exception thrown when a route cannot be generated because of missing + * mandatory parameters. + * + * @author Alexandre Salomé + */ +class MissingMandatoryParametersException extends \InvalidArgumentException implements ExceptionInterface +{ + private string $routeName = ''; + private array $missingParameters = []; + + /** + * @param string[] $missingParameters + * @param int $code + */ + public function __construct(string $routeName = '', $missingParameters = null, $code = 0, ?\Throwable $previous = null) + { + if (\is_array($missingParameters)) { + $this->routeName = $routeName; + $this->missingParameters = $missingParameters; + $message = sprintf('Some mandatory parameters are missing ("%s") to generate a URL for route "%s".', implode('", "', $missingParameters), $routeName); + } else { + trigger_deprecation('symfony/routing', '6.1', 'Construction of "%s" with an exception message is deprecated, provide the route name and an array of missing parameters instead.', __CLASS__); + $message = $routeName; + $previous = $code instanceof \Throwable ? $code : null; + $code = (int) $missingParameters; + } + + parent::__construct($message, $code, $previous); + } + + /** + * @return string[] + */ + public function getMissingParameters(): array + { + return $this->missingParameters; + } + + public function getRouteName(): string + { + return $this->routeName; + } +} diff --git a/3rdparty/symfony/routing/Exception/NoConfigurationException.php b/3rdparty/symfony/routing/Exception/NoConfigurationException.php new file mode 100644 index 00000000..333bc743 --- /dev/null +++ b/3rdparty/symfony/routing/Exception/NoConfigurationException.php @@ -0,0 +1,21 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Component\Routing\Exception; + +/** + * Exception thrown when no routes are configured. + * + * @author Yonel Ceruto + */ +class NoConfigurationException extends ResourceNotFoundException +{ +} diff --git a/3rdparty/symfony/routing/Exception/ResourceNotFoundException.php b/3rdparty/symfony/routing/Exception/ResourceNotFoundException.php new file mode 100644 index 00000000..ccbca152 --- /dev/null +++ b/3rdparty/symfony/routing/Exception/ResourceNotFoundException.php @@ -0,0 +1,23 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Component\Routing\Exception; + +/** + * The resource was not found. + * + * This exception should trigger an HTTP 404 response in your application code. + * + * @author Kris Wallsmith + */ +class ResourceNotFoundException extends \RuntimeException implements ExceptionInterface +{ +} diff --git a/3rdparty/symfony/routing/Exception/RouteCircularReferenceException.php b/3rdparty/symfony/routing/Exception/RouteCircularReferenceException.php new file mode 100644 index 00000000..841e3598 --- /dev/null +++ b/3rdparty/symfony/routing/Exception/RouteCircularReferenceException.php @@ -0,0 +1,20 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Component\Routing\Exception; + +class RouteCircularReferenceException extends RuntimeException +{ + public function __construct(string $routeId, array $path) + { + parent::__construct(sprintf('Circular reference detected for route "%s", path: "%s".', $routeId, implode(' -> ', $path))); + } +} diff --git a/3rdparty/symfony/routing/Exception/RouteNotFoundException.php b/3rdparty/symfony/routing/Exception/RouteNotFoundException.php new file mode 100644 index 00000000..24ab0b44 --- /dev/null +++ b/3rdparty/symfony/routing/Exception/RouteNotFoundException.php @@ -0,0 +1,21 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Component\Routing\Exception; + +/** + * Exception thrown when a route does not exist. + * + * @author Alexandre Salomé + */ +class RouteNotFoundException extends \InvalidArgumentException implements ExceptionInterface +{ +} diff --git a/3rdparty/symfony/routing/Exception/RuntimeException.php b/3rdparty/symfony/routing/Exception/RuntimeException.php new file mode 100644 index 00000000..48da62ec --- /dev/null +++ b/3rdparty/symfony/routing/Exception/RuntimeException.php @@ -0,0 +1,16 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Component\Routing\Exception; + +class RuntimeException extends \RuntimeException implements ExceptionInterface +{ +} diff --git a/3rdparty/symfony/routing/Generator/CompiledUrlGenerator.php b/3rdparty/symfony/routing/Generator/CompiledUrlGenerator.php new file mode 100644 index 00000000..de209cdc --- /dev/null +++ b/3rdparty/symfony/routing/Generator/CompiledUrlGenerator.php @@ -0,0 +1,69 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Component\Routing\Generator; + +use Psr\Log\LoggerInterface; +use Symfony\Component\Routing\Exception\RouteNotFoundException; +use Symfony\Component\Routing\RequestContext; + +/** + * Generates URLs based on rules dumped by CompiledUrlGeneratorDumper. + */ +class CompiledUrlGenerator extends UrlGenerator +{ + private array $compiledRoutes = []; + private ?string $defaultLocale; + + public function __construct(array $compiledRoutes, RequestContext $context, ?LoggerInterface $logger = null, ?string $defaultLocale = null) + { + $this->compiledRoutes = $compiledRoutes; + $this->context = $context; + $this->logger = $logger; + $this->defaultLocale = $defaultLocale; + } + + public function generate(string $name, array $parameters = [], int $referenceType = self::ABSOLUTE_PATH): string + { + $locale = $parameters['_locale'] + ?? $this->context->getParameter('_locale') + ?: $this->defaultLocale; + + if (null !== $locale) { + do { + if (($this->compiledRoutes[$name.'.'.$locale][1]['_canonical_route'] ?? null) === $name) { + $name .= '.'.$locale; + break; + } + } while (false !== $locale = strstr($locale, '_', true)); + } + + if (!isset($this->compiledRoutes[$name])) { + throw new RouteNotFoundException(sprintf('Unable to generate a URL for the named route "%s" as such route does not exist.', $name)); + } + + [$variables, $defaults, $requirements, $tokens, $hostTokens, $requiredSchemes, $deprecations] = $this->compiledRoutes[$name] + [6 => []]; + + foreach ($deprecations as $deprecation) { + trigger_deprecation($deprecation['package'], $deprecation['version'], $deprecation['message']); + } + + if (isset($defaults['_canonical_route']) && isset($defaults['_locale'])) { + if (!\in_array('_locale', $variables, true)) { + unset($parameters['_locale']); + } elseif (!isset($parameters['_locale'])) { + $parameters['_locale'] = $defaults['_locale']; + } + } + + return $this->doGenerate($variables, $defaults, $requirements, $tokens, $parameters, $name, $referenceType, $hostTokens, $requiredSchemes); + } +} diff --git a/3rdparty/symfony/routing/Generator/ConfigurableRequirementsInterface.php b/3rdparty/symfony/routing/Generator/ConfigurableRequirementsInterface.php new file mode 100644 index 00000000..cbbbf045 --- /dev/null +++ b/3rdparty/symfony/routing/Generator/ConfigurableRequirementsInterface.php @@ -0,0 +1,53 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Component\Routing\Generator; + +/** + * ConfigurableRequirementsInterface must be implemented by URL generators that + * can be configured whether an exception should be generated when the parameters + * do not match the requirements. It is also possible to disable the requirements + * check for URL generation completely. + * + * The possible configurations and use-cases: + * - setStrictRequirements(true): Throw an exception for mismatching requirements. This + * is mostly useful in development environment. + * - setStrictRequirements(false): Don't throw an exception but return an empty string as URL for + * mismatching requirements and log the problem. Useful when you cannot control all + * params because they come from third party libs but don't want to have a 404 in + * production environment. It should log the mismatch so one can review it. + * - setStrictRequirements(null): Return the URL with the given parameters without + * checking the requirements at all. When generating a URL you should either trust + * your params or you validated them beforehand because otherwise it would break your + * link anyway. So in production environment you should know that params always pass + * the requirements. Thus this option allows to disable the check on URL generation for + * performance reasons (saving a preg_match for each requirement every time a URL is + * generated). + * + * @author Fabien Potencier + * @author Tobias Schultze + */ +interface ConfigurableRequirementsInterface +{ + /** + * Enables or disables the exception on incorrect parameters. + * Passing null will deactivate the requirements check completely. + * + * @return void + */ + public function setStrictRequirements(?bool $enabled); + + /** + * Returns whether to throw an exception on incorrect parameters. + * Null means the requirements check is deactivated completely. + */ + public function isStrictRequirements(): ?bool; +} diff --git a/3rdparty/symfony/routing/Generator/Dumper/CompiledUrlGeneratorDumper.php b/3rdparty/symfony/routing/Generator/Dumper/CompiledUrlGeneratorDumper.php new file mode 100644 index 00000000..1144fed5 --- /dev/null +++ b/3rdparty/symfony/routing/Generator/Dumper/CompiledUrlGeneratorDumper.php @@ -0,0 +1,121 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Component\Routing\Generator\Dumper; + +use Symfony\Component\Routing\Exception\RouteCircularReferenceException; +use Symfony\Component\Routing\Exception\RouteNotFoundException; +use Symfony\Component\Routing\Matcher\Dumper\CompiledUrlMatcherDumper; + +/** + * CompiledUrlGeneratorDumper creates a PHP array to be used with CompiledUrlGenerator. + * + * @author Fabien Potencier + * @author Tobias Schultze + * @author Nicolas Grekas + */ +class CompiledUrlGeneratorDumper extends GeneratorDumper +{ + public function getCompiledRoutes(): array + { + $compiledRoutes = []; + foreach ($this->getRoutes()->all() as $name => $route) { + $compiledRoute = $route->compile(); + + $compiledRoutes[$name] = [ + $compiledRoute->getVariables(), + $route->getDefaults(), + $route->getRequirements(), + $compiledRoute->getTokens(), + $compiledRoute->getHostTokens(), + $route->getSchemes(), + [], + ]; + } + + return $compiledRoutes; + } + + public function getCompiledAliases(): array + { + $routes = $this->getRoutes(); + $compiledAliases = []; + foreach ($routes->getAliases() as $name => $alias) { + $deprecations = $alias->isDeprecated() ? [$alias->getDeprecation($name)] : []; + $currentId = $alias->getId(); + $visited = []; + while (null !== $alias = $routes->getAlias($currentId) ?? null) { + if (false !== $searchKey = array_search($currentId, $visited)) { + $visited[] = $currentId; + + throw new RouteCircularReferenceException($currentId, \array_slice($visited, $searchKey)); + } + + if ($alias->isDeprecated()) { + $deprecations[] = $deprecation = $alias->getDeprecation($currentId); + trigger_deprecation($deprecation['package'], $deprecation['version'], $deprecation['message']); + } + + $visited[] = $currentId; + $currentId = $alias->getId(); + } + + if (null === $target = $routes->get($currentId)) { + throw new RouteNotFoundException(sprintf('Target route "%s" for alias "%s" does not exist.', $currentId, $name)); + } + + $compiledTarget = $target->compile(); + + $compiledAliases[$name] = [ + $compiledTarget->getVariables(), + $target->getDefaults(), + $target->getRequirements(), + $compiledTarget->getTokens(), + $compiledTarget->getHostTokens(), + $target->getSchemes(), + $deprecations, + ]; + } + + return $compiledAliases; + } + + public function dump(array $options = []): string + { + return <<generateDeclaredRoutes()} +]; + +EOF; + } + + /** + * Generates PHP code representing an array of defined routes + * together with the routes properties (e.g. requirements). + */ + private function generateDeclaredRoutes(): string + { + $routes = ''; + foreach ($this->getCompiledRoutes() as $name => $properties) { + $routes .= sprintf("\n '%s' => %s,", $name, CompiledUrlMatcherDumper::export($properties)); + } + + foreach ($this->getCompiledAliases() as $alias => $properties) { + $routes .= sprintf("\n '%s' => %s,", $alias, CompiledUrlMatcherDumper::export($properties)); + } + + return $routes; + } +} diff --git a/3rdparty/symfony/routing/Generator/Dumper/GeneratorDumper.php b/3rdparty/symfony/routing/Generator/Dumper/GeneratorDumper.php new file mode 100644 index 00000000..b82ff97b --- /dev/null +++ b/3rdparty/symfony/routing/Generator/Dumper/GeneratorDumper.php @@ -0,0 +1,34 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Component\Routing\Generator\Dumper; + +use Symfony\Component\Routing\RouteCollection; + +/** + * GeneratorDumper is the base class for all built-in generator dumpers. + * + * @author Fabien Potencier + */ +abstract class GeneratorDumper implements GeneratorDumperInterface +{ + private RouteCollection $routes; + + public function __construct(RouteCollection $routes) + { + $this->routes = $routes; + } + + public function getRoutes(): RouteCollection + { + return $this->routes; + } +} diff --git a/3rdparty/symfony/routing/Generator/Dumper/GeneratorDumperInterface.php b/3rdparty/symfony/routing/Generator/Dumper/GeneratorDumperInterface.php new file mode 100644 index 00000000..d3294ce2 --- /dev/null +++ b/3rdparty/symfony/routing/Generator/Dumper/GeneratorDumperInterface.php @@ -0,0 +1,33 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Component\Routing\Generator\Dumper; + +use Symfony\Component\Routing\RouteCollection; + +/** + * GeneratorDumperInterface is the interface that all generator dumper classes must implement. + * + * @author Fabien Potencier + */ +interface GeneratorDumperInterface +{ + /** + * Dumps a set of routes to a string representation of executable code + * that can then be used to generate a URL of such a route. + */ + public function dump(array $options = []): string; + + /** + * Gets the routes to dump. + */ + public function getRoutes(): RouteCollection; +} diff --git a/3rdparty/symfony/routing/Generator/UrlGenerator.php b/3rdparty/symfony/routing/Generator/UrlGenerator.php new file mode 100644 index 00000000..28f30d61 --- /dev/null +++ b/3rdparty/symfony/routing/Generator/UrlGenerator.php @@ -0,0 +1,358 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Component\Routing\Generator; + +use Psr\Log\LoggerInterface; +use Symfony\Component\Routing\Exception\InvalidParameterException; +use Symfony\Component\Routing\Exception\MissingMandatoryParametersException; +use Symfony\Component\Routing\Exception\RouteNotFoundException; +use Symfony\Component\Routing\RequestContext; +use Symfony\Component\Routing\RouteCollection; + +/** + * UrlGenerator can generate a URL or a path for any route in the RouteCollection + * based on the passed parameters. + * + * @author Fabien Potencier + * @author Tobias Schultze + */ +class UrlGenerator implements UrlGeneratorInterface, ConfigurableRequirementsInterface +{ + private const QUERY_FRAGMENT_DECODED = [ + // RFC 3986 explicitly allows those in the query/fragment to reference other URIs unencoded + '%2F' => '/', + '%252F' => '%2F', + '%3F' => '?', + // reserved chars that have no special meaning for HTTP URIs in a query or fragment + // this excludes esp. "&", "=" and also "+" because PHP would treat it as a space (form-encoded) + '%40' => '@', + '%3A' => ':', + '%21' => '!', + '%3B' => ';', + '%2C' => ',', + '%2A' => '*', + ]; + + protected $routes; + protected $context; + + /** + * @var bool|null + */ + protected $strictRequirements = true; + + protected $logger; + + private ?string $defaultLocale; + + /** + * This array defines the characters (besides alphanumeric ones) that will not be percent-encoded in the path segment of the generated URL. + * + * PHP's rawurlencode() encodes all chars except "a-zA-Z0-9-._~" according to RFC 3986. But we want to allow some chars + * to be used in their literal form (reasons below). Other chars inside the path must of course be encoded, e.g. + * "?" and "#" (would be interpreted wrongly as query and fragment identifier), + * "'" and """ (are used as delimiters in HTML). + */ + protected $decodedChars = [ + // the slash can be used to designate a hierarchical structure and we want allow using it with this meaning + // some webservers don't allow the slash in encoded form in the path for security reasons anyway + // see http://stackoverflow.com/questions/4069002/http-400-if-2f-part-of-get-url-in-jboss + '%2F' => '/', + '%252F' => '%2F', + // the following chars are general delimiters in the URI specification but have only special meaning in the authority component + // so they can safely be used in the path in unencoded form + '%40' => '@', + '%3A' => ':', + // these chars are only sub-delimiters that have no predefined meaning and can therefore be used literally + // so URI producing applications can use these chars to delimit subcomponents in a path segment without being encoded for better readability + '%3B' => ';', + '%2C' => ',', + '%3D' => '=', + '%2B' => '+', + '%21' => '!', + '%2A' => '*', + '%7C' => '|', + ]; + + public function __construct(RouteCollection $routes, RequestContext $context, ?LoggerInterface $logger = null, ?string $defaultLocale = null) + { + $this->routes = $routes; + $this->context = $context; + $this->logger = $logger; + $this->defaultLocale = $defaultLocale; + } + + /** + * @return void + */ + public function setContext(RequestContext $context) + { + $this->context = $context; + } + + public function getContext(): RequestContext + { + return $this->context; + } + + /** + * @return void + */ + public function setStrictRequirements(?bool $enabled) + { + $this->strictRequirements = $enabled; + } + + public function isStrictRequirements(): ?bool + { + return $this->strictRequirements; + } + + public function generate(string $name, array $parameters = [], int $referenceType = self::ABSOLUTE_PATH): string + { + $route = null; + $locale = $parameters['_locale'] ?? $this->context->getParameter('_locale') ?: $this->defaultLocale; + + if (null !== $locale) { + do { + if (null !== ($route = $this->routes->get($name.'.'.$locale)) && $route->getDefault('_canonical_route') === $name) { + break; + } + } while (false !== $locale = strstr($locale, '_', true)); + } + + if (null === $route ??= $this->routes->get($name)) { + throw new RouteNotFoundException(sprintf('Unable to generate a URL for the named route "%s" as such route does not exist.', $name)); + } + + // the Route has a cache of its own and is not recompiled as long as it does not get modified + $compiledRoute = $route->compile(); + + $defaults = $route->getDefaults(); + $variables = $compiledRoute->getVariables(); + + if (isset($defaults['_canonical_route']) && isset($defaults['_locale'])) { + if (!\in_array('_locale', $variables, true)) { + unset($parameters['_locale']); + } elseif (!isset($parameters['_locale'])) { + $parameters['_locale'] = $defaults['_locale']; + } + } + + return $this->doGenerate($variables, $defaults, $route->getRequirements(), $compiledRoute->getTokens(), $parameters, $name, $referenceType, $compiledRoute->getHostTokens(), $route->getSchemes()); + } + + /** + * @throws MissingMandatoryParametersException When some parameters are missing that are mandatory for the route + * @throws InvalidParameterException When a parameter value for a placeholder is not correct because + * it does not match the requirement + */ + protected function doGenerate(array $variables, array $defaults, array $requirements, array $tokens, array $parameters, string $name, int $referenceType, array $hostTokens, array $requiredSchemes = []): string + { + $variables = array_flip($variables); + $mergedParams = array_replace($defaults, $this->context->getParameters(), $parameters); + + // all params must be given + if ($diff = array_diff_key($variables, $mergedParams)) { + throw new MissingMandatoryParametersException($name, array_keys($diff)); + } + + $url = ''; + $optional = true; + $message = 'Parameter "{parameter}" for route "{route}" must match "{expected}" ("{given}" given) to generate a corresponding URL.'; + foreach ($tokens as $token) { + if ('variable' === $token[0]) { + $varName = $token[3]; + // variable is not important by default + $important = $token[5] ?? false; + + if (!$optional || $important || !\array_key_exists($varName, $defaults) || (null !== $mergedParams[$varName] && (string) $mergedParams[$varName] !== (string) $defaults[$varName])) { + // check requirement (while ignoring look-around patterns) + if (null !== $this->strictRequirements && !preg_match('#^'.preg_replace('/\(\?(?:=|<=|!|strictRequirements) { + throw new InvalidParameterException(strtr($message, ['{parameter}' => $varName, '{route}' => $name, '{expected}' => $token[2], '{given}' => $mergedParams[$varName]])); + } + + $this->logger?->error($message, ['parameter' => $varName, 'route' => $name, 'expected' => $token[2], 'given' => $mergedParams[$varName]]); + + return ''; + } + + $url = $token[1].$mergedParams[$varName].$url; + $optional = false; + } + } else { + // static text + $url = $token[1].$url; + $optional = false; + } + } + + if ('' === $url) { + $url = '/'; + } + + // the contexts base URL is already encoded (see Symfony\Component\HttpFoundation\Request) + $url = strtr(rawurlencode($url), $this->decodedChars); + + // the path segments "." and ".." are interpreted as relative reference when resolving a URI; see http://tools.ietf.org/html/rfc3986#section-3.3 + // so we need to encode them as they are not used for this purpose here + // otherwise we would generate a URI that, when followed by a user agent (e.g. browser), does not match this route + $url = strtr($url, ['/../' => '/%2E%2E/', '/./' => '/%2E/']); + if (str_ends_with($url, '/..')) { + $url = substr($url, 0, -2).'%2E%2E'; + } elseif (str_ends_with($url, '/.')) { + $url = substr($url, 0, -1).'%2E'; + } + + $schemeAuthority = ''; + $host = $this->context->getHost(); + $scheme = $this->context->getScheme(); + + if ($requiredSchemes) { + if (!\in_array($scheme, $requiredSchemes, true)) { + $referenceType = self::ABSOLUTE_URL; + $scheme = current($requiredSchemes); + } + } + + if ($hostTokens) { + $routeHost = ''; + foreach ($hostTokens as $token) { + if ('variable' === $token[0]) { + // check requirement (while ignoring look-around patterns) + if (null !== $this->strictRequirements && !preg_match('#^'.preg_replace('/\(\?(?:=|<=|!|strictRequirements) { + throw new InvalidParameterException(strtr($message, ['{parameter}' => $token[3], '{route}' => $name, '{expected}' => $token[2], '{given}' => $mergedParams[$token[3]]])); + } + + $this->logger?->error($message, ['parameter' => $token[3], 'route' => $name, 'expected' => $token[2], 'given' => $mergedParams[$token[3]]]); + + return ''; + } + + $routeHost = $token[1].$mergedParams[$token[3]].$routeHost; + } else { + $routeHost = $token[1].$routeHost; + } + } + + if ($routeHost !== $host) { + $host = $routeHost; + if (self::ABSOLUTE_URL !== $referenceType) { + $referenceType = self::NETWORK_PATH; + } + } + } + + if (self::ABSOLUTE_URL === $referenceType || self::NETWORK_PATH === $referenceType) { + if ('' !== $host || ('' !== $scheme && 'http' !== $scheme && 'https' !== $scheme)) { + $port = ''; + if ('http' === $scheme && 80 !== $this->context->getHttpPort()) { + $port = ':'.$this->context->getHttpPort(); + } elseif ('https' === $scheme && 443 !== $this->context->getHttpsPort()) { + $port = ':'.$this->context->getHttpsPort(); + } + + $schemeAuthority = self::NETWORK_PATH === $referenceType || '' === $scheme ? '//' : "$scheme://"; + $schemeAuthority .= $host.$port; + } + } + + if (self::RELATIVE_PATH === $referenceType) { + $url = self::getRelativePath($this->context->getPathInfo(), $url); + } else { + $url = $schemeAuthority.$this->context->getBaseUrl().$url; + } + + // add a query string if needed + $extra = array_udiff_assoc(array_diff_key($parameters, $variables), $defaults, fn ($a, $b) => $a == $b ? 0 : 1); + + array_walk_recursive($extra, $caster = static function (&$v) use (&$caster) { + if (\is_object($v)) { + if ($vars = get_object_vars($v)) { + array_walk_recursive($vars, $caster); + $v = $vars; + } elseif (method_exists($v, '__toString')) { + $v = (string) $v; + } + } + }); + + // extract fragment + $fragment = $defaults['_fragment'] ?? ''; + + if (isset($extra['_fragment'])) { + $fragment = $extra['_fragment']; + unset($extra['_fragment']); + } + + if ($extra && $query = http_build_query($extra, '', '&', \PHP_QUERY_RFC3986)) { + $url .= '?'.strtr($query, self::QUERY_FRAGMENT_DECODED); + } + + if ('' !== $fragment) { + $url .= '#'.strtr(rawurlencode($fragment), self::QUERY_FRAGMENT_DECODED); + } + + return $url; + } + + /** + * Returns the target path as relative reference from the base path. + * + * Only the URIs path component (no schema, host etc.) is relevant and must be given, starting with a slash. + * Both paths must be absolute and not contain relative parts. + * Relative URLs from one resource to another are useful when generating self-contained downloadable document archives. + * Furthermore, they can be used to reduce the link size in documents. + * + * Example target paths, given a base path of "/a/b/c/d": + * - "/a/b/c/d" -> "" + * - "/a/b/c/" -> "./" + * - "/a/b/" -> "../" + * - "/a/b/c/other" -> "other" + * - "/a/x/y" -> "../../x/y" + * + * @param string $basePath The base path + * @param string $targetPath The target path + */ + public static function getRelativePath(string $basePath, string $targetPath): string + { + if ($basePath === $targetPath) { + return ''; + } + + $sourceDirs = explode('/', isset($basePath[0]) && '/' === $basePath[0] ? substr($basePath, 1) : $basePath); + $targetDirs = explode('/', isset($targetPath[0]) && '/' === $targetPath[0] ? substr($targetPath, 1) : $targetPath); + array_pop($sourceDirs); + $targetFile = array_pop($targetDirs); + + foreach ($sourceDirs as $i => $dir) { + if (isset($targetDirs[$i]) && $dir === $targetDirs[$i]) { + unset($sourceDirs[$i], $targetDirs[$i]); + } else { + break; + } + } + + $targetDirs[] = $targetFile; + $path = str_repeat('../', \count($sourceDirs)).implode('/', $targetDirs); + + // A reference to the same base directory or an empty subdirectory must be prefixed with "./". + // This also applies to a segment with a colon character (e.g., "file:colon") that cannot be used + // as the first segment of a relative-path reference, as it would be mistaken for a scheme name + // (see http://tools.ietf.org/html/rfc3986#section-4.2). + return '' === $path || '/' === $path[0] + || false !== ($colonPos = strpos($path, ':')) && ($colonPos < ($slashPos = strpos($path, '/')) || false === $slashPos) + ? "./$path" : $path; + } +} diff --git a/3rdparty/symfony/routing/Generator/UrlGeneratorInterface.php b/3rdparty/symfony/routing/Generator/UrlGeneratorInterface.php new file mode 100644 index 00000000..51210b4b --- /dev/null +++ b/3rdparty/symfony/routing/Generator/UrlGeneratorInterface.php @@ -0,0 +1,80 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Component\Routing\Generator; + +use Symfony\Component\Routing\Exception\InvalidParameterException; +use Symfony\Component\Routing\Exception\MissingMandatoryParametersException; +use Symfony\Component\Routing\Exception\RouteNotFoundException; +use Symfony\Component\Routing\RequestContextAwareInterface; + +/** + * UrlGeneratorInterface is the interface that all URL generator classes must implement. + * + * The constants in this interface define the different types of resource references that + * are declared in RFC 3986: http://tools.ietf.org/html/rfc3986 + * We are using the term "URL" instead of "URI" as this is more common in web applications + * and we do not need to distinguish them as the difference is mostly semantical and + * less technical. Generating URIs, i.e. representation-independent resource identifiers, + * is also possible. + * + * @author Fabien Potencier + * @author Tobias Schultze + */ +interface UrlGeneratorInterface extends RequestContextAwareInterface +{ + /** + * Generates an absolute URL, e.g. "http://example.com/dir/file". + */ + public const ABSOLUTE_URL = 0; + + /** + * Generates an absolute path, e.g. "/dir/file". + */ + public const ABSOLUTE_PATH = 1; + + /** + * Generates a relative path based on the current request path, e.g. "../parent-file". + * + * @see UrlGenerator::getRelativePath() + */ + public const RELATIVE_PATH = 2; + + /** + * Generates a network path, e.g. "//example.com/dir/file". + * Such reference reuses the current scheme but specifies the host. + */ + public const NETWORK_PATH = 3; + + /** + * Generates a URL or path for a specific route based on the given parameters. + * + * Parameters that reference placeholders in the route pattern will substitute them in the + * path or host. Extra params are added as query string to the URL. + * + * When the passed reference type cannot be generated for the route because it requires a different + * host or scheme than the current one, the method will return a more comprehensive reference + * that includes the required params. For example, when you call this method with $referenceType = ABSOLUTE_PATH + * but the route requires the https scheme whereas the current scheme is http, it will instead return an + * ABSOLUTE_URL with the https scheme and the current host. This makes sure the generated URL matches + * the route in any case. + * + * If there is no route with the given name, the generator must throw the RouteNotFoundException. + * + * The special parameter _fragment will be used as the document fragment suffixed to the final URL. + * + * @throws RouteNotFoundException If the named route doesn't exist + * @throws MissingMandatoryParametersException When some parameters are missing that are mandatory for the route + * @throws InvalidParameterException When a parameter value for a placeholder is not correct because + * it does not match the requirement + */ + public function generate(string $name, array $parameters = [], int $referenceType = self::ABSOLUTE_PATH): string; +} diff --git a/3rdparty/symfony/routing/LICENSE b/3rdparty/symfony/routing/LICENSE new file mode 100644 index 00000000..0138f8f0 --- /dev/null +++ b/3rdparty/symfony/routing/LICENSE @@ -0,0 +1,19 @@ +Copyright (c) 2004-present Fabien Potencier + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is furnished +to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +THE SOFTWARE. diff --git a/3rdparty/symfony/routing/Loader/AnnotationClassLoader.php b/3rdparty/symfony/routing/Loader/AnnotationClassLoader.php new file mode 100644 index 00000000..b2c52ce9 --- /dev/null +++ b/3rdparty/symfony/routing/Loader/AnnotationClassLoader.php @@ -0,0 +1,25 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Component\Routing\Loader; + +trigger_deprecation('symfony/routing', '6.4', 'The "%s" class is deprecated, use "%s" instead.', AnnotationClassLoader::class, AttributeClassLoader::class); + +class_exists(AttributeClassLoader::class); + +if (false) { + /** + * @deprecated since Symfony 6.4, to be removed in 7.0, use {@link AttributeClassLoader} instead + */ + abstract class AnnotationClassLoader extends AttributeClassLoader + { + } +} diff --git a/3rdparty/symfony/routing/Loader/AnnotationDirectoryLoader.php b/3rdparty/symfony/routing/Loader/AnnotationDirectoryLoader.php new file mode 100644 index 00000000..169b1e60 --- /dev/null +++ b/3rdparty/symfony/routing/Loader/AnnotationDirectoryLoader.php @@ -0,0 +1,25 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Component\Routing\Loader; + +trigger_deprecation('symfony/routing', '6.4', 'The "%s" class is deprecated, use "%s" instead.', AnnotationDirectoryLoader::class, AttributeDirectoryLoader::class); + +class_exists(AttributeDirectoryLoader::class); + +if (false) { + /** + * @deprecated since Symfony 6.4, to be removed in 7.0, use {@link AttributeDirectoryLoader} instead + */ + class AnnotationDirectoryLoader extends AttributeDirectoryLoader + { + } +} diff --git a/3rdparty/symfony/routing/Loader/AnnotationFileLoader.php b/3rdparty/symfony/routing/Loader/AnnotationFileLoader.php new file mode 100644 index 00000000..60487bb2 --- /dev/null +++ b/3rdparty/symfony/routing/Loader/AnnotationFileLoader.php @@ -0,0 +1,25 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Component\Routing\Loader; + +trigger_deprecation('symfony/routing', '6.4', 'The "%s" class is deprecated, use "%s" instead.', AnnotationFileLoader::class, AttributeFileLoader::class); + +class_exists(AttributeFileLoader::class); + +if (false) { + /** + * @deprecated since Symfony 6.4, to be removed in 7.0, use {@link AttributeFileLoader} instead + */ + class AnnotationFileLoader extends AttributeFileLoader + { + } +} diff --git a/3rdparty/symfony/routing/Loader/AttributeClassLoader.php b/3rdparty/symfony/routing/Loader/AttributeClassLoader.php new file mode 100644 index 00000000..132da802 --- /dev/null +++ b/3rdparty/symfony/routing/Loader/AttributeClassLoader.php @@ -0,0 +1,431 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Component\Routing\Loader; + +use Doctrine\Common\Annotations\Reader; +use Symfony\Component\Config\Loader\LoaderInterface; +use Symfony\Component\Config\Loader\LoaderResolverInterface; +use Symfony\Component\Config\Resource\FileResource; +use Symfony\Component\Routing\Attribute\Route as RouteAnnotation; +use Symfony\Component\Routing\Route; +use Symfony\Component\Routing\RouteCollection; + +/** + * AttributeClassLoader loads routing information from a PHP class and its methods. + * + * You need to define an implementation for the configureRoute() method. Most of the + * time, this method should define some PHP callable to be called for the route + * (a controller in MVC speak). + * + * The #[Route] attribute can be set on the class (for global parameters), + * and on each method. + * + * The #[Route] attribute main value is the route path. The attribute also + * recognizes several parameters: requirements, options, defaults, schemes, + * methods, host, and name. The name parameter is mandatory. + * Here is an example of how you should be able to use it: + * + * #[Route('/Blog')] + * class Blog + * { + * #[Route('/', name: 'blog_index')] + * public function index() + * { + * } + * #[Route('/{id}', name: 'blog_post', requirements: ["id" => '\d+'])] + * public function show() + * { + * } + * } + * + * @author Fabien Potencier + * @author Alexander M. Turek + * @author Alexandre Daubois + */ +abstract class AttributeClassLoader implements LoaderInterface +{ + /** + * @var Reader|null + * + * @deprecated in Symfony 6.4, this property will be removed in Symfony 7. + */ + protected $reader; + + /** + * @var string|null + */ + protected $env; + + /** + * @var string + */ + protected $routeAnnotationClass = RouteAnnotation::class; + + /** + * @var int + */ + protected $defaultRouteIndex = 0; + + private bool $hasDeprecatedAnnotations = false; + + /** + * @param string|null $env + */ + public function __construct($env = null) + { + if ($env instanceof Reader || null === $env && \func_num_args() > 1 && null !== func_get_arg(1)) { + trigger_deprecation('symfony/routing', '6.4', 'Passing an instance of "%s" as first and the environment as second argument to "%s" is deprecated. Pass the environment as first argument instead.', Reader::class, __METHOD__); + + $this->reader = $env; + $env = \func_num_args() > 1 ? func_get_arg(1) : null; + } + + if (\is_string($env) || null === $env) { + $this->env = $env; + } elseif ($env instanceof \Stringable || \is_scalar($env)) { + $this->env = (string) $env; + } else { + throw new \TypeError(__METHOD__.sprintf(': Parameter $env was expected to be a string or null, "%s" given.', get_debug_type($env))); + } + } + + /** + * Sets the annotation class to read route properties from. + * + * @return void + */ + public function setRouteAnnotationClass(string $class) + { + $this->routeAnnotationClass = $class; + } + + /** + * @throws \InvalidArgumentException When route can't be parsed + */ + public function load(mixed $class, ?string $type = null): RouteCollection + { + if (!class_exists($class)) { + throw new \InvalidArgumentException(sprintf('Class "%s" does not exist.', $class)); + } + + $class = new \ReflectionClass($class); + if ($class->isAbstract()) { + throw new \InvalidArgumentException(sprintf('Attributes from class "%s" cannot be read as it is abstract.', $class->getName())); + } + + $this->hasDeprecatedAnnotations = false; + + try { + $globals = $this->getGlobals($class); + $collection = new RouteCollection(); + $collection->addResource(new FileResource($class->getFileName())); + if ($globals['env'] && $this->env !== $globals['env']) { + return $collection; + } + $fqcnAlias = false; + foreach ($class->getMethods() as $method) { + $this->defaultRouteIndex = 0; + $routeNamesBefore = array_keys($collection->all()); + foreach ($this->getAnnotations($method) as $annot) { + $this->addRoute($collection, $annot, $globals, $class, $method); + if ('__invoke' === $method->name) { + $fqcnAlias = true; + } + } + + if (1 === $collection->count() - \count($routeNamesBefore)) { + $newRouteName = current(array_diff(array_keys($collection->all()), $routeNamesBefore)); + if ($newRouteName !== $aliasName = sprintf('%s::%s', $class->name, $method->name)) { + $collection->addAlias($aliasName, $newRouteName); + } + } + } + if (0 === $collection->count() && $class->hasMethod('__invoke')) { + $globals = $this->resetGlobals(); + foreach ($this->getAnnotations($class) as $annot) { + $this->addRoute($collection, $annot, $globals, $class, $class->getMethod('__invoke')); + $fqcnAlias = true; + } + } + if ($fqcnAlias && 1 === $collection->count()) { + $invokeRouteName = key($collection->all()); + if ($invokeRouteName !== $class->name) { + $collection->addAlias($class->name, $invokeRouteName); + } + + if ($invokeRouteName !== $aliasName = sprintf('%s::__invoke', $class->name)) { + $collection->addAlias($aliasName, $invokeRouteName); + } + } + + if ($this->hasDeprecatedAnnotations) { + trigger_deprecation('symfony/routing', '6.4', 'Class "%s" uses Doctrine Annotations to configure routes, which is deprecated. Use PHP attributes instead.', $class->getName()); + } + } finally { + $this->hasDeprecatedAnnotations = false; + } + + return $collection; + } + + /** + * @param RouteAnnotation $annot or an object that exposes a similar interface + * + * @return void + */ + protected function addRoute(RouteCollection $collection, object $annot, array $globals, \ReflectionClass $class, \ReflectionMethod $method) + { + if ($annot->getEnv() && $annot->getEnv() !== $this->env) { + return; + } + + $name = $annot->getName() ?? $this->getDefaultRouteName($class, $method); + $name = $globals['name'].$name; + + $requirements = $annot->getRequirements(); + + foreach ($requirements as $placeholder => $requirement) { + if (\is_int($placeholder)) { + throw new \InvalidArgumentException(sprintf('A placeholder name must be a string (%d given). Did you forget to specify the placeholder key for the requirement "%s" of route "%s" in "%s::%s()"?', $placeholder, $requirement, $name, $class->getName(), $method->getName())); + } + } + + $defaults = array_replace($globals['defaults'], $annot->getDefaults()); + $requirements = array_replace($globals['requirements'], $requirements); + $options = array_replace($globals['options'], $annot->getOptions()); + $schemes = array_unique(array_merge($globals['schemes'], $annot->getSchemes())); + $methods = array_unique(array_merge($globals['methods'], $annot->getMethods())); + + $host = $annot->getHost() ?? $globals['host']; + $condition = $annot->getCondition() ?? $globals['condition']; + $priority = $annot->getPriority() ?? $globals['priority']; + + $path = $annot->getLocalizedPaths() ?: $annot->getPath(); + $prefix = $globals['localized_paths'] ?: $globals['path']; + $paths = []; + + if (\is_array($path)) { + if (!\is_array($prefix)) { + foreach ($path as $locale => $localePath) { + $paths[$locale] = $prefix.$localePath; + } + } elseif ($missing = array_diff_key($prefix, $path)) { + throw new \LogicException(sprintf('Route to "%s" is missing paths for locale(s) "%s".', $class->name.'::'.$method->name, implode('", "', array_keys($missing)))); + } else { + foreach ($path as $locale => $localePath) { + if (!isset($prefix[$locale])) { + throw new \LogicException(sprintf('Route to "%s" with locale "%s" is missing a corresponding prefix in class "%s".', $method->name, $locale, $class->name)); + } + + $paths[$locale] = $prefix[$locale].$localePath; + } + } + } elseif (\is_array($prefix)) { + foreach ($prefix as $locale => $localePrefix) { + $paths[$locale] = $localePrefix.$path; + } + } else { + $paths[] = $prefix.$path; + } + + foreach ($method->getParameters() as $param) { + if (isset($defaults[$param->name]) || !$param->isDefaultValueAvailable()) { + continue; + } + foreach ($paths as $locale => $path) { + if (preg_match(sprintf('/\{%s(?:<.*?>)?\}/', preg_quote($param->name)), $path)) { + if (\is_scalar($defaultValue = $param->getDefaultValue()) || null === $defaultValue) { + $defaults[$param->name] = $defaultValue; + } elseif ($defaultValue instanceof \BackedEnum) { + $defaults[$param->name] = $defaultValue->value; + } + break; + } + } + } + + foreach ($paths as $locale => $path) { + $route = $this->createRoute($path, $defaults, $requirements, $options, $host, $schemes, $methods, $condition); + $this->configureRoute($route, $class, $method, $annot); + if (0 !== $locale) { + $route->setDefault('_locale', $locale); + $route->setRequirement('_locale', preg_quote($locale)); + $route->setDefault('_canonical_route', $name); + $collection->add($name.'.'.$locale, $route, $priority); + } else { + $collection->add($name, $route, $priority); + } + } + } + + public function supports(mixed $resource, ?string $type = null): bool + { + if ('annotation' === $type) { + trigger_deprecation('symfony/routing', '6.4', 'The "annotation" route type is deprecated, use the "attribute" route type instead.'); + } + + return \is_string($resource) && preg_match('/^(?:\\\\?[a-zA-Z_\x7f-\xff][a-zA-Z0-9_\x7f-\xff]*)+$/', $resource) && (!$type || \in_array($type, ['annotation', 'attribute'], true)); + } + + public function setResolver(LoaderResolverInterface $resolver): void + { + } + + public function getResolver(): LoaderResolverInterface + { + } + + /** + * Gets the default route name for a class method. + * + * @return string + */ + protected function getDefaultRouteName(\ReflectionClass $class, \ReflectionMethod $method) + { + $name = str_replace('\\', '_', $class->name).'_'.$method->name; + $name = \function_exists('mb_strtolower') && preg_match('//u', $name) ? mb_strtolower($name, 'UTF-8') : strtolower($name); + if ($this->defaultRouteIndex > 0) { + $name .= '_'.$this->defaultRouteIndex; + } + ++$this->defaultRouteIndex; + + return $name; + } + + /** + * @return array + */ + protected function getGlobals(\ReflectionClass $class) + { + $globals = $this->resetGlobals(); + + $annot = null; + if ($attribute = $class->getAttributes($this->routeAnnotationClass, \ReflectionAttribute::IS_INSTANCEOF)[0] ?? null) { + $annot = $attribute->newInstance(); + } + if (!$annot && $annot = $this->reader?->getClassAnnotation($class, $this->routeAnnotationClass)) { + $this->hasDeprecatedAnnotations = true; + } + + if ($annot) { + if (null !== $annot->getName()) { + $globals['name'] = $annot->getName(); + } + + if (null !== $annot->getPath()) { + $globals['path'] = $annot->getPath(); + } + + $globals['localized_paths'] = $annot->getLocalizedPaths(); + + if (null !== $annot->getRequirements()) { + $globals['requirements'] = $annot->getRequirements(); + } + + if (null !== $annot->getOptions()) { + $globals['options'] = $annot->getOptions(); + } + + if (null !== $annot->getDefaults()) { + $globals['defaults'] = $annot->getDefaults(); + } + + if (null !== $annot->getSchemes()) { + $globals['schemes'] = $annot->getSchemes(); + } + + if (null !== $annot->getMethods()) { + $globals['methods'] = $annot->getMethods(); + } + + if (null !== $annot->getHost()) { + $globals['host'] = $annot->getHost(); + } + + if (null !== $annot->getCondition()) { + $globals['condition'] = $annot->getCondition(); + } + + $globals['priority'] = $annot->getPriority() ?? 0; + $globals['env'] = $annot->getEnv(); + + foreach ($globals['requirements'] as $placeholder => $requirement) { + if (\is_int($placeholder)) { + throw new \InvalidArgumentException(sprintf('A placeholder name must be a string (%d given). Did you forget to specify the placeholder key for the requirement "%s" in "%s"?', $placeholder, $requirement, $class->getName())); + } + } + } + + return $globals; + } + + private function resetGlobals(): array + { + return [ + 'path' => null, + 'localized_paths' => [], + 'requirements' => [], + 'options' => [], + 'defaults' => [], + 'schemes' => [], + 'methods' => [], + 'host' => '', + 'condition' => '', + 'name' => '', + 'priority' => 0, + 'env' => null, + ]; + } + + /** + * @return Route + */ + protected function createRoute(string $path, array $defaults, array $requirements, array $options, ?string $host, array $schemes, array $methods, ?string $condition) + { + return new Route($path, $defaults, $requirements, $options, $host, $schemes, $methods, $condition); + } + + /** + * @return void + */ + abstract protected function configureRoute(Route $route, \ReflectionClass $class, \ReflectionMethod $method, object $annot); + + /** + * @return iterable + */ + private function getAnnotations(\ReflectionClass|\ReflectionMethod $reflection): iterable + { + foreach ($reflection->getAttributes($this->routeAnnotationClass, \ReflectionAttribute::IS_INSTANCEOF) as $attribute) { + yield $attribute->newInstance(); + } + + if (!$this->reader) { + return; + } + + $annotations = $reflection instanceof \ReflectionClass + ? $this->reader->getClassAnnotations($reflection) + : $this->reader->getMethodAnnotations($reflection); + + foreach ($annotations as $annotation) { + if ($annotation instanceof $this->routeAnnotationClass) { + $this->hasDeprecatedAnnotations = true; + + yield $annotation; + } + } + } +} + +if (!class_exists(AnnotationClassLoader::class, false)) { + class_alias(AttributeClassLoader::class, AnnotationClassLoader::class); +} diff --git a/3rdparty/symfony/routing/Loader/AttributeDirectoryLoader.php b/3rdparty/symfony/routing/Loader/AttributeDirectoryLoader.php new file mode 100644 index 00000000..a070937d --- /dev/null +++ b/3rdparty/symfony/routing/Loader/AttributeDirectoryLoader.php @@ -0,0 +1,92 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Component\Routing\Loader; + +use Symfony\Component\Config\Resource\DirectoryResource; +use Symfony\Component\Routing\RouteCollection; + +/** + * AttributeDirectoryLoader loads routing information from attributes set + * on PHP classes and methods. + * + * @author Fabien Potencier + * @author Alexandre Daubois + */ +class AttributeDirectoryLoader extends AttributeFileLoader +{ + /** + * @throws \InvalidArgumentException When the directory does not exist or its routes cannot be parsed + */ + public function load(mixed $path, ?string $type = null): ?RouteCollection + { + if (!is_dir($dir = $this->locator->locate($path))) { + return parent::supports($path, $type) ? parent::load($path, $type) : new RouteCollection(); + } + + $collection = new RouteCollection(); + $collection->addResource(new DirectoryResource($dir, '/\.php$/')); + $files = iterator_to_array(new \RecursiveIteratorIterator( + new \RecursiveCallbackFilterIterator( + new \RecursiveDirectoryIterator($dir, \FilesystemIterator::SKIP_DOTS | \FilesystemIterator::FOLLOW_SYMLINKS), + fn (\SplFileInfo $current) => !str_starts_with($current->getBasename(), '.') + ), + \RecursiveIteratorIterator::LEAVES_ONLY + )); + usort($files, fn (\SplFileInfo $a, \SplFileInfo $b) => (string) $a > (string) $b ? 1 : -1); + + foreach ($files as $file) { + if (!$file->isFile() || !str_ends_with($file->getFilename(), '.php')) { + continue; + } + + if ($class = $this->findClass($file)) { + $refl = new \ReflectionClass($class); + if ($refl->isAbstract()) { + continue; + } + + $collection->addCollection($this->loader->load($class, $type)); + } + } + + return $collection; + } + + public function supports(mixed $resource, ?string $type = null): bool + { + if (!\is_string($resource)) { + return false; + } + + if (\in_array($type, ['annotation', 'attribute'], true)) { + if ('annotation' === $type) { + trigger_deprecation('symfony/routing', '6.4', 'The "annotation" route type is deprecated, use the "attribute" route type instead.'); + } + + return true; + } + + if ($type) { + return false; + } + + try { + return is_dir($this->locator->locate($resource)); + } catch (\Exception) { + return false; + } + } +} + +if (!class_exists(AnnotationDirectoryLoader::class, false)) { + class_alias(AttributeDirectoryLoader::class, AnnotationDirectoryLoader::class); +} diff --git a/3rdparty/symfony/routing/Loader/AttributeFileLoader.php b/3rdparty/symfony/routing/Loader/AttributeFileLoader.php new file mode 100644 index 00000000..e9a13e59 --- /dev/null +++ b/3rdparty/symfony/routing/Loader/AttributeFileLoader.php @@ -0,0 +1,145 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Component\Routing\Loader; + +use Symfony\Component\Config\FileLocatorInterface; +use Symfony\Component\Config\Loader\FileLoader; +use Symfony\Component\Config\Resource\FileResource; +use Symfony\Component\Routing\RouteCollection; + +/** + * AttributeFileLoader loads routing information from attributes set + * on a PHP class and its methods. + * + * @author Fabien Potencier + * @author Alexandre Daubois + */ +class AttributeFileLoader extends FileLoader +{ + protected $loader; + + public function __construct(FileLocatorInterface $locator, AttributeClassLoader $loader) + { + if (!\function_exists('token_get_all')) { + throw new \LogicException('The Tokenizer extension is required for the routing attribute loader.'); + } + + parent::__construct($locator); + + $this->loader = $loader; + } + + /** + * Loads from attributes from a file. + * + * @throws \InvalidArgumentException When the file does not exist or its routes cannot be parsed + */ + public function load(mixed $file, ?string $type = null): ?RouteCollection + { + $path = $this->locator->locate($file); + + $collection = new RouteCollection(); + if ($class = $this->findClass($path)) { + $refl = new \ReflectionClass($class); + if ($refl->isAbstract()) { + return null; + } + + $collection->addResource(new FileResource($path)); + $collection->addCollection($this->loader->load($class, $type)); + } + + gc_mem_caches(); + + return $collection; + } + + public function supports(mixed $resource, ?string $type = null): bool + { + if ('annotation' === $type) { + trigger_deprecation('symfony/routing', '6.4', 'The "annotation" route type is deprecated, use the "attribute" route type instead.'); + } + + return \is_string($resource) && 'php' === pathinfo($resource, \PATHINFO_EXTENSION) && (!$type || \in_array($type, ['annotation', 'attribute'], true)); + } + + /** + * Returns the full class name for the first class in the file. + */ + protected function findClass(string $file): string|false + { + $class = false; + $namespace = false; + $tokens = token_get_all(file_get_contents($file)); + + if (1 === \count($tokens) && \T_INLINE_HTML === $tokens[0][0]) { + throw new \InvalidArgumentException(sprintf('The file "%s" does not contain PHP code. Did you forget to add the " true, \T_STRING => true]; + if (\defined('T_NAME_QUALIFIED')) { + $nsTokens[\T_NAME_QUALIFIED] = true; + } + for ($i = 0; isset($tokens[$i]); ++$i) { + $token = $tokens[$i]; + if (!isset($token[1])) { + continue; + } + + if (true === $class && \T_STRING === $token[0]) { + return $namespace.'\\'.$token[1]; + } + + if (true === $namespace && isset($nsTokens[$token[0]])) { + $namespace = $token[1]; + while (isset($tokens[++$i][1], $nsTokens[$tokens[$i][0]])) { + $namespace .= $tokens[$i][1]; + } + $token = $tokens[$i]; + } + + if (\T_CLASS === $token[0]) { + // Skip usage of ::class constant and anonymous classes + $skipClassToken = false; + for ($j = $i - 1; $j > 0; --$j) { + if (!isset($tokens[$j][1])) { + if ('(' === $tokens[$j] || ',' === $tokens[$j]) { + $skipClassToken = true; + } + break; + } + + if (\T_DOUBLE_COLON === $tokens[$j][0] || \T_NEW === $tokens[$j][0]) { + $skipClassToken = true; + break; + } elseif (!\in_array($tokens[$j][0], [\T_WHITESPACE, \T_DOC_COMMENT, \T_COMMENT])) { + break; + } + } + + if (!$skipClassToken) { + $class = true; + } + } + + if (\T_NAMESPACE === $token[0]) { + $namespace = true; + } + } + + return false; + } +} + +if (!class_exists(AnnotationFileLoader::class, false)) { + class_alias(AttributeFileLoader::class, AnnotationFileLoader::class); +} diff --git a/3rdparty/symfony/routing/Loader/ClosureLoader.php b/3rdparty/symfony/routing/Loader/ClosureLoader.php new file mode 100644 index 00000000..dcc5ee33 --- /dev/null +++ b/3rdparty/symfony/routing/Loader/ClosureLoader.php @@ -0,0 +1,38 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Component\Routing\Loader; + +use Symfony\Component\Config\Loader\Loader; +use Symfony\Component\Routing\RouteCollection; + +/** + * ClosureLoader loads routes from a PHP closure. + * + * The Closure must return a RouteCollection instance. + * + * @author Fabien Potencier + */ +class ClosureLoader extends Loader +{ + /** + * Loads a Closure. + */ + public function load(mixed $closure, ?string $type = null): RouteCollection + { + return $closure($this->env); + } + + public function supports(mixed $resource, ?string $type = null): bool + { + return $resource instanceof \Closure && (!$type || 'closure' === $type); + } +} diff --git a/3rdparty/symfony/routing/Loader/Configurator/AliasConfigurator.php b/3rdparty/symfony/routing/Loader/Configurator/AliasConfigurator.php new file mode 100644 index 00000000..c908456e --- /dev/null +++ b/3rdparty/symfony/routing/Loader/Configurator/AliasConfigurator.php @@ -0,0 +1,43 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Component\Routing\Loader\Configurator; + +use Symfony\Component\DependencyInjection\Exception\InvalidArgumentException; +use Symfony\Component\Routing\Alias; + +class AliasConfigurator +{ + private Alias $alias; + + public function __construct(Alias $alias) + { + $this->alias = $alias; + } + + /** + * Whether this alias is deprecated, that means it should not be called anymore. + * + * @param string $package The name of the composer package that is triggering the deprecation + * @param string $version The version of the package that introduced the deprecation + * @param string $message The deprecation message to use + * + * @return $this + * + * @throws InvalidArgumentException when the message template is invalid + */ + public function deprecate(string $package, string $version, string $message): static + { + $this->alias->setDeprecated($package, $version, $message); + + return $this; + } +} diff --git a/3rdparty/symfony/routing/Loader/Configurator/CollectionConfigurator.php b/3rdparty/symfony/routing/Loader/Configurator/CollectionConfigurator.php new file mode 100644 index 00000000..1abf3bc0 --- /dev/null +++ b/3rdparty/symfony/routing/Loader/Configurator/CollectionConfigurator.php @@ -0,0 +1,128 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Component\Routing\Loader\Configurator; + +use Symfony\Component\Routing\Route; +use Symfony\Component\Routing\RouteCollection; + +/** + * @author Nicolas Grekas + */ +class CollectionConfigurator +{ + use Traits\AddTrait; + use Traits\HostTrait; + use Traits\RouteTrait; + + private RouteCollection $parent; + private ?CollectionConfigurator $parentConfigurator; + private ?array $parentPrefixes; + private string|array|null $host = null; + + public function __construct(RouteCollection $parent, string $name, ?self $parentConfigurator = null, ?array $parentPrefixes = null) + { + $this->parent = $parent; + $this->name = $name; + $this->collection = new RouteCollection(); + $this->route = new Route(''); + $this->parentConfigurator = $parentConfigurator; // for GC control + $this->parentPrefixes = $parentPrefixes; + } + + public function __sleep(): array + { + throw new \BadMethodCallException('Cannot serialize '.__CLASS__); + } + + /** + * @return void + */ + public function __wakeup() + { + throw new \BadMethodCallException('Cannot unserialize '.__CLASS__); + } + + public function __destruct() + { + if (null === $this->prefixes) { + $this->collection->addPrefix($this->route->getPath()); + } + if (null !== $this->host) { + $this->addHost($this->collection, $this->host); + } + + $this->parent->addCollection($this->collection); + } + + /** + * Creates a sub-collection. + */ + final public function collection(string $name = ''): self + { + return new self($this->collection, $this->name.$name, $this, $this->prefixes); + } + + /** + * Sets the prefix to add to the path of all child routes. + * + * @param string|array $prefix the prefix, or the localized prefixes + * + * @return $this + */ + final public function prefix(string|array $prefix): static + { + if (\is_array($prefix)) { + if (null === $this->parentPrefixes) { + // no-op + } elseif ($missing = array_diff_key($this->parentPrefixes, $prefix)) { + throw new \LogicException(sprintf('Collection "%s" is missing prefixes for locale(s) "%s".', $this->name, implode('", "', array_keys($missing)))); + } else { + foreach ($prefix as $locale => $localePrefix) { + if (!isset($this->parentPrefixes[$locale])) { + throw new \LogicException(sprintf('Collection "%s" with locale "%s" is missing a corresponding prefix in its parent collection.', $this->name, $locale)); + } + + $prefix[$locale] = $this->parentPrefixes[$locale].$localePrefix; + } + } + $this->prefixes = $prefix; + $this->route->setPath('/'); + } else { + $this->prefixes = null; + $this->route->setPath($prefix); + } + + return $this; + } + + /** + * Sets the host to use for all child routes. + * + * @param string|array $host the host, or the localized hosts + * + * @return $this + */ + final public function host(string|array $host): static + { + $this->host = $host; + + return $this; + } + + /** + * This method overrides the one from LocalizedRouteTrait. + */ + private function createRoute(string $path): Route + { + return (clone $this->route)->setPath($path); + } +} diff --git a/3rdparty/symfony/routing/Loader/Configurator/ImportConfigurator.php b/3rdparty/symfony/routing/Loader/Configurator/ImportConfigurator.php new file mode 100644 index 00000000..9c92a7d7 --- /dev/null +++ b/3rdparty/symfony/routing/Loader/Configurator/ImportConfigurator.php @@ -0,0 +1,90 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Component\Routing\Loader\Configurator; + +use Symfony\Component\Routing\RouteCollection; + +/** + * @author Nicolas Grekas + */ +class ImportConfigurator +{ + use Traits\HostTrait; + use Traits\PrefixTrait; + use Traits\RouteTrait; + + private RouteCollection $parent; + + public function __construct(RouteCollection $parent, RouteCollection $route) + { + $this->parent = $parent; + $this->route = $route; + } + + public function __sleep(): array + { + throw new \BadMethodCallException('Cannot serialize '.__CLASS__); + } + + /** + * @return void + */ + public function __wakeup() + { + throw new \BadMethodCallException('Cannot unserialize '.__CLASS__); + } + + public function __destruct() + { + $this->parent->addCollection($this->route); + } + + /** + * Sets the prefix to add to the path of all child routes. + * + * @param string|array $prefix the prefix, or the localized prefixes + * + * @return $this + */ + final public function prefix(string|array $prefix, bool $trailingSlashOnRoot = true): static + { + $this->addPrefix($this->route, $prefix, $trailingSlashOnRoot); + + return $this; + } + + /** + * Sets the prefix to add to the name of all child routes. + * + * @return $this + */ + final public function namePrefix(string $namePrefix): static + { + $this->route->addNamePrefix($namePrefix); + + return $this; + } + + /** + * Sets the host to use for all child routes. + * + * @param string|array $host the host, or the localized hosts + * + * @return $this + */ + final public function host(string|array $host): static + { + $this->addHost($this->route, $host); + + return $this; + } +} diff --git a/3rdparty/symfony/routing/Loader/Configurator/RouteConfigurator.php b/3rdparty/symfony/routing/Loader/Configurator/RouteConfigurator.php new file mode 100644 index 00000000..d9d441da --- /dev/null +++ b/3rdparty/symfony/routing/Loader/Configurator/RouteConfigurator.php @@ -0,0 +1,49 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Component\Routing\Loader\Configurator; + +use Symfony\Component\Routing\RouteCollection; + +/** + * @author Nicolas Grekas + */ +class RouteConfigurator +{ + use Traits\AddTrait; + use Traits\HostTrait; + use Traits\RouteTrait; + + protected $parentConfigurator; + + public function __construct(RouteCollection $collection, RouteCollection $route, string $name = '', ?CollectionConfigurator $parentConfigurator = null, ?array $prefixes = null) + { + $this->collection = $collection; + $this->route = $route; + $this->name = $name; + $this->parentConfigurator = $parentConfigurator; // for GC control + $this->prefixes = $prefixes; + } + + /** + * Sets the host to use for all child routes. + * + * @param string|array $host the host, or the localized hosts + * + * @return $this + */ + final public function host(string|array $host): static + { + $this->addHost($this->route, $host); + + return $this; + } +} diff --git a/3rdparty/symfony/routing/Loader/Configurator/RoutingConfigurator.php b/3rdparty/symfony/routing/Loader/Configurator/RoutingConfigurator.php new file mode 100644 index 00000000..fa88aa67 --- /dev/null +++ b/3rdparty/symfony/routing/Loader/Configurator/RoutingConfigurator.php @@ -0,0 +1,78 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Component\Routing\Loader\Configurator; + +use Symfony\Component\Routing\Loader\PhpFileLoader; +use Symfony\Component\Routing\RouteCollection; + +/** + * @author Nicolas Grekas + */ +class RoutingConfigurator +{ + use Traits\AddTrait; + + private PhpFileLoader $loader; + private string $path; + private string $file; + private ?string $env; + + public function __construct(RouteCollection $collection, PhpFileLoader $loader, string $path, string $file, ?string $env = null) + { + $this->collection = $collection; + $this->loader = $loader; + $this->path = $path; + $this->file = $file; + $this->env = $env; + } + + /** + * @param string|string[]|null $exclude Glob patterns to exclude from the import + */ + final public function import(string|array $resource, ?string $type = null, bool $ignoreErrors = false, string|array|null $exclude = null): ImportConfigurator + { + $this->loader->setCurrentDir(\dirname($this->path)); + + $imported = $this->loader->import($resource, $type, $ignoreErrors, $this->file, $exclude) ?: []; + if (!\is_array($imported)) { + return new ImportConfigurator($this->collection, $imported); + } + + $mergedCollection = new RouteCollection(); + foreach ($imported as $subCollection) { + $mergedCollection->addCollection($subCollection); + } + + return new ImportConfigurator($this->collection, $mergedCollection); + } + + final public function collection(string $name = ''): CollectionConfigurator + { + return new CollectionConfigurator($this->collection, $name); + } + + /** + * Get the current environment to be able to write conditional configuration. + */ + final public function env(): ?string + { + return $this->env; + } + + final public function withPath(string $path): static + { + $clone = clone $this; + $clone->path = $clone->file = $path; + + return $clone; + } +} diff --git a/3rdparty/symfony/routing/Loader/Configurator/Traits/AddTrait.php b/3rdparty/symfony/routing/Loader/Configurator/Traits/AddTrait.php new file mode 100644 index 00000000..5698df5d --- /dev/null +++ b/3rdparty/symfony/routing/Loader/Configurator/Traits/AddTrait.php @@ -0,0 +1,60 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Component\Routing\Loader\Configurator\Traits; + +use Symfony\Component\Routing\Loader\Configurator\AliasConfigurator; +use Symfony\Component\Routing\Loader\Configurator\CollectionConfigurator; +use Symfony\Component\Routing\Loader\Configurator\RouteConfigurator; +use Symfony\Component\Routing\RouteCollection; + +/** + * @author Nicolas Grekas + */ +trait AddTrait +{ + use LocalizedRouteTrait; + + /** + * @var RouteCollection + */ + protected $collection; + protected $name = ''; + protected $prefixes; + + /** + * Adds a route. + * + * @param string|array $path the path, or the localized paths of the route + */ + public function add(string $name, string|array $path): RouteConfigurator + { + $parentConfigurator = $this instanceof CollectionConfigurator ? $this : ($this instanceof RouteConfigurator ? $this->parentConfigurator : null); + $route = $this->createLocalizedRoute($this->collection, $name, $path, $this->name, $this->prefixes); + + return new RouteConfigurator($this->collection, $route, $this->name, $parentConfigurator, $this->prefixes); + } + + public function alias(string $name, string $alias): AliasConfigurator + { + return new AliasConfigurator($this->collection->addAlias($name, $alias)); + } + + /** + * Adds a route. + * + * @param string|array $path the path, or the localized paths of the route + */ + public function __invoke(string $name, string|array $path): RouteConfigurator + { + return $this->add($name, $path); + } +} diff --git a/3rdparty/symfony/routing/Loader/Configurator/Traits/HostTrait.php b/3rdparty/symfony/routing/Loader/Configurator/Traits/HostTrait.php new file mode 100644 index 00000000..d275f6c6 --- /dev/null +++ b/3rdparty/symfony/routing/Loader/Configurator/Traits/HostTrait.php @@ -0,0 +1,49 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Component\Routing\Loader\Configurator\Traits; + +use Symfony\Component\Routing\RouteCollection; + +/** + * @internal + */ +trait HostTrait +{ + final protected function addHost(RouteCollection $routes, string|array $hosts): void + { + if (!$hosts || !\is_array($hosts)) { + $routes->setHost($hosts ?: ''); + + return; + } + + foreach ($routes->all() as $name => $route) { + if (null === $locale = $route->getDefault('_locale')) { + $routes->remove($name); + foreach ($hosts as $locale => $host) { + $localizedRoute = clone $route; + $localizedRoute->setDefault('_locale', $locale); + $localizedRoute->setRequirement('_locale', preg_quote($locale)); + $localizedRoute->setDefault('_canonical_route', $name); + $localizedRoute->setHost($host); + $routes->add($name.'.'.$locale, $localizedRoute); + } + } elseif (!isset($hosts[$locale])) { + throw new \InvalidArgumentException(sprintf('Route "%s" with locale "%s" is missing a corresponding host in its parent collection.', $name, $locale)); + } else { + $route->setHost($hosts[$locale]); + $route->setRequirement('_locale', preg_quote($locale)); + $routes->add($name, $route); + } + } + } +} diff --git a/3rdparty/symfony/routing/Loader/Configurator/Traits/LocalizedRouteTrait.php b/3rdparty/symfony/routing/Loader/Configurator/Traits/LocalizedRouteTrait.php new file mode 100644 index 00000000..a26a7342 --- /dev/null +++ b/3rdparty/symfony/routing/Loader/Configurator/Traits/LocalizedRouteTrait.php @@ -0,0 +1,76 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Component\Routing\Loader\Configurator\Traits; + +use Symfony\Component\Routing\Route; +use Symfony\Component\Routing\RouteCollection; + +/** + * @internal + * + * @author Nicolas Grekas + * @author Jules Pietri + */ +trait LocalizedRouteTrait +{ + /** + * Creates one or many routes. + * + * @param string|array $path the path, or the localized paths of the route + */ + final protected function createLocalizedRoute(RouteCollection $collection, string $name, string|array $path, string $namePrefix = '', ?array $prefixes = null): RouteCollection + { + $paths = []; + + $routes = new RouteCollection(); + + if (\is_array($path)) { + if (null === $prefixes) { + $paths = $path; + } elseif ($missing = array_diff_key($prefixes, $path)) { + throw new \LogicException(sprintf('Route "%s" is missing routes for locale(s) "%s".', $name, implode('", "', array_keys($missing)))); + } else { + foreach ($path as $locale => $localePath) { + if (!isset($prefixes[$locale])) { + throw new \LogicException(sprintf('Route "%s" with locale "%s" is missing a corresponding prefix in its parent collection.', $name, $locale)); + } + + $paths[$locale] = $prefixes[$locale].$localePath; + } + } + } elseif (null !== $prefixes) { + foreach ($prefixes as $locale => $prefix) { + $paths[$locale] = $prefix.$path; + } + } else { + $routes->add($namePrefix.$name, $route = $this->createRoute($path)); + $collection->add($namePrefix.$name, $route); + + return $routes; + } + + foreach ($paths as $locale => $path) { + $routes->add($name.'.'.$locale, $route = $this->createRoute($path)); + $collection->add($namePrefix.$name.'.'.$locale, $route); + $route->setDefault('_locale', $locale); + $route->setRequirement('_locale', preg_quote($locale)); + $route->setDefault('_canonical_route', $namePrefix.$name); + } + + return $routes; + } + + private function createRoute(string $path): Route + { + return new Route($path); + } +} diff --git a/3rdparty/symfony/routing/Loader/Configurator/Traits/PrefixTrait.php b/3rdparty/symfony/routing/Loader/Configurator/Traits/PrefixTrait.php new file mode 100644 index 00000000..89a65d8f --- /dev/null +++ b/3rdparty/symfony/routing/Loader/Configurator/Traits/PrefixTrait.php @@ -0,0 +1,63 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Component\Routing\Loader\Configurator\Traits; + +use Symfony\Component\Routing\Route; +use Symfony\Component\Routing\RouteCollection; + +/** + * @internal + * + * @author Nicolas Grekas + */ +trait PrefixTrait +{ + final protected function addPrefix(RouteCollection $routes, string|array $prefix, bool $trailingSlashOnRoot): void + { + if (\is_array($prefix)) { + foreach ($prefix as $locale => $localePrefix) { + $prefix[$locale] = trim(trim($localePrefix), '/'); + } + foreach ($routes->all() as $name => $route) { + if (null === $locale = $route->getDefault('_locale')) { + $priority = $routes->getPriority($name) ?? 0; + $routes->remove($name); + foreach ($prefix as $locale => $localePrefix) { + $localizedRoute = clone $route; + $localizedRoute->setDefault('_locale', $locale); + $localizedRoute->setRequirement('_locale', preg_quote($locale)); + $localizedRoute->setDefault('_canonical_route', $name); + $localizedRoute->setPath($localePrefix.(!$trailingSlashOnRoot && '/' === $route->getPath() ? '' : $route->getPath())); + $routes->add($name.'.'.$locale, $localizedRoute, $priority); + } + } elseif (!isset($prefix[$locale])) { + throw new \InvalidArgumentException(sprintf('Route "%s" with locale "%s" is missing a corresponding prefix in its parent collection.', $name, $locale)); + } else { + $route->setPath($prefix[$locale].(!$trailingSlashOnRoot && '/' === $route->getPath() ? '' : $route->getPath())); + $routes->add($name, $route, $routes->getPriority($name) ?? 0); + } + } + + return; + } + + $routes->addPrefix($prefix); + if (!$trailingSlashOnRoot) { + $rootPath = (new Route(trim(trim($prefix), '/').'/'))->getPath(); + foreach ($routes->all() as $route) { + if ($route->getPath() === $rootPath) { + $route->setPath(rtrim($rootPath, '/')); + } + } + } + } +} diff --git a/3rdparty/symfony/routing/Loader/Configurator/Traits/RouteTrait.php b/3rdparty/symfony/routing/Loader/Configurator/Traits/RouteTrait.php new file mode 100644 index 00000000..16dc43d0 --- /dev/null +++ b/3rdparty/symfony/routing/Loader/Configurator/Traits/RouteTrait.php @@ -0,0 +1,175 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Component\Routing\Loader\Configurator\Traits; + +use Symfony\Component\Routing\Route; +use Symfony\Component\Routing\RouteCollection; + +trait RouteTrait +{ + /** + * @var RouteCollection|Route + */ + protected $route; + + /** + * Adds defaults. + * + * @return $this + */ + final public function defaults(array $defaults): static + { + $this->route->addDefaults($defaults); + + return $this; + } + + /** + * Adds requirements. + * + * @return $this + */ + final public function requirements(array $requirements): static + { + $this->route->addRequirements($requirements); + + return $this; + } + + /** + * Adds options. + * + * @return $this + */ + final public function options(array $options): static + { + $this->route->addOptions($options); + + return $this; + } + + /** + * Whether paths should accept utf8 encoding. + * + * @return $this + */ + final public function utf8(bool $utf8 = true): static + { + $this->route->addOptions(['utf8' => $utf8]); + + return $this; + } + + /** + * Sets the condition. + * + * @return $this + */ + final public function condition(string $condition): static + { + $this->route->setCondition($condition); + + return $this; + } + + /** + * Sets the pattern for the host. + * + * @return $this + */ + final public function host(string $pattern): static + { + $this->route->setHost($pattern); + + return $this; + } + + /** + * Sets the schemes (e.g. 'https') this route is restricted to. + * So an empty array means that any scheme is allowed. + * + * @param string[] $schemes + * + * @return $this + */ + final public function schemes(array $schemes): static + { + $this->route->setSchemes($schemes); + + return $this; + } + + /** + * Sets the HTTP methods (e.g. 'POST') this route is restricted to. + * So an empty array means that any method is allowed. + * + * @param string[] $methods + * + * @return $this + */ + final public function methods(array $methods): static + { + $this->route->setMethods($methods); + + return $this; + } + + /** + * Adds the "_controller" entry to defaults. + * + * @param callable|string|array $controller a callable or parseable pseudo-callable + * + * @return $this + */ + final public function controller(callable|string|array $controller): static + { + $this->route->addDefaults(['_controller' => $controller]); + + return $this; + } + + /** + * Adds the "_locale" entry to defaults. + * + * @return $this + */ + final public function locale(string $locale): static + { + $this->route->addDefaults(['_locale' => $locale]); + + return $this; + } + + /** + * Adds the "_format" entry to defaults. + * + * @return $this + */ + final public function format(string $format): static + { + $this->route->addDefaults(['_format' => $format]); + + return $this; + } + + /** + * Adds the "_stateless" entry to defaults. + * + * @return $this + */ + final public function stateless(bool $stateless = true): static + { + $this->route->addDefaults(['_stateless' => $stateless]); + + return $this; + } +} diff --git a/3rdparty/symfony/routing/Loader/ContainerLoader.php b/3rdparty/symfony/routing/Loader/ContainerLoader.php new file mode 100644 index 00000000..af325be0 --- /dev/null +++ b/3rdparty/symfony/routing/Loader/ContainerLoader.php @@ -0,0 +1,40 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Component\Routing\Loader; + +use Psr\Container\ContainerInterface; + +/** + * A route loader that executes a service from a PSR-11 container to load the routes. + * + * @author Ryan Weaver + */ +class ContainerLoader extends ObjectLoader +{ + private ContainerInterface $container; + + public function __construct(ContainerInterface $container, ?string $env = null) + { + $this->container = $container; + parent::__construct($env); + } + + public function supports(mixed $resource, ?string $type = null): bool + { + return 'service' === $type && \is_string($resource); + } + + protected function getObject(string $id): object + { + return $this->container->get($id); + } +} diff --git a/3rdparty/symfony/routing/Loader/DirectoryLoader.php b/3rdparty/symfony/routing/Loader/DirectoryLoader.php new file mode 100644 index 00000000..6c6c48e2 --- /dev/null +++ b/3rdparty/symfony/routing/Loader/DirectoryLoader.php @@ -0,0 +1,52 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Component\Routing\Loader; + +use Symfony\Component\Config\Loader\FileLoader; +use Symfony\Component\Config\Resource\DirectoryResource; +use Symfony\Component\Routing\RouteCollection; + +class DirectoryLoader extends FileLoader +{ + public function load(mixed $file, ?string $type = null): mixed + { + $path = $this->locator->locate($file); + + $collection = new RouteCollection(); + $collection->addResource(new DirectoryResource($path)); + + foreach (scandir($path) as $dir) { + if ('.' !== $dir[0]) { + $this->setCurrentDir($path); + $subPath = $path.'/'.$dir; + $subType = null; + + if (is_dir($subPath)) { + $subPath .= '/'; + $subType = 'directory'; + } + + $subCollection = $this->import($subPath, $subType, false, $path); + $collection->addCollection($subCollection); + } + } + + return $collection; + } + + public function supports(mixed $resource, ?string $type = null): bool + { + // only when type is forced to directory, not to conflict with AttributeLoader + + return 'directory' === $type; + } +} diff --git a/3rdparty/symfony/routing/Loader/GlobFileLoader.php b/3rdparty/symfony/routing/Loader/GlobFileLoader.php new file mode 100644 index 00000000..65afa5a3 --- /dev/null +++ b/3rdparty/symfony/routing/Loader/GlobFileLoader.php @@ -0,0 +1,41 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Component\Routing\Loader; + +use Symfony\Component\Config\Loader\FileLoader; +use Symfony\Component\Routing\RouteCollection; + +/** + * GlobFileLoader loads files from a glob pattern. + * + * @author Nicolas Grekas + */ +class GlobFileLoader extends FileLoader +{ + public function load(mixed $resource, ?string $type = null): mixed + { + $collection = new RouteCollection(); + + foreach ($this->glob($resource, false, $globResource) as $path => $info) { + $collection->addCollection($this->import($path)); + } + + $collection->addResource($globResource); + + return $collection; + } + + public function supports(mixed $resource, ?string $type = null): bool + { + return 'glob' === $type; + } +} diff --git a/3rdparty/symfony/routing/Loader/ObjectLoader.php b/3rdparty/symfony/routing/Loader/ObjectLoader.php new file mode 100644 index 00000000..c2ad6a03 --- /dev/null +++ b/3rdparty/symfony/routing/Loader/ObjectLoader.php @@ -0,0 +1,77 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Component\Routing\Loader; + +use Symfony\Component\Config\Loader\Loader; +use Symfony\Component\Config\Resource\FileResource; +use Symfony\Component\Routing\RouteCollection; + +/** + * A route loader that calls a method on an object to load the routes. + * + * @author Ryan Weaver + */ +abstract class ObjectLoader extends Loader +{ + /** + * Returns the object that the method will be called on to load routes. + * + * For example, if your application uses a service container, + * the $id may be a service id. + */ + abstract protected function getObject(string $id): object; + + /** + * Calls the object method that will load the routes. + */ + public function load(mixed $resource, ?string $type = null): RouteCollection + { + if (!preg_match('/^[^\:]+(?:::(?:[^\:]+))?$/', $resource)) { + throw new \InvalidArgumentException(sprintf('Invalid resource "%s" passed to the %s route loader: use the format "object_id::method" or "object_id" if your object class has an "__invoke" method.', $resource, \is_string($type) ? '"'.$type.'"' : 'object')); + } + + $parts = explode('::', $resource); + $method = $parts[1] ?? '__invoke'; + + $loaderObject = $this->getObject($parts[0]); + + if (!\is_object($loaderObject)) { + throw new \TypeError(sprintf('"%s:getObject()" must return an object: "%s" returned.', static::class, get_debug_type($loaderObject))); + } + + if (!\is_callable([$loaderObject, $method])) { + throw new \BadMethodCallException(sprintf('Method "%s" not found on "%s" when importing routing resource "%s".', $method, get_debug_type($loaderObject), $resource)); + } + + $routeCollection = $loaderObject->$method($this, $this->env); + + if (!$routeCollection instanceof RouteCollection) { + $type = get_debug_type($routeCollection); + + throw new \LogicException(sprintf('The "%s::%s()" method must return a RouteCollection: "%s" returned.', get_debug_type($loaderObject), $method, $type)); + } + + // make the object file tracked so that if it changes, the cache rebuilds + $this->addClassResource(new \ReflectionClass($loaderObject), $routeCollection); + + return $routeCollection; + } + + private function addClassResource(\ReflectionClass $class, RouteCollection $collection): void + { + do { + if (is_file($class->getFileName())) { + $collection->addResource(new FileResource($class->getFileName())); + } + } while ($class = $class->getParentClass()); + } +} diff --git a/3rdparty/symfony/routing/Loader/PhpFileLoader.php b/3rdparty/symfony/routing/Loader/PhpFileLoader.php new file mode 100644 index 00000000..adf7eed3 --- /dev/null +++ b/3rdparty/symfony/routing/Loader/PhpFileLoader.php @@ -0,0 +1,77 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Component\Routing\Loader; + +use Symfony\Component\Config\Loader\FileLoader; +use Symfony\Component\Config\Resource\FileResource; +use Symfony\Component\Routing\Loader\Configurator\RoutingConfigurator; +use Symfony\Component\Routing\RouteCollection; + +/** + * PhpFileLoader loads routes from a PHP file. + * + * The file must return a RouteCollection instance. + * + * @author Fabien Potencier + * @author Nicolas grekas + * @author Jules Pietri + */ +class PhpFileLoader extends FileLoader +{ + /** + * Loads a PHP file. + */ + public function load(mixed $file, ?string $type = null): RouteCollection + { + $path = $this->locator->locate($file); + $this->setCurrentDir(\dirname($path)); + + // the closure forbids access to the private scope in the included file + $loader = $this; + $load = \Closure::bind(static function ($file) use ($loader) { + return include $file; + }, null, ProtectedPhpFileLoader::class); + + $result = $load($path); + + if (\is_object($result) && \is_callable($result)) { + $collection = $this->callConfigurator($result, $path, $file); + } else { + $collection = $result; + } + + $collection->addResource(new FileResource($path)); + + return $collection; + } + + public function supports(mixed $resource, ?string $type = null): bool + { + return \is_string($resource) && 'php' === pathinfo($resource, \PATHINFO_EXTENSION) && (!$type || 'php' === $type); + } + + protected function callConfigurator(callable $result, string $path, string $file): RouteCollection + { + $collection = new RouteCollection(); + + $result(new RoutingConfigurator($collection, $this, $path, $file, $this->env)); + + return $collection; + } +} + +/** + * @internal + */ +final class ProtectedPhpFileLoader extends PhpFileLoader +{ +} diff --git a/3rdparty/symfony/routing/Loader/Psr4DirectoryLoader.php b/3rdparty/symfony/routing/Loader/Psr4DirectoryLoader.php new file mode 100644 index 00000000..bbf99418 --- /dev/null +++ b/3rdparty/symfony/routing/Loader/Psr4DirectoryLoader.php @@ -0,0 +1,91 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Component\Routing\Loader; + +use Symfony\Component\Config\FileLocatorInterface; +use Symfony\Component\Config\Loader\DirectoryAwareLoaderInterface; +use Symfony\Component\Config\Loader\Loader; +use Symfony\Component\Config\Resource\DirectoryResource; +use Symfony\Component\Routing\RouteCollection; + +/** + * A loader that discovers controller classes in a directory that follows PSR-4. + * + * @author Alexander M. Turek + */ +final class Psr4DirectoryLoader extends Loader implements DirectoryAwareLoaderInterface +{ + private ?string $currentDirectory = null; + + public function __construct( + private readonly FileLocatorInterface $locator, + ) { + // PSR-4 directory loader has no env-aware logic, so we drop the $env constructor parameter. + parent::__construct(); + } + + /** + * @param array{path: string, namespace: string} $resource + */ + public function load(mixed $resource, ?string $type = null): ?RouteCollection + { + $path = $this->locator->locate($resource['path'], $this->currentDirectory); + if (!is_dir($path)) { + return new RouteCollection(); + } + + return $this->loadFromDirectory($path, trim($resource['namespace'], '\\')); + } + + public function supports(mixed $resource, ?string $type = null): bool + { + return ('attribute' === $type || 'annotation' === $type) && \is_array($resource) && isset($resource['path'], $resource['namespace']); + } + + public function forDirectory(string $currentDirectory): static + { + $loader = clone $this; + $loader->currentDirectory = $currentDirectory; + + return $loader; + } + + private function loadFromDirectory(string $directory, string $psr4Prefix): RouteCollection + { + $collection = new RouteCollection(); + $collection->addResource(new DirectoryResource($directory, '/\.php$/')); + $files = iterator_to_array(new \RecursiveIteratorIterator( + new \RecursiveCallbackFilterIterator( + new \RecursiveDirectoryIterator($directory, \FilesystemIterator::SKIP_DOTS | \FilesystemIterator::FOLLOW_SYMLINKS), + fn (\SplFileInfo $current) => !str_starts_with($current->getBasename(), '.') + ), + \RecursiveIteratorIterator::SELF_FIRST + )); + usort($files, fn (\SplFileInfo $a, \SplFileInfo $b) => (string) $a > (string) $b ? 1 : -1); + + /** @var \SplFileInfo $file */ + foreach ($files as $file) { + if ($file->isDir()) { + $collection->addCollection($this->loadFromDirectory($file->getPathname(), $psr4Prefix.'\\'.$file->getFilename())); + + continue; + } + if ('php' !== $file->getExtension() || !class_exists($className = $psr4Prefix.'\\'.$file->getBasename('.php')) || (new \ReflectionClass($className))->isAbstract()) { + continue; + } + + $collection->addCollection($this->import($className, 'attribute')); + } + + return $collection; + } +} diff --git a/3rdparty/symfony/routing/Loader/XmlFileLoader.php b/3rdparty/symfony/routing/Loader/XmlFileLoader.php new file mode 100644 index 00000000..2518161a --- /dev/null +++ b/3rdparty/symfony/routing/Loader/XmlFileLoader.php @@ -0,0 +1,470 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Component\Routing\Loader; + +use Symfony\Component\Config\Loader\FileLoader; +use Symfony\Component\Config\Resource\FileResource; +use Symfony\Component\Config\Util\XmlUtils; +use Symfony\Component\Routing\Loader\Configurator\Traits\HostTrait; +use Symfony\Component\Routing\Loader\Configurator\Traits\LocalizedRouteTrait; +use Symfony\Component\Routing\Loader\Configurator\Traits\PrefixTrait; +use Symfony\Component\Routing\RouteCollection; + +/** + * XmlFileLoader loads XML routing files. + * + * @author Fabien Potencier + * @author Tobias Schultze + */ +class XmlFileLoader extends FileLoader +{ + use HostTrait; + use LocalizedRouteTrait; + use PrefixTrait; + + public const NAMESPACE_URI = 'http://symfony.com/schema/routing'; + public const SCHEME_PATH = '/schema/routing/routing-1.0.xsd'; + + /** + * @throws \InvalidArgumentException when the file cannot be loaded or when the XML cannot be + * parsed because it does not validate against the scheme + */ + public function load(mixed $file, ?string $type = null): RouteCollection + { + $path = $this->locator->locate($file); + + $xml = $this->loadFile($path); + + $collection = new RouteCollection(); + $collection->addResource(new FileResource($path)); + + // process routes and imports + foreach ($xml->documentElement->childNodes as $node) { + if (!$node instanceof \DOMElement) { + continue; + } + + $this->parseNode($collection, $node, $path, $file); + } + + return $collection; + } + + /** + * Parses a node from a loaded XML file. + * + * @return void + * + * @throws \InvalidArgumentException When the XML is invalid + */ + protected function parseNode(RouteCollection $collection, \DOMElement $node, string $path, string $file) + { + if (self::NAMESPACE_URI !== $node->namespaceURI) { + return; + } + + switch ($node->localName) { + case 'route': + $this->parseRoute($collection, $node, $path); + break; + case 'import': + $this->parseImport($collection, $node, $path, $file); + break; + case 'when': + if (!$this->env || $node->getAttribute('env') !== $this->env) { + break; + } + foreach ($node->childNodes as $node) { + if ($node instanceof \DOMElement) { + $this->parseNode($collection, $node, $path, $file); + } + } + break; + default: + throw new \InvalidArgumentException(sprintf('Unknown tag "%s" used in file "%s". Expected "route" or "import".', $node->localName, $path)); + } + } + + public function supports(mixed $resource, ?string $type = null): bool + { + return \is_string($resource) && 'xml' === pathinfo($resource, \PATHINFO_EXTENSION) && (!$type || 'xml' === $type); + } + + /** + * Parses a route and adds it to the RouteCollection. + * + * @return void + * + * @throws \InvalidArgumentException When the XML is invalid + */ + protected function parseRoute(RouteCollection $collection, \DOMElement $node, string $path) + { + if ('' === $id = $node->getAttribute('id')) { + throw new \InvalidArgumentException(sprintf('The element in file "%s" must have an "id" attribute.', $path)); + } + + if ('' !== $alias = $node->getAttribute('alias')) { + $alias = $collection->addAlias($id, $alias); + + if ($deprecationInfo = $this->parseDeprecation($node, $path)) { + $alias->setDeprecated($deprecationInfo['package'], $deprecationInfo['version'], $deprecationInfo['message']); + } + + return; + } + + $schemes = preg_split('/[\s,\|]++/', $node->getAttribute('schemes'), -1, \PREG_SPLIT_NO_EMPTY); + $methods = preg_split('/[\s,\|]++/', $node->getAttribute('methods'), -1, \PREG_SPLIT_NO_EMPTY); + + [$defaults, $requirements, $options, $condition, $paths, /* $prefixes */, $hosts] = $this->parseConfigs($node, $path); + + if (!$paths && '' === $node->getAttribute('path')) { + throw new \InvalidArgumentException(sprintf('The element in file "%s" must have a "path" attribute or child nodes.', $path)); + } + + if ($paths && '' !== $node->getAttribute('path')) { + throw new \InvalidArgumentException(sprintf('The element in file "%s" must not have both a "path" attribute and child nodes.', $path)); + } + + $routes = $this->createLocalizedRoute($collection, $id, $paths ?: $node->getAttribute('path')); + $routes->addDefaults($defaults); + $routes->addRequirements($requirements); + $routes->addOptions($options); + $routes->setSchemes($schemes); + $routes->setMethods($methods); + $routes->setCondition($condition); + + if (null !== $hosts) { + $this->addHost($routes, $hosts); + } + } + + /** + * Parses an import and adds the routes in the resource to the RouteCollection. + * + * @return void + * + * @throws \InvalidArgumentException When the XML is invalid + */ + protected function parseImport(RouteCollection $collection, \DOMElement $node, string $path, string $file) + { + /** @var \DOMElement $resourceElement */ + if (!($resource = $node->getAttribute('resource') ?: null) && $resourceElement = $node->getElementsByTagName('resource')[0] ?? null) { + $resource = []; + /** @var \DOMAttr $attribute */ + foreach ($resourceElement->attributes as $attribute) { + $resource[$attribute->name] = $attribute->value; + } + } + + if (!$resource) { + throw new \InvalidArgumentException(sprintf('The element in file "%s" must have a "resource" attribute or element.', $path)); + } + + $type = $node->getAttribute('type'); + $prefix = $node->getAttribute('prefix'); + $schemes = $node->hasAttribute('schemes') ? preg_split('/[\s,\|]++/', $node->getAttribute('schemes'), -1, \PREG_SPLIT_NO_EMPTY) : null; + $methods = $node->hasAttribute('methods') ? preg_split('/[\s,\|]++/', $node->getAttribute('methods'), -1, \PREG_SPLIT_NO_EMPTY) : null; + $trailingSlashOnRoot = $node->hasAttribute('trailing-slash-on-root') ? XmlUtils::phpize($node->getAttribute('trailing-slash-on-root')) : true; + $namePrefix = $node->getAttribute('name-prefix') ?: null; + + [$defaults, $requirements, $options, $condition, /* $paths */, $prefixes, $hosts] = $this->parseConfigs($node, $path); + + if ('' !== $prefix && $prefixes) { + throw new \InvalidArgumentException(sprintf('The element in file "%s" must not have both a "prefix" attribute and child nodes.', $path)); + } + + $exclude = []; + foreach ($node->childNodes as $child) { + if ($child instanceof \DOMElement && $child->localName === $exclude && self::NAMESPACE_URI === $child->namespaceURI) { + $exclude[] = $child->nodeValue; + } + } + + if ($node->hasAttribute('exclude')) { + if ($exclude) { + throw new \InvalidArgumentException('You cannot use both the attribute "exclude" and tags at the same time.'); + } + $exclude = [$node->getAttribute('exclude')]; + } + + $this->setCurrentDir(\dirname($path)); + + /** @var RouteCollection[] $imported */ + $imported = $this->import($resource, '' !== $type ? $type : null, false, $file, $exclude) ?: []; + + if (!\is_array($imported)) { + $imported = [$imported]; + } + + foreach ($imported as $subCollection) { + $this->addPrefix($subCollection, $prefixes ?: $prefix, $trailingSlashOnRoot); + + if (null !== $hosts) { + $this->addHost($subCollection, $hosts); + } + + if (null !== $condition) { + $subCollection->setCondition($condition); + } + if (null !== $schemes) { + $subCollection->setSchemes($schemes); + } + if (null !== $methods) { + $subCollection->setMethods($methods); + } + if (null !== $namePrefix) { + $subCollection->addNamePrefix($namePrefix); + } + $subCollection->addDefaults($defaults); + $subCollection->addRequirements($requirements); + $subCollection->addOptions($options); + + $collection->addCollection($subCollection); + } + } + + /** + * @throws \InvalidArgumentException When loading of XML file fails because of syntax errors + * or when the XML structure is not as expected by the scheme - + * see validate() + */ + protected function loadFile(string $file): \DOMDocument + { + return XmlUtils::loadFile($file, __DIR__.static::SCHEME_PATH); + } + + /** + * Parses the config elements (default, requirement, option). + * + * @throws \InvalidArgumentException When the XML is invalid + */ + private function parseConfigs(\DOMElement $node, string $path): array + { + $defaults = []; + $requirements = []; + $options = []; + $condition = null; + $prefixes = []; + $paths = []; + $hosts = []; + + /** @var \DOMElement $n */ + foreach ($node->getElementsByTagNameNS(self::NAMESPACE_URI, '*') as $n) { + if ($node !== $n->parentNode) { + continue; + } + + switch ($n->localName) { + case 'path': + $paths[$n->getAttribute('locale')] = trim($n->textContent); + break; + case 'host': + $hosts[$n->getAttribute('locale')] = trim($n->textContent); + break; + case 'prefix': + $prefixes[$n->getAttribute('locale')] = trim($n->textContent); + break; + case 'default': + if ($this->isElementValueNull($n)) { + $defaults[$n->getAttribute('key')] = null; + } else { + $defaults[$n->getAttribute('key')] = $this->parseDefaultsConfig($n, $path); + } + + break; + case 'requirement': + $requirements[$n->getAttribute('key')] = trim($n->textContent); + break; + case 'option': + $options[$n->getAttribute('key')] = XmlUtils::phpize(trim($n->textContent)); + break; + case 'condition': + $condition = trim($n->textContent); + break; + case 'resource': + break; + default: + throw new \InvalidArgumentException(sprintf('Unknown tag "%s" used in file "%s". Expected "default", "requirement", "option" or "condition".', $n->localName, $path)); + } + } + + if ($controller = $node->getAttribute('controller')) { + if (isset($defaults['_controller'])) { + $name = $node->hasAttribute('id') ? sprintf('"%s".', $node->getAttribute('id')) : sprintf('the "%s" tag.', $node->tagName); + + throw new \InvalidArgumentException(sprintf('The routing file "%s" must not specify both the "controller" attribute and the defaults key "_controller" for ', $path).$name); + } + + $defaults['_controller'] = $controller; + } + if ($node->hasAttribute('locale')) { + $defaults['_locale'] = $node->getAttribute('locale'); + } + if ($node->hasAttribute('format')) { + $defaults['_format'] = $node->getAttribute('format'); + } + if ($node->hasAttribute('utf8')) { + $options['utf8'] = XmlUtils::phpize($node->getAttribute('utf8')); + } + if ($stateless = $node->getAttribute('stateless')) { + if (isset($defaults['_stateless'])) { + $name = $node->hasAttribute('id') ? sprintf('"%s".', $node->getAttribute('id')) : sprintf('the "%s" tag.', $node->tagName); + + throw new \InvalidArgumentException(sprintf('The routing file "%s" must not specify both the "stateless" attribute and the defaults key "_stateless" for ', $path).$name); + } + + $defaults['_stateless'] = XmlUtils::phpize($stateless); + } + + if (!$hosts) { + $hosts = $node->hasAttribute('host') ? $node->getAttribute('host') : null; + } + + return [$defaults, $requirements, $options, $condition, $paths, $prefixes, $hosts]; + } + + /** + * Parses the "default" elements. + */ + private function parseDefaultsConfig(\DOMElement $element, string $path): array|bool|float|int|string|null + { + if ($this->isElementValueNull($element)) { + return null; + } + + // Check for existing element nodes in the default element. There can + // only be a single element inside a default element. So this element + // (if one was found) can safely be returned. + foreach ($element->childNodes as $child) { + if (!$child instanceof \DOMElement) { + continue; + } + + if (self::NAMESPACE_URI !== $child->namespaceURI) { + continue; + } + + return $this->parseDefaultNode($child, $path); + } + + // If the default element doesn't contain a nested "bool", "int", "float", + // "string", "list", or "map" element, the element contents will be treated + // as the string value of the associated default option. + return trim($element->textContent); + } + + /** + * Recursively parses the value of a "default" element. + * + * @throws \InvalidArgumentException when the XML is invalid + */ + private function parseDefaultNode(\DOMElement $node, string $path): array|bool|float|int|string|null + { + if ($this->isElementValueNull($node)) { + return null; + } + + switch ($node->localName) { + case 'bool': + return 'true' === trim($node->nodeValue) || '1' === trim($node->nodeValue); + case 'int': + return (int) trim($node->nodeValue); + case 'float': + return (float) trim($node->nodeValue); + case 'string': + return trim($node->nodeValue); + case 'list': + $list = []; + + foreach ($node->childNodes as $element) { + if (!$element instanceof \DOMElement) { + continue; + } + + if (self::NAMESPACE_URI !== $element->namespaceURI) { + continue; + } + + $list[] = $this->parseDefaultNode($element, $path); + } + + return $list; + case 'map': + $map = []; + + foreach ($node->childNodes as $element) { + if (!$element instanceof \DOMElement) { + continue; + } + + if (self::NAMESPACE_URI !== $element->namespaceURI) { + continue; + } + + $map[$element->getAttribute('key')] = $this->parseDefaultNode($element, $path); + } + + return $map; + default: + throw new \InvalidArgumentException(sprintf('Unknown tag "%s" used in file "%s". Expected "bool", "int", "float", "string", "list", or "map".', $node->localName, $path)); + } + } + + private function isElementValueNull(\DOMElement $element): bool + { + $namespaceUri = 'http://www.w3.org/2001/XMLSchema-instance'; + + if (!$element->hasAttributeNS($namespaceUri, 'nil')) { + return false; + } + + return 'true' === $element->getAttributeNS($namespaceUri, 'nil') || '1' === $element->getAttributeNS($namespaceUri, 'nil'); + } + + /** + * Parses the deprecation elements. + * + * @throws \InvalidArgumentException When the XML is invalid + */ + private function parseDeprecation(\DOMElement $node, string $path): array + { + $deprecatedNode = null; + foreach ($node->childNodes as $child) { + if (!$child instanceof \DOMElement || self::NAMESPACE_URI !== $child->namespaceURI) { + continue; + } + if ('deprecated' !== $child->localName) { + throw new \InvalidArgumentException(sprintf('Invalid child element "%s" defined for alias "%s" in "%s".', $child->localName, $node->getAttribute('id'), $path)); + } + + $deprecatedNode = $child; + } + + if (null === $deprecatedNode) { + return []; + } + + if (!$deprecatedNode->hasAttribute('package')) { + throw new \InvalidArgumentException(sprintf('The element in file "%s" must have a "package" attribute.', $path)); + } + if (!$deprecatedNode->hasAttribute('version')) { + throw new \InvalidArgumentException(sprintf('The element in file "%s" must have a "version" attribute.', $path)); + } + + return [ + 'package' => $deprecatedNode->getAttribute('package'), + 'version' => $deprecatedNode->getAttribute('version'), + 'message' => trim($deprecatedNode->nodeValue), + ]; + } +} diff --git a/3rdparty/symfony/routing/Loader/YamlFileLoader.php b/3rdparty/symfony/routing/Loader/YamlFileLoader.php new file mode 100644 index 00000000..9605e9a8 --- /dev/null +++ b/3rdparty/symfony/routing/Loader/YamlFileLoader.php @@ -0,0 +1,302 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Component\Routing\Loader; + +use Symfony\Component\Config\Loader\FileLoader; +use Symfony\Component\Config\Resource\FileResource; +use Symfony\Component\Routing\Loader\Configurator\Traits\HostTrait; +use Symfony\Component\Routing\Loader\Configurator\Traits\LocalizedRouteTrait; +use Symfony\Component\Routing\Loader\Configurator\Traits\PrefixTrait; +use Symfony\Component\Routing\RouteCollection; +use Symfony\Component\Yaml\Exception\ParseException; +use Symfony\Component\Yaml\Parser as YamlParser; +use Symfony\Component\Yaml\Yaml; + +/** + * YamlFileLoader loads Yaml routing files. + * + * @author Fabien Potencier + * @author Tobias Schultze + */ +class YamlFileLoader extends FileLoader +{ + use HostTrait; + use LocalizedRouteTrait; + use PrefixTrait; + + private const AVAILABLE_KEYS = [ + 'resource', 'type', 'prefix', 'path', 'host', 'schemes', 'methods', 'defaults', 'requirements', 'options', 'condition', 'controller', 'name_prefix', 'trailing_slash_on_root', 'locale', 'format', 'utf8', 'exclude', 'stateless', + ]; + private YamlParser $yamlParser; + + /** + * @throws \InvalidArgumentException When a route can't be parsed because YAML is invalid + */ + public function load(mixed $file, ?string $type = null): RouteCollection + { + $path = $this->locator->locate($file); + + if (!stream_is_local($path)) { + throw new \InvalidArgumentException(sprintf('This is not a local file "%s".', $path)); + } + + if (!file_exists($path)) { + throw new \InvalidArgumentException(sprintf('File "%s" not found.', $path)); + } + + $this->yamlParser ??= new YamlParser(); + + try { + $parsedConfig = $this->yamlParser->parseFile($path, Yaml::PARSE_CONSTANT); + } catch (ParseException $e) { + throw new \InvalidArgumentException(sprintf('The file "%s" does not contain valid YAML: ', $path).$e->getMessage(), 0, $e); + } + + $collection = new RouteCollection(); + $collection->addResource(new FileResource($path)); + + // empty file + if (null === $parsedConfig) { + return $collection; + } + + // not an array + if (!\is_array($parsedConfig)) { + throw new \InvalidArgumentException(sprintf('The file "%s" must contain a YAML array.', $path)); + } + + foreach ($parsedConfig as $name => $config) { + if (str_starts_with($name, 'when@')) { + if (!$this->env || 'when@'.$this->env !== $name) { + continue; + } + + foreach ($config as $name => $config) { + $this->validate($config, $name.'" when "@'.$this->env, $path); + + if (isset($config['resource'])) { + $this->parseImport($collection, $config, $path, $file); + } else { + $this->parseRoute($collection, $name, $config, $path); + } + } + + continue; + } + + $this->validate($config, $name, $path); + + if (isset($config['resource'])) { + $this->parseImport($collection, $config, $path, $file); + } else { + $this->parseRoute($collection, $name, $config, $path); + } + } + + return $collection; + } + + public function supports(mixed $resource, ?string $type = null): bool + { + return \is_string($resource) && \in_array(pathinfo($resource, \PATHINFO_EXTENSION), ['yml', 'yaml'], true) && (!$type || 'yaml' === $type); + } + + /** + * Parses a route and adds it to the RouteCollection. + * + * @return void + */ + protected function parseRoute(RouteCollection $collection, string $name, array $config, string $path) + { + if (isset($config['alias'])) { + $alias = $collection->addAlias($name, $config['alias']); + $deprecation = $config['deprecated'] ?? null; + if (null !== $deprecation) { + $alias->setDeprecated( + $deprecation['package'], + $deprecation['version'], + $deprecation['message'] ?? '' + ); + } + + return; + } + + $defaults = $config['defaults'] ?? []; + $requirements = $config['requirements'] ?? []; + $options = $config['options'] ?? []; + + foreach ($requirements as $placeholder => $requirement) { + if (\is_int($placeholder)) { + throw new \InvalidArgumentException(sprintf('A placeholder name must be a string (%d given). Did you forget to specify the placeholder key for the requirement "%s" of route "%s" in "%s"?', $placeholder, $requirement, $name, $path)); + } + } + + if (isset($config['controller'])) { + $defaults['_controller'] = $config['controller']; + } + if (isset($config['locale'])) { + $defaults['_locale'] = $config['locale']; + } + if (isset($config['format'])) { + $defaults['_format'] = $config['format']; + } + if (isset($config['utf8'])) { + $options['utf8'] = $config['utf8']; + } + if (isset($config['stateless'])) { + $defaults['_stateless'] = $config['stateless']; + } + + $routes = $this->createLocalizedRoute($collection, $name, $config['path']); + $routes->addDefaults($defaults); + $routes->addRequirements($requirements); + $routes->addOptions($options); + $routes->setSchemes($config['schemes'] ?? []); + $routes->setMethods($config['methods'] ?? []); + $routes->setCondition($config['condition'] ?? null); + + if (isset($config['host'])) { + $this->addHost($routes, $config['host']); + } + } + + /** + * Parses an import and adds the routes in the resource to the RouteCollection. + * + * @return void + */ + protected function parseImport(RouteCollection $collection, array $config, string $path, string $file) + { + $type = $config['type'] ?? null; + $prefix = $config['prefix'] ?? ''; + $defaults = $config['defaults'] ?? []; + $requirements = $config['requirements'] ?? []; + $options = $config['options'] ?? []; + $host = $config['host'] ?? null; + $condition = $config['condition'] ?? null; + $schemes = $config['schemes'] ?? null; + $methods = $config['methods'] ?? null; + $trailingSlashOnRoot = $config['trailing_slash_on_root'] ?? true; + $namePrefix = $config['name_prefix'] ?? null; + $exclude = $config['exclude'] ?? null; + + if (isset($config['controller'])) { + $defaults['_controller'] = $config['controller']; + } + if (isset($config['locale'])) { + $defaults['_locale'] = $config['locale']; + } + if (isset($config['format'])) { + $defaults['_format'] = $config['format']; + } + if (isset($config['utf8'])) { + $options['utf8'] = $config['utf8']; + } + if (isset($config['stateless'])) { + $defaults['_stateless'] = $config['stateless']; + } + + $this->setCurrentDir(\dirname($path)); + + /** @var RouteCollection[] $imported */ + $imported = $this->import($config['resource'], $type, false, $file, $exclude) ?: []; + + if (!\is_array($imported)) { + $imported = [$imported]; + } + + foreach ($imported as $subCollection) { + $this->addPrefix($subCollection, $prefix, $trailingSlashOnRoot); + + if (null !== $host) { + $this->addHost($subCollection, $host); + } + if (null !== $condition) { + $subCollection->setCondition($condition); + } + if (null !== $schemes) { + $subCollection->setSchemes($schemes); + } + if (null !== $methods) { + $subCollection->setMethods($methods); + } + if (null !== $namePrefix) { + $subCollection->addNamePrefix($namePrefix); + } + $subCollection->addDefaults($defaults); + $subCollection->addRequirements($requirements); + $subCollection->addOptions($options); + + $collection->addCollection($subCollection); + } + } + + /** + * @return void + * + * @throws \InvalidArgumentException If one of the provided config keys is not supported, + * something is missing or the combination is nonsense + */ + protected function validate(mixed $config, string $name, string $path) + { + if (!\is_array($config)) { + throw new \InvalidArgumentException(sprintf('The definition of "%s" in "%s" must be a YAML array.', $name, $path)); + } + if (isset($config['alias'])) { + $this->validateAlias($config, $name, $path); + + return; + } + if ($extraKeys = array_diff(array_keys($config), self::AVAILABLE_KEYS)) { + throw new \InvalidArgumentException(sprintf('The routing file "%s" contains unsupported keys for "%s": "%s". Expected one of: "%s".', $path, $name, implode('", "', $extraKeys), implode('", "', self::AVAILABLE_KEYS))); + } + if (isset($config['resource']) && isset($config['path'])) { + throw new \InvalidArgumentException(sprintf('The routing file "%s" must not specify both the "resource" key and the "path" key for "%s". Choose between an import and a route definition.', $path, $name)); + } + if (!isset($config['resource']) && isset($config['type'])) { + throw new \InvalidArgumentException(sprintf('The "type" key for the route definition "%s" in "%s" is unsupported. It is only available for imports in combination with the "resource" key.', $name, $path)); + } + if (!isset($config['resource']) && !isset($config['path'])) { + throw new \InvalidArgumentException(sprintf('You must define a "path" for the route "%s" in file "%s".', $name, $path)); + } + if (isset($config['controller']) && isset($config['defaults']['_controller'])) { + throw new \InvalidArgumentException(sprintf('The routing file "%s" must not specify both the "controller" key and the defaults key "_controller" for "%s".', $path, $name)); + } + if (isset($config['stateless']) && isset($config['defaults']['_stateless'])) { + throw new \InvalidArgumentException(sprintf('The routing file "%s" must not specify both the "stateless" key and the defaults key "_stateless" for "%s".', $path, $name)); + } + } + + /** + * @throws \InvalidArgumentException If one of the provided config keys is not supported, + * something is missing or the combination is nonsense + */ + private function validateAlias(array $config, string $name, string $path): void + { + foreach ($config as $key => $value) { + if (!\in_array($key, ['alias', 'deprecated'], true)) { + throw new \InvalidArgumentException(sprintf('The routing file "%s" must not specify other keys than "alias" and "deprecated" for "%s".', $path, $name)); + } + + if ('deprecated' === $key) { + if (!isset($value['package'])) { + throw new \InvalidArgumentException(sprintf('The routing file "%s" must specify the attribute "package" of the "deprecated" option for "%s".', $path, $name)); + } + + if (!isset($value['version'])) { + throw new \InvalidArgumentException(sprintf('The routing file "%s" must specify the attribute "version" of the "deprecated" option for "%s".', $path, $name)); + } + } + } + } +} diff --git a/3rdparty/symfony/routing/Loader/schema/routing/routing-1.0.xsd b/3rdparty/symfony/routing/Loader/schema/routing/routing-1.0.xsd new file mode 100644 index 00000000..1b24dfdc --- /dev/null +++ b/3rdparty/symfony/routing/Loader/schema/routing/routing-1.0.xsd @@ -0,0 +1,201 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/3rdparty/symfony/routing/Matcher/CompiledUrlMatcher.php b/3rdparty/symfony/routing/Matcher/CompiledUrlMatcher.php new file mode 100644 index 00000000..ae13fd70 --- /dev/null +++ b/3rdparty/symfony/routing/Matcher/CompiledUrlMatcher.php @@ -0,0 +1,31 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Component\Routing\Matcher; + +use Symfony\Component\Routing\Matcher\Dumper\CompiledUrlMatcherTrait; +use Symfony\Component\Routing\RequestContext; + +/** + * Matches URLs based on rules dumped by CompiledUrlMatcherDumper. + * + * @author Nicolas Grekas + */ +class CompiledUrlMatcher extends UrlMatcher +{ + use CompiledUrlMatcherTrait; + + public function __construct(array $compiledRoutes, RequestContext $context) + { + $this->context = $context; + [$this->matchHost, $this->staticRoutes, $this->regexpList, $this->dynamicRoutes, $this->checkCondition] = $compiledRoutes; + } +} diff --git a/3rdparty/symfony/routing/Matcher/Dumper/CompiledUrlMatcherDumper.php b/3rdparty/symfony/routing/Matcher/Dumper/CompiledUrlMatcherDumper.php new file mode 100644 index 00000000..a7639cd4 --- /dev/null +++ b/3rdparty/symfony/routing/Matcher/Dumper/CompiledUrlMatcherDumper.php @@ -0,0 +1,501 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Component\Routing\Matcher\Dumper; + +use Symfony\Component\ExpressionLanguage\ExpressionFunctionProviderInterface; +use Symfony\Component\ExpressionLanguage\ExpressionLanguage; +use Symfony\Component\Routing\Route; +use Symfony\Component\Routing\RouteCollection; + +/** + * CompiledUrlMatcherDumper creates PHP arrays to be used with CompiledUrlMatcher. + * + * @author Fabien Potencier + * @author Tobias Schultze + * @author Arnaud Le Blanc + * @author Nicolas Grekas + */ +class CompiledUrlMatcherDumper extends MatcherDumper +{ + private ExpressionLanguage $expressionLanguage; + private ?\Exception $signalingException = null; + + /** + * @var ExpressionFunctionProviderInterface[] + */ + private array $expressionLanguageProviders = []; + + public function dump(array $options = []): string + { + return <<generateCompiledRoutes()}]; + +EOF; + } + + /** + * @return void + */ + public function addExpressionLanguageProvider(ExpressionFunctionProviderInterface $provider) + { + $this->expressionLanguageProviders[] = $provider; + } + + /** + * Generates the arrays for CompiledUrlMatcher's constructor. + */ + public function getCompiledRoutes(bool $forDump = false): array + { + // Group hosts by same-suffix, re-order when possible + $matchHost = false; + $routes = new StaticPrefixCollection(); + foreach ($this->getRoutes()->all() as $name => $route) { + if ($host = $route->getHost()) { + $matchHost = true; + $host = '/'.strtr(strrev($host), '}.{', '(/)'); + } + + $routes->addRoute($host ?: '/(.*)', [$name, $route]); + } + + if ($matchHost) { + $compiledRoutes = [true]; + $routes = $routes->populateCollection(new RouteCollection()); + } else { + $compiledRoutes = [false]; + $routes = $this->getRoutes(); + } + + [$staticRoutes, $dynamicRoutes] = $this->groupStaticRoutes($routes); + + $conditions = [null]; + $compiledRoutes[] = $this->compileStaticRoutes($staticRoutes, $conditions); + $chunkLimit = \count($dynamicRoutes); + + while (true) { + try { + $this->signalingException = new \RuntimeException('Compilation failed: regular expression is too large'); + $compiledRoutes = array_merge($compiledRoutes, $this->compileDynamicRoutes($dynamicRoutes, $matchHost, $chunkLimit, $conditions)); + + break; + } catch (\Exception $e) { + if (1 < $chunkLimit && $this->signalingException === $e) { + $chunkLimit = 1 + ($chunkLimit >> 1); + continue; + } + throw $e; + } + } + + if ($forDump) { + $compiledRoutes[2] = $compiledRoutes[4]; + } + unset($conditions[0]); + + if ($conditions) { + foreach ($conditions as $expression => $condition) { + $conditions[$expression] = "case {$condition}: return {$expression};"; + } + + $checkConditionCode = <<indent(implode("\n", $conditions), 3)} + } + } +EOF; + $compiledRoutes[4] = $forDump ? $checkConditionCode.",\n" : eval('return '.$checkConditionCode.';'); + } else { + $compiledRoutes[4] = $forDump ? " null, // \$checkCondition\n" : null; + } + + return $compiledRoutes; + } + + private function generateCompiledRoutes(): string + { + [$matchHost, $staticRoutes, $regexpCode, $dynamicRoutes, $checkConditionCode] = $this->getCompiledRoutes(true); + + $code = self::export($matchHost).', // $matchHost'."\n"; + + $code .= '[ // $staticRoutes'."\n"; + foreach ($staticRoutes as $path => $routes) { + $code .= sprintf(" %s => [\n", self::export($path)); + foreach ($routes as $route) { + $code .= vsprintf(" [%s, %s, %s, %s, %s, %s, %s],\n", array_map([__CLASS__, 'export'], $route)); + } + $code .= " ],\n"; + } + $code .= "],\n"; + + $code .= sprintf("[ // \$regexpList%s\n],\n", $regexpCode); + + $code .= '[ // $dynamicRoutes'."\n"; + foreach ($dynamicRoutes as $path => $routes) { + $code .= sprintf(" %s => [\n", self::export($path)); + foreach ($routes as $route) { + $code .= vsprintf(" [%s, %s, %s, %s, %s, %s, %s],\n", array_map([__CLASS__, 'export'], $route)); + } + $code .= " ],\n"; + } + $code .= "],\n"; + $code = preg_replace('/ => \[\n (\[.+?),\n \],/', ' => [$1],', $code); + + return $this->indent($code, 1).$checkConditionCode; + } + + /** + * Splits static routes from dynamic routes, so that they can be matched first, using a simple switch. + */ + private function groupStaticRoutes(RouteCollection $collection): array + { + $staticRoutes = $dynamicRegex = []; + $dynamicRoutes = new RouteCollection(); + + foreach ($collection->all() as $name => $route) { + $compiledRoute = $route->compile(); + $staticPrefix = rtrim($compiledRoute->getStaticPrefix(), '/'); + $hostRegex = $compiledRoute->getHostRegex(); + $regex = $compiledRoute->getRegex(); + if ($hasTrailingSlash = '/' !== $route->getPath()) { + $pos = strrpos($regex, '$'); + $hasTrailingSlash = '/' === $regex[$pos - 1]; + $regex = substr_replace($regex, '/?$', $pos - $hasTrailingSlash, 1 + $hasTrailingSlash); + } + + if (!$compiledRoute->getPathVariables()) { + $host = !$compiledRoute->getHostVariables() ? $route->getHost() : ''; + $url = $route->getPath(); + if ($hasTrailingSlash) { + $url = substr($url, 0, -1); + } + foreach ($dynamicRegex as [$hostRx, $rx, $prefix]) { + if (('' === $prefix || str_starts_with($url, $prefix)) && (preg_match($rx, $url) || preg_match($rx, $url.'/')) && (!$host || !$hostRx || preg_match($hostRx, $host))) { + $dynamicRegex[] = [$hostRegex, $regex, $staticPrefix]; + $dynamicRoutes->add($name, $route); + continue 2; + } + } + + $staticRoutes[$url][$name] = [$route, $hasTrailingSlash]; + } else { + $dynamicRegex[] = [$hostRegex, $regex, $staticPrefix]; + $dynamicRoutes->add($name, $route); + } + } + + return [$staticRoutes, $dynamicRoutes]; + } + + /** + * Compiles static routes in a switch statement. + * + * Condition-less paths are put in a static array in the switch's default, with generic matching logic. + * Paths that can match two or more routes, or have user-specified conditions are put in separate switch's cases. + * + * @throws \LogicException + */ + private function compileStaticRoutes(array $staticRoutes, array &$conditions): array + { + if (!$staticRoutes) { + return []; + } + $compiledRoutes = []; + + foreach ($staticRoutes as $url => $routes) { + $compiledRoutes[$url] = []; + foreach ($routes as $name => [$route, $hasTrailingSlash]) { + $compiledRoutes[$url][] = $this->compileRoute($route, $name, (!$route->compile()->getHostVariables() ? $route->getHost() : $route->compile()->getHostRegex()) ?: null, $hasTrailingSlash, false, $conditions); + } + } + + return $compiledRoutes; + } + + /** + * Compiles a regular expression followed by a switch statement to match dynamic routes. + * + * The regular expression matches both the host and the pathinfo at the same time. For stellar performance, + * it is built as a tree of patterns, with re-ordering logic to group same-prefix routes together when possible. + * + * Patterns are named so that we know which one matched (https://pcre.org/current/doc/html/pcre2syntax.html#SEC23). + * This name is used to "switch" to the additional logic required to match the final route. + * + * Condition-less paths are put in a static array in the switch's default, with generic matching logic. + * Paths that can match two or more routes, or have user-specified conditions are put in separate switch's cases. + * + * Last but not least: + * - Because it is not possible to mix unicode/non-unicode patterns in a single regexp, several of them can be generated. + * - The same regexp can be used several times when the logic in the switch rejects the match. When this happens, the + * matching-but-failing subpattern is excluded by replacing its name by "(*F)", which forces a failure-to-match. + * To ease this backlisting operation, the name of subpatterns is also the string offset where the replacement should occur. + */ + private function compileDynamicRoutes(RouteCollection $collection, bool $matchHost, int $chunkLimit, array &$conditions): array + { + if (!$collection->all()) { + return [[], [], '']; + } + $regexpList = []; + $code = ''; + $state = (object) [ + 'regexMark' => 0, + 'regex' => [], + 'routes' => [], + 'mark' => 0, + 'markTail' => 0, + 'hostVars' => [], + 'vars' => [], + ]; + $state->getVars = static function ($m) use ($state) { + if ('_route' === $m[1]) { + return '?:'; + } + + $state->vars[] = $m[1]; + + return ''; + }; + + $chunkSize = 0; + $prev = null; + $perModifiers = []; + foreach ($collection->all() as $name => $route) { + preg_match('#[a-zA-Z]*$#', $route->compile()->getRegex(), $rx); + if ($chunkLimit < ++$chunkSize || $prev !== $rx[0] && $route->compile()->getPathVariables()) { + $chunkSize = 1; + $routes = new RouteCollection(); + $perModifiers[] = [$rx[0], $routes]; + $prev = $rx[0]; + } + $routes->add($name, $route); + } + + foreach ($perModifiers as [$modifiers, $routes]) { + $prev = false; + $perHost = []; + foreach ($routes->all() as $name => $route) { + $regex = $route->compile()->getHostRegex(); + if ($prev !== $regex) { + $routes = new RouteCollection(); + $perHost[] = [$regex, $routes]; + $prev = $regex; + } + $routes->add($name, $route); + } + $prev = false; + $rx = '{^(?'; + $code .= "\n {$state->mark} => ".self::export($rx); + $startingMark = $state->mark; + $state->mark += \strlen($rx); + $state->regex = $rx; + + foreach ($perHost as [$hostRegex, $routes]) { + if ($matchHost) { + if ($hostRegex) { + preg_match('#^.\^(.*)\$.[a-zA-Z]*$#', $hostRegex, $rx); + $state->vars = []; + $hostRegex = '(?i:'.preg_replace_callback('#\?P<([^>]++)>#', $state->getVars, $rx[1]).')\.'; + $state->hostVars = $state->vars; + } else { + $hostRegex = '(?:(?:[^./]*+\.)++)'; + $state->hostVars = []; + } + $state->mark += \strlen($rx = ($prev ? ')' : '')."|{$hostRegex}(?"); + $code .= "\n .".self::export($rx); + $state->regex .= $rx; + $prev = true; + } + + $tree = new StaticPrefixCollection(); + foreach ($routes->all() as $name => $route) { + preg_match('#^.\^(.*)\$.[a-zA-Z]*$#', $route->compile()->getRegex(), $rx); + + $state->vars = []; + $regex = preg_replace_callback('#\?P<([^>]++)>#', $state->getVars, $rx[1]); + if ($hasTrailingSlash = '/' !== $regex && '/' === $regex[-1]) { + $regex = substr($regex, 0, -1); + } + $hasTrailingVar = (bool) preg_match('#\{[\w\x80-\xFF]+\}/?$#', $route->getPath()); + + $tree->addRoute($regex, [$name, $regex, $state->vars, $route, $hasTrailingSlash, $hasTrailingVar]); + } + + $code .= $this->compileStaticPrefixCollection($tree, $state, 0, $conditions); + } + if ($matchHost) { + $code .= "\n .')'"; + $state->regex .= ')'; + } + $rx = ")/?$}{$modifiers}"; + $code .= "\n .'{$rx}',"; + $state->regex .= $rx; + $state->markTail = 0; + + // if the regex is too large, throw a signaling exception to recompute with smaller chunk size + set_error_handler(fn ($type, $message) => throw str_contains($message, $this->signalingException->getMessage()) ? $this->signalingException : new \ErrorException($message)); + try { + preg_match($state->regex, ''); + } finally { + restore_error_handler(); + } + + $regexpList[$startingMark] = $state->regex; + } + + $state->routes[$state->mark][] = [null, null, null, null, false, false, 0]; + unset($state->getVars); + + return [$regexpList, $state->routes, $code]; + } + + /** + * Compiles a regexp tree of subpatterns that matches nested same-prefix routes. + * + * @param \stdClass $state A simple state object that keeps track of the progress of the compilation, + * and gathers the generated switch's "case" and "default" statements + */ + private function compileStaticPrefixCollection(StaticPrefixCollection $tree, \stdClass $state, int $prefixLen, array &$conditions): string + { + $code = ''; + $prevRegex = null; + $routes = $tree->getRoutes(); + + foreach ($routes as $i => $route) { + if ($route instanceof StaticPrefixCollection) { + $prevRegex = null; + $prefix = substr($route->getPrefix(), $prefixLen); + $state->mark += \strlen($rx = "|{$prefix}(?"); + $code .= "\n .".self::export($rx); + $state->regex .= $rx; + $code .= $this->indent($this->compileStaticPrefixCollection($route, $state, $prefixLen + \strlen($prefix), $conditions)); + $code .= "\n .')'"; + $state->regex .= ')'; + ++$state->markTail; + continue; + } + + [$name, $regex, $vars, $route, $hasTrailingSlash, $hasTrailingVar] = $route; + $compiledRoute = $route->compile(); + $vars = array_merge($state->hostVars, $vars); + + if ($compiledRoute->getRegex() === $prevRegex) { + $state->routes[$state->mark][] = $this->compileRoute($route, $name, $vars, $hasTrailingSlash, $hasTrailingVar, $conditions); + continue; + } + + $state->mark += 3 + $state->markTail + \strlen($regex) - $prefixLen; + $state->markTail = 2 + \strlen($state->mark); + $rx = sprintf('|%s(*:%s)', substr($regex, $prefixLen), $state->mark); + $code .= "\n .".self::export($rx); + $state->regex .= $rx; + + $prevRegex = $compiledRoute->getRegex(); + $state->routes[$state->mark] = [$this->compileRoute($route, $name, $vars, $hasTrailingSlash, $hasTrailingVar, $conditions)]; + } + + return $code; + } + + /** + * Compiles a single Route to PHP code used to match it against the path info. + */ + private function compileRoute(Route $route, string $name, string|array|null $vars, bool $hasTrailingSlash, bool $hasTrailingVar, array &$conditions): array + { + $defaults = $route->getDefaults(); + + if (isset($defaults['_canonical_route'])) { + $name = $defaults['_canonical_route']; + unset($defaults['_canonical_route']); + } + + if ($condition = $route->getCondition()) { + $condition = $this->getExpressionLanguage()->compile($condition, ['context', 'request', 'params']); + $condition = $conditions[$condition] ??= (str_contains($condition, '$request') ? 1 : -1) * \count($conditions); + } else { + $condition = null; + } + + return [ + ['_route' => $name] + $defaults, + $vars, + array_flip($route->getMethods()) ?: null, + array_flip($route->getSchemes()) ?: null, + $hasTrailingSlash, + $hasTrailingVar, + $condition, + ]; + } + + private function getExpressionLanguage(): ExpressionLanguage + { + if (!isset($this->expressionLanguage)) { + if (!class_exists(ExpressionLanguage::class)) { + throw new \LogicException('Unable to use expressions as the Symfony ExpressionLanguage component is not installed. Try running "composer require symfony/expression-language".'); + } + $this->expressionLanguage = new ExpressionLanguage(null, $this->expressionLanguageProviders); + } + + return $this->expressionLanguage; + } + + private function indent(string $code, int $level = 1): string + { + return preg_replace('/^./m', str_repeat(' ', $level).'$0', $code); + } + + /** + * @internal + */ + public static function export(mixed $value): string + { + if (null === $value) { + return 'null'; + } + if (!\is_array($value)) { + if (\is_object($value)) { + throw new \InvalidArgumentException('Symfony\Component\Routing\Route cannot contain objects.'); + } + + return str_replace("\n", '\'."\n".\'', var_export($value, true)); + } + if (!$value) { + return '[]'; + } + + $i = 0; + $export = '['; + + foreach ($value as $k => $v) { + if ($i === $k) { + ++$i; + } else { + $export .= self::export($k).' => '; + + if (\is_int($k) && $i < $k) { + $i = 1 + $k; + } + } + + $export .= self::export($v).', '; + } + + return substr_replace($export, ']', -2); + } +} diff --git a/3rdparty/symfony/routing/Matcher/Dumper/CompiledUrlMatcherTrait.php b/3rdparty/symfony/routing/Matcher/Dumper/CompiledUrlMatcherTrait.php new file mode 100644 index 00000000..50abf458 --- /dev/null +++ b/3rdparty/symfony/routing/Matcher/Dumper/CompiledUrlMatcherTrait.php @@ -0,0 +1,186 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Component\Routing\Matcher\Dumper; + +use Symfony\Component\Routing\Exception\MethodNotAllowedException; +use Symfony\Component\Routing\Exception\NoConfigurationException; +use Symfony\Component\Routing\Exception\ResourceNotFoundException; +use Symfony\Component\Routing\Matcher\RedirectableUrlMatcherInterface; +use Symfony\Component\Routing\RequestContext; + +/** + * @author Nicolas Grekas + * + * @internal + * + * @property RequestContext $context + */ +trait CompiledUrlMatcherTrait +{ + private bool $matchHost = false; + private array $staticRoutes = []; + private array $regexpList = []; + private array $dynamicRoutes = []; + private ?\Closure $checkCondition; + + public function match(string $pathinfo): array + { + $allow = $allowSchemes = []; + if ($ret = $this->doMatch($pathinfo, $allow, $allowSchemes)) { + return $ret; + } + if ($allow) { + throw new MethodNotAllowedException(array_keys($allow)); + } + if (!$this instanceof RedirectableUrlMatcherInterface) { + throw new ResourceNotFoundException(sprintf('No routes found for "%s".', $pathinfo)); + } + if (!\in_array($this->context->getMethod(), ['HEAD', 'GET'], true)) { + // no-op + } elseif ($allowSchemes) { + redirect_scheme: + $scheme = $this->context->getScheme(); + $this->context->setScheme(key($allowSchemes)); + try { + if ($ret = $this->doMatch($pathinfo)) { + return $this->redirect($pathinfo, $ret['_route'], $this->context->getScheme()) + $ret; + } + } finally { + $this->context->setScheme($scheme); + } + } elseif ('/' !== $trimmedPathinfo = rtrim($pathinfo, '/') ?: '/') { + $pathinfo = $trimmedPathinfo === $pathinfo ? $pathinfo.'/' : $trimmedPathinfo; + if ($ret = $this->doMatch($pathinfo, $allow, $allowSchemes)) { + return $this->redirect($pathinfo, $ret['_route']) + $ret; + } + if ($allowSchemes) { + goto redirect_scheme; + } + } + + throw new ResourceNotFoundException(sprintf('No routes found for "%s".', $pathinfo)); + } + + private function doMatch(string $pathinfo, array &$allow = [], array &$allowSchemes = []): array + { + $allow = $allowSchemes = []; + $pathinfo = rawurldecode($pathinfo) ?: '/'; + $trimmedPathinfo = rtrim($pathinfo, '/') ?: '/'; + $context = $this->context; + $requestMethod = $canonicalMethod = $context->getMethod(); + + if ($this->matchHost) { + $host = strtolower($context->getHost()); + } + + if ('HEAD' === $requestMethod) { + $canonicalMethod = 'GET'; + } + $supportsRedirections = 'GET' === $canonicalMethod && $this instanceof RedirectableUrlMatcherInterface; + + foreach ($this->staticRoutes[$trimmedPathinfo] ?? [] as [$ret, $requiredHost, $requiredMethods, $requiredSchemes, $hasTrailingSlash, , $condition]) { + if ($requiredHost) { + if ('{' !== $requiredHost[0] ? $requiredHost !== $host : !preg_match($requiredHost, $host, $hostMatches)) { + continue; + } + if ('{' === $requiredHost[0] && $hostMatches) { + $hostMatches['_route'] = $ret['_route']; + $ret = $this->mergeDefaults($hostMatches, $ret); + } + } + + if ($condition && !($this->checkCondition)($condition, $context, 0 < $condition ? $request ??= $this->request ?: $this->createRequest($pathinfo) : null, $ret)) { + continue; + } + + if ('/' !== $pathinfo && $hasTrailingSlash === ($trimmedPathinfo === $pathinfo)) { + if ($supportsRedirections && (!$requiredMethods || isset($requiredMethods['GET']))) { + return $allow = $allowSchemes = []; + } + continue; + } + + $hasRequiredScheme = !$requiredSchemes || isset($requiredSchemes[$context->getScheme()]); + if ($hasRequiredScheme && $requiredMethods && !isset($requiredMethods[$canonicalMethod]) && !isset($requiredMethods[$requestMethod])) { + $allow += $requiredMethods; + continue; + } + + if (!$hasRequiredScheme) { + $allowSchemes += $requiredSchemes; + continue; + } + + return $ret; + } + + $matchedPathinfo = $this->matchHost ? $host.'.'.$pathinfo : $pathinfo; + + foreach ($this->regexpList as $offset => $regex) { + while (preg_match($regex, $matchedPathinfo, $matches)) { + foreach ($this->dynamicRoutes[$m = (int) $matches['MARK']] as [$ret, $vars, $requiredMethods, $requiredSchemes, $hasTrailingSlash, $hasTrailingVar, $condition]) { + if (0 === $condition) { // marks the last route in the regexp + continue 3; + } + + $hasTrailingVar = $trimmedPathinfo !== $pathinfo && $hasTrailingVar; + + if ($hasTrailingVar && ($hasTrailingSlash || (null === $n = $matches[\count($vars)] ?? null) || '/' !== ($n[-1] ?? '/')) && preg_match($regex, $this->matchHost ? $host.'.'.$trimmedPathinfo : $trimmedPathinfo, $n) && $m === (int) $n['MARK']) { + if ($hasTrailingSlash) { + $matches = $n; + } else { + $hasTrailingVar = false; + } + } + + foreach ($vars as $i => $v) { + if (isset($matches[1 + $i])) { + $ret[$v] = $matches[1 + $i]; + } + } + + if ($condition && !($this->checkCondition)($condition, $context, 0 < $condition ? $request ??= $this->request ?: $this->createRequest($pathinfo) : null, $ret)) { + continue; + } + + if ('/' !== $pathinfo && !$hasTrailingVar && $hasTrailingSlash === ($trimmedPathinfo === $pathinfo)) { + if ($supportsRedirections && (!$requiredMethods || isset($requiredMethods['GET']))) { + return $allow = $allowSchemes = []; + } + continue; + } + + if ($requiredSchemes && !isset($requiredSchemes[$context->getScheme()])) { + $allowSchemes += $requiredSchemes; + continue; + } + + if ($requiredMethods && !isset($requiredMethods[$canonicalMethod]) && !isset($requiredMethods[$requestMethod])) { + $allow += $requiredMethods; + continue; + } + + return $ret; + } + + $regex = substr_replace($regex, 'F', $m - $offset, 1 + \strlen($m)); + $offset += \strlen($m); + } + } + + if ('/' === $pathinfo && !$allow && !$allowSchemes) { + throw new NoConfigurationException(); + } + + return []; + } +} diff --git a/3rdparty/symfony/routing/Matcher/Dumper/MatcherDumper.php b/3rdparty/symfony/routing/Matcher/Dumper/MatcherDumper.php new file mode 100644 index 00000000..085f3ba3 --- /dev/null +++ b/3rdparty/symfony/routing/Matcher/Dumper/MatcherDumper.php @@ -0,0 +1,34 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Component\Routing\Matcher\Dumper; + +use Symfony\Component\Routing\RouteCollection; + +/** + * MatcherDumper is the abstract class for all built-in matcher dumpers. + * + * @author Fabien Potencier + */ +abstract class MatcherDumper implements MatcherDumperInterface +{ + private RouteCollection $routes; + + public function __construct(RouteCollection $routes) + { + $this->routes = $routes; + } + + public function getRoutes(): RouteCollection + { + return $this->routes; + } +} diff --git a/3rdparty/symfony/routing/Matcher/Dumper/MatcherDumperInterface.php b/3rdparty/symfony/routing/Matcher/Dumper/MatcherDumperInterface.php new file mode 100644 index 00000000..92cc4db2 --- /dev/null +++ b/3rdparty/symfony/routing/Matcher/Dumper/MatcherDumperInterface.php @@ -0,0 +1,33 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Component\Routing\Matcher\Dumper; + +use Symfony\Component\Routing\RouteCollection; + +/** + * MatcherDumperInterface is the interface that all matcher dumper classes must implement. + * + * @author Fabien Potencier + */ +interface MatcherDumperInterface +{ + /** + * Dumps a set of routes to a string representation of executable code + * that can then be used to match a request against these routes. + */ + public function dump(array $options = []): string; + + /** + * Gets the routes to dump. + */ + public function getRoutes(): RouteCollection; +} diff --git a/3rdparty/symfony/routing/Matcher/Dumper/StaticPrefixCollection.php b/3rdparty/symfony/routing/Matcher/Dumper/StaticPrefixCollection.php new file mode 100644 index 00000000..42ca799f --- /dev/null +++ b/3rdparty/symfony/routing/Matcher/Dumper/StaticPrefixCollection.php @@ -0,0 +1,204 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Component\Routing\Matcher\Dumper; + +use Symfony\Component\Routing\RouteCollection; + +/** + * Prefix tree of routes preserving routes order. + * + * @author Frank de Jonge + * @author Nicolas Grekas + * + * @internal + */ +class StaticPrefixCollection +{ + private string $prefix; + + /** + * @var string[] + */ + private array $staticPrefixes = []; + + /** + * @var string[] + */ + private array $prefixes = []; + + /** + * @var array[]|self[] + */ + private array $items = []; + + public function __construct(string $prefix = '/') + { + $this->prefix = $prefix; + } + + public function getPrefix(): string + { + return $this->prefix; + } + + /** + * @return array[]|self[] + */ + public function getRoutes(): array + { + return $this->items; + } + + /** + * Adds a route to a group. + */ + public function addRoute(string $prefix, array|self $route): void + { + [$prefix, $staticPrefix] = $this->getCommonPrefix($prefix, $prefix); + + for ($i = \count($this->items) - 1; 0 <= $i; --$i) { + $item = $this->items[$i]; + + [$commonPrefix, $commonStaticPrefix] = $this->getCommonPrefix($prefix, $this->prefixes[$i]); + + if ($this->prefix === $commonPrefix) { + // the new route and a previous one have no common prefix, let's see if they are exclusive to each others + + if ($this->prefix !== $staticPrefix && $this->prefix !== $this->staticPrefixes[$i]) { + // the new route and the previous one have exclusive static prefixes + continue; + } + + if ($this->prefix === $staticPrefix && $this->prefix === $this->staticPrefixes[$i]) { + // the new route and the previous one have no static prefix + break; + } + + if ($this->prefixes[$i] !== $this->staticPrefixes[$i] && $this->prefix === $this->staticPrefixes[$i]) { + // the previous route is non-static and has no static prefix + break; + } + + if ($prefix !== $staticPrefix && $this->prefix === $staticPrefix) { + // the new route is non-static and has no static prefix + break; + } + + continue; + } + + if ($item instanceof self && $this->prefixes[$i] === $commonPrefix) { + // the new route is a child of a previous one, let's nest it + $item->addRoute($prefix, $route); + } else { + // the new route and a previous one have a common prefix, let's merge them + $child = new self($commonPrefix); + [$child->prefixes[0], $child->staticPrefixes[0]] = $child->getCommonPrefix($this->prefixes[$i], $this->prefixes[$i]); + [$child->prefixes[1], $child->staticPrefixes[1]] = $child->getCommonPrefix($prefix, $prefix); + $child->items = [$this->items[$i], $route]; + + $this->staticPrefixes[$i] = $commonStaticPrefix; + $this->prefixes[$i] = $commonPrefix; + $this->items[$i] = $child; + } + + return; + } + + // No optimised case was found, in this case we simple add the route for possible + // grouping when new routes are added. + $this->staticPrefixes[] = $staticPrefix; + $this->prefixes[] = $prefix; + $this->items[] = $route; + } + + /** + * Linearizes back a set of nested routes into a collection. + */ + public function populateCollection(RouteCollection $routes): RouteCollection + { + foreach ($this->items as $route) { + if ($route instanceof self) { + $route->populateCollection($routes); + } else { + $routes->add(...$route); + } + } + + return $routes; + } + + /** + * Gets the full and static common prefixes between two route patterns. + * + * The static prefix stops at last at the first opening bracket. + */ + private function getCommonPrefix(string $prefix, string $anotherPrefix): array + { + $baseLength = \strlen($this->prefix); + $end = min(\strlen($prefix), \strlen($anotherPrefix)); + $staticLength = null; + set_error_handler(self::handleError(...)); + + try { + for ($i = $baseLength; $i < $end && $prefix[$i] === $anotherPrefix[$i]; ++$i) { + if ('(' === $prefix[$i]) { + $staticLength ??= $i; + for ($j = 1 + $i, $n = 1; $j < $end && 0 < $n; ++$j) { + if ($prefix[$j] !== $anotherPrefix[$j]) { + break 2; + } + if ('(' === $prefix[$j]) { + ++$n; + } elseif (')' === $prefix[$j]) { + --$n; + } elseif ('\\' === $prefix[$j] && (++$j === $end || $prefix[$j] !== $anotherPrefix[$j])) { + --$j; + break; + } + } + if (0 < $n) { + break; + } + if (('?' === ($prefix[$j] ?? '') || '?' === ($anotherPrefix[$j] ?? '')) && ($prefix[$j] ?? '') !== ($anotherPrefix[$j] ?? '')) { + break; + } + $subPattern = substr($prefix, $i, $j - $i); + if ($prefix !== $anotherPrefix && !preg_match('/^\(\[[^\]]++\]\+\+\)$/', $subPattern) && !preg_match('{(?> 6) && preg_match('//u', $prefix.' '.$anotherPrefix)) { + do { + // Prevent cutting in the middle of an UTF-8 characters + --$i; + } while (0b10 === (\ord($prefix[$i]) >> 6)); + } + + return [substr($prefix, 0, $i), substr($prefix, 0, $staticLength ?? $i)]; + } + + public static function handleError(int $type, string $msg): bool + { + return str_contains($msg, 'Compilation failed: lookbehind assertion is not fixed length') + || str_contains($msg, 'Compilation failed: length of lookbehind assertion is not limited'); + } +} diff --git a/3rdparty/symfony/routing/Matcher/ExpressionLanguageProvider.php b/3rdparty/symfony/routing/Matcher/ExpressionLanguageProvider.php new file mode 100644 index 00000000..3aeebe69 --- /dev/null +++ b/3rdparty/symfony/routing/Matcher/ExpressionLanguageProvider.php @@ -0,0 +1,51 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Component\Routing\Matcher; + +use Symfony\Component\ExpressionLanguage\ExpressionFunction; +use Symfony\Component\ExpressionLanguage\ExpressionFunctionProviderInterface; +use Symfony\Contracts\Service\ServiceProviderInterface; + +/** + * Exposes functions defined in the request context to route conditions. + * + * @author Ahmed TAILOULOUTE + */ +class ExpressionLanguageProvider implements ExpressionFunctionProviderInterface +{ + private ServiceProviderInterface $functions; + + public function __construct(ServiceProviderInterface $functions) + { + $this->functions = $functions; + } + + public function getFunctions(): array + { + $functions = []; + + foreach ($this->functions->getProvidedServices() as $function => $type) { + $functions[] = new ExpressionFunction( + $function, + static fn (...$args) => sprintf('($context->getParameter(\'_functions\')->get(%s)(%s))', var_export($function, true), implode(', ', $args)), + fn ($values, ...$args) => $values['context']->getParameter('_functions')->get($function)(...$args) + ); + } + + return $functions; + } + + public function get(string $function): callable + { + return $this->functions->get($function); + } +} diff --git a/3rdparty/symfony/routing/Matcher/RedirectableUrlMatcher.php b/3rdparty/symfony/routing/Matcher/RedirectableUrlMatcher.php new file mode 100644 index 00000000..8d1ad4f9 --- /dev/null +++ b/3rdparty/symfony/routing/Matcher/RedirectableUrlMatcher.php @@ -0,0 +1,61 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Component\Routing\Matcher; + +use Symfony\Component\Routing\Exception\ExceptionInterface; +use Symfony\Component\Routing\Exception\ResourceNotFoundException; + +/** + * @author Fabien Potencier + */ +abstract class RedirectableUrlMatcher extends UrlMatcher implements RedirectableUrlMatcherInterface +{ + public function match(string $pathinfo): array + { + try { + return parent::match($pathinfo); + } catch (ResourceNotFoundException $e) { + if (!\in_array($this->context->getMethod(), ['HEAD', 'GET'], true)) { + throw $e; + } + + if ($this->allowSchemes) { + redirect_scheme: + $scheme = $this->context->getScheme(); + $this->context->setScheme(current($this->allowSchemes)); + try { + $ret = parent::match($pathinfo); + + return $this->redirect($pathinfo, $ret['_route'] ?? null, $this->context->getScheme()) + $ret; + } catch (ExceptionInterface) { + throw $e; + } finally { + $this->context->setScheme($scheme); + } + } elseif ('/' === $trimmedPathinfo = rtrim($pathinfo, '/') ?: '/') { + throw $e; + } else { + try { + $pathinfo = $trimmedPathinfo === $pathinfo ? $pathinfo.'/' : $trimmedPathinfo; + $ret = parent::match($pathinfo); + + return $this->redirect($pathinfo, $ret['_route'] ?? null) + $ret; + } catch (ExceptionInterface) { + if ($this->allowSchemes) { + goto redirect_scheme; + } + throw $e; + } + } + } + } +} diff --git a/3rdparty/symfony/routing/Matcher/RedirectableUrlMatcherInterface.php b/3rdparty/symfony/routing/Matcher/RedirectableUrlMatcherInterface.php new file mode 100644 index 00000000..e4bcedda --- /dev/null +++ b/3rdparty/symfony/routing/Matcher/RedirectableUrlMatcherInterface.php @@ -0,0 +1,29 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Component\Routing\Matcher; + +/** + * RedirectableUrlMatcherInterface knows how to redirect the user. + * + * @author Fabien Potencier + */ +interface RedirectableUrlMatcherInterface +{ + /** + * Redirects the user to another URL and returns the parameters for the redirection. + * + * @param string $path The path info to redirect to + * @param string $route The route name that matched + * @param string|null $scheme The URL scheme (null to keep the current one) + */ + public function redirect(string $path, string $route, ?string $scheme = null): array; +} diff --git a/3rdparty/symfony/routing/Matcher/RequestMatcherInterface.php b/3rdparty/symfony/routing/Matcher/RequestMatcherInterface.php new file mode 100644 index 00000000..febba95b --- /dev/null +++ b/3rdparty/symfony/routing/Matcher/RequestMatcherInterface.php @@ -0,0 +1,37 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Component\Routing\Matcher; + +use Symfony\Component\HttpFoundation\Request; +use Symfony\Component\Routing\Exception\MethodNotAllowedException; +use Symfony\Component\Routing\Exception\NoConfigurationException; +use Symfony\Component\Routing\Exception\ResourceNotFoundException; + +/** + * RequestMatcherInterface is the interface that all request matcher classes must implement. + * + * @author Fabien Potencier + */ +interface RequestMatcherInterface +{ + /** + * Tries to match a request with a set of routes. + * + * If the matcher cannot find information, it must throw one of the exceptions documented + * below. + * + * @throws NoConfigurationException If no routing configuration could be found + * @throws ResourceNotFoundException If no matching resource could be found + * @throws MethodNotAllowedException If a matching resource was found but the request method is not allowed + */ + public function matchRequest(Request $request): array; +} diff --git a/3rdparty/symfony/routing/Matcher/TraceableUrlMatcher.php b/3rdparty/symfony/routing/Matcher/TraceableUrlMatcher.php new file mode 100644 index 00000000..3c7e24d0 --- /dev/null +++ b/3rdparty/symfony/routing/Matcher/TraceableUrlMatcher.php @@ -0,0 +1,172 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Component\Routing\Matcher; + +use Symfony\Component\HttpFoundation\Request; +use Symfony\Component\Routing\Exception\ExceptionInterface; +use Symfony\Component\Routing\Route; +use Symfony\Component\Routing\RouteCollection; + +/** + * TraceableUrlMatcher helps debug path info matching by tracing the match. + * + * @author Fabien Potencier + */ +class TraceableUrlMatcher extends UrlMatcher +{ + public const ROUTE_DOES_NOT_MATCH = 0; + public const ROUTE_ALMOST_MATCHES = 1; + public const ROUTE_MATCHES = 2; + + protected $traces; + + /** + * @return array + */ + public function getTraces(string $pathinfo) + { + $this->traces = []; + + try { + $this->match($pathinfo); + } catch (ExceptionInterface) { + } + + return $this->traces; + } + + /** + * @return array + */ + public function getTracesForRequest(Request $request) + { + $this->request = $request; + $traces = $this->getTraces($request->getPathInfo()); + $this->request = null; + + return $traces; + } + + protected function matchCollection(string $pathinfo, RouteCollection $routes): array + { + // HEAD and GET are equivalent as per RFC + if ('HEAD' === $method = $this->context->getMethod()) { + $method = 'GET'; + } + $supportsTrailingSlash = 'GET' === $method && $this instanceof RedirectableUrlMatcherInterface; + $trimmedPathinfo = rtrim($pathinfo, '/') ?: '/'; + + foreach ($routes as $name => $route) { + $compiledRoute = $route->compile(); + $staticPrefix = rtrim($compiledRoute->getStaticPrefix(), '/'); + $requiredMethods = $route->getMethods(); + + // check the static prefix of the URL first. Only use the more expensive preg_match when it matches + if ('' !== $staticPrefix && !str_starts_with($trimmedPathinfo, $staticPrefix)) { + $this->addTrace(sprintf('Path "%s" does not match', $route->getPath()), self::ROUTE_DOES_NOT_MATCH, $name, $route); + continue; + } + $regex = $compiledRoute->getRegex(); + + $pos = strrpos($regex, '$'); + $hasTrailingSlash = '/' === $regex[$pos - 1]; + $regex = substr_replace($regex, '/?$', $pos - $hasTrailingSlash, 1 + $hasTrailingSlash); + + if (!preg_match($regex, $pathinfo, $matches)) { + // does it match without any requirements? + $r = new Route($route->getPath(), $route->getDefaults(), [], $route->getOptions()); + $cr = $r->compile(); + if (!preg_match($cr->getRegex(), $pathinfo)) { + $this->addTrace(sprintf('Path "%s" does not match', $route->getPath()), self::ROUTE_DOES_NOT_MATCH, $name, $route); + + continue; + } + + foreach ($route->getRequirements() as $n => $regex) { + $r = new Route($route->getPath(), $route->getDefaults(), [$n => $regex], $route->getOptions()); + $cr = $r->compile(); + + if (\in_array($n, $cr->getVariables()) && !preg_match($cr->getRegex(), $pathinfo)) { + $this->addTrace(sprintf('Requirement for "%s" does not match (%s)', $n, $regex), self::ROUTE_ALMOST_MATCHES, $name, $route); + + continue 2; + } + } + + continue; + } + + $hasTrailingVar = $trimmedPathinfo !== $pathinfo && preg_match('#\{[\w\x80-\xFF]+\}/?$#', $route->getPath()); + + if ($hasTrailingVar && ($hasTrailingSlash || (null === $m = $matches[\count($compiledRoute->getPathVariables())] ?? null) || '/' !== ($m[-1] ?? '/')) && preg_match($regex, $trimmedPathinfo, $m)) { + if ($hasTrailingSlash) { + $matches = $m; + } else { + $hasTrailingVar = false; + } + } + + $hostMatches = []; + if ($compiledRoute->getHostRegex() && !preg_match($compiledRoute->getHostRegex(), $this->context->getHost(), $hostMatches)) { + $this->addTrace(sprintf('Host "%s" does not match the requirement ("%s")', $this->context->getHost(), $route->getHost()), self::ROUTE_ALMOST_MATCHES, $name, $route); + continue; + } + + $attributes = $this->getAttributes($route, $name, array_replace($matches, $hostMatches)); + + $status = $this->handleRouteRequirements($pathinfo, $name, $route, $attributes); + + if (self::REQUIREMENT_MISMATCH === $status[0]) { + $this->addTrace(sprintf('Condition "%s" does not evaluate to "true"', $route->getCondition()), self::ROUTE_ALMOST_MATCHES, $name, $route); + continue; + } + + if ('/' !== $pathinfo && !$hasTrailingVar && $hasTrailingSlash === ($trimmedPathinfo === $pathinfo)) { + if ($supportsTrailingSlash && (!$requiredMethods || \in_array('GET', $requiredMethods))) { + $this->addTrace('Route matches!', self::ROUTE_MATCHES, $name, $route); + + return $this->allow = $this->allowSchemes = []; + } + $this->addTrace(sprintf('Path "%s" does not match', $route->getPath()), self::ROUTE_DOES_NOT_MATCH, $name, $route); + continue; + } + + if ($route->getSchemes() && !$route->hasScheme($this->context->getScheme())) { + $this->allowSchemes = array_merge($this->allowSchemes, $route->getSchemes()); + $this->addTrace(sprintf('Scheme "%s" does not match any of the required schemes (%s)', $this->context->getScheme(), implode(', ', $route->getSchemes())), self::ROUTE_ALMOST_MATCHES, $name, $route); + continue; + } + + if ($requiredMethods && !\in_array($method, $requiredMethods)) { + $this->allow = array_merge($this->allow, $requiredMethods); + $this->addTrace(sprintf('Method "%s" does not match any of the required methods (%s)', $this->context->getMethod(), implode(', ', $requiredMethods)), self::ROUTE_ALMOST_MATCHES, $name, $route); + continue; + } + + $this->addTrace('Route matches!', self::ROUTE_MATCHES, $name, $route); + + return array_replace($attributes, $status[1] ?? []); + } + + return []; + } + + private function addTrace(string $log, int $level = self::ROUTE_DOES_NOT_MATCH, ?string $name = null, ?Route $route = null): void + { + $this->traces[] = [ + 'log' => $log, + 'name' => $name, + 'level' => $level, + 'path' => $route?->getPath(), + ]; + } +} diff --git a/3rdparty/symfony/routing/Matcher/UrlMatcher.php b/3rdparty/symfony/routing/Matcher/UrlMatcher.php new file mode 100644 index 00000000..778d154e --- /dev/null +++ b/3rdparty/symfony/routing/Matcher/UrlMatcher.php @@ -0,0 +1,287 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Component\Routing\Matcher; + +use Symfony\Component\ExpressionLanguage\ExpressionFunctionProviderInterface; +use Symfony\Component\ExpressionLanguage\ExpressionLanguage; +use Symfony\Component\HttpFoundation\Request; +use Symfony\Component\Routing\Exception\MethodNotAllowedException; +use Symfony\Component\Routing\Exception\NoConfigurationException; +use Symfony\Component\Routing\Exception\ResourceNotFoundException; +use Symfony\Component\Routing\RequestContext; +use Symfony\Component\Routing\Route; +use Symfony\Component\Routing\RouteCollection; + +/** + * UrlMatcher matches URL based on a set of routes. + * + * @author Fabien Potencier + */ +class UrlMatcher implements UrlMatcherInterface, RequestMatcherInterface +{ + public const REQUIREMENT_MATCH = 0; + public const REQUIREMENT_MISMATCH = 1; + public const ROUTE_MATCH = 2; + + /** @var RequestContext */ + protected $context; + + /** + * Collects HTTP methods that would be allowed for the request. + */ + protected $allow = []; + + /** + * Collects URI schemes that would be allowed for the request. + * + * @internal + */ + protected array $allowSchemes = []; + + protected $routes; + protected $request; + protected $expressionLanguage; + + /** + * @var ExpressionFunctionProviderInterface[] + */ + protected $expressionLanguageProviders = []; + + public function __construct(RouteCollection $routes, RequestContext $context) + { + $this->routes = $routes; + $this->context = $context; + } + + /** + * @return void + */ + public function setContext(RequestContext $context) + { + $this->context = $context; + } + + public function getContext(): RequestContext + { + return $this->context; + } + + public function match(string $pathinfo): array + { + $this->allow = $this->allowSchemes = []; + + if ($ret = $this->matchCollection(rawurldecode($pathinfo) ?: '/', $this->routes)) { + return $ret; + } + + if ('/' === $pathinfo && !$this->allow && !$this->allowSchemes) { + throw new NoConfigurationException(); + } + + throw 0 < \count($this->allow) ? new MethodNotAllowedException(array_unique($this->allow)) : new ResourceNotFoundException(sprintf('No routes found for "%s".', $pathinfo)); + } + + public function matchRequest(Request $request): array + { + $this->request = $request; + + $ret = $this->match($request->getPathInfo()); + + $this->request = null; + + return $ret; + } + + /** + * @return void + */ + public function addExpressionLanguageProvider(ExpressionFunctionProviderInterface $provider) + { + $this->expressionLanguageProviders[] = $provider; + } + + /** + * Tries to match a URL with a set of routes. + * + * @param string $pathinfo The path info to be parsed + * + * @throws NoConfigurationException If no routing configuration could be found + * @throws ResourceNotFoundException If the resource could not be found + * @throws MethodNotAllowedException If the resource was found but the request method is not allowed + */ + protected function matchCollection(string $pathinfo, RouteCollection $routes): array + { + // HEAD and GET are equivalent as per RFC + if ('HEAD' === $method = $this->context->getMethod()) { + $method = 'GET'; + } + $supportsTrailingSlash = 'GET' === $method && $this instanceof RedirectableUrlMatcherInterface; + $trimmedPathinfo = rtrim($pathinfo, '/') ?: '/'; + + foreach ($routes as $name => $route) { + $compiledRoute = $route->compile(); + $staticPrefix = rtrim($compiledRoute->getStaticPrefix(), '/'); + $requiredMethods = $route->getMethods(); + + // check the static prefix of the URL first. Only use the more expensive preg_match when it matches + if ('' !== $staticPrefix && !str_starts_with($trimmedPathinfo, $staticPrefix)) { + continue; + } + $regex = $compiledRoute->getRegex(); + + $pos = strrpos($regex, '$'); + $hasTrailingSlash = '/' === $regex[$pos - 1]; + $regex = substr_replace($regex, '/?$', $pos - $hasTrailingSlash, 1 + $hasTrailingSlash); + + if (!preg_match($regex, $pathinfo, $matches)) { + continue; + } + + $hasTrailingVar = $trimmedPathinfo !== $pathinfo && preg_match('#\{[\w\x80-\xFF]+\}/?$#', $route->getPath()); + + if ($hasTrailingVar && ($hasTrailingSlash || (null === $m = $matches[\count($compiledRoute->getPathVariables())] ?? null) || '/' !== ($m[-1] ?? '/')) && preg_match($regex, $trimmedPathinfo, $m)) { + if ($hasTrailingSlash) { + $matches = $m; + } else { + $hasTrailingVar = false; + } + } + + $hostMatches = []; + if ($compiledRoute->getHostRegex() && !preg_match($compiledRoute->getHostRegex(), $this->context->getHost(), $hostMatches)) { + continue; + } + + $attributes = $this->getAttributes($route, $name, array_replace($matches, $hostMatches)); + + $status = $this->handleRouteRequirements($pathinfo, $name, $route, $attributes); + + if (self::REQUIREMENT_MISMATCH === $status[0]) { + continue; + } + + if ('/' !== $pathinfo && !$hasTrailingVar && $hasTrailingSlash === ($trimmedPathinfo === $pathinfo)) { + if ($supportsTrailingSlash && (!$requiredMethods || \in_array('GET', $requiredMethods))) { + return $this->allow = $this->allowSchemes = []; + } + continue; + } + + if ($route->getSchemes() && !$route->hasScheme($this->context->getScheme())) { + $this->allowSchemes = array_merge($this->allowSchemes, $route->getSchemes()); + continue; + } + + if ($requiredMethods && !\in_array($method, $requiredMethods)) { + $this->allow = array_merge($this->allow, $requiredMethods); + continue; + } + + return array_replace($attributes, $status[1] ?? []); + } + + return []; + } + + /** + * Returns an array of values to use as request attributes. + * + * As this method requires the Route object, it is not available + * in matchers that do not have access to the matched Route instance + * (like the PHP and Apache matcher dumpers). + */ + protected function getAttributes(Route $route, string $name, array $attributes): array + { + $defaults = $route->getDefaults(); + if (isset($defaults['_canonical_route'])) { + $name = $defaults['_canonical_route']; + unset($defaults['_canonical_route']); + } + $attributes['_route'] = $name; + + return $this->mergeDefaults($attributes, $defaults); + } + + /** + * Handles specific route requirements. + * + * @return array The first element represents the status, the second contains additional information + */ + protected function handleRouteRequirements(string $pathinfo, string $name, Route $route/* , array $routeParameters */): array + { + if (\func_num_args() < 4) { + trigger_deprecation('symfony/routing', '6.1', 'The "%s()" method will have a new "array $routeParameters" argument in version 7.0, not defining it is deprecated.', __METHOD__); + $routeParameters = []; + } else { + $routeParameters = func_get_arg(3); + + if (!\is_array($routeParameters)) { + throw new \TypeError(sprintf('"%s": Argument $routeParameters is expected to be an array, got "%s".', __METHOD__, get_debug_type($routeParameters))); + } + } + + // expression condition + if ($route->getCondition() && !$this->getExpressionLanguage()->evaluate($route->getCondition(), [ + 'context' => $this->context, + 'request' => $this->request ?: $this->createRequest($pathinfo), + 'params' => $routeParameters, + ])) { + return [self::REQUIREMENT_MISMATCH, null]; + } + + return [self::REQUIREMENT_MATCH, null]; + } + + /** + * Get merged default parameters. + */ + protected function mergeDefaults(array $params, array $defaults): array + { + foreach ($params as $key => $value) { + if (!\is_int($key) && null !== $value) { + $defaults[$key] = $value; + } + } + + return $defaults; + } + + /** + * @return ExpressionLanguage + */ + protected function getExpressionLanguage() + { + if (!isset($this->expressionLanguage)) { + if (!class_exists(ExpressionLanguage::class)) { + throw new \LogicException('Unable to use expressions as the Symfony ExpressionLanguage component is not installed. Try running "composer require symfony/expression-language".'); + } + $this->expressionLanguage = new ExpressionLanguage(null, $this->expressionLanguageProviders); + } + + return $this->expressionLanguage; + } + + /** + * @internal + */ + protected function createRequest(string $pathinfo): ?Request + { + if (!class_exists(Request::class)) { + return null; + } + + return Request::create($this->context->getScheme().'://'.$this->context->getHost().$this->context->getBaseUrl().$pathinfo, $this->context->getMethod(), $this->context->getParameters(), [], [], [ + 'SCRIPT_FILENAME' => $this->context->getBaseUrl(), + 'SCRIPT_NAME' => $this->context->getBaseUrl(), + ]); + } +} diff --git a/3rdparty/symfony/routing/Matcher/UrlMatcherInterface.php b/3rdparty/symfony/routing/Matcher/UrlMatcherInterface.php new file mode 100644 index 00000000..68a3737f --- /dev/null +++ b/3rdparty/symfony/routing/Matcher/UrlMatcherInterface.php @@ -0,0 +1,39 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Component\Routing\Matcher; + +use Symfony\Component\Routing\Exception\MethodNotAllowedException; +use Symfony\Component\Routing\Exception\NoConfigurationException; +use Symfony\Component\Routing\Exception\ResourceNotFoundException; +use Symfony\Component\Routing\RequestContextAwareInterface; + +/** + * UrlMatcherInterface is the interface that all URL matcher classes must implement. + * + * @author Fabien Potencier + */ +interface UrlMatcherInterface extends RequestContextAwareInterface +{ + /** + * Tries to match a URL path with a set of routes. + * + * If the matcher cannot find information, it must throw one of the exceptions documented + * below. + * + * @param string $pathinfo The path info to be parsed (raw format, i.e. not urldecoded) + * + * @throws NoConfigurationException If no routing configuration could be found + * @throws ResourceNotFoundException If the resource could not be found + * @throws MethodNotAllowedException If the resource was found but the request method is not allowed + */ + public function match(string $pathinfo): array; +} diff --git a/3rdparty/symfony/routing/RequestContext.php b/3rdparty/symfony/routing/RequestContext.php new file mode 100644 index 00000000..e3f4831b --- /dev/null +++ b/3rdparty/symfony/routing/RequestContext.php @@ -0,0 +1,303 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Component\Routing; + +use Symfony\Component\HttpFoundation\Request; + +/** + * Holds information about the current request. + * + * This class implements a fluent interface. + * + * @author Fabien Potencier + * @author Tobias Schultze + */ +class RequestContext +{ + private string $baseUrl; + private string $pathInfo; + private string $method; + private string $host; + private string $scheme; + private int $httpPort; + private int $httpsPort; + private string $queryString; + private array $parameters = []; + + public function __construct(string $baseUrl = '', string $method = 'GET', string $host = 'localhost', string $scheme = 'http', int $httpPort = 80, int $httpsPort = 443, string $path = '/', string $queryString = '') + { + $this->setBaseUrl($baseUrl); + $this->setMethod($method); + $this->setHost($host); + $this->setScheme($scheme); + $this->setHttpPort($httpPort); + $this->setHttpsPort($httpsPort); + $this->setPathInfo($path); + $this->setQueryString($queryString); + } + + public static function fromUri(string $uri, string $host = 'localhost', string $scheme = 'http', int $httpPort = 80, int $httpsPort = 443): self + { + $uri = parse_url($uri); + $scheme = $uri['scheme'] ?? $scheme; + $host = $uri['host'] ?? $host; + + if (isset($uri['port'])) { + if ('http' === $scheme) { + $httpPort = $uri['port']; + } elseif ('https' === $scheme) { + $httpsPort = $uri['port']; + } + } + + return new self($uri['path'] ?? '', 'GET', $host, $scheme, $httpPort, $httpsPort); + } + + /** + * Updates the RequestContext information based on a HttpFoundation Request. + * + * @return $this + */ + public function fromRequest(Request $request): static + { + $this->setBaseUrl($request->getBaseUrl()); + $this->setPathInfo($request->getPathInfo()); + $this->setMethod($request->getMethod()); + $this->setHost($request->getHost()); + $this->setScheme($request->getScheme()); + $this->setHttpPort($request->isSecure() || null === $request->getPort() ? $this->httpPort : $request->getPort()); + $this->setHttpsPort($request->isSecure() && null !== $request->getPort() ? $request->getPort() : $this->httpsPort); + $this->setQueryString($request->server->get('QUERY_STRING', '')); + + return $this; + } + + /** + * Gets the base URL. + */ + public function getBaseUrl(): string + { + return $this->baseUrl; + } + + /** + * Sets the base URL. + * + * @return $this + */ + public function setBaseUrl(string $baseUrl): static + { + $this->baseUrl = rtrim($baseUrl, '/'); + + return $this; + } + + /** + * Gets the path info. + */ + public function getPathInfo(): string + { + return $this->pathInfo; + } + + /** + * Sets the path info. + * + * @return $this + */ + public function setPathInfo(string $pathInfo): static + { + $this->pathInfo = $pathInfo; + + return $this; + } + + /** + * Gets the HTTP method. + * + * The method is always an uppercased string. + */ + public function getMethod(): string + { + return $this->method; + } + + /** + * Sets the HTTP method. + * + * @return $this + */ + public function setMethod(string $method): static + { + $this->method = strtoupper($method); + + return $this; + } + + /** + * Gets the HTTP host. + * + * The host is always lowercased because it must be treated case-insensitive. + */ + public function getHost(): string + { + return $this->host; + } + + /** + * Sets the HTTP host. + * + * @return $this + */ + public function setHost(string $host): static + { + $this->host = strtolower($host); + + return $this; + } + + /** + * Gets the HTTP scheme. + */ + public function getScheme(): string + { + return $this->scheme; + } + + /** + * Sets the HTTP scheme. + * + * @return $this + */ + public function setScheme(string $scheme): static + { + $this->scheme = strtolower($scheme); + + return $this; + } + + /** + * Gets the HTTP port. + */ + public function getHttpPort(): int + { + return $this->httpPort; + } + + /** + * Sets the HTTP port. + * + * @return $this + */ + public function setHttpPort(int $httpPort): static + { + $this->httpPort = $httpPort; + + return $this; + } + + /** + * Gets the HTTPS port. + */ + public function getHttpsPort(): int + { + return $this->httpsPort; + } + + /** + * Sets the HTTPS port. + * + * @return $this + */ + public function setHttpsPort(int $httpsPort): static + { + $this->httpsPort = $httpsPort; + + return $this; + } + + /** + * Gets the query string without the "?". + */ + public function getQueryString(): string + { + return $this->queryString; + } + + /** + * Sets the query string. + * + * @return $this + */ + public function setQueryString(?string $queryString): static + { + // string cast to be fault-tolerant, accepting null + $this->queryString = (string) $queryString; + + return $this; + } + + /** + * Returns the parameters. + */ + public function getParameters(): array + { + return $this->parameters; + } + + /** + * Sets the parameters. + * + * @param array $parameters The parameters + * + * @return $this + */ + public function setParameters(array $parameters): static + { + $this->parameters = $parameters; + + return $this; + } + + /** + * Gets a parameter value. + */ + public function getParameter(string $name): mixed + { + return $this->parameters[$name] ?? null; + } + + /** + * Checks if a parameter value is set for the given parameter. + */ + public function hasParameter(string $name): bool + { + return \array_key_exists($name, $this->parameters); + } + + /** + * Sets a parameter value. + * + * @return $this + */ + public function setParameter(string $name, mixed $parameter): static + { + $this->parameters[$name] = $parameter; + + return $this; + } + + public function isSecure(): bool + { + return 'https' === $this->scheme; + } +} diff --git a/3rdparty/symfony/routing/RequestContextAwareInterface.php b/3rdparty/symfony/routing/RequestContextAwareInterface.php new file mode 100644 index 00000000..04acbdc8 --- /dev/null +++ b/3rdparty/symfony/routing/RequestContextAwareInterface.php @@ -0,0 +1,27 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Component\Routing; + +interface RequestContextAwareInterface +{ + /** + * Sets the request context. + * + * @return void + */ + public function setContext(RequestContext $context); + + /** + * Gets the request context. + */ + public function getContext(): RequestContext; +} diff --git a/3rdparty/symfony/routing/Requirement/EnumRequirement.php b/3rdparty/symfony/routing/Requirement/EnumRequirement.php new file mode 100644 index 00000000..3ab2ed33 --- /dev/null +++ b/3rdparty/symfony/routing/Requirement/EnumRequirement.php @@ -0,0 +1,56 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Component\Routing\Requirement; + +use Symfony\Component\Routing\Exception\InvalidArgumentException; + +final class EnumRequirement implements \Stringable +{ + private string $requirement; + + /** + * @template T of \BackedEnum + * + * @param class-string|list $cases + */ + public function __construct(string|array $cases = []) + { + if (\is_string($cases)) { + if (!is_subclass_of($cases, \BackedEnum::class, true)) { + throw new InvalidArgumentException(sprintf('"%s" is not a "BackedEnum" class.', $cases)); + } + + $cases = $cases::cases(); + } else { + $class = null; + + foreach ($cases as $case) { + if (!$case instanceof \BackedEnum) { + throw new InvalidArgumentException(sprintf('Case must be a "BackedEnum" instance, "%s" given.', get_debug_type($case))); + } + + $class ??= $case::class; + + if (!$case instanceof $class) { + throw new InvalidArgumentException(sprintf('"%s::%s" is not a case of "%s".', get_debug_type($case), $case->name, $class)); + } + } + } + + $this->requirement = implode('|', array_map(static fn ($e) => preg_quote($e->value), $cases)); + } + + public function __toString(): string + { + return $this->requirement; + } +} diff --git a/3rdparty/symfony/routing/Requirement/Requirement.php b/3rdparty/symfony/routing/Requirement/Requirement.php new file mode 100644 index 00000000..dfbb801f --- /dev/null +++ b/3rdparty/symfony/routing/Requirement/Requirement.php @@ -0,0 +1,36 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Component\Routing\Requirement; + +/* + * A collection of universal regular-expression constants to use as route parameter requirements. + */ +enum Requirement +{ + public const ASCII_SLUG = '[A-Za-z0-9]+(?:-[A-Za-z0-9]+)*'; // symfony/string AsciiSlugger default implementation + public const CATCH_ALL = '.+'; + public const DATE_YMD = '[0-9]{4}-(?:0[1-9]|1[012])-(?:0[1-9]|[12][0-9]|(? + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Component\Routing; + +/** + * A Route describes a route and its parameters. + * + * @author Fabien Potencier + * @author Tobias Schultze + */ +class Route implements \Serializable +{ + private string $path = '/'; + private string $host = ''; + private array $schemes = []; + private array $methods = []; + private array $defaults = []; + private array $requirements = []; + private array $options = []; + private string $condition = ''; + private ?CompiledRoute $compiled = null; + + /** + * Constructor. + * + * Available options: + * + * * compiler_class: A class name able to compile this route instance (RouteCompiler by default) + * * utf8: Whether UTF-8 matching is enforced ot not + * + * @param string $path The path pattern to match + * @param array $defaults An array of default parameter values + * @param array $requirements An array of requirements for parameters (regexes) + * @param array $options An array of options + * @param string|null $host The host pattern to match + * @param string|string[] $schemes A required URI scheme or an array of restricted schemes + * @param string|string[] $methods A required HTTP method or an array of restricted methods + * @param string|null $condition A condition that should evaluate to true for the route to match + */ + public function __construct(string $path, array $defaults = [], array $requirements = [], array $options = [], ?string $host = '', string|array $schemes = [], string|array $methods = [], ?string $condition = '') + { + $this->setPath($path); + $this->addDefaults($defaults); + $this->addRequirements($requirements); + $this->setOptions($options); + $this->setHost($host); + $this->setSchemes($schemes); + $this->setMethods($methods); + $this->setCondition($condition); + } + + public function __serialize(): array + { + return [ + 'path' => $this->path, + 'host' => $this->host, + 'defaults' => $this->defaults, + 'requirements' => $this->requirements, + 'options' => $this->options, + 'schemes' => $this->schemes, + 'methods' => $this->methods, + 'condition' => $this->condition, + 'compiled' => $this->compiled, + ]; + } + + /** + * @internal + */ + final public function serialize(): string + { + throw new \BadMethodCallException('Cannot serialize '.__CLASS__); + } + + public function __unserialize(array $data): void + { + $this->path = $data['path']; + $this->host = $data['host']; + $this->defaults = $data['defaults']; + $this->requirements = $data['requirements']; + $this->options = $data['options']; + $this->schemes = $data['schemes']; + $this->methods = $data['methods']; + + if (isset($data['condition'])) { + $this->condition = $data['condition']; + } + if (isset($data['compiled'])) { + $this->compiled = $data['compiled']; + } + } + + /** + * @internal + */ + final public function unserialize(string $serialized): void + { + $this->__unserialize(unserialize($serialized)); + } + + public function getPath(): string + { + return $this->path; + } + + /** + * @return $this + */ + public function setPath(string $pattern): static + { + $pattern = $this->extractInlineDefaultsAndRequirements($pattern); + + // A pattern must start with a slash and must not have multiple slashes at the beginning because the + // generated path for this route would be confused with a network path, e.g. '//domain.com/path'. + $this->path = '/'.ltrim(trim($pattern), '/'); + $this->compiled = null; + + return $this; + } + + public function getHost(): string + { + return $this->host; + } + + /** + * @return $this + */ + public function setHost(?string $pattern): static + { + $this->host = $this->extractInlineDefaultsAndRequirements((string) $pattern); + $this->compiled = null; + + return $this; + } + + /** + * Returns the lowercased schemes this route is restricted to. + * So an empty array means that any scheme is allowed. + * + * @return string[] + */ + public function getSchemes(): array + { + return $this->schemes; + } + + /** + * Sets the schemes (e.g. 'https') this route is restricted to. + * So an empty array means that any scheme is allowed. + * + * @param string|string[] $schemes The scheme or an array of schemes + * + * @return $this + */ + public function setSchemes(string|array $schemes): static + { + $this->schemes = array_map('strtolower', (array) $schemes); + $this->compiled = null; + + return $this; + } + + /** + * Checks if a scheme requirement has been set. + */ + public function hasScheme(string $scheme): bool + { + return \in_array(strtolower($scheme), $this->schemes, true); + } + + /** + * Returns the uppercased HTTP methods this route is restricted to. + * So an empty array means that any method is allowed. + * + * @return string[] + */ + public function getMethods(): array + { + return $this->methods; + } + + /** + * Sets the HTTP methods (e.g. 'POST') this route is restricted to. + * So an empty array means that any method is allowed. + * + * @param string|string[] $methods The method or an array of methods + * + * @return $this + */ + public function setMethods(string|array $methods): static + { + $this->methods = array_map('strtoupper', (array) $methods); + $this->compiled = null; + + return $this; + } + + public function getOptions(): array + { + return $this->options; + } + + /** + * @return $this + */ + public function setOptions(array $options): static + { + $this->options = [ + 'compiler_class' => RouteCompiler::class, + ]; + + return $this->addOptions($options); + } + + /** + * @return $this + */ + public function addOptions(array $options): static + { + foreach ($options as $name => $option) { + $this->options[$name] = $option; + } + $this->compiled = null; + + return $this; + } + + /** + * Sets an option value. + * + * @return $this + */ + public function setOption(string $name, mixed $value): static + { + $this->options[$name] = $value; + $this->compiled = null; + + return $this; + } + + /** + * Returns the option value or null when not found. + */ + public function getOption(string $name): mixed + { + return $this->options[$name] ?? null; + } + + public function hasOption(string $name): bool + { + return \array_key_exists($name, $this->options); + } + + public function getDefaults(): array + { + return $this->defaults; + } + + /** + * @return $this + */ + public function setDefaults(array $defaults): static + { + $this->defaults = []; + + return $this->addDefaults($defaults); + } + + /** + * @return $this + */ + public function addDefaults(array $defaults): static + { + if (isset($defaults['_locale']) && $this->isLocalized()) { + unset($defaults['_locale']); + } + + foreach ($defaults as $name => $default) { + $this->defaults[$name] = $default; + } + $this->compiled = null; + + return $this; + } + + public function getDefault(string $name): mixed + { + return $this->defaults[$name] ?? null; + } + + public function hasDefault(string $name): bool + { + return \array_key_exists($name, $this->defaults); + } + + /** + * @return $this + */ + public function setDefault(string $name, mixed $default): static + { + if ('_locale' === $name && $this->isLocalized()) { + return $this; + } + + $this->defaults[$name] = $default; + $this->compiled = null; + + return $this; + } + + public function getRequirements(): array + { + return $this->requirements; + } + + /** + * @return $this + */ + public function setRequirements(array $requirements): static + { + $this->requirements = []; + + return $this->addRequirements($requirements); + } + + /** + * @return $this + */ + public function addRequirements(array $requirements): static + { + if (isset($requirements['_locale']) && $this->isLocalized()) { + unset($requirements['_locale']); + } + + foreach ($requirements as $key => $regex) { + $this->requirements[$key] = $this->sanitizeRequirement($key, $regex); + } + $this->compiled = null; + + return $this; + } + + public function getRequirement(string $key): ?string + { + return $this->requirements[$key] ?? null; + } + + public function hasRequirement(string $key): bool + { + return \array_key_exists($key, $this->requirements); + } + + /** + * @return $this + */ + public function setRequirement(string $key, string $regex): static + { + if ('_locale' === $key && $this->isLocalized()) { + return $this; + } + + $this->requirements[$key] = $this->sanitizeRequirement($key, $regex); + $this->compiled = null; + + return $this; + } + + public function getCondition(): string + { + return $this->condition; + } + + /** + * @return $this + */ + public function setCondition(?string $condition): static + { + $this->condition = (string) $condition; + $this->compiled = null; + + return $this; + } + + /** + * Compiles the route. + * + * @throws \LogicException If the Route cannot be compiled because the + * path or host pattern is invalid + * + * @see RouteCompiler which is responsible for the compilation process + */ + public function compile(): CompiledRoute + { + if (null !== $this->compiled) { + return $this->compiled; + } + + $class = $this->getOption('compiler_class'); + + return $this->compiled = $class::compile($this); + } + + private function extractInlineDefaultsAndRequirements(string $pattern): string + { + if (false === strpbrk($pattern, '?<')) { + return $pattern; + } + + return preg_replace_callback('#\{(!?)([\w\x80-\xFF]++)(<.*?>)?(\?[^\}]*+)?\}#', function ($m) { + if (isset($m[4][0])) { + $this->setDefault($m[2], '?' !== $m[4] ? substr($m[4], 1) : null); + } + if (isset($m[3][0])) { + $this->setRequirement($m[2], substr($m[3], 1, -1)); + } + + return '{'.$m[1].$m[2].'}'; + }, $pattern); + } + + private function sanitizeRequirement(string $key, string $regex): string + { + if ('' !== $regex) { + if ('^' === $regex[0]) { + $regex = substr($regex, 1); + } elseif (str_starts_with($regex, '\\A')) { + $regex = substr($regex, 2); + } + } + + if (str_ends_with($regex, '$')) { + $regex = substr($regex, 0, -1); + } elseif (\strlen($regex) - 2 === strpos($regex, '\\z')) { + $regex = substr($regex, 0, -2); + } + + if ('' === $regex) { + throw new \InvalidArgumentException(sprintf('Routing requirement for "%s" cannot be empty.', $key)); + } + + return $regex; + } + + private function isLocalized(): bool + { + return isset($this->defaults['_locale']) && isset($this->defaults['_canonical_route']) && ($this->requirements['_locale'] ?? null) === preg_quote($this->defaults['_locale']); + } +} diff --git a/3rdparty/symfony/routing/RouteCollection.php b/3rdparty/symfony/routing/RouteCollection.php new file mode 100644 index 00000000..2f49ad21 --- /dev/null +++ b/3rdparty/symfony/routing/RouteCollection.php @@ -0,0 +1,415 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Component\Routing; + +use Symfony\Component\Config\Resource\ResourceInterface; +use Symfony\Component\Routing\Exception\InvalidArgumentException; +use Symfony\Component\Routing\Exception\RouteCircularReferenceException; + +/** + * A RouteCollection represents a set of Route instances. + * + * When adding a route at the end of the collection, an existing route + * with the same name is removed first. So there can only be one route + * with a given name. + * + * @author Fabien Potencier + * @author Tobias Schultze + * + * @implements \IteratorAggregate + */ +class RouteCollection implements \IteratorAggregate, \Countable +{ + /** + * @var array + */ + private array $routes = []; + + /** + * @var array + */ + private array $aliases = []; + + /** + * @var array + */ + private array $resources = []; + + /** + * @var array + */ + private array $priorities = []; + + public function __clone() + { + foreach ($this->routes as $name => $route) { + $this->routes[$name] = clone $route; + } + + foreach ($this->aliases as $name => $alias) { + $this->aliases[$name] = clone $alias; + } + } + + /** + * Gets the current RouteCollection as an Iterator that includes all routes. + * + * It implements \IteratorAggregate. + * + * @see all() + * + * @return \ArrayIterator + */ + public function getIterator(): \ArrayIterator + { + return new \ArrayIterator($this->all()); + } + + /** + * Gets the number of Routes in this collection. + */ + public function count(): int + { + return \count($this->routes); + } + + /** + * @return void + */ + public function add(string $name, Route $route, int $priority = 0) + { + unset($this->routes[$name], $this->priorities[$name], $this->aliases[$name]); + + $this->routes[$name] = $route; + + if ($priority) { + $this->priorities[$name] = $priority; + } + } + + /** + * Returns all routes in this collection. + * + * @return array + */ + public function all(): array + { + if ($this->priorities) { + $priorities = $this->priorities; + $keysOrder = array_flip(array_keys($this->routes)); + uksort($this->routes, static fn ($n1, $n2) => (($priorities[$n2] ?? 0) <=> ($priorities[$n1] ?? 0)) ?: ($keysOrder[$n1] <=> $keysOrder[$n2])); + } + + return $this->routes; + } + + /** + * Gets a route by name. + */ + public function get(string $name): ?Route + { + $visited = []; + while (null !== $alias = $this->aliases[$name] ?? null) { + if (false !== $searchKey = array_search($name, $visited)) { + $visited[] = $name; + + throw new RouteCircularReferenceException($name, \array_slice($visited, $searchKey)); + } + + if ($alias->isDeprecated()) { + $deprecation = $alias->getDeprecation($name); + + trigger_deprecation($deprecation['package'], $deprecation['version'], $deprecation['message']); + } + + $visited[] = $name; + $name = $alias->getId(); + } + + return $this->routes[$name] ?? null; + } + + /** + * Removes a route or an array of routes by name from the collection. + * + * @param string|string[] $name The route name or an array of route names + * + * @return void + */ + public function remove(string|array $name) + { + $routes = []; + foreach ((array) $name as $n) { + if (isset($this->routes[$n])) { + $routes[] = $n; + } + + unset($this->routes[$n], $this->priorities[$n], $this->aliases[$n]); + } + + if (!$routes) { + return; + } + + foreach ($this->aliases as $k => $alias) { + if (\in_array($alias->getId(), $routes, true)) { + unset($this->aliases[$k]); + } + } + } + + /** + * Adds a route collection at the end of the current set by appending all + * routes of the added collection. + * + * @return void + */ + public function addCollection(self $collection) + { + // we need to remove all routes with the same names first because just replacing them + // would not place the new route at the end of the merged array + foreach ($collection->all() as $name => $route) { + unset($this->routes[$name], $this->priorities[$name], $this->aliases[$name]); + $this->routes[$name] = $route; + + if (isset($collection->priorities[$name])) { + $this->priorities[$name] = $collection->priorities[$name]; + } + } + + foreach ($collection->getAliases() as $name => $alias) { + unset($this->routes[$name], $this->priorities[$name], $this->aliases[$name]); + + $this->aliases[$name] = $alias; + } + + foreach ($collection->getResources() as $resource) { + $this->addResource($resource); + } + } + + /** + * Adds a prefix to the path of all child routes. + * + * @return void + */ + public function addPrefix(string $prefix, array $defaults = [], array $requirements = []) + { + $prefix = trim(trim($prefix), '/'); + + if ('' === $prefix) { + return; + } + + foreach ($this->routes as $route) { + $route->setPath('/'.$prefix.$route->getPath()); + $route->addDefaults($defaults); + $route->addRequirements($requirements); + } + } + + /** + * Adds a prefix to the name of all the routes within in the collection. + * + * @return void + */ + public function addNamePrefix(string $prefix) + { + $prefixedRoutes = []; + $prefixedPriorities = []; + $prefixedAliases = []; + + foreach ($this->routes as $name => $route) { + $prefixedRoutes[$prefix.$name] = $route; + if (null !== $canonicalName = $route->getDefault('_canonical_route')) { + $route->setDefault('_canonical_route', $prefix.$canonicalName); + } + if (isset($this->priorities[$name])) { + $prefixedPriorities[$prefix.$name] = $this->priorities[$name]; + } + } + + foreach ($this->aliases as $name => $alias) { + $prefixedAliases[$prefix.$name] = $alias->withId($prefix.$alias->getId()); + } + + $this->routes = $prefixedRoutes; + $this->priorities = $prefixedPriorities; + $this->aliases = $prefixedAliases; + } + + /** + * Sets the host pattern on all routes. + * + * @return void + */ + public function setHost(?string $pattern, array $defaults = [], array $requirements = []) + { + foreach ($this->routes as $route) { + $route->setHost($pattern); + $route->addDefaults($defaults); + $route->addRequirements($requirements); + } + } + + /** + * Sets a condition on all routes. + * + * Existing conditions will be overridden. + * + * @return void + */ + public function setCondition(?string $condition) + { + foreach ($this->routes as $route) { + $route->setCondition($condition); + } + } + + /** + * Adds defaults to all routes. + * + * An existing default value under the same name in a route will be overridden. + * + * @return void + */ + public function addDefaults(array $defaults) + { + if ($defaults) { + foreach ($this->routes as $route) { + $route->addDefaults($defaults); + } + } + } + + /** + * Adds requirements to all routes. + * + * An existing requirement under the same name in a route will be overridden. + * + * @return void + */ + public function addRequirements(array $requirements) + { + if ($requirements) { + foreach ($this->routes as $route) { + $route->addRequirements($requirements); + } + } + } + + /** + * Adds options to all routes. + * + * An existing option value under the same name in a route will be overridden. + * + * @return void + */ + public function addOptions(array $options) + { + if ($options) { + foreach ($this->routes as $route) { + $route->addOptions($options); + } + } + } + + /** + * Sets the schemes (e.g. 'https') all child routes are restricted to. + * + * @param string|string[] $schemes The scheme or an array of schemes + * + * @return void + */ + public function setSchemes(string|array $schemes) + { + foreach ($this->routes as $route) { + $route->setSchemes($schemes); + } + } + + /** + * Sets the HTTP methods (e.g. 'POST') all child routes are restricted to. + * + * @param string|string[] $methods The method or an array of methods + * + * @return void + */ + public function setMethods(string|array $methods) + { + foreach ($this->routes as $route) { + $route->setMethods($methods); + } + } + + /** + * Returns an array of resources loaded to build this collection. + * + * @return ResourceInterface[] + */ + public function getResources(): array + { + return array_values($this->resources); + } + + /** + * Adds a resource for this collection. If the resource already exists + * it is not added. + * + * @return void + */ + public function addResource(ResourceInterface $resource) + { + $key = (string) $resource; + + if (!isset($this->resources[$key])) { + $this->resources[$key] = $resource; + } + } + + /** + * Sets an alias for an existing route. + * + * @param string $name The alias to create + * @param string $alias The route to alias + * + * @throws InvalidArgumentException if the alias is for itself + */ + public function addAlias(string $name, string $alias): Alias + { + if ($name === $alias) { + throw new InvalidArgumentException(sprintf('Route alias "%s" can not reference itself.', $name)); + } + + unset($this->routes[$name], $this->priorities[$name]); + + return $this->aliases[$name] = new Alias($alias); + } + + /** + * @return array + */ + public function getAliases(): array + { + return $this->aliases; + } + + public function getAlias(string $name): ?Alias + { + return $this->aliases[$name] ?? null; + } + + public function getPriority(string $name): ?int + { + return $this->priorities[$name] ?? null; + } +} diff --git a/3rdparty/symfony/routing/RouteCompiler.php b/3rdparty/symfony/routing/RouteCompiler.php new file mode 100644 index 00000000..330639f4 --- /dev/null +++ b/3rdparty/symfony/routing/RouteCompiler.php @@ -0,0 +1,339 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Component\Routing; + +/** + * RouteCompiler compiles Route instances to CompiledRoute instances. + * + * @author Fabien Potencier + * @author Tobias Schultze + */ +class RouteCompiler implements RouteCompilerInterface +{ + /** + * This string defines the characters that are automatically considered separators in front of + * optional placeholders (with default and no static text following). Such a single separator + * can be left out together with the optional placeholder from matching and generating URLs. + */ + public const SEPARATORS = '/,;.:-_~+*=@|'; + + /** + * The maximum supported length of a PCRE subpattern name + * http://pcre.org/current/doc/html/pcre2pattern.html#SEC16. + * + * @internal + */ + public const VARIABLE_MAXIMUM_LENGTH = 32; + + /** + * @throws \InvalidArgumentException if a path variable is named _fragment + * @throws \LogicException if a variable is referenced more than once + * @throws \DomainException if a variable name starts with a digit or if it is too long to be successfully used as + * a PCRE subpattern + */ + public static function compile(Route $route): CompiledRoute + { + $hostVariables = []; + $variables = []; + $hostRegex = null; + $hostTokens = []; + + if ('' !== $host = $route->getHost()) { + $result = self::compilePattern($route, $host, true); + + $hostVariables = $result['variables']; + $variables = $hostVariables; + + $hostTokens = $result['tokens']; + $hostRegex = $result['regex']; + } + + $locale = $route->getDefault('_locale'); + if (null !== $locale && null !== $route->getDefault('_canonical_route') && preg_quote($locale) === $route->getRequirement('_locale')) { + $requirements = $route->getRequirements(); + unset($requirements['_locale']); + $route->setRequirements($requirements); + $route->setPath(str_replace('{_locale}', $locale, $route->getPath())); + } + + $path = $route->getPath(); + + $result = self::compilePattern($route, $path, false); + + $staticPrefix = $result['staticPrefix']; + + $pathVariables = $result['variables']; + + foreach ($pathVariables as $pathParam) { + if ('_fragment' === $pathParam) { + throw new \InvalidArgumentException(sprintf('Route pattern "%s" cannot contain "_fragment" as a path parameter.', $route->getPath())); + } + } + + $variables = array_merge($variables, $pathVariables); + + $tokens = $result['tokens']; + $regex = $result['regex']; + + return new CompiledRoute( + $staticPrefix, + $regex, + $tokens, + $pathVariables, + $hostRegex, + $hostTokens, + $hostVariables, + array_unique($variables) + ); + } + + private static function compilePattern(Route $route, string $pattern, bool $isHost): array + { + $tokens = []; + $variables = []; + $matches = []; + $pos = 0; + $defaultSeparator = $isHost ? '.' : '/'; + $useUtf8 = preg_match('//u', $pattern); + $needsUtf8 = $route->getOption('utf8'); + + if (!$needsUtf8 && $useUtf8 && preg_match('/[\x80-\xFF]/', $pattern)) { + throw new \LogicException(sprintf('Cannot use UTF-8 route patterns without setting the "utf8" option for route "%s".', $route->getPath())); + } + if (!$useUtf8 && $needsUtf8) { + throw new \LogicException(sprintf('Cannot mix UTF-8 requirements with non-UTF-8 pattern "%s".', $pattern)); + } + + // Match all variables enclosed in "{}" and iterate over them. But we only want to match the innermost variable + // in case of nested "{}", e.g. {foo{bar}}. This in ensured because \w does not match "{" or "}" itself. + preg_match_all('#\{(!)?([\w\x80-\xFF]+)\}#', $pattern, $matches, \PREG_OFFSET_CAPTURE | \PREG_SET_ORDER); + foreach ($matches as $match) { + $important = $match[1][1] >= 0; + $varName = $match[2][0]; + // get all static text preceding the current variable + $precedingText = substr($pattern, $pos, $match[0][1] - $pos); + $pos = $match[0][1] + \strlen($match[0][0]); + + if (!\strlen($precedingText)) { + $precedingChar = ''; + } elseif ($useUtf8) { + preg_match('/.$/u', $precedingText, $precedingChar); + $precedingChar = $precedingChar[0]; + } else { + $precedingChar = substr($precedingText, -1); + } + $isSeparator = '' !== $precedingChar && str_contains(static::SEPARATORS, $precedingChar); + + // A PCRE subpattern name must start with a non-digit. Also a PHP variable cannot start with a digit so the + // variable would not be usable as a Controller action argument. + if (preg_match('/^\d/', $varName)) { + throw new \DomainException(sprintf('Variable name "%s" cannot start with a digit in route pattern "%s". Please use a different name.', $varName, $pattern)); + } + if (\in_array($varName, $variables)) { + throw new \LogicException(sprintf('Route pattern "%s" cannot reference variable name "%s" more than once.', $pattern, $varName)); + } + + if (\strlen($varName) > self::VARIABLE_MAXIMUM_LENGTH) { + throw new \DomainException(sprintf('Variable name "%s" cannot be longer than %d characters in route pattern "%s". Please use a shorter name.', $varName, self::VARIABLE_MAXIMUM_LENGTH, $pattern)); + } + + if ($isSeparator && $precedingText !== $precedingChar) { + $tokens[] = ['text', substr($precedingText, 0, -\strlen($precedingChar))]; + } elseif (!$isSeparator && '' !== $precedingText) { + $tokens[] = ['text', $precedingText]; + } + + $regexp = $route->getRequirement($varName); + if (null === $regexp) { + $followingPattern = (string) substr($pattern, $pos); + // Find the next static character after the variable that functions as a separator. By default, this separator and '/' + // are disallowed for the variable. This default requirement makes sure that optional variables can be matched at all + // and that the generating-matching-combination of URLs unambiguous, i.e. the params used for generating the URL are + // the same that will be matched. Example: new Route('/{page}.{_format}', ['_format' => 'html']) + // If {page} would also match the separating dot, {_format} would never match as {page} will eagerly consume everything. + // Also even if {_format} was not optional the requirement prevents that {page} matches something that was originally + // part of {_format} when generating the URL, e.g. _format = 'mobile.html'. + $nextSeparator = self::findNextSeparator($followingPattern, $useUtf8); + $regexp = sprintf( + '[^%s%s]+', + preg_quote($defaultSeparator), + $defaultSeparator !== $nextSeparator && '' !== $nextSeparator ? preg_quote($nextSeparator) : '' + ); + if (('' !== $nextSeparator && !preg_match('#^\{[\w\x80-\xFF]+\}#', $followingPattern)) || '' === $followingPattern) { + // When we have a separator, which is disallowed for the variable, we can optimize the regex with a possessive + // quantifier. This prevents useless backtracking of PCRE and improves performance by 20% for matching those patterns. + // Given the above example, there is no point in backtracking into {page} (that forbids the dot) when a dot must follow + // after it. This optimization cannot be applied when the next char is no real separator or when the next variable is + // directly adjacent, e.g. '/{x}{y}'. + $regexp .= '+'; + } + } else { + if (!preg_match('//u', $regexp)) { + $useUtf8 = false; + } elseif (!$needsUtf8 && preg_match('/[\x80-\xFF]|(?= 0; --$i) { + $token = $tokens[$i]; + // variable is optional when it is not important and has a default value + if ('variable' === $token[0] && !($token[5] ?? false) && $route->hasDefault($token[3])) { + $firstOptional = $i; + } else { + break; + } + } + } + + // compute the matching regexp + $regexp = ''; + for ($i = 0, $nbToken = \count($tokens); $i < $nbToken; ++$i) { + $regexp .= self::computeRegexp($tokens, $i, $firstOptional); + } + $regexp = '{^'.$regexp.'$}sD'.($isHost ? 'i' : ''); + + // enable Utf8 matching if really required + if ($needsUtf8) { + $regexp .= 'u'; + for ($i = 0, $nbToken = \count($tokens); $i < $nbToken; ++$i) { + if ('variable' === $tokens[$i][0]) { + $tokens[$i][4] = true; + } + } + } + + return [ + 'staticPrefix' => self::determineStaticPrefix($route, $tokens), + 'regex' => $regexp, + 'tokens' => array_reverse($tokens), + 'variables' => $variables, + ]; + } + + /** + * Determines the longest static prefix possible for a route. + */ + private static function determineStaticPrefix(Route $route, array $tokens): string + { + if ('text' !== $tokens[0][0]) { + return ($route->hasDefault($tokens[0][3]) || '/' === $tokens[0][1]) ? '' : $tokens[0][1]; + } + + $prefix = $tokens[0][1]; + + if (isset($tokens[1][1]) && '/' !== $tokens[1][1] && false === $route->hasDefault($tokens[1][3])) { + $prefix .= $tokens[1][1]; + } + + return $prefix; + } + + /** + * Returns the next static character in the Route pattern that will serve as a separator (or the empty string when none available). + */ + private static function findNextSeparator(string $pattern, bool $useUtf8): string + { + if ('' == $pattern) { + // return empty string if pattern is empty or false (false which can be returned by substr) + return ''; + } + // first remove all placeholders from the pattern so we can find the next real static character + if ('' === $pattern = preg_replace('#\{[\w\x80-\xFF]+\}#', '', $pattern)) { + return ''; + } + if ($useUtf8) { + preg_match('/^./u', $pattern, $pattern); + } + + return str_contains(static::SEPARATORS, $pattern[0]) ? $pattern[0] : ''; + } + + /** + * Computes the regexp used to match a specific token. It can be static text or a subpattern. + * + * @param array $tokens The route tokens + * @param int $index The index of the current token + * @param int $firstOptional The index of the first optional token + */ + private static function computeRegexp(array $tokens, int $index, int $firstOptional): string + { + $token = $tokens[$index]; + if ('text' === $token[0]) { + // Text tokens + return preg_quote($token[1]); + } else { + // Variable tokens + if (0 === $index && 0 === $firstOptional) { + // When the only token is an optional variable token, the separator is required + return sprintf('%s(?P<%s>%s)?', preg_quote($token[1]), $token[3], $token[2]); + } else { + $regexp = sprintf('%s(?P<%s>%s)', preg_quote($token[1]), $token[3], $token[2]); + if ($index >= $firstOptional) { + // Enclose each optional token in a subpattern to make it optional. + // "?:" means it is non-capturing, i.e. the portion of the subject string that + // matched the optional subpattern is not passed back. + $regexp = "(?:$regexp"; + $nbTokens = \count($tokens); + if ($nbTokens - 1 == $index) { + // Close the optional subpatterns + $regexp .= str_repeat(')?', $nbTokens - $firstOptional - (0 === $firstOptional ? 1 : 0)); + } + } + + return $regexp; + } + } + } + + private static function transformCapturingGroupsToNonCapturings(string $regexp): string + { + for ($i = 0; $i < \strlen($regexp); ++$i) { + if ('\\' === $regexp[$i]) { + ++$i; + continue; + } + if ('(' !== $regexp[$i] || !isset($regexp[$i + 2])) { + continue; + } + if ('*' === $regexp[++$i] || '?' === $regexp[$i]) { + ++$i; + continue; + } + $regexp = substr_replace($regexp, '?:', $i, 0); + ++$i; + } + + return $regexp; + } +} diff --git a/3rdparty/symfony/routing/RouteCompilerInterface.php b/3rdparty/symfony/routing/RouteCompilerInterface.php new file mode 100644 index 00000000..62156117 --- /dev/null +++ b/3rdparty/symfony/routing/RouteCompilerInterface.php @@ -0,0 +1,28 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Component\Routing; + +/** + * RouteCompilerInterface is the interface that all RouteCompiler classes must implement. + * + * @author Fabien Potencier + */ +interface RouteCompilerInterface +{ + /** + * Compiles the current route instance. + * + * @throws \LogicException If the Route cannot be compiled because the + * path or host pattern is invalid + */ + public static function compile(Route $route): CompiledRoute; +} diff --git a/3rdparty/symfony/routing/Router.php b/3rdparty/symfony/routing/Router.php new file mode 100644 index 00000000..b769caee --- /dev/null +++ b/3rdparty/symfony/routing/Router.php @@ -0,0 +1,358 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Component\Routing; + +use Psr\Log\LoggerInterface; +use Symfony\Component\Config\ConfigCacheFactory; +use Symfony\Component\Config\ConfigCacheFactoryInterface; +use Symfony\Component\Config\ConfigCacheInterface; +use Symfony\Component\Config\Loader\LoaderInterface; +use Symfony\Component\ExpressionLanguage\ExpressionFunctionProviderInterface; +use Symfony\Component\HttpFoundation\Request; +use Symfony\Component\Routing\Generator\CompiledUrlGenerator; +use Symfony\Component\Routing\Generator\ConfigurableRequirementsInterface; +use Symfony\Component\Routing\Generator\Dumper\CompiledUrlGeneratorDumper; +use Symfony\Component\Routing\Generator\Dumper\GeneratorDumperInterface; +use Symfony\Component\Routing\Generator\UrlGeneratorInterface; +use Symfony\Component\Routing\Matcher\CompiledUrlMatcher; +use Symfony\Component\Routing\Matcher\Dumper\CompiledUrlMatcherDumper; +use Symfony\Component\Routing\Matcher\Dumper\MatcherDumperInterface; +use Symfony\Component\Routing\Matcher\RequestMatcherInterface; +use Symfony\Component\Routing\Matcher\UrlMatcherInterface; + +/** + * The Router class is an example of the integration of all pieces of the + * routing system for easier use. + * + * @author Fabien Potencier + */ +class Router implements RouterInterface, RequestMatcherInterface +{ + /** + * @var UrlMatcherInterface|null + */ + protected $matcher; + + /** + * @var UrlGeneratorInterface|null + */ + protected $generator; + + /** + * @var RequestContext + */ + protected $context; + + /** + * @var LoaderInterface + */ + protected $loader; + + /** + * @var RouteCollection|null + */ + protected $collection; + + /** + * @var mixed + */ + protected $resource; + + /** + * @var array + */ + protected $options = []; + + /** + * @var LoggerInterface|null + */ + protected $logger; + + /** + * @var string|null + */ + protected $defaultLocale; + + private ConfigCacheFactoryInterface $configCacheFactory; + + /** + * @var ExpressionFunctionProviderInterface[] + */ + private array $expressionLanguageProviders = []; + + private static ?array $cache = []; + + public function __construct(LoaderInterface $loader, mixed $resource, array $options = [], ?RequestContext $context = null, ?LoggerInterface $logger = null, ?string $defaultLocale = null) + { + $this->loader = $loader; + $this->resource = $resource; + $this->logger = $logger; + $this->context = $context ?? new RequestContext(); + $this->setOptions($options); + $this->defaultLocale = $defaultLocale; + } + + /** + * Sets options. + * + * Available options: + * + * * cache_dir: The cache directory (or null to disable caching) + * * debug: Whether to enable debugging or not (false by default) + * * generator_class: The name of a UrlGeneratorInterface implementation + * * generator_dumper_class: The name of a GeneratorDumperInterface implementation + * * matcher_class: The name of a UrlMatcherInterface implementation + * * matcher_dumper_class: The name of a MatcherDumperInterface implementation + * * resource_type: Type hint for the main resource (optional) + * * strict_requirements: Configure strict requirement checking for generators + * implementing ConfigurableRequirementsInterface (default is true) + * + * @return void + * + * @throws \InvalidArgumentException When unsupported option is provided + */ + public function setOptions(array $options) + { + $this->options = [ + 'cache_dir' => null, + 'debug' => false, + 'generator_class' => CompiledUrlGenerator::class, + 'generator_dumper_class' => CompiledUrlGeneratorDumper::class, + 'matcher_class' => CompiledUrlMatcher::class, + 'matcher_dumper_class' => CompiledUrlMatcherDumper::class, + 'resource_type' => null, + 'strict_requirements' => true, + ]; + + // check option names and live merge, if errors are encountered Exception will be thrown + $invalid = []; + foreach ($options as $key => $value) { + if (\array_key_exists($key, $this->options)) { + $this->options[$key] = $value; + } else { + $invalid[] = $key; + } + } + + if ($invalid) { + throw new \InvalidArgumentException(sprintf('The Router does not support the following options: "%s".', implode('", "', $invalid))); + } + } + + /** + * Sets an option. + * + * @return void + * + * @throws \InvalidArgumentException + */ + public function setOption(string $key, mixed $value) + { + if (!\array_key_exists($key, $this->options)) { + throw new \InvalidArgumentException(sprintf('The Router does not support the "%s" option.', $key)); + } + + $this->options[$key] = $value; + } + + /** + * Gets an option value. + * + * @throws \InvalidArgumentException + */ + public function getOption(string $key): mixed + { + if (!\array_key_exists($key, $this->options)) { + throw new \InvalidArgumentException(sprintf('The Router does not support the "%s" option.', $key)); + } + + return $this->options[$key]; + } + + /** + * @return RouteCollection + */ + public function getRouteCollection() + { + return $this->collection ??= $this->loader->load($this->resource, $this->options['resource_type']); + } + + /** + * @return void + */ + public function setContext(RequestContext $context) + { + $this->context = $context; + + if (isset($this->matcher)) { + $this->getMatcher()->setContext($context); + } + if (isset($this->generator)) { + $this->getGenerator()->setContext($context); + } + } + + public function getContext(): RequestContext + { + return $this->context; + } + + /** + * Sets the ConfigCache factory to use. + * + * @return void + */ + public function setConfigCacheFactory(ConfigCacheFactoryInterface $configCacheFactory) + { + $this->configCacheFactory = $configCacheFactory; + } + + public function generate(string $name, array $parameters = [], int $referenceType = self::ABSOLUTE_PATH): string + { + return $this->getGenerator()->generate($name, $parameters, $referenceType); + } + + public function match(string $pathinfo): array + { + return $this->getMatcher()->match($pathinfo); + } + + public function matchRequest(Request $request): array + { + $matcher = $this->getMatcher(); + if (!$matcher instanceof RequestMatcherInterface) { + // fallback to the default UrlMatcherInterface + return $matcher->match($request->getPathInfo()); + } + + return $matcher->matchRequest($request); + } + + /** + * Gets the UrlMatcher or RequestMatcher instance associated with this Router. + */ + public function getMatcher(): UrlMatcherInterface|RequestMatcherInterface + { + if (isset($this->matcher)) { + return $this->matcher; + } + + if (null === $this->options['cache_dir']) { + $routes = $this->getRouteCollection(); + $compiled = is_a($this->options['matcher_class'], CompiledUrlMatcher::class, true); + if ($compiled) { + $routes = (new CompiledUrlMatcherDumper($routes))->getCompiledRoutes(); + } + $this->matcher = new $this->options['matcher_class']($routes, $this->context); + if (method_exists($this->matcher, 'addExpressionLanguageProvider')) { + foreach ($this->expressionLanguageProviders as $provider) { + $this->matcher->addExpressionLanguageProvider($provider); + } + } + + return $this->matcher; + } + + $cache = $this->getConfigCacheFactory()->cache($this->options['cache_dir'].'/url_matching_routes.php', + function (ConfigCacheInterface $cache) { + $dumper = $this->getMatcherDumperInstance(); + if (method_exists($dumper, 'addExpressionLanguageProvider')) { + foreach ($this->expressionLanguageProviders as $provider) { + $dumper->addExpressionLanguageProvider($provider); + } + } + + $cache->write($dumper->dump(), $this->getRouteCollection()->getResources()); + unset(self::$cache[$cache->getPath()]); + } + ); + + return $this->matcher = new $this->options['matcher_class'](self::getCompiledRoutes($cache->getPath()), $this->context); + } + + /** + * Gets the UrlGenerator instance associated with this Router. + */ + public function getGenerator(): UrlGeneratorInterface + { + if (isset($this->generator)) { + return $this->generator; + } + + if (null === $this->options['cache_dir']) { + $routes = $this->getRouteCollection(); + $compiled = is_a($this->options['generator_class'], CompiledUrlGenerator::class, true); + if ($compiled) { + $generatorDumper = new CompiledUrlGeneratorDumper($routes); + $routes = array_merge($generatorDumper->getCompiledRoutes(), $generatorDumper->getCompiledAliases()); + } + $this->generator = new $this->options['generator_class']($routes, $this->context, $this->logger, $this->defaultLocale); + } else { + $cache = $this->getConfigCacheFactory()->cache($this->options['cache_dir'].'/url_generating_routes.php', + function (ConfigCacheInterface $cache) { + $dumper = $this->getGeneratorDumperInstance(); + + $cache->write($dumper->dump(), $this->getRouteCollection()->getResources()); + unset(self::$cache[$cache->getPath()]); + } + ); + + $this->generator = new $this->options['generator_class'](self::getCompiledRoutes($cache->getPath()), $this->context, $this->logger, $this->defaultLocale); + } + + if ($this->generator instanceof ConfigurableRequirementsInterface) { + $this->generator->setStrictRequirements($this->options['strict_requirements']); + } + + return $this->generator; + } + + /** + * @return void + */ + public function addExpressionLanguageProvider(ExpressionFunctionProviderInterface $provider) + { + $this->expressionLanguageProviders[] = $provider; + } + + protected function getGeneratorDumperInstance(): GeneratorDumperInterface + { + return new $this->options['generator_dumper_class']($this->getRouteCollection()); + } + + protected function getMatcherDumperInstance(): MatcherDumperInterface + { + return new $this->options['matcher_dumper_class']($this->getRouteCollection()); + } + + /** + * Provides the ConfigCache factory implementation, falling back to a + * default implementation if necessary. + */ + private function getConfigCacheFactory(): ConfigCacheFactoryInterface + { + return $this->configCacheFactory ??= new ConfigCacheFactory($this->options['debug']); + } + + private static function getCompiledRoutes(string $path): array + { + if ([] === self::$cache && \function_exists('opcache_invalidate') && filter_var(\ini_get('opcache.enable'), \FILTER_VALIDATE_BOOL) && (!\in_array(\PHP_SAPI, ['cli', 'phpdbg', 'embed'], true) || filter_var(\ini_get('opcache.enable_cli'), \FILTER_VALIDATE_BOOL))) { + self::$cache = null; + } + + if (null === self::$cache) { + return require $path; + } + + return self::$cache[$path] ??= require $path; + } +} diff --git a/3rdparty/symfony/routing/RouterInterface.php b/3rdparty/symfony/routing/RouterInterface.php new file mode 100644 index 00000000..6912f8a1 --- /dev/null +++ b/3rdparty/symfony/routing/RouterInterface.php @@ -0,0 +1,35 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Component\Routing; + +use Symfony\Component\Routing\Generator\UrlGeneratorInterface; +use Symfony\Component\Routing\Matcher\UrlMatcherInterface; + +/** + * RouterInterface is the interface that all Router classes must implement. + * + * This interface is the concatenation of UrlMatcherInterface and UrlGeneratorInterface. + * + * @author Fabien Potencier + */ +interface RouterInterface extends UrlMatcherInterface, UrlGeneratorInterface +{ + /** + * Gets the RouteCollection instance associated with this Router. + * + * WARNING: This method should never be used at runtime as it is SLOW. + * You might use it in a cache warmer though. + * + * @return RouteCollection + */ + public function getRouteCollection(); +} diff --git a/3rdparty/symfony/service-contracts/Attribute/Required.php b/3rdparty/symfony/service-contracts/Attribute/Required.php new file mode 100644 index 00000000..9df85118 --- /dev/null +++ b/3rdparty/symfony/service-contracts/Attribute/Required.php @@ -0,0 +1,25 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Contracts\Service\Attribute; + +/** + * A required dependency. + * + * This attribute indicates that a property holds a required dependency. The annotated property or method should be + * considered during the instantiation process of the containing class. + * + * @author Alexander M. Turek + */ +#[\Attribute(\Attribute::TARGET_METHOD | \Attribute::TARGET_PROPERTY)] +final class Required +{ +} diff --git a/3rdparty/symfony/service-contracts/Attribute/SubscribedService.php b/3rdparty/symfony/service-contracts/Attribute/SubscribedService.php new file mode 100644 index 00000000..f850b840 --- /dev/null +++ b/3rdparty/symfony/service-contracts/Attribute/SubscribedService.php @@ -0,0 +1,47 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Contracts\Service\Attribute; + +use Symfony\Contracts\Service\ServiceMethodsSubscriberTrait; +use Symfony\Contracts\Service\ServiceSubscriberInterface; + +/** + * For use as the return value for {@see ServiceSubscriberInterface}. + * + * @example new SubscribedService('http_client', HttpClientInterface::class, false, new Target('githubApi')) + * + * Use with {@see ServiceMethodsSubscriberTrait} to mark a method's return type + * as a subscribed service. + * + * @author Kevin Bond + */ +#[\Attribute(\Attribute::TARGET_METHOD)] +final class SubscribedService +{ + /** @var object[] */ + public array $attributes; + + /** + * @param string|null $key The key to use for the service + * @param class-string|null $type The service class + * @param bool $nullable Whether the service is optional + * @param object|object[] $attributes One or more dependency injection attributes to use + */ + public function __construct( + public ?string $key = null, + public ?string $type = null, + public bool $nullable = false, + array|object $attributes = [], + ) { + $this->attributes = \is_array($attributes) ? $attributes : [$attributes]; + } +} diff --git a/3rdparty/symfony/service-contracts/LICENSE b/3rdparty/symfony/service-contracts/LICENSE new file mode 100644 index 00000000..7536caea --- /dev/null +++ b/3rdparty/symfony/service-contracts/LICENSE @@ -0,0 +1,19 @@ +Copyright (c) 2018-present Fabien Potencier + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is furnished +to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +THE SOFTWARE. diff --git a/3rdparty/symfony/service-contracts/ResetInterface.php b/3rdparty/symfony/service-contracts/ResetInterface.php new file mode 100644 index 00000000..a4f389b0 --- /dev/null +++ b/3rdparty/symfony/service-contracts/ResetInterface.php @@ -0,0 +1,33 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Contracts\Service; + +/** + * Provides a way to reset an object to its initial state. + * + * When calling the "reset()" method on an object, it should be put back to its + * initial state. This usually means clearing any internal buffers and forwarding + * the call to internal dependencies. All properties of the object should be put + * back to the same state it had when it was first ready to use. + * + * This method could be called, for example, to recycle objects that are used as + * services, so that they can be used to handle several requests in the same + * process loop (note that we advise making your services stateless instead of + * implementing this interface when possible.) + */ +interface ResetInterface +{ + /** + * @return void + */ + public function reset(); +} diff --git a/3rdparty/symfony/service-contracts/ServiceCollectionInterface.php b/3rdparty/symfony/service-contracts/ServiceCollectionInterface.php new file mode 100644 index 00000000..2333139c --- /dev/null +++ b/3rdparty/symfony/service-contracts/ServiceCollectionInterface.php @@ -0,0 +1,26 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Contracts\Service; + +/** + * A ServiceProviderInterface that is also countable and iterable. + * + * @author Kevin Bond + * + * @template-covariant T of mixed + * + * @extends ServiceProviderInterface + * @extends \IteratorAggregate + */ +interface ServiceCollectionInterface extends ServiceProviderInterface, \Countable, \IteratorAggregate +{ +} diff --git a/3rdparty/symfony/service-contracts/ServiceLocatorTrait.php b/3rdparty/symfony/service-contracts/ServiceLocatorTrait.php new file mode 100644 index 00000000..b62ec3e5 --- /dev/null +++ b/3rdparty/symfony/service-contracts/ServiceLocatorTrait.php @@ -0,0 +1,115 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Contracts\Service; + +use Psr\Container\ContainerExceptionInterface; +use Psr\Container\NotFoundExceptionInterface; + +// Help opcache.preload discover always-needed symbols +class_exists(ContainerExceptionInterface::class); +class_exists(NotFoundExceptionInterface::class); + +/** + * A trait to help implement ServiceProviderInterface. + * + * @author Robin Chalas + * @author Nicolas Grekas + */ +trait ServiceLocatorTrait +{ + private array $factories; + private array $loading = []; + private array $providedTypes; + + /** + * @param array $factories + */ + public function __construct(array $factories) + { + $this->factories = $factories; + } + + public function has(string $id): bool + { + return isset($this->factories[$id]); + } + + public function get(string $id): mixed + { + if (!isset($this->factories[$id])) { + throw $this->createNotFoundException($id); + } + + if (isset($this->loading[$id])) { + $ids = array_values($this->loading); + $ids = \array_slice($this->loading, array_search($id, $ids)); + $ids[] = $id; + + throw $this->createCircularReferenceException($id, $ids); + } + + $this->loading[$id] = $id; + try { + return $this->factories[$id]($this); + } finally { + unset($this->loading[$id]); + } + } + + public function getProvidedServices(): array + { + if (!isset($this->providedTypes)) { + $this->providedTypes = []; + + foreach ($this->factories as $name => $factory) { + if (!\is_callable($factory)) { + $this->providedTypes[$name] = '?'; + } else { + $type = (new \ReflectionFunction($factory))->getReturnType(); + + $this->providedTypes[$name] = $type ? ($type->allowsNull() ? '?' : '').($type instanceof \ReflectionNamedType ? $type->getName() : $type) : '?'; + } + } + } + + return $this->providedTypes; + } + + private function createNotFoundException(string $id): NotFoundExceptionInterface + { + if (!$alternatives = array_keys($this->factories)) { + $message = 'is empty...'; + } else { + $last = array_pop($alternatives); + if ($alternatives) { + $message = sprintf('only knows about the "%s" and "%s" services.', implode('", "', $alternatives), $last); + } else { + $message = sprintf('only knows about the "%s" service.', $last); + } + } + + if ($this->loading) { + $message = sprintf('The service "%s" has a dependency on a non-existent service "%s". This locator %s', end($this->loading), $id, $message); + } else { + $message = sprintf('Service "%s" not found: the current service locator %s', $id, $message); + } + + return new class($message) extends \InvalidArgumentException implements NotFoundExceptionInterface { + }; + } + + private function createCircularReferenceException(string $id, array $path): ContainerExceptionInterface + { + return new class(sprintf('Circular reference detected for service "%s", path: "%s".', $id, implode(' -> ', $path))) extends \RuntimeException implements ContainerExceptionInterface { + }; + } +} diff --git a/3rdparty/symfony/service-contracts/ServiceMethodsSubscriberTrait.php b/3rdparty/symfony/service-contracts/ServiceMethodsSubscriberTrait.php new file mode 100644 index 00000000..0d89d9f2 --- /dev/null +++ b/3rdparty/symfony/service-contracts/ServiceMethodsSubscriberTrait.php @@ -0,0 +1,80 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Contracts\Service; + +use Psr\Container\ContainerInterface; +use Symfony\Contracts\Service\Attribute\Required; +use Symfony\Contracts\Service\Attribute\SubscribedService; + +/** + * Implementation of ServiceSubscriberInterface that determines subscribed services + * from methods that have the #[SubscribedService] attribute. + * + * Service ids are available as "ClassName::methodName" so that the implementation + * of subscriber methods can be just `return $this->container->get(__METHOD__);`. + * + * @author Kevin Bond + */ +trait ServiceMethodsSubscriberTrait +{ + protected ContainerInterface $container; + + public static function getSubscribedServices(): array + { + $services = method_exists(get_parent_class(self::class) ?: '', __FUNCTION__) ? parent::getSubscribedServices() : []; + + foreach ((new \ReflectionClass(self::class))->getMethods() as $method) { + if (self::class !== $method->getDeclaringClass()->name) { + continue; + } + + if (!$attribute = $method->getAttributes(SubscribedService::class)[0] ?? null) { + continue; + } + + if ($method->isStatic() || $method->isAbstract() || $method->isGenerator() || $method->isInternal() || $method->getNumberOfRequiredParameters()) { + throw new \LogicException(sprintf('Cannot use "%s" on method "%s::%s()" (can only be used on non-static, non-abstract methods with no parameters).', SubscribedService::class, self::class, $method->name)); + } + + if (!$returnType = $method->getReturnType()) { + throw new \LogicException(sprintf('Cannot use "%s" on methods without a return type in "%s::%s()".', SubscribedService::class, $method->name, self::class)); + } + + /* @var SubscribedService $attribute */ + $attribute = $attribute->newInstance(); + $attribute->key ??= self::class.'::'.$method->name; + $attribute->type ??= $returnType instanceof \ReflectionNamedType ? $returnType->getName() : (string) $returnType; + $attribute->nullable = $returnType->allowsNull(); + + if ($attribute->attributes) { + $services[] = $attribute; + } else { + $services[$attribute->key] = ($attribute->nullable ? '?' : '').$attribute->type; + } + } + + return $services; + } + + #[Required] + public function setContainer(ContainerInterface $container): ?ContainerInterface + { + $ret = null; + if (method_exists(get_parent_class(self::class) ?: '', __FUNCTION__)) { + $ret = parent::setContainer($container); + } + + $this->container = $container; + + return $ret; + } +} diff --git a/3rdparty/symfony/service-contracts/ServiceProviderInterface.php b/3rdparty/symfony/service-contracts/ServiceProviderInterface.php new file mode 100644 index 00000000..2e71f00c --- /dev/null +++ b/3rdparty/symfony/service-contracts/ServiceProviderInterface.php @@ -0,0 +1,45 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Contracts\Service; + +use Psr\Container\ContainerInterface; + +/** + * A ServiceProviderInterface exposes the identifiers and the types of services provided by a container. + * + * @author Nicolas Grekas + * @author Mateusz Sip + * + * @template-covariant T of mixed + */ +interface ServiceProviderInterface extends ContainerInterface +{ + /** + * @return T + */ + public function get(string $id): mixed; + + public function has(string $id): bool; + + /** + * Returns an associative array of service types keyed by the identifiers provided by the current container. + * + * Examples: + * + * * ['logger' => 'Psr\Log\LoggerInterface'] means the object provides a service named "logger" that implements Psr\Log\LoggerInterface + * * ['foo' => '?'] means the container provides service name "foo" of unspecified type + * * ['bar' => '?Bar\Baz'] means the container provides a service "bar" of type Bar\Baz|null + * + * @return array The provided service types, keyed by service names + */ + public function getProvidedServices(): array; +} diff --git a/3rdparty/symfony/service-contracts/ServiceSubscriberInterface.php b/3rdparty/symfony/service-contracts/ServiceSubscriberInterface.php new file mode 100644 index 00000000..3da19169 --- /dev/null +++ b/3rdparty/symfony/service-contracts/ServiceSubscriberInterface.php @@ -0,0 +1,62 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Contracts\Service; + +use Symfony\Contracts\Service\Attribute\SubscribedService; + +/** + * A ServiceSubscriber exposes its dependencies via the static {@link getSubscribedServices} method. + * + * The getSubscribedServices method returns an array of service types required by such instances, + * optionally keyed by the service names used internally. Service types that start with an interrogation + * mark "?" are optional, while the other ones are mandatory service dependencies. + * + * The injected service locators SHOULD NOT allow access to any other services not specified by the method. + * + * It is expected that ServiceSubscriber instances consume PSR-11-based service locators internally. + * This interface does not dictate any injection method for these service locators, although constructor + * injection is recommended. + * + * @author Nicolas Grekas + */ +interface ServiceSubscriberInterface +{ + /** + * Returns an array of service types (or {@see SubscribedService} objects) required + * by such instances, optionally keyed by the service names used internally. + * + * For mandatory dependencies: + * + * * ['logger' => 'Psr\Log\LoggerInterface'] means the objects use the "logger" name + * internally to fetch a service which must implement Psr\Log\LoggerInterface. + * * ['loggers' => 'Psr\Log\LoggerInterface[]'] means the objects use the "loggers" name + * internally to fetch an iterable of Psr\Log\LoggerInterface instances. + * * ['Psr\Log\LoggerInterface'] is a shortcut for + * * ['Psr\Log\LoggerInterface' => 'Psr\Log\LoggerInterface'] + * + * otherwise: + * + * * ['logger' => '?Psr\Log\LoggerInterface'] denotes an optional dependency + * * ['loggers' => '?Psr\Log\LoggerInterface[]'] denotes an optional iterable dependency + * * ['?Psr\Log\LoggerInterface'] is a shortcut for + * * ['Psr\Log\LoggerInterface' => '?Psr\Log\LoggerInterface'] + * + * additionally, an array of {@see SubscribedService}'s can be returned: + * + * * [new SubscribedService('logger', Psr\Log\LoggerInterface::class)] + * * [new SubscribedService(type: Psr\Log\LoggerInterface::class, nullable: true)] + * * [new SubscribedService('http_client', HttpClientInterface::class, attributes: new Target('githubApi'))] + * + * @return string[]|SubscribedService[] The required service types, optionally keyed by service names + */ + public static function getSubscribedServices(): array; +} diff --git a/3rdparty/symfony/service-contracts/ServiceSubscriberTrait.php b/3rdparty/symfony/service-contracts/ServiceSubscriberTrait.php new file mode 100644 index 00000000..cc3bc321 --- /dev/null +++ b/3rdparty/symfony/service-contracts/ServiceSubscriberTrait.php @@ -0,0 +1,84 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Contracts\Service; + +use Psr\Container\ContainerInterface; +use Symfony\Contracts\Service\Attribute\Required; +use Symfony\Contracts\Service\Attribute\SubscribedService; + +trigger_deprecation('symfony/contracts', 'v3.5', '"%s" is deprecated, use "ServiceMethodsSubscriberTrait" instead.', ServiceSubscriberTrait::class); + +/** + * Implementation of ServiceSubscriberInterface that determines subscribed services + * from methods that have the #[SubscribedService] attribute. + * + * Service ids are available as "ClassName::methodName" so that the implementation + * of subscriber methods can be just `return $this->container->get(__METHOD__);`. + * + * @property ContainerInterface $container + * + * @author Kevin Bond + * + * @deprecated since symfony/contracts v3.5, use ServiceMethodsSubscriberTrait instead + */ +trait ServiceSubscriberTrait +{ + public static function getSubscribedServices(): array + { + $services = method_exists(get_parent_class(self::class) ?: '', __FUNCTION__) ? parent::getSubscribedServices() : []; + + foreach ((new \ReflectionClass(self::class))->getMethods() as $method) { + if (self::class !== $method->getDeclaringClass()->name) { + continue; + } + + if (!$attribute = $method->getAttributes(SubscribedService::class)[0] ?? null) { + continue; + } + + if ($method->isStatic() || $method->isAbstract() || $method->isGenerator() || $method->isInternal() || $method->getNumberOfRequiredParameters()) { + throw new \LogicException(sprintf('Cannot use "%s" on method "%s::%s()" (can only be used on non-static, non-abstract methods with no parameters).', SubscribedService::class, self::class, $method->name)); + } + + if (!$returnType = $method->getReturnType()) { + throw new \LogicException(sprintf('Cannot use "%s" on methods without a return type in "%s::%s()".', SubscribedService::class, $method->name, self::class)); + } + + /* @var SubscribedService $attribute */ + $attribute = $attribute->newInstance(); + $attribute->key ??= self::class.'::'.$method->name; + $attribute->type ??= $returnType instanceof \ReflectionNamedType ? $returnType->getName() : (string) $returnType; + $attribute->nullable = $returnType->allowsNull(); + + if ($attribute->attributes) { + $services[] = $attribute; + } else { + $services[$attribute->key] = ($attribute->nullable ? '?' : '').$attribute->type; + } + } + + return $services; + } + + #[Required] + public function setContainer(ContainerInterface $container): ?ContainerInterface + { + $ret = null; + if (method_exists(get_parent_class(self::class) ?: '', __FUNCTION__)) { + $ret = parent::setContainer($container); + } + + $this->container = $container; + + return $ret; + } +} diff --git a/3rdparty/symfony/string/AbstractString.php b/3rdparty/symfony/string/AbstractString.php new file mode 100644 index 00000000..f55c7216 --- /dev/null +++ b/3rdparty/symfony/string/AbstractString.php @@ -0,0 +1,702 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Component\String; + +use Symfony\Component\String\Exception\ExceptionInterface; +use Symfony\Component\String\Exception\InvalidArgumentException; +use Symfony\Component\String\Exception\RuntimeException; + +/** + * Represents a string of abstract characters. + * + * Unicode defines 3 types of "characters" (bytes, code points and grapheme clusters). + * This class is the abstract type to use as a type-hint when the logic you want to + * implement doesn't care about the exact variant it deals with. + * + * @author Nicolas Grekas + * @author Hugo Hamon + * + * @throws ExceptionInterface + */ +abstract class AbstractString implements \Stringable, \JsonSerializable +{ + public const PREG_PATTERN_ORDER = \PREG_PATTERN_ORDER; + public const PREG_SET_ORDER = \PREG_SET_ORDER; + public const PREG_OFFSET_CAPTURE = \PREG_OFFSET_CAPTURE; + public const PREG_UNMATCHED_AS_NULL = \PREG_UNMATCHED_AS_NULL; + + public const PREG_SPLIT = 0; + public const PREG_SPLIT_NO_EMPTY = \PREG_SPLIT_NO_EMPTY; + public const PREG_SPLIT_DELIM_CAPTURE = \PREG_SPLIT_DELIM_CAPTURE; + public const PREG_SPLIT_OFFSET_CAPTURE = \PREG_SPLIT_OFFSET_CAPTURE; + + protected $string = ''; + protected $ignoreCase = false; + + abstract public function __construct(string $string = ''); + + /** + * Unwraps instances of AbstractString back to strings. + * + * @return string[]|array + */ + public static function unwrap(array $values): array + { + foreach ($values as $k => $v) { + if ($v instanceof self) { + $values[$k] = $v->__toString(); + } elseif (\is_array($v) && $values[$k] !== $v = static::unwrap($v)) { + $values[$k] = $v; + } + } + + return $values; + } + + /** + * Wraps (and normalizes) strings in instances of AbstractString. + * + * @return static[]|array + */ + public static function wrap(array $values): array + { + $i = 0; + $keys = null; + + foreach ($values as $k => $v) { + if (\is_string($k) && '' !== $k && $k !== $j = (string) new static($k)) { + $keys ??= array_keys($values); + $keys[$i] = $j; + } + + if (\is_string($v)) { + $values[$k] = new static($v); + } elseif (\is_array($v) && $values[$k] !== $v = static::wrap($v)) { + $values[$k] = $v; + } + + ++$i; + } + + return null !== $keys ? array_combine($keys, $values) : $values; + } + + /** + * @param string|string[] $needle + */ + public function after(string|iterable $needle, bool $includeNeedle = false, int $offset = 0): static + { + $str = clone $this; + $i = \PHP_INT_MAX; + + if (\is_string($needle)) { + $needle = [$needle]; + } + + foreach ($needle as $n) { + $n = (string) $n; + $j = $this->indexOf($n, $offset); + + if (null !== $j && $j < $i) { + $i = $j; + $str->string = $n; + } + } + + if (\PHP_INT_MAX === $i) { + return $str; + } + + if (!$includeNeedle) { + $i += $str->length(); + } + + return $this->slice($i); + } + + /** + * @param string|string[] $needle + */ + public function afterLast(string|iterable $needle, bool $includeNeedle = false, int $offset = 0): static + { + $str = clone $this; + $i = null; + + if (\is_string($needle)) { + $needle = [$needle]; + } + + foreach ($needle as $n) { + $n = (string) $n; + $j = $this->indexOfLast($n, $offset); + + if (null !== $j && $j >= $i) { + $i = $offset = $j; + $str->string = $n; + } + } + + if (null === $i) { + return $str; + } + + if (!$includeNeedle) { + $i += $str->length(); + } + + return $this->slice($i); + } + + abstract public function append(string ...$suffix): static; + + /** + * @param string|string[] $needle + */ + public function before(string|iterable $needle, bool $includeNeedle = false, int $offset = 0): static + { + $str = clone $this; + $i = \PHP_INT_MAX; + + if (\is_string($needle)) { + $needle = [$needle]; + } + + foreach ($needle as $n) { + $n = (string) $n; + $j = $this->indexOf($n, $offset); + + if (null !== $j && $j < $i) { + $i = $j; + $str->string = $n; + } + } + + if (\PHP_INT_MAX === $i) { + return $str; + } + + if ($includeNeedle) { + $i += $str->length(); + } + + return $this->slice(0, $i); + } + + /** + * @param string|string[] $needle + */ + public function beforeLast(string|iterable $needle, bool $includeNeedle = false, int $offset = 0): static + { + $str = clone $this; + $i = null; + + if (\is_string($needle)) { + $needle = [$needle]; + } + + foreach ($needle as $n) { + $n = (string) $n; + $j = $this->indexOfLast($n, $offset); + + if (null !== $j && $j >= $i) { + $i = $offset = $j; + $str->string = $n; + } + } + + if (null === $i) { + return $str; + } + + if ($includeNeedle) { + $i += $str->length(); + } + + return $this->slice(0, $i); + } + + /** + * @return int[] + */ + public function bytesAt(int $offset): array + { + $str = $this->slice($offset, 1); + + return '' === $str->string ? [] : array_values(unpack('C*', $str->string)); + } + + abstract public function camel(): static; + + /** + * @return static[] + */ + abstract public function chunk(int $length = 1): array; + + public function collapseWhitespace(): static + { + $str = clone $this; + $str->string = trim(preg_replace("/(?:[ \n\r\t\x0C]{2,}+|[\n\r\t\x0C])/", ' ', $str->string), " \n\r\t\x0C"); + + return $str; + } + + /** + * @param string|string[] $needle + */ + public function containsAny(string|iterable $needle): bool + { + return null !== $this->indexOf($needle); + } + + /** + * @param string|string[] $suffix + */ + public function endsWith(string|iterable $suffix): bool + { + if (\is_string($suffix)) { + throw new \TypeError(sprintf('Method "%s()" must be overridden by class "%s" to deal with non-iterable values.', __FUNCTION__, static::class)); + } + + foreach ($suffix as $s) { + if ($this->endsWith((string) $s)) { + return true; + } + } + + return false; + } + + public function ensureEnd(string $suffix): static + { + if (!$this->endsWith($suffix)) { + return $this->append($suffix); + } + + $suffix = preg_quote($suffix); + $regex = '{('.$suffix.')(?:'.$suffix.')++$}D'; + + return $this->replaceMatches($regex.($this->ignoreCase ? 'i' : ''), '$1'); + } + + public function ensureStart(string $prefix): static + { + $prefix = new static($prefix); + + if (!$this->startsWith($prefix)) { + return $this->prepend($prefix); + } + + $str = clone $this; + $i = $prefixLen = $prefix->length(); + + while ($this->indexOf($prefix, $i) === $i) { + $str = $str->slice($prefixLen); + $i += $prefixLen; + } + + return $str; + } + + /** + * @param string|string[] $string + */ + public function equalsTo(string|iterable $string): bool + { + if (\is_string($string)) { + throw new \TypeError(sprintf('Method "%s()" must be overridden by class "%s" to deal with non-iterable values.', __FUNCTION__, static::class)); + } + + foreach ($string as $s) { + if ($this->equalsTo((string) $s)) { + return true; + } + } + + return false; + } + + abstract public function folded(): static; + + public function ignoreCase(): static + { + $str = clone $this; + $str->ignoreCase = true; + + return $str; + } + + /** + * @param string|string[] $needle + */ + public function indexOf(string|iterable $needle, int $offset = 0): ?int + { + if (\is_string($needle)) { + throw new \TypeError(sprintf('Method "%s()" must be overridden by class "%s" to deal with non-iterable values.', __FUNCTION__, static::class)); + } + + $i = \PHP_INT_MAX; + + foreach ($needle as $n) { + $j = $this->indexOf((string) $n, $offset); + + if (null !== $j && $j < $i) { + $i = $j; + } + } + + return \PHP_INT_MAX === $i ? null : $i; + } + + /** + * @param string|string[] $needle + */ + public function indexOfLast(string|iterable $needle, int $offset = 0): ?int + { + if (\is_string($needle)) { + throw new \TypeError(sprintf('Method "%s()" must be overridden by class "%s" to deal with non-iterable values.', __FUNCTION__, static::class)); + } + + $i = null; + + foreach ($needle as $n) { + $j = $this->indexOfLast((string) $n, $offset); + + if (null !== $j && $j >= $i) { + $i = $offset = $j; + } + } + + return $i; + } + + public function isEmpty(): bool + { + return '' === $this->string; + } + + abstract public function join(array $strings, ?string $lastGlue = null): static; + + public function jsonSerialize(): string + { + return $this->string; + } + + abstract public function length(): int; + + abstract public function lower(): static; + + /** + * Matches the string using a regular expression. + * + * Pass PREG_PATTERN_ORDER or PREG_SET_ORDER as $flags to get all occurrences matching the regular expression. + * + * @return array All matches in a multi-dimensional array ordered according to flags + */ + abstract public function match(string $regexp, int $flags = 0, int $offset = 0): array; + + abstract public function padBoth(int $length, string $padStr = ' '): static; + + abstract public function padEnd(int $length, string $padStr = ' '): static; + + abstract public function padStart(int $length, string $padStr = ' '): static; + + abstract public function prepend(string ...$prefix): static; + + public function repeat(int $multiplier): static + { + if (0 > $multiplier) { + throw new InvalidArgumentException(sprintf('Multiplier must be positive, %d given.', $multiplier)); + } + + $str = clone $this; + $str->string = str_repeat($str->string, $multiplier); + + return $str; + } + + abstract public function replace(string $from, string $to): static; + + abstract public function replaceMatches(string $fromRegexp, string|callable $to): static; + + abstract public function reverse(): static; + + abstract public function slice(int $start = 0, ?int $length = null): static; + + abstract public function snake(): static; + + abstract public function splice(string $replacement, int $start = 0, ?int $length = null): static; + + /** + * @return static[] + */ + public function split(string $delimiter, ?int $limit = null, ?int $flags = null): array + { + if (null === $flags) { + throw new \TypeError('Split behavior when $flags is null must be implemented by child classes.'); + } + + if ($this->ignoreCase) { + $delimiter .= 'i'; + } + + set_error_handler(static fn ($t, $m) => throw new InvalidArgumentException($m)); + + try { + if (false === $chunks = preg_split($delimiter, $this->string, $limit, $flags)) { + throw new RuntimeException('Splitting failed with error: '.preg_last_error_msg()); + } + } finally { + restore_error_handler(); + } + + $str = clone $this; + + if (self::PREG_SPLIT_OFFSET_CAPTURE & $flags) { + foreach ($chunks as &$chunk) { + $str->string = $chunk[0]; + $chunk[0] = clone $str; + } + } else { + foreach ($chunks as &$chunk) { + $str->string = $chunk; + $chunk = clone $str; + } + } + + return $chunks; + } + + /** + * @param string|string[] $prefix + */ + public function startsWith(string|iterable $prefix): bool + { + if (\is_string($prefix)) { + throw new \TypeError(sprintf('Method "%s()" must be overridden by class "%s" to deal with non-iterable values.', __FUNCTION__, static::class)); + } + + foreach ($prefix as $prefix) { + if ($this->startsWith((string) $prefix)) { + return true; + } + } + + return false; + } + + abstract public function title(bool $allWords = false): static; + + public function toByteString(?string $toEncoding = null): ByteString + { + $b = new ByteString(); + + $toEncoding = \in_array($toEncoding, ['utf8', 'utf-8', 'UTF8'], true) ? 'UTF-8' : $toEncoding; + + if (null === $toEncoding || $toEncoding === $fromEncoding = $this instanceof AbstractUnicodeString || preg_match('//u', $b->string) ? 'UTF-8' : 'Windows-1252') { + $b->string = $this->string; + + return $b; + } + + try { + $b->string = mb_convert_encoding($this->string, $toEncoding, 'UTF-8'); + } catch (\ValueError $e) { + if (!\function_exists('iconv')) { + throw new InvalidArgumentException($e->getMessage(), $e->getCode(), $e); + } + + $b->string = iconv('UTF-8', $toEncoding, $this->string); + } + + return $b; + } + + public function toCodePointString(): CodePointString + { + return new CodePointString($this->string); + } + + public function toString(): string + { + return $this->string; + } + + public function toUnicodeString(): UnicodeString + { + return new UnicodeString($this->string); + } + + abstract public function trim(string $chars = " \t\n\r\0\x0B\x0C\u{A0}\u{FEFF}"): static; + + abstract public function trimEnd(string $chars = " \t\n\r\0\x0B\x0C\u{A0}\u{FEFF}"): static; + + /** + * @param string|string[] $prefix + */ + public function trimPrefix($prefix): static + { + if (\is_array($prefix) || $prefix instanceof \Traversable) { // don't use is_iterable(), it's slow + foreach ($prefix as $s) { + $t = $this->trimPrefix($s); + + if ($t->string !== $this->string) { + return $t; + } + } + + return clone $this; + } + + $str = clone $this; + + if ($prefix instanceof self) { + $prefix = $prefix->string; + } else { + $prefix = (string) $prefix; + } + + if ('' !== $prefix && \strlen($this->string) >= \strlen($prefix) && 0 === substr_compare($this->string, $prefix, 0, \strlen($prefix), $this->ignoreCase)) { + $str->string = substr($this->string, \strlen($prefix)); + } + + return $str; + } + + abstract public function trimStart(string $chars = " \t\n\r\0\x0B\x0C\u{A0}\u{FEFF}"): static; + + /** + * @param string|string[] $suffix + */ + public function trimSuffix($suffix): static + { + if (\is_array($suffix) || $suffix instanceof \Traversable) { // don't use is_iterable(), it's slow + foreach ($suffix as $s) { + $t = $this->trimSuffix($s); + + if ($t->string !== $this->string) { + return $t; + } + } + + return clone $this; + } + + $str = clone $this; + + if ($suffix instanceof self) { + $suffix = $suffix->string; + } else { + $suffix = (string) $suffix; + } + + if ('' !== $suffix && \strlen($this->string) >= \strlen($suffix) && 0 === substr_compare($this->string, $suffix, -\strlen($suffix), null, $this->ignoreCase)) { + $str->string = substr($this->string, 0, -\strlen($suffix)); + } + + return $str; + } + + public function truncate(int $length, string $ellipsis = '', bool $cut = true): static + { + $stringLength = $this->length(); + + if ($stringLength <= $length) { + return clone $this; + } + + $ellipsisLength = '' !== $ellipsis ? (new static($ellipsis))->length() : 0; + + if ($length < $ellipsisLength) { + $ellipsisLength = 0; + } + + if (!$cut) { + if (null === $length = $this->indexOf([' ', "\r", "\n", "\t"], ($length ?: 1) - 1)) { + return clone $this; + } + + $length += $ellipsisLength; + } + + $str = $this->slice(0, $length - $ellipsisLength); + + return $ellipsisLength ? $str->trimEnd()->append($ellipsis) : $str; + } + + abstract public function upper(): static; + + /** + * Returns the printable length on a terminal. + */ + abstract public function width(bool $ignoreAnsiDecoration = true): int; + + public function wordwrap(int $width = 75, string $break = "\n", bool $cut = false): static + { + $lines = '' !== $break ? $this->split($break) : [clone $this]; + $chars = []; + $mask = ''; + + if (1 === \count($lines) && '' === $lines[0]->string) { + return $lines[0]; + } + + foreach ($lines as $i => $line) { + if ($i) { + $chars[] = $break; + $mask .= '#'; + } + + foreach ($line->chunk() as $char) { + $chars[] = $char->string; + $mask .= ' ' === $char->string ? ' ' : '?'; + } + } + + $string = ''; + $j = 0; + $b = $i = -1; + $mask = wordwrap($mask, $width, '#', $cut); + + while (false !== $b = strpos($mask, '#', $b + 1)) { + for (++$i; $i < $b; ++$i) { + $string .= $chars[$j]; + unset($chars[$j++]); + } + + if ($break === $chars[$j] || ' ' === $chars[$j]) { + unset($chars[$j++]); + } + + $string .= $break; + } + + $str = clone $this; + $str->string = $string.implode('', $chars); + + return $str; + } + + public function __sleep(): array + { + return ['string']; + } + + public function __clone() + { + $this->ignoreCase = false; + } + + public function __toString(): string + { + return $this->string; + } +} diff --git a/3rdparty/symfony/string/AbstractUnicodeString.php b/3rdparty/symfony/string/AbstractUnicodeString.php new file mode 100644 index 00000000..70598e40 --- /dev/null +++ b/3rdparty/symfony/string/AbstractUnicodeString.php @@ -0,0 +1,590 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Component\String; + +use Symfony\Component\String\Exception\ExceptionInterface; +use Symfony\Component\String\Exception\InvalidArgumentException; +use Symfony\Component\String\Exception\RuntimeException; + +/** + * Represents a string of abstract Unicode characters. + * + * Unicode defines 3 types of "characters" (bytes, code points and grapheme clusters). + * This class is the abstract type to use as a type-hint when the logic you want to + * implement is Unicode-aware but doesn't care about code points vs grapheme clusters. + * + * @author Nicolas Grekas + * + * @throws ExceptionInterface + */ +abstract class AbstractUnicodeString extends AbstractString +{ + public const NFC = \Normalizer::NFC; + public const NFD = \Normalizer::NFD; + public const NFKC = \Normalizer::NFKC; + public const NFKD = \Normalizer::NFKD; + + // all ASCII letters sorted by typical frequency of occurrence + private const ASCII = "\x20\x65\x69\x61\x73\x6E\x74\x72\x6F\x6C\x75\x64\x5D\x5B\x63\x6D\x70\x27\x0A\x67\x7C\x68\x76\x2E\x66\x62\x2C\x3A\x3D\x2D\x71\x31\x30\x43\x32\x2A\x79\x78\x29\x28\x4C\x39\x41\x53\x2F\x50\x22\x45\x6A\x4D\x49\x6B\x33\x3E\x35\x54\x3C\x44\x34\x7D\x42\x7B\x38\x46\x77\x52\x36\x37\x55\x47\x4E\x3B\x4A\x7A\x56\x23\x48\x4F\x57\x5F\x26\x21\x4B\x3F\x58\x51\x25\x59\x5C\x09\x5A\x2B\x7E\x5E\x24\x40\x60\x7F\x00\x01\x02\x03\x04\x05\x06\x07\x08\x0B\x0C\x0D\x0E\x0F\x10\x11\x12\x13\x14\x15\x16\x17\x18\x19\x1A\x1B\x1C\x1D\x1E\x1F"; + + // the subset of folded case mappings that is not in lower case mappings + private const FOLD_FROM = ['İ', 'µ', 'ſ', "\xCD\x85", 'ς', 'ϐ', 'ϑ', 'ϕ', 'ϖ', 'ϰ', 'ϱ', 'ϵ', 'ẛ', "\xE1\xBE\xBE", 'ß', 'ʼn', 'ǰ', 'ΐ', 'ΰ', 'և', 'ẖ', 'ẗ', 'ẘ', 'ẙ', 'ẚ', 'ẞ', 'ὐ', 'ὒ', 'ὔ', 'ὖ', 'ᾀ', 'ᾁ', 'ᾂ', 'ᾃ', 'ᾄ', 'ᾅ', 'ᾆ', 'ᾇ', 'ᾈ', 'ᾉ', 'ᾊ', 'ᾋ', 'ᾌ', 'ᾍ', 'ᾎ', 'ᾏ', 'ᾐ', 'ᾑ', 'ᾒ', 'ᾓ', 'ᾔ', 'ᾕ', 'ᾖ', 'ᾗ', 'ᾘ', 'ᾙ', 'ᾚ', 'ᾛ', 'ᾜ', 'ᾝ', 'ᾞ', 'ᾟ', 'ᾠ', 'ᾡ', 'ᾢ', 'ᾣ', 'ᾤ', 'ᾥ', 'ᾦ', 'ᾧ', 'ᾨ', 'ᾩ', 'ᾪ', 'ᾫ', 'ᾬ', 'ᾭ', 'ᾮ', 'ᾯ', 'ᾲ', 'ᾳ', 'ᾴ', 'ᾶ', 'ᾷ', 'ᾼ', 'ῂ', 'ῃ', 'ῄ', 'ῆ', 'ῇ', 'ῌ', 'ῒ', 'ῖ', 'ῗ', 'ῢ', 'ῤ', 'ῦ', 'ῧ', 'ῲ', 'ῳ', 'ῴ', 'ῶ', 'ῷ', 'ῼ', 'ff', 'fi', 'fl', 'ffi', 'ffl', 'ſt', 'st', 'ﬓ', 'ﬔ', 'ﬕ', 'ﬖ', 'ﬗ']; + private const FOLD_TO = ['i̇', 'μ', 's', 'ι', 'σ', 'β', 'θ', 'φ', 'π', 'κ', 'ρ', 'ε', 'ṡ', 'ι', 'ss', 'ʼn', 'ǰ', 'ΐ', 'ΰ', 'եւ', 'ẖ', 'ẗ', 'ẘ', 'ẙ', 'aʾ', 'ss', 'ὐ', 'ὒ', 'ὔ', 'ὖ', 'ἀι', 'ἁι', 'ἂι', 'ἃι', 'ἄι', 'ἅι', 'ἆι', 'ἇι', 'ἀι', 'ἁι', 'ἂι', 'ἃι', 'ἄι', 'ἅι', 'ἆι', 'ἇι', 'ἠι', 'ἡι', 'ἢι', 'ἣι', 'ἤι', 'ἥι', 'ἦι', 'ἧι', 'ἠι', 'ἡι', 'ἢι', 'ἣι', 'ἤι', 'ἥι', 'ἦι', 'ἧι', 'ὠι', 'ὡι', 'ὢι', 'ὣι', 'ὤι', 'ὥι', 'ὦι', 'ὧι', 'ὠι', 'ὡι', 'ὢι', 'ὣι', 'ὤι', 'ὥι', 'ὦι', 'ὧι', 'ὰι', 'αι', 'άι', 'ᾶ', 'ᾶι', 'αι', 'ὴι', 'ηι', 'ήι', 'ῆ', 'ῆι', 'ηι', 'ῒ', 'ῖ', 'ῗ', 'ῢ', 'ῤ', 'ῦ', 'ῧ', 'ὼι', 'ωι', 'ώι', 'ῶ', 'ῶι', 'ωι', 'ff', 'fi', 'fl', 'ffi', 'ffl', 'st', 'st', 'մն', 'մե', 'մի', 'վն', 'մխ']; + + // the subset of https://github.com/unicode-org/cldr/blob/master/common/transforms/Latin-ASCII.xml that is not in NFKD + 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', '\'', '\'', ',', '\'', '"', '"', ',,', '"', '\'', '"', '"', '"', '<<', '>>', '<', '>', '-', '-', '-', '-', '-', '-', '-', '-', '-', '||', '/', '[', ']', '*', ',', '.', '<', '>', '<<', '>>', '[', ']', '[', ']', '[', ']', ',', '.', '[', ']', '<<', '>>', '<', '>', ',', '[', ']', '((', '))', '.', ',', '*', '/', '-', '/', '\\', '|', '||', '<<', '>>', '((', '))']; + + private static array $transliterators = []; + private static array $tableZero; + private static array $tableWide; + + public static function fromCodePoints(int ...$codes): static + { + $string = ''; + + foreach ($codes as $code) { + if (0x80 > $code %= 0x200000) { + $string .= \chr($code); + } elseif (0x800 > $code) { + $string .= \chr(0xC0 | $code >> 6).\chr(0x80 | $code & 0x3F); + } elseif (0x10000 > $code) { + $string .= \chr(0xE0 | $code >> 12).\chr(0x80 | $code >> 6 & 0x3F).\chr(0x80 | $code & 0x3F); + } else { + $string .= \chr(0xF0 | $code >> 18).\chr(0x80 | $code >> 12 & 0x3F).\chr(0x80 | $code >> 6 & 0x3F).\chr(0x80 | $code & 0x3F); + } + } + + return new static($string); + } + + /** + * Generic UTF-8 to ASCII transliteration. + * + * Install the intl extension for best results. + * + * @param string[]|\Transliterator[]|\Closure[] $rules See "*-Latin" rules from Transliterator::listIDs() + */ + public function ascii(array $rules = []): self + { + $str = clone $this; + $s = $str->string; + $str->string = ''; + + array_unshift($rules, 'nfd'); + $rules[] = 'latin-ascii'; + + if (\function_exists('transliterator_transliterate')) { + $rules[] = 'any-latin/bgn'; + } + + $rules[] = 'nfkd'; + $rules[] = '[:nonspacing mark:] remove'; + + while (\strlen($s) - 1 > $i = strspn($s, self::ASCII)) { + if (0 < --$i) { + $str->string .= substr($s, 0, $i); + $s = substr($s, $i); + } + + if (!$rule = array_shift($rules)) { + $rules = []; // An empty rule interrupts the next ones + } + + if ($rule instanceof \Transliterator) { + $s = $rule->transliterate($s); + } elseif ($rule instanceof \Closure) { + $s = $rule($s); + } elseif ($rule) { + if ('nfd' === $rule = strtolower($rule)) { + normalizer_is_normalized($s, self::NFD) ?: $s = normalizer_normalize($s, self::NFD); + } elseif ('nfkd' === $rule) { + normalizer_is_normalized($s, self::NFKD) ?: $s = normalizer_normalize($s, self::NFKD); + } elseif ('[:nonspacing mark:] remove' === $rule) { + $s = preg_replace('/\p{Mn}++/u', '', $s); + } elseif ('latin-ascii' === $rule) { + $s = str_replace(self::TRANSLIT_FROM, self::TRANSLIT_TO, $s); + } elseif ('de-ascii' === $rule) { + $s = preg_replace("/([AUO])\u{0308}(?=\p{Ll})/u", '$1e', $s); + $s = str_replace(["a\u{0308}", "o\u{0308}", "u\u{0308}", "A\u{0308}", "O\u{0308}", "U\u{0308}"], ['ae', 'oe', 'ue', 'AE', 'OE', 'UE'], $s); + } elseif (\function_exists('transliterator_transliterate')) { + if (null === $transliterator = self::$transliterators[$rule] ??= \Transliterator::create($rule)) { + if ('any-latin/bgn' === $rule) { + $rule = 'any-latin'; + $transliterator = self::$transliterators[$rule] ??= \Transliterator::create($rule); + } + + if (null === $transliterator) { + throw new InvalidArgumentException(sprintf('Unknown transliteration rule "%s".', $rule)); + } + + self::$transliterators['any-latin/bgn'] = $transliterator; + } + + $s = $transliterator->transliterate($s); + } + } elseif (!\function_exists('iconv')) { + $s = preg_replace('/[^\x00-\x7F]/u', '?', $s); + } else { + $s = @preg_replace_callback('/[^\x00-\x7F]/u', static function ($c) { + $c = (string) iconv('UTF-8', 'ASCII//TRANSLIT', $c[0]); + + if ('' === $c && '' === iconv('UTF-8', 'ASCII//TRANSLIT', '²')) { + throw new \LogicException(sprintf('"%s" requires a translit-able iconv implementation, try installing "gnu-libiconv" if you\'re using Alpine Linux.', static::class)); + } + + return 1 < \strlen($c) ? ltrim($c, '\'`"^~') : ('' !== $c ? $c : '?'); + }, $s); + } + } + + $str->string .= $s; + + return $str; + } + + public function camel(): static + { + $str = clone $this; + $str->string = str_replace(' ', '', preg_replace_callback('/\b.(?!\p{Lu})/u', static function ($m) { + static $i = 0; + + return 1 === ++$i ? ('İ' === $m[0] ? 'i̇' : mb_strtolower($m[0], 'UTF-8')) : mb_convert_case($m[0], \MB_CASE_TITLE, 'UTF-8'); + }, preg_replace('/[^\pL0-9]++/u', ' ', $this->string))); + + return $str; + } + + /** + * @return int[] + */ + public function codePointsAt(int $offset): array + { + $str = $this->slice($offset, 1); + + if ('' === $str->string) { + return []; + } + + $codePoints = []; + + foreach (preg_split('//u', $str->string, -1, \PREG_SPLIT_NO_EMPTY) as $c) { + $codePoints[] = mb_ord($c, 'UTF-8'); + } + + return $codePoints; + } + + public function folded(bool $compat = true): static + { + $str = clone $this; + + if (!$compat || !\defined('Normalizer::NFKC_CF')) { + $str->string = normalizer_normalize($str->string, $compat ? \Normalizer::NFKC : \Normalizer::NFC); + $str->string = mb_strtolower(str_replace(self::FOLD_FROM, self::FOLD_TO, $str->string), 'UTF-8'); + } else { + $str->string = normalizer_normalize($str->string, \Normalizer::NFKC_CF); + } + + return $str; + } + + public function join(array $strings, ?string $lastGlue = null): static + { + $str = clone $this; + + $tail = null !== $lastGlue && 1 < \count($strings) ? $lastGlue.array_pop($strings) : ''; + $str->string = implode($this->string, $strings).$tail; + + if (!preg_match('//u', $str->string)) { + throw new InvalidArgumentException('Invalid UTF-8 string.'); + } + + return $str; + } + + public function lower(): static + { + $str = clone $this; + $str->string = mb_strtolower(str_replace('İ', 'i̇', $str->string), 'UTF-8'); + + return $str; + } + + public function match(string $regexp, int $flags = 0, int $offset = 0): array + { + $match = ((\PREG_PATTERN_ORDER | \PREG_SET_ORDER) & $flags) ? 'preg_match_all' : 'preg_match'; + + if ($this->ignoreCase) { + $regexp .= 'i'; + } + + set_error_handler(static fn ($t, $m) => throw new InvalidArgumentException($m)); + + try { + if (false === $match($regexp.'u', $this->string, $matches, $flags | \PREG_UNMATCHED_AS_NULL, $offset)) { + throw new RuntimeException('Matching failed with error: '.preg_last_error_msg()); + } + } finally { + restore_error_handler(); + } + + return $matches; + } + + public function normalize(int $form = self::NFC): static + { + if (!\in_array($form, [self::NFC, self::NFD, self::NFKC, self::NFKD])) { + throw new InvalidArgumentException('Unsupported normalization form.'); + } + + $str = clone $this; + normalizer_is_normalized($str->string, $form) ?: $str->string = normalizer_normalize($str->string, $form); + + return $str; + } + + public function padBoth(int $length, string $padStr = ' '): static + { + if ('' === $padStr || !preg_match('//u', $padStr)) { + throw new InvalidArgumentException('Invalid UTF-8 string.'); + } + + $pad = clone $this; + $pad->string = $padStr; + + return $this->pad($length, $pad, \STR_PAD_BOTH); + } + + public function padEnd(int $length, string $padStr = ' '): static + { + if ('' === $padStr || !preg_match('//u', $padStr)) { + throw new InvalidArgumentException('Invalid UTF-8 string.'); + } + + $pad = clone $this; + $pad->string = $padStr; + + return $this->pad($length, $pad, \STR_PAD_RIGHT); + } + + public function padStart(int $length, string $padStr = ' '): static + { + if ('' === $padStr || !preg_match('//u', $padStr)) { + throw new InvalidArgumentException('Invalid UTF-8 string.'); + } + + $pad = clone $this; + $pad->string = $padStr; + + return $this->pad($length, $pad, \STR_PAD_LEFT); + } + + public function replaceMatches(string $fromRegexp, string|callable $to): static + { + if ($this->ignoreCase) { + $fromRegexp .= 'i'; + } + + if (\is_array($to) || $to instanceof \Closure) { + $replace = 'preg_replace_callback'; + $to = static function (array $m) use ($to): string { + $to = $to($m); + + if ('' !== $to && (!\is_string($to) || !preg_match('//u', $to))) { + throw new InvalidArgumentException('Replace callback must return a valid UTF-8 string.'); + } + + return $to; + }; + } elseif ('' !== $to && !preg_match('//u', $to)) { + throw new InvalidArgumentException('Invalid UTF-8 string.'); + } else { + $replace = 'preg_replace'; + } + + set_error_handler(static fn ($t, $m) => throw new InvalidArgumentException($m)); + + try { + if (null === $string = $replace($fromRegexp.'u', $to, $this->string)) { + $lastError = preg_last_error(); + + foreach (get_defined_constants(true)['pcre'] as $k => $v) { + if ($lastError === $v && str_ends_with($k, '_ERROR')) { + throw new RuntimeException('Matching failed with '.$k.'.'); + } + } + + throw new RuntimeException('Matching failed with unknown error code.'); + } + } finally { + restore_error_handler(); + } + + $str = clone $this; + $str->string = $string; + + return $str; + } + + public function reverse(): static + { + $str = clone $this; + $str->string = implode('', array_reverse(preg_split('/(\X)/u', $str->string, -1, \PREG_SPLIT_DELIM_CAPTURE | \PREG_SPLIT_NO_EMPTY))); + + return $str; + } + + public function snake(): static + { + $str = $this->camel(); + $str->string = mb_strtolower(preg_replace(['/(\p{Lu}+)(\p{Lu}\p{Ll})/u', '/([\p{Ll}0-9])(\p{Lu})/u'], '\1_\2', $str->string), 'UTF-8'); + + return $str; + } + + public function title(bool $allWords = false): static + { + $str = clone $this; + + $limit = $allWords ? -1 : 1; + + $str->string = preg_replace_callback('/\b./u', static fn (array $m): string => mb_convert_case($m[0], \MB_CASE_TITLE, 'UTF-8'), $str->string, $limit); + + return $str; + } + + 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)) { + throw new InvalidArgumentException('Invalid UTF-8 chars.'); + } + $chars = preg_quote($chars); + + $str = clone $this; + $str->string = preg_replace("{^[$chars]++|[$chars]++$}uD", '', $str->string); + + return $str; + } + + 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)) { + throw new InvalidArgumentException('Invalid UTF-8 chars.'); + } + $chars = preg_quote($chars); + + $str = clone $this; + $str->string = preg_replace("{[$chars]++$}uD", '', $str->string); + + return $str; + } + + public function trimPrefix($prefix): static + { + if (!$this->ignoreCase) { + return parent::trimPrefix($prefix); + } + + $str = clone $this; + + if ($prefix instanceof \Traversable) { + $prefix = iterator_to_array($prefix, false); + } elseif ($prefix instanceof parent) { + $prefix = $prefix->string; + } + + $prefix = implode('|', array_map('preg_quote', (array) $prefix)); + $str->string = preg_replace("{^(?:$prefix)}iuD", '', $this->string); + + return $str; + } + + 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)) { + throw new InvalidArgumentException('Invalid UTF-8 chars.'); + } + $chars = preg_quote($chars); + + $str = clone $this; + $str->string = preg_replace("{^[$chars]++}uD", '', $str->string); + + return $str; + } + + public function trimSuffix($suffix): static + { + if (!$this->ignoreCase) { + return parent::trimSuffix($suffix); + } + + $str = clone $this; + + if ($suffix instanceof \Traversable) { + $suffix = iterator_to_array($suffix, false); + } elseif ($suffix instanceof parent) { + $suffix = $suffix->string; + } + + $suffix = implode('|', array_map('preg_quote', (array) $suffix)); + $str->string = preg_replace("{(?:$suffix)$}iuD", '', $this->string); + + return $str; + } + + public function upper(): static + { + $str = clone $this; + $str->string = mb_strtoupper($str->string, 'UTF-8'); + + return $str; + } + + public function width(bool $ignoreAnsiDecoration = true): int + { + $width = 0; + $s = str_replace(["\x00", "\x05", "\x07"], '', $this->string); + + if (str_contains($s, "\r")) { + $s = str_replace(["\r\n", "\r"], "\n", $s); + } + + if (!$ignoreAnsiDecoration) { + $s = preg_replace('/[\p{Cc}\x7F]++/u', '', $s); + } + + foreach (explode("\n", $s) as $s) { + if ($ignoreAnsiDecoration) { + $s = preg_replace('/(?:\x1B(?: + \[ [\x30-\x3F]*+ [\x20-\x2F]*+ [\x40-\x7E] + | [P\]X^_] .*? \x1B\\\\ + | [\x41-\x7E] + )|[\p{Cc}\x7F]++)/xu', '', $s); + } + + $lineWidth = $this->wcswidth($s); + + if ($lineWidth > $width) { + $width = $lineWidth; + } + } + + return $width; + } + + private function pad(int $len, self $pad, int $type): static + { + $sLen = $this->length(); + + if ($len <= $sLen) { + return clone $this; + } + + $padLen = $pad->length(); + $freeLen = $len - $sLen; + $len = $freeLen % $padLen; + + switch ($type) { + case \STR_PAD_RIGHT: + return $this->append(str_repeat($pad->string, intdiv($freeLen, $padLen)).($len ? $pad->slice(0, $len) : '')); + + case \STR_PAD_LEFT: + return $this->prepend(str_repeat($pad->string, intdiv($freeLen, $padLen)).($len ? $pad->slice(0, $len) : '')); + + case \STR_PAD_BOTH: + $freeLen /= 2; + + $rightLen = ceil($freeLen); + $len = $rightLen % $padLen; + $str = $this->append(str_repeat($pad->string, intdiv($rightLen, $padLen)).($len ? $pad->slice(0, $len) : '')); + + $leftLen = floor($freeLen); + $len = $leftLen % $padLen; + + return $str->prepend(str_repeat($pad->string, intdiv($leftLen, $padLen)).($len ? $pad->slice(0, $len) : '')); + + default: + throw new InvalidArgumentException('Invalid padding type.'); + } + } + + /** + * Based on https://github.com/jquast/wcwidth, a Python implementation of https://www.cl.cam.ac.uk/~mgk25/ucs/wcwidth.c. + */ + private function wcswidth(string $string): int + { + $width = 0; + + foreach (preg_split('//u', $string, -1, \PREG_SPLIT_NO_EMPTY) as $c) { + $codePoint = mb_ord($c, 'UTF-8'); + + if (0 === $codePoint // NULL + || 0x034F === $codePoint // COMBINING GRAPHEME JOINER + || (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 + ) { + continue; + } + + // Non printable characters + if (32 > $codePoint // C0 control characters + || (0x07F <= $codePoint && 0x0A0 > $codePoint) // C1 control characters and DEL + ) { + return -1; + } + + 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]) { + $lbound = 0; + while ($ubound >= $lbound) { + $mid = floor(($lbound + $ubound) / 2); + + if ($codePoint > self::$tableZero[$mid][1]) { + $lbound = $mid + 1; + } elseif ($codePoint < self::$tableZero[$mid][0]) { + $ubound = $mid - 1; + } else { + continue 2; + } + } + } + + self::$tableWide ??= require __DIR__.'/Resources/data/wcswidth_table_wide.php'; + + if ($codePoint >= self::$tableWide[0][0] && $codePoint <= self::$tableWide[$ubound = \count(self::$tableWide) - 1][1]) { + $lbound = 0; + while ($ubound >= $lbound) { + $mid = floor(($lbound + $ubound) / 2); + + if ($codePoint > self::$tableWide[$mid][1]) { + $lbound = $mid + 1; + } elseif ($codePoint < self::$tableWide[$mid][0]) { + $ubound = $mid - 1; + } else { + $width += 2; + + continue 2; + } + } + } + + ++$width; + } + + return $width; + } +} diff --git a/3rdparty/symfony/string/ByteString.php b/3rdparty/symfony/string/ByteString.php new file mode 100644 index 00000000..3ebe43c1 --- /dev/null +++ b/3rdparty/symfony/string/ByteString.php @@ -0,0 +1,485 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Component\String; + +use Symfony\Component\String\Exception\ExceptionInterface; +use Symfony\Component\String\Exception\InvalidArgumentException; +use Symfony\Component\String\Exception\RuntimeException; + +/** + * Represents a binary-safe string of bytes. + * + * @author Nicolas Grekas + * @author Hugo Hamon + * + * @throws ExceptionInterface + */ +class ByteString extends AbstractString +{ + private const ALPHABET_ALPHANUMERIC = '123456789ABCDEFGHJKLMNPQRSTUVWXYZabcdefghijkmnopqrstuvwxyz'; + + public function __construct(string $string = '') + { + $this->string = $string; + } + + /* + * The following method was derived from code of the Hack Standard Library (v4.40 - 2020-05-03) + * + * https://github.com/hhvm/hsl/blob/80a42c02f036f72a42f0415e80d6b847f4bf62d5/src/random/private.php#L16 + * + * Code subject to the MIT license (https://github.com/hhvm/hsl/blob/master/LICENSE). + * + * Copyright (c) 2004-2020, Facebook, Inc. (https://www.facebook.com/) + */ + + public static function fromRandom(int $length = 16, ?string $alphabet = null): self + { + if ($length <= 0) { + throw new InvalidArgumentException(sprintf('A strictly positive length is expected, "%d" given.', $length)); + } + + $alphabet ??= self::ALPHABET_ALPHANUMERIC; + $alphabetSize = \strlen($alphabet); + $bits = (int) ceil(log($alphabetSize, 2.0)); + if ($bits <= 0 || $bits > 56) { + throw new InvalidArgumentException('The length of the alphabet must in the [2^1, 2^56] range.'); + } + + $ret = ''; + while ($length > 0) { + $urandomLength = (int) ceil(2 * $length * $bits / 8.0); + $data = random_bytes($urandomLength); + $unpackedData = 0; + $unpackedBits = 0; + for ($i = 0; $i < $urandomLength && $length > 0; ++$i) { + // Unpack 8 bits + $unpackedData = ($unpackedData << 8) | \ord($data[$i]); + $unpackedBits += 8; + + // While we have enough bits to select a character from the alphabet, keep + // consuming the random data + for (; $unpackedBits >= $bits && $length > 0; $unpackedBits -= $bits) { + $index = ($unpackedData & ((1 << $bits) - 1)); + $unpackedData >>= $bits; + // Unfortunately, the alphabet size is not necessarily a power of two. + // Worst case, it is 2^k + 1, which means we need (k+1) bits and we + // have around a 50% chance of missing as k gets larger + if ($index < $alphabetSize) { + $ret .= $alphabet[$index]; + --$length; + } + } + } + } + + return new static($ret); + } + + public function bytesAt(int $offset): array + { + $str = $this->string[$offset] ?? ''; + + return '' === $str ? [] : [\ord($str)]; + } + + public function append(string ...$suffix): static + { + $str = clone $this; + $str->string .= 1 >= \count($suffix) ? ($suffix[0] ?? '') : implode('', $suffix); + + return $str; + } + + public function camel(): static + { + $str = clone $this; + + $parts = explode(' ', trim(ucwords(preg_replace('/[^a-zA-Z0-9\x7f-\xff]++/', ' ', $this->string)))); + $parts[0] = 1 !== \strlen($parts[0]) && ctype_upper($parts[0]) ? $parts[0] : lcfirst($parts[0]); + $str->string = implode('', $parts); + + return $str; + } + + public function chunk(int $length = 1): array + { + if (1 > $length) { + throw new InvalidArgumentException('The chunk length must be greater than zero.'); + } + + if ('' === $this->string) { + return []; + } + + $str = clone $this; + $chunks = []; + + foreach (str_split($this->string, $length) as $chunk) { + $str->string = $chunk; + $chunks[] = clone $str; + } + + return $chunks; + } + + public function endsWith(string|iterable|AbstractString $suffix): bool + { + if ($suffix instanceof AbstractString) { + $suffix = $suffix->string; + } elseif (!\is_string($suffix)) { + return parent::endsWith($suffix); + } + + return '' !== $suffix && \strlen($this->string) >= \strlen($suffix) && 0 === substr_compare($this->string, $suffix, -\strlen($suffix), null, $this->ignoreCase); + } + + public function equalsTo(string|iterable|AbstractString $string): bool + { + if ($string instanceof AbstractString) { + $string = $string->string; + } elseif (!\is_string($string)) { + return parent::equalsTo($string); + } + + if ('' !== $string && $this->ignoreCase) { + return 0 === strcasecmp($string, $this->string); + } + + return $string === $this->string; + } + + public function folded(): static + { + $str = clone $this; + $str->string = strtolower($str->string); + + return $str; + } + + public function indexOf(string|iterable|AbstractString $needle, int $offset = 0): ?int + { + if ($needle instanceof AbstractString) { + $needle = $needle->string; + } elseif (!\is_string($needle)) { + return parent::indexOf($needle, $offset); + } + + if ('' === $needle) { + return null; + } + + $i = $this->ignoreCase ? stripos($this->string, $needle, $offset) : strpos($this->string, $needle, $offset); + + return false === $i ? null : $i; + } + + public function indexOfLast(string|iterable|AbstractString $needle, int $offset = 0): ?int + { + if ($needle instanceof AbstractString) { + $needle = $needle->string; + } elseif (!\is_string($needle)) { + return parent::indexOfLast($needle, $offset); + } + + if ('' === $needle) { + return null; + } + + $i = $this->ignoreCase ? strripos($this->string, $needle, $offset) : strrpos($this->string, $needle, $offset); + + return false === $i ? null : $i; + } + + public function isUtf8(): bool + { + return '' === $this->string || preg_match('//u', $this->string); + } + + public function join(array $strings, ?string $lastGlue = null): static + { + $str = clone $this; + + $tail = null !== $lastGlue && 1 < \count($strings) ? $lastGlue.array_pop($strings) : ''; + $str->string = implode($this->string, $strings).$tail; + + return $str; + } + + public function length(): int + { + return \strlen($this->string); + } + + public function lower(): static + { + $str = clone $this; + $str->string = strtolower($str->string); + + return $str; + } + + public function match(string $regexp, int $flags = 0, int $offset = 0): array + { + $match = ((\PREG_PATTERN_ORDER | \PREG_SET_ORDER) & $flags) ? 'preg_match_all' : 'preg_match'; + + if ($this->ignoreCase) { + $regexp .= 'i'; + } + + set_error_handler(static fn ($t, $m) => throw new InvalidArgumentException($m)); + + try { + if (false === $match($regexp, $this->string, $matches, $flags | \PREG_UNMATCHED_AS_NULL, $offset)) { + throw new RuntimeException('Matching failed with error: '.preg_last_error_msg()); + } + } finally { + restore_error_handler(); + } + + return $matches; + } + + public function padBoth(int $length, string $padStr = ' '): static + { + $str = clone $this; + $str->string = str_pad($this->string, $length, $padStr, \STR_PAD_BOTH); + + return $str; + } + + public function padEnd(int $length, string $padStr = ' '): static + { + $str = clone $this; + $str->string = str_pad($this->string, $length, $padStr, \STR_PAD_RIGHT); + + return $str; + } + + public function padStart(int $length, string $padStr = ' '): static + { + $str = clone $this; + $str->string = str_pad($this->string, $length, $padStr, \STR_PAD_LEFT); + + return $str; + } + + public function prepend(string ...$prefix): static + { + $str = clone $this; + $str->string = (1 >= \count($prefix) ? ($prefix[0] ?? '') : implode('', $prefix)).$str->string; + + return $str; + } + + public function replace(string $from, string $to): static + { + $str = clone $this; + + if ('' !== $from) { + $str->string = $this->ignoreCase ? str_ireplace($from, $to, $this->string) : str_replace($from, $to, $this->string); + } + + return $str; + } + + public function replaceMatches(string $fromRegexp, string|callable $to): static + { + if ($this->ignoreCase) { + $fromRegexp .= 'i'; + } + + $replace = \is_array($to) || $to instanceof \Closure ? 'preg_replace_callback' : 'preg_replace'; + + set_error_handler(static fn ($t, $m) => throw new InvalidArgumentException($m)); + + try { + if (null === $string = $replace($fromRegexp, $to, $this->string)) { + $lastError = preg_last_error(); + + foreach (get_defined_constants(true)['pcre'] as $k => $v) { + if ($lastError === $v && str_ends_with($k, '_ERROR')) { + throw new RuntimeException('Matching failed with '.$k.'.'); + } + } + + throw new RuntimeException('Matching failed with unknown error code.'); + } + } finally { + restore_error_handler(); + } + + $str = clone $this; + $str->string = $string; + + return $str; + } + + public function reverse(): static + { + $str = clone $this; + $str->string = strrev($str->string); + + return $str; + } + + public function slice(int $start = 0, ?int $length = null): static + { + $str = clone $this; + $str->string = (string) substr($this->string, $start, $length ?? \PHP_INT_MAX); + + return $str; + } + + public function snake(): static + { + $str = $this->camel(); + $str->string = strtolower(preg_replace(['/([A-Z]+)([A-Z][a-z])/', '/([a-z\d])([A-Z])/'], '\1_\2', $str->string)); + + return $str; + } + + public function splice(string $replacement, int $start = 0, ?int $length = null): static + { + $str = clone $this; + $str->string = substr_replace($this->string, $replacement, $start, $length ?? \PHP_INT_MAX); + + return $str; + } + + public function split(string $delimiter, ?int $limit = null, ?int $flags = null): array + { + if (1 > $limit ??= \PHP_INT_MAX) { + throw new InvalidArgumentException('Split limit must be a positive integer.'); + } + + if ('' === $delimiter) { + throw new InvalidArgumentException('Split delimiter is empty.'); + } + + if (null !== $flags) { + return parent::split($delimiter, $limit, $flags); + } + + $str = clone $this; + $chunks = $this->ignoreCase + ? preg_split('{'.preg_quote($delimiter).'}iD', $this->string, $limit) + : explode($delimiter, $this->string, $limit); + + foreach ($chunks as &$chunk) { + $str->string = $chunk; + $chunk = clone $str; + } + + return $chunks; + } + + public function startsWith(string|iterable|AbstractString $prefix): bool + { + if ($prefix instanceof AbstractString) { + $prefix = $prefix->string; + } elseif (!\is_string($prefix)) { + return parent::startsWith($prefix); + } + + return '' !== $prefix && 0 === ($this->ignoreCase ? strncasecmp($this->string, $prefix, \strlen($prefix)) : strncmp($this->string, $prefix, \strlen($prefix))); + } + + public function title(bool $allWords = false): static + { + $str = clone $this; + $str->string = $allWords ? ucwords($str->string) : ucfirst($str->string); + + return $str; + } + + public function toUnicodeString(?string $fromEncoding = null): UnicodeString + { + return new UnicodeString($this->toCodePointString($fromEncoding)->string); + } + + public function toCodePointString(?string $fromEncoding = null): CodePointString + { + $u = new CodePointString(); + + if (\in_array($fromEncoding, [null, 'utf8', 'utf-8', 'UTF8', 'UTF-8'], true) && preg_match('//u', $this->string)) { + $u->string = $this->string; + + return $u; + } + + set_error_handler(static fn ($t, $m) => throw new InvalidArgumentException($m)); + + try { + try { + $validEncoding = false !== mb_detect_encoding($this->string, $fromEncoding ?? 'Windows-1252', true); + } catch (InvalidArgumentException $e) { + if (!\function_exists('iconv')) { + throw $e; + } + + $u->string = iconv($fromEncoding ?? 'Windows-1252', 'UTF-8', $this->string); + + return $u; + } + } finally { + restore_error_handler(); + } + + if (!$validEncoding) { + throw new InvalidArgumentException(sprintf('Invalid "%s" string.', $fromEncoding ?? 'Windows-1252')); + } + + $u->string = mb_convert_encoding($this->string, 'UTF-8', $fromEncoding ?? 'Windows-1252'); + + return $u; + } + + public function trim(string $chars = " \t\n\r\0\x0B\x0C"): static + { + $str = clone $this; + $str->string = trim($str->string, $chars); + + return $str; + } + + public function trimEnd(string $chars = " \t\n\r\0\x0B\x0C"): static + { + $str = clone $this; + $str->string = rtrim($str->string, $chars); + + return $str; + } + + public function trimStart(string $chars = " \t\n\r\0\x0B\x0C"): static + { + $str = clone $this; + $str->string = ltrim($str->string, $chars); + + return $str; + } + + public function upper(): static + { + $str = clone $this; + $str->string = strtoupper($str->string); + + return $str; + } + + public function width(bool $ignoreAnsiDecoration = true): int + { + $string = preg_match('//u', $this->string) ? $this->string : preg_replace('/[\x80-\xFF]/', '?', $this->string); + + return (new CodePointString($string))->width($ignoreAnsiDecoration); + } +} diff --git a/3rdparty/symfony/string/CodePointString.php b/3rdparty/symfony/string/CodePointString.php new file mode 100644 index 00000000..337bfc12 --- /dev/null +++ b/3rdparty/symfony/string/CodePointString.php @@ -0,0 +1,260 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Component\String; + +use Symfony\Component\String\Exception\ExceptionInterface; +use Symfony\Component\String\Exception\InvalidArgumentException; + +/** + * Represents a string of Unicode code points encoded as UTF-8. + * + * @author Nicolas Grekas + * @author Hugo Hamon + * + * @throws ExceptionInterface + */ +class CodePointString extends AbstractUnicodeString +{ + public function __construct(string $string = '') + { + if ('' !== $string && !preg_match('//u', $string)) { + throw new InvalidArgumentException('Invalid UTF-8 string.'); + } + + $this->string = $string; + } + + public function append(string ...$suffix): static + { + $str = clone $this; + $str->string .= 1 >= \count($suffix) ? ($suffix[0] ?? '') : implode('', $suffix); + + if (!preg_match('//u', $str->string)) { + throw new InvalidArgumentException('Invalid UTF-8 string.'); + } + + return $str; + } + + public function chunk(int $length = 1): array + { + if (1 > $length) { + throw new InvalidArgumentException('The chunk length must be greater than zero.'); + } + + if ('' === $this->string) { + return []; + } + + $rx = '/('; + while (65535 < $length) { + $rx .= '.{65535}'; + $length -= 65535; + } + $rx .= '.{'.$length.'})/us'; + + $str = clone $this; + $chunks = []; + + foreach (preg_split($rx, $this->string, -1, \PREG_SPLIT_DELIM_CAPTURE | \PREG_SPLIT_NO_EMPTY) as $chunk) { + $str->string = $chunk; + $chunks[] = clone $str; + } + + return $chunks; + } + + public function codePointsAt(int $offset): array + { + $str = $offset ? $this->slice($offset, 1) : $this; + + return '' === $str->string ? [] : [mb_ord($str->string, 'UTF-8')]; + } + + public function endsWith(string|iterable|AbstractString $suffix): bool + { + if ($suffix instanceof AbstractString) { + $suffix = $suffix->string; + } elseif (!\is_string($suffix)) { + return parent::endsWith($suffix); + } + + if ('' === $suffix || !preg_match('//u', $suffix)) { + return false; + } + + if ($this->ignoreCase) { + return preg_match('{'.preg_quote($suffix).'$}iuD', $this->string); + } + + return \strlen($this->string) >= \strlen($suffix) && 0 === substr_compare($this->string, $suffix, -\strlen($suffix)); + } + + public function equalsTo(string|iterable|AbstractString $string): bool + { + if ($string instanceof AbstractString) { + $string = $string->string; + } elseif (!\is_string($string)) { + return parent::equalsTo($string); + } + + if ('' !== $string && $this->ignoreCase) { + return \strlen($string) === \strlen($this->string) && 0 === mb_stripos($this->string, $string, 0, 'UTF-8'); + } + + return $string === $this->string; + } + + public function indexOf(string|iterable|AbstractString $needle, int $offset = 0): ?int + { + if ($needle instanceof AbstractString) { + $needle = $needle->string; + } elseif (!\is_string($needle)) { + return parent::indexOf($needle, $offset); + } + + if ('' === $needle) { + return null; + } + + $i = $this->ignoreCase ? mb_stripos($this->string, $needle, $offset, 'UTF-8') : mb_strpos($this->string, $needle, $offset, 'UTF-8'); + + return false === $i ? null : $i; + } + + public function indexOfLast(string|iterable|AbstractString $needle, int $offset = 0): ?int + { + if ($needle instanceof AbstractString) { + $needle = $needle->string; + } elseif (!\is_string($needle)) { + return parent::indexOfLast($needle, $offset); + } + + if ('' === $needle) { + return null; + } + + $i = $this->ignoreCase ? mb_strripos($this->string, $needle, $offset, 'UTF-8') : mb_strrpos($this->string, $needle, $offset, 'UTF-8'); + + return false === $i ? null : $i; + } + + public function length(): int + { + return mb_strlen($this->string, 'UTF-8'); + } + + public function prepend(string ...$prefix): static + { + $str = clone $this; + $str->string = (1 >= \count($prefix) ? ($prefix[0] ?? '') : implode('', $prefix)).$this->string; + + if (!preg_match('//u', $str->string)) { + throw new InvalidArgumentException('Invalid UTF-8 string.'); + } + + return $str; + } + + public function replace(string $from, string $to): static + { + $str = clone $this; + + if ('' === $from || !preg_match('//u', $from)) { + return $str; + } + + if ('' !== $to && !preg_match('//u', $to)) { + throw new InvalidArgumentException('Invalid UTF-8 string.'); + } + + if ($this->ignoreCase) { + $str->string = implode($to, preg_split('{'.preg_quote($from).'}iuD', $this->string)); + } else { + $str->string = str_replace($from, $to, $this->string); + } + + return $str; + } + + public function slice(int $start = 0, ?int $length = null): static + { + $str = clone $this; + $str->string = mb_substr($this->string, $start, $length, 'UTF-8'); + + return $str; + } + + public function splice(string $replacement, int $start = 0, ?int $length = null): static + { + if (!preg_match('//u', $replacement)) { + throw new InvalidArgumentException('Invalid UTF-8 string.'); + } + + $str = clone $this; + $start = $start ? \strlen(mb_substr($this->string, 0, $start, 'UTF-8')) : 0; + $length = $length ? \strlen(mb_substr($this->string, $start, $length, 'UTF-8')) : $length; + $str->string = substr_replace($this->string, $replacement, $start, $length ?? \PHP_INT_MAX); + + return $str; + } + + public function split(string $delimiter, ?int $limit = null, ?int $flags = null): array + { + if (1 > $limit ??= \PHP_INT_MAX) { + throw new InvalidArgumentException('Split limit must be a positive integer.'); + } + + if ('' === $delimiter) { + throw new InvalidArgumentException('Split delimiter is empty.'); + } + + if (null !== $flags) { + return parent::split($delimiter.'u', $limit, $flags); + } + + if (!preg_match('//u', $delimiter)) { + throw new InvalidArgumentException('Split delimiter is not a valid UTF-8 string.'); + } + + $str = clone $this; + $chunks = $this->ignoreCase + ? preg_split('{'.preg_quote($delimiter).'}iuD', $this->string, $limit) + : explode($delimiter, $this->string, $limit); + + foreach ($chunks as &$chunk) { + $str->string = $chunk; + $chunk = clone $str; + } + + return $chunks; + } + + public function startsWith(string|iterable|AbstractString $prefix): bool + { + if ($prefix instanceof AbstractString) { + $prefix = $prefix->string; + } elseif (!\is_string($prefix)) { + return parent::startsWith($prefix); + } + + if ('' === $prefix || !preg_match('//u', $prefix)) { + return false; + } + + if ($this->ignoreCase) { + return 0 === mb_stripos($this->string, $prefix, 0, 'UTF-8'); + } + + return 0 === strncmp($this->string, $prefix, \strlen($prefix)); + } +} diff --git a/3rdparty/symfony/string/Exception/ExceptionInterface.php b/3rdparty/symfony/string/Exception/ExceptionInterface.php new file mode 100644 index 00000000..36197865 --- /dev/null +++ b/3rdparty/symfony/string/Exception/ExceptionInterface.php @@ -0,0 +1,16 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Component\String\Exception; + +interface ExceptionInterface extends \Throwable +{ +} diff --git a/3rdparty/symfony/string/Exception/InvalidArgumentException.php b/3rdparty/symfony/string/Exception/InvalidArgumentException.php new file mode 100644 index 00000000..6aa586bc --- /dev/null +++ b/3rdparty/symfony/string/Exception/InvalidArgumentException.php @@ -0,0 +1,16 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Component\String\Exception; + +class InvalidArgumentException extends \InvalidArgumentException implements ExceptionInterface +{ +} diff --git a/3rdparty/symfony/string/Exception/RuntimeException.php b/3rdparty/symfony/string/Exception/RuntimeException.php new file mode 100644 index 00000000..77cb091f --- /dev/null +++ b/3rdparty/symfony/string/Exception/RuntimeException.php @@ -0,0 +1,16 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Component\String\Exception; + +class RuntimeException extends \RuntimeException implements ExceptionInterface +{ +} diff --git a/3rdparty/symfony/string/Inflector/EnglishInflector.php b/3rdparty/symfony/string/Inflector/EnglishInflector.php new file mode 100644 index 00000000..a5be28d6 --- /dev/null +++ b/3rdparty/symfony/string/Inflector/EnglishInflector.php @@ -0,0 +1,586 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Component\String\Inflector; + +final class EnglishInflector implements InflectorInterface +{ + /** + * Map English plural to singular suffixes. + * + * @see http://english-zone.com/spelling/plurals.html + */ + private const PLURAL_MAP = [ + // First entry: plural suffix, reversed + // Second entry: length of plural suffix + // Third entry: Whether the suffix may succeed a vowel + // Fourth entry: Whether the suffix may succeed a consonant + // Fifth entry: singular suffix, normal + + // bacteria (bacterium) + ['airetcab', 8, true, true, 'bacterium'], + + // corpora (corpus) + ['aroproc', 7, true, true, 'corpus'], + + // criteria (criterion) + ['airetirc', 8, true, true, 'criterion'], + + // curricula (curriculum) + ['alucirruc', 9, true, true, 'curriculum'], + + // quora (quorum) + ['arouq', 5, true, true, 'quorum'], + + // genera (genus) + ['areneg', 6, true, true, 'genus'], + + // media (medium) + ['aidem', 5, true, true, 'medium'], + + // memoranda (memorandum) + ['adnaromem', 9, true, true, 'memorandum'], + + // phenomena (phenomenon) + ['anemonehp', 9, true, true, 'phenomenon'], + + // strata (stratum) + ['atarts', 6, true, true, 'stratum'], + + // nebulae (nebula) + ['ea', 2, true, true, 'a'], + + // services (service) + ['secivres', 8, true, true, 'service'], + + // mice (mouse), lice (louse) + ['eci', 3, false, true, 'ouse'], + + // geese (goose) + ['esee', 4, false, true, 'oose'], + + // fungi (fungus), alumni (alumnus), syllabi (syllabus), radii (radius) + ['i', 1, true, true, 'us'], + + // men (man), women (woman) + ['nem', 3, true, true, 'man'], + + // children (child) + ['nerdlihc', 8, true, true, 'child'], + + // oxen (ox) + ['nexo', 4, false, false, 'ox'], + + // indices (index), appendices (appendix), prices (price) + ['seci', 4, false, true, ['ex', 'ix', 'ice']], + + // codes (code) + ['sedoc', 5, false, true, 'code'], + + // selfies (selfie) + ['seifles', 7, true, true, 'selfie'], + + // zombies (zombie) + ['seibmoz', 7, true, true, 'zombie'], + + // movies (movie) + ['seivom', 6, true, true, 'movie'], + + // names (name) + ['seman', 5, true, false, 'name'], + + // conspectuses (conspectus), prospectuses (prospectus) + ['sesutcep', 8, true, true, 'pectus'], + + // feet (foot) + ['teef', 4, true, true, 'foot'], + + // geese (goose) + ['eseeg', 5, true, true, 'goose'], + + // teeth (tooth) + ['hteet', 5, true, true, 'tooth'], + + // news (news) + ['swen', 4, true, true, 'news'], + + // series (series) + ['seires', 6, true, true, 'series'], + + // babies (baby) + ['sei', 3, false, true, 'y'], + + // accesses (access), addresses (address), kisses (kiss) + ['sess', 4, true, false, 'ss'], + + // statuses (status) + ['sesutats', 8, true, true, 'status'], + + // article (articles), ancle (ancles) + ['sel', 3, true, true, 'le'], + + // analyses (analysis), ellipses (ellipsis), fungi (fungus), + // neuroses (neurosis), theses (thesis), emphases (emphasis), + // oases (oasis), crises (crisis), houses (house), bases (base), + // atlases (atlas) + ['ses', 3, true, true, ['s', 'se', 'sis']], + + // objectives (objective), alternative (alternatives) + ['sevit', 5, true, true, 'tive'], + + // drives (drive) + ['sevird', 6, false, true, 'drive'], + + // lives (life), wives (wife) + ['sevi', 4, false, true, 'ife'], + + // moves (move) + ['sevom', 5, true, true, 'move'], + + // hooves (hoof), dwarves (dwarf), elves (elf), leaves (leaf), caves (cave), staves (staff) + ['sev', 3, true, true, ['f', 've', 'ff']], + + // axes (axis), axes (ax), axes (axe) + ['sexa', 4, false, false, ['ax', 'axe', 'axis']], + + // indexes (index), matrixes (matrix) + ['sex', 3, true, false, 'x'], + + // quizzes (quiz) + ['sezz', 4, true, false, 'z'], + + // bureaus (bureau) + ['suae', 4, false, true, 'eau'], + + // fees (fee), trees (tree), employees (employee) + ['see', 3, true, true, 'ee'], + + // edges (edge) + ['segd', 4, true, true, 'dge'], + + // roses (rose), garages (garage), cassettes (cassette), + // waltzes (waltz), heroes (hero), bushes (bush), arches (arch), + // shoes (shoe) + ['se', 2, true, true, ['', 'e']], + + // status (status) + ['sutats', 6, true, true, 'status'], + + // tags (tag) + ['s', 1, true, true, ''], + + // chateaux (chateau) + ['xuae', 4, false, true, 'eau'], + + // people (person) + ['elpoep', 6, true, true, 'person'], + ]; + + /** + * Map English singular to plural suffixes. + * + * @see http://english-zone.com/spelling/plurals.html + */ + private const SINGULAR_MAP = [ + // First entry: singular suffix, reversed + // Second entry: length of singular suffix + // Third entry: Whether the suffix may succeed a vowel + // Fourth entry: Whether the suffix may succeed a consonant + // Fifth entry: plural suffix, normal + + // axes (axis) + ['sixa', 4, false, false, 'axes'], + + // criterion (criteria) + ['airetirc', 8, false, false, 'criterion'], + + // nebulae (nebula) + ['aluben', 6, false, false, 'nebulae'], + + // children (child) + ['dlihc', 5, true, true, 'children'], + + // prices (price) + ['eci', 3, false, true, 'ices'], + + // services (service) + ['ecivres', 7, true, true, 'services'], + + // lives (life), wives (wife) + ['efi', 3, false, true, 'ives'], + + // selfies (selfie) + ['eifles', 6, true, true, 'selfies'], + + // movies (movie) + ['eivom', 5, true, true, 'movies'], + + // lice (louse) + ['esuol', 5, false, true, 'lice'], + + // mice (mouse) + ['esuom', 5, false, true, 'mice'], + + // geese (goose) + ['esoo', 4, false, true, 'eese'], + + // houses (house), bases (base) + ['es', 2, true, true, 'ses'], + + // geese (goose) + ['esoog', 5, true, true, 'geese'], + + // caves (cave) + ['ev', 2, true, true, 'ves'], + + // drives (drive) + ['evird', 5, false, true, 'drives'], + + // objectives (objective), alternative (alternatives) + ['evit', 4, true, true, 'tives'], + + // moves (move) + ['evom', 4, true, true, 'moves'], + + // staves (staff) + ['ffats', 5, true, true, 'staves'], + + // hooves (hoof), dwarves (dwarf), elves (elf), leaves (leaf) + ['ff', 2, true, true, 'ffs'], + + // hooves (hoof), dwarves (dwarf), elves (elf), leaves (leaf) + ['f', 1, true, true, ['fs', 'ves']], + + // arches (arch) + ['hc', 2, true, true, 'ches'], + + // bushes (bush) + ['hs', 2, true, true, 'shes'], + + // teeth (tooth) + ['htoot', 5, true, true, 'teeth'], + + // albums (album) + ['mubla', 5, true, true, 'albums'], + + // quorums (quorum) + ['murouq', 6, true, true, ['quora', 'quorums']], + + // bacteria (bacterium), curricula (curriculum), media (medium), memoranda (memorandum), phenomena (phenomenon), strata (stratum) + ['mu', 2, true, true, 'a'], + + // men (man), women (woman) + ['nam', 3, true, true, 'men'], + + // people (person) + ['nosrep', 6, true, true, ['persons', 'people']], + + // criteria (criterion) + ['noiretirc', 9, true, true, 'criteria'], + + // phenomena (phenomenon) + ['nonemonehp', 10, true, true, 'phenomena'], + + // echoes (echo) + ['ohce', 4, true, true, 'echoes'], + + // heroes (hero) + ['oreh', 4, true, true, 'heroes'], + + // atlases (atlas) + ['salta', 5, true, true, 'atlases'], + + // aliases (alias) + ['saila', 5, true, true, 'aliases'], + + // irises (iris) + ['siri', 4, true, true, 'irises'], + + // analyses (analysis), ellipses (ellipsis), neuroses (neurosis) + // theses (thesis), emphases (emphasis), oases (oasis), + // crises (crisis) + ['sis', 3, true, true, 'ses'], + + // accesses (access), addresses (address), kisses (kiss) + ['ss', 2, true, false, 'sses'], + + // syllabi (syllabus) + ['suballys', 8, true, true, 'syllabi'], + + // buses (bus) + ['sub', 3, true, true, 'buses'], + + // circuses (circus) + ['suc', 3, true, true, 'cuses'], + + // hippocampi (hippocampus) + ['supmacoppih', 11, false, false, 'hippocampi'], + + // campuses (campus) + ['sup', 3, true, true, 'puses'], + + // status (status) + ['sutats', 6, true, true, ['status', 'statuses']], + + // conspectuses (conspectus), prospectuses (prospectus) + ['sutcep', 6, true, true, 'pectuses'], + + // fungi (fungus), alumni (alumnus), syllabi (syllabus), radii (radius) + ['su', 2, true, true, 'i'], + + // news (news) + ['swen', 4, true, true, 'news'], + + // feet (foot) + ['toof', 4, true, true, 'feet'], + + // chateaux (chateau), bureaus (bureau) + ['uae', 3, false, true, ['eaus', 'eaux']], + + // oxen (ox) + ['xo', 2, false, false, 'oxen'], + + // hoaxes (hoax) + ['xaoh', 4, true, false, 'hoaxes'], + + // indices (index) + ['xedni', 5, false, true, ['indicies', 'indexes']], + + // fax (faxes, faxxes) + ['xaf', 3, true, true, ['faxes', 'faxxes']], + + // boxes (box) + ['xo', 2, false, true, 'oxes'], + + // indexes (index), matrixes (matrix), appendices (appendix) + ['x', 1, true, false, ['ces', 'xes']], + + // babies (baby) + ['y', 1, false, true, 'ies'], + + // quizzes (quiz) + ['ziuq', 4, true, false, 'quizzes'], + + // waltzes (waltz) + ['z', 1, true, true, 'zes'], + ]; + + /** + * A list of words which should not be inflected, reversed. + */ + private const UNINFLECTED = [ + '', + + // data + 'atad', + + // deer + 'reed', + + // equipment + 'tnempiuqe', + + // feedback + 'kcabdeef', + + // fish + 'hsif', + + // health + 'htlaeh', + + // history + 'yrotsih', + + // info + 'ofni', + + // information + 'noitamrofni', + + // money + 'yenom', + + // moose + 'esoom', + + // series + 'seires', + + // sheep + 'peehs', + + // species + 'seiceps', + + // traffic + 'ciffart', + + // aircraft + 'tfarcria', + + // hardware + 'erawdrah', + ]; + + public function singularize(string $plural): array + { + $pluralRev = strrev($plural); + $lowerPluralRev = strtolower($pluralRev); + $pluralLength = \strlen($lowerPluralRev); + + // Check if the word is one which is not inflected, return early if so + if (\in_array($lowerPluralRev, self::UNINFLECTED, true)) { + return [$plural]; + } + + // The outer loop iterates over the entries of the plural table + // The inner loop $j iterates over the characters of the plural suffix + // in the plural table to compare them with the characters of the actual + // given plural suffix + foreach (self::PLURAL_MAP as $map) { + $suffix = $map[0]; + $suffixLength = $map[1]; + $j = 0; + + // Compare characters in the plural table and of the suffix of the + // given plural one by one + while ($suffix[$j] === $lowerPluralRev[$j]) { + // Let $j point to the next character + ++$j; + + // Successfully compared the last character + // Add an entry with the singular suffix to the singular array + if ($j === $suffixLength) { + // Is there any character preceding the suffix in the plural string? + if ($j < $pluralLength) { + $nextIsVowel = str_contains('aeiou', $lowerPluralRev[$j]); + + if (!$map[2] && $nextIsVowel) { + // suffix may not succeed a vowel but next char is one + break; + } + + if (!$map[3] && !$nextIsVowel) { + // suffix may not succeed a consonant but next char is one + break; + } + } + + $newBase = substr($plural, 0, $pluralLength - $suffixLength); + $newSuffix = $map[4]; + + // Check whether the first character in the plural suffix + // is uppercased. If yes, uppercase the first character in + // the singular suffix too + $firstUpper = ctype_upper($pluralRev[$j - 1]); + + if (\is_array($newSuffix)) { + $singulars = []; + + foreach ($newSuffix as $newSuffixEntry) { + $singulars[] = $newBase.($firstUpper ? ucfirst($newSuffixEntry) : $newSuffixEntry); + } + + return $singulars; + } + + return [$newBase.($firstUpper ? ucfirst($newSuffix) : $newSuffix)]; + } + + // Suffix is longer than word + if ($j === $pluralLength) { + break; + } + } + } + + // Assume that plural and singular is identical + return [$plural]; + } + + public function pluralize(string $singular): array + { + $singularRev = strrev($singular); + $lowerSingularRev = strtolower($singularRev); + $singularLength = \strlen($lowerSingularRev); + + // Check if the word is one which is not inflected, return early if so + if (\in_array($lowerSingularRev, self::UNINFLECTED, true)) { + return [$singular]; + } + + // The outer loop iterates over the entries of the singular table + // The inner loop $j iterates over the characters of the singular suffix + // in the singular table to compare them with the characters of the actual + // given singular suffix + foreach (self::SINGULAR_MAP as $map) { + $suffix = $map[0]; + $suffixLength = $map[1]; + $j = 0; + + // Compare characters in the singular table and of the suffix of the + // given plural one by one + + while ($suffix[$j] === $lowerSingularRev[$j]) { + // Let $j point to the next character + ++$j; + + // Successfully compared the last character + // Add an entry with the plural suffix to the plural array + if ($j === $suffixLength) { + // Is there any character preceding the suffix in the plural string? + if ($j < $singularLength) { + $nextIsVowel = str_contains('aeiou', $lowerSingularRev[$j]); + + if (!$map[2] && $nextIsVowel) { + // suffix may not succeed a vowel but next char is one + break; + } + + if (!$map[3] && !$nextIsVowel) { + // suffix may not succeed a consonant but next char is one + break; + } + } + + $newBase = substr($singular, 0, $singularLength - $suffixLength); + $newSuffix = $map[4]; + + // Check whether the first character in the singular suffix + // is uppercased. If yes, uppercase the first character in + // the singular suffix too + $firstUpper = ctype_upper($singularRev[$j - 1]); + + if (\is_array($newSuffix)) { + $plurals = []; + + foreach ($newSuffix as $newSuffixEntry) { + $plurals[] = $newBase.($firstUpper ? ucfirst($newSuffixEntry) : $newSuffixEntry); + } + + return $plurals; + } + + return [$newBase.($firstUpper ? ucfirst($newSuffix) : $newSuffix)]; + } + + // Suffix is longer than word + if ($j === $singularLength) { + break; + } + } + } + + // Assume that plural is singular with a trailing `s` + return [$singular.'s']; + } +} diff --git a/3rdparty/symfony/string/Inflector/FrenchInflector.php b/3rdparty/symfony/string/Inflector/FrenchInflector.php new file mode 100644 index 00000000..955abbf4 --- /dev/null +++ b/3rdparty/symfony/string/Inflector/FrenchInflector.php @@ -0,0 +1,151 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Component\String\Inflector; + +/** + * French inflector. + * + * This class does only inflect nouns; not adjectives nor composed words like "soixante-dix". + */ +final class FrenchInflector implements InflectorInterface +{ + /** + * A list of all rules for pluralise. + * + * @see https://la-conjugaison.nouvelobs.com/regles/grammaire/le-pluriel-des-noms-121.php + */ + private const PLURALIZE_REGEXP = [ + // First entry: regexp + // Second entry: replacement + + // Words finishing with "s", "x" or "z" are invariables + // Les mots finissant par "s", "x" ou "z" sont invariables + ['/(s|x|z)$/i', '\1'], + + // Words finishing with "eau" are pluralized with a "x" + // Les mots finissant par "eau" prennent tous un "x" au pluriel + ['/(eau)$/i', '\1x'], + + // Words finishing with "au" are pluralized with a "x" excepted "landau" + // Les mots finissant par "au" prennent un "x" au pluriel sauf "landau" + ['/^(landau)$/i', '\1s'], + ['/(au)$/i', '\1x'], + + // Words finishing with "eu" are pluralized with a "x" excepted "pneu", "bleu", "émeu" + // Les mots finissant en "eu" prennent un "x" au pluriel sauf "pneu", "bleu", "émeu" + ['/^(pneu|bleu|émeu)$/i', '\1s'], + ['/(eu)$/i', '\1x'], + + // Words finishing with "al" are pluralized with a "aux" excepted + // Les mots finissant en "al" se terminent en "aux" sauf + ['/^(bal|carnaval|caracal|chacal|choral|corral|étal|festival|récital|val)$/i', '\1s'], + ['/al$/i', '\1aux'], + + // Aspirail, bail, corail, émail, fermail, soupirail, travail, vantail et vitrail font leur pluriel en -aux + ['/^(aspir|b|cor|ém|ferm|soupir|trav|vant|vitr)ail$/i', '\1aux'], + + // Bijou, caillou, chou, genou, hibou, joujou et pou qui prennent un x au pluriel + ['/^(bij|caill|ch|gen|hib|jouj|p)ou$/i', '\1oux'], + + // Invariable words + ['/^(cinquante|soixante|mille)$/i', '\1'], + + // French titles + ['/^(mon|ma)(sieur|dame|demoiselle|seigneur)$/', 'mes\2s'], + ['/^(Mon|Ma)(sieur|dame|demoiselle|seigneur)$/', 'Mes\2s'], + ]; + + /** + * A list of all rules for singularize. + */ + private const SINGULARIZE_REGEXP = [ + // First entry: regexp + // Second entry: replacement + + // Aspirail, bail, corail, émail, fermail, soupirail, travail, vantail et vitrail font leur pluriel en -aux + ['/((aspir|b|cor|ém|ferm|soupir|trav|vant|vitr))aux$/i', '\1ail'], + + // Words finishing with "eau" are pluralized with a "x" + // Les mots finissant par "eau" prennent tous un "x" au pluriel + ['/(eau)x$/i', '\1'], + + // Words finishing with "al" are pluralized with a "aux" expected + // Les mots finissant en "al" se terminent en "aux" sauf + ['/(amir|anim|arsen|boc|can|capit|capor|chev|crist|génér|hopit|hôpit|idé|journ|littor|loc|m|mét|minér|princip|radic|termin)aux$/i', '\1al'], + + // Words finishing with "au" are pluralized with a "x" excepted "landau" + // Les mots finissant par "au" prennent un "x" au pluriel sauf "landau" + ['/(au)x$/i', '\1'], + + // Words finishing with "eu" are pluralized with a "x" excepted "pneu", "bleu", "émeu" + // Les mots finissant en "eu" prennent un "x" au pluriel sauf "pneu", "bleu", "émeu" + ['/(eu)x$/i', '\1'], + + // Words finishing with "ou" are pluralized with a "s" excepted bijou, caillou, chou, genou, hibou, joujou, pou + // Les mots finissant par "ou" prennent un "s" sauf bijou, caillou, chou, genou, hibou, joujou, pou + ['/(bij|caill|ch|gen|hib|jouj|p)oux$/i', '\1ou'], + + // French titles + ['/^mes(dame|demoiselle)s$/', 'ma\1'], + ['/^Mes(dame|demoiselle)s$/', 'Ma\1'], + ['/^mes(sieur|seigneur)s$/', 'mon\1'], + ['/^Mes(sieur|seigneur)s$/', 'Mon\1'], + + // Default rule + ['/s$/i', ''], + ]; + + /** + * A list of words which should not be inflected. + * This list is only used by singularize. + */ + private const UNINFLECTED = '/^(abcès|accès|abus|albatros|anchois|anglais|autobus|bois|brebis|carquois|cas|chas|colis|concours|corps|cours|cyprès|décès|devis|discours|dos|embarras|engrais|entrelacs|excès|fils|fois|gâchis|gars|glas|héros|intrus|jars|jus|kermès|lacis|legs|lilas|marais|mars|matelas|mépris|mets|mois|mors|obus|os|palais|paradis|parcours|pardessus|pays|plusieurs|poids|pois|pouls|printemps|processus|progrès|puits|pus|rabais|radis|recors|recours|refus|relais|remords|remous|rictus|rhinocéros|repas|rubis|sans|sas|secours|sens|souris|succès|talus|tapis|tas|taudis|temps|tiers|univers|velours|verglas|vernis|virus)$/i'; + + public function singularize(string $plural): array + { + if ($this->isInflectedWord($plural)) { + return [$plural]; + } + + foreach (self::SINGULARIZE_REGEXP as $rule) { + [$regexp, $replace] = $rule; + + if (1 === preg_match($regexp, $plural)) { + return [preg_replace($regexp, $replace, $plural)]; + } + } + + return [$plural]; + } + + public function pluralize(string $singular): array + { + if ($this->isInflectedWord($singular)) { + return [$singular]; + } + + foreach (self::PLURALIZE_REGEXP as $rule) { + [$regexp, $replace] = $rule; + + if (1 === preg_match($regexp, $singular)) { + return [preg_replace($regexp, $replace, $singular)]; + } + } + + return [$singular.'s']; + } + + private function isInflectedWord(string $word): bool + { + return 1 === preg_match(self::UNINFLECTED, $word); + } +} diff --git a/3rdparty/symfony/string/Inflector/InflectorInterface.php b/3rdparty/symfony/string/Inflector/InflectorInterface.php new file mode 100644 index 00000000..67f28340 --- /dev/null +++ b/3rdparty/symfony/string/Inflector/InflectorInterface.php @@ -0,0 +1,33 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Component\String\Inflector; + +interface InflectorInterface +{ + /** + * Returns the singular forms of a string. + * + * If the method can't determine the form with certainty, several possible singulars are returned. + * + * @return string[] + */ + public function singularize(string $plural): array; + + /** + * Returns the plural forms of a string. + * + * If the method can't determine the form with certainty, several possible plurals are returned. + * + * @return string[] + */ + public function pluralize(string $singular): array; +} diff --git a/3rdparty/symfony/string/LICENSE b/3rdparty/symfony/string/LICENSE new file mode 100644 index 00000000..f37c76b5 --- /dev/null +++ b/3rdparty/symfony/string/LICENSE @@ -0,0 +1,19 @@ +Copyright (c) 2019-present Fabien Potencier + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is furnished +to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +THE SOFTWARE. diff --git a/3rdparty/symfony/string/LazyString.php b/3rdparty/symfony/string/LazyString.php new file mode 100644 index 00000000..3d893ef9 --- /dev/null +++ b/3rdparty/symfony/string/LazyString.php @@ -0,0 +1,145 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Component\String; + +/** + * A string whose value is computed lazily by a callback. + * + * @author Nicolas Grekas + */ +class LazyString implements \Stringable, \JsonSerializable +{ + private \Closure|string $value; + + /** + * @param callable|array $callback A callable or a [Closure, method] lazy-callable + */ + public static function fromCallable(callable|array $callback, mixed ...$arguments): static + { + if (\is_array($callback) && !\is_callable($callback) && !(($callback[0] ?? null) instanceof \Closure || 2 < \count($callback))) { + throw new \TypeError(sprintf('Argument 1 passed to "%s()" must be a callable or a [Closure, method] lazy-callable, "%s" given.', __METHOD__, '['.implode(', ', array_map('get_debug_type', $callback)).']')); + } + + $lazyString = new static(); + $lazyString->value = static function () use (&$callback, &$arguments): string { + static $value; + + if (null !== $arguments) { + if (!\is_callable($callback)) { + $callback[0] = $callback[0](); + $callback[1] ??= '__invoke'; + } + $value = $callback(...$arguments); + $callback = !\is_scalar($value) && !$value instanceof \Stringable ? self::getPrettyName($callback) : 'callable'; + $arguments = null; + } + + return $value ?? ''; + }; + + return $lazyString; + } + + public static function fromStringable(string|int|float|bool|\Stringable $value): static + { + if (\is_object($value)) { + return static::fromCallable($value->__toString(...)); + } + + $lazyString = new static(); + $lazyString->value = (string) $value; + + return $lazyString; + } + + /** + * Tells whether the provided value can be cast to string. + */ + final public static function isStringable(mixed $value): bool + { + return \is_string($value) || $value instanceof \Stringable || \is_scalar($value); + } + + /** + * Casts scalars and stringable objects to strings. + * + * @throws \TypeError When the provided value is not stringable + */ + final public static function resolve(\Stringable|string|int|float|bool $value): string + { + return $value; + } + + public function __toString(): string + { + if (\is_string($this->value)) { + return $this->value; + } + + try { + return $this->value = ($this->value)(); + } catch (\Throwable $e) { + if (\TypeError::class === $e::class && __FILE__ === $e->getFile()) { + $type = explode(', ', $e->getMessage()); + $type = substr(array_pop($type), 0, -\strlen(' returned')); + $r = new \ReflectionFunction($this->value); + $callback = $r->getStaticVariables()['callback']; + + $e = new \TypeError(sprintf('Return value of %s() passed to %s::fromCallable() must be of the type string, %s returned.', $callback, static::class, $type)); + } + + throw $e; + } + } + + public function __sleep(): array + { + $this->__toString(); + + return ['value']; + } + + public function jsonSerialize(): string + { + return $this->__toString(); + } + + private function __construct() + { + } + + private static function getPrettyName(callable $callback): string + { + if (\is_string($callback)) { + return $callback; + } + + if (\is_array($callback)) { + $class = \is_object($callback[0]) ? get_debug_type($callback[0]) : $callback[0]; + $method = $callback[1]; + } elseif ($callback instanceof \Closure) { + $r = new \ReflectionFunction($callback); + + if (str_contains($r->name, '{closure') || !$class = \PHP_VERSION_ID >= 80111 ? $r->getClosureCalledClass() : $r->getClosureScopeClass()) { + return $r->name; + } + + $class = $class->name; + $method = $r->name; + } else { + $class = get_debug_type($callback); + $method = '__invoke'; + } + + return $class.'::'.$method; + } +} diff --git a/3rdparty/symfony/string/Resources/functions.php b/3rdparty/symfony/string/Resources/functions.php new file mode 100644 index 00000000..7a970400 --- /dev/null +++ b/3rdparty/symfony/string/Resources/functions.php @@ -0,0 +1,38 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Component\String; + +if (!\function_exists(u::class)) { + function u(?string $string = ''): UnicodeString + { + return new UnicodeString($string ?? ''); + } +} + +if (!\function_exists(b::class)) { + function b(?string $string = ''): ByteString + { + return new ByteString($string ?? ''); + } +} + +if (!\function_exists(s::class)) { + /** + * @return UnicodeString|ByteString + */ + function s(?string $string = ''): AbstractString + { + $string ??= ''; + + return preg_match('//u', $string) ? new UnicodeString($string) : new ByteString($string); + } +} diff --git a/3rdparty/symfony/string/Slugger/AsciiSlugger.php b/3rdparty/symfony/string/Slugger/AsciiSlugger.php new file mode 100644 index 00000000..a9693d49 --- /dev/null +++ b/3rdparty/symfony/string/Slugger/AsciiSlugger.php @@ -0,0 +1,210 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Component\String\Slugger; + +use Symfony\Component\Intl\Transliterator\EmojiTransliterator; +use Symfony\Component\String\AbstractUnicodeString; +use Symfony\Component\String\UnicodeString; +use Symfony\Contracts\Translation\LocaleAwareInterface; + +if (!interface_exists(LocaleAwareInterface::class)) { + throw new \LogicException('You cannot use the "Symfony\Component\String\Slugger\AsciiSlugger" as the "symfony/translation-contracts" package is not installed. Try running "composer require symfony/translation-contracts".'); +} + +/** + * @author Titouan Galopin + */ +class AsciiSlugger implements SluggerInterface, LocaleAwareInterface +{ + private const LOCALE_TO_TRANSLITERATOR_ID = [ + 'am' => 'Amharic-Latin', + 'ar' => 'Arabic-Latin', + 'az' => 'Azerbaijani-Latin', + 'be' => 'Belarusian-Latin', + 'bg' => 'Bulgarian-Latin', + 'bn' => 'Bengali-Latin', + 'de' => 'de-ASCII', + 'el' => 'Greek-Latin', + 'fa' => 'Persian-Latin', + 'he' => 'Hebrew-Latin', + 'hy' => 'Armenian-Latin', + 'ka' => 'Georgian-Latin', + 'kk' => 'Kazakh-Latin', + 'ky' => 'Kirghiz-Latin', + 'ko' => 'Korean-Latin', + 'mk' => 'Macedonian-Latin', + 'mn' => 'Mongolian-Latin', + 'or' => 'Oriya-Latin', + 'ps' => 'Pashto-Latin', + 'ru' => 'Russian-Latin', + 'sr' => 'Serbian-Latin', + 'sr_Cyrl' => 'Serbian-Latin', + 'th' => 'Thai-Latin', + 'tk' => 'Turkmen-Latin', + 'uk' => 'Ukrainian-Latin', + 'uz' => 'Uzbek-Latin', + 'zh' => 'Han-Latin', + ]; + + private ?string $defaultLocale; + private \Closure|array $symbolsMap = [ + 'en' => ['@' => 'at', '&' => 'and'], + ]; + private bool|string $emoji = false; + + /** + * Cache of transliterators per locale. + * + * @var \Transliterator[] + */ + private array $transliterators = []; + + public function __construct(?string $defaultLocale = null, array|\Closure|null $symbolsMap = null) + { + $this->defaultLocale = $defaultLocale; + $this->symbolsMap = $symbolsMap ?? $this->symbolsMap; + } + + /** + * @return void + */ + public function setLocale(string $locale) + { + $this->defaultLocale = $locale; + } + + public function getLocale(): string + { + return $this->defaultLocale; + } + + /** + * @param bool|string $emoji true will use the same locale, + * false will disable emoji, + * and a string to use a specific locale + */ + public function withEmoji(bool|string $emoji = true): static + { + if (false !== $emoji && !class_exists(EmojiTransliterator::class)) { + throw new \LogicException(sprintf('You cannot use the "%s()" method as the "symfony/intl" package is not installed. Try running "composer require symfony/intl".', __METHOD__)); + } + + $new = clone $this; + $new->emoji = $emoji; + + return $new; + } + + public function slug(string $string, string $separator = '-', ?string $locale = null): AbstractUnicodeString + { + $locale ??= $this->defaultLocale; + + $transliterator = []; + if ($locale && ('de' === $locale || 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) { + $transliterator = (array) $this->createTransliterator($locale); + } + + if ($emojiTransliterator = $this->createEmojiTransliterator($locale)) { + $transliterator[] = $emojiTransliterator; + } + + if ($this->symbolsMap instanceof \Closure) { + // If the symbols map is passed as a closure, there is no need to fallback to the parent locale + // as the closure can just provide substitutions for all locales of interest. + $symbolsMap = $this->symbolsMap; + array_unshift($transliterator, static fn ($s) => $symbolsMap($s, $locale)); + } + + $unicodeString = (new UnicodeString($string))->ascii($transliterator); + + if (\is_array($this->symbolsMap)) { + $map = null; + if (isset($this->symbolsMap[$locale])) { + $map = $this->symbolsMap[$locale]; + } else { + $parent = self::getParentLocale($locale); + if ($parent && isset($this->symbolsMap[$parent])) { + $map = $this->symbolsMap[$parent]; + } + } + if ($map) { + foreach ($map as $char => $replace) { + $unicodeString = $unicodeString->replace($char, ' '.$replace.' '); + } + } + } + + return $unicodeString + ->replaceMatches('/[^A-Za-z0-9]++/', $separator) + ->trim($separator) + ; + } + + private function createTransliterator(string $locale): ?\Transliterator + { + if (\array_key_exists($locale, $this->transliterators)) { + return $this->transliterators[$locale]; + } + + // Exact locale supported, cache and return + if ($id = self::LOCALE_TO_TRANSLITERATOR_ID[$locale] ?? null) { + return $this->transliterators[$locale] = \Transliterator::create($id.'/BGN') ?? \Transliterator::create($id); + } + + // Locale not supported and no parent, fallback to any-latin + if (!$parent = self::getParentLocale($locale)) { + return $this->transliterators[$locale] = null; + } + + // 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 ?? null; + } + + private function createEmojiTransliterator(?string $locale): ?EmojiTransliterator + { + if (\is_string($this->emoji)) { + $locale = $this->emoji; + } elseif (!$this->emoji) { + return null; + } + + while (null !== $locale) { + try { + return EmojiTransliterator::create("emoji-$locale"); + } catch (\IntlException) { + $locale = self::getParentLocale($locale); + } + } + + return null; + } + + private static function getParentLocale(?string $locale): ?string + { + if (!$locale) { + return null; + } + if (false === $str = strrchr($locale, '_')) { + // no parent locale + return null; + } + + return substr($locale, 0, -\strlen($str)); + } +} diff --git a/3rdparty/symfony/string/Slugger/SluggerInterface.php b/3rdparty/symfony/string/Slugger/SluggerInterface.php new file mode 100644 index 00000000..dd0d5810 --- /dev/null +++ b/3rdparty/symfony/string/Slugger/SluggerInterface.php @@ -0,0 +1,27 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Component\String\Slugger; + +use Symfony\Component\String\AbstractUnicodeString; + +/** + * Creates a URL-friendly slug from a given string. + * + * @author Titouan Galopin + */ +interface SluggerInterface +{ + /** + * Creates a slug for the given string and locale, using appropriate transliteration when needed. + */ + public function slug(string $string, string $separator = '-', ?string $locale = null): AbstractUnicodeString; +} diff --git a/3rdparty/symfony/string/UnicodeString.php b/3rdparty/symfony/string/UnicodeString.php new file mode 100644 index 00000000..75af2da4 --- /dev/null +++ b/3rdparty/symfony/string/UnicodeString.php @@ -0,0 +1,385 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Component\String; + +use Symfony\Component\String\Exception\ExceptionInterface; +use Symfony\Component\String\Exception\InvalidArgumentException; + +/** + * Represents a string of Unicode grapheme clusters encoded as UTF-8. + * + * A letter followed by combining characters (accents typically) form what Unicode defines + * as a grapheme cluster: a character as humans mean it in written texts. This class knows + * about the concept and won't split a letter apart from its combining accents. It also + * ensures all string comparisons happen on their canonically-composed representation, + * ignoring e.g. the order in which accents are listed when a letter has many of them. + * + * @see https://unicode.org/reports/tr15/ + * + * @author Nicolas Grekas + * @author Hugo Hamon + * + * @throws ExceptionInterface + */ +class UnicodeString extends AbstractUnicodeString +{ + public function __construct(string $string = '') + { + if ('' === $string || normalizer_is_normalized($this->string = $string)) { + return; + } + + if (false === $string = normalizer_normalize($string)) { + throw new InvalidArgumentException('Invalid UTF-8 string.'); + } + + $this->string = $string; + } + + public function append(string ...$suffix): static + { + $str = clone $this; + $str->string = $this->string.(1 >= \count($suffix) ? ($suffix[0] ?? '') : implode('', $suffix)); + + if (normalizer_is_normalized($str->string)) { + return $str; + } + + if (false === $string = normalizer_normalize($str->string)) { + throw new InvalidArgumentException('Invalid UTF-8 string.'); + } + + $str->string = $string; + + return $str; + } + + public function chunk(int $length = 1): array + { + if (1 > $length) { + throw new InvalidArgumentException('The chunk length must be greater than zero.'); + } + + if ('' === $this->string) { + return []; + } + + $rx = '/('; + while (65535 < $length) { + $rx .= '\X{65535}'; + $length -= 65535; + } + $rx .= '\X{'.$length.'})/u'; + + $str = clone $this; + $chunks = []; + + foreach (preg_split($rx, $this->string, -1, \PREG_SPLIT_DELIM_CAPTURE | \PREG_SPLIT_NO_EMPTY) as $chunk) { + $str->string = $chunk; + $chunks[] = clone $str; + } + + return $chunks; + } + + public function endsWith(string|iterable|AbstractString $suffix): bool + { + if ($suffix instanceof AbstractString) { + $suffix = $suffix->string; + } elseif (!\is_string($suffix)) { + return parent::endsWith($suffix); + } + + $form = null === $this->ignoreCase ? \Normalizer::NFD : \Normalizer::NFC; + normalizer_is_normalized($suffix, $form) ?: $suffix = normalizer_normalize($suffix, $form); + + if ('' === $suffix || false === $suffix) { + return false; + } + + if ($this->ignoreCase) { + return 0 === mb_stripos(grapheme_extract($this->string, \strlen($suffix), \GRAPHEME_EXTR_MAXBYTES, \strlen($this->string) - \strlen($suffix)), $suffix, 0, 'UTF-8'); + } + + return $suffix === grapheme_extract($this->string, \strlen($suffix), \GRAPHEME_EXTR_MAXBYTES, \strlen($this->string) - \strlen($suffix)); + } + + public function equalsTo(string|iterable|AbstractString $string): bool + { + if ($string instanceof AbstractString) { + $string = $string->string; + } elseif (!\is_string($string)) { + return parent::equalsTo($string); + } + + $form = null === $this->ignoreCase ? \Normalizer::NFD : \Normalizer::NFC; + normalizer_is_normalized($string, $form) ?: $string = normalizer_normalize($string, $form); + + if ('' !== $string && false !== $string && $this->ignoreCase) { + return \strlen($string) === \strlen($this->string) && 0 === mb_stripos($this->string, $string, 0, 'UTF-8'); + } + + return $string === $this->string; + } + + public function indexOf(string|iterable|AbstractString $needle, int $offset = 0): ?int + { + if ($needle instanceof AbstractString) { + $needle = $needle->string; + } elseif (!\is_string($needle)) { + return parent::indexOf($needle, $offset); + } + + $form = null === $this->ignoreCase ? \Normalizer::NFD : \Normalizer::NFC; + normalizer_is_normalized($needle, $form) ?: $needle = normalizer_normalize($needle, $form); + + if ('' === $needle || false === $needle) { + return null; + } + + try { + $i = $this->ignoreCase ? grapheme_stripos($this->string, $needle, $offset) : grapheme_strpos($this->string, $needle, $offset); + } catch (\ValueError) { + return null; + } + + return false === $i ? null : $i; + } + + public function indexOfLast(string|iterable|AbstractString $needle, int $offset = 0): ?int + { + if ($needle instanceof AbstractString) { + $needle = $needle->string; + } elseif (!\is_string($needle)) { + return parent::indexOfLast($needle, $offset); + } + + $form = null === $this->ignoreCase ? \Normalizer::NFD : \Normalizer::NFC; + normalizer_is_normalized($needle, $form) ?: $needle = normalizer_normalize($needle, $form); + + if ('' === $needle || false === $needle) { + return null; + } + + $string = $this->string; + + if (0 > $offset) { + // workaround https://bugs.php.net/74264 + if (0 > $offset += grapheme_strlen($needle)) { + $string = grapheme_substr($string, 0, $offset); + } + $offset = 0; + } + + $i = $this->ignoreCase ? grapheme_strripos($string, $needle, $offset) : grapheme_strrpos($string, $needle, $offset); + + return false === $i ? null : $i; + } + + public function join(array $strings, ?string $lastGlue = null): static + { + $str = parent::join($strings, $lastGlue); + normalizer_is_normalized($str->string) ?: $str->string = normalizer_normalize($str->string); + + return $str; + } + + public function length(): int + { + return grapheme_strlen($this->string); + } + + public function normalize(int $form = self::NFC): static + { + $str = clone $this; + + if (\in_array($form, [self::NFC, self::NFKC], true)) { + normalizer_is_normalized($str->string, $form) ?: $str->string = normalizer_normalize($str->string, $form); + } elseif (!\in_array($form, [self::NFD, self::NFKD], true)) { + throw new InvalidArgumentException('Unsupported normalization form.'); + } elseif (!normalizer_is_normalized($str->string, $form)) { + $str->string = normalizer_normalize($str->string, $form); + $str->ignoreCase = null; + } + + return $str; + } + + public function prepend(string ...$prefix): static + { + $str = clone $this; + $str->string = (1 >= \count($prefix) ? ($prefix[0] ?? '') : implode('', $prefix)).$this->string; + + if (normalizer_is_normalized($str->string)) { + return $str; + } + + if (false === $string = normalizer_normalize($str->string)) { + throw new InvalidArgumentException('Invalid UTF-8 string.'); + } + + $str->string = $string; + + return $str; + } + + public function replace(string $from, string $to): static + { + $str = clone $this; + normalizer_is_normalized($from) ?: $from = normalizer_normalize($from); + + 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)); + } + + $str->string = $result.$tail; + + if (normalizer_is_normalized($str->string)) { + return $str; + } + + if (false === $string = normalizer_normalize($str->string)) { + throw new InvalidArgumentException('Invalid UTF-8 string.'); + } + + $str->string = $string; + } + + return $str; + } + + public function replaceMatches(string $fromRegexp, string|callable $to): static + { + $str = parent::replaceMatches($fromRegexp, $to); + normalizer_is_normalized($str->string) ?: $str->string = normalizer_normalize($str->string); + + return $str; + } + + public function slice(int $start = 0, ?int $length = null): static + { + $str = clone $this; + + $str->string = (string) grapheme_substr($this->string, $start, $length ?? 2147483647); + + return $str; + } + + public function splice(string $replacement, int $start = 0, ?int $length = null): static + { + $str = clone $this; + + $start = $start ? \strlen(grapheme_substr($this->string, 0, $start)) : 0; + $length = $length ? \strlen(grapheme_substr($this->string, $start, $length ?? 2147483647)) : $length; + $str->string = substr_replace($this->string, $replacement, $start, $length ?? 2147483647); + + if (normalizer_is_normalized($str->string)) { + return $str; + } + + if (false === $string = normalizer_normalize($str->string)) { + throw new InvalidArgumentException('Invalid UTF-8 string.'); + } + + $str->string = $string; + + return $str; + } + + public function split(string $delimiter, ?int $limit = null, ?int $flags = null): array + { + if (1 > $limit ??= 2147483647) { + throw new InvalidArgumentException('Split limit must be a positive integer.'); + } + + if ('' === $delimiter) { + throw new InvalidArgumentException('Split delimiter is empty.'); + } + + if (null !== $flags) { + return parent::split($delimiter.'u', $limit, $flags); + } + + normalizer_is_normalized($delimiter) ?: $delimiter = normalizer_normalize($delimiter); + + if (false === $delimiter) { + throw new InvalidArgumentException('Split delimiter is not a valid UTF-8 string.'); + } + + $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); + $chunks[] = clone $str; + $tail = substr($tail, \strlen($str->string) + \strlen($delimiter)); + --$limit; + } + + $str->string = $tail; + $chunks[] = clone $str; + + return $chunks; + } + + public function startsWith(string|iterable|AbstractString $prefix): bool + { + if ($prefix instanceof AbstractString) { + $prefix = $prefix->string; + } elseif (!\is_string($prefix)) { + return parent::startsWith($prefix); + } + + $form = null === $this->ignoreCase ? \Normalizer::NFD : \Normalizer::NFC; + normalizer_is_normalized($prefix, $form) ?: $prefix = normalizer_normalize($prefix, $form); + + if ('' === $prefix || false === $prefix) { + return false; + } + + if ($this->ignoreCase) { + return 0 === mb_stripos(grapheme_extract($this->string, \strlen($prefix), \GRAPHEME_EXTR_MAXBYTES), $prefix, 0, 'UTF-8'); + } + + return $prefix === grapheme_extract($this->string, \strlen($prefix), \GRAPHEME_EXTR_MAXBYTES); + } + + /** + * @return void + */ + public function __wakeup() + { + if (!\is_string($this->string)) { + throw new \BadMethodCallException('Cannot unserialize '.__CLASS__); + } + + normalizer_is_normalized($this->string) ?: $this->string = normalizer_normalize($this->string); + } + + public function __clone() + { + if (null === $this->ignoreCase) { + normalizer_is_normalized($this->string) ?: $this->string = normalizer_normalize($this->string); + } + + $this->ignoreCase = false; + } +} diff --git a/3rdparty/symfony/translation-contracts/LICENSE b/3rdparty/symfony/translation-contracts/LICENSE new file mode 100644 index 00000000..7536caea --- /dev/null +++ b/3rdparty/symfony/translation-contracts/LICENSE @@ -0,0 +1,19 @@ +Copyright (c) 2018-present Fabien Potencier + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is furnished +to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +THE SOFTWARE. diff --git a/3rdparty/symfony/translation-contracts/LocaleAwareInterface.php b/3rdparty/symfony/translation-contracts/LocaleAwareInterface.php new file mode 100644 index 00000000..db40ba13 --- /dev/null +++ b/3rdparty/symfony/translation-contracts/LocaleAwareInterface.php @@ -0,0 +1,29 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Contracts\Translation; + +interface LocaleAwareInterface +{ + /** + * Sets the current locale. + * + * @return void + * + * @throws \InvalidArgumentException If the locale contains invalid characters + */ + public function setLocale(string $locale); + + /** + * Returns the current locale. + */ + public function getLocale(): string; +} diff --git a/3rdparty/symfony/translation-contracts/TranslatableInterface.php b/3rdparty/symfony/translation-contracts/TranslatableInterface.php new file mode 100644 index 00000000..8554697e --- /dev/null +++ b/3rdparty/symfony/translation-contracts/TranslatableInterface.php @@ -0,0 +1,20 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Contracts\Translation; + +/** + * @author Nicolas Grekas + */ +interface TranslatableInterface +{ + public function trans(TranslatorInterface $translator, ?string $locale = null): string; +} diff --git a/3rdparty/symfony/translation-contracts/TranslatorInterface.php b/3rdparty/symfony/translation-contracts/TranslatorInterface.php new file mode 100644 index 00000000..7fa69878 --- /dev/null +++ b/3rdparty/symfony/translation-contracts/TranslatorInterface.php @@ -0,0 +1,68 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Contracts\Translation; + +/** + * @author Fabien Potencier + */ +interface TranslatorInterface +{ + /** + * Translates the given message. + * + * When a number is provided as a parameter named "%count%", the message is parsed for plural + * forms and a translation is chosen according to this number using the following rules: + * + * Given a message with different plural translations separated by a + * pipe (|), this method returns the correct portion of the message based + * on the given number, locale and the pluralization rules in the message + * itself. + * + * The message supports two different types of pluralization rules: + * + * interval: {0} There are no apples|{1} There is one apple|]1,Inf] There are %count% apples + * indexed: There is one apple|There are %count% apples + * + * The indexed solution can also contain labels (e.g. one: There is one apple). + * This is purely for making the translations more clear - it does not + * affect the functionality. + * + * The two methods can also be mixed: + * {0} There are no apples|one: There is one apple|more: There are %count% apples + * + * An interval can represent a finite set of numbers: + * {1,2,3,4} + * + * An interval can represent numbers between two numbers: + * [1, +Inf] + * ]-1,2[ + * + * The left delimiter can be [ (inclusive) or ] (exclusive). + * The right delimiter can be [ (exclusive) or ] (inclusive). + * Beside numbers, you can use -Inf and +Inf for the infinite. + * + * @see https://en.wikipedia.org/wiki/ISO_31-11 + * + * @param string $id The message id (may also be an object that can be cast to string) + * @param array $parameters An array of parameters for the message + * @param string|null $domain The domain for the message or null to use the default + * @param string|null $locale The locale or null to use the default + * + * @throws \InvalidArgumentException If the locale contains invalid characters + */ + public function trans(string $id, array $parameters = [], ?string $domain = null, ?string $locale = null): string; + + /** + * Returns the default locale. + */ + public function getLocale(): string; +} diff --git a/3rdparty/symfony/translation-contracts/TranslatorTrait.php b/3rdparty/symfony/translation-contracts/TranslatorTrait.php new file mode 100644 index 00000000..63f6fb33 --- /dev/null +++ b/3rdparty/symfony/translation-contracts/TranslatorTrait.php @@ -0,0 +1,225 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Contracts\Translation; + +use Symfony\Component\Translation\Exception\InvalidArgumentException; + +/** + * A trait to help implement TranslatorInterface and LocaleAwareInterface. + * + * @author Fabien Potencier + */ +trait TranslatorTrait +{ + private ?string $locale = null; + + /** + * @return void + */ + public function setLocale(string $locale) + { + $this->locale = $locale; + } + + public function getLocale(): string + { + return $this->locale ?: (class_exists(\Locale::class) ? \Locale::getDefault() : 'en'); + } + + public function trans(?string $id, array $parameters = [], ?string $domain = null, ?string $locale = null): string + { + if (null === $id || '' === $id) { + return ''; + } + + if (!isset($parameters['%count%']) || !is_numeric($parameters['%count%'])) { + return strtr($id, $parameters); + } + + $number = (float) $parameters['%count%']; + $locale = $locale ?: $this->getLocale(); + + $parts = []; + if (preg_match('/^\|++$/', $id)) { + $parts = explode('|', $id); + } elseif (preg_match_all('/(?:\|\||[^\|])++/', $id, $matches)) { + $parts = $matches[0]; + } + + $intervalRegexp = <<<'EOF' +/^(?P + ({\s* + (\-?\d+(\.\d+)?[\s*,\s*\-?\d+(\.\d+)?]*) + \s*}) + + | + + (?P[\[\]]) + \s* + (?P-Inf|\-?\d+(\.\d+)?) + \s*,\s* + (?P\+?Inf|\-?\d+(\.\d+)?) + \s* + (?P[\[\]]) +)\s*(?P.*?)$/xs +EOF; + + $standardRules = []; + foreach ($parts as $part) { + $part = trim(str_replace('||', '|', $part)); + + // try to match an explicit rule, then fallback to the standard ones + if (preg_match($intervalRegexp, $part, $matches)) { + if ($matches[2]) { + foreach (explode(',', $matches[3]) as $n) { + if ($number == $n) { + return strtr($matches['message'], $parameters); + } + } + } else { + $leftNumber = '-Inf' === $matches['left'] ? -\INF : (float) $matches['left']; + $rightNumber = is_numeric($matches['right']) ? (float) $matches['right'] : \INF; + + if (('[' === $matches['left_delimiter'] ? $number >= $leftNumber : $number > $leftNumber) + && (']' === $matches['right_delimiter'] ? $number <= $rightNumber : $number < $rightNumber) + ) { + return strtr($matches['message'], $parameters); + } + } + } elseif (preg_match('/^\w+\:\s*(.*?)$/', $part, $matches)) { + $standardRules[] = $matches[1]; + } else { + $standardRules[] = $part; + } + } + + $position = $this->getPluralizationRule($number, $locale); + + if (!isset($standardRules[$position])) { + // when there's exactly one rule given, and that rule is a standard + // rule, use this rule + if (1 === \count($parts) && isset($standardRules[0])) { + return strtr($standardRules[0], $parameters); + } + + $message = sprintf('Unable to choose a translation for "%s" with locale "%s" for value "%d". Double check that this translation has the correct plural options (e.g. "There is one apple|There are %%count%% apples").', $id, $locale, $number); + + if (class_exists(InvalidArgumentException::class)) { + throw new InvalidArgumentException($message); + } + + throw new \InvalidArgumentException($message); + } + + return strtr($standardRules[$position], $parameters); + } + + /** + * Returns the plural position to use for the given locale and number. + * + * The plural rules are derived from code of the Zend Framework (2010-09-25), + * which is subject to the new BSD license (http://framework.zend.com/license/new-bsd). + * Copyright (c) 2005-2010 Zend Technologies USA Inc. (http://www.zend.com) + */ + private function getPluralizationRule(float $number, string $locale): int + { + $number = abs($number); + + return match ('pt_BR' !== $locale && 'en_US_POSIX' !== $locale && \strlen($locale) > 3 ? substr($locale, 0, strrpos($locale, '_')) : $locale) { + 'af', + 'bn', + 'bg', + 'ca', + 'da', + 'de', + 'el', + 'en', + 'en_US_POSIX', + 'eo', + 'es', + 'et', + 'eu', + 'fa', + 'fi', + 'fo', + 'fur', + 'fy', + 'gl', + 'gu', + 'ha', + 'he', + 'hu', + 'is', + 'it', + 'ku', + 'lb', + 'ml', + 'mn', + 'mr', + 'nah', + 'nb', + 'ne', + 'nl', + 'nn', + 'no', + 'oc', + 'om', + 'or', + 'pa', + 'pap', + 'ps', + 'pt', + 'so', + 'sq', + 'sv', + 'sw', + 'ta', + 'te', + 'tk', + 'ur', + 'zu' => (1 == $number) ? 0 : 1, + 'am', + 'bh', + 'fil', + 'fr', + 'gun', + 'hi', + 'hy', + 'ln', + 'mg', + 'nso', + 'pt_BR', + 'ti', + 'wa' => ($number < 2) ? 0 : 1, + 'be', + 'bs', + 'hr', + 'ru', + 'sh', + 'sr', + 'uk' => ((1 == $number % 10) && (11 != $number % 100)) ? 0 : ((($number % 10 >= 2) && ($number % 10 <= 4) && (($number % 100 < 10) || ($number % 100 >= 20))) ? 1 : 2), + 'cs', + 'sk' => (1 == $number) ? 0 : ((($number >= 2) && ($number <= 4)) ? 1 : 2), + 'ga' => (1 == $number) ? 0 : ((2 == $number) ? 1 : 2), + 'lt' => ((1 == $number % 10) && (11 != $number % 100)) ? 0 : ((($number % 10 >= 2) && (($number % 100 < 10) || ($number % 100 >= 20))) ? 1 : 2), + 'sl' => (1 == $number % 100) ? 0 : ((2 == $number % 100) ? 1 : (((3 == $number % 100) || (4 == $number % 100)) ? 2 : 3)), + 'mk' => (1 == $number % 10) ? 0 : 1, + 'mt' => (1 == $number) ? 0 : (((0 == $number) || (($number % 100 > 1) && ($number % 100 < 11))) ? 1 : ((($number % 100 > 10) && ($number % 100 < 20)) ? 2 : 3)), + 'lv' => (0 == $number) ? 0 : (((1 == $number % 10) && (11 != $number % 100)) ? 1 : 2), + 'pl' => (1 == $number) ? 0 : ((($number % 10 >= 2) && ($number % 10 <= 4) && (($number % 100 < 12) || ($number % 100 > 14))) ? 1 : 2), + 'cy' => (1 == $number) ? 0 : ((2 == $number) ? 1 : (((8 == $number) || (11 == $number)) ? 2 : 3)), + 'ro' => (1 == $number) ? 0 : (((0 == $number) || (($number % 100 > 0) && ($number % 100 < 20))) ? 1 : 2), + 'ar' => (0 == $number) ? 0 : ((1 == $number) ? 1 : ((2 == $number) ? 2 : ((($number % 100 >= 3) && ($number % 100 <= 10)) ? 3 : ((($number % 100 >= 11) && ($number % 100 <= 99)) ? 4 : 5)))), + default => 0, + }; + } +} diff --git a/3rdparty/symfony/translation/Catalogue/AbstractOperation.php b/3rdparty/symfony/translation/Catalogue/AbstractOperation.php new file mode 100644 index 00000000..7dff58ff --- /dev/null +++ b/3rdparty/symfony/translation/Catalogue/AbstractOperation.php @@ -0,0 +1,187 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Component\Translation\Catalogue; + +use Symfony\Component\Translation\Exception\InvalidArgumentException; +use Symfony\Component\Translation\Exception\LogicException; +use Symfony\Component\Translation\MessageCatalogue; +use Symfony\Component\Translation\MessageCatalogueInterface; + +/** + * Base catalogues binary operation class. + * + * A catalogue binary operation performs operation on + * source (the left argument) and target (the right argument) catalogues. + * + * @author Jean-François Simon + */ +abstract class AbstractOperation implements OperationInterface +{ + public const OBSOLETE_BATCH = 'obsolete'; + public const NEW_BATCH = 'new'; + public const ALL_BATCH = 'all'; + + protected $source; + protected $target; + protected $result; + + /** + * This array stores 'all', 'new' and 'obsolete' messages for all valid domains. + * + * The data structure of this array is as follows: + * + * [ + * 'domain 1' => [ + * 'all' => [...], + * 'new' => [...], + * 'obsolete' => [...] + * ], + * 'domain 2' => [ + * 'all' => [...], + * 'new' => [...], + * 'obsolete' => [...] + * ], + * ... + * ] + * + * @var array The array that stores 'all', 'new' and 'obsolete' messages + */ + protected $messages; + + private array $domains; + + /** + * @throws LogicException + */ + public function __construct(MessageCatalogueInterface $source, MessageCatalogueInterface $target) + { + if ($source->getLocale() !== $target->getLocale()) { + throw new LogicException('Operated catalogues must belong to the same locale.'); + } + + $this->source = $source; + $this->target = $target; + $this->result = new MessageCatalogue($source->getLocale()); + $this->messages = []; + } + + public function getDomains(): array + { + if (!isset($this->domains)) { + $domains = []; + foreach ([$this->source, $this->target] as $catalogue) { + foreach ($catalogue->getDomains() as $domain) { + $domains[$domain] = $domain; + + if ($catalogue->all($domainIcu = $domain.MessageCatalogueInterface::INTL_DOMAIN_SUFFIX)) { + $domains[$domainIcu] = $domainIcu; + } + } + } + + $this->domains = array_values($domains); + } + + return $this->domains; + } + + public function getMessages(string $domain): array + { + if (!\in_array($domain, $this->getDomains())) { + throw new InvalidArgumentException(sprintf('Invalid domain: "%s".', $domain)); + } + + if (!isset($this->messages[$domain][self::ALL_BATCH])) { + $this->processDomain($domain); + } + + return $this->messages[$domain][self::ALL_BATCH]; + } + + public function getNewMessages(string $domain): array + { + if (!\in_array($domain, $this->getDomains())) { + throw new InvalidArgumentException(sprintf('Invalid domain: "%s".', $domain)); + } + + if (!isset($this->messages[$domain][self::NEW_BATCH])) { + $this->processDomain($domain); + } + + return $this->messages[$domain][self::NEW_BATCH]; + } + + public function getObsoleteMessages(string $domain): array + { + if (!\in_array($domain, $this->getDomains())) { + throw new InvalidArgumentException(sprintf('Invalid domain: "%s".', $domain)); + } + + if (!isset($this->messages[$domain][self::OBSOLETE_BATCH])) { + $this->processDomain($domain); + } + + return $this->messages[$domain][self::OBSOLETE_BATCH]; + } + + public function getResult(): MessageCatalogueInterface + { + foreach ($this->getDomains() as $domain) { + if (!isset($this->messages[$domain])) { + $this->processDomain($domain); + } + } + + return $this->result; + } + + /** + * @param self::*_BATCH $batch + */ + public function moveMessagesToIntlDomainsIfPossible(string $batch = self::ALL_BATCH): void + { + // If MessageFormatter class does not exists, intl domains are not supported. + if (!class_exists(\MessageFormatter::class)) { + return; + } + + foreach ($this->getDomains() as $domain) { + $intlDomain = $domain.MessageCatalogueInterface::INTL_DOMAIN_SUFFIX; + $messages = match ($batch) { + self::OBSOLETE_BATCH => $this->getObsoleteMessages($domain), + self::NEW_BATCH => $this->getNewMessages($domain), + self::ALL_BATCH => $this->getMessages($domain), + default => throw new \InvalidArgumentException(sprintf('$batch argument must be one of ["%s", "%s", "%s"].', self::ALL_BATCH, self::NEW_BATCH, self::OBSOLETE_BATCH)), + }; + + if (!$messages || (!$this->source->all($intlDomain) && $this->source->all($domain))) { + continue; + } + + $result = $this->getResult(); + $allIntlMessages = $result->all($intlDomain); + $currentMessages = array_diff_key($messages, $result->all($domain)); + $result->replace($currentMessages, $domain); + $result->replace($allIntlMessages + $messages, $intlDomain); + } + } + + /** + * Performs operation on source and target catalogues for the given domain and + * stores the results. + * + * @param string $domain The domain which the operation will be performed for + * + * @return void + */ + abstract protected function processDomain(string $domain); +} diff --git a/3rdparty/symfony/translation/Catalogue/MergeOperation.php b/3rdparty/symfony/translation/Catalogue/MergeOperation.php new file mode 100644 index 00000000..1b777a84 --- /dev/null +++ b/3rdparty/symfony/translation/Catalogue/MergeOperation.php @@ -0,0 +1,72 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Component\Translation\Catalogue; + +use Symfony\Component\Translation\MessageCatalogueInterface; + +/** + * Merge operation between two catalogues as follows: + * all = source ∪ target = {x: x ∈ source ∨ x ∈ target} + * new = all ∖ source = {x: x ∈ target ∧ x ∉ source} + * obsolete = source ∖ all = {x: x ∈ source ∧ x ∉ source ∧ x ∉ target} = ∅ + * Basically, the result contains messages from both catalogues. + * + * @author Jean-François Simon + */ +class MergeOperation extends AbstractOperation +{ + /** + * @return void + */ + protected function processDomain(string $domain) + { + $this->messages[$domain] = [ + 'all' => [], + 'new' => [], + 'obsolete' => [], + ]; + $intlDomain = $domain.MessageCatalogueInterface::INTL_DOMAIN_SUFFIX; + + foreach ($this->target->getCatalogueMetadata('', $domain) ?? [] as $key => $value) { + if (null === $this->result->getCatalogueMetadata($key, $domain)) { + $this->result->setCatalogueMetadata($key, $value, $domain); + } + } + + foreach ($this->target->getCatalogueMetadata('', $intlDomain) ?? [] as $key => $value) { + if (null === $this->result->getCatalogueMetadata($key, $intlDomain)) { + $this->result->setCatalogueMetadata($key, $value, $intlDomain); + } + } + + foreach ($this->source->all($domain) as $id => $message) { + $this->messages[$domain]['all'][$id] = $message; + $d = $this->source->defines($id, $intlDomain) ? $intlDomain : $domain; + $this->result->add([$id => $message], $d); + if (null !== $keyMetadata = $this->source->getMetadata($id, $d)) { + $this->result->setMetadata($id, $keyMetadata, $d); + } + } + + foreach ($this->target->all($domain) as $id => $message) { + if (!$this->source->has($id, $domain)) { + $this->messages[$domain]['all'][$id] = $message; + $this->messages[$domain]['new'][$id] = $message; + $d = $this->target->defines($id, $intlDomain) ? $intlDomain : $domain; + $this->result->add([$id => $message], $d); + if (null !== $keyMetadata = $this->target->getMetadata($id, $d)) { + $this->result->setMetadata($id, $keyMetadata, $d); + } + } + } + } +} diff --git a/3rdparty/symfony/translation/Catalogue/OperationInterface.php b/3rdparty/symfony/translation/Catalogue/OperationInterface.php new file mode 100644 index 00000000..1fe9534e --- /dev/null +++ b/3rdparty/symfony/translation/Catalogue/OperationInterface.php @@ -0,0 +1,61 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Component\Translation\Catalogue; + +use Symfony\Component\Translation\MessageCatalogueInterface; + +/** + * Represents an operation on catalogue(s). + * + * An instance of this interface performs an operation on one or more catalogues and + * stores intermediate and final results of the operation. + * + * The first catalogue in its argument(s) is called the 'source catalogue' or 'source' and + * the following results are stored: + * + * Messages: also called 'all', are valid messages for the given domain after the operation is performed. + * + * New Messages: also called 'new' (new = all ∖ source = {x: x ∈ all ∧ x ∉ source}). + * + * Obsolete Messages: also called 'obsolete' (obsolete = source ∖ all = {x: x ∈ source ∧ x ∉ all}). + * + * Result: also called 'result', is the resulting catalogue for the given domain that holds the same messages as 'all'. + * + * @author Jean-François Simon + */ +interface OperationInterface +{ + /** + * Returns domains affected by operation. + */ + public function getDomains(): array; + + /** + * Returns all valid messages ('all') after operation. + */ + public function getMessages(string $domain): array; + + /** + * Returns new messages ('new') after operation. + */ + public function getNewMessages(string $domain): array; + + /** + * Returns obsolete messages ('obsolete') after operation. + */ + public function getObsoleteMessages(string $domain): array; + + /** + * Returns resulting catalogue ('result'). + */ + public function getResult(): MessageCatalogueInterface; +} diff --git a/3rdparty/symfony/translation/Catalogue/TargetOperation.php b/3rdparty/symfony/translation/Catalogue/TargetOperation.php new file mode 100644 index 00000000..2c0ec722 --- /dev/null +++ b/3rdparty/symfony/translation/Catalogue/TargetOperation.php @@ -0,0 +1,86 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Component\Translation\Catalogue; + +use Symfony\Component\Translation\MessageCatalogueInterface; + +/** + * Target operation between two catalogues: + * intersection = source ∩ target = {x: x ∈ source ∧ x ∈ target} + * all = intersection ∪ (target ∖ intersection) = target + * new = all ∖ source = {x: x ∈ target ∧ x ∉ source} + * obsolete = source ∖ all = source ∖ target = {x: x ∈ source ∧ x ∉ target} + * Basically, the result contains messages from the target catalogue. + * + * @author Michael Lee + */ +class TargetOperation extends AbstractOperation +{ + /** + * @return void + */ + protected function processDomain(string $domain) + { + $this->messages[$domain] = [ + 'all' => [], + 'new' => [], + 'obsolete' => [], + ]; + $intlDomain = $domain.MessageCatalogueInterface::INTL_DOMAIN_SUFFIX; + + foreach ($this->target->getCatalogueMetadata('', $domain) ?? [] as $key => $value) { + if (null === $this->result->getCatalogueMetadata($key, $domain)) { + $this->result->setCatalogueMetadata($key, $value, $domain); + } + } + + foreach ($this->target->getCatalogueMetadata('', $intlDomain) ?? [] as $key => $value) { + if (null === $this->result->getCatalogueMetadata($key, $intlDomain)) { + $this->result->setCatalogueMetadata($key, $value, $intlDomain); + } + } + + // For 'all' messages, the code can't be simplified as ``$this->messages[$domain]['all'] = $target->all($domain);``, + // because doing so will drop messages like {x: x ∈ source ∧ x ∉ target.all ∧ x ∈ target.fallback} + // + // For 'new' messages, the code can't be simplified as ``array_diff_assoc($this->target->all($domain), $this->source->all($domain));`` + // because doing so will not exclude messages like {x: x ∈ target ∧ x ∉ source.all ∧ x ∈ source.fallback} + // + // For 'obsolete' messages, the code can't be simplified as ``array_diff_assoc($this->source->all($domain), $this->target->all($domain))`` + // because doing so will not exclude messages like {x: x ∈ source ∧ x ∉ target.all ∧ x ∈ target.fallback} + + foreach ($this->source->all($domain) as $id => $message) { + if ($this->target->has($id, $domain)) { + $this->messages[$domain]['all'][$id] = $message; + $d = $this->source->defines($id, $intlDomain) ? $intlDomain : $domain; + $this->result->add([$id => $message], $d); + if (null !== $keyMetadata = $this->source->getMetadata($id, $d)) { + $this->result->setMetadata($id, $keyMetadata, $d); + } + } else { + $this->messages[$domain]['obsolete'][$id] = $message; + } + } + + foreach ($this->target->all($domain) as $id => $message) { + if (!$this->source->has($id, $domain)) { + $this->messages[$domain]['all'][$id] = $message; + $this->messages[$domain]['new'][$id] = $message; + $d = $this->target->defines($id, $intlDomain) ? $intlDomain : $domain; + $this->result->add([$id => $message], $d); + if (null !== $keyMetadata = $this->target->getMetadata($id, $d)) { + $this->result->setMetadata($id, $keyMetadata, $d); + } + } + } + } +} diff --git a/3rdparty/symfony/translation/CatalogueMetadataAwareInterface.php b/3rdparty/symfony/translation/CatalogueMetadataAwareInterface.php new file mode 100644 index 00000000..c845959f --- /dev/null +++ b/3rdparty/symfony/translation/CatalogueMetadataAwareInterface.php @@ -0,0 +1,48 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Component\Translation; + +/** + * This interface is used to get, set, and delete metadata about the Catalogue. + * + * @author Hugo Alliaume + */ +interface CatalogueMetadataAwareInterface +{ + /** + * Gets catalogue metadata for the given domain and key. + * + * Passing an empty domain will return an array with all catalogue metadata indexed by + * domain and then by key. Passing an empty key will return an array with all + * catalogue metadata for the given domain. + * + * @return mixed The value that was set or an array with the domains/keys or null + */ + public function getCatalogueMetadata(string $key = '', string $domain = 'messages'): mixed; + + /** + * Adds catalogue metadata to a message domain. + * + * @return void + */ + public function setCatalogueMetadata(string $key, mixed $value, string $domain = 'messages'); + + /** + * Deletes catalogue metadata for the given key and domain. + * + * Passing an empty domain will delete all catalogue metadata. Passing an empty key will + * delete all metadata for the given domain. + * + * @return void + */ + public function deleteCatalogueMetadata(string $key = '', string $domain = 'messages'); +} diff --git a/3rdparty/symfony/translation/Command/TranslationPullCommand.php b/3rdparty/symfony/translation/Command/TranslationPullCommand.php new file mode 100644 index 00000000..5d9c092c --- /dev/null +++ b/3rdparty/symfony/translation/Command/TranslationPullCommand.php @@ -0,0 +1,184 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Component\Translation\Command; + +use Symfony\Component\Console\Attribute\AsCommand; +use Symfony\Component\Console\Command\Command; +use Symfony\Component\Console\Completion\CompletionInput; +use Symfony\Component\Console\Completion\CompletionSuggestions; +use Symfony\Component\Console\Input\InputArgument; +use Symfony\Component\Console\Input\InputInterface; +use Symfony\Component\Console\Input\InputOption; +use Symfony\Component\Console\Output\OutputInterface; +use Symfony\Component\Console\Style\SymfonyStyle; +use Symfony\Component\Translation\Catalogue\TargetOperation; +use Symfony\Component\Translation\MessageCatalogue; +use Symfony\Component\Translation\Provider\TranslationProviderCollection; +use Symfony\Component\Translation\Reader\TranslationReaderInterface; +use Symfony\Component\Translation\Writer\TranslationWriterInterface; + +/** + * @author Mathieu Santostefano + */ +#[AsCommand(name: 'translation:pull', description: 'Pull translations from a given provider.')] +final class TranslationPullCommand extends Command +{ + use TranslationTrait; + + private TranslationProviderCollection $providerCollection; + private TranslationWriterInterface $writer; + private TranslationReaderInterface $reader; + private string $defaultLocale; + private array $transPaths; + private array $enabledLocales; + + public function __construct(TranslationProviderCollection $providerCollection, TranslationWriterInterface $writer, TranslationReaderInterface $reader, string $defaultLocale, array $transPaths = [], array $enabledLocales = []) + { + $this->providerCollection = $providerCollection; + $this->writer = $writer; + $this->reader = $reader; + $this->defaultLocale = $defaultLocale; + $this->transPaths = $transPaths; + $this->enabledLocales = $enabledLocales; + + parent::__construct(); + } + + public function complete(CompletionInput $input, CompletionSuggestions $suggestions): void + { + if ($input->mustSuggestArgumentValuesFor('provider')) { + $suggestions->suggestValues($this->providerCollection->keys()); + + return; + } + + if ($input->mustSuggestOptionValuesFor('domains')) { + $provider = $this->providerCollection->get($input->getArgument('provider')); + + if (method_exists($provider, 'getDomains')) { + $suggestions->suggestValues($provider->getDomains()); + } + + return; + } + + if ($input->mustSuggestOptionValuesFor('locales')) { + $suggestions->suggestValues($this->enabledLocales); + + return; + } + + if ($input->mustSuggestOptionValuesFor('format')) { + $suggestions->suggestValues(['php', 'xlf', 'xlf12', 'xlf20', 'po', 'mo', 'yml', 'yaml', 'ts', 'csv', 'json', 'ini', 'res']); + } + } + + protected function configure(): void + { + $keys = $this->providerCollection->keys(); + $defaultProvider = 1 === \count($keys) ? $keys[0] : null; + + $this + ->setDefinition([ + new InputArgument('provider', null !== $defaultProvider ? InputArgument::OPTIONAL : InputArgument::REQUIRED, 'The provider to pull translations from.', $defaultProvider), + new InputOption('force', null, InputOption::VALUE_NONE, 'Override existing translations with provider ones (it will delete not synchronized messages).'), + new InputOption('intl-icu', null, InputOption::VALUE_NONE, 'Associated to --force option, it will write messages in "%domain%+intl-icu.%locale%.xlf" files.'), + new InputOption('domains', null, InputOption::VALUE_OPTIONAL | InputOption::VALUE_IS_ARRAY, 'Specify the domains to pull.'), + new InputOption('locales', null, InputOption::VALUE_OPTIONAL | InputOption::VALUE_IS_ARRAY, 'Specify the locales to pull.'), + new InputOption('format', null, InputOption::VALUE_OPTIONAL, 'Override the default output format.', 'xlf12'), + new InputOption('as-tree', null, InputOption::VALUE_OPTIONAL, 'Write messages as a tree-like structure. Needs --format=yaml. The given value defines the level where to switch to inline YAML'), + ]) + ->setHelp(<<<'EOF' +The %command.name% command pulls translations from the given provider. Only +new translations are pulled, existing ones are not overwritten. + +You can overwrite existing translations (and remove the missing ones on local side) by using the --force flag: + + php %command.full_name% --force provider + +Full example: + + php %command.full_name% provider --force --domains=messages --domains=validators --locales=en + +This command pulls all translations associated with the messages and validators domains for the en locale. +Local translations for the specified domains and locale are deleted if they're not present on the provider and overwritten if it's the case. +Local translations for others domains and locales are ignored. +EOF + ) + ; + } + + protected function execute(InputInterface $input, OutputInterface $output): int + { + $io = new SymfonyStyle($input, $output); + + $provider = $this->providerCollection->get($input->getArgument('provider')); + $force = $input->getOption('force'); + $intlIcu = $input->getOption('intl-icu'); + $locales = $input->getOption('locales') ?: $this->enabledLocales; + $domains = $input->getOption('domains'); + $format = $input->getOption('format'); + $asTree = (int) $input->getOption('as-tree'); + $xliffVersion = '1.2'; + + if ($intlIcu && !$force) { + $io->note('--intl-icu option only has an effect when used with --force. Here, it will be ignored.'); + } + + switch ($format) { + case 'xlf20': $xliffVersion = '2.0'; + // no break + case 'xlf12': $format = 'xlf'; + } + + $writeOptions = [ + 'path' => end($this->transPaths), + 'xliff_version' => $xliffVersion, + 'default_locale' => $this->defaultLocale, + 'as_tree' => (bool) $asTree, + 'inline' => $asTree, + ]; + + if (!$domains) { + $domains = $provider->getDomains(); + } + + $providerTranslations = $provider->read($domains, $locales); + + if ($force) { + foreach ($providerTranslations->getCatalogues() as $catalogue) { + $operation = new TargetOperation(new MessageCatalogue($catalogue->getLocale()), $catalogue); + if ($intlIcu) { + $operation->moveMessagesToIntlDomainsIfPossible(); + } + $this->writer->write($operation->getResult(), $format, $writeOptions); + } + + $io->success(sprintf('Local translations has been updated from "%s" (for "%s" locale(s), and "%s" domain(s)).', parse_url($provider, \PHP_URL_SCHEME), implode(', ', $locales), implode(', ', $domains))); + + return 0; + } + + $localTranslations = $this->readLocalTranslations($locales, $domains, $this->transPaths); + + // Append pulled translations to local ones. + $localTranslations->addBag($providerTranslations->diff($localTranslations)); + + foreach ($localTranslations->getCatalogues() as $catalogue) { + $this->writer->write($catalogue, $format, $writeOptions); + } + + $io->success(sprintf('New translations from "%s" has been written locally (for "%s" locale(s), and "%s" domain(s)).', parse_url($provider, \PHP_URL_SCHEME), implode(', ', $locales), implode(', ', $domains))); + + return 0; + } +} diff --git a/3rdparty/symfony/translation/Command/TranslationPushCommand.php b/3rdparty/symfony/translation/Command/TranslationPushCommand.php new file mode 100644 index 00000000..1d04adbc --- /dev/null +++ b/3rdparty/symfony/translation/Command/TranslationPushCommand.php @@ -0,0 +1,182 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Component\Translation\Command; + +use Symfony\Component\Console\Attribute\AsCommand; +use Symfony\Component\Console\Command\Command; +use Symfony\Component\Console\Completion\CompletionInput; +use Symfony\Component\Console\Completion\CompletionSuggestions; +use Symfony\Component\Console\Exception\InvalidArgumentException; +use Symfony\Component\Console\Input\InputArgument; +use Symfony\Component\Console\Input\InputInterface; +use Symfony\Component\Console\Input\InputOption; +use Symfony\Component\Console\Output\OutputInterface; +use Symfony\Component\Console\Style\SymfonyStyle; +use Symfony\Component\Translation\Provider\FilteringProvider; +use Symfony\Component\Translation\Provider\TranslationProviderCollection; +use Symfony\Component\Translation\Reader\TranslationReaderInterface; +use Symfony\Component\Translation\TranslatorBag; + +/** + * @author Mathieu Santostefano + */ +#[AsCommand(name: 'translation:push', description: 'Push translations to a given provider.')] +final class TranslationPushCommand extends Command +{ + use TranslationTrait; + + private TranslationProviderCollection $providers; + private TranslationReaderInterface $reader; + private array $transPaths; + private array $enabledLocales; + + public function __construct(TranslationProviderCollection $providers, TranslationReaderInterface $reader, array $transPaths = [], array $enabledLocales = []) + { + $this->providers = $providers; + $this->reader = $reader; + $this->transPaths = $transPaths; + $this->enabledLocales = $enabledLocales; + + parent::__construct(); + } + + public function complete(CompletionInput $input, CompletionSuggestions $suggestions): void + { + if ($input->mustSuggestArgumentValuesFor('provider')) { + $suggestions->suggestValues($this->providers->keys()); + + return; + } + + if ($input->mustSuggestOptionValuesFor('domains')) { + $provider = $this->providers->get($input->getArgument('provider')); + + if ($provider && method_exists($provider, 'getDomains')) { + $domains = $provider->getDomains(); + $suggestions->suggestValues($domains); + } + + return; + } + + if ($input->mustSuggestOptionValuesFor('locales')) { + $suggestions->suggestValues($this->enabledLocales); + } + } + + protected function configure(): void + { + $keys = $this->providers->keys(); + $defaultProvider = 1 === \count($keys) ? $keys[0] : null; + + $this + ->setDefinition([ + new InputArgument('provider', null !== $defaultProvider ? InputArgument::OPTIONAL : InputArgument::REQUIRED, 'The provider to push translations to.', $defaultProvider), + new InputOption('force', null, InputOption::VALUE_NONE, 'Override existing translations with local ones (it will delete not synchronized messages).'), + new InputOption('delete-missing', null, InputOption::VALUE_NONE, 'Delete translations available on provider but not locally.'), + new InputOption('domains', null, InputOption::VALUE_OPTIONAL | InputOption::VALUE_IS_ARRAY, 'Specify the domains to push.'), + new InputOption('locales', null, InputOption::VALUE_OPTIONAL | InputOption::VALUE_IS_ARRAY, 'Specify the locales to push.', $this->enabledLocales), + ]) + ->setHelp(<<<'EOF' +The %command.name% command pushes translations to the given provider. Only new +translations are pushed, existing ones are not overwritten. + +You can overwrite existing translations by using the --force flag: + + php %command.full_name% --force provider + +You can delete provider translations which are not present locally by using the --delete-missing flag: + + php %command.full_name% --delete-missing provider + +Full example: + + php %command.full_name% provider --force --delete-missing --domains=messages --domains=validators --locales=en + +This command pushes all translations associated with the messages and validators domains for the en locale. +Provider translations for the specified domains and locale are deleted if they're not present locally and overwritten if it's the case. +Provider translations for others domains and locales are ignored. +EOF + ) + ; + } + + protected function execute(InputInterface $input, OutputInterface $output): int + { + $provider = $this->providers->get($input->getArgument('provider')); + + if (!$this->enabledLocales) { + throw new InvalidArgumentException(sprintf('You must define "framework.enabled_locales" or "framework.translator.providers.%s.locales" config key in order to work with translation providers.', parse_url($provider, \PHP_URL_SCHEME))); + } + + $io = new SymfonyStyle($input, $output); + $domains = $input->getOption('domains'); + $locales = $input->getOption('locales'); + $force = $input->getOption('force'); + $deleteMissing = $input->getOption('delete-missing'); + + if (!$domains && $provider instanceof FilteringProvider) { + $domains = $provider->getDomains(); + } + + // Reading local translations must be done after retrieving the domains from the provider + // in order to manage only translations from configured domains + $localTranslations = $this->readLocalTranslations($locales, $domains, $this->transPaths); + + if (!$domains) { + $domains = $this->getDomainsFromTranslatorBag($localTranslations); + } + + if (!$deleteMissing && $force) { + $provider->write($localTranslations); + + $io->success(sprintf('All local translations has been sent to "%s" (for "%s" locale(s), and "%s" domain(s)).', parse_url($provider, \PHP_URL_SCHEME), implode(', ', $locales), implode(', ', $domains))); + + return 0; + } + + $providerTranslations = $provider->read($domains, $locales); + + if ($deleteMissing) { + $provider->delete($providerTranslations->diff($localTranslations)); + + $io->success(sprintf('Missing translations on "%s" has been deleted (for "%s" locale(s), and "%s" domain(s)).', parse_url($provider, \PHP_URL_SCHEME), implode(', ', $locales), implode(', ', $domains))); + + // Read provider translations again, after missing translations deletion, + // to avoid push freshly deleted translations. + $providerTranslations = $provider->read($domains, $locales); + } + + $translationsToWrite = $localTranslations->diff($providerTranslations); + + if ($force) { + $translationsToWrite->addBag($localTranslations->intersect($providerTranslations)); + } + + $provider->write($translationsToWrite); + + $io->success(sprintf('%s local translations has been sent to "%s" (for "%s" locale(s), and "%s" domain(s)).', $force ? 'All' : 'New', parse_url($provider, \PHP_URL_SCHEME), implode(', ', $locales), implode(', ', $domains))); + + return 0; + } + + private function getDomainsFromTranslatorBag(TranslatorBag $translatorBag): array + { + $domains = []; + + foreach ($translatorBag->getCatalogues() as $catalogue) { + $domains += $catalogue->getDomains(); + } + + return array_unique($domains); + } +} diff --git a/3rdparty/symfony/translation/Command/TranslationTrait.php b/3rdparty/symfony/translation/Command/TranslationTrait.php new file mode 100644 index 00000000..eafaffd3 --- /dev/null +++ b/3rdparty/symfony/translation/Command/TranslationTrait.php @@ -0,0 +1,77 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Component\Translation\Command; + +use Symfony\Component\Translation\MessageCatalogue; +use Symfony\Component\Translation\MessageCatalogueInterface; +use Symfony\Component\Translation\TranslatorBag; + +/** + * @internal + */ +trait TranslationTrait +{ + private function readLocalTranslations(array $locales, array $domains, array $transPaths): TranslatorBag + { + $bag = new TranslatorBag(); + + foreach ($locales as $locale) { + $catalogue = new MessageCatalogue($locale); + foreach ($transPaths as $path) { + $this->reader->read($path, $catalogue); + } + + if ($domains) { + foreach ($domains as $domain) { + $bag->addCatalogue($this->filterCatalogue($catalogue, $domain)); + } + } else { + $bag->addCatalogue($catalogue); + } + } + + return $bag; + } + + private function filterCatalogue(MessageCatalogue $catalogue, string $domain): MessageCatalogue + { + $filteredCatalogue = new MessageCatalogue($catalogue->getLocale()); + + // extract intl-icu messages only + $intlDomain = $domain.MessageCatalogueInterface::INTL_DOMAIN_SUFFIX; + if ($intlMessages = $catalogue->all($intlDomain)) { + $filteredCatalogue->add($intlMessages, $intlDomain); + } + + // extract all messages and subtract intl-icu messages + if ($messages = array_diff($catalogue->all($domain), $intlMessages)) { + $filteredCatalogue->add($messages, $domain); + } + foreach ($catalogue->getResources() as $resource) { + $filteredCatalogue->addResource($resource); + } + + if ($metadata = $catalogue->getMetadata('', $intlDomain)) { + foreach ($metadata as $k => $v) { + $filteredCatalogue->setMetadata($k, $v, $intlDomain); + } + } + + if ($metadata = $catalogue->getMetadata('', $domain)) { + foreach ($metadata as $k => $v) { + $filteredCatalogue->setMetadata($k, $v, $domain); + } + } + + return $filteredCatalogue; + } +} diff --git a/3rdparty/symfony/translation/Command/XliffLintCommand.php b/3rdparty/symfony/translation/Command/XliffLintCommand.php new file mode 100644 index 00000000..ba946389 --- /dev/null +++ b/3rdparty/symfony/translation/Command/XliffLintCommand.php @@ -0,0 +1,285 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Component\Translation\Command; + +use Symfony\Component\Console\Attribute\AsCommand; +use Symfony\Component\Console\CI\GithubActionReporter; +use Symfony\Component\Console\Command\Command; +use Symfony\Component\Console\Completion\CompletionInput; +use Symfony\Component\Console\Completion\CompletionSuggestions; +use Symfony\Component\Console\Exception\RuntimeException; +use Symfony\Component\Console\Input\InputArgument; +use Symfony\Component\Console\Input\InputInterface; +use Symfony\Component\Console\Input\InputOption; +use Symfony\Component\Console\Output\OutputInterface; +use Symfony\Component\Console\Style\SymfonyStyle; +use Symfony\Component\Translation\Exception\InvalidArgumentException; +use Symfony\Component\Translation\Util\XliffUtils; + +/** + * Validates XLIFF files syntax and outputs encountered errors. + * + * @author Grégoire Pineau + * @author Robin Chalas + * @author Javier Eguiluz + */ +#[AsCommand(name: 'lint:xliff', description: 'Lint an XLIFF file and outputs encountered errors')] +class XliffLintCommand extends Command +{ + private string $format; + private bool $displayCorrectFiles; + private ?\Closure $directoryIteratorProvider; + private ?\Closure $isReadableProvider; + private bool $requireStrictFileNames; + + public function __construct(?string $name = null, ?callable $directoryIteratorProvider = null, ?callable $isReadableProvider = null, bool $requireStrictFileNames = true) + { + parent::__construct($name); + + $this->directoryIteratorProvider = null === $directoryIteratorProvider ? null : $directoryIteratorProvider(...); + $this->isReadableProvider = null === $isReadableProvider ? null : $isReadableProvider(...); + $this->requireStrictFileNames = $requireStrictFileNames; + } + + /** + * @return void + */ + protected function configure() + { + $this + ->addArgument('filename', InputArgument::IS_ARRAY, 'A file, a directory or "-" for reading from STDIN') + ->addOption('format', null, InputOption::VALUE_REQUIRED, sprintf('The output format ("%s")', implode('", "', $this->getAvailableFormatOptions()))) + ->setHelp(<<%command.name% command lints an XLIFF file and outputs to STDOUT +the first encountered syntax error. + +You can validates XLIFF contents passed from STDIN: + + cat filename | php %command.full_name% - + +You can also validate the syntax of a file: + + php %command.full_name% filename + +Or of a whole directory: + + php %command.full_name% dirname + php %command.full_name% dirname --format=json + +EOF + ) + ; + } + + protected function execute(InputInterface $input, OutputInterface $output): int + { + $io = new SymfonyStyle($input, $output); + $filenames = (array) $input->getArgument('filename'); + $this->format = $input->getOption('format') ?? (GithubActionReporter::isGithubActionEnvironment() ? 'github' : 'txt'); + $this->displayCorrectFiles = $output->isVerbose(); + + if (['-'] === $filenames) { + return $this->display($io, [$this->validate(file_get_contents('php://stdin'))]); + } + + if (!$filenames) { + throw new RuntimeException('Please provide a filename or pipe file content to STDIN.'); + } + + $filesInfo = []; + foreach ($filenames as $filename) { + if (!$this->isReadable($filename)) { + throw new RuntimeException(sprintf('File or directory "%s" is not readable.', $filename)); + } + + foreach ($this->getFiles($filename) as $file) { + $filesInfo[] = $this->validate(file_get_contents($file), $file); + } + } + + return $this->display($io, $filesInfo); + } + + private function validate(string $content, ?string $file = null): array + { + $errors = []; + + // Avoid: Warning DOMDocument::loadXML(): Empty string supplied as input + if ('' === trim($content)) { + return ['file' => $file, 'valid' => true]; + } + + $internal = libxml_use_internal_errors(true); + + $document = new \DOMDocument(); + $document->loadXML($content); + + if (null !== $targetLanguage = $this->getTargetLanguageFromFile($document)) { + $normalizedLocalePattern = sprintf('(%s|%s)', preg_quote($targetLanguage, '/'), preg_quote(str_replace('-', '_', $targetLanguage), '/')); + // strict file names require translation files to be named '____.locale.xlf' + // otherwise, both '____.locale.xlf' and 'locale.____.xlf' are allowed + // also, the regexp matching must be case-insensitive, as defined for 'target-language' values + // http://docs.oasis-open.org/xliff/v1.2/os/xliff-core.html#target-language + $expectedFilenamePattern = $this->requireStrictFileNames ? sprintf('/^.*\.(?i:%s)\.(?:xlf|xliff)/', $normalizedLocalePattern) : sprintf('/^(?:.*\.(?i:%s)|(?i:%s)\..*)\.(?:xlf|xliff)/', $normalizedLocalePattern, $normalizedLocalePattern); + + if (0 === preg_match($expectedFilenamePattern, basename($file))) { + $errors[] = [ + 'line' => -1, + 'column' => -1, + 'message' => sprintf('There is a mismatch between the language included in the file name ("%s") and the "%s" value used in the "target-language" attribute of the file.', basename($file), $targetLanguage), + ]; + } + } + + foreach (XliffUtils::validateSchema($document) as $xmlError) { + $errors[] = [ + 'line' => $xmlError['line'], + 'column' => $xmlError['column'], + 'message' => $xmlError['message'], + ]; + } + + libxml_clear_errors(); + libxml_use_internal_errors($internal); + + return ['file' => $file, 'valid' => 0 === \count($errors), 'messages' => $errors]; + } + + private function display(SymfonyStyle $io, array $files): int + { + return match ($this->format) { + 'txt' => $this->displayTxt($io, $files), + 'json' => $this->displayJson($io, $files), + 'github' => $this->displayTxt($io, $files, true), + default => throw new InvalidArgumentException(sprintf('Supported formats are "%s".', implode('", "', $this->getAvailableFormatOptions()))), + }; + } + + private function displayTxt(SymfonyStyle $io, array $filesInfo, bool $errorAsGithubAnnotations = false): int + { + $countFiles = \count($filesInfo); + $erroredFiles = 0; + $githubReporter = $errorAsGithubAnnotations ? new GithubActionReporter($io) : null; + + foreach ($filesInfo as $info) { + if ($info['valid'] && $this->displayCorrectFiles) { + $io->comment('OK'.($info['file'] ? sprintf(' in %s', $info['file']) : '')); + } elseif (!$info['valid']) { + ++$erroredFiles; + $io->text(' ERROR '.($info['file'] ? sprintf(' in %s', $info['file']) : '')); + $io->listing(array_map(function ($error) use ($info, $githubReporter) { + // general document errors have a '-1' line number + $line = -1 === $error['line'] ? null : $error['line']; + + $githubReporter?->error($error['message'], $info['file'], $line, null !== $line ? $error['column'] : null); + + return null === $line ? $error['message'] : sprintf('Line %d, Column %d: %s', $line, $error['column'], $error['message']); + }, $info['messages'])); + } + } + + if (0 === $erroredFiles) { + $io->success(sprintf('All %d XLIFF files contain valid syntax.', $countFiles)); + } else { + $io->warning(sprintf('%d XLIFF files have valid syntax and %d contain errors.', $countFiles - $erroredFiles, $erroredFiles)); + } + + return min($erroredFiles, 1); + } + + private function displayJson(SymfonyStyle $io, array $filesInfo): int + { + $errors = 0; + + array_walk($filesInfo, function (&$v) use (&$errors) { + $v['file'] = (string) $v['file']; + if (!$v['valid']) { + ++$errors; + } + }); + + $io->writeln(json_encode($filesInfo, \JSON_PRETTY_PRINT | \JSON_UNESCAPED_SLASHES)); + + return min($errors, 1); + } + + /** + * @return iterable<\SplFileInfo> + */ + private function getFiles(string $fileOrDirectory): iterable + { + if (is_file($fileOrDirectory)) { + yield new \SplFileInfo($fileOrDirectory); + + return; + } + + foreach ($this->getDirectoryIterator($fileOrDirectory) as $file) { + if (!\in_array($file->getExtension(), ['xlf', 'xliff'])) { + continue; + } + + yield $file; + } + } + + /** + * @return iterable<\SplFileInfo> + */ + private function getDirectoryIterator(string $directory): iterable + { + $default = fn ($directory) => new \RecursiveIteratorIterator( + new \RecursiveDirectoryIterator($directory, \FilesystemIterator::SKIP_DOTS | \FilesystemIterator::FOLLOW_SYMLINKS), + \RecursiveIteratorIterator::LEAVES_ONLY + ); + + if (null !== $this->directoryIteratorProvider) { + return ($this->directoryIteratorProvider)($directory, $default); + } + + return $default($directory); + } + + private function isReadable(string $fileOrDirectory): bool + { + $default = fn ($fileOrDirectory) => is_readable($fileOrDirectory); + + if (null !== $this->isReadableProvider) { + return ($this->isReadableProvider)($fileOrDirectory, $default); + } + + return $default($fileOrDirectory); + } + + private function getTargetLanguageFromFile(\DOMDocument $xliffContents): ?string + { + foreach ($xliffContents->getElementsByTagName('file')[0]->attributes ?? [] as $attribute) { + if ('target-language' === $attribute->nodeName) { + return $attribute->nodeValue; + } + } + + return null; + } + + public function complete(CompletionInput $input, CompletionSuggestions $suggestions): void + { + if ($input->mustSuggestOptionValuesFor('format')) { + $suggestions->suggestValues($this->getAvailableFormatOptions()); + } + } + + private function getAvailableFormatOptions(): array + { + return ['txt', 'json', 'github']; + } +} diff --git a/3rdparty/symfony/translation/DataCollector/TranslationDataCollector.php b/3rdparty/symfony/translation/DataCollector/TranslationDataCollector.php new file mode 100644 index 00000000..d4f49cc6 --- /dev/null +++ b/3rdparty/symfony/translation/DataCollector/TranslationDataCollector.php @@ -0,0 +1,148 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Component\Translation\DataCollector; + +use Symfony\Component\HttpFoundation\Request; +use Symfony\Component\HttpFoundation\Response; +use Symfony\Component\HttpKernel\DataCollector\DataCollector; +use Symfony\Component\HttpKernel\DataCollector\LateDataCollectorInterface; +use Symfony\Component\Translation\DataCollectorTranslator; +use Symfony\Component\VarDumper\Cloner\Data; + +/** + * @author Abdellatif Ait boudad + * + * @final + */ +class TranslationDataCollector extends DataCollector implements LateDataCollectorInterface +{ + private DataCollectorTranslator $translator; + + public function __construct(DataCollectorTranslator $translator) + { + $this->translator = $translator; + } + + public function lateCollect(): void + { + $messages = $this->sanitizeCollectedMessages($this->translator->getCollectedMessages()); + + $this->data += $this->computeCount($messages); + $this->data['messages'] = $messages; + + $this->data = $this->cloneVar($this->data); + } + + public function collect(Request $request, Response $response, ?\Throwable $exception = null): void + { + $this->data['locale'] = $this->translator->getLocale(); + $this->data['fallback_locales'] = $this->translator->getFallbackLocales(); + } + + public function reset(): void + { + $this->data = []; + } + + public function getMessages(): array|Data + { + return $this->data['messages'] ?? []; + } + + public function getCountMissings(): int + { + return $this->data[DataCollectorTranslator::MESSAGE_MISSING] ?? 0; + } + + public function getCountFallbacks(): int + { + return $this->data[DataCollectorTranslator::MESSAGE_EQUALS_FALLBACK] ?? 0; + } + + public function getCountDefines(): int + { + return $this->data[DataCollectorTranslator::MESSAGE_DEFINED] ?? 0; + } + + public function getLocale(): ?string + { + return !empty($this->data['locale']) ? $this->data['locale'] : null; + } + + /** + * @internal + */ + public function getFallbackLocales(): Data|array + { + return (isset($this->data['fallback_locales']) && \count($this->data['fallback_locales']) > 0) ? $this->data['fallback_locales'] : []; + } + + public function getName(): string + { + return 'translation'; + } + + private function sanitizeCollectedMessages(array $messages): array + { + $result = []; + foreach ($messages as $key => $message) { + $messageId = $message['locale'].$message['domain'].$message['id']; + + if (!isset($result[$messageId])) { + $message['count'] = 1; + $message['parameters'] = !empty($message['parameters']) ? [$message['parameters']] : []; + $messages[$key]['translation'] = $this->sanitizeString($message['translation']); + $result[$messageId] = $message; + } else { + if (!empty($message['parameters'])) { + $result[$messageId]['parameters'][] = $message['parameters']; + } + + ++$result[$messageId]['count']; + } + + unset($messages[$key]); + } + + return $result; + } + + private function computeCount(array $messages): array + { + $count = [ + DataCollectorTranslator::MESSAGE_DEFINED => 0, + DataCollectorTranslator::MESSAGE_MISSING => 0, + DataCollectorTranslator::MESSAGE_EQUALS_FALLBACK => 0, + ]; + + foreach ($messages as $message) { + ++$count[$message['state']]; + } + + return $count; + } + + private function sanitizeString(string $string, int $length = 80): string + { + $string = trim(preg_replace('/\s+/', ' ', $string)); + + if (false !== $encoding = mb_detect_encoding($string, null, true)) { + if (mb_strlen($string, $encoding) > $length) { + return mb_substr($string, 0, $length - 3, $encoding).'...'; + } + } elseif (\strlen($string) > $length) { + return substr($string, 0, $length - 3).'...'; + } + + return $string; + } +} diff --git a/3rdparty/symfony/translation/DataCollectorTranslator.php b/3rdparty/symfony/translation/DataCollectorTranslator.php new file mode 100644 index 00000000..a2832ee8 --- /dev/null +++ b/3rdparty/symfony/translation/DataCollectorTranslator.php @@ -0,0 +1,143 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Component\Translation; + +use Symfony\Component\HttpKernel\CacheWarmer\WarmableInterface; +use Symfony\Component\Translation\Exception\InvalidArgumentException; +use Symfony\Contracts\Translation\LocaleAwareInterface; +use Symfony\Contracts\Translation\TranslatorInterface; + +/** + * @author Abdellatif Ait boudad + */ +class DataCollectorTranslator implements TranslatorInterface, TranslatorBagInterface, LocaleAwareInterface, WarmableInterface +{ + public const MESSAGE_DEFINED = 0; + public const MESSAGE_MISSING = 1; + public const MESSAGE_EQUALS_FALLBACK = 2; + + private TranslatorInterface $translator; + private array $messages = []; + + /** + * @param TranslatorInterface&TranslatorBagInterface&LocaleAwareInterface $translator + */ + public function __construct(TranslatorInterface $translator) + { + if (!$translator instanceof TranslatorBagInterface || !$translator instanceof LocaleAwareInterface) { + throw new InvalidArgumentException(sprintf('The Translator "%s" must implement TranslatorInterface, TranslatorBagInterface and LocaleAwareInterface.', get_debug_type($translator))); + } + + $this->translator = $translator; + } + + public function trans(?string $id, array $parameters = [], ?string $domain = null, ?string $locale = null): string + { + $trans = $this->translator->trans($id = (string) $id, $parameters, $domain, $locale); + $this->collectMessage($locale, $domain, $id, $trans, $parameters); + + return $trans; + } + + /** + * @return void + */ + public function setLocale(string $locale) + { + $this->translator->setLocale($locale); + } + + public function getLocale(): string + { + return $this->translator->getLocale(); + } + + public function getCatalogue(?string $locale = null): MessageCatalogueInterface + { + return $this->translator->getCatalogue($locale); + } + + public function getCatalogues(): array + { + return $this->translator->getCatalogues(); + } + + public function warmUp(string $cacheDir, ?string $buildDir = null): array + { + if ($this->translator instanceof WarmableInterface) { + return (array) $this->translator->warmUp($cacheDir, $buildDir); + } + + return []; + } + + /** + * Gets the fallback locales. + */ + public function getFallbackLocales(): array + { + if ($this->translator instanceof Translator || method_exists($this->translator, 'getFallbackLocales')) { + return $this->translator->getFallbackLocales(); + } + + return []; + } + + /** + * @return mixed + */ + public function __call(string $method, array $args) + { + return $this->translator->{$method}(...$args); + } + + public function getCollectedMessages(): array + { + return $this->messages; + } + + private function collectMessage(?string $locale, ?string $domain, string $id, string $translation, ?array $parameters = []): void + { + $domain ??= 'messages'; + + $catalogue = $this->translator->getCatalogue($locale); + $locale = $catalogue->getLocale(); + $fallbackLocale = null; + if ($catalogue->defines($id, $domain)) { + $state = self::MESSAGE_DEFINED; + } elseif ($catalogue->has($id, $domain)) { + $state = self::MESSAGE_EQUALS_FALLBACK; + + $fallbackCatalogue = $catalogue->getFallbackCatalogue(); + while ($fallbackCatalogue) { + if ($fallbackCatalogue->defines($id, $domain)) { + $fallbackLocale = $fallbackCatalogue->getLocale(); + break; + } + $fallbackCatalogue = $fallbackCatalogue->getFallbackCatalogue(); + } + } else { + $state = self::MESSAGE_MISSING; + } + + $this->messages[] = [ + 'locale' => $locale, + 'fallbackLocale' => $fallbackLocale, + 'domain' => $domain, + 'id' => $id, + 'translation' => $translation, + 'parameters' => $parameters, + 'state' => $state, + 'transChoiceNumber' => isset($parameters['%count%']) && is_numeric($parameters['%count%']) ? $parameters['%count%'] : null, + ]; + } +} diff --git a/3rdparty/symfony/translation/DependencyInjection/DataCollectorTranslatorPass.php b/3rdparty/symfony/translation/DependencyInjection/DataCollectorTranslatorPass.php new file mode 100644 index 00000000..cdf63be4 --- /dev/null +++ b/3rdparty/symfony/translation/DependencyInjection/DataCollectorTranslatorPass.php @@ -0,0 +1,36 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Component\Translation\DependencyInjection; + +use Symfony\Component\DependencyInjection\Compiler\CompilerPassInterface; +use Symfony\Component\DependencyInjection\ContainerBuilder; +use Symfony\Component\Translation\TranslatorBagInterface; + +/** + * @author Christian Flothmann + */ +class DataCollectorTranslatorPass implements CompilerPassInterface +{ + public function process(ContainerBuilder $container): void + { + if (!$container->has('translator')) { + return; + } + + $translatorClass = $container->getParameterBag()->resolveValue($container->findDefinition('translator')->getClass()); + + if (!is_subclass_of($translatorClass, TranslatorBagInterface::class)) { + $container->removeDefinition('translator.data_collector'); + $container->removeDefinition('data_collector.translation'); + } + } +} diff --git a/3rdparty/symfony/translation/DependencyInjection/LoggingTranslatorPass.php b/3rdparty/symfony/translation/DependencyInjection/LoggingTranslatorPass.php new file mode 100644 index 00000000..c21552f9 --- /dev/null +++ b/3rdparty/symfony/translation/DependencyInjection/LoggingTranslatorPass.php @@ -0,0 +1,59 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Component\Translation\DependencyInjection; + +use Symfony\Component\DependencyInjection\Compiler\CompilerPassInterface; +use Symfony\Component\DependencyInjection\ContainerBuilder; +use Symfony\Component\DependencyInjection\Exception\InvalidArgumentException; +use Symfony\Component\Translation\TranslatorBagInterface; +use Symfony\Contracts\Translation\TranslatorInterface; + +/** + * @author Abdellatif Ait boudad + */ +class LoggingTranslatorPass implements CompilerPassInterface +{ + public function process(ContainerBuilder $container): void + { + if (!$container->hasAlias('logger') || !$container->hasAlias('translator')) { + return; + } + + if (!$container->hasParameter('translator.logging') || !$container->getParameter('translator.logging')) { + return; + } + + $translatorAlias = $container->getAlias('translator'); + $definition = $container->getDefinition((string) $translatorAlias); + $class = $container->getParameterBag()->resolveValue($definition->getClass()); + + if (!$r = $container->getReflectionClass($class)) { + throw new InvalidArgumentException(sprintf('Class "%s" used for service "%s" cannot be found.', $class, $translatorAlias)); + } + + if (!$r->isSubclassOf(TranslatorInterface::class) || !$r->isSubclassOf(TranslatorBagInterface::class)) { + return; + } + + $container->getDefinition('translator.logging')->setDecoratedService('translator'); + $warmer = $container->getDefinition('translation.warmer'); + $subscriberAttributes = $warmer->getTag('container.service_subscriber'); + $warmer->clearTag('container.service_subscriber'); + + foreach ($subscriberAttributes as $k => $v) { + if ((!isset($v['id']) || 'translator' !== $v['id']) && (!isset($v['key']) || 'translator' !== $v['key'])) { + $warmer->addTag('container.service_subscriber', $v); + } + } + $warmer->addTag('container.service_subscriber', ['key' => 'translator', 'id' => 'translator.logging.inner']); + } +} diff --git a/3rdparty/symfony/translation/DependencyInjection/TranslationDumperPass.php b/3rdparty/symfony/translation/DependencyInjection/TranslationDumperPass.php new file mode 100644 index 00000000..2ece6ac7 --- /dev/null +++ b/3rdparty/symfony/translation/DependencyInjection/TranslationDumperPass.php @@ -0,0 +1,38 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Component\Translation\DependencyInjection; + +use Symfony\Component\DependencyInjection\Compiler\CompilerPassInterface; +use Symfony\Component\DependencyInjection\ContainerBuilder; +use Symfony\Component\DependencyInjection\Reference; + +/** + * Adds tagged translation.formatter services to translation writer. + */ +class TranslationDumperPass implements CompilerPassInterface +{ + /** + * @return void + */ + public function process(ContainerBuilder $container) + { + if (!$container->hasDefinition('translation.writer')) { + return; + } + + $definition = $container->getDefinition('translation.writer'); + + foreach ($container->findTaggedServiceIds('translation.dumper', true) as $id => $attributes) { + $definition->addMethodCall('addDumper', [$attributes[0]['alias'], new Reference($id)]); + } + } +} diff --git a/3rdparty/symfony/translation/DependencyInjection/TranslationExtractorPass.php b/3rdparty/symfony/translation/DependencyInjection/TranslationExtractorPass.php new file mode 100644 index 00000000..1baf9341 --- /dev/null +++ b/3rdparty/symfony/translation/DependencyInjection/TranslationExtractorPass.php @@ -0,0 +1,43 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Component\Translation\DependencyInjection; + +use Symfony\Component\DependencyInjection\Compiler\CompilerPassInterface; +use Symfony\Component\DependencyInjection\ContainerBuilder; +use Symfony\Component\DependencyInjection\Exception\RuntimeException; +use Symfony\Component\DependencyInjection\Reference; + +/** + * Adds tagged translation.extractor services to translation extractor. + */ +class TranslationExtractorPass implements CompilerPassInterface +{ + /** + * @return void + */ + public function process(ContainerBuilder $container) + { + if (!$container->hasDefinition('translation.extractor')) { + return; + } + + $definition = $container->getDefinition('translation.extractor'); + + foreach ($container->findTaggedServiceIds('translation.extractor', true) as $id => $attributes) { + if (!isset($attributes[0]['alias'])) { + throw new RuntimeException(sprintf('The alias for the tag "translation.extractor" of service "%s" must be set.', $id)); + } + + $definition->addMethodCall('addExtractor', [$attributes[0]['alias'], new Reference($id)]); + } + } +} diff --git a/3rdparty/symfony/translation/DependencyInjection/TranslatorPass.php b/3rdparty/symfony/translation/DependencyInjection/TranslatorPass.php new file mode 100644 index 00000000..dd6ea3c8 --- /dev/null +++ b/3rdparty/symfony/translation/DependencyInjection/TranslatorPass.php @@ -0,0 +1,94 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Component\Translation\DependencyInjection; + +use Symfony\Component\DependencyInjection\Compiler\CompilerPassInterface; +use Symfony\Component\DependencyInjection\Compiler\ServiceLocatorTagPass; +use Symfony\Component\DependencyInjection\ContainerBuilder; +use Symfony\Component\DependencyInjection\Reference; + +class TranslatorPass implements CompilerPassInterface +{ + /** + * @return void + */ + public function process(ContainerBuilder $container) + { + if (!$container->hasDefinition('translator.default')) { + return; + } + + $loaders = []; + $loaderRefs = []; + foreach ($container->findTaggedServiceIds('translation.loader', true) as $id => $attributes) { + $loaderRefs[$id] = new Reference($id); + $loaders[$id][] = $attributes[0]['alias']; + if (isset($attributes[0]['legacy-alias'])) { + $loaders[$id][] = $attributes[0]['legacy-alias']; + } + } + + if ($container->hasDefinition('translation.reader')) { + $definition = $container->getDefinition('translation.reader'); + foreach ($loaders as $id => $formats) { + foreach ($formats as $format) { + $definition->addMethodCall('addLoader', [$format, $loaderRefs[$id]]); + } + } + } + + $container + ->findDefinition('translator.default') + ->replaceArgument(0, ServiceLocatorTagPass::register($container, $loaderRefs)) + ->replaceArgument(3, $loaders) + ; + + if ($container->hasDefinition('validator') && $container->hasDefinition('translation.extractor.visitor.constraint')) { + $constraintVisitorDefinition = $container->getDefinition('translation.extractor.visitor.constraint'); + $constraintClassNames = []; + + foreach ($container->getDefinitions() as $definition) { + if (!$definition->hasTag('validator.constraint_validator')) { + continue; + } + // Resolve constraint validator FQCN even if defined as %foo.validator.class% parameter + $className = $container->getParameterBag()->resolveValue($definition->getClass()); + // Extraction of the constraint class name from the Constraint Validator FQCN + $constraintClassNames[] = str_replace('Validator', '', substr(strrchr($className, '\\'), 1)); + } + + $constraintVisitorDefinition->setArgument(0, $constraintClassNames); + } + + if (!$container->hasParameter('twig.default_path')) { + return; + } + + $paths = array_keys($container->getDefinition('twig.template_iterator')->getArgument(1)); + if ($container->hasDefinition('console.command.translation_debug')) { + $definition = $container->getDefinition('console.command.translation_debug'); + $definition->replaceArgument(4, $container->getParameter('twig.default_path')); + + if (\count($definition->getArguments()) > 6) { + $definition->replaceArgument(6, $paths); + } + } + if ($container->hasDefinition('console.command.translation_extract')) { + $definition = $container->getDefinition('console.command.translation_extract'); + $definition->replaceArgument(5, $container->getParameter('twig.default_path')); + + if (\count($definition->getArguments()) > 7) { + $definition->replaceArgument(7, $paths); + } + } + } +} diff --git a/3rdparty/symfony/translation/DependencyInjection/TranslatorPathsPass.php b/3rdparty/symfony/translation/DependencyInjection/TranslatorPathsPass.php new file mode 100644 index 00000000..1756e3c8 --- /dev/null +++ b/3rdparty/symfony/translation/DependencyInjection/TranslatorPathsPass.php @@ -0,0 +1,145 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Component\Translation\DependencyInjection; + +use Symfony\Component\DependencyInjection\Compiler\AbstractRecursivePass; +use Symfony\Component\DependencyInjection\ContainerBuilder; +use Symfony\Component\DependencyInjection\Definition; +use Symfony\Component\DependencyInjection\Reference; +use Symfony\Component\DependencyInjection\ServiceLocator; +use Symfony\Component\HttpKernel\Controller\ArgumentResolver\TraceableValueResolver; + +/** + * @author Yonel Ceruto + */ +class TranslatorPathsPass extends AbstractRecursivePass +{ + protected bool $skipScalars = true; + + private int $level = 0; + + /** + * @var array + */ + private array $paths = []; + + /** + * @var array + */ + private array $definitions = []; + + /** + * @var array> + */ + private array $controllers = []; + + /** + * @return void + */ + public function process(ContainerBuilder $container) + { + if (!$container->hasDefinition('translator')) { + return; + } + + foreach ($this->findControllerArguments($container) as $controller => $argument) { + $id = substr($controller, 0, strpos($controller, ':') ?: \strlen($controller)); + if ($container->hasDefinition($id)) { + [$locatorRef] = $argument->getValues(); + $this->controllers[(string) $locatorRef][$container->getDefinition($id)->getClass()] = true; + } + } + + try { + parent::process($container); + + $paths = []; + foreach ($this->paths as $class => $_) { + if (($r = $container->getReflectionClass($class)) && !$r->isInterface()) { + $paths[] = $r->getFileName(); + foreach ($r->getTraits() as $trait) { + $paths[] = $trait->getFileName(); + } + } + } + if ($paths) { + if ($container->hasDefinition('console.command.translation_debug')) { + $definition = $container->getDefinition('console.command.translation_debug'); + $definition->replaceArgument(6, array_merge($definition->getArgument(6), $paths)); + } + if ($container->hasDefinition('console.command.translation_extract')) { + $definition = $container->getDefinition('console.command.translation_extract'); + $definition->replaceArgument(7, array_merge($definition->getArgument(7), $paths)); + } + } + } finally { + $this->level = 0; + $this->paths = []; + $this->definitions = []; + } + } + + protected function processValue(mixed $value, bool $isRoot = false): mixed + { + if ($value instanceof Reference) { + if ('translator' === (string) $value) { + for ($i = $this->level - 1; $i >= 0; --$i) { + $class = $this->definitions[$i]->getClass(); + + if (ServiceLocator::class === $class) { + if (!isset($this->controllers[$this->currentId])) { + continue; + } + foreach ($this->controllers[$this->currentId] as $class => $_) { + $this->paths[$class] = true; + } + } else { + $this->paths[$class] = true; + } + + break; + } + } + + return $value; + } + + if ($value instanceof Definition) { + $this->definitions[$this->level++] = $value; + $value = parent::processValue($value, $isRoot); + unset($this->definitions[--$this->level]); + + return $value; + } + + return parent::processValue($value, $isRoot); + } + + private function findControllerArguments(ContainerBuilder $container): array + { + if (!$container->has('argument_resolver.service')) { + return []; + } + $resolverDef = $container->findDefinition('argument_resolver.service'); + + if (TraceableValueResolver::class === $resolverDef->getClass()) { + $resolverDef = $container->getDefinition($resolverDef->getArgument(0)); + } + + $argument = $resolverDef->getArgument(0); + if ($argument instanceof Reference) { + $argument = $container->getDefinition($argument); + } + + return $argument->getArgument(0); + } +} diff --git a/3rdparty/symfony/translation/Dumper/CsvFileDumper.php b/3rdparty/symfony/translation/Dumper/CsvFileDumper.php new file mode 100644 index 00000000..8f547525 --- /dev/null +++ b/3rdparty/symfony/translation/Dumper/CsvFileDumper.php @@ -0,0 +1,56 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Component\Translation\Dumper; + +use Symfony\Component\Translation\MessageCatalogue; + +/** + * CsvFileDumper generates a csv formatted string representation of a message catalogue. + * + * @author Stealth35 + */ +class CsvFileDumper extends FileDumper +{ + private string $delimiter = ';'; + private string $enclosure = '"'; + + public function formatCatalogue(MessageCatalogue $messages, string $domain, array $options = []): string + { + $handle = fopen('php://memory', 'r+'); + + foreach ($messages->all($domain) as $source => $target) { + fputcsv($handle, [$source, $target], $this->delimiter, $this->enclosure); + } + + rewind($handle); + $output = stream_get_contents($handle); + fclose($handle); + + return $output; + } + + /** + * Sets the delimiter and escape character for CSV. + * + * @return void + */ + public function setCsvControl(string $delimiter = ';', string $enclosure = '"') + { + $this->delimiter = $delimiter; + $this->enclosure = $enclosure; + } + + protected function getExtension(): string + { + return 'csv'; + } +} diff --git a/3rdparty/symfony/translation/Dumper/DumperInterface.php b/3rdparty/symfony/translation/Dumper/DumperInterface.php new file mode 100644 index 00000000..6bf42931 --- /dev/null +++ b/3rdparty/symfony/translation/Dumper/DumperInterface.php @@ -0,0 +1,32 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Component\Translation\Dumper; + +use Symfony\Component\Translation\MessageCatalogue; + +/** + * DumperInterface is the interface implemented by all translation dumpers. + * There is no common option. + * + * @author Michel Salib + */ +interface DumperInterface +{ + /** + * Dumps the message catalogue. + * + * @param array $options Options that are used by the dumper + * + * @return void + */ + public function dump(MessageCatalogue $messages, array $options = []); +} diff --git a/3rdparty/symfony/translation/Dumper/FileDumper.php b/3rdparty/symfony/translation/Dumper/FileDumper.php new file mode 100644 index 00000000..e30d4770 --- /dev/null +++ b/3rdparty/symfony/translation/Dumper/FileDumper.php @@ -0,0 +1,108 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Component\Translation\Dumper; + +use Symfony\Component\Translation\Exception\InvalidArgumentException; +use Symfony\Component\Translation\Exception\RuntimeException; +use Symfony\Component\Translation\MessageCatalogue; + +/** + * FileDumper is an implementation of DumperInterface that dump a message catalogue to file(s). + * + * Options: + * - path (mandatory): the directory where the files should be saved + * + * @author Michel Salib + */ +abstract class FileDumper implements DumperInterface +{ + /** + * A template for the relative paths to files. + * + * @var string + */ + protected $relativePathTemplate = '%domain%.%locale%.%extension%'; + + /** + * Sets the template for the relative paths to files. + * + * @return void + */ + public function setRelativePathTemplate(string $relativePathTemplate) + { + $this->relativePathTemplate = $relativePathTemplate; + } + + /** + * @return void + */ + public function dump(MessageCatalogue $messages, array $options = []) + { + if (!\array_key_exists('path', $options)) { + throw new InvalidArgumentException('The file dumper needs a path option.'); + } + + // save a file for each domain + foreach ($messages->getDomains() as $domain) { + $fullpath = $options['path'].'/'.$this->getRelativePath($domain, $messages->getLocale()); + if (!file_exists($fullpath)) { + $directory = \dirname($fullpath); + if (!file_exists($directory) && !@mkdir($directory, 0777, true)) { + throw new RuntimeException(sprintf('Unable to create directory "%s".', $directory)); + } + } + + $intlDomain = $domain.MessageCatalogue::INTL_DOMAIN_SUFFIX; + $intlMessages = $messages->all($intlDomain); + + if ($intlMessages) { + $intlPath = $options['path'].'/'.$this->getRelativePath($intlDomain, $messages->getLocale()); + file_put_contents($intlPath, $this->formatCatalogue($messages, $intlDomain, $options)); + + $messages->replace([], $intlDomain); + + try { + if ($messages->all($domain)) { + file_put_contents($fullpath, $this->formatCatalogue($messages, $domain, $options)); + } + continue; + } finally { + $messages->replace($intlMessages, $intlDomain); + } + } + + file_put_contents($fullpath, $this->formatCatalogue($messages, $domain, $options)); + } + } + + /** + * Transforms a domain of a message catalogue to its string representation. + */ + abstract public function formatCatalogue(MessageCatalogue $messages, string $domain, array $options = []): string; + + /** + * Gets the file extension of the dumper. + */ + abstract protected function getExtension(): string; + + /** + * Gets the relative file path using the template. + */ + private function getRelativePath(string $domain, string $locale): string + { + return strtr($this->relativePathTemplate, [ + '%domain%' => $domain, + '%locale%' => $locale, + '%extension%' => $this->getExtension(), + ]); + } +} diff --git a/3rdparty/symfony/translation/Dumper/IcuResFileDumper.php b/3rdparty/symfony/translation/Dumper/IcuResFileDumper.php new file mode 100644 index 00000000..72c1ec08 --- /dev/null +++ b/3rdparty/symfony/translation/Dumper/IcuResFileDumper.php @@ -0,0 +1,95 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Component\Translation\Dumper; + +use Symfony\Component\Translation\MessageCatalogue; + +/** + * IcuResDumper generates an ICU ResourceBundle formatted string representation of a message catalogue. + * + * @author Stealth35 + */ +class IcuResFileDumper extends FileDumper +{ + protected $relativePathTemplate = '%domain%/%locale%.%extension%'; + + public function formatCatalogue(MessageCatalogue $messages, string $domain, array $options = []): string + { + $data = $indexes = $resources = ''; + + foreach ($messages->all($domain) as $source => $target) { + $indexes .= pack('v', \strlen($data) + 28); + $data .= $source."\0"; + } + + $data .= $this->writePadding($data); + + $keyTop = $this->getPosition($data); + + foreach ($messages->all($domain) as $source => $target) { + $resources .= pack('V', $this->getPosition($data)); + + $data .= pack('V', \strlen($target)) + .mb_convert_encoding($target."\0", 'UTF-16LE', 'UTF-8') + .$this->writePadding($data) + ; + } + + $resOffset = $this->getPosition($data); + + $data .= pack('v', \count($messages->all($domain))) + .$indexes + .$this->writePadding($data) + .$resources + ; + + $bundleTop = $this->getPosition($data); + + $root = pack('V7', + $resOffset + (2 << 28), // Resource Offset + Resource Type + 6, // Index length + $keyTop, // Index keys top + $bundleTop, // Index resources top + $bundleTop, // Index bundle top + \count($messages->all($domain)), // Index max table length + 0 // Index attributes + ); + + $header = pack('vC2v4C12@32', + 32, // Header size + 0xDA, 0x27, // Magic number 1 and 2 + 20, 0, 0, 2, // Rest of the header, ..., Size of a char + 0x52, 0x65, 0x73, 0x42, // Data format identifier + 1, 2, 0, 0, // Data version + 1, 4, 0, 0 // Unicode version + ); + + return $header.$root.$data; + } + + private function writePadding(string $data): ?string + { + $padding = \strlen($data) % 4; + + return $padding ? str_repeat("\xAA", 4 - $padding) : null; + } + + private function getPosition(string $data): float|int + { + return (\strlen($data) + 28) / 4; + } + + protected function getExtension(): string + { + return 'res'; + } +} diff --git a/3rdparty/symfony/translation/Dumper/IniFileDumper.php b/3rdparty/symfony/translation/Dumper/IniFileDumper.php new file mode 100644 index 00000000..6cbdef60 --- /dev/null +++ b/3rdparty/symfony/translation/Dumper/IniFileDumper.php @@ -0,0 +1,39 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Component\Translation\Dumper; + +use Symfony\Component\Translation\MessageCatalogue; + +/** + * IniFileDumper generates an ini formatted string representation of a message catalogue. + * + * @author Stealth35 + */ +class IniFileDumper extends FileDumper +{ + public function formatCatalogue(MessageCatalogue $messages, string $domain, array $options = []): string + { + $output = ''; + + foreach ($messages->all($domain) as $source => $target) { + $escapeTarget = str_replace('"', '\"', $target); + $output .= $source.'="'.$escapeTarget."\"\n"; + } + + return $output; + } + + protected function getExtension(): string + { + return 'ini'; + } +} diff --git a/3rdparty/symfony/translation/Dumper/JsonFileDumper.php b/3rdparty/symfony/translation/Dumper/JsonFileDumper.php new file mode 100644 index 00000000..e5035397 --- /dev/null +++ b/3rdparty/symfony/translation/Dumper/JsonFileDumper.php @@ -0,0 +1,34 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Component\Translation\Dumper; + +use Symfony\Component\Translation\MessageCatalogue; + +/** + * JsonFileDumper generates an json formatted string representation of a message catalogue. + * + * @author singles + */ +class JsonFileDumper extends FileDumper +{ + public function formatCatalogue(MessageCatalogue $messages, string $domain, array $options = []): string + { + $flags = $options['json_encoding'] ?? \JSON_PRETTY_PRINT; + + return json_encode($messages->all($domain), $flags); + } + + protected function getExtension(): string + { + return 'json'; + } +} diff --git a/3rdparty/symfony/translation/Dumper/MoFileDumper.php b/3rdparty/symfony/translation/Dumper/MoFileDumper.php new file mode 100644 index 00000000..9ded5f4e --- /dev/null +++ b/3rdparty/symfony/translation/Dumper/MoFileDumper.php @@ -0,0 +1,76 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Component\Translation\Dumper; + +use Symfony\Component\Translation\Loader\MoFileLoader; +use Symfony\Component\Translation\MessageCatalogue; + +/** + * MoFileDumper generates a gettext formatted string representation of a message catalogue. + * + * @author Stealth35 + */ +class MoFileDumper extends FileDumper +{ + public function formatCatalogue(MessageCatalogue $messages, string $domain, array $options = []): string + { + $sources = $targets = $sourceOffsets = $targetOffsets = ''; + $offsets = []; + $size = 0; + + foreach ($messages->all($domain) as $source => $target) { + $offsets[] = array_map('strlen', [$sources, $source, $targets, $target]); + $sources .= "\0".$source; + $targets .= "\0".$target; + ++$size; + } + + $header = [ + 'magicNumber' => MoFileLoader::MO_LITTLE_ENDIAN_MAGIC, + 'formatRevision' => 0, + 'count' => $size, + 'offsetId' => MoFileLoader::MO_HEADER_SIZE, + 'offsetTranslated' => MoFileLoader::MO_HEADER_SIZE + (8 * $size), + 'sizeHashes' => 0, + 'offsetHashes' => MoFileLoader::MO_HEADER_SIZE + (16 * $size), + ]; + + $sourcesSize = \strlen($sources); + $sourcesStart = $header['offsetHashes'] + 1; + + foreach ($offsets as $offset) { + $sourceOffsets .= $this->writeLong($offset[1]) + .$this->writeLong($offset[0] + $sourcesStart); + $targetOffsets .= $this->writeLong($offset[3]) + .$this->writeLong($offset[2] + $sourcesStart + $sourcesSize); + } + + $output = implode('', array_map($this->writeLong(...), $header)) + .$sourceOffsets + .$targetOffsets + .$sources + .$targets + ; + + return $output; + } + + protected function getExtension(): string + { + return 'mo'; + } + + private function writeLong(mixed $str): string + { + return pack('V*', $str); + } +} diff --git a/3rdparty/symfony/translation/Dumper/PhpFileDumper.php b/3rdparty/symfony/translation/Dumper/PhpFileDumper.php new file mode 100644 index 00000000..51e90665 --- /dev/null +++ b/3rdparty/symfony/translation/Dumper/PhpFileDumper.php @@ -0,0 +1,32 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Component\Translation\Dumper; + +use Symfony\Component\Translation\MessageCatalogue; + +/** + * PhpFileDumper generates PHP files from a message catalogue. + * + * @author Michel Salib + */ +class PhpFileDumper extends FileDumper +{ + public function formatCatalogue(MessageCatalogue $messages, string $domain, array $options = []): string + { + return "all($domain), true).";\n"; + } + + protected function getExtension(): string + { + return 'php'; + } +} diff --git a/3rdparty/symfony/translation/Dumper/PoFileDumper.php b/3rdparty/symfony/translation/Dumper/PoFileDumper.php new file mode 100644 index 00000000..a2d0deb7 --- /dev/null +++ b/3rdparty/symfony/translation/Dumper/PoFileDumper.php @@ -0,0 +1,131 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Component\Translation\Dumper; + +use Symfony\Component\Translation\MessageCatalogue; + +/** + * PoFileDumper generates a gettext formatted string representation of a message catalogue. + * + * @author Stealth35 + */ +class PoFileDumper extends FileDumper +{ + public function formatCatalogue(MessageCatalogue $messages, string $domain, array $options = []): string + { + $output = 'msgid ""'."\n"; + $output .= 'msgstr ""'."\n"; + $output .= '"Content-Type: text/plain; charset=UTF-8\n"'."\n"; + $output .= '"Content-Transfer-Encoding: 8bit\n"'."\n"; + $output .= '"Language: '.$messages->getLocale().'\n"'."\n"; + $output .= "\n"; + + $newLine = false; + foreach ($messages->all($domain) as $source => $target) { + if ($newLine) { + $output .= "\n"; + } else { + $newLine = true; + } + $metadata = $messages->getMetadata($source, $domain); + + if (isset($metadata['comments'])) { + $output .= $this->formatComments($metadata['comments']); + } + if (isset($metadata['flags'])) { + $output .= $this->formatComments(implode(',', (array) $metadata['flags']), ','); + } + if (isset($metadata['sources'])) { + $output .= $this->formatComments(implode(' ', (array) $metadata['sources']), ':'); + } + + $sourceRules = $this->getStandardRules($source); + $targetRules = $this->getStandardRules($target); + if (2 == \count($sourceRules) && [] !== $targetRules) { + $output .= sprintf('msgid "%s"'."\n", $this->escape($sourceRules[0])); + $output .= sprintf('msgid_plural "%s"'."\n", $this->escape($sourceRules[1])); + foreach ($targetRules as $i => $targetRule) { + $output .= sprintf('msgstr[%d] "%s"'."\n", $i, $this->escape($targetRule)); + } + } else { + $output .= sprintf('msgid "%s"'."\n", $this->escape($source)); + $output .= sprintf('msgstr "%s"'."\n", $this->escape($target)); + } + } + + return $output; + } + + private function getStandardRules(string $id): array + { + // Partly copied from TranslatorTrait::trans. + $parts = []; + if (preg_match('/^\|++$/', $id)) { + $parts = explode('|', $id); + } elseif (preg_match_all('/(?:\|\||[^\|])++/', $id, $matches)) { + $parts = $matches[0]; + } + + $intervalRegexp = <<<'EOF' +/^(?P + ({\s* + (\-?\d+(\.\d+)?[\s*,\s*\-?\d+(\.\d+)?]*) + \s*}) + + | + + (?P[\[\]]) + \s* + (?P-Inf|\-?\d+(\.\d+)?) + \s*,\s* + (?P\+?Inf|\-?\d+(\.\d+)?) + \s* + (?P[\[\]]) +)\s*(?P.*?)$/xs +EOF; + + $standardRules = []; + foreach ($parts as $part) { + $part = trim(str_replace('||', '|', $part)); + + if (preg_match($intervalRegexp, $part)) { + // Explicit rule is not a standard rule. + return []; + } else { + $standardRules[] = $part; + } + } + + return $standardRules; + } + + protected function getExtension(): string + { + return 'po'; + } + + private function escape(string $str): string + { + return addcslashes($str, "\0..\37\42\134"); + } + + private function formatComments(string|array $comments, string $prefix = ''): ?string + { + $output = null; + + foreach ((array) $comments as $comment) { + $output .= sprintf('#%s %s'."\n", $prefix, $comment); + } + + return $output; + } +} diff --git a/3rdparty/symfony/translation/Dumper/QtFileDumper.php b/3rdparty/symfony/translation/Dumper/QtFileDumper.php new file mode 100644 index 00000000..0373e9c1 --- /dev/null +++ b/3rdparty/symfony/translation/Dumper/QtFileDumper.php @@ -0,0 +1,55 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Component\Translation\Dumper; + +use Symfony\Component\Translation\MessageCatalogue; + +/** + * QtFileDumper generates ts files from a message catalogue. + * + * @author Benjamin Eberlei + */ +class QtFileDumper extends FileDumper +{ + public function formatCatalogue(MessageCatalogue $messages, string $domain, array $options = []): string + { + $dom = new \DOMDocument('1.0', 'utf-8'); + $dom->formatOutput = true; + $ts = $dom->appendChild($dom->createElement('TS')); + $context = $ts->appendChild($dom->createElement('context')); + $context->appendChild($dom->createElement('name', $domain)); + + foreach ($messages->all($domain) as $source => $target) { + $message = $context->appendChild($dom->createElement('message')); + $metadata = $messages->getMetadata($source, $domain); + if (isset($metadata['sources'])) { + foreach ((array) $metadata['sources'] as $location) { + $loc = explode(':', $location, 2); + $location = $message->appendChild($dom->createElement('location')); + $location->setAttribute('filename', $loc[0]); + if (isset($loc[1])) { + $location->setAttribute('line', $loc[1]); + } + } + } + $message->appendChild($dom->createElement('source', $source)); + $message->appendChild($dom->createElement('translation', $target)); + } + + return $dom->saveXML(); + } + + protected function getExtension(): string + { + return 'ts'; + } +} diff --git a/3rdparty/symfony/translation/Dumper/XliffFileDumper.php b/3rdparty/symfony/translation/Dumper/XliffFileDumper.php new file mode 100644 index 00000000..d0f016b2 --- /dev/null +++ b/3rdparty/symfony/translation/Dumper/XliffFileDumper.php @@ -0,0 +1,221 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Component\Translation\Dumper; + +use Symfony\Component\Translation\Exception\InvalidArgumentException; +use Symfony\Component\Translation\MessageCatalogue; + +/** + * XliffFileDumper generates xliff files from a message catalogue. + * + * @author Michel Salib + */ +class XliffFileDumper extends FileDumper +{ + public function __construct( + private string $extension = 'xlf', + ) { + } + + public function formatCatalogue(MessageCatalogue $messages, string $domain, array $options = []): string + { + $xliffVersion = '1.2'; + if (\array_key_exists('xliff_version', $options)) { + $xliffVersion = $options['xliff_version']; + } + + if (\array_key_exists('default_locale', $options)) { + $defaultLocale = $options['default_locale']; + } else { + $defaultLocale = \Locale::getDefault(); + } + + if ('1.2' === $xliffVersion) { + return $this->dumpXliff1($defaultLocale, $messages, $domain, $options); + } + if ('2.0' === $xliffVersion) { + return $this->dumpXliff2($defaultLocale, $messages, $domain); + } + + throw new InvalidArgumentException(sprintf('No support implemented for dumping XLIFF version "%s".', $xliffVersion)); + } + + protected function getExtension(): string + { + return $this->extension; + } + + private function dumpXliff1(string $defaultLocale, MessageCatalogue $messages, ?string $domain, array $options = []): string + { + $toolInfo = ['tool-id' => 'symfony', 'tool-name' => 'Symfony']; + if (\array_key_exists('tool_info', $options)) { + $toolInfo = array_merge($toolInfo, $options['tool_info']); + } + + $dom = new \DOMDocument('1.0', 'utf-8'); + $dom->formatOutput = true; + + $xliff = $dom->appendChild($dom->createElement('xliff')); + $xliff->setAttribute('version', '1.2'); + $xliff->setAttribute('xmlns', 'urn:oasis:names:tc:xliff:document:1.2'); + + $xliffFile = $xliff->appendChild($dom->createElement('file')); + $xliffFile->setAttribute('source-language', str_replace('_', '-', $defaultLocale)); + $xliffFile->setAttribute('target-language', str_replace('_', '-', $messages->getLocale())); + $xliffFile->setAttribute('datatype', 'plaintext'); + $xliffFile->setAttribute('original', 'file.ext'); + + $xliffHead = $xliffFile->appendChild($dom->createElement('header')); + $xliffTool = $xliffHead->appendChild($dom->createElement('tool')); + foreach ($toolInfo as $id => $value) { + $xliffTool->setAttribute($id, $value); + } + + if ($catalogueMetadata = $messages->getCatalogueMetadata('', $domain) ?? []) { + $xliffPropGroup = $xliffHead->appendChild($dom->createElement('prop-group')); + foreach ($catalogueMetadata as $key => $value) { + $xliffProp = $xliffPropGroup->appendChild($dom->createElement('prop')); + $xliffProp->setAttribute('prop-type', $key); + $xliffProp->appendChild($dom->createTextNode($value)); + } + } + + $xliffBody = $xliffFile->appendChild($dom->createElement('body')); + foreach ($messages->all($domain) as $source => $target) { + $translation = $dom->createElement('trans-unit'); + + $translation->setAttribute('id', strtr(substr(base64_encode(hash('sha256', $source, true)), 0, 7), '/+', '._')); + $translation->setAttribute('resname', $source); + + $s = $translation->appendChild($dom->createElement('source')); + $s->appendChild($dom->createTextNode($source)); + + // Does the target contain characters requiring a CDATA section? + $text = 1 === preg_match('/[&<>]/', $target) ? $dom->createCDATASection($target) : $dom->createTextNode($target); + + $targetElement = $dom->createElement('target'); + $metadata = $messages->getMetadata($source, $domain); + if ($this->hasMetadataArrayInfo('target-attributes', $metadata)) { + foreach ($metadata['target-attributes'] as $name => $value) { + $targetElement->setAttribute($name, $value); + } + } + $t = $translation->appendChild($targetElement); + $t->appendChild($text); + + if ($this->hasMetadataArrayInfo('notes', $metadata)) { + foreach ($metadata['notes'] as $note) { + if (!isset($note['content'])) { + continue; + } + + $n = $translation->appendChild($dom->createElement('note')); + $n->appendChild($dom->createTextNode($note['content'])); + + if (isset($note['priority'])) { + $n->setAttribute('priority', $note['priority']); + } + + if (isset($note['from'])) { + $n->setAttribute('from', $note['from']); + } + } + } + + $xliffBody->appendChild($translation); + } + + return $dom->saveXML(); + } + + private function dumpXliff2(string $defaultLocale, MessageCatalogue $messages, ?string $domain): string + { + $dom = new \DOMDocument('1.0', 'utf-8'); + $dom->formatOutput = true; + + $xliff = $dom->appendChild($dom->createElement('xliff')); + $xliff->setAttribute('xmlns', 'urn:oasis:names:tc:xliff:document:2.0'); + $xliff->setAttribute('version', '2.0'); + $xliff->setAttribute('srcLang', str_replace('_', '-', $defaultLocale)); + $xliff->setAttribute('trgLang', str_replace('_', '-', $messages->getLocale())); + + $xliffFile = $xliff->appendChild($dom->createElement('file')); + if (str_ends_with($domain, MessageCatalogue::INTL_DOMAIN_SUFFIX)) { + $xliffFile->setAttribute('id', substr($domain, 0, -\strlen(MessageCatalogue::INTL_DOMAIN_SUFFIX)).'.'.$messages->getLocale()); + } else { + $xliffFile->setAttribute('id', $domain.'.'.$messages->getLocale()); + } + + if ($catalogueMetadata = $messages->getCatalogueMetadata('', $domain) ?? []) { + $xliff->setAttribute('xmlns:m', 'urn:oasis:names:tc:xliff:metadata:2.0'); + $xliffMetadata = $xliffFile->appendChild($dom->createElement('m:metadata')); + foreach ($catalogueMetadata as $key => $value) { + $xliffMeta = $xliffMetadata->appendChild($dom->createElement('prop')); + $xliffMeta->setAttribute('type', $key); + $xliffMeta->appendChild($dom->createTextNode($value)); + } + } + + foreach ($messages->all($domain) as $source => $target) { + $translation = $dom->createElement('unit'); + $translation->setAttribute('id', strtr(substr(base64_encode(hash('sha256', $source, true)), 0, 7), '/+', '._')); + + if (\strlen($source) <= 80) { + $translation->setAttribute('name', $source); + } + + $metadata = $messages->getMetadata($source, $domain); + + // Add notes section + if ($this->hasMetadataArrayInfo('notes', $metadata)) { + $notesElement = $dom->createElement('notes'); + foreach ($metadata['notes'] as $note) { + $n = $dom->createElement('note'); + $n->appendChild($dom->createTextNode($note['content'] ?? '')); + unset($note['content']); + + foreach ($note as $name => $value) { + $n->setAttribute($name, $value); + } + $notesElement->appendChild($n); + } + $translation->appendChild($notesElement); + } + + $segment = $translation->appendChild($dom->createElement('segment')); + + $s = $segment->appendChild($dom->createElement('source')); + $s->appendChild($dom->createTextNode($source)); + + // Does the target contain characters requiring a CDATA section? + $text = 1 === preg_match('/[&<>]/', $target) ? $dom->createCDATASection($target) : $dom->createTextNode($target); + + $targetElement = $dom->createElement('target'); + if ($this->hasMetadataArrayInfo('target-attributes', $metadata)) { + foreach ($metadata['target-attributes'] as $name => $value) { + $targetElement->setAttribute($name, $value); + } + } + $t = $segment->appendChild($targetElement); + $t->appendChild($text); + + $xliffFile->appendChild($translation); + } + + return $dom->saveXML(); + } + + private function hasMetadataArrayInfo(string $key, ?array $metadata = null): bool + { + return is_iterable($metadata[$key] ?? null); + } +} diff --git a/3rdparty/symfony/translation/Dumper/YamlFileDumper.php b/3rdparty/symfony/translation/Dumper/YamlFileDumper.php new file mode 100644 index 00000000..d2670331 --- /dev/null +++ b/3rdparty/symfony/translation/Dumper/YamlFileDumper.php @@ -0,0 +1,56 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Component\Translation\Dumper; + +use Symfony\Component\Translation\Exception\LogicException; +use Symfony\Component\Translation\MessageCatalogue; +use Symfony\Component\Translation\Util\ArrayConverter; +use Symfony\Component\Yaml\Yaml; + +/** + * YamlFileDumper generates yaml files from a message catalogue. + * + * @author Michel Salib + */ +class YamlFileDumper extends FileDumper +{ + private string $extension; + + public function __construct(string $extension = 'yml') + { + $this->extension = $extension; + } + + public function formatCatalogue(MessageCatalogue $messages, string $domain, array $options = []): string + { + if (!class_exists(Yaml::class)) { + throw new LogicException('Dumping translations in the YAML format requires the Symfony Yaml component.'); + } + + $data = $messages->all($domain); + + if (isset($options['as_tree']) && $options['as_tree']) { + $data = ArrayConverter::expandToTree($data); + } + + if (isset($options['inline']) && ($inline = (int) $options['inline']) > 0) { + return Yaml::dump($data, $inline); + } + + return Yaml::dump($data); + } + + protected function getExtension(): string + { + return $this->extension; + } +} diff --git a/3rdparty/symfony/translation/Exception/ExceptionInterface.php b/3rdparty/symfony/translation/Exception/ExceptionInterface.php new file mode 100644 index 00000000..8f9c54ef --- /dev/null +++ b/3rdparty/symfony/translation/Exception/ExceptionInterface.php @@ -0,0 +1,21 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Component\Translation\Exception; + +/** + * Exception interface for all exceptions thrown by the component. + * + * @author Fabien Potencier + */ +interface ExceptionInterface extends \Throwable +{ +} diff --git a/3rdparty/symfony/translation/Exception/IncompleteDsnException.php b/3rdparty/symfony/translation/Exception/IncompleteDsnException.php new file mode 100644 index 00000000..b304bde0 --- /dev/null +++ b/3rdparty/symfony/translation/Exception/IncompleteDsnException.php @@ -0,0 +1,24 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Component\Translation\Exception; + +class IncompleteDsnException extends InvalidArgumentException +{ + public function __construct(string $message, ?string $dsn = null, ?\Throwable $previous = null) + { + if ($dsn) { + $message = sprintf('Invalid "%s" provider DSN: ', $dsn).$message; + } + + parent::__construct($message, 0, $previous); + } +} diff --git a/3rdparty/symfony/translation/Exception/InvalidArgumentException.php b/3rdparty/symfony/translation/Exception/InvalidArgumentException.php new file mode 100644 index 00000000..90d06690 --- /dev/null +++ b/3rdparty/symfony/translation/Exception/InvalidArgumentException.php @@ -0,0 +1,21 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Component\Translation\Exception; + +/** + * Base InvalidArgumentException for the Translation component. + * + * @author Abdellatif Ait boudad + */ +class InvalidArgumentException extends \InvalidArgumentException implements ExceptionInterface +{ +} diff --git a/3rdparty/symfony/translation/Exception/InvalidResourceException.php b/3rdparty/symfony/translation/Exception/InvalidResourceException.php new file mode 100644 index 00000000..cf079432 --- /dev/null +++ b/3rdparty/symfony/translation/Exception/InvalidResourceException.php @@ -0,0 +1,21 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Component\Translation\Exception; + +/** + * Thrown when a resource cannot be loaded. + * + * @author Fabien Potencier + */ +class InvalidResourceException extends \InvalidArgumentException implements ExceptionInterface +{ +} diff --git a/3rdparty/symfony/translation/Exception/LogicException.php b/3rdparty/symfony/translation/Exception/LogicException.php new file mode 100644 index 00000000..9019c7e7 --- /dev/null +++ b/3rdparty/symfony/translation/Exception/LogicException.php @@ -0,0 +1,21 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Component\Translation\Exception; + +/** + * Base LogicException for Translation component. + * + * @author Abdellatif Ait boudad + */ +class LogicException extends \LogicException implements ExceptionInterface +{ +} diff --git a/3rdparty/symfony/translation/Exception/MissingRequiredOptionException.php b/3rdparty/symfony/translation/Exception/MissingRequiredOptionException.php new file mode 100644 index 00000000..46152e25 --- /dev/null +++ b/3rdparty/symfony/translation/Exception/MissingRequiredOptionException.php @@ -0,0 +1,25 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Component\Translation\Exception; + +/** + * @author Oskar Stark + */ +class MissingRequiredOptionException extends IncompleteDsnException +{ + public function __construct(string $option, ?string $dsn = null, ?\Throwable $previous = null) + { + $message = sprintf('The option "%s" is required but missing.', $option); + + parent::__construct($message, $dsn, $previous); + } +} diff --git a/3rdparty/symfony/translation/Exception/NotFoundResourceException.php b/3rdparty/symfony/translation/Exception/NotFoundResourceException.php new file mode 100644 index 00000000..cff73ae3 --- /dev/null +++ b/3rdparty/symfony/translation/Exception/NotFoundResourceException.php @@ -0,0 +1,21 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Component\Translation\Exception; + +/** + * Thrown when a resource does not exist. + * + * @author Fabien Potencier + */ +class NotFoundResourceException extends \InvalidArgumentException implements ExceptionInterface +{ +} diff --git a/3rdparty/symfony/translation/Exception/ProviderException.php b/3rdparty/symfony/translation/Exception/ProviderException.php new file mode 100644 index 00000000..f2981f58 --- /dev/null +++ b/3rdparty/symfony/translation/Exception/ProviderException.php @@ -0,0 +1,41 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Component\Translation\Exception; + +use Symfony\Contracts\HttpClient\ResponseInterface; + +/** + * @author Fabien Potencier + */ +class ProviderException extends RuntimeException implements ProviderExceptionInterface +{ + private ResponseInterface $response; + private string $debug; + + public function __construct(string $message, ResponseInterface $response, int $code = 0, ?\Exception $previous = null) + { + $this->response = $response; + $this->debug = $response->getInfo('debug') ?? ''; + + parent::__construct($message, $code, $previous); + } + + public function getResponse(): ResponseInterface + { + return $this->response; + } + + public function getDebug(): string + { + return $this->debug; + } +} diff --git a/3rdparty/symfony/translation/Exception/ProviderExceptionInterface.php b/3rdparty/symfony/translation/Exception/ProviderExceptionInterface.php new file mode 100644 index 00000000..922e8272 --- /dev/null +++ b/3rdparty/symfony/translation/Exception/ProviderExceptionInterface.php @@ -0,0 +1,23 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Component\Translation\Exception; + +/** + * @author Fabien Potencier + */ +interface ProviderExceptionInterface extends ExceptionInterface +{ + /* + * Returns debug info coming from the Symfony\Contracts\HttpClient\ResponseInterface + */ + public function getDebug(): string; +} diff --git a/3rdparty/symfony/translation/Exception/RuntimeException.php b/3rdparty/symfony/translation/Exception/RuntimeException.php new file mode 100644 index 00000000..dcd79408 --- /dev/null +++ b/3rdparty/symfony/translation/Exception/RuntimeException.php @@ -0,0 +1,21 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Component\Translation\Exception; + +/** + * Base RuntimeException for the Translation component. + * + * @author Abdellatif Ait boudad + */ +class RuntimeException extends \RuntimeException implements ExceptionInterface +{ +} diff --git a/3rdparty/symfony/translation/Exception/UnsupportedSchemeException.php b/3rdparty/symfony/translation/Exception/UnsupportedSchemeException.php new file mode 100644 index 00000000..8d329518 --- /dev/null +++ b/3rdparty/symfony/translation/Exception/UnsupportedSchemeException.php @@ -0,0 +1,58 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Component\Translation\Exception; + +use Symfony\Component\Translation\Bridge; +use Symfony\Component\Translation\Provider\Dsn; + +class UnsupportedSchemeException extends LogicException +{ + private const SCHEME_TO_PACKAGE_MAP = [ + 'crowdin' => [ + 'class' => Bridge\Crowdin\CrowdinProviderFactory::class, + 'package' => 'symfony/crowdin-translation-provider', + ], + 'loco' => [ + 'class' => Bridge\Loco\LocoProviderFactory::class, + 'package' => 'symfony/loco-translation-provider', + ], + 'lokalise' => [ + 'class' => Bridge\Lokalise\LokaliseProviderFactory::class, + 'package' => 'symfony/lokalise-translation-provider', + ], + 'phrase' => [ + 'class' => Bridge\Phrase\PhraseProviderFactory::class, + 'package' => 'symfony/phrase-translation-provider', + ], + ]; + + public function __construct(Dsn $dsn, ?string $name = null, array $supported = []) + { + $provider = $dsn->getScheme(); + if (false !== $pos = strpos($provider, '+')) { + $provider = substr($provider, 0, $pos); + } + $package = self::SCHEME_TO_PACKAGE_MAP[$provider] ?? null; + if ($package && !class_exists($package['class'])) { + parent::__construct(sprintf('Unable to synchronize translations via "%s" as the provider is not installed. Try running "composer require %s".', $provider, $package['package'])); + + return; + } + + $message = sprintf('The "%s" scheme is not supported', $dsn->getScheme()); + if ($name && $supported) { + $message .= sprintf('; supported schemes for translation provider "%s" are: "%s"', $name, implode('", "', $supported)); + } + + parent::__construct($message.'.'); + } +} diff --git a/3rdparty/symfony/translation/Extractor/AbstractFileExtractor.php b/3rdparty/symfony/translation/Extractor/AbstractFileExtractor.php new file mode 100644 index 00000000..4c088b94 --- /dev/null +++ b/3rdparty/symfony/translation/Extractor/AbstractFileExtractor.php @@ -0,0 +1,67 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Component\Translation\Extractor; + +use Symfony\Component\Translation\Exception\InvalidArgumentException; + +/** + * Base class used by classes that extract translation messages from files. + * + * @author Marcos D. Sánchez + */ +abstract class AbstractFileExtractor +{ + protected function extractFiles(string|iterable $resource): iterable + { + if (is_iterable($resource)) { + $files = []; + foreach ($resource as $file) { + if ($this->canBeExtracted($file)) { + $files[] = $this->toSplFileInfo($file); + } + } + } elseif (is_file($resource)) { + $files = $this->canBeExtracted($resource) ? [$this->toSplFileInfo($resource)] : []; + } else { + $files = $this->extractFromDirectory($resource); + } + + return $files; + } + + private function toSplFileInfo(string $file): \SplFileInfo + { + return new \SplFileInfo($file); + } + + /** + * @throws InvalidArgumentException + */ + protected function isFile(string $file): bool + { + if (!is_file($file)) { + throw new InvalidArgumentException(sprintf('The "%s" file does not exist.', $file)); + } + + return true; + } + + /** + * @return bool + */ + abstract protected function canBeExtracted(string $file); + + /** + * @return iterable + */ + abstract protected function extractFromDirectory(string|array $resource); +} diff --git a/3rdparty/symfony/translation/Extractor/ChainExtractor.php b/3rdparty/symfony/translation/Extractor/ChainExtractor.php new file mode 100644 index 00000000..d36f7f38 --- /dev/null +++ b/3rdparty/symfony/translation/Extractor/ChainExtractor.php @@ -0,0 +1,59 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Component\Translation\Extractor; + +use Symfony\Component\Translation\MessageCatalogue; + +/** + * ChainExtractor extracts translation messages from template files. + * + * @author Michel Salib + */ +class ChainExtractor implements ExtractorInterface +{ + /** + * The extractors. + * + * @var ExtractorInterface[] + */ + private array $extractors = []; + + /** + * Adds a loader to the translation extractor. + * + * @return void + */ + public function addExtractor(string $format, ExtractorInterface $extractor) + { + $this->extractors[$format] = $extractor; + } + + /** + * @return void + */ + public function setPrefix(string $prefix) + { + foreach ($this->extractors as $extractor) { + $extractor->setPrefix($prefix); + } + } + + /** + * @return void + */ + public function extract(string|iterable $directory, MessageCatalogue $catalogue) + { + foreach ($this->extractors as $extractor) { + $extractor->extract($directory, $catalogue); + } + } +} diff --git a/3rdparty/symfony/translation/Extractor/ExtractorInterface.php b/3rdparty/symfony/translation/Extractor/ExtractorInterface.php new file mode 100644 index 00000000..642130af --- /dev/null +++ b/3rdparty/symfony/translation/Extractor/ExtractorInterface.php @@ -0,0 +1,39 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Component\Translation\Extractor; + +use Symfony\Component\Translation\MessageCatalogue; + +/** + * Extracts translation messages from a directory or files to the catalogue. + * New found messages are injected to the catalogue using the prefix. + * + * @author Michel Salib + */ +interface ExtractorInterface +{ + /** + * Extracts translation messages from files, a file or a directory to the catalogue. + * + * @param string|iterable $resource Files, a file or a directory + * + * @return void + */ + public function extract(string|iterable $resource, MessageCatalogue $catalogue); + + /** + * Sets the prefix that should be used for new found messages. + * + * @return void + */ + public function setPrefix(string $prefix); +} diff --git a/3rdparty/symfony/translation/Extractor/PhpAstExtractor.php b/3rdparty/symfony/translation/Extractor/PhpAstExtractor.php new file mode 100644 index 00000000..06fc77de --- /dev/null +++ b/3rdparty/symfony/translation/Extractor/PhpAstExtractor.php @@ -0,0 +1,85 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Component\Translation\Extractor; + +use PhpParser\NodeTraverser; +use PhpParser\NodeVisitor; +use PhpParser\Parser; +use PhpParser\ParserFactory; +use Symfony\Component\Finder\Finder; +use Symfony\Component\Translation\Extractor\Visitor\AbstractVisitor; +use Symfony\Component\Translation\MessageCatalogue; + +/** + * PhpAstExtractor extracts translation messages from a PHP AST. + * + * @author Mathieu Santostefano + */ +final class PhpAstExtractor extends AbstractFileExtractor implements ExtractorInterface +{ + private Parser $parser; + + public function __construct( + /** + * @param iterable $visitors + */ + private readonly iterable $visitors, + private string $prefix = '', + ) { + if (!class_exists(ParserFactory::class)) { + throw new \LogicException(sprintf('You cannot use "%s" as the "nikic/php-parser" package is not installed. Try running "composer require nikic/php-parser".', static::class)); + } + + $this->parser = (new ParserFactory())->createForHostVersion(); + } + + public function extract(iterable|string $resource, MessageCatalogue $catalogue): void + { + foreach ($this->extractFiles($resource) as $file) { + $traverser = new NodeTraverser(); + + // This is needed to resolve namespaces in class methods/constants. + $nameResolver = new NodeVisitor\NameResolver(); + $traverser->addVisitor($nameResolver); + + /** @var AbstractVisitor&NodeVisitor $visitor */ + foreach ($this->visitors as $visitor) { + $visitor->initialize($catalogue, $file, $this->prefix); + $traverser->addVisitor($visitor); + } + + $nodes = $this->parser->parse(file_get_contents($file)); + $traverser->traverse($nodes); + } + } + + public function setPrefix(string $prefix): void + { + $this->prefix = $prefix; + } + + protected function canBeExtracted(string $file): bool + { + return 'php' === pathinfo($file, \PATHINFO_EXTENSION) + && $this->isFile($file) + && preg_match('/\bt\(|->trans\(|TranslatableMessage|Symfony\\\\Component\\\\Validator\\\\Constraints/i', file_get_contents($file)); + } + + protected function extractFromDirectory(array|string $resource): iterable|Finder + { + if (!class_exists(Finder::class)) { + throw new \LogicException(sprintf('You cannot use "%s" as the "symfony/finder" package is not installed. Try running "composer require symfony/finder".', static::class)); + } + + return (new Finder())->files()->name('*.php')->in($resource); + } +} diff --git a/3rdparty/symfony/translation/Extractor/PhpExtractor.php b/3rdparty/symfony/translation/Extractor/PhpExtractor.php new file mode 100644 index 00000000..7ff27f7c --- /dev/null +++ b/3rdparty/symfony/translation/Extractor/PhpExtractor.php @@ -0,0 +1,333 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Component\Translation\Extractor; + +trigger_deprecation('symfony/translation', '6.2', '"%s" is deprecated, use "%s" instead.', PhpExtractor::class, PhpAstExtractor::class); + +use Symfony\Component\Finder\Finder; +use Symfony\Component\Translation\MessageCatalogue; + +/** + * PhpExtractor extracts translation messages from a PHP template. + * + * @author Michel Salib + * + * @deprecated since Symfony 6.2, use the PhpAstExtractor instead + */ +class PhpExtractor extends AbstractFileExtractor implements ExtractorInterface +{ + public const MESSAGE_TOKEN = 300; + public const METHOD_ARGUMENTS_TOKEN = 1000; + public const DOMAIN_TOKEN = 1001; + + /** + * Prefix for new found message. + */ + private string $prefix = ''; + + /** + * The sequence that captures translation messages. + */ + protected $sequences = [ + [ + '->', + 'trans', + '(', + self::MESSAGE_TOKEN, + ',', + self::METHOD_ARGUMENTS_TOKEN, + ',', + self::DOMAIN_TOKEN, + ], + [ + '->', + 'trans', + '(', + self::MESSAGE_TOKEN, + ], + [ + 'new', + 'TranslatableMessage', + '(', + self::MESSAGE_TOKEN, + ',', + self::METHOD_ARGUMENTS_TOKEN, + ',', + self::DOMAIN_TOKEN, + ], + [ + 'new', + 'TranslatableMessage', + '(', + self::MESSAGE_TOKEN, + ], + [ + 'new', + '\\', + 'Symfony', + '\\', + 'Component', + '\\', + 'Translation', + '\\', + 'TranslatableMessage', + '(', + self::MESSAGE_TOKEN, + ',', + self::METHOD_ARGUMENTS_TOKEN, + ',', + self::DOMAIN_TOKEN, + ], + [ + 'new', + '\Symfony\Component\Translation\TranslatableMessage', + '(', + self::MESSAGE_TOKEN, + ',', + self::METHOD_ARGUMENTS_TOKEN, + ',', + self::DOMAIN_TOKEN, + ], + [ + 'new', + '\\', + 'Symfony', + '\\', + 'Component', + '\\', + 'Translation', + '\\', + 'TranslatableMessage', + '(', + self::MESSAGE_TOKEN, + ], + [ + 'new', + '\Symfony\Component\Translation\TranslatableMessage', + '(', + self::MESSAGE_TOKEN, + ], + [ + 't', + '(', + self::MESSAGE_TOKEN, + ',', + self::METHOD_ARGUMENTS_TOKEN, + ',', + self::DOMAIN_TOKEN, + ], + [ + 't', + '(', + self::MESSAGE_TOKEN, + ], + ]; + + /** + * @return void + */ + public function extract(string|iterable $resource, MessageCatalogue $catalog) + { + $files = $this->extractFiles($resource); + foreach ($files as $file) { + $this->parseTokens(token_get_all(file_get_contents($file)), $catalog, $file); + + gc_mem_caches(); + } + } + + /** + * @return void + */ + public function setPrefix(string $prefix) + { + $this->prefix = $prefix; + } + + /** + * Normalizes a token. + */ + protected function normalizeToken(mixed $token): ?string + { + if (isset($token[1]) && 'b"' !== $token) { + return $token[1]; + } + + return $token; + } + + /** + * Seeks to a non-whitespace token. + */ + private function seekToNextRelevantToken(\Iterator $tokenIterator): void + { + for (; $tokenIterator->valid(); $tokenIterator->next()) { + $t = $tokenIterator->current(); + if (\T_WHITESPACE !== $t[0]) { + break; + } + } + } + + private function skipMethodArgument(\Iterator $tokenIterator): void + { + $openBraces = 0; + + for (; $tokenIterator->valid(); $tokenIterator->next()) { + $t = $tokenIterator->current(); + + if ('[' === $t[0] || '(' === $t[0]) { + ++$openBraces; + } + + if (']' === $t[0] || ')' === $t[0]) { + --$openBraces; + } + + if ((0 === $openBraces && ',' === $t[0]) || (-1 === $openBraces && ')' === $t[0])) { + break; + } + } + } + + /** + * Extracts the message from the iterator while the tokens + * match allowed message tokens. + */ + private function getValue(\Iterator $tokenIterator): string + { + $message = ''; + $docToken = ''; + $docPart = ''; + + for (; $tokenIterator->valid(); $tokenIterator->next()) { + $t = $tokenIterator->current(); + if ('.' === $t) { + // Concatenate with next token + continue; + } + if (!isset($t[1])) { + break; + } + + switch ($t[0]) { + case \T_START_HEREDOC: + $docToken = $t[1]; + break; + case \T_ENCAPSED_AND_WHITESPACE: + case \T_CONSTANT_ENCAPSED_STRING: + if ('' === $docToken) { + $message .= PhpStringTokenParser::parse($t[1]); + } else { + $docPart = $t[1]; + } + break; + case \T_END_HEREDOC: + if ($indentation = strspn($t[1], ' ')) { + $docPartWithLineBreaks = $docPart; + $docPart = ''; + + foreach (preg_split('~(\r\n|\n|\r)~', $docPartWithLineBreaks, -1, \PREG_SPLIT_DELIM_CAPTURE) as $str) { + if (\in_array($str, ["\r\n", "\n", "\r"], true)) { + $docPart .= $str; + } else { + $docPart .= substr($str, $indentation); + } + } + } + + $message .= PhpStringTokenParser::parseDocString($docToken, $docPart); + $docToken = ''; + $docPart = ''; + break; + case \T_WHITESPACE: + break; + default: + break 2; + } + } + + return $message; + } + + /** + * Extracts trans message from PHP tokens. + * + * @return void + */ + protected function parseTokens(array $tokens, MessageCatalogue $catalog, string $filename) + { + $tokenIterator = new \ArrayIterator($tokens); + + for ($key = 0; $key < $tokenIterator->count(); ++$key) { + foreach ($this->sequences as $sequence) { + $message = ''; + $domain = 'messages'; + $tokenIterator->seek($key); + + foreach ($sequence as $sequenceKey => $item) { + $this->seekToNextRelevantToken($tokenIterator); + + if ($this->normalizeToken($tokenIterator->current()) === $item) { + $tokenIterator->next(); + continue; + } elseif (self::MESSAGE_TOKEN === $item) { + $message = $this->getValue($tokenIterator); + + if (\count($sequence) === ($sequenceKey + 1)) { + break; + } + } elseif (self::METHOD_ARGUMENTS_TOKEN === $item) { + $this->skipMethodArgument($tokenIterator); + } elseif (self::DOMAIN_TOKEN === $item) { + $domainToken = $this->getValue($tokenIterator); + if ('' !== $domainToken) { + $domain = $domainToken; + } + + break; + } else { + break; + } + } + + if ($message) { + $catalog->set($message, $this->prefix.$message, $domain); + $metadata = $catalog->getMetadata($message, $domain) ?? []; + $normalizedFilename = preg_replace('{[\\\\/]+}', '/', $filename); + $metadata['sources'][] = $normalizedFilename.':'.$tokens[$key][2]; + $catalog->setMetadata($message, $metadata, $domain); + break; + } + } + } + } + + /** + * @throws \InvalidArgumentException + */ + protected function canBeExtracted(string $file): bool + { + return $this->isFile($file) && 'php' === pathinfo($file, \PATHINFO_EXTENSION); + } + + protected function extractFromDirectory(string|array $directory): iterable + { + if (!class_exists(Finder::class)) { + throw new \LogicException(sprintf('You cannot use "%s" as the "symfony/finder" package is not installed. Try running "composer require symfony/finder".', static::class)); + } + + $finder = new Finder(); + + return $finder->files()->name('*.php')->in($directory); + } +} diff --git a/3rdparty/symfony/translation/Extractor/PhpStringTokenParser.php b/3rdparty/symfony/translation/Extractor/PhpStringTokenParser.php new file mode 100644 index 00000000..2dfc1e38 --- /dev/null +++ b/3rdparty/symfony/translation/Extractor/PhpStringTokenParser.php @@ -0,0 +1,141 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Component\Translation\Extractor; + +trigger_deprecation('symfony/translation', '6.2', '"%s" is deprecated.', PhpStringTokenParser::class); + +/* + * The following is derived from code at http://github.com/nikic/PHP-Parser + * + * Copyright (c) 2011 by Nikita Popov + * + * Some rights reserved. + * + * Redistribution and use in source and binary forms, with or without + * modification, are permitted provided that the following conditions are + * met: + * + * * Redistributions of source code must retain the above copyright + * notice, this list of conditions and the following disclaimer. + * + * * Redistributions in binary form must reproduce the above + * copyright notice, this list of conditions and the following + * disclaimer in the documentation and/or other materials provided + * with the distribution. + * + * * The names of the contributors may not be used to endorse or + * promote products derived from this software without specific + * prior written permission. + * + * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS + * "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT + * LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR + * A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT + * OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, + * SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT + * LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, + * DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY + * THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT + * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE + * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + */ + +/** + * @deprecated since Symfony 6.2 + */ +class PhpStringTokenParser +{ + protected static $replacements = [ + '\\' => '\\', + '$' => '$', + 'n' => "\n", + 'r' => "\r", + 't' => "\t", + 'f' => "\f", + 'v' => "\v", + 'e' => "\x1B", + ]; + + /** + * Parses a string token. + * + * @param string $str String token content + */ + public static function parse(string $str): string + { + $bLength = 0; + if ('b' === $str[0]) { + $bLength = 1; + } + + if ('\'' === $str[$bLength]) { + return str_replace( + ['\\\\', '\\\''], + ['\\', '\''], + substr($str, $bLength + 1, -1) + ); + } else { + return self::parseEscapeSequences(substr($str, $bLength + 1, -1), '"'); + } + } + + /** + * Parses escape sequences in strings (all string types apart from single quoted). + * + * @param string $str String without quotes + * @param string|null $quote Quote type + */ + public static function parseEscapeSequences(string $str, ?string $quote = null): string + { + if (null !== $quote) { + $str = str_replace('\\'.$quote, $quote, $str); + } + + return preg_replace_callback( + '~\\\\([\\\\$nrtfve]|[xX][0-9a-fA-F]{1,2}|[0-7]{1,3})~', + [__CLASS__, 'parseCallback'], + $str + ); + } + + private static function parseCallback(array $matches): string + { + $str = $matches[1]; + + if (isset(self::$replacements[$str])) { + return self::$replacements[$str]; + } elseif ('x' === $str[0] || 'X' === $str[0]) { + return \chr(hexdec($str)); + } else { + return \chr(octdec($str)); + } + } + + /** + * Parses a constant doc string. + * + * @param string $startToken Doc string start token content (<< + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Component\Translation\Extractor\Visitor; + +use PhpParser\Node; +use Symfony\Component\Translation\MessageCatalogue; + +/** + * @author Mathieu Santostefano + */ +abstract class AbstractVisitor +{ + private MessageCatalogue $catalogue; + private \SplFileInfo $file; + private string $messagePrefix; + + public function initialize(MessageCatalogue $catalogue, \SplFileInfo $file, string $messagePrefix): void + { + $this->catalogue = $catalogue; + $this->file = $file; + $this->messagePrefix = $messagePrefix; + } + + protected function addMessageToCatalogue(string $message, ?string $domain, int $line): void + { + $domain ??= 'messages'; + $this->catalogue->set($message, $this->messagePrefix.$message, $domain); + $metadata = $this->catalogue->getMetadata($message, $domain) ?? []; + $normalizedFilename = preg_replace('{[\\\\/]+}', '/', $this->file); + $metadata['sources'][] = $normalizedFilename.':'.$line; + $this->catalogue->setMetadata($message, $metadata, $domain); + } + + protected function getStringArguments(Node\Expr\CallLike|Node\Attribute|Node\Expr\New_ $node, int|string $index, bool $indexIsRegex = false): array + { + if (\is_string($index)) { + return $this->getStringNamedArguments($node, $index, $indexIsRegex); + } + + $args = $node instanceof Node\Expr\CallLike ? $node->getRawArgs() : $node->args; + + if (!($arg = $args[$index] ?? null) instanceof Node\Arg) { + return []; + } + + return (array) $this->getStringValue($arg->value); + } + + protected function hasNodeNamedArguments(Node\Expr\CallLike|Node\Attribute|Node\Expr\New_ $node): bool + { + $args = $node instanceof Node\Expr\CallLike ? $node->getRawArgs() : $node->args; + + foreach ($args as $arg) { + if ($arg instanceof Node\Arg && null !== $arg->name) { + return true; + } + } + + return false; + } + + protected function nodeFirstNamedArgumentIndex(Node\Expr\CallLike|Node\Attribute|Node\Expr\New_ $node): int + { + $args = $node instanceof Node\Expr\CallLike ? $node->getRawArgs() : $node->args; + + foreach ($args as $i => $arg) { + if ($arg instanceof Node\Arg && null !== $arg->name) { + return $i; + } + } + + return \PHP_INT_MAX; + } + + private function getStringNamedArguments(Node\Expr\CallLike|Node\Attribute $node, ?string $argumentName = null, bool $isArgumentNamePattern = false): array + { + $args = $node instanceof Node\Expr\CallLike ? $node->getArgs() : $node->args; + $argumentValues = []; + + foreach ($args as $arg) { + if (!$isArgumentNamePattern && $arg->name?->toString() === $argumentName) { + $argumentValues[] = $this->getStringValue($arg->value); + } elseif ($isArgumentNamePattern && preg_match($argumentName, $arg->name?->toString() ?? '') > 0) { + $argumentValues[] = $this->getStringValue($arg->value); + } + } + + return array_filter($argumentValues); + } + + private function getStringValue(Node $node): ?string + { + if ($node instanceof Node\Scalar\String_) { + return $node->value; + } + + if ($node instanceof Node\Expr\BinaryOp\Concat) { + if (null === $left = $this->getStringValue($node->left)) { + return null; + } + + if (null === $right = $this->getStringValue($node->right)) { + return null; + } + + return $left.$right; + } + + if ($node instanceof Node\Expr\Assign && $node->expr instanceof Node\Scalar\String_) { + return $node->expr->value; + } + + if ($node instanceof Node\Expr\ClassConstFetch) { + try { + $reflection = new \ReflectionClass($node->class->toString()); + $constant = $reflection->getReflectionConstant($node->name->toString()); + if (false !== $constant && \is_string($constant->getValue())) { + return $constant->getValue(); + } + } catch (\ReflectionException) { + } + } + + return null; + } +} diff --git a/3rdparty/symfony/translation/Extractor/Visitor/ConstraintVisitor.php b/3rdparty/symfony/translation/Extractor/Visitor/ConstraintVisitor.php new file mode 100644 index 00000000..00fb9eed --- /dev/null +++ b/3rdparty/symfony/translation/Extractor/Visitor/ConstraintVisitor.php @@ -0,0 +1,112 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Component\Translation\Extractor\Visitor; + +use PhpParser\Node; +use PhpParser\NodeVisitor; + +/** + * @author Mathieu Santostefano + * + * Code mostly comes from https://github.com/php-translation/extractor/blob/master/src/Visitor/Php/Symfony/Constraint.php + */ +final class ConstraintVisitor extends AbstractVisitor implements NodeVisitor +{ + public function __construct( + private readonly array $constraintClassNames = [] + ) { + } + + public function beforeTraverse(array $nodes): ?Node + { + return null; + } + + public function enterNode(Node $node): ?Node + { + return null; + } + + public function leaveNode(Node $node): ?Node + { + if (!$node instanceof Node\Expr\New_ && !$node instanceof Node\Attribute) { + return null; + } + + $className = $node instanceof Node\Attribute ? $node->name : $node->class; + if (!$className instanceof Node\Name) { + return null; + } + + $parts = $className->getParts(); + $isConstraintClass = false; + + foreach ($parts as $part) { + if (\in_array($part, $this->constraintClassNames, true)) { + $isConstraintClass = true; + + break; + } + } + + if (!$isConstraintClass) { + return null; + } + + $arg = $node->args[0] ?? null; + if (!$arg instanceof Node\Arg) { + return null; + } + + if ($this->hasNodeNamedArguments($node)) { + $messages = $this->getStringArguments($node, '/message/i', true); + } else { + if (!$arg->value instanceof Node\Expr\Array_) { + // There is no way to guess which argument is a message to be translated. + return null; + } + + $messages = []; + $options = $arg->value; + + /** @var Node\Expr\ArrayItem $item */ + foreach ($options->items as $item) { + if (!$item->key instanceof Node\Scalar\String_) { + continue; + } + + if (false === stripos($item->key->value ?? '', 'message')) { + continue; + } + + if (!$item->value instanceof Node\Scalar\String_) { + continue; + } + + $messages[] = $item->value->value; + + break; + } + } + + foreach ($messages as $message) { + $this->addMessageToCatalogue($message, 'validators', $node->getStartLine()); + } + + return null; + } + + public function afterTraverse(array $nodes): ?Node + { + return null; + } +} diff --git a/3rdparty/symfony/translation/Extractor/Visitor/TransMethodVisitor.php b/3rdparty/symfony/translation/Extractor/Visitor/TransMethodVisitor.php new file mode 100644 index 00000000..011bf3b1 --- /dev/null +++ b/3rdparty/symfony/translation/Extractor/Visitor/TransMethodVisitor.php @@ -0,0 +1,65 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Component\Translation\Extractor\Visitor; + +use PhpParser\Node; +use PhpParser\NodeVisitor; + +/** + * @author Mathieu Santostefano + */ +final class TransMethodVisitor extends AbstractVisitor implements NodeVisitor +{ + public function beforeTraverse(array $nodes): ?Node + { + return null; + } + + public function enterNode(Node $node): ?Node + { + return null; + } + + public function leaveNode(Node $node): ?Node + { + if (!$node instanceof Node\Expr\MethodCall && !$node instanceof Node\Expr\FuncCall) { + return null; + } + + if (!\is_string($node->name) && !$node->name instanceof Node\Identifier && !$node->name instanceof Node\Name) { + return null; + } + + $name = $node->name instanceof Node\Name ? $node->name->getLast() : (string) $node->name; + + if ('trans' === $name || 't' === $name) { + $firstNamedArgumentIndex = $this->nodeFirstNamedArgumentIndex($node); + + if (!$messages = $this->getStringArguments($node, 0 < $firstNamedArgumentIndex ? 0 : 'message')) { + return null; + } + + $domain = $this->getStringArguments($node, 2 < $firstNamedArgumentIndex ? 2 : 'domain')[0] ?? null; + + foreach ($messages as $message) { + $this->addMessageToCatalogue($message, $domain, $node->getStartLine()); + } + } + + return null; + } + + public function afterTraverse(array $nodes): ?Node + { + return null; + } +} diff --git a/3rdparty/symfony/translation/Extractor/Visitor/TranslatableMessageVisitor.php b/3rdparty/symfony/translation/Extractor/Visitor/TranslatableMessageVisitor.php new file mode 100644 index 00000000..6bd8bb02 --- /dev/null +++ b/3rdparty/symfony/translation/Extractor/Visitor/TranslatableMessageVisitor.php @@ -0,0 +1,65 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Component\Translation\Extractor\Visitor; + +use PhpParser\Node; +use PhpParser\NodeVisitor; + +/** + * @author Mathieu Santostefano + */ +final class TranslatableMessageVisitor extends AbstractVisitor implements NodeVisitor +{ + public function beforeTraverse(array $nodes): ?Node + { + return null; + } + + public function enterNode(Node $node): ?Node + { + return null; + } + + public function leaveNode(Node $node): ?Node + { + if (!$node instanceof Node\Expr\New_) { + return null; + } + + if (!($className = $node->class) instanceof Node\Name) { + return null; + } + + if (!\in_array('TranslatableMessage', $className->getParts(), true)) { + return null; + } + + $firstNamedArgumentIndex = $this->nodeFirstNamedArgumentIndex($node); + + if (!$messages = $this->getStringArguments($node, 0 < $firstNamedArgumentIndex ? 0 : 'message')) { + return null; + } + + $domain = $this->getStringArguments($node, 2 < $firstNamedArgumentIndex ? 2 : 'domain')[0] ?? null; + + foreach ($messages as $message) { + $this->addMessageToCatalogue($message, $domain, $node->getStartLine()); + } + + return null; + } + + public function afterTraverse(array $nodes): ?Node + { + return null; + } +} diff --git a/3rdparty/symfony/translation/Formatter/IntlFormatter.php b/3rdparty/symfony/translation/Formatter/IntlFormatter.php new file mode 100644 index 00000000..e62de253 --- /dev/null +++ b/3rdparty/symfony/translation/Formatter/IntlFormatter.php @@ -0,0 +1,57 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Component\Translation\Formatter; + +use Symfony\Component\Translation\Exception\InvalidArgumentException; +use Symfony\Component\Translation\Exception\LogicException; + +/** + * @author Guilherme Blanco + * @author Abdellatif Ait boudad + */ +class IntlFormatter implements IntlFormatterInterface +{ + private bool $hasMessageFormatter; + private array $cache = []; + + public function formatIntl(string $message, string $locale, array $parameters = []): string + { + // MessageFormatter constructor throws an exception if the message is empty + if ('' === $message) { + return ''; + } + + if (!$formatter = $this->cache[$locale][$message] ?? null) { + if (!$this->hasMessageFormatter ??= class_exists(\MessageFormatter::class)) { + throw new LogicException('Cannot parse message translation: please install the "intl" PHP extension or the "symfony/polyfill-intl-messageformatter" package.'); + } + try { + $this->cache[$locale][$message] = $formatter = new \MessageFormatter($locale, $message); + } catch (\IntlException $e) { + throw new InvalidArgumentException(sprintf('Invalid message format (error #%d): ', intl_get_error_code()).intl_get_error_message(), 0, $e); + } + } + + foreach ($parameters as $key => $value) { + if (\in_array($key[0] ?? null, ['%', '{'], true)) { + unset($parameters[$key]); + $parameters[trim($key, '%{ }')] = $value; + } + } + + if (false === $message = $formatter->format($parameters)) { + throw new InvalidArgumentException(sprintf('Unable to format message (error #%s): ', $formatter->getErrorCode()).$formatter->getErrorMessage()); + } + + return $message; + } +} diff --git a/3rdparty/symfony/translation/Formatter/IntlFormatterInterface.php b/3rdparty/symfony/translation/Formatter/IntlFormatterInterface.php new file mode 100644 index 00000000..02fc6acb --- /dev/null +++ b/3rdparty/symfony/translation/Formatter/IntlFormatterInterface.php @@ -0,0 +1,27 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Component\Translation\Formatter; + +/** + * Formats ICU message patterns. + * + * @author Nicolas Grekas + */ +interface IntlFormatterInterface +{ + /** + * Formats a localized message using rules defined by ICU MessageFormat. + * + * @see http://icu-project.org/apiref/icu4c/classMessageFormat.html#details + */ + public function formatIntl(string $message, string $locale, array $parameters = []): string; +} diff --git a/3rdparty/symfony/translation/Formatter/MessageFormatter.php b/3rdparty/symfony/translation/Formatter/MessageFormatter.php new file mode 100644 index 00000000..d5255bdc --- /dev/null +++ b/3rdparty/symfony/translation/Formatter/MessageFormatter.php @@ -0,0 +1,46 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Component\Translation\Formatter; + +use Symfony\Component\Translation\IdentityTranslator; +use Symfony\Contracts\Translation\TranslatorInterface; + +// Help opcache.preload discover always-needed symbols +class_exists(IntlFormatter::class); + +/** + * @author Abdellatif Ait boudad + */ +class MessageFormatter implements MessageFormatterInterface, IntlFormatterInterface +{ + private TranslatorInterface $translator; + private IntlFormatterInterface $intlFormatter; + + /** + * @param TranslatorInterface|null $translator An identity translator to use as selector for pluralization + */ + public function __construct(?TranslatorInterface $translator = null, ?IntlFormatterInterface $intlFormatter = null) + { + $this->translator = $translator ?? new IdentityTranslator(); + $this->intlFormatter = $intlFormatter ?? new IntlFormatter(); + } + + public function format(string $message, string $locale, array $parameters = []): string + { + return $this->translator->trans($message, $parameters, null, $locale); + } + + public function formatIntl(string $message, string $locale, array $parameters = []): string + { + return $this->intlFormatter->formatIntl($message, $locale, $parameters); + } +} diff --git a/3rdparty/symfony/translation/Formatter/MessageFormatterInterface.php b/3rdparty/symfony/translation/Formatter/MessageFormatterInterface.php new file mode 100644 index 00000000..d5c41c19 --- /dev/null +++ b/3rdparty/symfony/translation/Formatter/MessageFormatterInterface.php @@ -0,0 +1,28 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Component\Translation\Formatter; + +/** + * @author Guilherme Blanco + * @author Abdellatif Ait boudad + */ +interface MessageFormatterInterface +{ + /** + * Formats a localized message pattern with given arguments. + * + * @param string $message The message (may also be an object that can be cast to string) + * @param string $locale The message locale + * @param array $parameters An array of parameters for the message + */ + public function format(string $message, string $locale, array $parameters = []): string; +} diff --git a/3rdparty/symfony/translation/IdentityTranslator.php b/3rdparty/symfony/translation/IdentityTranslator.php new file mode 100644 index 00000000..46875edf --- /dev/null +++ b/3rdparty/symfony/translation/IdentityTranslator.php @@ -0,0 +1,26 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Component\Translation; + +use Symfony\Contracts\Translation\LocaleAwareInterface; +use Symfony\Contracts\Translation\TranslatorInterface; +use Symfony\Contracts\Translation\TranslatorTrait; + +/** + * IdentityTranslator does not translate anything. + * + * @author Fabien Potencier + */ +class IdentityTranslator implements TranslatorInterface, LocaleAwareInterface +{ + use TranslatorTrait; +} diff --git a/3rdparty/symfony/translation/LICENSE b/3rdparty/symfony/translation/LICENSE new file mode 100644 index 00000000..0138f8f0 --- /dev/null +++ b/3rdparty/symfony/translation/LICENSE @@ -0,0 +1,19 @@ +Copyright (c) 2004-present Fabien Potencier + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is furnished +to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +THE SOFTWARE. diff --git a/3rdparty/symfony/translation/Loader/ArrayLoader.php b/3rdparty/symfony/translation/Loader/ArrayLoader.php new file mode 100644 index 00000000..e63a7d05 --- /dev/null +++ b/3rdparty/symfony/translation/Loader/ArrayLoader.php @@ -0,0 +1,57 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Component\Translation\Loader; + +use Symfony\Component\Translation\MessageCatalogue; + +/** + * ArrayLoader loads translations from a PHP array. + * + * @author Fabien Potencier + */ +class ArrayLoader implements LoaderInterface +{ + public function load(mixed $resource, string $locale, string $domain = 'messages'): MessageCatalogue + { + $resource = $this->flatten($resource); + $catalogue = new MessageCatalogue($locale); + $catalogue->add($resource, $domain); + + return $catalogue; + } + + /** + * Flattens an nested array of translations. + * + * The scheme used is: + * 'key' => ['key2' => ['key3' => 'value']] + * Becomes: + * 'key.key2.key3' => 'value' + */ + private function flatten(array $messages): array + { + $result = []; + foreach ($messages as $key => $value) { + if (\is_array($value)) { + foreach ($this->flatten($value) as $k => $v) { + if (null !== $v) { + $result[$key.'.'.$k] = $v; + } + } + } elseif (null !== $value) { + $result[$key] = $value; + } + } + + return $result; + } +} diff --git a/3rdparty/symfony/translation/Loader/CsvFileLoader.php b/3rdparty/symfony/translation/Loader/CsvFileLoader.php new file mode 100644 index 00000000..7f2f96be --- /dev/null +++ b/3rdparty/symfony/translation/Loader/CsvFileLoader.php @@ -0,0 +1,64 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Component\Translation\Loader; + +use Symfony\Component\Translation\Exception\NotFoundResourceException; + +/** + * CsvFileLoader loads translations from CSV files. + * + * @author Saša Stamenković + */ +class CsvFileLoader extends FileLoader +{ + private string $delimiter = ';'; + private string $enclosure = '"'; + private string $escape = '\\'; + + protected function loadResource(string $resource): array + { + $messages = []; + + try { + $file = new \SplFileObject($resource, 'rb'); + } catch (\RuntimeException $e) { + throw new NotFoundResourceException(sprintf('Error opening file "%s".', $resource), 0, $e); + } + + $file->setFlags(\SplFileObject::READ_CSV | \SplFileObject::SKIP_EMPTY); + $file->setCsvControl($this->delimiter, $this->enclosure, $this->escape); + + foreach ($file as $data) { + if (false === $data) { + continue; + } + + if (!str_starts_with($data[0], '#') && isset($data[1]) && 2 === \count($data)) { + $messages[$data[0]] = $data[1]; + } + } + + return $messages; + } + + /** + * Sets the delimiter, enclosure, and escape character for CSV. + * + * @return void + */ + public function setCsvControl(string $delimiter = ';', string $enclosure = '"', string $escape = '\\') + { + $this->delimiter = $delimiter; + $this->enclosure = $enclosure; + $this->escape = $escape; + } +} diff --git a/3rdparty/symfony/translation/Loader/FileLoader.php b/3rdparty/symfony/translation/Loader/FileLoader.php new file mode 100644 index 00000000..877c3bbc --- /dev/null +++ b/3rdparty/symfony/translation/Loader/FileLoader.php @@ -0,0 +1,57 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Component\Translation\Loader; + +use Symfony\Component\Config\Resource\FileResource; +use Symfony\Component\Translation\Exception\InvalidResourceException; +use Symfony\Component\Translation\Exception\NotFoundResourceException; +use Symfony\Component\Translation\MessageCatalogue; + +/** + * @author Abdellatif Ait boudad + */ +abstract class FileLoader extends ArrayLoader +{ + public function load(mixed $resource, string $locale, string $domain = 'messages'): MessageCatalogue + { + if (!stream_is_local($resource)) { + throw new InvalidResourceException(sprintf('This is not a local file "%s".', $resource)); + } + + if (!file_exists($resource)) { + throw new NotFoundResourceException(sprintf('File "%s" not found.', $resource)); + } + + $messages = $this->loadResource($resource); + + // empty resource + $messages ??= []; + + // not an array + if (!\is_array($messages)) { + throw new InvalidResourceException(sprintf('Unable to load file "%s".', $resource)); + } + + $catalogue = parent::load($messages, $locale, $domain); + + if (class_exists(FileResource::class)) { + $catalogue->addResource(new FileResource($resource)); + } + + return $catalogue; + } + + /** + * @throws InvalidResourceException if stream content has an invalid format + */ + abstract protected function loadResource(string $resource): array; +} diff --git a/3rdparty/symfony/translation/Loader/IcuDatFileLoader.php b/3rdparty/symfony/translation/Loader/IcuDatFileLoader.php new file mode 100644 index 00000000..76e4e7f0 --- /dev/null +++ b/3rdparty/symfony/translation/Loader/IcuDatFileLoader.php @@ -0,0 +1,58 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Component\Translation\Loader; + +use Symfony\Component\Config\Resource\FileResource; +use Symfony\Component\Translation\Exception\InvalidResourceException; +use Symfony\Component\Translation\Exception\NotFoundResourceException; +use Symfony\Component\Translation\MessageCatalogue; + +/** + * IcuResFileLoader loads translations from a resource bundle. + * + * @author stealth35 + */ +class IcuDatFileLoader extends IcuResFileLoader +{ + public function load(mixed $resource, string $locale, string $domain = 'messages'): MessageCatalogue + { + if (!stream_is_local($resource.'.dat')) { + throw new InvalidResourceException(sprintf('This is not a local file "%s".', $resource)); + } + + if (!file_exists($resource.'.dat')) { + throw new NotFoundResourceException(sprintf('File "%s" not found.', $resource)); + } + + try { + $rb = new \ResourceBundle($locale, $resource); + } catch (\Exception) { + $rb = null; + } + + if (!$rb) { + throw new InvalidResourceException(sprintf('Cannot load resource "%s".', $resource)); + } elseif (intl_is_failure($rb->getErrorCode())) { + throw new InvalidResourceException($rb->getErrorMessage(), $rb->getErrorCode()); + } + + $messages = $this->flatten($rb); + $catalogue = new MessageCatalogue($locale); + $catalogue->add($messages, $domain); + + if (class_exists(FileResource::class)) { + $catalogue->addResource(new FileResource($resource.'.dat')); + } + + return $catalogue; + } +} diff --git a/3rdparty/symfony/translation/Loader/IcuResFileLoader.php b/3rdparty/symfony/translation/Loader/IcuResFileLoader.php new file mode 100644 index 00000000..949dd979 --- /dev/null +++ b/3rdparty/symfony/translation/Loader/IcuResFileLoader.php @@ -0,0 +1,86 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Component\Translation\Loader; + +use Symfony\Component\Config\Resource\DirectoryResource; +use Symfony\Component\Translation\Exception\InvalidResourceException; +use Symfony\Component\Translation\Exception\NotFoundResourceException; +use Symfony\Component\Translation\MessageCatalogue; + +/** + * IcuResFileLoader loads translations from a resource bundle. + * + * @author stealth35 + */ +class IcuResFileLoader implements LoaderInterface +{ + public function load(mixed $resource, string $locale, string $domain = 'messages'): MessageCatalogue + { + if (!stream_is_local($resource)) { + throw new InvalidResourceException(sprintf('This is not a local file "%s".', $resource)); + } + + if (!is_dir($resource)) { + throw new NotFoundResourceException(sprintf('File "%s" not found.', $resource)); + } + + try { + $rb = new \ResourceBundle($locale, $resource); + } catch (\Exception) { + $rb = null; + } + + if (!$rb) { + throw new InvalidResourceException(sprintf('Cannot load resource "%s".', $resource)); + } elseif (intl_is_failure($rb->getErrorCode())) { + throw new InvalidResourceException($rb->getErrorMessage(), $rb->getErrorCode()); + } + + $messages = $this->flatten($rb); + $catalogue = new MessageCatalogue($locale); + $catalogue->add($messages, $domain); + + if (class_exists(DirectoryResource::class)) { + $catalogue->addResource(new DirectoryResource($resource)); + } + + return $catalogue; + } + + /** + * Flattens an ResourceBundle. + * + * The scheme used is: + * key { key2 { key3 { "value" } } } + * Becomes: + * 'key.key2.key3' => 'value' + * + * This function takes an array by reference and will modify it + * + * @param \ResourceBundle $rb The ResourceBundle that will be flattened + * @param array $messages Used internally for recursive calls + * @param string|null $path Current path being parsed, used internally for recursive calls + */ + protected function flatten(\ResourceBundle $rb, array &$messages = [], ?string $path = null): array + { + foreach ($rb as $key => $value) { + $nodePath = $path ? $path.'.'.$key : $key; + if ($value instanceof \ResourceBundle) { + $this->flatten($value, $messages, $nodePath); + } else { + $messages[$nodePath] = $value; + } + } + + return $messages; + } +} diff --git a/3rdparty/symfony/translation/Loader/IniFileLoader.php b/3rdparty/symfony/translation/Loader/IniFileLoader.php new file mode 100644 index 00000000..3126896c --- /dev/null +++ b/3rdparty/symfony/translation/Loader/IniFileLoader.php @@ -0,0 +1,25 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Component\Translation\Loader; + +/** + * IniFileLoader loads translations from an ini file. + * + * @author stealth35 + */ +class IniFileLoader extends FileLoader +{ + protected function loadResource(string $resource): array + { + return parse_ini_file($resource, true); + } +} diff --git a/3rdparty/symfony/translation/Loader/JsonFileLoader.php b/3rdparty/symfony/translation/Loader/JsonFileLoader.php new file mode 100644 index 00000000..385553ef --- /dev/null +++ b/3rdparty/symfony/translation/Loader/JsonFileLoader.php @@ -0,0 +1,51 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Component\Translation\Loader; + +use Symfony\Component\Translation\Exception\InvalidResourceException; + +/** + * JsonFileLoader loads translations from an json file. + * + * @author singles + */ +class JsonFileLoader extends FileLoader +{ + protected function loadResource(string $resource): array + { + $messages = []; + if ($data = file_get_contents($resource)) { + $messages = json_decode($data, true); + + if (0 < $errorCode = json_last_error()) { + throw new InvalidResourceException('Error parsing JSON: '.$this->getJSONErrorMessage($errorCode)); + } + } + + return $messages; + } + + /** + * Translates JSON_ERROR_* constant into meaningful message. + */ + private function getJSONErrorMessage(int $errorCode): string + { + return match ($errorCode) { + \JSON_ERROR_DEPTH => 'Maximum stack depth exceeded', + \JSON_ERROR_STATE_MISMATCH => 'Underflow or the modes mismatch', + \JSON_ERROR_CTRL_CHAR => 'Unexpected control character found', + \JSON_ERROR_SYNTAX => 'Syntax error, malformed JSON', + \JSON_ERROR_UTF8 => 'Malformed UTF-8 characters, possibly incorrectly encoded', + default => 'Unknown error', + }; + } +} diff --git a/3rdparty/symfony/translation/Loader/LoaderInterface.php b/3rdparty/symfony/translation/Loader/LoaderInterface.php new file mode 100644 index 00000000..29d5560d --- /dev/null +++ b/3rdparty/symfony/translation/Loader/LoaderInterface.php @@ -0,0 +1,32 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Component\Translation\Loader; + +use Symfony\Component\Translation\Exception\InvalidResourceException; +use Symfony\Component\Translation\Exception\NotFoundResourceException; +use Symfony\Component\Translation\MessageCatalogue; + +/** + * LoaderInterface is the interface implemented by all translation loaders. + * + * @author Fabien Potencier + */ +interface LoaderInterface +{ + /** + * Loads a locale. + * + * @throws NotFoundResourceException when the resource cannot be found + * @throws InvalidResourceException when the resource cannot be loaded + */ + public function load(mixed $resource, string $locale, string $domain = 'messages'): MessageCatalogue; +} diff --git a/3rdparty/symfony/translation/Loader/MoFileLoader.php b/3rdparty/symfony/translation/Loader/MoFileLoader.php new file mode 100644 index 00000000..8427c393 --- /dev/null +++ b/3rdparty/symfony/translation/Loader/MoFileLoader.php @@ -0,0 +1,138 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Component\Translation\Loader; + +use Symfony\Component\Translation\Exception\InvalidResourceException; + +/** + * @copyright Copyright (c) 2010, Union of RAD http://union-of-rad.org (http://lithify.me/) + */ +class MoFileLoader extends FileLoader +{ + /** + * Magic used for validating the format of an MO file as well as + * detecting if the machine used to create that file was little endian. + */ + public const MO_LITTLE_ENDIAN_MAGIC = 0x950412DE; + + /** + * Magic used for validating the format of an MO file as well as + * detecting if the machine used to create that file was big endian. + */ + public const MO_BIG_ENDIAN_MAGIC = 0xDE120495; + + /** + * The size of the header of an MO file in bytes. + */ + public const MO_HEADER_SIZE = 28; + + /** + * Parses machine object (MO) format, independent of the machine's endian it + * was created on. Both 32bit and 64bit systems are supported. + */ + protected function loadResource(string $resource): array + { + $stream = fopen($resource, 'r'); + + $stat = fstat($stream); + + if ($stat['size'] < self::MO_HEADER_SIZE) { + throw new InvalidResourceException('MO stream content has an invalid format.'); + } + $magic = unpack('V1', fread($stream, 4)); + $magic = hexdec(substr(dechex(current($magic)), -8)); + + if (self::MO_LITTLE_ENDIAN_MAGIC == $magic) { + $isBigEndian = false; + } elseif (self::MO_BIG_ENDIAN_MAGIC == $magic) { + $isBigEndian = true; + } else { + throw new InvalidResourceException('MO stream content has an invalid format.'); + } + + // formatRevision + $this->readLong($stream, $isBigEndian); + $count = $this->readLong($stream, $isBigEndian); + $offsetId = $this->readLong($stream, $isBigEndian); + $offsetTranslated = $this->readLong($stream, $isBigEndian); + // sizeHashes + $this->readLong($stream, $isBigEndian); + // offsetHashes + $this->readLong($stream, $isBigEndian); + + $messages = []; + + for ($i = 0; $i < $count; ++$i) { + $pluralId = null; + $translated = null; + + fseek($stream, $offsetId + $i * 8); + + $length = $this->readLong($stream, $isBigEndian); + $offset = $this->readLong($stream, $isBigEndian); + + if ($length < 1) { + continue; + } + + fseek($stream, $offset); + $singularId = fread($stream, $length); + + if (str_contains($singularId, "\000")) { + [$singularId, $pluralId] = explode("\000", $singularId); + } + + fseek($stream, $offsetTranslated + $i * 8); + $length = $this->readLong($stream, $isBigEndian); + $offset = $this->readLong($stream, $isBigEndian); + + if ($length < 1) { + continue; + } + + fseek($stream, $offset); + $translated = fread($stream, $length); + + if (str_contains($translated, "\000")) { + $translated = explode("\000", $translated); + } + + $ids = ['singular' => $singularId, 'plural' => $pluralId]; + $item = compact('ids', 'translated'); + + if (!empty($item['ids']['singular'])) { + $id = $item['ids']['singular']; + if (isset($item['ids']['plural'])) { + $id .= '|'.$item['ids']['plural']; + } + $messages[$id] = stripcslashes(implode('|', (array) $item['translated'])); + } + } + + fclose($stream); + + return array_filter($messages); + } + + /** + * Reads an unsigned long from stream respecting endianness. + * + * @param resource $stream + */ + private function readLong($stream, bool $isBigEndian): int + { + $result = unpack($isBigEndian ? 'N1' : 'V1', fread($stream, 4)); + $result = current($result); + + return (int) substr($result, -8); + } +} diff --git a/3rdparty/symfony/translation/Loader/PhpFileLoader.php b/3rdparty/symfony/translation/Loader/PhpFileLoader.php new file mode 100644 index 00000000..541b6c83 --- /dev/null +++ b/3rdparty/symfony/translation/Loader/PhpFileLoader.php @@ -0,0 +1,35 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Component\Translation\Loader; + +/** + * PhpFileLoader loads translations from PHP files returning an array of translations. + * + * @author Fabien Potencier + */ +class PhpFileLoader extends FileLoader +{ + private static ?array $cache = []; + + protected function loadResource(string $resource): array + { + if ([] === self::$cache && \function_exists('opcache_invalidate') && filter_var(\ini_get('opcache.enable'), \FILTER_VALIDATE_BOOL) && (!\in_array(\PHP_SAPI, ['cli', 'phpdbg', 'embed'], true) || filter_var(\ini_get('opcache.enable_cli'), \FILTER_VALIDATE_BOOL))) { + self::$cache = null; + } + + if (null === self::$cache) { + return require $resource; + } + + return self::$cache[$resource] ??= require $resource; + } +} diff --git a/3rdparty/symfony/translation/Loader/PoFileLoader.php b/3rdparty/symfony/translation/Loader/PoFileLoader.php new file mode 100644 index 00000000..620d9733 --- /dev/null +++ b/3rdparty/symfony/translation/Loader/PoFileLoader.php @@ -0,0 +1,147 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Component\Translation\Loader; + +/** + * @copyright Copyright (c) 2010, Union of RAD https://github.com/UnionOfRAD/lithium + * @copyright Copyright (c) 2012, Clemens Tolboom + */ +class PoFileLoader extends FileLoader +{ + /** + * Parses portable object (PO) format. + * + * From https://www.gnu.org/software/gettext/manual/gettext.html#PO-Files + * we should be able to parse files having: + * + * white-space + * # translator-comments + * #. extracted-comments + * #: reference... + * #, flag... + * #| msgid previous-untranslated-string + * msgid untranslated-string + * msgstr translated-string + * + * extra or different lines are: + * + * #| msgctxt previous-context + * #| msgid previous-untranslated-string + * msgctxt context + * + * #| msgid previous-untranslated-string-singular + * #| msgid_plural previous-untranslated-string-plural + * msgid untranslated-string-singular + * msgid_plural untranslated-string-plural + * msgstr[0] translated-string-case-0 + * ... + * msgstr[N] translated-string-case-n + * + * The definition states: + * - white-space and comments are optional. + * - msgid "" that an empty singleline defines a header. + * + * This parser sacrifices some features of the reference implementation the + * differences to that implementation are as follows. + * - No support for comments spanning multiple lines. + * - Translator and extracted comments are treated as being the same type. + * - Message IDs are allowed to have other encodings as just US-ASCII. + * + * Items with an empty id are ignored. + */ + protected function loadResource(string $resource): array + { + $stream = fopen($resource, 'r'); + + $defaults = [ + 'ids' => [], + 'translated' => null, + ]; + + $messages = []; + $item = $defaults; + $flags = []; + + while ($line = fgets($stream)) { + $line = trim($line); + + if ('' === $line) { + // Whitespace indicated current item is done + if (!\in_array('fuzzy', $flags)) { + $this->addMessage($messages, $item); + } + $item = $defaults; + $flags = []; + } elseif (str_starts_with($line, '#,')) { + $flags = array_map('trim', explode(',', substr($line, 2))); + } elseif (str_starts_with($line, 'msgid "')) { + // We start a new msg so save previous + // TODO: this fails when comments or contexts are added + $this->addMessage($messages, $item); + $item = $defaults; + $item['ids']['singular'] = substr($line, 7, -1); + } elseif (str_starts_with($line, 'msgstr "')) { + $item['translated'] = substr($line, 8, -1); + } elseif ('"' === $line[0]) { + $continues = isset($item['translated']) ? 'translated' : 'ids'; + + if (\is_array($item[$continues])) { + end($item[$continues]); + $item[$continues][key($item[$continues])] .= substr($line, 1, -1); + } else { + $item[$continues] .= substr($line, 1, -1); + } + } elseif (str_starts_with($line, 'msgid_plural "')) { + $item['ids']['plural'] = substr($line, 14, -1); + } elseif (str_starts_with($line, 'msgstr[')) { + $size = strpos($line, ']'); + $item['translated'][(int) substr($line, 7, 1)] = substr($line, $size + 3, -1); + } + } + // save last item + if (!\in_array('fuzzy', $flags)) { + $this->addMessage($messages, $item); + } + fclose($stream); + + return $messages; + } + + /** + * Save a translation item to the messages. + * + * A .po file could contain by error missing plural indexes. We need to + * fix these before saving them. + */ + private function addMessage(array &$messages, array $item): void + { + if (!empty($item['ids']['singular'])) { + $id = stripcslashes($item['ids']['singular']); + if (isset($item['ids']['plural'])) { + $id .= '|'.stripcslashes($item['ids']['plural']); + } + + $translated = (array) $item['translated']; + // PO are by definition indexed so sort by index. + ksort($translated); + // Make sure every index is filled. + end($translated); + $count = key($translated); + // Fill missing spots with '-'. + $empties = array_fill(0, $count + 1, '-'); + $translated += $empties; + ksort($translated); + + $messages[$id] = stripcslashes(implode('|', $translated)); + } + } +} diff --git a/3rdparty/symfony/translation/Loader/QtFileLoader.php b/3rdparty/symfony/translation/Loader/QtFileLoader.php new file mode 100644 index 00000000..235f85ee --- /dev/null +++ b/3rdparty/symfony/translation/Loader/QtFileLoader.php @@ -0,0 +1,78 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Component\Translation\Loader; + +use Symfony\Component\Config\Resource\FileResource; +use Symfony\Component\Config\Util\XmlUtils; +use Symfony\Component\Translation\Exception\InvalidResourceException; +use Symfony\Component\Translation\Exception\NotFoundResourceException; +use Symfony\Component\Translation\Exception\RuntimeException; +use Symfony\Component\Translation\MessageCatalogue; + +/** + * QtFileLoader loads translations from QT Translations XML files. + * + * @author Benjamin Eberlei + */ +class QtFileLoader implements LoaderInterface +{ + public function load(mixed $resource, string $locale, string $domain = 'messages'): MessageCatalogue + { + if (!class_exists(XmlUtils::class)) { + throw new RuntimeException('Loading translations from the QT format requires the Symfony Config component.'); + } + + if (!stream_is_local($resource)) { + throw new InvalidResourceException(sprintf('This is not a local file "%s".', $resource)); + } + + if (!file_exists($resource)) { + throw new NotFoundResourceException(sprintf('File "%s" not found.', $resource)); + } + + try { + $dom = XmlUtils::loadFile($resource); + } catch (\InvalidArgumentException $e) { + throw new InvalidResourceException(sprintf('Unable to load "%s".', $resource), $e->getCode(), $e); + } + + $internalErrors = libxml_use_internal_errors(true); + libxml_clear_errors(); + + $xpath = new \DOMXPath($dom); + $nodes = $xpath->evaluate('//TS/context/name[text()="'.$domain.'"]'); + + $catalogue = new MessageCatalogue($locale); + if (1 == $nodes->length) { + $translations = $nodes->item(0)->nextSibling->parentNode->parentNode->getElementsByTagName('message'); + foreach ($translations as $translation) { + $translationValue = (string) $translation->getElementsByTagName('translation')->item(0)->nodeValue; + + if (!empty($translationValue)) { + $catalogue->set( + (string) $translation->getElementsByTagName('source')->item(0)->nodeValue, + $translationValue, + $domain + ); + } + } + + if (class_exists(FileResource::class)) { + $catalogue->addResource(new FileResource($resource)); + } + } + + libxml_use_internal_errors($internalErrors); + + return $catalogue; + } +} diff --git a/3rdparty/symfony/translation/Loader/XliffFileLoader.php b/3rdparty/symfony/translation/Loader/XliffFileLoader.php new file mode 100644 index 00000000..31b3251b --- /dev/null +++ b/3rdparty/symfony/translation/Loader/XliffFileLoader.php @@ -0,0 +1,237 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Component\Translation\Loader; + +use Symfony\Component\Config\Resource\FileResource; +use Symfony\Component\Config\Util\Exception\InvalidXmlException; +use Symfony\Component\Config\Util\Exception\XmlParsingException; +use Symfony\Component\Config\Util\XmlUtils; +use Symfony\Component\Translation\Exception\InvalidResourceException; +use Symfony\Component\Translation\Exception\NotFoundResourceException; +use Symfony\Component\Translation\Exception\RuntimeException; +use Symfony\Component\Translation\MessageCatalogue; +use Symfony\Component\Translation\Util\XliffUtils; + +/** + * XliffFileLoader loads translations from XLIFF files. + * + * @author Fabien Potencier + */ +class XliffFileLoader implements LoaderInterface +{ + public function load(mixed $resource, string $locale, string $domain = 'messages'): MessageCatalogue + { + if (!class_exists(XmlUtils::class)) { + throw new RuntimeException('Loading translations from the Xliff format requires the Symfony Config component.'); + } + + if (!$this->isXmlString($resource)) { + if (!stream_is_local($resource)) { + throw new InvalidResourceException(sprintf('This is not a local file "%s".', $resource)); + } + + if (!file_exists($resource)) { + throw new NotFoundResourceException(sprintf('File "%s" not found.', $resource)); + } + + if (!is_file($resource)) { + throw new InvalidResourceException(sprintf('This is neither a file nor an XLIFF string "%s".', $resource)); + } + } + + try { + if ($this->isXmlString($resource)) { + $dom = XmlUtils::parse($resource); + } else { + $dom = XmlUtils::loadFile($resource); + } + } catch (\InvalidArgumentException|XmlParsingException|InvalidXmlException $e) { + throw new InvalidResourceException(sprintf('Unable to load "%s": ', $resource).$e->getMessage(), $e->getCode(), $e); + } + + if ($errors = XliffUtils::validateSchema($dom)) { + throw new InvalidResourceException(sprintf('Invalid resource provided: "%s"; Errors: ', $resource).XliffUtils::getErrorsAsString($errors)); + } + + $catalogue = new MessageCatalogue($locale); + $this->extract($dom, $catalogue, $domain); + + if (is_file($resource) && class_exists(FileResource::class)) { + $catalogue->addResource(new FileResource($resource)); + } + + return $catalogue; + } + + private function extract(\DOMDocument $dom, MessageCatalogue $catalogue, string $domain): void + { + $xliffVersion = XliffUtils::getVersionNumber($dom); + + if ('1.2' === $xliffVersion) { + $this->extractXliff1($dom, $catalogue, $domain); + } + + if ('2.0' === $xliffVersion) { + $this->extractXliff2($dom, $catalogue, $domain); + } + } + + /** + * Extract messages and metadata from DOMDocument into a MessageCatalogue. + */ + private function extractXliff1(\DOMDocument $dom, MessageCatalogue $catalogue, string $domain): void + { + $xml = simplexml_import_dom($dom); + $encoding = $dom->encoding ? strtoupper($dom->encoding) : null; + + $namespace = 'urn:oasis:names:tc:xliff:document:1.2'; + $xml->registerXPathNamespace('xliff', $namespace); + + foreach ($xml->xpath('//xliff:file') as $file) { + $fileAttributes = $file->attributes(); + + $file->registerXPathNamespace('xliff', $namespace); + + foreach ($file->xpath('.//xliff:prop') as $prop) { + $catalogue->setCatalogueMetadata($prop->attributes()['prop-type'], (string) $prop, $domain); + } + + foreach ($file->xpath('.//xliff:trans-unit') as $translation) { + $attributes = $translation->attributes(); + + if (!(isset($attributes['resname']) || isset($translation->source))) { + continue; + } + + if (isset($translation->target) && 'needs-translation' === (string) $translation->target->attributes()['state']) { + continue; + } + + $source = isset($attributes['resname']) && $attributes['resname'] ? $attributes['resname'] : $translation->source; + // If the xlf file has another encoding specified, try to convert it because + // simple_xml will always return utf-8 encoded values + $target = $this->utf8ToCharset((string) ($translation->target ?? $translation->source), $encoding); + + $catalogue->set((string) $source, $target, $domain); + + $metadata = [ + 'source' => (string) $translation->source, + 'file' => [ + 'original' => (string) $fileAttributes['original'], + ], + ]; + if ($notes = $this->parseNotesMetadata($translation->note, $encoding)) { + $metadata['notes'] = $notes; + } + + if (isset($translation->target) && $translation->target->attributes()) { + $metadata['target-attributes'] = []; + foreach ($translation->target->attributes() as $key => $value) { + $metadata['target-attributes'][$key] = (string) $value; + } + } + + if (isset($attributes['id'])) { + $metadata['id'] = (string) $attributes['id']; + } + + $catalogue->setMetadata((string) $source, $metadata, $domain); + } + } + } + + private function extractXliff2(\DOMDocument $dom, MessageCatalogue $catalogue, string $domain): void + { + $xml = simplexml_import_dom($dom); + $encoding = $dom->encoding ? strtoupper($dom->encoding) : null; + + $xml->registerXPathNamespace('xliff', 'urn:oasis:names:tc:xliff:document:2.0'); + + foreach ($xml->xpath('//xliff:unit') as $unit) { + foreach ($unit->segment as $segment) { + $attributes = $unit->attributes(); + $source = $attributes['name'] ?? $segment->source; + + // If the xlf file has another encoding specified, try to convert it because + // simple_xml will always return utf-8 encoded values + $target = $this->utf8ToCharset((string) ($segment->target ?? $segment->source), $encoding); + + $catalogue->set((string) $source, $target, $domain); + + $metadata = []; + if (isset($segment->target) && $segment->target->attributes()) { + $metadata['target-attributes'] = []; + foreach ($segment->target->attributes() as $key => $value) { + $metadata['target-attributes'][$key] = (string) $value; + } + } + + if (isset($unit->notes)) { + $metadata['notes'] = []; + foreach ($unit->notes->note as $noteNode) { + $note = []; + foreach ($noteNode->attributes() as $key => $value) { + $note[$key] = (string) $value; + } + $note['content'] = (string) $noteNode; + $metadata['notes'][] = $note; + } + } + + $catalogue->setMetadata((string) $source, $metadata, $domain); + } + } + } + + /** + * Convert a UTF8 string to the specified encoding. + */ + private function utf8ToCharset(string $content, ?string $encoding = null): string + { + if ('UTF-8' !== $encoding && !empty($encoding)) { + return mb_convert_encoding($content, $encoding, 'UTF-8'); + } + + return $content; + } + + private function parseNotesMetadata(?\SimpleXMLElement $noteElement = null, ?string $encoding = null): array + { + $notes = []; + + if (null === $noteElement) { + return $notes; + } + + /** @var \SimpleXMLElement $xmlNote */ + foreach ($noteElement as $xmlNote) { + $noteAttributes = $xmlNote->attributes(); + $note = ['content' => $this->utf8ToCharset((string) $xmlNote, $encoding)]; + if (isset($noteAttributes['priority'])) { + $note['priority'] = (int) $noteAttributes['priority']; + } + + if (isset($noteAttributes['from'])) { + $note['from'] = (string) $noteAttributes['from']; + } + + $notes[] = $note; + } + + return $notes; + } + + private function isXmlString(string $resource): bool + { + return str_starts_with($resource, ' + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Component\Translation\Loader; + +use Symfony\Component\Translation\Exception\InvalidResourceException; +use Symfony\Component\Translation\Exception\LogicException; +use Symfony\Component\Yaml\Exception\ParseException; +use Symfony\Component\Yaml\Parser as YamlParser; +use Symfony\Component\Yaml\Yaml; + +/** + * YamlFileLoader loads translations from Yaml files. + * + * @author Fabien Potencier + */ +class YamlFileLoader extends FileLoader +{ + private YamlParser $yamlParser; + + protected function loadResource(string $resource): array + { + if (!isset($this->yamlParser)) { + if (!class_exists(\Symfony\Component\Yaml\Parser::class)) { + throw new LogicException('Loading translations from the YAML format requires the Symfony Yaml component.'); + } + + $this->yamlParser = new YamlParser(); + } + + try { + $messages = $this->yamlParser->parseFile($resource, Yaml::PARSE_CONSTANT); + } catch (ParseException $e) { + throw new InvalidResourceException(sprintf('The file "%s" does not contain valid YAML: ', $resource).$e->getMessage(), 0, $e); + } + + if (null !== $messages && !\is_array($messages)) { + throw new InvalidResourceException(sprintf('Unable to load file "%s".', $resource)); + } + + return $messages ?: []; + } +} diff --git a/3rdparty/symfony/translation/LocaleSwitcher.php b/3rdparty/symfony/translation/LocaleSwitcher.php new file mode 100644 index 00000000..c07809c5 --- /dev/null +++ b/3rdparty/symfony/translation/LocaleSwitcher.php @@ -0,0 +1,78 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Component\Translation; + +use Symfony\Component\Routing\RequestContext; +use Symfony\Contracts\Translation\LocaleAwareInterface; + +/** + * @author Kevin Bond + */ +class LocaleSwitcher implements LocaleAwareInterface +{ + private string $defaultLocale; + + /** + * @param LocaleAwareInterface[] $localeAwareServices + */ + public function __construct( + private string $locale, + private iterable $localeAwareServices, + private ?RequestContext $requestContext = null, + ) { + $this->defaultLocale = $locale; + } + + public function setLocale(string $locale): void + { + if (class_exists(\Locale::class)) { + \Locale::setDefault($locale); + } + $this->locale = $locale; + $this->requestContext?->setParameter('_locale', $locale); + + foreach ($this->localeAwareServices as $service) { + $service->setLocale($locale); + } + } + + public function getLocale(): string + { + return $this->locale; + } + + /** + * Switch to a new locale, execute a callback, then switch back to the original. + * + * @template T + * + * @param callable(string $locale):T $callback + * + * @return T + */ + public function runWithLocale(string $locale, callable $callback): mixed + { + $original = $this->getLocale(); + $this->setLocale($locale); + + try { + return $callback($locale); + } finally { + $this->setLocale($original); + } + } + + public function reset(): void + { + $this->setLocale($this->defaultLocale); + } +} diff --git a/3rdparty/symfony/translation/LoggingTranslator.php b/3rdparty/symfony/translation/LoggingTranslator.php new file mode 100644 index 00000000..4a560bd6 --- /dev/null +++ b/3rdparty/symfony/translation/LoggingTranslator.php @@ -0,0 +1,115 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Component\Translation; + +use Psr\Log\LoggerInterface; +use Symfony\Component\Translation\Exception\InvalidArgumentException; +use Symfony\Contracts\Translation\LocaleAwareInterface; +use Symfony\Contracts\Translation\TranslatorInterface; + +/** + * @author Abdellatif Ait boudad + */ +class LoggingTranslator implements TranslatorInterface, TranslatorBagInterface, LocaleAwareInterface +{ + private TranslatorInterface $translator; + private LoggerInterface $logger; + + /** + * @param TranslatorInterface&TranslatorBagInterface&LocaleAwareInterface $translator The translator must implement TranslatorBagInterface + */ + public function __construct(TranslatorInterface $translator, LoggerInterface $logger) + { + if (!$translator instanceof TranslatorBagInterface || !$translator instanceof LocaleAwareInterface) { + throw new InvalidArgumentException(sprintf('The Translator "%s" must implement TranslatorInterface, TranslatorBagInterface and LocaleAwareInterface.', get_debug_type($translator))); + } + + $this->translator = $translator; + $this->logger = $logger; + } + + public function trans(?string $id, array $parameters = [], ?string $domain = null, ?string $locale = null): string + { + $trans = $this->translator->trans($id = (string) $id, $parameters, $domain, $locale); + $this->log($id, $domain, $locale); + + return $trans; + } + + /** + * @return void + */ + public function setLocale(string $locale) + { + $prev = $this->translator->getLocale(); + $this->translator->setLocale($locale); + if ($prev === $locale) { + return; + } + + $this->logger->debug(sprintf('The locale of the translator has changed from "%s" to "%s".', $prev, $locale)); + } + + public function getLocale(): string + { + return $this->translator->getLocale(); + } + + public function getCatalogue(?string $locale = null): MessageCatalogueInterface + { + return $this->translator->getCatalogue($locale); + } + + public function getCatalogues(): array + { + return $this->translator->getCatalogues(); + } + + /** + * Gets the fallback locales. + */ + public function getFallbackLocales(): array + { + if ($this->translator instanceof Translator || method_exists($this->translator, 'getFallbackLocales')) { + return $this->translator->getFallbackLocales(); + } + + return []; + } + + /** + * @return mixed + */ + public function __call(string $method, array $args) + { + return $this->translator->{$method}(...$args); + } + + /** + * Logs for missing translations. + */ + private function log(string $id, ?string $domain, ?string $locale): void + { + $domain ??= 'messages'; + + $catalogue = $this->translator->getCatalogue($locale); + if ($catalogue->defines($id, $domain)) { + return; + } + + if ($catalogue->has($id, $domain)) { + $this->logger->debug('Translation use fallback catalogue.', ['id' => $id, 'domain' => $domain, 'locale' => $catalogue->getLocale()]); + } else { + $this->logger->warning('Translation not found.', ['id' => $id, 'domain' => $domain, 'locale' => $catalogue->getLocale()]); + } + } +} diff --git a/3rdparty/symfony/translation/MessageCatalogue.php b/3rdparty/symfony/translation/MessageCatalogue.php new file mode 100644 index 00000000..d56f0439 --- /dev/null +++ b/3rdparty/symfony/translation/MessageCatalogue.php @@ -0,0 +1,338 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Component\Translation; + +use Symfony\Component\Config\Resource\ResourceInterface; +use Symfony\Component\Translation\Exception\LogicException; + +/** + * @author Fabien Potencier + */ +class MessageCatalogue implements MessageCatalogueInterface, MetadataAwareInterface, CatalogueMetadataAwareInterface +{ + private array $messages = []; + private array $metadata = []; + private array $catalogueMetadata = []; + private array $resources = []; + private string $locale; + private ?MessageCatalogueInterface $fallbackCatalogue = null; + private ?self $parent = null; + + /** + * @param array $messages An array of messages classified by domain + */ + public function __construct(string $locale, array $messages = []) + { + $this->locale = $locale; + $this->messages = $messages; + } + + public function getLocale(): string + { + return $this->locale; + } + + public function getDomains(): array + { + $domains = []; + + foreach ($this->messages as $domain => $messages) { + if (str_ends_with($domain, self::INTL_DOMAIN_SUFFIX)) { + $domain = substr($domain, 0, -\strlen(self::INTL_DOMAIN_SUFFIX)); + } + $domains[$domain] = $domain; + } + + return array_values($domains); + } + + public function all(?string $domain = null): array + { + if (null !== $domain) { + // skip messages merge if intl-icu requested explicitly + if (str_ends_with($domain, self::INTL_DOMAIN_SUFFIX)) { + return $this->messages[$domain] ?? []; + } + + return ($this->messages[$domain.self::INTL_DOMAIN_SUFFIX] ?? []) + ($this->messages[$domain] ?? []); + } + + $allMessages = []; + + foreach ($this->messages as $domain => $messages) { + if (str_ends_with($domain, self::INTL_DOMAIN_SUFFIX)) { + $domain = substr($domain, 0, -\strlen(self::INTL_DOMAIN_SUFFIX)); + $allMessages[$domain] = $messages + ($allMessages[$domain] ?? []); + } else { + $allMessages[$domain] = ($allMessages[$domain] ?? []) + $messages; + } + } + + return $allMessages; + } + + /** + * @return void + */ + public function set(string $id, string $translation, string $domain = 'messages') + { + $this->add([$id => $translation], $domain); + } + + public function has(string $id, string $domain = 'messages'): bool + { + if (isset($this->messages[$domain][$id]) || isset($this->messages[$domain.self::INTL_DOMAIN_SUFFIX][$id])) { + return true; + } + + if (null !== $this->fallbackCatalogue) { + return $this->fallbackCatalogue->has($id, $domain); + } + + return false; + } + + public function defines(string $id, string $domain = 'messages'): bool + { + return isset($this->messages[$domain][$id]) || isset($this->messages[$domain.self::INTL_DOMAIN_SUFFIX][$id]); + } + + public function get(string $id, string $domain = 'messages'): string + { + if (isset($this->messages[$domain.self::INTL_DOMAIN_SUFFIX][$id])) { + return $this->messages[$domain.self::INTL_DOMAIN_SUFFIX][$id]; + } + + if (isset($this->messages[$domain][$id])) { + return $this->messages[$domain][$id]; + } + + if (null !== $this->fallbackCatalogue) { + return $this->fallbackCatalogue->get($id, $domain); + } + + return $id; + } + + /** + * @return void + */ + public function replace(array $messages, string $domain = 'messages') + { + unset($this->messages[$domain], $this->messages[$domain.self::INTL_DOMAIN_SUFFIX]); + + $this->add($messages, $domain); + } + + /** + * @return void + */ + public function add(array $messages, string $domain = 'messages') + { + $altDomain = str_ends_with($domain, self::INTL_DOMAIN_SUFFIX) ? substr($domain, 0, -\strlen(self::INTL_DOMAIN_SUFFIX)) : $domain.self::INTL_DOMAIN_SUFFIX; + foreach ($messages as $id => $message) { + unset($this->messages[$altDomain][$id]); + $this->messages[$domain][$id] = $message; + } + + if ([] === ($this->messages[$altDomain] ?? null)) { + unset($this->messages[$altDomain]); + } + } + + /** + * @return void + */ + public function addCatalogue(MessageCatalogueInterface $catalogue) + { + if ($catalogue->getLocale() !== $this->locale) { + throw new LogicException(sprintf('Cannot add a catalogue for locale "%s" as the current locale for this catalogue is "%s".', $catalogue->getLocale(), $this->locale)); + } + + foreach ($catalogue->all() as $domain => $messages) { + if ($intlMessages = $catalogue->all($domain.self::INTL_DOMAIN_SUFFIX)) { + $this->add($intlMessages, $domain.self::INTL_DOMAIN_SUFFIX); + $messages = array_diff_key($messages, $intlMessages); + } + $this->add($messages, $domain); + } + + foreach ($catalogue->getResources() as $resource) { + $this->addResource($resource); + } + + if ($catalogue instanceof MetadataAwareInterface) { + $metadata = $catalogue->getMetadata('', ''); + $this->addMetadata($metadata); + } + + if ($catalogue instanceof CatalogueMetadataAwareInterface) { + $catalogueMetadata = $catalogue->getCatalogueMetadata('', ''); + $this->addCatalogueMetadata($catalogueMetadata); + } + } + + /** + * @return void + */ + public function addFallbackCatalogue(MessageCatalogueInterface $catalogue) + { + // detect circular references + $c = $catalogue; + while ($c = $c->getFallbackCatalogue()) { + if ($c->getLocale() === $this->getLocale()) { + throw new LogicException(sprintf('Circular reference detected when adding a fallback catalogue for locale "%s".', $catalogue->getLocale())); + } + } + + $c = $this; + do { + if ($c->getLocale() === $catalogue->getLocale()) { + throw new LogicException(sprintf('Circular reference detected when adding a fallback catalogue for locale "%s".', $catalogue->getLocale())); + } + + foreach ($catalogue->getResources() as $resource) { + $c->addResource($resource); + } + } while ($c = $c->parent); + + $catalogue->parent = $this; + $this->fallbackCatalogue = $catalogue; + + foreach ($catalogue->getResources() as $resource) { + $this->addResource($resource); + } + } + + public function getFallbackCatalogue(): ?MessageCatalogueInterface + { + return $this->fallbackCatalogue; + } + + public function getResources(): array + { + return array_values($this->resources); + } + + /** + * @return void + */ + public function addResource(ResourceInterface $resource) + { + $this->resources[$resource->__toString()] = $resource; + } + + public function getMetadata(string $key = '', string $domain = 'messages'): mixed + { + if ('' == $domain) { + return $this->metadata; + } + + if (isset($this->metadata[$domain])) { + if ('' == $key) { + return $this->metadata[$domain]; + } + + if (isset($this->metadata[$domain][$key])) { + return $this->metadata[$domain][$key]; + } + } + + return null; + } + + /** + * @return void + */ + public function setMetadata(string $key, mixed $value, string $domain = 'messages') + { + $this->metadata[$domain][$key] = $value; + } + + /** + * @return void + */ + public function deleteMetadata(string $key = '', string $domain = 'messages') + { + if ('' == $domain) { + $this->metadata = []; + } elseif ('' == $key) { + unset($this->metadata[$domain]); + } else { + unset($this->metadata[$domain][$key]); + } + } + + public function getCatalogueMetadata(string $key = '', string $domain = 'messages'): mixed + { + if (!$domain) { + return $this->catalogueMetadata; + } + + if (isset($this->catalogueMetadata[$domain])) { + if (!$key) { + return $this->catalogueMetadata[$domain]; + } + + if (isset($this->catalogueMetadata[$domain][$key])) { + return $this->catalogueMetadata[$domain][$key]; + } + } + + return null; + } + + /** + * @return void + */ + public function setCatalogueMetadata(string $key, mixed $value, string $domain = 'messages') + { + $this->catalogueMetadata[$domain][$key] = $value; + } + + /** + * @return void + */ + public function deleteCatalogueMetadata(string $key = '', string $domain = 'messages') + { + if (!$domain) { + $this->catalogueMetadata = []; + } elseif (!$key) { + unset($this->catalogueMetadata[$domain]); + } else { + unset($this->catalogueMetadata[$domain][$key]); + } + } + + /** + * Adds current values with the new values. + * + * @param array $values Values to add + */ + private function addMetadata(array $values): void + { + foreach ($values as $domain => $keys) { + foreach ($keys as $key => $value) { + $this->setMetadata($key, $value, $domain); + } + } + } + + private function addCatalogueMetadata(array $values): void + { + foreach ($values as $domain => $keys) { + foreach ($keys as $key => $value) { + $this->setCatalogueMetadata($key, $value, $domain); + } + } + } +} diff --git a/3rdparty/symfony/translation/MessageCatalogueInterface.php b/3rdparty/symfony/translation/MessageCatalogueInterface.php new file mode 100644 index 00000000..fd0d26d7 --- /dev/null +++ b/3rdparty/symfony/translation/MessageCatalogueInterface.php @@ -0,0 +1,134 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Component\Translation; + +use Symfony\Component\Config\Resource\ResourceInterface; + +/** + * MessageCatalogueInterface. + * + * @author Fabien Potencier + */ +interface MessageCatalogueInterface +{ + public const INTL_DOMAIN_SUFFIX = '+intl-icu'; + + /** + * Gets the catalogue locale. + */ + public function getLocale(): string; + + /** + * Gets the domains. + */ + public function getDomains(): array; + + /** + * Gets the messages within a given domain. + * + * If $domain is null, it returns all messages. + */ + public function all(?string $domain = null): array; + + /** + * Sets a message translation. + * + * @param string $id The message id + * @param string $translation The messages translation + * @param string $domain The domain name + * + * @return void + */ + public function set(string $id, string $translation, string $domain = 'messages'); + + /** + * Checks if a message has a translation. + * + * @param string $id The message id + * @param string $domain The domain name + */ + public function has(string $id, string $domain = 'messages'): bool; + + /** + * Checks if a message has a translation (it does not take into account the fallback mechanism). + * + * @param string $id The message id + * @param string $domain The domain name + */ + public function defines(string $id, string $domain = 'messages'): bool; + + /** + * Gets a message translation. + * + * @param string $id The message id + * @param string $domain The domain name + */ + public function get(string $id, string $domain = 'messages'): string; + + /** + * Sets translations for a given domain. + * + * @param array $messages An array of translations + * @param string $domain The domain name + * + * @return void + */ + public function replace(array $messages, string $domain = 'messages'); + + /** + * Adds translations for a given domain. + * + * @param array $messages An array of translations + * @param string $domain The domain name + * + * @return void + */ + public function add(array $messages, string $domain = 'messages'); + + /** + * Merges translations from the given Catalogue into the current one. + * + * The two catalogues must have the same locale. + * + * @return void + */ + public function addCatalogue(self $catalogue); + + /** + * Merges translations from the given Catalogue into the current one + * only when the translation does not exist. + * + * This is used to provide default translations when they do not exist for the current locale. + * + * @return void + */ + public function addFallbackCatalogue(self $catalogue); + + /** + * Gets the fallback catalogue. + */ + public function getFallbackCatalogue(): ?self; + + /** + * Returns an array of resources loaded to build this collection. + * + * @return ResourceInterface[] + */ + public function getResources(): array; + + /** + * Adds a resource for this collection. + * + * @return void + */ + public function addResource(ResourceInterface $resource); +} diff --git a/3rdparty/symfony/translation/MetadataAwareInterface.php b/3rdparty/symfony/translation/MetadataAwareInterface.php new file mode 100644 index 00000000..39e5326c --- /dev/null +++ b/3rdparty/symfony/translation/MetadataAwareInterface.php @@ -0,0 +1,48 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Component\Translation; + +/** + * This interface is used to get, set, and delete metadata about the translation messages. + * + * @author Fabien Potencier + */ +interface MetadataAwareInterface +{ + /** + * Gets metadata for the given domain and key. + * + * Passing an empty domain will return an array with all metadata indexed by + * domain and then by key. Passing an empty key will return an array with all + * metadata for the given domain. + * + * @return mixed The value that was set or an array with the domains/keys or null + */ + public function getMetadata(string $key = '', string $domain = 'messages'): mixed; + + /** + * Adds metadata to a message domain. + * + * @return void + */ + public function setMetadata(string $key, mixed $value, string $domain = 'messages'); + + /** + * Deletes metadata for the given key and domain. + * + * Passing an empty domain will delete all metadata. Passing an empty key will + * delete all metadata for the given domain. + * + * @return void + */ + public function deleteMetadata(string $key = '', string $domain = 'messages'); +} diff --git a/3rdparty/symfony/translation/Provider/AbstractProviderFactory.php b/3rdparty/symfony/translation/Provider/AbstractProviderFactory.php new file mode 100644 index 00000000..f0c11d85 --- /dev/null +++ b/3rdparty/symfony/translation/Provider/AbstractProviderFactory.php @@ -0,0 +1,37 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Component\Translation\Provider; + +use Symfony\Component\Translation\Exception\IncompleteDsnException; + +abstract class AbstractProviderFactory implements ProviderFactoryInterface +{ + public function supports(Dsn $dsn): bool + { + return \in_array($dsn->getScheme(), $this->getSupportedSchemes(), true); + } + + /** + * @return string[] + */ + abstract protected function getSupportedSchemes(): array; + + protected function getUser(Dsn $dsn): string + { + return $dsn->getUser() ?? throw new IncompleteDsnException('User is not set.', $dsn->getScheme().'://'.$dsn->getHost()); + } + + protected function getPassword(Dsn $dsn): string + { + return $dsn->getPassword() ?? throw new IncompleteDsnException('Password is not set.', $dsn->getOriginalDsn()); + } +} diff --git a/3rdparty/symfony/translation/Provider/Dsn.php b/3rdparty/symfony/translation/Provider/Dsn.php new file mode 100644 index 00000000..1d90e27f --- /dev/null +++ b/3rdparty/symfony/translation/Provider/Dsn.php @@ -0,0 +1,110 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Component\Translation\Provider; + +use Symfony\Component\Translation\Exception\InvalidArgumentException; +use Symfony\Component\Translation\Exception\MissingRequiredOptionException; + +/** + * @author Fabien Potencier + * @author Oskar Stark + */ +final class Dsn +{ + private ?string $scheme; + private ?string $host; + private ?string $user; + private ?string $password; + private ?int $port; + private ?string $path; + private array $options = []; + private string $originalDsn; + + public function __construct(#[\SensitiveParameter] string $dsn) + { + $this->originalDsn = $dsn; + + if (false === $params = parse_url($dsn)) { + throw new InvalidArgumentException('The translation provider DSN is invalid.'); + } + + if (!isset($params['scheme'])) { + throw new InvalidArgumentException('The translation provider DSN must contain a scheme.'); + } + $this->scheme = $params['scheme']; + + if (!isset($params['host'])) { + throw new InvalidArgumentException('The translation provider DSN must contain a host (use "default" by default).'); + } + $this->host = $params['host']; + + $this->user = '' !== ($params['user'] ?? '') ? rawurldecode($params['user']) : null; + $this->password = '' !== ($params['pass'] ?? '') ? rawurldecode($params['pass']) : null; + $this->port = $params['port'] ?? null; + $this->path = $params['path'] ?? null; + parse_str($params['query'] ?? '', $this->options); + } + + public function getScheme(): string + { + return $this->scheme; + } + + public function getHost(): string + { + return $this->host; + } + + public function getUser(): ?string + { + return $this->user; + } + + public function getPassword(): ?string + { + return $this->password; + } + + public function getPort(?int $default = null): ?int + { + return $this->port ?? $default; + } + + public function getOption(string $key, mixed $default = null): mixed + { + return $this->options[$key] ?? $default; + } + + public function getRequiredOption(string $key): mixed + { + if (!\array_key_exists($key, $this->options) || '' === trim($this->options[$key])) { + throw new MissingRequiredOptionException($key); + } + + return $this->options[$key]; + } + + public function getOptions(): array + { + return $this->options; + } + + public function getPath(): ?string + { + return $this->path; + } + + public function getOriginalDsn(): string + { + return $this->originalDsn; + } +} diff --git a/3rdparty/symfony/translation/Provider/FilteringProvider.php b/3rdparty/symfony/translation/Provider/FilteringProvider.php new file mode 100644 index 00000000..d4465b9f --- /dev/null +++ b/3rdparty/symfony/translation/Provider/FilteringProvider.php @@ -0,0 +1,62 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Component\Translation\Provider; + +use Symfony\Component\Translation\TranslatorBag; +use Symfony\Component\Translation\TranslatorBagInterface; + +/** + * Filters domains and locales between the Translator config values and those specific to each provider. + * + * @author Mathieu Santostefano + */ +class FilteringProvider implements ProviderInterface +{ + private ProviderInterface $provider; + private array $locales; + private array $domains; + + public function __construct(ProviderInterface $provider, array $locales, array $domains = []) + { + $this->provider = $provider; + $this->locales = $locales; + $this->domains = $domains; + } + + public function __toString(): string + { + return (string) $this->provider; + } + + public function write(TranslatorBagInterface $translatorBag): void + { + $this->provider->write($translatorBag); + } + + public function read(array $domains, array $locales): TranslatorBag + { + $domains = !$this->domains ? $domains : array_intersect($this->domains, $domains); + $locales = array_intersect($this->locales, $locales); + + return $this->provider->read($domains, $locales); + } + + public function delete(TranslatorBagInterface $translatorBag): void + { + $this->provider->delete($translatorBag); + } + + public function getDomains(): array + { + return $this->domains; + } +} diff --git a/3rdparty/symfony/translation/Provider/NullProvider.php b/3rdparty/symfony/translation/Provider/NullProvider.php new file mode 100644 index 00000000..f00392ea --- /dev/null +++ b/3rdparty/symfony/translation/Provider/NullProvider.php @@ -0,0 +1,39 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Component\Translation\Provider; + +use Symfony\Component\Translation\TranslatorBag; +use Symfony\Component\Translation\TranslatorBagInterface; + +/** + * @author Mathieu Santostefano + */ +class NullProvider implements ProviderInterface +{ + public function __toString(): string + { + return 'null'; + } + + public function write(TranslatorBagInterface $translatorBag, bool $override = false): void + { + } + + public function read(array $domains, array $locales): TranslatorBag + { + return new TranslatorBag(); + } + + public function delete(TranslatorBagInterface $translatorBag): void + { + } +} diff --git a/3rdparty/symfony/translation/Provider/NullProviderFactory.php b/3rdparty/symfony/translation/Provider/NullProviderFactory.php new file mode 100644 index 00000000..f350f160 --- /dev/null +++ b/3rdparty/symfony/translation/Provider/NullProviderFactory.php @@ -0,0 +1,34 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Component\Translation\Provider; + +use Symfony\Component\Translation\Exception\UnsupportedSchemeException; + +/** + * @author Mathieu Santostefano + */ +final class NullProviderFactory extends AbstractProviderFactory +{ + public function create(Dsn $dsn): ProviderInterface + { + if ('null' === $dsn->getScheme()) { + return new NullProvider(); + } + + throw new UnsupportedSchemeException($dsn, 'null', $this->getSupportedSchemes()); + } + + protected function getSupportedSchemes(): array + { + return ['null']; + } +} diff --git a/3rdparty/symfony/translation/Provider/ProviderFactoryInterface.php b/3rdparty/symfony/translation/Provider/ProviderFactoryInterface.php new file mode 100644 index 00000000..3fd4494b --- /dev/null +++ b/3rdparty/symfony/translation/Provider/ProviderFactoryInterface.php @@ -0,0 +1,26 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Component\Translation\Provider; + +use Symfony\Component\Translation\Exception\IncompleteDsnException; +use Symfony\Component\Translation\Exception\UnsupportedSchemeException; + +interface ProviderFactoryInterface +{ + /** + * @throws UnsupportedSchemeException + * @throws IncompleteDsnException + */ + public function create(Dsn $dsn): ProviderInterface; + + public function supports(Dsn $dsn): bool; +} diff --git a/3rdparty/symfony/translation/Provider/ProviderInterface.php b/3rdparty/symfony/translation/Provider/ProviderInterface.php new file mode 100644 index 00000000..0e47083b --- /dev/null +++ b/3rdparty/symfony/translation/Provider/ProviderInterface.php @@ -0,0 +1,30 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Component\Translation\Provider; + +use Symfony\Component\Translation\TranslatorBag; +use Symfony\Component\Translation\TranslatorBagInterface; + +interface ProviderInterface extends \Stringable +{ + /** + * Translations available in the TranslatorBag only must be created. + * Translations available in both the TranslatorBag and on the provider + * must be overwritten. + * Translations available on the provider only must be kept. + */ + public function write(TranslatorBagInterface $translatorBag): void; + + public function read(array $domains, array $locales): TranslatorBag; + + public function delete(TranslatorBagInterface $translatorBag): void; +} diff --git a/3rdparty/symfony/translation/Provider/TranslationProviderCollection.php b/3rdparty/symfony/translation/Provider/TranslationProviderCollection.php new file mode 100644 index 00000000..b917415b --- /dev/null +++ b/3rdparty/symfony/translation/Provider/TranslationProviderCollection.php @@ -0,0 +1,57 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Component\Translation\Provider; + +use Symfony\Component\Translation\Exception\InvalidArgumentException; + +/** + * @author Mathieu Santostefano + */ +final class TranslationProviderCollection +{ + /** + * @var array + */ + private array $providers; + + /** + * @param array $providers + */ + public function __construct(iterable $providers) + { + $this->providers = \is_array($providers) ? $providers : iterator_to_array($providers); + } + + public function __toString(): string + { + return '['.implode(',', array_keys($this->providers)).']'; + } + + public function has(string $name): bool + { + return isset($this->providers[$name]); + } + + public function get(string $name): ProviderInterface + { + if (!$this->has($name)) { + throw new InvalidArgumentException(sprintf('Provider "%s" not found. Available: "%s".', $name, (string) $this)); + } + + return $this->providers[$name]; + } + + public function keys(): array + { + return array_keys($this->providers); + } +} diff --git a/3rdparty/symfony/translation/Provider/TranslationProviderCollectionFactory.php b/3rdparty/symfony/translation/Provider/TranslationProviderCollectionFactory.php new file mode 100644 index 00000000..6300c875 --- /dev/null +++ b/3rdparty/symfony/translation/Provider/TranslationProviderCollectionFactory.php @@ -0,0 +1,57 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Component\Translation\Provider; + +use Symfony\Component\Translation\Exception\UnsupportedSchemeException; + +/** + * @author Mathieu Santostefano + */ +class TranslationProviderCollectionFactory +{ + private iterable $factories; + private array $enabledLocales; + + /** + * @param iterable $factories + */ + public function __construct(iterable $factories, array $enabledLocales) + { + $this->factories = $factories; + $this->enabledLocales = $enabledLocales; + } + + public function fromConfig(array $config): TranslationProviderCollection + { + $providers = []; + foreach ($config as $name => $currentConfig) { + $providers[$name] = $this->fromDsnObject( + new Dsn($currentConfig['dsn']), + !$currentConfig['locales'] ? $this->enabledLocales : $currentConfig['locales'], + !$currentConfig['domains'] ? [] : $currentConfig['domains'] + ); + } + + return new TranslationProviderCollection($providers); + } + + public function fromDsnObject(Dsn $dsn, array $locales, array $domains = []): ProviderInterface + { + foreach ($this->factories as $factory) { + if ($factory->supports($dsn)) { + return new FilteringProvider($factory->create($dsn), $locales, $domains); + } + } + + throw new UnsupportedSchemeException($dsn); + } +} diff --git a/3rdparty/symfony/translation/PseudoLocalizationTranslator.php b/3rdparty/symfony/translation/PseudoLocalizationTranslator.php new file mode 100644 index 00000000..f26909f5 --- /dev/null +++ b/3rdparty/symfony/translation/PseudoLocalizationTranslator.php @@ -0,0 +1,365 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Component\Translation; + +use Symfony\Contracts\Translation\TranslatorInterface; + +/** + * This translator should only be used in a development environment. + */ +final class PseudoLocalizationTranslator implements TranslatorInterface +{ + private const EXPANSION_CHARACTER = '~'; + + private TranslatorInterface $translator; + private bool $accents; + private float $expansionFactor; + private bool $brackets; + private bool $parseHTML; + + /** + * @var string[] + */ + private array $localizableHTMLAttributes; + + /** + * Available options: + * * accents: + * type: boolean + * default: true + * description: replace ASCII characters of the translated string with accented versions or similar characters + * example: if true, "foo" => "ƒöö". + * + * * expansion_factor: + * type: float + * default: 1 + * validation: it must be greater than or equal to 1 + * description: expand the translated string by the given factor with spaces and tildes + * example: if 2, "foo" => "~foo ~" + * + * * brackets: + * type: boolean + * default: true + * description: wrap the translated string with brackets + * example: if true, "foo" => "[foo]" + * + * * parse_html: + * type: boolean + * default: false + * description: parse the translated string as HTML - looking for HTML tags has a performance impact but allows to preserve them from alterations - it also allows to compute the visible translated string length which is useful to correctly expand ot when it contains HTML + * warning: unclosed tags are unsupported, they will be fixed (closed) by the parser - eg, "foo
    bar" => "foo
    bar
    " + * + * * localizable_html_attributes: + * type: string[] + * default: [] + * description: the list of HTML attributes whose values can be altered - it is only useful when the "parse_html" option is set to true + * example: if ["title"], and with the "accents" option set to true, "Profile" => "Þŕöƒîļé" - if "title" was not in the "localizable_html_attributes" list, the title attribute data would be left unchanged. + */ + public function __construct(TranslatorInterface $translator, array $options = []) + { + $this->translator = $translator; + $this->accents = $options['accents'] ?? true; + + if (1.0 > ($this->expansionFactor = $options['expansion_factor'] ?? 1.0)) { + throw new \InvalidArgumentException('The expansion factor must be greater than or equal to 1.'); + } + + $this->brackets = $options['brackets'] ?? true; + + $this->parseHTML = $options['parse_html'] ?? false; + if ($this->parseHTML && !$this->accents && 1.0 === $this->expansionFactor) { + $this->parseHTML = false; + } + + $this->localizableHTMLAttributes = $options['localizable_html_attributes'] ?? []; + } + + public function trans(string $id, array $parameters = [], ?string $domain = null, ?string $locale = null): string + { + $trans = ''; + $visibleText = ''; + + foreach ($this->getParts($this->translator->trans($id, $parameters, $domain, $locale)) as [$visible, $localizable, $text]) { + if ($visible) { + $visibleText .= $text; + } + + if (!$localizable) { + $trans .= $text; + + continue; + } + + $this->addAccents($trans, $text); + } + + $this->expand($trans, $visibleText); + + $this->addBrackets($trans); + + return $trans; + } + + public function getLocale(): string + { + return $this->translator->getLocale(); + } + + private function getParts(string $originalTrans): array + { + if (!$this->parseHTML) { + return [[true, true, $originalTrans]]; + } + + $html = mb_encode_numericentity($originalTrans, [0x80, 0x10FFFF, 0, 0x1FFFFF], mb_detect_encoding($originalTrans, null, true) ?: 'UTF-8'); + + $useInternalErrors = libxml_use_internal_errors(true); + + $dom = new \DOMDocument(); + $dom->loadHTML(''.$html.''); + + libxml_clear_errors(); + libxml_use_internal_errors($useInternalErrors); + + return $this->parseNode($dom->childNodes->item(1)->childNodes->item(0)->childNodes->item(0)); + } + + private function parseNode(\DOMNode $node): array + { + $parts = []; + + foreach ($node->childNodes as $childNode) { + if (!$childNode instanceof \DOMElement) { + $parts[] = [true, true, $childNode->nodeValue]; + + continue; + } + + $parts[] = [false, false, '<'.$childNode->tagName]; + + /** @var \DOMAttr $attribute */ + foreach ($childNode->attributes as $attribute) { + $parts[] = [false, false, ' '.$attribute->nodeName.'="']; + + $localizableAttribute = \in_array($attribute->nodeName, $this->localizableHTMLAttributes, true); + foreach (preg_split('/(&(?:amp|quot|#039|lt|gt);+)/', htmlspecialchars($attribute->nodeValue, \ENT_QUOTES, 'UTF-8'), -1, \PREG_SPLIT_DELIM_CAPTURE) as $i => $match) { + if ('' === $match) { + continue; + } + + $parts[] = [false, $localizableAttribute && 0 === $i % 2, $match]; + } + + $parts[] = [false, false, '"']; + } + + $parts[] = [false, false, '>']; + + $parts = array_merge($parts, $this->parseNode($childNode, $parts)); + + $parts[] = [false, false, 'tagName.'>']; + } + + return $parts; + } + + private function addAccents(string &$trans, string $text): void + { + $trans .= $this->accents ? strtr($text, [ + ' ' => ' ', + '!' => '¡', + '"' => '″', + '#' => '♯', + '$' => '€', + '%' => '‰', + '&' => '⅋', + '\'' => '´', + '(' => '{', + ')' => '}', + '*' => '⁎', + '+' => '⁺', + ',' => '،', + '-' => '‐', + '.' => '·', + '/' => '⁄', + '0' => '⓪', + '1' => '①', + '2' => '②', + '3' => '③', + '4' => '④', + '5' => '⑤', + '6' => '⑥', + '7' => '⑦', + '8' => '⑧', + '9' => '⑨', + ':' => '∶', + ';' => '⁏', + '<' => '≤', + '=' => '≂', + '>' => '≥', + '?' => '¿', + '@' => '՞', + 'A' => 'Å', + 'B' => 'Ɓ', + 'C' => 'Ç', + 'D' => 'Ð', + 'E' => 'É', + 'F' => 'Ƒ', + 'G' => 'Ĝ', + 'H' => 'Ĥ', + 'I' => 'Î', + 'J' => 'Ĵ', + 'K' => 'Ķ', + 'L' => 'Ļ', + 'M' => 'Ṁ', + 'N' => 'Ñ', + 'O' => 'Ö', + 'P' => 'Þ', + 'Q' => 'Ǫ', + 'R' => 'Ŕ', + 'S' => 'Š', + 'T' => 'Ţ', + 'U' => 'Û', + 'V' => 'Ṽ', + 'W' => 'Ŵ', + 'X' => 'Ẋ', + 'Y' => 'Ý', + 'Z' => 'Ž', + '[' => '⁅', + '\\' => '∖', + ']' => '⁆', + '^' => '˄', + '_' => '‿', + '`' => '‵', + 'a' => 'å', + 'b' => 'ƀ', + 'c' => 'ç', + 'd' => 'ð', + 'e' => 'é', + 'f' => 'ƒ', + 'g' => 'ĝ', + 'h' => 'ĥ', + 'i' => 'î', + 'j' => 'ĵ', + 'k' => 'ķ', + 'l' => 'ļ', + 'm' => 'ɱ', + 'n' => 'ñ', + 'o' => 'ö', + 'p' => 'þ', + 'q' => 'ǫ', + 'r' => 'ŕ', + 's' => 'š', + 't' => 'ţ', + 'u' => 'û', + 'v' => 'ṽ', + 'w' => 'ŵ', + 'x' => 'ẋ', + 'y' => 'ý', + 'z' => 'ž', + '{' => '(', + '|' => '¦', + '}' => ')', + '~' => '˞', + ]) : $text; + } + + private function expand(string &$trans, string $visibleText): void + { + if (1.0 >= $this->expansionFactor) { + return; + } + + $visibleLength = $this->strlen($visibleText); + $missingLength = (int) ceil($visibleLength * $this->expansionFactor) - $visibleLength; + if ($this->brackets) { + $missingLength -= 2; + } + + if (0 >= $missingLength) { + return; + } + + $words = []; + $wordsCount = 0; + foreach (preg_split('/ +/', $visibleText, -1, \PREG_SPLIT_NO_EMPTY) as $word) { + $wordLength = $this->strlen($word); + + if ($wordLength >= $missingLength) { + continue; + } + + if (!isset($words[$wordLength])) { + $words[$wordLength] = 0; + } + + ++$words[$wordLength]; + ++$wordsCount; + } + + if (!$words) { + $trans .= 1 === $missingLength ? self::EXPANSION_CHARACTER : ' '.str_repeat(self::EXPANSION_CHARACTER, $missingLength - 1); + + return; + } + + arsort($words, \SORT_NUMERIC); + + $longestWordLength = max(array_keys($words)); + + while (true) { + $r = mt_rand(1, $wordsCount); + + foreach ($words as $length => $count) { + $r -= $count; + if ($r <= 0) { + break; + } + } + + $trans .= ' '.str_repeat(self::EXPANSION_CHARACTER, $length); + + $missingLength -= $length + 1; + + if (0 === $missingLength) { + return; + } + + while ($longestWordLength >= $missingLength) { + $wordsCount -= $words[$longestWordLength]; + unset($words[$longestWordLength]); + + if (!$words) { + $trans .= 1 === $missingLength ? self::EXPANSION_CHARACTER : ' '.str_repeat(self::EXPANSION_CHARACTER, $missingLength - 1); + + return; + } + + $longestWordLength = max(array_keys($words)); + } + } + } + + private function addBrackets(string &$trans): void + { + if (!$this->brackets) { + return; + } + + $trans = '['.$trans.']'; + } + + private function strlen(string $s): int + { + return false === ($encoding = mb_detect_encoding($s, null, true)) ? \strlen($s) : mb_strlen($s, $encoding); + } +} diff --git a/3rdparty/symfony/translation/Reader/TranslationReader.php b/3rdparty/symfony/translation/Reader/TranslationReader.php new file mode 100644 index 00000000..01408d4d --- /dev/null +++ b/3rdparty/symfony/translation/Reader/TranslationReader.php @@ -0,0 +1,64 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Component\Translation\Reader; + +use Symfony\Component\Finder\Finder; +use Symfony\Component\Translation\Loader\LoaderInterface; +use Symfony\Component\Translation\MessageCatalogue; + +/** + * TranslationReader reads translation messages from translation files. + * + * @author Michel Salib + */ +class TranslationReader implements TranslationReaderInterface +{ + /** + * Loaders used for import. + * + * @var array + */ + private array $loaders = []; + + /** + * Adds a loader to the translation extractor. + * + * @param string $format The format of the loader + * + * @return void + */ + public function addLoader(string $format, LoaderInterface $loader) + { + $this->loaders[$format] = $loader; + } + + /** + * @return void + */ + public function read(string $directory, MessageCatalogue $catalogue) + { + if (!is_dir($directory)) { + return; + } + + foreach ($this->loaders as $format => $loader) { + // load any existing translation files + $finder = new Finder(); + $extension = $catalogue->getLocale().'.'.$format; + $files = $finder->files()->name('*.'.$extension)->in($directory); + foreach ($files as $file) { + $domain = substr($file->getFilename(), 0, -1 * \strlen($extension) - 1); + $catalogue->addCatalogue($loader->load($file->getPathname(), $catalogue->getLocale(), $domain)); + } + } + } +} diff --git a/3rdparty/symfony/translation/Reader/TranslationReaderInterface.php b/3rdparty/symfony/translation/Reader/TranslationReaderInterface.php new file mode 100644 index 00000000..ea74dc23 --- /dev/null +++ b/3rdparty/symfony/translation/Reader/TranslationReaderInterface.php @@ -0,0 +1,29 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Component\Translation\Reader; + +use Symfony\Component\Translation\MessageCatalogue; + +/** + * TranslationReader reads translation messages from translation files. + * + * @author Tobias Nyholm + */ +interface TranslationReaderInterface +{ + /** + * Reads translation messages from a directory to the catalogue. + * + * @return void + */ + public function read(string $directory, MessageCatalogue $catalogue); +} diff --git a/3rdparty/symfony/translation/Resources/functions.php b/3rdparty/symfony/translation/Resources/functions.php new file mode 100644 index 00000000..0d2a037a --- /dev/null +++ b/3rdparty/symfony/translation/Resources/functions.php @@ -0,0 +1,22 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Component\Translation; + +if (!\function_exists(t::class)) { + /** + * @author Nate Wiebe + */ + function t(string $message, array $parameters = [], ?string $domain = null): TranslatableMessage + { + return new TranslatableMessage($message, $parameters, $domain); + } +} diff --git a/3rdparty/symfony/translation/Resources/schemas/xliff-core-1.2-transitional.xsd b/3rdparty/symfony/translation/Resources/schemas/xliff-core-1.2-transitional.xsd new file mode 100644 index 00000000..1f38de72 --- /dev/null +++ b/3rdparty/symfony/translation/Resources/schemas/xliff-core-1.2-transitional.xsd @@ -0,0 +1,2261 @@ + + + + + + + + + + + + + + Values for the attribute 'context-type'. + + + + + Indicates a database content. + + + + + Indicates the content of an element within an XML document. + + + + + Indicates the name of an element within an XML document. + + + + + Indicates the line number from the sourcefile (see context-type="sourcefile") where the <source> is found. + + + + + Indicates a the number of parameters contained within the <source>. + + + + + Indicates notes pertaining to the parameters in the <source>. + + + + + Indicates the content of a record within a database. + + + + + Indicates the name of a record within a database. + + + + + Indicates the original source file in the case that multiple files are merged to form the original file from which the XLIFF file is created. This differs from the original <file> attribute in that this sourcefile is one of many that make up that file. + + + + + + + Values for the attribute 'count-type'. + + + + + Indicates the count units are items that are used X times in a certain context; example: this is a reusable text unit which is used 42 times in other texts. + + + + + Indicates the count units are translation units existing already in the same document. + + + + + Indicates a total count. + + + + + + + Values for the attribute 'ctype' when used other elements than <ph> or <x>. + + + + + Indicates a run of bolded text. + + + + + Indicates a run of text in italics. + + + + + Indicates a run of underlined text. + + + + + Indicates a run of hyper-text. + + + + + + + Values for the attribute 'ctype' when used with <ph> or <x>. + + + + + Indicates a inline image. + + + + + Indicates a page break. + + + + + Indicates a line break. + + + + + + + + + + + + Values for the attribute 'datatype'. + + + + + Indicates Active Server Page data. + + + + + Indicates C source file data. + + + + + Indicates Channel Definition Format (CDF) data. + + + + + Indicates ColdFusion data. + + + + + Indicates C++ source file data. + + + + + Indicates C-Sharp data. + + + + + Indicates strings from C, ASM, and driver files data. + + + + + Indicates comma-separated values data. + + + + + Indicates database data. + + + + + Indicates portions of document that follows data and contains metadata. + + + + + Indicates portions of document that precedes data and contains metadata. + + + + + Indicates data from standard UI file operations dialogs (e.g., Open, Save, Save As, Export, Import). + + + + + Indicates standard user input screen data. + + + + + Indicates HyperText Markup Language (HTML) data - document instance. + + + + + Indicates content within an HTML document’s <body> element. + + + + + Indicates Windows INI file data. + + + + + Indicates Interleaf data. + + + + + Indicates Java source file data (extension '.java'). + + + + + Indicates Java property resource bundle data. + + + + + Indicates Java list resource bundle data. + + + + + Indicates JavaScript source file data. + + + + + Indicates JScript source file data. + + + + + Indicates information relating to formatting. + + + + + Indicates LISP source file data. + + + + + Indicates information relating to margin formats. + + + + + Indicates a file containing menu. + + + + + Indicates numerically identified string table. + + + + + Indicates Maker Interchange Format (MIF) data. + + + + + Indicates that the datatype attribute value is a MIME Type value and is defined in the mime-type attribute. + + + + + Indicates GNU Machine Object data. + + + + + Indicates Message Librarian strings created by Novell's Message Librarian Tool. + + + + + Indicates information to be displayed at the bottom of each page of a document. + + + + + Indicates information to be displayed at the top of each page of a document. + + + + + Indicates a list of property values (e.g., settings within INI files or preferences dialog). + + + + + Indicates Pascal source file data. + + + + + Indicates Hypertext Preprocessor data. + + + + + Indicates plain text file (no formatting other than, possibly, wrapping). + + + + + Indicates GNU Portable Object file. + + + + + Indicates dynamically generated user defined document. e.g. Oracle Report, Crystal Report, etc. + + + + + Indicates Windows .NET binary resources. + + + + + Indicates Windows .NET Resources. + + + + + Indicates Rich Text Format (RTF) data. + + + + + Indicates Standard Generalized Markup Language (SGML) data - document instance. + + + + + Indicates Standard Generalized Markup Language (SGML) data - Document Type Definition (DTD). + + + + + Indicates Scalable Vector Graphic (SVG) data. + + + + + Indicates VisualBasic Script source file. + + + + + Indicates warning message. + + + + + Indicates Windows (Win32) resources (i.e. resources extracted from an RC script, a message file, or a compiled file). + + + + + Indicates Extensible HyperText Markup Language (XHTML) data - document instance. + + + + + Indicates Extensible Markup Language (XML) data - document instance. + + + + + Indicates Extensible Markup Language (XML) data - Document Type Definition (DTD). + + + + + Indicates Extensible Stylesheet Language (XSL) data. + + + + + Indicates XUL elements. + + + + + + + Values for the attribute 'mtype'. + + + + + Indicates the marked text is an abbreviation. + + + + + ISO-12620 2.1.8: A term resulting from the omission of any part of the full term while designating the same concept. + + + + + ISO-12620 2.1.8.1: An abbreviated form of a simple term resulting from the omission of some of its letters (e.g. 'adj.' for 'adjective'). + + + + + ISO-12620 2.1.8.4: An abbreviated form of a term made up of letters from the full form of a multiword term strung together into a sequence pronounced only syllabically (e.g. 'radar' for 'radio detecting and ranging'). + + + + + ISO-12620: A proper-name term, such as the name of an agency or other proper entity. + + + + + ISO-12620 2.1.18.1: A recurrent word combination characterized by cohesion in that the components of the collocation must co-occur within an utterance or series of utterances, even though they do not necessarily have to maintain immediate proximity to one another. + + + + + ISO-12620 2.1.5: A synonym for an international scientific term that is used in general discourse in a given language. + + + + + Indicates the marked text is a date and/or time. + + + + + ISO-12620 2.1.15: An expression used to represent a concept based on a statement that two mathematical expressions are, for instance, equal as identified by the equal sign (=), or assigned to one another by a similar sign. + + + + + ISO-12620 2.1.7: The complete representation of a term for which there is an abbreviated form. + + + + + ISO-12620 2.1.14: Figures, symbols or the like used to express a concept briefly, such as a mathematical or chemical formula. + + + + + ISO-12620 2.1.1: The concept designation that has been chosen to head a terminological record. + + + + + ISO-12620 2.1.8.3: An abbreviated form of a term consisting of some of the initial letters of the words making up a multiword term or the term elements making up a compound term when these letters are pronounced individually (e.g. 'BSE' for 'bovine spongiform encephalopathy'). + + + + + ISO-12620 2.1.4: A term that is part of an international scientific nomenclature as adopted by an appropriate scientific body. + + + + + ISO-12620 2.1.6: A term that has the same or nearly identical orthographic or phonemic form in many languages. + + + + + ISO-12620 2.1.16: An expression used to represent a concept based on mathematical or logical relations, such as statements of inequality, set relationships, Boolean operations, and the like. + + + + + ISO-12620 2.1.17: A unit to track object. + + + + + Indicates the marked text is a name. + + + + + ISO-12620 2.1.3: A term that represents the same or a very similar concept as another term in the same language, but for which interchangeability is limited to some contexts and inapplicable in others. + + + + + ISO-12620 2.1.17.2: A unique alphanumeric designation assigned to an object in a manufacturing system. + + + + + Indicates the marked text is a phrase. + + + + + ISO-12620 2.1.18: Any group of two or more words that form a unit, the meaning of which frequently cannot be deduced based on the combined sense of the words making up the phrase. + + + + + Indicates the marked text should not be translated. + + + + + ISO-12620 2.1.12: A form of a term resulting from an operation whereby non-Latin writing systems are converted to the Latin alphabet. + + + + + Indicates that the marked text represents a segment. + + + + + ISO-12620 2.1.18.2: A fixed, lexicalized phrase. + + + + + ISO-12620 2.1.8.2: A variant of a multiword term that includes fewer words than the full form of the term (e.g. 'Group of Twenty-four' for 'Intergovernmental Group of Twenty-four on International Monetary Affairs'). + + + + + ISO-12620 2.1.17.1: Stock keeping unit, an inventory item identified by a unique alphanumeric designation assigned to an object in an inventory control system. + + + + + ISO-12620 2.1.19: A fixed chunk of recurring text. + + + + + ISO-12620 2.1.13: A designation of a concept by letters, numerals, pictograms or any combination thereof. + + + + + ISO-12620 2.1.2: Any term that represents the same or a very similar concept as the main entry term in a term entry. + + + + + ISO-12620 2.1.18.3: Phraseological unit in a language that expresses the same semantic content as another phrase in that same language. + + + + + Indicates the marked text is a term. + + + + + ISO-12620 2.1.11: A form of a term resulting from an operation whereby the characters of one writing system are represented by characters from another writing system, taking into account the pronunciation of the characters converted. + + + + + ISO-12620 2.1.10: A form of a term resulting from an operation whereby the characters of an alphabetic writing system are represented by characters from another alphabetic writing system. + + + + + ISO-12620 2.1.8.5: An abbreviated form of a term resulting from the omission of one or more term elements or syllables (e.g. 'flu' for 'influenza'). + + + + + ISO-12620 2.1.9: One of the alternate forms of a term. + + + + + + + Values for the attribute 'restype'. + + + + + Indicates a Windows RC AUTO3STATE control. + + + + + Indicates a Windows RC AUTOCHECKBOX control. + + + + + Indicates a Windows RC AUTORADIOBUTTON control. + + + + + Indicates a Windows RC BEDIT control. + + + + + Indicates a bitmap, for example a BITMAP resource in Windows. + + + + + Indicates a button object, for example a BUTTON control Windows. + + + + + Indicates a caption, such as the caption of a dialog box. + + + + + Indicates the cell in a table, for example the content of the <td> element in HTML. + + + + + Indicates check box object, for example a CHECKBOX control in Windows. + + + + + Indicates a menu item with an associated checkbox. + + + + + Indicates a list box, but with a check-box for each item. + + + + + Indicates a color selection dialog. + + + + + Indicates a combination of edit box and listbox object, for example a COMBOBOX control in Windows. + + + + + Indicates an initialization entry of an extended combobox DLGINIT resource block. (code 0x1234). + + + + + Indicates an initialization entry of a combobox DLGINIT resource block (code 0x0403). + + + + + Indicates a UI base class element that cannot be represented by any other element. + + + + + Indicates a context menu. + + + + + Indicates a Windows RC CTEXT control. + + + + + Indicates a cursor, for example a CURSOR resource in Windows. + + + + + Indicates a date/time picker. + + + + + Indicates a Windows RC DEFPUSHBUTTON control. + + + + + Indicates a dialog box. + + + + + Indicates a Windows RC DLGINIT resource block. + + + + + Indicates an edit box object, for example an EDIT control in Windows. + + + + + Indicates a filename. + + + + + Indicates a file dialog. + + + + + Indicates a footnote. + + + + + Indicates a font name. + + + + + Indicates a footer. + + + + + Indicates a frame object. + + + + + Indicates a XUL grid element. + + + + + Indicates a groupbox object, for example a GROUPBOX control in Windows. + + + + + Indicates a header item. + + + + + Indicates a heading, such has the content of <h1>, <h2>, etc. in HTML. + + + + + Indicates a Windows RC HEDIT control. + + + + + Indicates a horizontal scrollbar. + + + + + Indicates an icon, for example an ICON resource in Windows. + + + + + Indicates a Windows RC IEDIT control. + + + + + Indicates keyword list, such as the content of the Keywords meta-data in HTML, or a K footnote in WinHelp RTF. + + + + + Indicates a label object. + + + + + Indicates a label that is also a HTML link (not necessarily a URL). + + + + + Indicates a list (a group of list-items, for example an <ol> or <ul> element in HTML). + + + + + Indicates a listbox object, for example an LISTBOX control in Windows. + + + + + Indicates an list item (an entry in a list). + + + + + Indicates a Windows RC LTEXT control. + + + + + Indicates a menu (a group of menu-items). + + + + + Indicates a toolbar containing one or more tope level menus. + + + + + Indicates a menu item (an entry in a menu). + + + + + Indicates a XUL menuseparator element. + + + + + Indicates a message, for example an entry in a MESSAGETABLE resource in Windows. + + + + + Indicates a calendar control. + + + + + Indicates an edit box beside a spin control. + + + + + Indicates a catch all for rectangular areas. + + + + + Indicates a standalone menu not necessarily associated with a menubar. + + + + + Indicates a pushbox object, for example a PUSHBOX control in Windows. + + + + + Indicates a Windows RC PUSHBUTTON control. + + + + + Indicates a radio button object. + + + + + Indicates a menuitem with associated radio button. + + + + + Indicates raw data resources for an application. + + + + + Indicates a row in a table. + + + + + Indicates a Windows RC RTEXT control. + + + + + Indicates a user navigable container used to show a portion of a document. + + + + + Indicates a generic divider object (e.g. menu group separator). + + + + + Windows accelerators, shortcuts in resource or property files. + + + + + Indicates a UI control to indicate process activity but not progress. + + + + + Indicates a splitter bar. + + + + + Indicates a Windows RC STATE3 control. + + + + + Indicates a window for providing feedback to the users, like 'read-only', etc. + + + + + Indicates a string, for example an entry in a STRINGTABLE resource in Windows. + + + + + Indicates a layers of controls with a tab to select layers. + + + + + Indicates a display and edits regular two-dimensional tables of cells. + + + + + Indicates a XUL textbox element. + + + + + Indicates a UI button that can be toggled to on or off state. + + + + + Indicates an array of controls, usually buttons. + + + + + Indicates a pop up tool tip text. + + + + + Indicates a bar with a pointer indicating a position within a certain range. + + + + + Indicates a control that displays a set of hierarchical data. + + + + + Indicates a URI (URN or URL). + + + + + Indicates a Windows RC USERBUTTON control. + + + + + Indicates a user-defined control like CONTROL control in Windows. + + + + + Indicates the text of a variable. + + + + + Indicates version information about a resource like VERSIONINFO in Windows. + + + + + Indicates a vertical scrollbar. + + + + + Indicates a graphical window. + + + + + + + Values for the attribute 'size-unit'. + + + + + Indicates a size in 8-bit bytes. + + + + + Indicates a size in Unicode characters. + + + + + Indicates a size in columns. Used for HTML text area. + + + + + Indicates a size in centimeters. + + + + + Indicates a size in dialog units, as defined in Windows resources. + + + + + Indicates a size in 'font-size' units (as defined in CSS). + + + + + Indicates a size in 'x-height' units (as defined in CSS). + + + + + Indicates a size in glyphs. A glyph is considered to be one or more combined Unicode characters that represent a single displayable text character. Sometimes referred to as a 'grapheme cluster' + + + + + Indicates a size in inches. + + + + + Indicates a size in millimeters. + + + + + Indicates a size in percentage. + + + + + Indicates a size in pixels. + + + + + Indicates a size in point. + + + + + Indicates a size in rows. Used for HTML text area. + + + + + + + Values for the attribute 'state'. + + + + + Indicates the terminating state. + + + + + Indicates only non-textual information needs adaptation. + + + + + Indicates both text and non-textual information needs adaptation. + + + + + Indicates only non-textual information needs review. + + + + + Indicates both text and non-textual information needs review. + + + + + Indicates that only the text of the item needs to be reviewed. + + + + + Indicates that the item needs to be translated. + + + + + Indicates that the item is new. For example, translation units that were not in a previous version of the document. + + + + + Indicates that changes are reviewed and approved. + + + + + Indicates that the item has been translated. + + + + + + + Values for the attribute 'state-qualifier'. + + + + + Indicates an exact match. An exact match occurs when a source text of a segment is exactly the same as the source text of a segment that was translated previously. + + + + + Indicates a fuzzy match. A fuzzy match occurs when a source text of a segment is very similar to the source text of a segment that was translated previously (e.g. when the difference is casing, a few changed words, white-space discripancy, etc.). + + + + + Indicates a match based on matching IDs (in addition to matching text). + + + + + Indicates a translation derived from a glossary. + + + + + Indicates a translation derived from existing translation. + + + + + Indicates a translation derived from machine translation. + + + + + Indicates a translation derived from a translation repository. + + + + + Indicates a translation derived from a translation memory. + + + + + Indicates the translation is suggested by machine translation. + + + + + Indicates that the item has been rejected because of incorrect grammar. + + + + + Indicates that the item has been rejected because it is incorrect. + + + + + Indicates that the item has been rejected because it is too long or too short. + + + + + Indicates that the item has been rejected because of incorrect spelling. + + + + + Indicates the translation is suggested by translation memory. + + + + + + + Values for the attribute 'unit'. + + + + + Refers to words. + + + + + Refers to pages. + + + + + Refers to <trans-unit> elements. + + + + + Refers to <bin-unit> elements. + + + + + Refers to glyphs. + + + + + Refers to <trans-unit> and/or <bin-unit> elements. + + + + + Refers to the occurrences of instances defined by the count-type value. + + + + + Refers to characters. + + + + + Refers to lines. + + + + + Refers to sentences. + + + + + Refers to paragraphs. + + + + + Refers to segments. + + + + + Refers to placeables (inline elements). + + + + + + + Values for the attribute 'priority'. + + + + + Highest priority. + + + + + High priority. + + + + + High priority, but not as important as 2. + + + + + High priority, but not as important as 3. + + + + + Medium priority, but more important than 6. + + + + + Medium priority, but less important than 5. + + + + + Low priority, but more important than 8. + + + + + Low priority, but more important than 9. + + + + + Low priority. + + + + + Lowest priority. + + + + + + + + + This value indicates that all properties can be reformatted. This value must be used alone. + + + + + This value indicates that no properties should be reformatted. This value must be used alone. + + + + + + + + + + + + + This value indicates that all information in the coord attribute can be modified. + + + + + This value indicates that the x information in the coord attribute can be modified. + + + + + This value indicates that the y information in the coord attribute can be modified. + + + + + This value indicates that the cx information in the coord attribute can be modified. + + + + + This value indicates that the cy information in the coord attribute can be modified. + + + + + This value indicates that all the information in the font attribute can be modified. + + + + + This value indicates that the name information in the font attribute can be modified. + + + + + This value indicates that the size information in the font attribute can be modified. + + + + + This value indicates that the weight information in the font attribute can be modified. + + + + + This value indicates that the information in the css-style attribute can be modified. + + + + + This value indicates that the information in the style attribute can be modified. + + + + + This value indicates that the information in the exstyle attribute can be modified. + + + + + + + + + + + + + Indicates that the context is informational in nature, specifying for example, how a term should be translated. Thus, should be displayed to anyone editing the XLIFF document. + + + + + Indicates that the context-group is used to specify where the term was found in the translatable source. Thus, it is not displayed. + + + + + Indicates that the context information should be used during translation memory lookups. Thus, it is not displayed. + + + + + + + + + Represents a translation proposal from a translation memory or other resource. + + + + + Represents a previous version of the target element. + + + + + Represents a rejected version of the target element. + + + + + Represents a translation to be used for reference purposes only, for example from a related product or a different language. + + + + + Represents a proposed translation that was used for the translation of the trans-unit, possibly modified. + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + Values for the attribute 'coord'. + + + + + + + + Version values: 1.0 and 1.1 are allowed for backward compatibility. + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/3rdparty/symfony/translation/Resources/schemas/xliff-core-2.0.xsd b/3rdparty/symfony/translation/Resources/schemas/xliff-core-2.0.xsd new file mode 100644 index 00000000..963232f9 --- /dev/null +++ b/3rdparty/symfony/translation/Resources/schemas/xliff-core-2.0.xsd @@ -0,0 +1,411 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/3rdparty/symfony/translation/Resources/schemas/xml.xsd b/3rdparty/symfony/translation/Resources/schemas/xml.xsd new file mode 100644 index 00000000..a46162a7 --- /dev/null +++ b/3rdparty/symfony/translation/Resources/schemas/xml.xsd @@ -0,0 +1,309 @@ + + + + + + +
    +

    About the XML namespace

    + +
    +

    + + This schema document describes the XML namespace, in a form + suitable for import by other schema documents. +

    +

    + See + http://www.w3.org/XML/1998/namespace.html and + + http://www.w3.org/TR/REC-xml for information + about this namespace. +

    + +

    + Note that local names in this namespace are intended to be + defined only by the World Wide Web Consortium or its subgroups. + The names currently defined in this namespace are listed below. + They should not be used with conflicting semantics by any Working + Group, specification, or document instance. +

    +

    + See further below in this document for more information about how to refer to this schema document from your own + XSD schema documents and about the + namespace-versioning policy governing this schema document. +

    +
    +
    + +
    +
    + + + + +
    + +

    lang (as an attribute name)

    +

    + + denotes an attribute whose value + is a language code for the natural language of the content of + any element; its value is inherited. This name is reserved + by virtue of its definition in the XML specification.

    + +
    +
    +

    Notes

    +

    + Attempting to install the relevant ISO 2- and 3-letter + codes as the enumerated possible values is probably never + going to be a realistic possibility. +

    +

    + + See BCP 47 at + http://www.rfc-editor.org/rfc/bcp/bcp47.txt + and the IANA language subtag registry at + + http://www.iana.org/assignments/language-subtag-registry + for further information. +

    +

    + + The union allows for the 'un-declaration' of xml:lang with + the empty string. +

    +
    +
    +
    + + + + + + + + + + +
    + + + + + +
    + +

    space (as an attribute name)

    +

    + denotes an attribute whose + value is a keyword indicating what whitespace processing + discipline is intended for the content of the element; its + value is inherited. This name is reserved by virtue of its + definition in the XML specification.

    + +
    +
    +
    + + + + + + + +
    + + + + +
    + +

    base (as an attribute name)

    +

    + denotes an attribute whose value + provides a URI to be used as the base for interpreting any + relative URIs in the scope of the element on which it + appears; its value is inherited. This name is reserved + by virtue of its definition in the XML Base specification.

    + +

    + See http://www.w3.org/TR/xmlbase/ + for information about this attribute. +

    + +
    +
    +
    +
    + + + + +
    + +

    id (as an attribute name)

    +

    + + denotes an attribute whose value + should be interpreted as if declared to be of type ID. + This name is reserved by virtue of its definition in the + xml:id specification.

    + +

    + See http://www.w3.org/TR/xml-id/ + for information about this attribute. +

    +
    +
    +
    + +
    + + + + + + + + + + + +
    + +

    Father (in any context at all)

    + +
    +

    + denotes Jon Bosak, the chair of + the original XML Working Group. This name is reserved by + the following decision of the W3C XML Plenary and + XML Coordination groups: +

    +
    +

    + + In appreciation for his vision, leadership and + dedication the W3C XML Plenary on this 10th day of + February, 2000, reserves for Jon Bosak in perpetuity + the XML name "xml:Father". +

    +
    +
    +
    +
    +
    + + + + +
    +

    About this schema document

    + +
    +

    + This schema defines attributes and an attribute group suitable + for use by schemas wishing to allow xml:base, + xml:lang, xml:space or + xml:id attributes on elements they define. +

    + +

    + To enable this, such a schema must import this schema for + the XML namespace, e.g. as follows: +

    +
    +          <schema.. .>
    +          .. .
    +           <import namespace="http://www.w3.org/XML/1998/namespace"
    +                      schemaLocation="http://www.w3.org/2001/xml.xsd"/>
    +     
    +

    + or +

    +
    +
    +           <import namespace="http://www.w3.org/XML/1998/namespace"
    +                      schemaLocation="http://www.w3.org/2009/01/xml.xsd"/>
    +     
    +

    + Subsequently, qualified reference to any of the attributes or the + group defined below will have the desired effect, e.g. +

    +
    +          <type.. .>
    +          .. .
    +           <attributeGroup ref="xml:specialAttrs"/>
    +     
    +

    + will define a type which will schema-validate an instance element + with any of those attributes. +

    + +
    +
    +
    +
    + + + +
    +

    Versioning policy for this schema document

    + +
    +

    + In keeping with the XML Schema WG's standard versioning + policy, this schema document will persist at + + http://www.w3.org/2009/01/xml.xsd. +

    +

    + At the date of issue it can also be found at + + http://www.w3.org/2001/xml.xsd. +

    + +

    + The schema document at that URI may however change in the future, + in order to remain compatible with the latest version of XML + Schema itself, or with the XML namespace itself. In other words, + if the XML Schema or XML namespaces change, the version of this + document at + http://www.w3.org/2001/xml.xsd + + will change accordingly; the version at + + http://www.w3.org/2009/01/xml.xsd + + will not change. +

    +

    + + Previous dated (and unchanging) versions of this schema + document are at: +

    + +
    +
    +
    +
    + +
    diff --git a/3rdparty/symfony/translation/TranslatableMessage.php b/3rdparty/symfony/translation/TranslatableMessage.php new file mode 100644 index 00000000..c591e68c --- /dev/null +++ b/3rdparty/symfony/translation/TranslatableMessage.php @@ -0,0 +1,60 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Component\Translation; + +use Symfony\Contracts\Translation\TranslatableInterface; +use Symfony\Contracts\Translation\TranslatorInterface; + +/** + * @author Nate Wiebe + */ +class TranslatableMessage implements TranslatableInterface +{ + private string $message; + private array $parameters; + private ?string $domain; + + public function __construct(string $message, array $parameters = [], ?string $domain = null) + { + $this->message = $message; + $this->parameters = $parameters; + $this->domain = $domain; + } + + public function __toString(): string + { + return $this->getMessage(); + } + + public function getMessage(): string + { + return $this->message; + } + + public function getParameters(): array + { + return $this->parameters; + } + + public function getDomain(): ?string + { + return $this->domain; + } + + public function trans(TranslatorInterface $translator, ?string $locale = null): string + { + return $translator->trans($this->getMessage(), array_map( + static fn ($parameter) => $parameter instanceof TranslatableInterface ? $parameter->trans($translator, $locale) : $parameter, + $this->getParameters() + ), $this->getDomain(), $locale); + } +} diff --git a/3rdparty/symfony/translation/Translator.php b/3rdparty/symfony/translation/Translator.php new file mode 100644 index 00000000..1973d079 --- /dev/null +++ b/3rdparty/symfony/translation/Translator.php @@ -0,0 +1,472 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Component\Translation; + +use Symfony\Component\Config\ConfigCacheFactory; +use Symfony\Component\Config\ConfigCacheFactoryInterface; +use Symfony\Component\Config\ConfigCacheInterface; +use Symfony\Component\Translation\Exception\InvalidArgumentException; +use Symfony\Component\Translation\Exception\NotFoundResourceException; +use Symfony\Component\Translation\Exception\RuntimeException; +use Symfony\Component\Translation\Formatter\IntlFormatterInterface; +use Symfony\Component\Translation\Formatter\MessageFormatter; +use Symfony\Component\Translation\Formatter\MessageFormatterInterface; +use Symfony\Component\Translation\Loader\LoaderInterface; +use Symfony\Contracts\Translation\LocaleAwareInterface; +use Symfony\Contracts\Translation\TranslatableInterface; +use Symfony\Contracts\Translation\TranslatorInterface; + +// Help opcache.preload discover always-needed symbols +class_exists(MessageCatalogue::class); + +/** + * @author Fabien Potencier + */ +class Translator implements TranslatorInterface, TranslatorBagInterface, LocaleAwareInterface +{ + /** + * @var MessageCatalogueInterface[] + */ + protected $catalogues = []; + + private string $locale; + + /** + * @var string[] + */ + private array $fallbackLocales = []; + + /** + * @var LoaderInterface[] + */ + private array $loaders = []; + + private array $resources = []; + + private MessageFormatterInterface $formatter; + + private ?string $cacheDir; + + private bool $debug; + + private array $cacheVary; + + private ?ConfigCacheFactoryInterface $configCacheFactory; + + private array $parentLocales; + + private bool $hasIntlFormatter; + + /** + * @throws InvalidArgumentException If a locale contains invalid characters + */ + public function __construct(string $locale, ?MessageFormatterInterface $formatter = null, ?string $cacheDir = null, bool $debug = false, array $cacheVary = []) + { + $this->setLocale($locale); + + $this->formatter = $formatter ??= new MessageFormatter(); + $this->cacheDir = $cacheDir; + $this->debug = $debug; + $this->cacheVary = $cacheVary; + $this->hasIntlFormatter = $formatter instanceof IntlFormatterInterface; + } + + /** + * @return void + */ + public function setConfigCacheFactory(ConfigCacheFactoryInterface $configCacheFactory) + { + $this->configCacheFactory = $configCacheFactory; + } + + /** + * Adds a Loader. + * + * @param string $format The name of the loader (@see addResource()) + * + * @return void + */ + public function addLoader(string $format, LoaderInterface $loader) + { + $this->loaders[$format] = $loader; + } + + /** + * Adds a Resource. + * + * @param string $format The name of the loader (@see addLoader()) + * @param mixed $resource The resource name + * + * @return void + * + * @throws InvalidArgumentException If the locale contains invalid characters + */ + public function addResource(string $format, mixed $resource, string $locale, ?string $domain = null) + { + $domain ??= 'messages'; + + $this->assertValidLocale($locale); + $locale ?: $locale = class_exists(\Locale::class) ? \Locale::getDefault() : 'en'; + + $this->resources[$locale][] = [$format, $resource, $domain]; + + if (\in_array($locale, $this->fallbackLocales)) { + $this->catalogues = []; + } else { + unset($this->catalogues[$locale]); + } + } + + /** + * @return void + */ + public function setLocale(string $locale) + { + $this->assertValidLocale($locale); + $this->locale = $locale; + } + + public function getLocale(): string + { + return $this->locale ?: (class_exists(\Locale::class) ? \Locale::getDefault() : 'en'); + } + + /** + * Sets the fallback locales. + * + * @param string[] $locales + * + * @return void + * + * @throws InvalidArgumentException If a locale contains invalid characters + */ + public function setFallbackLocales(array $locales) + { + // needed as the fallback locales are linked to the already loaded catalogues + $this->catalogues = []; + + foreach ($locales as $locale) { + $this->assertValidLocale($locale); + } + + $this->fallbackLocales = $this->cacheVary['fallback_locales'] = $locales; + } + + /** + * Gets the fallback locales. + * + * @internal + */ + public function getFallbackLocales(): array + { + return $this->fallbackLocales; + } + + public function trans(?string $id, array $parameters = [], ?string $domain = null, ?string $locale = null): string + { + if (null === $id || '' === $id) { + return ''; + } + + $domain ??= 'messages'; + + $catalogue = $this->getCatalogue($locale); + $locale = $catalogue->getLocale(); + while (!$catalogue->defines($id, $domain)) { + if ($cat = $catalogue->getFallbackCatalogue()) { + $catalogue = $cat; + $locale = $catalogue->getLocale(); + } else { + break; + } + } + + $parameters = array_map(fn ($parameter) => $parameter instanceof TranslatableInterface ? $parameter->trans($this, $locale) : $parameter, $parameters); + + $len = \strlen(MessageCatalogue::INTL_DOMAIN_SUFFIX); + if ($this->hasIntlFormatter + && ($catalogue->defines($id, $domain.MessageCatalogue::INTL_DOMAIN_SUFFIX) + || (\strlen($domain) > $len && 0 === substr_compare($domain, MessageCatalogue::INTL_DOMAIN_SUFFIX, -$len, $len))) + ) { + return $this->formatter->formatIntl($catalogue->get($id, $domain), $locale, $parameters); + } + + return $this->formatter->format($catalogue->get($id, $domain), $locale, $parameters); + } + + public function getCatalogue(?string $locale = null): MessageCatalogueInterface + { + if (!$locale) { + $locale = $this->getLocale(); + } else { + $this->assertValidLocale($locale); + } + + if (!isset($this->catalogues[$locale])) { + $this->loadCatalogue($locale); + } + + return $this->catalogues[$locale]; + } + + public function getCatalogues(): array + { + return array_values($this->catalogues); + } + + /** + * Gets the loaders. + * + * @return LoaderInterface[] + */ + protected function getLoaders(): array + { + return $this->loaders; + } + + /** + * @return void + */ + protected function loadCatalogue(string $locale) + { + if (null === $this->cacheDir) { + $this->initializeCatalogue($locale); + } else { + $this->initializeCacheCatalogue($locale); + } + } + + /** + * @return void + */ + protected function initializeCatalogue(string $locale) + { + $this->assertValidLocale($locale); + + try { + $this->doLoadCatalogue($locale); + } catch (NotFoundResourceException $e) { + if (!$this->computeFallbackLocales($locale)) { + throw $e; + } + } + $this->loadFallbackCatalogues($locale); + } + + private function initializeCacheCatalogue(string $locale): void + { + if (isset($this->catalogues[$locale])) { + /* Catalogue already initialized. */ + return; + } + + $this->assertValidLocale($locale); + $cache = $this->getConfigCacheFactory()->cache($this->getCatalogueCachePath($locale), + function (ConfigCacheInterface $cache) use ($locale) { + $this->dumpCatalogue($locale, $cache); + } + ); + + if (isset($this->catalogues[$locale])) { + /* Catalogue has been initialized as it was written out to cache. */ + return; + } + + /* Read catalogue from cache. */ + $this->catalogues[$locale] = include $cache->getPath(); + } + + private function dumpCatalogue(string $locale, ConfigCacheInterface $cache): void + { + $this->initializeCatalogue($locale); + $fallbackContent = $this->getFallbackContent($this->catalogues[$locale]); + + $content = sprintf(<<getAllMessages($this->catalogues[$locale]), true), + $fallbackContent + ); + + $cache->write($content, $this->catalogues[$locale]->getResources()); + } + + private function getFallbackContent(MessageCatalogue $catalogue): string + { + $fallbackContent = ''; + $current = ''; + $replacementPattern = '/[^a-z0-9_]/i'; + $fallbackCatalogue = $catalogue->getFallbackCatalogue(); + while ($fallbackCatalogue) { + $fallback = $fallbackCatalogue->getLocale(); + $fallbackSuffix = ucfirst(preg_replace($replacementPattern, '_', $fallback)); + $currentSuffix = ucfirst(preg_replace($replacementPattern, '_', $current)); + + $fallbackContent .= sprintf(<<<'EOF' +$catalogue%s = new MessageCatalogue('%s', %s); +$catalogue%s->addFallbackCatalogue($catalogue%s); + +EOF + , + $fallbackSuffix, + $fallback, + var_export($this->getAllMessages($fallbackCatalogue), true), + $currentSuffix, + $fallbackSuffix + ); + $current = $fallbackCatalogue->getLocale(); + $fallbackCatalogue = $fallbackCatalogue->getFallbackCatalogue(); + } + + return $fallbackContent; + } + + private function getCatalogueCachePath(string $locale): string + { + return $this->cacheDir.'/catalogue.'.$locale.'.'.strtr(substr(base64_encode(hash('sha256', serialize($this->cacheVary), true)), 0, 7), '/', '_').'.php'; + } + + /** + * @internal + */ + protected function doLoadCatalogue(string $locale): void + { + $this->catalogues[$locale] = new MessageCatalogue($locale); + + if (isset($this->resources[$locale])) { + foreach ($this->resources[$locale] as $resource) { + if (!isset($this->loaders[$resource[0]])) { + if (\is_string($resource[1])) { + throw new RuntimeException(sprintf('No loader is registered for the "%s" format when loading the "%s" resource.', $resource[0], $resource[1])); + } + + throw new RuntimeException(sprintf('No loader is registered for the "%s" format.', $resource[0])); + } + $this->catalogues[$locale]->addCatalogue($this->loaders[$resource[0]]->load($resource[1], $locale, $resource[2])); + } + } + } + + private function loadFallbackCatalogues(string $locale): void + { + $current = $this->catalogues[$locale]; + + foreach ($this->computeFallbackLocales($locale) as $fallback) { + if (!isset($this->catalogues[$fallback])) { + $this->initializeCatalogue($fallback); + } + + $fallbackCatalogue = new MessageCatalogue($fallback, $this->getAllMessages($this->catalogues[$fallback])); + foreach ($this->catalogues[$fallback]->getResources() as $resource) { + $fallbackCatalogue->addResource($resource); + } + $current->addFallbackCatalogue($fallbackCatalogue); + $current = $fallbackCatalogue; + } + } + + /** + * @return array + */ + protected function computeFallbackLocales(string $locale) + { + $this->parentLocales ??= json_decode(file_get_contents(__DIR__.'/Resources/data/parents.json'), true); + + $originLocale = $locale; + $locales = []; + + while ($locale) { + $parent = $this->parentLocales[$locale] ?? null; + + if ($parent) { + $locale = 'root' !== $parent ? $parent : null; + } elseif (\function_exists('locale_parse')) { + $localeSubTags = locale_parse($locale); + $locale = null; + if (1 < \count($localeSubTags)) { + array_pop($localeSubTags); + $locale = locale_compose($localeSubTags) ?: null; + } + } elseif ($i = strrpos($locale, '_') ?: strrpos($locale, '-')) { + $locale = substr($locale, 0, $i); + } else { + $locale = null; + } + + if (null !== $locale) { + $locales[] = $locale; + } + } + + foreach ($this->fallbackLocales as $fallback) { + if ($fallback === $originLocale) { + continue; + } + + $locales[] = $fallback; + } + + return array_unique($locales); + } + + /** + * Asserts that the locale is valid, throws an Exception if not. + * + * @return void + * + * @throws InvalidArgumentException If the locale contains invalid characters + */ + protected function assertValidLocale(string $locale) + { + if (!preg_match('/^[a-z0-9@_\\.\\-]*$/i', $locale)) { + throw new InvalidArgumentException(sprintf('Invalid "%s" locale.', $locale)); + } + } + + /** + * Provides the ConfigCache factory implementation, falling back to a + * default implementation if necessary. + */ + private function getConfigCacheFactory(): ConfigCacheFactoryInterface + { + $this->configCacheFactory ??= new ConfigCacheFactory($this->debug); + + return $this->configCacheFactory; + } + + private function getAllMessages(MessageCatalogueInterface $catalogue): array + { + $allMessages = []; + + foreach ($catalogue->all() as $domain => $messages) { + if ($intlMessages = $catalogue->all($domain.MessageCatalogue::INTL_DOMAIN_SUFFIX)) { + $allMessages[$domain.MessageCatalogue::INTL_DOMAIN_SUFFIX] = $intlMessages; + $messages = array_diff_key($messages, $intlMessages); + } + if ($messages) { + $allMessages[$domain] = $messages; + } + } + + return $allMessages; + } +} diff --git a/3rdparty/symfony/translation/TranslatorBag.php b/3rdparty/symfony/translation/TranslatorBag.php new file mode 100644 index 00000000..3b47aece --- /dev/null +++ b/3rdparty/symfony/translation/TranslatorBag.php @@ -0,0 +1,102 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Component\Translation; + +use Symfony\Component\Translation\Catalogue\AbstractOperation; +use Symfony\Component\Translation\Catalogue\TargetOperation; + +final class TranslatorBag implements TranslatorBagInterface +{ + /** @var MessageCatalogue[] */ + private array $catalogues = []; + + public function addCatalogue(MessageCatalogue $catalogue): void + { + if (null !== $existingCatalogue = $this->getCatalogue($catalogue->getLocale())) { + $catalogue->addCatalogue($existingCatalogue); + } + + $this->catalogues[$catalogue->getLocale()] = $catalogue; + } + + public function addBag(TranslatorBagInterface $bag): void + { + foreach ($bag->getCatalogues() as $catalogue) { + $this->addCatalogue($catalogue); + } + } + + public function getCatalogue(?string $locale = null): MessageCatalogueInterface + { + if (null === $locale || !isset($this->catalogues[$locale])) { + $this->catalogues[$locale] = new MessageCatalogue($locale); + } + + return $this->catalogues[$locale]; + } + + public function getCatalogues(): array + { + return array_values($this->catalogues); + } + + public function diff(TranslatorBagInterface $diffBag): self + { + $diff = new self(); + + foreach ($this->catalogues as $locale => $catalogue) { + if (null === $diffCatalogue = $diffBag->getCatalogue($locale)) { + $diff->addCatalogue($catalogue); + + continue; + } + + $operation = new TargetOperation($diffCatalogue, $catalogue); + $operation->moveMessagesToIntlDomainsIfPossible(AbstractOperation::NEW_BATCH); + $newCatalogue = new MessageCatalogue($locale); + + foreach ($catalogue->getDomains() as $domain) { + $newCatalogue->add($operation->getNewMessages($domain), $domain); + } + + $diff->addCatalogue($newCatalogue); + } + + return $diff; + } + + public function intersect(TranslatorBagInterface $intersectBag): self + { + $diff = new self(); + + foreach ($this->catalogues as $locale => $catalogue) { + if (null === $intersectCatalogue = $intersectBag->getCatalogue($locale)) { + continue; + } + + $operation = new TargetOperation($catalogue, $intersectCatalogue); + $operation->moveMessagesToIntlDomainsIfPossible(AbstractOperation::OBSOLETE_BATCH); + $obsoleteCatalogue = new MessageCatalogue($locale); + + foreach ($operation->getDomains() as $domain) { + $obsoleteCatalogue->add( + array_diff($operation->getMessages($domain), $operation->getNewMessages($domain)), + $domain + ); + } + + $diff->addCatalogue($obsoleteCatalogue); + } + + return $diff; + } +} diff --git a/3rdparty/symfony/translation/TranslatorBagInterface.php b/3rdparty/symfony/translation/TranslatorBagInterface.php new file mode 100644 index 00000000..365d1f13 --- /dev/null +++ b/3rdparty/symfony/translation/TranslatorBagInterface.php @@ -0,0 +1,36 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Component\Translation; + +use Symfony\Component\Translation\Exception\InvalidArgumentException; + +/** + * @author Abdellatif Ait boudad + */ +interface TranslatorBagInterface +{ + /** + * Gets the catalogue by locale. + * + * @param string|null $locale The locale or null to use the default + * + * @throws InvalidArgumentException If the locale contains invalid characters + */ + public function getCatalogue(?string $locale = null): MessageCatalogueInterface; + + /** + * Returns all catalogues of the instance. + * + * @return MessageCatalogueInterface[] + */ + public function getCatalogues(): array; +} diff --git a/3rdparty/symfony/translation/Util/ArrayConverter.php b/3rdparty/symfony/translation/Util/ArrayConverter.php new file mode 100644 index 00000000..64e15b48 --- /dev/null +++ b/3rdparty/symfony/translation/Util/ArrayConverter.php @@ -0,0 +1,142 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Component\Translation\Util; + +/** + * ArrayConverter generates tree like structure from a message catalogue. + * e.g. this + * 'foo.bar1' => 'test1', + * 'foo.bar2' => 'test2' + * converts to follows: + * foo: + * bar1: test1 + * bar2: test2. + * + * @author Gennady Telegin + */ +class ArrayConverter +{ + /** + * Converts linear messages array to tree-like array. + * For example this array('foo.bar' => 'value') will be converted to ['foo' => ['bar' => 'value']]. + * + * @param array $messages Linear messages array + */ + public static function expandToTree(array $messages): array + { + $tree = []; + + foreach ($messages as $id => $value) { + $referenceToElement = &self::getElementByPath($tree, self::getKeyParts($id)); + + $referenceToElement = $value; + + unset($referenceToElement); + } + + return $tree; + } + + private static function &getElementByPath(array &$tree, array $parts): mixed + { + $elem = &$tree; + $parentOfElem = null; + + foreach ($parts as $i => $part) { + if (isset($elem[$part]) && \is_string($elem[$part])) { + /* Process next case: + * 'foo': 'test1', + * 'foo.bar': 'test2' + * + * $tree['foo'] was string before we found array {bar: test2}. + * Treat new element as string too, e.g. add $tree['foo.bar'] = 'test2'; + */ + $elem = &$elem[implode('.', \array_slice($parts, $i))]; + break; + } + + $parentOfElem = &$elem; + $elem = &$elem[$part]; + } + + if ($elem && \is_array($elem) && $parentOfElem) { + /* Process next case: + * 'foo.bar': 'test1' + * 'foo': 'test2' + * + * $tree['foo'] was array = {bar: 'test1'} before we found string constant `foo`. + * Cancel treating $tree['foo'] as array and cancel back it expansion, + * e.g. make it $tree['foo.bar'] = 'test1' again. + */ + self::cancelExpand($parentOfElem, $part, $elem); + } + + return $elem; + } + + private static function cancelExpand(array &$tree, string $prefix, array $node): void + { + $prefix .= '.'; + + foreach ($node as $id => $value) { + if (\is_string($value)) { + $tree[$prefix.$id] = $value; + } else { + self::cancelExpand($tree, $prefix.$id, $value); + } + } + } + + /** + * @return string[] + */ + private static function getKeyParts(string $key): array + { + $parts = explode('.', $key); + $partsCount = \count($parts); + + $result = []; + $buffer = ''; + + foreach ($parts as $index => $part) { + if (0 === $index && '' === $part) { + $buffer = '.'; + + continue; + } + + if ($index === $partsCount - 1 && '' === $part) { + $buffer .= '.'; + $result[] = $buffer; + + continue; + } + + if (isset($parts[$index + 1]) && '' === $parts[$index + 1]) { + $buffer .= $part; + + continue; + } + + if ($buffer) { + $result[] = $buffer.$part; + $buffer = ''; + + continue; + } + + $result[] = $part; + } + + return $result; + } +} diff --git a/3rdparty/symfony/translation/Util/XliffUtils.php b/3rdparty/symfony/translation/Util/XliffUtils.php new file mode 100644 index 00000000..335c34be --- /dev/null +++ b/3rdparty/symfony/translation/Util/XliffUtils.php @@ -0,0 +1,191 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Component\Translation\Util; + +use Symfony\Component\Translation\Exception\InvalidArgumentException; +use Symfony\Component\Translation\Exception\InvalidResourceException; + +/** + * Provides some utility methods for XLIFF translation files, such as validating + * their contents according to the XSD schema. + * + * @author Fabien Potencier + */ +class XliffUtils +{ + /** + * Gets xliff file version based on the root "version" attribute. + * + * Defaults to 1.2 for backwards compatibility. + * + * @throws InvalidArgumentException + */ + public static function getVersionNumber(\DOMDocument $dom): string + { + /** @var \DOMNode $xliff */ + foreach ($dom->getElementsByTagName('xliff') as $xliff) { + $version = $xliff->attributes->getNamedItem('version'); + if ($version) { + return $version->nodeValue; + } + + $namespace = $xliff->attributes->getNamedItem('xmlns'); + if ($namespace) { + if (0 !== substr_compare('urn:oasis:names:tc:xliff:document:', $namespace->nodeValue, 0, 34)) { + throw new InvalidArgumentException(sprintf('Not a valid XLIFF namespace "%s".', $namespace)); + } + + return substr($namespace, 34); + } + } + + // Falls back to v1.2 + return '1.2'; + } + + /** + * Validates and parses the given file into a DOMDocument. + * + * @throws InvalidResourceException + */ + public static function validateSchema(\DOMDocument $dom): array + { + $xliffVersion = static::getVersionNumber($dom); + $internalErrors = libxml_use_internal_errors(true); + if ($shouldEnable = self::shouldEnableEntityLoader()) { + $disableEntities = libxml_disable_entity_loader(false); + } + try { + $isValid = @$dom->schemaValidateSource(self::getSchema($xliffVersion)); + if (!$isValid) { + return self::getXmlErrors($internalErrors); + } + } finally { + if ($shouldEnable) { + libxml_disable_entity_loader($disableEntities); + } + } + + $dom->normalizeDocument(); + + libxml_clear_errors(); + libxml_use_internal_errors($internalErrors); + + return []; + } + + private static function shouldEnableEntityLoader(): bool + { + static $dom, $schema; + if (null === $dom) { + $dom = new \DOMDocument(); + $dom->loadXML(''); + + $tmpfile = tempnam(sys_get_temp_dir(), 'symfony'); + register_shutdown_function(static function () use ($tmpfile) { + @unlink($tmpfile); + }); + $schema = ' + + +'; + file_put_contents($tmpfile, ' + + + +'); + } + + return !@$dom->schemaValidateSource($schema); + } + + public static function getErrorsAsString(array $xmlErrors): string + { + $errorsAsString = ''; + + foreach ($xmlErrors as $error) { + $errorsAsString .= sprintf("[%s %s] %s (in %s - line %d, column %d)\n", + \LIBXML_ERR_WARNING === $error['level'] ? 'WARNING' : 'ERROR', + $error['code'], + $error['message'], + $error['file'], + $error['line'], + $error['column'] + ); + } + + return $errorsAsString; + } + + private static function getSchema(string $xliffVersion): string + { + if ('1.2' === $xliffVersion) { + $schemaSource = file_get_contents(__DIR__.'/../Resources/schemas/xliff-core-1.2-transitional.xsd'); + $xmlUri = 'http://www.w3.org/2001/xml.xsd'; + } elseif ('2.0' === $xliffVersion) { + $schemaSource = file_get_contents(__DIR__.'/../Resources/schemas/xliff-core-2.0.xsd'); + $xmlUri = 'informativeCopiesOf3rdPartySchemas/w3c/xml.xsd'; + } else { + throw new InvalidArgumentException(sprintf('No support implemented for loading XLIFF version "%s".', $xliffVersion)); + } + + return self::fixXmlLocation($schemaSource, $xmlUri); + } + + /** + * Internally changes the URI of a dependent xsd to be loaded locally. + */ + private static function fixXmlLocation(string $schemaSource, string $xmlUri): string + { + $newPath = str_replace('\\', '/', __DIR__).'/../Resources/schemas/xml.xsd'; + $parts = explode('/', $newPath); + $locationstart = 'file:///'; + if (0 === stripos($newPath, 'phar://')) { + $tmpfile = tempnam(sys_get_temp_dir(), 'symfony'); + if ($tmpfile) { + copy($newPath, $tmpfile); + $parts = explode('/', str_replace('\\', '/', $tmpfile)); + } else { + array_shift($parts); + $locationstart = 'phar:///'; + } + } + + $drive = '\\' === \DIRECTORY_SEPARATOR ? array_shift($parts).'/' : ''; + $newPath = $locationstart.$drive.implode('/', array_map('rawurlencode', $parts)); + + return str_replace($xmlUri, $newPath, $schemaSource); + } + + /** + * Returns the XML errors of the internal XML parser. + */ + private static function getXmlErrors(bool $internalErrors): array + { + $errors = []; + foreach (libxml_get_errors() as $error) { + $errors[] = [ + 'level' => \LIBXML_ERR_WARNING == $error->level ? 'WARNING' : 'ERROR', + 'code' => $error->code, + 'message' => trim($error->message), + 'file' => $error->file ?: 'n/a', + 'line' => $error->line, + 'column' => $error->column, + ]; + } + + libxml_clear_errors(); + libxml_use_internal_errors($internalErrors); + + return $errors; + } +} diff --git a/3rdparty/symfony/translation/Writer/TranslationWriter.php b/3rdparty/symfony/translation/Writer/TranslationWriter.php new file mode 100644 index 00000000..61e03cb0 --- /dev/null +++ b/3rdparty/symfony/translation/Writer/TranslationWriter.php @@ -0,0 +1,75 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Component\Translation\Writer; + +use Symfony\Component\Translation\Dumper\DumperInterface; +use Symfony\Component\Translation\Exception\InvalidArgumentException; +use Symfony\Component\Translation\Exception\RuntimeException; +use Symfony\Component\Translation\MessageCatalogue; + +/** + * TranslationWriter writes translation messages. + * + * @author Michel Salib + */ +class TranslationWriter implements TranslationWriterInterface +{ + /** + * @var array + */ + private array $dumpers = []; + + /** + * Adds a dumper to the writer. + * + * @return void + */ + public function addDumper(string $format, DumperInterface $dumper) + { + $this->dumpers[$format] = $dumper; + } + + /** + * Obtains the list of supported formats. + */ + public function getFormats(): array + { + return array_keys($this->dumpers); + } + + /** + * Writes translation from the catalogue according to the selected format. + * + * @param string $format The format to use to dump the messages + * @param array $options Options that are passed to the dumper + * + * @return void + * + * @throws InvalidArgumentException + */ + public function write(MessageCatalogue $catalogue, string $format, array $options = []) + { + if (!isset($this->dumpers[$format])) { + throw new InvalidArgumentException(sprintf('There is no dumper associated with format "%s".', $format)); + } + + // get the right dumper + $dumper = $this->dumpers[$format]; + + if (isset($options['path']) && !is_dir($options['path']) && !@mkdir($options['path'], 0777, true) && !is_dir($options['path'])) { + throw new RuntimeException(sprintf('Translation Writer was not able to create directory "%s".', $options['path'])); + } + + // save + $dumper->dump($catalogue, $options); + } +} diff --git a/3rdparty/symfony/translation/Writer/TranslationWriterInterface.php b/3rdparty/symfony/translation/Writer/TranslationWriterInterface.php new file mode 100644 index 00000000..5ebb9794 --- /dev/null +++ b/3rdparty/symfony/translation/Writer/TranslationWriterInterface.php @@ -0,0 +1,35 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Component\Translation\Writer; + +use Symfony\Component\Translation\Exception\InvalidArgumentException; +use Symfony\Component\Translation\MessageCatalogue; + +/** + * TranslationWriter writes translation messages. + * + * @author Michel Salib + */ +interface TranslationWriterInterface +{ + /** + * Writes translation from the catalogue according to the selected format. + * + * @param string $format The format to use to dump the messages + * @param array $options Options that are passed to the dumper + * + * @return void + * + * @throws InvalidArgumentException + */ + public function write(MessageCatalogue $catalogue, string $format, array $options = []); +} diff --git a/3rdparty/symfony/uid/AbstractUid.php b/3rdparty/symfony/uid/AbstractUid.php new file mode 100644 index 00000000..c67e67e8 --- /dev/null +++ b/3rdparty/symfony/uid/AbstractUid.php @@ -0,0 +1,176 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Component\Uid; + +/** + * @author Nicolas Grekas + */ +abstract class AbstractUid implements \JsonSerializable, \Stringable +{ + /** + * The identifier in its canonic representation. + */ + protected $uid; + + /** + * Whether the passed value is valid for the constructor of the current class. + */ + abstract public static function isValid(string $uid): bool; + + /** + * Creates an AbstractUid from an identifier represented in any of the supported formats. + * + * @throws \InvalidArgumentException When the passed value is not valid + */ + abstract public static function fromString(string $uid): static; + + /** + * @throws \InvalidArgumentException When the passed value is not valid + */ + public static function fromBinary(string $uid): static + { + if (16 !== \strlen($uid)) { + throw new \InvalidArgumentException('Invalid binary uid provided.'); + } + + return static::fromString($uid); + } + + /** + * @throws \InvalidArgumentException When the passed value is not valid + */ + public static function fromBase58(string $uid): static + { + if (22 !== \strlen($uid)) { + throw new \InvalidArgumentException('Invalid base-58 uid provided.'); + } + + return static::fromString($uid); + } + + /** + * @throws \InvalidArgumentException When the passed value is not valid + */ + public static function fromBase32(string $uid): static + { + if (26 !== \strlen($uid)) { + throw new \InvalidArgumentException('Invalid base-32 uid provided.'); + } + + return static::fromString($uid); + } + + /** + * @throws \InvalidArgumentException When the passed value is not valid + */ + public static function fromRfc4122(string $uid): static + { + if (36 !== \strlen($uid)) { + throw new \InvalidArgumentException('Invalid RFC4122 uid provided.'); + } + + return static::fromString($uid); + } + + /** + * Returns the identifier as a raw binary string. + */ + abstract public function toBinary(): string; + + /** + * Returns the identifier as a base58 case sensitive string. + * + * @example 2AifFTC3zXgZzK5fPrrprL (len=22) + */ + public function toBase58(): string + { + return strtr(sprintf('%022s', BinaryUtil::toBase($this->toBinary(), BinaryUtil::BASE58)), '0', '1'); + } + + /** + * Returns the identifier as a base32 case insensitive string. + * + * @see https://tools.ietf.org/html/rfc4648#section-6 + * + * @example 09EJ0S614A9FXVG9C5537Q9ZE1 (len=26) + */ + public function toBase32(): string + { + $uid = bin2hex($this->toBinary()); + $uid = sprintf('%02s%04s%04s%04s%04s%04s%04s', + base_convert(substr($uid, 0, 2), 16, 32), + base_convert(substr($uid, 2, 5), 16, 32), + base_convert(substr($uid, 7, 5), 16, 32), + base_convert(substr($uid, 12, 5), 16, 32), + base_convert(substr($uid, 17, 5), 16, 32), + base_convert(substr($uid, 22, 5), 16, 32), + base_convert(substr($uid, 27, 5), 16, 32) + ); + + return strtr($uid, 'abcdefghijklmnopqrstuv', 'ABCDEFGHJKMNPQRSTVWXYZ'); + } + + /** + * Returns the identifier as a RFC4122 case insensitive string. + * + * @see https://tools.ietf.org/html/rfc4122#section-3 + * + * @example 09748193-048a-4bfb-b825-8528cf74fdc1 (len=36) + */ + public function toRfc4122(): string + { + // don't use uuid_unparse(), it's slower + $uuid = bin2hex($this->toBinary()); + $uuid = substr_replace($uuid, '-', 8, 0); + $uuid = substr_replace($uuid, '-', 13, 0); + $uuid = substr_replace($uuid, '-', 18, 0); + + return substr_replace($uuid, '-', 23, 0); + } + + /** + * Returns the identifier as a prefixed hexadecimal case insensitive string. + * + * @example 0x09748193048a4bfbb8258528cf74fdc1 (len=34) + */ + public function toHex(): string + { + return '0x'.bin2hex($this->toBinary()); + } + + /** + * Returns whether the argument is an AbstractUid and contains the same value as the current instance. + */ + public function equals(mixed $other): bool + { + if (!$other instanceof self) { + return false; + } + + return $this->uid === $other->uid; + } + + public function compare(self $other): int + { + return (\strlen($this->uid) - \strlen($other->uid)) ?: ($this->uid <=> $other->uid); + } + + public function __toString(): string + { + return $this->uid; + } + + public function jsonSerialize(): string + { + return $this->uid; + } +} diff --git a/3rdparty/symfony/uid/BinaryUtil.php b/3rdparty/symfony/uid/BinaryUtil.php new file mode 100644 index 00000000..8fd19d86 --- /dev/null +++ b/3rdparty/symfony/uid/BinaryUtil.php @@ -0,0 +1,175 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Component\Uid; + +/** + * @internal + * + * @author Nicolas Grekas + */ +class BinaryUtil +{ + public const BASE10 = [ + '' => '0123456789', + 0, 1, 2, 3, 4, 5, 6, 7, 8, 9, + ]; + + public const BASE58 = [ + '' => '123456789ABCDEFGHJKLMNPQRSTUVWXYZabcdefghijkmnopqrstuvwxyz', + 1 => 0, 1, 2, 3, 4, 5, 6, 7, 8, 'A' => 9, + 'B' => 10, 'C' => 11, 'D' => 12, 'E' => 13, 'F' => 14, 'G' => 15, + 'H' => 16, 'J' => 17, 'K' => 18, 'L' => 19, 'M' => 20, 'N' => 21, + 'P' => 22, 'Q' => 23, 'R' => 24, 'S' => 25, 'T' => 26, 'U' => 27, + 'V' => 28, 'W' => 29, 'X' => 30, 'Y' => 31, 'Z' => 32, 'a' => 33, + 'b' => 34, 'c' => 35, 'd' => 36, 'e' => 37, 'f' => 38, 'g' => 39, + 'h' => 40, 'i' => 41, 'j' => 42, 'k' => 43, 'm' => 44, 'n' => 45, + 'o' => 46, 'p' => 47, 'q' => 48, 'r' => 49, 's' => 50, 't' => 51, + 'u' => 52, 'v' => 53, 'w' => 54, 'x' => 55, 'y' => 56, 'z' => 57, + ]; + + // https://tools.ietf.org/html/rfc4122#section-4.1.4 + // 0x01b21dd213814000 is the number of 100-ns intervals between the + // UUID epoch 1582-10-15 00:00:00 and the Unix epoch 1970-01-01 00:00:00. + private const TIME_OFFSET_INT = 0x01B21DD213814000; + private const TIME_OFFSET_BIN = "\x01\xb2\x1d\xd2\x13\x81\x40\x00"; + private const TIME_OFFSET_COM1 = "\xfe\x4d\xe2\x2d\xec\x7e\xbf\xff"; + private const TIME_OFFSET_COM2 = "\xfe\x4d\xe2\x2d\xec\x7e\xc0\x00"; + + public static function toBase(string $bytes, array $map): string + { + $base = \strlen($alphabet = $map['']); + $bytes = array_values(unpack(\PHP_INT_SIZE >= 8 ? 'n*' : 'C*', $bytes)); + $digits = ''; + + while ($count = \count($bytes)) { + $quotient = []; + $remainder = 0; + + for ($i = 0; $i !== $count; ++$i) { + $carry = $bytes[$i] + ($remainder << (\PHP_INT_SIZE >= 8 ? 16 : 8)); + $digit = intdiv($carry, $base); + $remainder = $carry % $base; + + if ($digit || $quotient) { + $quotient[] = $digit; + } + } + + $digits = $alphabet[$remainder].$digits; + $bytes = $quotient; + } + + return $digits; + } + + public static function fromBase(string $digits, array $map): string + { + $base = \strlen($map['']); + $count = \strlen($digits); + $bytes = []; + + while ($count) { + $quotient = []; + $remainder = 0; + + for ($i = 0; $i !== $count; ++$i) { + $carry = ($bytes ? $digits[$i] : $map[$digits[$i]]) + $remainder * $base; + + if (\PHP_INT_SIZE >= 8) { + $digit = $carry >> 16; + $remainder = $carry & 0xFFFF; + } else { + $digit = $carry >> 8; + $remainder = $carry & 0xFF; + } + + if ($digit || $quotient) { + $quotient[] = $digit; + } + } + + $bytes[] = $remainder; + $count = \count($digits = $quotient); + } + + return pack(\PHP_INT_SIZE >= 8 ? 'n*' : 'C*', ...array_reverse($bytes)); + } + + public static function add(string $a, string $b): string + { + $carry = 0; + for ($i = 7; 0 <= $i; --$i) { + $carry += \ord($a[$i]) + \ord($b[$i]); + $a[$i] = \chr($carry & 0xFF); + $carry >>= 8; + } + + return $a; + } + + /** + * @param string $time Count of 100-nanosecond intervals since the UUID epoch 1582-10-15 00:00:00 in hexadecimal + */ + public static function hexToDateTime(string $time): \DateTimeImmutable + { + if (\PHP_INT_SIZE >= 8) { + $time = (string) (hexdec($time) - self::TIME_OFFSET_INT); + } else { + $time = str_pad(hex2bin($time), 8, "\0", \STR_PAD_LEFT); + + if (self::TIME_OFFSET_BIN <= $time) { + $time = self::add($time, self::TIME_OFFSET_COM2); + $time[0] = $time[0] & "\x7F"; + $time = self::toBase($time, self::BASE10); + } else { + $time = self::add($time, self::TIME_OFFSET_COM1); + $time = '-'.self::toBase($time ^ "\xff\xff\xff\xff\xff\xff\xff\xff", self::BASE10); + } + } + + if (9 > \strlen($time)) { + $time = '-' === $time[0] ? '-'.str_pad(substr($time, 1), 8, '0', \STR_PAD_LEFT) : str_pad($time, 8, '0', \STR_PAD_LEFT); + } + + return \DateTimeImmutable::createFromFormat('U.u?', substr_replace($time, '.', -7, 0)); + } + + /** + * @return string Count of 100-nanosecond intervals since the UUID epoch 1582-10-15 00:00:00 in hexadecimal + */ + public static function dateTimeToHex(\DateTimeInterface $time): string + { + if (\PHP_INT_SIZE >= 8) { + if (-self::TIME_OFFSET_INT > $time = (int) $time->format('Uu0')) { + throw new \InvalidArgumentException('The given UUID date cannot be earlier than 1582-10-15.'); + } + + return str_pad(dechex(self::TIME_OFFSET_INT + $time), 16, '0', \STR_PAD_LEFT); + } + + $time = $time->format('Uu0'); + $negative = '-' === $time[0]; + if ($negative && self::TIME_OFFSET_INT < $time = substr($time, 1)) { + throw new \InvalidArgumentException('The given UUID date cannot be earlier than 1582-10-15.'); + } + $time = self::fromBase($time, self::BASE10); + $time = str_pad($time, 8, "\0", \STR_PAD_LEFT); + + if ($negative) { + $time = self::add($time, self::TIME_OFFSET_COM1) ^ "\xff\xff\xff\xff\xff\xff\xff\xff"; + } else { + $time = self::add($time, self::TIME_OFFSET_BIN); + } + + return bin2hex($time); + } +} diff --git a/3rdparty/symfony/uid/Command/GenerateUlidCommand.php b/3rdparty/symfony/uid/Command/GenerateUlidCommand.php new file mode 100644 index 00000000..6c8d1638 --- /dev/null +++ b/3rdparty/symfony/uid/Command/GenerateUlidCommand.php @@ -0,0 +1,119 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Component\Uid\Command; + +use Symfony\Component\Console\Attribute\AsCommand; +use Symfony\Component\Console\Command\Command; +use Symfony\Component\Console\Completion\CompletionInput; +use Symfony\Component\Console\Completion\CompletionSuggestions; +use Symfony\Component\Console\Input\InputInterface; +use Symfony\Component\Console\Input\InputOption; +use Symfony\Component\Console\Output\ConsoleOutputInterface; +use Symfony\Component\Console\Output\OutputInterface; +use Symfony\Component\Console\Style\SymfonyStyle; +use Symfony\Component\Uid\Factory\UlidFactory; + +#[AsCommand(name: 'ulid:generate', description: 'Generate a ULID')] +class GenerateUlidCommand extends Command +{ + private UlidFactory $factory; + + public function __construct(?UlidFactory $factory = null) + { + $this->factory = $factory ?? new UlidFactory(); + + parent::__construct(); + } + + protected function configure(): void + { + $this + ->setDefinition([ + new InputOption('time', null, InputOption::VALUE_REQUIRED, 'The ULID timestamp: a parsable date/time string'), + new InputOption('count', 'c', InputOption::VALUE_REQUIRED, 'The number of ULID to generate', 1), + new InputOption('format', 'f', InputOption::VALUE_REQUIRED, sprintf('The ULID output format ("%s")', implode('", "', $this->getAvailableFormatOptions())), 'base32'), + ]) + ->setHelp(<<<'EOF' +The %command.name% command generates a ULID. + + php %command.full_name% + +To specify the timestamp: + + php %command.full_name% --time="2021-02-16 14:09:08" + +To generate several ULIDs: + + php %command.full_name% --count=10 + +To output a specific format: + + php %command.full_name% --format=rfc4122 +EOF + ) + ; + } + + protected function execute(InputInterface $input, OutputInterface $output): int + { + $io = new SymfonyStyle($input, $output instanceof ConsoleOutputInterface ? $output->getErrorOutput() : $output); + + if (null !== $time = $input->getOption('time')) { + try { + $time = new \DateTimeImmutable($time); + } catch (\Exception $e) { + $io->error(sprintf('Invalid timestamp "%s": %s', $time, str_replace('DateTimeImmutable::__construct(): ', '', $e->getMessage()))); + + return 1; + } + } + + $formatOption = $input->getOption('format'); + + if (\in_array($formatOption, $this->getAvailableFormatOptions())) { + $format = 'to'.ucfirst($formatOption); + } else { + $io->error(sprintf('Invalid format "%s", supported formats are "%s".', $formatOption, implode('", "', $this->getAvailableFormatOptions()))); + + return 1; + } + + $count = (int) $input->getOption('count'); + try { + for ($i = 0; $i < $count; ++$i) { + $output->writeln($this->factory->create($time)->$format()); + } + } catch (\Exception $e) { + $io->error($e->getMessage()); + + return 1; + } + + return 0; + } + + public function complete(CompletionInput $input, CompletionSuggestions $suggestions): void + { + if ($input->mustSuggestOptionValuesFor('format')) { + $suggestions->suggestValues($this->getAvailableFormatOptions()); + } + } + + private function getAvailableFormatOptions(): array + { + return [ + 'base32', + 'base58', + 'rfc4122', + ]; + } +} diff --git a/3rdparty/symfony/uid/Command/GenerateUuidCommand.php b/3rdparty/symfony/uid/Command/GenerateUuidCommand.php new file mode 100644 index 00000000..6dad923c --- /dev/null +++ b/3rdparty/symfony/uid/Command/GenerateUuidCommand.php @@ -0,0 +1,208 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Component\Uid\Command; + +use Symfony\Component\Console\Attribute\AsCommand; +use Symfony\Component\Console\Command\Command; +use Symfony\Component\Console\Completion\CompletionInput; +use Symfony\Component\Console\Completion\CompletionSuggestions; +use Symfony\Component\Console\Input\InputInterface; +use Symfony\Component\Console\Input\InputOption; +use Symfony\Component\Console\Output\ConsoleOutputInterface; +use Symfony\Component\Console\Output\OutputInterface; +use Symfony\Component\Console\Style\SymfonyStyle; +use Symfony\Component\Uid\Factory\UuidFactory; +use Symfony\Component\Uid\Uuid; + +#[AsCommand(name: 'uuid:generate', description: 'Generate a UUID')] +class GenerateUuidCommand extends Command +{ + private UuidFactory $factory; + + public function __construct(?UuidFactory $factory = null) + { + $this->factory = $factory ?? new UuidFactory(); + + parent::__construct(); + } + + protected function configure(): void + { + $this + ->setDefinition([ + new InputOption('time-based', null, InputOption::VALUE_REQUIRED, 'The timestamp, to generate a time-based UUID: a parsable date/time string'), + new InputOption('node', null, InputOption::VALUE_REQUIRED, 'The UUID whose node part should be used as the node of the generated UUID'), + new InputOption('name-based', null, InputOption::VALUE_REQUIRED, 'The name, to generate a name-based UUID'), + new InputOption('namespace', null, InputOption::VALUE_REQUIRED, 'The UUID to use at the namespace for named-based UUIDs, predefined namespaces keywords "dns", "url", "oid" and "x500" are accepted'), + new InputOption('random-based', null, InputOption::VALUE_NONE, 'To generate a random-based UUID'), + new InputOption('count', 'c', InputOption::VALUE_REQUIRED, 'The number of UUID to generate', 1), + new InputOption('format', 'f', InputOption::VALUE_REQUIRED, sprintf('The UUID output format ("%s")', implode('", "', $this->getAvailableFormatOptions())), 'rfc4122'), + ]) + ->setHelp(<<<'EOF' +The %command.name% generates a UUID. + + php %command.full_name% + +To generate a time-based UUID: + + php %command.full_name% --time-based=now + +To specify a time-based UUID's node: + + php %command.full_name% --time-based=@1613480254 --node=fb3502dc-137e-4849-8886-ac90d07f64a7 + +To generate a name-based UUID: + + php %command.full_name% --name-based=foo + +To specify a name-based UUID's namespace: + + php %command.full_name% --name-based=bar --namespace=fb3502dc-137e-4849-8886-ac90d07f64a7 + +To generate a random-based UUID: + + php %command.full_name% --random-based + +To generate several UUIDs: + + php %command.full_name% --count=10 + +To output a specific format: + + php %command.full_name% --format=base58 +EOF + ) + ; + } + + protected function execute(InputInterface $input, OutputInterface $output): int + { + $io = new SymfonyStyle($input, $output instanceof ConsoleOutputInterface ? $output->getErrorOutput() : $output); + + $time = $input->getOption('time-based'); + $node = $input->getOption('node'); + $name = $input->getOption('name-based'); + $namespace = $input->getOption('namespace'); + $random = $input->getOption('random-based'); + + if (false !== ($time ?? $name ?? $random) && 1 < ((null !== $time) + (null !== $name) + $random)) { + $io->error('Only one of "--time-based", "--name-based" or "--random-based" can be provided at a time.'); + + return 1; + } + + if (null === $time && null !== $node) { + $io->error('Option "--node" can only be used with "--time-based".'); + + return 1; + } + + if (null === $name && null !== $namespace) { + $io->error('Option "--namespace" can only be used with "--name-based".'); + + return 1; + } + + switch (true) { + case null !== $time: + if (null !== $node) { + try { + $node = Uuid::fromString($node); + } catch (\InvalidArgumentException $e) { + $io->error(sprintf('Invalid node "%s": %s', $node, $e->getMessage())); + + return 1; + } + } + + try { + new \DateTimeImmutable($time); + } catch (\Exception $e) { + $io->error(sprintf('Invalid timestamp "%s": %s', $time, str_replace('DateTimeImmutable::__construct(): ', '', $e->getMessage()))); + + return 1; + } + + $create = fn (): Uuid => $this->factory->timeBased($node)->create(new \DateTimeImmutable($time)); + break; + + case null !== $name: + if ($namespace && !\in_array($namespace, ['dns', 'url', 'oid', 'x500'], true)) { + try { + $namespace = Uuid::fromString($namespace); + } catch (\InvalidArgumentException $e) { + $io->error(sprintf('Invalid namespace "%s": %s', $namespace, $e->getMessage())); + + return 1; + } + } + + $create = function () use ($namespace, $name): Uuid { + try { + $factory = $this->factory->nameBased($namespace); + } catch (\LogicException) { + throw new \InvalidArgumentException('Missing namespace: use the "--namespace" option or configure a default namespace in the underlying factory.'); + } + + return $factory->create($name); + }; + break; + + case $random: + $create = $this->factory->randomBased()->create(...); + break; + + default: + $create = $this->factory->create(...); + break; + } + + $formatOption = $input->getOption('format'); + + if (\in_array($formatOption, $this->getAvailableFormatOptions())) { + $format = 'to'.ucfirst($formatOption); + } else { + $io->error(sprintf('Invalid format "%s", supported formats are "%s".', $formatOption, implode('", "', $this->getAvailableFormatOptions()))); + + return 1; + } + + $count = (int) $input->getOption('count'); + try { + for ($i = 0; $i < $count; ++$i) { + $output->writeln($create()->$format()); + } + } catch (\Exception $e) { + $io->error($e->getMessage()); + + return 1; + } + + return 0; + } + + public function complete(CompletionInput $input, CompletionSuggestions $suggestions): void + { + if ($input->mustSuggestOptionValuesFor('format')) { + $suggestions->suggestValues($this->getAvailableFormatOptions()); + } + } + + private function getAvailableFormatOptions(): array + { + return [ + 'base32', + 'base58', + 'rfc4122', + ]; + } +} diff --git a/3rdparty/symfony/uid/Command/InspectUlidCommand.php b/3rdparty/symfony/uid/Command/InspectUlidCommand.php new file mode 100644 index 00000000..b3a1b024 --- /dev/null +++ b/3rdparty/symfony/uid/Command/InspectUlidCommand.php @@ -0,0 +1,67 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Component\Uid\Command; + +use Symfony\Component\Console\Attribute\AsCommand; +use Symfony\Component\Console\Command\Command; +use Symfony\Component\Console\Helper\TableSeparator; +use Symfony\Component\Console\Input\InputArgument; +use Symfony\Component\Console\Input\InputInterface; +use Symfony\Component\Console\Output\ConsoleOutputInterface; +use Symfony\Component\Console\Output\OutputInterface; +use Symfony\Component\Console\Style\SymfonyStyle; +use Symfony\Component\Uid\Ulid; + +#[AsCommand(name: 'ulid:inspect', description: 'Inspect a ULID')] +class InspectUlidCommand extends Command +{ + protected function configure(): void + { + $this + ->setDefinition([ + new InputArgument('ulid', InputArgument::REQUIRED, 'The ULID to inspect'), + ]) + ->setHelp(<<<'EOF' +The %command.name% displays information about a ULID. + + php %command.full_name% 01EWAKBCMWQ2C94EXNN60ZBS0Q + php %command.full_name% 1BVdfLn3ERmbjYBLCdaaLW + php %command.full_name% 01771535-b29c-b898-923b-b5a981f5e417 +EOF + ) + ; + } + + protected function execute(InputInterface $input, OutputInterface $output): int + { + $io = new SymfonyStyle($input, $output instanceof ConsoleOutputInterface ? $output->getErrorOutput() : $output); + + try { + $ulid = Ulid::fromString($input->getArgument('ulid')); + } catch (\InvalidArgumentException $e) { + $io->error($e->getMessage()); + + return 1; + } + + $io->table(['Label', 'Value'], [ + ['toBase32 (canonical)', (string) $ulid], + ['toBase58', $ulid->toBase58()], + ['toRfc4122', $ulid->toRfc4122()], + ['toHex', $ulid->toHex()], + new TableSeparator(), + ['Time', $ulid->getDateTime()->format('Y-m-d H:i:s.v \U\T\C')], + ]); + + return 0; + } +} diff --git a/3rdparty/symfony/uid/Command/InspectUuidCommand.php b/3rdparty/symfony/uid/Command/InspectUuidCommand.php new file mode 100644 index 00000000..cc830bc3 --- /dev/null +++ b/3rdparty/symfony/uid/Command/InspectUuidCommand.php @@ -0,0 +1,85 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Component\Uid\Command; + +use Symfony\Component\Console\Attribute\AsCommand; +use Symfony\Component\Console\Command\Command; +use Symfony\Component\Console\Helper\TableSeparator; +use Symfony\Component\Console\Input\InputArgument; +use Symfony\Component\Console\Input\InputInterface; +use Symfony\Component\Console\Output\ConsoleOutputInterface; +use Symfony\Component\Console\Output\OutputInterface; +use Symfony\Component\Console\Style\SymfonyStyle; +use Symfony\Component\Uid\MaxUuid; +use Symfony\Component\Uid\NilUuid; +use Symfony\Component\Uid\TimeBasedUidInterface; +use Symfony\Component\Uid\Uuid; + +#[AsCommand(name: 'uuid:inspect', description: 'Inspect a UUID')] +class InspectUuidCommand extends Command +{ + protected function configure(): void + { + $this + ->setDefinition([ + new InputArgument('uuid', InputArgument::REQUIRED, 'The UUID to inspect'), + ]) + ->setHelp(<<<'EOF' +The %command.name% displays information about a UUID. + + php %command.full_name% a7613e0a-5986-11eb-a861-2bf05af69e52 + php %command.full_name% MfnmaUvvQ1h8B14vTwt6dX + php %command.full_name% 57C4Z0MPC627NTGR9BY1DFD7JJ +EOF + ) + ; + } + + protected function execute(InputInterface $input, OutputInterface $output): int + { + $io = new SymfonyStyle($input, $output instanceof ConsoleOutputInterface ? $output->getErrorOutput() : $output); + + try { + /** @var Uuid $uuid */ + $uuid = Uuid::fromString($input->getArgument('uuid')); + } catch (\InvalidArgumentException $e) { + $io->error($e->getMessage()); + + return 1; + } + + if (new NilUuid() == $uuid) { + $version = 'nil'; + } elseif (new MaxUuid() == $uuid) { + $version = 'max'; + } else { + $version = uuid_type($uuid); + } + + $rows = [ + ['Version', $version], + ['toRfc4122 (canonical)', (string) $uuid], + ['toBase58', $uuid->toBase58()], + ['toBase32', $uuid->toBase32()], + ['toHex', $uuid->toHex()], + ]; + + if ($uuid instanceof TimeBasedUidInterface) { + $rows[] = new TableSeparator(); + $rows[] = ['Time', $uuid->getDateTime()->format('Y-m-d H:i:s.u \U\T\C')]; + } + + $io->table(['Label', 'Value'], $rows); + + return 0; + } +} diff --git a/3rdparty/symfony/uid/Factory/NameBasedUuidFactory.php b/3rdparty/symfony/uid/Factory/NameBasedUuidFactory.php new file mode 100644 index 00000000..c8cf3666 --- /dev/null +++ b/3rdparty/symfony/uid/Factory/NameBasedUuidFactory.php @@ -0,0 +1,44 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Component\Uid\Factory; + +use Symfony\Component\Uid\Uuid; +use Symfony\Component\Uid\UuidV3; +use Symfony\Component\Uid\UuidV5; + +class NameBasedUuidFactory +{ + private string $class; + private Uuid $namespace; + + public function __construct(string $class, Uuid $namespace) + { + $this->class = $class; + $this->namespace = $namespace; + } + + public function create(string $name): UuidV5|UuidV3 + { + switch ($class = $this->class) { + case UuidV5::class: return Uuid::v5($this->namespace, $name); + case UuidV3::class: return Uuid::v3($this->namespace, $name); + } + + if (is_subclass_of($class, UuidV5::class)) { + $uuid = Uuid::v5($this->namespace, $name); + } else { + $uuid = Uuid::v3($this->namespace, $name); + } + + return new $class($uuid); + } +} diff --git a/3rdparty/symfony/uid/Factory/RandomBasedUuidFactory.php b/3rdparty/symfony/uid/Factory/RandomBasedUuidFactory.php new file mode 100644 index 00000000..67b14760 --- /dev/null +++ b/3rdparty/symfony/uid/Factory/RandomBasedUuidFactory.php @@ -0,0 +1,31 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Component\Uid\Factory; + +use Symfony\Component\Uid\UuidV4; + +class RandomBasedUuidFactory +{ + private string $class; + + public function __construct(string $class) + { + $this->class = $class; + } + + public function create(): UuidV4 + { + $class = $this->class; + + return new $class(); + } +} diff --git a/3rdparty/symfony/uid/Factory/TimeBasedUuidFactory.php b/3rdparty/symfony/uid/Factory/TimeBasedUuidFactory.php new file mode 100644 index 00000000..9c545ffc --- /dev/null +++ b/3rdparty/symfony/uid/Factory/TimeBasedUuidFactory.php @@ -0,0 +1,44 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Component\Uid\Factory; + +use Symfony\Component\Uid\TimeBasedUidInterface; +use Symfony\Component\Uid\Uuid; + +class TimeBasedUuidFactory +{ + /** + * @var class-string + */ + private string $class; + private ?Uuid $node; + + /** + * @param class-string $class + */ + public function __construct(string $class, ?Uuid $node = null) + { + $this->class = $class; + $this->node = $node; + } + + public function create(?\DateTimeInterface $time = null): Uuid&TimeBasedUidInterface + { + $class = $this->class; + + if (null === $time && null === $this->node) { + return new $class(); + } + + return new $class($class::generate($time, $this->node)); + } +} diff --git a/3rdparty/symfony/uid/Factory/UlidFactory.php b/3rdparty/symfony/uid/Factory/UlidFactory.php new file mode 100644 index 00000000..9dd9d004 --- /dev/null +++ b/3rdparty/symfony/uid/Factory/UlidFactory.php @@ -0,0 +1,22 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Component\Uid\Factory; + +use Symfony\Component\Uid\Ulid; + +class UlidFactory +{ + public function create(?\DateTimeInterface $time = null): Ulid + { + return new Ulid(null === $time ? null : Ulid::generate($time)); + } +} diff --git a/3rdparty/symfony/uid/Factory/UuidFactory.php b/3rdparty/symfony/uid/Factory/UuidFactory.php new file mode 100644 index 00000000..d1935c4b --- /dev/null +++ b/3rdparty/symfony/uid/Factory/UuidFactory.php @@ -0,0 +1,95 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Component\Uid\Factory; + +use Symfony\Component\Uid\Uuid; +use Symfony\Component\Uid\UuidV1; +use Symfony\Component\Uid\UuidV4; +use Symfony\Component\Uid\UuidV5; +use Symfony\Component\Uid\UuidV6; + +class UuidFactory +{ + private string $defaultClass; + private string $timeBasedClass; + private string $nameBasedClass; + private string $randomBasedClass; + private ?Uuid $timeBasedNode; + private ?Uuid $nameBasedNamespace; + + public function __construct(string|int $defaultClass = UuidV6::class, string|int $timeBasedClass = UuidV6::class, string|int $nameBasedClass = UuidV5::class, string|int $randomBasedClass = UuidV4::class, Uuid|string|null $timeBasedNode = null, Uuid|string|null $nameBasedNamespace = null) + { + if (null !== $timeBasedNode && !$timeBasedNode instanceof Uuid) { + $timeBasedNode = Uuid::fromString($timeBasedNode); + } + + if (null !== $nameBasedNamespace) { + $nameBasedNamespace = $this->getNamespace($nameBasedNamespace); + } + + $this->defaultClass = is_numeric($defaultClass) ? Uuid::class.'V'.$defaultClass : $defaultClass; + $this->timeBasedClass = is_numeric($timeBasedClass) ? Uuid::class.'V'.$timeBasedClass : $timeBasedClass; + $this->nameBasedClass = is_numeric($nameBasedClass) ? Uuid::class.'V'.$nameBasedClass : $nameBasedClass; + $this->randomBasedClass = is_numeric($randomBasedClass) ? Uuid::class.'V'.$randomBasedClass : $randomBasedClass; + $this->timeBasedNode = $timeBasedNode; + $this->nameBasedNamespace = $nameBasedNamespace; + } + + public function create(): Uuid + { + $class = $this->defaultClass; + + return new $class(); + } + + public function randomBased(): RandomBasedUuidFactory + { + return new RandomBasedUuidFactory($this->randomBasedClass); + } + + public function timeBased(Uuid|string|null $node = null): TimeBasedUuidFactory + { + $node ??= $this->timeBasedNode; + + if (null !== $node && !$node instanceof Uuid) { + $node = Uuid::fromString($node); + } + + return new TimeBasedUuidFactory($this->timeBasedClass, $node); + } + + public function nameBased(Uuid|string|null $namespace = null): NameBasedUuidFactory + { + $namespace ??= $this->nameBasedNamespace; + + if (null === $namespace) { + throw new \LogicException(sprintf('A namespace should be defined when using "%s()".', __METHOD__)); + } + + return new NameBasedUuidFactory($this->nameBasedClass, $this->getNamespace($namespace)); + } + + private function getNamespace(Uuid|string $namespace): Uuid + { + if ($namespace instanceof Uuid) { + return $namespace; + } + + return match ($namespace) { + 'dns' => new UuidV1(Uuid::NAMESPACE_DNS), + 'url' => new UuidV1(Uuid::NAMESPACE_URL), + 'oid' => new UuidV1(Uuid::NAMESPACE_OID), + 'x500' => new UuidV1(Uuid::NAMESPACE_X500), + default => Uuid::fromString($namespace), + }; + } +} diff --git a/3rdparty/symfony/uid/LICENSE b/3rdparty/symfony/uid/LICENSE new file mode 100644 index 00000000..0ed3a246 --- /dev/null +++ b/3rdparty/symfony/uid/LICENSE @@ -0,0 +1,19 @@ +Copyright (c) 2020-present Fabien Potencier + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is furnished +to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +THE SOFTWARE. diff --git a/3rdparty/symfony/uid/MaxUlid.php b/3rdparty/symfony/uid/MaxUlid.php new file mode 100644 index 00000000..9cc34576 --- /dev/null +++ b/3rdparty/symfony/uid/MaxUlid.php @@ -0,0 +1,20 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Component\Uid; + +class MaxUlid extends Ulid +{ + public function __construct() + { + $this->uid = parent::MAX; + } +} diff --git a/3rdparty/symfony/uid/MaxUuid.php b/3rdparty/symfony/uid/MaxUuid.php new file mode 100644 index 00000000..48eb656e --- /dev/null +++ b/3rdparty/symfony/uid/MaxUuid.php @@ -0,0 +1,22 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Component\Uid; + +class MaxUuid extends Uuid +{ + protected const TYPE = -1; + + public function __construct() + { + $this->uid = parent::MAX; + } +} diff --git a/3rdparty/symfony/uid/NilUlid.php b/3rdparty/symfony/uid/NilUlid.php new file mode 100644 index 00000000..56f6f199 --- /dev/null +++ b/3rdparty/symfony/uid/NilUlid.php @@ -0,0 +1,20 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Component\Uid; + +class NilUlid extends Ulid +{ + public function __construct() + { + $this->uid = parent::NIL; + } +} diff --git a/3rdparty/symfony/uid/NilUuid.php b/3rdparty/symfony/uid/NilUuid.php new file mode 100644 index 00000000..9d9746bb --- /dev/null +++ b/3rdparty/symfony/uid/NilUuid.php @@ -0,0 +1,25 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Component\Uid; + +/** + * @author Grégoire Pineau + */ +class NilUuid extends Uuid +{ + protected const TYPE = -1; + + public function __construct() + { + $this->uid = parent::NIL; + } +} diff --git a/3rdparty/symfony/uid/TimeBasedUidInterface.php b/3rdparty/symfony/uid/TimeBasedUidInterface.php new file mode 100644 index 00000000..f296aefb --- /dev/null +++ b/3rdparty/symfony/uid/TimeBasedUidInterface.php @@ -0,0 +1,22 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Component\Uid; + +/** + * Interface to describe UIDs that contain a DateTimeImmutable as part of their behaviour. + * + * @author Barney Hanlon + */ +interface TimeBasedUidInterface +{ + public function getDateTime(): \DateTimeImmutable; +} diff --git a/3rdparty/symfony/uid/Ulid.php b/3rdparty/symfony/uid/Ulid.php new file mode 100644 index 00000000..5359067a --- /dev/null +++ b/3rdparty/symfony/uid/Ulid.php @@ -0,0 +1,208 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Component\Uid; + +/** + * A ULID is lexicographically sortable and contains a 48-bit timestamp and 80-bit of crypto-random entropy. + * + * @see https://github.com/ulid/spec + * + * @author Nicolas Grekas + */ +class Ulid extends AbstractUid implements TimeBasedUidInterface +{ + protected const NIL = '00000000000000000000000000'; + protected const MAX = '7ZZZZZZZZZZZZZZZZZZZZZZZZZ'; + + private static string $time = ''; + private static array $rand = []; + + public function __construct(?string $ulid = null) + { + if (null === $ulid) { + $this->uid = static::generate(); + } elseif (self::NIL === $ulid) { + $this->uid = $ulid; + } elseif (self::MAX === strtr($ulid, 'z', 'Z')) { + $this->uid = $ulid; + } else { + if (!self::isValid($ulid)) { + throw new \InvalidArgumentException(sprintf('Invalid ULID: "%s".', $ulid)); + } + + $this->uid = strtoupper($ulid); + } + } + + public static function isValid(string $ulid): bool + { + if (26 !== \strlen($ulid)) { + return false; + } + + if (26 !== strspn($ulid, '0123456789ABCDEFGHJKMNPQRSTVWXYZabcdefghjkmnpqrstvwxyz')) { + return false; + } + + return $ulid[0] <= '7'; + } + + public static function fromString(string $ulid): static + { + if (36 === \strlen($ulid) && preg_match('{^[0-9a-f]{8}(?:-[0-9a-f]{4}){3}-[0-9a-f]{12}$}Di', $ulid)) { + $ulid = uuid_parse($ulid); + } elseif (22 === \strlen($ulid) && 22 === strspn($ulid, BinaryUtil::BASE58[''])) { + $ulid = str_pad(BinaryUtil::fromBase($ulid, BinaryUtil::BASE58), 16, "\0", \STR_PAD_LEFT); + } + + if (16 !== \strlen($ulid)) { + return match (strtr($ulid, 'z', 'Z')) { + self::NIL => new NilUlid(), + self::MAX => new MaxUlid(), + default => new static($ulid), + }; + } + + $ulid = bin2hex($ulid); + $ulid = sprintf('%02s%04s%04s%04s%04s%04s%04s', + base_convert(substr($ulid, 0, 2), 16, 32), + base_convert(substr($ulid, 2, 5), 16, 32), + base_convert(substr($ulid, 7, 5), 16, 32), + base_convert(substr($ulid, 12, 5), 16, 32), + base_convert(substr($ulid, 17, 5), 16, 32), + base_convert(substr($ulid, 22, 5), 16, 32), + base_convert(substr($ulid, 27, 5), 16, 32) + ); + + if (self::NIL === $ulid) { + return new NilUlid(); + } + + if (self::MAX === $ulid = strtr($ulid, 'abcdefghijklmnopqrstuv', 'ABCDEFGHJKMNPQRSTVWXYZ')) { + return new MaxUlid(); + } + + $u = new static(self::NIL); + $u->uid = $ulid; + + return $u; + } + + public function toBinary(): string + { + $ulid = strtr($this->uid, 'ABCDEFGHJKMNPQRSTVWXYZ', 'abcdefghijklmnopqrstuv'); + + $ulid = sprintf('%02s%05s%05s%05s%05s%05s%05s', + base_convert(substr($ulid, 0, 2), 32, 16), + base_convert(substr($ulid, 2, 4), 32, 16), + base_convert(substr($ulid, 6, 4), 32, 16), + base_convert(substr($ulid, 10, 4), 32, 16), + base_convert(substr($ulid, 14, 4), 32, 16), + base_convert(substr($ulid, 18, 4), 32, 16), + base_convert(substr($ulid, 22, 4), 32, 16) + ); + + return hex2bin($ulid); + } + + /** + * Returns the identifier as a base32 case insensitive string. + * + * @see https://tools.ietf.org/html/rfc4648#section-6 + * + * @example 09EJ0S614A9FXVG9C5537Q9ZE1 (len=26) + */ + public function toBase32(): string + { + return $this->uid; + } + + public function getDateTime(): \DateTimeImmutable + { + $time = strtr(substr($this->uid, 0, 10), 'ABCDEFGHJKMNPQRSTVWXYZ', 'abcdefghijklmnopqrstuv'); + + if (\PHP_INT_SIZE >= 8) { + $time = (string) hexdec(base_convert($time, 32, 16)); + } else { + $time = sprintf('%02s%05s%05s', + base_convert(substr($time, 0, 2), 32, 16), + base_convert(substr($time, 2, 4), 32, 16), + base_convert(substr($time, 6, 4), 32, 16) + ); + $time = BinaryUtil::toBase(hex2bin($time), BinaryUtil::BASE10); + } + + if (4 > \strlen($time)) { + $time = '000'.$time; + } + + return \DateTimeImmutable::createFromFormat('U.u', substr_replace($time, '.', -3, 0)); + } + + public static function generate(?\DateTimeInterface $time = null): string + { + if (null === $mtime = $time) { + $time = microtime(false); + $time = substr($time, 11).substr($time, 2, 3); + } elseif (0 > $time = $time->format('Uv')) { + throw new \InvalidArgumentException('The timestamp must be positive.'); + } + + if ($time > self::$time || (null !== $mtime && $time !== self::$time)) { + randomize: + $r = unpack('n*', random_bytes(10)); + $r[1] |= ($r[5] <<= 4) & 0xF0000; + $r[2] |= ($r[5] <<= 4) & 0xF0000; + $r[3] |= ($r[5] <<= 4) & 0xF0000; + $r[4] |= ($r[5] <<= 4) & 0xF0000; + unset($r[5]); + self::$rand = $r; + self::$time = $time; + } elseif ([1 => 0xFFFFF, 0xFFFFF, 0xFFFFF, 0xFFFFF] === self::$rand) { + if (\PHP_INT_SIZE >= 8 || 10 > \strlen($time = self::$time)) { + $time = (string) (1 + $time); + } elseif ('999999999' === $mtime = substr($time, -9)) { + $time = (1 + substr($time, 0, -9)).'000000000'; + } else { + $time = substr_replace($time, str_pad(++$mtime, 9, '0', \STR_PAD_LEFT), -9); + } + + goto randomize; + } else { + for ($i = 4; $i > 0 && 0xFFFFF === self::$rand[$i]; --$i) { + self::$rand[$i] = 0; + } + + ++self::$rand[$i]; + $time = self::$time; + } + + if (\PHP_INT_SIZE >= 8) { + $time = base_convert($time, 10, 32); + } else { + $time = str_pad(bin2hex(BinaryUtil::fromBase($time, BinaryUtil::BASE10)), 12, '0', \STR_PAD_LEFT); + $time = sprintf('%s%04s%04s', + base_convert(substr($time, 0, 2), 16, 32), + base_convert(substr($time, 2, 5), 16, 32), + base_convert(substr($time, 7, 5), 16, 32) + ); + } + + return strtr(sprintf('%010s%04s%04s%04s%04s', + $time, + base_convert(self::$rand[1], 10, 32), + base_convert(self::$rand[2], 10, 32), + base_convert(self::$rand[3], 10, 32), + base_convert(self::$rand[4], 10, 32) + ), 'abcdefghijklmnopqrstuv', 'ABCDEFGHJKMNPQRSTVWXYZ'); + } +} diff --git a/3rdparty/symfony/uid/Uuid.php b/3rdparty/symfony/uid/Uuid.php new file mode 100644 index 00000000..85d366c1 --- /dev/null +++ b/3rdparty/symfony/uid/Uuid.php @@ -0,0 +1,185 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Component\Uid; + +/** + * @author Grégoire Pineau + * + * @see https://tools.ietf.org/html/rfc4122#appendix-C for details about namespaces + */ +class Uuid extends AbstractUid +{ + public const NAMESPACE_DNS = '6ba7b810-9dad-11d1-80b4-00c04fd430c8'; + public const NAMESPACE_URL = '6ba7b811-9dad-11d1-80b4-00c04fd430c8'; + public const NAMESPACE_OID = '6ba7b812-9dad-11d1-80b4-00c04fd430c8'; + public const NAMESPACE_X500 = '6ba7b814-9dad-11d1-80b4-00c04fd430c8'; + + protected const TYPE = 0; + protected const NIL = '00000000-0000-0000-0000-000000000000'; + protected const MAX = 'ffffffff-ffff-ffff-ffff-ffffffffffff'; + + public function __construct(string $uuid, bool $checkVariant = false) + { + $type = preg_match('{^[0-9a-f]{8}(?:-[0-9a-f]{4}){3}-[0-9a-f]{12}$}Di', $uuid) ? (int) $uuid[14] : false; + + if (false === $type || (static::TYPE ?: $type) !== $type) { + throw new \InvalidArgumentException(sprintf('Invalid UUID%s: "%s".', static::TYPE ? 'v'.static::TYPE : '', $uuid)); + } + + $this->uid = strtolower($uuid); + + if ($checkVariant && !\in_array($this->uid[19], ['8', '9', 'a', 'b'], true)) { + throw new \InvalidArgumentException(sprintf('Invalid UUID%s: "%s".', static::TYPE ? 'v'.static::TYPE : '', $uuid)); + } + } + + public static function fromString(string $uuid): static + { + if (22 === \strlen($uuid) && 22 === strspn($uuid, BinaryUtil::BASE58[''])) { + $uuid = str_pad(BinaryUtil::fromBase($uuid, BinaryUtil::BASE58), 16, "\0", \STR_PAD_LEFT); + } + + if (16 === \strlen($uuid)) { + // don't use uuid_unparse(), it's slower + $uuid = bin2hex($uuid); + $uuid = substr_replace($uuid, '-', 8, 0); + $uuid = substr_replace($uuid, '-', 13, 0); + $uuid = substr_replace($uuid, '-', 18, 0); + $uuid = substr_replace($uuid, '-', 23, 0); + } elseif (26 === \strlen($uuid) && Ulid::isValid($uuid)) { + $ulid = new NilUlid(); + $ulid->uid = strtoupper($uuid); + $uuid = $ulid->toRfc4122(); + } + + if (__CLASS__ !== static::class || 36 !== \strlen($uuid)) { + return new static($uuid); + } + + if (self::NIL === $uuid) { + return new NilUuid(); + } + + if (self::MAX === $uuid = strtr($uuid, 'F', 'f')) { + return new MaxUuid(); + } + + if (!\in_array($uuid[19], ['8', '9', 'a', 'b', 'A', 'B'], true)) { + return new self($uuid); + } + + return match ((int) $uuid[14]) { + UuidV1::TYPE => new UuidV1($uuid), + UuidV3::TYPE => new UuidV3($uuid), + UuidV4::TYPE => new UuidV4($uuid), + UuidV5::TYPE => new UuidV5($uuid), + UuidV6::TYPE => new UuidV6($uuid), + UuidV7::TYPE => new UuidV7($uuid), + UuidV8::TYPE => new UuidV8($uuid), + default => new self($uuid), + }; + } + + final public static function v1(): UuidV1 + { + return new UuidV1(); + } + + final public static function v3(self $namespace, string $name): UuidV3 + { + // don't use uuid_generate_md5(), some versions are buggy + $uuid = md5(hex2bin(str_replace('-', '', $namespace->uid)).$name, true); + + return new UuidV3(self::format($uuid, '-3')); + } + + final public static function v4(): UuidV4 + { + return new UuidV4(); + } + + final public static function v5(self $namespace, string $name): UuidV5 + { + // don't use uuid_generate_sha1(), some versions are buggy + $uuid = substr(sha1(hex2bin(str_replace('-', '', $namespace->uid)).$name, true), 0, 16); + + return new UuidV5(self::format($uuid, '-5')); + } + + final public static function v6(): UuidV6 + { + return new UuidV6(); + } + + final public static function v7(): UuidV7 + { + return new UuidV7(); + } + + final public static function v8(string $uuid): UuidV8 + { + return new UuidV8($uuid); + } + + public static function isValid(string $uuid): bool + { + if (self::NIL === $uuid && \in_array(static::class, [__CLASS__, NilUuid::class], true)) { + return true; + } + + if (self::MAX === strtr($uuid, 'F', 'f') && \in_array(static::class, [__CLASS__, MaxUuid::class], true)) { + return true; + } + + if (!preg_match('{^[0-9a-f]{8}(?:-[0-9a-f]{4}){2}-[89ab][0-9a-f]{3}-[0-9a-f]{12}$}Di', $uuid)) { + return false; + } + + return __CLASS__ === static::class || static::TYPE === (int) $uuid[14]; + } + + public function toBinary(): string + { + return uuid_parse($this->uid); + } + + /** + * Returns the identifier as a RFC4122 case insensitive string. + * + * @see https://tools.ietf.org/html/rfc4122#section-3 + * + * @example 09748193-048a-4bfb-b825-8528cf74fdc1 (len=36) + */ + public function toRfc4122(): string + { + return $this->uid; + } + + public function compare(AbstractUid $other): int + { + if (false !== $cmp = uuid_compare($this->uid, $other->uid)) { + return $cmp; + } + + return parent::compare($other); + } + + private static function format(string $uuid, string $version): string + { + $uuid[8] = $uuid[8] & "\x3F" | "\x80"; + $uuid = substr_replace(bin2hex($uuid), '-', 8, 0); + $uuid = substr_replace($uuid, $version, 13, 1); + $uuid = substr_replace($uuid, '-', 18, 0); + + return substr_replace($uuid, '-', 23, 0); + } +} diff --git a/3rdparty/symfony/uid/UuidV1.php b/3rdparty/symfony/uid/UuidV1.php new file mode 100644 index 00000000..1ec00416 --- /dev/null +++ b/3rdparty/symfony/uid/UuidV1.php @@ -0,0 +1,73 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Component\Uid; + +/** + * A v1 UUID contains a 60-bit timestamp and 62 extra unique bits. + * + * @author Grégoire Pineau + */ +class UuidV1 extends Uuid implements TimeBasedUidInterface +{ + protected const TYPE = 1; + + private static string $clockSeq; + + public function __construct(?string $uuid = null) + { + if (null === $uuid) { + $this->uid = uuid_create(static::TYPE); + } else { + parent::__construct($uuid, true); + } + } + + public function getDateTime(): \DateTimeImmutable + { + return BinaryUtil::hexToDateTime('0'.substr($this->uid, 15, 3).substr($this->uid, 9, 4).substr($this->uid, 0, 8)); + } + + public function getNode(): string + { + return uuid_mac($this->uid); + } + + public static function generate(?\DateTimeInterface $time = null, ?Uuid $node = null): string + { + $uuid = !$time || !$node ? uuid_create(static::TYPE) : parent::NIL; + + if ($time) { + if ($node) { + // use clock_seq from the node + $seq = substr($node->uid, 19, 4); + } elseif (!$seq = self::$clockSeq ?? '') { + // generate a static random clock_seq to prevent any collisions with the real one + $seq = substr($uuid, 19, 4); + + do { + self::$clockSeq = sprintf('%04x', random_int(0, 0x3FFF) | 0x8000); + } while ($seq === self::$clockSeq); + + $seq = self::$clockSeq; + } + + $time = BinaryUtil::dateTimeToHex($time); + $uuid = substr($time, 8).'-'.substr($time, 4, 4).'-1'.substr($time, 1, 3).'-'.$seq.substr($uuid, 23); + } + + if ($node) { + $uuid = substr($uuid, 0, 24).substr($node->uid, 24); + } + + return $uuid; + } +} diff --git a/3rdparty/symfony/uid/UuidV3.php b/3rdparty/symfony/uid/UuidV3.php new file mode 100644 index 00000000..cc9f016b --- /dev/null +++ b/3rdparty/symfony/uid/UuidV3.php @@ -0,0 +1,29 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Component\Uid; + +/** + * A v3 UUID contains an MD5 hash of another UUID and a name. + * + * Use Uuid::v3() to compute one. + * + * @author Grégoire Pineau + */ +class UuidV3 extends Uuid +{ + protected const TYPE = 3; + + public function __construct(string $uuid) + { + parent::__construct($uuid, true); + } +} diff --git a/3rdparty/symfony/uid/UuidV4.php b/3rdparty/symfony/uid/UuidV4.php new file mode 100644 index 00000000..9b96ef34 --- /dev/null +++ b/3rdparty/symfony/uid/UuidV4.php @@ -0,0 +1,36 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Component\Uid; + +/** + * A v4 UUID contains a 122-bit random number. + * + * @author Grégoire Pineau + */ +class UuidV4 extends Uuid +{ + protected const TYPE = 4; + + public function __construct(?string $uuid = null) + { + if (null === $uuid) { + $uuid = random_bytes(16); + $uuid[6] = $uuid[6] & "\x0F" | "\x40"; + $uuid[8] = $uuid[8] & "\x3F" | "\x80"; + $uuid = bin2hex($uuid); + + $this->uid = substr($uuid, 0, 8).'-'.substr($uuid, 8, 4).'-'.substr($uuid, 12, 4).'-'.substr($uuid, 16, 4).'-'.substr($uuid, 20, 12); + } else { + parent::__construct($uuid, true); + } + } +} diff --git a/3rdparty/symfony/uid/UuidV5.php b/3rdparty/symfony/uid/UuidV5.php new file mode 100644 index 00000000..74ab133a --- /dev/null +++ b/3rdparty/symfony/uid/UuidV5.php @@ -0,0 +1,29 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Component\Uid; + +/** + * A v5 UUID contains a SHA1 hash of another UUID and a name. + * + * Use Uuid::v5() to compute one. + * + * @author Grégoire Pineau + */ +class UuidV5 extends Uuid +{ + protected const TYPE = 5; + + public function __construct(string $uuid) + { + parent::__construct($uuid, true); + } +} diff --git a/3rdparty/symfony/uid/UuidV6.php b/3rdparty/symfony/uid/UuidV6.php new file mode 100644 index 00000000..b3492083 --- /dev/null +++ b/3rdparty/symfony/uid/UuidV6.php @@ -0,0 +1,66 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Component\Uid; + +/** + * A v6 UUID is lexicographically sortable and contains a 60-bit timestamp and 62 extra unique bits. + * + * Unlike UUIDv1, this implementation of UUIDv6 doesn't leak the MAC address of the host. + * + * @author Nicolas Grekas + */ +class UuidV6 extends Uuid implements TimeBasedUidInterface +{ + protected const TYPE = 6; + + private static string $node; + + public function __construct(?string $uuid = null) + { + if (null === $uuid) { + $this->uid = static::generate(); + } else { + parent::__construct($uuid, true); + } + } + + public function getDateTime(): \DateTimeImmutable + { + return BinaryUtil::hexToDateTime('0'.substr($this->uid, 0, 8).substr($this->uid, 9, 4).substr($this->uid, 15, 3)); + } + + public function getNode(): string + { + return substr($this->uid, 24); + } + + public static function generate(?\DateTimeInterface $time = null, ?Uuid $node = null): string + { + $uuidV1 = UuidV1::generate($time, $node); + $uuid = substr($uuidV1, 15, 3).substr($uuidV1, 9, 4).$uuidV1[0].'-'.substr($uuidV1, 1, 4).'-6'.substr($uuidV1, 5, 3).substr($uuidV1, 18, 6); + + if ($node) { + return $uuid.substr($uuidV1, 24); + } + + // uuid_create() returns a stable "node" that can leak the MAC of the host, but + // UUIDv6 prefers a truly random number here, let's XOR both to preserve the entropy + + if (!isset(self::$node)) { + $seed = [random_int(0, 0xFFFFFF), random_int(0, 0xFFFFFF)]; + $node = unpack('N2', hex2bin('00'.substr($uuidV1, 24, 6)).hex2bin('00'.substr($uuidV1, 30))); + self::$node = sprintf('%06x%06x', ($seed[0] ^ $node[1]) | 0x010000, $seed[1] ^ $node[2]); + } + + return $uuid.self::$node; + } +} diff --git a/3rdparty/symfony/uid/UuidV7.php b/3rdparty/symfony/uid/UuidV7.php new file mode 100644 index 00000000..43740b67 --- /dev/null +++ b/3rdparty/symfony/uid/UuidV7.php @@ -0,0 +1,125 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Component\Uid; + +/** + * A v7 UUID is lexicographically sortable and contains a 48-bit timestamp and 74 extra unique bits. + * + * Within the same millisecond, monotonicity is ensured by incrementing the random part by a random increment. + * + * @author Nicolas Grekas + */ +class UuidV7 extends Uuid implements TimeBasedUidInterface +{ + protected const TYPE = 7; + + private static string $time = ''; + private static array $rand = []; + private static string $seed; + private static array $seedParts; + private static int $seedIndex = 0; + + public function __construct(?string $uuid = null) + { + if (null === $uuid) { + $this->uid = static::generate(); + } else { + parent::__construct($uuid, true); + } + } + + public function getDateTime(): \DateTimeImmutable + { + $time = substr($this->uid, 0, 8).substr($this->uid, 9, 4); + $time = \PHP_INT_SIZE >= 8 ? (string) hexdec($time) : BinaryUtil::toBase(hex2bin($time), BinaryUtil::BASE10); + + if (4 > \strlen($time)) { + $time = '000'.$time; + } + + return \DateTimeImmutable::createFromFormat('U.v', substr_replace($time, '.', -3, 0)); + } + + public static function generate(?\DateTimeInterface $time = null): string + { + if (null === $mtime = $time) { + $time = microtime(false); + $time = substr($time, 11).substr($time, 2, 3); + } elseif (0 > $time = $time->format('Uv')) { + throw new \InvalidArgumentException('The timestamp must be positive.'); + } + + if ($time > self::$time || (null !== $mtime && $time !== self::$time)) { + randomize: + self::$rand = unpack('n*', isset(self::$seed) ? random_bytes(10) : self::$seed = random_bytes(16)); + self::$rand[1] &= 0x03FF; + self::$time = $time; + } else { + // Within the same ms, we increment the rand part by a random 24-bit number. + // Instead of getting this number from random_bytes(), which is slow, we get + // it by sha512-hashing self::$seed. This produces 64 bytes of entropy, + // which we need to split in a list of 24-bit numbers. unpack() first splits + // them into 16 x 32-bit numbers; we take the first byte of each of these + // numbers to get 5 extra 24-bit numbers. Then, we consume those numbers + // one-by-one and run this logic every 21 iterations. + // self::$rand holds the random part of the UUID, split into 5 x 16-bit + // numbers for x86 portability. We increment this random part by the next + // 24-bit number in the self::$seedParts list and decrement self::$seedIndex. + + if (!self::$seedIndex) { + $s = unpack('l*', self::$seed = hash('sha512', self::$seed, true)); + $s[] = ($s[1] >> 8 & 0xFF0000) | ($s[2] >> 16 & 0xFF00) | ($s[3] >> 24 & 0xFF); + $s[] = ($s[4] >> 8 & 0xFF0000) | ($s[5] >> 16 & 0xFF00) | ($s[6] >> 24 & 0xFF); + $s[] = ($s[7] >> 8 & 0xFF0000) | ($s[8] >> 16 & 0xFF00) | ($s[9] >> 24 & 0xFF); + $s[] = ($s[10] >> 8 & 0xFF0000) | ($s[11] >> 16 & 0xFF00) | ($s[12] >> 24 & 0xFF); + $s[] = ($s[13] >> 8 & 0xFF0000) | ($s[14] >> 16 & 0xFF00) | ($s[15] >> 24 & 0xFF); + self::$seedParts = $s; + self::$seedIndex = 21; + } + + self::$rand[5] = 0xFFFF & $carry = self::$rand[5] + 1 + (self::$seedParts[self::$seedIndex--] & 0xFFFFFF); + self::$rand[4] = 0xFFFF & $carry = self::$rand[4] + ($carry >> 16); + self::$rand[3] = 0xFFFF & $carry = self::$rand[3] + ($carry >> 16); + self::$rand[2] = 0xFFFF & $carry = self::$rand[2] + ($carry >> 16); + self::$rand[1] += $carry >> 16; + + if (0xFC00 & self::$rand[1]) { + if (\PHP_INT_SIZE >= 8 || 10 > \strlen($time = self::$time)) { + $time = (string) (1 + $time); + } elseif ('999999999' === $mtime = substr($time, -9)) { + $time = (1 + substr($time, 0, -9)).'000000000'; + } else { + $time = substr_replace($time, str_pad(++$mtime, 9, '0', \STR_PAD_LEFT), -9); + } + + goto randomize; + } + + $time = self::$time; + } + + if (\PHP_INT_SIZE >= 8) { + $time = dechex($time); + } else { + $time = bin2hex(BinaryUtil::fromBase($time, BinaryUtil::BASE10)); + } + + return substr_replace(sprintf('%012s-%04x-%04x-%04x%04x%04x', + $time, + 0x7000 | (self::$rand[1] << 2) | (self::$rand[2] >> 14), + 0x8000 | (self::$rand[2] & 0x3FFF), + self::$rand[3], + self::$rand[4], + self::$rand[5], + ), '-', 8, 0); + } +} diff --git a/3rdparty/symfony/uid/UuidV8.php b/3rdparty/symfony/uid/UuidV8.php new file mode 100644 index 00000000..c194a6f6 --- /dev/null +++ b/3rdparty/symfony/uid/UuidV8.php @@ -0,0 +1,27 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Component\Uid; + +/** + * A v8 UUID has no explicit requirements except embedding its version + variant bits. + * + * @author Nicolas Grekas + */ +class UuidV8 extends Uuid +{ + protected const TYPE = 8; + + public function __construct(string $uuid) + { + parent::__construct($uuid, true); + } +} diff --git a/3rdparty/wapmorgan/mp3info/LICENSE b/3rdparty/wapmorgan/mp3info/LICENSE new file mode 100644 index 00000000..6600f1c9 --- /dev/null +++ b/3rdparty/wapmorgan/mp3info/LICENSE @@ -0,0 +1,165 @@ +GNU LESSER GENERAL PUBLIC LICENSE + Version 3, 29 June 2007 + + Copyright (C) 2007 Free Software Foundation, Inc. + Everyone is permitted to copy and distribute verbatim copies + of this license document, but changing it is not allowed. + + + This version of the GNU Lesser General Public License incorporates +the terms and conditions of version 3 of the GNU General Public +License, supplemented by the additional permissions listed below. + + 0. Additional Definitions. + + As used herein, "this License" refers to version 3 of the GNU Lesser +General Public License, and the "GNU GPL" refers to version 3 of the GNU +General Public License. + + "The Library" refers to a covered work governed by this License, +other than an Application or a Combined Work as defined below. + + An "Application" is any work that makes use of an interface provided +by the Library, but which is not otherwise based on the Library. +Defining a subclass of a class defined by the Library is deemed a mode +of using an interface provided by the Library. + + A "Combined Work" is a work produced by combining or linking an +Application with the Library. The particular version of the Library +with which the Combined Work was made is also called the "Linked +Version". + + The "Minimal Corresponding Source" for a Combined Work means the +Corresponding Source for the Combined Work, excluding any source code +for portions of the Combined Work that, considered in isolation, are +based on the Application, and not on the Linked Version. + + The "Corresponding Application Code" for a Combined Work means the +object code and/or source code for the Application, including any data +and utility programs needed for reproducing the Combined Work from the +Application, but excluding the System Libraries of the Combined Work. + + 1. Exception to Section 3 of the GNU GPL. + + You may convey a covered work under sections 3 and 4 of this License +without being bound by section 3 of the GNU GPL. + + 2. Conveying Modified Versions. + + If you modify a copy of the Library, and, in your modifications, a +facility refers to a function or data to be supplied by an Application +that uses the facility (other than as an argument passed when the +facility is invoked), then you may convey a copy of the modified +version: + + a) under this License, provided that you make a good faith effort to + ensure that, in the event an Application does not supply the + function or data, the facility still operates, and performs + whatever part of its purpose remains meaningful, or + + b) under the GNU GPL, with none of the additional permissions of + this License applicable to that copy. + + 3. Object Code Incorporating Material from Library Header Files. + + The object code form of an Application may incorporate material from +a header file that is part of the Library. You may convey such object +code under terms of your choice, provided that, if the incorporated +material is not limited to numerical parameters, data structure +layouts and accessors, or small macros, inline functions and templates +(ten or fewer lines in length), you do both of the following: + + a) Give prominent notice with each copy of the object code that the + Library is used in it and that the Library and its use are + covered by this License. + + b) Accompany the object code with a copy of the GNU GPL and this license + document. + + 4. Combined Works. + + You may convey a Combined Work under terms of your choice that, +taken together, effectively do not restrict modification of the +portions of the Library contained in the Combined Work and reverse +engineering for debugging such modifications, if you also do each of +the following: + + a) Give prominent notice with each copy of the Combined Work that + the Library is used in it and that the Library and its use are + covered by this License. + + b) Accompany the Combined Work with a copy of the GNU GPL and this license + document. + + c) For a Combined Work that displays copyright notices during + execution, include the copyright notice for the Library among + these notices, as well as a reference directing the user to the + copies of the GNU GPL and this license document. + + d) Do one of the following: + + 0) Convey the Minimal Corresponding Source under the terms of this + License, and the Corresponding Application Code in a form + suitable for, and under terms that permit, the user to + recombine or relink the Application with a modified version of + the Linked Version to produce a modified Combined Work, in the + manner specified by section 6 of the GNU GPL for conveying + Corresponding Source. + + 1) Use a suitable shared library mechanism for linking with the + Library. A suitable mechanism is one that (a) uses at run time + a copy of the Library already present on the user's computer + system, and (b) will operate properly with a modified version + of the Library that is interface-compatible with the Linked + Version. + + e) Provide Installation Information, but only if you would otherwise + be required to provide such information under section 6 of the + GNU GPL, and only to the extent that such information is + necessary to install and execute a modified version of the + Combined Work produced by recombining or relinking the + Application with a modified version of the Linked Version. (If + you use option 4d0, the Installation Information must accompany + the Minimal Corresponding Source and Corresponding Application + Code. If you use option 4d1, you must provide the Installation + Information in the manner specified by section 6 of the GNU GPL + for conveying Corresponding Source.) + + 5. Combined Libraries. + + You may place library facilities that are a work based on the +Library side by side in a single library together with other library +facilities that are not Applications and are not covered by this +License, and convey such a combined library under terms of your +choice, if you do both of the following: + + a) Accompany the combined library with a copy of the same work based + on the Library, uncombined with any other library facilities, + conveyed under the terms of this License. + + b) Give prominent notice with the combined library that part of it + is a work based on the Library, and explaining where to find the + accompanying uncombined form of the same work. + + 6. Revised Versions of the GNU Lesser General Public License. + + The Free Software Foundation may publish revised and/or new versions +of the GNU Lesser General Public License from time to time. Such new +versions will be similar in spirit to the present version, but may +differ in detail to address new problems or concerns. + + Each version is given a distinguishing version number. If the +Library as you received it specifies that a certain numbered version +of the GNU Lesser General Public License "or any later version" +applies to it, you have the option of following the terms and +conditions either of that published version or of any later version +published by the Free Software Foundation. If the Library as you +received it does not specify a version number of the GNU Lesser +General Public License, you may choose any version of the GNU Lesser +General Public License ever published by the Free Software Foundation. + + If the Library as you received it specifies that a proxy can decide +whether future versions of the GNU Lesser General Public License shall +apply, that proxy's public statement of acceptance of any version is +permanent authorization for you to choose that version for the +Library. diff --git a/3rdparty/wapmorgan/mp3info/PATCHES.txt b/3rdparty/wapmorgan/mp3info/PATCHES.txt new file mode 100644 index 00000000..9d10c66d --- /dev/null +++ b/3rdparty/wapmorgan/mp3info/PATCHES.txt @@ -0,0 +1,11 @@ +This file was automatically generated by Composer Patches (https://github.com/cweagans/composer-patches) +Patches applied to this directory: + +Break frame parsing on invalid frame +Source: .patches/mp3info-break-frame-parsing.patch + + +fix incorrect lookup for mpeg header +Source: .patches/mp3info-fix-incorrect-lookup-for-mpeg-header.patch + + diff --git a/3rdparty/wapmorgan/mp3info/src/Mp3Info.php b/3rdparty/wapmorgan/mp3info/src/Mp3Info.php new file mode 100644 index 00000000..24781d7c --- /dev/null +++ b/3rdparty/wapmorgan/mp3info/src/Mp3Info.php @@ -0,0 +1,1090 @@ + [21, 36], + self::MPEG_2 => [13, 21], + self::MPEG_25 => [13, 21], + ]; + + /** + * @var int Limit in bytes for seeking a mpeg header in file + */ + public static $headerSeekLimit = 2048; + + public static $framesCountRead = 2; + + /** + * @var int MPEG codec version (1 or 2 or 2.5 or undefined) + */ + public $codecVersion; + + /** + * @var int Audio layer version (1 or 2 or 3) + */ + public $layerVersion; + + /** + * @var int Audio size in bytes. Note that this value is NOT equals file size. + */ + public $audioSize; + + /** + * @var float Audio duration in seconds.microseconds (e.g. 3603.0171428571) + */ + public $duration; + + /** + * @var int Audio bit rate in bps (e.g. 128000) + */ + public $bitRate; + + /** + * @var int Audio sample rate in Hz (e.g. 44100) + */ + public $sampleRate; + + /** + * @var boolean Contains true if audio has variable bit rate + */ + public $isVbr = false; + + /** + * @var boolean Contains true if audio has cover + */ + public $hasCover = false; + + /** + * @var array Contains VBR properties + */ + public $vbrProperties = []; + + /** + * @var array Contains picture properties + */ + public $coverProperties = []; + + /** + * Channel mode (stereo or dual_mono or joint_stereo or mono) + * @var string + */ + public $channel; + + /** + * @var array Unified list of tags (id3v1 and id3v2 united) + */ + public $tags = []; + + /** + * @var array Audio tags ver. 1 (aka id3v1) + */ + public $tags1 = []; + + /** + * @var array Audio tags ver. 2 (aka id3v2) + */ + public $tags2 = []; + + /** + * @var int Major version of id3v2 tag (if id3v2 present) (2 or 3 or 4) + */ + public $id3v2MajorVersion; + + /** + * @var int Minor version of id3v2 tag (if id3v2 present) + */ + public $id3v2MinorVersion; + + /** + * @var array List of id3v2 header flags (if id3v2 present) + */ + public $id3v2Flags = []; + + /** + * @var array List of id3v2 tags flags (if id3v2 present) + */ + public $id3v2TagsFlags = []; + + /** + * @var string Contains audio file name + */ + public $_fileName; + + /** + * @var int Contains file size + */ + public $_fileSize; + + /** + * @var int Number of audio frames in file + */ + public $_framesCount = 0; + + /** + * @var float Contains time spent to read&extract audio information. + */ + public $_parsingTime; + + /** + * @var int Calculated frame size for Constant Bit Rate + */ + private $_cbrFrameSize; + + /** + * @var int|null Size of id3v2-data + */ + public $_id3Size; + + /** + * $mode is self::META, self::TAGS or their combination. + * + * @param string $filename + * @param bool $parseTags + * + * @throws \Exception + */ + public function __construct($filename, $parseTags = false) { + if (self::$_bitRateTable === null) + self::$_bitRateTable = require dirname(__FILE__).'/../data/bitRateTable.php'; + if (self::$_sampleRateTable === null) + self::$_sampleRateTable = require dirname(__FILE__).'/../data/sampleRateTable.php'; + + $this->_fileName = $filename; + $isLocal = (strpos($filename, '://') === false); + if (!$isLocal) { + $this->_fileSize = static::getUrlContentLength($filename); + } else { + if (!file_exists($filename)) { + throw new \Exception('File ' . $filename . ' is not present!'); + } + $this->_fileSize = filesize($filename); + } + + if ($isLocal and !static::isValidAudio($filename)) { + throw new \Exception('File ' . $filename . ' is not mpeg/audio!'); + } + + $mode = $parseTags ? self::META | self::TAGS : self::META; + $this->audioSize = $this->parseAudio($this->_fileName, $this->_fileSize, $mode); + } + + + /** + * @return bool|null|string + */ + public function getCover() + { + if (empty($this->coverProperties)) { + return null; + } + + $fp = fopen($this->_fileName, 'rb'); + if ($fp === false) { + return false; + } + fseek($fp, $this->coverProperties['offset']); + $data = fread($fp, $this->coverProperties['size']); + fclose($fp); + return $data; + } + + /** + * Reads audio file in binary mode. + * mpeg audio file structure: + * ID3V2 TAG - provides a lot of meta data. [optional] + * MPEG AUDIO FRAMES - contains audio data. A frame consists of a frame header and a frame data. The first frame may contain extra information about mp3 (marked with "Xing" or "Info" string). Rest of frames can contain only audio data. + * ID3V1 TAG - provides a few of meta data. [optional] + * @param string $filename + * @param int $fileSize + * @param int $mode + * @return float|int + * @throws \Exception + */ + private function parseAudio($filename, $fileSize, $mode) { + $time = microtime(true); + + // create temp storage for media + if (strpos($filename, '://') !== false) { + $fp = fopen('php://memory', 'rwb'); + fwrite($fp, file_get_contents($filename)); + rewind($fp); + } else { + $fp = fopen($filename, 'rb'); + } + + /** @var int Size of audio data (exclude tags size) */ + $audioSize = $fileSize; + + // parse tags + if (fread($fp, 3) == self::TAG2_SYNC) { + if ($mode & self::TAGS) $audioSize -= ($this->_id3Size = $this->readId3v2Body($fp)); + else { + fseek($fp, 2, SEEK_CUR); // 2 bytes of tag version + fseek($fp, 1, SEEK_CUR); // 1 byte of tag flags + $sizeBytes = $this->readBytes($fp, 4); + array_walk($sizeBytes, function (&$value) { + $value = substr(str_pad(base_convert($value, 10, 2), 8, 0, STR_PAD_LEFT), 1); + }); + $size = bindec(implode($sizeBytes)) + 10; + $audioSize -= ($this->_id3Size = $size); + } + } + + fseek($fp, $fileSize - 128); + if (fread($fp, 3) == self::TAG1_SYNC) { + if ($mode & self::TAGS) $audioSize -= $this->readId3v1Body($fp); + else $audioSize -= 128; + } + + if ($mode & self::TAGS) { + $this->fillTags(); + } + + fseek($fp, 0); + // audio meta + if ($mode & self::META) { + if ($this->_id3Size !== null) fseek($fp, $this->_id3Size); + /** + * First frame can lie. Need to fix in the future. + * @link https://github.com/wapmorgan/Mp3Info/issues/13#issuecomment-447470813 + * Read first N frames + */ + for ($i = 0; $i < self::$framesCountRead; $i++) { + $framesCount = $this->readMpegFrame($fp); + } + + $this->_framesCount = $framesCount !== null + ? $framesCount + : ceil($audioSize / $this->_cbrFrameSize); + + // recalculate average bit rate in vbr case + if ($this->isVbr && $framesCount !== null) { + $avgFrameSize = $audioSize / $framesCount; + $this->bitRate = $avgFrameSize * $this->sampleRate / (1000 * $this->layerVersion == self::LAYER_3 ? 12 : 144); + } + + // The faster way to detect audio duration: + $samples_in_second = $this->layerVersion == 1 ? self::LAYER_1_FRAME_SIZE : self::LAYERS_23_FRAME_SIZE; + // for VBR: adjust samples in second according to VBR quality + // disabled for now +// if ($this->isVbr && isset($this->vbrProperties['quality'])) { +// $samples_in_second = floor($samples_in_second * $this->vbrProperties['quality'] / 100); +// } + // Calculate total number of audio samples (framesCount * sampleInFrameCount) / samplesInSecondCount + $this->duration = ($this->_framesCount - 1) * $samples_in_second / $this->sampleRate; + } + fclose($fp); + + $this->_parsingTime = microtime(true) - $time; + return $audioSize; + } + + /** + * Read first frame information. + * @param resource $fp + * @return int Number of frames (if present if first frame of VBR-file) + * @throws \Exception + */ + private function readMpegFrame($fp) { + $header_seek_pos = ftell($fp) + self::$headerSeekLimit; + do { + $pos = ftell($fp); + $first_header_byte = $this->readBytes($fp, 1); + if ($first_header_byte[0] === 0xFF) { + $second_header_byte = $this->readBytes($fp, 1); + if ((($second_header_byte[0] >> 5) & 0b111) == 0b111) { + fseek($fp, $pos); + $header_bytes = $this->readBytes($fp, 4); + break; + } + } + } while (ftell($fp) <= $header_seek_pos); + + if (!isset($header_bytes) || $header_bytes[0] !== 0xFF || (($header_bytes[1] >> 5) & 0b111) != 0b111) { + throw new \Exception('At '.$pos + .'(0x'.dechex($pos).') should be a frame header!'); + } + + switch ($header_bytes[1] >> 3 & 0b11) { + case 0b00: $this->codecVersion = self::MPEG_25; break; + case 0b10: $this->codecVersion = self::MPEG_2; break; + case 0b11: $this->codecVersion = self::MPEG_1; break; + } + + switch ($header_bytes[1] >> 1 & 0b11) { + case 0b01: $this->layerVersion = self::LAYER_3; break; + case 0b10: $this->layerVersion = self::LAYER_2; break; + case 0b11: $this->layerVersion = self::LAYER_1; break; + } + + if (!isset($this->codecVersion) || !isset($this->layerVersion) || !isset($header_bytes[2])) { + throw new \Exception('Unrecognized codecVersion or layerVersion headers!'); + } + $this->bitRate = self::$_bitRateTable[$this->codecVersion][$this->layerVersion][$header_bytes[2] >> 4]; + $this->sampleRate = self::$_sampleRateTable[$this->codecVersion][($header_bytes[2] >> 2) & 0b11]; + + switch ($header_bytes[3] >> 6) { + case 0b00: $this->channel = self::STEREO; break; + case 0b01: $this->channel = self::JOINT_STEREO; break; + case 0b10: $this->channel = self::DUAL_MONO; break; + case 0b11: $this->channel = self::MONO; break; + } + + if (!isset($this->channel)) { + throw new \Exception('Unrecognized channel header!'); + } + $vbr_offset = self::$_vbrOffsets[$this->codecVersion][$this->channel == self::MONO ? 0 : 1]; + + // check for VBR + fseek($fp, $pos + $vbr_offset); + if (fread($fp, 4) == self::VBR_SYNC) { + $this->isVbr = true; + $flagsBytes = $this->readBytes($fp, 4); + + // VBR frames count presence + if (($flagsBytes[3] & 2)) { + $this->vbrProperties['frames'] = implode(unpack('N', fread($fp, 4))); + } + // VBR stream size presence + if ($flagsBytes[3] & 4) { + $this->vbrProperties['bytes'] = implode(unpack('N', fread($fp, 4))); + } + // VBR TOC presence + if ($flagsBytes[3] & 1) { + fseek($fp, 100, SEEK_CUR); + } + // VBR quality + if ($flagsBytes[3] & 8) { + $this->vbrProperties['quality'] = implode(unpack('N', fread($fp, 4))); + } + } + + // go to the end of frame + if ($this->layerVersion == self::LAYER_1) { + $this->_cbrFrameSize = floor((12 * $this->bitRate / $this->sampleRate + ($header_bytes[2] >> 1 & 0b1)) * 4); + } else { + $this->_cbrFrameSize = floor(144 * $this->bitRate / $this->sampleRate + ($header_bytes[2] >> 1 & 0b1)); + } + + fseek($fp, $pos + $this->_cbrFrameSize); + + return isset($this->vbrProperties['frames']) ? $this->vbrProperties['frames'] : null; + } + + /** + * @param $fp + * @param $n + * + * @return array + * @throws \Exception + */ + private function readBytes($fp, $n) { + $raw = fread($fp, $n); + if (strlen($raw) !== $n) throw new \Exception('Unexpected end of file!'); + $bytes = array(); + for($i = 0; $i < $n; $i++) $bytes[$i] = ord($raw[$i]); + return $bytes; + } + + /** + * Reads id3v1 tag. + * @return int Returns length of id3v1 tag. + */ + private function readId3v1Body($fp) { + $this->tags1['song'] = trim(fread($fp, 30)); + $this->tags1['artist'] = trim(fread($fp, 30)); + $this->tags1['album'] = trim(fread($fp, 30)); + $this->tags1['year'] = trim(fread($fp, 4)); + $this->tags1['comment'] = trim(fread($fp, 28)); + fseek($fp, 1, SEEK_CUR); + $this->tags1['track'] = ord(fread($fp, 1)); + $this->tags1['genre'] = ord(fread($fp, 1)); + return 128; + } + + /** + * Reads id3v2 tag. + * ----------------------------------- + * Overall tag header structure (10 bytes) + * ID3v2/file identifier "ID3" (3 bytes) + * ID3v2 version (2 bytes) + * ID3v2 flags (1 byte) + * ID3v2 size 4 * %0xxxxxxx (4 bytes) + * ----------------------------------- + * id3v2.2.0 tag header (10 bytes) + * ID3/file identifier "ID3" (3 bytes) + * ID3 version $02 00 (2 bytes) + * ID3 flags %xx000000 (1 byte) + * ID3 size 4 * %0xxxxxxx (4 bytes) + * Flags: + * x (bit 7) - unsynchronisation + * x (bit 6) - compression + * ----------------------------------- + * id3v2.3.0 tag header (10 bytes) + * ID3v2/file identifier "ID3" (3 bytes) + * ID3v2 version $03 00 (2 bytes) + * ID3v2 flags %abc00000 (1 byte) + * ID3v2 size 4 * %0xxxxxxx (4 bytes) + * Flags: + * a - Unsynchronisation + * b - Extended header + * c - Experimental indicator + * Extended header structure (10 bytes) + * Extended header size $xx xx xx xx + * Extended Flags $xx xx + * Size of padding $xx xx xx xx + * Extended flags: + * %x0000000 00000000 + * x - CRC data present + * ----------------------------------- + * id3v2.4.0 tag header (10 bytes) + * ID3v2/file identifier "ID3" (3 bytes) + * ID3v2 version $04 00 (2 bytes) + * ID3v2 flags %abcd0000 (1 byte) + * ID3v2 size 4 * %0xxxxxxx (4 bytes) + * Flags: + * a - Unsynchronisation + * b - Extended header + * c - Experimental indicator + * d - Footer present + * @param resource $fp + * @return int Returns length of id3v2 tag. + * @throws \Exception + */ + private function readId3v2Body($fp) + { + // read the rest of the id3v2 header + $raw = fread($fp, 7); + $data = unpack('cmajor_version/cminor_version/H*', $raw); + $this->id3v2MajorVersion = $data['major_version']; + $this->id3v2MinorVersion = $data['minor_version']; + $data = str_pad(base_convert($data[1], 16, 2), 40, 0, STR_PAD_LEFT); + $flags = substr($data, 0, 8); + if ($this->id3v2MajorVersion == 2) { // parse id3v2.2.0 header flags + $this->id3v2Flags = array( + 'unsynchronisation' => (bool)substr($flags, 0, 1), + 'compression' => (bool)substr($flags, 1, 1), + ); + } else if ($this->id3v2MajorVersion == 3) { // parse id3v2.3.0 header flags + $this->id3v2Flags = array( + 'unsynchronisation' => (bool)substr($flags, 0, 1), + 'extended_header' => (bool)substr($flags, 1, 1), + 'experimental_indicator' => (bool)substr($flags, 2, 1), + ); + if ($this->id3v2Flags['extended_header']) + throw new \Exception('NEED TO PARSE EXTENDED HEADER!'); + } else if ($this->id3v2MajorVersion == 4) { // parse id3v2.4.0 header flags + $this->id3v2Flags = array( + 'unsynchronisation' => (bool)substr($flags, 0, 1), + 'extended_header' => (bool)substr($flags, 1, 1), + 'experimental_indicator' => (bool)substr($flags, 2, 1), + 'footer_present' => (bool)substr($flags, 3, 1), + ); + if ($this->id3v2Flags['extended_header']) + throw new \Exception('NEED TO PARSE EXTENDED HEADER!'); + if ($this->id3v2Flags['footer_present']) + throw new \Exception('NEED TO PARSE id3v2.4 FOOTER!'); + } + $size = substr($data, 8, 32); + + // some fucking shit + // getting only 7 of 8 bits of size bytes + $sizes = str_split($size, 8); + array_walk($sizes, function (&$value) { + $value = substr($value, 1); + }); + $size = implode($sizes); + $size = bindec($size); + + if ($this->id3v2MajorVersion == 2) { + // parse id3v2.2.0 body + /*throw new \Exception('NEED TO PARSE id3v2.2.0 flags!');*/ + } else if ($this->id3v2MajorVersion == 3) { + // parse id3v2.3.0 body + $this->parseId3v23Body($fp, 10 + $size); + } else if ($this->id3v2MajorVersion == 4) { + // parse id3v2.4.0 body + $this->parseId3v24Body($fp, 10 + $size); + } + + return 10 + $size; // 10 bytes - header, rest - body + } + + /** + * Parses id3v2.3.0 tag body. + * @todo Complete. + */ + protected function parseId3v23Body($fp, $lastByte) { + while (ftell($fp) < $lastByte) { + $raw = fread($fp, 10); + $frame_id = substr($raw, 0, 4); + + if (strlen($raw) < 10) { + fseek($fp, $lastByte); + break; + } + + if ($frame_id == str_repeat(chr(0), 4)) { + fseek($fp, $lastByte); + break; + } + + $data = unpack('Nframe_size/H2flags', substr($raw, 4)); + $frame_size = $data['frame_size']; + $flags = base_convert($data['flags'], 16, 2); + $this->id3v2TagsFlags[$frame_id] = array( + 'flags' => array( + 'tag_alter_preservation' => (bool)substr($flags, 0, 1), + 'file_alter_preservation' => (bool)substr($flags, 1, 1), + 'read_only' => (bool)substr($flags, 2, 1), + 'compression' => (bool)substr($flags, 8, 1), + 'encryption' => (bool)substr($flags, 9, 1), + 'grouping_identity' => (bool)substr($flags, 10, 1), + ), + ); + + switch ($frame_id) { + // case 'UFID': # Unique file identifier + // break; + + ################# Text information frames + case 'TALB': # Album/Movie/Show title + case 'TCON': # Content type + case 'TYER': # Year + case 'TXXX': # User defined text information frame + case 'TRCK': # Track number/Position in set + case 'TIT2': # Title/songname/content description + case 'TPE1': # Lead performer(s)/Soloist(s) + case 'TBPM': # BPM (beats per minute) + case 'TCOM': # Composer + case 'TCOP': # Copyright message + case 'TDAT': # Date + case 'TDLY': # Playlist delay + case 'TENC': # Encoded by + case 'TEXT': # Lyricist/Text writer + case 'TFLT': # File type + case 'TIME': # Time + case 'TIT1': # Content group description + case 'TIT3': # Subtitle/Description refinement + case 'TKEY': # Initial key + case 'TLAN': # Language(s) + case 'TLEN': # Length + case 'TMED': # Media type + case 'TOAL': # Original album/movie/show title + case 'TOFN': # Original filename + case 'TOLY': # Original lyricist(s)/text writer(s) + case 'TOPE': # Original artist(s)/performer(s) + case 'TORY': # Original release year + case 'TOWN': # File owner/licensee + case 'TPE2': # Band/orchestra/accompaniment + case 'TPE3': # Conductor/performer refinement + case 'TPE4': # Interpreted, remixed, or otherwise modified by + case 'TPOS': # Part of a set + case 'TPUB': # Publisher + case 'TRDA': # Recording dates + case 'TRSN': # Internet radio station name + case 'TRSO': # Internet radio station owner + case 'TSIZ': # Size + case 'TSRC': # ISRC (international standard recording code) + case 'TSSE': # Software/Hardware and settings used for encoding + $this->tags2[$frame_id] = $this->handleTextFrame($frame_size, fread($fp, $frame_size)); + break; + ################# Text information frames + + ################# URL link frames + // case 'WCOM': # Commercial information + // break; + // case 'WCOP': # Copyright/Legal information + // break; + // case 'WOAF': # Official audio file webpage + // break; + // case 'WOAR': # Official artist/performer webpage + // break; + // case 'WOAS': # Official audio source webpage + // break; + // case 'WORS': # Official internet radio station homepage + // break; + // case 'WPAY': # Payment + // break; + // case 'WPUB': # Publishers official webpage + // break; + // case 'WXXX': # User defined URL link frame + // break; + ################# URL link frames + + // case 'IPLS': # Involved people list + // break; + // case 'MCDI': # Music CD identifier + // break; + // case 'ETCO': # Event timing codes + // break; + // case 'MLLT': # MPEG location lookup table + // break; + // case 'SYTC': # Synchronized tempo codes + // break; + // case 'USLT': # Unsychronized lyric/text transcription + // break; + // case 'SYLT': # Synchronized lyric/text + // break; + case 'COMM': # Comments + $dataEnd = ftell($fp) + $frame_size; + $raw = fread($fp, 4); + $data = unpack('C1encoding/A3language', $raw); + // read until \null character + $short_description = ''; + $last_null = false; + $actual_text = false; + while (ftell($fp) < $dataEnd) { + $char = fgetc($fp); + if ($char == "\00" && $actual_text === false) { + if ($data['encoding'] == 0x1) { # two null-bytes for utf-16 + if ($last_null) + $actual_text = null; + else + $last_null = true; + } else # no condition for iso-8859-1 + $actual_text = null; + + } + else if ($actual_text !== false) $actual_text .= $char; + else $short_description .= $char; + } + if ($actual_text === false) $actual_text = $short_description; + // list($short_description, $actual_text) = sscanf("s".chr(0)."s", $data['texts']); + // list($short_description, $actual_text) = explode(chr(0), $data['texts']); + $this->tags2[$frame_id][$data['language']] = array( + 'short' => (bool)($data['encoding'] == 0x00) ? mb_convert_encoding($short_description, 'utf-8', 'iso-8859-1') : mb_convert_encoding($short_description, 'utf-8', 'utf-16'), + 'actual' => (bool)($data['encoding'] == 0x00) ? mb_convert_encoding($actual_text, 'utf-8', 'iso-8859-1') : mb_convert_encoding($actual_text, 'utf-8', 'utf-16'), + ); + break; + // case 'RVAD': # Relative volume adjustment + // break; + // case 'EQUA': # Equalization + // break; + // case 'RVRB': # Reverb + // break; + case 'APIC': # Attached picture + $this->hasCover = true; + $last_byte = ftell($fp) + $frame_size; + $this->coverProperties = ['text_encoding' => ord(fread($fp, 1))]; +// fseek($fp, $frame_size - 4, SEEK_CUR); + $this->coverProperties['mime_type'] = $this->readTextUntilNull($fp, $last_byte); + $this->coverProperties['picture_type'] = ord(fread($fp, 1)); + $this->coverProperties['description'] = $this->readTextUntilNull($fp, $last_byte); + $this->coverProperties['offset'] = ftell($fp); + $this->coverProperties['size'] = $last_byte - ftell($fp); + fseek($fp, $last_byte); + break; + // case 'GEOB': # General encapsulated object + // break; + case 'PCNT': # Play counter + $data = unpack('L', fread($fp, $frame_size)); + $this->tags2[$frame_id] = $data[1]; + break; + // case 'POPM': # Popularimeter + // break; + // case 'RBUF': # Recommended buffer size + // break; + // case 'AENC': # Audio encryption + // break; + // case 'LINK': # Linked information + // break; + // case 'POSS': # Position synchronisation frame + // break; + // case 'USER': # Terms of use + // break; + // case 'OWNE': # Ownership frame + // break; + // case 'COMR': # Commercial frame + // break; + // case 'ENCR': # Encryption method registration + // break; + // case 'GRID': # Group identification registration + // break; + // case 'PRIV': # Private frame + // break; + default: + fseek($fp, $frame_size, SEEK_CUR); + break; + } + } + } + + /** + * Parses id3v2.4.0 tag body. + * @param $fp + * @param $lastByte + */ + protected function parseId3v24Body($fp, $lastByte) + { + while (ftell($fp) < $lastByte) { + $raw = fread($fp, 10); + $frame_id = substr($raw, 0, 4); + + if ($frame_id == str_repeat(chr(0), 4)) { + fseek($fp, $lastByte); + break; + } + + $data = unpack('Nframe_size/H2flags', substr($raw, 4)); + $frame_size = $data['frame_size']; + $flags = base_convert($data['flags'], 16, 2); + $this->id3v2TagsFlags[$frame_id] = array( + 'flags' => array( + 'tag_alter_preservation' => (bool)substr($flags, 1, 1), + 'file_alter_preservation' => (bool)substr($flags, 2, 1), + 'read_only' => (bool)substr($flags, 3, 1), + 'grouping_identity' => (bool)substr($flags, 9, 1), + 'compression' => (bool)substr($flags, 12, 1), + 'encryption' => (bool)substr($flags, 13, 1), + 'unsynchronisation' => (bool)substr($flags, 14, 1), + 'data_length_indicator' => (bool)substr($flags, 15, 1), + ), + ); + + switch ($frame_id) { + // case 'UFID': # Unique file identifier + // break; + + ################# Text information frames + case 'TALB': # Album/Movie/Show title + case 'TCON': # Content type + case 'TYER': # Year + case 'TXXX': # User defined text information frame + case 'TRCK': # Track number/Position in set + case 'TIT2': # Title/songname/content description + case 'TPE1': # Lead performer(s)/Soloist(s) + case 'TBPM': # BPM (beats per minute) + case 'TCOM': # Composer + case 'TCOP': # Copyright message + case 'TDAT': # Date + case 'TDLY': # Playlist delay + case 'TENC': # Encoded by + case 'TEXT': # Lyricist/Text writer + case 'TFLT': # File type + case 'TIME': # Time + case 'TIT1': # Content group description + case 'TIT3': # Subtitle/Description refinement + case 'TKEY': # Initial key + case 'TLAN': # Language(s) + case 'TLEN': # Length + case 'TMED': # Media type + case 'TOAL': # Original album/movie/show title + case 'TOFN': # Original filename + case 'TOLY': # Original lyricist(s)/text writer(s) + case 'TOPE': # Original artist(s)/performer(s) + case 'TORY': # Original release year + case 'TOWN': # File owner/licensee + case 'TPE2': # Band/orchestra/accompaniment + case 'TPE3': # Conductor/performer refinement + case 'TPE4': # Interpreted, remixed, or otherwise modified by + case 'TPOS': # Part of a set + case 'TPUB': # Publisher + case 'TRDA': # Recording dates + case 'TRSN': # Internet radio station name + case 'TRSO': # Internet radio station owner + case 'TSIZ': # Size + case 'TSRC': # ISRC (international standard recording code) + case 'TSSE': # Software/Hardware and settings used for encoding + $this->tags2[$frame_id] = $this->handleTextFrame($frame_size, fread($fp, $frame_size)); + break; + + ################# Text information frames + + ################# URL link frames + // case 'WCOM': # Commercial information + // break; + // case 'WCOP': # Copyright/Legal information + // break; + // case 'WOAF': # Official audio file webpage + // break; + // case 'WOAR': # Official artist/performer webpage + // break; + // case 'WOAS': # Official audio source webpage + // break; + // case 'WORS': # Official internet radio station homepage + // break; + // case 'WPAY': # Payment + // break; + // case 'WPUB': # Publishers official webpage + // break; + // case 'WXXX': # User defined URL link frame + // break; + ################# URL link frames + + // case 'IPLS': # Involved people list + // break; + // case 'MCDI': # Music CD identifier + // break; + // case 'ETCO': # Event timing codes + // break; + // case 'MLLT': # MPEG location lookup table + // break; + // case 'SYTC': # Synchronized tempo codes + // break; + // case 'USLT': # Unsychronized lyric/text transcription + // break; + // case 'SYLT': # Synchronized lyric/text + // break; + case 'COMM': # Comments + $dataEnd = ftell($fp) + $frame_size; + $raw = fread($fp, 4); + $data = unpack('C1encoding/A3language', $raw); + // read until \null character + $short_description = null; + $last_null = false; + $actual_text = false; + while (ftell($fp) < $dataEnd) { + $char = fgetc($fp); + if ($char == "\00" && $actual_text === false) { + if ($data['encoding'] == 0x1) { # two null-bytes for utf-16 + if ($last_null) + $actual_text = null; + else + $last_null = true; + } else # no condition for iso-8859-1 + $actual_text = null; + + } + else if ($actual_text !== false) $actual_text .= $char; + else $short_description .= $char; + } + if ($actual_text === false) $actual_text = $short_description; + // list($short_description, $actual_text) = sscanf("s".chr(0)."s", $data['texts']); + // list($short_description, $actual_text) = explode(chr(0), $data['texts']); + $this->tags2[$frame_id][$data['language']] = array( + 'short' => (bool)($data['encoding'] == 0x00) ? mb_convert_encoding($short_description, 'utf-8', 'iso-8859-1') : mb_convert_encoding($short_description, 'utf-8', 'utf-16'), + 'actual' => (bool)($data['encoding'] == 0x00) ? mb_convert_encoding($actual_text, 'utf-8', 'iso-8859-1') : mb_convert_encoding($actual_text, 'utf-8', 'utf-16'), + ); + break; + // case 'RVAD': # Relative volume adjustment + // break; + // case 'EQUA': # Equalization + // break; + // case 'RVRB': # Reverb + // break; + case 'APIC': # Attached picture + $this->hasCover = true; + $last_byte = ftell($fp) + $frame_size; + $this->coverProperties = ['text_encoding' => ord(fread($fp, 1))]; +// fseek($fp, $frame_size - 4, SEEK_CUR); + $this->coverProperties['mime_type'] = $this->readTextUntilNull($fp, $last_byte); + $this->coverProperties['picture_type'] = ord(fread($fp, 1)); + $this->coverProperties['description'] = $this->readTextUntilNull($fp, $last_byte); + $this->coverProperties['offset'] = ftell($fp); + $this->coverProperties['size'] = $last_byte - ftell($fp); + fseek($fp, $last_byte); + break; + // case 'GEOB': # General encapsulated object + // break; + case 'PCNT': # Play counter + $data = unpack('L', fread($fp, $frame_size)); + $this->tags2[$frame_id] = $data[1]; + break; + // case 'POPM': # Popularimeter + // break; + // case 'RBUF': # Recommended buffer size + // break; + // case 'AENC': # Audio encryption + // break; + // case 'LINK': # Linked information + // break; + // case 'POSS': # Position synchronisation frame + // break; + // case 'USER': # Terms of use + // break; + // case 'OWNE': # Ownership frame + // break; + // case 'COMR': # Commercial frame + // break; + // case 'ENCR': # Encryption method registration + // break; + // case 'GRID': # Group identification registration + // break; + // case 'PRIV': # Private frame + // break; + default: + fseek($fp, $frame_size, SEEK_CUR); + break; + } + } + } + + /** + * @param $frameSize + * @param $raw + * + * @return string + */ + private function handleTextFrame($frameSize, $raw) + { + $data = unpack('C1encoding/A' . ($frameSize - 1) . 'information', $raw); + + switch($data['encoding']) { + case 0x00: # ISO-8859-1 + return mb_convert_encoding($data['information'], 'utf-8', 'iso-8859-1'); + case 0x01: # utf-16 with BOM + return mb_convert_encoding($data['information'] . "\00", 'utf-8', 'utf-16'); + + # Following is for id3v2.4.x only + case 0x02: # utf-16 without BOM + return mb_convert_encoding($data['information'] . "\00", 'utf-8', 'utf-16'); + case 0x03: # utf-8 + return $data['information']; + + default: + throw new RuntimeException('Unknown text encoding type: '.$data['encoding']); + } + } + + /** + * @param resource $fp + * @param int $dataEnd + * @return string|null + */ + private function readTextUntilNull($fp, $dataEnd) + { + $text = null; + while (ftell($fp) < $dataEnd) { + $char = fgetc($fp); + if ($char === "\00") { + return $text; + } + $text .= $char; + } + return $text; + } + + /** + * Fills `tags` property with values id3v2 and id3v1 tags. + */ + protected function fillTags() + { + foreach ([ + 'song' => 'TIT2', + 'artist' => 'TPE1', + 'album' => 'TALB', + 'year' => 'TYER', + 'comment' => 'COMM', + 'track' => 'TRCK', + 'genre' => 'TCON', + ] as $tag => $id3v2_tag) { + if (!isset($this->tags2[$id3v2_tag]) && (!isset($this->tags1[$tag]) || empty($this->tags1[$tag]))) + continue; + + $this->tags[$tag] = isset($this->tags2[$id3v2_tag]) + ? ($id3v2_tag === 'COMM' ? current($this->tags2[$id3v2_tag])['actual'] : $this->tags2[$id3v2_tag]) + : $this->tags1[$tag]; + } + } + + /** + * Simple function that checks mpeg-audio correctness of given file. + * Actually it checks that first 3 bytes of file is a id3v2 tag mark or + * that first 11 bits of file is a frame header sync mark or that 3 bytes on -128 position of file is id3v1 tag. + * To perform full test create an instance of Mp3Info with given file. + * + * @param string $filename File to be tested. + * @return boolean True if file looks that correct mpeg audio, False otherwise. + * @throws \Exception + */ + public static function isValidAudio($filename) { + if (!file_exists($filename) && strpos($filename, '://') == false) { + throw new Exception('File ' . $filename . ' is not present!'); + } + + $filesize = file_exists($filename) ? filesize($filename) : static::getUrlContentLength($filename); + + $raw = file_get_contents($filename, false, null, 0, 3); + return $raw === self::TAG2_SYNC // id3v2 tag + || (self::FRAME_SYNC === (unpack('n*', $raw)[1] & self::FRAME_SYNC)) // mpeg header tag + || ( + $filesize > 128 + && file_get_contents($filename, false, null, -128, 3) === self::TAG1_SYNC + ) // id3v1 tag + ; + } + + /** + * @param string $url + * @return int|mixed|string + */ + public static function getUrlContentLength($url) { + $context = stream_context_create(['http' => ['method' => 'HEAD']]); + $head = array_change_key_case(get_headers($url, true, $context)); + // content-length of download (in bytes), read from Content-Length: field + $clen = isset($head['content-length']) ? $head['content-length'] : 0; + + // cannot retrieve file size, return "-1" + if (!$clen) { + return -1; + } + + return $clen; // return size in bytes + } +} diff --git a/3rdparty/web-auth/cose-lib/LICENSE b/3rdparty/web-auth/cose-lib/LICENSE new file mode 100644 index 00000000..25cfdd66 --- /dev/null +++ b/3rdparty/web-auth/cose-lib/LICENSE @@ -0,0 +1,21 @@ +MIT License + +Copyright (c) 2018 Spomky-Labs + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. diff --git a/3rdparty/web-auth/cose-lib/src/Algorithm/Algorithm.php b/3rdparty/web-auth/cose-lib/src/Algorithm/Algorithm.php new file mode 100644 index 00000000..8c6083df --- /dev/null +++ b/3rdparty/web-auth/cose-lib/src/Algorithm/Algorithm.php @@ -0,0 +1,10 @@ +checKey($key); + $signature = hash_hmac($this->getHashAlgorithm(), $data, (string) $key->get(SymmetricKey::DATA_K), true); + + return mb_substr($signature, 0, intdiv($this->getSignatureLength(), 8), '8bit'); + } + + public function verify(string $data, Key $key, string $signature): bool + { + return hash_equals($this->hash($data, $key), $signature); + } + + abstract protected function getHashAlgorithm(): string; + + abstract protected function getSignatureLength(): int; + + private function checKey(Key $key): void + { + if ($key->type() !== Key::TYPE_OCT && $key->type() !== Key::TYPE_NAME_OCT) { + throw new InvalidArgumentException('Invalid key. Must be of type symmetric'); + } + + if (! $key->has(SymmetricKey::DATA_K)) { + throw new InvalidArgumentException('Invalid key. The value of the key is missing'); + } + } +} diff --git a/3rdparty/web-auth/cose-lib/src/Algorithm/Mac/Mac.php b/3rdparty/web-auth/cose-lib/src/Algorithm/Mac/Mac.php new file mode 100644 index 00000000..46ad3222 --- /dev/null +++ b/3rdparty/web-auth/cose-lib/src/Algorithm/Mac/Mac.php @@ -0,0 +1,15 @@ + + */ + private array $algorithms = []; + + public static function create(): self + { + return new self(); + } + + public function add(Algorithm ...$algorithms): self + { + foreach ($algorithms as $algorithm) { + $identifier = $algorithm::identifier(); + $this->algorithms[$identifier] = $algorithm; + } + + return $this; + } + + /** + * @return iterable + */ + public function list(): iterable + { + yield from array_keys($this->algorithms); + } + + /** + * @return iterable + */ + public function all(): iterable + { + yield from $this->algorithms; + } + + public function has(int $identifier): bool + { + return array_key_exists($identifier, $this->algorithms); + } + + public function get(int $identifier): Algorithm + { + if (! $this->has($identifier)) { + throw new InvalidArgumentException('Unsupported algorithm'); + } + + return $this->algorithms[$identifier]; + } +} diff --git a/3rdparty/web-auth/cose-lib/src/Algorithm/ManagerFactory.php b/3rdparty/web-auth/cose-lib/src/Algorithm/ManagerFactory.php new file mode 100644 index 00000000..548e0a72 --- /dev/null +++ b/3rdparty/web-auth/cose-lib/src/Algorithm/ManagerFactory.php @@ -0,0 +1,57 @@ + + */ + private array $algorithms = []; + + public static function create(): self + { + return new self(); + } + + public function add(string $alias, Algorithm $algorithm): self + { + $this->algorithms[$alias] = $algorithm; + + return $this; + } + + /** + * @return string[] + */ + public function list(): iterable + { + yield from array_keys($this->algorithms); + } + + /** + * @return Algorithm[] + */ + public function all(): iterable + { + yield from $this->algorithms; + } + + public function generate(string ...$aliases): Manager + { + $manager = Manager::create(); + foreach ($aliases as $alias) { + if (! array_key_exists($alias, $this->algorithms)) { + throw new InvalidArgumentException(sprintf('The algorithm with alias "%s" is not supported', $alias)); + } + $manager->add($this->algorithms[$alias]); + } + + return $manager; + } +} diff --git a/3rdparty/web-auth/cose-lib/src/Algorithm/Signature/ECDSA/ECDSA.php b/3rdparty/web-auth/cose-lib/src/Algorithm/Signature/ECDSA/ECDSA.php new file mode 100644 index 00000000..dc78ac76 --- /dev/null +++ b/3rdparty/web-auth/cose-lib/src/Algorithm/Signature/ECDSA/ECDSA.php @@ -0,0 +1,50 @@ +handleKey($key); + openssl_sign($data, $signature, $key->asPEM(), $this->getHashAlgorithm()); + + return ECSignature::fromAsn1($signature, $this->getSignaturePartLength()); + } + + public function verify(string $data, Key $key, string $signature): bool + { + $key = $this->handleKey($key); + $publicKey = $key->toPublic(); + $signature = ECSignature::toAsn1($signature, $this->getSignaturePartLength()); + return openssl_verify($data, $signature, $publicKey->asPEM(), $this->getHashAlgorithm()) === 1; + } + + abstract protected function getCurve(): int; + + abstract protected function getHashAlgorithm(): int; + + abstract protected function getSignaturePartLength(): int; + + private function handleKey(Key $key): Ec2Key + { + $key = Ec2Key::create($key->getData()); + if ($key->curve() !== $this->getCurve()) { + throw new InvalidArgumentException('This key cannot be used with this algorithm'); + } + + return $key; + } +} diff --git a/3rdparty/web-auth/cose-lib/src/Algorithm/Signature/ECDSA/ECSignature.php b/3rdparty/web-auth/cose-lib/src/Algorithm/Signature/ECDSA/ECSignature.php new file mode 100644 index 00000000..43547001 --- /dev/null +++ b/3rdparty/web-auth/cose-lib/src/Algorithm/Signature/ECDSA/ECSignature.php @@ -0,0 +1,132 @@ + self::ASN1_MAX_SINGLE_BYTE ? self::ASN1_LENGTH_2BYTES : ''; + + return hex2bin( + self::ASN1_SEQUENCE + . $lengthPrefix . dechex($totalLength) + . self::ASN1_INTEGER . dechex($lengthR) . $pointR + . self::ASN1_INTEGER . dechex($lengthS) . $pointS + ); + } + + public static function fromAsn1(string $signature, int $length): string + { + $message = bin2hex($signature); + $position = 0; + + if (self::readAsn1Content($message, $position, self::BYTE_SIZE) !== self::ASN1_SEQUENCE) { + throw new InvalidArgumentException('Invalid data. Should start with a sequence.'); + } + + // @phpstan-ignore-next-line + if (self::readAsn1Content($message, $position, self::BYTE_SIZE) === self::ASN1_LENGTH_2BYTES) { + $position += self::BYTE_SIZE; + } + + $pointR = self::retrievePositiveInteger(self::readAsn1Integer($message, $position)); + $pointS = self::retrievePositiveInteger(self::readAsn1Integer($message, $position)); + + return hex2bin(str_pad($pointR, $length, '0', STR_PAD_LEFT) . str_pad($pointS, $length, '0', STR_PAD_LEFT)); + } + + private static function octetLength(string $data): int + { + return intdiv(mb_strlen($data, '8bit'), self::BYTE_SIZE); + } + + private static function preparePositiveInteger(string $data): string + { + if (mb_substr($data, 0, self::BYTE_SIZE, '8bit') > self::ASN1_BIG_INTEGER_LIMIT) { + return self::ASN1_NEGATIVE_INTEGER . $data; + } + + while ( + mb_strpos($data, self::ASN1_NEGATIVE_INTEGER, 0, '8bit') === 0 + && mb_substr($data, 2, self::BYTE_SIZE, '8bit') <= self::ASN1_BIG_INTEGER_LIMIT + ) { + $data = mb_substr($data, 2, null, '8bit'); + } + + return $data; + } + + private static function readAsn1Content(string $message, int &$position, int $length): string + { + $content = mb_substr($message, $position, $length, '8bit'); + $position += $length; + + return $content; + } + + private static function readAsn1Integer(string $message, int &$position): string + { + if (self::readAsn1Content($message, $position, self::BYTE_SIZE) !== self::ASN1_INTEGER) { + throw new InvalidArgumentException('Invalid data. Should contain an integer.'); + } + + $length = (int) hexdec(self::readAsn1Content($message, $position, self::BYTE_SIZE)); + + return self::readAsn1Content($message, $position, $length * self::BYTE_SIZE); + } + + private static function retrievePositiveInteger(string $data): string + { + while ( + mb_strpos($data, self::ASN1_NEGATIVE_INTEGER, 0, '8bit') === 0 + && mb_substr($data, 2, self::BYTE_SIZE, '8bit') > self::ASN1_BIG_INTEGER_LIMIT + ) { + $data = mb_substr($data, 2, null, '8bit'); + } + + return $data; + } +} diff --git a/3rdparty/web-auth/cose-lib/src/Algorithm/Signature/ECDSA/ES256.php b/3rdparty/web-auth/cose-lib/src/Algorithm/Signature/ECDSA/ES256.php new file mode 100644 index 00000000..a5b97dac --- /dev/null +++ b/3rdparty/web-auth/cose-lib/src/Algorithm/Signature/ECDSA/ES256.php @@ -0,0 +1,38 @@ +handleKey($key); + if (! $key->isPrivate()) { + throw new InvalidArgumentException('The key is not private.'); + } + + $x = $key->x(); + $d = $key->d(); + $secret = $d . $x; + + return match ($key->curve()) { + OkpKey::CURVE_ED25519 => sodium_crypto_sign_detached($data, $secret), + OkpKey::CURVE_NAME_ED25519 => sodium_crypto_sign_detached($data, $secret), + default => throw new InvalidArgumentException('Unsupported curve'), + }; + } + + public function verify(string $data, Key $key, string $signature): bool + { + $key = $this->handleKey($key); + if ($key->curve() !== OkpKey::CURVE_ED25519 && $key->curve() !== OkpKey::CURVE_NAME_ED25519) { + throw new InvalidArgumentException('Unsupported curve'); + } + try { + sodium_crypto_sign_verify_detached($signature, $data, $key->x()); + } catch (Throwable) { + return false; + } + + return true; + } + + public static function identifier(): int + { + return Algorithms::COSE_ALGORITHM_EDDSA; + } + + private function handleKey(Key $key): OkpKey + { + return OkpKey::create($key->getData()); + } +} diff --git a/3rdparty/web-auth/cose-lib/src/Algorithm/Signature/RSA/PS256.php b/3rdparty/web-auth/cose-lib/src/Algorithm/Signature/RSA/PS256.php new file mode 100644 index 00000000..3a82578d --- /dev/null +++ b/3rdparty/web-auth/cose-lib/src/Algorithm/Signature/RSA/PS256.php @@ -0,0 +1,27 @@ +handleKey($key); + $modulusLength = mb_strlen($key->n(), '8bit'); + + $em = $this->encodeEMSAPSS($data, 8 * $modulusLength - 1, $this->getHashAlgorithm()); + $message = BigInteger::createFromBinaryString($em); + $signature = $this->exponentiate($key, $message); + + return $this->convertIntegerToOctetString($signature, $modulusLength); + } + + public function verify(string $data, Key $key, string $signature): bool + { + $key = $this->handleKey($key); + $modulusLength = mb_strlen($key->n(), '8bit'); + if (mb_strlen($signature, '8bit') !== $modulusLength) { + throw new InvalidArgumentException('Invalid modulus length'); + } + $s2 = BigInteger::createFromBinaryString($signature); + $m2 = $this->exponentiate($key, $s2); + $em = $this->convertIntegerToOctetString($m2, $modulusLength); + $modBits = 8 * $modulusLength; + + return $this->verifyEMSAPSS($data, $em, $modBits - 1, $this->getHashAlgorithm()); + } + + /** + * Exponentiate with or without Chinese Remainder Theorem. Operation with primes 'p' and 'q' is appox. 2x faster. + */ + public function exponentiate(RsaKey $key, BigInteger $c): BigInteger + { + if ($c->compare(BigInteger::createFromDecimal(0)) < 0 || $c->compare( + BigInteger::createFromBinaryString($key->n()) + ) > 0) { + throw new RuntimeException(); + } + if ($key->isPublic() || ! $key->hasPrimes() || ! $key->hasExponents() || ! $key->hasCoefficient()) { + return $c->modPow( + BigInteger::createFromBinaryString($key->e()), + BigInteger::createFromBinaryString($key->n()) + ); + } + + [$pS, $qS] = $key->primes(); + [$dPS, $dQS] = $key->exponents(); + $qInv = BigInteger::createFromBinaryString($key->QInv()); + $p = BigInteger::createFromBinaryString($pS); + $q = BigInteger::createFromBinaryString($qS); + $dP = BigInteger::createFromBinaryString($dPS); + $dQ = BigInteger::createFromBinaryString($dQS); + + $m1 = $c->modPow($dP, $p); + $m2 = $c->modPow($dQ, $q); + $h = $qInv->multiply($m1->subtract($m2)->add($p)) + ->mod($p) + ; + + return $m2->add($h->multiply($q)); + } + + abstract protected function getHashAlgorithm(): Hash; + + private function handleKey(Key $key): RsaKey + { + return RsaKey::create($key->getData()); + } + + private function convertIntegerToOctetString(BigInteger $x, int $xLen): string + { + $xB = $x->toBytes(); + if (mb_strlen($xB, '8bit') > $xLen) { + throw new RuntimeException('Unable to convert the integer'); + } + + return str_pad($xB, $xLen, chr(0), STR_PAD_LEFT); + } + + /** + * MGF1. + */ + private function getMGF1(string $mgfSeed, int $maskLen, Hash $mgfHash): string + { + $t = ''; + $count = ceil($maskLen / $mgfHash->getLength()); + for ($i = 0; $i < $count; ++$i) { + $c = pack('N', $i); + $t .= $mgfHash->hash($mgfSeed . $c); + } + + return mb_substr($t, 0, $maskLen, '8bit'); + } + + /** + * EMSA-PSS-ENCODE. + */ + private function encodeEMSAPSS(string $message, int $modulusLength, Hash $hash): string + { + $emLen = ($modulusLength + 1) >> 3; + $sLen = $hash->getLength(); + $mHash = $hash->hash($message); + if ($emLen <= $hash->getLength() + $sLen + 2) { + throw new RuntimeException(); + } + $salt = random_bytes($sLen); + $m2 = "\0\0\0\0\0\0\0\0" . $mHash . $salt; + $h = $hash->hash($m2); + $ps = str_repeat(chr(0), $emLen - $sLen - $hash->getLength() - 2); + $db = $ps . chr(1) . $salt; + $dbMask = $this->getMGF1($h, $emLen - $hash->getLength() - 1, $hash); + $maskedDB = $db ^ $dbMask; + $maskedDB[0] = ~chr(0xFF << ($modulusLength & 7)) & $maskedDB[0]; + + return $maskedDB . $h . chr(0xBC); + } + + /** + * EMSA-PSS-VERIFY. + */ + private function verifyEMSAPSS(string $m, string $em, int $emBits, Hash $hash): bool + { + $emLen = ($emBits + 1) >> 3; + $sLen = $hash->getLength(); + $mHash = $hash->hash($m); + if ($emLen < $hash->getLength() + $sLen + 2) { + throw new InvalidArgumentException(); + } + if ($em[mb_strlen($em, '8bit') - 1] !== chr(0xBC)) { + throw new InvalidArgumentException(); + } + $maskedDB = mb_substr($em, 0, -$hash->getLength() - 1, '8bit'); + $h = mb_substr($em, -$hash->getLength() - 1, $hash->getLength(), '8bit'); + $temp = chr(0xFF << ($emBits & 7)); + if ((~$maskedDB[0] & $temp) !== $temp) { + throw new InvalidArgumentException(); + } + $dbMask = $this->getMGF1($h, $emLen - $hash->getLength() - 1, $hash/*MGF*/); + $db = $maskedDB ^ $dbMask; + $db[0] = ~chr(0xFF << ($emBits & 7)) & $db[0]; + $temp = $emLen - $hash->getLength() - $sLen - 2; + if (mb_strpos($db, str_repeat(chr(0), $temp), 0, '8bit') !== 0) { + throw new InvalidArgumentException(); + } + if (ord($db[$temp]) !== 1) { + throw new InvalidArgumentException(); + } + $salt = mb_substr($db, $temp + 1, null, '8bit'); // should be $sLen long + $m2 = "\0\0\0\0\0\0\0\0" . $mHash . $salt; + $h2 = $hash->hash($m2); + + return hash_equals($h, $h2); + } +} diff --git a/3rdparty/web-auth/cose-lib/src/Algorithm/Signature/RSA/RS1.php b/3rdparty/web-auth/cose-lib/src/Algorithm/Signature/RSA/RS1.php new file mode 100644 index 00000000..9cd60f32 --- /dev/null +++ b/3rdparty/web-auth/cose-lib/src/Algorithm/Signature/RSA/RS1.php @@ -0,0 +1,27 @@ +handleKey($key); + if (! $key->isPrivate()) { + throw new InvalidArgumentException('The key is not private.'); + } + + try { + openssl_sign($data, $signature, $key->asPem(), $this->getHashAlgorithm()); + } catch (Throwable $e) { + throw new InvalidArgumentException('Unable to sign the data', 0, $e); + } + + return $signature; + } + + public function verify(string $data, Key $key, string $signature): bool + { + $key = $this->handleKey($key); + + return openssl_verify($data, $signature, $key->toPublic()->asPem(), $this->getHashAlgorithm()) === 1; + } + + abstract protected function getHashAlgorithm(): int; + + private function handleKey(Key $key): RsaKey + { + return RsaKey::create($key->getData()); + } +} diff --git a/3rdparty/web-auth/cose-lib/src/Algorithm/Signature/Signature.php b/3rdparty/web-auth/cose-lib/src/Algorithm/Signature/Signature.php new file mode 100644 index 00000000..b0fd869b --- /dev/null +++ b/3rdparty/web-auth/cose-lib/src/Algorithm/Signature/Signature.php @@ -0,0 +1,15 @@ + OPENSSL_ALGO_SHA256, + self::COSE_ALGORITHM_ES384 => OPENSSL_ALGO_SHA384, + self::COSE_ALGORITHM_ES512 => OPENSSL_ALGO_SHA512, + self::COSE_ALGORITHM_RS256 => OPENSSL_ALGO_SHA256, + self::COSE_ALGORITHM_RS384 => OPENSSL_ALGO_SHA384, + self::COSE_ALGORITHM_RS512 => OPENSSL_ALGO_SHA512, + self::COSE_ALGORITHM_RS1 => OPENSSL_ALGO_SHA1, + ]; + + final public const COSE_HASH_MAP = [ + self::COSE_ALGORITHM_ES256K => 'sha256', + self::COSE_ALGORITHM_ES256 => 'sha256', + self::COSE_ALGORITHM_ES384 => 'sha384', + self::COSE_ALGORITHM_ES512 => 'sha512', + self::COSE_ALGORITHM_RS256 => 'sha256', + self::COSE_ALGORITHM_RS384 => 'sha384', + self::COSE_ALGORITHM_RS512 => 'sha512', + self::COSE_ALGORITHM_PS256 => 'sha256', + self::COSE_ALGORITHM_PS384 => 'sha384', + self::COSE_ALGORITHM_PS512 => 'sha512', + self::COSE_ALGORITHM_RS1 => 'sha1', + ]; + + public static function getOpensslAlgorithmFor(int $algorithmIdentifier): int + { + if (! array_key_exists($algorithmIdentifier, self::COSE_ALGORITHM_MAP)) { + throw new InvalidArgumentException('The specified algorithm identifier is not supported'); + } + + return self::COSE_ALGORITHM_MAP[$algorithmIdentifier]; + } + + public static function getHashAlgorithmFor(int $algorithmIdentifier): string + { + if (! array_key_exists($algorithmIdentifier, self::COSE_HASH_MAP)) { + throw new InvalidArgumentException('The specified algorithm identifier is not supported'); + } + + return self::COSE_HASH_MAP[$algorithmIdentifier]; + } +} diff --git a/3rdparty/web-auth/cose-lib/src/BigInteger.php b/3rdparty/web-auth/cose-lib/src/BigInteger.php new file mode 100644 index 00000000..e0ef082f --- /dev/null +++ b/3rdparty/web-auth/cose-lib/src/BigInteger.php @@ -0,0 +1,108 @@ +value->isEqualTo(BrickBigInteger::zero())) { + return ''; + } + + $temp = $this->value->toBase(16); + $temp = 0 !== (mb_strlen($temp, '8bit') & 1) ? '0' . $temp : $temp; + $temp = hex2bin($temp); + + return ltrim($temp, chr(0)); + } + + /** + * Adds two BigIntegers. + */ + public function add(self $y): self + { + $value = $this->value->plus($y->value); + + return new self($value); + } + + /** + * Subtracts two BigIntegers. + */ + public function subtract(self $y): self + { + $value = $this->value->minus($y->value); + + return new self($value); + } + + /** + * Multiplies two BigIntegers. + */ + public function multiply(self $x): self + { + $value = $this->value->multipliedBy($x->value); + + return new self($value); + } + + /** + * Performs modular exponentiation. + */ + public function modPow(self $e, self $n): self + { + $value = $this->value->modPow($e->value, $n->value); + + return new self($value); + } + + /** + * Performs modular exponentiation. + */ + public function mod(self $d): self + { + $value = $this->value->mod($d->value); + + return new self($value); + } + + /** + * Compares two numbers. + */ + public function compare(self $y): int + { + return $this->value->compareTo($y->value); + } +} diff --git a/3rdparty/web-auth/cose-lib/src/Hash.php b/3rdparty/web-auth/cose-lib/src/Hash.php new file mode 100644 index 00000000..5be1a92a --- /dev/null +++ b/3rdparty/web-auth/cose-lib/src/Hash.php @@ -0,0 +1,61 @@ +length; + } + + /** + * Compute the HMAC. + */ + public function hash(string $text): string + { + return hash($this->hash, $text, true); + } + + public function name(): string + { + return $this->hash; + } + + public function t(): string + { + return $this->t; + } +} diff --git a/3rdparty/web-auth/cose-lib/src/Key/Ec2Key.php b/3rdparty/web-auth/cose-lib/src/Key/Ec2Key.php new file mode 100644 index 00000000..e0fa7e19 --- /dev/null +++ b/3rdparty/web-auth/cose-lib/src/Key/Ec2Key.php @@ -0,0 +1,195 @@ + '1.2.840.10045.3.1.7', + // NIST P-256 / secp256r1 + self::CURVE_P256K => '1.3.132.0.10', + // NIST P-256K / secp256k1 + self::CURVE_P384 => '1.3.132.0.34', + // NIST P-384 / secp384r1 + self::CURVE_P521 => '1.3.132.0.35', + // NIST P-521 / secp521r1 + ]; + + private const CURVE_KEY_LENGTH = [ + self::CURVE_P256 => 32, + self::CURVE_P256K => 32, + self::CURVE_P384 => 48, + self::CURVE_P521 => 66, + self::CURVE_NAME_P256 => 32, + self::CURVE_NAME_P256K => 32, + self::CURVE_NAME_P384 => 48, + self::CURVE_NAME_P521 => 66, + ]; + + /** + * @param array $data + */ + public function __construct(array $data) + { + foreach ([self::DATA_CURVE, self::TYPE] as $key) { + if (is_numeric($data[$key])) { + $data[$key] = (int) $data[$key]; + } + } + parent::__construct($data); + if ($data[self::TYPE] !== self::TYPE_EC2 && $data[self::TYPE] !== self::TYPE_NAME_EC2) { + throw new InvalidArgumentException('Invalid EC2 key. The key type does not correspond to an EC2 key'); + } + if (! isset($data[self::DATA_CURVE], $data[self::DATA_X], $data[self::DATA_Y])) { + throw new InvalidArgumentException('Invalid EC2 key. The curve or the "x/y" coordinates are missing'); + } + if (mb_strlen((string) $data[self::DATA_X], '8bit') !== self::CURVE_KEY_LENGTH[$data[self::DATA_CURVE]]) { + throw new InvalidArgumentException('Invalid length for x coordinate'); + } + if (mb_strlen((string) $data[self::DATA_Y], '8bit') !== self::CURVE_KEY_LENGTH[$data[self::DATA_CURVE]]) { + throw new InvalidArgumentException('Invalid length for y coordinate'); + } + if (is_int($data[self::DATA_CURVE])) { + if (! in_array($data[self::DATA_CURVE], self::SUPPORTED_CURVES_INT, true)) { + throw new InvalidArgumentException('The curve is not supported'); + } + } elseif (! in_array($data[self::DATA_CURVE], self::SUPPORTED_CURVES_NAMES, true)) { + throw new InvalidArgumentException('The curve is not supported'); + } + } + + /** + * @param array $data + */ + public static function create(array $data): self + { + return new self($data); + } + + public function toPublic(): self + { + $data = $this->getData(); + unset($data[self::DATA_D]); + + return new self($data); + } + + public function x(): string + { + return $this->get(self::DATA_X); + } + + public function y(): string + { + return $this->get(self::DATA_Y); + } + + public function isPrivate(): bool + { + return array_key_exists(self::DATA_D, $this->getData()); + } + + public function d(): string + { + if (! $this->isPrivate()) { + throw new InvalidArgumentException('The key is not private.'); + } + return $this->get(self::DATA_D); + } + + public function curve(): int|string + { + return $this->get(self::DATA_CURVE); + } + + public function asPEM(): string + { + if ($this->isPrivate()) { + $der = Sequence::create( + Integer::create(1), + OctetString::create($this->d()), + ExplicitlyTaggedType::create(0, ObjectIdentifier::create($this->getCurveOid())), + ExplicitlyTaggedType::create(1, BitString::create($this->getUncompressedCoordinates())), + ); + + return $this->pem('EC PRIVATE KEY', $der->toDER()); + } + + $der = Sequence::create( + Sequence::create( + ObjectIdentifier::create('1.2.840.10045.2.1'), + ObjectIdentifier::create($this->getCurveOid()) + ), + BitString::create($this->getUncompressedCoordinates()) + ); + + return $this->pem('PUBLIC KEY', $der->toDER()); + } + + public function getUncompressedCoordinates(): string + { + return "\x04" . $this->x() . $this->y(); + } + + private function getCurveOid(): string + { + return self::NAMED_CURVE_OID[$this->curve()]; + } + + private function pem(string $type, string $der): string + { + return sprintf("-----BEGIN %s-----\n", mb_strtoupper($type)) . + chunk_split(base64_encode($der), 64, "\n") . + sprintf("-----END %s-----\n", mb_strtoupper($type)); + } +} diff --git a/3rdparty/web-auth/cose-lib/src/Key/Key.php b/3rdparty/web-auth/cose-lib/src/Key/Key.php new file mode 100644 index 00000000..7b28f7b9 --- /dev/null +++ b/3rdparty/web-auth/cose-lib/src/Key/Key.php @@ -0,0 +1,111 @@ + + */ + private readonly array $data; + + /** + * @param array $data + */ + public function __construct(array $data) + { + if (! array_key_exists(self::TYPE, $data)) { + throw new InvalidArgumentException('Invalid key: the type is not defined'); + } + $this->data = $data; + } + + /** + * @param array $data + */ + public static function create(array $data): self + { + return new self($data); + } + + /** + * @param array $data + */ + public static function createFromData(array $data): self + { + if (! array_key_exists(self::TYPE, $data)) { + throw new InvalidArgumentException('Invalid key: the type is not defined'); + } + + return match ($data[self::TYPE]) { + '1' => new OkpKey($data), + '2' => new Ec2Key($data), + '3' => new RsaKey($data), + '4' => new SymmetricKey($data), + default => self::create($data), + }; + } + + public function type(): int|string + { + return $this->data[self::TYPE]; + } + + public function alg(): int + { + return (int) $this->get(self::ALG); + } + + /** + * @return array + */ + public function getData(): array + { + return $this->data; + } + + public function has(int|string $key): bool + { + return array_key_exists($key, $this->data); + } + + public function get(int|string $key): mixed + { + if (! array_key_exists($key, $this->data)) { + throw new InvalidArgumentException(sprintf('The key has no data at index %d', $key)); + } + + return $this->data[$key]; + } +} diff --git a/3rdparty/web-auth/cose-lib/src/Key/OkpKey.php b/3rdparty/web-auth/cose-lib/src/Key/OkpKey.php new file mode 100644 index 00000000..ad2b8006 --- /dev/null +++ b/3rdparty/web-auth/cose-lib/src/Key/OkpKey.php @@ -0,0 +1,110 @@ + $data + */ + public function __construct(array $data) + { + foreach ([self::DATA_CURVE, self::TYPE] as $key) { + if (is_numeric($data[$key])) { + $data[$key] = (int) $data[$key]; + } + } + parent::__construct($data); + if ($data[self::TYPE] !== self::TYPE_OKP && $data[self::TYPE] !== self::TYPE_NAME_OKP) { + throw new InvalidArgumentException('Invalid OKP key. The key type does not correspond to an OKP key'); + } + if (! isset($data[self::DATA_CURVE], $data[self::DATA_X])) { + throw new InvalidArgumentException('Invalid EC2 key. The curve or the "x" coordinate is missing'); + } + if (is_numeric($data[self::DATA_CURVE])) { + if (! in_array((int) $data[self::DATA_CURVE], self::SUPPORTED_CURVES_INT, true)) { + throw new InvalidArgumentException('The curve is not supported'); + } + } elseif (! in_array($data[self::DATA_CURVE], self::SUPPORTED_CURVES_NAME, true)) { + throw new InvalidArgumentException('The curve is not supported'); + } + } + + /** + * @param array $data + */ + public static function create(array $data): self + { + return new self($data); + } + + public function x(): string + { + return $this->get(self::DATA_X); + } + + public function isPrivate(): bool + { + return array_key_exists(self::DATA_D, $this->getData()); + } + + public function d(): string + { + if (! $this->isPrivate()) { + throw new InvalidArgumentException('The key is not private.'); + } + + return $this->get(self::DATA_D); + } + + public function curve(): int|string + { + return $this->get(self::DATA_CURVE); + } +} diff --git a/3rdparty/web-auth/cose-lib/src/Key/RsaKey.php b/3rdparty/web-auth/cose-lib/src/Key/RsaKey.php new file mode 100644 index 00000000..77e2af50 --- /dev/null +++ b/3rdparty/web-auth/cose-lib/src/Key/RsaKey.php @@ -0,0 +1,262 @@ + $data + */ + public function __construct(array $data) + { + foreach ([self::TYPE] as $key) { + if (is_numeric($data[$key])) { + $data[$key] = (int) $data[$key]; + } + } + parent::__construct($data); + if ($data[self::TYPE] !== self::TYPE_RSA && $data[self::TYPE] !== self::TYPE_NAME_RSA) { + throw new InvalidArgumentException('Invalid RSA key. The key type does not correspond to a RSA key'); + } + if (! isset($data[self::DATA_N], $data[self::DATA_E])) { + throw new InvalidArgumentException('Invalid RSA key. The modulus or the exponent is missing'); + } + } + + /** + * @param array $data + */ + public static function create(array $data): self + { + return new self($data); + } + + public function n(): string + { + return $this->get(self::DATA_N); + } + + public function e(): string + { + return $this->get(self::DATA_E); + } + + public function d(): string + { + $this->checkKeyIsPrivate(); + + return $this->get(self::DATA_D); + } + + public function p(): string + { + $this->checkKeyIsPrivate(); + + return $this->get(self::DATA_P); + } + + public function q(): string + { + $this->checkKeyIsPrivate(); + + return $this->get(self::DATA_Q); + } + + public function dP(): string + { + $this->checkKeyIsPrivate(); + + return $this->get(self::DATA_DP); + } + + public function dQ(): string + { + $this->checkKeyIsPrivate(); + + return $this->get(self::DATA_DQ); + } + + public function QInv(): string + { + $this->checkKeyIsPrivate(); + + return $this->get(self::DATA_QI); + } + + /** + * @return array + */ + public function other(): array + { + $this->checkKeyIsPrivate(); + + return $this->get(self::DATA_OTHER); + } + + public function rI(): string + { + $this->checkKeyIsPrivate(); + + return $this->get(self::DATA_RI); + } + + public function dI(): string + { + $this->checkKeyIsPrivate(); + + return $this->get(self::DATA_DI); + } + + public function tI(): string + { + $this->checkKeyIsPrivate(); + + return $this->get(self::DATA_TI); + } + + public function hasPrimes(): bool + { + return $this->has(self::DATA_P) && $this->has(self::DATA_Q); + } + + /** + * @return string[] + */ + public function primes(): array + { + return [$this->p(), $this->q()]; + } + + public function hasExponents(): bool + { + return $this->has(self::DATA_DP) && $this->has(self::DATA_DQ); + } + + /** + * @return string[] + */ + public function exponents(): array + { + return [$this->dP(), $this->dQ()]; + } + + public function hasCoefficient(): bool + { + return $this->has(self::DATA_QI); + } + + public function isPublic(): bool + { + return ! $this->isPrivate(); + } + + public function isPrivate(): bool + { + return array_key_exists(self::DATA_D, $this->getData()); + } + + public function asPem(): string + { + if ($this->isPrivate()) { + $privateKey = RSAPrivateKey::create( + $this->binaryToBigInteger($this->n()), + $this->binaryToBigInteger($this->e()), + $this->binaryToBigInteger($this->d()), + $this->binaryToBigInteger($this->p()), + $this->binaryToBigInteger($this->q()), + $this->binaryToBigInteger($this->dP()), + $this->binaryToBigInteger($this->dQ()), + $this->binaryToBigInteger($this->QInv()) + ); + + return $privateKey->toPEM() + ->string(); + } + + $publicKey = RSAPublicKey::create( + $this->binaryToBigInteger($this->n()), + $this->binaryToBigInteger($this->e()) + ); + $rsaKey = PublicKeyInfo::fromPublicKey($publicKey); + + return $rsaKey->toPEM() + ->string(); + } + + public function toPublic(): static + { + $toBeRemoved = [ + self::DATA_D, + self::DATA_P, + self::DATA_Q, + self::DATA_DP, + self::DATA_DQ, + self::DATA_QI, + self::DATA_OTHER, + self::DATA_RI, + self::DATA_DI, + self::DATA_TI, + ]; + $data = $this->getData(); + foreach ($data as $k => $v) { + if (in_array($k, $toBeRemoved, true)) { + unset($data[$k]); + } + } + + return new static($data); + } + + private function checkKeyIsPrivate(): void + { + if (! $this->isPrivate()) { + throw new InvalidArgumentException('The key is not private.'); + } + } + + private function binaryToBigInteger(string $data): string + { + $res = unpack('H*', $data); + $res = current($res); + + return BigInteger::fromBase($res, 16)->toBase(10); + } +} diff --git a/3rdparty/web-auth/cose-lib/src/Key/SymmetricKey.php b/3rdparty/web-auth/cose-lib/src/Key/SymmetricKey.php new file mode 100644 index 00000000..57ca7cd5 --- /dev/null +++ b/3rdparty/web-auth/cose-lib/src/Key/SymmetricKey.php @@ -0,0 +1,44 @@ + $data + */ + public function __construct(array $data) + { + parent::__construct($data); + if (! isset($data[self::TYPE]) || (int) $data[self::TYPE] !== self::TYPE_OCT) { + throw new InvalidArgumentException( + 'Invalid symmetric key. The key type does not correspond to a symmetric key' + ); + } + if (! isset($data[self::DATA_K])) { + throw new InvalidArgumentException('Invalid symmetric key. The parameter "k" is missing'); + } + } + + /** + * @param array $data + */ + public static function create(array $data): self + { + return new self($data); + } + + public function k(): string + { + return $this->get(self::DATA_K); + } +} diff --git a/3rdparty/web-auth/webauthn-lib/LICENSE b/3rdparty/web-auth/webauthn-lib/LICENSE new file mode 100644 index 00000000..8cb7b748 --- /dev/null +++ b/3rdparty/web-auth/webauthn-lib/LICENSE @@ -0,0 +1,21 @@ +MIT License + +Copyright (c) 2018-2022 Spomky-Labs + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. diff --git a/3rdparty/web-auth/webauthn-lib/src/AttestationStatement/AndroidKeyAttestationStatementSupport.php b/3rdparty/web-auth/webauthn-lib/src/AttestationStatement/AndroidKeyAttestationStatementSupport.php new file mode 100644 index 00000000..eb54b7b4 --- /dev/null +++ b/3rdparty/web-auth/webauthn-lib/src/AttestationStatement/AndroidKeyAttestationStatementSupport.php @@ -0,0 +1,227 @@ +decoder = Decoder::create(); + $this->dispatcher = new NullEventDispatcher(); + } + + public function setEventDispatcher(EventDispatcherInterface $eventDispatcher): void + { + $this->dispatcher = $eventDispatcher; + } + + public static function create(): self + { + return new self(); + } + + public function name(): string + { + return 'android-key'; + } + + /** + * @param array $attestation + */ + public function load(array $attestation): AttestationStatement + { + array_key_exists('attStmt', $attestation) || throw AttestationStatementLoadingException::create($attestation); + foreach (['sig', 'x5c', 'alg'] as $key) { + array_key_exists($key, $attestation['attStmt']) || throw AttestationStatementLoadingException::create( + $attestation, + sprintf('The attestation statement value "%s" is missing.', $key) + ); + } + $certificates = $attestation['attStmt']['x5c']; + (is_countable($certificates) ? count( + $certificates + ) : 0) > 0 || throw AttestationStatementLoadingException::create( + $attestation, + 'The attestation statement value "x5c" must be a list with at least one certificate.' + ); + $certificates = CertificateToolbox::convertAllDERToPEM($certificates); + + $attestationStatement = AttestationStatement::createBasic( + $attestation['fmt'], + $attestation['attStmt'], + CertificateTrustPath::create($certificates) + ); + $this->dispatcher->dispatch(AttestationStatementLoaded::create($attestationStatement)); + + return $attestationStatement; + } + + public function isValid( + string $clientDataJSONHash, + AttestationStatement $attestationStatement, + AuthenticatorData $authenticatorData + ): bool { + $trustPath = $attestationStatement->trustPath; + $trustPath instanceof CertificateTrustPath || throw InvalidAttestationStatementException::create( + $attestationStatement, + 'Invalid trust path. Shall contain certificates.' + ); + + $certificates = $trustPath->certificates; + + //Decode leaf attestation certificate + $leaf = $certificates[0]; + $this->checkCertificate($leaf, $clientDataJSONHash, $authenticatorData); + + $signedData = $authenticatorData->authData . $clientDataJSONHash; + $alg = $attestationStatement->get('alg'); + + return openssl_verify( + $signedData, + $attestationStatement->get('sig'), + $leaf, + Algorithms::getOpensslAlgorithmFor((int) $alg) + ) === 1; + } + + private function checkCertificate( + string $certificate, + string $clientDataHash, + AuthenticatorData $authenticatorData + ): void { + $resource = openssl_pkey_get_public($certificate); + $details = openssl_pkey_get_details($resource); + is_array($details) || throw AttestationStatementVerificationException::create( + 'Unable to read the certificate' + ); + + //Check that authData publicKey matches the public key in the attestation certificate + $attestedCredentialData = $authenticatorData->attestedCredentialData; + $attestedCredentialData !== null || throw AttestationStatementVerificationException::create( + 'No attested credential data found' + ); + $publicKeyData = $attestedCredentialData->credentialPublicKey; + $publicKeyData !== null || throw AttestationStatementVerificationException::create( + 'No attested public key found' + ); + $publicDataStream = new StringStream($publicKeyData); + $coseKey = $this->decoder->decode($publicDataStream); + $coseKey instanceof Normalizable || throw AttestationStatementVerificationException::create( + 'Invalid attested public key found' + ); + + $publicDataStream->isEOF() || throw AttestationStatementVerificationException::create( + 'Invalid public key data. Presence of extra bytes.' + ); + $publicDataStream->close(); + $publicKey = Key::createFromData($coseKey->normalize()); + + ($publicKey instanceof Ec2Key) || ($publicKey instanceof RsaKey) || throw AttestationStatementVerificationException::create( + 'Unsupported key type' + ); + $publicKey->asPEM() === $details['key'] || throw AttestationStatementVerificationException::create( + 'Invalid key' + ); + + /*---------------------------*/ + $certDetails = openssl_x509_parse($certificate); + + //Find Android KeyStore Extension with OID "1.3.6.1.4.1.11129.2.1.17" in certificate extensions + is_array( + $certDetails + ) || throw AttestationStatementVerificationException::create('The certificate is not valid'); + array_key_exists('extensions', $certDetails) || throw AttestationStatementVerificationException::create( + 'The certificate has no extension' + ); + is_array($certDetails['extensions']) || throw AttestationStatementVerificationException::create( + 'The certificate has no extension' + ); + array_key_exists( + '1.3.6.1.4.1.11129.2.1.17', + $certDetails['extensions'] + ) || throw AttestationStatementVerificationException::create( + 'The certificate extension "1.3.6.1.4.1.11129.2.1.17" is missing' + ); + $extension = $certDetails['extensions']['1.3.6.1.4.1.11129.2.1.17']; + $extensionAsAsn1 = Sequence::fromDER($extension); + $extensionAsAsn1->has(4); + + //Check that attestationChallenge is set to the clientDataHash. + $extensionAsAsn1->has(4) || throw AttestationStatementVerificationException::create( + 'The certificate extension "1.3.6.1.4.1.11129.2.1.17" is invalid' + ); + $ext = $extensionAsAsn1->at(4) + ->asElement(); + $ext instanceof OctetString || throw AttestationStatementVerificationException::create( + 'The certificate extension "1.3.6.1.4.1.11129.2.1.17" is invalid' + ); + $clientDataHash === $ext->string() || throw AttestationStatementVerificationException::create( + 'The client data hash is not valid' + ); + + //Check that both teeEnforced and softwareEnforced structures don't contain allApplications(600) tag. + $extensionAsAsn1->has(6) || throw AttestationStatementVerificationException::create( + 'The certificate extension "1.3.6.1.4.1.11129.2.1.17" is invalid' + ); + + $softwareEnforcedFlags = $extensionAsAsn1->at(6) + ->asElement(); + $softwareEnforcedFlags instanceof Sequence || throw AttestationStatementVerificationException::create( + 'The certificate extension "1.3.6.1.4.1.11129.2.1.17" is invalid' + ); + $this->checkAbsenceOfAllApplicationsTag($softwareEnforcedFlags); + + $extensionAsAsn1->has(7) || throw AttestationStatementVerificationException::create( + 'The certificate extension "1.3.6.1.4.1.11129.2.1.17" is invalid' + ); + $teeEnforcedFlags = $extensionAsAsn1->at(7) + ->asElement(); + $teeEnforcedFlags instanceof Sequence || throw AttestationStatementVerificationException::create( + 'The certificate extension "1.3.6.1.4.1.11129.2.1.17" is invalid' + ); + $this->checkAbsenceOfAllApplicationsTag($teeEnforcedFlags); + } + + private function checkAbsenceOfAllApplicationsTag(Sequence $sequence): void + { + foreach ($sequence->elements() as $tag) { + $tag->asElement() instanceof ExplicitTagging || throw AttestationStatementVerificationException::create( + 'Invalid tag' + ); + $tag->asElement() + ->tag() !== 600 || throw AttestationStatementVerificationException::create('Forbidden tag 600 found'); + } + } +} diff --git a/3rdparty/web-auth/webauthn-lib/src/AttestationStatement/AndroidSafetyNetAttestationStatementSupport.php b/3rdparty/web-auth/webauthn-lib/src/AttestationStatement/AndroidSafetyNetAttestationStatementSupport.php new file mode 100644 index 00000000..cd87757c --- /dev/null +++ b/3rdparty/web-auth/webauthn-lib/src/AttestationStatement/AndroidSafetyNetAttestationStatementSupport.php @@ -0,0 +1,382 @@ +clock === null) { + trigger_deprecation( + 'web-auth/webauthn-lib', + '4.8.0', + 'The parameter "$clock" will be required in 5.0.0. Please set a clock instance.' + ); + } + if (! class_exists(RS256::class) || ! class_exists(JWKFactory::class)) { + throw UnsupportedFeatureException::create( + 'The algorithm RS256 is missing. Did you forget to install the package web-token/jwt-library?' + ); + } + $this->jwsSerializer = new CompactSerializer(); + $this->initJwsVerifier(); + $this->dispatcher = new NullEventDispatcher(); + } + + public function setEventDispatcher(EventDispatcherInterface $eventDispatcher): void + { + $this->dispatcher = $eventDispatcher; + } + + public static function create(null|ClockInterface $clock = null): self + { + return new self($clock); + } + + public function enableApiVerification( + ClientInterface|HttpClientInterface $client, + string $apiKey, + ?RequestFactoryInterface $requestFactory = null + ): self { + $this->apiKey = $apiKey; + $this->client = $client; + $this->requestFactory = $requestFactory; + if ($requestFactory !== null && ! $client instanceof HttpClientInterface) { + trigger_deprecation( + 'web-auth/metadata-service', + '4.7.0', + 'The parameter "$requestFactory" will be removed in 5.0.0. Please set it to null and set an Symfony\Contracts\HttpClient\HttpClientInterface as "$client" argument.' + ); + } + + return $this; + } + + public function setMaxAge(int $maxAge): self + { + $this->maxAge = $maxAge; + + return $this; + } + + public function setLeeway(int $leeway): self + { + $this->leeway = $leeway; + + return $this; + } + + public function name(): string + { + return 'android-safetynet'; + } + + /** + * @param array $attestation + */ + public function load(array $attestation): AttestationStatement + { + array_key_exists('attStmt', $attestation) || throw AttestationStatementLoadingException::create( + $attestation + ); + foreach (['ver', 'response'] as $key) { + array_key_exists($key, $attestation['attStmt']) || throw AttestationStatementLoadingException::create( + $attestation, + sprintf('The attestation statement value "%s" is missing.', $key) + ); + $attestation['attStmt'][$key] !== '' || throw AttestationStatementLoadingException::create( + $attestation, + sprintf('The attestation statement value "%s" is empty.', $key) + ); + } + $jws = $this->jwsSerializer->unserialize($attestation['attStmt']['response']); + $jwsHeader = $jws->getSignature(0) + ->getProtectedHeader(); + array_key_exists('x5c', $jwsHeader) || throw AttestationStatementLoadingException::create( + $attestation, + 'The response in the attestation statement must contain a "x5c" header.' + ); + (is_countable($jwsHeader['x5c']) ? count( + $jwsHeader['x5c'] + ) : 0) > 0 || throw AttestationStatementLoadingException::create( + $attestation, + 'The "x5c" parameter in the attestation statement response must contain at least one certificate.' + ); + $certificates = $this->convertCertificatesToPem($jwsHeader['x5c']); + $attestation['attStmt']['jws'] = $jws; + + $attestationStatement = AttestationStatement::createBasic( + $this->name(), + $attestation['attStmt'], + CertificateTrustPath::create($certificates) + ); + $this->dispatcher->dispatch(AttestationStatementLoaded::create($attestationStatement)); + + return $attestationStatement; + } + + public function isValid( + string $clientDataJSONHash, + AttestationStatement $attestationStatement, + AuthenticatorData $authenticatorData + ): bool { + $trustPath = $attestationStatement->trustPath; + $trustPath instanceof CertificateTrustPath || throw InvalidAttestationStatementException::create( + $attestationStatement, + 'Invalid trust path' + ); + $certificates = $trustPath->certificates; + $firstCertificate = current($certificates); + is_string($firstCertificate) || throw InvalidAttestationStatementException::create( + $attestationStatement, + 'No certificate' + ); + + $parsedCertificate = openssl_x509_parse($firstCertificate); + is_array($parsedCertificate) || throw InvalidAttestationStatementException::create( + $attestationStatement, + 'Invalid attestation object' + ); + array_key_exists('subject', $parsedCertificate) || throw InvalidAttestationStatementException::create( + $attestationStatement, + 'Invalid attestation object' + ); + array_key_exists('CN', $parsedCertificate['subject']) || throw InvalidAttestationStatementException::create( + $attestationStatement, + 'Invalid attestation object' + ); + $parsedCertificate['subject']['CN'] === 'attest.android.com' || throw InvalidAttestationStatementException::create( + $attestationStatement, + 'Invalid attestation object' + ); + + /** @var JWS $jws */ + $jws = $attestationStatement->get('jws'); + $payload = $jws->getPayload(); + $this->validatePayload($payload, $clientDataJSONHash, $authenticatorData); + + //Check the signature + $this->validateSignature($jws, $trustPath); + + //Check against Google service + $this->validateUsingGoogleApi($attestationStatement); + + return true; + } + + private function validatePayload( + ?string $payload, + string $clientDataJSONHash, + AuthenticatorData $authenticatorData + ): void { + $payload !== null || throw AttestationStatementVerificationException::create('Invalid attestation object'); + $payload = json_decode($payload, true, flags: JSON_THROW_ON_ERROR); + array_key_exists('nonce', $payload) || throw AttestationStatementVerificationException::create( + 'Invalid attestation object. "nonce" is missing.' + ); + $payload['nonce'] === base64_encode( + hash('sha256', $authenticatorData->authData . $clientDataJSONHash, true) + ) || throw AttestationStatementVerificationException::create('Invalid attestation object. Invalid nonce'); + array_key_exists('ctsProfileMatch', $payload) || throw AttestationStatementVerificationException::create( + 'Invalid attestation object. "ctsProfileMatch" is missing.' + ); + $payload['ctsProfileMatch'] || throw AttestationStatementVerificationException::create( + 'Invalid attestation object. "ctsProfileMatch" value is false.' + ); + array_key_exists('timestampMs', $payload) || throw AttestationStatementVerificationException::create( + 'Invalid attestation object. Timestamp is missing.' + ); + is_int($payload['timestampMs']) || throw AttestationStatementVerificationException::create( + 'Invalid attestation object. Timestamp shall be an integer.' + ); + + $currentTime = ($this->clock?->now()->getTimestamp() ?? time()) * 1000; + $payload['timestampMs'] <= $currentTime + $this->leeway || throw AttestationStatementVerificationException::create( + sprintf( + 'Invalid attestation object. Issued in the future. Current time: %d. Response time: %d', + $currentTime, + $payload['timestampMs'] + ) + ); + $currentTime - $payload['timestampMs'] <= $this->maxAge || throw AttestationStatementVerificationException::create( + sprintf( + 'Invalid attestation object. Too old. Current time: %d. Response time: %d', + $currentTime, + $payload['timestampMs'] + ) + ); + } + + private function validateSignature(JWS $jws, CertificateTrustPath $trustPath): void + { + $jwk = JWKFactory::createFromCertificate($trustPath->certificates[0]); + $isValid = $this->jwsVerifier?->verifyWithKey($jws, $jwk, 0); + $isValid === true || throw AttestationStatementVerificationException::create('Invalid response signature'); + } + + private function validateUsingGoogleApi(AttestationStatement $attestationStatement): void + { + if ($this->client === null || $this->apiKey === null) { + return; + } + $uri = sprintf( + 'https://www.googleapis.com/androidcheck/v1/attestations/verify?key=%s', + urlencode($this->apiKey) + ); + $requestBody = sprintf('{"signedAttestation":"%s"}', $attestationStatement->get('response')); + if ($this->client instanceof HttpClientInterface) { + $responseBody = $this->validateUsingGoogleApiWithSymfonyClient($requestBody, $uri); + } else { + $responseBody = $this->validateUsingGoogleApiWithPsrClient($requestBody, $uri); + } + $responseBodyJson = json_decode($responseBody, true, flags: JSON_THROW_ON_ERROR); + array_key_exists( + 'isValidSignature', + $responseBodyJson + ) || throw AttestationStatementVerificationException::create('Invalid response.'); + $responseBodyJson['isValidSignature'] === true || throw AttestationStatementVerificationException::create( + 'Invalid response.' + ); + } + + private function getResponseBody(ResponseInterface $response): string + { + $responseBody = ''; + $response->getBody() + ->rewind(); + do { + $tmp = $response->getBody() + ->read(1024); + if ($tmp === '') { + break; + } + $responseBody .= $tmp; + } while (true); + + return $responseBody; + } + + /** + * @param string[] $certificates + * + * @return string[] + */ + private function convertCertificatesToPem(array $certificates): array + { + foreach ($certificates as $k => $v) { + $certificates[$k] = CertificateToolbox::fixPEMStructure($v); + } + + return $certificates; + } + + private function initJwsVerifier(): void + { + $algorithmClasses = [ + RS256::class, RS384::class, RS512::class, + PS256::class, PS384::class, PS512::class, + ES256::class, ES384::class, ES512::class, + EdDSA::class, + ]; + /** @var AlgorithmInterface[] $algorithms */ + $algorithms = []; + foreach ($algorithmClasses as $algorithm) { + if (class_exists($algorithm)) { + $algorithms[] = new $algorithm(); + } + } + $algorithmManager = new AlgorithmManager($algorithms); + $this->jwsVerifier = new JWSVerifier($algorithmManager); + } + + private function validateUsingGoogleApiWithSymfonyClient(string $requestBody, string $uri): string + { + $response = $this->client->request('POST', $uri, [ + 'headers' => [ + 'content-type' => 'application/json', + ], + 'body' => $requestBody, + ]); + $response->getStatusCode() === 200 || throw AttestationStatementVerificationException::create( + 'Request did not succeeded' + ); + + return $response->getContent(); + } + + private function validateUsingGoogleApiWithPsrClient(string $requestBody, string $uri): string + { + $request = $this->requestFactory->createRequest('POST', $uri); + $request = $request->withHeader('content-type', 'application/json'); + $request->getBody() + ->write($requestBody); + + $response = $this->client->sendRequest($request); + $response->getStatusCode() === 200 || throw AttestationStatementVerificationException::create( + 'Request did not succeeded' + ); + + return $this->getResponseBody($response); + } +} diff --git a/3rdparty/web-auth/webauthn-lib/src/AttestationStatement/AppleAttestationStatementSupport.php b/3rdparty/web-auth/webauthn-lib/src/AttestationStatement/AppleAttestationStatementSupport.php new file mode 100644 index 00000000..e57c3383 --- /dev/null +++ b/3rdparty/web-auth/webauthn-lib/src/AttestationStatement/AppleAttestationStatementSupport.php @@ -0,0 +1,177 @@ +decoder = Decoder::create(); + $this->dispatcher = new NullEventDispatcher(); + } + + public function setEventDispatcher(EventDispatcherInterface $eventDispatcher): void + { + $this->dispatcher = $eventDispatcher; + } + + public static function create(): self + { + return new self(); + } + + public function name(): string + { + return 'apple'; + } + + /** + * @param array $attestation + */ + public function load(array $attestation): AttestationStatement + { + array_key_exists('attStmt', $attestation) || throw AttestationStatementLoadingException::create( + $attestation, + 'Invalid attestation object' + ); + array_key_exists('x5c', $attestation['attStmt']) || throw AttestationStatementLoadingException::create( + $attestation, + 'The attestation statement value "x5c" is missing.' + ); + $certificates = $attestation['attStmt']['x5c']; + (is_countable($certificates) ? count( + $certificates + ) : 0) > 0 || throw AttestationStatementLoadingException::create( + $attestation, + 'The attestation statement value "x5c" must be a list with at least one certificate.' + ); + $certificates = CertificateToolbox::convertAllDERToPEM($certificates); + + $attestationStatement = AttestationStatement::createAnonymizationCA( + $attestation['fmt'], + $attestation['attStmt'], + CertificateTrustPath::create($certificates) + ); + $this->dispatcher->dispatch(AttestationStatementLoaded::create($attestationStatement)); + + return $attestationStatement; + } + + public function isValid( + string $clientDataJSONHash, + AttestationStatement $attestationStatement, + AuthenticatorData $authenticatorData + ): bool { + $trustPath = $attestationStatement->trustPath; + $trustPath instanceof CertificateTrustPath || throw InvalidAttestationStatementException::create( + $attestationStatement, + 'Invalid trust path' + ); + + $certificates = $trustPath->certificates; + + //Decode leaf attestation certificate + $leaf = $certificates[0]; + + $this->checkCertificateAndGetPublicKey($leaf, $clientDataJSONHash, $authenticatorData); + + return true; + } + + private function checkCertificateAndGetPublicKey( + string $certificate, + string $clientDataHash, + AuthenticatorData $authenticatorData + ): void { + $resource = openssl_pkey_get_public($certificate); + $details = openssl_pkey_get_details($resource); + is_array($details) || throw AttestationStatementVerificationException::create( + 'Unable to read the certificate' + ); + + //Check that authData publicKey matches the public key in the attestation certificate + $attestedCredentialData = $authenticatorData->attestedCredentialData; + $attestedCredentialData !== null || throw AttestationStatementVerificationException::create( + 'No attested credential data found' + ); + $publicKeyData = $attestedCredentialData->credentialPublicKey; + $publicKeyData !== null || throw AttestationStatementVerificationException::create( + 'No attested public key found' + ); + $publicDataStream = new StringStream($publicKeyData); + $coseKey = $this->decoder->decode($publicDataStream); + $coseKey instanceof Normalizable || throw AttestationStatementVerificationException::create( + 'Invalid attested public key found' + ); + $publicDataStream->isEOF() || throw AttestationStatementVerificationException::create( + 'Invalid public key data. Presence of extra bytes.' + ); + $publicDataStream->close(); + $publicKey = Key::createFromData($coseKey->normalize()); + + ($publicKey instanceof Ec2Key) || ($publicKey instanceof RsaKey) || throw AttestationStatementVerificationException::create( + 'Unsupported key type' + ); + + //We check the attested key corresponds to the key in the certificate + $publicKey->asPEM() === $details['key'] || throw AttestationStatementVerificationException::create( + 'Invalid key' + ); + + /*---------------------------*/ + $certDetails = openssl_x509_parse($certificate); + + //Find Apple Extension with OID "1.2.840.113635.100.8.2" in certificate extensions + is_array( + $certDetails + ) || throw AttestationStatementVerificationException::create('The certificate is not valid'); + array_key_exists('extensions', $certDetails) || throw AttestationStatementVerificationException::create( + 'The certificate has no extension' + ); + is_array($certDetails['extensions']) || throw AttestationStatementVerificationException::create( + 'The certificate has no extension' + ); + array_key_exists( + '1.2.840.113635.100.8.2', + $certDetails['extensions'] + ) || throw AttestationStatementVerificationException::create( + 'The certificate extension "1.2.840.113635.100.8.2" is missing' + ); + $extension = $certDetails['extensions']['1.2.840.113635.100.8.2']; + + $nonceToHash = $authenticatorData->authData . $clientDataHash; + $nonce = hash('sha256', $nonceToHash); + + //'3024a1220420' corresponds to the Sequence+Explicitly Tagged Object + Octet Object + '3024a1220420' . $nonce === bin2hex( + (string) $extension + ) || throw AttestationStatementVerificationException::create('The client data hash is not valid'); + } +} diff --git a/3rdparty/web-auth/webauthn-lib/src/AttestationStatement/AttestationObject.php b/3rdparty/web-auth/webauthn-lib/src/AttestationStatement/AttestationObject.php new file mode 100644 index 00000000..a89cccac --- /dev/null +++ b/3rdparty/web-auth/webauthn-lib/src/AttestationStatement/AttestationObject.php @@ -0,0 +1,84 @@ +rawAttestationObject; + } + + /** + * @deprecated since 4.7.0. Please use the property directly. + * @infection-ignore-all + */ + public function getAttStmt(): AttestationStatement + { + return $this->attStmt; + } + + /** + * @deprecated since 4.7.0. Please use the property directly. + * @infection-ignore-all + */ + public function setAttStmt(AttestationStatement $attStmt): void + { + $this->attStmt = $attStmt; + } + + /** + * @deprecated since 4.7.0. Please use the property directly. + * @infection-ignore-all + */ + public function getAuthData(): AuthenticatorData + { + return $this->authData; + } + + /** + * @deprecated since 4.7.0. Please use the property directly. + * @infection-ignore-all + */ + public function getMetadataStatement(): ?MetadataStatement + { + return $this->metadataStatement; + } + + /** + * @deprecated since 4.7.0. Please use the property directly. + * @infection-ignore-all + */ + public function setMetadataStatement(MetadataStatement $metadataStatement): self + { + $this->metadataStatement = $metadataStatement; + + return $this; + } +} diff --git a/3rdparty/web-auth/webauthn-lib/src/AttestationStatement/AttestationObjectLoader.php b/3rdparty/web-auth/webauthn-lib/src/AttestationStatement/AttestationObjectLoader.php new file mode 100644 index 00000000..8ad284c4 --- /dev/null +++ b/3rdparty/web-auth/webauthn-lib/src/AttestationStatement/AttestationObjectLoader.php @@ -0,0 +1,115 @@ +logger = new NullLogger(); + $this->dispatcher = new NullEventDispatcher(); + } + + public function setEventDispatcher(EventDispatcherInterface $eventDispatcher): void + { + $this->dispatcher = $eventDispatcher; + } + + public static function create(AttestationStatementSupportManager $attestationStatementSupportManager): self + { + return new self($attestationStatementSupportManager); + } + + public function load(string $data): AttestationObject + { + try { + $this->logger->info('Trying to load the data', [ + 'data' => $data, + ]); + $decodedData = Base64::decode($data); + $stream = new StringStream($decodedData); + $parsed = Decoder::create()->decode($stream); + + $this->logger->info('Loading the Attestation Statement'); + $parsed instanceof Normalizable || throw InvalidDataException::create( + $parsed, + 'Invalid attestation object. Unexpected object.' + ); + $attestationObject = $parsed->normalize(); + $stream->isEOF() || throw InvalidDataException::create( + null, + 'Invalid attestation object. Presence of extra bytes.' + ); + $stream->close(); + is_array($attestationObject) || throw InvalidDataException::create( + $attestationObject, + 'Invalid attestation object' + ); + array_key_exists('authData', $attestationObject) || throw InvalidDataException::create( + $attestationObject, + 'Invalid attestation object' + ); + array_key_exists('fmt', $attestationObject) || throw InvalidDataException::create( + $attestationObject, + 'Invalid attestation object' + ); + array_key_exists('attStmt', $attestationObject) || throw InvalidDataException::create( + $attestationObject, + 'Invalid attestation object' + ); + + $attestationStatementSupport = $this->attestationStatementSupportManager->get($attestationObject['fmt']); + $attestationStatement = $attestationStatementSupport->load($attestationObject); + $this->logger->info('Attestation Statement loaded'); + $this->logger->debug('Attestation Statement loaded', [ + 'attestationStatement' => $attestationStatement, + ]); + $authData = $attestationObject['authData']; + $authDataLoader = AuthenticatorDataLoader::create(); + $authenticatorData = $authDataLoader->load($authData); + + $attestationObject = AttestationObject::create($data, $attestationStatement, $authenticatorData); + $this->logger->info('Attestation Object loaded'); + $this->logger->debug('Attestation Object', [ + 'ed' => $attestationObject, + ]); + $this->dispatcher->dispatch(AttestationObjectLoaded::create($attestationObject)); + + return $attestationObject; + } catch (Throwable $throwable) { + $this->logger->error('An error occurred', [ + 'exception' => $throwable, + ]); + throw $throwable; + } + } + + public function setLogger(LoggerInterface $logger): void + { + $this->logger = $logger; + } +} diff --git a/3rdparty/web-auth/webauthn-lib/src/AttestationStatement/AttestationStatement.php b/3rdparty/web-auth/webauthn-lib/src/AttestationStatement/AttestationStatement.php new file mode 100644 index 00000000..5285de2e --- /dev/null +++ b/3rdparty/web-auth/webauthn-lib/src/AttestationStatement/AttestationStatement.php @@ -0,0 +1,190 @@ + $attStmt + */ + public function __construct( + public readonly string $fmt, + public readonly array $attStmt, + public readonly string $type, + public readonly TrustPath $trustPath + ) { + } + + public static function create(string $fmt, array $attStmt, string $type, TrustPath $trustPath): self + { + return new self($fmt, $attStmt, $type, $trustPath); + } + + /** + * @param array $attStmt + */ + public static function createNone(string $fmt, array $attStmt, TrustPath $trustPath): self + { + return self::create($fmt, $attStmt, self::TYPE_NONE, $trustPath); + } + + /** + * @param array $attStmt + */ + public static function createBasic(string $fmt, array $attStmt, TrustPath $trustPath): self + { + return self::create($fmt, $attStmt, self::TYPE_BASIC, $trustPath); + } + + /** + * @param array $attStmt + */ + public static function createSelf(string $fmt, array $attStmt, TrustPath $trustPath): self + { + return self::create($fmt, $attStmt, self::TYPE_SELF, $trustPath); + } + + /** + * @param array $attStmt + */ + public static function createAttCA(string $fmt, array $attStmt, TrustPath $trustPath): self + { + return self::create($fmt, $attStmt, self::TYPE_ATTCA, $trustPath); + } + + /** + * @param array $attStmt + * + * @deprecated since 4.2.0 and will be removed in 5.0.0. The ECDAA Trust Anchor does no longer exist in Webauthn specification. + * @infection-ignore-all + */ + public static function createEcdaa(string $fmt, array $attStmt, TrustPath $trustPath): self + { + return self::create($fmt, $attStmt, self::TYPE_ECDAA, $trustPath); + } + + /** + * @param array $attStmt + */ + public static function createAnonymizationCA(string $fmt, array $attStmt, TrustPath $trustPath): self + { + return self::create($fmt, $attStmt, self::TYPE_ANONCA, $trustPath); + } + + /** + * @deprecated since 4.7.0. Please use the property directly. + * @infection-ignore-all + */ + public function getFmt(): string + { + return $this->fmt; + } + + /** + * @return mixed[] + * @deprecated since 4.7.0. Please use the property directly. + * @infection-ignore-all + */ + public function getAttStmt(): array + { + return $this->attStmt; + } + + public function has(string $key): bool + { + return array_key_exists($key, $this->attStmt); + } + + public function get(string $key): mixed + { + $this->has($key) || throw InvalidDataException::create($this->attStmt, sprintf( + 'The attestation statement has no key "%s".', + $key + )); + + return $this->attStmt[$key]; + } + + /** + * @deprecated since 4.7.0. Please use the property directly. + * @infection-ignore-all + */ + public function getTrustPath(): TrustPath + { + return $this->trustPath; + } + + /** + * @deprecated since 4.7.0. Please use the property directly. + * @infection-ignore-all + */ + public function getType(): string + { + return $this->type; + } + + /** + * @param mixed[] $data + * @deprecated since 4.8.0. Please use {Webauthn\Denormalizer\WebauthnSerializerFactory} for converting the object. + * @infection-ignore-all + */ + public static function createFromArray(array $data): self + { + foreach (['fmt', 'attStmt', 'trustPath', 'type'] as $key) { + array_key_exists($key, $data) || throw InvalidDataException::create($data, sprintf( + 'The key "%s" is missing', + $key + )); + } + + return self::create( + $data['fmt'], + $data['attStmt'], + $data['type'], + TrustPathLoader::loadTrustPath($data['trustPath']) + ); + } + + /** + * @return mixed[] + */ + public function jsonSerialize(): array + { + trigger_deprecation( + 'web-auth/webauthn-bundle', + '4.9.0', + 'The "%s" method is deprecated and will be removed in 5.0. Please use the serializer instead.', + __METHOD__ + ); + return [ + 'fmt' => $this->fmt, + 'attStmt' => $this->attStmt, + 'trustPath' => $this->trustPath, + 'type' => $this->type, + ]; + } +} diff --git a/3rdparty/web-auth/webauthn-lib/src/AttestationStatement/AttestationStatementSupport.php b/3rdparty/web-auth/webauthn-lib/src/AttestationStatement/AttestationStatementSupport.php new file mode 100644 index 00000000..05d8348e --- /dev/null +++ b/3rdparty/web-auth/webauthn-lib/src/AttestationStatement/AttestationStatementSupport.php @@ -0,0 +1,23 @@ + $attestation + */ + public function load(array $attestation): AttestationStatement; + + public function isValid( + string $clientDataJSONHash, + AttestationStatement $attestationStatement, + AuthenticatorData $authenticatorData + ): bool; +} diff --git a/3rdparty/web-auth/webauthn-lib/src/AttestationStatement/AttestationStatementSupportManager.php b/3rdparty/web-auth/webauthn-lib/src/AttestationStatement/AttestationStatementSupportManager.php new file mode 100644 index 00000000..08902881 --- /dev/null +++ b/3rdparty/web-auth/webauthn-lib/src/AttestationStatement/AttestationStatementSupportManager.php @@ -0,0 +1,51 @@ +add(new NoneAttestationStatementSupport()); + foreach ($attestationStatementSupports as $attestationStatementSupport) { + $this->add($attestationStatementSupport); + } + } + + /** + * @param AttestationStatementSupport[] $attestationStatementSupports + */ + public static function create(array $attestationStatementSupports = []): self + { + return new self($attestationStatementSupports); + } + + public function add(AttestationStatementSupport $attestationStatementSupport): void + { + $this->attestationStatementSupports[$attestationStatementSupport->name()] = $attestationStatementSupport; + } + + public function has(string $name): bool + { + return array_key_exists($name, $this->attestationStatementSupports); + } + + public function get(string $name): AttestationStatementSupport + { + $this->has($name) || throw InvalidDataException::create($name, sprintf( + 'The attestation statement format "%s" is not supported.', + $name + )); + + return $this->attestationStatementSupports[$name]; + } +} diff --git a/3rdparty/web-auth/webauthn-lib/src/AttestationStatement/FidoU2FAttestationStatementSupport.php b/3rdparty/web-auth/webauthn-lib/src/AttestationStatement/FidoU2FAttestationStatementSupport.php new file mode 100644 index 00000000..db4bf3a1 --- /dev/null +++ b/3rdparty/web-auth/webauthn-lib/src/AttestationStatement/FidoU2FAttestationStatementSupport.php @@ -0,0 +1,181 @@ +decoder = Decoder::create(); + $this->dispatcher = new NullEventDispatcher(); + } + + public function setEventDispatcher(EventDispatcherInterface $eventDispatcher): void + { + $this->dispatcher = $eventDispatcher; + } + + public static function create(): self + { + return new self(); + } + + public function name(): string + { + return 'fido-u2f'; + } + + /** + * @param array $attestation + */ + public function load(array $attestation): AttestationStatement + { + array_key_exists('attStmt', $attestation) || throw AttestationStatementLoadingException::create( + $attestation, + 'Invalid attestation object' + ); + foreach (['sig', 'x5c'] as $key) { + array_key_exists($key, $attestation['attStmt']) || throw AttestationStatementLoadingException::create( + $attestation, + sprintf('The attestation statement value "%s" is missing.', $key) + ); + } + $certificates = $attestation['attStmt']['x5c']; + is_array($certificates) || throw AttestationStatementLoadingException::create( + $attestation, + 'The attestation statement value "x5c" must be a list with one certificate.' + ); + count($certificates) === 1 || throw AttestationStatementLoadingException::create( + $attestation, + 'The attestation statement value "x5c" must be a list with one certificate.' + ); + + reset($certificates); + $certificates = CertificateToolbox::convertAllDERToPEM($certificates); + $this->checkCertificate($certificates[0]); + + $attestationStatement = AttestationStatement::createBasic( + $attestation['fmt'], + $attestation['attStmt'], + CertificateTrustPath::create($certificates) + ); + $this->dispatcher->dispatch(AttestationStatementLoaded::create($attestationStatement)); + + return $attestationStatement; + } + + public function isValid( + string $clientDataJSONHash, + AttestationStatement $attestationStatement, + AuthenticatorData $authenticatorData + ): bool { + $authenticatorData->attestedCredentialData + ?->aaguid + ->__toString() === '00000000-0000-0000-0000-000000000000' || throw InvalidAttestationStatementException::create( + $attestationStatement, + 'Invalid AAGUID for fido-u2f attestation statement. Shall be "00000000-0000-0000-0000-000000000000"' + ); + $trustPath = $attestationStatement->trustPath; + $trustPath instanceof CertificateTrustPath || throw InvalidAttestationStatementException::create( + $attestationStatement, + 'Invalid trust path' + ); + $dataToVerify = "\0"; + $dataToVerify .= $authenticatorData->rpIdHash; + $dataToVerify .= $clientDataJSONHash; + $dataToVerify .= $authenticatorData->attestedCredentialData + ->credentialId; + $dataToVerify .= $this->extractPublicKey($authenticatorData->attestedCredentialData ->credentialPublicKey); + + return openssl_verify( + $dataToVerify, + $attestationStatement->get('sig'), + $trustPath->certificates[0], + OPENSSL_ALGO_SHA256 + ) === 1; + } + + private function extractPublicKey(?string $publicKey): string + { + $publicKey !== null || throw AttestationStatementVerificationException::create( + 'The attested credential data does not contain a valid public key.' + ); + + $publicKeyStream = new StringStream($publicKey); + $coseKey = $this->decoder->decode($publicKeyStream); + $publicKeyStream->isEOF() || throw AttestationStatementVerificationException::create( + 'Invalid public key. Presence of extra bytes.' + ); + $publicKeyStream->close(); + $coseKey instanceof MapObject || throw AttestationStatementVerificationException::create( + 'The attested credential data does not contain a valid public key.' + ); + + $coseKey = $coseKey->normalize(); + $ec2Key = new Ec2Key($coseKey + [ + Ec2Key::TYPE => 2, + Ec2Key::DATA_CURVE => Ec2Key::CURVE_P256, + ]); + + return "\x04" . $ec2Key->x() . $ec2Key->y(); + } + + private function checkCertificate(string $publicKey): void + { + try { + $resource = openssl_pkey_get_public($publicKey); + $details = openssl_pkey_get_details($resource); + } catch (Throwable $throwable) { + throw AttestationStatementVerificationException::create( + 'Invalid certificate or certificate chain', + $throwable + ); + } + is_array($details) || throw AttestationStatementVerificationException::create( + 'Invalid certificate or certificate chain' + ); + array_key_exists('ec', $details) || throw AttestationStatementVerificationException::create( + 'Invalid certificate or certificate chain' + ); + array_key_exists('curve_name', $details['ec']) || throw AttestationStatementVerificationException::create( + 'Invalid certificate or certificate chain' + ); + $details['ec']['curve_name'] === 'prime256v1' || throw AttestationStatementVerificationException::create( + 'Invalid certificate or certificate chain' + ); + array_key_exists('curve_oid', $details['ec']) || throw AttestationStatementVerificationException::create( + 'Invalid certificate or certificate chain' + ); + $details['ec']['curve_oid'] === '1.2.840.10045.3.1.7' || throw AttestationStatementVerificationException::create( + 'Invalid certificate or certificate chain' + ); + } +} diff --git a/3rdparty/web-auth/webauthn-lib/src/AttestationStatement/NoneAttestationStatementSupport.php b/3rdparty/web-auth/webauthn-lib/src/AttestationStatement/NoneAttestationStatementSupport.php new file mode 100644 index 00000000..a28e3e6c --- /dev/null +++ b/3rdparty/web-auth/webauthn-lib/src/AttestationStatement/NoneAttestationStatementSupport.php @@ -0,0 +1,78 @@ +dispatcher = new NullEventDispatcher(); + } + + public function setEventDispatcher(EventDispatcherInterface $eventDispatcher): void + { + $this->dispatcher = $eventDispatcher; + } + + public static function create(): self + { + return new self(); + } + + public function name(): string + { + return 'none'; + } + + /** + * @param array $attestation + */ + public function load(array $attestation): AttestationStatement + { + $format = $attestation['fmt'] ?? null; + $attestationStatement = $attestation['attStmt'] ?? []; + + (is_string($format) && $format !== '') || throw AttestationStatementLoadingException::create( + $attestation, + 'Invalid attestation object' + ); + (is_array( + $attestationStatement + ) && $attestationStatement === []) || throw AttestationStatementLoadingException::create( + $attestation, + 'Invalid attestation object' + ); + + $attestationStatement = AttestationStatement::createNone( + $format, + $attestationStatement, + EmptyTrustPath::create() + ); + $this->dispatcher->dispatch(AttestationStatementLoaded::create($attestationStatement)); + + return $attestationStatement; + } + + public function isValid( + string $clientDataJSONHash, + AttestationStatement $attestationStatement, + AuthenticatorData $authenticatorData + ): bool { + return count($attestationStatement->attStmt) === 0; + } +} diff --git a/3rdparty/web-auth/webauthn-lib/src/AttestationStatement/PackedAttestationStatementSupport.php b/3rdparty/web-auth/webauthn-lib/src/AttestationStatement/PackedAttestationStatementSupport.php new file mode 100644 index 00000000..7af35f58 --- /dev/null +++ b/3rdparty/web-auth/webauthn-lib/src/AttestationStatement/PackedAttestationStatementSupport.php @@ -0,0 +1,303 @@ +decoder = Decoder::create(); + $this->dispatcher = new NullEventDispatcher(); + } + + public function setEventDispatcher(EventDispatcherInterface $eventDispatcher): void + { + $this->dispatcher = $eventDispatcher; + } + + public static function create(Manager $algorithmManager): self + { + return new self($algorithmManager); + } + + public function name(): string + { + return 'packed'; + } + + /** + * @param array $attestation + */ + public function load(array $attestation): AttestationStatement + { + array_key_exists('sig', $attestation['attStmt']) || throw AttestationStatementLoadingException::create( + $attestation, + 'The attestation statement value "sig" is missing.' + ); + array_key_exists('alg', $attestation['attStmt']) || throw AttestationStatementLoadingException::create( + $attestation, + 'The attestation statement value "alg" is missing.' + ); + is_string($attestation['attStmt']['sig']) || throw AttestationStatementLoadingException::create( + $attestation, + 'The attestation statement value "sig" is missing.' + ); + + return match (true) { + array_key_exists('x5c', $attestation['attStmt']) => $this->loadBasicType($attestation), + array_key_exists('ecdaaKeyId', $attestation['attStmt']) => $this->loadEcdaaType($attestation['attStmt']), + default => $this->loadEmptyType($attestation), + }; + } + + public function isValid( + string $clientDataJSONHash, + AttestationStatement $attestationStatement, + AuthenticatorData $authenticatorData + ): bool { + $trustPath = $attestationStatement->trustPath; + + return match (true) { + $trustPath instanceof CertificateTrustPath => $this->processWithCertificate( + $clientDataJSONHash, + $attestationStatement, + $authenticatorData, + $trustPath + ), + $trustPath instanceof EcdaaKeyIdTrustPath => $this->processWithECDAA(), + $trustPath instanceof EmptyTrustPath => $this->processWithSelfAttestation( + $clientDataJSONHash, + $attestationStatement, + $authenticatorData + ), + default => throw InvalidAttestationStatementException::create( + $attestationStatement, + 'Unsupported attestation statement' + ), + }; + } + + /** + * @param mixed[] $attestation + */ + private function loadBasicType(array $attestation): AttestationStatement + { + $certificates = $attestation['attStmt']['x5c']; + is_array($certificates) || throw AttestationStatementVerificationException::create( + 'The attestation statement value "x5c" must be a list with at least one certificate.' + ); + count($certificates) > 0 || throw AttestationStatementVerificationException::create( + 'The attestation statement value "x5c" must be a list with at least one certificate.' + ); + $certificates = CertificateToolbox::convertAllDERToPEM($certificates); + + $attestationStatement = AttestationStatement::createBasic( + $attestation['fmt'], + $attestation['attStmt'], + CertificateTrustPath::create($certificates) + ); + $this->dispatcher->dispatch(AttestationStatementLoaded::create($attestationStatement)); + + return $attestationStatement; + } + + /** + * @param array $attestation + */ + private function loadEcdaaType(array $attestation): AttestationStatement + { + $ecdaaKeyId = $attestation['attStmt']['ecdaaKeyId']; + is_string($ecdaaKeyId) || throw AttestationStatementVerificationException::create( + 'The attestation statement value "ecdaaKeyId" is invalid.' + ); + + $attestationStatement = AttestationStatement::createEcdaa( + $attestation['fmt'], + $attestation['attStmt'], + new EcdaaKeyIdTrustPath($attestation['ecdaaKeyId']) + ); + $this->dispatcher->dispatch(AttestationStatementLoaded::create($attestationStatement)); + + return $attestationStatement; + } + + /** + * @param mixed[] $attestation + */ + private function loadEmptyType(array $attestation): AttestationStatement + { + $attestationStatement = AttestationStatement::createSelf( + $attestation['fmt'], + $attestation['attStmt'], + EmptyTrustPath::create() + ); + $this->dispatcher->dispatch(AttestationStatementLoaded::create($attestationStatement)); + + return $attestationStatement; + } + + private function checkCertificate(string $attestnCert, AuthenticatorData $authenticatorData): void + { + $parsed = openssl_x509_parse($attestnCert); + is_array($parsed) || throw AttestationStatementVerificationException::create('Invalid certificate'); + + //Check version + isset($parsed['version']) || throw AttestationStatementVerificationException::create( + 'Invalid certificate version' + ); + $parsed['version'] === 2 || throw AttestationStatementVerificationException::create( + 'Invalid certificate version' + ); + + //Check subject field + isset($parsed['name']) || throw AttestationStatementVerificationException::create( + 'Invalid certificate name. The Subject Organization Unit must be "Authenticator Attestation"' + ); + str_contains( + (string) $parsed['name'], + '/OU=Authenticator Attestation' + ) || throw AttestationStatementVerificationException::create( + 'Invalid certificate name. The Subject Organization Unit must be "Authenticator Attestation"' + ); + + //Check extensions + isset($parsed['extensions']) || throw AttestationStatementVerificationException::create( + 'Certificate extensions are missing' + ); + is_array($parsed['extensions']) || throw AttestationStatementVerificationException::create( + 'Certificate extensions are missing' + ); + + //Check certificate is not a CA cert + isset($parsed['extensions']['basicConstraints']) || throw AttestationStatementVerificationException::create( + 'The Basic Constraints extension must have the CA component set to false' + ); + $parsed['extensions']['basicConstraints'] === 'CA:FALSE' || throw AttestationStatementVerificationException::create( + 'The Basic Constraints extension must have the CA component set to false' + ); + + $attestedCredentialData = $authenticatorData->attestedCredentialData; + $attestedCredentialData !== null || throw AttestationStatementVerificationException::create( + 'No attested credential available' + ); + + // id-fido-gen-ce-aaguid OID check + if (in_array('1.3.6.1.4.1.45724.1.1.4', $parsed['extensions'], true)) { + hash_equals( + $attestedCredentialData->aaguid + ->toBinary(), + $parsed['extensions']['1.3.6.1.4.1.45724.1.1.4'] + ) || throw AttestationStatementVerificationException::create( + 'The value of the "aaguid" does not match with the certificate' + ); + } + } + + private function processWithCertificate( + string $clientDataJSONHash, + AttestationStatement $attestationStatement, + AuthenticatorData $authenticatorData, + CertificateTrustPath $trustPath + ): bool { + $certificates = $trustPath->certificates; + + // Check leaf certificate + $this->checkCertificate($certificates[0], $authenticatorData); + + // Get the COSE algorithm identifier and the corresponding OpenSSL one + $coseAlgorithmIdentifier = (int) $attestationStatement->get('alg'); + $opensslAlgorithmIdentifier = Algorithms::getOpensslAlgorithmFor($coseAlgorithmIdentifier); + + // Verification of the signature + $signedData = $authenticatorData->authData . $clientDataJSONHash; + $result = openssl_verify( + $signedData, + $attestationStatement->get('sig'), + $certificates[0], + $opensslAlgorithmIdentifier + ); + + return $result === 1; + } + + private function processWithECDAA(): never + { + throw UnsupportedFeatureException::create('ECDAA not supported'); + } + + private function processWithSelfAttestation( + string $clientDataJSONHash, + AttestationStatement $attestationStatement, + AuthenticatorData $authenticatorData + ): bool { + $attestedCredentialData = $authenticatorData->attestedCredentialData; + $attestedCredentialData !== null || throw AttestationStatementVerificationException::create( + 'No attested credential available' + ); + $credentialPublicKey = $attestedCredentialData->credentialPublicKey; + $credentialPublicKey !== null || throw AttestationStatementVerificationException::create( + 'No credential public key available' + ); + $publicKeyStream = new StringStream($credentialPublicKey); + $publicKey = $this->decoder->decode($publicKeyStream); + $publicKeyStream->isEOF() || throw AttestationStatementVerificationException::create( + 'Invalid public key. Presence of extra bytes.' + ); + $publicKeyStream->close(); + $publicKey instanceof MapObject || throw AttestationStatementVerificationException::create( + 'The attested credential data does not contain a valid public key.' + ); + $publicKey = $publicKey->normalize(); + $publicKey = new Key($publicKey); + $publicKey->alg() === (int) $attestationStatement->get( + 'alg' + ) || throw AttestationStatementVerificationException::create( + 'The algorithm of the attestation statement and the key are not identical.' + ); + + $dataToVerify = $authenticatorData->authData . $clientDataJSONHash; + $algorithm = $this->algorithmManager->get((int) $attestationStatement->get('alg')); + if (! $algorithm instanceof Signature) { + throw InvalidDataException::create($algorithm, 'Invalid algorithm'); + } + $signature = CoseSignatureFixer::fix($attestationStatement->get('sig'), $algorithm); + + return $algorithm->verify($dataToVerify, $publicKey, $signature); + } +} diff --git a/3rdparty/web-auth/webauthn-lib/src/AttestationStatement/TPMAttestationStatementSupport.php b/3rdparty/web-auth/webauthn-lib/src/AttestationStatement/TPMAttestationStatementSupport.php new file mode 100644 index 00000000..71a60ca6 --- /dev/null +++ b/3rdparty/web-auth/webauthn-lib/src/AttestationStatement/TPMAttestationStatementSupport.php @@ -0,0 +1,445 @@ +clock = $clock; + $this->dispatcher = new NullEventDispatcher(); + } + + public function setEventDispatcher(EventDispatcherInterface $eventDispatcher): void + { + $this->dispatcher = $eventDispatcher; + } + + public static function create(null|Clock|ClockInterface $clock = null): self + { + return new self($clock); + } + + public function name(): string + { + return 'tpm'; + } + + /** + * @param array $attestation + */ + public function load(array $attestation): AttestationStatement + { + array_key_exists('attStmt', $attestation) || throw AttestationStatementLoadingException::create( + $attestation, + 'Invalid attestation object' + ); + ! array_key_exists( + 'ecdaaKeyId', + $attestation['attStmt'] + ) || throw AttestationStatementLoadingException::create($attestation, 'ECDAA not supported'); + foreach (['ver', 'ver', 'sig', 'alg', 'certInfo', 'pubArea'] as $key) { + array_key_exists($key, $attestation['attStmt']) || throw AttestationStatementLoadingException::create( + $attestation, + sprintf('The attestation statement value "%s" is missing.', $key) + ); + } + $attestation['attStmt']['ver'] === '2.0' || throw AttestationStatementLoadingException::create( + $attestation, + 'Invalid attestation object' + ); + + $certInfo = $this->checkCertInfo($attestation['attStmt']['certInfo']); + bin2hex((string) $certInfo['type']) === '8017' || throw AttestationStatementLoadingException::create( + $attestation, + 'Invalid attestation object' + ); + + $pubArea = $this->checkPubArea($attestation['attStmt']['pubArea']); + $pubAreaAttestedNameAlg = mb_substr((string) $certInfo['attestedName'], 0, 2, '8bit'); + $pubAreaHash = hash( + $this->getTPMHash($pubAreaAttestedNameAlg), + (string) $attestation['attStmt']['pubArea'], + true + ); + $attestedName = $pubAreaAttestedNameAlg . $pubAreaHash; + $attestedName === $certInfo['attestedName'] || throw AttestationStatementLoadingException::create( + $attestation, + 'Invalid attested name' + ); + + $attestation['attStmt']['parsedCertInfo'] = $certInfo; + $attestation['attStmt']['parsedPubArea'] = $pubArea; + + $certificates = CertificateToolbox::convertAllDERToPEM($attestation['attStmt']['x5c']); + count($certificates) > 0 || throw AttestationStatementLoadingException::create( + $attestation, + 'The attestation statement value "x5c" must be a list with at least one certificate.' + ); + + $attestationStatement = AttestationStatement::createAttCA( + $this->name(), + $attestation['attStmt'], + CertificateTrustPath::create($certificates) + ); + $this->dispatcher->dispatch(AttestationStatementLoaded::create($attestationStatement)); + + return $attestationStatement; + } + + public function isValid( + string $clientDataJSONHash, + AttestationStatement $attestationStatement, + AuthenticatorData $authenticatorData + ): bool { + $attToBeSigned = $authenticatorData->authData . $clientDataJSONHash; + $attToBeSignedHash = hash( + Algorithms::getHashAlgorithmFor((int) $attestationStatement->get('alg')), + $attToBeSigned, + true + ); + $attestationStatement->get( + 'parsedCertInfo' + )['extraData'] === $attToBeSignedHash || throw InvalidAttestationStatementException::create( + $attestationStatement, + 'Invalid attestation hash' + ); + $credentialPublicKey = $authenticatorData->attestedCredentialData?->credentialPublicKey; + $credentialPublicKey !== null || throw InvalidAttestationStatementException::create( + $attestationStatement, + 'Not credential public key available in the attested credential data' + ); + $this->checkUniquePublicKey($attestationStatement->get('parsedPubArea')['unique'], $credentialPublicKey); + + return match (true) { + $attestationStatement->trustPath instanceof CertificateTrustPath => $this->processWithCertificate( + $attestationStatement, + $authenticatorData + ), + $attestationStatement->trustPath instanceof EcdaaKeyIdTrustPath => $this->processWithECDAA(), + default => throw InvalidAttestationStatementException::create( + $attestationStatement, + 'Unsupported attestation statement' + ), + }; + } + + private function checkUniquePublicKey(string $unique, string $cborPublicKey): void + { + $cborDecoder = Decoder::create(); + $publicKey = $cborDecoder->decode(new StringStream($cborPublicKey)); + $publicKey instanceof MapObject || throw AttestationStatementVerificationException::create( + 'Invalid public key' + ); + $key = Key::create($publicKey->normalize()); + + switch ($key->type()) { + case Key::TYPE_OKP: + $uniqueFromKey = (new OkpKey($key->getData()))->x(); + break; + case Key::TYPE_EC2: + $ec2Key = new Ec2Key($key->getData()); + $uniqueFromKey = "\x04" . $ec2Key->x() . $ec2Key->y(); + break; + case Key::TYPE_RSA: + $uniqueFromKey = (new RsaKey($key->getData()))->n(); + break; + default: + throw AttestationStatementVerificationException::create('Invalid or unsupported key type.'); + } + + $unique === $uniqueFromKey || throw AttestationStatementVerificationException::create( + 'Invalid pubArea.unique value' + ); + } + + /** + * @return mixed[] + */ + private function checkCertInfo(string $data): array + { + $certInfo = new StringStream($data); + + $magic = $certInfo->read(4); + bin2hex($magic) === 'ff544347' || throw AttestationStatementVerificationException::create( + 'Invalid attestation object' + ); + + $type = $certInfo->read(2); + + $qualifiedSignerLength = unpack('n', $certInfo->read(2))[1]; + $qualifiedSigner = $certInfo->read($qualifiedSignerLength); //Ignored + + $extraDataLength = unpack('n', $certInfo->read(2))[1]; + $extraData = $certInfo->read($extraDataLength); + + $clockInfo = $certInfo->read(17); //Ignore + + $firmwareVersion = $certInfo->read(8); + + $attestedNameLength = unpack('n', $certInfo->read(2))[1]; + $attestedName = $certInfo->read($attestedNameLength); + + $attestedQualifiedNameLength = unpack('n', $certInfo->read(2))[1]; + $attestedQualifiedName = $certInfo->read($attestedQualifiedNameLength); //Ignore + $certInfo->isEOF() || throw AttestationStatementVerificationException::create( + 'Invalid certificate information. Presence of extra bytes.' + ); + $certInfo->close(); + + return [ + 'magic' => $magic, + 'type' => $type, + 'qualifiedSigner' => $qualifiedSigner, + 'extraData' => $extraData, + 'clockInfo' => $clockInfo, + 'firmwareVersion' => $firmwareVersion, + 'attestedName' => $attestedName, + 'attestedQualifiedName' => $attestedQualifiedName, + ]; + } + + /** + * @return mixed[] + */ + private function checkPubArea(string $data): array + { + $pubArea = new StringStream($data); + + $type = $pubArea->read(2); + + $nameAlg = $pubArea->read(2); + + $objectAttributes = $pubArea->read(4); + + $authPolicyLength = unpack('n', $pubArea->read(2))[1]; + $authPolicy = $pubArea->read($authPolicyLength); + + $parameters = $this->getParameters($type, $pubArea); + + $unique = $this->getUnique($type, $pubArea); + $pubArea->isEOF() || throw AttestationStatementVerificationException::create( + 'Invalid public area. Presence of extra bytes.' + ); + $pubArea->close(); + + return [ + 'type' => $type, + 'nameAlg' => $nameAlg, + 'objectAttributes' => $objectAttributes, + 'authPolicy' => $authPolicy, + 'parameters' => $parameters, + 'unique' => $unique, + ]; + } + + /** + * @return mixed[] + */ + private function getParameters(string $type, StringStream $stream): array + { + return match (bin2hex($type)) { + '0001' => [ + 'symmetric' => $stream->read(2), + 'scheme' => $stream->read(2), + 'keyBits' => unpack('n', $stream->read(2))[1], + 'exponent' => $this->getExponent($stream->read(4)), + ], + '0023' => [ + 'symmetric' => $stream->read(2), + 'scheme' => $stream->read(2), + 'curveId' => $stream->read(2), + 'kdf' => $stream->read(2), + ], + default => throw AttestationStatementVerificationException::create('Unsupported type'), + }; + } + + private function getUnique(string $type, StringStream $stream): string + { + switch (bin2hex($type)) { + case '0001': + $uniqueLength = unpack('n', $stream->read(2))[1]; + return $stream->read($uniqueLength); + case '0023': + $xLen = unpack('n', $stream->read(2))[1]; + $x = $stream->read($xLen); + $yLen = unpack('n', $stream->read(2))[1]; + $y = $stream->read($yLen); + return "\04" . $x . $y; + default: + throw AttestationStatementVerificationException::create('Unsupported type'); + } + } + + private function getExponent(string $exponent): string + { + return bin2hex($exponent) === '00000000' ? Base64UrlSafe::decodeNoPadding('AQAB') : $exponent; + } + + private function getTPMHash(string $nameAlg): string + { + return match (bin2hex($nameAlg)) { + '0004' => 'sha1', + '000b' => 'sha256', + '000c' => 'sha384', + '000d' => 'sha512', + default => throw AttestationStatementVerificationException::create('Unsupported hash algorithm'), + }; + } + + private function processWithCertificate( + AttestationStatement $attestationStatement, + AuthenticatorData $authenticatorData + ): bool { + $trustPath = $attestationStatement->trustPath; + $trustPath instanceof CertificateTrustPath || throw AttestationStatementVerificationException::create( + 'Invalid trust path' + ); + + $certificates = $trustPath->certificates; + + // Check certificate CA chain and returns the Attestation Certificate + $this->checkCertificate($certificates[0], $authenticatorData); + + // Get the COSE algorithm identifier and the corresponding OpenSSL one + $coseAlgorithmIdentifier = (int) $attestationStatement->get('alg'); + $opensslAlgorithmIdentifier = Algorithms::getOpensslAlgorithmFor($coseAlgorithmIdentifier); + + $result = openssl_verify( + $attestationStatement->get('certInfo'), + $attestationStatement->get('sig'), + $certificates[0], + $opensslAlgorithmIdentifier + ); + + return $result === 1; + } + + private function checkCertificate(string $attestnCert, AuthenticatorData $authenticatorData): void + { + $parsed = openssl_x509_parse($attestnCert); + is_array($parsed) || throw AttestationStatementVerificationException::create('Invalid certificate'); + + //Check version + (isset($parsed['version']) && $parsed['version'] === 2) || throw AttestationStatementVerificationException::create( + 'Invalid certificate version' + ); + + //Check subject field is empty + isset($parsed['subject']) || throw AttestationStatementVerificationException::create( + 'Invalid certificate name. The Subject should be empty' + ); + is_array($parsed['subject']) || throw AttestationStatementVerificationException::create( + 'Invalid certificate name. The Subject should be empty' + ); + count($parsed['subject']) === 0 || throw AttestationStatementVerificationException::create( + 'Invalid certificate name. The Subject should be empty' + ); + + // Check period of validity + array_key_exists( + 'validFrom_time_t', + $parsed + ) || throw AttestationStatementVerificationException::create('Invalid certificate start date.'); + is_int($parsed['validFrom_time_t']) || throw AttestationStatementVerificationException::create( + 'Invalid certificate start date.' + ); + $startDate = (new DateTimeImmutable())->setTimestamp($parsed['validFrom_time_t']); + $startDate < $this->clock->now() || throw AttestationStatementVerificationException::create( + 'Invalid certificate start date.' + ); + + array_key_exists('validTo_time_t', $parsed) || throw AttestationStatementVerificationException::create( + 'Invalid certificate end date.' + ); + is_int($parsed['validTo_time_t']) || throw AttestationStatementVerificationException::create( + 'Invalid certificate end date.' + ); + $endDate = (new DateTimeImmutable())->setTimestamp($parsed['validTo_time_t']); + $endDate > $this->clock->now() || throw AttestationStatementVerificationException::create( + 'Invalid certificate end date.' + ); + + //Check extensions + (isset($parsed['extensions']) && is_array( + $parsed['extensions'] + )) || throw AttestationStatementVerificationException::create('Certificate extensions are missing'); + + //Check subjectAltName + isset($parsed['extensions']['subjectAltName']) || throw AttestationStatementVerificationException::create( + 'The "subjectAltName" is missing' + ); + + //Check extendedKeyUsage + isset($parsed['extensions']['extendedKeyUsage']) || throw AttestationStatementVerificationException::create( + 'The "subjectAltName" is missing' + ); + $parsed['extensions']['extendedKeyUsage'] === '2.23.133.8.3' || throw AttestationStatementVerificationException::create( + 'The "extendedKeyUsage" is invalid' + ); + + // id-fido-gen-ce-aaguid OID check + in_array('1.3.6.1.4.1.45724.1.1.4', $parsed['extensions'], true) && ! hash_equals( + $authenticatorData->attestedCredentialData + ?->aaguid + ->toBinary() ?? '', + $parsed['extensions']['1.3.6.1.4.1.45724.1.1.4'] + ) && throw AttestationStatementVerificationException::create( + 'The value of the "aaguid" does not match with the certificate' + ); + } + + private function processWithECDAA(): never + { + throw UnsupportedFeatureException::create('ECDAA not supported'); + } +} diff --git a/3rdparty/web-auth/webauthn-lib/src/AttestedCredentialData.php b/3rdparty/web-auth/webauthn-lib/src/AttestedCredentialData.php new file mode 100644 index 00000000..a97f3fa4 --- /dev/null +++ b/3rdparty/web-auth/webauthn-lib/src/AttestedCredentialData.php @@ -0,0 +1,128 @@ +aaguid; + } + + /** + * @deprecated since 4.7.0. Please use the property directly. + * @infection-ignore-all + */ + public function setAaguid(Uuid $aaguid): void + { + $this->aaguid = $aaguid; + } + + /** + * @deprecated since 4.7.0. Please use the property directly. + * @infection-ignore-all + */ + public function getCredentialId(): string + { + return $this->credentialId; + } + + /** + * @deprecated since 4.7.0. Please use the property directly. + * @infection-ignore-all + */ + public function getCredentialPublicKey(): ?string + { + return $this->credentialPublicKey; + } + + /** + * @param mixed[] $json + * @deprecated since 4.9.0 and will be removed in 5.0.0. Please use the serializer instead. + */ + public static function createFromArray(array $json): self + { + array_key_exists('aaguid', $json) || throw InvalidDataException::create( + $json, + 'Invalid input. "aaguid" is missing.' + ); + $aaguid = $json['aaguid']; + is_string($aaguid) || throw InvalidDataException::create( + $json, + 'Invalid input. "aaguid" shall be a string of 36 characters' + ); + mb_strlen($aaguid, '8bit') === 36 || throw InvalidDataException::create( + $json, + 'Invalid input. "aaguid" shall be a string of 36 characters' + ); + $uuid = Uuid::fromString($aaguid); + + array_key_exists('credentialId', $json) || throw InvalidDataException::create( + $json, + 'Invalid input. "credentialId" is missing.' + ); + $credentialId = $json['credentialId']; + is_string($credentialId) || throw InvalidDataException::create( + $json, + 'Invalid input. "credentialId" shall be a string' + ); + $credentialId = Base64::decode($credentialId, true); + + $credentialPublicKey = null; + if (isset($json['credentialPublicKey'])) { + $credentialPublicKey = Base64::decode($json['credentialPublicKey'], true); + } + + return self::create($uuid, $credentialId, $credentialPublicKey); + } + + /** + * @return mixed[] + */ + public function jsonSerialize(): array + { + trigger_deprecation( + 'web-auth/webauthn-bundle', + '4.9.0', + 'The "%s" method is deprecated and will be removed in 5.0. Please use the serializer instead.', + __METHOD__ + ); + $result = [ + 'aaguid' => $this->aaguid->__toString(), + 'credentialId' => base64_encode($this->credentialId), + ]; + if ($this->credentialPublicKey !== null) { + $result['credentialPublicKey'] = base64_encode($this->credentialPublicKey); + } + + return $result; + } +} diff --git a/3rdparty/web-auth/webauthn-lib/src/AuthenticationExtensions/AuthenticationExtension.php b/3rdparty/web-auth/webauthn-lib/src/AuthenticationExtensions/AuthenticationExtension.php new file mode 100644 index 00000000..384476a1 --- /dev/null +++ b/3rdparty/web-auth/webauthn-lib/src/AuthenticationExtensions/AuthenticationExtension.php @@ -0,0 +1,50 @@ +name; + } + + /** + * @deprecated since 4.7.0. Please use the property directly. + * @infection-ignore-all + */ + public function value(): mixed + { + return $this->value; + } + + public function jsonSerialize(): mixed + { + trigger_deprecation( + 'web-auth/webauthn-bundle', + '4.9.0', + 'The "%s" method is deprecated and will be removed in 5.0. Please use the serializer instead.', + __METHOD__ + ); + return $this->value; + } +} diff --git a/3rdparty/web-auth/webauthn-lib/src/AuthenticationExtensions/AuthenticationExtensions.php b/3rdparty/web-auth/webauthn-lib/src/AuthenticationExtensions/AuthenticationExtensions.php new file mode 100644 index 00000000..2ef2bc65 --- /dev/null +++ b/3rdparty/web-auth/webauthn-lib/src/AuthenticationExtensions/AuthenticationExtensions.php @@ -0,0 +1,164 @@ + + * @final + */ +class AuthenticationExtensions implements JsonSerializable, Countable, IteratorAggregate, ArrayAccess +{ + /** + * @var array + * @readonly + */ + public array $extensions; + + /** + * @param array $extensions + */ + public function __construct(array $extensions = []) + { + $list = []; + foreach ($extensions as $key => $extension) { + if ($extension instanceof AuthenticationExtension) { + $list[$extension->name] = $extension; + + continue; + } + if (is_string($key)) { + $list[$key] = AuthenticationExtension::create($key, $extension); + continue; + } + throw new AuthenticationExtensionException('Invalid extension'); + } + $this->extensions = $list; + } + + /** + * @param array $extensions + */ + public static function create(array $extensions = []): static + { + return new static($extensions); + } + + /** + * @deprecated since 4.7.0. Please use the property directly. + * @infection-ignore-all + */ + public function add(AuthenticationExtension ...$extensions): static + { + foreach ($extensions as $extension) { + $this->extensions[$extension->name] = $extension; + } + + return $this; + } + + /** + * @param array $json + * @deprecated since 4.8.0. Please use {Webauthn\Denormalizer\WebauthnSerializerFactory} for converting the object. + * @infection-ignore-all + */ + public static function createFromArray(array $json): static + { + return static::create( + array_map( + static fn (string $key, mixed $value): AuthenticationExtension => AuthenticationExtension::create( + $key, + $value + ), + array_keys($json), + $json + ) + ); + } + + public function has(string $key): bool + { + return array_key_exists($key, $this->extensions); + } + + public function get(string $key): AuthenticationExtension + { + $this->has($key) || throw AuthenticationExtensionException::create(sprintf( + 'The extension with key "%s" is not available', + $key + )); + + return $this->extensions[$key]; + } + + /** + * @return array + */ + public function jsonSerialize(): array + { + trigger_deprecation( + 'web-auth/webauthn-bundle', + '4.9.0', + 'The "%s" method is deprecated and will be removed in 5.0. Please use the serializer instead.', + __METHOD__ + ); + return $this->extensions; + } + + /** + * @return Iterator + */ + public function getIterator(): Iterator + { + return new ArrayIterator($this->extensions); + } + + public function count(int $mode = COUNT_NORMAL): int + { + return count($this->extensions, $mode); + } + + public function offsetExists(mixed $offset): bool + { + return array_key_exists($offset, $this->extensions); + } + + public function offsetGet(mixed $offset): mixed + { + return $this->extensions[$offset]; + } + + public function offsetSet(mixed $offset, mixed $value): void + { + if ($value === null) { + return; + } + if ($value instanceof AuthenticationExtension) { + $this->extensions[$value->name] = $value; + return; + } + if (is_string($offset)) { + $this->extensions[$offset] = AuthenticationExtension::create($offset, $value); + return; + } + throw new AuthenticationExtensionException('Invalid extension'); + } + + public function offsetUnset(mixed $offset): void + { + unset($this->extensions[$offset]); + } +} diff --git a/3rdparty/web-auth/webauthn-lib/src/AuthenticationExtensions/AuthenticationExtensionsClientInputs.php b/3rdparty/web-auth/webauthn-lib/src/AuthenticationExtensions/AuthenticationExtensionsClientInputs.php new file mode 100644 index 00000000..5e054840 --- /dev/null +++ b/3rdparty/web-auth/webauthn-lib/src/AuthenticationExtensions/AuthenticationExtensionsClientInputs.php @@ -0,0 +1,12 @@ +normalize(); + return AuthenticationExtensionsClientOutputs::create( + array_map( + fn (mixed $value, string $key) => AuthenticationExtension::create($key, $value), + $data, + array_keys($data) + ) + ); + } +} diff --git a/3rdparty/web-auth/webauthn-lib/src/AuthenticationExtensions/ExtensionOutputChecker.php b/3rdparty/web-auth/webauthn-lib/src/AuthenticationExtensions/ExtensionOutputChecker.php new file mode 100644 index 00000000..7dda0796 --- /dev/null +++ b/3rdparty/web-auth/webauthn-lib/src/AuthenticationExtensions/ExtensionOutputChecker.php @@ -0,0 +1,10 @@ +checkers[] = $checker; + } + + public function check(AuthenticationExtensions $inputs, AuthenticationExtensions $outputs): void + { + foreach ($this->checkers as $checker) { + $checker->check($inputs, $outputs); + } + } +} diff --git a/3rdparty/web-auth/webauthn-lib/src/AuthenticationExtensions/ExtensionOutputError.php b/3rdparty/web-auth/webauthn-lib/src/AuthenticationExtensions/ExtensionOutputError.php new file mode 100644 index 00000000..9adcc706 --- /dev/null +++ b/3rdparty/web-auth/webauthn-lib/src/AuthenticationExtensions/ExtensionOutputError.php @@ -0,0 +1,29 @@ +authenticationExtension; + } +} diff --git a/3rdparty/web-auth/webauthn-lib/src/AuthenticatorAssertionResponse.php b/3rdparty/web-auth/webauthn-lib/src/AuthenticatorAssertionResponse.php new file mode 100644 index 00000000..8b105a42 --- /dev/null +++ b/3rdparty/web-auth/webauthn-lib/src/AuthenticatorAssertionResponse.php @@ -0,0 +1,51 @@ +signature; + } + + /** + * @deprecated since 4.7.0. Please use the property directly. + * @infection-ignore-all + */ + public function getUserHandle(): ?string + { + return $this->userHandle; + } +} diff --git a/3rdparty/web-auth/webauthn-lib/src/AuthenticatorAssertionResponseValidator.php b/3rdparty/web-auth/webauthn-lib/src/AuthenticatorAssertionResponseValidator.php new file mode 100644 index 00000000..fa433b21 --- /dev/null +++ b/3rdparty/web-auth/webauthn-lib/src/AuthenticatorAssertionResponseValidator.php @@ -0,0 +1,325 @@ +publicKeyCredentialSourceRepository !== null) { + trigger_deprecation( + 'web-auth/webauthn-lib', + '4.6.0', + 'The parameter "$publicKeyCredentialSourceRepository" is deprecated since 4.6.0 and will be removed in 5.0.0. Please set "null" instead.' + ); + } + if ($this->tokenBindingHandler !== null) { + trigger_deprecation( + 'web-auth/webauthn-lib', + '4.3.0', + 'The parameter "$tokenBindingHandler" is deprecated since 4.3.0 and will be removed in 5.0.0. Please set "null" instead.' + ); + } + if ($extensionOutputCheckerHandler !== null) { + trigger_deprecation( + 'web-auth/webauthn-lib', + '4.8.0', + 'The parameter "$extensionOutputCheckerHandler" is deprecated since 4.8.0 and will be removed in 5.0.0. Please set "null" instead and inject a CheckExtensions object into the CeremonyStepManager.' + ); + } + if ($algorithmManager !== null) { + trigger_deprecation( + 'web-auth/webauthn-lib', + '4.8.0', + 'The parameter "$algorithmManager" is deprecated since 4.8.0 and will be removed in 5.0.0. Please set "null" instead and inject a CheckSignature object into the CeremonyStepManager.' + ); + } + $this->eventDispatcher = $eventDispatcher ?? new NullEventDispatcher(); + if ($eventDispatcher !== null) { + trigger_deprecation( + 'web-auth/webauthn-lib', + '4.5.0', + 'The parameter "$eventDispatcher" is deprecated since 4.5.0 will be removed in 5.0.0. Please use `setEventDispatcher` instead.' + ); + } + if ($this->ceremonyStepManager === null) { + trigger_deprecation( + 'web-auth/webauthn-lib', + '4.8.0', + 'The parameter "$ceremonyStepManager" will mandatory in 5.0.0. Please set a CeremonyStepManager object instead and set null for $algorithmManager and $extensionOutputCheckerHandler.' + ); + } + $this->logger = new NullLogger(); + + $this->ceremonyStepManagerFactory = new CeremonyStepManagerFactory(); + if ($extensionOutputCheckerHandler !== null) { + $this->ceremonyStepManagerFactory->setExtensionOutputCheckerHandler($extensionOutputCheckerHandler); + } + if ($algorithmManager !== null) { + $this->ceremonyStepManagerFactory->setAlgorithmManager($algorithmManager); + } + } + + public static function create( + null|PublicKeyCredentialSourceRepository $publicKeyCredentialSourceRepository = null, + null|TokenBindingHandler $tokenBindingHandler = null, + null|ExtensionOutputCheckerHandler $extensionOutputCheckerHandler = null, + null|Manager $algorithmManager = null, + null|EventDispatcherInterface $eventDispatcher = null, + null|CeremonyStepManager $ceremonyStepManager = null + ): self { + return new self( + $publicKeyCredentialSourceRepository, + $tokenBindingHandler, + $extensionOutputCheckerHandler, + $algorithmManager, + $eventDispatcher, + $ceremonyStepManager + ); + } + + /** + * @param string[] $securedRelyingPartyId + * + * @see https://www.w3.org/TR/webauthn/#verifying-assertion + */ + public function check( + string|PublicKeyCredentialSource $credentialId, + AuthenticatorAssertionResponse $authenticatorAssertionResponse, + PublicKeyCredentialRequestOptions $publicKeyCredentialRequestOptions, + ServerRequestInterface|string $request, + ?string $userHandle, + null|array $securedRelyingPartyId = null + ): PublicKeyCredentialSource { + if ($request instanceof ServerRequestInterface) { + trigger_deprecation( + 'web-auth/webauthn-lib', + '4.5.0', + sprintf( + 'Passing a %s to the method `check` of the class "%s" is deprecated since 4.5.0 and will be removed in 5.0.0. Please inject the host as a string instead.', + ServerRequestInterface::class, + self::class + ) + ); + } + if (is_string($credentialId)) { + trigger_deprecation( + 'web-auth/webauthn-lib', + '4.6.0', + sprintf( + 'Passing a string as first to the method `check` of the class "%s" is deprecated since 4.6.0. Please inject a %s object instead.', + self::class, + PublicKeyCredentialSource::class + ) + ); + } + if ($securedRelyingPartyId !== null) { + trigger_deprecation( + 'web-auth/webauthn-lib', + '4.8.0', + sprintf( + 'Passing a list or secured relying party IDs to the method `check` of the class "%s" is deprecated since 4.8.0 and will be removed in 5.0.0. Please inject a CheckOrigin into the CeremonyStepManager instead.', + self::class + ) + ); + } + + if ($credentialId instanceof PublicKeyCredentialSource) { + $publicKeyCredentialSource = $credentialId; + } else { + $this->publicKeyCredentialSourceRepository instanceof PublicKeyCredentialSourceRepository || throw AuthenticatorResponseVerificationException::create( + 'Please pass the Public Key Credential Source to the method "check".' + ); + $publicKeyCredentialSource = $this->publicKeyCredentialSourceRepository->findOneByCredentialId( + $credentialId + ); + } + $publicKeyCredentialSource !== null || throw AuthenticatorResponseVerificationException::create( + 'The credential ID is invalid.' + ); + $host = is_string($request) ? $request : $request->getUri() + ->getHost(); + + if ($this->ceremonyStepManager === null) { + $this->ceremonyStepManager = $this->ceremonyStepManagerFactory->requestCeremony($securedRelyingPartyId); + } + + try { + $this->logger->info('Checking the authenticator assertion response', [ + 'credentialId' => $credentialId, + 'publicKeyCredentialSource' => $publicKeyCredentialSource, + 'authenticatorAssertionResponse' => $authenticatorAssertionResponse, + 'publicKeyCredentialRequestOptions' => $publicKeyCredentialRequestOptions, + 'host' => $host, + 'userHandle' => $userHandle, + ]); + + $this->ceremonyStepManager->process( + $publicKeyCredentialSource, + $authenticatorAssertionResponse, + $publicKeyCredentialRequestOptions, + $userHandle, + $host + ); + + $publicKeyCredentialSource->counter = $authenticatorAssertionResponse->authenticatorData->signCount; //26.1. + $publicKeyCredentialSource->backupEligible = $authenticatorAssertionResponse->authenticatorData->isBackupEligible(); //26.2. + $publicKeyCredentialSource->backupStatus = $authenticatorAssertionResponse->authenticatorData->isBackedUp(); //26.2. + if ($publicKeyCredentialSource->uvInitialized === false) { + $publicKeyCredentialSource->uvInitialized = $authenticatorAssertionResponse->authenticatorData->isUserVerified(); //26.3. + } + /* + * 26.3. + * OPTIONALLY, if response.attestationObject is present, update credentialRecord.attestationObject to the value of response.attestationObject and update credentialRecord.attestationClientDataJSON to the value of response.clientDataJSON. + */ + + if (is_string( + $credentialId + ) && ($this->publicKeyCredentialSourceRepository instanceof PublicKeyCredentialSourceRepository)) { + $this->publicKeyCredentialSourceRepository->saveCredentialSource($publicKeyCredentialSource); + } + //All good. We can continue. + $this->logger->info('The assertion is valid'); + $this->logger->debug('Public Key Credential Source', [ + 'publicKeyCredentialSource' => $publicKeyCredentialSource, + ]); + $this->eventDispatcher->dispatch( + $this->createAuthenticatorAssertionResponseValidationSucceededEvent( + null, + $authenticatorAssertionResponse, + $publicKeyCredentialRequestOptions, + $host, + $userHandle, + $publicKeyCredentialSource + ) + ); + // 27. + return $publicKeyCredentialSource; + } catch (AuthenticatorResponseVerificationException $throwable) { + $this->logger->error('An error occurred', [ + 'exception' => $throwable, + ]); + $this->eventDispatcher->dispatch( + $this->createAuthenticatorAssertionResponseValidationFailedEvent( + $publicKeyCredentialSource, + $authenticatorAssertionResponse, + $publicKeyCredentialRequestOptions, + $host, + $userHandle, + $throwable + ) + ); + throw $throwable; + } + } + + public function setLogger(LoggerInterface $logger): void + { + $this->logger = $logger; + } + + public function setEventDispatcher(EventDispatcherInterface $eventDispatcher): void + { + $this->eventDispatcher = $eventDispatcher; + } + + /** + * @deprecated since 4.8.0 and will be removed in 5.0.0. Please inject a CheckCounter object into a CeremonyStepManager instead. + */ + public function setCounterChecker(CounterChecker $counterChecker): self + { + $this->ceremonyStepManagerFactory->setCounterChecker($counterChecker); + return $this; + } + + protected function createAuthenticatorAssertionResponseValidationSucceededEvent( + null|string $credentialId, + AuthenticatorAssertionResponse $authenticatorAssertionResponse, + PublicKeyCredentialRequestOptions $publicKeyCredentialRequestOptions, + ServerRequestInterface|string $host, + ?string $userHandle, + PublicKeyCredentialSource $publicKeyCredentialSource + ): AuthenticatorAssertionResponseValidationSucceededEvent { + if ($host instanceof ServerRequestInterface) { + trigger_deprecation( + 'web-auth/webauthn-lib', + '4.5.0', + sprintf( + 'Passing a %s to the method `createAuthenticatorAssertionResponseValidationSucceededEvent` of the class "%s" is deprecated since 4.5.0 and will be removed in 5.0.0. Please inject the host as a string instead.', + ServerRequestInterface::class, + self::class + ) + ); + } + return new AuthenticatorAssertionResponseValidationSucceededEvent( + $credentialId, + $authenticatorAssertionResponse, + $publicKeyCredentialRequestOptions, + $host, + $userHandle, + $publicKeyCredentialSource + ); + } + + protected function createAuthenticatorAssertionResponseValidationFailedEvent( + string|PublicKeyCredentialSource $publicKeyCredentialSource, + AuthenticatorAssertionResponse $authenticatorAssertionResponse, + PublicKeyCredentialRequestOptions $publicKeyCredentialRequestOptions, + ServerRequestInterface|string $host, + ?string $userHandle, + Throwable $throwable + ): AuthenticatorAssertionResponseValidationFailedEvent { + if ($host instanceof ServerRequestInterface) { + trigger_deprecation( + 'web-auth/webauthn-lib', + '4.5.0', + sprintf( + 'Passing a %s to the method `createAuthenticatorAssertionResponseValidationFailedEvent` of the class "%s" is deprecated since 4.5.0 and will be removed in 5.0.0. Please inject the host as a string instead.', + ServerRequestInterface::class, + self::class + ) + ); + } + return new AuthenticatorAssertionResponseValidationFailedEvent( + $publicKeyCredentialSource, + $authenticatorAssertionResponse, + $publicKeyCredentialRequestOptions, + $host, + $userHandle, + $throwable + ); + } +} diff --git a/3rdparty/web-auth/webauthn-lib/src/AuthenticatorAttestationResponse.php b/3rdparty/web-auth/webauthn-lib/src/AuthenticatorAttestationResponse.php new file mode 100644 index 00000000..875bc525 --- /dev/null +++ b/3rdparty/web-auth/webauthn-lib/src/AuthenticatorAttestationResponse.php @@ -0,0 +1,55 @@ +attestationObject; + } + + /** + * @deprecated since 4.7.0. Please use the property directly. + * @infection-ignore-all + * + * @return string[] + */ + public function getTransports(): array + { + return $this->transports; + } +} diff --git a/3rdparty/web-auth/webauthn-lib/src/AuthenticatorAttestationResponseValidator.php b/3rdparty/web-auth/webauthn-lib/src/AuthenticatorAttestationResponseValidator.php new file mode 100644 index 00000000..97d43402 --- /dev/null +++ b/3rdparty/web-auth/webauthn-lib/src/AuthenticatorAttestationResponseValidator.php @@ -0,0 +1,328 @@ +publicKeyCredentialSourceRepository !== null) { + trigger_deprecation( + 'web-auth/webauthn-lib', + '4.6.0', + 'The parameter "$publicKeyCredentialSourceRepository" is deprecated since 4.6.0 and will be removed in 5.0.0. Please set "null" instead.' + ); + } + if ($this->tokenBindingHandler !== null) { + trigger_deprecation( + 'web-auth/webauthn-lib', + '4.3.0', + 'The parameter "$tokenBindingHandler" is deprecated since 4.3.0 and will be removed in 5.0.0. Please set "null" instead.' + ); + } + if ($extensionOutputCheckerHandler !== null) { + trigger_deprecation( + 'web-auth/webauthn-lib', + '4.8.0', + 'The parameter "$extensionOutputCheckerHandler" is deprecated since 4.8.0 and will be removed in 5.0.0. Please set "null" instead and inject a CheckExtensions object into the CeremonyStepManager.' + ); + } + $this->eventDispatcher = $eventDispatcher ?? new NullEventDispatcher(); + if ($eventDispatcher !== null) { + trigger_deprecation( + 'web-auth/webauthn-lib', + '4.5.0', + 'The parameter "$eventDispatcher" is deprecated since 4.5.0 will be removed in 5.0.0. Please use `setEventDispatcher` instead.' + ); + } + if ($this->ceremonyStepManager === null) { + trigger_deprecation( + 'web-auth/webauthn-lib', + '4.8.0', + 'The parameter "$ceremonyStepManager" will mandatory in 5.0.0. Please set a CeremonyStepManager object instead and set null for $attestationStatementSupportManager and $extensionOutputCheckerHandler.' + ); + } + $this->logger = new NullLogger(); + $this->ceremonyStepManagerFactory = new CeremonyStepManagerFactory(); + if ($attestationStatementSupportManager !== null) { + $this->ceremonyStepManagerFactory->setAttestationStatementSupportManager( + $attestationStatementSupportManager + ); + trigger_deprecation( + 'web-auth/webauthn-lib', + '4.8.0', + 'The parameter "$attestationStatementSupportManager" is deprecated since 4.8.0 will be removed in 5.0.0. Please set a CheckAttestationFormatIsKnownAndValid object into CeremonyStepManager object instead.' + ); + } + if ($extensionOutputCheckerHandler !== null) { + $this->ceremonyStepManagerFactory->setExtensionOutputCheckerHandler($extensionOutputCheckerHandler); + } + } + + /** + * @private Will become private in 5.0.0 + */ + public static function create( + null|AttestationStatementSupportManager $attestationStatementSupportManager = null, + null|PublicKeyCredentialSourceRepository $publicKeyCredentialSourceRepository = null, + null|TokenBindingHandler $tokenBindingHandler = null, + null|ExtensionOutputCheckerHandler $extensionOutputCheckerHandler = null, + null|EventDispatcherInterface $eventDispatcher = null, + null|CeremonyStepManager $ceremonyStepManager = null, + ): self { + return new self( + $attestationStatementSupportManager, + $publicKeyCredentialSourceRepository, + $tokenBindingHandler, + $extensionOutputCheckerHandler, + $eventDispatcher, + $ceremonyStepManager + ); + } + + public function setLogger(LoggerInterface $logger): void + { + $this->logger = $logger; + } + + public function setEventDispatcher(EventDispatcherInterface $eventDispatcher): void + { + $this->eventDispatcher = $eventDispatcher; + } + + /** + * @deprecated since 4.8.0 and will be removed in 5.0.0. Please use the CheckMetadataStatement object from the CeremonyStepManager instead. + */ + public function setCertificateChainValidator(CertificateChainValidator $certificateChainValidator): self + { + $this->ceremonyStepManagerFactory->enableCertificateChainValidator($certificateChainValidator); + return $this; + } + + /** + * @deprecated since 4.8.0 and will be removed in 5.0.0. Please use the CheckMetadataStatement object from the CeremonyStepManager instead. + */ + public function enableMetadataStatementSupport( + MetadataStatementRepository $metadataStatementRepository, + StatusReportRepository $statusReportRepository, + CertificateChainValidator $certificateChainValidator + ): self { + $this->ceremonyStepManagerFactory->enableMetadataStatementSupport( + $metadataStatementRepository, + $statusReportRepository, + $certificateChainValidator + ); + return $this; + } + + /** + * @param string[] $securedRelyingPartyId + * + * @see https://www.w3.org/TR/webauthn/#registering-a-new-credential + */ + public function check( + AuthenticatorAttestationResponse $authenticatorAttestationResponse, + PublicKeyCredentialCreationOptions $publicKeyCredentialCreationOptions, + ServerRequestInterface|string $request, + null|array $securedRelyingPartyId = null, + ): PublicKeyCredentialSource { + if ($request instanceof ServerRequestInterface) { + trigger_deprecation( + 'web-auth/webauthn-lib', + '4.5.0', + sprintf( + 'Passing a %s to the method `check` of the class "%s" is deprecated since 4.5.0 and will be removed in 5.0.0. Please inject the host as a string instead.', + ServerRequestInterface::class, + self::class + ) + ); + } + if ($securedRelyingPartyId !== null) { + trigger_deprecation( + 'web-auth/webauthn-lib', + '4.8.0', + sprintf( + 'Passing a list or secured relying party IDs to the method `check` of the class "%s" is deprecated since 4.8.0 and will be removed in 5.0.0. Please inject the list instead.', + self::class + ) + ); + } + $host = is_string($request) ? $request : $request->getUri() + ->getHost(); + try { + $this->logger->info('Checking the authenticator attestation response', [ + 'authenticatorAttestationResponse' => $authenticatorAttestationResponse, + 'publicKeyCredentialCreationOptions' => $publicKeyCredentialCreationOptions, + 'host' => $host, + ]); + if ($this->ceremonyStepManager === null) { + $this->ceremonyStepManager = $this->ceremonyStepManagerFactory->creationCeremony( + $securedRelyingPartyId + ); + } + + $publicKeyCredentialSource = $this->createPublicKeyCredentialSource( + $authenticatorAttestationResponse, + $publicKeyCredentialCreationOptions + ); + + $this->ceremonyStepManager->process( + $publicKeyCredentialSource, + $authenticatorAttestationResponse, + $publicKeyCredentialCreationOptions, + $publicKeyCredentialCreationOptions->user->id, + $host + ); + + $publicKeyCredentialSource->counter = $authenticatorAttestationResponse->attestationObject->authData->signCount; + $publicKeyCredentialSource->backupEligible = $authenticatorAttestationResponse->attestationObject->authData->isBackupEligible(); + $publicKeyCredentialSource->backupStatus = $authenticatorAttestationResponse->attestationObject->authData->isBackedUp(); + $publicKeyCredentialSource->uvInitialized = $authenticatorAttestationResponse->attestationObject->authData->isUserVerified(); + + $this->logger->info('The attestation is valid'); + $this->logger->debug('Public Key Credential Source', [ + 'publicKeyCredentialSource' => $publicKeyCredentialSource, + ]); + $this->eventDispatcher->dispatch( + $this->createAuthenticatorAttestationResponseValidationSucceededEvent( + $authenticatorAttestationResponse, + $publicKeyCredentialCreationOptions, + $host, + $publicKeyCredentialSource + ) + ); + return $publicKeyCredentialSource; + } catch (Throwable $throwable) { + $this->logger->error('An error occurred', [ + 'exception' => $throwable, + ]); + $this->eventDispatcher->dispatch( + $this->createAuthenticatorAttestationResponseValidationFailedEvent( + $authenticatorAttestationResponse, + $publicKeyCredentialCreationOptions, + $host, + $throwable + ) + ); + throw $throwable; + } + } + + protected function createAuthenticatorAttestationResponseValidationSucceededEvent( + AuthenticatorAttestationResponse $authenticatorAttestationResponse, + PublicKeyCredentialCreationOptions $publicKeyCredentialCreationOptions, + ServerRequestInterface|string $host, + PublicKeyCredentialSource $publicKeyCredentialSource + ): AuthenticatorAttestationResponseValidationSucceededEvent { + if ($host instanceof ServerRequestInterface) { + trigger_deprecation( + 'web-auth/webauthn-lib', + '4.5.0', + sprintf( + 'Passing a %s to the method `createAuthenticatorAttestationResponseValidationSucceededEvent` of the class "%s" is deprecated since 4.5.0 and will be removed in 5.0.0. Please inject the host as a string instead.', + ServerRequestInterface::class, + self::class + ) + ); + } + return new AuthenticatorAttestationResponseValidationSucceededEvent( + $authenticatorAttestationResponse, + $publicKeyCredentialCreationOptions, + $host, + $publicKeyCredentialSource + ); + } + + protected function createAuthenticatorAttestationResponseValidationFailedEvent( + AuthenticatorAttestationResponse $authenticatorAttestationResponse, + PublicKeyCredentialCreationOptions $publicKeyCredentialCreationOptions, + ServerRequestInterface|string $host, + Throwable $throwable + ): AuthenticatorAttestationResponseValidationFailedEvent { + if ($host instanceof ServerRequestInterface) { + trigger_deprecation( + 'web-auth/webauthn-lib', + '4.5.0', + sprintf( + 'Passing a %s to the method `createAuthenticatorAttestationResponseValidationFailedEvent` of the class "%s" is deprecated since 4.5.0 and will be removed in 5.0.0. Please inject the host as a string instead.', + ServerRequestInterface::class, + self::class + ) + ); + } + return new AuthenticatorAttestationResponseValidationFailedEvent( + $authenticatorAttestationResponse, + $publicKeyCredentialCreationOptions, + $host, + $throwable + ); + } + + private function createPublicKeyCredentialSource( + AuthenticatorAttestationResponse $authenticatorAttestationResponse, + PublicKeyCredentialCreationOptions $publicKeyCredentialCreationOptions, + ): PublicKeyCredentialSource { + $attestationObject = $authenticatorAttestationResponse->attestationObject; + $attestedCredentialData = $attestationObject->authData->attestedCredentialData; + $attestedCredentialData !== null || throw AuthenticatorResponseVerificationException::create( + 'Not attested credential data' + ); + $credentialId = $attestedCredentialData->credentialId; + $credentialPublicKey = $attestedCredentialData->credentialPublicKey; + $credentialPublicKey !== null || throw AuthenticatorResponseVerificationException::create( + 'Not credential public key available in the attested credential data' + ); + $userHandle = $publicKeyCredentialCreationOptions->user->id; + $transports = $authenticatorAttestationResponse->transports; + + return PublicKeyCredentialSource::create( + $credentialId, + PublicKeyCredentialDescriptor::CREDENTIAL_TYPE_PUBLIC_KEY, + $transports, + $attestationObject->attStmt + ->type, + $attestationObject->attStmt + ->trustPath, + $attestedCredentialData->aaguid, + $credentialPublicKey, + $userHandle, + $attestationObject->authData + ->signCount, + ); + } +} diff --git a/3rdparty/web-auth/webauthn-lib/src/AuthenticatorData.php b/3rdparty/web-auth/webauthn-lib/src/AuthenticatorData.php new file mode 100644 index 00000000..f21ff062 --- /dev/null +++ b/3rdparty/web-auth/webauthn-lib/src/AuthenticatorData.php @@ -0,0 +1,140 @@ +authData; + } + + /** + * @deprecated since 4.7.0. Please use the property directly. + * @infection-ignore-all + */ + public function getRpIdHash(): string + { + return $this->rpIdHash; + } + + public function isUserPresent(): bool + { + return 0 !== (ord($this->flags) & self::FLAG_UP); + } + + public function isUserVerified(): bool + { + return 0 !== (ord($this->flags) & self::FLAG_UV); + } + + public function isBackupEligible(): bool + { + return 0 !== (ord($this->flags) & self::FLAG_BE); + } + + public function isBackedUp(): bool + { + return 0 !== (ord($this->flags) & self::FLAG_BS); + } + + public function hasAttestedCredentialData(): bool + { + return 0 !== (ord($this->flags) & self::FLAG_AT); + } + + public function hasExtensions(): bool + { + return 0 !== (ord($this->flags) & self::FLAG_ED); + } + + public function getReservedForFutureUse1(): int + { + return ord($this->flags) & self::FLAG_RFU1; + } + + public function getReservedForFutureUse2(): int + { + return ord($this->flags) & self::FLAG_RFU2; + } + + /** + * @deprecated since 4.7.0. Please use the property directly. + * @infection-ignore-all + */ + public function getSignCount(): int + { + return $this->signCount; + } + + /** + * @deprecated since 4.7.0. Please use the property directly. + * @infection-ignore-all + */ + public function getAttestedCredentialData(): ?AttestedCredentialData + { + return $this->attestedCredentialData; + } + + /** + * @deprecated since 4.7.0. Please use the property directly. + * @infection-ignore-all + */ + public function getExtensions(): ?AuthenticationExtensions + { + return $this->extensions !== null && $this->hasExtensions() ? $this->extensions : null; + } +} diff --git a/3rdparty/web-auth/webauthn-lib/src/AuthenticatorDataLoader.php b/3rdparty/web-auth/webauthn-lib/src/AuthenticatorDataLoader.php new file mode 100644 index 00000000..e09e6437 --- /dev/null +++ b/3rdparty/web-auth/webauthn-lib/src/AuthenticatorDataLoader.php @@ -0,0 +1,117 @@ +decoder = Decoder::create(); + } + + public static function create(): self + { + return new self(); + } + + public function load(string $authData): AuthenticatorData + { + $authData = $this->fixIncorrectEdDSAKey($authData); + $authDataStream = new StringStream($authData); + $rp_id_hash = $authDataStream->read(32); + $flags = $authDataStream->read(1); + $signCount = $authDataStream->read(4); + $signCount = unpack('N', $signCount); + + $attestedCredentialData = null; + if (0 !== (ord($flags) & AuthenticatorData::FLAG_AT)) { + $aaguid = Uuid::fromBinary($authDataStream->read(16)); + $credentialLength = $authDataStream->read(2); + $credentialLength = unpack('n', $credentialLength); + $credentialId = $authDataStream->read($credentialLength[1]); + $credentialPublicKey = $this->decoder->decode($authDataStream); + $credentialPublicKey instanceof MapObject || throw InvalidDataException::create( + $authData, + 'The data does not contain a valid credential public key.' + ); + $attestedCredentialData = AttestedCredentialData::create( + $aaguid, + $credentialId, + (string) $credentialPublicKey + ); + } + + $extension = null; + if (0 !== (ord($flags) & AuthenticatorData::FLAG_ED)) { + $extension = $this->decoder->decode($authDataStream); + $extension = AuthenticationExtensionsClientOutputsLoader::load($extension); + } + $authDataStream->isEOF() || throw InvalidDataException::create( + $authData, + 'Invalid authentication data. Presence of extra bytes.' + ); + $authDataStream->close(); + + return AuthenticatorData::create( + $authData, + $rp_id_hash, + $flags, + $signCount[1], + $attestedCredentialData, + $extension + ); + } + + private function fixIncorrectEdDSAKey(string $data): string + { + $needle = hex2bin('a301634f4b500327206745643235353139'); + $correct = hex2bin('a401634f4b500327206745643235353139'); + $position = mb_strpos($data, $needle, 0, '8bit'); + if ($position === false) { + return $data; + } + + $begin = mb_substr($data, 0, $position, '8bit'); + $end = mb_substr($data, $position, null, '8bit'); + $end = str_replace($needle, $correct, $end); + $cbor = new StringStream($end); + $badKey = $this->decoder->decode($cbor); + + ($badKey instanceof MapObject && $cbor->isEOF()) || throw InvalidDataException::create( + $end, + 'Invalid authentication data. Presence of extra bytes.' + ); + $badX = $badKey->get(-2); + $badX instanceof ListObject || throw InvalidDataException::create($end, 'Invalid authentication data.'); + $keyBytes = array_reduce( + $badX->normalize(), + static fn (string $carry, string $item): string => $carry . chr((int) $item), + '' + ); + $correctX = ByteStringObject::create($keyBytes); + $correctKey = MapObject::create() + ->add(UnsignedIntegerObject::create(1), TextStringObject::create('OKP')) + ->add(UnsignedIntegerObject::create(3), NegativeIntegerObject::create(-8)) + ->add(NegativeIntegerObject::create(-1), TextStringObject::create('Ed25519')) + ->add(NegativeIntegerObject::create(-2), $correctX); + + return $begin . $correctKey; + } +} diff --git a/3rdparty/web-auth/webauthn-lib/src/AuthenticatorResponse.php b/3rdparty/web-auth/webauthn-lib/src/AuthenticatorResponse.php new file mode 100644 index 00000000..162c24db --- /dev/null +++ b/3rdparty/web-auth/webauthn-lib/src/AuthenticatorResponse.php @@ -0,0 +1,25 @@ +clientDataJSON; + } +} diff --git a/3rdparty/web-auth/webauthn-lib/src/AuthenticatorSelectionCriteria.php b/3rdparty/web-auth/webauthn-lib/src/AuthenticatorSelectionCriteria.php new file mode 100644 index 00000000..ac794688 --- /dev/null +++ b/3rdparty/web-auth/webauthn-lib/src/AuthenticatorSelectionCriteria.php @@ -0,0 +1,252 @@ +residentKey === null && $this->requireResidentKey === true) { + $this->residentKey = self::RESIDENT_KEY_REQUIREMENT_REQUIRED; + } + $this->requireResidentKey = $requireResidentKey ?? ($residentKey === null ? null : $residentKey === self::RESIDENT_KEY_REQUIREMENT_REQUIRED); + } + + public static function create( + ?string $authenticatorAttachment = null, + string $userVerification = self::USER_VERIFICATION_REQUIREMENT_PREFERRED, + null|string $residentKey = self::RESIDENT_KEY_REQUIREMENT_NO_PREFERENCE, + null|bool $requireResidentKey = null + ): self { + return new self($authenticatorAttachment, $userVerification, $residentKey, $requireResidentKey); + } + + /** + * @deprecated since 4.7.0. Please use the {self::create} instead. + * @infection-ignore-all + */ + public function setAuthenticatorAttachment(?string $authenticatorAttachment): self + { + $this->authenticatorAttachment = $authenticatorAttachment; + + return $this; + } + + /** + * @deprecated since v4.1. Please use the {self::create} instead. + * @infection-ignore-all + */ + public function setRequireResidentKey(bool $requireResidentKey): self + { + $this->requireResidentKey = $requireResidentKey; + if ($requireResidentKey === true) { + $this->residentKey = self::RESIDENT_KEY_REQUIREMENT_REQUIRED; + } + + return $this; + } + + /** + * @deprecated since 4.7.0. Please use the {self::create} instead. + * @infection-ignore-all + */ + public function setUserVerification(string $userVerification): self + { + $this->userVerification = $userVerification; + + return $this; + } + + /** + * @deprecated since 4.7.0. Please use the {self::create} instead. + * @infection-ignore-all + */ + public function setResidentKey(null|string $residentKey): self + { + $this->residentKey = $residentKey; + $this->requireResidentKey = $residentKey === self::RESIDENT_KEY_REQUIREMENT_REQUIRED; + + return $this; + } + + /** + * @deprecated since 4.7.0. Please use the property directly. + * @infection-ignore-all + */ + public function getAuthenticatorAttachment(): ?string + { + return $this->authenticatorAttachment; + } + + /** + * @deprecated Will be removed in 5.0. Please use the property directly. + * @infection-ignore-all + */ + public function isRequireResidentKey(): bool + { + return $this->requireResidentKey; + } + + /** + * @deprecated since 4.7.0. Please use the property directly. + * @infection-ignore-all + */ + public function getUserVerification(): string + { + return $this->userVerification; + } + + /** + * @deprecated since 4.7.0. Please use the property directly. + * @infection-ignore-all + */ + public function getResidentKey(): null|string + { + return $this->residentKey; + } + + /** + * @deprecated since 4.8.0. Please use {Webauthn\Denormalizer\WebauthnSerializerFactory} for converting the object. + * @infection-ignore-all + */ + public static function createFromString(string $data): self + { + $data = json_decode($data, true, flags: JSON_THROW_ON_ERROR); + + return self::createFromArray($data); + } + + /** + * @param mixed[] $json + * @deprecated since 4.8.0. Please use {Webauthn\Denormalizer\WebauthnSerializerFactory} for converting the object. + * @infection-ignore-all + */ + public static function createFromArray(array $json): self + { + $authenticatorAttachment = $json['authenticatorAttachment'] ?? null; + $requireResidentKey = $json['requireResidentKey'] ?? null; + $userVerification = $json['userVerification'] ?? self::USER_VERIFICATION_REQUIREMENT_PREFERRED; + $residentKey = $json['residentKey'] ?? null; + + $authenticatorAttachment === null || is_string($authenticatorAttachment) || throw InvalidDataException::create( + $json, + 'Invalid "authenticatorAttachment" value' + ); + ($requireResidentKey === null || is_bool($requireResidentKey)) || throw InvalidDataException::create( + $json, + 'Invalid "requireResidentKey" value' + ); + is_string($userVerification) || throw InvalidDataException::create($json, 'Invalid "userVerification" value'); + ($residentKey === null || is_string($residentKey)) || throw InvalidDataException::create( + $json, + 'Invalid "residentKey" value' + ); + + return self::create( + $authenticatorAttachment ?? null, + $userVerification, + $residentKey, + $requireResidentKey, + ); + } + + /** + * @return mixed[] + */ + public function jsonSerialize(): array + { + trigger_deprecation( + 'web-auth/webauthn-bundle', + '4.9.0', + 'The "%s" method is deprecated and will be removed in 5.0. Please use the serializer instead.', + __METHOD__ + ); + $json = [ + 'requireResidentKey' => $this->requireResidentKey, + 'userVerification' => $this->userVerification, + 'residentKey' => $this->residentKey, + 'authenticatorAttachment' => $this->authenticatorAttachment, + ]; + foreach ($json as $key => $value) { + if ($value === null) { + unset($json[$key]); + } + } + + return $json; + } +} diff --git a/3rdparty/web-auth/webauthn-lib/src/CeremonyStep/CeremonyStep.php b/3rdparty/web-auth/webauthn-lib/src/CeremonyStep/CeremonyStep.php new file mode 100644 index 00000000..d015e00a --- /dev/null +++ b/3rdparty/web-auth/webauthn-lib/src/CeremonyStep/CeremonyStep.php @@ -0,0 +1,22 @@ +steps as $step) { + $step->process( + $publicKeyCredentialSource, + $authenticatorResponse, + $publicKeyCredentialOptions, + $userHandle, + $host + ); + } + } +} diff --git a/3rdparty/web-auth/webauthn-lib/src/CeremonyStep/CeremonyStepManagerFactory.php b/3rdparty/web-auth/webauthn-lib/src/CeremonyStep/CeremonyStepManagerFactory.php new file mode 100644 index 00000000..cc58ae3b --- /dev/null +++ b/3rdparty/web-auth/webauthn-lib/src/CeremonyStep/CeremonyStepManagerFactory.php @@ -0,0 +1,159 @@ +counterChecker = new ThrowExceptionIfInvalid(); + $this->algorithmManager = Manager::create()->add(ES256::create(), RS256::create()); + $this->attestationStatementSupportManager = new AttestationStatementSupportManager([ + new NoneAttestationStatementSupport(), + ]); + $this->extensionOutputCheckerHandler = new ExtensionOutputCheckerHandler(); + } + + public function setCounterChecker(CounterChecker $counterChecker): void + { + $this->counterChecker = $counterChecker; + } + + /** + * @param string[] $securedRelyingPartyId + */ + public function setSecuredRelyingPartyId(array $securedRelyingPartyId): void + { + $this->securedRelyingPartyId = $securedRelyingPartyId; + } + + public function setExtensionOutputCheckerHandler(ExtensionOutputCheckerHandler $extensionOutputCheckerHandler): void + { + $this->extensionOutputCheckerHandler = $extensionOutputCheckerHandler; + } + + public function setAttestationStatementSupportManager( + AttestationStatementSupportManager $attestationStatementSupportManager + ): void { + $this->attestationStatementSupportManager = $attestationStatementSupportManager; + } + + public function setAlgorithmManager(Manager $algorithmManager): void + { + $this->algorithmManager = $algorithmManager; + } + + public function enableMetadataStatementSupport( + MetadataStatementRepository $metadataStatementRepository, + StatusReportRepository $statusReportRepository, + CertificateChainValidator $certificateChainValidator + ): void { + $this->metadataStatementRepository = $metadataStatementRepository; + $this->statusReportRepository = $statusReportRepository; + $this->certificateChainValidator = $certificateChainValidator; + } + + public function enableCertificateChainValidator(CertificateChainValidator $certificateChainValidator): void + { + $this->certificateChainValidator = $certificateChainValidator; + } + + public function enableTopOriginValidator(TopOriginValidator $topOriginValidator): void + { + $this->topOriginValidator = $topOriginValidator; + } + + /** + * @param null|string[] $securedRelyingPartyId + */ + public function creationCeremony(null|array $securedRelyingPartyId = null): CeremonyStepManager + { + $metadataStatementChecker = new CheckMetadataStatement(); + if ($this->certificateChainValidator !== null) { + $metadataStatementChecker->enableCertificateChainValidator($this->certificateChainValidator); + } + if ($this->metadataStatementRepository !== null && $this->statusReportRepository !== null && $this->certificateChainValidator !== null) { + $metadataStatementChecker->enableMetadataStatementSupport( + $this->metadataStatementRepository, + $this->statusReportRepository, + $this->certificateChainValidator, + ); + } + + /* @see https://www.w3.org/TR/webauthn-3/#sctn-registering-a-new-credential */ + return new CeremonyStepManager([ + new CheckClientDataCollectorType(), + new CheckChallenge(), + new CheckOrigin($this->securedRelyingPartyId ?? $securedRelyingPartyId ?? []), + new CheckTopOrigin($this->topOriginValidator), + new CheckRelyingPartyIdIdHash(), + new CheckUserWasPresent(), + new CheckUserVerification(), + new CheckBackupBitsAreConsistent(), + new CheckAlgorithm(), + new CheckExtensions($this->extensionOutputCheckerHandler), + new CheckAttestationFormatIsKnownAndValid($this->attestationStatementSupportManager), + new CheckHasAttestedCredentialData(), + $metadataStatementChecker, + new CheckCredentialId(), + ]); + } + + /** + * @param null|string[] $securedRelyingPartyId + */ + public function requestCeremony(null|array $securedRelyingPartyId = null): CeremonyStepManager + { + /* @see https://www.w3.org/TR/webauthn-3/#sctn-verifying-assertion */ + return new CeremonyStepManager([ + new CheckAllowedCredentialList(), + new CheckUserHandle(), + new CheckClientDataCollectorType(), + new CheckChallenge(), + new CheckOrigin($this->securedRelyingPartyId ?? $securedRelyingPartyId ?? []), + new CheckTopOrigin(null), + new CheckRelyingPartyIdIdHash(), + new CheckUserWasPresent(), + new CheckUserVerification(), + new CheckBackupBitsAreConsistent(), + new CheckExtensions($this->extensionOutputCheckerHandler), + new CheckSignature($this->algorithmManager), + new CheckCounter($this->counterChecker), + ]); + } +} diff --git a/3rdparty/web-auth/webauthn-lib/src/CeremonyStep/CheckAlgorithm.php b/3rdparty/web-auth/webauthn-lib/src/CeremonyStep/CheckAlgorithm.php new file mode 100644 index 00000000..9b5e39b2 --- /dev/null +++ b/3rdparty/web-auth/webauthn-lib/src/CeremonyStep/CheckAlgorithm.php @@ -0,0 +1,76 @@ +getAttestedCredentialData() +->credentialPublicKey; + $credentialPublicKey !== null || throw AuthenticatorResponseVerificationException::create( + 'No public key available.' + ); + $algorithms = array_map( + fn ($pubKeyCredParam) => $pubKeyCredParam->alg, + $publicKeyCredentialOptions->pubKeyCredParams + ); + if (count($algorithms) === 0) { + $algorithms = [Algorithms::COSE_ALGORITHM_ES256, Algorithms::COSE_ALGORITHM_RS256]; + } + $coseKey = $this->getCoseKey($credentialPublicKey); + in_array($coseKey->alg(), $algorithms, true) || throw AuthenticatorResponseVerificationException::create( + sprintf('Invalid algorithm. Expected one of %s but got %d', implode(', ', $algorithms), $coseKey->alg()) + ); + } + + private function getCoseKey(string $credentialPublicKey): Key + { + $isU2F = U2FPublicKey::isU2FKey($credentialPublicKey); + if ($isU2F === true) { + $credentialPublicKey = U2FPublicKey::convertToCoseKey($credentialPublicKey); + } + $stream = new StringStream($credentialPublicKey); + $credentialPublicKeyStream = Decoder::create()->decode($stream); + $stream->isEOF() || throw AuthenticatorResponseVerificationException::create( + 'Invalid key. Presence of extra bytes.' + ); + $stream->close(); + $credentialPublicKeyStream instanceof Normalizable || throw AuthenticatorResponseVerificationException::create( + 'Invalid attestation object. Unexpected object.' + ); + $normalizedData = $credentialPublicKeyStream->normalize(); + is_array($normalizedData) || throw AuthenticatorResponseVerificationException::create( + 'Invalid attestation object. Unexpected object.' + ); + /** @var array $normalizedData */ + + return Key::create($normalizedData); + } +} diff --git a/3rdparty/web-auth/webauthn-lib/src/CeremonyStep/CheckAllowedCredentialList.php b/3rdparty/web-auth/webauthn-lib/src/CeremonyStep/CheckAllowedCredentialList.php new file mode 100644 index 00000000..bd51f61e --- /dev/null +++ b/3rdparty/web-auth/webauthn-lib/src/CeremonyStep/CheckAllowedCredentialList.php @@ -0,0 +1,38 @@ +allowCredentials) === 0) { + return; + } + + foreach ($publicKeyCredentialOptions->allowCredentials as $allowedCredential) { + if (hash_equals($allowedCredential->id, $publicKeyCredentialSource->publicKeyCredentialId)) { + return; + } + } + throw AuthenticatorResponseVerificationException::create('The credential ID is not allowed.'); + } +} diff --git a/3rdparty/web-auth/webauthn-lib/src/CeremonyStep/CheckAttestationFormatIsKnownAndValid.php b/3rdparty/web-auth/webauthn-lib/src/CeremonyStep/CheckAttestationFormatIsKnownAndValid.php new file mode 100644 index 00000000..147ffcee --- /dev/null +++ b/3rdparty/web-auth/webauthn-lib/src/CeremonyStep/CheckAttestationFormatIsKnownAndValid.php @@ -0,0 +1,48 @@ +attestationObject; + if ($attestationObject === null) { + return; + } + + $fmt = $attestationObject->attStmt + ->fmt; + $this->attestationStatementSupportManager->has( + $fmt + ) || throw AuthenticatorResponseVerificationException::create('Unsupported attestation statement format.'); + + $attestationStatementSupport = $this->attestationStatementSupportManager->get($fmt); + $clientDataJSONHash = hash('sha256', $authenticatorResponse->clientDataJSON ->rawData, true); + $attestationStatementSupport->isValid( + $clientDataJSONHash, + $attestationObject->attStmt, + $attestationObject->authData + ) || throw AuthenticatorResponseVerificationException::create('Invalid attestation statement.'); + } +} diff --git a/3rdparty/web-auth/webauthn-lib/src/CeremonyStep/CheckBackupBitsAreConsistent.php b/3rdparty/web-auth/webauthn-lib/src/CeremonyStep/CheckBackupBitsAreConsistent.php new file mode 100644 index 00000000..dffa239c --- /dev/null +++ b/3rdparty/web-auth/webauthn-lib/src/CeremonyStep/CheckBackupBitsAreConsistent.php @@ -0,0 +1,31 @@ +authenticatorData : $authenticatorResponse->attestationObject->authData; + if ($authData->isBackupEligible()) { + return; + } + $authData->isBackedUp() !== true || throw AuthenticatorResponseVerificationException::create( + 'Backup up bit is set but the backup is not eligible.' + ); + } +} diff --git a/3rdparty/web-auth/webauthn-lib/src/CeremonyStep/CheckChallenge.php b/3rdparty/web-auth/webauthn-lib/src/CeremonyStep/CheckChallenge.php new file mode 100644 index 00000000..a1c05970 --- /dev/null +++ b/3rdparty/web-auth/webauthn-lib/src/CeremonyStep/CheckChallenge.php @@ -0,0 +1,31 @@ +challenge !== '' || throw AuthenticatorResponseVerificationException::create( + 'Invalid challenge.' + ); + hash_equals( + $publicKeyCredentialOptions->challenge, + $authenticatorResponse->clientDataJSON->challenge + ) || throw AuthenticatorResponseVerificationException::create('Invalid challenge.'); + } +} diff --git a/3rdparty/web-auth/webauthn-lib/src/CeremonyStep/CheckClientDataCollectorType.php b/3rdparty/web-auth/webauthn-lib/src/CeremonyStep/CheckClientDataCollectorType.php new file mode 100644 index 00000000..645cb904 --- /dev/null +++ b/3rdparty/web-auth/webauthn-lib/src/CeremonyStep/CheckClientDataCollectorType.php @@ -0,0 +1,41 @@ +clientDataCollectorManager = $clientDataCollectorManager ?? new ClientDataCollectorManager([ + new WebauthnAuthenticationCollector(), + ]); + } + + public function process( + PublicKeyCredentialSource $publicKeyCredentialSource, + AuthenticatorAssertionResponse|AuthenticatorAttestationResponse $authenticatorResponse, + PublicKeyCredentialRequestOptions|PublicKeyCredentialCreationOptions $publicKeyCredentialOptions, + ?string $userHandle, + string $host + ): void { + $this->clientDataCollectorManager->collect( + $authenticatorResponse->clientDataJSON, + $publicKeyCredentialOptions, + $authenticatorResponse, + $host + ); + } +} diff --git a/3rdparty/web-auth/webauthn-lib/src/CeremonyStep/CheckCounter.php b/3rdparty/web-auth/webauthn-lib/src/CeremonyStep/CheckCounter.php new file mode 100644 index 00000000..dcf84536 --- /dev/null +++ b/3rdparty/web-auth/webauthn-lib/src/CeremonyStep/CheckCounter.php @@ -0,0 +1,36 @@ +authenticatorData : $authenticatorResponse->attestationObject->authData; + $storedCounter = $publicKeyCredentialSource->counter; + $responseCounter = $authData->signCount; + if ($responseCounter !== 0 || $storedCounter !== 0) { + $this->counterChecker->check($publicKeyCredentialSource, $responseCounter); + } + $publicKeyCredentialSource->counter = $responseCounter; + } +} diff --git a/3rdparty/web-auth/webauthn-lib/src/CeremonyStep/CheckCredentialId.php b/3rdparty/web-auth/webauthn-lib/src/CeremonyStep/CheckCredentialId.php new file mode 100644 index 00000000..aeb20205 --- /dev/null +++ b/3rdparty/web-auth/webauthn-lib/src/CeremonyStep/CheckCredentialId.php @@ -0,0 +1,28 @@ +publicKeyCredentialId; + mb_strlen($credentialId) <= 1023 || throw new AuthenticatorResponseVerificationException( + 'Credential ID too long.' + ); + } +} diff --git a/3rdparty/web-auth/webauthn-lib/src/CeremonyStep/CheckExtensions.php b/3rdparty/web-auth/webauthn-lib/src/CeremonyStep/CheckExtensions.php new file mode 100644 index 00000000..b74aadea --- /dev/null +++ b/3rdparty/web-auth/webauthn-lib/src/CeremonyStep/CheckExtensions.php @@ -0,0 +1,37 @@ +authenticatorData : $authenticatorResponse->attestationObject->authData; + $extensionsClientOutputs = $authData->extensions; + if ($extensionsClientOutputs !== null) { + $this->extensionOutputCheckerHandler->check( + $publicKeyCredentialOptions->extensions, + $extensionsClientOutputs + ); + } + } +} diff --git a/3rdparty/web-auth/webauthn-lib/src/CeremonyStep/CheckHasAttestedCredentialData.php b/3rdparty/web-auth/webauthn-lib/src/CeremonyStep/CheckHasAttestedCredentialData.php new file mode 100644 index 00000000..cf1cd23d --- /dev/null +++ b/3rdparty/web-auth/webauthn-lib/src/CeremonyStep/CheckHasAttestedCredentialData.php @@ -0,0 +1,32 @@ +authenticatorData : $authenticatorResponse->attestationObject->authData; + $authData + ->hasAttestedCredentialData() || throw AuthenticatorResponseVerificationException::create( + 'There is no attested credential data.' + ); + $authData->attestedCredentialData !== null || throw AuthenticatorResponseVerificationException::create( + 'There is no attested credential data.' + ); + } +} diff --git a/3rdparty/web-auth/webauthn-lib/src/CeremonyStep/CheckMetadataStatement.php b/3rdparty/web-auth/webauthn-lib/src/CeremonyStep/CheckMetadataStatement.php new file mode 100644 index 00000000..ce482a4a --- /dev/null +++ b/3rdparty/web-auth/webauthn-lib/src/CeremonyStep/CheckMetadataStatement.php @@ -0,0 +1,190 @@ +logger = new NullLogger(); + } + + public function enableMetadataStatementSupport( + MetadataStatementRepository $metadataStatementRepository, + StatusReportRepository $statusReportRepository, + CertificateChainValidator $certificateChainValidator + ): void { + $this->metadataStatementRepository = $metadataStatementRepository; + $this->statusReportRepository = $statusReportRepository; + $this->certificateChainValidator = $certificateChainValidator; + } + + public function enableCertificateChainValidator(CertificateChainValidator $certificateChainValidator): void + { + $this->certificateChainValidator = $certificateChainValidator; + } + + public function setLogger(LoggerInterface $logger): void + { + $this->logger = $logger; + } + + public function process( + PublicKeyCredentialSource $publicKeyCredentialSource, + AuthenticatorAssertionResponse|AuthenticatorAttestationResponse $authenticatorResponse, + PublicKeyCredentialRequestOptions|PublicKeyCredentialCreationOptions $publicKeyCredentialOptions, + ?string $userHandle, + string $host + ): void { + if ( + ! $publicKeyCredentialOptions instanceof PublicKeyCredentialCreationOptions + || ! $authenticatorResponse instanceof AuthenticatorAttestationResponse + ) { + return; + } + + $attestationStatement = $authenticatorResponse->attestationObject->attStmt; + $attestedCredentialData = $authenticatorResponse->attestationObject->authData + ->attestedCredentialData; + $attestedCredentialData !== null || throw AuthenticatorResponseVerificationException::create( + 'No attested credential data found' + ); + $aaguid = $attestedCredentialData->aaguid + ->__toString(); + if ($publicKeyCredentialOptions->attestation === null || $publicKeyCredentialOptions->attestation === PublicKeyCredentialCreationOptions::ATTESTATION_CONVEYANCE_PREFERENCE_NONE) { + $this->logger->debug('No attestation is asked.'); + if ($aaguid === '00000000-0000-0000-0000-000000000000' && in_array( + $attestationStatement->type, + [AttestationStatement::TYPE_NONE, AttestationStatement::TYPE_SELF], + true + )) { + $this->logger->debug('The Attestation Statement is anonymous.'); + $this->checkCertificateChain($attestationStatement, null); + return; + } + return; + } + // If no Attestation Statement has been returned or if null AAGUID (=00000000-0000-0000-0000-000000000000) + // => nothing to check + if ($attestationStatement->type === AttestationStatement::TYPE_NONE) { + $this->logger->debug('No attestation returned.'); + //No attestation is returned. We shall ensure that the AAGUID is a null one. + //if ($aaguid !== '00000000-0000-0000-0000-000000000000') { + //$this->logger->debug('Anonymization required. AAGUID and Attestation Statement changed.', [ + // 'aaguid' => $aaguid, + // 'AttestationStatement' => $attestationStatement, + //]); + //$attestedCredentialData->aaguid = Uuid::fromString('00000000-0000-0000-0000-000000000000'); + // return; + //} + return; + } + if ($aaguid === '00000000-0000-0000-0000-000000000000') { + //No need to continue if the AAGUID is null. + // This could be the case e.g. with AnonCA type + return; + } + //The MDS Repository is mandatory here + $this->metadataStatementRepository !== null || throw AuthenticatorResponseVerificationException::create( + 'The Metadata Statement Repository is mandatory when requesting attestation objects.' + ); + $metadataStatement = $this->metadataStatementRepository->findOneByAAGUID($aaguid); + // At this point, the Metadata Statement is mandatory + $metadataStatement !== null || throw AuthenticatorResponseVerificationException::create( + sprintf('The Metadata Statement for the AAGUID "%s" is missing', $aaguid) + ); + // We check the last status report + $this->checkStatusReport($aaguid); + // We check the certificate chain (if any) + $this->checkCertificateChain($attestationStatement, $metadataStatement); + // Check Attestation Type is allowed + if (count($metadataStatement->attestationTypes) !== 0) { + $type = $this->getAttestationType($attestationStatement); + in_array( + $type, + $metadataStatement->attestationTypes, + true + ) || throw AuthenticatorResponseVerificationException::create( + sprintf( + 'Invalid attestation statement. The attestation type "%s" is not allowed for this authenticator.', + $type + ) + ); + } + } + + private function getAttestationType(AttestationStatement $attestationStatement): string + { + return match ($attestationStatement->type) { + AttestationStatement::TYPE_BASIC => MetadataStatement::ATTESTATION_BASIC_FULL, + AttestationStatement::TYPE_SELF => MetadataStatement::ATTESTATION_BASIC_SURROGATE, + AttestationStatement::TYPE_ATTCA => MetadataStatement::ATTESTATION_ATTCA, + AttestationStatement::TYPE_ECDAA => MetadataStatement::ATTESTATION_ECDAA, + AttestationStatement::TYPE_ANONCA => MetadataStatement::ATTESTATION_ANONCA, + default => throw AuthenticatorResponseVerificationException::create('Invalid attestation type'), + }; + } + + private function checkStatusReport(string $aaguid): void + { + $statusReports = $this->statusReportRepository === null ? [] : $this->statusReportRepository->findStatusReportsByAAGUID( + $aaguid + ); + if (count($statusReports) !== 0) { + $lastStatusReport = end($statusReports); + if ($lastStatusReport->isCompromised()) { + throw AuthenticatorResponseVerificationException::create( + 'The authenticator is compromised and cannot be used' + ); + } + } + } + + private function checkCertificateChain( + AttestationStatement $attestationStatement, + ?MetadataStatement $metadataStatement + ): void { + $trustPath = $attestationStatement->trustPath; + if (! $trustPath instanceof CertificateTrustPath) { + return; + } + $authenticatorCertificates = $trustPath->certificates; + if ($metadataStatement === null) { + $this->certificateChainValidator?->check($authenticatorCertificates, []); + return; + } + $trustedCertificates = CertificateToolbox::fixPEMStructures( + $metadataStatement->attestationRootCertificates + ); + $this->certificateChainValidator?->check($authenticatorCertificates, $trustedCertificates); + } +} diff --git a/3rdparty/web-auth/webauthn-lib/src/CeremonyStep/CheckOrigin.php b/3rdparty/web-auth/webauthn-lib/src/CeremonyStep/CheckOrigin.php new file mode 100644 index 00000000..950cf6f8 --- /dev/null +++ b/3rdparty/web-auth/webauthn-lib/src/CeremonyStep/CheckOrigin.php @@ -0,0 +1,78 @@ +authenticatorData : $authenticatorResponse->attestationObject->authData; + $C = $authenticatorResponse->clientDataJSON; + $rpId = $publicKeyCredentialOptions->rpId ?? $publicKeyCredentialOptions->rp->id ?? $host; + $facetId = $this->getFacetId($rpId, $publicKeyCredentialOptions->extensions, $authData->extensions); + $parsedRelyingPartyId = parse_url($C->origin); + is_array($parsedRelyingPartyId) || throw AuthenticatorResponseVerificationException::create( + 'Invalid origin' + ); + if (! in_array($facetId, $this->securedRelyingPartyId, true)) { + $scheme = $parsedRelyingPartyId['scheme'] ?? ''; + $scheme === 'https' || throw AuthenticatorResponseVerificationException::create( + 'Invalid scheme. HTTPS required.' + ); + } + $clientDataRpId = $parsedRelyingPartyId['host'] ?? ''; + $clientDataRpId !== '' || throw AuthenticatorResponseVerificationException::create('Invalid origin rpId.'); + $rpIdLength = mb_strlen($facetId); + + mb_substr( + '.' . $clientDataRpId, + -($rpIdLength + 1) + ) === '.' . $facetId || throw AuthenticatorResponseVerificationException::create('rpId mismatch.'); + } + + private function getFacetId( + string $rpId, + AuthenticationExtensions $authenticationExtensionsClientInputs, + null|AuthenticationExtensions $authenticationExtensionsClientOutputs + ): string { + if ($authenticationExtensionsClientOutputs === null || ! $authenticationExtensionsClientInputs->has( + 'appid' + ) || ! $authenticationExtensionsClientOutputs->has('appid')) { + return $rpId; + } + $appId = $authenticationExtensionsClientInputs->get('appid') + ->value; + $wasUsed = $authenticationExtensionsClientOutputs->get('appid') + ->value; + if (! is_string($appId) || $wasUsed !== true) { + return $rpId; + } + return $appId; + } +} diff --git a/3rdparty/web-auth/webauthn-lib/src/CeremonyStep/CheckRelyingPartyIdIdHash.php b/3rdparty/web-auth/webauthn-lib/src/CeremonyStep/CheckRelyingPartyIdIdHash.php new file mode 100644 index 00000000..de45b27b --- /dev/null +++ b/3rdparty/web-auth/webauthn-lib/src/CeremonyStep/CheckRelyingPartyIdIdHash.php @@ -0,0 +1,63 @@ +authenticatorData : $authenticatorResponse->attestationObject->authData; + $C = $authenticatorResponse->clientDataJSON; + $attestedCredentialData = $publicKeyCredentialSource->getAttestedCredentialData(); + $credentialPublicKey = $attestedCredentialData->credentialPublicKey; + $credentialPublicKey !== null || throw AuthenticatorResponseVerificationException::create( + 'No public key available.' + ); + $isU2F = U2FPublicKey::isU2FKey($credentialPublicKey); + $rpId = $publicKeyCredentialOptions->rpId ?? $publicKeyCredentialOptions->rp->id ?? $host; + $facetId = $this->getFacetId($rpId, $publicKeyCredentialOptions->extensions, $authData ->extensions); + $rpIdHash = hash('sha256', $isU2F ? $C->origin : $facetId, true); + hash_equals( + $rpIdHash, + $authData + ->rpIdHash + ) || throw AuthenticatorResponseVerificationException::create('rpId hash mismatch.'); + } + + private function getFacetId( + string $rpId, + AuthenticationExtensions $authenticationExtensionsClientInputs, + null|AuthenticationExtensions $authenticationExtensionsClientOutputs + ): string { + if ($authenticationExtensionsClientOutputs === null || ! $authenticationExtensionsClientInputs->has( + 'appid' + ) || ! $authenticationExtensionsClientOutputs->has('appid')) { + return $rpId; + } + $appId = $authenticationExtensionsClientInputs->get('appid') + ->value; + $wasUsed = $authenticationExtensionsClientOutputs->get('appid') + ->value; + if (! is_string($appId) || $wasUsed !== true) { + return $rpId; + } + return $appId; + } +} diff --git a/3rdparty/web-auth/webauthn-lib/src/CeremonyStep/CheckSignature.php b/3rdparty/web-auth/webauthn-lib/src/CeremonyStep/CheckSignature.php new file mode 100644 index 00000000..a89fef32 --- /dev/null +++ b/3rdparty/web-auth/webauthn-lib/src/CeremonyStep/CheckSignature.php @@ -0,0 +1,90 @@ +algorithmManager = $algorithmManager ?? Manager::create()->add(ES256::create(), RS256::create()); + } + + public function process( + PublicKeyCredentialSource $publicKeyCredentialSource, + AuthenticatorAssertionResponse|AuthenticatorAttestationResponse $authenticatorResponse, + PublicKeyCredentialRequestOptions|PublicKeyCredentialCreationOptions $publicKeyCredentialOptions, + ?string $userHandle, + string $host + ): void { + if (! $authenticatorResponse instanceof AuthenticatorAssertionResponse) { + return; + } + $credentialPublicKey = $publicKeyCredentialSource->getAttestedCredentialData() +->credentialPublicKey; + $credentialPublicKey !== null || throw AuthenticatorResponseVerificationException::create( + 'No public key available.' + ); + $coseKey = $this->getCoseKey($credentialPublicKey); + + $getClientDataJSONHash = hash('sha256', $authenticatorResponse->clientDataJSON->rawData, true); + $dataToVerify = $authenticatorResponse->authenticatorData->authData . $getClientDataJSONHash; + $signature = $authenticatorResponse->signature; + $algorithm = $this->algorithmManager->get($coseKey->alg()); + $algorithm instanceof Signature || throw AuthenticatorResponseVerificationException::create( + 'Invalid algorithm identifier. Should refer to a signature algorithm' + ); + $signature = CoseSignatureFixer::fix($signature, $algorithm); + $algorithm->verify( + $dataToVerify, + $coseKey, + $signature + ) || throw AuthenticatorResponseVerificationException::create('Invalid signature.'); + } + + private function getCoseKey(string $credentialPublicKey): Key + { + $isU2F = U2FPublicKey::isU2FKey($credentialPublicKey); + if ($isU2F === true) { + $credentialPublicKey = U2FPublicKey::convertToCoseKey($credentialPublicKey); + } + $stream = new StringStream($credentialPublicKey); + $credentialPublicKeyStream = Decoder::create()->decode($stream); + $stream->isEOF() || throw AuthenticatorResponseVerificationException::create( + 'Invalid key. Presence of extra bytes.' + ); + $stream->close(); + $credentialPublicKeyStream instanceof Normalizable || throw AuthenticatorResponseVerificationException::create( + 'Invalid attestation object. Unexpected object.' + ); + $normalizedData = $credentialPublicKeyStream->normalize(); + is_array($normalizedData) || throw AuthenticatorResponseVerificationException::create( + 'Invalid attestation object. Unexpected object.' + ); + /** @var array $normalizedData */ + + return Key::create($normalizedData); + } +} diff --git a/3rdparty/web-auth/webauthn-lib/src/CeremonyStep/CheckTopOrigin.php b/3rdparty/web-auth/webauthn-lib/src/CeremonyStep/CheckTopOrigin.php new file mode 100644 index 00000000..bb9a5554 --- /dev/null +++ b/3rdparty/web-auth/webauthn-lib/src/CeremonyStep/CheckTopOrigin.php @@ -0,0 +1,41 @@ +clientDataJSON->topOrigin; + if ($topOrigin === null) { + return; + } + if ($authenticatorResponse->clientDataJSON->crossOrigin !== true) { + throw AuthenticatorResponseVerificationException::create('The response is not cross-origin.'); + } + if ($this->topOriginValidator === null) { + (new HostTopOriginValidator($host))->validate($topOrigin); + } else { + $this->topOriginValidator->validate($topOrigin); + } + } +} diff --git a/3rdparty/web-auth/webauthn-lib/src/CeremonyStep/CheckUserHandle.php b/3rdparty/web-auth/webauthn-lib/src/CeremonyStep/CheckUserHandle.php new file mode 100644 index 00000000..27585754 --- /dev/null +++ b/3rdparty/web-auth/webauthn-lib/src/CeremonyStep/CheckUserHandle.php @@ -0,0 +1,37 @@ +userHandle; + $responseUserHandle = $authenticatorResponse->userHandle; + if ($userHandle !== null) { //If the user was identified before the authentication ceremony was initiated, + $credentialUserHandle === $userHandle || throw InvalidUserHandleException::create(); + if ($responseUserHandle !== null && $responseUserHandle !== '') { + $credentialUserHandle === $responseUserHandle || throw InvalidUserHandleException::create(); + } + } else { + ($responseUserHandle !== '' && $credentialUserHandle === $responseUserHandle) || throw InvalidUserHandleException::create(); + } + } +} diff --git a/3rdparty/web-auth/webauthn-lib/src/CeremonyStep/CheckUserVerification.php b/3rdparty/web-auth/webauthn-lib/src/CeremonyStep/CheckUserVerification.php new file mode 100644 index 00000000..392eca8b --- /dev/null +++ b/3rdparty/web-auth/webauthn-lib/src/CeremonyStep/CheckUserVerification.php @@ -0,0 +1,33 @@ +userVerification : $publicKeyCredentialOptions->authenticatorSelection?->userVerification; + if ($userVerification !== AuthenticatorSelectionCriteria::USER_VERIFICATION_REQUIREMENT_REQUIRED) { + return; + } + $authData = $authenticatorResponse instanceof AuthenticatorAssertionResponse ? $authenticatorResponse->authenticatorData : $authenticatorResponse->attestationObject->authData; + $authData->isUserVerified() || throw AuthenticatorResponseVerificationException::create( + 'User authentication required.' + ); + } +} diff --git a/3rdparty/web-auth/webauthn-lib/src/CeremonyStep/CheckUserWasPresent.php b/3rdparty/web-auth/webauthn-lib/src/CeremonyStep/CheckUserWasPresent.php new file mode 100644 index 00000000..32fd7fff --- /dev/null +++ b/3rdparty/web-auth/webauthn-lib/src/CeremonyStep/CheckUserWasPresent.php @@ -0,0 +1,26 @@ +authenticatorData : $authenticatorResponse->attestationObject->authData; + $authData->isUserPresent() || throw AuthenticatorResponseVerificationException::create('User was not present'); + } +} diff --git a/3rdparty/web-auth/webauthn-lib/src/CeremonyStep/HostTopOriginValidator.php b/3rdparty/web-auth/webauthn-lib/src/CeremonyStep/HostTopOriginValidator.php new file mode 100644 index 00000000..dfccbccc --- /dev/null +++ b/3rdparty/web-auth/webauthn-lib/src/CeremonyStep/HostTopOriginValidator.php @@ -0,0 +1,22 @@ +host || throw AuthenticatorResponseVerificationException::create( + 'The top origin does not correspond to the host.' + ); + } +} diff --git a/3rdparty/web-auth/webauthn-lib/src/CeremonyStep/TopOriginValidator.php b/3rdparty/web-auth/webauthn-lib/src/CeremonyStep/TopOriginValidator.php new file mode 100644 index 00000000..d189a611 --- /dev/null +++ b/3rdparty/web-auth/webauthn-lib/src/CeremonyStep/TopOriginValidator.php @@ -0,0 +1,10 @@ +clientDataCollectors as $clientDataCollector) { + if (in_array($collectedClientData->type, $clientDataCollector->supportedTypes(), true)) { + $clientDataCollector->verifyCollectedClientData( + $collectedClientData, + $publicKeyCredentialOptions, + $authenticatorResponse, + $host + ); + return; + } + } + + throw AuthenticatorResponseVerificationException::create('No client data collector found.'); + } +} diff --git a/3rdparty/web-auth/webauthn-lib/src/ClientDataCollector/WebauthnAuthenticationCollector.php b/3rdparty/web-auth/webauthn-lib/src/ClientDataCollector/WebauthnAuthenticationCollector.php new file mode 100644 index 00000000..1a316e25 --- /dev/null +++ b/3rdparty/web-auth/webauthn-lib/src/ClientDataCollector/WebauthnAuthenticationCollector.php @@ -0,0 +1,34 @@ +type, + $this->supportedTypes(), + true + ) || throw AuthenticatorResponseVerificationException::create( + sprintf('The client data type is not "%s" supported.', implode('", "', $this->supportedTypes())) + ); + } +} diff --git a/3rdparty/web-auth/webauthn-lib/src/CollectedClientData.php b/3rdparty/web-auth/webauthn-lib/src/CollectedClientData.php new file mode 100644 index 00000000..3731207b --- /dev/null +++ b/3rdparty/web-auth/webauthn-lib/src/CollectedClientData.php @@ -0,0 +1,177 @@ +type = $type; + + $challenge = $data['challenge'] ?? ''; + is_string($challenge) || throw InvalidDataException::create( + $data, + 'Invalid parameter "challenge". Shall be a string.' + ); + $challenge = Base64UrlSafe::decodeNoPadding($challenge); + $challenge !== '' || throw InvalidDataException::create( + $data, + 'Invalid parameter "challenge". Shall not be empty.' + ); + $this->challenge = $challenge; + + $origin = $data['origin'] ?? ''; + (is_string($origin) && $origin !== '') || throw InvalidDataException::create( + $data, + 'Invalid parameter "origin". Shall be a non-empty string.' + ); + $this->origin = $origin; + + $this->topOrigin = $data['topOrigin'] ?? null; + $this->crossOrigin = $data['crossOrigin'] ?? false; + + $tokenBinding = $data['tokenBinding'] ?? null; + $tokenBinding === null || is_array($tokenBinding) || throw InvalidDataException::create( + $data, + 'Invalid parameter "tokenBinding". Shall be an object or .' + ); + $this->tokenBinding = $tokenBinding; + + $this->data = $data; + } + + /** + * @param mixed[] $data + */ + public static function create(string $rawData, array $data): self + { + return new self($rawData, $data); + } + + public static function createFormJson(string $data): self + { + $rawData = Base64UrlSafe::decodeNoPadding($data); + $json = json_decode($rawData, true, flags: JSON_THROW_ON_ERROR); + is_array($json) || throw InvalidDataException::create($data, 'Invalid JSON data.'); + + return self::create($rawData, $json); + } + + /** + * @deprecated since 4.7.0. Please use the property directly. + * @infection-ignore-all + */ + public function getType(): string + { + return $this->type; + } + + /** + * @deprecated since 4.7.0. Please use the property directly. + * @infection-ignore-all + */ + public function getChallenge(): string + { + return $this->challenge; + } + + /** + * @deprecated since 4.7.0. Please use the property directly. + * @infection-ignore-all + */ + public function getOrigin(): string + { + return $this->origin; + } + + /** + * @deprecated since 4.7.0. Please use the property directly. + * @infection-ignore-all + */ + public function getCrossOrigin(): bool + { + return $this->crossOrigin; + } + + /** + * @deprecated Since 4.3.0 and will be removed in 5.0.0 + * @infection-ignore-all + */ + public function getTokenBinding(): ?TokenBinding + { + return $this->tokenBinding === null ? null : TokenBinding::createFormArray($this->tokenBinding); + } + + /** + * @deprecated since 4.7.0. Please use the property directly. + * @infection-ignore-all + */ + public function getRawData(): string + { + return $this->rawData; + } + + /** + * @return string[] + */ + public function all(): array + { + return array_keys($this->data); + } + + public function has(string $key): bool + { + return array_key_exists($key, $this->data); + } + + public function get(string $key): mixed + { + if (! $this->has($key)) { + throw InvalidDataException::create($this->data, sprintf('The key "%s" is missing', $key)); + } + + return $this->data[$key]; + } +} diff --git a/3rdparty/web-auth/webauthn-lib/src/Counter/CounterChecker.php b/3rdparty/web-auth/webauthn-lib/src/Counter/CounterChecker.php new file mode 100644 index 00000000..a23d7ee4 --- /dev/null +++ b/3rdparty/web-auth/webauthn-lib/src/Counter/CounterChecker.php @@ -0,0 +1,12 @@ +logger = $logger; + } + + public function check(PublicKeyCredentialSource $publicKeyCredentialSource, int $currentCounter): void + { + try { + $currentCounter > $publicKeyCredentialSource->counter || throw CounterException::create( + $currentCounter, + $publicKeyCredentialSource->counter, + 'Invalid counter.' + ); + } catch (CounterException $throwable) { + $this->logger->error('The counter is invalid', [ + 'current' => $currentCounter, + 'new' => $publicKeyCredentialSource->counter, + ]); + throw $throwable; + } + } +} diff --git a/3rdparty/web-auth/webauthn-lib/src/Credential.php b/3rdparty/web-auth/webauthn-lib/src/Credential.php new file mode 100644 index 00000000..7b0e7b28 --- /dev/null +++ b/3rdparty/web-auth/webauthn-lib/src/Credential.php @@ -0,0 +1,60 @@ +id = $id; + $this->rawId = $rawId ?? Base64UrlSafe::decodeNoPadding($id); + } + + /** + * @deprecated since 4.7.0. Please use the property directly. + * @infection-ignore-all + */ + public function getId(): string + { + return $this->id; + } + + /** + * @deprecated since 4.7.0. Please use the property directly. + * @infection-ignore-all + */ + public function getType(): string + { + return $this->type; + } +} diff --git a/3rdparty/web-auth/webauthn-lib/src/Denormalizer/AttestationObjectDenormalizer.php b/3rdparty/web-auth/webauthn-lib/src/Denormalizer/AttestationObjectDenormalizer.php new file mode 100644 index 00000000..dfd4519b --- /dev/null +++ b/3rdparty/web-auth/webauthn-lib/src/Denormalizer/AttestationObjectDenormalizer.php @@ -0,0 +1,63 @@ +decode($stream); + + $parsed instanceof Normalizable || throw InvalidDataException::create( + $parsed, + 'Invalid attestation object. Unexpected object.' + ); + $attestationObject = $parsed->normalize(); + $stream->isEOF() || throw InvalidDataException::create( + null, + 'Invalid attestation object. Presence of extra bytes.' + ); + $stream->close(); + $authData = $attestationObject['authData'] ?? throw InvalidDataException::create( + $attestationObject, + 'Invalid attestation object. Missing "authData" field.' + ); + + return AttestationObject::create( + $data, + $this->denormalizer->denormalize($attestationObject, AttestationStatement::class, $format, $context), + $this->denormalizer->denormalize($authData, AuthenticatorData::class, $format, $context), + ); + } + + public function supportsDenormalization(mixed $data, string $type, string $format = null, array $context = []): bool + { + return $type === AttestationObject::class; + } + + /** + * @return array + */ + public function getSupportedTypes(?string $format): array + { + return [ + AttestationObject::class => true, + ]; + } +} diff --git a/3rdparty/web-auth/webauthn-lib/src/Denormalizer/AttestationStatementDenormalizer.php b/3rdparty/web-auth/webauthn-lib/src/Denormalizer/AttestationStatementDenormalizer.php new file mode 100644 index 00000000..fea020bc --- /dev/null +++ b/3rdparty/web-auth/webauthn-lib/src/Denormalizer/AttestationStatementDenormalizer.php @@ -0,0 +1,39 @@ +attestationStatementSupportManager->get($data['fmt']); + + return $attestationStatementSupport->load($data); + } + + public function supportsDenormalization(mixed $data, string $type, string $format = null, array $context = []): bool + { + return $type === AttestationStatement::class; + } + + /** + * @return array + */ + public function getSupportedTypes(?string $format): array + { + return [ + AttestationStatement::class => true, + ]; + } +} diff --git a/3rdparty/web-auth/webauthn-lib/src/Denormalizer/AttestedCredentialDataNormalizer.php b/3rdparty/web-auth/webauthn-lib/src/Denormalizer/AttestedCredentialDataNormalizer.php new file mode 100644 index 00000000..05bd0910 --- /dev/null +++ b/3rdparty/web-auth/webauthn-lib/src/Denormalizer/AttestedCredentialDataNormalizer.php @@ -0,0 +1,45 @@ + + */ + public function normalize(mixed $data, ?string $format = null, array $context = []): array + { + assert($data instanceof AttestedCredentialData); + $result = [ + 'aaguid' => $this->normalizer->normalize($data->aaguid, $format, $context), + 'credentialId' => base64_encode($data->credentialId), + ]; + if ($data->credentialPublicKey !== null) { + $result['credentialPublicKey'] = base64_encode($data->credentialPublicKey); + } + + return $result; + } + + public function supportsNormalization(mixed $data, ?string $format = null, array $context = []): bool + { + return $data instanceof AttestedCredentialData; + } + + public function getSupportedTypes(?string $format): array + { + return [ + AttestedCredentialData::class => true, + ]; + } +} diff --git a/3rdparty/web-auth/webauthn-lib/src/Denormalizer/AuthenticationExtensionNormalizer.php b/3rdparty/web-auth/webauthn-lib/src/Denormalizer/AuthenticationExtensionNormalizer.php new file mode 100644 index 00000000..b34d29de --- /dev/null +++ b/3rdparty/web-auth/webauthn-lib/src/Denormalizer/AuthenticationExtensionNormalizer.php @@ -0,0 +1,37 @@ + + */ + public function getSupportedTypes(?string $format): array + { + return [ + AuthenticationExtension::class => true, + ]; + } + + /** + * @return array + */ + public function normalize(mixed $data, ?string $format = null, array $context = []): array + { + assert($data instanceof AuthenticationExtension); + + return $data->value; + } + + public function supportsNormalization(mixed $data, ?string $format = null, array $context = []): bool + { + return $data instanceof AuthenticationExtension; + } +} diff --git a/3rdparty/web-auth/webauthn-lib/src/Denormalizer/AuthenticationExtensionsDenormalizer.php b/3rdparty/web-auth/webauthn-lib/src/Denormalizer/AuthenticationExtensionsDenormalizer.php new file mode 100644 index 00000000..e5bacefd --- /dev/null +++ b/3rdparty/web-auth/webauthn-lib/src/Denormalizer/AuthenticationExtensionsDenormalizer.php @@ -0,0 +1,79 @@ +extensions); + } + assert(is_array($data), 'The data should be an array.'); + foreach ($data as $key => $value) { + if (! is_string($key)) { + continue; + } + $data[$key] = AuthenticationExtension::create($key, $value); + } + + return AuthenticationExtensions::create($data); + } + + public function supportsDenormalization(mixed $data, string $type, string $format = null, array $context = []): bool + { + return in_array( + $type, + [ + AuthenticationExtensions::class, + AuthenticationExtensionsClientOutputs::class, + AuthenticationExtensionsClientInputs::class, + ], + true + ); + } + + /** + * @return array + */ + public function getSupportedTypes(?string $format): array + { + return [ + AuthenticationExtensions::class => true, + AuthenticationExtensionsClientInputs::class => true, + AuthenticationExtensionsClientOutputs::class => true, + ]; + } + + /** + * @return array + */ + public function normalize(mixed $data, ?string $format = null, array $context = []): array + { + assert($data instanceof AuthenticationExtensions); + $extensions = []; + foreach ($data->extensions as $extension) { + $extensions[$extension->name] = $extension->value; + } + + return $extensions; + } + + public function supportsNormalization(mixed $data, ?string $format = null, array $context = []): bool + { + return $data instanceof AuthenticationExtensions; + } +} diff --git a/3rdparty/web-auth/webauthn-lib/src/Denormalizer/AuthenticatorAssertionResponseDenormalizer.php b/3rdparty/web-auth/webauthn-lib/src/Denormalizer/AuthenticatorAssertionResponseDenormalizer.php new file mode 100644 index 00000000..613331c7 --- /dev/null +++ b/3rdparty/web-auth/webauthn-lib/src/Denormalizer/AuthenticatorAssertionResponseDenormalizer.php @@ -0,0 +1,59 @@ +denormalizer->denormalize($data['clientDataJSON'], CollectedClientData::class, $format, $context), + $this->denormalizer->denormalize($data['authenticatorData'], AuthenticatorData::class, $format, $context), + $data['signature'], + $userHandle ?? null, + ! isset($data['attestationObject']) ? null : $this->denormalizer->denormalize( + $data['attestationObject'], + AttestationObject::class, + $format, + $context + ), + ); + } + + public function supportsDenormalization(mixed $data, string $type, string $format = null, array $context = []): bool + { + return $type === AuthenticatorAssertionResponse::class; + } + + /** + * @return array + */ + public function getSupportedTypes(?string $format): array + { + return [ + AuthenticatorAssertionResponse::class => true, + ]; + } +} diff --git a/3rdparty/web-auth/webauthn-lib/src/Denormalizer/AuthenticatorAttestationResponseDenormalizer.php b/3rdparty/web-auth/webauthn-lib/src/Denormalizer/AuthenticatorAttestationResponseDenormalizer.php new file mode 100644 index 00000000..5f6569dc --- /dev/null +++ b/3rdparty/web-auth/webauthn-lib/src/Denormalizer/AuthenticatorAttestationResponseDenormalizer.php @@ -0,0 +1,59 @@ +denormalizer->denormalize( + $data['clientDataJSON'], + CollectedClientData::class, + $format, + $context + ); + $attestationObject = $this->denormalizer->denormalize( + $data['attestationObject'], + AttestationObject::class, + $format, + $context + ); + + return AuthenticatorAttestationResponse::create( + $clientDataJSON, + $attestationObject, + $data['transports'] ?? [], + ); + } + + public function supportsDenormalization(mixed $data, string $type, string $format = null, array $context = []): bool + { + return $type === AuthenticatorAttestationResponse::class; + } + + /** + * @return array + */ + public function getSupportedTypes(?string $format): array + { + return [ + AuthenticatorAttestationResponse::class => true, + ]; + } +} diff --git a/3rdparty/web-auth/webauthn-lib/src/Denormalizer/AuthenticatorDataDenormalizer.php b/3rdparty/web-auth/webauthn-lib/src/Denormalizer/AuthenticatorDataDenormalizer.php new file mode 100644 index 00000000..f13450d0 --- /dev/null +++ b/3rdparty/web-auth/webauthn-lib/src/Denormalizer/AuthenticatorDataDenormalizer.php @@ -0,0 +1,140 @@ +decoder = Decoder::create(); + } + + public function denormalize(mixed $data, string $type, string $format = null, array $context = []): mixed + { + $authData = $this->fixIncorrectEdDSAKey($data); + $authDataStream = new StringStream($authData); + $rp_id_hash = $authDataStream->read(32); + $flags = $authDataStream->read(1); + $signCount = $authDataStream->read(4); + $signCount = unpack('N', $signCount); + + $attestedCredentialData = null; + if (0 !== (ord($flags) & AuthenticatorData::FLAG_AT)) { + $aaguid = Uuid::fromBinary($authDataStream->read(16)); + $credentialLength = $authDataStream->read(2); + $credentialLength = unpack('n', $credentialLength); + $credentialId = $authDataStream->read($credentialLength[1]); + $credentialPublicKey = $this->decoder->decode($authDataStream); + $credentialPublicKey instanceof MapObject || throw InvalidDataException::create( + $authData, + 'The data does not contain a valid credential public key.' + ); + $attestedCredentialData = AttestedCredentialData::create( + $aaguid, + $credentialId, + (string) $credentialPublicKey, + ); + } + $extension = null; + if (0 !== (ord($flags) & AuthenticatorData::FLAG_ED)) { + $extension = $this->decoder->decode($authDataStream); + $extension = AuthenticationExtensionsClientOutputsLoader::load($extension); + } + $authDataStream->isEOF() || throw InvalidDataException::create( + $authData, + 'Invalid authentication data. Presence of extra bytes.' + ); + $authDataStream->close(); + + return AuthenticatorData::create( + $authData, + $rp_id_hash, + $flags, + $signCount[1], + $attestedCredentialData, + $extension === null ? null : $this->denormalizer->denormalize( + $extension, + AuthenticationExtensions::class, + $format, + $context + ), + ); + } + + public function supportsDenormalization(mixed $data, string $type, string $format = null, array $context = []): bool + { + return $type === AuthenticatorData::class; + } + + /** + * @return array + */ + public function getSupportedTypes(?string $format): array + { + return [ + AuthenticatorData::class => true, + ]; + } + + private function fixIncorrectEdDSAKey(string $data): string + { + $needle = hex2bin('a301634f4b500327206745643235353139'); + $correct = hex2bin('a401634f4b500327206745643235353139'); + $position = mb_strpos($data, $needle, 0, '8bit'); + if ($position === false) { + return $data; + } + + $begin = mb_substr($data, 0, $position, '8bit'); + $end = mb_substr($data, $position, null, '8bit'); + $end = str_replace($needle, $correct, $end); + $cbor = new StringStream($end); + $badKey = $this->decoder->decode($cbor); + + ($badKey instanceof MapObject && $cbor->isEOF()) || throw InvalidDataException::create( + $end, + 'Invalid authentication data. Presence of extra bytes.' + ); + $badX = $badKey->get(-2); + $badX instanceof ListObject || throw InvalidDataException::create($end, 'Invalid authentication data.'); + $keyBytes = array_reduce( + $badX->normalize(), + static fn (string $carry, string $item): string => $carry . chr((int) $item), + '' + ); + $correctX = ByteStringObject::create($keyBytes); + $correctKey = MapObject::create() + ->add(UnsignedIntegerObject::create(1), TextStringObject::create('OKP')) + ->add(UnsignedIntegerObject::create(3), NegativeIntegerObject::create(-8)) + ->add(NegativeIntegerObject::create(-1), TextStringObject::create('Ed25519')) + ->add(NegativeIntegerObject::create(-2), $correctX); + + return $begin . $correctKey; + } +} diff --git a/3rdparty/web-auth/webauthn-lib/src/Denormalizer/AuthenticatorResponseDenormalizer.php b/3rdparty/web-auth/webauthn-lib/src/Denormalizer/AuthenticatorResponseDenormalizer.php new file mode 100644 index 00000000..6a47309d --- /dev/null +++ b/3rdparty/web-auth/webauthn-lib/src/Denormalizer/AuthenticatorResponseDenormalizer.php @@ -0,0 +1,45 @@ + AuthenticatorAttestationResponse::class, + array_key_exists('signature', $data) => AuthenticatorAssertionResponse::class, + default => throw InvalidDataException::create($data, 'Unable to create the response object'), + }; + + return $this->denormalizer->denormalize($data, $realType, $format, $context); + } + + public function supportsDenormalization(mixed $data, string $type, string $format = null, array $context = []): bool + { + return $type === AuthenticatorResponse::class; + } + + /** + * @return array + */ + public function getSupportedTypes(?string $format): array + { + return [ + AuthenticatorResponse::class => true, + ]; + } +} diff --git a/3rdparty/web-auth/webauthn-lib/src/Denormalizer/CollectedClientDataDenormalizer.php b/3rdparty/web-auth/webauthn-lib/src/Denormalizer/CollectedClientDataDenormalizer.php new file mode 100644 index 00000000..e9e5264c --- /dev/null +++ b/3rdparty/web-auth/webauthn-lib/src/Denormalizer/CollectedClientDataDenormalizer.php @@ -0,0 +1,36 @@ + + */ + public function getSupportedTypes(?string $format): array + { + return [ + CollectedClientData::class => true, + ]; + } +} diff --git a/3rdparty/web-auth/webauthn-lib/src/Denormalizer/ExtensionDescriptorDenormalizer.php b/3rdparty/web-auth/webauthn-lib/src/Denormalizer/ExtensionDescriptorDenormalizer.php new file mode 100644 index 00000000..4109ffb4 --- /dev/null +++ b/3rdparty/web-auth/webauthn-lib/src/Denormalizer/ExtensionDescriptorDenormalizer.php @@ -0,0 +1,52 @@ +denormalizer->denormalize($data, $type, $format, $context); + } + + public function supportsDenormalization(mixed $data, string $type, string $format = null, array $context = []): bool + { + if ($context[self::ALREADY_CALLED] ?? false) { + return false; + } + + return $type === ExtensionDescriptor::class; + } + + /** + * @return array + */ + public function getSupportedTypes(?string $format): array + { + return [ + ExtensionDescriptor::class => false, + ]; + } +} diff --git a/3rdparty/web-auth/webauthn-lib/src/Denormalizer/PublicKeyCredentialDenormalizer.php b/3rdparty/web-auth/webauthn-lib/src/Denormalizer/PublicKeyCredentialDenormalizer.php new file mode 100644 index 00000000..33899b70 --- /dev/null +++ b/3rdparty/web-auth/webauthn-lib/src/Denormalizer/PublicKeyCredentialDenormalizer.php @@ -0,0 +1,53 @@ +denormalizer->denormalize($data['response'], AuthenticatorResponse::class, $format, $context), + ); + } + + public function supportsDenormalization(mixed $data, string $type, string $format = null, array $context = []): bool + { + return $type === PublicKeyCredential::class; + } + + /** + * @return array + */ + public function getSupportedTypes(?string $format): array + { + return [ + PublicKeyCredential::class => true, + ]; + } +} diff --git a/3rdparty/web-auth/webauthn-lib/src/Denormalizer/PublicKeyCredentialDescriptorNormalizer.php b/3rdparty/web-auth/webauthn-lib/src/Denormalizer/PublicKeyCredentialDescriptorNormalizer.php new file mode 100644 index 00000000..ec6a4b3d --- /dev/null +++ b/3rdparty/web-auth/webauthn-lib/src/Denormalizer/PublicKeyCredentialDescriptorNormalizer.php @@ -0,0 +1,47 @@ + + */ + public function normalize(mixed $data, ?string $format = null, array $context = []): array + { + assert($data instanceof PublicKeyCredentialDescriptor); + $result = [ + 'type' => $data->type, + 'id' => Base64UrlSafe::encodeUnpadded($data->id), + ]; + if (count($data->transports) !== 0) { + $result['transports'] = $data->transports; + } + + return $result; + } + + public function supportsNormalization(mixed $data, ?string $format = null, array $context = []): bool + { + return $data instanceof PublicKeyCredentialDescriptor; + } + + public function getSupportedTypes(?string $format): array + { + return [ + PublicKeyCredentialDescriptor::class => true, + ]; + } +} diff --git a/3rdparty/web-auth/webauthn-lib/src/Denormalizer/PublicKeyCredentialOptionsDenormalizer.php b/3rdparty/web-auth/webauthn-lib/src/Denormalizer/PublicKeyCredentialOptionsDenormalizer.php new file mode 100644 index 00000000..1b59091d --- /dev/null +++ b/3rdparty/web-auth/webauthn-lib/src/Denormalizer/PublicKeyCredentialOptionsDenormalizer.php @@ -0,0 +1,179 @@ + $allowCredential) { + $data[$key][$item]['id'] = Base64UrlSafe::decodeNoPadding($allowCredential['id']); + } + } + } + if ($type === PublicKeyCredentialCreationOptions::class) { + return PublicKeyCredentialCreationOptions::create( + $this->denormalizer->denormalize($data['rp'], PublicKeyCredentialRpEntity::class, $format, $context), + $this->denormalizer->denormalize( + $data['user'], + PublicKeyCredentialUserEntity::class, + $format, + $context + ), + $data['challenge'], + ! isset($data['pubKeyCredParams']) ? [] : $this->denormalizer->denormalize( + $data['pubKeyCredParams'], + PublicKeyCredentialParameters::class . '[]', + $format, + $context + ), + ! isset($data['authenticatorSelection']) ? null : $this->denormalizer->denormalize( + $data['authenticatorSelection'], + AuthenticatorSelectionCriteria::class, + $format, + $context + ), + $data['attestation'] ?? null, + ! isset($data['excludeCredentials']) ? [] : $this->denormalizer->denormalize( + $data['excludeCredentials'], + PublicKeyCredentialDescriptor::class . '[]', + $format, + $context + ), + $data['timeout'] ?? null, + ! isset($data['extensions']) ? null : $this->denormalizer->denormalize( + $data['extensions'], + AuthenticationExtensions::class, + $format, + $context + ), + ); + } + if ($type === PublicKeyCredentialRequestOptions::class) { + return PublicKeyCredentialRequestOptions::create( + $data['challenge'], + $data['rpId'] ?? null, + ! isset($data['allowCredentials']) ? [] : $this->denormalizer->denormalize( + $data['allowCredentials'], + PublicKeyCredentialDescriptor::class . '[]', + $format, + $context + ), + $data['userVerification'] ?? null, + $data['timeout'] ?? null, + ! isset($data['extensions']) ? null : $this->denormalizer->denormalize( + $data['extensions'], + AuthenticationExtensions::class, + $format, + $context + ), + ); + } + throw new BadMethodCallException('Unsupported type'); + } + + public function supportsDenormalization(mixed $data, string $type, string $format = null, array $context = []): bool + { + return in_array( + $type, + [PublicKeyCredentialCreationOptions::class, PublicKeyCredentialRequestOptions::class], + true + ); + } + + public function supportsNormalization(mixed $data, ?string $format = null, array $context = []): bool + { + return $data instanceof PublicKeyCredentialCreationOptions || $data instanceof PublicKeyCredentialRequestOptions; + } + + /** + * @return array + */ + public function getSupportedTypes(?string $format): array + { + return [ + PublicKeyCredentialCreationOptions::class => true, + PublicKeyCredentialRequestOptions::class => true, + ]; + } + + /** + * @return array + */ + public function normalize(mixed $data, ?string $format = null, array $context = []): array + { + assert( + $data instanceof PublicKeyCredentialCreationOptions || $data instanceof PublicKeyCredentialRequestOptions + ); + $json = [ + 'challenge' => Base64UrlSafe::encodeUnpadded($data->challenge), + 'timeout' => $data->timeout, + 'extensions' => $data->extensions->count() === 0 ? null : $this->normalizer->normalize( + $data->extensions, + $format, + $context + ), + ]; + + if ($data instanceof PublicKeyCredentialCreationOptions) { + $json = [ + ...$json, + 'rp' => $this->normalizer->normalize($data->rp, $format, $context), + 'user' => $this->normalizer->normalize($data->user, $format, $context), + 'pubKeyCredParams' => $this->normalizer->normalize( + $data->pubKeyCredParams, + PublicKeyCredentialParameters::class . '[]', + $context + ), + 'authenticatorSelection' => $data->authenticatorSelection === null ? null : $this->normalizer->normalize( + $data->authenticatorSelection, + $format, + $context + ), + 'attestation' => $data->attestation, + 'excludeCredentials' => $this->normalizer->normalize($data->excludeCredentials, $format, $context), + ]; + } + if ($data instanceof PublicKeyCredentialRequestOptions) { + $json = [ + ...$json, + 'rpId' => $data->rpId, + 'allowCredentials' => $this->normalizer->normalize($data->allowCredentials, $format, $context), + 'userVerification' => $data->userVerification, + ]; + } + + return array_filter($json, static fn ($value) => $value !== null && $value !== []); + } +} diff --git a/3rdparty/web-auth/webauthn-lib/src/Denormalizer/PublicKeyCredentialParametersDenormalizer.php b/3rdparty/web-auth/webauthn-lib/src/Denormalizer/PublicKeyCredentialParametersDenormalizer.php new file mode 100644 index 00000000..b5d64068 --- /dev/null +++ b/3rdparty/web-auth/webauthn-lib/src/Denormalizer/PublicKeyCredentialParametersDenormalizer.php @@ -0,0 +1,37 @@ + + */ + public function getSupportedTypes(?string $format): array + { + return [ + PublicKeyCredentialParameters::class => true, + ]; + } +} diff --git a/3rdparty/web-auth/webauthn-lib/src/Denormalizer/PublicKeyCredentialSourceDenormalizer.php b/3rdparty/web-auth/webauthn-lib/src/Denormalizer/PublicKeyCredentialSourceDenormalizer.php new file mode 100644 index 00000000..bf8a5ae3 --- /dev/null +++ b/3rdparty/web-auth/webauthn-lib/src/Denormalizer/PublicKeyCredentialSourceDenormalizer.php @@ -0,0 +1,96 @@ +denormalizer->denormalize($data['trustPath'], TrustPath::class, $format, $context), + Uuid::fromString($data['aaguid']), + $data['credentialPublicKey'], + $data['userHandle'], + $data['counter'], + $data['otherUI'] ?? null, + $data['backupEligible'] ?? null, + $data['backupStatus'] ?? null, + $data['uvInitialized'] ?? null, + ); + } + + public function supportsDenormalization(mixed $data, string $type, string $format = null, array $context = []): bool + { + return $type === PublicKeyCredentialSource::class; + } + + /** + * @return array + */ + public function getSupportedTypes(?string $format): array + { + return [ + PublicKeyCredentialSource::class => true, + ]; + } + + /** + * @return array + */ + public function normalize(mixed $data, ?string $format = null, array $context = []): array + { + assert($data instanceof PublicKeyCredentialSource); + $result = [ + 'publicKeyCredentialId' => Base64UrlSafe::encodeUnpadded($data->publicKeyCredentialId), + 'type' => $data->type, + 'transports' => $data->transports, + 'attestationType' => $data->attestationType, + 'trustPath' => $this->normalizer->normalize($data->trustPath, $format, $context), + 'aaguid' => $this->normalizer->normalize($data->aaguid, $format, $context), + 'credentialPublicKey' => Base64UrlSafe::encodeUnpadded($data->credentialPublicKey), + 'userHandle' => Base64UrlSafe::encodeUnpadded($data->userHandle), + 'counter' => $data->counter, + 'otherUI' => $data->otherUI, + 'backupEligible' => $data->backupEligible, + 'backupStatus' => $data->backupStatus, + 'uvInitialized' => $data->uvInitialized, + ]; + + return array_filter($result, static fn ($value): bool => $value !== null); + } + + public function supportsNormalization(mixed $data, ?string $format = null, array $context = []): bool + { + return $data instanceof PublicKeyCredentialSource; + } +} diff --git a/3rdparty/web-auth/webauthn-lib/src/Denormalizer/PublicKeyCredentialUserEntityDenormalizer.php b/3rdparty/web-auth/webauthn-lib/src/Denormalizer/PublicKeyCredentialUserEntityDenormalizer.php new file mode 100644 index 00000000..275b2548 --- /dev/null +++ b/3rdparty/web-auth/webauthn-lib/src/Denormalizer/PublicKeyCredentialUserEntityDenormalizer.php @@ -0,0 +1,67 @@ + + */ + public function getSupportedTypes(?string $format): array + { + return [ + PublicKeyCredentialUserEntity::class => true, + ]; + } + + /** + * @return array + */ + public function normalize(mixed $data, ?string $format = null, array $context = []): array + { + assert($data instanceof PublicKeyCredentialUserEntity); + $normalized = [ + 'id' => Base64UrlSafe::encodeUnpadded($data->id), + 'name' => $data->name, + 'displayName' => $data->displayName, + 'icon' => $data->icon, + ]; + + return array_filter($normalized, fn ($value) => $value !== null); + } + + public function supportsNormalization(mixed $data, ?string $format = null, array $context = []): bool + { + return $data instanceof PublicKeyCredentialUserEntity; + } +} diff --git a/3rdparty/web-auth/webauthn-lib/src/Denormalizer/TrustPathDenormalizer.php b/3rdparty/web-auth/webauthn-lib/src/Denormalizer/TrustPathDenormalizer.php new file mode 100644 index 00000000..741dbcd8 --- /dev/null +++ b/3rdparty/web-auth/webauthn-lib/src/Denormalizer/TrustPathDenormalizer.php @@ -0,0 +1,66 @@ + new EcdaaKeyIdTrustPath($data), + array_key_exists('x5c', $data) => CertificateTrustPath::create($data), + $data === [], isset($data['type']) && $data['type'] === EmptyTrustPath::class => EmptyTrustPath::create(), + default => throw new InvalidTrustPathException('Unsupported trust path type'), + }; + } + + public function supportsDenormalization(mixed $data, string $type, string $format = null, array $context = []): bool + { + return $type === TrustPath::class; + } + + /** + * @return array + */ + public function getSupportedTypes(?string $format): array + { + return [ + TrustPath::class => true, + ]; + } + + /** + * @return array + */ + public function normalize(mixed $data, ?string $format = null, array $context = []): array + { + assert($data instanceof TrustPath); + return match (true) { + $data instanceof EcdaaKeyIdTrustPath => [ + 'ecdaaKeyId' => $data->getEcdaaKeyId(), + ], + $data instanceof CertificateTrustPath => [ + 'x5c' => $data->certificates, + ], + $data instanceof EmptyTrustPath => [], + default => throw new InvalidTrustPathException('Unsupported trust path type'), + }; + } + + public function supportsNormalization(mixed $data, ?string $format = null, array $context = []): bool + { + return $data instanceof TrustPath; + } +} diff --git a/3rdparty/web-auth/webauthn-lib/src/Denormalizer/VerificationMethodANDCombinationsDenormalizer.php b/3rdparty/web-auth/webauthn-lib/src/Denormalizer/VerificationMethodANDCombinationsDenormalizer.php new file mode 100644 index 00000000..64f69439 --- /dev/null +++ b/3rdparty/web-auth/webauthn-lib/src/Denormalizer/VerificationMethodANDCombinationsDenormalizer.php @@ -0,0 +1,45 @@ + + */ + public function getSupportedTypes(?string $format): array + { + return [ + VerificationMethodANDCombinations::class => true, + ]; + } + + /** + * @return array + */ + public function normalize(mixed $object, ?string $format = null, array $context = []): array + { + assert($object instanceof VerificationMethodANDCombinations); + + return array_map( + fn ($verificationMethod) => $this->normalizer->normalize($verificationMethod, $format, $context), + $object->verificationMethods + ); + } + + public function supportsNormalization(mixed $data, ?string $format = null, array $context = []): bool + { + return $data instanceof VerificationMethodANDCombinations; + } +} diff --git a/3rdparty/web-auth/webauthn-lib/src/Denormalizer/WebauthnSerializerFactory.php b/3rdparty/web-auth/webauthn-lib/src/Denormalizer/WebauthnSerializerFactory.php new file mode 100644 index 00000000..be9f7ce2 --- /dev/null +++ b/3rdparty/web-auth/webauthn-lib/src/Denormalizer/WebauthnSerializerFactory.php @@ -0,0 +1,92 @@ + $package) { + if (! class_exists($class)) { + throw new RuntimeException(sprintf( + 'The class "%s" is required. Please install the package "%s" to use this feature.', + $class, + $package + )); + } + } + + $denormalizers = [ + new ExtensionDescriptorDenormalizer(), + new VerificationMethodANDCombinationsDenormalizer(), + new AuthenticationExtensionNormalizer(), + new PublicKeyCredentialDescriptorNormalizer(), + new AttestedCredentialDataNormalizer(), + new AttestationObjectDenormalizer(), + new AttestationStatementDenormalizer($this->attestationStatementSupportManager), + new AuthenticationExtensionsDenormalizer(), + new AuthenticatorAssertionResponseDenormalizer(), + new AuthenticatorAttestationResponseDenormalizer(), + new AuthenticatorDataDenormalizer(), + new AuthenticatorResponseDenormalizer(), + new CollectedClientDataDenormalizer(), + new PublicKeyCredentialDenormalizer(), + new PublicKeyCredentialOptionsDenormalizer(), + new PublicKeyCredentialSourceDenormalizer(), + new PublicKeyCredentialUserEntityDenormalizer(), + new TrustPathDenormalizer(), + new UidNormalizer(), + new ArrayDenormalizer(), + new ObjectNormalizer( + propertyTypeExtractor: new PropertyInfoExtractor(typeExtractors: [ + new PhpDocExtractor(), + new ReflectionExtractor(), + ]) + ), + ]; + + return new Serializer($denormalizers, [new JsonEncoder()]); + } + + /** + * @return array + */ + private static function getRequiredSerializerClasses(): array + { + return [ + UidNormalizer::class => self::PACKAGE_SYMFONY_SERIALIZER, + ArrayDenormalizer::class => self::PACKAGE_SYMFONY_SERIALIZER, + ObjectNormalizer::class => self::PACKAGE_SYMFONY_SERIALIZER, + PropertyInfoExtractor::class => self::PACKAGE_SYMFONY_PROPERTY_INFO, + PhpDocExtractor::class => self::PACKAGE_PHPDOCUMENTOR_REFLECTION_DOCBLOCK, + ReflectionExtractor::class => self::PACKAGE_SYMFONY_PROPERTY_INFO, + JsonEncoder::class => self::PACKAGE_SYMFONY_SERIALIZER, + Serializer::class => self::PACKAGE_SYMFONY_SERIALIZER, + ]; + } +} diff --git a/3rdparty/web-auth/webauthn-lib/src/Event/AttestationObjectLoaded.php b/3rdparty/web-auth/webauthn-lib/src/Event/AttestationObjectLoaded.php new file mode 100644 index 00000000..f3cbb14b --- /dev/null +++ b/3rdparty/web-auth/webauthn-lib/src/Event/AttestationObjectLoaded.php @@ -0,0 +1,20 @@ +credentialId instanceof PublicKeyCredentialSource) { + trigger_deprecation( + 'web-auth/webauthn-lib', + '4.6.0', + 'Passing a string for the argument "$credentialId" is deprecated since 4.6.0. Please set the PublicKeyCredentialSource instead.' + ); + } + } + + /** + * @deprecated since 4.7.0 and will be removed in 5.0.0. Please use the `getCredential()` method instead + * @infection-ignore-all + */ + public function getCredentialId(): string + { + return $this->credentialId instanceof PublicKeyCredentialSource ? $this->credentialId->publicKeyCredentialId : $this->credentialId; + } + + public function getCredential(): ?PublicKeyCredentialSource + { + return $this->credentialId instanceof PublicKeyCredentialSource ? $this->credentialId : null; + } + + /** + * @deprecated since 4.8.0. Will be removed in 5.0.0. Please use the property instead. + */ + public function getAuthenticatorAssertionResponse(): AuthenticatorAssertionResponse + { + return $this->authenticatorAssertionResponse; + } + + public function getPublicKeyCredentialRequestOptions(): PublicKeyCredentialRequestOptions + { + return $this->publicKeyCredentialRequestOptions; + } + + /** + * @deprecated since 4.5.0 and will be removed in 5.0.0. Please use the `host` property instead + * @infection-ignore-all + */ + public function getRequest(): ServerRequestInterface|string + { + return $this->host; + } + + /** + * @deprecated since 4.8.0. Will be removed in 5.0.0. Please use the property instead. + */ + public function getUserHandle(): ?string + { + return $this->userHandle; + } + + /** + * @deprecated since 4.8.0. Will be removed in 5.0.0. Please use the property instead. + */ + public function getThrowable(): Throwable + { + return $this->throwable; + } +} diff --git a/3rdparty/web-auth/webauthn-lib/src/Event/AuthenticatorAssertionResponseValidationSucceededEvent.php b/3rdparty/web-auth/webauthn-lib/src/Event/AuthenticatorAssertionResponseValidationSucceededEvent.php new file mode 100644 index 00000000..88442b17 --- /dev/null +++ b/3rdparty/web-auth/webauthn-lib/src/Event/AuthenticatorAssertionResponseValidationSucceededEvent.php @@ -0,0 +1,90 @@ +credentialId !== null) { + trigger_deprecation( + 'web-auth/webauthn-lib', + '4.6.0', + 'The argument "$credentialId" is deprecated since 4.6.0 and will be removed in 5.0.0. Please set null instead.' + ); + } + } + + /** + * @deprecated since 4.8.0. Will be removed in 5.0.0. Please use the property instead. + */ + public function getCredentialId(): string + { + return $this->publicKeyCredentialSource->publicKeyCredentialId; + } + + /** + * @deprecated since 4.8.0. Will be removed in 5.0.0. Please use the property instead. + */ + public function getAuthenticatorAssertionResponse(): AuthenticatorAssertionResponse + { + return $this->authenticatorAssertionResponse; + } + + /** + * @deprecated since 4.8.0. Will be removed in 5.0.0. Please use the property instead. + */ + public function getPublicKeyCredentialRequestOptions(): PublicKeyCredentialRequestOptions + { + return $this->publicKeyCredentialRequestOptions; + } + + /** + * @deprecated since 4.5.0 and will be removed in 5.0.0. Please use the `host` property instead + * @infection-ignore-all + */ + public function getRequest(): ServerRequestInterface|string + { + return $this->host; + } + + /** + * @deprecated since 4.8.0. Will be removed in 5.0.0. Please use the property instead. + */ + public function getUserHandle(): ?string + { + return $this->userHandle; + } + + /** + * @deprecated since 4.8.0. Will be removed in 5.0.0. Please use the property instead. + */ + public function getPublicKeyCredentialSource(): PublicKeyCredentialSource + { + return $this->publicKeyCredentialSource; + } +} diff --git a/3rdparty/web-auth/webauthn-lib/src/Event/AuthenticatorAttestationResponseValidationFailedEvent.php b/3rdparty/web-auth/webauthn-lib/src/Event/AuthenticatorAttestationResponseValidationFailedEvent.php new file mode 100644 index 00000000..59f7403b --- /dev/null +++ b/3rdparty/web-auth/webauthn-lib/src/Event/AuthenticatorAttestationResponseValidationFailedEvent.php @@ -0,0 +1,65 @@ +authenticatorAttestationResponse; + } + + /** + * @deprecated since 4.8.0. Will be removed in 5.0.0. Please use the property instead. + */ + public function getPublicKeyCredentialCreationOptions(): PublicKeyCredentialCreationOptions + { + return $this->publicKeyCredentialCreationOptions; + } + + /** + * @deprecated since 4.5.0 and will be removed in 5.0.0. Please use the `host` property instead + * @infection-ignore-all + */ + public function getRequest(): ServerRequestInterface|string + { + return $this->host; + } + + /** + * @deprecated since 4.8.0. Will be removed in 5.0.0. Please use the property instead. + */ + public function getThrowable(): Throwable + { + return $this->throwable; + } +} diff --git a/3rdparty/web-auth/webauthn-lib/src/Event/AuthenticatorAttestationResponseValidationSucceededEvent.php b/3rdparty/web-auth/webauthn-lib/src/Event/AuthenticatorAttestationResponseValidationSucceededEvent.php new file mode 100644 index 00000000..59cca951 --- /dev/null +++ b/3rdparty/web-auth/webauthn-lib/src/Event/AuthenticatorAttestationResponseValidationSucceededEvent.php @@ -0,0 +1,65 @@ +authenticatorAttestationResponse; + } + + /** + * @deprecated since 4.8.0. Will be removed in 5.0.0. Please use the property instead. + */ + public function getPublicKeyCredentialCreationOptions(): PublicKeyCredentialCreationOptions + { + return $this->publicKeyCredentialCreationOptions; + } + + /** + * @deprecated since 4.5.0 and will be removed in 5.0.0. Please use the `host` property instead + * @infection-ignore-all + */ + public function getRequest(): ServerRequestInterface|string + { + return $this->host; + } + + /** + * @deprecated since 4.8.0. Will be removed in 5.0.0. Please use the property instead. + */ + public function getPublicKeyCredentialSource(): PublicKeyCredentialSource + { + return $this->publicKeyCredentialSource; + } +} diff --git a/3rdparty/web-auth/webauthn-lib/src/Event/BeforeCertificateChainValidation.php b/3rdparty/web-auth/webauthn-lib/src/Event/BeforeCertificateChainValidation.php new file mode 100644 index 00000000..f871ae03 --- /dev/null +++ b/3rdparty/web-auth/webauthn-lib/src/Event/BeforeCertificateChainValidation.php @@ -0,0 +1,28 @@ + $attestation + */ + public function __construct( + public readonly array $attestation, + string $message, + ?Throwable $previous = null + ) { + parent::__construct($message, $previous); + } + + /** + * @param array $attestation + */ + public static function create( + array $attestation, + string $message = 'Invalid attestation object', + ?Throwable $previous = null + ): self { + return new self($attestation, $message, $previous); + } +} diff --git a/3rdparty/web-auth/webauthn-lib/src/Exception/AttestationStatementVerificationException.php b/3rdparty/web-auth/webauthn-lib/src/Exception/AttestationStatementVerificationException.php new file mode 100644 index 00000000..763e2fed --- /dev/null +++ b/3rdparty/web-auth/webauthn-lib/src/Exception/AttestationStatementVerificationException.php @@ -0,0 +1,15 @@ + $untrustedCertificates + * @param array $trustedCertificates + */ + public function __construct( + public readonly array $untrustedCertificates, + public readonly array $trustedCertificates, + string $message, + ?Throwable $previous = null + ) { + parent::__construct($message, $previous); + } + + /** + * @param array $untrustedCertificates + * @param array $trustedCertificates + */ + public static function create( + array $untrustedCertificates, + array $trustedCertificates, + string $message = 'Unable to validate the certificate chain.', + ?Throwable $previous = null + ): self { + return new self($untrustedCertificates, $trustedCertificates, $message, $previous); + } +} diff --git a/3rdparty/web-auth/webauthn-lib/src/Exception/CertificateException.php b/3rdparty/web-auth/webauthn-lib/src/Exception/CertificateException.php new file mode 100644 index 00000000..25927762 --- /dev/null +++ b/3rdparty/web-auth/webauthn-lib/src/Exception/CertificateException.php @@ -0,0 +1,18 @@ + self::fixPEMStructure($d, $type), $data); + } + + public static function fixPEMStructure(string $data, string $type = 'CERTIFICATE'): string + { + if (str_contains($data, self::PEM_HEADER)) { + return trim($data); + } + $pem = self::PEM_HEADER . $type . '-----' . PHP_EOL; + $pem .= chunk_split($data, 64, PHP_EOL); + + return $pem . (self::PEM_FOOTER . $type . '-----' . PHP_EOL); + } + + /** + * @deprecated since 4.7.0 and will be removed in 5.0.0. No replacement as not used internally. + * @infection-ignore-all + */ + public static function convertPEMToDER(string $data): string + { + if (! str_contains($data, self::PEM_HEADER)) { + return $data; + } + $data = preg_replace('/\-{5}.*\-{5}[\r\n]*/', '', $data); + $data = preg_replace("/[\r\n]*/", '', (string) $data); + + return Base64::decode(trim((string) $data), true); + } + + public static function convertDERToPEM(string $data, string $type = 'CERTIFICATE'): string + { + if (str_contains($data, self::PEM_HEADER)) { + return $data; + } + + return self::fixPEMStructure(base64_encode($data), $type); + } + + /** + * @param string[] $data + * + * @return string[] + */ + public static function convertAllDERToPEM(iterable $data, string $type = 'CERTIFICATE'): array + { + return array_map(static fn ($d): string => self::convertDERToPEM($d, $type), $data); + } +} diff --git a/3rdparty/web-auth/webauthn-lib/src/MetadataService/CertificateChain/PhpCertificateChainValidator.php b/3rdparty/web-auth/webauthn-lib/src/MetadataService/CertificateChain/PhpCertificateChainValidator.php new file mode 100644 index 00000000..b7a676d1 --- /dev/null +++ b/3rdparty/web-auth/webauthn-lib/src/MetadataService/CertificateChain/PhpCertificateChainValidator.php @@ -0,0 +1,295 @@ +clock = $clock; + $this->dispatcher = new NullEventDispatcher(); + } + + public static function create( + HttpClientInterface $client, + null|Clock|ClockInterface $clock = null, + bool $allowFailures = true + ): self { + return new self($client, null, $clock, $allowFailures); + } + + public function setEventDispatcher(EventDispatcherInterface $eventDispatcher): void + { + $this->dispatcher = $eventDispatcher; + } + + /** + * @param string[] $untrustedCertificates + * @param string[] $trustedCertificates + */ + public function check(array $untrustedCertificates, array $trustedCertificates): void + { + foreach ($trustedCertificates as $trustedCertificate) { + $this->dispatcher->dispatch( + BeforeCertificateChainValidation::create($untrustedCertificates, $trustedCertificate) + ); + try { + if ($this->validateChain($untrustedCertificates, $trustedCertificate)) { + $this->dispatcher->dispatch( + CertificateChainValidationSucceeded::create($untrustedCertificates, $trustedCertificate) + ); + return; + } + } catch (Throwable $exception) { + $this->dispatcher->dispatch( + CertificateChainValidationFailed::create($untrustedCertificates, $trustedCertificate) + ); + throw $exception; + } + } + + throw CertificateChainException::create($untrustedCertificates, $trustedCertificates); + } + + /** + * @param string[] $untrustedCertificates + */ + private function validateChain(array $untrustedCertificates, string $trustedCertificate): bool + { + $untrustedCertificates = array_map( + static fn (string $cert): Certificate => Certificate::fromPEM(PEM::fromString($cert)), + array_reverse($untrustedCertificates) + ); + $trustedCertificate = Certificate::fromPEM(PEM::fromString($trustedCertificate)); + + // The trust path and the authenticator certificate are the same + if (count( + $untrustedCertificates + ) === 1 && $untrustedCertificates[0]->toPEM()->string() === $trustedCertificate->toPEM()->string()) { + return true; + } + $uniqueCertificates = array_map( + static fn (Certificate $cert): string => $cert->toPEM() + ->string(), + [...$untrustedCertificates, $trustedCertificate] + ); + count(array_unique($uniqueCertificates)) === count( + $uniqueCertificates + ) || throw CertificateChainException::create( + $untrustedCertificates, + [$trustedCertificate], + 'Invalid certificate chain with duplicated certificates.' + ); + + if (! $this->validateCertificates($trustedCertificate, ...$untrustedCertificates)) { + return false; + } + + $certificates = [$trustedCertificate, ...$untrustedCertificates]; + $numCerts = count($certificates); + for ($i = 1; $i < $numCerts; $i++) { + if ($this->isRevoked($certificates[$i])) { + throw CertificateChainException::create( + $untrustedCertificates, + [$trustedCertificate], + 'Unable to validate the certificate chain. Revoked certificate found.' + ); + } + } + + return true; + } + + private function isRevoked(Certificate $subject): bool + { + try { + $csn = $subject->tbsCertificate() + ->serialNumber(); + } catch (Throwable $e) { + throw InvalidCertificateException::create( + $subject->toPEM() + ->string(), + sprintf('Failed to parse certificate: %s', $e->getMessage()), + $e + ); + } + + try { + $urls = $this->getCrlUrlList($subject); + } catch (Throwable $e) { + if ($this->allowFailures) { + return false; + } + throw InvalidCertificateException::create( + $subject->toPEM() + ->string(), + 'Failed to get CRL distribution points: ' . $e->getMessage(), + $e + ); + } + + foreach ($urls as $url) { + try { + $revokedCertificates = $this->retrieveRevokedSerialNumbers($url); + + if (in_array($csn, $revokedCertificates, true)) { + return true; + } + } catch (Throwable $e) { + if ($this->allowFailures) { + return false; + } + throw CertificateRevocationListException::create($url, sprintf( + 'Failed to retrieve the CRL:' . PHP_EOL . '%s', + $e->getMessage() + ), $e); + } + } + return false; + } + + private function validateCertificates(Certificate ...$certificates): bool + { + try { + $config = PathValidationConfig::create($this->clock->now(), self::MAX_VALIDATION_LENGTH); + CertificationPath::create(...$certificates)->validate($config); + + return true; + } catch (Throwable) { + return false; + } + } + + /** + * @return string[] + */ + private function retrieveRevokedSerialNumbers(string $url): array + { + try { + if ($this->client instanceof HttpClientInterface) { + $crlData = $this->client->request('GET', $url) + ->getContent(); + } else { + $crlData = $this->sendPsrRequest($url); + } + $crl = UnspecifiedType::fromDER($crlData)->asSequence(); + count($crl) === 3 || throw CertificateRevocationListException::create($url); + $tbsCertList = $crl->at(0) + ->asSequence(); + count($tbsCertList) >= 6 || throw CertificateRevocationListException::create($url); + $list = $tbsCertList->at(5) + ->asSequence(); + + return array_map(static function (UnspecifiedType $r) use ($url): string { + $sequence = $r->asSequence(); + count($sequence) >= 1 || throw CertificateRevocationListException::create($url); + return $sequence->at(0) + ->asInteger() + ->number(); + }, $list->elements()); + } catch (Throwable $e) { + throw CertificateRevocationListException::create($url, 'Failed to download the CRL', $e); + } + } + + /** + * @return string[] + */ + private function getCrlUrlList(Certificate $subject): array + { + try { + $urls = []; + + $extensions = $subject->tbsCertificate() + ->extensions(); + if ($extensions->hasCRLDistributionPoints()) { + $crlDists = $extensions->crlDistributionPoints(); + foreach ($crlDists->distributionPoints() as $dist) { + $url = $dist->fullName() + ->names() + ->firstURI(); + $scheme = parse_url($url, PHP_URL_SCHEME); + if (! in_array($scheme, ['http', 'https'], true)) { + continue; + } + $urls[] = $url; + } + } + return $urls; + } catch (Throwable $e) { + throw InvalidCertificateException::create( + $subject->toPEM() + ->string(), + 'Failed to get CRL distribution points from certificate: ' . $e->getMessage(), + $e + ); + } + } + + private function sendPsrRequest(string $url): string + { + $request = $this->requestFactory->createRequest('GET', $url); + $response = $this->client->sendRequest($request); + if ($response->getStatusCode() !== 200) { + throw CertificateRevocationListException::create($url, 'Failed to download the CRL'); + } + + return $response->getBody() + ->getContents(); + } +} diff --git a/3rdparty/web-auth/webauthn-lib/src/MetadataService/Denormalizer/ExtensionDescriptorDenormalizer.php b/3rdparty/web-auth/webauthn-lib/src/MetadataService/Denormalizer/ExtensionDescriptorDenormalizer.php new file mode 100644 index 00000000..b31eec3d --- /dev/null +++ b/3rdparty/web-auth/webauthn-lib/src/MetadataService/Denormalizer/ExtensionDescriptorDenormalizer.php @@ -0,0 +1,14 @@ +create(); + } +} diff --git a/3rdparty/web-auth/webauthn-lib/src/MetadataService/Event/BeforeCertificateChainValidation.php b/3rdparty/web-auth/webauthn-lib/src/MetadataService/Event/BeforeCertificateChainValidation.php new file mode 100644 index 00000000..bf6f5f91 --- /dev/null +++ b/3rdparty/web-auth/webauthn-lib/src/MetadataService/Event/BeforeCertificateChainValidation.php @@ -0,0 +1,14 @@ +requestFactory->createRequest($method, $baseUri . $url); + $body = $options['body'] ?? null; + if ($body !== null) { + $request = $request->withBody($this->streamFactory->createStream($body)); + } + foreach ($this->options as $name => $value) { + $request = $request->withHeader($name, $value); + } + foreach ($options['headers'] ?? [] as $name => $value) { + $request = $request->withHeader($name, $value); + } + $response = $this->client->sendRequest($request); + + return static::fromPsr17($response); + } + + /** + * @param ResponseInterface|iterable $responses + */ + public function stream(iterable|ResponseInterface $responses, float $timeout = null): ResponseStreamInterface + { + throw new LogicException('Not implemented'); + } + + public function withOptions(array $options): static + { + $this->options = $options; + return $this; + } + + protected static function fromPsr17(Psr17ResponseInterface $response): ResponseInterface + { + $headers = $response->getHeaders(); + $content = $response->getBody() + ->getContents(); + $status = $response->getStatusCode(); + + return new class($status, $headers, $content) implements ResponseInterface { + /** + * @param array $headers + */ + public function __construct( + private readonly int $status, + private readonly array $headers, + private readonly string $content, + ) { + } + + public function getStatusCode(): int + { + return $this->status; + } + + /** + * @return array + */ + public function getHeaders(bool $throw = true): array + { + return $this->headers; + } + + public function getContent(bool $throw = true): string + { + return $this->content; + } + + /** + * @return array + */ + public function toArray(bool $throw = true): array + { + $result = json_decode($this->content, true); + if (! is_array($result) || json_last_error() !== JSON_ERROR_NONE) { + throw new JsonException('Failed to decode JSON response: ' . json_last_error_msg()); + } + + return $result; + } + + public function cancel(): void + { + // noop + } + + public function getInfo(string $type = null): mixed + { + return null; + } + }; + } +} diff --git a/3rdparty/web-auth/webauthn-lib/src/MetadataService/Service/ChainedMetadataServices.php b/3rdparty/web-auth/webauthn-lib/src/MetadataService/Service/ChainedMetadataServices.php new file mode 100644 index 00000000..f6cae8b7 --- /dev/null +++ b/3rdparty/web-auth/webauthn-lib/src/MetadataService/Service/ChainedMetadataServices.php @@ -0,0 +1,66 @@ +addServices($service); + } + } + + public static function create(MetadataService ...$services): self + { + return new self(...$services); + } + + public function addServices(MetadataService ...$services): self + { + foreach ($services as $service) { + $this->services[] = $service; + } + + return $this; + } + + public function list(): iterable + { + foreach ($this->services as $service) { + yield from $service->list(); + } + } + + public function has(string $aaguid): bool + { + foreach ($this->services as $service) { + if ($service->has($aaguid)) { + return true; + } + } + + return false; + } + + public function get(string $aaguid): MetadataStatement + { + foreach ($this->services as $service) { + if ($service->has($aaguid)) { + return $service->get($aaguid); + } + } + + throw MissingMetadataStatementException::create($aaguid); + } +} diff --git a/3rdparty/web-auth/webauthn-lib/src/MetadataService/Service/DistantResourceMetadataService.php b/3rdparty/web-auth/webauthn-lib/src/MetadataService/Service/DistantResourceMetadataService.php new file mode 100644 index 00000000..e8100694 --- /dev/null +++ b/3rdparty/web-auth/webauthn-lib/src/MetadataService/Service/DistantResourceMetadataService.php @@ -0,0 +1,170 @@ + $additionalHeaderParameters + */ + public function __construct( + private readonly ?RequestFactoryInterface $requestFactory, + private readonly ClientInterface|HttpClientInterface $httpClient, + private readonly string $uri, + private readonly bool $isBase64Encoded = false, + private readonly array $additionalHeaderParameters = [], + ?SerializerInterface $serializer = null, + ) { + if ($requestFactory !== null && ! $httpClient instanceof HttpClientInterface) { + trigger_deprecation( + 'web-auth/metadata-service', + '4.7.0', + 'The parameter "$requestFactory" will be removed in 5.0.0. Please set it to null and set an Symfony\Contracts\HttpClient\HttpClientInterface as "$httpClient" argument.' + ); + } + $this->serializer = $serializer ?? (new WebauthnSerializerFactory( + AttestationStatementSupportManager::create() + ))->create(); + $this->dispatcher = new NullEventDispatcher(); + } + + public function setEventDispatcher(EventDispatcherInterface $eventDispatcher): void + { + $this->dispatcher = $eventDispatcher; + } + + /** + * @param array $additionalHeaderParameters + */ + public static function create( + ?RequestFactoryInterface $requestFactory, + ClientInterface|HttpClientInterface $httpClient, + string $uri, + bool $isBase64Encoded = false, + array $additionalHeaderParameters = [], + ?SerializerInterface $serializer = null + ): self { + return new self($requestFactory, $httpClient, $uri, $isBase64Encoded, $additionalHeaderParameters, $serializer); + } + + public function list(): iterable + { + $this->loadData(); + $this->statement !== null || throw MetadataStatementLoadingException::create(); + $aaguid = $this->statement->aaguid; + if ($aaguid === null) { + yield from []; + } else { + yield from [$aaguid]; + } + } + + public function has(string $aaguid): bool + { + $this->loadData(); + $this->statement !== null || throw MetadataStatementLoadingException::create(); + + return $aaguid === $this->statement->aaguid; + } + + public function get(string $aaguid): MetadataStatement + { + $this->loadData(); + $this->statement !== null || throw MetadataStatementLoadingException::create(); + + if ($aaguid === $this->statement->aaguid) { + $this->dispatcher->dispatch(MetadataStatementFound::create($this->statement)); + + return $this->statement; + } + + throw MissingMetadataStatementException::create($aaguid); + } + + private function loadData(): void + { + if ($this->statement !== null) { + return; + } + + $content = $this->fetch(); + if ($this->isBase64Encoded) { + $content = Base64::decode($content, true); + } + if ($this->serializer !== null) { + $this->statement = $this->serializer->deserialize($content, MetadataStatement::class, 'json'); + return; + } + + $this->statement = MetadataStatement::createFromString($content); + } + + private function fetch(): string + { + if ($this->httpClient instanceof HttpClientInterface) { + $content = $this->sendSymfonyRequest(); + } else { + $content = $this->sendPsrRequest(); + } + $content !== '' || throw MetadataStatementLoadingException::create( + 'Unable to contact the server. The response has no content' + ); + + return $content; + } + + private function sendPsrRequest(): string + { + $request = $this->requestFactory->createRequest('GET', $this->uri); + foreach ($this->additionalHeaderParameters as $k => $v) { + $request = $request->withHeader($k, $v); + } + $response = $this->httpClient->sendRequest($request); + $response->getStatusCode() === 200 || throw MetadataStatementLoadingException::create(sprintf( + 'Unable to contact the server. Response code is %d', + $response->getStatusCode() + )); + $response->getBody() + ->rewind(); + + return $response->getBody() + ->getContents(); + } + + private function sendSymfonyRequest(): string + { + $response = $this->httpClient->request('GET', $this->uri, [ + 'headers' => $this->additionalHeaderParameters, + ]); + $response->getStatusCode() === 200 || throw MetadataStatementLoadingException::create(sprintf( + 'Unable to contact the server. Response code is %d', + $response->getStatusCode() + )); + + return $response->getContent(); + } +} diff --git a/3rdparty/web-auth/webauthn-lib/src/MetadataService/Service/FidoAllianceCompliantMetadataService.php b/3rdparty/web-auth/webauthn-lib/src/MetadataService/Service/FidoAllianceCompliantMetadataService.php new file mode 100644 index 00000000..5182a810 --- /dev/null +++ b/3rdparty/web-auth/webauthn-lib/src/MetadataService/Service/FidoAllianceCompliantMetadataService.php @@ -0,0 +1,286 @@ +> + */ + private array $statusReports = []; + + private EventDispatcherInterface $dispatcher; + + private readonly ?SerializerInterface $serializer; + + /** + * @param array $additionalHeaderParameters + */ + public function __construct( + private readonly ?RequestFactoryInterface $requestFactory, + private readonly ClientInterface|HttpClientInterface $httpClient, + private readonly string $uri, + private readonly array $additionalHeaderParameters = [], + private readonly ?CertificateChainValidator $certificateChainValidator = null, + private readonly ?string $rootCertificateUri = null, + ?SerializerInterface $serializer = null, + ) { + if ($requestFactory !== null && ! $httpClient instanceof HttpClientInterface) { + trigger_deprecation( + 'web-auth/metadata-service', + '4.7.0', + 'The parameter "$requestFactory" will be removed in 5.0.0. Please set it to null and set an Symfony\Contracts\HttpClient\HttpClientInterface as "$httpClient" argument.' + ); + } + $this->serializer = $serializer ?? (new WebauthnSerializerFactory( + AttestationStatementSupportManager::create() + ))->create(); + $this->dispatcher = new NullEventDispatcher(); + } + + public function setEventDispatcher(EventDispatcherInterface $eventDispatcher): void + { + $this->dispatcher = $eventDispatcher; + } + + /** + * @param array $additionalHeaderParameters + */ + public static function create( + ?RequestFactoryInterface $requestFactory, + ClientInterface|HttpClientInterface $httpClient, + string $uri, + array $additionalHeaderParameters = [], + ?CertificateChainValidator $certificateChainValidator = null, + ?string $rootCertificateUri = null, + ?SerializerInterface $serializer = null, + ): self { + return new self( + $requestFactory, + $httpClient, + $uri, + $additionalHeaderParameters, + $certificateChainValidator, + $rootCertificateUri, + $serializer, + ); + } + + /** + * @return string[] + */ + public function list(): iterable + { + $this->loadData(); + + yield from array_keys($this->statements); + } + + public function has(string $aaguid): bool + { + $this->loadData(); + + return array_key_exists($aaguid, $this->statements); + } + + public function get(string $aaguid): MetadataStatement + { + $this->loadData(); + array_key_exists($aaguid, $this->statements) || throw MissingMetadataStatementException::create($aaguid); + $mds = $this->statements[$aaguid]; + $this->dispatcher->dispatch(MetadataStatementFound::create($mds)); + + return $mds; + } + + /** + * @return StatusReport[] + */ + public function getStatusReports(string $aaguid): iterable + { + $this->loadData(); + + return $this->statusReports[$aaguid] ?? []; + } + + private function loadData(): void + { + if ($this->loaded) { + return; + } + + $content = $this->fetch($this->uri, $this->additionalHeaderParameters); + $jwtCertificates = []; + try { + $payload = $this->getJwsPayload($content, $jwtCertificates); + $this->validateCertificates(...$jwtCertificates); + if ($this->serializer !== null) { + $blob = $this->serializer->deserialize($payload, MetadataBLOBPayload::class, 'json'); + foreach ($blob->entries as $entry) { + $mds = $entry->metadataStatement; + if ($mds !== null && $entry->aaguid !== null) { + $this->statements[$entry->aaguid] = $mds; + $this->statusReports[$entry->aaguid] = $entry->statusReports; + } + } + $this->loaded = true; + return; + } + $data = json_decode($payload, true, flags: JSON_THROW_ON_ERROR); + + foreach ($data['entries'] as $datum) { + $entry = MetadataBLOBPayloadEntry::createFromArray($datum); + + $mds = $entry->metadataStatement; + if ($mds !== null && $entry->aaguid !== null) { + $this->statements[$entry->aaguid] = $mds; + $this->statusReports[$entry->aaguid] = $entry->statusReports; + } + } + } catch (Throwable) { + // Nothing to do + } + + $this->loaded = true; + } + + /** + * @param array $headerParameters + */ + private function fetch(string $uri, array $headerParameters): string + { + if ($this->httpClient instanceof HttpClientInterface) { + $content = $this->sendSymfonyRequest($uri, $headerParameters); + } else { + $content = $this->sendPsrRequest($uri, $headerParameters); + } + $content !== '' || throw MetadataStatementLoadingException::create( + 'Unable to contact the server. The response has no content' + ); + + return $content; + } + + /** + * @param string[] $rootCertificates + */ + private function getJwsPayload(string $token, array &$rootCertificates): string + { + $jws = (new CompactSerializer())->unserialize($token); + $jws->countSignatures() === 1 || throw MetadataStatementLoadingException::create( + 'Invalid response from the metadata service. Only one signature shall be present.' + ); + $signature = $jws->getSignature(0); + $payload = $jws->getPayload(); + $payload !== '' || throw MetadataStatementLoadingException::create( + 'Invalid response from the metadata service. The token payload is empty.' + ); + $header = $signature->getProtectedHeader(); + array_key_exists('alg', $header) || throw MetadataStatementLoadingException::create( + 'The "alg" parameter is missing.' + ); + array_key_exists('x5c', $header) || throw MetadataStatementLoadingException::create( + 'The "x5c" parameter is missing.' + ); + is_array($header['x5c']) || throw MetadataStatementLoadingException::create( + 'The "x5c" parameter should be an array.' + ); + $key = JWKFactory::createFromX5C($header['x5c']); + $rootCertificates = $header['x5c']; + + $verifier = new JWSVerifier(new AlgorithmManager([new ES256(), new RS256()])); + $isValid = $verifier->verifyWithKey($jws, $key, 0); + $isValid || throw MetadataStatementLoadingException::create( + 'Invalid response from the metadata service. The token signature is invalid.' + ); + $payload = $jws->getPayload(); + $payload !== null || throw MetadataStatementLoadingException::create( + 'Invalid response from the metadata service. The payload is missing.' + ); + + return $payload; + } + + private function validateCertificates(string ...$untrustedCertificates): void + { + if ($this->certificateChainValidator === null || $this->rootCertificateUri === null) { + return; + } + $untrustedCertificates = CertificateToolbox::fixPEMStructures($untrustedCertificates); + $rootCertificate = CertificateToolbox::convertDERToPEM($this->fetch($this->rootCertificateUri, [])); + $this->certificateChainValidator->check($untrustedCertificates, [$rootCertificate]); + } + + /** + * @param array $headerParameters + */ + private function sendPsrRequest(string $uri, array $headerParameters): string + { + $request = $this->requestFactory->createRequest('GET', $uri); + foreach ($headerParameters as $k => $v) { + $request = $request->withHeader($k, $v); + } + $response = $this->httpClient->sendRequest($request); + $response->getStatusCode() === 200 || throw MetadataStatementLoadingException::create(sprintf( + 'Unable to contact the server. Response code is %d', + $response->getStatusCode() + )); + $response->getBody() + ->rewind(); + return $response->getBody() + ->getContents(); + } + + /** + * @param array $headerParameters + */ + private function sendSymfonyRequest(string $uri, array $headerParameters): string + { + $response = $this->httpClient->request('GET', $uri, [ + 'headers' => $headerParameters, + ]); + $response->getStatusCode() === 200 || throw MetadataStatementLoadingException::create(sprintf( + 'Unable to contact the server. Response code is %d', + $response->getStatusCode() + )); + + return $response->getContent(); + } +} diff --git a/3rdparty/web-auth/webauthn-lib/src/MetadataService/Service/FolderResourceMetadataService.php b/3rdparty/web-auth/webauthn-lib/src/MetadataService/Service/FolderResourceMetadataService.php new file mode 100644 index 00000000..e554ed9a --- /dev/null +++ b/3rdparty/web-auth/webauthn-lib/src/MetadataService/Service/FolderResourceMetadataService.php @@ -0,0 +1,79 @@ +serializer = $serializer ?? (new WebauthnSerializerFactory( + AttestationStatementSupportManager::create() + ))->create(); + $this->rootPath = rtrim($rootPath, DIRECTORY_SEPARATOR); + is_dir($this->rootPath) || throw new InvalidArgumentException('The given parameter is not a valid folder.'); + is_readable($this->rootPath) || throw new InvalidArgumentException( + 'The given parameter is not a valid folder.' + ); + } + + public static function create(string $rootPath, ?SerializerInterface $serializer = null): self + { + return new self($rootPath, $serializer); + } + + public function list(): iterable + { + $files = glob($this->rootPath . DIRECTORY_SEPARATOR . '*'); + is_array($files) || throw MetadataStatementLoadingException::create('Unable to read files.'); + foreach ($files as $file) { + if (is_dir($file) || ! is_readable($file)) { + continue; + } + + yield basename($file); + } + } + + public function has(string $aaguid): bool + { + $filename = $this->rootPath . DIRECTORY_SEPARATOR . $aaguid; + + return is_file($filename) && is_readable($filename); + } + + public function get(string $aaguid): MetadataStatement + { + $this->has($aaguid) || throw new InvalidArgumentException(sprintf( + 'The MDS with the AAGUID "%s" does not exist.', + $aaguid + )); + $filename = $this->rootPath . DIRECTORY_SEPARATOR . $aaguid; + $data = trim(file_get_contents($filename)); + if ($this->serializer !== null) { + $mds = $this->serializer->deserialize($data, MetadataStatement::class, 'json'); + } else { + $mds = MetadataStatement::createFromString($data); + } + + $mds->aaguid !== null || throw MetadataStatementLoadingException::create('Invalid Metadata Statement.'); + + return $mds; + } +} diff --git a/3rdparty/web-auth/webauthn-lib/src/MetadataService/Service/InMemoryMetadataService.php b/3rdparty/web-auth/webauthn-lib/src/MetadataService/Service/InMemoryMetadataService.php new file mode 100644 index 00000000..6ff6f994 --- /dev/null +++ b/3rdparty/web-auth/webauthn-lib/src/MetadataService/Service/InMemoryMetadataService.php @@ -0,0 +1,73 @@ +addStatements($statement); + } + $this->dispatcher = new NullEventDispatcher(); + } + + public function setEventDispatcher(EventDispatcherInterface $eventDispatcher): void + { + $this->dispatcher = $eventDispatcher; + } + + public static function create(MetadataStatement ...$statements): self + { + return new self(...$statements); + } + + public function addStatements(MetadataStatement ...$statements): self + { + foreach ($statements as $statement) { + $aaguid = $statement->aaguid; + if ($aaguid === null) { + continue; + } + $this->statements[$aaguid] = $statement; + } + + return $this; + } + + public function list(): iterable + { + yield from array_keys($this->statements); + } + + public function has(string $aaguid): bool + { + return array_key_exists($aaguid, $this->statements); + } + + public function get(string $aaguid): MetadataStatement + { + array_key_exists($aaguid, $this->statements) || throw MissingMetadataStatementException::create($aaguid); + $mds = $this->statements[$aaguid]; + $this->dispatcher->dispatch(MetadataStatementFound::create($mds)); + + return $mds; + } +} diff --git a/3rdparty/web-auth/webauthn-lib/src/MetadataService/Service/JsonMetadataService.php b/3rdparty/web-auth/webauthn-lib/src/MetadataService/Service/JsonMetadataService.php new file mode 100644 index 00000000..62bb5df4 --- /dev/null +++ b/3rdparty/web-auth/webauthn-lib/src/MetadataService/Service/JsonMetadataService.php @@ -0,0 +1,81 @@ +dispatcher = new NullEventDispatcher(); + $this->serializer = $serializer ?? (new WebauthnSerializerFactory( + AttestationStatementSupportManager::create() + ))->create(); + foreach ($statements as $statement) { + $this->addStatement($statement); + } + } + + public function setEventDispatcher(EventDispatcherInterface $eventDispatcher): void + { + $this->dispatcher = $eventDispatcher; + } + + public function list(): iterable + { + yield from array_keys($this->statements); + } + + public function has(string $aaguid): bool + { + return array_key_exists($aaguid, $this->statements); + } + + public function get(string $aaguid): MetadataStatement + { + array_key_exists($aaguid, $this->statements) || throw MissingMetadataStatementException::create($aaguid); + $mds = $this->statements[$aaguid]; + $this->dispatcher->dispatch(MetadataStatementFound::create($mds)); + + return $mds; + } + + private function addStatement(string $statement): void + { + if ($this->serializer === null) { + $mds = MetadataStatement::createFromString($statement); + } else { + $mds = $this->serializer->deserialize($statement, MetadataStatement::class, 'json'); + } + if ($mds->aaguid === null) { + return; + } + $this->statements[$mds->aaguid] = $mds; + } +} diff --git a/3rdparty/web-auth/webauthn-lib/src/MetadataService/Service/LocalResourceMetadataService.php b/3rdparty/web-auth/webauthn-lib/src/MetadataService/Service/LocalResourceMetadataService.php new file mode 100644 index 00000000..c510d4fd --- /dev/null +++ b/3rdparty/web-auth/webauthn-lib/src/MetadataService/Service/LocalResourceMetadataService.php @@ -0,0 +1,102 @@ +serializer = $serializer ?? (new WebauthnSerializerFactory( + AttestationStatementSupportManager::create() + ))->create(); + $this->dispatcher = new NullEventDispatcher(); + } + + public static function create( + string $filename, + bool $isBase64Encoded = false, + ?SerializerInterface $serializer = null + ): self { + return new self($filename, $isBase64Encoded, $serializer); + } + + public function setEventDispatcher(EventDispatcherInterface $eventDispatcher): void + { + $this->dispatcher = $eventDispatcher; + } + + public function list(): iterable + { + $this->loadData(); + $this->statement !== null || throw MetadataStatementLoadingException::create(); + $aaguid = $this->statement->aaguid; + if ($aaguid === null) { + yield from []; + } else { + yield from [$aaguid]; + } + } + + public function has(string $aaguid): bool + { + $this->loadData(); + $this->statement !== null || throw MetadataStatementLoadingException::create(); + + return $aaguid === $this->statement->aaguid; + } + + public function get(string $aaguid): MetadataStatement + { + $this->loadData(); + $this->statement !== null || throw MetadataStatementLoadingException::create(); + + if ($aaguid === $this->statement->aaguid) { + $this->dispatcher->dispatch(MetadataStatementFound::create($this->statement)); + + return $this->statement; + } + + throw MissingMetadataStatementException::create($aaguid); + } + + private function loadData(): void + { + if ($this->statement !== null) { + return; + } + + $content = file_get_contents($this->filename); + if ($this->isBase64Encoded) { + $content = Base64::decode($content, true); + } + if ($this->serializer !== null) { + $this->statement = $this->serializer->deserialize($content, MetadataStatement::class, 'json'); + } else { + $this->statement = MetadataStatement::createFromString($content); + } + } +} diff --git a/3rdparty/web-auth/webauthn-lib/src/MetadataService/Service/MetadataBLOBPayload.php b/3rdparty/web-auth/webauthn-lib/src/MetadataService/Service/MetadataBLOBPayload.php new file mode 100644 index 00000000..0bd5df80 --- /dev/null +++ b/3rdparty/web-auth/webauthn-lib/src/MetadataService/Service/MetadataBLOBPayload.php @@ -0,0 +1,162 @@ +entries[] = $entry; + + return $this; + } + + /** + * @deprecated since 4.7.0. Please use the property directly. + * @infection-ignore-all + */ + public function getLegalHeader(): ?string + { + return $this->legalHeader; + } + + /** + * @deprecated since 4.7.0. Please use the property directly. + * @infection-ignore-all + */ + public function getNo(): int + { + return $this->no; + } + + /** + * @deprecated since 4.7.0. Please use the property directly. + * @infection-ignore-all + */ + public function getNextUpdate(): string + { + return $this->nextUpdate; + } + + /** + * @return MetadataBLOBPayloadEntry[] + * @deprecated since 4.7.0. Please use the property directly. + * @infection-ignore-all + */ + public function getEntries(): array + { + return $this->entries; + } + + /** + * @param array $data + * @deprecated since 4.7.0. Please use the symfony/serializer for converting the object. + * @infection-ignore-all + */ + public static function createFromArray(array $data): self + { + $data = self::filterNullValues($data); + foreach (['no', 'nextUpdate', 'entries'] as $key) { + array_key_exists($key, $data) || throw MetadataStatementLoadingException::create(sprintf( + 'Invalid data. The parameter "%s" is missing', + $key + )); + } + is_int($data['no']) || throw MetadataStatementLoadingException::create( + 'Invalid data. The parameter "no" shall be an integer' + ); + is_string($data['nextUpdate']) || throw MetadataStatementLoadingException::create( + 'Invalid data. The parameter "nextUpdate" shall be a string' + ); + is_array($data['entries']) || throw MetadataStatementLoadingException::create( + 'Invalid data. The parameter "entries" shall be a n array of entries' + ); + $object = new self($data['no'], $data['nextUpdate'], $data['legalHeader'] ?? null); + foreach ($data['entries'] as $entry) { + $object->entries[] = MetadataBLOBPayloadEntry::createFromArray($entry); + } + + return $object; + } + + /** + * @return array + */ + public function jsonSerialize(): array + { + trigger_deprecation( + 'web-auth/webauthn-bundle', + '4.9.0', + 'The "%s" method is deprecated and will be removed in 5.0. Please use the serializer instead.', + __METHOD__ + ); + trigger_deprecation( + 'web-auth/webauthn-bundle', + '4.9.0', + 'The "%s" method is deprecated and will be removed in 5.0. Please use the serializer instead.', + __METHOD__ + ); + $data = [ + 'legalHeader' => $this->legalHeader, + 'nextUpdate' => $this->nextUpdate, + 'no' => $this->no, + 'entries' => $this->entries, + ]; + + return self::filterNullValues($data); + } + + /** + * @return string[] + * @deprecated since 4.7.0. Please use the property directly. + * @infection-ignore-all + */ + public function getRootCertificates(): array + { + return $this->rootCertificates; + } + + /** + * @param string[] $rootCertificates + * @deprecated since 4.7.0. Please use the property directly. + * @infection-ignore-all + */ + public function setRootCertificates(array $rootCertificates): self + { + $this->rootCertificates = $rootCertificates; + + return $this; + } +} diff --git a/3rdparty/web-auth/webauthn-lib/src/MetadataService/Service/MetadataBLOBPayloadEntry.php b/3rdparty/web-auth/webauthn-lib/src/MetadataService/Service/MetadataBLOBPayloadEntry.php new file mode 100644 index 00000000..3e06cfde --- /dev/null +++ b/3rdparty/web-auth/webauthn-lib/src/MetadataService/Service/MetadataBLOBPayloadEntry.php @@ -0,0 +1,231 @@ +aaid; + } + + /** + * @deprecated since 4.7.0. Please use the property directly. + * @infection-ignore-all + */ + public function getAaguid(): ?string + { + return $this->aaguid; + } + + /** + * @return string[] + * @deprecated since 4.7.0. Please use the property directly. + * @infection-ignore-all + */ + public function getAttestationCertificateKeyIdentifiers(): array + { + return $this->attestationCertificateKeyIdentifiers; + } + + /** + * @deprecated since 4.7.0. Please use the property directly. + * @infection-ignore-all + */ + public function getMetadataStatement(): ?MetadataStatement + { + return $this->metadataStatement; + } + + /** + * @deprecated since 4.7.0. Please use the property directly. + * @infection-ignore-all + */ + public function addBiometricStatusReports(BiometricStatusReport ...$biometricStatusReports): self + { + foreach ($biometricStatusReports as $biometricStatusReport) { + $this->biometricStatusReports[] = $biometricStatusReport; + } + + return $this; + } + + /** + * @return BiometricStatusReport[] + * @deprecated since 4.7.0. Please use the property directly. + * @infection-ignore-all + */ + public function getBiometricStatusReports(): array + { + return $this->biometricStatusReports; + } + + /** + * @deprecated since 4.7.0. Please use the property directly. + * @infection-ignore-all + */ + public function addStatusReports(StatusReport ...$statusReports): self + { + foreach ($statusReports as $statusReport) { + $this->statusReports[] = $statusReport; + } + + return $this; + } + + /** + * @return StatusReport[] + * @deprecated since 4.7.0. Please use the property directly. + * @infection-ignore-all + */ + public function getStatusReports(): array + { + return $this->statusReports; + } + + /** + * @deprecated since 4.7.0. Please use the property directly. + * @infection-ignore-all + */ + public function getTimeOfLastStatusChange(): string + { + return $this->timeOfLastStatusChange; + } + + /** + * @deprecated since 4.7.0. Please use the property directly. + * @infection-ignore-all + */ + public function getRogueListURL(): string|null + { + return $this->rogueListURL; + } + + /** + * @deprecated since 4.7.0. Please use the property directly. + * @infection-ignore-all + */ + public function getRogueListHash(): string|null + { + return $this->rogueListHash; + } + + /** + * @param array $data + * @deprecated since 4.7.0. Please use the symfony/serializer for converting the object. + * @infection-ignore-all + */ + public static function createFromArray(array $data): self + { + $data = self::filterNullValues($data); + array_key_exists('timeOfLastStatusChange', $data) || throw MetadataStatementLoadingException::create( + 'Invalid data. The parameter "timeOfLastStatusChange" is missing' + ); + array_key_exists('statusReports', $data) || throw MetadataStatementLoadingException::create( + 'Invalid data. The parameter "statusReports" is missing' + ); + is_array($data['statusReports']) || throw MetadataStatementLoadingException::create( + 'Invalid data. The parameter "statusReports" shall be an array of StatusReport objects' + ); + + return new self( + $data['timeOfLastStatusChange'], + array_map( + static fn (array $statusReport) => StatusReport::createFromArray($statusReport), + $data['statusReports'] + ), + $data['aaid'] ?? null, + $data['aaguid'] ?? null, + $data['attestationCertificateKeyIdentifiers'] ?? [], + isset($data['metadataStatement']) ? MetadataStatement::createFromArray($data['metadataStatement']) : null, + $data['rogueListURL'] ?? null, + $data['rogueListHash'] ?? null, + array_map( + static fn (array $biometricStatusReport) => BiometricStatusReport::createFromArray( + $biometricStatusReport + ), + $data['biometricStatusReports'] ?? [] + ) + ); + } + + /** + * @return array + */ + public function jsonSerialize(): array + { + trigger_deprecation( + 'web-auth/webauthn-bundle', + '4.9.0', + 'The "%s" method is deprecated and will be removed in 5.0. Please use the serializer instead.', + __METHOD__ + ); + $data = [ + 'aaid' => $this->aaid, + 'aaguid' => $this->aaguid, + 'attestationCertificateKeyIdentifiers' => $this->attestationCertificateKeyIdentifiers, + 'statusReports' => $this->statusReports, + 'timeOfLastStatusChange' => $this->timeOfLastStatusChange, + 'rogueListURL' => $this->rogueListURL, + 'rogueListHash' => $this->rogueListHash, + ]; + + return self::filterNullValues($data); + } +} diff --git a/3rdparty/web-auth/webauthn-lib/src/MetadataService/Service/MetadataService.php b/3rdparty/web-auth/webauthn-lib/src/MetadataService/Service/MetadataService.php new file mode 100644 index 00000000..3bc676df --- /dev/null +++ b/3rdparty/web-auth/webauthn-lib/src/MetadataService/Service/MetadataService.php @@ -0,0 +1,19 @@ +addStatements(MetadataStatement::createFromString($statement)); + } + $this->dispatcher = new NullEventDispatcher(); + } + + public function setEventDispatcher(EventDispatcherInterface $eventDispatcher): void + { + $this->dispatcher = $eventDispatcher; + } + + public static function create(string ...$statements): self + { + return new self(...$statements); + } + + public function addStatements(MetadataStatement ...$statements): self + { + foreach ($statements as $statement) { + $aaguid = $statement->aaguid; + if ($aaguid === null) { + continue; + } + $this->statements[$aaguid] = $statement; + } + + return $this; + } + + public function list(): iterable + { + yield from array_keys($this->statements); + } + + public function has(string $aaguid): bool + { + return array_key_exists($aaguid, $this->statements); + } + + public function get(string $aaguid): MetadataStatement + { + array_key_exists($aaguid, $this->statements) || throw MissingMetadataStatementException::create($aaguid); + $mds = $this->statements[$aaguid]; + $this->dispatcher->dispatch(MetadataStatementFound::create($mds)); + + return $mds; + } +} diff --git a/3rdparty/web-auth/webauthn-lib/src/MetadataService/Statement/AbstractDescriptor.php b/3rdparty/web-auth/webauthn-lib/src/MetadataService/Statement/AbstractDescriptor.php new file mode 100644 index 00000000..494cc060 --- /dev/null +++ b/3rdparty/web-auth/webauthn-lib/src/MetadataService/Statement/AbstractDescriptor.php @@ -0,0 +1,41 @@ += 0 || throw MetadataStatementLoadingException::create( + 'Invalid data. The value of "maxRetries" must be a positive integer' + ); + $blockSlowdown >= 0 || throw MetadataStatementLoadingException::create( + 'Invalid data. The value of "blockSlowdown" must be a positive integer' + ); + } + + /** + * @deprecated since 4.7.0. Please use the property directly. + * @infection-ignore-all + */ + public function getMaxRetries(): ?int + { + return $this->maxRetries; + } + + /** + * @deprecated since 4.7.0. Please use the property directly. + * @infection-ignore-all + */ + public function getBlockSlowdown(): ?int + { + return $this->blockSlowdown; + } +} diff --git a/3rdparty/web-auth/webauthn-lib/src/MetadataService/Statement/AlternativeDescriptions.php b/3rdparty/web-auth/webauthn-lib/src/MetadataService/Statement/AlternativeDescriptions.php new file mode 100644 index 00000000..0baf0f2f --- /dev/null +++ b/3rdparty/web-auth/webauthn-lib/src/MetadataService/Statement/AlternativeDescriptions.php @@ -0,0 +1,61 @@ + $descriptions + */ + public function __construct( + public array $descriptions = [] + ) { + } + + /** + * @param array $descriptions + */ + public static function create(array $descriptions = []): self + { + return new self($descriptions); + } + + /** + * @return array + * @deprecated since 4.7.0. Please use the property directly. + * @infection-ignore-all + */ + public function all(): array + { + return $this->descriptions; + } + + /** + * @deprecated since 4.7.0. Please use the property directly. + * @infection-ignore-all + */ + public function add(string $locale, string $description): self + { + $this->descriptions[$locale] = $description; + + return $this; + } + + /** + * @return array + */ + public function jsonSerialize(): array + { + trigger_deprecation( + 'web-auth/webauthn-bundle', + '4.9.0', + 'The "%s" method is deprecated and will be removed in 5.0. Please use the serializer instead.', + __METHOD__ + ); + return $this->descriptions; + } +} diff --git a/3rdparty/web-auth/webauthn-lib/src/MetadataService/Statement/AuthenticatorGetInfo.php b/3rdparty/web-auth/webauthn-lib/src/MetadataService/Statement/AuthenticatorGetInfo.php new file mode 100644 index 00000000..c1360a61 --- /dev/null +++ b/3rdparty/web-auth/webauthn-lib/src/MetadataService/Statement/AuthenticatorGetInfo.php @@ -0,0 +1,51 @@ + $info + */ + public function __construct( + public array $info = [] + ) { + } + + /** + * @param array $info + */ + public static function create(array $info = []): self + { + return new self($info); + } + + /** + * @deprecated since 4.7.0. Please use the constructor directly. + * @infection-ignore-all + */ + public function add(string|int $key, mixed $value): self + { + $this->info[$key] = $value; + + return $this; + } + + /** + * @return string[] + */ + public function jsonSerialize(): array + { + trigger_deprecation( + 'web-auth/webauthn-bundle', + '4.9.0', + 'The "%s" method is deprecated and will be removed in 5.0. Please use the serializer instead.', + __METHOD__ + ); + return $this->info; + } +} diff --git a/3rdparty/web-auth/webauthn-lib/src/MetadataService/Statement/AuthenticatorStatus.php b/3rdparty/web-auth/webauthn-lib/src/MetadataService/Statement/AuthenticatorStatus.php new file mode 100644 index 00000000..03a008be --- /dev/null +++ b/3rdparty/web-auth/webauthn-lib/src/MetadataService/Statement/AuthenticatorStatus.php @@ -0,0 +1,72 @@ +selfAttestedFRR; + } + + /** + * @deprecated since 4.7.0. Please use the property directly. + */ + public function getSelfAttestedFAR(): ?float + { + return $this->selfAttestedFAR; + } + + /** + * @deprecated since 4.7.0. Please use the property directly. + */ + public function getMaxTemplates(): ?float + { + return $this->maxTemplates; + } + + /** + * @param array $data + * @deprecated since 4.7.0. Please use the symfony/serializer for converting the object. + */ + public static function createFromArray(array $data): self + { + return self::create( + $data['selfAttestedFRR'] ?? null, + $data['selfAttestedFAR'] ?? null, + $data['maxTemplates'] ?? null, + $data['maxRetries'] ?? null, + $data['blockSlowdown'] ?? null + ); + } + + /** + * @return array + */ + public function jsonSerialize(): array + { + trigger_deprecation( + 'web-auth/webauthn-bundle', + '4.9.0', + 'The "%s" method is deprecated and will be removed in 5.0. Please use the serializer instead.', + __METHOD__ + ); + $data = [ + 'selfAttestedFRR' => $this->selfAttestedFRR, + 'selfAttestedFAR' => $this->selfAttestedFAR, + 'maxTemplates' => $this->maxTemplates, + 'maxRetries' => $this->maxRetries, + 'blockSlowdown' => $this->blockSlowdown, + ]; + + return self::filterNullValues($data); + } +} diff --git a/3rdparty/web-auth/webauthn-lib/src/MetadataService/Statement/BiometricStatusReport.php b/3rdparty/web-auth/webauthn-lib/src/MetadataService/Statement/BiometricStatusReport.php new file mode 100644 index 00000000..ab5472f8 --- /dev/null +++ b/3rdparty/web-auth/webauthn-lib/src/MetadataService/Statement/BiometricStatusReport.php @@ -0,0 +1,146 @@ +certLevel; + } + + /** + * @deprecated since 4.7.0. Please use the property directly. + * @infection-ignore-all + */ + public function getModality(): int|null + { + return $this->modality; + } + + /** + * @deprecated since 4.7.0. Please use the property directly. + * @infection-ignore-all + */ + public function getEffectiveDate(): ?string + { + return $this->effectiveDate; + } + + /** + * @deprecated since 4.7.0. Please use the property directly. + * @infection-ignore-all + */ + public function getCertificationDescriptor(): ?string + { + return $this->certificationDescriptor; + } + + /** + * @deprecated since 4.7.0. Please use the property directly. + * @infection-ignore-all + */ + public function getCertificateNumber(): ?string + { + return $this->certificateNumber; + } + + /** + * @deprecated since 4.7.0. Please use the property directly. + * @infection-ignore-all + */ + public function getCertificationPolicyVersion(): ?string + { + return $this->certificationPolicyVersion; + } + + /** + * @deprecated since 4.7.0. Please use the property directly. + * @infection-ignore-all + */ + public function getCertificationRequirementsVersion(): ?string + { + return $this->certificationRequirementsVersion; + } + + /** + * @param array $data + * @deprecated since 4.7.0. Please use the symfony/serializer for converting the object. + * @infection-ignore-all + */ + public static function createFromArray(array $data): self + { + return self::create( + $data['certLevel'] ?? null, + $data['modality'] ?? null, + $data['effectiveDate'] ?? null, + $data['certificationDescriptor'] ?? null, + $data['certificateNumber'] ?? null, + $data['certificationPolicyVersion'] ?? null, + $data['certificationRequirementsVersion'] ?? null, + ); + } + + /** + * @return array + */ + public function jsonSerialize(): array + { + trigger_deprecation( + 'web-auth/webauthn-bundle', + '4.9.0', + 'The "%s" method is deprecated and will be removed in 5.0. Please use the serializer instead.', + __METHOD__ + ); + $data = [ + 'certLevel' => $this->certLevel, + 'modality' => $this->modality, + 'effectiveDate' => $this->effectiveDate, + 'certificationDescriptor' => $this->certificationDescriptor, + 'certificateNumber' => $this->certificateNumber, + 'certificationPolicyVersion' => $this->certificationPolicyVersion, + 'certificationRequirementsVersion' => $this->certificationRequirementsVersion, + ]; + + return array_filter($data, static fn ($var): bool => $var !== null); + } +} diff --git a/3rdparty/web-auth/webauthn-lib/src/MetadataService/Statement/CodeAccuracyDescriptor.php b/3rdparty/web-auth/webauthn-lib/src/MetadataService/Statement/CodeAccuracyDescriptor.php new file mode 100644 index 00000000..aa7f8f0f --- /dev/null +++ b/3rdparty/web-auth/webauthn-lib/src/MetadataService/Statement/CodeAccuracyDescriptor.php @@ -0,0 +1,95 @@ += 0 || throw MetadataStatementLoadingException::create( + 'Invalid data. The value of "base" must be a positive integer' + ); + $minLength >= 0 || throw MetadataStatementLoadingException::create( + 'Invalid data. The value of "minLength" must be a positive integer' + ); + parent::__construct($maxRetries, $blockSlowdown); + } + + public static function create(int $base, int $minLength, ?int $maxRetries = null, ?int $blockSlowdown = null): self + { + return new self($base, $minLength, $maxRetries, $blockSlowdown); + } + + /** + * @deprecated since 4.7.0. Please use the property directly. + * @infection-ignore-all + */ + public function getBase(): int + { + return $this->base; + } + + /** + * @deprecated since 4.7.0. Please use the property directly. + * @infection-ignore-all + */ + public function getMinLength(): int + { + return $this->minLength; + } + + /** + * @param array $data + * @deprecated since 4.7.0. Please use the symfony/serializer for converting the object. + * @infection-ignore-all + */ + public static function createFromArray(array $data): self + { + array_key_exists('base', $data) || throw MetadataStatementLoadingException::create( + 'The parameter "base" is missing' + ); + array_key_exists('minLength', $data) || throw MetadataStatementLoadingException::create( + 'The parameter "minLength" is missing' + ); + + return self::create( + $data['base'], + $data['minLength'], + $data['maxRetries'] ?? null, + $data['blockSlowdown'] ?? null + ); + } + + /** + * @return array + */ + public function jsonSerialize(): array + { + trigger_deprecation( + 'web-auth/webauthn-bundle', + '4.9.0', + 'The "%s" method is deprecated and will be removed in 5.0. Please use the serializer instead.', + __METHOD__ + ); + $data = [ + 'base' => $this->base, + 'minLength' => $this->minLength, + 'maxRetries' => $this->maxRetries, + 'blockSlowdown' => $this->blockSlowdown, + ]; + + return self::filterNullValues($data); + } +} diff --git a/3rdparty/web-auth/webauthn-lib/src/MetadataService/Statement/DisplayPNGCharacteristicsDescriptor.php b/3rdparty/web-auth/webauthn-lib/src/MetadataService/Statement/DisplayPNGCharacteristicsDescriptor.php new file mode 100644 index 00000000..f6970303 --- /dev/null +++ b/3rdparty/web-auth/webauthn-lib/src/MetadataService/Statement/DisplayPNGCharacteristicsDescriptor.php @@ -0,0 +1,206 @@ += 0 || throw MetadataStatementLoadingException::create('Invalid width'); + $height >= 0 || throw MetadataStatementLoadingException::create('Invalid height'); + ($bitDepth >= 0 && $bitDepth <= 254) || throw MetadataStatementLoadingException::create('Invalid bit depth'); + ($colorType >= 0 && $colorType <= 254) || throw MetadataStatementLoadingException::create( + 'Invalid color type' + ); + ($compression >= 0 && $compression <= 254) || throw MetadataStatementLoadingException::create( + 'Invalid compression' + ); + ($filter >= 0 && $filter <= 254) || throw MetadataStatementLoadingException::create('Invalid filter'); + ($interlace >= 0 && $interlace <= 254) || throw MetadataStatementLoadingException::create( + 'Invalid interlace' + ); + } + + /** + * @param RgbPaletteEntry[] $plte + */ + public static function create( + int $width, + int $height, + int $bitDepth, + int $colorType, + int $compression, + int $filter, + int $interlace, + array $plte = [] + ): self { + return new self($width, $height, $bitDepth, $colorType, $compression, $filter, $interlace, $plte); + } + + /** + * @deprecated since 4.7.0. Please use {self::create} directly. + * @infection-ignore-all + */ + public function addPalettes(RgbPaletteEntry ...$rgbPaletteEntries): self + { + foreach ($rgbPaletteEntries as $rgbPaletteEntry) { + $this->plte[] = $rgbPaletteEntry; + } + + return $this; + } + + /** + * @deprecated since 4.7.0. Please use the property directly. + * @infection-ignore-all + */ + public function getWidth(): int + { + return $this->width; + } + + /** + * @deprecated since 4.7.0. Please use the property directly. + * @infection-ignore-all + */ + public function getHeight(): int + { + return $this->height; + } + + /** + * @deprecated since 4.7.0. Please use the property directly. + * @infection-ignore-all + */ + public function getBitDepth(): int + { + return $this->bitDepth; + } + + /** + * @deprecated since 4.7.0. Please use the property directly. + * @infection-ignore-all + */ + public function getColorType(): int + { + return $this->colorType; + } + + /** + * @deprecated since 4.7.0. Please use the property directly. + * @infection-ignore-all + */ + public function getCompression(): int + { + return $this->compression; + } + + /** + * @deprecated since 4.7.0. Please use the property directly. + * @infection-ignore-all + */ + public function getFilter(): int + { + return $this->filter; + } + + /** + * @deprecated since 4.7.0. Please use the property directly. + * @infection-ignore-all + */ + public function getInterlace(): int + { + return $this->interlace; + } + + /** + * @return RgbPaletteEntry[] + * @deprecated since 4.7.0. Please use the property directly. + * @infection-ignore-all + */ + public function getPaletteEntries(): array + { + return $this->plte; + } + + /** + * @param array $data + * @deprecated since 4.7.0. Please use the symfony/serializer for converting the object. + * @infection-ignore-all + */ + public static function createFromArray(array $data): self + { + $data = self::filterNullValues($data); + foreach ([ + 'width', + 'compression', + 'height', + 'bitDepth', + 'colorType', + 'compression', + 'filter', + 'interlace', + ] as $key) { + array_key_exists($key, $data) || throw MetadataStatementLoadingException::create(sprintf( + 'Invalid data. The key "%s" is missing', + $key + )); + } + return self::create( + $data['width'], + $data['height'], + $data['bitDepth'], + $data['colorType'], + $data['compression'], + $data['filter'], + $data['interlace'], + array_map(static fn (array $item) => RgbPaletteEntry::createFromArray($item), $data['plte'] ?? []) + ); + } + + /** + * @return array + */ + public function jsonSerialize(): array + { + trigger_deprecation( + 'web-auth/webauthn-bundle', + '4.9.0', + 'The "%s" method is deprecated and will be removed in 5.0. Please use the serializer instead.', + __METHOD__ + ); + $data = [ + 'width' => $this->width, + 'height' => $this->height, + 'bitDepth' => $this->bitDepth, + 'colorType' => $this->colorType, + 'compression' => $this->compression, + 'filter' => $this->filter, + 'interlace' => $this->interlace, + 'plte' => $this->plte, + ]; + + return self::filterNullValues($data); + } +} diff --git a/3rdparty/web-auth/webauthn-lib/src/MetadataService/Statement/EcdaaTrustAnchor.php b/3rdparty/web-auth/webauthn-lib/src/MetadataService/Statement/EcdaaTrustAnchor.php new file mode 100644 index 00000000..c55171b3 --- /dev/null +++ b/3rdparty/web-auth/webauthn-lib/src/MetadataService/Statement/EcdaaTrustAnchor.php @@ -0,0 +1,107 @@ +X; + } + + public function getY(): string + { + return $this->Y; + } + + public function getC(): string + { + return $this->c; + } + + public function getSx(): string + { + return $this->sx; + } + + public function getSy(): string + { + return $this->sy; + } + + public function getG1Curve(): string + { + return $this->G1Curve; + } + + /** + * @param array $data + * @deprecated since 4.7.0. Please use the symfony/serializer for converting the object. + */ + public static function createFromArray(array $data): self + { + $data = self::filterNullValues($data); + foreach (['X', 'Y', 'c', 'sx', 'sy', 'G1Curve'] as $key) { + array_key_exists($key, $data) || throw MetadataStatementLoadingException::create(sprintf( + 'Invalid data. The key "%s" is missing', + $key + )); + } + + return new self( + Base64UrlSafe::decode($data['X']), + Base64UrlSafe::decode($data['Y']), + Base64UrlSafe::decode($data['c']), + Base64UrlSafe::decode($data['sx']), + Base64UrlSafe::decode($data['sy']), + $data['G1Curve'] + ); + } + + /** + * @return array + */ + public function jsonSerialize(): array + { + trigger_deprecation( + 'web-auth/webauthn-bundle', + '4.9.0', + 'The "%s" method is deprecated and will be removed in 5.0. Please use the serializer instead.', + __METHOD__ + ); + $data = [ + 'X' => Base64UrlSafe::encodeUnpadded($this->X), + 'Y' => Base64UrlSafe::encodeUnpadded($this->Y), + 'c' => Base64UrlSafe::encodeUnpadded($this->c), + 'sx' => Base64UrlSafe::encodeUnpadded($this->sx), + 'sy' => Base64UrlSafe::encodeUnpadded($this->sy), + 'G1Curve' => $this->G1Curve, + ]; + + return self::filterNullValues($data); + } +} diff --git a/3rdparty/web-auth/webauthn-lib/src/MetadataService/Statement/ExtensionDescriptor.php b/3rdparty/web-auth/webauthn-lib/src/MetadataService/Statement/ExtensionDescriptor.php new file mode 100644 index 00000000..9c6b0a76 --- /dev/null +++ b/3rdparty/web-auth/webauthn-lib/src/MetadataService/Statement/ExtensionDescriptor.php @@ -0,0 +1,112 @@ += 0 || throw MetadataStatementLoadingException::create( + 'Invalid data. The parameter "tag" shall be a positive integer' + ); + } + } + + public static function create( + string $id, + ?int $tag = null, + ?string $data = null, + bool $failIfUnknown = false + ): self { + return new self($id, $tag, $data, $failIfUnknown); + } + + /** + * @deprecated since 4.7.0. Please use the property directly. + * @infection-ignore-all + */ + public function getId(): string + { + return $this->id; + } + + /** + * @deprecated since 4.7.0. Please use the property directly. + * @infection-ignore-all + */ + public function getTag(): ?int + { + return $this->tag; + } + + /** + * @deprecated since 4.7.0. Please use the property directly. + * @infection-ignore-all + */ + public function getData(): ?string + { + return $this->data; + } + + /** + * @deprecated since 4.7.0. Please use the property directly. + * @infection-ignore-all + */ + public function isFailIfUnknown(): bool + { + return $this->failIfUnknown; + } + + /** + * @param array $data + * @deprecated since 4.7.0. Please use the symfony/serializer for converting the object. + * @infection-ignore-all + */ + public static function createFromArray(array $data): self + { + $data = self::filterNullValues($data); + array_key_exists('id', $data) || throw MetadataStatementLoadingException::create( + 'Invalid data. The parameter "id" is missing' + ); + array_key_exists('fail_if_unknown', $data) || throw MetadataStatementLoadingException::create( + 'Invalid data. The parameter "fail_if_unknown" is missing' + ); + + return new self($data['id'], $data['tag'] ?? null, $data['data'] ?? null, $data['fail_if_unknown']); + } + + /** + * @return array + */ + public function jsonSerialize(): array + { + trigger_deprecation( + 'web-auth/webauthn-bundle', + '4.9.0', + 'The "%s" method is deprecated and will be removed in 5.0. Please use the serializer instead.', + __METHOD__ + ); + $result = [ + 'id' => $this->id, + 'tag' => $this->tag, + 'data' => $this->data, + 'fail_if_unknown' => $this->failIfUnknown, + ]; + + return self::filterNullValues($result); + } +} diff --git a/3rdparty/web-auth/webauthn-lib/src/MetadataService/Statement/MetadataStatement.php b/3rdparty/web-auth/webauthn-lib/src/MetadataService/Statement/MetadataStatement.php new file mode 100644 index 00000000..899430ea --- /dev/null +++ b/3rdparty/web-auth/webauthn-lib/src/MetadataService/Statement/MetadataStatement.php @@ -0,0 +1,700 @@ +authenticatorGetInfo = $authenticatorGetInfo ?? AuthenticatorGetInfo::create($attestationTypes); + } + + public static function create( + string $description, + int $authenticatorVersion, + string $protocolFamily, + int $schema, + array $upv, + array $authenticationAlgorithms, + array $publicKeyAlgAndEncodings, + array $attestationTypes, + array $userVerificationDetails, + array $matcherProtection, + array $tcDisplay, + array $attestationRootCertificates, + array $alternativeDescriptions = [], + ?string $legalHeader = null, + ?string $aaid = null, + ?string $aaguid = null, + array $attestationCertificateKeyIdentifiers = [], + array $keyProtection = [], + ?bool $isKeyRestricted = null, + ?bool $isFreshUserVerificationRequired = null, + ?int $cryptoStrength = null, + array $attachmentHint = [], + ?string $tcDisplayContentType = null, + array $tcDisplayPNGCharacteristics = [], + array $ecdaaTrustAnchors = [], + ?string $icon = null, + array $supportedExtensions = [], + ?AuthenticatorGetInfo $authenticatorGetInfo = null, + ): self { + return new self( + $description, + $authenticatorVersion, + $protocolFamily, + $schema, + $upv, + $authenticationAlgorithms, + $publicKeyAlgAndEncodings, + $attestationTypes, + $userVerificationDetails, + $matcherProtection, + $tcDisplay, + $attestationRootCertificates, + AlternativeDescriptions::create($alternativeDescriptions), + $legalHeader, + $aaid, + $aaguid, + $attestationCertificateKeyIdentifiers, + $keyProtection, + $isKeyRestricted, + $isFreshUserVerificationRequired, + $cryptoStrength, + $attachmentHint, + $tcDisplayContentType, + $tcDisplayPNGCharacteristics, + $ecdaaTrustAnchors, + $icon, + $supportedExtensions, + $authenticatorGetInfo, + ); + } + + /** + * @deprecated since 4.7.0. Please use the symfony/serializer for converting the object. + * @infection-ignore-all + */ + public static function createFromString(string $statement): self + { + $data = json_decode($statement, true, flags: JSON_THROW_ON_ERROR); + + return self::createFromArray($data); + } + + /** + * @deprecated since 4.7.0. Please use the property directly. + * @infection-ignore-all + */ + public function getLegalHeader(): ?string + { + return $this->legalHeader; + } + + /** + * @deprecated since 4.7.0. Please use the property directly. + * @infection-ignore-all + */ + public function getAaid(): ?string + { + return $this->aaid; + } + + /** + * @deprecated since 4.7.0. Please use the property directly. + * @infection-ignore-all + */ + public function getAaguid(): ?string + { + return $this->aaguid; + } + + public function isKeyRestricted(): ?bool + { + return $this->isKeyRestricted; + } + + /** + * @deprecated since 4.7.0. Please use the property directly. + * @infection-ignore-all + */ + public function isFreshUserVerificationRequired(): ?bool + { + return $this->isFreshUserVerificationRequired; + } + + /** + * @deprecated since 4.7.0. Please use the property directly. + * @infection-ignore-all + */ + public function getAuthenticatorGetInfo(): AuthenticatorGetInfo|null + { + return $this->authenticatorGetInfo; + } + + /** + * @return string[] + * @deprecated since 4.7.0. Please use the property directly. + * @infection-ignore-all + */ + public function getAttestationCertificateKeyIdentifiers(): array + { + return $this->attestationCertificateKeyIdentifiers; + } + + /** + * @deprecated since 4.7.0. Please use the property directly. + * @infection-ignore-all + */ + public function getDescription(): string + { + return $this->description; + } + + /** + * @deprecated since 4.7.0. Please use the property directly. + * @infection-ignore-all + */ + public function getAlternativeDescriptions(): null|AlternativeDescriptions + { + return $this->alternativeDescriptions; + } + + /** + * @deprecated since 4.7.0. Please use the property directly. + * @infection-ignore-all + */ + public function getAuthenticatorVersion(): int + { + return $this->authenticatorVersion; + } + + /** + * @deprecated since 4.7.0. Please use the property directly. + * @infection-ignore-all + */ + public function getProtocolFamily(): string + { + return $this->protocolFamily; + } + + /** + * @return Version[] + * @deprecated since 4.7.0. Please use the property directly. + * @infection-ignore-all + */ + public function getUpv(): array + { + return $this->upv; + } + + /** + * @deprecated since 4.7.0. Please use the property directly. + * @infection-ignore-all + */ + public function getSchema(): ?int + { + return $this->schema; + } + + /** + * @return string[] + * @deprecated since 4.7.0. Please use the property directly. + * @infection-ignore-all + */ + public function getAuthenticationAlgorithms(): array + { + return $this->authenticationAlgorithms; + } + + /** + * @return string[] + * @deprecated since 4.7.0. Please use the property directly. + * @infection-ignore-all + */ + public function getPublicKeyAlgAndEncodings(): array + { + return $this->publicKeyAlgAndEncodings; + } + + /** + * @return string[] + * @deprecated since 4.7.0. Please use the property directly. + * @infection-ignore-all + */ + public function getAttestationTypes(): array + { + return $this->attestationTypes; + } + + /** + * @return VerificationMethodANDCombinations[] + * @deprecated since 4.7.0. Please use the property directly. + * @infection-ignore-all + */ + public function getUserVerificationDetails(): array + { + return $this->userVerificationDetails; + } + + /** + * @return string[] + * @deprecated since 4.7.0. Please use the property directly. + * @infection-ignore-all + */ + public function getKeyProtection(): array + { + return $this->keyProtection; + } + + /** + * @return string[] + * @deprecated since 4.7.0. Please use the property directly. + * @infection-ignore-all + */ + public function getMatcherProtection(): array + { + return $this->matcherProtection; + } + + /** + * @deprecated since 4.7.0. Please use the property directly. + * @infection-ignore-all + */ + public function getCryptoStrength(): ?int + { + return $this->cryptoStrength; + } + + /** + * @return string[] + * @deprecated since 4.7.0. Please use the property directly. + * @infection-ignore-all + */ + public function getAttachmentHint(): array + { + return $this->attachmentHint; + } + + /** + * @return string[] + * @deprecated since 4.7.0. Please use the property directly. + * @infection-ignore-all + */ + public function getTcDisplay(): array + { + return $this->tcDisplay; + } + + /** + * @deprecated since 4.7.0. Please use the property directly. + * @infection-ignore-all + */ + public function getTcDisplayContentType(): ?string + { + return $this->tcDisplayContentType; + } + + /** + * @return DisplayPNGCharacteristicsDescriptor[] + * @deprecated since 4.7.0. Please use the property directly. + * @infection-ignore-all + */ + public function getTcDisplayPNGCharacteristics(): array + { + return $this->tcDisplayPNGCharacteristics; + } + + /** + * @return string[] + * @deprecated since 4.7.0. Please use the property directly. + * @infection-ignore-all + */ + public function getAttestationRootCertificates(): array + { + return $this->attestationRootCertificates; + } + + /** + * @return EcdaaTrustAnchor[] + * + * @deprecated since 4.2.0 and will be removed in 5.0.0. The ECDAA Trust Anchor does no longer exist in Webauthn specification. + * @infection-ignore-all + */ + public function getEcdaaTrustAnchors(): array + { + return $this->ecdaaTrustAnchors; + } + + /** + * @deprecated since 4.7.0. Please use the property directly. + * @infection-ignore-all + */ + public function getIcon(): ?string + { + return $this->icon; + } + + /** + * @return ExtensionDescriptor[] + * @deprecated since 4.7.0. Please use the property directly. + * @infection-ignore-all + */ + public function getSupportedExtensions(): array + { + return $this->supportedExtensions; + } + + /** + * @param array $data + * @deprecated since 4.7.0. Please use the symfony/serializer for converting the object. + * @infection-ignore-all + */ + public static function createFromArray(array $data): self + { + $requiredKeys = [ + 'description', + 'authenticatorVersion', + 'protocolFamily', + 'schema', + 'upv', + 'authenticationAlgorithms', + 'publicKeyAlgAndEncodings', + 'attestationTypes', + 'userVerificationDetails', + 'matcherProtection', + 'tcDisplay', + 'attestationRootCertificates', + ]; + foreach ($requiredKeys as $key) { + array_key_exists($key, $data) || throw MetadataStatementLoadingException::create(sprintf( + 'Invalid data. The key "%s" is missing', + $key + )); + } + $subObjects = [ + 'authenticationAlgorithms', + 'publicKeyAlgAndEncodings', + 'attestationTypes', + 'matcherProtection', + 'tcDisplay', + 'attestationRootCertificates', + ]; + foreach ($subObjects as $subObject) { + is_array($data[$subObject]) || throw MetadataStatementLoadingException::create(sprintf( + 'Invalid Metadata Statement. The parameter "%s" shall be a list of strings.', + $subObject + )); + foreach ($data[$subObject] as $datum) { + is_string($datum) || throw MetadataStatementLoadingException::create(sprintf( + 'Invalid Metadata Statement. The parameter "%s" shall be a list of strings.', + $subObject + )); + } + } + + return self::create( + $data['description'], + $data['authenticatorVersion'], + $data['protocolFamily'], + $data['schema'], + array_map(static function ($upv): Version { + is_array($upv) || throw MetadataStatementLoadingException::create( + 'Invalid Metadata Statement. The parameter "upv" shall be a list of objects.' + ); + + return Version::createFromArray($upv); + }, $data['upv']), + $data['authenticationAlgorithms'], + $data['publicKeyAlgAndEncodings'], + $data['attestationTypes'], + array_map(static function ($userVerificationDetails): VerificationMethodANDCombinations { + is_array($userVerificationDetails) || throw MetadataStatementLoadingException::create( + 'Invalid Metadata Statement. The parameter "userVerificationDetails" shall be a list of objects.' + ); + + return VerificationMethodANDCombinations::createFromArray($userVerificationDetails); + }, $data['userVerificationDetails']), + $data['matcherProtection'], + $data['tcDisplay'], + CertificateToolbox::fixPEMStructures($data['attestationRootCertificates']), + $data['alternativeDescriptions'] ?? [], + $data['legalHeader'] ?? null, + $data['aaid'] ?? null, + $data['aaguid'] ?? null, + $data['attestationCertificateKeyIdentifiers'] ?? [], + $data['keyProtection'] ?? [], + $data['isKeyRestricted'] ?? null, + $data['isFreshUserVerificationRequired'] ?? null, + $data['cryptoStrength'] ?? null, + $data['attachmentHint'] ?? [], + $data['tcDisplayContentType'] ?? null, + array_map( + static fn (array $data): DisplayPNGCharacteristicsDescriptor => DisplayPNGCharacteristicsDescriptor::createFromArray( + $data + ), + $data['tcDisplayPNGCharacteristics'] ?? [] + ), + $data['ecdaaTrustAnchors'] ?? [], + $data['icon'] ?? null, + array_map( + static fn ($supportedExtension): ExtensionDescriptor => ExtensionDescriptor::createFromArray( + $supportedExtension + ), + $data['supportedExtensions'] ?? [] + ), + isset($data['authenticatorGetInfo']) ? AuthenticatorGetInfo::create($data['authenticatorGetInfo']) : null, + ); + } + + /** + * @return array + */ + public function jsonSerialize(): array + { + trigger_deprecation( + 'web-auth/webauthn-bundle', + '4.9.0', + 'The "%s" method is deprecated and will be removed in 5.0. Please use the serializer instead.', + __METHOD__ + ); + $data = [ + 'legalHeader' => $this->legalHeader, + 'aaid' => $this->aaid, + 'aaguid' => $this->aaguid, + 'attestationCertificateKeyIdentifiers' => $this->attestationCertificateKeyIdentifiers, + 'description' => $this->description, + 'alternativeDescriptions' => $this->alternativeDescriptions, + 'authenticatorVersion' => $this->authenticatorVersion, + 'protocolFamily' => $this->protocolFamily, + 'schema' => $this->schema, + 'upv' => $this->upv, + 'authenticationAlgorithms' => $this->authenticationAlgorithms, + 'publicKeyAlgAndEncodings' => $this->publicKeyAlgAndEncodings, + 'attestationTypes' => $this->attestationTypes, + 'userVerificationDetails' => $this->userVerificationDetails, + 'keyProtection' => $this->keyProtection, + 'isKeyRestricted' => $this->isKeyRestricted, + 'isFreshUserVerificationRequired' => $this->isFreshUserVerificationRequired, + 'matcherProtection' => $this->matcherProtection, + 'cryptoStrength' => $this->cryptoStrength, + 'attachmentHint' => $this->attachmentHint, + 'tcDisplay' => $this->tcDisplay, + 'tcDisplayContentType' => $this->tcDisplayContentType, + 'tcDisplayPNGCharacteristics' => $this->tcDisplayPNGCharacteristics, + 'attestationRootCertificates' => CertificateToolbox::fixPEMStructures($this->attestationRootCertificates), + 'ecdaaTrustAnchors' => $this->ecdaaTrustAnchors, + 'icon' => $this->icon, + 'authenticatorGetInfo' => $this->authenticatorGetInfo, + 'supportedExtensions' => $this->supportedExtensions, + ]; + + return self::filterNullValues($data); + } +} diff --git a/3rdparty/web-auth/webauthn-lib/src/MetadataService/Statement/PatternAccuracyDescriptor.php b/3rdparty/web-auth/webauthn-lib/src/MetadataService/Statement/PatternAccuracyDescriptor.php new file mode 100644 index 00000000..f065bbee --- /dev/null +++ b/3rdparty/web-auth/webauthn-lib/src/MetadataService/Statement/PatternAccuracyDescriptor.php @@ -0,0 +1,82 @@ += 0 || throw MetadataStatementLoadingException::create( + 'Invalid data. The value of "minComplexity" must be a positive integer' + ); + parent::__construct($maxRetries, $blockSlowdown); + } + + public static function create(int $minComplexity, ?int $maxRetries = null, ?int $blockSlowdown = null): self + { + return new self($minComplexity, $maxRetries, $blockSlowdown); + } + + /** + * @deprecated since 4.7.0. Please use the property directly. + * @infection-ignore-all + */ + public function getMinComplexity(): int + { + return $this->minComplexity; + } + + /** + * @param array $data + * @deprecated since 4.7.0. Please use the symfony/serializer for converting the object. + * @infection-ignore-all + */ + public static function createFromArray(array $data): self + { + $data = self::filterNullValues($data); + array_key_exists('minComplexity', $data) || throw MetadataStatementLoadingException::create( + 'The key "minComplexity" is missing' + ); + foreach (['minComplexity', 'maxRetries', 'blockSlowdown'] as $key) { + if (array_key_exists($key, $data)) { + is_int($data[$key]) || throw MetadataStatementLoadingException::create( + sprintf('Invalid data. The value of "%s" must be a positive integer', $key) + ); + } + } + + return self::create($data['minComplexity'], $data['maxRetries'] ?? null, $data['blockSlowdown'] ?? null); + } + + /** + * @return array + */ + public function jsonSerialize(): array + { + trigger_deprecation( + 'web-auth/webauthn-bundle', + '4.9.0', + 'The "%s" method is deprecated and will be removed in 5.0. Please use the serializer instead.', + __METHOD__ + ); + $data = [ + 'minComplexity' => $this->minComplexity, + 'maxRetries' => $this->maxRetries, + 'blockSlowdown' => $this->blockSlowdown, + ]; + + return self::filterNullValues($data); + } +} diff --git a/3rdparty/web-auth/webauthn-lib/src/MetadataService/Statement/RgbPaletteEntry.php b/3rdparty/web-auth/webauthn-lib/src/MetadataService/Statement/RgbPaletteEntry.php new file mode 100644 index 00000000..ca850789 --- /dev/null +++ b/3rdparty/web-auth/webauthn-lib/src/MetadataService/Statement/RgbPaletteEntry.php @@ -0,0 +1,93 @@ += 0 && $r <= 255) || throw MetadataStatementLoadingException::create('The key "r" is invalid'); + ($g >= 0 && $g <= 255) || throw MetadataStatementLoadingException::create('The key "g" is invalid'); + ($b >= 0 && $b <= 255) || throw MetadataStatementLoadingException::create('The key "b" is invalid'); + } + + public static function create(int $r, int $g, int $b): self + { + return new self($r, $g, $b); + } + + /** + * @deprecated since 4.7.0. Please use the property directly. + * @infection-ignore-all + */ + public function getR(): int + { + return $this->r; + } + + /** + * @deprecated since 4.7.0. Please use the property directly. + * @infection-ignore-all + */ + public function getG(): int + { + return $this->g; + } + + /** + * @deprecated since 4.7.0. Please use the property directly. + * @infection-ignore-all + */ + public function getB(): int + { + return $this->b; + } + + /** + * @param array $data + * @deprecated since 4.7.0. Please use the symfony/serializer for converting the object. + * @infection-ignore-all + */ + public static function createFromArray(array $data): self + { + foreach (['r', 'g', 'b'] as $key) { + array_key_exists($key, $data) || throw MetadataStatementLoadingException::create(sprintf( + 'The key "%s" is missing', + $key + )); + is_int($data[$key]) || throw MetadataStatementLoadingException::create( + sprintf('The key "%s" is invalid', $key) + ); + } + + return self::create($data['r'], $data['g'], $data['b']); + } + + /** + * @return array + */ + public function jsonSerialize(): array + { + trigger_deprecation( + 'web-auth/webauthn-bundle', + '4.9.0', + 'The "%s" method is deprecated and will be removed in 5.0. Please use the serializer instead.', + __METHOD__ + ); + return [ + 'r' => $this->r, + 'g' => $this->g, + 'b' => $this->b, + ]; + } +} diff --git a/3rdparty/web-auth/webauthn-lib/src/MetadataService/Statement/RogueListEntry.php b/3rdparty/web-auth/webauthn-lib/src/MetadataService/Statement/RogueListEntry.php new file mode 100644 index 00000000..4cf5f292 --- /dev/null +++ b/3rdparty/web-auth/webauthn-lib/src/MetadataService/Statement/RogueListEntry.php @@ -0,0 +1,76 @@ +sk; + } + + /** + * @deprecated since 4.7.0. Please use the property directly. + * @infection-ignore-all + */ + public function getDate(): ?string + { + return $this->date; + } + + /** + * @param array $data + * @deprecated since 4.7.0. Please use the symfony/serializer for converting the object. + * @infection-ignore-all + */ + public static function createFromArray(array $data): self + { + array_key_exists('sk', $data) || throw MetadataStatementLoadingException::create('The key "sk" is missing'); + is_string($data['sk']) || throw MetadataStatementLoadingException::create('The key "date" is invalid'); + array_key_exists('date', $data) || throw MetadataStatementLoadingException::create( + 'The key "date" is missing' + ); + is_string($data['date']) || throw MetadataStatementLoadingException::create('The key "date" is invalid'); + + return self::create($data['sk'], $data['date']); + } + + /** + * @return array + */ + public function jsonSerialize(): array + { + trigger_deprecation( + 'web-auth/webauthn-bundle', + '4.9.0', + 'The "%s" method is deprecated and will be removed in 5.0. Please use the serializer instead.', + __METHOD__ + ); + return [ + 'sk' => $this->sk, + 'date' => $this->date, + ]; + } +} diff --git a/3rdparty/web-auth/webauthn-lib/src/MetadataService/Statement/StatusReport.php b/3rdparty/web-auth/webauthn-lib/src/MetadataService/Statement/StatusReport.php new file mode 100644 index 00000000..c1fba596 --- /dev/null +++ b/3rdparty/web-auth/webauthn-lib/src/MetadataService/Statement/StatusReport.php @@ -0,0 +1,205 @@ +status, [ + AuthenticatorStatus::ATTESTATION_KEY_COMPROMISE, + AuthenticatorStatus::USER_KEY_PHYSICAL_COMPROMISE, + AuthenticatorStatus::USER_KEY_REMOTE_COMPROMISE, + AuthenticatorStatus::USER_VERIFICATION_BYPASS, + ], true); + } + + /** + * @deprecated since 4.7.0. Please use the property directly. + * @infection-ignore-all + */ + public function getStatus(): string + { + return $this->status; + } + + /** + * @deprecated since 4.7.0. Please use the property directly. + * @infection-ignore-all + */ + public function getEffectiveDate(): ?string + { + return $this->effectiveDate; + } + + /** + * @deprecated since 4.7.0. Please use the property directly. + * @infection-ignore-all + */ + public function getCertificate(): ?string + { + return $this->certificate; + } + + /** + * @deprecated since 4.7.0. Please use the property directly. + * @infection-ignore-all + */ + public function getUrl(): ?string + { + return $this->url; + } + + /** + * @deprecated since 4.7.0. Please use the property directly. + * @infection-ignore-all + */ + public function getCertificationDescriptor(): ?string + { + return $this->certificationDescriptor; + } + + /** + * @deprecated since 4.7.0. Please use the property directly. + * @infection-ignore-all + */ + public function getCertificateNumber(): ?string + { + return $this->certificateNumber; + } + + /** + * @deprecated since 4.7.0. Please use the property directly. + * @infection-ignore-all + */ + public function getCertificationPolicyVersion(): ?string + { + return $this->certificationPolicyVersion; + } + + /** + * @deprecated since 4.7.0. Please use the property directly. + * @infection-ignore-all + */ + public function getCertificationRequirementsVersion(): ?string + { + return $this->certificationRequirementsVersion; + } + + /** + * @param array $data + * @deprecated since 4.7.0. Please use the symfony/serializer for converting the object. + * @infection-ignore-all + */ + public static function createFromArray(array $data): self + { + $data = self::filterNullValues($data); + array_key_exists('status', $data) || throw MetadataStatementLoadingException::create( + 'The key "status" is missing' + ); + foreach ([ + 'effectiveDate', + 'certificate', + 'url', + 'certificationDescriptor', + 'certificateNumber', + 'certificationPolicyVersion', + 'certificationRequirementsVersion', + ] as $key) { + if (isset($data[$key])) { + $value = $data[$key]; + $value === null || is_string($value) || throw MetadataStatementLoadingException::create(sprintf( + 'The value of the key "%s" is invalid', + $key + )); + } + } + + return self::create( + $data['status'], + $data['effectiveDate'] ?? null, + $data['certificate'] ?? null, + $data['url'] ?? null, + $data['certificationDescriptor'] ?? null, + $data['certificateNumber'] ?? null, + $data['certificationPolicyVersion'] ?? null, + $data['certificationRequirementsVersion'] ?? null + ); + } + + /** + * @return array + */ + public function jsonSerialize(): array + { + trigger_deprecation( + 'web-auth/webauthn-bundle', + '4.9.0', + 'The "%s" method is deprecated and will be removed in 5.0. Please use the serializer instead.', + __METHOD__ + ); + $data = [ + 'status' => $this->status, + 'effectiveDate' => $this->effectiveDate, + 'certificate' => $this->certificate, + 'url' => $this->url, + 'certificationDescriptor' => $this->certificationDescriptor, + 'certificateNumber' => $this->certificateNumber, + 'certificationPolicyVersion' => $this->certificationPolicyVersion, + 'certificationRequirementsVersion' => $this->certificationRequirementsVersion, + ]; + + return self::filterNullValues($data); + } +} diff --git a/3rdparty/web-auth/webauthn-lib/src/MetadataService/Statement/VerificationMethodANDCombinations.php b/3rdparty/web-auth/webauthn-lib/src/MetadataService/Statement/VerificationMethodANDCombinations.php new file mode 100644 index 00000000..581d5f59 --- /dev/null +++ b/3rdparty/web-auth/webauthn-lib/src/MetadataService/Statement/VerificationMethodANDCombinations.php @@ -0,0 +1,79 @@ +verificationMethods[] = $verificationMethodDescriptor; + + return $this; + } + + /** + * @return VerificationMethodDescriptor[] + * @deprecated since 4.7.0. Please use the property directly. + * @infection-ignore-all + */ + public function getVerificationMethods(): array + { + return $this->verificationMethods; + } + + /** + * @param array $data + * @deprecated since 4.7.0. Please use the symfony/serializer for converting the object. + * @infection-ignore-all + */ + public static function createFromArray(array $data): self + { + return self::create( + array_map( + static fn (array $datum): VerificationMethodDescriptor => VerificationMethodDescriptor::createFromArray( + $datum + ), + $data + ) + ); + } + + /** + * @return array + */ + public function jsonSerialize(): array + { + trigger_deprecation( + 'web-auth/webauthn-bundle', + '4.9.0', + 'The "%s" method is deprecated and will be removed in 5.0. Please use the serializer instead.', + __METHOD__ + ); + return $this->verificationMethods; + } +} diff --git a/3rdparty/web-auth/webauthn-lib/src/MetadataService/Statement/VerificationMethodDescriptor.php b/3rdparty/web-auth/webauthn-lib/src/MetadataService/Statement/VerificationMethodDescriptor.php new file mode 100644 index 00000000..fe700f83 --- /dev/null +++ b/3rdparty/web-auth/webauthn-lib/src/MetadataService/Statement/VerificationMethodDescriptor.php @@ -0,0 +1,273 @@ += 0 || throw MetadataStatementLoadingException::create( + 'The parameter "userVerificationMethod" is invalid' + ); + } + + public static function create( + string $userVerificationMethod, + ?CodeAccuracyDescriptor $caDesc = null, + ?BiometricAccuracyDescriptor $baDesc = null, + ?PatternAccuracyDescriptor $paDesc = null + ): self { + return new self($userVerificationMethod, $caDesc, $baDesc, $paDesc); + } + + /** + * @deprecated since 4.7.0. Please use the property directly. + * @infection-ignore-all + */ + public function getUserVerificationMethod(): string + { + return $this->userVerificationMethod; + } + + public function userPresence(): bool + { + return $this->userVerificationMethod === self::USER_VERIFY_PRESENCE_INTERNAL; + } + + public function fingerprint(): bool + { + return $this->userVerificationMethod === self::USER_VERIFY_FINGERPRINT_INTERNAL; + } + + public function passcodeInternal(): bool + { + return $this->userVerificationMethod === self::USER_VERIFY_PASSCODE_INTERNAL; + } + + public function voicePrint(): bool + { + return $this->userVerificationMethod === self::USER_VERIFY_VOICEPRINT_INTERNAL; + } + + public function facePrint(): bool + { + return $this->userVerificationMethod === self::USER_VERIFY_FACEPRINT_INTERNAL; + } + + public function location(): bool + { + return $this->userVerificationMethod === self::USER_VERIFY_LOCATION_INTERNAL; + } + + public function eyePrint(): bool + { + return $this->userVerificationMethod === self::USER_VERIFY_EYEPRINT_INTERNAL; + } + + public function patternInternal(): bool + { + return $this->userVerificationMethod === self::USER_VERIFY_PATTERN_INTERNAL; + } + + public function handprint(): bool + { + return $this->userVerificationMethod === self::USER_VERIFY_HANDPRINT_INTERNAL; + } + + public function passcodeExternal(): bool + { + return $this->userVerificationMethod === self::USER_VERIFY_PASSCODE_EXTERNAL; + } + + public function patternExternal(): bool + { + return $this->userVerificationMethod === self::USER_VERIFY_PATTERN_EXTERNAL; + } + + public function none(): bool + { + return $this->userVerificationMethod === self::USER_VERIFY_NONE; + } + + public function all(): bool + { + return $this->userVerificationMethod === self::USER_VERIFY_ALL; + } + + /** + * @deprecated since 4.7.0. Please use the property directly. + * @infection-ignore-all + */ + public function getCaDesc(): ?CodeAccuracyDescriptor + { + return $this->caDesc; + } + + /** + * @deprecated since 4.7.0. Please use the property directly. + * @infection-ignore-all + */ + public function getBaDesc(): ?BiometricAccuracyDescriptor + { + return $this->baDesc; + } + + /** + * @deprecated since 4.7.0. Please use the property directly. + * @infection-ignore-all + */ + public function getPaDesc(): ?PatternAccuracyDescriptor + { + return $this->paDesc; + } + + /** + * @param array $data + * @deprecated since 4.7.0. Please use the symfony/serializer for converting the object. + * @infection-ignore-all + */ + public static function createFromArray(array $data): self + { + $data = self::filterNullValues($data); + if (isset($data['userVerification']) && ! isset($data['userVerificationMethod'])) { + $data['userVerificationMethod'] = $data['userVerification']; + unset($data['userVerification']); + } + array_key_exists('userVerificationMethod', $data) || throw MetadataStatementLoadingException::create( + 'The parameters "userVerificationMethod" is missing' + ); + + foreach (['caDesc', 'baDesc', 'paDesc'] as $key) { + if (isset($data[$key])) { + is_array($data[$key]) || throw MetadataStatementLoadingException::create( + sprintf('Invalid parameter "%s"', $key) + ); + } + } + + $caDesc = isset($data['caDesc']) ? CodeAccuracyDescriptor::createFromArray($data['caDesc']) : null; + $baDesc = isset($data['baDesc']) ? BiometricAccuracyDescriptor::createFromArray($data['baDesc']) : null; + $paDesc = isset($data['paDesc']) ? PatternAccuracyDescriptor::createFromArray($data['paDesc']) : null; + + return self::create($data['userVerificationMethod'], $caDesc, $baDesc, $paDesc); + } + + /** + * @return array + */ + public function jsonSerialize(): array + { + trigger_deprecation( + 'web-auth/webauthn-bundle', + '4.9.0', + 'The "%s" method is deprecated and will be removed in 5.0. Please use the serializer instead.', + __METHOD__ + ); + $data = [ + 'userVerificationMethod' => $this->userVerificationMethod, + 'caDesc' => $this->caDesc, + 'baDesc' => $this->baDesc, + 'paDesc' => $this->paDesc, + ]; + + return self::filterNullValues($data); + } +} diff --git a/3rdparty/web-auth/webauthn-lib/src/MetadataService/Statement/Version.php b/3rdparty/web-auth/webauthn-lib/src/MetadataService/Statement/Version.php new file mode 100644 index 00000000..1a0f7715 --- /dev/null +++ b/3rdparty/web-auth/webauthn-lib/src/MetadataService/Statement/Version.php @@ -0,0 +1,88 @@ += 0 || throw MetadataStatementLoadingException::create('Invalid argument "major"'); + $minor >= 0 || throw MetadataStatementLoadingException::create('Invalid argument "minor"'); + } + + public static function create(?int $major, ?int $minor): self + { + return new self($major, $minor); + } + + /** + * @deprecated since 4.7.0. Please use the property directly. + * @infection-ignore-all + */ + public function getMajor(): ?int + { + return $this->major; + } + + /** + * @deprecated since 4.7.0. Please use the property directly. + * @infection-ignore-all + */ + public function getMinor(): ?int + { + return $this->minor; + } + + /** + * @param array $data + * @deprecated since 4.7.0. Please use the symfony/serializer for converting the object. + * @infection-ignore-all + */ + public static function createFromArray(array $data): self + { + $data = self::filterNullValues($data); + foreach (['major', 'minor'] as $key) { + if (array_key_exists($key, $data)) { + is_int($data[$key]) || throw MetadataStatementLoadingException::create( + sprintf('Invalid value for key "%s"', $key) + ); + } + } + + return self::create($data['major'] ?? null, $data['minor'] ?? null); + } + + /** + * @return array + */ + public function jsonSerialize(): array + { + trigger_deprecation( + 'web-auth/webauthn-bundle', + '4.9.0', + 'The "%s" method is deprecated and will be removed in 5.0. Please use the serializer instead.', + __METHOD__ + ); + $data = [ + 'major' => $this->major, + 'minor' => $this->minor, + ]; + + return self::filterNullValues($data); + } +} diff --git a/3rdparty/web-auth/webauthn-lib/src/MetadataService/StatusReportRepository.php b/3rdparty/web-auth/webauthn-lib/src/MetadataService/StatusReportRepository.php new file mode 100644 index 00000000..be963667 --- /dev/null +++ b/3rdparty/web-auth/webauthn-lib/src/MetadataService/StatusReportRepository.php @@ -0,0 +1,15 @@ + $data + * + * @return array + */ + private static function filterNullValues(array $data): array + { + return array_filter($data, static fn ($var): bool => $var !== null); + } +} diff --git a/3rdparty/web-auth/webauthn-lib/src/PublicKeyCredential.php b/3rdparty/web-auth/webauthn-lib/src/PublicKeyCredential.php new file mode 100644 index 00000000..8e09ad65 --- /dev/null +++ b/3rdparty/web-auth/webauthn-lib/src/PublicKeyCredential.php @@ -0,0 +1,80 @@ +getPublicKeyCredentialDescriptor(), JSON_THROW_ON_ERROR); + } + + public static function create(null|string $id, string $type, string $rawId, AuthenticatorResponse $response): self + { + return new self($id, $type, $rawId, $response); + } + + /** + * @deprecated since 4.7.0. Please use the property directly. + * @infection-ignore-all + */ + public function getRawId(): string + { + return $this->rawId; + } + + /** + * @deprecated since 4.7.0. Please use the property directly. + * @infection-ignore-all + */ + public function getResponse(): AuthenticatorResponse + { + return $this->response; + } + + /** + * @param string[] $transport + */ + public function getPublicKeyCredentialDescriptor(null|array $transport = null): PublicKeyCredentialDescriptor + { + if ($transport !== null) { + trigger_deprecation( + 'web-auth/webauthn-lib', + '4.8.0', + 'The parameter "$transport" is deprecated and will be removed in 5.0.0.' + ); + @trigger_error( + sprintf( + 'The $transport argument of %s() is deprecated since 4.8.0 and will be removed in 5.0.0.', + __METHOD__ + ), + E_USER_DEPRECATED + ); + } + $transport ??= $this->response instanceof AuthenticatorAttestationResponse ? $this->response->transports : []; + + return PublicKeyCredentialDescriptor::create($this->type, $this->rawId, $transport); + } +} diff --git a/3rdparty/web-auth/webauthn-lib/src/PublicKeyCredentialCreationOptions.php b/3rdparty/web-auth/webauthn-lib/src/PublicKeyCredentialCreationOptions.php new file mode 100644 index 00000000..3e1377eb --- /dev/null +++ b/3rdparty/web-auth/webauthn-lib/src/PublicKeyCredentialCreationOptions.php @@ -0,0 +1,351 @@ +pubKeyCredParams[] = $pubKeyCredParam; + + return $this; + } + + /** + * @deprecated since 4.7.0. No replacement. Please use the {self::create} instead. + * @infection-ignore-all + */ + public function addPubKeyCredParams(PublicKeyCredentialParameters ...$pubKeyCredParams): self + { + foreach ($pubKeyCredParams as $pubKeyCredParam) { + $this->pubKeyCredParams[] = $pubKeyCredParam; + } + + return $this; + } + + /** + * @deprecated since 4.7.0. Please use the {self::create} instead. + * @infection-ignore-all + */ + public function excludeCredential(PublicKeyCredentialDescriptor $excludeCredential): self + { + $this->excludeCredentials[] = $excludeCredential; + + return $this; + } + + /** + * @deprecated since 4.7.0. No replacement. Please use the {self::create} instead. + * @infection-ignore-all + */ + public function excludeCredentials(PublicKeyCredentialDescriptor ...$excludeCredentials): self + { + foreach ($excludeCredentials as $excludeCredential) { + $this->excludeCredentials[] = $excludeCredential; + } + + return $this; + } + + /** + * @deprecated since 4.7.0. Please use the {self::create} instead. + * @infection-ignore-all + */ + public function setAuthenticatorSelection(?AuthenticatorSelectionCriteria $authenticatorSelection): self + { + $this->authenticatorSelection = $authenticatorSelection; + + return $this; + } + + /** + * @deprecated since 4.7.0. Please use the {self::create} instead. + * @infection-ignore-all + */ + public function setAttestation(string $attestation): self + { + in_array($attestation, self::ATTESTATION_CONVEYANCE_PREFERENCES, true) || throw InvalidDataException::create( + $attestation, + 'Invalid attestation conveyance mode' + ); + $this->attestation = $attestation; + + return $this; + } + + /** + * @deprecated since 4.7.0. Please use the property directly. + * @infection-ignore-all + */ + public function getRp(): PublicKeyCredentialRpEntity + { + return $this->rp; + } + + /** + * @deprecated since 4.7.0. Please use the property directly. + * @infection-ignore-all + */ + public function getUser(): PublicKeyCredentialUserEntity + { + return $this->user; + } + + /** + * @return PublicKeyCredentialParameters[] + * @deprecated since 4.7.0. Please use the property directly. + * @infection-ignore-all + */ + public function getPubKeyCredParams(): array + { + return $this->pubKeyCredParams; + } + + /** + * @return PublicKeyCredentialDescriptor[] + * @deprecated since 4.7.0. Please use the property directly. + * @infection-ignore-all + */ + public function getExcludeCredentials(): array + { + return $this->excludeCredentials; + } + + /** + * @deprecated since 4.7.0. Please use the property directly. + * @infection-ignore-all + */ + public function getAuthenticatorSelection(): ?AuthenticatorSelectionCriteria + { + return $this->authenticatorSelection; + } + + /** + * @deprecated since 4.7.0. Please use the property directly. + * @infection-ignore-all + */ + public function getAttestation(): ?string + { + return $this->attestation; + } + + /** + * @deprecated since 4.8.0. Please use {Webauthn\Denormalizer\WebauthnSerializerFactory} for converting the object. + * @infection-ignore-all + */ + public static function createFromString(string $data): static + { + $data = json_decode($data, true, flags: JSON_THROW_ON_ERROR); + + return self::createFromArray($data); + } + + /** + * @deprecated since 4.8.0. Please use {Webauthn\Denormalizer\WebauthnSerializerFactory} for converting the object. + * @infection-ignore-all + */ + public static function createFromArray(array $json): static + { + array_key_exists('rp', $json) || throw InvalidDataException::create($json, 'Invalid input. "rp" is missing.'); + array_key_exists('pubKeyCredParams', $json) || throw InvalidDataException::create( + $json, + 'Invalid input. "pubKeyCredParams" is missing.' + ); + is_array($json['pubKeyCredParams']) || throw InvalidDataException::create( + $json, + 'Invalid input. "pubKeyCredParams" is not an array.' + ); + array_key_exists('challenge', $json) || throw InvalidDataException::create( + $json, + 'Invalid input. "challenge" is missing.' + ); + array_key_exists('attestation', $json) || throw InvalidDataException::create( + $json, + 'Invalid input. "attestation" is missing.' + ); + array_key_exists('user', $json) || throw InvalidDataException::create( + $json, + 'Invalid input. "user" is missing.' + ); + + $pubKeyCredParams = []; + foreach ($json['pubKeyCredParams'] as $pubKeyCredParam) { + if (! is_array($pubKeyCredParam)) { + continue; + } + $pubKeyCredParams[] = PublicKeyCredentialParameters::createFromArray($pubKeyCredParam); + } + $excludeCredentials = []; + if (isset($json['excludeCredentials'])) { + foreach ($json['excludeCredentials'] as $excludeCredential) { + $excludeCredentials[] = PublicKeyCredentialDescriptor::createFromArray($excludeCredential); + } + } + + $challenge = Base64::decode($json['challenge']); + + $authenticatorSelection = isset($json['authenticatorSelection']) ? AuthenticatorSelectionCriteria::createFromArray( + $json['authenticatorSelection'] + ) : null + ; + $extensions = + isset($json['extensions']) ? AuthenticationExtensionsClientInputs::createFromArray( + $json['extensions'] + ) : AuthenticationExtensionsClientInputs::create() + ; + return self + ::create( + PublicKeyCredentialRpEntity::createFromArray($json['rp']), + PublicKeyCredentialUserEntity::createFromArray($json['user']), + $challenge, + $pubKeyCredParams, + $authenticatorSelection, + $json['attestation'] ?? null, + $excludeCredentials, + $json['timeout'] ?? null, + $extensions + ); + } + + /** + * @return mixed[] + */ + public function jsonSerialize(): array + { + trigger_deprecation( + 'web-auth/webauthn-bundle', + '4.9.0', + 'The "%s" method is deprecated and will be removed in 5.0. Please use the serializer instead.', + __METHOD__ + ); + $json = [ + 'rp' => $this->rp, + 'user' => $this->user, + 'challenge' => Base64UrlSafe::encodeUnpadded($this->challenge), + 'pubKeyCredParams' => $this->pubKeyCredParams, + ]; + + if ($this->timeout !== null) { + $json['timeout'] = $this->timeout; + } + + if (count($this->excludeCredentials) !== 0) { + $json['excludeCredentials'] = $this->excludeCredentials; + } + + if ($this->authenticatorSelection !== null) { + $json['authenticatorSelection'] = $this->authenticatorSelection; + } + + if ($this->attestation !== null) { + $json['attestation'] = $this->attestation; + } + + if ($this->extensions->count() !== 0) { + $json['extensions'] = $this->extensions; + } + + return $json; + } +} diff --git a/3rdparty/web-auth/webauthn-lib/src/PublicKeyCredentialDescriptor.php b/3rdparty/web-auth/webauthn-lib/src/PublicKeyCredentialDescriptor.php new file mode 100644 index 00000000..3a351be2 --- /dev/null +++ b/3rdparty/web-auth/webauthn-lib/src/PublicKeyCredentialDescriptor.php @@ -0,0 +1,130 @@ +type; + } + + /** + * @deprecated since 4.7.0. Please use the property directly. + * @infection-ignore-all + */ + public function getId(): string + { + return $this->id; + } + + /** + * @return string[] + * @deprecated since 4.7.0. Please use the property directly. + * @infection-ignore-all + */ + public function getTransports(): array + { + return $this->transports; + } + + /** + * @deprecated since 4.9.0 and will be removed in 5.0.0. Please use the serializer instead. + */ + public static function createFromString(string $data): self + { + $data = json_decode($data, true, flags: JSON_THROW_ON_ERROR); + + return self::createFromArray($data); + } + + /** + * @param mixed[] $json + * @deprecated since 4.9.0 and will be removed in 5.0.0. Please use the serializer instead. + */ + public static function createFromArray(array $json): self + { + array_key_exists('type', $json) || throw InvalidDataException::create( + $json, + 'Invalid input. "type" is missing.' + ); + array_key_exists('id', $json) || throw InvalidDataException::create($json, 'Invalid input. "id" is missing.'); + + $id = Base64UrlSafe::decodeNoPadding($json['id']); + + return self::create($json['type'], $id, $json['transports'] ?? []); + } + + /** + * @return mixed[] + */ + public function jsonSerialize(): array + { + trigger_deprecation( + 'web-auth/webauthn-bundle', + '4.9.0', + 'The "%s" method is deprecated and will be removed in 5.0. Please use the serializer instead.', + __METHOD__ + ); + $json = [ + 'type' => $this->type, + 'id' => Base64UrlSafe::encodeUnpadded($this->id), + ]; + if (count($this->transports) !== 0) { + $json['transports'] = $this->transports; + } + + return $json; + } +} diff --git a/3rdparty/web-auth/webauthn-lib/src/PublicKeyCredentialDescriptorCollection.php b/3rdparty/web-auth/webauthn-lib/src/PublicKeyCredentialDescriptorCollection.php new file mode 100644 index 00000000..d5f1481c --- /dev/null +++ b/3rdparty/web-auth/webauthn-lib/src/PublicKeyCredentialDescriptorCollection.php @@ -0,0 +1,137 @@ + + * @deprecated since 4.8.0 and will be removed in 5.0.0. + * @infection-ignore-all + */ +class PublicKeyCredentialDescriptorCollection implements JsonSerializable, Countable, IteratorAggregate +{ + /** + * @var array + * @readonly + */ + public array $publicKeyCredentialDescriptors; + + /** + * @private + * @param PublicKeyCredentialDescriptor[] $pkCredentialDescriptors + */ + public function __construct( + array $pkCredentialDescriptors = [] + ) { + $this->publicKeyCredentialDescriptors = []; + foreach ($pkCredentialDescriptors as $pkCredentialDescriptor) { + $pkCredentialDescriptor instanceof PublicKeyCredentialDescriptor || throw new InvalidArgumentException( + 'Expected only instances of ' . PublicKeyCredentialDescriptor::class + ); + $this->publicKeyCredentialDescriptors[$pkCredentialDescriptor->id] = $pkCredentialDescriptor; + } + } + + /** + * @param PublicKeyCredentialDescriptor[] $publicKeyCredentialDescriptors + */ + public static function create(array $publicKeyCredentialDescriptors): self + { + return new self($publicKeyCredentialDescriptors); + } + + /** + * @infection-ignore-all + */ + public function add(PublicKeyCredentialDescriptor ...$publicKeyCredentialDescriptors): void + { + foreach ($publicKeyCredentialDescriptors as $publicKeyCredentialDescriptor) { + $this->publicKeyCredentialDescriptors[$publicKeyCredentialDescriptor->id] = $publicKeyCredentialDescriptor; + } + } + + /** + * @infection-ignore-all + */ + public function has(string $id): bool + { + return array_key_exists($id, $this->publicKeyCredentialDescriptors); + } + + /** + * @infection-ignore-all + */ + public function remove(string $id): void + { + if (! array_key_exists($id, $this->publicKeyCredentialDescriptors)) { + return; + } + + unset($this->publicKeyCredentialDescriptors[$id]); + } + + /** + * @return Iterator + */ + public function getIterator(): Iterator + { + return new ArrayIterator($this->publicKeyCredentialDescriptors); + } + + public function count(int $mode = COUNT_NORMAL): int + { + return count($this->publicKeyCredentialDescriptors, $mode); + } + + /** + * @return array[] + */ + public function jsonSerialize(): array + { + trigger_deprecation( + 'web-auth/webauthn-bundle', + '4.9.0', + 'The "%s" method is deprecated and will be removed in 5.0. Please use the serializer instead.', + __METHOD__ + ); + return $this->publicKeyCredentialDescriptors; + } + + /** + * @infection-ignore-all + */ + public static function createFromString(string $data): self + { + $data = json_decode($data, true, flags: JSON_THROW_ON_ERROR); + + return self::createFromArray($data); + } + + /** + * @param mixed[] $json + * @infection-ignore-all + */ + public static function createFromArray(array $json): self + { + return self::create( + array_map( + static fn (array $item): PublicKeyCredentialDescriptor => PublicKeyCredentialDescriptor::createFromArray( + $item + ), + $json + ) + ); + } +} diff --git a/3rdparty/web-auth/webauthn-lib/src/PublicKeyCredentialEntity.php b/3rdparty/web-auth/webauthn-lib/src/PublicKeyCredentialEntity.php new file mode 100644 index 00000000..844a078b --- /dev/null +++ b/3rdparty/web-auth/webauthn-lib/src/PublicKeyCredentialEntity.php @@ -0,0 +1,49 @@ +name; + } + + /** + * @deprecated since 4.7.0. Please use the property directly. + * @infection-ignore-all + */ + public function getIcon(): ?string + { + return $this->icon; + } + + /** + * @return mixed[] + */ + public function jsonSerialize(): array + { + $json = [ + 'name' => $this->name, + ]; + if ($this->icon !== null) { + $json['icon'] = $this->icon; + } + + return $json; + } +} diff --git a/3rdparty/web-auth/webauthn-lib/src/PublicKeyCredentialLoader.php b/3rdparty/web-auth/webauthn-lib/src/PublicKeyCredentialLoader.php new file mode 100644 index 00000000..88b2a9c1 --- /dev/null +++ b/3rdparty/web-auth/webauthn-lib/src/PublicKeyCredentialLoader.php @@ -0,0 +1,191 @@ +attestationObjectLoader === null && $this->serializer === null) { + throw new InvalidArgumentException('You must provide an attestation object loader or a serializer'); + } + $this->logger = new NullLogger(); + } + + public static function create( + null|AttestationObjectLoader $attestationObjectLoader, + null|SerializerInterface $serializer = null + ): self { + return new self($attestationObjectLoader, $serializer); + } + + public function setLogger(LoggerInterface $logger): void + { + $this->logger = $logger; + } + + /** + * @param mixed[] $json + * @infection-ignore-all + */ + public function loadArray(array $json): PublicKeyCredential + { + $this->logger->info('Trying to load data from an array', [ + 'data' => $json, + ]); + try { + foreach (['id', 'rawId', 'type'] as $key) { + array_key_exists($key, $json) || throw InvalidDataException::create($json, sprintf( + 'The parameter "%s" is missing', + $key + )); + is_string($json[$key]) || throw InvalidDataException::create($json, sprintf( + 'The parameter "%s" shall be a string', + $key + )); + } + array_key_exists('response', $json) || throw InvalidDataException::create( + $json, + 'The parameter "response" is missing' + ); + is_array($json['response']) || throw InvalidDataException::create( + $json, + 'The parameter "response" shall be an array' + ); + $json['type'] === 'public-key' || throw InvalidDataException::create($json, sprintf( + 'Unsupported type "%s"', + $json['type'] + )); + + $id = Base64UrlSafe::decodeNoPadding($json['id']); + $rawId = Base64::decode($json['rawId']); + hash_equals($id, $rawId) || throw InvalidDataException::create($json, 'Invalid ID'); + + $publicKeyCredential = PublicKeyCredential::create( + null, + $json['type'], + $rawId, + $this->createResponse($json['response']) + ); + $this->logger->info('The data has been loaded'); + $this->logger->debug('Public Key Credential', [ + 'publicKeyCredential' => $publicKeyCredential, + ]); + + return $publicKeyCredential; + } catch (Throwable $throwable) { + $this->logger->error('An error occurred', [ + 'exception' => $throwable, + ]); + throw $throwable; + } + } + + public function load(string $data): PublicKeyCredential + { + $this->logger->info('Trying to load data from a string', [ + 'data' => $data, + ]); + try { + if ($this->serializer !== null) { + return $this->serializer->deserialize($data, PublicKeyCredential::class, 'json'); + } + $json = json_decode($data, true, flags: JSON_THROW_ON_ERROR); + + return $this->loadArray($json); + } catch (Throwable $throwable) { + $this->logger->error('An error occurred', [ + 'exception' => $throwable, + ]); + throw InvalidDataException::create($data, 'Unable to load the data', $throwable); + } + } + + /** + * @param mixed[] $response + */ + private function createResponse(array $response): AuthenticatorResponse + { + array_key_exists('clientDataJSON', $response) || throw InvalidDataException::create( + $response, + 'Invalid data. The parameter "clientDataJSON" is missing' + ); + is_string($response['clientDataJSON']) || throw InvalidDataException::create( + $response, + 'Invalid data. The parameter "clientDataJSON" is invalid' + ); + $userHandle = $response['userHandle'] ?? null; + $userHandle === null || is_string($userHandle) || throw InvalidDataException::create( + $response, + 'Invalid data. The parameter "userHandle" is invalid' + ); + /** @var string[] $transports */ + $transports = $response['transports'] ?? []; + is_array($transports) || throw InvalidDataException::create( + $response, + 'Invalid data. The parameter "transports" is invalid' + ); + if ($this->serializer !== null) { + return $this->serializer->deserialize($response, AuthenticatorResponse::class, 'json'); + } + switch (true) { + case array_key_exists('attestationObject', $response): + $attestationObject = $this->attestationObjectLoader->load($response['attestationObject']); + + return AuthenticatorAttestationResponse::create(CollectedClientData::createFormJson( + $response['clientDataJSON'] + ), $attestationObject, $transports); + case array_key_exists('signature', $response): + $authDataLoader = AuthenticatorDataLoader::create(); + $authData = Base64UrlSafe::decodeNoPadding($response['authenticatorData'] ?? ''); + $authenticatorData = $authDataLoader->load($authData); + + try { + $signature = Base64::decode($response['signature']); + } catch (Throwable $e) { + throw InvalidDataException::create( + $response['signature'], + 'The signature shall be Base64 Url Safe encoded', + $e + ); + } + $userHandle = $response['userHandle'] ?? null; + if ($userHandle !== '' && $userHandle !== null) { + $userHandle = Base64::decode($userHandle); + } + + return AuthenticatorAssertionResponse::create( + CollectedClientData::createFormJson($response['clientDataJSON']), + $authenticatorData, + $signature, + $userHandle + ); + default: + throw InvalidDataException::create($response, 'Unable to create the response object'); + } + } +} diff --git a/3rdparty/web-auth/webauthn-lib/src/PublicKeyCredentialOptions.php b/3rdparty/web-auth/webauthn-lib/src/PublicKeyCredentialOptions.php new file mode 100644 index 00000000..8e5ffbe4 --- /dev/null +++ b/3rdparty/web-auth/webauthn-lib/src/PublicKeyCredentialOptions.php @@ -0,0 +1,123 @@ + $extensions + * @protected + */ + public function __construct( + public readonly string $challenge, + public null|int $timeout = null, + null|array|AuthenticationExtensions $extensions = null, + ) { + ($this->timeout === null || $this->timeout > 0) || throw new InvalidArgumentException('Invalid timeout'); + if ($extensions === null) { + $this->extensions = AuthenticationExtensionsClientInputs::create(); + } elseif ($extensions instanceof AuthenticationExtensions) { + $this->extensions = $extensions; + } else { + $this->extensions = AuthenticationExtensions::create($extensions); + } + } + + /** + * @deprecated since 4.7.0. Please use the {self::create} instead. + * @infection-ignore-all + */ + public function setTimeout(?int $timeout): static + { + $this->timeout = $timeout; + + return $this; + } + + /** + * @deprecated since 4.7.0. Please use the {self::create} instead. + * @infection-ignore-all + */ + public function addExtension(AuthenticationExtension $extension): static + { + $this->extensions[$extension->name] = $extension; + + return $this; + } + + /** + * @param AuthenticationExtension[] $extensions + * @deprecated since 4.7.0. No replacement. Please use the {self::create} instead. + * @infection-ignore-all + */ + public function addExtensions(array $extensions): static + { + foreach ($extensions as $extension) { + $this->extensions[$extension->name] = $extension; + } + + return $this; + } + + /** + * @deprecated since 4.7.0. Please use the {self::create} instead. + * @infection-ignore-all + */ + public function setExtensions(AuthenticationExtensions $extensions): static + { + $this->extensions = $extensions; + + return $this; + } + + /** + * @deprecated since 4.7.0. Please use the property directly. + * @infection-ignore-all + */ + public function getChallenge(): string + { + return $this->challenge; + } + + /** + * @deprecated since 4.7.0. Please use the property directly. + * @infection-ignore-all + */ + public function getTimeout(): ?int + { + return $this->timeout; + } + + /** + * @deprecated since 4.7.0. Please use the property directly. + * @infection-ignore-all + */ + public function getExtensions(): AuthenticationExtensions + { + return $this->extensions; + } + + /** + * @deprecated since 4.8.0. Please use {Webauthn\Denormalizer\WebauthnSerializerFactory} for converting the object. + * @infection-ignore-all + */ + abstract public static function createFromString(string $data): static; + + /** + * @param mixed[] $json + * @deprecated since 4.8.0. Please use {Webauthn\Denormalizer\WebauthnSerializerFactory} for converting the object. + * @infection-ignore-all + */ + abstract public static function createFromArray(array $json): static; +} diff --git a/3rdparty/web-auth/webauthn-lib/src/PublicKeyCredentialParameters.php b/3rdparty/web-auth/webauthn-lib/src/PublicKeyCredentialParameters.php new file mode 100644 index 00000000..7ea3cea8 --- /dev/null +++ b/3rdparty/web-auth/webauthn-lib/src/PublicKeyCredentialParameters.php @@ -0,0 +1,97 @@ +type; + } + + /** + * @deprecated since 4.7.0. Please use the property directly. + * @infection-ignore-all + */ + public function getAlg(): int + { + return $this->alg; + } + + /** + * @deprecated since 4.8.0. Please use {Webauthn\Denormalizer\WebauthnSerializerFactory} for converting the object. + * @infection-ignore-all + */ + public static function createFromString(string $data): self + { + $data = json_decode($data, true, flags: JSON_THROW_ON_ERROR); + + return self::createFromArray($data); + } + + /** + * @param mixed[] $json + * @deprecated since 4.8.0. Please use {Webauthn\Denormalizer\WebauthnSerializerFactory} for converting the object. + * @infection-ignore-all + */ + public static function createFromArray(array $json): self + { + array_key_exists('type', $json) || throw InvalidDataException::create( + $json, + 'Invalid input. "type" is missing.' + ); + array_key_exists('alg', $json) || throw InvalidDataException::create( + $json, + 'Invalid input. "alg" is missing.' + ); + + return self::create($json['type'], $json['alg']); + } + + /** + * @return mixed[] + */ + public function jsonSerialize(): array + { + trigger_deprecation( + 'web-auth/webauthn-bundle', + '4.9.0', + 'The "%s" method is deprecated and will be removed in 5.0. Please use the serializer instead.', + __METHOD__ + ); + return [ + 'type' => $this->type, + 'alg' => $this->alg, + ]; + } +} diff --git a/3rdparty/web-auth/webauthn-lib/src/PublicKeyCredentialRequestOptions.php b/3rdparty/web-auth/webauthn-lib/src/PublicKeyCredentialRequestOptions.php new file mode 100644 index 00000000..46b6777d --- /dev/null +++ b/3rdparty/web-auth/webauthn-lib/src/PublicKeyCredentialRequestOptions.php @@ -0,0 +1,239 @@ + $extensions + */ + public function __construct( + string $challenge, + public null|string $rpId = null, + public array $allowCredentials = [], + public null|string $userVerification = null, + null|int $timeout = null, + null|array|AuthenticationExtensions $extensions = null, + ) { + in_array($userVerification, self::USER_VERIFICATION_REQUIREMENTS, true) || throw InvalidDataException::create( + $userVerification, + 'Invalid user verification requirement' + ); + parent::__construct( + $challenge, + $timeout, + $extensions + ); + } + + /** + * @param PublicKeyCredentialDescriptor[] $allowCredentials + * @param positive-int $timeout + * @param null|AuthenticationExtensions|array $extensions + */ + public static function create( + string $challenge, + null|string $rpId = null, + array $allowCredentials = [], + null|string $userVerification = null, + null|int $timeout = null, + null|array|AuthenticationExtensions $extensions = null, + ): self { + return new self($challenge, $rpId, $allowCredentials, $userVerification, $timeout, $extensions); + } + + /** + * @deprecated since 4.7.0. Please use the property directly. + * @infection-ignore-all + */ + public function setRpId(?string $rpId): self + { + $this->rpId = $rpId; + + return $this; + } + + /** + * @deprecated since 4.7.0. Please use the property directly. + * @infection-ignore-all + */ + public function allowCredential(PublicKeyCredentialDescriptor $allowCredential): self + { + $this->allowCredentials[] = $allowCredential; + + return $this; + } + + /** + * @deprecated since 4.7.0. No replacement. Please use the property directly. + * @infection-ignore-all + */ + public function allowCredentials(PublicKeyCredentialDescriptor ...$allowCredentials): self + { + foreach ($allowCredentials as $allowCredential) { + $this->allowCredentials[] = $allowCredential; + } + + return $this; + } + + /** + * @deprecated since 4.7.0. Please use the property directly. + * @infection-ignore-all + */ + public function setUserVerification(?string $userVerification): self + { + if ($userVerification === null) { + $this->rpId = null; + + return $this; + } + in_array($userVerification, [ + self::USER_VERIFICATION_REQUIREMENT_REQUIRED, + self::USER_VERIFICATION_REQUIREMENT_PREFERRED, + self::USER_VERIFICATION_REQUIREMENT_DISCOURAGED, + ], true) || throw InvalidDataException::create($userVerification, 'Invalid user verification requirement'); + $this->userVerification = $userVerification; + + return $this; + } + + /** + * @deprecated since 4.7.0. Please use the property directly. + * @infection-ignore-all + */ + public function getRpId(): ?string + { + return $this->rpId; + } + + /** + * @return PublicKeyCredentialDescriptor[] + * @deprecated since 4.7.0. Please use the property directly. + * @infection-ignore-all + */ + public function getAllowCredentials(): array + { + return $this->allowCredentials; + } + + /** + * @deprecated since 4.7.0. Please use the property directly. + * @infection-ignore-all + */ + public function getUserVerification(): ?string + { + return $this->userVerification; + } + + /** + * @deprecated since 4.8.0. Please use {Webauthn\Denormalizer\WebauthnSerializerFactory} for converting the object. + * @infection-ignore-all + */ + public static function createFromString(string $data): static + { + $data = json_decode($data, true, flags: JSON_THROW_ON_ERROR); + + return self::createFromArray($data); + } + + /** + * @param mixed[] $json + * @deprecated since 4.8.0. Please use {Webauthn\Denormalizer\WebauthnSerializerFactory} for converting the object. + * @infection-ignore-all + */ + public static function createFromArray(array $json): static + { + array_key_exists('challenge', $json) || throw InvalidDataException::create( + $json, + 'Invalid input. "challenge" is missing.' + ); + + $allowCredentials = []; + $allowCredentialList = $json['allowCredentials'] ?? []; + foreach ($allowCredentialList as $allowCredential) { + $allowCredentials[] = PublicKeyCredentialDescriptor::createFromArray($allowCredential); + } + + $challenge = Base64::decode($json['challenge']); + $extensions = isset($json['extensions']) ? AuthenticationExtensionsClientInputs::createFromArray( + $json['extensions'] + ) : AuthenticationExtensionsClientInputs::create(); + + return self::create( + $challenge, + $json['rpId'] ?? null, + $allowCredentials, + $json['userVerification'] ?? null, + $json['timeout'] ?? null, + $extensions + ); + } + + /** + * @return mixed[] + */ + public function jsonSerialize(): array + { + trigger_deprecation( + 'web-auth/webauthn-bundle', + '4.9.0', + 'The "%s" method is deprecated and will be removed in 5.0. Please use the serializer instead.', + __METHOD__ + ); + $json = [ + 'challenge' => Base64UrlSafe::encodeUnpadded($this->challenge), + ]; + + if ($this->rpId !== null) { + $json['rpId'] = $this->rpId; + } + + if ($this->userVerification !== null) { + $json['userVerification'] = $this->userVerification; + } + + if (count($this->allowCredentials) !== 0) { + $json['allowCredentials'] = $this->allowCredentials; + } + + if ($this->extensions->count() !== 0) { + $json['extensions'] = $this->extensions; + } + + if ($this->timeout !== null) { + $json['timeout'] = $this->timeout; + } + + return $json; + } +} diff --git a/3rdparty/web-auth/webauthn-lib/src/PublicKeyCredentialRpEntity.php b/3rdparty/web-auth/webauthn-lib/src/PublicKeyCredentialRpEntity.php new file mode 100644 index 00000000..670daefa --- /dev/null +++ b/3rdparty/web-auth/webauthn-lib/src/PublicKeyCredentialRpEntity.php @@ -0,0 +1,67 @@ +id; + } + + /** + * @param mixed[] $json + * @deprecated since 4.8.0. Please use {Webauthn\Denormalizer\WebauthnSerializerFactory} for converting the object. + * @infection-ignore-all + */ + public static function createFromArray(array $json): self + { + array_key_exists('name', $json) || throw InvalidDataException::create( + $json, + 'Invalid input. "name" is missing.' + ); + + return self::create($json['name'], $json['id'] ?? null, $json['icon'] ?? null); + } + + /** + * @return mixed[] + */ + public function jsonSerialize(): array + { + trigger_deprecation( + 'web-auth/webauthn-bundle', + '4.9.0', + 'The "%s" method is deprecated and will be removed in 5.0. Please use the serializer instead.', + __METHOD__ + ); + $json = parent::jsonSerialize(); + if ($this->id !== null) { + $json['id'] = $this->id; + } + + return $json; + } +} diff --git a/3rdparty/web-auth/webauthn-lib/src/PublicKeyCredentialSource.php b/3rdparty/web-auth/webauthn-lib/src/PublicKeyCredentialSource.php new file mode 100644 index 00000000..2aef84ef --- /dev/null +++ b/3rdparty/web-auth/webauthn-lib/src/PublicKeyCredentialSource.php @@ -0,0 +1,275 @@ +|null $otherUI + */ + public function __construct( + public string $publicKeyCredentialId, + public string $type, + public array $transports, + public string $attestationType, + public TrustPath $trustPath, + public Uuid $aaguid, + public string $credentialPublicKey, + public string $userHandle, + public int $counter, + public ?array $otherUI = null, + public ?bool $backupEligible = null, + public ?bool $backupStatus = null, + public ?bool $uvInitialized = null, + ) { + } + + /** + * @param string[] $transports + * @param array|null $otherUI + */ + public static function create( + string $publicKeyCredentialId, + string $type, + array $transports, + string $attestationType, + TrustPath $trustPath, + Uuid $aaguid, + string $credentialPublicKey, + string $userHandle, + int $counter, + ?array $otherUI = null, + ?bool $backupEligible = null, + ?bool $backupStatus = null, + ?bool $uvInitialized = null, + ): self { + return new self( + $publicKeyCredentialId, + $type, + $transports, + $attestationType, + $trustPath, + $aaguid, + $credentialPublicKey, + $userHandle, + $counter, + $otherUI, + $backupEligible, + $backupStatus, + $uvInitialized + ); + } + + /** + * @deprecated since 4.7.0. Please use the property directly. + * @infection-ignore-all + */ + public function getPublicKeyCredentialId(): string + { + return $this->publicKeyCredentialId; + } + + public function getPublicKeyCredentialDescriptor(): PublicKeyCredentialDescriptor + { + return PublicKeyCredentialDescriptor::create($this->type, $this->publicKeyCredentialId, $this->transports); + } + + /** + * @deprecated since 4.7.0. Please use the property directly. + * @infection-ignore-all + */ + public function getAttestationType(): string + { + return $this->attestationType; + } + + /** + * @deprecated since 4.7.0. Please use the property directly. + * @infection-ignore-all + */ + public function getTrustPath(): TrustPath + { + return $this->trustPath; + } + + public function getAttestedCredentialData(): AttestedCredentialData + { + return AttestedCredentialData::create($this->aaguid, $this->publicKeyCredentialId, $this->credentialPublicKey); + } + + /** + * @deprecated since 4.7.0. Please use the property directly. + * @infection-ignore-all + */ + public function getType(): string + { + return $this->type; + } + + /** + * @return string[] + * @deprecated since 4.7.0. Please use the property directly. + * @infection-ignore-all + */ + public function getTransports(): array + { + return $this->transports; + } + + /** + * @deprecated since 4.7.0. Please use the property directly. + * @infection-ignore-all + */ + public function getAaguid(): Uuid + { + return $this->aaguid; + } + + /** + * @deprecated since 4.7.0. Please use the property directly. + * @infection-ignore-all + */ + public function getCredentialPublicKey(): string + { + return $this->credentialPublicKey; + } + + /** + * @deprecated since 4.7.0. Please use the property directly. + * @infection-ignore-all + */ + public function getUserHandle(): string + { + return $this->userHandle; + } + + /** + * @deprecated since 4.7.0. Please use the property directly. + * @infection-ignore-all + */ + public function getCounter(): int + { + return $this->counter; + } + + /** + * @deprecated since 4.7.0. Please use the property directly. + * @infection-ignore-all + */ + public function setCounter(int $counter): void + { + $this->counter = $counter; + } + + /** + * @return array|null + * @deprecated since 4.7.0. Please use the property directly. + * @infection-ignore-all + */ + public function getOtherUI(): ?array + { + return $this->otherUI; + } + + /** + * @param array|null $otherUI + * @deprecated since 4.7.0. Please use the property directly. + * @infection-ignore-all + */ + public function setOtherUI(?array $otherUI): self + { + $this->otherUI = $otherUI; + + return $this; + } + + /** + * @param mixed[] $data + * @deprecated since 4.8.0. Please use {Webauthn\Denormalizer\WebauthnSerializerFactory} for converting the object. + * @infection-ignore-all + */ + public static function createFromArray(array $data): self + { + $keys = array_keys(get_class_vars(self::class)); + foreach ($keys as $key) { + if (in_array($key, ['otherUI', 'backupEligible', 'backupStatus', 'uvInitialized'], true)) { + continue; + } + array_key_exists($key, $data) || throw InvalidDataException::create($data, sprintf( + 'The parameter "%s" is missing', + $key + )); + } + mb_strlen((string) $data['aaguid'], '8bit') === 36 || throw InvalidDataException::create( + $data, + 'Invalid AAGUID' + ); + $uuid = Uuid::fromString($data['aaguid']); + + try { + return self::create( + Base64UrlSafe::decodeNoPadding($data['publicKeyCredentialId']), + $data['type'], + $data['transports'], + $data['attestationType'], + TrustPathLoader::loadTrustPath($data['trustPath']), + $uuid, + Base64UrlSafe::decodeNoPadding($data['credentialPublicKey']), + Base64UrlSafe::decodeNoPadding($data['userHandle']), + $data['counter'], + $data['otherUI'] ?? null, + $data['backupEligible'] ?? null, + $data['backupStatus'] ?? null, + ); + } catch (Throwable $throwable) { + throw InvalidDataException::create($data, 'Unable to load the data', $throwable); + } + } + + /** + * @return mixed[] + */ + public function jsonSerialize(): array + { + trigger_deprecation( + 'web-auth/webauthn-bundle', + '4.9.0', + 'The "%s" method is deprecated and will be removed in 5.0. Please use the serializer instead.', + __METHOD__ + ); + $result = [ + 'publicKeyCredentialId' => Base64UrlSafe::encodeUnpadded($this->publicKeyCredentialId), + 'type' => $this->type, + 'transports' => $this->transports, + 'attestationType' => $this->attestationType, + 'trustPath' => $this->trustPath, + 'aaguid' => $this->aaguid->__toString(), + 'credentialPublicKey' => Base64UrlSafe::encodeUnpadded($this->credentialPublicKey), + 'userHandle' => Base64UrlSafe::encodeUnpadded($this->userHandle), + 'counter' => $this->counter, + 'otherUI' => $this->otherUI, + 'backupEligible' => $this->backupEligible, + 'backupStatus' => $this->backupStatus, + 'uvInitialized' => $this->uvInitialized, + ]; + + return array_filter($result, static fn ($value): bool => $value !== null); + } +} diff --git a/3rdparty/web-auth/webauthn-lib/src/PublicKeyCredentialSourceRepository.php b/3rdparty/web-auth/webauthn-lib/src/PublicKeyCredentialSourceRepository.php new file mode 100644 index 00000000..5174a418 --- /dev/null +++ b/3rdparty/web-auth/webauthn-lib/src/PublicKeyCredentialSourceRepository.php @@ -0,0 +1,21 @@ +id = $id; + } + + public static function create(string $name, string $id, string $displayName, ?string $icon = null): self + { + return new self($name, $id, $displayName, $icon); + } + + /** + * @deprecated since 4.7.0. Please use the property directly. + * @infection-ignore-all + */ + public function getId(): string + { + return $this->id; + } + + /** + * @deprecated since 4.7.0. Please use the property directly. + * @infection-ignore-all + */ + public function getDisplayName(): string + { + return $this->displayName; + } + + /** + * @deprecated since 4.8.0. Please use {Webauthn\Denormalizer\WebauthnSerializerFactory} for converting the object. + * @infection-ignore-all + */ + public static function createFromString(string $data): self + { + $data = json_decode($data, true, flags: JSON_THROW_ON_ERROR); + is_array($data) || throw InvalidDataException::create($data, 'Invalid data'); + + return self::createFromArray($data); + } + + /** + * @param mixed[] $json + * @deprecated since 4.8.0. Please use {Webauthn\Denormalizer\WebauthnSerializerFactory} for converting the object. + * @infection-ignore-all + */ + public static function createFromArray(array $json): self + { + array_key_exists('name', $json) || throw InvalidDataException::create( + $json, + 'Invalid input. "name" is missing.' + ); + array_key_exists('id', $json) || throw InvalidDataException::create($json, 'Invalid input. "id" is missing.'); + array_key_exists('displayName', $json) || throw InvalidDataException::create( + $json, + 'Invalid input. "displayName" is missing.' + ); + $id = Base64::decode($json['id'], true); + + return self::create($json['name'], $id, $json['displayName'], $json['icon'] ?? null); + } + + /** + * @return mixed[] + */ + public function jsonSerialize(): array + { + trigger_deprecation( + 'web-auth/webauthn-bundle', + '4.9.0', + 'The "%s" method is deprecated and will be removed in 5.0. Please use the serializer instead.', + __METHOD__ + ); + $json = parent::jsonSerialize(); + $json['id'] = Base64UrlSafe::encodeUnpadded($this->id); + $json['displayName'] = $this->displayName; + + return $json; + } +} diff --git a/3rdparty/web-auth/webauthn-lib/src/SimpleFakeCredentialGenerator.php b/3rdparty/web-auth/webauthn-lib/src/SimpleFakeCredentialGenerator.php new file mode 100644 index 00000000..a0c4fb90 --- /dev/null +++ b/3rdparty/web-auth/webauthn-lib/src/SimpleFakeCredentialGenerator.php @@ -0,0 +1,67 @@ +cache === null) { + return $this->generateCredentials($username); + } + + $cacheKey = 'fake_credentials_' . hash('xxh128', $username); + $cacheItem = $this->cache->getItem($cacheKey); + if ($cacheItem->isHit()) { + return $cacheItem->get(); + } + + $credentials = $this->generateCredentials($username); + $cacheItem->set($credentials); + $this->cache->save($cacheItem); + + return $credentials; + } + + /** + * @return PublicKeyCredentialDescriptor[] + */ + private function generateCredentials(string $username): array + { + $transports = [ + PublicKeyCredentialDescriptor::AUTHENTICATOR_TRANSPORT_USB, + PublicKeyCredentialDescriptor::AUTHENTICATOR_TRANSPORT_NFC, + PublicKeyCredentialDescriptor::AUTHENTICATOR_TRANSPORT_BLE, + ]; + $credentials = []; + for ($i = 0; $i < random_int(1, 3); $i++) { + $randomTransportKeys = array_rand($transports, random_int(1, count($transports))); + if (is_int($randomTransportKeys)) { + $randomTransportKeys = [$randomTransportKeys]; + } + $randomTransports = array_values(array_intersect_key($transports, array_flip($randomTransportKeys))); + $credentials[] = PublicKeyCredentialDescriptor::create( + PublicKeyCredentialDescriptor::CREDENTIAL_TYPE_PUBLIC_KEY, + hash('sha256', random_bytes(16) . $username), + $randomTransports + ); + } + + return $credentials; + } +} diff --git a/3rdparty/web-auth/webauthn-lib/src/StringStream.php b/3rdparty/web-auth/webauthn-lib/src/StringStream.php new file mode 100644 index 00000000..06784e7c --- /dev/null +++ b/3rdparty/web-auth/webauthn-lib/src/StringStream.php @@ -0,0 +1,61 @@ +length = mb_strlen($data, '8bit'); + $resource = fopen('php://memory', 'rb+'); + fwrite($resource, $data); + rewind($resource); + $this->data = $resource; + } + + public function read(int $length): string + { + if ($length <= 0) { + return ''; + } + $read = fread($this->data, $length); + $bytesRead = mb_strlen($read, '8bit'); + mb_strlen($read, '8bit') === $length || throw InvalidDataException::create(null, sprintf( + 'Out of range. Expected: %d, read: %d.', + $length, + $bytesRead + )); + $this->totalRead += $bytesRead; + + return $read; + } + + public function close(): void + { + fclose($this->data); + } + + public function isEOF(): bool + { + return $this->totalRead === $this->length; + } +} diff --git a/3rdparty/web-auth/webauthn-lib/src/TokenBinding/IgnoreTokenBindingHandler.php b/3rdparty/web-auth/webauthn-lib/src/TokenBinding/IgnoreTokenBindingHandler.php new file mode 100644 index 00000000..037fd8c7 --- /dev/null +++ b/3rdparty/web-auth/webauthn-lib/src/TokenBinding/IgnoreTokenBindingHandler.php @@ -0,0 +1,24 @@ +getStatus() !== TokenBinding::TOKEN_BINDING_STATUS_PRESENT) { + return; + } + + $request->hasHeader('Sec-Token-Binding') || throw InvalidDataException::create( + $tokenBinding, + 'The header parameter "Sec-Token-Binding" is missing.' + ); + $tokenBindingIds = $request->getHeader('Sec-Token-Binding'); + count($tokenBindingIds) === 1 || throw InvalidDataException::create( + $tokenBinding, + 'The header parameter "Sec-Token-Binding" is invalid.' + ); + $tokenBindingId = reset($tokenBindingIds); + $tokenBindingId === $tokenBinding->getId() || throw InvalidDataException::create( + $tokenBinding, + 'The header parameter "Sec-Token-Binding" is invalid.' + ); + } +} diff --git a/3rdparty/web-auth/webauthn-lib/src/TokenBinding/TokenBinding.php b/3rdparty/web-auth/webauthn-lib/src/TokenBinding/TokenBinding.php new file mode 100644 index 00000000..cffc6ded --- /dev/null +++ b/3rdparty/web-auth/webauthn-lib/src/TokenBinding/TokenBinding.php @@ -0,0 +1,78 @@ +status = $status; + $this->id = $id; + } + + /** + * @param mixed[] $json + */ + public static function createFormArray(array $json): self + { + array_key_exists('status', $json) || throw InvalidDataException::create( + $json, + 'The member "status" is required' + ); + $status = $json['status']; + in_array($status, self::getSupportedStatus(), true) || throw InvalidDataException::create($json, sprintf( + 'The member "status" is invalid. Supported values are: %s', + implode(', ', self::getSupportedStatus()) + )); + $id = array_key_exists('id', $json) ? Base64UrlSafe::decodeNoPadding($json['id']) : null; + + return new self($status, $id); + } + + public function getStatus(): string + { + return $this->status; + } + + public function getId(): ?string + { + return $this->id; + } + + /** + * @return string[] + */ + private static function getSupportedStatus(): array + { + return [ + self::TOKEN_BINDING_STATUS_PRESENT, + self::TOKEN_BINDING_STATUS_SUPPORTED, + self::TOKEN_BINDING_STATUS_NOT_SUPPORTED, + ]; + } +} diff --git a/3rdparty/web-auth/webauthn-lib/src/TokenBinding/TokenBindingHandler.php b/3rdparty/web-auth/webauthn-lib/src/TokenBinding/TokenBindingHandler.php new file mode 100644 index 00000000..8c907fdf --- /dev/null +++ b/3rdparty/web-auth/webauthn-lib/src/TokenBinding/TokenBindingHandler.php @@ -0,0 +1,16 @@ +getStatus() !== TokenBinding::TOKEN_BINDING_STATUS_PRESENT || throw InvalidDataException::create( + $tokenBinding, + 'Token binding not supported.' + ); + } +} diff --git a/3rdparty/web-auth/webauthn-lib/src/TrustPath/CertificateTrustPath.php b/3rdparty/web-auth/webauthn-lib/src/TrustPath/CertificateTrustPath.php new file mode 100644 index 00000000..31d250c2 --- /dev/null +++ b/3rdparty/web-auth/webauthn-lib/src/TrustPath/CertificateTrustPath.php @@ -0,0 +1,70 @@ +certificates; + } + + /** + * @deprecated since 4.8.0. Please use {Webauthn\Denormalizer\WebauthnSerializerFactory} for converting the object. + * @infection-ignore-all + */ + public static function createFromArray(array $data): static + { + array_key_exists('x5c', $data) || throw InvalidTrustPathException::create('The trust path type is invalid'); + $x5c = $data['x5c']; + is_array($x5c) || throw InvalidTrustPathException::create( + 'The trust path type is invalid. The parameter "x5c" shall contain strings.' + ); + + return self::create($x5c); + } + + /** + * @return mixed[] + */ + public function jsonSerialize(): array + { + trigger_deprecation( + 'web-auth/webauthn-bundle', + '4.9.0', + 'The "%s" method is deprecated and will be removed in 5.0. Please use the serializer instead.', + __METHOD__ + ); + return [ + 'type' => self::class, + 'x5c' => $this->certificates, + ]; + } +} diff --git a/3rdparty/web-auth/webauthn-lib/src/TrustPath/EcdaaKeyIdTrustPath.php b/3rdparty/web-auth/webauthn-lib/src/TrustPath/EcdaaKeyIdTrustPath.php new file mode 100644 index 00000000..d0aa7ffa --- /dev/null +++ b/3rdparty/web-auth/webauthn-lib/src/TrustPath/EcdaaKeyIdTrustPath.php @@ -0,0 +1,45 @@ +ecdaaKeyId; + } + + /** + * @return string[] + */ + public function jsonSerialize(): array + { + return [ + 'type' => self::class, + 'ecdaaKeyId' => $this->ecdaaKeyId, + ]; + } + + public static function createFromArray(array $data): static + { + array_key_exists('ecdaaKeyId', $data) || throw InvalidTrustPathException::create( + 'The trust path type is invalid' + ); + + return new self($data['ecdaaKeyId']); + } +} diff --git a/3rdparty/web-auth/webauthn-lib/src/TrustPath/EmptyTrustPath.php b/3rdparty/web-auth/webauthn-lib/src/TrustPath/EmptyTrustPath.php new file mode 100644 index 00000000..e1acb0ab --- /dev/null +++ b/3rdparty/web-auth/webauthn-lib/src/TrustPath/EmptyTrustPath.php @@ -0,0 +1,38 @@ + self::class, + ]; + } + + /** + * @deprecated since 4.8.0. Please use {Webauthn\Denormalizer\WebauthnSerializerFactory} for converting the object. + * @infection-ignore-all + */ + public static function createFromArray(array $data): static + { + return self::create(); + } +} diff --git a/3rdparty/web-auth/webauthn-lib/src/TrustPath/TrustPath.php b/3rdparty/web-auth/webauthn-lib/src/TrustPath/TrustPath.php new file mode 100644 index 00000000..f28097e0 --- /dev/null +++ b/3rdparty/web-auth/webauthn-lib/src/TrustPath/TrustPath.php @@ -0,0 +1,17 @@ + $data + * @deprecated since 4.8.0. Please use {Webauthn\Denormalizer\WebauthnSerializerFactory} for converting the object. + * @infection-ignore-all + */ + public static function createFromArray(array $data): static; +} diff --git a/3rdparty/web-auth/webauthn-lib/src/TrustPath/TrustPathLoader.php b/3rdparty/web-auth/webauthn-lib/src/TrustPath/TrustPathLoader.php new file mode 100644 index 00000000..017f5525 --- /dev/null +++ b/3rdparty/web-auth/webauthn-lib/src/TrustPath/TrustPathLoader.php @@ -0,0 +1,33 @@ + EmptyTrustPath::class, + ] => EmptyTrustPath::create(), + array_key_exists('x5c', $data) && is_array($data['x5c']) => CertificateTrustPath::create($data['x5c']), + array_key_exists('ecdaaKeyId', $data) && is_string($data['ecdaaKeyId']) => new EcdaaKeyIdTrustPath( + $data['ecdaaKeyId'] + ), + default => throw InvalidTrustPathException::create('Unsupported trust path'), + }; + } +} diff --git a/3rdparty/web-auth/webauthn-lib/src/U2FPublicKey.php b/3rdparty/web-auth/webauthn-lib/src/U2FPublicKey.php new file mode 100644 index 00000000..bbd85c2b --- /dev/null +++ b/3rdparty/web-auth/webauthn-lib/src/U2FPublicKey.php @@ -0,0 +1,56 @@ +__toString(); + } +} diff --git a/3rdparty/web-auth/webauthn-lib/src/Util/Base64.php b/3rdparty/web-auth/webauthn-lib/src/Util/Base64.php new file mode 100644 index 00000000..71bcaf36 --- /dev/null +++ b/3rdparty/web-auth/webauthn-lib/src/Util/Base64.php @@ -0,0 +1,26 @@ + https (для работы за прокси)" fi + # Включение приложений по умолчанию (mail, office, talk, circles и др.) + echo "" + echo "Включение приложений (mail, richdocuments, spreed, circles, calendar, contacts, ...)..." + DEFAULT_APPS="mail richdocuments spreed circles calendar contacts deck files_pdfviewer files_reminders firstrunwizard notifications dashboard" + for app in $DEFAULT_APPS; do + if [ -d "$INSTALL_DIR/apps/$app" ]; then + if sudo -u $APACHE_USER php occ app:enable "$app" 2>/dev/null; then + echo " ✓ $app" + else + echo " ⚠ $app (пропущено или уже включено)" + fi + else + echo " − $app (не найдено в apps/)" + fi + done + echo "✓ Приложения обработаны" + # Настройка F7Talk (если приложение установлено) echo "" TALK_CONFIG_FILE="$INSTALL_DIR/config/talk-config.php" diff --git a/lib/private/Log.php b/lib/private/Log.php index 1fe4e142..98775fda 100644 --- a/lib/private/Log.php +++ b/lib/private/Log.php @@ -9,7 +9,7 @@ declare(strict_types=1); namespace OC; use Exception; -use Nextcloud\LogNormalizer\Normalizer; +use F7cloud\LogNormalizer\Normalizer; use OC\AppFramework\Bootstrap\Coordinator; use OC\Log\ExceptionSerializer; use OCP\EventDispatcher\IEventDispatcher;